dsh-api-dashboard 1.4.3 → 1.4.5

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
@@ -125,7 +125,7 @@ dsh plugin --profile web add link:/root/dsha-api-dashboard
125
125
 
126
126
  ## 更新日志
127
127
 
128
- 最近一版 **v1.4.3** —— 子代理胶囊只显示**它自己新产生**的消耗(不再把父会话继承来的历史、兄弟或后代子代理的消耗算进去)。一版一行的完整历史见 [`CHANGELOG.md`](CHANGELOG.md)。
128
+ 最近一版 **v1.4.5** —— 消耗估算口径三处修正:**峰谷改按「事件发生时刻」计价**(旧实现按「打开面板那一刻」,谷时跑的量在峰时查看会被翻倍)、**法定节假日全天算谷时**、**缓存写入按官方 ×125% 单独计价**;同时把十一家厂商的价格表逐条对官方原文重核,并补录 40+ 条新模型(GPT-6 Astra、Claude Fable 5.1、GLM-5.3-FlashX、MiniMax-M3、Gemini 3.5 Flash、Grok 4.7 / 4.6、MiMo V2.6 等);另修「点设置抽搐一下」与刷新间隔 300 秒两处交互根因。一版一行的完整历史见 [`CHANGELOG.md`](CHANGELOG.md)。
129
129
 
130
130
  ## License
131
131
 
package/client/client.js CHANGED
@@ -1445,7 +1445,7 @@ window.__ModuleLoader__.load({
1445
1445
  //#endregion
1446
1446
 
1447
1447
  //#region 设置面板 (安全阈值 + 中转站)
1448
- function SettingsModal({ isOpen, onClose, onBack, t, config, initialSection }) {
1448
+ function SettingsModal({ isOpen, onClose, onBack, t, config, initialSection, underScrim }) {
1449
1449
  const [section, setSection] = react.useState(initialSection || "basic");
1450
1450
  const [relays, setRelays] = react.useState([]);
1451
1451
  const [models, setModels] = react.useState([]);
@@ -1938,21 +1938,30 @@ window.__ModuleLoader__.load({
1938
1938
  ]),
1939
1939
  // v1.3.3: 更新结果改为醒目横幅 —— 原来的 11px 灰色字在移动端几乎看不到,
1940
1940
  // 用户点完「一键更新」后以为没反应。成功=绿色底, 失败=红色底, 检查有新版=原来的灰色。
1941
- upd.msg ? react.createElement("div", { key: "upd_msg", style: {
1942
- color: upd.phase === "fail" ? "#b33" : upd.phase === "done" ? "#1a7a3a" : "#777b84",
1943
- background: upd.phase === "fail" ? "#fde8e8" : upd.phase === "done" ? "#e8fde8" : "transparent",
1944
- fontSize: upd.phase === "done" || upd.phase === "fail" ? "13px" : "11px",
1945
- fontWeight: upd.phase === "done" || upd.phase === "fail" ? "600" : "400",
1946
- padding: upd.phase === "done" || upd.phase === "fail" ? "8px 10px" : "0",
1947
- borderRadius: upd.phase === "done" || upd.phase === "fail" ? "6px" : "0",
1948
- marginTop: "8px",
1949
- lineHeight: "1.4"
1950
- } }, upd.msg) : (upd.info?.hasUpdate ? react.createElement("div", { key: "upd_msg", style: { color: "#777b84", fontSize: "11px", marginTop: "6px" } },
1951
- t("update.available") + " v" + upd.info.remote) : null),
1941
+ // 2026-09-22: 外面这层是**固定高度槽位**。原来结果行是条件渲染, 打开面板时它还不存在,
1942
+ // checkUpdate 回来才插入(约 23px) —— 而面板是 position:fixed + bottom:0 + 高度随内容,
1943
+ // 于是「上边缘往上跳一下」; 本地请求 <20ms, 往往在滑入动画(.22s)期间就填充 边滑边长。
1944
+ // 槽位恒定后结果填进同一高度, 打开过程中版面零位移。
1945
+ react.createElement("div", { key: "upd_slot", style: { minHeight: "16px", marginTop: "8px" } }, [
1946
+ upd.msg ? react.createElement("div", { key: "upd_msg", style: {
1947
+ color: upd.phase === "fail" ? "#b33" : upd.phase === "done" ? "#1a7a3a" : "#777b84",
1948
+ background: upd.phase === "fail" ? "#fde8e8" : upd.phase === "done" ? "#e8fde8" : "transparent",
1949
+ fontSize: upd.phase === "done" || upd.phase === "fail" ? "13px" : "11px",
1950
+ fontWeight: upd.phase === "done" || upd.phase === "fail" ? "600" : "400",
1951
+ padding: upd.phase === "done" || upd.phase === "fail" ? "8px 10px" : "0",
1952
+ borderRadius: upd.phase === "done" || upd.phase === "fail" ? "6px" : "0",
1953
+ lineHeight: "1.4"
1954
+ } }, upd.msg) : (upd.info?.hasUpdate ? react.createElement("div", { key: "upd_msg", style: { color: "#777b84", fontSize: "11px" } },
1955
+ t("update.available") + " v" + upd.info.remote) : null),
1956
+ ]),
1952
1957
  ]),
1953
1958
  ]),
1954
1959
  ]);
1955
- return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, [
1960
+ // 2026-09-22: 从看板打开设置时, 下层 DashboardDrawer .dshadb_scrim **仍在**(S1 的修法就是保留它),
1961
+ // 而两层遮罩同为 rgba(0,0,0,0.35) → 叠加后总暗度 1-(0.65)² ≈ 0.58, 打开瞬间背景从 0.35 渐暗到 0.58
1962
+ // (dshadb-fadein .15s), 看上去「又抽了一下」。已知下层已有遮罩时本层不再重复加深 ——
1963
+ // 只把背景设为透明, scrim 本身保留(继续拦截点击 / 点空白关闭)。
1964
+ return react.createElement("div", { className: "dshadb_scrim", style: underScrim ? { background: "transparent" } : undefined, onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, [
1956
1965
  react.createElement("div", { className: "dshadb_swipeguard", "aria-hidden": "true", key: "swipeguard" }),
1957
1966
  output,
1958
1967
  ]);
@@ -2620,7 +2629,12 @@ window.__ModuleLoader__.load({
2620
2629
  const openDetail = (id) => { setDetailId(id); setView("detail"); };
2621
2630
  const closeDetail = () => setView("list");
2622
2631
  const closeList = () => setView("bar");
2623
- const openSettings = (section) => { setSettingsSection(section || "basic"); setView("bar"); setSettingsOpen(true); };
2632
+ // 2026-09-22: 原来这里是 setView("bar") + setSettingsOpen(true) 同帧生效 —— 从看板点设置时,
2633
+ // DashboardDrawer 会被**当场卸载**、SettingsModal 同时挂载, 两块全屏遮罩一换一, 而 drawer
2634
+ // 的 .dshadb_scrim 只有入场动画没有出场动画 → 视觉上「啪」一下(维护者反馈「点设置会抽搐一下」)。
2635
+ // 改为: 打开设置时**不**收看板(设置面板照常盖在它上面), 关闭设置时再收 —— 交互结果不变
2636
+ // (关掉设置后一样回到状态条), 但中间那次同帧卸载/挂载消失了。
2637
+ const openSettings = (section) => { setSettingsSection(section || "basic"); setSettingsOpen(true); };
2624
2638
 
2625
2639
  // v0.6.0: 启动自动检查更新 (一次静默请求, 服务端 5 分钟缓存; 结果供设置弹窗直接显示)
2626
2640
  react.useEffect(() => {
@@ -2656,7 +2670,8 @@ window.__ModuleLoader__.load({
2656
2670
  react.createElement(BarReadout, { t, onOpen: openList, onOpenSettings: () => openSettings("basic"), selectedId, onSelect: handleSelect, config, cost: costDisp, provider: curProvider, key: "bar" }),
2657
2671
  view === "list" ? react.createElement(DashboardDrawer, { isOpen: true, onClose: closeList, t, selectedId, onSelect: handleSelect, onOpenDetail: (id) => { setDetailId(id); setView("detail"); }, onAddRelay: () => openSettings("relays"), onAddCustom: () => openSettings("models"), onOpenSettings: () => openSettings("basic"), config, currentModel, key: "drawer" }) : null,
2658
2672
  view === "detail" ? react.createElement(PlatformDetail, { isOpen: true, onClose: closeDetail, platformId: detailId, t, useProjection, config, key: "detail" }) : null,
2659
- isSettingsOpen ? react.createElement(SettingsModal, { isOpen: true, onClose: () => setSettingsOpen(false), onBack: () => { setSettingsOpen(false); setView("list"); }, t, config, initialSection: settingsSection, key: "settings" }) : null,
2673
+ // 2026-09-22: underScrim —— view list/detail 时下层已有全屏遮罩, 设置面板不再叠加第二层 ( SettingsModal)
2674
+ isSettingsOpen ? react.createElement(SettingsModal, { isOpen: true, underScrim: view !== "bar", onClose: () => { setSettingsOpen(false); setView("bar"); }, onBack: () => { setSettingsOpen(false); setView("list"); }, t, config, initialSection: settingsSection, key: "settings" }) : null,
2660
2675
  ]),
2661
2676
  subRow,
2662
2677
  ]);
package/cordis.patch.yml CHANGED
@@ -3,6 +3,6 @@
3
3
  - id: dsh-api-dashboard
4
4
  name: dsh-api-dashboard
5
5
  config:
6
- refreshIntervalMs: 300000
7
- clientPollIntervalMs: 30000
6
+ refreshIntervalMs: 5000
7
+ clientPollIntervalMs: 5000
8
8
  timeoutMs: 8000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-api-dashboard",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "DeepSeek Harness 多平台 API 余额 / 用量看板:输入框下方实时显示各平台余额与本会话估算消耗",
5
5
  "keywords": [
6
6
  "dsh",
package/src/index.js CHANGED
@@ -618,6 +618,17 @@ export const planBalancesFetch = ({ force = false, peek = false, hasData = false
618
618
  /** v1.4.0: 刷新间隔白名单化 —— 1~60 秒 (下限由 5 秒放宽到 1 秒, 用户要求更快) */
619
619
  export const clampRefreshSec = (v) => Math.min(Math.max(Math.round(Number(v) || 1), 1), 60)
620
620
 
621
+ /**
622
+ * 毫秒级夹取 (2026-09-22 补漏): sanitizePersistedShape 只夹**状态文件**读出来的值,
623
+ * 而从 config (cordis.patch.yml / profile 配置) 进来的值**没有任何夹取** ——
624
+ * 于是 patch 里写死的 refreshIntervalMs: 300000 (300 秒) 会绕过 clampRefreshSec 直接生效,
625
+ * 用户看到「刷新间隔 300 秒」。这里给 config 这条路补上同一道闸门。
626
+ */
627
+ export const clampMs = (v, min, max, fallback) => {
628
+ const n = Number(v)
629
+ return Number.isFinite(n) ? Math.min(Math.max(Math.round(n), min), max) : fallback
630
+ }
631
+
621
632
  /**
622
633
  * 第 2 层自动判定的补充素材: 提取 settings.yaml 里 `llm-pi-ai.providers.<name>` 的
623
634
  * **全部 provider 名**,包括没有写 baseURL 的 provider。这样即使别人没有手写 URL,
@@ -755,10 +766,14 @@ const fnv1a = (str) => {
755
766
  // 北京时间 09:00~12:00 / 14:00~18:00 为峰时(100%), 其余时段谷时特惠(5折)
756
767
  // ============================================================
757
768
  // v1.3.4 (2026-09-10): 官方同日 12:00 起调整 Flash 系列定价(最高降幅 60%), 并收敛模型名 ——
758
- // deepseek-v4-flash → deepseek-flash (旧名仍可调用, 由 V4.1-Flash 服务并按 Flash 价计费, 定价页注 1);
759
- // 2026-09-14 12:00 deepseek-v4-pro 的请求将全部路由到 V4.1-Flash 并按 Flash 价计费(官方计划下线 Pro, 注 2)。
769
+ // deepseek-v4-flash → deepseek-flash (旧名仍可调用, 由 V4.1-Flash 服务并按 Flash 价计费, 定价页注 1)
770
+ // ⚠️ 2026-09-22 复核更正: 官方注(2) 已改写为「为响应广大用户的需求, 我们决定在 2026 9 14 日之后
771
+ // 继续提供 DeepSeek V4 Pro 的 API 调用服务, 计费方式保持不变」—— **Pro 档仍在售、仍按 Pro 价计费**,
772
+ // 故本表 pro 档保留正确。此前「9-14 后 pro 请求全部路由到 Flash 并按 Flash 价计费」的记录已作废, 勿再回填。
760
773
  // 来源: https://api-docs.deepseek.com/zh-cn/quick_start/pricing (CNY) 与 /quick_start/pricing (USD) ——
761
- // USD 表为官方直发(非 ÷7 换算, 实际口径约 1 USD ≈ 6.67 CNY), pro 档与调整前一致, 未变动。
774
+ // USD 表为官方直发(非 ÷7 换算, 实际口径约 1 USD ≈ 6.67 CNY), 已逐项核对与表内值一致 (2026-09-22)。
775
+ // ⚠️ 官方英文页明确: 高峰时段为 UTC 01:00-04:00 / 06:00-10:00 (北京 9-12 / 14-18) 周一至周五,
776
+ // **excluding Chinese public holidays**(不含中国法定节假日) —— 见 isPeakTime 的已知缺口。
762
777
  export const V4_RATES = {
763
778
  CNY: {
764
779
  peak: { 'deepseek-flash': { cacheHit: 0.04, cacheMiss: 2, output: 8 }, 'deepseek-v4-pro': { cacheHit: 0.3, cacheMiss: 9, output: 27 } },
@@ -778,10 +793,41 @@ export const V4_RATES = {
778
793
  */
779
794
  const USD_TO_CNY_RATE = 7
780
795
 
781
- /** 北京时间(UTC+8)的星期与小时, 先 +8h 再取值, 避免跨日界(00:00~08:00)星期比北京时间早一天 */
796
+ /**
797
+ * 中国法定节假日 (2026 年) —— DeepSeek 官方定价页**英文版**明确:
798
+ * "Peak hours are 01:00 - 04:00 and 06:00 - 10:00 UTC, Monday through Friday,
799
+ * **excluding Chinese public holidays**. All other hours are off-peak, including weekends
800
+ * and Chinese public holidays in full."
801
+ * 即法定节假日**全天按空闲(谷)价**计费。这不是可选项: 漏掉它会在这 33 天里把消耗按峰价高估一倍。
802
+ * 来源: 国务院办公厅《关于2026年部分节假日安排的通知》(2025-11-04 发布)
803
+ * https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm
804
+ * ⚠️ 维护: 国务院办每年 11 月公布**次年**安排 —— 跨年前必须补下一年表; 表外年份按"无节假日"回落旧行为(保守)。
805
+ * ⚠️ 调休上班的周末(2026 年为 02-14 / 02-28 / 05-09 / 09-20 / 10-10)不列入本表:
806
+ * 官方口径是 "Monday through Friday" 字面 —— 周末本身就已是谷时, 无需专门处理。
807
+ */
808
+ const CN_HOLIDAY_RANGES = {
809
+ 2026: [
810
+ ['2026-01-01', '2026-01-03'], // 元旦 3 天
811
+ ['2026-02-15', '2026-02-23'], // 春节 9 天
812
+ ['2026-04-04', '2026-04-06'], // 清明节 3 天
813
+ ['2026-05-01', '2026-05-05'], // 劳动节 5 天
814
+ ['2026-06-19', '2026-06-21'], // 端午节 3 天
815
+ ['2026-09-25', '2026-09-27'], // 中秋节 3 天
816
+ ['2026-10-01', '2026-10-07'], // 国庆节 7 天
817
+ ],
818
+ }
819
+
820
+ /** 该北京日期 (yyyy-mm-dd) 是否落在法定节假日区间内。未收录的年份返回 false (不表态)。 */
821
+ export const isCnHoliday = (ymd) => {
822
+ const ranges = CN_HOLIDAY_RANGES[Number(String(ymd).slice(0, 4))]
823
+ if (!Array.isArray(ranges)) return false
824
+ return ranges.some(([from, to]) => ymd >= from && ymd <= to)
825
+ }
826
+
827
+ /** 北京时间(UTC+8)的日期/星期/小时, 先 +8h 再取值, 避免跨日界(00:00~08:00)星期比北京时间早一天 */
782
828
  const bjtParts = (timestamp) => {
783
829
  const d = new Date(timestamp + 8 * 3600 * 1000)
784
- return { weekday: d.getUTCDay(), hour: d.getUTCHours() }
830
+ return { weekday: d.getUTCDay(), hour: d.getUTCHours(), ymd: d.toISOString().slice(0, 10) }
785
831
  }
786
832
 
787
833
  /**
@@ -790,13 +836,15 @@ const bjtParts = (timestamp) => {
790
836
  * 周末(周六日): 整天都是谷时特惠。
791
837
  */
792
838
  export const isPeakTime = (timestamp = Date.now()) => {
793
- const { weekday, hour } = bjtParts(timestamp)
839
+ const { weekday, hour, ymd } = bjtParts(timestamp)
794
840
  // 周末(0=周日, 6=周六)整天谷时
795
841
  if (weekday === 0 || weekday === 6) return false
842
+ // 法定节假日全天谷时 (官方英文定价页: "excluding Chinese public holidays ... in full")
843
+ if (isCnHoliday(ymd)) return false
796
844
  return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18)
797
845
  }
798
846
 
799
- /** 当前是否周末 */
847
+ /** 当前是否周末 (注意: 周末与法定节假日是两回事, 峰谷判定请用 isPeakTime) */
800
848
  export const isWeekend = (timestamp = Date.now()) => {
801
849
  const { weekday } = bjtParts(timestamp)
802
850
  return weekday === 0 || weekday === 6
@@ -831,27 +879,76 @@ export const MODEL_PRICES = {
831
879
  // 来源: modelradar.cn 2026-09-03 快照 (各模型 sourceUrl 均指官方定价页)。
832
880
  // 仅采纳与官方口径无分歧的条目; 与原表冲突时保留原值并注明 ——
833
881
  // radar 的 GPT-5.6 系输出价全呈「输入×1.25」异常模式, 疑似抓错列, 未采纳。
834
- // ⚠️ OpenAI / Anthropic / Gemini 官方定价页在本容器环境被 403 / 地域封锁, v1.4.0 未能取到原文复核,
835
- // 下列海外条目仍为 radar/hermes-agent 二手源, 未逐条核实 —— 有账单单据时优先以单据为准。
882
+ // ⚠️ 上注为 v1.4.0 时期状况。v1.4.5 (2026-09-22) 起: OpenAI / Anthropic / **Gemini** 三家均已用官方原文逐条核对
883
+ // (OpenAI / Anthropic 的价格藏在页面内嵌转义 JSON 里; Gemini 走 ai.google.dev 定价页的 Standard 档表),
884
+ // 并**整段补录 xAI Grok** —— 此前表内没有任何 grok 键, 全部落 defaultPrices。
885
+ // 下列海外条目现为官方现价; 有账单单据时仍以单据为准。
836
886
  // OpenAI GPT-5.6 系列 (radar 报 sol 输出 $5 / terra $2.5 / luna $0.25, 均为输入×1.25 异常模式, 未采纳)
837
- 'gpt-5.6-sol': { cacheHit: 0.5, cacheMiss: 4.0, output: 20.0 }, // 临时促销价(至少到 2026-11-21)
887
+ // —— 2026-09-22 官方 API 定价页 (openai.com/api/pricing 的内嵌 JSON) 逐条核对 ——
888
+ // 注: 该页 HTML 里的价格藏在 <script> 的双层转义 JSON 中, 不在 <table> 里; 本容器内 chromium 无网络渲染不了,
889
+ // 但直接取页面 + 还原转义即可拿到官方原文 (无需代理)。同一页还列出 GPT-Image-2.5 / GPT-Live-1 / GPT-Realtime-2.1 mini。
890
+ // ⚠️ 官方现列表价 $5/$30, 已无促销标记 —— 旧值 4/20 记的是"临时促销价", 按 AGENTS.md 红线(促销必须有官方原文兜底)改用官方现价。
891
+ 'gpt-6-astra': { cacheHit: 1.0, cacheMiss: 10.0, output: 50.0 }, // 官方 Input $10 / Cached input $1 / Output $50 (官方页当前最强档)
892
+ 'gpt-5.6-sol': { cacheHit: 0.5, cacheMiss: 5.0, output: 30.0 }, // 官方 $5 / $0.5 / $30
838
893
  'gpt-5.6-terra': { cacheHit: 0.2, cacheMiss: 2.0, output: 12.0 }, // 2026-07-30 降价
839
894
  'gpt-5.6-luna': { cacheHit: 0.02, cacheMiss: 0.2, output: 1.2 }, // 2026-07-30 降价
840
- 'gpt-5.3-codex': { cacheHit: 0.175, cacheMiss: 1.75, output: 14.0 }, // radar 2026-09-03, OpenAI 官方页
895
+ 'gpt-5.3-codex': { cacheHit: 0.175, cacheMiss: 1.75, output: 14.0 }, // ⚠️ 2026-09-22 复核: OpenAI 官方 API 定价页现**未列**此模型(页面只列 GPT-6 Astra / GPT-5.6 三档 / 图像 / Live / Realtime), 值仍为 radar 二手源, 未能取证
841
896
  // Anthropic Claude 5
842
- 'claude-opus-5': { cacheHit: 0.5, cacheMiss: 5.0, output: 25.0 }, // v1.2.0 修正缓存读价: Anthropic 缓存读=0.1×输入, radar 对照 claude.com/pricing (opus-4-8 亦 $0.5); 原误标"无缓存折扣"
843
- 'claude-sonnet-5': { cacheHit: 0.2, cacheMiss: 2.0, output: 10.0 },
844
- 'claude-sonnet-4-6': { cacheHit: 0.30, cacheMiss: 3.00, output: 15.00 },
845
- 'claude-haiku-4-5': { cacheHit: 0.10, cacheMiss: 1.00, output: 5.00 },
846
- // Google Gemini 3.x
847
- 'gemini-3.7-flash': { cacheHit: 0.075, cacheMiss: 0.75, output: 3.75 }, // 促销至 2026-12-31, 之后翻倍
848
- 'gemini-3.8-flash': { cacheHit: 0.075, cacheMiss: 0.75, output: 3.75 }, // radar 2026-09-02 新增, 与 3.7/3.6 同价
849
- 'gemini-3.6-flash': { cacheHit: 0.075, cacheMiss: 0.75, output: 3.75 }, // 促销至 2026-12-31, 之后翻倍
850
- 'gemini-3-flash-preview': { cacheHit: 0.025, cacheMiss: 0.5, output: 3.0 },
851
- 'gemini-3.5-flash-lite': { cacheHit: 0.3, cacheMiss: 0.3, output: 2.5 }, // 无缓存折扣
852
- 'gemini-3.1-pro': { cacheHit: 2.0, cacheMiss: 2.0, output: 12.0 }, // 无缓存折扣; 长上下文 $4/$24
853
- 'gemini-2.5-pro': { cacheHit: 0.125, cacheMiss: 1.25, output: 10.00 },
854
- 'gemini-2.5-flash': { cacheHit: 0.03, cacheMiss: 0.3, output: 2.5 }, // radar 2026-09-03, 1M ctx
897
+ 'claude-opus-5': { cacheHit: 0.5, cacheMiss: 5.0, cacheWrite: 6.25, output: 25.0 }, // v1.2.0 修正缓存读价: Anthropic 缓存读=0.1×输入, radar 对照 claude.com/pricing (opus-4-8 亦 $0.5); 原误标"无缓存折扣"
898
+ 'claude-sonnet-5': { cacheHit: 0.2, cacheMiss: 2.0, cacheWrite: 2.5, output: 10.0 },
899
+ 'claude-sonnet-4-6': { cacheHit: 0.30, cacheMiss: 3.00, cacheWrite: 3.75, output: 15.00 },
900
+ 'claude-haiku-4-5': { cacheHit: 0.10, cacheMiss: 1.00, cacheWrite: 1.25, output: 5.00 },
901
+ // —— 2026-09-22 官方 API 定价页 (anthropic.com/pricing 的 ApiTab) 核对后补录 ——
902
+ // 官方**同时**给出 Prompt caching Read/Write 两档: Write 一律 = 输入价 ×125%
903
+ // (Opus 5 $6.25/$5、Sonnet 5 $2.50/$2、Haiku 4.5 $1.25/$1、Fable 5.1 $12.50/$10 全部吻合),
904
+ // Read 多数 = 输入价 ×10%(本表 cacheHit 口径), **Fable 5.1 例外仅 2.5%**。
905
+ // ⚠️ 现有 claude-opus-5 / sonnet-5 / sonnet-4-6 / haiku-4-5 四条经核对与官方逐项一致, 未改动。
906
+ 'claude-fable-5.1': { cacheHit: 0.25, cacheMiss: 10.0, cacheWrite: 12.5, output: 50.0 }, // 官方 $10 / $0.25(缓存读仅 2.5%) / $50; 缓存写 $12.50
907
+ 'claude-fable-5': { cacheHit: 1.0, cacheMiss: 10.0, cacheWrite: 12.5, output: 50.0 }, // 官方 $10 / $1 / $50; 缓存写 $12.50
908
+ 'claude-opus-4-8': { cacheHit: 0.5, cacheMiss: 5.0, cacheWrite: 6.25, output: 25.0 }, // 官方 $5 / $0.5 / $25; 缓存写 $6.25
909
+ 'claude-sonnet-4-5': { cacheHit: 0.30, cacheMiss: 3.0, cacheWrite: 3.75, output: 15.0 }, // 官方 $3 / $0.30 / $15; 缓存写 $3.75
910
+ // —— Google Gemini (2026-09-22 官方页 ai.google.dev/gemini-api/docs/pricing 的 Standard 档 Text API 逐条核对) ——
911
+ // 官方口径: Context caching price 一律 = 输入价 **10%**(3.7/3.8/3.6 Flash $0.075/$0.75、3.5 Flash $0.15/$1.50、
912
+ // 3.5 Flash-Lite $0.03/$0.30、3.1 Flash-Lite $0.025/$0.25、3.1 Pro $0.20/$2.00、3 Flash Preview $0.05/$0.50、
913
+ // 2.5 Pro $0.125/$1.25、2.5 Flash $0.03/$0.30、2.5 Flash-Lite $0.01/$0.10 全部吻合)。
914
+ // ⇒ 凡官方页**列出** Context caching price 的条目一律按官方值入库。旧表把 3 条标成「无缓存折扣」是错的(见下三条 ✗→✓)。
915
+ // ⚠️ 促销价: 3.6/3.7/3.8 Flash 官方明文 "through December 31, 2026", 到期翻倍($1.50/$7.50/缓存 $0.15) —— 有到期日兜底才可入库。
916
+ // ⚠️ 分档: 3.1 Pro / 2.5 Pro 官方另有 >200k 档, 本表取主档(与 Claude 同口径), 更贵档见各条注释。
917
+ 'gemini-3.8-flash': { cacheHit: 0.075, cacheMiss: 0.75, output: 3.75 }, // 官方 $0.75/$3.75/缓存 $0.075; 促销至 2026-12-31, 之后 $1.50/$7.50
918
+ 'gemini-3.7-flash': { cacheHit: 0.075, cacheMiss: 0.75, output: 3.75 }, // 同上
919
+ 'gemini-3.6-flash': { cacheHit: 0.075, cacheMiss: 0.75, output: 3.75 }, // 同上
920
+ 'gemini-3.5-flash': { cacheHit: 0.15, cacheMiss: 1.5, output: 9.0 }, // 官方 $1.50/$9.00/缓存 $0.15 ✚补录: 旧表无此键 → 落默认价($1/$2), 输出低估 4.5 倍
921
+ 'gemini-3.5-flash-lite': { cacheHit: 0.03, cacheMiss: 0.3, output: 2.5 }, // 官方 $0.30/$2.50/缓存 $0.03 ✗→✓ 旧 cacheHit 0.3 且标「无缓存折扣」, 官方页明列 $0.03 → 高估缓存读 10 倍
922
+ 'gemini-3.1-flash-lite': { cacheHit: 0.025, cacheMiss: 0.25, output: 1.5 }, // 官方 $0.25(text/图/视频)/$1.50/缓存 $0.025 ✚补录
923
+ 'gemini-3-flash-preview': { cacheHit: 0.05, cacheMiss: 0.5, output: 3.0 }, // 官方 $0.50/$3.00/缓存 $0.05 ✗→✓ 旧 0.025 = 输入的 5%, 官方是 10%
924
+ 'gemini-3.1-pro': { cacheHit: 0.20, cacheMiss: 2.0, output: 12.0 }, // 官方 $2.00/$12.00/缓存 $0.20 ✗→✓ 旧 cacheHit 2.0 且标「无缓存折扣」→ 高估缓存读 10 倍; >200k 档 $4/$18/缓存 $0.40
925
+ 'gemini-omni-flash': { cacheHit: 1.5, cacheMiss: 1.5, output: 9.0 }, // 官方 $1.50/$9.00(text) ✚补录; 官方页**未列**缓存价 → 按输入价计, 不假设折扣(video 输出 $17.50 未建模)
926
+ 'gemini-2.5-pro': { cacheHit: 0.125, cacheMiss: 1.25, output: 10.00 }, // 官方 $1.25/$10.00/缓存 $0.125; >200k 档 $2.50/$15.00/缓存 $0.25
927
+ 'gemini-2.5-flash': { cacheHit: 0.03, cacheMiss: 0.3, output: 2.5 }, // 官方 $0.30(text/图/视频; audio $1.00)/$2.50/缓存 $0.03
928
+ 'gemini-2.5-flash-lite':{ cacheHit: 0.01, cacheMiss: 0.10, output: 0.40 }, // 官方 $0.10/$0.40/缓存 $0.01 ✚补录: 旧表无此键 → 落默认价, 输入高估 10 倍 / 输出高估 5 倍
929
+
930
+ // —— xAI Grok (2026-09-22 官方页 docs.x.ai/docs/pricing 的 Text API 表逐条核对, **整段补录**) ——
931
+ // ⚠️ v1.4.5 之前本表**没有任何 grok 键**: 所有 grok 请求落 defaultPrices(USD $1/$2, 缓存读 $0.1),
932
+ // 而 grok-4.7 / 4.6 真价 $2/$6 → 输入少算 2 倍、输出少算 3 倍; grok-4.3 / 4.20 系输入输出各少算 25%。
933
+ // 官方口径: 每模型两档, 长上下文阈值一律 **200k tokens**(≥200k 时输入×2、输出×2、缓存×2);
934
+ // 本表取**主档(短上下文)**入库, 与同为 200k 阈值的 gemini-3.1-pro / gemini-2.5-pro 处理一致。
935
+ // 交叉验证: 官方页面内嵌机读数据 __XAI_PUBLIC_MODELS__ 的 us-east-1 / us-west-2 集群与本表逐项吻合;
936
+ // 其 us-central-1 集群是 US 区域端点价(= 标准 ×1.1, 目前只覆盖 4.7/4.6), 未入库。
937
+ // 未建模: Batch API 对 4.3 / 4.20 系有 20% 折扣; Priority Processing 一律 ×2 标准价。
938
+ // 未收录: 官方页现只列 grok-4.3 起的在售模型, grok-3 / grok-4 / grok-4-fast 等已不在页内 → 落默认价。
939
+ 'grok-4.7': { cacheHit: 0.50, cacheMiss: 2.0, output: 6.0 }, // 官方 $2.00/$6.00/缓存 $0.50 (500k 上限); ≥200k: $4/$12/缓存 $1
940
+ 'grok-4.6': { cacheHit: 0.50, cacheMiss: 2.0, output: 6.0 }, // 同上
941
+ 'grok-4.5': { cacheHit: 0.30, cacheMiss: 2.0, output: 6.0 }, // 官方 $2.00/$6.00/缓存 $0.30; ≥200k: $4/$12/缓存 $0.60
942
+ 'grok-4.3': { cacheHit: 0.20, cacheMiss: 1.25, output: 2.5 }, // 官方 $1.25/$2.50/缓存 $0.20 (1M 上限); ≥200k: $2.50/$5/缓存 $0.40
943
+ 'grok-4.20': { cacheHit: 0.20, cacheMiss: 1.25, output: 2.5 }, // 官方别名(等价 grok-4.20-0309-reasoning, 同价)
944
+ 'grok-4.20-reasoning': { cacheHit: 0.20, cacheMiss: 1.25, output: 2.5 }, // 官方别名; 连带 -latest / -0309 等
945
+ 'grok-4.20-non-reasoning': { cacheHit: 0.20, cacheMiss: 1.25, output: 2.5 }, // 官方 $1.25/$2.50/缓存 $0.20 (1M)
946
+ 'grok-4.20-multi-agent-0309': { cacheHit: 0.20, cacheMiss: 1.25, output: 2.5 }, // 官方 $1.25/$2.50/缓存 $0.20 (1M)
947
+ 'grok-4.20-0309-reasoning': { cacheHit: 0.20, cacheMiss: 1.25, output: 2.5 }, // 官方 $1.25/$2.50/缓存 $0.20 (1M)
948
+ 'grok-4.20-0309-non-reasoning': { cacheHit: 0.20, cacheMiss: 1.25, output: 2.5 }, // 官方 $1.25/$2.50/缓存 $0.20 (1M)
949
+ 'grok-build-0.1': { cacheHit: 0.20, cacheMiss: 1.0, output: 2.0 }, // 官方 $1.00/$2.00/缓存 $0.20 (256k); ≥200k: $2/$4/缓存 $0.40
950
+ 'grok-code-fast-1': { cacheHit: 0.20, cacheMiss: 1.0, output: 2.0 }, // grok-build-0.1 的官方别名(-1-0825 亦由安全后缀命中)
951
+ 'grok-code-fast': { cacheHit: 0.20, cacheMiss: 1.0, output: 2.0 }, // 同上(别名短名)
855
952
  // —— 国内厂商: 单位 CNY/百万tokens (原生官方价, 不要再 ÷7) ——
856
953
  // 阿里云百炼 Qwen3 (华北2/北京; help.aliyun.com/zh/model-studio/model-pricing 2026-09-10 抓取)
857
954
  // 官方上下文缓存规则: 命中按「标准输入单价 10%」计费。
@@ -862,6 +959,10 @@ export const MODEL_PRICES = {
862
959
  'qwen3.7-plus': { cacheHit: 0.16, cacheMiss: 1.6, output: 6.4 }, // v1.4.0: 官方限时 8 折 (原价 ¥2/¥8)
863
960
  'qwen3.7-flash': { cacheHit: 0.02, cacheMiss: 0.2, output: 0.8 }, // v1.4.0: 官方 ¥0.2/¥0.8 (旧值 0.21/0.91 系中转站高档位, 已废)
864
961
  'qwen3.8-flash': { cacheHit: 0.1, cacheMiss: 0.8, output: 2.7 }, // 官方 ¥0.8/¥2.7; cacheHit ¥0.1 同 3.8-max 为控制台例外价
962
+ // —— 2026-09-22 官方百炼定价页 (help.aliyun.com/zh/model-studio/model-pricing) 核对后补录 ——
963
+ 'qwen3.8-max-prime': { cacheHit: 2.4, cacheMiss: 24, output: 72 }, // 官方优速模式(Prime) ¥24/¥72; 官方未单列缓存价, 按"显式缓存命中=标准输入 10%"规则推
964
+ 'qwen3.8-omni-flash': { cacheHit: 0.1, cacheMiss: 0.8, output: 2.7 }, // 官方 ¥0.8/¥2.7/缓存命中 ¥0.1 (3.8 系里唯一官方单列缓存价的)
965
+ 'qwen3.5-plus': { cacheHit: 0.08, cacheMiss: 0.8, output: 4.8 }, // 官方 [0,128K) ¥0.8/¥4.8
865
966
  'qwen3.8-27b': { cacheHit: 0.3, cacheMiss: 3, output: 12 }, // v1.4.0: 官方 ¥3/¥12; 缓存命中按官方 10% 规则 → ¥0.3 (旧值 ¥0.6 偏高 100%)
866
967
  'qwen3.6-plus': { cacheHit: 0.2, cacheMiss: 2, output: 12 }, // 官方 ¥2/¥12 (256K 档 ¥8/¥48 未做分档)
867
968
  // 智谱 GLM (docs.bigmodel.cn/cn/guide/start/pricing 2026-09-10 抓取)
@@ -871,13 +972,30 @@ export const MODEL_PRICES = {
871
972
  'glm-5.1': { cacheHit: 2, cacheMiss: 8, output: 28 }, // 官方 ≥32K 档 ¥8/¥28/缓存 ¥2 ([0,32K) 档为 ¥6/¥24/¥1.3)
872
973
  'glm-5-turbo': { cacheHit: 1.8, cacheMiss: 7, output: 26 }, // v1.4.0: 官方 ≥32K 档 ¥7/¥26/缓存 ¥1.8 (旧值 1.68/8.4/28 两档都不符)
873
974
  'glm-5.3-flash': { cacheHit: 0.23, cacheMiss: 0.8, output: 2.8 }, // 官方 ¥0.8/¥2.8/缓存 ¥0.23
975
+ // —— 2026-09-22 官方定价页 (docs.bigmodel.cn/cn/guide/start/pricing) 逐条核对后补录 ——
976
+ // 分档口径 = 按**输入长度**在 32K 切开 ([0, 32K) / ≥32K); 分档模型一律取更贵的 ≥32K 档(保守)。
977
+ 'glm-5.3-flashx': { cacheHit: 0.57, cacheMiss: 2, output: 7 }, // 官方 ¥2/¥7/缓存 ¥0.57 (1M)
978
+ 'glm-5': { cacheHit: 1.5, cacheMiss: 6, output: 22 }, // 官方 ≥32K 档 ¥6/¥22/缓存 ¥1.5 ([0,32K) 档为 ¥4/¥18/缓存 ¥1)
979
+ 'glm-4.7': { cacheHit: 0.8, cacheMiss: 4, output: 16 }, // 官方 [32K,200K) 档 ¥4/¥16/缓存 ¥0.8 (低档 ¥2~3/¥8~14)
980
+ 'glm-4.7-flashx': { cacheHit: 0.1, cacheMiss: 0.5, output: 3 }, // 官方 ¥0.5/¥3/缓存 ¥0.1 (200K)
981
+ 'glm-4.5-air': { cacheHit: 0.24, cacheMiss: 1.2, output: 8 }, // 官方 [32K,128K) 档 ¥1.2/¥8/缓存 ¥0.24
982
+ 'glm-4-air-250414': { cacheHit: 0.25, cacheMiss: 0.5, output: 0.5 }, // 官方 ¥0.5/¥0.5/缓存 ¥0.25
983
+ 'glm-4-long': { cacheHit: 0.5, cacheMiss: 1, output: 1 }, // 官方 ¥1/¥1/缓存 ¥0.5 (1M)
984
+ // ⚠️ 这一条同时修掉一个键名错配: 原来 'glm-4-flashx-250414' 没有任何键能收它(旧值 0.05/0.1/0.1 被挂在
985
+ // 'glm-4-flash' 上), 而官方 GLM-4-Flash-250414 其实是**免费**、GLM-4-FlashX-250414 才是 ¥0.1/¥0.1/缓存 ¥0.05。
986
+ // 'glm-4-flashx' 比 'glm-4-flash' 长, matchModelPrice 取最长键 → FlashX 现在能命中自己的价。
987
+ 'glm-4-flashx': { cacheHit: 0.05, cacheMiss: 0.1, output: 0.1 }, // 官方 GLM-4-FlashX-250414 ¥0.1/¥0.1/缓存 ¥0.05
874
988
  // Kimi / Moonshot (platform.kimi.com/docs/pricing/* 2026-09-10 抓取, 均 CNY)
875
989
  'kimi-k3': { cacheHit: 2, cacheMiss: 20, output: 100 }, // v1.4.0: 官方 ¥2/¥20/¥100 (旧值全线 +5%)
876
990
  'kimi-k2.7-code': { cacheHit: 1.3, cacheMiss: 6.5, output: 27 }, // 官方 ¥1.3/¥6.5/¥27
877
991
  'kimi-k2.7-code-highspeed': { cacheHit: 2.6, cacheMiss: 13, output: 54 },// v1.4.0 新增: 官方高速版 ¥2.6/¥13/¥54
878
992
  'kimi-k2.6': { cacheHit: 1.1, cacheMiss: 6.5, output: 27 }, // v1.4.0: 官方缓存命中 ¥1.1 (旧值误抄成 k2.7-code 的 ¥1.3)
879
- 'kimi-k2.5': { cacheHit: 0.679, cacheMiss: 3.864, output: 20.279 }, // ⚠️ 未核实: 官方页未列(历史款), 由 v1.3.4 USD 值 ×7 保号迁移
880
- // 字节豆包 Seed (火山方舟; ⚠️ 官方页是 SPA, v1.4.0 未能取到原文 ×7 保号迁移, 未核实)
993
+ // ⚠️ 官方已于 **2026-08-31 全平台下线** (platform.kimi.com/docs/models.md, 调用返回 404), 官方无价目。
994
+ // 值仍为 v1.3.4 USD×7 保号迁移, **非官方价**, 仅作旧会话估算占位 —— 2026-09-22 复核确认下线, 未能取证。
995
+ 'kimi-k2.5': { cacheHit: 0.679, cacheMiss: 3.864, output: 20.279 },
996
+ // 字节豆包 Seed —— ⚠️ 2026-09-22 复核: 火山方舟官方文档/定价页都是 SPA, fetch 只拿到壳(0 表),
997
+ // 而本容器内 chromium 无网络(无法渲染), 故**仍未能取证**。下列值仍是 v1.3.4 的 ×7 保号迁移, **非官方价**。
998
+ // 取证路径: 用带网络的浏览器打开 volcengine.com/docs/82379/1544106 抄表后回填。
881
999
  'doubao-seed-2.0-pro-32k': { cacheHit: 0.616, cacheMiss: 3.087, output: 15.449 },
882
1000
  'doubao-seed-2.0-pro-128k': { cacheHit: 0.924, cacheMiss: 4.634, output: 23.17 },
883
1001
  'doubao-seed-2.0-pro-256k': { cacheHit: 1.855, cacheMiss: 9.268, output: 46.347 },
@@ -896,19 +1014,36 @@ export const MODEL_PRICES = {
896
1014
  // MiniMax (platform.minimaxi.com/docs/guides/pricing-paygo 2026-09-10 抓取)
897
1015
  'minimax-m2.7': { cacheHit: 0.42, cacheMiss: 2.1, output: 8.4 }, // v1.4.0 修复: 官方缓存读 ¥0.42 (旧值拿 cacheMiss ¥2.1 顶替 → 长会话高估 5 倍, 同 AGENTS.md 红线 4)
898
1016
  'minimax-m2.7-highspeed': { cacheHit: 0.42, cacheMiss: 4.2, output: 16.8 }, // v1.4.0 新增: 官方高速版
1017
+ 'minimax-m3': { cacheHit: 0.42, cacheMiss: 2.1, output: 8.4 }, // 2026-09-22 官方"永久五折" ≤512k 输入档 ¥2.1/¥8.4/缓存 ¥0.42 (>512k 档 ¥4.2/¥16.8/缓存 ¥0.84)
899
1018
  // 美团 LongCat (中转站实测; 官方页未取到明文)
900
1019
  'longcat-2.0': { cacheHit: 0.1, cacheMiss: 5, output: 20 }, // 实测 ¥5/¥20/缓存 ¥0.1
901
- // 腾讯混元 (⚠️ 官方页是 SPA, v1.4.0 未能取到原文 → ×7 保号迁移, 未核实)
1020
+ // 腾讯混元 —— ⚠️ 2026-09-22 复核: 官方计费页 (cloud.tencent.com/document/product/1729/97731) 与模型列表里
1021
+ // **均无以下三个 API 名** (官方现役计费项为 Hunyuan-a13b / hunyuan-role-latest / hunyuan-translation /
1022
+ // Tencent HY Vision 1.5 Instruct 等), 无从对照 → 下列值仍是 v1.3.4 的 ×7 保号迁移, **非官方价**。
1023
+ // 若确认是历史名/中转站命名, 可整段删除; 现在保留只为老会话估算不断档。
902
1024
  'hunyuan-2.0-instruct-128k': { cacheHit: 4.347, cacheMiss: 4.347, output: 10.745 },
903
1025
  'hunyuan-2.0-think-128k': { cacheHit: 5.117, cacheMiss: 5.117, output: 20.468 },
904
1026
  'hunyuan-turbo-s': { cacheHit: 0.77, cacheMiss: 0.77, output: 1.932 },
905
1027
  // 阶跃星辰 (platform.stepfun.com/docs/zh/guides/pricing/details 2026-09-10 抓取)
906
1028
  'step-3.7-flash': { cacheHit: 0.27, cacheMiss: 1.35, output: 8.1 }, // 官方 ¥1.35/¥8.1/缓存 ¥0.27
907
1029
  'step-3.5-flash': { cacheHit: 0.14, cacheMiss: 0.7, output: 2.1 }, // 官方 ¥0.7/¥2.1/缓存 ¥0.14
1030
+ 'step-5-preview': { cacheHit: 0.35, cacheMiss: 7, output: 20 }, // 2026-09-22 官方新增 ¥7/¥20/缓存 ¥0.35
1031
+ 'step-1o-turbo-vision': { cacheHit: 0.5, cacheMiss: 2.5, output: 8 }, // 2026-09-22 官方 ¥2.5/¥8/缓存 ¥0.5
908
1032
  // 小米 MiMo — 官方 2026-05-27 起「永久降价」(最高降幅 99%), 取消上下文分档; 与中转站 tokenrhythm 实时报价一致。
909
1033
  // v1.3.4 修的「除两次 7」结论正确, v1.4.0 起改为直接存官方 CNY 原值, 不再有 ÷7 环节。
910
- 'mimo-v2.5': { cacheHit: 0.02, cacheMiss: 1, output: 2 }, // 官方 ¥1/¥2/缓存 ¥0.02
911
- 'mimo-v2.5-pro': { cacheHit: 0.025, cacheMiss: 3, output: 6 }, // 官方 ¥3/¥6/缓存 ¥0.025
1034
+ // —— 2026-09-22 官方按量付费页 (platform.xiaomimimo.com/docs/zh-CN/price/pay-as-you-go) 逐条核对 ——
1035
+ // V2.6 系列于当日发布(官方 updates/model 2026-09-22「模型发布」), V2.5 系**同价**(实时推理档)。
1036
+ // ✚补录 3 条: 此前表内无任何 v2.6 键 → 全部落 defaultPrices(USD $1/$2), 按国内口径换算后为 ¥7/¥14,
1037
+ // 即 v2.6-pro 高估 2.33 倍、v2.6-flash 高估 7 倍。
1038
+ // ⚠️ V2.5 系官方下线公告 (updates/deprecate): **北京时间 2026-10-21 10:00 直接下线, 无系统替换模型** —
1039
+ // 保留其键仅供旧会话估算, 到期后新调用会失败, 别再把新流量算作 v2.5。
1040
+ // 未建模: 批量推理档(v2.6-pro ¥1.5/¥3、v2.6-flash ¥0.5/¥1)、mimo-v2.5-asr(按音频时长 ¥0.5/小时)、
1041
+ // 国内联网服务(¥16/1000 次); 官方另有 USD 表($0.435/$0.87 等), 按「原生币种存储」规则本表只用 CNY 表。
1042
+ 'mimo-v2.6-pro': { cacheHit: 0.025, cacheMiss: 3, output: 6 }, // 官方实时推理 ¥3/¥6/缓存 ¥0.025 (与 v2.5-pro 同价)
1043
+ 'mimo-v2.6-flash': { cacheHit: 0.02, cacheMiss: 1, output: 2 }, // 官方实时推理 ¥1/¥2/缓存 ¥0.02 (与 v2.5 同价)
1044
+ 'mimo-v2.6-pro-ultraspeed': { cacheHit: 0.25, cacheMiss: 30, output: 60 }, // 官方 ¥30/¥60/缓存 ¥0.25 (旗舰性能 + 最高 20 倍推理速度)
1045
+ 'mimo-v2.5': { cacheHit: 0.02, cacheMiss: 1, output: 2 }, // 官方 ¥1/¥2/缓存 ¥0.02 ⚠️ 2026-10-21 10:00 下线
1046
+ 'mimo-v2.5-pro': { cacheHit: 0.025, cacheMiss: 3, output: 6 }, // 官方 ¥3/¥6/缓存 ¥0.025 ⚠️ 2026-10-21 10:00 下线
912
1047
  // —— 以下为历史/参考模型 (2025-08, 实际以平台为准) ——
913
1048
  // 币种规则同上: 海外的写 USD, 国内的写 CNY。
914
1049
  // 🔴 v1.4.0 重要修复: 本段「国内」条目历来填的是**官方 CNY 原值**(不是 ÷7 后的 USD),
@@ -927,7 +1062,9 @@ export const MODEL_PRICES = {
927
1062
  'claude-3-5-sonnet': { cacheHit: 0.3, cacheMiss: 3, output: 15 },
928
1063
  'claude-3-5-haiku': { cacheHit: 0.08, cacheMiss: 0.8, output: 4 },
929
1064
  'claude-3-opus': { cacheHit: 1.5, cacheMiss: 15, output: 75 },
930
- // Gemini — v1.4.0 修正: 缓存读 = 输入 ×25% (Gemini 官方 75% off 口径)。旧值 50% 偏高。
1065
+ // Gemini 历史条目 ⚠️ 2026-09-22 复核: 官方定价页现**已不列** 2.0 / 1.5 全系(页面自 Gemini 2.5 ),
1066
+ // 故这三条的「缓存读 = 输入 ×25%」口径**无法取证**, 原样保留仅作旧会话估算占位, **非官方现价**。
1067
+ // 现役 Gemini 3.x / 2.5 官方页一律「缓存读 = 输入 ×10%」(2026-09-22 逐条吻合) —— 历史 25% 口径勿再套用到新条目。
931
1068
  'gemini-2.0-flash': { cacheHit: 0.025, cacheMiss: 0.1, output: 0.4 },
932
1069
  'gemini-2.0-pro': { cacheHit: 0.625, cacheMiss: 2.5, output: 10 },
933
1070
  'gemini-1.5-pro': { cacheHit: 0.875, cacheMiss: 3.5, output: 10.5 },
@@ -943,15 +1080,20 @@ export const MODEL_PRICES = {
943
1080
  'glm-4-plus': { cacheHit: 2.5, cacheMiss: 5, output: 5 }, // ✅ v1.4.0 修复: 官方 ¥2.5/¥5/¥5 (旧口径下显示 ¥17.5/¥35/¥35, 高 7 倍)
944
1081
  'glm-4-flash': { cacheHit: 0.05, cacheMiss: 0.1, output: 0.1 }, // ⚠️ 官方 GLM-4-Flash-250414 现为免费; 此处保留历史 ¥0.1 档(宁高不低, 中转站可能仍计费)
945
1082
  // 通义千问 (官方 CNY; 2026-09-10 抓取)
946
- 'qwen-plus': { cacheHit: 0.4, cacheMiss: 0.8, output: 2 }, // 官方 ¥0.8/¥2 ✅; ⚠️ cacheHit 0.4(=50%) 未核实
1083
+ 'qwen-plus': { cacheHit: 0.08, cacheMiss: 0.8, output: 2 }, // 官方 ¥0.8/¥2 (2026-09-22 核对: 现役 qwen-plus-2025-01-25 无阶梯计价);
1084
+ // ⚠️ cacheHit 原值 0.4(=输入 50%) **无官方依据** —— 官方明文「显式缓存命中按标准输入单价的 10% 计费」,
1085
+ // 同表其余 8 个 qwen 条目也都严格等于 10%, 只有 qwen-plus / qwen-turbo 是 50% → 已按官方规则改为 0.08 (旧值高估缓存读 5 倍)。
947
1086
  'qwen-max': { cacheHit: 0.24, cacheMiss: 2.4, output: 9.6 },// v1.4.0: 官方现价 ¥2.4/¥9.6 (旧值 20/60 是远古价)
948
- 'qwen-turbo': { cacheHit: 0.15, cacheMiss: 0.3, output: 0.6 },// 官方 ¥0.3/¥0.6 ✅; ⚠️ cacheHit 未核实
949
- 'qwen2.5-72b-instruct': { cacheHit: 2, cacheMiss: 4, output: 12 }, // 官方 ¥4/¥12
950
- // Kimi — ⚠️ 未核实: 官方页未列旧款, 且这些值与 Moonshot 官方历史价(¥12/¥12 一档)对不上, 待重新取证
1087
+ 'qwen-turbo': { cacheHit: 0.03, cacheMiss: 0.3, output: 0.6 },// 官方 ¥0.3/¥0.6 (思考模式输出 ¥3) ✅; cacheHit 同上按官方 10% 规则改为 0.03 (旧值 0.15=50%)
1088
+ 'qwen2.5-72b-instruct': { cacheHit: 2, cacheMiss: 4, output: 12 }, // ⚠️ 官方 2026-05-13 已下线(百炼下线公告), 保留仅为旧会话估算占位
1089
+ // Kimi — ⚠️ 官方 **2026-08-31 已全平台下线 moonshot-v1 全系** (调用 404), 官方价目已撤, 无法取证。
1090
+ // 下列值与原 Moonshot 官方历史价(¥12/¥12 一档)对不上, 仅作旧会话估算占位, 别当官方价引用。
951
1091
  'moonshot-v1-8k': { cacheHit: 0.6, cacheMiss: 1.2, output: 2.4 },
952
1092
  'moonshot-v1-32k': { cacheHit: 1.2, cacheMiss: 2.4, output: 4.8 },
953
1093
  'moonshot-v1-128k': { cacheHit: 3, cacheMiss: 6, output: 12 },
954
- // 阶跃星辰 ⚠️ 未核实: 官方页未列旧款
1094
+ // 阶跃星辰 —— ⚠️ 2026-09-22 复核: 官方定价页现仅列 step-5-preview / step-3.7-flash / step-3.5-flash(-2603)
1095
+ // / step-1o-turbo-vision 与音频系列, **无下列 step-1-flash / step-1-8k / step-1-32k**(历史款),
1096
+ // 官方无从对照, 值仍为旧存留, **非当前官方价**。
955
1097
  'step-1-flash': { cacheHit: 0.5, cacheMiss: 1, output: 2 },
956
1098
  'step-1-8k': { cacheHit: 2, cacheMiss: 4, output: 8 },
957
1099
  'step-1-32k': { cacheHit: 4, cacheMiss: 8, output: 15 },
@@ -995,9 +1137,13 @@ export const nativeCurrencyOf = (model) => (modelRegion(model) === '海外' ? 'U
995
1137
  * 默认配置(国内 CNY / 海外 USD)下两边同币种, 根本不走换算 —— 这正是 v1.4.0 想达到的效果。
996
1138
  */
997
1139
  const convertPrice = (price, from, to) => {
998
- if (from === to) return { cacheHit: price.cacheHit, cacheMiss: price.cacheMiss, output: price.output }
1140
+ // v1.5.0: cacheWrite 是**可选**档 (官方 Anthropic/阿里 = 输入 ×125%) —— 必须一并带过,
1141
+ // 否则会被这里静默丢掉, 缓存写入就退化成按 cacheMiss 计 (这正是 T14 第一次没测过的原因)。
1142
+ const cw = price.cacheWrite
1143
+ const withCw = (o) => (cw !== undefined ? { ...o, cacheWrite: cw } : o)
1144
+ if (from === to) return withCw({ cacheHit: price.cacheHit, cacheMiss: price.cacheMiss, output: price.output })
999
1145
  const k = from === 'USD' ? USD_TO_CNY_RATE : 1 / USD_TO_CNY_RATE
1000
- return { cacheHit: price.cacheHit * k, cacheMiss: price.cacheMiss * k, output: price.output * k }
1146
+ return withCw({ cacheHit: price.cacheHit * k, cacheMiss: price.cacheMiss * k, output: price.output * k })
1001
1147
  }
1002
1148
 
1003
1149
  /**
@@ -1038,7 +1184,7 @@ const matchModelPrice = (model) => {
1038
1184
  }
1039
1185
 
1040
1186
  /** 解析模型单价, 仅 deepseek-flash / deepseek-v4-* 支持峰谷自动切换; chat/reasoner 等走通用价格表 */
1041
- export const resolveModelPrice = (configOrGetter, model, timestamp = Date.now()) => {
1187
+ export const resolveModelPrice = (configOrGetter, model, timestamp = Date.now(), phase = null) => {
1042
1188
  const config = typeof configOrGetter === 'function' ? configOrGetter() : configOrGetter
1043
1189
  const peak = isPeakTime(timestamp)
1044
1190
  const display = currencyForModel(config, model)
@@ -1056,7 +1202,10 @@ export const resolveModelPrice = (configOrGetter, model, timestamp = Date.now())
1056
1202
  if (typeof model === 'string' && (model.startsWith('deepseek-v4') || model.startsWith('deepseek-flash'))) {
1057
1203
  const table = V4_RATES[display] ?? V4_RATES.CNY
1058
1204
  const key = model.startsWith('deepseek-v4-pro') ? 'deepseek-v4-pro' : 'deepseek-flash'
1059
- const hit = (peak ? table.peak[key] : table.offPeak[key])
1205
+ // v1.5.0 峰谷分相: phase 显式给出时以它为准(历史上的每笔消耗按各自发生时刻计价),
1206
+ // 为 null 才回退到「按 timestamp 判定」的老行为 —— 默认参数保持向后兼容。
1207
+ const usePeak = phase === 'peak' ? true : phase === 'offPeak' ? false : peak
1208
+ const hit = (usePeak ? table.peak[key] : table.offPeak[key])
1060
1209
  if (hit) return { ...hit }
1061
1210
  }
1062
1211
 
@@ -1703,32 +1852,64 @@ let subagentCostSummarize = null
1703
1852
 
1704
1853
  const safeSessionId = (id) => typeof id === 'string' && /^[A-Za-z0-9._-]{1,128}$/.test(id) ? id : null
1705
1854
 
1706
- /** 读某会话的投影缓存记录(带 TTL 内存缓存)。任何异常 → null。 */
1707
- const readSessionCacheRecord = (sessionId) => {
1855
+ /**
1856
+ * 读某会话的投影缓存记录(带 TTL 内存缓存)。任何异常 null。
1857
+ * v1.4.4: 同时把 `identity` 带出来 —— 里面记着 `isSeeded` / `inheritedEventCount`(fork 边界),
1858
+ * 旧版本的缓存行(状态里没有 own)要靠它判断「自身用量」能不能精确还原。
1859
+ */
1860
+ const loadCacheEntry = (sessionId) => {
1708
1861
  const id = safeSessionId(sessionId)
1709
1862
  if (id === null) return null
1710
1863
  const now = Date.now()
1711
1864
  const hit = sessionCacheFiles.get(id)
1712
- if (hit !== undefined && now - hit.at < SUBAGENT_FILE_TTL_MS) return hit.rows
1713
- let rows = null
1865
+ if (hit !== undefined && now - hit.at < SUBAGENT_FILE_TTL_MS) return hit
1866
+ let entry = null
1714
1867
  try {
1715
1868
  const home = process.env.DSH_HOME || join(homedir(), '.dsh')
1716
1869
  const file = join(home, 'storages', 'session_projcache', 'sessions', `${id}.json`)
1717
1870
  const parsed = JSON.parse(readFileSync(file, 'utf8'))
1718
1871
  const r = parsed?.record?.rows
1719
- if (r !== null && typeof r === 'object') rows = r
1720
- } catch { rows = null }
1872
+ if (r !== null && typeof r === 'object') {
1873
+ const identity = parsed?.record?.identity
1874
+ entry = { at: now, rows: r, identity: (identity !== null && typeof identity === 'object') ? identity : null }
1875
+ }
1876
+ } catch { entry = null }
1721
1877
  // 只缓存成功结果, 避免一次读失败被 TTL 钉住 3 秒
1722
- if (rows !== null) sessionCacheFiles.set(id, { at: now, rows })
1878
+ if (entry !== null) sessionCacheFiles.set(id, entry)
1723
1879
  if (sessionCacheFiles.size > 256) sessionCacheFiles.clear()
1724
- return rows
1880
+ return entry
1725
1881
  }
1726
1882
 
1727
- /** 冷路径: 从缓存文件里取该会话的 queryBalanceCost **状态**(不是 wire 视图)。 */
1883
+ /** 读某会话的投影缓存 rows。任何异常 null。 */
1884
+ const readSessionCacheRecord = (sessionId) => loadCacheEntry(sessionId)?.rows ?? null
1885
+
1886
+ /**
1887
+ * 冷路径: 从缓存文件里取该会话的 queryBalanceCost **状态**(不是 wire 视图)。
1888
+ *
1889
+ * ⚠️ v1.4.4 修正 v1.4.3 的一个过严判断:v1.4.3 只认 `ver === 3`,于是**升级后所有旧缓存行
1890
+ * 一律显示 `~—`**(老框架不会再给已结束的子会话重写缓存,那些行永远好不了)。
1891
+ * 实际上旧缓存也能精确还原**非分叉**子会话的自身用量:
1892
+ * · `inheritedEventCount === 0`(没有继承任何事件)→ 自身 == 全量,`byModel` 就是精确值;
1893
+ * · 分叉过的子会话 → 旧缓存分不出继承段,**仍然返回 null**(上层显示「等待」),
1894
+ * 绝不退化成把父会话历史算进去的错数字。
1895
+ */
1728
1896
  const cachedCostState = (sessionId) => {
1729
- const row = readSessionCacheRecord(sessionId)?.queryBalanceCost
1730
- const val = row?.ver === 3 ? row.val : null
1731
- return (val !== null && typeof val === 'object' && Array.isArray(val.modelOrder) && val.byModel !== null && typeof val.byModel === 'object') ? val : null
1897
+ const entry = loadCacheEntry(sessionId)
1898
+ const row = entry?.rows?.queryBalanceCost
1899
+ const val = row?.val
1900
+ if (val === null || typeof val !== 'object' || !Array.isArray(val.modelOrder) || val.byModel === null || typeof val.byModel !== 'object') return null
1901
+ if (row.ver === 3 && val.own !== undefined) return val
1902
+ const inherited = entry?.identity?.inheritedEventCount
1903
+ if (inherited === 0) {
1904
+ return {
1905
+ ...val, ownBoundaryKnown: true, inheritedEventCount: 0,
1906
+ own: {
1907
+ currentModel: val.currentModel ?? null, currentProvider: val.currentProvider ?? null,
1908
+ last: val.last ?? null, byModel: val.byModel, modelOrder: val.modelOrder,
1909
+ },
1910
+ }
1911
+ }
1912
+ return null
1732
1913
  }
1733
1914
 
1734
1915
  /** 冷路径: 从缓存文件里取该会话的 subagentCatalog 条目。 */
@@ -1880,6 +2061,29 @@ export function makeCostProjection(configOrGetter, services) {
1880
2061
  })
1881
2062
  const round6 = (n) => Math.round(n * 1e6) / 1e6
1882
2063
 
2064
+ /**
2065
+ * v1.5.0: 某模型是否走 V4 峰谷表 —— 条件必须与 resolveModelPrice 内部一致
2066
+ * (用户在 config.prices 里自填过价的那一档不算峰谷, 自填价不做峰谷切换)。
2067
+ */
2068
+ const phaseOfModel = (model, ts) => {
2069
+ if (typeof model !== 'string') return null
2070
+ if (!(model.startsWith('deepseek-v4') || model.startsWith('deepseek-flash'))) return null
2071
+ const cfg = getConfig()
2072
+ if (cfg?.prices && Object.prototype.hasOwnProperty.call(cfg.prices, model) && cfg.prices[model]) return null
2073
+ return isPeakTime(ts) ? 'peak' : 'offPeak'
2074
+ }
2075
+ /** 往分相桶加一份用量 (不可变更新)。 */
2076
+ const phaseAdd = (map, model, phase, buckets) => {
2077
+ const cur = map[model] ?? {}
2078
+ return { ...map, [model]: { ...cur, [phase]: addBuckets(cur[phase] ?? zero(), buckets) } }
2079
+ }
2080
+ /** 从分相桶减一份 (替换语义); 该相不存在时原样返回。 */
2081
+ const phaseSub = (map, model, phase, buckets) => {
2082
+ const cur = map[model]
2083
+ if (cur === undefined || cur[phase] === undefined) return map
2084
+ return { ...map, [model]: { ...cur, [phase]: subBuckets(cur[phase], buckets) } }
2085
+ }
2086
+
1883
2087
  /**
1884
2088
  * v1.4.0: 从一条 assistant stream 里取**最后一次** usage chunk。
1885
2089
  * 与 dsh-llm 的 `lastAssistantStreamChunk(stream, 'usage')` 同语义, 本地实现以免给插件引入额外依赖
@@ -1927,8 +2131,16 @@ export function makeCostProjection(configOrGetter, services) {
1927
2131
  tokens.cacheWrite += b.cacheWriteTokens
1928
2132
  tokens.output += b.outputTokens
1929
2133
  // 支持 DeepSeek 谷峰自动计费
1930
- const price = resolveModelPrice(cfg, model)
1931
- const c = ((b.uncachedInputTokens + b.cacheWriteTokens) * price.cacheMiss + b.cacheReadTokens * price.cacheHit + b.outputTokens * price.output) / 1e6
2134
+ // v1.5.0: 缓存写入单独计价 —— 官方普遍按**标准输入价的 125%**收 (Anthropic $6.25/$5、阿里显式缓存明文 125%、
2135
+ // MiniMax 2.625/2.1), Kimi K3 更是另有 ¥20/1M 的写入费。表里没给 cacheWrite 的条目回落到 cacheMiss (= 旧行为, 结果不变)
2136
+ const costOf = (bk, price) => ((bk.uncachedInputTokens * price.cacheMiss + bk.cacheWriteTokens * (price.cacheWrite ?? price.cacheMiss) + bk.cacheReadTokens * price.cacheHit + bk.outputTokens * price.output)) / 1e6
2137
+ // v1.5.0: 有分相数据就按峰/谷各自计价 (每笔用量用**它发生那一刻**的价),
2138
+ // 没有(老缓存/非峰谷模型)才回退到按当前时刻计价的老行为。
2139
+ const pb = state?.phaseBuckets?.[model]
2140
+ const c = (pb !== undefined && (pb.peak !== undefined || pb.offPeak !== undefined))
2141
+ ? costOf(pb.peak ?? zero(), resolveModelPrice(cfg, model, undefined, 'peak'))
2142
+ + costOf(pb.offPeak ?? zero(), resolveModelPrice(cfg, model, undefined, 'offPeak'))
2143
+ : (() => { const price = resolveModelPrice(cfg, model); return costOf(b, price) })()
1932
2144
  // v1.3.2: 该模型实际币种 (海外模型可能与主货币不同)
1933
2145
  const cur = currencyForModel(cfg, model)
1934
2146
  if (c > 0) {
@@ -1981,15 +2193,26 @@ export function makeCostProjection(configOrGetter, services) {
1981
2193
  if (usage === null) return unchanged ? state : { ...state, currentModel: nextModel, currentProvider: nextProvider }
1982
2194
  const model = nextModel ?? 'unknown'
1983
2195
  const buckets = bucketsOf(usage)
2196
+ // v1.5.0: 按**事件发生时刻**(event.time)判定峰谷, 而不是等 summarize 时用 Date.now()。
2197
+ // 后者会把整段历史消耗都按"打开面板那一刻"的时段计价 —— DeepSeek 峰谷价差 2 倍,
2198
+ // 谷时跑的量在峰时查看会被翻倍, 反之漏掉一半。事件无 time 时回退当前时刻。
2199
+ const eventTime = Number.isFinite(event?.time) ? event.time : Date.now()
2200
+ const phase = phaseOfModel(model, eventTime)
1984
2201
  const prev = state.last !== null && state.last.turn === turn && state.last.step === step ? state.last : null
1985
2202
  if (prev !== null && prev.model === model && bucketsEqual(prev.buckets, buckets)) {
1986
2203
  return unchanged ? state : { ...state, currentModel: nextModel, currentProvider: nextProvider }
1987
2204
  }
1988
2205
  const isNewModel = !(model in state.byModel)
1989
2206
  let byModel = state.byModel
1990
- if (prev !== null) byModel = { ...byModel, [prev.model]: subBuckets(byModel[prev.model] ?? zero(), prev.buckets) }
2207
+ let phaseBuckets = state.phaseBuckets ?? {}
2208
+ if (prev !== null) {
2209
+ byModel = { ...byModel, [prev.model]: subBuckets(byModel[prev.model] ?? zero(), prev.buckets) }
2210
+ // 替换槽位时, 分相桶也要按**上一个事件当时的相**回退, 否则重试会把同一笔算两次。
2211
+ if (prev.phase) phaseBuckets = phaseSub(phaseBuckets, prev.model, prev.phase, prev.buckets)
2212
+ }
1991
2213
  byModel = { ...byModel, [model]: addBuckets(byModel[model] ?? zero(), buckets) }
1992
- return { ...state, currentModel: nextModel, currentProvider: nextProvider, last: { turn, step, model, buckets }, byModel, modelOrder: isNewModel ? [...state.modelOrder, model] : state.modelOrder }
2214
+ if (phase) phaseBuckets = phaseAdd(phaseBuckets, model, phase, buckets)
2215
+ return { ...state, currentModel: nextModel, currentProvider: nextProvider, last: { turn, step, model, buckets, phase }, byModel, phaseBuckets, modelOrder: isNewModel ? [...state.modelOrder, model] : state.modelOrder }
1993
2216
  }
1994
2217
 
1995
2218
  return {
@@ -2009,6 +2232,8 @@ export function makeCostProjection(configOrGetter, services) {
2009
2232
  cacheWriteTokens: z.number(),
2010
2233
  outputTokens: z.number(),
2011
2234
  }),
2235
+ /** v1.5.0: 该事件发生时的峰谷相, 替换槽位时要按同一个相回退分相桶。 */
2236
+ phase: z.string().nullable().optional(),
2012
2237
  }).nullable(),
2013
2238
  byModel: z.record(z.string(), z.object({
2014
2239
  uncachedInputTokens: z.number(),
@@ -2016,21 +2241,26 @@ export function makeCostProjection(configOrGetter, services) {
2016
2241
  cacheWriteTokens: z.number(),
2017
2242
  outputTokens: z.number(),
2018
2243
  })),
2244
+ /** v1.5.0: 按事件发生时刻拆分的峰/谷用量桶 (仅走 V4 峰谷表的模型才会出现)。 */
2245
+ phaseBuckets: z.record(z.string(), z.object({
2246
+ peak: z.object({ uncachedInputTokens: z.number(), cacheReadTokens: z.number(), cacheWriteTokens: z.number(), outputTokens: z.number() }).optional(),
2247
+ offPeak: z.object({ uncachedInputTokens: z.number(), cacheReadTokens: z.number(), cacheWriteTokens: z.number(), outputTokens: z.number() }).optional(),
2248
+ })).optional(),
2019
2249
  modelOrder: z.array(z.string()),
2020
2250
  /** v1.4.0: 本投影所属会话 id —— 子代理汇总要拿它去查 `subagentCatalog`。空串表示未知。 */
2021
2251
  sessionId: z.string(),
2022
2252
  inheritedEventCount: z.number().int().nonnegative(),
2023
2253
  ownBoundaryKnown: z.boolean(),
2024
2254
  ownChildIds: z.array(z.string()),
2025
- own: z.object({ currentModel: z.string().nullable(), currentProvider: z.string().nullable(), last: z.any().nullable(), byModel: z.record(z.string(), z.object({ uncachedInputTokens: z.number().nonnegative(), cacheReadTokens: z.number().nonnegative(), cacheWriteTokens: z.number().nonnegative(), outputTokens: z.number().nonnegative() })), modelOrder: z.array(z.string()) }),
2255
+ own: z.object({ currentModel: z.string().nullable(), currentProvider: z.string().nullable(), last: z.any().nullable(), byModel: z.record(z.string(), z.object({ uncachedInputTokens: z.number().nonnegative(), cacheReadTokens: z.number().nonnegative(), cacheWriteTokens: z.number().nonnegative(), outputTokens: z.number().nonnegative() })), phaseBuckets: z.record(z.string(), z.any()).optional(), modelOrder: z.array(z.string()) }),
2026
2256
  }),
2027
2257
  init: (header, inheritedEventCount) => ({
2028
- currentModel: null, currentProvider: null, last: null, byModel: {}, modelOrder: [],
2258
+ currentModel: null, currentProvider: null, last: null, byModel: {}, phaseBuckets: {}, modelOrder: [],
2029
2259
  sessionId: typeof header?.id === 'string' ? header.id : '',
2030
2260
  inheritedEventCount: Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0 ? inheritedEventCount : 0,
2031
2261
  ownBoundaryKnown: (Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0) || !header?.isSeeded,
2032
2262
  ownChildIds: [],
2033
- own: { currentModel: null, currentProvider: null, last: null, byModel: {}, modelOrder: [] },
2263
+ own: { currentModel: null, currentProvider: null, last: null, byModel: {}, phaseBuckets: {}, modelOrder: [] },
2034
2264
  }),
2035
2265
  apply: (state, event) => {
2036
2266
  const full = foldUsage(state, event)
@@ -2099,7 +2329,7 @@ export function makeCostProjection(configOrGetter, services) {
2099
2329
  }
2100
2330
  },
2101
2331
  },
2102
- stateVersion: 3,
2332
+ stateVersion: 4,
2103
2333
  }
2104
2334
  }
2105
2335
 
@@ -2120,9 +2350,10 @@ export function apply(ctx, config) {
2120
2350
  }
2121
2351
  } catch { /* 忽略 */ }
2122
2352
  const runtimeConfig = {
2123
- refreshIntervalMs: persisted.refreshIntervalMs ?? config.refreshIntervalMs ?? 5000,
2124
- clientPollIntervalMs: persisted.clientPollIntervalMs ?? config.clientPollIntervalMs ?? 5000,
2125
- timeoutMs: persisted.timeoutMs ?? config.timeoutMs ?? 8000,
2353
+ // ⚠️ 两个来源都要夹: persisted 已被 sanitizePersistedShape 夹过, 但 config 没有 (见 clampMs 注释)。
2354
+ refreshIntervalMs: clampMs(persisted.refreshIntervalMs ?? config.refreshIntervalMs, 1000, 60000, 5000),
2355
+ clientPollIntervalMs: clampMs(persisted.clientPollIntervalMs ?? config.clientPollIntervalMs, 1000, 60000, 5000),
2356
+ timeoutMs: clampMs(persisted.timeoutMs ?? config.timeoutMs, 1000, 60000, 8000),
2126
2357
  presets: config.presets ?? PLATFORM_PRESETS.map(p => p.id),
2127
2358
  // H-1 (v1.4.1): 这里必须带 Array.isArray —— 状态文件形状跑偏时 apply() 抛出 =
2128
2359
  // 整个 dsh web 启动失败(migratePersistedState 已消毒, 这里是第二道防线)