wezard 2.0.1 → 2.0.3
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/README.md +14 -25
- package/dist/daemon/detail.js +1 -2
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/inbound.js +35 -12
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +33 -26
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +69 -37
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/wizard.js +24 -1
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +41 -15
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +11 -8
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/config.js +6 -6
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +35 -16
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js +13 -2
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/role-view.js +27 -7
- package/dist/shared/role-view.js.map +1 -1
- package/package.json +1 -1
- package/web/chat.css +83 -44
- package/web/chat.js +141 -90
package/web/chat.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
// at / recvAt: 服务端快照时刻与本地收到时刻。所有"现在几点"的判断都换算到
|
|
24
24
|
// 服务端时钟, 否则客户端时钟偏几分钟就会把运行中的会话判成已结束。
|
|
25
|
-
var R = { at: 0, recvAt: 0, role: null, sessions: [], convs: [], relations: false, schedules: 0, stats: null };
|
|
25
|
+
var R = { at: 0, recvAt: 0, role: null, sessions: [], convs: [], relations: false, schedules: 0, stats: null, winStats: null };
|
|
26
26
|
// frags: 当前窗口的原始片段 (id → 片段)。片段不带方向, 换视角时拿它就地重包左右。
|
|
27
27
|
var S = { es: null, pinned: true, gen: 0, frags: {} };
|
|
28
28
|
// 关系/日程两栏共用的世界快照。sel = 当前聚焦的节点, kinds = 边类型开关。
|
|
@@ -128,11 +128,23 @@
|
|
|
128
128
|
return k.label || (n && n.label) || ({ human: '👤', task: '⏰', system: '🧙' })[kindOf(id)] || '🧙';
|
|
129
129
|
};
|
|
130
130
|
// `.name` 的点由 CSS 画 —— 人与定时任务没有地址, 不画点。
|
|
131
|
-
var nm = function (id, name) {
|
|
132
|
-
return '<span class="nm ' + kindOf(id) + '">' + esc(name || roleName(id)) + '</span>';
|
|
133
|
-
};
|
|
134
131
|
var nameOf = function (id) { return (kindOf(id) === 'wizard' ? '.' : '') + roleName(id); };
|
|
135
132
|
var canSwitch = function (id) { return !!id && kindOf(id) !== 'task' && kindOf(id) !== 'system' && id !== 'human:'; };
|
|
133
|
+
// 头像与名字就是进入那个 role 视角的入口 —— 侧栏项、会话头、消息行同一种读法。
|
|
134
|
+
// 是 <span> 不是 <button>: 侧栏里它嵌在整行那颗按钮里。
|
|
135
|
+
var goSpan = function (cls, id, inner) {
|
|
136
|
+
var on = canSwitch(id) && id !== ROLE;
|
|
137
|
+
return '<span class="' + cls + (on ? ' go" data-r="' + esc(id) + '" title="' + esc('切到 ' + nameOf(id) + ' 的视角') : '') + '">' + inner + '</span>';
|
|
138
|
+
};
|
|
139
|
+
var nm = function (id, name, go) {
|
|
140
|
+
var cls = 'nm ' + kindOf(id), txt = esc(name || roleName(id));
|
|
141
|
+
return go ? goSpan(cls, id, txt) : '<span class="' + cls + '">' + txt + '</span>';
|
|
142
|
+
};
|
|
143
|
+
var bindGo = function (root, sel) {
|
|
144
|
+
root.querySelectorAll(sel || '.go[data-r]').forEach(function (b) {
|
|
145
|
+
b.onclick = function (e) { e.stopPropagation(); switchRole(b.getAttribute('data-r')); };
|
|
146
|
+
});
|
|
147
|
+
};
|
|
136
148
|
|
|
137
149
|
// ── markdown 渲染 (只对未渲染过的节点做, 复用节点不重绘) ──
|
|
138
150
|
var md = null;
|
|
@@ -207,10 +219,35 @@
|
|
|
207
219
|
var d = document.createElement('div'); d.innerHTML = html; return d;
|
|
208
220
|
};
|
|
209
221
|
|
|
222
|
+
// ── 工具调用正文: 第二级数据, 展开那一刻才取 ──
|
|
223
|
+
// 片段里的 .tool-body 是个空壳 (data-lazy-turn / data-lazy-use), 首屏只带摘要行。
|
|
224
|
+
// toggle 不冒泡, 所以在线程根上用捕获听 —— 用户点开, 和 reconcile 换了新节点后
|
|
225
|
+
// restoreOpen 回填展开态, 走的是同一个事件: 后者正好把「展开着等结果」的那次调用
|
|
226
|
+
// 重取成带结果的正文。取到之后正文就留在节点上, 收起再展开不会重取。
|
|
227
|
+
var loadTool = function (d) {
|
|
228
|
+
var body = childBy(d, 'tool-body');
|
|
229
|
+
if (!body || !body.hasAttribute('data-lazy-turn') || body._busy || body.firstChild) return;
|
|
230
|
+
body._busy = true; body.removeAttribute('data-err');
|
|
231
|
+
api('api/tool', { turn: body.getAttribute('data-lazy-turn'), use: body.getAttribute('data-lazy-use') })
|
|
232
|
+
.then(function (r) {
|
|
233
|
+
if (!r.ok) throw new Error(r.error);
|
|
234
|
+
var stick = S.pinned;
|
|
235
|
+
body.innerHTML = r.html;
|
|
236
|
+
if (stick) toBottom(true);
|
|
237
|
+
})
|
|
238
|
+
// 失败留着空壳 (CSS 给出提示), 再展开一次就是重试。
|
|
239
|
+
.catch(function () { body.setAttribute('data-err', '1'); })
|
|
240
|
+
.then(function () { body._busy = false; });
|
|
241
|
+
};
|
|
242
|
+
|
|
210
243
|
// ── 滚动: 默认到底; 用户手动上翻后解除吸附, 回到底部再吸附 ──
|
|
211
244
|
var atBottom = function () { return thread.scrollHeight - thread.scrollTop - thread.clientHeight < 80; };
|
|
212
245
|
var toBottom = function (force) { if (force || S.pinned) thread.scrollTop = thread.scrollHeight; };
|
|
213
246
|
thread.addEventListener('scroll', function () { S.pinned = atBottom(); });
|
|
247
|
+
inner.addEventListener('toggle', function (e) {
|
|
248
|
+
var d = e.target;
|
|
249
|
+
if (d.open && d.classList && d.classList.contains('tool-call')) loadTool(d);
|
|
250
|
+
}, true);
|
|
214
251
|
|
|
215
252
|
// ── 左栏: 当前 role 的名片 + 会话列表 ──
|
|
216
253
|
var convOf = function (key) { return R.convs.filter(function (c) { return c.key === key; })[0]; };
|
|
@@ -223,63 +260,70 @@
|
|
|
223
260
|
});
|
|
224
261
|
return out;
|
|
225
262
|
};
|
|
226
|
-
|
|
227
|
-
|
|
263
|
+
// ── 状态: wizard 是否在执行中 ──
|
|
264
|
+
// 服务端只给事实 (busy / alive / runningUntil), 亮不亮在这里按本地时钟判 ——
|
|
265
|
+
// 到点没有新写入就自己熄 (同 expireRows), 不必等下一次推送。
|
|
266
|
+
var ST = { run: '执行中', idle: '空闲', off: '已关闭' };
|
|
267
|
+
var stateOf = function (s) { return wRunning(s) ? 'run' : s.alive ? 'idle' : 'off'; };
|
|
268
|
+
// 侧栏只在执行中时点一盏灯: 列表里安静是常态, 亮着的才值得看一眼。
|
|
269
|
+
var lampOf = function (s) {
|
|
270
|
+
return s && wRunning(s) ? '<i class="live" title="' + ST.run + '"></i>' : '';
|
|
271
|
+
};
|
|
272
|
+
var paintStatus = function () {
|
|
273
|
+
var el = $('#rb-st'), r = R.role;
|
|
274
|
+
if (!el || !r) return;
|
|
275
|
+
var k = stateOf(r);
|
|
276
|
+
el.className = 'st ' + k; el.textContent = ST[k];
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
var avatarOf = function (c, extra) {
|
|
280
|
+
if (c.kind !== 'group') return goSpan('av', c.peer, esc(c.label) + (extra || ''));
|
|
228
281
|
var ms = membersOf(c).slice(0, 4);
|
|
229
282
|
return '<span class="av mosaic n' + ms.length + '" aria-hidden="true">' +
|
|
230
283
|
ms.map(function (m) { return '<i>' + esc(m.label || roleLabel(m.role)) + '</i>'; }).join('') + '</span>';
|
|
231
284
|
};
|
|
232
285
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
var SWAP = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M3 5.5h9M9.5 3l2.5 2.5L9.5 8M13 10.5H4M6.5 8 4 10.5 6.5 13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
236
|
-
var swapOf = function (role) {
|
|
237
|
-
return canSwitch(role) && role !== ROLE
|
|
238
|
-
? '<span class="sw" role="button" tabindex="0" data-sw="' + esc(role) + '" title="' + esc('切到 ' + nameOf(role) + ' 的视角') + '">' + SWAP + '</span>'
|
|
239
|
-
: '';
|
|
240
|
-
};
|
|
241
|
-
var line = function (title, sw, ts, pv) {
|
|
242
|
-
return '<span class="b"><span class="l1"><span class="t">' + title + '</span>' + sw +
|
|
286
|
+
var line = function (title, ts, pv) {
|
|
287
|
+
return '<span class="b"><span class="l1"><span class="t">' + title + '</span>' +
|
|
243
288
|
'<span class="ts">' + esc(fmtAgo(ts)) + '</span></span>' +
|
|
244
289
|
'<span class="pv">' + esc(pv) + '</span></span>';
|
|
245
290
|
};
|
|
246
291
|
|
|
247
292
|
var convItem = function (c) {
|
|
248
293
|
var on = c.key === CONV;
|
|
249
|
-
var title = c.kind === 'wizard' ? nm(c.peer, c.name) : '<span class="nm chat">' + esc(c.name) + '</span>';
|
|
250
|
-
|
|
251
|
-
|
|
294
|
+
var title = c.kind === 'wizard' ? nm(c.peer, c.name, true) : '<span class="nm chat">' + esc(c.name) + '</span>';
|
|
295
|
+
// 只列与我有往来的: 在群里但没和我说过话的人, 点进去也是空的。
|
|
296
|
+
var talked = on ? c.subs.filter(function (s) { return s.count; }) : [];
|
|
297
|
+
var subs = talked.length
|
|
298
|
+
? '<div class="subs">' + talked.map(function (s) {
|
|
252
299
|
var sel = s.role === WITH;
|
|
253
|
-
return '<button class="si' + (sel ? ' on' : '') +
|
|
254
|
-
'title="' + esc(
|
|
255
|
-
'
|
|
256
|
-
line(nm(s.role, s.name
|
|
300
|
+
return '<button class="si' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '" data-with="' + esc(s.role) + '" ' +
|
|
301
|
+
'title="' + esc('我与 ' + nameOf(s.role) + ' 在这里的 ' + s.count + ' 条往来') + '">' +
|
|
302
|
+
goSpan('av', s.role, esc(s.label) + lampOf(s.status)) +
|
|
303
|
+
line(nm(s.role, s.name, true), s.lastTs, s.preview) + '</button>';
|
|
257
304
|
}).join('') + '</div>'
|
|
258
305
|
: '';
|
|
259
306
|
var sel = on && !WITH;
|
|
260
307
|
return '<button class="ci' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '">' +
|
|
261
|
-
avatarOf(c
|
|
308
|
+
avatarOf(c, lampOf(c.status)) + line(title, c.lastTs, c.preview) +
|
|
262
309
|
'</button>' + subs;
|
|
263
310
|
};
|
|
264
311
|
|
|
265
312
|
var renderConvs = function () {
|
|
266
313
|
var groups = R.convs.filter(function (c) { return c.kind === 'group'; });
|
|
267
314
|
var dms = R.convs.filter(function (c) { return c.kind !== 'group'; });
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
315
|
+
// 空着的那一栏不画 —— 对谁都一样: 人没有私聊只是这条规则的一个特例。
|
|
316
|
+
var sec = function (title, list) {
|
|
317
|
+
return list.length ? '<h2>' + title + '<span>' + list.length + '</span></h2>' + list.map(convItem).join('') : '';
|
|
271
318
|
};
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
319
|
+
// 没变就不碰 DOM: 心跳每 3s 来一次, 重建会把列表的滚动与焦点蹭掉。
|
|
320
|
+
var html = sec('群聊', groups) + sec('私聊', dms);
|
|
321
|
+
if (convsEl._html === html) return;
|
|
322
|
+
convsEl._html = html; convsEl.innerHTML = html;
|
|
275
323
|
convsEl.querySelectorAll('[data-conv]').forEach(function (b) {
|
|
276
324
|
b.onclick = function () { selectConv(b.getAttribute('data-conv'), b.getAttribute('data-with') || ''); };
|
|
277
325
|
});
|
|
278
|
-
convsEl
|
|
279
|
-
var go = function (e) { e.stopPropagation(); switchRole(x.getAttribute('data-sw')); };
|
|
280
|
-
x.onclick = go;
|
|
281
|
-
x.onkeydown = function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(e); } };
|
|
282
|
-
});
|
|
326
|
+
bindGo(convsEl);
|
|
283
327
|
};
|
|
284
328
|
|
|
285
329
|
var shortCwd = function (p) {
|
|
@@ -334,7 +378,7 @@
|
|
|
334
378
|
if (SESS_OPEN && e.key === 'Escape') setSessOpen(false);
|
|
335
379
|
});
|
|
336
380
|
|
|
337
|
-
// 名片: 身份 · 出身 · cwd ·
|
|
381
|
+
// 名片: 身份 · 出身 · cwd · 出生; 名片下面一排是关系 / 日程入口。
|
|
338
382
|
var renderRole = function () {
|
|
339
383
|
var r = R.role;
|
|
340
384
|
if (!r) return;
|
|
@@ -358,17 +402,18 @@
|
|
|
358
402
|
$('#rb-who').innerHTML =
|
|
359
403
|
'<div class="id"><span class="av">' + esc(r.label) + '</span>' +
|
|
360
404
|
'<span class="l">' + nm(r.id, r.name) +
|
|
361
|
-
'<span class="k">' + kind + '</span></span></div>' +
|
|
405
|
+
'<span class="k">' + kind + (r.kind === 'wizard' ? '<span class="st" id="rb-st"></span>' : '') + '</span></span></div>' +
|
|
362
406
|
(r.description ? '<p class="job">' + esc(r.description) + '</p>' : '') +
|
|
363
407
|
'<div class="facts">' + facts.join('') + '</div>';
|
|
408
|
+
paintStatus();
|
|
364
409
|
$('#rb-who').querySelectorAll('.go').forEach(function (g) {
|
|
365
410
|
g.onclick = function () { switchRole(g.getAttribute('data-r')); };
|
|
366
411
|
});
|
|
367
412
|
$('#rb-sess').innerHTML = R.sessions.length > 1 ? sessPicker() : '';
|
|
413
|
+
$('#rb-foot').hidden = R.sessions.length < 2;
|
|
368
414
|
bindSessPicker();
|
|
369
|
-
syncFoot();
|
|
370
415
|
var acts = [];
|
|
371
|
-
//
|
|
416
|
+
// 入口只在有东西可看时出现 —— 挂在名片下、会话列表上, 不挤进名片: 名片的主角是身份。
|
|
372
417
|
if (R.relations) acts.push('<button class="bd' + (VIEW === 'world' ? ' on' : '') + '" data-view="world">关系图</button>');
|
|
373
418
|
if (R.schedules) acts.push('<button class="bd' + (VIEW === 'plan' ? ' on' : '') + '" data-view="plan">日程<b>' + R.schedules + '</b></button>');
|
|
374
419
|
$('#rb-acts').innerHTML = acts.join('');
|
|
@@ -379,60 +424,64 @@
|
|
|
379
424
|
document.title = nameOf(r.id) + ' 的视角';
|
|
380
425
|
};
|
|
381
426
|
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
var COLORS = { input: '#0a7d6b', cacheRead: '#8250df', cacheWrite: '#953800', output: '#1a7f37' };
|
|
427
|
+
// ── 用量条: 一组轮次的总账 ──
|
|
428
|
+
// usageHTML 只管「一本账长什么样」(TagSummary → 一条), 账是谁的、挂在哪由 renderUsage 定。
|
|
429
|
+
var SEGS = [
|
|
430
|
+
['input', '输入', '#0a7d6b'], ['cacheRead', '缓存读', '#8250df'],
|
|
431
|
+
['cacheWrite', '缓存写', '#953800'], ['output', '输出', '#1a7f37'],
|
|
432
|
+
];
|
|
389
433
|
var TIP = {
|
|
390
434
|
turns: '对话轮数', tools: '工具调用次数', api: 'API 请求次数',
|
|
391
|
-
ctx: '上下文峰值 — 单次请求送入的 input + 缓存 的最高值',
|
|
392
|
-
out: '累计输出 token', cache: '累计缓存 token (读 + 写)', time: '累计耗时',
|
|
435
|
+
ctx: '上下文峰值 — 单次请求送入的 input + 缓存 的最高值', time: '累计耗时',
|
|
393
436
|
};
|
|
394
437
|
var liveDur = function (t) {
|
|
395
438
|
var d = (t.usage && t.usage.durationMs) || 0;
|
|
396
439
|
if (!t.runningUntil) return d;
|
|
397
440
|
return d + Math.max(0, Math.min(srvNow(), t.runningUntil) - R.at);
|
|
398
441
|
};
|
|
399
|
-
var
|
|
400
|
-
var el = $('#rb-prof');
|
|
401
|
-
var t = R.stats;
|
|
402
|
-
if (!t) { el.innerHTML = ''; syncFoot(); return; }
|
|
442
|
+
var usageHTML = function (t) {
|
|
403
443
|
var u = t.usage || {};
|
|
404
|
-
var segs =
|
|
405
|
-
.filter(function (s) { return u[s[0]] > 0; });
|
|
444
|
+
var segs = SEGS.filter(function (s) { return u[s[0]] > 0; });
|
|
406
445
|
var total = segs.reduce(function (a, s) { return a + u[s[0]]; }, 0);
|
|
446
|
+
// 值为 0 = 该指标没有数据 (老记录 / 网关不报 usage), 不占位置。
|
|
447
|
+
var kv = function (k, n, text) {
|
|
448
|
+
return n ? '<span class="kv" title="' + esc(TIP[k] || k) + '"><b>' + esc(text) + '</b>' + k + '</span>' : '';
|
|
449
|
+
};
|
|
450
|
+
// 输出 / 缓存不再单列成指标 —— 它们就是 I/O 图例里的那几格。
|
|
407
451
|
var io = total > 0
|
|
408
|
-
? '<
|
|
452
|
+
? '<span class="u-io" title="累计 token I/O · 共 ' + fmtTok(total) + '">' +
|
|
409
453
|
'<span class="bar">' + segs.map(function (s) {
|
|
410
454
|
return '<span class="seg" style="width:' + (u[s[0]] / total * 100).toFixed(2) + '%;background:' +
|
|
411
|
-
|
|
455
|
+
s[2] + '" title="' + s[1] + ': ' + fmtTok(u[s[0]]) + '"></span>';
|
|
412
456
|
}).join('') + '</span>' +
|
|
413
457
|
'<span class="leg">' + segs.map(function (s) {
|
|
414
|
-
return '<span class="lg"><i style="background:' +
|
|
458
|
+
return '<span class="lg"><i style="background:' + s[2] + '"></i>' + s[1] +
|
|
415
459
|
'<b>' + fmtTok(u[s[0]]) + '</b></span>';
|
|
416
460
|
}).join('') + '</span>' +
|
|
417
|
-
'</
|
|
461
|
+
'</span>'
|
|
418
462
|
: '';
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
'
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
463
|
+
return '<span class="u-lb">用量</span>' +
|
|
464
|
+
'<span class="u-kvs">' +
|
|
465
|
+
kv('turns', t.turns, t.turns) + kv('tools', u.tools, u.tools) + kv('api', u.calls, u.calls) +
|
|
466
|
+
kv('ctx', u.ctxPeak, fmtTok(u.ctxPeak)) + kv('time', liveDur(t), fmtDur(liveDur(t))) +
|
|
467
|
+
'</span>' + io +
|
|
468
|
+
(t.model ? '<span class="u-model" title="' + esc(t.model) + '">' + esc(t.model.replace(/^claude-/, '')) + '</span>' : '');
|
|
469
|
+
};
|
|
470
|
+
// 没变就不碰 DOM: 心跳每 3s 来一次, 重建会把悬停中的 tooltip 蹭掉。
|
|
471
|
+
var putUsage = function (el, t) {
|
|
472
|
+
var html = t ? usageHTML(t) : '';
|
|
473
|
+
// !el: web/ 是热更的, 外壳 HTML 要等 daemon 重启 —— 新脚本可能先遇上没有挂载点的旧外壳。
|
|
474
|
+
if (!el || el._html === html) return;
|
|
475
|
+
el._html = html; el.innerHTML = html; el.hidden = !html;
|
|
476
|
+
// 条的高度挤的是消息区 —— 原本贴底的继续贴底。
|
|
477
|
+
toBottom();
|
|
478
|
+
};
|
|
479
|
+
// wizard: 整页页脚, 它自己跑过的全部轮次 (选了 session 就只算那一段)。
|
|
480
|
+
// 人: 自己不跑轮次 —— 在群里点开某个 wizard 之后, 窗口底下是这段往来的账。
|
|
481
|
+
var renderUsage = function () {
|
|
482
|
+
var wiz = !!R.role && R.role.kind === 'wizard';
|
|
483
|
+
putUsage($('#pg-usage'), wiz ? R.stats : null);
|
|
484
|
+
putUsage($('#ch-usage'), !wiz && WITH && VIEW === 'msgs' ? R.winStats : null);
|
|
436
485
|
};
|
|
437
486
|
|
|
438
487
|
// ── 右栏头: 这个群聊 / 私聊是什么 (关系 / 日程视图时是视图名) ──
|
|
@@ -448,7 +497,7 @@
|
|
|
448
497
|
var c = convOf(CONV);
|
|
449
498
|
if (!c) { who.innerHTML = ''; acts.innerHTML = ''; return; }
|
|
450
499
|
if (c.kind === 'wizard') {
|
|
451
|
-
who.innerHTML = avatarOf(c) + '<span class="l"><span class="t">' + nm(c.peer, c.name) + '</span>' +
|
|
500
|
+
who.innerHTML = avatarOf(c) + '<span class="l"><span class="t">' + nm(c.peer, c.name, true) + '</span>' +
|
|
452
501
|
'<span class="sub">私聊 · ' + c.count + ' 条</span></span>';
|
|
453
502
|
} else {
|
|
454
503
|
var ms = membersOf(c);
|
|
@@ -458,8 +507,9 @@
|
|
|
458
507
|
(ms.length > 8 ? '…' : '') + '</span></span>';
|
|
459
508
|
}
|
|
460
509
|
acts.innerHTML = WITH
|
|
461
|
-
? '<span class="with">只看我与 ' + nm(WITH) + '</span><button class="vb" id="ch-all">看全部</button>'
|
|
510
|
+
? '<span class="with">只看我与 ' + nm(WITH, '', true) + '</span><button class="vb" id="ch-all">看全部</button>'
|
|
462
511
|
: '';
|
|
512
|
+
bindGo(who); bindGo(acts);
|
|
463
513
|
var x = $('#ch-all');
|
|
464
514
|
if (x) x.onclick = function () { selectConv(CONV, ''); };
|
|
465
515
|
};
|
|
@@ -480,11 +530,11 @@
|
|
|
480
530
|
// 看 key 而不是会话列表: 换视角就地重包时, 列表还是上一个 role 的。
|
|
481
531
|
var group = CONV.indexOf('p:') !== 0;
|
|
482
532
|
// 群里我不是收信方的那条 (X → Y), 头上写清是说给谁的。
|
|
483
|
-
var to = !mine && m.to !== ROLE && group ? '<span class="to">→ ' + nm(m.to, m.toName) + '</span>' : '';
|
|
533
|
+
var to = !mine && m.to !== ROLE && group ? '<span class="to">→ ' + nm(m.to, m.toName, true) + '</span>' : '';
|
|
484
534
|
var priv = !m.channel && group ? '<span class="ch priv">私聊</span>' : '';
|
|
485
535
|
var who = mine
|
|
486
|
-
? '<span class="to">' + (m.to && m.to !== 'human:' ? '→ ' + nm(m.to, m.toName) : '') + '</span>' + stamp(m.ts)
|
|
487
|
-
: nm(m.from, m.fromName) + to + priv + stamp(m.ts);
|
|
536
|
+
? '<span class="to">' + (m.to && m.to !== 'human:' ? '→ ' + nm(m.to, m.toName, true) : '') + '</span>' + stamp(m.ts)
|
|
537
|
+
: nm(m.from, m.fromName, true) + to + priv + stamp(m.ts);
|
|
488
538
|
var sw = canSwitch(other);
|
|
489
539
|
var flip = '<button class="flip" data-r="' + esc(other) + '"' + (sw ? '' : ' disabled tabindex="-1"') +
|
|
490
540
|
' aria-label="' + esc(sw ? '切到 ' + nameOf(other) + ' 的视角' : '') + '">' +
|
|
@@ -541,9 +591,7 @@
|
|
|
541
591
|
};
|
|
542
592
|
|
|
543
593
|
var bindRow = function (el) {
|
|
544
|
-
el
|
|
545
|
-
b.onclick = function () { switchRole(b.getAttribute('data-r')); };
|
|
546
|
-
});
|
|
594
|
+
bindGo(el, '.flip[data-r]:not([disabled]), .mwho .go[data-r]');
|
|
547
595
|
};
|
|
548
596
|
var rowNode = function (id) {
|
|
549
597
|
var list = inner.querySelectorAll('.mrow');
|
|
@@ -625,7 +673,8 @@
|
|
|
625
673
|
var applyRole = function (d) {
|
|
626
674
|
R.at = d.at || Date.now(); R.recvAt = Date.now();
|
|
627
675
|
R.role = d.role; R.sessions = d.sessions || []; R.convs = d.convs || [];
|
|
628
|
-
R.relations = !!d.relations; R.schedules = d.schedules || 0;
|
|
676
|
+
R.relations = !!d.relations; R.schedules = d.schedules || 0;
|
|
677
|
+
R.stats = d.stats || null; R.winStats = d.winStats || null;
|
|
629
678
|
ROLE = d.role.id;
|
|
630
679
|
learn(d.role.id, d.role.name, d.role.label);
|
|
631
680
|
R.convs.forEach(function (c) {
|
|
@@ -633,7 +682,7 @@
|
|
|
633
682
|
c.subs.forEach(function (s) { learn(s.role, s.name, s.label); });
|
|
634
683
|
});
|
|
635
684
|
if (!CONV || !convOf(CONV)) { CONV = d.conv || ''; WITH = ''; }
|
|
636
|
-
renderRole();
|
|
685
|
+
renderRole(); renderUsage(); renderConvs(); renderHead();
|
|
637
686
|
};
|
|
638
687
|
|
|
639
688
|
// ── SSE: role 摘要 + 当前窗口的消息增量。断线指数退避重连。 ──
|
|
@@ -674,7 +723,9 @@
|
|
|
674
723
|
CONV = key; WITH = withRole || '';
|
|
675
724
|
if (VIEW !== 'msgs') setView('msgs');
|
|
676
725
|
app.classList.add('reading');
|
|
677
|
-
|
|
726
|
+
// 上一个窗口的账不属于这个窗口 —— 先收起, 新的随 SSE 的首个 role 事件到。
|
|
727
|
+
R.winStats = null;
|
|
728
|
+
renderConvs(); renderHead(); renderUsage();
|
|
678
729
|
S.gen++;
|
|
679
730
|
syncUrl();
|
|
680
731
|
inner.innerHTML = '<div class="empty">加载中…</div>';
|
|
@@ -1463,7 +1514,7 @@
|
|
|
1463
1514
|
app.classList.toggle('outer', v !== 'msgs');
|
|
1464
1515
|
// 手机上关系/日程占满主区 —— 进入即阅读态。
|
|
1465
1516
|
if (v !== 'msgs') app.classList.add('reading');
|
|
1466
|
-
renderRole(); renderHead();
|
|
1517
|
+
renderRole(); renderHead(); renderUsage();
|
|
1467
1518
|
if (v === 'msgs') { fxStop(); toBottom(true); }
|
|
1468
1519
|
else {
|
|
1469
1520
|
// 关系图一打开就聚焦在当前 role 身上: 它的邻居亮着, 其余压暗。
|
|
@@ -1484,13 +1535,13 @@
|
|
|
1484
1535
|
// 本地心跳: 相对时间、运行中判定、耗时都随时间变化, 但服务端没有新事件可推。
|
|
1485
1536
|
setInterval(function () {
|
|
1486
1537
|
if (!R.role) return;
|
|
1487
|
-
|
|
1538
|
+
renderUsage();
|
|
1488
1539
|
expireRows();
|
|
1540
|
+
// 侧栏的「几分钟前」与状态灯: 内容没变时 renderConvs 不碰 DOM。
|
|
1541
|
+
paintStatus(); renderConvs();
|
|
1489
1542
|
if (VIEW === 'world') tickWorld();
|
|
1490
1543
|
else if (VIEW === 'plan') renderPlan();
|
|
1491
1544
|
}, TICK_MS);
|
|
1492
|
-
// 侧栏的「几分钟前」一分钟刷一次就够 —— 每 3s 重建会把列表的滚动与焦点蹭掉。
|
|
1493
|
-
setInterval(function () { if (R.role && VIEW === 'msgs') renderConvs(); }, 60000);
|
|
1494
1545
|
|
|
1495
1546
|
// ── boot ──
|
|
1496
1547
|
// 带着 role / conv 来的链接直接进阅读态 (手机上不先落在会话列表)。
|