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,
|
||||
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} 低于阈值,走默认通道)`,
|
||||
|
||||
@@ -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
@@ -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 '';
|
||||
|
||||
@@ -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, /禁止询问是否还要发布/);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ description: 商品宣传 / 活动页技能:基于商品链接、主题、图
|
||||
- 用户上传商品图、Logo、模特图、详情图,希望生成页面文案和视觉结构
|
||||
- 用户要做促销、上新、种草、达人推荐、节日活动或私域转化页面
|
||||
|
||||
## 何时不用
|
||||
|
||||
- 旅游攻略、城市介绍、游记、主题内容页 → 使用 `static-page-publish`,直接生成 HTML 并返回链接
|
||||
- 用户未提供商品链接、也不要求购买跳转时,不要套用本技能
|
||||
|
||||
## 必问信息
|
||||
|
||||
如果用户没有一次性给全,优先只问这 3 个问题:
|
||||
|
||||
@@ -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 下载时才生成二进制文件
|
||||
|
||||
Reference in New Issue
Block a user