@gamegeek-saikel/dsh-cost-meter 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -5
- package/README.zh-CN.md +28 -5
- package/lib/client.js +255 -11
- package/lib/client.js.map +1 -1
- package/lib/index.js +213 -25
- package/lib/types/client/CostPluginCard.d.ts.map +1 -1
- package/lib/types/client/CostPluginCard.js +18 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +6 -0
- package/lib/types/holidays.d.ts +100 -0
- package/lib/types/holidays.d.ts.map +1 -0
- package/lib/types/holidays.js +142 -0
- package/lib/types/index.d.ts +13 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +6 -0
- package/lib/types/pricebook.d.ts +15 -1
- package/lib/types/pricebook.d.ts.map +1 -1
- package/lib/types/pricebook.js +31 -3
- package/lib/types/pricing.d.ts +18 -4
- package/lib/types/pricing.d.ts.map +1 -1
- package/lib/types/pricing.js +50 -16
- package/lib/types/types.d.ts +14 -0
- package/lib/types/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/client/CostPluginCard.tsx +47 -0
- package/src/client/locales.ts +6 -0
- package/src/holidays.ts +167 -0
- package/src/index.ts +19 -0
- package/src/pricebook.ts +34 -3
- package/src/pricing.ts +47 -15
- package/src/types.ts +14 -0
package/README.md
CHANGED
|
@@ -23,13 +23,23 @@ Install it into a web profile with the official DSH CLI (via npx — no global i
|
|
|
23
23
|
npx @deepseek-ai/dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
That installs the npm `latest` tag, which tracks the current DSH release line. This plugin publishes on the same two npm channels DSH itself uses, and a build only runs on the DSH line it was built for, so install the channel that matches your runtime:
|
|
27
|
+
|
|
28
|
+
| npm dist-tag | Tracks | Install command |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `latest` | the current DSH release line | `dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter` |
|
|
31
|
+
| `next` | the upcoming DSH release line | `dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter@next` |
|
|
32
|
+
| `alpha` | alpha builds | `dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter@alpha` |
|
|
33
|
+
|
|
34
|
+
`npm dist-tag ls @deepseek-ai/dsh` shows which DSH version serves each line, and `npm view @gamegeek-saikel/dsh-cost-meter dist-tags` shows which plugin version serves it. A DSH runtime refuses to load a bundle whose peer ranges reject it, so picking the wrong channel fails the install outright rather than half-loading the plugin.
|
|
35
|
+
|
|
26
36
|
Then start the harness:
|
|
27
37
|
|
|
28
38
|
```bash
|
|
29
39
|
npx @deepseek-ai/dsh web
|
|
30
40
|
```
|
|
31
41
|
|
|
32
|
-
If you have the DSH CLI installed globally, you can also use `dsh` instead of `npx @deepseek-ai/dsh`. To install into another profile, replace `web` with your profile name.
|
|
42
|
+
If you have the DSH CLI installed globally, you can also use `dsh` instead of `npx @deepseek-ai/dsh`. To install into another profile, replace `web` with your profile name. Each published version declares and is verified against the DSH line its channel names above. The host half requires Node `^22.19.0 || >=24.0.0` and pnpm `11.7.0` for development.
|
|
33
43
|
|
|
34
44
|
## Overview
|
|
35
45
|
|
|
@@ -44,7 +54,7 @@ Chat costs in DeepSeek pricing change over time (list prices, USD→CNY exchange
|
|
|
44
54
|
| Cost anchoring | Append-only pricebook snapshots; step cost computed once at the event's own time |
|
|
45
55
|
| Price sources | Manual override > official pricing page > built-in fallback > OpenRouter (fallback only, USD→CNY) > none |
|
|
46
56
|
| Official page | Parses the current 2026-09-10 combined zh/en tables (the `deepseek-flash` / `deepseek-v4-pro` columns with OFF-PEAK/PEAK cells per bucket row, and the English UTC schedule) as well as the earlier combined and legacy split tables; the retired `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` ids have no column of their own and bill at the Flash price, exactly as the page states. The built-in list still anchors `single` when the page no longer carries a pre-rollout table |
|
|
47
|
-
| Peak pricing | 2026-08-17 00:00 Beijing rollout; peak windows apply Monday–Friday only (the zh and en pages may state different timezones; fallback 09:00–12:00 / 14:00–18:00 Beijing), so
|
|
57
|
+
| Peak pricing | 2026-08-17 00:00 Beijing rollout; peak windows apply Monday–Friday only and exclude Chinese public holidays (the zh and en pages may state different timezones; fallback 09:00–12:00 / 14:00–18:00 Beijing), so weekends, holidays, and all other hours are off-peak at half price |
|
|
48
58
|
| Cost formula | Uncached input + cache reads (hit rate) + cache writes (billed at uncached input rate) + output, per 1M tokens, CNY |
|
|
49
59
|
| Account balance | Official `GET /user/balance`, cached 60 s, single in-flight request, trust-fenced route |
|
|
50
60
|
| Subagent support | Enumerates the conversation's durable subagent tree through `subagents.listDescendants` (nested delegations, settled children, and cold subagent sessions included, with no depth cap); a ledger is read from the resident session's projection when available and otherwise cold-restored from the child's stored log (`sessionQuery.readSession` + `sessionProjections.restore`), so historical subagent spend is also counted after a host restart; falls back to a BFS over the live agent tree when neither service is mounted |
|
|
@@ -63,7 +73,7 @@ Once installed, the plugin contributes five browser surfaces (all text shown in
|
|
|
63
73
|
| Cost view tab | `conversation.view` | Whole-conversation totals (main + every subagent nesting level), category totals, the per-subagent list with its depth, and the per-reply anchored ledger |
|
|
64
74
|
| Per-reply cost chip | `conversation.chat.assistant-actions` | The anchored cost of one finalized reply as the row's own stat capsule — the same trigger + anchored dialog the shipped 用量 and 用时 capsules use (28px pill, hover fill, `aria-expanded` dialog above the trigger). It closes the row's stat run: after 用量 and 用时, before the timestamp, spaced by the row's own gap, and coloured by the band that priced it (green off-peak, red peak). The dialog lists the three billed categories, the band with its multiplier, the model, and the snapshot version; the slot hands over the message id, which the `sessionCostIndex` projection resolves to ledger coordinates (dash `—` when unpriced) |
|
|
65
75
|
| Header pill | `conversation.session.header.utilities` | Anchored total, or a live `≈ ¥x.xx (estimate)` while streaming; click for the detail panel |
|
|
66
|
-
| Plugin configuration page | `plugins.row.config` | The cost-meter row's page under 设置 → 插件 (DSH 0.2.0 hosts a plugin's own configuration there, keyed `<package>#<row id>`): per-model overrides, OpenRouter aliases, cache-read discount, FX mode, toggles, and manual refresh. The editable fields are the schema's `volatile()` ones — the deployment fields (endpoint, credential reference, refresh cadence, trusted hosts, history cap) stay hand-edited in the profile patch, which is exactly the split DSH's configuration projection can write |
|
|
76
|
+
| Plugin configuration page | `plugins.row.config` | The cost-meter row's page under 设置 → 插件 (DSH 0.2.0 hosts a plugin's own configuration there, keyed `<package>#<row id>`): per-model overrides, OpenRouter aliases, extra rest days and working days for the holiday calendar, cache-read discount, FX mode, toggles, and manual refresh. The editable fields are the schema's `volatile()` ones — the deployment fields (endpoint, credential reference, refresh cadence, trusted hosts, history cap) stay hand-edited in the profile patch, which is exactly the split DSH's configuration projection can write |
|
|
67
77
|
|
|
68
78
|
The `/cost-meter` host route serves the balance snapshot, the pricebook view, and the subagent totals over GET, and applies manual refresh over POST (`{"action":"refresh"}`). Like the `/api` fence, the route only answers requests whose `Host` header names a loopback address or a declared trusted host — the DNS-rebinding-safe check.
|
|
69
79
|
|
|
@@ -73,7 +83,7 @@ The pricebook (`src/pricebook.ts`) is the durable price source, persisted on the
|
|
|
73
83
|
|
|
74
84
|
- **Priority chain** — per canonical model key (`provider/model`, bare model, or the `flash`/`pro` pricing key for DeepSeek-family models): manual override > official page > built-in fallback > OpenRouter (fallback only, USD→CNY, cache reads at the configured discount) > none.
|
|
75
85
|
- **Snapshot selection** — `snapshotForTime` picks the newest snapshot with `effectiveAt <= event time` (pre-install sessions anchor to the first snapshot once).
|
|
76
|
-
- **Peak/off-peak** — before the 2026-08-17 rollout all steps price at the single list price; after it, the band is chosen by the EVENT's own time against the schedule of the pricebook's own page (the zh and en pages each parse their own peak windows — the redesigned English page states UTC — falling back to Beijing 09:00–12:00 / 14:00–18:00, everything else off-peak). The official pages restrict peak windows to Monday–Friday, so Saturdays and Sundays are off-peak all day in the schedule's own timezone. When the combined page has no legacy single-price column, `single` keeps anchoring to the built-in historical list; a page that still carries a separate legacy table wins. Each step's band is anchored once at fold time, so the per-reply cards and chips always show the band that round was billed at — never the band of the moment you are looking.
|
|
86
|
+
- **Peak/off-peak** — before the 2026-08-17 rollout all steps price at the single list price; after it, the band is chosen by the EVENT's own time against the schedule of the pricebook's own page (the zh and en pages each parse their own peak windows — the redesigned English page states UTC — falling back to Beijing 09:00–12:00 / 14:00–18:00, everything else off-peak). The official pages restrict peak windows to Monday–Friday and exclude Chinese public holidays, so Saturdays and Sundays are off-peak all day in the schedule's own timezone, and so is every holiday in full — judged on the Beijing calendar date even under the English page's UTC windows. The dates come from the State Council notices, transcribed in [`src/holidays.ts`](src/holidays.ts) (2026, per 国办发明电〔2025〕7号); a year no notice covers yet falls back to the plain Monday–Friday rule, and the settings page's rest-day/workday lists patch the gap without waiting for a release. The 调休 working weekends stay off-peak, because the pages say Monday–Friday; list them as working days to bill them at peak. When the combined page has no legacy single-price column, `single` keeps anchoring to the built-in historical list; a page that still carries a separate legacy table wins. Each step's band is anchored once at fold time, so the per-reply cards and chips always show the band that round was billed at — never the band of the moment you are looking.
|
|
77
87
|
- **Immutable ledger** — the `sessionCost` projection (`src/session-cost-projection.ts`) folds `request/header` (model) and usage-carrying events into per-step rows; a second usage sample for the same (turn, step) replaces the first (same-step finalization, not a re-price), with O(1) incremental totals. The companion `sessionCostIndex` projection (`src/session-cost-index.ts`) maps each finalized assistant-message id to its ledger coordinates; it reads no pricebook and computes no cost, so folding it can never re-price a session.
|
|
78
88
|
|
|
79
89
|
## Project Structure
|
|
@@ -86,6 +96,7 @@ src/
|
|
|
86
96
|
pricebook.ts # Append-only snapshots, priority chain, storage domain
|
|
87
97
|
session-cost-projection.ts # sessionCost projection (immutable per-step ledger)
|
|
88
98
|
session-cost-index.ts # sessionCostIndex: message id → ledger coordinates
|
|
99
|
+
holidays.ts # Chinese statutory-holiday calendar + its deployment overrides
|
|
89
100
|
subagent-cost.ts # Subagent cost aggregation (durable tree + live-registry fallback)
|
|
90
101
|
invariant.ts # Route-disposer symmetry invariant companion
|
|
91
102
|
client/ # Browser half: 5 slot components + math/format/locales
|
|
@@ -107,9 +118,21 @@ pnpm build # tsc -b && tsdown (lib/ + lib/client.js)
|
|
|
107
118
|
|
|
108
119
|
The test suites are fully offline: pricing-page HTML, OpenRouter models, and the FX endpoint are all stubbed. Tests cover the trust fence, balance parsing, the pricebook priority chain and snapshot selection, the immutable ledger fold (including same-step replacement and peak/off-peak band selection at the *event* time), the subagent aggregation (nested and cold children, listing-failure fallback, and a multi-level chain over a real `SessionStore` plus projection registry), and the client surfaces (jsdom).
|
|
109
120
|
|
|
121
|
+
### Release channels
|
|
122
|
+
|
|
123
|
+
A `v*` tag push publishes through `.github/workflows/publish.yml`. The tag decides the npm dist-tag, and that choice matters beyond bookkeeping: the community market installs the bare package name — whatever `latest` names — into a visitor's runtime. DSH ships two release lines at once (`@deepseek-ai/dsh` names the current one under `latest` and the upcoming one under `next`), and a plugin version only runs on the line its `@deepseek-ai/dsh*` peer ranges accept, so `scripts/dist-tag.mjs` reads those ranges against the registry's own DSH lines and picks the channel:
|
|
124
|
+
|
|
125
|
+
| Version | Channel |
|
|
126
|
+
|---|---|
|
|
127
|
+
| accepts the DSH `latest` line | `latest` |
|
|
128
|
+
| accepts only the DSH `next` line | `next` |
|
|
129
|
+
| carries a `-` prerelease suffix | `alpha` |
|
|
130
|
+
|
|
131
|
+
`node scripts/dist-tag.mjs pairs` prints that table for every published version; `node scripts/dist-tag.mjs reconcile` re-points each tag at the newest version supporting it, which also repairs a tag an earlier release process set wrong (`.github/workflows/dist-tags.yml` runs the same reconcile on demand).
|
|
132
|
+
|
|
110
133
|
## Documentation
|
|
111
134
|
|
|
112
|
-
- [`src/pricing.ts`](src/pricing.ts), [`src/pricebook.ts`](src/pricebook.ts), [`src/session-cost-projection.ts`](src/session-cost-projection.ts), [`src/session-cost-index.ts`](src/session-cost-index.ts) — detailed module docs on parsing, anchoring, and the ledger contract
|
|
135
|
+
- [`src/pricing.ts`](src/pricing.ts), [`src/pricebook.ts`](src/pricebook.ts), [`src/session-cost-projection.ts`](src/session-cost-projection.ts), [`src/session-cost-index.ts`](src/session-cost-index.ts), [`src/holidays.ts`](src/holidays.ts) — detailed module docs on parsing, anchoring, the holiday calendar, and the ledger contract
|
|
113
136
|
- [`README.zh-CN.md`](README.zh-CN.md) — 简体中文版本
|
|
114
137
|
|
|
115
138
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -23,13 +23,23 @@
|
|
|
23
23
|
npx @deepseek-ai/dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
上面装的是 npm 的 `latest` 标签,它跟随当前 DSH 发布线。本插件与 DSH 使用同样的两条 npm 通道,而每个构建只能在它对应的那条 DSH 发布线上运行,因此请按你的运行时选择通道:
|
|
27
|
+
|
|
28
|
+
| npm dist-tag | 对应 | 安装命令 |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `latest` | 当前 DSH 发布线 | `dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter` |
|
|
31
|
+
| `next` | 下一条 DSH 发布线 | `dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter@next` |
|
|
32
|
+
| `alpha` | alpha 构建 | `dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter@alpha` |
|
|
33
|
+
|
|
34
|
+
用 `npm dist-tag ls @deepseek-ai/dsh` 可查看各条通道对应哪个 DSH 版本,用 `npm view @gamegeek-saikel/dsh-cost-meter dist-tags` 查看各条通道对应哪个插件版本。DSH 运行时会拒绝加载对等依赖不匹配的 bundle,因此选错通道会直接安装失败,而不会把插件装成半可用的状态。
|
|
35
|
+
|
|
26
36
|
然后启动:
|
|
27
37
|
|
|
28
38
|
```bash
|
|
29
39
|
npx @deepseek-ai/dsh web
|
|
30
40
|
```
|
|
31
41
|
|
|
32
|
-
如果已全局安装 DSH CLI,也可以使用 `dsh` 代替 `npx @deepseek-ai/dsh`。安装到其他 profile 时,把 `web` 替换成你的 profile
|
|
42
|
+
如果已全局安装 DSH CLI,也可以使用 `dsh` 代替 `npx @deepseek-ai/dsh`。安装到其他 profile 时,把 `web` 替换成你的 profile 名称即可。每个已发布版本都声明并已验证兼容上表所列对应通道的 DSH 发布线。开发环境要求 Node `^22.19.0 || >=24.0.0` 与 pnpm `11.7.0`。
|
|
33
43
|
|
|
34
44
|
## 概述
|
|
35
45
|
|
|
@@ -44,7 +54,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、2026-08
|
|
|
44
54
|
| 成本锚定 | 只追加价格簿快照;步成本在事件自身时刻只计算一次 |
|
|
45
55
|
| 价格来源 | 手动覆盖 > 官方价格页 > 内置回退 > OpenRouter(仅回退,USD→CNY)> 无 |
|
|
46
56
|
| 官方页适配 | 解析当前 2026-09-10 版中英文页面(`deepseek-flash` / `deepseek-v4-pro` 两列、每个 token 桶行内的空闲/高峰单元格,以及英文 UTC 时段),同时兼容此前的合并表与旧版分表;已下线的 `deepseek-v4-flash`、`deepseek-v4-flash-vision-exp` 不再有独立列,按页面说明直接以 Flash 价格计费;页面不再提供旧单价表时继续用内置历史单价锚定 `single` |
|
|
47
|
-
| 峰值定价 | 2026-08-17 00:00
|
|
57
|
+
| 峰值定价 | 2026-08-17 00:00 北京生效;高峰窗口仅周一至周五,且不含中国法定节假日(中英文页面时区可能不同,缺省 09:00–12:00 / 14:00–18:00 北京),因此周末、法定节假日全天及其余时段均为半价闲时 |
|
|
48
58
|
| 成本公式 | 未命中输入 + 缓存命中(命中价)+ 缓存写入(按未命中输入价计)+ 输出,每百万 tokens,CNY |
|
|
49
59
|
| 账户余额 | 官方 `GET /user/balance`,缓存 60 秒,单飞请求,路由带信任围栏 |
|
|
50
60
|
| 子代理支持 | 沿 `subagents.listDescendants` 枚举本会话持久子代理树(子代理再派生的孙代理、已结束或已冷启动的子会话都计入,无层数上限);账本优先取常驻会话的投影,未常驻的经 `sessionQuery.readSession` + `sessionProjections.restore` 从其持久日志冷读——因此主机重启后,旧会话的历史子代理花费同样会被补算;未挂载该服务时回退到活跃代理树 BFS |
|
|
@@ -63,7 +73,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、2026-08
|
|
|
63
73
|
| 花费标签页 | `conversation.view` | 全对话总花费(主会话 + 任意层级的子代理)、分类小计、带层级的子代理列表与逐回复锚定账本 |
|
|
64
74
|
| 每条回复成本小标签 | `conversation.chat.assistant-actions` | 单条已定稿回复的锚定成本,做成与官方「用量」「用时」完全同形的统计胶囊(28px 药丸、悬停底色、`aria-expanded` 展开锚定在触发件上方的对话框);位置收在统计串末:在「用量」「用时」之后、末尾时刻文本之前,间距沿用行自身的 gap;按计价档位着色(闲时绿、高峰红);对话框列出三类计费、档位与倍率、模型与快照版本;插槽只给出消息 id,由 `sessionCostIndex` 投影解析为账本坐标(无价格时显示 `—`) |
|
|
65
75
|
| 头部胶囊 | `conversation.session.header.utilities` | 锚定总花费;流式中显示 `预计 ¥x.xx(估算)`;点击展开详情面板 |
|
|
66
|
-
| 插件配置页 | `plugins.row.config` | 设置 → 插件 中 cost-meter 行自己的配置页(DSH 0.2.0 以 `<包名>#<行 id>` 为键托管插件自带配置):按模型覆盖价、OpenRouter
|
|
76
|
+
| 插件配置页 | `plugins.row.config` | 设置 → 插件 中 cost-meter 行自己的配置页(DSH 0.2.0 以 `<包名>#<行 id>` 为键托管插件自带配置):按模型覆盖价、OpenRouter 别名、节假日日历的额外休息日与工作日、缓存折扣、汇率模式、开关与立即刷新。可编辑字段即 schema 中标记 `volatile()` 的字段——部署类字段(端点、凭据引用、刷新周期、可信主机、历史上限)仍在 profile patch 中手工维护,这正是 DSH 配置投影能够写入的边界 |
|
|
67
77
|
|
|
68
78
|
`/cost-meter` 宿主路由通过 GET 提供余额快照、价格簿视图与子代理合计;通过 POST(`{"action":"refresh"}`)执行手动刷新。与 `/api` 围栏一致,路由只应答 `Host` 头为回环地址或已声明可信主机的请求——这是防 DNS 重绑定的安全校验。
|
|
69
79
|
|
|
@@ -73,7 +83,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、2026-08
|
|
|
73
83
|
|
|
74
84
|
- **优先级链**——按规范模型键(`provider/model`、裸模型名,或 DeepSeek 系模型的 `flash`/`pro` 定价键):手动覆盖 > 官方页面 > 内置回退 > OpenRouter(仅回退,USD→CNY,缓存读按配置折扣)> 无。
|
|
75
85
|
- **快照选取**——`snapshotForTime` 取 `effectiveAt <= 事件时间` 的最新快照(安装前的会话一次性锚到首个快照)。
|
|
76
|
-
- **峰值/闲时**——2026-08-17 上线前所有步按单一价目计费;上线后按事件自身时刻与该价格簿页面的时段表选档(中英文页面各自解析自己的高峰窗口,英文新版为 UTC,抓取失败时回退到北京 09:00–12:00 / 14:00–18:00
|
|
86
|
+
- **峰值/闲时**——2026-08-17 上线前所有步按单一价目计费;上线后按事件自身时刻与该价格簿页面的时段表选档(中英文页面各自解析自己的高峰窗口,英文新版为 UTC,抓取失败时回退到北京 09:00–12:00 / 14:00–18:00,其余闲时)。官方页面限定高峰仅周一至周五且不含中国法定节假日,因此时区表内的周六、周日全天为闲时,法定节假日同样全天闲时——即便在英文页面的 UTC 窗口下,节假日也按北京日历日判定。日期取自国务院办公厅通知,转录在 [`src/holidays.ts`](src/holidays.ts)(2026 年,依据国办发明电〔2025〕7号);尚未覆盖的年份回退到「周一至周五」原规则,可用设置页的休息日/工作日清单在不发版的情况下补齐。调休来的周末工作日仍按闲时计(因为页面写的是周一至周五),若要按高峰计,把它们列进工作日清单即可。新版合并表没有旧单价列时,`single` 继续锚定内置历史单价;页面若有独立旧表,则优先用它。每步的档位在折叠时锚定一次,逐回复卡片与回复小标签永远显示该轮对话当时被计价的档位,而不是查看时刻的档位。
|
|
77
87
|
- **不可变账本**——`sessionCost` 投影(`src/session-cost-projection.ts`)把 `request/header`(模型)与携带用量的事件折叠为逐步记录;同一 (turn, step) 的第二次用量样本**替换**第一条(同一步终结,而非重新计价),总计以 O(1) 增量维护。配套的 `sessionCostIndex` 投影(`src/session-cost-index.ts`)把每条已定稿助手消息 id 映射到其账本坐标;它不读价格簿、不算成本,因此折叠它绝不会重新计价任何会话。
|
|
78
88
|
|
|
79
89
|
## 项目结构
|
|
@@ -86,6 +96,7 @@ src/
|
|
|
86
96
|
pricebook.ts # 只追加快照、优先级链、存储域
|
|
87
97
|
session-cost-projection.ts # sessionCost 投影(不可变逐步账本)
|
|
88
98
|
session-cost-index.ts # sessionCostIndex:消息 id → 账本坐标
|
|
99
|
+
holidays.ts # 中国法定节假日日历及其部署覆盖项
|
|
89
100
|
subagent-cost.ts # 子代理成本聚合(持久子代理树 + 活跃代理树回退)
|
|
90
101
|
invariant.ts # 路由释放对称性 invariant 伴生插件
|
|
91
102
|
client/ # 浏览器半区:5 个插槽组件 + 数学/格式化/本地化
|
|
@@ -107,9 +118,21 @@ pnpm build # tsc -b && tsdown(lib/ + lib/client.js)
|
|
|
107
118
|
|
|
108
119
|
测试套件完全离线:价格页 HTML、OpenRouter 模型目录与汇率接口全部 stub。覆盖:信任围栏、余额解析、价格簿优先级链与快照选取、不可变账本折叠(含同一步替换与按*事件时间*选峰值/闲时档)、子代理聚合(持久子代理树的嵌套/冷会话/失败回退,以及真实 `SessionStore` + 投影注册表上的多层链路),以及客户端表面(jsdom)。
|
|
109
120
|
|
|
121
|
+
### 发布通道
|
|
122
|
+
|
|
123
|
+
推送 `v*` 标签后由 `.github/workflows/publish.yml` 发布。标签决定 npm dist-tag,而这不只是记账问题:社区市场安装的是**不带版本号的包名**,也就是 `latest` 指向的那个版本,直接装进访客的运行时。DSH 同时并行两条发布线(`@deepseek-ai/dsh` 把当前线放在 `latest`、把下一条线放在 `next`),而插件版本只能在它的 `@deepseek-ai/dsh*` 对等依赖所接受的那条线上运行,因此 `scripts/dist-tag.mjs` 会用这些依赖范围去比对 registry 上 DSH 自己的发布线,据此选定通道:
|
|
124
|
+
|
|
125
|
+
| 版本 | 通道 |
|
|
126
|
+
|---|---|
|
|
127
|
+
| 接受 DSH `latest` 线 | `latest` |
|
|
128
|
+
| 只接受 DSH `next` 线 | `next` |
|
|
129
|
+
| 带 `-` 先行版本后缀 | `alpha` |
|
|
130
|
+
|
|
131
|
+
`node scripts/dist-tag.mjs pairs` 会打印所有已发布版本的归属表;`node scripts/dist-tag.mjs reconcile` 会把每个标签重新指向支持它的最新版本,同时修正早期发布流程写错的标签(`.github/workflows/dist-tags.yml` 可按需单独执行同一套 reconcile)。
|
|
132
|
+
|
|
110
133
|
## 文档
|
|
111
134
|
|
|
112
|
-
- [`src/pricing.ts`](src/pricing.ts)、[`src/pricebook.ts`](src/pricebook.ts)、[`src/session-cost-projection.ts`](src/session-cost-projection.ts)、[`src/session-cost-index.ts`](src/session-cost-index.ts)
|
|
135
|
+
- [`src/pricing.ts`](src/pricing.ts)、[`src/pricebook.ts`](src/pricebook.ts)、[`src/session-cost-projection.ts`](src/session-cost-projection.ts)、[`src/session-cost-index.ts`](src/session-cost-index.ts)、[`src/holidays.ts`](src/holidays.ts)——解析、锚定、节假日日历与账本契约的详细模块注释
|
|
113
136
|
- [`README.md`](README.md) — English version
|
|
114
137
|
|
|
115
138
|
## 许可证
|
package/lib/client.js
CHANGED
|
@@ -89,6 +89,9 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
"settings.refreshing": "刷新中…",
|
|
90
90
|
"settings.overrides": "按模型价目表(手动覆盖,元/百万 tokens)",
|
|
91
91
|
"settings.aliases": "OpenRouter 别名映射",
|
|
92
|
+
"settings.holidayRestDays": "额外休息日(节假日日历)",
|
|
93
|
+
"settings.holidayWorkdays": "额外工作日(上班时段按高峰计)",
|
|
94
|
+
"settings.holidayHint": "JSON 字符串数组,元素为 YYYY-MM-DD。内置日历已含 2026 年中国法定节假日——官方页面规定这些日期全天为闲时;未覆盖的年份可用这里补上。",
|
|
92
95
|
"settings.discount": "缓存折扣系数(OpenRouter)",
|
|
93
96
|
"settings.fx": "汇率(USD→CNY)",
|
|
94
97
|
"settings.fx.auto": "自动",
|
|
@@ -181,6 +184,9 @@ window.__ModuleLoader__.load({
|
|
|
181
184
|
"settings.refreshing": "Refreshing…",
|
|
182
185
|
"settings.overrides": "Per-model prices (manual overrides, CNY per 1M tokens)",
|
|
183
186
|
"settings.aliases": "OpenRouter alias map",
|
|
187
|
+
"settings.holidayRestDays": "Extra rest days (holiday calendar)",
|
|
188
|
+
"settings.holidayWorkdays": "Extra working days (peak windows apply)",
|
|
189
|
+
"settings.holidayHint": "JSON array of YYYY-MM-DD strings. The built-in calendar already covers the 2026 Chinese public holidays, which both pages bill off-peak in full; list a year here while the calendar does not cover it yet.",
|
|
184
190
|
"settings.discount": "Cache-read discount (OpenRouter)",
|
|
185
191
|
"settings.fx": "Exchange rate (USD→CNY)",
|
|
186
192
|
"settings.fx.auto": "Auto",
|
|
@@ -197,19 +203,158 @@ window.__ModuleLoader__.load({
|
|
|
197
203
|
"settings.jsonHint": "JSON object keyed by model; values carry single/offPeak/peak price buckets"
|
|
198
204
|
};
|
|
199
205
|
//#endregion
|
|
206
|
+
//#region src/holidays.ts
|
|
207
|
+
/** Timezone the holiday calendar is expressed in. */
|
|
208
|
+
const HOLIDAY_TIMEZONE = "Asia/Shanghai";
|
|
209
|
+
/**
|
|
210
|
+
* Statutory holidays by year. Every day listed here is off-peak in full,
|
|
211
|
+
* whatever its weekday and whatever the time.
|
|
212
|
+
*
|
|
213
|
+
* 2026, per 国办发明电〔2025〕7号: 元旦 01-01..01-03; 春节 02-15..02-23;
|
|
214
|
+
* 清明节 04-04..04-06; 劳动节 05-01..05-05; 端午节 06-19..06-21;
|
|
215
|
+
* 中秋节 09-25..09-27; 国庆节 10-01..10-07.
|
|
216
|
+
*
|
|
217
|
+
* The notice also names 调休 working days (2026: 01-04, 02-14, 02-28, 05-09,
|
|
218
|
+
* 09-20, 10-10). They are intentionally NOT in this table: the pages restrict
|
|
219
|
+
* peak to Monday through Friday, and those dates are weekend days, so they stay
|
|
220
|
+
* off-peak. A deployment that reads "工作日" more broadly can list them in
|
|
221
|
+
* {@link HolidayOverrides.workdays}.
|
|
222
|
+
*/
|
|
223
|
+
const CHINA_HOLIDAYS = { 2026: {
|
|
224
|
+
notice: "国办发明电〔2025〕7号",
|
|
225
|
+
dates: [
|
|
226
|
+
"2026-01-01",
|
|
227
|
+
"2026-01-02",
|
|
228
|
+
"2026-01-03",
|
|
229
|
+
"2026-02-15",
|
|
230
|
+
"2026-02-16",
|
|
231
|
+
"2026-02-17",
|
|
232
|
+
"2026-02-18",
|
|
233
|
+
"2026-02-19",
|
|
234
|
+
"2026-02-20",
|
|
235
|
+
"2026-02-21",
|
|
236
|
+
"2026-02-22",
|
|
237
|
+
"2026-02-23",
|
|
238
|
+
"2026-04-04",
|
|
239
|
+
"2026-04-05",
|
|
240
|
+
"2026-04-06",
|
|
241
|
+
"2026-05-01",
|
|
242
|
+
"2026-05-02",
|
|
243
|
+
"2026-05-03",
|
|
244
|
+
"2026-05-04",
|
|
245
|
+
"2026-05-05",
|
|
246
|
+
"2026-06-19",
|
|
247
|
+
"2026-06-20",
|
|
248
|
+
"2026-06-21",
|
|
249
|
+
"2026-09-25",
|
|
250
|
+
"2026-09-26",
|
|
251
|
+
"2026-09-27",
|
|
252
|
+
"2026-10-01",
|
|
253
|
+
"2026-10-02",
|
|
254
|
+
"2026-10-03",
|
|
255
|
+
"2026-10-04",
|
|
256
|
+
"2026-10-05",
|
|
257
|
+
"2026-10-06",
|
|
258
|
+
"2026-10-07"
|
|
259
|
+
]
|
|
260
|
+
} };
|
|
261
|
+
/** Every transcribed holiday, flattened (the built-in calendar). */
|
|
262
|
+
const DEFAULT_HOLIDAYS = Object.values(CHINA_HOLIDAYS).flatMap((year) => year.dates);
|
|
263
|
+
Object.keys(CHINA_HOLIDAYS).map(Number).sort((left, right) => left - right);
|
|
264
|
+
/** `YYYY-MM-DD`, the only spelling a calendar date is accepted in. */
|
|
265
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
266
|
+
/**
|
|
267
|
+
* Whether one string is a real calendar date in `YYYY-MM-DD` form (the shape
|
|
268
|
+
* alone would also accept `2026-02-31`).
|
|
269
|
+
* @param value - the candidate date.
|
|
270
|
+
* @returns true for an existing calendar date.
|
|
271
|
+
*/
|
|
272
|
+
function isCalendarDate(value) {
|
|
273
|
+
if (!DATE_PATTERN.test(value)) return false;
|
|
274
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
275
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Keep the valid dates, in the order given, without duplicates.
|
|
279
|
+
* @param values - candidate dates from configuration.
|
|
280
|
+
* @returns the normalized dates.
|
|
281
|
+
*/
|
|
282
|
+
function normalizeDates(values) {
|
|
283
|
+
if (values === void 0) return [];
|
|
284
|
+
const seen = /* @__PURE__ */ new Set();
|
|
285
|
+
for (const value of values) if (typeof value === "string" && isCalendarDate(value.trim())) seen.add(value.trim());
|
|
286
|
+
return [...seen];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The Beijing-calendar date of one instant as `YYYY-MM-DD`.
|
|
290
|
+
* @param instant - the moment to place on the Beijing calendar.
|
|
291
|
+
* @returns the date key.
|
|
292
|
+
*/
|
|
293
|
+
function beijingDateKey(instant) {
|
|
294
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
295
|
+
timeZone: HOLIDAY_TIMEZONE,
|
|
296
|
+
year: "numeric",
|
|
297
|
+
month: "2-digit",
|
|
298
|
+
day: "2-digit"
|
|
299
|
+
}).formatToParts(instant);
|
|
300
|
+
const pick = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
301
|
+
return `${pick("year")}-${pick("month")}-${pick("day")}`;
|
|
302
|
+
}
|
|
303
|
+
/** Rest-day and workday lookup sets, memoized per schedule object. */
|
|
304
|
+
const CALENDARS = /* @__PURE__ */ new WeakMap();
|
|
305
|
+
/**
|
|
306
|
+
* The schedule's calendars as lookup sets. The schedule travels over the wire
|
|
307
|
+
* and through storage as plain arrays, so the sets are derived here instead of
|
|
308
|
+
* being carried.
|
|
309
|
+
* @param schedule - the schedule to read.
|
|
310
|
+
* @returns the rest-day and forced-workday sets.
|
|
311
|
+
*/
|
|
312
|
+
function calendarsOf(schedule) {
|
|
313
|
+
const cached = CALENDARS.get(schedule);
|
|
314
|
+
if (cached !== void 0) return cached;
|
|
315
|
+
const calendars = {
|
|
316
|
+
rest: new Set(schedule.holidays ?? DEFAULT_HOLIDAYS),
|
|
317
|
+
work: new Set(schedule.workdays ?? [])
|
|
318
|
+
};
|
|
319
|
+
CALENDARS.set(schedule, calendars);
|
|
320
|
+
return calendars;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Attach the built-in calendar, plus a deployment's adjustments, to one page
|
|
324
|
+
* schedule. Every schedule the plugin serves or classifies against is composed
|
|
325
|
+
* here, so an override and the page's own windows always travel together.
|
|
326
|
+
* @param schedule - the schedule as parsed from the page (or the fallback).
|
|
327
|
+
* @param overrides - the deployment's extra rest days and forced workdays.
|
|
328
|
+
* @returns the schedule carrying its effective calendars.
|
|
329
|
+
*/
|
|
330
|
+
function withHolidayCalendar(schedule, overrides = {}) {
|
|
331
|
+
return {
|
|
332
|
+
...schedule,
|
|
333
|
+
holidays: [...DEFAULT_HOLIDAYS, ...normalizeDates(overrides.restDays)],
|
|
334
|
+
workdays: normalizeDates(overrides.workdays)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
//#endregion
|
|
200
338
|
//#region src/pricing.ts
|
|
201
339
|
/** Official peak-pricing rollout: 2026-08-17 00:00 Beijing time (UTC+8). */
|
|
202
340
|
const PEAK_PRICING_START_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
|
|
203
341
|
/**
|
|
204
342
|
* Built-in fallback zh peak schedule: the official page's windows in Beijing
|
|
205
343
|
* time (peak 09:00-12:00 / 14:00-18:00, Monday–Friday; everything else,
|
|
206
|
-
* including all of Saturday and
|
|
344
|
+
* including all of Saturday, Sunday, and Chinese public holidays, off-peak).
|
|
345
|
+
* The built-in holiday calendar is attached so the fallback classifies exactly
|
|
346
|
+
* like the page it stands in for.
|
|
207
347
|
*/
|
|
208
|
-
const PEAK_SCHEDULE_ZH = {
|
|
348
|
+
const PEAK_SCHEDULE_ZH = withHolidayCalendar({
|
|
209
349
|
timezone: "Asia/Shanghai",
|
|
210
350
|
ranges: [[9, 12], [14, 18]],
|
|
211
351
|
weekdaysOnly: true
|
|
212
|
-
};
|
|
352
|
+
});
|
|
353
|
+
withHolidayCalendar({
|
|
354
|
+
timezone: "UTC",
|
|
355
|
+
ranges: [[1, 4], [6, 10]],
|
|
356
|
+
weekdaysOnly: true
|
|
357
|
+
});
|
|
213
358
|
({ ...{
|
|
214
359
|
flash: {
|
|
215
360
|
cacheReadPerMillion: .007,
|
|
@@ -225,9 +370,21 @@ window.__ModuleLoader__.load({
|
|
|
225
370
|
/**
|
|
226
371
|
* Whether the given instant is a peak-pricing hour under one schedule: the
|
|
227
372
|
* zh fallback is Beijing time 09:00-12:00 and 14:00-18:00; each pricebook
|
|
228
|
-
* passes the schedule parsed from its own (zh or en) pricing page.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
373
|
+
* passes the schedule parsed from its own (zh or en) pricing page.
|
|
374
|
+
*
|
|
375
|
+
* The days are decided in this order, which is exactly what the pages state:
|
|
376
|
+
* 1. a date in the schedule's `workdays` follows the working-day windows, even
|
|
377
|
+
* on a weekend or a holiday (the 调休 escape hatch);
|
|
378
|
+
* 2. otherwise a date in its `holidays` (the built-in Chinese public holidays,
|
|
379
|
+
* plus any the deployment added) is off-peak in full;
|
|
380
|
+
* 3. otherwise, when the schedule is Monday–Friday only, Saturdays and Sundays
|
|
381
|
+
* in the schedule's timezone are off-peak all day;
|
|
382
|
+
* 4. otherwise the hour decides, against the schedule's own windows.
|
|
383
|
+
*
|
|
384
|
+
* Holiday membership is always judged on the Beijing calendar date, whichever
|
|
385
|
+
* timezone the schedule states its windows in — the holidays are Chinese public
|
|
386
|
+
* holidays, and the English page's UTC windows are the same Beijing hours.
|
|
387
|
+
*
|
|
231
388
|
* @param now - the instant to classify.
|
|
232
389
|
* @param schedule - the peak-hour schedule to classify against.
|
|
233
390
|
* @returns true during peak hours.
|
|
@@ -237,15 +394,27 @@ window.__ModuleLoader__.load({
|
|
|
237
394
|
timeZone: schedule.timezone,
|
|
238
395
|
hour: "numeric",
|
|
239
396
|
hour12: false,
|
|
240
|
-
weekday: "short"
|
|
397
|
+
weekday: "short",
|
|
398
|
+
year: "numeric",
|
|
399
|
+
month: "2-digit",
|
|
400
|
+
day: "2-digit"
|
|
241
401
|
}).formatToParts(now);
|
|
402
|
+
const pick = (type) => parts.find((part) => part.type === type)?.value;
|
|
403
|
+
const inWindows = () => {
|
|
404
|
+
const hour = Number(pick("hour"));
|
|
405
|
+
if (Number.isNaN(hour)) return false;
|
|
406
|
+
return schedule.ranges.some(([start, end]) => hour >= start && hour < end);
|
|
407
|
+
};
|
|
408
|
+
const calendars = calendarsOf(schedule);
|
|
409
|
+
const localDate = `${pick("year")}-${pick("month")}-${pick("day")}`;
|
|
410
|
+
const beijingDate = schedule.timezone === "Asia/Shanghai" ? localDate : beijingDateKey(now);
|
|
411
|
+
if (calendars.work.has(beijingDate)) return inWindows();
|
|
412
|
+
if (calendars.rest.has(beijingDate)) return false;
|
|
242
413
|
if (schedule.weekdaysOnly === true) {
|
|
243
|
-
const weekday =
|
|
414
|
+
const weekday = pick("weekday");
|
|
244
415
|
if (weekday === "Sat" || weekday === "Sun") return false;
|
|
245
416
|
}
|
|
246
|
-
|
|
247
|
-
if (Number.isNaN(hour)) return false;
|
|
248
|
-
return schedule.ranges.some(([start, end]) => hour >= start && hour < end);
|
|
417
|
+
return inWindows();
|
|
249
418
|
}
|
|
250
419
|
//#endregion
|
|
251
420
|
//#region src/client/cost-math.ts
|
|
@@ -1395,6 +1564,14 @@ window.__ModuleLoader__.load({
|
|
|
1395
1564
|
* refresh button is a plain action against the plugin's own host route.
|
|
1396
1565
|
*/
|
|
1397
1566
|
const json = (value) => JSON.stringify(value ?? {}, null, 2);
|
|
1567
|
+
/** Render one date list; a missing list stages as an empty array, not an object. */
|
|
1568
|
+
const jsonDates = (value) => JSON.stringify(Array.isArray(value) ? value : [], null, 2);
|
|
1569
|
+
/** Parse one staged date list; the Host schema rejects a malformed date. */
|
|
1570
|
+
const parseDates = (text) => {
|
|
1571
|
+
const parsed = JSON.parse(text);
|
|
1572
|
+
if (!Array.isArray(parsed)) throw new Error("expected a JSON array of YYYY-MM-DD date strings");
|
|
1573
|
+
return parsed;
|
|
1574
|
+
};
|
|
1398
1575
|
/** The section's set of user-overridden fields, read from the raw user layer. */
|
|
1399
1576
|
const overridden = (user) => typeof user === "object" && user !== null && !Array.isArray(user) ? user : {};
|
|
1400
1577
|
const CostPluginCard = (0, react.memo)(function CostPluginCard({ view, form, t }) {
|
|
@@ -1410,6 +1587,8 @@ window.__ModuleLoader__.load({
|
|
|
1410
1587
|
setStaged({
|
|
1411
1588
|
overridesText: json(value.overrides),
|
|
1412
1589
|
aliasesText: json(value.aliases),
|
|
1590
|
+
holidayRestDaysText: jsonDates(value.holidayRestDays),
|
|
1591
|
+
holidayWorkdaysText: jsonDates(value.holidayWorkdays),
|
|
1413
1592
|
cacheReadDiscount: String(value.cacheReadDiscount ?? .25),
|
|
1414
1593
|
fxMode: value.fxMode ?? "auto",
|
|
1415
1594
|
manualRate: String(value.manualRate ?? 7.2),
|
|
@@ -1449,9 +1628,13 @@ window.__ModuleLoader__.load({
|
|
|
1449
1628
|
setError(null);
|
|
1450
1629
|
let overrides;
|
|
1451
1630
|
let aliases;
|
|
1631
|
+
let restDays;
|
|
1632
|
+
let workdays;
|
|
1452
1633
|
try {
|
|
1453
1634
|
overrides = JSON.parse(staged.overridesText);
|
|
1454
1635
|
aliases = JSON.parse(staged.aliasesText);
|
|
1636
|
+
restDays = parseDates(staged.holidayRestDaysText);
|
|
1637
|
+
workdays = parseDates(staged.holidayWorkdaysText);
|
|
1455
1638
|
} catch (cause) {
|
|
1456
1639
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
1457
1640
|
setBusy(false);
|
|
@@ -1468,6 +1651,16 @@ window.__ModuleLoader__.load({
|
|
|
1468
1651
|
path: ["aliases"],
|
|
1469
1652
|
value: aliases
|
|
1470
1653
|
},
|
|
1654
|
+
{
|
|
1655
|
+
op: "set",
|
|
1656
|
+
path: ["holidayRestDays"],
|
|
1657
|
+
value: restDays
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
op: "set",
|
|
1661
|
+
path: ["holidayWorkdays"],
|
|
1662
|
+
value: workdays
|
|
1663
|
+
},
|
|
1471
1664
|
{
|
|
1472
1665
|
op: "set",
|
|
1473
1666
|
path: ["fxMode"],
|
|
@@ -1730,6 +1923,57 @@ window.__ModuleLoader__.load({
|
|
|
1730
1923
|
})
|
|
1731
1924
|
})]
|
|
1732
1925
|
}),
|
|
1926
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1927
|
+
className: CostPluginCard_module_css_default.field,
|
|
1928
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1929
|
+
className: CostPluginCard_module_css_default.head,
|
|
1930
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1931
|
+
className: CostPluginCard_module_css_default.label,
|
|
1932
|
+
children: t("settings.holidayRestDays")
|
|
1933
|
+
}), "holidayRestDays" in user && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1934
|
+
className: CostPluginCard_module_css_default.overridden,
|
|
1935
|
+
children: t("settings.overridden")
|
|
1936
|
+
})]
|
|
1937
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1938
|
+
className: CostPluginCard_module_css_default.textarea,
|
|
1939
|
+
value: staged?.holidayRestDaysText ?? "",
|
|
1940
|
+
spellCheck: false,
|
|
1941
|
+
readOnly,
|
|
1942
|
+
onChange: (event) => setStaged((prev) => prev === null ? prev : {
|
|
1943
|
+
...prev,
|
|
1944
|
+
holidayRestDaysText: event.target.value
|
|
1945
|
+
})
|
|
1946
|
+
})]
|
|
1947
|
+
}),
|
|
1948
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1949
|
+
className: CostPluginCard_module_css_default.field,
|
|
1950
|
+
children: [
|
|
1951
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1952
|
+
className: CostPluginCard_module_css_default.head,
|
|
1953
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1954
|
+
className: CostPluginCard_module_css_default.label,
|
|
1955
|
+
children: t("settings.holidayWorkdays")
|
|
1956
|
+
}), "holidayWorkdays" in user && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1957
|
+
className: CostPluginCard_module_css_default.overridden,
|
|
1958
|
+
children: t("settings.overridden")
|
|
1959
|
+
})]
|
|
1960
|
+
}),
|
|
1961
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1962
|
+
className: CostPluginCard_module_css_default.textarea,
|
|
1963
|
+
value: staged?.holidayWorkdaysText ?? "",
|
|
1964
|
+
spellCheck: false,
|
|
1965
|
+
readOnly,
|
|
1966
|
+
onChange: (event) => setStaged((prev) => prev === null ? prev : {
|
|
1967
|
+
...prev,
|
|
1968
|
+
holidayWorkdaysText: event.target.value
|
|
1969
|
+
})
|
|
1970
|
+
}),
|
|
1971
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1972
|
+
className: CostPluginCard_module_css_default.hint,
|
|
1973
|
+
children: t("settings.holidayHint")
|
|
1974
|
+
})
|
|
1975
|
+
]
|
|
1976
|
+
}),
|
|
1733
1977
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1734
1978
|
className: CostPluginCard_module_css_default.error,
|
|
1735
1979
|
children: t("settings.error", { error })
|