// Keep browser-safe: do not import user-publish.mjs (uses node:fs/path/url). const PUBLISH_SKILL_NAME = 'static-page-publish'; export const PAGE_DATA_COLLECT_SKILL_NAME = 'page-data-collect'; export const SKILL_ROUTER_V2_ENV = 'TKMIND_SKILL_ROUTER_V2'; 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, ]; const PAGE_DATA_INTENT_PATTERNS = [ /(?:问卷|调查|签到表|签到登记|签到收集|投票|意见反馈|数据上报)/u, /(?:报名表|报名登记|在线报名|收集报名)/u, /(?:作业登记|作业打卡|作业记录|每日作业|打卡登记|台账|记录表)/u, /(?:表单|数据采集|数据交互|存数据|保存提交|提交记录)/u, /(?:后台|管理入口|管理后台).{0,20}(?:查看|记录|数据|提交)/u, /(?:密码|口令).{0,12}(?:查看|后台|管理|进入)/u, /(?:sqlite|数据库|page[\s-]?data)/i, /(?:每个用户|各用户).{0,12}(?:提交|记录)/u, /(?:记账|账本|收支|流水|日记|习惯打卡|每日打卡).{0,40}(?:页面|记录|管理|汇总|统计)/u, /(?:管理页面|管理页).{0,30}(?:查看|记录|汇总|统计|明细|删除|修改)/u, /(?:页面|网页|H5|h5).{0,80}(?:每天|每日|新增|添加|填写|记录).{0,80}(?:所有记录|历史记录|管理|汇总|统计)/u, ]; const INTERACTIVE_PAGE_DATA_SUBJECT_PATTERN = /(?:便签|便利贴|备忘录|待办|清单)/u; const INTERACTIVE_PAGE_DATA_SURFACE_PATTERN = /(?:应用|app|页面|网页|H5|h5|HTML|html|时间轴|看板)/u; const INTERACTIVE_PAGE_DATA_FEATURE_PATTERNS = [ /(?:写|填写|提交|保存)/u, /(?:添加|新增|创建)/u, /(?:编辑|修改)/u, /(?:删除|完成)/u, /(?:筛选|统计|汇总|管理)/u, /(?:提醒时间|优先级|分类标签|时间轴)/u, ]; function isInteractivePageDataIntent(text) { if (!INTERACTIVE_PAGE_DATA_SUBJECT_PATTERN.test(text)) return false; const featureCount = INTERACTIVE_PAGE_DATA_FEATURE_PATTERNS.reduce( (count, pattern) => count + (pattern.test(text) ? 1 : 0), 0, ); return INTERACTIVE_PAGE_DATA_SURFACE_PATTERN.test(text) || featureCount >= 2; } export function isPageDataIntent(text) { const normalized = String(text ?? '').trim(); if (!normalized) return false; return PAGE_DATA_INTENT_PATTERNS.some((pattern) => pattern.test(normalized)) || isInteractivePageDataIntent(normalized); } 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: 'page-data-collect', label: '数据页面', icon: 'form', skillName: PAGE_DATA_COLLECT_SKILL_NAME, requiresSkill: PAGE_DATA_COLLECT_SKILL_NAME, requiresPublish: true, promptKey: 'page-data-collect', }, { 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 'page-data-collect': return ( `请使用 ${skillName ?? PAGE_DATA_COLLECT_SKILL_NAME} 技能:在 MindSpace 页面中实现可提交、可持久化的数据收集(问卷/报名/台账等),必须使用 Page Data API。` + '先匹配技能内能力分支(默认 A:匿名前台 public insert + 独立后台 password read,口令默认 88888888);“帮我做/创建”本身就是创建授权,展示简短方案摘要后必须同一轮继续执行,只有互斥需求或非法口令才追问。' + '流程:load_skill → private_data_execute 建表 → private_data_register_dataset → write_file/edit_file 写 public/*.html(含 /assets/page-data-client.js)→ private_data_bind_workspace_page 发布并写策略。' + '当前 session 的 private_data_* 已绑定当前用户专属 PostgreSQL schema;所有需保存或再次读取的数据必须走 PG + Page Data API。禁止 localStorage、sessionStorage、IndexedDB、SQLite、静态 JSON/JS 或内存持久化 fallback;禁止自建 Express/独立端口(如 8899)、禁止 HTML 硬编码 127.0.0.1 API、禁止连续空转不调用工具。HTML 视觉规范参照 static-page-publish。完成后只返回 workspaceUrl(/MindSpace/<用户ID>/public/...),禁止给用户 /u/用户名/pages/... 链接,并说明后台入口与口令。' ); 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;允许图表、动画等动态效果,但脚本必须适合沙箱发布,优先使用平台已支持的本地/可信库,禁止 javascript:、onxxx 内联事件、未知第三方脚本和跨用户私有资源。' + '完成前必须验证文件已写入、页面安全检查/发布状态可用、链接返回 200 且正文包含关键数据;验证失败时不要宣称“已完成”,应修复后再回复 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) { const pageDataGranted = skill.skillName === PAGE_DATA_COLLECT_SKILL_NAME && ctx.grantedSkills?.includes(PAGE_DATA_COLLECT_SKILL_NAME); if (!pageDataGranted) return false; } if (skill.requiresSkill && !ctx.grantedSkills?.includes(skill.requiresSkill)) return false; return true; }); } const PROMPT_KEY_BY_SKILL_NAME = new Map( CHAT_SKILL_DEFINITIONS.filter((item) => item.skillName && item.promptKey).map((item) => [ item.skillName, item.promptKey, ]), ); /** Opt-in Skill Router v2. Default off unless TKMIND_SKILL_ROUTER_V2=1 or options.skillRouterV2=true. */ export function isSkillRouterV2Enabled(explicit) { if (explicit === true) return true; if (explicit === false) return false; const raw = typeof process !== 'undefined' && process?.env ? process.env[SKILL_ROUTER_V2_ENV] : undefined; return /^(1|true|yes)$/i.test(String(raw ?? '')); } /** Build manifest routes from platform catalog entries that declare trigger.keywords. */ export function buildManifestRoutesFromCatalog(catalog = []) { return catalog .filter((item) => Array.isArray(item?.manifest?.trigger?.keywords) && item.manifest.trigger.keywords.length > 0) .map((item) => ({ skillName: item.name, keywords: item.manifest.trigger.keywords, priority: Number(item.manifest?.router?.priority ?? 0), promptKey: item.manifest?.router?.promptKey ?? PROMPT_KEY_BY_SKILL_NAME.get(item.name) ?? null, promptVariant: item.manifest?.router?.promptVariant ?? null, })) .sort((a, b) => b.priority - a.priority || a.skillName.localeCompare(b.skillName)); } export function matchManifestSkillRoute(text, routes = [], grantedSkills = []) { const normalized = String(text ?? '').trim().toLowerCase(); if (!normalized || !Array.isArray(routes) || routes.length === 0) return null; const granted = new Set(grantedSkills); for (const route of routes) { if (!route?.skillName || !granted.has(route.skillName)) continue; const keywords = Array.isArray(route.keywords) ? route.keywords : []; if (keywords.some((keyword) => normalized.includes(String(keyword).trim().toLowerCase()))) { return route; } } return null; } function buildManifestSkillPrompt(route) { if (!route) return ''; if (route.promptVariant === 'web-news') { return buildWebNewsSkillPrompt(route.skillName); } if (route.promptKey) { return buildChatSkillPrompt(route.promptKey, route.skillName); } return `请使用 ${route.skillName} 技能:`; } function buildLegacyAutoChatSkillPrefix(trimmed, grantedSkills) { if ( grantedSkills.includes(PAGE_DATA_COLLECT_SKILL_NAME) && isPageDataIntent(trimmed) ) { return buildChatSkillPrompt('page-data-collect', PAGE_DATA_COLLECT_SKILL_NAME); } 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 buildAutoChatSkillPrefixOptions(publicConfig) { if (!publicConfig?.routerV2Enabled || !publicConfig?.manifestRoutingEnabled) { return {}; } return { skillRouterV2: true, manifestRoutes: publicConfig.manifestRoutes ?? [], }; } export function buildAutoChatSkillPrefix(text, grantedSkills = [], options = {}) { const trimmed = String(text ?? '').trim(); if (!trimmed) return ''; if ( isSkillRouterV2Enabled(options.skillRouterV2) && Array.isArray(options.manifestRoutes) && options.manifestRoutes.length > 0 ) { const matched = matchManifestSkillRoute(trimmed, options.manifestRoutes, grantedSkills); if (matched) { return buildManifestSkillPrompt(matched); } } return buildLegacyAutoChatSkillPrefix(trimmed, grantedSkills); } 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; }