From ec8d0464a3fa3f8309230947d07fc7c11235408c Mon Sep 17 00:00:00 2001 From: john Date: Tue, 7 Jul 2026 10:55:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat-intent):=20=E9=9A=90=E5=BC=8F=E6=94=BB?= =?UTF-8?q?=E7=95=A5/=E9=A1=B5=E9=9D=A2=E7=94=9F=E6=88=90=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=9B=B4=E6=8E=A5=E8=B7=AF=E7=94=B1=E5=88=B0=20static?= =?UTF-8?q?-page-publish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 短句如"苏州攻略页面"此前会被规则/LLM 误判为 product-campaign-page(先追问 促销信息),导致用户体验成"卡住不动"。规则层扩大页面生成意图识别,并新增 coercePageGenerationSkill 在 LLM 判为 product-campaign-page 但明显是内容页 时强制纠正为 static-page-publish,避免不必要的追问。同步更新两个技能文档 的适用边界说明。 Co-authored-by: Cursor --- chat-intent-router.mjs | 53 +++++++++++++++++++++------ chat-intent-router.test.mjs | 32 ++++++++++++++++ chat-skills.mjs | 25 +++++++++++-- chat-skills.test.mjs | 13 +++++++ skills/product-campaign-page/SKILL.md | 5 +++ skills/static-page-publish/SKILL.md | 8 ++++ 6 files changed, 121 insertions(+), 15 deletions(-) diff --git a/chat-intent-router.mjs b/chat-intent-router.mjs index 3ce2c5f..81e5d37 100644 --- a/chat-intent-router.mjs +++ b/chat-intent-router.mjs @@ -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} 低于阈值,走默认通道)`, diff --git a/chat-intent-router.test.mjs b/chat-intent-router.test.mjs index 63c7844..6942eff 100644 --- a/chat-intent-router.test.mjs +++ b/chat-intent-router.test.mjs @@ -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: '帮我写个主题文章', diff --git a/chat-skills.mjs b/chat-skills.mjs index 7c797b9..c2679cb 100644 --- a/chat-skills.mjs +++ b/chat-skills.mjs @@ -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 ''; diff --git a/chat-skills.test.mjs b/chat-skills.test.mjs index d2b745b..3c6c029 100644 --- a/chat-skills.test.mjs +++ b/chat-skills.test.mjs @@ -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, /禁止询问是否还要发布/); +}); diff --git a/skills/product-campaign-page/SKILL.md b/skills/product-campaign-page/SKILL.md index c1ffe14..f3d4877 100644 --- a/skills/product-campaign-page/SKILL.md +++ b/skills/product-campaign-page/SKILL.md @@ -14,6 +14,11 @@ description: 商品宣传 / 活动页技能:基于商品链接、主题、图 - 用户上传商品图、Logo、模特图、详情图,希望生成页面文案和视觉结构 - 用户要做促销、上新、种草、达人推荐、节日活动或私域转化页面 +## 何时不用 + +- 旅游攻略、城市介绍、游记、主题内容页 → 使用 `static-page-publish`,直接生成 HTML 并返回链接 +- 用户未提供商品链接、也不要求购买跳转时,不要套用本技能 + ## 必问信息 如果用户没有一次性给全,优先只问这 3 个问题: diff --git a/skills/static-page-publish/SKILL.md b/skills/static-page-publish/SKILL.md index ba7db46..1d836dc 100644 --- a/skills/static-page-publish/SKILL.md +++ b/skills/static-page-publish/SKILL.md @@ -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 下载时才生成二进制文件