ai-helper-agent 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AI Helper Agent
2
2
 
3
- AI Helper 本地代理服务,为 [AI Helper Chrome 扩展](https://github.com/xiweicheng/ai-helper) 提供本地文件读写和系统命令执行能力。
3
+ AI Helper 代理服务,为 [AI Helper Chrome 扩展](https://github.com/xiweicheng/ai-helper) 提供本地文件读写、系统命令执行、Skill 技能系统和 MCP 协议扩展能力。
4
4
 
5
5
  ## 安装
6
6
 
@@ -24,16 +24,16 @@ ai-helper-agent start -b
24
24
  ai-helper-agent start --workdir /path/to/your/project --port 18911
25
25
  ```
26
26
 
27
- 启动后终端会显示 4 位配对码,在 Chrome 扩展设置页「本地Agent」标签中填入即可完成配对。
27
+ 启动后终端会显示 4 位配对码,在 Chrome 扩展设置页「代理」标签中填入即可完成配对。
28
28
 
29
29
  ## CLI 命令
30
30
 
31
31
  | 命令 | 说明 |
32
32
  |------|------|
33
- | `start` | 启动 Agent 服务(前台运行,显示实时日志) |
33
+ | `start` | 启动代理服务(前台运行,显示实时日志) |
34
34
  | `start --background` / `start -b` | 后台启动(守护进程模式) |
35
35
  | `stop` | 停止正在运行的 Agent |
36
- | `restart` | 重启 Agent 服务(支持 -b 后台重启) |
36
+ | `restart` | 重启代理服务(支持 -b 后台重启) |
37
37
  | `status` | 查看运行状态 |
38
38
  | `paircode` | 查看配对码提示 |
39
39
  | `config` | 查看当前配置 |
@@ -207,7 +207,7 @@ GET /api/logs/dates
207
207
  |------|------|------|
208
208
  | POST | `/api/pair` | 配对认证 |
209
209
  | GET | `/api/status` | 健康检查(版本号、平台信息、搜索工具可用性) |
210
- | POST | `/api/shutdown` | 关闭 Agent 服务(仅限本地访问) |
210
+ | POST | `/api/shutdown` | 关闭代理服务(仅限本地访问) |
211
211
 
212
212
  ### 需要认证(Bearer Token)
213
213
 
@@ -243,6 +243,120 @@ Agent 内置多层异常保护,防止单个错误导致整个服务崩溃:
243
243
  - **文件 I/O 保护**:配置文件读写失败不影响服务运行
244
244
  - **进程管理保护**:`SIGTERM`/`SIGKILL` 发送对已退出进程有 try-catch 保护
245
245
 
246
+ ## Skill 系统
247
+
248
+ Agent 服务内置 Skill 系统,支持将操作流程沉淀为可复用的技能。
249
+
250
+ ### Skill 类型
251
+
252
+ | 类型 | 定义格式 | 执行方式 | 用途 |
253
+ |------|----------|----------|------|
254
+ | **Workflow Skill** | JSON/YAML | 直接执行 | 自动化流程,按步骤执行 |
255
+ | **Agent Skill** | SKILL.md | AI 自主调用 | 知识沉淀,在对话中触发 |
256
+
257
+ ### Skill 目录结构
258
+
259
+ 所有 Skill 存储在 `~/.ai-helper-agent/skills/` 目录下:
260
+
261
+ ```
262
+ ~/.ai-helper-agent/skills/
263
+ ├── workflow-skill.json # Workflow Skill(JSON 格式)
264
+ ├── another-skill.yaml # Workflow Skill(YAML 格式)
265
+ └── agent-skill/ # Agent Skill(目录形式)
266
+ ├── SKILL.md # 技能定义文件
267
+ └── _meta.json # 元数据(可选)
268
+ ```
269
+
270
+ ### SKILL.md 格式
271
+
272
+ ```markdown
273
+ ---
274
+ name: <skill-name>
275
+ description: "<简明描述,包含:(1) 技能做什么,(2) 何时触发调用>"
276
+ enabled: true
277
+ ---
278
+
279
+ # <技能标题>
280
+
281
+ ## When to Use This Skill
282
+
283
+ - 触发条件1
284
+ - 触发条件2
285
+
286
+ ## Core Capabilities
287
+
288
+ - 能力1
289
+ - 能力2
290
+
291
+ ## Usage
292
+
293
+ ### Step-by-Step
294
+
295
+ 1. 步骤1
296
+ 2. 步骤2
297
+
298
+ ## Examples
299
+
300
+ [具体示例]
301
+
302
+ ## Source
303
+
304
+ 从对话中沉淀,创建日期:YYYY-MM-DD
305
+ ```
306
+
307
+ ### 内置技能
308
+
309
+ - **skill-creator**:元技能,用于从对话中创建和更新其他技能
310
+
311
+ ### Skill API
312
+
313
+ | 方法 | 路径 | 说明 |
314
+ |------|------|------|
315
+ | GET | `/api/skill/list` | 获取所有 Skill 列表 |
316
+ | GET | `/api/skill/:name` | 获取单个 Skill 完整定义 |
317
+ | POST | `/api/skill/import` | 导入新 Skill |
318
+ | POST | `/api/skill/:name/toggle` | 切换启用/停用状态 |
319
+ | DELETE | `/api/skill/:name` | 删除 Skill |
320
+ | POST | `/api/skill/:name/run` | 执行 Workflow Skill |
321
+
322
+ ## MCP 协议扩展
323
+
324
+ 支持 Model Context Protocol(MCP),扩展第三方工具能力。
325
+
326
+ ### MCP Server 配置
327
+
328
+ 配置文件路径:`~/.ai-helper-agent/config.json`
329
+
330
+ ```json
331
+ {
332
+ "mcpServers": [
333
+ {
334
+ "id": "my-mcp-server",
335
+ "name": "My MCP Server",
336
+ "command": ["python", "-m", "my_mcp_server"],
337
+ "enabled": true
338
+ }
339
+ ]
340
+ }
341
+ ```
342
+
343
+ ### MCP API
344
+
345
+ | 方法 | 路径 | 说明 |
346
+ |------|------|------|
347
+ | GET | `/api/mcp/status` | 获取所有 MCP Server 状态 |
348
+ | POST | `/api/mcp/:serverId/connect` | 连接指定 MCP Server |
349
+ | POST | `/api/mcp/:serverId/disconnect` | 断开连接 |
350
+ | GET | `/api/mcp/tools` | 获取所有 MCP 工具列表 |
351
+
352
+ ### 工作原理
353
+
354
+ 1. Agent 启动时自动连接所有启用的 MCP Server
355
+ 2. 通过 stdio 建立 JSON-RPC 2.0 通信
356
+ 3. 自动发现 MCP Server 提供的工具
357
+ 4. 工具调用请求通过 Agent 转发到 MCP Server
358
+ 5. 工具结果返回给扩展
359
+
246
360
  ## 技术栈
247
361
 
248
362
  - Node.js >= 18
package/bin/agent.js CHANGED
@@ -9,6 +9,8 @@ import { homedir } from 'os';
9
9
  const AGENT_DIR = join(homedir(), '.ai-helper-agent');
10
10
  const CONFIG_FILE = join(AGENT_DIR, 'config.json');
11
11
  const PID_FILE = join(AGENT_DIR, 'agent.pid');
12
+ const SKILLS_DIR = join(AGENT_DIR, 'skills');
13
+ const WORKSPACE_DIR = join(AGENT_DIR, 'workspace');
12
14
 
13
15
  /**
14
16
  * 读取 Agent 配置文件
@@ -29,12 +31,18 @@ function readAgentConfig() {
29
31
  * 确保 Agent 配置目录存在
30
32
  */
31
33
  function ensureAgentDir() {
32
- if (!existsSync(AGENT_DIR)) {
33
- try {
34
+ try {
35
+ if (!existsSync(AGENT_DIR)) {
34
36
  mkdirSync(AGENT_DIR, { recursive: true });
35
- } catch (err) {
36
- console.error(`[Agent] 无法创建配置目录 ${AGENT_DIR}: ${err.message}`);
37
37
  }
38
+ // 确保 skills 和 workspace 子目录存在
39
+ for (const dir of [SKILLS_DIR, WORKSPACE_DIR]) {
40
+ if (!existsSync(dir)) {
41
+ mkdirSync(dir, { recursive: true });
42
+ }
43
+ }
44
+ } catch (err) {
45
+ console.error(`[Agent] 无法创建配置目录 ${AGENT_DIR}: ${err.message}`);
38
46
  }
39
47
  }
40
48
 
@@ -122,10 +130,10 @@ function printHelp() {
122
130
  console.log('用法: ai-helper-agent <命令> [选项]');
123
131
  console.log('');
124
132
  console.log('命令:');
125
- console.log(' start 启动 Agent 服务(前台运行)');
126
- console.log(' stop 停止正在运行的 Agent');
127
- console.log(' restart 重启 Agent 服务');
128
- console.log(' status 查看 Agent 运行状态');
133
+ console.log(' start 启动代理服务(前台运行)');
134
+ console.log(' stop 停止正在运行的代理');
135
+ console.log(' restart 重启代理服务');
136
+ console.log(' status 查看代理运行状态');
129
137
  console.log(' paircode 查看当前配对码(需认证状态)');
130
138
  console.log(' config 查看当前配置');
131
139
  console.log(' help 显示此帮助信息');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-helper-agent",
3
- "version": "1.3.3",
4
- "description": "AI Helper 本地代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
3
+ "version": "1.4.0",
4
+ "description": "AI Helper 代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
7
7
  "bin": {
@@ -39,6 +39,6 @@
39
39
  },
40
40
  "homepage": "",
41
41
  "bugs": {
42
- "url": ""
42
+ "url": "https://github.com/xiweicheng/ai-helper/issues"
43
43
  }
44
44
  }
package/src/auth.js CHANGED
@@ -6,6 +6,12 @@ import { loadConfig, loadPairings, savePairing } from './config.js';
6
6
  let currentPairCode = null;
7
7
  let pairCodeTimer = null;
8
8
 
9
+ // 最后一次成功认证的时间戳(用于判断是否有活跃连接)
10
+ let lastAuthTime = 0;
11
+
12
+ // 判定"活跃"的时间窗口:此时间内有过认证则认为仍有扩展连接
13
+ const ACTIVE_WINDOW_MS = 5 * 60 * 1000; // 5 分钟
14
+
9
15
  /**
10
16
  * 生成随机配对码(4位数字,使用密码学安全的随机数)
11
17
  */
@@ -22,9 +28,28 @@ function getCurrentPairCode() {
22
28
 
23
29
  /**
24
30
  * 启动配对码定时刷新
31
+ * - 如果有配对记录且最近有活跃认证(5分钟内),跳过刷新
32
+ * - 否则启动定时刷新,允许新扩展配对
25
33
  */
26
34
  function startPairCodeRotation() {
27
35
  const config = loadConfig();
36
+ const pairings = loadPairings();
37
+ const hasPairings = Object.keys(pairings).length > 0;
38
+ const isRecentlyActive = (Date.now() - lastAuthTime) < ACTIVE_WINDOW_MS;
39
+
40
+ // 有配对且最近有活跃连接,无需刷新配对码
41
+ if (hasPairings && isRecentlyActive) {
42
+ console.log(`[Agent] 已有 ${Object.keys(pairings).length} 个配对扩展,最近 ${Math.round((Date.now() - lastAuthTime) / 1000)}s 前有认证活动,跳过配对码刷新`);
43
+ currentPairCode = generatePairCode();
44
+ console.log(`[Agent] 当前配对码(用于新增配对): ${currentPairCode}`);
45
+ return;
46
+ }
47
+
48
+ // 有配对但无活跃连接,给出提示
49
+ if (hasPairings && !isRecentlyActive) {
50
+ console.log(`[Agent] 存在配对记录但无活跃连接(超过 ${ACTIVE_WINDOW_MS / 60000} 分钟),恢复配对码刷新`);
51
+ }
52
+
28
53
  const ttl = (config.pairCodeTTL || 30) * 1000;
29
54
 
30
55
  currentPairCode = generatePairCode();
@@ -55,6 +80,7 @@ function verifyToken(token) {
55
80
  const pairings = loadPairings();
56
81
  for (const [extId, pairing] of Object.entries(pairings)) {
57
82
  if (pairing.token === token) {
83
+ lastAuthTime = Date.now();
58
84
  return extId;
59
85
  }
60
86
  }
package/src/config.js CHANGED
@@ -6,6 +6,8 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs
6
6
  const AGENT_DIR = join(homedir(), '.ai-helper-agent');
7
7
  const CONFIG_FILE = join(AGENT_DIR, 'config.json');
8
8
  const PAIRINGS_FILE = join(AGENT_DIR, 'pairings.json');
9
+ const SKILLS_DIR = join(AGENT_DIR, 'skills');
10
+ const WORKSPACE_DIR = join(AGENT_DIR, 'workspace');
9
11
 
10
12
  const DEFAULTS = {
11
13
  port: 18910,
@@ -37,13 +39,20 @@ function withWriteLock(fn) {
37
39
  }
38
40
 
39
41
  function ensureAgentDir() {
40
- if (!existsSync(AGENT_DIR)) {
41
- try {
42
+ try {
43
+ // 确保 Agent 根目录存在
44
+ if (!existsSync(AGENT_DIR)) {
42
45
  mkdirSync(AGENT_DIR, { recursive: true });
43
- } catch (err) {
44
- console.error('[Config] 无法创建 Agent 目录:', err.message);
45
- throw err;
46
46
  }
47
+ // 确保 skills 和 workspace 子目录存在
48
+ for (const dir of [SKILLS_DIR, WORKSPACE_DIR]) {
49
+ if (!existsSync(dir)) {
50
+ mkdirSync(dir, { recursive: true });
51
+ }
52
+ }
53
+ } catch (err) {
54
+ console.error('[Config] 无法创建 Agent 目录:', err.message);
55
+ throw err;
47
56
  }
48
57
  }
49
58
 
@@ -82,13 +91,18 @@ export function loadConfig() {
82
91
  const userConfig = JSON.parse(raw);
83
92
  configCache = { ...DEFAULTS, ...userConfig };
84
93
  configCacheMtime = mtime;
85
- return configCache;
86
94
  } catch (err) {
87
95
  console.error('[Config] 配置文件解析失败:', err.message);
88
96
  configCache = { ...DEFAULTS, allowedPaths: [DEFAULTS.workdir] };
89
97
  configCacheMtime = 0;
90
- return configCache;
91
98
  }
99
+
100
+ // 自动将 skills 目录加入白名单,确保 AI 可以直接写入 skill 文件
101
+ if (!configCache.allowedPaths.includes(SKILLS_DIR)) {
102
+ configCache.allowedPaths.push(SKILLS_DIR);
103
+ }
104
+
105
+ return configCache;
92
106
  }
93
107
 
94
108
  /**
@@ -0,0 +1,244 @@
1
+ // mcp/client.js - MCP Client 核心
2
+ // 管理与单个 MCP Server 的 JSON-RPC 2.0 通信
3
+ import { createStdioTransport } from './transport.js';
4
+
5
+ let requestIdCounter = 0;
6
+
7
+ /**
8
+ * MCP Client 类
9
+ * 管理与一个 MCP Server 的完整生命周期
10
+ */
11
+ export class McpClient {
12
+ constructor(serverConfig) {
13
+ this.serverId = serverConfig.id;
14
+ this.serverName = serverConfig.name || serverConfig.id;
15
+ this.serverConfig = serverConfig;
16
+ this.transport = null;
17
+ this.connected = false;
18
+ this.tools = [];
19
+ this.serverInfo = null;
20
+
21
+ // JSON-RPC 请求管理
22
+ this.pendingRequests = new Map(); // id → { resolve, reject, timeoutId }
23
+ }
24
+
25
+ /**
26
+ * 连接到 MCP Server
27
+ * 流程: 启动子进程 → initialize → tools/list
28
+ */
29
+ async connect() {
30
+ if (this.connected) return { success: true };
31
+
32
+ try {
33
+ // 1. 创建 stdio 传输
34
+ this.transport = await createStdioTransport(this.serverConfig);
35
+
36
+ // 2. 设置响应数据处理器
37
+ this.transport.onData((line) => this._handleLine(line));
38
+
39
+ // 2.5 监听进程崩溃(已连接后)
40
+ this.transport.onExit((code, signal) => {
41
+ console.error(`[MCP:${this.serverId}] Server 进程意外退出 (code=${code}, signal=${signal}),标记为断开`);
42
+ this.connected = false;
43
+ this.tools = [];
44
+ // 拒绝所有进行中的请求
45
+ for (const [id, pending] of this.pendingRequests) {
46
+ clearTimeout(pending.timeoutId);
47
+ pending.reject(new Error('MCP Server 进程已退出'));
48
+ }
49
+ this.pendingRequests.clear();
50
+ });
51
+
52
+ console.log(`[MCP:${this.serverId}] 子进程已启动`);
53
+
54
+ // 3. 发送 initialize 请求
55
+ const initResult = await this._sendRequest('initialize', {
56
+ protocolVersion: '2024-11-05',
57
+ capabilities: {
58
+ tools: {}
59
+ },
60
+ clientInfo: {
61
+ name: 'ai-helper-agent',
62
+ version: '1.3.4'
63
+ }
64
+ });
65
+
66
+ if (!initResult || initResult.error) {
67
+ throw new Error(`Initialize 失败: ${initResult?.error?.message || '未知错误'}`);
68
+ }
69
+
70
+ this.serverInfo = initResult;
71
+ console.log(`[MCP:${this.serverId}] Initialize 成功, server:`, initResult.serverInfo?.name);
72
+
73
+ // 4. 发送 initialized 通知(MCP 协议要求)
74
+ this._sendNotification('notifications/initialized', {});
75
+
76
+ // 5. 发现工具
77
+ const toolsResult = await this._sendRequest('tools/list', {});
78
+ if (toolsResult && toolsResult.tools) {
79
+ this.tools = toolsResult.tools;
80
+ console.log(`[MCP:${this.serverId}] 发现 ${this.tools.length} 个工具:`,
81
+ this.tools.map(t => t.name).join(', '));
82
+ } else {
83
+ this.tools = [];
84
+ }
85
+
86
+ this.connected = true;
87
+ return { success: true, toolCount: this.tools.length };
88
+ } catch (err) {
89
+ console.error(`[MCP:${this.serverId}] 连接失败:`, err.message);
90
+ this.disconnect();
91
+ return { success: false, error: err.message };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 调用 MCP 工具
97
+ * @param {string} toolName
98
+ * @param {Object} args
99
+ */
100
+ async callTool(toolName, args) {
101
+ if (!this.connected) {
102
+ return { success: false, error: `MCP Server "${this.serverName}" 未连接` };
103
+ }
104
+
105
+ try {
106
+ const result = await this._sendRequest('tools/call', {
107
+ name: toolName,
108
+ arguments: args || {}
109
+ });
110
+
111
+ if (result && result.error) {
112
+ return { success: false, error: result.error.message || '工具调用失败' };
113
+ }
114
+
115
+ // MCP tools/call 返回 { content: [{ type: 'text', text: '...' }], isError: false }
116
+ const content = result?.content || [];
117
+ const textContent = content
118
+ .filter(c => c.type === 'text')
119
+ .map(c => c.text)
120
+ .join('\n');
121
+
122
+ return {
123
+ success: !result?.isError,
124
+ content: textContent,
125
+ raw: result
126
+ };
127
+ } catch (err) {
128
+ return { success: false, error: `工具调用异常: ${err.message}` };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 断开连接
134
+ */
135
+ disconnect() {
136
+ this.connected = false;
137
+ this.tools = [];
138
+
139
+ // 清理所有未完成的请求
140
+ for (const [id, pending] of this.pendingRequests) {
141
+ clearTimeout(pending.timeoutId);
142
+ pending.reject(new Error('连接已断开'));
143
+ }
144
+ this.pendingRequests.clear();
145
+
146
+ if (this.transport) {
147
+ try { this.transport.close(); } catch {}
148
+ this.transport = null;
149
+ }
150
+
151
+ console.log(`[MCP:${this.serverId}] 已断开连接`);
152
+ }
153
+
154
+ /**
155
+ * 获取状态信息
156
+ */
157
+ getStatus() {
158
+ return {
159
+ id: this.serverId,
160
+ name: this.serverName,
161
+ connected: this.connected,
162
+ toolCount: this.tools.length,
163
+ serverInfo: this.serverInfo?.serverInfo || null
164
+ };
165
+ }
166
+
167
+ // ========== 私有方法 ==========
168
+
169
+ /**
170
+ * 处理从 transport 收到的每一行 JSON-RPC 消息
171
+ */
172
+ _handleLine(line) {
173
+ try {
174
+ const message = JSON.parse(line);
175
+ const id = message.id;
176
+
177
+ if (id !== undefined && id !== null) {
178
+ // 有 id = 响应
179
+ const pending = this.pendingRequests.get(id);
180
+ if (pending) {
181
+ clearTimeout(pending.timeoutId);
182
+ this.pendingRequests.delete(id);
183
+
184
+ if (message.error) {
185
+ pending.reject(new Error(message.error.message || 'JSON-RPC Error'));
186
+ } else {
187
+ pending.resolve(message.result);
188
+ }
189
+ }
190
+ } else {
191
+ // 无 id = 通知,仅记录
192
+ if (message.method) {
193
+ console.log(`[MCP:${this.serverId}] 通知:`, message.method);
194
+ }
195
+ }
196
+ } catch (err) {
197
+ // 非 JSON 行,忽略
198
+ }
199
+ }
200
+
201
+ /**
202
+ * 发送 JSON-RPC 请求并等待响应
203
+ */
204
+ _sendRequest(method, params) {
205
+ return new Promise((resolve, reject) => {
206
+ const id = ++requestIdCounter;
207
+
208
+ const timeoutId = setTimeout(() => {
209
+ this.pendingRequests.delete(id);
210
+ reject(new Error(`MCP 请求超时 (60s): ${method}`));
211
+ }, 60000);
212
+
213
+ this.pendingRequests.set(id, { resolve, reject, timeoutId });
214
+
215
+ try {
216
+ this.transport.send({
217
+ jsonrpc: '2.0',
218
+ id,
219
+ method,
220
+ params
221
+ });
222
+ } catch (err) {
223
+ clearTimeout(timeoutId);
224
+ this.pendingRequests.delete(id);
225
+ reject(err);
226
+ }
227
+ });
228
+ }
229
+
230
+ /**
231
+ * 发送 JSON-RPC 通知(无需响应)
232
+ */
233
+ _sendNotification(method, params) {
234
+ try {
235
+ this.transport.send({
236
+ jsonrpc: '2.0',
237
+ method,
238
+ params
239
+ });
240
+ } catch {
241
+ // 通知发送失败不抛出
242
+ }
243
+ }
244
+ }
@@ -0,0 +1,124 @@
1
+ // mcp/mcp-config.js - MCP Server 配置管理
2
+ // 复用 agent/config.js 的缓存 + 写锁模式
3
+ import { readFileSync, writeFileSync, existsSync, statSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+
7
+ const AGENT_DIR = join(homedir(), '.ai-helper-agent');
8
+ const MCP_CONFIG_FILE = join(AGENT_DIR, 'mcp_servers.json');
9
+
10
+ // 内存缓存
11
+ let configCache = null;
12
+ let configCacheMtime = 0;
13
+
14
+ // 写入互斥锁
15
+ let writeLock = Promise.resolve();
16
+
17
+ function withWriteLock(fn) {
18
+ const prev = writeLock;
19
+ let release;
20
+ writeLock = new Promise(resolve => { release = resolve; });
21
+ return prev.then(() => fn().finally(release));
22
+ }
23
+
24
+ function getMtime(filePath) {
25
+ try { return statSync(filePath).mtimeMs; }
26
+ catch { return 0; }
27
+ }
28
+
29
+ /**
30
+ * 加载 MCP Server 配置(带缓存)
31
+ * @returns {{ servers: Array }}
32
+ */
33
+ export function loadMcpConfig() {
34
+ const mtime = getMtime(MCP_CONFIG_FILE);
35
+
36
+ if (configCache && mtime === configCacheMtime) {
37
+ return configCache;
38
+ }
39
+
40
+ if (!existsSync(MCP_CONFIG_FILE)) {
41
+ const defaults = { servers: [] };
42
+ try { writeFileSync(MCP_CONFIG_FILE, JSON.stringify(defaults, null, 2), 'utf-8'); } catch {}
43
+ configCache = defaults;
44
+ configCacheMtime = getMtime(MCP_CONFIG_FILE);
45
+ return defaults;
46
+ }
47
+
48
+ try {
49
+ const raw = readFileSync(MCP_CONFIG_FILE, 'utf-8');
50
+ configCache = JSON.parse(raw);
51
+ if (!configCache.servers) configCache.servers = [];
52
+ configCacheMtime = mtime;
53
+ return configCache;
54
+ } catch (err) {
55
+ console.error('[MCP Config] 配置文件解析失败:', err.message);
56
+ configCache = { servers: [] };
57
+ configCacheMtime = 0;
58
+ return configCache;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 保存 MCP Server 配置
64
+ */
65
+ export async function saveMcpConfig(config) {
66
+ return withWriteLock(async () => {
67
+ try {
68
+ writeFileSync(MCP_CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
69
+ configCache = JSON.parse(JSON.stringify(config)); // 深拷贝
70
+ configCacheMtime = getMtime(MCP_CONFIG_FILE);
71
+ } catch (err) {
72
+ console.error('[MCP Config] 保存配置失败:', err.message);
73
+ throw err;
74
+ }
75
+ });
76
+ }
77
+
78
+ /**
79
+ * 添加或更新 MCP Server
80
+ * @param {Object} server - { id, name, command, args, env, enabled }
81
+ */
82
+ export async function addMcpServer(server) {
83
+ const config = loadMcpConfig();
84
+ const existingIdx = config.servers.findIndex(s => s.id === server.id);
85
+
86
+ if (existingIdx >= 0) {
87
+ config.servers[existingIdx] = { ...config.servers[existingIdx], ...server };
88
+ } else {
89
+ config.servers.push(server);
90
+ }
91
+
92
+ await saveMcpConfig(config);
93
+ return { success: true };
94
+ }
95
+
96
+ /**
97
+ * 删除 MCP Server
98
+ */
99
+ export async function removeMcpServer(serverId) {
100
+ const config = loadMcpConfig();
101
+ const idx = config.servers.findIndex(s => s.id === serverId);
102
+ if (idx < 0) {
103
+ return { success: false, error: `MCP Server "${serverId}" 不存在` };
104
+ }
105
+ config.servers.splice(idx, 1);
106
+ await saveMcpConfig(config);
107
+ return { success: true };
108
+ }
109
+
110
+ /**
111
+ * 更新 MCP Server 的启用状态
112
+ */
113
+ export async function toggleMcpServer(serverId, enabled) {
114
+ const config = loadMcpConfig();
115
+ const server = config.servers.find(s => s.id === serverId);
116
+ if (!server) {
117
+ return { success: false, error: `MCP Server "${serverId}" 不存在` };
118
+ }
119
+ server.enabled = enabled;
120
+ await saveMcpConfig(config);
121
+ return { success: true };
122
+ }
123
+
124
+ export { MCP_CONFIG_FILE };