@ian2018cs/agenthub 0.2.6 → 0.2.8
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,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import cron from 'node-cron';
|
|
3
4
|
import { cronScheduler } from './cron-scheduler.js';
|
|
4
5
|
import { feishuDb } from '../../database/db.js';
|
|
@@ -78,17 +79,36 @@ export const cronCreateTool = {
|
|
|
78
79
|
|
|
79
80
|
systemPrompt: `
|
|
80
81
|
## 定时任务(Cron)
|
|
81
|
-
使用以下 Bash
|
|
82
|
+
使用以下 Bash 命令操作定时任务(任务持久化到数据库,服务重启自动恢复,循环任务永久运行直到用户手动删除,**没有 7 天过期限制**):
|
|
82
83
|
|
|
83
|
-
###
|
|
84
|
+
### 创建定时任务(**必须用文件方式**,避免 prompt 含特殊字符时解析失败)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
**步骤一**:用 Write 工具把参数写入临时文件(纯文本,无需 JSON,文件名用时间戳保证唯一):
|
|
88
|
+
\`\`\`
|
|
89
|
+
Write /tmp/<userUuid>/cron/task_<timestamp>.txt
|
|
90
|
+
<第1行: cron 表达式>
|
|
91
|
+
<第2行: true 或 false(是否循环)>
|
|
92
|
+
<第3行起: prompt 内容,原样保留,支持任意字符和换行>
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
**示例**:
|
|
84
96
|
\`\`\`
|
|
85
|
-
|
|
97
|
+
Write /tmp/abc123/cron/task_1700000000.txt
|
|
98
|
+
30 9 * * *
|
|
99
|
+
true
|
|
100
|
+
[channel:feishu]每天查询前天广告收入,分析数据并发送到飞书群
|
|
101
|
+
\`\`\`
|
|
102
|
+
|
|
103
|
+
**步骤二**:把文件路径传给 __cron_create__:
|
|
104
|
+
\`\`\`
|
|
105
|
+
Bash(__cron_create__ /tmp/abc123/cron/task_1700000000.txt)
|
|
86
106
|
\`\`\`
|
|
87
107
|
|
|
88
108
|
参数说明:
|
|
89
|
-
- cron: 标准 5 字段 cron 表达式(如 "*/5 * * * *")
|
|
90
|
-
-
|
|
91
|
-
-
|
|
109
|
+
- 第1行 cron: 标准 5 字段 cron 表达式(如 "*/5 * * * *")
|
|
110
|
+
- 第2行 recurring: true=循环(永久),false=单次(触发后删除)
|
|
111
|
+
- 第3行起 prompt: 触发时执行的内容,开头可加前缀指定会话方式和投递渠道
|
|
92
112
|
|
|
93
113
|
### 前缀说明(**创建前务必询问用户**,在 prompt 开头使用,顺序任意,均可选):
|
|
94
114
|
|
|
@@ -107,6 +127,31 @@ Bash(__cron_create__ '{"cron":"0 9 * * 1-5","prompt":"分析昨日 git 提交","
|
|
|
107
127
|
|
|
108
128
|
所有执行均在服务端完成,不依赖用户是否在线。每用户最多 ${MAX_PER_USER} 个并发任务。
|
|
109
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
|
+
|
|
110
155
|
### 删除定时任务
|
|
111
156
|
\`\`\`
|
|
112
157
|
Bash(__cron_delete__ '{"id":"任务ID"}')
|
|
@@ -124,12 +169,41 @@ Bash(__cron_list__)
|
|
|
124
169
|
},
|
|
125
170
|
|
|
126
171
|
async execute(hookInput, ctx) {
|
|
127
|
-
const rawArgs = hookInput.tool_input.command.replace(/^\s*__cron_create__\s*/, '');
|
|
172
|
+
const rawArgs = hookInput.tool_input.command.replace(/^\s*__cron_create__\s*/, '').trim();
|
|
128
173
|
let params;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
174
|
+
|
|
175
|
+
if (rawArgs.startsWith('/')) {
|
|
176
|
+
// 文件路径模式:纯文本格式(无 JSON,彻底避免转义问题)
|
|
177
|
+
// 第1行: cron 表达式
|
|
178
|
+
// 第2行: recurring (true/false)
|
|
179
|
+
// 第3行起: prompt 内容(原样保留,支持任意字符和换行)
|
|
180
|
+
let fileContent;
|
|
181
|
+
try {
|
|
182
|
+
fileContent = fs.readFileSync(rawArgs, 'utf8');
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return { decision: 'deny', reason: `读取参数文件失败: ${e.message}(路径: ${rawArgs})` };
|
|
185
|
+
} finally {
|
|
186
|
+
try { fs.unlinkSync(rawArgs); } catch {}
|
|
187
|
+
}
|
|
188
|
+
const lines = fileContent.split('\n');
|
|
189
|
+
const cronExprLine = lines[0]?.trim();
|
|
190
|
+
const recurringLine = lines[1]?.trim().toLowerCase();
|
|
191
|
+
const promptText = lines.slice(2).join('\n').trim();
|
|
192
|
+
if (!cronExprLine || !promptText) {
|
|
193
|
+
return { decision: 'deny', reason: '❌ 参数文件格式错误:第1行应为 cron 表达式,第2行为 true/false,第3行起为 prompt 内容' };
|
|
194
|
+
}
|
|
195
|
+
params = {
|
|
196
|
+
cron: cronExprLine,
|
|
197
|
+
recurring: recurringLine !== 'false',
|
|
198
|
+
prompt: promptText,
|
|
199
|
+
};
|
|
200
|
+
} else {
|
|
201
|
+
// 内联 JSON 模式(兼容旧用法,不推荐)
|
|
202
|
+
try {
|
|
203
|
+
params = parseJsonArg(rawArgs);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
return { decision: 'deny', reason: `参数解析失败: ${e.message}。请改用文件方式:Write /tmp/<uuid>/cron/task_<ts>.txt,第1行cron表达式,第2行true/false,第3行起prompt内容,再调 Bash(__cron_create__ /tmp/.../task.txt)` };
|
|
206
|
+
}
|
|
133
207
|
}
|
|
134
208
|
|
|
135
209
|
const { cron: cronExpr, prompt: rawPrompt, recurring = true } = params;
|
|
@@ -197,10 +271,10 @@ Bash(__cron_list__)
|
|
|
197
271
|
return {
|
|
198
272
|
decision: 'deny',
|
|
199
273
|
reason: [
|
|
200
|
-
`✅
|
|
274
|
+
`✅ 定时任务已创建并持久化(无过期限制,永久有效)`,
|
|
201
275
|
`任务 ID: ${id}`,
|
|
202
276
|
`频率: ${cronExpr}`,
|
|
203
|
-
`类型: ${recurring ? '
|
|
277
|
+
`类型: ${recurring ? '循环(永久运行)' : '单次(触发后删除)'}`,
|
|
204
278
|
`会话方式: ${sessionDesc}`,
|
|
205
279
|
`投递渠道: ${channelDesc}`,
|
|
206
280
|
`内容: ${prompt}`,
|
|
@@ -269,11 +343,140 @@ export const cronListTool = {
|
|
|
269
343
|
},
|
|
270
344
|
};
|
|
271
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
|
+
|
|
272
474
|
// ─── SDK 原生工具重定向(拦截并引导 AI 使用 Bash 版本)─────────────────────────
|
|
273
475
|
|
|
274
476
|
const SDK_CRON_REDIRECT = `❌ 请勿使用 SDK 内置的 CronCreate/CronDelete/CronList 工具,它们在本系统中无法持久化。
|
|
275
|
-
请改用以下 Bash
|
|
276
|
-
-
|
|
477
|
+
请改用以下 Bash 命令(必须用文件方式创建/更新,避免 prompt 特殊字符导致解析失败):
|
|
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)
|
|
277
480
|
- 删除:Bash(__cron_delete__ '{"id":"任务ID"}')
|
|
278
481
|
- 列表:Bash(__cron_list__)`;
|
|
279
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);
|