wezard 2.0.1 → 2.0.2
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 +52 -26
- 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 +10 -7
- 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 +59 -31
- package/web/chat.js +112 -60
package/web/chat.css
CHANGED
|
@@ -18,14 +18,18 @@
|
|
|
18
18
|
--pub: #237a55; /* 公开 */
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
html
|
|
21
|
+
html { height: 100% }
|
|
22
22
|
/* overscroll-behavior:none 阻止整页橡皮筋回弹 —— webview 里它会和内层
|
|
23
23
|
.thread 抢滚动手势, 表现为"滚一下卡住/整页在弹"。 */
|
|
24
|
-
body
|
|
24
|
+
/* body 是一根竖轴: 上面 .app (两栏), 下面整页页脚 (wizard 的用量条)。 */
|
|
25
|
+
body {
|
|
26
|
+
height: 100vh; height: 100dvh; display: flex; flex-direction: column;
|
|
27
|
+
overflow: hidden; overscroll-behavior: none; color: var(--ink);
|
|
28
|
+
}
|
|
25
29
|
button { font: inherit; color: inherit }
|
|
26
30
|
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px }
|
|
27
31
|
|
|
28
|
-
.app { display: flex;
|
|
32
|
+
.app { display: flex; flex: 1; min-height: 0 }
|
|
29
33
|
@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .3 } }
|
|
30
34
|
|
|
31
35
|
/* 名字 = 地址。点是地址的一部分, 也是这一页唯一的颜色记号。 */
|
|
@@ -49,6 +53,16 @@ button { font: inherit; color: inherit }
|
|
|
49
53
|
.rolebar .id .l { min-width: 0; display: flex; flex-direction: column; gap: 2px }
|
|
50
54
|
.rolebar .id .nm { font-size: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
51
55
|
.rolebar .id .k { font-size: 11.5px; color: var(--mute) }
|
|
56
|
+
/* 名片上的状态: 只有执行中带颜色与呼吸, 空闲 / 已关闭是一个安静的词。 */
|
|
57
|
+
.rolebar .st { margin-left: 8px; white-space: nowrap }
|
|
58
|
+
.rolebar .st::before {
|
|
59
|
+
content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 4px;
|
|
60
|
+
border-radius: 50%; background: #b1bac4; vertical-align: 1px;
|
|
61
|
+
}
|
|
62
|
+
.rolebar .st.run { color: #1a7f37 }
|
|
63
|
+
.rolebar .st.run::before { background: #1a7f37; animation: pulse 1.4s infinite }
|
|
64
|
+
.rolebar .st.off { color: var(--faint) }
|
|
65
|
+
.rolebar .st.off::before { background: transparent; box-shadow: inset 0 0 0 1.5px #b1bac4 }
|
|
52
66
|
.rolebar .job { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--ink); opacity: .8 }
|
|
53
67
|
.rolebar .facts { display: flex; gap: 6px 12px; flex-wrap: wrap; font-size: 11.5px; color: var(--mute) }
|
|
54
68
|
.rolebar .facts span { white-space: nowrap }
|
|
@@ -99,36 +113,35 @@ button { font: inherit; color: inherit }
|
|
|
99
113
|
.sp-it.all { border-bottom: 1px solid var(--line); border-radius: 6px 6px 0 0; margin-bottom: 3px }
|
|
100
114
|
.sp-it.all .id { font-family: inherit }
|
|
101
115
|
|
|
102
|
-
/* 左栏底: session 切换
|
|
103
|
-
.sidefoot {
|
|
104
|
-
flex: none; border-top: 1px solid var(--line); padding: 10px 12px 12px;
|
|
105
|
-
display: flex; flex-direction: column; gap: 8px; background: var(--paper);
|
|
106
|
-
}
|
|
107
|
-
.sidefoot[hidden] { display: none }
|
|
108
|
-
#rb-sess:empty, #rb-prof:empty { display: none }
|
|
116
|
+
/* 左栏底: session 切换 —— 轮次的事, 与顶上的身份分开, 钉在会话列表下面。 */
|
|
117
|
+
.sidefoot { flex: none; border-top: 1px solid var(--line); padding: 10px 12px; background: var(--paper) }
|
|
109
118
|
/* 在底部, 浮层往上开。 */
|
|
110
119
|
.sidefoot .sp-list { top: auto; bottom: calc(100% + 4px); transform: translateY(4px) }
|
|
111
120
|
.sidefoot .sp.open .sp-list { transform: none }
|
|
112
121
|
|
|
113
|
-
/*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
.
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
.
|
|
129
|
-
.
|
|
130
|
-
.
|
|
131
|
-
.
|
|
122
|
+
/* ── 用量条: 一组轮次的总账 ─────────────────────────────────────────
|
|
123
|
+
一个组件两处挂: 整页页脚 (wizard 的总账) / 聊天窗口底 (人与某个 wizard 这段往来的账)。
|
|
124
|
+
宽屏一行排完: 标签 · 指标 · I/O 条 + 图例 · 模型。每一组自身不折行, 一行排不下时
|
|
125
|
+
整组落到下一行 —— 窄屏自然变成两三行, 不需要另一套结构。 */
|
|
126
|
+
.usage {
|
|
127
|
+
flex: none; display: flex; align-items: center; flex-wrap: wrap; gap: 6px 20px;
|
|
128
|
+
padding: 7px 20px; border-top: 1px solid var(--line); background: var(--paper);
|
|
129
|
+
font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--mute);
|
|
130
|
+
font-variant-numeric: tabular-nums;
|
|
131
|
+
}
|
|
132
|
+
.usage b { font-weight: 700; color: var(--ink) }
|
|
133
|
+
.usage .u-lb { font: 10px system-ui, sans-serif; color: var(--faint); letter-spacing: .4px }
|
|
134
|
+
.usage .u-kvs { display: flex; gap: 4px 16px }
|
|
135
|
+
.usage .kv { white-space: nowrap; font-size: 9.5px; color: var(--faint); text-transform: uppercase; letter-spacing: .4px }
|
|
136
|
+
.usage .kv b { font-size: 12.5px; margin-right: 4px; letter-spacing: 0; text-transform: none }
|
|
137
|
+
.usage .u-io { display: flex; align-items: center; gap: 12px; min-width: 0 }
|
|
138
|
+
.usage .bar { flex: none; width: 140px; display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--line) }
|
|
139
|
+
.usage .bar .seg { height: 100% }
|
|
140
|
+
.usage .leg { display: flex; gap: 4px 12px }
|
|
141
|
+
.usage .lg { display: flex; align-items: center; gap: 4px; white-space: nowrap }
|
|
142
|
+
.usage .lg i { width: 8px; height: 8px; border-radius: 2px; flex: none }
|
|
143
|
+
.usage .lg b { margin-left: 1px }
|
|
144
|
+
.usage .u-model { margin-left: auto; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
132
145
|
|
|
133
146
|
/* 群聊头像 = 参与者头像拼图: 1 格居中, 2 格左右, 3/4 格田字。 */
|
|
134
147
|
.mosaic {
|
|
@@ -148,7 +161,6 @@ button { font: inherit; color: inherit }
|
|
|
148
161
|
display: flex; align-items: baseline; gap: 6px;
|
|
149
162
|
}
|
|
150
163
|
.convs h2 span { font-weight: 400; color: var(--faint); font-variant-numeric: tabular-nums }
|
|
151
|
-
.convs .none { padding: 6px 10px; font-size: 12px; color: var(--faint) }
|
|
152
164
|
|
|
153
165
|
.ci {
|
|
154
166
|
display: flex; gap: 10px; align-items: flex-start; width: 100%; text-align: left; overflow: hidden;
|
|
@@ -192,7 +204,15 @@ button { font: inherit; color: inherit }
|
|
|
192
204
|
.si .ts { margin-left: auto; flex: none; font-size: 10.5px; color: var(--faint); font-variant-numeric: tabular-nums }
|
|
193
205
|
.si .pv { display: block; font-size: 11.5px; color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
194
206
|
.si .pv:empty { display: none }
|
|
195
|
-
|
|
207
|
+
|
|
208
|
+
/* 执行中的 wizard: 头像角上一盏呼吸灯 (同关系图的 .wav .live)。 */
|
|
209
|
+
.ci > .av, .si > .av { position: relative }
|
|
210
|
+
.ci > .av .live, .si > .av .live {
|
|
211
|
+
position: absolute; right: 1px; bottom: -3px; width: 9px; height: 9px;
|
|
212
|
+
border-radius: 50%; background: #1a7f37; border: 2px solid #fff;
|
|
213
|
+
animation: pulse 1.4s infinite;
|
|
214
|
+
}
|
|
215
|
+
.si > .av .live { right: -1px; width: 8px; height: 8px }
|
|
196
216
|
|
|
197
217
|
/* ── 右栏头: 这个群聊 / 私聊是什么 ───────────────────────────────── */
|
|
198
218
|
.main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--card) }
|
|
@@ -271,6 +291,9 @@ button { font: inherit; color: inherit }
|
|
|
271
291
|
.mrow.mine .mb .md-body pre code { background: transparent }
|
|
272
292
|
.mrow.mine .mb .bubble details, .mrow.mine .mb .tg-head { border-color: var(--mine-line) }
|
|
273
293
|
.mrow.mine .mb .tool-body { border-left-color: var(--mine-line) }
|
|
294
|
+
/* 工具正文是展开时才取的 (chat.js loadTool) —— 空壳期间说一句在干什么。 */
|
|
295
|
+
.tool-body[data-lazy-turn]:empty::before { content: "加载中…"; color: #8c959f; font-size: 12px }
|
|
296
|
+
.tool-body[data-lazy-turn][data-err]:empty::before { content: "加载失败 · 收起再展开重试"; color: #cf222e }
|
|
274
297
|
.mrow.mine .mb .io-bar { background: var(--mine-soft) }
|
|
275
298
|
|
|
276
299
|
/* 气泡对面那片空白 = 换视角的门。平时不可见, 悬停才露出要去谁那里。 */
|
|
@@ -376,6 +399,11 @@ button { font: inherit; color: inherit }
|
|
|
376
399
|
.flip { min-width: 16px; padding: 0 4px; font-size: 0 }
|
|
377
400
|
.turn-group { padding: 12px }
|
|
378
401
|
.thread { padding-bottom: calc(12px + env(safe-area-inset-bottom)) }
|
|
402
|
+
.usage { padding: 8px 12px; gap: 6px 14px }
|
|
403
|
+
.usage .u-kvs, .usage .leg { flex-wrap: wrap }
|
|
404
|
+
.usage .u-io { flex: 1 1 100%; flex-wrap: wrap; gap: 6px 12px }
|
|
405
|
+
.usage .bar { flex: 1 1 100%; width: auto }
|
|
406
|
+
.usage.page { padding-bottom: calc(8px + env(safe-area-inset-bottom)) }
|
|
379
407
|
}
|
|
380
408
|
|
|
381
409
|
/* UA 的 [hidden] 规则和 `.gbar{display:flex}` 这类类选择器同权重, 而作者样式
|
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 = 边类型开关。
|
|
@@ -207,10 +207,35 @@
|
|
|
207
207
|
var d = document.createElement('div'); d.innerHTML = html; return d;
|
|
208
208
|
};
|
|
209
209
|
|
|
210
|
+
// ── 工具调用正文: 第二级数据, 展开那一刻才取 ──
|
|
211
|
+
// 片段里的 .tool-body 是个空壳 (data-lazy-turn / data-lazy-use), 首屏只带摘要行。
|
|
212
|
+
// toggle 不冒泡, 所以在线程根上用捕获听 —— 用户点开, 和 reconcile 换了新节点后
|
|
213
|
+
// restoreOpen 回填展开态, 走的是同一个事件: 后者正好把「展开着等结果」的那次调用
|
|
214
|
+
// 重取成带结果的正文。取到之后正文就留在节点上, 收起再展开不会重取。
|
|
215
|
+
var loadTool = function (d) {
|
|
216
|
+
var body = childBy(d, 'tool-body');
|
|
217
|
+
if (!body || !body.hasAttribute('data-lazy-turn') || body._busy || body.firstChild) return;
|
|
218
|
+
body._busy = true; body.removeAttribute('data-err');
|
|
219
|
+
api('api/tool', { turn: body.getAttribute('data-lazy-turn'), use: body.getAttribute('data-lazy-use') })
|
|
220
|
+
.then(function (r) {
|
|
221
|
+
if (!r.ok) throw new Error(r.error);
|
|
222
|
+
var stick = S.pinned;
|
|
223
|
+
body.innerHTML = r.html;
|
|
224
|
+
if (stick) toBottom(true);
|
|
225
|
+
})
|
|
226
|
+
// 失败留着空壳 (CSS 给出提示), 再展开一次就是重试。
|
|
227
|
+
.catch(function () { body.setAttribute('data-err', '1'); })
|
|
228
|
+
.then(function () { body._busy = false; });
|
|
229
|
+
};
|
|
230
|
+
|
|
210
231
|
// ── 滚动: 默认到底; 用户手动上翻后解除吸附, 回到底部再吸附 ──
|
|
211
232
|
var atBottom = function () { return thread.scrollHeight - thread.scrollTop - thread.clientHeight < 80; };
|
|
212
233
|
var toBottom = function (force) { if (force || S.pinned) thread.scrollTop = thread.scrollHeight; };
|
|
213
234
|
thread.addEventListener('scroll', function () { S.pinned = atBottom(); });
|
|
235
|
+
inner.addEventListener('toggle', function (e) {
|
|
236
|
+
var d = e.target;
|
|
237
|
+
if (d.open && d.classList && d.classList.contains('tool-call')) loadTool(d);
|
|
238
|
+
}, true);
|
|
214
239
|
|
|
215
240
|
// ── 左栏: 当前 role 的名片 + 会话列表 ──
|
|
216
241
|
var convOf = function (key) { return R.convs.filter(function (c) { return c.key === key; })[0]; };
|
|
@@ -223,8 +248,24 @@
|
|
|
223
248
|
});
|
|
224
249
|
return out;
|
|
225
250
|
};
|
|
226
|
-
|
|
227
|
-
|
|
251
|
+
// ── 状态: wizard 是否在执行中 ──
|
|
252
|
+
// 服务端只给事实 (busy / alive / runningUntil), 亮不亮在这里按本地时钟判 ——
|
|
253
|
+
// 到点没有新写入就自己熄 (同 expireRows), 不必等下一次推送。
|
|
254
|
+
var ST = { run: '执行中', idle: '空闲', off: '已关闭' };
|
|
255
|
+
var stateOf = function (s) { return wRunning(s) ? 'run' : s.alive ? 'idle' : 'off'; };
|
|
256
|
+
// 侧栏只在执行中时点一盏灯: 列表里安静是常态, 亮着的才值得看一眼。
|
|
257
|
+
var lampOf = function (s) {
|
|
258
|
+
return s && wRunning(s) ? '<i class="live" title="' + ST.run + '"></i>' : '';
|
|
259
|
+
};
|
|
260
|
+
var paintStatus = function () {
|
|
261
|
+
var el = $('#rb-st'), r = R.role;
|
|
262
|
+
if (!el || !r) return;
|
|
263
|
+
var k = stateOf(r);
|
|
264
|
+
el.className = 'st ' + k; el.textContent = ST[k];
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
var avatarOf = function (c, extra) {
|
|
268
|
+
if (c.kind !== 'group') return '<span class="av">' + esc(c.label) + (extra || '') + '</span>';
|
|
228
269
|
var ms = membersOf(c).slice(0, 4);
|
|
229
270
|
return '<span class="av mosaic n' + ms.length + '" aria-hidden="true">' +
|
|
230
271
|
ms.map(function (m) { return '<i>' + esc(m.label || roleLabel(m.role)) + '</i>'; }).join('') + '</span>';
|
|
@@ -247,31 +288,34 @@
|
|
|
247
288
|
var convItem = function (c) {
|
|
248
289
|
var on = c.key === CONV;
|
|
249
290
|
var title = c.kind === 'wizard' ? nm(c.peer, c.name) : '<span class="nm chat">' + esc(c.name) + '</span>';
|
|
250
|
-
|
|
251
|
-
|
|
291
|
+
// 只列与我有往来的: 在群里但没和我说过话的人, 点进去也是空的。
|
|
292
|
+
var talked = on ? c.subs.filter(function (s) { return s.count; }) : [];
|
|
293
|
+
var subs = talked.length
|
|
294
|
+
? '<div class="subs">' + talked.map(function (s) {
|
|
252
295
|
var sel = s.role === WITH;
|
|
253
|
-
return '<button class="si' + (sel ? ' on' : '') +
|
|
254
|
-
'title="' + esc(
|
|
255
|
-
'<span class="av">' + esc(s.label) + '</span>' +
|
|
296
|
+
return '<button class="si' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '" data-with="' + esc(s.role) + '" ' +
|
|
297
|
+
'title="' + esc('我与 ' + nameOf(s.role) + ' 在这里的 ' + s.count + ' 条往来') + '">' +
|
|
298
|
+
'<span class="av">' + esc(s.label) + lampOf(s.status) + '</span>' +
|
|
256
299
|
line(nm(s.role, s.name), sel ? swapOf(s.role) : '', s.lastTs, s.preview) + '</button>';
|
|
257
300
|
}).join('') + '</div>'
|
|
258
301
|
: '';
|
|
259
302
|
var sel = on && !WITH;
|
|
260
303
|
return '<button class="ci' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '">' +
|
|
261
|
-
avatarOf(c) + line(title, sel && c.kind === 'wizard' ? swapOf(c.peer) : '', c.lastTs, c.preview) +
|
|
304
|
+
avatarOf(c, lampOf(c.status)) + line(title, sel && c.kind === 'wizard' ? swapOf(c.peer) : '', c.lastTs, c.preview) +
|
|
262
305
|
'</button>' + subs;
|
|
263
306
|
};
|
|
264
307
|
|
|
265
308
|
var renderConvs = function () {
|
|
266
309
|
var groups = R.convs.filter(function (c) { return c.kind === 'group'; });
|
|
267
310
|
var dms = R.convs.filter(function (c) { return c.kind !== 'group'; });
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
311
|
+
// 空着的那一栏不画 —— 对谁都一样: 人没有私聊只是这条规则的一个特例。
|
|
312
|
+
var sec = function (title, list) {
|
|
313
|
+
return list.length ? '<h2>' + title + '<span>' + list.length + '</span></h2>' + list.map(convItem).join('') : '';
|
|
271
314
|
};
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
315
|
+
// 没变就不碰 DOM: 心跳每 3s 来一次, 重建会把列表的滚动与焦点蹭掉。
|
|
316
|
+
var html = sec('群聊', groups) + sec('私聊', dms);
|
|
317
|
+
if (convsEl._html === html) return;
|
|
318
|
+
convsEl._html = html; convsEl.innerHTML = html;
|
|
275
319
|
convsEl.querySelectorAll('[data-conv]').forEach(function (b) {
|
|
276
320
|
b.onclick = function () { selectConv(b.getAttribute('data-conv'), b.getAttribute('data-with') || ''); };
|
|
277
321
|
});
|
|
@@ -358,15 +402,16 @@
|
|
|
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>');
|
|
@@ -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
|
// ── 右栏头: 这个群聊 / 私聊是什么 (关系 / 日程视图时是视图名) ──
|
|
@@ -625,7 +674,8 @@
|
|
|
625
674
|
var applyRole = function (d) {
|
|
626
675
|
R.at = d.at || Date.now(); R.recvAt = Date.now();
|
|
627
676
|
R.role = d.role; R.sessions = d.sessions || []; R.convs = d.convs || [];
|
|
628
|
-
R.relations = !!d.relations; R.schedules = d.schedules || 0;
|
|
677
|
+
R.relations = !!d.relations; R.schedules = d.schedules || 0;
|
|
678
|
+
R.stats = d.stats || null; R.winStats = d.winStats || null;
|
|
629
679
|
ROLE = d.role.id;
|
|
630
680
|
learn(d.role.id, d.role.name, d.role.label);
|
|
631
681
|
R.convs.forEach(function (c) {
|
|
@@ -633,7 +683,7 @@
|
|
|
633
683
|
c.subs.forEach(function (s) { learn(s.role, s.name, s.label); });
|
|
634
684
|
});
|
|
635
685
|
if (!CONV || !convOf(CONV)) { CONV = d.conv || ''; WITH = ''; }
|
|
636
|
-
renderRole();
|
|
686
|
+
renderRole(); renderUsage(); renderConvs(); renderHead();
|
|
637
687
|
};
|
|
638
688
|
|
|
639
689
|
// ── SSE: role 摘要 + 当前窗口的消息增量。断线指数退避重连。 ──
|
|
@@ -674,7 +724,9 @@
|
|
|
674
724
|
CONV = key; WITH = withRole || '';
|
|
675
725
|
if (VIEW !== 'msgs') setView('msgs');
|
|
676
726
|
app.classList.add('reading');
|
|
677
|
-
|
|
727
|
+
// 上一个窗口的账不属于这个窗口 —— 先收起, 新的随 SSE 的首个 role 事件到。
|
|
728
|
+
R.winStats = null;
|
|
729
|
+
renderConvs(); renderHead(); renderUsage();
|
|
678
730
|
S.gen++;
|
|
679
731
|
syncUrl();
|
|
680
732
|
inner.innerHTML = '<div class="empty">加载中…</div>';
|
|
@@ -1463,7 +1515,7 @@
|
|
|
1463
1515
|
app.classList.toggle('outer', v !== 'msgs');
|
|
1464
1516
|
// 手机上关系/日程占满主区 —— 进入即阅读态。
|
|
1465
1517
|
if (v !== 'msgs') app.classList.add('reading');
|
|
1466
|
-
renderRole(); renderHead();
|
|
1518
|
+
renderRole(); renderHead(); renderUsage();
|
|
1467
1519
|
if (v === 'msgs') { fxStop(); toBottom(true); }
|
|
1468
1520
|
else {
|
|
1469
1521
|
// 关系图一打开就聚焦在当前 role 身上: 它的邻居亮着, 其余压暗。
|
|
@@ -1484,13 +1536,13 @@
|
|
|
1484
1536
|
// 本地心跳: 相对时间、运行中判定、耗时都随时间变化, 但服务端没有新事件可推。
|
|
1485
1537
|
setInterval(function () {
|
|
1486
1538
|
if (!R.role) return;
|
|
1487
|
-
|
|
1539
|
+
renderUsage();
|
|
1488
1540
|
expireRows();
|
|
1541
|
+
// 侧栏的「几分钟前」与状态灯: 内容没变时 renderConvs 不碰 DOM。
|
|
1542
|
+
paintStatus(); renderConvs();
|
|
1489
1543
|
if (VIEW === 'world') tickWorld();
|
|
1490
1544
|
else if (VIEW === 'plan') renderPlan();
|
|
1491
1545
|
}, TICK_MS);
|
|
1492
|
-
// 侧栏的「几分钟前」一分钟刷一次就够 —— 每 3s 重建会把列表的滚动与焦点蹭掉。
|
|
1493
|
-
setInterval(function () { if (R.role && VIEW === 'msgs') renderConvs(); }, 60000);
|
|
1494
1546
|
|
|
1495
1547
|
// ── boot ──
|
|
1496
1548
|
// 带着 role / conv 来的链接直接进阅读态 (手机上不先落在会话列表)。
|