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]:
return {
"product_role": "",
"stage_goal_type": "",
"time_horizon": "",
"success_criteria": "",
"non_goals": "",

View File

@ -15,6 +15,7 @@ from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window
# 与策略生成表单 POST 字段一致:任一则视为业务已提供「实质决策」,否则由模型基于数据推断草案。
_STRATEGY_DECISION_SUBSTANTIVE_KEYS: tuple[str, ...] = (
"product_role",
"stage_goal_type",
"battlefield_one_line",
"audience_segment",
"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` false **适用上文业务决策未填写时的成稿义务**禁止**请先填表类表述搪塞全篇
- **成稿阶段避免**反复请业务决策不确定时在 §2.1 类目/细类+假设待业务确认**写清****禁止**只写泛化一句

View File

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

View File

@ -410,6 +410,13 @@ class StrategyDraftRequestSerializer(serializers.Serializer):
product_role = serializers.CharField(
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(
required=False, allow_blank=True, default="", max_length=200, trim_whitespace=False
)

View File

@ -20,6 +20,9 @@ class StrategyDraftTests(SimpleTestCase):
self.assertTrue(
strategy_decisions_substantive({"product_role": "新品"})
)
self.assertTrue(
strategy_decisions_substantive({"stage_goal_type": "B 转化爬坡"})
)
def test_for_llm_input_omits_dev_traces(self) -> None:
brief = {
@ -78,6 +81,24 @@ class StrategyDraftTests(SimpleTestCase):
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:
brief = {"schema_version": 1, "keyword": "K", "batch_label": "b"}
decisions = {

View File

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

View File

@ -49,6 +49,7 @@ const briefMatrixErr = ref('')
const decisions = reactive({
product_role: '',
stage_goal_type: '',
time_horizon: '',
success_criteria: '',
non_goals: '',
@ -96,6 +97,7 @@ function buildPayload() {
generator,
business_notes: businessNotes.value,
product_role: decisions.product_role,
stage_goal_type: decisions.stage_goal_type,
time_horizon: decisions.time_horizon,
success_criteria: decisions.success_criteria,
non_goals: decisions.non_goals,
@ -315,7 +317,7 @@ watch(
<fieldset class="fieldset">
<legend>策略范围与前提</legend>
<p class="fieldset-hint">
界定本次策略的任务边界与阶段目标监测词批次由任务自动带出<strong>主推类目</strong>以顶部矩阵细类为准角色战场客群会进入策略稿开篇并与下一栏主要对标衔接
界定本次策略的任务边界与阶段目标监测词批次由任务自动带出<strong>主推类目</strong>以顶部矩阵细类为准角色<strong>本阶段策略目标类型</strong>战场客群会进入策略稿开篇目标类型填好后大模型不得擅自改判并与下一栏主要对标衔接
</p>
<label class="fld">
<span>本品角色策略服务对象</span>
@ -325,6 +327,14 @@ watch(
placeholder="如:追赶型 / 新品 / 防守 / 拓品类"
/>
</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">
<span>一句话战场</span>
<textarea