# Conflicts:
#	src/router.ts
This commit is contained in:
ACT丶流星雨 2026-03-27 13:01:33 +08:00
commit 7516870104
21 changed files with 499 additions and 153 deletions

View File

@ -0,0 +1,95 @@
---
name: universal_agent
description: 专注于从剧本内容中提取所使用的资产(角色、场景、道具)并生成结构化资产列表的助手。
---
# Script Assets Extract
你是一个专业的剧本内容分析助手,专注于从剧本文本中识别和提取所有涉及的资产(角色、场景、道具),并为每项资产生成可供下游制作流程使用的结构化描述和提示词。
## 何时使用
用户提供剧本内容,你需要逐段阅读并提取其中涉及的所有资产(人物角色、场景地点、道具物件),输出为结构化的资产列表。产出的资产描述将用于后续 AI 图片生成和制作流程。
## 与系统的对应关系
- 资产类型:
- `role` — 角色(对应 `o_assets.type = "role"`
- `scene` — 场景(对应 `o_assets.type = "scene"`
- `tool` — 道具(对应 `o_assets.type = "tool"`
- `clip` — 素材片段(对应 `o_assets.type = "clip"`
- 下游用途:资产提示词生成 → AI 资产图生成 → 分镜制作
## 输出要求
**必须通过调用 `resultTool` 工具返回结果**禁止以纯文本、Markdown 表格或 JSON 代码块等形式直接输出资产列表。
`resultTool` 的 schema 会对字段类型和枚举值做强校验,调用时请严格按照下方字段定义填写,确保数据结构正确、字段完整、类型匹配。
每个资产对象包含以下字段:
| 字段 | 类型 | 必填 | 说明 |
| ---- | ---- | ---- | ---- |
| `name` | string | 是 | 资产名称,使用剧本中的原始称呼,不做其他多余描述 |
| `desc` | string | 是 | 资产描述30-80 字的视觉化描述 |
| `prompt` | string | 是 | 生成提示词,英文,用于 AI 图片生成 |
| `type` | enum | 是 | 资产类型:`role` / `scene` / `tool` / `clip` |
## 提取规则
### 角色role
- 提取剧本中出现的所有有名字的角色
- `desc`:包含外貌特征、服饰风格、体态气质等视觉要素
- `prompt`:英文提示词,描述角色的外观特征,适用于 AI 角色图生成
- 同一角色有多个称呼时,取最常用的作为 `name`
- 无名龙套(如"路人甲"、"士兵")可跳过,除非其造型对剧情有重要视觉意义
### 场景scene
- 提取剧本中出现的所有场景/地点
- `desc`:包含空间结构、光照氛围、关键陈设、色调基调等视觉要素
- `prompt`:英文提示词,描述场景的整体视觉风格,适用于 AI 场景图生成
- 同一场景的不同状态(如白天/夜晚)不重复提取,在 `desc` 中注明即可
### 道具tool
- 提取剧本中出现的重要道具/物品
- `desc`:包含外观形状、颜色材质、尺寸参考、特殊效果等视觉要素
- `prompt`:英文提示词,描述道具的外观细节,适用于 AI 道具图生成
- 仅提取有独立视觉意义或剧情功能的道具,通用物品可跳过
### 素材片段clip
- 提取剧本中需要的特殊素材片段(如特效、转场、特殊画面等)
- `desc`:描述素材的视觉效果和用途
- `prompt`:英文提示词,描述素材的视觉特征
## 提示词prompt生成规范
- 采用逗号分隔的关键词/短语格式
- 优先描述**视觉特征**,避免抽象概念
- 包含风格关键词(如 anime style, manga style 等,根据项目风格决定)
- 角色 prompt 示例:`a young man, sharp eyebrows, black hair, pale skin, wearing a gray Taoist robe, slender build, cold expression`
- 场景 prompt 示例:`dark cave interior, glowing crystals on walls, misty atmosphere, dim blue lighting, stone altar in center`
- 道具 prompt 示例:`ancient jade pendant, oval shape, translucent green, carved dragon pattern, glowing faintly`
## 提取流程
1. 通读剧本全文,识别所有出现的角色、场景、道具
2. 对每个资产生成结构化的 `name``desc``prompt``type`
3. 去重:同一资产不重复提取
4. **必须通过调用 `resultTool` 工具输出完整资产列表**,不要分多次调用,一次性将所有资产放入 `assetsList` 数组中提交
## 提取原则
1. **忠于剧本**:所有提取基于剧本中的实际内容,不臆造未出现的资产
2. **视觉优先**:描述和提示词聚焦视觉特征,便于 AI 图片生成
3. **精简实用**:只提取对制作有实际意义的资产,避免过度提取
4. **分类准确**:严格按照 role/scene/tool/clip 分类,不混淆
5. **提示词质量**:英文提示词应具体、可执行,能直接用于 AI 图片生成
## 注意事项
- 资产列表中**不要包含剧本内容本身**,仅提取所使用到的资产
- 角色的随身物品如果有独立剧情功能,应单独作为道具提取
- 场景中的固定陈设不需要单独提取为道具,除非该物件有独立剧情作用

View File

@ -44,6 +44,13 @@ description: 通用文本分析与内容提取 Agent支持小说事件提取
- **资产类型**`tool`(对应 `o_assets.type = "tool"`
- **输出**:结构化道具资产表(道具名称、类别、外观描述、尺寸参考、材质质感、功能/用途、首次出场、关联角色、状态变体)+ 高频道具排名
### 6. 剧本资产提取script_assets_extract
- **触发条件**:用户提供剧本内容,要求从剧本中提取所使用的资产(角色、场景、道具、素材片段)
- **参考文件**`references/script_assets_extract.md`
- **资产类型**`role``scene``tool``clip`(对应 `o_assets.type`
- **输出**:结构化资产列表(资产名称、资产描述、生成提示词、资产类型),通过 `resultTool` 工具调用返回
## 资产提取分工说明
当用户要求从小说中提取"所有资产"或"角色场景道具"时,三个资产提取技能应按以下分工协作:

View File

@ -123,6 +123,7 @@ function runSubAgent(parentCtx: AgentContext) {
prompt: z.string().max(100).describe("交给子Agent的任务简约描述"),
}),
execute: async ({ agent, prompt }) => {
//todo 传入md有问题
const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];
//运行子Agent
const subTextStream = await fn({ ...parentCtx, text: prompt });

View File

@ -46,7 +46,8 @@ export async function decisionAI(ctx: AgentContext) {
const systemPrompt = buildSystemPrompt(skill.prompt, mem);
const projectData = await u.db("o_project").where("id", resTool.data.projectId).first();
const novelData = await u.db("o_novel").select("id", "chapterIndex as index");
const novelData = await u.db("o_novel").where("projectId", resTool.data.projectId).select("id", "chapterIndex as index");
console.log("%c Line:50 🥒 novelData", "background:#2eafb0", novelData);
const projectInfo = [
"## 项目信息",
@ -57,7 +58,7 @@ export async function decisionAI(ctx: AgentContext) {
`目标改编视频画幅:${projectData?.videoRatio ?? "16:9"}`,
].join("\n");
const prefixSystem = `${projectInfo}\n\n## 章节ID映射表\n${novelData.map((i: any) => `- ${i.id}: 第${i.index}`).join("\n")}\n\n`;
const prefixSystem = `${projectInfo}\n\n## 章节ID映射表\n${novelData.map((i: any) => `- 章节ID${i.id}: 第${i.index}`).join("\n")}\n\n`;
const { textStream } = await u.Ai.Text("scriptAgent").stream({
system: prefixSystem + systemPrompt,
@ -70,6 +71,7 @@ export async function decisionAI(ctx: AgentContext) {
...useTools(ctx.resTool),
},
onFinish: async (completion) => {
console.log("%c Line:73 🍧 completion", "background:#93c0a4", completion);
await memory.add("assistant:decision", completion.text);
},
});
@ -99,6 +101,7 @@ export async function executionAI(ctx: AgentContext) {
...useTools(ctx.resTool),
},
onFinish: async (completion) => {
console.log("%c Line:102 🍻 completion", "background:#fca650", completion);
await memory.add("assistant:execution", completion.text);
},
});
@ -125,6 +128,7 @@ export async function supervisionAI(ctx: AgentContext) {
...useTools(ctx.resTool),
},
onFinish: async (completion) => {
console.log("%c Line:129 🍣 completion", "background:#3f7cff", completion);
await memory.add("assistant:supervision", completion.text);
},
});

View File

@ -12,7 +12,7 @@ export const AssetSchema = z.object({
type: z.enum(["role", "tool", "scene", "clip"]).describe("资产类型"),
});
export const ScriptSchema = z.object({
id: z.number().describe("剧本ID,如果新增则为空").optional(),
id: z.number().describe("剧本ID"),
name: z.string().describe("剧本名称"),
content: z.string().describe("剧本内容"),
});
@ -35,13 +35,14 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
get_novel_events: tool({
description: "获取章节事件",
inputSchema: z.object({
ids: z.array(z.number()).describe("章节id"),
ids: z.array(z.number()).describe("章节id,注意区分"),
}),
execute: async ({ ids }) => {
resTool.systemMessage(`正在阅读 章节事件 数据...`);
console.log("[tools] get_novel_events", ids);
const data = await u
.db("o_novel")
.where("projectId", resTool.data.projectId)
.select("id", "chapterIndex as index", "reel", "chapter", "chapterData", "event", "eventState")
.whereIn("id", ids);
const eventString = data.map((i: any) => [`${i.index}章,标题:${i.chapter},事件:${i.event}`].join("\n")).join("\n");
@ -90,41 +91,48 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
return true;
},
}),
insert_script_to_sqlite: tool({
description: "将剧本内容插入sqlite数据库,供后续业务使用",
update_script_to_sqlite: tool({
description: "更新剧本,修改数据库对应剧本,供后续业务使用",
inputSchema: z.object({
script: ScriptSchema,
// assetsList: z.array(AssetSchema).describe("剧本所使用资产列表,注意不要包含剧本内容,仅为所使用到的 道具、人物、场景、素材"),
}),
execute: async ({ script }) => {
console.log("%c Line:103 🍷 script", "background:#42b983", script);
// console.log("[tools] insert_script_to_sqlite", assetsList);
await u.db("o_script").where({ id: script.id }).update({
name: script.name,
content: script.content,
});
socket.emit("setPlanData", { key: "script", value: script.id });
return true;
},
}),
insert_script_to_sqlite: tool({
description: "新增剧本,将剧本内容插入sqlite数据库供后续业务使用",
inputSchema: z.object({
script: ScriptSchema.omit({ id: true }),
}),
execute: async ({ script }) => {
console.log("%c Line:103 🍷 script", "background:#42b983", script);
const [scriptId] = await u.db("o_script").insert({
name: script.name,
content: script.content,
projectId: resTool.data.projectId,
createTime: Date.now(),
});
// if (assetsList && assetsList.length) {
// const assetId = [];
// for (const i of assetsList) {
// if (i.id) {
// assetId.push(i.id);
// continue;
// }
// const [id] = await u.db("o_assets").insert({
// name: i.name,
// prompt: i.prompt,
// type: i.type,
// describe: i.desc,
// projectId: resTool.data.projectId,
// startTime: Date.now(),
// });
// assetId.push(id);
// }
// await u.db("o_scriptAssets").insert(assetId.map((i) => ({ scriptId, assetId: i })));
// }
socket.emit("setPlanData", { key: "script", value: scriptId });
return true;
},
}),
delete_script_to_sqlite: tool({
description: "删除剧本,将剧本内容从sqlite数据库中删除",
inputSchema: z.object({
scriptId: z.string().describe("剧本id"),
}),
execute: async ({ scriptId }) => {
console.log("[tools] delete_script_to_sqlite", scriptId);
await u.db("o_script").where({ id: scriptId }).delete();
socket.emit("setPlanData", { key: "script", value: scriptId });
return true;
},

View File

@ -1,4 +1,4 @@
// @routes-hash 8dc4df496e2771e0081f32f4d8b2c2ae
// @routes-hash 557dfd43a824a4bd4170d0e2c9a6b45c
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -71,30 +71,30 @@ import route67 from "./routes/script/getScrptApi";
import route68 from "./routes/script/updateScript";
import route69 from "./routes/scriptAgent/getPlanData";
import route70 from "./routes/scriptAgent/setPlanData";
import route71 from "./routes/setting/about/checkUpdate";
import route72 from "./routes/setting/agentDeploy/agentSetKey";
import route73 from "./routes/setting/agentDeploy/deployAgentModel";
import route74 from "./routes/setting/agentDeploy/getAgentDeploy";
import route75 from "./routes/setting/dbConfig/clearData";
import route76 from "./routes/setting/fileManagement/openFolder";
import route77 from "./routes/setting/getTextModel";
import route78 from "./routes/setting/loginConfig/getUser";
import route79 from "./routes/setting/loginConfig/updateUserPwd";
import route80 from "./routes/setting/memoryConfig/delAllMemory";
import route81 from "./routes/setting/memoryConfig/getMemory";
import route82 from "./routes/setting/memoryConfig/sureMemory";
import route83 from "./routes/setting/skillManagement/addSkill";
import route84 from "./routes/setting/skillManagement/deleteSkill";
import route85 from "./routes/setting/skillManagement/embeddingSkill";
import route86 from "./routes/setting/skillManagement/generateDescription";
import route87 from "./routes/setting/skillManagement/getSkillList";
import route88 from "./routes/setting/skillManagement/scanSkills";
import route89 from "./routes/setting/skillManagement/updateSkill";
import route90 from "./routes/setting/vendorConfig/addVendor";
import route91 from "./routes/setting/vendorConfig/deleteVendor";
import route92 from "./routes/setting/vendorConfig/getVendorList";
import route93 from "./routes/setting/vendorConfig/modelTest";
import route94 from "./routes/setting/vendorConfig/updateVendor";
import route71 from "./routes/setting/agentDeploy/agentSetKey";
import route72 from "./routes/setting/agentDeploy/deployAgentModel";
import route73 from "./routes/setting/agentDeploy/getAgentDeploy";
import route74 from "./routes/setting/dbConfig/clearData";
import route75 from "./routes/setting/fileManagement/openFolder";
import route76 from "./routes/setting/getTextModel";
import route77 from "./routes/setting/loginConfig/getUser";
import route78 from "./routes/setting/loginConfig/updateUserPwd";
import route79 from "./routes/setting/memoryConfig/delAllMemory";
import route80 from "./routes/setting/memoryConfig/getMemory";
import route81 from "./routes/setting/memoryConfig/sureMemory";
import route82 from "./routes/setting/skillManagement/addSkill";
import route83 from "./routes/setting/skillManagement/deleteSkill";
import route84 from "./routes/setting/skillManagement/embeddingSkill";
import route85 from "./routes/setting/skillManagement/generateDescription";
import route86 from "./routes/setting/skillManagement/getSkillList";
import route87 from "./routes/setting/skillManagement/scanSkills";
import route88 from "./routes/setting/skillManagement/updateSkill";
import route89 from "./routes/setting/vendorConfig/addVendor";
import route90 from "./routes/setting/vendorConfig/deleteVendor";
import route91 from "./routes/setting/vendorConfig/getVendorList";
import route92 from "./routes/setting/vendorConfig/modelTest";
import route93 from "./routes/setting/vendorConfig/updateVendor";
import route94 from "./routes/task/getProject";
import route95 from "./routes/task/getTaskApi";
import route96 from "./routes/task/getTaskCategories";
import route97 from "./routes/task/taskDetails";
@ -171,30 +171,30 @@ export default async (app: Express) => {
app.use("/api/script/updateScript", route68);
app.use("/api/scriptAgent/getPlanData", route69);
app.use("/api/scriptAgent/setPlanData", route70);
app.use("/api/setting/about/checkUpdate", route71);
app.use("/api/setting/agentDeploy/agentSetKey", route72);
app.use("/api/setting/agentDeploy/deployAgentModel", route73);
app.use("/api/setting/agentDeploy/getAgentDeploy", route74);
app.use("/api/setting/dbConfig/clearData", route75);
app.use("/api/setting/fileManagement/openFolder", route76);
app.use("/api/setting/getTextModel", route77);
app.use("/api/setting/loginConfig/getUser", route78);
app.use("/api/setting/loginConfig/updateUserPwd", route79);
app.use("/api/setting/memoryConfig/delAllMemory", route80);
app.use("/api/setting/memoryConfig/getMemory", route81);
app.use("/api/setting/memoryConfig/sureMemory", route82);
app.use("/api/setting/skillManagement/addSkill", route83);
app.use("/api/setting/skillManagement/deleteSkill", route84);
app.use("/api/setting/skillManagement/embeddingSkill", route85);
app.use("/api/setting/skillManagement/generateDescription", route86);
app.use("/api/setting/skillManagement/getSkillList", route87);
app.use("/api/setting/skillManagement/scanSkills", route88);
app.use("/api/setting/skillManagement/updateSkill", route89);
app.use("/api/setting/vendorConfig/addVendor", route90);
app.use("/api/setting/vendorConfig/deleteVendor", route91);
app.use("/api/setting/vendorConfig/getVendorList", route92);
app.use("/api/setting/vendorConfig/modelTest", route93);
app.use("/api/setting/vendorConfig/updateVendor", route94);
app.use("/api/setting/agentDeploy/agentSetKey", route71);
app.use("/api/setting/agentDeploy/deployAgentModel", route72);
app.use("/api/setting/agentDeploy/getAgentDeploy", route73);
app.use("/api/setting/dbConfig/clearData", route74);
app.use("/api/setting/fileManagement/openFolder", route75);
app.use("/api/setting/getTextModel", route76);
app.use("/api/setting/loginConfig/getUser", route77);
app.use("/api/setting/loginConfig/updateUserPwd", route78);
app.use("/api/setting/memoryConfig/delAllMemory", route79);
app.use("/api/setting/memoryConfig/getMemory", route80);
app.use("/api/setting/memoryConfig/sureMemory", route81);
app.use("/api/setting/skillManagement/addSkill", route82);
app.use("/api/setting/skillManagement/deleteSkill", route83);
app.use("/api/setting/skillManagement/embeddingSkill", route84);
app.use("/api/setting/skillManagement/generateDescription", route85);
app.use("/api/setting/skillManagement/getSkillList", route86);
app.use("/api/setting/skillManagement/scanSkills", route87);
app.use("/api/setting/skillManagement/updateSkill", route88);
app.use("/api/setting/vendorConfig/addVendor", route89);
app.use("/api/setting/vendorConfig/deleteVendor", route90);
app.use("/api/setting/vendorConfig/getVendorList", route91);
app.use("/api/setting/vendorConfig/modelTest", route92);
app.use("/api/setting/vendorConfig/updateVendor", route93);
app.use("/api/task/getProject", route94);
app.use("/api/task/getTaskApi", route95);
app.use("/api/task/getTaskCategories", route96);
app.use("/api/task/taskDetails", route97);

View File

@ -14,7 +14,7 @@ export default router.post(
async (req, res) => {
const { assetsId } = req.body;
const assets = await u.db("o_assets").where("id", assetsId).select("id", "imageId", "type", "state").first();
const assets = await u.db("o_assets").where("id", assetsId).select("id", "imageId", "type").first();
const rawTempAssets = await u.db("o_image").where("assetsId", assetsId).select("id", "filePath", "assetsId", "type", "state");
@ -28,10 +28,10 @@ export default router.post(
const data = {
id: assets!.id,
state: assets!.state,
imageId: assets!.imageId ?? null,
tempAssets,
};
console.log("%c Line:30 🥤 data", "background:#465975", data);
res.status(200).send(success(data));
},
);

View File

@ -122,6 +122,7 @@ export default router.post(
messages: [{ role: "user", content: "小说原文" + novelText }],
tools: skill.tools,
})) as any;
if (!_output) return res.status(500).send("失败");
await u.db("o_assets").where("id", assetsId).update({ prompt: _output });

View File

@ -14,8 +14,13 @@ export default router.post(
data: flowDataSchema,
}),
async (req, res) => {
const { projectId, episodesId } = req.body;
const { data, projectId, episodesId } = req.body;
const sqlData = await u.db("o_agentWorkData").where("projectId", String(projectId)).andWhere("episodesId", String(episodesId)).first();
for (let item of data.storyboard) {
await u.db("o_storyboard").where("id", item.id).update({
index: item.id,
});
}
if (!sqlData) {
await u.db("o_agentWorkData").insert({
projectId,

View File

@ -7,13 +7,13 @@ const router = express.Router();
// 删除剧本
export default router.post(
"/",
validateFields({
id: z.number(),
}),
async (req, res) => {
const { id } = req.body;
await u.db("o_script").where({ id }).delete();
res.status(200).send(success({ message: "删除剧本成功" }));
},
"/",
validateFields({
id: z.array(z.number()),
}),
async (req, res) => {
const { id } = req.body;
await u.db("o_script").whereIn("id", id).delete();
res.status(200).send(success({ message: "删除剧本成功" }));
},
);

View File

@ -5,30 +5,88 @@ import { error, success } from "@/lib/responseFormat";
import compressing from "compressing";
import { validateFields } from "@/middleware/middleware";
import { useSkill } from "@/utils/agent/skillsTools";
import { Output, tool } from "ai";
const router = express.Router();
export const AssetSchema = z.object({
prompt: z.string().describe("生成提示词"),
name: z.string().describe("资产名称,仅为名称不做其他任何表述"),
desc: z.string().describe("资产描述"),
type: z.enum(["role", "tool", "scene"]).describe("资产类型"),
});
export default router.post(
"/",
validateFields({
scriptIds: z.array(z.number()),
projectId: z.number(),
}),
async (req, res) => {
const { scriptIds } = req.body;
const { scriptIds, projectId } = req.body;
if (!scriptIds.length) return res.status(400).send(error("请先选择剧本"));
const scripts = await u.db("o_script").whereIn("id", scriptIds);
const intansce = u.Ai.Text("universalAgent");
const skill = await useSkill("universal_agent.md");
const novelData = await u.db("o_novel").where("projectId", projectId).select("chapterData");
if (!novelData || novelData.length === 0) return res.status(400).send(error("请先上传小说"));
const resData = await intansce.invoke({
system: skill.prompt,
messages: [
{
role: "user",
content: "请根据以下小说章节生成事件摘要:\n",
async function getAssets() {
return await u.db("o_assets").where("projectId", projectId).select("id", "name");
}
for (const scriptId of scriptIds) {
const resultTool = tool({
description: "返回结果时必须调用这个工具,",
inputSchema: z.object({
assetsList: z.array(AssetSchema).describe("剧本所使用资产列表,注意不要包含剧本内容,仅为所使用到的 道具、人物、场景、素材"),
}),
execute: async ({ assetsList }) => {
console.log("[tools] set_flowData script", assetsList);
if (assetsList && assetsList.length) {
const assetId = [];
const existingAssets = await getAssets();
for (const i of assetsList) {
if (existingAssets.length) {
const exist = existingAssets.find((j) => j.name === i.name);
if (exist) {
assetId.push(exist.id);
continue;
}
}
const [id] = await u.db("o_assets").insert({
name: i.name,
prompt: i.prompt,
type: i.type,
describe: i.desc,
projectId: projectId,
startTime: Date.now(),
});
assetId.push(id);
}
await u.db("o_scriptAssets").insert(assetId.map((i) => ({ scriptId: scriptId, assetId: i })));
}
return true;
},
],
tools: skill.tools,
});
});
try {
const skill = await useSkill("universal_agent.md");
const resData = await intansce.invoke({
messages: [
{
role: "system",
content:
skill.prompt +
"\n\n提取剧本中涉及的资产角色、场景、道具参考技能 script_assets_extract 规范,结果必须通过 resultTool 工具返回。",
},
{
role: "user",
content: `请根据以下剧本提取对应的剧本资产(角色、场景、道具、素材片段):\n\n${scripts.map((i) => i.content).join("\n\n---\n\n")}`,
},
],
tools: { ...skill.tools, resultTool },
});
console.log("%c Line:47 🥝 resData", "background:#2eafb0", resData);
} catch (e) {
console.log("%c Line:52 🍢 e", "background:#42b983", e);
}
}
},
);

View File

@ -13,11 +13,11 @@ export default router.post(
}),
async (req, res) => {
const { projectId, agentType } = req.body;
const data = await u.db("o_agentWorkData").where({ id: projectId, key: agentType }).first();
const data = await u.db("o_agentWorkData").where({ projectId: projectId, key: agentType }).first();
if (!data) {
await u.db("o_agentWorkData").insert({
id: projectId,
projectId: projectId,
key: agentType,
data: JSON.stringify({
storySkeleton: "",

View File

@ -19,7 +19,7 @@ export default router.post(
const { projectId, agentType, data } = req.body;
await u
.db("o_agentWorkData")
.where({ id: projectId, key: agentType })
.where({ projectId: projectId, key: agentType })
.update({
data: JSON.stringify(data),
});

View File

@ -0,0 +1,10 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
const router = express.Router();
export default router.post("/", async (req, res) => {
const list = await u.db("o_project").select("id", "name").groupBy("name");
const data = list.filter((item) => item.name);
res.status(200).send(success(data));
});

View File

@ -9,11 +9,12 @@ export default router.post(
validateFields({
state: z.string().optional().nullable(),
taskClass: z.string().optional().nullable(),
projectId: z.number().optional().nullable(),
page: z.number(),
limit: z.number(),
}),
async (req, res) => {
const { taskClass, state, page = 1, limit = 10 }: any = req.body;
const { taskClass, state, projectId, page = 1, limit = 10 }: any = req.body;
const offset = (page - 1) * limit;
const data = await u
.db("o_tasks")
@ -25,6 +26,9 @@ export default router.post(
if (state) {
qb.andWhere("o_tasks.state", state);
}
if (projectId) {
qb.andWhere("o_tasks.projectId", projectId);
}
})
.select("o_tasks.*", "o_project.* ")
.offset(offset)
@ -36,6 +40,9 @@ export default router.post(
if (taskClass) {
qb.andWhere("o_tasks.taskClass", taskClass);
}
if (projectId) {
qb.andWhere("o_tasks.projectId", projectId);
}
if (state) {
qb.andWhere("o_tasks.state", state);
}

View File

@ -1,17 +1,10 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { number, z } from "zod";
const router = express.Router();
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const data = await u.db("o_tasks").where("projectId", req.body.projectId).select("taskClass").groupBy("taskClass");
res.status(200).send(success(data));
},
);
export default router.post("/", async (req, res) => {
const list = await u.db("o_tasks").select("taskClass").groupBy("taskClass");
const data = list.filter((item) => item.taskClass);
res.status(200).send(success(data));
});

View File

@ -1,6 +1,36 @@
// @db-hash d807205fbb27fc5ddb04cae060fb4430
// @db-hash 579a004cc745580469a24ee71f5f51c3
//该文件由脚本自动生成,请勿手动修改
export interface _o_project_old_20260326 {
'artStyle'?: string | null;
'createTime'?: number | null;
'id'?: number | null;
'intro'?: string | null;
'name'?: string | null;
'projectType'?: string | null;
'type'?: string | null;
'userId'?: number | null;
'videoRatio'?: string | null;
}
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;
@ -220,6 +250,8 @@ export interface o_videoConfig {
}
export interface DB {
"_o_project_old_20260326": _o_project_old_20260326;
"_o_storyboard_old_20260325": _o_storyboard_old_20260325;
"memories": memories;
"o_agentDeploy": o_agentDeploy;
"o_agentWorkData": o_agentWorkData;

View File

@ -137,8 +137,6 @@ class AiVideo {
async run(input: VideoConfig) {
return withTaskRecord(this.key, input.taskClass, input.describe, input.relatedObjects, input.projectId, async (modelName) => {
const fn = await getVendorTemplateFn("videoRequest", modelName);
console.log("%c Line:142 🎂 input", "background:#42b983", input);
this.result = await fn(input);
if (this.result.startsWith("http")) this.result = await urlToBase64(this.result);
return this;

View File

@ -16,46 +16,68 @@ export interface EventType {
class CleanNovel {
emitter: EventEmitter;
constructor() {
/** 最大并发数 */
concurrency: number;
constructor(concurrency: number = 5) {
this.emitter = new EventEmitter();
this.concurrency = concurrency;
}
private async processChapter(novel: o_novel, intansce: ReturnType<typeof u.Ai.Text>): Promise<EventType | null> {
try {
const skill = await useSkill("universal_agent.md");
const resData = await intansce.invoke({
system: skill.prompt,
messages: [
{
role: "user",
content: "请根据以下小说章节生成事件摘要:\n" + novel.chapterData!,
},
],
tools: skill.tools,
});
const preData = resData.text;
this.emitter.emit("item", { id: novel.id, event: preData });
return { id: novel.id!, event: preData };
} catch (e) {
this.emitter.emit("item", { id: novel.id, event: null, errorReason: u.error(e).message });
return null;
}
}
async start(allChapters: o_novel[], projectId: number): Promise<EventType[]> {
//所有事件
let totalEvent: EventType[] = [];
const totalEvent: EventType[] = [];
const intansce = u.Ai.Text("universalAgent");
try {
for (let gi = 0; gi < allChapters.length; gi++) {
const novel = allChapters[gi];
let resData;
try {
const skill = await useSkill("universal_agent.md");
// 并发控制:通过信号量限制同时执行的任务数
let running = 0;
let index = 0;
const results: Promise<void>[] = [];
resData = await intansce.invoke({
system: skill.prompt,
messages: [
{
role: "user",
content: "请根据以下小说章节生成事件摘要:\n" + novel.chapterData!,
},
],
tools: skill.tools,
});
console.log("%c Line:35 🍆 resData", "background:#fca650", resData);
const runNext = (): Promise<void> => {
if (index >= allChapters.length) return Promise.resolve();
const novel = allChapters[index++];
running++;
const preData = resData.text;
return this.processChapter(novel, intansce).then((result) => {
if (result) totalEvent.push(result);
running--;
return runNext();
});
};
// 启动最多 concurrency 个并发任务
const workers = Array.from(
{ length: Math.min(this.concurrency, allChapters.length) },
() => runNext()
);
await Promise.all(workers);
this.emitter.emit("item", { id: novel.id, event: preData });
totalEvent.push({ id: novel.id!, event: preData });
} catch (e) {
console.log("%c Line:51 🍩 e", "background:#93c0a4", e);
this.emitter.emit("item", { id: novel.id, event: null, errorReason: u.error(e).message });
}
}
} catch (e) {
console.error(e);
throw e;
}
return totalEvent;
}
}

106
src/utils/stripThink.ts Normal file
View File

@ -0,0 +1,106 @@
/**
* <think>...</think>
*
* 1. stripThink(text) <think>
* 2. createThinkStreamFilter() chunk
*/
/**
* <think>...</think>
*/
export function stripThink(text: string): string {
return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
}
/**
* chunk
*
*
* ```ts
* const filter = createThinkStreamFilter();
* for await (const chunk of textStream) {
* const filtered = filter.push(chunk);
* if (filtered) msg.send(filtered);
* }
* ```
*/
export function createThinkStreamFilter() {
let insideThink = false;
let buffer = "";
return {
/**
* chunk
*/
push(chunk: string): string {
let output = "";
let i = 0;
while (i < chunk.length) {
if (insideThink) {
// 正在 <think> 内部,寻找 </think>
const closeIdx = chunk.indexOf("</think>", i);
if (closeIdx !== -1) {
// 找到闭合标签,跳过标签内容
insideThink = false;
i = closeIdx + "</think>".length;
} else {
// 整个剩余 chunk 都在 think 内,全部丢弃
break;
}
} else {
// 不在 <think> 内部
const openIdx = chunk.indexOf("<think>", i);
if (openIdx !== -1) {
// 找到开启标签,输出标签之前的内容
output += buffer + chunk.slice(i, openIdx);
buffer = "";
insideThink = true;
i = openIdx + "<think>".length;
} else {
// 没有发现 <think>,但可能 chunk 末尾是不完整的 "<thi..."
// 缓冲末尾可能是 "<" 开头的不完整标签片段
const potentialStart = findPartialTag(chunk, i);
if (potentialStart !== -1) {
output += buffer + chunk.slice(i, potentialStart);
buffer = chunk.slice(potentialStart);
} else {
output += buffer + chunk.slice(i);
buffer = "";
}
break;
}
}
}
return output;
},
/**
*
*/
flush(): string {
const remaining = buffer;
buffer = "";
return remaining;
},
};
}
/**
* chunk[startIdx..] "<think>"
* "<", "<t", "<th", "<thi", "<thin", "<think"
* -1
*/
function findPartialTag(chunk: string, startIdx: number): number {
const tag = "<think>";
// 只需检查末尾最多 tag.length - 1 个字符
const searchStart = Math.max(startIdx, chunk.length - (tag.length - 1));
for (let i = searchStart; i < chunk.length; i++) {
const remaining = chunk.slice(i);
if (tag.startsWith(remaining)) {
return i;
}
}
return -1;
}

View File

@ -44,7 +44,6 @@ export default function runCode(code: string) {
return exports as Record<string, any>;
}
/**
* size
*/