evolclaw 3.1.4 → 3.1.6
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 +60 -0
- package/dist/agents/claude-runner.js +398 -161
- package/dist/agents/kit-renderer.js +191 -25
- package/dist/aun/aid/agentmd.js +75 -103
- package/dist/aun/aid/client.js +1 -29
- package/dist/aun/aid/identity.js +105 -64
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/group.js +2 -2
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +174 -99
- package/dist/channels/dingtalk.js +2 -1
- package/dist/channels/feishu.js +301 -199
- package/dist/channels/qqbot.js +2 -1
- package/dist/channels/wechat.js +2 -1
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent.js +21 -16
- package/dist/cli/bench.js +41 -28
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +176 -87
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +549 -0
- package/dist/cli/net-check.js +133 -50
- package/dist/cli/watch-msg.js +7 -7
- package/dist/cli/watch-web/debug-log.js +18 -0
- package/dist/cli/watch-web/server.js +306 -0
- package/dist/cli/watch-web/sources/aid.js +63 -0
- package/dist/cli/watch-web/sources/msg.js +70 -0
- package/dist/cli/watch-web/sources/session.js +638 -0
- package/dist/cli/watch-web/sources/types.js +10 -0
- package/dist/cli/watch-web/static/app.js +546 -0
- package/dist/cli/watch-web/static/index.html +54 -0
- package/dist/cli/watch-web/static/style.css +247 -0
- package/dist/config-store.js +1 -22
- package/dist/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +261 -133
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -22
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +13 -9
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +211 -123
- package/dist/core/message/stream-idle-monitor.js +21 -0
- package/dist/core/model/model-catalog.js +215 -0
- package/dist/core/model/model-scope.js +250 -0
- package/dist/core/relation/peer-identity.js +58 -55
- package/dist/core/relation/peer-key.js +16 -0
- package/dist/core/session/session-fs-store.js +34 -55
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +308 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +3 -3
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +22 -7
- package/dist/index.js +61 -7
- package/dist/ipc.js +23 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +8 -8
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +182 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/agent.md +49 -0
- package/kits/docs/evolclaw/aid.md +49 -0
- package/kits/docs/evolclaw/ctl.md +46 -0
- package/kits/docs/evolclaw/group.md +89 -0
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +91 -0
- package/kits/docs/evolclaw/rpc.md +35 -0
- package/kits/docs/evolclaw/storage.md +49 -0
- package/kits/docs/venues/aun-group.md +10 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +23 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +81 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +34 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +7 -5
- package/kits/templates/system-fragments/commands.md +19 -0
- package/kits/templates/system-fragments/session.md +19 -3
- package/kits/templates/system-fragments/venue.md +24 -0
- package/package.json +10 -5
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
- package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
- package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
- package/kits/docs/evolclaw/tools.md +0 -25
package/dist/cli/net-check.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import net from 'net';
|
|
4
5
|
import tls from 'tls';
|
|
5
6
|
import dns from 'dns/promises';
|
|
@@ -7,7 +8,7 @@ import https from 'https';
|
|
|
7
8
|
// @ts-ignore
|
|
8
9
|
import { WebSocket } from 'ws';
|
|
9
10
|
import { aunPath as defaultAunPath } from '../paths.js';
|
|
10
|
-
import {
|
|
11
|
+
import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
|
|
11
12
|
import { isHelpFlag } from './help.js';
|
|
12
13
|
const GREEN = '\x1b[32m';
|
|
13
14
|
const RED = '\x1b[31m';
|
|
@@ -21,7 +22,7 @@ function ok(msg) { return ` ${GREEN}✓${RST} ${msg}`; }
|
|
|
21
22
|
function fail(msg) { return ` ${RED}✗${RST} ${msg}`; }
|
|
22
23
|
function skip(msg) { return ` ${YELLOW}○${RST} ${DIM}${msg}${RST}`; }
|
|
23
24
|
function ms(n) { return `${DIM}${n}ms${RST}`; }
|
|
24
|
-
function step(n, label) { return `${DIM}[${n}/
|
|
25
|
+
function step(n, label) { return `${DIM}[${n}/11]${RST} ${CYAN}${label}${RST}`; }
|
|
25
26
|
const isZh = (process.env.LANG || process.env.LC_ALL || process.env.LANGUAGE || Intl.DateTimeFormat().resolvedOptions().locale || '').toLowerCase().startsWith('zh');
|
|
26
27
|
const i18n = {
|
|
27
28
|
resolve: isZh ? '解析' : 'resolve',
|
|
@@ -124,7 +125,7 @@ function suppressSdkOutput(fn) {
|
|
|
124
125
|
});
|
|
125
126
|
}
|
|
126
127
|
// ==================== Check pipeline ====================
|
|
127
|
-
async function runCheck(aid, formatJson) {
|
|
128
|
+
async function runCheck(aid, formatJson, kickTest) {
|
|
128
129
|
const results = [];
|
|
129
130
|
const log = (r) => {
|
|
130
131
|
results.push(r);
|
|
@@ -216,18 +217,23 @@ async function runCheck(aid, formatJson) {
|
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
219
|
// ── Step 7: AID 认证 ──
|
|
219
|
-
let
|
|
220
|
+
let authResult;
|
|
220
221
|
try {
|
|
221
222
|
const start = Date.now();
|
|
222
223
|
const aunPath = process.env.AUN_HOME || defaultAunPath();
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
authResult = await suppressSdkOutput(async () => {
|
|
225
|
+
const store = await getAidStore({ slotId: SLOT.netcheck, aunPath });
|
|
226
|
+
try {
|
|
227
|
+
const client = await loadClient(store, aid);
|
|
228
|
+
const result = await client.authenticate();
|
|
229
|
+
await client.close();
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
store.close();
|
|
234
|
+
}
|
|
229
235
|
});
|
|
230
|
-
accessToken =
|
|
236
|
+
const accessToken = authResult?.access_token;
|
|
231
237
|
const elapsed = Date.now() - start;
|
|
232
238
|
if (accessToken) {
|
|
233
239
|
log({ step: 'Auth', index: 7, ok: true, detail: `${aid} ${i18n.authOk} (login1→login2→token)`, ms: elapsed });
|
|
@@ -270,16 +276,17 @@ async function runCheck(aid, formatJson) {
|
|
|
270
276
|
try {
|
|
271
277
|
const start = Date.now();
|
|
272
278
|
const aunPath = process.env.AUN_HOME || defaultAunPath();
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
await suppressSdkOutput(async () => {
|
|
280
|
+
const store = await getAidStore({ slotId: SLOT.netcheck, aunPath });
|
|
281
|
+
try {
|
|
282
|
+
const client = await loadClient(store, aid);
|
|
283
|
+
await client.connect();
|
|
284
|
+
await client.call('meta.ping', {});
|
|
285
|
+
await client.close();
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
store.close();
|
|
289
|
+
}
|
|
283
290
|
});
|
|
284
291
|
const elapsed = Date.now() - start;
|
|
285
292
|
log({ step: 'Ping', index: 9, ok: true, detail: `meta.ping ${i18n.pingOk}`, ms: elapsed });
|
|
@@ -362,14 +369,14 @@ async function runCheck(aid, formatJson) {
|
|
|
362
369
|
return results;
|
|
363
370
|
}
|
|
364
371
|
if (!formatJson) {
|
|
365
|
-
console.log(` ${DIM}[10/
|
|
372
|
+
console.log(` ${DIM}[10/11]${RST} ${CYAN}Echo${RST} ${DIM}${targets.length} target(s)${RST}`);
|
|
366
373
|
}
|
|
367
374
|
const echoResults = [];
|
|
368
375
|
// 读取所有目标的 agent.md(含签名验证)获取昵称和类型
|
|
369
376
|
const { agentmdGet: agentmdGetFn } = await import('../aun/aid/index.js');
|
|
370
377
|
const targetMeta = new Map();
|
|
371
378
|
if (!formatJson) {
|
|
372
|
-
console.log(` ${DIM}[10/
|
|
379
|
+
console.log(` ${DIM}[10/11]${RST} ${CYAN}Echo${RST} ${DIM}reading agent.md for ${targets.length} target(s)...${RST}`);
|
|
373
380
|
}
|
|
374
381
|
await Promise.all(targets.map(async (t) => {
|
|
375
382
|
const start = Date.now();
|
|
@@ -420,29 +427,31 @@ async function runCheck(aid, formatJson) {
|
|
|
420
427
|
const label = targetLabel(target);
|
|
421
428
|
try {
|
|
422
429
|
const replyText = await suppressSdkOutput(async () => {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
await client.call('message.
|
|
430
|
+
const store = await getAidStore({ slotId: SLOT.netcheck, aunPath });
|
|
431
|
+
try {
|
|
432
|
+
const client = await loadClient(store, aid);
|
|
433
|
+
await client.connect();
|
|
434
|
+
// 取基线 seq
|
|
435
|
+
const baseline = await client.call('message.pull', { limit: 100 });
|
|
436
|
+
const baselineSeq = baseline?.latest_seq ?? 0;
|
|
437
|
+
if (baselineSeq > 0) {
|
|
438
|
+
await client.call('message.ack', { seq: baselineSeq });
|
|
439
|
+
}
|
|
440
|
+
await client.call('message.send', {
|
|
441
|
+
to: target,
|
|
442
|
+
payload: { type: 'text', text: 'echo[nc]' },
|
|
443
|
+
encrypt: false,
|
|
444
|
+
});
|
|
445
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
446
|
+
const pullResult = await client.call('message.pull', { after_seq: baselineSeq, limit: 10 });
|
|
447
|
+
await client.close();
|
|
448
|
+
const messages = pullResult?.messages || [];
|
|
449
|
+
const reply = messages.find((m) => m.from === target && m.payload?.text?.includes('[EvolClaw.'));
|
|
450
|
+
return reply?.payload?.text || null;
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
store.close();
|
|
434
454
|
}
|
|
435
|
-
await client.call('message.send', {
|
|
436
|
-
to: target,
|
|
437
|
-
payload: { type: 'text', text: 'echo[nc]' },
|
|
438
|
-
encrypt: false,
|
|
439
|
-
});
|
|
440
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
441
|
-
const pullResult = await client.call('message.pull', { after_seq: baselineSeq, limit: 10 });
|
|
442
|
-
await client.close().catch(() => { });
|
|
443
|
-
const messages = pullResult?.messages || [];
|
|
444
|
-
const reply = messages.find((m) => m.from === target && m.payload?.text?.includes('[EvolClaw.'));
|
|
445
|
-
return reply?.payload?.text || null;
|
|
446
455
|
});
|
|
447
456
|
const elapsed = Date.now() - targetStart;
|
|
448
457
|
if (replyText) {
|
|
@@ -481,6 +490,77 @@ async function runCheck(aid, formatJson) {
|
|
|
481
490
|
catch (e) {
|
|
482
491
|
log({ step: 'Echo', index: 10, ok: false, detail: `echo ${i18n.failed}: ${e.message?.slice(0, 100) || String(e)}` });
|
|
483
492
|
}
|
|
493
|
+
// ── Step 11: 踢人测试 / extra_info 验证 ──
|
|
494
|
+
if (kickTest) {
|
|
495
|
+
try {
|
|
496
|
+
const kickStart = Date.now();
|
|
497
|
+
const aunPath = process.env.AUN_HOME || defaultAunPath();
|
|
498
|
+
const hostname = os.hostname();
|
|
499
|
+
const kickResult = await suppressSdkOutput(async () => {
|
|
500
|
+
const store = await getAidStore({ slotId: SLOT.netcheck, aunPath });
|
|
501
|
+
try {
|
|
502
|
+
const client = await loadClient(store, aid);
|
|
503
|
+
const disconnectEvents = [];
|
|
504
|
+
client.on('gateway.disconnect', (payload) => {
|
|
505
|
+
disconnectEvents.push(payload);
|
|
506
|
+
});
|
|
507
|
+
await client.connect({
|
|
508
|
+
connection_kind: 'long',
|
|
509
|
+
extra_info: {
|
|
510
|
+
app: 'evolclaw',
|
|
511
|
+
role: 'netcheck-kicktest',
|
|
512
|
+
pid: process.pid,
|
|
513
|
+
hostname,
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
// 等待 3 秒收集可能的 disconnect 事件
|
|
517
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
518
|
+
await client.close();
|
|
519
|
+
return { connected: true, disconnectEvents };
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
store.close();
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
const elapsed = Date.now() - kickStart;
|
|
526
|
+
const { connected, disconnectEvents } = kickResult;
|
|
527
|
+
if (connected) {
|
|
528
|
+
if (disconnectEvents.length > 0) {
|
|
529
|
+
const evt = disconnectEvents[0];
|
|
530
|
+
const selfInfo = evt.detail?.self_extra_info ? JSON.stringify(evt.detail.self_extra_info) : 'none';
|
|
531
|
+
const newInfo = evt.detail?.new_extra_info ? JSON.stringify(evt.detail.new_extra_info) : 'none';
|
|
532
|
+
log({
|
|
533
|
+
step: 'KickTest',
|
|
534
|
+
index: 11,
|
|
535
|
+
ok: true,
|
|
536
|
+
detail: `connected, received disconnect (code=${evt.code}, self=${selfInfo}, new=${newInfo})`,
|
|
537
|
+
ms: elapsed,
|
|
538
|
+
});
|
|
539
|
+
if (!formatJson) {
|
|
540
|
+
console.log(` ${YELLOW}⚠${RST} ${DIM}This test may have disconnected the running daemon (it will auto-reconnect)${RST}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
log({
|
|
545
|
+
step: 'KickTest',
|
|
546
|
+
index: 11,
|
|
547
|
+
ok: true,
|
|
548
|
+
detail: `connected, no disconnect event (daemon may not be running or already disconnected)`,
|
|
549
|
+
ms: elapsed,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
log({ step: 'KickTest', index: 11, ok: false, detail: `failed to connect`, ms: elapsed });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
log({ step: 'KickTest', index: 11, ok: false, detail: `kick test failed: ${e.message?.slice(0, 100) || String(e)}` });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
log({ step: 'KickTest', index: 11, ok: true, detail: `skipped (use --kick-test to enable)`, skipped: true });
|
|
563
|
+
}
|
|
484
564
|
return results;
|
|
485
565
|
}
|
|
486
566
|
// 添加这个常量到 ANSI 块(如果还没有)
|
|
@@ -563,10 +643,11 @@ function shuffle(arr) {
|
|
|
563
643
|
export async function cmdNet(args) {
|
|
564
644
|
const sub = args[0];
|
|
565
645
|
const formatJson = args.includes('--format') && args.includes('json');
|
|
646
|
+
const kickTest = args.includes('--kick-test');
|
|
566
647
|
if (isHelpFlag(sub)) {
|
|
567
|
-
console.log(`用法: evolclaw net check [<aid>] [--format json]
|
|
648
|
+
console.log(`用法: evolclaw net check [<aid>] [--format json] [--kick-test]
|
|
568
649
|
|
|
569
|
-
检查 AUN 网络链路连通性(
|
|
650
|
+
检查 AUN 网络链路连通性(11 步逐层诊断)。
|
|
570
651
|
|
|
571
652
|
步骤:
|
|
572
653
|
1. DNS (AID) AID 域名解析
|
|
@@ -578,13 +659,15 @@ export async function cmdNet(args) {
|
|
|
578
659
|
7. Auth AID 认证(login1 + login2 → token)
|
|
579
660
|
8. Session 会话建立
|
|
580
661
|
9. Ping meta.ping RPC
|
|
581
|
-
10.
|
|
662
|
+
10. Echo self-to-self 消息收发
|
|
663
|
+
11. KickTest 踢人测试 / extra_info 验证(可选)
|
|
582
664
|
|
|
583
665
|
参数:
|
|
584
666
|
<aid> 要检查的 AID(可选,默认取前 3 个本地 AID)
|
|
585
667
|
|
|
586
668
|
选项:
|
|
587
|
-
--format json JSON
|
|
669
|
+
--format json JSON 格式输出
|
|
670
|
+
--kick-test 启用踢人测试(会断开 daemon 长连接,daemon 会自动重连)`);
|
|
588
671
|
return;
|
|
589
672
|
}
|
|
590
673
|
if (sub && sub !== 'check' && !sub.startsWith('-') && !sub.includes('.')) {
|
|
@@ -616,7 +699,7 @@ export async function cmdNet(args) {
|
|
|
616
699
|
if (!formatJson) {
|
|
617
700
|
console.log(`\n${BOLD}── ${targetAid} ──${RST}\n`);
|
|
618
701
|
}
|
|
619
|
-
const results = await runCheck(targetAid, formatJson);
|
|
702
|
+
const results = await runCheck(targetAid, formatJson, kickTest);
|
|
620
703
|
allResults.push({ aid: targetAid, checks: results });
|
|
621
704
|
}
|
|
622
705
|
if (formatJson) {
|
package/dist/cli/watch-msg.js
CHANGED
|
@@ -131,11 +131,11 @@ function shortAid(aid) {
|
|
|
131
131
|
return aid.split('.')[0];
|
|
132
132
|
}
|
|
133
133
|
// ==================== Data Layer ====================
|
|
134
|
-
function getSessionsAunDir() {
|
|
134
|
+
export function getSessionsAunDir() {
|
|
135
135
|
const p = resolvePaths();
|
|
136
136
|
return path.join(p.sessionsDir, 'aun');
|
|
137
137
|
}
|
|
138
|
-
function listLocalAids(aunDir) {
|
|
138
|
+
export function listLocalAids(aunDir) {
|
|
139
139
|
try {
|
|
140
140
|
return fs.readdirSync(aunDir, { withFileTypes: true })
|
|
141
141
|
.filter(e => e.isDirectory())
|
|
@@ -145,7 +145,7 @@ function listLocalAids(aunDir) {
|
|
|
145
145
|
return [];
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
function listPeers(aunDir, localAid) {
|
|
148
|
+
export function listPeers(aunDir, localAid) {
|
|
149
149
|
const aidDir = path.join(aunDir, encodeSegment(localAid));
|
|
150
150
|
try {
|
|
151
151
|
return fs.readdirSync(aidDir, { withFileTypes: true })
|
|
@@ -156,7 +156,7 @@ function listPeers(aunDir, localAid) {
|
|
|
156
156
|
return [];
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
function readMessages(aunDir, localAid, peerId) {
|
|
159
|
+
export function readMessages(aunDir, localAid, peerId) {
|
|
160
160
|
const msgPath = path.join(aunDir, encodeSegment(localAid), encodeSegment(peerId), 'messages.jsonl');
|
|
161
161
|
return readAllJsonlLines(msgPath);
|
|
162
162
|
}
|
|
@@ -173,7 +173,7 @@ function readPeerName(aunDir, localAid, peerId) {
|
|
|
173
173
|
function encodeSegment(s) {
|
|
174
174
|
return s.replace(/[/%\\:*?"<>|]/g, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
175
175
|
}
|
|
176
|
-
function loadAidInfo(aunDir, aid) {
|
|
176
|
+
export function loadAidInfo(aunDir, aid) {
|
|
177
177
|
const peers = listPeers(aunDir, aid);
|
|
178
178
|
let totalIn = 0, totalOut = 0;
|
|
179
179
|
for (const peer of peers) {
|
|
@@ -187,7 +187,7 @@ function loadAidInfo(aunDir, aid) {
|
|
|
187
187
|
}
|
|
188
188
|
return { aid, totalIn, totalOut, peerCount: peers.length };
|
|
189
189
|
}
|
|
190
|
-
function loadPeerInfos(aunDir, localAid) {
|
|
190
|
+
export function loadPeerInfos(aunDir, localAid) {
|
|
191
191
|
const peers = listPeers(aunDir, localAid);
|
|
192
192
|
const infos = [];
|
|
193
193
|
for (const peerId of peers) {
|
|
@@ -207,7 +207,7 @@ function loadPeerInfos(aunDir, localAid) {
|
|
|
207
207
|
infos.sort((a, b) => b.lastAt - a.lastAt);
|
|
208
208
|
return infos;
|
|
209
209
|
}
|
|
210
|
-
function loadAllMessages(aunDir, localAid) {
|
|
210
|
+
export function loadAllMessages(aunDir, localAid) {
|
|
211
211
|
const peers = listPeers(aunDir, localAid);
|
|
212
212
|
const all = [];
|
|
213
213
|
for (const peer of peers) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* watch-web 调试日志 — 写入 $EVOLCLAW_HOME/logs/watch-web.log。
|
|
3
|
+
*
|
|
4
|
+
* cmdWatchWeb 启动时清空该文件并调用 setDebugLog 注入 writer,
|
|
5
|
+
* 各 source / server 通过 dlog() 写调试信息,建立「运行→看日志→定位」的闭环。
|
|
6
|
+
*/
|
|
7
|
+
let _writer = null;
|
|
8
|
+
export function setDebugLog(writer) {
|
|
9
|
+
_writer = writer;
|
|
10
|
+
}
|
|
11
|
+
export function dlog(line) {
|
|
12
|
+
if (_writer) {
|
|
13
|
+
try {
|
|
14
|
+
_writer(line);
|
|
15
|
+
}
|
|
16
|
+
catch { /* ignore */ }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch Web 服务 — 本地浏览器监控面板的后端。
|
|
3
|
+
*
|
|
4
|
+
* - HTTP: 静态资源 + 配对 API
|
|
5
|
+
* - WebSocket: 订阅式实时推送(aid / msg / session)
|
|
6
|
+
* - 鉴权: 6 位配对码(5 分钟有效)→ token(24h,有访问自动续期),持久化到磁盘
|
|
7
|
+
* - 安全: 绑定 0.0.0.0(支持远程访问),token 校验,只读
|
|
8
|
+
*
|
|
9
|
+
* 借鉴 Kite 控制台:配对码换 token、首消息鉴权、订阅式推送、访问日志。
|
|
10
|
+
*/
|
|
11
|
+
import http from 'http';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { WebSocketServer } from 'ws';
|
|
17
|
+
import { resolvePaths } from '../../paths.js';
|
|
18
|
+
import { setDebugLog } from './debug-log.js';
|
|
19
|
+
import { aidSource } from './sources/aid.js';
|
|
20
|
+
import { msgSource } from './sources/msg.js';
|
|
21
|
+
import { sessionSource } from './sources/session.js';
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const STATIC_DIR = path.join(__dirname, 'static');
|
|
24
|
+
const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 小时
|
|
25
|
+
const PAIRING_TTL_MS = 5 * 60 * 1000; // 配对码 5 分钟
|
|
26
|
+
const DEFAULT_PORT = 20030;
|
|
27
|
+
const SOURCES = {
|
|
28
|
+
aid: aidSource,
|
|
29
|
+
msg: msgSource,
|
|
30
|
+
session: sessionSource,
|
|
31
|
+
};
|
|
32
|
+
// ── Token 持久化 ──
|
|
33
|
+
function tokenStorePath() {
|
|
34
|
+
return path.join(resolvePaths().instanceDir, 'watch-web-tokens.json');
|
|
35
|
+
}
|
|
36
|
+
function loadTokens() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(tokenStorePath(), 'utf-8');
|
|
39
|
+
const store = JSON.parse(raw);
|
|
40
|
+
if (Array.isArray(store.tokens))
|
|
41
|
+
return store;
|
|
42
|
+
}
|
|
43
|
+
catch { /* missing or corrupt */ }
|
|
44
|
+
return { tokens: [] };
|
|
45
|
+
}
|
|
46
|
+
function saveTokens(store) {
|
|
47
|
+
try {
|
|
48
|
+
fs.mkdirSync(resolvePaths().instanceDir, { recursive: true });
|
|
49
|
+
const tmp = tokenStorePath() + '.tmp';
|
|
50
|
+
fs.writeFileSync(tmp, JSON.stringify(store, null, 2));
|
|
51
|
+
fs.renameSync(tmp, tokenStorePath());
|
|
52
|
+
}
|
|
53
|
+
catch { /* best effort */ }
|
|
54
|
+
}
|
|
55
|
+
function pruneExpired(store, now) {
|
|
56
|
+
const before = store.tokens.length;
|
|
57
|
+
store.tokens = store.tokens.filter(t => now - t.lastActive < TOKEN_TTL_MS);
|
|
58
|
+
return store.tokens.length !== before;
|
|
59
|
+
}
|
|
60
|
+
/** 校验 token,命中则续期(更新 lastActive)并持久化 */
|
|
61
|
+
function validateAndRenew(token, now) {
|
|
62
|
+
if (!token)
|
|
63
|
+
return false;
|
|
64
|
+
const store = loadTokens();
|
|
65
|
+
const changed = pruneExpired(store, now);
|
|
66
|
+
const rec = store.tokens.find(t => t.token === token);
|
|
67
|
+
if (rec) {
|
|
68
|
+
rec.lastActive = now;
|
|
69
|
+
saveTokens(store);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (changed)
|
|
73
|
+
saveTokens(store);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
// ── 静态资源 ──
|
|
77
|
+
const MIME = {
|
|
78
|
+
'.html': 'text/html; charset=utf-8',
|
|
79
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
80
|
+
'.css': 'text/css; charset=utf-8',
|
|
81
|
+
'.json': 'application/json; charset=utf-8',
|
|
82
|
+
'.svg': 'image/svg+xml',
|
|
83
|
+
};
|
|
84
|
+
function serveStatic(req, res) {
|
|
85
|
+
let urlPath = (req.url || '/').split('?')[0];
|
|
86
|
+
if (urlPath === '/')
|
|
87
|
+
urlPath = '/index.html';
|
|
88
|
+
// 防目录穿越
|
|
89
|
+
const safe = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, '');
|
|
90
|
+
const file = path.join(STATIC_DIR, safe);
|
|
91
|
+
if (!file.startsWith(STATIC_DIR)) {
|
|
92
|
+
res.writeHead(403).end('Forbidden');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
fs.readFile(file, (err, data) => {
|
|
96
|
+
if (err) {
|
|
97
|
+
res.writeHead(404).end('Not Found');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' });
|
|
101
|
+
res.end(data);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function genPairingCode() {
|
|
105
|
+
return String(crypto.randomInt(0, 1000000)).padStart(6, '0');
|
|
106
|
+
}
|
|
107
|
+
function clientIp(req) {
|
|
108
|
+
const fwd = req.headers['x-forwarded-for'];
|
|
109
|
+
if (typeof fwd === 'string' && fwd)
|
|
110
|
+
return fwd.split(',')[0].trim();
|
|
111
|
+
return req.socket.remoteAddress || '?';
|
|
112
|
+
}
|
|
113
|
+
export async function startWatchWebServer(opts = {}) {
|
|
114
|
+
const log = opts.log || (() => { });
|
|
115
|
+
setDebugLog(log); // 把日志 writer 注入各 source,建立调试闭环
|
|
116
|
+
const pairingCode = genPairingCode();
|
|
117
|
+
const pairingExpiry = Date.now() + PAIRING_TTL_MS;
|
|
118
|
+
const server = http.createServer((req, res) => {
|
|
119
|
+
const url = req.url || '/';
|
|
120
|
+
if (req.method === 'POST' && url === '/api/pair') {
|
|
121
|
+
handlePair(req, res, pairingCode, pairingExpiry, log);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
serveStatic(req, res);
|
|
125
|
+
});
|
|
126
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
127
|
+
server.on('upgrade', (req, socket, head) => {
|
|
128
|
+
const { query } = parseUrl(req.url || '');
|
|
129
|
+
const token = query.token || '';
|
|
130
|
+
if (!validateAndRenew(token, Date.now())) {
|
|
131
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
132
|
+
socket.destroy();
|
|
133
|
+
log(`✗ WS 拒绝(无效 token) from ${req.socket.remoteAddress}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
137
|
+
handleConnection(ws, req, log);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
const port = await bindPort(server, opts.port ?? DEFAULT_PORT);
|
|
141
|
+
const url = `http://0.0.0.0:${port}`;
|
|
142
|
+
return {
|
|
143
|
+
url,
|
|
144
|
+
port,
|
|
145
|
+
pairingCode,
|
|
146
|
+
close() {
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
for (const client of wss.clients) {
|
|
149
|
+
try {
|
|
150
|
+
client.close();
|
|
151
|
+
}
|
|
152
|
+
catch { /* ignore */ }
|
|
153
|
+
}
|
|
154
|
+
wss.close();
|
|
155
|
+
server.close(() => resolve());
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// ── 配对 ──
|
|
161
|
+
function handlePair(req, res, pairingCode, pairingExpiry, log) {
|
|
162
|
+
let body = '';
|
|
163
|
+
req.on('data', (chunk) => {
|
|
164
|
+
body += chunk;
|
|
165
|
+
if (body.length > 4096)
|
|
166
|
+
req.destroy();
|
|
167
|
+
});
|
|
168
|
+
req.on('end', () => {
|
|
169
|
+
const ip = clientIp(req);
|
|
170
|
+
let code = '';
|
|
171
|
+
try {
|
|
172
|
+
code = String(JSON.parse(body).code || '');
|
|
173
|
+
}
|
|
174
|
+
catch { /* bad json */ }
|
|
175
|
+
if (Date.now() > pairingExpiry) {
|
|
176
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
177
|
+
res.end(JSON.stringify({ ok: false, reason: '配对码已过期,请重启 watch web' }));
|
|
178
|
+
log(`✗ 配对失败(码已过期) from ${ip}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (code !== pairingCode) {
|
|
182
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
183
|
+
res.end(JSON.stringify({ ok: false, reason: '配对码错误' }));
|
|
184
|
+
log(`✗ 配对失败(码错误: ${code}) from ${ip}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// 配对成功,发放持久 token
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
190
|
+
const store = loadTokens();
|
|
191
|
+
pruneExpired(store, now);
|
|
192
|
+
store.tokens.push({ token, createdAt: now, lastActive: now, label: ip });
|
|
193
|
+
saveTokens(store);
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
195
|
+
res.end(JSON.stringify({ ok: true, token }));
|
|
196
|
+
log(`✓ 配对成功 from ${ip}(token 已缓存,24h 有效)`);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// ── URL 解析 ──
|
|
200
|
+
function parseUrl(rawUrl) {
|
|
201
|
+
const qIdx = rawUrl.indexOf('?');
|
|
202
|
+
if (qIdx === -1)
|
|
203
|
+
return { path: rawUrl, query: {} };
|
|
204
|
+
const query = {};
|
|
205
|
+
for (const pair of rawUrl.slice(qIdx + 1).split('&')) {
|
|
206
|
+
const [k, v] = pair.split('=');
|
|
207
|
+
if (k)
|
|
208
|
+
query[decodeURIComponent(k)] = decodeURIComponent(v || '');
|
|
209
|
+
}
|
|
210
|
+
return { path: rawUrl.slice(0, qIdx), query };
|
|
211
|
+
}
|
|
212
|
+
// ── WebSocket 连接 ──
|
|
213
|
+
function handleConnection(ws, req, log) {
|
|
214
|
+
const ip = clientIp(req);
|
|
215
|
+
let unsubscribe = null;
|
|
216
|
+
let currentView = null;
|
|
217
|
+
log(`◆ WS 连接 from ${ip}`);
|
|
218
|
+
const send = (obj) => {
|
|
219
|
+
if (ws.readyState === ws.OPEN) {
|
|
220
|
+
try {
|
|
221
|
+
ws.send(JSON.stringify(obj));
|
|
222
|
+
}
|
|
223
|
+
catch { /* ignore */ }
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const switchSubscription = async (view, params) => {
|
|
227
|
+
if (unsubscribe) {
|
|
228
|
+
unsubscribe();
|
|
229
|
+
unsubscribe = null;
|
|
230
|
+
}
|
|
231
|
+
currentView = view;
|
|
232
|
+
const source = SOURCES[view];
|
|
233
|
+
if (!source) {
|
|
234
|
+
send({ type: 'error', message: `unknown view: ${view}` });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const snap = await source.snapshot(params);
|
|
239
|
+
send({ type: 'snapshot', view, data: snap });
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
send({ type: 'error', message: `snapshot failed: ${e?.message || e}` });
|
|
243
|
+
}
|
|
244
|
+
unsubscribe = source.subscribe(params, (data) => {
|
|
245
|
+
if (currentView === view)
|
|
246
|
+
send({ type: 'delta', view, data });
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
ws.on('message', async (raw) => {
|
|
250
|
+
let msg;
|
|
251
|
+
try {
|
|
252
|
+
msg = JSON.parse(raw.toString());
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (msg.type === 'ping') {
|
|
258
|
+
send({ type: 'pong' });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (msg.type === 'subscribe' && msg.view) {
|
|
262
|
+
const params = {};
|
|
263
|
+
if (msg.aid)
|
|
264
|
+
params.aid = msg.aid;
|
|
265
|
+
if (msg.peer)
|
|
266
|
+
params.peer = msg.peer;
|
|
267
|
+
if (msg.sessionId)
|
|
268
|
+
params.sessionId = msg.sessionId;
|
|
269
|
+
if (msg.project)
|
|
270
|
+
params.project = msg.project;
|
|
271
|
+
log(`▸ 订阅 ${msg.view}` +
|
|
272
|
+
`${msg.project ? ` project=${String(msg.project).slice(-24)}` : ''}` +
|
|
273
|
+
`${msg.aid ? ` aid=${String(msg.aid).split('.')[0]}` : ''}` +
|
|
274
|
+
`${msg.peer ? ` peer=${String(msg.peer).split('.')[0]}` : ''}` +
|
|
275
|
+
`${msg.sessionId ? ` session=${String(msg.sessionId).slice(0, 8)}` : ''} from ${ip}`);
|
|
276
|
+
await switchSubscription(msg.view, params);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
ws.on('close', () => {
|
|
280
|
+
if (unsubscribe) {
|
|
281
|
+
unsubscribe();
|
|
282
|
+
unsubscribe = null;
|
|
283
|
+
}
|
|
284
|
+
log(`◇ WS 断开 from ${ip}`);
|
|
285
|
+
});
|
|
286
|
+
ws.on('error', () => { });
|
|
287
|
+
}
|
|
288
|
+
// ── 端口绑定(首选端口被占则 +1,最多尝试 10 次)──
|
|
289
|
+
function bindPort(server, preferred) {
|
|
290
|
+
return new Promise((resolve, reject) => {
|
|
291
|
+
let attempt = 0;
|
|
292
|
+
const tryBind = (port) => {
|
|
293
|
+
server.once('error', (err) => {
|
|
294
|
+
if (err.code === 'EADDRINUSE' && attempt < 10) {
|
|
295
|
+
attempt++;
|
|
296
|
+
tryBind(port + 1);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
reject(err);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
server.listen(port, '0.0.0.0', () => resolve(port));
|
|
303
|
+
};
|
|
304
|
+
tryBind(preferred);
|
|
305
|
+
});
|
|
306
|
+
}
|