# Conflicts:
#	src/router.ts
#	src/types/database.d.ts
This commit is contained in:
zhishi 2026-03-26 11:37:38 +08:00
commit 50359dc997
47 changed files with 1332 additions and 280 deletions

View File

@ -21,7 +21,7 @@ description: >
### 提取衍生资产流程
1. 调用 `get_flowData` 分别获取 `script`(剧本)和 `assets`(现有资产列表)
2. 根据[衍生资产提取](references/derive-assets-extraction.md)文档中的提取原则,分析剧本内容,为每个角色资产识别出关联的衍生资产(道具、服饰、法器、座驾、场景物件等)
2. 根据[衍生资产提取](references/derive_assets_extraction.md)文档中的提取原则,分析剧本内容,为每个角色资产识别出关联的衍生资产(道具、服饰、法器、座驾、场景物件等)
3. 对每个有衍生状态的资产调用 `set_flowData_assets` 保存
4. 告知用户提取完成,列出为每个角色提取的衍生资产概要
5. **询问用户是否需要生成衍生资产图片**
@ -32,7 +32,7 @@ description: >
### 生成分镜表流程
1. 调用 `get_flowData` 分别获取 `script`(剧本)和 `assets`(现有资产列表)
2. 根据[分镜表生成](references/storyboard-generation.md)文档中的拆分原则和字段填写指引将剧本拆分为分镜填写每条分镜的所有字段id、title、description、camera、duration、frameMode、prompt、lines、sound、associateAssetsIds
2. 根据[分镜表生成](references/storyboard_generation.md)文档中的拆分原则和字段填写指引将剧本拆分为分镜填写每条分镜的所有字段id、title、description、camera、duration、frameMode、prompt、lines、sound、associateAssetsIds
3. 调用 `set_flowData({ key: "storyboard", value: 分镜数组 })` 一次性保存完整分镜表
4. 告知用户分镜表生成完成,列出分镜概要(总条数、主要场景)
5. **询问用户是否需要生成分镜图片**
@ -43,7 +43,7 @@ description: >
本技能附带以下参考资料,根据任务需要使用 `read_skill_file` 工具按需加载:
- [衍生资产提取](references/derive-assets-extraction.md) — 从剧本和角色资产中提取衍生资产的原则和示例
- [分镜表生成](references/storyboard-generation.md) — 从剧本和资产生成分镜表的拆分原则、字段规范和示例
- [衍生资产提取](references/derive_assets_extraction.md) — 从剧本和角色资产中提取衍生资产的原则和示例
- [分镜表生成](references/storyboard_generation.md) — 从剧本和资产生成分镜表的拆分原则、字段规范和示例
**注意**:根据用户当前任务选择性加载对应参考资料,不要一次性全部加载。

View File

@ -0,0 +1,77 @@
# 烤龙肉的做法
## 前言
龙肉,作为幻想世界中最珍贵的食材之一,其烹饪方法自古以来就是冒险者和宫廷厨师们追求的终极秘技。以下是一份经典的烤龙肉食谱。
---
## 食材准备
| 食材 | 用量 | 备注 |
|------|------|------|
| 龙肉(里脊或腿肉) | 2公斤 | 需提前放血处理 |
| 龙息椒粉 | 2大勺 | 普通辣椒粉可替代 |
| 凤凰蛋黄 | 3个 | 鸡蛋黄可替代 |
| 精灵橄榄油 | 100毫升 | 特级初榨橄榄油可替代 |
| 矮人黑盐 | 适量 | 海盐可替代 |
| 迷迭香、百里香 | 各一把 | 新鲜为佳 |
| 蒜头 | 8瓣 | 拍碎 |
| 蜂蜜 | 3大勺 | 用于表面刷酱 |
---
## 烹饪步骤
### 第一步:腌制龙肉(需提前一天)
1. 将龙肉洗净,用厨房纸巾吸干表面水分
2. 用锋利的刀在肉面划几道深约1厘米的斜纹便于入味
3. 将蛋黄、橄榄油、龙息椒粉、黑盐混合,调成腌料
4. 均匀涂抹在龙肉表面,放入密封容器
5. 冷藏腌制12-24小时
### 第二步:准备香草束
1. 将迷迭香、百里香捆扎成束
2. 蒜瓣拍碎备用
3. 准备好蜂蜜刷酱
### 第三步:烤制
1. **预热烤炉**将烤炉预热至220°C若使用龙焰烤炉调至"青龙"档位)
2. **初次高温烤制**
- 将腌制好的龙肉放入烤盘
- 铺上香草束和蒜瓣
- 220°C烤制20分钟封住肉汁
3. **转中温慢烤**
- 温度降至160°C
- 每隔20分钟翻面一次并刷上蜂蜜
- 继续烤制约1.5-2小时视肉块大小调整
4. **检验熟度**
- 用探针温度计测量中心温度达65°C为五分熟
- 达75°C为全熟
- 龙肉建议七分熟约70°C口感最佳
### 第四步:静置与切片
1. 出炉后用锡纸包裹静置15分钟
2. 逆着纹理切成1厘米厚的片状
3. 摆盘,淋上烤盘中的肉汁
---
## 小贴士
- ⚠️ **安全提示**:处理龙肉时请佩戴防火手套,残余龙息可能引发灼伤
- 🍷 **搭配推荐**:建议搭配精灵白葡萄酒或矮人麦芽啤酒
- 🔥 若龙肉带有冰属性如霜龙烤制时间需延长30%
---
## 成品效果
外皮金黄酥脆,内部肉质鲜嫩多汁,带有独特的硫磺香气与蜂蜜的甜润,回味中隐约有龙息的微辣感。

View File

@ -1,5 +1,5 @@
---
name: universal-agent
name: universal_agent
description: 专注于从小说原文中提取结构化事件信息的助手。
---

View File

@ -1,5 +1,5 @@
---
name: universal-agent
name: universal_agent
description: 专注于从小说原文中提取角色信息并生成视觉化角色描述的助手。
---

View File

@ -1,5 +1,5 @@
---
name: universal-agent
name: universal_agent
description: 专注于从小说原文中提取道具物品信息并生成视觉化描述的助手。
---

View File

@ -1,5 +1,5 @@
---
name: universal-agent
name: universal_agent
description: 专注于从小说原文中提取场景信息并生成视觉化场景描述的助手。
---

View File

@ -206,7 +206,7 @@ set_flowData({
camera: "中景 · 缓慢推进",
duration: 3,
frameMode: "endFrame",
prompt: "A white-haired young man in white robes kneeling on the floor, spitting blood, trembling body, pale face, dramatic lighting, cinematic composition",
prompt: "A white_haired young man in white robes kneeling on the floor, spitting blood, trembling body, pale face, dramatic lighting, cinematic composition",
lines: null,
sound: "喷血声",
associateAssetsIds: [0]
@ -218,7 +218,7 @@ set_flowData({
camera: "大特写",
duration: 3,
frameMode: "firstFrame",
prompt: "Close-up of a jade token with glowing runes fading, fine cracks appearing on the surface, dark moody lighting, cinematic detail shot",
prompt: "Close_up of a jade token with glowing runes fading, fine cracks appearing on the surface, dark moody lighting, cinematic detail shot",
lines: null,
sound: "玉石碎裂声",
associateAssetsIds: [2]

View File

@ -1,5 +1,5 @@
---
name: universal-agent
name: universal_agent
description: 专注于从视频分镜提示词中提取结构化台词、旁白与音效信息的助手。
---

View File

@ -77,7 +77,7 @@ const planData = {
4. 生成汇总统计(总章节、强主线章节数、可压缩章节、预估总时长、目标时长、压缩比)
5. 输出 Markdown 表格格式的事件表,作为后续任务上下文(不写入 planData
**输出格式**:参考 [event-format.md](references/event-format.md)
**输出格式**:参考 [event_format.md](references/event_format.md)
**关键原则**
- 核心事件描述必须包含**动作**和**结果**,不能只写状态
@ -115,7 +115,7 @@ const planData = {
7. 设计付费卡点(位置、内容、钩子类型)
8. 调用 `set_planData_storySkeleton` 保存
**输出格式**:参考 [skeleton-format.md](references/skeleton-format.md)
**输出格式**:参考 [skeleton_format.md](references/skeleton_format.md)
**关键约束**
- 总时长 = 【项目配置】中的集数 × 单集时长
@ -145,7 +145,7 @@ const planData = {
- 角色态度作为世界观锚点
5. 调用 `set_planData_adaptationStrategy` 保存
**输出格式**:参考 [adaptation-format.md](references/adaptation-format.md)
**输出格式**:参考 [adaptation_format.md](references/adaptation_format.md)
**关键原则**
- 故事核优先:主角是"被定义为疯子却选择活下去的人",所有决策服务于此弧线
@ -179,7 +179,7 @@ const planData = {
- 表演指示(情绪、动作细节)
6. 仅当指令中包含 `用户已确认写入SQL: 是` 时,调用 `insert_script_to_sqlite` 写入剧本
**输出格式**:参考 [script-format.md](references/script-format.md)
**输出格式**:参考 [script_format.md](references/script_format.md)
**关键约束**
- 单集总时长严格控制在【项目配置】指定的单集时长 ±10秒

View File

@ -84,7 +84,7 @@ description: >-
| 转场标注 | 节拍间转场方式明确 | 轻微 |
| 情绪连贯 | 节拍间情绪过渡自然 | 中等 |
详细审核标准请参考 [quality-criteria.md](references/quality-criteria.md)。
详细审核标准请参考 [quality_criteria.md](references/quality_criteria.md)。
## 审核报告格式

View File

@ -1,5 +1,5 @@
---
name: universal-agent
name: universal_agent
description: 通用文本分析与内容提取 Agent支持小说事件提取、视频台词提取、角色/场景/道具资产描述生成等多种结构化内容处理任务。
---
@ -11,36 +11,36 @@ description: 通用文本分析与内容提取 Agent支持小说事件提取
你拥有以下参考技能references根据用户请求自动匹配对应技能执行
### 1. 小说章节事件提取event-extract
### 1. 小说章节事件提取event_extract
- **触发条件**:用户提供小说原文,要求提取章节事件、生成事件表
- **参考文件**`references/event-extract.md`
- **参考文件**`references/event_extract.md`
- **输出**:结构化事件表格(章节、角色、核心事件、主线关系、信息密度、预估集长、情绪强度)+ 汇总统计
### 2. 视频提示词台词提取video-dialogue-extract
### 2. 视频提示词台词提取video_dialogue_extract
- **触发条件**:用户提供视频分镜提示词/画面描述,要求从中提取或还原台词、旁白、音效文本
- **参考文件**`references/video-dialogue-extract.md`
- **参考文件**`references/video_dialogue_extract.md`
- **输出**:结构化台词表(镜号、角色、台词内容、台词类型、情绪标注、时长估算)
### 3. 小说角色提取novel-character-extract
### 3. 小说角色提取novel_character_extract
- **触发条件**:用户提供小说原文,要求提取角色信息、生成角色视觉描述
- **参考文件**`references/novel-character-extract.md`
- **参考文件**`references/novel_character_extract.md`
- **资产类型**`role`(对应 `o_assets.type = "role"`
- **输出**:结构化角色资产表(角色名称、角色定位、外貌特征、服饰描述、体型体态、标志性特征、性格气质、首次出场、出场章节数、状态变体)+ 核心角色卡片
### 4. 小说场景提取novel-scene-extract
### 4. 小说场景提取novel_scene_extract
- **触发条件**:用户提供小说原文,要求提取场景/地点信息、生成场景视觉描述
- **参考文件**`references/novel-scene-extract.md`
- **参考文件**`references/novel_scene_extract.md`
- **资产类型**`scene`(对应 `o_assets.type = "scene"`
- **输出**:结构化场景资产表(场景名称、场景类型、空间描述、光照氛围、关键陈设、色调基调、首次出场、出场章节数、关联角色、状态变体)+ 核心场景卡片
### 5. 小说道具提取novel-props-extract
### 5. 小说道具提取novel_props_extract
- **触发条件**:用户提供小说原文,要求提取道具/物品/器物信息、生成道具视觉描述
- **参考文件**`references/novel-props-extract.md`
- **参考文件**`references/novel_props_extract.md`
- **资产类型**`tool`(对应 `o_assets.type = "tool"`
- **输出**:结构化道具资产表(道具名称、类别、外观描述、尺寸参考、材质质感、功能/用途、首次出场、关联角色、状态变体)+ 高频道具排名

58
skillList.json Normal file
View File

@ -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"
}
]

View File

@ -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);

View File

@ -493,7 +493,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
resTool.systemMessage(`图片生成调度计划:共 ${levels.length} 层,${images.length} 张图片`);
// --- 准备公共数据 ---
const skill = await useSkill("universal-agent");
const skill = await useSkill("universal_agent.md");
const projectData = await u.db("o_project").where("id", resTool.data.projectId).select("videoRatio").first();
const imageModel = resTool.data.imageModel;
@ -596,7 +596,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
),
}),
execute: async ({ images }) => {
const skill = await useSkill("universal-agent");
const skill = await useSkill("universal_agent.md");
console.log("[tools] generate_assets_images", images);
//先获取到前端资产数据
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));

View File

@ -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);

View File

@ -118,6 +118,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
type: i.type,
describe: i.desc,
projectId: resTool.data.projectId,
startTime: Date.now(),
});
assetId.push(id);
}

View File

@ -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);

View File

@ -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;
@ -28,6 +30,8 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
builder: (table) => {
table.integer("id");
table.string("projectType");
table.string("imageModel");
table.string("videoModel");
table.text("name");
table.text("intro");
table.text("type");
@ -397,24 +401,6 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
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",
@ -445,6 +431,386 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
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) {

View File

@ -1,4 +1,4 @@
// @routes-hash ced882fe9cc49f6e16ade49cf276b583
// @routes-hash e48d3637c019a24988e008910e734d8c
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -49,45 +49,50 @@ import route45 from "./routes/production/getFlowData";
import route46 from "./routes/production/getProductionData";
import route47 from "./routes/production/getStoryboardData";
import route48 from "./routes/production/saveFlowData";
import route49 from "./routes/production/storyboard/downPreviewImage";
import route50 from "./routes/production/storyboard/getStoryboardData";
import route51 from "./routes/production/storyboard/previewImage";
import route52 from "./routes/production/workbench/confirmSelection";
import route53 from "./routes/production/workbench/delVideo";
import route54 from "./routes/production/workbench/generateVideo";
import route55 from "./routes/production/workbench/getChatLines";
import route56 from "./routes/production/workbench/getVideoModelDetail";
import route57 from "./routes/production/workbench/videoPolling";
import route58 from "./routes/project/addProject";
import route59 from "./routes/project/delProject";
import route60 from "./routes/project/editProject";
import route61 from "./routes/project/getProject";
import route62 from "./routes/script/addScript";
import route63 from "./routes/script/delScript";
import route64 from "./routes/script/exportScript";
import route65 from "./routes/script/getScrptApi";
import route66 from "./routes/script/updateScript";
import route67 from "./routes/scriptAgent/getPlanData";
import route68 from "./routes/scriptAgent/setPlanData";
import route69 from "./routes/setting/agentDeploy/agentSetKey";
import route70 from "./routes/setting/agentDeploy/deployAgentModel";
import route71 from "./routes/setting/agentDeploy/getAgentDeploy";
import route72 from "./routes/setting/dbConfig/clearData";
import route73 from "./routes/setting/fileManagement/openFolder";
import route74 from "./routes/setting/getTextModel";
import route75 from "./routes/setting/loginConfig/getUser";
import route76 from "./routes/setting/loginConfig/updateUserPwd";
import route77 from "./routes/setting/memoryConfig/getMemory";
import route78 from "./routes/setting/memoryConfig/sureMemory";
import route79 from "./routes/setting/vendorConfig/addVendor";
import route80 from "./routes/setting/vendorConfig/deleteVendor";
import route81 from "./routes/setting/vendorConfig/getVendorList";
import route82 from "./routes/setting/vendorConfig/modelTest";
import route83 from "./routes/setting/vendorConfig/updateVendor";
import route84 from "./routes/task/getTaskApi";
import route85 from "./routes/task/getTaskCategories";
import route86 from "./routes/task/taskDetails";
import route87 from "./routes/test/test";
import route49 from "./routes/production/storyboard/previewImage";
import route50 from "./routes/production/workbench/confirmSelection";
import route51 from "./routes/production/workbench/delVideo";
import route52 from "./routes/production/workbench/generateVideo";
import route53 from "./routes/production/workbench/getChatLines";
import route54 from "./routes/production/workbench/getVideoModelDetail";
import route55 from "./routes/production/workbench/videoPolling";
import route56 from "./routes/project/addProject";
import route57 from "./routes/project/delProject";
import route58 from "./routes/project/editProject";
import route59 from "./routes/project/getProject";
import route60 from "./routes/script/addScript";
import route61 from "./routes/script/delScript";
import route62 from "./routes/script/exportScript";
import route63 from "./routes/script/getScrptApi";
import route64 from "./routes/script/updateScript";
import route65 from "./routes/scriptAgent/getPlanData";
import route66 from "./routes/scriptAgent/setPlanData";
import route67 from "./routes/setting/agentDeploy/agentSetKey";
import route68 from "./routes/setting/agentDeploy/deployAgentModel";
import route69 from "./routes/setting/agentDeploy/getAgentDeploy";
import route70 from "./routes/setting/dbConfig/clearData";
import route71 from "./routes/setting/fileManagement/openFolder";
import route72 from "./routes/setting/getTextModel";
import route73 from "./routes/setting/loginConfig/getUser";
import route74 from "./routes/setting/loginConfig/updateUserPwd";
import route75 from "./routes/setting/memoryConfig/getMemory";
import route76 from "./routes/setting/memoryConfig/sureMemory";
import route77 from "./routes/setting/skillManagement/addSkill";
import route78 from "./routes/setting/skillManagement/deleteSkill";
import route79 from "./routes/setting/skillManagement/embeddingSkill";
import route80 from "./routes/setting/skillManagement/generateDescription";
import route81 from "./routes/setting/skillManagement/getSkillList";
import route82 from "./routes/setting/skillManagement/scanSkills";
import route83 from "./routes/setting/skillManagement/updateSkill";
import route84 from "./routes/setting/vendorConfig/addVendor";
import route85 from "./routes/setting/vendorConfig/deleteVendor";
import route86 from "./routes/setting/vendorConfig/getVendorList";
import route87 from "./routes/setting/vendorConfig/modelTest";
import route88 from "./routes/setting/vendorConfig/updateVendor";
import route89 from "./routes/task/getTaskApi";
import route90 from "./routes/task/getTaskCategories";
import route91 from "./routes/task/taskDetails";
import route92 from "./routes/test/test";
export default async (app: Express) => {
app.use("/api/agents/clearMemory", route1);
@ -138,43 +143,48 @@ export default async (app: Express) => {
app.use("/api/production/getProductionData", route46);
app.use("/api/production/getStoryboardData", route47);
app.use("/api/production/saveFlowData", route48);
app.use("/api/production/storyboard/downPreviewImage", route49);
app.use("/api/production/storyboard/getStoryboardData", route50);
app.use("/api/production/storyboard/previewImage", route51);
app.use("/api/production/workbench/confirmSelection", route52);
app.use("/api/production/workbench/delVideo", route53);
app.use("/api/production/workbench/generateVideo", route54);
app.use("/api/production/workbench/getChatLines", route55);
app.use("/api/production/workbench/getVideoModelDetail", route56);
app.use("/api/production/workbench/videoPolling", route57);
app.use("/api/project/addProject", route58);
app.use("/api/project/delProject", route59);
app.use("/api/project/editProject", route60);
app.use("/api/project/getProject", route61);
app.use("/api/script/addScript", route62);
app.use("/api/script/delScript", route63);
app.use("/api/script/exportScript", route64);
app.use("/api/script/getScrptApi", route65);
app.use("/api/script/updateScript", route66);
app.use("/api/scriptAgent/getPlanData", route67);
app.use("/api/scriptAgent/setPlanData", route68);
app.use("/api/setting/agentDeploy/agentSetKey", route69);
app.use("/api/setting/agentDeploy/deployAgentModel", route70);
app.use("/api/setting/agentDeploy/getAgentDeploy", route71);
app.use("/api/setting/dbConfig/clearData", route72);
app.use("/api/setting/fileManagement/openFolder", route73);
app.use("/api/setting/getTextModel", route74);
app.use("/api/setting/loginConfig/getUser", route75);
app.use("/api/setting/loginConfig/updateUserPwd", route76);
app.use("/api/setting/memoryConfig/getMemory", route77);
app.use("/api/setting/memoryConfig/sureMemory", route78);
app.use("/api/setting/vendorConfig/addVendor", route79);
app.use("/api/setting/vendorConfig/deleteVendor", route80);
app.use("/api/setting/vendorConfig/getVendorList", route81);
app.use("/api/setting/vendorConfig/modelTest", route82);
app.use("/api/setting/vendorConfig/updateVendor", route83);
app.use("/api/task/getTaskApi", route84);
app.use("/api/task/getTaskCategories", route85);
app.use("/api/task/taskDetails", route86);
app.use("/api/test/test", route87);
app.use("/api/production/storyboard/previewImage", route49);
app.use("/api/production/workbench/confirmSelection", route50);
app.use("/api/production/workbench/delVideo", route51);
app.use("/api/production/workbench/generateVideo", route52);
app.use("/api/production/workbench/getChatLines", route53);
app.use("/api/production/workbench/getVideoModelDetail", route54);
app.use("/api/production/workbench/videoPolling", route55);
app.use("/api/project/addProject", route56);
app.use("/api/project/delProject", route57);
app.use("/api/project/editProject", route58);
app.use("/api/project/getProject", route59);
app.use("/api/script/addScript", route60);
app.use("/api/script/delScript", route61);
app.use("/api/script/exportScript", route62);
app.use("/api/script/getScrptApi", route63);
app.use("/api/script/updateScript", route64);
app.use("/api/scriptAgent/getPlanData", route65);
app.use("/api/scriptAgent/setPlanData", route66);
app.use("/api/setting/agentDeploy/agentSetKey", route67);
app.use("/api/setting/agentDeploy/deployAgentModel", route68);
app.use("/api/setting/agentDeploy/getAgentDeploy", route69);
app.use("/api/setting/dbConfig/clearData", route70);
app.use("/api/setting/fileManagement/openFolder", route71);
app.use("/api/setting/getTextModel", route72);
app.use("/api/setting/loginConfig/getUser", route73);
app.use("/api/setting/loginConfig/updateUserPwd", route74);
app.use("/api/setting/memoryConfig/getMemory", route75);
app.use("/api/setting/memoryConfig/sureMemory", route76);
app.use("/api/setting/skillManagement/addSkill", route77);
app.use("/api/setting/skillManagement/deleteSkill", route78);
app.use("/api/setting/skillManagement/embeddingSkill", route79);
app.use("/api/setting/skillManagement/generateDescription", route80);
app.use("/api/setting/skillManagement/getSkillList", route81);
app.use("/api/setting/skillManagement/scanSkills", route82);
app.use("/api/setting/skillManagement/updateSkill", route83);
app.use("/api/setting/vendorConfig/addVendor", route84);
app.use("/api/setting/vendorConfig/deleteVendor", route85);
app.use("/api/setting/vendorConfig/getVendorList", route86);
app.use("/api/setting/vendorConfig/modelTest", route87);
app.use("/api/setting/vendorConfig/updateVendor", route88);
app.use("/api/task/getTaskApi", route89);
app.use("/api/task/getTaskCategories", route90);
app.use("/api/task/taskDetails", route91);
app.use("/api/test/test", route92);
}

View File

@ -98,7 +98,7 @@ export default router.post(
const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[];
const novelText = mergeNovelText(novelData);
const skill = await useSkill("universal-agent");
const skill = await useSkill("universal_agent.md");
const systemPrompt = `${skill.prompt}

View File

@ -28,6 +28,7 @@ export default router.post(
.db("o_assets")
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
.select("o_assets.*", "o_image.filePath")
// @ts-ignore
.where("o_assets.id", "in", assetIds)
.whereNull("o_assets.assetsId")
.where("o_assets.projectId", projectId);
@ -36,9 +37,9 @@ export default router.post(
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
.select("o_assets.*", "o_image.filePath")
.where("o_assets.projectId", projectId)
// @ts-ignore
.where("o_assets.id", "in", assetIds)
.whereNotNull("o_assets.assetsId");
console.log("%c Line:35 🥚 childAssetsData", "background:#f5ce50", childAssetsData);
if (!sqlData) {
const flowData: FlowData = {

View File

@ -30,7 +30,7 @@ export default router.post(
);
async function getLines(prompt: string) {
const skill = await useSkill("eventExtract-agent");
const skill = await useSkill("universal_agent.md");
const resText = await u.Ai.Text("universalAgent").invoke({
system: skill.prompt,

View File

@ -15,9 +15,11 @@ export default router.post(
type: z.string(),
artStyle: z.string(),
videoRatio: z.string(),
imageModel: z.string(),
videoModel: z.string(),
}),
async (req, res) => {
const { projectType, name, intro, type, artStyle, videoRatio } = req.body;
const { projectType, name, intro, type, artStyle, videoRatio, imageModel, videoModel } = req.body;
await u.db("o_project").insert({
projectType,
@ -27,6 +29,8 @@ export default router.post(
artStyle,
videoRatio,
userId: 1,
imageModel,
videoModel,
createTime: Date.now(),
});

View File

@ -15,9 +15,11 @@ export default router.post(
type: z.string(),
artStyle: z.string(),
videoRatio: z.string(),
imageModel: z.string(),
videoModel: z.string(),
}),
async (req, res) => {
const { id, name, intro, type, artStyle, videoRatio } = req.body;
const { id, name, intro, type, artStyle, videoRatio, imageModel, videoModel } = req.body;
await u.db("o_project").where("id", id).update({
name,
@ -25,6 +27,8 @@ export default router.post(
type,
artStyle,
videoRatio,
imageModel,
videoModel,
});
res.status(200).send(success({ message: "新增项目成功" }));

View File

@ -22,11 +22,8 @@ export default router.post(
const assetsData = await u
.db("o_assets")
.leftJoin("o_scriptAssets", "o_assets.id", "o_scriptAssets.assetId")
.where(
"o_scriptAssets.scriptId",
"in",
data.map((i) => i.id),
)
// @ts-ignore
.whereIn( "o_scriptAssets.scriptId", data.map((i) => i.id))
.select("o_assets.id", "o_assets.name", "o_scriptAssets.scriptId");
const scriptAssetsMap: Record<number, { id: number; name: string }[]> = {};
assetsData.forEach((i) => {

View File

@ -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 || "新增技能失败"));
}
},
);

View File

@ -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 || "删除技能失败"));
}
},
);

View File

@ -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("技能向量生成成功"));
},
);

View File

@ -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));
},
);

View File

@ -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<string, string[]>();
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),
}),
);
},
);

View File

@ -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<string>();
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),
}),
);
});

View File

@ -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 || "更新技能失败"));
}
},
);

View File

@ -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: `如何烹饪龙肉` }],
tools: skill.tools,
});
res.send(result.text);
});

View File

@ -1,25 +1,6 @@
// @db-hash 71b2e55243e59382321a140a8d9a64ff
// @db-hash a4883a3df3fda68054d467ee6dd26523
//该文件由脚本自动生成,请勿手动修改
export interface _o_storyboard_old_20260325 {
'camera'?: string | null;
'createTime'?: number | null;
'description'?: string | null;
'duration'?: string | null;
'filePath'?: string | null;
'frameMode'?: string | null;
'id'?: number;
'lines'?: string | null;
'mode'?: string | null;
'model'?: string | null;
'prompt'?: string | null;
'reason'?: string | null;
'resolution'?: string | null;
'scriptId'?: number | null;
'sound'?: string | null;
'state'?: string | null;
'title'?: string | null;
}
export interface memories {
'content': string;
'createTime': number;
@ -128,6 +109,7 @@ export interface o_project {
'createTime'?: number | null;
'id'?: number | null;
'intro'?: string | null;
'model'?: string | null;
'name'?: string | null;
'projectType'?: string | null;
'type'?: string | null;
@ -149,6 +131,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;
@ -221,7 +219,6 @@ export interface o_videoConfig {
}
export interface DB {
"_o_storyboard_old_20260325": _o_storyboard_old_20260325;
"memories": memories;
"o_agentDeploy": o_agentDeploy;
"o_agentWorkData": o_agentWorkData;
@ -239,6 +236,8 @@ 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_tasks": o_tasks;
"o_user": o_user;

View File

@ -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,127 @@ 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<string[]> {
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<SkillRecord | null> {
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) => ` <skill>\n <name>${s.name}</name>\n <description>${s.description}</description>\n </skill>`).join("\n");
function buildPrompt(skill: SkillRecord): string {
return `## Skills
activate_skill
read_skill_file
<available_skills>
${entries}
<skill>
<name>${skill.name}</name>
<description>${skill.description}</description>
</skill>
</available_skills>`;
}
// ==================== 激活 + 执行工具 ====================
function createSkillTools(skills: SkillRecord[]) {
function createSkillTools(skill: SkillRecord, mainSkillName: string) {
const activated = new Set<string>();
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);
console.log("%c Line:120 🌮 resources", "background:#b03734", resources);
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<skill_resources>\n${resources.map((f) => ` <file>${f}</file>`).join("\n")}\n</skill_resources>` : "";
return {
content: `<skill_content name="${skill.name}">
${body}
Skill directory: ${skill.baseDir}
使 read_skill_file
${resourcesXml}
</skill_content>`,
};
console.log(`[Skill] 📖 已激活:${name}${body.length} 字符,${resources.length} 资源)`);
let content = "";
content = `<skill_content name="${name}">\n`;
content += body + "\n\n";
content += `Skill directory: ${skill.baseDir}\n`;
content += "相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。\n";
if (resources.length > 0) {
content += "\n<skill_resources>\n";
for (const { path } of resources) {
content += ` <file>${path}</file>\n`;
}
content += "</skill_resources>\n";
}
content += "\n<skill_tools_guide>\n";
content += "- read_skill_file读取上方 skill_resources 中列出的资源文件。\n";
content += "- discover_skill_docs当上方资源不足以完成任务时使用关键词检索更多相关文档。传入与当前任务相关的关键词列表即可获取推荐。\n";
content += "</skill_tools_guide>\n";
content += "</skill_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 +184,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 +201,3 @@ ${resourcesXml}
}),
};
}
// ==================== 对外接口 ====================
export async function useSkill(...segments: string[]) {
if (segments.length === 0) return { prompt: "", tools: {} };
const skills = new Map<string, SkillRecord>();
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) };
}

View File

@ -29,7 +29,7 @@ class CleanNovel {
const novel = allChapters[gi];
let resData;
try {
const skill = await useSkill("universal-agent");
const skill = await useSkill("universal_agent.md");
resData = await intansce.invoke({
system: skill.prompt,