evolclaw 3.1.4 → 3.1.5

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 (85) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agents/claude-runner.js +348 -156
  3. package/dist/agents/kit-renderer.js +176 -21
  4. package/dist/aun/aid/agentmd.js +68 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/p2p.js +26 -2
  10. package/dist/aun/rpc/connection.js +23 -30
  11. package/dist/channels/aun.js +77 -88
  12. package/dist/channels/dingtalk.js +1 -0
  13. package/dist/channels/feishu.js +270 -190
  14. package/dist/channels/qqbot.js +1 -0
  15. package/dist/channels/wechat.js +1 -0
  16. package/dist/channels/wecom.js +1 -0
  17. package/dist/cli/agent.js +11 -5
  18. package/dist/cli/bench.js +40 -23
  19. package/dist/cli/index.js +170 -44
  20. package/dist/cli/init-channel.js +5 -1
  21. package/dist/cli/model.js +324 -0
  22. package/dist/cli/net-check.js +133 -50
  23. package/dist/cli/watch-msg.js +7 -7
  24. package/dist/cli/watch-web/debug-log.js +18 -0
  25. package/dist/cli/watch-web/server.js +306 -0
  26. package/dist/cli/watch-web/sources/aid.js +63 -0
  27. package/dist/cli/watch-web/sources/msg.js +70 -0
  28. package/dist/cli/watch-web/sources/session.js +638 -0
  29. package/dist/cli/watch-web/sources/types.js +10 -0
  30. package/dist/cli/watch-web/static/app.js +546 -0
  31. package/dist/cli/watch-web/static/index.html +54 -0
  32. package/dist/cli/watch-web/static/style.css +247 -0
  33. package/dist/core/channel-loader.js +7 -4
  34. package/dist/core/command-handler.js +81 -86
  35. package/dist/core/evolagent-registry.js +1 -1
  36. package/dist/core/evolagent.js +4 -4
  37. package/dist/core/interaction-router.js +59 -0
  38. package/dist/core/message/message-bridge.js +6 -6
  39. package/dist/core/message/message-log.js +2 -2
  40. package/dist/core/message/message-processor.js +86 -101
  41. package/dist/core/message/stream-idle-monitor.js +21 -0
  42. package/dist/core/model/model-catalog.js +215 -0
  43. package/dist/core/model/model-scope.js +250 -0
  44. package/dist/core/relation/peer-identity.js +40 -49
  45. package/dist/core/relation/peer-key.js +16 -0
  46. package/dist/core/session/session-fs-store.js +34 -55
  47. package/dist/core/session/session-key.js +24 -0
  48. package/dist/core/session/session-manager.js +308 -251
  49. package/dist/core/session/session-mapper.js +9 -4
  50. package/dist/core/trigger/manager.js +3 -3
  51. package/dist/core/trigger/scheduler.js +2 -1
  52. package/dist/index.js +6 -2
  53. package/dist/ipc.js +22 -0
  54. package/kits/docs/GUIDE.md +2 -2
  55. package/kits/docs/INDEX.md +11 -7
  56. package/kits/docs/channels/aun.md +56 -17
  57. package/kits/docs/channels/feishu.md +41 -12
  58. package/kits/docs/context-assembly.md +181 -0
  59. package/kits/docs/evolclaw/agent.md +49 -0
  60. package/kits/docs/evolclaw/aid.md +49 -0
  61. package/kits/docs/evolclaw/ctl.md +46 -0
  62. package/kits/docs/evolclaw/group.md +82 -0
  63. package/kits/docs/evolclaw/msg.md +86 -0
  64. package/kits/docs/evolclaw/rpc.md +35 -0
  65. package/kits/docs/evolclaw/storage.md +49 -0
  66. package/kits/docs/venues/aun-group.md +10 -0
  67. package/kits/docs/venues/aun-private.md +10 -0
  68. package/kits/docs/venues/client-desktop.md +10 -0
  69. package/kits/docs/venues/client-mobile.md +10 -0
  70. package/kits/docs/venues/feishu-group.md +13 -0
  71. package/kits/docs/venues/feishu-private.md +9 -0
  72. package/kits/docs/venues/group.md +11 -0
  73. package/kits/docs/venues/private.md +10 -0
  74. package/kits/eck_manifest.json +72 -36
  75. package/kits/rules/01-overview.md +20 -10
  76. package/kits/rules/06-channel.md +30 -27
  77. package/kits/templates/system-fragments/session.md +10 -3
  78. package/kits/templates/system-fragments/venue.md +9 -0
  79. package/package.json +11 -6
  80. package/dist/aun/aid/lifecycle-log.js +0 -33
  81. package/dist/utils/aid-lifecycle-log.js +0 -33
  82. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  83. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  84. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  85. package/kits/docs/evolclaw/tools.md +0 -25
@@ -0,0 +1,324 @@
1
+ /**
2
+ * `evolclaw model` —— 面向 agent 的模型管理命令集。
3
+ *
4
+ * 三级作用域由参数决定:
5
+ * (无) → 全局 defaults.json
6
+ * --self → agent config.json
7
+ * --self --peer → 关系 relations/<peerKey>/preferences.json
8
+ *
9
+ * 改某作用域后,对应范围所有会话的下一条消息即时生效(运行时按 关系>agent>全局 解析)。
10
+ * 与对话内 slash(/model /setmodel /effort /baseagent)互不影响。
11
+ * 设计见 docs/model-command-design.md。
12
+ */
13
+ import { isHelpFlag, wantsHelp } from './help.js';
14
+ import { ModelScopeError, normalizePeer, determineScope, activeBaseagent, readScope, writeScope, clearScope, resolveEffectiveModel, } from '../core/model/model-scope.js';
15
+ import { getCatalog, getModelInfo } from '../core/model/model-catalog.js';
16
+ const ALL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
17
+ const SCOPE_LABEL = {
18
+ global: '全局', agent: 'agent级', relation: '关系级',
19
+ };
20
+ function getArgValue(args, flag) {
21
+ const idx = args.indexOf(flag);
22
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
23
+ }
24
+ /** 输出 JSON 并退出(success=false 时 exit 1)。 */
25
+ function emit(formatJson, payload, textFn) {
26
+ if (formatJson) {
27
+ console.log(JSON.stringify(payload, null, 2));
28
+ }
29
+ else {
30
+ console.log(textFn());
31
+ }
32
+ }
33
+ function fail(formatJson, code, message) {
34
+ if (formatJson) {
35
+ console.log(JSON.stringify({ ok: false, code, error: message }, null, 2));
36
+ }
37
+ else {
38
+ console.error(`❌ ${message} (${code})`);
39
+ }
40
+ process.exit(1);
41
+ }
42
+ /**
43
+ * 解析 --self / --peer 为作用域选择器。
44
+ */
45
+ function parseSelector(args, formatJson) {
46
+ const self = getArgValue(args, '--self');
47
+ const peerRaw = getArgValue(args, '--peer');
48
+ let peerKey;
49
+ if (peerRaw !== undefined) {
50
+ try {
51
+ peerKey = normalizePeer(peerRaw);
52
+ }
53
+ catch (e) {
54
+ if (e instanceof ModelScopeError)
55
+ fail(formatJson, e.code, e.message);
56
+ throw e;
57
+ }
58
+ }
59
+ const sel = { self, peerKey };
60
+ // 触发依赖校验(PEER_WITHOUT_SELF)
61
+ try {
62
+ determineScope(sel);
63
+ }
64
+ catch (e) {
65
+ if (e instanceof ModelScopeError)
66
+ fail(formatJson, e.code, e.message);
67
+ throw e;
68
+ }
69
+ return sel;
70
+ }
71
+ const HELP = `用法: evolclaw model <command> [options]
72
+
73
+ Commands:
74
+ list 列出可用模型,标注各作用域命中
75
+ current 显示按优先级解析后实际生效的模型 + 来源
76
+ info <model-id> 查看单个模型详情(厂商/上下文/价格/模态/effort/状态)
77
+ use <model-id> 设置模型(作用域由 --self/--peer 决定)
78
+ reset 清除指定作用域的设置,回落上一级
79
+ effort <level> 设置推理强度(low|medium|high|xhigh|max|auto)
80
+
81
+ 作用域(由参数决定,越具体越优先:关系 > agent > 全局):
82
+ (无参数) 全局默认 → defaults.json
83
+ --self <aid> agent级 → config.json
84
+ --self <aid> --peer <X> 关系级 → relations/<peerKey>/preferences.json
85
+
86
+ 改某作用域后,对应范围所有会话的下一条消息即时生效。
87
+
88
+ Options:
89
+ --self <aid> 本端 AID
90
+ --peer <X> 对端:channelType#channelId 或裸 aid(裸 aid 视为 aun#<aid>)
91
+ --effort <level> (use 专用)同时设置推理强度
92
+ --format json 输出 JSON
93
+ --help, -h 各子命令均支持
94
+
95
+ 示例:
96
+ evolclaw model list
97
+ evolclaw model current --self bot.agentid.pub --peer aun#alice.agentid.pub
98
+ evolclaw model info deepseek-v4-pro
99
+ evolclaw model use opus
100
+ evolclaw model use deepseek-v4-pro --self bot.agentid.pub --peer alice.agentid.pub
101
+ evolclaw model effort high --self bot.agentid.pub
102
+ evolclaw model reset --self bot.agentid.pub --peer alice.agentid.pub`;
103
+ async function dispatch(sub, args, formatJson) {
104
+ switch (sub) {
105
+ case 'list': return await cmdList(args, formatJson);
106
+ case 'current': return await cmdCurrent(args, formatJson);
107
+ case 'info': return await cmdInfo(args, formatJson);
108
+ case 'use': return await cmdUse(args, formatJson);
109
+ case 'reset': return await cmdReset(args, formatJson);
110
+ case 'effort': return await cmdEffort(args, formatJson);
111
+ default:
112
+ fail(formatJson, 'UNKNOWN_SUBCOMMAND', `未知子命令: ${sub}(model --help 查看用法)`);
113
+ }
114
+ }
115
+ // ── list ──────────────────────────────────────────────────────────────
116
+ const ICON = {
117
+ global: '⬡', agent: '◆', relation: '★',
118
+ };
119
+ async function cmdList(args, formatJson) {
120
+ if (wantsHelp(args)) {
121
+ console.log(HELP);
122
+ return;
123
+ }
124
+ const sel = parseSelector(args, formatJson);
125
+ const ba = activeBaseagent(sel.self);
126
+ const cat = await getCatalog(sel.self, ba);
127
+ const resolved = resolveEffectiveModel(sel, ba);
128
+ // 各作用域当前值(仅可达作用域)
129
+ const scopes = {};
130
+ scopes.global = readScope('global', sel, ba);
131
+ if (sel.self)
132
+ scopes.agent = readScope('agent', sel, ba);
133
+ if (sel.self && sel.peerKey)
134
+ scopes.relation = readScope('relation', sel, ba);
135
+ emit(formatJson, {
136
+ ok: true,
137
+ effective: { model: resolved.model ?? null, source: resolved.source ?? null },
138
+ scopes,
139
+ catalogSource: cat.source,
140
+ models: cat.models,
141
+ }, () => {
142
+ const lines = [];
143
+ lines.push(`当前生效: ${resolved.model ?? '(未设置,回落 SDK 默认)'}` +
144
+ (resolved.source ? ` (来源: ${SCOPE_LABEL[resolved.source]})` : ''));
145
+ lines.push('');
146
+ const srcTag = cat.source === 'mock' ? ' [mock]'
147
+ : cat.source === 'remote' ? ' [remote]'
148
+ : '';
149
+ lines.push(`可用模型 (${cat.models.length})${srcTag}:`);
150
+ const byScope = (m) => {
151
+ const tags = [];
152
+ for (const s of ['relation', 'agent', 'global']) {
153
+ if (scopes[s]?.model === m)
154
+ tags.push(`${ICON[s]}${SCOPE_LABEL[s]}`);
155
+ }
156
+ return tags.join(' ');
157
+ };
158
+ for (const e of cat.models) {
159
+ const live = resolved.model === e.id ? '✓' : ' ';
160
+ const tag = byScope(e.id);
161
+ lines.push(` ${live} ${e.id.padEnd(28)} ${tag}`.trimEnd());
162
+ }
163
+ return lines.join('\n');
164
+ });
165
+ }
166
+ // ── current ───────────────────────────────────────────────────────────
167
+ async function cmdCurrent(args, formatJson) {
168
+ if (wantsHelp(args)) {
169
+ console.log(HELP);
170
+ return;
171
+ }
172
+ const sel = parseSelector(args, formatJson);
173
+ const ba = activeBaseagent(sel.self);
174
+ const resolved = resolveEffectiveModel(sel, ba);
175
+ emit(formatJson, {
176
+ ok: true,
177
+ model: resolved.model ?? null,
178
+ effort: resolved.effort ?? null,
179
+ source: resolved.source ?? null,
180
+ chain: resolved.chain.map(c => ({ scope: c.scope, model: c.model ?? null, hit: c.hit })),
181
+ }, () => {
182
+ const lines = [];
183
+ lines.push(`当前生效模型: ${resolved.model ?? '(未设置,回落 SDK 默认)'}`);
184
+ lines.push(`推理强度: ${resolved.effort ?? 'auto'}`);
185
+ lines.push(`来源: ${resolved.source ? SCOPE_LABEL[resolved.source] : '无'}`);
186
+ const chain = resolved.chain.map(c => `${SCOPE_LABEL[c.scope]}${c.hit ? ' ✓' : ''}${c.model ? `(${c.model})` : ''}`).join(' > ');
187
+ lines.push(`解析链: ${chain}`);
188
+ return lines.join('\n');
189
+ });
190
+ }
191
+ // ── info ──────────────────────────────────────────────────────────────
192
+ async function cmdInfo(args, formatJson) {
193
+ if (wantsHelp(args)) {
194
+ console.log(HELP);
195
+ return;
196
+ }
197
+ const modelId = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
198
+ if (!modelId)
199
+ fail(formatJson, 'MISSING_MODEL_ID', 'info 需要 <model-id>');
200
+ const self = getArgValue(args, '--self');
201
+ const info = await getModelInfo(modelId, self);
202
+ emit(formatJson, { ok: true, ...info }, () => {
203
+ const price = info.pricing;
204
+ const fmtPrice = (v) => v === null ? '— (mock)' : `$${v} / 1M tokens`;
205
+ return [
206
+ `模型: ${info.id}`,
207
+ ` 厂商: ${info.owned_by}`,
208
+ ` 上下文窗口: ${info.context_window ?? '—'} tokens`,
209
+ ` 最大输出: ${info.max_output_tokens ?? '—'} tokens`,
210
+ ` 输入价格: ${fmtPrice(price?.input_per_mtok ?? null)}`,
211
+ ` 输出价格: ${fmtPrice(price?.output_per_mtok ?? null)}`,
212
+ ` 支持模态: ${info.modalities.join(', ')}${info.mocked ? ' (mock)' : ''}`,
213
+ ` 推理强度: ${info.supports_effort ? '支持' : '不支持'}`,
214
+ ` 状态: ${info.status === 'available' ? '✓ 可用' : info.status}`,
215
+ ].join('\n');
216
+ });
217
+ }
218
+ // ── use / effort / reset 共用写入逻辑 ───────────────────────────────────
219
+ function describeWrite(scope, sel, model, effort, formatJson) {
220
+ emit(formatJson, {
221
+ ok: true,
222
+ scope,
223
+ self: sel.self ?? null,
224
+ peerKey: sel.peerKey ?? null,
225
+ model: model ?? undefined,
226
+ effort: effort ?? undefined,
227
+ }, () => {
228
+ const lines = ['✓ 已设置', ` 作用域: ${SCOPE_LABEL[scope]}` +
229
+ (sel.peerKey ? ` (${sel.peerKey})` : '') + (sel.self ? ` [self=${sel.self}]` : '')];
230
+ if (model !== null)
231
+ lines.push(` 模型: ${model}`);
232
+ if (effort !== null)
233
+ lines.push(` 推理强度: ${effort}`);
234
+ lines.push(' 生效: 该范围所有会话的下一条消息起生效。');
235
+ return lines.join('\n');
236
+ });
237
+ }
238
+ async function cmdUse(args, formatJson) {
239
+ if (wantsHelp(args)) {
240
+ console.log(HELP);
241
+ return;
242
+ }
243
+ const modelId = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
244
+ if (!modelId)
245
+ fail(formatJson, 'MISSING_MODEL_ID', 'use 需要 <model-id>');
246
+ const sel = parseSelector(args, formatJson);
247
+ const scope = determineScope(sel);
248
+ const ba = activeBaseagent(sel.self);
249
+ // 校验模型在 catalog 中
250
+ const cat = await getCatalog(sel.self, ba);
251
+ if (!cat.models.some(m => m.id === modelId)) {
252
+ fail(formatJson, 'UNKNOWN_MODEL', `模型不在目录中: ${modelId}(model list 查看可用模型)`);
253
+ }
254
+ const effort = getArgValue(args, '--effort');
255
+ if (effort !== undefined && !ALL_EFFORTS.includes(effort)) {
256
+ fail(formatJson, 'INVALID_EFFORT', `无效推理强度: ${effort}(${ALL_EFFORTS.join('/')})`);
257
+ }
258
+ const effortVal = effort === 'auto' ? null : (effort ?? undefined);
259
+ try {
260
+ writeScope(scope, sel, ba, { model: modelId, effort: effortVal });
261
+ }
262
+ catch (e) {
263
+ if (e instanceof ModelScopeError)
264
+ fail(formatJson, e.code, e.message);
265
+ throw e;
266
+ }
267
+ describeWrite(scope, sel, modelId, effort ?? null, formatJson);
268
+ }
269
+ async function cmdEffort(args, formatJson) {
270
+ if (wantsHelp(args)) {
271
+ console.log(HELP);
272
+ return;
273
+ }
274
+ const level = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
275
+ if (!level)
276
+ fail(formatJson, 'INVALID_EFFORT', 'effort 需要 <level>(low|medium|high|xhigh|max|auto)');
277
+ if (!ALL_EFFORTS.includes(level)) {
278
+ fail(formatJson, 'INVALID_EFFORT', `无效推理强度: ${level}(${ALL_EFFORTS.join('/')})`);
279
+ }
280
+ const sel = parseSelector(args, formatJson);
281
+ const scope = determineScope(sel);
282
+ const ba = activeBaseagent(sel.self);
283
+ const val = level === 'auto' ? null : level;
284
+ try {
285
+ writeScope(scope, sel, ba, { effort: val });
286
+ }
287
+ catch (e) {
288
+ if (e instanceof ModelScopeError)
289
+ fail(formatJson, e.code, e.message);
290
+ throw e;
291
+ }
292
+ describeWrite(scope, sel, null, level, formatJson);
293
+ }
294
+ async function cmdReset(args, formatJson) {
295
+ if (wantsHelp(args)) {
296
+ console.log(HELP);
297
+ return;
298
+ }
299
+ const sel = parseSelector(args, formatJson);
300
+ const scope = determineScope(sel);
301
+ const ba = activeBaseagent(sel.self);
302
+ try {
303
+ clearScope(scope, sel, ba);
304
+ }
305
+ catch (e) {
306
+ if (e instanceof ModelScopeError)
307
+ fail(formatJson, e.code, e.message);
308
+ throw e;
309
+ }
310
+ emit(formatJson, {
311
+ ok: true, scope, self: sel.self ?? null, peerKey: sel.peerKey ?? null,
312
+ }, () => {
313
+ return `✓ 已清除 ${SCOPE_LABEL[scope]} 设置,回落上一级(该范围所有会话下条消息生效)`;
314
+ });
315
+ }
316
+ export async function cmdModel(args) {
317
+ const sub = args[0];
318
+ const formatJson = getArgValue(args, '--format') === 'json';
319
+ if (!sub || isHelpFlag(sub)) {
320
+ console.log(HELP);
321
+ return;
322
+ }
323
+ await dispatch(sub, args, formatJson);
324
+ }
@@ -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 { createAunClient } from '../aun/aid/client.js';
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}/10]${RST} ${CYAN}${label}${RST}`; }
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 accessToken;
220
+ let authResult;
220
221
  try {
221
222
  const start = Date.now();
222
223
  const aunPath = process.env.AUN_HOME || defaultAunPath();
223
- const result = await suppressSdkOutput(async () => {
224
- const client = await createAunClient({ aunPath });
225
- await client.auth.createAid({ aid });
226
- const authResult = await client.auth.authenticate({ aid });
227
- await client.close().catch(() => { });
228
- return authResult;
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 = result?.access_token;
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
- const sendResult = await suppressSdkOutput(async () => {
274
- const client = await createAunClient({ aunPath });
275
- await client.auth.createAid({ aid });
276
- const authResult = await client.auth.authenticate({ aid });
277
- const at = authResult?.access_token || client._access_token;
278
- const gw = client._gatewayUrl || authResult?.gateway;
279
- await client.connect({ access_token: at, gateway: gw, connection_kind: 'short' }, { auto_reconnect: false });
280
- const r = await client.call('meta.ping', {});
281
- await client.close().catch(() => { });
282
- return r;
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/10]${RST} ${CYAN}Echo${RST} ${DIM}${targets.length} target(s)${RST}`);
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/10]${RST} ${CYAN}Echo${RST} ${DIM}reading agent.md for ${targets.length} target(s)...${RST}`);
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 client = await createAunClient({ aunPath });
424
- await client.auth.createAid({ aid });
425
- const authResult = await client.auth.authenticate({ aid });
426
- const at = authResult?.access_token || client._access_token;
427
- const gw = client._gatewayUrl || authResult?.gateway;
428
- await client.connect({ access_token: at, gateway: gw, slot_id: 'net-check', connection_kind: 'short' }, { auto_reconnect: false });
429
- // 取基线 seq
430
- const baseline = await client.call('message.pull', { limit: 100 });
431
- const baselineSeq = baseline?.latest_seq ?? 0;
432
- if (baselineSeq > 0) {
433
- await client.call('message.ack', { seq: baselineSeq });
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 网络链路连通性(10 步逐层诊断)。
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. Message self-to-self 消息收发
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) {
@@ -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
+ }