ai-helper-agent 1.6.1 → 1.6.3
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/package.json +6 -3
- package/src/mcp/client.js +24 -157
- package/src/security.js +26 -9
- package/src/server.js +120 -0
- package/src/skill/skill-creator-seed.js +5 -4
- package/src/mcp/transport.js +0 -100
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-helper-agent",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "AI Helper 代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"start": "node bin/agent.js start",
|
|
17
17
|
"stop": "node bin/agent.js stop",
|
|
18
|
-
"status": "node bin/agent.js status"
|
|
18
|
+
"status": "node bin/agent.js status",
|
|
19
|
+
"publish": "bash publish.sh"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
23
|
+
"ws": "^8.18.0",
|
|
24
|
+
"zod": "^4.4.3"
|
|
22
25
|
},
|
|
23
26
|
"engines": {
|
|
24
27
|
"node": ">=18.0.0"
|
package/src/mcp/client.js
CHANGED
|
@@ -1,80 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { createStdioTransport } from './transport.js';
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
4
3
|
|
|
5
|
-
let requestIdCounter = 0;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* MCP Client 类
|
|
9
|
-
* 管理与一个 MCP Server 的完整生命周期
|
|
10
|
-
*/
|
|
11
4
|
export class McpClient {
|
|
12
5
|
constructor(serverConfig) {
|
|
13
6
|
this.serverId = serverConfig.id;
|
|
14
7
|
this.serverName = serverConfig.name || serverConfig.id;
|
|
15
8
|
this.serverConfig = serverConfig;
|
|
9
|
+
this.client = null;
|
|
16
10
|
this.transport = null;
|
|
17
11
|
this.connected = false;
|
|
18
12
|
this.tools = [];
|
|
19
13
|
this.serverInfo = null;
|
|
20
|
-
|
|
21
|
-
// JSON-RPC 请求管理
|
|
22
|
-
this.pendingRequests = new Map(); // id → { resolve, reject, timeoutId }
|
|
23
14
|
}
|
|
24
15
|
|
|
25
|
-
/**
|
|
26
|
-
* 连接到 MCP Server
|
|
27
|
-
* 流程: 启动子进程 → initialize → tools/list
|
|
28
|
-
*/
|
|
29
16
|
async connect() {
|
|
30
17
|
if (this.connected) return { success: true };
|
|
31
18
|
|
|
32
19
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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();
|
|
20
|
+
this.transport = new StdioClientTransport({
|
|
21
|
+
command: this.serverConfig.command,
|
|
22
|
+
args: this.serverConfig.args || [],
|
|
23
|
+
env: { ...(this.serverConfig.env || {}) },
|
|
24
|
+
cwd: this.serverConfig.cwd,
|
|
25
|
+
stderr: 'pipe'
|
|
50
26
|
});
|
|
51
27
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
}
|
|
28
|
+
this.client = new Client({
|
|
29
|
+
name: 'ai-helper-agent',
|
|
30
|
+
version: '1.3.4'
|
|
64
31
|
});
|
|
65
32
|
|
|
66
|
-
|
|
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);
|
|
33
|
+
await this.client.connect(this.transport);
|
|
72
34
|
|
|
73
|
-
|
|
74
|
-
|
|
35
|
+
this.serverInfo = {
|
|
36
|
+
serverInfo: this.client.getServerVersion(),
|
|
37
|
+
capabilities: this.client.getServerCapabilities()
|
|
38
|
+
};
|
|
39
|
+
console.log(`[MCP:${this.serverId}] Initialize 成功, server:`, this.serverInfo.serverInfo?.name);
|
|
75
40
|
|
|
76
|
-
|
|
77
|
-
const toolsResult = await this._sendRequest('tools/list', {});
|
|
41
|
+
const toolsResult = await this.client.listTools({});
|
|
78
42
|
if (toolsResult && toolsResult.tools) {
|
|
79
43
|
this.tools = toolsResult.tools;
|
|
80
44
|
console.log(`[MCP:${this.serverId}] 发现 ${this.tools.length} 个工具:`,
|
|
@@ -92,18 +56,13 @@ export class McpClient {
|
|
|
92
56
|
}
|
|
93
57
|
}
|
|
94
58
|
|
|
95
|
-
/**
|
|
96
|
-
* 调用 MCP 工具
|
|
97
|
-
* @param {string} toolName
|
|
98
|
-
* @param {Object} args
|
|
99
|
-
*/
|
|
100
59
|
async callTool(toolName, args) {
|
|
101
60
|
if (!this.connected) {
|
|
102
61
|
return { success: false, error: `MCP Server "${this.serverName}" 未连接` };
|
|
103
62
|
}
|
|
104
63
|
|
|
105
64
|
try {
|
|
106
|
-
const result = await this.
|
|
65
|
+
const result = await this.client.callTool({
|
|
107
66
|
name: toolName,
|
|
108
67
|
arguments: args || {}
|
|
109
68
|
});
|
|
@@ -112,7 +71,6 @@ export class McpClient {
|
|
|
112
71
|
return { success: false, error: result.error.message || '工具调用失败' };
|
|
113
72
|
}
|
|
114
73
|
|
|
115
|
-
// MCP tools/call 返回 { content: [{ type: 'text', text: '...' }], isError: false }
|
|
116
74
|
const content = result?.content || [];
|
|
117
75
|
const textContent = content
|
|
118
76
|
.filter(c => c.type === 'text')
|
|
@@ -129,31 +87,18 @@ export class McpClient {
|
|
|
129
87
|
}
|
|
130
88
|
}
|
|
131
89
|
|
|
132
|
-
/**
|
|
133
|
-
* 断开连接
|
|
134
|
-
*/
|
|
135
90
|
disconnect() {
|
|
136
91
|
this.connected = false;
|
|
137
92
|
this.tools = [];
|
|
138
93
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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;
|
|
94
|
+
if (this.client) {
|
|
95
|
+
try { this.client.close(); } catch {}
|
|
96
|
+
this.client = null;
|
|
149
97
|
}
|
|
150
98
|
|
|
151
99
|
console.log(`[MCP:${this.serverId}] 已断开连接`);
|
|
152
100
|
}
|
|
153
101
|
|
|
154
|
-
/**
|
|
155
|
-
* 获取状态信息
|
|
156
|
-
*/
|
|
157
102
|
getStatus() {
|
|
158
103
|
return {
|
|
159
104
|
id: this.serverId,
|
|
@@ -163,82 +108,4 @@ export class McpClient {
|
|
|
163
108
|
serverInfo: this.serverInfo?.serverInfo || null
|
|
164
109
|
};
|
|
165
110
|
}
|
|
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
|
-
}
|
|
111
|
+
}
|
package/src/security.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
// agent/src/security.js - 安全管控:文件沙箱 + 命令分级
|
|
2
|
-
import { resolve, normalize, sep, isAbsolute, join } from 'path';
|
|
2
|
+
import { resolve, normalize, sep, isAbsolute, join, dirname } from 'path';
|
|
3
3
|
import { realpathSync } from 'fs';
|
|
4
|
-
import { homedir } from 'os';
|
|
4
|
+
import { homedir, platform } from 'os';
|
|
5
5
|
import { loadConfig } from './config.js';
|
|
6
6
|
|
|
7
|
+
const isWin32 = platform() === 'win32';
|
|
8
|
+
|
|
9
|
+
function normalizePathFormat(pathStr) {
|
|
10
|
+
if (!pathStr || typeof pathStr !== 'string') return pathStr;
|
|
11
|
+
|
|
12
|
+
if (isWin32) {
|
|
13
|
+
pathStr = pathStr.replace(/\/([a-zA-Z])\//g, '$1:/');
|
|
14
|
+
pathStr = pathStr.replace(/\/([a-zA-Z]):/g, '$1:/');
|
|
15
|
+
pathStr = pathStr.replace(/\//g, '\\');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return pathStr;
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
// ==================== 硬阻止目录(任何情况下都不可访问) ====================
|
|
8
22
|
|
|
9
23
|
const AGENT_DIR = join(homedir(), '.ai-helper-agent');
|
|
@@ -49,8 +63,10 @@ function checkPath(pathStr) {
|
|
|
49
63
|
try {
|
|
50
64
|
const config = loadConfig();
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
const
|
|
66
|
+
const normalizedPathStr = normalizePathFormat(pathStr);
|
|
67
|
+
const normalizedWorkdir = normalizePathFormat(config.workdir);
|
|
68
|
+
|
|
69
|
+
const resolved = isAbsolute(normalizedPathStr) ? resolve(normalizedPathStr) : resolve(normalizedWorkdir, normalizedPathStr);
|
|
54
70
|
const normalized = normalize(resolved);
|
|
55
71
|
|
|
56
72
|
// 0. 硬阻止检查(优先级最高,不可绕过)
|
|
@@ -58,13 +74,12 @@ function checkPath(pathStr) {
|
|
|
58
74
|
return { allowed: false, reason: '禁止访问代理系统文件' };
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
const allowedPaths = config.allowedPaths.length > 0 ? config.allowedPaths : [
|
|
77
|
+
const allowedPaths = config.allowedPaths.length > 0 ? config.allowedPaths.map(p => normalizePathFormat(p)) : [normalizedWorkdir];
|
|
62
78
|
|
|
63
79
|
// 先做前缀检查(快速路径,兼容 Windows/Unix)
|
|
64
80
|
let prefixMatch = false;
|
|
65
81
|
for (const allowed of allowedPaths) {
|
|
66
82
|
const allowedResolved = resolve(allowed);
|
|
67
|
-
// 统一使用 OS 分隔符构造前缀
|
|
68
83
|
const prefix = allowedResolved.endsWith(sep) ? allowedResolved : allowedResolved + sep;
|
|
69
84
|
if (normalized.startsWith(prefix) || normalized === allowedResolved) {
|
|
70
85
|
prefixMatch = true;
|
|
@@ -81,10 +96,12 @@ function checkPath(pathStr) {
|
|
|
81
96
|
realPath = realpathSync(resolved);
|
|
82
97
|
} catch (err) {
|
|
83
98
|
if (err.code === 'ENOENT') {
|
|
84
|
-
// 文件/目录不存在时,向上查找直到找到存在的父目录
|
|
85
99
|
let existing = resolved;
|
|
86
|
-
while (
|
|
87
|
-
const parent =
|
|
100
|
+
while (true) {
|
|
101
|
+
const parent = dirname(existing);
|
|
102
|
+
if (parent === existing) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
88
105
|
try {
|
|
89
106
|
realPath = realpathSync(parent) + sep + normalized.slice(resolve(parent).length).replace(/^[/\\]/, '');
|
|
90
107
|
break;
|
package/src/server.js
CHANGED
|
@@ -158,6 +158,85 @@ function parseBody(req) {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* 解析 multipart/form-data,提取文件数据
|
|
163
|
+
* @param {http.IncomingMessage} req
|
|
164
|
+
* @returns {Promise<{fileBuffer: Buffer, fileName: string, mimeType: string}>}
|
|
165
|
+
*/
|
|
166
|
+
function parseMultipartBody(req) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const contentType = req.headers['content-type'] || '';
|
|
169
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
170
|
+
if (!boundaryMatch) {
|
|
171
|
+
reject(new Error('不是有效的 multipart/form-data 请求'));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const boundary = boundaryMatch[1].trim();
|
|
175
|
+
const boundaryBuffer = Buffer.from('--' + boundary);
|
|
176
|
+
|
|
177
|
+
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
178
|
+
if (contentLength > MAX_BODY_SIZE) {
|
|
179
|
+
reject(new Error('文件过大,超出限制'));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const chunks = [];
|
|
184
|
+
let totalSize = 0;
|
|
185
|
+
|
|
186
|
+
req.on('data', (chunk) => {
|
|
187
|
+
totalSize += chunk.length;
|
|
188
|
+
if (totalSize > MAX_BODY_SIZE) {
|
|
189
|
+
reject(new Error('文件过大,超出限制'));
|
|
190
|
+
req.destroy();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
chunks.push(chunk);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
req.on('end', () => {
|
|
197
|
+
const buffer = Buffer.concat(chunks);
|
|
198
|
+
|
|
199
|
+
// 找到文件部分的起始位置
|
|
200
|
+
const startIdx = buffer.indexOf(boundaryBuffer) + boundaryBuffer.length + 2; // +2 for \r\n
|
|
201
|
+
if (startIdx < boundaryBuffer.length + 2) {
|
|
202
|
+
reject(new Error('multipart 数据格式不正确'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 找 headers 结束位置(\r\n\r\n)
|
|
207
|
+
const headersEnd = buffer.indexOf('\r\n\r\n', startIdx);
|
|
208
|
+
if (headersEnd === -1) {
|
|
209
|
+
reject(new Error('multipart headers 格式不正确'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 解析 headers
|
|
214
|
+
const headersStr = buffer.slice(startIdx, headersEnd).toString();
|
|
215
|
+
const dispMatch = headersStr.match(/filename="([^"]*)"/);
|
|
216
|
+
const fileName = dispMatch ? dispMatch[1] : 'file';
|
|
217
|
+
|
|
218
|
+
// 找数据结束位置(下一个 boundary)
|
|
219
|
+
const dataStart = headersEnd + 4; // after \r\n\r\n
|
|
220
|
+
const dataEnd = buffer.indexOf(boundaryBuffer, dataStart) - 2; // -2 for \r\n before boundary
|
|
221
|
+
|
|
222
|
+
if (dataEnd <= dataStart) {
|
|
223
|
+
reject(new Error('找不到文件数据'));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const fileBuffer = buffer.slice(dataStart, dataEnd);
|
|
228
|
+
|
|
229
|
+
resolve({
|
|
230
|
+
fileBuffer,
|
|
231
|
+
fileName,
|
|
232
|
+
mimeType: headersStr.match(/Content-Type:\s*(.+?)\r?\n/i)?.[1]?.trim() || 'application/octet-stream'
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
req.on('error', () => reject(new Error('读取文件失败')));
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
161
240
|
/**
|
|
162
241
|
* 创建并启动服务器
|
|
163
242
|
*/
|
|
@@ -271,6 +350,47 @@ export function startServer() {
|
|
|
271
350
|
});
|
|
272
351
|
}
|
|
273
352
|
|
|
353
|
+
// === 文件上传(multipart/form-data,必须在通用 JSON body 解析之前) ===
|
|
354
|
+
if (req.method === 'POST' && pathname === '/api/files/upload') {
|
|
355
|
+
try {
|
|
356
|
+
const { fileBuffer, fileName, mimeType } = await parseMultipartBody(req);
|
|
357
|
+
|
|
358
|
+
// 安全检查:过滤文件名中的危险字符
|
|
359
|
+
const safeName = fileName.replace(/[/\\:*?"<>|]/g, '_');
|
|
360
|
+
const destPath = join(config.workdir, safeName);
|
|
361
|
+
|
|
362
|
+
// 去重:如果文件已存在,添加后缀
|
|
363
|
+
let finalPath = destPath;
|
|
364
|
+
if (existsSync(finalPath)) {
|
|
365
|
+
const ext = safeName.lastIndexOf('.') > 0 ? safeName.substring(safeName.lastIndexOf('.')) : '';
|
|
366
|
+
const base = safeName.substring(0, safeName.length - ext.length) || safeName;
|
|
367
|
+
let counter = 1;
|
|
368
|
+
while (existsSync(finalPath)) {
|
|
369
|
+
finalPath = join(config.workdir, `${base}_${counter}${ext}`);
|
|
370
|
+
counter++;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 确保工作目录存在
|
|
375
|
+
mkdirSync(config.workdir, { recursive: true });
|
|
376
|
+
|
|
377
|
+
// 写入文件
|
|
378
|
+
writeFileSync(finalPath, fileBuffer);
|
|
379
|
+
|
|
380
|
+
logFs('upload', { path: finalPath, size: fileBuffer.length, mimeType });
|
|
381
|
+
|
|
382
|
+
return jsonResponse(res, 200, {
|
|
383
|
+
success: true,
|
|
384
|
+
path: finalPath,
|
|
385
|
+
name: safeName,
|
|
386
|
+
size: fileBuffer.length
|
|
387
|
+
});
|
|
388
|
+
} catch (err) {
|
|
389
|
+
logError('fs', 'upload_error', { error: err.message });
|
|
390
|
+
return jsonResponse(res, 400, { success: false, error: `文件上传失败: ${err.message}` });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
274
394
|
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
|
|
275
395
|
let body;
|
|
276
396
|
try { body = await parseBody(req); }
|
|
@@ -106,10 +106,11 @@ enabled: true
|
|
|
106
106
|
- **Windows (PowerShell)**:\`New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\\.ai-helper-agent\\skills\\<skill-name>"\`
|
|
107
107
|
- 如果 \`mkdir\` 报错,尝试不带参数或使用 \`agent_write_file\` 工具在目标路径写入一个临时文件,让系统自动创建父目录。
|
|
108
108
|
|
|
109
|
-
5. **写入 SKILL.md**:使用 \`agent_write_file\`
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
5. **写入 SKILL.md**:使用 \`agent_write_file\` 工具将内容写入目标文件。
|
|
110
|
+
|
|
111
|
+
> ⚠️ **关键限制**:\`agent_write_file\` 不接受 \`~\` 或相对路径,**必须传入绝对路径**。
|
|
112
|
+
|
|
113
|
+
**推荐做法**:先用 \`agent_exec_command\` 获取用户主目录的绝对路径,再拼接完整路径。
|
|
113
114
|
|
|
114
115
|
6. **告知用户**:创建完成后,告知用户技能已就绪,说明触发关键词。建议用户在工具箱页面点击「重新加载」使技能生效。
|
|
115
116
|
|
package/src/mcp/transport.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// mcp/transport.js - MCP stdio 传输层
|
|
2
|
-
// MCP 协议使用 JSON-RPC 2.0 over stdin/stdout,每行一个 JSON 消息
|
|
3
|
-
import { spawn } from 'child_process';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 创建 MCP 子进程传输
|
|
7
|
-
* MCP 协议中,客户端先发 initialize 请求,服务端才会响应。因此 spawn 后立即 resolve,
|
|
8
|
-
* 由上层 McpClient 负责发送 initialize 和等待响应。
|
|
9
|
-
*
|
|
10
|
-
* @param {Object} serverConfig - { command, args, env }
|
|
11
|
-
* @returns {Promise<{send: Function, close: Function, onData: Function}>}
|
|
12
|
-
*/
|
|
13
|
-
export function createStdioTransport(serverConfig) {
|
|
14
|
-
const { command, args = [], env = {} } = serverConfig;
|
|
15
|
-
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
let resolved = false;
|
|
18
|
-
let responseBuffer = '';
|
|
19
|
-
let dataHandler = null;
|
|
20
|
-
let exitHandler = null;
|
|
21
|
-
|
|
22
|
-
const child = spawn(command, args, {
|
|
23
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
-
env: { ...process.env, ...env },
|
|
25
|
-
detached: false
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const cleanup = () => {
|
|
29
|
-
if (child.stdout) child.stdout.removeAllListeners();
|
|
30
|
-
if (child.stderr) child.stderr.removeAllListeners();
|
|
31
|
-
child.removeAllListeners();
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
child.stderr.on('data', (data) => {
|
|
35
|
-
const text = data.toString();
|
|
36
|
-
if (text.trim()) {
|
|
37
|
-
console.log(`[MCP:stderr] [${serverConfig.id || command}] ${text.trim()}`);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
child.on('error', (err) => {
|
|
42
|
-
if (!resolved) {
|
|
43
|
-
resolved = true;
|
|
44
|
-
cleanup();
|
|
45
|
-
reject(new Error(`无法启动 MCP Server "${command}": ${err.message}`));
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
child.on('exit', (code, signal) => {
|
|
50
|
-
console.log(`[MCP] Server 进程退出: ${command} (code=${code}, signal=${signal})`);
|
|
51
|
-
if (!resolved) {
|
|
52
|
-
resolved = true;
|
|
53
|
-
cleanup();
|
|
54
|
-
reject(new Error(`MCP Server 异常退出 (code=${code}, signal=${signal}): ${command}`));
|
|
55
|
-
} else if (exitHandler) {
|
|
56
|
-
exitHandler(code, signal);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
child.stdout.on('data', (data) => {
|
|
61
|
-
responseBuffer += data.toString();
|
|
62
|
-
|
|
63
|
-
let newlineIdx;
|
|
64
|
-
while ((newlineIdx = responseBuffer.indexOf('\n')) !== -1) {
|
|
65
|
-
const line = responseBuffer.slice(0, newlineIdx).trim();
|
|
66
|
-
responseBuffer = responseBuffer.slice(newlineIdx + 1);
|
|
67
|
-
|
|
68
|
-
if (!line) continue;
|
|
69
|
-
|
|
70
|
-
if (dataHandler) {
|
|
71
|
-
try { dataHandler(line); } catch {}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (!resolved) {
|
|
77
|
-
resolved = true;
|
|
78
|
-
resolve({
|
|
79
|
-
send: (jsonRpcMessage) => {
|
|
80
|
-
const msg = JSON.stringify(jsonRpcMessage) + '\n';
|
|
81
|
-
child.stdin.write(msg);
|
|
82
|
-
},
|
|
83
|
-
close: () => {
|
|
84
|
-
cleanup();
|
|
85
|
-
dataHandler = null;
|
|
86
|
-
try { child.stdin.end(); } catch {}
|
|
87
|
-
setTimeout(() => {
|
|
88
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
89
|
-
}, 1000);
|
|
90
|
-
},
|
|
91
|
-
onData: (handler) => {
|
|
92
|
-
dataHandler = handler;
|
|
93
|
-
},
|
|
94
|
-
onExit: (handler) => {
|
|
95
|
-
exitHandler = handler;
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
}
|