dsh-dock 0.11.2 → 0.11.3

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.
Files changed (38) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +242 -396
  3. package/client.js +173 -9
  4. package/cordis.patch.yml +2 -2
  5. package/features/animation/host.js +119 -119
  6. package/features/animation/view.jsx +1662 -1662
  7. package/features/balance/host.js +494 -494
  8. package/features/balance/view.js +351 -351
  9. package/features/games/games/breakout.jsx +227 -227
  10. package/features/games/games/gomoku.jsx +244 -244
  11. package/features/games/games/racer.jsx +169 -169
  12. package/features/games/games/shared.jsx +118 -118
  13. package/features/games/games/snake.jsx +144 -144
  14. package/features/games/games/sokoban.jsx +180 -180
  15. package/features/games/games/tank.jsx +128 -128
  16. package/features/games/games/tetris.jsx +308 -308
  17. package/features/games/games/xiangqi.jsx +277 -277
  18. package/features/games/tank-core.js +152 -152
  19. package/features/games/view.jsx +639 -639
  20. package/features/games/xiangqi-core.js +299 -299
  21. package/features/heartbeat/view.js +35 -35
  22. package/features/mobile-relay/gateway.js +472 -472
  23. package/features/mobile-relay/host.js +570 -570
  24. package/features/mobile-relay/view.jsx +300 -300
  25. package/features/modelconfig/host.js +406 -406
  26. package/features/modelconfig/view.js +490 -490
  27. package/features/notify/host.js +374 -374
  28. package/features/notify/view.jsx +751 -751
  29. package/features/theme/view.js +61 -61
  30. package/features/tokenlog/host.js +882 -883
  31. package/features/tokenlog/view.jsx +1117 -932
  32. package/features/visionproxy/host.js +315 -315
  33. package/index.js +195 -195
  34. package/package.json +66 -64
  35. package/src/client.jsx +677 -677
  36. package/src/host-core.js +207 -207
  37. package/src/shared.js +192 -192
  38. package/src/task-track.js +354 -359
package/client.js CHANGED
@@ -2342,6 +2342,55 @@ function fmtTime(ts) {
2342
2342
  const p = (x) => String(x).padStart(2, "0");
2343
2343
  return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) + " " + p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
2344
2344
  }
2345
+ function fmtCostCnyExact(usd, rate) {
2346
+ const v = (Number(usd) || 0) * (Number(rate) > 0 ? Number(rate) : 7.2);
2347
+ return "\xA5" + v.toFixed(4);
2348
+ }
2349
+ var EXPORT_COLUMNS = [
2350
+ ["\u65F6\u95F4", (r) => fmtTime(r.time)],
2351
+ ["\u4F1A\u8BDDID", (r) => r.sessionId || ""],
2352
+ ["\u63D0\u4F9B\u5546", (r) => r.provider || "\u2014"],
2353
+ ["\u6A21\u578B", (r) => r.model || "\u2014"],
2354
+ ["\u8F93\u5165(\u547D\u4E2D)", (r) => fmtNum(r.cacheReadTokens)],
2355
+ ["\u8F93\u5165(\u672A\u547D\u4E2D)", (r) => fmtNum(r.inputTokens)],
2356
+ ["\u547D\u4E2D%", (r) => (Number(r.cacheHitPercent) || 0) + "%"],
2357
+ ["\u8F93\u51FA", (r) => fmtNum(r.outputTokens)],
2358
+ ["\u63A8\u7406", (r) => fmtNum(r.reasoningTokens)],
2359
+ ["\u603B\u989D", (r) => fmtNum(r.totalTokens)],
2360
+ ["\u91D1\u989D\uFF08\u4EBA\u6C11\u5E01\uFF09", (r, rate) => fmtCostCnyExact(r.cost, rate)],
2361
+ ["\u5F3A\u5EA6", (r) => r.effort || "\u2014"],
2362
+ ["\u72B6\u6001", (r) => statusInfo(r).label],
2363
+ ["\u8017\u65F6", (r) => fmtDuration(r.llmMs)]
2364
+ ];
2365
+ function csvCell(v) {
2366
+ const s = String(v === void 0 || v === null ? "" : v);
2367
+ return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
2368
+ }
2369
+ function buildExportCsv(rows, rate) {
2370
+ const list = Array.isArray(rows) ? rows : [];
2371
+ const lines = [EXPORT_COLUMNS.map(([title]) => csvCell(title)).join(",")];
2372
+ for (const r of list) lines.push(EXPORT_COLUMNS.map(([, get]) => csvCell(get(r, rate))).join(","));
2373
+ if (list.length) {
2374
+ const sum = (k) => list.reduce((a, r) => a + (Number(r[k]) || 0), 0);
2375
+ const totalInput = sum("inputTokens") + sum("cacheReadTokens");
2376
+ const hitPct = totalInput > 0 ? Math.round(sum("cacheReadTokens") / totalInput * 100) : 0;
2377
+ const totals = {
2378
+ "\u8C03\u7528\u6B21\u6570": String(list.length),
2379
+ "\u8F93\u5165(\u547D\u4E2D)": fmtNum(sum("cacheReadTokens")),
2380
+ "\u8F93\u5165(\u672A\u547D\u4E2D)": fmtNum(sum("inputTokens")),
2381
+ "\u547D\u4E2D%": hitPct + "%",
2382
+ "\u8F93\u51FA": fmtNum(sum("outputTokens")),
2383
+ "\u63A8\u7406": fmtNum(sum("reasoningTokens")),
2384
+ "\u603B\u989D": fmtNum(sum("totalTokens")),
2385
+ "\u91D1\u989D\uFF08\u4EBA\u6C11\u5E01\uFF09": fmtCostCnyExact(sum("cost"), rate)
2386
+ };
2387
+ lines.push(EXPORT_COLUMNS.map(([title]) => {
2388
+ if (title === "\u63D0\u4F9B\u5546") return csvCell("\u5408\u8BA1(" + list.length + " \u6761\u8C03\u7528)");
2389
+ return csvCell(totals[title] === void 0 ? "" : totals[title]);
2390
+ }).join(","));
2391
+ }
2392
+ return lines.join("\r\n");
2393
+ }
2345
2394
  var STORE_KEY = "dsh-dock/tokenlog/filters/v1";
2346
2395
  var FILTER_FIELDS = ["fromStr", "toStr", "provider", "model", "status", "effort", "sessionId", "dim"];
2347
2396
  function loadSavedFilters() {
@@ -2425,6 +2474,58 @@ var css = `
2425
2474
  .dtok-pager.top{margin:0;}
2426
2475
  .dtok-pager.bottom{margin:0;}
2427
2476
  .dtok-err{color:var(--dsw-alias-state-error-primary,#ff7a7a);font-size:12px;}
2477
+ /* ---- \u67E5\u8BE2\u52A0\u8F7D\u52A8\u753B ----
2478
+ \u6570\u636E\u91CF\u5927\u65F6\u300C\u67E5\u8BE2\u300D\u8981\u7B49\u597D\u51E0\u79D2\uFF0C\u65E7\u7248\u53EA\u6709\u4E00\u884C\u300C\u52A0\u8F7D\u4E2D\u2026\u300D\uFF1A\u7528\u6237\u65E2\u4E0D\u786E\u5B9A\u70B9\u4E0A\u6CA1\u70B9\u4E0A\uFF0C
2479
+ \u4E5F\u4E0D\u77E5\u9053\u6570\u636E\u4EC0\u4E48\u65F6\u5019\u6362\u6389\u3002\u8FD9\u91CC\u7528\u4E00\u6BB5\u6709\u8DA3\u7684\u300C\u8BB0\u8D26\u5C0F\u961F\u6E05\u70B9\u300D\u52A8\u753B\u586B\u6EE1\u7B49\u5F85\uFF0C\u5E76\u5728\u7ED3\u679C
2480
+ \u771F\u6B63\u5230\u4F4D\u65F6\u7ED9\u4E00\u6B21\u300C\u2713 \u6570\u636E\u5DF2\u66F4\u65B0\u300D+ KPI \u5361\u56DE\u5F39\uFF0C\u8BA9\u300C\u6570\u636E\u53D8\u4E86\u300D\u8FD9\u4EF6\u4E8B\u88AB\u770B\u89C1\u3002 */
2481
+ .dtok-btn{transition:transform .08s ease,background .15s ease,filter .15s ease,border-color .15s ease;}
2482
+ .dtok-btn.primary:not([disabled]):active{transform:scale(.95);}
2483
+ .dtok-btn.loading{position:relative;overflow:hidden;opacity:1;cursor:progress;border-color:var(--dk-accent);}
2484
+ .dtok-btn.loading::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:linear-gradient(100deg,transparent 18%,rgb(255 255 255 / .4) 50%,transparent 82%);transform:translateX(-130%);animation:dtok-btn-sheen 1.05s linear infinite;}
2485
+ @keyframes dtok-btn-sheen{to{transform:translateX(130%);}}
2486
+ .dtok-spin{display:inline-block;width:10px;height:10px;margin-right:5px;vertical-align:-1px;border:2px solid rgb(255 255 255 / .35);border-top-color:#fff;border-radius:50%;animation:dtok-rotate .7s linear infinite;}
2487
+ .dtok-spin.inline{width:9px;height:9px;margin:0;vertical-align:0;border-color:rgb(127 127 127 / .35);border-top-color:var(--dk-accent);}
2488
+ @keyframes dtok-rotate{to{transform:rotate(360deg);}}
2489
+ /* \u72B6\u6001\u884C\u91CC\u7684\u300C\u27F3 \u52A0\u8F7D\u4E2D...\u300D\uFF1A\u8F6C\u5708 + \u7701\u7565\u53F7\u4E00\u4E2A\u4E00\u4E2A\u8E66\u51FA\u6765\uFF0C\u6700\u7ECF\u5178\u7684\u90A3\u79CD\u52A0\u8F7D\u52A8\u753B */
2490
+ .dtok-busy{display:inline-flex;align-items:center;gap:5px;}
2491
+ .dtok-dots{display:inline-block;width:12px;text-align:left;}
2492
+ .dtok-dots i{font-style:normal;opacity:0;animation:dtok-dot 1.2s linear infinite;}
2493
+ .dtok-dots i:nth-child(2){animation-delay:.2s;}
2494
+ .dtok-dots i:nth-child(3){animation-delay:.4s;}
2495
+ @keyframes dtok-dot{0%{opacity:0;}25%,80%{opacity:1;}100%{opacity:0;}}
2496
+ .dtok-body-inner{display:flex;flex-direction:column;gap:8px;min-width:0;transition:opacity .25s ease;}
2497
+ .dtok-body-inner.busy{opacity:.45;pointer-events:none;}
2498
+ .dtok-loading{position:sticky;top:0;z-index:6;display:flex;align-items:center;flex-wrap:wrap;gap:10px 12px;padding:9px 14px;border-radius:12px;border:1px solid var(--dk-accent);background:linear-gradient(180deg,rgb(255 255 255 / .07),rgb(0 0 0 / .1)),var(--dsw-alias-bg-layer-2,#1c212b);box-shadow:0 10px 26px rgb(0 0 0 / .3);animation:dtok-load-in .3s ease;}
2499
+ @keyframes dtok-load-in{from{opacity:0;transform:translateY(-6px);}to{opacity:1;transform:none;}}
2500
+ .dtok-load-scene{position:relative;display:flex;align-items:flex-end;gap:1px;height:30px;flex:none;padding:0 6px;}
2501
+ .dtok-load-emoji{display:inline-block;font-size:19px;line-height:1;animation:dtok-hop 1.1s cubic-bezier(.36,.07,.19,.97) infinite;filter:drop-shadow(0 2px 3px rgb(0 0 0 / .3));}
2502
+ .dtok-load-emoji.e2{animation-delay:.15s;}
2503
+ .dtok-load-emoji.e3{animation-delay:.3s;}
2504
+ @keyframes dtok-hop{0%,100%{transform:translateY(0) rotate(-5deg);}35%{transform:translateY(-9px) rotate(2deg) scale(1.1);}65%{transform:translateY(0) rotate(5deg);}}
2505
+ .dtok-load-spark{position:absolute;font-size:11px;color:var(--dk-accent);animation:dtok-twinkle 1.4s ease-in-out infinite;}
2506
+ .dtok-load-spark.s1{left:-2px;top:-4px;}
2507
+ .dtok-load-spark.s2{right:-2px;top:2px;animation-delay:.6s;}
2508
+ @keyframes dtok-twinkle{0%,100%{opacity:0;transform:scale(.4) rotate(0);}45%{opacity:1;transform:scale(1.2) rotate(90deg);}}
2509
+ .dtok-load-msg{flex:1 1 170px;min-width:0;display:flex;flex-direction:column;gap:2px;}
2510
+ .dtok-load-msg b{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background:linear-gradient(90deg,var(--dsw-alias-label-primary),var(--dk-accent),var(--dsw-alias-label-primary));background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:dtok-shine 2.6s linear infinite,dtok-msg-in .32s ease;}
2511
+ @keyframes dtok-shine{0%{background-position:130% 0;}100%{background-position:-130% 0;}}
2512
+ @keyframes dtok-msg-in{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}
2513
+ .dtok-load-sub{font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
2514
+ .dtok-load-sub.wait{color:var(--dk-warn);}
2515
+ .dtok-load-track{position:relative;flex:0 1 140px;min-width:88px;height:6px;border-radius:999px;background:rgb(127 127 127 / .22);overflow:hidden;}
2516
+ .dtok-load-fill{position:absolute;top:0;bottom:0;width:38%;border-radius:999px;background:linear-gradient(90deg,transparent,var(--dk-accent),transparent);animation:dtok-sweep 1.15s cubic-bezier(.45,.05,.55,.95) infinite;}
2517
+ @keyframes dtok-sweep{0%{transform:translateX(-115%);}100%{transform:translateX(380%);}}
2518
+ .dtok-cards.dtok-pop .dtok-card{animation:dtok-card-in .5s cubic-bezier(.2,.9,.3,1.3) both;animation-delay:calc(var(--i,0) * 45ms);}
2519
+ @keyframes dtok-card-in{from{opacity:0;transform:translateY(7px) scale(.96);}to{opacity:1;transform:none;}}
2520
+ .dtok-updated{font-size:11px;color:var(--dsw-alias-state-success-primary,#4ade80);animation:dtok-updated 2.4s ease forwards;}
2521
+ @keyframes dtok-updated{0%{opacity:0;transform:translateY(-3px) scale(.85);}12%{opacity:1;transform:none;}70%{opacity:1;}100%{opacity:0;}}
2522
+ @media (prefers-reduced-motion:reduce){
2523
+ .dtok-load-emoji,.dtok-load-spark,.dtok-load-fill,.dtok-load-msg b,.dtok-btn.loading::after,.dtok-spin,.dtok-loading,.dtok-cards.dtok-pop .dtok-card{animation:none !important;}
2524
+ .dtok-dots i{animation:none !important;opacity:1;}
2525
+ .dtok-load-emoji{transform:none;}
2526
+ .dtok-body-inner.busy{opacity:.75;}
2527
+ .dtok-btn.primary:not([disabled]):active{transform:none;}
2528
+ }
2428
2529
  /* ---- \u5355\u4EF7\u8BBE\u7F6E\uFF08\u5B50\u5F39\u7A97\uFF09 ---- */
2429
2530
  .dtok-pm-backdrop{position:fixed;inset:0;z-index:2100;background:rgba(15,17,21,.5);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;}
2430
2531
  .dtok-pm{box-sizing:border-box;width:min(980px,calc(100vw - 32px));height:min(760px,calc(100vh - 32px));display:flex;flex-direction:column;border-radius:14px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2,#1c212b);color:var(--dsw-alias-label-primary);box-shadow:0 20px 64px rgb(0 0 0 / .4);overflow:hidden;}
@@ -2821,6 +2922,39 @@ function PricingEditor({ onClose, onSaved, embedded }) {
2821
2922
  ] })
2822
2923
  ] });
2823
2924
  }
2925
+ var LOAD_MSGS = [
2926
+ "\u6B63\u5728\u7FFB\u5F00\u8D26\u672C",
2927
+ "\u6B63\u5728\u4E00\u679A\u4E00\u679A\u6570 Token",
2928
+ "\u6B63\u5728\u7ED9\u6BCF\u6761\u8C03\u7528\u8D34\u4EF7\u7B7E",
2929
+ "\u6B63\u5728\u6838\u5BF9\u7F13\u5B58\u547D\u4E2D\u7387",
2930
+ "\u6B63\u5728\u628A\u5355\u4EF7\u4E58\u8FDB\u6BCF\u4E00\u884C",
2931
+ "\u6B63\u5728\u628A\u8868\u683C\u7801\u6574\u9F50"
2932
+ ];
2933
+ function LoadingBanner() {
2934
+ const [tick, setTick] = (0, import_react2.useState)(0);
2935
+ (0, import_react2.useEffect)(() => {
2936
+ const timer = setInterval(() => setTick((t) => t + 1), 1300);
2937
+ return () => clearInterval(timer);
2938
+ }, []);
2939
+ const long = tick >= 4;
2940
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-loading", role: "status", "aria-live": "polite", children: [
2941
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dtok-load-scene", "aria-hidden": "true", children: [
2942
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-load-spark s1", children: "\u2726" }),
2943
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-load-emoji e1", children: "\u{1F9EE}" }),
2944
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-load-emoji e2", children: "\u{1F4D2}" }),
2945
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-load-emoji e3", children: "\u{1F50D}" }),
2946
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-load-spark s2", children: "\u2726" })
2947
+ ] }),
2948
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dtok-load-msg", children: [
2949
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("b", { children: [
2950
+ LOAD_MSGS[tick % LOAD_MSGS.length],
2951
+ "\u2026"
2952
+ ] }, tick),
2953
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-load-sub" + (long ? " wait" : ""), children: long ? "\u6570\u636E\u8F83\u591A\uFF0C\u518D\u7B49\u4E00\u5C0F\u4F1A\u513F\uFF0C\u6B63\u5728\u5168\u529B\u6E05\u70B9\uFF5E" : "\u6B63\u5728\u6309\u5F53\u524D\u6761\u4EF6\u91CD\u65B0\u7EDF\u8BA1\uFF0C\u901A\u5E38\u51E0\u79D2\u5185\u5B8C\u6210" })
2954
+ ] }),
2955
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-load-track", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-load-fill" }) })
2956
+ ] });
2957
+ }
2824
2958
  function TokenLogView(props) {
2825
2959
  const navSession = props && props.params && props.params.sessionId ? props.params.sessionId : null;
2826
2960
  const navAt = props && props.params && props.params.navAt ? props.params.navAt : 0;
@@ -2841,6 +2975,19 @@ function TokenLogView(props) {
2841
2975
  const [page, setPage] = (0, import_react2.useState)(0);
2842
2976
  const [detailRec, setDetailRec] = (0, import_react2.useState)(null);
2843
2977
  const [showPricing, setShowPricing] = (0, import_react2.useState)(() => !!(props && props.params && props.params.openPricing));
2978
+ const [fetchedAt, setFetchedAt] = (0, import_react2.useState)(0);
2979
+ const [justUpdated, setJustUpdated] = (0, import_react2.useState)(false);
2980
+ const wasLoadingRef = (0, import_react2.useRef)(false);
2981
+ (0, import_react2.useEffect)(() => {
2982
+ if (wasLoadingRef.current && !loading && !err) setFetchedAt(Date.now());
2983
+ wasLoadingRef.current = loading;
2984
+ }, [loading, err]);
2985
+ (0, import_react2.useEffect)(() => {
2986
+ if (!fetchedAt) return;
2987
+ setJustUpdated(true);
2988
+ const timer = setTimeout(() => setJustUpdated(false), 2400);
2989
+ return () => clearTimeout(timer);
2990
+ }, [fetchedAt]);
2844
2991
  const pageSize = 100;
2845
2992
  (0, import_react2.useEffect)(() => {
2846
2993
  saveFilters({ fromStr, toStr, provider, model, status, effort, sessionId, dim });
@@ -2906,6 +3053,7 @@ function TokenLogView(props) {
2906
3053
  setPage(0);
2907
3054
  }).catch((e) => setErr(String(e && e.message || e))).finally(() => setLoading(false));
2908
3055
  }, [navAt]);
3056
+ const rateCny = data && data.rateUsdCny || 7.2;
2909
3057
  const resetFilters = (0, import_react2.useCallback)(() => {
2910
3058
  setFromStr("");
2911
3059
  setToStr("");
@@ -2924,7 +3072,8 @@ function TokenLogView(props) {
2924
3072
  }, []);
2925
3073
  const exportCsv = (0, import_react2.useCallback)(() => {
2926
3074
  rpcCall("export", buildQ(false)).then((d) => {
2927
- const blob = new Blob([d.csv], { type: "text/csv;charset=utf-8" });
3075
+ const csv = d && Array.isArray(d.rows) ? buildExportCsv(d.rows, d && d.rateUsdCny || rateCny) : String(d && d.csv || "");
3076
+ const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" });
2928
3077
  const url = URL.createObjectURL(blob);
2929
3078
  const a = document.createElement("a");
2930
3079
  a.href = url;
@@ -2932,7 +3081,7 @@ function TokenLogView(props) {
2932
3081
  a.click();
2933
3082
  URL.revokeObjectURL(url);
2934
3083
  }).catch((e) => setErr(String(e && e.message || e)));
2935
- }, [buildQ]);
3084
+ }, [buildQ, rateCny]);
2936
3085
  (0, import_react2.useEffect)(() => {
2937
3086
  const timer = setInterval(() => {
2938
3087
  rpcCall("query", buildQ(true)).then((d) => {
@@ -2970,7 +3119,6 @@ function TokenLogView(props) {
2970
3119
  const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
2971
3120
  const pageSafe = Math.min(page, pageCount - 1);
2972
3121
  const pageRows = sorted.slice(pageSafe * pageSize, (pageSafe + 1) * pageSize);
2973
- const rateCny = data && data.rateUsdCny || 7.2;
2974
3122
  const cards = totals ? [
2975
3123
  { v: fmtNum(totals.calls), l: "\u8C03\u7528\u6B21\u6570" },
2976
3124
  { v: fmtCompact(totals.totalTokens), l: "\u603B Token" },
@@ -3035,10 +3183,11 @@ function TokenLogView(props) {
3035
3183
  err
3036
3184
  ] }, "err"));
3037
3185
  if (cards.length) bodyNodes.push(
3038
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-cards", children: cards.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-card", children: [
3186
+ // key 带 fetchedAt:每次「手动查询 / 首次加载」拿到新数据时重建一次,KPI 卡重播回弹动画
3187
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-cards" + (fetchedAt ? " dtok-pop" : ""), children: cards.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-card", style: { "--i": String(i) }, children: [
3039
3188
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "v", children: c.v }),
3040
3189
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "l", children: c.l })
3041
- ] }, c.l)) }, "cards")
3190
+ ] }, c.l)) }, "cards-" + fetchedAt)
3042
3191
  );
3043
3192
  if (summaryRows.length) bodyNodes.push(
3044
3193
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-section-title", children: [
@@ -3113,7 +3262,16 @@ function TokenLogView(props) {
3113
3262
  }
3114
3263
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-root", onClick: (e) => e.stopPropagation(), children: [
3115
3264
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-status", children: [
3116
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "count", children: loading ? "\u52A0\u8F7D\u4E2D\u2026" : data ? data.counts.matching + " / " + data.counts.total + " \u6761" : "" }),
3265
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "count", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dtok-busy", children: [
3266
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-spin inline", "aria-hidden": "true" }),
3267
+ "\u52A0\u8F7D\u4E2D",
3268
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dtok-dots", "aria-hidden": "true", children: [
3269
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { children: "." }),
3270
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { children: "." }),
3271
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { children: "." })
3272
+ ] })
3273
+ ] }) : data ? data.counts.matching + " / " + data.counts.total + " \u6761" : "" }),
3274
+ !loading && justUpdated && !err ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-updated", children: "\u2713 \u6570\u636E\u5DF2\u66F4\u65B0" }, fetchedAt) : null,
3117
3275
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u5168\u5C4F\u8BF7\u7528\u9762\u677F\u53F3\u4E0A\u89D2\u300C\u6700\u5927\u5316\u300D\uFF1B\u9762\u677F\u6253\u5F00\u671F\u95F4\u6BCF 5 \u79D2\u81EA\u52A8\u5237\u65B0" })
3118
3276
  ] }),
3119
3277
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-filter", children: [
@@ -3149,7 +3307,10 @@ function TokenLogView(props) {
3149
3307
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "\u5168\u90E8" }),
3150
3308
  opts(data && data.efforts)
3151
3309
  ] }),
3152
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dtok-btn primary", onClick: runQuery, children: "\u67E5\u8BE2" }),
3310
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { className: "dtok-btn primary" + (loading ? " loading" : ""), disabled: loading, title: "\u6309\u5F53\u524D\u6761\u4EF6\u67E5\u8BE2", onClick: () => runQuery(), children: [
3311
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-spin", "aria-hidden": "true" }) : null,
3312
+ loading ? "\u67E5\u8BE2\u4E2D\u2026" : "\u67E5\u8BE2"
3313
+ ] }),
3153
3314
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dtok-btn", onClick: resetFilters, children: "\u91CD\u7F6E" }),
3154
3315
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dtok-btn", onClick: exportCsv, children: "\u5BFC\u51FA CSV" }),
3155
3316
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dtok-btn" + (showPricing ? " primary" : ""), onClick: () => setShowPricing(true), title: "\u914D\u7F6E\u5404\u6A21\u578B\u5355\u4EF7\uFF0C\u8D39\u7528\u6309\u81EA\u586B\u5355\u4EF7\u8BA1\u7B97", children: "\u5355\u4EF7\u8BBE\u7F6E" })
@@ -3161,7 +3322,10 @@ function TokenLogView(props) {
3161
3322
  " \xB7 \u5B98\u7F51\u81EA\u52A8\u540C\u6B65\uFF1A" + (data.pricingInfo.fetchOfficial ? "\u5F00" : "\u5173"),
3162
3323
  data.pricingInfo.sources && data.pricingInfo.sources.length ? " \xB7 \u672C\u9875\u6D89\u53CA\u6765\u6E90\uFF1A" + data.pricingInfo.sources.join("/") : ""
3163
3324
  ] }) : null }),
3164
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-body", children: bodyNodes }),
3325
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-body", children: [
3326
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoadingBanner, {}) : null,
3327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-body-inner" + (loading ? " busy" : ""), children: bodyNodes })
3328
+ ] }),
3165
3329
  detailRec ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Detail, { rec: detailRec, onClose: () => setDetailRec(null), rate: rateCny }) : null
3166
3330
  ] });
3167
3331
  }
@@ -10883,7 +11047,7 @@ var feature10 = {
10883
11047
  };
10884
11048
 
10885
11049
  // src/client.jsx
10886
- var DOCK_VERSION = "0.11.2";
11050
+ var DOCK_VERSION = "0.11.3";
10887
11051
  var BUILTIN_FEATURES = [feature, feature2, feature3, feature4, feature5, feature6, feature7, feature8, feature9, feature10];
10888
11052
  var PLANNED_FEATURES = [];
10889
11053
  var PLANNED_NOTES = {};
package/cordis.patch.yml CHANGED
@@ -1,3 +1,3 @@
1
- - insert:
2
- - id: dsh-dock
1
+ - insert:
2
+ - id: dsh-dock
3
3
  name: dsh-dock
@@ -1,119 +1,119 @@
1
- // dsh-dock · 功能模块【任务动画】· 宿主半部
2
- //
3
- // 职责:动画配置持久化(动效开关 / 模式 / 桌面伙伴大小)+ 状态路由。
4
- // 会话级任务追踪在 src/task-track.js(与【任务通知】【运行状态】共用一份数据);
5
- // 通知在 features/notify/、运行状态在 features/runstate/,三个功能各自独立启停。
6
- //
7
- // RPC(webServer HTTP 路由,前缀 /dsh-dock/animation/):
8
- // POST /status —— 活跃任务 + 最近完成 + 动画配置(客户端据此渲染动效)
9
- // POST /config —— 增量更新动画配置字段并持久化到 settings(dsh-dock 命名空间 animation 段)
10
- //
11
- // 配置模型(schemastery schema 见 src/host-core.js DockConfig.animation):
12
- // animationEnabled / effectMode / robotScale
13
- import { DOCK_NS, ANIMATION_MODES, sendJson, readBody } from '../../src/host-core.js'
14
- import { acquireTaskTracker } from '../../src/task-track.js'
15
-
16
- // 默认配置(schema 默认值一致;settings.get 未挂载时的兜底)
17
- function defaultConfig() {
18
- return {
19
- animationEnabled: true,
20
- effectMode: 'flow',
21
- robotScale: 1.35,
22
- }
23
- }
24
-
25
- // 读 settings 里的 animation 配置(resolved 值已含 schema 默认),异常时回退默认
26
- function readConfig(ctx) {
27
- const cfg = defaultConfig()
28
- try {
29
- const settings = ctx.get('settings')
30
- const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
31
- const a = v && typeof v === 'object' && v.animation && typeof v.animation === 'object' ? v.animation : null
32
- if (a) {
33
- for (const key of Object.keys(cfg)) {
34
- if (a[key] !== undefined) cfg[key] = a[key]
35
- }
36
- if (!ANIMATION_MODES.includes(cfg.effectMode)) cfg.effectMode = 'flow'
37
- }
38
- } catch { /* settings 未挂载,用默认 */ }
39
- return cfg
40
- }
41
-
42
- export const feature = {
43
- id: 'animation',
44
- name: '任务动画',
45
- description: '19 种任务运行动画(速度随任务活动联动,配置持久化;通知见「任务通知」,任务状态见「运行状态」)',
46
- defaultEnabled: false,
47
- setup(ctx) {
48
- const disposers = []
49
- const dispose = () => {
50
- while (disposers.length > 0) {
51
- const fn = disposers.pop()
52
- try { if (typeof fn === 'function') fn() } catch { /* 停用清理失败不阻断 */ }
53
- }
54
- }
55
-
56
- // 会话级任务追踪(与【任务通知】共享一份;两个功能都停用后自动退订会话事件)
57
- const lease = acquireTaskTracker(ctx)
58
- disposers.push(() => lease.release())
59
- const tracker = lease.tracker
60
-
61
- // ===== RPC 路由 =====
62
- disposers.push(ctx.inject(['webServer'], (wsCtx) => {
63
- wsCtx.effect(() => wsCtx.webServer.register({
64
- kind: 'prefix',
65
- path: '/dsh-dock/animation',
66
- async handler(req, res) {
67
- try {
68
- const url = new URL(req.url || '/', 'http://dsh.internal')
69
- const method = url.pathname.replace(/^\/dsh-dock\/animation\/?/, '').split('/')[0] || ''
70
- const payload = await readBody(req)
71
-
72
- if (method === 'status') {
73
- return sendJson(res, 200, {
74
- ok: true,
75
- data: Object.assign({}, tracker.snapshot(Date.now()), { config: readConfig(ctx) }),
76
- })
77
- }
78
-
79
- if (method === 'config') {
80
- // 增量合并:只接受已知字段,类型不符忽略;整体写回 animation 段
81
- const cfg = readConfig(ctx)
82
- const p = payload || {}
83
- if (typeof p.animationEnabled === 'boolean') cfg.animationEnabled = p.animationEnabled
84
- if (typeof p.effectMode === 'string' && ANIMATION_MODES.includes(p.effectMode)) cfg.effectMode = p.effectMode
85
- if (typeof p.robotScale === 'number' && Number.isFinite(p.robotScale)) {
86
- cfg.robotScale = Math.max(0.85, Math.min(2.2, Math.round(p.robotScale * 100) / 100))
87
- }
88
-
89
- const settings = ctx.get('settings')
90
- if (!settings || typeof settings.mutate !== 'function') {
91
- throw new Error('settings 服务不可用,配置无法持久化')
92
- }
93
- try {
94
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['animation'], value: cfg }])
95
- } catch (e) {
96
- const err = new Error('保存配置被拒绝:' + ((e && e.message) || String(e)))
97
- err.statusCode = 400
98
- throw err
99
- }
100
- console.log('[dsh-dock] animation config saved:', cfg.effectMode, 'anim=' + cfg.animationEnabled)
101
- return sendJson(res, 200, { ok: true, data: { config: cfg, savedAt: Date.now() } })
102
- }
103
-
104
- return sendJson(res, 404, { ok: false, error: { code: 'method-not-found', message: 'unknown method: ' + method } })
105
- } catch (e) {
106
- const status = e && e.statusCode ? e.statusCode : 500
107
- console.error('[dsh-dock] animation HTTP error:', status, e && e.message)
108
- return sendJson(res, status, {
109
- ok: false,
110
- error: { code: status >= 500 ? 'internal' : 'bad-request', message: (e && e.message) || String(e) },
111
- })
112
- }
113
- },
114
- }), 'dsh-dock animation: /dsh-dock/animation HTTP route')
115
- }))
116
-
117
- return dispose
118
- },
119
- }
1
+ // dsh-dock · 功能模块【任务动画】· 宿主半部
2
+ //
3
+ // 职责:动画配置持久化(动效开关 / 模式 / 桌面伙伴大小)+ 状态路由。
4
+ // 会话级任务追踪在 src/task-track.js(与【任务通知】【运行状态】共用一份数据);
5
+ // 通知在 features/notify/、运行状态在 features/runstate/,三个功能各自独立启停。
6
+ //
7
+ // RPC(webServer HTTP 路由,前缀 /dsh-dock/animation/):
8
+ // POST /status —— 活跃任务 + 最近完成 + 动画配置(客户端据此渲染动效)
9
+ // POST /config —— 增量更新动画配置字段并持久化到 settings(dsh-dock 命名空间 animation 段)
10
+ //
11
+ // 配置模型(schemastery schema 见 src/host-core.js DockConfig.animation):
12
+ // animationEnabled / effectMode / robotScale
13
+ import { DOCK_NS, ANIMATION_MODES, sendJson, readBody } from '../../src/host-core.js'
14
+ import { acquireTaskTracker } from '../../src/task-track.js'
15
+
16
+ // 默认配置(schema 默认值一致;settings.get 未挂载时的兜底)
17
+ function defaultConfig() {
18
+ return {
19
+ animationEnabled: true,
20
+ effectMode: 'flow',
21
+ robotScale: 1.35,
22
+ }
23
+ }
24
+
25
+ // 读 settings 里的 animation 配置(resolved 值已含 schema 默认),异常时回退默认
26
+ function readConfig(ctx) {
27
+ const cfg = defaultConfig()
28
+ try {
29
+ const settings = ctx.get('settings')
30
+ const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
31
+ const a = v && typeof v === 'object' && v.animation && typeof v.animation === 'object' ? v.animation : null
32
+ if (a) {
33
+ for (const key of Object.keys(cfg)) {
34
+ if (a[key] !== undefined) cfg[key] = a[key]
35
+ }
36
+ if (!ANIMATION_MODES.includes(cfg.effectMode)) cfg.effectMode = 'flow'
37
+ }
38
+ } catch { /* settings 未挂载,用默认 */ }
39
+ return cfg
40
+ }
41
+
42
+ export const feature = {
43
+ id: 'animation',
44
+ name: '任务动画',
45
+ description: '19 种任务运行动画(速度随任务活动联动,配置持久化;通知见「任务通知」,任务状态见「运行状态」)',
46
+ defaultEnabled: false,
47
+ setup(ctx) {
48
+ const disposers = []
49
+ const dispose = () => {
50
+ while (disposers.length > 0) {
51
+ const fn = disposers.pop()
52
+ try { if (typeof fn === 'function') fn() } catch { /* 停用清理失败不阻断 */ }
53
+ }
54
+ }
55
+
56
+ // 会话级任务追踪(与【任务通知】共享一份;两个功能都停用后自动退订会话事件)
57
+ const lease = acquireTaskTracker(ctx)
58
+ disposers.push(() => lease.release())
59
+ const tracker = lease.tracker
60
+
61
+ // ===== RPC 路由 =====
62
+ disposers.push(ctx.inject(['webServer'], (wsCtx) => {
63
+ wsCtx.effect(() => wsCtx.webServer.register({
64
+ kind: 'prefix',
65
+ path: '/dsh-dock/animation',
66
+ async handler(req, res) {
67
+ try {
68
+ const url = new URL(req.url || '/', 'http://dsh.internal')
69
+ const method = url.pathname.replace(/^\/dsh-dock\/animation\/?/, '').split('/')[0] || ''
70
+ const payload = await readBody(req)
71
+
72
+ if (method === 'status') {
73
+ return sendJson(res, 200, {
74
+ ok: true,
75
+ data: Object.assign({}, tracker.snapshot(Date.now()), { config: readConfig(ctx) }),
76
+ })
77
+ }
78
+
79
+ if (method === 'config') {
80
+ // 增量合并:只接受已知字段,类型不符忽略;整体写回 animation 段
81
+ const cfg = readConfig(ctx)
82
+ const p = payload || {}
83
+ if (typeof p.animationEnabled === 'boolean') cfg.animationEnabled = p.animationEnabled
84
+ if (typeof p.effectMode === 'string' && ANIMATION_MODES.includes(p.effectMode)) cfg.effectMode = p.effectMode
85
+ if (typeof p.robotScale === 'number' && Number.isFinite(p.robotScale)) {
86
+ cfg.robotScale = Math.max(0.85, Math.min(2.2, Math.round(p.robotScale * 100) / 100))
87
+ }
88
+
89
+ const settings = ctx.get('settings')
90
+ if (!settings || typeof settings.mutate !== 'function') {
91
+ throw new Error('settings 服务不可用,配置无法持久化')
92
+ }
93
+ try {
94
+ await settings.mutate(DOCK_NS, [{ op: 'set', path: ['animation'], value: cfg }])
95
+ } catch (e) {
96
+ const err = new Error('保存配置被拒绝:' + ((e && e.message) || String(e)))
97
+ err.statusCode = 400
98
+ throw err
99
+ }
100
+ console.log('[dsh-dock] animation config saved:', cfg.effectMode, 'anim=' + cfg.animationEnabled)
101
+ return sendJson(res, 200, { ok: true, data: { config: cfg, savedAt: Date.now() } })
102
+ }
103
+
104
+ return sendJson(res, 404, { ok: false, error: { code: 'method-not-found', message: 'unknown method: ' + method } })
105
+ } catch (e) {
106
+ const status = e && e.statusCode ? e.statusCode : 500
107
+ console.error('[dsh-dock] animation HTTP error:', status, e && e.message)
108
+ return sendJson(res, status, {
109
+ ok: false,
110
+ error: { code: status >= 500 ? 'internal' : 'bad-request', message: (e && e.message) || String(e) },
111
+ })
112
+ }
113
+ },
114
+ }), 'dsh-dock animation: /dsh-dock/animation HTTP route')
115
+ }))
116
+
117
+ return dispose
118
+ },
119
+ }