evolclaw 2.8.2 → 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 (106) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +105 -30
  3. package/dist/agents/codex-runner.js +15 -7
  4. package/dist/agents/gemini-runner.js +14 -5
  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 +1064 -279
  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/baseagent-loader.js +48 -0
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +1090 -838
  40. package/dist/core/evolagent-registry.js +191 -360
  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 +326 -145
  48. package/dist/core/message/message-queue.js +5 -5
  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 +437 -273
  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 -576
  92. package/dist/core/agent-loader.js +0 -39
  93. package/dist/core/agent-registry.js +0 -450
  94. package/dist/core/evolagent-schema.js +0 -72
  95. package/dist/core/message/stream-flusher.js +0 -238
  96. package/dist/core/message/thought-emitter.js +0 -162
  97. package/dist/core/reload-hooks.js +0 -87
  98. package/dist/prompts/templates.js +0 -122
  99. package/dist/templates/skills.md +0 -66
  100. package/dist/utils/channel-fingerprint.js +0 -59
  101. package/dist/utils/error-dict.js +0 -63
  102. package/dist/utils/format.js +0 -32
  103. package/dist/utils/init.js +0 -645
  104. package/dist/utils/migrate-project.js +0 -122
  105. package/dist/utils/reload-hooks.js +0 -87
  106. package/dist/utils/stats-collector.js +0 -99
@@ -1,322 +1,245 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { EvolAgent, validateEvolAgentConfig } from './evolagent.js';
1
+ import { EvolAgent } from './evolagent.js';
4
2
  import { logger } from '../utils/logger.js';
3
+ import { loadDefaults, loadAllAgents, mergeForAgent, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
5
4
  // ── Channel Fingerprint ────────────────────────────────────────────────────
6
- // 为每个 channel 实例提取一个全局唯一标识,用于冲突检测和路由索引。
5
+ // 用于检测多 agent 之间复用同一外部凭证的冲突(appId、aid、token 等)。
7
6
  // 格式:{type}:{primaryKey}
8
7
  const PRIMARY_KEY_MAP = {
9
8
  feishu: 'appId',
10
- aun: 'aid',
9
+ aun: '__aid__', // AUN 实例的"凭证"就是 agent 自身 aid
11
10
  wechat: 'token',
12
11
  wecom: 'botId',
13
12
  dingtalk: 'clientId',
14
13
  qqbot: 'appId',
15
14
  };
16
- export function extractFingerprint(channelType, instance) {
15
+ export function extractFingerprint(channelType, instance, agentAid) {
17
16
  const keyField = PRIMARY_KEY_MAP[channelType];
18
17
  if (!keyField)
19
18
  return null;
19
+ if (keyField === '__aid__')
20
+ return `${channelType}:${agentAid}`;
20
21
  const value = instance[keyField];
21
22
  if (!value || typeof value !== 'string')
22
23
  return null;
23
24
  return `${channelType}:${value}`;
24
25
  }
25
- export function detectDuplicates(config) {
26
+ /**
27
+ * 跨 agent 检查同一外部凭证是否被多次声明(飞书 appId、AUN aid 等)。
28
+ */
29
+ export function detectDuplicates(agents) {
26
30
  const seen = new Map();
27
- const channels = config.channels || {};
28
- for (const [type, raw] of Object.entries(channels)) {
29
- if (type === 'defaultChannel')
30
- continue;
31
- const instances = Array.isArray(raw) ? raw : [raw];
32
- for (const inst of instances) {
33
- if (!inst || typeof inst !== 'object')
34
- continue;
35
- const fingerprint = extractFingerprint(type, inst);
36
- if (!fingerprint)
31
+ for (const agent of agents) {
32
+ for (const inst of agent.config.channels) {
33
+ const fp = extractFingerprint(inst.type, inst, agent.aid);
34
+ if (!fp)
37
35
  continue;
38
- const instName = inst.name ?? type;
39
- const entry = seen.get(fingerprint);
40
- if (entry) {
41
- entry.instances.push(instName);
42
- }
43
- else {
44
- seen.set(fingerprint, { channelType: type, instances: [instName] });
45
- }
36
+ const arr = seen.get(fp) ?? [];
37
+ arr.push({ aid: agent.aid, channelName: agent.effectiveChannelName(inst.type, inst.name) });
38
+ seen.set(fp, arr);
46
39
  }
47
40
  }
48
- const duplicates = [];
49
- for (const [fingerprint, entry] of seen) {
50
- if (entry.instances.length > 1) {
51
- duplicates.push({
52
- fingerprint,
53
- channelType: entry.channelType,
54
- instances: entry.instances,
55
- });
41
+ const out = [];
42
+ for (const [fp, arr] of seen) {
43
+ if (arr.length > 1) {
44
+ const [type] = fp.split(':');
45
+ out.push({ fingerprint: fp, channelType: type, agents: arr });
56
46
  }
57
47
  }
58
- return duplicates;
48
+ return out;
59
49
  }
50
+ // ── Registry ───────────────────────────────────────────────────────────────
60
51
  export class EvolAgentRegistry {
61
- agentsDir;
52
+ _agentsDir;
62
53
  agents = new Map();
63
- defaultAgent = null;
54
+ /** channel key (`<aid>#<type>#<name>`) → agent aid */
64
55
  channelIndex = new Map();
65
- globalWriter;
66
- constructor(agentsDir, globalWriter) {
67
- this.agentsDir = agentsDir;
68
- this.globalWriter = globalWriter;
56
+ /** 启动期被 ConfigStore 跳过的目录(命名非法 / 缺 config.json / 校验失败等) */
57
+ skipped = [];
58
+ /**
59
+ * agentsDir 参数保留作 ctor 兼容,但实际加载走 ConfigStore(基于 paths.ts)。
60
+ * globalWriter 已废弃——构造期接受但忽略,阶段 2c 删除。
61
+ */
62
+ constructor(_agentsDir, _globalWriter) {
63
+ this._agentsDir = _agentsDir;
64
+ void _globalWriter;
69
65
  }
70
- /** Late-binding setter for tests / index.ts wiring order. */
71
- setGlobalWriter(writer) {
72
- this.globalWriter = writer;
66
+ setGlobalWriter(_writer) {
67
+ void _writer; // no-op,废弃 API
73
68
  }
74
- loadAll(globalConfig) {
69
+ /**
70
+ * 扫描 agents/ 目录加载所有 self-agent 配置(合并 defaults),构造 EvolAgent
71
+ * 实例并建立 channel 路由索引。
72
+ *
73
+ * `globalConfig` 参数保留作签名兼容,但被忽略——defaults 由 ConfigStore 自己加载。
74
+ */
75
+ loadAll(_globalConfig) {
76
+ void _globalConfig;
75
77
  this.agents.clear();
76
78
  this.channelIndex.clear();
77
- const files = fs.existsSync(this.agentsDir)
78
- ? fs.readdirSync(this.agentsDir).filter(f => f.endsWith('.json'))
79
- : [];
80
- for (const file of files) {
81
- const fullPath = path.join(this.agentsDir, file);
79
+ this.skipped = [];
80
+ const defaults = loadDefaults();
81
+ const { agents: rawAgents, skipped } = loadAllAgents();
82
+ this.skipped = skipped;
83
+ for (const raw of rawAgents) {
82
84
  try {
83
- const raw = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
84
- const validation = validateEvolAgentConfig(raw);
85
- if (!validation.valid) {
86
- const name = raw?.name || path.basename(file, '.json');
87
- const errorAgent = new EvolAgent(fullPath, { ...raw, name });
88
- errorAgent.status = 'error';
89
- errorAgent.error = validation.errors.join('; ');
90
- this.agents.set(name, errorAgent);
91
- logger.warn(`[EvolAgentRegistry] ${file}: ${validation.errors.join('; ')}`);
92
- continue;
93
- }
94
- const agent = new EvolAgent(fullPath, raw);
95
- this.agents.set(agent.name, agent);
85
+ const merged = mergeForAgent(raw, defaults);
86
+ const agent = new EvolAgent(raw, merged);
87
+ ensureAgentDirSkeleton(raw.aid);
88
+ this.agents.set(agent.aid, agent);
96
89
  }
97
90
  catch (e) {
98
- logger.warn(`[EvolAgentRegistry] Failed to load ${file}: ${e}`);
91
+ logger.warn(`[EvolAgentRegistry] failed to construct agent ${raw.aid}: ${e}`);
99
92
  }
100
93
  }
101
- this.defaultAgent = this.buildDefaultAgent(globalConfig);
102
94
  this.detectAndFlagConflicts();
103
95
  this.buildChannelIndex();
104
96
  }
105
- buildDefaultAgent(globalConfig) {
106
- const agents = globalConfig.agents || {};
107
- const defaultName = agents.defaultAgent || 'claude';
108
- const cfg = {
109
- name: '[default]',
110
- enabled: true,
111
- agents: { [defaultName]: agents[defaultName] || {} },
112
- channels: globalConfig.channels || {},
113
- projects: { defaultPath: globalConfig.projects?.defaultPath || process.cwd() },
114
- chatmode: globalConfig.chatmode,
115
- };
116
- return new EvolAgent(null, cfg, { isDefault: true });
117
- }
118
97
  detectAndFlagConflicts() {
119
- const seen = new Map();
120
- const record = (agentName, channelsBlock) => {
121
- for (const [type, raw] of Object.entries(channelsBlock || {})) {
122
- if (type === 'defaultChannel')
123
- continue;
124
- const instances = Array.isArray(raw) ? raw : [raw];
125
- for (const inst of instances) {
126
- if (!inst || typeof inst !== 'object')
127
- continue;
128
- const fp = extractFingerprint(type, inst);
129
- if (!fp)
130
- continue;
131
- const instName = inst.name ?? type;
132
- const entry = seen.get(fp) || [];
133
- entry.push({ agent: agentName, instance: instName });
134
- seen.set(fp, entry);
135
- }
136
- }
137
- };
138
- for (const agent of this.agents.values()) {
139
- if (agent.status === 'error')
140
- continue;
141
- record(agent.name, agent.config.channels);
142
- }
143
- if (this.defaultAgent) {
144
- record(this.defaultAgent.name, this.defaultAgent.config.channels);
145
- }
146
- for (const [_fp, occurrences] of seen) {
147
- if (occurrences.length <= 1)
148
- continue;
149
- const msg = `Channel conflict: fingerprint claimed by ${occurrences.map(o => `${o.agent}(${o.instance})`).join(', ')}`;
150
- const involvedNames = [...new Set(occurrences.map(o => o.agent))];
151
- for (const name of involvedNames) {
152
- if (name === '[default]')
153
- continue;
154
- const a = this.agents.get(name);
98
+ const dups = detectDuplicates([...this.agents.values()]);
99
+ for (const d of dups) {
100
+ const owners = d.agents.map(o => `${o.aid}(${o.channelName})`).join(', ');
101
+ const msg = `Channel conflict: ${d.fingerprint} claimed by ${owners}`;
102
+ logger.error(`[EvolAgentRegistry] ${msg}`);
103
+ // 把所有涉及的 agent error;首个保留为 active 也不安全——直接全部 error
104
+ for (const o of d.agents) {
105
+ const a = this.agents.get(o.aid);
155
106
  if (a && a.status !== 'error') {
156
107
  a.status = 'error';
157
108
  a.error = msg;
158
109
  }
159
110
  }
160
- logger.error(`[EvolAgentRegistry] ${msg}`);
161
111
  }
162
112
  }
163
113
  buildChannelIndex() {
164
114
  for (const agent of this.agents.values()) {
165
115
  if (agent.status === 'error' || agent.status === 'disabled')
166
116
  continue;
167
- for (const name of agent.channelInstanceNames()) {
168
- this.channelIndex.set(name, agent.name);
169
- }
170
- }
171
- if (this.defaultAgent) {
172
- for (const name of this.defaultAgent.channelInstanceNames()) {
173
- if (this.channelIndex.has(name))
174
- continue;
175
- this.channelIndex.set(name, '[default]');
117
+ for (const key of agent.channelInstanceNames()) {
118
+ const prev = this.channelIndex.get(key);
119
+ if (prev && prev !== agent.aid) {
120
+ logger.warn(`[EvolAgentRegistry] channel key "${key}" claimed by both ${prev} and ${agent.aid}`);
121
+ }
122
+ this.channelIndex.set(key, agent.aid);
176
123
  }
177
124
  }
178
125
  }
179
- resolveByChannel(channelName) {
180
- const agentName = this.channelIndex.get(channelName);
181
- if (!agentName)
126
+ // ── Lookup / Routing ─────────────────────────────────────────────────
127
+ resolveByChannel(channelKey) {
128
+ const aid = this.channelIndex.get(channelKey);
129
+ if (!aid)
182
130
  return null;
183
- if (agentName === '[default]')
184
- return this.defaultAgent;
185
- return this.agents.get(agentName) || null;
131
+ return this.agents.get(aid) ?? null;
186
132
  }
187
133
  /**
188
- * Check ownership of a channel via the agent that owns it.
189
- * - For named EvolAgent: reads agent.config (memory; reflects agent.json).
190
- * - For DefaultAgent or unknown channel: invokes `globalFallback`, which
191
- * should consult the global config (evolclaw.json) via `config.ts:isOwner`.
134
+ * `globalFallback` 参数保留作签名兼容(EvolAgentRegistryHandle),新结构下不再使用。
192
135
  */
193
- isOwner(channelName, userId, globalFallback) {
194
- const agent = this.resolveByChannel(channelName);
195
- if (agent && !agent.isDefault)
196
- return agent.isOwner(channelName, userId);
197
- return globalFallback(channelName, userId);
136
+ isOwner(channelKey, userId, _globalFallback) {
137
+ void _globalFallback;
138
+ const agent = this.resolveByChannel(channelKey);
139
+ return agent?.isOwner(channelKey, userId) ?? false;
198
140
  }
199
- /** Same routing logic as `isOwner`, applied to admin checks. */
200
- isAdmin(channelName, userId, globalFallback) {
201
- const agent = this.resolveByChannel(channelName);
202
- if (agent && !agent.isDefault)
203
- return agent.isAdmin(channelName, userId);
204
- return globalFallback(channelName, userId);
141
+ isAdmin(channelKey, userId, _globalFallback) {
142
+ void _globalFallback;
143
+ const agent = this.resolveByChannel(channelKey);
144
+ return agent?.isAdmin(channelKey, userId) ?? false;
205
145
  }
206
- /** Lookup current owner — agent first, then DefaultAgent (which mirrors evolclaw.json). */
207
- getOwner(channelName) {
208
- const agent = this.resolveByChannel(channelName);
209
- if (!agent)
210
- return undefined;
211
- return agent.getOwner(channelName);
146
+ getOwner(channelKey) {
147
+ return this.resolveByChannel(channelKey)?.getOwner(channelKey);
212
148
  }
213
- /**
214
- * Persist owner. Routes to agent.json for named agents, or to evolclaw.json
215
- * via the configured `globalWriter` for DefaultAgent. No-ops with a warning
216
- * when the channel is unknown or no global writer is wired.
217
- */
218
- setChannelOwner(channelName, userId) {
219
- const agent = this.resolveByChannel(channelName);
149
+ setChannelOwner(channelKey, userId) {
150
+ const agent = this.resolveByChannel(channelKey);
220
151
  if (!agent) {
221
- logger.warn(`[EvolAgentRegistry] setChannelOwner: channel "${channelName}" not found`);
152
+ logger.warn(`[EvolAgentRegistry] setChannelOwner: channel "${channelKey}" not found`);
222
153
  return;
223
154
  }
224
- if (agent.isDefault) {
225
- if (!this.globalWriter) {
226
- logger.warn(`[EvolAgentRegistry] setChannelOwner: no globalWriter wired for default channel "${channelName}"`);
227
- return;
228
- }
229
- this.globalWriter.setOwner(channelName, userId);
230
- }
231
- else {
232
- agent.setOwner(channelName, userId);
233
- }
155
+ agent.setOwner(channelKey, userId);
234
156
  }
235
- /**
236
- * Read showActivities mode. Falls back to `'all'` when the channel is
237
- * unknown — matches the prior behavior of `config.ts:getChannelShowActivities`.
238
- */
239
- getShowActivities(channelName) {
240
- const agent = this.resolveByChannel(channelName);
241
- if (!agent)
242
- return 'all';
243
- return agent.getShowActivities(channelName);
157
+ getShowActivities(channelKey) {
158
+ return this.resolveByChannel(channelKey)?.getShowActivities(channelKey) ?? 'all';
244
159
  }
245
- /** Persist showActivities. Routes to agent.json or evolclaw.json. */
246
- setShowActivities(channelName, mode) {
247
- const agent = this.resolveByChannel(channelName);
160
+ setShowActivities(channelKey, mode) {
161
+ const agent = this.resolveByChannel(channelKey);
248
162
  if (!agent) {
249
- logger.warn(`[EvolAgentRegistry] setShowActivities: channel "${channelName}" not found`);
163
+ logger.warn(`[EvolAgentRegistry] setShowActivities: channel "${channelKey}" not found`);
250
164
  return;
251
165
  }
252
- if (agent.isDefault) {
253
- if (!this.globalWriter?.setShowActivities) {
254
- logger.warn(`[EvolAgentRegistry] setShowActivities: no globalWriter wired for default channel "${channelName}"`);
255
- return;
256
- }
257
- this.globalWriter.setShowActivities(channelName, mode);
258
- }
259
- else {
260
- agent.setShowActivities(channelName, mode);
261
- }
166
+ agent.setShowActivities(channelKey, mode);
262
167
  }
263
- get(name) {
264
- if (name === '[default]')
265
- return this.defaultAgent;
266
- return this.agents.get(name) || null;
168
+ // ── Agent enumeration ────────────────────────────────────────────────
169
+ get(aidOrName) {
170
+ return this.agents.get(aidOrName) ?? null;
267
171
  }
268
172
  list() {
269
- const result = [];
270
- for (const agent of this.agents.values()) {
271
- result.push(this.toInfo(agent));
272
- }
273
- if (this.defaultAgent) {
274
- result.push(this.toInfo(this.defaultAgent));
275
- }
276
- return result;
173
+ return [...this.agents.values()].map(a => this.toInfo(a));
277
174
  }
175
+ /** 启动后还能跑(status === 'stopped')的 agents——给 AgentLoader 起 runner 用。 */
278
176
  runnableAgents() {
279
177
  return [...this.agents.values()].filter(a => a.status === 'stopped');
280
178
  }
281
- async reload(name, hooks) {
282
- const oldAgent = this.agents.get(name);
283
- if (!oldAgent)
284
- throw new Error(`Agent "${name}" not found`);
285
- if (!oldAgent.configPath)
286
- throw new Error(`Cannot reload DefaultAgent`);
287
- // 1. Re-read config from disk
288
- const raw = JSON.parse(fs.readFileSync(oldAgent.configPath, 'utf-8'));
289
- const validation = validateEvolAgentConfig(raw);
290
- if (!validation.valid) {
291
- throw new Error(`Invalid config after edit: ${validation.errors.join('; ')}`);
179
+ getSkipped() {
180
+ return [...this.skipped];
181
+ }
182
+ // ── 热加载新 agent ──────────────────────────────────────────────────
183
+ /**
184
+ * 动态加载一个新 agent(磁盘上已有 config.json 但运行时还没加载)。
185
+ * 返回新创建的 EvolAgent,或 null(已存在 / 校验失败)。
186
+ */
187
+ loadNewAgent(aid) {
188
+ if (this.agents.has(aid)) {
189
+ logger.info(`[EvolAgentRegistry] agent ${aid} already loaded, skipping`);
190
+ return this.agents.get(aid);
292
191
  }
293
- const newAgent = new EvolAgent(oldAgent.configPath, raw);
294
- // Warn if projectPath changed — existing sessions retain old path (by design,
295
- // to avoid breaking SDK conversation history at .claude/<encoded-path>/...)
296
- if (oldAgent.projectPath !== newAgent.projectPath) {
297
- logger.warn(`[EvolAgentRegistry] Agent "${name}" projectPath changed: ${oldAgent.projectPath} → ${newAgent.projectPath}. ` +
298
- `Existing sessions retain the old path; only new sessions will use the new path. ` +
299
- `To migrate, manually UPDATE sessions SET project_path=? WHERE id=? (warning: SDK conversation history may be lost).`);
192
+ const raw = loadAgent(aid);
193
+ if (!raw) {
194
+ logger.warn(`[EvolAgentRegistry] loadNewAgent: ${aid}/config.json not found`);
195
+ return null;
300
196
  }
301
- // 2. Fingerprint conflict check (against all others except self)
302
- const conflict = this.checkConflictForReload(newAgent, name);
303
- if (conflict) {
304
- throw new Error(`Channel conflict: ${conflict}`);
197
+ const errs = validateAgentConfig(raw);
198
+ if (errs.length > 0) {
199
+ logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${errs.join('; ')}`);
200
+ return null;
305
201
  }
306
- // 3. Compute channel diff
202
+ const defaults = loadDefaults();
203
+ const merged = mergeForAgent(raw, defaults);
204
+ const agent = new EvolAgent(raw, merged);
205
+ ensureAgentDirSkeleton(aid);
206
+ this.agents.set(aid, agent);
207
+ // 重建 channel index
208
+ for (const key of agent.channelInstanceNames()) {
209
+ this.channelIndex.set(key, aid);
210
+ }
211
+ logger.info(`[EvolAgentRegistry] ✓ Hot-loaded agent: ${aid}`);
212
+ return agent;
213
+ }
214
+ // ── Reload ───────────────────────────────────────────────────────────
215
+ async reload(aidOrName, hooks) {
216
+ const oldAgent = this.agents.get(aidOrName);
217
+ if (!oldAgent)
218
+ throw new Error(`Agent "${aidOrName}" not found`);
219
+ const raw = loadAgent(oldAgent.aid);
220
+ if (!raw)
221
+ throw new Error(`Agent ${oldAgent.aid}/config.json missing on reload`);
222
+ const errs = validateAgentConfig(raw);
223
+ if (errs.length > 0)
224
+ throw new Error(`Invalid config after edit: ${errs.join('; ')}`);
225
+ const defaults = loadDefaults();
226
+ const merged = mergeForAgent(raw, defaults);
227
+ const conflict = this.checkConflictForReload(raw, oldAgent.aid);
228
+ if (conflict)
229
+ throw new Error(`Channel conflict: ${conflict}`);
307
230
  const oldChannels = new Set(oldAgent.channelInstanceNames());
308
- const newChannels = new Set(newAgent.channelInstanceNames());
231
+ // 计算新 channel keys(用 EvolAgent 的格式化)
232
+ const newChannels = new Set(raw.channels.map(c => oldAgent.effectiveChannelName(c.type, c.name)));
309
233
  const toRemove = [...oldChannels].filter(c => !newChannels.has(c));
310
234
  const toAdd = [...newChannels].filter(c => !oldChannels.has(c));
311
235
  const kept = [...oldChannels].filter(c => newChannels.has(c));
312
- // I6: detect kept-channel credential changes — treat as remove+add so
313
- // the channel reconnects with new credentials (e.g. appSecret rotated).
236
+ // 凭证变化的 kept channel remove+add 处理(强制重建以使用新凭证)
314
237
  const credentialsChanged = [];
315
238
  const trulyKept = [];
316
239
  for (const ch of kept) {
317
- const oldCh = getChannelInstanceConfig(oldAgent, ch);
318
- const newCh = getChannelInstanceConfig(newAgent, ch);
319
- if (oldCh && newCh && channelConfigChanged(oldCh.config, newCh.config)) {
240
+ const oldInst = oldAgent.findChannelInstance(ch);
241
+ const newInst = findInstanceByKey(raw, oldAgent, ch);
242
+ if (oldInst && newInst && JSON.stringify(oldInst) !== JSON.stringify(newInst)) {
320
243
  credentialsChanged.push(ch);
321
244
  }
322
245
  else {
@@ -325,179 +248,87 @@ export class EvolAgentRegistry {
325
248
  }
326
249
  toRemove.push(...credentialsChanged);
327
250
  toAdd.push(...credentialsChanged);
328
- // Track what was removed/added so we can roll back on failure
329
251
  const removedSuccessfully = [];
330
252
  const addedSuccessfully = [];
331
253
  try {
332
- // 4. Drain channels being removed
333
- for (const ch of toRemove) {
254
+ for (const ch of toRemove)
334
255
  await hooks.drainChannel(ch);
335
- }
336
- // 5. Disconnect removed channels
337
256
  for (const ch of toRemove) {
338
257
  await hooks.disconnectChannel(ch);
339
258
  removedSuccessfully.push(ch);
340
259
  }
341
- // 6. Start new channels
260
+ // swap config 后再起新 channel —— startChannel hook 需要看到新 config
261
+ oldAgent.swapConfig(raw, merged);
342
262
  for (const ch of toAdd) {
343
- await hooks.startChannel(newAgent, ch);
263
+ await hooks.startChannel(oldAgent, ch);
344
264
  addedSuccessfully.push(ch);
345
265
  }
346
- // 7. Transfer kept channel adapters from old to new (only truly unchanged ones)
347
- for (const ch of trulyKept) {
348
- const adapter = oldAgent.channels.get(ch);
349
- if (adapter)
350
- newAgent.channels.set(ch, adapter);
351
- }
352
- // 8. Preserve runtime state
353
- // I5: only set 'running' when oldAgent was running; preserve error/disabled
354
- newAgent.activeSessions = oldAgent.activeSessions;
355
- newAgent.lastActivity = oldAgent.lastActivity;
266
+ // truly kept adapter 实例已经在 oldAgent.channels 里,无需迁移
356
267
  if (oldAgent.status === 'error' || oldAgent.status === 'disabled') {
357
- newAgent.status = oldAgent.status;
358
- newAgent.error = oldAgent.error;
268
+ // 保持原态——swap 不改 status
359
269
  }
360
270
  else {
361
- newAgent.status = 'running';
271
+ oldAgent.status = 'running';
272
+ }
273
+ // 重启触发器调度器(如果已初始化)
274
+ if (oldAgent.triggerScheduler) {
275
+ oldAgent.triggerScheduler.stop();
276
+ oldAgent.triggerScheduler.init().catch(err => {
277
+ logger.error(`[Reload] TriggerScheduler re-init failed for ${oldAgent.aid}: ${err}`);
278
+ });
362
279
  }
363
- // 9. Swap in registry
364
- this.agents.set(name, newAgent);
365
- // 10. Rebuild channel index
366
280
  this.channelIndex.clear();
367
281
  this.buildChannelIndex();
368
282
  }
369
283
  catch (err) {
370
- // C1: Rollback restore original channels, keep oldAgent in registry
371
- logger.error(`[Reload] Failed: ${err}. Attempting rollback for "${name}".`);
284
+ logger.error(`[Reload] Failed: ${err}. Attempting rollback for "${aidOrName}".`);
372
285
  for (const ch of addedSuccessfully) {
373
286
  try {
374
287
  await hooks.disconnectChannel(ch);
375
288
  }
376
- catch (_) { /* best effort */ }
377
- }
378
- for (const ch of removedSuccessfully) {
379
- try {
380
- await hooks.startChannel(oldAgent, ch);
381
- }
382
- catch (_) { /* best effort */ }
289
+ catch { /* best effort */ }
383
290
  }
384
- // Don't swap registry oldAgent stays in place
291
+ // 这里没法 rollback 到旧 raw(已经被 swapConfig 覆盖)——记录错误,让 oldAgent error
385
292
  oldAgent.status = 'error';
386
- oldAgent.error = `Reload failed (rollback attempted): ${err instanceof Error ? err.message : String(err)}`;
293
+ oldAgent.error = `Reload failed (rollback partial): ${err instanceof Error ? err.message : String(err)}`;
387
294
  throw err;
388
295
  }
389
296
  }
390
- checkConflictForReload(newAgent, excludeName) {
391
- const newFingerprints = new Map(); // fp → instanceName
392
- for (const [type, raw] of Object.entries(newAgent.config.channels || {})) {
393
- if (type === 'defaultChannel')
394
- continue;
395
- const instances = Array.isArray(raw) ? raw : [raw];
396
- for (const inst of instances) {
397
- if (!inst || typeof inst !== 'object')
398
- continue;
399
- const fp = extractFingerprint(type, inst);
400
- if (!fp)
401
- continue;
402
- const instName = inst.name ?? type;
403
- newFingerprints.set(fp, instName);
404
- }
297
+ checkConflictForReload(newRaw, excludeAid) {
298
+ const newFps = new Set();
299
+ for (const inst of newRaw.channels) {
300
+ const fp = extractFingerprint(inst.type, inst, newRaw.aid);
301
+ if (fp)
302
+ newFps.add(fp);
405
303
  }
406
- // Check against all other agents (excluding self)
407
- for (const [agentName, agent] of this.agents) {
408
- if (agentName === excludeName)
304
+ for (const [aid, agent] of this.agents) {
305
+ if (aid === excludeAid)
409
306
  continue;
410
307
  if (agent.status === 'error' || agent.status === 'disabled')
411
308
  continue;
412
- for (const [type, raw] of Object.entries(agent.config.channels || {})) {
413
- if (type === 'defaultChannel')
414
- continue;
415
- const instances = Array.isArray(raw) ? raw : [raw];
416
- for (const inst of instances) {
417
- if (!inst || typeof inst !== 'object')
418
- continue;
419
- const fp = extractFingerprint(type, inst);
420
- if (!fp)
421
- continue;
422
- if (newFingerprints.has(fp)) {
423
- return `${fp} conflicts with agent "${agentName}"`;
424
- }
425
- }
426
- }
427
- }
428
- // Check against DefaultAgent
429
- if (this.defaultAgent) {
430
- for (const [type, raw] of Object.entries(this.defaultAgent.config.channels || {})) {
431
- if (type === 'defaultChannel')
432
- continue;
433
- const instances = Array.isArray(raw) ? raw : [raw];
434
- for (const inst of instances) {
435
- if (!inst || typeof inst !== 'object')
436
- continue;
437
- const fp = extractFingerprint(type, inst);
438
- if (!fp)
439
- continue;
440
- if (newFingerprints.has(fp)) {
441
- return `${fp} conflicts with DefaultAgent`;
442
- }
443
- }
309
+ for (const inst of agent.config.channels) {
310
+ const fp = extractFingerprint(inst.type, inst, agent.aid);
311
+ if (fp && newFps.has(fp))
312
+ return `${fp} conflicts with agent "${aid}"`;
444
313
  }
445
314
  }
446
315
  return null;
447
316
  }
448
317
  toInfo(agent) {
449
- let baseagent = 'claude';
450
- let model;
451
- let effort;
452
- try {
453
- baseagent = agent.baseagent;
454
- model = agent.model;
455
- effort = agent.effort;
456
- }
457
- catch { /* invalid config */ }
458
318
  return {
459
319
  name: agent.name,
460
320
  status: agent.status,
461
321
  channels: agent.channelInstanceNames(),
462
- projectPath: agent.config.projects?.defaultPath ?? '',
463
- baseagent,
464
- model,
465
- effort,
322
+ projectPath: agent.projectPath,
323
+ baseagent: agent.baseagent,
324
+ model: agent.model,
325
+ effort: agent.effort,
466
326
  lastActivity: agent.lastActivity,
467
327
  activeSessions: agent.activeSessions,
468
328
  error: agent.error,
469
- isDefault: agent.isDefault,
470
329
  };
471
330
  }
472
331
  }
473
- /**
474
- * Locate the raw config of a channel instance by name within an agent config.
475
- * Returns `{ type, config }` or null if not found.
476
- *
477
- * Matches against the effective channel name (with agent prefix for EvolAgents),
478
- * mirroring `EvolAgent.findChannelInstance`. Only used by `reload()` to detect
479
- * kept-channel credential changes.
480
- */
481
- function getChannelInstanceConfig(agent, channelName) {
482
- for (const [type, raw] of Object.entries(agent.config?.channels || {})) {
483
- if (type === 'defaultChannel')
484
- continue;
485
- const instances = Array.isArray(raw) ? raw : [raw];
486
- for (const inst of instances) {
487
- if (!inst || typeof inst !== 'object')
488
- continue;
489
- const effName = agent.effectiveChannelName(type, inst.name);
490
- if (effName === channelName)
491
- return { type, config: inst };
492
- }
493
- }
494
- return null;
495
- }
496
- /**
497
- * Compare two channel-instance configs by serialized JSON. Channel configs
498
- * are plain JSON-shaped objects (no functions/Buffers), so this is a sound
499
- * structural compare for "did anything in this channel block change".
500
- */
501
- function channelConfigChanged(oldConfig, newConfig) {
502
- return JSON.stringify(oldConfig) !== JSON.stringify(newConfig);
332
+ function findInstanceByKey(raw, agent, channelKey) {
333
+ return raw.channels.find(c => agent.effectiveChannelName(c.type, c.name) === channelKey) ?? null;
503
334
  }