You've already forked iFlow-Settings-Editor-GUI
新增 配置文件多管理功能
This commit is contained in:
137
AGENTS.md
Normal file
137
AGENTS.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# iFlow Settings Editor - Agent 文档
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
iFlow 设置编辑器是一个基于 Electron + Vue 3 的桌面应用程序,用于编辑 `C:\Users\<USER>\.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';
|
||||||
|
```
|
||||||
201
main.js
201
main.js
@@ -1,19 +1,22 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const fs = require('fs');
|
const fs = require('fs')
|
||||||
|
|
||||||
console.log('main.js loaded');
|
console.log('main.js loaded')
|
||||||
console.log('app.getPath("home"):', app.getPath('home'));
|
console.log('app.getPath(\"home\"):', app.getPath('home'))
|
||||||
|
|
||||||
const SETTINGS_FILE = path.join(app.getPath('home'), '.iflow', 'settings.json');
|
const DEFAULT_SETTINGS_FILE = path.join(app.getPath('home'), '.iflow', 'settings.json')
|
||||||
console.log('SETTINGS_FILE:', SETTINGS_FILE);
|
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() {
|
function createWindow() {
|
||||||
console.log('Creating window...');
|
console.log('Creating window...')
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1100,
|
width: 1100,
|
||||||
@@ -24,94 +27,184 @@ function createWindow() {
|
|||||||
frame: false,
|
frame: false,
|
||||||
show: false,
|
show: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
devTools: true,
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
webSecurity: false
|
webSecurity: false,
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
console.log('Loading index.html...');
|
console.log('Loading index.html...')
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.loadURL('http://localhost:5173');
|
mainWindow.loadURL('http://localhost:5173')
|
||||||
} else {
|
} 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) => {
|
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) => {
|
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', () => {
|
mainWindow.once('ready-to-show', () => {
|
||||||
console.log('Window ready to show');
|
console.log('Window ready to show')
|
||||||
mainWindow.show();
|
mainWindow.show()
|
||||||
});
|
})
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
mainWindow = null;
|
mainWindow = null
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(createWindow);
|
app.whenReady().then(createWindow)
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (mainWindow === null) {
|
if (mainWindow === null) {
|
||||||
createWindow();
|
createWindow()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Window controls
|
// Window controls
|
||||||
ipcMain.on('window-minimize', () => mainWindow.minimize());
|
ipcMain.on('window-minimize', () => mainWindow.minimize())
|
||||||
ipcMain.on('window-maximize', () => {
|
ipcMain.on('window-maximize', () => {
|
||||||
if (mainWindow.isMaximized()) {
|
if (mainWindow.isMaximized()) {
|
||||||
mainWindow.unmaximize();
|
mainWindow.unmaximize()
|
||||||
} else {
|
} 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
|
// IPC Handlers
|
||||||
ipcMain.handle('load-settings', async () => {
|
ipcMain.handle('load-settings', async () => {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
if (!fs.existsSync(currentSettingsFile)) {
|
||||||
return { success: false, error: 'File not found', data: null };
|
return { success: false, error: 'File not found', data: null }
|
||||||
}
|
}
|
||||||
const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
|
const data = fs.readFileSync(currentSettingsFile, 'utf-8')
|
||||||
const json = JSON.parse(data);
|
const json = JSON.parse(data)
|
||||||
return { success: true, data: json };
|
return { success: true, data: json }
|
||||||
} catch (error) {
|
} 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) => {
|
ipcMain.handle('save-settings', async (event, data) => {
|
||||||
try {
|
try {
|
||||||
// Backup
|
if (fs.existsSync(currentSettingsFile)) {
|
||||||
if (fs.existsSync(SETTINGS_FILE)) {
|
const backupPath = currentSettingsFile + '.bak'
|
||||||
const backupPath = SETTINGS_FILE + '.bak';
|
fs.copyFileSync(currentSettingsFile, backupPath)
|
||||||
fs.copyFileSync(SETTINGS_FILE, backupPath);
|
|
||||||
}
|
}
|
||||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
fs.writeFileSync(currentSettingsFile, JSON.stringify(data, null, 2), 'utf-8')
|
||||||
return { success: true };
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message }
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
ipcMain.handle('show-message', async (event, { type, title, message }) => {
|
ipcMain.handle('show-message', async (event, { type, title, message }) => {
|
||||||
return dialog.showMessageBox(mainWindow, { type, title, message });
|
return dialog.showMessageBox(mainWindow, { type, title, message })
|
||||||
});
|
})
|
||||||
|
|||||||
12
preload.js
12
preload.js
@@ -1,4 +1,4 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron')
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
loadSettings: () => ipcRenderer.invoke('load-settings'),
|
loadSettings: () => ipcRenderer.invoke('load-settings'),
|
||||||
@@ -7,5 +7,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
isMaximized: () => ipcRenderer.invoke('is-maximized'),
|
isMaximized: () => ipcRenderer.invoke('is-maximized'),
|
||||||
minimize: () => ipcRenderer.send('window-minimize'),
|
minimize: () => ipcRenderer.send('window-minimize'),
|
||||||
maximize: () => ipcRenderer.send('window-maximize'),
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
|||||||
202
src/App.vue
202
src/App.vue
@@ -119,6 +119,30 @@
|
|||||||
<h1 class="content-title">API 配置</h1>
|
<h1 class="content-title">API 配置</h1>
|
||||||
<p class="content-desc">配置 AI 服务和搜索 API</p>
|
<p class="content-desc">配置 AI 服务和搜索 API</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<Exchange size="16" />
|
||||||
|
配置文件管理
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">当前配置文件</label>
|
||||||
|
<select class="form-select" v-model="currentConfigFilePath" @change="switchConfig">
|
||||||
|
<option v-for="cfg in configList" :key="cfg.filePath" :value="cfg.filePath">{{ cfg.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="display: flex; align-items: flex-end; gap: 8px;">
|
||||||
|
<button class="btn btn-secondary" @click="createNewConfig" style="height: 40px;">
|
||||||
|
<Add size="14" />
|
||||||
|
新建
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" @click="deleteConfig" style="height: 40px;" :disabled="configList.length <= 1">
|
||||||
|
<Delete size="14" />
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<Robot size="16" />
|
<Robot size="16" />
|
||||||
@@ -129,6 +153,7 @@
|
|||||||
<select class="form-select" v-model="settings.selectedAuthType">
|
<select class="form-select" v-model="settings.selectedAuthType">
|
||||||
<option value="iflow">iFlow</option>
|
<option value="iflow">iFlow</option>
|
||||||
<option value="api">API Key</option>
|
<option value="api">API Key</option>
|
||||||
|
<option value="openai-compatible">OpenAI 兼容</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -238,16 +263,37 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="footer-status">
|
<div class="footer-status">
|
||||||
<div class="footer-status-dot"></div>
|
<div class="footer-status-dot"></div>
|
||||||
<span>C:\Users\MSI\.iflow\settings.json</span>
|
<span>{{ currentConfigFilePath }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span :class="{ 'footer-modified': modified }">{{ modified ? '● 已修改' : '✓ 未修改' }}</span>
|
<span :class="{ 'footer-modified': modified }">{{ modified ? '● 已修改' : '✓ 未修改' }}</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Input Dialog -->
|
||||||
|
<div v-if="showInputDialog.show" class="dialog-overlay" @click.self="closeInputDialog(false)">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-title">{{ showInputDialog.title }}</div>
|
||||||
|
<div v-if="showInputDialog.isConfirm" class="dialog-confirm-text">{{ showInputDialog.placeholder }}</div>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
v-model="inputDialogValue"
|
||||||
|
:placeholder="showInputDialog.placeholder"
|
||||||
|
@keyup.enter="closeInputDialog(true)"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button class="btn btn-secondary" @click="closeInputDialog(false)">取消</button>
|
||||||
|
<button class="btn btn-primary" @click="closeInputDialog(true)">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||||
import { Refresh, Save, Config, Key, Server, Globe, Setting, Robot, Search, Add, Edit, Delete } from '@icon-park/vue-next';
|
import { Refresh, Save, Config, Key, Server, Globe, Setting, Robot, Search, Add, Edit, Delete, Exchange } from '@icon-park/vue-next';
|
||||||
|
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
language: 'zh-CN',
|
language: 'zh-CN',
|
||||||
@@ -268,11 +314,82 @@ const modified = ref(false);
|
|||||||
const currentSection = ref('general');
|
const currentSection = ref('general');
|
||||||
const currentServerName = ref(null);
|
const currentServerName = ref(null);
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
|
const configList = ref([]);
|
||||||
|
const currentConfigFilePath = ref('');
|
||||||
|
const showInputDialog = ref({ show: false, title: '', placeholder: '', callback: null });
|
||||||
|
const inputDialogValue = ref('');
|
||||||
|
|
||||||
|
// Load config list
|
||||||
|
const loadConfigList = async () => {
|
||||||
|
const result = await window.electronAPI.listConfigs();
|
||||||
|
if (result.success) {
|
||||||
|
configList.value = result.configs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Switch config
|
||||||
|
const switchConfig = async () => {
|
||||||
|
if (modified.value) {
|
||||||
|
const confirmed = await new Promise((resolve) => {
|
||||||
|
showInputDialog.value = { show: true, title: '切换配置', placeholder: '当前有未保存的更改,切换配置将丢失这些更改,确定要切换吗?', callback: resolve, isConfirm: true };
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
if (currentConfigFilePath.value) {
|
||||||
|
const result = await window.electronAPI.switchConfig(currentConfigFilePath.value);
|
||||||
|
if (result.success) {
|
||||||
|
await loadSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new config
|
||||||
|
const createNewConfig = async () => {
|
||||||
|
const name = await new Promise((resolve) => {
|
||||||
|
showInputDialog.value = { show: true, title: '新建配置文件', placeholder: '请输入配置名称', callback: resolve };
|
||||||
|
});
|
||||||
|
if (!name) return;
|
||||||
|
const result = await window.electronAPI.createConfig(name);
|
||||||
|
if (result.success) {
|
||||||
|
await loadConfigList();
|
||||||
|
currentConfigFilePath.value = result.filePath;
|
||||||
|
await loadSettings();
|
||||||
|
await window.electronAPI.showMessage({ type: 'info', title: '创建成功', message: `配置文件 "${name}" 已创建` });
|
||||||
|
} else {
|
||||||
|
await window.electronAPI.showMessage({ type: 'error', title: '创建失败', message: result.error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete config
|
||||||
|
const deleteConfig = async () => {
|
||||||
|
if (configList.value.length <= 1) {
|
||||||
|
await window.electronAPI.showMessage({ type: 'warning', title: '无法删除', message: '至少需要保留一个配置文件' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cfg = configList.value.find(c => c.filePath === currentConfigFilePath.value);
|
||||||
|
if (!cfg) return;
|
||||||
|
const confirmed = await new Promise((resolve) => {
|
||||||
|
showInputDialog.value = { show: true, title: '删除配置文件', placeholder: `确定要删除配置文件 "${cfg.name}" 吗?`, callback: resolve, isConfirm: true };
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
|
const result = await window.electronAPI.deleteConfig(currentConfigFilePath.value);
|
||||||
|
if (result.success) {
|
||||||
|
await loadConfigList();
|
||||||
|
if (configList.value.length > 0) {
|
||||||
|
currentConfigFilePath.value = configList.value[0].filePath;
|
||||||
|
await window.electronAPI.switchConfig(currentConfigFilePath.value);
|
||||||
|
await loadSettings();
|
||||||
|
}
|
||||||
|
await window.electronAPI.showMessage({ type: 'info', title: '删除成功', message: `配置文件 "${cfg.name}" 已删除` });
|
||||||
|
} else {
|
||||||
|
await window.electronAPI.showMessage({ type: 'error', title: '删除失败', message: result.error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
const result = await window.electronAPI.loadSettings();
|
const result = await window.electronAPI.loadSettings();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const data = result.data;
|
const data = JSON.parse(JSON.stringify(result.data));
|
||||||
if (!data.checkpointing) data.checkpointing = { enabled: true };
|
if (!data.checkpointing) data.checkpointing = { enabled: true };
|
||||||
if (!data.mcpServers) data.mcpServers = {};
|
if (!data.mcpServers) data.mcpServers = {};
|
||||||
settings.value = data;
|
settings.value = data;
|
||||||
@@ -284,7 +401,8 @@ const loadSettings = async () => {
|
|||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
collectServerData();
|
collectServerData();
|
||||||
const result = await window.electronAPI.saveSettings(settings.value);
|
const dataToSave = JSON.parse(JSON.stringify(settings.value));
|
||||||
|
const result = await window.electronAPI.saveSettings(dataToSave);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
originalSettings.value = JSON.parse(JSON.stringify(settings.value));
|
originalSettings.value = JSON.parse(JSON.stringify(settings.value));
|
||||||
modified.value = false;
|
modified.value = false;
|
||||||
@@ -295,7 +413,12 @@ const saveSettings = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const reloadSettings = async () => {
|
const reloadSettings = async () => {
|
||||||
if (modified.value && !confirm('当前有未保存的更改,确定要重新加载吗?')) return;
|
if (modified.value) {
|
||||||
|
const confirmed = await new Promise((resolve) => {
|
||||||
|
showInputDialog.value = { show: true, title: '重新加载', placeholder: '当前有未保存的更改,确定要重新加载吗?', callback: resolve, isConfirm: true };
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
currentServerName.value = null;
|
currentServerName.value = null;
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
};
|
};
|
||||||
@@ -308,21 +431,26 @@ const serverCount = computed(() => settings.value.mcpServers ? Object.keys(setti
|
|||||||
|
|
||||||
const selectServer = (name) => { currentServerName.value = name; };
|
const selectServer = (name) => { currentServerName.value = name; };
|
||||||
|
|
||||||
const addServer = () => {
|
const addServer = async () => {
|
||||||
const name = prompt('请输入服务器名称:');
|
const name = await new Promise((resolve) => {
|
||||||
|
showInputDialog.value = { show: true, title: '添加服务器', placeholder: '请输入服务器名称', callback: resolve };
|
||||||
|
});
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
if (!settings.value.mcpServers) settings.value.mcpServers = {};
|
if (!settings.value.mcpServers) settings.value.mcpServers = {};
|
||||||
if (settings.value.mcpServers[name]) {
|
if (settings.value.mcpServers[name]) {
|
||||||
alert('服务器已存在');
|
await window.electronAPI.showMessage({ type: 'warning', title: '错误', message: '服务器已存在' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.value.mcpServers[name] = { command: 'npx', args: ['-y', 'package-name'] };
|
settings.value.mcpServers[name] = { command: 'npx', args: ['-y', 'package-name'] };
|
||||||
currentServerName.value = name;
|
currentServerName.value = name;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteServer = () => {
|
const deleteServer = async () => {
|
||||||
if (!currentServerName.value) return;
|
if (!currentServerName.value) return;
|
||||||
if (!confirm(`确定要删除服务器 "${currentServerName.value}" 吗?`)) return;
|
const confirmed = await new Promise((resolve) => {
|
||||||
|
showInputDialog.value = { show: true, title: '删除服务器', placeholder: `确定要删除服务器 "${currentServerName.value}" 吗?`, callback: resolve, isConfirm: true };
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
delete settings.value.mcpServers[currentServerName.value];
|
delete settings.value.mcpServers[currentServerName.value];
|
||||||
currentServerName.value = null;
|
currentServerName.value = null;
|
||||||
};
|
};
|
||||||
@@ -369,7 +497,23 @@ const minimize = () => window.electronAPI.minimize();
|
|||||||
const maximize = () => window.electronAPI.maximize();
|
const maximize = () => window.electronAPI.maximize();
|
||||||
const close = () => window.electronAPI.close();
|
const close = () => window.electronAPI.close();
|
||||||
|
|
||||||
onMounted(() => { loadSettings(); });
|
const closeInputDialog = (result) => {
|
||||||
|
if (showInputDialog.value.callback) {
|
||||||
|
showInputDialog.value.callback(showInputDialog.value.isConfirm ? result : inputDialogValue.value);
|
||||||
|
}
|
||||||
|
showInputDialog.value.show = false;
|
||||||
|
showInputDialog.value.isConfirm = false;
|
||||||
|
inputDialogValue.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadConfigList();
|
||||||
|
const current = await window.electronAPI.getCurrentConfig();
|
||||||
|
if (current.filePath) {
|
||||||
|
currentConfigFilePath.value = current.filePath;
|
||||||
|
}
|
||||||
|
await loadSettings();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -510,5 +654,41 @@ body {
|
|||||||
.footer-status-dot { width: 6px; height: 6px; border-radius: 50%; background: #6ccb5f; }
|
.footer-status-dot { width: 6px; height: 6px; border-radius: 50%; background: #6ccb5f; }
|
||||||
.footer-modified { color: var(--accent); }
|
.footer-modified { color: var(--accent); }
|
||||||
|
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.dialog {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px;
|
||||||
|
min-width: 320px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.dialog-confirm-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.iconpark-icon { display: inline-block; vertical-align: middle; }
|
.iconpark-icon { display: inline-block; vertical-align: middle; }
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user