evolclaw 2.1.2 → 2.2.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 (42) hide show
  1. package/README.md +10 -3
  2. package/data/evolclaw.sample.json +9 -1
  3. package/dist/agents/claude-runner.js +612 -0
  4. package/dist/agents/codex-runner.js +310 -0
  5. package/dist/channels/aun.js +416 -9
  6. package/dist/channels/feishu.js +397 -104
  7. package/dist/channels/wechat.js +84 -2
  8. package/dist/cli.js +427 -126
  9. package/dist/config.js +102 -4
  10. package/dist/core/adapters/claude-session-file-adapter.js +144 -0
  11. package/dist/core/adapters/codex-session-file-adapter.js +196 -0
  12. package/dist/core/agent-loader.js +39 -0
  13. package/dist/core/channel-loader.js +60 -0
  14. package/dist/core/command-handler.js +908 -304
  15. package/dist/core/event-bus.js +32 -0
  16. package/dist/core/ipc-server.js +71 -0
  17. package/dist/core/message-bridge.js +187 -0
  18. package/dist/core/message-processor.js +370 -227
  19. package/dist/core/message-queue.js +153 -29
  20. package/dist/core/permission.js +58 -0
  21. package/dist/core/session-file-adapter.js +7 -0
  22. package/dist/core/session-manager.js +567 -205
  23. package/dist/core/stats-collector.js +86 -0
  24. package/dist/index.js +309 -243
  25. package/dist/paths.js +1 -0
  26. package/dist/utils/init-feishu.js +2 -0
  27. package/dist/utils/init-wechat.js +2 -0
  28. package/dist/utils/init.js +285 -53
  29. package/dist/utils/ipc-client.js +36 -0
  30. package/dist/utils/migrate-project.js +122 -0
  31. package/dist/utils/{permission.js → permission-utils.js} +31 -3
  32. package/dist/utils/rich-content-renderer.js +228 -0
  33. package/dist/utils/session-file-health.js +11 -34
  34. package/dist/utils/stream-debouncer.js +122 -0
  35. package/dist/utils/stream-idle-monitor.js +1 -1
  36. package/package.json +3 -1
  37. package/dist/core/agent-runner.js +0 -348
  38. package/dist/core/message-stream.js +0 -59
  39. package/dist/index.js.bak +0 -340
  40. package/dist/utils/markdown-to-feishu.js +0 -94
  41. /package/dist/utils/{platform.js → cross-platform.js} +0 -0
  42. /package/dist/{core → utils}/message-cache.js +0 -0
package/dist/index.js CHANGED
@@ -1,13 +1,23 @@
1
- import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig } from './config.js';
1
+ import { ClaudeSessionFileAdapter } from './core/adapters/claude-session-file-adapter.js';
2
+ import { CodexSessionFileAdapter } from './core/adapters/codex-session-file-adapter.js';
3
+ import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, validateConfigIntegrity } from './config.js';
2
4
  import { SessionManager } from './core/session-manager.js';
3
- import { AgentRunner } from './core/agent-runner.js';
4
- import { FeishuChannel } from './channels/feishu.js';
5
- import { AUNChannel } from './channels/aun.js';
6
- import { WechatChannel } from './channels/wechat.js';
5
+ import { ClaudeAgentPlugin } from './agents/claude-runner.js';
6
+ import { CodexAgentPlugin } from './agents/codex-runner.js';
7
+ import { FeishuChannelPlugin } from './channels/feishu.js';
8
+ import { WechatChannelPlugin } from './channels/wechat.js';
9
+ import { AUNChannelPlugin } from './channels/aun.js';
7
10
  import { MessageProcessor } from './core/message-processor.js';
8
11
  import { MessageQueue } from './core/message-queue.js';
9
- import { MessageCache } from './core/message-cache.js';
12
+ import { MessageBridge } from './core/message-bridge.js';
13
+ import { MessageCache } from './utils/message-cache.js';
10
14
  import { CommandHandler } from './core/command-handler.js';
15
+ import { EventBus } from './core/event-bus.js';
16
+ import { StatsCollector } from './core/stats-collector.js';
17
+ import { PermissionGateway } from './core/permission.js';
18
+ import { ChannelLoader } from './core/channel-loader.js';
19
+ import { AgentLoader } from './core/agent-loader.js';
20
+ import { IpcServer } from './core/ipc-server.js';
11
21
  import { logger } from './utils/logger.js';
12
22
  import path from 'path';
13
23
  import fs from 'fs';
@@ -34,22 +44,59 @@ async function main() {
34
44
  ensureDataDirs();
35
45
  // 加载配置
36
46
  const config = loadConfig();
47
+ // 配置完整性校验
48
+ const integrity = validateConfigIntegrity(config);
49
+ if (!integrity.valid) {
50
+ const msg = `❌ Config integrity check failed:\n ${integrity.reasons.join('\n ')}`;
51
+ logger.error(msg);
52
+ console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
53
+ process.exit(1);
54
+ }
37
55
  const anthropic = resolveAnthropicConfig(config);
38
56
  logger.info('✓ Config loaded (API keys hidden)');
39
57
  if (anthropic.baseUrl) {
40
58
  logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
41
59
  }
42
- // 初始化数据库
43
- const sessionManager = new SessionManager();
60
+ // 创建事件总线
61
+ const eventBus = new EventBus();
62
+ logger.info('✓ Event bus initialized');
63
+ // 统计收集器(近 1 小时滚动统计)
64
+ const statsCollector = new StatsCollector(eventBus);
65
+ // 初始化数据库(带 ownerResolver)
66
+ const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => {
67
+ return isOwner(config, channel, userId);
68
+ });
44
69
  logger.info('✓ Database initialized');
45
- // 初始化 Agent Runner(带持久化回调)
46
- const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
47
- await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
48
- }, anthropic.baseUrl, config);
49
- if (anthropic.effort) {
50
- agentRunner.setEffort(anthropic.effort);
70
+ // 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
71
+ sessionManager.registerFileAdapter(new ClaudeSessionFileAdapter());
72
+ sessionManager.registerFileAdapter(new CodexSessionFileAdapter());
73
+ // Agent 插件系统
74
+ const agentLoader = new AgentLoader();
75
+ agentLoader.register(new ClaudeAgentPlugin());
76
+ agentLoader.register(new CodexAgentPlugin());
77
+ const agentInstances = agentLoader.createAll(config, {
78
+ onSessionIdUpdate: async (sessionId, agentSessionId) => {
79
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
80
+ },
81
+ });
82
+ // 构建 agent map,支持按 agentId 路由(当前默认使用第一个 agent)
83
+ const agentMap = new Map();
84
+ for (const inst of agentInstances) {
85
+ agentMap.set(inst.agent.name, inst.agent);
86
+ }
87
+ const defaultAgent = config.agents?.defaultAgent || 'claude';
88
+ const agentRunner = agentMap.get(defaultAgent) || agentInstances[0]?.agent;
89
+ if (!agentRunner) {
90
+ throw new Error('No agent backend available. Check agents config.');
91
+ }
92
+ logger.info(`✓ Agent runner ready (default: ${agentRunner.name}, available: ${[...agentMap.keys()].join(', ')})`);
93
+ // 权限审批网关
94
+ const permissionGateway = new PermissionGateway();
95
+ permissionGateway.setEventBus(eventBus);
96
+ // 为所有支持权限的 agent 设置 gateway
97
+ for (const inst of agentInstances) {
98
+ inst.agent.setPermissionGateway?.(permissionGateway);
51
99
  }
52
- logger.info('✓ Agent runner ready');
53
100
  // 创建消息缓存
54
101
  const messageCache = new MessageCache();
55
102
  logger.info('✓ Message cache initialized');
@@ -57,276 +104,293 @@ async function main() {
57
104
  setInterval(() => {
58
105
  messageCache.cleanupExpired();
59
106
  }, 60 * 60 * 1000);
60
- // 飞书渠道(条件初始化)
61
- let feishu = null;
62
- if (config.channels?.feishu?.enabled !== false && config.channels?.feishu?.appId) {
63
- feishu = new FeishuChannel({
64
- appId: config.channels.feishu.appId,
65
- appSecret: config.channels.feishu.appSecret,
66
- db: sessionManager.getDatabase()
67
- });
68
- // 设置项目路径提供器
69
- feishu.onProjectPathRequest(async (chatId) => {
70
- const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
71
- return path.isAbsolute(session.projectPath)
72
- ? session.projectPath
73
- : path.resolve(process.cwd(), session.projectPath);
74
- });
75
- }
76
- // AUN 渠道(条件初始化)
77
- let aun = null;
78
- if (config.channels?.aun?.enabled !== false && config.channels?.aun?.domain) {
79
- aun = new AUNChannel({ domain: config.channels.aun.domain, agentName: config.channels.aun.agentName });
80
- }
107
+ // 渠道插件系统
108
+ const channelLoader = new ChannelLoader();
109
+ channelLoader.register(new FeishuChannelPlugin());
110
+ channelLoader.register(new WechatChannelPlugin());
111
+ channelLoader.register(new AUNChannelPlugin());
112
+ const channelInstances = await channelLoader.createAll(config);
113
+ logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
81
114
  // 创建命令处理器
82
- const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
115
+ const cmdHandler = new CommandHandler(sessionManager, agentMap, config, messageCache, eventBus, defaultAgent);
116
+ cmdHandler.setPermissionGateway(permissionGateway);
117
+ cmdHandler.setStatsCollector(statsCollector);
83
118
  // 创建消息处理器
84
- const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
119
+ const processor = new MessageProcessor(agentMap, sessionManager, config, messageCache, eventBus, (content, channel, channelId, userId, threadId) => {
85
120
  const sendFn = async (id, text, opts) => {
86
121
  const adapter = cmdHandler.getAdapter(channel);
87
122
  if (!adapter)
88
123
  return;
89
- // 文件标记处理(通过 adapter.sendFile 能力判断,不按渠道名分支)
90
- if (adapter.sendFile) {
91
- const fileMarkerPattern = /\[SEND_FILE:([^\]]+)\]/g;
92
- const fileMatches = [...text.matchAll(fileMarkerPattern)];
93
- for (const match of fileMatches) {
94
- const filePath = match[1].trim();
95
- // 跳过占位符/代码片段中的伪路径
96
- if (!filePath || /[\\[\]{}*+?|^$]/.test(filePath))
97
- continue;
98
- const session = await sessionManager.getActiveSession(channel, channelId);
99
- const projectPath = session?.projectPath || process.cwd();
100
- const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
101
- try {
102
- await adapter.sendFile(id, absoluteFilePath);
103
- }
104
- catch (error) {
105
- logger.error(`[${channel}] Failed to send file: ${absoluteFilePath}`, error);
106
- }
107
- }
108
- text = text.replace(fileMarkerPattern, '').trim();
109
- }
110
124
  if (text) {
111
125
  await adapter.sendText(id, text, opts);
112
126
  }
113
127
  };
114
128
  return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
115
- });
129
+ }, defaultAgent);
116
130
  // 回填 processor 和 messageQueue 的引用
117
131
  cmdHandler.setProcessor(processor);
118
- // 设置 compact 开始回调
119
- agentRunner.setCompactStartCallback((sessionId) => {
120
- processor.handleCompactStart();
121
- });
132
+ // 设置 compact 开始回调(对所有支持的 agent)
133
+ for (const inst of agentInstances) {
134
+ inst.agent.setCompactStartCallback?.((sessionId) => {
135
+ processor.handleCompactStart(sessionId);
136
+ });
137
+ }
122
138
  // 创建消息队列
123
139
  const messageQueue = new MessageQueue(async (message) => {
124
140
  await processor.processMessage(message);
125
141
  });
126
- // 设置中断回调
127
- messageQueue.setInterruptCallback(async (sessionKey) => {
128
- await agentRunner.interrupt(sessionKey);
142
+ // 设置中断回调(精确中断正在处理的 agent)
143
+ messageQueue.setInterruptCallback(async (sessionKey, agentId) => {
144
+ const agent = agentMap.get(agentId || defaultAgent);
145
+ if (agent?.hasActiveStream(sessionKey)) {
146
+ await agent.interrupt(sessionKey);
147
+ }
129
148
  });
149
+ messageQueue.setEventBus(eventBus);
130
150
  // 回填 messageQueue 引用
131
151
  cmdHandler.setMessageQueue(messageQueue);
132
- // 注册 Feishu 适配器(如果已初始化)
133
- if (feishu) {
134
- const feishuAdapter = {
135
- name: 'feishu',
136
- sendText: (channelId, text, options) => feishu.sendMessage(channelId, text, options),
137
- sendFile: (channelId, filePath) => feishu.sendFile(channelId, filePath),
138
- isGroupChat: (channelId) => feishu.getChatMode(channelId).then(m => m === 'group'),
139
- };
140
- const feishuOptions = {
141
- systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt] 路径支持相对路径(相对项目目录)或绝对路径。系统会自动上传并发送。',
142
- fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
143
- supportsImages: true,
144
- };
145
- processor.registerChannel(feishuAdapter, feishuOptions);
146
- cmdHandler.registerAdapter(feishuAdapter);
152
+ // 默认策略
153
+ const defaultPolicy = {
154
+ canSwitchProject: (chatType, role) => chatType === 'private' || role === 'owner',
155
+ canListProjects: (chatType, role) => chatType === 'private' || role === 'owner',
156
+ canCreateSession: () => true,
157
+ canDeleteSession: (chatType, role) => chatType === 'private' || role === 'owner',
158
+ canImportCliSession: (chatType, role) => chatType === 'private' || role === 'owner',
159
+ messagePrefix: () => '',
160
+ showMiddleResult: () => true,
161
+ showIdleMonitor: () => true,
162
+ accumulateErrors: () => true,
163
+ };
164
+ // 注册渠道插件的 adapter 和 policy
165
+ for (const inst of channelInstances) {
166
+ // 设置项目路径提供器(如果需要)
167
+ if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
168
+ inst.channel.onProjectPathRequest(async (channelId) => {
169
+ const session = await sessionManager.getOrCreateSession(inst.adapter.name, channelId, config.projects?.defaultPath || process.cwd(), undefined, undefined, undefined, undefined);
170
+ return path.isAbsolute(session.projectPath)
171
+ ? session.projectPath
172
+ : path.resolve(process.cwd(), session.projectPath);
173
+ });
174
+ }
175
+ // 注册 adapter、policy 和 options
176
+ processor.registerChannel(inst.adapter, inst.policy || defaultPolicy, inst.options);
177
+ cmdHandler.registerAdapter(inst.adapter);
178
+ cmdHandler.registerChannel(inst.adapter.name, inst.channel);
179
+ if (inst.policy) {
180
+ cmdHandler.registerPolicy(inst.adapter.name, inst.policy);
181
+ }
147
182
  }
148
- // 注册 AUN 适配器(如果已初始化)
149
- if (aun) {
150
- const aunAdapter = {
151
- name: 'aun',
152
- sendText: (channelId, text) => aun.sendMessage(channelId, text),
153
- };
154
- processor.registerChannel(aunAdapter);
155
- cmdHandler.registerAdapter(aunAdapter);
183
+ // ── MessageBridge:Channel ↔ Core 消息桥梁 ──
184
+ const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
185
+ // ── 渠道消息注册 ──
186
+ // 连接插件系统的渠道
187
+ for (const inst of channelInstances) {
188
+ if (inst.adapter.name === 'feishu') {
189
+ msgBridge.register('feishu', (handler) => inst.channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType }) => {
190
+ handler({
191
+ channel: 'feishu', channelId: chatId, content, images, chatType,
192
+ peerId: peerId || '', peerName, messageId, mentions, threadId,
193
+ replyContext: rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined,
194
+ });
195
+ }), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
196
+ replyToMessageId: replyContext?.replyToMessageId,
197
+ replyInThread: true,
198
+ }), inst.adapter);
199
+ inst.channel.onRecall?.((messageId) => {
200
+ msgBridge.cancel(messageId);
201
+ });
202
+ }
203
+ if (inst.adapter.name === 'wechat') {
204
+ msgBridge.register('wechat', (handler) => inst.channel.onMessage(async (channelId, content, peerId, images, chatType) => {
205
+ handler({
206
+ channel: 'wechat',
207
+ channelId,
208
+ content,
209
+ images,
210
+ chatType: chatType || 'private',
211
+ peerId: peerId || '',
212
+ });
213
+ }), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter);
214
+ }
215
+ if (inst.adapter.name === 'aun') {
216
+ msgBridge.register('aun', (handler) => inst.channel.onMessage(async (opts) => {
217
+ handler({
218
+ channel: 'aun',
219
+ channelId: opts.channelId,
220
+ content: opts.content,
221
+ chatType: opts.chatType || 'private',
222
+ peerId: opts.peerId || '',
223
+ messageId: opts.messageId,
224
+ mentions: opts.mentions,
225
+ threadId: opts.threadId,
226
+ replyContext: opts.replyContext,
227
+ });
228
+ }), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, replyContext), inst.adapter);
229
+ }
156
230
  }
157
- // ── WeChat 渠道(条件初始化)──
158
- let wechat = null;
159
- if (config.channels?.wechat?.enabled && config.channels?.wechat?.token) {
160
- wechat = new WechatChannel({
161
- baseUrl: config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com',
162
- token: config.channels.wechat.token,
163
- });
164
- // 设置项目路径提供器(用于接收文件保存)
165
- wechat.onProjectPathRequest(async (channelId) => {
166
- const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
167
- return path.isAbsolute(session.projectPath)
168
- ? session.projectPath
169
- : path.resolve(process.cwd(), session.projectPath);
170
- });
171
- const wechatAdapter = {
172
- name: 'wechat',
173
- sendText: (channelId, text) => wechat.sendMessage(channelId, text),
174
- sendFile: (channelId, filePath) => wechat.sendFile(channelId, filePath),
175
- };
176
- const wechatOptions = {
177
- systemPromptAppend: '[系统功能] 你可以发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt]',
178
- fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
179
- };
180
- processor.registerChannel(wechatAdapter, wechatOptions);
181
- cmdHandler.registerAdapter(wechatAdapter);
182
- // Session 过期通知(通过 Feishu 等其他渠道告知用户)
183
- wechat.onSessionExpiredNotify(async (message) => {
184
- // 尝试通过已注册的 Feishu owner 通知
185
- const feishuOwner = config.channels?.feishu?.owner;
186
- if (feishuOwner) {
187
- try {
188
- // Feishu owner ID 是 open_id,但 sendMessage 需要 chat_id
189
- // 这里只记日志,因为 owner 的 chat_id 需要从 session 中获取
190
- logger.warn(`[WeChat] ${message}`);
191
- }
192
- catch { }
193
- }
194
- else {
195
- logger.warn(`[WeChat] ${message}`);
196
- }
197
- });
198
- wechat.onMessage(async (channelId, content, userId, images) => {
199
- content = content.trim();
200
- // 首次交互自动绑定主人
201
- if (userId && !config.channels?.wechat?.owner) {
202
- const { setOwner } = await import('./config.js');
203
- setOwner(config, 'wechat', userId);
204
- logger.info(`[Owner] Auto-bound WeChat owner: ${userId}`);
205
- }
206
- // 命令快速路径
207
- if (cmdHandler.isCommand(content)) {
208
- const cmdResult = await cmdHandler.handle(content, 'wechat', channelId, undefined, userId);
209
- if (cmdResult !== null) {
210
- if (cmdResult) {
211
- try {
212
- await wechat.sendMessage(channelId, cmdResult);
213
- }
214
- catch (error) {
215
- logger.error('[WeChat] Failed to send command response:', error);
216
- }
217
- }
218
- return;
219
- }
220
- }
221
- // 获取当前项目路径
222
- const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
223
- // 普通消息进入队列
224
- await messageQueue.enqueue(`wechat-${channelId}`, { channel: 'wechat', channelId, content, images, timestamp: Date.now(), userId }, session.projectPath);
231
+ // ── 连接所有渠道 ──
232
+ const connected = await channelLoader.connectAll(channelInstances);
233
+ // 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
234
+ for (const inst of channelInstances) {
235
+ if (inst.adapter.name === 'feishu' && 'preloadThreads' in inst.channel) {
236
+ const threadIds = sessionManager.getKnownThreadIds('feishu');
237
+ inst.channel.preloadThreads(threadIds);
238
+ }
239
+ }
240
+ for (const name of connected) {
241
+ eventBus.publish({
242
+ type: 'channel:connected',
243
+ channel: name.toLowerCase(),
244
+ timestamp: Date.now()
225
245
  });
226
246
  }
227
- // Feishu 消息处理
228
- if (feishu) {
229
- feishu.onMessage(async ({ channelId: chatId, content: rawContent, images, userId, userName, messageId, mentions, threadId, rootId }) => {
230
- let content = rawContent.trim();
231
- // 首次交互自动绑定主人
232
- if (userId && !config.channels?.feishu?.owner) {
233
- const { setOwner } = await import('./config.js');
234
- setOwner(config, 'feishu', userId);
235
- logger.info(`[Owner] Auto-bound owner: ${userName} (${userId})`);
236
- }
237
- // 命令立即处理,不进入队列
238
- if (cmdHandler.isCommand(content)) {
239
- const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId, threadId);
240
- if (cmdResult !== null) {
241
- if (cmdResult) {
242
- try {
243
- await feishu.sendMessage(chatId, cmdResult, { forceText: true, replyToMessageId: rootId, replyInThread: true });
244
- }
245
- catch (error) {
246
- logger.error('[Feishu] Failed to send command response:', error);
247
- }
247
+ // AUN 重连失败通知:通过其他渠道给 owner 发消息
248
+ for (const inst of channelInstances) {
249
+ if (inst.adapter.name === 'aun' && inst.channel.setOnChannelDown) {
250
+ inst.channel.setOnChannelDown(() => {
251
+ logger.error('[AUN] All reconnect attempts exhausted, notifying owners');
252
+ const msg = '⚠️ AUN 渠道断连,自动重试已用尽。\n使用 /check reconnect 手动重连';
253
+ for (const other of channelInstances) {
254
+ if (other.adapter.name === inst.adapter.name)
255
+ continue;
256
+ const ownerCfg = config.channels?.[other.adapter.name];
257
+ const ownerId = ownerCfg?.owner;
258
+ if (ownerId) {
259
+ other.adapter.sendText(ownerId, msg).catch(err => {
260
+ logger.error(`[AUN] Failed to notify ${other.adapter.name} owner:`, err);
261
+ });
248
262
  }
249
- return;
250
263
  }
251
- }
252
- // 获取当前项目路径(话题会话自动创建,携带 metadata)
253
- const metadata = rootId ? { feishu: { rootId } } : undefined;
254
- const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd(), threadId, metadata);
255
- // 群聊消息添加用户名前缀
256
- const chatMode = await feishu.getChatMode(chatId);
257
- if (chatMode === 'group' && userName) {
258
- content = `[${userName}] ${content}`;
259
- }
260
- // 普通消息进入队列(使用 session.id 作为 key,话题间可并行)
261
- await messageQueue.enqueue(session.id, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions, threadId }, session.projectPath);
262
- });
264
+ });
265
+ }
263
266
  }
264
- // AUN 消息处理
265
- if (aun) {
266
- aun.onMessage(async (sessionId, content) => {
267
- content = content.trim();
268
- // 首次交互自动绑定主人
269
- if (!config.channels?.aun?.owner) {
270
- const { setOwner } = await import('./config.js');
271
- setOwner(config, 'aun', sessionId);
272
- logger.info(`[Owner] Auto-bound AUN owner: ${sessionId}`);
267
+ logger.info(`\n🚀 EvolClaw is running with ${connected.length} channel(s): ${connected.join(', ')}\n`);
268
+ eventBus.publish({
269
+ type: 'system:started',
270
+ channels: connected.map(c => c.toLowerCase()),
271
+ timestamp: Date.now()
272
+ });
273
+ // 恢复重启前未完成的会话
274
+ const pendingSessions = sessionManager.getPendingProcessingSessions();
275
+ if (pendingSessions.length > 0) {
276
+ logger.info(`[Resume] Found ${pendingSessions.length} pending session(s) from before restart`);
277
+ for (const session of pendingSessions) {
278
+ if (!session.agentSessionId) {
279
+ sessionManager.clearProcessing(session.id);
280
+ continue;
273
281
  }
274
- // 命令立即处理,不进入队列
275
- if (cmdHandler.isCommand(content)) {
276
- const cmdResult = await cmdHandler.handle(content, 'aun', sessionId, undefined, sessionId);
277
- if (cmdResult) {
278
- await aun.sendMessage(sessionId, cmdResult);
279
- return;
280
- }
282
+ const agent = agentMap.get(session.agentId) || agentMap.get(defaultAgent);
283
+ if (!agent) {
284
+ sessionManager.clearProcessing(session.id);
285
+ continue;
281
286
  }
282
- // 获取当前项目路径
283
- const session = await sessionManager.getOrCreateSession('aun', sessionId, config.projects?.defaultPath || process.cwd());
284
- // 普通消息进入队列
285
- await messageQueue.enqueue(`aun-${sessionId}`, { channel: 'aun', channelId: sessionId, content, timestamp: Date.now(), userId: sessionId }, session.projectPath);
286
- });
287
+ logger.info(`[Resume] Resuming session: ${session.id} (agent: ${session.agentId})`);
288
+ const resumeMessage = {
289
+ channel: session.channel,
290
+ channelId: session.channelId,
291
+ content: '服务已重启,请继续之前未完成的任务。',
292
+ timestamp: Date.now(),
293
+ peerId: '',
294
+ threadId: session.threadId || undefined,
295
+ replyContext: session.metadata?.replyContext,
296
+ };
297
+ // 清除状态后入队(processMessage 会重新标记)
298
+ sessionManager.clearProcessing(session.id);
299
+ messageQueue.enqueue(session.id, resumeMessage, session.projectPath).catch(err => {
300
+ logger.error(`[Resume] Failed to resume session ${session.id}:`, err);
301
+ });
302
+ }
287
303
  }
288
- // 连接渠道
289
- const channels = [];
290
- const channelInstances = [
291
- ...(feishu ? [{ name: 'Feishu', instance: feishu, timeout: 5000 }] : []),
292
- ...(aun ? [{ name: 'AUN', instance: aun }] : []),
293
- ...(wechat ? [{ name: 'WeChat', instance: wechat }] : []),
294
- ];
295
- for (const { name, instance, timeout } of channelInstances) {
304
+ // 重启通知:通过渠道 adapter 发送(channel-agnostic)
305
+ const pendingFile = path.join(resolvePaths().dataDir, 'restart-pending.json');
306
+ if (fs.existsSync(pendingFile)) {
296
307
  try {
297
- if (timeout) {
298
- await Promise.race([
299
- instance.connect(),
300
- new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), timeout))
301
- ]);
308
+ const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
309
+ const adapter = cmdHandler.getAdapter(pending.channel);
310
+ if (adapter) {
311
+ const replyContext = pending.rootId
312
+ ? { replyToMessageId: pending.rootId, replyInThread: true }
313
+ : undefined;
314
+ await adapter.sendText(pending.channelId, '✅ 服务重启成功!', replyContext);
315
+ logger.info(`[Restart] Notification sent via ${pending.channel}`);
302
316
  }
303
- else {
304
- await instance.connect();
305
- }
306
- logger.info(`✓ ${name} connected`);
307
- channels.push(name);
317
+ fs.unlinkSync(pendingFile);
308
318
  }
309
- catch (error) {
310
- logger.warn(`⚠ ${name} connection failed (will continue without it)`);
311
- if (error instanceof Error) {
312
- logger.warn(` Reason: ${error.message}`);
313
- }
319
+ catch (e) {
320
+ logger.error('[Restart] Failed to send restart notification:', e);
314
321
  }
315
322
  }
316
- logger.info(`\n🚀 EvolClaw is running with ${channels.length} channel(s): ${channels.join(', ')}\n`);
317
323
  // 写入 ready 信号,供 restart-monitor 检测启动成功
318
324
  const readySignalPath = resolvePaths().readySignal;
319
325
  fs.writeFileSync(readySignalPath, String(Date.now()));
320
326
  logger.info(`✓ Ready signal written: ${readySignalPath}`);
327
+ // IPC server — 供 CLI 查询实时状态
328
+ const ipcServer = new IpcServer(resolvePaths().socket, () => {
329
+ const channels = {};
330
+ for (const inst of channelInstances) {
331
+ const name = inst.adapter.name;
332
+ channels[name] = inst.channel.getStatus?.() ?? { connected: true };
333
+ }
334
+ const snap = statsCollector.getSnapshot();
335
+ return {
336
+ pid: process.pid,
337
+ uptime: snap.uptimeMs,
338
+ channels,
339
+ queue: {
340
+ pending: messageQueue.getGlobalQueueLength(),
341
+ processing: messageQueue.getGlobalProcessingCount(),
342
+ },
343
+ stats: {
344
+ received: snap.lastHour.received,
345
+ completed: snap.lastHour.completed,
346
+ errors: snap.lastHour.errors,
347
+ avgResponseMs: snap.lastHour.avgResponseMs,
348
+ },
349
+ };
350
+ });
351
+ ipcServer.start();
352
+ // 运行时配置文件监控
353
+ const configPath = resolvePaths().config;
354
+ fs.watchFile(configPath, { interval: 5000 }, (_curr, _prev) => {
355
+ let newConfig;
356
+ try {
357
+ newConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
358
+ }
359
+ catch {
360
+ // JSON 解析失败 → 视为坏文件,备份内存中的好副本
361
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
362
+ const backupPath = path.join(resolvePaths().dataDir, `evolclaw-${ts}.json`);
363
+ fs.writeFileSync(backupPath, JSON.stringify(config, null, 2));
364
+ logger.warn(`[Config Watch] Config file is not valid JSON. In-memory snapshot saved to ${backupPath}`);
365
+ eventBus.publish({ type: 'config:corrupted', backupPath, reasons: ['Invalid JSON'] });
366
+ return;
367
+ }
368
+ const result = validateConfigIntegrity(newConfig);
369
+ if (!result.valid) {
370
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
371
+ const backupPath = path.join(resolvePaths().dataDir, `evolclaw-${ts}.json`);
372
+ fs.writeFileSync(backupPath, JSON.stringify(config, null, 2));
373
+ logger.warn(`[Config Watch] Bad config write detected. Reasons: ${result.reasons.join('; ')}. In-memory snapshot saved to ${backupPath}`);
374
+ eventBus.publish({ type: 'config:corrupted', backupPath, reasons: result.reasons });
375
+ }
376
+ else {
377
+ logger.debug(`[Config Watch] Config file modified, passes integrity check`);
378
+ }
379
+ });
321
380
  // 优雅关闭
322
381
  const shutdown = async () => {
323
382
  logger.info('\n\nShutting down gracefully...');
324
- if (feishu)
325
- await feishu.disconnect();
326
- if (aun)
327
- await aun.disconnect();
328
- if (wechat)
329
- await wechat.disconnect();
383
+ fs.unwatchFile(configPath);
384
+ ipcServer.stop();
385
+ eventBus.publish({
386
+ type: 'system:shutdown',
387
+ timestamp: Date.now()
388
+ });
389
+ // 断开插件系统的渠道
390
+ await channelLoader.disconnectAll(channelInstances);
391
+ for (const inst of channelInstances) {
392
+ eventBus.publish({ type: 'channel:disconnected', channel: inst.adapter.name, reason: 'shutdown' });
393
+ }
330
394
  sessionManager.close();
331
395
  logger.info('✓ Shutdown complete');
332
396
  process.exit(0);
@@ -335,6 +399,8 @@ async function main() {
335
399
  process.on('SIGTERM', shutdown);
336
400
  }
337
401
  main().catch((error) => {
402
+ const msg = `Fatal error: ${error?.stack || error}`;
338
403
  logger.error('Fatal error:', error);
404
+ console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
339
405
  process.exit(1);
340
406
  });
package/dist/paths.js CHANGED
@@ -33,6 +33,7 @@ export function resolvePaths() {
33
33
  lineStats: path.join(root, 'logs', 'line-stats.log'),
34
34
  readySignal: path.join(root, 'logs', 'ready.signal'),
35
35
  selfHealLog: path.join(root, 'logs', 'self-heal.md'),
36
+ socket: path.join(root, 'logs', 'evolclaw.sock'),
36
37
  };
37
38
  }
38
39
  export function ensureDataDirs() {
@@ -247,6 +247,8 @@ export async function cmdInitFeishu() {
247
247
  else {
248
248
  delete config.channels.feishu.owner;
249
249
  }
250
+ if (!config.channels.defaultChannel)
251
+ config.channels.defaultChannel = 'feishu';
250
252
  fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
251
253
  console.log(`\n✅ 飞书连接成功!`);
252
254
  console.log(` App ID: ${result.appId}`);