evolclaw 2.1.1 → 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.
- package/README.md +10 -3
- package/data/evolclaw.sample.json +9 -1
- package/dist/agents/claude-runner.js +612 -0
- package/dist/agents/codex-runner.js +310 -0
- package/dist/channels/aun.js +416 -9
- package/dist/channels/feishu.js +397 -104
- package/dist/channels/wechat.js +84 -2
- package/dist/cli.js +427 -126
- package/dist/config.js +102 -4
- package/dist/core/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/adapters/codex-session-file-adapter.js +196 -0
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +60 -0
- package/dist/core/command-handler.js +908 -304
- package/dist/core/event-bus.js +32 -0
- package/dist/core/ipc-server.js +71 -0
- package/dist/core/message-bridge.js +187 -0
- package/dist/core/message-processor.js +370 -227
- package/dist/core/message-queue.js +153 -29
- package/dist/core/permission.js +58 -0
- package/dist/core/session-file-adapter.js +7 -0
- package/dist/core/session-manager.js +571 -223
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/error-utils.js +4 -2
- package/dist/utils/init-feishu.js +2 -0
- package/dist/utils/init-wechat.js +2 -0
- package/dist/utils/init.js +285 -53
- package/dist/utils/ipc-client.js +36 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/{permission.js → permission-utils.js} +31 -3
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/session-file-health.js +11 -34
- package/dist/utils/stream-debouncer.js +122 -0
- package/dist/utils/stream-idle-monitor.js +1 -1
- package/package.json +3 -1
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-stream.js +0 -59
- package/dist/index.js.bak +0 -340
- package/dist/utils/markdown-to-feishu.js +0 -94
- /package/dist/utils/{platform.js → cross-platform.js} +0 -0
- /package/dist/{core → utils}/message-cache.js +0 -0
package/dist/index.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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,
|
|
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(
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
// ──
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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 (
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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) {
|
|
304
|
+
// 重启通知:通过渠道 adapter 发送(channel-agnostic)
|
|
305
|
+
const pendingFile = path.join(resolvePaths().dataDir, 'restart-pending.json');
|
|
306
|
+
if (fs.existsSync(pendingFile)) {
|
|
296
307
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
await instance.connect();
|
|
305
|
-
}
|
|
306
|
-
logger.info(`✓ ${name} connected`);
|
|
307
|
-
channels.push(name);
|
|
317
|
+
fs.unlinkSync(pendingFile);
|
|
308
318
|
}
|
|
309
|
-
catch (
|
|
310
|
-
logger.
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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() {
|