diff --git a/backend/pipeline/llm/generate_strategy.py b/backend/pipeline/llm/generate_strategy.py index a751715..fa7dc92 100644 --- a/backend/pipeline/llm/generate_strategy.py +++ b/backend/pipeline/llm/generate_strategy.py @@ -17,7 +17,7 @@ STRATEGY_SYSTEM = """你是市场策略顾问,根据**结构化监测摘要** **输入**:`rules_draft_markdown`(规则骨架,与同任务数据一致)、`structured_brief`(摘要子集)、`strategy_decisions`、`business_notes`;可选 `report_strategy_excerpt`(与同任务宿主报告**第九章**「策略与机会」正文同源)。 **决策边界(硬性)**: -- **业务已在 `strategy_decisions` 中填写的项**(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。 +- **业务已在 `strategy_decisions` 中填写的项**(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。 - **表单中为空或占位(如 *待填*、*骨架占位*)的项**:由你结合 `structured_brief`、`report_strategy_excerpt`(若非空)与数据摘录**补全为可执行表述**;补全须与数据方向一致,**不得**编造输入中不存在的销量、GMV、未给出的价格或占比。 - **成稿阶段禁止**:再写一轮「请业务决策」「待确认后再定」「假设:待验证」等**二次决策话术**;不确定性用一句带过即可(如「需下周用原评论抽样核对」),勿重复堆砌。 @@ -101,7 +101,7 @@ def generate_strategy_draft_markdown_llm( if report_uses_chapter8_text_mining_probe(report_config): payload["structured_brief_omission_note"] = ( "已启用第八章文本挖掘:structured_brief 已省略关注词/场景子串计数字段,避免与报告正文口径冲突。" - "请依报告 §8 与摘要其他字段,将底稿第五节写成具体执行要点(需求焦点、场景、传播),勿逐条编造子串命中列表。" + "请依报告 §8 与摘要其他字段,将底稿「用户与评论侧」一节写成具体执行要点(需求焦点、场景、传播),勿逐条编造子串命中列表。" ) raw = json.dumps(payload, ensure_ascii=False) if len(raw) > 500_000: diff --git a/backend/pipeline/reporting/strategy_draft.py b/backend/pipeline/reporting/strategy_draft.py index 110342f..9bda1e3 100644 --- a/backend/pipeline/reporting/strategy_draft.py +++ b/backend/pipeline/reporting/strategy_draft.py @@ -102,7 +102,7 @@ def report_uses_chapter8_text_mining_probe(report_config: dict[str, Any] | None) """ 与任务 ``report_config`` 中 ``chapter8_text_mining_probe`` 一致;未显式设置时默认 ``True`` (与 ``jd.runner.get_default_report_config`` 一致)。 - 为 ``True`` 时,策略稿第五节不再逐条列举关注词/场景子串命中,以免与当前报告正文口径冲突。 + 为 ``True`` 时,策略稿「用户与评论侧」一节不再逐条列举关注词/场景子串命中,以免与当前报告正文口径冲突。 """ if not isinstance(report_config, dict): return True @@ -170,12 +170,30 @@ def build_strategy_draft_markdown( ] ) + lines.extend( + [ + "## 二、营销策略与总体方向", + "", + _goal_bullet( + "营销策略", + str(d.get("marketing_strategy") or ""), + "传播、活动、投放、内容主线等(可选)", + ), + _goal_bullet( + "总体策略", + str(d.get("general_strategy") or ""), + "增长/品类/经营层面的总原则,可与 4P 支柱呼应(可选)", + ), + "", + ] + ) + scope = brief.get("scope") or {} merged_n = scope.get("merged_sku_count") comm_n = scope.get("comment_flat_rows") lines.extend( [ - "## 二、战场与样本", + "## 三、战场与样本", "", f"- **监测关键词 / 货架语境**:{kw}", f"- **批次**:{_esc(brief.get('batch_label')) or '—'}", @@ -197,7 +215,7 @@ def build_strategy_draft_markdown( conc = brief.get("concentration") or {} shops = conc.get("shops_from_list") or {} dbrand = conc.get("detail_brand_among_merged") or {} - lines.extend(["## 三、竞争结构(摘录)", ""]) + lines.extend(["## 四、竞争结构(摘录)", ""]) n_shop = _cr_narrative( "列表侧店铺集中度", concentration_first_share(shops), @@ -244,7 +262,7 @@ def build_strategy_draft_markdown( lines.append("") pst = brief.get("price_stats") or {} - lines.extend(["## 四、价格带与定位(业务已选)", ""]) + lines.extend(["## 五、价格带与定位(业务已选)", ""]) if pst.get("n"): src = _esc(brief.get("price_stats_source")) or "—" lines.extend( @@ -278,7 +296,7 @@ def build_strategy_draft_markdown( ckw = brief.get("comment_focus_keywords") or [] usc = brief.get("usage_scenarios") or [] - lines.extend(["## 五、用户与评论侧", ""]) + lines.extend(["## 六、用户与评论侧", ""]) if use_ch8_probe: lines.extend( [ @@ -317,7 +335,7 @@ def build_strategy_draft_markdown( hints = brief.get("strategy_hints") or [] lines.extend( [ - "## 六、机会与策略支柱", + "## 七、机会与策略支柱", "", "### 摘要提示(`strategy_hints`)", "", @@ -357,7 +375,7 @@ def build_strategy_draft_markdown( ) lines.extend( [ - "## 七、风险核对项", + "## 八、风险核对项", "", _risk_line(rk, rk_kw), _risk_line(rp, "价格带是否含大促/异常挂价?(需核对清洗规则)"), @@ -369,7 +387,7 @@ def build_strategy_draft_markdown( notes = _esc(business_notes) lines.extend( [ - "## 八、业务约束与备注", + "## 九、业务约束与备注", "", (notes if notes else "*(未填写业务备注。)*"), "", @@ -378,10 +396,10 @@ def build_strategy_draft_markdown( lines.extend( [ - "## 九、下一步(可执行)", + "## 十、下一步(可执行)", "", - "- [ ] 对齐 **§一** 目标与 **§八** 约束,锁定 1~2 条主命题。", - "- [ ] 为 **§六** 支柱各补 **1 条数据证据** + **12 周内可交付动作**(负责人 + 时间)。", + "- [ ] 对齐 **§一** 目标与 **§九** 约束,锁定 1~2 条主命题。", + "- [ ] 为 **§七** 支柱各补 **1 条数据证据** + **12 周内可交付动作**(负责人 + 时间)。", "", "---", "", diff --git a/backend/pipeline/serializers.py b/backend/pipeline/serializers.py index 84edcc0..ab5c64e 100644 --- a/backend/pipeline/serializers.py +++ b/backend/pipeline/serializers.py @@ -447,6 +447,12 @@ class StrategyDraftRequestSerializer(serializers.Serializer): resource_notes = serializers.CharField( required=False, allow_blank=True, default="", max_length=1000, trim_whitespace=False ) + marketing_strategy = serializers.CharField( + required=False, allow_blank=True, default="", max_length=2000, trim_whitespace=False + ) + general_strategy = serializers.CharField( + required=False, allow_blank=True, default="", max_length=2000, trim_whitespace=False + ) ack_risk_keywords = serializers.BooleanField(required=False, default=False) ack_risk_price = serializers.BooleanField(required=False, default=False) ack_risk_concentration = serializers.BooleanField(required=False, default=False) diff --git a/backend/pipeline/tests/test_strategy_draft.py b/backend/pipeline/tests/test_strategy_draft.py index e755d3a..774a6aa 100644 --- a/backend/pipeline/tests/test_strategy_draft.py +++ b/backend/pipeline/tests/test_strategy_draft.py @@ -27,6 +27,7 @@ class StrategyDraftTests(SimpleTestCase): self.assertIn("假设A", md) self.assertIn("重点:华东", md) self.assertIn("目标与边界", md) + self.assertIn("营销策略与总体方向", md) self.assertIn("市场策略制定草稿", md) def test_strategy_decisions_merge(self) -> None: @@ -35,6 +36,8 @@ class StrategyDraftTests(SimpleTestCase): "product_role": "追赶型", "positioning_choice": "mid", "competitive_stance": "flank", + "marketing_strategy": "内容种草+搜索承接", + "general_strategy": "先腰后顶", "pillar_product": "做低糖配方", "ack_risk_keywords": True, "ack_risk_price": False, @@ -48,6 +51,8 @@ class StrategyDraftTests(SimpleTestCase): report_config={"chapter8_text_mining_probe": False}, ) self.assertIn("**本品角色**:追赶型", md) + self.assertIn("**营销策略**:内容种草+搜索承接", md) + self.assertIn("**总体策略**:先腰后顶", md) self.assertIn("- [x] **卡腰**", md) self.assertIn("- [ ] **贴顶**", md) self.assertIn("侧翼切入", md) diff --git a/backend/pipeline/views/job_report_views.py b/backend/pipeline/views/job_report_views.py index 3d62839..dd5ca44 100644 --- a/backend/pipeline/views/job_report_views.py +++ b/backend/pipeline/views/job_report_views.py @@ -136,6 +136,8 @@ class JobStrategyDraftView(APIView): "audience_segment": vd.get("audience_segment") or "", "competitor_reference": vd.get("competitor_reference") or "", "resource_notes": vd.get("resource_notes") or "", + "marketing_strategy": vd.get("marketing_strategy") or "", + "general_strategy": vd.get("general_strategy") or "", "ack_risk_keywords": bool(vd.get("ack_risk_keywords")), "ack_risk_price": bool(vd.get("ack_risk_price")), "ack_risk_concentration": bool(vd.get("ack_risk_concentration")), diff --git a/docs/openapi/pipeline-jobs.openapi.yaml b/docs/openapi/pipeline-jobs.openapi.yaml index 9988727..1f1d885 100644 --- a/docs/openapi/pipeline-jobs.openapi.yaml +++ b/docs/openapi/pipeline-jobs.openapi.yaml @@ -561,6 +561,14 @@ components: type: string maxLength: 1000 description: 资源与预算量级备注(可选) + marketing_strategy: + type: string + maxLength: 2000 + description: 营销策略(传播、活动、投放、内容等,可选) + general_strategy: + type: string + maxLength: 2000 + description: 总体策略(增长/品类/经营总原则,可选) ack_risk_keywords: type: boolean default: false diff --git a/frontend/src/views/jd/JdStrategyBuildView.vue b/frontend/src/views/jd/JdStrategyBuildView.vue index 8ab82cc..267e63d 100644 --- a/frontend/src/views/jd/JdStrategyBuildView.vue +++ b/frontend/src/views/jd/JdStrategyBuildView.vue @@ -51,6 +51,8 @@ const decisions = reactive({ audience_segment: '', competitor_reference: '', resource_notes: '', + marketing_strategy: '', + general_strategy: '', ack_risk_keywords: false, ack_risk_price: false, ack_risk_concentration: false, @@ -95,6 +97,8 @@ function buildPayload() { audience_segment: decisions.audience_segment, competitor_reference: decisions.competitor_reference, resource_notes: decisions.resource_notes, + marketing_strategy: decisions.marketing_strategy, + general_strategy: decisions.general_strategy, ack_risk_keywords: decisions.ack_risk_keywords, ack_risk_price: decisions.ack_risk_price, ack_risk_concentration: decisions.ack_risk_concentration, @@ -294,6 +298,26 @@ watch( +
+ 五、营销策略与总体策略 +