wezard 2.4.1 → 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 +11 -7
- 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 +5 -6
- 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 +57 -14
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +297 -65
- 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 +109 -92
- 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 +35 -15
- 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 +29 -23
- 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 +145 -3
- 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/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 +47 -19
- 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 +59 -28
- 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 +35 -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 +4 -3
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-view.js +97 -62
- 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/package.json +6 -2
- package/web/chat.css +26 -5
- package/web/chat.js +128 -63
package/web/chat.css
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
--priv: #9a5b10; /* 私聊 */
|
|
21
21
|
--human: #12715a; /* 人名 —— 青绿, 与 wizard (墨色) 和群名 (暗金) 三分 */
|
|
22
22
|
--chat: #7d6410; /* 群名 —— 暗金, 与 role 名 (墨色 + 视角色的点) 分开: 群是频道不是 role */
|
|
23
|
+
--wezard: #6b3fa0; /* 系统 wezard (守护进程与认不出的发话方) —— 紫金: 紫起金收, 两端都压暗到正文对比度 */
|
|
24
|
+
--wezard-gold: #9a6a00;
|
|
23
25
|
/* 头像底: 不透明, 且由 card/accent 推出 —— 叠在选中/高亮/关系图任何底色上都不变色, 换主题时随 token 走。 */
|
|
24
26
|
--av-bg: var(--card);
|
|
25
27
|
--av-line: var(--line);
|
|
@@ -46,6 +48,11 @@ button { font: inherit; color: inherit }
|
|
|
46
48
|
.nm.human::before, .nm.task::before, .nm.system::before, .nm.chat::before { content: none }
|
|
47
49
|
.nm.chat, .ch-who .sub .up { color: var(--chat) }
|
|
48
50
|
.nm.human { color: var(--human) }
|
|
51
|
+
.nm.system {
|
|
52
|
+
color: var(--wezard);
|
|
53
|
+
background: linear-gradient(90deg, var(--wezard), var(--wezard-gold));
|
|
54
|
+
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
|
|
55
|
+
}
|
|
49
56
|
|
|
50
57
|
/* ── 侧栏: 我 + 会话 ─────────────────────────────────────────────── */
|
|
51
58
|
.side {
|
|
@@ -330,6 +337,14 @@ span.av.go:hover { background: var(--av-hot); box-shadow: 0 0 0 3px var(--av-hot
|
|
|
330
337
|
padding: 16px 20px 28px; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
|
|
331
338
|
}
|
|
332
339
|
.thread-in { display: flex; flex-direction: column; gap: 14px }
|
|
340
|
+
/* 换窗口时旧内容留着等新内容: 不接点击, 稍候才变暗 (快的切换看不出闪), 页头底边一道细进度条。 */
|
|
341
|
+
.main.switching .thread-in, .main.switching .chathead > :not(.back) { pointer-events: none }
|
|
342
|
+
.main.switching .thread-in { opacity: .6; transition: opacity .2s .15s }
|
|
343
|
+
.main.switching .chathead {
|
|
344
|
+
background-image: linear-gradient(var(--accent), var(--accent)); background-repeat: no-repeat;
|
|
345
|
+
background-size: 40% 2px; animation: swbar 1s ease-in-out infinite;
|
|
346
|
+
}
|
|
347
|
+
@keyframes swbar { from { background-position: -67% 100% } to { background-position: 167% 100% } }
|
|
333
348
|
.empty { color: var(--faint); text-align: center; padding: 60px 0; font-size: 13px }
|
|
334
349
|
.more-btn {
|
|
335
350
|
align-self: center; background: var(--card); border: 1px solid var(--line); border-radius: 16px;
|
|
@@ -351,6 +366,9 @@ span.av.go:hover { background: var(--av-hot); box-shadow: 0 0 0 3px var(--av-hot
|
|
|
351
366
|
.mrow.mine .mcol { align-items: flex-end }
|
|
352
367
|
.mwho { font-size: 11.5px; color: var(--mute); display: flex; gap: 6px; align-items: center; flex-wrap: wrap }
|
|
353
368
|
.mwho .nm { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
369
|
+
/* 发言头 `🦊.a:` (WZ.said): 头像贴着名字、冒号贴着名字, 不吃 .mwho 的 gap。 */
|
|
370
|
+
.mwho .said { display: inline-flex; align-items: center; min-width: 0; max-width: 100% }
|
|
371
|
+
.mwho .said .av { margin-right: 2px }
|
|
354
372
|
.mwho .ch { font-size: 10.5px; border-radius: 4px; padding: 0 4px }
|
|
355
373
|
.mwho .ch.priv { color: var(--priv); background: #9a5b1014 }
|
|
356
374
|
/* 时刻只此一处 —— 气泡外, 跟着名字走。等宽数字让一列时间对得齐。 */
|
|
@@ -382,16 +400,16 @@ button.mchip.rcpt:hover { text-decoration: underline }
|
|
|
382
400
|
.mchip.rcpt:is(.st-timeout, .st-silent, .st-dead, .st-canceled) { color: #cf222e; background: #cf222e10 }
|
|
383
401
|
/* 说给谁 (inline): 塞进正文第一段开头的头像 + 名字, 像行内 mention; 小圆角底色, 点它走移交行同一套跳转。 */
|
|
384
402
|
.to-in {
|
|
385
|
-
display: inline-flex; align-items: center; gap:
|
|
403
|
+
display: inline-flex; align-items: center; gap: 0; vertical-align: baseline; padding: 1px 6px; border-radius: 5px;
|
|
386
404
|
line-height: 18px; background: color-mix(in srgb, #0969da 10%, transparent); color: var(--ink);
|
|
387
405
|
cursor: pointer; transition: background .12s;
|
|
388
406
|
}
|
|
389
407
|
.to-in:hover { background: color-mix(in srgb, #0969da 20%, transparent) }
|
|
390
|
-
.to-in .av { width: 16px; height: 16px; border-radius: 50%; border: 1px solid var(--line); background: var(--card); font-size: 10px; line-height: 14px; text-align: center; align-self: center }
|
|
408
|
+
.to-in .av { margin-right: 2px; width: 16px; height: 16px; border-radius: 50%; border: 1px solid var(--line); background: var(--card); font-size: 10px; line-height: 14px; text-align: center; align-self: center }
|
|
391
409
|
.to-in .nm { font-weight: 500; font-size: 13px }
|
|
392
410
|
.to-in .nm::before { content: none }
|
|
393
411
|
/* 移交行里的对方: 小头像 + 名字。整行是入口 (开双方的往来), 悬停时整行提亮。 */
|
|
394
|
-
.handoff .ho-who { display: inline-flex; align-items: center; gap:
|
|
412
|
+
.handoff .ho-who { display: inline-flex; align-items: center; gap: 2px; color: var(--ink) }
|
|
395
413
|
.handoff .ho-line[data-hto]:hover { background: var(--av-hot); border-radius: 6px }
|
|
396
414
|
.handoff .ho-who .av { width: 16px; height: 16px; border-radius: 50%; border: 1px solid var(--line); background: var(--card); font-size: 10px; line-height: 14px; text-align: center }
|
|
397
415
|
.handoff .ho-who .nm { font-weight: 500 }
|
|
@@ -906,8 +924,8 @@ button.mchip.rcpt:hover { text-decoration: underline }
|
|
|
906
924
|
.sk-it.on .k { opacity: 1; color: var(--accent) }
|
|
907
925
|
.sk-it .ts { margin-left: auto; flex: none; font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums }
|
|
908
926
|
.sk-it .pv { display: block; font-size: 12px; color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
909
|
-
.sk-it .
|
|
910
|
-
.sk-it .
|
|
927
|
+
.sk-it .said { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
928
|
+
.sk-it .said .nm + .nm { font-weight: 500; color: var(--mute) }
|
|
911
929
|
.sk-it .tag { flex: none; font-size: 10.5px; color: var(--mute); background: var(--paper); border-radius: 4px; padding: 1px 5px }
|
|
912
930
|
/* 命中的那一截: 正文色, 最多两行; 命中词是这一页的视角色淡底 —— 与选中态一个色系。 */
|
|
913
931
|
.sk-it .sn {
|
|
@@ -949,6 +967,9 @@ button.mchip.rcpt:hover { text-decoration: underline }
|
|
|
949
967
|
.jtag { flex: none; margin-left: 6px; font-size: 10.5px; line-height: 15px; padding: 0 5px; border-radius: 4px; color: var(--faint); background: #f1f3f6; font-weight: 500 }
|
|
950
968
|
.jtag.open { color: #94741f; background: #bf870014 }
|
|
951
969
|
.jstuck { flex: none; margin-left: 4px; font-size: 10.5px; line-height: 15px; padding: 0 5px; border-radius: 4px; color: #8250df; background: #fbefff; font-weight: 500 }
|
|
970
|
+
.wxm { flex: none; margin-left: 4px; padding: 0 4px; border-radius: 4px; font-size: 10.5px; white-space: nowrap; color: #2f7d32; background: #2f7d3214 }
|
|
971
|
+
.wxm.paused { color: #94741f; background: #bf870014 }
|
|
972
|
+
.wxm.down { color: #c62828; background: #c6282814 }
|
|
952
973
|
.jmk { flex: none; margin-left: 4px; padding: 0 3px; border-radius: 4px; font-size: 10.5px; opacity: .8; cursor: pointer }
|
|
953
974
|
.jmk:hover { opacity: 1; background: #bf870022 }
|
|
954
975
|
.jmk i { font-style: normal; margin-left: 1px; color: #94741f; font-size: 10px }
|
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(); };
|
|
@@ -144,11 +144,11 @@
|
|
|
144
144
|
};
|
|
145
145
|
var roleLabel = function (id) {
|
|
146
146
|
var k = names[id] || {}, n = nodeOf(id);
|
|
147
|
-
return k.label || (n && n.label) || ({ human: '👤', task: '⏰', system: '
|
|
147
|
+
return k.label || (n && n.label) || ({ human: '👤', task: '⏰', system: '🧙♂️' })[kindOf(id)] || '🧙';
|
|
148
148
|
};
|
|
149
149
|
// `.name` 的点由 CSS 画 —— 人与定时任务没有地址, 不画点。
|
|
150
150
|
var nameOf = function (id) { return (kindOf(id) === 'wizard' ? '.' : '') + roleName(id); };
|
|
151
|
-
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'; };
|
|
152
152
|
// 头像与名字就是进入那个 role 视角的入口 —— 侧栏项、会话头、消息行同一种读法。
|
|
153
153
|
// 是 <span> 不是 <button>: 侧栏里它嵌在整行那颗按钮里。
|
|
154
154
|
var goSpan = function (cls, id, inner) {
|
|
@@ -426,6 +426,10 @@
|
|
|
426
426
|
var jobMark = function (ids) {
|
|
427
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>' : '';
|
|
428
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
|
+
};
|
|
429
433
|
// 视角不在里面的工单不在 R.convs 里: 按服务端的账 (R.jobIndex, 全部工单) 现造一项; 账里没有 = 已清掉 (job 为空)。
|
|
430
434
|
var jobConv = function (key) {
|
|
431
435
|
var id = key.slice(2), j = (R.jobIndex || {})[id];
|
|
@@ -513,30 +517,45 @@
|
|
|
513
517
|
};
|
|
514
518
|
|
|
515
519
|
// ── 未读: 别人说完、还没在详情里读到的话 ──
|
|
516
|
-
//
|
|
517
|
-
//
|
|
520
|
+
// 服务端给每一行 (侧栏会话项 / 子项、关系图卡片) 一份账, 条目 = [说完的时刻, 发话方, 收信方, 消息 id], 按点开这一行
|
|
521
|
+
// 看到的消息 (与详情区取消息同一个 windowOf) 切成不相交的两份: heard = 这里看得见的未读 (右边红点) —— 页面视角的,
|
|
522
|
+
// 加上这一行主人的; theirs = 主人在这里看不见的未读 (名字旁) —— 切到它的视角才看得到。一句话在一行里最多数一次,
|
|
523
|
+
// 点开这一行读到的只从红点里减, 名字旁不因此变。
|
|
518
524
|
// 已读只有一份 (SEEN): 这句话说完的样子在详情里上过屏 (页面可见、正在看消息) 就记下它的 id ——
|
|
519
525
|
// 与页面视角、详情视角、侧栏 / 关系图无关, 哪一处读到, 所有视图里同一句话一起消失, 不必等下一份摘要。
|
|
520
|
-
//
|
|
521
|
-
//
|
|
526
|
+
// SEEN 落在 svr 上 (全局一份): 新看过的攒一小会儿批量写回, 服务端下发的账已经减掉记过的, 并带回基线 (seenBase) ——
|
|
527
|
+
// 刷新、另一个标签页 (下一次心跳) 都看得到。没落盘的服务端 (daemon 自己的页、老 svr) 或读写失败, 退回只活在内存里:
|
|
528
|
+
// 基线是「打开页面这一刻」(服务端时钟, 首次拿到快照时定), 刷新即重置。
|
|
522
529
|
var BASE = 0;
|
|
523
530
|
var SEEN = {};
|
|
524
|
-
var
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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) }; };
|
|
538
557
|
var peerBadge = function (id, n) {
|
|
539
|
-
return n && id !== ROLE ? '<b class="pub" title="' + esc(nameOf(id) + ' 的视角下还有 ' + n + '
|
|
558
|
+
return n && id !== ROLE ? '<b class="pub" title="' + esc(nameOf(id) + ' 的视角下还有 ' + n + ' 条未读, 这一项里看不到') + '">' + (n > 99 ? '99+' : n) + '</b>' : '';
|
|
540
559
|
};
|
|
541
560
|
// 详情区真在屏上: 窄屏退回列表时它整个 display:none (offsetParent 为空)。
|
|
542
561
|
var reading = function () { return VIEW === 'msgs' && !document.hidden && thread.offsetParent !== null; };
|
|
@@ -546,6 +565,7 @@
|
|
|
546
565
|
if (!reading()) return;
|
|
547
566
|
var hit = frags.filter(function (m) { return m && m.fin && !((SEEN[m.id] || 0) >= m.fin); });
|
|
548
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; }, {}));
|
|
549
569
|
if (hit.length && R.convs) renderConvs();
|
|
550
570
|
};
|
|
551
571
|
var seeAll = function () { see(Object.keys(S.frags || {}).map(function (k) { return S.frags[k]; })); };
|
|
@@ -606,11 +626,11 @@
|
|
|
606
626
|
};
|
|
607
627
|
|
|
608
628
|
// ── 会话项 / 子项 / 关系图卡片共用的三样: 一行的数据 (glance)、一行的画法 (roleRow)、排序 (recentFirst) ──
|
|
609
|
-
// glance = 时刻 · 最近一句 ·
|
|
610
|
-
//
|
|
611
|
-
var glance = function (
|
|
612
|
-
var
|
|
613
|
-
return { lastTs:
|
|
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 };
|
|
614
634
|
};
|
|
615
635
|
// 同层按最近活动排, 有新话就上浮。
|
|
616
636
|
var recentFirst = function (a, b) { return b.lastTs - a.lastTs; };
|
|
@@ -637,17 +657,16 @@
|
|
|
637
657
|
var convRow = function (c) {
|
|
638
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)));
|
|
639
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));
|
|
640
|
-
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));
|
|
641
661
|
};
|
|
642
|
-
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))); };
|
|
643
663
|
|
|
644
664
|
// 「chat 内全部」下与我无往来的那几项: 一行的数据是它在群里的全部记录 (服务端 whole, 与关系图卡片同一个
|
|
645
|
-
// glanceOfTalk), 点开看的也是那一份 (`a:<它>||<群>`, 对端不限)
|
|
646
|
-
//
|
|
665
|
+
// glanceOfTalk), 点开看的也是那一份 (`a:<它>||<群>`, 对端不限), 未读账也按那一份切 (服务端 subs 的 heard / theirs)。
|
|
666
|
+
// +N 数它的全部叶子, 这一项本身不是叶子。
|
|
647
667
|
var farKey = function (c, s) { return 'a:' + s.role + '||' + c.base; };
|
|
648
668
|
var farRow = function (s) {
|
|
649
|
-
|
|
650
|
-
return roleRow(s.role, s.name, s.label, { lastTs: w.lastTs, preview: w.preview, unread: 0, theirs: theirsOf(s.role) }, s.status, '', otherChats(s.role, ''));
|
|
669
|
+
return roleRow(s.role, s.name, s.label, glance(s, s.whole), s.status, '', otherChats(s.role, ''));
|
|
651
670
|
};
|
|
652
671
|
|
|
653
672
|
var convItem = function (c) {
|
|
@@ -684,7 +703,7 @@
|
|
|
684
703
|
if (c.kind === 'group' && c.subs && c.subs.length) {
|
|
685
704
|
// 不能是 <button>: 嵌在外层 .ci 按钮里会被解析器提前闭合外层, chevron 就掉到卡片外
|
|
686
705
|
var chevron = '<span role="button" class="ci-chevron' + (OPEN[c.key] ? ' on' : '') + '" data-toggle="' + esc(c.key) + '" title="' + (OPEN[c.key] ? '折叠群' : '展开群') + '"></span>';
|
|
687
|
-
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>' +
|
|
688
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>';
|
|
689
708
|
return '<button class="ci' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '">' + content + '</button>' + subs;
|
|
690
709
|
}
|
|
@@ -972,11 +991,29 @@
|
|
|
972
991
|
return n < .01 ? '<$0.01' : '$' + (n < 10 ? n.toFixed(2) : n < 1000 ? n.toFixed(1) : Math.round(n));
|
|
973
992
|
};
|
|
974
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
|
+
};
|
|
975
1007
|
var costKv = function (u) {
|
|
976
1008
|
if (!u.cost) return '';
|
|
977
1009
|
var txt = (u.unpriced ? '≥' : '') + fmtUsd(u.cost);
|
|
978
|
-
var
|
|
979
|
-
|
|
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+$/, '');
|
|
980
1017
|
return '<span class="kv kv-cost" title="' + esc(tip) + '" data-tip="' + esc(ICON.cost + ' ' + txt) +
|
|
981
1018
|
'"><i class="u-ic">' + ICON.cost + '</i><b>' + esc(txt) + '</b></span>';
|
|
982
1019
|
};
|
|
@@ -1197,7 +1234,7 @@
|
|
|
1197
1234
|
var peer = pairPeer(c);
|
|
1198
1235
|
var pairName = peer && pairTitle(peer);
|
|
1199
1236
|
who.innerHTML = peer ? '<span class="t" title="' + esc(pairName) + '">' + esc(pairName) + '</span>'
|
|
1200
|
-
: '<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>';
|
|
1201
1238
|
acts.innerHTML = '';
|
|
1202
1239
|
bindBack();
|
|
1203
1240
|
bindGo(who);
|
|
@@ -1270,13 +1307,13 @@
|
|
|
1270
1307
|
var c = convOf(CONV);
|
|
1271
1308
|
var two = !group || !!pairPeer(c) || (!!c && c.kind === 'all' && c.peers.length === 1);
|
|
1272
1309
|
var dst = two ? '' : mine
|
|
1273
|
-
? (m.to && m.to !== '
|
|
1310
|
+
? (m.to && m.to !== 'system:' && m.to !== peer ? m.to : '')
|
|
1274
1311
|
: (m.to !== me && group ? m.to : '');
|
|
1275
1312
|
// 说给谁画进文本泡泡开头 (头像 + 名字); 点它走移交行同一套跳转 (data-hfrom/hto/hch + gid/gts), 不换页面视角。
|
|
1276
1313
|
var toIn = dst
|
|
1277
1314
|
? '<span class="to-in" role="button" tabindex="0" data-hfrom="' + esc(m.from) + '" data-hto="' + esc(m.to) + '" data-hch="' + esc(m.channel || '') +
|
|
1278
|
-
'" data-gid="' + esc(m.id) + '" data-gts="' + m.ts + '" title="' + esc('发给 ' + nameOf(m.to) + ' —— 看这段往来') + '"
|
|
1279
|
-
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>'
|
|
1280
1317
|
: '';
|
|
1281
1318
|
var withTo = function (html) {
|
|
1282
1319
|
if (!toIn) return html;
|
|
@@ -1295,7 +1332,7 @@
|
|
|
1295
1332
|
var stat = m.meta ? '<span class="mstat">' + jobChips(m.meta) + '</span>' : '';
|
|
1296
1333
|
var who = mine
|
|
1297
1334
|
? stamp(m.ts) + stat + avBtn(m.from)
|
|
1298
|
-
: avBtn(m.from)
|
|
1335
|
+
: '<span class="said">' + WZ.said(WZ.nameHead(avBtn(m.from), nm(m.from, '', true))) + '</span>' + priv + stamp(m.ts) + stat;
|
|
1299
1336
|
var sw = canSwitch(other);
|
|
1300
1337
|
var flip = '<button class="flip" data-r="' + esc(other) + '"' + (sw ? '' : ' disabled tabindex="-1"') +
|
|
1301
1338
|
' aria-label="' + esc(sw ? '切到 ' + nameOf(other) + ' 的视角' : '') + '">' +
|
|
@@ -1429,9 +1466,11 @@
|
|
|
1429
1466
|
// early: 换视角时与摘要并行发出的同一个请求 (见 refresh) —— 窗口参数没变才拿来用。
|
|
1430
1467
|
var loadMsgs = function (limit, land, early) {
|
|
1431
1468
|
var gen = S.gen;
|
|
1432
|
-
if (!CONV) { inner.innerHTML = '<div class="empty">选一个会话</div>'; return Promise.resolve(); }
|
|
1469
|
+
if (!CONV) { unswitch(); inner.innerHTML = '<div class="empty">选一个会话</div>'; return Promise.resolve(); }
|
|
1433
1470
|
return (early || api('api/msgs', viewParams(limit ? { limit: limit } : {}))).then(function (d) {
|
|
1434
|
-
if (
|
|
1471
|
+
if (gen !== S.gen) return;
|
|
1472
|
+
if (!d.ok) { if (unswitch()) inner.innerHTML = '<div class="empty">载入失败</div>'; return; }
|
|
1473
|
+
unswitch();
|
|
1435
1474
|
S.frags = {};
|
|
1436
1475
|
d.msgs.forEach(function (m) { S.frags[m.id] = m; });
|
|
1437
1476
|
if (!d.msgs.length) { inner.innerHTML = '<div class="empty">这里还没有消息</div>'; return; }
|
|
@@ -1508,8 +1547,8 @@
|
|
|
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;
|
|
1512
|
-
R.role = d.role; R.sessions = d.sessions || []; R.convs = d.convs || []; R.chatKeys = d.chatKeys;
|
|
1550
|
+
if (d.seenBase) BASE = d.seenBase; else if (!BASE) BASE = R.at;
|
|
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 || [];
|
|
1515
1554
|
// 工单的账变了 (或比消息晚到): 已上屏的工单 badge 按新账重填一遍 —— 片段里只写了工单号。
|
|
@@ -1539,6 +1578,8 @@
|
|
|
1539
1578
|
var backoff = 1000;
|
|
1540
1579
|
var connect = function () {
|
|
1541
1580
|
if (S.es) { S.es.close(); S.es = null; }
|
|
1581
|
+
// 换窗口的新内容还没上屏: 新窗口的增量会落进旧内容里 —— 等 loadMsgs 替换完再连 (断线重连的定时器也在这儿挡下)。
|
|
1582
|
+
if (S.swapGen === S.gen) return;
|
|
1542
1583
|
var p = new URLSearchParams(roleParams({ id: TOKEN }));
|
|
1543
1584
|
var es = new EventSource('api/role-events?' + p.toString());
|
|
1544
1585
|
S.es = es;
|
|
@@ -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
|
// 停在关系图时换视角 = 选中新视角自己那张卡片: 先开它的全部对话 (不留旧选中 / 空白),
|
|
@@ -1982,26 +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
|
-
if (!w) return base;
|
|
1994
|
-
// 未读也取自卡片自己的窗口 (与侧栏同一个 unreadIn): 点开它看得到的才数; 名字旁的是卡片主人整个视角的 (theirsOf)。
|
|
1995
|
-
var mine = unreadIn(w.mine);
|
|
1996
|
-
return { lastTs: w.lastTs, preview: w.preview, unread: mine.length, theirs: theirsOf(t, mine) };
|
|
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 };
|
|
1997
2063
|
};
|
|
1998
2064
|
// 卡片窗口的 glance: 树里画了谁变了就取一次, 世界快照每次轮询也顺带刷新。
|
|
1999
2065
|
var loadGlance = function () {
|
|
2000
2066
|
var keys = W.gKeys, role = ROLE;
|
|
2001
2067
|
if (!keys) return;
|
|
2002
|
-
api('api/glance', { role: role, keys: keys }).then(function (d) {
|
|
2068
|
+
api('api/glance', { role: role, keys: keys, since: BASE }).then(function (d) {
|
|
2003
2069
|
if (!d.ok || role !== ROLE || keys !== W.gKeys) return;
|
|
2004
|
-
W.glance = d.glances || {};
|
|
2070
|
+
W.glance = d.glances || {}; W.gRole = role;
|
|
2005
2071
|
renderWorld();
|
|
2006
2072
|
}).catch(function () { });
|
|
2007
2073
|
};
|
|
@@ -2524,12 +2590,11 @@
|
|
|
2524
2590
|
};
|
|
2525
2591
|
// 三层: 谁 → 对谁 · 何时 / 命中的那一截 / 在哪个会话。
|
|
2526
2592
|
var msgItem = function (h, q) {
|
|
2527
|
-
var to = h.to && h.to !== h.from && h.to !== '
|
|
2528
|
-
? '<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;
|
|
2529
2594
|
return {
|
|
2530
2595
|
kind: 'msg',
|
|
2531
2596
|
html: '<span class="av">' + esc(h.fromLabel) + '</span><span class="b"><span class="l1">' +
|
|
2532
|
-
nm(h.from, h.fromName) +
|
|
2597
|
+
'<span class="said">' + WZ.speakerHead(nm(h.from, h.fromName), to) + '</span>' + skTs(h.ts) + '</span>' +
|
|
2533
2598
|
'<span class="sn">' + hl(h.snippet, q) + '</span>' +
|
|
2534
2599
|
'<span class="l3">' + whereOf(h.convKind, h.convLabel, h.convName) +
|
|
2535
2600
|
(h.hits > 1 ? '<span class="cnt">' + h.hits + ' 处命中</span>' : '') + '</span></span>',
|