@zhin.js/core 1.0.30 → 1.0.32
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/CHANGELOG.md +18 -0
- package/lib/ai/agent.d.ts +4 -0
- package/lib/ai/agent.d.ts.map +1 -1
- package/lib/ai/agent.js +22 -0
- package/lib/ai/agent.js.map +1 -1
- package/lib/ai/builtin-tools.d.ts +2 -1
- package/lib/ai/builtin-tools.d.ts.map +1 -1
- package/lib/ai/builtin-tools.js +86 -7
- package/lib/ai/builtin-tools.js.map +1 -1
- package/lib/ai/cron-engine.d.ts +92 -0
- package/lib/ai/cron-engine.d.ts.map +1 -0
- package/lib/ai/cron-engine.js +278 -0
- package/lib/ai/cron-engine.js.map +1 -0
- package/lib/ai/index.d.ts +3 -1
- package/lib/ai/index.d.ts.map +1 -1
- package/lib/ai/index.js +3 -1
- package/lib/ai/index.js.map +1 -1
- package/lib/ai/init.d.ts.map +1 -1
- package/lib/ai/init.js +115 -51
- package/lib/ai/init.js.map +1 -1
- package/lib/ai/service.d.ts +3 -0
- package/lib/ai/service.d.ts.map +1 -1
- package/lib/ai/service.js +4 -0
- package/lib/ai/service.js.map +1 -1
- package/lib/ai/types.d.ts +15 -0
- package/lib/ai/types.d.ts.map +1 -1
- package/lib/ai/zhin-agent.d.ts +18 -0
- package/lib/ai/zhin-agent.d.ts.map +1 -1
- package/lib/ai/zhin-agent.js +102 -10
- package/lib/ai/zhin-agent.js.map +1 -1
- package/lib/built/tool.d.ts +2 -0
- package/lib/built/tool.d.ts.map +1 -1
- package/lib/built/tool.js +8 -0
- package/lib/built/tool.js.map +1 -1
- package/lib/plugin.js +2 -2
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +2 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ai/agent.ts +21 -0
- package/src/ai/builtin-tools.ts +79 -7
- package/src/ai/cron-engine.ts +337 -0
- package/src/ai/index.ts +21 -1
- package/src/ai/init.ts +109 -55
- package/src/ai/service.ts +4 -0
- package/src/ai/types.ts +15 -0
- package/src/ai/zhin-agent.ts +114 -10
- package/src/built/tool.ts +8 -0
- package/src/plugin.ts +2 -2
- package/src/types.ts +3 -0
package/src/ai/builtin-tools.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import * as fs from 'fs';
|
|
18
|
+
import * as os from 'os';
|
|
18
19
|
import * as path from 'path';
|
|
19
20
|
import { exec } from 'child_process';
|
|
20
21
|
import { promisify } from 'util';
|
|
@@ -54,6 +55,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
54
55
|
.desc('读取文件内容(带行号,支持 offset/limit 分页)')
|
|
55
56
|
.keyword('读文件', '查看', '打开', 'cat', 'read')
|
|
56
57
|
.tag('file', 'read')
|
|
58
|
+
.kind('file')
|
|
57
59
|
.param('file_path', { type: 'string', description: '文件路径(绝对或相对)' }, true)
|
|
58
60
|
.param('offset', { type: 'number', description: '起始行号(0-based,默认 0)' })
|
|
59
61
|
.param('limit', { type: 'number', description: '最大读取行数(默认全部)' })
|
|
@@ -78,6 +80,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
78
80
|
.desc('创建或覆盖文件(自动创建目录)')
|
|
79
81
|
.keyword('写文件', '创建文件', '保存', 'write')
|
|
80
82
|
.tag('file', 'write')
|
|
83
|
+
.kind('file')
|
|
81
84
|
.param('file_path', { type: 'string', description: '文件路径' }, true)
|
|
82
85
|
.param('content', { type: 'string', description: '写入内容' }, true)
|
|
83
86
|
.execute(async (args) => {
|
|
@@ -97,6 +100,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
97
100
|
.desc('查找并替换文件内容(old_string 必须唯一匹配)。注意:old_string 应包含完整的行内容(含前后文),不要只匹配单个数字或单词')
|
|
98
101
|
.keyword('编辑', '修改', '替换', 'edit')
|
|
99
102
|
.tag('file', 'edit')
|
|
103
|
+
.kind('file')
|
|
100
104
|
.param('file_path', { type: 'string', description: '文件路径' }, true)
|
|
101
105
|
.param('old_string', { type: 'string', description: '要替换的原文(必须在文件中唯一出现,建议包含完整行)' }, true)
|
|
102
106
|
.param('new_string', { type: 'string', description: '替换后的文本(必须是替换 old_string 后的完整内容)' }, true)
|
|
@@ -125,6 +129,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
125
129
|
.desc('按 glob 模式查找文件(如 **/*.ts)')
|
|
126
130
|
.keyword('查找文件', '搜索文件', '文件列表', 'ls', 'find')
|
|
127
131
|
.tag('file', 'search')
|
|
132
|
+
.kind('file')
|
|
128
133
|
.param('pattern', { type: 'string', description: 'Glob 模式(如 **/*.ts)' }, true)
|
|
129
134
|
.param('cwd', { type: 'string', description: '工作目录(默认项目根目录)' })
|
|
130
135
|
.execute(async (args) => {
|
|
@@ -150,6 +155,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
150
155
|
.desc('按正则搜索文件内容,返回匹配行和行号')
|
|
151
156
|
.keyword('搜索', '查找内容', 'grep', '正则')
|
|
152
157
|
.tag('search', 'regex')
|
|
158
|
+
.kind('file')
|
|
153
159
|
.param('pattern', { type: 'string', description: '正则表达式' }, true)
|
|
154
160
|
.param('path', { type: 'string', description: '搜索路径(默认 .)' })
|
|
155
161
|
.param('include', { type: 'string', description: '文件类型过滤(如 *.ts)' })
|
|
@@ -175,6 +181,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
175
181
|
.desc('执行 Shell 命令(带超时保护)')
|
|
176
182
|
.keyword('执行', '运行', '命令', '终端', 'shell', 'bash')
|
|
177
183
|
.tag('shell', 'exec')
|
|
184
|
+
.kind('shell')
|
|
178
185
|
.param('command', { type: 'string', description: 'Shell 命令' }, true)
|
|
179
186
|
.param('cwd', { type: 'string', description: '工作目录' })
|
|
180
187
|
.param('timeout', { type: 'number', description: '超时毫秒数(默认 30000)' })
|
|
@@ -264,6 +271,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
264
271
|
.desc('抓取网页内容(去除 HTML 标签,最大 20KB)')
|
|
265
272
|
.keyword('抓取', '网页', 'fetch', 'url', '链接')
|
|
266
273
|
.tag('web', 'fetch')
|
|
274
|
+
.kind('web')
|
|
267
275
|
.param('url', { type: 'string', description: 'URL 地址' }, true)
|
|
268
276
|
.execute(async (args) => {
|
|
269
277
|
try {
|
|
@@ -293,6 +301,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
293
301
|
.desc('读取当前任务计划列表,用于查看进度和待办事项')
|
|
294
302
|
.keyword('任务', '计划', '进度', 'todo', '待办')
|
|
295
303
|
.tag('plan', 'todo')
|
|
304
|
+
.kind('plan')
|
|
296
305
|
.param('chat_id', { type: 'string', description: '聊天范围(传 "global" 表示全局,或传具体聊天 ID)' }, true)
|
|
297
306
|
.execute(async (args) => {
|
|
298
307
|
try {
|
|
@@ -318,6 +327,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
318
327
|
.desc('创建或更新任务计划,用于分解复杂任务并跟踪进度')
|
|
319
328
|
.keyword('创建计划', '更新任务', '标记完成', 'todo')
|
|
320
329
|
.tag('plan', 'todo')
|
|
330
|
+
.kind('plan')
|
|
321
331
|
.param('items', { type: 'array', description: '任务列表 [{title, detail?, status: pending|in-progress|done}]' } as any, true)
|
|
322
332
|
.param('chat_id', { type: 'string', description: '聊天范围(可选)' })
|
|
323
333
|
.execute(async (args) => {
|
|
@@ -341,6 +351,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
341
351
|
.desc('读取持久化记忆(AGENTS.md)。记忆跨会话保持。scope: global(共享)或 chat(按聊天隔离)')
|
|
342
352
|
.keyword('记忆', '记住', '回忆', '之前', '上次', 'memory')
|
|
343
353
|
.tag('memory', 'agents')
|
|
354
|
+
.kind('memory')
|
|
344
355
|
.param('scope', { type: 'string', description: "'global' 或 'chat'(默认 chat)", enum: ['global', 'chat'] }, true)
|
|
345
356
|
.param('chat_id', { type: 'string', description: '聊天 ID(chat scope 时使用)' })
|
|
346
357
|
.execute(async (args) => {
|
|
@@ -362,6 +373,7 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
362
373
|
.desc('写入持久化记忆。当用户说"记住…"、"记录…"时使用此工具')
|
|
363
374
|
.keyword('记住', '保存', 'remember', '记录')
|
|
364
375
|
.tag('memory', 'agents')
|
|
376
|
+
.kind('memory')
|
|
365
377
|
.param('content', { type: 'string', description: '要保存的记忆内容(Markdown)' }, true)
|
|
366
378
|
.param('scope', { type: 'string', description: "'global' 或 'chat'(默认 chat)", enum: ['global', 'chat'] })
|
|
367
379
|
.param('chat_id', { type: 'string', description: '聊天 ID' })
|
|
@@ -385,19 +397,24 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
385
397
|
.desc('按名称激活技能,加载其完整指令。当判断某个技能与用户请求相关时使用')
|
|
386
398
|
.keyword('技能', '激活', '启用', '使用', 'skill', 'activate', 'use')
|
|
387
399
|
.tag('skill', 'activate')
|
|
400
|
+
.kind('skill')
|
|
388
401
|
.param('name', { type: 'string', description: '技能名称' }, true)
|
|
389
402
|
.execute(async (args) => {
|
|
390
403
|
try {
|
|
404
|
+
// 与 discoverWorkspaceSkills 顺序一致:Workspace > Local > Bundled
|
|
391
405
|
const dirs = [
|
|
392
406
|
path.join(process.cwd(), 'skills'),
|
|
407
|
+
path.join(os.homedir(), '.zhin', 'skills'),
|
|
393
408
|
path.join(DATA_DIR, 'skills'),
|
|
394
409
|
];
|
|
395
410
|
for (const dir of dirs) {
|
|
396
411
|
const skillPath = path.join(dir, args.name, 'SKILL.md');
|
|
397
412
|
if (fs.existsSync(skillPath)) {
|
|
398
413
|
const fullContent = await fs.promises.readFile(skillPath, 'utf-8');
|
|
399
|
-
//
|
|
400
|
-
|
|
414
|
+
// 5.3 可执行环境检查:若 SKILL 声明了 deps,再次检查;缺失则在返回内容中提示
|
|
415
|
+
const depWarning = await checkSkillDeps(fullContent);
|
|
416
|
+
const instructions = extractSkillInstructions(args.name, fullContent);
|
|
417
|
+
return depWarning ? `${depWarning}\n\n${instructions}` : instructions;
|
|
401
418
|
}
|
|
402
419
|
}
|
|
403
420
|
return `Skill '${args.name}' not found. Check skills/ directory.`;
|
|
@@ -411,6 +428,36 @@ export function createBuiltinTools(): ZhinTool[] {
|
|
|
411
428
|
return tools;
|
|
412
429
|
}
|
|
413
430
|
|
|
431
|
+
/**
|
|
432
|
+
* 检查技能声明的依赖是否在环境中可用;若有缺失返回提示文案,否则返回空字符串
|
|
433
|
+
*/
|
|
434
|
+
async function checkSkillDeps(content: string): Promise<string> {
|
|
435
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
436
|
+
if (!fmMatch) return '';
|
|
437
|
+
let yaml: any;
|
|
438
|
+
try {
|
|
439
|
+
yaml = await import('yaml');
|
|
440
|
+
if (yaml.default) yaml = yaml.default;
|
|
441
|
+
} catch {
|
|
442
|
+
return '';
|
|
443
|
+
}
|
|
444
|
+
const metadata = yaml.parse(fmMatch[1]);
|
|
445
|
+
if (!metadata) return '';
|
|
446
|
+
const compat = metadata.compatibility || {};
|
|
447
|
+
const deps = compat.deps || metadata.deps;
|
|
448
|
+
if (!deps || !Array.isArray(deps)) return '';
|
|
449
|
+
const missing: string[] = [];
|
|
450
|
+
for (const dep of deps) {
|
|
451
|
+
try {
|
|
452
|
+
await execAsync(`which ${dep} 2>/dev/null`);
|
|
453
|
+
} catch {
|
|
454
|
+
missing.push(dep);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (missing.length === 0) return '';
|
|
458
|
+
return `⚠️ 当前环境缺少以下依赖,请先安装后再使用本技能:${missing.join(', ')}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
414
461
|
/**
|
|
415
462
|
* 从 SKILL.md 全文中提取精简的执行指令
|
|
416
463
|
* 只保留 frontmatter(工具列表)和执行规则,去掉示例、测试场景等冗余内容
|
|
@@ -433,11 +480,27 @@ function extractSkillInstructions(name: string, content: string): string {
|
|
|
433
480
|
}
|
|
434
481
|
}
|
|
435
482
|
|
|
436
|
-
// 2.
|
|
437
|
-
const rulesMatch = content.match(/## 执行规则[\s\S]*?(?=\n## [
|
|
483
|
+
// 2. 提取执行指导:优先 "## 执行规则",否则尝试 "## Workflow" / "## Instructions" / "## 使用说明",再否则用正文(去掉 frontmatter)
|
|
484
|
+
const rulesMatch = content.match(/## 执行规则[\s\S]*?(?=\n## [^\s]|$)/);
|
|
485
|
+
const workflowMatch = content.match(/## (?:Workflow|Instructions|使用说明)[\s\S]*?(?=\n## [^\s]|$)/);
|
|
486
|
+
const bodyAfterFm = fmMatch && fmMatch.index !== undefined
|
|
487
|
+
? content.slice(fmMatch.index + fmMatch[0].length).replace(/^\s+/, '')
|
|
488
|
+
: content;
|
|
438
489
|
if (rulesMatch) {
|
|
439
490
|
lines.push(rulesMatch[0].trim());
|
|
440
491
|
lines.push('');
|
|
492
|
+
} else if (workflowMatch) {
|
|
493
|
+
lines.push(workflowMatch[0].trim());
|
|
494
|
+
lines.push('');
|
|
495
|
+
} else if (bodyAfterFm.trim()) {
|
|
496
|
+
// 无上述标题时使用正文(去除 frontmatter 后)作为指导
|
|
497
|
+
const firstH2 = bodyAfterFm.match(/\n## [^\s]/);
|
|
498
|
+
const main = firstH2 ? bodyAfterFm.slice(0, firstH2.index).trim() : bodyAfterFm.trim();
|
|
499
|
+
if (main) {
|
|
500
|
+
lines.push('## 指导');
|
|
501
|
+
lines.push(main);
|
|
502
|
+
lines.push('');
|
|
503
|
+
}
|
|
441
504
|
}
|
|
442
505
|
|
|
443
506
|
// 3. 添加强制执行提醒
|
|
@@ -462,15 +525,18 @@ interface SkillMeta {
|
|
|
462
525
|
}
|
|
463
526
|
|
|
464
527
|
/**
|
|
465
|
-
*
|
|
528
|
+
* 扫描技能目录,发现 SKILL.md 技能文件
|
|
529
|
+
* 加载顺序:Workspace(cwd/skills)> Local(~/.zhin/skills)> Bundled(data/skills),同名技能先发现者优先
|
|
466
530
|
* 支持平台/依赖兼容性过滤
|
|
467
531
|
*/
|
|
468
532
|
export async function discoverWorkspaceSkills(): Promise<SkillMeta[]> {
|
|
469
533
|
const skills: SkillMeta[] = [];
|
|
534
|
+
const seenNames = new Set<string>();
|
|
470
535
|
const dataDir = getDataDir();
|
|
471
536
|
const skillDirs = [
|
|
472
|
-
path.join(process.cwd(), 'skills'),
|
|
473
|
-
path.join(
|
|
537
|
+
path.join(process.cwd(), 'skills'), // Workspace
|
|
538
|
+
path.join(os.homedir(), '.zhin', 'skills'), // Local
|
|
539
|
+
path.join(dataDir, 'skills'), // Bundled / 默认 data
|
|
474
540
|
];
|
|
475
541
|
|
|
476
542
|
// 确保 data/skills 目录存在
|
|
@@ -546,6 +612,12 @@ export async function discoverWorkspaceSkills(): Promise<SkillMeta[]> {
|
|
|
546
612
|
if (missing) continue;
|
|
547
613
|
}
|
|
548
614
|
|
|
615
|
+
if (seenNames.has(metadata.name)) {
|
|
616
|
+
logger.debug(`Skill '${metadata.name}' 已由先序目录加载,跳过: ${skillMdPath}`);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
seenNames.add(metadata.name);
|
|
620
|
+
|
|
549
621
|
skills.push({
|
|
550
622
|
name: metadata.name,
|
|
551
623
|
description: metadata.description,
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 持久化定时任务
|
|
3
|
+
*
|
|
4
|
+
* 将定时任务持久化到 data/cron-jobs.json,进程重启后自动加载;
|
|
5
|
+
* 触发时以 prompt 调用 ZhinAgent,实现「到点执行 AI 任务」。
|
|
6
|
+
*
|
|
7
|
+
* - 存储:id, cronExpression, prompt, label?, enabled, createdAt
|
|
8
|
+
* - 启动时:读取文件 → 为每条启用的任务创建 Cron → 注册到 CronFeature
|
|
9
|
+
* - CLI / AI 工具:可对持久化任务做 list / add / remove / pause / resume(AI 侧立即生效)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { Cron } from '../cron.js';
|
|
15
|
+
import { Logger } from '@zhin.js/logger';
|
|
16
|
+
import { ZhinTool } from '../built/tool.js';
|
|
17
|
+
|
|
18
|
+
const logger = new Logger(null, 'cron-engine');
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// 类型与存储路径
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export const CRON_JOBS_FILENAME = 'cron-jobs.json';
|
|
25
|
+
|
|
26
|
+
export interface CronJobRecord {
|
|
27
|
+
id: string;
|
|
28
|
+
/** Cron 表达式,5 字段:分 时 日 月 周 */
|
|
29
|
+
cronExpression: string;
|
|
30
|
+
/** 触发时发给 AI 的 prompt */
|
|
31
|
+
prompt: string;
|
|
32
|
+
/** 可选标签,便于识别 */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** 是否启用(暂停的任务不加载) */
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getCronJobsFilePath(dataDir: string): string {
|
|
40
|
+
return path.join(dataDir, CRON_JOBS_FILENAME);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function readCronJobsFile(dataDir: string): Promise<CronJobRecord[]> {
|
|
44
|
+
const filePath = getCronJobsFilePath(dataDir);
|
|
45
|
+
try {
|
|
46
|
+
const raw = await fs.promises.readFile(filePath, 'utf-8');
|
|
47
|
+
const data = JSON.parse(raw);
|
|
48
|
+
if (!Array.isArray(data)) return [];
|
|
49
|
+
return data;
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
if (e?.code === 'ENOENT') return [];
|
|
52
|
+
logger.warn('读取定时任务文件失败: ' + (e?.message || String(e)));
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function writeCronJobsFile(dataDir: string, jobs: CronJobRecord[]): Promise<void> {
|
|
58
|
+
const filePath = getCronJobsFilePath(dataDir);
|
|
59
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
60
|
+
await fs.promises.writeFile(filePath, JSON.stringify(jobs, null, 2), 'utf-8');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// 持久化引擎:加载文件并注册到 CronFeature
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export type CronRunner = (prompt: string, jobId: string) => void | Promise<void>;
|
|
68
|
+
|
|
69
|
+
export type AddCronFn = (cron: Cron) => () => void;
|
|
70
|
+
|
|
71
|
+
export interface PersistentCronEngineOptions {
|
|
72
|
+
dataDir: string;
|
|
73
|
+
addCron: AddCronFn;
|
|
74
|
+
runner: CronRunner;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class PersistentCronEngine {
|
|
78
|
+
private options: PersistentCronEngineOptions;
|
|
79
|
+
/** jobId -> dispose */
|
|
80
|
+
private disposes = new Map<string, () => void>();
|
|
81
|
+
|
|
82
|
+
constructor(options: PersistentCronEngineOptions) {
|
|
83
|
+
this.options = options;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getDataDir(): string {
|
|
87
|
+
return this.options.dataDir;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 从文件加载任务并注册到 CronFeature;仅加载 enabled 的任务。
|
|
92
|
+
*/
|
|
93
|
+
load(): void {
|
|
94
|
+
const { dataDir, addCron, runner } = this.options;
|
|
95
|
+
readCronJobsFile(dataDir).then((jobs) => {
|
|
96
|
+
for (const job of jobs) {
|
|
97
|
+
if (!job.enabled) continue;
|
|
98
|
+
this.registerOne(job, addCron, runner);
|
|
99
|
+
}
|
|
100
|
+
if (jobs.filter((j) => j.enabled).length > 0) {
|
|
101
|
+
logger.info(`已加载 ${this.disposes.size} 个持久化定时任务`);
|
|
102
|
+
}
|
|
103
|
+
}).catch((e) => {
|
|
104
|
+
logger.warn('加载持久化定时任务失败: ' + (e?.message || String(e)));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private registerOne(
|
|
109
|
+
job: CronJobRecord,
|
|
110
|
+
addCron: AddCronFn,
|
|
111
|
+
runner: CronRunner,
|
|
112
|
+
): void {
|
|
113
|
+
const { prompt, id: jobId, cronExpression } = job;
|
|
114
|
+
try {
|
|
115
|
+
const cron = new Cron(cronExpression, async () => {
|
|
116
|
+
await runner(prompt, jobId);
|
|
117
|
+
});
|
|
118
|
+
cron.id = jobId;
|
|
119
|
+
const dispose = addCron(cron);
|
|
120
|
+
this.disposes.set(jobId, dispose);
|
|
121
|
+
} catch (e: any) {
|
|
122
|
+
logger.warn(`定时任务加载失败 [${jobId}]: ${e?.message || String(e)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 列出所有持久化任务(从文件读取)
|
|
128
|
+
*/
|
|
129
|
+
async listJobs(): Promise<CronJobRecord[]> {
|
|
130
|
+
return readCronJobsFile(this.options.dataDir);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 添加持久化任务并立即生效
|
|
135
|
+
*/
|
|
136
|
+
async addJob(record: Omit<CronJobRecord, 'createdAt'> & { createdAt?: number }): Promise<CronJobRecord> {
|
|
137
|
+
const jobs = await readCronJobsFile(this.options.dataDir);
|
|
138
|
+
const full: CronJobRecord = {
|
|
139
|
+
...record,
|
|
140
|
+
createdAt: record.createdAt ?? Date.now(),
|
|
141
|
+
enabled: record.enabled ?? true,
|
|
142
|
+
};
|
|
143
|
+
jobs.push(full);
|
|
144
|
+
await writeCronJobsFile(this.options.dataDir, jobs);
|
|
145
|
+
if (full.enabled) {
|
|
146
|
+
this.registerOne(full, this.options.addCron, this.options.runner);
|
|
147
|
+
}
|
|
148
|
+
return full;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 删除持久化任务并立即生效
|
|
153
|
+
*/
|
|
154
|
+
async removeJob(id: string): Promise<boolean> {
|
|
155
|
+
const jobs = await readCronJobsFile(this.options.dataDir);
|
|
156
|
+
const next = jobs.filter((j) => j.id !== id);
|
|
157
|
+
if (next.length === jobs.length) return false;
|
|
158
|
+
await writeCronJobsFile(this.options.dataDir, next);
|
|
159
|
+
const dispose = this.disposes.get(id);
|
|
160
|
+
if (dispose) {
|
|
161
|
+
dispose();
|
|
162
|
+
this.disposes.delete(id);
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 暂停任务(不删除,停止调度)
|
|
169
|
+
*/
|
|
170
|
+
async pauseJob(id: string): Promise<boolean> {
|
|
171
|
+
const jobs = await readCronJobsFile(this.options.dataDir);
|
|
172
|
+
const j = jobs.find((x) => x.id === id);
|
|
173
|
+
if (!j) return false;
|
|
174
|
+
j.enabled = false;
|
|
175
|
+
await writeCronJobsFile(this.options.dataDir, jobs);
|
|
176
|
+
const dispose = this.disposes.get(id);
|
|
177
|
+
if (dispose) {
|
|
178
|
+
dispose();
|
|
179
|
+
this.disposes.delete(id);
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 恢复已暂停的任务
|
|
186
|
+
*/
|
|
187
|
+
async resumeJob(id: string): Promise<boolean> {
|
|
188
|
+
const jobs = await readCronJobsFile(this.options.dataDir);
|
|
189
|
+
const j = jobs.find((x) => x.id === id);
|
|
190
|
+
if (!j) return false;
|
|
191
|
+
j.enabled = true;
|
|
192
|
+
await writeCronJobsFile(this.options.dataDir, jobs);
|
|
193
|
+
this.registerOne(j, this.options.addCron, this.options.runner);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 卸载所有由本引擎注册的定时任务(用于 dispose)
|
|
199
|
+
*/
|
|
200
|
+
unload(): void {
|
|
201
|
+
for (const dispose of this.disposes.values()) {
|
|
202
|
+
try {
|
|
203
|
+
dispose();
|
|
204
|
+
} catch (_) {}
|
|
205
|
+
}
|
|
206
|
+
this.disposes.clear();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 生成唯一 ID(用于 CLI / AI 添加时)
|
|
212
|
+
*/
|
|
213
|
+
export function generateCronJobId(): string {
|
|
214
|
+
return `cron_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
// 供 AI 工具使用的 Cron 管理器引用(init 中设置)
|
|
219
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
export interface CronManager {
|
|
222
|
+
cronFeature: { getStatus(): Array<{ expression: string; running: boolean; nextExecution: Date | null; plugin: string }> };
|
|
223
|
+
engine: PersistentCronEngine | null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let cronManager: CronManager | null = null;
|
|
227
|
+
|
|
228
|
+
export function setCronManager(m: CronManager | null): void {
|
|
229
|
+
cronManager = m;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getCronManager(): CronManager | null {
|
|
233
|
+
return cronManager;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
// AI 可调用的定时任务管理工具
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
export function createCronTools(): ZhinTool[] {
|
|
241
|
+
const listTool = new ZhinTool('cron_list')
|
|
242
|
+
.desc('列出所有定时任务:包括插件注册的内存任务与持久化任务(持久化任务有 id,可用于 cron_remove/cron_pause/cron_resume)')
|
|
243
|
+
.keyword('定时任务', 'cron', '计划任务', '任务列表')
|
|
244
|
+
.tag('cron', 'schedule')
|
|
245
|
+
.execute(async () => {
|
|
246
|
+
const m = getCronManager();
|
|
247
|
+
if (!m) {
|
|
248
|
+
return { error: '定时任务服务不可用' };
|
|
249
|
+
}
|
|
250
|
+
const memory = m.cronFeature.getStatus().map((s) => ({
|
|
251
|
+
type: 'memory' as const,
|
|
252
|
+
expression: s.expression,
|
|
253
|
+
running: s.running,
|
|
254
|
+
nextExecution: s.nextExecution?.toISOString() ?? null,
|
|
255
|
+
plugin: s.plugin,
|
|
256
|
+
}));
|
|
257
|
+
const persistent = m.engine
|
|
258
|
+
? (await m.engine.listJobs()).map((j) => ({
|
|
259
|
+
type: 'persistent' as const,
|
|
260
|
+
id: j.id,
|
|
261
|
+
cronExpression: j.cronExpression,
|
|
262
|
+
prompt: j.prompt,
|
|
263
|
+
label: j.label,
|
|
264
|
+
enabled: j.enabled,
|
|
265
|
+
createdAt: j.createdAt,
|
|
266
|
+
}))
|
|
267
|
+
: [];
|
|
268
|
+
return { memory, persistent };
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const addTool = new ZhinTool('cron_add')
|
|
272
|
+
.desc('添加一条持久化定时任务:到点由 AI 执行指定 prompt,重启后仍保留')
|
|
273
|
+
.keyword('添加定时', '新建定时任务', 'cron add')
|
|
274
|
+
.tag('cron', 'schedule')
|
|
275
|
+
.param('cron_expression', { type: 'string', description: 'Cron 表达式,如 "0 9 * * *" 表示每天 9:00' }, true)
|
|
276
|
+
.param('prompt', { type: 'string', description: '到点触发时发给 AI 的提示词' }, true)
|
|
277
|
+
.param('label', { type: 'string', description: '可选标签,便于识别' })
|
|
278
|
+
.execute(async (args) => {
|
|
279
|
+
const m = getCronManager();
|
|
280
|
+
if (!m?.engine) {
|
|
281
|
+
return { error: '持久化定时任务引擎不可用' };
|
|
282
|
+
}
|
|
283
|
+
const id = generateCronJobId();
|
|
284
|
+
const job = await m.engine.addJob({
|
|
285
|
+
id,
|
|
286
|
+
cronExpression: args.cron_expression as string,
|
|
287
|
+
prompt: args.prompt as string,
|
|
288
|
+
label: args.label as string | undefined,
|
|
289
|
+
enabled: true,
|
|
290
|
+
});
|
|
291
|
+
return { success: true, id: job.id, message: '已添加并立即生效' };
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const removeTool = new ZhinTool('cron_remove')
|
|
295
|
+
.desc('按 id 删除一条持久化定时任务')
|
|
296
|
+
.keyword('删除定时', '取消定时', 'cron remove')
|
|
297
|
+
.tag('cron', 'schedule')
|
|
298
|
+
.param('id', { type: 'string', description: '任务 ID(cron_list 中 persistent 的 id)' }, true)
|
|
299
|
+
.execute(async (args) => {
|
|
300
|
+
const m = getCronManager();
|
|
301
|
+
if (!m?.engine) {
|
|
302
|
+
return { error: '持久化定时任务引擎不可用' };
|
|
303
|
+
}
|
|
304
|
+
const ok = await m.engine.removeJob(args.id as string);
|
|
305
|
+
return ok ? { success: true, message: '已删除' } : { error: '未找到该任务' };
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const pauseTool = new ZhinTool('cron_pause')
|
|
309
|
+
.desc('暂停一条持久化定时任务(不删除,可 cron_resume 恢复)')
|
|
310
|
+
.keyword('暂停定时', 'cron pause')
|
|
311
|
+
.tag('cron', 'schedule')
|
|
312
|
+
.param('id', { type: 'string', description: '任务 ID' }, true)
|
|
313
|
+
.execute(async (args) => {
|
|
314
|
+
const m = getCronManager();
|
|
315
|
+
if (!m?.engine) {
|
|
316
|
+
return { error: '持久化定时任务引擎不可用' };
|
|
317
|
+
}
|
|
318
|
+
const ok = await m.engine.pauseJob(args.id as string);
|
|
319
|
+
return ok ? { success: true, message: '已暂停' } : { error: '未找到该任务' };
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const resumeTool = new ZhinTool('cron_resume')
|
|
323
|
+
.desc('恢复已暂停的持久化定时任务')
|
|
324
|
+
.keyword('恢复定时', 'cron resume')
|
|
325
|
+
.tag('cron', 'schedule')
|
|
326
|
+
.param('id', { type: 'string', description: '任务 ID' }, true)
|
|
327
|
+
.execute(async (args) => {
|
|
328
|
+
const m = getCronManager();
|
|
329
|
+
if (!m?.engine) {
|
|
330
|
+
return { error: '持久化定时任务引擎不可用' };
|
|
331
|
+
}
|
|
332
|
+
const ok = await m.engine.resumeJob(args.id as string);
|
|
333
|
+
return ok ? { success: true, message: '已恢复' } : { error: '未找到该任务' };
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return [listTool, addTool, removeTool, pauseTool, resumeTool];
|
|
337
|
+
}
|
package/src/ai/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type {
|
|
|
26
26
|
export { AIService } from './service.js';
|
|
27
27
|
|
|
28
28
|
// ── Agent ──
|
|
29
|
-
export { Agent, createAgent } from './agent.js';
|
|
29
|
+
export { Agent, createAgent, formatToolTitle } from './agent.js';
|
|
30
30
|
|
|
31
31
|
// ── Session ──
|
|
32
32
|
export {
|
|
@@ -74,6 +74,26 @@ export type { RateLimitConfig, RateLimitResult } from './rate-limiter.js';
|
|
|
74
74
|
export { FollowUpManager, AI_FOLLOWUP_MODEL } from './follow-up.js';
|
|
75
75
|
export type { FollowUpRecord, FollowUpSender } from './follow-up.js';
|
|
76
76
|
|
|
77
|
+
// ── 持久化定时任务引擎 ──
|
|
78
|
+
export {
|
|
79
|
+
PersistentCronEngine,
|
|
80
|
+
readCronJobsFile,
|
|
81
|
+
writeCronJobsFile,
|
|
82
|
+
getCronJobsFilePath,
|
|
83
|
+
generateCronJobId,
|
|
84
|
+
createCronTools,
|
|
85
|
+
setCronManager,
|
|
86
|
+
getCronManager,
|
|
87
|
+
CRON_JOBS_FILENAME,
|
|
88
|
+
} from './cron-engine.js';
|
|
89
|
+
export type {
|
|
90
|
+
CronJobRecord,
|
|
91
|
+
CronRunner,
|
|
92
|
+
AddCronFn,
|
|
93
|
+
PersistentCronEngineOptions,
|
|
94
|
+
CronManager,
|
|
95
|
+
} from './cron-engine.js';
|
|
96
|
+
|
|
77
97
|
// ── Tone Detector ──
|
|
78
98
|
export { detectTone } from './tone-detector.js';
|
|
79
99
|
export type { Tone } from './tone-detector.js';
|