fix(chat-intent): 隐式攻略/页面生成请求直接路由到 static-page-publish

短句如"苏州攻略页面"此前会被规则/LLM 误判为 product-campaign-page(先追问
促销信息),导致用户体验成"卡住不动"。规则层扩大页面生成意图识别,并新增
coercePageGenerationSkill 在 LLM 判为 product-campaign-page 但明显是内容页
时强制纠正为 static-page-publish,避免不必要的追问。同步更新两个技能文档
的适用边界说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-07 10:55:20 +08:00
parent 7edafefc56
commit ec8d0464a3
6 changed files with 121 additions and 15 deletions
+42 -11
View File
@@ -3,6 +3,8 @@ import {
buildWebNewsSkillPrompt,
extractSelectedChatSkillName,
hasExplicitChatSkillPrompt,
isPageGenerationIntent,
isProductCampaignIntent,
} from './chat-skills.mjs';
import { isDirectChatSessionId } from './direct-chat-service.mjs';
import {
@@ -111,6 +113,26 @@ function buildRealtimeInfoClassification() {
}, { source: 'rule' });
}
export function coercePageGenerationSkill(classification, text, { grantedSkills = [] } = {}) {
if (!classification || classification.route !== CHAT_INTENT_ROUTE.AGENT) return classification;
if (!isPageGenerationIntent(text) || isProductCampaignIntent(text)) return classification;
if (grantedSkills.length > 0 && !grantedSkills.includes('static-page-publish')) return classification;
const shouldUsePublishSkill =
!classification.suggestedSkill || classification.suggestedSkill === 'product-campaign-page';
if (!shouldUsePublishSkill) return classification;
const reasonSuffix = classification.suggestedSkill === 'product-campaign-page'
? '(内容页意图,改用 static-page-publish'
: '(内容页生成意图)';
return {
...classification,
suggestedSkill: 'static-page-publish',
agentBrief: '生成或更新 MindSpace 公开内容页面,并返回可访问链接;信息不足时使用合理默认,不要停在追问。',
reason: `${classification.reason}${reasonSuffix}`,
};
}
export function coerceRealtimeWebSkill(classification, text, { grantedSkills = [] } = {}) {
if (!classification || classification.route !== CHAT_INTENT_ROUTE.AGENT) return classification;
if (!isRealtimeInfoQuestion(text)) return classification;
@@ -276,6 +298,10 @@ function buildRouterSystemPrompt(grantedSkills = []) {
? '若走 agent_orchestration,可在 suggested_skill 中填写最匹配的 skill 名称(须来自上述列表),否则填 null。'
: 'suggested_skill 通常填 null。',
'',
'skill 选择补充:',
'- static-page-publish:攻略/游记/主题内容页、城市介绍、活动介绍、任何「做页面/生成链接」且无商品购买跳转需求。',
'- product-campaign-page:仅当用户明确提供商品链接或要求购买按钮/电商转化时使用,不要用于旅游攻略或纯内容页。',
'',
'只输出 JSON,不要 markdown,不要解释:',
'{"route":"direct_chat|agent_orchestration","confidence":0.0,"reason":"一句话","suggested_skill":null,"agent_brief":"给 Agent 的执行要点,direct_chat 时可为空"}',
].join('\n');
@@ -713,14 +739,15 @@ export function classifyWithRules({
if (
includeIntentPatterns &&
normalized &&
OBVIOUS_AGENT_PATTERNS.some((pattern) => pattern.test(normalized))
(OBVIOUS_AGENT_PATTERNS.some((pattern) => pattern.test(normalized))
|| isPageGenerationIntent(normalized))
) {
return finalizeRouterClassification(normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 0.95,
reason: '明确需要生成或发布页面/文件',
suggested_skill: 'static-page-publish',
agent_brief: '生成或更新 MindSpace 公开页面,并返回可访问链接。',
agent_brief: '生成或更新 MindSpace 公开页面,并返回可访问链接;信息不足时使用合理默认,不要停在追问。',
}, { source: 'rule' }), decisionContext);
}
if (includeIntentPatterns && normalized && isRealtimeInfoQuestion(normalized)) {
@@ -848,12 +875,16 @@ export function createChatIntentRouter(options = {}) {
sessionId,
sessionMessageCount,
};
const finalizeWithRealtimeCoercion = (classification) => {
const finalizeWithCoercion = (classification) => {
if (!classification) return classification;
const base = { ...classification };
delete base.decision;
return finalizeRouterClassification(
coerceRealtimeWebSkill(base, text, { grantedSkills }),
coercePageGenerationSkill(
coerceRealtimeWebSkill(base, text, { grantedSkills }),
text,
{ grantedSkills },
),
decisionContext,
);
};
@@ -867,9 +898,9 @@ export function createChatIntentRouter(options = {}) {
includeIntentPatterns: true,
llmRouterEnabled: Boolean(policy.enabled),
});
if (ruleResult) return finalizeWithRealtimeCoercion(ruleResult);
if (ruleResult) return finalizeWithCoercion(ruleResult);
if (!isEnabled()) {
return finalizeWithRealtimeCoercion(normalizeClassification({
return finalizeWithCoercion(normalizeClassification({
route: policy.fallbackRoute,
confidence: 0.5,
reason: '意图路由未启用,走默认通道',
@@ -905,7 +936,7 @@ export function createChatIntentRouter(options = {}) {
'Chat intent router',
);
} catch (err) {
return finalizeWithRealtimeCoercion(normalizeClassification({
return finalizeWithCoercion(normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: err instanceof Error ? err.message : '意图路由失败,走默认通道',
@@ -913,7 +944,7 @@ export function createChatIntentRouter(options = {}) {
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
}
if (!completion?.ok) {
return finalizeWithRealtimeCoercion(normalizeClassification({
return finalizeWithCoercion(normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: completion?.message ?? '意图路由失败,走默认通道',
@@ -923,7 +954,7 @@ export function createChatIntentRouter(options = {}) {
const parsed = parseRouterJson(completion.reply);
if (!parsed) {
return finalizeWithRealtimeCoercion(normalizeClassification({
return finalizeWithCoercion(normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: '意图路由响应无法解析,走默认通道',
@@ -931,7 +962,7 @@ export function createChatIntentRouter(options = {}) {
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
}
const classification = finalizeWithRealtimeCoercion({
const classification = finalizeWithCoercion({
...normalizeClassification(parsed, { source: 'llm', fallbackRoute: policy.fallbackRoute }),
providerKeyId: completion.providerKeyId ?? policy.modelProviderKeyId ?? null,
model: completion.model ?? policy.model ?? null,
@@ -941,7 +972,7 @@ export function createChatIntentRouter(options = {}) {
classification.route === CHAT_INTENT_ROUTE.DIRECT_CHAT &&
classification.confidence < policy.minConfidence
) {
return finalizeWithRealtimeCoercion(normalizeClassification({
return finalizeWithCoercion(normalizeClassification({
...classification,
route: policy.fallbackRoute,
reason: `${classification.reason}(置信度 ${classification.confidence} 低于阈值,走默认通道)`,
+32
View File
@@ -7,6 +7,7 @@ import {
buildRouterNormalizedDecision,
CHAT_INTENT_ROUTE,
classifyWithRules,
coercePageGenerationSkill,
createChatIntentRouter,
createManagedChatIntentRouter,
isNormalizedRouterDecisionEnabled,
@@ -49,6 +50,37 @@ test('classifyWithRules routes page generation to agent orchestration', () => {
assert.equal(result.suggestedSkill, 'static-page-publish');
});
test('classifyWithRules routes implicit travel guide page requests to static-page-publish', () => {
const result = classifyWithRules({
text: '苏州攻略页面',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '苏州攻略页面' }],
metadata: { displayText: '苏州攻略页面' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'static-page-publish');
assert.match(result.agentBrief, /不要停在追问/);
});
test('coercePageGenerationSkill replaces product-campaign-page for content pages', () => {
const coerced = coercePageGenerationSkill(
{
route: CHAT_INTENT_ROUTE.AGENT,
suggestedSkill: 'product-campaign-page',
reason: '模型路由判定',
agentBrief: '',
confidence: 0.9,
source: 'llm',
},
'苏州攻略页面',
{ grantedSkills: ['static-page-publish', 'product-campaign-page'] },
);
assert.equal(coerced.suggestedSkill, 'static-page-publish');
assert.match(coerced.reason, /static-page-publish/);
});
test('classifyWithRules routes themed article requests to agent orchestration', () => {
const result = classifyWithRules({
text: '帮我写个主题文章',
+21 -4
View File
@@ -17,8 +17,28 @@ const PAGE_GENERATION_INTENT_PATTERNS = [
/(?:生成|做|制作|创建|设计|写|出)(?:一个|一份|个)?(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
/(?:帮我|给我).*(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
/(?:publish|create|generate|make|build|design).*(?:html|web\s?page|landing\s?page|h5)/i,
// 隐式页面需求:主题/攻略 + 页面,无显式动词(如「苏州攻略页面」)
/(?:攻略|指南|游记|手册|简介|介绍|展示).{0,8}(?:页面|网页|H5|h5|html)/u,
/[^\s,。!?]{2,20}(?:攻略|指南).{0,4}(?:页面|网页)/u,
];
const PRODUCT_CAMPAIGN_INTENT_PATTERNS = [
/(?:商品|购买|下单|种草|带货|促销|上架|SKU|店铺|电商)/u,
/(?:https?:\/\/|taobao|tmall|jd\.com|商品链接|购买链接|购买按钮)/iu,
];
export function isPageGenerationIntent(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
return PAGE_GENERATION_INTENT_PATTERNS.some((pattern) => pattern.test(normalized));
}
export function isProductCampaignIntent(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
return PRODUCT_CAMPAIGN_INTENT_PATTERNS.some((pattern) => pattern.test(normalized));
}
export const CHAT_SKILL_DEFINITIONS = [
{
id: 'web-search',
@@ -149,10 +169,7 @@ export function filterChatSkills(options, ctx) {
export function buildAutoChatSkillPrefix(text, grantedSkills = []) {
const trimmed = String(text ?? '').trim();
if (!trimmed) return '';
if (
grantedSkills.includes(PUBLISH_SKILL_NAME) &&
PAGE_GENERATION_INTENT_PATTERNS.some((pattern) => pattern.test(trimmed))
) {
if (grantedSkills.includes(PUBLISH_SKILL_NAME) && isPageGenerationIntent(trimmed)) {
return buildChatSkillPrompt('generate-page', PUBLISH_SKILL_NAME);
}
if (!grantedSkills.includes('web')) return '';
+13
View File
@@ -5,6 +5,7 @@ import {
buildChatSkillPrompt,
CHAT_SKILL_DEFINITIONS,
filterChatSkills,
isPageGenerationIntent,
} from './chat-skills.mjs';
test('filterChatSkills shows summarize and analyze without granted skills', () => {
@@ -79,3 +80,15 @@ test('buildAutoChatSkillPrefix enables publish for page generation requests', ()
assert.match(prefix, /禁止只给本地路径/);
assert.equal(buildAutoChatSkillPrefix('帮我做一个 Hello 糖的 H5 页面', []), '');
});
test('isPageGenerationIntent matches implicit travel guide page requests', () => {
assert.equal(isPageGenerationIntent('苏州攻略页面'), true);
assert.equal(isPageGenerationIntent('帮我看看苏州攻略,1日攻略,做一个页面'), true);
assert.equal(isPageGenerationIntent('你好'), false);
});
test('buildAutoChatSkillPrefix enables publish for implicit page requests', () => {
const prefix = buildAutoChatSkillPrefix('苏州攻略页面', ['static-page-publish']);
assert.match(prefix, /static-page-publish/);
assert.match(prefix, /禁止询问是否还要发布/);
});
+5
View File
@@ -14,6 +14,11 @@ description: 商品宣传 / 活动页技能:基于商品链接、主题、图
- 用户上传商品图、Logo、模特图、详情图,希望生成页面文案和视觉结构
- 用户要做促销、上新、种草、达人推荐、节日活动或私域转化页面
## 何时不用
- 旅游攻略、城市介绍、游记、主题内容页 → 使用 `static-page-publish`,直接生成 HTML 并返回链接
- 用户未提供商品链接、也不要求购买跳转时,不要套用本技能
## 必问信息
如果用户没有一次性给全,优先只问这 3 个问题:
+8
View File
@@ -123,6 +123,14 @@ npm run check:mindspace-cover
`data-mindspace-page-tag` 的区域为平台固定信息:用户在编辑模式中不可见、不可改;预览与发布后正常显示。**禁止**把该行 CSS 透明度设过低(如 `opacity: 0.25`),否则页内看不见品牌。
## 中文字体(推荐)
中文页面正文与标题优先使用系统无衬线字体栈,**避免** Georgia、Times New Roman 等西文衬线体(中文显示发虚、不协调):
```css
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
```
## 按需附带文件下载(Word / PDF)
- 只有用户明确要求 Word / PDF 下载时才生成二进制文件