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
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
|
|
2
2
|
import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
|
|
3
3
|
import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
|
|
4
|
-
import {
|
|
4
|
+
import { ensureDataDirs, resolvePaths, getPackageRoot } from './paths.js';
|
|
5
|
+
import { resolveAnthropicConfig } from './agents/resolve.js';
|
|
6
|
+
import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded } from './config-store.js';
|
|
7
|
+
import { CONFIG_SCHEMA_VERSION } from './types.js';
|
|
5
8
|
import { SessionManager } from './core/session/session-manager.js';
|
|
6
9
|
import { ClaudeAgentPlugin } from './agents/claude-runner.js';
|
|
7
10
|
import { CodexAgentPlugin } from './agents/codex-runner.js';
|
|
@@ -12,25 +15,114 @@ import { AUNChannelPlugin } from './channels/aun.js';
|
|
|
12
15
|
import { DingtalkChannelPlugin } from './channels/dingtalk.js';
|
|
13
16
|
import { QQBotChannelPlugin } from './channels/qqbot.js';
|
|
14
17
|
import { WecomChannelPlugin } from './channels/wecom.js';
|
|
15
|
-
import { MessageProcessor } from './core/message/message-processor.js';
|
|
18
|
+
import { MessageProcessor, buildEnvelope } from './core/message/message-processor.js';
|
|
16
19
|
import { MessageQueue } from './core/message/message-queue.js';
|
|
17
20
|
import { MessageBridge } from './core/message/message-bridge.js';
|
|
18
21
|
import { MessageCache } from './core/message/message-cache.js';
|
|
19
22
|
import { CommandHandler } from './core/command-handler.js';
|
|
20
23
|
import { EventBus } from './core/event-bus.js';
|
|
21
|
-
import { StatsCollector } from './utils/stats
|
|
24
|
+
import { StatsCollector } from './utils/stats.js';
|
|
25
|
+
import { AidStatsCollector } from './utils/stats.js';
|
|
22
26
|
import { PermissionGateway } from './core/permission.js';
|
|
23
27
|
import { InteractionRouter } from './core/interaction-router.js';
|
|
24
28
|
import { ChannelLoader } from './core/channel-loader.js';
|
|
25
|
-
import { AgentLoader } from './core/
|
|
29
|
+
import { AgentLoader } from './core/baseagent-loader.js';
|
|
26
30
|
import { EvolAgentRegistry } from './core/evolagent-registry.js';
|
|
27
|
-
import { buildReloadHooks } from './
|
|
31
|
+
import { buildReloadHooks } from './core/channel-loader.js';
|
|
28
32
|
import { IpcServer } from './ipc.js';
|
|
29
33
|
import { logger, setLogLevel } from './utils/logger.js';
|
|
34
|
+
import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
|
|
30
35
|
import { detectDuplicates } from './core/evolagent-registry.js';
|
|
31
|
-
import {
|
|
36
|
+
import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './agents/kit-renderer.js';
|
|
37
|
+
import { initEck } from './eck/init.js';
|
|
38
|
+
import { TriggerManager } from './core/trigger/manager.js';
|
|
39
|
+
import { TriggerScheduler, calcNextFireAt } from './core/trigger/scheduler.js';
|
|
40
|
+
import { agentTriggersDir } from './paths.js';
|
|
41
|
+
import { isLinkedInstall } from './utils/npm-ops.js';
|
|
32
42
|
import path from 'path';
|
|
33
43
|
import fs from 'fs';
|
|
44
|
+
import os from 'os';
|
|
45
|
+
import crypto from 'crypto';
|
|
46
|
+
import { fileURLToPath } from 'url';
|
|
47
|
+
/** 出站 payload 摘要(用于 channel-out.log) */
|
|
48
|
+
function summarizeOutboundPayload(payload) {
|
|
49
|
+
if (!payload)
|
|
50
|
+
return { kind: 'unknown' };
|
|
51
|
+
const s = { kind: payload.kind };
|
|
52
|
+
switch (payload.kind) {
|
|
53
|
+
case 'activity.batch':
|
|
54
|
+
s.itemCount = payload.items?.length ?? 0;
|
|
55
|
+
s.items = payload.items;
|
|
56
|
+
break;
|
|
57
|
+
case 'result.text':
|
|
58
|
+
s.isFinal = payload.isFinal;
|
|
59
|
+
s.text = payload.text;
|
|
60
|
+
break;
|
|
61
|
+
case 'result.file':
|
|
62
|
+
s.filePath = payload.filePath;
|
|
63
|
+
break;
|
|
64
|
+
case 'system.notice':
|
|
65
|
+
case 'system.error':
|
|
66
|
+
s.subtype = payload.subtype;
|
|
67
|
+
s.text = payload.text;
|
|
68
|
+
break;
|
|
69
|
+
case 'interaction':
|
|
70
|
+
s.interactionId = payload.interaction?.id;
|
|
71
|
+
s.interactionKind = payload.interaction?.kind?.kind;
|
|
72
|
+
break;
|
|
73
|
+
case 'status.started':
|
|
74
|
+
case 'status.completed':
|
|
75
|
+
case 'status.interrupted':
|
|
76
|
+
case 'status.error':
|
|
77
|
+
case 'status.timeout':
|
|
78
|
+
s.metadata = payload.metadata;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 通过 adapter.send 发送系统类 payload(system.notice / system.error / 等)。
|
|
85
|
+
*
|
|
86
|
+
* 网关层(本文件)的所有出站系统通知(上线 / 重启完成 / 渠道告警 / agent 启动失败等)
|
|
87
|
+
* 走这里集中调度,让渠道按 capabilities 决定呈现方式。
|
|
88
|
+
*
|
|
89
|
+
* 当 adapter 还没实现 send(旧 adapter)时,按 payload.kind 降级到 sendText。
|
|
90
|
+
*
|
|
91
|
+
* Exported for unit test coverage; runtime callers are inside main() closure.
|
|
92
|
+
*/
|
|
93
|
+
export async function sendSystemPayload(adapter, envelope, payload) {
|
|
94
|
+
await adapter.send(envelope, payload);
|
|
95
|
+
}
|
|
96
|
+
function readEvolclawVersion() {
|
|
97
|
+
try {
|
|
98
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
|
|
99
|
+
return pkg.version || 'unknown';
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return 'unknown';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function readFastaunVersion() {
|
|
106
|
+
try {
|
|
107
|
+
const url = import.meta.resolve?.('@agentunion/fastaun');
|
|
108
|
+
if (!url)
|
|
109
|
+
return 'unknown';
|
|
110
|
+
let dir = path.dirname(fileURLToPath(url));
|
|
111
|
+
while (dir !== path.dirname(dir)) {
|
|
112
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
113
|
+
if (fs.existsSync(pkgPath)) {
|
|
114
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
115
|
+
if (pkg.name === '@agentunion/fastaun')
|
|
116
|
+
return pkg.version || 'unknown';
|
|
117
|
+
}
|
|
118
|
+
dir = path.dirname(dir);
|
|
119
|
+
}
|
|
120
|
+
return 'unknown';
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return 'unknown';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
34
126
|
async function main() {
|
|
35
127
|
// 过滤飞书 SDK 的 info 日志
|
|
36
128
|
const originalLog = console.log;
|
|
@@ -49,83 +141,136 @@ async function main() {
|
|
|
49
141
|
return;
|
|
50
142
|
originalInfo(...args);
|
|
51
143
|
};
|
|
52
|
-
logger.info(
|
|
144
|
+
logger.info(`EvolClaw v${readEvolclawVersion()} starting... (fastaun v${readFastaunVersion()})`);
|
|
53
145
|
// 确保数据目录存在
|
|
54
146
|
ensureDataDirs();
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
147
|
+
// ── 单实例保护(pre-check + post-write self-check)──
|
|
148
|
+
// pre-check:发现已有活 main 直接退出,避免起任何副作用
|
|
149
|
+
{
|
|
150
|
+
const pre = scanInstances();
|
|
151
|
+
const aliveOthers = pre.mains.filter(m => m.alive && m.record.pid !== process.pid);
|
|
152
|
+
if (aliveOthers.length > 0) {
|
|
153
|
+
const pids = aliveOthers.map(m => m.record.pid).join(', ');
|
|
154
|
+
const msg = `❌ Another EvolClaw instance is already running (PID: ${pids}). Use 'evolclaw restart' to replace it.`;
|
|
155
|
+
logger.error(msg);
|
|
156
|
+
console.error(msg);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
62
159
|
}
|
|
160
|
+
// 立即登记自己(让其他并发启动者能看见我)
|
|
161
|
+
const launchedBy = process.env.EVOLCLAW_LAUNCHED_BY || 'start';
|
|
162
|
+
writeMain(launchedBy);
|
|
163
|
+
logger.info(`✓ Instance record written: main-${process.pid}.json`);
|
|
164
|
+
// post-write 自检:写完 record 后再扫一次,发现并发对手时按 (startedAt, pid) 选赢家
|
|
165
|
+
{
|
|
166
|
+
const verdict = isMainWinner();
|
|
167
|
+
if (!verdict.winner) {
|
|
168
|
+
logger.warn(`Lost main election to PID ${verdict.conflictingPid}, yielding`);
|
|
169
|
+
console.error(`⚠ Another instance (PID ${verdict.conflictingPid}) started concurrently and won the election. Yielding.`);
|
|
170
|
+
removeAll();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ── 自动迁移 ──
|
|
175
|
+
migrateIdentitiesIfNeeded();
|
|
176
|
+
autoMigrateIfNeeded();
|
|
177
|
+
// ── ECK 运行时初始化 ──
|
|
178
|
+
initEck();
|
|
179
|
+
// 加载 ECK manifest + 清理旧调试文件
|
|
180
|
+
cleanEckDebug();
|
|
181
|
+
loadKitManifest();
|
|
182
|
+
// 加载配置(新结构:defaults.json + per-agent config.json)
|
|
183
|
+
const defaults = loadDefaults() ?? { $schema_version: CONFIG_SCHEMA_VERSION };
|
|
184
|
+
// 应用配置中的日志级别(优先于环境变量)
|
|
185
|
+
// logLevel 现在不在新结构中——若要保留,将来可加 defaults.debug.logLevel
|
|
186
|
+
// 阶段 2c 暂跳过
|
|
63
187
|
const paths = resolvePaths();
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
188
|
+
// ── EvolAgent Registry:加载 agents/<aid>/config.json ──
|
|
189
|
+
const agentRegistry = new EvolAgentRegistry(paths.agentsDir);
|
|
190
|
+
agentRegistry.loadAll();
|
|
191
|
+
const agentInfos = agentRegistry.list();
|
|
192
|
+
// 启动期硬约束:必须至少有一个 self-agent
|
|
193
|
+
if (agentInfos.length === 0) {
|
|
194
|
+
const skipped = agentRegistry.getSkipped();
|
|
195
|
+
const lines = [
|
|
196
|
+
'❌ No self-agent configured.',
|
|
197
|
+
` Run \`evolclaw aid new <name>\` to create one.`,
|
|
198
|
+
];
|
|
199
|
+
if (skipped.length > 0) {
|
|
200
|
+
lines.push(` Skipped ${skipped.length} dir(s):`);
|
|
201
|
+
for (const s of skipped)
|
|
202
|
+
lines.push(` - ${s.dirName}: ${s.reason}`);
|
|
203
|
+
}
|
|
204
|
+
const msg = lines.join('\n');
|
|
68
205
|
logger.error(msg);
|
|
69
|
-
console.error(msg);
|
|
206
|
+
console.error(msg);
|
|
70
207
|
process.exit(1);
|
|
71
208
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
209
|
+
logger.info(`✓ Loaded ${agentInfos.length} self-agent(s)`);
|
|
210
|
+
for (const info of agentInfos) {
|
|
211
|
+
if (info.status === 'error') {
|
|
212
|
+
logger.error(` ✗ ${info.name}: ${info.error}`);
|
|
213
|
+
}
|
|
214
|
+
else if (info.status === 'disabled') {
|
|
215
|
+
logger.info(` ○ ${info.name} (disabled)`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
logger.info(` ● ${info.name} ${info.baseagent} @ ${path.basename(info.projectPath)}`);
|
|
82
219
|
}
|
|
83
220
|
}
|
|
84
|
-
|
|
85
|
-
|
|
221
|
+
// 跨 agent 凭证冲突
|
|
222
|
+
{
|
|
223
|
+
const dups = detectDuplicates(agentRegistry.runnableAgents());
|
|
224
|
+
for (const d of dups) {
|
|
225
|
+
const owners = d.agents.map(o => `${o.aid}(${o.channelName})`).join(', ');
|
|
226
|
+
logger.warn(`⚠ Duplicate channel credential: ${d.fingerprint} claimed by ${owners}.`);
|
|
227
|
+
}
|
|
86
228
|
}
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
229
|
+
// 选定主 agent(启动期 anthropic resolve 用,配合 IPC `evolagent.list` 显示)
|
|
230
|
+
// 主 agent 取第一个非 error 非 disabled 的 self-agent。
|
|
231
|
+
const primaryAgent = agentRegistry.runnableAgents()[0];
|
|
232
|
+
if (!primaryAgent) {
|
|
233
|
+
const msg = '❌ No runnable self-agent (all are error/disabled). Aborting.';
|
|
234
|
+
logger.error(msg);
|
|
235
|
+
console.error(msg);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
// 进程级设置(从 defaults 取,不属于任何 agent)
|
|
239
|
+
const globalSettings = {
|
|
240
|
+
idleMonitor: defaults.idleMonitor,
|
|
241
|
+
debug: defaults.debug,
|
|
242
|
+
};
|
|
243
|
+
if (globalSettings.debug?.logLevel) {
|
|
244
|
+
setLogLevel(globalSettings.debug.logLevel);
|
|
245
|
+
}
|
|
246
|
+
// 启动期 anthropic 凭证校验(用 primaryAgent 的 baseagents.claude)
|
|
247
|
+
const anthropic = resolveAnthropicConfig({
|
|
248
|
+
agents: { claude: primaryAgent.config.baseagents?.claude },
|
|
95
249
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (evolagentCount > 0) {
|
|
100
|
-
logger.info(`✓ Loaded ${evolagentCount} evolagent(s)`);
|
|
101
|
-
for (const info of agentInfos) {
|
|
102
|
-
if (info.isDefault)
|
|
103
|
-
continue;
|
|
104
|
-
if (info.status === 'error') {
|
|
105
|
-
logger.error(` ✗ [${info.name}] ${info.error}`);
|
|
106
|
-
}
|
|
107
|
-
else if (info.status === 'disabled') {
|
|
108
|
-
logger.info(` ○ [${info.name}] disabled`);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
logger.info(` ● [${info.name}] ${info.baseagent} @ ${path.basename(info.projectPath)}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
250
|
+
logger.info('✓ Config loaded (API keys hidden)');
|
|
251
|
+
if (anthropic.baseUrl) {
|
|
252
|
+
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
114
253
|
}
|
|
115
254
|
// Store for IPC access (T10 will wire this)
|
|
116
255
|
// M4: removed dead globalThis.__evolclaw_agentRegistry assignment
|
|
117
256
|
// 创建事件总线
|
|
118
257
|
const eventBus = new EventBus();
|
|
119
258
|
logger.info('✓ Event bus initialized');
|
|
259
|
+
// 把所有事件录到 events.log(受 EVENT_LOG 环境变量控制)
|
|
260
|
+
eventBus.subscribeAll((event) => logger.event(event));
|
|
120
261
|
// 统计收集器(近 1 小时滚动统计)
|
|
121
262
|
const statsCollector = new StatsCollector(eventBus);
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
const sessionManager = new SessionManager(
|
|
126
|
-
// sessionMode
|
|
127
|
-
sessionManager.setSessionModeResolver((
|
|
128
|
-
|
|
263
|
+
// Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
|
|
264
|
+
const aidStatsCollector = new AidStatsCollector();
|
|
265
|
+
// 初始化 SessionManager(文件系统后端)
|
|
266
|
+
const sessionManager = new SessionManager(paths.sessionsDir, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId), (channel, userId) => agentRegistry.isAdmin(channel, userId));
|
|
267
|
+
// sessionMode 解析:从 channel 路由到具体 agent,按 agent.config.chatmode
|
|
268
|
+
sessionManager.setSessionModeResolver((channelKey, chatType) => {
|
|
269
|
+
const agent = agentRegistry.resolveByChannel(channelKey);
|
|
270
|
+
const cm = agent?.config.chatmode;
|
|
271
|
+
if (!cm)
|
|
272
|
+
return undefined;
|
|
273
|
+
return chatType === 'group' ? cm.group : cm.private;
|
|
129
274
|
});
|
|
130
275
|
logger.info('✓ Database initialized');
|
|
131
276
|
// 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
|
|
@@ -137,23 +282,23 @@ async function main() {
|
|
|
137
282
|
agentLoader.register(new ClaudeAgentPlugin());
|
|
138
283
|
agentLoader.register(new CodexAgentPlugin());
|
|
139
284
|
agentLoader.register(new GeminiAgentPlugin());
|
|
140
|
-
const agentInstances = agentLoader.createAll(
|
|
285
|
+
const agentInstances = agentLoader.createAll(agentRegistry, {
|
|
141
286
|
onSessionIdUpdate: async (sessionId, agentSessionId) => {
|
|
142
287
|
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
143
288
|
},
|
|
144
289
|
});
|
|
145
|
-
// agentMap 复合键:${
|
|
290
|
+
// agentMap 复合键:${aid}::${baseagent}
|
|
146
291
|
const agentMap = new Map();
|
|
147
292
|
for (const inst of agentInstances) {
|
|
148
293
|
agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
|
|
149
294
|
}
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const agentRunner = agentMap.get(
|
|
295
|
+
const primaryBaseagent = primaryAgent.baseagent;
|
|
296
|
+
const primaryRunnerKey = `${primaryAgent.aid}::${primaryBaseagent}`;
|
|
297
|
+
const agentRunner = agentMap.get(primaryRunnerKey) || agentInstances[0]?.agent;
|
|
153
298
|
if (!agentRunner) {
|
|
154
|
-
throw new Error('No agent backend available. Check
|
|
299
|
+
throw new Error('No agent backend available. Check baseagents config (no runners created).');
|
|
155
300
|
}
|
|
156
|
-
logger.info(`✓ Runners ready (
|
|
301
|
+
logger.info(`✓ Runners ready (primary key: ${primaryRunnerKey}, total: ${agentMap.size}, keys: ${[...agentMap.keys()].join(', ')})`);
|
|
157
302
|
// 权限审批网关
|
|
158
303
|
const permissionGateway = new PermissionGateway();
|
|
159
304
|
permissionGateway.setEventBus(eventBus);
|
|
@@ -178,67 +323,46 @@ async function main() {
|
|
|
178
323
|
channelLoader.register(new DingtalkChannelPlugin());
|
|
179
324
|
channelLoader.register(new QQBotChannelPlugin());
|
|
180
325
|
channelLoader.register(new WecomChannelPlugin());
|
|
181
|
-
// Create channel instances:
|
|
182
|
-
const defaultInstances = await channelLoader.createAll(config);
|
|
326
|
+
// Create channel instances: 每个 self-agent 各自的 channels
|
|
183
327
|
const evolagentInstances = [];
|
|
184
328
|
for (const agent of agentRegistry.runnableAgents()) {
|
|
185
|
-
// Rewrite channel instance names with agent prefix to avoid collisions
|
|
186
|
-
// with DefaultAgent and other EvolAgents.
|
|
187
|
-
// Rule (EvolAgent only):
|
|
188
|
-
// - explicit name → `${agent.name}-${type}-${name}`
|
|
189
|
-
// - omitted name → `${agent.name}-${type}`
|
|
190
|
-
const rewrittenChannels = {};
|
|
191
|
-
for (const [type, raw] of Object.entries(agent.config.channels || {})) {
|
|
192
|
-
if (type === 'defaultChannel') {
|
|
193
|
-
rewrittenChannels[type] = raw;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
const instances = Array.isArray(raw) ? raw : [raw];
|
|
197
|
-
const rewritten = instances.map((inst) => {
|
|
198
|
-
if (!inst || typeof inst !== 'object')
|
|
199
|
-
return inst;
|
|
200
|
-
const effName = agent.effectiveChannelName(type, inst.name);
|
|
201
|
-
return { ...inst, name: effName };
|
|
202
|
-
});
|
|
203
|
-
// Preserve original shape (array vs single object)
|
|
204
|
-
rewrittenChannels[type] = Array.isArray(raw) ? rewritten : rewritten[0];
|
|
205
|
-
}
|
|
206
|
-
const agentConfig = {
|
|
207
|
-
agents: agent.config.agents,
|
|
208
|
-
channels: rewrittenChannels,
|
|
209
|
-
projects: agent.config.projects,
|
|
210
|
-
};
|
|
211
329
|
try {
|
|
212
|
-
const instances = await channelLoader.
|
|
330
|
+
const instances = await channelLoader.createForAgent(agent);
|
|
213
331
|
evolagentInstances.push(...instances);
|
|
214
332
|
}
|
|
215
333
|
catch (e) {
|
|
216
|
-
logger.error(`[
|
|
334
|
+
logger.error(`[Agent ${agent.aid}] Failed to create channels: ${e}`);
|
|
217
335
|
agent.status = 'error';
|
|
218
336
|
agent.error = `Channel creation failed: ${e}`;
|
|
219
337
|
}
|
|
220
338
|
}
|
|
221
|
-
const channelInstances =
|
|
222
|
-
logger.info(`✓ Created ${channelInstances.length} channel instance(s)
|
|
223
|
-
//
|
|
224
|
-
|
|
339
|
+
const channelInstances = evolagentInstances;
|
|
340
|
+
logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
|
|
341
|
+
// 初始化触发器调度器(每个 EvolAgent 独立)
|
|
342
|
+
for (const agent of agentRegistry.runnableAgents()) {
|
|
343
|
+
const triggersDir = agentTriggersDir(agent.aid);
|
|
344
|
+
const triggerManager = new TriggerManager(agent.aid, triggersDir);
|
|
345
|
+
const triggerScheduler = new TriggerScheduler(agent.aid, triggerManager, eventBus);
|
|
346
|
+
agent.triggerManager = triggerManager;
|
|
347
|
+
agent.triggerScheduler = triggerScheduler;
|
|
348
|
+
}
|
|
225
349
|
// 创建命令处理器
|
|
226
|
-
const cmdHandler = new CommandHandler(sessionManager, agentMap,
|
|
350
|
+
const cmdHandler = new CommandHandler(sessionManager, agentMap, messageCache, eventBus, primaryRunnerKey);
|
|
227
351
|
cmdHandler.setPermissionGateway(permissionGateway);
|
|
228
352
|
cmdHandler.setInteractionRouter(interactionRouter);
|
|
229
353
|
cmdHandler.setStatsCollector(statsCollector);
|
|
230
354
|
// 创建消息处理器
|
|
231
|
-
const processor = new MessageProcessor(agentMap, sessionManager,
|
|
355
|
+
const processor = new MessageProcessor(agentMap, sessionManager, globalSettings, messageCache, eventBus, (content, channel, channelId, userId, threadId) => {
|
|
232
356
|
const sendFn = async (id, text, opts) => {
|
|
233
357
|
const adapter = cmdHandler.getAdapter(channel);
|
|
234
358
|
if (!adapter)
|
|
235
359
|
return;
|
|
236
360
|
if (text) {
|
|
237
|
-
await adapter.
|
|
361
|
+
await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: id, replyContext: opts }), { kind: 'system.notice', text, subtype: 'health' });
|
|
238
362
|
}
|
|
239
363
|
};
|
|
240
364
|
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
241
|
-
},
|
|
365
|
+
}, primaryRunnerKey);
|
|
242
366
|
// 回填 processor 和 messageQueue 的引用
|
|
243
367
|
cmdHandler.setProcessor(processor);
|
|
244
368
|
// Inject EvolAgentRegistry (methods added by T6/T7)
|
|
@@ -262,10 +386,10 @@ async function main() {
|
|
|
262
386
|
});
|
|
263
387
|
// 设置中断回调(精确中断正在处理的 agent)
|
|
264
388
|
messageQueue.setInterruptCallback(async (sessionKey, agentId, evolagentName) => {
|
|
265
|
-
const baseagent = agentId ||
|
|
266
|
-
const evol = evolagentName ||
|
|
389
|
+
const baseagent = agentId || primaryBaseagent;
|
|
390
|
+
const evol = evolagentName || primaryAgent.aid;
|
|
267
391
|
const agent = agentMap.get(`${evol}::${baseagent}`)
|
|
268
|
-
|| agentMap.get(
|
|
392
|
+
|| agentMap.get(primaryRunnerKey);
|
|
269
393
|
if (agent?.hasActiveStream(sessionKey)) {
|
|
270
394
|
await agent.interrupt(sessionKey);
|
|
271
395
|
}
|
|
@@ -274,6 +398,83 @@ async function main() {
|
|
|
274
398
|
// 回填 messageQueue 引用
|
|
275
399
|
cmdHandler.setMessageQueue(messageQueue);
|
|
276
400
|
processor.setMessageQueue(messageQueue);
|
|
401
|
+
// 启动触发器调度器,设置 fireCallback 投递合成消息
|
|
402
|
+
for (const agent of agentRegistry.runnableAgents()) {
|
|
403
|
+
if (!agent.triggerScheduler || !agent.triggerManager)
|
|
404
|
+
continue;
|
|
405
|
+
const scheduler = agent.triggerScheduler;
|
|
406
|
+
const primaryProjectPath = agent.config.projects?.defaultPath ?? primaryAgent.projectPath;
|
|
407
|
+
scheduler.setFireCallback((msg, trigger) => {
|
|
408
|
+
const sessionKey = `${msg.channel}:${msg.channelId}`;
|
|
409
|
+
messageQueue.enqueue(sessionKey, msg, primaryProjectPath, { interruptible: false }).catch(err => {
|
|
410
|
+
logger.error(`[Trigger] Failed to enqueue trigger ${trigger.id}: ${err}`);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
// Subscribe to trigger:completed/failed/skipped to update cron inflight state
|
|
414
|
+
eventBus.subscribe('trigger:completed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'completed'));
|
|
415
|
+
eventBus.subscribe('trigger:failed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'failed'));
|
|
416
|
+
eventBus.subscribe('trigger:skipped', (ev) => {
|
|
417
|
+
if (ev.reason === 'interrupted')
|
|
418
|
+
scheduler.onTriggerComplete(ev.triggerId, 'interrupted');
|
|
419
|
+
});
|
|
420
|
+
// Note: only the primary agent's scheduler is wired to cmdHandler.
|
|
421
|
+
// Non-primary agent channels will receive "⚠️ 触发器功能未启用" when using /trigger.
|
|
422
|
+
// Full per-channel scheduler routing is a future improvement.
|
|
423
|
+
try {
|
|
424
|
+
await scheduler.init();
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
logger.error(`[Trigger] Scheduler init failed for ${agent.aid}: ${err}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Inject primary agent's trigger scheduler into cmdHandler
|
|
431
|
+
const primaryAgentForTrigger = agentRegistry.runnableAgents()[0];
|
|
432
|
+
if (primaryAgentForTrigger?.triggerScheduler && primaryAgentForTrigger?.triggerManager) {
|
|
433
|
+
cmdHandler.setTriggerScheduler(primaryAgentForTrigger.triggerScheduler, primaryAgentForTrigger.triggerManager);
|
|
434
|
+
}
|
|
435
|
+
// Seed default __upgrade-check trigger (daily at random time 3:00~3:59)
|
|
436
|
+
// 用户可通过 /trigger cancel __upgrade-check 永久禁用(不会再自动重建)
|
|
437
|
+
if (!isLinkedInstall() && primaryAgentForTrigger?.triggerManager && primaryAgentForTrigger?.triggerScheduler) {
|
|
438
|
+
const mgr = primaryAgentForTrigger.triggerManager;
|
|
439
|
+
const sched = primaryAgentForTrigger.triggerScheduler;
|
|
440
|
+
const UPGRADE_TRIGGER_NAME = '__upgrade-check';
|
|
441
|
+
if (!mgr.getByName(UPGRADE_TRIGGER_NAME)) {
|
|
442
|
+
// Check history: if user cancelled it before, respect that decision
|
|
443
|
+
const { history } = mgr.listAll();
|
|
444
|
+
const wasCancelled = history.some(h => h.name === UPGRADE_TRIGGER_NAME && h.doneReason === 'cancelled');
|
|
445
|
+
if (!wasCancelled) {
|
|
446
|
+
// Random minute in 3:00~3:59 to avoid all instances hitting registry simultaneously
|
|
447
|
+
const randomMinute = Math.floor(Math.random() * 60);
|
|
448
|
+
const cronExpr = `${randomMinute} 3 * * *`;
|
|
449
|
+
// Use first channel instance as target (command doesn't need real channelId)
|
|
450
|
+
const firstChannel = channelInstances[0]?.adapter?.channelName || 'system';
|
|
451
|
+
const trigger = {
|
|
452
|
+
id: crypto.randomUUID(),
|
|
453
|
+
name: UPGRADE_TRIGGER_NAME,
|
|
454
|
+
scheduleType: 'cron',
|
|
455
|
+
scheduleValue: cronExpr,
|
|
456
|
+
nextFireAt: calcNextFireAt('cron', cronExpr),
|
|
457
|
+
targetChannel: firstChannel,
|
|
458
|
+
targetChannelId: '__system__',
|
|
459
|
+
targetSessionStrategy: 'silent',
|
|
460
|
+
prompt: '检查 evolclaw 是否有新版本可用。执行 `npm view evolclaw version` 获取最新版本,与当前版本(执行 `evolclaw --version`)对比。如果有新版本,执行 /restart 进行升级。如果已是最新版本,无需任何操作。',
|
|
461
|
+
createdByPeerId: '__system__',
|
|
462
|
+
createdByChannel: '__system__',
|
|
463
|
+
fireCount: 0,
|
|
464
|
+
createdAt: Date.now(),
|
|
465
|
+
updatedAt: Date.now(),
|
|
466
|
+
};
|
|
467
|
+
try {
|
|
468
|
+
mgr.register(trigger);
|
|
469
|
+
sched.register(trigger);
|
|
470
|
+
logger.info(`[Trigger] Seeded default trigger: ${UPGRADE_TRIGGER_NAME} (cron ${cronExpr})`);
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
logger.warn(`[Trigger] Failed to seed ${UPGRADE_TRIGGER_NAME}: ${e}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
277
478
|
// 默认策略
|
|
278
479
|
const defaultPolicy = {
|
|
279
480
|
canSwitchProject: (chatType, role) => chatType === 'private' ? (role === 'owner' || role === 'admin') : role === 'owner',
|
|
@@ -287,18 +488,23 @@ async function main() {
|
|
|
287
488
|
accumulateErrors: () => true,
|
|
288
489
|
};
|
|
289
490
|
// ── MessageBridge:Channel ↔ Core 消息桥梁 ──
|
|
290
|
-
const msgBridge = new MessageBridge(
|
|
491
|
+
const msgBridge = new MessageBridge(primaryAgent.projectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, primaryAgent.config.debounce);
|
|
291
492
|
msgBridge.setAgentRegistry(agentRegistry);
|
|
292
493
|
// ── Channel instance registration (shared by startup and hot-load) ──
|
|
293
494
|
function registerChannelInstance(inst) {
|
|
495
|
+
// 0. 包装 adapter.send,记录所有出站到 channel-out.log
|
|
496
|
+
const originalSend = inst.adapter.send.bind(inst.adapter);
|
|
497
|
+
inst.adapter.send = async (envelope, payload) => {
|
|
498
|
+
logger.channelOut({ channel: inst.adapter.channelName, channelId: envelope.channelId, taskId: envelope.taskId, payload: summarizeOutboundPayload(payload) });
|
|
499
|
+
return originalSend(envelope, payload);
|
|
500
|
+
};
|
|
294
501
|
// 1. 项目路径提供器
|
|
295
502
|
if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
|
|
296
503
|
inst.channel.onProjectPathRequest(async (channelId) => {
|
|
297
|
-
// Effective default path: agent
|
|
504
|
+
// Effective default path: use the agent that owns this channel.
|
|
298
505
|
const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelName);
|
|
299
|
-
const effectiveDefault =
|
|
300
|
-
|
|
301
|
-
: (config.projects?.defaultPath || process.cwd());
|
|
506
|
+
const effectiveDefault = owningAgent?.projectPath
|
|
507
|
+
?? primaryAgent.projectPath;
|
|
302
508
|
const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
|
|
303
509
|
return path.isAbsolute(session.projectPath)
|
|
304
510
|
? session.projectPath
|
|
@@ -321,114 +527,14 @@ async function main() {
|
|
|
321
527
|
interactionRouter.handle(response);
|
|
322
528
|
});
|
|
323
529
|
}
|
|
324
|
-
// 4. MessageBridge
|
|
530
|
+
// 4. MessageBridge 注册
|
|
325
531
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
await handler({
|
|
329
|
-
channel: channelType, channelId: chatId, content, images, chatType,
|
|
330
|
-
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
331
|
-
// 只在话题场景(threadId 有值)才设置 replyContext;
|
|
332
|
-
// 纯引用回复(rootId 有值但无 threadId)不设置,避免所有回复都带引用头
|
|
333
|
-
replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
|
|
334
|
-
});
|
|
335
|
-
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
|
|
336
|
-
replyToMessageId: replyContext?.replyToMessageId,
|
|
337
|
-
replyInThread: replyContext?.replyInThread,
|
|
338
|
-
}), inst.adapter, channelType);
|
|
339
|
-
}
|
|
340
|
-
if (channelType === 'wechat') {
|
|
341
|
-
if (inst.channel.setEventBus) {
|
|
342
|
-
inst.channel.setEventBus(eventBus);
|
|
343
|
-
}
|
|
344
|
-
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (channelId, content, peerId, images, chatType) => {
|
|
345
|
-
handler({
|
|
346
|
-
channel: channelType,
|
|
347
|
-
channelId,
|
|
348
|
-
content,
|
|
349
|
-
images,
|
|
350
|
-
chatType: chatType || 'private',
|
|
351
|
-
peerId: peerId || '',
|
|
352
|
-
});
|
|
353
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
354
|
-
}
|
|
355
|
-
if (channelType === 'aun') {
|
|
356
|
-
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (opts) => {
|
|
357
|
-
handler({
|
|
358
|
-
channel: channelType,
|
|
359
|
-
channelId: opts.channelId,
|
|
360
|
-
content: opts.content,
|
|
361
|
-
chatType: opts.chatType || 'private',
|
|
362
|
-
peerId: opts.peerId || '',
|
|
363
|
-
peerName: opts.peerName,
|
|
364
|
-
messageId: opts.messageId,
|
|
365
|
-
mentions: opts.mentions,
|
|
366
|
-
threadId: opts.threadId,
|
|
367
|
-
replyContext: opts.replyContext,
|
|
368
|
-
});
|
|
369
|
-
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, replyContext), inst.adapter, channelType);
|
|
370
|
-
// AUN 重连失败通知
|
|
371
|
-
if (inst.channel.setOnChannelDown) {
|
|
372
|
-
inst.channel.setOnChannelDown(() => {
|
|
373
|
-
eventBus.publish({
|
|
374
|
-
type: 'channel:health',
|
|
375
|
-
channel: channelType,
|
|
376
|
-
channelName: inst.adapter.channelName,
|
|
377
|
-
status: 'auth_error',
|
|
378
|
-
message: `⚠️ AUN 渠道 ${inst.adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
379
|
-
timestamp: Date.now(),
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
// proactive 模式入站白名单:注入 sessionMode 查询器
|
|
384
|
-
if (typeof inst.channel.setSessionModeResolver === 'function') {
|
|
385
|
-
const chName = inst.adapter.channelName;
|
|
386
|
-
inst.channel.setSessionModeResolver(async (channelId) => {
|
|
387
|
-
const session = await sessionManager.getActiveSession(chName, channelId);
|
|
388
|
-
return session?.sessionMode;
|
|
389
|
-
});
|
|
390
|
-
}
|
|
532
|
+
if (inst.registerBridge) {
|
|
533
|
+
inst.registerBridge(msgBridge, channelType);
|
|
391
534
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
channel: channelType,
|
|
396
|
-
channelId: event.channelId,
|
|
397
|
-
content: event.content,
|
|
398
|
-
images: event.images,
|
|
399
|
-
chatType: event.chatType || 'private',
|
|
400
|
-
peerId: event.peerId || '',
|
|
401
|
-
peerName: event.peerName,
|
|
402
|
-
messageId: event.messageId,
|
|
403
|
-
});
|
|
404
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
405
|
-
}
|
|
406
|
-
if (channelType === 'qqbot') {
|
|
407
|
-
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
|
|
408
|
-
handler({
|
|
409
|
-
channel: channelType,
|
|
410
|
-
channelId: event.channelId,
|
|
411
|
-
content: event.content,
|
|
412
|
-
images: event.images,
|
|
413
|
-
chatType: event.chatType || 'private',
|
|
414
|
-
peerId: event.peerId || '',
|
|
415
|
-
peerName: event.peerName,
|
|
416
|
-
messageId: event.messageId,
|
|
417
|
-
});
|
|
418
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
419
|
-
}
|
|
420
|
-
if (channelType === 'wecom') {
|
|
421
|
-
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
|
|
422
|
-
handler({
|
|
423
|
-
channel: channelType,
|
|
424
|
-
channelId: event.channelId,
|
|
425
|
-
content: event.content,
|
|
426
|
-
chatType: event.chatType || 'private',
|
|
427
|
-
peerId: event.peerId || '',
|
|
428
|
-
peerName: event.peerName,
|
|
429
|
-
messageId: event.messageId,
|
|
430
|
-
});
|
|
431
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
535
|
+
// 4b. 生命周期钩子
|
|
536
|
+
if (inst.registerHooks) {
|
|
537
|
+
inst.registerHooks({ eventBus, sessionManager });
|
|
432
538
|
}
|
|
433
539
|
// 5. 撤回消息 → 中断执行中任务
|
|
434
540
|
inst.channel.onRecall?.((messageId) => {
|
|
@@ -439,20 +545,22 @@ async function main() {
|
|
|
439
545
|
for (const inst of channelInstances) {
|
|
440
546
|
registerChannelInstance(inst);
|
|
441
547
|
}
|
|
442
|
-
//
|
|
443
|
-
const connected = await channelLoader.connectAll(channelInstances);
|
|
444
|
-
// Bind connected adapters to their owning agents
|
|
445
|
-
// I1: only mark 'running' if a channel actually connected for that agent
|
|
446
|
-
const connectedSet = new Set(connected);
|
|
548
|
+
// Bind adapters to their owning agents and mark running
|
|
447
549
|
for (const inst of channelInstances) {
|
|
448
550
|
const agent = agentRegistry.resolveByChannel(inst.adapter.channelName);
|
|
449
551
|
if (!agent || agent.status === 'error')
|
|
450
552
|
continue;
|
|
451
553
|
agent.channels.set(inst.adapter.channelName, inst.adapter);
|
|
452
|
-
if (agent.status === 'stopped'
|
|
554
|
+
if (agent.status === 'stopped') {
|
|
453
555
|
agent.status = 'running';
|
|
454
556
|
}
|
|
455
557
|
}
|
|
558
|
+
// 写入 ready 信号(核心服务已就绪,channel 连接不阻塞启动判定)
|
|
559
|
+
const readySignalPath = resolvePaths().readySignal;
|
|
560
|
+
fs.writeFileSync(readySignalPath, String(Date.now()));
|
|
561
|
+
logger.info(`✓ Ready signal written: ${readySignalPath}`);
|
|
562
|
+
// ── 连接所有渠道(异步,AUN 等 WebSocket 渠道在后台重连)──
|
|
563
|
+
const connected = await channelLoader.connectAll(channelInstances);
|
|
456
564
|
// 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
|
|
457
565
|
for (const inst of channelInstances) {
|
|
458
566
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
@@ -471,10 +579,51 @@ async function main() {
|
|
|
471
579
|
timestamp: Date.now()
|
|
472
580
|
});
|
|
473
581
|
}
|
|
582
|
+
// 上线通知:延迟 1-3 秒后向 owner 发送上线消息(带 name + 工作目录)
|
|
583
|
+
// 需在配置中 debug.upmsg: true 手动开启
|
|
584
|
+
setTimeout(() => {
|
|
585
|
+
for (const name of connected) {
|
|
586
|
+
const agent = agentRegistry.resolveByChannel(name);
|
|
587
|
+
if (!agent)
|
|
588
|
+
continue;
|
|
589
|
+
if (!agent.config.debug?.upmsg)
|
|
590
|
+
continue;
|
|
591
|
+
const ownerAid = agent.config.owners?.[0];
|
|
592
|
+
if (!ownerAid)
|
|
593
|
+
continue;
|
|
594
|
+
const adapter = agent.channels.get(name);
|
|
595
|
+
if (!adapter)
|
|
596
|
+
continue;
|
|
597
|
+
// 尝试从 agent.md 读取 name
|
|
598
|
+
let agentName = agent.aid;
|
|
599
|
+
try {
|
|
600
|
+
const aunPath = process.env.AUN_HOME || path.join(os.homedir(), '.aun');
|
|
601
|
+
const agentMdPath = path.join(aunPath, 'AIDs', agent.aid, 'agent.md');
|
|
602
|
+
const content = fs.readFileSync(agentMdPath, 'utf-8');
|
|
603
|
+
const nameMatch = content.match(/^name:\s*"?([^"\n]+)/m);
|
|
604
|
+
if (nameMatch)
|
|
605
|
+
agentName = nameMatch[1].trim().replace(/"$/, '');
|
|
606
|
+
}
|
|
607
|
+
catch { }
|
|
608
|
+
const projectDir = path.basename(agent.projectPath);
|
|
609
|
+
const text = `✓ ${agentName} 已上线 | 工作目录: ${projectDir}`;
|
|
610
|
+
const envelope = buildEnvelope({
|
|
611
|
+
taskId: `system-online-${crypto.randomBytes(5).toString('hex')}`,
|
|
612
|
+
channel: adapter.channelName,
|
|
613
|
+
channelId: ownerAid,
|
|
614
|
+
agentName,
|
|
615
|
+
});
|
|
616
|
+
sendSystemPayload(adapter, envelope, {
|
|
617
|
+
kind: 'system.notice',
|
|
618
|
+
text,
|
|
619
|
+
subtype: 'restarted',
|
|
620
|
+
}).catch(() => { });
|
|
621
|
+
}
|
|
622
|
+
}, 1000 + Math.random() * 2000);
|
|
474
623
|
// 统一 channel:health 跨通道通知(仅 auth_error)
|
|
475
624
|
// 按 (channelType, ownerId) 去重,避免同类型多实例重复通知
|
|
476
|
-
eventBus.subscribe('channel:
|
|
477
|
-
if (event.type !== 'channel:
|
|
625
|
+
eventBus.subscribe('channel:error', (event) => {
|
|
626
|
+
if (event.type !== 'channel:error' || event.status !== 'auth_error')
|
|
478
627
|
return;
|
|
479
628
|
const sourceChannelType = event.channel;
|
|
480
629
|
const sourceChannelName = event.channelName || sourceChannelType;
|
|
@@ -487,28 +636,46 @@ async function main() {
|
|
|
487
636
|
continue; // 跳过同类型通道
|
|
488
637
|
if (notified.has(otherType))
|
|
489
638
|
continue; // 同类型已通知过
|
|
490
|
-
const ownerId = agentRegistry.getOwner(other.adapter.channelName)
|
|
639
|
+
const ownerId = agentRegistry.getOwner(other.adapter.channelName);
|
|
491
640
|
if (!ownerId)
|
|
492
641
|
continue;
|
|
493
642
|
notified.add(otherType);
|
|
494
|
-
other.adapter.
|
|
643
|
+
const owningAgent = agentRegistry.resolveByChannel(other.adapter.channelName);
|
|
644
|
+
const envelope = buildEnvelope({
|
|
645
|
+
taskId: `system-channel-down-${crypto.randomBytes(5).toString('hex')}`,
|
|
646
|
+
channel: other.adapter.channelName,
|
|
647
|
+
channelId: ownerId,
|
|
648
|
+
agentName: owningAgent?.aid || 'evolclaw',
|
|
649
|
+
});
|
|
650
|
+
sendSystemPayload(other.adapter, envelope, {
|
|
651
|
+
kind: 'system.error',
|
|
652
|
+
text: msg,
|
|
653
|
+
subtype: 'channel_down',
|
|
654
|
+
recoverable: false,
|
|
655
|
+
}).catch(err => {
|
|
495
656
|
logger.error(`[ChannelHealth] Failed to notify ${other.adapter.channelName} owner:`, err);
|
|
496
657
|
});
|
|
497
658
|
}
|
|
498
659
|
});
|
|
499
|
-
// 按 channelType
|
|
500
|
-
const
|
|
660
|
+
// 按 channelType 归组显示连接摘要(启动 banner 只显示类型+计数,详情看 `evolclaw status`)
|
|
661
|
+
const connectedTypeCount = new Map();
|
|
662
|
+
const typeOrder = [];
|
|
501
663
|
for (const inst of channelInstances) {
|
|
502
664
|
const name = inst.adapter.channelName;
|
|
503
665
|
if (!connected.includes(name))
|
|
504
666
|
continue;
|
|
505
667
|
const type = inst.channelType || name;
|
|
506
|
-
if (!
|
|
507
|
-
|
|
508
|
-
|
|
668
|
+
if (!connectedTypeCount.has(type)) {
|
|
669
|
+
connectedTypeCount.set(type, 0);
|
|
670
|
+
typeOrder.push(type);
|
|
671
|
+
}
|
|
672
|
+
connectedTypeCount.set(type, connectedTypeCount.get(type) + 1);
|
|
509
673
|
}
|
|
510
|
-
const channelSummary =
|
|
511
|
-
.map(
|
|
674
|
+
const channelSummary = typeOrder
|
|
675
|
+
.map(type => {
|
|
676
|
+
const n = connectedTypeCount.get(type);
|
|
677
|
+
return n === 1 ? type : `${type}×${n}`;
|
|
678
|
+
})
|
|
512
679
|
.join(', ');
|
|
513
680
|
const totalCount = connected.length;
|
|
514
681
|
logger.info(`🚀 EvolClaw is running with ${totalCount} channel(s): ${channelSummary}`);
|
|
@@ -526,11 +693,16 @@ async function main() {
|
|
|
526
693
|
sessionManager.clearProcessing(session.id);
|
|
527
694
|
continue;
|
|
528
695
|
}
|
|
529
|
-
// 复合键:${
|
|
696
|
+
// 复合键:${aid}::${baseagent},从 channel 反查 self-agent
|
|
530
697
|
const owningAgent = agentRegistry.resolveByChannel(session.channel);
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
698
|
+
if (!owningAgent) {
|
|
699
|
+
logger.warn(`[Resume] session ${session.id}: channel "${session.channel}" not routable, skipping`);
|
|
700
|
+
sessionManager.clearProcessing(session.id);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const evolName = owningAgent.aid;
|
|
704
|
+
const baseagentName = session.agentId || primaryBaseagent;
|
|
705
|
+
const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(primaryRunnerKey);
|
|
534
706
|
if (!agent) {
|
|
535
707
|
sessionManager.clearProcessing(session.id);
|
|
536
708
|
continue;
|
|
@@ -562,7 +734,19 @@ async function main() {
|
|
|
562
734
|
const replyContext = pending.rootId
|
|
563
735
|
? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
|
|
564
736
|
: undefined;
|
|
565
|
-
|
|
737
|
+
const owningAgent = agentRegistry.resolveByChannel(adapter.channelName);
|
|
738
|
+
const envelope = buildEnvelope({
|
|
739
|
+
taskId: `system-restart-${process.pid}`,
|
|
740
|
+
channel: adapter.channelName,
|
|
741
|
+
channelId: pending.channelId,
|
|
742
|
+
agentName: owningAgent?.aid || 'evolclaw',
|
|
743
|
+
replyContext,
|
|
744
|
+
});
|
|
745
|
+
await sendSystemPayload(adapter, envelope, {
|
|
746
|
+
kind: 'system.notice',
|
|
747
|
+
text: '✅ 服务重启成功!',
|
|
748
|
+
subtype: 'restarted',
|
|
749
|
+
});
|
|
566
750
|
logger.info(`[Restart] Notification sent via ${pending.channel}`);
|
|
567
751
|
}
|
|
568
752
|
fs.unlinkSync(pendingFile);
|
|
@@ -571,10 +755,6 @@ async function main() {
|
|
|
571
755
|
logger.error('[Restart] Failed to send restart notification:', e);
|
|
572
756
|
}
|
|
573
757
|
}
|
|
574
|
-
// 写入 ready 信号,供 restart-monitor 检测启动成功
|
|
575
|
-
const readySignalPath = resolvePaths().readySignal;
|
|
576
|
-
fs.writeFileSync(readySignalPath, String(Date.now()));
|
|
577
|
-
logger.info(`✓ Ready signal written: ${readySignalPath}`);
|
|
578
758
|
// IPC server — 供 CLI 查询实时状态 + Agent ctl 指令执行
|
|
579
759
|
const ipcServer = new IpcServer(resolvePaths().socket, () => {
|
|
580
760
|
const channels = {};
|
|
@@ -608,6 +788,37 @@ async function main() {
|
|
|
608
788
|
}, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
|
|
609
789
|
// M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
|
|
610
790
|
ipcServer.setAgentRegistry(agentRegistry);
|
|
791
|
+
// 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
|
|
792
|
+
ipcServer.setAunAidProvider(() => {
|
|
793
|
+
const out = [];
|
|
794
|
+
for (const inst of channelInstances) {
|
|
795
|
+
if (inst.channelType !== 'aun')
|
|
796
|
+
continue;
|
|
797
|
+
const ch = inst.channel;
|
|
798
|
+
if (typeof ch?.getAidState === 'function') {
|
|
799
|
+
try {
|
|
800
|
+
out.push(ch.getAidState());
|
|
801
|
+
}
|
|
802
|
+
catch { /* ignore */ }
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return out;
|
|
806
|
+
});
|
|
807
|
+
// 注入 Per-AID 统计收集器到所有 AUN channel 实例
|
|
808
|
+
for (const inst of channelInstances) {
|
|
809
|
+
if (inst.channelType !== 'aun')
|
|
810
|
+
continue;
|
|
811
|
+
const ch = inst.channel;
|
|
812
|
+
if (typeof ch?.setAidStatsCollector === 'function') {
|
|
813
|
+
ch.setAidStatsCollector(aidStatsCollector);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// 注入 Per-AID 统计 IPC provider
|
|
817
|
+
aidStatsCollector.setQueueStatsProvider((agentName) => ({
|
|
818
|
+
processing: messageQueue.getProcessingCountByAgent(agentName),
|
|
819
|
+
queued: messageQueue.getQueueLengthByAgent(agentName),
|
|
820
|
+
}));
|
|
821
|
+
ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
|
|
611
822
|
// ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
|
|
612
823
|
const reloadHooks = buildReloadHooks({
|
|
613
824
|
channelLoader,
|
|
@@ -617,36 +828,94 @@ async function main() {
|
|
|
617
828
|
});
|
|
618
829
|
// Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
|
|
619
830
|
globalThis.__evolclaw_reloadHooks = reloadHooks;
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
632
|
-
const backupPath = path.join(resolvePaths().dataDir, `evolclaw-${ts}.json`);
|
|
633
|
-
fs.writeFileSync(backupPath, JSON.stringify(config, null, 2));
|
|
634
|
-
logger.warn(`[Config Watch] Config file is not valid JSON. In-memory snapshot saved to ${backupPath}`);
|
|
635
|
-
eventBus.publish({ type: 'config:corrupted', backupPath, reasons: ['Invalid JSON'] });
|
|
636
|
-
return;
|
|
831
|
+
// Hot-load handler: dynamically add a new agent at runtime
|
|
832
|
+
globalThis.__evolclaw_hotLoadAgent = async (aid) => {
|
|
833
|
+
const agent = agentRegistry.loadNewAgent(aid);
|
|
834
|
+
if (!agent)
|
|
835
|
+
throw new Error(`Failed to load agent ${aid}`);
|
|
836
|
+
// 创建 channels
|
|
837
|
+
const instances = await channelLoader.createForAgent(agent);
|
|
838
|
+
for (const inst of instances) {
|
|
839
|
+
registerChannelInstance(inst);
|
|
840
|
+
agent.channels.set(inst.adapter.channelName, inst.adapter);
|
|
841
|
+
channelInstances.push(inst);
|
|
637
842
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
843
|
+
agent.status = 'running';
|
|
844
|
+
// 连接
|
|
845
|
+
await channelLoader.connectAll(instances);
|
|
846
|
+
logger.info(`[HotLoad] ✓ Agent ${aid} online with ${instances.length} channel(s)`);
|
|
847
|
+
};
|
|
848
|
+
// Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
|
|
849
|
+
globalThis.__evolclaw_resyncAgents = async () => {
|
|
850
|
+
const { loadAllAgents: scanAgents, loadDefaults: readDefaults, mergeForAgent: merge } = await import('./config-store.js');
|
|
851
|
+
const freshDefaults = readDefaults();
|
|
852
|
+
const { agents: diskAgents } = scanAgents();
|
|
853
|
+
const diskAidSet = new Set(diskAgents.map(a => a.aid));
|
|
854
|
+
const results = [];
|
|
855
|
+
// 1. 下线:运行时有但磁盘上没有 / disabled 的
|
|
856
|
+
for (const [aid, agent] of [...agentRegistry.agents.entries()]) {
|
|
857
|
+
const diskCfg = diskAgents.find(a => a.aid === aid);
|
|
858
|
+
if (!diskCfg || diskCfg.enabled === false) {
|
|
859
|
+
// 断开所有 channels
|
|
860
|
+
for (const chName of agent.channelInstanceNames()) {
|
|
861
|
+
const inst = channelInstances.find(i => i.adapter.channelName === chName);
|
|
862
|
+
if (inst) {
|
|
863
|
+
try {
|
|
864
|
+
await inst.disconnect();
|
|
865
|
+
}
|
|
866
|
+
catch { }
|
|
867
|
+
const idx = channelInstances.indexOf(inst);
|
|
868
|
+
if (idx >= 0)
|
|
869
|
+
channelInstances.splice(idx, 1);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
agentRegistry.agents.delete(aid);
|
|
873
|
+
results.push(`- ${aid} (offline)`);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
645
876
|
}
|
|
646
|
-
|
|
647
|
-
|
|
877
|
+
// 2. 新增:磁盘上有但运行时没有的
|
|
878
|
+
for (const cfg of diskAgents) {
|
|
879
|
+
if (cfg.enabled === false)
|
|
880
|
+
continue;
|
|
881
|
+
if (agentRegistry.agents.has(cfg.aid))
|
|
882
|
+
continue;
|
|
883
|
+
try {
|
|
884
|
+
await globalThis.__evolclaw_hotLoadAgent(cfg.aid);
|
|
885
|
+
results.push(`+ ${cfg.aid} (online)`);
|
|
886
|
+
}
|
|
887
|
+
catch (e) {
|
|
888
|
+
results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
|
|
889
|
+
}
|
|
648
890
|
}
|
|
649
|
-
|
|
891
|
+
// 3. 已有的:重新 reload(config 可能改了)
|
|
892
|
+
const hooks = globalThis.__evolclaw_reloadHooks;
|
|
893
|
+
for (const cfg of diskAgents) {
|
|
894
|
+
if (cfg.enabled === false)
|
|
895
|
+
continue;
|
|
896
|
+
if (!agentRegistry.agents.has(cfg.aid))
|
|
897
|
+
continue;
|
|
898
|
+
// 只有磁盘上存在且运行时也存在的才 reload
|
|
899
|
+
try {
|
|
900
|
+
await agentRegistry.reload(cfg.aid, hooks);
|
|
901
|
+
results.push(`↻ ${cfg.aid} (reloaded)`);
|
|
902
|
+
}
|
|
903
|
+
catch (e) {
|
|
904
|
+
results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// 重建 channel index + 清除 kit 缓存
|
|
908
|
+
invalidateKitCache();
|
|
909
|
+
agentRegistry.channelIndex.clear();
|
|
910
|
+
agentRegistry.buildChannelIndex();
|
|
911
|
+
logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
|
|
912
|
+
return results;
|
|
913
|
+
};
|
|
914
|
+
// I3: start IPC server LAST, after all hook setup, to eliminate race window
|
|
915
|
+
ipcServer.start();
|
|
916
|
+
// 配置 reload 走 IPC `evolagent.reload` 触发,不再用 watchFile。
|
|
917
|
+
// 双 rename 原子写下 watchFile 的语义会被破坏,且新结构有 N 个 config.json 要监控;
|
|
918
|
+
// 显式触发更可控。
|
|
650
919
|
// 优雅关闭
|
|
651
920
|
let shutdownSignal = 'unknown';
|
|
652
921
|
const shutdown = async (signal) => {
|
|
@@ -655,7 +924,6 @@ async function main() {
|
|
|
655
924
|
const pid = process.pid;
|
|
656
925
|
const ppid = process.ppid;
|
|
657
926
|
logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
|
|
658
|
-
fs.unwatchFile(configPath);
|
|
659
927
|
ipcServer.stop();
|
|
660
928
|
eventBus.publish({
|
|
661
929
|
type: 'system:shutdown',
|
|
@@ -668,15 +936,24 @@ async function main() {
|
|
|
668
936
|
eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
|
|
669
937
|
}
|
|
670
938
|
sessionManager.close();
|
|
939
|
+
removeAll();
|
|
671
940
|
logger.info('✓ Shutdown complete');
|
|
672
941
|
process.exit(0);
|
|
673
942
|
};
|
|
674
943
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
675
944
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
945
|
+
// 兜底:进程退出前同步删除 instance 文件(防 async shutdown 未完成就被杀)
|
|
946
|
+
process.on('exit', () => {
|
|
947
|
+
removeAll();
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
// 仅在直接执行时启动;导入此模块(如单元测试)时不触发 main()。
|
|
951
|
+
import { isMainScript } from './utils/cross-platform.js';
|
|
952
|
+
if (isMainScript(import.meta.url)) {
|
|
953
|
+
main().catch((error) => {
|
|
954
|
+
const msg = `Fatal error: ${error?.stack || error}`;
|
|
955
|
+
logger.error('Fatal error:', error);
|
|
956
|
+
console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
|
|
957
|
+
process.exit(1);
|
|
958
|
+
});
|
|
676
959
|
}
|
|
677
|
-
main().catch((error) => {
|
|
678
|
-
const msg = `Fatal error: ${error?.stack || error}`;
|
|
679
|
-
logger.error('Fatal error:', error);
|
|
680
|
-
console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
|
|
681
|
-
process.exit(1);
|
|
682
|
-
});
|