first commit

This commit is contained in:
2026-04-09 21:35:06 +08:00
commit 22fe6e069c
38 changed files with 12631 additions and 0 deletions

218
src/main/index.js Normal file
View File

@@ -0,0 +1,218 @@
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());