evolclaw 2.8.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +21 -12
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +108 -46
  5. package/dist/agents/codex-runner.js +13 -14
  6. package/dist/agents/gemini-runner.js +15 -17
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/agents/resolve.js +134 -0
  9. package/dist/aun/aid/agentmd.js +186 -0
  10. package/dist/aun/aid/client.js +134 -0
  11. package/dist/aun/aid/identity.js +159 -0
  12. package/dist/aun/aid/index.js +3 -0
  13. package/dist/aun/aid/lifecycle-log.js +33 -0
  14. package/dist/aun/aid/types.js +1 -0
  15. package/dist/aun/aid/validation.js +21 -0
  16. package/dist/aun/msg/group.js +293 -0
  17. package/dist/aun/msg/index.js +4 -0
  18. package/dist/aun/msg/p2p.js +147 -0
  19. package/dist/aun/msg/payload-type.js +27 -0
  20. package/dist/aun/msg/upload.js +98 -0
  21. package/dist/aun/outbox.js +138 -0
  22. package/dist/aun/rpc/caller.js +42 -0
  23. package/dist/aun/rpc/connection.js +34 -0
  24. package/dist/aun/rpc/index.js +2 -0
  25. package/dist/aun/storage/download.js +29 -0
  26. package/dist/aun/storage/index.js +3 -0
  27. package/dist/aun/storage/manage.js +10 -0
  28. package/dist/aun/storage/upload.js +35 -0
  29. package/dist/channels/aun.js +1340 -349
  30. package/dist/channels/dingtalk.js +59 -5
  31. package/dist/channels/feishu.js +381 -32
  32. package/dist/channels/qqbot.js +68 -12
  33. package/dist/channels/wechat.js +63 -4
  34. package/dist/channels/wecom.js +59 -5
  35. package/dist/cli/agent.js +800 -0
  36. package/dist/cli/bench.js +1219 -0
  37. package/dist/cli/index.js +4513 -0
  38. package/dist/{utils → cli}/init-channel.js +211 -621
  39. package/dist/cli/init.js +178 -0
  40. package/dist/cli/link-rules.js +245 -0
  41. package/dist/cli/net-check.js +640 -0
  42. package/dist/cli/watch-msg.js +589 -0
  43. package/dist/config-store.js +645 -0
  44. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  45. package/dist/core/channel-loader.js +176 -12
  46. package/dist/core/command-handler.js +883 -848
  47. package/dist/core/evolagent-registry.js +191 -371
  48. package/dist/core/evolagent.js +202 -238
  49. package/dist/core/interaction-router.js +52 -5
  50. package/dist/core/message/im-renderer.js +486 -0
  51. package/dist/core/message/items-formatter.js +68 -0
  52. package/dist/core/message/message-bridge.js +109 -56
  53. package/dist/core/message/message-log.js +93 -0
  54. package/dist/core/message/message-processor.js +430 -212
  55. package/dist/core/message/message-queue.js +13 -6
  56. package/dist/core/permission.js +116 -11
  57. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  58. package/dist/core/session/session-fs-store.js +230 -0
  59. package/dist/core/session/session-manager.js +740 -777
  60. package/dist/core/session/session-mapper.js +87 -0
  61. package/dist/core/trigger/manager.js +122 -0
  62. package/dist/core/trigger/parser.js +128 -0
  63. package/dist/core/trigger/scheduler.js +224 -0
  64. package/dist/data/error-dict.json +118 -0
  65. package/dist/eck/baseagent-caps.js +18 -0
  66. package/dist/eck/detect.js +47 -0
  67. package/dist/eck/init.js +77 -0
  68. package/dist/eck/rules-loader.js +28 -0
  69. package/dist/index.js +560 -283
  70. package/dist/ipc.js +49 -0
  71. package/dist/net-check.js +640 -0
  72. package/dist/paths.js +73 -9
  73. package/dist/types.js +8 -2
  74. package/dist/utils/aid-lifecycle-log.js +33 -0
  75. package/dist/utils/atomic-write.js +89 -0
  76. package/dist/utils/channel-helpers.js +46 -0
  77. package/dist/utils/cross-platform.js +17 -26
  78. package/dist/utils/error-utils.js +10 -2
  79. package/dist/utils/instance-registry.js +434 -0
  80. package/dist/utils/log-writer.js +217 -0
  81. package/dist/utils/logger.js +34 -77
  82. package/dist/utils/media-cache.js +23 -0
  83. package/dist/utils/npm-ops.js +163 -0
  84. package/dist/utils/process-introspect.js +122 -0
  85. package/dist/utils/stats.js +192 -0
  86. package/dist/watch-msg.js +544 -0
  87. package/evolclaw-install-aun.md +127 -47
  88. package/kits/docs/GUIDE.md +20 -0
  89. package/kits/docs/INDEX.md +52 -0
  90. package/kits/docs/aun/CHEATSHEET.md +17 -0
  91. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  92. package/kits/docs/channels/aun.md +25 -0
  93. package/kits/docs/channels/feishu.md +27 -0
  94. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  95. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  96. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  97. package/kits/docs/eck_templates/runtime.template.md +19 -0
  98. package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
  99. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  100. package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
  101. package/kits/docs/evolclaw/self-summary.md +29 -0
  102. package/kits/docs/evolclaw/tools.md +25 -0
  103. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  104. package/kits/docs/identity/PATH_OPS.md +16 -0
  105. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  106. package/kits/docs/identity/identity-tools.md +26 -0
  107. package/kits/docs/path-registry.md +43 -0
  108. package/kits/eck_manifest.json +95 -0
  109. package/kits/rules/01-overview.md +120 -0
  110. package/kits/rules/02-navigation.md +75 -0
  111. package/kits/rules/03-identity.md +34 -0
  112. package/kits/rules/04-relation.md +49 -0
  113. package/kits/rules/05-venue.md +45 -0
  114. package/kits/rules/06-channel.md +43 -0
  115. package/kits/templates/system-fragments/baseagent.md +2 -0
  116. package/kits/templates/system-fragments/channel.md +10 -0
  117. package/kits/templates/system-fragments/identity.md +12 -0
  118. package/kits/templates/system-fragments/relation.md +9 -0
  119. package/kits/templates/system-fragments/runtime.md +19 -0
  120. package/kits/templates/system-fragments/venue.md +5 -0
  121. package/package.json +10 -6
  122. package/data/evolclaw.sample.json +0 -60
  123. package/dist/agents/templates.js +0 -122
  124. package/dist/channels/aun-ops.js +0 -275
  125. package/dist/cli.js +0 -2178
  126. package/dist/config.js +0 -591
  127. package/dist/core/agent-registry.js +0 -450
  128. package/dist/core/evolagent-schema.js +0 -72
  129. package/dist/core/message/stream-flusher.js +0 -238
  130. package/dist/core/message/thought-emitter.js +0 -162
  131. package/dist/core/reload-hooks.js +0 -87
  132. package/dist/prompts/templates.js +0 -122
  133. package/dist/templates/prompts.md +0 -104
  134. package/dist/templates/skills.md +0 -66
  135. package/dist/utils/channel-fingerprint.js +0 -59
  136. package/dist/utils/error-dict.js +0 -63
  137. package/dist/utils/format.js +0 -32
  138. package/dist/utils/init.js +0 -645
  139. package/dist/utils/migrate-project.js +0 -122
  140. package/dist/utils/reload-hooks.js +0 -87
  141. package/dist/utils/stats-collector.js +0 -99
  142. package/dist/utils/upgrade.js +0 -100
@@ -1,299 +1,246 @@
1
- import fs from 'fs';
2
1
  import path from 'path';
2
+ import fs from 'fs';
3
3
  import { logger } from '../utils/logger.js';
4
- import { validateDefaultChannelRef } from '../config.js';
5
- const VALID_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
6
- const VALID_CHANNEL_TYPES = new Set(['feishu', 'aun', 'wechat', 'wecom', 'dingtalk', 'qqbot']);
7
- const VALID_CHATMODES = new Set(['interactive', 'proactive']);
8
- export function validateEvolAgentConfig(raw) {
9
- const errors = [];
10
- if (!raw || typeof raw !== 'object') {
11
- return { valid: false, errors: ['config must be an object'] };
12
- }
13
- if (typeof raw.name !== 'string' || raw.name.trim() === '') {
14
- errors.push('name is required and must be a non-empty string');
15
- }
16
- if (raw.enabled !== undefined && typeof raw.enabled !== 'boolean') {
17
- errors.push('enabled must be a boolean if present');
18
- }
19
- if (!raw.agents || typeof raw.agents !== 'object') {
20
- errors.push('agents must be an object with exactly one baseagent block');
21
- }
22
- else {
23
- const keys = Object.keys(raw.agents).filter(k => VALID_BASEAGENTS.has(k));
24
- const unknownKeys = Object.keys(raw.agents).filter(k => !VALID_BASEAGENTS.has(k));
25
- if (unknownKeys.length > 0) {
26
- errors.push(`agents contains unknown baseagent keys: ${unknownKeys.join(', ')}`);
27
- }
28
- if (keys.length === 0) {
29
- errors.push('agents must contain exactly one of: claude | codex | gemini | hermes');
30
- }
31
- else if (keys.length > 1) {
32
- errors.push(`agents must contain exactly one baseagent (single baseagent only), got: ${keys.join(', ')}`);
33
- }
34
- }
35
- if (!raw.channels || typeof raw.channels !== 'object') {
36
- errors.push('channels is required');
37
- }
38
- else {
39
- const channelKeys = Object.keys(raw.channels).filter(k => k !== 'defaultChannel');
40
- if (channelKeys.length === 0) {
41
- errors.push('channels must contain at least one channel type');
42
- }
43
- for (const key of channelKeys) {
44
- if (!VALID_CHANNEL_TYPES.has(key)) {
45
- errors.push(`unknown channel type: ${key}`);
46
- }
47
- }
48
- // defaultChannel reference validation (same rules as evolclaw.json)
49
- let totalInstances = 0;
50
- for (const key of channelKeys) {
51
- const block = raw.channels[key];
52
- const insts = Array.isArray(block) ? block : (block ? [block] : []);
53
- totalInstances += insts.length;
54
- }
55
- const dc = raw.channels.defaultChannel;
56
- if (dc) {
57
- const err = validateDefaultChannelRef(dc, raw.channels);
58
- if (err)
59
- errors.push(err);
60
- }
61
- else if (totalInstances > 1) {
62
- errors.push('channels.defaultChannel is required when multiple channel instances are configured (use "type" or "type/instanceName")');
63
- }
64
- }
65
- if (!raw.projects || typeof raw.projects !== 'object') {
66
- errors.push('projects is required');
67
- }
68
- else {
69
- const p = raw.projects.defaultPath;
70
- if (typeof p !== 'string' || p === '') {
71
- errors.push('projects.defaultPath is required');
72
- }
73
- else if (!path.isAbsolute(p)) {
74
- errors.push(`projects.defaultPath must be absolute, got: ${p}`);
75
- }
76
- }
77
- if (raw.chatmode !== undefined) {
78
- if (typeof raw.chatmode !== 'object' || raw.chatmode === null) {
79
- errors.push('chatmode must be an object if present');
80
- }
81
- else {
82
- for (const key of ['private', 'group']) {
83
- const val = raw.chatmode[key];
84
- if (val !== undefined && !VALID_CHATMODES.has(val)) {
85
- errors.push(`chatmode.${key} must be 'interactive' or 'proactive'`);
86
- }
87
- }
88
- }
89
- }
90
- return { valid: errors.length === 0, errors };
91
- }
4
+ import { saveAgent } from '../config-store.js';
5
+ import { formatChannelKey, tryParseChannelKey } from './channel-loader.js';
6
+ import { agentPersonalDir } from '../paths.js';
7
+ /**
8
+ * EvolAgent —— 一个 self-agent 的运行时表示。
9
+ *
10
+ * 输入:MergedAgentConfig(defaults + per-agent 合并后的 effective 形态)。
11
+ * 持有该 agent channels Map、活跃状态、生命周期。
12
+ *
13
+ * 写入永远落到 agents/<aid>/config.json(通过 ConfigStore.saveAgent 走双 rename),
14
+ * 不再有 "DefaultAgent" 概念,不再有 globalWriter / configPath null 的路径。
15
+ */
92
16
  export class EvolAgent {
17
+ aid;
18
+ /** 兼容字段:name = aid。channel routing / log / IPC 都用 aid 作 agent 标识。 */
93
19
  name;
94
- configPath;
95
- config;
96
- isDefault;
20
+ /** in-memory effective config(含 defaults 合并结果);写盘时只回写 per-agent 部分。 */
21
+ merged;
22
+ /** per-agent 原始配置(写盘真相源) */
23
+ rawAgent;
97
24
  channels = new Map();
98
25
  activeSessions = 0;
99
26
  lastActivity;
100
27
  status;
101
28
  error;
102
- constructor(configPath, config, opts = {}) {
103
- this.configPath = configPath;
104
- this.config = config;
105
- this.name = config.name;
106
- this.isDefault = opts.isDefault === true;
107
- this.status = config.enabled === false ? 'disabled' : 'stopped';
29
+ triggerScheduler;
30
+ triggerManager;
31
+ constructor(rawAgent, merged) {
32
+ if (rawAgent.aid !== merged.aid) {
33
+ throw new Error(`EvolAgent: rawAgent.aid (${rawAgent.aid}) != merged.aid (${merged.aid})`);
34
+ }
35
+ this.rawAgent = rawAgent;
36
+ this.merged = merged;
37
+ this.aid = rawAgent.aid;
38
+ this.name = rawAgent.aid;
39
+ this.status = rawAgent.enabled === false ? 'disabled' : 'stopped';
40
+ }
41
+ /** 当前 effective config(合并后的) */
42
+ get config() {
43
+ return this.merged;
108
44
  }
109
45
  get baseagent() {
110
- const keys = Object.keys(this.config.agents);
111
- return keys[0] || 'claude';
46
+ return this.merged.active_baseagent || 'claude';
112
47
  }
113
48
  get model() {
114
- return this.config.agents[this.baseagent]?.model;
49
+ const ba = this.baseagent;
50
+ const block = this.merged.baseagents?.[ba];
51
+ return block?.model;
115
52
  }
116
53
  get effort() {
117
- return this.config.agents[this.baseagent]?.effort;
54
+ const ba = this.baseagent;
55
+ const block = this.merged.baseagents?.[ba];
56
+ if (ba === 'codex')
57
+ return block?.effort ?? block?.reasoning;
58
+ return block?.effort;
118
59
  }
119
60
  get projectPath() {
120
- return this.config.projects.defaultPath;
61
+ return this.merged.projects?.defaultPath || process.cwd();
121
62
  }
63
+ // ── Channels ──────────────────────────────────────────────────────────
122
64
  /**
123
- * Compute the effective channel-instance name (used as registry key, session.channel, etc).
124
- *
125
- * - DefaultAgent: rawName ?? type (preserves backward-compat with evolclaw.json)
126
- * - EvolAgent:
127
- * - rawName present → `${agent.name}-${type}-${rawName}`
128
- * - rawName absent → `${agent.name}-${type}`
129
- *
130
- * The agent-name prefix avoids collisions with DefaultAgent channels, e.g.
131
- * test-bot's aun → "test-bot-aun" instead of "aun".
65
+ * effective channel key:`<aid>#<type>#<name>`。AUN 实例一个 agent 只有一条;
66
+ * 其它类型靠 name 区分。
132
67
  */
133
68
  effectiveChannelName(type, rawName) {
134
- if (this.isDefault)
135
- return rawName ?? type;
136
- return rawName ? `${this.name}-${type}-${rawName}` : `${this.name}-${type}`;
69
+ return formatChannelKey({ aid: this.aid, type, name: rawName });
137
70
  }
138
71
  channelInstanceNames() {
139
- const names = [];
140
- for (const [type, raw] of Object.entries(this.config.channels || {})) {
141
- const instances = Array.isArray(raw) ? raw : [raw];
142
- for (const inst of instances) {
143
- if (!inst || typeof inst !== 'object')
144
- continue;
145
- names.push(this.effectiveChannelName(type, inst.name));
146
- }
147
- }
148
- return names;
72
+ // AUN channel 隐式存在(从 agent.aid 派生),不需要在 channels[] 里声明
73
+ const aunKey = this.effectiveChannelName('aun', 'main');
74
+ const others = this.merged.channels
75
+ .filter(c => c.type !== 'aun')
76
+ .map(c => this.effectiveChannelName(c.type, c.name));
77
+ return [aunKey, ...others];
78
+ }
79
+ /** 列出所有 channel 实例(含 effective key) */
80
+ listChannels() {
81
+ return this.merged.channels.map(inst => ({
82
+ key: this.effectiveChannelName(inst.type, inst.name),
83
+ instance: inst,
84
+ }));
149
85
  }
150
86
  /**
151
- * Locate a channel-instance config block within this agent's config by
152
- * matching the effective channel name (with agent prefix for EvolAgents).
153
- * Returns the raw mutable instance object, or `null` if not found.
87
+ * effective channel key 找到 instance(只读视图)。
88
+ * 找不到返回 null。
154
89
  */
155
- findChannelInstance(channelName) {
156
- const channels = this.config.channels || {};
157
- for (const [type, raw] of Object.entries(channels)) {
158
- if (type === 'defaultChannel')
159
- continue;
160
- const instances = Array.isArray(raw) ? raw : [raw];
161
- for (const inst of instances) {
162
- if (!inst || typeof inst !== 'object')
163
- continue;
164
- const effName = this.effectiveChannelName(type, inst.name);
165
- if (effName === channelName)
166
- return inst;
167
- }
168
- }
169
- return null;
170
- }
171
- /** Get owner of a specific channel instance owned by this agent. */
172
- getOwner(channelName) {
173
- const inst = this.findChannelInstance(channelName);
174
- return inst?.owner;
175
- }
176
- /** True when `userId` is the owner of `channelName`. */
177
- isOwner(channelName, userId) {
178
- return this.getOwner(channelName) === userId;
90
+ findChannelInstance(channelKey) {
91
+ return this.merged.channels.find(c => this.effectiveChannelName(c.type, c.name) === channelKey) ?? null;
179
92
  }
93
+ // ── Owner / Admin(per-channel-instance;AUN 走顶层 owners/admins)──────
180
94
  /**
181
- * True when `userId` is admin (or owner) of `channelName`.
182
- * Owner implicitly has admin rights.
95
+ * AUN channel 是隐式的,不在 channels[] 里——其 owner/admin 存于 EvolAgent 顶层
96
+ * `owners`/`admins`(config 加载时由 aunBlock.owner/admins 收集而来)。
183
97
  */
184
- isAdmin(channelName, userId) {
185
- if (this.isOwner(channelName, userId))
98
+ isAunChannelKey(channelKey) {
99
+ const parsed = tryParseChannelKey(channelKey);
100
+ return parsed?.type === 'aun' && parsed.aid === this.aid;
101
+ }
102
+ getOwner(channelKey) {
103
+ if (this.isAunChannelKey(channelKey)) {
104
+ return this.merged.owners?.[0];
105
+ }
106
+ const inst = this.findChannelInstance(channelKey);
107
+ return inst?.owners?.[0];
108
+ }
109
+ isOwner(channelKey, userId) {
110
+ if (this.isAunChannelKey(channelKey)) {
111
+ return this.merged.owners?.includes(userId) ?? false;
112
+ }
113
+ const inst = this.findChannelInstance(channelKey);
114
+ return inst?.owners?.includes(userId) ?? false;
115
+ }
116
+ isAdmin(channelKey, userId) {
117
+ if (this.isOwner(channelKey, userId))
186
118
  return true;
187
- const inst = this.findChannelInstance(channelName);
188
- const admins = inst?.admins || [];
189
- return admins.includes(userId);
119
+ if (this.isAunChannelKey(channelKey)) {
120
+ return this.merged.admins?.includes(userId) ?? false;
121
+ }
122
+ const inst = this.findChannelInstance(channelKey);
123
+ return inst?.admins?.includes(userId) ?? false;
190
124
  }
191
- /**
192
- * Set owner for a channel instance and persist to agent.json.
193
- * Throws when called on DefaultAgent (no configPath) — callers must use
194
- * the global config setter for default channels.
195
- */
196
- setOwner(channelName, userId) {
197
- const inst = this.findChannelInstance(channelName);
125
+ setOwner(channelKey, userId) {
126
+ // AUN:写到 rawAgent 顶层 owners(merged 也指向同一份引用)
127
+ if (this.isAunChannelKey(channelKey)) {
128
+ if (!this.rawAgent.owners)
129
+ this.rawAgent.owners = [];
130
+ if (!this.rawAgent.owners.includes(userId))
131
+ this.rawAgent.owners.push(userId);
132
+ // merged.owners 是从 rawAgent.owners 派生的拷贝;同步内存视图避免重新 merge
133
+ if (!this.merged.owners)
134
+ this.merged.owners = [];
135
+ if (!this.merged.owners.includes(userId))
136
+ this.merged.owners.push(userId);
137
+ this.persist();
138
+ return;
139
+ }
140
+ const inst = this.findRawChannelInstance(channelKey);
198
141
  if (!inst) {
199
- logger.warn(`[EvolAgent] setOwner: channel "${channelName}" not found in agent "${this.name}"`);
142
+ logger.warn(`[EvolAgent ${this.aid}] setOwner: channel "${channelKey}" not found`);
200
143
  return;
201
144
  }
202
- inst.owner = userId;
145
+ // 顶层 owners 是单值列表(首通信者即 owner);channel 实例 owners 也允许多值
146
+ if (!inst.owners)
147
+ inst.owners = [];
148
+ if (!inst.owners.includes(userId))
149
+ inst.owners.push(userId);
203
150
  this.persist();
204
151
  }
205
- /** Get showActivities mode for a channel instance owned by this agent. */
206
- getShowActivities(channelName) {
207
- const inst = this.findChannelInstance(channelName);
208
- return inst?.showActivities ?? 'all';
152
+ // ── ShowActivities ────────────────────────────────────────────────────
153
+ getShowActivities(_channelKey) {
154
+ return this.merged.show_activities ?? 'all';
209
155
  }
210
- /**
211
- * Set showActivities for a channel instance and persist to agent.json.
212
- * Throws when called on DefaultAgent — callers must use the global setter.
213
- */
214
- setShowActivities(channelName, mode) {
215
- const inst = this.findChannelInstance(channelName);
216
- if (!inst) {
217
- logger.warn(`[EvolAgent] setShowActivities: channel "${channelName}" not found in agent "${this.name}"`);
218
- return;
219
- }
220
- inst.showActivities = mode;
156
+ setShowActivities(_channelKey, mode) {
157
+ this.rawAgent.show_activities = mode;
158
+ this.merged.show_activities = mode;
221
159
  this.persist();
222
160
  }
223
- /**
224
- * Set this agent's baseagent.model and persist to agent.json.
225
- * Refuses for DefaultAgent. Writes to config.agents[baseagent].model.
226
- */
161
+ // ── Baseagent 字段写入 ────────────────────────────────────────────────
227
162
  setBaseagentModel(value) {
228
163
  const ba = this.baseagent;
229
- if (!this.config.agents[ba])
230
- this.config.agents[ba] = {};
231
- if (value === undefined) {
232
- delete this.config.agents[ba].model;
233
- }
234
- else {
235
- this.config.agents[ba].model = value;
236
- }
164
+ if (!this.rawAgent.baseagents)
165
+ this.rawAgent.baseagents = {};
166
+ const block = (this.rawAgent.baseagents[ba] ??= {});
167
+ if (value === undefined)
168
+ delete block.model;
169
+ else
170
+ block.model = value;
237
171
  this.persist();
238
172
  }
239
- /**
240
- * Get the agent's project list (defaults to a single entry derived from
241
- * projects.defaultPath when projects.list is empty/absent).
242
- */
173
+ setBaseagentEffort(value) {
174
+ const ba = this.baseagent;
175
+ if (!this.rawAgent.baseagents)
176
+ this.rawAgent.baseagents = {};
177
+ const block = (this.rawAgent.baseagents[ba] ??= {});
178
+ const fieldName = ba === 'codex' ? 'reasoning' : 'effort';
179
+ if (value === undefined)
180
+ delete block[fieldName];
181
+ else
182
+ block[fieldName] = value;
183
+ this.persist();
184
+ }
185
+ // ── Projects ──────────────────────────────────────────────────────────
243
186
  getProjects() {
244
- const list = this.config.projects?.list;
187
+ const list = this.merged.projects?.list;
245
188
  if (list && Object.keys(list).length > 0)
246
189
  return { ...list };
247
- const dp = this.config.projects?.defaultPath;
190
+ const dp = this.merged.projects?.defaultPath;
248
191
  if (dp)
249
192
  return { [path.basename(dp)]: dp };
250
193
  return {};
251
194
  }
252
- /**
253
- * Add (or update) a named project in this agent's projects.list and persist.
254
- * Throws for DefaultAgent (caller should write to evolclaw.json instead).
255
- */
256
195
  addProject(name, projectPath) {
257
- if (!this.config.projects)
258
- this.config.projects = { defaultPath: projectPath, list: {} };
259
- if (!this.config.projects.list)
260
- this.config.projects.list = {};
261
- this.config.projects.list[name] = projectPath;
196
+ if (!this.rawAgent.projects)
197
+ this.rawAgent.projects = { defaultPath: projectPath, list: {} };
198
+ if (!this.rawAgent.projects.list)
199
+ this.rawAgent.projects.list = {};
200
+ this.rawAgent.projects.list[name] = projectPath;
262
201
  this.persist();
263
202
  }
203
+ // ── Personal layer ────────────────────────────────────────────────────
204
+ _personaCache = undefined;
264
205
  /**
265
- * Set this agent's baseagent.effort and persist to agent.json.
266
- * For codex, the field is named `reasoning` (alias). Refuses for DefaultAgent.
206
+ * 读取 personal/persona.md 内容(缓存,首次调用时从磁盘读)。
207
+ * 文件不存在返回 null。
267
208
  */
268
- setBaseagentEffort(value) {
269
- const ba = this.baseagent;
270
- if (!this.config.agents[ba])
271
- this.config.agents[ba] = {};
272
- const fieldName = ba === 'codex' ? 'reasoning' : 'effort';
273
- if (value === undefined) {
274
- delete this.config.agents[ba][fieldName];
209
+ getPersona() {
210
+ if (this._personaCache !== undefined)
211
+ return this._personaCache;
212
+ const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
213
+ try {
214
+ this._personaCache = fs.readFileSync(personaPath, 'utf-8').trim() || null;
275
215
  }
276
- else {
277
- this.config.agents[ba][fieldName] = value;
216
+ catch {
217
+ this._personaCache = null;
278
218
  }
279
- this.persist();
219
+ return this._personaCache;
280
220
  }
281
221
  /**
282
- * Persist the in-memory config back to the agent.json file.
283
- * Refuses for DefaultAgent: it is built from evolclaw.json and has no
284
- * dedicated file — callers must route writes through the global config.
222
+ * 读取 personal/memory/working.md 内容(不缓存,每次会话开始时读)。
285
223
  */
286
- persist() {
287
- if (!this.configPath) {
288
- throw new Error('Cannot persist DefaultAgent config; use global config setters');
224
+ getWorkingMemory() {
225
+ const workingPath = path.join(agentPersonalDir(this.aid), 'memory', 'working.md');
226
+ try {
227
+ const content = fs.readFileSync(workingPath, 'utf-8').trim();
228
+ return content || null;
229
+ }
230
+ catch {
231
+ return null;
289
232
  }
290
- fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2) + '\n', 'utf-8');
291
233
  }
292
- getContext(channelName, chatType, globalChatmode) {
234
+ /** 清除 persona 缓存(reload 后重新读取) */
235
+ invalidatePersonaCache() {
236
+ this._personaCache = undefined;
237
+ }
238
+ // ── Context(喂给 message-processor / command-handler) ──────────────
239
+ getContext(_channelKey, chatType, globalChatmode) {
293
240
  const chatMode = this.resolveChatMode(chatType, globalChatmode);
294
241
  return {
295
242
  name: this.name,
296
- isOwned: !this.isDefault,
243
+ isOwned: true,
297
244
  baseagent: this.baseagent,
298
245
  model: this.model,
299
246
  effort: this.effort,
@@ -302,14 +249,31 @@ export class EvolAgent {
302
249
  };
303
250
  }
304
251
  resolveChatMode(chatType, globalChatmode) {
305
- const agentCm = this.config.chatmode;
306
252
  const key = chatType === 'group' ? 'group' : 'private';
307
- if (agentCm) {
308
- return (agentCm[key] || 'interactive');
309
- }
310
- if (globalChatmode) {
311
- return (globalChatmode[key] || 'interactive');
253
+ return (this.merged.chatmode?.[key]
254
+ ?? globalChatmode?.[key]
255
+ ?? (key === 'group' ? 'proactive' : 'interactive'));
256
+ }
257
+ // ── Reload 支持:替换 in-memory config 并复用 channels Map ───────────
258
+ /** 用新的 raw + merged 替换 in-memory 状态。channels 由调用方决定如何 reconcile。 */
259
+ swapConfig(rawAgent, merged) {
260
+ if (rawAgent.aid !== this.aid) {
261
+ throw new Error(`EvolAgent.swapConfig: aid mismatch (${rawAgent.aid} vs ${this.aid})`);
312
262
  }
313
- return 'interactive';
263
+ this.rawAgent = rawAgent;
264
+ this.merged = merged;
265
+ }
266
+ // ── 内部辅助 ─────────────────────────────────────────────────────────
267
+ /**
268
+ * 找 rawAgent.channels 里的可变实例,用于写入。
269
+ *
270
+ * merged.channels 是 deep clone 时 raw 跟 merged 的 channels 引用同一份(mergeForAgent
271
+ * 直接 `agent.channels` 透传),所以 raw 里的实例就等于 merged 里的实例。
272
+ */
273
+ findRawChannelInstance(channelKey) {
274
+ return this.rawAgent.channels.find(c => this.effectiveChannelName(c.type, c.name) === channelKey) ?? null;
275
+ }
276
+ persist() {
277
+ saveAgent(this.rawAgent);
314
278
  }
315
279
  }
@@ -2,7 +2,6 @@ import { logger } from '../utils/logger.js';
2
2
  export class InteractionRouter {
3
3
  handlers = new Map();
4
4
  register(id, sessionId, callback, opts) {
5
- // Clear any existing handler for this ID
6
5
  const existing = this.handlers.get(id);
7
6
  if (existing?.timer)
8
7
  clearTimeout(existing.timer);
@@ -14,7 +13,13 @@ export class InteractionRouter {
14
13
  opts.onTimeout?.();
15
14
  }, opts.timeoutMs);
16
15
  }
17
- this.handlers.set(id, { callback, timer, sessionId, messageId: opts?.messageId });
16
+ this.handlers.set(id, {
17
+ callback,
18
+ timer,
19
+ sessionId,
20
+ initiatorId: opts?.initiatorId,
21
+ fallbackCommand: opts?.fallbackCommand,
22
+ });
18
23
  }
19
24
  handle(response) {
20
25
  const handler = this.handlers.get(response.id);
@@ -25,7 +30,6 @@ export class InteractionRouter {
25
30
  this.handlers.delete(response.id);
26
31
  try {
27
32
  const result = handler.callback(response.action, response.values, response.operatorId);
28
- // Catch async callback errors to prevent unhandled rejections
29
33
  if (result && typeof result.catch === 'function') {
30
34
  result.catch((err) => {
31
35
  logger.error(`[InteractionRouter] Async callback error for ${response.id}:`, err);
@@ -62,7 +66,50 @@ export class InteractionRouter {
62
66
  }
63
67
  return ids;
64
68
  }
65
- getMessageId(id) {
66
- return this.handlers.get(id)?.messageId;
69
+ getInitiator(id) {
70
+ return this.handlers.get(id)?.initiatorId;
67
71
  }
72
+ findPendingByCommand(sessionId, command) {
73
+ for (const [id, handler] of this.handlers.entries()) {
74
+ if (handler.sessionId === sessionId && handler.fallbackCommand === command) {
75
+ return id;
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+ }
81
+ /** 把 CommandCard 渲染为文本提示,channel 不支持卡片时使用 */
82
+ export function renderCommandCardAsText(card) {
83
+ const lines = [card.title];
84
+ if (card.body)
85
+ lines.push(card.body);
86
+ lines.push('', '可用命令:');
87
+ for (const btn of card.buttons) {
88
+ const marker = btn.disabled ? '✓' : ' ';
89
+ lines.push(` ${marker} ${btn.command} ← ${btn.label}`);
90
+ }
91
+ return lines.join('\n');
92
+ }
93
+ /** 把 ActionInteraction 渲染为文本提示,channel 不支持卡片或卡片发送失败时使用 */
94
+ export function renderActionAsText(req) {
95
+ if (req.kind.kind !== 'action') {
96
+ throw new Error('[renderActionAsText] expected ActionInteraction, got ' + req.kind.kind);
97
+ }
98
+ const action = req.kind;
99
+ const fb = req.fallback;
100
+ const lines = [action.title];
101
+ if (action.body)
102
+ lines.push(action.body);
103
+ if (!fb) {
104
+ return lines.join('\n');
105
+ }
106
+ lines.push('', '回复:');
107
+ for (const btn of action.buttons) {
108
+ const arg = fb.buttonArgMap?.[btn.key] ?? btn.key;
109
+ lines.push(` /${fb.command} ${arg} ← ${btn.label}`);
110
+ }
111
+ if (fb.acceptFreeText && fb.freeTextHint) {
112
+ lines.push(` ${fb.freeTextHint}`);
113
+ }
114
+ return lines.join('\n');
68
115
  }