From 95aef170eb3d3d809c87f5e262752dbb0c110e0e Mon Sep 17 00:00:00 2001 From: yuantao Date: Tue, 14 Apr 2026 17:42:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=9A=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 137 +++++++++++++++++++++++++++++++++++ main.js | 201 +++++++++++++++++++++++++++++++++++++-------------- preload.js | 12 +++- src/App.vue | 202 +++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 484 insertions(+), 68 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..234a5dd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,137 @@ +# iFlow Settings Editor - Agent 文档 + +## 项目概述 + +iFlow 设置编辑器是一个基于 Electron + Vue 3 的桌面应用程序,用于编辑 `C:\Users\\.iflow\settings.json` 配置文件。 + +## 技术栈 + +| 技术 | 版本 | 用途 | +|------|------|------| +| Electron | ^28.0.0 | 桌面应用框架 | +| Vue | ^3.4.0 | 前端框架 (组合式 API) | +| Vite | ^8.0.8 | 构建工具 | +| @icon-park/vue-next | ^1.4.2 | 图标库 | +| @vitejs/plugin-vue | ^6.0.6 | Vue 插件 | + +## 项目结构 + +``` +iflow-settings-editor/ +├── main.js # Electron 主进程 (窗口管理、IPC、文件操作) +├── preload.js # 预加载脚本 (contextBridge API) +├── index.html # HTML 入口 +├── package.json # 项目配置 +├── vite.config.js # Vite 配置 +├── src/ +│ ├── main.js # Vue 入口 +│ └── App.vue # 主组件 (所有业务逻辑) +``` + +## 核心架构 + +### 进程模型 +- **Main Process (main.js)**: Electron 主进程,处理窗口管理、IPC 通信、文件系统操作 +- **Preload (preload.js)**: 通过 `contextBridge.exposeInMainWorld` 暴露安全 API +- **Renderer (Vue)**: 渲染进程,只通过 preload 暴露的 API 与主进程通信 + +### IPC 通信 +```javascript +// preload.js 暴露的 API +window.electronAPI = { + loadSettings: () => ipcRenderer.invoke('load-settings'), + saveSettings: (data) => ipcRenderer.invoke('save-settings', data), + showMessage: (options) => ipcRenderer.invoke('show-message', options), + isMaximized: () => ipcRenderer.invoke('is-maximized'), + minimize: () => ipcRenderer.send('window-minimize'), + maximize: () => ipcRenderer.send('window-maximize'), + close: () => ipcRenderer.send('window-close') +} +``` + +### 窗口配置 +- 窗口尺寸: 1100x750,最小尺寸: 900x600 +- 无边框窗口 (frame: false),自定义标题栏 +- 开发模式加载 `http://localhost:5173`,生产模式加载 `dist/index.html` + +### API 配置切换 +- 支持多环境配置: 默认配置、开发环境、预发布环境、生产环境 +- 切换前检查未保存的更改 +- 单独保存每个环境的 API 配置到 `apiProfiles` 对象 + +## 可用命令 + +```bash +npm install # 安装依赖 +npm run dev # 启动 Vite 开发服务器 +npm run build # 构建 Vue 应用到 dist 目录 +npm start # 运行 Electron (需先build) +npm run electron:dev # 同时运行 Vite + Electron (开发模式) +npm run electron:start # 构建 + 运行 Electron (生产模式) +``` + +## 功能模块 + +### 1. 常规设置 (General) +- **语言**: zh-CN / en-US / ja-JP +- **主题**: Xcode / Dark / Light / Solarized Dark +- **启动动画**: 已显示 / 未显示 +- **检查点保存**: 启用 / 禁用 + +### 2. API 配置 (API) +- **配置切换**: 支持多环境 (默认/开发/预发布/生产) +- **认证方式**: iFlow / API Key +- **API Key**: 密码输入框 +- **Base URL**: API 端点 +- **模型名称**: AI 模型标识 +- **搜索 API Key**: 搜索服务认证 +- **CNA**: CNA 标识 + +### 3. MCP 服务器管理 (MCP) +- 服务器列表展示 +- 添加/编辑/删除服务器 +- 服务器配置: 名称、描述、命令、工作目录、参数(每行一个)、环境变量(JSON) + +## 关键实现细节 + +### 设置文件路径 +```javascript +const SETTINGS_FILE = path.join(app.getPath('home'), '.iflow', 'settings.json'); +``` + +### 保存时自动备份 +```javascript +if (fs.existsSync(SETTINGS_FILE)) { + const backupPath = SETTINGS_FILE + '.bak'; + fs.copyFileSync(SETTINGS_FILE, backupPath); +} +``` + +### 安全配置 +- `contextIsolation: true` - 隔离上下文 +- `nodeIntegration: false` - 禁用 Node.js +- `webSecurity: false` - 仅开发环境解决 CSP 问题 + +### Vue 组件状态管理 +- `settings` - 当前设置 (ref) +- `originalSettings` - 原始设置 (用于检测修改) +- `modified` - 是否已修改 (computed/diff) +- `currentSection` - 当前显示的板块 +- `currentServerName` - 当前选中的 MCP 服务器 + +## 开发注意事项 + +1. **修改检测**: 通过 `watch(settings, () => { modified.value = true }, { deep: true })` 深度监听 +2. **服务器编辑**: 使用 DOM 操作收集表单数据 (`collectServerData`) +3. **MCP 参数**: 每行一个参数,通过换行分割 +4. **环境变量**: 支持 JSON 格式输入 +5. **窗口控制**: 通过 IPC 发送指令,主进程处理实际窗口操作 +6. **API 配置切换**: 多个环境配置存储在 `settings.apiProfiles` 对象中 +7. **序列化问题**: IPC 通信使用 `JSON.parse(JSON.stringify())` 避免 Vue 响应式代理问题 + +## 图标使用 + +使用 `@icon-park/vue-next` 图标库: +```javascript +import { Refresh, Save, Config, Key, Server, Globe, Setting, Robot, Search, Add, Edit, Delete } from '@icon-park/vue-next'; +``` diff --git a/main.js b/main.js index 5b59ce9..8f364a8 100644 --- a/main.js +++ b/main.js @@ -1,19 +1,22 @@ -const { app, BrowserWindow, ipcMain, dialog } = require('electron'); -const path = require('path'); -const fs = require('fs'); +const { app, BrowserWindow, ipcMain, dialog } = require('electron') +const path = require('path') +const fs = require('fs') -console.log('main.js loaded'); -console.log('app.getPath("home"):', app.getPath('home')); +console.log('main.js loaded') +console.log('app.getPath(\"home\"):', app.getPath('home')) -const SETTINGS_FILE = path.join(app.getPath('home'), '.iflow', 'settings.json'); -console.log('SETTINGS_FILE:', SETTINGS_FILE); +const DEFAULT_SETTINGS_FILE = path.join(app.getPath('home'), '.iflow', 'settings.json') +const CONFIG_DIR = path.join(app.getPath('home'), '.iflow', 'configs') +console.log('DEFAULT_SETTINGS_FILE:', DEFAULT_SETTINGS_FILE) +console.log('CONFIG_DIR:', CONFIG_DIR) -let mainWindow; +let currentSettingsFile = DEFAULT_SETTINGS_FILE +let mainWindow -const isDev = process.argv.includes('--dev'); +const isDev = process.argv.includes('--dev') function createWindow() { - console.log('Creating window...'); + console.log('Creating window...') mainWindow = new BrowserWindow({ width: 1100, @@ -24,94 +27,184 @@ function createWindow() { frame: false, show: false, webPreferences: { + devTools: true, preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, - webSecurity: false - } - }); + webSecurity: false, + }, + }) - console.log('Loading index.html...'); + console.log('Loading index.html...') if (isDev) { - mainWindow.loadURL('http://localhost:5173'); + mainWindow.loadURL('http://localhost:5173') } else { - mainWindow.loadFile(path.join(__dirname, 'dist', 'index.html')); + mainWindow.loadFile(path.join(__dirname, 'dist', 'index.html')) } - console.log('index.html loading initiated'); + console.log('index.html loading initiated') mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => { - console.error('Failed to load:', errorCode, errorDescription); - }); + console.error('Failed to load:', errorCode, errorDescription) + }) mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => { - console.log('Console [' + level + ']:', message); - }); + console.log('Console [' + level + ']:', message) + }) mainWindow.once('ready-to-show', () => { - console.log('Window ready to show'); - mainWindow.show(); - }); + console.log('Window ready to show') + mainWindow.show() + }) mainWindow.on('closed', () => { - mainWindow = null; - }); + mainWindow = null + }) } -app.whenReady().then(createWindow); +app.whenReady().then(createWindow) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { - app.quit(); + app.quit() } -}); +}) app.on('activate', () => { if (mainWindow === null) { - createWindow(); + createWindow() } -}); +}) // Window controls -ipcMain.on('window-minimize', () => mainWindow.minimize()); +ipcMain.on('window-minimize', () => mainWindow.minimize()) ipcMain.on('window-maximize', () => { if (mainWindow.isMaximized()) { - mainWindow.unmaximize(); + mainWindow.unmaximize() } else { - mainWindow.maximize(); + mainWindow.maximize() } -}); -ipcMain.on('window-close', () => mainWindow.close()); +}) +ipcMain.on('window-close', () => mainWindow.close()) -ipcMain.handle('is-maximized', () => mainWindow.isMaximized()); +ipcMain.handle('is-maximized', () => mainWindow.isMaximized()) + +// Get current config file path +ipcMain.handle('get-current-config', () => { + return { filePath: currentSettingsFile, fileName: path.basename(currentSettingsFile) } +}) + +// List all config files +ipcMain.handle('list-configs', async () => { + try { + const configs = [] + // Always include default settings + if (fs.existsSync(DEFAULT_SETTINGS_FILE)) { + const stats = fs.statSync(DEFAULT_SETTINGS_FILE) + configs.push({ + name: '默认配置', + filePath: DEFAULT_SETTINGS_FILE, + modified: stats.mtime + }) + } + // Add configs from CONFIG_DIR + if (fs.existsSync(CONFIG_DIR)) { + const files = fs.readdirSync(CONFIG_DIR).filter(f => f.endsWith('.json')) + for (const file of files) { + const filePath = path.join(CONFIG_DIR, file) + const stats = fs.statSync(filePath) + configs.push({ + name: file.replace('.json', ''), + filePath: filePath, + modified: stats.mtime + }) + } + } else { + fs.mkdirSync(CONFIG_DIR, { recursive: true }) + } + return { success: true, configs: configs } + } catch (error) { + return { success: false, error: error.message, configs: [] } + } +}) + +// Create new config +ipcMain.handle('create-config', async (event, name) => { + try { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }) + } + const fileName = name + '.json' + const filePath = path.join(CONFIG_DIR, fileName) + if (fs.existsSync(filePath)) { + return { success: false, error: '配置文件已存在' } + } + let data = {} + if (fs.existsSync(currentSettingsFile)) { + const content = fs.readFileSync(currentSettingsFile, 'utf-8') + data = JSON.parse(content) + } + fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8') + return { success: true, filePath: filePath } + } catch (error) { + return { success: false, error: error.message } + } +}) + +// Delete config +ipcMain.handle('delete-config', async (event, filePath) => { + try { + if (filePath === DEFAULT_SETTINGS_FILE) { + return { success: false, error: '不能删除默认配置' } + } + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath) + } + return { success: true } + } catch (error) { + return { success: false, error: error.message } + } +}) + +// Switch to config +ipcMain.handle('switch-config', async (event, filePath) => { + try { + if (!fs.existsSync(filePath)) { + return { success: false, error: '配置文件不存在' } + } + currentSettingsFile = filePath + return { success: true, filePath: currentSettingsFile } + } catch (error) { + return { success: false, error: error.message } + } +}) // IPC Handlers ipcMain.handle('load-settings', async () => { try { - if (!fs.existsSync(SETTINGS_FILE)) { - return { success: false, error: 'File not found', data: null }; + if (!fs.existsSync(currentSettingsFile)) { + return { success: false, error: 'File not found', data: null } } - const data = fs.readFileSync(SETTINGS_FILE, 'utf-8'); - const json = JSON.parse(data); - return { success: true, data: json }; + const data = fs.readFileSync(currentSettingsFile, 'utf-8') + const json = JSON.parse(data) + return { success: true, data: json } } catch (error) { - return { success: false, error: error.message, data: null }; + return { success: false, error: error.message, data: null } } -}); +}) ipcMain.handle('save-settings', async (event, data) => { try { - // Backup - if (fs.existsSync(SETTINGS_FILE)) { - const backupPath = SETTINGS_FILE + '.bak'; - fs.copyFileSync(SETTINGS_FILE, backupPath); + if (fs.existsSync(currentSettingsFile)) { + const backupPath = currentSettingsFile + '.bak' + fs.copyFileSync(currentSettingsFile, backupPath) } - fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2), 'utf-8'); - return { success: true }; + fs.writeFileSync(currentSettingsFile, JSON.stringify(data, null, 2), 'utf-8') + return { success: true } } catch (error) { - return { success: false, error: error.message }; + return { success: false, error: error.message } } -}); +}) ipcMain.handle('show-message', async (event, { type, title, message }) => { - return dialog.showMessageBox(mainWindow, { type, title, message }); -}); + return dialog.showMessageBox(mainWindow, { type, title, message }) +}) diff --git a/preload.js b/preload.js index c48996b..e983d1d 100644 --- a/preload.js +++ b/preload.js @@ -1,4 +1,4 @@ -const { contextBridge, ipcRenderer } = require('electron'); +const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { loadSettings: () => ipcRenderer.invoke('load-settings'), @@ -7,5 +7,11 @@ contextBridge.exposeInMainWorld('electronAPI', { isMaximized: () => ipcRenderer.invoke('is-maximized'), minimize: () => ipcRenderer.send('window-minimize'), maximize: () => ipcRenderer.send('window-maximize'), - close: () => ipcRenderer.send('window-close') -}); + close: () => ipcRenderer.send('window-close'), + getCurrentConfig: () => ipcRenderer.invoke('get-current-config'), + listConfigs: () => ipcRenderer.invoke('list-configs'), + createConfig: (name) => ipcRenderer.invoke('create-config', name), + deleteConfig: (filePath) => ipcRenderer.invoke('delete-config', filePath), + switchConfig: (filePath) => ipcRenderer.invoke('switch-config', filePath) +}) + diff --git a/src/App.vue b/src/App.vue index 07c93c5..07e83f0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -119,6 +119,30 @@

API 配置

配置 AI 服务和搜索 API

+
+
+ + 配置文件管理 +
+
+
+ + +
+
+ + +
+
+
@@ -129,6 +153,7 @@
@@ -238,16 +263,37 @@
{{ modified ? '● 已修改' : '✓ 未修改' }}
+ + +
+
+
{{ showInputDialog.title }}
+
{{ showInputDialog.placeholder }}
+ +
+ + +
+
+
\ No newline at end of file