From c83f23c319e7a97cdcdf2a4b01d131ac1b897265 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 18 Mar 2026 16:17:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20SDD=20(=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E9=A9=B1=E5=8A=A8=E5=BC=80=E5=8F=91)=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.ts | 21 +++- electron/preload.ts | 4 + electron/services/sddService.ts | 175 ++++++++++++++++++++++++++++++++ shared/modules/sdd/index.ts | 12 +++ src/modules/sdd/SDDPage.tsx | 86 ++++++++++++++++ src/modules/sdd/index.tsx | 11 ++ src/types/electron.d.ts | 4 + 7 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 electron/services/sddService.ts create mode 100644 shared/modules/sdd/index.ts create mode 100644 src/modules/sdd/SDDPage.tsx create mode 100644 src/modules/sdd/index.tsx diff --git a/electron/main.ts b/electron/main.ts index 0a49e3a..3b4324b 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -7,6 +7,7 @@ import { generatePdf } from './services/pdfGenerator'; import { selectHtmlFile } from './services/htmlImport'; import { opencodeService } from './services/opencodeService'; import { xcOpenCodeWebService } from './services/xcOpenCodeWebService'; +import { sddService } from './services/sddService'; import { electronState } from './state'; log.initialize(); @@ -387,6 +388,22 @@ ipcMain.handle('xc-opencode-web-stop', async () => { return await xcOpenCodeWebService.stop(); }); +ipcMain.handle('sdd-get-status', () => { + return sddService.getStatus(); +}); + +ipcMain.handle('sdd-get-port', () => { + return { port: sddService.port }; +}); + +ipcMain.handle('sdd-start', async () => { + return await sddService.start(); +}); + +ipcMain.handle('sdd-stop', async () => { + return await sddService.stop(); +}); + async function startServer() { if (electronState.isDevelopment()) { log.info('In dev mode, assuming external servers are running.'); @@ -457,6 +474,7 @@ app.on('window-all-closed', () => { globalShortcut.unregisterAll(); opencodeService.stop(); xcOpenCodeWebService.stop(); + sddService.stop(); stopClipboardWatcher(); if (process.platform !== 'darwin') { app.quit(); @@ -474,7 +492,8 @@ app.on('before-quit', async (event) => { await Promise.all([ opencodeService.stop(), - xcOpenCodeWebService.stop() + xcOpenCodeWebService.stop(), + sddService.stop() ]); log.info('[App] All services stopped'); diff --git a/electron/preload.ts b/electron/preload.ts index d9fa6d9..0ef222d 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -45,4 +45,8 @@ contextBridge.exposeInMainWorld('electronAPI', { xcOpenCodeWebStop: () => ipcRenderer.invoke('xc-opencode-web-stop'), xcOpenCodeWebGetStatus: () => ipcRenderer.invoke('xc-opencode-web-get-status'), xcOpenCodeWebGetPort: () => ipcRenderer.invoke('xc-opencode-web-get-port'), + sddStart: () => ipcRenderer.invoke('sdd-start'), + sddStop: () => ipcRenderer.invoke('sdd-stop'), + sddGetStatus: () => ipcRenderer.invoke('sdd-get-status'), + sddGetPort: () => ipcRenderer.invoke('sdd-get-port'), }) diff --git a/electron/services/sddService.ts b/electron/services/sddService.ts new file mode 100644 index 0000000..edead73 --- /dev/null +++ b/electron/services/sddService.ts @@ -0,0 +1,175 @@ +import { spawn, exec, ChildProcess } from 'child_process'; +import { app } from 'electron'; +import path from 'path'; +import log from 'electron-log'; + +const SDD_PORT = 9998; +const HEALTH_CHECK_INTERVAL = 10000; +const HEALTH_CHECK_TIMEOUT = 2000; + +class SDDService { + private process: ChildProcess | null = null; + private processPid: number | null = null; + private healthCheckTimer: NodeJS.Timeout | null = null; + private _isRunning = false; + private _isStarting = false; + + get port(): number { + return SDD_PORT; + } + + isRunning(): boolean { + return this._isRunning; + } + + getStatus(): { running: boolean; port: number } { + return { + running: this._isRunning, + port: this.port, + }; + } + + private getExePath(): string { + const exeName = 'XCSDD.exe'; + const isPackaged = app.isPackaged; + + let basePath: string; + if (isPackaged) { + basePath = path.join(process.resourcesPath, 'app.asar.unpacked'); + } else { + basePath = process.cwd(); + } + + return path.join(basePath, 'services', 'xcsdd', exeName); + } + + private getExeArgs(): string[] { + return ['--port', this.port.toString()]; + } + + 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('[SDDService] 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('[SDDService] Service not responding'); + } + }, HEALTH_CHECK_INTERVAL); + } + + async start(): Promise<{ success: boolean; error?: string }> { + if (this._isRunning && this.process) { + log.info('[SDDService] Already running'); + return { success: true }; + } + + if (this._isStarting) { + log.info('[SDDService] Already starting'); + return { success: true }; + } + + this._isStarting = true; + + try { + const exePath = this.getExePath(); + const exeArgs = this.getExeArgs(); + log.info(`[SDDService] Starting from: ${exePath} with args: ${exeArgs.join(' ')}`); + + this.process = spawn(exePath, exeArgs, { + stdio: 'pipe', + }); + + this.processPid = this.process.pid ?? null; + + this.process.stdout?.on('data', (data) => { + log.info(`[SDD] ${data}`); + }); + + this.process.stderr?.on('data', (data) => { + log.error(`[SDD error] ${data}`); + }); + + this.process.on('error', (err) => { + log.error('[SDDService] Process error:', err); + this._isRunning = false; + this._isStarting = false; + this.process = null; + this.processPid = null; + }); + + this.process.on('exit', (code) => { + log.info(`[SDDService] Process exited with code ${code}`); + this._isRunning = false; + this._isStarting = false; + this.process = null; + this.processPid = null; + }); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + this._isRunning = true; + this._isStarting = false; + this.startHealthCheck(); + + log.info(`[SDDService] Started successfully on port ${this.port}`); + return { success: true }; + } catch (error: any) { + log.error('[SDDService] Failed to start:', error); + this._isRunning = false; + this._isStarting = 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.processPid) { + log.info('[SDDService] Not running'); + return { success: true }; + } + + return new Promise((resolve) => { + log.info(`[SDDService] Stopping process ${this.processPid}...`); + + exec(`taskkill /F /PID ${this.processPid}`, (error) => { + this.process = null; + this.processPid = null; + this._isRunning = false; + + if (error) { + log.error('[SDDService] Failed to stop:', error); + resolve({ success: false, error: error.message }); + } else { + log.info('[SDDService] Stopped'); + resolve({ success: true }); + } + }); + }); + } +} + +export const sddService = new SDDService(); diff --git a/shared/modules/sdd/index.ts b/shared/modules/sdd/index.ts new file mode 100644 index 0000000..3dcf9c8 --- /dev/null +++ b/shared/modules/sdd/index.ts @@ -0,0 +1,12 @@ +import { defineModule } from '../types.js' + +export const SDD_MODULE = defineModule({ + id: 'sdd', + name: 'SDD', + basePath: '/sdd', + order: 16, + version: '1.0.0', + backend: { + enabled: true, + }, +}) diff --git a/src/modules/sdd/SDDPage.tsx b/src/modules/sdd/SDDPage.tsx new file mode 100644 index 0000000..b2dcbea --- /dev/null +++ b/src/modules/sdd/SDDPage.tsx @@ -0,0 +1,86 @@ +import React, { useEffect, useState, useRef } from 'react' + +export const SDDPage: React.FC = () => { + const [isHealthy, setIsHealthy] = useState(false) + const [port, setPort] = useState(9998) + const startedRef = useRef(false) + const restartingRef = useRef(false) + const webviewRef = useRef(null) + + useEffect(() => { + let mounted = true + + const start = async () => { + try { + const portResult = await window.electronAPI.sddGetPort() + if (mounted) { + setPort(portResult.port) + } + + const result = await window.electronAPI.sddStart() + if (!result.success && mounted) { + console.error('Failed to start SDD:', result.error) + } + restartingRef.current = false + } catch (err) { + console.error('Failed to start SDD:', err) + restartingRef.current = false + } + } + + const checkStatus = async () => { + try { + const status = await window.electronAPI.sddGetStatus() + if (mounted) { + setIsHealthy(status.running) + if (!status.running && !restartingRef.current) { + restartingRef.current = true + start() + } + } + } catch (err) { + if (mounted) { + setIsHealthy(false) + if (!restartingRef.current) { + restartingRef.current = true + start() + } + } + } + } + + if (!startedRef.current) { + startedRef.current = true + start() + } + + const interval = setInterval(checkStatus, 2000) + + return () => { + mounted = false + clearInterval(interval) + window.electronAPI.sddStop() + startedRef.current = false + } + }, []) + + return ( +
+ + {!isHealthy && ( +
+
+
+ )} + {isHealthy && ( + + )} +
+ ) +} diff --git a/src/modules/sdd/index.tsx b/src/modules/sdd/index.tsx new file mode 100644 index 0000000..2ab74a9 --- /dev/null +++ b/src/modules/sdd/index.tsx @@ -0,0 +1,11 @@ +import { FileCode } from 'lucide-react' +import { SDDPage } from './SDDPage' +import { SDD_MODULE } from '@shared/modules/sdd' +import { createFrontendModule } from '@/lib/module-registry' + +export default createFrontendModule(SDD_MODULE, { + icon: FileCode, + component: SDDPage, +}) + +export { SDDPage } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 28e5596..4611852 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -45,6 +45,10 @@ export interface ElectronAPI { xcOpenCodeWebGetStatus: () => Promise<{ running: boolean; port: number }> xcOpenCodeWebGetPort: () => Promise<{ port: number }> xcOpenCodeWebCheckReady: () => Promise<{ ready: boolean }> + sddStart: () => Promise<{ success: boolean; error?: string }> + sddStop: () => Promise<{ success: boolean; error?: string }> + sddGetStatus: () => Promise<{ running: boolean; port: number }> + sddGetPort: () => Promise<{ port: number }> } declare global {