Files
WYF-koubo/electron/mapi/compshare/main.ts
T
2026-06-19 18:45:55 +08:00

350 lines
11 KiB
TypeScript

import { ipcMain } from 'electron';
import logger from '../log/main';
import axios from 'axios';
import crypto from 'crypto';
import FormData from 'form-data';
import fs from 'fs';
import path from 'path';
import { spawn } from 'child_process';
import { getPythonExecutablePath, resourceExists } from '../../lib/resource-path';
import { buildRuntimeProcessEnv } from '../shell';
const COMPSHARE_API_BASE = 'https://api.compshare.cn';
let autoShutdownTimer: NodeJS.Timeout | null = null;
let lastTaskTime: number = 0;
const AUTO_SHUTDOWN_DELAY = 10 * 60 * 1000;
interface SelfServerConfig {
region: string;
zone: string;
projectId: string;
uhostId: string;
apiUrl: string;
}
let currentServerConfig: SelfServerConfig | null = null;
let apiKey: { publicKey: string; privateKey: string } | null = null;
export function setCompShareApiKey(publicKey: string, privateKey: string) {
apiKey = { publicKey, privateKey };
}
function generateSignature(params: Record<string, string>): string {
if (!apiKey) {
throw new Error('API密钥未配置');
}
const sortedKeys = Object.keys(params).sort();
const paramString = sortedKeys.map(key => `${key}${params[key]}`).join('');
const stringToSign = paramString + apiKey.privateKey;
return crypto.createHash('sha1').update(stringToSign).digest('hex');
}
function buildSignedParams(action: string, extraParams: Record<string, string> = {}): URLSearchParams {
if (!apiKey) {
throw new Error('API密钥未配置');
}
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = Math.random().toString(36).substring(2);
const params: Record<string, string> = {
Action: action,
PublicKey: apiKey.publicKey,
Timestamp: timestamp,
Nonce: nonce,
...extraParams
};
const signature = generateSignature(params);
params.Signature = signature;
const urlParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
urlParams.append(key, value);
}
return urlParams;
}
export function getSelfServerConfig(): SelfServerConfig | null {
return currentServerConfig;
}
export function setSelfServerConfig(config: SelfServerConfig) {
currentServerConfig = config;
}
export async function startCompShareInstance(config: {
region: string;
zone: string;
projectId: string;
uhostId: string;
}): Promise<{ success: boolean; message?: string }> {
try {
const extraParams: Record<string, string> = {
Region: config.region,
Zone: config.zone,
UHostId: config.uhostId,
WithoutGpu: 'false'
};
if (config.projectId) {
extraParams.ProjectId = config.projectId;
}
const params = buildSignedParams('StartCompShareInstance', extraParams);
const response = await axios.get(`${COMPSHARE_API_BASE}/?${params.toString()}`);
const data = response.data;
if (data.RetCode === 0) {
logger.info('[CompShare] 实例启动成功', { uhostId: config.uhostId });
return { success: true, message: '实例启动成功' };
} else {
logger.error('[CompShare] 实例启动失败', data);
return { success: false, message: `启动失败: ${data.Message || '未知错误'}` };
}
} catch (error: any) {
logger.error('[CompShare] 启动实例异常', error);
return { success: false, message: error.message };
}
}
export async function stopCompShareInstance(config: {
region: string;
zone: string;
projectId: string;
uhostId: string;
}): Promise<{ success: boolean; message?: string }> {
try {
const extraParams: Record<string, string> = {
Region: config.region,
Zone: config.zone,
UHostId: config.uhostId
};
if (config.projectId) {
extraParams.ProjectId = config.projectId;
}
const params = buildSignedParams('StopCompShareInstance', extraParams);
const response = await axios.get(`${COMPSHARE_API_BASE}/?${params.toString()}`);
const data = response.data;
if (data.RetCode === 0) {
logger.info('[CompShare] 实例关闭成功', { uhostId: config.uhostId });
return { success: true, message: '实例关闭成功' };
} else {
logger.error('[CompShare] 实例关闭失败', data);
return { success: false, message: `关闭失败: ${data.Message || '未知错误'}` };
}
} catch (error: any) {
logger.error('[CompShare] 关闭实例异常', error);
return { success: false, message: error.message };
}
}
export async function describeCompShareInstance(config: {
region?: string;
zone?: string;
projectId?: string;
uhostIds?: string[];
}): Promise<{ success: boolean; instances?: any[]; message?: string }> {
try {
const extraParams: Record<string, string> = {};
if (config.region) extraParams.Region = config.region;
if (config.zone) extraParams.Zone = config.zone;
if (config.projectId) extraParams.ProjectId = config.projectId;
if (config.uhostIds && config.uhostIds.length > 0) {
config.uhostIds.forEach((id, index) => {
extraParams[`UHostIds.${index}`] = id;
});
}
const params = buildSignedParams('DescribeCompShareInstance', extraParams);
const response = await axios.get(`${COMPSHARE_API_BASE}/?${params.toString()}`);
const data = response.data;
if (data.RetCode === 0) {
return { success: true, instances: data.UHostSet || [] };
} else {
return { success: false, message: `查询失败: ${data.Message || '未知错误'}` };
}
} catch (error: any) {
logger.error('[CompShare] 查询实例异常', error);
return { success: false, message: error.message };
}
}
export async function processDigitalHuman(params: {
apiUrl: string;
audioFile: string;
videoFile: string;
}): Promise<{ success: boolean; videoPath?: string; message?: string }> {
const { apiUrl, audioFile, videoFile } = params;
logger.info('[CompShare] 开始数字人处理', { apiUrl, audioFile, videoFile });
try {
// 使用 Python 脚本调用 Gradio API
let scriptPath: string = '';
// 尝试多个可能的路径
const possiblePaths = [
path.join(process.resourcesPath || '', 'scripts', 'digital_human_process.py'),
path.join(process.env.APP_ROOT || '', 'scripts', 'digital_human_process.py'),
path.join(process.cwd(), 'scripts', 'digital_human_process.py'),
path.join(process.cwd(), '..', 'scripts', 'digital_human_process.py'),
];
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
scriptPath = p;
break;
}
}
if (!scriptPath) {
// 如果都找不到,使用 cwd 版本
scriptPath = path.join(process.cwd(), 'scripts', 'digital_human_process.py');
}
logger.info('[CompShare] Python 脚本路径', { scriptPath });
// ✅ 关键修复:使用内嵌的 Python,避免依赖系统安装
let pythonExe = getPythonExecutablePath();
if (!resourceExists(pythonExe)) {
logger.warn(`[CompShare] 内嵌Python不存在: ${pythonExe},尝试系统python`);
pythonExe = 'python'; // 回退到系统 python
}
logger.info('[CompShare] 使用Python路径', { pythonExe });
return new Promise((resolve) => {
const pythonProcess = spawn(pythonExe, [scriptPath, apiUrl, audioFile, videoFile], {
cwd: process.cwd(),
env: buildRuntimeProcessEnv(),
windowsHide: true
});
let stdout = '';
let stderr = '';
pythonProcess.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
});
pythonProcess.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});
pythonProcess.on('close', (code: number) => {
logger.info('[CompShare] Python 脚本执行完成', { code, stdout: stdout.substring(0, 500) });
if (code === 0 && stdout.trim()) {
try {
const stdoutLines = stdout
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean);
const jsonLine = [...stdoutLines].reverse().find(line => line.startsWith('{') && line.endsWith('}'));
if (!jsonLine) {
resolve({ success: false, message: `解析结果失败: ${stdout.substring(0, 200)}` });
return;
}
const result = JSON.parse(jsonLine);
if (result.success) {
resolve({ success: true, videoPath: result.videoPath });
} else {
resolve({ success: false, message: result.error || '处理失败' });
}
} catch (e) {
resolve({ success: false, message: `解析结果失败: ${stdout.substring(0, 200)}` });
}
} else {
resolve({ success: false, message: stderr || `进程退出码: ${code}` });
}
});
pythonProcess.on('error', (err: Error) => {
logger.error('[CompShare] Python 进程错误', err);
resolve({ success: false, message: err.message });
});
// 10分钟超时
setTimeout(() => {
pythonProcess.kill();
resolve({ success: false, message: '处理超时' });
}, 600000);
});
} catch (error: any) {
const errorDetail = error.message || '未知错误';
logger.error('[CompShare] 数字人处理失败', errorDetail);
return { success: false, message: errorDetail };
}
}
export function updateLastTaskTime() {
lastTaskTime = Date.now();
if (autoShutdownTimer) {
clearTimeout(autoShutdownTimer);
}
autoShutdownTimer = setTimeout(async () => {
const now = Date.now();
if (now - lastTaskTime >= AUTO_SHUTDOWN_DELAY && currentServerConfig) {
logger.info('[CompShare] 10分钟无任务,自动关闭实例');
await stopCompShareInstance(currentServerConfig);
autoShutdownTimer = null;
}
}, AUTO_SHUTDOWN_DELAY);
}
export function registerCompShareHandlers() {
ipcMain.handle('compshare:setApiKey', async (event, params: { publicKey: string; privateKey: string }) => {
logger.info('[CompShare:IPC] 设置API密钥');
setCompShareApiKey(params.publicKey, params.privateKey);
return { success: true };
});
ipcMain.handle('compshare:startInstance', async (event, params) => {
logger.info('[CompShare:IPC] 启动实例', params);
const result = await startCompShareInstance(params);
if (result.success) {
setSelfServerConfig({
region: params.region,
zone: params.zone,
projectId: params.projectId,
uhostId: params.uhostId,
apiUrl: ''
});
updateLastTaskTime();
}
return result;
});
ipcMain.handle('compshare:stopInstance', async (event, params) => {
logger.info('[CompShare:IPC] 关闭实例', params);
return await stopCompShareInstance(params);
});
ipcMain.handle('compshare:describeInstance', async (event, params) => {
logger.info('[CompShare:IPC] 查询实例', params);
return await describeCompShareInstance(params);
});
ipcMain.handle('compshare:digitalHumanProcess', async (event, params) => {
logger.info('[CompShare:IPC] 数字人处理', { apiUrl: params.apiUrl });
const result = await processDigitalHuman(params);
if (result.success) {
updateLastTaskTime();
}
return result;
});
logger.info('[CompShare:IPC] Handlers已注册');
}