diff --git a/data/skills/production-agent/SKILL.md b/data/skills/production-agent/SKILL.md deleted file mode 100644 index 4b7f24a..0000000 --- a/data/skills/production-agent/SKILL.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: production-agent -description: 短剧漫剧制作助手。协助用户进行剧本创作、分镜设计、角色设定、场景描述、对白润色及制作流程规划。当用户提及剧本、分镜、角色、场景、对白、短剧、漫剧等关键词时使用此技能。 ---- - -# Production Agent - -短剧漫剧制作专用技能,提供剧本创作、分镜设计、角色设定等全流程指导。 - -## 何时使用 - -当用户需要以下帮助时激活此技能: - -- 剧本创作与优化 -- 分镜脚本设计 -- 角色设定与描述 -- 场景构建与描绘 -- 对白润色与调整 -- 制作流程规划 - -## 工作指引 - -1. 理解用户的创作意图,根据项目类型(短剧/漫剧)调整输出风格 -2. 遵循标准的剧本格式,包含场景描述、角色动作、对白等要素 -3. 保持角色一致性,关注剧情连贯性 -4. 输出使用中文 - -## 参考资料 - -本技能附带以下参考资料,根据任务需要使用 `read_skill_file` 工具按需加载: - -- [剧本格式规范](references/script-format.md) — 场景描述、对白、旁白、转场等标准写法 -- [角色设定模板](references/character-template.md) — 基础信息、性格特点、背景设定、角色弧光 -- [分镜设计指南](references/storyboard-guide.md) — 景别分类、常用构图、镜头运动、分镜描述模板 - -**注意**:根据用户当前任务选择性加载对应参考资料,不要一次性全部加载。 diff --git a/data/skills/production-agent/decision/SKILL.md b/data/skills/production-agent/decision/SKILL.md new file mode 100644 index 0000000..0698fdb --- /dev/null +++ b/data/skills/production-agent/decision/SKILL.md @@ -0,0 +1,66 @@ +--- +name: decision +description: 短剧漫剧制作决策层。负责分析用户需求、制定执行计划并协调执行层完成制作任务。 +--- + +# Decision Agent + +短剧漫剧制作的指挥层,负责整体决策和协调。接收用户需求后,先制定计划、获得用户确认,再将任务逐步交给执行层完成。 + +## 可用工具 + +| 工具 | 说明 | +|------|------| +| `get_project_info` | 获取当前项目信息(名称、风格、类型、描述、集数进度等) | +| `get_state` | 获取当前执行状态,用于了解已完成的工作 | +| `read_skill_file` | 读取技能参考资料文件 | +| `execution` | 执行层工具,传入任务计划文本,由执行层完成具体工作 | + +## 核心工作流程(必须严格遵循) + +### 首先:判断用户意图 + +收到用户消息时,**先判断当前处于哪个阶段**,再决定下一步动作: + +- **用户发起新的制作任务**(如"开始制作第4集"、"帮我拆分剧本"等明确的新需求) → 进入阶段一 +- **用户确认计划**(如"可以"、"确认"、"开始吧"、"没问题"等) → 直接进入阶段三执行,**不要重新制定计划** +- **用户要求修改计划**(如"第2步改一下"、"加一个步骤"等) → 留在阶段二,修改后重新回复计划 + +**禁止**:把用户的确认或简短回复当作新任务重新走阶段一。 + +### 阶段一:收集信息(仅新任务触发) + +1. 调用 `get_project_info` 获取项目基本信息 +2. 调用 `get_state` 了解当前已完成的工作进度 +3. 使用 `read_skill_file` 加载 `references/plan.md` 获取计划制定规范 + +### 阶段二:制定计划并确认 + +1. 结合项目信息、当前状态和用户需求,按照 `plan.md` 的规范生成**结构化执行计划** +2. **将计划回复给用户**,请求确认 +3. 如果用户要求调整,修改计划后重新回复,直到用户确认 +4. 输出计划后**停止并等待用户回复**,不要自行继续 + +### 阶段三:按计划执行(仅用户确认后触发) + +用户确认后,按步骤顺序逐步调用 `execution` 工具: + +1. 每次调用 `execution` 时,将当前步骤的任务描述作为 `taskDescription` 参数传入 +2. 检查返回结果是否符合预期,不符合则调整指令重试 +3. 将上一步的输出作为上下文传入下一步(如有依赖) +4. 全部步骤完成后,向用户汇报整体结果 + +## 决策策略 + +- 根据项目类型(短剧/漫剧)和风格调整策略 +- 复杂任务拆分为可独立执行的小步骤 +- 关注步骤间的依赖关系,确保顺序合理 +- 利用 `get_state` 避免重复已完成的工作 + +## 参考资料 + +使用 `read_skill_file` 按需加载: + +- [生成计划](references/plan.md) — 计划制定规范和回复模板 + +**注意**:按需加载参考资料,不要一次性全部加载。 diff --git a/data/skills/production-agent/decision/references/plan.md b/data/skills/production-agent/decision/references/plan.md new file mode 100644 index 0000000..1d31c1c --- /dev/null +++ b/data/skills/production-agent/decision/references/plan.md @@ -0,0 +1,65 @@ +# 生成计划 + +## 计划制定规范 + +根据 `get_project_info` 返回的项目信息和用户需求,按以下规范生成执行计划。 + +## 计划结构 + +### 1. 任务总览 + +一段话概述: +- 项目名称和类型(来自项目信息) +- 用户本次的核心需求 +- 预期最终产出 + +### 2. 步骤列表 + +将任务拆解为执行步骤: + +| 字段 | 说明 | +|------|------| +| 步骤编号 | 从 1 开始 | +| 步骤名称 | 简明标题 | +| 具体内容 | 要做什么(需足够详细,可直接作为 `execution` 工具的 `plan` 参数) | +| 预期输出 | 完成后应产出什么 | +| 依赖步骤 | 前置步骤编号(无依赖填"无") | + +**关键要求**:每个步骤的"具体内容"必须是一段完整的任务描述文本,能够独立传给 `execution` 工具执行,不能是模糊的一句话。 + +### 3. 执行顺序 + +标注哪些步骤必须串行(有依赖),哪些可以并行(无依赖)。 + +## 回复模板 + +``` +## 📋 执行计划 + +**项目**:[项目名称] · [项目类型] +**目标**:[一句话描述本次目标] +**预计步骤**:[N] 步 + +### 步骤 + +1. **[步骤名称]** + - 内容:[具体内容] + - 产出:[预期输出] + +2. **[步骤名称]** + - 内容:[具体内容] + - 产出:[预期输出] + - 依赖:步骤 1 + +... + +请确认此计划,或告诉我需要调整的部分。 +``` + +## 注意事项 + +- 步骤粒度适中:每步对应一次 `execution` 调用 +- 结合项目当前进度(`get_state`),跳过已完成的工作 +- 考虑用户已有的素材和资源,避免重复 +- 每个步骤的内容描述要包含足够上下文,使执行层无需额外信息即可工作 + diff --git a/data/skills/production-agent/execution/SKILL.md b/data/skills/production-agent/execution/SKILL.md new file mode 100644 index 0000000..d4049fe --- /dev/null +++ b/data/skills/production-agent/execution/SKILL.md @@ -0,0 +1,29 @@ +--- +name: execution +description: 短剧漫剧制作助手。协助用户进行制作视频。 +--- + +# Production Agent + +短剧漫剧制作专用技能,提供剧本创作、分镜设计、角色设定等全流程指导。 + +## 何时使用 + +当用户需要以下帮助时激活此技能: + +- 开始制作视频 + +## 工作指引 + +1. 理解用户的创作意图,根据项目类型(短剧/漫剧)调整输出风格 +2. 遵循标准的剧本格式,包含场景描述、角色动作、对白等要素 +3. 保持角色一致性,关注剧情连贯性 +4. 输出使用中文 + +## 参考资料 + +本技能附带以下参考资料,根据任务需要使用 `read_skill_file` 工具按需加载: + +- [剧本拆分](references/script-splitting.md) — 将剧本拆分成视频模型能够处理的片段,包含拆分原则和示例 + +**注意**:根据用户当前任务选择性加载对应参考资料,不要一次性全部加载。 diff --git a/data/skills/production-agent/execution/references/script-splitting.md b/data/skills/production-agent/execution/references/script-splitting.md new file mode 100644 index 0000000..67f04e8 --- /dev/null +++ b/data/skills/production-agent/execution/references/script-splitting.md @@ -0,0 +1,98 @@ +# 剧本拆分(仅原始文本切割,输出 string[]) + +本指南只做一件事: +把一份长剧本原文切成多个文本片段,输出 `string[]`。 + +不做结构化字段提取,不输出 JSON 对象,不附加角色卡、锚点、分镜元数据。 + +## 1. 输入与输出 + +### 输入 + +- 一段完整长剧本文本(字符串) + +### 输出 + +- `string[]` +- 数组每个元素是“原始剧本的一段子串” +- 拼接后应可还原原文语义顺序(允许空白规范化差异) + +示例: + +```ts +[ + "第一段原文...", + "第二段原文...", + "第三段原文..." +] +``` + +## 2. 切割原则 + +1. 只切原文,不改写剧情。 +2. 尽量在自然边界切分: + - 场景切换处 + - 段落边界 + - 完整对白后 +3. 禁止在以下位置切分: + - 一句对白中间 + - 一个动作描述中间 + - 专有名词中间 +4. 每段尽量长度均衡,避免极短碎片。 + +## 3. 长度约束(建议) + +按模型容量设置目标长度,建议采用“软上限 + 硬上限”: + +- `targetLen`:目标字符数(例如 800 到 1500) +- `maxLen`:硬上限字符数(例如 1800) + +规则: + +1. 优先接近 `targetLen`。 +2. 若继续追加会超过 `maxLen`,必须切分。 +3. 超长单段(天然超过 `maxLen`)可在最近标点处强制切分。 + +## 4. 切割流程 + +1. 预处理:统一换行符、去除明显重复空行。 +2. 粗切:按双换行或场景标记(如“场景X”“INT/EXT”)分段。 +3. 合并:将粗切段按顺序累加到当前块,直到接近 `targetLen`。 +4. 截断:若超过 `maxLen`,在最近的句末标点处切开。 +5. 收尾:去掉首尾多余空白,输出 `string[]`。 + +## 5. 最低质量检查 + +输出前检查: + +1. 数组不为空。 +2. 每个元素非空字符串。 +3. 顺序与原文一致。 +4. 不存在明显断句错误(例如对白断半句)。 + +## 6. 给智能体的固定执行指令 + +当用户要求“拆分长剧本给多个视频模型并行生成”时: + +1. 仅基于原始剧本文本切割。 +2. 不增加任何结构化字段。 +3. 最终仅输出 `string[]`。 +4. 不输出额外解释性对象。 + +## 7. 简例 + +原文(节选): + +- 场景1:雨夜街道。林夏快步穿过巷口。 +- 场景2:天台。她停下,回头看向门口。 +- 场景3:脚步声逼近。她握紧手机。 + +可能输出: + +```ts +[ + "场景1:雨夜街道。林夏快步穿过巷口。", + "场景2:天台。她停下,回头看向门口。", + "场景3:脚步声逼近。她握紧手机。" +] +``` diff --git a/data/skills/production-agent/references/character-template.md b/data/skills/production-agent/references/character-template.md deleted file mode 100644 index 41bafc6..0000000 --- a/data/skills/production-agent/references/character-template.md +++ /dev/null @@ -1,67 +0,0 @@ -# 角色设定模板 - -## 基础信息 - -- **姓名**: -- **年龄**: -- **性别**: -- **职业**: -- **外貌特征**:身高、体型、发型、标志性特征 - -## 性格特点 - -- **核心性格**:用 3-5 个关键词概括 -- **优点**: -- **缺点**: -- **口头禅**: -- **说话风格**:正式/随意/文艺/粗犷 - -## 人物背景 - -- **家庭背景**: -- **成长经历**: -- **关键转折事件**: - -## 人物关系 - -| 角色 | 关系 | 互动方式 | -|------|------|----------| -| | | | - -## 角色弧光 - -- **起点状态**:故事开始时的状态 -- **核心冲突**:角色面临的主要矛盾 -- **转变契机**:什么事件触发角色转变 -- **终点状态**:故事结束时的状态 - -## 填写示例 - -### 基础信息 - -- **姓名**:林晓 -- **年龄**:26 岁 -- **性别**:女 -- **职业**:自由插画师 -- **外貌特征**:165cm,偏瘦,黑色短发微卷,左耳戴银色耳钉 - -### 性格特点 - -- **核心性格**:敏感、倔强、善良 -- **优点**:对艺术有极强的感知力,重情义 -- **缺点**:逃避冲突,容易钻牛角尖 -- **口头禅**:"算了吧" -- **说话风格**:随意,偶尔会冒出文艺的比喻 - -### 人物背景 - -- **家庭背景**:单亲家庭,母亲独自经营花店 -- **成长经历**:从小在花店长大,高中获美术竞赛一等奖后决定走艺术道路 -- **关键转折事件**:大学毕业作品展遭导师否定后退学 - -### 角色弧光 - -- **起点状态**:对自己的能力缺乏信心,靠接零散商稿维生 -- **核心冲突**:内心的艺术追求与现实生存压力 -- **转变契机**:一位老画家看中她的画作并邀请合作 -- **终点状态**:重新找回创作的勇气,举办个人画展 diff --git a/data/skills/production-agent/references/script-format.md b/data/skills/production-agent/references/script-format.md deleted file mode 100644 index 709e45b..0000000 --- a/data/skills/production-agent/references/script-format.md +++ /dev/null @@ -1,61 +0,0 @@ -# 标准剧本格式规范 - -## 场景描述 - -``` -场景 1 - 内景/咖啡厅/日 -``` - -- 以"场景 + 编号"开头 -- 标注内景/外景 -- 标注地点 -- 标注时间(日/夜/黄昏/清晨) - -## 角色动作 - -用括号标注角色动作和表情: - -``` -小明(皱眉,放下咖啡杯) -``` - -## 对白格式 - -``` -小明:你确定这个计划可行? -小红:(犹豫片刻)我……我不确定。 -``` - -## 旁白/画外音 - -``` -【旁白】三年前的那个夏天,一切都还没有开始。 -``` - -## 转场标注 - -- 切至:硬切 -- 淡入/淡出:渐变过渡 -- 叠化:两个画面重叠过渡 - -## 完整示例 - -``` -场景 1 - 内景/咖啡厅/日 - -(阳光透过落地窗洒进咖啡厅,背景音乐轻柔) - -小明独自坐在靠窗的位置,低头搅动着咖啡。 - -小红推门走入,环顾四周后看到小明。 - -小红:(微笑着走过来)好久不见。 -小明:(抬头,愣了一下)你……怎么来了? -小红:(坐下,放下手提包)路过这里,想起你说过喜欢这家店。 - -【旁白】他们已经三年没有见面了。 - -—— 切至 —— - -场景 2 - 外景/街道/夜 -``` diff --git a/data/skills/production-agent/references/storyboard-guide.md b/data/skills/production-agent/references/storyboard-guide.md deleted file mode 100644 index 12fdc27..0000000 --- a/data/skills/production-agent/references/storyboard-guide.md +++ /dev/null @@ -1,61 +0,0 @@ -# 分镜设计指南 - -## 景别分类 - -| 景别 | 范围 | 用途 | -|------|------|------| -| 远景 | 环境全貌 | 交代环境、气氛渲染 | -| 全景 | 人物全身 | 展示人物与环境关系 | -| 中景 | 膝盖以上 | 日常对话、叙事推进 | -| 近景 | 胸部以上 | 表情细节、情绪传达 | -| 特写 | 面部/物件 | 强调情绪或关键道具 | - -## 常用构图 - -- **三分法**:主体置于三分线交叉点 -- **对称构图**:营造庄重、对峙感 -- **引导线构图**:利用线条引导视线 -- **框中框**:通过门窗等框住主体 -- **前景遮挡**:增加画面层次感 - -## 镜头运动 - -- **推**:由远及近,聚焦主体 -- **拉**:由近及远,展示全貌 -- **摇**:固定位置旋转,扫视场景 -- **移**:跟随角色移动 -- **升降**:垂直运动,营造压迫或释放感 -- **手持**:模拟真实视角,增加临场感 - -## 分镜描述模板 - -``` -镜号:001 -景别:近景 -角度:平视 -构图:三分法,人物偏左 -动作:小明缓缓抬起头 -对白:小明:"原来是你。" -音效:雨声渐弱 -时长:3s -``` - -## 完整分镜示例 - -### 场景:咖啡厅重逢 - -| 镜号 | 景别 | 构图 | 内容描述 | 对白/音效 | 时长 | -|------|------|------|----------|-----------|------| -| 001 | 远景 | 对称 | 咖啡厅外观,暖黄灯光 | 轻柔钢琴BGM | 2s | -| 002 | 中景 | 三分法 | 小明独坐窗边,搅动咖啡 | 咖啡杯碰撞声 | 3s | -| 003 | 全景 | 引导线 | 小红推门进入,逆光 | 门铃声 | 2s | -| 004 | 近景 | 居中 | 小明抬头,表情从平淡到惊讶 | 小红:"好久不见。" | 3s | -| 005 | 特写 | 居中 | 小明的眼睛,瞳孔微微放大 | BGM渐弱 | 1.5s | -| 006 | 中景 | 对称 | 两人面对面坐下 | 小明:"你怎么来了?" | 3s | - -### 情绪节奏说明 - -- 001-002:平静、孤独(慢节奏) -- 003:转折信号(节奏变化) -- 004-005:情绪高点(短促镜头) -- 006:回归对话(节奏恢复) \ No newline at end of file diff --git a/scripts/build.ts b/scripts/build.ts index 11638e8..2282b29 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -19,7 +19,20 @@ if (!fs.existsSync(envFile)) { console.log(`📄 已自动创建环境变量文件: ${envFile}`); } -const external = ["electron", "sqlite3", "better-sqlite3", "mysql", "mysql2", "pg", "pg-query-stream", "oracledb", "tedious", "mssql"]; +const external = [ + "electron", + "@huggingface/transformers", + "vm2", + "sqlite3", + "better-sqlite3", + "mysql", + "mysql2", + "pg", + "pg-query-stream", + "oracledb", + "tedious", + "mssql", +]; // 后端服务打包配置 const appBuildConfig: esbuild.BuildOptions = { diff --git a/src/agents/productionAgent/tools.ts b/src/agents/productionAgent/tools.ts index daa7e6a..44780db 100644 --- a/src/agents/productionAgent/tools.ts +++ b/src/agents/productionAgent/tools.ts @@ -1,9 +1,75 @@ -class ProductionAgentTools { - state: Record = {}; - constructor(isolationKey: string) { +import { tool } from "ai"; +import { z } from "zod"; +import u from "@/utils"; +import { useSkill } from "@/utils/agent/skillsTools"; +import { createAGUIStream } from "@/utils/agent/aguiTools"; - } +interface FlowData { + script: { + blocks: string[]; + }; +} - +export default (isolationKey: string, agui: ReturnType) => { + const flowData: FlowData = { + script: { + blocks: [], + }, + }; + return { + get_project_info: tool({ + description: "获取项目信息", + inputSchema: z.object({}), + execute: async () => { + return ` + 项目名称:仙逆 + 视频风格:玄幻3D动漫 + 视频类型:短剧 + 项目描述:讲述了乡村平凡少年王林以心中之感动,逆仙而修,求的不仅是长生,更多的是摆脱那背后的蝼蚁之身。他坚信道在人为,以平庸的资质踏入修真仙途,历经坎坷风雨,凭着其聪睿的心智,一步一步走向巅峰,凭一己之力,扬名修真界。 + 总集数:24集每集2分钟 + 当前集数:3集 + `; + }, + }), + get_state: tool({ + description: "获取工作流指定板块数据", + inputSchema: z.object({ + block: z.enum(["script"]).describe("板块名称,如 script"), + }), + execute: async ({ block }) => { + return flowData[block]; + }, + }), + execution: tool({ + description: "执行层,负责具体执行具体的任务", + inputSchema: z.object({ + taskDescription: z.string().describe("具体的任务描述详细信息"), + }), + execute: async ({ taskDescription }) => { + agui.custom("systemMessage", "已由 执行层AI 接管对话"); -} \ No newline at end of file + const skill = await useSkill("production-agent", "execution"); + + const { textStream } = await u.Ai.Text("productionAgent").stream({ + system: skill.prompt, + messages: [{ role: "user", content: `请完成任务:${taskDescription}` }], + tools: { + ...skill.tools, + }, + }); + + let msg: ReturnType | null = null; + let fullResponse = ""; + + for await (const chunk of textStream) { + if (!msg) msg = agui.textMessage(); + msg.send(chunk); + fullResponse += chunk; + } + msg?.end(); + + return { found: true, memories: ["第一条记忆内容", "第二条记忆内容"] }; + }, + }), + }; +}; diff --git a/src/routes/agents/productionAgent.ts b/src/routes/agents/productionAgent.ts index 2b92169..1d383a0 100644 --- a/src/routes/agents/productionAgent.ts +++ b/src/routes/agents/productionAgent.ts @@ -3,6 +3,7 @@ import { createAGUIStream } from "@/utils/agent/aguiTools"; import u from "@/utils"; import Memory from "@/utils/agent/memory"; import { useSkill } from "@/utils/agent/skillsTools"; +import tools from "@/agents/productionAgent/tools"; const router = express.Router(); @@ -17,15 +18,16 @@ export default router.post("/", async (req, res) => { //记忆 const memory = new Memory("productionAgent", isolationKey); //skill - const skill = await useSkill("production-agent"); + const skill = await useSkill("production-agent", "decision"); const agui = createAGUIStream(res); agui.runStarted(); + agui.custom("systemMessage", "已由 决策层AI 接管对话"); // 存入用户消息 await memory.add("user", text); - // 获取记忆上下文 + // 获取记忆上下文 const mem = await memory.get(text); const memoryContext = [ mem.rag.length > 0 && `[相关记忆]\n${mem.rag.map((r) => r.content).join("\n")}`, @@ -35,28 +37,21 @@ export default router.post("/", async (req, res) => { .filter(Boolean) .join("\n\n"); - const systemPrompt = [skill.prompt, memoryContext && `## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`] .filter(Boolean) .join("\n\n"); - const messages = [ - { - role: "user" as const, - content: text, - }, - ]; - const { textStream } = await u.Ai.Text("productionAgent").stream({ system: systemPrompt, - messages, + messages: [{ role: "user", content: text }], tools: { ...skill.tools, ...memory.getTools(), + ...tools(isolationKey, agui), }, onFinish: async (completion) => { // 存入助手回复 - await memory.add("assistant", completion.text); + await memory.add("decisionAI", completion.text); }, }); @@ -65,9 +60,8 @@ export default router.post("/", async (req, res) => { for await (const chunk of textStream) { if (!msg) msg = agui.textMessage(); - msg.content(chunk); + msg.send(chunk); fullResponse += chunk; - await delay(1); } msg?.end(); diff --git a/src/routes/test/test.ts b/src/routes/test/test.ts index 535779f..91adc64 100644 --- a/src/routes/test/test.ts +++ b/src/routes/test/test.ts @@ -1,38 +1,4 @@ import express from "express"; -import u from "@/utils"; -import { z } from "zod"; -import { success } from "@/lib/responseFormat"; -import { validateFields } from "@/middleware/middleware"; const router = express.Router(); -import { MemoryManager, Memory } from "@/utils/agent/memory"; -import { initEmbedding } from "@/utils/agent/embedding"; - -// 新增剧本 -export default router.get("/", async (req, res) => { - await initEmbedding(); - const memory = new MemoryManager(); - const userMessage = "小明喜欢什么?"; - const relevantMemories = await memory.searchMemories(userMessage, 1, 3, 0.4); - console.log("%c Line:17 🍖 relevantMemories", "background:#b03734", relevantMemories); - res.status(200).send(success({ message: "添加剧本成功" })); -}); - -function buildMemoryContext(relevant: Memory[], recent: Memory[]): string { - const parts: string[] = []; - - if (relevant.length > 0) { - parts.push("【相关记忆】"); - relevant.forEach((m) => parts.push(`- ${m.content}`)); - } - - if (recent.length > 0) { - const recentNotInRelevant = recent.filter((r) => !relevant.some((rel) => rel.id === r.id)); - if (recentNotInRelevant.length > 0) { - parts.push("【近期记忆】"); - recentNotInRelevant.forEach((m) => parts.push(`- ${m.content}`)); - } - } - - return parts.length > 0 ? parts.join("\n") : ""; -} +export default router.get("/", async (req, res) => {}); diff --git a/src/types/database.d.ts b/src/types/database.d.ts index e3a7e5f..4c3b490 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,4 +1,4 @@ -// @db-hash 04e1150a9773602183de5f660a52b092 +// @db-hash feca77a2c2ec5b6a2989347f982558d5 //该文件由脚本自动生成,请勿手动修改 export interface memories { @@ -35,12 +35,18 @@ export interface o_assets { 'projectId'?: number | null; 'prompt'?: string | null; 'remark'?: string | null; - 'scriptId'?: number | null; 'sonId'?: number | null; 'startTime'?: number | null; 'state'?: string | null; 'type'?: string | null; } +export interface o_chatHistory { + 'data'?: string | null; + 'id'?: number; + 'novel'?: string | null; + 'projectId'?: number | null; + 'type'?: string | null; +} export interface o_event { 'createTime'?: number | null; 'detail'?: string | null; @@ -61,10 +67,33 @@ export interface o_image { 'assetsId'?: number | null; 'filePath'?: string | null; 'id'?: number; - 'model'?: string | null; - 'resolution'?: string | null; + 'projectId'?: number | null; + 'scriptId'?: number | null; 'state'?: string | null; 'type'?: string | null; + 'videoId'?: number | null; +} +export interface o_model { + 'apiKey'?: string | null; + 'baseUrl'?: string | null; + 'createTime'?: number | null; + 'id'?: number; + 'index'?: number | null; + 'manufacturer'?: string | null; + 'model'?: string | null; + 'modelType'?: string | null; + 'type'?: string | null; +} +export interface o_myTasks { + 'describe'?: string | null; + 'id'?: number; + 'model'?: string | null; + 'projectId'?: number | null; + 'reason'?: string | null; + 'relatedObjects'?: string | null; + 'startTime'?: number | null; + 'state'?: string | null; + 'taskClass'?: string | null; } export interface o_novel { 'chapter'?: string | null; @@ -97,6 +126,15 @@ export interface o_project { 'userId'?: number | null; 'videoRatio'?: string | null; } +export interface o_prompts { + 'code'?: string | null; + 'customValue'?: string | null; + 'defaultValue'?: string | null; + 'id'?: number; + 'name'?: string | null; + 'parentCode'?: string | null; + 'type'?: string | null; +} export interface o_script { 'content'?: string | null; 'createTime'?: number | null; @@ -104,15 +142,35 @@ export interface o_script { 'name'?: string | null; 'projectId'?: number | null; } +export interface o_scriptAssets { + 'assetsId'?: number | null; + 'id'?: number; + 'scriptId'?: number | null; +} +export interface o_scriptOutline { + 'id'?: number; + 'outlineId'?: number | null; + 'scriptId'?: number | null; +} export interface o_setting { 'key'?: string | null; 'value'?: string | null; } +export interface o_skills { + 'id'?: number; + 'name'?: string | null; + 'startTime'?: number | null; +} export interface o_storyboard { 'createTime'?: number | null; 'id'?: number; 'name'?: string | null; } +export interface o_storyboardScript { + 'id'?: number; + 'scriptId'?: number | null; + 'storyboardId'?: number | null; +} export interface o_tasks { 'describe'?: string | null; 'id'?: number; @@ -178,17 +236,25 @@ export interface DB { "o_agentDeploy": o_agentDeploy; "o_artStyle": o_artStyle; "o_assets": o_assets; + "o_chatHistory": o_chatHistory; "o_event": o_event; "o_eventChapter": o_eventChapter; "o_flowData": o_flowData; "o_image": o_image; + "o_model": o_model; + "o_myTasks": o_myTasks; "o_novel": o_novel; "o_outline": o_outline; "o_outlineNovel": o_outlineNovel; "o_project": o_project; + "o_prompts": o_prompts; "o_script": o_script; + "o_scriptAssets": o_scriptAssets; + "o_scriptOutline": o_scriptOutline; "o_setting": o_setting; + "o_skills": o_skills; "o_storyboard": o_storyboard; + "o_storyboardScript": o_storyboardScript; "o_tasks": o_tasks; "o_user": o_user; "o_vendorConfig": o_vendorConfig; diff --git a/src/utils/agent/aguiTools.ts b/src/utils/agent/aguiTools.ts index 75762de..fc1e32a 100644 --- a/src/utils/agent/aguiTools.ts +++ b/src/utils/agent/aguiTools.ts @@ -98,13 +98,14 @@ export class AGUIStream { role, } satisfies TextMessageStartEvent); - return { - content: (delta: string) => { + const handle = { + send: (delta: string) => { this.send({ type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta, } satisfies TextMessageContentEvent); + return handle; }, end: () => { this.send({ @@ -113,12 +114,13 @@ export class AGUIStream { } satisfies TextMessageEndEvent); }, }; + return handle; } /** 一次性发送完整文本消息 */ textMessageFull(content: string, role: Role = "assistant") { const msg = this.textMessage(role); - msg.content(content); + msg.send(content); msg.end(); return this; } diff --git a/src/utils/agent/skillsTools.ts b/src/utils/agent/skillsTools.ts index 6c6f77d..1094ca7 100644 --- a/src/utils/agent/skillsTools.ts +++ b/src/utils/agent/skillsTools.ts @@ -5,21 +5,15 @@ import fs from "fs/promises"; import isPathInside from "is-path-inside"; import getPath from "@/utils/getPath"; -// ==================== 类型 ==================== - interface SkillRecord { name: string; description: string; - location: string; // SKILL.md 绝对路径 - baseDir: string; // skill 目录绝对路径 + location: string; + baseDir: string; } -// ==================== Step 2: 解析 SKILL.md ==================== +// ==================== 解析 SKILL.md ==================== -/** - * 解析 SKILL.md frontmatter - * 支持 YAML 单行值及多行块标量(>、>-、|、|-) - */ function parseFrontmatter(content: string): { name: string; description: string } { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!match?.[1]) throw new Error("No frontmatter found"); @@ -27,35 +21,24 @@ function parseFrontmatter(content: string): { name: string; description: string const result: Record = {}; const lines = match[1].split("\n"); - let i = 0; - while (i < lines.length) { - const line = lines[i]; - const colonIndex = line.indexOf(":"); + for (let i = 0; i < lines.length; ) { + const colonIndex = lines[i].indexOf(":"); if (colonIndex === -1) { i++; continue; } - const key = line.slice(0, colonIndex).trim(); + const key = lines[i].slice(0, colonIndex).trim(); if (!key) { i++; continue; } - let value = line.slice(colonIndex + 1).trim(); + let value = lines[i].slice(colonIndex + 1).trim(); + i++; - // 检测 YAML 块标量指示符 (>, >-, |, |-) if (/^[>|]-?$/.test(value)) { const fold = value.startsWith(">"); const parts: string[] = []; - i++; - while (i < lines.length) { - const next = lines[i]; - // 缩进行属于当前块 - if (/^\s+/.test(next)) { - parts.push(next.trim()); - i++; - } else { - break; - } + while (i < lines.length && /^\s+/.test(lines[i])) { + parts.push(lines[i].trim()); + i++; } value = fold ? parts.join(" ") : parts.join("\n"); - } else { - i++; } result[key] = value; @@ -63,35 +46,24 @@ function parseFrontmatter(content: string): { name: string; description: string if (!result.name) throw new Error("Frontmatter missing required field: name"); if (!result.description) throw new Error("Frontmatter missing required field: description"); - return { name: result.name, description: result.description }; } -/** - * 去除 frontmatter,返回正文(body) - */ function stripFrontmatter(content: string): string { - const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/); - return match ? content.slice(match[0].length).trim() : content.trim(); + return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim(); } // ==================== 资源枚举 ==================== -/** - * 递归扫描目录,返回相对路径列表(排除 SKILL.md) - */ -async function listResources(dir: string, base: string = ""): Promise { - const files: string[] = []; +async function listResources(dir: string, base = ""): Promise { let entries; - try { - entries = await fs.readdir(dir, { withFileTypes: true }); - } catch { - return files; - } + try { entries = await fs.readdir(dir, { withFileTypes: true }); } 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))); + files.push(...await listResources(path.join(dir, entry.name), rel)); } else if (entry.name !== "SKILL.md") { files.push(rel); } @@ -99,88 +71,9 @@ async function listResources(dir: string, base: string = ""): Promise return files; } -// ==================== Step 1: 发现 skills ==================== - -/** - * 扫描指定目录,发现所有包含 SKILL.md 的子目录 - */ -async function discoverSkills(directories: string[]): Promise { - const skills: SkillRecord[] = []; - const seenNames = new Set(); - - for (const dir of directories) { - let entries; - try { - entries = await fs.readdir(dir, { withFileTypes: true }); - } catch { - continue; - } - - for (const entry of entries) { - if (!entry.isDirectory()) continue; - - const baseDir = path.join(dir, entry.name); - const location = path.join(baseDir, "SKILL.md"); - - let content: string; - try { - content = await fs.readFile(location, "utf-8"); - } catch { - continue; - } - - let metadata: { name: string; description: string }; - try { - metadata = parseFrontmatter(content); - } catch (e) { - console.log(`[Skill] ⚠️ 跳过 "${entry.name}":${(e as Error).message}`); - continue; - } - - // 宽松校验:name 与目录名不匹配时仅告警 - if (metadata.name !== entry.name) { - console.log(`[Skill] ⚠️ 技能名 "${metadata.name}" 与目录名 "${entry.name}" 不一致,仍加载`); - } - if (metadata.name.length > 64) { - console.log(`[Skill] ⚠️ 技能名 "${metadata.name}" 超过 64 字符,仍加载`); - } - - // 先发现的同名 skill 优先(项目级覆盖用户级) - if (seenNames.has(metadata.name)) { - console.log(`[Skill] ⚠️ 技能 "${metadata.name}" 名称冲突,已被先前发现的同名技能覆盖`); - continue; - } - seenNames.add(metadata.name); - - skills.push({ - name: metadata.name, - description: metadata.description, - location, - baseDir, - }); - - console.log(`[Skill] ✅ 发现技能:${metadata.name} — ${metadata.description}`); - } - } - - return skills; -} - -// ==================== Step 3: 构建技能目录 ==================== - -/** - * 构建 XML 格式的技能目录 + 行为指令,注入到 system prompt - */ -function buildCatalog(skills: SkillRecord[]): string { - if (skills.length === 0) return ""; - - const skillsXml = skills - .map( - (s) => - ` \n ${s.name}\n ${s.description}\n ` - ) - .join("\n"); +// ==================== 构建技能目录 ==================== +function buildCatalog(skill: SkillRecord): string { return [ "## Skills", "以下技能提供了专业任务的专用指令。", @@ -188,20 +81,18 @@ function buildCatalog(skills: SkillRecord[]): string { "加载后遵循技能指令执行任务,需要时调用 read_skill_file 读取资源文件内容。", "", "", - skillsXml, + ` `, + ` ${skill.name}`, + ` ${skill.description}`, + ` `, "", ].join("\n"); } -// ==================== Step 4 & 5: 激活 + 执行工具 ==================== +// ==================== 激活 + 执行工具 ==================== -/** - * 创建 activate_skill 和 read_skill_file 工具 - */ function createSkillTools(skills: SkillRecord[]) { - // 激活去重:记录当前会话已激活的 skill const activated = new Set(); - const validNames = skills.map((s) => s.name); return { @@ -217,45 +108,38 @@ function createSkillTools(skills: SkillRecord[]) { return { error: `Skill '${name}' not found` }; } - // Step 5: 去重检查 if (activated.has(name)) { console.log(`[Skill] ℹ️ 技能 "${name}" 已在当前会话中激活,跳过重复注入`); return { already_active: true, message: `技能 "${name}" 已激活,无需重复加载` }; } let content: string; - try { - content = await fs.readFile(skill.location, "utf-8"); - } catch { + try { content = await fs.readFile(skill.location, "utf-8"); } catch { console.log(`[Skill] ❌ 激活失败:无法读取 ${skill.location}`); return { error: `Failed to read SKILL.md for '${name}'` }; } const body = stripFrontmatter(content); const resources = await listResources(skill.baseDir); - activated.add(name); - const resourcesXml = - resources.length > 0 - ? `\n\n${resources.map((f) => ` ${f}`).join("\n")}\n` - : ""; + console.log(`[Skill] 📖 已激活技能:${skill.name}(${body.length} 字符,${resources.length} 个资源文件)`); - const wrapped = [ - ``, - body, - "", - `Skill directory: ${skill.baseDir}`, - `相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。`, - resourcesXml, - ``, - ].join("\n"); + const resourcesXml = resources.length > 0 + ? `\n\n${resources.map((f) => ` ${f}`).join("\n")}\n` + : ""; - console.log( - `[Skill] 📖 已激活技能:${skill.name}(${body.length} 字符,${resources.length} 个资源文件)` - ); - - return { content: wrapped }; + return { + content: [ + ``, + body, + "", + `Skill directory: ${skill.baseDir}`, + `相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。`, + resourcesXml, + ``, + ].join("\n"), + }; }, }), @@ -273,7 +157,6 @@ function createSkillTools(skills: SkillRecord[]) { } 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" }; @@ -294,30 +177,29 @@ function createSkillTools(skills: SkillRecord[]) { // ==================== 对外接口 ==================== -/** - * 使用指定 skill(渐进式披露) - * - * 遵循 agentskills.io 规范: - * Step 1 — Discovery: 扫描 data/skills/{name} 目录 - * Step 2 — Parse: 提取 frontmatter 元数据 - * Step 3 — Disclose: 构建 XML 目录注入 system prompt - * Step 4 — Activate: activate_skill 工具加载完整指令 + 结构化包装 + 资源列表 - * Step 5 — Manage: read_skill_file 读取资源 + 激活去重 - * - * @param name skill 名称,对应 data/skills/{name} 目录 - */ -export async function useSkill(name: string) { - const skills = await discoverSkills([getPath("skills")]); +export async function useSkill(...segments: string[]) { + if (segments.length === 0) return { prompt: "", tools: {} }; - // 过滤出指定 skill - const matched = skills.filter((s) => s.name === name); - if (matched.length === 0) { - console.log(`[Skill] ⚠️ 未发现名为 "${name}" 的技能`); + const baseDir = path.join(getPath("skills"), ...segments); + const location = path.join(baseDir, "SKILL.md"); + + let content: string; + try { content = await fs.readFile(location, "utf-8"); } catch { + console.log(`[Skill] ⚠️ 未发现技能:${segments.join("/")}`); return { prompt: "", tools: {} }; } + let metadata: { name: string; description: string }; + try { metadata = parseFrontmatter(content); } catch (e) { + console.log(`[Skill] ⚠️ 解析失败 "${segments.join("/")}":${(e as Error).message}`); + return { prompt: "", tools: {} }; + } + + const skill: SkillRecord = { ...metadata, location, baseDir }; + console.log(`[Skill] ✅ 发现技能:${skill.name} — ${skill.description}`); + return { - prompt: buildCatalog(matched), - tools: createSkillTools(matched), + prompt: buildCatalog(skill), + tools: createSkillTools([skill]), }; }