dsh-dock 0.11.6 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  | 功能 | 说明 |
18
18
  |---|---|
19
- | **用量记录** | 记录全部 LLM API 调用:秒级时间筛选、Token/费用统计(**可配置各模型单价、持久保存并按自填单价计费**,支持多段分时价,内置峰谷计价 + 官网价目自动同步作兜底)、分组汇总、明细检索与 CSV 导出 |
19
+ | **用量记录** | 记录全部 LLM API 调用:秒级时间筛选、Token/费用统计(**可配置各模型单价、持久保存并按自填单价计费**,支持多段分时价与周末/节假日谷价,内置峰谷计价 + 官网价目自动同步作兜底)、分组汇总、明细检索与 CSV 导出 |
20
20
  | **模型设置** | 编辑各 Provider 模型目录:输入类型(文本/图片 + 标注)与思考强度档位;图片能力可由你手动声明,新增 Provider 自动刷新加载,写回官方配置热生效 |
21
21
  | **模型余额** | 展示所有模型 Provider 账户余额(5 分钟自动刷新) |
22
22
  | **任务动画** | 19 种任务运行动画(含星际远征、星云潮汐、曲速航道、量子雷达、星座网络、深海脉动、神经电路等),速度随任务活动联动,配置全部持久化 |
@@ -123,7 +123,7 @@ dsh plugin --profile web remove dsh-dock # 卸载
123
123
  - 「模型耗时排行」横条图:按模型累计 LLM 耗时降序,看哪个模型最占时间
124
124
  - 「分布统计」横条图:维度一键切换 模型 / 提供商 / 状态 / 推理强度
125
125
  - 指标可切换 总 Token / 调用次数 / 金额 / 输出 Token;所有图都基于当前筛选结果即时聚合,筛选一变图表即变
126
- 9. **费用不准?点「单价设置」按模型填真实单价**:官网刊例价是抓来的,与实际计费(第三方中转、折扣、自建端点)常有出入。点开独立的「单价设置」子弹窗(可最大化、拖动、缩放),可按模型逐条配置单价(人民币元 / 百万 tokens,匹配模型名子串即可),并设置兜底单价、USD→CNY 汇率、是否继续自动同步官网价。每个模型还可配置**多段分时价**(如 DeepSeek 的高峰/优惠时段),命中哪段用哪段、时段外回落基准价,支持跨零点(如 `23~7`)与半点边界(如 `8.5` = 08:30)。保存后写入宿主配置**持久保存**,费用立即按自填单价重算(无需重扫历史);计价优先级:**自定义单价 > 官网同步价 > 内置价 > 兜底价**。明细表里走兜底价的金额会标「兜底」,方便你补配。
126
+ 9. **费用不准?点「单价设置」按模型填真实单价**:官网刊例价是抓来的,与实际计费(第三方中转、折扣、自建端点)常有出入。点开独立的「单价设置」子弹窗(可最大化、拖动、缩放),可按模型逐条配置单价(人民币元 / 百万 tokens,匹配模型名子串即可),并设置兜底单价、USD→CNY 汇率、是否继续自动同步官网价。每个模型还可配置**多段分时价**(如 DeepSeek 的高峰/优惠时段),命中哪段用哪段、时段外回落基准价,支持跨零点(如 `23~7`)与半点边界(如 `8.5` = 08:30)。每段可选**日期类型**:每天 / **仅工作日** / **仅周末或节假日**;再配合顶部的**法定节假日**列表(`2026-10-01` 或 `2026-10-01~2026-10-07`),即可表达「周末与节假日全天谷价、只有非节假日非周末的高峰时段双倍」(内置 DeepSeek V4 即此口径:工作日 9~12、14~18 双倍)。保存后写入宿主配置**持久保存**,费用立即按自填单价重算(无需重扫历史);计价优先级:**自定义单价 > 官网同步价 > 内置价 > 兜底价**。明细表里走兜底价的金额会标「兜底」,方便你补配。
127
127
 
128
128
  **模型余额**
129
129
 
@@ -207,7 +207,7 @@ dsh plugin --profile web remove dsh-dock # 卸载
207
207
  - **功能开关刷新后重置**:v0.5.0 起功能开关已持久化(浏览器本地存储),刷新/重启后保持。
208
208
  - **任务动画/任务通知/运行状态不生效**:三者的路由都在宿主进程内(动画/通知的配置持久化也在宿主),旧宿主需重启 `dsh web` 加载(面板页会提示「宿主进程是旧版本」)。另注意通知与运行状态已各自独立成菜单项,需在功能坞里分别开启「任务通知」「运行状态」。
209
209
  - **用量记录空白**:确认 dsh 是较新版本且插件宿主已加载(查看启动日志含 `[dsh-dock] host half loaded`);旧宿主进程需重启 dsh web 加载新路由。
210
- - **费用不准 / 与实际账单对不上**:金额是按单价估算的,官网抓取来的刊例价不代表你的实际计费。在用量记录页点「单价设置」(独立子弹窗,可最大化/拖动/缩放),按模型填真实单价并保存(持久化到宿主配置),费用会立即按自填单价重算;每个模型还支持多段分时价(跨零点 / 半点边界)。计价优先级为「自定义单价 > 官网同步价 > 内置价 > 兜底价」,明细里走兜底价的金额会标「兜底」。
210
+ - **费用不准 / 与实际账单对不上**:金额是按单价估算的,官网抓取来的刊例价不代表你的实际计费。在用量记录页点「单价设置」(独立子弹窗,可最大化/拖动/缩放),按模型填真实单价并保存(持久化到宿主配置),费用会立即按自填单价重算;每个模型还支持多段分时价(跨零点 / 半点边界 / 每天·仅工作日·仅周末节假日)+ 全局法定节假日列表。计价优先级为「自定义单价 > 官网同步价 > 内置价 > 兜底价」,明细里走兜底价的金额会标「兜底」。
211
211
  - **余额显示「需登录 / 无密钥」**:对应 Provider 未在 dsh 凭据中配置可查询余额的密钥,属正常状态提示。
212
212
  - **切模型 / 打开历史会话卡住,报 `preset "…" failed to mount`**:这是该 **agent 预设**与 dsh 版本
213
213
  的 schema 漂移,不是本插件的问题。预设目录在 `~/.dsh/.agent-presets/<id>/`,dsh 升级后预设里的
package/client.js CHANGED
@@ -2764,6 +2764,9 @@ var css = `
2764
2764
  .dtok-pseg{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
2765
2765
  .dtok-pseg-name{font-size:11px;color:var(--dsw-alias-label-secondary);min-width:112px;}
2766
2766
  .dtok-pseg-tilde{font-size:11px;color:var(--dsw-alias-label-secondary);}
2767
+ .dtok-price select{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 6px;font-size:12px;font-family:inherit;}
2768
+ .dtok-price textarea{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px;font-size:12px;font-family:inherit;min-height:64px;resize:vertical;width:100%;box-sizing:border-box;}
2769
+ .dtok-days{font-size:11px;}
2767
2770
  .dtok-src{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);}
2768
2771
  .dtok-src.custom{color:var(--dsw-alias-state-success-primary,#4ade80);}
2769
2772
  .dtok-src.fallback{color:var(--dk-warn);}
@@ -2836,9 +2839,11 @@ function numStr(v) {
2836
2839
  return v === void 0 || v === null ? "" : String(v);
2837
2840
  }
2838
2841
  function segToDraft(s) {
2842
+ const d = s && s.days;
2839
2843
  return {
2840
2844
  start: numStr(s && s.start),
2841
2845
  end: numStr(s && s.end),
2846
+ days: d === "workday" || d === "nonworkday" ? d : "all",
2842
2847
  input: numStr(s && s.input),
2843
2848
  output: numStr(s && s.output),
2844
2849
  cacheRead: numStr(s && s.cacheRead),
@@ -2943,23 +2948,27 @@ function PricingEditor({ onClose, onSaved, embedded }) {
2943
2948
  const [rate, setRate] = (0, import_react2.useState)("7.2");
2944
2949
  const [fetchOn, setFetchOn] = (0, import_react2.useState)(true);
2945
2950
  const [fb, setFb] = (0, import_react2.useState)({ input: "", output: "", cacheRead: "", cacheWrite: "" });
2951
+ const [holidaysText, setHolidaysText] = (0, import_react2.useState)("");
2946
2952
  const [newMatch, setNewMatch] = (0, import_react2.useState)("");
2947
2953
  const [msg, setMsg] = (0, import_react2.useState)(null);
2948
2954
  const [saving, setSaving] = (0, import_react2.useState)(false);
2955
+ const applyCfg = (d) => {
2956
+ setCfg(d);
2957
+ setRate(numStr(d.usdCnyRate));
2958
+ setFetchOn(!!d.fetchOfficial);
2959
+ setFb({
2960
+ input: numStr(d.fallback && d.fallback.input),
2961
+ output: numStr(d.fallback && d.fallback.output),
2962
+ cacheRead: numStr(d.fallback && d.fallback.cacheRead),
2963
+ cacheWrite: numStr(d.fallback && d.fallback.cacheWrite)
2964
+ });
2965
+ setHolidaysText((d.holidays || []).join("\n"));
2966
+ setRows((d.pricing || []).map(rowToDraft));
2967
+ };
2949
2968
  (0, import_react2.useEffect)(() => {
2950
2969
  let cancel = false;
2951
2970
  rpcCall("pricing", {}).then((d) => {
2952
- if (cancel) return;
2953
- setCfg(d);
2954
- setRate(numStr(d.usdCnyRate));
2955
- setFetchOn(!!d.fetchOfficial);
2956
- setFb({
2957
- input: numStr(d.fallback && d.fallback.input),
2958
- output: numStr(d.fallback && d.fallback.output),
2959
- cacheRead: numStr(d.fallback && d.fallback.cacheRead),
2960
- cacheWrite: numStr(d.fallback && d.fallback.cacheWrite)
2961
- });
2962
- setRows((d.pricing || []).map(rowToDraft));
2971
+ if (!cancel) applyCfg(d);
2963
2972
  }).catch((e) => {
2964
2973
  if (!cancel) setMsg({ ok: false, text: String(e && e.message || e) });
2965
2974
  });
@@ -2997,6 +3006,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
2997
3006
  const peaks = (r.peaks || []).map((s) => ({
2998
3007
  start: Number(s.start || 0),
2999
3008
  end: Number(s.end || 0),
3009
+ days: s.days === "workday" || s.days === "nonworkday" ? s.days : "all",
3000
3010
  input: Number(s.input || 0),
3001
3011
  output: Number(s.output || 0),
3002
3012
  cacheRead: Number(s.cacheRead || 0),
@@ -3005,6 +3015,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
3005
3015
  if (peaks.length) out.peaks = peaks;
3006
3016
  return out;
3007
3017
  });
3018
+ payload.holidays = holidaysText.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
3008
3019
  payload.fallback = {
3009
3020
  input: Number(fb.input || 0),
3010
3021
  output: Number(fb.output || 0),
@@ -3014,8 +3025,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
3014
3025
  setSaving(true);
3015
3026
  setMsg(null);
3016
3027
  rpcCall("setpricing", payload).then((d) => {
3017
- setCfg(d);
3018
- setRows((d.pricing || []).map(rowToDraft));
3028
+ applyCfg(d);
3019
3029
  setMsg({ ok: true, text: "\u5355\u4EF7\u5DF2\u4FDD\u5B58\uFF0C\u8D39\u7528\u5DF2\u6309\u65B0\u4EF7\u5373\u65F6\u91CD\u7B97" });
3020
3030
  if (onSaved) onSaved();
3021
3031
  }).catch((e) => setMsg({ ok: false, text: String(e && e.message || e) })).finally(() => setSaving(false));
@@ -3040,9 +3050,15 @@ function PricingEditor({ onClose, onSaved, embedded }) {
3040
3050
  );
3041
3051
  const segLabel = (s) => {
3042
3052
  const a = numStr(s.start), b = numStr(s.end);
3043
- if (a === "" || b === "") return "\u65F6\u6BB5";
3044
- return a + "~" + b + (Number(a) === Number(b) ? "\uFF08\u5168\u5929\uFF09" : Number(a) > Number(b) ? "\uFF08\u8DE8\u96F6\u70B9\uFF09" : "");
3053
+ const dayTag = s.days === "workday" ? "\u5DE5\u4F5C\u65E5" : s.days === "nonworkday" ? "\u5468\u672B/\u8282\u5047\u65E5" : "";
3054
+ const time = a === "" || b === "" ? "\u65F6\u6BB5" : a + "~" + b + (Number(a) === Number(b) ? "\uFF08\u5168\u5929\uFF09" : Number(a) > Number(b) ? "\uFF08\u8DE8\u96F6\u70B9\uFF09" : "");
3055
+ return dayTag ? dayTag + " " + time : time;
3045
3056
  };
3057
+ const daysSelect = (value, onChange) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { className: "dtok-days", value: value || "all", onChange: (e) => onChange(e.target.value), title: "\u8BE5\u65F6\u6BB5\u5728\u54EA\u7C7B\u65E5\u671F\u751F\u6548", children: [
3058
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "all", children: "\u6BCF\u5929" }),
3059
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "workday", children: "\u4EC5\u5DE5\u4F5C\u65E5" }),
3060
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "nonworkday", children: "\u4EC5\u5468\u672B/\u8282\u5047\u65E5" })
3061
+ ] });
3046
3062
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: embedded ? "dtok-price emb" : "dtok-price", onClick: (e) => e.stopPropagation(), children: [
3047
3063
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-price-grid", children: [
3048
3064
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-price-field", children: [
@@ -3054,6 +3070,22 @@ function PricingEditor({ onClose, onSaved, embedded }) {
3054
3070
  "\u5B98\u7F51\u4EF7\u76EE\u81EA\u52A8\u540C\u6B65\uFF08\u5173\u95ED\u540E\u4EC5\u7528\u81EA\u5B9A\u4E49/\u5185\u7F6E\u4EF7\uFF09"
3055
3071
  ] })
3056
3072
  ] }),
3073
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
3074
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-section-title", style: { margin: "0 0 4px" }, children: "\u6CD5\u5B9A\u8282\u5047\u65E5\uFF08\u4F9B\u300C\u4EC5\u5DE5\u4F5C\u65E5\u300D\u5206\u65F6\u6BB5\u5224\u5B9A\uFF09" }),
3075
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-price-field", style: { maxWidth: 420 }, children: [
3076
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { children: "\u6BCF\u884C\u4E00\u6761\uFF1A\u5355\u65E5 2026-10-01\uFF0C\u6216\u533A\u95F4 2026-10-01~2026-10-07\uFF08\u542B\u4E24\u7AEF\uFF09" }),
3077
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3078
+ "textarea",
3079
+ {
3080
+ value: holidaysText,
3081
+ placeholder: "2026-10-01~2026-10-07\n2027-01-01",
3082
+ onChange: (e) => setHolidaysText(e.target.value),
3083
+ spellCheck: false
3084
+ }
3085
+ )
3086
+ ] }),
3087
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-price-hint", children: "\u5468\u672B\uFF08\u5468\u516D/\u5468\u65E5\uFF09\u4E0E\u8FD9\u4E9B\u8282\u5047\u65E5\u89C6\u4E3A\u975E\u5DE5\u4F5C\u65E5\uFF1B\u6807\u4E86\u300C\u4EC5\u5DE5\u4F5C\u65E5\u300D\u7684\u9AD8\u5CF0\u6BB5\u5728\u8FD9\u4E9B\u65E5\u671F\u56DE\u843D\u57FA\u51C6\u4EF7\uFF08\u8C37\u4EF7\uFF09\u3002" })
3088
+ ] }),
3057
3089
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
3058
3090
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-section-title", style: { margin: "0 0 4px" }, children: [
3059
3091
  "\u6309\u6A21\u578B\u914D\u7F6E\u5355\u4EF7\uFF08",
@@ -3092,10 +3124,11 @@ function PricingEditor({ onClose, onSaved, embedded }) {
3092
3124
  ] })
3093
3125
  ] }),
3094
3126
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-price-hint", style: { marginTop: 2 }, children: "\u7F13\u5B58\u5199\u5165\uFF1D\u672A\u547D\u4E2D\u7F13\u5B58\u3001\u672C\u6B21\u65B0\u5EFA\u7F13\u5B58\u7684\u90A3\u90E8\u5206\u8F93\u5165\uFF08Claude \u7B49\u6309\u6EA2\u4EF7\u5355\u72EC\u6536\u5EFA\u7F13\u5B58\u8D39\uFF09\uFF1BDeepSeek \u4E0D\u6536\u6B64\u9879\uFF0C\u586B 0 \u5373\u53EF\u3002" }),
3095
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-price-hint", style: { marginTop: 2 }, children: "\u5206\u65F6\u6BB5\u4EF7\uFF08\u53EF\u591A\u6BB5\uFF0C\u547D\u4E2D\u54EA\u6BB5\u7528\u54EA\u6BB5\uFF1B\u65F6\u6BB5\u5916\u56DE\u843D\u5230\u57FA\u51C6\u4EF7\u3002start>end \u8868\u793A\u8DE8\u96F6\u70B9\uFF0C\u5982 23~7\uFF1Bstart=end \u8868\u793A\u5168\u5929\uFF09" }),
3127
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-price-hint", style: { marginTop: 2 }, children: "\u5206\u65F6\u6BB5\u4EF7\uFF08\u53EF\u591A\u6BB5\uFF0C\u547D\u4E2D\u54EA\u6BB5\u7528\u54EA\u6BB5\uFF1B\u65F6\u6BB5\u5916\u56DE\u843D\u5230\u57FA\u51C6\u4EF7\u3002start>end \u8868\u793A\u8DE8\u96F6\u70B9\uFF0C\u5982 23~7\uFF1Bstart=end \u8868\u793A\u5168\u5929\u3002 \u6BCF\u6BB5\u53EF\u9009\u65E5\u671F\u7C7B\u578B\uFF1A\u6BCF\u5929 / \u4EC5\u5DE5\u4F5C\u65E5\uFF08\u975E\u5468\u672B\u4E14\u975E\u4E0B\u65B9\u8282\u5047\u65E5\uFF09/ \u4EC5\u5468\u672B\u6216\u8282\u5047\u65E5\u2014\u2014\u5982 DeepSeek\u300C\u5DE5\u4F5C\u65E5 9~12\u300114~18 \u53CC\u500D\uFF0C\u5468\u672B\u4E0E\u8282\u5047\u65E5\u5168\u5929\u8C37\u4EF7\u300D\uFF09" }),
3096
3128
  r.peaks.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-price-hint", children: "\uFF08\u672A\u8BBE\u7F6E\u5206\u65F6\u6BB5\uFF0C\u6309\u57FA\u51C6\u4EF7\u8BA1\u8D39\uFF09" }) : null,
3097
3129
  r.peaks.map((s, k) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-pseg", children: [
3098
3130
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-pseg-name", children: segLabel(s) }),
3131
+ daysSelect(s.days, (v) => patchSeg(i, k, { days: v })),
3099
3132
  num(s.start, (v) => patchSeg(i, k, { start: v }), { width: 56 }),
3100
3133
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtok-pseg-tilde", children: "~" }),
3101
3134
  num(s.end, (v) => patchSeg(i, k, { end: v }), { width: 56 }),
@@ -3106,7 +3139,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
3106
3139
  segNum(s.cacheWrite, (v) => patchSeg(i, k, { cacheWrite: v }), "\u5199\u5165"),
3107
3140
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dtok-btn tiny", onClick: () => delSeg(i, k), children: "\u5220\u9664\u65F6\u6BB5" })
3108
3141
  ] }, k)),
3109
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-price-hint", style: { opacity: 0.85 }, children: "\u65F6\u6BB5\u4EF7\u4ECE\u5DE6\u5230\u53F3\uFF1A\u8F93\u5165\uFF08\u547D\u4E2D\uFF09/ \u8F93\u5165\uFF08\u672A\u547D\u4E2D\uFF09/ \u8F93\u51FA / \u7F13\u5B58\u5199\u5165" }),
3142
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtok-price-hint", style: { opacity: 0.85 }, children: "\u65F6\u6BB5\u4EF7\u4ECE\u5DE6\u5230\u53F3\uFF1A\u65E5\u671F\u7C7B\u578B / \u8D77~\u6B62\u65F6 / \u8F93\u5165\uFF08\u547D\u4E2D\uFF09/ \u8F93\u5165\uFF08\u672A\u547D\u4E2D\uFF09/ \u8F93\u51FA / \u7F13\u5B58\u5199\u5165" }),
3110
3143
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dtok-btn tiny", onClick: () => addSeg(i), children: "+ \u6DFB\u52A0\u65F6\u6BB5" })
3111
3144
  ] }, i)),
3112
3145
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtok-price-grid", style: { marginTop: 6 }, children: [
@@ -3165,7 +3198,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
3165
3198
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: b.input }),
3166
3199
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: b.output }),
3167
3200
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: b.cacheWrite }),
3168
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: b.peaks && b.peaks.length ? b.peaks.map((s) => s.start + "~" + s.end).join("\u3001") + "\u65F6" : "\u2014" })
3201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: b.peaks && b.peaks.length ? b.peaks.map((s) => (s.days === "workday" ? "\u5DE5\u4F5C\u65E5" : s.days === "nonworkday" ? "\u5468\u672B/\u8282\u5047" : "") + s.start + "~" + s.end).join("\u3001") + "\u65F6" : "\u2014" })
3169
3202
  ] }, b.match)) })
3170
3203
  ] }) })
3171
3204
  ] }) : null,
@@ -3669,7 +3702,7 @@ var feature = {
3669
3702
  name: "\u7528\u91CF\u8BB0\u5F55",
3670
3703
  order: 110,
3671
3704
  accent: "#fbbf24",
3672
- description: "\u8BB0\u5F55\u5168\u90E8 LLM API \u8C03\u7528\uFF1A\u79D2\u7EA7\u65F6\u95F4\u7B5B\u9009\u3001Token/\u8D39\u7528\u7EDF\u8BA1\uFF08\u53EF\u914D\u7F6E\u5404\u6A21\u578B\u5355\u4EF7\uFF0C\u6301\u4E45\u4FDD\u5B58\u5E76\u6309\u81EA\u586B\u5355\u4EF7\u8BA1\u8D39\uFF1B\u5185\u7F6E\u5CF0\u8C37\u8BA1\u4EF7+\u5B98\u7F51\u4EF7\u76EE\u81EA\u52A8\u540C\u6B65\u4F5C\u515C\u5E95\uFF09\u3001\u5206\u7EC4\u6C47\u603B\u3001\u660E\u7EC6\u68C0\u7D22\u4E0E CSV \u5BFC\u51FA",
3705
+ description: "\u8BB0\u5F55\u5168\u90E8 LLM API \u8C03\u7528\uFF1A\u79D2\u7EA7\u65F6\u95F4\u7B5B\u9009\u3001Token/\u8D39\u7528\u7EDF\u8BA1\uFF08\u53EF\u914D\u7F6E\u5404\u6A21\u578B\u5355\u4EF7\uFF0C\u6301\u4E45\u4FDD\u5B58\u5E76\u6309\u81EA\u586B\u5355\u4EF7\u8BA1\u8D39\uFF1B\u591A\u6BB5\u5206\u65F6\u4EF7\u53EF\u9650\u5B9A\u5DE5\u4F5C\u65E5/\u5468\u672B\u8282\u5047\u65E5\uFF0C\u5185\u7F6E\u5CF0\u8C37\u8BA1\u4EF7+\u5B98\u7F51\u4EF7\u76EE\u81EA\u52A8\u540C\u6B65\u4F5C\u515C\u5E95\uFF09\u3001\u5206\u7EC4\u6C47\u603B\u3001\u660E\u7EC6\u68C0\u7D22\u4E0E CSV \u5BFC\u51FA",
3673
3706
  css,
3674
3707
  View: TokenLogView,
3675
3708
  HomeStat: TokenLogHomeStat,
@@ -11392,7 +11425,7 @@ var feature10 = {
11392
11425
  };
11393
11426
 
11394
11427
  // src/client.jsx
11395
- var DOCK_VERSION = "0.11.6";
11428
+ var DOCK_VERSION = "0.12.0";
11396
11429
  var BUILTIN_FEATURES = [feature, feature2, feature3, feature4, feature5, feature6, feature7, feature8, feature9, feature10];
11397
11430
  var PLANNED_FEATURES = [];
11398
11431
  var PLANNED_NOTES = {};
@@ -21,9 +21,12 @@
21
21
  // fetchOfficial: true
22
22
  // pricing:
23
23
  // - { match: deepseek-v4-flash, input: 1.5, output: 4.5, cacheRead: 0.05, cacheWrite: 0,
24
- // peaks: [{ start: 9, end: 14, input: 3, output: 9, cacheRead: 0.1, cacheWrite: 0 }] }
24
+ // peaks: [{ start: 9, end: 12, days: workday, input: 3, output: 9, cacheRead: 0.1, cacheWrite: 0 },
25
+ // { start: 14, end: 18, days: workday, input: 3, output: 9, cacheRead: 0.1, cacheWrite: 0 }] }
26
+ // holidays: ['2026-10-01~2026-10-07', '2027-01-01'] # 法定节假日(供 days=workday 判定)
25
27
  // 定价优先级:用户自定义单价 > 官网抓取刊例价 > 内置默认表 > 兜底单价。
26
28
  // 分时段价支持多段(start>end 跨零点、start=end 全天、小数小时表示半点);旧单段 peak 自动归一。
29
+ // 每段可带 days=all|workday|nonworkday:workday=非周末且非 holidays;nonworkday=周末或节假日。
27
30
  // 费用在查询/导出时按当前单价即时重算,因此改价无需重扫历史。
28
31
  import { sendJson, readBody, readDockRoot, mutateDockSection } from '../../src/host-core.js'
29
32
 
@@ -70,14 +73,22 @@ export const feature = {
70
73
  let usdCnyRate = 7.2
71
74
  let fallbackPricing = { match: '*', ...DEFAULT_FALLBACK }
72
75
  // 内置定价表(人民币元 / 百万 tokens), 按 model 子串匹配; 未匹配走 fallback。
73
- // 支持分时段定价: 条目可带 peaks=[{start,end,input,output,cacheRead,cacheWrite}, ...],
76
+ // 支持分时段定价: 条目可带 peaks=[{start,end,days,input,output,cacheRead,cacheWrite}, ...],
74
77
  // 命中某段(插件运行机器本地时间)用该段价, 其余时段用基准价; 支持多段与跨零点(start>end)。
75
- // 官方来源: DeepSeek-V4 系列 2026-08-17 生效的分时段刊例(高峰每日 9:00-14:00, 空闲为高峰一半)。
78
+ // days 默认 all(每天);workday=仅工作日(非周末且非配置的法定节假日)。
79
+ // 官方来源(按用户口径): DeepSeek 高峰为工作日 9:00-12:00、14:00-18:00 双倍;
80
+ // 周末与节假日全天为谷价(基准价),高峰外时段也回落基准价。
76
81
  const DEFAULT_PRICING = [
77
82
  { match: 'deepseek-v4-flash', input: 1.5, output: 4.5, cacheRead: 0.05, cacheWrite: 0,
78
- peaks: [{ start: 9, end: 14, input: 3.0, output: 9.0, cacheRead: 0.10, cacheWrite: 0 }] },
83
+ peaks: [
84
+ { start: 9, end: 12, days: 'workday', input: 3.0, output: 9.0, cacheRead: 0.10, cacheWrite: 0 },
85
+ { start: 14, end: 18, days: 'workday', input: 3.0, output: 9.0, cacheRead: 0.10, cacheWrite: 0 },
86
+ ] },
79
87
  { match: 'deepseek-v4-pro', input: 4.5, output: 13.5, cacheRead: 0.15, cacheWrite: 0,
80
- peaks: [{ start: 9, end: 14, input: 9.0, output: 27.0, cacheRead: 0.30, cacheWrite: 0 }] },
88
+ peaks: [
89
+ { start: 9, end: 12, days: 'workday', input: 9.0, output: 27.0, cacheRead: 0.30, cacheWrite: 0 },
90
+ { start: 14, end: 18, days: 'workday', input: 9.0, output: 27.0, cacheRead: 0.30, cacheWrite: 0 },
91
+ ] },
81
92
  { match: 'deepseek-v3', input: 3.6, output: 7.2, cacheRead: 0.72, cacheWrite: 7.2 },
82
93
  { match: 'deepseek-chat', input: 1.94, output: 7.92, cacheRead: 0.5, cacheWrite: 7.92 },
83
94
  { match: 'deepseek-reasoner', input: 3.96, output: 15.77, cacheRead: 1.01, cacheWrite: 7.92 },
@@ -87,6 +98,8 @@ export const feature = {
87
98
  let pricingTable = DEFAULT_PRICING
88
99
  // 用户自定义单价(最高优先)与官网抓取开关/地址——均由 loadPricingConfig() 从 settings 刷新。
89
100
  let userPricing = []
101
+ // 法定节假日(YYYY-MM-DD 或 区间 YYYY-MM-DD~YYYY-MM-DD),供 peaks.days 判定工作日。
102
+ let holidays = []
90
103
  let fetchOfficialEnabled = true
91
104
  let pricingUrl = DEFAULT_PRICING_URL
92
105
  let fetchIntervalMs = DEFAULT_FETCH_INTERVAL_HOURS * 3600000
@@ -94,7 +107,7 @@ export const feature = {
94
107
  function normalizePricing(match, e) {
95
108
  const n = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : 0)
96
109
  const row = { match, input: n(e.input), output: n(e.output), cacheRead: n(e.cacheRead), cacheWrite: n(e.cacheWrite) }
97
- // 分时段价支持多段: peaks=[{start,end,input,output,cacheRead,cacheWrite}, ...]。
110
+ // 分时段价支持多段: peaks=[{start,end,days,input,output,cacheRead,cacheWrite}, ...]。
98
111
  // 兼容旧的单段写法 peak={...}(以及旧版本存盘数据), 统一归一到 peaks。
99
112
  const src = Array.isArray(e.peaks) && e.peaks.length ? e.peaks : (e.peak && typeof e.peak === 'object' ? [e.peak] : [])
100
113
  const peaks = []
@@ -102,6 +115,7 @@ export const feature = {
102
115
  if (!s || typeof s !== 'object') continue
103
116
  peaks.push({
104
117
  start: n(s.start), end: n(s.end),
118
+ days: normalizeDays(s.days),
105
119
  input: n(s.input), output: n(s.output),
106
120
  cacheRead: n(s.cacheRead), cacheWrite: n(s.cacheWrite),
107
121
  })
@@ -110,6 +124,11 @@ export const feature = {
110
124
  return row
111
125
  }
112
126
 
127
+ /** 归一日期类型:仅接受 all/workday/nonworkday,其余(含缺失)一律 all(旧数据零迁移)。 */
128
+ function normalizeDays(v) {
129
+ return v === 'workday' || v === 'nonworkday' ? v : 'all'
130
+ }
131
+
113
132
  /** 浅拷贝分时段数组(RPC 下发与落盘都用它,避免调用方改到内存态)。 */
114
133
  function clonePeaks(peaks) {
115
134
  return Array.isArray(peaks) ? peaks.map((s) => Object.assign({}, s)) : []
@@ -133,19 +152,53 @@ export const feature = {
133
152
  .filter((e) => e && typeof e === 'object' && typeof e.match === 'string' && e.match)
134
153
  .map((e) => normalizePricing(String(e.match).toLowerCase(), e))
135
154
  : []
155
+ holidays = Array.isArray(t.holidays)
156
+ ? t.holidays.filter((h) => typeof h === 'string' && h)
157
+ : []
136
158
  if (t.fallback && typeof t.fallback === 'object') fallbackPricing = normalizePricing('*', t.fallback)
137
159
  } catch (e) {
138
160
  console.error('[dsh-dock] tokenlog read pricing config failed', e && e.message)
139
161
  }
140
162
  }
141
163
 
142
- // 分时段命中: start<end 为普通区间 [start,end); start>end 表示跨零点(如 23~7 点);
164
+ // 本地日期键 YYYY-MM-DD(供节假日区间比较;字符串序即日期序)。
165
+ function dateKey(d) {
166
+ const p = (n) => (n < 10 ? '0' : '') + n
167
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
168
+ }
169
+
170
+ // 是否法定节假日:单日等值,或落入 start~end 区间(含两端)。
171
+ function isHoliday(d, list) {
172
+ const key = dateKey(d)
173
+ for (const h of list || []) {
174
+ const i = h.indexOf('~')
175
+ if (i > 0) {
176
+ if (key >= h.slice(0, i) && key <= h.slice(i + 1)) return true
177
+ } else if (h === key) return true
178
+ }
179
+ return false
180
+ }
181
+
182
+ // 段的日期类型是否匹配:all=每天;workday=非周末且非节假日;nonworkday=周末或节假日。
183
+ function daysMatch(days, d, holidayList) {
184
+ const kind = normalizeDays(days)
185
+ if (kind === 'all') return true
186
+ const w = d.getDay()
187
+ const weekend = w === 0 || w === 6
188
+ const holiday = isHoliday(d, holidayList)
189
+ if (kind === 'workday') return !weekend && !holiday
190
+ return weekend || holiday // nonworkday
191
+ }
192
+
193
+ // 分时段命中: 先过 days(日期类型),再过小时区间。
194
+ // start<end 为普通区间 [start,end); start>end 表示跨零点(如 23~7 点);
143
195
  // start===end 视为全天。多段命中时取第一段(按配置顺序)。
144
- function pickPeakSegment(peaks, hour) {
145
- if (!Array.isArray(peaks) || hour < 0) return null
196
+ function pickPeakSegment(peaks, hour, d, holidayList) {
197
+ if (!Array.isArray(peaks) || hour < 0 || !(d instanceof Date) || Number.isNaN(d.getTime())) return null
146
198
  for (const s of peaks) {
147
199
  const a = Number(s.start), b = Number(s.end)
148
200
  if (!Number.isFinite(a) || !Number.isFinite(b)) continue
201
+ if (!daysMatch(s.days, d, holidayList)) continue
149
202
  const hit = a === b ? true : (a < b ? (hour >= a && hour < b) : (hour >= a || hour < b))
150
203
  if (hit) return s
151
204
  }
@@ -180,13 +233,14 @@ export const feature = {
180
233
  }
181
234
  // 4) 兜底
182
235
  if (!row) { row = fallbackPricing; source = 'fallback' }
183
- // 分时段判断(本地时间, 支持多段与跨零点; 用小数小时以便 08:30 这类半点边界); 未命中用基准价
236
+ // 分时段判断(本地时间, 支持多段/跨零点/days 日期类型; 用小数小时以便 08:30 这类半点边界); 未命中用基准价
184
237
  let h = -1
238
+ let d = null
185
239
  if (typeof timeMs === 'number' && Number.isFinite(timeMs)) {
186
- const d = new Date(timeMs)
240
+ d = new Date(timeMs)
187
241
  h = d.getHours() + d.getMinutes() / 60
188
242
  }
189
- const seg = pickPeakSegment(row.peaks, h)
243
+ const seg = pickPeakSegment(row.peaks, h, d, holidays)
190
244
  const prices = seg
191
245
  ? { input: seg.input, output: seg.output, cacheRead: seg.cacheRead, cacheWrite: seg.cacheWrite }
192
246
  : { input: row.input, output: row.output, cacheRead: row.cacheRead, cacheWrite: row.cacheWrite }
@@ -235,6 +289,7 @@ export const feature = {
235
289
  pricingUrl,
236
290
  pricingFetchIntervalHours: Math.round(fetchIntervalMs / 3600000),
237
291
  pricing: userPricing.map((r) => Object.assign({}, r, { peaks: clonePeaks(r.peaks) })),
292
+ holidays: holidays.slice(),
238
293
  fallback: Object.assign({}, fallbackPricing),
239
294
  builtin: pricingTable.map((r) => ({ match: r.match, input: r.input, output: r.output, cacheRead: r.cacheRead, cacheWrite: r.cacheWrite, peaks: clonePeaks(r.peaks) })),
240
295
  models: [...seen].sort(),
@@ -281,6 +336,7 @@ export const feature = {
281
336
  }
282
337
  // 分时段价(可多段): 每段 start/end 为 0~24 的本地小时(支持 8.5 表示 08:30);
283
338
  // start>end 表示跨零点, start===end 表示全天。
339
+ // days: all|workday|nonworkday —— 周末/节假日谷价时把高峰段标成 workday。
284
340
  const rawPeaks = Array.isArray(raw.peaks) ? raw.peaks : []
285
341
  const peaks = []
286
342
  for (const seg of rawPeaks) {
@@ -289,8 +345,12 @@ export const feature = {
289
345
  if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || start > 24 || end < 0 || end > 24) {
290
346
  throw bad(`「${match}」分时段时刻需为 0~24 的小时数`)
291
347
  }
348
+ const days = seg.days === undefined || seg.days === null || seg.days === '' ? 'all' : String(seg.days)
349
+ if (days !== 'all' && days !== 'workday' && days !== 'nonworkday') {
350
+ throw bad(`「${match}」分时段日期类型需为 all / workday / nonworkday`)
351
+ }
292
352
  peaks.push({
293
- start, end,
353
+ start, end, days,
294
354
  input: nonNeg(seg.input, `${match} 分时段输入单价`),
295
355
  output: nonNeg(seg.output, `${match} 分时段输出单价`),
296
356
  cacheRead: nonNeg(seg.cacheRead, `${match} 分时段缓存命中单价`),
@@ -302,6 +362,25 @@ export const feature = {
302
362
  }
303
363
  next.pricing = rows
304
364
  }
365
+ if (input.holidays !== undefined) {
366
+ if (!Array.isArray(input.holidays)) throw bad('「法定节假日」需为数组')
367
+ const dayRe = /^\d{4}-\d{2}-\d{2}$/
368
+ const rangeRe = /^\d{4}-\d{2}-\d{2}~\d{4}-\d{2}-\d{2}$/
369
+ const list = []
370
+ for (const raw of input.holidays) {
371
+ const h = String(raw == null ? '' : raw).trim()
372
+ if (!h) continue
373
+ if (!dayRe.test(h) && !rangeRe.test(h)) {
374
+ throw bad(`节假日「${h}」格式不符:单日用 2026-10-01,区间用 2026-10-01~2026-10-07`)
375
+ }
376
+ if (rangeRe.test(h)) {
377
+ const [a, b] = h.split('~')
378
+ if (a > b) throw bad(`节假日区间「${h}」起始日期不能晚于结束日期`)
379
+ }
380
+ list.push(h)
381
+ }
382
+ next.holidays = list
383
+ }
305
384
  if (input.fallback !== undefined) {
306
385
  const f = input.fallback
307
386
  if (!f || typeof f !== 'object' || Array.isArray(f)) throw bad('「兜底单价」格式不符')
@@ -4,7 +4,7 @@
4
4
  // - 秒级时间范围查询 + 会话ID/提供商/模型(联动)/状态/推理强度 筛选,条件本地暂存
5
5
  // - 9 张 KPI 卡 + 分组统计表 + 明细表(点击表头排序、会话ID点击即筛选、100 行/页上下双分页)
6
6
  // - 状态列显示 HTTP 状态码徽章,行内【查看详情】弹窗展示完整信息;CSV 导出(中文表头,列序与明细表一致 + 合计行)
7
- // - 独立的「单价设置」子弹窗:按模型配置单价(支持多段分时价)并持久化,费用按自填单价重算
7
+ // - 独立的「单价设置」子弹窗:按模型配置单价(多段分时价 + 日期类型/法定节假日)并持久化,费用按自填单价重算
8
8
  // - 挂载即扫描历史+按暂存条件查询;挂载期间每 5s 静默自动刷新
9
9
  // - 查询等待动画:点「查询」后按钮转圈扫光 + 数据区顶部「记账小队清点」banner(轮换俏皮文案,
10
10
  // 等久了换语气),旧数据压暗禁点;结果到位时闪一次「✓ 数据已更新」并让 KPI 卡回弹
@@ -488,6 +488,9 @@ const css = `
488
488
  .dtok-pseg{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
489
489
  .dtok-pseg-name{font-size:11px;color:var(--dsw-alias-label-secondary);min-width:112px;}
490
490
  .dtok-pseg-tilde{font-size:11px;color:var(--dsw-alias-label-secondary);}
491
+ .dtok-price select{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 6px;font-size:12px;font-family:inherit;}
492
+ .dtok-price textarea{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px;font-size:12px;font-family:inherit;min-height:64px;resize:vertical;width:100%;box-sizing:border-box;}
493
+ .dtok-days{font-size:11px;}
491
494
  .dtok-src{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);}
492
495
  .dtok-src.custom{color:var(--dsw-alias-state-success-primary,#4ade80);}
493
496
  .dtok-src.fallback{color:var(--dk-warn);}
@@ -571,10 +574,12 @@ function Detail({ rec, onClose, rate }) {
571
574
  // 独立子弹窗(可最大化 / 拖动 / 缩放),与功能坞面板解耦,方便配置较长时段表。
572
575
  function numStr(v) { return v === undefined || v === null ? "" : String(v); }
573
576
 
574
- /** 时段草稿:一个 { start,end,input,output,cacheRead,cacheWrite } 的可编辑副本。 */
577
+ /** 时段草稿:一个 { start,end,days,input,output,cacheRead,cacheWrite } 的可编辑副本。 */
575
578
  function segToDraft(s) {
579
+ const d = s && s.days;
576
580
  return {
577
581
  start: numStr(s && s.start), end: numStr(s && s.end),
582
+ days: d === "workday" || d === "nonworkday" ? d : "all",
578
583
  input: numStr(s && s.input), output: numStr(s && s.output),
579
584
  cacheRead: numStr(s && s.cacheRead), cacheWrite: numStr(s && s.cacheWrite),
580
585
  };
@@ -692,24 +697,27 @@ function PricingEditor({ onClose, onSaved, embedded }) {
692
697
  const [rate, setRate] = useState("7.2");
693
698
  const [fetchOn, setFetchOn] = useState(true);
694
699
  const [fb, setFb] = useState({ input: "", output: "", cacheRead: "", cacheWrite: "" });
700
+ const [holidaysText, setHolidaysText] = useState("");
695
701
  const [newMatch, setNewMatch] = useState("");
696
702
  const [msg, setMsg] = useState(null);
697
703
  const [saving, setSaving] = useState(false);
698
704
 
705
+ const applyCfg = (d) => {
706
+ setCfg(d);
707
+ setRate(numStr(d.usdCnyRate));
708
+ setFetchOn(!!d.fetchOfficial);
709
+ setFb({
710
+ input: numStr(d.fallback && d.fallback.input), output: numStr(d.fallback && d.fallback.output),
711
+ cacheRead: numStr(d.fallback && d.fallback.cacheRead), cacheWrite: numStr(d.fallback && d.fallback.cacheWrite),
712
+ });
713
+ setHolidaysText((d.holidays || []).join("\n"));
714
+ setRows((d.pricing || []).map(rowToDraft));
715
+ };
716
+
699
717
  useEffect(() => {
700
718
  let cancel = false;
701
719
  rpcCall("pricing", {})
702
- .then((d) => {
703
- if (cancel) return;
704
- setCfg(d);
705
- setRate(numStr(d.usdCnyRate));
706
- setFetchOn(!!d.fetchOfficial);
707
- setFb({
708
- input: numStr(d.fallback && d.fallback.input), output: numStr(d.fallback && d.fallback.output),
709
- cacheRead: numStr(d.fallback && d.fallback.cacheRead), cacheWrite: numStr(d.fallback && d.fallback.cacheWrite),
710
- });
711
- setRows((d.pricing || []).map(rowToDraft));
712
- })
720
+ .then((d) => { if (!cancel) applyCfg(d); })
713
721
  .catch((e) => { if (!cancel) setMsg({ ok: false, text: String((e && e.message) || e) }); });
714
722
  return () => { cancel = true; };
715
723
  }, []);
@@ -741,12 +749,14 @@ function PricingEditor({ onClose, onSaved, embedded }) {
741
749
  };
742
750
  const peaks = (r.peaks || []).map((s) => ({
743
751
  start: Number(s.start || 0), end: Number(s.end || 0),
752
+ days: s.days === "workday" || s.days === "nonworkday" ? s.days : "all",
744
753
  input: Number(s.input || 0), output: Number(s.output || 0),
745
754
  cacheRead: Number(s.cacheRead || 0), cacheWrite: Number(s.cacheWrite || 0),
746
755
  }));
747
756
  if (peaks.length) out.peaks = peaks;
748
757
  return out;
749
758
  });
759
+ payload.holidays = holidaysText.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
750
760
  payload.fallback = {
751
761
  input: Number(fb.input || 0), output: Number(fb.output || 0),
752
762
  cacheRead: Number(fb.cacheRead || 0), cacheWrite: Number(fb.cacheWrite || 0),
@@ -754,8 +764,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
754
764
  setSaving(true); setMsg(null);
755
765
  rpcCall("setpricing", payload)
756
766
  .then((d) => {
757
- setCfg(d);
758
- setRows((d.pricing || []).map(rowToDraft));
767
+ applyCfg(d);
759
768
  setMsg({ ok: true, text: "单价已保存,费用已按新价即时重算" });
760
769
  if (onSaved) onSaved();
761
770
  })
@@ -778,9 +787,18 @@ function PricingEditor({ onClose, onSaved, embedded }) {
778
787
  );
779
788
  const segLabel = (s) => {
780
789
  const a = numStr(s.start), b = numStr(s.end);
781
- if (a === "" || b === "") return "时段";
782
- return a + "~" + b + (Number(a) === Number(b) ? "(全天)" : Number(a) > Number(b) ? "(跨零点)" : "");
790
+ const dayTag = s.days === "workday" ? "工作日" : s.days === "nonworkday" ? "周末/节假日" : "";
791
+ const time = a === "" || b === "" ? "时段"
792
+ : a + "~" + b + (Number(a) === Number(b) ? "(全天)" : Number(a) > Number(b) ? "(跨零点)" : "");
793
+ return dayTag ? dayTag + " " + time : time;
783
794
  };
795
+ const daysSelect = (value, onChange) => (
796
+ <select className="dtok-days" value={value || "all"} onChange={(e) => onChange(e.target.value)} title="该时段在哪类日期生效">
797
+ <option value="all">每天</option>
798
+ <option value="workday">仅工作日</option>
799
+ <option value="nonworkday">仅周末/节假日</option>
800
+ </select>
801
+ );
784
802
 
785
803
  return (
786
804
  <div className={embedded ? "dtok-price emb" : "dtok-price"} onClick={(e) => e.stopPropagation()}>
@@ -795,6 +813,20 @@ function PricingEditor({ onClose, onSaved, embedded }) {
795
813
  </label>
796
814
  </div>
797
815
 
816
+ <div>
817
+ <div className="dtok-section-title" style={{ margin: "0 0 4px" }}>法定节假日(供「仅工作日」分时段判定)</div>
818
+ <div className="dtok-price-field" style={{ maxWidth: 420 }}>
819
+ <label>每行一条:单日 2026-10-01,或区间 2026-10-01~2026-10-07(含两端)</label>
820
+ <textarea
821
+ value={holidaysText}
822
+ placeholder={"2026-10-01~2026-10-07\n2027-01-01"}
823
+ onChange={(e) => setHolidaysText(e.target.value)}
824
+ spellCheck={false}
825
+ />
826
+ </div>
827
+ <div className="dtok-price-hint">周末(周六/周日)与这些节假日视为非工作日;标了「仅工作日」的高峰段在这些日期回落基准价(谷价)。</div>
828
+ </div>
829
+
798
830
  <div>
799
831
  <div className="dtok-section-title" style={{ margin: "0 0 4px" }}>按模型配置单价({rows.length} 条)</div>
800
832
  {onFallback.length > 0 ? (
@@ -819,12 +851,14 @@ function PricingEditor({ onClose, onSaved, embedded }) {
819
851
  缓存写入=未命中缓存、本次新建缓存的那部分输入(Claude 等按溢价单独收建缓存费);DeepSeek 不收此项,填 0 即可。
820
852
  </div>
821
853
  <div className="dtok-price-hint" style={{ marginTop: 2 }}>
822
- 分时段价(可多段,命中哪段用哪段;时段外回落到基准价。start&gt;end 表示跨零点,如 23~7;start=end 表示全天)
854
+ 分时段价(可多段,命中哪段用哪段;时段外回落到基准价。start&gt;end 表示跨零点,如 23~7;start=end 表示全天。
855
+ 每段可选日期类型:每天 / 仅工作日(非周末且非下方节假日)/ 仅周末或节假日——如 DeepSeek「工作日 9~12、14~18 双倍,周末与节假日全天谷价」)
823
856
  </div>
824
857
  {r.peaks.length === 0 ? <div className="dtok-price-hint">(未设置分时段,按基准价计费)</div> : null}
825
858
  {r.peaks.map((s, k) => (
826
859
  <div className="dtok-pseg" key={k}>
827
860
  <span className="dtok-pseg-name">{segLabel(s)}</span>
861
+ {daysSelect(s.days, (v) => patchSeg(i, k, { days: v }))}
828
862
  {num(s.start, (v) => patchSeg(i, k, { start: v }), { width: 56 })}
829
863
  <span className="dtok-pseg-tilde">~</span>
830
864
  {num(s.end, (v) => patchSeg(i, k, { end: v }), { width: 56 })}
@@ -836,7 +870,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
836
870
  <button className="dtok-btn tiny" onClick={() => delSeg(i, k)}>删除时段</button>
837
871
  </div>
838
872
  ))}
839
- <div className="dtok-price-hint" style={{ opacity: .85 }}>时段价从左到右:输入(命中)/ 输入(未命中)/ 输出 / 缓存写入</div>
873
+ <div className="dtok-price-hint" style={{ opacity: .85 }}>时段价从左到右:日期类型 / 起~止时 / 输入(命中)/ 输入(未命中)/ 输出 / 缓存写入</div>
840
874
  <button className="dtok-btn tiny" onClick={() => addSeg(i)}>+ 添加时段</button>
841
875
  </div>
842
876
  ))}
@@ -871,7 +905,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
871
905
  {cfg.builtin.map((b) => (
872
906
  <tr key={b.match}>
873
907
  <td>{b.match}</td><td>{b.cacheRead}</td><td>{b.input}</td><td>{b.output}</td><td>{b.cacheWrite}</td>
874
- <td>{b.peaks && b.peaks.length ? b.peaks.map((s) => s.start + "~" + s.end).join("、") + "时" : "—"}</td>
908
+ <td>{b.peaks && b.peaks.length ? b.peaks.map((s) => (s.days === "workday" ? "工作日" : s.days === "nonworkday" ? "周末/节假" : "") + s.start + "~" + s.end).join("、") + "时" : "—"}</td>
875
909
  </tr>
876
910
  ))}
877
911
  </tbody>
@@ -1374,7 +1408,7 @@ export const feature = {
1374
1408
  name: "用量记录",
1375
1409
  order: 110,
1376
1410
  accent: "#fbbf24",
1377
- description: "记录全部 LLM API 调用:秒级时间筛选、Token/费用统计(可配置各模型单价,持久保存并按自填单价计费;内置峰谷计价+官网价目自动同步作兜底)、分组汇总、明细检索与 CSV 导出",
1411
+ description: "记录全部 LLM API 调用:秒级时间筛选、Token/费用统计(可配置各模型单价,持久保存并按自填单价计费;多段分时价可限定工作日/周末节假日,内置峰谷计价+官网价目自动同步作兜底)、分组汇总、明细检索与 CSV 导出",
1378
1412
  css,
1379
1413
  View: TokenLogView,
1380
1414
  HomeStat: TokenLogHomeStat,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-dock",
3
- "version": "0.11.6",
4
- "description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价与多段分时价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
3
+ "version": "0.12.0",
4
+ "description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价、多段分时价与周末/节假日谷价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "files": [
package/src/client.jsx CHANGED
@@ -30,7 +30,7 @@ import { feature as fGames } from "../features/games/view.jsx";
30
30
  import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
31
31
 
32
32
  const name = "dsh-dock";
33
- const DOCK_VERSION = "0.11.6";
33
+ const DOCK_VERSION = "0.12.0";
34
34
 
35
35
  // ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
36
36
  const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay];
package/src/host-core.js CHANGED
@@ -51,7 +51,9 @@ export const DockConfig = z.object({
51
51
  cacheRead: z.number().default(0),
52
52
  cacheWrite: z.number().default(0),
53
53
  /** 分时段价(可多段,如 DeepSeek 的高峰/优惠时段):命中某段用该段价,否则用上面的基准价。
54
- * start/end 为本地小时数 0~24;start>end 表示跨零点;单段等价旧的 peak。 */
54
+ * start/end 为本地小时数 0~24;start>end 表示跨零点;单段等价旧的 peak。
55
+ * days:日期类型——all=每天(默认);workday=仅工作日(非周末且非下方 holidays);
56
+ * nonworkday=仅周末或节假日。用于「周末/节假日谷价、仅工作日高峰」这类计价。 */
55
57
  peaks: z.array(z.object({
56
58
  start: z.number().default(0),
57
59
  end: z.number().default(0),
@@ -59,8 +61,12 @@ export const DockConfig = z.object({
59
61
  output: z.number().default(0),
60
62
  cacheRead: z.number().default(0),
61
63
  cacheWrite: z.number().default(0),
64
+ days: z.string().default('all'),
62
65
  })).default([]),
63
66
  })).default([]),
67
+ /** 法定节假日:单日 YYYY-MM-DD 或区间 YYYY-MM-DD~YYYY-MM-DD(含两端)。
68
+ * 供 peaks.days=workday/nonworkday 判断;周末(周六/周日)始终视为非工作日。 */
69
+ holidays: z.array(z.string()).default([]),
64
70
  /** 未匹配任何条目的模型所用兜底单价。 */
65
71
  fallback: z.object({
66
72
  input: z.number().default(2.16),