evolclaw 2.8.3 → 3.0.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.
Files changed (105) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +102 -38
  3. package/dist/agents/codex-runner.js +11 -14
  4. package/dist/agents/gemini-runner.js +10 -12
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1051 -288
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +858 -847
  40. package/dist/core/evolagent-registry.js +191 -371
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +309 -142
  48. package/dist/core/message/message-queue.js +3 -3
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +431 -275
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -591
  92. package/dist/core/agent-registry.js +0 -450
  93. package/dist/core/evolagent-schema.js +0 -72
  94. package/dist/core/message/stream-flusher.js +0 -238
  95. package/dist/core/message/thought-emitter.js +0 -162
  96. package/dist/core/reload-hooks.js +0 -87
  97. package/dist/prompts/templates.js +0 -122
  98. package/dist/templates/skills.md +0 -66
  99. package/dist/utils/channel-fingerprint.js +0 -59
  100. package/dist/utils/error-dict.js +0 -63
  101. package/dist/utils/format.js +0 -32
  102. package/dist/utils/init.js +0 -645
  103. package/dist/utils/migrate-project.js +0 -122
  104. package/dist/utils/reload-hooks.js +0 -87
  105. package/dist/utils/stats-collector.js +0 -99
@@ -0,0 +1,87 @@
1
+ import { formatTimestamp } from './session-fs-store.js';
2
+ export function sessionToFile(session) {
3
+ const metadata = {};
4
+ if (session.metadata) {
5
+ if (session.metadata.peerId)
6
+ metadata.peerId = session.metadata.peerId;
7
+ if (session.metadata.peerName)
8
+ metadata.peerName = session.metadata.peerName;
9
+ if (session.metadata.groupId)
10
+ metadata.groupId = session.metadata.groupId;
11
+ if (session.metadata.replyContext)
12
+ metadata.replyContext = session.metadata.replyContext;
13
+ if (session.metadata.agentSessions)
14
+ metadata.agentSessions = session.metadata.agentSessions;
15
+ if (session.metadata.resumeAt)
16
+ metadata.resumeAt = session.metadata.resumeAt;
17
+ for (const [k, v] of Object.entries(session.metadata)) {
18
+ if (['isActive', 'channelName', 'permissionMode', 'peerId', 'peerName', 'groupId', 'replyContext', 'agentSessions', 'resumeAt'].includes(k))
19
+ continue;
20
+ if (v !== undefined)
21
+ metadata[k] = v;
22
+ }
23
+ }
24
+ const now = session.updatedAt || Date.now();
25
+ return {
26
+ id: session.id,
27
+ channel: session.channel,
28
+ channelType: session.channelType || session.channel,
29
+ channelId: session.channelId,
30
+ selfId: session.selfId ?? null,
31
+ agentType: session.agentId || 'claude',
32
+ threadId: session.threadId || '',
33
+ chatType: session.chatType || 'private',
34
+ chatMode: session.sessionMode || 'interactive',
35
+ projectPath: session.projectPath,
36
+ agentSessionId: session.agentSessionId ?? null,
37
+ name: session.name ?? null,
38
+ activeTask: session.processingState ?? null,
39
+ permissionMode: session.metadata?.permissionMode || 'auto',
40
+ metadata,
41
+ createdAt: session.createdAt,
42
+ createdAtStr: formatTimestamp(session.createdAt),
43
+ updatedAt: now,
44
+ updatedAtStr: formatTimestamp(now),
45
+ };
46
+ }
47
+ export function fileToSession(file) {
48
+ const metadata = {};
49
+ if (file.metadata.peerId)
50
+ metadata.peerId = file.metadata.peerId;
51
+ if (file.metadata.peerName)
52
+ metadata.peerName = file.metadata.peerName;
53
+ if (file.metadata.groupId)
54
+ metadata.groupId = file.metadata.groupId;
55
+ if (file.metadata.replyContext)
56
+ metadata.replyContext = file.metadata.replyContext;
57
+ if (file.metadata.agentSessions)
58
+ metadata.agentSessions = file.metadata.agentSessions;
59
+ if (file.metadata.resumeAt)
60
+ metadata.resumeAt = file.metadata.resumeAt;
61
+ if (file.permissionMode)
62
+ metadata.permissionMode = file.permissionMode;
63
+ for (const [k, v] of Object.entries(file.metadata)) {
64
+ if (['peerId', 'peerName', 'groupId', 'replyContext', 'agentSessions', 'resumeAt'].includes(k))
65
+ continue;
66
+ if (v !== undefined)
67
+ metadata[k] = v;
68
+ }
69
+ return {
70
+ id: file.id,
71
+ channel: file.channel,
72
+ channelType: file.channelType,
73
+ channelId: file.channelId,
74
+ selfId: file.selfId ?? undefined,
75
+ agentId: file.agentType,
76
+ threadId: file.threadId,
77
+ chatType: file.chatType,
78
+ sessionMode: file.chatMode,
79
+ projectPath: file.projectPath,
80
+ agentSessionId: file.agentSessionId ?? undefined,
81
+ name: file.name ?? undefined,
82
+ processingState: file.activeTask ?? undefined,
83
+ metadata,
84
+ createdAt: file.createdAt,
85
+ updatedAt: file.updatedAt,
86
+ };
87
+ }
@@ -0,0 +1,122 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { atomicWriteJson, appendJsonl } from '../session/session-fs-store.js';
4
+ import { logger } from '../../utils/logger.js';
5
+ export class TriggerManager {
6
+ aid;
7
+ triggersPath;
8
+ historyPath;
9
+ triggers = new Map();
10
+ constructor(aid, triggersDir) {
11
+ this.aid = aid;
12
+ fs.mkdirSync(triggersDir, { recursive: true });
13
+ this.triggersPath = path.join(triggersDir, 'triggers.json');
14
+ this.historyPath = path.join(triggersDir, 'history.jsonl');
15
+ }
16
+ load() {
17
+ if (!fs.existsSync(this.triggersPath)) {
18
+ this.triggers = new Map();
19
+ return [];
20
+ }
21
+ try {
22
+ const raw = fs.readFileSync(this.triggersPath, 'utf8');
23
+ const data = JSON.parse(raw);
24
+ this.triggers = new Map(Object.entries(data.triggers ?? {}));
25
+ return [...this.triggers.values()];
26
+ }
27
+ catch (e) {
28
+ logger.warn(`[TriggerManager] Failed to parse ${this.triggersPath}, starting empty: ${e}`);
29
+ this.triggers = new Map();
30
+ return [];
31
+ }
32
+ }
33
+ save() {
34
+ const data = {
35
+ version: 1,
36
+ triggers: Object.fromEntries(this.triggers),
37
+ };
38
+ atomicWriteJson(this.triggersPath, data);
39
+ }
40
+ register(trigger) {
41
+ if (this.triggers.has(trigger.id)) {
42
+ throw new Error(`触发器 ID 已存在:${trigger.id}`);
43
+ }
44
+ // Check name uniqueness within this agent
45
+ for (const t of this.triggers.values()) {
46
+ if (t.name === trigger.name) {
47
+ throw new Error(`触发器名称已存在:${trigger.name}`);
48
+ }
49
+ }
50
+ this.triggers.set(trigger.id, trigger);
51
+ this.save();
52
+ }
53
+ getById(id) {
54
+ return this.triggers.get(id);
55
+ }
56
+ getByName(name) {
57
+ for (const t of this.triggers.values()) {
58
+ if (t.name === name)
59
+ return t;
60
+ }
61
+ return undefined;
62
+ }
63
+ // Scoped lookup: only returns triggers owned by (peerId, channel) — prevents info disclosure
64
+ getByNameScoped(name, peerId, channel) {
65
+ for (const t of this.triggers.values()) {
66
+ if (t.name === name && t.createdByPeerId === peerId && t.createdByChannel === channel)
67
+ return t;
68
+ }
69
+ return undefined;
70
+ }
71
+ // Scoped ID lookup: allows creator to cancel by UUID without revealing others' triggers
72
+ getByIdScoped(id, peerId, channel) {
73
+ const t = this.triggers.get(id);
74
+ if (t && t.createdByPeerId === peerId && t.createdByChannel === channel)
75
+ return t;
76
+ return undefined;
77
+ }
78
+ listActive() {
79
+ return [...this.triggers.values()].sort((a, b) => a.nextFireAt - b.nextFireAt);
80
+ }
81
+ listAll() {
82
+ const active = this.listActive();
83
+ const history = [];
84
+ if (fs.existsSync(this.historyPath)) {
85
+ const lines = fs.readFileSync(this.historyPath, 'utf8').split('\n').filter(Boolean);
86
+ for (const line of lines) {
87
+ try {
88
+ history.push(JSON.parse(line));
89
+ }
90
+ catch { /* skip malformed */ }
91
+ }
92
+ }
93
+ return { active, history };
94
+ }
95
+ updateFireStats(id, firedAt) {
96
+ const t = this.triggers.get(id);
97
+ if (!t)
98
+ return;
99
+ t.lastFiredAt = firedAt;
100
+ t.fireCount += 1;
101
+ t.updatedAt = Date.now();
102
+ this.save();
103
+ }
104
+ updateNextFireAt(id, nextFireAt) {
105
+ const t = this.triggers.get(id);
106
+ if (!t)
107
+ return;
108
+ t.nextFireAt = nextFireAt;
109
+ t.updatedAt = Date.now();
110
+ this.save();
111
+ }
112
+ moveToDone(id, reason) {
113
+ const t = this.triggers.get(id);
114
+ if (!t)
115
+ return undefined;
116
+ this.triggers.delete(id);
117
+ this.save();
118
+ const entry = { ...t, doneAt: Date.now(), doneReason: reason };
119
+ appendJsonl(this.historyPath, entry);
120
+ return t;
121
+ }
122
+ }
@@ -0,0 +1,128 @@
1
+ import { CronExpressionParser } from 'cron-parser';
2
+ // Note: unquoted multi-word values (e.g. --prompt=hello world) are not supported.
3
+ // The second word would be treated as an unknown token. Always quote multi-word values:
4
+ // --prompt "hello world" or --prompt='hello world'
5
+ function parseFlags(args) {
6
+ const flags = new Map();
7
+ const re = /--(\w[\w-]*)(?:=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)|(?:\s+(?!--)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+))?)?/g;
8
+ let m;
9
+ while ((m = re.exec(args)) !== null) {
10
+ const key = m[1];
11
+ const val = m[2] ?? m[3];
12
+ if (val === undefined) {
13
+ flags.set(key, true);
14
+ }
15
+ else {
16
+ flags.set(key, val.replace(/^["']|["']$/g, ''));
17
+ }
18
+ }
19
+ return flags;
20
+ }
21
+ export function parseDuration(s) {
22
+ const re = /^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/;
23
+ const m = re.exec(s.trim().toLowerCase());
24
+ if (!m || !s.trim())
25
+ return null;
26
+ const [, d, h, min, sec] = m;
27
+ const total = (parseInt(d ?? '0') * 86400 +
28
+ parseInt(h ?? '0') * 3600 +
29
+ parseInt(min ?? '0') * 60 +
30
+ parseInt(sec ?? '0')) * 1000;
31
+ return total > 0 ? total : null;
32
+ }
33
+ export function parseIsoDate(s) {
34
+ const d = new Date(s);
35
+ return isNaN(d.getTime()) ? null : d.getTime();
36
+ }
37
+ export function validateCronExpr(expr) {
38
+ try {
39
+ CronExpressionParser.parse(expr);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ export function parseTriggerSet(args) {
47
+ const flags = parseFlags(args);
48
+ const hasDelay = flags.has('delay');
49
+ const hasAt = flags.has('at');
50
+ const hasCron = flags.has('cron');
51
+ const timeCount = [hasDelay, hasAt, hasCron].filter(Boolean).length;
52
+ if (timeCount === 0) {
53
+ return { ok: false, error: '必须指定时间参数:--delay <时长> | --at <ISO时间> | --cron <表达式>' };
54
+ }
55
+ if (timeCount > 1) {
56
+ return { ok: false, error: '--delay、--at、--cron 互斥,只能指定一个' };
57
+ }
58
+ let scheduleType;
59
+ let scheduleValue;
60
+ if (hasDelay) {
61
+ const raw = flags.get('delay');
62
+ const ms = parseDuration(raw);
63
+ if (ms === null) {
64
+ return { ok: false, error: `无法解析 --delay "${raw}",支持格式:30m、2h、1d、2h30m` };
65
+ }
66
+ scheduleType = 'delay';
67
+ scheduleValue = String(ms);
68
+ }
69
+ else if (hasAt) {
70
+ const raw = flags.get('at');
71
+ const ts = parseIsoDate(raw);
72
+ if (ts === null) {
73
+ return { ok: false, error: `无法解析 --at "${raw}",请使用 ISO 格式,如 2026-05-15T09:00` };
74
+ }
75
+ if (ts <= Date.now()) {
76
+ return { ok: false, error: `--at 时间已过期:${raw}` };
77
+ }
78
+ scheduleType = 'at';
79
+ scheduleValue = new Date(ts).toISOString();
80
+ }
81
+ else {
82
+ const raw = flags.get('cron');
83
+ if (!validateCronExpr(raw)) {
84
+ return { ok: false, error: `无效的 cron 表达式:"${raw}"` };
85
+ }
86
+ scheduleType = 'cron';
87
+ scheduleValue = raw;
88
+ }
89
+ const prompt = flags.get('prompt');
90
+ if (!prompt || prompt === true) {
91
+ return { ok: false, error: '--prompt 为必填项' };
92
+ }
93
+ if (typeof prompt === 'string' && prompt.length > 4096) {
94
+ return { ok: false, error: '--prompt 超过 4096 字符限制' };
95
+ }
96
+ const hasThread = flags.has('thread');
97
+ const hasSession = flags.has('session');
98
+ if (hasThread && hasSession) {
99
+ return { ok: false, error: '--thread 与 --session 互斥,只能指定一个' };
100
+ }
101
+ const hasChannel = flags.has('channel');
102
+ const hasChannelId = flags.has('channelid');
103
+ if (hasChannel !== hasChannelId) {
104
+ return { ok: false, error: '--channel 与 --channelid 必须同时指定或同时省略' };
105
+ }
106
+ let targetSessionStrategy = 'latest';
107
+ if (hasSession) {
108
+ const sv = flags.get('session');
109
+ if (sv !== 'latest' && sv !== 'silent') {
110
+ return { ok: false, error: '--session 只接受 latest 或 silent' };
111
+ }
112
+ targetSessionStrategy = sv;
113
+ }
114
+ return {
115
+ ok: true,
116
+ value: {
117
+ scheduleType,
118
+ scheduleValue,
119
+ targetChannel: hasChannel ? flags.get('channel') : undefined,
120
+ targetChannelId: hasChannelId ? flags.get('channelid') : undefined,
121
+ targetThreadId: hasThread ? flags.get('thread') : undefined,
122
+ targetSessionStrategy,
123
+ agentId: flags.has('agent') ? flags.get('agent') : undefined,
124
+ name: flags.has('name') ? flags.get('name') : undefined,
125
+ prompt: prompt,
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,224 @@
1
+ import { CronExpressionParser } from 'cron-parser';
2
+ import { logger as baseLogger } from '../../utils/logger.js';
3
+ const logger = {
4
+ info: (msg) => baseLogger.info(msg),
5
+ warn: (msg) => baseLogger.warn(msg),
6
+ debug: (msg) => baseLogger.debug(msg),
7
+ };
8
+ // Min-heap ordered by nextFireAt
9
+ class TriggerHeap {
10
+ heap = [];
11
+ push(t) {
12
+ this.heap.push(t);
13
+ this.bubbleUp(this.heap.length - 1);
14
+ }
15
+ pop() {
16
+ if (this.heap.length === 0)
17
+ return undefined;
18
+ const top = this.heap[0];
19
+ const last = this.heap.pop();
20
+ if (this.heap.length > 0) {
21
+ this.heap[0] = last;
22
+ this.sinkDown(0);
23
+ }
24
+ return top;
25
+ }
26
+ peek() {
27
+ return this.heap[0];
28
+ }
29
+ remove(id) {
30
+ const idx = this.heap.findIndex(t => t.id === id);
31
+ if (idx === -1)
32
+ return false;
33
+ const last = this.heap.pop();
34
+ if (idx < this.heap.length) {
35
+ this.heap[idx] = last;
36
+ this.bubbleUp(idx);
37
+ this.sinkDown(idx);
38
+ }
39
+ return true;
40
+ }
41
+ bubbleUp(i) {
42
+ while (i > 0) {
43
+ const parent = (i - 1) >> 1;
44
+ if (this.heap[parent].nextFireAt <= this.heap[i].nextFireAt)
45
+ break;
46
+ [this.heap[parent], this.heap[i]] = [this.heap[i], this.heap[parent]];
47
+ i = parent;
48
+ }
49
+ }
50
+ sinkDown(i) {
51
+ const n = this.heap.length;
52
+ while (true) {
53
+ let smallest = i;
54
+ const l = 2 * i + 1, r = 2 * i + 2;
55
+ if (l < n && this.heap[l].nextFireAt < this.heap[smallest].nextFireAt)
56
+ smallest = l;
57
+ if (r < n && this.heap[r].nextFireAt < this.heap[smallest].nextFireAt)
58
+ smallest = r;
59
+ if (smallest === i)
60
+ break;
61
+ [this.heap[smallest], this.heap[i]] = [this.heap[i], this.heap[smallest]];
62
+ i = smallest;
63
+ }
64
+ }
65
+ get size() { return this.heap.length; }
66
+ }
67
+ /**
68
+ * Calculate the next fire timestamp for a trigger.
69
+ *
70
+ * For `delay` type: `now` is the reference point — returns `now + delayMs`.
71
+ * Pass `Date.now()` at registration time to get the original fire time.
72
+ * Do NOT pass a stored `nextFireAt` as `now` — that would double-add the delay.
73
+ * For `at` type: `now` is ignored; returns the absolute ISO timestamp.
74
+ * For `cron` type: returns the next occurrence after `now`.
75
+ */
76
+ export function calcNextFireAt(scheduleType, scheduleValue, now = Date.now()) {
77
+ if (scheduleType === 'delay') {
78
+ return now + parseInt(scheduleValue);
79
+ }
80
+ if (scheduleType === 'at') {
81
+ return new Date(scheduleValue).getTime();
82
+ }
83
+ // cron
84
+ const interval = CronExpressionParser.parse(scheduleValue, { currentDate: new Date(now) });
85
+ return interval.next().getTime();
86
+ }
87
+ export class TriggerScheduler {
88
+ aid;
89
+ manager;
90
+ eventBus;
91
+ heap = new TriggerHeap();
92
+ timer = null;
93
+ inflightCron = new Set(); // trigger IDs currently executing (cron)
94
+ fireCallback;
95
+ constructor(aid, manager, eventBus) {
96
+ this.aid = aid;
97
+ this.manager = manager;
98
+ this.eventBus = eventBus;
99
+ }
100
+ setFireCallback(cb) {
101
+ this.fireCallback = cb;
102
+ }
103
+ async init() {
104
+ const triggers = this.manager.load();
105
+ const now = Date.now();
106
+ for (const t of triggers) {
107
+ if (t.scheduleType === 'cron') {
108
+ // Recalculate next fire from now (don't backfill missed cron runs)
109
+ const next = calcNextFireAt('cron', t.scheduleValue, now);
110
+ if (next !== t.nextFireAt) {
111
+ this.manager.updateNextFireAt(t.id, next);
112
+ t.nextFireAt = next;
113
+ }
114
+ this.heap.push(t);
115
+ }
116
+ else {
117
+ // delay/at: if missed, fire immediately (backfill)
118
+ if (t.nextFireAt < now) {
119
+ logger.info(`[${this.aid}] Backfilling missed trigger: ${t.name} (${t.id})`);
120
+ this.heap.push({ ...t, nextFireAt: now });
121
+ }
122
+ else {
123
+ this.heap.push(t);
124
+ }
125
+ }
126
+ }
127
+ this.resetTimer();
128
+ logger.info(`[${this.aid}] Scheduler initialized with ${triggers.length} trigger(s)`);
129
+ }
130
+ register(trigger) {
131
+ this.heap.push(trigger);
132
+ this.resetTimer();
133
+ this.eventBus.publish({ type: 'trigger:registered', triggerId: trigger.id, name: trigger.name, peerId: trigger.createdByPeerId });
134
+ }
135
+ cancel(id) {
136
+ this.heap.remove(id);
137
+ this.inflightCron.delete(id);
138
+ this.resetTimer();
139
+ }
140
+ stop() {
141
+ if (this.timer) {
142
+ clearTimeout(this.timer);
143
+ this.timer = null;
144
+ }
145
+ this.inflightCron.clear();
146
+ }
147
+ resetTimer() {
148
+ if (this.timer) {
149
+ clearTimeout(this.timer);
150
+ this.timer = null;
151
+ }
152
+ const top = this.heap.peek();
153
+ if (!top)
154
+ return;
155
+ const delay = Math.max(0, top.nextFireAt - Date.now());
156
+ this.timer = setTimeout(() => this.onFire(), delay);
157
+ }
158
+ onFire() {
159
+ this.timer = null;
160
+ const now = Date.now();
161
+ // Fire all triggers that are due
162
+ while (this.heap.peek() && this.heap.peek().nextFireAt <= now + 50) {
163
+ const trigger = this.heap.pop();
164
+ if (trigger.scheduleType === 'cron' && this.inflightCron.has(trigger.id)) {
165
+ // Previous run still in flight — skip
166
+ logger.warn(`[${this.aid}] Cron trigger ${trigger.name} still running, skipping`);
167
+ this.eventBus.publish({ type: 'trigger:skipped', triggerId: trigger.id, reason: 'overlap' });
168
+ // Re-schedule next cron occurrence
169
+ const next = calcNextFireAt('cron', trigger.scheduleValue, now);
170
+ this.manager.updateNextFireAt(trigger.id, next);
171
+ this.heap.push({ ...trigger, nextFireAt: next });
172
+ continue;
173
+ }
174
+ this.fireTrigger(trigger, now);
175
+ }
176
+ this.resetTimer();
177
+ }
178
+ fireTrigger(trigger, now) {
179
+ const messageId = `trigger:${trigger.id}:${now}`;
180
+ const msg = this.buildSyntheticMessage(trigger, messageId);
181
+ logger.info(`[${this.aid}] Firing trigger: ${trigger.name} (${trigger.id})`);
182
+ // Update stats before moving to done so history captures the updated count
183
+ this.manager.updateFireStats(trigger.id, now);
184
+ if (trigger.scheduleType === 'cron') {
185
+ this.inflightCron.add(trigger.id);
186
+ // Re-schedule next occurrence
187
+ const next = calcNextFireAt('cron', trigger.scheduleValue, now);
188
+ this.manager.updateNextFireAt(trigger.id, next);
189
+ this.heap.push({ ...trigger, nextFireAt: next });
190
+ }
191
+ else {
192
+ // delay/at: one-shot, move to done
193
+ this.manager.moveToDone(trigger.id, 'fired');
194
+ }
195
+ this.eventBus.publish({ type: 'trigger:fired', triggerId: trigger.id, name: trigger.name, fireTime: now });
196
+ if (this.fireCallback) {
197
+ this.fireCallback(msg, trigger);
198
+ }
199
+ }
200
+ // Called by MessageProcessor when a trigger message completes/fails/is interrupted
201
+ onTriggerComplete(triggerId, _outcome) {
202
+ // Only clear inflight state — message-processor already published the relevant events
203
+ this.inflightCron.delete(triggerId);
204
+ }
205
+ buildSyntheticMessage(trigger, messageId) {
206
+ return {
207
+ channel: trigger.targetChannel,
208
+ channelId: trigger.targetChannelId,
209
+ selfId: this.aid,
210
+ threadId: trigger.targetThreadId ?? '',
211
+ agentId: trigger.agentId,
212
+ chatType: 'private',
213
+ peerId: `__trigger__:${trigger.id}`, // unique per trigger to prevent greedy merge
214
+ content: trigger.prompt,
215
+ messageId,
216
+ timestamp: Date.now(),
217
+ source: 'trigger',
218
+ triggerMeta: {
219
+ triggerId: trigger.id,
220
+ silent: trigger.targetSessionStrategy === 'silent',
221
+ },
222
+ };
223
+ }
224
+ }
@@ -24,12 +24,44 @@
24
24
  可多次调用发送多条消息 ,如果不想回复停止调用即可。
25
25
  禁止使用 AskUserQuestion 和 ExitPlanMode 工具——proactive 模式下应由你主动用 ctl send 与用户沟通。
26
26
 
27
+ ## trigger
28
+
29
+ [触发器] 你可以通过 /trigger 命令设置延迟或定时任务,系统会在指定时间重新激活你执行任务。
30
+
31
+ 注册触发器:
32
+ /trigger set --delay <时长> --prompt "<任务内容>" 延迟执行,如 30m、2h、1d
33
+ /trigger set --at <ISO时间> --prompt "<任务内容>" 指定时刻,如 2026-05-15T09:00
34
+ /trigger set --cron <表达式> --prompt "<任务内容>" 周期执行,如 "0 9 * * *"
35
+
36
+ 定位参数(默认当前上下文):
37
+ --channel <实例名> 目标通道实例
38
+ --channelid <id> 目标对话 ID
39
+ --thread <id> 目标 thread(与 --session 互斥,需通道支持)
40
+ --session latest 续接最后活跃会话(默认,用户可见输出)
41
+ --session silent 新建独立会话静默执行(不打扰用户,适合后台任务)
42
+
43
+ 其他参数:
44
+ --name <标识> 触发器名称(默认自动生成)
45
+ --agent <名称> 目标 agent(默认当前)
46
+
47
+ 管理:
48
+ /trigger 查看活跃触发器
49
+ /trigger list 查看所有触发器(含历史)
50
+ /trigger cancel <名称> 取消触发器
51
+
52
+ 使用原则:
53
+ - 用户要求"稍后/明天/定时"做某事时使用
54
+ - 你判断某任务需要延迟到特定时刻才合适时主动使用
55
+ - silent 适合:清理、扫描、生成文件等后台任务
56
+ - latest 适合:提醒用户、跟进对话、结果需要用户看到
57
+ - 触发器不支持修改,改内容请 cancel 后重建
58
+
27
59
 
28
60
  ---
29
61
 
30
62
  ## 格式说明
31
63
 
32
- 模板由多个以 `## 段名` 分隔的段组成,加载器只识别 `runtime`、`group`、`proactive` 三段,其它段(包括本说明)会被忽略,可以随意增删。
64
+ 模板由多个以 `## 段名` 分隔的段组成,加载器只识别 `runtime`、`group`、`proactive`、`trigger` 四段,其它段(包括本说明)会被忽略,可以随意增删。
33
65
 
34
66
  **占位符语法:**
35
67
 
@@ -46,6 +78,7 @@
46
78
  | `runtime` | 每次消息 | 每条用户消息都会注入 |
47
79
  | `group` | `chatType === 'group' && peerId` | 仅群聊消息注入 |
48
80
  | `proactive` | `sessionMode === 'proactive'` | 仅 proactive 会话注入 |
81
+ | `trigger` | 非触发器来源的消息 | 让 AI 知道可以使用 /trigger 命令 |
49
82
 
50
83
  三段以换行拼接,追加到该消息的 system prompt 末尾。
51
84