wezard 2.0.2 → 2.0.4

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/web/chat.js CHANGED
@@ -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;
@@ -265,29 +277,21 @@
265
277
  };
266
278
 
267
279
  var avatarOf = function (c, extra) {
268
- if (c.kind !== 'group') return '<span class="av">' + esc(c.label) + (extra || '') + '</span>';
280
+ if (c.kind !== 'group') return goSpan('av', c.peer, esc(c.label) + (extra || ''));
269
281
  var ms = membersOf(c).slice(0, 4);
270
282
  return '<span class="av mosaic n' + ms.length + '" aria-hidden="true">' +
271
283
  ms.map(function (m) { return '<i>' + esc(m.label || roleLabel(m.role)) + '</i>'; }).join('') + '</span>';
272
284
  };
273
285
 
274
- // 选中的那一项若是个 role (私聊对端 / 群里的某人), 行内给一个换视角的开关。
275
- // 不能是 <button>: 它嵌在整行那颗按钮里。
276
- 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>';
277
- var swapOf = function (role) {
278
- return canSwitch(role) && role !== ROLE
279
- ? '<span class="sw" role="button" tabindex="0" data-sw="' + esc(role) + '" title="' + esc('切到 ' + nameOf(role) + ' 的视角') + '">' + SWAP + '</span>'
280
- : '';
281
- };
282
- var line = function (title, sw, ts, pv) {
283
- 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>' +
284
288
  '<span class="ts">' + esc(fmtAgo(ts)) + '</span></span>' +
285
289
  '<span class="pv">' + esc(pv) + '</span></span>';
286
290
  };
287
291
 
288
292
  var convItem = function (c) {
289
293
  var on = c.key === CONV;
290
- var title = c.kind === 'wizard' ? nm(c.peer, c.name) : '<span class="nm chat">' + esc(c.name) + '</span>';
294
+ var title = c.kind === 'wizard' ? nm(c.peer, c.name, true) : '<span class="nm chat">' + esc(c.name) + '</span>';
291
295
  // 只列与我有往来的: 在群里但没和我说过话的人, 点进去也是空的。
292
296
  var talked = on ? c.subs.filter(function (s) { return s.count; }) : [];
293
297
  var subs = talked.length
@@ -295,13 +299,13 @@
295
299
  var sel = s.role === WITH;
296
300
  return '<button class="si' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '" data-with="' + esc(s.role) + '" ' +
297
301
  'title="' + esc('我与 ' + nameOf(s.role) + ' 在这里的 ' + s.count + ' 条往来') + '">' +
298
- '<span class="av">' + esc(s.label) + lampOf(s.status) + '</span>' +
299
- line(nm(s.role, s.name), sel ? swapOf(s.role) : '', s.lastTs, s.preview) + '</button>';
302
+ goSpan('av', s.role, esc(s.label) + lampOf(s.status)) +
303
+ line(nm(s.role, s.name, true), s.lastTs, s.preview) + '</button>';
300
304
  }).join('') + '</div>'
301
305
  : '';
302
306
  var sel = on && !WITH;
303
307
  return '<button class="ci' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '">' +
304
- avatarOf(c, lampOf(c.status)) + line(title, sel && c.kind === 'wizard' ? swapOf(c.peer) : '', c.lastTs, c.preview) +
308
+ avatarOf(c, lampOf(c.status)) + line(title, c.lastTs, c.preview) +
305
309
  '</button>' + subs;
306
310
  };
307
311
 
@@ -319,11 +323,7 @@
319
323
  convsEl.querySelectorAll('[data-conv]').forEach(function (b) {
320
324
  b.onclick = function () { selectConv(b.getAttribute('data-conv'), b.getAttribute('data-with') || ''); };
321
325
  });
322
- convsEl.querySelectorAll('[data-sw]').forEach(function (x) {
323
- var go = function (e) { e.stopPropagation(); switchRole(x.getAttribute('data-sw')); };
324
- x.onclick = go;
325
- x.onkeydown = function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(e); } };
326
- });
326
+ bindGo(convsEl);
327
327
  };
328
328
 
329
329
  var shortCwd = function (p) {
@@ -378,7 +378,7 @@
378
378
  if (SESS_OPEN && e.key === 'Escape') setSessOpen(false);
379
379
  });
380
380
 
381
- // 名片: 身份 · 出身 · cwd · 出生 · 关系 / 日程入口。
381
+ // 名片: 身份 · 出身 · cwd · 出生; 名片下面一排是关系 / 日程入口。
382
382
  var renderRole = function () {
383
383
  var r = R.role;
384
384
  if (!r) return;
@@ -413,7 +413,7 @@
413
413
  $('#rb-foot').hidden = R.sessions.length < 2;
414
414
  bindSessPicker();
415
415
  var acts = [];
416
- // 入口只在有东西可看时出现, 且只是一枚徽标 —— 名片的主角是身份, 不是按钮。
416
+ // 入口只在有东西可看时出现 —— 挂在名片下、会话列表上, 不挤进名片: 名片的主角是身份。
417
417
  if (R.relations) acts.push('<button class="bd' + (VIEW === 'world' ? ' on' : '') + '" data-view="world">关系图</button>');
418
418
  if (R.schedules) acts.push('<button class="bd' + (VIEW === 'plan' ? ' on' : '') + '" data-view="plan">日程<b>' + R.schedules + '</b></button>');
419
419
  $('#rb-acts').innerHTML = acts.join('');
@@ -460,12 +460,13 @@
460
460
  }).join('') + '</span>' +
461
461
  '</span>'
462
462
  : '';
463
- return '<span class="u-lb">用量</span>' +
463
+ // 条首的标签就是模型名: 这本账是拿什么跑出来的。没有模型记录 (老轮次) 才退回「用量」。
464
+ return '<span class="u-lb" title="' + esc(t.model || '') + '">' +
465
+ esc(t.model ? t.model.replace(/^claude-/, '') : '用量') + '</span>' +
464
466
  '<span class="u-kvs">' +
465
467
  kv('turns', t.turns, t.turns) + kv('tools', u.tools, u.tools) + kv('api', u.calls, u.calls) +
466
468
  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
+ '</span>' + io;
469
470
  };
470
471
  // 没变就不碰 DOM: 心跳每 3s 来一次, 重建会把悬停中的 tooltip 蹭掉。
471
472
  var putUsage = function (el, t) {
@@ -484,6 +485,14 @@
484
485
  putUsage($('#ch-usage'), !wiz && WITH && VIEW === 'msgs' ? R.winStats : null);
485
486
  };
486
487
 
488
+ // 只有真群 (`chat:<roomid>`) 才有跳转: `user:` 是人与 bot 的单聊, userid 指向的是人自己而不是这段会话。
489
+ var JUMP = '<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M9.5 2.5h4v4M13.5 2.5 7.5 8.5M11.5 9.5v3a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
490
+ var openInWecom = function (base) {
491
+ return base.indexOf('chat:') === 0
492
+ ? '<a class="jump" href="wxwork://openconversation?roomid=' + encodeURIComponent(base.slice(5)) + '" title="在企业微信中打开这个群聊">' + JUMP + '</a>'
493
+ : '';
494
+ };
495
+
487
496
  // ── 右栏头: 这个群聊 / 私聊是什么 (关系 / 日程视图时是视图名) ──
488
497
  var renderHead = function () {
489
498
  var who = $('#ch-who'), acts = $('#ch-acts');
@@ -497,18 +506,19 @@
497
506
  var c = convOf(CONV);
498
507
  if (!c) { who.innerHTML = ''; acts.innerHTML = ''; return; }
499
508
  if (c.kind === 'wizard') {
500
- who.innerHTML = avatarOf(c) + '<span class="l"><span class="t">' + nm(c.peer, c.name) + '</span>' +
509
+ who.innerHTML = avatarOf(c) + '<span class="l"><span class="t">' + nm(c.peer, c.name, true) + '</span>' +
501
510
  '<span class="sub">私聊 · ' + c.count + ' 条</span></span>';
502
511
  } else {
503
512
  var ms = membersOf(c);
504
- who.innerHTML = avatarOf(c) + '<span class="l"><span class="t">' + esc(c.name) + '</span>' +
513
+ who.innerHTML = avatarOf(c) + '<span class="l"><span class="tr"><span class="t">' + esc(c.name) + '</span>' + openInWecom(c.base) + '</span>' +
505
514
  '<span class="sub">群聊 · ' + ms.length + ' 位成员 · ' +
506
515
  ms.slice(0, 8).map(function (m) { return esc(m.label || roleLabel(m.role)); }).join('') +
507
516
  (ms.length > 8 ? '…' : '') + '</span></span>';
508
517
  }
509
518
  acts.innerHTML = WITH
510
- ? '<span class="with">只看我与 ' + nm(WITH) + '</span><button class="vb" id="ch-all">看全部</button>'
519
+ ? '<span class="with">只看我与 ' + nm(WITH, '', true) + '</span><button class="vb" id="ch-all">看全部</button>'
511
520
  : '';
521
+ bindGo(who); bindGo(acts);
512
522
  var x = $('#ch-all');
513
523
  if (x) x.onclick = function () { selectConv(CONV, ''); };
514
524
  };
@@ -529,11 +539,11 @@
529
539
  // 看 key 而不是会话列表: 换视角就地重包时, 列表还是上一个 role 的。
530
540
  var group = CONV.indexOf('p:') !== 0;
531
541
  // 群里我不是收信方的那条 (X → Y), 头上写清是说给谁的。
532
- var to = !mine && m.to !== ROLE && group ? '<span class="to">→ ' + nm(m.to, m.toName) + '</span>' : '';
542
+ var to = !mine && m.to !== ROLE && group ? '<span class="to">→ ' + nm(m.to, m.toName, true) + '</span>' : '';
533
543
  var priv = !m.channel && group ? '<span class="ch priv">私聊</span>' : '';
534
544
  var who = mine
535
- ? '<span class="to">' + (m.to && m.to !== 'human:' ? '→ ' + nm(m.to, m.toName) : '') + '</span>' + stamp(m.ts)
536
- : nm(m.from, m.fromName) + to + priv + stamp(m.ts);
545
+ ? '<span class="to">' + (m.to && m.to !== 'human:' ? '→ ' + nm(m.to, m.toName, true) : '') + '</span>' + stamp(m.ts)
546
+ : nm(m.from, m.fromName, true) + to + priv + stamp(m.ts);
537
547
  var sw = canSwitch(other);
538
548
  var flip = '<button class="flip" data-r="' + esc(other) + '"' + (sw ? '' : ' disabled tabindex="-1"') +
539
549
  ' aria-label="' + esc(sw ? '切到 ' + nameOf(other) + ' 的视角' : '') + '">' +
@@ -590,9 +600,7 @@
590
600
  };
591
601
 
592
602
  var bindRow = function (el) {
593
- el.querySelectorAll('.flip[data-r]:not([disabled]), .av.go').forEach(function (b) {
594
- b.onclick = function () { switchRole(b.getAttribute('data-r')); };
595
- });
603
+ bindGo(el, '.flip[data-r]:not([disabled]), .mwho .go[data-r]');
596
604
  };
597
605
  var rowNode = function (id) {
598
606
  var list = inner.querySelectorAll('.mrow');