优化功能上bug

This commit is contained in:
zhishi 2026-03-27 22:16:32 +08:00
parent f1edd0ee86
commit 0a2c7b0e13
15 changed files with 470 additions and 218 deletions

View File

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

View File

@ -54,7 +54,7 @@ export async function decisionAI(ctx: AgentContext) {
`小说名称:${projectData?.name ?? "未知"}`,
`小说类型:${projectData?.type ?? "未知"}`,
`小说简介:${projectData?.intro ?? "无"}`,
`目标改编影视画风:${projectData?.artStyle ?? "无"}`,
`目标改编影视视觉手册|画风:${projectData?.artStyle ?? "无"}`,
`目标改编视频画幅:${projectData?.videoRatio ?? "16:9"}`,
].join("\n");
@ -71,7 +71,6 @@ 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);
},
});
@ -101,7 +100,6 @@ 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);
},
});
@ -128,7 +126,6 @@ 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

@ -67,8 +67,9 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
id: z.string().describe("章节id"),
}),
execute: async ({ id }) => {
console.log(id);
return "";
console.log("[tools] get_novel_text", id);
const data = await u.db("o_novel").where({ id }).select("chapterData").first();
return data && data?.chapterData ? data.chapterData : "";
},
}),
set_planData_storySkeleton: tool({

View File

@ -256,7 +256,9 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
table.text("name");
table.text("content");
table.integer("projectId");
table.integer("extractState");
table.integer("createTime");
table.text("errorReason");
table.primary(["id"]);
table.unique(["id"]);
},

View File

@ -1,4 +1,4 @@
// @routes-hash 4397feb795394ec5313dafe4f6a9aa67
// @routes-hash 1e5ddf0bf4594499634aaa6c96a492c7
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -68,41 +68,43 @@ import route64 from "./routes/script/delScript";
import route65 from "./routes/script/exportScript";
import route66 from "./routes/script/extractAssets";
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/dev/getSwitchAiDevTool";
import route77 from "./routes/setting/dev/updateSwitchAiDevTool";
import route78 from "./routes/setting/fileManagement/openFolder";
import route79 from "./routes/setting/getTextModel";
import route80 from "./routes/setting/loginConfig/getUser";
import route81 from "./routes/setting/loginConfig/updateUserPwd";
import route82 from "./routes/setting/memoryConfig/delAllMemory";
import route83 from "./routes/setting/memoryConfig/getMemory";
import route84 from "./routes/setting/memoryConfig/sureMemory";
import route85 from "./routes/setting/skillManagement/addSkill";
import route86 from "./routes/setting/skillManagement/deleteSkill";
import route87 from "./routes/setting/skillManagement/embeddingSkill";
import route88 from "./routes/setting/skillManagement/generateDescription";
import route89 from "./routes/setting/skillManagement/getSkillList";
import route90 from "./routes/setting/skillManagement/scanSkills";
import route91 from "./routes/setting/skillManagement/updateSkill";
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/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";
import route68 from "./routes/script/pollScriptAssets";
import route69 from "./routes/script/updateScript";
import route70 from "./routes/scriptAgent/getPlanData";
import route71 from "./routes/scriptAgent/setPlanData";
import route72 from "./routes/setting/about/checkUpdate";
import route73 from "./routes/setting/about/downloadApp";
import route74 from "./routes/setting/agentDeploy/agentSetKey";
import route75 from "./routes/setting/agentDeploy/deployAgentModel";
import route76 from "./routes/setting/agentDeploy/getAgentDeploy";
import route77 from "./routes/setting/dbConfig/clearData";
import route78 from "./routes/setting/dev/getSwitchAiDevTool";
import route79 from "./routes/setting/dev/updateSwitchAiDevTool";
import route80 from "./routes/setting/fileManagement/openFolder";
import route81 from "./routes/setting/getTextModel";
import route82 from "./routes/setting/loginConfig/getUser";
import route83 from "./routes/setting/loginConfig/updateUserPwd";
import route84 from "./routes/setting/memoryConfig/delAllMemory";
import route85 from "./routes/setting/memoryConfig/getMemory";
import route86 from "./routes/setting/memoryConfig/sureMemory";
import route87 from "./routes/setting/skillManagement/addSkill";
import route88 from "./routes/setting/skillManagement/deleteSkill";
import route89 from "./routes/setting/skillManagement/embeddingSkill";
import route90 from "./routes/setting/skillManagement/generateDescription";
import route91 from "./routes/setting/skillManagement/getSkillList";
import route92 from "./routes/setting/skillManagement/scanSkills";
import route93 from "./routes/setting/skillManagement/updateSkill";
import route94 from "./routes/setting/vendorConfig/addVendor";
import route95 from "./routes/setting/vendorConfig/deleteVendor";
import route96 from "./routes/setting/vendorConfig/getVendorList";
import route97 from "./routes/setting/vendorConfig/modelTest";
import route98 from "./routes/setting/vendorConfig/updateCode";
import route99 from "./routes/setting/vendorConfig/updateVendor";
import route100 from "./routes/task/getProject";
import route101 from "./routes/task/getTaskApi";
import route102 from "./routes/task/getTaskCategories";
import route103 from "./routes/task/taskDetails";
import route104 from "./routes/test/test";
export default async (app: Express) => {
app.use("/api/agents/clearMemory", route1);
@ -172,39 +174,41 @@ export default async (app: Express) => {
app.use("/api/script/exportScript", route65);
app.use("/api/script/extractAssets", route66);
app.use("/api/script/getScrptApi", route67);
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/dev/getSwitchAiDevTool", route76);
app.use("/api/setting/dev/updateSwitchAiDevTool", route77);
app.use("/api/setting/fileManagement/openFolder", route78);
app.use("/api/setting/getTextModel", route79);
app.use("/api/setting/loginConfig/getUser", route80);
app.use("/api/setting/loginConfig/updateUserPwd", route81);
app.use("/api/setting/memoryConfig/delAllMemory", route82);
app.use("/api/setting/memoryConfig/getMemory", route83);
app.use("/api/setting/memoryConfig/sureMemory", route84);
app.use("/api/setting/skillManagement/addSkill", route85);
app.use("/api/setting/skillManagement/deleteSkill", route86);
app.use("/api/setting/skillManagement/embeddingSkill", route87);
app.use("/api/setting/skillManagement/generateDescription", route88);
app.use("/api/setting/skillManagement/getSkillList", route89);
app.use("/api/setting/skillManagement/scanSkills", route90);
app.use("/api/setting/skillManagement/updateSkill", route91);
app.use("/api/setting/vendorConfig/addVendor", route92);
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/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);
app.use("/api/script/pollScriptAssets", route68);
app.use("/api/script/updateScript", route69);
app.use("/api/scriptAgent/getPlanData", route70);
app.use("/api/scriptAgent/setPlanData", route71);
app.use("/api/setting/about/checkUpdate", route72);
app.use("/api/setting/about/downloadApp", route73);
app.use("/api/setting/agentDeploy/agentSetKey", route74);
app.use("/api/setting/agentDeploy/deployAgentModel", route75);
app.use("/api/setting/agentDeploy/getAgentDeploy", route76);
app.use("/api/setting/dbConfig/clearData", route77);
app.use("/api/setting/dev/getSwitchAiDevTool", route78);
app.use("/api/setting/dev/updateSwitchAiDevTool", route79);
app.use("/api/setting/fileManagement/openFolder", route80);
app.use("/api/setting/getTextModel", route81);
app.use("/api/setting/loginConfig/getUser", route82);
app.use("/api/setting/loginConfig/updateUserPwd", route83);
app.use("/api/setting/memoryConfig/delAllMemory", route84);
app.use("/api/setting/memoryConfig/getMemory", route85);
app.use("/api/setting/memoryConfig/sureMemory", route86);
app.use("/api/setting/skillManagement/addSkill", route87);
app.use("/api/setting/skillManagement/deleteSkill", route88);
app.use("/api/setting/skillManagement/embeddingSkill", route89);
app.use("/api/setting/skillManagement/generateDescription", route90);
app.use("/api/setting/skillManagement/getSkillList", route91);
app.use("/api/setting/skillManagement/scanSkills", route92);
app.use("/api/setting/skillManagement/updateSkill", route93);
app.use("/api/setting/vendorConfig/addVendor", route94);
app.use("/api/setting/vendorConfig/deleteVendor", route95);
app.use("/api/setting/vendorConfig/getVendorList", route96);
app.use("/api/setting/vendorConfig/modelTest", route97);
app.use("/api/setting/vendorConfig/updateCode", route98);
app.use("/api/setting/vendorConfig/updateVendor", route99);
app.use("/api/task/getProject", route100);
app.use("/api/task/getTaskApi", route101);
app.use("/api/task/getTaskCategories", route102);
app.use("/api/task/taskDetails", route103);
app.use("/api/test/test", route104);
}

View File

@ -50,7 +50,7 @@ export default router.post(
filePath: savePath,
type,
assetsId: id,
state: "1",
state: "已完成",
});
await u.db("o_assets").where("id", id).update({
imageId: imageId,

View File

@ -20,8 +20,13 @@ export default router.post(
u.db("o_novel").where("projectId", projectId).whereIn("id", novelIds),
Promise.resolve(new u.cleanNovel()),
]);
await u.db("o_novel").where("projectId", projectId).update({ eventState: 0, event: null });
if (allChapters.length === 0) {
return res.status(400).send(success("没有对应章节"));
}
if (allChapters.filter((item) => item.eventState === 0).length) {
return res.status(400).send(success("存在未完成事件,请先等待事件完成"));
}
await u.db("o_novel").where("projectId", projectId).whereIn("id", novelIds).update({ eventState: 0, event: null });
novel.emitter.on("item", async (item) => {
await u
.db("o_novel")

View File

@ -5,7 +5,6 @@ import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取原文数据
export default router.post(
"/",
validateFields({
@ -13,7 +12,7 @@ export default router.post(
}),
async (req, res) => {
const { ids } = req.body;
const data = await u.db("o_novel").whereIn("id", ids).whereNot("eventState", 0).select("id", "event", "eventState");
const data = await u.db("o_novel").whereIn("id", ids).whereNot("eventState", 0).select("id", "event", "eventState", "errorReason");
res.status(200).send(success(data));
},
);

View File

@ -17,18 +17,21 @@ export const AssetSchema = z.object({
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]);
}
/** 按批次并发执行,每批 batchSize 个同时跑,批次完成后调用 onBatchDone */
async function pMapBatch<T, R>(
items: T[],
fn: (item: T) => Promise<R>,
batchSize: number,
onBatchDone?: (batchResults: R[]) => Promise<void>,
): Promise<R[]> {
const allResults: R[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(fn));
allResults.push(...batchResults);
if (onBatchDone) await onBatchDone(batchResults);
}
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
return results;
return allResults;
}
export default router.post(
@ -45,23 +48,94 @@ export default router.post(
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("请先上传小说"));
// 每个 scriptId 对应提取出的资产列表
const scriptAssetsMap = new Map<number, Asset[]>();
await u.db("o_script").whereIn("id", scriptIds).update({
extractState: 0,
});
// 构建 scriptId -> script 内容的映射
const scriptMap = new Map(scripts.map((s: o_script) => [s.id, s]));
const errors: { scriptId: number; error: string }[] = [];
let successCount = 0;
// 并发提取所有剧本的资产,每个剧本单独跑一次 AI
await pMap(
// 每批提取结果scriptId -> 资产列表
type BatchResult = { scriptId: number; assets: Asset[] } | null;
/** 一批剧本提取完成后统一入库并建立关联 */
async function persistBatch(batchResults: BatchResult[]) {
const validResults = batchResults.filter((r): r is { scriptId: number; assets: Asset[] } => r !== null && r.assets.length > 0);
if (!validResults.length) return;
// 合并本批所有资产,同名去重
const mergedAssetsMap = new Map<string, Asset>();
const assetScriptIds = new Map<string, number[]>();
for (const { scriptId, assets } of validResults) {
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 与资产的关联
const batchScriptIds = validResults.map((r) => r.scriptId);
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 });
}
}
}
// 先删除本批 scriptId 的旧关联,再插入新的
await u.db("o_scriptAssets").whereIn("scriptId", batchScriptIds).delete();
if (scriptAssetRows.length) {
await u.db("o_scriptAssets").insert(scriptAssetRows);
}
// 本批成功的剧本状态更新为 1成功
await u.db("o_script").whereIn("id", batchScriptIds).update({
extractState: 1,
errorReason: null,
});
}
// 按批次并发提取剧本资产,每批完成后统一入库
await pMapBatch<number, BatchResult>(
scriptIds,
async (scriptId: number) => {
const script = scriptMap.get(scriptId);
if (!script) {
errors.push({ scriptId, error: "未找到对应剧本" });
return;
await u.db("o_script").where("id", scriptId).update({ extractState: -1, errorReason: "未找到对应剧本" });
return null;
}
// 用闭包收集当前 scriptId 的资产
@ -102,78 +176,23 @@ export default router.post(
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;
await u.db("o_script").where("id", scriptId).update({ extractState: -1, errorReason: u.error(e).message });
return null;
}
if (!collected.length) {
errors.push({ scriptId, error: "AI 未返回任何资产" });
return;
await u.db("o_script").where("id", scriptId).update({ extractState: -1, errorReason: "AI 未返回任何资产" });
return null;
}
scriptAssetsMap.set(scriptId, collected);
successCount++;
return { scriptId, assets: collected };
},
concurrency,
persistBatch,
);
// 如果全部失败,直接返回错误
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")}` : "资产提取完成"));
return res.send(success("开始提取资产"));
},
);

View File

@ -22,8 +22,10 @@ export default router.post(
const assetsData = await u
.db("o_assets")
.leftJoin("o_scriptAssets", "o_assets.id", "o_scriptAssets.assetId")
// @ts-ignore
.whereIn( "o_scriptAssets.scriptId", data.map((i) => i.id))
.whereIn(
"o_scriptAssets.scriptId",
data.map((i) => i.id!),
)
.select("o_assets.id", "o_assets.name", "o_scriptAssets.scriptId");
const scriptAssetsMap: Record<number, { id: number; name: string }[]> = {};
assetsData.forEach((i) => {
@ -37,6 +39,8 @@ export default router.post(
id: i.id,
name: i.name,
content: i.content,
extractState: i.extractState,
errorReason: i.errorReason,
createTime: i.createTime,
relatedAssets: scriptAssetsMap[i.id!] || [],
}));

View File

@ -0,0 +1,18 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
ids: z.array(z.number()),
}),
async (req, res) => {
const { ids } = req.body;
const data = await u.db("o_script").whereIn("id", ids).whereNot("extractState", "生成中").select("id", "extractState", "errorReason");
res.status(200).send(success(data));
},
);

View File

@ -0,0 +1,238 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import getPath from "@/utils/getPath";
import z from "zod";
import fs from "fs";
import path from "path";
import axios from "axios";
import compressing from "compressing";
import { validateFields } from "@/middleware/middleware";
import { spawn } from "child_process";
const router = express.Router();
/** 仓库源配置 */
const REPO_SOURCES = {
github: {
repo: "HBAI-Ltd/Toonflow-app",
api: "https://api.github.com/repos/HBAI-Ltd/Toonflow-app/releases/latest",
headers: { Accept: "application/vnd.github.v3+json" },
},
gitee: {
repo: "HBAI-Ltd/Toonflow-app",
api: "https://gitee.com/api/v5/repos/HBAI-Ltd/Toonflow-app/releases/latest",
headers: {},
},
} as const;
type SourceType = keyof typeof REPO_SOURCES;
function normalizeAssets(source: SourceType, release: any): { name: string; browser_download_url: string }[] {
if (source === "github") {
return (release.assets ?? []).map((a: any) => ({
name: a.name,
browser_download_url: a.browser_download_url,
}));
}
return (release.assets ?? []).map((a: any) => ({
name: a.name,
browser_download_url: a.browser_download_url,
}));
}
/** 获取当前系统平台和架构标识,用于匹配安装包文件名 */
function getPlatformArch(): { platform: string; arch: string } {
const platform = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux";
const arch = process.arch === "arm64" ? "arm64" : "x64";
return { platform, arch };
}
/** 匹配安装包资产(.exe / .dmg / .AppImage / .portable.exe */
function findInstallerAsset(assets: any[]): any | null {
const { platform, arch } = getPlatformArch();
const installerExtensions: Record<string, string[]> = {
win: [".exe"],
mac: [".dmg"],
linux: [".AppImage"],
};
const exts = installerExtensions[platform] || [".exe"];
// 优先找 nsis 安装包(排除 portable如果没有再找 portable
return (
assets.find(
(a: any) =>
exts.some((ext) => a.name.endsWith(ext)) &&
a.name.includes(arch) &&
!a.name.toLowerCase().includes("portable") &&
!a.name.endsWith(".blockmap"),
) ??
assets.find((a: any) => exts.some((ext) => a.name.endsWith(ext)) && a.name.includes(arch) && !a.name.endsWith(".blockmap")) ??
null
);
}
/**
*
*/
async function downloadFile(url: string, destPath: string): Promise<void> {
const dir = path.dirname(destPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const response = await axios.get(url, {
responseType: "stream",
headers: { Accept: "application/octet-stream" },
timeout: 600_000, // 10 分钟超时
});
const writer = fs.createWriteStream(destPath);
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on("finish", resolve);
writer.on("error", reject);
});
}
export default router.post(
"/",
validateFields({
source: z.enum(["github", "gitee"]),
reinstall: z.boolean(),
latestVersion: z.string(),
}),
async (req, res) => {
try {
const { reinstall, latestVersion, source } = req.body as {
reinstall: boolean;
latestVersion: string;
source: string;
};
if (!latestVersion) {
return res.status(400).send(error("缺少目标版本号 latestVersion"));
}
const sourceConfig = REPO_SOURCES[source as SourceType] ?? REPO_SOURCES.github;
// ─── 获取 Release 信息(支持 GitHub / Gitee ──────────────────────
let releaseRes;
try {
releaseRes = await axios.get(sourceConfig.api, {
headers: sourceConfig.headers,
timeout: 30_000,
});
} catch (e) {
return res.status(500).send(error(`获取 ${source} Release 信息失败`));
}
const release = releaseRes.data;
const assets = normalizeAssets(source as SourceType, release);
if (reinstall) {
// ═══════════════ 模式 A下载完整安装包 ═══════════════
const installerAsset = findInstallerAsset(assets);
if (!installerAsset) {
return res.status(404).send(error("未找到当前平台的安装包,请前往 GitHub Releases 手动下载"));
}
const tempDir = getPath(["temp"]);
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
const installerPath = path.join(tempDir, installerAsset.name);
// 如果已经下载过相同文件,跳过下载
if (!fs.existsSync(installerPath)) {
await downloadFile(installerAsset.browser_download_url, installerPath);
}
// 使用 shell 打开安装程序
const sub = spawn("cmd", ["/c", `${installerPath}`], {
cwd: tempDir,
detached: true,
stdio: "ignore",
windowsHide: false,
});
sub.unref();
return res.status(200).send(
success({
type: "reinstall",
version: latestVersion,
filePath: installerPath,
message: "安装包已下载并打开,请按照安装向导完成更新",
}),
);
} else {
// ═══════════════ 模式 Bdata 补丁热更新 ═══════════════
const patchAsset = assets.find((a: any) => a.name.startsWith(latestVersion) && a.name.endsWith(".zip")) ?? null;
if (!patchAsset) {
return res.status(404).send(error("未找到 data 补丁包,请前往 GitHub Releases 手动下载"));
}
//
const tempDir = getPath(["temp"]);
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
const patchZipPath = path.join(tempDir, `${latestVersion}.zip`);
// 下载补丁 zip
await downloadFile(patchAsset.browser_download_url, patchZipPath);
// 解压覆盖到 data 目录(同名文件夹先删除再解压,确保完全替换)
const dataDir = getPath();
// 先读取 zip 内的顶层文件夹/文件列表,删除 data 目录下的同名项
const zipStream = new compressing.zip.UncompressStream({ source: patchZipPath, zipFileNameEncoding: "utf8" });
const topLevelEntries = new Set<string>();
await new Promise<void>((resolve, reject) => {
zipStream.on("entry", (_header: any, stream: any, next: () => void) => {
const entryName: string = _header.name || "";
// 取顶层名称(第一个 / 之前的部分)
const topName = entryName.split("/")[0];
if (topName) topLevelEntries.add(topName);
stream.resume();
next();
});
zipStream.on("finish", resolve);
zipStream.on("error", reject);
});
// 删除 data 目录下与 zip 顶层同名的文件夹/文件
for (const name of topLevelEntries) {
const targetPath = path.join(dataDir, name);
if (fs.existsSync(targetPath)) {
const stat = fs.statSync(targetPath);
if (stat.isDirectory()) {
fs.rmSync(targetPath, { recursive: true, force: true });
} else {
fs.unlinkSync(targetPath);
}
}
}
await compressing.zip.uncompress(patchZipPath, dataDir, { zipFileNameEncoding: "utf8" });
// 清理临时文件
try {
fs.unlinkSync(patchZipPath);
} catch {
// 忽略清理失败
}
return res.status(200).send(
success({
type: "patch",
version: latestVersion,
message: "补丁更新完成,请重启应用以使更新生效",
restartRequired: true,
}),
);
}
} catch (err: any) {
console.error("[downloadApp] 更新失败:", err);
const message = err?.response?.status === 404 ? "未找到更新资源,请检查版本号或稍后重试" : (err?.message ?? "更新失败,请稍后重试");
return res.status(500).send(error(message));
}
},
);

View File

@ -73,7 +73,6 @@ export default router.post(
"/",
validateFields({
id: z.string(),
tsCode: z.string(),
inputValues: z.record(z.string(), z.string()),
inputs: z.array(
z.object({
@ -121,57 +120,16 @@ export default router.post(
),
}),
async (req, res) => {
const { id, tsCode, name, models, inputs, inputValues, icon } = 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}`));
}
const replaceBlockValue = (code: string, key: string, newValue: string): string => {
const open = newValue.trimStart()[0] as "[" | "{";
const close = open === "[" ? "]" : "}";
const keyMatch = code.match(new RegExp(`\\b${key}\\s*:\\s*[\\[{]`));
if (!keyMatch || keyMatch.index === undefined) return code;
const valueStart = keyMatch.index + keyMatch[0].length - 1;
let depth = 0;
let valueEnd = -1;
for (let i = valueStart; i < code.length; i++) {
if (code[i] === open) depth++;
else if (code[i] === close) {
depth--;
if (depth === 0) {
valueEnd = i;
break;
}
}
}
if (valueEnd === -1) return code;
return code.slice(0, valueStart) + newValue + code.slice(valueEnd + 1);
};
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));
updatedTsCode = replaceBlockValue(updatedTsCode, "models", JSON.stringify(models ?? vendor.models, null, 2));
const { id, name, models, inputs, inputValues, icon } = req.body;
await u
.db("o_vendorConfig")
.where("id", id)
.update({
inputs: inputs ? JSON.stringify(inputs) : JSON.stringify(vendor.inputs),
inputValues: inputValues ? JSON.stringify(inputValues) : JSON.stringify(vendor.inputValues),
models: models ? JSON.stringify(models) : JSON.stringify(vendor.models),
code: updatedTsCode,
inputs: JSON.stringify(inputs),
inputValues: JSON.stringify(inputValues),
models: JSON.stringify(models),
});
res.status(200).send(success(result.data));
res.status(200).send(success("更新成功"));
},
);

View File

@ -1,6 +1,13 @@
// @db-hash 0041ea9843a4bb46f03412c516ec323b
// @db-hash 1af54b27110c54bf92390a017ee6b240
//该文件由脚本自动生成,请勿手动修改
export interface _o_script_old_20260327 {
'content'?: string | null;
'createTime'?: number | null;
'id'?: number;
'name'?: string | null;
'projectId'?: number | null;
}
export interface memories {
'content': string;
'createTime': number;
@ -120,6 +127,8 @@ export interface o_project {
export interface o_script {
'content'?: string | null;
'createTime'?: number | null;
'errorReason'?: string | null;
'extractState'?: number | null;
'id'?: number;
'name'?: string | null;
'projectId'?: number | null;
@ -221,6 +230,7 @@ export interface o_videoConfig {
}
export interface DB {
"_o_script_old_20260327": _o_script_old_20260327;
"memories": memories;
"o_agentDeploy": o_agentDeploy;
"o_agentWorkData": o_agentWorkData;

View File

@ -71,10 +71,7 @@ class CleanNovel {
};
// 启动最多 concurrency 个并发任务
const workers = Array.from(
{ length: Math.min(this.concurrency, allChapters.length) },
() => runNext()
);
const workers = Array.from({ length: Math.min(this.concurrency, allChapters.length) }, () => runNext());
await Promise.all(workers);