From 345ba6e0201a55a74c31d7eb95a6c3721d737220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ACT=E4=B8=B6=E6=B5=81=E6=98=9F=E9=9B=A8?= <1340145680@qq.com> Date: Thu, 26 Mar 2026 00:42:45 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84md=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SKILL.md => production_agent_decision.md} | 0 ...SKILL.md => production_agent_execution.md} | 0 data/skills/production_agent_supervision.md | 0 .../references/adaptation-format.md | 0 .../references/derive-assets-extraction.md | 0 .../references/event-extract.md | 0 .../execution => }/references/event-format.md | 0 .../references/novel-character-extract.md | 0 .../references/novel-props-extract.md | 0 .../references/novel-scene-extract.md | 0 .../decision => }/references/pipeline.md | 0 .../decision => }/references/plan.md | 0 .../references/quality-criteria.md | 0 .../references/script-format.md | 0 .../references/skeleton-format.md | 0 .../references/storyboard-generation.md | 0 .../references/video-dialogue-extract.md | 0 .../SKILL.md => script_agent_decision.md} | 0 .../SKILL.md => script_agent_execution.md} | 0 .../SKILL.md => script_agent_supervision.md} | 0 .../SKILL.md => universal_agent.md} | 0 skillList.json | 58 +++ src/agents/productionAgent/index.ts | 6 +- src/agents/productionAgent/tools.ts | 4 +- src/agents/scriptAgent/index.ts | 6 +- src/app.ts | 1 - src/lib/initDB.ts | 400 +++++++++++++++++- src/router.ts | 52 ++- .../setting/skillManagement/addSkill.ts | 102 +++++ .../setting/skillManagement/deleteSkill.ts | 49 +++ .../setting/skillManagement/embeddingSkill.ts | 31 ++ .../skillManagement/generateDescription.ts | 35 ++ .../setting/skillManagement/getSkillList.ts | 92 ++++ .../setting/skillManagement/scanSkills.ts | 121 ++++++ .../setting/skillManagement/updateSkill.ts | 118 ++++++ src/routes/test/test.ts | 14 +- src/types/database.d.ts | 26 +- src/utils/agent/skillsTools.ts | 202 ++++----- 38 files changed, 1149 insertions(+), 168 deletions(-) rename data/skills/{production-agent/decision/SKILL.md => production_agent_decision.md} (100%) rename data/skills/{production-agent/execution/SKILL.md => production_agent_execution.md} (100%) create mode 100644 data/skills/production_agent_supervision.md rename data/skills/{script-agent/execution => }/references/adaptation-format.md (100%) rename data/skills/{production-agent/execution => }/references/derive-assets-extraction.md (100%) rename data/skills/{universal-agent => }/references/event-extract.md (100%) rename data/skills/{script-agent/execution => }/references/event-format.md (100%) rename data/skills/{universal-agent => }/references/novel-character-extract.md (100%) rename data/skills/{universal-agent => }/references/novel-props-extract.md (100%) rename data/skills/{universal-agent => }/references/novel-scene-extract.md (100%) rename data/skills/{script-agent/decision => }/references/pipeline.md (100%) rename data/skills/{production-agent/decision => }/references/plan.md (100%) rename data/skills/{script-agent/supervision => }/references/quality-criteria.md (100%) rename data/skills/{script-agent/execution => }/references/script-format.md (100%) rename data/skills/{script-agent/execution => }/references/skeleton-format.md (100%) rename data/skills/{production-agent/execution => }/references/storyboard-generation.md (100%) rename data/skills/{universal-agent => }/references/video-dialogue-extract.md (100%) rename data/skills/{script-agent/decision/SKILL.md => script_agent_decision.md} (100%) rename data/skills/{script-agent/execution/SKILL.md => script_agent_execution.md} (100%) rename data/skills/{script-agent/supervision/SKILL.md => script_agent_supervision.md} (100%) rename data/skills/{universal-agent/SKILL.md => universal_agent.md} (100%) create mode 100644 skillList.json create mode 100644 src/routes/setting/skillManagement/addSkill.ts create mode 100644 src/routes/setting/skillManagement/deleteSkill.ts create mode 100644 src/routes/setting/skillManagement/embeddingSkill.ts create mode 100644 src/routes/setting/skillManagement/generateDescription.ts create mode 100644 src/routes/setting/skillManagement/getSkillList.ts create mode 100644 src/routes/setting/skillManagement/scanSkills.ts create mode 100644 src/routes/setting/skillManagement/updateSkill.ts diff --git a/data/skills/production-agent/decision/SKILL.md b/data/skills/production_agent_decision.md similarity index 100% rename from data/skills/production-agent/decision/SKILL.md rename to data/skills/production_agent_decision.md diff --git a/data/skills/production-agent/execution/SKILL.md b/data/skills/production_agent_execution.md similarity index 100% rename from data/skills/production-agent/execution/SKILL.md rename to data/skills/production_agent_execution.md diff --git a/data/skills/production_agent_supervision.md b/data/skills/production_agent_supervision.md new file mode 100644 index 0000000..e69de29 diff --git a/data/skills/script-agent/execution/references/adaptation-format.md b/data/skills/references/adaptation-format.md similarity index 100% rename from data/skills/script-agent/execution/references/adaptation-format.md rename to data/skills/references/adaptation-format.md diff --git a/data/skills/production-agent/execution/references/derive-assets-extraction.md b/data/skills/references/derive-assets-extraction.md similarity index 100% rename from data/skills/production-agent/execution/references/derive-assets-extraction.md rename to data/skills/references/derive-assets-extraction.md diff --git a/data/skills/universal-agent/references/event-extract.md b/data/skills/references/event-extract.md similarity index 100% rename from data/skills/universal-agent/references/event-extract.md rename to data/skills/references/event-extract.md diff --git a/data/skills/script-agent/execution/references/event-format.md b/data/skills/references/event-format.md similarity index 100% rename from data/skills/script-agent/execution/references/event-format.md rename to data/skills/references/event-format.md diff --git a/data/skills/universal-agent/references/novel-character-extract.md b/data/skills/references/novel-character-extract.md similarity index 100% rename from data/skills/universal-agent/references/novel-character-extract.md rename to data/skills/references/novel-character-extract.md diff --git a/data/skills/universal-agent/references/novel-props-extract.md b/data/skills/references/novel-props-extract.md similarity index 100% rename from data/skills/universal-agent/references/novel-props-extract.md rename to data/skills/references/novel-props-extract.md diff --git a/data/skills/universal-agent/references/novel-scene-extract.md b/data/skills/references/novel-scene-extract.md similarity index 100% rename from data/skills/universal-agent/references/novel-scene-extract.md rename to data/skills/references/novel-scene-extract.md diff --git a/data/skills/script-agent/decision/references/pipeline.md b/data/skills/references/pipeline.md similarity index 100% rename from data/skills/script-agent/decision/references/pipeline.md rename to data/skills/references/pipeline.md diff --git a/data/skills/production-agent/decision/references/plan.md b/data/skills/references/plan.md similarity index 100% rename from data/skills/production-agent/decision/references/plan.md rename to data/skills/references/plan.md diff --git a/data/skills/script-agent/supervision/references/quality-criteria.md b/data/skills/references/quality-criteria.md similarity index 100% rename from data/skills/script-agent/supervision/references/quality-criteria.md rename to data/skills/references/quality-criteria.md diff --git a/data/skills/script-agent/execution/references/script-format.md b/data/skills/references/script-format.md similarity index 100% rename from data/skills/script-agent/execution/references/script-format.md rename to data/skills/references/script-format.md diff --git a/data/skills/script-agent/execution/references/skeleton-format.md b/data/skills/references/skeleton-format.md similarity index 100% rename from data/skills/script-agent/execution/references/skeleton-format.md rename to data/skills/references/skeleton-format.md diff --git a/data/skills/production-agent/execution/references/storyboard-generation.md b/data/skills/references/storyboard-generation.md similarity index 100% rename from data/skills/production-agent/execution/references/storyboard-generation.md rename to data/skills/references/storyboard-generation.md diff --git a/data/skills/universal-agent/references/video-dialogue-extract.md b/data/skills/references/video-dialogue-extract.md similarity index 100% rename from data/skills/universal-agent/references/video-dialogue-extract.md rename to data/skills/references/video-dialogue-extract.md diff --git a/data/skills/script-agent/decision/SKILL.md b/data/skills/script_agent_decision.md similarity index 100% rename from data/skills/script-agent/decision/SKILL.md rename to data/skills/script_agent_decision.md diff --git a/data/skills/script-agent/execution/SKILL.md b/data/skills/script_agent_execution.md similarity index 100% rename from data/skills/script-agent/execution/SKILL.md rename to data/skills/script_agent_execution.md diff --git a/data/skills/script-agent/supervision/SKILL.md b/data/skills/script_agent_supervision.md similarity index 100% rename from data/skills/script-agent/supervision/SKILL.md rename to data/skills/script_agent_supervision.md diff --git a/data/skills/universal-agent/SKILL.md b/data/skills/universal_agent.md similarity index 100% rename from data/skills/universal-agent/SKILL.md rename to data/skills/universal_agent.md diff --git a/skillList.json b/skillList.json new file mode 100644 index 0000000..563d0de --- /dev/null +++ b/skillList.json @@ -0,0 +1,58 @@ +[ + { + "skillId": "52c51fa8655f899a1b7aae9b6aad7251", + "attribution": "universal-agent.md" + }, + { + "skillId": "6d46cdca10b2f49e07e515885d1387a0", + "attribution": "universal-agent.md" + }, + { + "skillId": "1864df75d1d65f76e275046649ecaef8", + "attribution": "universal-agent.md" + }, + { + "skillId": "3e5efec258c8d8e6a39bcef12f8ee058", + "attribution": "universal-agent.md" + }, + { + "skillId": "7fbce6f90d7d85496ba9817e9622e640", + "attribution": "universal-agent.md" + }, + { + "skillId": "31fb5c5a1f514ec1e66b4eba9f22d4db", + "attribution": "script_agent_decision.md" + }, + { + "skillId": "27dc2dfc901de2180227d0269217583a", + "attribution": "script_agent_execution.md" + }, + { + "skillId": "d49fa09504fe784a8e6eb102756c6d56", + "attribution": "script_agent_execution.md" + }, + { + "skillId": "797906c2ddf0750f050bcdeae23eae3d", + "attribution": "script_agent_execution.md" + }, + { + "skillId": "1abd8675c0c3e62b20c0b151d2ec0fb1", + "attribution": "script_agent_execution.md" + }, + { + "skillId": "0b7828d7a6ab458a4b201122f08d6c16", + "attribution": "script_agent_supervision.md" + }, + { + "skillId": "5c1772b5f9c420d9eae9ca02914ba087", + "attribution": "production_agent_decision.md" + }, + { + "skillId": "75a45cf996015ca819582873887ec301", + "attribution": "production_agent_execution.md" + }, + { + "skillId": "fce75f69d704c19bebcb356bc1bd6e81", + "attribution": "production_agent_execution.md" + } +] \ No newline at end of file diff --git a/src/agents/productionAgent/index.ts b/src/agents/productionAgent/index.ts index 9899a39..eb9553d 100644 --- a/src/agents/productionAgent/index.ts +++ b/src/agents/productionAgent/index.ts @@ -38,7 +38,7 @@ export async function decisionAI(ctx: AgentContext) { const { isolationKey, text, abortSignal } = ctx; const memory = new Memory("productionAgent", isolationKey); await memory.add("user", text); - const [skill, mem] = await Promise.all([useSkill("production-agent", "decision"), memory.get(text)]); + const [skill, mem] = await Promise.all([useSkill("production_agent_decision.md"), memory.get(text)]); const systemPrompt = buildSystemPrompt(skill.prompt, mem); @@ -70,7 +70,7 @@ export async function executionAI(ctx: AgentContext) { resTool.systemMessage("执行层AI 接管聊天"); const memory = new Memory("productionAgent", isolationKey); - const [skill, mem] = await Promise.all([useSkill("production-agent", "execution"), memory.get(text)]); + const [skill, mem] = await Promise.all([useSkill("production_agent_execution.md"), memory.get(text)]); const systemPrompt = buildSystemPrompt(skill.prompt, mem); @@ -94,7 +94,7 @@ export async function executionAI(ctx: AgentContext) { export async function supervisionAI(ctx: AgentContext) { const { isolationKey, text, abortSignal } = ctx; const memory = new Memory("productionAgent", isolationKey); - const [skill, mem] = await Promise.all([useSkill("production-agent", "supervision"), memory.get(text)]); + const [skill, mem] = await Promise.all([useSkill("production_agent_supervision.md"), memory.get(text)]); const systemPrompt = buildSystemPrompt(skill.prompt, mem); diff --git a/src/agents/productionAgent/tools.ts b/src/agents/productionAgent/tools.ts index 4cbb36c..7cf6d69 100644 --- a/src/agents/productionAgent/tools.ts +++ b/src/agents/productionAgent/tools.ts @@ -277,7 +277,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => { execute: async ({ images }) => { console.log("[tools] generated_assets", images); - const skill = await useSkill("universal-agent"); + const skill = await useSkill("universal_agent.md"); for (const item of images) { resTool.systemMessage(`生在生成分镜 id:${item.id} 图片`); //更新对应分镜状态 @@ -335,7 +335,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => { `, inputSchema: z.object({ images: z.array(z.object({ assetId: z.number(), prompt: z.string() })) }), execute: async ({ images }) => { - const skill = await useSkill("universal-agent"); + const skill = await useSkill("universal_agent.md"); //获取所设置模型 const imageModel = resTool.data.imageModel; for (const item of images) { diff --git a/src/agents/scriptAgent/index.ts b/src/agents/scriptAgent/index.ts index 4b4c196..d94555a 100644 --- a/src/agents/scriptAgent/index.ts +++ b/src/agents/scriptAgent/index.ts @@ -42,7 +42,7 @@ export async function decisionAI(ctx: AgentContext) { const memory = new Memory("scriptAgent", isolationKey); console.log("%c Line:43 🥟 isolationKey", "background:#4fff4B", isolationKey); await memory.add("user", text); - const [skill, mem] = await Promise.all([useSkill("script-agent", "decision"), memory.get(text)]); + const [skill, mem] = await Promise.all([useSkill("script_agent_decision.md"), memory.get(text)]); const systemPrompt = buildSystemPrompt(skill.prompt, mem); @@ -87,7 +87,7 @@ export async function executionAI(ctx: AgentContext) { resTool.systemMessage("执行层AI 接管聊天"); const memory = new Memory("scriptAgent", isolationKey); - const [skill, mem] = await Promise.all([useSkill("script-agent", "execution"), memory.get(text)]); + const [skill, mem] = await Promise.all([useSkill("script_agent_execution.md"), memory.get(text)]); const systemPrompt = buildSystemPrompt(skill.prompt, mem); @@ -114,7 +114,7 @@ export async function supervisionAI(ctx: AgentContext) { resTool.systemMessage("监督层AI 接管聊天"); const memory = new Memory("scriptAgent", isolationKey); - const [skill, mem] = await Promise.all([useSkill("script-agent", "supervision"), memory.get(text)]); + const [skill, mem] = await Promise.all([useSkill("script_agent_supervision.md"), memory.get(text)]); const systemPrompt = buildSystemPrompt(skill.prompt, mem); diff --git a/src/app.ts b/src/app.ts index 297bb14..3458637 100644 --- a/src/app.ts +++ b/src/app.ts @@ -17,7 +17,6 @@ const app = express(); const server = http.createServer(app); export default async function startServe(randomPort: Boolean = false) { - console.log("%c Line:20 🍰 randomPort", "background:#b03734", randomPort); const io = new Server(server, { cors: { origin: "*" } }); socketInit(io); diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index cef93a0..179c877 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -1,5 +1,7 @@ import { Knex } from "knex"; import { v4 as uuid } from "uuid"; +import { getEmbedding } from "@/utils/agent/embedding"; + interface TableSchema { name: string; builder: (table: Knex.CreateTableBuilder) => void; @@ -396,24 +398,6 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => table.unique(["id"]); }, }, - //记忆表(message=原始消息, summary=压缩摘要) - { - name: "memories", - builder: (table) => { - table.text("id").notNullable(); - table.text("isolationKey").notNullable(); // 记忆隔离键 - table.text("type").notNullable(); // 'message' | 'summary' - table.text("role"); // 'user' | 'assistant' - table.text("content").notNullable(); - table.text("embedding"); // 向量嵌入 JSON - table.text("relatedMessageIds"); // summary关联的message id列表 JSON - table.integer("summarized").defaultTo(0); // message是否已被总结 0/1 - table.integer("createTime").notNullable(); - table.primary(["id"]); - table.index(["isolationKey", "type"]); - table.index(["isolationKey", "summarized"]); - }, - }, //图片工作流表 { name: "o_imageFlow", @@ -444,6 +428,386 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => table.unique(["scriptId", "assetId"]); }, }, + { + name: "o_skillList", + builder: (table) => { + table.text("id").notNullable(); + table.text("md5").notNullable(); + table.text("path").notNullable(); + table.text("name").notNullable(); //文件名 + table.text("description").notNullable(); //描述 + table.text("embedding"); // 向量嵌入 JSON + table.text("type").notNullable(); // "main" | "references" + table.integer("createTime").notNullable(); + table.integer("updateTime").notNullable(); + table.integer("state").notNullable(); // 1正常,0正在生成description,-1description为空。-2归属为空,-3md5变动 + table.primary(["id"]); + }, + initData: async (knex) => { + const list = [ + { + id: "4fb36012e56e395b425569987f5dab0e", + md5: "fca3c269c5f325a65dafa663c9bb9773", + path: "production_agent_decision.md", + name: "production_agent_decision", + description: "", + embedding: "", + type: "main", + createTime: 1774447310118, + updateTime: 1774447310118, + state: -1, + }, + { + id: "017b6338d7aa227cd614ec1fb25fd83e", + md5: "2610b80abe4bd048fe61c73adc7388ac", + path: "production_agent_execution.md", + name: "production_agent_execution", + description: "", + embedding: "", + type: "main", + createTime: 1774447310118, + updateTime: 1774447310118, + state: -1, + }, + { + id: "f03c8e67b61580de9ea5b9d166521b67", + md5: "d41d8cd98f00b204e9800998ecf8427e", + path: "production_agent_supervision.md", + name: "production_agent_supervision", + description: "", + embedding: "", + type: "main", + createTime: 1774447310118, + updateTime: 1774447310118, + state: -1, + }, + { + id: "50b49d8af5d364665b463c23f6a4d8bb", + md5: "fbba66e0df2426996277b299710c3033", + path: "script_agent_decision.md", + name: "script_agent_decision", + description: "", + embedding: "", + type: "main", + createTime: 1774447310118, + updateTime: 1774447310118, + state: -1, + }, + { + id: "427727727e1095c54b6840cd21382d82", + md5: "7e5911242af7233854d533278c6a8ccb", + path: "script_agent_execution.md", + name: "script_agent_execution", + description: "", + embedding: "", + type: "main", + createTime: 1774447310118, + updateTime: 1774447310118, + state: -1, + }, + { + id: "02848fb0dd582fd926502c77ecf9679c", + md5: "7a8b6a311b015cd47bf17cc52b935348", + path: "script_agent_supervision.md", + name: "script_agent_supervision", + description: "", + embedding: "", + type: "main", + createTime: 1774447310118, + updateTime: 1774447310118, + state: -1, + }, + { + id: "a1e818cc03a0b355b239ac1fb0512969", + md5: "1fd22029e8047aa30b0dfd703cb837ed", + path: "universal-agent.md", + name: "universal-agent", + description: "", + embedding: "", + type: "main", + createTime: 1774447310118, + updateTime: 1774447310118, + state: -1, + }, + { + id: "3e5efec258c8d8e6a39bcef12f8ee058", + md5: "efccb0464cfd472861b49ebf737d4820", + path: "references/event-extract.md", + name: "event-extract", + description: + "专为小说改编短剧设计的文本分析助手,逐章提取涉及角色、核心事件、主线关系、信息密度、预估集长及情绪强度等结构化信息,以Markdown表格形式输出,并附汇总统计,辅助短剧制作的内容规划与时长估算。", + embedding: "", + type: "references", + createTime: 1774447310118, + updateTime: 1774450165911, + state: 1, + }, + { + id: "52c51fa8655f899a1b7aae9b6aad7251", + md5: "783678aaab829b34e7c30a414c356bf6", + path: "references/novel-character-extract.md", + name: "novel-character-extract", + description: + "专为小说内容分析设计的角色提取助手,从原文中识别并结构化输出所有重要角色的视觉描述信息,包括外貌、服饰、体态、状态变体等字段,供美术制作和AI角色图生成使用。", + embedding: "", + type: "references", + createTime: 1774447310118, + updateTime: 1774450080903, + state: 1, + }, + { + id: "6d46cdca10b2f49e07e515885d1387a0", + md5: "10544d12c4ef011e6b3b63a99b8c7fa8", + path: "references/novel-props-extract.md", + name: "novel-props-extract", + description: + "专注于从小说原文中提取道具物品信息的分析助手,能识别武器、法器、药物等各类道具,生成包含外观、材质、尺寸、功能及状态变体的结构化视觉描述表格,供美术制作和AI绘图使用。", + embedding: "", + type: "references", + createTime: 1774447310118, + updateTime: 1774450094771, + state: 1, + }, + { + id: "1864df75d1d65f76e275046649ecaef8", + md5: "65603aa495a541f54c55b7f30e149f45", + path: "references/novel-scene-extract.md", + name: "novel-scene-extract", + description: + "专注于从小说原文中提取并结构化场景信息的分析助手,可识别各类场景地点,输出包含空间描述、光照氛围、关键陈设、色调基调等字段的标准化场景资产表,用于美术制作和AI绘图的场景概念图生成。", + embedding: "", + type: "references", + createTime: 1774447310118, + updateTime: 1774450161878, + state: 1, + }, + { + id: "7fbce6f90d7d85496ba9817e9622e640", + md5: "830559e8f2cd5d0fa8e6df48a164fe2d", + path: "references/video-dialogue-extract.md", + name: "video-dialogue-extract", + description: + "这是一个专门从视频分镜提示词中提取结构化台词、旁白与音效信息的AI助手配置文档,定义了完整的输出格式(含镜号、角色、台词类型、表演指导等字段)、提取规则及处理流程,用于将视频分镜描述转化为标准化台词表。", + embedding: "", + type: "references", + createTime: 1774447310118, + updateTime: 1774450180712, + state: 1, + }, + { + id: "31fb5c5a1f514ec1e66b4eba9f22d4db", + md5: "43e63450efe0c9af8a3a40b036d36cb4", + path: "references/pipeline.md", + name: "pipeline", + description: + "面向短剧改编项目的四阶段流水线说明文档,涵盖事件提取、故事骨架、改编策略、剧本编写的串行执行流程,定义了决策层、执行层、监督层的协作规范及派发、审核、修复的交互格式与质量门控标准。", + embedding: "", + type: "references", + createTime: 1774451946248, + updateTime: 1774451984533, + state: 1, + }, + { + id: "27dc2dfc901de2180227d0269217583a", + md5: "7d353be4bab7a794436d9abff2b9c6ee", + path: "references/adaptation-format.md", + name: "adaptation-format", + description: + "本文档规定了改编策略输出的标准格式,包括核心改编原则、删除决策和世界观呈现策略三大模块的书写规范,明确各模块所需涵盖的维度与要素,用于指导竖屏短剧等载体的文学改编工作。", + embedding: "", + type: "references", + createTime: 1774452010535, + updateTime: 1774452022083, + state: 1, + }, + { + id: "d49fa09504fe784a8e6eb102756c6d56", + md5: "2ef08a7479f29d74986999ceb02092c8", + path: "references/event-format.md", + name: "event-format", + description: + "本文档规定了影视改编项目中事件表的标准输出格式,包括文件头、事件表格、各字段填写规范(章节、角色、核心事件、主线关系、情绪强度、预估时长)及汇总统计模板,用于指导从原著提取事件并评估改编集数与压缩比的第一阶段工作。", + embedding: "", + type: "references", + createTime: 1774452010535, + updateTime: 1774452030858, + state: 1, + }, + { + id: "797906c2ddf0750f050bcdeae23eae3d", + md5: "f5e7fe6db7e05db69d5dc327c4c538f2", + path: "references/script-format.md", + name: "script-format", + description: + "本文档为竖屏短剧剧本的输出格式规范,定义了文件头、节拍结构、分镜脚本、画面描述、台词、转场标注等标准格式要求,并附有时长控制参数与自查清单,供AI视频生成和导演制作使用。", + embedding: "", + type: "references", + createTime: 1774452010535, + updateTime: 1774452042934, + state: 1, + }, + { + id: "1abd8675c0c3e62b20c0b151d2ec0fb1", + md5: "a587532c737ce15022e1522021f099bb", + path: "references/skeleton-format.md", + name: "skeleton-format", + description: + "本文档定义了故事骨架文件(skeleton.md)的标准化输出格式,涵盖故事核、人物成长隐线、三幕结构、分集决策模板、全局删减记录、付费卡点设计及自查清单,用于指导编剧将章节事件列表转化为结构完整的剧集改编方案。", + embedding: "", + type: "references", + createTime: 1774452010535, + updateTime: 1774452057184, + state: 1, + }, + { + id: "0b7828d7a6ab458a4b201122f08d6c16", + md5: "120b3c856f1b2a8a429e11319e8c95fe", + path: "references/quality-criteria.md", + name: "quality-criteria", + description: + "本文档为影视/短剧项目的质量审核标准手册,涵盖事件表、故事骨架、改编策略和剧本四大模块的详细审核规则,规定了格式规范、角色名称统一、时长合理性、画面可执行性及场景氛围一致性等审核要求,用于确保各阶段产出物的内容准确性与制作可行性。", + embedding: "", + type: "references", + createTime: 1774452068093, + updateTime: 1774452087877, + state: 1, + }, + { + id: "5c1772b5f9c420d9eae9ca02914ba087", + md5: "c710ab7d237e1f0c5aa3d208e0f5b484", + path: "references/plan.md", + name: "plan", + description: + "该文档定义了AI代理生成执行计划的规范,包括任务总览、步骤列表(含编号、名称、详细内容、预期输出及依赖关系)和执行顺序标注,并提供标准回复模板,用于将用户需求拆解为可直接传入子代理工具执行的具体步骤。", + embedding: "", + type: "references", + createTime: 1774452098447, + updateTime: 1774452109574, + state: 1, + }, + { + id: "75a45cf996015ca819582873887ec301", + md5: "6045d76873fd58b8b87a914a21a38439", + path: "references/derive-assets-extraction.md", + name: "derive-assets-extraction", + description: + "本文档是一份技术操作指南,说明如何根据剧本内容和已有资产列表,提取每个资产在剧情中出现的不同视觉状态变体(derive),并通过工具函数读取和写入数据,用于后续图片生成参考。", + embedding: "", + type: "references", + createTime: 1774452119499, + updateTime: 1774452129516, + state: 1, + }, + { + id: "fce75f69d704c19bebcb356bc1bd6e81", + md5: "a3b3432854970f22949ba47236a6532f", + path: "references/storyboard-generation.md", + name: "storyboard-generation", + description: + "根据剧本和资产列表生成结构化分镜面板的工具指南,涵盖分镜拆分原则、字段填写规范及工具调用流程,用于将剧本转化为含画面描述、镜头语言、台词和AI绘图提示词的分镜数据。", + embedding: "", + type: "references", + createTime: 1774452119499, + updateTime: 1774452140873, + state: 1, + }, + ]; + await Promise.all( + list.map(async (item) => { + const embedding = await getEmbedding(item.description); + item.embedding = JSON.stringify(embedding); + }), + ); + await knex("o_skillList").insert(list); + }, + }, + { + name: "o_skillAttribution", + builder: (table) => { + table.text("skillId").notNullable().references("id").inTable("o_skillList").onDelete("CASCADE"); + table.text("attribution").notNullable(); // "production_agent_decision.md" | "production_agent_execution.md" | "production_agent_supervision.md" | "script_agent_decision.md" | "script_agent_execution.md" | "script_agent_supervision.md" | "universal-agent.md" + table.primary(["skillId", "attribution"]); + table.index(["attribution"]); + }, + initData: async (knex) => { + await knex("o_skillAttribution").insert([ + { + skillId: "52c51fa8655f899a1b7aae9b6aad7251", + attribution: "universal-agent.md", + }, + { + skillId: "6d46cdca10b2f49e07e515885d1387a0", + attribution: "universal-agent.md", + }, + { + skillId: "1864df75d1d65f76e275046649ecaef8", + attribution: "universal-agent.md", + }, + { + skillId: "3e5efec258c8d8e6a39bcef12f8ee058", + attribution: "universal-agent.md", + }, + { + skillId: "7fbce6f90d7d85496ba9817e9622e640", + attribution: "universal-agent.md", + }, + { + skillId: "31fb5c5a1f514ec1e66b4eba9f22d4db", + attribution: "script_agent_decision.md", + }, + { + skillId: "27dc2dfc901de2180227d0269217583a", + attribution: "script_agent_execution.md", + }, + { + skillId: "d49fa09504fe784a8e6eb102756c6d56", + attribution: "script_agent_execution.md", + }, + { + skillId: "797906c2ddf0750f050bcdeae23eae3d", + attribution: "script_agent_execution.md", + }, + { + skillId: "1abd8675c0c3e62b20c0b151d2ec0fb1", + attribution: "script_agent_execution.md", + }, + { + skillId: "0b7828d7a6ab458a4b201122f08d6c16", + attribution: "script_agent_supervision.md", + }, + { + skillId: "5c1772b5f9c420d9eae9ca02914ba087", + attribution: "production_agent_decision.md", + }, + { + skillId: "75a45cf996015ca819582873887ec301", + attribution: "production_agent_execution.md", + }, + { + skillId: "fce75f69d704c19bebcb356bc1bd6e81", + attribution: "production_agent_execution.md", + }, + ]); + }, + }, + //记忆表(message=原始消息, summary=压缩摘要) + { + name: "memories", + builder: (table) => { + table.text("id").notNullable(); + table.text("isolationKey").notNullable(); // 记忆隔离键 + table.text("type").notNullable(); // 'message' | 'summary' + table.text("role"); // 'user' | 'assistant' + table.text("content").notNullable(); + table.text("embedding"); // 向量嵌入 JSON + table.text("relatedMessageIds"); // summary关联的message id列表 JSON + table.integer("summarized").defaultTo(0); // message是否已被总结 0/1 + table.integer("createTime").notNullable(); + table.primary(["id"]); + table.index(["isolationKey", "type"]); + table.index(["isolationKey", "summarized"]); + }, + }, ]; for (const t of tables) { diff --git a/src/router.ts b/src/router.ts index bac9636..1eb3676 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash bf3c43509342cfaa6f58c3551570331d +// @routes-hash 62dafbea4285d1f7bd1a6acf5ddbfacc import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -76,15 +76,22 @@ import route72 from "./routes/setting/loginConfig/getUser"; import route73 from "./routes/setting/loginConfig/updateUserPwd"; import route74 from "./routes/setting/memoryConfig/getMemory"; import route75 from "./routes/setting/memoryConfig/sureMemory"; -import route76 from "./routes/setting/vendorConfig/addVendor"; -import route77 from "./routes/setting/vendorConfig/deleteVendor"; -import route78 from "./routes/setting/vendorConfig/getVendorList"; -import route79 from "./routes/setting/vendorConfig/modelTest"; -import route80 from "./routes/setting/vendorConfig/updateVendor"; -import route81 from "./routes/task/getTaskApi"; -import route82 from "./routes/task/getTaskCategories"; -import route83 from "./routes/task/taskDetails"; -import route84 from "./routes/test/test"; +import route76 from "./routes/setting/skillManagement/addSkill"; +import route77 from "./routes/setting/skillManagement/deleteSkill"; +import route78 from "./routes/setting/skillManagement/embeddingSkill"; +import route79 from "./routes/setting/skillManagement/generateDescription"; +import route80 from "./routes/setting/skillManagement/getSkillList"; +import route81 from "./routes/setting/skillManagement/scanSkills"; +import route82 from "./routes/setting/skillManagement/updateSkill"; +import route83 from "./routes/setting/vendorConfig/addVendor"; +import route84 from "./routes/setting/vendorConfig/deleteVendor"; +import route85 from "./routes/setting/vendorConfig/getVendorList"; +import route86 from "./routes/setting/vendorConfig/modelTest"; +import route87 from "./routes/setting/vendorConfig/updateVendor"; +import route88 from "./routes/task/getTaskApi"; +import route89 from "./routes/task/getTaskCategories"; +import route90 from "./routes/task/taskDetails"; +import route91 from "./routes/test/test"; export default async (app: Express) => { app.use("/api/agents/clearMemory", route1); @@ -162,13 +169,20 @@ export default async (app: Express) => { app.use("/api/setting/loginConfig/updateUserPwd", route73); app.use("/api/setting/memoryConfig/getMemory", route74); app.use("/api/setting/memoryConfig/sureMemory", route75); - app.use("/api/setting/vendorConfig/addVendor", route76); - app.use("/api/setting/vendorConfig/deleteVendor", route77); - app.use("/api/setting/vendorConfig/getVendorList", route78); - app.use("/api/setting/vendorConfig/modelTest", route79); - app.use("/api/setting/vendorConfig/updateVendor", route80); - app.use("/api/task/getTaskApi", route81); - app.use("/api/task/getTaskCategories", route82); - app.use("/api/task/taskDetails", route83); - app.use("/api/test/test", route84); + app.use("/api/setting/skillManagement/addSkill", route76); + app.use("/api/setting/skillManagement/deleteSkill", route77); + app.use("/api/setting/skillManagement/embeddingSkill", route78); + app.use("/api/setting/skillManagement/generateDescription", route79); + app.use("/api/setting/skillManagement/getSkillList", route80); + app.use("/api/setting/skillManagement/scanSkills", route81); + app.use("/api/setting/skillManagement/updateSkill", route82); + app.use("/api/setting/vendorConfig/addVendor", route83); + app.use("/api/setting/vendorConfig/deleteVendor", route84); + app.use("/api/setting/vendorConfig/getVendorList", route85); + app.use("/api/setting/vendorConfig/modelTest", route86); + app.use("/api/setting/vendorConfig/updateVendor", route87); + app.use("/api/task/getTaskApi", route88); + app.use("/api/task/getTaskCategories", route89); + app.use("/api/task/taskDetails", route90); + app.use("/api/test/test", route91); } diff --git a/src/routes/setting/skillManagement/addSkill.ts b/src/routes/setting/skillManagement/addSkill.ts new file mode 100644 index 0000000..05802ae --- /dev/null +++ b/src/routes/setting/skillManagement/addSkill.ts @@ -0,0 +1,102 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import fs from "fs/promises"; +import path from "path"; +import crypto from "crypto"; + +const router = express.Router(); + +const buildSkillFileName = (name: string) => { + const trimmed = name.trim(); + const fileName = trimmed.endsWith(".md") ? trimmed : `${trimmed}.md`; + const normalized = fileName.replace(/\\/g, "/"); + if (!normalized || normalized.includes("/")) { + throw new Error("技能名称不能包含路径分隔符"); + } + return normalized; +}; + +const buildRelativePath = (type: "main" | "references", fileName: string) => { + return type === "references" ? path.posix.join("references", fileName) : fileName; +}; + +const resolveSkillFilePath = (relativePath: string) => { + const normalizedPath = relativePath.replace(/\\/g, "/"); + if (normalizedPath.startsWith("references/")) { + return path.join(u.getPath("skills"), normalizedPath); + } + return path.join(u.getPath("skills"), normalizedPath); +}; + +const resolveState = (description: string, attributions: string[]) => { + if (!description.trim()) return -1; + if (attributions.length === 0) return -2; + return 1; +}; + +export default router.post( + "/", + validateFields({ + name: z.string().min(1).max(100), + description: z.string().optional(), + content: z.string().optional(), + attributions: z.array(z.string()).optional(), + type: z.enum(["main", "references"]).optional(), + }), + async (req, res) => { + try { + const { name, description, content, attributions, type } = req.body; + const finalType: "main" | "references" = type === "main" ? "main" : "references"; + const finalDescription = description ?? ""; + const finalContent = content ?? ""; + const rawAttributions = Array.isArray(attributions) ? attributions : []; + const finalAttributions = Array.from( + new Set(rawAttributions.filter((item: unknown): item is string => typeof item === "string" && item.trim().length > 0)), + ); + const fileName = buildSkillFileName(name); + const relativePath = buildRelativePath(finalType, fileName); + const skillId = crypto.createHash("md5").update(relativePath).digest("hex"); + const md5 = crypto.createHash("md5").update(finalContent).digest("hex"); + const filePath = resolveSkillFilePath(relativePath); + const now = Date.now(); + + const existed = await u.db("o_skillList").where("id", skillId).first(); + if (existed) { + return res.status(400).send(error("技能已存在,请使用其他名称")); + } + + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, finalContent, "utf-8"); + + await u.db("o_skillList").insert({ + id: skillId, + md5, + path: relativePath, + name: path.basename(fileName, ".md"), + description: finalDescription, + embedding: null, + type: finalType, + createTime: now, + updateTime: now, + state: resolveState(finalDescription, finalAttributions), + }); + + if (finalAttributions.length > 0) { + await u.db("o_skillAttribution").insert( + finalAttributions.map((attribution: string) => ({ + skillId, + attribution, + })), + ); + } + + res.status(200).send(success("新增技能成功")); + } catch (err: any) { + console.log(err); + res.status(400).send(error(err?.message || "新增技能失败")); + } + }, +); diff --git a/src/routes/setting/skillManagement/deleteSkill.ts b/src/routes/setting/skillManagement/deleteSkill.ts new file mode 100644 index 0000000..e798ff7 --- /dev/null +++ b/src/routes/setting/skillManagement/deleteSkill.ts @@ -0,0 +1,49 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import fs from "fs/promises"; +import path from "path"; + +const router = express.Router(); + +const resolveSkillFilePath = (type: string, relativePath: string) => { + const normalizedPath = (relativePath || "").replace(/\\/g, "/"); + const isPrefixedReferencePath = normalizedPath.startsWith("references/"); + if (type === "references" && !isPrefixedReferencePath) { + return path.join(u.getPath(["skills", "references"]), normalizedPath); + } + return path.join(u.getPath("skills"), normalizedPath); +}; + +export default router.post( + "/", + validateFields({ + id: z.string().min(1), + }), + async (req, res) => { + try { + const { id } = req.body; + const skill = await u.db("o_skillList").where("id", id).first(); + + if (!skill) { + return res.status(404).send(error("技能不存在")); + } + + const filePath = resolveSkillFilePath(skill.type, skill.path || ""); + await u.db("o_skillList").where("id", id).delete(); + + try { + await fs.unlink(filePath); + } catch { + // 文件不存在时可忽略,数据库记录已删除 + } + + res.status(200).send(success("删除技能成功")); + } catch (err: any) { + console.log(err); + res.status(400).send(error(err?.message || "删除技能失败")); + } + }, +); diff --git a/src/routes/setting/skillManagement/embeddingSkill.ts b/src/routes/setting/skillManagement/embeddingSkill.ts new file mode 100644 index 0000000..7c1e88c --- /dev/null +++ b/src/routes/setting/skillManagement/embeddingSkill.ts @@ -0,0 +1,31 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import { getEmbedding } from "@/utils/agent/embedding"; + +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + id: z.string(), + }), + async (req, res) => { + const { id } = req.body; + + const skill = await u.db("o_skillList").where("id", id).first(); + + if (!skill) return res.status(404).send(error("技能不存在")); + if (skill.embedding) return res.status(400).send(error("技能已存在向量,请勿重复生成")); + if (!skill.description) return res.status(400).send(error("技能描述不存在")); + const embedding = await getEmbedding(skill.description); + await u + .db("o_skillList") + .where("id", id) + .update({ embedding: JSON.stringify(embedding) }); + + res.status(200).send(success("技能向量生成成功")); + }, +); diff --git a/src/routes/setting/skillManagement/generateDescription.ts b/src/routes/setting/skillManagement/generateDescription.ts new file mode 100644 index 0000000..98ac70d --- /dev/null +++ b/src/routes/setting/skillManagement/generateDescription.ts @@ -0,0 +1,35 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import fs from "fs/promises"; +import path from "path"; + +const router = express.Router(); + +const resolveSkillFilePath = (type: string, relativePath: string) => { + const normalizedPath = (relativePath || "").replace(/\\/g, "/"); + const isPrefixedReferencePath = normalizedPath.startsWith("references/"); + if (type === "references" && !isPrefixedReferencePath) { + return path.join(u.getPath(["skills", "references"]), normalizedPath); + } + return path.join(u.getPath("skills"), normalizedPath); +}; + +export default router.post( + "/", + validateFields({ + content: z.string(), + }), + async (req, res) => { + const { content } = req.body; + const result = await u.Ai.Text("universalAgent").invoke({ + system: + "你是一个文档摘要助手。根据给定的文档内容生成一句简洁的中文描述(不超过100字),概括文档的核心主题和用途。只输出描述文本,不要添加任何前缀或格式。", + messages: [{ role: "user", content: `内容:\n${content}` }], + }); + const description = result.text.trim(); + res.status(200).send(success(description)); + }, +); diff --git a/src/routes/setting/skillManagement/getSkillList.ts b/src/routes/setting/skillManagement/getSkillList.ts new file mode 100644 index 0000000..4dd0cf0 --- /dev/null +++ b/src/routes/setting/skillManagement/getSkillList.ts @@ -0,0 +1,92 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import fs from "fs"; +import path from "path"; + +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(20), + search: z.string().optional().default(""), + type: z.enum(["main", "references"]).optional(), + attributions: z.array(z.string()).optional(), + }), + async (req, res) => { + const { page, limit, search, type, attributions } = req.body; + const offset = (page - 1) * limit; + + let query = u.db("o_skillList"); + let countQuery = u.db("o_skillList"); + + // 搜索条件 + if (search) { + const searchPattern = `%${search}%`; + const whereBuilder = (builder: any) => { + builder.where("name", "like", searchPattern).orWhere("path", "like", searchPattern).orWhere("description", "like", searchPattern); + }; + query = query.where(whereBuilder); + countQuery = countQuery.where(whereBuilder); + } + + // 查询总数 + const [{ count }]: any = await countQuery.count("* as count"); + + // 查询列表 + if (type) { + query = query.where("type", type); + countQuery = countQuery.where("type", type); + } + if (attributions && attributions.length > 0) { + query = query.whereIn("id", function () { + this.select("skillId").from("o_skillAttribution").whereIn("attribution", attributions); + }); + countQuery = countQuery.whereIn("id", function () { + this.select("skillId").from("o_skillAttribution").whereIn("attribution", attributions); + }); + } + + const list = await query.select("*").orderBy("updateTime", "desc").orderBy("type", "desc").limit(limit).offset(offset); + + // 查询每个技能的归属 + const skillIds = list.map((item: any) => item.id); + const attributionsList = await u.db("o_skillAttribution").whereIn("skillId", skillIds).select("skillId", "attribution"); + + // 将归属信息合并到列表中 + const attributionMap = new Map(); + for (const attr of attributionsList) { + if (!attributionMap.has(attr.skillId!)) { + attributionMap.set(attr.skillId!, []); + } + attributionMap.get(attr.skillId!)!.push(attr.attribution!); + } + + const listWithAttributions = list.map((item: any) => { + const normalizedPath = (item.path || "").replace(/\\/g, "/"); + const isPrefixedReferencePath = normalizedPath.startsWith("references/"); + const skillFilePath = + item.type === "references" && !isPrefixedReferencePath + ? path.join(u.getPath(["skills", "references"]), item.path!) + : path.join(u.getPath("skills"), item.path!); + + return { + ...item, + attributions: attributionMap.get(item.id) || [], + content: fs.readFileSync(skillFilePath, "utf-8"), + embedding: item.embedding ? true : false, + }; + }); + + res.status(200).send( + success({ + list: listWithAttributions, + total: Number(count), + }), + ); + }, +); diff --git a/src/routes/setting/skillManagement/scanSkills.ts b/src/routes/setting/skillManagement/scanSkills.ts new file mode 100644 index 0000000..8208888 --- /dev/null +++ b/src/routes/setting/skillManagement/scanSkills.ts @@ -0,0 +1,121 @@ +import express from "express"; +import u from "@/utils"; +import path from "path"; +import fs from "fs/promises"; +import crypto from "crypto"; +import { success } from "@/lib/responseFormat"; +import fg from "fast-glob"; +import getPath from "@/utils/getPath"; + +const router = express.Router(); + +export default router.post("/", async (req, res) => { + const skillsRoot = getPath(["skills"]); + const referencesRoot = path.join(skillsRoot, "references"); + + const [mainEntries, referenceEntries] = await Promise.all([ + fg("*.md", { + cwd: skillsRoot.replace(/\\/g, "/"), + onlyFiles: true, + }), + fg("**/*.md", { + cwd: referencesRoot.replace(/\\/g, "/"), + onlyFiles: true, + }), + ]); + + const scanItems = [ + ...mainEntries.map((entry) => ({ + entry, + relativePath: entry, + fullPath: path.join(skillsRoot, entry), + type: "main", + })), + ...referenceEntries.map((entry) => ({ + entry, + relativePath: path.posix.join("references", entry.replace(/\\/g, "/")), + fullPath: path.join(referencesRoot, entry), + type: "references", + })), + ]; + + const now = Date.now(); + let insertedCount = 0; + let updatedCount = 0; + let removedCount = 0; + + const scannedIds = new Set(); + const existingRows = await u.db("o_skillList").whereIn("type", ["main", "references"]).select("id", "md5", "type", "path"); + + for (const item of scanItems) { + const id = crypto.createHash("md5").update(item.relativePath).digest("hex"); + const name = path.basename(item.entry, ".md"); + const content = await fs.readFile(item.fullPath, "utf-8"); + const md5 = crypto.createHash("md5").update(content).digest("hex"); + const existing = existingRows.find((row: any) => row.id === id); + + scannedIds.add(id); + + if (!existing) { + await u.db("o_skillList").insert({ + id, + path: item.relativePath, + name, + description: "", + embedding: null, + type: item.type, + createTime: now, + updateTime: now, + md5, + state: -1, + }); + insertedCount++; + continue; + } + + if (existing.md5 !== md5 || existing.path !== item.relativePath || existing.type !== item.type) { + await u.db("o_skillList").where("id", id).update({ + path: item.relativePath, + name, + md5, + type: item.type, + updateTime: now, + state: -3, + }); + updatedCount++; + } + } + + const removedIds = existingRows.map((row: any) => row.id).filter((id: string) => !scannedIds.has(id)); + if (removedIds.length > 0) { + await u.db("o_skillList").whereIn("id", removedIds).delete(); + removedCount = removedIds.length; + } + + const [{ noDescriptionSkillCount }]: any = await u + .db("o_skillList") + .where("type", "references") + .andWhere((builder: any) => { + builder.whereNull("description").orWhere("description", ""); + }) + .count({ noDescriptionSkillCount: "*" }); + + const [{ noAttributionSkillCount }]: any = await u + .db("o_skillList as sl") + .leftJoin("o_skillAttribution as sa", "sl.id", "sa.skillId") + .where("sl.type", "references") + .whereNull("sa.skillId") + .countDistinct({ noAttributionSkillCount: "sl.id" }); + + res.status(200).send( + success({ + message: "更新技能文档成功", + insertedCount, + updatedCount, + removedCount, + totalFiles: scanItems.length, + noDescriptionSkillCount: Number(noDescriptionSkillCount), + noAttributionSkillCount: Number(noAttributionSkillCount), + }), + ); +}); diff --git a/src/routes/setting/skillManagement/updateSkill.ts b/src/routes/setting/skillManagement/updateSkill.ts new file mode 100644 index 0000000..e389b24 --- /dev/null +++ b/src/routes/setting/skillManagement/updateSkill.ts @@ -0,0 +1,118 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import fs from "fs/promises"; +import path from "path"; +import crypto from "crypto"; + +const router = express.Router(); + +const buildSkillFileName = (name: string) => { + const trimmed = name.trim(); + const fileName = trimmed.endsWith(".md") ? trimmed : `${trimmed}.md`; + const normalized = fileName.replace(/\\/g, "/"); + if (!normalized || normalized.includes("/")) { + throw new Error("技能名称不能包含路径分隔符"); + } + return normalized; +}; + +const buildRelativePath = (type: string, fileName: string) => { + return type === "references" ? path.posix.join("references", fileName) : fileName; +}; + +const resolveSkillFilePath = (relativePath: string) => { + return path.join(u.getPath("skills"), relativePath.replace(/\\/g, "/")); +}; + +const resolveState = (description: string, attributions: string[]) => { + if (!description.trim()) return -1; + if (attributions.length === 0) return -2; + return 1; +}; + +export default router.post( + "/", + validateFields({ + id: z.string().min(1), + name: z.string().min(1).max(100), + description: z.string().optional(), + content: z.string().optional(), + attributions: z.array(z.string()).optional(), + }), + async (req, res) => { + try { + const { id, name, description, content, attributions } = req.body; + const current = await u.db("o_skillList").where("id", id).first(); + + if (!current) { + return res.status(404).send(error("技能不存在")); + } + + const finalDescription = description ?? ""; + const finalContent = content ?? ""; + const rawAttributions = Array.isArray(attributions) ? attributions : []; + const finalAttributions = Array.from( + new Set(rawAttributions.filter((item: unknown): item is string => typeof item === "string" && item.trim().length > 0)), + ); + const fileName = buildSkillFileName(name); + const relativePath = buildRelativePath(current.type, fileName); + const nextId = crypto.createHash("md5").update(relativePath).digest("hex"); + const md5 = crypto.createHash("md5").update(finalContent).digest("hex"); + const oldFilePath = resolveSkillFilePath(current.path); + const newFilePath = resolveSkillFilePath(relativePath); + const now = Date.now(); + + if (nextId !== id) { + const conflict = await u.db("o_skillList").where("id", nextId).first(); + if (conflict) { + return res.status(400).send(error("技能名称冲突,请使用其他名称")); + } + } + + await fs.mkdir(path.dirname(newFilePath), { recursive: true }); + if (oldFilePath !== newFilePath) { + try { + await fs.rename(oldFilePath, newFilePath); + } catch { + // 文件不存在时直接按新路径写入即可 + } + } + await fs.writeFile(newFilePath, finalContent, "utf-8"); + + if (nextId !== id) { + await u.db("o_skillAttribution").where("skillId", id).update({ skillId: nextId }); + } + + await u + .db("o_skillList") + .where("id", id) + .update({ + id: nextId, + path: relativePath, + name: path.basename(fileName, ".md"), + description: finalDescription, + md5, + updateTime: now, + state: resolveState(finalDescription, finalAttributions), + }); + + await u.db("o_skillAttribution").where("skillId", nextId).delete(); + if (finalAttributions.length > 0) { + await u.db("o_skillAttribution").insert( + finalAttributions.map((attribution: string) => ({ + skillId: nextId, + attribution, + })), + ); + } + + res.status(200).send(success("更新技能成功")); + } catch (err: any) { + console.log(err); + res.status(400).send(error(err?.message || "更新技能失败")); + } + }, +); diff --git a/src/routes/test/test.ts b/src/routes/test/test.ts index 91adc64..92b9ba3 100644 --- a/src/routes/test/test.ts +++ b/src/routes/test/test.ts @@ -1,4 +1,16 @@ import express from "express"; const router = express.Router(); +import u from "@/utils"; +import fs from "fs"; +import { useSkill } from "@/utils/agent/skillsTools"; -export default router.get("/", async (req, res) => {}); +export default router.get("/", async (req, res) => { + const skill = await useSkill("universal_agent.md"); + const result = await u.Ai.Text("universalAgent").invoke({ + system: "请直接调用activate_skill工具激活技能" + skill.prompt, + messages: [{ role: "user", content: `开始激活,然后随机使用read_skill_file调用一个技能` }], + tools: skill.tools, + }); + + res.send(result.text); +}); diff --git a/src/types/database.d.ts b/src/types/database.d.ts index a4b9c30..b6d8661 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,4 +1,4 @@ -// @db-hash 62a748aea9d1ecee865c4cf05add24fc +// @db-hash d7bc24a5440e2cc7136872da7ed6c4c7 //该文件由脚本自动生成,请勿手动修改 export interface memories { @@ -130,6 +130,22 @@ export interface o_setting { 'key'?: string | null; 'value'?: string | null; } +export interface o_skillAttribution { + 'attribution'?: string; + 'skillId'?: string; +} +export interface o_skillList { + 'createTime': number; + 'description': string; + 'embedding'?: string | null; + 'id'?: string; + 'md5': string; + 'name': string; + 'path': string; + 'state': number; + 'type': string; + 'updateTime': number; +} export interface o_storyboard { 'camera'?: string | null; 'createTime'?: number | null; @@ -149,11 +165,6 @@ export interface o_storyboard { 'state'?: string | null; 'title'?: string | null; } -export interface o_storyboardFlow { - 'flowData': string; - 'id'?: number; - 'storyboardId': number; -} export interface o_tasks { 'describe'?: string | null; 'id'?: number; @@ -223,8 +234,9 @@ export interface DB { "o_script": o_script; "o_scriptAssets": o_scriptAssets; "o_setting": o_setting; + "o_skillAttribution": o_skillAttribution; + "o_skillList": o_skillList; "o_storyboard": o_storyboard; - "o_storyboardFlow": o_storyboardFlow; "o_tasks": o_tasks; "o_user": o_user; "o_vendorConfig": o_vendorConfig; diff --git a/src/utils/agent/skillsTools.ts b/src/utils/agent/skillsTools.ts index 21077ea..628b0d0 100644 --- a/src/utils/agent/skillsTools.ts +++ b/src/utils/agent/skillsTools.ts @@ -3,7 +3,9 @@ import { z } from "zod"; import path from "path"; import fs from "fs/promises"; import isPathInside from "is-path-inside"; +import u from "@/utils"; import getPath from "@/utils/getPath"; +import { getEmbedding, cosineSimilarity } from "./embedding"; interface SkillRecord { name: string; @@ -54,118 +56,125 @@ function parseFrontmatter(content: string): { name: string; description: string return { name: result.name, description: result.description }; } -function stripFrontmatter(content: string): string { - return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim(); -} +type SkillAttribution = + | "production_agent_decision.md" + | "production_agent_execution.md" + | "production_agent_supervision.md" + | "script_agent_decision.md" + | "script_agent_execution.md" + | "script_agent_supervision.md" + | "universal_agent.md"; -// ==================== 资源枚举 ==================== +export async function useSkill(mainSkillName: SkillAttribution) { + const skillsRoot = getPath("skills"); + const targetSkill = path.join(skillsRoot, mainSkillName); + + if (!isPathInside(targetSkill, skillsRoot)) throw new Error("技能名称无效:检测到路径穿越"); -async function listResources(dir: string, base = ""): Promise { - let entries; try { - entries = await fs.readdir(dir, { withFileTypes: true }); + const content = await fs.readFile(targetSkill, "utf-8"); + const skill = { ...parseFrontmatter(content), location: targetSkill, baseDir: skillsRoot }; + return { prompt: buildPrompt(skill), tools: createSkillTools(skill, mainSkillName) }; } catch { - return []; - } - - const files: string[] = []; - for (const entry of entries) { - const rel = base ? `${base}/${entry.name}` : entry.name; - if (entry.isDirectory()) { - files.push(...(await listResources(path.join(dir, entry.name), rel))); - } else if (entry.name !== "SKILL.md") { - files.push(rel); - } - } - return files; -} - -// ==================== 读取单个技能 ==================== - -async function readSkillFromDir(skillDir: string): Promise { - const location = path.join(skillDir, "SKILL.md"); - let content: string; - try { - content = await fs.readFile(location, "utf-8"); - } catch { - return null; - } - try { - const meta = parseFrontmatter(content); - console.log(`[Skill] ✅ 发现技能:${meta.name} — ${meta.description}`); - return { ...meta, location, baseDir: skillDir }; - } catch (e) { - console.log(`[Skill] ⚠️ 解析失败 "${skillDir}":${(e as Error).message}`); - return null; + throw new Error(`技能文件不存在:${mainSkillName}`); } } -// ==================== 构建技能目录 ==================== - -function buildCatalog(skills: SkillRecord[]): string { - const entries = skills.map((s) => ` \n ${s.name}\n ${s.description}\n `).join("\n"); - +function buildPrompt(skill: SkillRecord): string { return `## Skills 以下技能提供了专业任务的专用指令。 当任务与某个技能的描述匹配时,调用 activate_skill 工具并传入技能名称来加载完整指令。 加载后遵循技能指令执行任务,需要时调用 read_skill_file 读取资源文件内容。 -${entries} + + ${skill.name} + ${skill.description} + `; } -// ==================== 激活 + 执行工具 ==================== - -function createSkillTools(skills: SkillRecord[]) { +function createSkillTools(skill: SkillRecord, mainSkillName: string) { const activated = new Set(); - const validNames = skills.map((s) => s.name); - return { activate_skill: tool({ - description: `激活一个技能,加载其完整指令和捆绑资源列表到上下文。可用技能:${validNames.join(", ")}`, + description: `激活一个技能,加载其完整指令和捆绑资源列表到上下文。可用技能:${skill.name}`, inputSchema: z.object({ - name: z.enum(validNames as [string, ...string[]]).describe("要激活的技能名称"), + name: z.enum([skill.name] as [string, ...string[]]).describe("要激活的技能名称"), }), execute: async ({ name }) => { - const skill = skills.find((s) => s.name === name); - if (!skill) { - console.log(`[Skill] ❌ 激活失败:未找到技能 "${name}"`); - return { error: `Skill '${name}' not found` }; - } if (activated.has(name)) { - console.log(`[Skill] ℹ️ 技能 "${name}" 已在当前会话中激活,跳过重复注入`); - return { already_active: true, message: `技能 "${name}" 已激活,无需重复加载` }; + console.log(`[Skill] ℹ️ 技能 "${name}" 已激活,跳过重复注入`); + return { alreadyActive: true, message: `技能 "${name}" 已激活,无需重复加载` }; } - - let content: string; + let raw: string; try { - content = await fs.readFile(skill.location, "utf-8"); + raw = await fs.readFile(skill.location, "utf-8"); } catch { console.log(`[Skill] ❌ 激活失败:无法读取 ${skill.location}`); - return { error: `Failed to read SKILL.md for '${name}'` }; + return { error: `无法读取技能文件:${name}` }; } + const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim(); + + const resources = await u + .db("o_skillList") + .distinct("o_skillList.path") + .innerJoin("o_skillAttribution", "o_skillList.id", "o_skillAttribution.skillId") + .where("o_skillList.state", 1) + .andWhere("o_skillAttribution.attribution", mainSkillName); - const body = stripFrontmatter(content); - const resources = await listResources(skill.baseDir); activated.add(name); - console.log(`[Skill] 📖 已激活技能:${skill.name}(${body.length} 字符,${resources.length} 个资源文件)`); - - const resourcesXml = - resources.length > 0 ? `\n\n${resources.map((f) => ` ${f}`).join("\n")}\n` : ""; - - return { - content: ` -${body} - -Skill directory: ${skill.baseDir} -相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。 -${resourcesXml} -`, - }; + console.log(`[Skill] 📖 已激活:${name}(${body.length} 字符,${resources.length} 资源)`); + let content = ""; + content = `\n`; + content += body + "\n\n"; + content += `Skill directory: ${skill.baseDir}\n`; + content += "相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。\n"; + if (resources.length > 0) { + content += "\n\n"; + for (const { path } of resources) { + content += ` ${path}\n`; + } + content += "\n"; + } + content += "\n\n"; + content += "- read_skill_file:读取上方 skill_resources 中列出的资源文件。\n"; + content += "- discover_skill_docs:当上方资源不足以完成任务时,使用关键词检索更多相关文档。传入与当前任务相关的关键词列表即可获取推荐。\n"; + content += "\n"; + content += ""; + console.log("%c Line:133 🍊 content", "background:#2eafb0", content); + return { content }; }, }), + discover_skill_docs: tool({ + description: "根据关键词主动发现全部技能文档(MD),返回相关度排序的推荐列表。适用于技能指令中未明确指定资源文件但需要补充信息的场景。", + inputSchema: z.object({ + keywords: z.array(z.string().max(100)).min(1).max(20).describe("用于检索技能文档的关键词列表"), + topK: z.number().int().min(1).max(20).default(5).describe("返回推荐文档数量"), + }), + execute: async ({ keywords, topK }) => { + const queryText = keywords.join(" "); + const queryVec = await getEmbedding(queryText); + const activeRows = await u.db("o_skillList").where("state", 1).whereNotNull("embedding").select(); + const scored = activeRows + .map((row) => { + const emb = JSON.parse(row.embedding!) as number[]; + return { + name: row.name, + filePath: row.path, + type: row.type, + description: row.description, + score: cosineSimilarity(queryVec, emb), + }; + }) + .sort((a, b) => b.score - a.score) + .slice(0, topK); + + console.log(`[Skill] ✅ discover_skill_docs 返回 ${scored.length} 条推荐`); + return { recommendations: scored }; + }, + }), read_skill_file: tool({ description: "读取已激活技能目录下的资源文件。传入 activate_skill 返回的 skill_resources 中的文件路径。", inputSchema: z.object({ @@ -173,18 +182,11 @@ ${resourcesXml} filePath: z.string().describe("资源文件的相对路径,来自 activate_skill 返回的 skill_resources"), }), execute: async ({ skillName, filePath: relPath }) => { - const skill = skills.find((s) => s.name === skillName); - if (!skill) { - console.log(`[Skill] ❌ 读取失败:未找到技能 "${skillName}"`); - return { error: `Skill '${skillName}' not found` }; - } - const fullPath = path.resolve(path.join(skill.baseDir, relPath)); if (!isPathInside(fullPath, skill.baseDir)) { console.log(`[Skill] 🚫 路径越界已拦截:"${relPath}" 超出技能目录范围`); return { error: "Access denied: path is outside skill directory" }; } - try { const fileContent = await fs.readFile(fullPath, "utf-8"); console.log(`[Skill] 📄 已读取文件:${skillName}/${relPath}(${fileContent.length} 字符)`); @@ -197,31 +199,3 @@ ${resourcesXml} }), }; } - -// ==================== 对外接口 ==================== - -export async function useSkill(...segments: string[]) { - if (segments.length === 0) return { prompt: "", tools: {} }; - - const skills = new Map(); - - const primary = await readSkillFromDir(path.join(getPath("skills"), ...segments)); - if (primary) skills.set(primary.name, primary); - - const publicDir = path.join(getPath("skills"), "public"); - try { - const entries = await fs.readdir(publicDir, { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isDirectory()) continue; - const skill = await readSkillFromDir(path.join(publicDir, entry.name)); - if (skill && !skills.has(skill.name)) skills.set(skill.name, skill); - } - } catch { - /* public dir not found */ - } - - if (skills.size === 0) return { prompt: "", tools: {} }; - - const allSkills = [...skills.values()]; - return { prompt: buildCatalog(allSkills), tools: createSkillTools(allSkills) }; -}