fix(web): prefer named deliverable over negated 需求

Explicit aliases (including 系统设计输入) must win before keyword fallback so UI-11 does not route to intake.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-13 18:42:44 +00:00
co-authored by Cursor
parent 77f5db1e90
commit 56e052f3cb
2 changed files with 40 additions and 6 deletions
+20
View File
@@ -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);
});
+20 -6
View File
@@ -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);