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:
+42
-11
@@ -3,6 +3,8 @@ import {
|
|||||||
buildWebNewsSkillPrompt,
|
buildWebNewsSkillPrompt,
|
||||||
extractSelectedChatSkillName,
|
extractSelectedChatSkillName,
|
||||||
hasExplicitChatSkillPrompt,
|
hasExplicitChatSkillPrompt,
|
||||||
|
isPageGenerationIntent,
|
||||||
|
isProductCampaignIntent,
|
||||||
} from './chat-skills.mjs';
|
} from './chat-skills.mjs';
|
||||||
import { isDirectChatSessionId } from './direct-chat-service.mjs';
|
import { isDirectChatSessionId } from './direct-chat-service.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -111,6 +113,26 @@ function buildRealtimeInfoClassification() {
|
|||||||
}, { source: 'rule' });
|
}, { 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 = [] } = {}) {
|
export function coerceRealtimeWebSkill(classification, text, { grantedSkills = [] } = {}) {
|
||||||
if (!classification || classification.route !== CHAT_INTENT_ROUTE.AGENT) return classification;
|
if (!classification || classification.route !== CHAT_INTENT_ROUTE.AGENT) return classification;
|
||||||
if (!isRealtimeInfoQuestion(text)) return classification;
|
if (!isRealtimeInfoQuestion(text)) return classification;
|
||||||
@@ -276,6 +298,10 @@ function buildRouterSystemPrompt(grantedSkills = []) {
|
|||||||
? '若走 agent_orchestration,可在 suggested_skill 中填写最匹配的 skill 名称(须来自上述列表),否则填 null。'
|
? '若走 agent_orchestration,可在 suggested_skill 中填写最匹配的 skill 名称(须来自上述列表),否则填 null。'
|
||||||
: 'suggested_skill 通常填 null。',
|
: 'suggested_skill 通常填 null。',
|
||||||
'',
|
'',
|
||||||
|
'skill 选择补充:',
|
||||||
|
'- static-page-publish:攻略/游记/主题内容页、城市介绍、活动介绍、任何「做页面/生成链接」且无商品购买跳转需求。',
|
||||||
|
'- product-campaign-page:仅当用户明确提供商品链接或要求购买按钮/电商转化时使用,不要用于旅游攻略或纯内容页。',
|
||||||
|
'',
|
||||||
'只输出 JSON,不要 markdown,不要解释:',
|
'只输出 JSON,不要 markdown,不要解释:',
|
||||||
'{"route":"direct_chat|agent_orchestration","confidence":0.0,"reason":"一句话","suggested_skill":null,"agent_brief":"给 Agent 的执行要点,direct_chat 时可为空"}',
|
'{"route":"direct_chat|agent_orchestration","confidence":0.0,"reason":"一句话","suggested_skill":null,"agent_brief":"给 Agent 的执行要点,direct_chat 时可为空"}',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
@@ -713,14 +739,15 @@ export function classifyWithRules({
|
|||||||
if (
|
if (
|
||||||
includeIntentPatterns &&
|
includeIntentPatterns &&
|
||||||
normalized &&
|
normalized &&
|
||||||
OBVIOUS_AGENT_PATTERNS.some((pattern) => pattern.test(normalized))
|
(OBVIOUS_AGENT_PATTERNS.some((pattern) => pattern.test(normalized))
|
||||||
|
|| isPageGenerationIntent(normalized))
|
||||||
) {
|
) {
|
||||||
return finalizeRouterClassification(normalizeClassification({
|
return finalizeRouterClassification(normalizeClassification({
|
||||||
route: CHAT_INTENT_ROUTE.AGENT,
|
route: CHAT_INTENT_ROUTE.AGENT,
|
||||||
confidence: 0.95,
|
confidence: 0.95,
|
||||||
reason: '明确需要生成或发布页面/文件',
|
reason: '明确需要生成或发布页面/文件',
|
||||||
suggested_skill: 'static-page-publish',
|
suggested_skill: 'static-page-publish',
|
||||||
agent_brief: '生成或更新 MindSpace 公开页面,并返回可访问链接。',
|
agent_brief: '生成或更新 MindSpace 公开页面,并返回可访问链接;信息不足时使用合理默认,不要停在追问。',
|
||||||
}, { source: 'rule' }), decisionContext);
|
}, { source: 'rule' }), decisionContext);
|
||||||
}
|
}
|
||||||
if (includeIntentPatterns && normalized && isRealtimeInfoQuestion(normalized)) {
|
if (includeIntentPatterns && normalized && isRealtimeInfoQuestion(normalized)) {
|
||||||
@@ -848,12 +875,16 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
sessionId,
|
sessionId,
|
||||||
sessionMessageCount,
|
sessionMessageCount,
|
||||||
};
|
};
|
||||||
const finalizeWithRealtimeCoercion = (classification) => {
|
const finalizeWithCoercion = (classification) => {
|
||||||
if (!classification) return classification;
|
if (!classification) return classification;
|
||||||
const base = { ...classification };
|
const base = { ...classification };
|
||||||
delete base.decision;
|
delete base.decision;
|
||||||
return finalizeRouterClassification(
|
return finalizeRouterClassification(
|
||||||
coerceRealtimeWebSkill(base, text, { grantedSkills }),
|
coercePageGenerationSkill(
|
||||||
|
coerceRealtimeWebSkill(base, text, { grantedSkills }),
|
||||||
|
text,
|
||||||
|
{ grantedSkills },
|
||||||
|
),
|
||||||
decisionContext,
|
decisionContext,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -867,9 +898,9 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
includeIntentPatterns: true,
|
includeIntentPatterns: true,
|
||||||
llmRouterEnabled: Boolean(policy.enabled),
|
llmRouterEnabled: Boolean(policy.enabled),
|
||||||
});
|
});
|
||||||
if (ruleResult) return finalizeWithRealtimeCoercion(ruleResult);
|
if (ruleResult) return finalizeWithCoercion(ruleResult);
|
||||||
if (!isEnabled()) {
|
if (!isEnabled()) {
|
||||||
return finalizeWithRealtimeCoercion(normalizeClassification({
|
return finalizeWithCoercion(normalizeClassification({
|
||||||
route: policy.fallbackRoute,
|
route: policy.fallbackRoute,
|
||||||
confidence: 0.5,
|
confidence: 0.5,
|
||||||
reason: '意图路由未启用,走默认通道',
|
reason: '意图路由未启用,走默认通道',
|
||||||
@@ -905,7 +936,7 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
'Chat intent router',
|
'Chat intent router',
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return finalizeWithRealtimeCoercion(normalizeClassification({
|
return finalizeWithCoercion(normalizeClassification({
|
||||||
route: policy.fallbackRoute,
|
route: policy.fallbackRoute,
|
||||||
confidence: 0,
|
confidence: 0,
|
||||||
reason: err instanceof Error ? err.message : '意图路由失败,走默认通道',
|
reason: err instanceof Error ? err.message : '意图路由失败,走默认通道',
|
||||||
@@ -913,7 +944,7 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
|
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
|
||||||
}
|
}
|
||||||
if (!completion?.ok) {
|
if (!completion?.ok) {
|
||||||
return finalizeWithRealtimeCoercion(normalizeClassification({
|
return finalizeWithCoercion(normalizeClassification({
|
||||||
route: policy.fallbackRoute,
|
route: policy.fallbackRoute,
|
||||||
confidence: 0,
|
confidence: 0,
|
||||||
reason: completion?.message ?? '意图路由失败,走默认通道',
|
reason: completion?.message ?? '意图路由失败,走默认通道',
|
||||||
@@ -923,7 +954,7 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
|
|
||||||
const parsed = parseRouterJson(completion.reply);
|
const parsed = parseRouterJson(completion.reply);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return finalizeWithRealtimeCoercion(normalizeClassification({
|
return finalizeWithCoercion(normalizeClassification({
|
||||||
route: policy.fallbackRoute,
|
route: policy.fallbackRoute,
|
||||||
confidence: 0,
|
confidence: 0,
|
||||||
reason: '意图路由响应无法解析,走默认通道',
|
reason: '意图路由响应无法解析,走默认通道',
|
||||||
@@ -931,7 +962,7 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
|
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const classification = finalizeWithRealtimeCoercion({
|
const classification = finalizeWithCoercion({
|
||||||
...normalizeClassification(parsed, { source: 'llm', fallbackRoute: policy.fallbackRoute }),
|
...normalizeClassification(parsed, { source: 'llm', fallbackRoute: policy.fallbackRoute }),
|
||||||
providerKeyId: completion.providerKeyId ?? policy.modelProviderKeyId ?? null,
|
providerKeyId: completion.providerKeyId ?? policy.modelProviderKeyId ?? null,
|
||||||
model: completion.model ?? policy.model ?? null,
|
model: completion.model ?? policy.model ?? null,
|
||||||
@@ -941,7 +972,7 @@ export function createChatIntentRouter(options = {}) {
|
|||||||
classification.route === CHAT_INTENT_ROUTE.DIRECT_CHAT &&
|
classification.route === CHAT_INTENT_ROUTE.DIRECT_CHAT &&
|
||||||
classification.confidence < policy.minConfidence
|
classification.confidence < policy.minConfidence
|
||||||
) {
|
) {
|
||||||
return finalizeWithRealtimeCoercion(normalizeClassification({
|
return finalizeWithCoercion(normalizeClassification({
|
||||||
...classification,
|
...classification,
|
||||||
route: policy.fallbackRoute,
|
route: policy.fallbackRoute,
|
||||||
reason: `${classification.reason}(置信度 ${classification.confidence} 低于阈值,走默认通道)`,
|
reason: `${classification.reason}(置信度 ${classification.confidence} 低于阈值,走默认通道)`,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
buildRouterNormalizedDecision,
|
buildRouterNormalizedDecision,
|
||||||
CHAT_INTENT_ROUTE,
|
CHAT_INTENT_ROUTE,
|
||||||
classifyWithRules,
|
classifyWithRules,
|
||||||
|
coercePageGenerationSkill,
|
||||||
createChatIntentRouter,
|
createChatIntentRouter,
|
||||||
createManagedChatIntentRouter,
|
createManagedChatIntentRouter,
|
||||||
isNormalizedRouterDecisionEnabled,
|
isNormalizedRouterDecisionEnabled,
|
||||||
@@ -49,6 +50,37 @@ test('classifyWithRules routes page generation to agent orchestration', () => {
|
|||||||
assert.equal(result.suggestedSkill, 'static-page-publish');
|
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', () => {
|
test('classifyWithRules routes themed article requests to agent orchestration', () => {
|
||||||
const result = classifyWithRules({
|
const result = classifyWithRules({
|
||||||
text: '帮我写个主题文章',
|
text: '帮我写个主题文章',
|
||||||
|
|||||||
+21
-4
@@ -17,8 +17,28 @@ const PAGE_GENERATION_INTENT_PATTERNS = [
|
|||||||
/(?:生成|做|制作|创建|设计|写|出)(?:一个|一份|个)?(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
|
/(?:生成|做|制作|创建|设计|写|出)(?:一个|一份|个)?(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
|
||||||
/(?:帮我|给我).*(?: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,
|
/(?: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 = [
|
export const CHAT_SKILL_DEFINITIONS = [
|
||||||
{
|
{
|
||||||
id: 'web-search',
|
id: 'web-search',
|
||||||
@@ -149,10 +169,7 @@ export function filterChatSkills(options, ctx) {
|
|||||||
export function buildAutoChatSkillPrefix(text, grantedSkills = []) {
|
export function buildAutoChatSkillPrefix(text, grantedSkills = []) {
|
||||||
const trimmed = String(text ?? '').trim();
|
const trimmed = String(text ?? '').trim();
|
||||||
if (!trimmed) return '';
|
if (!trimmed) return '';
|
||||||
if (
|
if (grantedSkills.includes(PUBLISH_SKILL_NAME) && isPageGenerationIntent(trimmed)) {
|
||||||
grantedSkills.includes(PUBLISH_SKILL_NAME) &&
|
|
||||||
PAGE_GENERATION_INTENT_PATTERNS.some((pattern) => pattern.test(trimmed))
|
|
||||||
) {
|
|
||||||
return buildChatSkillPrompt('generate-page', PUBLISH_SKILL_NAME);
|
return buildChatSkillPrompt('generate-page', PUBLISH_SKILL_NAME);
|
||||||
}
|
}
|
||||||
if (!grantedSkills.includes('web')) return '';
|
if (!grantedSkills.includes('web')) return '';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
buildChatSkillPrompt,
|
buildChatSkillPrompt,
|
||||||
CHAT_SKILL_DEFINITIONS,
|
CHAT_SKILL_DEFINITIONS,
|
||||||
filterChatSkills,
|
filterChatSkills,
|
||||||
|
isPageGenerationIntent,
|
||||||
} from './chat-skills.mjs';
|
} from './chat-skills.mjs';
|
||||||
|
|
||||||
test('filterChatSkills shows summarize and analyze without granted skills', () => {
|
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.match(prefix, /禁止只给本地路径/);
|
||||||
assert.equal(buildAutoChatSkillPrefix('帮我做一个 Hello 糖的 H5 页面', []), '');
|
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, /禁止询问是否还要发布/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ description: 商品宣传 / 活动页技能:基于商品链接、主题、图
|
|||||||
- 用户上传商品图、Logo、模特图、详情图,希望生成页面文案和视觉结构
|
- 用户上传商品图、Logo、模特图、详情图,希望生成页面文案和视觉结构
|
||||||
- 用户要做促销、上新、种草、达人推荐、节日活动或私域转化页面
|
- 用户要做促销、上新、种草、达人推荐、节日活动或私域转化页面
|
||||||
|
|
||||||
|
## 何时不用
|
||||||
|
|
||||||
|
- 旅游攻略、城市介绍、游记、主题内容页 → 使用 `static-page-publish`,直接生成 HTML 并返回链接
|
||||||
|
- 用户未提供商品链接、也不要求购买跳转时,不要套用本技能
|
||||||
|
|
||||||
## 必问信息
|
## 必问信息
|
||||||
|
|
||||||
如果用户没有一次性给全,优先只问这 3 个问题:
|
如果用户没有一次性给全,优先只问这 3 个问题:
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ npm run check:mindspace-cover
|
|||||||
|
|
||||||
带 `data-mindspace-page-tag` 的区域为平台固定信息:用户在编辑模式中不可见、不可改;预览与发布后正常显示。**禁止**把该行 CSS 透明度设过低(如 `opacity: 0.25`),否则页内看不见品牌。
|
带 `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)
|
||||||
|
|
||||||
- 只有用户明确要求 Word / PDF 下载时才生成二进制文件
|
- 只有用户明确要求 Word / PDF 下载时才生成二进制文件
|
||||||
|
|||||||
Reference in New Issue
Block a user