Files
electron-opencode/src/main/index.js
houakang c738e638cf refactor: 统一代码风格并更新依赖配置
- 统一使用单引号替代双引号
- 移除不必要的分号
- 更新prettier配置使用更宽松的格式
- 添加eslint配置文件和相关依赖
- 更新package.json中的脚本和依赖版本
2026-04-10 10:39:41 +08:00

221 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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())