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.
Files changed (99) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/agents/claude-runner.js +398 -161
  3. package/dist/agents/kit-renderer.js +191 -25
  4. package/dist/aun/aid/agentmd.js +75 -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/group.js +2 -2
  10. package/dist/aun/msg/p2p.js +26 -2
  11. package/dist/aun/rpc/connection.js +23 -30
  12. package/dist/channels/aun.js +174 -99
  13. package/dist/channels/dingtalk.js +2 -1
  14. package/dist/channels/feishu.js +301 -199
  15. package/dist/channels/qqbot.js +2 -1
  16. package/dist/channels/wechat.js +2 -1
  17. package/dist/channels/wecom.js +2 -1
  18. package/dist/cli/agent.js +21 -16
  19. package/dist/cli/bench.js +41 -28
  20. package/dist/cli/help.js +8 -0
  21. package/dist/cli/index.js +176 -87
  22. package/dist/cli/init-channel.js +5 -1
  23. package/dist/cli/init.js +37 -21
  24. package/dist/cli/link-rules.js +1 -7
  25. package/dist/cli/model.js +549 -0
  26. package/dist/cli/net-check.js +133 -50
  27. package/dist/cli/watch-msg.js +7 -7
  28. package/dist/cli/watch-web/debug-log.js +18 -0
  29. package/dist/cli/watch-web/server.js +306 -0
  30. package/dist/cli/watch-web/sources/aid.js +63 -0
  31. package/dist/cli/watch-web/sources/msg.js +70 -0
  32. package/dist/cli/watch-web/sources/session.js +638 -0
  33. package/dist/cli/watch-web/sources/types.js +10 -0
  34. package/dist/cli/watch-web/static/app.js +546 -0
  35. package/dist/cli/watch-web/static/index.html +54 -0
  36. package/dist/cli/watch-web/static/style.css +247 -0
  37. package/dist/config-store.js +1 -22
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +261 -133
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -22
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/im-renderer.js +9 -20
  44. package/dist/core/message/message-bridge.js +13 -9
  45. package/dist/core/message/message-log.js +2 -2
  46. package/dist/core/message/message-processor.js +211 -123
  47. package/dist/core/message/stream-idle-monitor.js +21 -0
  48. package/dist/core/model/model-catalog.js +215 -0
  49. package/dist/core/model/model-scope.js +250 -0
  50. package/dist/core/relation/peer-identity.js +58 -55
  51. package/dist/core/relation/peer-key.js +16 -0
  52. package/dist/core/session/session-fs-store.js +34 -55
  53. package/dist/core/session/session-key.js +24 -0
  54. package/dist/core/session/session-manager.js +308 -251
  55. package/dist/core/session/session-mapper.js +9 -4
  56. package/dist/core/trigger/manager.js +3 -3
  57. package/dist/core/trigger/parser.js +4 -4
  58. package/dist/core/trigger/scheduler.js +22 -7
  59. package/dist/index.js +61 -7
  60. package/dist/ipc.js +23 -1
  61. package/dist/utils/error-utils.js +6 -0
  62. package/dist/utils/process-introspect.js +7 -5
  63. package/kits/docs/GUIDE.md +2 -2
  64. package/kits/docs/INDEX.md +8 -8
  65. package/kits/docs/channels/aun.md +56 -17
  66. package/kits/docs/channels/feishu.md +41 -12
  67. package/kits/docs/context-assembly.md +182 -0
  68. package/kits/docs/evolclaw/INDEX.md +43 -0
  69. package/kits/docs/evolclaw/agent.md +49 -0
  70. package/kits/docs/evolclaw/aid.md +49 -0
  71. package/kits/docs/evolclaw/ctl.md +46 -0
  72. package/kits/docs/evolclaw/group.md +89 -0
  73. package/kits/docs/evolclaw/model.md +51 -0
  74. package/kits/docs/evolclaw/msg.md +91 -0
  75. package/kits/docs/evolclaw/rpc.md +35 -0
  76. package/kits/docs/evolclaw/storage.md +49 -0
  77. package/kits/docs/venues/aun-group.md +10 -0
  78. package/kits/docs/venues/aun-private.md +10 -0
  79. package/kits/docs/venues/client-desktop.md +10 -0
  80. package/kits/docs/venues/client-mobile.md +10 -0
  81. package/kits/docs/venues/feishu-group.md +13 -0
  82. package/kits/docs/venues/feishu-private.md +9 -0
  83. package/kits/docs/venues/group.md +23 -0
  84. package/kits/docs/venues/private.md +10 -0
  85. package/kits/eck_manifest.json +81 -36
  86. package/kits/rules/01-overview.md +20 -10
  87. package/kits/rules/06-channel.md +34 -27
  88. package/kits/templates/system-fragments/baseagent.md +7 -1
  89. package/kits/templates/system-fragments/channel.md +7 -5
  90. package/kits/templates/system-fragments/commands.md +19 -0
  91. package/kits/templates/system-fragments/session.md +19 -3
  92. package/kits/templates/system-fragments/venue.md +24 -0
  93. package/package.json +10 -5
  94. package/dist/aun/aid/lifecycle-log.js +0 -33
  95. package/dist/utils/aid-lifecycle-log.js +0 -33
  96. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  97. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  98. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  99. package/kits/docs/evolclaw/tools.md +0 -25
@@ -0,0 +1,546 @@
1
+ /* EvolClaw Watch — 前端 WS 客户端 + 三 tab 渲染 */
2
+
3
+ const $ = (sel) => document.querySelector(sel);
4
+ const TOKEN_KEY = 'ecWatchToken';
5
+
6
+ // ── 配对 ──
7
+ async function pair(code) {
8
+ const resp = await fetch('/api/pair', {
9
+ method: 'POST',
10
+ headers: { 'Content-Type': 'application/json' },
11
+ body: JSON.stringify({ code }),
12
+ });
13
+ return resp.json();
14
+ }
15
+
16
+ function showPairPage() {
17
+ $('#pair-page').style.display = 'flex';
18
+ $('#app').style.display = 'none';
19
+ }
20
+ function showApp() {
21
+ $('#pair-page').style.display = 'none';
22
+ $('#app').style.display = 'flex';
23
+ }
24
+
25
+ function initPairUI() {
26
+ const input = $('#pair-input');
27
+ const btn = $('#pair-btn');
28
+ const err = $('#pair-error');
29
+ const submit = async () => {
30
+ const code = input.value.trim();
31
+ if (code.length !== 6) { err.textContent = '请输入 6 位配对码'; return; }
32
+ btn.disabled = true; err.textContent = '';
33
+ try {
34
+ const res = await pair(code);
35
+ if (res.ok) {
36
+ localStorage.setItem(TOKEN_KEY, res.token);
37
+ showApp();
38
+ startApp();
39
+ } else {
40
+ err.textContent = res.reason || '配对失败';
41
+ }
42
+ } catch (e) {
43
+ err.textContent = '网络错误';
44
+ } finally {
45
+ btn.disabled = false;
46
+ }
47
+ };
48
+ btn.onclick = submit;
49
+ input.onkeydown = (e) => { if (e.key === 'Enter') submit(); };
50
+ input.focus();
51
+ }
52
+
53
+ // ── WebSocket 客户端(自动重连)──
54
+ let ws = null;
55
+ let reconnectDelay = 1000;
56
+ let currentView = 'aid';
57
+ let pendingSub = null; // 重连后要恢复的订阅
58
+ const state = { aid: null, msg: null, session: null };
59
+
60
+ function setConnStatus(text, cls) {
61
+ const el = $('#conn-status');
62
+ el.textContent = text;
63
+ el.className = 'conn-status' + (cls ? ' ' + cls : '');
64
+ }
65
+
66
+ function connect() {
67
+ const token = localStorage.getItem(TOKEN_KEY);
68
+ if (!token) { showPairPage(); return; }
69
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
70
+ ws = new WebSocket(`${proto}://${location.host}/ws?token=${encodeURIComponent(token)}`);
71
+
72
+ ws.onopen = () => {
73
+ setConnStatus('● 已连接', 'ok');
74
+ reconnectDelay = 1000;
75
+ subscribe(currentView, pendingSub || {});
76
+ };
77
+
78
+ ws.onmessage = (ev) => {
79
+ let msg;
80
+ try { msg = JSON.parse(ev.data); } catch { return; }
81
+ if (msg.type === 'pong') return;
82
+ if (msg.type === 'error') { console.warn('server error:', msg.message); return; }
83
+ if (msg.type === 'snapshot' || msg.type === 'delta') {
84
+ state[msg.view] = msg.data;
85
+ if (msg.view === currentView) renderView(currentView);
86
+ }
87
+ };
88
+
89
+ ws.onclose = (ev) => {
90
+ if (ev.code === 1006 || ev.code === 4001) {
91
+ // 可能是 token 失效
92
+ }
93
+ setConnStatus('○ 重连中…', 'err');
94
+ setTimeout(connect, reconnectDelay);
95
+ reconnectDelay = Math.min(reconnectDelay * 1.5, 15000);
96
+ };
97
+
98
+ ws.onerror = () => { try { ws.close(); } catch {} };
99
+ }
100
+
101
+ function subscribe(view, params) {
102
+ pendingSub = params;
103
+ if (ws && ws.readyState === WebSocket.OPEN) {
104
+ ws.send(JSON.stringify({ type: 'subscribe', view, ...params }));
105
+ }
106
+ }
107
+
108
+ // 心跳
109
+ setInterval(() => {
110
+ if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
111
+ }, 20000);
112
+
113
+ // ── Tab 切换 ──
114
+ let msgSel = { aid: null, peer: null };
115
+ let sessSel = { sessionId: null, project: null };
116
+ let sessSearch = '';
117
+ let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
118
+
119
+ function switchView(view) {
120
+ currentView = view;
121
+ document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
122
+ document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
123
+ // 切换时按当前选择恢复订阅
124
+ if (view === 'msg') subscribe('msg', { aid: msgSel.aid, peer: msgSel.peer });
125
+ else if (view === 'session') subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project });
126
+ else subscribe('aid', {});
127
+ if (state[view]) renderView(view);
128
+ }
129
+
130
+ function initTabs() {
131
+ document.querySelectorAll('.tab').forEach(tab => {
132
+ tab.onclick = () => switchView(tab.dataset.view);
133
+ });
134
+ }
135
+
136
+ function renderView(view) {
137
+ if (view === 'aid') renderAid(state.aid);
138
+ else if (view === 'msg') renderMsg(state.msg);
139
+ else if (view === 'session') renderSession(state.session);
140
+ }
141
+
142
+ // ── 工具 ──
143
+ function esc(s) {
144
+ return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
145
+ }
146
+ function shortAid(aid) { return String(aid || '').split('.')[0]; }
147
+ function fmtBytes(b) {
148
+ if (!b) return '0';
149
+ const u = ['B', 'KB', 'MB', 'GB']; let i = Math.min(Math.floor(Math.log(b) / Math.log(1024)), 3);
150
+ return (b / Math.pow(1024, i)).toFixed(i ? 1 : 0) + u[i];
151
+ }
152
+ function fmtAgo(ts) {
153
+ if (!ts) return '—';
154
+ const s = Math.floor((Date.now() - ts) / 1000);
155
+ if (s < 60) return s + 's';
156
+ if (s < 3600) return Math.floor(s / 60) + 'm';
157
+ if (s < 86400) return Math.floor(s / 3600) + 'h';
158
+ return Math.floor(s / 86400) + 'd';
159
+ }
160
+ function fmtTime(ts) {
161
+ if (!ts) return '';
162
+ const d = new Date(ts);
163
+ const p = (n) => String(n).padStart(2, '0');
164
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
165
+ }
166
+
167
+ // ── AID 视图 ──
168
+ function renderAid(data) {
169
+ const el = $('#view-aid');
170
+ if (!data) { el.innerHTML = '<div class="empty">加载中…</div>'; return; }
171
+ const aids = data.aids || [];
172
+ const statsByAid = {};
173
+ for (const s of (data.stats || [])) statsByAid[s.aid] = s;
174
+
175
+ let html = '';
176
+ if (!data.daemonRunning) {
177
+ html += '<div class="banner">⚠ EvolClaw 主进程未运行,仅显示最近活动记录</div>';
178
+ }
179
+ if (!aids.length) {
180
+ html += '<div class="empty">暂无 AID</div>';
181
+ el.innerHTML = html;
182
+ return;
183
+ }
184
+
185
+ html += '<table><thead><tr>' +
186
+ '<th>状态</th><th>AID</th><th>收</th><th>发</th><th>系统</th>' +
187
+ '<th>入字节</th><th>出字节</th><th>peers</th><th>重连</th><th>最后活动</th><th>最近消息</th>' +
188
+ '</tr></thead><tbody>';
189
+
190
+ for (const a of aids) {
191
+ const s = statsByAid[a.aid] || {};
192
+ const status = a.status || (a.lastEvent === 'disconnected' ? 'disconnected' : 'connected');
193
+ const dotCls = status === 'connected' ? 'on' : (status === 'reconnecting' ? 'idle' : 'off');
194
+ const name = s.selfName || a.agentName || '';
195
+ const lastTs = Math.max(s.lastReceivedAt || 0, s.lastSentAt || 0, a.lastActivity || 0);
196
+ let preview = '';
197
+ if (s.lastReceivedText && (s.lastReceivedAt || 0) >= (s.lastSentAt || 0)) {
198
+ preview = '↓ ' + shortAid(s.lastReceivedFrom) + ': ' + s.lastReceivedText;
199
+ } else if (s.lastSentText) {
200
+ preview = '↑ ' + shortAid(s.lastSentTo) + ': ' + s.lastSentText;
201
+ }
202
+ html += '<tr>' +
203
+ `<td><span class="dot ${dotCls}"></span>${esc(status)}</td>` +
204
+ `<td>${esc(shortAid(a.aid))}${name ? ` <span style="color:var(--dim)">(${esc(name)})</span>` : ''}</td>` +
205
+ `<td>${s.messagesReceived ?? 0}</td><td>${s.messagesSent ?? 0}</td>` +
206
+ `<td>${s.systemReceived ?? 0}/${s.systemSent ?? 0}</td>` +
207
+ `<td>${fmtBytes(s.bytesReceived)}</td><td>${fmtBytes(s.bytesSent)}</td>` +
208
+ `<td>${s.uniquePeerCount ?? a.peerCount ?? 0}</td><td>${a.reconnectCount ?? 0}</td>` +
209
+ `<td>${fmtAgo(lastTs)}</td>` +
210
+ `<td class="preview">${esc(preview.replace(/\n/g, ' ').slice(0, 80))}</td>` +
211
+ '</tr>';
212
+ }
213
+ html += '</tbody></table>';
214
+ el.innerHTML = html;
215
+ }
216
+
217
+ // ── Messages 视图 ──
218
+ function renderMsg(data) {
219
+ if (!data) return;
220
+ const aids = data.aids || [];
221
+ const peers = data.peers || [];
222
+ const messages = data.messages || [];
223
+
224
+ // 左:AID 列表
225
+ let aidsHtml = '<div class="col-title">AID</div>';
226
+ for (const a of aids) {
227
+ const sel = a.aid === msgSel.aid ? ' sel' : '';
228
+ aidsHtml += `<div class="list-item${sel}" data-aid="${esc(a.aid)}">` +
229
+ `<div class="name">${esc(shortAid(a.aid))}</div>` +
230
+ `<div class="sub">↓${a.totalIn} ↑${a.totalOut} · ${a.peerCount} peers</div></div>`;
231
+ }
232
+ $('#msg-aids').innerHTML = aidsHtml;
233
+ $('#msg-aids').querySelectorAll('.list-item').forEach(item => {
234
+ item.onclick = () => { msgSel = { aid: item.dataset.aid, peer: null }; subscribe('msg', msgSel); };
235
+ });
236
+
237
+ // 中:对端列表
238
+ let peersHtml = '<div class="col-title">Peers</div>';
239
+ if (msgSel.aid) {
240
+ const allSel = msgSel.peer === null ? ' sel' : '';
241
+ peersHtml += `<div class="list-item${allSel}" data-peer=""><div class="name">All</div>` +
242
+ `<div class="sub">${peers.length} peers</div></div>`;
243
+ for (const p of peers) {
244
+ const sel = p.peerId === msgSel.peer ? ' sel' : '';
245
+ peersHtml += `<div class="list-item${sel}" data-peer="${esc(p.peerId)}">` +
246
+ `<div class="name">${esc(p.peerName || shortAid(p.peerId))}</div>` +
247
+ `<div class="sub">↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
248
+ }
249
+ } else {
250
+ peersHtml += '<div class="empty">← 选择一个 AID</div>';
251
+ }
252
+ $('#msg-peers').innerHTML = peersHtml;
253
+ $('#msg-peers').querySelectorAll('.list-item').forEach(item => {
254
+ item.onclick = () => { msgSel = { aid: msgSel.aid, peer: item.dataset.peer || null }; subscribe('msg', msgSel); };
255
+ });
256
+
257
+ // 右:消息流
258
+ const stream = $('#msg-stream');
259
+ if (!msgSel.aid) { stream.innerHTML = '<div class="empty">选择 AID 查看消息</div>'; return; }
260
+ const atBottom = stream.scrollHeight - stream.scrollTop - stream.clientHeight < 60;
261
+ let msgHtml = '';
262
+ for (const m of messages) {
263
+ const cls = m.dir === 'in' ? 'in' : 'out';
264
+ const arrow = m.dir === 'in' ? '↓' : '↑';
265
+ const from = shortAid(m.from), to = shortAid(m.to);
266
+ const tags = [];
267
+ if (m.chatType === 'group') tags.push('群聊');
268
+ if (m.encrypt != null) tags.push(m.encrypt ? '密文' : '明文');
269
+ if (m.chatmode) tags.push(m.chatmode === 'proactive' ? '自主' : '响应');
270
+ const tagHtml = tags.map(t => `<span class="tag">${esc(t)}</span>`).join('');
271
+ msgHtml += `<div class="bubble ${cls}">` +
272
+ `<div class="meta">${fmtTime(m.ts)} ${arrow} ${esc(from)}→${esc(to)}${tagHtml}</div>` +
273
+ `<div class="body">${esc(m.content)}</div></div>`;
274
+ }
275
+ stream.innerHTML = msgHtml || '<div class="empty">暂无消息</div>';
276
+ if (atBottom) stream.scrollTop = stream.scrollHeight;
277
+ }
278
+
279
+ // ── Sessions 视图 ──
280
+ function renderSession(data) {
281
+ if (!data) return;
282
+ const projects = data.projects || [];
283
+ const transcripts = data.transcripts || [];
284
+ const turns = data.turns || [];
285
+ // 项目选择:用户显式选过就以本地状态为准(避免 stale snapshot 把下拉拨回去);
286
+ // 否则跟随服务端解析出的默认项目。
287
+ if (!sessSel.project) sessSel.project = data.project || null;
288
+ // 若本次 snapshot 不是当前选中项目的数据(stale),忽略其列表,等正确的回来
289
+ if (sessSel.project && data.project && data.project !== sessSel.project) {
290
+ return;
291
+ }
292
+
293
+ // 搜索过滤
294
+ const q = sessSearch.trim().toLowerCase();
295
+ const filtered = q
296
+ ? transcripts.filter(t => (t.title || '').toLowerCase().includes(q) || (t.firstUser || '').toLowerCase().includes(q))
297
+ : transcripts;
298
+
299
+ // 左栏:过滤条 + 列表
300
+ const projOpts = projects.map(p =>
301
+ `<option value="${esc(p.encoded)}"${p.encoded === sessSel.project ? ' selected' : ''}>${esc(p.label)} (${p.count})</option>`
302
+ ).join('');
303
+ let listHtml = '<div class="sess-filter">' +
304
+ `<select id="sess-project">${projOpts}</select>` +
305
+ `<input id="sess-search" type="text" placeholder="搜索标题/首条消息…" value="${esc(sessSearch)}">` +
306
+ `<div class="sess-count">${filtered.length} / ${transcripts.length} 个会话</div></div>` +
307
+ '<div class="sess-items">';
308
+
309
+ if (!filtered.length) {
310
+ listHtml += '<div class="empty">' + (transcripts.length ? '无匹配会话' : '该项目暂无会话') + '</div>';
311
+ }
312
+ for (const t of filtered) {
313
+ const sel = t.id === sessSel.sessionId ? ' sel' : '';
314
+ const title = t.title || t.firstUser || t.id.slice(0, 8);
315
+ let badge = '';
316
+ if (t.bound) {
317
+ const dot = t.online ? '<span class="dot on"></span>' : '<span class="dot idle"></span>';
318
+ badge = `<span class="bind-badge">${dot}${esc(t.boundChannel || '')}·${esc(shortAid(t.boundPeer || ''))}</span>`;
319
+ }
320
+ const msgs = `<span class="msg-count" title="用户输入 ${t.userMsgs || 0} 条 / 共 ${t.totalMsgs || 0} 条消息">💬 ${t.userMsgs || 0}/${t.totalMsgs || 0}</span>`;
321
+ listHtml += `<div class="list-item${sel}" data-sid="${esc(t.id)}">` +
322
+ `<div class="name">${esc(title)}</div>` +
323
+ `<div class="sub">${fmtAgo(t.lastActivity)} · ${msgs}${t.gitBranch ? ' · ' + esc(t.gitBranch) : ''}${badge}</div>` +
324
+ '</div>';
325
+ }
326
+ listHtml += '</div>';
327
+ $('#sess-list').innerHTML = listHtml;
328
+
329
+ // 绑定交互(注意保持搜索框焦点)
330
+ const projSel = $('#sess-project');
331
+ if (projSel) projSel.onchange = () => {
332
+ sessSel = { sessionId: null, project: projSel.value };
333
+ sessSearch = '';
334
+ subscribe('session', { project: sessSel.project });
335
+ };
336
+ const searchEl = $('#sess-search');
337
+ if (searchEl) {
338
+ searchEl.oninput = () => { sessSearch = searchEl.value; renderSession(state.session); };
339
+ if (q) { searchEl.focus(); searchEl.setSelectionRange(searchEl.value.length, searchEl.value.length); }
340
+ }
341
+ $('#sess-list').querySelectorAll('.list-item').forEach(item => {
342
+ item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project }; subscribe('session', sessSel); };
343
+ });
344
+
345
+ // 右:transcript 详情
346
+ const detail = $('#sess-detail');
347
+ if (!sessSel.sessionId) { detail.innerHTML = '<div class="empty">选择会话查看 CC 日志</div>'; return; }
348
+ if (!turns.length) { detail.innerHTML = '<div class="empty">该会话暂无内容</div>'; return; }
349
+ const h = data.header || {};
350
+ const atBottom = detail.scrollHeight - detail.scrollTop - detail.clientHeight < 60;
351
+ let html = renderSessHeader(h);
352
+ // 视图切换工具条
353
+ html += '<div class="sess-toolbar">' +
354
+ `<button class="view-toggle${sessChatMode ? ' active' : ''}" id="chat-toggle">` +
355
+ `${sessChatMode ? '💬 对话视图' : '📜 完整视图'}</button>` +
356
+ `<span class="toolbar-hint">${sessChatMode ? '只看用户与 Agent 的对话,处理过程已折叠' : '显示全部消息'}</span>` +
357
+ '</div>';
358
+ html += '<div class="turn-list">' + (sessChatMode ? renderChatView(turns) : renderFullView(turns)) + '</div>';
359
+ detail.innerHTML = html;
360
+
361
+ const toggle = $('#chat-toggle');
362
+ if (toggle) toggle.onclick = () => { sessChatMode = !sessChatMode; renderSession(state.session); };
363
+ if (atBottom) detail.scrollTop = detail.scrollHeight;
364
+ }
365
+
366
+ // 完整视图:所有轮次按 4 类渲染
367
+ function renderFullView(turns) {
368
+ let html = '';
369
+ for (const t of turns) {
370
+ const cat = t.category || t.role;
371
+ const c = CAT_META[cat] || CAT_META.system;
372
+ const usage = (t.inputTokens || t.outputTokens)
373
+ ? `<span class="turn-usage">${esc(t.model || '')} · in ${t.inputTokens || 0} / out ${t.outputTokens || 0}</span>` : '';
374
+ html += `<div class="turn cat-${cat}">` +
375
+ `<div class="turn-head"><span class="turn-role">${c.icon} ${c.label}</span>` +
376
+ `<span class="turn-time">${t.ts ? fmtTime(t.ts) : ''}</span>${usage}</div>` +
377
+ `<div class="turn-blocks">${renderBlocks(t.blocks || [])}</div></div>`;
378
+ }
379
+ return html;
380
+ }
381
+
382
+ // 对话视图:仿微信。只显示用户输入(左) + ec msg send 发出的消息(右),
383
+ // 其余连续的处理过程折叠成一个可展开的「处理过程」分隔条。
384
+ function renderChatView(turns) {
385
+ // 先把 turns 摊平成「对话项」与「处理项」的线性序列
386
+ const items = []; // {type:'in'|'out'|'proc', ...}
387
+ for (const t of turns) {
388
+ if (t.category === 'user_input') {
389
+ const text = (t.blocks || []).filter(b => b.kind === 'text').map(b => b.text).join('\n');
390
+ items.push({ type: 'in', text, ts: t.ts });
391
+ continue;
392
+ }
393
+ // 找该轮里的 ec msg send 发送块(可能多条)
394
+ const sends = (t.blocks || []).filter(b => b.kind === 'tool_use' && b.chat);
395
+ if (sends.length) {
396
+ for (const s of sends) items.push({ type: 'out', text: s.chat.text, peer: s.chat.peer, self: s.chat.self, ts: t.ts });
397
+ }
398
+ // 该轮里非对话的内容 → 处理过程(含思考/其他工具/结果/模型纯文本)
399
+ const procBlocks = (t.blocks || []).filter(b => !(b.kind === 'tool_use' && b.chat));
400
+ if (procBlocks.length && !(t.category === 'user_input')) {
401
+ items.push({ type: 'proc', cat: t.category, blocks: procBlocks, ts: t.ts });
402
+ }
403
+ }
404
+
405
+ // 合并连续的 proc 项为一组,渲染成可折叠分隔条
406
+ let html = '';
407
+ let i = 0;
408
+ while (i < items.length) {
409
+ const it = items[i];
410
+ if (it.type === 'in') {
411
+ html += `<div class="chat-row in"><div class="chat-bubble">${esc(it.text)}</div>` +
412
+ `<div class="chat-time">${it.ts ? fmtTime(it.ts) : ''}</div></div>`;
413
+ i++;
414
+ } else if (it.type === 'out') {
415
+ const peer = it.peer ? shortAid(it.peer) : '';
416
+ html += `<div class="chat-row out"><div class="chat-bubble">${esc(it.text)}</div>` +
417
+ `<div class="chat-time">${it.ts ? fmtTime(it.ts) : ''}${peer ? ' → ' + esc(peer) : ''}</div></div>`;
418
+ i++;
419
+ } else {
420
+ // 收集连续 proc
421
+ const group = [];
422
+ while (i < items.length && items[i].type === 'proc') { group.push(items[i]); i++; }
423
+ let inner = '';
424
+ for (const g of group) {
425
+ const c = CAT_META[g.cat] || CAT_META.system;
426
+ inner += `<div class="turn cat-${g.cat}"><div class="turn-head"><span class="turn-role">${c.icon} ${c.label}</span>` +
427
+ `<span class="turn-time">${g.ts ? fmtTime(g.ts) : ''}</span></div>` +
428
+ `<div class="turn-blocks">${renderBlocks(g.blocks)}</div></div>`;
429
+ }
430
+ html += `<details class="proc-group"><summary>⋯ ${group.length} 条处理过程(思考·工具·结果)</summary><div class="proc-body">${inner}</div></details>`;
431
+ }
432
+ }
433
+ if (!html) html = '<div class="empty">该会话没有用户对话消息</div>';
434
+ return html;
435
+ }
436
+
437
+ // 类别展示元数据
438
+ const CAT_META = {
439
+ user_input: { label: '用户输入', icon: '🟢' },
440
+ model_output: { label: '模型输出', icon: '🔵' },
441
+ tool_call: { label: '工具调用', icon: '🟣' },
442
+ tool_result: { label: '工具结果', icon: '🟠' },
443
+ msg_send: { label: '发送消息', icon: '📤' },
444
+ system: { label: '系统', icon: '⚪' },
445
+ };
446
+
447
+ function renderSessHeader(h) {
448
+ if (!h || !h.sessionId) return '';
449
+ const title = h.title || h.sessionId.slice(0, 8);
450
+ const tok = (h.inputTokens || h.outputTokens)
451
+ ? `<span class="sh-stat">🔢 in ${fmtNum(h.inputTokens)} / out ${fmtNum(h.outputTokens)}</span>` : '';
452
+ const ctx = h.contextTokens
453
+ ? `<span class="sh-stat" title="最后一轮喂给模型的完整上下文大小">📐 ${fmtNum(h.contextTokens)} ctx</span>` : '';
454
+ const cost = h.costUsd != null && h.costUsd > 0
455
+ ? `<span class="sh-stat" title="累计费用(按模型定价估算)">💰 $${h.costUsd < 0.01 ? h.costUsd.toFixed(4) : h.costUsd.toFixed(2)}</span>` : '';
456
+ let bind = '';
457
+ if (h.bound) {
458
+ const dot = h.online ? '<span class="dot on"></span>在线' : '<span class="dot idle"></span>离线';
459
+ bind = `<span class="sh-stat">🔗 ${esc(h.boundChannel || '')} · ${esc(shortAid(h.boundPeer || ''))} ${dot}</span>`;
460
+ }
461
+ return '<div class="sess-header">' +
462
+ `<div class="sh-title">${esc(title)}</div>` +
463
+ '<div class="sh-stats">' +
464
+ `<span class="sh-stat" title="用户输入 ${h.userMsgs || 0} 条 / 共 ${h.totalMsgs || 0} 条消息">💬 ${h.userMsgs || 0}/${h.totalMsgs || 0} 条</span>` +
465
+ (h.model ? `<span class="sh-stat">🤖 ${esc(h.model)}</span>` : '') +
466
+ tok + ctx + cost +
467
+ (h.gitBranch ? `<span class="sh-stat">🌿 ${esc(h.gitBranch)}</span>` : '') +
468
+ (h.version ? `<span class="sh-stat">cc ${esc(h.version)}</span>` : '') +
469
+ bind +
470
+ '</div>' +
471
+ renderCatBar(h.counts) +
472
+ `<div class="sh-path" title="${esc(h.cwd || '')}">${esc(h.cwd || '')}</div>` +
473
+ '</div>';
474
+ }
475
+
476
+ function renderCatBar(counts) {
477
+ if (!counts) return '';
478
+ const items = [
479
+ ['user_input', counts.userInput],
480
+ ['model_output', counts.modelOutput],
481
+ ['tool_call', counts.toolCall],
482
+ ['tool_result', counts.toolResult],
483
+ ['msg_send', counts.msgSend],
484
+ ];
485
+ let s = '<div class="sh-cats">';
486
+ for (const [cat, n] of items) {
487
+ const m = CAT_META[cat];
488
+ s += `<span class="cat-chip cat-${cat}"><span class="cat-swatch"></span>${m.label} ${n || 0}</span>`;
489
+ }
490
+ return s + '</div>';
491
+ }
492
+
493
+ function fmtNum(n) {
494
+ if (!n) return '0';
495
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
496
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
497
+ return String(n);
498
+ }
499
+
500
+ const TOOL_ICONS = {
501
+ Read: '📄', Write: '✏️', Edit: '✏️', MultiEdit: '✏️', NotebookEdit: '✏️',
502
+ Bash: '⌘', Glob: '🔍', Grep: '🔍', Task: '🤖', WebFetch: '🌐', WebSearch: '🌐',
503
+ };
504
+
505
+ function renderBlocks(blocks) {
506
+ let out = '';
507
+ for (const b of blocks) {
508
+ if (b.kind === 'text') {
509
+ out += `<div class="blk blk-text">${esc(b.text)}</div>`;
510
+ } else if (b.kind === 'thinking') {
511
+ out += `<details class="blk blk-thinking"><summary>💭 思考</summary><div class="blk-thinking-body">${esc(b.text)}</div></details>`;
512
+ } else if (b.kind === 'tool_use') {
513
+ const icon = TOOL_ICONS[b.tool] || '🔧';
514
+ let params = '';
515
+ for (const p of (b.params || [])) {
516
+ params += `<div class="tool-param"><span class="pk">${esc(p.k)}</span><code class="pv">${esc(p.v)}</code></div>`;
517
+ }
518
+ out += `<div class="blk blk-tool"><div class="tool-head">${icon} <span class="tool-name">${esc(b.tool)}</span></div>${params}</div>`;
519
+ } else if (b.kind === 'tool_result') {
520
+ const cls = b.isError ? 'blk-result err' : 'blk-result';
521
+ out += `<details class="blk ${cls}"><summary>${b.isError ? '✗ 结果' : '↳ 结果'}</summary><pre class="result-body">${esc(b.text)}</pre></details>`;
522
+ }
523
+ }
524
+ return out;
525
+ }
526
+
527
+ // ── 启动 ──
528
+ function startApp() {
529
+ initTabs();
530
+ connect();
531
+ }
532
+
533
+ window.addEventListener('DOMContentLoaded', () => {
534
+ initPairUI();
535
+ if (localStorage.getItem(TOKEN_KEY)) {
536
+ showApp();
537
+ startApp();
538
+ } else {
539
+ showPairPage();
540
+ }
541
+ });
542
+
543
+
544
+
545
+
546
+
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>EvolClaw Watch</title>
7
+ <link rel="icon" href="data:,">
8
+ <link rel="stylesheet" href="/style.css">
9
+ </head>
10
+ <body>
11
+ <!-- 配对页 -->
12
+ <div id="pair-page" class="pair-page">
13
+ <div class="pair-box">
14
+ <h1>🔭 EvolClaw Watch</h1>
15
+ <p class="pair-hint">输入终端显示的 6 位配对码</p>
16
+ <input id="pair-input" type="text" inputmode="numeric" maxlength="6" placeholder="000000" autocomplete="off">
17
+ <button id="pair-btn">配对</button>
18
+ <div id="pair-error" class="pair-error"></div>
19
+ </div>
20
+ </div>
21
+
22
+ <!-- 主面板 -->
23
+ <div id="app" class="app" style="display:none">
24
+ <header class="topbar">
25
+ <span class="brand">🔭 EvolClaw Watch</span>
26
+ <nav class="tabs">
27
+ <button class="tab active" data-view="aid">AID</button>
28
+ <button class="tab" data-view="msg">Messages</button>
29
+ <button class="tab" data-view="session">Sessions</button>
30
+ </nav>
31
+ <span id="conn-status" class="conn-status">连接中…</span>
32
+ </header>
33
+
34
+ <main class="content">
35
+ <section id="view-aid" class="view active"></section>
36
+ <section id="view-msg" class="view">
37
+ <div class="msg-layout">
38
+ <div id="msg-aids" class="msg-col msg-aids"></div>
39
+ <div id="msg-peers" class="msg-col msg-peers"></div>
40
+ <div id="msg-stream" class="msg-col msg-stream"></div>
41
+ </div>
42
+ </section>
43
+ <section id="view-session" class="view">
44
+ <div class="sess-layout">
45
+ <div id="sess-list" class="sess-col sess-list"></div>
46
+ <div id="sess-detail" class="sess-col sess-detail"></div>
47
+ </div>
48
+ </section>
49
+ </main>
50
+ </div>
51
+
52
+ <script src="/app.js"></script>
53
+ </body>
54
+ </html>