ec8d0464a3
短句如"苏州攻略页面"此前会被规则/LLM 误判为 product-campaign-page(先追问 促销信息),导致用户体验成"卡住不动"。规则层扩大页面生成意图识别,并新增 coercePageGenerationSkill 在 LLM 判为 product-campaign-page 但明显是内容页 时强制纠正为 static-page-publish,避免不必要的追问。同步更新两个技能文档 的适用边界说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
224 lines
9.1 KiB
JavaScript
224 lines
9.1 KiB
JavaScript
// 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;
|
||
}
|