feat(strategy): 表单可填本阶段策略目标类型 stage_goal_type

接入 StrategyDraftRequestSerializer、规则底稿表与 1.3 要点、strategy_decisions_substantive 与 STRATEGY_SYSTEM 决策边界;前端策略生成页增加输入框。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-23 10:49:56 +08:00
parent 6c37a2def2
commit 6faef2180e
7 changed files with 54 additions and 6 deletions

View File

@ -36,6 +36,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
def _strategy_decisions_empty() -> dict[str, Any]: def _strategy_decisions_empty() -> dict[str, Any]:
return { return {
"product_role": "", "product_role": "",
"stage_goal_type": "",
"time_horizon": "", "time_horizon": "",
"success_criteria": "", "success_criteria": "",
"non_goals": "", "non_goals": "",

View File

@ -15,6 +15,7 @@ from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window
# 与策略生成表单 POST 字段一致:任一则视为业务已提供「实质决策」,否则由模型基于数据推断草案。 # 与策略生成表单 POST 字段一致:任一则视为业务已提供「实质决策」,否则由模型基于数据推断草案。
_STRATEGY_DECISION_SUBSTANTIVE_KEYS: tuple[str, ...] = ( _STRATEGY_DECISION_SUBSTANTIVE_KEYS: tuple[str, ...] = (
"product_role", "product_role",
"stage_goal_type",
"battlefield_one_line", "battlefield_one_line",
"audience_segment", "audience_segment",
"time_horizon", "time_horizon",
@ -142,7 +143,7 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要**
- 仍须遵守全局禁止编造:数字、品牌、店铺、用户原话、活动规则仅可来自输入依据;无依据处用假设语气。 - 仍须遵守全局禁止编造:数字、品牌、店铺、用户原话、活动规则仅可来自输入依据;无依据处用假设语气。
**决策边界(硬性)**: **决策边界(硬性)**:
- **当 `strategy_decisions_substantive` 为 true 时**:业务已在 `strategy_decisions` 中填写的项(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。 - **当 `strategy_decisions_substantive` 为 true 时**:业务已在 `strategy_decisions` 中填写的项(角色、**本阶段策略目标类型**(`stage_goal_type`)、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。**若 `stage_goal_type` 非空**,「策略范围与前提」表中「本阶段策略目标类型」列须**直接采用该表述**(可略作语序润色),**不得**改判为另一类阶段目标。
- **当 `strategy_decisions_substantive` 为 true** 而部分表单项仍为空或占位:结合监测摘要与节选**补全为可执行表述**,与数据方向一致。 - **当 `strategy_decisions_substantive` 为 true** 而部分表单项仍为空或占位:结合监测摘要与节选**补全为可执行表述**,与数据方向一致。
- **当 `strategy_decisions_substantive` 为 false 时**:适用上文「业务决策未填写时的成稿义务」,**禁止**以「请先填表」类表述搪塞全篇。 - **当 `strategy_decisions_substantive` 为 false 时**:适用上文「业务决策未填写时的成稿义务」,**禁止**以「请先填表」类表述搪塞全篇。
- **成稿阶段避免**:反复「请业务决策」;不确定时在 §2.1 用「类目/细类」+「假设:」「待业务确认:」**写清**,**禁止**只写泛化一句。 - **成稿阶段避免**:反复「请业务决策」;不确定时在 §2.1 用「类目/细类」+「假设:」「待业务确认:」**写清**,**禁止**只写泛化一句。

View File

@ -238,11 +238,13 @@ def build_strategy_draft_markdown(
if not for_llm_input if not for_llm_input
else "*(待确认)*" else "*(待确认)*"
) )
_table_goal_type = ( sgt = _esc(d.get("stage_goal_type") or "").strip()
"*(成稿从规划文档「目标类型选项」择一或组合,并与下列成功标准一致)*" _goal_type_placeholder = (
if not for_llm_input "待确认:可填如「A 心智占位 / B 转化爬坡 …」,见规划文档「目标类型选项」"
else "*(待确认)*" if for_llm_input
else "成稿从规划文档「目标类型选项」择一或组合,并与下列成功标准一致"
) )
_table_goal_type = _scope_cell(sgt, _goal_type_placeholder)
_summary_user_side = ( _summary_user_side = (
"- **用户侧**:*(一两句结论即可:讨论焦点与负向主题;**按细类分句**归纳,**勿**混成「全站用户」一句;**勿**展开与报告重复的细类统计、词频。)*" "- **用户侧**:*(一两句结论即可:讨论焦点与负向主题;**按细类分句**归纳,**勿**混成「全站用户」一句;**勿**展开与报告重复的细类统计、词频。)*"
if not for_llm_input if not for_llm_input
@ -357,6 +359,11 @@ def build_strategy_draft_markdown(
] ]
), ),
_goal_bullet("本品角色", str(d.get("product_role") or ""), "新品 / 追赶 / 防守 / 拓品类 …"), _goal_bullet("本品角色", str(d.get("product_role") or ""), "新品 / 追赶 / 防守 / 拓品类 …"),
_goal_bullet(
"本阶段策略目标类型",
str(d.get("stage_goal_type") or ""),
"见规划文档「目标类型选项」或自填(如心智占位、转化爬坡)",
),
_goal_bullet( _goal_bullet(
"目标客群", "目标客群",
str(d.get("audience_segment") or ""), str(d.get("audience_segment") or ""),

View File

@ -410,6 +410,13 @@ class StrategyDraftRequestSerializer(serializers.Serializer):
product_role = serializers.CharField( product_role = serializers.CharField(
required=False, allow_blank=True, default="", max_length=500, trim_whitespace=False required=False, allow_blank=True, default="", max_length=500, trim_whitespace=False
) )
stage_goal_type = serializers.CharField(
required=False,
allow_blank=True,
default="",
max_length=1000,
trim_whitespace=False,
)
time_horizon = serializers.CharField( time_horizon = serializers.CharField(
required=False, allow_blank=True, default="", max_length=200, trim_whitespace=False required=False, allow_blank=True, default="", max_length=200, trim_whitespace=False
) )

View File

@ -20,6 +20,9 @@ class StrategyDraftTests(SimpleTestCase):
self.assertTrue( self.assertTrue(
strategy_decisions_substantive({"product_role": "新品"}) strategy_decisions_substantive({"product_role": "新品"})
) )
self.assertTrue(
strategy_decisions_substantive({"stage_goal_type": "B 转化爬坡"})
)
def test_for_llm_input_omits_dev_traces(self) -> None: def test_for_llm_input_omits_dev_traces(self) -> None:
brief = { brief = {
@ -78,6 +81,24 @@ class StrategyDraftTests(SimpleTestCase):
self.assertIn("## 七、品牌四线", md) self.assertIn("## 七、品牌四线", md)
self.assertIn("市场策略制定草稿", md) self.assertIn("市场策略制定草稿", md)
def test_stage_goal_type_in_scope_table(self) -> None:
brief = {
"schema_version": 1,
"keyword": "K",
"batch_label": "b1",
"scope": {"merged_sku_count": 2},
}
md = build_strategy_draft_markdown(
job_id=1,
keyword="K",
brief=brief,
strategy_decisions={"stage_goal_type": "A 心智占位"},
generated_at_iso="2026-04-09T12:00:00",
for_llm_input=True,
)
self.assertIn("| **本阶段策略目标类型** | A 心智占位 |", md)
self.assertIn("**本阶段策略目标类型**:A 心智占位", md)
def test_strategy_decisions_merge(self) -> None: def test_strategy_decisions_merge(self) -> None:
brief = {"schema_version": 1, "keyword": "K", "batch_label": "b"} brief = {"schema_version": 1, "keyword": "K", "batch_label": "b"}
decisions = { decisions = {

View File

@ -141,6 +141,7 @@ class JobStrategyDraftView(APIView):
notes = (vd.get("business_notes") or "").strip() notes = (vd.get("business_notes") or "").strip()
strategy_decisions = { strategy_decisions = {
"product_role": vd.get("product_role") or "", "product_role": vd.get("product_role") or "",
"stage_goal_type": vd.get("stage_goal_type") or "",
"time_horizon": vd.get("time_horizon") or "", "time_horizon": vd.get("time_horizon") or "",
"success_criteria": vd.get("success_criteria") or "", "success_criteria": vd.get("success_criteria") or "",
"non_goals": vd.get("non_goals") or "", "non_goals": vd.get("non_goals") or "",

View File

@ -49,6 +49,7 @@ const briefMatrixErr = ref('')
const decisions = reactive({ const decisions = reactive({
product_role: '', product_role: '',
stage_goal_type: '',
time_horizon: '', time_horizon: '',
success_criteria: '', success_criteria: '',
non_goals: '', non_goals: '',
@ -96,6 +97,7 @@ function buildPayload() {
generator, generator,
business_notes: businessNotes.value, business_notes: businessNotes.value,
product_role: decisions.product_role, product_role: decisions.product_role,
stage_goal_type: decisions.stage_goal_type,
time_horizon: decisions.time_horizon, time_horizon: decisions.time_horizon,
success_criteria: decisions.success_criteria, success_criteria: decisions.success_criteria,
non_goals: decisions.non_goals, non_goals: decisions.non_goals,
@ -315,7 +317,7 @@ watch(
<fieldset class="fieldset"> <fieldset class="fieldset">
<legend>策略范围与前提</legend> <legend>策略范围与前提</legend>
<p class="fieldset-hint"> <p class="fieldset-hint">
界定本次策略的任务边界与阶段目标。监测词、批次由任务自动带出;<strong>主推类目</strong>以顶部「矩阵细类」为准。角色、战场、客群会进入策略稿开篇,并与下一栏「主要对标」衔接。 界定本次策略的任务边界与阶段目标。监测词、批次由任务自动带出;<strong>主推类目</strong>以顶部「矩阵细类」为准。角色、<strong>本阶段策略目标类型</strong>、战场、客群会进入策略稿开篇;目标类型填好后大模型不得擅自改判。并与下一栏「主要对标」衔接。
</p> </p>
<label class="fld"> <label class="fld">
<span>本品角色(策略服务对象)</span> <span>本品角色(策略服务对象)</span>
@ -325,6 +327,14 @@ watch(
placeholder="如:追赶型 / 新品 / 防守 / 拓品类" placeholder="如:追赶型 / 新品 / 防守 / 拓品类"
/> />
</label> </label>
<label class="fld fld-block">
<span>本阶段策略目标类型</span>
<textarea
v-model="decisions.stage_goal_type"
rows="2"
placeholder="由业务选定,如:A 心智占位 / B 转化与复购爬坡 / C 份额防守……详见仓库 docs/planning/策略生成-框架确定.md;填后大模型须落实,不得擅自改判"
/>
</label>
<label class="fld fld-block"> <label class="fld fld-block">
<span>一句话战场</span> <span>一句话战场</span>
<textarea <textarea