evolclaw 2.8.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -12
- package/dist/agents/claude-runner.js +105 -30
- package/dist/agents/codex-runner.js +15 -7
- package/dist/agents/gemini-runner.js +14 -5
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1064 -279
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/baseagent-loader.js +48 -0
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +1090 -838
- package/dist/core/evolagent-registry.js +191 -360
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +326 -145
- package/dist/core/message/message-queue.js +5 -5
- package/dist/core/permission.js +21 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +704 -775
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/{templates → data}/prompts.md +34 -1
- package/dist/index.js +437 -273
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -576
- package/dist/core/agent-loader.js +0 -39
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
package/dist/utils/logger.js
CHANGED
|
@@ -1,85 +1,30 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
1
|
import { resolvePaths } from '../paths.js';
|
|
2
|
+
import { LogWriter } from './log-writer.js';
|
|
4
3
|
const LOG_DIR = resolvePaths().logs;
|
|
5
4
|
let currentLevel = process.env.LOG_LEVEL || 'INFO';
|
|
6
5
|
const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
|
7
|
-
const HOUR_MS = 60 * 60 * 1000;
|
|
8
|
-
const RETAIN_HOURS = 12;
|
|
9
|
-
const LOG_FILE_RE = /^evolclaw-\d{8}-\d{2}\.log$/;
|
|
10
6
|
const config = {
|
|
11
7
|
messageLog: process.env.MESSAGE_LOG === 'true',
|
|
12
8
|
eventLog: process.env.EVENT_LOG === 'true'
|
|
13
9
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
continue;
|
|
30
|
-
try {
|
|
31
|
-
const full = path.join(LOG_DIR, name);
|
|
32
|
-
if (fs.statSync(full).mtimeMs < cutoff)
|
|
33
|
-
fs.unlinkSync(full);
|
|
34
|
-
}
|
|
35
|
-
catch { }
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
catch { }
|
|
39
|
-
}
|
|
40
|
-
let mainHourTag = currentHourTag();
|
|
41
|
-
let mainStream = fs.createWriteStream(path.join(LOG_DIR, `evolclaw-${mainHourTag}.log`), { flags: 'a' });
|
|
42
|
-
// 同时保留 evolclaw.log 软链接指向当前文件,方便 tail -f
|
|
43
|
-
function updateSymlink() {
|
|
44
|
-
const link = path.join(LOG_DIR, 'evolclaw.log');
|
|
45
|
-
const target = `evolclaw-${mainHourTag}.log`;
|
|
46
|
-
try {
|
|
47
|
-
fs.unlinkSync(link);
|
|
48
|
-
}
|
|
49
|
-
catch { }
|
|
50
|
-
try {
|
|
51
|
-
fs.symlinkSync(target, link);
|
|
52
|
-
}
|
|
53
|
-
catch { }
|
|
54
|
-
}
|
|
55
|
-
updateSymlink();
|
|
56
|
-
// 启动时清理一次,之后每小时清理
|
|
57
|
-
cleanupOldLogs();
|
|
58
|
-
const cleanupTimer = setInterval(cleanupOldLogs, HOUR_MS);
|
|
59
|
-
cleanupTimer.unref?.();
|
|
60
|
-
function rotateMainIfNeeded() {
|
|
61
|
-
const tag = currentHourTag();
|
|
62
|
-
if (tag === mainHourTag)
|
|
63
|
-
return;
|
|
64
|
-
mainStream.end();
|
|
65
|
-
mainHourTag = tag;
|
|
66
|
-
mainStream = fs.createWriteStream(path.join(LOG_DIR, `evolclaw-${mainHourTag}.log`), { flags: 'a' });
|
|
67
|
-
updateSymlink();
|
|
68
|
-
cleanupOldLogs();
|
|
69
|
-
}
|
|
70
|
-
const streams = {
|
|
71
|
-
message: config.messageLog ? fs.createWriteStream(path.join(LOG_DIR, 'messages.log'), { flags: 'a' }) : null,
|
|
72
|
-
event: config.eventLog ? fs.createWriteStream(path.join(LOG_DIR, 'events.log'), { flags: 'a' }) : null
|
|
73
|
-
};
|
|
10
|
+
// 主日志:按小时切片,保留 12 小时
|
|
11
|
+
const mainWriter = new LogWriter({
|
|
12
|
+
baseName: 'evolclaw',
|
|
13
|
+
logDir: LOG_DIR,
|
|
14
|
+
rotation: 'hourly',
|
|
15
|
+
retention: { hours: 12 },
|
|
16
|
+
});
|
|
17
|
+
// 消息日志:按小时切片,保留 12 小时
|
|
18
|
+
const messageWriter = config.messageLog
|
|
19
|
+
? new LogWriter({ baseName: 'messages', logDir: LOG_DIR, rotation: 'hourly', retention: { hours: 12 } })
|
|
20
|
+
: null;
|
|
21
|
+
// 事件日志:按小时切片,保留 12 小时(由 index.ts 订阅 EventBus 后填充)
|
|
22
|
+
const eventWriter = config.eventLog
|
|
23
|
+
? new LogWriter({ baseName: 'events', logDir: LOG_DIR, rotation: 'hourly', retention: { hours: 12 } })
|
|
24
|
+
: null;
|
|
74
25
|
function shouldLog(level) {
|
|
75
26
|
return (LEVELS[level] ?? 1) >= (LEVELS[currentLevel] ?? 1);
|
|
76
27
|
}
|
|
77
|
-
function write(stream, data) {
|
|
78
|
-
if (!stream)
|
|
79
|
-
return;
|
|
80
|
-
const line = typeof data === 'string' ? data : JSON.stringify(data);
|
|
81
|
-
stream.write(`${line}\n`);
|
|
82
|
-
}
|
|
83
28
|
export function localTimestamp() {
|
|
84
29
|
const d = new Date();
|
|
85
30
|
const pad = (n) => String(n).padStart(2, '0');
|
|
@@ -88,10 +33,8 @@ export function localTimestamp() {
|
|
|
88
33
|
function log(level, ...args) {
|
|
89
34
|
if (!shouldLog(level))
|
|
90
35
|
return;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const msg = `[${timestamp}] [${level}] ${args.join(' ')}`;
|
|
94
|
-
mainStream.write(msg + '\n');
|
|
36
|
+
const msg = `[${localTimestamp()}] [${level}] ${args.join(' ')}`;
|
|
37
|
+
mainWriter.write(msg);
|
|
95
38
|
}
|
|
96
39
|
/**
|
|
97
40
|
* 设置日志级别(config 加载后调用,覆盖环境变量默认值)
|
|
@@ -112,9 +55,13 @@ export const logger = {
|
|
|
112
55
|
warn: (...args) => log('WARN', ...args),
|
|
113
56
|
error: (...args) => log('ERROR', ...args),
|
|
114
57
|
message: (data) => {
|
|
115
|
-
|
|
58
|
+
if (!messageWriter)
|
|
59
|
+
return;
|
|
60
|
+
messageWriter.write(JSON.stringify({ ts: localTimestamp(), ...data }));
|
|
116
61
|
},
|
|
117
62
|
event: (data) => {
|
|
118
|
-
|
|
63
|
+
if (!eventWriter)
|
|
64
|
+
return;
|
|
65
|
+
eventWriter.write(JSON.stringify({ ts: localTimestamp(), ...data }));
|
|
119
66
|
}
|
|
120
67
|
};
|
|
@@ -207,3 +207,26 @@ export class DownloadCache {
|
|
|
207
207
|
this.cache.clear();
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
// ── MIME / Size helpers ──────────────────────────────────────────────────────
|
|
211
|
+
const MIME_MAP = {
|
|
212
|
+
'.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json',
|
|
213
|
+
'.js': 'text/javascript', '.ts': 'text/typescript', '.py': 'text/x-python',
|
|
214
|
+
'.html': 'text/html', '.css': 'text/css', '.csv': 'text/csv',
|
|
215
|
+
'.pdf': 'application/pdf', '.zip': 'application/zip', '.gz': 'application/gzip',
|
|
216
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
217
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
218
|
+
'.xml': 'application/xml', '.yaml': 'application/x-yaml', '.yml': 'application/x-yaml',
|
|
219
|
+
'.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm', '.avi': 'video/x-msvideo',
|
|
220
|
+
'.opus': 'audio/ogg', '.mp3': 'audio/mpeg', '.aac': 'audio/aac', '.m4a': 'audio/mp4', '.wav': 'audio/wav',
|
|
221
|
+
};
|
|
222
|
+
export function guessMime(filename) {
|
|
223
|
+
const ext = path.extname(filename).toLowerCase();
|
|
224
|
+
return MIME_MAP[ext] || 'application/octet-stream';
|
|
225
|
+
}
|
|
226
|
+
export function formatSize(bytes) {
|
|
227
|
+
if (bytes < 1024)
|
|
228
|
+
return `${bytes} B`;
|
|
229
|
+
if (bytes < 1048576)
|
|
230
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
231
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
232
|
+
}
|
|
@@ -1,7 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm operations
|
|
3
|
+
*
|
|
4
|
+
* 集中管理本仓库所有 `npm install -g` / `npm view` 相关的子进程调用:
|
|
5
|
+
* - tryUpgrade() — evolclaw 自我升级
|
|
6
|
+
* - requireOptional() — 可选依赖动态加载 + 自动安装
|
|
7
|
+
* - npmInstallGlobal() — 全局安装(含 EACCES → sudo 回退、Windows npm.cmd)
|
|
8
|
+
*/
|
|
1
9
|
import fs from 'fs';
|
|
2
10
|
import path from 'path';
|
|
3
11
|
import { execFile } from 'child_process';
|
|
12
|
+
import { promisify } from 'util';
|
|
4
13
|
import { getPackageRoot } from '../paths.js';
|
|
14
|
+
import { isWindows } from './cross-platform.js';
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
// ── npm install -g (shared) ────────────────────────────────────────────────
|
|
17
|
+
export async function npmInstallGlobal(pkg) {
|
|
18
|
+
const npmCmd = isWindows ? 'npm.cmd' : 'npm';
|
|
19
|
+
const execOpts = { timeout: 180000, shell: isWindows };
|
|
20
|
+
try {
|
|
21
|
+
await execFileAsync(npmCmd, ['install', '-g', pkg], execOpts);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES')) {
|
|
25
|
+
if (isWindows) {
|
|
26
|
+
throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
|
|
27
|
+
}
|
|
28
|
+
await execFileAsync('sudo', ['npm', 'install', '-g', pkg], { timeout: 180000 });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Dynamic import with auto-install fallback for optional dependencies */
|
|
36
|
+
export async function requireOptional(pkg, autoInstall = true) {
|
|
37
|
+
try {
|
|
38
|
+
return await import(pkg);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
if (e.code !== 'ERR_MODULE_NOT_FOUND' && e.code !== 'MODULE_NOT_FOUND')
|
|
42
|
+
throw e;
|
|
43
|
+
if (!autoInstall)
|
|
44
|
+
throw new Error(`依赖 ${pkg} 未安装。请运行: npm install -g ${pkg}`);
|
|
45
|
+
const { logger } = await import('./logger.js');
|
|
46
|
+
logger.info(`正在安装可选依赖 ${pkg}...`);
|
|
47
|
+
await npmInstallGlobal(pkg);
|
|
48
|
+
return await import(pkg);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
5
51
|
/**
|
|
6
52
|
* 比较两个 semver 版本号 (a.b.c 格式)
|
|
7
53
|
* 返回 -1 (a < b), 0 (a == b), 1 (a > b)
|
|
@@ -52,21 +98,6 @@ export function checkLatestVersion() {
|
|
|
52
98
|
});
|
|
53
99
|
});
|
|
54
100
|
}
|
|
55
|
-
/**
|
|
56
|
-
* 执行 npm install -g evolclaw@latest
|
|
57
|
-
*/
|
|
58
|
-
function runInstall() {
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
execFile('npm', ['install', '-g', 'evolclaw@latest'], { timeout: 120000 }, (err, _stdout, stderr) => {
|
|
61
|
-
if (err) {
|
|
62
|
-
resolve({ ok: false, error: stderr || err.message });
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
resolve({ ok: true });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
101
|
/**
|
|
71
102
|
* 完整升级流程:检查 → 比较 → 安装(失败重试一次)
|
|
72
103
|
*/
|
|
@@ -86,15 +117,15 @@ export async function tryUpgrade() {
|
|
|
86
117
|
return { status: 'no-update', from: localVer };
|
|
87
118
|
}
|
|
88
119
|
// 有新版本,执行升级(失败重试一次)
|
|
120
|
+
let lastError;
|
|
89
121
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
try {
|
|
123
|
+
await npmInstallGlobal('evolclaw@latest');
|
|
92
124
|
return { status: 'upgraded', from: localVer, to: remoteVer };
|
|
93
125
|
}
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
catch (e) {
|
|
127
|
+
lastError = e.stderr || e.message || String(e);
|
|
96
128
|
}
|
|
97
129
|
}
|
|
98
|
-
|
|
99
|
-
return { status: 'failed', from: localVer, to: remoteVer };
|
|
130
|
+
return { status: 'failed', from: localVer, to: remoteVer, error: lastError };
|
|
100
131
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform process introspection.
|
|
3
|
+
*
|
|
4
|
+
* Provides process start time retrieval for PID reuse detection.
|
|
5
|
+
* Returns null when start time cannot be obtained — callers should treat
|
|
6
|
+
* this as "do not match" (conservative: prefer leaving an orphan over
|
|
7
|
+
* killing the wrong process).
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
import { isWindows } from './cross-platform.js';
|
|
12
|
+
const isMacOS = process.platform === 'darwin';
|
|
13
|
+
/** 容差:2 秒(覆盖 macOS 秒级精度 + 时钟漂移) */
|
|
14
|
+
export const START_TIME_TOLERANCE_MS = 2000;
|
|
15
|
+
/**
|
|
16
|
+
* 获取进程启动时间(Unix 毫秒)。
|
|
17
|
+
* 拿不到时返回 null,调用方应保守处理(不杀该 PID)。
|
|
18
|
+
*/
|
|
19
|
+
export function getProcessStartTime(pid) {
|
|
20
|
+
try {
|
|
21
|
+
if (isWindows)
|
|
22
|
+
return getStartTimeWindows(pid);
|
|
23
|
+
if (isMacOS)
|
|
24
|
+
return getStartTimeMacOS(pid);
|
|
25
|
+
return getStartTimeLinux(pid);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 比对记录的启动时间与实际启动时间是否匹配(容差 2 秒)。
|
|
33
|
+
* actual === null 时返回 false(保守不匹配)。
|
|
34
|
+
*/
|
|
35
|
+
export function startTimeMatches(recorded, actual) {
|
|
36
|
+
if (actual === null)
|
|
37
|
+
return false;
|
|
38
|
+
return Math.abs(recorded - actual) < START_TIME_TOLERANCE_MS;
|
|
39
|
+
}
|
|
40
|
+
// ── Linux ──
|
|
41
|
+
function getStartTimeLinux(pid) {
|
|
42
|
+
// /proc/<pid>/stat 第 22 字段:starttime(jiffies since boot)
|
|
43
|
+
// 注意 comm 字段(第 2 字段)含括号,可能包含空格,从最后一个 ')' 之后切
|
|
44
|
+
let stat;
|
|
45
|
+
try {
|
|
46
|
+
stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const tail = stat.slice(stat.lastIndexOf(')') + 2);
|
|
52
|
+
const fields = tail.split(' ');
|
|
53
|
+
// tail 从第 3 字段(state)开始,第 22 字段索引为 22 - 3 = 19
|
|
54
|
+
const starttimeJiffies = parseInt(fields[19], 10);
|
|
55
|
+
if (isNaN(starttimeJiffies))
|
|
56
|
+
return null;
|
|
57
|
+
let uptimeSec;
|
|
58
|
+
try {
|
|
59
|
+
uptimeSec = parseFloat(fs.readFileSync('/proc/uptime', 'utf-8').split(' ')[0]);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (isNaN(uptimeSec))
|
|
65
|
+
return null;
|
|
66
|
+
// CLK_TCK 在绝大多数 Linux 系统是 100
|
|
67
|
+
const clkTck = 100;
|
|
68
|
+
const bootTimeMs = Date.now() - uptimeSec * 1000;
|
|
69
|
+
return bootTimeMs + (starttimeJiffies / clkTck) * 1000;
|
|
70
|
+
}
|
|
71
|
+
// ── macOS ──
|
|
72
|
+
function getStartTimeMacOS(pid) {
|
|
73
|
+
let out;
|
|
74
|
+
try {
|
|
75
|
+
out = execFileSync('ps', ['-p', String(pid), '-o', 'lstart='], {
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
timeout: 5000,
|
|
78
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
79
|
+
}).trim();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (!out)
|
|
85
|
+
return null;
|
|
86
|
+
// 输出格式:"Fri May 16 08:00:00 2026"
|
|
87
|
+
const t = Date.parse(out);
|
|
88
|
+
return isNaN(t) ? null : t;
|
|
89
|
+
}
|
|
90
|
+
// ── Windows ──
|
|
91
|
+
function getStartTimeWindows(pid) {
|
|
92
|
+
// 优先 PowerShell Get-CimInstance(现代)
|
|
93
|
+
const fromPwsh = winPowerShellCreationDate(pid);
|
|
94
|
+
if (fromPwsh !== null)
|
|
95
|
+
return fromPwsh;
|
|
96
|
+
// 降级 wmic
|
|
97
|
+
return winWmicCreationDate(pid);
|
|
98
|
+
}
|
|
99
|
+
function winPowerShellCreationDate(pid) {
|
|
100
|
+
let out;
|
|
101
|
+
try {
|
|
102
|
+
out = execFileSync('powershell', [
|
|
103
|
+
'-NoProfile',
|
|
104
|
+
'-Command',
|
|
105
|
+
`(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CreationDate`,
|
|
106
|
+
], { encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return parseCimDate(out);
|
|
112
|
+
}
|
|
113
|
+
function winWmicCreationDate(pid) {
|
|
114
|
+
let out;
|
|
115
|
+
try {
|
|
116
|
+
out = execFileSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'CreationDate', '/value'], { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
// wmic 输出 "CreationDate=20260516080000.000000+480"
|
|
122
|
+
const m = out.match(/CreationDate=([^\r\n]+)/);
|
|
123
|
+
if (!m)
|
|
124
|
+
return null;
|
|
125
|
+
return parseCimDate(m[1].trim());
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 解析 CIM/WMI 日期格式:yyyyMMddHHmmss.ffffff±TZZZ
|
|
129
|
+
* TZZZ 是相对 UTC 的分钟偏移(中国是 +480)
|
|
130
|
+
*/
|
|
131
|
+
export function parseCimDate(s) {
|
|
132
|
+
if (!s)
|
|
133
|
+
return null;
|
|
134
|
+
const m = s.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{6})([+-]\d+)$/);
|
|
135
|
+
if (!m)
|
|
136
|
+
return null;
|
|
137
|
+
const [, y, mo, d, h, mi, sec, us, tz] = m;
|
|
138
|
+
const tzMin = parseInt(tz, 10);
|
|
139
|
+
if (isNaN(tzMin))
|
|
140
|
+
return null;
|
|
141
|
+
// CIM 的时间是"本地时区时间",要换成 UTC:UTC = local - offset
|
|
142
|
+
const localUtcMs = Date.UTC(+y, +mo - 1, +d, +h, +mi, +sec, Math.floor(+us / 1000));
|
|
143
|
+
return localUtcMs - tzMin * 60 * 1000;
|
|
144
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export class StatsCollector {
|
|
2
|
+
events = [];
|
|
3
|
+
startTime;
|
|
4
|
+
HOUR_MS = 3_600_000;
|
|
5
|
+
constructor(eventBus) {
|
|
6
|
+
this.startTime = Date.now();
|
|
7
|
+
// 订阅相关事件
|
|
8
|
+
eventBus.subscribe('message:received', (event) => {
|
|
9
|
+
const e = event;
|
|
10
|
+
this.recordEvent({ type: 'received', timestamp: e.timestamp || Date.now(), agentName: e.agentName });
|
|
11
|
+
});
|
|
12
|
+
eventBus.subscribe('task:completed', (event) => {
|
|
13
|
+
const e = event;
|
|
14
|
+
this.recordEvent({ type: 'completed', timestamp: e.timestamp || Date.now(), durationMs: e.durationMs, agentName: e.agentName });
|
|
15
|
+
});
|
|
16
|
+
eventBus.subscribe('task:error', (event) => {
|
|
17
|
+
const e = event;
|
|
18
|
+
this.recordEvent({ type: 'error', timestamp: Date.now(), errorType: e.errorType, agentName: e.agentName });
|
|
19
|
+
});
|
|
20
|
+
eventBus.subscribe('task:interrupted', (event) => {
|
|
21
|
+
const e = event;
|
|
22
|
+
this.recordEvent({ type: 'interrupted', timestamp: Date.now(), agentName: e.agentName });
|
|
23
|
+
});
|
|
24
|
+
eventBus.subscribe('tool:result', (event) => {
|
|
25
|
+
const e = event;
|
|
26
|
+
if (e.isError) {
|
|
27
|
+
this.recordEvent({ type: 'tool-error', timestamp: Date.now(), toolName: e.toolName, agentName: e.agentName });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
recordEvent(record) {
|
|
32
|
+
this.events.push(record);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 获取统计快照。可选 agentName 过滤:未传则全局;传入则只统计该 agent。
|
|
36
|
+
* 自动裁剪 >1h 的事件。
|
|
37
|
+
*/
|
|
38
|
+
getSnapshot(agentName) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const cutoff = now - this.HOUR_MS;
|
|
41
|
+
// 裁剪过期事件
|
|
42
|
+
this.events = this.events.filter(e => e.timestamp >= cutoff);
|
|
43
|
+
// 聚合统计(可按 agent 过滤)
|
|
44
|
+
const filtered = agentName === undefined
|
|
45
|
+
? this.events
|
|
46
|
+
: this.events.filter(e => (e.agentName ?? '<unknown>') === agentName);
|
|
47
|
+
let received = 0;
|
|
48
|
+
let completed = 0;
|
|
49
|
+
let errors = 0;
|
|
50
|
+
const errorsByType = {};
|
|
51
|
+
let toolErrors = 0;
|
|
52
|
+
const toolErrorsByName = {};
|
|
53
|
+
let interrupts = 0;
|
|
54
|
+
let totalDuration = 0;
|
|
55
|
+
let durationCount = 0;
|
|
56
|
+
for (const event of filtered) {
|
|
57
|
+
switch (event.type) {
|
|
58
|
+
case 'received':
|
|
59
|
+
received++;
|
|
60
|
+
break;
|
|
61
|
+
case 'completed':
|
|
62
|
+
completed++;
|
|
63
|
+
if (event.durationMs !== undefined) {
|
|
64
|
+
totalDuration += event.durationMs;
|
|
65
|
+
durationCount++;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case 'error':
|
|
69
|
+
errors++;
|
|
70
|
+
if (event.errorType) {
|
|
71
|
+
errorsByType[event.errorType] = (errorsByType[event.errorType] || 0) + 1;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
case 'tool-error':
|
|
75
|
+
toolErrors++;
|
|
76
|
+
if (event.toolName) {
|
|
77
|
+
toolErrorsByName[event.toolName] = (toolErrorsByName[event.toolName] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case 'interrupted':
|
|
81
|
+
interrupts++;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
uptimeMs: now - this.startTime,
|
|
87
|
+
lastHour: {
|
|
88
|
+
received,
|
|
89
|
+
completed,
|
|
90
|
+
errors,
|
|
91
|
+
errorsByType,
|
|
92
|
+
toolErrors,
|
|
93
|
+
toolErrorsByName,
|
|
94
|
+
interrupts,
|
|
95
|
+
avgResponseMs: durationCount > 0 ? totalDuration / durationCount : 0
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export class AidStatsCollector {
|
|
101
|
+
entries = new Map();
|
|
102
|
+
queueStatsProvider;
|
|
103
|
+
setQueueStatsProvider(provider) {
|
|
104
|
+
this.queueStatsProvider = provider;
|
|
105
|
+
}
|
|
106
|
+
getOrCreate(aid) {
|
|
107
|
+
let entry = this.entries.get(aid);
|
|
108
|
+
if (!entry) {
|
|
109
|
+
entry = {
|
|
110
|
+
aid,
|
|
111
|
+
selfName: null,
|
|
112
|
+
messagesReceived: 0,
|
|
113
|
+
messagesSent: 0,
|
|
114
|
+
systemReceived: 0,
|
|
115
|
+
systemSent: 0,
|
|
116
|
+
bytesReceived: 0,
|
|
117
|
+
bytesSent: 0,
|
|
118
|
+
lastReceivedAt: null,
|
|
119
|
+
lastSentAt: null,
|
|
120
|
+
lastReceivedText: null,
|
|
121
|
+
lastReceivedFrom: null,
|
|
122
|
+
lastSentText: null,
|
|
123
|
+
lastSentTo: null,
|
|
124
|
+
uniquePeers: new Set(),
|
|
125
|
+
};
|
|
126
|
+
this.entries.set(aid, entry);
|
|
127
|
+
}
|
|
128
|
+
return entry;
|
|
129
|
+
}
|
|
130
|
+
setSelfName(aid, name) {
|
|
131
|
+
const entry = this.getOrCreate(aid);
|
|
132
|
+
entry.selfName = name;
|
|
133
|
+
}
|
|
134
|
+
recordInbound(aid, fromPeer, byteLength, text, isSystem = false) {
|
|
135
|
+
const entry = this.getOrCreate(aid);
|
|
136
|
+
if (isSystem) {
|
|
137
|
+
entry.systemReceived++;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
entry.messagesReceived++;
|
|
141
|
+
entry.lastReceivedAt = Date.now();
|
|
142
|
+
entry.lastReceivedFrom = fromPeer;
|
|
143
|
+
if (text)
|
|
144
|
+
entry.lastReceivedText = text.length > 100 ? text.slice(0, 100) + '…' : text;
|
|
145
|
+
}
|
|
146
|
+
entry.bytesReceived += byteLength;
|
|
147
|
+
entry.uniquePeers.add(fromPeer);
|
|
148
|
+
}
|
|
149
|
+
recordOutbound(aid, toPeer, byteLength, text, isSystem = false) {
|
|
150
|
+
const entry = this.getOrCreate(aid);
|
|
151
|
+
if (isSystem) {
|
|
152
|
+
entry.systemSent++;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
entry.messagesSent++;
|
|
156
|
+
entry.lastSentAt = Date.now();
|
|
157
|
+
entry.lastSentTo = toPeer;
|
|
158
|
+
if (text)
|
|
159
|
+
entry.lastSentText = text.length > 100 ? text.slice(0, 100) + '…' : text;
|
|
160
|
+
}
|
|
161
|
+
entry.bytesSent += byteLength;
|
|
162
|
+
entry.uniquePeers.add(toPeer);
|
|
163
|
+
}
|
|
164
|
+
getAllSnapshots() {
|
|
165
|
+
const out = [];
|
|
166
|
+
for (const entry of this.entries.values()) {
|
|
167
|
+
const queueStats = this.queueStatsProvider
|
|
168
|
+
? this.queueStatsProvider(entry.aid)
|
|
169
|
+
: { processing: 0, queued: 0 };
|
|
170
|
+
out.push({
|
|
171
|
+
aid: entry.aid,
|
|
172
|
+
selfName: entry.selfName,
|
|
173
|
+
messagesReceived: entry.messagesReceived,
|
|
174
|
+
messagesSent: entry.messagesSent,
|
|
175
|
+
systemReceived: entry.systemReceived,
|
|
176
|
+
systemSent: entry.systemSent,
|
|
177
|
+
bytesReceived: entry.bytesReceived,
|
|
178
|
+
bytesSent: entry.bytesSent,
|
|
179
|
+
lastReceivedAt: entry.lastReceivedAt,
|
|
180
|
+
lastSentAt: entry.lastSentAt,
|
|
181
|
+
lastReceivedText: entry.lastReceivedText,
|
|
182
|
+
lastReceivedFrom: entry.lastReceivedFrom,
|
|
183
|
+
lastSentText: entry.lastSentText,
|
|
184
|
+
lastSentTo: entry.lastSentTo,
|
|
185
|
+
uniquePeerCount: entry.uniquePeers.size,
|
|
186
|
+
processing: queueStats.processing,
|
|
187
|
+
queued: queueStats.queued,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
}
|