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
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 { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getDefaultSessionMode, setOwner as setOwnerInGlobalConfig, setChannelShowActivities as setShowActivitiesInGlobalConfig } from './config.js';
4
+ import { ensureDataDirs, resolvePaths, syncKitsFromPackage } from './paths.js';
5
+ import { resolveAnthropicConfig } from './agents/resolve.js';
6
+ import { loadDefaults, autoMigrateIfNeeded } 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,45 @@ 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-collector.js';
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/agent-loader.js';
29
+ import { AgentLoader } from './core/baseagent-loader.js';
26
30
  import { EvolAgentRegistry } from './core/evolagent-registry.js';
27
- import { buildReloadHooks } from './utils/reload-hooks.js';
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
36
  import { loadPromptTemplates } from './agents/templates.js';
37
+ import { TriggerManager } from './core/trigger/manager.js';
38
+ import { TriggerScheduler } from './core/trigger/scheduler.js';
39
+ import { agentTriggersDir } from './paths.js';
32
40
  import path from 'path';
33
41
  import fs from 'fs';
42
+ import os from 'os';
43
+ import crypto from 'crypto';
44
+ /**
45
+ * 通过 adapter.send 发送系统类 payload(system.notice / system.error / 等)。
46
+ *
47
+ * 网关层(本文件)的所有出站系统通知(上线 / 重启完成 / 渠道告警 / agent 启动失败等)
48
+ * 走这里集中调度,让渠道按 capabilities 决定呈现方式。
49
+ *
50
+ * 当 adapter 还没实现 send(旧 adapter)时,按 payload.kind 降级到 sendText。
51
+ *
52
+ * Exported for unit test coverage; runtime callers are inside main() closure.
53
+ */
54
+ export async function sendSystemPayload(adapter, envelope, payload) {
55
+ await adapter.send(envelope, payload);
56
+ }
34
57
  async function main() {
35
58
  // 过滤飞书 SDK 的 info 日志
36
59
  const originalLog = console.log;
@@ -52,80 +75,131 @@ async function main() {
52
75
  logger.info('EvolClaw starting...');
53
76
  // 确保数据目录存在
54
77
  ensureDataDirs();
78
+ // 同步包内 kits/ 到 EVOLCLAW_HOME/kits/(首次启动或升级时)
79
+ syncKitsFromPackage();
80
+ // ── 单实例保护(pre-check + post-write self-check)──
81
+ // pre-check:发现已有活 main 直接退出,避免起任何副作用
82
+ {
83
+ const pre = scanInstances();
84
+ const aliveOthers = pre.mains.filter(m => m.alive && m.record.pid !== process.pid);
85
+ if (aliveOthers.length > 0) {
86
+ const pids = aliveOthers.map(m => m.record.pid).join(', ');
87
+ const msg = `❌ Another EvolClaw instance is already running (PID: ${pids}). Use 'evolclaw restart' to replace it.`;
88
+ logger.error(msg);
89
+ console.error(msg);
90
+ process.exit(1);
91
+ }
92
+ }
93
+ // 立即登记自己(让其他并发启动者能看见我)
94
+ const launchedBy = process.env.EVOLCLAW_LAUNCHED_BY || 'start';
95
+ writeMain(launchedBy);
96
+ logger.info(`✓ Instance record written: main-${process.pid}.json`);
97
+ // post-write 自检:写完 record 后再扫一次,发现并发对手时按 (startedAt, pid) 选赢家
98
+ {
99
+ const verdict = isMainWinner();
100
+ if (!verdict.winner) {
101
+ logger.warn(`Lost main election to PID ${verdict.conflictingPid}, yielding`);
102
+ console.error(`⚠ Another instance (PID ${verdict.conflictingPid}) started concurrently and won the election. Yielding.`);
103
+ removeAll();
104
+ process.exit(0);
105
+ }
106
+ }
55
107
  // 加载提示词模板
56
108
  loadPromptTemplates();
57
- // 加载配置
58
- const config = loadConfig();
109
+ // 加载配置(新结构:defaults.json + per-agent config.json)
110
+ const defaults = loadDefaults() ?? { $schema_version: CONFIG_SCHEMA_VERSION };
59
111
  // 应用配置中的日志级别(优先于环境变量)
60
- if (config.debug?.logLevel) {
61
- setLogLevel(config.debug.logLevel);
62
- }
112
+ // logLevel 现在不在新结构中——若要保留,将来可加 defaults.debug.logLevel
113
+ // 阶段 2c 暂跳过
63
114
  const paths = resolvePaths();
64
- // 配置完整性校验
65
- const integrity = validateConfigIntegrity(config);
66
- if (!integrity.valid) {
67
- const msg = `❌ Config integrity check failed:\n ${integrity.reasons.join('\n ')}`;
115
+ // ── 自动迁移:旧 data/evolclaw.json → 新结构 ──
116
+ autoMigrateIfNeeded();
117
+ // ── EvolAgent Registry:加载 agents/<aid>/config.json ──
118
+ const agentRegistry = new EvolAgentRegistry(paths.agentsDir);
119
+ agentRegistry.loadAll();
120
+ const agentInfos = agentRegistry.list();
121
+ // 启动期硬约束:必须至少有一个 self-agent
122
+ if (agentInfos.length === 0) {
123
+ const skipped = agentRegistry.getSkipped();
124
+ const lines = [
125
+ '❌ No self-agent configured.',
126
+ ` Run \`evolclaw aid new <name>\` to create one.`,
127
+ ];
128
+ if (skipped.length > 0) {
129
+ lines.push(` Skipped ${skipped.length} dir(s):`);
130
+ for (const s of skipped)
131
+ lines.push(` - ${s.dirName}: ${s.reason}`);
132
+ }
133
+ const msg = lines.join('\n');
68
134
  logger.error(msg);
69
- console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
135
+ console.error(msg);
70
136
  process.exit(1);
71
137
  }
72
- const anthropic = resolveAnthropicConfig(config);
73
- logger.info('✓ Config loaded (API keys hidden)');
74
- // Channel instance name uniqueness check
75
- validateChannelInstanceNames(config);
76
- // Detect duplicate channel credentials
77
- const duplicates = detectDuplicates(config);
78
- if (duplicates.length > 0) {
79
- for (const d of duplicates) {
80
- logger.warn(`⚠ Duplicate channel credential: ${d.fingerprint} is used by instances [${d.instances.join(', ')}]. ` +
81
- `Only the first instance will be active.`);
138
+ logger.info(`✓ Loaded ${agentInfos.length} self-agent(s)`);
139
+ for (const info of agentInfos) {
140
+ if (info.status === 'error') {
141
+ logger.error(` ✗ ${info.name}: ${info.error}`);
142
+ }
143
+ else if (info.status === 'disabled') {
144
+ logger.info(` ○ ${info.name} (disabled)`);
145
+ }
146
+ else {
147
+ logger.info(` ${info.name} ${info.baseagent} @ ${path.basename(info.projectPath)}`);
82
148
  }
83
149
  }
84
- if (anthropic.baseUrl) {
85
- logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
150
+ // agent 凭证冲突
151
+ {
152
+ const dups = detectDuplicates(agentRegistry.runnableAgents());
153
+ for (const d of dups) {
154
+ const owners = d.agents.map(o => `${o.aid}(${o.channelName})`).join(', ');
155
+ logger.warn(`⚠ Duplicate channel credential: ${d.fingerprint} claimed by ${owners}.`);
156
+ }
86
157
  }
87
- // EvolAgent Registry
88
- const agentRegistry = new EvolAgentRegistry(paths.agentsDir, {
89
- setOwner: (channelName, userId) => {
90
- setOwnerInGlobalConfig(config, channelName, userId);
91
- },
92
- setShowActivities: (channelName, mode) => {
93
- setShowActivitiesInGlobalConfig(config, channelName, mode);
94
- },
158
+ // 选定主 agent(启动期 anthropic resolve 用,配合 IPC `evolagent.list` 显示)
159
+ // agent 取第一个非 error 非 disabled 的 self-agent。
160
+ const primaryAgent = agentRegistry.runnableAgents()[0];
161
+ if (!primaryAgent) {
162
+ const msg = '❌ No runnable self-agent (all are error/disabled). Aborting.';
163
+ logger.error(msg);
164
+ console.error(msg);
165
+ process.exit(1);
166
+ }
167
+ // 进程级设置(从 defaults 取,不属于任何 agent)
168
+ const globalSettings = {
169
+ idleMonitor: defaults.idleMonitor,
170
+ debug: defaults.debug,
171
+ };
172
+ if (globalSettings.debug?.logLevel) {
173
+ setLogLevel(globalSettings.debug.logLevel);
174
+ }
175
+ // 启动期 anthropic 凭证校验(用 primaryAgent 的 baseagents.claude)
176
+ const anthropic = resolveAnthropicConfig({
177
+ agents: { claude: primaryAgent.config.baseagents?.claude },
95
178
  });
96
- agentRegistry.loadAll(config);
97
- const agentInfos = agentRegistry.list();
98
- const evolagentCount = agentInfos.filter(i => !i.isDefault).length;
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
- }
179
+ logger.info('✓ Config loaded (API keys hidden)');
180
+ if (anthropic.baseUrl) {
181
+ logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
114
182
  }
115
183
  // Store for IPC access (T10 will wire this)
116
184
  // M4: removed dead globalThis.__evolclaw_agentRegistry assignment
117
185
  // 创建事件总线
118
186
  const eventBus = new EventBus();
119
187
  logger.info('✓ Event bus initialized');
188
+ // 把所有事件录到 events.log(受 EVENT_LOG 环境变量控制)
189
+ eventBus.subscribeAll((event) => logger.event(event));
120
190
  // 统计收集器(近 1 小时滚动统计)
121
191
  const statsCollector = new StatsCollector(eventBus);
122
- // 初始化数据库(带 ownerResolver)
123
- // Registry-first: agent-owned channels resolve via EvolAgent.isOwner/isAdmin,
124
- // default-agent channels fall back to global config (evolclaw.json).
125
- const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId, (ch, uid) => isOwner(config, ch, uid)), (channel, userId) => agentRegistry.isAdmin(channel, userId, (ch, uid) => isAdmin(config, ch, uid)));
126
- // sessionMode 解析:全局 chatmode 配置 > 默认 'interactive'
127
- sessionManager.setSessionModeResolver((_channel, chatType) => {
128
- return getDefaultSessionMode(config, chatType);
192
+ // Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
193
+ const aidStatsCollector = new AidStatsCollector();
194
+ // 初始化 SessionManager(文件系统后端)
195
+ const sessionManager = new SessionManager(paths.sessionsDir, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId), (channel, userId) => agentRegistry.isAdmin(channel, userId));
196
+ // sessionMode 解析:从 channel 路由到具体 agent,按 agent.config.chatmode
197
+ sessionManager.setSessionModeResolver((channelKey, chatType) => {
198
+ const agent = agentRegistry.resolveByChannel(channelKey);
199
+ const cm = agent?.config.chatmode;
200
+ if (!cm)
201
+ return undefined;
202
+ return chatType === 'group' ? cm.group : cm.private;
129
203
  });
130
204
  logger.info('✓ Database initialized');
131
205
  // 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
@@ -137,23 +211,23 @@ async function main() {
137
211
  agentLoader.register(new ClaudeAgentPlugin());
138
212
  agentLoader.register(new CodexAgentPlugin());
139
213
  agentLoader.register(new GeminiAgentPlugin());
140
- const agentInstances = agentLoader.createAll(config, agentRegistry, {
214
+ const agentInstances = agentLoader.createAll(agentRegistry, {
141
215
  onSessionIdUpdate: async (sessionId, agentSessionId) => {
142
216
  await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
143
217
  },
144
218
  });
145
- // agentMap 复合键:${evolagentName}::${baseagent}
219
+ // agentMap 复合键:${aid}::${baseagent}
146
220
  const agentMap = new Map();
147
221
  for (const inst of agentInstances) {
148
222
  agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
149
223
  }
150
- const defaultAgent = config.agents?.defaultAgent || 'claude';
151
- const defaultAgentKey = `[default]::${defaultAgent}`;
152
- const agentRunner = agentMap.get(defaultAgentKey) || agentInstances[0]?.agent;
224
+ const primaryBaseagent = primaryAgent.baseagent;
225
+ const primaryRunnerKey = `${primaryAgent.aid}::${primaryBaseagent}`;
226
+ const agentRunner = agentMap.get(primaryRunnerKey) || agentInstances[0]?.agent;
153
227
  if (!agentRunner) {
154
- throw new Error('No agent backend available. Check agents config (no runners created).');
228
+ throw new Error('No agent backend available. Check baseagents config (no runners created).');
155
229
  }
156
- logger.info(`✓ Runners ready (default key: ${defaultAgentKey}, total: ${agentMap.size}, keys: ${[...agentMap.keys()].join(', ')})`);
230
+ logger.info(`✓ Runners ready (primary key: ${primaryRunnerKey}, total: ${agentMap.size}, keys: ${[...agentMap.keys()].join(', ')})`);
157
231
  // 权限审批网关
158
232
  const permissionGateway = new PermissionGateway();
159
233
  permissionGateway.setEventBus(eventBus);
@@ -178,67 +252,46 @@ async function main() {
178
252
  channelLoader.register(new DingtalkChannelPlugin());
179
253
  channelLoader.register(new QQBotChannelPlugin());
180
254
  channelLoader.register(new WecomChannelPlugin());
181
- // Create channel instances: default (from evolclaw.json) + each evolagent
182
- const defaultInstances = await channelLoader.createAll(config);
255
+ // Create channel instances: 每个 self-agent 各自的 channels
183
256
  const evolagentInstances = [];
184
257
  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
258
  try {
212
- const instances = await channelLoader.createAll(agentConfig);
259
+ const instances = await channelLoader.createForAgent(agent);
213
260
  evolagentInstances.push(...instances);
214
261
  }
215
262
  catch (e) {
216
- logger.error(`[EvolAgent] Failed to create channels for ${agent.name}: ${e}`);
263
+ logger.error(`[Agent ${agent.aid}] Failed to create channels: ${e}`);
217
264
  agent.status = 'error';
218
265
  agent.error = `Channel creation failed: ${e}`;
219
266
  }
220
267
  }
221
- const channelInstances = [...defaultInstances, ...evolagentInstances];
222
- logger.info(`✓ Created ${channelInstances.length} channel instance(s)${evolagentInstances.length > 0 ? ` (${defaultInstances.length} default + ${evolagentInstances.length} agent)` : ''}`);
223
- // 启动迁移:将 sessions.channel 从 channelType 回填为实例名
224
- sessionManager.migrateChannelToInstanceName();
268
+ const channelInstances = evolagentInstances;
269
+ logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
270
+ // 初始化触发器调度器(每个 EvolAgent 独立)
271
+ for (const agent of agentRegistry.runnableAgents()) {
272
+ const triggersDir = agentTriggersDir(agent.aid);
273
+ const triggerManager = new TriggerManager(agent.aid, triggersDir);
274
+ const triggerScheduler = new TriggerScheduler(agent.aid, triggerManager, eventBus);
275
+ agent.triggerManager = triggerManager;
276
+ agent.triggerScheduler = triggerScheduler;
277
+ }
225
278
  // 创建命令处理器
226
- const cmdHandler = new CommandHandler(sessionManager, agentMap, config, messageCache, eventBus, defaultAgentKey);
279
+ const cmdHandler = new CommandHandler(sessionManager, agentMap, messageCache, eventBus, primaryRunnerKey);
227
280
  cmdHandler.setPermissionGateway(permissionGateway);
228
281
  cmdHandler.setInteractionRouter(interactionRouter);
229
282
  cmdHandler.setStatsCollector(statsCollector);
230
283
  // 创建消息处理器
231
- const processor = new MessageProcessor(agentMap, sessionManager, config, messageCache, eventBus, (content, channel, channelId, userId, threadId) => {
284
+ const processor = new MessageProcessor(agentMap, sessionManager, globalSettings, messageCache, eventBus, (content, channel, channelId, userId, threadId) => {
232
285
  const sendFn = async (id, text, opts) => {
233
286
  const adapter = cmdHandler.getAdapter(channel);
234
287
  if (!adapter)
235
288
  return;
236
289
  if (text) {
237
- await adapter.sendText(id, text, opts);
290
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: id, replyContext: opts }), { kind: 'system.notice', text, subtype: 'health' });
238
291
  }
239
292
  };
240
293
  return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
241
- }, defaultAgentKey);
294
+ }, primaryRunnerKey);
242
295
  // 回填 processor 和 messageQueue 的引用
243
296
  cmdHandler.setProcessor(processor);
244
297
  // Inject EvolAgentRegistry (methods added by T6/T7)
@@ -262,10 +315,10 @@ async function main() {
262
315
  });
263
316
  // 设置中断回调(精确中断正在处理的 agent)
264
317
  messageQueue.setInterruptCallback(async (sessionKey, agentId, evolagentName) => {
265
- const baseagent = agentId || defaultAgent;
266
- const evol = evolagentName || '[default]';
318
+ const baseagent = agentId || primaryBaseagent;
319
+ const evol = evolagentName || primaryAgent.aid;
267
320
  const agent = agentMap.get(`${evol}::${baseagent}`)
268
- || agentMap.get(defaultAgentKey);
321
+ || agentMap.get(primaryRunnerKey);
269
322
  if (agent?.hasActiveStream(sessionKey)) {
270
323
  await agent.interrupt(sessionKey);
271
324
  }
@@ -274,6 +327,40 @@ async function main() {
274
327
  // 回填 messageQueue 引用
275
328
  cmdHandler.setMessageQueue(messageQueue);
276
329
  processor.setMessageQueue(messageQueue);
330
+ // 启动触发器调度器,设置 fireCallback 投递合成消息
331
+ for (const agent of agentRegistry.runnableAgents()) {
332
+ if (!agent.triggerScheduler || !agent.triggerManager)
333
+ continue;
334
+ const scheduler = agent.triggerScheduler;
335
+ const primaryProjectPath = agent.config.projects?.defaultPath ?? primaryAgent.projectPath;
336
+ scheduler.setFireCallback((msg, trigger) => {
337
+ const sessionKey = `${msg.channel}:${msg.channelId}`;
338
+ messageQueue.enqueue(sessionKey, msg, primaryProjectPath, { interruptible: false }).catch(err => {
339
+ logger.error(`[Trigger] Failed to enqueue trigger ${trigger.id}: ${err}`);
340
+ });
341
+ });
342
+ // Subscribe to trigger:completed/failed/skipped to update cron inflight state
343
+ eventBus.subscribe('trigger:completed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'completed'));
344
+ eventBus.subscribe('trigger:failed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'failed'));
345
+ eventBus.subscribe('trigger:skipped', (ev) => {
346
+ if (ev.reason === 'interrupted')
347
+ scheduler.onTriggerComplete(ev.triggerId, 'interrupted');
348
+ });
349
+ // Note: only the primary agent's scheduler is wired to cmdHandler.
350
+ // Non-primary agent channels will receive "⚠️ 触发器功能未启用" when using /trigger.
351
+ // Full per-channel scheduler routing is a future improvement.
352
+ try {
353
+ await scheduler.init();
354
+ }
355
+ catch (err) {
356
+ logger.error(`[Trigger] Scheduler init failed for ${agent.aid}: ${err}`);
357
+ }
358
+ }
359
+ // Inject primary agent's trigger scheduler into cmdHandler
360
+ const primaryAgentForTrigger = agentRegistry.runnableAgents()[0];
361
+ if (primaryAgentForTrigger?.triggerScheduler && primaryAgentForTrigger?.triggerManager) {
362
+ cmdHandler.setTriggerScheduler(primaryAgentForTrigger.triggerScheduler, primaryAgentForTrigger.triggerManager);
363
+ }
277
364
  // 默认策略
278
365
  const defaultPolicy = {
279
366
  canSwitchProject: (chatType, role) => chatType === 'private' ? (role === 'owner' || role === 'admin') : role === 'owner',
@@ -287,18 +374,17 @@ async function main() {
287
374
  accumulateErrors: () => true,
288
375
  };
289
376
  // ── MessageBridge:Channel ↔ Core 消息桥梁 ──
290
- const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
377
+ const msgBridge = new MessageBridge(primaryAgent.projectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, primaryAgent.config.debounce);
291
378
  msgBridge.setAgentRegistry(agentRegistry);
292
379
  // ── Channel instance registration (shared by startup and hot-load) ──
293
380
  function registerChannelInstance(inst) {
294
381
  // 1. 项目路径提供器
295
382
  if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
296
383
  inst.channel.onProjectPathRequest(async (channelId) => {
297
- // Effective default path: agent's projectPath if agent-owned, else global
384
+ // Effective default path: use the agent that owns this channel.
298
385
  const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelName);
299
- const effectiveDefault = (owningAgent && !owningAgent.isDefault)
300
- ? owningAgent.projectPath
301
- : (config.projects?.defaultPath || process.cwd());
386
+ const effectiveDefault = owningAgent?.projectPath
387
+ ?? primaryAgent.projectPath;
302
388
  const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
303
389
  return path.isAbsolute(session.projectPath)
304
390
  ? session.projectPath
@@ -321,114 +407,14 @@ async function main() {
321
407
  interactionRouter.handle(response);
322
408
  });
323
409
  }
324
- // 4. MessageBridge 注册(按 channelType 分发)
410
+ // 4. MessageBridge 注册
325
411
  const channelType = inst.channelType || inst.adapter.channelName;
326
- if (channelType === 'feishu') {
327
- msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType }) => {
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
- }
412
+ if (inst.registerBridge) {
413
+ inst.registerBridge(msgBridge, channelType);
391
414
  }
392
- if (channelType === 'dingtalk') {
393
- msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
394
- handler({
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);
415
+ // 4b. 生命周期钩子
416
+ if (inst.registerHooks) {
417
+ inst.registerHooks({ eventBus, sessionManager });
432
418
  }
433
419
  // 5. 撤回消息 → 中断执行中任务
434
420
  inst.channel.onRecall?.((messageId) => {
@@ -439,20 +425,18 @@ async function main() {
439
425
  for (const inst of channelInstances) {
440
426
  registerChannelInstance(inst);
441
427
  }
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);
428
+ // Bind adapters to their owning agents and mark running
447
429
  for (const inst of channelInstances) {
448
430
  const agent = agentRegistry.resolveByChannel(inst.adapter.channelName);
449
431
  if (!agent || agent.status === 'error')
450
432
  continue;
451
433
  agent.channels.set(inst.adapter.channelName, inst.adapter);
452
- if (agent.status === 'stopped' && connectedSet.has(inst.adapter.channelName)) {
434
+ if (agent.status === 'stopped') {
453
435
  agent.status = 'running';
454
436
  }
455
437
  }
438
+ // ── 连接所有渠道(异步,AUN 等 WebSocket 渠道在后台重连)──
439
+ const connected = await channelLoader.connectAll(channelInstances);
456
440
  // 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
457
441
  for (const inst of channelInstances) {
458
442
  const channelType = inst.channelType || inst.adapter.channelName;
@@ -471,10 +455,51 @@ async function main() {
471
455
  timestamp: Date.now()
472
456
  });
473
457
  }
458
+ // 上线通知:延迟 1-3 秒后向 owner 发送上线消息(带 name + 工作目录)
459
+ // 需在配置中 debug.upmsg: true 手动开启
460
+ setTimeout(() => {
461
+ for (const name of connected) {
462
+ const agent = agentRegistry.resolveByChannel(name);
463
+ if (!agent)
464
+ continue;
465
+ if (!agent.config.debug?.upmsg)
466
+ continue;
467
+ const ownerAid = agent.config.owners?.[0];
468
+ if (!ownerAid)
469
+ continue;
470
+ const adapter = agent.channels.get(name);
471
+ if (!adapter)
472
+ continue;
473
+ // 尝试从 agent.md 读取 name
474
+ let agentName = agent.aid;
475
+ try {
476
+ const aunPath = process.env.AUN_HOME || path.join(os.homedir(), '.aun');
477
+ const agentMdPath = path.join(aunPath, 'AIDs', agent.aid, 'agent.md');
478
+ const content = fs.readFileSync(agentMdPath, 'utf-8');
479
+ const nameMatch = content.match(/^name:\s*"?([^"\n]+)/m);
480
+ if (nameMatch)
481
+ agentName = nameMatch[1].trim().replace(/"$/, '');
482
+ }
483
+ catch { }
484
+ const projectDir = path.basename(agent.projectPath);
485
+ const text = `✓ ${agentName} 已上线 | 工作目录: ${projectDir}`;
486
+ const envelope = buildEnvelope({
487
+ taskId: `system-online-${crypto.randomBytes(5).toString('hex')}`,
488
+ channel: adapter.channelName,
489
+ channelId: ownerAid,
490
+ agentName,
491
+ });
492
+ sendSystemPayload(adapter, envelope, {
493
+ kind: 'system.notice',
494
+ text,
495
+ subtype: 'restarted',
496
+ }).catch(() => { });
497
+ }
498
+ }, 1000 + Math.random() * 2000);
474
499
  // 统一 channel:health 跨通道通知(仅 auth_error)
475
500
  // 按 (channelType, ownerId) 去重,避免同类型多实例重复通知
476
- eventBus.subscribe('channel:health', (event) => {
477
- if (event.type !== 'channel:health' || event.status !== 'auth_error')
501
+ eventBus.subscribe('channel:error', (event) => {
502
+ if (event.type !== 'channel:error' || event.status !== 'auth_error')
478
503
  return;
479
504
  const sourceChannelType = event.channel;
480
505
  const sourceChannelName = event.channelName || sourceChannelType;
@@ -487,28 +512,46 @@ async function main() {
487
512
  continue; // 跳过同类型通道
488
513
  if (notified.has(otherType))
489
514
  continue; // 同类型已通知过
490
- const ownerId = agentRegistry.getOwner(other.adapter.channelName) ?? getOwner(config, other.adapter.channelName);
515
+ const ownerId = agentRegistry.getOwner(other.adapter.channelName);
491
516
  if (!ownerId)
492
517
  continue;
493
518
  notified.add(otherType);
494
- other.adapter.sendText(ownerId, msg).catch(err => {
519
+ const owningAgent = agentRegistry.resolveByChannel(other.adapter.channelName);
520
+ const envelope = buildEnvelope({
521
+ taskId: `system-channel-down-${crypto.randomBytes(5).toString('hex')}`,
522
+ channel: other.adapter.channelName,
523
+ channelId: ownerId,
524
+ agentName: owningAgent?.aid || 'evolclaw',
525
+ });
526
+ sendSystemPayload(other.adapter, envelope, {
527
+ kind: 'system.error',
528
+ text: msg,
529
+ subtype: 'channel_down',
530
+ recoverable: false,
531
+ }).catch(err => {
495
532
  logger.error(`[ChannelHealth] Failed to notify ${other.adapter.channelName} owner:`, err);
496
533
  });
497
534
  }
498
535
  });
499
- // 按 channelType 归组显示连接摘要
500
- const connectedGroups = new Map();
536
+ // 按 channelType 归组显示连接摘要(启动 banner 只显示类型+计数,详情看 `evolclaw status`)
537
+ const connectedTypeCount = new Map();
538
+ const typeOrder = [];
501
539
  for (const inst of channelInstances) {
502
540
  const name = inst.adapter.channelName;
503
541
  if (!connected.includes(name))
504
542
  continue;
505
543
  const type = inst.channelType || name;
506
- if (!connectedGroups.has(type))
507
- connectedGroups.set(type, []);
508
- connectedGroups.get(type).push(name);
544
+ if (!connectedTypeCount.has(type)) {
545
+ connectedTypeCount.set(type, 0);
546
+ typeOrder.push(type);
547
+ }
548
+ connectedTypeCount.set(type, connectedTypeCount.get(type) + 1);
509
549
  }
510
- const channelSummary = Array.from(connectedGroups.entries())
511
- .map(([type, names]) => names.length === 1 ? names[0] : `${type}[${names.join(', ')}]`)
550
+ const channelSummary = typeOrder
551
+ .map(type => {
552
+ const n = connectedTypeCount.get(type);
553
+ return n === 1 ? type : `${type}×${n}`;
554
+ })
512
555
  .join(', ');
513
556
  const totalCount = connected.length;
514
557
  logger.info(`🚀 EvolClaw is running with ${totalCount} channel(s): ${channelSummary}`);
@@ -526,11 +569,16 @@ async function main() {
526
569
  sessionManager.clearProcessing(session.id);
527
570
  continue;
528
571
  }
529
- // 复合键:${evolagentName}::${baseagent},从 channel 反查 evolagent
572
+ // 复合键:${aid}::${baseagent},从 channel 反查 self-agent
530
573
  const owningAgent = agentRegistry.resolveByChannel(session.channel);
531
- const evolName = owningAgent?.name || '[default]';
532
- const baseagentName = session.agentId || defaultAgent;
533
- const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(defaultAgentKey);
574
+ if (!owningAgent) {
575
+ logger.warn(`[Resume] session ${session.id}: channel "${session.channel}" not routable, skipping`);
576
+ sessionManager.clearProcessing(session.id);
577
+ continue;
578
+ }
579
+ const evolName = owningAgent.aid;
580
+ const baseagentName = session.agentId || primaryBaseagent;
581
+ const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(primaryRunnerKey);
534
582
  if (!agent) {
535
583
  sessionManager.clearProcessing(session.id);
536
584
  continue;
@@ -562,7 +610,19 @@ async function main() {
562
610
  const replyContext = pending.rootId
563
611
  ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
564
612
  : undefined;
565
- await adapter.sendText(pending.channelId, '✅ 服务重启成功!', replyContext);
613
+ const owningAgent = agentRegistry.resolveByChannel(adapter.channelName);
614
+ const envelope = buildEnvelope({
615
+ taskId: `system-restart-${process.pid}`,
616
+ channel: adapter.channelName,
617
+ channelId: pending.channelId,
618
+ agentName: owningAgent?.aid || 'evolclaw',
619
+ replyContext,
620
+ });
621
+ await sendSystemPayload(adapter, envelope, {
622
+ kind: 'system.notice',
623
+ text: '✅ 服务重启成功!',
624
+ subtype: 'restarted',
625
+ });
566
626
  logger.info(`[Restart] Notification sent via ${pending.channel}`);
567
627
  }
568
628
  fs.unlinkSync(pendingFile);
@@ -608,6 +668,37 @@ async function main() {
608
668
  }, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
609
669
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
610
670
  ipcServer.setAgentRegistry(agentRegistry);
671
+ // 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
672
+ ipcServer.setAunAidProvider(() => {
673
+ const out = [];
674
+ for (const inst of channelInstances) {
675
+ if (inst.channelType !== 'aun')
676
+ continue;
677
+ const ch = inst.channel;
678
+ if (typeof ch?.getAidState === 'function') {
679
+ try {
680
+ out.push(ch.getAidState());
681
+ }
682
+ catch { /* ignore */ }
683
+ }
684
+ }
685
+ return out;
686
+ });
687
+ // 注入 Per-AID 统计收集器到所有 AUN channel 实例
688
+ for (const inst of channelInstances) {
689
+ if (inst.channelType !== 'aun')
690
+ continue;
691
+ const ch = inst.channel;
692
+ if (typeof ch?.setAidStatsCollector === 'function') {
693
+ ch.setAidStatsCollector(aidStatsCollector);
694
+ }
695
+ }
696
+ // 注入 Per-AID 统计 IPC provider
697
+ aidStatsCollector.setQueueStatsProvider((agentName) => ({
698
+ processing: messageQueue.getProcessingCountByAgent(agentName),
699
+ queued: messageQueue.getQueueLengthByAgent(agentName),
700
+ }));
701
+ ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
611
702
  // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
612
703
  const reloadHooks = buildReloadHooks({
613
704
  channelLoader,
@@ -617,36 +708,93 @@ async function main() {
617
708
  });
618
709
  // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
619
710
  globalThis.__evolclaw_reloadHooks = reloadHooks;
620
- // I3: start IPC server LAST, after all hook setup, to eliminate race window
621
- ipcServer.start();
622
- // 运行时配置文件监控
623
- const configPath = resolvePaths().config;
624
- fs.watchFile(configPath, { interval: 5000 }, (_curr, _prev) => {
625
- let newConfig;
626
- try {
627
- newConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
628
- }
629
- catch {
630
- // JSON 解析失败 → 视为坏文件,备份内存中的好副本
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;
711
+ // Hot-load handler: dynamically add a new agent at runtime
712
+ globalThis.__evolclaw_hotLoadAgent = async (aid) => {
713
+ const agent = agentRegistry.loadNewAgent(aid);
714
+ if (!agent)
715
+ throw new Error(`Failed to load agent ${aid}`);
716
+ // 创建 channels
717
+ const instances = await channelLoader.createForAgent(agent);
718
+ for (const inst of instances) {
719
+ registerChannelInstance(inst);
720
+ agent.channels.set(inst.adapter.channelName, inst.adapter);
721
+ channelInstances.push(inst);
637
722
  }
638
- const result = validateConfigIntegrity(newConfig);
639
- if (!result.valid) {
640
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
641
- const backupPath = path.join(resolvePaths().dataDir, `evolclaw-${ts}.json`);
642
- fs.writeFileSync(backupPath, JSON.stringify(config, null, 2));
643
- logger.warn(`[Config Watch] Bad config write detected. Reasons: ${result.reasons.join('; ')}. In-memory snapshot saved to ${backupPath}`);
644
- eventBus.publish({ type: 'config:corrupted', backupPath, reasons: result.reasons });
723
+ agent.status = 'running';
724
+ // 连接
725
+ await channelLoader.connectAll(instances);
726
+ logger.info(`[HotLoad] Agent ${aid} online with ${instances.length} channel(s)`);
727
+ };
728
+ // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
729
+ globalThis.__evolclaw_resyncAgents = async () => {
730
+ const { loadAllAgents: scanAgents, loadDefaults: readDefaults, mergeForAgent: merge } = await import('./config-store.js');
731
+ const freshDefaults = readDefaults();
732
+ const { agents: diskAgents } = scanAgents();
733
+ const diskAidSet = new Set(diskAgents.map(a => a.aid));
734
+ const results = [];
735
+ // 1. 下线:运行时有但磁盘上没有 / disabled 的
736
+ for (const [aid, agent] of [...agentRegistry.agents.entries()]) {
737
+ const diskCfg = diskAgents.find(a => a.aid === aid);
738
+ if (!diskCfg || diskCfg.enabled === false) {
739
+ // 断开所有 channels
740
+ for (const chName of agent.channelInstanceNames()) {
741
+ const inst = channelInstances.find(i => i.adapter.channelName === chName);
742
+ if (inst) {
743
+ try {
744
+ await inst.disconnect();
745
+ }
746
+ catch { }
747
+ const idx = channelInstances.indexOf(inst);
748
+ if (idx >= 0)
749
+ channelInstances.splice(idx, 1);
750
+ }
751
+ }
752
+ agentRegistry.agents.delete(aid);
753
+ results.push(`- ${aid} (offline)`);
754
+ continue;
755
+ }
645
756
  }
646
- else {
647
- logger.debug(`[Config Watch] Config file modified, passes integrity check`);
757
+ // 2. 新增:磁盘上有但运行时没有的
758
+ for (const cfg of diskAgents) {
759
+ if (cfg.enabled === false)
760
+ continue;
761
+ if (agentRegistry.agents.has(cfg.aid))
762
+ continue;
763
+ try {
764
+ await globalThis.__evolclaw_hotLoadAgent(cfg.aid);
765
+ results.push(`+ ${cfg.aid} (online)`);
766
+ }
767
+ catch (e) {
768
+ results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
769
+ }
648
770
  }
649
- });
771
+ // 3. 已有的:重新 reload(config 可能改了)
772
+ const hooks = globalThis.__evolclaw_reloadHooks;
773
+ for (const cfg of diskAgents) {
774
+ if (cfg.enabled === false)
775
+ continue;
776
+ if (!agentRegistry.agents.has(cfg.aid))
777
+ continue;
778
+ // 只有磁盘上存在且运行时也存在的才 reload
779
+ try {
780
+ await agentRegistry.reload(cfg.aid, hooks);
781
+ results.push(`↻ ${cfg.aid} (reloaded)`);
782
+ }
783
+ catch (e) {
784
+ results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
785
+ }
786
+ }
787
+ // 重建 channel index
788
+ agentRegistry.channelIndex.clear();
789
+ agentRegistry.buildChannelIndex();
790
+ logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
791
+ return results;
792
+ };
793
+ // I3: start IPC server LAST, after all hook setup, to eliminate race window
794
+ ipcServer.start();
795
+ // 配置 reload 走 IPC `evolagent.reload` 触发,不再用 watchFile。
796
+ // 双 rename 原子写下 watchFile 的语义会被破坏,且新结构有 N 个 config.json 要监控;
797
+ // 显式触发更可控。
650
798
  // 优雅关闭
651
799
  let shutdownSignal = 'unknown';
652
800
  const shutdown = async (signal) => {
@@ -655,7 +803,6 @@ async function main() {
655
803
  const pid = process.pid;
656
804
  const ppid = process.ppid;
657
805
  logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
658
- fs.unwatchFile(configPath);
659
806
  ipcServer.stop();
660
807
  eventBus.publish({
661
808
  type: 'system:shutdown',
@@ -668,15 +815,24 @@ async function main() {
668
815
  eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
669
816
  }
670
817
  sessionManager.close();
818
+ removeAll();
671
819
  logger.info('✓ Shutdown complete');
672
820
  process.exit(0);
673
821
  };
674
822
  process.on('SIGINT', () => shutdown('SIGINT'));
675
823
  process.on('SIGTERM', () => shutdown('SIGTERM'));
824
+ // 兜底:进程退出前同步删除 instance 文件(防 async shutdown 未完成就被杀)
825
+ process.on('exit', () => {
826
+ removeAll();
827
+ });
828
+ }
829
+ // 仅在直接执行时启动;导入此模块(如单元测试)时不触发 main()。
830
+ import { isMainScript } from './utils/cross-platform.js';
831
+ if (isMainScript(import.meta.url)) {
832
+ main().catch((error) => {
833
+ const msg = `Fatal error: ${error?.stack || error}`;
834
+ logger.error('Fatal error:', error);
835
+ console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
836
+ process.exit(1);
837
+ });
676
838
  }
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
- });