evolclaw 2.1.2 → 2.3.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.
- package/README.md +59 -30
- package/data/evolclaw.sample.json +15 -4
- package/dist/agents/claude-runner.js +685 -0
- package/dist/agents/codex-runner.js +315 -0
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +580 -10
- package/dist/channels/feishu.js +888 -135
- package/dist/channels/wechat.js +127 -21
- package/dist/cli.js +519 -136
- package/dist/config.js +277 -25
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +67 -0
- package/dist/core/command-handler.js +1537 -392
- package/dist/core/event-bus.js +32 -0
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/message/message-processor.js +1028 -0
- package/dist/core/message/message-queue.js +240 -0
- package/dist/core/message/stream-debouncer.js +122 -0
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
- package/dist/core/permission.js +259 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-manager.js +1072 -0
- package/dist/index.js +402 -252
- package/dist/ipc.js +106 -0
- package/dist/paths.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/{platform.js → cross-platform.js} +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +190 -53
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/stats-collector.js +102 -0
- package/package.json +4 -2
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-processor.js +0 -604
- package/dist/core/message-queue.js +0 -116
- package/dist/core/message-stream.js +0 -59
- package/dist/core/session-manager.js +0 -664
- package/dist/index.js.bak +0 -340
- package/dist/utils/init-feishu.js +0 -261
- package/dist/utils/init-wechat.js +0 -170
- package/dist/utils/markdown-to-feishu.js +0 -94
- package/dist/utils/permission.js +0 -43
- package/dist/utils/session-file-health.js +0 -68
- /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
|
|
2
|
+
import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
|
|
3
|
+
import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
|
|
4
|
+
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, validateConfigIntegrity, validateChannelInstanceNames, getOwner } from './config.js';
|
|
5
|
+
import { SessionManager } from './core/session/session-manager.js';
|
|
6
|
+
import { ClaudeAgentPlugin } from './agents/claude-runner.js';
|
|
7
|
+
import { CodexAgentPlugin } from './agents/codex-runner.js';
|
|
8
|
+
import { GeminiAgentPlugin } from './agents/gemini-runner.js';
|
|
9
|
+
import { FeishuChannelPlugin } from './channels/feishu.js';
|
|
10
|
+
import { WechatChannelPlugin } from './channels/wechat.js';
|
|
11
|
+
import { AUNChannelPlugin } from './channels/aun.js';
|
|
12
|
+
import { MessageProcessor } from './core/message/message-processor.js';
|
|
13
|
+
import { MessageQueue } from './core/message/message-queue.js';
|
|
14
|
+
import { MessageBridge } from './core/message/message-bridge.js';
|
|
15
|
+
import { MessageCache } from './core/message/message-cache.js';
|
|
10
16
|
import { CommandHandler } from './core/command-handler.js';
|
|
17
|
+
import { EventBus } from './core/event-bus.js';
|
|
18
|
+
import { StatsCollector } from './utils/stats-collector.js';
|
|
19
|
+
import { PermissionGateway } from './core/permission.js';
|
|
20
|
+
import { InteractionRouter } from './core/interaction-router.js';
|
|
21
|
+
import { ChannelLoader } from './core/channel-loader.js';
|
|
22
|
+
import { AgentLoader } from './core/agent-loader.js';
|
|
23
|
+
import { IpcServer } from './ipc.js';
|
|
11
24
|
import { logger } from './utils/logger.js';
|
|
12
25
|
import path from 'path';
|
|
13
26
|
import fs from 'fs';
|
|
@@ -34,22 +47,65 @@ async function main() {
|
|
|
34
47
|
ensureDataDirs();
|
|
35
48
|
// 加载配置
|
|
36
49
|
const config = loadConfig();
|
|
50
|
+
// 配置完整性校验
|
|
51
|
+
const integrity = validateConfigIntegrity(config);
|
|
52
|
+
if (!integrity.valid) {
|
|
53
|
+
const msg = `❌ Config integrity check failed:\n ${integrity.reasons.join('\n ')}`;
|
|
54
|
+
logger.error(msg);
|
|
55
|
+
console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
37
58
|
const anthropic = resolveAnthropicConfig(config);
|
|
38
59
|
logger.info('✓ Config loaded (API keys hidden)');
|
|
60
|
+
// Channel instance name uniqueness check
|
|
61
|
+
validateChannelInstanceNames(config);
|
|
39
62
|
if (anthropic.baseUrl) {
|
|
40
63
|
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
41
64
|
}
|
|
42
|
-
//
|
|
43
|
-
const
|
|
65
|
+
// 创建事件总线
|
|
66
|
+
const eventBus = new EventBus();
|
|
67
|
+
logger.info('✓ Event bus initialized');
|
|
68
|
+
// 统计收集器(近 1 小时滚动统计)
|
|
69
|
+
const statsCollector = new StatsCollector(eventBus);
|
|
70
|
+
// 初始化数据库(带 ownerResolver)
|
|
71
|
+
const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => {
|
|
72
|
+
return isOwner(config, channel, userId);
|
|
73
|
+
});
|
|
44
74
|
logger.info('✓ Database initialized');
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
// 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
|
|
76
|
+
sessionManager.registerFileAdapter(new ClaudeSessionFileAdapter());
|
|
77
|
+
sessionManager.registerFileAdapter(new CodexSessionFileAdapter());
|
|
78
|
+
sessionManager.registerFileAdapter(new GeminiSessionFileAdapter());
|
|
79
|
+
// Agent 插件系统
|
|
80
|
+
const agentLoader = new AgentLoader();
|
|
81
|
+
agentLoader.register(new ClaudeAgentPlugin());
|
|
82
|
+
agentLoader.register(new CodexAgentPlugin());
|
|
83
|
+
agentLoader.register(new GeminiAgentPlugin());
|
|
84
|
+
const agentInstances = agentLoader.createAll(config, {
|
|
85
|
+
onSessionIdUpdate: async (sessionId, agentSessionId) => {
|
|
86
|
+
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
// 构建 agent map,支持按 agentId 路由(当前默认使用第一个 agent)
|
|
90
|
+
const agentMap = new Map();
|
|
91
|
+
for (const inst of agentInstances) {
|
|
92
|
+
agentMap.set(inst.agent.name, inst.agent);
|
|
93
|
+
}
|
|
94
|
+
const defaultAgent = config.agents?.defaultAgent || 'claude';
|
|
95
|
+
const agentRunner = agentMap.get(defaultAgent) || agentInstances[0]?.agent;
|
|
96
|
+
if (!agentRunner) {
|
|
97
|
+
throw new Error('No agent backend available. Check agents config.');
|
|
98
|
+
}
|
|
99
|
+
logger.info(`✓ Agent runner ready (default: ${agentRunner.name}, available: ${[...agentMap.keys()].join(', ')})`);
|
|
100
|
+
// 权限审批网关
|
|
101
|
+
const permissionGateway = new PermissionGateway();
|
|
102
|
+
permissionGateway.setEventBus(eventBus);
|
|
103
|
+
// 交互路由器
|
|
104
|
+
const interactionRouter = new InteractionRouter();
|
|
105
|
+
// 为所有支持权限的 agent 设置 gateway
|
|
106
|
+
for (const inst of agentInstances) {
|
|
107
|
+
inst.agent.setPermissionGateway?.(permissionGateway);
|
|
51
108
|
}
|
|
52
|
-
logger.info('✓ Agent runner ready');
|
|
53
109
|
// 创建消息缓存
|
|
54
110
|
const messageCache = new MessageCache();
|
|
55
111
|
logger.info('✓ Message cache initialized');
|
|
@@ -57,284 +113,378 @@ async function main() {
|
|
|
57
113
|
setInterval(() => {
|
|
58
114
|
messageCache.cleanupExpired();
|
|
59
115
|
}, 60 * 60 * 1000);
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
}
|
|
116
|
+
// 渠道插件系统
|
|
117
|
+
const channelLoader = new ChannelLoader();
|
|
118
|
+
channelLoader.register(new FeishuChannelPlugin());
|
|
119
|
+
channelLoader.register(new WechatChannelPlugin());
|
|
120
|
+
channelLoader.register(new AUNChannelPlugin());
|
|
121
|
+
const channelInstances = await channelLoader.createAll(config);
|
|
122
|
+
logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
|
|
123
|
+
// 启动迁移:将 sessions.channel 从 channelType 回填为实例名
|
|
124
|
+
sessionManager.migrateChannelToInstanceName();
|
|
81
125
|
// 创建命令处理器
|
|
82
|
-
const cmdHandler = new CommandHandler(sessionManager,
|
|
126
|
+
const cmdHandler = new CommandHandler(sessionManager, agentMap, config, messageCache, eventBus, defaultAgent);
|
|
127
|
+
cmdHandler.setPermissionGateway(permissionGateway);
|
|
128
|
+
cmdHandler.setInteractionRouter(interactionRouter);
|
|
129
|
+
cmdHandler.setStatsCollector(statsCollector);
|
|
83
130
|
// 创建消息处理器
|
|
84
|
-
const processor = new MessageProcessor(
|
|
131
|
+
const processor = new MessageProcessor(agentMap, sessionManager, config, messageCache, eventBus, (content, channel, channelId, userId, threadId) => {
|
|
85
132
|
const sendFn = async (id, text, opts) => {
|
|
86
133
|
const adapter = cmdHandler.getAdapter(channel);
|
|
87
134
|
if (!adapter)
|
|
88
135
|
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
136
|
if (text) {
|
|
111
137
|
await adapter.sendText(id, text, opts);
|
|
112
138
|
}
|
|
113
139
|
};
|
|
114
140
|
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
115
|
-
});
|
|
141
|
+
}, defaultAgent);
|
|
116
142
|
// 回填 processor 和 messageQueue 的引用
|
|
117
143
|
cmdHandler.setProcessor(processor);
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
144
|
+
// 设置交互路由器
|
|
145
|
+
processor.setInteractionRouter(interactionRouter);
|
|
146
|
+
// 设置 compact 开始回调(对所有支持的 agent)
|
|
147
|
+
for (const inst of agentInstances) {
|
|
148
|
+
inst.agent.setCompactStartCallback?.((sessionId) => {
|
|
149
|
+
processor.handleCompactStart(sessionId);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
122
152
|
// 创建消息队列
|
|
123
153
|
const messageQueue = new MessageQueue(async (message) => {
|
|
124
154
|
await processor.processMessage(message);
|
|
125
155
|
});
|
|
126
|
-
//
|
|
127
|
-
messageQueue.setInterruptCallback(async (sessionKey) => {
|
|
128
|
-
|
|
156
|
+
// 设置中断回调(精确中断正在处理的 agent)
|
|
157
|
+
messageQueue.setInterruptCallback(async (sessionKey, agentId) => {
|
|
158
|
+
const agent = agentMap.get(agentId || defaultAgent);
|
|
159
|
+
if (agent?.hasActiveStream(sessionKey)) {
|
|
160
|
+
await agent.interrupt(sessionKey);
|
|
161
|
+
}
|
|
129
162
|
});
|
|
163
|
+
messageQueue.setEventBus(eventBus);
|
|
130
164
|
// 回填 messageQueue 引用
|
|
131
165
|
cmdHandler.setMessageQueue(messageQueue);
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
166
|
+
// 默认策略
|
|
167
|
+
const defaultPolicy = {
|
|
168
|
+
canSwitchProject: (chatType, role) => chatType === 'private' || role === 'owner',
|
|
169
|
+
canListProjects: (chatType, role) => chatType === 'private' || role === 'owner',
|
|
170
|
+
canCreateSession: () => true,
|
|
171
|
+
canDeleteSession: (chatType, role) => chatType === 'private' || role === 'owner',
|
|
172
|
+
canImportCliSession: (chatType, role) => chatType === 'private' || role === 'owner',
|
|
173
|
+
messagePrefix: () => '',
|
|
174
|
+
showMiddleResult: () => true,
|
|
175
|
+
showIdleMonitor: () => true,
|
|
176
|
+
accumulateErrors: () => true,
|
|
177
|
+
};
|
|
178
|
+
// 注册渠道插件的 adapter 和 policy
|
|
179
|
+
for (const inst of channelInstances) {
|
|
180
|
+
// 设置项目路径提供器(如果需要)
|
|
181
|
+
if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
|
|
182
|
+
inst.channel.onProjectPathRequest(async (channelId) => {
|
|
183
|
+
const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, config.projects?.defaultPath || process.cwd(), undefined, undefined, undefined, undefined);
|
|
184
|
+
return path.isAbsolute(session.projectPath)
|
|
185
|
+
? session.projectPath
|
|
186
|
+
: path.resolve(process.cwd(), session.projectPath);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// 注册 adapter、policy 和 options(注入 channelType)
|
|
190
|
+
const opts = inst.channelType
|
|
191
|
+
? { ...inst.options, channelType: inst.channelType }
|
|
192
|
+
: inst.options;
|
|
193
|
+
processor.registerChannel(inst.adapter, inst.policy || defaultPolicy, opts);
|
|
194
|
+
cmdHandler.registerAdapter(inst.adapter);
|
|
195
|
+
cmdHandler.registerChannel(inst.adapter.channelName, inst.channel, inst.channelType);
|
|
196
|
+
if (inst.policy) {
|
|
197
|
+
cmdHandler.registerPolicy(inst.adapter.channelName, inst.policy);
|
|
198
|
+
}
|
|
199
|
+
// 注册交互回调:渠道收到用户操作后路由到 InteractionRouter
|
|
200
|
+
if (inst.adapter.onInteraction) {
|
|
201
|
+
inst.adapter.onInteraction((response) => {
|
|
202
|
+
interactionRouter.handle(response);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
156
205
|
}
|
|
157
|
-
// ──
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
}
|
|
206
|
+
// ── MessageBridge:Channel ↔ Core 消息桥梁 ──
|
|
207
|
+
const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
|
|
208
|
+
// ── 渠道消息注册 ──
|
|
209
|
+
// 连接插件系统的渠道
|
|
210
|
+
for (const inst of channelInstances) {
|
|
211
|
+
const channelType = inst.channelType || inst.adapter.channelName;
|
|
212
|
+
if (channelType === 'feishu') {
|
|
213
|
+
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType }) => {
|
|
214
|
+
await handler({
|
|
215
|
+
channel: channelType, channelId: chatId, content, images, chatType,
|
|
216
|
+
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
217
|
+
replyContext: rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined,
|
|
218
|
+
});
|
|
219
|
+
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
|
|
220
|
+
replyToMessageId: replyContext?.replyToMessageId,
|
|
221
|
+
replyInThread: true,
|
|
222
|
+
}), inst.adapter, channelType);
|
|
223
|
+
inst.channel.onRecall?.((messageId) => {
|
|
224
|
+
msgBridge.cancel(messageId);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (channelType === 'wechat') {
|
|
228
|
+
// 注入 EventBus(用于 channel:health 事件)
|
|
229
|
+
if (inst.channel.setEventBus) {
|
|
230
|
+
inst.channel.setEventBus(eventBus);
|
|
220
231
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (channelId, content, peerId, images, chatType) => {
|
|
233
|
+
handler({
|
|
234
|
+
channel: channelType,
|
|
235
|
+
channelId,
|
|
236
|
+
content,
|
|
237
|
+
images,
|
|
238
|
+
chatType: chatType || 'private',
|
|
239
|
+
peerId: peerId || '',
|
|
240
|
+
});
|
|
241
|
+
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
242
|
+
}
|
|
243
|
+
if (channelType === 'aun') {
|
|
244
|
+
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (opts) => {
|
|
245
|
+
handler({
|
|
246
|
+
channel: channelType,
|
|
247
|
+
channelId: opts.channelId,
|
|
248
|
+
content: opts.content,
|
|
249
|
+
chatType: opts.chatType || 'private',
|
|
250
|
+
peerId: opts.peerId || '',
|
|
251
|
+
peerName: opts.peerName,
|
|
252
|
+
messageId: opts.messageId,
|
|
253
|
+
mentions: opts.mentions,
|
|
254
|
+
threadId: opts.threadId,
|
|
255
|
+
replyContext: opts.replyContext,
|
|
256
|
+
});
|
|
257
|
+
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, replyContext), inst.adapter, channelType);
|
|
258
|
+
}
|
|
226
259
|
}
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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);
|
|
260
|
+
// ── 连接所有渠道 ──
|
|
261
|
+
const connected = await channelLoader.connectAll(channelInstances);
|
|
262
|
+
// 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
|
|
263
|
+
for (const inst of channelInstances) {
|
|
264
|
+
const channelType = inst.channelType || inst.adapter.channelName;
|
|
265
|
+
if (channelType === 'feishu' && 'preloadThreads' in inst.channel) {
|
|
266
|
+
const threadIds = sessionManager.getKnownThreadIds(inst.adapter.channelName);
|
|
267
|
+
inst.channel.preloadThreads(threadIds);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const name of connected) {
|
|
271
|
+
// 查找对应实例以获取 channelType
|
|
272
|
+
const inst = channelInstances.find(i => i.adapter.channelName === name);
|
|
273
|
+
const type = inst?.channelType || name;
|
|
274
|
+
eventBus.publish({
|
|
275
|
+
type: 'channel:connected',
|
|
276
|
+
channel: type.toLowerCase(),
|
|
277
|
+
channelName: name,
|
|
278
|
+
timestamp: Date.now()
|
|
262
279
|
});
|
|
263
280
|
}
|
|
264
|
-
// AUN
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
// AUN 重连失败通知:通过 channel:health 事件
|
|
282
|
+
for (const inst of channelInstances) {
|
|
283
|
+
const channelType = inst.channelType || inst.adapter.channelName;
|
|
284
|
+
if (channelType === 'aun' && inst.channel.setOnChannelDown) {
|
|
285
|
+
inst.channel.setOnChannelDown(() => {
|
|
286
|
+
eventBus.publish({
|
|
287
|
+
type: 'channel:health',
|
|
288
|
+
channel: channelType,
|
|
289
|
+
channelName: inst.adapter.channelName,
|
|
290
|
+
status: 'auth_error',
|
|
291
|
+
message: `⚠️ AUN 渠道 ${inst.adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
292
|
+
timestamp: Date.now(),
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// 统一 channel:health 跨通道通知(仅 auth_error)
|
|
298
|
+
// 按 (channelType, ownerId) 去重,避免同类型多实例重复通知
|
|
299
|
+
eventBus.subscribe('channel:health', (event) => {
|
|
300
|
+
if (event.type !== 'channel:health' || event.status !== 'auth_error')
|
|
301
|
+
return;
|
|
302
|
+
const sourceChannelType = event.channel;
|
|
303
|
+
const sourceChannelName = event.channelName || sourceChannelType;
|
|
304
|
+
const msg = event.message;
|
|
305
|
+
logger.error(`[ChannelHealth] ${sourceChannelName} auth_error: ${msg}`);
|
|
306
|
+
const notified = new Set(); // channelType:ownerId 去重
|
|
307
|
+
for (const other of channelInstances) {
|
|
308
|
+
const otherType = other.channelType || other.adapter.channelName;
|
|
309
|
+
if (otherType === sourceChannelType)
|
|
310
|
+
continue; // 跳过同类型通道
|
|
311
|
+
const ownerId = getOwner(config, other.adapter.channelName);
|
|
312
|
+
if (!ownerId)
|
|
313
|
+
continue;
|
|
314
|
+
const key = `${otherType}:${ownerId}`;
|
|
315
|
+
if (notified.has(key))
|
|
316
|
+
continue; // 同类型已通知过此 owner
|
|
317
|
+
notified.add(key);
|
|
318
|
+
other.adapter.sendText(ownerId, msg).catch(err => {
|
|
319
|
+
logger.error(`[ChannelHealth] Failed to notify ${other.adapter.channelName} owner:`, err);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
// 按 channelType 归组显示连接摘要
|
|
324
|
+
const connectedGroups = new Map();
|
|
325
|
+
for (const inst of channelInstances) {
|
|
326
|
+
const name = inst.adapter.channelName;
|
|
327
|
+
if (!connected.includes(name))
|
|
328
|
+
continue;
|
|
329
|
+
const type = inst.channelType || name;
|
|
330
|
+
if (!connectedGroups.has(type))
|
|
331
|
+
connectedGroups.set(type, []);
|
|
332
|
+
connectedGroups.get(type).push(name);
|
|
333
|
+
}
|
|
334
|
+
const channelSummary = Array.from(connectedGroups.entries())
|
|
335
|
+
.map(([type, names]) => names.length === 1 ? names[0] : `${type}[${names.join(', ')}]`)
|
|
336
|
+
.join(', ');
|
|
337
|
+
const totalCount = connected.length;
|
|
338
|
+
logger.info(`\n🚀 EvolClaw is running with ${totalCount} channel(s): ${channelSummary}\n`);
|
|
339
|
+
eventBus.publish({
|
|
340
|
+
type: 'system:started',
|
|
341
|
+
channels: connected.map(c => c.toLowerCase()),
|
|
342
|
+
timestamp: Date.now()
|
|
343
|
+
});
|
|
344
|
+
// 恢复重启前未完成的会话
|
|
345
|
+
const pendingSessions = sessionManager.getPendingProcessingSessions();
|
|
346
|
+
if (pendingSessions.length > 0) {
|
|
347
|
+
logger.info(`[Resume] Found ${pendingSessions.length} pending session(s) from before restart`);
|
|
348
|
+
for (const session of pendingSessions) {
|
|
349
|
+
if (!session.agentSessionId) {
|
|
350
|
+
sessionManager.clearProcessing(session.id);
|
|
351
|
+
continue;
|
|
273
352
|
}
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
await aun.sendMessage(sessionId, cmdResult);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
353
|
+
const agent = agentMap.get(session.agentId) || agentMap.get(defaultAgent);
|
|
354
|
+
if (!agent) {
|
|
355
|
+
sessionManager.clearProcessing(session.id);
|
|
356
|
+
continue;
|
|
281
357
|
}
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
358
|
+
logger.info(`[Resume] Resuming session: ${session.id} (agent: ${session.agentId})`);
|
|
359
|
+
const resumeMessage = {
|
|
360
|
+
channel: session.channel,
|
|
361
|
+
channelId: session.channelId,
|
|
362
|
+
content: '服务已重启,请继续之前未完成的任务。',
|
|
363
|
+
timestamp: Date.now(),
|
|
364
|
+
peerId: '',
|
|
365
|
+
threadId: session.threadId || undefined,
|
|
366
|
+
replyContext: session.metadata?.replyContext,
|
|
367
|
+
};
|
|
368
|
+
// 清除状态后入队(processMessage 会重新标记)
|
|
369
|
+
sessionManager.clearProcessing(session.id);
|
|
370
|
+
messageQueue.enqueue(session.id, resumeMessage, session.projectPath).catch(err => {
|
|
371
|
+
logger.error(`[Resume] Failed to resume session ${session.id}:`, err);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
287
374
|
}
|
|
288
|
-
//
|
|
289
|
-
const
|
|
290
|
-
|
|
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) {
|
|
375
|
+
// 重启通知:通过渠道 adapter 发送(channel-agnostic)
|
|
376
|
+
const pendingFile = path.join(resolvePaths().dataDir, 'restart-pending.json');
|
|
377
|
+
if (fs.existsSync(pendingFile)) {
|
|
296
378
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
379
|
+
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
|
380
|
+
const adapter = cmdHandler.getAdapter(pending.channel);
|
|
381
|
+
if (adapter) {
|
|
382
|
+
const replyContext = pending.rootId
|
|
383
|
+
? { replyToMessageId: pending.rootId, replyInThread: true }
|
|
384
|
+
: undefined;
|
|
385
|
+
await adapter.sendText(pending.channelId, '✅ 服务重启成功!', replyContext);
|
|
386
|
+
logger.info(`[Restart] Notification sent via ${pending.channel}`);
|
|
305
387
|
}
|
|
306
|
-
|
|
307
|
-
channels.push(name);
|
|
388
|
+
fs.unlinkSync(pendingFile);
|
|
308
389
|
}
|
|
309
|
-
catch (
|
|
310
|
-
logger.
|
|
311
|
-
if (error instanceof Error) {
|
|
312
|
-
logger.warn(` Reason: ${error.message}`);
|
|
313
|
-
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
logger.error('[Restart] Failed to send restart notification:', e);
|
|
314
392
|
}
|
|
315
393
|
}
|
|
316
|
-
logger.info(`\n🚀 EvolClaw is running with ${channels.length} channel(s): ${channels.join(', ')}\n`);
|
|
317
394
|
// 写入 ready 信号,供 restart-monitor 检测启动成功
|
|
318
395
|
const readySignalPath = resolvePaths().readySignal;
|
|
319
396
|
fs.writeFileSync(readySignalPath, String(Date.now()));
|
|
320
397
|
logger.info(`✓ Ready signal written: ${readySignalPath}`);
|
|
398
|
+
// IPC server — 供 CLI 查询实时状态
|
|
399
|
+
const ipcServer = new IpcServer(resolvePaths().socket, () => {
|
|
400
|
+
const channels = {};
|
|
401
|
+
const channelsByType = {};
|
|
402
|
+
for (const inst of channelInstances) {
|
|
403
|
+
const name = inst.adapter.channelName;
|
|
404
|
+
const status = inst.channel.getStatus?.() ?? { connected: true };
|
|
405
|
+
const channelType = inst.channelType || name;
|
|
406
|
+
channels[name] = { ...status, channelType };
|
|
407
|
+
if (!channelsByType[channelType])
|
|
408
|
+
channelsByType[channelType] = [];
|
|
409
|
+
channelsByType[channelType].push(name);
|
|
410
|
+
}
|
|
411
|
+
const snap = statsCollector.getSnapshot();
|
|
412
|
+
return {
|
|
413
|
+
pid: process.pid,
|
|
414
|
+
uptime: snap.uptimeMs,
|
|
415
|
+
channels,
|
|
416
|
+
channelsByType,
|
|
417
|
+
queue: {
|
|
418
|
+
pending: messageQueue.getGlobalQueueLength(),
|
|
419
|
+
processing: messageQueue.getGlobalProcessingCount(),
|
|
420
|
+
},
|
|
421
|
+
stats: {
|
|
422
|
+
received: snap.lastHour.received,
|
|
423
|
+
completed: snap.lastHour.completed,
|
|
424
|
+
errors: snap.lastHour.errors,
|
|
425
|
+
avgResponseMs: snap.lastHour.avgResponseMs,
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
ipcServer.start();
|
|
430
|
+
// 运行时配置文件监控
|
|
431
|
+
const configPath = resolvePaths().config;
|
|
432
|
+
fs.watchFile(configPath, { interval: 5000 }, (_curr, _prev) => {
|
|
433
|
+
let newConfig;
|
|
434
|
+
try {
|
|
435
|
+
newConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// JSON 解析失败 → 视为坏文件,备份内存中的好副本
|
|
439
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
440
|
+
const backupPath = path.join(resolvePaths().dataDir, `evolclaw-${ts}.json`);
|
|
441
|
+
fs.writeFileSync(backupPath, JSON.stringify(config, null, 2));
|
|
442
|
+
logger.warn(`[Config Watch] Config file is not valid JSON. In-memory snapshot saved to ${backupPath}`);
|
|
443
|
+
eventBus.publish({ type: 'config:corrupted', backupPath, reasons: ['Invalid JSON'] });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const result = validateConfigIntegrity(newConfig);
|
|
447
|
+
if (!result.valid) {
|
|
448
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
449
|
+
const backupPath = path.join(resolvePaths().dataDir, `evolclaw-${ts}.json`);
|
|
450
|
+
fs.writeFileSync(backupPath, JSON.stringify(config, null, 2));
|
|
451
|
+
logger.warn(`[Config Watch] Bad config write detected. Reasons: ${result.reasons.join('; ')}. In-memory snapshot saved to ${backupPath}`);
|
|
452
|
+
eventBus.publish({ type: 'config:corrupted', backupPath, reasons: result.reasons });
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
logger.debug(`[Config Watch] Config file modified, passes integrity check`);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
321
458
|
// 优雅关闭
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
459
|
+
let shutdownSignal = 'unknown';
|
|
460
|
+
const shutdown = async (signal) => {
|
|
461
|
+
if (signal)
|
|
462
|
+
shutdownSignal = signal;
|
|
463
|
+
const pid = process.pid;
|
|
464
|
+
const ppid = process.ppid;
|
|
465
|
+
logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
|
|
466
|
+
fs.unwatchFile(configPath);
|
|
467
|
+
ipcServer.stop();
|
|
468
|
+
eventBus.publish({
|
|
469
|
+
type: 'system:shutdown',
|
|
470
|
+
timestamp: Date.now()
|
|
471
|
+
});
|
|
472
|
+
// 断开插件系统的渠道
|
|
473
|
+
await channelLoader.disconnectAll(channelInstances);
|
|
474
|
+
for (const inst of channelInstances) {
|
|
475
|
+
const type = inst.channelType || inst.adapter.channelName;
|
|
476
|
+
eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
|
|
477
|
+
}
|
|
330
478
|
sessionManager.close();
|
|
331
479
|
logger.info('✓ Shutdown complete');
|
|
332
480
|
process.exit(0);
|
|
333
481
|
};
|
|
334
|
-
process.on('SIGINT', shutdown);
|
|
335
|
-
process.on('SIGTERM', shutdown);
|
|
482
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
483
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
336
484
|
}
|
|
337
485
|
main().catch((error) => {
|
|
486
|
+
const msg = `Fatal error: ${error?.stack || error}`;
|
|
338
487
|
logger.error('Fatal error:', error);
|
|
488
|
+
console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
|
|
339
489
|
process.exit(1);
|
|
340
490
|
});
|