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
@@ -1,299 +1,251 @@
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
+ const inst = this.findChannelInstance(channelKey);
155
+ return inst?.showActivities ?? this.merged.show_activities ?? 'all';
209
156
  }
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);
157
+ setShowActivities(channelKey, mode) {
158
+ const inst = this.findRawChannelInstance(channelKey);
216
159
  if (!inst) {
217
- logger.warn(`[EvolAgent] setShowActivities: channel "${channelName}" not found in agent "${this.name}"`);
160
+ logger.warn(`[EvolAgent ${this.aid}] setShowActivities: channel "${channelKey}" not found`);
218
161
  return;
219
162
  }
220
163
  inst.showActivities = mode;
221
164
  this.persist();
222
165
  }
223
- /**
224
- * Set this agent's baseagent.model and persist to agent.json.
225
- * Refuses for DefaultAgent. Writes to config.agents[baseagent].model.
226
- */
166
+ // ── Baseagent 字段写入 ────────────────────────────────────────────────
227
167
  setBaseagentModel(value) {
228
168
  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
- }
169
+ if (!this.rawAgent.baseagents)
170
+ this.rawAgent.baseagents = {};
171
+ const block = (this.rawAgent.baseagents[ba] ??= {});
172
+ if (value === undefined)
173
+ delete block.model;
174
+ else
175
+ block.model = value;
237
176
  this.persist();
238
177
  }
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
- */
178
+ setBaseagentEffort(value) {
179
+ const ba = this.baseagent;
180
+ if (!this.rawAgent.baseagents)
181
+ this.rawAgent.baseagents = {};
182
+ const block = (this.rawAgent.baseagents[ba] ??= {});
183
+ const fieldName = ba === 'codex' ? 'reasoning' : 'effort';
184
+ if (value === undefined)
185
+ delete block[fieldName];
186
+ else
187
+ block[fieldName] = value;
188
+ this.persist();
189
+ }
190
+ // ── Projects ──────────────────────────────────────────────────────────
243
191
  getProjects() {
244
- const list = this.config.projects?.list;
192
+ const list = this.merged.projects?.list;
245
193
  if (list && Object.keys(list).length > 0)
246
194
  return { ...list };
247
- const dp = this.config.projects?.defaultPath;
195
+ const dp = this.merged.projects?.defaultPath;
248
196
  if (dp)
249
197
  return { [path.basename(dp)]: dp };
250
198
  return {};
251
199
  }
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
200
  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;
201
+ if (!this.rawAgent.projects)
202
+ this.rawAgent.projects = { defaultPath: projectPath, list: {} };
203
+ if (!this.rawAgent.projects.list)
204
+ this.rawAgent.projects.list = {};
205
+ this.rawAgent.projects.list[name] = projectPath;
262
206
  this.persist();
263
207
  }
208
+ // ── Personal layer ────────────────────────────────────────────────────
209
+ _personaCache = undefined;
264
210
  /**
265
- * Set this agent's baseagent.effort and persist to agent.json.
266
- * For codex, the field is named `reasoning` (alias). Refuses for DefaultAgent.
211
+ * 读取 personal/persona.md 内容(缓存,首次调用时从磁盘读)。
212
+ * 文件不存在返回 null。
267
213
  */
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];
214
+ getPersona() {
215
+ if (this._personaCache !== undefined)
216
+ return this._personaCache;
217
+ const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
218
+ try {
219
+ this._personaCache = fs.readFileSync(personaPath, 'utf-8').trim() || null;
275
220
  }
276
- else {
277
- this.config.agents[ba][fieldName] = value;
221
+ catch {
222
+ this._personaCache = null;
278
223
  }
279
- this.persist();
224
+ return this._personaCache;
280
225
  }
281
226
  /**
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.
227
+ * 读取 personal/memory/working.md 内容(不缓存,每次会话开始时读)。
285
228
  */
286
- persist() {
287
- if (!this.configPath) {
288
- throw new Error('Cannot persist DefaultAgent config; use global config setters');
229
+ getWorkingMemory() {
230
+ const workingPath = path.join(agentPersonalDir(this.aid), 'memory', 'working.md');
231
+ try {
232
+ const content = fs.readFileSync(workingPath, 'utf-8').trim();
233
+ return content || null;
234
+ }
235
+ catch {
236
+ return null;
289
237
  }
290
- fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2) + '\n', 'utf-8');
291
238
  }
292
- getContext(channelName, chatType, globalChatmode) {
239
+ /** 清除 persona 缓存(reload 后重新读取) */
240
+ invalidatePersonaCache() {
241
+ this._personaCache = undefined;
242
+ }
243
+ // ── Context(喂给 message-processor / command-handler) ──────────────
244
+ getContext(_channelKey, chatType, globalChatmode) {
293
245
  const chatMode = this.resolveChatMode(chatType, globalChatmode);
294
246
  return {
295
247
  name: this.name,
296
- isOwned: !this.isDefault,
248
+ isOwned: true,
297
249
  baseagent: this.baseagent,
298
250
  model: this.model,
299
251
  effort: this.effort,
@@ -302,14 +254,31 @@ export class EvolAgent {
302
254
  };
303
255
  }
304
256
  resolveChatMode(chatType, globalChatmode) {
305
- const agentCm = this.config.chatmode;
306
257
  const key = chatType === 'group' ? 'group' : 'private';
307
- if (agentCm) {
308
- return (agentCm[key] || 'interactive');
309
- }
310
- if (globalChatmode) {
311
- return (globalChatmode[key] || 'interactive');
258
+ return (this.merged.chatmode?.[key]
259
+ ?? globalChatmode?.[key]
260
+ ?? (key === 'group' ? 'proactive' : 'interactive'));
261
+ }
262
+ // ── Reload 支持:替换 in-memory config 并复用 channels Map ───────────
263
+ /** 用新的 raw + merged 替换 in-memory 状态。channels 由调用方决定如何 reconcile。 */
264
+ swapConfig(rawAgent, merged) {
265
+ if (rawAgent.aid !== this.aid) {
266
+ throw new Error(`EvolAgent.swapConfig: aid mismatch (${rawAgent.aid} vs ${this.aid})`);
312
267
  }
313
- return 'interactive';
268
+ this.rawAgent = rawAgent;
269
+ this.merged = merged;
270
+ }
271
+ // ── 内部辅助 ─────────────────────────────────────────────────────────
272
+ /**
273
+ * 找 rawAgent.channels 里的可变实例,用于写入。
274
+ *
275
+ * merged.channels 是 deep clone 时 raw 跟 merged 的 channels 引用同一份(mergeForAgent
276
+ * 直接 `agent.channels` 透传),所以 raw 里的实例就等于 merged 里的实例。
277
+ */
278
+ findRawChannelInstance(channelKey) {
279
+ return this.rawAgent.channels.find(c => this.effectiveChannelName(c.type, c.name) === channelKey) ?? null;
280
+ }
281
+ persist() {
282
+ saveAgent(this.rawAgent);
314
283
  }
315
284
  }
@@ -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
  }