ai-helper-agent 1.3.4 → 1.5.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 +119 -5
- package/bin/agent.js +16 -8
- package/package.json +3 -3
- package/src/auth.js +26 -0
- package/src/config.js +21 -7
- package/src/mcp/client.js +244 -0
- package/src/mcp/mcp-config.js +124 -0
- package/src/mcp/registry.js +165 -0
- package/src/mcp/transport.js +100 -0
- package/src/security.js +73 -5
- package/src/server.js +366 -3
- package/src/skill/executor.js +369 -0
- package/src/skill/loader.js +308 -0
- package/src/skill/markdown-loader.js +411 -0
- package/src/skill/registry.js +315 -0
- package/src/skill/skill-creator-seed.js +144 -0
- package/src/skill/template.js +96 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AI Helper Agent
|
|
2
2
|
|
|
3
|
-
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
|
|
27
|
+
启动后终端会显示 4 位配对码,在 Chrome 扩展设置页「代理」标签中填入即可完成配对。
|
|
28
28
|
|
|
29
29
|
## CLI 命令
|
|
30
30
|
|
|
31
31
|
| 命令 | 说明 |
|
|
32
32
|
|------|------|
|
|
33
|
-
| `start` |
|
|
33
|
+
| `start` | 启动代理服务(前台运行,显示实时日志) |
|
|
34
34
|
| `start --background` / `start -b` | 后台启动(守护进程模式) |
|
|
35
35
|
| `stop` | 停止正在运行的 Agent |
|
|
36
|
-
| `restart` |
|
|
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` |
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
126
|
-
console.log(' stop
|
|
127
|
-
console.log(' restart
|
|
128
|
-
console.log(' status
|
|
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.
|
|
4
|
-
"description": "AI Helper
|
|
3
|
+
"version": "1.5.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
|
-
|
|
41
|
-
|
|
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 };
|