Files
TrueGrowth/vendor/aigcpanel/electron/mapi/httpserver/main.ts
Jammy 52636c91ae
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled
Track bundled vendor runtime sources
2026-07-07 10:05:50 +08:00

826 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import express from "express";
import type { Request, Response } from "express";
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { ipcMain } from "electron";
import { Log } from "../log/main";
import ConfigMain from "../config/main";
import StorageMain from "../storage/main";
import { Events } from "../event/main";
import { DBMain } from "../db/main";
import { AppEnv } from "../env";
import docHtml from "./doc.html?raw";
let server: http.Server | null = null;
let isRunning = false;
let runningPort = 0;
let runningToken = "";
const getAvailablePort = (): Promise<number> => {
return new Promise((resolve, reject) => {
const s = http.createServer();
s.listen(0, "127.0.0.1", () => {
const addr = s.address() as { port: number };
const port = addr.port;
s.close(() => resolve(port));
});
s.on("error", reject);
});
};
const generateToken = (): string => {
return (
crypto.randomUUID().replace(/-/g, "") +
crypto.randomUUID().replace(/-/g, "")
);
};
const writeCliAuthFile = (port: number, token: string): void => {
try {
const filePath = path.join(AppEnv.userData, "cli-auth.json");
fs.writeFileSync(filePath, JSON.stringify({ port, token }), "utf-8");
} catch (e) {
Log.error("httpserver.writeCliAuthFile.error", e);
}
};
const functionArgsMap: Record<string, string[]> = {
soundTts: ["text"],
soundClone: ["text", "promptAudio", "promptText"],
videoGen: ["video", "audio"],
asr: ["audio"],
textToImage: ["prompt"],
imageToImage: ["image", "prompt"],
live: [],
};
const functionBizMap: Record<string, string> = {
soundTts: "SoundGenerate",
soundClone: "SoundGenerate",
videoGen: "VideoGen",
asr: "SoundAsr",
textToImage: "TextToImage",
imageToImage: "ImageToImage",
};
const getInstalledServers = async () => {
const storageData = await StorageMain.read("server", null);
const records = storageData?.records || [];
return records
.filter((r: any) => r.name && r.version)
.map((r: any) => ({
id: `${r.name}|${r.version}`,
name: r.name,
version: r.version,
title: r.title || r.name,
functions: (r.functions || []).map((funcName: string) => ({
name: funcName,
args: functionArgsMap[funcName] || [],
param: r.config?.functions?.[funcName]?.param || [],
})),
}));
};
const getServerRecord = async (serverName: string, serverVersion: string) => {
const storageData = await StorageMain.read("server", null);
const records = storageData?.records || [];
return records.find(
(r: any) => r.name === serverName && r.version === serverVersion,
);
};
const buildModelConfig = (
funcName: string,
serverName: string,
serverTitle: string,
serverVersion: string,
param: any,
) => {
switch (funcName) {
case "soundTts":
return {
type: "SoundTts",
ttsServerKey: `${serverName}|${serverVersion}`,
ttsParam: param?.param || {},
text: param?.text || "",
};
case "soundClone":
return {
type: "SoundClone",
cloneServerKey: `${serverName}|${serverVersion}`,
cloneParam: param?.param || {},
text: param?.text || "",
promptUrl: param?.promptAudio || "",
promptText: param?.promptText || "",
};
case "videoGen":
return {
soundType: "soundCustom",
soundCustomFile: param?.audio || "",
videoTemplateUrl: param?.video || "",
};
case "asr":
return {
audio: param?.audio || "",
};
case "textToImage":
return {
prompt: param?.prompt || "",
textToImage: {
serverName,
serverTitle,
serverVersion,
type: "TextToImage",
serverKey: `${serverName}|${serverVersion}`,
param: param?.param || {},
},
};
case "imageToImage":
return {
image: param?.image || "",
prompt: param?.prompt || "",
imageToImage: {
serverName,
serverTitle,
serverVersion,
type: "ImageToImage",
serverKey: `${serverName}|${serverVersion}`,
param: param?.param || {},
},
};
default:
return param || {};
}
};
const buildTaskParam = (funcName: string, param: any) => {
if (funcName === "videoGen" || funcName === "asr") {
return param?.param || {};
}
return {};
};
const buildTaskTitle = (funcName: string, param: any): string => {
switch (funcName) {
case "soundTts":
return param?.text ? String(param.text).slice(0, 20) : "TTS任务";
case "soundClone":
return param?.text
? String(param.text).slice(0, 20)
: "音色克隆任务";
case "videoGen":
return "AI数字人视频";
case "asr":
return "ASR识别任务";
case "textToImage":
return param?.prompt
? String(param.prompt).slice(0, 20)
: "文生图任务";
case "imageToImage":
return param?.prompt
? String(param.prompt).slice(0, 20)
: "图生图任务";
default:
return "任务";
}
};
const sendJson = (res: Response, statusCode: number, data: any) => {
res.status(statusCode).json(data);
};
const createApp = (port: number, token: string) => {
const app = express();
app.use(express.json());
app.use((_req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization",
);
if (_req.method === "OPTIONS") {
res.status(200).end();
return;
}
next();
});
app.get("/doc", (_req, res) => {
const html = docHtml.replace(/\{\{PORT\}\}/g, String(port));
res.status(200)
.set("Content-Type", "text/html; charset=utf-8")
.send(html);
});
app.use((_req, res, next) => {
const auth = _req.headers["authorization"] || "";
if (!auth.startsWith("Bearer ") || auth.slice(7) !== token) {
sendJson(res, 401, { code: -1, msg: "Unauthorized" });
return;
}
next();
});
app.post("/api/model/list", async (_req, res) => {
try {
const servers = await getInstalledServers();
sendJson(res, 200, { code: 0, data: servers });
} catch (e) {
sendJson(res, 500, { code: -1, msg: String(e) });
}
});
app.post("/api/model/call", async (req, res) => {
try {
const {
model,
version,
function: funcName,
param,
} = req.body || {};
let serverName: string;
let serverVersion: string;
if (version !== undefined) {
// 新格式model 和 version 分别传递
serverName = model || "";
serverVersion = version || "";
} else {
// 兼容旧格式model 为 "name|version"
const parts = (model || "").split("|");
serverName = parts[0];
serverVersion = parts.slice(1).join("|");
}
if (!serverName || !serverVersion) {
sendJson(res, 400, {
code: -1,
msg: "Missing model or version",
});
return;
}
if (!funcName) {
sendJson(res, 400, { code: -1, msg: "Missing function" });
return;
}
const biz = functionBizMap[funcName];
if (!biz) {
sendJson(res, 400, {
code: -1,
msg: `Unknown function: ${funcName}`,
});
return;
}
const serverRecord = await getServerRecord(
serverName,
serverVersion,
);
if (!serverRecord) {
sendJson(res, 400, {
code: -1,
msg: `Server not found: ${serverName}|${serverVersion}`,
});
return;
}
const serverTitle = serverRecord.title || serverName;
const modelConfig = buildModelConfig(
funcName,
serverName,
serverTitle,
serverVersion,
param,
);
const taskParam = buildTaskParam(funcName, param);
const title = buildTaskTitle(funcName, param);
const taskDbId = await DBMain.insert(
`INSERT INTO data_task (biz, title, status, startTime, serverName, serverTitle, serverVersion, param, jobResult, modelConfig, result, type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
biz,
title,
"queue",
Date.now(),
serverName,
serverTitle,
serverVersion,
JSON.stringify(taskParam),
JSON.stringify({}),
JSON.stringify(modelConfig),
JSON.stringify({}),
1,
],
);
const taskId = String(taskDbId);
sendJson(res, 200, { code: 0, data: { taskId } });
Events.callPage("main", "httpserver:submitTask", {
biz,
taskId,
}).catch((err) => {
Log.error("httpserver.submitTask.error", err);
});
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
app.post("/api/model/query", async (req, res) => {
try {
const { taskId } = req.body || {};
if (!taskId) {
sendJson(res, 400, { code: -1, msg: "Missing taskId" });
return;
}
const LONG_POLL_MS = 60_000;
const POLL_INTERVAL_MS = 500;
const deadline = Date.now() + LONG_POLL_MS;
const queryOnce = async () => {
const record = await DBMain.first(
"SELECT * FROM data_task WHERE id = ?",
[taskId],
);
if (!record) {
return {
done: true,
payload: {
code: 0,
data: { status: "error", error: "Task not found" },
},
};
}
if (record.status === "success") {
let result: any = null;
try {
const parsed = JSON.parse(record.result);
if (parsed && Object.keys(parsed).length > 0)
result = parsed;
} catch (_) {}
if (result) {
return {
done: true,
payload: {
code: 0,
data: {
status: "success",
result: {
code: 0,
msg: "ok",
data: {
type: "success",
start: record.startTime || 0,
end: record.endTime || 0,
data: result,
},
},
},
},
};
}
} else if (record.status === "fail") {
return {
done: true,
payload: {
code: 0,
data: {
status: "error",
error: record.statusMsg || "Task failed",
},
},
};
} else if (record.status === "pause") {
let jobResult: any = null;
try {
jobResult = JSON.parse(record.jobResult);
} catch (_) {}
return {
done: true,
payload: {
code: 0,
data: {
status: "pause",
taskId: String(record.id),
step: jobResult?.step || null,
statusMsg: record.statusMsg || "Task paused",
},
},
};
}
return { done: false, payload: null };
};
while (true) {
const { done, payload } = await queryOnce();
if (done) {
sendJson(res, 200, payload!);
return;
}
if (Date.now() >= deadline) break;
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
sendJson(res, 200, { code: 0, data: { status: "pending" } });
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
// ── GET /api/workflow/list ───────────────────────────────────────────
app.get("/api/workflow/list", async (_req, res) => {
try {
const rows = await DBMain.select(
"SELECT id, name, createdAt, updatedAt FROM workflow ORDER BY createdAt DESC",
[],
);
sendJson(res, 200, { code: 0, data: { list: rows } });
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
// ── POST /api/workflow/run-named ─────────────────────────────────────
// 运行已存在的工作流(按名称查找),创建运行记录并执行
app.post("/api/workflow/run-named", async (req, res) => {
try {
const { name } = req.body || {};
if (!name) {
sendJson(res, 400, { code: -1, msg: "Missing name" });
return;
}
const wf = await DBMain.first(
"SELECT * FROM workflow WHERE name = ? ORDER BY createdAt DESC LIMIT 1",
[name],
);
if (!wf) {
sendJson(res, 404, {
code: -1,
msg: `Workflow not found: ${name}`,
});
return;
}
let workflowData: any;
try {
workflowData = JSON.parse(wf.data);
} catch {
workflowData = {};
}
workflowData.status = "idle";
const now = Math.floor(Date.now() / 1000);
const workflowLogId = await DBMain.insert(
`INSERT INTO workflow_log (createdAt, updatedAt, workflowId, name, data, status, startTime, endTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
now,
now,
wf.id,
name,
JSON.stringify(workflowData),
"idle",
Date.now(),
0,
],
);
sendJson(res, 200, {
code: 0,
data: {
workflowLogId: String(workflowLogId),
workflowId: String(wf.id),
},
});
Events.callPage("main", "httpserver:submitWorkflow", {
workflowLogId: String(workflowLogId),
}).catch((err) => {
Log.error("httpserver.submitWorkflow.error", err);
});
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
// ── POST /api/workflow/cleanup-test ──────────────────────────────────
// 删除所有 _test_ 前缀及"新建工作流"命名的工作流及其运行记录
app.post("/api/workflow/cleanup-test", async (_req, res) => {
try {
const testWorkflows = await DBMain.select(
`SELECT id FROM workflow WHERE name LIKE '_test_%' OR name = '新建工作流'`,
[],
);
for (const wf of testWorkflows) {
await DBMain.execute(
"DELETE FROM workflow_log WHERE workflowId = ?",
[wf.id],
);
await DBMain.execute("DELETE FROM workflow WHERE id = ?", [
wf.id,
]);
}
sendJson(res, 200, {
code: 0,
data: { deleted: testWorkflows.length },
});
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
// ── POST /api/workflow/run ───────────────────────────────────────────
app.post("/api/workflow/run", async (req, res) => {
try {
const { data: workflowData } = req.body || {};
if (!workflowData || !Array.isArray(workflowData.nodes)) {
sendJson(res, 400, { code: -1, msg: "Missing workflow data" });
return;
}
const now = Math.floor(Date.now() / 1000);
const workflowId = await DBMain.insert(
`INSERT INTO workflow (createdAt, updatedAt, name, data) VALUES (?, ?, ?, ?)`,
[now, now, "_test_" + now, JSON.stringify(workflowData)],
);
workflowData.status = "idle";
const workflowLogId = await DBMain.insert(
`INSERT INTO workflow_log (createdAt, updatedAt, workflowId, name, data, status, startTime, endTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
now,
now,
workflowId,
"_test_" + now,
JSON.stringify(workflowData),
"idle",
Date.now(),
0,
],
);
sendJson(res, 200, {
code: 0,
data: {
workflowLogId: String(workflowLogId),
workflowId: String(workflowId),
},
});
Events.callPage("main", "httpserver:submitWorkflow", {
workflowLogId: String(workflowLogId),
}).catch((err) => {
Log.error("httpserver.submitWorkflow.error", err);
});
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
// ── POST /api/workflow/query ─────────────────────────────────────────
app.post("/api/workflow/query", async (req, res) => {
try {
const { workflowLogId } = req.body || {};
if (!workflowLogId) {
sendJson(res, 400, { code: -1, msg: "Missing workflowLogId" });
return;
}
const LONG_POLL_MS = 10_000;
const POLL_INTERVAL_MS = 500;
const deadline = Date.now() + LONG_POLL_MS;
while (true) {
const record = await DBMain.first(
"SELECT * FROM workflow_log WHERE id = ?",
[workflowLogId],
);
if (!record) {
sendJson(res, 200, {
code: 0,
data: { status: "error", statusMsg: "Not found" },
});
return;
}
if (
record.status === "success" ||
record.status === "error" ||
record.status === "pause"
) {
let logData: any = null;
try {
logData = JSON.parse(record.data);
} catch {}
sendJson(res, 200, {
code: 0,
data: {
status: record.status,
statusMsg: record.statusMsg,
logData,
},
});
return;
}
if (Date.now() >= deadline) break;
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
sendJson(res, 200, { code: 0, data: { status: "pending" } });
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
// ── POST /api/task/submit ────────────────────────────────────────────
app.post("/api/task/submit", async (req, res) => {
try {
const {
biz,
modelConfig,
param,
title,
serverName,
serverTitle,
serverVersion,
} = req.body || {};
if (!biz) {
sendJson(res, 400, { code: -1, msg: "Missing biz" });
return;
}
const taskTitle = title || biz + "-task";
const taskDbId = await DBMain.insert(
`INSERT INTO data_task (biz, title, status, startTime, serverName, serverTitle, serverVersion, param, jobResult, modelConfig, result, type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
biz,
taskTitle,
"queue",
Date.now(),
serverName || "",
serverTitle || serverName || "",
serverVersion || "",
JSON.stringify(param || {}),
JSON.stringify({}),
JSON.stringify(modelConfig || {}),
JSON.stringify({}),
1,
],
);
const taskId = String(taskDbId);
sendJson(res, 200, { code: 0, data: { taskId } });
Events.callPage("main", "httpserver:submitTask", {
biz,
taskId,
}).catch((err) => {
Log.error("httpserver.submitTask.error", err);
});
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
// ── POST /api/task/continue ──────────────────────────────────────────
// Continue a paused task with stage-specific data.
// Body: { taskId, stage, data }
// stage: the current paused step (e.g. "Config", "Confirm")
// data: the stage-specific payload (e.g. { times: [...] })
app.post("/api/task/continue", async (req, res) => {
try {
const { taskId, stage, data } = req.body || {};
if (!taskId || !stage) {
sendJson(res, 400, {
code: -1,
msg: "Missing taskId or stage",
});
return;
}
const record = await DBMain.first(
"SELECT * FROM data_task WHERE id = ?",
[taskId],
);
if (!record) {
sendJson(res, 200, {
code: -1,
msg: `Task not found: ${taskId}`,
});
return;
}
if (record.status !== "pause") {
sendJson(res, 200, {
code: -1,
msg: `Task is not paused (current status: ${record.status})`,
});
return;
}
// Map stage → next step
const stageTransitions: Record<string, Record<string, string>> = {
VideoZoom: { Config: "Render", RenderConfirm: "End" },
VideoMark: { Config: "Render", RenderConfirm: "End" },
VideoSpeedPart: { Config: "Render" },
VideoKeepPart: { Config: "Render" },
VideoQuickCut: { Confirm: "Merge" },
};
const biz = record.biz;
const transitions = stageTransitions[biz] || {};
const nextStep = transitions[stage];
if (!nextStep) {
sendJson(res, 200, {
code: -1,
msg: `Unknown stage "${stage}" for biz "${biz}"`,
});
return;
}
// Merge jobResult update
let currentJobResult: any = {};
try {
currentJobResult = JSON.parse(record.jobResult) || {};
} catch (_) {}
const updatedJobResult = {
...currentJobResult,
step: nextStep,
[stage]: {
...((currentJobResult[stage] as any) || {}),
...(data || {}),
},
};
await DBMain.execute(
"UPDATE data_task SET status = ?, jobResult = ?, statusMsg = ? WHERE id = ?",
["queue", JSON.stringify(updatedJobResult), "", taskId],
);
sendJson(res, 200, { code: 0, data: { taskId } });
Events.callPage("main", "httpserver:submitTask", {
biz,
taskId,
}).catch((err) => {
Log.error("httpserver.continueTask.error", err);
});
} catch (e) {
sendJson(res, 500, { code: -1, msg: `Internal error: ${e}` });
}
});
app.use((_req, res) => {
sendJson(res, 404, { code: -1, msg: "Not found" });
});
return app;
};
const start = async (port?: number): Promise<void> => {
if (isRunning) {
await stop();
}
const resolvedPort = port || (await getAvailablePort());
const token = generateToken();
return new Promise((resolve, reject) => {
const app = createApp(resolvedPort, token);
const s = http.createServer(app);
s.listen(resolvedPort, "127.0.0.1", async () => {
server = s;
isRunning = true;
runningPort = resolvedPort;
runningToken = token;
await ConfigMain.set("httpServerPort", resolvedPort);
await ConfigMain.set("httpServerToken", token);
writeCliAuthFile(resolvedPort, token);
Log.info("httpserver.start", { port: resolvedPort });
resolve();
});
s.on("error", (err: any) => {
Log.error("httpserver.error", err);
reject(err);
});
});
};
const stop = async (): Promise<void> => {
return new Promise((resolve) => {
if (!server) {
isRunning = false;
runningPort = 0;
resolve();
return;
}
server.close(() => {
server = null;
isRunning = false;
runningPort = 0;
resolve();
});
});
};
const status = () => ({
running: isRunning,
port: runningPort,
});
ipcMain.handle("httpserver:status", async () => {
return status();
});
ipcMain.handle("httpserver:start", async (_, port?: number) => {
try {
await start(port);
return { code: 0 };
} catch (e) {
return { code: -1, msg: String(e) };
}
});
ipcMain.handle("httpserver:stop", async () => {
await stop();
return { code: 0 };
});
export const HttpServerMain = {
start,
stop,
status,
};
export default HttpServerMain;