evolclaw 2.1.0 → 2.1.1

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.
@@ -19,10 +19,12 @@ export class MessageProcessor {
19
19
  currentFlusher;
20
20
  currentIsGroup = false;
21
21
  shouldSuppressActivities = false;
22
- /** 话题 session 永远不是后台任务;主 session 与当前活跃 session 比对 */
22
+ /** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
23
23
  async isBackgroundSession(session, channel, channelId) {
24
+ // 话题会话独立运行,不是后台任务
24
25
  if (session.threadId)
25
26
  return false;
27
+ // 主会话:与当前活跃会话比对
26
28
  const active = await this.sessionManager.getActiveSession(channel, channelId);
27
29
  return active ? session.id !== active.id : false;
28
30
  }
@@ -61,7 +63,7 @@ export class MessageProcessor {
61
63
  const isOwnerUser = isOwner(this.config, message.channel, message.userId || '');
62
64
  // 非主人(群聊或单聊):空闲监控静默/简短
63
65
  const quietMode = isGroup || !isOwnerUser;
64
- // 计算是否抑制活动输出
66
+ // 计算是否抑制中间输出(工具活动 + 流式文本)
65
67
  const shouldSuppress = () => {
66
68
  const mode = this.config.showActivities ?? 'all';
67
69
  if (mode === 'all')
@@ -70,6 +72,8 @@ export class MessageProcessor {
70
72
  return isGroup;
71
73
  if (mode === 'owner-dm-only')
72
74
  return isGroup || !isOwnerUser;
75
+ if (mode === 'none')
76
+ return true;
73
77
  return false;
74
78
  };
75
79
  this.shouldSuppressActivities = shouldSuppress();
@@ -265,7 +269,7 @@ ${suggestions}`, sendOpts);
265
269
  await adapter.sendText(message.channelId, text, Object.keys(opts).length ? opts : undefined);
266
270
  }
267
271
  // 后台任务:静默,不发送输出
268
- }, this.config.flushDelay ? this.config.flushDelay * 1000 : 4000, options?.fileMarkerPattern);
272
+ }, (this.config.flushDelay || 4) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag);
269
273
  // 保存当前 flusher,用于 compact 事件
270
274
  this.currentFlusher = flusher;
271
275
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
@@ -311,7 +315,7 @@ ${suggestions}`, sendOpts);
311
315
  // 文件存在性检查:真实路径但文件不存在,告知用户
312
316
  if (!fs.existsSync(resolvedPath)) {
313
317
  logger.warn(`[${adapter.name}] File not found: ${resolvedPath}`);
314
- await adapter.sendText(message.channelId, `⚠️ 文件未找到: ${filePath}`);
318
+ await adapter.sendText(message.channelId, `⚠️ 文件未找到: ${filePath}`, this.getThreadSendOpts(session));
315
319
  continue;
316
320
  }
317
321
  logger.info(`[${adapter.name}] Sending file: ${resolvedPath}`);
@@ -320,7 +324,7 @@ ${suggestions}`, sendOpts);
320
324
  }
321
325
  catch (error) {
322
326
  logger.error(`[${adapter.name}] Failed to send file: ${resolvedPath}`, error);
323
- await adapter.sendText(message.channelId, `❌ 文件发送失败: ${filePath}`);
327
+ await adapter.sendText(message.channelId, `❌ 文件发送失败: ${filePath}`, this.getThreadSendOpts(session));
324
328
  }
325
329
  }
326
330
  }
@@ -380,7 +384,14 @@ ${suggestions}`, sendOpts);
380
384
  }
381
385
  else {
382
386
  const userMessage = getErrorMessage(error);
383
- await adapter.sendText(message.channelId, userMessage);
387
+ // 获取 session 用于话题回复(如果 resolveSession 已执行)
388
+ let sendOpts;
389
+ try {
390
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
391
+ sendOpts = this.getThreadSendOpts(session);
392
+ }
393
+ catch { }
394
+ await adapter.sendText(message.channelId, userMessage, sendOpts);
384
395
  }
385
396
  }
386
397
  }
@@ -388,7 +399,11 @@ ${suggestions}`, sendOpts);
388
399
  * 解析会话和项目路径
389
400
  */
390
401
  async resolveSession(message) {
391
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
402
+ // 话题会话:传入 rootId metadata(首条消息的 messageId 作为 rootId)
403
+ const metadata = message.threadId && message.messageId
404
+ ? { feishu: { rootId: message.messageId } }
405
+ : undefined;
406
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId, metadata);
392
407
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
393
408
  ? session.projectPath
394
409
  : path.resolve(process.cwd(), session.projectPath);
@@ -419,11 +434,13 @@ ${suggestions}`, sendOpts);
419
434
  const isCurrentlyBackground = await this.isBackgroundSession(session, session.channel, session.channelId);
420
435
  // === 前台任务:正常处理所有事件 ===
421
436
  if (!isCurrentlyBackground) {
422
- // 流式文本事件
437
+ // 流式文本事件(抑制时跳过,只累积到 allText)
423
438
  if (event.type === 'text_delta' && event.text) {
424
439
  hasTextDelta = true;
425
440
  hasReceivedText = true;
426
- flusher.addText(event.text);
441
+ if (!shouldSuppress()) {
442
+ flusher.addText(event.text);
443
+ }
427
444
  }
428
445
  // 系统事件:compact_boundary(群聊时静默)
429
446
  if (event.type === 'system' && event.subtype === 'compact_boundary') {
@@ -457,7 +474,9 @@ ${suggestions}`, sendOpts);
457
474
  else if (content.type === 'text' && content.text && !hasTextDelta) {
458
475
  // 仅在没有 text_delta 事件时从 assistant 事件提取文本,避免重复
459
476
  hasReceivedText = true;
460
- flusher.addTextBlock(content.text);
477
+ if (!shouldSuppress()) {
478
+ flusher.addTextBlock(content.text);
479
+ }
461
480
  }
462
481
  }
463
482
  }
@@ -470,14 +489,19 @@ ${suggestions}`, sendOpts);
470
489
  flusher.addActivity(`⚠️ ${toolName}: ${errorMsg}`);
471
490
  }
472
491
  }
473
- // Result 事件:仅在没有流式文本时使用 result 作为最终输出
492
+ // Result 事件:最终输出
474
493
  if (event.type === 'result' && event.result) {
475
- logger.debug(`[MessageProcessor] result event: hasReceivedText=${hasReceivedText}, result="${event.result}"`);
476
- if (!hasReceivedText) {
477
- // 没有通过 text_delta 或 assistant 收到文本,使用 result 作为兜底
494
+ logger.debug(`[MessageProcessor] result event: hasReceivedText=${hasReceivedText}, shouldSuppress=${shouldSuppress()}, result="${event.result}"`);
495
+ if (shouldSuppress()) {
496
+ // 抑制模式:直接发送 result(跳过中间输出)
497
+ flusher.addText(event.result);
498
+ }
499
+ else if (!hasReceivedText) {
500
+ // 非抑制模式 + 无流式文本:使用 result 作为兜底
478
501
  flusher.addText(event.result);
479
502
  }
480
- await flusher.flush();
503
+ // 非抑制模式 + 有流式文本:已通过 text_delta 累积,无需再添加
504
+ await flusher.flush(true); // isFinal=true 标记最终输出
481
505
  }
482
506
  continue;
483
507
  }
@@ -170,13 +170,11 @@ export class SessionManager {
170
170
  `);
171
171
  }
172
172
  async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name) {
173
- const normalizedThreadId = threadId || '';
174
- // 话题会话:直接按 thread_id 查找/创建
175
- if (normalizedThreadId) {
176
- return this.getOrCreateThreadSession(channel, channelId, normalizedThreadId, defaultProjectPath, metadata, name);
173
+ // 话题会话:独立查找/创建,不参与 isActive 竞争
174
+ if (threadId) {
175
+ return this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name);
177
176
  }
178
- // 主会话:原有逻辑
179
- // 1. 查找该聊天的活跃会话
177
+ // 主会话:查找活跃会话
180
178
  const active = this.db.prepare(`
181
179
  SELECT * FROM sessions
182
180
  WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
@@ -185,28 +183,24 @@ export class SessionManager {
185
183
  const validSessionId = this.validateSessionFile(active);
186
184
  return { ...this.rowToSession(active), agentSessionId: validSessionId };
187
185
  }
188
- // 2. 没有活跃会话,查找该聊天在默认项目的会话(匹配 thread_id)
186
+ // 查找默认项目的主会话
189
187
  const existing = this.db.prepare(`
190
188
  SELECT * FROM sessions
191
- WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ?
189
+ WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ''
192
190
  ORDER BY updated_at DESC LIMIT 1
193
- `).get(channel, channelId, defaultProjectPath, normalizedThreadId);
191
+ `).get(channel, channelId, defaultProjectPath);
194
192
  if (existing) {
195
193
  const validSessionId = this.validateSessionFile(existing);
196
- // 激活该会话
197
- this.db.prepare(`
198
- UPDATE sessions SET is_active = 1, updated_at = ?
199
- WHERE id = ?
200
- `).run(Date.now(), existing.id);
194
+ this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), existing.id);
201
195
  return { ...this.rowToSession(existing), agentSessionId: validSessionId, isActive: true };
202
196
  }
203
- // 3. 创建新会话(默认为活跃)
197
+ // 创建新主会话
204
198
  const session = {
205
199
  id: `${channel}-${channelId}-${Date.now()}`,
206
200
  channel,
207
201
  channelId,
208
202
  projectPath: defaultProjectPath,
209
- threadId: normalizedThreadId,
203
+ threadId: '',
210
204
  agentType: 'claude',
211
205
  metadata,
212
206
  name: name || '默认会话',
@@ -214,22 +208,7 @@ export class SessionManager {
214
208
  createdAt: Date.now(),
215
209
  updatedAt: Date.now()
216
210
  };
217
- // 使用 INSERT OR IGNORE 避免并发时的 UNIQUE 约束冲突
218
- const result = this.db.prepare(`
219
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, thread_id, agent_type, agent_session_id, name, is_active, created_at, updated_at, metadata)
220
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
221
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId, session.agentType, session.agentSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
222
- // 如果插入被忽略(已存在),重新查询
223
- if (result.changes === 0) {
224
- const recheck = this.db.prepare(`
225
- SELECT * FROM sessions
226
- WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ?
227
- `).get(channel, channelId, defaultProjectPath, normalizedThreadId);
228
- if (recheck) {
229
- this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), recheck.id);
230
- return { ...this.rowToSession(recheck), isActive: true, updatedAt: Date.now() };
231
- }
232
- }
211
+ this.insertSession(session);
233
212
  return session;
234
213
  }
235
214
  async updateSession(sessionId, updates) {
@@ -247,15 +226,16 @@ export class SessionManager {
247
226
  this.db.prepare(`UPDATE sessions SET ${fields}, updated_at = ? WHERE id = ?`).run(...values, Date.now(), sessionId);
248
227
  }
249
228
  getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name) {
229
+ // 查找已有话题会话
250
230
  const existing = this.db.prepare(`
251
231
  SELECT * FROM sessions
252
232
  WHERE channel = ? AND channel_id = ? AND thread_id = ?
253
- LIMIT 1
254
233
  `).get(channel, channelId, threadId);
255
234
  if (existing) {
256
235
  const validSessionId = this.validateSessionFile(existing);
257
- const existingMeta = this.rowToSession(existing).metadata;
236
+ // 合并 metadata(如果提供)
258
237
  if (metadata) {
238
+ const existingMeta = this.rowToSession(existing).metadata;
259
239
  const merged = existingMeta ? { ...existingMeta, ...metadata } : metadata;
260
240
  this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
261
241
  .run(JSON.stringify(merged), Date.now(), existing.id);
@@ -263,12 +243,13 @@ export class SessionManager {
263
243
  }
264
244
  return { ...this.rowToSession(existing), agentSessionId: validSessionId };
265
245
  }
266
- const activeSession = this.db.prepare(`
246
+ // 继承当前活跃主会话的项目路径
247
+ const activeMain = this.db.prepare(`
267
248
  SELECT project_path FROM sessions
268
249
  WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
269
- LIMIT 1
270
250
  `).get(channel, channelId);
271
- const projectPath = activeSession?.project_path || defaultProjectPath;
251
+ const projectPath = activeMain?.project_path || defaultProjectPath;
252
+ // 创建新话题会话(isActive 固定为 false)
272
253
  const session = {
273
254
  id: `${channel}-${channelId}-${Date.now()}`,
274
255
  channel,
@@ -283,15 +264,6 @@ export class SessionManager {
283
264
  updatedAt: Date.now()
284
265
  };
285
266
  this.insertSession(session);
286
- // Race condition 保护
287
- const recheck = this.db.prepare(`
288
- SELECT * FROM sessions
289
- WHERE channel = ? AND channel_id = ? AND thread_id = ?
290
- LIMIT 1
291
- `).get(channel, channelId, threadId);
292
- if (recheck && recheck.id !== session.id) {
293
- return this.rowToSession(recheck);
294
- }
295
267
  return session;
296
268
  }
297
269
  async switchProject(channel, channelId, newProjectPath) {
@@ -411,6 +383,12 @@ export class SessionManager {
411
383
  `).run(newName, Date.now(), sessionId);
412
384
  return result.changes > 0;
413
385
  }
386
+ async unbindSession(sessionId) {
387
+ const result = this.db.prepare(`
388
+ DELETE FROM sessions WHERE id = ?
389
+ `).run(sessionId);
390
+ return result.changes > 0;
391
+ }
414
392
  async createNewSession(channel, channelId, projectPath, name) {
415
393
  // 取消当前活跃会话
416
394
  this.deactivateAll(channel, channelId);
package/dist/index.js CHANGED
@@ -46,6 +46,9 @@ async function main() {
46
46
  const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
47
47
  await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
48
48
  }, anthropic.baseUrl, config);
49
+ if (anthropic.effort) {
50
+ agentRunner.setEffort(anthropic.effort);
51
+ }
49
52
  logger.info('✓ Agent runner ready');
50
53
  // 创建消息缓存
51
54
  const messageCache = new MessageCache();
@@ -79,7 +82,7 @@ async function main() {
79
82
  const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
80
83
  // 创建消息处理器
81
84
  const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
82
- const sendFn = async (id, text) => {
85
+ const sendFn = async (id, text, opts) => {
83
86
  const adapter = cmdHandler.getAdapter(channel);
84
87
  if (!adapter)
85
88
  return;
@@ -105,7 +108,7 @@ async function main() {
105
108
  text = text.replace(fileMarkerPattern, '').trim();
106
109
  }
107
110
  if (text) {
108
- await adapter.sendText(id, text);
111
+ await adapter.sendText(id, text, opts);
109
112
  }
110
113
  };
111
114
  return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
@@ -0,0 +1,340 @@
1
+ import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig } from './config.js';
2
+ 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';
7
+ import { MessageProcessor } from './core/message-processor.js';
8
+ import { MessageQueue } from './core/message-queue.js';
9
+ import { MessageCache } from './core/message-cache.js';
10
+ import { CommandHandler } from './core/command-handler.js';
11
+ import { logger } from './utils/logger.js';
12
+ import path from 'path';
13
+ import fs from 'fs';
14
+ async function main() {
15
+ // 过滤飞书 SDK 的 info 日志
16
+ const originalLog = console.log;
17
+ const originalInfo = console.info;
18
+ const filter = (...args) => {
19
+ const firstArg = String(args[0] || '');
20
+ return firstArg.includes('[info]') || firstArg.includes('[ws]');
21
+ };
22
+ console.log = (...args) => {
23
+ if (filter(...args))
24
+ return;
25
+ originalLog(...args);
26
+ };
27
+ console.info = (...args) => {
28
+ if (filter(...args))
29
+ return;
30
+ originalInfo(...args);
31
+ };
32
+ logger.info('EvolClaw starting...');
33
+ // 确保数据目录存在
34
+ ensureDataDirs();
35
+ // 加载配置
36
+ const config = loadConfig();
37
+ const anthropic = resolveAnthropicConfig(config);
38
+ logger.info('✓ Config loaded (API keys hidden)');
39
+ if (anthropic.baseUrl) {
40
+ logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
41
+ }
42
+ // 初始化数据库
43
+ const sessionManager = new SessionManager();
44
+ 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);
51
+ }
52
+ logger.info('✓ Agent runner ready');
53
+ // 创建消息缓存
54
+ const messageCache = new MessageCache();
55
+ logger.info('✓ Message cache initialized');
56
+ // 定期清理过期消息(每小时)
57
+ setInterval(() => {
58
+ messageCache.cleanupExpired();
59
+ }, 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
+ }
81
+ // 创建命令处理器
82
+ const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
83
+ // 创建消息处理器
84
+ const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
85
+ const sendFn = async (id, text, opts) => {
86
+ const adapter = cmdHandler.getAdapter(channel);
87
+ if (!adapter)
88
+ 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
+ if (text) {
111
+ await adapter.sendText(id, text, opts);
112
+ }
113
+ };
114
+ return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
115
+ });
116
+ // 回填 processor 和 messageQueue 的引用
117
+ cmdHandler.setProcessor(processor);
118
+ // 设置 compact 开始回调
119
+ agentRunner.setCompactStartCallback((sessionId) => {
120
+ processor.handleCompactStart();
121
+ });
122
+ // 创建消息队列
123
+ const messageQueue = new MessageQueue(async (message) => {
124
+ await processor.processMessage(message);
125
+ });
126
+ // 设置中断回调
127
+ messageQueue.setInterruptCallback(async (sessionKey) => {
128
+ await agentRunner.interrupt(sessionKey);
129
+ });
130
+ // 回填 messageQueue 引用
131
+ 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);
147
+ }
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);
156
+ }
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);
225
+ });
226
+ }
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
+ }
248
+ }
249
+ return;
250
+ }
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
+ });
263
+ }
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}`);
273
+ }
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
+ }
281
+ }
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
+ }
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) {
296
+ try {
297
+ if (timeout) {
298
+ await Promise.race([
299
+ instance.connect(),
300
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), timeout))
301
+ ]);
302
+ }
303
+ else {
304
+ await instance.connect();
305
+ }
306
+ logger.info(`✓ ${name} connected`);
307
+ channels.push(name);
308
+ }
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
+ }
314
+ }
315
+ }
316
+ logger.info(`\n🚀 EvolClaw is running with ${channels.length} channel(s): ${channels.join(', ')}\n`);
317
+ // 写入 ready 信号,供 restart-monitor 检测启动成功
318
+ const readySignalPath = resolvePaths().readySignal;
319
+ fs.writeFileSync(readySignalPath, String(Date.now()));
320
+ logger.info(`✓ Ready signal written: ${readySignalPath}`);
321
+ // 优雅关闭
322
+ const shutdown = async () => {
323
+ 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();
330
+ sessionManager.close();
331
+ logger.info('✓ Shutdown complete');
332
+ process.exit(0);
333
+ };
334
+ process.on('SIGINT', shutdown);
335
+ process.on('SIGTERM', shutdown);
336
+ }
337
+ main().catch((error) => {
338
+ logger.error('Fatal error:', error);
339
+ process.exit(1);
340
+ });