Merge branch '108' of https://github.com/HBAI-Ltd/Toonflow-app into 108
# Conflicts: # src/types/database.d.ts
This commit is contained in:
commit
0b8fb718e9
@ -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) — 景别分类、常用构图、镜头运动、分镜描述模板
|
|
||||||
|
|
||||||
**注意**:根据用户当前任务选择性加载对应参考资料,不要一次性全部加载。
|
|
||||||
66
data/skills/production-agent/decision/SKILL.md
Normal file
66
data/skills/production-agent/decision/SKILL.md
Normal file
@ -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) — 计划制定规范和回复模板
|
||||||
|
|
||||||
|
**注意**:按需加载参考资料,不要一次性全部加载。
|
||||||
65
data/skills/production-agent/decision/references/plan.md
Normal file
65
data/skills/production-agent/decision/references/plan.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# 生成计划
|
||||||
|
|
||||||
|
## 计划制定规范
|
||||||
|
|
||||||
|
根据 `get_project_info` 返回的项目信息和用户需求,按以下规范生成执行计划。
|
||||||
|
|
||||||
|
## 计划结构
|
||||||
|
|
||||||
|
### 1. 任务总览
|
||||||
|
|
||||||
|
一段话概述:
|
||||||
|
- 项目名称和类型(来自项目信息)
|
||||||
|
- 用户本次的核心需求
|
||||||
|
- 预期最终产出
|
||||||
|
|
||||||
|
### 2. 步骤列表
|
||||||
|
|
||||||
|
将任务拆解为执行步骤:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 步骤编号 | 从 1 开始 |
|
||||||
|
| 步骤名称 | 简明标题 |
|
||||||
|
| 具体内容 | 要做什么(需足够详细,可直接作为 `execution` 工具的 `plan` 参数) |
|
||||||
|
| 预期输出 | 完成后应产出什么 |
|
||||||
|
| 依赖步骤 | 前置步骤编号(无依赖填"无") |
|
||||||
|
|
||||||
|
**关键要求**:每个步骤的"具体内容"必须是一段完整的任务描述文本,能够独立传给 `execution` 工具执行,不能是模糊的一句话。
|
||||||
|
|
||||||
|
### 3. 执行顺序
|
||||||
|
|
||||||
|
标注哪些步骤必须串行(有依赖),哪些可以并行(无依赖)。
|
||||||
|
|
||||||
|
## 回复模板
|
||||||
|
|
||||||
|
```
|
||||||
|
## 📋 执行计划
|
||||||
|
|
||||||
|
**项目**:[项目名称] · [项目类型]
|
||||||
|
**目标**:[一句话描述本次目标]
|
||||||
|
**预计步骤**:[N] 步
|
||||||
|
|
||||||
|
### 步骤
|
||||||
|
|
||||||
|
1. **[步骤名称]**
|
||||||
|
- 内容:[具体内容]
|
||||||
|
- 产出:[预期输出]
|
||||||
|
|
||||||
|
2. **[步骤名称]**
|
||||||
|
- 内容:[具体内容]
|
||||||
|
- 产出:[预期输出]
|
||||||
|
- 依赖:步骤 1
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
请确认此计划,或告诉我需要调整的部分。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 步骤粒度适中:每步对应一次 `execution` 调用
|
||||||
|
- 结合项目当前进度(`get_state`),跳过已完成的工作
|
||||||
|
- 考虑用户已有的素材和资源,避免重复
|
||||||
|
- 每个步骤的内容描述要包含足够上下文,使执行层无需额外信息即可工作
|
||||||
|
|
||||||
29
data/skills/production-agent/execution/SKILL.md
Normal file
29
data/skills/production-agent/execution/SKILL.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: execution
|
||||||
|
description: 短剧漫剧制作助手。协助用户进行制作视频。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Production Agent
|
||||||
|
|
||||||
|
短剧漫剧制作专用技能,提供剧本创作、分镜设计、角色设定等全流程指导。
|
||||||
|
|
||||||
|
## 何时使用
|
||||||
|
|
||||||
|
当用户需要以下帮助时激活此技能:
|
||||||
|
|
||||||
|
- 开始制作视频
|
||||||
|
|
||||||
|
## 工作指引
|
||||||
|
|
||||||
|
1. 理解用户的创作意图,根据项目类型(短剧/漫剧)调整输出风格
|
||||||
|
2. 遵循标准的剧本格式,包含场景描述、角色动作、对白等要素
|
||||||
|
3. 保持角色一致性,关注剧情连贯性
|
||||||
|
4. 输出使用中文
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
本技能附带以下参考资料,根据任务需要使用 `read_skill_file` 工具按需加载:
|
||||||
|
|
||||||
|
- [剧本拆分](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:脚步声逼近。她握紧手机。"
|
||||||
|
]
|
||||||
|
```
|
||||||
@ -1,67 +0,0 @@
|
|||||||
# 角色设定模板
|
|
||||||
|
|
||||||
## 基础信息
|
|
||||||
|
|
||||||
- **姓名**:
|
|
||||||
- **年龄**:
|
|
||||||
- **性别**:
|
|
||||||
- **职业**:
|
|
||||||
- **外貌特征**:身高、体型、发型、标志性特征
|
|
||||||
|
|
||||||
## 性格特点
|
|
||||||
|
|
||||||
- **核心性格**:用 3-5 个关键词概括
|
|
||||||
- **优点**:
|
|
||||||
- **缺点**:
|
|
||||||
- **口头禅**:
|
|
||||||
- **说话风格**:正式/随意/文艺/粗犷
|
|
||||||
|
|
||||||
## 人物背景
|
|
||||||
|
|
||||||
- **家庭背景**:
|
|
||||||
- **成长经历**:
|
|
||||||
- **关键转折事件**:
|
|
||||||
|
|
||||||
## 人物关系
|
|
||||||
|
|
||||||
| 角色 | 关系 | 互动方式 |
|
|
||||||
|------|------|----------|
|
|
||||||
| | | |
|
|
||||||
|
|
||||||
## 角色弧光
|
|
||||||
|
|
||||||
- **起点状态**:故事开始时的状态
|
|
||||||
- **核心冲突**:角色面临的主要矛盾
|
|
||||||
- **转变契机**:什么事件触发角色转变
|
|
||||||
- **终点状态**:故事结束时的状态
|
|
||||||
|
|
||||||
## 填写示例
|
|
||||||
|
|
||||||
### 基础信息
|
|
||||||
|
|
||||||
- **姓名**:林晓
|
|
||||||
- **年龄**:26 岁
|
|
||||||
- **性别**:女
|
|
||||||
- **职业**:自由插画师
|
|
||||||
- **外貌特征**:165cm,偏瘦,黑色短发微卷,左耳戴银色耳钉
|
|
||||||
|
|
||||||
### 性格特点
|
|
||||||
|
|
||||||
- **核心性格**:敏感、倔强、善良
|
|
||||||
- **优点**:对艺术有极强的感知力,重情义
|
|
||||||
- **缺点**:逃避冲突,容易钻牛角尖
|
|
||||||
- **口头禅**:"算了吧"
|
|
||||||
- **说话风格**:随意,偶尔会冒出文艺的比喻
|
|
||||||
|
|
||||||
### 人物背景
|
|
||||||
|
|
||||||
- **家庭背景**:单亲家庭,母亲独自经营花店
|
|
||||||
- **成长经历**:从小在花店长大,高中获美术竞赛一等奖后决定走艺术道路
|
|
||||||
- **关键转折事件**:大学毕业作品展遭导师否定后退学
|
|
||||||
|
|
||||||
### 角色弧光
|
|
||||||
|
|
||||||
- **起点状态**:对自己的能力缺乏信心,靠接零散商稿维生
|
|
||||||
- **核心冲突**:内心的艺术追求与现实生存压力
|
|
||||||
- **转变契机**:一位老画家看中她的画作并邀请合作
|
|
||||||
- **终点状态**:重新找回创作的勇气,举办个人画展
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
# 标准剧本格式规范
|
|
||||||
|
|
||||||
## 场景描述
|
|
||||||
|
|
||||||
```
|
|
||||||
场景 1 - 内景/咖啡厅/日
|
|
||||||
```
|
|
||||||
|
|
||||||
- 以"场景 + 编号"开头
|
|
||||||
- 标注内景/外景
|
|
||||||
- 标注地点
|
|
||||||
- 标注时间(日/夜/黄昏/清晨)
|
|
||||||
|
|
||||||
## 角色动作
|
|
||||||
|
|
||||||
用括号标注角色动作和表情:
|
|
||||||
|
|
||||||
```
|
|
||||||
小明(皱眉,放下咖啡杯)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 对白格式
|
|
||||||
|
|
||||||
```
|
|
||||||
小明:你确定这个计划可行?
|
|
||||||
小红:(犹豫片刻)我……我不确定。
|
|
||||||
```
|
|
||||||
|
|
||||||
## 旁白/画外音
|
|
||||||
|
|
||||||
```
|
|
||||||
【旁白】三年前的那个夏天,一切都还没有开始。
|
|
||||||
```
|
|
||||||
|
|
||||||
## 转场标注
|
|
||||||
|
|
||||||
- 切至:硬切
|
|
||||||
- 淡入/淡出:渐变过渡
|
|
||||||
- 叠化:两个画面重叠过渡
|
|
||||||
|
|
||||||
## 完整示例
|
|
||||||
|
|
||||||
```
|
|
||||||
场景 1 - 内景/咖啡厅/日
|
|
||||||
|
|
||||||
(阳光透过落地窗洒进咖啡厅,背景音乐轻柔)
|
|
||||||
|
|
||||||
小明独自坐在靠窗的位置,低头搅动着咖啡。
|
|
||||||
|
|
||||||
小红推门走入,环顾四周后看到小明。
|
|
||||||
|
|
||||||
小红:(微笑着走过来)好久不见。
|
|
||||||
小明:(抬头,愣了一下)你……怎么来了?
|
|
||||||
小红:(坐下,放下手提包)路过这里,想起你说过喜欢这家店。
|
|
||||||
|
|
||||||
【旁白】他们已经三年没有见面了。
|
|
||||||
|
|
||||||
—— 切至 ——
|
|
||||||
|
|
||||||
场景 2 - 外景/街道/夜
|
|
||||||
```
|
|
||||||
@ -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:回归对话(节奏恢复)
|
|
||||||
@ -19,7 +19,20 @@ if (!fs.existsSync(envFile)) {
|
|||||||
console.log(`📄 已自动创建环境变量文件: ${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 = {
|
const appBuildConfig: esbuild.BuildOptions = {
|
||||||
|
|||||||
@ -1,9 +1,75 @@
|
|||||||
class ProductionAgentTools {
|
import { tool } from "ai";
|
||||||
state: Record<string, any> = {};
|
import { z } from "zod";
|
||||||
constructor(isolationKey: string) {
|
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<typeof createAGUIStream>) => {
|
||||||
|
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 接管对话");
|
||||||
|
|
||||||
|
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<typeof agui.textMessage> | 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: ["第一条记忆内容", "第二条记忆内容"] };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { createAGUIStream } from "@/utils/agent/aguiTools";
|
|||||||
import u from "@/utils";
|
import u from "@/utils";
|
||||||
import Memory from "@/utils/agent/memory";
|
import Memory from "@/utils/agent/memory";
|
||||||
import { useSkill } from "@/utils/agent/skillsTools";
|
import { useSkill } from "@/utils/agent/skillsTools";
|
||||||
|
import tools from "@/agents/productionAgent/tools";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@ -17,15 +18,16 @@ export default router.post("/", async (req, res) => {
|
|||||||
//记忆
|
//记忆
|
||||||
const memory = new Memory("productionAgent", isolationKey);
|
const memory = new Memory("productionAgent", isolationKey);
|
||||||
//skill
|
//skill
|
||||||
const skill = await useSkill("production-agent");
|
const skill = await useSkill("production-agent", "decision");
|
||||||
|
|
||||||
const agui = createAGUIStream(res);
|
const agui = createAGUIStream(res);
|
||||||
agui.runStarted();
|
agui.runStarted();
|
||||||
|
agui.custom("systemMessage", "已由 决策层AI 接管对话");
|
||||||
|
|
||||||
// 存入用户消息
|
// 存入用户消息
|
||||||
await memory.add("user", text);
|
await memory.add("user", text);
|
||||||
|
|
||||||
// 获取记忆上下文
|
// 获取记忆上下文
|
||||||
const mem = await memory.get(text);
|
const mem = await memory.get(text);
|
||||||
const memoryContext = [
|
const memoryContext = [
|
||||||
mem.rag.length > 0 && `[相关记忆]\n${mem.rag.map((r) => r.content).join("\n")}`,
|
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)
|
.filter(Boolean)
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
|
|
||||||
const systemPrompt = [skill.prompt, memoryContext && `## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`]
|
const systemPrompt = [skill.prompt, memoryContext && `## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
const messages = [
|
|
||||||
{
|
|
||||||
role: "user" as const,
|
|
||||||
content: text,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { textStream } = await u.Ai.Text("productionAgent").stream({
|
const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
messages,
|
messages: [{ role: "user", content: text }],
|
||||||
tools: {
|
tools: {
|
||||||
...skill.tools,
|
...skill.tools,
|
||||||
...memory.getTools(),
|
...memory.getTools(),
|
||||||
|
...tools(isolationKey, agui),
|
||||||
},
|
},
|
||||||
onFinish: async (completion) => {
|
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) {
|
for await (const chunk of textStream) {
|
||||||
if (!msg) msg = agui.textMessage();
|
if (!msg) msg = agui.textMessage();
|
||||||
msg.content(chunk);
|
msg.send(chunk);
|
||||||
fullResponse += chunk;
|
fullResponse += chunk;
|
||||||
await delay(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msg?.end();
|
msg?.end();
|
||||||
|
|||||||
@ -1,38 +1,4 @@
|
|||||||
import express from "express";
|
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();
|
const router = express.Router();
|
||||||
|
|
||||||
import { MemoryManager, Memory } from "@/utils/agent/memory";
|
export default router.get("/", async (req, res) => {});
|
||||||
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") : "";
|
|
||||||
}
|
|
||||||
|
|||||||
74
src/types/database.d.ts
vendored
74
src/types/database.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
// @db-hash 04e1150a9773602183de5f660a52b092
|
// @db-hash feca77a2c2ec5b6a2989347f982558d5
|
||||||
//该文件由脚本自动生成,请勿手动修改
|
//该文件由脚本自动生成,请勿手动修改
|
||||||
|
|
||||||
export interface memories {
|
export interface memories {
|
||||||
@ -35,12 +35,18 @@ export interface o_assets {
|
|||||||
'projectId'?: number | null;
|
'projectId'?: number | null;
|
||||||
'prompt'?: string | null;
|
'prompt'?: string | null;
|
||||||
'remark'?: string | null;
|
'remark'?: string | null;
|
||||||
'scriptId'?: number | null;
|
|
||||||
'sonId'?: number | null;
|
'sonId'?: number | null;
|
||||||
'startTime'?: number | null;
|
'startTime'?: number | null;
|
||||||
'state'?: string | null;
|
'state'?: string | null;
|
||||||
'type'?: 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 {
|
export interface o_event {
|
||||||
'createTime'?: number | null;
|
'createTime'?: number | null;
|
||||||
'detail'?: string | null;
|
'detail'?: string | null;
|
||||||
@ -61,10 +67,33 @@ export interface o_image {
|
|||||||
'assetsId'?: number | null;
|
'assetsId'?: number | null;
|
||||||
'filePath'?: string | null;
|
'filePath'?: string | null;
|
||||||
'id'?: number;
|
'id'?: number;
|
||||||
'model'?: string | null;
|
'projectId'?: number | null;
|
||||||
'resolution'?: string | null;
|
'scriptId'?: number | null;
|
||||||
'state'?: string | null;
|
'state'?: string | null;
|
||||||
'type'?: 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 {
|
export interface o_novel {
|
||||||
'chapter'?: string | null;
|
'chapter'?: string | null;
|
||||||
@ -97,6 +126,15 @@ export interface o_project {
|
|||||||
'userId'?: number | null;
|
'userId'?: number | null;
|
||||||
'videoRatio'?: string | 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 {
|
export interface o_script {
|
||||||
'content'?: string | null;
|
'content'?: string | null;
|
||||||
'createTime'?: number | null;
|
'createTime'?: number | null;
|
||||||
@ -104,15 +142,35 @@ export interface o_script {
|
|||||||
'name'?: string | null;
|
'name'?: string | null;
|
||||||
'projectId'?: number | 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 {
|
export interface o_setting {
|
||||||
'key'?: string | null;
|
'key'?: string | null;
|
||||||
'value'?: string | null;
|
'value'?: string | null;
|
||||||
}
|
}
|
||||||
|
export interface o_skills {
|
||||||
|
'id'?: number;
|
||||||
|
'name'?: string | null;
|
||||||
|
'startTime'?: number | null;
|
||||||
|
}
|
||||||
export interface o_storyboard {
|
export interface o_storyboard {
|
||||||
'createTime'?: number | null;
|
'createTime'?: number | null;
|
||||||
'id'?: number;
|
'id'?: number;
|
||||||
'name'?: string | null;
|
'name'?: string | null;
|
||||||
}
|
}
|
||||||
|
export interface o_storyboardScript {
|
||||||
|
'id'?: number;
|
||||||
|
'scriptId'?: number | null;
|
||||||
|
'storyboardId'?: number | null;
|
||||||
|
}
|
||||||
export interface o_tasks {
|
export interface o_tasks {
|
||||||
'describe'?: string | null;
|
'describe'?: string | null;
|
||||||
'id'?: number;
|
'id'?: number;
|
||||||
@ -178,17 +236,25 @@ export interface DB {
|
|||||||
"o_agentDeploy": o_agentDeploy;
|
"o_agentDeploy": o_agentDeploy;
|
||||||
"o_artStyle": o_artStyle;
|
"o_artStyle": o_artStyle;
|
||||||
"o_assets": o_assets;
|
"o_assets": o_assets;
|
||||||
|
"o_chatHistory": o_chatHistory;
|
||||||
"o_event": o_event;
|
"o_event": o_event;
|
||||||
"o_eventChapter": o_eventChapter;
|
"o_eventChapter": o_eventChapter;
|
||||||
"o_flowData": o_flowData;
|
"o_flowData": o_flowData;
|
||||||
"o_image": o_image;
|
"o_image": o_image;
|
||||||
|
"o_model": o_model;
|
||||||
|
"o_myTasks": o_myTasks;
|
||||||
"o_novel": o_novel;
|
"o_novel": o_novel;
|
||||||
"o_outline": o_outline;
|
"o_outline": o_outline;
|
||||||
"o_outlineNovel": o_outlineNovel;
|
"o_outlineNovel": o_outlineNovel;
|
||||||
"o_project": o_project;
|
"o_project": o_project;
|
||||||
|
"o_prompts": o_prompts;
|
||||||
"o_script": o_script;
|
"o_script": o_script;
|
||||||
|
"o_scriptAssets": o_scriptAssets;
|
||||||
|
"o_scriptOutline": o_scriptOutline;
|
||||||
"o_setting": o_setting;
|
"o_setting": o_setting;
|
||||||
|
"o_skills": o_skills;
|
||||||
"o_storyboard": o_storyboard;
|
"o_storyboard": o_storyboard;
|
||||||
|
"o_storyboardScript": o_storyboardScript;
|
||||||
"o_tasks": o_tasks;
|
"o_tasks": o_tasks;
|
||||||
"o_user": o_user;
|
"o_user": o_user;
|
||||||
"o_vendorConfig": o_vendorConfig;
|
"o_vendorConfig": o_vendorConfig;
|
||||||
|
|||||||
@ -98,13 +98,14 @@ export class AGUIStream {
|
|||||||
role,
|
role,
|
||||||
} satisfies TextMessageStartEvent);
|
} satisfies TextMessageStartEvent);
|
||||||
|
|
||||||
return {
|
const handle = {
|
||||||
content: (delta: string) => {
|
send: (delta: string) => {
|
||||||
this.send({
|
this.send({
|
||||||
type: EventType.TEXT_MESSAGE_CONTENT,
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
||||||
messageId,
|
messageId,
|
||||||
delta,
|
delta,
|
||||||
} satisfies TextMessageContentEvent);
|
} satisfies TextMessageContentEvent);
|
||||||
|
return handle;
|
||||||
},
|
},
|
||||||
end: () => {
|
end: () => {
|
||||||
this.send({
|
this.send({
|
||||||
@ -113,12 +114,13 @@ export class AGUIStream {
|
|||||||
} satisfies TextMessageEndEvent);
|
} satisfies TextMessageEndEvent);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一次性发送完整文本消息 */
|
/** 一次性发送完整文本消息 */
|
||||||
textMessageFull(content: string, role: Role = "assistant") {
|
textMessageFull(content: string, role: Role = "assistant") {
|
||||||
const msg = this.textMessage(role);
|
const msg = this.textMessage(role);
|
||||||
msg.content(content);
|
msg.send(content);
|
||||||
msg.end();
|
msg.end();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,21 +5,15 @@ import fs from "fs/promises";
|
|||||||
import isPathInside from "is-path-inside";
|
import isPathInside from "is-path-inside";
|
||||||
import getPath from "@/utils/getPath";
|
import getPath from "@/utils/getPath";
|
||||||
|
|
||||||
// ==================== 类型 ====================
|
|
||||||
|
|
||||||
interface SkillRecord {
|
interface SkillRecord {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
location: string; // SKILL.md 绝对路径
|
location: string;
|
||||||
baseDir: string; // skill 目录绝对路径
|
baseDir: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Step 2: 解析 SKILL.md ====================
|
// ==================== 解析 SKILL.md ====================
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 SKILL.md frontmatter
|
|
||||||
* 支持 YAML 单行值及多行块标量(>、>-、|、|-)
|
|
||||||
*/
|
|
||||||
function parseFrontmatter(content: string): { name: string; description: string } {
|
function parseFrontmatter(content: string): { name: string; description: string } {
|
||||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
if (!match?.[1]) throw new Error("No frontmatter found");
|
if (!match?.[1]) throw new Error("No frontmatter found");
|
||||||
@ -27,35 +21,24 @@ function parseFrontmatter(content: string): { name: string; description: string
|
|||||||
const result: Record<string, string> = {};
|
const result: Record<string, string> = {};
|
||||||
const lines = match[1].split("\n");
|
const lines = match[1].split("\n");
|
||||||
|
|
||||||
let i = 0;
|
for (let i = 0; i < lines.length; ) {
|
||||||
while (i < lines.length) {
|
const colonIndex = lines[i].indexOf(":");
|
||||||
const line = lines[i];
|
|
||||||
const colonIndex = line.indexOf(":");
|
|
||||||
if (colonIndex === -1) { i++; continue; }
|
if (colonIndex === -1) { i++; continue; }
|
||||||
|
|
||||||
const key = line.slice(0, colonIndex).trim();
|
const key = lines[i].slice(0, colonIndex).trim();
|
||||||
if (!key) { i++; continue; }
|
if (!key) { i++; continue; }
|
||||||
|
|
||||||
let value = line.slice(colonIndex + 1).trim();
|
let value = lines[i].slice(colonIndex + 1).trim();
|
||||||
|
i++;
|
||||||
|
|
||||||
// 检测 YAML 块标量指示符 (>, >-, |, |-)
|
|
||||||
if (/^[>|]-?$/.test(value)) {
|
if (/^[>|]-?$/.test(value)) {
|
||||||
const fold = value.startsWith(">");
|
const fold = value.startsWith(">");
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
i++;
|
while (i < lines.length && /^\s+/.test(lines[i])) {
|
||||||
while (i < lines.length) {
|
parts.push(lines[i].trim());
|
||||||
const next = lines[i];
|
i++;
|
||||||
// 缩进行属于当前块
|
|
||||||
if (/^\s+/.test(next)) {
|
|
||||||
parts.push(next.trim());
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
value = fold ? parts.join(" ") : parts.join("\n");
|
value = fold ? parts.join(" ") : parts.join("\n");
|
||||||
} else {
|
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result[key] = value;
|
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.name) throw new Error("Frontmatter missing required field: name");
|
||||||
if (!result.description) throw new Error("Frontmatter missing required field: description");
|
if (!result.description) throw new Error("Frontmatter missing required field: description");
|
||||||
|
|
||||||
return { name: result.name, description: result.description };
|
return { name: result.name, description: result.description };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 去除 frontmatter,返回正文(body)
|
|
||||||
*/
|
|
||||||
function stripFrontmatter(content: string): string {
|
function stripFrontmatter(content: string): string {
|
||||||
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
|
||||||
return match ? content.slice(match[0].length).trim() : content.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 资源枚举 ====================
|
// ==================== 资源枚举 ====================
|
||||||
|
|
||||||
/**
|
async function listResources(dir: string, base = ""): Promise<string[]> {
|
||||||
* 递归扫描目录,返回相对路径列表(排除 SKILL.md)
|
|
||||||
*/
|
|
||||||
async function listResources(dir: string, base: string = ""): Promise<string[]> {
|
|
||||||
const files: string[] = [];
|
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return []; }
|
||||||
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
} catch {
|
const files: string[] = [];
|
||||||
return files;
|
|
||||||
}
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const rel = base ? `${base}/${entry.name}` : entry.name;
|
const rel = base ? `${base}/${entry.name}` : entry.name;
|
||||||
if (entry.isDirectory()) {
|
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") {
|
} else if (entry.name !== "SKILL.md") {
|
||||||
files.push(rel);
|
files.push(rel);
|
||||||
}
|
}
|
||||||
@ -99,88 +71,9 @@ async function listResources(dir: string, base: string = ""): Promise<string[]>
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Step 1: 发现 skills ====================
|
// ==================== 构建技能目录 ====================
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫描指定目录,发现所有包含 SKILL.md 的子目录
|
|
||||||
*/
|
|
||||||
async function discoverSkills(directories: string[]): Promise<SkillRecord[]> {
|
|
||||||
const skills: SkillRecord[] = [];
|
|
||||||
const seenNames = new Set<string>();
|
|
||||||
|
|
||||||
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) =>
|
|
||||||
` <skill>\n <name>${s.name}</name>\n <description>${s.description}</description>\n </skill>`
|
|
||||||
)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
|
function buildCatalog(skill: SkillRecord): string {
|
||||||
return [
|
return [
|
||||||
"## Skills",
|
"## Skills",
|
||||||
"以下技能提供了专业任务的专用指令。",
|
"以下技能提供了专业任务的专用指令。",
|
||||||
@ -188,20 +81,18 @@ function buildCatalog(skills: SkillRecord[]): string {
|
|||||||
"加载后遵循技能指令执行任务,需要时调用 read_skill_file 读取资源文件内容。",
|
"加载后遵循技能指令执行任务,需要时调用 read_skill_file 读取资源文件内容。",
|
||||||
"",
|
"",
|
||||||
"<available_skills>",
|
"<available_skills>",
|
||||||
skillsXml,
|
` <skill>`,
|
||||||
|
` <name>${skill.name}</name>`,
|
||||||
|
` <description>${skill.description}</description>`,
|
||||||
|
` </skill>`,
|
||||||
"</available_skills>",
|
"</available_skills>",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Step 4 & 5: 激活 + 执行工具 ====================
|
// ==================== 激活 + 执行工具 ====================
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 activate_skill 和 read_skill_file 工具
|
|
||||||
*/
|
|
||||||
function createSkillTools(skills: SkillRecord[]) {
|
function createSkillTools(skills: SkillRecord[]) {
|
||||||
// 激活去重:记录当前会话已激活的 skill
|
|
||||||
const activated = new Set<string>();
|
const activated = new Set<string>();
|
||||||
|
|
||||||
const validNames = skills.map((s) => s.name);
|
const validNames = skills.map((s) => s.name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -217,45 +108,38 @@ function createSkillTools(skills: SkillRecord[]) {
|
|||||||
return { error: `Skill '${name}' not found` };
|
return { error: `Skill '${name}' not found` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: 去重检查
|
|
||||||
if (activated.has(name)) {
|
if (activated.has(name)) {
|
||||||
console.log(`[Skill] ℹ️ 技能 "${name}" 已在当前会话中激活,跳过重复注入`);
|
console.log(`[Skill] ℹ️ 技能 "${name}" 已在当前会话中激活,跳过重复注入`);
|
||||||
return { already_active: true, message: `技能 "${name}" 已激活,无需重复加载` };
|
return { already_active: true, message: `技能 "${name}" 已激活,无需重复加载` };
|
||||||
}
|
}
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
try {
|
try { content = await fs.readFile(skill.location, "utf-8"); } catch {
|
||||||
content = await fs.readFile(skill.location, "utf-8");
|
|
||||||
} catch {
|
|
||||||
console.log(`[Skill] ❌ 激活失败:无法读取 ${skill.location}`);
|
console.log(`[Skill] ❌ 激活失败:无法读取 ${skill.location}`);
|
||||||
return { error: `Failed to read SKILL.md for '${name}'` };
|
return { error: `Failed to read SKILL.md for '${name}'` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = stripFrontmatter(content);
|
const body = stripFrontmatter(content);
|
||||||
const resources = await listResources(skill.baseDir);
|
const resources = await listResources(skill.baseDir);
|
||||||
|
|
||||||
activated.add(name);
|
activated.add(name);
|
||||||
|
|
||||||
const resourcesXml =
|
console.log(`[Skill] 📖 已激活技能:${skill.name}(${body.length} 字符,${resources.length} 个资源文件)`);
|
||||||
resources.length > 0
|
|
||||||
? `\n<skill_resources>\n${resources.map((f) => ` <file>${f}</file>`).join("\n")}\n</skill_resources>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const wrapped = [
|
const resourcesXml = resources.length > 0
|
||||||
`<skill_content name="${skill.name}">`,
|
? `\n<skill_resources>\n${resources.map((f) => ` <file>${f}</file>`).join("\n")}\n</skill_resources>`
|
||||||
body,
|
: "";
|
||||||
"",
|
|
||||||
`Skill directory: ${skill.baseDir}`,
|
|
||||||
`相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。`,
|
|
||||||
resourcesXml,
|
|
||||||
`</skill_content>`,
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
console.log(
|
return {
|
||||||
`[Skill] 📖 已激活技能:${skill.name}(${body.length} 字符,${resources.length} 个资源文件)`
|
content: [
|
||||||
);
|
`<skill_content name="${skill.name}">`,
|
||||||
|
body,
|
||||||
return { content: wrapped };
|
"",
|
||||||
|
`Skill directory: ${skill.baseDir}`,
|
||||||
|
`相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。`,
|
||||||
|
resourcesXml,
|
||||||
|
`</skill_content>`,
|
||||||
|
].join("\n"),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -273,7 +157,6 @@ function createSkillTools(skills: SkillRecord[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = path.resolve(path.join(skill.baseDir, relPath));
|
const fullPath = path.resolve(path.join(skill.baseDir, relPath));
|
||||||
|
|
||||||
if (!isPathInside(fullPath, skill.baseDir)) {
|
if (!isPathInside(fullPath, skill.baseDir)) {
|
||||||
console.log(`[Skill] 🚫 路径越界已拦截:"${relPath}" 超出技能目录范围`);
|
console.log(`[Skill] 🚫 路径越界已拦截:"${relPath}" 超出技能目录范围`);
|
||||||
return { error: "Access denied: path is outside skill directory" };
|
return { error: "Access denied: path is outside skill directory" };
|
||||||
@ -294,30 +177,29 @@ function createSkillTools(skills: SkillRecord[]) {
|
|||||||
|
|
||||||
// ==================== 对外接口 ====================
|
// ==================== 对外接口 ====================
|
||||||
|
|
||||||
/**
|
export async function useSkill(...segments: string[]) {
|
||||||
* 使用指定 skill(渐进式披露)
|
if (segments.length === 0) return { prompt: "", tools: {} };
|
||||||
*
|
|
||||||
* 遵循 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")]);
|
|
||||||
|
|
||||||
// 过滤出指定 skill
|
const baseDir = path.join(getPath("skills"), ...segments);
|
||||||
const matched = skills.filter((s) => s.name === name);
|
const location = path.join(baseDir, "SKILL.md");
|
||||||
if (matched.length === 0) {
|
|
||||||
console.log(`[Skill] ⚠️ 未发现名为 "${name}" 的技能`);
|
let content: string;
|
||||||
|
try { content = await fs.readFile(location, "utf-8"); } catch {
|
||||||
|
console.log(`[Skill] ⚠️ 未发现技能:${segments.join("/")}`);
|
||||||
return { prompt: "", tools: {} };
|
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 {
|
return {
|
||||||
prompt: buildCatalog(matched),
|
prompt: buildCatalog(skill),
|
||||||
tools: createSkillTools(matched),
|
tools: createSkillTools([skill]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user