diff --git a/.gitignore b/.gitignore index 03fcb72..82d517b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ dist-electron/ # XCOpenCodeWeb (来自独立仓库 https://github.com/anomalyco/XCOpenCodeWeb) remote/xcopencodeweb/XCOpenCodeWeb.exe service/xcopencodeweb/XCOpenCodeWeb.exe +bin/XCOpenCodeWeb.exe # Tools output tools/tongyi/ppt_output/ diff --git a/electron/main.ts b/electron/main.ts index ccedfe7..5d7a6dd 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -6,6 +6,7 @@ import log from 'electron-log'; import { generatePdf } from './services/pdfGenerator'; import { selectHtmlFile } from './services/htmlImport'; import { opencodeService } from './services/opencodeService'; +import { xcOpenCodeWebService } from './services/xcOpenCodeWebService'; import { electronState } from './state'; log.initialize(); @@ -361,6 +362,18 @@ ipcMain.handle('opencode-stop-server', async () => { return await opencodeService.stop(); }); +ipcMain.handle('xc-opencode-web-get-status', () => { + return xcOpenCodeWebService.getStatus(); +}); + +ipcMain.handle('xc-opencode-web-start', async () => { + return await xcOpenCodeWebService.start(); +}); + +ipcMain.handle('xc-opencode-web-stop', async () => { + return await xcOpenCodeWebService.stop(); +}); + async function startServer() { if (electronState.isDevelopment()) { log.info('In dev mode, assuming external servers are running.'); diff --git a/electron/preload.ts b/electron/preload.ts index 259651d..687b51b 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -41,4 +41,7 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('remote-download-file', id, serverHost, port, fileName, remotePath, localPath, password), opencodeStartServer: () => ipcRenderer.invoke('opencode-start-server'), opencodeStopServer: () => ipcRenderer.invoke('opencode-stop-server'), + xcOpenCodeWebStart: () => ipcRenderer.invoke('xc-opencode-web-start'), + xcOpenCodeWebStop: () => ipcRenderer.invoke('xc-opencode-web-stop'), + xcOpenCodeWebGetStatus: () => ipcRenderer.invoke('xc-opencode-web-get-status'), }) diff --git a/electron/services/xcOpenCodeWebService.ts b/electron/services/xcOpenCodeWebService.ts new file mode 100644 index 0000000..26f0a12 --- /dev/null +++ b/electron/services/xcOpenCodeWebService.ts @@ -0,0 +1,158 @@ +import { spawn, ChildProcess } from 'child_process'; +import { app } from 'electron'; +import path from 'path'; +import log from 'electron-log'; + +const XCOPENCODEWEB_PORT = 3002; +const HEALTH_CHECK_INTERVAL = 10000; +const HEALTH_CHECK_TIMEOUT = 2000; + +class XCOpenCodeWebService { + private process: ChildProcess | null = null; + private healthCheckTimer: NodeJS.Timeout | null = null; + private _isRunning = false; + + get port(): number { + return XCOPENCODEWEB_PORT; + } + + isRunning(): boolean { + return this._isRunning; + } + + getStatus(): { running: boolean; port: number } { + return { + running: this._isRunning, + port: this.port, + }; + } + + private getExePath(): string { + const exeName = 'XCOpenCodeWeb.exe'; + const isPackaged = app.isPackaged; + + let basePath: string; + if (isPackaged) { + basePath = path.dirname(app.getPath('exe')); + } else { + basePath = process.cwd(); + } + + return path.join(basePath, 'bin', exeName); + } + + private async checkHealth(): Promise { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT); + + const response = await fetch(`http://127.0.0.1:${this.port}`, { + signal: controller.signal, + }); + + clearTimeout(timeoutId); + return response.ok || response.status === 401; + } catch (error) { + log.warn('[XCOpenCodeWebService] Health check failed:', error); + return false; + } + } + + private startHealthCheck(): void { + if (this.healthCheckTimer) { + clearInterval(this.healthCheckTimer); + } + + this.healthCheckTimer = setInterval(async () => { + const isHealthy = await this.checkHealth(); + if (!isHealthy && this._isRunning) { + log.warn('[XCOpenCodeWebService] Service not responding'); + } + }, HEALTH_CHECK_INTERVAL); + } + + async start(): Promise<{ success: boolean; error?: string }> { + if (this._isRunning && this.process) { + log.info('[XCOpenCodeWebService] Already running'); + return { success: true }; + } + + try { + const exePath = this.getExePath(); + log.info(`[XCOpenCodeWebService] Starting from: ${exePath}`); + + this.process = spawn(exePath, [], { + stdio: 'pipe', + shell: true, + detached: false, + }); + + this.process.stdout?.on('data', (data) => { + log.info(`[XCOpenCodeWeb] ${data}`); + }); + + this.process.stderr?.on('data', (data) => { + log.error(`[XCOpenCodeWeb error] ${data}`); + }); + + this.process.on('error', (err) => { + log.error('[XCOpenCodeWebService] Process error:', err); + this._isRunning = false; + this.process = null; + }); + + this.process.on('exit', (code) => { + log.info(`[XCOpenCodeWebService] Process exited with code ${code}`); + this._isRunning = false; + this.process = null; + }); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + this._isRunning = true; + this.startHealthCheck(); + + log.info(`[XCOpenCodeWebService] Started successfully on port ${this.port}`); + return { success: true }; + } catch (error: any) { + log.error('[XCOpenCodeWebService] Failed to start:', error); + this._isRunning = false; + return { success: false, error: error.message }; + } + } + + async stop(): Promise<{ success: boolean; error?: string }> { + if (this.healthCheckTimer) { + clearInterval(this.healthCheckTimer); + this.healthCheckTimer = null; + } + + if (!this.process) { + log.info('[XCOpenCodeWebService] Not running'); + return { success: true }; + } + + try { + log.info('[XCOpenCodeWebService] Stopping...'); + + this.process.kill('SIGTERM'); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (this.process && !this.process.killed) { + this.process.kill('SIGKILL'); + } + + this.process = null; + this._isRunning = false; + + log.info('[XCOpenCodeWebService] Stopped'); + return { success: true }; + } catch (error: any) { + log.error('[XCOpenCodeWebService] Failed to stop:', error); + return { success: false, error: error.message }; + } + } +} + +export const xcOpenCodeWebService = new XCOpenCodeWebService(); diff --git a/package.json b/package.json index 2f60ff8..04d07db 100644 --- a/package.json +++ b/package.json @@ -122,10 +122,12 @@ "dist-api/**/*", "shared/**/*", "tools/**/*", + "bin/**/*", "package.json" ], "asarUnpack": [ - "tools/**/*" + "tools/**/*", + "bin/**/*" ], "win": { "target": "nsis" diff --git a/src/modules/opencode/OpenCodePage.tsx b/src/modules/opencode/OpenCodePage.tsx index c623740..4bf330a 100644 --- a/src/modules/opencode/OpenCodePage.tsx +++ b/src/modules/opencode/OpenCodePage.tsx @@ -1,11 +1,61 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' + +const XCOPENCODEWEB_PORT = 3002 export const OpenCodePage: React.FC = () => { + const [isRunning, setIsRunning] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + let mounted = true + + const startService = async () => { + try { + const result = await window.electronAPI.xcOpenCodeWebStart() + if (mounted) { + if (result.success) { + setIsRunning(true) + setError(null) + } else { + setError(result.error || '启动失败') + } + } + } catch (err) { + if (mounted) { + setError(err instanceof Error ? err.message : '启动失败') + } + } + } + + startService() + + return () => { + mounted = false + window.electronAPI.xcOpenCodeWebStop() + setIsRunning(false) + } + }, []) + return ( -
-

- OpenCode -

+
+
+

+ OpenCode +

+ + {isRunning ? '运行中' : '启动中...'} + + {error && ( + {error} + )} +
+ {isRunning && ( +