evolclaw 3.2.0 → 3.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/CHANGELOG.md +53 -0
- package/README.md +7 -4
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -31
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1152 -140
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +58 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/outbox.js +14 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +869 -358
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +125 -154
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +23 -8
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +23 -4905
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +88 -83
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +82 -0
- package/dist/core/evolagent.js +17 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- package/dist/core/message/im-renderer.js +91 -51
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +73 -24
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +432 -94
- package/dist/core/message/message-queue.js +70 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +25 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +86 -26
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +334 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/data/error-dict.json +7 -0
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +174 -9
- package/dist/ipc.js +116 -1
- package/dist/utils/cross-platform.js +58 -5
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +77 -6
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/package.json +5 -6
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/command-handler.js +0 -3876
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /package/dist/{agents → eck}/kit-renderer.js +0 -0
package/dist/index.js
CHANGED
|
@@ -2,9 +2,8 @@ import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session
|
|
|
2
2
|
import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
|
|
3
3
|
import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
|
|
4
4
|
import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath } from './paths.js';
|
|
5
|
-
import { resolveAnthropicConfig } from './agents/
|
|
6
|
-
import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded, migrateProcessConfigIfNeeded } from './config-store.js';
|
|
7
|
-
import { loadEvolclawConfig } from './evolclaw-config.js';
|
|
5
|
+
import { resolveAnthropicConfig } from './agents/baseagent.js';
|
|
6
|
+
import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded, migrateProcessConfigIfNeeded, loadEvolclawConfig } from './config-store.js';
|
|
8
7
|
import { CONFIG_SCHEMA_VERSION } from './types.js';
|
|
9
8
|
import dotenv from 'dotenv';
|
|
10
9
|
import { SessionManager } from './core/session/session-manager.js';
|
|
@@ -21,7 +20,7 @@ import { MessageProcessor, buildEnvelope } from './core/message/message-processo
|
|
|
21
20
|
import { MessageQueue } from './core/message/message-queue.js';
|
|
22
21
|
import { MessageBridge } from './core/message/message-bridge.js';
|
|
23
22
|
import { MessageCache } from './core/message/message-cache.js';
|
|
24
|
-
import { CommandHandler } from './core/command-handler.js';
|
|
23
|
+
import { CommandHandler, isProcessLevelOwner } from './core/command/command-handler.js';
|
|
25
24
|
import { EventBus } from './core/event-bus.js';
|
|
26
25
|
import { StatsCollector } from './utils/stats.js';
|
|
27
26
|
import { AidStatsCollector } from './utils/stats.js';
|
|
@@ -33,9 +32,10 @@ import { EvolAgentRegistry } from './core/evolagent-registry.js';
|
|
|
33
32
|
import { buildReloadHooks } from './core/channel-loader.js';
|
|
34
33
|
import { IpcServer } from './ipc.js';
|
|
35
34
|
import { logger, setLogLevel } from './utils/logger.js';
|
|
35
|
+
import { fetchEcwebPairCode } from './utils/ecweb-pair.js';
|
|
36
36
|
import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
|
|
37
37
|
import { detectDuplicates } from './core/evolagent-registry.js';
|
|
38
|
-
import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './
|
|
38
|
+
import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './eck/kit-renderer.js';
|
|
39
39
|
import { initEck } from './eck/init.js';
|
|
40
40
|
import { TriggerManager } from './core/trigger/manager.js';
|
|
41
41
|
import { TriggerScheduler, calcNextFireAt } from './core/trigger/scheduler.js';
|
|
@@ -59,6 +59,11 @@ function summarizeOutboundPayload(payload) {
|
|
|
59
59
|
s.isFinal = payload.isFinal;
|
|
60
60
|
s.text = payload.text;
|
|
61
61
|
break;
|
|
62
|
+
case 'command.result':
|
|
63
|
+
case 'command.error':
|
|
64
|
+
case 'result.error':
|
|
65
|
+
s.text = payload.text;
|
|
66
|
+
break;
|
|
62
67
|
case 'result.file':
|
|
63
68
|
s.filePath = payload.filePath;
|
|
64
69
|
break;
|
|
@@ -72,6 +77,8 @@ function summarizeOutboundPayload(payload) {
|
|
|
72
77
|
s.interactionKind = payload.interaction?.kind?.kind;
|
|
73
78
|
break;
|
|
74
79
|
case 'status.started':
|
|
80
|
+
case 'status.progress':
|
|
81
|
+
case 'status.queued':
|
|
75
82
|
case 'status.completed':
|
|
76
83
|
case 'status.interrupted':
|
|
77
84
|
case 'status.error':
|
|
@@ -316,6 +323,40 @@ async function main() {
|
|
|
316
323
|
// Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
|
|
317
324
|
const aidStatsCollector = new AidStatsCollector(eventBus);
|
|
318
325
|
aidStatsCollector.setSessionsDir(paths.sessionsDir);
|
|
326
|
+
// 持久化网络流量到 message_events 表
|
|
327
|
+
aidStatsCollector.onMessage = (ev) => {
|
|
328
|
+
import('./core/stats/writer.js').then(({ insertMessageEvent }) => {
|
|
329
|
+
insertMessageEvent(paths.root, ev);
|
|
330
|
+
}).catch(() => { });
|
|
331
|
+
};
|
|
332
|
+
// 日聚合表 usage_daily:首次启动回填 + 每日自愈。
|
|
333
|
+
// 首次:表为空但明细非空时全量回填历史数据;之后靠 writer 写时增量维护。
|
|
334
|
+
// 自愈:每日全量重建一次,纠正任何写时漂移。
|
|
335
|
+
import('./core/stats/db.js').then(({ getDb, rebuildDailyRollup }) => {
|
|
336
|
+
const db = getDb(paths.root);
|
|
337
|
+
if (!db)
|
|
338
|
+
return;
|
|
339
|
+
try {
|
|
340
|
+
const daily = db.prepare('SELECT COUNT(*) AS n FROM usage_daily').get();
|
|
341
|
+
const events = db.prepare('SELECT COUNT(*) AS n FROM usage_events').get();
|
|
342
|
+
if (daily.n === 0 && events.n > 0) {
|
|
343
|
+
logger.info('[Stats] usage_daily 为空,回填历史数据…');
|
|
344
|
+
rebuildDailyRollup(paths.root);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
logger.warn(`[Stats] usage_daily 回填检测失败(非致命): ${e}`);
|
|
349
|
+
}
|
|
350
|
+
// 每日自愈(24h),纠正写时增量漂移。
|
|
351
|
+
setInterval(() => {
|
|
352
|
+
try {
|
|
353
|
+
rebuildDailyRollup(paths.root);
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
logger.warn(`[Stats] usage_daily 自愈失败(非致命): ${e}`);
|
|
357
|
+
}
|
|
358
|
+
}, 24 * 60 * 60 * 1000);
|
|
359
|
+
}).catch(() => { });
|
|
319
360
|
// 初始化 SessionManager(文件系统后端)
|
|
320
361
|
const sessionManager = new SessionManager(paths.sessionsDir, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId), (channel, userId) => agentRegistry.isAdmin(channel, userId));
|
|
321
362
|
// sessionMode 解析:从 channel 路由到具体 agent,按 agent.config.chatmode
|
|
@@ -449,7 +490,7 @@ async function main() {
|
|
|
449
490
|
const evol = evolagentName || primaryAgent.aid;
|
|
450
491
|
const agent = agentMap.get(`${evol}::${baseagent}`)
|
|
451
492
|
|| agentMap.get(primaryRunnerKey);
|
|
452
|
-
if (agent
|
|
493
|
+
if (agent) {
|
|
453
494
|
await agent.interrupt(sessionKey);
|
|
454
495
|
}
|
|
455
496
|
});
|
|
@@ -497,6 +538,17 @@ async function main() {
|
|
|
497
538
|
}
|
|
498
539
|
}
|
|
499
540
|
scheduler.setFireCallback((msg, trigger) => {
|
|
541
|
+
const onEnqueueFailed = (err) => {
|
|
542
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
543
|
+
logger.error(`[Trigger] Enqueue failed ${trigger.id}: ${error}`);
|
|
544
|
+
eventBus.publish({
|
|
545
|
+
type: 'trigger:failed', triggerId: trigger.id, name: trigger.name,
|
|
546
|
+
messageId: msg.messageId || '', error,
|
|
547
|
+
targetChannel: trigger.targetChannel, targetChannelId: trigger.targetChannelId,
|
|
548
|
+
fireTime: msg.triggerMeta?.fireTime ?? Date.now(), phase: 'enqueue',
|
|
549
|
+
});
|
|
550
|
+
scheduler.onTriggerComplete(trigger.id, 'failed');
|
|
551
|
+
};
|
|
500
552
|
if (trigger.targetSessionStrategy === 'current' && trigger.boundSessionId) {
|
|
501
553
|
const boundId = trigger.boundSessionId;
|
|
502
554
|
if (messageQueue.isProcessing(boundId)) {
|
|
@@ -509,12 +561,12 @@ async function main() {
|
|
|
509
561
|
return;
|
|
510
562
|
}
|
|
511
563
|
messageQueue.enqueue(boundId, msg, bound.projectPath, { interruptible: false })
|
|
512
|
-
.catch(
|
|
564
|
+
.catch(onEnqueueFailed);
|
|
513
565
|
});
|
|
514
566
|
return;
|
|
515
567
|
}
|
|
516
568
|
messageQueue.enqueue(`${msg.channel}:${msg.channelId}`, msg, primaryProjectPath, { interruptible: false })
|
|
517
|
-
.catch(
|
|
569
|
+
.catch(onEnqueueFailed);
|
|
518
570
|
});
|
|
519
571
|
// Subscribe to trigger:completed/failed/skipped to update cron inflight state
|
|
520
572
|
eventBus.subscribe('trigger:completed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'completed'));
|
|
@@ -523,6 +575,41 @@ async function main() {
|
|
|
523
575
|
if (ev.reason === 'interrupted')
|
|
524
576
|
scheduler.onTriggerComplete(ev.triggerId, 'interrupted');
|
|
525
577
|
});
|
|
578
|
+
// ── Trigger 失败/跳过通知:向 targetChannel 发送告警消息 ──
|
|
579
|
+
eventBus.subscribe('trigger:failed', (ev) => {
|
|
580
|
+
const adapter = processor.getAdapter(ev.targetChannel);
|
|
581
|
+
if (!adapter)
|
|
582
|
+
return;
|
|
583
|
+
const phaseLabel = ev.phase === 'enqueue' ? '入队' : '执行';
|
|
584
|
+
const timeStr = ev.fireTime ? new Date(ev.fireTime).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) : '未知';
|
|
585
|
+
const text = `⚠️ 定时任务 [${ev.name || ev.triggerId}] 执行失败\n阶段:${phaseLabel}\n原因:${ev.error}\n触发时间:${timeStr}`;
|
|
586
|
+
const envelope = {
|
|
587
|
+
taskId: `trigger-notify:${ev.triggerId}`,
|
|
588
|
+
channel: ev.targetChannel,
|
|
589
|
+
channelId: ev.targetChannelId,
|
|
590
|
+
agentName: 'system',
|
|
591
|
+
chatmode: 'interactive',
|
|
592
|
+
timestamp: Date.now(),
|
|
593
|
+
};
|
|
594
|
+
adapter.send(envelope, { kind: 'result.text', text, isFinal: true, format: 'plain' }).catch(() => { });
|
|
595
|
+
});
|
|
596
|
+
eventBus.subscribe('trigger:skipped', (ev) => {
|
|
597
|
+
if (ev.reason !== 'overlap')
|
|
598
|
+
return;
|
|
599
|
+
const adapter = processor.getAdapter(ev.targetChannel);
|
|
600
|
+
if (!adapter)
|
|
601
|
+
return;
|
|
602
|
+
const text = `⚠️ 定时任务 [${ev.name || ev.triggerId}] 本次跳过(上次执行仍在进行中)`;
|
|
603
|
+
const envelope = {
|
|
604
|
+
taskId: `trigger-notify:${ev.triggerId}`,
|
|
605
|
+
channel: ev.targetChannel,
|
|
606
|
+
channelId: ev.targetChannelId,
|
|
607
|
+
agentName: 'system',
|
|
608
|
+
chatmode: 'interactive',
|
|
609
|
+
timestamp: Date.now(),
|
|
610
|
+
};
|
|
611
|
+
adapter.send(envelope, { kind: 'result.text', text, isFinal: true, format: 'plain' }).catch(() => { });
|
|
612
|
+
});
|
|
526
613
|
// Note: only the primary agent's scheduler is wired to cmdHandler.
|
|
527
614
|
// Non-primary agent channels will receive "⚠️ 触发器功能未启用" when using /trigger.
|
|
528
615
|
// Full per-channel scheduler routing is a future improvement.
|
|
@@ -651,6 +738,7 @@ async function main() {
|
|
|
651
738
|
createdByPeerId: '__system__',
|
|
652
739
|
createdByChannel: '__system__',
|
|
653
740
|
fireCount: 0,
|
|
741
|
+
failCount: 0,
|
|
654
742
|
createdAt: Date.now(),
|
|
655
743
|
updatedAt: Date.now(),
|
|
656
744
|
};
|
|
@@ -700,6 +788,15 @@ async function main() {
|
|
|
700
788
|
});
|
|
701
789
|
}
|
|
702
790
|
// ── 控制 AID(daemon 进程身份):pureIdentity 接入 AUN,独立于 evolagent ──
|
|
791
|
+
// 证书缺失检测/生成在 CLI 侧(evolclaw start)完成。daemon 是后台进程无终端,
|
|
792
|
+
// 这里只做兜底:证书缺失时 warn 并继续(AUNChannel 内部后台重连),绝不阻塞。
|
|
793
|
+
if (evolclawCfg.aid) {
|
|
794
|
+
const aunPath = resolvePaths().root;
|
|
795
|
+
const certKey = path.join(aunPath, 'AIDs', evolclawCfg.aid, 'private', 'key.json');
|
|
796
|
+
if (!fs.existsSync(certKey)) {
|
|
797
|
+
logger.warn(`控制 AID 证书缺失:${evolclawCfg.aid}(AUN 控制通道后台重连;如需重建运行 evolclaw init)`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
703
800
|
let controlChannel;
|
|
704
801
|
if (evolclawCfg.aid) {
|
|
705
802
|
controlChannel = new AUNChannel({
|
|
@@ -719,6 +816,51 @@ async function main() {
|
|
|
719
816
|
catch (e) {
|
|
720
817
|
logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
|
|
721
818
|
}
|
|
819
|
+
// 控制 AID 接收 owner 指令:
|
|
820
|
+
// 1. /pair — ECWeb 配对码(文本快路径)
|
|
821
|
+
// 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
|
|
822
|
+
// 发送方身份由 AUN X.509 证书链验证,非 owner 完全静默。
|
|
823
|
+
controlChannel.onMessage(async (opts) => {
|
|
824
|
+
try {
|
|
825
|
+
if (!isProcessLevelOwner(opts.peerId, evolclawCfg.owners)) {
|
|
826
|
+
logger.debug(`控制 AID 收到非 owner 消息,忽略: from=${opts.peerId}`);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const text = (opts.content || '').trim();
|
|
830
|
+
if (text.toLowerCase() === '/pair') {
|
|
831
|
+
const port = evolclawCfg.ecweb?.port ?? 42705;
|
|
832
|
+
const pair = await fetchEcwebPairCode(port);
|
|
833
|
+
let reply;
|
|
834
|
+
if (pair) {
|
|
835
|
+
const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
|
|
836
|
+
reply = `ECWeb 配对码:${pair.code}(约 ${mins} 分钟内有效)\n在浏览器打开 ECWeb 后输入此码登录`;
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
reply = 'ECWeb 未运行或暂不可达。请在主机运行 ec watch web 启动后重试。';
|
|
840
|
+
}
|
|
841
|
+
await controlChannel.sendMessage(opts.channelId, reply);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
// menu.* JSON 路由:owner 已在上方校验,转交 execMenuForControl(fromControlChannel=true)
|
|
845
|
+
let parsed;
|
|
846
|
+
try {
|
|
847
|
+
parsed = JSON.parse(text);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
parsed = null;
|
|
851
|
+
}
|
|
852
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string' && parsed.type.startsWith('menu.')) {
|
|
853
|
+
const response = await cmdHandler.execMenuForControl(parsed, opts.peerId);
|
|
854
|
+
await controlChannel.sendMessage(opts.channelId, JSON.stringify(response));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
// owner 发的其他内容:提示可用指令
|
|
858
|
+
await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)');
|
|
859
|
+
}
|
|
860
|
+
catch (e) {
|
|
861
|
+
logger.warn(`控制 AID 消息处理失败: ${e?.message || e}`);
|
|
862
|
+
}
|
|
863
|
+
});
|
|
722
864
|
}
|
|
723
865
|
// 上线通知:延迟 1-3 秒后向 owner 发送上线消息(带 name + 工作目录)
|
|
724
866
|
// 需在配置中 debug.upmsg: true 手动开启
|
|
@@ -934,6 +1076,7 @@ async function main() {
|
|
|
934
1076
|
}, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
|
|
935
1077
|
// M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
|
|
936
1078
|
ipcServer.setAgentRegistry(agentRegistry);
|
|
1079
|
+
ipcServer.setMenuExecutor((payload) => cmdHandler.execMenuForEcweb(payload));
|
|
937
1080
|
// 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
|
|
938
1081
|
ipcServer.setAunAidProvider(() => {
|
|
939
1082
|
const out = [];
|
|
@@ -963,16 +1106,30 @@ async function main() {
|
|
|
963
1106
|
aidStatsCollector.setQueueStatsProvider((agentName) => ({
|
|
964
1107
|
processing: messageQueue.getProcessingCountByAgent(agentName),
|
|
965
1108
|
queued: messageQueue.getQueueLengthByAgent(agentName),
|
|
1109
|
+
muted: messageQueue.isAgentMuted(agentName),
|
|
966
1110
|
}));
|
|
967
1111
|
ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
|
|
968
1112
|
ipcServer.setAunAidStatsRecorder((params) => {
|
|
969
|
-
aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode);
|
|
1113
|
+
aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
|
|
970
1114
|
});
|
|
971
1115
|
// ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
|
|
972
1116
|
const reloadHooks = buildReloadHooks({
|
|
973
1117
|
channelLoader,
|
|
974
1118
|
channelInstances,
|
|
975
1119
|
registerChannelInstance,
|
|
1120
|
+
unregisterChannelInstance: (channelName) => {
|
|
1121
|
+
processor.unregisterChannel(channelName);
|
|
1122
|
+
cmdHandler.unregisterChannel(channelName);
|
|
1123
|
+
msgBridge.removeChannel(channelName);
|
|
1124
|
+
},
|
|
1125
|
+
onChannelStarted: (inst) => {
|
|
1126
|
+
// startChannel 重建渠道时重新注入 AidStatsCollector(与 hot-load 路径对齐)
|
|
1127
|
+
if (inst.channelType === 'aun') {
|
|
1128
|
+
const ch = inst.channel;
|
|
1129
|
+
if (typeof ch?.setAidStatsCollector === 'function')
|
|
1130
|
+
ch.setAidStatsCollector(aidStatsCollector);
|
|
1131
|
+
}
|
|
1132
|
+
},
|
|
976
1133
|
messageQueue,
|
|
977
1134
|
});
|
|
978
1135
|
// Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
|
|
@@ -986,6 +1143,11 @@ async function main() {
|
|
|
986
1143
|
const instances = await channelLoader.createForAgent(agent);
|
|
987
1144
|
for (const inst of instances) {
|
|
988
1145
|
registerChannelInstance(inst);
|
|
1146
|
+
if (inst.channelType === 'aun') {
|
|
1147
|
+
const ch = inst.channel;
|
|
1148
|
+
if (typeof ch?.setAidStatsCollector === 'function')
|
|
1149
|
+
ch.setAidStatsCollector(aidStatsCollector);
|
|
1150
|
+
}
|
|
989
1151
|
agent.channels.set(inst.adapter.channelKey, inst.adapter);
|
|
990
1152
|
channelInstances.push(inst);
|
|
991
1153
|
}
|
|
@@ -1062,6 +1224,8 @@ async function main() {
|
|
|
1062
1224
|
};
|
|
1063
1225
|
// I3: start IPC server LAST, after all hook setup, to eliminate race window
|
|
1064
1226
|
ipcServer.start();
|
|
1227
|
+
ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
|
|
1228
|
+
ipcServer.startCpuTracking();
|
|
1065
1229
|
// 配置 reload 走 IPC `evolagent.reload` 触发,不再用 watchFile。
|
|
1066
1230
|
// 双 rename 原子写下 watchFile 的语义会被破坏,且新结构有 N 个 config.json 要监控;
|
|
1067
1231
|
// 显式触发更可控。
|
|
@@ -1073,6 +1237,7 @@ async function main() {
|
|
|
1073
1237
|
const pid = process.pid;
|
|
1074
1238
|
const ppid = process.ppid;
|
|
1075
1239
|
logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
|
|
1240
|
+
ipcServer.stopCpuTracking();
|
|
1076
1241
|
ipcServer.stop();
|
|
1077
1242
|
eventBus.publish({
|
|
1078
1243
|
type: 'system:shutdown',
|
package/dist/ipc.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import { logger } from './utils/logger.js';
|
|
4
|
-
import { fileCache } from './core/
|
|
5
|
+
import { fileCache } from './core/daemon-file-cache.js';
|
|
5
6
|
const isWindows = process.platform === 'win32';
|
|
6
7
|
const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
|
|
7
8
|
export class IpcServer {
|
|
@@ -13,6 +14,18 @@ export class IpcServer {
|
|
|
13
14
|
aunAidProvider;
|
|
14
15
|
aunAidStatsProvider;
|
|
15
16
|
aunAidStatsRecorder;
|
|
17
|
+
menuExecutor;
|
|
18
|
+
statsProvider;
|
|
19
|
+
// CPU 占用追踪:IPC handler 是一次性同步调用,无法在响应里做 200ms 异步采样,
|
|
20
|
+
// 故用后台 1s interval 累积 process.cpuUsage() 增量,handler 直接读最近值。
|
|
21
|
+
// procCpuPercent = 本 daemon 进程占单核的百分比(可 >100% 仅当多核,已 clamp 到 100);
|
|
22
|
+
// sysCpuPercent = 整机所有核平均忙碌百分比(由 os.cpus() times 增量算出)。
|
|
23
|
+
lastCpuUsage = process.cpuUsage();
|
|
24
|
+
lastCpuTs = Date.now();
|
|
25
|
+
cpuPercent = 0; // 进程级
|
|
26
|
+
sysCpuPercent = 0; // 系统级
|
|
27
|
+
lastCpuTimes = null;
|
|
28
|
+
cpuTimer = null;
|
|
16
29
|
constructor(socketPath, getStatus, commandExecutor) {
|
|
17
30
|
this.socketPath = socketPath;
|
|
18
31
|
this.getStatus = getStatus;
|
|
@@ -22,6 +35,10 @@ export class IpcServer {
|
|
|
22
35
|
setAgentRegistry(registry) {
|
|
23
36
|
this.agentRegistry = registry;
|
|
24
37
|
}
|
|
38
|
+
/** Inject menu.* executor (ECWeb Control proxies menu requests through this) */
|
|
39
|
+
setMenuExecutor(executor) {
|
|
40
|
+
this.menuExecutor = executor;
|
|
41
|
+
}
|
|
25
42
|
/** Inject AUN AID state aggregator for aun-aids IPC handler */
|
|
26
43
|
setAunAidProvider(provider) {
|
|
27
44
|
this.aunAidProvider = provider;
|
|
@@ -34,6 +51,52 @@ export class IpcServer {
|
|
|
34
51
|
setAunAidStatsRecorder(recorder) {
|
|
35
52
|
this.aunAidStatsRecorder = recorder;
|
|
36
53
|
}
|
|
54
|
+
/** Inject global StatsSnapshot provider for monitor-snapshot IPC handler */
|
|
55
|
+
setStatsProvider(provider) {
|
|
56
|
+
this.statsProvider = provider;
|
|
57
|
+
}
|
|
58
|
+
/** Start the 1s background CPU sampling loop (for monitor-snapshot). Call after start(). */
|
|
59
|
+
startCpuTracking() {
|
|
60
|
+
if (this.cpuTimer)
|
|
61
|
+
return;
|
|
62
|
+
this.cpuTimer = setInterval(() => {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const elapsedUs = (now - this.lastCpuTs) * 1000; // wall time in microseconds
|
|
65
|
+
if (elapsedUs > 0) {
|
|
66
|
+
const usage = process.cpuUsage(this.lastCpuUsage); // delta since last sample
|
|
67
|
+
this.cpuPercent = Math.min(100, ((usage.user + usage.system) / elapsedUs) * 100);
|
|
68
|
+
}
|
|
69
|
+
this.lastCpuUsage = process.cpuUsage();
|
|
70
|
+
this.lastCpuTs = now;
|
|
71
|
+
// 系统级 CPU:os.cpus() 累计 times 的增量 → 整机平均忙碌率
|
|
72
|
+
try {
|
|
73
|
+
const cpus = os.cpus();
|
|
74
|
+
let idle = 0, total = 0;
|
|
75
|
+
for (const c of cpus) {
|
|
76
|
+
idle += c.times.idle;
|
|
77
|
+
total += c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq;
|
|
78
|
+
}
|
|
79
|
+
if (this.lastCpuTimes) {
|
|
80
|
+
const idleDelta = idle - this.lastCpuTimes.idle;
|
|
81
|
+
const totalDelta = total - this.lastCpuTimes.total;
|
|
82
|
+
if (totalDelta > 0) {
|
|
83
|
+
this.sysCpuPercent = Math.max(0, Math.min(100, (1 - idleDelta / totalDelta) * 100));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.lastCpuTimes = { idle, total };
|
|
87
|
+
}
|
|
88
|
+
catch { /* os.cpus() 理论上不抛 */ }
|
|
89
|
+
}, 1000);
|
|
90
|
+
// Don't keep the event loop alive for sampling alone.
|
|
91
|
+
this.cpuTimer.unref?.();
|
|
92
|
+
}
|
|
93
|
+
/** Stop the CPU sampling loop. */
|
|
94
|
+
stopCpuTracking() {
|
|
95
|
+
if (this.cpuTimer) {
|
|
96
|
+
clearInterval(this.cpuTimer);
|
|
97
|
+
this.cpuTimer = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
37
100
|
start() {
|
|
38
101
|
// Remove stale socket file (Unix only — named pipes auto-cleanup on process exit)
|
|
39
102
|
if (!isNamedPipe(this.socketPath)) {
|
|
@@ -206,6 +269,58 @@ export class IpcServer {
|
|
|
206
269
|
return { ok: false, error: e?.message || String(e) };
|
|
207
270
|
}
|
|
208
271
|
}
|
|
272
|
+
case 'menu.exec': {
|
|
273
|
+
if (!this.menuExecutor)
|
|
274
|
+
return { ok: false, error: 'menu.exec not configured' };
|
|
275
|
+
try {
|
|
276
|
+
const response = await this.menuExecutor(cmd.payload);
|
|
277
|
+
return { ok: true, response };
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
return { ok: false, error: e?.message ?? String(e) };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
case 'monitor-snapshot': {
|
|
284
|
+
// watch web Monitor 页用:进程级 + 系统级运行指标 + 全局 stats + per-agent 汇总。
|
|
285
|
+
const mem = process.memoryUsage();
|
|
286
|
+
const totalMem = os.totalmem();
|
|
287
|
+
const freeMem = os.freemem();
|
|
288
|
+
const aids = this.aunAidProvider ? this.aunAidProvider() : [];
|
|
289
|
+
const aidStats = this.aunAidStatsProvider ? this.aunAidStatsProvider() : [];
|
|
290
|
+
const statsMap = new Map(aidStats.map((s) => [s.aid, s]));
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
snapshot: {
|
|
294
|
+
ts: Date.now(),
|
|
295
|
+
uptimeMs: Math.round(process.uptime() * 1000),
|
|
296
|
+
cpuCount: os.cpus().length,
|
|
297
|
+
// 进程级:本 daemon 进程
|
|
298
|
+
memory: {
|
|
299
|
+
rss: mem.rss,
|
|
300
|
+
heapUsed: mem.heapUsed,
|
|
301
|
+
heapTotal: mem.heapTotal,
|
|
302
|
+
external: mem.external,
|
|
303
|
+
},
|
|
304
|
+
cpuPercent: Math.round(this.cpuPercent * 10) / 10,
|
|
305
|
+
// 系统级:整机
|
|
306
|
+
system: {
|
|
307
|
+
memTotal: totalMem,
|
|
308
|
+
memUsed: totalMem - freeMem,
|
|
309
|
+
memFree: freeMem,
|
|
310
|
+
cpuPercent: Math.round(this.sysCpuPercent * 10) / 10,
|
|
311
|
+
loadAvg: os.loadavg(), // [1m, 5m, 15m](Windows 恒 0)
|
|
312
|
+
},
|
|
313
|
+
stats: this.statsProvider ? this.statsProvider() : null,
|
|
314
|
+
agents: aids.map((a) => ({
|
|
315
|
+
aid: a.aid,
|
|
316
|
+
agentName: a.agentName,
|
|
317
|
+
channelName: a.channelName,
|
|
318
|
+
status: a.status,
|
|
319
|
+
stats: statsMap.get(a.aid) ?? null,
|
|
320
|
+
})),
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
209
324
|
default:
|
|
210
325
|
return { error: `unknown command: ${cmd.type}` };
|
|
211
326
|
}
|
|
@@ -5,15 +5,33 @@ import { promisify } from 'util';
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
7
|
export const isWindows = process.platform === 'win32';
|
|
8
|
+
const ENCODE_PATH_MAX = 200;
|
|
9
|
+
function encodePathHash(s) {
|
|
10
|
+
let h = 0;
|
|
11
|
+
for (let i = 0; i < s.length; i++)
|
|
12
|
+
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
13
|
+
return Math.abs(h).toString(36);
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* Encode project path as directory name (Claude SDK convention).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
17
|
+
* Mirrors the SDK's F0(L_(path)) logic:
|
|
18
|
+
* 1. path.resolve → realpath (if exists) → Unicode NFC
|
|
19
|
+
* 2. replace every non-alphanumeric character with '-'
|
|
20
|
+
* 3. truncate to 200 chars + hash suffix for long paths
|
|
21
|
+
* This must stay in sync with the SDK so evolclaw can locate session files.
|
|
13
22
|
*/
|
|
14
23
|
export function encodePath(projectPath) {
|
|
15
|
-
const
|
|
16
|
-
|
|
24
|
+
const resolved = path.resolve(projectPath);
|
|
25
|
+
let real = resolved;
|
|
26
|
+
try {
|
|
27
|
+
real = fs.realpathSync(resolved);
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
const nfc = real.normalize('NFC');
|
|
31
|
+
const encoded = nfc.replace(/[^a-zA-Z0-9]/g, '-');
|
|
32
|
+
if (encoded.length <= ENCODE_PATH_MAX)
|
|
33
|
+
return encoded;
|
|
34
|
+
return `${encoded.slice(0, ENCODE_PATH_MAX)}-${encodePathHash(nfc)}`;
|
|
17
35
|
}
|
|
18
36
|
/**
|
|
19
37
|
* Cross-platform process liveness check.
|
|
@@ -67,6 +85,41 @@ export function findProcesses(pattern) {
|
|
|
67
85
|
return [];
|
|
68
86
|
}
|
|
69
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Cross-platform: find PIDs listening on a TCP port.
|
|
90
|
+
* Used to clean up stale/orphaned listeners (e.g. manually-spawned ecweb
|
|
91
|
+
* that never registered a pid file) before binding the port again.
|
|
92
|
+
*/
|
|
93
|
+
export function findProcessByPort(port) {
|
|
94
|
+
const pids = new Set();
|
|
95
|
+
try {
|
|
96
|
+
if (isWindows) {
|
|
97
|
+
const result = spawnSync('netstat', ['-ano', '-p', 'TCP'], { encoding: 'utf-8', windowsHide: true });
|
|
98
|
+
const output = result.stdout || '';
|
|
99
|
+
for (const line of output.split('\n')) {
|
|
100
|
+
// 形如: TCP 0.0.0.0:42705 0.0.0.0:0 LISTENING 23004
|
|
101
|
+
if (!/LISTENING/i.test(line))
|
|
102
|
+
continue;
|
|
103
|
+
const m = line.match(/:(\d+)\s+\S+\s+LISTENING\s+(\d+)/i);
|
|
104
|
+
if (m && Number(m[1]) === port) {
|
|
105
|
+
const pid = Number(m[2]);
|
|
106
|
+
if (pid && pid !== process.pid)
|
|
107
|
+
pids.add(pid);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const output = execFileSync('lsof', ['-ti', `tcp:${port}`, '-sTCP:LISTEN'], { encoding: 'utf-8' }).trim();
|
|
113
|
+
for (const line of output.split('\n')) {
|
|
114
|
+
const pid = parseInt(line.trim(), 10);
|
|
115
|
+
if (pid && pid !== process.pid)
|
|
116
|
+
pids.add(pid);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { /* netstat/lsof missing or no match */ }
|
|
121
|
+
return [...pids];
|
|
122
|
+
}
|
|
70
123
|
export function getProcessInfo(pid) {
|
|
71
124
|
try {
|
|
72
125
|
if (isWindows) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { resolveGlobalPkg } from './npm-ops.js';
|
|
4
|
+
import { isWindows as platformIsWindows, resolveCommandPath } from './cross-platform.js';
|
|
5
|
+
const ECWEB_PKG = 'evolclaw-web';
|
|
6
|
+
const ECWEB_BIN = 'evolclaw-web';
|
|
7
|
+
function readEcwebPackageEntry(pkgJsonPath) {
|
|
8
|
+
try {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
10
|
+
const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[ECWEB_BIN];
|
|
11
|
+
if (!bin)
|
|
12
|
+
return null;
|
|
13
|
+
const entry = path.resolve(path.dirname(pkgJsonPath), bin);
|
|
14
|
+
return fs.existsSync(entry) ? entry : null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function packageJsonBesideNpmShim(commandPath, isWindows) {
|
|
21
|
+
if (!isWindows || !/\.(cmd|bat)$/i.test(commandPath))
|
|
22
|
+
return null;
|
|
23
|
+
const pkgJsonPath = path.join(path.dirname(commandPath), 'node_modules', ECWEB_PKG, 'package.json');
|
|
24
|
+
return fs.existsSync(pkgJsonPath) ? pkgJsonPath : null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve how to launch ecweb as a real background process.
|
|
28
|
+
*
|
|
29
|
+
* On Windows, npm bins are usually .cmd shims. Launching that shim through a
|
|
30
|
+
* shell can create a visible console window; launching the package's JS entry
|
|
31
|
+
* through the current Node executable avoids that wrapper entirely.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveEcwebLaunchCommand(ecwebArgs, opts = {}) {
|
|
34
|
+
const nodePath = opts.nodePath ?? process.execPath;
|
|
35
|
+
const installed = opts.installedPkg !== undefined ? opts.installedPkg : resolveGlobalPkg(ECWEB_PKG);
|
|
36
|
+
let entry = installed?.path ? readEcwebPackageEntry(installed.path) : null;
|
|
37
|
+
if (entry) {
|
|
38
|
+
return { command: nodePath, args: [entry, ...ecwebArgs], entry, source: 'package-bin' };
|
|
39
|
+
}
|
|
40
|
+
const commandPath = opts.commandPath !== undefined ? opts.commandPath : resolveCommandPath(ECWEB_BIN);
|
|
41
|
+
if (!commandPath)
|
|
42
|
+
return null;
|
|
43
|
+
const shimPkgJson = packageJsonBesideNpmShim(commandPath, opts.isWindows ?? platformIsWindows);
|
|
44
|
+
entry = shimPkgJson ? readEcwebPackageEntry(shimPkgJson) : null;
|
|
45
|
+
if (entry) {
|
|
46
|
+
return { command: nodePath, args: [entry, ...ecwebArgs], entry, source: 'package-bin' };
|
|
47
|
+
}
|
|
48
|
+
return { command: commandPath, args: ecwebArgs, source: 'command' };
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECWeb 配对码取码 helper(共享给 daemon 和 CLI)。
|
|
3
|
+
*
|
|
4
|
+
* 配对码是 ecweb 进程自己生成并持有的内部状态。daemon/CLI 通过 ecweb 的
|
|
5
|
+
* localhost-only HTTP 接口 GET /api/pair-code 取当前码(远程访问被 ecweb 403 拒绝)。
|
|
6
|
+
*/
|
|
7
|
+
/** 经 localhost 拉取 ecweb 当前配对码(仅本机可取)。失败返回 null。 */
|
|
8
|
+
export async function fetchEcwebPairCode(port) {
|
|
9
|
+
try {
|
|
10
|
+
const resp = await fetch(`http://127.0.0.1:${port}/api/pair-code`, {
|
|
11
|
+
signal: AbortSignal.timeout(2000),
|
|
12
|
+
});
|
|
13
|
+
if (!resp.ok)
|
|
14
|
+
return null;
|
|
15
|
+
return await resp.json();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -191,6 +191,22 @@ export function _setDictPath(p) {
|
|
|
191
191
|
_dictPath = p;
|
|
192
192
|
_lastMtime = 0; // 强制下次刷新
|
|
193
193
|
}
|
|
194
|
+
// ── 上下文过长检测(统一真相源)─────────────────────────────────────
|
|
195
|
+
//
|
|
196
|
+
// 覆盖所有已知的「上下文/输入超限」错误措辞,来源包括:
|
|
197
|
+
// - Anthropic 标准:prompt is too long / input is too long
|
|
198
|
+
// - OpenAI 兼容:context_length_exceeded / maximum context length
|
|
199
|
+
// - 网关自定义:reached its context window limit / context window limit
|
|
200
|
+
// - 中文:上下文过长
|
|
201
|
+
//
|
|
202
|
+
// ⚠️ 新增措辞统一往这里加,不要再在各模块本地复制正则。
|
|
203
|
+
export const CONTEXT_TOO_LONG_PATTERN = /prompt is too long|input is too long|context too long|context limit|context_length_exceeded|context_window_exceeded|context window limit|reached its context window|exceed(?:s|ed)? the context window|maximum context length|上下文过长/i;
|
|
204
|
+
/** 判断一段文本是否为「上下文过长」类错误。空文本返回 false。 */
|
|
205
|
+
export function isContextTooLongText(text) {
|
|
206
|
+
if (!text)
|
|
207
|
+
return false;
|
|
208
|
+
return CONTEXT_TOO_LONG_PATTERN.test(text);
|
|
209
|
+
}
|
|
194
210
|
// ── 错误分类 / 重试 / 消息 ──────────────────────────────────────────
|
|
195
211
|
export function classifyError(error) {
|
|
196
212
|
const msg = (error?.message || '').toLowerCase();
|
|
@@ -206,9 +222,7 @@ export function classifyError(error) {
|
|
|
206
222
|
return ErrorType.UNKNOWN;
|
|
207
223
|
}
|
|
208
224
|
// 内置兜底规则(结构性、稳定的错误模式)
|
|
209
|
-
if (msg.includes('
|
|
210
|
-
|| msg.includes('context limit') || msg.includes('input is too long')
|
|
211
|
-
|| msg.includes('上下文过长')) {
|
|
225
|
+
if (msg.includes('context_compact_failed') || isContextTooLongText(msg)) {
|
|
212
226
|
return ErrorType.CONTEXT_TOO_LONG;
|
|
213
227
|
}
|
|
214
228
|
if (msg.includes('invalid_model') || msg.includes('model_not_found')
|
|
@@ -285,8 +299,7 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
|
285
299
|
// 内置兜底规则(结构性错误)
|
|
286
300
|
const warnPrefix = includeEmoji ? '⚠️ ' : '';
|
|
287
301
|
const errPrefix = includeEmoji ? '❌ ' : '';
|
|
288
|
-
if (msg.includes('CONTEXT_COMPACT_FAILED') || msg
|
|
289
|
-
|| msg.includes('Context limit')) {
|
|
302
|
+
if (msg.includes('CONTEXT_COMPACT_FAILED') || isContextTooLongText(msg)) {
|
|
290
303
|
return `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
|
|
291
304
|
}
|
|
292
305
|
if (msg.includes('401') || msg.includes('authentication_error')) {
|