- 统一使用单引号替代双引号 - 移除不必要的分号 - 更新prettier配置使用更宽松的格式 - 添加eslint配置文件和相关依赖 - 更新package.json中的脚本和依赖版本
221 lines
6.3 KiB
JavaScript
221 lines
6.3 KiB
JavaScript
import { app, BrowserWindow, shell, ipcMain } from 'electron'
|
||
import path from 'node:path'
|
||
import fs from 'node:fs'
|
||
import net from 'node:net'
|
||
import { spawn } from 'node:child_process'
|
||
import started from 'electron-squirrel-startup'
|
||
|
||
if (started) app.quit()
|
||
|
||
// ========== OpenCode 服务管理 ==========
|
||
const DEFAULT_PORT = 4096
|
||
let opencodeProcess = null
|
||
let opencodePort = null
|
||
let opencodeStarting = null
|
||
|
||
function isPortAvailable(port) {
|
||
return new Promise((resolve) => {
|
||
const server = net.createServer()
|
||
server.once('error', () => resolve(false))
|
||
server.once('listening', () => server.close(() => resolve(true)))
|
||
server.listen(port, '127.0.0.1')
|
||
})
|
||
}
|
||
|
||
async function resolvePort() {
|
||
let port = DEFAULT_PORT
|
||
while (!(await isPortAvailable(port))) {
|
||
port++
|
||
if (port > 65535) throw new Error('没有可用的端口')
|
||
}
|
||
return port
|
||
}
|
||
|
||
function waitForReady(port, timeout = 15000) {
|
||
return new Promise((resolve, reject) => {
|
||
const start = Date.now()
|
||
const check = () => {
|
||
const socket = net.createConnection({ port, host: '127.0.0.1' })
|
||
socket.once('connect', () => {
|
||
socket.end()
|
||
resolve()
|
||
})
|
||
socket.once('error', () => {
|
||
socket.destroy()
|
||
if (Date.now() - start >= timeout) return reject(new Error('OpenCode 服务启动超时'))
|
||
setTimeout(check, 300)
|
||
})
|
||
}
|
||
check()
|
||
})
|
||
}
|
||
|
||
function buildInfo() {
|
||
return {
|
||
running: !!opencodeProcess,
|
||
port: opencodePort,
|
||
url: opencodePort ? `http://127.0.0.1:${opencodePort}` : null,
|
||
}
|
||
}
|
||
|
||
function buildEnv(exeDir) {
|
||
const env = { ...process.env }
|
||
for (const key of Object.keys(env)) {
|
||
if (key.startsWith('npm_')) delete env[key]
|
||
}
|
||
env.INIT_CWD = exeDir
|
||
env.PWD = exeDir
|
||
return env
|
||
}
|
||
|
||
function getExePath() {
|
||
// 开发模式:__dirname = .vite/build,往上两级到项目根
|
||
// 打包模式:用 process.resourcesPath
|
||
if (app.isPackaged) {
|
||
return path.join(process.resourcesPath, 'opencode.exe')
|
||
}
|
||
return path.join(__dirname, '..', '..', 'resources', 'windows', 'x64', 'opencode.exe')
|
||
}
|
||
|
||
async function startOpencode() {
|
||
if (opencodeProcess) return buildInfo()
|
||
if (opencodeStarting) return opencodeStarting
|
||
|
||
opencodeStarting = (async () => {
|
||
const exePath = getExePath()
|
||
console.log('[opencode] exe path:', exePath)
|
||
const exeDir = path.dirname(exePath)
|
||
await fs.promises.access(exePath, fs.constants.F_OK)
|
||
|
||
opencodePort = await resolvePort()
|
||
opencodeProcess = spawn(exePath, ['serve', '--port', String(opencodePort)], {
|
||
cwd: exeDir,
|
||
windowsHide: true,
|
||
env: buildEnv(exeDir),
|
||
})
|
||
|
||
opencodeProcess.stdout?.on('data', (d) => console.log(`[opencode] ${d.toString().trim()}`))
|
||
opencodeProcess.stderr?.on('data', (d) => console.error(`[opencode error] ${d.toString().trim()}`))
|
||
opencodeProcess.once('error', (e) => console.error('[opencode spawn error]', e))
|
||
opencodeProcess.once('close', (code) => {
|
||
console.log(`[opencode exited] code=${code}`)
|
||
opencodeProcess = null
|
||
opencodePort = null
|
||
opencodeStarting = null
|
||
})
|
||
|
||
await waitForReady(opencodePort)
|
||
return buildInfo()
|
||
})()
|
||
|
||
try {
|
||
return await opencodeStarting
|
||
} catch (err) {
|
||
opencodeProcess?.kill()
|
||
opencodeProcess = null
|
||
opencodePort = null
|
||
opencodeStarting = null
|
||
throw err
|
||
}
|
||
}
|
||
|
||
function stopOpencode() {
|
||
opencodeProcess?.kill()
|
||
opencodeProcess = null
|
||
opencodePort = null
|
||
opencodeStarting = null
|
||
}
|
||
|
||
// ========== IPC Handlers ==========
|
||
function registerIpcHandlers() {
|
||
ipcMain.handle('opencode:start', () => startOpencode())
|
||
ipcMain.handle('opencode:stop', () => {
|
||
stopOpencode()
|
||
return buildInfo()
|
||
})
|
||
ipcMain.handle('opencode:info', () => buildInfo())
|
||
ipcMain.handle('opencode:port', () => opencodePort)
|
||
|
||
ipcMain.handle('opencode:health', async () => {
|
||
if (!opencodePort) throw new Error('OpenCode 服务未启动')
|
||
const res = await fetch(`http://127.0.0.1:${opencodePort}/global/health`)
|
||
if (!res.ok) throw new Error(`健康检查失败: ${res.status}`)
|
||
return res.json()
|
||
})
|
||
|
||
ipcMain.handle('opencode:session:create', async (_e, data) => {
|
||
if (!opencodePort) throw new Error('OpenCode 服务未启动')
|
||
const res = await fetch(`http://127.0.0.1:${opencodePort}/session`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data ?? {}),
|
||
})
|
||
if (!res.ok) throw new Error(`创建会话失败: ${res.status}`)
|
||
return res.json()
|
||
})
|
||
|
||
ipcMain.handle('opencode:session:send', async (_e, sessionId, text) => {
|
||
if (!opencodePort) throw new Error('OpenCode 服务未启动')
|
||
const res = await fetch(`http://127.0.0.1:${opencodePort}/session/${sessionId}/message`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ parts: [{ type: 'text', text }] }),
|
||
})
|
||
if (!res.ok) throw new Error(`发送消息失败: ${res.status}`)
|
||
return res.json()
|
||
})
|
||
}
|
||
|
||
// ========== 窗口 ==========
|
||
const createWindow = () => {
|
||
const mainWindow = new BrowserWindow({
|
||
width: 1280,
|
||
height: 800,
|
||
minWidth: 800,
|
||
minHeight: 600,
|
||
webPreferences: {
|
||
preload: path.join(__dirname, 'index.js'),
|
||
contextIsolation: true,
|
||
nodeIntegration: false,
|
||
},
|
||
titleBarStyle: 'hiddenInset',
|
||
show: false,
|
||
})
|
||
|
||
mainWindow.once('ready-to-show', () => mainWindow.show())
|
||
|
||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||
shell.openExternal(url)
|
||
return { action: 'deny' }
|
||
})
|
||
|
||
// 注入 baseUrl,让渲染进程的 getBaseUrl() 能拿到正确端口
|
||
mainWindow.webContents.on('did-finish-load', () => {
|
||
if (opencodePort) {
|
||
mainWindow.webContents.executeJavaScript(`window.__opencodeBaseUrl = 'http://127.0.0.1:${opencodePort}'`)
|
||
}
|
||
})
|
||
|
||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
|
||
} else {
|
||
mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`))
|
||
}
|
||
}
|
||
|
||
app.whenReady().then(() => {
|
||
registerIpcHandlers()
|
||
createWindow()
|
||
|
||
app.on('activate', () => {
|
||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||
})
|
||
})
|
||
|
||
app.on('window-all-closed', () => {
|
||
stopOpencode()
|
||
if (process.platform !== 'darwin') app.quit()
|
||
})
|
||
|
||
app.on('before-quit', () => stopOpencode())
|