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 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 })
|
||||
})
|
||||
|
||||
12
preload.js
12
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)
|
||||
})
|
||||
|
||||
|
||||
202
src/App.vue
202
src/App.vue
@@ -119,6 +119,30 @@
|
||||
<h1 class="content-title">API 配置</h1>
|
||||
<p class="content-desc">配置 AI 服务和搜索 API</p>
|
||||
</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-title">
|
||||
<Robot size="16" />
|
||||
@@ -129,6 +153,7 @@
|
||||
<select class="form-select" v-model="settings.selectedAuthType">
|
||||
<option value="iflow">iFlow</option>
|
||||
<option value="api">API Key</option>
|
||||
<option value="openai-compatible">OpenAI 兼容</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -238,16 +263,37 @@
|
||||
<footer class="footer">
|
||||
<div class="footer-status">
|
||||
<div class="footer-status-dot"></div>
|
||||
<span>C:\Users\MSI\.iflow\settings.json</span>
|
||||
<span>{{ currentConfigFilePath }}</span>
|
||||
</div>
|
||||
<span :class="{ 'footer-modified': modified }">{{ modified ? '● 已修改' : '✓ 未修改' }}</span>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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({
|
||||
language: 'zh-CN',
|
||||
@@ -268,11 +314,82 @@ const modified = ref(false);
|
||||
const currentSection = ref('general');
|
||||
const currentServerName = ref(null);
|
||||
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 result = await window.electronAPI.loadSettings();
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
const data = JSON.parse(JSON.stringify(result.data));
|
||||
if (!data.checkpointing) data.checkpointing = { enabled: true };
|
||||
if (!data.mcpServers) data.mcpServers = {};
|
||||
settings.value = data;
|
||||
@@ -284,7 +401,8 @@ const loadSettings = async () => {
|
||||
|
||||
const saveSettings = async () => {
|
||||
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) {
|
||||
originalSettings.value = JSON.parse(JSON.stringify(settings.value));
|
||||
modified.value = false;
|
||||
@@ -295,7 +413,12 @@ const saveSettings = 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;
|
||||
await loadSettings();
|
||||
};
|
||||
@@ -308,21 +431,26 @@ const serverCount = computed(() => settings.value.mcpServers ? Object.keys(setti
|
||||
|
||||
const selectServer = (name) => { currentServerName.value = name; };
|
||||
|
||||
const addServer = () => {
|
||||
const name = prompt('请输入服务器名称:');
|
||||
const addServer = async () => {
|
||||
const name = await new Promise((resolve) => {
|
||||
showInputDialog.value = { show: true, title: '添加服务器', placeholder: '请输入服务器名称', callback: resolve };
|
||||
});
|
||||
if (!name) return;
|
||||
if (!settings.value.mcpServers) settings.value.mcpServers = {};
|
||||
if (settings.value.mcpServers[name]) {
|
||||
alert('服务器已存在');
|
||||
await window.electronAPI.showMessage({ type: 'warning', title: '错误', message: '服务器已存在' });
|
||||
return;
|
||||
}
|
||||
settings.value.mcpServers[name] = { command: 'npx', args: ['-y', 'package-name'] };
|
||||
currentServerName.value = name;
|
||||
};
|
||||
|
||||
const deleteServer = () => {
|
||||
const deleteServer = async () => {
|
||||
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];
|
||||
currentServerName.value = null;
|
||||
};
|
||||
@@ -369,7 +497,23 @@ const minimize = () => window.electronAPI.minimize();
|
||||
const maximize = () => window.electronAPI.maximize();
|
||||
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>
|
||||
|
||||
<style>
|
||||
@@ -510,5 +654,41 @@ body {
|
||||
.footer-status-dot { width: 6px; height: 6px; border-radius: 50%; background: #6ccb5f; }
|
||||
.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; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user