dsh-desktop-statusbar 2.0.0 → 2.0.1

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.en.md CHANGED
@@ -27,9 +27,9 @@ Replaces the stats line under the message input in the DSH desktop app with a co
27
27
  | Field | Content |
28
28
  | --- | --- |
29
29
  | Session status | Context occupancy: system prompt / tool definitions / messages |
30
- | Combined hit | Highest / lowest cache hit rate |
31
- | TTFT | Fastest / slowest first-token delay |
32
- | TPS | Fastest / slowest output speed |
30
+ | Combined hit | This turn / highest / lowest cache hit rate |
31
+ | TTFT | This turn / fastest / slowest first-token delay |
32
+ | TPS | This turn / fastest / slowest output speed |
33
33
  | Run time | Model time / tool calls |
34
34
  | This turn cost | Token breakdown for this turn |
35
35
  | Session cost | Cumulative token breakdown for the whole session |
package/README.md CHANGED
@@ -27,9 +27,9 @@
27
27
  | 字段 | 内容 |
28
28
  | --- | --- |
29
29
  | 会话状态 | 上下文占用:系统提示词 / 工具定义 / 对话消息 |
30
- | 综合命中 | 最高 / 最低缓存命中率 |
31
- | 首字平均 | 最快 / 最慢首字延迟 |
32
- | 输出速度 | 最快 / 最慢输出速度 |
30
+ | 综合命中 | 本轮 / 最高 / 最低缓存命中率 |
31
+ | 首字平均 | 本轮 / 最快 / 最慢首字延迟 |
32
+ | 输出速度 | 本轮 / 最快 / 最慢输出速度 |
33
33
  | 运行用时 | 模型用时 / 工具调用 |
34
34
  | 本轮费用 | 本轮会话的 token 明细 |
35
35
  | 总计费用 | 整个会话累计的 token 明细 |
package/docs/settings.png CHANGED
Binary file
package/lib/client.js CHANGED
@@ -40,6 +40,8 @@ window.__ModuleLoader__.load({
40
40
  * v6 起:`segments` 存全部段的有序列表(顺序对整份列表生效),`hidden` 存未勾选的段。
41
41
  */
42
42
  const CONFIG_VERSION = 6;
43
+ /** 插件版本:显示在设置页底部,用来确认页面加载的是哪一版客户端代码。 */
44
+ const VERSION = "2.0.1";
43
45
  /** 官方参考价的版本:官方调价时改这个数字,老价格库会自动并入新参考价。 */
44
46
  const PRICE_VERSION = 2;
45
47
 
@@ -447,12 +449,15 @@ window.__ModuleLoader__.load({
447
449
  f_tokensTurn: "本轮用量",
448
450
  tipModelTime: "模型用时",
449
451
  tipToolTime: "工具调用",
452
+ tipTtftTurn: "本轮平均首字",
450
453
  tipTtftFast: "最快首字延迟",
451
454
  tipTtftSlow: "最慢首字延迟",
455
+ tipTpsTurn: "本轮平均速度",
452
456
  tipTpsFast: "最快输出速度",
453
457
  tipTpsSlow: "最慢输出速度",
454
458
  tipTopUp: "充值余额",
455
459
  tipGranted: "赠金余额",
460
+ tipHitTurn: "本轮缓存命中率",
456
461
  tipHitHigh: "最高缓存命中率",
457
462
  tipHitLow: "最低缓存命中率",
458
463
  contextAria: "上下文已用 {percent}",
@@ -533,12 +538,15 @@ window.__ModuleLoader__.load({
533
538
  f_tokensTurn: "This turn usage",
534
539
  tipModelTime: "Model time",
535
540
  tipToolTime: "Tool calls",
541
+ tipTtftTurn: "Average TTFT this turn",
536
542
  tipTtftFast: "Fastest TTFT",
537
543
  tipTtftSlow: "Slowest TTFT",
544
+ tipTpsTurn: "Average TPS this turn",
538
545
  tipTpsFast: "Fastest TPS",
539
546
  tipTpsSlow: "Slowest TPS",
540
547
  tipTopUp: "Top-up balance",
541
548
  tipGranted: "Granted balance",
549
+ tipHitTurn: "Cache hit rate this turn",
542
550
  tipHitHigh: "Highest cache hit rate",
543
551
  tipHitLow: "Lowest cache hit rate",
544
552
  contextAria: "{percent} of context used",
@@ -999,12 +1007,52 @@ window.__ModuleLoader__.load({
999
1007
  return panel === null ? null : { kind: "tokens", title: title, panel: panel };
1000
1008
  }
1001
1009
 
1002
- /** 毫秒 → 面板里的时长文本;0 或缺数据给横杠。 */
1010
+ /** 毫秒 → 面板里的时长文本(精确到秒);0 或缺数据给横杠。 */
1003
1011
  function tipDuration(ms) {
1004
- const text = formatDuration(ms);
1012
+ const text = formatDurationFine(ms);
1005
1013
  return text === null ? "-" : text;
1006
1014
  }
1007
1015
 
1016
+ /** 时长精确到秒:1h23m45s / 23m45s / 45s。底栏那条用 formatDuration(省掉秒,宽度有限)。 */
1017
+ function formatDurationFine(ms) {
1018
+ const n = Number(ms) || 0;
1019
+ if (n <= 0) return null;
1020
+ const whole = Math.round(n / 1000);
1021
+ if (whole < 60) return whole + "s";
1022
+ const minutes = Math.floor(whole / 60);
1023
+ if (minutes < 60) return minutes + "m" + (whole % 60) + "s";
1024
+ return Math.floor(minutes / 60) + "h" + (minutes % 60) + "m" + (whole % 60) + "s";
1025
+ }
1026
+
1027
+ /** 本轮缓存命中率:这一轮的输入 token 里命中缓存的比例;本轮还没有调用给横杠。
1028
+ 读插件自己的 desktopStatusbarUsage 投影 —— 官方 tokenUsage 只有会话累计值。
1029
+ 注意字段是短名(input / cacheRead / cacheWrite),与官方投影的 *Tokens 不同名。 */
1030
+ function turnHitRate(sessionUsage) {
1031
+ const current = sessionUsage === undefined || sessionUsage === null ? null : sessionUsage.current;
1032
+ if (current === null || current === undefined) return "-";
1033
+ const denominator = (current.input || 0) + (current.cacheRead || 0) + (current.cacheWrite || 0);
1034
+ if (denominator <= 0) return "-";
1035
+ return hitRateText(((current.cacheRead || 0) / denominator) * 100);
1036
+ }
1037
+
1038
+ /** 本轮平均首字;本轮还没有样本给横杠。 */
1039
+ function turnAverageTtft(timing) {
1040
+ if (timing === null || timing === undefined) return "-";
1041
+ const samples = timing.turnTtftSamples || 0;
1042
+ if (samples <= 0) return "-";
1043
+ const text = formatSeconds(timing.turnTtftMs / samples);
1044
+ return text === null ? "-" : text + "s";
1045
+ }
1046
+
1047
+ /** 本轮平均输出速度;本轮还没有可测样本给横杠。 */
1048
+ function turnAverageTps(timing, t) {
1049
+ if (timing === null || timing === undefined) return "-";
1050
+ const decodeMs = timing.turnDecodeMs || 0;
1051
+ const tokens = timing.turnDecodeTokens || 0;
1052
+ if (decodeMs <= 0 || tokens <= 0) return "-";
1053
+ return formatThroughput(tokens / (decodeMs / 1000)) + t("unitSpeed");
1054
+ }
1055
+
1008
1056
  /** 首字极值(毫秒)→ "9.3s";没有样本给横杠。 */
1009
1057
  function tipSeconds(mark) {
1010
1058
  if (mark === null || mark === undefined) return "-";
@@ -1126,6 +1174,7 @@ window.__ModuleLoader__.load({
1126
1174
  kind: "rows",
1127
1175
  title: t("segTtft"),
1128
1176
  rows: [
1177
+ { label: t("tipTtftTurn"), value: turnAverageTtft(timing) },
1129
1178
  { label: t("tipTtftFast"), value: tipSeconds(timing === null ? null : timing.fastestTtft) },
1130
1179
  { label: t("tipTtftSlow"), value: tipSeconds(timing === null ? null : timing.slowestTtft) }
1131
1180
  ]
@@ -1147,6 +1196,7 @@ window.__ModuleLoader__.load({
1147
1196
  kind: "rows",
1148
1197
  title: t("segCacheHit"),
1149
1198
  rows: [
1199
+ { label: t("tipHitTurn"), value: turnHitRate(src.sessionUsage) },
1150
1200
  { label: t("tipHitHigh"), value: extremes === null ? "-" : extremes.high },
1151
1201
  { label: t("tipHitLow"), value: extremes === null ? "-" : extremes.low }
1152
1202
  ]
@@ -1165,6 +1215,7 @@ window.__ModuleLoader__.load({
1165
1215
  kind: "rows",
1166
1216
  title: t("segTps"),
1167
1217
  rows: [
1218
+ { label: t("tipTpsTurn"), value: turnAverageTps(timing, t) },
1168
1219
  { label: t("tipTpsFast"), value: tipSpeed(timing === null ? null : timing.fastestTps, t) },
1169
1220
  { label: t("tipTpsSlow"), value: tipSpeed(timing === null ? null : timing.slowestTps, t) }
1170
1221
  ]
@@ -1475,6 +1526,8 @@ window.__ModuleLoader__.load({
1475
1526
  ".dsb-settings .dsb-action:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1))}",
1476
1527
  ".dsb-settings .dsb-action-sm{height:24px;padding:0 8px;font-size:12px}",
1477
1528
  ".dsb-settings .dsb-foot{display:flex;align-items:center;gap:12px;flex-wrap:wrap}",
1529
+ /* 版本号贴右侧、与按钮底边对齐 */
1530
+ ".dsb-settings .dsb-version{margin-left:auto;align-self:flex-end;font-size:11px;line-height:1;color:var(--dsw-alias-label-tertiary);opacity:.75}",
1478
1531
  ".dsb-settings .dsb-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}"
1479
1532
  ].join("");
1480
1533
 
@@ -1878,21 +1931,38 @@ window.__ModuleLoader__.load({
1878
1931
  const currencyRef = react.useRef(null);
1879
1932
  const editPopRef = react.useRef(null);
1880
1933
 
1881
- /* 编辑浮层比卡片本身高,会被设置页的滚动容器裁掉下半截 —— 打开后滚一下,
1882
- 把它的底边对齐到可视区底部,整块就都看得见了。 */
1934
+ /* 编辑浮层打开后按需滚动:block:"nearest" 只补上被遮住的那一段,
1935
+ 本来就在视野里就一动不动(旧版用 "end" 会平白往上滚,明明下面有地方)。 */
1883
1936
  react.useEffect(() => {
1884
1937
  if (editing === null) return;
1885
1938
  const node = editPopRef.current;
1886
1939
  if (node === null || node === undefined || typeof node.scrollIntoView !== "function") return;
1887
- node.scrollIntoView({ block: "end", behavior: "smooth" });
1940
+ node.scrollIntoView({ block: "nearest", behavior: "smooth" });
1888
1941
  }, [editing]);
1889
1942
 
1890
- /* 编辑浮层:点别处就收起,草稿一律丢弃 —— 改了一半的数字、新模型里填的空值都不留下 */
1943
+ /* 编辑浮层:点别处收起并丢弃草稿。判断"里面"上三重保险 —— 浮层节点、价格卡节点,
1944
+ 以及鼠标坐标是否落在卡片或币种下拉的矩形内。切币种时点在卡片内的控件上,
1945
+ 无论 DOM 结构怎么变都不该把编辑区收掉。 */
1891
1946
  react.useEffect(() => {
1892
1947
  if (editing === null) return undefined;
1893
- const onDocClick = (event) => {
1948
+ const inside = (event) => {
1949
+ const target = event.target;
1950
+ if (target !== null && target !== undefined && typeof target.closest === "function"
1951
+ && target.closest(".dsb-model") !== null) return true;
1894
1952
  const node = editPopRef.current;
1895
- if (node !== null && node !== undefined && typeof node.contains === "function" && node.contains(event.target)) return;
1953
+ if (node !== null && node !== undefined && typeof node.contains === "function" && node.contains(target)) return true;
1954
+ const x = event.clientX;
1955
+ const y = event.clientY;
1956
+ if (typeof x !== "number" || typeof y !== "number") return false;
1957
+ return [node, currencyRef.current].some((one) => {
1958
+ if (one === null || one === undefined || typeof one.getBoundingClientRect !== "function") return false;
1959
+ const box = one.getBoundingClientRect();
1960
+ return box.width > 0 && box.height > 0
1961
+ && x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
1962
+ });
1963
+ };
1964
+ const onDocClick = (event) => {
1965
+ if (inside(event)) return;
1896
1966
  cancelEdit();
1897
1967
  };
1898
1968
  document.addEventListener("click", onDocClick);
@@ -2280,7 +2350,9 @@ window.__ModuleLoader__.load({
2280
2350
  modelOrder: Object.keys(DEFAULT_PRICES)
2281
2351
  })
2282
2352
  }, t("reset")),
2283
- h("div", { className: "dsb-actions", key: "__actions" }, holidayActions)
2353
+ h("div", { className: "dsb-actions", key: "__actions" }, holidayActions),
2354
+ /* 版本标记贴右、与行内底边对齐:改了客户端代码要刷新页面才生效,这行字让"跑的是哪版"一眼可见 */
2355
+ h("span", { className: "dsb-version", key: "__version" }, "dsh-desktop-statusbar " + VERSION)
2284
2356
  ]));
2285
2357
 
2286
2358
  return h("div", { className: "dsb-settings" }, sections);
package/lib/index.js CHANGED
@@ -340,6 +340,12 @@ const timingViewSchema = z.object({
340
340
  slowestTtft: timingMark.nullable(),
341
341
  fastestTps: timingMark.nullable(),
342
342
  slowestTps: timingMark.nullable(),
343
+ /* 本轮累计:气泡里"本轮平均首字 / 本轮平均速度"要用(官方投影只有会话累计值) */
344
+ turn: z.number().nullable(),
345
+ turnTtftMs: z.number().nonnegative(),
346
+ turnTtftSamples: z.number().int().nonnegative(),
347
+ turnDecodeMs: z.number().nonnegative(),
348
+ turnDecodeTokens: z.number().nonnegative(),
343
349
  }).strict()
344
350
 
345
351
  const sessionTimingSchema = timingViewSchema.extend({ openStep: timingStep.nullable() }).strict()
@@ -417,12 +423,25 @@ const sessionTimingProjection = {
417
423
  slowestTtft: null,
418
424
  fastestTps: null,
419
425
  slowestTps: null,
426
+ turn: null,
427
+ turnTtftMs: 0,
428
+ turnTtftSamples: 0,
429
+ turnDecodeMs: 0,
430
+ turnDecodeTokens: 0,
420
431
  openStep: null,
421
432
  }),
422
433
  apply: (state, event) => {
423
434
  if (event.type === 'step/start') {
435
+ /* 换轮就把本轮累计清零:气泡里的"本轮平均…"只算当前这一轮 */
436
+ const turn = event.data.turn
437
+ const sameTurn = state.turn === turn
424
438
  return {
425
439
  ...state,
440
+ turn: turn,
441
+ turnTtftMs: sameTurn ? state.turnTtftMs : 0,
442
+ turnTtftSamples: sameTurn ? state.turnTtftSamples : 0,
443
+ turnDecodeMs: sameTurn ? state.turnDecodeMs : 0,
444
+ turnDecodeTokens: sameTurn ? state.turnDecodeTokens : 0,
426
445
  openStep: {
427
446
  turn: event.data.turn, step: event.data.step, startTime: event.time,
428
447
  firstTokenTime: null, firstTokenTool: false,
@@ -451,6 +470,8 @@ const sessionTimingProjection = {
451
470
  next.samples = state.samples + 1
452
471
  next.fastestTtft = pickSmaller(state.fastestTtft, { value: ttft, at: event.time, model })
453
472
  next.slowestTtft = pickLarger(state.slowestTtft, { value: ttft, at: event.time, model })
473
+ next.turnTtftMs = state.turnTtftMs + ttft
474
+ next.turnTtftSamples = state.turnTtftSamples + 1
454
475
  const output = stepOutputTokens(event.data.usage)
455
476
  const decode = Math.max(0, event.time - first.time)
456
477
  /* 纯工具调用的步骤不进速度极值:分片时间戳挤在同一刻,算出来是假值(首字照旧统计) */
@@ -458,6 +479,8 @@ const sessionTimingProjection = {
458
479
  const speed = { value: output / (decode / 1000), at: event.time, model }
459
480
  next.fastestTps = pickLarger(state.fastestTps, speed)
460
481
  next.slowestTps = pickSmaller(state.slowestTps, speed)
482
+ next.turnDecodeMs = state.turnDecodeMs + decode
483
+ next.turnDecodeTokens = state.turnDecodeTokens + output
461
484
  }
462
485
  return next
463
486
  }
@@ -475,9 +498,14 @@ const sessionTimingProjection = {
475
498
  slowestTtft: state.slowestTtft,
476
499
  fastestTps: state.fastestTps,
477
500
  slowestTps: state.slowestTps,
501
+ turn: state.turn,
502
+ turnTtftMs: state.turnTtftMs,
503
+ turnTtftSamples: state.turnTtftSamples,
504
+ turnDecodeMs: state.turnDecodeMs,
505
+ turnDecodeTokens: state.turnDecodeTokens,
478
506
  }),
479
507
  },
480
- stateVersion: 2,
508
+ stateVersion: 3,
481
509
  }
482
510
 
483
511
  /* ---------------------------------------------------------------- 余额查询 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-desktop-statusbar",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "description": "把 DSH 桌面端对话区底部的统计行换成可配置状态栏:9 个字段可选可排序,按官方峰谷口径估算费用,支持自定义模型单价与账户余额。",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",