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.
Files changed (106) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +105 -30
  3. package/dist/agents/codex-runner.js +15 -7
  4. package/dist/agents/gemini-runner.js +14 -5
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1064 -279
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/baseagent-loader.js +48 -0
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +1090 -838
  40. package/dist/core/evolagent-registry.js +191 -360
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +326 -145
  48. package/dist/core/message/message-queue.js +5 -5
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +437 -273
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -576
  92. package/dist/core/agent-loader.js +0 -39
  93. package/dist/core/agent-registry.js +0 -450
  94. package/dist/core/evolagent-schema.js +0 -72
  95. package/dist/core/message/stream-flusher.js +0 -238
  96. package/dist/core/message/thought-emitter.js +0 -162
  97. package/dist/core/reload-hooks.js +0 -87
  98. package/dist/prompts/templates.js +0 -122
  99. package/dist/templates/skills.md +0 -66
  100. package/dist/utils/channel-fingerprint.js +0 -59
  101. package/dist/utils/error-dict.js +0 -63
  102. package/dist/utils/format.js +0 -32
  103. package/dist/utils/init.js +0 -645
  104. package/dist/utils/migrate-project.js +0 -122
  105. package/dist/utils/reload-hooks.js +0 -87
  106. package/dist/utils/stats-collector.js +0 -99
@@ -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
- if (!fs.existsSync(LOG_DIR)) {
15
- fs.mkdirSync(LOG_DIR, { recursive: true });
16
- }
17
- /** 获取当前小时标识 YYYYMMDD-HH */
18
- function currentHourTag() {
19
- const d = new Date();
20
- const pad = (n) => String(n).padStart(2, '0');
21
- return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}`;
22
- }
23
- /** 清理超过 RETAIN_HOURS 的旧日志文件 */
24
- function cleanupOldLogs() {
25
- const cutoff = Date.now() - RETAIN_HOURS * HOUR_MS;
26
- try {
27
- for (const name of fs.readdirSync(LOG_DIR)) {
28
- if (!LOG_FILE_RE.test(name))
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
- rotateMainIfNeeded();
92
- const timestamp = localTimestamp();
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
- write(streams.message, { ts: localTimestamp(), ...data });
58
+ if (!messageWriter)
59
+ return;
60
+ messageWriter.write(JSON.stringify({ ts: localTimestamp(), ...data }));
116
61
  },
117
62
  event: (data) => {
118
- write(streams.event, { ts: localTimestamp(), ...data });
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
- const result = await runInstall();
91
- if (result.ok) {
122
+ try {
123
+ await npmInstallGlobal('evolclaw@latest');
92
124
  return { status: 'upgraded', from: localVer, to: remoteVer };
93
125
  }
94
- if (attempt === 1) {
95
- return { status: 'failed', from: localVer, to: remoteVer, error: result.error };
126
+ catch (e) {
127
+ lastError = e.stderr || e.message || String(e);
96
128
  }
97
129
  }
98
- // unreachable
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
+ }