ai-helper-agent 1.7.0 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-helper-agent",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "AI Helper 代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
package/src/config.js CHANGED
@@ -8,6 +8,7 @@ const CONFIG_FILE = join(AGENT_DIR, 'config.json');
8
8
  const PAIRINGS_FILE = join(AGENT_DIR, 'pairings.json');
9
9
  const SKILLS_DIR = join(AGENT_DIR, 'skills');
10
10
  const WORKSPACE_DIR = join(AGENT_DIR, 'workspace');
11
+ const MEMORY_DIR = join(AGENT_DIR, 'memory');
11
12
 
12
13
  const DEFAULTS = {
13
14
  port: 18910,
@@ -44,8 +45,8 @@ function ensureAgentDir() {
44
45
  if (!existsSync(AGENT_DIR)) {
45
46
  mkdirSync(AGENT_DIR, { recursive: true });
46
47
  }
47
- // 确保 skills 和 workspace 子目录存在
48
- for (const dir of [SKILLS_DIR, WORKSPACE_DIR]) {
48
+ // 确保 skills、workspace 和 memory 子目录存在
49
+ for (const dir of [SKILLS_DIR, WORKSPACE_DIR, MEMORY_DIR]) {
49
50
  if (!existsSync(dir)) {
50
51
  mkdirSync(dir, { recursive: true });
51
52
  }
@@ -198,4 +199,4 @@ export async function savePairing(extensionId, token) {
198
199
  });
199
200
  }
200
201
 
201
- export { AGENT_DIR, CONFIG_FILE };
202
+ export { AGENT_DIR, CONFIG_FILE, MEMORY_DIR };
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 = 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'
26
- });
82
+ this.transport = this._createTransport();
27
83
 
28
84
  this.client = new Client({
29
85
  name: 'ai-helper-agent',
package/src/security.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { resolve, normalize, sep, isAbsolute, join, dirname } from 'path';
3
3
  import { realpathSync } from 'fs';
4
4
  import { homedir, platform } from 'os';
5
- import { loadConfig } from './config.js';
5
+ import { loadConfig, MEMORY_DIR } from './config.js';
6
6
 
7
7
  const isWin32 = platform() === 'win32';
8
8
 
@@ -60,6 +60,13 @@ function checkPath(pathStr) {
60
60
  return { allowed: false, reason: '路径无效' };
61
61
  }
62
62
 
63
+ // 展开 ~ 到用户主目录(扩展端无法知道 Agent 所在机器的 home 路径)
64
+ if (pathStr === '~') {
65
+ pathStr = homedir();
66
+ } else if (pathStr.startsWith('~/')) {
67
+ pathStr = join(homedir(), pathStr.slice(2));
68
+ }
69
+
63
70
  try {
64
71
  const config = loadConfig();
65
72
 
@@ -74,6 +81,12 @@ function checkPath(pathStr) {
74
81
  return { allowed: false, reason: '禁止访问代理系统文件' };
75
82
  }
76
83
 
84
+ // 0.5. 系统目录白名单:记忆存储目录(Agent 内部管理,可读写)
85
+ const memoryPrefix = normalize(join(MEMORY_DIR, sep));
86
+ if (normalized.startsWith(memoryPrefix) || normalized === normalize(MEMORY_DIR)) {
87
+ return { allowed: true, resolved: normalized };
88
+ }
89
+
77
90
  const allowedPaths = config.allowedPaths.length > 0 ? config.allowedPaths.map(p => normalizePathFormat(p)) : [normalizedWorkdir];
78
91
 
79
92
  // 先做前缀检查(快速路径,兼容 Windows/Unix)
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
- if (!body.id || !body.command) {
773
- return jsonResponse(res, 400, { success: false, error: '缺少必要参数: id, command' });
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 });
@@ -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: skill.fullPrompt || skill.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] 持久化 "${name}" 的启用状态失败:`, err.message);
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 };