evolclaw 2.8.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +102 -38
  3. package/dist/agents/codex-runner.js +11 -14
  4. package/dist/agents/gemini-runner.js +10 -12
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1051 -288
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +858 -847
  40. package/dist/core/evolagent-registry.js +191 -371
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +309 -142
  48. package/dist/core/message/message-queue.js +3 -3
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +431 -275
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -591
  92. package/dist/core/agent-registry.js +0 -450
  93. package/dist/core/evolagent-schema.js +0 -72
  94. package/dist/core/message/stream-flusher.js +0 -238
  95. package/dist/core/message/thought-emitter.js +0 -162
  96. package/dist/core/reload-hooks.js +0 -87
  97. package/dist/prompts/templates.js +0 -122
  98. package/dist/templates/skills.md +0 -66
  99. package/dist/utils/channel-fingerprint.js +0 -59
  100. package/dist/utils/error-dict.js +0 -63
  101. package/dist/utils/format.js +0 -32
  102. package/dist/utils/init.js +0 -645
  103. package/dist/utils/migrate-project.js +0 -122
  104. package/dist/utils/reload-hooks.js +0 -87
  105. package/dist/utils/stats-collector.js +0 -99
@@ -0,0 +1,613 @@
1
+ /**
2
+ * ConfigStore —— 新结构(evolclaw-home-directory.md)的配置加载/合并/写入。
3
+ *
4
+ * agents/defaults.json ← DefaultsConfig
5
+ * agents/<aid>/config.json ← AgentConfig(per-agent)
6
+ *
7
+ * 合并规则(mergeForAgent):
8
+ * - 深合并:models / chatmode / aun / baseagents / projects 子字段
9
+ * - 标量覆盖:active_baseagent / show_activities / flush_delay / debounce
10
+ * - per-agent only:aid / enabled / owners / admins / channels(不进 defaults)
11
+ *
12
+ * 写入:通过 atomic-write 双 rename,避免崩溃损坏。
13
+ *
14
+ * 旧 data/evolclaw.json / agents/<name>.json 不识别、不兼容——视若不存在。
15
+ */
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import { resolvePaths, agentConfig as agentConfigPath, agentDir, } from './paths.js';
19
+ import { atomicReadJson, atomicWriteJson } from './utils/atomic-write.js';
20
+ import { checkAgentDir, isValidAid } from './aun/aid/validation.js';
21
+ import { isValidChannelName } from './core/channel-loader.js';
22
+ import { CONFIG_SCHEMA_VERSION } from './types.js';
23
+ import { logger } from './utils/logger.js';
24
+ const SUPPORTED_CHANNEL_TYPES = new Set([
25
+ 'aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom',
26
+ ]);
27
+ const ENV_PREFIX = '$ENV:';
28
+ // ── env 展开 ────────────────────────────────────────────────────────────
29
+ /**
30
+ * 递归展开对象中形如 "$ENV:NAME" 的字符串。
31
+ * - 命中环境变量 → 替换为变量值
32
+ * - 未设置环境变量 → 字段视为空字符串,并 warning 提示一次(同一变量名只警告一次)
33
+ *
34
+ * 真正"漏配是否致命"由调用方在 use 时报错。
35
+ */
36
+ const warnedEnvKeys = new Set();
37
+ export function expandEnvRefs(value) {
38
+ return walk(value);
39
+ }
40
+ function walk(v) {
41
+ if (typeof v === 'string') {
42
+ if (v.startsWith(ENV_PREFIX)) {
43
+ const name = v.slice(ENV_PREFIX.length);
44
+ const env = process.env[name];
45
+ if (env === undefined) {
46
+ if (!warnedEnvKeys.has(name)) {
47
+ logger.warn(`[config] env "${name}" not set; field will be empty`);
48
+ warnedEnvKeys.add(name);
49
+ }
50
+ return '';
51
+ }
52
+ return env;
53
+ }
54
+ return v;
55
+ }
56
+ if (Array.isArray(v))
57
+ return v.map(walk);
58
+ if (v && typeof v === 'object') {
59
+ const out = {};
60
+ for (const [k, val] of Object.entries(v))
61
+ out[k] = walk(val);
62
+ return out;
63
+ }
64
+ return v;
65
+ }
66
+ // ── 加载/写入 ──────────────────────────────────────────────────────────
67
+ export function loadDefaults() {
68
+ const p = resolvePaths().defaultsConfig;
69
+ const raw = atomicReadJson(p);
70
+ if (raw === null)
71
+ return null;
72
+ if (typeof raw.$schema_version !== 'number') {
73
+ logger.warn(`[config] ${p}: missing $schema_version, treating as ${CONFIG_SCHEMA_VERSION}`);
74
+ }
75
+ return expandEnvRefs(raw);
76
+ }
77
+ export function saveDefaults(value) {
78
+ atomicWriteJson(resolvePaths().defaultsConfig, value);
79
+ }
80
+ // ── 自动迁移 ───────────────────────────────────────────────────────────
81
+ /**
82
+ * 启动时自动迁移:如果 agents/defaults.json 不存在但 data/evolclaw.json 存在,
83
+ * 从旧配置构造新结构(defaults.json + per-agent config.json),然后把旧文件改名。
84
+ *
85
+ * 同时处理旧 agents/<name>.json 文件(friendly-name 形态)。
86
+ *
87
+ * 幂等:已迁移过(defaults.json 存在)则跳过。
88
+ */
89
+ export function autoMigrateIfNeeded() {
90
+ const p = resolvePaths();
91
+ const defaultsPath = p.defaultsConfig;
92
+ const oldConfigPath = path.join(p.dataDir, 'evolclaw.json');
93
+ if (fs.existsSync(defaultsPath))
94
+ return;
95
+ if (!fs.existsSync(oldConfigPath))
96
+ return;
97
+ logger.info('[migrate] Detected legacy data/evolclaw.json without agents/defaults.json — auto-migrating...');
98
+ let oldConfig;
99
+ try {
100
+ oldConfig = JSON.parse(fs.readFileSync(oldConfigPath, 'utf-8'));
101
+ }
102
+ catch (e) {
103
+ logger.error(`[migrate] Failed to parse ${oldConfigPath}: ${e}`);
104
+ return;
105
+ }
106
+ // 1. 构造 defaults.json
107
+ const defaults = {
108
+ $schema_version: CONFIG_SCHEMA_VERSION,
109
+ active_baseagent: oldConfig.agents?.defaultAgent || 'claude',
110
+ baseagents: {},
111
+ models: oldConfig.models,
112
+ projects: oldConfig.projects ? { defaultPath: oldConfig.projects.defaultPath, list: oldConfig.projects.list, autoCreate: oldConfig.projects.autoCreate } : undefined,
113
+ chatmode: oldConfig.chatmode,
114
+ show_activities: oldConfig.showActivities,
115
+ flush_delay: oldConfig.flushDelay,
116
+ debounce: oldConfig.debounce,
117
+ aun: oldConfig.channels?.aun?.keystorePath ? { keystorePath: oldConfig.channels.aun.keystorePath } : undefined,
118
+ };
119
+ // 搬 baseagents
120
+ const KNOWN_BASEAGENTS = ['claude', 'codex', 'gemini', 'hermes'];
121
+ for (const ba of KNOWN_BASEAGENTS) {
122
+ const block = oldConfig.agents?.[ba];
123
+ if (block && typeof block === 'object') {
124
+ defaults.baseagents[ba] = { ...block };
125
+ }
126
+ }
127
+ fs.mkdirSync(path.dirname(defaultsPath), { recursive: true });
128
+ atomicWriteJson(defaultsPath, defaults);
129
+ logger.info(`[migrate] ✓ Created ${defaultsPath}`);
130
+ // 2. 从旧 evolclaw.json 的 channels 块 + 旧 agents/<name>.json 构造 per-agent config.json
131
+ // 旧结构:evolclaw.json.channels 是全局 channel dict(属于 "default agent"),
132
+ // agents/<name>.json 是 named agent。
133
+ // 2a. 处理旧 agents/<name>.json 文件
134
+ const agentsDir = p.agentsDir;
135
+ if (fs.existsSync(agentsDir)) {
136
+ const entries = fs.readdirSync(agentsDir);
137
+ for (const entry of entries) {
138
+ if (!entry.endsWith('.json') || entry === 'defaults.json' || entry.startsWith('schema-'))
139
+ continue;
140
+ const filePath = path.join(agentsDir, entry);
141
+ if (!fs.statSync(filePath).isFile())
142
+ continue;
143
+ try {
144
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
145
+ const aid = raw.channels?.aun?.aid;
146
+ if (!aid || !isValidAid(aid)) {
147
+ logger.warn(`[migrate] skip ${entry}: no valid AID in channels.aun.aid`);
148
+ continue;
149
+ }
150
+ const agentConfig = buildAgentConfigFromLegacy(aid, raw, oldConfig);
151
+ const agentDirPath = path.join(agentsDir, aid);
152
+ fs.mkdirSync(agentDirPath, { recursive: true });
153
+ atomicWriteJson(path.join(agentDirPath, 'config.json'), agentConfig);
154
+ ensureAgentDirSkeleton(aid);
155
+ // 改名旧文件
156
+ fs.renameSync(filePath, filePath + '_');
157
+ logger.info(`[migrate] ✓ ${entry} → agents/${aid}/config.json`);
158
+ }
159
+ catch (e) {
160
+ logger.warn(`[migrate] skip ${entry}: ${e}`);
161
+ }
162
+ }
163
+ }
164
+ // 2b. 如果 evolclaw.json 的 channels 里有 AUN 实例且没有对应的 agents/<name>.json,
165
+ // 说明旧结构只有一个"default agent"——为它也建一个 per-agent config.json
166
+ const globalAun = oldConfig.channels?.aun;
167
+ const globalAunAid = Array.isArray(globalAun)
168
+ ? globalAun[0]?.aid
169
+ : globalAun?.aid;
170
+ if (globalAunAid && isValidAid(globalAunAid)) {
171
+ const targetDir = path.join(agentsDir, globalAunAid);
172
+ if (!fs.existsSync(path.join(targetDir, 'config.json'))) {
173
+ const agentConfig = buildAgentConfigFromGlobalChannels(globalAunAid, oldConfig);
174
+ fs.mkdirSync(targetDir, { recursive: true });
175
+ atomicWriteJson(path.join(targetDir, 'config.json'), agentConfig);
176
+ ensureAgentDirSkeleton(globalAunAid);
177
+ logger.info(`[migrate] ✓ global channels → agents/${globalAunAid}/config.json`);
178
+ }
179
+ }
180
+ // 3. 改名旧 evolclaw.json
181
+ try {
182
+ fs.renameSync(oldConfigPath, oldConfigPath + '_');
183
+ logger.info(`[migrate] ✓ Renamed ${oldConfigPath} → evolclaw.json_`);
184
+ }
185
+ catch (e) {
186
+ logger.warn(`[migrate] Failed to rename old config: ${e}`);
187
+ }
188
+ logger.info('[migrate] Auto-migration complete.');
189
+ }
190
+ /**
191
+ * 从旧 agents/<name>.json 构造新 AgentConfig。
192
+ * 旧格式:{ name, enabled, agents: { claude: {...} }, channels: { aun: {...}, feishu: {...} }, projects, chatmode }
193
+ */
194
+ function buildAgentConfigFromLegacy(aid, raw, globalConfig) {
195
+ const channels = [];
196
+ // AUN 是隐式的(从 aid 派生),不放进 channels[]
197
+ const aunBlock = raw.channels?.aun;
198
+ // 其它 channels(非 AUN)
199
+ for (const [type, block] of Object.entries(raw.channels || {})) {
200
+ if (type === 'aun' || type === 'defaultChannel')
201
+ continue;
202
+ const instances = Array.isArray(block) ? block : [block];
203
+ for (const inst of instances) {
204
+ if (!inst || typeof inst !== 'object')
205
+ continue;
206
+ channels.push({
207
+ ...inst,
208
+ type,
209
+ name: inst.name || 'main',
210
+ });
211
+ }
212
+ }
213
+ // owners / admins
214
+ const owners = [];
215
+ const admins = [];
216
+ if (aunBlock?.owner)
217
+ owners.push(aunBlock.owner);
218
+ if (aunBlock?.admins)
219
+ admins.push(...aunBlock.admins);
220
+ // baseagent
221
+ const agentKeys = Object.keys(raw.agents || {}).filter(k => k !== 'defaultAgent');
222
+ const activeBaseagent = agentKeys[0] || globalConfig.agents?.defaultAgent || 'claude';
223
+ return {
224
+ $schema_version: CONFIG_SCHEMA_VERSION,
225
+ aid,
226
+ enabled: raw.enabled !== false,
227
+ owners,
228
+ admins: admins.length > 0 ? admins : undefined,
229
+ channels,
230
+ active_baseagent: activeBaseagent,
231
+ baseagents: raw.agents ? filterBaseagents(raw.agents) : undefined,
232
+ projects: raw.projects,
233
+ chatmode: raw.chatmode,
234
+ };
235
+ }
236
+ /**
237
+ * 从旧 evolclaw.json 的全局 channels 块构造 per-agent config("default agent" 场景)。
238
+ */
239
+ function buildAgentConfigFromGlobalChannels(aid, globalConfig) {
240
+ const channels = [];
241
+ // AUN 是隐式的(从 aid 派生),不放进 channels[]
242
+ const aunRaw = globalConfig.channels?.aun;
243
+ const aunInst = Array.isArray(aunRaw) ? aunRaw[0] : aunRaw;
244
+ // 其它 channels(非 AUN)
245
+ for (const [type, block] of Object.entries(globalConfig.channels || {})) {
246
+ if (type === 'aun' || type === 'defaultChannel')
247
+ continue;
248
+ const instances = Array.isArray(block) ? block : [block];
249
+ for (const inst of instances) {
250
+ if (!inst || typeof inst !== 'object')
251
+ continue;
252
+ if (inst.enabled === false)
253
+ continue;
254
+ channels.push({
255
+ ...inst,
256
+ type,
257
+ name: inst.name || 'main',
258
+ });
259
+ }
260
+ }
261
+ const owners = [];
262
+ const admins = [];
263
+ if (aunInst?.owner)
264
+ owners.push(aunInst.owner);
265
+ if (aunInst?.admins)
266
+ admins.push(...aunInst.admins);
267
+ const activeBaseagent = globalConfig.agents?.defaultAgent || 'claude';
268
+ return {
269
+ $schema_version: CONFIG_SCHEMA_VERSION,
270
+ aid,
271
+ enabled: true,
272
+ owners,
273
+ admins: admins.length > 0 ? admins : undefined,
274
+ channels,
275
+ active_baseagent: activeBaseagent,
276
+ projects: globalConfig.projects,
277
+ chatmode: globalConfig.chatmode,
278
+ };
279
+ }
280
+ function filterBaseagents(agents) {
281
+ const result = {};
282
+ const KNOWN = ['claude', 'codex', 'gemini', 'hermes'];
283
+ for (const k of KNOWN) {
284
+ if (agents[k] && typeof agents[k] === 'object')
285
+ result[k] = agents[k];
286
+ }
287
+ return Object.keys(result).length > 0 ? result : undefined;
288
+ }
289
+ export function loadAgent(aid) {
290
+ const p = agentConfigPath(aid);
291
+ const raw = atomicReadJson(p);
292
+ if (raw === null)
293
+ return null;
294
+ if (raw.aid !== aid) {
295
+ throw new Error(`[config] ${p}: aid field "${raw.aid}" != directory name "${aid}"`);
296
+ }
297
+ return expandEnvRefs(raw);
298
+ }
299
+ export function saveAgent(value) {
300
+ atomicWriteJson(agentConfigPath(value.aid), value);
301
+ }
302
+ /**
303
+ * 扫描 agents/ 目录加载所有 self-agent 配置。
304
+ *
305
+ * 跳过条件(warn-and-skip):
306
+ * - 目录名不是合法 AID
307
+ * - 缺 config.json
308
+ * - config.json 解析或基础校验失败
309
+ *
310
+ * 不抛错——返回 skipped 列表交调用方决定是否继续。
311
+ */
312
+ export function loadAllAgents() {
313
+ const agentsDir = resolvePaths().agentsDir;
314
+ const result = { agents: [], skipped: [] };
315
+ if (!fs.existsSync(agentsDir))
316
+ return result;
317
+ const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
318
+ for (const entry of entries) {
319
+ if (!entry.isDirectory())
320
+ continue;
321
+ const dirName = entry.name;
322
+ const why = checkAgentDir(agentsDir, dirName);
323
+ if (why) {
324
+ result.skipped.push({ dirName, reason: why });
325
+ logger.warn(`[config] skip agents/${dirName}: ${why}`);
326
+ continue;
327
+ }
328
+ try {
329
+ const cfg = loadAgent(dirName);
330
+ if (cfg === null) {
331
+ result.skipped.push({ dirName, reason: 'config.json missing after dir check' });
332
+ continue;
333
+ }
334
+ const errs = validateAgentConfig(cfg);
335
+ if (errs.length > 0) {
336
+ const reason = errs.join('; ');
337
+ result.skipped.push({ dirName, reason });
338
+ logger.warn(`[config] skip agents/${dirName}: ${reason}`);
339
+ continue;
340
+ }
341
+ result.agents.push(cfg);
342
+ }
343
+ catch (e) {
344
+ const reason = e instanceof Error ? e.message : String(e);
345
+ result.skipped.push({ dirName, reason });
346
+ logger.warn(`[config] skip agents/${dirName}: ${reason}`);
347
+ }
348
+ }
349
+ return result;
350
+ }
351
+ // ── 校验 ───────────────────────────────────────────────────────────────
352
+ export function validateAgentConfig(cfg) {
353
+ const errs = [];
354
+ if (!cfg.aid || !isValidAid(cfg.aid))
355
+ errs.push(`invalid aid: ${cfg.aid}`);
356
+ if (!Array.isArray(cfg.channels)) {
357
+ errs.push('channels must be an array');
358
+ return errs;
359
+ }
360
+ let aunCount = 0;
361
+ const seenNamesByType = new Map();
362
+ for (const [i, ch] of cfg.channels.entries()) {
363
+ if (!ch || typeof ch !== 'object') {
364
+ errs.push(`channels[${i}] must be an object`);
365
+ continue;
366
+ }
367
+ if (ch.type === 'aun') {
368
+ // AUN 是隐式的(从 agent.aid 派生),不应出现在 channels[] 里
369
+ // 容忍但 warn——不算 error,跳过校验
370
+ aunCount++;
371
+ continue;
372
+ }
373
+ if (!SUPPORTED_CHANNEL_TYPES.has(ch.type)) {
374
+ errs.push(`channels[${i}].type "${ch.type}" not supported`);
375
+ continue;
376
+ }
377
+ if (!isValidChannelName(ch.name)) {
378
+ errs.push(`channels[${i}].name invalid (empty or contains '#'): ${JSON.stringify(ch.name)}`);
379
+ continue;
380
+ }
381
+ const set = seenNamesByType.get(ch.type) ?? new Set();
382
+ if (set.has(ch.name)) {
383
+ errs.push(`channels[${i}]: duplicate name "${ch.name}" within type "${ch.type}"`);
384
+ }
385
+ else {
386
+ set.add(ch.name);
387
+ seenNamesByType.set(ch.type, set);
388
+ }
389
+ }
390
+ if (aunCount > 0) {
391
+ logger.warn(`[config] agent ${cfg.aid}: channels[] contains ${aunCount} AUN entry(s) — AUN is implicit, these will be ignored`);
392
+ }
393
+ return errs;
394
+ }
395
+ // ── 合并 ───────────────────────────────────────────────────────────────
396
+ /**
397
+ * defaults + per-agent → MergedAgentConfig
398
+ * defaults 缺失时直接返回 per-agent(视 defaults 为空对象)。
399
+ */
400
+ export function mergeForAgent(agent, defaults) {
401
+ const d = defaults ?? { $schema_version: CONFIG_SCHEMA_VERSION };
402
+ const merged = {
403
+ $schema_version: agent.$schema_version ?? CONFIG_SCHEMA_VERSION,
404
+ aid: agent.aid,
405
+ enabled: agent.enabled,
406
+ owners: agent.owners,
407
+ admins: agent.admins,
408
+ aun: deepMergeBlocks(d.aun, agent.aun),
409
+ channels: agent.channels,
410
+ active_baseagent: agent.active_baseagent ?? d.active_baseagent,
411
+ baseagents: deepMergeBlocks(d.baseagents, agent.baseagents),
412
+ models: deepMergeBlocks(d.models, agent.models),
413
+ projects: deepMergeBlocks(d.projects, agent.projects),
414
+ chatmode: deepMergeBlocks(d.chatmode, agent.chatmode),
415
+ show_activities: agent.show_activities ?? d.show_activities,
416
+ flush_delay: agent.flush_delay ?? d.flush_delay,
417
+ debounce: agent.debounce ?? d.debounce,
418
+ debug: deepMergeBlocks(d.debug, agent.debug),
419
+ enable_rich_content: agent.enable_rich_content ?? d.enable_rich_content,
420
+ };
421
+ return merged;
422
+ }
423
+ function deepMergeBlocks(base, overlay) {
424
+ if (!base && !overlay)
425
+ return undefined;
426
+ if (!base)
427
+ return overlay;
428
+ if (!overlay)
429
+ return base;
430
+ return deepMergeObject(base, overlay);
431
+ }
432
+ /** 递归对象合并:overlay 覆盖 base;标量与数组按 overlay 直接替换;plain object 递归。 */
433
+ function deepMergeObject(base, overlay) {
434
+ if (overlay === undefined)
435
+ return base;
436
+ if (base === undefined)
437
+ return overlay;
438
+ if (!isPlainObject(base) || !isPlainObject(overlay))
439
+ return overlay;
440
+ const out = { ...base };
441
+ for (const [k, v] of Object.entries(overlay)) {
442
+ out[k] = deepMergeObject(base[k], v);
443
+ }
444
+ return out;
445
+ }
446
+ function isPlainObject(v) {
447
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
448
+ }
449
+ // ── 目录骨架 ───────────────────────────────────────────────────────────
450
+ /**
451
+ * 为某个 self-agent 创建文档约定的子目录骨架(personal/、identities/、venues/ 等)。
452
+ * 已存在则跳过,幂等可重复调用。
453
+ */
454
+ export function ensureAgentDirSkeleton(aid) {
455
+ const root = agentDir(aid);
456
+ const subs = [
457
+ 'personal',
458
+ 'identities/contacts',
459
+ 'identities/_observed',
460
+ 'identities/_index',
461
+ 'identities/_observed/_index',
462
+ 'identities/_trash',
463
+ 'venues',
464
+ 'venues/_index',
465
+ 'venues/_trash',
466
+ 'sessions',
467
+ 'data/cache',
468
+ ];
469
+ for (const sub of subs) {
470
+ fs.mkdirSync(path.join(root, sub), { recursive: true });
471
+ }
472
+ }
473
+ // ── Project Migration ────────────────────────────────────────────────────────
474
+ import os from 'os';
475
+ /** 将绝对路径编码为 Claude Code 的目录名格式(/ \ . 替换为 -) */
476
+ function encodePath(p) {
477
+ return p.replace(/[/\\\.]/g, '-');
478
+ }
479
+ /** 查找最新的 ~/.codex/state_*.sqlite */
480
+ function findCodexDb() {
481
+ const codexHome = path.join(os.homedir(), '.codex');
482
+ if (!fs.existsSync(codexHome))
483
+ return null;
484
+ const files = fs.readdirSync(codexHome)
485
+ .filter(f => /^state_\d+\.sqlite$/.test(f))
486
+ .sort((a, b) => {
487
+ const va = parseInt(a.match(/state_(\d+)/)?.[1] || '0');
488
+ const vb = parseInt(b.match(/state_(\d+)/)?.[1] || '0');
489
+ return vb - va;
490
+ });
491
+ return files.length > 0 ? path.join(codexHome, files[0]) : null;
492
+ }
493
+ export async function migrateProject(oldPath, newPath) {
494
+ const result = {
495
+ claudeSessionsMoved: false,
496
+ claudeHistoryUpdated: false,
497
+ codexUpdated: 0,
498
+ evolclawDbUpdated: 0,
499
+ evolclawConfigUpdated: false,
500
+ directoryMoved: false,
501
+ };
502
+ const oldAbs = path.resolve(oldPath);
503
+ const newAbs = path.resolve(newPath);
504
+ if (!fs.existsSync(oldAbs))
505
+ throw new Error(`源目录不存在: ${oldAbs}`);
506
+ if (fs.existsSync(newAbs))
507
+ throw new Error(`目标目录已存在: ${newAbs}`);
508
+ const claudeProjects = path.join(os.homedir(), '.claude', 'projects');
509
+ const oldEncoded = encodePath(oldAbs);
510
+ const newEncoded = encodePath(newAbs);
511
+ // 1. 迁移 ~/.claude/projects/{encoded}/
512
+ const oldClaudeDir = path.join(claudeProjects, oldEncoded);
513
+ const newClaudeDir = path.join(claudeProjects, newEncoded);
514
+ if (fs.existsSync(oldClaudeDir)) {
515
+ fs.renameSync(oldClaudeDir, newClaudeDir);
516
+ result.claudeSessionsMoved = true;
517
+ }
518
+ // 2. .jsonl 内部路径不需要替换 — 它们是历史对话记录,
519
+ // resume 时模型会根据当前 cwd 工作,旧路径只是历史上下文
520
+ // 3. 更新 ~/.claude/history.jsonl
521
+ const historyFile = path.join(os.homedir(), '.claude', 'history.jsonl');
522
+ if (fs.existsSync(historyFile)) {
523
+ const lines = fs.readFileSync(historyFile, 'utf-8').split('\n');
524
+ const updated = lines.map(line => {
525
+ if (!line.trim())
526
+ return line;
527
+ try {
528
+ const obj = JSON.parse(line);
529
+ if (obj.project === oldAbs) {
530
+ obj.project = newAbs;
531
+ return JSON.stringify(obj);
532
+ }
533
+ }
534
+ catch { /* skip */ }
535
+ return line;
536
+ });
537
+ const newContent = updated.join('\n');
538
+ if (newContent !== fs.readFileSync(historyFile, 'utf-8')) {
539
+ fs.writeFileSync(historyFile, newContent, 'utf-8');
540
+ result.claudeHistoryUpdated = true;
541
+ }
542
+ }
543
+ // 4. 更新 Codex SQLite threads.cwd
544
+ const codexDbPath = findCodexDb();
545
+ if (codexDbPath) {
546
+ try {
547
+ const { DatabaseSync } = await import('node:sqlite');
548
+ const db = new DatabaseSync(codexDbPath);
549
+ const r = db.prepare('UPDATE threads SET cwd = ? WHERE cwd = ?').run(newAbs, oldAbs);
550
+ result.codexUpdated = r.changes ?? 0;
551
+ db.close();
552
+ }
553
+ catch { /* Codex not installed or DB locked */ }
554
+ }
555
+ // 5. 移动项目目录
556
+ fs.renameSync(oldAbs, newAbs);
557
+ result.directoryMoved = true;
558
+ // 6. 更新 EvolClaw sessions(文件系统)
559
+ const p = resolvePaths();
560
+ if (fs.existsSync(p.sessionsDir)) {
561
+ try {
562
+ const { scanChatDirs, scanMetaFiles, readJsonFile, atomicWriteJson, appendJsonl } = await import('./core/session/session-fs-store.js');
563
+ let updated = 0;
564
+ const chatDirs = scanChatDirs(p.sessionsDir);
565
+ for (const { dirPath } of chatDirs) {
566
+ // 更新 active.json
567
+ const activePath = path.join(dirPath, 'active.json');
568
+ const active = readJsonFile(activePath);
569
+ if (active && active.projectPath === oldAbs) {
570
+ active.projectPath = newAbs;
571
+ active.updatedAt = Date.now();
572
+ atomicWriteJson(activePath, active);
573
+ updated++;
574
+ }
575
+ // 更新各 meta jsonl 的最后一行(append 一条新快照标记 projectPath 变化)
576
+ for (const metaFile of scanMetaFiles(dirPath)) {
577
+ const metaPath = path.join(dirPath, metaFile);
578
+ const { readLastJsonlLine } = await import('./core/session/session-fs-store.js');
579
+ const meta = readLastJsonlLine(metaPath);
580
+ if (meta && meta.projectPath === oldAbs) {
581
+ meta.projectPath = newAbs;
582
+ meta.updatedAt = Date.now();
583
+ appendJsonl(metaPath, meta);
584
+ updated++;
585
+ }
586
+ }
587
+ }
588
+ result.evolclawDbUpdated = updated;
589
+ }
590
+ catch { /* fs not accessible */ }
591
+ }
592
+ // 7. 更新各 self-agent config.json 的 projects.list
593
+ try {
594
+ const { agents } = loadAllAgents();
595
+ for (const cfg of agents) {
596
+ if (!cfg.projects?.list)
597
+ continue;
598
+ let changed = false;
599
+ for (const [k, v] of Object.entries(cfg.projects.list)) {
600
+ if (v === oldAbs) {
601
+ cfg.projects.list[k] = newAbs;
602
+ changed = true;
603
+ }
604
+ }
605
+ if (changed) {
606
+ saveAgent(cfg);
607
+ result.evolclawConfigUpdated = true;
608
+ }
609
+ }
610
+ }
611
+ catch { /* agents not accessible */ }
612
+ return result;
613
+ }
@@ -17,26 +17,20 @@ export class AgentLoader {
17
17
  logger.debug(`Registered agent plugin: ${plugin.name}`);
18
18
  }
19
19
  /**
20
- * Iterate over all EvolAgents (DefaultAgent + each named agent in registry)
21
- * × all registered plugins. Each successful (agent, plugin) pair yields one
22
- * runner instance.
20
+ * Iterate over all EvolAgents in the registry × all registered plugins.
21
+ * Each successful (agent, plugin) pair yields one runner instance.
23
22
  */
24
- createAll(globalConfig, registry, callbacks) {
23
+ createAll(registry, callbacks) {
25
24
  const instances = [];
26
- const allAgents = [];
27
- const def = registry.get('[default]');
28
- if (def)
29
- allAgents.push(def);
30
- for (const a of registry.runnableAgents())
31
- allAgents.push(a);
25
+ const allAgents = registry.runnableAgents();
32
26
  for (const agent of allAgents) {
33
27
  for (const [pluginName, plugin] of this.plugins) {
34
- if (!plugin.isEnabled(globalConfig, agent)) {
28
+ if (!plugin.isEnabled(agent)) {
35
29
  logger.debug(`Plugin '${pluginName}' disabled for agent '${agent.name}', skipping`);
36
30
  continue;
37
31
  }
38
32
  try {
39
- const instance = plugin.createAgent(globalConfig, agent, callbacks);
33
+ const instance = plugin.createAgent(agent, callbacks);
40
34
  if (!instance) {
41
35
  logger.debug(`Plugin '${pluginName}' returned null for agent '${agent.name}', skipping`);
42
36
  continue;