From 56e052f3cb7a4b8b0e82c834e981ee79baeb6b5e Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 13 Sep 2026 18:42:44 +0000 Subject: [PATCH] =?UTF-8?q?fix(web):=20prefer=20named=20deliverable=20over?= =?UTF-8?q?=20negated=20=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explicit aliases (including 系统设计输入) must win before keyword fallback so UI-11 does not route to intake. Co-authored-by: Cursor --- web/src/engine/templateMatch.test.ts | 20 ++++++++++++++++++++ web/src/engine/templateMatch.ts | 26 ++++++++++++++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/web/src/engine/templateMatch.test.ts b/web/src/engine/templateMatch.test.ts index 63166daa..7c396860 100644 --- a/web/src/engine/templateMatch.test.ts +++ b/web/src/engine/templateMatch.test.ts @@ -203,3 +203,23 @@ test('no active doc does not invent a pin', () => { assert.equal(resolved?.docId, undefined); assert.equal(resolved?.pinnedToActiveDoc, undefined); }); + +const UI11_SECOND_DOC_PROMPT = + '请另行生成一份《系统设计输入》受控文档(第二份),不要覆盖上一份需求文档,并给出可打开的交付物。'; + +test('UI-11 named 系统设计输入 beats negated 需求 background', () => { + assert.equal(resolveTaskKeyFromMessage(UI11_SECOND_DOC_PROMPT), 'prelim_design'); + assert.equal(resolveTaskTemplateForMessage(UI11_SECOND_DOC_PROMPT, bothPool, activeIntake)?.taskKey, 'prelim_design'); +}); + +test('mixed generate-design + do-not-cover-requirements stays prelim_design', () => { + const text = '请生成《系统设计输入》,不要覆盖上一份需求文档'; + assert.equal(resolveTaskKeyFromMessage(text), 'prelim_design'); +}); + +test('current-doc pin still wins when no other deliverable is named', () => { + const text = '请改写当前受控文档中的风险管理章节,不要覆盖上一份需求文档'; + const resolved = resolveTaskTemplateForMessage(text, bothPool, activeIntake); + assert.equal(resolved?.taskKey, 'intake'); + assert.equal(resolved?.pinnedToActiveDoc, true); +}); diff --git a/web/src/engine/templateMatch.ts b/web/src/engine/templateMatch.ts index 3b21c60d..c73b9d5c 100644 --- a/web/src/engine/templateMatch.ts +++ b/web/src/engine/templateMatch.ts @@ -12,7 +12,7 @@ const NAMED_DELIVERABLE_ALIASES: { taskKey: string; patterns: RegExp[] }[] = [ { taskKey: 'research', patterns: [/调研报告/, /竞品调研/] }, { taskKey: 'feasibility_reg', patterns: [/注册路径可行性/, /法规可行性评估/, /注册可行性评估/] }, { taskKey: 'feasibility_dev', patterns: [/设计开发可行性/, /研发可行性评估/] }, - { taskKey: 'prelim_design', patterns: [/初步设计说明书/, /系统设计规格/] }, + { taskKey: 'prelim_design', patterns: [/系统设计输入/, /初步设计说明书/, /系统设计规格/] }, { taskKey: 'cdo_quote', patterns: [/CDO报价/, /报价核算表/] }, ]; @@ -66,11 +66,7 @@ function explicitNamedDeliverableTaskKey( for (const d of titled) { if (text.includes(d.title)) return d.taskKey; } - for (const alias of NAMED_DELIVERABLE_ALIASES) { - if (alias.taskKey === excludeTaskKey) continue; - if (alias.patterns.some((p) => p.test(text))) return alias.taskKey; - } - return undefined; + return namedAliasTaskKey(text, excludeTaskKey); } /** @@ -118,12 +114,24 @@ function pickRequirementsTask(text: string, pool: DeliverableItem[]): Deliverabl return undefined; } +function namedAliasTaskKey(text: string, excludeTaskKey?: string): string | undefined { + for (const alias of NAMED_DELIVERABLE_ALIASES) { + if (alias.taskKey === excludeTaskKey) continue; + if (alias.patterns.some((p) => p.test(text))) return alias.taskKey; + } + return undefined; +} + /** 根据用户输入解析应触发的文档任务键(不依赖交付物池顺序)。 */ export function resolveTaskKeyFromMessage(text: string): string | undefined { const normalized = text.trim(); // 模板名只是操作对象,不代表用户授权写入。只读动词优先;只有明确写动作才走任务 API。 if (!hasDocumentWriteIntent(normalized)) return undefined; + // 明确点名的交付物优先于否定背景/旧文档里的「需求」等关键词。 + const named = namedAliasTaskKey(normalized); + if (named) return named; + if (normalized.includes('需求') || normalized.includes('摸底') || normalized.includes('用户') || normalized.includes('首轮')) { return resolveRequirementsTaskKey(normalized); } @@ -185,6 +193,12 @@ export function resolveTaskTemplateForMessage( } } + const named = explicitNamedDeliverableTaskKey(text, deliverables); + if (named) { + const template = matchTemplateDeliverableByKey(named, deliverables, text); + return { taskKey: named, template }; + } + const taskKey = resolveTaskKeyFromMessage(text); if (!taskKey) return undefined; const template = matchTemplateDeliverable(text, deliverables);