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.
Files changed (43) hide show
  1. package/README.md +49 -27
  2. package/data/evolclaw.sample.json +6 -3
  3. package/dist/agents/claude-runner.js +125 -52
  4. package/dist/agents/codex-runner.js +10 -5
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +283 -95
  7. package/dist/channels/feishu.js +556 -96
  8. package/dist/channels/wechat.js +98 -74
  9. package/dist/cli.js +232 -57
  10. package/dist/config.js +185 -31
  11. package/dist/core/channel-loader.js +11 -4
  12. package/dist/core/command-handler.js +803 -247
  13. package/dist/core/interaction-router.js +68 -0
  14. package/dist/core/message/message-bridge.js +217 -0
  15. package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
  16. package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
  17. package/dist/{utils → core/message}/stream-debouncer.js +1 -1
  18. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  19. package/dist/core/permission.js +212 -11
  20. package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
  21. package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
  22. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  23. package/dist/{utils → core/session}/session-file-health.js +1 -1
  24. package/dist/core/{session-manager.js → session/session-manager.js} +61 -11
  25. package/dist/index.js +140 -57
  26. package/dist/{core/ipc-server.js → ipc.js} +36 -1
  27. package/dist/types.js +3 -0
  28. package/dist/utils/cross-platform.js +38 -1
  29. package/dist/utils/error-utils.js +130 -5
  30. package/dist/utils/init-channel.js +649 -0
  31. package/dist/utils/init.js +55 -150
  32. package/dist/utils/logger.js +8 -3
  33. package/dist/utils/media-cache.js +207 -0
  34. package/dist/{core → utils}/stats-collector.js +16 -0
  35. package/package.json +3 -3
  36. package/dist/core/message-bridge.js +0 -187
  37. package/dist/utils/init-feishu.js +0 -263
  38. package/dist/utils/init-wechat.js +0 -172
  39. package/dist/utils/ipc-client.js +0 -36
  40. package/dist/utils/permission-utils.js +0 -71
  41. /package/dist/{utils → core/message}/message-cache.js +0 -0
  42. /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
  43. /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 { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, validateConfigIntegrity } from './config.js';
4
- import { SessionManager } from './core/session-manager.js';
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 './utils/message-cache.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';
14
16
  import { CommandHandler } from './core/command-handler.js';
15
17
  import { EventBus } from './core/event-bus.js';
16
- import { StatsCollector } from './core/stats-collector.js';
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 './core/ipc-server.js';
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.name, channelId, config.projects?.defaultPath || process.cwd(), undefined, undefined, undefined, undefined);
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
- processor.registerChannel(inst.adapter, inst.policy || defaultPolicy, inst.options);
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.name, inst.channel);
195
+ cmdHandler.registerChannel(inst.adapter.channelName, inst.channel, inst.channelType);
179
196
  if (inst.policy) {
180
- cmdHandler.registerPolicy(inst.adapter.name, 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
+ });
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
- 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,
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: true } : undefined,
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: true,
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 (inst.adapter.name === 'wechat') {
204
- msgBridge.register('wechat', (handler) => inst.channel.onMessage(async (channelId, content, peerId, images, chatType) => {
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: 'wechat',
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 (inst.adapter.name === 'aun') {
216
- msgBridge.register('aun', (handler) => inst.channel.onMessage(async (opts) => {
243
+ if (channelType === 'aun') {
244
+ msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (opts) => {
217
245
  handler({
218
- channel: 'aun',
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
- if (inst.adapter.name === 'feishu' && 'preloadThreads' in inst.channel) {
236
- const threadIds = sessionManager.getKnownThreadIds('feishu');
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: name.toLowerCase(),
276
+ channel: type.toLowerCase(),
277
+ channelName: name,
244
278
  timestamp: Date.now()
245
279
  });
246
280
  }
247
- // AUN 重连失败通知:通过其他渠道给 owner 发消息
281
+ // AUN 重连失败通知:通过 channel:health 事件
248
282
  for (const inst of channelInstances) {
249
- if (inst.adapter.name === 'aun' && inst.channel.setOnChannelDown) {
283
+ const channelType = inst.channelType || inst.adapter.channelName;
284
+ if (channelType === 'aun' && inst.channel.setOnChannelDown) {
250
285
  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
- });
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
- logger.info(`\n🚀 EvolClaw is running with ${connected.length} channel(s): ${connected.join(', ')}\n`);
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: true }
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.name;
332
- channels[name] = inst.channel.getStatus?.() ?? { connected: true };
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
- const shutdown = async () => {
382
- logger.info('\n\nShutting down gracefully...');
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
- eventBus.publish({ type: 'channel:disconnected', channel: inst.adapter.name, reason: 'shutdown' });
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 '../utils/logger.js';
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
@@ -1 +1,4 @@
1
+ // ── Channel config types ──
2
+ // Single-object form: `name` is optional (defaults to channel type name).
3
+ // Array form: `name` is required to distinguish instances.
1
4
  export {};
@@ -78,15 +78,52 @@ export function getProcessInfo(pid) {
78
78
  }
79
79
  }
80
80
  else {
81
- const uptime = execFileSync('ps', ['-p', String(pid), '-o', 'etime='], { encoding: 'utf-8' }).trim();
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
- export function getErrorMessage(error) {
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 '⚠️ API 服务暂时不可用,请稍后重试';
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
  }