first commit
This commit is contained in:
218
src/main/index.js
Normal file
218
src/main/index.js
Normal 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());
|
||||
Reference in New Issue
Block a user