This commit is contained in:
小帅 2026-03-27 17:49:09 +08:00
commit b8a5c1e273
12 changed files with 2865 additions and 10914 deletions

File diff suppressed because one or more lines are too long

8307
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,7 @@
"cors": "^2.8.5",
"custom-electron-titlebar": "^4.2.8",
"dotenv": "^17.2.3",
"electron-rebuild": "^3.2.9",
"express": "^5.2.1",
"express-ws": "^5.0.2",
"fast-glob": "^3.3.3",

View File

@ -153,6 +153,14 @@ app.whenReady().then(async () => {
app.exit(0);
return { ok: true };
},
apprestart: () => {
// 延迟执行,让响应先返回给前端
setTimeout(() => {
app.relaunch();
app.exit(0);
}, 500);
return { ok: true, message: "应用即将重启" };
},
windowismaximized: () => ({
maximized: mainWindow?.isMaximized() ?? false,
}),

View File

@ -1,4 +1,4 @@
// @routes-hash 57463134da0d81d65d10c163ee8a2b26
// @routes-hash 4397feb795394ec5313dafe4f6a9aa67
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -96,12 +96,13 @@ import route92 from "./routes/setting/vendorConfig/addVendor";
import route93 from "./routes/setting/vendorConfig/deleteVendor";
import route94 from "./routes/setting/vendorConfig/getVendorList";
import route95 from "./routes/setting/vendorConfig/modelTest";
import route96 from "./routes/setting/vendorConfig/updateVendor";
import route97 from "./routes/task/getProject";
import route98 from "./routes/task/getTaskApi";
import route99 from "./routes/task/getTaskCategories";
import route100 from "./routes/task/taskDetails";
import route101 from "./routes/test/test";
import route96 from "./routes/setting/vendorConfig/updateCode";
import route97 from "./routes/setting/vendorConfig/updateVendor";
import route98 from "./routes/task/getProject";
import route99 from "./routes/task/getTaskApi";
import route100 from "./routes/task/getTaskCategories";
import route101 from "./routes/task/taskDetails";
import route102 from "./routes/test/test";
export default async (app: Express) => {
app.use("/api/agents/clearMemory", route1);
@ -199,10 +200,11 @@ export default async (app: Express) => {
app.use("/api/setting/vendorConfig/deleteVendor", route93);
app.use("/api/setting/vendorConfig/getVendorList", route94);
app.use("/api/setting/vendorConfig/modelTest", route95);
app.use("/api/setting/vendorConfig/updateVendor", route96);
app.use("/api/task/getProject", route97);
app.use("/api/task/getTaskApi", route98);
app.use("/api/task/getTaskCategories", route99);
app.use("/api/task/taskDetails", route100);
app.use("/api/test/test", route101);
app.use("/api/setting/vendorConfig/updateCode", route96);
app.use("/api/setting/vendorConfig/updateVendor", route97);
app.use("/api/task/getProject", route98);
app.use("/api/task/getTaskApi", route99);
app.use("/api/task/getTaskCategories", route100);
app.use("/api/task/taskDetails", route101);
app.use("/api/test/test", route102);
}

View File

@ -12,32 +12,24 @@ export default router.post(
}),
async (req, res) => {
const { type } = req.body;
const data = await u.db("o_vendorConfig").select("id", "models", "name").first();
if (!data) {
const dataList = await u.db("o_vendorConfig").select("id", "models", "name");
if (!dataList || dataList.length === 0) {
return res.status(404).send({ error: "模型未找到" });
}
const models = JSON.parse(data.models!);
if (type === "all") {
const allData = models
.filter((item: { type: string }) => item.type !== "video")
.map((item: { name: string; modelName: string; type: string }) => ({
id: data.id,
label: item.name,
value: item.modelName,
type: item.type,
name: data.name,
}));
return res.status(200).send(success(allData));
}
const filteredData = models
.filter((item: { type: string }) => item.type === type)
.map((item: { name: string; modelName: string; type: string }) => ({
const result = dataList.flatMap((data) => {
const models = JSON.parse(data.models!);
const filtered =
type === "all"
? models.filter((item: { type: string }) => item.type !== "video")
: models.filter((item: { type: string }) => item.type === type);
return filtered.map((item: { name: string; modelName: string; type: string }) => ({
id: data.id,
label: item.name,
value: item.modelName,
type: item.type,
name: data.name,
}));
res.status(200).send(success(filteredData));
});
res.status(200).send(success(result));
},
);

View File

@ -2,10 +2,10 @@ import express from "express";
import u from "@/utils";
import { z } from "zod";
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";
import { tool } from "ai";
import { o_script } from "@/types/database";
const router = express.Router();
export const AssetSchema = z.object({
@ -14,79 +14,166 @@ export const AssetSchema = z.object({
desc: z.string().describe("资产描述"),
type: z.enum(["role", "tool", "scene"]).describe("资产类型"),
});
type Asset = z.infer<typeof AssetSchema>;
/** 控制并发的辅助函数 */
async function pMap<T, R>(items: T[], fn: (item: T) => Promise<R>, concurrency: number): Promise<R[]> {
const results: R[] = [];
let index = 0;
async function worker() {
while (index < items.length) {
const i = index++;
results[i] = await fn(items[i]);
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
return results;
}
export default router.post(
"/",
validateFields({
scriptIds: z.array(z.number()),
projectId: z.number(),
concurrency: z.number().min(1).max(20).optional(),
}),
async (req, res) => {
const { scriptIds, projectId } = req.body;
const { scriptIds, projectId, concurrency = 3 } = 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 novelData = await u.db("o_novel").where("projectId", projectId).select("chapterData");
if (!novelData || novelData.length === 0) return res.status(400).send(error("请先上传小说"));
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);
}
// 每个 scriptId 对应提取出的资产列表
const scriptAssetsMap = new Map<number, Asset[]>();
await u.db("o_scriptAssets").insert(assetId.map((i) => ({ scriptId: scriptId, assetId: i })));
}
return true;
},
});
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 },
// 构建 scriptId -> script 内容的映射
const scriptMap = new Map(scripts.map((s: o_script) => [s.id, s]));
const errors: { scriptId: number; error: string }[] = [];
// 并发提取所有剧本的资产,每个剧本单独跑一次 AI
await pMap(
scriptIds,
async (scriptId: number) => {
const script = scriptMap.get(scriptId);
if (!script) {
errors.push({ scriptId, error: "未找到对应剧本" });
return;
}
// 用闭包收集当前 scriptId 的资产
let collected: Asset[] = [];
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) {
collected = assetsList;
}
return true;
},
});
console.log("%c Line:47 🥝 resData", "background:#2eafb0", resData);
} catch (e) {
console.log("%c Line:52 🍢 e", "background:#42b983", e);
try {
const skill = await useSkill("universal_agent.md");
await intansce.invoke({
messages: [
{
role: "system",
content:
skill.prompt +
"\n\n提取剧本中涉及的资产角色、场景、道具参考技能 script_assets_extract 规范,结果必须通过 resultTool 工具返回。",
},
{
role: "user",
content: `请根据以下剧本提取对应的剧本资产(角色、场景、道具、素材片段):\n\n${script.content}`,
},
],
tools: { ...skill.tools, resultTool },
});
} catch (e: any) {
const msg = e?.message || String(e);
console.error(`[extractAssets] scriptId=${scriptId} name=${script.name} 提取失败:`, msg);
errors.push({ scriptId, error: script.name + ":" + u.error(e).message });
return;
}
if (!collected.length) {
errors.push({ scriptId, error: "AI 未返回任何资产" });
return;
}
scriptAssetsMap.set(scriptId, collected);
},
concurrency,
);
// 如果全部失败,直接返回错误
if (!scriptAssetsMap.size) {
return res.status(500).send(error("所有剧本资产提取均失败", errors));
}
// 按 name 合并所有资产,同名资产只保留第一个
const mergedAssetsMap = new Map<string, Asset>();
// 同时记录每个资产名称关联的 scriptId 列表
const assetScriptIds = new Map<string, number[]>();
for (const [scriptId, assets] of scriptAssetsMap) {
for (const asset of assets) {
if (!mergedAssetsMap.has(asset.name)) {
mergedAssetsMap.set(asset.name, asset);
}
const ids = assetScriptIds.get(asset.name) || [];
ids.push(scriptId);
assetScriptIds.set(asset.name, ids);
}
}
// 一次性查询数据库中已有的资产
const existingAssets = await u.db("o_assets").where("projectId", projectId).select("id", "name");
const existingMap = new Map(existingAssets.map((a) => [a.name!, a.id!]));
// 批量插入不存在的资产
const toInsert = [...mergedAssetsMap.values()].filter((asset) => !existingMap.has(asset.name));
if (toInsert.length) {
await u.db("o_assets").insert(
toInsert.map((asset) => ({
name: asset.name,
prompt: asset.prompt,
type: asset.type,
describe: asset.desc,
projectId: projectId,
startTime: Date.now(),
})),
);
}
// 重新查询所有资产,获取完整的 name -> id 映射
const allAssets = await u.db("o_assets").where("projectId", projectId).select("id", "name");
const nameToId = new Map(allAssets.map((a) => [a.name, a.id]));
// 批量建立 scriptId <-> assetId 的关联
const scriptAssetRows: { scriptId: number; assetId: number }[] = [];
for (const [name, sIds] of assetScriptIds) {
const assetId = nameToId.get(name);
if (assetId) {
for (const sid of sIds) {
scriptAssetRows.push({ scriptId: sid, assetId });
}
}
}
await u.db("o_scriptAssets").whereIn("scriptId", scriptIds).delete();
if (scriptAssetRows.length) {
await u.db("o_scriptAssets").insert(scriptAssetRows);
}
return res.send(success(errors.length ? `部分剧本资产提取失败\n${errors.map((i) => i.error).join("\n")}` : "资产提取完成"));
},
);

View File

@ -23,11 +23,11 @@ export default router.post("/", async (req, res) => {
const currentVersionList = APP_VERSION.split(".").map(Number);
//对比Major
if (taggerList[0] > currentVersionList[0]) {
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: true }));
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: false }));
}
//对比Minor
if (taggerList[1] > currentVersionList[1]) {
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: true }));
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: false }));
}
//Patch
if (taggerList[2] > currentVersionList[2]) {

View File

@ -5,15 +5,19 @@ import { z } from "zod";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post("/", validateFields({
export default router.post(
"/",
validateFields({
id: z.number(),
name: z.string(),
model: z.string(),
modelName: z.string(),
vendorId: z.number().nullable(),
desc: z.string(),
}), async (req, res) => {
}),
async (req, res) => {
const { id, name, model, modelName, vendorId, desc } = req.body;
await u.db("o_agentDeploy").where({ id }).update({ id, name, model, modelName, vendorId, desc });
res.status(200).send(success("配置成功"));
});
},
);

View File

@ -0,0 +1,115 @@
import express from "express";
import { serializeError } from "serialize-error";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
import { transform } from "sucrase";
const router = express.Router();
const vendorConfigSchema = z.object({
id: z.string(),
author: z.string(),
description: z.string().optional(),
name: z.string(),
icon: z.string().optional(),
inputs: z.array(
z.object({
key: z.string(),
label: z.string(),
type: z.enum(["text", "password", "url"]),
required: z.boolean(),
placeholder: z.string().optional(),
}),
),
inputValues: z.record(z.string(), z.string()),
models: z.array(
z.discriminatedUnion("type", [
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("text"),
multimodal: z.boolean(),
tool: z.boolean(),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("image"),
mode: z.array(z.enum(["text", "singleImage", "multiReference"])),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("video"),
mode: z.array(
z.union([
z.enum([
"singleImage",
"multiImage",
"gridImage",
"startEndRequired",
"endFrameOptional",
"startFrameOptional",
"text",
"audioReference",
"videoReference",
]),
z.array(z.enum(["video", "image", "audio", "text"])),
]),
),
audio: z.union([z.literal("optional"), z.boolean()]),
durationResolutionMap: z.array(
z.object({
duration: z.array(z.number()),
resolution: z.array(z.string()),
}),
),
}),
]),
),
});
export default router.post(
"/",
validateFields({
id: z.string(),
tsCode: z.string(),
}),
async (req, res) => {
try {
const { tsCode, id } = req.body;
const jsCode = transform(tsCode, { transforms: ["typescript"] }).code;
const exports = u.vm(jsCode);
if (!exports) return res.status(400).send(success("脚本文件必须导出对象"));
if (!exports.textRequest) return res.status(400).send(success("脚本文件必须导出文本请求对象"));
if (!exports.imageRequest) return res.status(400).send(success("脚本文件必须导出图像请求对象"));
if (!exports.videoRequest) return res.status(400).send(success("脚本文件必须导出视频请求对象"));
if (!exports.vendor) return res.status(400).send(success("脚本文件必须导出vendor对象"));
const vendor = exports.vendor;
const result = vendorConfigSchema.safeParse(vendor);
if (!result.success) {
const errorMsg = result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
return res.status(400).send(error(`vendor配置校验失败: ${errorMsg}`));
}
await u
.db("o_vendorConfig")
.where("id", id)
.update({
author: vendor.author,
description: vendor.description || "",
name: vendor.name,
icon: vendor.icon || "",
inputs: JSON.stringify(vendor.inputs ?? []),
inputValues: JSON.stringify(vendor.inputValues ?? {}),
models: JSON.stringify(vendor.models ?? []),
code: tsCode,
createTime: Date.now(),
});
res.status(200).send(success(result.data));
} catch (err) {
console.log(err);
res.status(400).send(error(serializeError(err).message || "未知错误"));
}
},
);

View File

@ -158,14 +158,6 @@ export default router.post(
return code.slice(0, valueStart) + newValue + code.slice(valueEnd + 1);
};
// 替换 tsCode 中指定 key 的字符串/数字值(如 name: "xxx" 或 version: 1
const replacePrimitiveValue = (code: string, key: string, newValue: string | number): string => {
return code.replace(
new RegExp(`(\\b${key}\\s*:\\s*)(?:"[^"]*"|'[^']*'|\\d+)`),
(_, prefix) => `${prefix}${typeof newValue === "string" ? JSON.stringify(newValue) : newValue}`,
);
};
let updatedTsCode = tsCode;
updatedTsCode = replaceBlockValue(updatedTsCode, "inputs", JSON.stringify(inputs ?? vendor.inputs, null, 2));
updatedTsCode = replaceBlockValue(updatedTsCode, "inputValues", JSON.stringify(inputValues ?? vendor.inputValues, null, 2));

2153
yarn.lock

File diff suppressed because it is too large Load Diff