evolclaw 2.8.3 → 3.1.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/README.md +21 -12
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +740 -777
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/prompts.md +0 -104
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
- package/dist/utils/upgrade.js +0 -100
|
@@ -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).unref();
|
|
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
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": [
|
|
3
|
+
{
|
|
4
|
+
"id": "context-too-long-cn",
|
|
5
|
+
"match": "上下文过长",
|
|
6
|
+
"action": "stop",
|
|
7
|
+
"type": "context_too_long",
|
|
8
|
+
"message": "⚠️ 上下文过长,请手动输入 /compact 压缩上下文"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "context-too-long-en",
|
|
12
|
+
"match": "context too long",
|
|
13
|
+
"action": "stop",
|
|
14
|
+
"type": "context_too_long",
|
|
15
|
+
"message": "⚠️ 上下文过长,请手动输入 /compact 压缩上下文"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "prompt-too-long",
|
|
19
|
+
"match": "prompt is too long",
|
|
20
|
+
"action": "stop",
|
|
21
|
+
"type": "context_too_long",
|
|
22
|
+
"message": "⚠️ 输入过长,请精简提问或使用 /compact 压缩上下文"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "invalid-api-key",
|
|
26
|
+
"match": "invalid api key",
|
|
27
|
+
"action": "stop",
|
|
28
|
+
"type": "auth_error",
|
|
29
|
+
"message": "❌ API Key 无效,请检查密钥配置。使用 /status 查看当前配置"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "key-not-found",
|
|
33
|
+
"match": "key_not_found",
|
|
34
|
+
"action": "stop",
|
|
35
|
+
"type": "auth_error",
|
|
36
|
+
"message": "❌ API Key 未找到,请检查密钥配置"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "credit-query-timeout",
|
|
40
|
+
"match": "积分查询超时",
|
|
41
|
+
"action": "retry",
|
|
42
|
+
"type": "api_error",
|
|
43
|
+
"message": "⚠️ 积分查询超时,正在重试..."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "api-error-400",
|
|
47
|
+
"match": "api error: 400",
|
|
48
|
+
"action": "stop",
|
|
49
|
+
"type": "api_error",
|
|
50
|
+
"message": "❌ 请求格式错误,请检查输入内容"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "api-error-403",
|
|
54
|
+
"match": "api error: 403",
|
|
55
|
+
"action": "retry",
|
|
56
|
+
"type": "api_error",
|
|
57
|
+
"message": "⚠️ API 访问受限,正在重试..."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "api-error-429",
|
|
61
|
+
"match": "api error: 429",
|
|
62
|
+
"action": "retry",
|
|
63
|
+
"type": "api_error",
|
|
64
|
+
"message": "⚠️ 请求过于频繁,正在重试..."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "api-error-500",
|
|
68
|
+
"match": "api error: 500",
|
|
69
|
+
"action": "retry",
|
|
70
|
+
"type": "api_error",
|
|
71
|
+
"message": "❌ API 服务暂时不可用,正在重试..."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "api-error-502",
|
|
75
|
+
"match": "api error: 502",
|
|
76
|
+
"action": "retry",
|
|
77
|
+
"type": "api_error"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "api-error-503",
|
|
81
|
+
"match": "api error: 503",
|
|
82
|
+
"action": "retry",
|
|
83
|
+
"type": "api_error"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "api-error-504",
|
|
87
|
+
"match": "api error: 504",
|
|
88
|
+
"action": "retry",
|
|
89
|
+
"type": "api_error"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "not-valid-json",
|
|
93
|
+
"match": "is not valid json",
|
|
94
|
+
"action": "retry",
|
|
95
|
+
"type": "api_error",
|
|
96
|
+
"message": "⚠️ API 返回异常响应,正在重试..."
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "feishu-permission",
|
|
100
|
+
"match": "im:resource",
|
|
101
|
+
"action": "stop",
|
|
102
|
+
"type": "unknown",
|
|
103
|
+
"message": "❌ 权限不足,请联系管理员配置应用权限"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "stream-error",
|
|
107
|
+
"match": "stream",
|
|
108
|
+
"action": "retry",
|
|
109
|
+
"type": "stream_error"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "request-aborted",
|
|
113
|
+
"match": "request was aborted",
|
|
114
|
+
"action": "ignore",
|
|
115
|
+
"type": "stream_error"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const BASEAGENT_CAPS = {
|
|
2
|
+
'claude-code': {
|
|
3
|
+
autoLoadsRules: true,
|
|
4
|
+
supportsSystemPrompt: true,
|
|
5
|
+
},
|
|
6
|
+
'claude': {
|
|
7
|
+
autoLoadsRules: true,
|
|
8
|
+
supportsSystemPrompt: true,
|
|
9
|
+
},
|
|
10
|
+
'codex': {
|
|
11
|
+
autoLoadsRules: false,
|
|
12
|
+
supportsSystemPrompt: true,
|
|
13
|
+
},
|
|
14
|
+
'gemini': {
|
|
15
|
+
autoLoadsRules: false,
|
|
16
|
+
supportsSystemPrompt: true,
|
|
17
|
+
},
|
|
18
|
+
};
|