Files
memind/chat-skills.mjs
T
john ec8d0464a3 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>
2026-07-07 10:56:26 +08:00

224 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Keep browser-safe: do not import user-publish.mjs (uses node:fs/path/url).
const PUBLISH_SKILL_NAME = 'static-page-publish';
const WEB_INTENT_PATTERNS = [
/(?:今天|今日|最新|热点|热搜|新闻|头条|头条新闻|最新消息|发生了什么|最近发生)/u,
/(?:latest|breaking)\s+news/i,
/what(?:'s| is)?\s+happening/i,
];
const WEB_NEWS_INTENT_PATTERNS = [
/(?:今天|今日|最新|热点|热搜|新闻|头条|头条新闻|热榜|热议|舆情|突发)/u,
/(?:today|latest|breaking|trending)\s+(?:news|stories|headlines|topics)/i,
/(?:what(?:'s| is)?\s+the\s+latest|what(?:'s| is)?\s+trending)/i,
];
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',
label: '查资料',
icon: 'web',
skillName: 'web',
requiresSkill: 'web',
prefillOnly: true,
promptKey: 'web',
},
{
id: 'code-search',
label: '搜代码',
icon: 'search',
skillName: 'search',
requiresSkill: 'search',
prefillOnly: true,
promptKey: 'search',
},
{
id: 'service-integration-smoke',
label: '联调测试',
icon: 'analyze',
skillName: 'service-integration-smoke',
requiresSkill: 'service-integration-smoke',
prefillOnly: true,
promptKey: 'service-integration-smoke',
},
{
id: 'form-collect',
label: '表单收集',
icon: 'form',
skillName: 'form-builder',
requiresSkill: 'form-builder',
prefillOnly: true,
promptKey: 'form-builder',
},
{
id: 'table-view',
label: '数据表格',
icon: 'table',
skillName: 'table-viewer',
requiresSkill: 'table-viewer',
prefillOnly: true,
promptKey: 'table-viewer',
},
{
id: 'product-campaign-page',
label: '商品页',
icon: 'page',
skillName: 'product-campaign-page',
requiresSkill: 'product-campaign-page',
promptKey: 'product-campaign-page',
},
{
id: 'summarize',
label: '总结内容',
icon: 'summary',
prefillOnly: true,
promptKey: 'summarize',
},
{
id: 'analyze',
label: '深度分析',
icon: 'analyze',
prefillOnly: true,
promptKey: 'analyze',
},
{
id: 'generate-page',
label: '生成页面',
icon: 'page',
skillName: PUBLISH_SKILL_NAME,
requiresPublish: true,
promptKey: 'generate-page',
},
];
export function buildChatSkillPrompt(promptKey, skillName) {
switch (promptKey) {
case 'web':
return `请使用 ${skillName ?? 'web'} 技能:帮我搜索并查阅相关资料(优先官方文档),并给出中文摘要与来源链接。我的问题是:`;
case 'search':
return `请使用 ${skillName ?? 'search'} 技能:帮我在工作区中查找代码或文件。我要找的是:`;
case 'form-builder':
return `请使用 ${skillName ?? 'form-builder'} 技能:请用交互式表单收集以下场景所需的结构化字段(字段不超过 8 个):`;
case 'service-integration-smoke':
return `请使用 ${skillName ?? 'service-integration-smoke'} 技能:按标准联调流程检查当前服务,覆盖身份、普通聊天、记忆读取,以及我本轮明确要求验证的技能/发布链路,并输出通过项、失败项、待确认项:`;
case 'table-viewer':
return `请使用 ${skillName ?? 'table-viewer'} 技能:请把以下数据整理成可排序、可筛选的交互式表格:`;
case 'product-campaign-page':
return (
`请使用 ${skillName ?? 'product-campaign-page'} 技能:帮我制作一个商品宣传 / 活动页。` +
'请先询问商品链接、页面主题、是否有商品图片或品牌素材;信息足够后,生成包含购买跳转按钮的页面方案。' +
'如果我确认要发布成 H5 页面,请继续使用 static-page-publish 技能生成可访问链接。'
);
case 'summarize':
return '请总结以下内容,提炼核心结论、重点信息和可执行建议(条理清晰、中文输出):';
case 'analyze':
return '请深入分析以下内容,给出结构化解读:背景、关键发现、风险或机会、以及建议下一步:';
case 'generate-page':
return (
`请使用 ${skillName ?? PUBLISH_SKILL_NAME} 技能:在我的专属 MindSpace 发布目录生成静态 HTML 页面,并给出可公网访问的完整链接。` +
'默认只生成 HTML:必须先 load_skill,再用 write_file/edit_file 写入 public/页面.html;完成后直接返回 Markdown 可点击公网链接 `[页面标题](URL)`。' +
'禁止只给本地路径(如 hello/index.html),禁止询问是否还要发布,除非写文件工具实际失败。' +
'只有用户明确要求 Word / docx 下载时,才先 `load_skill` → `docx-generate`,生成 `public/*.docx` 并确认文件已存在,再写 HTML 并用同目录相对路径链接该文档。' +
'只有用户明确要求长图下载时,才先 `load_skill` → `long-image-download`,调用 `generate_long_image` 生成 `public/*.long.png` 并确认文件已存在,再给长图预览链接和 `?download=long-image` 下载链接。不要在没有明确需求时强制生成伴生文件。'
);
default:
return '';
}
}
function buildWebNewsSkillPrompt(skillName) {
return `请使用 ${skillName ?? 'web'} 技能:先搜索今天/最新相关的新闻与热点,优先一手来源和权威媒体,整理 3-5 条最相关结果,按时间或热度排序;然后给出中文摘要、关键信息、事件背景和来源链接。我的问题是:`;
}
export { buildWebNewsSkillPrompt };
export function filterChatSkills(options, ctx) {
return options.filter((skill) => {
if (skill.requiresPublish && !ctx.canPublish) return false;
if (skill.requiresSkill && !ctx.grantedSkills?.includes(skill.requiresSkill)) return false;
return true;
});
}
export function buildAutoChatSkillPrefix(text, grantedSkills = []) {
const trimmed = String(text ?? '').trim();
if (!trimmed) return '';
if (grantedSkills.includes(PUBLISH_SKILL_NAME) && isPageGenerationIntent(trimmed)) {
return buildChatSkillPrompt('generate-page', PUBLISH_SKILL_NAME);
}
if (!grantedSkills.includes('web')) return '';
if (WEB_NEWS_INTENT_PATTERNS.some((pattern) => pattern.test(trimmed))) {
return buildWebNewsSkillPrompt('web');
}
if (!WEB_INTENT_PATTERNS.some((pattern) => pattern.test(trimmed))) return '';
return buildChatSkillPrompt('web', 'web');
}
export function stripKnownChatSkillPrompt(text) {
let next = String(text ?? '').trimStart();
const candidates = [];
for (const definition of CHAT_SKILL_DEFINITIONS) {
if (!definition.promptKey) continue;
candidates.push(buildChatSkillPrompt(definition.promptKey, definition.skillName));
}
candidates.push(buildWebNewsSkillPrompt('web'));
candidates.sort((a, b) => b.length - a.length);
for (const prompt of candidates) {
if (prompt && next.startsWith(prompt)) {
next = next.slice(prompt.length);
break;
}
}
return next.trim();
}
export function hasExplicitChatSkillPrompt(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
if (/请使用\s+[\w-]+\s+技能[:]/u.test(normalized)) return true;
return stripKnownChatSkillPrompt(normalized) !== normalized;
}
export function extractSelectedChatSkillName(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return null;
const match = normalized.match(/请使用\s+([\w-]+)\s+技能[:]/u);
if (match?.[1]) return match[1];
for (const definition of CHAT_SKILL_DEFINITIONS) {
if (!definition.promptKey) continue;
const prompt = buildChatSkillPrompt(definition.promptKey, definition.skillName);
if (prompt && normalized.includes(prompt)) {
return definition.skillName ?? definition.id;
}
}
const newsPrompt = buildWebNewsSkillPrompt('web');
if (newsPrompt && normalized.includes(newsPrompt)) return 'web';
return null;
}