import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' import { join, dirname } from 'node:path' import { promises as fs } from 'node:fs' import { existsSync } from 'node:fs' let win: BrowserWindow | null = null const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL function createWindow() { win = new BrowserWindow({ width: 1440, height: 900, minWidth: 1100, minHeight: 700, title: 'XsinfoApi', backgroundColor: '#0a1428', titleBarStyle: 'hidden', titleBarOverlay: { color: '#0a1428', symbolColor: '#7eb6ff', height: 36 }, webPreferences: { preload: join(__dirname, 'preload.js'), sandbox: false, contextIsolation: true } }) if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL) } else { win.loadFile(join(__dirname, '../dist/index.html')) } win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url) return { action: 'deny' } }) } app.whenReady().then(() => { createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) // ============ 项目工作区 IPC(关联 git 下的 json) ============ const WORKSPACE_FILE = '.xsinfo-workspace.json' interface WorkspaceMeta { rootDir: string | null } async function readWorkspaceMeta(): Promise { const metaPath = join(app.getPath('userData'), WORKSPACE_FILE) if (!existsSync(metaPath)) return { rootDir: null } try { const raw = await fs.readFile(metaPath, 'utf-8') return JSON.parse(raw) } catch { return { rootDir: null } } } async function writeWorkspaceMeta(meta: WorkspaceMeta): Promise { const metaPath = join(app.getPath('userData'), WORKSPACE_FILE) await fs.writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf-8') } // 选择工作区目录(一个 git 项目) ipcMain.handle('workspace:pick', async () => { const res = await dialog.showOpenDialog({ title: '选择 API 项目工作区 (git 仓库)', properties: ['openDirectory'] }) if (res.canceled || res.filePaths.length === 0) return null const root = res.filePaths[0] await writeWorkspaceMeta({ rootDir: root }) return root }) ipcMain.handle('workspace:get', async () => { const meta = await readWorkspaceMeta() return meta.rootDir }) ipcMain.handle('workspace:clear', async () => { await writeWorkspaceMeta({ rootDir: null }) return null }) // 在工作区根下创建约定目录 xsinfo-data/{env.json, apis/...} ipcMain.handle('workspace:init', async (_e, rootDir: string) => { const dataDir = join(rootDir, 'xsinfo-data') const apisDir = join(dataDir, 'apis') await fs.mkdir(apisDir, { recursive: true }) const envFile = join(dataDir, 'env.json') if (!existsSync(envFile)) { const initial = { activeEnv: 'dev', envs: [ { id: 'dev', name: '开发环境', baseUrl: 'http://localhost:8080', variables: [{ key: 'token', value: 'dev-token' }] }, { id: 'prod', name: '生产环境', baseUrl: 'https://api.example.com', variables: [{ key: 'token', value: '' }] } ] } await fs.writeFile(envFile, JSON.stringify(initial, null, 2), 'utf-8') } return true }) // 读取 env.json ipcMain.handle('env:read', async (_e, rootDir: string) => { const file = join(rootDir, 'xsinfo-data', 'env.json') if (!existsSync(file)) return null const raw = await fs.readFile(file, 'utf-8') return JSON.parse(raw) }) ipcMain.handle('env:write', async (_e, rootDir: string, data: unknown) => { const file = join(rootDir, 'xsinfo-data', 'env.json') await fs.mkdir(join(rootDir, 'xsinfo-data'), { recursive: true }) await fs.writeFile(file, JSON.stringify(data, null, 2), 'utf-8') return true }) // API 列表 / 树 (apifox 风格:project.json + 各分组下子文件) ipcMain.handle('api:project:read', async (_e, rootDir: string) => { const file = join(rootDir, 'xsinfo-data', 'project.json') if (!existsSync(file)) return null const raw = await fs.readFile(file, 'utf-8') return JSON.parse(raw) }) ipcMain.handle('api:project:write', async (_e, rootDir: string, data: unknown) => { const dir = join(rootDir, 'xsinfo-data', 'apis') const file = join(rootDir, 'xsinfo-data', 'project.json') await fs.mkdir(dir, { recursive: true }) await fs.writeFile(file, JSON.stringify(data, null, 2), 'utf-8') return true }) ipcMain.handle('api:read', async (_e, rootDir: string, relativePath: string) => { if (relativePath.includes('..')) throw new Error('非法路径') const file = join(rootDir, 'xsinfo-data', relativePath.replace(/\\/g, '/')) if (!existsSync(file)) return null const raw = await fs.readFile(file, 'utf-8') return JSON.parse(raw) }) ipcMain.handle('api:write', async (_e, rootDir: string, relativePath: string, data: unknown) => { if (relativePath.includes('..')) throw new Error('非法路径') const file = join(rootDir, 'xsinfo-data', relativePath.replace(/\\/g, '/')) await fs.mkdir(dirname(file), { recursive: true }) await fs.writeFile(file, JSON.stringify(data, null, 2), 'utf-8') return true }) // 在目录下新增空白接口文件 ipcMain.handle('api:create', async (_e, rootDir: string, relativeDir: string, apiData: any) => { const dir = join(rootDir, 'xsinfo-data', 'apis', relativeDir.replace(/\\/g, '/')) await fs.mkdir(dir, { recursive: true }) const id = `api-${Date.now()}` const fileName = `${id}.json` const file = join(dir, fileName) const payload = { id, name: apiData.name || '新建接口', method: apiData.method || 'GET', url: apiData.url || '/', ...apiData } await fs.writeFile(file, JSON.stringify(payload, null, 2), 'utf-8') return { id, filePath: `apis/${relativeDir}/${fileName}`.replace(/\\/g, '/') } }) // 在工作区打开系统资源管理器 ipcMain.handle('shell:openDir', async (_e, dir: string) => { await shell.openPath(dir) return true }) // 健康检查 ipcMain.handle('app:ping', async () => 'pong')