evolclaw 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +283 -95
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +232 -57
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +803 -247
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +217 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +61 -11
- package/dist/index.js +140 -57
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/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 +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
import { ClaudeSessionFileAdapter } from './core/adapters/claude-session-file-adapter.js';
|
|
2
|
-
import { CodexSessionFileAdapter } from './core/adapters/codex-session-file-adapter.js';
|
|
3
|
-
import {
|
|
4
|
-
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';
|
|
5
6
|
import { ClaudeAgentPlugin } from './agents/claude-runner.js';
|
|
6
7
|
import { CodexAgentPlugin } from './agents/codex-runner.js';
|
|
8
|
+
import { GeminiAgentPlugin } from './agents/gemini-runner.js';
|
|
7
9
|
import { FeishuChannelPlugin } from './channels/feishu.js';
|
|
8
10
|
import { WechatChannelPlugin } from './channels/wechat.js';
|
|
9
11
|
import { AUNChannelPlugin } from './channels/aun.js';
|
|
10
|
-
import { MessageProcessor } from './core/message-processor.js';
|
|
11
|
-
import { MessageQueue } from './core/message-queue.js';
|
|
12
|
-
import { MessageBridge } from './core/message-bridge.js';
|
|
13
|
-
import { MessageCache } from './
|
|
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';
|
|
14
16
|
import { CommandHandler } from './core/command-handler.js';
|
|
15
17
|
import { EventBus } from './core/event-bus.js';
|
|
16
|
-
import { StatsCollector } from './
|
|
18
|
+
import { StatsCollector } from './utils/stats-collector.js';
|
|
17
19
|
import { PermissionGateway } from './core/permission.js';
|
|
20
|
+
import { InteractionRouter } from './core/interaction-router.js';
|
|
18
21
|
import { ChannelLoader } from './core/channel-loader.js';
|
|
19
22
|
import { AgentLoader } from './core/agent-loader.js';
|
|
20
|
-
import { IpcServer } from './
|
|
23
|
+
import { IpcServer } from './ipc.js';
|
|
21
24
|
import { logger } from './utils/logger.js';
|
|
22
25
|
import path from 'path';
|
|
23
26
|
import fs from 'fs';
|
|
@@ -54,6 +57,8 @@ async function main() {
|
|
|
54
57
|
}
|
|
55
58
|
const anthropic = resolveAnthropicConfig(config);
|
|
56
59
|
logger.info('✓ Config loaded (API keys hidden)');
|
|
60
|
+
// Channel instance name uniqueness check
|
|
61
|
+
validateChannelInstanceNames(config);
|
|
57
62
|
if (anthropic.baseUrl) {
|
|
58
63
|
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
59
64
|
}
|
|
@@ -70,10 +75,12 @@ async function main() {
|
|
|
70
75
|
// 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
|
|
71
76
|
sessionManager.registerFileAdapter(new ClaudeSessionFileAdapter());
|
|
72
77
|
sessionManager.registerFileAdapter(new CodexSessionFileAdapter());
|
|
78
|
+
sessionManager.registerFileAdapter(new GeminiSessionFileAdapter());
|
|
73
79
|
// Agent 插件系统
|
|
74
80
|
const agentLoader = new AgentLoader();
|
|
75
81
|
agentLoader.register(new ClaudeAgentPlugin());
|
|
76
82
|
agentLoader.register(new CodexAgentPlugin());
|
|
83
|
+
agentLoader.register(new GeminiAgentPlugin());
|
|
77
84
|
const agentInstances = agentLoader.createAll(config, {
|
|
78
85
|
onSessionIdUpdate: async (sessionId, agentSessionId) => {
|
|
79
86
|
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
@@ -93,6 +100,8 @@ async function main() {
|
|
|
93
100
|
// 权限审批网关
|
|
94
101
|
const permissionGateway = new PermissionGateway();
|
|
95
102
|
permissionGateway.setEventBus(eventBus);
|
|
103
|
+
// 交互路由器
|
|
104
|
+
const interactionRouter = new InteractionRouter();
|
|
96
105
|
// 为所有支持权限的 agent 设置 gateway
|
|
97
106
|
for (const inst of agentInstances) {
|
|
98
107
|
inst.agent.setPermissionGateway?.(permissionGateway);
|
|
@@ -111,9 +120,12 @@ async function main() {
|
|
|
111
120
|
channelLoader.register(new AUNChannelPlugin());
|
|
112
121
|
const channelInstances = await channelLoader.createAll(config);
|
|
113
122
|
logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
|
|
123
|
+
// 启动迁移:将 sessions.channel 从 channelType 回填为实例名
|
|
124
|
+
sessionManager.migrateChannelToInstanceName();
|
|
114
125
|
// 创建命令处理器
|
|
115
126
|
const cmdHandler = new CommandHandler(sessionManager, agentMap, config, messageCache, eventBus, defaultAgent);
|
|
116
127
|
cmdHandler.setPermissionGateway(permissionGateway);
|
|
128
|
+
cmdHandler.setInteractionRouter(interactionRouter);
|
|
117
129
|
cmdHandler.setStatsCollector(statsCollector);
|
|
118
130
|
// 创建消息处理器
|
|
119
131
|
const processor = new MessageProcessor(agentMap, sessionManager, config, messageCache, eventBus, (content, channel, channelId, userId, threadId) => {
|
|
@@ -129,6 +141,8 @@ async function main() {
|
|
|
129
141
|
}, defaultAgent);
|
|
130
142
|
// 回填 processor 和 messageQueue 的引用
|
|
131
143
|
cmdHandler.setProcessor(processor);
|
|
144
|
+
// 设置交互路由器
|
|
145
|
+
processor.setInteractionRouter(interactionRouter);
|
|
132
146
|
// 设置 compact 开始回调(对所有支持的 agent)
|
|
133
147
|
for (const inst of agentInstances) {
|
|
134
148
|
inst.agent.setCompactStartCallback?.((sessionId) => {
|
|
@@ -166,18 +180,27 @@ async function main() {
|
|
|
166
180
|
// 设置项目路径提供器(如果需要)
|
|
167
181
|
if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
|
|
168
182
|
inst.channel.onProjectPathRequest(async (channelId) => {
|
|
169
|
-
const session = await sessionManager.getOrCreateSession(inst.adapter.
|
|
183
|
+
const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, config.projects?.defaultPath || process.cwd(), undefined, undefined, undefined, undefined);
|
|
170
184
|
return path.isAbsolute(session.projectPath)
|
|
171
185
|
? session.projectPath
|
|
172
186
|
: path.resolve(process.cwd(), session.projectPath);
|
|
173
187
|
});
|
|
174
188
|
}
|
|
175
|
-
// 注册 adapter、policy 和 options
|
|
176
|
-
|
|
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);
|
|
177
194
|
cmdHandler.registerAdapter(inst.adapter);
|
|
178
|
-
cmdHandler.registerChannel(inst.adapter.
|
|
195
|
+
cmdHandler.registerChannel(inst.adapter.channelName, inst.channel, inst.channelType);
|
|
179
196
|
if (inst.policy) {
|
|
180
|
-
cmdHandler.registerPolicy(inst.adapter.
|
|
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
|
+
});
|
|
181
204
|
}
|
|
182
205
|
}
|
|
183
206
|
// ── MessageBridge:Channel ↔ Core 消息桥梁 ──
|
|
@@ -185,86 +208,133 @@ async function main() {
|
|
|
185
208
|
// ── 渠道消息注册 ──
|
|
186
209
|
// 连接插件系统的渠道
|
|
187
210
|
for (const inst of channelInstances) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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,
|
|
192
216
|
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
193
|
-
replyContext: rootId ? { replyToMessageId: rootId, replyInThread:
|
|
217
|
+
replyContext: rootId ? { replyToMessageId: rootId, replyInThread: !!threadId } : undefined,
|
|
194
218
|
});
|
|
195
219
|
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
|
|
196
220
|
replyToMessageId: replyContext?.replyToMessageId,
|
|
197
|
-
replyInThread:
|
|
198
|
-
}), inst.adapter);
|
|
221
|
+
replyInThread: replyContext?.replyInThread,
|
|
222
|
+
}), inst.adapter, channelType);
|
|
199
223
|
inst.channel.onRecall?.((messageId) => {
|
|
200
224
|
msgBridge.cancel(messageId);
|
|
201
225
|
});
|
|
202
226
|
}
|
|
203
|
-
if (
|
|
204
|
-
|
|
227
|
+
if (channelType === 'wechat') {
|
|
228
|
+
// 注入 EventBus(用于 channel:health 事件)
|
|
229
|
+
if (inst.channel.setEventBus) {
|
|
230
|
+
inst.channel.setEventBus(eventBus);
|
|
231
|
+
}
|
|
232
|
+
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (channelId, content, peerId, images, chatType) => {
|
|
205
233
|
handler({
|
|
206
|
-
channel:
|
|
234
|
+
channel: channelType,
|
|
207
235
|
channelId,
|
|
208
236
|
content,
|
|
209
237
|
images,
|
|
210
238
|
chatType: chatType || 'private',
|
|
211
239
|
peerId: peerId || '',
|
|
212
240
|
});
|
|
213
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter);
|
|
241
|
+
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
214
242
|
}
|
|
215
|
-
if (
|
|
216
|
-
msgBridge.register(
|
|
243
|
+
if (channelType === 'aun') {
|
|
244
|
+
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (opts) => {
|
|
217
245
|
handler({
|
|
218
|
-
channel:
|
|
246
|
+
channel: channelType,
|
|
219
247
|
channelId: opts.channelId,
|
|
220
248
|
content: opts.content,
|
|
221
249
|
chatType: opts.chatType || 'private',
|
|
222
250
|
peerId: opts.peerId || '',
|
|
251
|
+
peerName: opts.peerName,
|
|
223
252
|
messageId: opts.messageId,
|
|
224
253
|
mentions: opts.mentions,
|
|
225
254
|
threadId: opts.threadId,
|
|
226
255
|
replyContext: opts.replyContext,
|
|
227
256
|
});
|
|
228
|
-
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, replyContext), inst.adapter);
|
|
257
|
+
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, replyContext), inst.adapter, channelType);
|
|
229
258
|
}
|
|
230
259
|
}
|
|
231
260
|
// ── 连接所有渠道 ──
|
|
232
261
|
const connected = await channelLoader.connectAll(channelInstances);
|
|
233
262
|
// 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
|
|
234
263
|
for (const inst of channelInstances) {
|
|
235
|
-
|
|
236
|
-
|
|
264
|
+
const channelType = inst.channelType || inst.adapter.channelName;
|
|
265
|
+
if (channelType === 'feishu' && 'preloadThreads' in inst.channel) {
|
|
266
|
+
const threadIds = sessionManager.getKnownThreadIds(inst.adapter.channelName);
|
|
237
267
|
inst.channel.preloadThreads(threadIds);
|
|
238
268
|
}
|
|
239
269
|
}
|
|
240
270
|
for (const name of connected) {
|
|
271
|
+
// 查找对应实例以获取 channelType
|
|
272
|
+
const inst = channelInstances.find(i => i.adapter.channelName === name);
|
|
273
|
+
const type = inst?.channelType || name;
|
|
241
274
|
eventBus.publish({
|
|
242
275
|
type: 'channel:connected',
|
|
243
|
-
channel:
|
|
276
|
+
channel: type.toLowerCase(),
|
|
277
|
+
channelName: name,
|
|
244
278
|
timestamp: Date.now()
|
|
245
279
|
});
|
|
246
280
|
}
|
|
247
|
-
// AUN
|
|
281
|
+
// AUN 重连失败通知:通过 channel:health 事件
|
|
248
282
|
for (const inst of channelInstances) {
|
|
249
|
-
|
|
283
|
+
const channelType = inst.channelType || inst.adapter.channelName;
|
|
284
|
+
if (channelType === 'aun' && inst.channel.setOnChannelDown) {
|
|
250
285
|
inst.channel.setOnChannelDown(() => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 去重(同类型只通知一次)
|
|
307
|
+
for (const other of channelInstances) {
|
|
308
|
+
const otherType = other.channelType || other.adapter.channelName;
|
|
309
|
+
if (otherType === sourceChannelType)
|
|
310
|
+
continue; // 跳过同类型通道
|
|
311
|
+
if (notified.has(otherType))
|
|
312
|
+
continue; // 同类型已通知过
|
|
313
|
+
const ownerId = getOwner(config, other.adapter.channelName);
|
|
314
|
+
if (!ownerId)
|
|
315
|
+
continue;
|
|
316
|
+
notified.add(otherType);
|
|
317
|
+
other.adapter.sendText(ownerId, msg).catch(err => {
|
|
318
|
+
logger.error(`[ChannelHealth] Failed to notify ${other.adapter.channelName} owner:`, err);
|
|
264
319
|
});
|
|
265
320
|
}
|
|
321
|
+
});
|
|
322
|
+
// 按 channelType 归组显示连接摘要
|
|
323
|
+
const connectedGroups = new Map();
|
|
324
|
+
for (const inst of channelInstances) {
|
|
325
|
+
const name = inst.adapter.channelName;
|
|
326
|
+
if (!connected.includes(name))
|
|
327
|
+
continue;
|
|
328
|
+
const type = inst.channelType || name;
|
|
329
|
+
if (!connectedGroups.has(type))
|
|
330
|
+
connectedGroups.set(type, []);
|
|
331
|
+
connectedGroups.get(type).push(name);
|
|
266
332
|
}
|
|
267
|
-
|
|
333
|
+
const channelSummary = Array.from(connectedGroups.entries())
|
|
334
|
+
.map(([type, names]) => names.length === 1 ? names[0] : `${type}[${names.join(', ')}]`)
|
|
335
|
+
.join(', ');
|
|
336
|
+
const totalCount = connected.length;
|
|
337
|
+
logger.info(`\n🚀 EvolClaw is running with ${totalCount} channel(s): ${channelSummary}\n`);
|
|
268
338
|
eventBus.publish({
|
|
269
339
|
type: 'system:started',
|
|
270
340
|
channels: connected.map(c => c.toLowerCase()),
|
|
@@ -309,7 +379,7 @@ async function main() {
|
|
|
309
379
|
const adapter = cmdHandler.getAdapter(pending.channel);
|
|
310
380
|
if (adapter) {
|
|
311
381
|
const replyContext = pending.rootId
|
|
312
|
-
? { replyToMessageId: pending.rootId, replyInThread:
|
|
382
|
+
? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
|
|
313
383
|
: undefined;
|
|
314
384
|
await adapter.sendText(pending.channelId, '✅ 服务重启成功!', replyContext);
|
|
315
385
|
logger.info(`[Restart] Notification sent via ${pending.channel}`);
|
|
@@ -327,15 +397,22 @@ async function main() {
|
|
|
327
397
|
// IPC server — 供 CLI 查询实时状态
|
|
328
398
|
const ipcServer = new IpcServer(resolvePaths().socket, () => {
|
|
329
399
|
const channels = {};
|
|
400
|
+
const channelsByType = {};
|
|
330
401
|
for (const inst of channelInstances) {
|
|
331
|
-
const name = inst.adapter.
|
|
332
|
-
|
|
402
|
+
const name = inst.adapter.channelName;
|
|
403
|
+
const status = inst.channel.getStatus?.() ?? { connected: true };
|
|
404
|
+
const channelType = inst.channelType || name;
|
|
405
|
+
channels[name] = { ...status, channelType };
|
|
406
|
+
if (!channelsByType[channelType])
|
|
407
|
+
channelsByType[channelType] = [];
|
|
408
|
+
channelsByType[channelType].push(name);
|
|
333
409
|
}
|
|
334
410
|
const snap = statsCollector.getSnapshot();
|
|
335
411
|
return {
|
|
336
412
|
pid: process.pid,
|
|
337
413
|
uptime: snap.uptimeMs,
|
|
338
414
|
channels,
|
|
415
|
+
channelsByType,
|
|
339
416
|
queue: {
|
|
340
417
|
pending: messageQueue.getGlobalQueueLength(),
|
|
341
418
|
processing: messageQueue.getGlobalProcessingCount(),
|
|
@@ -378,8 +455,13 @@ async function main() {
|
|
|
378
455
|
}
|
|
379
456
|
});
|
|
380
457
|
// 优雅关闭
|
|
381
|
-
|
|
382
|
-
|
|
458
|
+
let shutdownSignal = 'unknown';
|
|
459
|
+
const shutdown = async (signal) => {
|
|
460
|
+
if (signal)
|
|
461
|
+
shutdownSignal = signal;
|
|
462
|
+
const pid = process.pid;
|
|
463
|
+
const ppid = process.ppid;
|
|
464
|
+
logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
|
|
383
465
|
fs.unwatchFile(configPath);
|
|
384
466
|
ipcServer.stop();
|
|
385
467
|
eventBus.publish({
|
|
@@ -389,14 +471,15 @@ async function main() {
|
|
|
389
471
|
// 断开插件系统的渠道
|
|
390
472
|
await channelLoader.disconnectAll(channelInstances);
|
|
391
473
|
for (const inst of channelInstances) {
|
|
392
|
-
|
|
474
|
+
const type = inst.channelType || inst.adapter.channelName;
|
|
475
|
+
eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
|
|
393
476
|
}
|
|
394
477
|
sessionManager.close();
|
|
395
478
|
logger.info('✓ Shutdown complete');
|
|
396
479
|
process.exit(0);
|
|
397
480
|
};
|
|
398
|
-
process.on('SIGINT', shutdown);
|
|
399
|
-
process.on('SIGTERM', shutdown);
|
|
481
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
482
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
400
483
|
}
|
|
401
484
|
main().catch((error) => {
|
|
402
485
|
const msg = `Fatal error: ${error?.stack || error}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import { logger } from '
|
|
3
|
+
import { logger } from './utils/logger.js';
|
|
4
4
|
export class IpcServer {
|
|
5
5
|
socketPath;
|
|
6
6
|
getStatus;
|
|
@@ -69,3 +69,38 @@ export class IpcServer {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Query the running EvolClaw daemon via Unix socket.
|
|
74
|
+
* Returns null if the service is not running or the socket is unreachable.
|
|
75
|
+
*/
|
|
76
|
+
export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const conn = net.connect(socketPath);
|
|
79
|
+
let buf = '';
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
conn.destroy();
|
|
82
|
+
resolve(null);
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
conn.on('connect', () => {
|
|
85
|
+
conn.write(JSON.stringify(cmd) + '\n');
|
|
86
|
+
});
|
|
87
|
+
conn.on('data', (data) => {
|
|
88
|
+
buf += data.toString();
|
|
89
|
+
const idx = buf.indexOf('\n');
|
|
90
|
+
if (idx !== -1) {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
try {
|
|
93
|
+
resolve(JSON.parse(buf.slice(0, idx)));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
resolve(null);
|
|
97
|
+
}
|
|
98
|
+
conn.destroy();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
conn.on('error', () => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
resolve(null);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -78,15 +78,52 @@ export function getProcessInfo(pid) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
else {
|
|
81
|
-
const
|
|
81
|
+
const etimes = execFileSync('ps', ['-p', String(pid), '-o', 'etimes='], { encoding: 'utf-8' }).trim();
|
|
82
82
|
const cpu = execFileSync('ps', ['-p', String(pid), '-o', '%cpu='], { encoding: 'utf-8' }).trim();
|
|
83
83
|
const mem = execFileSync('ps', ['-p', String(pid), '-o', 'rss='], { encoding: 'utf-8' }).trim();
|
|
84
|
+
const uptime = formatUptime(parseInt(etimes, 10));
|
|
84
85
|
return { uptime, cpu, memory: mem };
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
catch { }
|
|
88
89
|
return {};
|
|
89
90
|
}
|
|
91
|
+
function formatUptime(totalSeconds) {
|
|
92
|
+
if (isNaN(totalSeconds) || totalSeconds < 0)
|
|
93
|
+
return 'unknown';
|
|
94
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
95
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
96
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
97
|
+
const seconds = totalSeconds % 60;
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (days > 0)
|
|
100
|
+
parts.push(`${days}d`);
|
|
101
|
+
if (hours > 0)
|
|
102
|
+
parts.push(`${hours}h`);
|
|
103
|
+
if (minutes > 0)
|
|
104
|
+
parts.push(`${minutes}m`);
|
|
105
|
+
if (parts.length === 0)
|
|
106
|
+
parts.push(`${seconds}s`);
|
|
107
|
+
return parts.join(' ');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Read a specific environment variable from a running process.
|
|
111
|
+
* Returns undefined if the process doesn't exist or the variable is not set.
|
|
112
|
+
* Linux: reads /proc/<pid>/environ; Windows: not supported (returns undefined).
|
|
113
|
+
*/
|
|
114
|
+
export function getProcessEnv(pid, varName) {
|
|
115
|
+
if (isWindows)
|
|
116
|
+
return undefined;
|
|
117
|
+
try {
|
|
118
|
+
const environ = fs.readFileSync(`/proc/${pid}/environ`, 'utf-8');
|
|
119
|
+
const prefix = `${varName}=`;
|
|
120
|
+
const entry = environ.split('\0').find(e => e.startsWith(prefix));
|
|
121
|
+
return entry ? entry.slice(prefix.length) : undefined;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
90
127
|
/**
|
|
91
128
|
* Cross-platform command existence check.
|
|
92
129
|
*/
|
|
@@ -2,11 +2,66 @@ export var ErrorType;
|
|
|
2
2
|
(function (ErrorType) {
|
|
3
3
|
ErrorType["SDK_TIMEOUT"] = "sdk_timeout";
|
|
4
4
|
ErrorType["API_ERROR"] = "api_error";
|
|
5
|
+
ErrorType["AUTH_ERROR"] = "auth_error";
|
|
5
6
|
ErrorType["FILE_CORRUPT"] = "file_corrupt";
|
|
6
7
|
ErrorType["STREAM_ERROR"] = "stream_error";
|
|
7
8
|
ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
|
|
8
9
|
ErrorType["UNKNOWN"] = "unknown";
|
|
9
10
|
})(ErrorType || (ErrorType = {}));
|
|
11
|
+
/**
|
|
12
|
+
* 错误来源前缀 — 区分基础设施异常 vs Agent 任务失败
|
|
13
|
+
*
|
|
14
|
+
* infra: 基础设施级(SDK 崩溃、API 不可用、文件损坏)— 应累计安全模式
|
|
15
|
+
* agent: Agent 任务级(权限拒绝、max turns、工具失败)— 仅统计,不累计安全模式
|
|
16
|
+
*/
|
|
17
|
+
export const ERROR_PREFIX = {
|
|
18
|
+
INFRA: 'infra',
|
|
19
|
+
AGENT: 'agent',
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 判断 Agent complete.subtype 是否属于系统级故障(应累计安全模式)
|
|
23
|
+
*
|
|
24
|
+
* 非系统级(用户操作或任务边界):
|
|
25
|
+
* - end_turn / max_turns: Agent 正常结束或达到轮次上限
|
|
26
|
+
* - permission_denied: 用户主动拒绝权限
|
|
27
|
+
* - stop: 用户主动停止
|
|
28
|
+
*
|
|
29
|
+
* 系统级(SDK/模型/平台故障):
|
|
30
|
+
* - error_model / error_tool_use / error_api 等:基础设施异常
|
|
31
|
+
* - 未知 subtype:保守地视为系统级
|
|
32
|
+
*
|
|
33
|
+
* terminalReason 提供更精确的判断(SDK 0.2.100+):
|
|
34
|
+
* - rapid_refill_breaker: API 限流,不是代码问题
|
|
35
|
+
* - tool_deferred: 工具延迟,不是错误
|
|
36
|
+
* - stop_hook_prevented: Stop hook 阻止,不是错误
|
|
37
|
+
* - aborted_streaming / aborted_tools: 中断,已有中断处理逻辑
|
|
38
|
+
*/
|
|
39
|
+
const NON_INFRA_SUBTYPES = new Set([
|
|
40
|
+
'end_turn',
|
|
41
|
+
'max_turns',
|
|
42
|
+
'permission_denied',
|
|
43
|
+
'stop',
|
|
44
|
+
]);
|
|
45
|
+
const NON_INFRA_TERMINAL_REASONS = new Set([
|
|
46
|
+
'rapid_refill_breaker',
|
|
47
|
+
'tool_deferred',
|
|
48
|
+
'stop_hook_prevented',
|
|
49
|
+
'aborted_streaming',
|
|
50
|
+
'aborted_tools',
|
|
51
|
+
]);
|
|
52
|
+
export function isInfraError(subtype, terminalReason) {
|
|
53
|
+
// terminalReason 优先级更高(更精确)
|
|
54
|
+
if (terminalReason && NON_INFRA_TERMINAL_REASONS.has(terminalReason)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!subtype)
|
|
58
|
+
return true; // 未知 subtype,保守视为系统级
|
|
59
|
+
return !NON_INFRA_SUBTYPES.has(subtype);
|
|
60
|
+
}
|
|
61
|
+
/** 为 errorType 添加来源前缀 */
|
|
62
|
+
export function prefixErrorType(prefix, errorType) {
|
|
63
|
+
return `${prefix}:${errorType}`;
|
|
64
|
+
}
|
|
10
65
|
export function classifyError(error) {
|
|
11
66
|
const msg = (error?.message || '').toLowerCase();
|
|
12
67
|
if (msg.includes('上下文过长') || msg.includes('context too long')
|
|
@@ -14,12 +69,21 @@ export function classifyError(error) {
|
|
|
14
69
|
|| msg.includes('prompt is too long') || msg.includes('context limit')) {
|
|
15
70
|
return ErrorType.CONTEXT_TOO_LONG;
|
|
16
71
|
}
|
|
72
|
+
// 认证错误(401 / Invalid API Key / key_not_found)— 不可恢复,不应触发安全模式
|
|
73
|
+
if (msg.includes('401') || msg.includes('invalid api key') || msg.includes('key_not_found')
|
|
74
|
+
|| msg.includes('authentication_error') || msg.includes('failed to authenticate')) {
|
|
75
|
+
return ErrorType.AUTH_ERROR;
|
|
76
|
+
}
|
|
17
77
|
if (msg.includes('timeout') || msg.includes('etimedout')) {
|
|
18
78
|
return ErrorType.SDK_TIMEOUT;
|
|
19
79
|
}
|
|
20
80
|
if (msg.includes('5') && (msg.includes('00') || msg.includes('02') || msg.includes('03') || msg.includes('04'))) {
|
|
21
81
|
return ErrorType.API_ERROR;
|
|
22
82
|
}
|
|
83
|
+
// "X is not valid JSON" — API 返回了非 JSON 响应(如算力池切换提示),属于 API 错误
|
|
84
|
+
if (msg.includes('is not valid json')) {
|
|
85
|
+
return ErrorType.API_ERROR;
|
|
86
|
+
}
|
|
23
87
|
if (msg.includes('enoent') || msg.includes('corrupt') || msg.includes('invalid json')) {
|
|
24
88
|
return ErrorType.FILE_CORRUPT;
|
|
25
89
|
}
|
|
@@ -28,7 +92,61 @@ export function classifyError(error) {
|
|
|
28
92
|
}
|
|
29
93
|
return ErrorType.UNKNOWN;
|
|
30
94
|
}
|
|
31
|
-
|
|
95
|
+
/**
|
|
96
|
+
* 判断错误是否可重试(暂时性 API 错误)
|
|
97
|
+
* 403 算力池切换、429 限流、5xx 服务端错误
|
|
98
|
+
* 注意:401 认证错误不可重试(API Key 无效不会因重试恢复)
|
|
99
|
+
*/
|
|
100
|
+
export function isRetryableError(error) {
|
|
101
|
+
const msg = error?.message || String(error);
|
|
102
|
+
const lower = msg.toLowerCase();
|
|
103
|
+
// 认证错误不可重试:重试不会恢复无效/缺失凭据
|
|
104
|
+
if (lower.includes('401')
|
|
105
|
+
|| lower.includes('invalid api key')
|
|
106
|
+
|| lower.includes('key_not_found')
|
|
107
|
+
|| lower.includes('authentication_error')
|
|
108
|
+
|| lower.includes('failed to authenticate')
|
|
109
|
+
|| (lower.includes('api error: 403') && (lower.includes('auth') || lower.includes('key') || lower.includes('token')))) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (msg.includes('API Error: 403'))
|
|
113
|
+
return true;
|
|
114
|
+
if (msg.includes('API Error: 429'))
|
|
115
|
+
return true;
|
|
116
|
+
if (msg.includes('API Error: 500'))
|
|
117
|
+
return true;
|
|
118
|
+
if (msg.includes('API Error: 502'))
|
|
119
|
+
return true;
|
|
120
|
+
if (msg.includes('API Error: 503'))
|
|
121
|
+
return true;
|
|
122
|
+
if (msg.includes('API Error: 504'))
|
|
123
|
+
return true;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
export function getErrorMessage(error, terminalReason) {
|
|
127
|
+
// terminalReason 提供更精确的错误提示(SDK 0.2.100+)
|
|
128
|
+
if (terminalReason) {
|
|
129
|
+
switch (terminalReason) {
|
|
130
|
+
case 'max_turns':
|
|
131
|
+
return '❌ 任务达到最大轮次限制,请简化需求或分步执行';
|
|
132
|
+
case 'prompt_too_long':
|
|
133
|
+
return '⚠️ 输入过长,请精简提问或使用 /compact 压缩上下文';
|
|
134
|
+
case 'rapid_refill_breaker':
|
|
135
|
+
return '⚠️ API 限流中,请稍后重试';
|
|
136
|
+
case 'context_compact_failed':
|
|
137
|
+
return '⚠️ 上下文过长,自动压缩失败,请手动输入 /compact 重试';
|
|
138
|
+
case 'model_error':
|
|
139
|
+
return '❌ 模型服务异常,请稍后重试';
|
|
140
|
+
case 'tool_error':
|
|
141
|
+
return '❌ 工具执行失败,请检查操作或重试';
|
|
142
|
+
case 'permission_denied':
|
|
143
|
+
return '❌ 权限被拒绝,操作已取消';
|
|
144
|
+
case 'aborted_streaming':
|
|
145
|
+
case 'aborted_tools':
|
|
146
|
+
return '❌ 任务已中断';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// 回退到原有的错误消息匹配逻辑
|
|
32
150
|
const msg = error?.message || String(error);
|
|
33
151
|
if (msg.includes('CONTEXT_COMPACT_FAILED')) {
|
|
34
152
|
return '⚠️ 上下文过长,自动压缩失败,请手动输入 /compact 重试';
|
|
@@ -38,10 +156,17 @@ export function getErrorMessage(error) {
|
|
|
38
156
|
return '⚠️ 上下文过长,自动压缩重试失败,请手动输入 /compact 重试';
|
|
39
157
|
}
|
|
40
158
|
if (msg.includes('API Error: 400')) {
|
|
41
|
-
return '
|
|
159
|
+
return '❌ 请求格式错误,请检查输入内容';
|
|
160
|
+
}
|
|
161
|
+
if (msg.includes('401') || msg.includes('Invalid API key') || msg.includes('key_not_found')
|
|
162
|
+
|| msg.includes('authentication_error')) {
|
|
163
|
+
return '❌ API Key 无效,请检查密钥配置。使用 /status 查看当前配置';
|
|
42
164
|
}
|
|
43
165
|
if (msg.includes('API Error: 500')) {
|
|
44
|
-
return '
|
|
166
|
+
return '❌ API 服务暂时不可用,请稍后重试';
|
|
167
|
+
}
|
|
168
|
+
if (msg.includes('API Error: 403')) {
|
|
169
|
+
return '❌ API 认证失败,请检查密钥配置或稍后重试';
|
|
45
170
|
}
|
|
46
171
|
if (msg.includes('API Error: 429')) {
|
|
47
172
|
return '⚠️ 请求过于频繁,请稍后再试';
|
|
@@ -50,7 +175,7 @@ export function getErrorMessage(error) {
|
|
|
50
175
|
return '⚠️ 请求超时,请重试';
|
|
51
176
|
}
|
|
52
177
|
if (msg.includes('permission') || msg.includes('im:resource')) {
|
|
53
|
-
return '
|
|
178
|
+
return '❌ 权限不足,请联系管理员配置应用权限';
|
|
54
179
|
}
|
|
55
|
-
return '
|
|
180
|
+
return '❌ 处理消息时出错,请稍后重试';
|
|
56
181
|
}
|