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
package/dist/cli/init.js CHANGED
@@ -4,10 +4,9 @@ import readline from 'readline';
4
4
  import { resolvePaths, ensureDataDirs } from '../paths.js';
5
5
  import { commandExists } from '../utils/cross-platform.js';
6
6
  import { scanInstances } from '../utils/instance-registry.js';
7
- import { saveDefaultsSafe, loadAllAgents, migrateProcessConfigIfNeeded } from '../config-store.js';
8
- import { loadEvolclawConfig, saveEvolclawConfig } from '../evolclaw-config.js';
7
+ import { saveDefaultsSafe, loadAllAgents, migrateProcessConfigIfNeeded, loadEvolclawConfig, saveEvolclawConfig } from '../config-store.js';
9
8
  import { generateControlAid } from '../aun/aid/control-aid.js';
10
- import { isCodexSdkAvailable } from '../agents/codex-runner.js';
9
+ import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
11
10
  // ==================== Helpers ====================
12
11
  function ask(rl, question) {
13
12
  return new Promise(resolve => rl.question(question, resolve));
@@ -15,7 +14,7 @@ function ask(rl, question) {
15
14
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
16
15
  function isBaseagentAvailable(baseagent) {
17
16
  if (baseagent === 'codex')
18
- return isCodexSdkAvailable();
17
+ return isCodexAppServerAvailable();
19
18
  return commandExists(baseagent);
20
19
  }
21
20
  function detectAvailable() {
@@ -80,7 +79,7 @@ export async function cmdInit(options) {
80
79
  console.log('❌ 未检测到可用 baseagent。请安装至少一款:');
81
80
  console.log(' - claude CLI');
82
81
  console.log(' - gemini CLI');
83
- console.log(' - optional dependency @openai/codex-sdk');
82
+ console.log(' - codex CLI with app-server');
84
83
  console.log('\n安装后重新运行 evolclaw init');
85
84
  return;
86
85
  }
@@ -104,7 +103,10 @@ export async function cmdInit(options) {
104
103
  return; // 硬错误:不落 tail
105
104
  }
106
105
  if (!available.includes(options.baseagent)) {
107
- console.log(`❌ ${options.baseagent} 当前环境不可用(可用: ${available.join('/')})`);
106
+ const reason = options.baseagent === 'codex'
107
+ ? getCodexAppServerAvailability().reason
108
+ : undefined;
109
+ console.log(`❌ ${options.baseagent} 当前环境不可用${reason ? `:${reason}` : `(可用: ${available.join('/')})`}`);
108
110
  return; // 硬错误:不落 tail
109
111
  }
110
112
  chosen = options.baseagent;
@@ -251,6 +253,31 @@ export async function initTail() {
251
253
  catch { /* ignore */ }
252
254
  }
253
255
  }
256
+ // ECWeb:交互式 + 未配置时询问是否启用
257
+ if (process.stdin.isTTY) {
258
+ const evcEcweb = loadEvolclawConfig();
259
+ if (evcEcweb.ecweb?.enabled === undefined) {
260
+ const rlEcweb = readline.createInterface({ input: process.stdin, output: process.stdout });
261
+ try {
262
+ const ans = (await ask(rlEcweb, '\n是否在 evolclaw start 时自动启动 ECWeb 控制台?[y/N] ')).trim().toLowerCase();
263
+ if (ans === 'y' || ans === 'yes') {
264
+ saveEvolclawConfig({ ...loadEvolclawConfig(), ecweb: { enabled: true } });
265
+ console.log(' ✓ 已启用 ECWeb(evolclaw start 将自动在后台启动)');
266
+ console.log(' 提示:首次访问运行 ec watch web 查看配对码和 URL');
267
+ }
268
+ else {
269
+ saveEvolclawConfig({ ...loadEvolclawConfig(), ecweb: { enabled: false } });
270
+ console.log(' 已跳过(可日后运行 ec watch web 手动启动,或编辑 evolclaw.json)');
271
+ }
272
+ }
273
+ finally {
274
+ try {
275
+ rlEcweb.close();
276
+ }
277
+ catch { }
278
+ }
279
+ }
280
+ }
254
281
  }
255
282
  /**
256
283
  * Present instance selection menu when existing instances are found.
package/dist/cli/model.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
14
14
  import { ModelScopeError, normalizePeer, determineScope, activeBaseagent, readScope, writeScope, clearScope, resolveEffectiveModel, } from '../core/model/model-scope.js';
15
15
  import { loadDefaults, loadAgent } from '../config-store.js';
16
- import { resolveAnthropicConfig } from '../agents/resolve.js';
16
+ import { resolveAnthropicConfig } from '../agents/baseagent.js';
17
17
  import { getCatalog, getModelInfo } from '../core/model/model-catalog.js';
18
18
  const ALL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
19
19
  const SCOPE_LABEL = {
@@ -0,0 +1,539 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawn, execFile } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { resolvePaths, getPackageRoot } from '../paths.js';
6
+ import { loadAllAgents } from '../config-store.js';
7
+ import * as platform from '../utils/cross-platform.js';
8
+ import { EventBus } from '../core/event-bus.js';
9
+ import { tryUpgrade, tryUpgradeAunSdk, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-ops.js';
10
+ import { resolveAunCoreSdkPkg, AUN_CORE_SDK_PKG } from '../aun/aid/client.js';
11
+ import { scanInstances, cleanupInstances, writeRestartMonitor, removeRestartMonitor, isRestartMonitorWinner } from '../utils/instance-registry.js';
12
+ const execFileAsync = promisify(execFile);
13
+ // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
14
+ function cleanEnv() {
15
+ for (const key of [
16
+ 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT',
17
+ 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
18
+ 'CLAUDE_CONFIG_DIR',
19
+ ]) {
20
+ delete process.env[key];
21
+ }
22
+ }
23
+ export async function cmdRestartMonitor() {
24
+ const p = resolvePaths();
25
+ const restartLog = path.join(p.logs, 'restart.log');
26
+ const MAX_HEAL_ATTEMPTS = 3;
27
+ const READY_TIMEOUT = 30000; // 30s(AUN sidecar 10s + Feishu 连接 12s)
28
+ const HEAL_TIMEOUT = 30 * 60 * 1000; // 30 分钟,让 claude 自然结束
29
+ const eventBus = new EventBus();
30
+ const log = (msg) => {
31
+ const _d = new Date();
32
+ const _p = (n) => String(n).padStart(2, '0');
33
+ const ts = `${_d.getFullYear()}-${_p(_d.getMonth() + 1)}-${_p(_d.getDate())} ${_p(_d.getHours())}:${_p(_d.getMinutes())}:${_p(_d.getSeconds())}`;
34
+ const line = `[${ts}] ${msg}\n`;
35
+ fs.appendFileSync(restartLog, line);
36
+ };
37
+ // 单实例保护:pre-check + post-write 自检(同 main 进程)
38
+ {
39
+ const pre = scanInstances();
40
+ const aliveOthers = pre.restartMonitors.filter(m => m.alive && m.record.pid !== process.pid);
41
+ if (aliveOthers.length > 0) {
42
+ log(`Another restart-monitor already running (PID: ${aliveOthers.map(m => m.record.pid).join(', ')}), exiting`);
43
+ process.exit(0);
44
+ }
45
+ }
46
+ // 立即登记自己;exit 路径上自动清理 record
47
+ writeRestartMonitor();
48
+ process.on('exit', () => removeRestartMonitor());
49
+ // post-write 自检:(startedAt, pid) 选最早赢家
50
+ {
51
+ const verdict = isRestartMonitorWinner();
52
+ if (!verdict.winner) {
53
+ log(`Lost restart-monitor election to PID ${verdict.conflictingPid}, yielding`);
54
+ removeRestartMonitor();
55
+ process.exit(0);
56
+ }
57
+ }
58
+ /** 检查服务是否已经在运行(ready signal 存在 + 至少一个活 main) */
59
+ const isServiceAlive = () => {
60
+ if (!fs.existsSync(p.readySignal))
61
+ return false;
62
+ const s = scanInstances();
63
+ return s.mains.some(m => m.alive);
64
+ };
65
+ log('Restart monitor started');
66
+ // 读取 restart-pending.json 用于后续通知
67
+ const pendingFile = path.join(p.dataDir, 'restart-pending.json');
68
+ let pendingInfo = null;
69
+ try {
70
+ if (fs.existsSync(pendingFile)) {
71
+ pendingInfo = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
72
+ }
73
+ }
74
+ catch { }
75
+ // 等待所有活 main 进程退出(可能不止一个)
76
+ const oldStatus = scanInstances();
77
+ const aliveMains = oldStatus.mains.filter(m => m.alive);
78
+ if (aliveMains.length > 0) {
79
+ const oldPids = aliveMains.map(m => m.record.pid);
80
+ log(`Monitoring ${oldPids.length} main process(es): ${oldPids.join(', ')}`);
81
+ // 先并行 SIGTERM 通知所有活 main
82
+ for (const pid of oldPids) {
83
+ try {
84
+ platform.killProcess(pid, false);
85
+ }
86
+ catch { }
87
+ }
88
+ await Promise.all(oldPids.map(oldPid => new Promise((resolve) => {
89
+ let waited = 0;
90
+ const interval = setInterval(() => {
91
+ waited++;
92
+ if (!platform.isProcessRunning(oldPid)) {
93
+ clearInterval(interval);
94
+ log(`Process ${oldPid} has exited`);
95
+ resolve();
96
+ return;
97
+ }
98
+ if (waited >= 30) {
99
+ clearInterval(interval);
100
+ log(`ERROR: Process ${oldPid} still running after 30s, force killing`);
101
+ platform.killProcess(oldPid, true);
102
+ resolve();
103
+ }
104
+ }, 1000);
105
+ })));
106
+ await sleep(3000);
107
+ cleanupInstances();
108
+ }
109
+ // 版本检查与自动升级
110
+ log('Checking for updates...');
111
+ const upgrade = await tryUpgrade();
112
+ switch (upgrade.status) {
113
+ case 'upgraded':
114
+ log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
115
+ await notifyChannel(p, pendingInfo, `📦 已升级 ${upgrade.from} → ${upgrade.to}`, log);
116
+ break;
117
+ case 'no-update':
118
+ log(`Already up to date (${upgrade.from})`);
119
+ break;
120
+ case 'skipped':
121
+ log(upgrade.error
122
+ ? 'Skipped upgrade (network unavailable)'
123
+ : 'Skipped upgrade check (dev mode)');
124
+ break;
125
+ case 'failed':
126
+ log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}): ${upgrade.error}`);
127
+ await notifyChannel(p, pendingInfo, `⚠️ 升级失败,使用当前版本继续`, log);
128
+ break;
129
+ }
130
+ // AUN SDK 版本检查与升级
131
+ const aunUpgrade = await tryUpgradeAunSdk(resolveAunCoreSdkPkg, AUN_CORE_SDK_PKG);
132
+ switch (aunUpgrade.status) {
133
+ case 'upgraded':
134
+ log(`✅ AUN SDK upgraded: ${aunUpgrade.from} → ${aunUpgrade.to}`);
135
+ await notifyChannel(p, pendingInfo, `📦 AUN SDK 已升级 ${aunUpgrade.from} → ${aunUpgrade.to}`, log);
136
+ break;
137
+ case 'no-update':
138
+ break;
139
+ case 'failed':
140
+ log(`⚠ AUN SDK upgrade failed (${aunUpgrade.from} → ${aunUpgrade.to}): ${aunUpgrade.error}`);
141
+ break;
142
+ }
143
+ // evolclaw-web 版本检查与升级(已安装才检查)
144
+ const ecwebUpgrade = await tryUpgradeGlobalPkg(() => resolveGlobalPkg('evolclaw-web'), 'evolclaw-web');
145
+ switch (ecwebUpgrade.status) {
146
+ case 'upgraded':
147
+ log(`✅ evolclaw-web upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
148
+ await notifyChannel(p, pendingInfo, `📦 evolclaw-web 已升级 ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`, log);
149
+ break;
150
+ case 'no-update':
151
+ break;
152
+ case 'failed':
153
+ log(`⚠ evolclaw-web upgrade failed (${ecwebUpgrade.from} → ${ecwebUpgrade.to}): ${ecwebUpgrade.error}`);
154
+ break;
155
+ }
156
+ // 启动并检测 ready signal
157
+ let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
158
+ if (started) {
159
+ log('✓ Service restarted successfully');
160
+ archiveSelfHealLog(p, log);
161
+ // 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
162
+ process.exit(0);
163
+ }
164
+ // 启动失败 — 测试环境下跳过 self-heal(避免 claude -p 污染会话列表、误杀生产进程)
165
+ if (p.root.startsWith('/tmp/') || process.env.EVOLCLAW_TEST === '1') {
166
+ log('❌ Service failed to start (test environment detected, skipping self-heal)');
167
+ await notifyChannel(p, pendingInfo, '❌ 服务启动失败(测试环境,已跳过自动修复)', log);
168
+ cleanupPendingFile(pendingFile, log);
169
+ process.exit(1);
170
+ }
171
+ // 启动失败,进入 self-heal 循环
172
+ log('❌ Service failed to start, entering self-heal loop');
173
+ eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
174
+ await notifyChannel(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
175
+ for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
176
+ // 前置检查:服务可能已被上一轮 claude 修复并启动
177
+ if (isServiceAlive()) {
178
+ log(`✓ Service already running before attempt ${attempt}, skipping`);
179
+ await sendHealSummary(p, pendingInfo, attempt - 1, log);
180
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt - 1 });
181
+ archiveSelfHealLog(p, log);
182
+ cleanupPendingFile(pendingFile, log);
183
+ process.exit(0);
184
+ }
185
+ log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
186
+ eventBus.publish({ type: 'self-heal:attempt', attemptNumber: attempt, maxAttempts: MAX_HEAL_ATTEMPTS });
187
+ await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
188
+ const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
189
+ // 后置检查:不管 invokeClaude 返回什么,都检查服务实际状态
190
+ if (isServiceAlive()) {
191
+ log(`✓ Service is running after attempt ${attempt}`);
192
+ await sendHealSummary(p, pendingInfo, attempt, log);
193
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
194
+ archiveSelfHealLog(p, log);
195
+ cleanupPendingFile(pendingFile, log);
196
+ process.exit(0);
197
+ }
198
+ if (!healed) {
199
+ log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
200
+ continue;
201
+ }
202
+ // claude 正常完成但服务没自动启动,尝试 spawn
203
+ started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
204
+ if (started) {
205
+ log(`✓ Self-heal succeeded on attempt ${attempt}`);
206
+ await sendHealSummary(p, pendingInfo, attempt, log);
207
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
208
+ archiveSelfHealLog(p, log);
209
+ cleanupPendingFile(pendingFile, log);
210
+ process.exit(0);
211
+ }
212
+ log(`Attempt ${attempt}: still failing after fix`);
213
+ }
214
+ // 全部失败 — 最后再检查一次
215
+ if (isServiceAlive()) {
216
+ log('✓ Service recovered during final check');
217
+ await sendHealSummary(p, pendingInfo, MAX_HEAL_ATTEMPTS, log);
218
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: MAX_HEAL_ATTEMPTS });
219
+ archiveSelfHealLog(p, log);
220
+ cleanupPendingFile(pendingFile, log);
221
+ process.exit(0);
222
+ }
223
+ log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
224
+ eventBus.publish({ type: 'self-heal:completed', success: false, attempts: MAX_HEAL_ATTEMPTS });
225
+ await notifyChannel(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
226
+ cleanupPendingFile(pendingFile, log);
227
+ process.exit(1);
228
+ }
229
+ /**
230
+ * 发送 self-heal 修复成功小结(从 self-heal.md 提取摘要)
231
+ */
232
+ async function sendHealSummary(p, pendingInfo, attempts, log) {
233
+ let summary = `✅ 自动修复成功(第 ${attempts || 1} 次尝试)`;
234
+ try {
235
+ if (fs.existsSync(p.selfHealLog)) {
236
+ const content = fs.readFileSync(p.selfHealLog, 'utf-8');
237
+ // 提取最后一个 ## 章节的要点
238
+ const sections = content.split(/^## /m).filter(Boolean);
239
+ const last = sections[sections.length - 1];
240
+ if (last) {
241
+ const lines = last.split('\n').filter(l => l.startsWith('- ')).map(l => l.trim());
242
+ if (lines.length > 0) {
243
+ summary += '\n' + lines.join('\n');
244
+ }
245
+ }
246
+ }
247
+ }
248
+ catch { }
249
+ summary += '\n\n⚠️ 修复前进行中的任务已中断,如需继续请重新发送。';
250
+ await notifyChannel(p, pendingInfo, summary, log);
251
+ }
252
+ function sleep(ms) {
253
+ return new Promise(resolve => setTimeout(resolve, ms));
254
+ }
255
+ function cleanupPendingFile(filePath, log) {
256
+ try {
257
+ if (fs.existsSync(filePath)) {
258
+ fs.unlinkSync(filePath);
259
+ log('Cleaned up restart-pending.json');
260
+ }
261
+ }
262
+ catch { }
263
+ }
264
+ /**
265
+ * 启动新进程并等待 ready.signal
266
+ */
267
+ async function spawnAndWaitReady(p, log, timeout) {
268
+ // 删除旧的 ready signal
269
+ try {
270
+ fs.unlinkSync(p.readySignal);
271
+ }
272
+ catch { }
273
+ // 清理残留 instance 文件和进程
274
+ cleanupInstances();
275
+ cleanEnv();
276
+ const stdoutLog = path.join(p.logs, 'stdout.log');
277
+ const out = fs.openSync(stdoutLog, 'a');
278
+ const err = fs.openSync(stdoutLog, 'a');
279
+ const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
280
+ const child = spawn('node', ['--no-warnings=ExperimentalWarning', appMain], {
281
+ detached: true,
282
+ stdio: ['ignore', out, err],
283
+ windowsHide: true,
284
+ env: {
285
+ ...process.env,
286
+ EVOLCLAW_HOME: p.root,
287
+ EVOLCLAW_LAUNCHED_BY: 'restart-monitor',
288
+ LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
289
+ MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
290
+ EVENT_LOG: process.env.EVENT_LOG || 'true',
291
+ }
292
+ });
293
+ const childPid = child.pid;
294
+ child.unref();
295
+ log(`Spawned new process PID: ${childPid}, waiting for ready signal...`);
296
+ // 轮询等待 ready.signal 出现
297
+ const start = Date.now();
298
+ while (Date.now() - start < timeout) {
299
+ await sleep(500);
300
+ // 进程已退出则提前失败
301
+ if (!platform.isProcessRunning(childPid)) {
302
+ log('Process exited before ready signal');
303
+ return false;
304
+ }
305
+ if (fs.existsSync(p.readySignal)) {
306
+ log('Ready signal detected');
307
+ return true;
308
+ }
309
+ }
310
+ log(`Ready signal not received within ${timeout / 1000}s`);
311
+ // 超时后杀掉进程
312
+ if (platform.isProcessRunning(childPid)) {
313
+ platform.killProcess(childPid);
314
+ }
315
+ cleanupInstances();
316
+ return false;
317
+ }
318
+ /**
319
+ * 调用 claude CLI 进行自动修复
320
+ */
321
+ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
322
+ const projectDir = getPackageRoot();
323
+ const selfHealLog = p.selfHealLog;
324
+ const stdoutLog = path.join(p.logs, 'stdout.log');
325
+ const selfHealExists = fs.existsSync(selfHealLog) ? '存在,请先阅读之前的修复记录' : '不存在(首次修复)';
326
+ const prompt = `EvolClaw 服务启动失败,需要你诊断并修复。这是第 ${attempt}/${maxAttempts} 次自动修复尝试。
327
+
328
+ 关键信息:
329
+ - 项目目录:${projectDir}
330
+ - EVOLCLAW_HOME:${p.root}
331
+ - 错误日志:${stdoutLog}
332
+ - 主日志:${p.logs}/evolclaw-*.log(按小时切片,读最新的那个,包含 config 校验失败等关键错误)
333
+ - 修复记录:${selfHealLog}(${selfHealExists})
334
+
335
+ ⚠️ 重要诊断技巧:
336
+ - stdout.log 可能是空的(进程秒退时 logger 输出不会到 stdout),一定要同时读 evolclaw-*.log 最新文件
337
+ - 必须实际运行进程来复现错误:\`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\`,观察输出和退出码
338
+ - 检查是否有旧进程仍在运行:\`ps aux | grep 'node.*dist/index.js' | grep -v grep\`,旧进程可能占用端口或锁文件
339
+ - 可以运行 \`EVOLCLAW_HOME=${p.root} node dist/cli/index.js diagnose\` 快速检查配置和数据库
340
+ - 如果进程无任何输出就 exit(1),说明是 process.exit(1) 被显式调用,搜索源码中所有 process.exit(1) 位置
341
+ - 配置文件使用双 rename 原子写(foo.json → foo.json_ → foo.json__),崩溃时可从 foo.json_ 恢复
342
+
343
+ 请执行以下步骤:
344
+ 1. 读取 ${stdoutLog} 和 ${p.logs}/evolclaw-*.log(最新文件)的最后 50 行
345
+ 2. 运行 \`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\` 复现错误(设置 10 秒超时)
346
+ 3. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
347
+ 4. 根据实际复现的错误修复代码
348
+ 5. 执行 npm run build 确认编译通过
349
+ 6. 验证修复:启动服务确认 ready.signal 已写入,然后执行 \`EVOLCLAW_HOME=${p.root} node dist/cli/index.js stop\` 优雅停止(restart-monitor 会负责最终启动)
350
+ 7. 将本次修复内容追加到 ${selfHealLog},格式:
351
+ ## 第 ${attempt} 次修复 - {时间}
352
+ - 错误原因:...
353
+ - 修复方案:...
354
+ - 修改文件:...
355
+
356
+ 注意:只修复导致启动失败的问题,不要做额外的重构或优化。`;
357
+ try {
358
+ log(`Invoking claude CLI (attempt ${attempt}, timeout ${timeout / 60000}min)...`);
359
+ const { stdout, stderr } = await execFileAsync('claude', [
360
+ '-p', prompt,
361
+ '--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
362
+ '--output-format', 'text',
363
+ '--no-session-persistence',
364
+ ], {
365
+ cwd: projectDir,
366
+ timeout,
367
+ env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'cli' },
368
+ maxBuffer: 10 * 1024 * 1024,
369
+ });
370
+ if (stdout)
371
+ log(`Claude output: ${stdout.slice(0, 500)}`);
372
+ if (stderr)
373
+ log(`Claude stderr: ${stderr.slice(0, 500)}`);
374
+ log(`Claude CLI completed (attempt ${attempt})`);
375
+ return true;
376
+ }
377
+ catch (error) {
378
+ if (error.killed) {
379
+ log(`Claude CLI timeout after ${timeout / 60000}min (attempt ${attempt})`);
380
+ }
381
+ else {
382
+ log(`Claude CLI error: exit code ${error.code ?? 'unknown'} (attempt ${attempt})`);
383
+ }
384
+ if (error.stdout)
385
+ log(`Claude output: ${String(error.stdout).slice(0, 500)}`);
386
+ if (error.stderr) {
387
+ const stderr = String(error.stderr).replace(/Warning: no stdin.*\n?/g, '').trim();
388
+ if (stderr)
389
+ log(`Claude stderr: ${stderr.slice(0, 300)}`);
390
+ }
391
+ return false;
392
+ }
393
+ }
394
+ /**
395
+ * 归档 self-heal.md
396
+ */
397
+ function archiveSelfHealLog(p, log) {
398
+ if (!fs.existsSync(p.selfHealLog))
399
+ return;
400
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
401
+ const archivePath = path.join(p.logs, `self-heal-${timestamp}.md`);
402
+ fs.renameSync(p.selfHealLog, archivePath);
403
+ log(`Archived self-heal log to ${archivePath}`);
404
+ }
405
+ /**
406
+ * Resolve a channel instance name to its type and config object.
407
+ * Searches across all channel types (feishu, wechat, aun) for a matching instance.
408
+ */
409
+ function resolveInstanceConfig(instanceName) {
410
+ // 新结构:channel key 是 <type>#<selfAID>#<name>,解析后从对应 agent 的 channels[] 找
411
+ const parts = instanceName.split('#');
412
+ if (parts.length === 3) {
413
+ const [type, selfAID, name] = parts;
414
+ const { agents } = loadAllAgents();
415
+ // AUN channel 的 selfAID 就是 agent.aid
416
+ const agent = agents.find(a => a.aid === selfAID);
417
+ if (!agent)
418
+ return null;
419
+ const inst = agent.channels.find((c) => c.type === type && c.name === name);
420
+ if (inst)
421
+ return { type, config: inst };
422
+ }
423
+ return null;
424
+ }
425
+ /**
426
+ * 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
427
+ * 支持 feishu / wechat,根据 pendingInfo.channel 路由
428
+ *
429
+ * Phase 3 例外说明(出站消息统一计划,docs/outbound-message-unification.md):
430
+ * 主网关进程出站系统通知(上线 / 重启完成 / channel:error 等)已迁到
431
+ * `adapter.send(envelope, { kind: 'system.notice' | 'system.error', ... })` 统一入口。
432
+ * 但 cli.ts 在 restart-monitor 子进程里跑,不持有 EvolAgent / ChannelAdapter 实例
433
+ * (主网关进程已退出,新进程还没起或起不来),只能直连协议 SDK 自发。
434
+ * 因此 self-heal 全流程(启动失败 / 修复中 / 修复成功 / 全部失败)和升级失败通知
435
+ * 留在这里直发,**不属于** Phase 3 改造范围。
436
+ */
437
+ async function notifyChannel(p, pendingInfo, message, log) {
438
+ if (!pendingInfo)
439
+ return;
440
+ const resolved = resolveInstanceConfig(pendingInfo.channel);
441
+ if (!resolved) {
442
+ log(`Channel instance "${pendingInfo.channel}" not found in any agent config`);
443
+ return;
444
+ }
445
+ if (resolved.type === 'feishu') {
446
+ try {
447
+ const inst = resolved.config;
448
+ if (!inst.appId || !inst.appSecret)
449
+ return;
450
+ const lark = await import('@larksuiteoapi/node-sdk');
451
+ const client = new lark.Client({
452
+ appId: inst.appId,
453
+ appSecret: inst.appSecret,
454
+ });
455
+ if (pendingInfo.rootId) {
456
+ await client.im.message.reply({
457
+ path: { message_id: pendingInfo.rootId },
458
+ data: {
459
+ msg_type: 'text',
460
+ content: JSON.stringify({ text: message }),
461
+ reply_in_thread: true,
462
+ },
463
+ });
464
+ }
465
+ else {
466
+ await client.im.message.create({
467
+ params: { receive_id_type: 'chat_id' },
468
+ data: {
469
+ receive_id: pendingInfo.channelId,
470
+ msg_type: 'text',
471
+ content: JSON.stringify({ text: message }),
472
+ },
473
+ });
474
+ }
475
+ log(`Feishu notification sent: ${message.slice(0, 50)}`);
476
+ }
477
+ catch (error) {
478
+ log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
479
+ }
480
+ }
481
+ else if (resolved.type === 'wechat') {
482
+ try {
483
+ const inst = resolved.config;
484
+ if (!inst.token)
485
+ return;
486
+ const crypto = await import('node:crypto');
487
+ const baseUrl = (inst.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
488
+ const token = inst.token;
489
+ // 读取缓存的 context_token
490
+ const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
491
+ let contextToken;
492
+ try {
493
+ if (fs.existsSync(syncBufPath)) {
494
+ const tokens = JSON.parse(fs.readFileSync(syncBufPath, 'utf-8'));
495
+ contextToken = tokens[pendingInfo.channelId];
496
+ }
497
+ }
498
+ catch { }
499
+ if (!contextToken) {
500
+ log(`WeChat notification skipped: no context_token for ${pendingInfo.channelId}`);
501
+ return;
502
+ }
503
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
504
+ const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
505
+ const body = JSON.stringify({
506
+ msg: {
507
+ from_user_id: '',
508
+ to_user_id: pendingInfo.channelId,
509
+ client_id: `evolclaw-restart:${Date.now()}`,
510
+ message_type: 2,
511
+ message_state: 2,
512
+ item_list: [{ type: 1, text_item: { text: message } }],
513
+ context_token: contextToken,
514
+ },
515
+ base_info: { channel_version: '1.0.0' },
516
+ });
517
+ const res = await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
518
+ method: 'POST',
519
+ headers: {
520
+ 'Content-Type': 'application/json',
521
+ 'AuthorizationType': 'ilink_bot_token',
522
+ 'Authorization': `Bearer ${token.trim()}`,
523
+ 'X-WECHAT-UIN': wechatUin,
524
+ 'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
525
+ },
526
+ body,
527
+ });
528
+ if (res.ok) {
529
+ log(`WeChat notification sent: ${message.slice(0, 50)}`);
530
+ }
531
+ else {
532
+ log(`WeChat notification failed: HTTP ${res.status}`);
533
+ }
534
+ }
535
+ catch (error) {
536
+ log(`WeChat notification failed: ${error.message?.slice(0, 200) || error}`);
537
+ }
538
+ }
539
+ }