# Conflicts:
#	src/agents/productionAgent/tools.ts
#	src/router.ts
#	src/types/database.d.ts
This commit is contained in:
ACT丶流星雨 2026-03-26 00:45:00 +08:00
commit 35993e7bfe
15 changed files with 774 additions and 268 deletions

View File

@ -120,7 +120,7 @@ function runSubAgent(parentCtx: AgentContext) {
description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI", description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI",
inputSchema: z.object({ inputSchema: z.object({
agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"), agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"),
prompt: z.string().describe("交给子Agent的任务描述"), prompt: z.string().max(100).describe("交给子Agent的任务简约描述"),
}), }),
execute: async ({ agent, prompt }) => { execute: async ({ agent, prompt }) => {
const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)]; const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];

View File

@ -6,7 +6,7 @@ import u from "@/utils";
import { useSkill } from "@/utils/agent/skillsTools"; import { useSkill } from "@/utils/agent/skillsTools";
import { urlToBase64 } from "@/utils/vm"; import { urlToBase64 } from "@/utils/vm";
export const deriveAssetSchema = z.object({ export const deriveAssetSchema = z.object({
id: z.number().describe("衍生资产ID,如果新增则为空").optional(), id: z.number().describe("衍生资产ID,如果新增则为空"),
assetsId: z.number().describe("关联的资产ID"), assetsId: z.number().describe("关联的资产ID"),
prompt: z.string().describe("生成提示词"), prompt: z.string().describe("生成提示词"),
name: z.string().describe("衍生资产名称"), name: z.string().describe("衍生资产名称"),
@ -24,7 +24,7 @@ export const assetItemSchema = z.object({
derive: z.array(deriveAssetSchema).describe("衍生资产列表"), derive: z.array(deriveAssetSchema).describe("衍生资产列表"),
}); });
export const storyboardSchema = z.object({ export const storyboardSchema = z.object({
id: z.number().optional().describe("分镜ID,未从工作区获得的分镜面板视为需要新增;如需新增则为空"), id: z.number().describe("分镜ID必须为真实id"),
title: z.string().describe("分镜标题"), title: z.string().describe("分镜标题"),
description: z.string().describe("分镜描述"), description: z.string().describe("分镜描述"),
camera: z.string().describe("镜头信息"), camera: z.string().describe("镜头信息"),
@ -104,83 +104,156 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
return true; return true;
}, },
}), }),
// add_flowData_assets: tool({ add_flowData_assets: tool({
// description: "新增对应衍生资产列表到工作区,严禁包含 不需要新增的数据", description: "新增对应衍生资产列表到工作区,严禁包含 不需要新增的数据",
// inputSchema: z.object({ value: z.array(deriveAssetSchema).describe("需要新增的资产列表") }), inputSchema: z.object({ value: z.array(deriveAssetSchema.omit({ id: true })).describe("需要新增的衍生资产列表") }),
execute: async ({ value }) => {
console.log("[tools] set_flowData add_flowData_assets", value);
resTool.systemMessage("正在保存 衍生资产 数据");
const setData = [...value] as z.infer<typeof deriveAssetSchema>[];
const { projectId, scriptId } = resTool.data;
const startTime = Date.now();
// 并行插入所有 o_assets 记录
await Promise.all(
setData.map(async (i) => {
const [insertedId] = await u.db("o_assets").insert({
assetsId: +i.assetsId || null,
projectId,
name: i.name,
type: i.type,
prompt: i.prompt,
describe: i.desc,
startTime,
});
i.id = insertedId;
}),
);
// 批量插入 o_scriptAssets
await u.db("o_scriptAssets").insert(setData.map((i) => ({ scriptId, assetId: i.id })));
const watiAddAssetsMap: Record<number, z.infer<typeof deriveAssetSchema>[]> = {};
setData.forEach((i) => {
if (watiAddAssetsMap[i.assetsId]) {
watiAddAssetsMap[i.assetsId].push(i);
} else {
watiAddAssetsMap[i.assetsId] = [i];
}
});
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
const assetsData = flowData.assets;
assetsData.forEach((i) => {
if (watiAddAssetsMap[i.id]) {
i.derive = [...(i.derive || []), ...watiAddAssetsMap[i.id]];
}
});
socket.emit("setFlowData", { key: "assets", value: assetsData });
return true;
},
}),
update_flowData_assets: tool({
description: "更新对应衍生资产列表到工作区",
inputSchema: z.object({ value: z.array(deriveAssetSchema).describe("需要更新的衍生资产列表") }),
execute: async ({ value }) => {
console.log("[tools] update_flowData update_flowData_assets", value);
resTool.systemMessage("正在保存 衍生资产 数据");
for (const i of value) {
await u
.db("o_assets")
.where("id", i.id)
.update({
assetsId: +i.assetsId || null,
projectId: resTool.data.projectId,
name: i.name,
type: i.type,
prompt: i.prompt,
describe: i.desc,
});
}
// 按 assetsId 分组,构建更新映射
const updateAssetsMap: Record<number, z.infer<typeof deriveAssetSchema>[]> = {};
value.forEach((i) => {
if (updateAssetsMap[i.assetsId]) {
updateAssetsMap[i.assetsId].push(i);
} else {
updateAssetsMap[i.assetsId] = [i];
}
});
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
const assetsData = flowData.assets;
// 将 derive 中已存在的条目替换为更新后的数据
assetsData.forEach((asset) => {
if (updateAssetsMap[asset.id]) {
const updatedMap = Object.fromEntries(updateAssetsMap[asset.id].map((d) => [d.id, d]));
asset.derive = (asset.derive || []).map((d) => updatedMap[d.id] ?? d);
}
});
socket.emit("setFlowData", { key: "assets", value: assetsData });
return true;
},
}),
delete_flowData_assets: tool({
description: "删除对应衍生资产",
inputSchema: z.object({ ids: z.array(z.number()).describe("需要删除的 衍生资产id ") }),
execute: async ({ ids }) => {
console.log("[tools] delete_flowData delete_flowData_assets", ids);
resTool.systemMessage("正在保存 衍生资产 数据");
await u.db("o_assets").whereIn("id", ids).delete();
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
const assetsData = flowData.assets;
assetsData.forEach((i) => {
i.derive = (i.derive || []).filter((d) => !ids.includes(d.id));
});
// 将 derive 中已存在的条目替换为更新后的数据
socket.emit("setFlowData", { key: "assets", value: assetsData });
return true;
},
}),
// set_flowData_assets: tool({
// description: "保存衍生资产列表到工作区",
// inputSchema: z.object({ value: flowDataSchema.shape.assets }),
// execute: async ({ value }) => { // execute: async ({ value }) => {
// console.log("[tools] set_flowData add_flowData_assets", value); // console.log("[tools] set_flowData assets", value);
// resTool.systemMessage("正在保存 衍生资产 数据"); // resTool.systemMessage("正在保存 衍生资产 数据");
// const addAssetsData = [];
// if (value && Array.isArray(value) && value.length) { // if (value && Array.isArray(value) && value.length) {
// for (const i of value) { // for (const i of value) {
// if (!i?.id) {
// const [insertedId] = await u.db("o_assets").insert({ // const [insertedId] = await u.db("o_assets").insert({
// assetsId: +i.assetsId || null, // assetsId: null,
// projectId: resTool.data.projectId,
// name: i.name, // name: i.name,
// type: i.type, // type: i.type,
// prompt: i.prompt, // prompt: i.prompt,
// describe: i.desc, // describe: i.desc,
// startTime: Date.now(), // startTime: Date.now(),
// }); // });
// console.log("%c Line:141 🍑 resTool.data.scriptId", "background:#ea7e5c", resTool.data.scriptId); // i.id = insertedId;
// }
// if (i.derive && Array.isArray(i.derive) && i.derive.length) {
// for (const sub of i.derive) {
// if (sub.id) continue;
// const [insertedId] = await u.db("o_assets").insert({
// assetsId: +i.id || null,
// projectId: resTool.data.projectId,
// name: sub.name,
// type: sub.type,
// prompt: sub.prompt,
// describe: sub.desc,
// startTime: Date.now(),
// });
// await u.db("o_scriptAssets").insert({ // await u.db("o_scriptAssets").insert({
// scriptId: resTool.data.scriptId, // scriptId: resTool.data.scriptId,
// assetId: insertedId, // assetId: insertedId,
// }); // });
// addAssetsData.push({ // sub.id = insertedId;
// ...i,
// id: insertedId,
// });
// } // }
// } // }
// socket.emit("setFlowData", { key: "addAssets", value: addAssetsData }); // }
// }
// socket.emit("setFlowData", { key: "assets", value });
// return true; // return true;
// }, // },
// }), // }),
set_flowData_assets: tool({
description: "保存衍生资产列表到工作区",
inputSchema: z.object({ value: flowDataSchema.shape.assets }),
execute: async ({ value }) => {
console.log("[tools] set_flowData assets", value);
resTool.systemMessage("正在保存 衍生资产 数据");
if (value && Array.isArray(value) && value.length) {
for (const i of value) {
if (!i?.id) {
const [insertedId] = await u.db("o_assets").insert({
assetsId: null,
name: i.name,
type: i.type,
prompt: i.prompt,
describe: i.desc,
startTime: Date.now(),
});
i.id = insertedId;
}
if (i.derive && Array.isArray(i.derive) && i.derive.length) {
for (const sub of i.derive) {
if (sub.id) continue;
const [insertedId] = await u.db("o_assets").insert({
assetsId: +i.id || null,
projectId: resTool.data.projectId,
name: sub.name,
type: sub.type,
prompt: sub.prompt,
describe: sub.desc,
startTime: Date.now(),
});
await u.db("o_scriptAssets").insert({
scriptId: resTool.data.scriptId,
assetId: insertedId,
});
sub.id = insertedId;
}
}
}
}
socket.emit("setFlowData", { key: "assets", value });
return true;
},
}),
set_flowData_storyboardTable: tool({ set_flowData_storyboardTable: tool({
description: "保存分镜表到工作区", description: "保存分镜表到工作区",
inputSchema: z.object({ value: flowDataSchema.shape.storyboardTable }), inputSchema: z.object({ value: flowDataSchema.shape.storyboardTable }),
@ -191,19 +264,19 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
return true; return true;
}, },
}), }),
set_flowData_storyboard: tool({ add_flowData_storyboard: tool({
description: "保存分镜面板到工作区", description: "新增分镜面板到工作区",
inputSchema: z.object({ value: flowDataSchema.shape.storyboard }), inputSchema: z.object({ value: z.array(storyboardSchema.omit({ id: true })) }),
execute: async ({ value }) => { execute: async ({ value }) => {
console.log("[tools] set_flowData storyboard", value); console.log("[tools] add_flowData storyboard", value);
resTool.systemMessage("正在保存 分镜面板 数据..."); resTool.systemMessage("正在新增 分镜面板 数据...");
for (const item of value) { const setData = [...value] as z.infer<typeof storyboardSchema>[];
if (!item.id) { for (const item of setData) {
item.src = "";
const [insertedId] = await u.db("o_storyboard").insert({ const [insertedId] = await u.db("o_storyboard").insert({
title: item.title, title: item.title,
prompt: item.prompt, prompt: item.prompt,
description: item.description, description: item.description,
filePath: item.src,
frameMode: item.frameMode, frameMode: item.frameMode,
duration: String(item.duration), duration: String(item.duration),
camera: item.camera, camera: item.camera,
@ -217,11 +290,103 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
} }
item.id = insertedId; item.id = insertedId;
} }
}
socket.emit("setFlowData", { key: "storyboard", value }); const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
const storyboardData = flowData["storyboard"].concat([...setData]);
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
return true; return true;
}, },
}), }),
update_flowData_storyboard: tool({
description: "更新指定分镜面板到工作区",
inputSchema: z.object({ value: flowDataSchema.shape.storyboard }),
execute: async ({ value }) => {
console.log("[tools] update_flowData storyboard", value);
resTool.systemMessage("正在更新 分镜面板 数据...");
for (const item of value) {
await u
.db("o_storyboard")
.where("id", item.id)
.update({
title: item.title,
prompt: item.prompt,
description: item.description,
frameMode: item.frameMode,
duration: String(item.duration),
camera: item.camera,
sound: item.sound,
lines: item.lines,
});
}
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
const storyboardData = flowData["storyboard"].map((existing) => {
const updated = value.find((v) => v.id === existing.id);
if (!updated) return existing;
return {
...existing,
title: updated.title,
prompt: updated.prompt,
description: updated.description,
frameMode: updated.frameMode,
duration: updated.duration,
camera: updated.camera,
sound: updated.sound,
lines: updated.lines,
};
});
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
return true;
},
}),
delete_flowData_storyboard: tool({
description: "删除指定分镜面板并更新工作区",
inputSchema: z.object({ ids: z.array(z.number()).describe("需要删除的 分镜id ") }),
execute: async ({ ids }) => {
console.log("[tools] delete_flowData storyboard", ids);
resTool.systemMessage("正在删除指定 分镜面板 数据...");
await u.db("o_storyboard").whereIn("id", ids).delete();
await u.db("o_assets2Storyboard").whereIn("storyboardId", ids).delete();
await u.db("o_storyboardFlow").whereIn("storyboardId", ids).delete();
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
const storyboardData = flowData["storyboard"].filter((item) => !ids.includes(item.id));
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
return true;
},
}),
// set_flowData_storyboard: tool({
// description: "保存分镜面板到工作区",
// inputSchema: z.object({ value: flowDataSchema.shape.storyboard }),
// execute: async ({ value }) => {
// console.log("[tools] set_flowData storyboard", value);
// resTool.systemMessage("正在保存 分镜面板 数据...");
// for (const item of value) {
// if (!item.id) {
// const [insertedId] = await u.db("o_storyboard").insert({
// title: item.title,
// prompt: item.prompt,
// description: item.description,
// filePath: item.src,
// frameMode: item.frameMode,
// duration: String(item.duration),
// camera: item.camera,
// sound: item.sound,
// lines: item.lines,
// state: "未生成",
// scriptId: resTool.data.scriptId,
// });
// console.log("%c Line:216 🥥 item.associateAssetsIds", "background:#6ec1c2", item.associateAssetsIds);
// if (item.associateAssetsIds.length) {
// await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i })));
// }
// item.id = insertedId;
// }
// }
// socket.emit("setFlowData", { key: "storyboard", value });
// return true;
// },
// }),
set_flowData_workbench: tool({ set_flowData_workbench: tool({
description: "保存工作台配置数据到工作区", description: "保存工作台配置数据到工作区",
inputSchema: z.object({ value: flowDataSchema.shape.workbench }), inputSchema: z.object({ value: flowDataSchema.shape.workbench }),
@ -242,14 +407,13 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
return true; return true;
}, },
}), }),
// todo 提示词待调
//todo referenceIds 图片未使用 提示词待调
generate_storyboard_images: tool({ generate_storyboard_images: tool({
description: `生成一组图片任务,支持图片间的依赖关系(以图生图) description: `生成一组图片任务,支持图片间的依赖关系(以图生图),基于有向无环图(DAG)拓扑排序执行
- images: 图片任务数组 - images: 图片任务数组
- id: 图片唯一标识符 - id: 图片唯一标识符id
- prompt: 图片生成提示词 - prompt: 图片生成提示词
- referenceIds: 依赖的参考图id数组[] - referenceIds: 依赖的参考图id数组[]
- assetIds: 参考的资产图id数组 - assetIds: 参考的资产图id数组
@ -261,66 +425,155 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
images: [ images: [
{id: "cat", prompt: "一只橘猫", referenceIds: [], assetIds: []}, {id: 1, prompt: "一只橘猫", referenceIds: [], assetIds: []},
{id: "dog", prompt: "风格相同的金毛犬", referenceIds: ["cat"], assetIds: []} {id: 2, prompt: "风格相同的金毛犬", referenceIds: [1], assetIds: []}
]`, ]`,
inputSchema: z.object({ inputSchema: z.object({
images: z.array( images: z.array(
z.object({ z.object({
id: z.number().describe("从工作区获取到的分镜id"), id: z.number().describe("从工作区获取到的分镜id"),
prompt: z.string().describe("图片生成提示词"), prompt: z.string().describe("图片生成提示词"),
referenceIds: z.array(z.string()).describe("依赖的参考图id数组无依赖填空数组[]"), referenceIds: z.array(z.number()).describe("依赖的参考 分镜图id数组无依赖填空数组[]"),
assetIds: z.array(z.number()).optional().describe("参考的资产图"), assetIds: z.array(z.number()).describe("参考的资产图"),
}), }),
), ),
}), }),
execute: async ({ images }) => { execute: async ({ images }) => {
console.log("[tools] generated_assets", images); console.log("[tools] generate_storyboard_images", images);
// --- 构建任务id集合 ---
const taskIds = new Set(images.map((item) => item.id));
const imageMap = new Map(images.map((item) => [item.id, item]));
// --- 检测循环依赖 (Kahn算法拓扑排序) ---
// 将 referenceIds 分为:本批次内依赖 vs 外部已有依赖
// 只有本批次内的依赖才参与 DAG 调度,外部依赖直接从数据库获取
const inDegree = new Map<number, number>();
// adjacency: 被依赖者 -> 依赖它的节点列表
const adjacency = new Map<number, number[]>();
const skill = await useSkill("universal_agent.md");
for (const item of images) { for (const item of images) {
resTool.systemMessage(`生在生成分镜 id:${item.id} 图片`); // 只统计本批次内的依赖作为入度
//更新对应分镜状态 const internalDeps = item.referenceIds.filter((refId) => taskIds.has(refId));
await u.db("o_storyboard").where("id", item.id).update({ state: "生成中" }); inDegree.set(item.id, internalDeps.length);
// 异步生成 for (const depId of internalDeps) {
if (!adjacency.has(depId)) adjacency.set(depId, []);
adjacency.get(depId)!.push(item.id);
}
}
// 拓扑排序,按层级分组(同层可并行)
const levels: number[][] = [];
let queue = images.filter((item) => (inDegree.get(item.id) ?? 0) === 0).map((item) => item.id);
const visited = new Set<number>();
while (queue.length > 0) {
levels.push([...queue]);
const nextQueue: number[] = [];
for (const nodeId of queue) {
visited.add(nodeId);
for (const childId of adjacency.get(nodeId) ?? []) {
inDegree.set(childId, (inDegree.get(childId) ?? 1) - 1);
if (inDegree.get(childId) === 0) {
nextQueue.push(childId);
}
}
}
queue = nextQueue;
}
// 循环依赖检测
if (visited.size !== images.length) {
const cyclicIds = images.filter((item) => !visited.has(item.id)).map((item) => item.id);
resTool.systemMessage(`检测到循环依赖涉及分镜id: ${cyclicIds.join(", ")},请修正后重试`);
return `错误检测到循环依赖涉及分镜id: ${cyclicIds.join(", ")}`;
}
console.log("%c Line:496 🌶", "background:#ea7e5c");
resTool.systemMessage(`图片生成调度计划:共 ${levels.length} 层,${images.length} 张图片`);
// --- 准备公共数据 ---
const skill = await useSkill("universal-agent");
const projectData = await u.db("o_project").where("id", resTool.data.projectId).select("videoRatio").first();
const imageModel = resTool.data.imageModel; const imageModel = resTool.data.imageModel;
u.Ai.Image(imageModel?.modelId) // 生成单张图片的函数
.run({ const generateOneImage = async (item: (typeof images)[0]) => {
resTool.systemMessage(`正在生成分镜 id:${item.id} 图片`);
// 更新数据库状态为生成中
await u.db("o_storyboard").where("id", item.id).update({ state: "生成中" });
// 更新前端为生成中
socket.emit("setFlowData", {
key: "setStoryboardImage",
value: { ...item, id: item.id, src: "", state: "生成中", referenceIds: item.referenceIds },
});
// 获取参考图base64包括资产图和已生成的分镜参考图
const [assetsBase64, referenceBase64] = await Promise.all([
getAssetsImageBase64(item.assetIds ?? []),
getStoryboardImageBase64(item.referenceIds),
]);
const imageCls = await u.Ai.Image(imageModel?.modelId).run({
systemPrompt: skill.prompt, systemPrompt: skill.prompt,
prompt: item.prompt, prompt: item.prompt,
imageBase64: await getAssetsImageBase64(item.assetIds ?? []), imageBase64: [...assetsBase64, ...referenceBase64],
size: imageModel?.quality, size: imageModel?.quality,
aspectRatio: imageModel?.ratio, aspectRatio: (projectData?.videoRatio as `${number}:${number}`) ?? "16:9",
taskClass: "生成图片", taskClass: "生成图片",
describe: "分镜图片生成", describe: "分镜图片生成",
relatedObjects: "hhhh", relatedObjects: "hhhh",
projectId: resTool.data.projectId, projectId: resTool.data.projectId,
}) });
.then(async (imageCls) => {
const savePath = `/${resTool.data.projectId}/storyboard/${u.uuid()}.jpg`; const savePath = `/${resTool.data.projectId}/storyboard/${u.uuid()}.jpg`;
await imageCls.save(savePath); await imageCls.save(savePath);
// 更新数据库状态为已完成
await u.db("o_storyboard").where("id", item.id).update({ state: "已完成", filePath: savePath });
const obj = { const obj = {
...item, ...item,
id: item.id, id: item.id,
src: await u.oss.getFileUrl(savePath), src: await u.oss.getFileUrl(savePath),
state: "已完成", state: "已完成",
referenceIds: item.referenceIds,
}; };
// 更新对应分镜状态
await u.db("o_storyboard").where("id", item.id).update({ state: "已完成", filePath: savePath });
// 前端对话框提示 // 前端对话框提示
resTool.systemMessage(`分镜 id:${item.id} 图片生成完成`); resTool.systemMessage(`分镜 id:${item.id} 图片生成完成`);
// 更新前端界面展示 // 更新前端界面展示
socket.emit("setFlowData", { key: "setStoryboardImage", value: obj }); socket.emit("setFlowData", { key: "setStoryboardImage", value: obj });
};
// --- 按层级顺序执行:同层并行,层间串行 ---
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
const levelIds = levels[levelIndex];
const levelItems = levelIds.map((id) => imageMap.get(id)!);
resTool.systemMessage(`开始生成第 ${levelIndex + 1}/${levels.length} 层,共 ${levelItems.length} 张图片 (ids: ${levelIds.join(", ")})`);
// 同层内所有图片并行生成,使用 allSettled 确保不会因单张失败中断整层
const results = await Promise.allSettled(levelItems.map((item) => generateOneImage(item)));
// 处理失败的任务
for (let i = 0; i < results.length; i++) {
if (results[i].status === "rejected") {
const failedId = levelIds[i];
const reason = (results[i] as PromiseRejectedResult).reason;
console.error(`[tools] 分镜 id:${failedId} 图片生成失败`, reason);
resTool.systemMessage(`分镜 id:${failedId} 图片生成失败: ${reason?.message || reason}`);
await u.db("o_storyboard").where("id", failedId).update({ state: "生成失败" });
socket.emit("setFlowData", {
key: "setStoryboardImage",
value: { id: failedId, src: "", state: "生成失败" },
}); });
//更新前端为生成中
socket.emit("setFlowData", { key: "setStoryboardImage", value: { ...item, id: item.id, src: "", state: "生成中" } });
} }
return "分镜图片生成中"; }
}
return "分镜图片生成完成";
}, },
}), }),
//todo 图片是否需要参考 原资产 提示词待调 //todo 提示词待调
generate_assets_images: tool({ generate_assets_images: tool({
description: ` description: `
@ -333,12 +586,34 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
{assetId: 1, prompt: "一张猫的图片"} {assetId: 1, prompt: "一张猫的图片"}
] ]
`, `,
inputSchema: z.object({ images: z.array(z.object({ assetId: z.number(), prompt: z.string() })) }), inputSchema: z.object({
images: z.array(
z.object({
assetId: z.number().describe("衍生资产id"),
prompt: z.string().describe("提示词"),
}),
),
}),
execute: async ({ images }) => { execute: async ({ images }) => {
const skill = await useSkill("universal_agent.md"); 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)));
const assetsData = flowData["assets"];
const assetsImage: { assetId: number; prompt: string; id?: number }[] = [...images];
//获取对应的 原资产id
assetsImage.forEach((item) => {
for (const i of assetsData) {
const findData = i.derive.find((m) => m.id == item.assetId);
if (findData) {
item.id = findData.id;
break;
}
}
});
//获取所设置模型 //获取所设置模型
const imageModel = resTool.data.imageModel; const imageModel = resTool.data.imageModel;
for (const item of images) { for (const item of assetsImage) {
const [imageId] = await u.db("o_image").insert({ const [imageId] = await u.db("o_image").insert({
// 数据库插入图片记录 // 数据库插入图片记录
assetsId: item.assetId, assetsId: item.assetId,
@ -348,11 +623,11 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
}); });
u.Ai.Image(imageModel?.modelId) u.Ai.Image(imageModel?.modelId)
.run({ .run({
systemPrompt: skill.prompt, // systemPrompt: skill.prompt,
prompt: item.prompt, prompt: item.prompt,
imageBase64: [], imageBase64: await getAssetsImageBase64(item.id ? [item.id] : []),
size: imageModel?.quality, size: imageModel?.quality,
aspectRatio: imageModel?.ratio, aspectRatio: "16:9",
taskClass: "生成图片", taskClass: "生成图片",
describe: "资产图片生成", describe: "资产图片生成",
relatedObjects: "hhhh", relatedObjects: "hhhh",
@ -376,7 +651,6 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
//通知前端更新状态 //通知前端更新状态
socket.emit("setFlowData", { key: "setAssetsImage", value: { ...item, id: item.assetId, src: "", state: "生成中" } }); socket.emit("setFlowData", { key: "setAssetsImage", value: { ...item, id: item.assetId, src: "", state: "生成中" } });
} }
console.log("[tools] generate_assets_images", images);
return "资产生成中"; return "资产生成中";
}, },
}), }),
@ -385,6 +659,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
return toolsNames ? Object.fromEntries(Object.entries(tools).filter(([n]) => toolsNames.includes(n))) : tools; return toolsNames ? Object.fromEntries(Object.entries(tools).filter(([n]) => toolsNames.includes(n))) : tools;
}; };
// 获取资产图片base64
async function getAssetsImageBase64(imageIds: number[]) { async function getAssetsImageBase64(imageIds: number[]) {
if (imageIds.length === 0) return []; if (imageIds.length === 0) return [];
const imagePaths = await u const imagePaths = await u
@ -396,7 +671,31 @@ async function getAssetsImageBase64(imageIds: number[]) {
const imageUrls = await Promise.all( const imageUrls = await Promise.all(
imagePaths.map(async (i) => { imagePaths.map(async (i) => {
if (i.filePath) { if (i.filePath) {
try {
return await urlToBase64(await u.oss.getFileUrl(i.filePath)); return await urlToBase64(await u.oss.getFileUrl(i.filePath));
} catch {
return null;
}
} else {
return null;
}
}),
);
return imageUrls.filter(Boolean) as string[];
}
//获取分镜图片base64
async function getStoryboardImageBase64(imageIds: number[]) {
if (!imageIds.length) return [];
const storayboardData = await u.db("o_storyboard").whereIn("id", imageIds).select("id", "filePath");
const imageUrls = await Promise.all(
storayboardData.map(async (i) => {
if (i.filePath) {
try {
return await urlToBase64(await u.oss.getFileUrl(i.filePath));
} catch {
return null;
}
} else { } else {
return null; return null;
} }

View File

@ -140,7 +140,7 @@ function runSubAgent(parentCtx: AgentContext) {
description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI", description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI",
inputSchema: z.object({ inputSchema: z.object({
agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"), agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"),
prompt: z.string().describe("交给子Agent的任务描述"), prompt: z.string().max(100).describe("交给子Agent的任务简约描述"),
}), }),
execute: async ({ agent, prompt }) => { execute: async ({ agent, prompt }) => {
const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)]; const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];

View File

@ -108,6 +108,10 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
if (assetsList && assetsList.length) { if (assetsList && assetsList.length) {
const assetId = []; const assetId = [];
for (const i of assetsList) { for (const i of assetsList) {
if (i.id) {
assetId.push(i.id);
continue;
}
const [id] = await u.db("o_assets").insert({ const [id] = await u.db("o_assets").insert({
name: i.name, name: i.name,
prompt: i.prompt, prompt: i.prompt,

View File

@ -30,6 +30,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
builder: (table) => { builder: (table) => {
table.integer("id"); table.integer("id");
table.string("projectType"); table.string("projectType");
table.string("model");
table.text("name"); table.text("name");
table.text("intro"); table.text("intro");
table.text("type"); table.text("type");
@ -310,6 +311,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
table.text("lines"); table.text("lines");
table.text("state"); table.text("state");
table.text("reason"); table.text("reason");
table.text("index");
table.integer("createTime"); table.integer("createTime");
table.primary(["id"]); table.primary(["id"]);
table.unique(["id"]); table.unique(["id"]);
@ -415,7 +417,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
builder: (table) => { builder: (table) => {
table.integer("storyboardId").notNullable(); table.integer("storyboardId").notNullable();
table.integer("assetId").notNullable(); table.integer("assetId").notNullable();
table.primary(["assetId", "assetId"]); table.primary(["storyboardId", "assetId"]);
table.unique(["storyboardId", "assetId"]); table.unique(["storyboardId", "assetId"]);
}, },
}, },

View File

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

View File

@ -83,6 +83,7 @@ export default router.post(
async (req, res) => { async (req, res) => {
const { model, references = {}, quality, ratio, prompt, projectId, type } = req.body; const { model, references = {}, quality, ratio, prompt, projectId, type } = req.body;
const { prompt: userPrompt, images: base64Images } = await convertDirectiveAndImages(references, prompt); const { prompt: userPrompt, images: base64Images } = await convertDirectiveAndImages(references, prompt);
console.log("%c Line:86 🥒 base64Images", "background:#42b983", base64Images.map((s) => s.slice(0, 4)));
const imageClass = await u.Ai.Image(model).run({ const imageClass = await u.Ai.Image(model).run({
prompt: userPrompt, prompt: userPrompt,
imageBase64: base64Images, imageBase64: base64Images,

View File

@ -13,9 +13,10 @@ export default router.post(
imageUrl: z.string(), imageUrl: z.string(),
id: z.number().nullable().optional(), id: z.number().nullable().optional(),
type: z.enum(["role", "scene", "storyboard", "clip", "tool"]), type: z.enum(["role", "scene", "storyboard", "clip", "tool"]),
episodesId: z.number(),
}), }),
async (req, res) => { async (req, res) => {
const { edges, nodes, imageUrl, id, type } = req.body; const { edges, nodes, imageUrl, id, type, episodesId } = req.body;
let imagePath = ""; let imagePath = "";
try { try {
imagePath = new URL(imageUrl).pathname; imagePath = new URL(imageUrl).pathname;
@ -56,6 +57,7 @@ export default router.post(
} else { } else {
const [storyboardId] = await u.db("o_storyboard").insert({ const [storyboardId] = await u.db("o_storyboard").insert({
filePath: imagePath, filePath: imagePath,
scriptId: episodesId,
createTime: Date.now(), createTime: Date.now(),
}); });
insertFlowId = storyboardId; insertFlowId = storyboardId;
@ -66,6 +68,6 @@ export default router.post(
flowData: JSON.stringify({ edges, nodes }), flowData: JSON.stringify({ edges, nodes }),
...(type == "assets" ? { assetsId: insertFlowId } : { storyboardId: insertFlowId }), ...(type == "assets" ? { assetsId: insertFlowId } : { storyboardId: insertFlowId }),
}); });
return res.status(200).send(success()); return res.status(200).send(success({ id: insertFlowId }));
}, },
); );

View File

@ -14,9 +14,10 @@ export default router.post(
imageUrl: z.string(), imageUrl: z.string(),
type: z.enum(["role", "scene", "storyboard", "clip", "tool"]), type: z.enum(["role", "scene", "storyboard", "clip", "tool"]),
flowId: z.number(), flowId: z.number(),
episodesId: z.number(),
}), }),
async (req, res) => { async (req, res) => {
const { edges, nodes, id, imageUrl, flowId, type } = req.body; const { edges, nodes, id, imageUrl, flowId, type, episodesId } = req.body;
nodes.forEach((node: any) => { nodes.forEach((node: any) => {
if (node.type == "upload") { if (node.type == "upload") {
node.data.image = node.data.image ? new URL(node.data.image).pathname : ""; node.data.image = node.data.image ? new URL(node.data.image).pathname : "";
@ -30,7 +31,6 @@ export default router.post(
imagePath = new URL(imageUrl).pathname; imagePath = new URL(imageUrl).pathname;
} catch (e) {} } catch (e) {}
if (imagePath) { if (imagePath) {
console.log("%c Line:34 🍰", "background:#33a5ff");
if (type == "storyboard") { if (type == "storyboard") {
await u.db("o_storyboard").where("id", id).update({ await u.db("o_storyboard").where("id", id).update({
filePath: imagePath, filePath: imagePath,

View File

@ -38,7 +38,6 @@ export default router.post(
.where("o_assets.projectId", projectId) .where("o_assets.projectId", projectId)
.where("o_assets.id", "in", assetIds) .where("o_assets.id", "in", assetIds)
.whereNotNull("o_assets.assetsId"); .whereNotNull("o_assets.assetsId");
console.log("%c Line:35 🥚 childAssetsData", "background:#f5ce50", childAssetsData);
if (!sqlData) { if (!sqlData) {
const flowData: FlowData = { const flowData: FlowData = {
@ -86,9 +85,50 @@ export default router.post(
return res.status(200).send(success(flowData)); return res.status(200).send(success(flowData));
} else { } else {
try { try {
const storyboardData = await u.db("o_storyboard").where("scriptId", episodesId);
console.log("%c Line:90 🍡 storyboardData", "background:#ed9ec7", storyboardData.length);
await Promise.all(
storyboardData.map(async (i) => {
if (i.filePath) {
try {
i.filePath = await u.oss.getFileUrl(i.filePath);
} catch {
i.filePath = "";
}
} else {
i.filePath = "";
}
}),
);
const storyboardIds = storyboardData.map((i) => i.id);
const assetsIds = await u.db("o_assets2Storyboard").whereIn("storyboardId", storyboardIds);
const assets2StoryboardMap: Record<number, number[]> = {};
assetsIds.forEach((i) => {
if (!assets2StoryboardMap[i.storyboardId!]) {
assets2StoryboardMap[i.storyboardId!] = [];
}
assets2StoryboardMap[i.storyboardId!].push(i.assetId!);
});
const flowData = JSON.parse(sqlData!.data ?? "{}"); const flowData = JSON.parse(sqlData!.data ?? "{}");
// 将原有 flowData.assets 按 id 建立索引,以便后续合并保留旧字段
const existingAssetsMap: Record<number, any> = {};
if (Array.isArray(flowData.assets)) {
flowData.assets.forEach((a: any) => {
existingAssetsMap[a.id] = a;
});
}
flowData.assets = await Promise.all( flowData.assets = await Promise.all(
assetsData.map(async (item) => ({ assetsData.map(async (item) => {
const existing = existingAssetsMap[item.id] ?? {};
// 将原有 derive 按 id 建立索引
const existingDeriveMap: Record<number, any> = {};
if (Array.isArray(existing.derive)) {
existing.derive.forEach((d: any) => {
existingDeriveMap[d.id] = d;
});
}
return {
...existing,
id: item.id, id: item.id,
name: item.name ?? "", name: item.name ?? "",
type: item.type ?? "", type: item.type ?? "",
@ -99,6 +139,7 @@ export default router.post(
childAssetsData childAssetsData
.filter((child) => child.assetsId === item.id) .filter((child) => child.assetsId === item.id)
.map(async (child) => ({ .map(async (child) => ({
...(existingDeriveMap[child.id] ?? {}),
id: child.id, id: child.id,
assetsId: item.id, assetsId: item.id,
name: child.name ?? "", name: child.name ?? "",
@ -109,11 +150,58 @@ export default router.post(
state: child.state ?? "未生成", //todo矫正状态值 state: child.state ?? "未生成", //todo矫正状态值
})), })),
), ),
})), };
}),
); );
// 将数据库 storyboardData 按 id 建立索引
const dbStoryboardMap: Record<number, (typeof storyboardData)[number]> = {};
storyboardData.forEach((i) => {
dbStoryboardMap[i.id!] = i;
});
// 用于构造单条 storyboard 的辅助函数
const buildStoryboardItem = (i: (typeof storyboardData)[number], existing: any = {}) => ({
...existing,
id: i.id,
title: i.title,
description: i.description,
camera: i.camera,
duration: i.duration ? +i.duration : 0,
frameMode: i.frameMode,
prompt: i.prompt,
lines: i.lines,
sound: i.sound,
associateAssetsIds: assets2StoryboardMap[i.id!] ?? [],
src: i.filePath,
state: i.state,
});
// 保持旧数据顺序,新增的追加到最后
const usedIds = new Set<number>();
const orderedStoryboard: any[] = [];
// 1. 按旧数据顺序遍历,若数据库中仍存在则合并更新
if (Array.isArray(flowData.storyboard)) {
flowData.storyboard.forEach((s: any) => {
const dbItem = dbStoryboardMap[s.id];
if (dbItem) {
orderedStoryboard.push(buildStoryboardItem(dbItem, s));
usedIds.add(s.id);
}
});
}
// 2. 数据库中新增的(旧数据中没有的)追加到最后
storyboardData.forEach((i) => {
if (!usedIds.has(i.id!)) {
orderedStoryboard.push(buildStoryboardItem(i));
}
});
flowData.storyboard = orderedStoryboard;
res.status(200).send(success(flowData)); res.status(200).send(success(flowData));
} catch (err) { } catch (err) {
res.status(200).send(error()); res.status(400).send(error());
} }
} }
}, },

View File

@ -0,0 +1,115 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import sharp from "sharp";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
storyboardIds: z.array(z.number()),
}),
async (req, res) => {
const { storyboardIds } = req.body;
const storyboardImage = await u.db("o_storyboard").whereIn("id", storyboardIds).select("id", "filePath");
// 按 storyboardIds 顺序构建 filePath 映射
const filePathMap: Record<number, string> = {};
storyboardImage.forEach((i) => {
filePathMap[i.id!] = i.filePath || "";
});
const orderedFilePaths = storyboardIds.map((id: number) => filePathMap[id]);
// 读取所有图片 buffer 并获取元数据
const loaded = await Promise.all(
orderedFilePaths.map(async (filePath: string) => {
if (!filePath) return null;
const buffer = await u.oss.getFile(filePath);
const metadata = await sharp(buffer).metadata();
return { buffer, width: metadata.width || 0, height: metadata.height || 0 };
}),
);
// 过滤掉无效图片
const validImages = loaded.filter((img): img is NonNullable<typeof img> => img !== null && img.width > 0 && img.height > 0);
if (validImages.length === 0) {
return res.status(200).send(success(null));
}
// 计算网格布局
const cols = Math.min(5, validImages.length);
const rows = Math.ceil(validImages.length / cols);
const colWidths: number[] = Array(cols).fill(0);
const rowHeights: number[] = Array(rows).fill(0);
validImages.forEach((img, idx) => {
const c = idx % cols;
const r = Math.floor(idx / cols);
colWidths[c] = Math.max(colWidths[c], img.width);
rowHeights[r] = Math.max(rowHeights[r], img.height);
});
const canvasWidth = colWidths.reduce((a, b) => a + b, 0);
const canvasHeight = rowHeights.reduce((a, b) => a + b, 0);
// 为每张图片生成带标号的合成层
const compositeInputs: sharp.OverlayOptions[] = [];
for (let i = 0; i < validImages.length; i++) {
const img = validImages[i];
const c = i % cols;
const r = Math.floor(i / cols);
const x = colWidths.slice(0, c).reduce((a, b) => a + b, 0);
const y = rowHeights.slice(0, r).reduce((a, b) => a + b, 0);
// 添加图片层
compositeInputs.push({
input: img.buffer,
left: x,
top: y,
});
// 生成标号标签 SVG
const label = `S${String(i + 1).padStart(2, "0")}`;
const fontSize = Math.max(14, Math.min(img.width, img.height) * 0.06);
const padding = Math.round(fontSize * 0.4);
// 估算文字宽度(等宽近似)
const textWidth = Math.round(label.length * fontSize * 0.65);
const bgW = textWidth + padding * 2;
const bgH = Math.round(fontSize) + padding * 2;
const labelSvg = Buffer.from(
`<svg xmlns="http://www.w3.org/2000/svg" width="${bgW}" height="${bgH}">
<rect x="0" y="0" width="${bgW}" height="${bgH}" rx="4" ry="4" fill="rgba(0,0,0,0.55)"/>
<text x="${padding}" y="${padding + fontSize * 0.85}" font-family="Arial, sans-serif" font-weight="bold" font-size="${fontSize}" fill="#fff">${label}</text>
</svg>`,
);
compositeInputs.push({
input: labelSvg,
left: x + 4,
top: y + 4,
});
}
// 使用 sharp 创建画布并合成
const resultBuffer = await sharp({
create: {
width: canvasWidth,
height: canvasHeight,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 1 },
},
})
.composite(compositeInputs)
.png()
.toBuffer();
const base64 = resultBuffer.toString("base64");
const dataUrl = `data:image/png;base64,${base64}`;
return res.status(200).send(success(dataUrl));
},
);

View File

@ -42,6 +42,8 @@ async function getLines(prompt: string) {
}), }),
}), }),
}); });
console.log("%c Line:36 🍉 resText", "background:#e41a6a", resText);
const parseLines = JSON.parse(resText.text); const parseLines = JSON.parse(resText.text);
const chatLines = parseLines.elements.map((i: any) => i.lines); const chatLines = parseLines.elements.map((i: any) => i.lines);
return chatLines; return chatLines;

View File

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

View File

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

View File

@ -1,4 +1,4 @@
// @db-hash d7bc24a5440e2cc7136872da7ed6c4c7 // @db-hash 2b2f9f6242d2d20e89412ba5117415df
//该文件由脚本自动生成,请勿手动修改 //该文件由脚本自动生成,请勿手动修改
export interface memories { export interface memories {
@ -53,7 +53,7 @@ export interface o_assets {
} }
export interface o_assets2Storyboard { export interface o_assets2Storyboard {
'assetId'?: number; 'assetId'?: number;
'storyboardId': number; 'storyboardId'?: number;
} }
export interface o_event { export interface o_event {
'createTime'?: number | null; 'createTime'?: number | null;
@ -109,6 +109,7 @@ export interface o_project {
'createTime'?: number | null; 'createTime'?: number | null;
'id'?: number | null; 'id'?: number | null;
'intro'?: string | null; 'intro'?: string | null;
'model'?: string | null;
'name'?: string | null; 'name'?: string | null;
'projectType'?: string | null; 'projectType'?: string | null;
'type'?: string | null; 'type'?: string | null;