@ian2018cs/agenthub 0.2.7 → 0.2.9

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": "@ian2018cs/agenthub",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "A web-based UI for AI Agents",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -511,8 +511,8 @@ async function loadMcpConfig(cwd, userUuid) {
511
511
  }
512
512
 
513
513
  // Add/override with project-specific MCP servers
514
- if (claudeConfig.claudeProjects && cwd) {
515
- const projectConfig = claudeConfig.claudeProjects[cwd];
514
+ if (claudeConfig.projects && cwd) {
515
+ const projectConfig = claudeConfig.projects[cwd];
516
516
  if (projectConfig && projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
517
517
  mcpServers = { ...mcpServers, ...projectConfig.mcpServers };
518
518
  console.log(`Loaded ${Object.keys(projectConfig.mcpServers).length} project-specific MCP servers`);
@@ -83,6 +83,7 @@ export const cronCreateTool = {
83
83
 
84
84
  ### 创建定时任务(**必须用文件方式**,避免 prompt 含特殊字符时解析失败)
85
85
 
86
+
86
87
  **步骤一**:用 Write 工具把参数写入临时文件(纯文本,无需 JSON,文件名用时间戳保证唯一):
87
88
  \`\`\`
88
89
  Write /tmp/<userUuid>/cron/task_<timestamp>.txt
@@ -126,6 +127,31 @@ Bash(__cron_create__ /tmp/abc123/cron/task_1700000000.txt)
126
127
 
127
128
  所有执行均在服务端完成,不依赖用户是否在线。每用户最多 ${MAX_PER_USER} 个并发任务。
128
129
 
130
+ ### 更新定时任务(**必须用文件方式**,留空行表示保留原值)
131
+
132
+ **步骤一**:把参数写入临时文件:
133
+ \`\`\`
134
+ Write /tmp/<userUuid>/cron/update_<timestamp>.txt
135
+ <第1行: 任务ID(必填)>
136
+ <第2行: 新cron表达式,留空=不修改>
137
+ <第3行: true 或 false,留空=不修改>
138
+ <第4行起: 新prompt内容,留空=不修改,支持任意字符和换行>
139
+ \`\`\`
140
+
141
+ **示例**(只改频率,其余不动):
142
+ \`\`\`
143
+ Write /tmp/abc123/cron/update_1700000000.txt
144
+ a1b2c3d4
145
+ 0 8 * * *
146
+
147
+
148
+ \`\`\`
149
+
150
+ **步骤二**:把文件路径传给 __cron_update__:
151
+ \`\`\`
152
+ Bash(__cron_update__ /tmp/abc123/cron/update_1700000000.txt)
153
+ \`\`\`
154
+
129
155
  ### 删除定时任务
130
156
  \`\`\`
131
157
  Bash(__cron_delete__ '{"id":"任务ID"}')
@@ -317,11 +343,140 @@ export const cronListTool = {
317
343
  },
318
344
  };
319
345
 
346
+ // ─── __cron_update__ ──────────────────────────────────────────────────────────
347
+
348
+ export const cronUpdateTool = {
349
+ name: '__cron_update__',
350
+
351
+ systemPrompt: '',
352
+
353
+ match(hookInput) {
354
+ return hookInput.tool_name === 'Bash' &&
355
+ hookInput.tool_input?.command?.trimStart().startsWith('__cron_update__');
356
+ },
357
+
358
+ async execute(hookInput, ctx) {
359
+ const rawArgs = hookInput.tool_input.command.replace(/^\s*__cron_update__\s*/, '').trim();
360
+ const { userUuid, mutableWriter } = ctx;
361
+
362
+ let id, rawCron, rawRecurring, rawPrompt;
363
+
364
+ if (rawArgs.startsWith('/')) {
365
+ // 文件路径模式:第1行 ID,第2行 cron,第3行 recurring,第4行起 prompt
366
+ let fileContent;
367
+ try {
368
+ fileContent = fs.readFileSync(rawArgs, 'utf8');
369
+ } catch (e) {
370
+ return { decision: 'deny', reason: `读取参数文件失败: ${e.message}(路径: ${rawArgs})` };
371
+ } finally {
372
+ try { fs.unlinkSync(rawArgs); } catch {}
373
+ }
374
+ const lines = fileContent.split('\n');
375
+ id = lines[0]?.trim();
376
+ rawCron = lines[1]?.trim() || null;
377
+ rawRecurring = lines[2]?.trim() || null;
378
+ rawPrompt = lines.slice(3).join('\n').trim() || null;
379
+ } else {
380
+ // 内联 JSON 模式(兼容)
381
+ try {
382
+ const params = parseJsonArg(rawArgs);
383
+ id = params.id;
384
+ rawCron = params.cron || null;
385
+ rawRecurring = params.recurring !== undefined ? String(params.recurring) : null;
386
+ rawPrompt = params.prompt || null;
387
+ } catch (e) {
388
+ return { decision: 'deny', reason: `参数解析失败: ${e.message}。请改用文件方式:Write /tmp/<uuid>/cron/update_<ts>.txt,第1行任务ID,第2行新cron(空=不改),第3行true/false(空=不改),第4行起新prompt(空=不改)` };
389
+ }
390
+ }
391
+
392
+ if (!id) return { decision: 'deny', reason: '❌ 缺少任务 ID(文件第1行)' };
393
+
394
+ // 查找当前任务
395
+ const tasks = cronScheduler.list(userUuid);
396
+ const existing = tasks.find(t => t.id === id);
397
+ if (!existing) {
398
+ return { decision: 'deny', reason: `❌ 任务 ${id} 不存在或无权限修改` };
399
+ }
400
+
401
+ // 验证新 cron 表达式
402
+ const finalCron = rawCron || existing.cronExpr;
403
+ if (rawCron && !cron.validate(rawCron)) {
404
+ return { decision: 'deny', reason: `❌ 无效的 cron 表达式: ${rawCron}(需要标准 5 字段格式)` };
405
+ }
406
+
407
+ // 处理 recurring
408
+ let finalRecurring = existing.recurring;
409
+ if (rawRecurring !== null) {
410
+ finalRecurring = rawRecurring.toLowerCase() !== 'false';
411
+ }
412
+
413
+ // 处理 prompt 和 channel(prompt 变化则重新解析前缀)
414
+ let finalPrompt = existing.prompt;
415
+ let finalChannel = existing.channel;
416
+ let finalWebhookUrl = existing.webhookUrl;
417
+
418
+ if (rawPrompt) {
419
+ const feishuOpenId = mutableWriter?.current?.feishuOpenId || null;
420
+ const defaults = feishuOpenId
421
+ ? { sessionMode: existing.sessionMode || 'new_session', channel: existing.channel || 'feishu' }
422
+ : { sessionMode: existing.sessionMode || 'new_session', channel: existing.channel || '' };
423
+ const parsed = parsePrefixes(rawPrompt, defaults);
424
+ finalPrompt = parsed.prompt;
425
+ finalChannel = parsed.channel;
426
+ finalWebhookUrl = parsed.webhookUrl;
427
+
428
+ if (finalChannel === 'webhook' && !finalWebhookUrl) {
429
+ return { decision: 'deny', reason: '❌ webhook 渠道需要指定 URL,格式: [channel:webhook:https://...]' };
430
+ }
431
+ if (finalChannel === 'feishu') {
432
+ const binding = feishuDb.getBindingByUserUuid(userUuid);
433
+ if (!binding) {
434
+ return { decision: 'deny', reason: '❌ 当前账号未绑定飞书,无法使用飞书投递渠道' };
435
+ }
436
+ }
437
+ }
438
+
439
+ const patch = {
440
+ cronExpr: finalCron,
441
+ prompt: finalPrompt,
442
+ recurring: finalRecurring,
443
+ channel: finalChannel,
444
+ webhookUrl: finalWebhookUrl || null,
445
+ feishuOpenId: existing.feishuOpenId,
446
+ chatId: existing.chatId,
447
+ projectPath: existing.projectPath,
448
+ };
449
+
450
+ const updated = cronScheduler.update(id, userUuid, patch);
451
+ if (!updated) {
452
+ return { decision: 'deny', reason: `❌ 更新失败:任务 ${id} 不存在或无权限` };
453
+ }
454
+
455
+ const channelDesc = {
456
+ '': '服务端执行(存入会话历史)',
457
+ 'feishu': '飞书直投',
458
+ 'webhook': `Webhook POST → ${finalWebhookUrl}`,
459
+ }[finalChannel] ?? finalChannel;
460
+
461
+ return {
462
+ decision: 'deny',
463
+ reason: [
464
+ `✅ 定时任务 ${id} 已更新`,
465
+ `频率: ${finalCron}`,
466
+ `类型: ${finalRecurring ? '循环(永久运行)' : '单次(触发后删除)'}`,
467
+ `投递渠道: ${channelDesc}`,
468
+ `内容: ${finalPrompt}`,
469
+ ].join('\n'),
470
+ };
471
+ },
472
+ };
473
+
320
474
  // ─── SDK 原生工具重定向(拦截并引导 AI 使用 Bash 版本)─────────────────────────
321
475
 
322
476
  const SDK_CRON_REDIRECT = `❌ 请勿使用 SDK 内置的 CronCreate/CronDelete/CronList 工具,它们在本系统中无法持久化。
323
- 请改用以下 Bash 命令(必须用文件方式创建,避免 prompt 特殊字符导致解析失败):
477
+ 请改用以下 Bash 命令(必须用文件方式创建/更新,避免 prompt 特殊字符导致解析失败):
324
478
  - 创建:先 Write /tmp/<uuid>/cron/task_<ts>.txt(第1行cron表达式,第2行true/false,第3行起prompt),再 Bash(__cron_create__ /tmp/<uuid>/cron/task_<ts>.txt)
479
+ - 更新:先 Write /tmp/<uuid>/cron/update_<ts>.txt(第1行任务ID,第2行新cron,第3行true/false,第4行起新prompt,留空=不改),再 Bash(__cron_update__ /tmp/<uuid>/cron/update_<ts>.txt)
325
480
  - 删除:Bash(__cron_delete__ '{"id":"任务ID"}')
326
481
  - 列表:Bash(__cron_list__)`;
327
482
 
@@ -169,10 +169,11 @@ import backgroundTask, { bgStatusTool } from './background-task.js';
169
169
  registry.register(backgroundTask);
170
170
  registry.register(bgStatusTool);
171
171
 
172
- import { cronCreateTool, cronDeleteTool, cronListTool, sdkCronCreateRedirect, sdkCronDeleteRedirect, sdkCronListRedirect } from './cron-tool.js';
172
+ import { cronCreateTool, cronDeleteTool, cronListTool, cronUpdateTool, sdkCronCreateRedirect, sdkCronDeleteRedirect, sdkCronListRedirect } from './cron-tool.js';
173
173
  registry.register(cronCreateTool);
174
174
  registry.register(cronDeleteTool);
175
175
  registry.register(cronListTool);
176
+ registry.register(cronUpdateTool);
176
177
  // 拦截 SDK 原生 CronCreate/CronDelete/CronList,引导 AI 使用 Bash 版本
177
178
  registry.register(sdkCronCreateRedirect);
178
179
  registry.register(sdkCronDeleteRedirect);