ai-helper-agent 1.8.0 → 1.9.1
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 +3 -2
- package/src/mcp/client.js +63 -7
- package/src/server.js +9 -2
- package/src/skill/registry.js +57 -7
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-helper-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "AI Helper 代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"ai-helper-agent": "./bin/agent.js"
|
|
8
|
+
"ai-helper-agent": "./bin/agent.js",
|
|
9
|
+
"aha": "./bin/agent.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"bin/",
|
package/src/mcp/client.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client';
|
|
2
2
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
4
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
+
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
|
|
6
|
+
|
|
7
|
+
// 支持的传输协议类型
|
|
8
|
+
const TRANSPORT_STDIO = 'stdio';
|
|
9
|
+
const TRANSPORT_SSE = 'sse';
|
|
10
|
+
const TRANSPORT_STREAMABLE_HTTP = 'streamableHttp';
|
|
11
|
+
const TRANSPORT_WEBSOCKET = 'websocket';
|
|
3
12
|
|
|
4
13
|
export class McpClient {
|
|
5
14
|
constructor(serverConfig) {
|
|
@@ -13,17 +22,64 @@ export class McpClient {
|
|
|
13
22
|
this.serverInfo = null;
|
|
14
23
|
}
|
|
15
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 根据配置创建对应的传输层实例
|
|
27
|
+
*/
|
|
28
|
+
_createTransport() {
|
|
29
|
+
const transportType = this.serverConfig.transport || TRANSPORT_STDIO;
|
|
30
|
+
const headers = this.serverConfig.headers || {};
|
|
31
|
+
|
|
32
|
+
switch (transportType) {
|
|
33
|
+
case TRANSPORT_SSE: {
|
|
34
|
+
const url = this.serverConfig.url;
|
|
35
|
+
if (!url) throw new Error('SSE 传输需要提供 url');
|
|
36
|
+
console.log(`[MCP:${this.serverId}] 使用 SSE 传输,URL: ${url}`);
|
|
37
|
+
const opts = {};
|
|
38
|
+
if (Object.keys(headers).length > 0) {
|
|
39
|
+
opts.requestInit = { headers };
|
|
40
|
+
}
|
|
41
|
+
return new SSEClientTransport(new URL(url), opts);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case TRANSPORT_STREAMABLE_HTTP: {
|
|
45
|
+
const url = this.serverConfig.url;
|
|
46
|
+
if (!url) throw new Error('StreamableHTTP 传输需要提供 url');
|
|
47
|
+
console.log(`[MCP:${this.serverId}] 使用 StreamableHTTP 传输,URL: ${url}`);
|
|
48
|
+
const opts = {};
|
|
49
|
+
if (Object.keys(headers).length > 0) {
|
|
50
|
+
opts.requestInit = { headers };
|
|
51
|
+
}
|
|
52
|
+
return new StreamableHTTPClientTransport(new URL(url), opts);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case TRANSPORT_WEBSOCKET: {
|
|
56
|
+
const url = this.serverConfig.url;
|
|
57
|
+
if (!url) throw new Error('WebSocket 传输需要提供 url');
|
|
58
|
+
console.log(`[MCP:${this.serverId}] 使用 WebSocket 传输,URL: ${url}`);
|
|
59
|
+
return new WebSocketClientTransport(new URL(url));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case TRANSPORT_STDIO:
|
|
63
|
+
default: {
|
|
64
|
+
const command = this.serverConfig.command;
|
|
65
|
+
if (!command) throw new Error('stdio 传输需要提供 command');
|
|
66
|
+
console.log(`[MCP:${this.serverId}] 使用 stdio 传输,命令: ${command}`);
|
|
67
|
+
return new StdioClientTransport({
|
|
68
|
+
command: this.serverConfig.command,
|
|
69
|
+
args: this.serverConfig.args || [],
|
|
70
|
+
env: { ...(this.serverConfig.env || {}) },
|
|
71
|
+
cwd: this.serverConfig.cwd,
|
|
72
|
+
stderr: 'pipe'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
16
78
|
async connect() {
|
|
17
79
|
if (this.connected) return { success: true };
|
|
18
80
|
|
|
19
81
|
try {
|
|
20
|
-
this.transport =
|
|
21
|
-
command: this.serverConfig.command,
|
|
22
|
-
args: this.serverConfig.args || [],
|
|
23
|
-
env: { ...(this.serverConfig.env || {}) },
|
|
24
|
-
cwd: this.serverConfig.cwd,
|
|
25
|
-
stderr: 'pipe'
|
|
26
|
-
});
|
|
82
|
+
this.transport = this._createTransport();
|
|
27
83
|
|
|
28
84
|
this.client = new Client({
|
|
29
85
|
name: 'ai-helper-agent',
|
package/src/server.js
CHANGED
|
@@ -769,8 +769,15 @@ export function startServer() {
|
|
|
769
769
|
|
|
770
770
|
// 添加 MCP 服务器
|
|
771
771
|
if (pathname === '/api/mcp/servers' && req.method === 'POST') {
|
|
772
|
-
|
|
773
|
-
|
|
772
|
+
const isHttp = body.transport === 'sse' || body.transport === 'streamableHttp' || body.transport === 'websocket';
|
|
773
|
+
if (!body.id) {
|
|
774
|
+
return jsonResponse(res, 400, { success: false, error: '缺少必要参数: id' });
|
|
775
|
+
}
|
|
776
|
+
if (isHttp && !body.url) {
|
|
777
|
+
return jsonResponse(res, 400, { success: false, error: 'HTTP 传输需要提供 url' });
|
|
778
|
+
}
|
|
779
|
+
if (!isHttp && !body.command) {
|
|
780
|
+
return jsonResponse(res, 400, { success: false, error: 'stdio 传输需要提供 command' });
|
|
774
781
|
}
|
|
775
782
|
const result = await addMcpServer(body);
|
|
776
783
|
logSystem('mcp_server_add', { serverId: body.id });
|
package/src/skill/registry.js
CHANGED
|
@@ -12,6 +12,35 @@ const skills = new Map();
|
|
|
12
12
|
// 内置技能状态持久化文件
|
|
13
13
|
const BUILTIN_STATE_FILE = join(dirname(SKILLS_DIR), 'builtin_skills_state.json');
|
|
14
14
|
|
|
15
|
+
// 用户停用技能状态持久化文件(比直接写 SKILL.md 更可靠,避免文件写入失败导致状态丢失)
|
|
16
|
+
const DISABLED_STATE_FILE = join(dirname(SKILLS_DIR), 'disabled_skills.json');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 读取用户停用的技能列表
|
|
20
|
+
* @returns {Set<string>}
|
|
21
|
+
*/
|
|
22
|
+
function loadDisabledState() {
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(DISABLED_STATE_FILE)) {
|
|
25
|
+
const data = JSON.parse(readFileSync(DISABLED_STATE_FILE, 'utf-8'));
|
|
26
|
+
return new Set(data.disabled || []);
|
|
27
|
+
}
|
|
28
|
+
} catch (e) { /* 忽略 */ }
|
|
29
|
+
return new Set();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 保存用户停用的技能列表
|
|
34
|
+
* @param {Set<string>} disabledSet
|
|
35
|
+
*/
|
|
36
|
+
function saveDisabledState(disabledSet) {
|
|
37
|
+
try {
|
|
38
|
+
writeFileSync(DISABLED_STATE_FILE, JSON.stringify({ disabled: [...disabledSet] }, null, 2), 'utf-8');
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error('[Skill Registry] 无法保存停用状态:', e.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
/**
|
|
16
45
|
* 读取内置技能的启用状态
|
|
17
46
|
* @returns {Record<string, boolean>} name → enabled
|
|
@@ -83,6 +112,15 @@ export function initializeSkillRegistry() {
|
|
|
83
112
|
}
|
|
84
113
|
}
|
|
85
114
|
|
|
115
|
+
// 5. 恢复用户手动停用的技能状态(所有类型,比 SKILL.md frontmatter 更可靠)
|
|
116
|
+
const disabledSet = loadDisabledState();
|
|
117
|
+
for (const name of disabledSet) {
|
|
118
|
+
const skill = skills.get(name);
|
|
119
|
+
if (skill) {
|
|
120
|
+
skill.enabled = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
86
124
|
const workflowCount = [...skills.values()].filter(s => s.type === 'workflow').length;
|
|
87
125
|
const agentCount = [...skills.values()].filter(s => s.type === 'agent').length;
|
|
88
126
|
const enabledCount = [...skills.values()].filter(s => s.enabled !== false).length;
|
|
@@ -200,6 +238,11 @@ export function getAgentSkillPrompt(name) {
|
|
|
200
238
|
if (!skill || skill.type !== 'agent') {
|
|
201
239
|
return { success: false, error: `Skill "${name}" 不存在或不是 Agent Skill` };
|
|
202
240
|
}
|
|
241
|
+
// 如果 skill 有 dirPath,在 prompt 前注入工作目录信息,方便 AI 解析相对路径引用
|
|
242
|
+
let prompt = skill.fullPrompt || skill.prompt || '';
|
|
243
|
+
if (skill.dirPath) {
|
|
244
|
+
prompt = `[技能工作目录: ${skill.dirPath}]\n\n${prompt}`;
|
|
245
|
+
}
|
|
203
246
|
return {
|
|
204
247
|
success: true,
|
|
205
248
|
skill: {
|
|
@@ -208,7 +251,7 @@ export function getAgentSkillPrompt(name) {
|
|
|
208
251
|
version: skill.version,
|
|
209
252
|
resources: skill.resources
|
|
210
253
|
},
|
|
211
|
-
prompt
|
|
254
|
+
prompt
|
|
212
255
|
};
|
|
213
256
|
}
|
|
214
257
|
|
|
@@ -271,17 +314,25 @@ export function toggleSkill(name) {
|
|
|
271
314
|
const newEnabled = skill.enabled === false;
|
|
272
315
|
skill.enabled = newEnabled;
|
|
273
316
|
|
|
274
|
-
//
|
|
317
|
+
// 1. 持久化到 disabled_skills.json(所有类型统一使用,比直接写 SKILL.md 更可靠)
|
|
318
|
+
const disabledSet = loadDisabledState();
|
|
319
|
+
if (newEnabled) {
|
|
320
|
+
disabledSet.delete(name);
|
|
321
|
+
} else {
|
|
322
|
+
disabledSet.add(name);
|
|
323
|
+
}
|
|
324
|
+
saveDisabledState(disabledSet);
|
|
325
|
+
|
|
326
|
+
// 2. 内置技能额外保存到 builtin_skills_state.json
|
|
275
327
|
if (skill.builtin) {
|
|
276
328
|
const state = loadBuiltinState();
|
|
277
329
|
state[name] = newEnabled;
|
|
278
330
|
saveBuiltinState(state);
|
|
279
|
-
return { success: true, enabled: newEnabled };
|
|
280
331
|
}
|
|
281
332
|
|
|
333
|
+
// 3. 尝试更新技能源文件(best-effort,失败不影响状态)
|
|
282
334
|
try {
|
|
283
335
|
if (skill.type === 'agent' && skill.skillMdPath) {
|
|
284
|
-
// 更新 SKILL.md 的 frontmatter
|
|
285
336
|
const content = readFileSync(skill.skillMdPath, 'utf-8');
|
|
286
337
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
287
338
|
if (frontmatterMatch) {
|
|
@@ -295,15 +346,14 @@ export function toggleSkill(name) {
|
|
|
295
346
|
writeFileSync(skill.skillMdPath, newContent, 'utf-8');
|
|
296
347
|
}
|
|
297
348
|
} else if (skill.type === 'workflow' && skill._filePath) {
|
|
298
|
-
// 更新 JSON 文件
|
|
299
349
|
const content = readFileSync(skill._filePath, 'utf-8');
|
|
300
350
|
const parsed = JSON.parse(content);
|
|
301
351
|
parsed.enabled = newEnabled;
|
|
302
352
|
writeFileSync(skill._filePath, JSON.stringify(parsed, null, 2), 'utf-8');
|
|
303
353
|
}
|
|
304
354
|
} catch (err) {
|
|
305
|
-
//
|
|
306
|
-
console.warn(`[Skill Registry]
|
|
355
|
+
// 写源文件失败不影响状态,disabled_skills.json 已经持久化
|
|
356
|
+
console.warn(`[Skill Registry] 更新 "${name}" 源文件失败(状态已通过 disabled_skills.json 保存):`, err.message);
|
|
307
357
|
}
|
|
308
358
|
|
|
309
359
|
return { success: true, enabled: newEnabled };
|