wezard 2.4.0 → 2.5.0
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/CLAUDE.md +1 -1
- package/README.md +113 -57
- package/cli/wezard.sh +2 -0
- package/config.example.jsonc +7 -4
- package/dist/cli/wx.js +78 -0
- package/dist/cli/wx.js.map +1 -0
- package/dist/daemon/aibot-media.js +62 -0
- package/dist/daemon/aibot-media.js.map +1 -0
- package/dist/daemon/approval.js +30 -20
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/ask.js +3 -2
- package/dist/daemon/ask.js.map +1 -1
- package/dist/daemon/cc-session.js +27 -4
- package/dist/daemon/cc-session.js.map +1 -1
- package/dist/daemon/chat-log.js +2 -2
- package/dist/daemon/chat-log.js.map +1 -1
- package/dist/daemon/chat-name.js +6 -2
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/config-api.js +25 -10
- package/dist/daemon/config-api.js.map +1 -1
- package/dist/daemon/direction.js +53 -0
- package/dist/daemon/direction.js.map +1 -0
- package/dist/daemon/ilink.js +162 -0
- package/dist/daemon/ilink.js.map +1 -0
- package/dist/daemon/inbound.js +64 -18
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +415 -109
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +4 -3
- package/dist/daemon/jobs.js.map +1 -1
- package/dist/daemon/last-response.js +6 -3
- package/dist/daemon/last-response.js.map +1 -1
- package/dist/daemon/media.js +108 -0
- package/dist/daemon/media.js.map +1 -0
- package/dist/daemon/mirror-bridge.js +111 -93
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/model-select.js +159 -54
- package/dist/daemon/model-select.js.map +1 -1
- package/dist/daemon/notices.js +59 -12
- package/dist/daemon/notices.js.map +1 -1
- package/dist/daemon/outbound.js +2 -1
- package/dist/daemon/outbound.js.map +1 -1
- package/dist/daemon/peers.js +46 -27
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/pending-items.js +1 -1
- package/dist/daemon/pending-items.js.map +1 -1
- package/dist/daemon/reach.js +6 -0
- package/dist/daemon/reach.js.map +1 -0
- package/dist/daemon/receipts.js +149 -7
- package/dist/daemon/receipts.js.map +1 -1
- package/dist/daemon/route.js +11 -4
- package/dist/daemon/route.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +157 -60
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/steward-guard.js +48 -7
- package/dist/daemon/steward-guard.js.map +1 -1
- package/dist/daemon/top-only-nudge.js +29 -0
- package/dist/daemon/top-only-nudge.js.map +1 -0
- package/dist/daemon/weixin-cmd.js +268 -0
- package/dist/daemon/weixin-cmd.js.map +1 -0
- package/dist/daemon/weixin-port.js +226 -0
- package/dist/daemon/weixin-port.js.map +1 -0
- package/dist/daemon/weixin.js +772 -0
- package/dist/daemon/weixin.js.map +1 -0
- package/dist/daemon/wizard.js +56 -18
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +43 -23
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +61 -20
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +9 -1
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/chat-view.js +16 -2
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/cli-backends.js +3 -0
- package/dist/shared/cli-backends.js.map +1 -1
- package/dist/shared/config.js +45 -10
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/json-map-store.js +1 -0
- package/dist/shared/json-map-store.js.map +1 -1
- package/dist/shared/reminder.js +1 -0
- package/dist/shared/reminder.js.map +1 -1
- package/dist/shared/role-render.js +7 -4
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-view.js +116 -37
- package/dist/shared/role-view.js.map +1 -1
- package/dist/shared/seen-log.js +26 -0
- package/dist/shared/seen-log.js.map +1 -0
- package/dist/shared/session-label.js +84 -31
- package/dist/shared/session-label.js.map +1 -1
- package/dist/shared/turn-state.js +1 -1
- package/dist/shared/turn-state.js.map +1 -1
- package/dist/shared/world.js.map +1 -1
- package/dist/shared/wx-text.js +101 -0
- package/dist/shared/wx-text.js.map +1 -0
- package/dist/svr/index.js +5 -2
- package/dist/svr/index.js.map +1 -1
- package/hooks/pre-tool-use.sh +9 -6
- package/package.json +6 -2
- package/web/chat.css +32 -5
- package/web/chat.js +160 -92
package/web/chat.js
CHANGED
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
// 关系/日程两栏共用的世界快照。treeFor = 已把谁滚进过视野。
|
|
36
36
|
var W = {
|
|
37
37
|
at: 0, nodes: [], edges: [], chats: [], jobs: [], schedules: [],
|
|
38
|
-
degraded: false, loaded: false, treeFor: '', glance: {}, gKeys: '',
|
|
38
|
+
degraded: false, loaded: false, treeFor: '', glance: {}, gKeys: '', gRole: '',
|
|
39
39
|
};
|
|
40
40
|
var VIEW = 'msgs';
|
|
41
41
|
// 侧栏是会话列表还是关系图 —— 与右边的 VIEW 无关: 关系图下右边照样是选中的那段对话。
|
|
42
42
|
// URL 带了 side 以它为准; 没带 (新链接) 用本地记下的, 见 WORLD_KEY。
|
|
43
43
|
var WORLD = qs.get('side') === 'world';
|
|
44
44
|
var $ = function (s) { return document.querySelector(s); };
|
|
45
|
-
var app = $('#app'), thread = $('#thread'), inner = $('#thread-in'), convsEl = $('#convs');
|
|
45
|
+
var app = $('#app'), main = $('.main'), thread = $('#thread'), inner = $('#thread-in'), convsEl = $('#convs');
|
|
46
46
|
var planEl = $('#plan-in');
|
|
47
47
|
|
|
48
48
|
var srvNow = function () { return R.at ? R.at + (Date.now() - R.recvAt) : Date.now(); };
|
|
@@ -105,6 +105,12 @@
|
|
|
105
105
|
Object.keys(extra || {}).forEach(function (k) { p[k] = extra[k]; });
|
|
106
106
|
return p;
|
|
107
107
|
};
|
|
108
|
+
// 摘要另带未读基线 (定了才带): 服务端据此只给基线之后的他人未读账。不进 URL。
|
|
109
|
+
var roleParams = function (extra) {
|
|
110
|
+
var p = viewParams(extra);
|
|
111
|
+
if (BASE) p.since = BASE;
|
|
112
|
+
return p;
|
|
113
|
+
};
|
|
108
114
|
// 视角状态回写 URL (replaceState: 不在历史里留一串中间态)。
|
|
109
115
|
var syncUrl = function () {
|
|
110
116
|
var p = new URLSearchParams({ id: TOKEN });
|
|
@@ -138,11 +144,11 @@
|
|
|
138
144
|
};
|
|
139
145
|
var roleLabel = function (id) {
|
|
140
146
|
var k = names[id] || {}, n = nodeOf(id);
|
|
141
|
-
return k.label || (n && n.label) || ({ human: '👤', task: '⏰', system: '
|
|
147
|
+
return k.label || (n && n.label) || ({ human: '👤', task: '⏰', system: '🧙♂️' })[kindOf(id)] || '🧙';
|
|
142
148
|
};
|
|
143
149
|
// `.name` 的点由 CSS 画 —— 人与定时任务没有地址, 不画点。
|
|
144
150
|
var nameOf = function (id) { return (kindOf(id) === 'wizard' ? '.' : '') + roleName(id); };
|
|
145
|
-
var canSwitch = function (id) { return !!id && kindOf(id) !== 'task' && kindOf(id) !== 'system'
|
|
151
|
+
var canSwitch = function (id) { return !!id && kindOf(id) !== 'task' && kindOf(id) !== 'system'; };
|
|
146
152
|
// 头像与名字就是进入那个 role 视角的入口 —— 侧栏项、会话头、消息行同一种读法。
|
|
147
153
|
// 是 <span> 不是 <button>: 侧栏里它嵌在整行那颗按钮里。
|
|
148
154
|
var goSpan = function (cls, id, inner) {
|
|
@@ -420,6 +426,10 @@
|
|
|
420
426
|
var jobMark = function (ids) {
|
|
421
427
|
return ids && ids.length ? '<span class="jmk" data-jobs="' + esc(ids.join(' ')) + '" title="' + esc((ids.length > 1 ? ids.length + ' 张工单: ' : '工单 ') + ids.join(' ') + ' —— 点开') + '">📋' + (ids.length > 1 ? '<i>' + ids.length + '</i>' : '') + '</span>' : '';
|
|
422
428
|
};
|
|
429
|
+
// 微信通道群名字旁的状态: 判定与文字都是服务端 wxMark 给的, 这里只画。
|
|
430
|
+
var wxTag = function (w) {
|
|
431
|
+
return w ? '<span class="wxm ' + esc(w.state) + '" title="' + esc(w.tip) + '">' + esc(w.text) + '</span>' : '';
|
|
432
|
+
};
|
|
423
433
|
// 视角不在里面的工单不在 R.convs 里: 按服务端的账 (R.jobIndex, 全部工单) 现造一项; 账里没有 = 已清掉 (job 为空)。
|
|
424
434
|
var jobConv = function (key) {
|
|
425
435
|
var id = key.slice(2), j = (R.jobIndex || {})[id];
|
|
@@ -506,52 +516,59 @@
|
|
|
506
516
|
(unread ? '<b class="ub">' + (unread > 99 ? '99+' : unread) + '</b>' : '') + '</span></span>';
|
|
507
517
|
};
|
|
508
518
|
|
|
509
|
-
// ── 未读:
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
519
|
+
// ── 未读: 别人说完、还没在详情里读到的话 ──
|
|
520
|
+
// 服务端给每一行 (侧栏会话项 / 子项、关系图卡片) 一份账, 条目 = [说完的时刻, 发话方, 收信方, 消息 id], 按点开这一行
|
|
521
|
+
// 看到的消息 (与详情区取消息同一个 windowOf) 切成不相交的两份: heard = 这里看得见的未读 (右边红点) —— 页面视角的,
|
|
522
|
+
// 加上这一行主人的; theirs = 主人在这里看不见的未读 (名字旁) —— 切到它的视角才看得到。一句话在一行里最多数一次,
|
|
523
|
+
// 点开这一行读到的只从红点里减, 名字旁不因此变。
|
|
524
|
+
// 已读只有一份 (SEEN): 这句话说完的样子在详情里上过屏 (页面可见、正在看消息) 就记下它的 id ——
|
|
525
|
+
// 与页面视角、详情视角、侧栏 / 关系图无关, 哪一处读到, 所有视图里同一句话一起消失, 不必等下一份摘要。
|
|
526
|
+
// SEEN 落在 svr 上 (全局一份): 新看过的攒一小会儿批量写回, 服务端下发的账已经减掉记过的, 并带回基线 (seenBase) ——
|
|
527
|
+
// 刷新、另一个标签页 (下一次心跳) 都看得到。没落盘的服务端 (daemon 自己的页、老 svr) 或读写失败, 退回只活在内存里:
|
|
528
|
+
// 基线是「打开页面这一刻」(服务端时钟, 首次拿到快照时定), 刷新即重置。
|
|
515
529
|
var BASE = 0;
|
|
516
|
-
var
|
|
517
|
-
var
|
|
518
|
-
var
|
|
519
|
-
var
|
|
520
|
-
|
|
521
|
-
var
|
|
522
|
-
|
|
523
|
-
var
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
})
|
|
529
|
-
};
|
|
530
|
-
var
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
};
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
var
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
};
|
|
548
|
-
//
|
|
549
|
-
|
|
530
|
+
var SEEN = {};
|
|
531
|
+
var OUTBOX = {}, OUT_T = 0;
|
|
532
|
+
var SEEN_URL = function () { return 'api/seen?id=' + encodeURIComponent(TOKEN); };
|
|
533
|
+
var flushSeen = function (leaving) {
|
|
534
|
+
clearTimeout(OUT_T); OUT_T = 0;
|
|
535
|
+
var ids = Object.keys(OUTBOX);
|
|
536
|
+
if (!ids.length) return;
|
|
537
|
+
var body = JSON.stringify(OUTBOX);
|
|
538
|
+
OUTBOX = {};
|
|
539
|
+
try {
|
|
540
|
+
if (leaving && navigator.sendBeacon) navigator.sendBeacon(SEEN_URL(), body);
|
|
541
|
+
else fetch(SEEN_URL(), { method: 'POST', body: body, keepalive: true }).catch(function () { });
|
|
542
|
+
} catch (e) { }
|
|
543
|
+
};
|
|
544
|
+
var queueSeen = function (rows) {
|
|
545
|
+
Object.keys(rows).forEach(function (id) { OUTBOX[id] = rows[id]; });
|
|
546
|
+
if (!OUT_T) OUT_T = setTimeout(flushSeen, 1500);
|
|
547
|
+
};
|
|
548
|
+
// 关页 / 切走时不等那 1.5s。
|
|
549
|
+
window.addEventListener('pagehide', function () { flushSeen(true); });
|
|
550
|
+
document.addEventListener('visibilitychange', function () { if (document.hidden) flushSeen(true); });
|
|
551
|
+
var unreadIn = function (hs) {
|
|
552
|
+
return (hs || []).filter(function (h) { return h[0] > BASE && !((SEEN[h[3]] || 0) >= h[0]); }).length;
|
|
553
|
+
};
|
|
554
|
+
var unreadOf = function (c) { return unreadIn(c.heard); };
|
|
555
|
+
// 一行的两枚未读, 侧栏会话项 / 子项与关系图卡片同一个: 红点 = 点开看得见的, 名字旁 = 主人在这里看不见的。
|
|
556
|
+
var tally = function (l) { return { unread: unreadIn(l && l.heard), theirs: unreadIn(l && l.theirs) }; };
|
|
557
|
+
var peerBadge = function (id, n) {
|
|
558
|
+
return n && id !== ROLE ? '<b class="pub" title="' + esc(nameOf(id) + ' 的视角下还有 ' + n + ' 条未读, 这一项里看不到') + '">' + (n > 99 ? '99+' : n) + '</b>' : '';
|
|
559
|
+
};
|
|
560
|
+
// 详情区真在屏上: 窄屏退回列表时它整个 display:none (offsetParent 为空)。
|
|
561
|
+
var reading = function () { return VIEW === 'msgs' && !document.hidden && thread.offsetParent !== null; };
|
|
562
|
+
// 上屏的片段记成已读 (正在看消息时): 记的是它说完的时刻, 还在吐字的那一版不算读到终句。
|
|
563
|
+
// 有新记的就重画侧栏 / 关系图 —— 两边从同一个 SEEN 派生, 同一拍变。
|
|
564
|
+
var see = function (frags) {
|
|
550
565
|
if (!reading()) return;
|
|
551
|
-
var
|
|
552
|
-
|
|
553
|
-
hit.
|
|
566
|
+
var hit = frags.filter(function (m) { return m && m.fin && !((SEEN[m.id] || 0) >= m.fin); });
|
|
567
|
+
hit.forEach(function (m) { SEEN[m.id] = m.fin; });
|
|
568
|
+
if (hit.length) queueSeen(hit.reduce(function (o, m) { o[m.id] = m.fin; return o; }, {}));
|
|
569
|
+
if (hit.length && R.convs) renderConvs();
|
|
554
570
|
};
|
|
571
|
+
var seeAll = function () { see(Object.keys(S.frags || {}).map(function (k) { return S.frags[k]; })); };
|
|
555
572
|
|
|
556
573
|
// 跨会话通用的个人偏好 (看全部、列表 / 关系、侧栏群节点的展开、session 选择): 记在本地, 刷新后还在。
|
|
557
574
|
// 取不到 / 写不进 (隐私窗口、禁存) 就按默认值, 页面照常。
|
|
@@ -601,7 +618,7 @@
|
|
|
601
618
|
var vp = viewpoint && viewpoint !== ROLE ? viewpoint : '';
|
|
602
619
|
if (key !== CONV || (withRole || '') !== WITH || VIEW !== 'msgs' || VIEWPOINT !== vp) return selectConv(key, withRole, undefined, viewpoint);
|
|
603
620
|
// 窄屏退回列表后再点它是要回去读, 不是要取消。
|
|
604
|
-
if (!app.classList.contains('reading'))
|
|
621
|
+
if (!app.classList.contains('reading')) { app.classList.add('reading'); return seeAll(); }
|
|
605
622
|
var fold = !withRole && OPEN[key];
|
|
606
623
|
if (fold) OPEN[key] = false;
|
|
607
624
|
selectConv('a:' + ROLE, '', undefined, '');
|
|
@@ -609,10 +626,11 @@
|
|
|
609
626
|
};
|
|
610
627
|
|
|
611
628
|
// ── 会话项 / 子项 / 关系图卡片共用的三样: 一行的数据 (glance)、一行的画法 (roleRow)、排序 (recentFirst) ──
|
|
612
|
-
// glance = 时刻 · 最近一句 ·
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
629
|
+
// glance = 时刻 · 最近一句 (g) · 两枚未读 (l 的账, 见 tally): 会话项与成对的子项两样都取自己 (子项 = 我与它在这个群里的那一对),
|
|
630
|
+
// 与我无往来的子项时刻与预览取它在群里的全部记录 (whole)。名字旁: 子项是它, 私聊项是对端, 群项没有主人。
|
|
631
|
+
var glance = function (l, g) {
|
|
632
|
+
var t = tally(l);
|
|
633
|
+
return { lastTs: (g || l).lastTs, preview: (g || l).preview, unread: t.unread, theirs: t.theirs };
|
|
616
634
|
};
|
|
617
635
|
// 同层按最近活动排, 有新话就上浮。
|
|
618
636
|
var recentFirst = function (a, b) { return b.lastTs - a.lastTs; };
|
|
@@ -621,7 +639,7 @@
|
|
|
621
639
|
var SWAP = '<i class="sw" aria-hidden="true">⇄</i>';
|
|
622
640
|
var roleRow = function (id, name, label, g, status, tail, badge) {
|
|
623
641
|
return goSpan('av', id, esc(label)) +
|
|
624
|
-
line(nm(id, name, true, SWAP + (badge || '')) + (tail || ''), g.lastTs, g.preview, stTag(status, true), g.unread);
|
|
642
|
+
line(nm(id, name, true, peerBadge(id, g.theirs) + SWAP + (badge || '')) + (tail || ''), g.lastTs, g.preview, stTag(status, true), g.unread);
|
|
625
643
|
};
|
|
626
644
|
// 名字旁的「+N」: 视图外还有 N 个 —— 同一个徽标, 数什么由所在的视图注入 (会话列表数会话, 关系图数关系)。
|
|
627
645
|
var moreTag = function (n, tip) {
|
|
@@ -639,17 +657,16 @@
|
|
|
639
657
|
var convRow = function (c) {
|
|
640
658
|
if (c.kind === 'wizard') return roleRow(c.peer, c.name, c.label, glance(c), c.status, jobMark(c.jobs), otherChats(c.peer, dmKey(c.peer)));
|
|
641
659
|
if (c.kind === 'job') return '<span class="av">' + esc(c.label) + '</span>' + line('<span class="nm chat">' + esc(c.name) + '</span>' + stageTag(c.job) + jobTag(c.job) + stuckTag(c.job), c.lastTs, c.preview, '', unreadOf(c));
|
|
642
|
-
return avatarOf(c) + line('<span class="nm chat">' + esc(c.name) + '</span>' + jobMark(c.jobs), c.lastTs, c.preview, stTag(c.status, true), unreadOf(c));
|
|
660
|
+
return avatarOf(c) + line('<span class="nm chat">' + esc(c.name) + '</span>' + wxTag(c.wx) + jobMark(c.jobs), c.lastTs, c.preview, stTag(c.status, true), unreadOf(c));
|
|
643
661
|
};
|
|
644
|
-
var subRow = function (c, s) { return roleRow(s.role, s.name, s.label, glance(
|
|
662
|
+
var subRow = function (c, s) { return roleRow(s.role, s.name, s.label, glance(s), s.status, jobMark(s.jobs), otherChats(s.role, c.key + '|' + pairKey(s.role))); };
|
|
645
663
|
|
|
646
664
|
// 「chat 内全部」下与我无往来的那几项: 一行的数据是它在群里的全部记录 (服务端 whole, 与关系图卡片同一个
|
|
647
|
-
// glanceOfTalk), 点开看的也是那一份 (`a:<它>||<群>`, 对端不限)
|
|
648
|
-
//
|
|
665
|
+
// glanceOfTalk), 点开看的也是那一份 (`a:<它>||<群>`, 对端不限), 未读账也按那一份切 (服务端 subs 的 heard / theirs)。
|
|
666
|
+
// +N 数它的全部叶子, 这一项本身不是叶子。
|
|
649
667
|
var farKey = function (c, s) { return 'a:' + s.role + '||' + c.base; };
|
|
650
668
|
var farRow = function (s) {
|
|
651
|
-
|
|
652
|
-
return roleRow(s.role, s.name, s.label, { lastTs: w.lastTs, preview: w.preview, unread: 0 }, s.status, '', otherChats(s.role, ''));
|
|
669
|
+
return roleRow(s.role, s.name, s.label, glance(s, s.whole), s.status, '', otherChats(s.role, ''));
|
|
653
670
|
};
|
|
654
671
|
|
|
655
672
|
var convItem = function (c) {
|
|
@@ -686,7 +703,7 @@
|
|
|
686
703
|
if (c.kind === 'group' && c.subs && c.subs.length) {
|
|
687
704
|
// 不能是 <button>: 嵌在外层 .ci 按钮里会被解析器提前闭合外层, chevron 就掉到卡片外
|
|
688
705
|
var chevron = '<span role="button" class="ci-chevron' + (OPEN[c.key] ? ' on' : '') + '" data-toggle="' + esc(c.key) + '" title="' + (OPEN[c.key] ? '折叠群' : '展开群') + '"></span>';
|
|
689
|
-
var content = avatarOf(c) + '<span class="b"><span class="l1"><span class="t"><span class="nm chat">' + esc(c.name) + '</span>' + jobMark(c.jobs) + '</span>' + chevron + '<span class="ts">' + esc(fmtAgo(c.lastTs)) + '</span></span>' +
|
|
706
|
+
var content = avatarOf(c) + '<span class="b"><span class="l1"><span class="t"><span class="nm chat">' + esc(c.name) + '</span>' + wxTag(c.wx) + jobMark(c.jobs) + '</span>' + chevron + '<span class="ts">' + esc(fmtAgo(c.lastTs)) + '</span></span>' +
|
|
690
707
|
'<span class="l2"><span class="pv">' + esc(c.preview) + '</span>' + (unreadOf(c) ? '<b class="ub">' + (unreadOf(c) > 99 ? '99+' : unreadOf(c)) + '</b>' : '') + '</span></span>';
|
|
691
708
|
return '<button class="ci' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '">' + content + '</button>' + subs;
|
|
692
709
|
}
|
|
@@ -709,7 +726,6 @@
|
|
|
709
726
|
var renderConvs = function () {
|
|
710
727
|
var groups = R.convs.filter(function (c) { return c.kind === 'group' && inWin(c.lastTs); }).sort(recentFirst);
|
|
711
728
|
var dms = R.convs.filter(function (c) { return c.kind === 'wizard' && inWin(c.lastTs); }).sort(recentFirst);
|
|
712
|
-
R.convs.forEach(markRead);
|
|
713
729
|
if (WORLD) return renderWorld();
|
|
714
730
|
// 空着的那一栏不画 —— 对谁都一样: 人没有私聊只是这条规则的一个特例。
|
|
715
731
|
var sec = function (title, list) {
|
|
@@ -848,7 +864,7 @@
|
|
|
848
864
|
});
|
|
849
865
|
};
|
|
850
866
|
var worldToggle = function () {
|
|
851
|
-
return '<button class="vt" data-vt title="' + (WORLD ? '
|
|
867
|
+
return '<button class="vt" data-vt title="' + (WORLD ? '换回聊天' : '换成关系') + '">' + (WORLD ? LIST_SVG + '聊天' : TREE_SVG + '关系') + '</button>';
|
|
852
868
|
};
|
|
853
869
|
var bindWorldToggle = function (scope) {
|
|
854
870
|
var b = scope.querySelector('[data-vt]');
|
|
@@ -975,11 +991,29 @@
|
|
|
975
991
|
return n < .01 ? '<$0.01' : '$' + (n < 10 ? n.toFixed(2) : n < 1000 ? n.toFixed(1) : Math.round(n));
|
|
976
992
|
};
|
|
977
993
|
// 费用: 有认不出价格的模型就在数前加「≥」—— 实际只多不少; 一分钱都没算出来就不占位。
|
|
994
|
+
// 计算过程: 服务端按模型分好行 (bill, 单价取自计价用的同一份价格表), 这里逐项摆成「token × 单价 = 金额」。
|
|
995
|
+
var BILL = [['input', 'in', 'in'], ['output', 'out', 'out'], ['cacheWrite', 'cw', 'cacheWrite'], ['cacheRead', 'cr', 'cacheRead']];
|
|
996
|
+
var usd4 = function (n) { return '$' + (n >= 100 ? n.toFixed(2) : n.toFixed(4)); };
|
|
997
|
+
var billLines = function (b) {
|
|
998
|
+
var head = b.label + (b.model && b.model !== b.label ? ' (' + b.model + ')' : '') + ' · ' + b.calls + ' 次请求';
|
|
999
|
+
if (!b.price) return [head, ' 价格表里认不出, 未计入'];
|
|
1000
|
+
return [head, ' 单价 $/百万: ' + BILL.map(function (k) { return k[1] + ' ' + b.price[k[2]]; }).join(' · ')]
|
|
1001
|
+
.concat(BILL.filter(function (k) { return b[k[0]] > 0; }).map(function (k) {
|
|
1002
|
+
return ' ' + k[1] + ' ' + b[k[0]].toLocaleString('en-US') + ' × $' + b.price[k[2]] + ' / 1M = ' +
|
|
1003
|
+
usd4(b[k[0]] * b.price[k[2]] / 1e6);
|
|
1004
|
+
}))
|
|
1005
|
+
.concat([' 小计 ' + usd4(b.cost)]);
|
|
1006
|
+
};
|
|
978
1007
|
var costKv = function (u) {
|
|
979
1008
|
if (!u.cost) return '';
|
|
980
1009
|
var txt = (u.unpriced ? '≥' : '') + fmtUsd(u.cost);
|
|
981
|
-
var
|
|
982
|
-
|
|
1010
|
+
var bill = u.bill || [];
|
|
1011
|
+
var priced = bill.filter(function (b) { return b.price; });
|
|
1012
|
+
var tip = ['估算费用 ' + usd4(u.cost) + ' — 按模型单价 (LiteLLM 价格表快照, USD / 百万 token) 逐项累加', '']
|
|
1013
|
+
.concat(bill.reduce(function (a, b) { return a.concat(billLines(b), ['']); }, []))
|
|
1014
|
+
.concat(priced.length > 1 ? ['合计 ' + priced.map(function (b) { return usd4(b.cost); }).join(' + ') + ' = ' + usd4(u.cost)] : [])
|
|
1015
|
+
.concat(u.unpriced ? ['另有 ' + u.unpriced + ' 次 API 请求的模型不在价格表里, 未计入'] : [])
|
|
1016
|
+
.join('\n').replace(/\n+$/, '');
|
|
983
1017
|
return '<span class="kv kv-cost" title="' + esc(tip) + '" data-tip="' + esc(ICON.cost + ' ' + txt) +
|
|
984
1018
|
'"><i class="u-ic">' + ICON.cost + '</i><b>' + esc(txt) + '</b></span>';
|
|
985
1019
|
};
|
|
@@ -1200,7 +1234,7 @@
|
|
|
1200
1234
|
var peer = pairPeer(c);
|
|
1201
1235
|
var pairName = peer && pairTitle(peer);
|
|
1202
1236
|
who.innerHTML = peer ? '<span class="t" title="' + esc(pairName) + '">' + esc(pairName) + '</span>'
|
|
1203
|
-
: '<span class="t">' + (c.kind === 'wizard' ? nm(c.peer, c.name, true) : '<span class="nm chat">' + esc(c.name) + '</span>') + '</span>';
|
|
1237
|
+
: '<span class="t">' + (c.kind === 'wizard' ? nm(c.peer, c.name, true) : '<span class="nm chat">' + esc(c.name) + '</span>' + wxTag(c.wx)) + '</span>';
|
|
1204
1238
|
acts.innerHTML = '';
|
|
1205
1239
|
bindBack();
|
|
1206
1240
|
bindGo(who);
|
|
@@ -1273,13 +1307,13 @@
|
|
|
1273
1307
|
var c = convOf(CONV);
|
|
1274
1308
|
var two = !group || !!pairPeer(c) || (!!c && c.kind === 'all' && c.peers.length === 1);
|
|
1275
1309
|
var dst = two ? '' : mine
|
|
1276
|
-
? (m.to && m.to !== '
|
|
1310
|
+
? (m.to && m.to !== 'system:' && m.to !== peer ? m.to : '')
|
|
1277
1311
|
: (m.to !== me && group ? m.to : '');
|
|
1278
1312
|
// 说给谁画进文本泡泡开头 (头像 + 名字); 点它走移交行同一套跳转 (data-hfrom/hto/hch + gid/gts), 不换页面视角。
|
|
1279
1313
|
var toIn = dst
|
|
1280
1314
|
? '<span class="to-in" role="button" tabindex="0" data-hfrom="' + esc(m.from) + '" data-hto="' + esc(m.to) + '" data-hch="' + esc(m.channel || '') +
|
|
1281
|
-
'" data-gid="' + esc(m.id) + '" data-gts="' + m.ts + '" title="' + esc('发给 ' + nameOf(m.to) + ' —— 看这段往来') + '"
|
|
1282
|
-
esc(roleLabel(m.to)) + '</span
|
|
1315
|
+
'" data-gid="' + esc(m.id) + '" data-gts="' + m.ts + '" title="' + esc('发给 ' + nameOf(m.to) + ' —— 看这段往来') + '">' +
|
|
1316
|
+
WZ.nameHead('<span class="av">' + esc(roleLabel(m.to)) + '</span>', '<span class="nm ' + kindOf(m.to) + '">' + esc(nameOf(m.to)) + '</span>') + '</span>'
|
|
1283
1317
|
: '';
|
|
1284
1318
|
var withTo = function (html) {
|
|
1285
1319
|
if (!toIn) return html;
|
|
@@ -1298,7 +1332,7 @@
|
|
|
1298
1332
|
var stat = m.meta ? '<span class="mstat">' + jobChips(m.meta) + '</span>' : '';
|
|
1299
1333
|
var who = mine
|
|
1300
1334
|
? stamp(m.ts) + stat + avBtn(m.from)
|
|
1301
|
-
: avBtn(m.from)
|
|
1335
|
+
: '<span class="said">' + WZ.said(WZ.nameHead(avBtn(m.from), nm(m.from, '', true))) + '</span>' + priv + stamp(m.ts) + stat;
|
|
1302
1336
|
var sw = canSwitch(other);
|
|
1303
1337
|
var flip = '<button class="flip" data-r="' + esc(other) + '"' + (sw ? '' : ' disabled tabindex="-1"') +
|
|
1304
1338
|
' aria-label="' + esc(sw ? '切到 ' + nameOf(other) + ' 的视角' : '') + '">' +
|
|
@@ -1424,6 +1458,7 @@
|
|
|
1424
1458
|
foldPings(inner);
|
|
1425
1459
|
expireRows();
|
|
1426
1460
|
if (keep) keep();
|
|
1461
|
+
see(fresh);
|
|
1427
1462
|
}).catch(retry);
|
|
1428
1463
|
};
|
|
1429
1464
|
|
|
@@ -1431,9 +1466,11 @@
|
|
|
1431
1466
|
// early: 换视角时与摘要并行发出的同一个请求 (见 refresh) —— 窗口参数没变才拿来用。
|
|
1432
1467
|
var loadMsgs = function (limit, land, early) {
|
|
1433
1468
|
var gen = S.gen;
|
|
1434
|
-
if (!CONV) { inner.innerHTML = '<div class="empty">选一个会话</div>'; return Promise.resolve(); }
|
|
1469
|
+
if (!CONV) { unswitch(); inner.innerHTML = '<div class="empty">选一个会话</div>'; return Promise.resolve(); }
|
|
1435
1470
|
return (early || api('api/msgs', viewParams(limit ? { limit: limit } : {}))).then(function (d) {
|
|
1436
|
-
if (
|
|
1471
|
+
if (gen !== S.gen) return;
|
|
1472
|
+
if (!d.ok) { if (unswitch()) inner.innerHTML = '<div class="empty">载入失败</div>'; return; }
|
|
1473
|
+
unswitch();
|
|
1437
1474
|
S.frags = {};
|
|
1438
1475
|
d.msgs.forEach(function (m) { S.frags[m.id] = m; });
|
|
1439
1476
|
if (!d.msgs.length) { inner.innerHTML = '<div class="empty">这里还没有消息</div>'; return; }
|
|
@@ -1445,6 +1482,7 @@
|
|
|
1445
1482
|
expireRows();
|
|
1446
1483
|
settleAbove();
|
|
1447
1484
|
applyFold(inner);
|
|
1485
|
+
see(d.msgs);
|
|
1448
1486
|
if (land && land()) return;
|
|
1449
1487
|
S.pinned = true; toBottom(true);
|
|
1450
1488
|
// CDN 字体/代码高亮加载完会改变高度, 再吸一次底。
|
|
@@ -1457,6 +1495,7 @@
|
|
|
1457
1495
|
var stick = S.pinned;
|
|
1458
1496
|
var cur = rowNode(m.id);
|
|
1459
1497
|
S.frags[m.id] = m;
|
|
1498
|
+
see([m]);
|
|
1460
1499
|
if (cur && cur.getAttribute('data-sig') === m.sig) return;
|
|
1461
1500
|
// 折行会把 .mrow 收进 details, 而下面的按时间插位假定它们都是 inner 的直接子节点。
|
|
1462
1501
|
unfoldPings(inner);
|
|
@@ -1508,7 +1547,7 @@
|
|
|
1508
1547
|
// 收下摘要 (视角、会话列表、落地窗口) 与按它画页头侧栏分开: 换视角整窗重拉时, 画要等到新行到手的同一帧。
|
|
1509
1548
|
var takeRole = function (d) {
|
|
1510
1549
|
R.at = d.at || Date.now(); R.recvAt = Date.now();
|
|
1511
|
-
if (!BASE) BASE = R.at;
|
|
1550
|
+
if (d.seenBase) BASE = d.seenBase; else if (!BASE) BASE = R.at;
|
|
1512
1551
|
R.role = d.role; R.sessions = d.sessions || []; R.convs = d.convs || []; R.chatKeys = d.chatKeys;
|
|
1513
1552
|
SESSION = d.session || '';
|
|
1514
1553
|
R.relations = !!d.relations; R.schedules = d.schedules || 0; R.plan = d.plan || null; R.inflight = d.inflight || [];
|
|
@@ -1533,13 +1572,15 @@
|
|
|
1533
1572
|
};
|
|
1534
1573
|
|
|
1535
1574
|
// 切回标签页时正在读的那项就算读过了。
|
|
1536
|
-
document.addEventListener('visibilitychange', function () { if (!document.hidden)
|
|
1575
|
+
document.addEventListener('visibilitychange', function () { if (!document.hidden) seeAll(); });
|
|
1537
1576
|
|
|
1538
1577
|
// ── SSE: role 摘要 + 当前窗口的消息增量。断线指数退避重连。 ──
|
|
1539
1578
|
var backoff = 1000;
|
|
1540
1579
|
var connect = function () {
|
|
1541
1580
|
if (S.es) { S.es.close(); S.es = null; }
|
|
1542
|
-
|
|
1581
|
+
// 换窗口的新内容还没上屏: 新窗口的增量会落进旧内容里 —— 等 loadMsgs 替换完再连 (断线重连的定时器也在这儿挡下)。
|
|
1582
|
+
if (S.swapGen === S.gen) return;
|
|
1583
|
+
var p = new URLSearchParams(roleParams({ id: TOKEN }));
|
|
1543
1584
|
var es = new EventSource('api/role-events?' + p.toString());
|
|
1544
1585
|
S.es = es;
|
|
1545
1586
|
es.addEventListener('role', function (e) { try { applyRole(JSON.parse(e.data)); } catch (err) { } });
|
|
@@ -1566,7 +1607,7 @@
|
|
|
1566
1607
|
var want = hold || !CONV ? '' : winSig();
|
|
1567
1608
|
var early = want ? api('api/msgs', viewParams()) : null;
|
|
1568
1609
|
W.pickHold = hold;
|
|
1569
|
-
return api('api/role',
|
|
1610
|
+
return api('api/role', roleParams()).then(function (d) {
|
|
1570
1611
|
if (!d.ok) {
|
|
1571
1612
|
document.body.innerHTML = '<div class="empty" style="padding:80px">' + esc(d.error || 'not found') + '</div>';
|
|
1572
1613
|
return;
|
|
@@ -1591,18 +1632,43 @@
|
|
|
1591
1632
|
};
|
|
1592
1633
|
|
|
1593
1634
|
// land: 见 loadMsgs —— 搜索跳到一条具体消息时由它来定位, 不吸底。
|
|
1635
|
+
// 详情区正看着一个窗口时换窗口不清屏: 旧页头 / 正文 / 页脚原样留着 (只变暗、不接点击, 顶上一道细进度条),
|
|
1636
|
+
// 新消息到手的同一帧整块替换 —— 已读也在那一刻才记 (see 跟着替换走)。连点只画最后一次 (gen)。
|
|
1637
|
+
// 详情区原本不可见 (窄屏在列表、停在关系图 / 日程) 就没有可留的, 照旧先清成「加载中」。
|
|
1594
1638
|
var selectConv = function (key, withRole, land, viewpoint) {
|
|
1639
|
+
var warm = VIEW === 'msgs' && thread.offsetParent !== null && !!inner.querySelector('.mrow, .empty');
|
|
1595
1640
|
CONV = key; WITH = withRole || ''; VIEWPOINT = viewpoint && viewpoint !== ROLE ? viewpoint : '';
|
|
1596
1641
|
reveal();
|
|
1597
1642
|
if (VIEW !== 'msgs') setView('msgs');
|
|
1598
1643
|
app.classList.add('reading');
|
|
1599
1644
|
// 上一个窗口的账不属于这个窗口 —— 先收起, 新的随 SSE 的首个 role 事件到。
|
|
1600
1645
|
R.winStats = null;
|
|
1601
|
-
renderConvs(); renderHead(); renderUsage();
|
|
1602
1646
|
S.gen++;
|
|
1603
1647
|
syncUrl();
|
|
1604
|
-
|
|
1605
|
-
|
|
1648
|
+
renderConvs();
|
|
1649
|
+
if (warm) {
|
|
1650
|
+
// 旧窗口的 SSE 先断: 它的增量 / 摘要不该再改留在屏上的旧内容, 新的等替换完 (connect 里挡着) 再连。
|
|
1651
|
+
if (S.es) { S.es.close(); S.es = null; }
|
|
1652
|
+
S.swapGen = S.gen;
|
|
1653
|
+
main.classList.add('switching');
|
|
1654
|
+
} else {
|
|
1655
|
+
renderHead(); renderUsage();
|
|
1656
|
+
inner.innerHTML = '<div class="empty">加载中…</div>';
|
|
1657
|
+
}
|
|
1658
|
+
var gen = S.gen;
|
|
1659
|
+
loadMsgs(undefined, land).catch(function () {
|
|
1660
|
+
if (gen !== S.gen) return;
|
|
1661
|
+
unswitch(); inner.innerHTML = '<div class="empty">载入失败</div>';
|
|
1662
|
+
}).then(function () { if (gen === S.gen) connect(); }); // 被后一次点击取代的由那一次去连
|
|
1663
|
+
};
|
|
1664
|
+
// 待替换的那次换窗口落地 (或被更新的一次取代): 收起加载态, 补画留着没动的页头页脚。返回它是否在等。
|
|
1665
|
+
var unswitch = function () {
|
|
1666
|
+
var was = S.swapGen != null;
|
|
1667
|
+
if (!was) return false;
|
|
1668
|
+
S.swapGen = null;
|
|
1669
|
+
main.classList.remove('switching');
|
|
1670
|
+
renderHead(); renderUsage();
|
|
1671
|
+
return true;
|
|
1606
1672
|
};
|
|
1607
1673
|
|
|
1608
1674
|
// 换视角: 窗口尽量留在同一个群 —— 从群里的一条消息切过去, 最想看的是对方在这个群
|
|
@@ -1616,7 +1682,7 @@
|
|
|
1616
1682
|
// 群里「我与 X」时换到 X, 对面看到的是「X 与 from」—— 同一段往来, 选中这一对而不是整个群。
|
|
1617
1683
|
var withBack = keep && WITH === id ? from : '';
|
|
1618
1684
|
// 整个频道 / 两人私聊 / 群里的一对, 消息集合都与视角无关 —— 集合没变就就地翻面。
|
|
1619
|
-
var same = VIEW === 'msgs' && (!WITH || withBack) && !SESSION && (keep || CONV.indexOf('p:') === 0);
|
|
1685
|
+
var same = VIEW === 'msgs' && (!WITH || withBack) && !SESSION && (keep || CONV.indexOf('p:') === 0) && S.swapGen !== S.gen;
|
|
1620
1686
|
ROLE = id; WITH = withBack; SESSION = ''; VIEWPOINT = '';
|
|
1621
1687
|
CONV = keep || (CONV.indexOf('p:') === 0 ? 'p:' + from : '');
|
|
1622
1688
|
// 停在关系图时换视角 = 选中新视角自己那张卡片: 先开它的全部对话 (不留旧选中 / 空白),
|
|
@@ -1757,7 +1823,7 @@
|
|
|
1757
1823
|
S.gen++;
|
|
1758
1824
|
syncUrl();
|
|
1759
1825
|
var gen = S.gen;
|
|
1760
|
-
api('api/role',
|
|
1826
|
+
api('api/role', roleParams()).then(function (d) {
|
|
1761
1827
|
if (!d.ok || gen !== S.gen) return;
|
|
1762
1828
|
applyRole(d);
|
|
1763
1829
|
// 断点 (/clear /new) 是旧视角 role 自己的, 新视角的由下面补拉。
|
|
@@ -1982,23 +2048,26 @@
|
|
|
1982
2048
|
// 而不是节点自己最后的动静 —— 卡片承载的是边。
|
|
1983
2049
|
// 身份 (头像 / 名字 / 状态灯) 永远是卡片自己的节点: 会话项的身份是「视角的对端」, 视角自己那张卡片
|
|
1984
2050
|
// (边的下端是视角) 的对端是它的父亲 —— 拿会话项的身份去画, 这张卡片就成了父亲的分身。
|
|
1985
|
-
//
|
|
1986
|
-
//
|
|
2051
|
+
// 时刻、最近一句与两枚未读取自卡片自己的窗口 (点它打开的那一份, 服务端 /api/glance 用同一个 talkOf 算, 说话人前缀
|
|
2052
|
+
// 也在那里定; 未读账与侧栏同一个 ledgerOf 按这一窗切, 主人 = 卡片上的 role)。窗口还没取到 (或第一遍量树时) 时刻与
|
|
2053
|
+
// 最近一句退回视角与对端的会话项 / 节点, 未读先不画 —— 那一项点开看的不是这一窗, 它的账不属于这张卡片。
|
|
1987
2054
|
var cardGlance = function (F, t, e) {
|
|
2055
|
+
// 账随看的 role 而变 (红点里有视角的未读): 换了视角, 同一个窗口键下的旧账不能拿来画。
|
|
2056
|
+
var w = F.links && W.gRole === ROLE && W.glance[talkKey(F.links, t)];
|
|
2057
|
+
if (w) return glance(w);
|
|
1988
2058
|
var hit = edgeConv(F, t, e), c = hit && convOf(hit[0]);
|
|
1989
2059
|
var s = c && hit[1] && (c.subs || []).filter(function (x) { return x.role === hit[1]; })[0];
|
|
1990
2060
|
var n = nodeOf(t) || {}, p = e || F.pp[t];
|
|
1991
|
-
var
|
|
1992
|
-
|
|
1993
|
-
return w ? { lastTs: w.lastTs, preview: w.preview, unread: base.unread } : base;
|
|
2061
|
+
var g = s || c || { lastTs: p ? p.last : n.lastTs || 0, preview: n.preview || '' };
|
|
2062
|
+
return { lastTs: g.lastTs, preview: g.preview, unread: 0, theirs: 0 };
|
|
1994
2063
|
};
|
|
1995
2064
|
// 卡片窗口的 glance: 树里画了谁变了就取一次, 世界快照每次轮询也顺带刷新。
|
|
1996
2065
|
var loadGlance = function () {
|
|
1997
2066
|
var keys = W.gKeys, role = ROLE;
|
|
1998
2067
|
if (!keys) return;
|
|
1999
|
-
api('api/glance', { role: role, keys: keys }).then(function (d) {
|
|
2068
|
+
api('api/glance', { role: role, keys: keys, since: BASE }).then(function (d) {
|
|
2000
2069
|
if (!d.ok || role !== ROLE || keys !== W.gKeys) return;
|
|
2001
|
-
W.glance = d.glances || {};
|
|
2070
|
+
W.glance = d.glances || {}; W.gRole = role;
|
|
2002
2071
|
renderWorld();
|
|
2003
2072
|
}).catch(function () { });
|
|
2004
2073
|
};
|
|
@@ -2323,7 +2392,7 @@
|
|
|
2323
2392
|
// 手机上关系/日程占满主区 —— 进入即阅读态。
|
|
2324
2393
|
if (v !== 'msgs') app.classList.add('reading');
|
|
2325
2394
|
renderRole(); renderHead(); renderUsage();
|
|
2326
|
-
if (v === 'msgs') { toBottom(true); renderConvs(); }
|
|
2395
|
+
if (v === 'msgs') { toBottom(true); renderConvs(); seeAll(); }
|
|
2327
2396
|
else if (v === 'charter') loadCharter();
|
|
2328
2397
|
else if (v === 'jobs') renderJobList();
|
|
2329
2398
|
else {
|
|
@@ -2424,7 +2493,7 @@
|
|
|
2424
2493
|
all().forEach(function (x) { x.ontoggle = sync; });
|
|
2425
2494
|
};
|
|
2426
2495
|
|
|
2427
|
-
// 侧栏换成关系图 /
|
|
2496
|
+
// 侧栏换成关系图 / 换回聊天。换回时选中的仍是在关系图里点开的那一项, 并把它滚进视野。
|
|
2428
2497
|
var setWorld = function (on) {
|
|
2429
2498
|
WORLD = on; W.treeFor = ''; W.pickSelf = '';
|
|
2430
2499
|
setPref(WORLD_KEY, on);
|
|
@@ -2521,12 +2590,11 @@
|
|
|
2521
2590
|
};
|
|
2522
2591
|
// 三层: 谁 → 对谁 · 何时 / 命中的那一截 / 在哪个会话。
|
|
2523
2592
|
var msgItem = function (h, q) {
|
|
2524
|
-
var to = h.to && h.to !== h.from && h.to !== '
|
|
2525
|
-
? '<span class="arr">→</span>' + nm(h.to, h.toName) : '';
|
|
2593
|
+
var to = h.to && h.to !== h.from && h.to !== 'system:' ? nm(h.to, h.toName) : undefined;
|
|
2526
2594
|
return {
|
|
2527
2595
|
kind: 'msg',
|
|
2528
2596
|
html: '<span class="av">' + esc(h.fromLabel) + '</span><span class="b"><span class="l1">' +
|
|
2529
|
-
nm(h.from, h.fromName) +
|
|
2597
|
+
'<span class="said">' + WZ.speakerHead(nm(h.from, h.fromName), to) + '</span>' + skTs(h.ts) + '</span>' +
|
|
2530
2598
|
'<span class="sn">' + hl(h.snippet, q) + '</span>' +
|
|
2531
2599
|
'<span class="l3">' + whereOf(h.convKind, h.convLabel, h.convName) +
|
|
2532
2600
|
(h.hits > 1 ? '<span class="cnt">' + h.hits + ' 处命中</span>' : '') + '</span></span>',
|