/** * HoloLake Desktop · Electron 主进程 * * 职责: * 1. 创建应用窗口 * 2. 启动知识库后端服务器(Express + Git 引擎) * 3. 管理应用生命周期 * * 架构: * Electron Main Process * ├── 启动 Express 服务器(Git 引擎层,端口 3890) * ├── 创建 BrowserWindow(加载 Vite 前端或打包后的 dist) * └── 数据存储:~/Library/Application Support/HoloLake Era/data/ */ import { app, BrowserWindow, shell, dialog, ipcMain, safeStorage } from 'electron'; import path from 'path'; import { spawn, ChildProcess } from 'child_process'; import fs from 'fs'; import { importKnowledgeFolder } from './folder-import.js'; import { evaluateDomainAccess } from '../../guanghu-knowledge-base/server/domain-access.js'; // ─── 配置 ─── const isDev = !app.isPackaged; const SERVER_PORT = 3890; const CLIENT_PORT = 5180; // 数据目录:macOS 标准位置 const DATA_DIR = path.join(app.getPath('userData'), 'data'); const KB_REPO_PATH = path.join(DATA_DIR, 'knowledge-base'); const MODEL_CONFIG_PATH = path.join(app.getPath('userData'), 'model-config.json'); const SERVER_AUTH_PATH = path.join(app.getPath('userData'), 'server-auth.json'); const SERVER_PROFILES_PATH = path.join(app.getPath('userData'), 'server-profiles.json'); const AGENT_STATE_PATH = path.join(app.getPath('userData'), 'agent-conversations.json'); const GIT_ASKPASS_PATH = path.join(app.getPath('userData'), 'hololake-git-askpass.sh'); interface ServerProfileDefinition { id: string; physicalNodeId: string; name: string; purpose: 'personal-fifth-domain' | 'enterprise-lighthouse'; sshAlias: string; tunnelPort: number; remoteForgejoPort: number; lighthouseTunnelPort?: number; remoteLighthousePort?: number; channelTitle?: string; channelSubtitle?: string; } const PUBLIC_SERVER_PROFILES: Record = { 'AW-GZ-001': { id: 'AW-GZ-001', physicalNodeId: 'GH-CVM-MAIN-PROD-01', name: '企业灯塔服务器', purpose: 'enterprise-lighthouse', sshAlias: 'gh-enterprise-main', tunnelPort: 13341, remoteForgejoPort: 3341, lighthouseTunnelPort: 18031, remoteLighthousePort: 8031, }, }; function readServerProfiles(): Record { const profiles = { ...PUBLIC_SERVER_PROFILES }; try { const localProfiles = JSON.parse(fs.readFileSync(SERVER_PROFILES_PATH, 'utf8')) as ServerProfileDefinition[]; if (!Array.isArray(localProfiles)) return profiles; for (const profile of localProfiles) { if ( !profile || typeof profile.id !== 'string' || typeof profile.physicalNodeId !== 'string' || typeof profile.name !== 'string' || profile.purpose !== 'personal-fifth-domain' || typeof profile.sshAlias !== 'string' || !Number.isInteger(profile.tunnelPort) || !Number.isInteger(profile.remoteForgejoPort) ) continue; profiles[profile.id] = profile; } } catch { // 新安装默认只知道企业公共灯塔;个人节点由本机私有配置登记,不进入安装包。 } return profiles; } function getServerProfile(nodeId: string): ServerProfileDefinition { const profile = readServerProfiles()[nodeId]; if (!profile) throw new Error('未登记的服务器节点'); return profile; } function defaultPersonalServerId(): string | undefined { return Object.values(readServerProfiles()).find(profile => profile.purpose === 'personal-fifth-domain')?.id; } function forgejoBaseUrl(profile: ServerProfileDefinition): string { return `http://127.0.0.1:${profile.tunnelPort}`; } function lighthouseBaseUrl(profile: ServerProfileDefinition): string { if (!profile.lighthouseTunnelPort) throw new Error('该节点未登记灯塔服务'); return `http://127.0.0.1:${profile.lighthouseTunnelPort}`; } interface ModelConfigFile { baseUrl: string; model: string; encryptedKey?: string; verifiedAt?: string; } interface ServerAuthFile { nodeId: string; username: string; encryptedToken: string; } function writeGitAskPass(): void { const content = `#!/bin/sh case "$1" in *Username*) printf '%s\\n' "$HOLOLAKE_FORGEJO_USERNAME" ;; *) printf '%s\\n' "$HOLOLAKE_FORGEJO_TOKEN" ;; esac `; fs.mkdirSync(path.dirname(GIT_ASKPASS_PATH), { recursive: true }); fs.writeFileSync(GIT_ASKPASS_PATH, content, { mode: 0o700 }); } function clearGitCredentialEnvironment(): void { delete process.env.HOLOLAKE_FORGEJO_USERNAME; delete process.env.HOLOLAKE_FORGEJO_TOKEN; delete process.env.GIT_ASKPASS; delete process.env.GIT_TERMINAL_PROMPT; } function applyStoredServerAuth(): { nodeId: string; username: string; token: string } | null { try { if (!safeStorage.isEncryptionAvailable()) return null; const stored = JSON.parse(fs.readFileSync(SERVER_AUTH_PATH, 'utf8')) as ServerAuthFile; getServerProfile(stored.nodeId); if (!stored.encryptedToken) return null; const token = safeStorage.decryptString(Buffer.from(stored.encryptedToken, 'base64')); writeGitAskPass(); process.env.HOLOLAKE_FORGEJO_USERNAME = stored.username; process.env.HOLOLAKE_FORGEJO_TOKEN = token; process.env.GIT_ASKPASS = GIT_ASKPASS_PATH; process.env.GIT_TERMINAL_PROMPT = '0'; return { nodeId: stored.nodeId, username: stored.username, token }; } catch { clearGitCredentialEnvironment(); return null; } } function saveServerAuth(nodeId: string, username: string, token: string): void { if (!safeStorage.isEncryptionAvailable()) throw new Error('macOS 加密存储当前不可用'); fs.mkdirSync(path.dirname(SERVER_AUTH_PATH), { recursive: true }); const encryptedToken = safeStorage.encryptString(token).toString('base64'); fs.writeFileSync( SERVER_AUTH_PATH, JSON.stringify({ nodeId, username, encryptedToken } satisfies ServerAuthFile), { mode: 0o600 }, ); applyStoredServerAuth(); } function readModelConfig(): ModelConfigFile { try { return JSON.parse(fs.readFileSync(MODEL_CONFIG_PATH, 'utf8')) as ModelConfigFile; } catch { return { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o' }; } } function applyModelConfig(): ModelConfigFile { const config = readModelConfig(); process.env.HOLOLAKE_LLM_BASE = config.baseUrl; process.env.HOLOLAKE_LLM_MODEL = config.model; if (config.encryptedKey && safeStorage.isEncryptionAvailable()) { process.env.HOLOLAKE_LLM_KEY = safeStorage.decryptString(Buffer.from(config.encryptedKey, 'base64')); } else { delete process.env.HOLOLAKE_LLM_KEY; } process.env.HOLOLAKE_LLM_VERIFIED = config.verifiedAt ? '1' : '0'; return config; } async function verifyModelConfig(): Promise<{ verifiedAt: string }> { const config = readModelConfig(); if (!config.encryptedKey || !safeStorage.isEncryptionAvailable()) throw new Error('请先保存模型密钥'); const apiKey = safeStorage.decryptString(Buffer.from(config.encryptedKey, 'base64')); const response = await fetch(`${config.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model: config.model, messages: [ { role: 'system', content: 'This is a HoloLake model connectivity check. Reply with OK.' }, { role: 'user', content: 'ping' }, ], max_tokens: 8, temperature: 0, }), signal: AbortSignal.timeout(20000), }); const data = await response.json().catch(() => null) as any; if (!response.ok) throw new Error(data?.error?.message || `模型服务返回 HTTP ${response.status}`); if (!data?.choices?.[0]?.message) throw new Error('模型服务没有返回兼容的响应'); const verifiedAt = new Date().toISOString(); fs.writeFileSync(MODEL_CONFIG_PATH, JSON.stringify({ ...config, verifiedAt }), { mode: 0o600 }); applyModelConfig(); return { verifiedAt }; } // ─── 后端服务器 ─── let serverProcess: ChildProcess | null = null; const serverTunnels = new Map(); async function isServerTunnelReady(profile: ServerProfileDefinition): Promise { try { const checks: Promise[] = [ fetch(`${forgejoBaseUrl(profile)}/user/login`, { signal: AbortSignal.timeout(1200) }), ]; if (profile.lighthouseTunnelPort) { checks.push(fetch(`${lighthouseBaseUrl(profile)}/health`, { signal: AbortSignal.timeout(1200) })); } const responses = await Promise.all(checks); return responses.every(response => response.status === 200); } catch { return false; } } async function ensureServerTunnel(profile: ServerProfileDefinition): Promise { if (await isServerTunnelReady(profile)) return; const currentTunnel = serverTunnels.get(profile.id); if (currentTunnel && currentTunnel.exitCode === null) { throw new Error(`${profile.name}连接正在建立,请稍后重试`); } const forwards = [ '-L', `127.0.0.1:${profile.tunnelPort}:127.0.0.1:${profile.remoteForgejoPort}`, ]; if (profile.lighthouseTunnelPort && profile.remoteLighthousePort) { forwards.push('-L', `127.0.0.1:${profile.lighthouseTunnelPort}:127.0.0.1:${profile.remoteLighthousePort}`); } const tunnel = spawn('/usr/bin/ssh', [ '-N', ...forwards, '-o', 'BatchMode=yes', '-o', 'ExitOnForwardFailure=yes', '-o', 'ServerAliveInterval=30', '-o', 'ServerAliveCountMax=3', profile.sshAlias, ], { stdio: 'ignore', }); serverTunnels.set(profile.id, tunnel); tunnel.once('exit', () => { serverTunnels.delete(profile.id); }); for (let attempt = 0; attempt < 20; attempt += 1) { if (await isServerTunnelReady(profile)) return; if (tunnel.exitCode !== null) break; await new Promise(resolve => setTimeout(resolve, 250)); } tunnel.kill('SIGTERM'); serverTunnels.delete(profile.id); throw new Error(`无法通过本机登记密钥连接${profile.name}`); } function stopServerTunnels(): void { for (const tunnel of serverTunnels.values()) tunnel.kill('SIGTERM'); serverTunnels.clear(); } async function detachKnowledgeRemote(): Promise { try { await fetch(`http://127.0.0.1:${SERVER_PORT}/api/forgejo/remote`, { method: 'DELETE' }); } catch { // 本地知识库服务不可用时不阻断账号退出;下次登录仍会重新校验仓库权限。 } } async function forgejoRequest( nodeId: string, route: string, init: RequestInit = {}, auth?: { username?: string; password?: string; token?: string }, ): Promise<{ response: Response; data: any }> { const profile = getServerProfile(nodeId); await ensureServerTunnel(profile); const headers = new Headers(init.headers); headers.set('Accept', 'application/json'); if (init.body) headers.set('Content-Type', 'application/json'); if (auth?.token) headers.set('Authorization', `token ${auth.token}`); if (auth?.username && auth.password) { headers.set('Authorization', `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`); } const response = await fetch(`${forgejoBaseUrl(profile)}${route}`, { ...init, headers, signal: AbortSignal.timeout(10000), }); const text = await response.text(); let data: any = null; if (text) { try { data = JSON.parse(text); } catch { data = { message: text }; } } return { response, data }; } function startServer(): Promise { return new Promise((resolve, reject) => { if (isDev) { // 开发模式:用 spawn 启动外部 tsx const serverScript = path.resolve(__dirname, '../../guanghu-knowledge-base/server/index.ts'); serverProcess = spawn('npx', ['tsx', serverScript], { env: { ...process.env, KB_PORT: String(SERVER_PORT), KB_REPO_PATH, HOLOLAKE_AGENT_STATE_PATH: AGENT_STATE_PATH, NODE_ENV: 'development', }, cwd: path.resolve(__dirname, '../..'), stdio: ['pipe', 'pipe', 'pipe'], }); serverProcess.stdout?.on('data', (data: Buffer) => { const msg = data.toString().trim(); console.log(`[KB Server] ${msg}`); if (msg.includes('已启动')) resolve(); }); serverProcess.stderr?.on('data', (data: Buffer) => { console.error(`[KB Server Error] ${data.toString().trim()}`); }); serverProcess.on('error', (err: Error) => { console.error('服务器启动失败:', err); reject(err); }); serverProcess.on('exit', (code: number | null) => { console.log(`[KB Server] 退出,code=${code}`); serverProcess = null; }); setTimeout(() => resolve(), 5000); } else { // 生产模式:直接在主进程内加载 server-bundle try { process.env.KB_PORT = String(SERVER_PORT); process.env.KB_REPO_PATH = KB_REPO_PATH; process.env.HOLOLAKE_AGENT_STATE_PATH = AGENT_STATE_PATH; const serverBundle = path.join(__dirname, 'server-bundle.cjs'); require(serverBundle); console.log(`[KB Server] 内嵌启动,端口 ${SERVER_PORT}`); resolve(); } catch (err) { console.error('服务器内嵌启动失败:', err); reject(err); } } }); } function stopServer(): void { if (serverProcess) { serverProcess.kill('SIGTERM'); serverProcess = null; } } async function waitForServerReady(): Promise { for (let attempt = 0; attempt < 40; attempt += 1) { try { const response = await fetch(`http://127.0.0.1:${SERVER_PORT}/api/health`, { signal: AbortSignal.timeout(800), }); if (response.ok) return; } catch { // 服务仍在启动,继续读取真实健康端点,不提前打开一个离线界面。 } await new Promise(resolve => setTimeout(resolve, 250)); } throw new Error('HoloLake 本地服务未在规定时间内就绪'); } // ─── 窗口 ─── let mainWindow: BrowserWindow | null = null; function createWindow(): void { mainWindow = new BrowserWindow({ width: 1280, height: 860, minWidth: 800, minHeight: 600, titleBarStyle: 'hiddenInset', trafficLightPosition: { x: 16, y: 16 }, backgroundColor: '#07111d', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, }, }); if (isDev) { mainWindow.loadURL(`http://localhost:${CLIENT_PORT}`); mainWindow.webContents.openDevTools({ mode: 'detach' }); } else { mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); } // 外部链接用系统浏览器打开 mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('http')) shell.openExternal(url); return { action: 'deny' }; }); mainWindow.on('closed', () => { mainWindow = null; }); } ipcMain.handle('get-data-path', () => DATA_DIR); ipcMain.handle('knowledge:import-folder', async () => { const options: Electron.OpenDialogOptions = { title: '导入本地知识文件夹', buttonLabel: '导入到 HoloLake', properties: ['openDirectory'], }; const selection = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options); if (selection.canceled || !selection.filePaths[0]) { return { cancelled: true, imported: 0, assets: 0, skipped: 0, failed: [] }; } return importKnowledgeFolder(selection.filePaths[0], KB_REPO_PATH); }); ipcMain.handle('agent:get-config', () => { const config = readModelConfig(); return { baseUrl: config.baseUrl, model: config.model, configured: Boolean(config.encryptedKey && safeStorage.isEncryptionAvailable()), operational: Boolean(config.encryptedKey && config.verifiedAt && safeStorage.isEncryptionAvailable()), verifiedAt: config.verifiedAt, }; }); ipcMain.handle('agent:save-config', (_event, input: { baseUrl: string; model: string; apiKey?: string }) => { const baseUrl = String(input.baseUrl || '').trim().replace(/\/$/, ''); const model = String(input.model || '').trim(); if (!/^https:\/\//i.test(baseUrl)) throw new Error('模型服务地址必须使用 HTTPS'); if (!model) throw new Error('模型名称不能为空'); if (!safeStorage.isEncryptionAvailable()) throw new Error('macOS 加密存储当前不可用'); const previous = readModelConfig(); const encryptedKey = input.apiKey ? safeStorage.encryptString(String(input.apiKey)).toString('base64') : previous.encryptedKey; if (!encryptedKey) throw new Error('请填写模型密钥'); const verificationStillValid = !input.apiKey && previous.baseUrl === baseUrl && previous.model === model; fs.mkdirSync(path.dirname(MODEL_CONFIG_PATH), { recursive: true }); fs.writeFileSync(MODEL_CONFIG_PATH, JSON.stringify({ baseUrl, model, encryptedKey, verifiedAt: verificationStillValid ? previous.verifiedAt : undefined, }), { mode: 0o600 }); applyModelConfig(); return { baseUrl, model, configured: true, operational: verificationStillValid && Boolean(previous.verifiedAt) }; }); ipcMain.handle('agent:test-config', () => verifyModelConfig()); ipcMain.handle('server:list', async () => { return Promise.all(Object.values(readServerProfiles()).map(async profile => ({ id: profile.id, physicalNodeId: profile.physicalNodeId, name: profile.name, purpose: profile.purpose, channelTitle: profile.channelTitle, channelSubtitle: profile.channelSubtitle, connected: await isServerTunnelReady(profile), verified: true, }))); }); ipcMain.handle('server:connect', async (_event, nodeId: string) => { const profile = getServerProfile(nodeId); await ensureServerTunnel(profile); return { id: profile.id, physicalNodeId: profile.physicalNodeId, name: profile.name, purpose: profile.purpose, channelTitle: profile.channelTitle, channelSubtitle: profile.channelSubtitle, connected: true, verified: true, }; }); ipcMain.handle('server:domain-registry', async () => { const profile = getServerProfile('AW-GZ-001'); try { await ensureServerTunnel(profile); } catch { // 公共灯塔不可达时返回真实离线状态,界面仍可浏览已登记的四域结构。 } const connected = await isServerTunnelReady(profile); let lighthouseStatus: any = null; let lighthouseHealth: any = null; if (connected) { const [statusResponse, healthResponse] = await Promise.all([ fetch(`${lighthouseBaseUrl(profile)}/v1/status`, { signal: AbortSignal.timeout(3000) }), fetch(`${lighthouseBaseUrl(profile)}/health`, { signal: AbortSignal.timeout(3000) }), ]); if (statusResponse.ok) lighthouseStatus = await statusResponse.json(); if (healthResponse.ok) lighthouseHealth = await healthResponse.json(); } const liveDomains = new Map((lighthouseStatus?.domains || []).map((domain: any) => [domain.id, domain])); const domain = (serverId: string, fallback: Record) => ({ ...fallback, serverId, live: liveDomains.get(serverId) || null, }); return { nodeId: profile.id, physicalNodeId: profile.physicalNodeId, connected, verified: Boolean(lighthouseStatus && lighthouseHealth), codeChannel: connected ? 'reachable' : 'not-connected', lighthouse: lighthouseHealth ? { mode: lighthouseHealth.mode, mapHash: lighthouseHealth.map_hash, execution: lighthouseHealth.execution, hostState: lighthouseStatus?.host_state, observedAt: lighthouseStatus?.observed_at, } : null, domains: [ domain('DOMAIN-MAIN', { id: 'main', number: 'HLDP-DOMAIN-MAIN-001', name: '光湖主域', responsibility: '公共定义、版本发布、广播与模块生态状态', repository: 'domain-main' }), domain('DOMAIN-SUB', { id: 'sub', number: 'HLDP-DOMAIN-SUB-001', name: '光湖分域', responsibility: '行业入口、模块目录与初始化频道路由', repository: 'domain-sub' }), domain('DOMAIN-ZERO', { id: 'zero', number: 'HLDP-DOMAIN-ZERO-001', name: '光湖零域', responsibility: '模块试装、实验、质量验证与可回滚预览', repository: 'domain-zero' }), domain('DOMAIN-ZS', { id: 'zero-sense', number: 'HLDP-DOMAIN-ZEROSENSE-001', name: '光湖零感域', responsibility: '团队身份、责任、权限、审计与个人节点发现', repository: 'domain-zero-sense' }), ], }; }); ipcMain.handle('server:session', async (_event, requestedNodeId?: string) => { const stored = applyStoredServerAuth(); const profiles = readServerProfiles(); const nodeId = requestedNodeId && profiles[requestedNodeId] ? requestedNodeId : stored?.nodeId || defaultPersonalServerId() || ''; if (!stored || stored.nodeId !== nodeId) return { authenticated: false, nodeId }; try { const { response, data } = await forgejoRequest(nodeId, '/api/v1/user', {}, { token: stored.token }); if (!response.ok) { fs.rmSync(SERVER_AUTH_PATH, { force: true }); clearGitCredentialEnvironment(); return { authenticated: false, nodeId }; } return { authenticated: true, nodeId, username: data.login }; } catch { return { authenticated: false, nodeId }; } }); ipcMain.handle('server:domain-access', async (_event, domainId = 'DOM-FIFTH-0001') => { const stored = applyStoredServerAuth(); let accountVerified = false; if (stored) { try { const { response } = await forgejoRequest(stored.nodeId, '/api/v1/user', {}, { token: stored.token }); accountVerified = response.ok; } catch { accountVerified = false; } } // Forgejo identity is only the first proof. A signed manifest, scoped capability and // matching online receipt must arrive from the lighthouse/domain before runtime entry. return evaluateDomainAccess({ accountVerified, domainId: String(domainId), nodeId: stored?.nodeId || defaultPersonalServerId() || '', }); }); ipcMain.handle('server:login', async (_event, input: { nodeId: string; username: string; password: string }) => { const username = String(input.username || '').trim(); const password = String(input.password || ''); getServerProfile(input.nodeId); if (!username || !password) throw new Error('请输入账号和密码'); const basicAuth = { username, password }; const identity = await forgejoRequest(input.nodeId, '/api/v1/user', {}, basicAuth); if (!identity.response.ok) throw new Error('账号或密码不正确'); const tokenName = 'hololake-desktop'; const existing = await forgejoRequest(input.nodeId, `/api/v1/users/${encodeURIComponent(username)}/tokens`, {}, basicAuth); if (existing.response.ok && Array.isArray(existing.data)) { for (const token of existing.data) { if (token?.name === tokenName && token?.id) { await forgejoRequest( input.nodeId, `/api/v1/users/${encodeURIComponent(username)}/tokens/${token.id}`, { method: 'DELETE' }, basicAuth, ); } } } const created = await forgejoRequest( input.nodeId, `/api/v1/users/${encodeURIComponent(username)}/tokens`, { method: 'POST', body: JSON.stringify({ name: tokenName, scopes: ['all'] }) }, basicAuth, ); if (created.response.status !== 201 || !created.data?.sha1) { throw new Error(created.data?.message || '无法建立 HoloLake 登录令牌'); } const previous = applyStoredServerAuth(); if (!previous || previous.nodeId !== input.nodeId || previous.username !== identity.data.login) { await detachKnowledgeRemote(); } saveServerAuth(input.nodeId, username, created.data.sha1); return { authenticated: true, nodeId: input.nodeId, username: identity.data.login }; }); ipcMain.handle('server:logout', async () => { const stored = applyStoredServerAuth(); await detachKnowledgeRemote(); fs.rmSync(SERVER_AUTH_PATH, { force: true }); clearGitCredentialEnvironment(); return { authenticated: false, nodeId: stored?.nodeId || defaultPersonalServerId() || '' }; }); ipcMain.handle('server:repositories', async () => { const stored = applyStoredServerAuth(); if (!stored) throw new Error('请先登录服务器'); const { response, data } = await forgejoRequest(stored.nodeId, '/api/v1/user/repos?limit=100', {}, { token: stored.token }); if (!response.ok || !Array.isArray(data)) throw new Error(data?.message || '读取服务器仓库失败'); return data.map(repo => ({ name: repo.name, fullName: repo.full_name, private: repo.private, defaultBranch: repo.default_branch || 'main', })); }); ipcMain.handle('server:create-repository', async (_event, input: { name: string; description?: string }) => { const stored = applyStoredServerAuth(); if (!stored) throw new Error('请先登录服务器'); const name = String(input.name || '').trim(); if (!/^[A-Za-z0-9._-]{1,100}$/.test(name)) throw new Error('仓库名称只能使用字母、数字、点、短横线或下划线'); const { response, data } = await forgejoRequest(stored.nodeId, '/api/v1/user/repos', { method: 'POST', body: JSON.stringify({ name, description: String(input.description || ''), private: true, auto_init: true, default_branch: 'main', }), }, { token: stored.token }); if (response.status !== 201) throw new Error(data?.message || '创建仓库失败'); return { name: data.name, fullName: data.full_name, private: data.private, defaultBranch: data.default_branch || 'main', }; }); ipcMain.handle('server:git-remote', async (_event, fullName: string) => { if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(String(fullName || ''))) { throw new Error('仓库路径无效'); } const stored = applyStoredServerAuth(); if (!stored) throw new Error('请先登录服务器'); const [owner, repository] = String(fullName).split('/'); const access = await forgejoRequest( stored.nodeId, `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}`, {}, { token: stored.token }, ); if (!access.response.ok || access.data?.full_name !== fullName) { throw new Error('当前账号无权访问该仓库'); } return `${forgejoBaseUrl(getServerProfile(stored.nodeId))}/${fullName}.git`; }); // ─── 应用生命周期 ─── app.whenReady().then(async () => { try { applyModelConfig(); await startServer(); await waitForServerReady(); createWindow(); } catch (err) { dialog.showErrorBox('启动失败', `知识库引擎启动失败:${err}`); app.quit(); } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { stopServer(); if (process.platform !== 'darwin') app.quit(); }); app.on('before-quit', () => { stopServer(); stopServerTunnels(); });