diff --git a/lib/canvas-skill-tags.ts b/lib/canvas-skill-tags.ts index f80312712..e71ca1487 100644 --- a/lib/canvas-skill-tags.ts +++ b/lib/canvas-skill-tags.ts @@ -1,6 +1,6 @@ export const CANVAS_SKILL_TAGS_SETTING_KEY = "canvas_skill_tags"; export const CANVAS_SKILL_TAG_ORDER_SETTING_KEY_PREFIX = "canvas_skill_tag_order:"; -export const CANVAS_SKILL_TAGS_CONFIG_VERSION = 5; +export const CANVAS_SKILL_TAGS_CONFIG_VERSION = 6; export const DEFAULT_CANVAS_SKILL_TAGS_TITLE = "Claw 快捷技能"; export type CanvasSkillTagSettings = { @@ -19,6 +19,27 @@ export type CanvasSkillTag = { iconUrl?: string; }; +export const CANVAS_SKILL_TAGS_V6_ADDITIONS: CanvasSkillTag[] = [ + { + id: "product-lifecycle-overview", + name: "产品生命周期总览", + iconName: "layout-grid", + placeholder: + "查看公司产品生命周期总览,分析经营阶段、库存风险、SKU迭代和停产清退", + prompt: + "【前台技能:商品全生命周期】【技能路由:product-lifecycle-archive-db】【技能目录:product-lifecycle-archive】全生命周期产品分析。用途:查看公司内部产品从开款、经营到停产的全生命周期经营总览,包括产品规模、生命周期阶段、SKU 迭代、品类经营、库存周转、停产清退和负责人。进入后直接返回公司产品经营总览,并提供库存风险 TOP10、低效产品结构、品类经营表现、SKU 迭代效果、停产与清退、负责人看板和导出经营盘点表等下一步选择。不要解释成市场类目的引入期/成长期分析,也不要转到市场数据库。", + }, + { + id: "new-product-progress", + name: "新品进度", + iconName: "sparkles", + placeholder: + "输入 SPU、SKU、开款编号、商品ID或流程号,查询新品当前节点和审批时间线", + prompt: + "【前台技能:商品全生命周期】【技能路由:product-lifecycle-archive-db】【技能目录:product-lifecycle-archive】新品进度查询。用途:查询单个新品当前审批进度,或汇总全部进行中的新品项目。单品查询时先要求用户输入 SPU、SKU、开款编号、商品 ID 或流程号,返回当前节点、当前处理人、审批时间线和流程表单;用户明确要求全部新品时,返回所有未完结新品、超时节点、当前处理人和进行中明细,并提供查看当前卡点、负责人负载、品类进度等下一步选择。不要转到市场新品趋势或商品策划技能。", + }, +]; + export const DEFAULT_CANVAS_SKILL_TAGS: CanvasSkillTag[] = [ { id: "video-gateway", @@ -52,6 +73,7 @@ export const DEFAULT_CANVAS_SKILL_TAGS: CanvasSkillTag[] = [ prompt: "【技能路由:product-detail-planning-v2】用途:通过可恢复的分步对话完成商品详情页全案策划,覆盖产品事实、竞品、真实评价与问大家、内容策划、视觉策划、详情页提示词、五张主图、主图视频、AI 买家秀,以及最终 HTML 和 17 子表 Excel 交付。触发关键词:商品策划案、详情页策划、完整详情策划、新品详情策划、竞品详情分析、详情页改版、主图策划、SKU图策划、主图视频策划、AI买家秀策划、继续详情策划、新版商品策划案、商品策划案V2。通用“商品策划案”默认进入 V2,只有用户明确要求旧版/V1时才使用兼容流程。公司已有商品可提供 SKU/SPU;新品可以提供产品名、现有资料并上传产品图。每轮只处理当前节点,完整展示当前节点数据、系统最优推荐和可选动作,但不得替用户确认或跨节点推进;图片只证明可见结构,不得自动证明材质、参数、检测或服务承诺。全部必需节点完成后生成 HTML 策划案和固定 17 子表 Excel,并返回查看或下载入口。", }, + ...CANVAS_SKILL_TAGS_V6_ADDITIONS, { id: "install-media-db", name: "安装资料", @@ -195,6 +217,17 @@ function normalizeId(value: unknown, fallbackIndex: number) { return id || `tag-${fallbackIndex + 1}`; } +function appendMissingCanvasSkillTags( + tags: CanvasSkillTag[], + additions: CanvasSkillTag[] +) { + const existingIds = new Set(tags.map((tag) => tag.id)); + return [ + ...tags, + ...additions.filter((tag) => !existingIds.has(tag.id)), + ].slice(0, 30); +} + export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] { const source = Array.isArray(value) ? value : DEFAULT_CANVAS_SKILL_TAGS; const usedIds = new Set(); @@ -236,10 +269,14 @@ export function normalizeCanvasSkillTagSettings( options: { requireCurrentVersion?: boolean } = {} ): CanvasSkillTagSettings { if (Array.isArray(value)) { + const tags = appendMissingCanvasSkillTags( + normalizeCanvasSkillTags(value), + CANVAS_SKILL_TAGS_V6_ADDITIONS + ); return { enabled: true, title: DEFAULT_CANVAS_SKILL_TAGS_TITLE, - tags: normalizeCanvasSkillTags(value), + tags, version: CANVAS_SKILL_TAGS_CONFIG_VERSION, }; } @@ -249,10 +286,14 @@ export function normalizeCanvasSkillTagSettings( const version = Number(row.version || 0); if (options.requireCurrentVersion && version < CANVAS_SKILL_TAGS_CONFIG_VERSION) { const title = normalizeString(row.title) || DEFAULT_CANVAS_SKILL_TAGS_TITLE; + const tags = appendMissingCanvasSkillTags( + normalizeCanvasSkillTags(row.tags), + CANVAS_SKILL_TAGS_V6_ADDITIONS + ); return { enabled: row.enabled !== false, title, - tags: normalizeCanvasSkillTags(row.tags), + tags, version: CANVAS_SKILL_TAGS_CONFIG_VERSION, }; } diff --git a/tests/routes/admin-settings.test.ts b/tests/routes/admin-settings.test.ts index 059972a77..7d685a589 100644 --- a/tests/routes/admin-settings.test.ts +++ b/tests/routes/admin-settings.test.ts @@ -1,5 +1,7 @@ import { expect, test } from "@playwright/test"; import { + CANVAS_SKILL_TAGS_CONFIG_VERSION, + CANVAS_SKILL_TAGS_V6_ADDITIONS, DEFAULT_CANVAS_SKILL_TAGS, normalizeCanvasSkillTagSettings, type CanvasSkillTag, @@ -161,7 +163,7 @@ test.describe.serial("/api/admin/settings", () => { ); }); - test("canvas skill tag upgrades preserve admin configured tags without restoring defaults", () => { + test("canvas skill tag upgrades preserve custom tags and append new managed skills", () => { const customTags: CanvasSkillTag[] = [ { id: "custom-claw-skill", @@ -183,17 +185,45 @@ test.describe.serial("/api/admin/settings", () => { ); expect(normalized.title).toBe("试试这些 Skills"); - expect(normalized.tags).toEqual(customTags); + expect(normalized.tags).toEqual([ + ...customTags, + ...CANVAS_SKILL_TAGS_V6_ADDITIONS, + ]); expect( normalized.tags.some((tag) => tag.id === "promotion-diagnosis-db") ).toBe(false); }); + test("current canvas skill tag settings can intentionally remove managed skills", () => { + const customTags: CanvasSkillTag[] = [ + { + id: "custom-claw-skill", + name: "自定义技能", + prompt: "只保留后台手动配置的技能。", + }, + ]; + + const normalized = normalizeCanvasSkillTagSettings( + { + enabled: true, + title: "Claw 快捷技能", + tags: customTags, + version: CANVAS_SKILL_TAGS_CONFIG_VERSION, + }, + { requireCurrentVersion: true } + ); + + expect(normalized.tags).toEqual(customTags); + }); + test("canvas skill tags still use defaults when no stored setting exists", () => { const normalized = normalizeCanvasSkillTagSettings(null, { requireCurrentVersion: true, }); expect(normalized.tags).toEqual(DEFAULT_CANVAS_SKILL_TAGS); + expect(normalized.tags.map((tag) => tag.name)).toEqual( + expect.arrayContaining(["产品生命周期总览", "新品进度"]) + ); }); });