Files
cherry-studio/src/renderer/src/services/PyodideService.ts
T

315 lines
8.7 KiB
TypeScript

import { loggerService } from '@logger'
import { uuid } from '@renderer/utils'
const logger = loggerService.withContext('PyodideService')
const SERVICE_CONFIG = {
WORKER: {
MAX_INIT_RETRY: 5, // 最大初始化重试次数
REQUEST_TIMEOUT: {
INIT: 30000, // 30 秒初始化超时
RUN: 60000 // 60 秒默认运行超时
}
}
}
// 定义结果类型接口
export interface PyodideOutput {
result: any
text: string | null
error: string | null
image?: string
}
export interface PyodideExecutionResult {
text: string
image?: string
}
/**
* Pyodide Web Worker 服务
*/
class PyodideService {
private static instance: PyodideService | null = null
private worker: Worker | null = null
private initPromise: Promise<void> | null = null
private initRetryCount: number = 0
private resolvers: Map<string, { resolve: (value: any) => void; reject: (error: Error) => void }> = new Map()
private constructor() {
// 单例模式
}
/**
* 获取 PyodideService 单例实例
*/
public static getInstance(): PyodideService {
if (!PyodideService.instance) {
PyodideService.instance = new PyodideService()
}
return PyodideService.instance
}
/**
* 初始化 Pyodide Worker
*/
private async initialize(): Promise<void> {
if (this.initPromise) {
return this.initPromise
}
if (this.worker) {
return Promise.resolve()
}
if (this.initRetryCount >= SERVICE_CONFIG.WORKER.MAX_INIT_RETRY) {
return Promise.reject(new Error('Pyodide worker initialization failed too many times'))
}
this.initPromise = new Promise<void>((resolve, reject) => {
// 动态导入 worker
import('../workers/pyodide.worker?worker')
.then((WorkerModule) => {
this.worker = new WorkerModule.default()
// 设置通用消息处理器
this.worker.onmessage = this.handleMessage.bind(this)
// 设置初始化超时
const timeout = setTimeout(() => {
this.worker = null
this.initPromise = null
this.initRetryCount++
reject(new Error('Pyodide initialization timeout'))
}, SERVICE_CONFIG.WORKER.REQUEST_TIMEOUT.INIT)
// 设置初始化处理器
const initHandler = (event: MessageEvent) => {
if (event.data?.type === 'initialized') {
clearTimeout(timeout)
this.worker?.removeEventListener('message', initHandler)
this.initRetryCount = 0
this.initPromise = null
resolve()
} else if (event.data?.type === 'init-error') {
clearTimeout(timeout)
this.worker?.removeEventListener('message', initHandler)
this.worker?.terminate()
this.worker = null
this.initPromise = null
this.initRetryCount++
reject(new Error(`Pyodide initialization failed: ${event.data.error}`))
}
}
this.worker.addEventListener('message', initHandler)
})
.catch((error) => {
this.worker = null
this.initPromise = null
this.initRetryCount++
reject(new Error(`Failed to load Pyodide worker: ${error instanceof Error ? error.message : String(error)}`))
})
})
return this.initPromise
}
/**
* 处理来自 Worker 的消息
*/
private handleMessage(event: MessageEvent): void {
const { type, error } = event.data
// 记录 Worker 错误消息
if (type === 'system-error') {
logger.error(error)
return
}
// 忽略初始化消息,已由专门的处理器处理
if (type === 'initialized' || type === 'init-error') {
return
}
const { id, output } = event.data
// 查找对应的解析器
const resolver = this.resolvers.get(id)
if (resolver) {
this.resolvers.delete(id)
resolver.resolve(output)
}
}
/**
* 执行Python脚本
* @param script 要执行的Python脚本
* @param context 可选的执行上下文
* @param timeout 超时时间(毫秒)
* @returns 格式化后的执行结果
*/
public async runScript(
script: string,
context: Record<string, any> = {},
timeout: number = SERVICE_CONFIG.WORKER.REQUEST_TIMEOUT.RUN
): Promise<PyodideExecutionResult> {
// 确保Pyodide已初始化
try {
await this.initialize()
} catch (error: unknown) {
logger.error('Pyodide initialization failed, cannot execute Python code', error as Error)
const text = `Initialization failed: ${error instanceof Error ? error.message : String(error)}`
return { text }
}
if (!this.worker) {
const text = 'Internal error: Pyodide worker is not initialized'
return { text }
}
try {
const output = await new Promise<PyodideOutput>((resolve, reject) => {
const id = uuid()
// 设置消息超时
const timeoutId = setTimeout(() => {
this.resolvers.delete(id)
reject(new Error('Python execution timed out'))
}, timeout)
this.resolvers.set(id, {
resolve: (output) => {
clearTimeout(timeoutId)
resolve(output)
},
reject: (error) => {
clearTimeout(timeoutId)
reject(error)
}
})
this.worker?.postMessage({
id,
python: script,
context
})
})
return { text: this.formatOutput(output), image: output.image }
} catch (error: unknown) {
const text = `Internal error: ${error instanceof Error ? error.message : String(error)}`
return { text }
}
}
/**
* 格式化 Pyodide 输出
*/
public formatOutput(output: PyodideOutput): string {
let displayText = ''
// 优先显示标准输出
if (output.text) {
displayText = output.text.trim()
}
// 如果有执行结果且无标准输出,显示结果
if (!displayText && output.result !== null && output.result !== undefined) {
if (typeof output.result === 'object' && output.result.__error__) {
displayText = `Result Error: ${output.result.details}`
} else {
try {
displayText =
typeof output.result === 'object' ? JSON.stringify(output.result, null, 2) : String(output.result)
} catch (e) {
displayText = `Result formatting failed: ${String(e)}`
}
}
}
// 如果有错误信息,附加显示
if (output.error) {
if (displayText) displayText += '\n\n'
displayText += output.error.trim()
}
// 如果没有任何输出,提供清晰提示
if (!displayText) {
displayText = 'Execution completed with no output.'
}
return displayText
}
/**
* 重置 Pyodide Worker
* 该方法会销毁当前的 Worker 并重新创建一个新的实例,
* 用于处理模块缓存或文件系统状态污染等罕见问题。
*/
public async resetWorker(): Promise<void> {
logger.verbose('Resetting Pyodide worker...')
this.terminate()
try {
await this.initialize()
logger.verbose('Pyodide worker has been reset successfully.')
} catch (error) {
logger.error('Failed to re-initialize Pyodide worker after reset.', error as Error)
throw error
}
}
/**
* 释放 Pyodide Worker 资源
*/
public terminate(): void {
if (this.worker) {
this.worker.terminate()
this.worker = null
this.initPromise = null
this.initRetryCount = 0
// 清理所有等待的请求
this.resolvers.forEach((resolver) => {
resolver.reject(new Error('Worker terminated'))
})
this.resolvers.clear()
}
}
}
// 创建并导出单例实例
export const pyodideService = PyodideService.getInstance()
// Set up IPC handler for main process requests
if (typeof window !== 'undefined' && window.electron?.ipcRenderer) {
interface PythonExecutionRequest {
id: string
script: string
context: Record<string, any>
timeout: number
}
interface PythonExecutionResponse {
id: string
result?: string
error?: string
}
window.electron.ipcRenderer.on('python-execution-request', async (_, request: PythonExecutionRequest) => {
try {
const { text } = await pyodideService.runScript(request.script, request.context, request.timeout)
const response: PythonExecutionResponse = {
id: request.id,
result: text
}
window.electron.ipcRenderer.send('python-execution-response', response)
} catch (error: unknown) {
const response: PythonExecutionResponse = {
id: request.id,
error: error instanceof Error ? error.message : String(error)
}
window.electron.ipcRenderer.send('python-execution-response', response)
}
})
}