import {Files} from "../mapi/file/main"; import {Log} from "../mapi/log/main"; import {SendType, ServerApiType, ServerFunctionDataType, ServerInfo} from "../mapi/server/type"; import {AigcServerUtil} from "./util"; import path from "path"; import {getFFmpegExecutablePath, resourceExists} from "../lib/resource-path"; type LauncherResultType = { result: { [key: string]: any; }; endTime: number | null; }; const normalizeEasyServerFunctions = (functions: Record = {}) => { const normalized = { ...functions, }; if (normalized.speechRecognition && !normalized.asr) { normalized.asr = normalized.speechRecognition; } return normalized; }; export const EasyServer = function (config: any) { const me = this; let controller: any = null; let controllerWatching = { id: null as string | null, launcherResult: null as LauncherResultType | null, resolve: null as ((value: any) => void) | null, reject: null as ((reason?: any) => void) | null, promiseResolved: false, }; const resetControllerWatching = () => { controllerWatching.id = null; controllerWatching.launcherResult = null; controllerWatching.resolve = null; controllerWatching.reject = null; controllerWatching.promiseResolved = false; }; const settleControllerWatching = async (type: "resolve" | "reject", reason?: any, stopController = false) => { if (controllerWatching.promiseResolved) { return; } controllerWatching.promiseResolved = true; const resolve = controllerWatching.resolve; const reject = controllerWatching.reject; if (stopController && controller) { const runningController = controller; controller = null; try { await runningController.stop(); } catch (e) { Log.warn("easyServer.settle.stop.error", e); } } if (type === "resolve") { resolve?.(undefined); } else { reject?.(reason); } }; this.serverConfig = { ...(config || {}), easyServer: { ...(config?.easyServer || {}), functions: normalizeEasyServerFunctions(config?.easyServer?.functions || {}), }, } as { easyServer: { entry: string; entryArgs: string[]; envs: string[]; content: string; functions: { [key: string]: { content?: string; param?: any[]; }; }; }; }; this.isRunning = false; this.ServerApi = null as ServerApiType | null; this.ServerInfo = null as ServerInfo | null; this.serverRuntime = { startTime: 0, }; this.send = function (type: SendType, data: any) { this.ServerApi.event.sendChannel(this.ServerInfo.eventChannelName, {type, data}); }; this.init = async function () { }; this.config = async function () { return { code: 0, msg: "ok", data: { httpUrl: null, content: this.serverConfig.easyServer.content || "", functions: this.serverConfig.easyServer.functions || {}, }, }; }; this.start = async function () { // console.log('start', this.ServerInfo) this.serverRuntime.startTime = Date.now(); this.send("starting", this.ServerInfo); }; this.ping = async function (): Promise { // console.log('ping', this.ServerInfo) return this.serverRuntime.startTime > 0; }; this.stop = async function () { // console.log('stop', this.ServerInfo) this.send("stopping", this.ServerInfo); this.serverRuntime.startTime = 0; if (controller) { try { await controller.stop(); } catch (e) { Log.warn("easyServer.stop.error", e); } controller = null; } this.send("stopped", this.ServerInfo); this.send("success", this.ServerInfo); }; this.cancel = async function () { if (controller) { await controller.stop(); controller = null; } }; this._controllerRunIfNeeded = async function ( configJsonPath: string | null, option: { timeout: number; } ) { if (!controller) { const _localPath = this.ServerInfo?.localPath || ''; Log.info("[EasyServer] ServerInfo.localPath", _localPath); let command = []; command.push(this.serverConfig.easyServer.entry); if (this.serverConfig.easyServer.entryArgs) { command = command.concat(this.serverConfig.easyServer.entryArgs); } for (let i = 0; i < command.length; i++) { command[i] = command[i].replace("${CONFIG}", `"${configJsonPath}"`); command[i] = command[i].replace("${ROOT}", _localPath); } Log.info("[EasyServer] command", JSON.stringify(command)); Log.info("[EasyServer] localPath", this.ServerInfo.localPath); const envMap = {}; Log.info('EasyServer.config', JSON.stringify(this.serverConfig.easyServer)); if (this.serverConfig.easyServer.entry === "launcher") { const systemEnv = await this.ServerApi.env(); // console.log('EasyServer.systemEnv', systemEnv) for (const k in systemEnv) { envMap[k] = systemEnv[k]; } } // 🔧 修复:添加 FFmpeg 目录到 PATH,让 Python 脚本能找到 ffmpeg const pathDirs = [ `${this.ServerInfo.localPath}`, `${this.ServerInfo.localPath}/binary`, ]; const ffmpegPath = getFFmpegExecutablePath(); if (resourceExists(ffmpegPath)) { const ffmpegDir = path.dirname(ffmpegPath); pathDirs.push(ffmpegDir); Log.info(`[EasyServer] ✓ 已添加 FFmpeg 目录到 PATH: ${ffmpegDir}`); } envMap["PATH"] = this.ServerApi.getPathEnv(pathDirs); envMap["PYTHONIOENCODING"] = "utf-8"; envMap["AIGCPANEL_SERVER_PLACEHOLDER_CONFIG"] = configJsonPath; envMap["AIGCPANEL_SERVER_PLACEHOLDER_ROOT"] = this.ServerInfo.localPath; if (this.serverConfig.easyServer.envs) { for (const e of this.serverConfig.easyServer.envs) { let pcs = e.split("="); const key = pcs.shift(); envMap[key] = pcs.join("="); } } for (const k in envMap) { envMap[k] = envMap[k].replace("${CONFIG}", `"${configJsonPath}"`); envMap[k] = envMap[k].replace("${ROOT}", this.ServerInfo.localPath); } const hasMoreQueue = async () => { const queueRoot = this.ServerInfo.localPath + `/aigcpanel-queue/`; await Files.mkdir(queueRoot); const files = await Files.list(queueRoot); const validQueueFiles = files.filter(f => f.name.match(/\.queue\.json$/)); if (validQueueFiles.length > 0) { const configJson = await Files.temp("json"); await Files.copy(validQueueFiles[0].pathname, configJson); await Files.deletes(validQueueFiles[0].pathname); await this._controllerRunIfNeeded(configJson, option); } }; let timer = null; if (option.timeout > 0) { timer = setTimeout(async () => { this.ServerApi.file.appendText(this.ServerInfo.logFile, "timeout", {isDataPath: true}); await settleControllerWatching("reject", new Error("timeout"), true); }, option.timeout * 1000); } let buffer = ""; controller = await this.ServerApi.app.spawnShell(command, { env: envMap, cwd: this.ServerInfo.localPath, stdout: _data => { // console.log('easyServer.stdout', _data) buffer += _data; // check if has \n and process the buffer let lines = buffer.split("\n"); buffer = lines.pop() || ""; this.ServerApi.file.appendText(this.ServerInfo.logFile, _data, {isDataPath: true}); // 鍙戦€佹棩蹇楀埌鍓嶇UI if (controllerWatching.id) { this.send("taskLog", { id: controllerWatching.id, type: "stdout", data: _data, timestamp: Date.now() }); } const result = this.ServerApi.extractResultFromLogs(controllerWatching.id, lines.join("\n") + "\n"); if (result) { if (controllerWatching.launcherResult) { controllerWatching.launcherResult.result = Object.assign(controllerWatching.launcherResult.result, result); } if (controllerWatching.id) { this.send("taskResult", {id: controllerWatching.id, result}); } } if (controllerWatching.launcherResult) { controllerWatching.launcherResult.result.error = AigcServerUtil.errorDetect(_data) || controllerWatching.launcherResult.result.error; // 馃敡 淇锛氬彧鏈夊湪End=true涓斿凡缁忔湁鏈夋晥缁撴灉(url/records/error)鏃舵墠resolve if (controllerWatching.launcherResult.result && controllerWatching.launcherResult.result['End']) { const hasValidResult = controllerWatching.launcherResult.result.url || controllerWatching.launcherResult.result.records || controllerWatching.launcherResult.result.error; if (hasValidResult) { settleControllerWatching("resolve", undefined, true).then(); } } } }, stderr: _data => { // console.log('easyServer.stderr', _data) this.ServerApi.file.appendText(this.ServerInfo.logFile, _data, {isDataPath: true}); // 鍙戦€佹棩蹇楀埌鍓嶇UI if (controllerWatching.id) { this.send("taskLog", { id: controllerWatching.id, type: "stderr", data: _data, timestamp: Date.now() }); } if (controllerWatching.launcherResult) { controllerWatching.launcherResult.result.error = AigcServerUtil.errorDetect(_data) || controllerWatching.launcherResult.result.error; } }, success: _data => { // console.log('easyServer.success', _data) clearTimeout(timer); controller = null; hasMoreQueue().then() settleControllerWatching("resolve").then(); }, error: (_data, code) => { // console.log('easyServer.error', {_data, controllerWatching}) this.ServerApi.file.appendText(this.ServerInfo.logFile, `exit code ${code}`, {isDataPath: true}); clearTimeout(timer); controller = null; hasMoreQueue().then() settleControllerWatching("reject").then(); }, }) } else if (configJsonPath) { const queueName = `${Date.now()}.queue.json`; const queuePath = this.ServerInfo.localPath + `/aigcpanel-queue/${queueName}`; await Files.copy(configJsonPath, queuePath); this.ServerApi.file.appendText( this.ServerInfo.logFile, `Another task is running, queued at ${queueName}`, {isDataPath: true} ); } }; this._callFunc = async function ( data: ServerFunctionDataType, configCalculator: (data: ServerFunctionDataType) => Promise, resultDataCalculator: (data: ServerFunctionDataType, launcherResult: LauncherResultType) => Promise, option: { timeout: number; } ) { option = Object.assign( { timeout: 24 * 3600, }, option ); const resultData = { // success, retry type: "success", start: 0, end: 0, data: {}, }; if (this.isRunning) { resultData.type = "retry"; return { code: 0, msg: "ok", data: resultData, }; } this.isRunning = true; resultData.start = Date.now(); let configJsonPath = null; try { this.send("taskRunning", {id: data.id}); const configData = await configCalculator(data); configData.setting = this.ServerInfo.setting; configJsonPath = await this.ServerApi.launcherPrepareConfigJson(configData); // console.log('EasyServer.envMap', envMap) const launcherResult: LauncherResultType = { result: {}, endTime: null, }; // console.log('easyServer.start', JSON.stringify({command, envMap, configData})) await (async () => { return new Promise((resolve, reject) => { controllerWatching.id = data.id; controllerWatching.launcherResult = launcherResult; controllerWatching.resolve = resolve; controllerWatching.reject = reject; controllerWatching.promiseResolved = false; me._controllerRunIfNeeded(configJsonPath, option); }); })(); resultData.end = Date.now(); resultData.data = await resultDataCalculator(data, launcherResult); // console.log('easyServer.end', launcherResult) await Files.deletes(configJsonPath); return { code: 0, msg: "ok", data: resultData, }; } catch (e) { throw e; } finally { resetControllerWatching(); this.isRunning = false; } }; this.soundTts = async function (data: ServerFunctionDataType) { // console.log('soundTts', {data, serverInfo: this.ServerInfo}) return this._callFunc( data, async (data: ServerFunctionDataType) => { return { id: data.id, mode: "local", modelConfig: { type: "soundTts", param: data.param, text: data.text, }, }; }, async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => { // 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒 if (!("url" in launcherResult.result)) { // 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿? if (launcherResult.result.End === true) { return launcherResult.result; } if (launcherResult.result.error) { throw launcherResult.result.error; } throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織"; } return { url: launcherResult.result.url, }; } ); }; this.soundClone = async function (data: ServerFunctionDataType) { // console.log('soundClone', {data, serverInfo: this.ServerInfo}) return this._callFunc( data, async (data: ServerFunctionDataType) => { return { id: data.id, mode: "local", modelConfig: { type: "soundClone", param: data.param, text: data.text, promptAudio: data.promptAudio, promptText: data.promptText, }, }; }, async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => { // 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒 if (!("url" in launcherResult.result)) { // 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿? if (launcherResult.result.End === true) { return launcherResult.result; } if (launcherResult.result.error) { throw launcherResult.result.error; } throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織"; } return { url: launcherResult.result.url, }; } ); }; this.videoGen = async function (data: ServerFunctionDataType) { // console.log('videoGen', JSON.stringify({data, serverInfo: this.ServerInfo})) return this._callFunc( data, async (data: ServerFunctionDataType) => { return { id: data.id, mode: "local", modelConfig: { type: "videoGen", param: data.param, video: data.video, audio: data.audio, }, }; }, async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => { // 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒 if (!("url" in launcherResult.result)) { // 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿? if (launcherResult.result.End === true) { return launcherResult.result; } if (launcherResult.result.error) { throw launcherResult.result.error; } throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織"; } return { url: launcherResult.result.url, }; } ); }; this.asr = async function (data: ServerFunctionDataType) { // console.log('videoGen', JSON.stringify({data, serverInfo: this.ServerInfo})) return this._callFunc( data, async (data: ServerFunctionDataType) => { return { id: data.id, mode: "local", modelConfig: { type: 'asr', audio: data.audio, param: data.param, }, }; }, async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => { // 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒 if (!("records" in launcherResult.result)) { // 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿? if (launcherResult.result.End === true) { return launcherResult.result; } if (launcherResult.result.error) { throw launcherResult.result.error; } throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織"; } return { records: launcherResult.result.records, }; } ); }; this.textToImage = async function (data: ServerFunctionDataType) { // console.log('textToImage', {data, serverInfo: this.ServerInfo}) return this._callFunc( data, async (data: ServerFunctionDataType) => { return { id: data.id, mode: "local", modelConfig: { type: "textToImage", prompt: data.prompt, param: data.param, }, }; }, async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => { // 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒 if (!("url" in launcherResult.result)) { // 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿? if (launcherResult.result.End === true) { return launcherResult.result; } if (launcherResult.result.error) { throw launcherResult.result.error; } throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織"; } return { url: launcherResult.result.url, }; } ); }; this.imageToImage = async function (data: ServerFunctionDataType) { // console.log('imageToImage', {data, serverInfo: this.ServerInfo}) return this._callFunc( data, async (data: ServerFunctionDataType) => { return { id: data.id, mode: "local", modelConfig: { type: "imageToImage", image: data.image, prompt: data.prompt, param: data.param, }, }; }, async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => { // 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒 if (!("url" in launcherResult.result)) { // 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿? if (launcherResult.result.End === true) { return launcherResult.result; } if (launcherResult.result.error) { throw launcherResult.result.error; } throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織"; } return { url: launcherResult.result.url, }; } ); }; };