完善打包流程

This commit is contained in:
ACT丶流星雨 2026-03-24 20:57:27 +08:00
parent f1389a370d
commit 029b5de84c
24 changed files with 92366 additions and 3258 deletions

2
.gitignore vendored
View File

@ -36,6 +36,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
build/*
data/serve/
upload/*
uploads/*

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

67731
data/web/ts.worker-DGHjMaqB.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -58,6 +58,7 @@ RUN apk add --no-cache nginx supervisor && \
# 复制后端文件
COPY --from=builder /app/build ./build
COPY --from=builder /app/data/serve ./data/serve
COPY --from=builder /app/package.json ./
COPY --from=builder /app/yarn.lock ./
@ -100,7 +101,7 @@ stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:app]
command=pm2-runtime start build/app.js --name app
command=pm2-runtime start data/serve/app.js --name app
directory=/app
autostart=true
autorestart=true

View File

@ -32,6 +32,7 @@ RUN apk add --no-cache nginx supervisor && \
# 复制后端文件
COPY --from=builder /app/build ./build
COPY --from=builder /app/data/serve ./data/serve
COPY --from=builder /app/package.json ./
COPY --from=builder /app/yarn.lock ./
@ -74,7 +75,7 @@ stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:app]
command=pm2-runtime start build/app.js --name app
command=pm2-runtime start data/serve/app.js --name app
directory=/app
autostart=true
autorestart=true

View File

@ -72,4 +72,4 @@ linux:
category: Development
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
publish: null
publish: null

8246
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "toonflow-app",
"version": "1.0.7",
"name": "toonflow",
"version": "1.0.8",
"description": "Toonflow 是一款 AI 短剧漫剧工具,能够利用 AI 技术将小说自动转化为剧本,并结合 AI 生成的图片和视频,实现高效的短剧创作。",
"author": "HBAI-Ltd <ltlctools@outlook.com>",
"homepage": "https://github.com/HBAI-Ltd/Toonflow-app#readme",
@ -27,7 +27,7 @@
"dist:win": "yarn build && electron-builder --win",
"dist:mac": "yarn build && electron-builder --mac",
"dist:linux": "yarn build && electron-builder --linux",
"test": "cross-env NODE_ENV=prod node build/app.js",
"test": "cross-env NODE_ENV=prod node data/serve/app.js",
"docker:build": "docker-compose -f docker/docker-compose.yml up -d --build",
"docker:local": "docker-compose -f docker/docker-compose.local.yml up -d --build",
"debug:ai": "npx @ai-sdk/devtools",
@ -46,7 +46,7 @@
"ai": "^6.0.67",
"axios": "^1.13.2",
"axios-retry": "^4.5.0",
"better-sqlite3": "^12.6.2",
"better-sqlite3": "^12.8.0",
"compressing": "^2.1.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
@ -58,14 +58,13 @@
"is-path-inside": "^4.0.0",
"js-md5": "^0.8.3",
"jsonwebtoken": "^9.0.3",
"knex": "^3.1.0",
"knex": "^3.2.5",
"lodash": "^4.17.23",
"morgan": "^1.10.1",
"qwen-ai-provider-v5": "^2.1.0",
"serialize-error": "^13.0.1",
"sharp": "^0.34.5",
"socket.io": "^4.8.3",
"sqlite3": "^5.1.7",
"sucrase": "^3.35.1",
"uuid": "^13.0.0",
"vm2": "^3.10.5",

View File

@ -22,6 +22,7 @@ if (!fs.existsSync(envFile)) {
const external = [
"electron",
"@huggingface/transformers",
"onnxruntime-node",
"vm2",
"sqlite3",
"better-sqlite3",
@ -41,7 +42,7 @@ const appBuildConfig: esbuild.BuildOptions = {
minify: false,
format: "cjs",
allowOverwrite: true,
outfile: `build/app.js`,
outfile: `data/serve/app.js`,
platform: "node",
target: "esnext",
tsconfig: "./tsconfig.json",

View File

@ -1,11 +1,87 @@
import { app, BrowserWindow } from "electron";
import path from "path";
import startServe, { closeServe } from "src/app";
import { number } from "zod";
import fs from "fs";
import Module from "module";
// 默认端口配置
const defaultPort = 10588;
/**
* extraResources data
*/
function initializeData(): void {
const srcDir = path.join(process.resourcesPath, "data");
const destDir = path.join(app.getPath("userData"), "data");
copyDirRecursive(srcDir, destDir);
}
function copyDirRecursive(src: string, dest: string): void {
if (!fs.existsSync(src)) return;
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirRecursive(srcPath, destPath);
} else if (!fs.existsSync(destPath)) {
fs.copyFileSync(srcPath, destPath);
}
}
}
//获取全部依赖路径,优先从 unpacked 加载原生模块,其他模块从 asar 加载
function getNodeModulesPaths(): string[] {
const paths: string[] = [];
if (app.isPackaged) {
// external 依赖(原生模块)在 unpacked 目录
const unpackedNodeModules = path.join(
process.resourcesPath,
"app.asar.unpacked",
"node_modules"
);
if (fs.existsSync(unpackedNodeModules)) {
paths.push(unpackedNodeModules);
}
// 普通依赖在 asar 内
const asarNodeModules = path.join(
process.resourcesPath,
"app.asar",
"node_modules"
);
paths.push(asarNodeModules);
} else {
paths.push(path.join(process.cwd(), "node_modules"));
}
return paths;
}
//动态加载
function requireWithCustomPaths(modulePath: string): any {
const appNodeModulesPaths = getNodeModulesPaths();
// 保存原始方法
const originalNodeModulePaths = (Module as any)._nodeModulePaths;
// 临时修改模块路径解析
(Module as any)._nodeModulePaths = function (from: string): string[] {
const paths = originalNodeModulePaths.call(this, from);
// 将主程序的 node_modules 添加到前面
for (let i = appNodeModulesPaths.length - 1; i >= 0; i--) {
const p = appNodeModulesPaths[i];
if (!paths.includes(p)) {
paths.unshift(p);
}
}
return paths;
};
try {
// 清除缓存确保加载最新
delete require.cache[require.resolve(modulePath)];
return require(modulePath);
} finally {
// 恢复原始方法
(Module as any)._nodeModulePaths = originalNodeModulePaths;
}
}
function createMainWindow(port: any): void {
const win = new BrowserWindow({
width: 900,
@ -15,27 +91,29 @@ function createMainWindow(port: any): void {
});
// 开发环境和生产环境使用不同的路径
const isDev = process.env.NODE_ENV === "dev" || !app.isPackaged;
const htmlPath = isDev
? path.join(process.cwd(), "scripts", "web", "index.html")
: path.join(app.getAppPath(), "scripts", "web", "index.html");
// 使用实际端口构建地址
const baseUrl = `http://localhost:${port}`;
const wsBaseUrl = `ws://localhost:${port}`;
// 构建带有 query 参数的 URL
const url = new URL(`file://${htmlPath}`);
url.searchParams.set("baseUrl", baseUrl);
url.searchParams.set("wsBaseUrl", wsBaseUrl);
console.log("%c Line:30 🥓 url", "background:#33a5ff", url.toString());
void win.loadURL(url.toString());
const htmlPath = isDev ? path.join(process.cwd(), "data", "web", "index.html") : path.join(app.getPath("userData"), "data", "web", "index.html");
void win.loadFile(htmlPath);
}
let closeServeFn: (() => Promise<void>) | undefined;
app.whenReady().then(async () => {
try {
const port = await startServe(false);
createMainWindow(10588);
let servePath: string;
if (app.isPackaged) {
// 生产环境:从 extraResources 初始化数据到用户目录,然后从用户目录加载后端服务
initializeData();
servePath = path.join(app.getPath("userData"), "data", "serve", "app.js");
} else {
// 开发环境直接加载源码tsx 通过 -r tsx 注册了 require 钩子)
servePath = path.join(process.cwd(), "src", "app.ts");
}
// 使用自定义路径加载模块
const mod = requireWithCustomPaths(servePath);
closeServeFn = mod.closeServe;
const port = await mod.default(false);
console.log("%c Line:37 🍺 port", "background:#e41a6a", port);
createMainWindow(port);
} catch (err) {
console.error("[服务启动失败]:", err);
// 如果服务启动失败,使用默认端口创建窗口
@ -55,5 +133,5 @@ app.on("activate", () => {
});
app.on("before-quit", async (event) => {
await closeServe();
if (closeServeFn) await closeServeFn();
});

View File

@ -1,30 +1,30 @@
import { serializeError } from "serialize-error";
// 处理未捕获的 Promise 拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('[未处理的 Promise 拒绝]');
process.on("unhandledRejection", (reason, promise) => {
console.error("[未处理的 Promise 拒绝]");
if (reason instanceof Error) {
console.error('错误名称:', reason.name);
console.error('错误消息:', reason.message);
console.error('堆栈信息:', reason.stack);
console.error('序列化详情:', JSON.stringify(serializeError(reason), null, 2));
console.error("错误名称:", reason.name);
console.error("错误消息:", reason.message);
console.error("堆栈信息:", reason.stack);
console.error("序列化详情:", JSON.stringify(serializeError(reason), null, 2));
} else {
console.error('原因:', reason);
console.error('类型:', typeof reason);
console.error("原因:", reason);
console.error("类型:", typeof reason);
try {
console.error('JSON:', JSON.stringify(reason, null, 2));
} catch {
console.error('(无法序列化)');
console.error("JSON:", JSON.stringify(reason, null, 2));
} catch {
console.error("(无法序列化)");
}
}
console.error('Promise:', promise);
console.error("Promise:", promise);
});
// 处理未捕获的异常
process.on('uncaughtException', (error) => {
console.error('[未捕获的异常]');
console.error('错误名称:', error.name);
console.error('错误消息:', error.message);
console.error('堆栈信息:', error.stack);
console.error('序列化详情:', JSON.stringify(serializeError(error), null, 2));
process.on("uncaughtException", (error) => {
console.error("[未捕获的异常]");
console.error("错误名称:", error.name);
console.error("错误消息:", error.message);
console.error("堆栈信息:", error.stack);
console.error("序列化详情:", JSON.stringify(serializeError(error), null, 2));
});

View File

@ -1,4 +1,4 @@
// @routes-hash 055f8c83508ff9dfc8b98eb5108287f7
// @routes-hash 074af9af2c664d3497c2c676a3423399
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -70,20 +70,21 @@ import route66 from "./routes/setting/agentDeploy/agentSetKey";
import route67 from "./routes/setting/agentDeploy/deployAgentModel";
import route68 from "./routes/setting/agentDeploy/getAgentDeploy";
import route69 from "./routes/setting/dbConfig/clearData";
import route70 from "./routes/setting/getTextModel";
import route71 from "./routes/setting/loginConfig/getUser";
import route72 from "./routes/setting/loginConfig/updateUserPwd";
import route73 from "./routes/setting/memoryConfig/getMemory";
import route74 from "./routes/setting/memoryConfig/sureMemory";
import route75 from "./routes/setting/vendorConfig/addVendor";
import route76 from "./routes/setting/vendorConfig/deleteVendor";
import route77 from "./routes/setting/vendorConfig/getVendorList";
import route78 from "./routes/setting/vendorConfig/modelTest";
import route79 from "./routes/setting/vendorConfig/updateVendor";
import route80 from "./routes/task/getTaskApi";
import route81 from "./routes/task/getTaskCategories";
import route82 from "./routes/task/taskDetails";
import route83 from "./routes/test/test";
import route70 from "./routes/setting/fileManagement/openFolder";
import route71 from "./routes/setting/getTextModel";
import route72 from "./routes/setting/loginConfig/getUser";
import route73 from "./routes/setting/loginConfig/updateUserPwd";
import route74 from "./routes/setting/memoryConfig/getMemory";
import route75 from "./routes/setting/memoryConfig/sureMemory";
import route76 from "./routes/setting/vendorConfig/addVendor";
import route77 from "./routes/setting/vendorConfig/deleteVendor";
import route78 from "./routes/setting/vendorConfig/getVendorList";
import route79 from "./routes/setting/vendorConfig/modelTest";
import route80 from "./routes/setting/vendorConfig/updateVendor";
import route81 from "./routes/task/getTaskApi";
import route82 from "./routes/task/getTaskCategories";
import route83 from "./routes/task/taskDetails";
import route84 from "./routes/test/test";
export default async (app: Express) => {
app.use("/api/agents/clearMemory", route1);
@ -155,18 +156,19 @@ export default async (app: Express) => {
app.use("/api/setting/agentDeploy/deployAgentModel", route67);
app.use("/api/setting/agentDeploy/getAgentDeploy", route68);
app.use("/api/setting/dbConfig/clearData", route69);
app.use("/api/setting/getTextModel", route70);
app.use("/api/setting/loginConfig/getUser", route71);
app.use("/api/setting/loginConfig/updateUserPwd", route72);
app.use("/api/setting/memoryConfig/getMemory", route73);
app.use("/api/setting/memoryConfig/sureMemory", route74);
app.use("/api/setting/vendorConfig/addVendor", route75);
app.use("/api/setting/vendorConfig/deleteVendor", route76);
app.use("/api/setting/vendorConfig/getVendorList", route77);
app.use("/api/setting/vendorConfig/modelTest", route78);
app.use("/api/setting/vendorConfig/updateVendor", route79);
app.use("/api/task/getTaskApi", route80);
app.use("/api/task/getTaskCategories", route81);
app.use("/api/task/taskDetails", route82);
app.use("/api/test/test", route83);
app.use("/api/setting/fileManagement/openFolder", route70);
app.use("/api/setting/getTextModel", route71);
app.use("/api/setting/loginConfig/getUser", route72);
app.use("/api/setting/loginConfig/updateUserPwd", route73);
app.use("/api/setting/memoryConfig/getMemory", route74);
app.use("/api/setting/memoryConfig/sureMemory", route75);
app.use("/api/setting/vendorConfig/addVendor", route76);
app.use("/api/setting/vendorConfig/deleteVendor", route77);
app.use("/api/setting/vendorConfig/getVendorList", route78);
app.use("/api/setting/vendorConfig/modelTest", route79);
app.use("/api/setting/vendorConfig/updateVendor", route80);
app.use("/api/task/getTaskApi", route81);
app.use("/api/task/getTaskCategories", route82);
app.use("/api/task/taskDetails", route83);
app.use("/api/test/test", route84);
}

View File

@ -42,7 +42,7 @@ export default router.post(
}
//连接旧数据库
db2 = knex({
client: "sqlite3",
client: "better-sqlite3",
connection: {
filename: db2Path,
},

View File

@ -0,0 +1,28 @@
import express from "express";
import { z } from "zod";
import { exec } from "child_process";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { isEletron } from "@/utils/getPath";
const router = express.Router();
export default router.post(
"/",
validateFields({
path: z.string(),
}),
async (req, res) => {
if (!isEletron()) {
return res.status(400).send(error("仅支持客户端打开文件夹"));
}
const { path: folderPath } = req.body;
const platform = process.platform;
const cmd = platform === "win32" ? `explorer "${folderPath}"` : platform === "darwin" ? `open "${folderPath}"` : `xdg-open "${folderPath}"`;
exec(cmd, (err) => {
if (err) {
return res.status(200).send(error(err.message));
}
res.status(200).send(success("打开文件夹成功"));
});
},
);

View File

@ -1,4 +1,4 @@
// @db-hash 47c0e014bdbd44b60c4ebc95f4d99e0e
// @db-hash f6a9a8164252ce954394431079615459
//该文件由脚本自动生成,请勿手动修改
export interface memories {
@ -32,14 +32,11 @@ export interface o_agentWorkData {
'updateTime'?: number | null;
}
export interface o_artStyle {
'fileUrl'?: string | null;
'id'?: number;
'label'?: string | null;
'name'?: string | null;
'prompt'?: string | null;
'styles'?: string | null;
}
export interface o_assets {
'assetsId'?: number | null;
'describe'?: string | null;
'id'?: number;
'imageId'?: number | null;
@ -48,7 +45,9 @@ export interface o_assets {
'prompt'?: string | null;
'remark'?: string | null;
'scriptId'?: number | null;
'sonId'?: number | null;
'startTime'?: number | null;
'state'?: string | null;
'type'?: string | null;
}
export interface o_assets2Storyboard {

View File

@ -0,0 +1,49 @@
import { pipeline, env as transformersEnv, FeatureExtractionPipeline } from "@huggingface/transformers";
import path from "path";
import fs from "fs";
import getPath from "@/utils/getPath";
import db from "@/utils/db";
// ── 模型配置 ──
// const modelOnnxFile = ["all-MiniLM-L6-v2", "onnx", "model_fp16.onnx"]; // 模型文件路径
// const modelDtype = "fp16" as const; // 量化类型fp32
let extractor: FeatureExtractionPipeline | null = null;
export async function initEmbedding(): Promise<void> {
if (extractor) return;
const modelConfigData = await db("o_setting").whereIn("key", ["modelOnnxFile", "modelDtype"]);
const modelObj: Record<string, string> = {};
Object.entries(modelConfigData).forEach(([key, value]) => {
modelObj[key] = value as string;
});
let modelOnnxFile = modelObj?.modelOnnxFile ? JSON.parse(modelObj.modelOnnxFile) : ["all-MiniLM-L6-v2", "onnx", "model_fp16.onnx"]; // 模型文件路径
let modelDtype = modelObj?.modelDtype ?? ("fp16" as const); // 量化类型fp32
const onnxPath = path.join(getPath("models"), ...modelOnnxFile);
if (!fs.existsSync(onnxPath)) {
throw new Error(`Embedding 模型文件不存在: ${onnxPath}`);
}
transformersEnv.allowRemoteModels = false;
transformersEnv.allowLocalModels = true;
transformersEnv.localModelPath = getPath("models").replace(/\\/g, "/") + "/";
const modelFolder = modelOnnxFile[0];
// @ts-ignore - pipeline 重载联合类型过于复杂
extractor = await pipeline("feature-extraction", modelFolder, { dtype: modelDtype });
}
export async function getEmbedding(text: string): Promise<number[]> {
if (!extractor) await initEmbedding();
const output = await extractor!(text, { pooling: "mean", normalize: true });
return Array.from(output.data as Float32Array);
}
export function cosineSimilarity(a: number[], b: number[]): number {
return a.reduce((dot, v, i) => dot + v * b[i], 0);
}
export async function disposeEmbedding(): Promise<void> {
await extractor?.dispose?.();
extractor = null;
}

View File

@ -1,3 +1,7 @@
import * as ONNX_WEB from "onnxruntime-web";
// 强制 @huggingface/transformers 使用 onnxruntime-web 而非 onnxruntime-node
(globalThis as any)[Symbol.for("onnxruntime")] = ONNX_WEB;
import { pipeline, env as transformersEnv, FeatureExtractionPipeline } from "@huggingface/transformers";
import path from "path";
import fs from "fs";

View File

@ -26,7 +26,7 @@ if (!fs.existsSync(dbPath)) {
}
const db = knex({
client: "sqlite3",
client: "better-sqlite3",
connection: {
filename: dbPath,
},

View File

@ -18,3 +18,12 @@ export default (fileName?: string[] | string) => {
}
return dbPath;
};
export function isEletron() {
if (typeof process.versions?.electron !== "undefined") {
const { app } = require("electron");
return true;
} else {
return false;
}
}

889
yarn.lock

File diff suppressed because it is too large Load Diff