dsh-dock 0.11.1 → 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.
- package/LICENSE +18 -18
- package/README.md +242 -396
- package/client.js +188 -17
- package/cordis.patch.yml +2 -2
- package/features/animation/host.js +119 -119
- package/features/animation/view.jsx +1662 -1662
- package/features/balance/host.js +494 -494
- package/features/balance/view.js +351 -351
- package/features/games/games/breakout.jsx +227 -227
- package/features/games/games/gomoku.jsx +244 -244
- package/features/games/games/racer.jsx +169 -169
- package/features/games/games/shared.jsx +118 -118
- package/features/games/games/snake.jsx +144 -144
- package/features/games/games/sokoban.jsx +180 -180
- package/features/games/games/tank.jsx +128 -128
- package/features/games/games/tetris.jsx +308 -308
- package/features/games/games/xiangqi.jsx +277 -277
- package/features/games/tank-core.js +152 -152
- package/features/games/view.jsx +639 -639
- package/features/games/xiangqi-core.js +299 -299
- package/features/heartbeat/view.js +35 -35
- package/features/mobile-relay/gateway.js +472 -472
- package/features/mobile-relay/host.js +570 -570
- package/features/mobile-relay/view.jsx +300 -300
- package/features/modelconfig/host.js +406 -406
- package/features/modelconfig/view.js +490 -490
- package/features/notify/host.js +374 -374
- package/features/notify/view.jsx +751 -751
- package/features/theme/view.js +61 -61
- package/features/tokenlog/host.js +882 -883
- package/features/tokenlog/view.jsx +1117 -922
- package/features/visionproxy/host.js +315 -315
- package/index.js +195 -195
- package/package.json +66 -64
- package/src/client.jsx +677 -674
- package/src/host-core.js +207 -207
- package/src/shared.js +192 -192
- 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,8 +2922,42 @@ 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;
|
|
2960
|
+
const navAt = props && props.params && props.params.navAt ? props.params.navAt : 0;
|
|
2826
2961
|
const [savedFilters] = (0, import_react2.useState)(loadSavedFilters);
|
|
2827
2962
|
const [fromStr, setFromStr] = (0, import_react2.useState)(() => savedFilters && savedFilters.fromStr || "");
|
|
2828
2963
|
const [toStr, setToStr] = (0, import_react2.useState)(() => savedFilters && savedFilters.toStr || "");
|
|
@@ -2830,7 +2965,7 @@ function TokenLogView(props) {
|
|
|
2830
2965
|
const [model, setModel] = (0, import_react2.useState)(() => savedFilters && savedFilters.model || "");
|
|
2831
2966
|
const [status, setStatus] = (0, import_react2.useState)(() => savedFilters && savedFilters.status || "");
|
|
2832
2967
|
const [effort, setEffort] = (0, import_react2.useState)(() => savedFilters && savedFilters.effort || "");
|
|
2833
|
-
const [sessionId, setSessionId] = (0, import_react2.useState)(() => savedFilters && savedFilters.sessionId || "");
|
|
2968
|
+
const [sessionId, setSessionId] = (0, import_react2.useState)(() => navSession || savedFilters && savedFilters.sessionId || "");
|
|
2834
2969
|
const [dim, setDim] = (0, import_react2.useState)(() => savedFilters && savedFilters.dim || "");
|
|
2835
2970
|
const [sortKey, setSortKey] = (0, import_react2.useState)("time");
|
|
2836
2971
|
const [sortDir, setSortDir] = (0, import_react2.useState)("desc");
|
|
@@ -2840,6 +2975,19 @@ function TokenLogView(props) {
|
|
|
2840
2975
|
const [page, setPage] = (0, import_react2.useState)(0);
|
|
2841
2976
|
const [detailRec, setDetailRec] = (0, import_react2.useState)(null);
|
|
2842
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]);
|
|
2843
2991
|
const pageSize = 100;
|
|
2844
2992
|
(0, import_react2.useEffect)(() => {
|
|
2845
2993
|
saveFilters({ fromStr, toStr, provider, model, status, effort, sessionId, dim });
|
|
@@ -2859,6 +3007,7 @@ function TokenLogView(props) {
|
|
|
2859
3007
|
if (savedFilters.sessionId) q.sessionId = savedFilters.sessionId;
|
|
2860
3008
|
if (savedFilters.dim) q.dim = savedFilters.dim;
|
|
2861
3009
|
}
|
|
3010
|
+
if (navSession) q.sessionId = navSession;
|
|
2862
3011
|
rpcCall("scan", {}).then(() => cancel ? null : rpcCall("query", q)).then((d) => {
|
|
2863
3012
|
if (cancel) return;
|
|
2864
3013
|
setData(d);
|
|
@@ -2892,8 +3041,10 @@ function TokenLogView(props) {
|
|
|
2892
3041
|
setPage(0);
|
|
2893
3042
|
}).catch((e) => setErr(String(e && e.message || e))).finally(() => setLoading(false));
|
|
2894
3043
|
}, [buildQ]);
|
|
3044
|
+
const navAppliedRef = (0, import_react2.useRef)(navAt);
|
|
2895
3045
|
(0, import_react2.useEffect)(() => {
|
|
2896
|
-
if (!navSession) return;
|
|
3046
|
+
if (!navSession || navAppliedRef.current === navAt) return;
|
|
3047
|
+
navAppliedRef.current = navAt;
|
|
2897
3048
|
setSessionId(navSession);
|
|
2898
3049
|
setLoading(true);
|
|
2899
3050
|
setErr("");
|
|
@@ -2901,7 +3052,8 @@ function TokenLogView(props) {
|
|
|
2901
3052
|
setData(d);
|
|
2902
3053
|
setPage(0);
|
|
2903
3054
|
}).catch((e) => setErr(String(e && e.message || e))).finally(() => setLoading(false));
|
|
2904
|
-
}, [
|
|
3055
|
+
}, [navAt]);
|
|
3056
|
+
const rateCny = data && data.rateUsdCny || 7.2;
|
|
2905
3057
|
const resetFilters = (0, import_react2.useCallback)(() => {
|
|
2906
3058
|
setFromStr("");
|
|
2907
3059
|
setToStr("");
|
|
@@ -2920,7 +3072,8 @@ function TokenLogView(props) {
|
|
|
2920
3072
|
}, []);
|
|
2921
3073
|
const exportCsv = (0, import_react2.useCallback)(() => {
|
|
2922
3074
|
rpcCall("export", buildQ(false)).then((d) => {
|
|
2923
|
-
const
|
|
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" });
|
|
2924
3077
|
const url = URL.createObjectURL(blob);
|
|
2925
3078
|
const a = document.createElement("a");
|
|
2926
3079
|
a.href = url;
|
|
@@ -2928,7 +3081,7 @@ function TokenLogView(props) {
|
|
|
2928
3081
|
a.click();
|
|
2929
3082
|
URL.revokeObjectURL(url);
|
|
2930
3083
|
}).catch((e) => setErr(String(e && e.message || e)));
|
|
2931
|
-
}, [buildQ]);
|
|
3084
|
+
}, [buildQ, rateCny]);
|
|
2932
3085
|
(0, import_react2.useEffect)(() => {
|
|
2933
3086
|
const timer = setInterval(() => {
|
|
2934
3087
|
rpcCall("query", buildQ(true)).then((d) => {
|
|
@@ -2966,7 +3119,6 @@ function TokenLogView(props) {
|
|
|
2966
3119
|
const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
|
|
2967
3120
|
const pageSafe = Math.min(page, pageCount - 1);
|
|
2968
3121
|
const pageRows = sorted.slice(pageSafe * pageSize, (pageSafe + 1) * pageSize);
|
|
2969
|
-
const rateCny = data && data.rateUsdCny || 7.2;
|
|
2970
3122
|
const cards = totals ? [
|
|
2971
3123
|
{ v: fmtNum(totals.calls), l: "\u8C03\u7528\u6B21\u6570" },
|
|
2972
3124
|
{ v: fmtCompact(totals.totalTokens), l: "\u603B Token" },
|
|
@@ -3031,10 +3183,11 @@ function TokenLogView(props) {
|
|
|
3031
3183
|
err
|
|
3032
3184
|
] }, "err"));
|
|
3033
3185
|
if (cards.length) bodyNodes.push(
|
|
3034
|
-
|
|
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: [
|
|
3035
3188
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "v", children: c.v }),
|
|
3036
3189
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "l", children: c.l })
|
|
3037
|
-
] }, c.l)) }, "cards")
|
|
3190
|
+
] }, c.l)) }, "cards-" + fetchedAt)
|
|
3038
3191
|
);
|
|
3039
3192
|
if (summaryRows.length) bodyNodes.push(
|
|
3040
3193
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-section-title", children: [
|
|
@@ -3109,7 +3262,16 @@ function TokenLogView(props) {
|
|
|
3109
3262
|
}
|
|
3110
3263
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-root", onClick: (e) => e.stopPropagation(), children: [
|
|
3111
3264
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-status", children: [
|
|
3112
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "count", children: loading ?
|
|
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,
|
|
3113
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" })
|
|
3114
3276
|
] }),
|
|
3115
3277
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-filter", children: [
|
|
@@ -3145,7 +3307,10 @@ function TokenLogView(props) {
|
|
|
3145
3307
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "\u5168\u90E8" }),
|
|
3146
3308
|
opts(data && data.efforts)
|
|
3147
3309
|
] }),
|
|
3148
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
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
|
+
] }),
|
|
3149
3314
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dtok-btn", onClick: resetFilters, children: "\u91CD\u7F6E" }),
|
|
3150
3315
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dtok-btn", onClick: exportCsv, children: "\u5BFC\u51FA CSV" }),
|
|
3151
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" })
|
|
@@ -3157,7 +3322,10 @@ function TokenLogView(props) {
|
|
|
3157
3322
|
" \xB7 \u5B98\u7F51\u81EA\u52A8\u540C\u6B65\uFF1A" + (data.pricingInfo.fetchOfficial ? "\u5F00" : "\u5173"),
|
|
3158
3323
|
data.pricingInfo.sources && data.pricingInfo.sources.length ? " \xB7 \u672C\u9875\u6D89\u53CA\u6765\u6E90\uFF1A" + data.pricingInfo.sources.join("/") : ""
|
|
3159
3324
|
] }) : null }),
|
|
3160
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
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
|
+
] }),
|
|
3161
3329
|
detailRec ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Detail, { rec: detailRec, onClose: () => setDetailRec(null), rate: rateCny }) : null
|
|
3162
3330
|
] });
|
|
3163
3331
|
}
|
|
@@ -3222,7 +3390,7 @@ function TokenLogChip(props) {
|
|
|
3222
3390
|
className: "dockchip" + (snap.err && !t ? " err" : ""),
|
|
3223
3391
|
title,
|
|
3224
3392
|
"aria-label": "\u4F1A\u8BDD\u7528\u91CF",
|
|
3225
|
-
onClick: () => openPanel("tokenlog", { sessionId: sid }),
|
|
3393
|
+
onClick: () => openPanel("tokenlog", { sessionId: sid, navAt: Date.now() }),
|
|
3226
3394
|
children: [
|
|
3227
3395
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dockchip-dot", style: { background: "var(--dk-warn)" } }),
|
|
3228
3396
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: label })
|
|
@@ -10879,7 +11047,7 @@ var feature10 = {
|
|
|
10879
11047
|
};
|
|
10880
11048
|
|
|
10881
11049
|
// src/client.jsx
|
|
10882
|
-
var DOCK_VERSION = "0.11.
|
|
11050
|
+
var DOCK_VERSION = "0.11.3";
|
|
10883
11051
|
var BUILTIN_FEATURES = [feature, feature2, feature3, feature4, feature5, feature6, feature7, feature8, feature9, feature10];
|
|
10884
11052
|
var PLANNED_FEATURES = [];
|
|
10885
11053
|
var PLANNED_NOTES = {};
|
|
@@ -11039,11 +11207,14 @@ var SHELL_CSS = [
|
|
|
11039
11207
|
// 注意:旧上限 min(280px,36cqw) 仍偏宽——标准 780px 输入卡下(.row 内容约 762px),
|
|
11040
11208
|
// add 钮(28)+工作区选择器(~214)+chips(280) 已与右侧模型选择器+发送键(~345px)合计超宽,
|
|
11041
11209
|
// 即便 chips 未截断 .row 也会换行把模型选择器/发送键挤到第二行。实测把上限收到 189px
|
|
11042
|
-
// (≈25cqw)并使 chips 内距收紧(padding 2px 6px、gap 2px
|
|
11043
|
-
// 两 chip
|
|
11210
|
+
// (≈25cqw)并使 chips 内距收紧(padding 2px 6px、gap 2px)后,「4~6M 用量 + 余额」
|
|
11211
|
+
// 两 chip 可完整显示且不再换行。v0.11.2:配置单价后费用段「· ¥0.03」让两 chip 合计
|
|
11212
|
+
// 约需 204px,189px 恰好截掉约 2 个数字(费用/余额小数位看不到)——按用户反馈放宽到
|
|
11213
|
+
// 216px(≈28cqw),标准卡下实测完整显示;若再出现把模型选择器挤换行,优先压缩用量
|
|
11214
|
+
// chip 的 ⛁ 前缀换空间,别再收这个上限。超限仍省略号截断,完整值在 title 悬浮提示里。
|
|
11044
11215
|
// 2) 截断:超限部分用省略号截断(完整数值在 title 悬浮提示里),防 chips 凸出输入卡圆角(悬空)。
|
|
11045
|
-
".dockchip-row{display:inline-flex;align-items:center;gap:2px;min-width:0;flex:0 1 auto;overflow:hidden;max-width:
|
|
11046
|
-
"@supports (width:1cqw){.dockchip-row{max-width:min(
|
|
11216
|
+
".dockchip-row{display:inline-flex;align-items:center;gap:2px;min-width:0;flex:0 1 auto;overflow:hidden;max-width:216px;}",
|
|
11217
|
+
"@supports (width:1cqw){.dockchip-row{max-width:min(216px,28cqw);}}",
|
|
11047
11218
|
".dockchip{display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px 6px;font-family:inherit;font-size:11px;line-height:18px;white-space:nowrap;min-width:0;overflow:hidden;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
|
|
11048
11219
|
".dockchip > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
|
|
11049
11220
|
".dockchip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
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
|
+
}
|