Files
WYF-koubo/electron/lib/python-spawn-util.ts
2026-06-19 18:45:55 +08:00

155 lines
4.5 KiB
TypeScript

/**
* Python 进程启动工具
* 统一处理中文路径问题
*/
import { spawn, SpawnOptions } from 'child_process';
import { normalizePath } from './path-util';
import * as path from 'path';
import * as os from 'os';
import logger from '../mapi/log/main';
export interface PythonSpawnOptions extends SpawnOptions {
/**
* 是否转换所有路径参数(默认 true)
*/
normalizePaths?: boolean;
}
/**
* 启动 Python 进程,自动处理中文路径
* @param pythonPath Python 可执行文件路径
* @param args Python 脚本和参数
* @param options spawn 选项
* @returns ChildProcess
*/
export function spawnPython(
pythonPath: string,
args: string[],
options?: PythonSpawnOptions
) {
const platform = os.platform();
const shouldNormalize = options?.normalizePaths !== false && platform === 'win32';
// Windows 平台:转换所有路径
if (shouldNormalize) {
// 转换 Python 可执行文件路径
const normalizedPythonPath = normalizePath(pythonPath);
// 转换参数中的路径
const normalizedArgs = args.map((arg, index) => {
// 检查是否是文件路径(包含路径分隔符或扩展名)
if (arg.includes('/') || arg.includes('\\') ||
(arg.includes('.') && !arg.startsWith('-'))) {
const normalized = normalizePath(arg);
if (normalized !== arg) {
logger.info('[PythonSpawn] 路径转换', {
index,
original: arg.substring(0, 100),
normalized: normalized.substring(0, 100)
});
}
return normalized;
}
return arg;
});
logger.info('[PythonSpawn] 启动 Python 进程', {
pythonPath: normalizedPythonPath.substring(0, 100),
argsCount: normalizedArgs.length,
cwd: options?.cwd
});
return spawn(normalizedPythonPath, normalizedArgs, options);
}
// macOS/Linux:直接启动
return spawn(pythonPath, args, options);
}
/**
* 执行 Python 脚本并返回 Promise
* @param pythonPath Python 可执行文件路径
* @param args Python 脚本和参数
* @param options spawn 选项
* @returns Promise<{stdout: string, stderr: string, exitCode: number}>
*/
export function execPython(
pythonPath: string,
args: string[],
options?: PythonSpawnOptions
): Promise<{stdout: string, stderr: string, exitCode: number}> {
return new Promise((resolve, reject) => {
const child = spawnPython(pythonPath, args, options);
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (exitCode) => {
resolve({
stdout,
stderr,
exitCode: exitCode || 0
});
});
child.on('error', (error) => {
reject(error);
});
});
}
/**
* 创建临时配置文件(确保在纯英文路径)
* @param config 配置对象
* @param prefix 文件名前缀
* @returns 配置文件路径
*/
export function createTempConfig(config: any, prefix: string = 'config'): string {
const fs = require('fs');
// 使用系统临时目录(通常是纯英文路径)
const tempDir = os.tmpdir();
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
const configPath = path.join(tempDir, `${prefix}_${timestamp}_${random}.json`);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
logger.info('[PythonSpawn] 创建临时配置文件', {
path: configPath,
size: JSON.stringify(config).length
});
return configPath;
}
/**
* 清理临时配置文件
* @param configPath 配置文件路径
*/
export function cleanupTempConfig(configPath: string): void {
try {
const fs = require('fs');
if (fs.existsSync(configPath)) {
fs.unlinkSync(configPath);
logger.info('[PythonSpawn] 清理临时配置文件', { path: configPath });
}
} catch (error) {
logger.warn('[PythonSpawn] 清理临时配置文件失败', {
path: configPath,
error: error instanceof Error ? error.message : String(error)
});
}
}