evolclaw 3.3.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 +36 -0
- package/README.md +7 -3
- package/dist/agents/claude-runner.js +23 -27
- package/dist/agents/codex-runner.js +90 -6
- package/dist/agents/runner-types.js +30 -0
- package/dist/aun/outbox.js +14 -2
- package/dist/channels/aun.js +506 -108
- package/dist/channels/feishu.js +29 -5
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +15 -3
- 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 +12 -5027
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/core/channel-loader.js +4 -1
- 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 +81 -0
- package/dist/core/evolagent.js +16 -0
- package/dist/core/message/im-renderer.js +67 -49
- package/dist/core/message/message-bridge.js +30 -9
- package/dist/core/message/message-processor.js +200 -122
- package/dist/core/message/message-queue.js +68 -0
- package/dist/core/permission.js +16 -0
- package/dist/core/session/session-manager.js +59 -13
- package/dist/core/stats/db.js +20 -0
- package/dist/core/stats/writer.js +3 -3
- package/dist/data/error-dict.json +7 -0
- package/dist/index.js +49 -6
- package/dist/ipc.js +99 -0
- package/dist/utils/cross-platform.js +35 -0
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +63 -6
- package/kits/eck_manifest.json +0 -12
- package/package.json +2 -3
- package/dist/core/command-handler.js +0 -4235
- package/dist/core/message/response-depth.js +0 -56
- package/kits/templates/system-fragments/response-depth.md +0 -16
package/dist/core/permission.js
CHANGED
|
@@ -143,6 +143,12 @@ export function summarizeToolInput(toolName, input) {
|
|
|
143
143
|
/** 为 Edit 工具生成 diff 风格摘要 */
|
|
144
144
|
function formatEditSummary(input) {
|
|
145
145
|
const filePath = input.file_path || '';
|
|
146
|
+
const protocolDiff = typeof input.unified_diff === 'string' ? input.unified_diff
|
|
147
|
+
: typeof input.unifiedDiff === 'string' ? input.unifiedDiff
|
|
148
|
+
: typeof input.diff === 'string' ? input.diff
|
|
149
|
+
: '';
|
|
150
|
+
if (protocolDiff)
|
|
151
|
+
return formatProtocolDiffSummary(filePath, protocolDiff);
|
|
146
152
|
const oldStr = typeof input.old_string === 'string' ? input.old_string : '';
|
|
147
153
|
const newStr = typeof input.new_string === 'string' ? input.new_string : '';
|
|
148
154
|
if (!oldStr && !newStr)
|
|
@@ -215,6 +221,16 @@ function formatEditSummary(input) {
|
|
|
215
221
|
}
|
|
216
222
|
return `${filePath}\n\`\`\`\n${diffLines.join('\n')}\n\`\`\``;
|
|
217
223
|
}
|
|
224
|
+
/** 展示 runner/协议已返回的 unified diff;不在 EvolClaw 内重新计算 diff。 */
|
|
225
|
+
function formatProtocolDiffSummary(filePath, diff) {
|
|
226
|
+
const MAX_DIFF_LINES = 32;
|
|
227
|
+
const lines = diff.trimEnd().split('\n');
|
|
228
|
+
const displayLines = lines.length > MAX_DIFF_LINES
|
|
229
|
+
? [...lines.slice(0, MAX_DIFF_LINES), `...(省略 ${lines.length - MAX_DIFF_LINES} 行)`]
|
|
230
|
+
: lines;
|
|
231
|
+
const body = displayLines.join('\n');
|
|
232
|
+
return `${filePath}\n\`\`\`diff\n${body}\n\`\`\``;
|
|
233
|
+
}
|
|
218
234
|
export class PermissionGateway {
|
|
219
235
|
pending = new Map();
|
|
220
236
|
eventBus;
|
|
@@ -5,6 +5,7 @@ import { encodePath } from '../../utils/cross-platform.js';
|
|
|
5
5
|
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
|
|
6
6
|
import { sessionToFile, fileToSession } from './session-mapper.js';
|
|
7
7
|
import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
|
|
8
|
+
import { tryParseChannelKey } from '../channel-loader.js';
|
|
8
9
|
import path from 'path';
|
|
9
10
|
import fs from 'fs';
|
|
10
11
|
import os from 'os';
|
|
@@ -110,6 +111,26 @@ export class SessionManager {
|
|
|
110
111
|
}
|
|
111
112
|
return chatDirPath(this.sessionsDir, session.channelType, session.channelId, session.selfAID);
|
|
112
113
|
}
|
|
114
|
+
deriveChannelIdentity(channel, channelId) {
|
|
115
|
+
const parsed = tryParseChannelKey(channel);
|
|
116
|
+
if (parsed) {
|
|
117
|
+
return { channelType: parsed.type, selfAID: parsed.type === 'aun' ? parsed.selfAID : '' };
|
|
118
|
+
}
|
|
119
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
120
|
+
if (existingDir) {
|
|
121
|
+
const active = readJsonFile(path.join(existingDir, 'active.json'));
|
|
122
|
+
if (active) {
|
|
123
|
+
return { channelType: active.channelType || channel, selfAID: active.selfAID || '' };
|
|
124
|
+
}
|
|
125
|
+
for (const metaFile of scanMetaFiles(existingDir)) {
|
|
126
|
+
const meta = readLastJsonlLine(path.join(existingDir, metaFile));
|
|
127
|
+
if (meta) {
|
|
128
|
+
return { channelType: meta.channelType || channel, selfAID: meta.selfAID || '' };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { channelType: channel, selfAID: '' };
|
|
133
|
+
}
|
|
113
134
|
/** Public accessor: get the chat directory path for a session (for message log etc.) */
|
|
114
135
|
getChatDir(session) {
|
|
115
136
|
return this.resolveChatDirFromSession(session);
|
|
@@ -127,6 +148,30 @@ export class SessionManager {
|
|
|
127
148
|
* 用于不知道 channelType/selfAID 的 caller 在调用 resolveChatDir 前定位已有目录。
|
|
128
149
|
*/
|
|
129
150
|
findExistingChatDir(channel, channelId) {
|
|
151
|
+
const parsed = tryParseChannelKey(channel);
|
|
152
|
+
if (parsed) {
|
|
153
|
+
const exactDir = this.resolveChatDir(channel, channelId, parsed.type, parsed.type === 'aun' ? parsed.selfAID : undefined);
|
|
154
|
+
const active = readJsonFile(path.join(exactDir, 'active.json'));
|
|
155
|
+
if (active && active.channel === channel)
|
|
156
|
+
return exactDir;
|
|
157
|
+
for (const mf of scanMetaFiles(exactDir)) {
|
|
158
|
+
const meta = readLastJsonlLine(path.join(exactDir, mf));
|
|
159
|
+
if (meta && meta.channel === channel)
|
|
160
|
+
return exactDir;
|
|
161
|
+
}
|
|
162
|
+
const threadsDir = path.join(exactDir, '_threads');
|
|
163
|
+
if (fs.existsSync(threadsDir)) {
|
|
164
|
+
const threadMetas = scanMetaFiles(threadsDir);
|
|
165
|
+
for (const mf of threadMetas) {
|
|
166
|
+
const meta = readLastJsonlLine(path.join(threadsDir, mf));
|
|
167
|
+
if (meta && meta.channel === channel)
|
|
168
|
+
return exactDir;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (fs.existsSync(exactDir))
|
|
172
|
+
return exactDir;
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
130
175
|
const dirs = scanChatDirs(this.sessionsDir);
|
|
131
176
|
for (const d of dirs) {
|
|
132
177
|
if (d.channelId !== channelId)
|
|
@@ -686,7 +731,8 @@ export class SessionManager {
|
|
|
686
731
|
const agentId = currentAgentId || 'claude';
|
|
687
732
|
logger.info(`[SessionManager] switchProject: channel=${channel} channelId=${channelId} newPath=${newProjectPath} agent=${agentId}`);
|
|
688
733
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
689
|
-
const
|
|
734
|
+
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
735
|
+
const chatDir = this.ensureResolvedChatDir(channel, channelId, identity.channelType, identity.selfAID);
|
|
690
736
|
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
691
737
|
const target = allSessions
|
|
692
738
|
.filter(s => s.projectPath === newProjectPath && (s.agentId || 'claude') === agentId && !s.threadId)
|
|
@@ -699,8 +745,8 @@ export class SessionManager {
|
|
|
699
745
|
}
|
|
700
746
|
// Derive selfAID and channelType from existing sessions in this chatDir
|
|
701
747
|
const existingAny = allSessions[0];
|
|
702
|
-
const selfAID = existingAny?.selfAID ||
|
|
703
|
-
const channelType = existingAny?.channelType ||
|
|
748
|
+
const selfAID = existingAny?.selfAID || identity.selfAID;
|
|
749
|
+
const channelType = existingAny?.channelType || identity.channelType;
|
|
704
750
|
const session = {
|
|
705
751
|
id: generateSessionId(),
|
|
706
752
|
channel,
|
|
@@ -750,12 +796,10 @@ export class SessionManager {
|
|
|
750
796
|
}
|
|
751
797
|
async switchAgent(channel, channelId, projectPath, newAgentId) {
|
|
752
798
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
const
|
|
757
|
-
const channelType = existingAny?.channelType || channel;
|
|
758
|
-
const selfAID = existingAny?.selfAID || '';
|
|
799
|
+
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
800
|
+
// Derive channelType/selfAID from existing sessions; fall back to parsed channel key.
|
|
801
|
+
const channelType = identity.channelType;
|
|
802
|
+
const selfAID = identity.selfAID;
|
|
759
803
|
const chatDir = this.ensureResolvedChatDir(channel, channelId, channelType, selfAID);
|
|
760
804
|
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
761
805
|
const target = allSessions
|
|
@@ -1030,8 +1074,9 @@ export class SessionManager {
|
|
|
1030
1074
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
1031
1075
|
// Derive selfAID and channelType from existing sessions
|
|
1032
1076
|
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
1033
|
-
|
|
1034
|
-
let
|
|
1077
|
+
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
1078
|
+
let channelType = identity.channelType;
|
|
1079
|
+
let selfAID = identity.selfAID;
|
|
1035
1080
|
let inheritedRole = 'guest';
|
|
1036
1081
|
if (existingDir) {
|
|
1037
1082
|
const active = readJsonFile(path.join(existingDir, 'active.json'));
|
|
@@ -1160,8 +1205,9 @@ export class SessionManager {
|
|
|
1160
1205
|
const name = fileInfo.title || `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
|
|
1161
1206
|
// Derive selfAID and channelType from existing sessions
|
|
1162
1207
|
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
1163
|
-
|
|
1164
|
-
let
|
|
1208
|
+
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
1209
|
+
let channelType = identity.channelType;
|
|
1210
|
+
let selfAID = identity.selfAID;
|
|
1165
1211
|
if (existingDir) {
|
|
1166
1212
|
const active = readJsonFile(path.join(existingDir, 'active.json'));
|
|
1167
1213
|
if (active) {
|
package/dist/core/stats/db.js
CHANGED
|
@@ -176,6 +176,9 @@ function _initTables(db) {
|
|
|
176
176
|
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
177
177
|
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
178
178
|
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
179
|
+
context_tokens INTEGER,
|
|
180
|
+
max_tokens INTEGER,
|
|
181
|
+
auto_compact_tokens INTEGER,
|
|
179
182
|
degraded INTEGER NOT NULL DEFAULT 0
|
|
180
183
|
);
|
|
181
184
|
CREATE INDEX IF NOT EXISTS idx_mc_task ON model_calls(task_id);
|
|
@@ -194,6 +197,23 @@ function _initTables(db) {
|
|
|
194
197
|
catch (e) {
|
|
195
198
|
logger.warn(`[StatsDB] usage_daily peer_type 迁移检测失败: ${e}`);
|
|
196
199
|
}
|
|
200
|
+
// 轻量迁移:model_calls 早期只记录 usage 原始 token,task.start 自动压缩需要
|
|
201
|
+
// 使用上轮真实 context/max/compact window,避免按模型名猜窗口。
|
|
202
|
+
try {
|
|
203
|
+
const cols = db.prepare(`PRAGMA table_info(model_calls)`).all();
|
|
204
|
+
if (!cols.some(c => c.name === 'context_tokens')) {
|
|
205
|
+
db.exec(`ALTER TABLE model_calls ADD COLUMN context_tokens INTEGER`);
|
|
206
|
+
}
|
|
207
|
+
if (!cols.some(c => c.name === 'max_tokens')) {
|
|
208
|
+
db.exec(`ALTER TABLE model_calls ADD COLUMN max_tokens INTEGER`);
|
|
209
|
+
}
|
|
210
|
+
if (!cols.some(c => c.name === 'auto_compact_tokens')) {
|
|
211
|
+
db.exec(`ALTER TABLE model_calls ADD COLUMN auto_compact_tokens INTEGER`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
logger.warn(`[StatsDB] model_calls context window 迁移检测失败: ${e}`);
|
|
216
|
+
}
|
|
197
217
|
}
|
|
198
218
|
/**
|
|
199
219
|
* 全量重建 usage_daily:从 usage_events 明细按天聚合重算。
|
|
@@ -96,12 +96,12 @@ export function insertModelCalls(evolclawHome, rows) {
|
|
|
96
96
|
INSERT INTO model_calls
|
|
97
97
|
(ts, task_id, session_id, agent_session_id, agent_aid, peer_key, call_index, model,
|
|
98
98
|
request_id, message_id, input_tokens, output_tokens, cache_creation_tokens,
|
|
99
|
-
cache_read_tokens, degraded)
|
|
100
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
99
|
+
cache_read_tokens, context_tokens, max_tokens, auto_compact_tokens, degraded)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
101
101
|
`);
|
|
102
102
|
db.exec('BEGIN');
|
|
103
103
|
for (const r of rows) {
|
|
104
|
-
stmt.run(r.ts, r.task_id, r.session_id ?? null, r.agent_session_id ?? null, r.agent_aid, r.peer_key, r.call_index, r.model, r.request_id ?? null, r.message_id ?? null, r.input_tokens, r.output_tokens, r.cache_creation_tokens, r.cache_read_tokens, r.degraded);
|
|
104
|
+
stmt.run(r.ts, r.task_id, r.session_id ?? null, r.agent_session_id ?? null, r.agent_aid, r.peer_key, r.call_index, r.model, r.request_id ?? null, r.message_id ?? null, r.input_tokens, r.output_tokens, r.cache_creation_tokens, r.cache_read_tokens, r.context_tokens ?? null, r.max_tokens ?? null, r.auto_compact_tokens ?? null, r.degraded);
|
|
105
105
|
}
|
|
106
106
|
db.exec('COMMIT');
|
|
107
107
|
}
|
|
@@ -95,6 +95,13 @@
|
|
|
95
95
|
"type": "api_error",
|
|
96
96
|
"message": "⚠️ API 返回异常响应,正在重试..."
|
|
97
97
|
},
|
|
98
|
+
{
|
|
99
|
+
"id": "json-parse-error",
|
|
100
|
+
"match": "json parse error",
|
|
101
|
+
"action": "retry",
|
|
102
|
+
"type": "api_error",
|
|
103
|
+
"message": "⚠️ API 返回 JSON 解析异常,正在重试..."
|
|
104
|
+
},
|
|
98
105
|
{
|
|
99
106
|
"id": "feishu-permission",
|
|
100
107
|
"match": "im:resource",
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { MessageProcessor, buildEnvelope } from './core/message/message-processo
|
|
|
20
20
|
import { MessageQueue } from './core/message/message-queue.js';
|
|
21
21
|
import { MessageBridge } from './core/message/message-bridge.js';
|
|
22
22
|
import { MessageCache } from './core/message/message-cache.js';
|
|
23
|
-
import { CommandHandler, isProcessLevelOwner } from './core/command-handler.js';
|
|
23
|
+
import { CommandHandler, isProcessLevelOwner } from './core/command/command-handler.js';
|
|
24
24
|
import { EventBus } from './core/event-bus.js';
|
|
25
25
|
import { StatsCollector } from './utils/stats.js';
|
|
26
26
|
import { AidStatsCollector } from './utils/stats.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':
|
|
@@ -809,9 +816,10 @@ async function main() {
|
|
|
809
816
|
catch (e) {
|
|
810
817
|
logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
|
|
811
818
|
}
|
|
812
|
-
// 控制 AID 接收 owner
|
|
813
|
-
//
|
|
814
|
-
//
|
|
819
|
+
// 控制 AID 接收 owner 指令:
|
|
820
|
+
// 1. /pair — ECWeb 配对码(文本快路径)
|
|
821
|
+
// 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
|
|
822
|
+
// 发送方身份由 AUN X.509 证书链验证,非 owner 完全静默。
|
|
815
823
|
controlChannel.onMessage(async (opts) => {
|
|
816
824
|
try {
|
|
817
825
|
if (!isProcessLevelOwner(opts.peerId, evolclawCfg.owners)) {
|
|
@@ -833,7 +841,20 @@ async function main() {
|
|
|
833
841
|
await controlChannel.sendMessage(opts.channelId, reply);
|
|
834
842
|
return;
|
|
835
843
|
}
|
|
836
|
-
//
|
|
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 发的其他内容:提示可用指令
|
|
837
858
|
await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)');
|
|
838
859
|
}
|
|
839
860
|
catch (e) {
|
|
@@ -1085,16 +1106,30 @@ async function main() {
|
|
|
1085
1106
|
aidStatsCollector.setQueueStatsProvider((agentName) => ({
|
|
1086
1107
|
processing: messageQueue.getProcessingCountByAgent(agentName),
|
|
1087
1108
|
queued: messageQueue.getQueueLengthByAgent(agentName),
|
|
1109
|
+
muted: messageQueue.isAgentMuted(agentName),
|
|
1088
1110
|
}));
|
|
1089
1111
|
ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
|
|
1090
1112
|
ipcServer.setAunAidStatsRecorder((params) => {
|
|
1091
|
-
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');
|
|
1092
1114
|
});
|
|
1093
1115
|
// ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
|
|
1094
1116
|
const reloadHooks = buildReloadHooks({
|
|
1095
1117
|
channelLoader,
|
|
1096
1118
|
channelInstances,
|
|
1097
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
|
+
},
|
|
1098
1133
|
messageQueue,
|
|
1099
1134
|
});
|
|
1100
1135
|
// Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
|
|
@@ -1108,6 +1143,11 @@ async function main() {
|
|
|
1108
1143
|
const instances = await channelLoader.createForAgent(agent);
|
|
1109
1144
|
for (const inst of instances) {
|
|
1110
1145
|
registerChannelInstance(inst);
|
|
1146
|
+
if (inst.channelType === 'aun') {
|
|
1147
|
+
const ch = inst.channel;
|
|
1148
|
+
if (typeof ch?.setAidStatsCollector === 'function')
|
|
1149
|
+
ch.setAidStatsCollector(aidStatsCollector);
|
|
1150
|
+
}
|
|
1111
1151
|
agent.channels.set(inst.adapter.channelKey, inst.adapter);
|
|
1112
1152
|
channelInstances.push(inst);
|
|
1113
1153
|
}
|
|
@@ -1184,6 +1224,8 @@ async function main() {
|
|
|
1184
1224
|
};
|
|
1185
1225
|
// I3: start IPC server LAST, after all hook setup, to eliminate race window
|
|
1186
1226
|
ipcServer.start();
|
|
1227
|
+
ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
|
|
1228
|
+
ipcServer.startCpuTracking();
|
|
1187
1229
|
// 配置 reload 走 IPC `evolagent.reload` 触发,不再用 watchFile。
|
|
1188
1230
|
// 双 rename 原子写下 watchFile 的语义会被破坏,且新结构有 N 个 config.json 要监控;
|
|
1189
1231
|
// 显式触发更可控。
|
|
@@ -1195,6 +1237,7 @@ async function main() {
|
|
|
1195
1237
|
const pid = process.pid;
|
|
1196
1238
|
const ppid = process.ppid;
|
|
1197
1239
|
logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
|
|
1240
|
+
ipcServer.stopCpuTracking();
|
|
1198
1241
|
ipcServer.stop();
|
|
1199
1242
|
eventBus.publish({
|
|
1200
1243
|
type: 'system:shutdown',
|
package/dist/ipc.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
5
|
import { fileCache } from './core/daemon-file-cache.js';
|
|
5
6
|
const isWindows = process.platform === 'win32';
|
|
@@ -14,6 +15,17 @@ export class IpcServer {
|
|
|
14
15
|
aunAidStatsProvider;
|
|
15
16
|
aunAidStatsRecorder;
|
|
16
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;
|
|
17
29
|
constructor(socketPath, getStatus, commandExecutor) {
|
|
18
30
|
this.socketPath = socketPath;
|
|
19
31
|
this.getStatus = getStatus;
|
|
@@ -39,6 +51,52 @@ export class IpcServer {
|
|
|
39
51
|
setAunAidStatsRecorder(recorder) {
|
|
40
52
|
this.aunAidStatsRecorder = recorder;
|
|
41
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
|
+
}
|
|
42
100
|
start() {
|
|
43
101
|
// Remove stale socket file (Unix only — named pipes auto-cleanup on process exit)
|
|
44
102
|
if (!isNamedPipe(this.socketPath)) {
|
|
@@ -222,6 +280,47 @@ export class IpcServer {
|
|
|
222
280
|
return { ok: false, error: e?.message ?? String(e) };
|
|
223
281
|
}
|
|
224
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
|
+
}
|
|
225
324
|
default:
|
|
226
325
|
return { error: `unknown command: ${cmd.type}` };
|
|
227
326
|
}
|
|
@@ -85,6 +85,41 @@ export function findProcesses(pattern) {
|
|
|
85
85
|
return [];
|
|
86
86
|
}
|
|
87
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
|
+
}
|
|
88
123
|
export function getProcessInfo(pid) {
|
|
89
124
|
try {
|
|
90
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
|
+
}
|
|
@@ -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')) {
|