feat: 实现 OpenCode 页面生命周期管理 XCOpenCodeWeb.exe
- 新增 electron/services/xcOpenCodeWebService.ts 服务管理模块 - 标签页打开时启动 XCOpenCodeWeb.exe,关闭时停止 - 使用 iframe 在 OpenCode 页面显示 Web 服务 (端口 3002) - 添加 bin 目录打包配置 - 添加 TypeScript 类型定义
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
|
||||
158
electron/services/xcOpenCodeWebService.ts
Normal file
158
electron/services/xcOpenCodeWebService.ts
Normal file
@@ -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<boolean> {
|
||||
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<void>((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<void>((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();
|
||||
@@ -122,10 +122,12 @@
|
||||
"dist-api/**/*",
|
||||
"shared/**/*",
|
||||
"tools/**/*",
|
||||
"bin/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"tools/**/*"
|
||||
"tools/**/*",
|
||||
"bin/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis"
|
||||
|
||||
@@ -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<string | null>(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 (
|
||||
<div className="max-w-4xl mx-auto w-full px-14 py-12 pb-40">
|
||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-8 flex items-center gap-2">
|
||||
OpenCode
|
||||
</h1>
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="max-w-4xl mx-auto w-full px-14 py-4 flex items-center gap-4">
|
||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||
OpenCode
|
||||
</h1>
|
||||
<span className={`px-2 py-1 text-xs rounded ${isRunning ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}`}>
|
||||
{isRunning ? '运行中' : '启动中...'}
|
||||
</span>
|
||||
{error && (
|
||||
<span className="text-red-500 text-sm">{error}</span>
|
||||
)}
|
||||
</div>
|
||||
{isRunning && (
|
||||
<iframe
|
||||
src={`http://localhost:${XCOPENCODEWEB_PORT}`}
|
||||
className="flex-1 w-full border-0"
|
||||
title="XCOpenCodeWeb"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
3
src/types/electron.d.ts
vendored
3
src/types/electron.d.ts
vendored
@@ -40,6 +40,9 @@ export interface ElectronAPI {
|
||||
}>
|
||||
opencodeStartServer: () => Promise<{ success: boolean; port?: number; error?: string }>
|
||||
opencodeStopServer: () => Promise<{ success: boolean; error?: string }>
|
||||
xcOpenCodeWebStart: () => Promise<{ success: boolean; error?: string }>
|
||||
xcOpenCodeWebStop: () => Promise<{ success: boolean; error?: string }>
|
||||
xcOpenCodeWebGetStatus: () => Promise<{ running: boolean; port: number }>
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
Reference in New Issue
Block a user