evolclaw 2.8.3 → 3.1.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 (142) hide show
  1. package/README.md +21 -12
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +108 -46
  5. package/dist/agents/codex-runner.js +13 -14
  6. package/dist/agents/gemini-runner.js +15 -17
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/agents/resolve.js +134 -0
  9. package/dist/aun/aid/agentmd.js +186 -0
  10. package/dist/aun/aid/client.js +134 -0
  11. package/dist/aun/aid/identity.js +159 -0
  12. package/dist/aun/aid/index.js +3 -0
  13. package/dist/aun/aid/lifecycle-log.js +33 -0
  14. package/dist/aun/aid/types.js +1 -0
  15. package/dist/aun/aid/validation.js +21 -0
  16. package/dist/aun/msg/group.js +293 -0
  17. package/dist/aun/msg/index.js +4 -0
  18. package/dist/aun/msg/p2p.js +147 -0
  19. package/dist/aun/msg/payload-type.js +27 -0
  20. package/dist/aun/msg/upload.js +98 -0
  21. package/dist/aun/outbox.js +138 -0
  22. package/dist/aun/rpc/caller.js +42 -0
  23. package/dist/aun/rpc/connection.js +34 -0
  24. package/dist/aun/rpc/index.js +2 -0
  25. package/dist/aun/storage/download.js +29 -0
  26. package/dist/aun/storage/index.js +3 -0
  27. package/dist/aun/storage/manage.js +10 -0
  28. package/dist/aun/storage/upload.js +35 -0
  29. package/dist/channels/aun.js +1340 -349
  30. package/dist/channels/dingtalk.js +59 -5
  31. package/dist/channels/feishu.js +381 -32
  32. package/dist/channels/qqbot.js +68 -12
  33. package/dist/channels/wechat.js +63 -4
  34. package/dist/channels/wecom.js +59 -5
  35. package/dist/cli/agent.js +800 -0
  36. package/dist/cli/bench.js +1219 -0
  37. package/dist/cli/index.js +4513 -0
  38. package/dist/{utils → cli}/init-channel.js +211 -621
  39. package/dist/cli/init.js +178 -0
  40. package/dist/cli/link-rules.js +245 -0
  41. package/dist/cli/net-check.js +640 -0
  42. package/dist/cli/watch-msg.js +589 -0
  43. package/dist/config-store.js +645 -0
  44. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  45. package/dist/core/channel-loader.js +176 -12
  46. package/dist/core/command-handler.js +883 -848
  47. package/dist/core/evolagent-registry.js +191 -371
  48. package/dist/core/evolagent.js +202 -238
  49. package/dist/core/interaction-router.js +52 -5
  50. package/dist/core/message/im-renderer.js +486 -0
  51. package/dist/core/message/items-formatter.js +68 -0
  52. package/dist/core/message/message-bridge.js +109 -56
  53. package/dist/core/message/message-log.js +93 -0
  54. package/dist/core/message/message-processor.js +430 -212
  55. package/dist/core/message/message-queue.js +13 -6
  56. package/dist/core/permission.js +116 -11
  57. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  58. package/dist/core/session/session-fs-store.js +230 -0
  59. package/dist/core/session/session-manager.js +740 -777
  60. package/dist/core/session/session-mapper.js +87 -0
  61. package/dist/core/trigger/manager.js +122 -0
  62. package/dist/core/trigger/parser.js +128 -0
  63. package/dist/core/trigger/scheduler.js +224 -0
  64. package/dist/data/error-dict.json +118 -0
  65. package/dist/eck/baseagent-caps.js +18 -0
  66. package/dist/eck/detect.js +47 -0
  67. package/dist/eck/init.js +77 -0
  68. package/dist/eck/rules-loader.js +28 -0
  69. package/dist/index.js +560 -283
  70. package/dist/ipc.js +49 -0
  71. package/dist/net-check.js +640 -0
  72. package/dist/paths.js +73 -9
  73. package/dist/types.js +8 -2
  74. package/dist/utils/aid-lifecycle-log.js +33 -0
  75. package/dist/utils/atomic-write.js +89 -0
  76. package/dist/utils/channel-helpers.js +46 -0
  77. package/dist/utils/cross-platform.js +17 -26
  78. package/dist/utils/error-utils.js +10 -2
  79. package/dist/utils/instance-registry.js +434 -0
  80. package/dist/utils/log-writer.js +217 -0
  81. package/dist/utils/logger.js +34 -77
  82. package/dist/utils/media-cache.js +23 -0
  83. package/dist/utils/npm-ops.js +163 -0
  84. package/dist/utils/process-introspect.js +122 -0
  85. package/dist/utils/stats.js +192 -0
  86. package/dist/watch-msg.js +544 -0
  87. package/evolclaw-install-aun.md +127 -47
  88. package/kits/docs/GUIDE.md +20 -0
  89. package/kits/docs/INDEX.md +52 -0
  90. package/kits/docs/aun/CHEATSHEET.md +17 -0
  91. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  92. package/kits/docs/channels/aun.md +25 -0
  93. package/kits/docs/channels/feishu.md +27 -0
  94. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  95. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  96. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  97. package/kits/docs/eck_templates/runtime.template.md +19 -0
  98. package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
  99. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  100. package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
  101. package/kits/docs/evolclaw/self-summary.md +29 -0
  102. package/kits/docs/evolclaw/tools.md +25 -0
  103. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  104. package/kits/docs/identity/PATH_OPS.md +16 -0
  105. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  106. package/kits/docs/identity/identity-tools.md +26 -0
  107. package/kits/docs/path-registry.md +43 -0
  108. package/kits/eck_manifest.json +95 -0
  109. package/kits/rules/01-overview.md +120 -0
  110. package/kits/rules/02-navigation.md +75 -0
  111. package/kits/rules/03-identity.md +34 -0
  112. package/kits/rules/04-relation.md +49 -0
  113. package/kits/rules/05-venue.md +45 -0
  114. package/kits/rules/06-channel.md +43 -0
  115. package/kits/templates/system-fragments/baseagent.md +2 -0
  116. package/kits/templates/system-fragments/channel.md +10 -0
  117. package/kits/templates/system-fragments/identity.md +12 -0
  118. package/kits/templates/system-fragments/relation.md +9 -0
  119. package/kits/templates/system-fragments/runtime.md +19 -0
  120. package/kits/templates/system-fragments/venue.md +5 -0
  121. package/package.json +10 -6
  122. package/data/evolclaw.sample.json +0 -60
  123. package/dist/agents/templates.js +0 -122
  124. package/dist/channels/aun-ops.js +0 -275
  125. package/dist/cli.js +0 -2178
  126. package/dist/config.js +0 -591
  127. package/dist/core/agent-registry.js +0 -450
  128. package/dist/core/evolagent-schema.js +0 -72
  129. package/dist/core/message/stream-flusher.js +0 -238
  130. package/dist/core/message/thought-emitter.js +0 -162
  131. package/dist/core/reload-hooks.js +0 -87
  132. package/dist/prompts/templates.js +0 -122
  133. package/dist/templates/prompts.md +0 -104
  134. package/dist/templates/skills.md +0 -66
  135. package/dist/utils/channel-fingerprint.js +0 -59
  136. package/dist/utils/error-dict.js +0 -63
  137. package/dist/utils/format.js +0 -32
  138. package/dist/utils/init.js +0 -645
  139. package/dist/utils/migrate-project.js +0 -122
  140. package/dist/utils/reload-hooks.js +0 -87
  141. package/dist/utils/stats-collector.js +0 -99
  142. package/dist/utils/upgrade.js +0 -100
@@ -1,85 +1,34 @@
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;
25
+ // 渠道入站日志:记录从渠道收到的原始消息
26
+ const channelInWriter = new LogWriter({ baseName: 'channel-in', logDir: LOG_DIR, rotation: 'hourly', retention: { hours: 12 } });
27
+ // 渠道出站日志:记录发往渠道的所有消息
28
+ const channelOutWriter = new LogWriter({ baseName: 'channel-out', logDir: LOG_DIR, rotation: 'hourly', retention: { hours: 12 } });
74
29
  function shouldLog(level) {
75
30
  return (LEVELS[level] ?? 1) >= (LEVELS[currentLevel] ?? 1);
76
31
  }
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
32
  export function localTimestamp() {
84
33
  const d = new Date();
85
34
  const pad = (n) => String(n).padStart(2, '0');
@@ -88,10 +37,8 @@ export function localTimestamp() {
88
37
  function log(level, ...args) {
89
38
  if (!shouldLog(level))
90
39
  return;
91
- rotateMainIfNeeded();
92
- const timestamp = localTimestamp();
93
- const msg = `[${timestamp}] [${level}] ${args.join(' ')}`;
94
- mainStream.write(msg + '\n');
40
+ const msg = `[${localTimestamp()}] [${level}] ${args.join(' ')}`;
41
+ mainWriter.write(msg);
95
42
  }
96
43
  /**
97
44
  * 设置日志级别(config 加载后调用,覆盖环境变量默认值)
@@ -112,9 +59,19 @@ export const logger = {
112
59
  warn: (...args) => log('WARN', ...args),
113
60
  error: (...args) => log('ERROR', ...args),
114
61
  message: (data) => {
115
- write(streams.message, { ts: localTimestamp(), ...data });
62
+ if (!messageWriter)
63
+ return;
64
+ messageWriter.write(JSON.stringify({ ts: localTimestamp(), ...data }));
116
65
  },
117
66
  event: (data) => {
118
- write(streams.event, { ts: localTimestamp(), ...data });
67
+ if (!eventWriter)
68
+ return;
69
+ eventWriter.write(JSON.stringify({ ts: localTimestamp(), ...data }));
70
+ },
71
+ channelIn: (data) => {
72
+ channelInWriter.write(JSON.stringify({ ts: localTimestamp(), ...data }));
73
+ },
74
+ channelOut: (data) => {
75
+ channelOutWriter.write(JSON.stringify({ ts: localTimestamp(), ...data }));
119
76
  }
120
77
  };
@@ -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
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * npm operations
3
+ *
4
+ * 集中管理本仓库所有 `npm install -g` / `npm view` 相关的子进程调用:
5
+ * - tryUpgrade() — evolclaw 自我升级
6
+ * - requireOptional() — 可选依赖动态加载 + 自动安装
7
+ * - npmInstallGlobal() — 全局安装(含 EACCES → sudo 回退、Windows npm.cmd)
8
+ */
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { execFile } from 'child_process';
12
+ import { promisify } from 'util';
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
+ }
51
+ /**
52
+ * 比较两个 semver 版本号 (a.b.c 格式)
53
+ * 返回 -1 (a < b), 0 (a == b), 1 (a > b)
54
+ * 自动剥离 pre-release 标签 (e.g. 2.6.0-beta.1 → 2.6.0)
55
+ */
56
+ export function compareVersions(a, b) {
57
+ const pa = a.split('-')[0].split('.').map(Number);
58
+ const pb = b.split('-')[0].split('.').map(Number);
59
+ const len = Math.max(pa.length, pb.length);
60
+ for (let i = 0; i < len; i++) {
61
+ const na = pa[i] ?? 0;
62
+ const nb = pb[i] ?? 0;
63
+ if (na < nb)
64
+ return -1;
65
+ if (na > nb)
66
+ return 1;
67
+ }
68
+ return 0;
69
+ }
70
+ /**
71
+ * 检查当前安装是否为 npm link 开发模式。
72
+ * 正式全局安装的路径结构为 .../node_modules/evolclaw,
73
+ * 而 npm link 指向项目源码目录,其父目录不是 node_modules。
74
+ */
75
+ export function isLinkedInstall() {
76
+ const pkgRoot = getPackageRoot();
77
+ return path.basename(path.dirname(pkgRoot)) !== 'node_modules';
78
+ }
79
+ /** 获取本地 package.json 中的版本号 */
80
+ export function getLocalVersion() {
81
+ const pkgPath = path.join(getPackageRoot(), 'package.json');
82
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
83
+ return pkg.version;
84
+ }
85
+ /**
86
+ * 查询 npm registry 上指定包的最新版本。
87
+ * 超时 15 秒,失败返回 null。
88
+ */
89
+ export function checkLatestVersion(pkg = 'evolclaw') {
90
+ return new Promise((resolve) => {
91
+ execFile('npm', ['view', pkg, 'version'], { timeout: 15000 }, (err, stdout) => {
92
+ if (err) {
93
+ resolve(null);
94
+ return;
95
+ }
96
+ const ver = stdout.trim();
97
+ resolve(ver || null);
98
+ });
99
+ });
100
+ }
101
+ /**
102
+ * 完整升级流程:检查 → 比较 → 安装(失败重试一次)
103
+ */
104
+ export async function tryUpgrade() {
105
+ // 开发模式跳过
106
+ if (isLinkedInstall()) {
107
+ return { status: 'skipped' };
108
+ }
109
+ const localVer = getLocalVersion();
110
+ // 查询 registry
111
+ const remoteVer = await checkLatestVersion();
112
+ if (!remoteVer) {
113
+ return { status: 'skipped', error: 'Failed to check remote version' };
114
+ }
115
+ // 版本比较
116
+ if (compareVersions(localVer, remoteVer) >= 0) {
117
+ return { status: 'no-update', from: localVer };
118
+ }
119
+ // 有新版本,执行升级(失败重试一次)
120
+ let lastError;
121
+ for (let attempt = 0; attempt < 2; attempt++) {
122
+ try {
123
+ await npmInstallGlobal('evolclaw@latest');
124
+ return { status: 'upgraded', from: localVer, to: remoteVer };
125
+ }
126
+ catch (e) {
127
+ lastError = e.stderr || e.message || String(e);
128
+ }
129
+ }
130
+ return { status: 'failed', from: localVer, to: remoteVer, error: lastError };
131
+ }
132
+ /**
133
+ * AUN SDK 升级流程:检查 → 比较 → 安装
134
+ * 仅在 SDK 已安装时检查升级,未安装则跳过。
135
+ */
136
+ export async function tryUpgradeAunSdk(resolveAunCoreSdkPkg, AUN_CORE_SDK_PKG) {
137
+ if (isLinkedInstall()) {
138
+ return { status: 'skipped' };
139
+ }
140
+ const installed = resolveAunCoreSdkPkg();
141
+ if (!installed) {
142
+ return { status: 'skipped' }; // SDK not installed, skip
143
+ }
144
+ const localVer = installed.version;
145
+ const remoteVer = await checkLatestVersion(AUN_CORE_SDK_PKG);
146
+ if (!remoteVer) {
147
+ return { status: 'skipped', error: 'Failed to check remote version' };
148
+ }
149
+ if (compareVersions(localVer, remoteVer) >= 0) {
150
+ return { status: 'no-update', from: localVer };
151
+ }
152
+ let lastError;
153
+ for (let attempt = 0; attempt < 2; attempt++) {
154
+ try {
155
+ await npmInstallGlobal(`${AUN_CORE_SDK_PKG}@latest`);
156
+ return { status: 'upgraded', from: localVer, to: remoteVer };
157
+ }
158
+ catch (e) {
159
+ lastError = e.stderr || e.message || String(e);
160
+ }
161
+ }
162
+ return { status: 'failed', from: localVer, to: remoteVer, error: lastError };
163
+ }
@@ -0,0 +1,122 @@
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 { spawnSync } 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
+ let stat;
43
+ try {
44
+ stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8');
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ const tail = stat.slice(stat.lastIndexOf(')') + 2);
50
+ const fields = tail.split(' ');
51
+ const starttimeJiffies = parseInt(fields[19], 10);
52
+ if (isNaN(starttimeJiffies))
53
+ return null;
54
+ let uptimeSec;
55
+ try {
56
+ uptimeSec = parseFloat(fs.readFileSync('/proc/uptime', 'utf-8').split(' ')[0]);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ if (isNaN(uptimeSec))
62
+ return null;
63
+ const clkTck = 100;
64
+ const bootTimeMs = Date.now() - uptimeSec * 1000;
65
+ return bootTimeMs + (starttimeJiffies / clkTck) * 1000;
66
+ }
67
+ // ── macOS ──
68
+ function getStartTimeMacOS(pid) {
69
+ const result = spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], {
70
+ encoding: 'utf-8',
71
+ timeout: 5000,
72
+ stdio: ['pipe', 'pipe', 'pipe'],
73
+ });
74
+ const out = result.stdout?.trim();
75
+ if (!out)
76
+ return null;
77
+ const t = Date.parse(out);
78
+ return isNaN(t) ? null : t;
79
+ }
80
+ // ── Windows ──
81
+ function getStartTimeWindows(pid) {
82
+ const fromPwsh = winPowerShellCreationDate(pid);
83
+ if (fromPwsh !== null)
84
+ return fromPwsh;
85
+ return winWmicCreationDate(pid);
86
+ }
87
+ function winPowerShellCreationDate(pid) {
88
+ const result = spawnSync('powershell', [
89
+ '-NoProfile',
90
+ '-Command',
91
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CreationDate`,
92
+ ], { encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
93
+ const out = result.stdout?.trim();
94
+ if (!out)
95
+ return null;
96
+ return parseCimDate(out);
97
+ }
98
+ function winWmicCreationDate(pid) {
99
+ const result = spawnSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'CreationDate', '/value'], { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
100
+ const out = result.stdout || '';
101
+ const m = out.match(/CreationDate=([^\r\n]+)/);
102
+ if (!m)
103
+ return null;
104
+ return parseCimDate(m[1].trim());
105
+ }
106
+ /**
107
+ * 解析 CIM/WMI 日期格式:yyyyMMddHHmmss.ffffff±TZZZ
108
+ * TZZZ 是相对 UTC 的分钟偏移(中国是 +480)
109
+ */
110
+ export function parseCimDate(s) {
111
+ if (!s)
112
+ return null;
113
+ const m = s.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{6})([+-]\d+)$/);
114
+ if (!m)
115
+ return null;
116
+ const [, y, mo, d, h, mi, sec, us, tz] = m;
117
+ const tzMin = parseInt(tz, 10);
118
+ if (isNaN(tzMin))
119
+ return null;
120
+ const localUtcMs = Date.UTC(+y, +mo - 1, +d, +h, +mi, +sec, Math.floor(+us / 1000));
121
+ return localUtcMs - tzMin * 60 * 1000;
122
+ }
@@ -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
+ }