evolclaw 3.2.0 → 3.4.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 (95) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -4
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -31
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1152 -140
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +58 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/outbox.js +14 -2
  11. package/dist/aun/storage/download.js +1 -1
  12. package/dist/aun/storage/upload.js +13 -1
  13. package/dist/channels/aun.js +869 -358
  14. package/dist/channels/dingtalk.js +77 -140
  15. package/dist/channels/feishu.js +125 -154
  16. package/dist/channels/qqbot.js +75 -138
  17. package/dist/channels/wechat.js +75 -136
  18. package/dist/channels/wecom.js +75 -138
  19. package/dist/cli/agent-command.js +591 -0
  20. package/dist/cli/agent.js +23 -8
  21. package/dist/cli/aun-commands.js +1444 -0
  22. package/dist/cli/ctl-command.js +78 -0
  23. package/dist/cli/daemon-commands.js +2707 -0
  24. package/dist/cli/index.js +23 -4905
  25. package/dist/cli/init.js +33 -6
  26. package/dist/cli/model.js +1 -1
  27. package/dist/cli/restart-monitor.js +539 -0
  28. package/dist/cli/stats.js +558 -0
  29. package/dist/cli/version.js +87 -0
  30. package/dist/cli/watch-logs.js +33 -0
  31. package/dist/cli/watch-msg.js +5 -2
  32. package/dist/config-store.js +12 -6
  33. package/dist/core/channel-loader.js +88 -83
  34. package/dist/core/command/command-handler.js +1189 -0
  35. package/dist/core/command/menu-handler.js +1478 -0
  36. package/dist/core/command/slash-gate.js +142 -0
  37. package/dist/core/command/slash-handler.js +2090 -0
  38. package/dist/core/evolagent-registry.js +82 -0
  39. package/dist/core/evolagent.js +17 -1
  40. package/dist/core/interaction-router.js +8 -0
  41. package/dist/core/message/command-handler-agent-control.js +63 -1
  42. package/dist/core/message/im-renderer.js +91 -51
  43. package/dist/core/message/items-formatter.js +9 -1
  44. package/dist/core/message/message-bridge.js +73 -24
  45. package/dist/core/message/message-log.js +1 -0
  46. package/dist/core/message/message-processor.js +432 -94
  47. package/dist/core/message/message-queue.js +70 -2
  48. package/dist/core/message/pending-hints.js +232 -0
  49. package/dist/core/model/model-catalog.js +1 -1
  50. package/dist/core/model/model-scope.js +2 -2
  51. package/dist/core/permission.js +25 -12
  52. package/dist/core/relation/peer-identity.js +16 -1
  53. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  54. package/dist/core/session/session-manager.js +86 -26
  55. package/dist/core/session/session-title.js +26 -0
  56. package/dist/core/stats/billing.js +151 -0
  57. package/dist/core/stats/budget.js +93 -0
  58. package/dist/core/stats/db.js +334 -0
  59. package/dist/core/stats/eck-vars.js +84 -0
  60. package/dist/core/stats/index.js +10 -0
  61. package/dist/core/stats/normalizer.js +78 -0
  62. package/dist/core/stats/query.js +760 -0
  63. package/dist/core/stats/writer.js +115 -0
  64. package/dist/core/trigger/manager.js +34 -0
  65. package/dist/core/trigger/parser.js +9 -3
  66. package/dist/core/trigger/scheduler.js +20 -17
  67. package/dist/data/error-dict.json +7 -0
  68. package/dist/{agents → eck}/manifest-engine.js +20 -1
  69. package/dist/{agents → eck}/message-renderer.js +24 -1
  70. package/dist/index.js +174 -9
  71. package/dist/ipc.js +116 -1
  72. package/dist/utils/cross-platform.js +58 -5
  73. package/dist/utils/ecweb-launch.js +49 -0
  74. package/dist/utils/ecweb-pair.js +20 -0
  75. package/dist/utils/error-utils.js +18 -5
  76. package/dist/utils/npm-ops.js +38 -8
  77. package/dist/utils/stats.js +77 -6
  78. package/kits/docs/evolclaw/INDEX.md +3 -1
  79. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  80. package/kits/docs/evolclaw/fs.md +131 -0
  81. package/kits/docs/evolclaw/group-fs.md +209 -0
  82. package/kits/docs/evolclaw/stats.md +70 -0
  83. package/kits/docs/venues/aun-group.md +29 -6
  84. package/kits/docs/venues/group.md +5 -4
  85. package/kits/eck_message_manifest.json +30 -3
  86. package/kits/rules/05-venue.md +1 -1
  87. package/kits/templates/message-fragments/inject-default.md +2 -0
  88. package/package.json +5 -6
  89. package/dist/agents/baseagent-normalize.js +0 -19
  90. package/dist/core/command-handler.js +0 -3876
  91. package/dist/core/relation/peer-key.js +0 -16
  92. package/dist/evolclaw-config.js +0 -11
  93. package/dist/utils/channel-helpers.js +0 -46
  94. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  95. /package/dist/{agents → eck}/kit-renderer.js +0 -0
@@ -12,6 +12,7 @@ const GREEN = isTTY ? '\x1b[32m' : '';
12
12
  const BLUE = isTTY ? '\x1b[34m' : '';
13
13
  const ORANGE = isTTY ? '\x1b[38;5;208m' : '';
14
14
  const MAGENTA = isTTY ? '\x1b[35m' : '';
15
+ const YELLOW = isTTY ? '\x1b[33m' : '';
15
16
  const BG_SEL = isTTY ? '\x1b[48;5;236m' : ''; // dark gray background for selected row
16
17
  // ==================== Helpers ====================
17
18
  function visualWidth(s) {
@@ -314,8 +315,10 @@ function renderMessagesPanel(state, width, height) {
314
315
  const encLabel = m.encrypt ? '密文' : '明文';
315
316
  const modeLabel = m.chatmode === 'proactive' ? '自主' : '响应';
316
317
  const metaTags = (m.encrypt != null || m.chatmode) ? `${MAGENTA}[${encLabel}|${modeLabel}]${RST}` : '';
318
+ // observer 插话:醒目标记,与对端真实消息区分
319
+ const injectTag = m.source === 'owner-inject' ? `${YELLOW}[插话]${RST}` : '';
317
320
  let typeTag = '';
318
- if (m.dir === 'out') {
321
+ if (m.dir === 'out' && m.source !== 'owner-inject') {
319
322
  const rawSource = m.source;
320
323
  // 4 种来源: daemon | ctl | msg | cli
321
324
  const source = (rawSource === 'ctl' || rawSource === 'msg' || rawSource === 'cli') ? rawSource : 'daemon';
@@ -326,7 +329,7 @@ function renderMessagesPanel(state, width, height) {
326
329
  const lenTag = `${DIM}${formatNumber(byteLen)}B${RST}`;
327
330
  const fromDisplay = isGroup && m.groupId && m.dir === 'in' ? m.groupId : m.from.split('.')[0];
328
331
  const toDisplay = isGroup && m.groupId && m.dir === 'out' ? m.groupId : m.to.split('.')[0];
329
- const header = `${DIM}${time}${RST} ${dir}${chatTag}${metaTags}${typeTag} ${ORANGE}${fromDisplay}${RST}${DIM}→${RST}${GREEN}${toDisplay}${RST} ${lenTag}`;
332
+ const header = `${DIM}${time}${RST} ${dir}${chatTag}${injectTag}${metaTags}${typeTag} ${ORANGE}${fromDisplay}${RST}${DIM}→${RST}${GREEN}${toDisplay}${RST} ${lenTag}`;
330
333
  const out = [padRight(header, msgWidth)];
331
334
  const rawContent = m.content.replace(/\n/g, ' ');
332
335
  const wrappedLines = wrapText(rawContent, contentLineWidth, maxContentLines);
@@ -17,11 +17,19 @@ import fs from 'fs';
17
17
  import path from 'path';
18
18
  import { resolvePaths, agentConfig as agentConfigPath, agentDir, } from './paths.js';
19
19
  import { atomicReadJson, atomicWriteJson } from './utils/atomic-write.js';
20
- import * as evolclawConfigModule from './evolclaw-config.js';
21
20
  import { checkAgentDir, isValidAid } from './aun/aid/validation.js';
22
21
  import { isValidChannelName } from './core/channel-loader.js';
23
22
  import { CONFIG_SCHEMA_VERSION } from './types.js';
24
23
  import { logger } from './utils/logger.js';
24
+ /** 读 {root}/evolclaw.json。文件不存在返回 {},不报错。 */
25
+ export function loadEvolclawConfig() {
26
+ const raw = atomicReadJson(resolvePaths().evolclawJson);
27
+ return raw ?? {};
28
+ }
29
+ /** 原子写入 {root}/evolclaw.json。调用方负责传完整对象(含要保留的字段)。 */
30
+ export function saveEvolclawConfig(value) {
31
+ atomicWriteJson(resolvePaths().evolclawJson, value);
32
+ }
25
33
  const SUPPORTED_CHANNEL_TYPES = new Set([
26
34
  'aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom',
27
35
  ]);
@@ -143,7 +151,6 @@ export function migrateProcessConfigIfNeeded() {
143
151
  const raw = atomicReadJson(oldPath);
144
152
  if (raw === null)
145
153
  return; // 不存在 → no-op
146
- const { loadEvolclawConfig, saveEvolclawConfig } = evolclawConfigModule;
147
154
  const evc = loadEvolclawConfig();
148
155
  // 仅当旧文件确实带 aun.encryptionSeed 字段时才搬(hasOwnProperty,保 null 语义)
149
156
  const didMigrateSeed = !!(raw.aun && Object.prototype.hasOwnProperty.call(raw.aun, 'encryptionSeed'));
@@ -601,10 +608,9 @@ export function migrateIdentitiesIfNeeded() {
601
608
  }
602
609
  // ── Project Migration ────────────────────────────────────────────────────────
603
610
  import os from 'os';
604
- /** 将绝对路径编码为 Claude Code 的目录名格式(/ \ . 替换为 -) */
605
- function encodePath(p) {
606
- return p.replace(/[/\\\.]/g, '-');
607
- }
611
+ // 复用与 Claude SDK 对齐的统一编码(resolve→realpath→NFC→非字母数字替换为 -),
612
+ // 避免本地实现规则不一致导致中文/非 ASCII 路径迁移时找不到 SDK 会话目录。
613
+ import { encodePath } from './utils/cross-platform.js';
608
614
  /** 查找最新的 ~/.codex/state_*.sqlite */
609
615
  function findCodexDb() {
610
616
  const codexHome = path.join(os.homedir(), '.codex');
@@ -6,11 +6,21 @@
6
6
  * The main service (index.ts) handles registration and message flow wiring.
7
7
  */
8
8
  import { logger } from '../utils/logger.js';
9
- /**
10
- * Channel Loader
11
- *
12
- * Manages channel plugin registration and lifecycle.
13
- */
9
+ /** Resolve showActivities for a single instance (instance overrides default). */
10
+ export function resolveShowActivities(inst) {
11
+ return inst.showActivities ?? 'all';
12
+ }
13
+ /** Standard showMiddleResult / showIdleMonitor policy function. */
14
+ export function showActivitiesPolicy(mode, chatType, identity) {
15
+ if (mode === 'none')
16
+ return false;
17
+ if (mode === 'dm-only')
18
+ return chatType === 'private';
19
+ if (mode === 'owner-dm-only')
20
+ return chatType === 'private' && identity === 'owner';
21
+ return true;
22
+ }
23
+ // ── ChannelLoader ──────────────────────────────────────────────────────────
14
24
  export class ChannelLoader {
15
25
  plugins = new Map();
16
26
  register(plugin) {
@@ -20,81 +30,70 @@ export class ChannelLoader {
20
30
  this.plugins.set(plugin.name, plugin);
21
31
  logger.debug(`Registered channel plugin: ${plugin.name}`);
22
32
  }
33
+ /** Look up a registered plugin by channel type (used by reload hooks). */
34
+ getPlugin(type) {
35
+ return this.plugins.get(type);
36
+ }
23
37
  /**
24
- * 新结构入口:从 EvolAgent channels[] 列表创建 channel 实例。
25
- *
26
- * 内部把 ChannelInstance[] 翻成各 plugin 期望的 dict 形态(`{ type: [instances...] }`),
27
- * 然后调用现有 plugin.createChannels / createChannel。
28
- *
29
- * 当所有 channel plugin 重写为直接吃 ChannelInstance[] 后,本方法可简化。
38
+ * Create all runtime channels for an agent directly from its config.channels[].
39
+ * AUN is always created implicitly from agent.aid (no explicit entry required).
30
40
  */
31
41
  async createForAgent(agent) {
32
- const rewrittenChannels = {};
33
- // AUN channel 从 agent.aid 隐式创建——不需要在 channels[] 里显式声明
42
+ const ctx = {
43
+ agentName: agent.aid,
44
+ defaultProjectPath: agent.config.projects?.defaultPath ?? process.cwd(),
45
+ enableRichContent: agent.config.enable_rich_content,
46
+ debug: agent.config.debug,
47
+ };
48
+ // Build the full list of config instances to create.
49
+ // AUN is synthesised from agent.aid; any explicit aun entry in channels[] is skipped.
34
50
  const aunEffName = agent.effectiveChannelName('aun', 'main');
35
- rewrittenChannels['aun'] = [{
36
- type: 'aun',
37
- name: aunEffName,
38
- aid: agent.aid,
39
- enabled: true,
40
- agentName: agent.aid,
41
- // agent 顶层 owners[0] 透传给 AUN channel.owner(用于首次连接发欢迎消息)
42
- owner: agent.config.owners?.[0],
43
- }];
44
- // 其它 channels(非 AUN)从 config.channels[] 取
51
+ const aunInst = {
52
+ type: 'aun',
53
+ name: aunEffName,
54
+ aid: agent.aid,
55
+ enabled: true,
56
+ owner: agent.config.owners?.[0],
57
+ };
58
+ const configInsts = [aunInst];
45
59
  for (const inst of agent.config.channels) {
46
60
  if (inst.type === 'aun')
47
- continue; // 跳过显式声明的 AUN(已隐式处理)
61
+ continue;
48
62
  const effName = agent.effectiveChannelName(inst.type, inst.name);
49
- const rewritten = { ...inst, name: effName, agentName: agent.aid };
50
- (rewrittenChannels[inst.type] ??= []).push(rewritten);
63
+ configInsts.push({ ...inst, name: effName });
51
64
  }
52
- // syntheticConfig 是老 Config schema(channel plugin 沿用旧接口),
53
- // 新 schema 字段命名为 snake_case,这里转 camelCase 透传。
54
- const syntheticConfig = {
55
- agents: agent.config.baseagents,
56
- channels: rewrittenChannels,
57
- projects: agent.config.projects,
58
- chatmode: agent.config.chatmode,
59
- debug: agent.config.debug,
60
- showActivities: agent.config.show_activities,
61
- flushDelay: agent.config.flush_delay,
62
- debounce: agent.config.debounce,
63
- enableRichContent: agent.config.enable_rich_content,
64
- };
65
- return this.createAll(syntheticConfig);
65
+ return this._buildInstances(configInsts, ctx);
66
66
  }
67
- async createAll(config) {
68
- const instances = [];
69
- for (const [name, plugin] of this.plugins) {
70
- if (!plugin.isEnabled(config)) {
71
- logger.info(`Channel '${name}' is disabled, skipping`);
67
+ /** Build runtime instances for a list of config instances + context. */
68
+ async _buildInstances(configInsts, ctx) {
69
+ const result = [];
70
+ for (const inst of configInsts) {
71
+ const plugin = this.plugins.get(inst.type);
72
+ if (!plugin) {
73
+ logger.debug(`No plugin for channel type '${inst.type}', skipping`);
72
74
  continue;
73
75
  }
74
76
  try {
75
- if (plugin.createChannels) {
76
- const channelInstances = await plugin.createChannels(config);
77
- instances.push(...channelInstances);
78
- logger.info(`✓ Channel '${name}' created ${channelInstances.length} instance(s)`);
77
+ const runtime = await plugin.createInstance(inst, ctx);
78
+ if (runtime) {
79
+ result.push(runtime);
80
+ logger.info(`✓ Channel '${inst.name}' (${inst.type}) created`);
79
81
  }
80
82
  else {
81
- const instance = await plugin.createChannel(config);
82
- instances.push(instance);
83
- logger.info(`✓ Channel '${name}' instance created`);
83
+ logger.info(`Channel '${inst.name}' (${inst.type}) disabled or invalid credentials, skipping`);
84
84
  }
85
85
  }
86
- catch (error) {
87
- logger.error(`✗ Failed to create channel '${name}':`, error);
86
+ catch (err) {
87
+ logger.error(`✗ Failed to create channel '${inst.name}' (${inst.type}):`, err);
88
88
  }
89
89
  }
90
- return instances;
90
+ return result;
91
91
  }
92
92
  async connectAll(instances, { concurrency = 3, intervalMs = 50 } = {}) {
93
93
  const connected = [];
94
94
  const failed = [];
95
95
  const inflight = new Set();
96
96
  for (const inst of instances) {
97
- // 等待并发数降到 concurrency 以下
98
97
  while (inflight.size >= concurrency) {
99
98
  await Promise.race(inflight);
100
99
  }
@@ -110,12 +109,10 @@ export class ChannelLoader {
110
109
  })();
111
110
  const tracked = task.then(() => { inflight.delete(tracked); });
112
111
  inflight.add(tracked);
113
- // 间隔发起,避免瞬间并发冲击网关
114
112
  if (intervalMs > 0) {
115
113
  await new Promise(r => setTimeout(r, intervalMs));
116
114
  }
117
115
  }
118
- // 等待所有剩余任务完成
119
116
  await Promise.allSettled(inflight);
120
117
  if (failed.length > 0) {
121
118
  logger.warn(`[connectAll] ${failed.length} channel(s) failed initial connect (will retry in background): ${failed.map(f => f.name).join(', ')}`);
@@ -123,7 +120,7 @@ export class ChannelLoader {
123
120
  return connected;
124
121
  }
125
122
  async disconnectAll(instances) {
126
- await Promise.allSettled(instances.map((inst) => inst.disconnect()));
123
+ await Promise.allSettled(instances.map(inst => inst.disconnect()));
127
124
  }
128
125
  }
129
126
  const SEP = '#';
@@ -156,7 +153,7 @@ export function isValidChannelName(name) {
156
153
  return typeof name === 'string' && name.length > 0 && !name.includes(SEP);
157
154
  }
158
155
  export function buildReloadHooks(deps) {
159
- const { channelLoader, channelInstances, registerChannelInstance, messageQueue } = deps;
156
+ const { channelLoader, channelInstances, registerChannelInstance, unregisterChannelInstance, messageQueue, onChannelStarted } = deps;
160
157
  const drainDelayMs = deps.drainDelayMs ?? 500;
161
158
  const drainTimeoutMs = deps.drainTimeoutMs ?? 30000;
162
159
  return {
@@ -189,6 +186,8 @@ export function buildReloadHooks(deps) {
189
186
  const idx = channelInstances.indexOf(inst);
190
187
  if (idx >= 0)
191
188
  channelInstances.splice(idx, 1);
189
+ // 从 core 各 map 注销,避免死实例残留在 /status「未归属渠道」、菜单路由和 adapter 查找里
190
+ unregisterChannelInstance?.(channelName);
192
191
  logger.info(`[Reload] Disconnected channel: ${channelName}`);
193
192
  }
194
193
  catch (e) {
@@ -197,35 +196,41 @@ export function buildReloadHooks(deps) {
197
196
  }
198
197
  },
199
198
  async startChannel(agent, channelName) {
200
- const channels = agent.config.channels;
201
- let channelType = null;
202
- for (const [type, raw] of Object.entries(channels)) {
203
- const instances = Array.isArray(raw) ? raw : [raw];
204
- for (const inst of instances) {
205
- const name = inst.name ?? type;
206
- if (name === channelName) {
207
- channelType = type;
208
- break;
209
- }
210
- }
211
- if (channelType)
212
- break;
213
- }
214
- if (!channelType) {
215
- const msg = `[Reload] Channel ${channelName} not found in agent ${agent.name} config`;
199
+ // The implicit AUN channel is synthesised from agent.aid (not in config.channels[]).
200
+ // Reconstruct it the same way createForAgent does before falling back to config scan.
201
+ const aid = agent.aid ?? agent.config?.aid;
202
+ const aunEffName = agent.effectiveChannelName?.('aun', 'main') ?? 'aun-main';
203
+ const isImplicitAun = channelName === aunEffName;
204
+ // Find config instance: implicit AUN gets a synthetic entry; others scan channels[].
205
+ const cfgInst = isImplicitAun
206
+ ? { type: 'aun', name: aunEffName, aid, enabled: true, owner: agent.config?.owners?.[0] }
207
+ : (() => {
208
+ const agentChannels = agent.config?.channels ?? [];
209
+ return agentChannels.find((i) => {
210
+ const effName = agent.effectiveChannelName?.(i.type, i.name) ?? i.name;
211
+ return effName === channelName;
212
+ }) ?? null;
213
+ })();
214
+ if (!cfgInst) {
215
+ const msg = `[Reload] Channel ${channelName} not found in agent config`;
216
216
  logger.error(msg);
217
217
  throw new Error(msg);
218
218
  }
219
- const partialConfig = {
220
- agents: agent.config.agents,
221
- channels: { [channelType]: channels[channelType] },
222
- projects: agent.config.projects,
219
+ const ctx = {
220
+ agentName: agent.aid ?? agent.config?.aid,
221
+ defaultProjectPath: agent.config?.projects?.defaultPath ?? process.cwd(),
222
+ enableRichContent: agent.config?.enable_rich_content,
223
+ debug: agent.config?.debug,
223
224
  };
224
- const newInstances = await channelLoader.createAll(partialConfig);
225
- const newInst = newInstances.find(i => i.adapter.channelName === channelName);
225
+ const plugin = channelLoader.getPlugin(cfgInst.type);
226
+ if (!plugin)
227
+ throw new Error(`[Reload] No plugin for channel type '${cfgInst.type}'`);
228
+ const effInst = { ...cfgInst, name: channelName };
229
+ const newInst = await plugin.createInstance(effInst, ctx);
226
230
  if (!newInst)
227
- throw new Error(`[Reload] Failed to create instance ${channelName}`);
231
+ throw new Error(`[Reload] createInstance returned null for ${channelName}`);
228
232
  registerChannelInstance(newInst);
233
+ onChannelStarted?.(newInst);
229
234
  await newInst.connect();
230
235
  channelInstances.push(newInst);
231
236
  logger.info(`[Reload] Started channel: ${channelName}`);