dsh-session-cost-meter 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +284 -2
- package/cordis.patch.yml +63 -0
- package/lib/client.js +498 -0
- package/lib/config.js +191 -0
- package/lib/index.js +452 -0
- package/lib/tariff.js +371 -0
- package/package.json +56 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felix Li
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,285 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-session-cost-meter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A DeepSeek Harness profile bundle that answers "what did **this session** cost?"
|
|
4
|
+
It shows a cost pill beside the composer, breaks the spend down by billing bucket,
|
|
5
|
+
tariff window and model, and prices every request with the rate that was in force
|
|
6
|
+
when that request was **dispatched** — so peak/off-peak (波峰/波谷) is handled per
|
|
7
|
+
request instead of being guessed from a session total.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌ 本次会话成本 ¥2.3691 ┐
|
|
11
|
+
│ 缓存命中 24,734,848 tok ¥0.9894 │
|
|
12
|
+
│ 未缓存输入 207,128 tok ¥0.4143 │
|
|
13
|
+
│ 输出 120,684 tok ¥0.9655 │
|
|
14
|
+
│ 合计 25,062,660 tok ¥2.3691 │
|
|
15
|
+
├───────────────────────────────────────────┤
|
|
16
|
+
│ 波峰 ¥2.3691 · 波谷 ¥0.0000 │
|
|
17
|
+
├───────────────────────────────────────────┤
|
|
18
|
+
│ deepseek-flash 25.1M tok ¥2.3691 │
|
|
19
|
+
├───────────────────────────────────────────┤
|
|
20
|
+
│ ● 当前时段:波峰 · 距切换 2h13m │
|
|
21
|
+
│ 高峰:周一至周五 09:00–12:00、14:00–18:00 │
|
|
22
|
+
│ 波谷价 = 波峰价 × 0.5 │
|
|
23
|
+
└───────────────────────────────────────────┘
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Where it appears
|
|
27
|
+
|
|
28
|
+
One pill joins the existing token chip on the composer's ambient row
|
|
29
|
+
(`conversation.composer.dock`, occupant id `session-cost`, order 5, so it sits
|
|
30
|
+
immediately after the shipped `stats` entry). Clicking it opens the breakdown
|
|
31
|
+
above. All text is localised through the Client locale service (`zh` + `en`), and
|
|
32
|
+
all styling uses only `--dsw-alias-*` theme tokens, so it follows the host theme
|
|
33
|
+
and light/dark switching.
|
|
34
|
+
|
|
35
|
+
## Pricing model
|
|
36
|
+
|
|
37
|
+
Rates are per 1,000,000 tokens, quoted exactly as published — the CNY column is
|
|
38
|
+
authoritative, and the USD column is stored separately rather than derived,
|
|
39
|
+
because the page rounds it independently.
|
|
40
|
+
|
|
41
|
+
| model | 缓存命中 (hit) peak | hit off-peak | 未缓存输入 (miss) peak | miss off-peak | 输出 (output) peak | output off-peak |
|
|
42
|
+
|---|---|---|---|---|---|---|
|
|
43
|
+
| `deepseek-flash` (CNY) | 0.04 | 0.02 | 2.00 | 1.00 | 8.00 | 4.00 |
|
|
44
|
+
| `deepseek-flash` (USD) | 0.006 | 0.003 | 0.30 | 0.15 | 1.20 | 0.60 |
|
|
45
|
+
| `deepseek-v4-pro` (CNY) | 0.30 | 0.15 | 9.00 | 4.50 | 27.00 | 13.50 |
|
|
46
|
+
| `deepseek-v4-pro` (USD) | 0.044 | 0.022 | 1.32 | 0.66 | 3.96 | 1.98 |
|
|
47
|
+
|
|
48
|
+
Source: <https://api-docs.deepseek.com/zh-cn/quick_start/pricing/> (CNY) and
|
|
49
|
+
<https://api-docs.deepseek.com/quick_start/pricing/> (USD), effective
|
|
50
|
+
2026-09-10 04:00 UTC. Off-peak is exactly half of peak for every model and every
|
|
51
|
+
bucket.
|
|
52
|
+
|
|
53
|
+
### Peak (波峰) / off-peak (波谷)
|
|
54
|
+
|
|
55
|
+
The rule this implements, verbatim from DeepSeek:
|
|
56
|
+
|
|
57
|
+
> 闲时段价格为高峰时段价格的一半。北京时间周一至周五(不含中国法定节假日)9:00 - 12:00、14:00 - 18:00 为高峰时段;其余时段,包括周末及中国法定节假日全天均为空闲时段。
|
|
58
|
+
|
|
59
|
+
Peak is **Monday–Friday, 09:00–12:00 and 14:00–18:00 Beijing time**
|
|
60
|
+
(01:00–04:00 and 06:00–10:00 UTC), **excluding Chinese public holidays**.
|
|
61
|
+
Everything else — every evening, every night, all weekend, and all holiday time —
|
|
62
|
+
is off-peak at half price.
|
|
63
|
+
|
|
64
|
+
DeepSeek publishes the windows but **never the holiday calendar**, so the plugin
|
|
65
|
+
ships it: `PRC_HOLIDAYS_2026` in `lib/tariff.js` carries the 33 published 2026
|
|
66
|
+
dates from 《国务院办公厅关于2026年部分节假日安排的通知》(郭办发明电〔2025〕7 号), and
|
|
67
|
+
the bundle patch repeats the same list so an already-installed plugin is correct
|
|
68
|
+
without waiting for a restart (a test asserts the two lists are identical).
|
|
69
|
+
|
|
70
|
+
**Make-up working days stay off-peak.** The notice designates Saturday/Sunday
|
|
71
|
+
workdays in 2026 (01-04, 02-14, 02-28, 05-09, 09-20, 10-10). Those are
|
|
72
|
+
deliberately *not* holidays and *not* peak: the rule defines peak as
|
|
73
|
+
Monday–Friday and says every other period, "including weekends", is off-peak.
|
|
74
|
+
`peakWeekdays` already excludes them.
|
|
75
|
+
|
|
76
|
+
Holidays are off-peak for the **whole local day**, so on 2026-10-01 the
|
|
77
|
+
09:00–12:00 and 14:00–18:00 windows bill at half price. Without the calendar,
|
|
78
|
+
holiday hours would be billed at peak — roughly **doubling** the reported cost —
|
|
79
|
+
so the panel warns in two cases: no calendar at all, or a calendar that no longer
|
|
80
|
+
covers the year the tariff clock is in (`holidayYears` is published for this).
|
|
81
|
+
A user's `config.holidays` replaces the shipped list entirely.
|
|
82
|
+
|
|
83
|
+
### How each request is priced
|
|
84
|
+
|
|
85
|
+
* `cost = cacheMiss × missRate + cacheHit × hitRate + cacheWrite × writeRate + output × outputRate`
|
|
86
|
+
* The four buckets are the harness's **disjoint** counts. DeepSeek's API reports a
|
|
87
|
+
cache-inclusive `prompt_tokens`; the adapter already subtracts cache hits, so
|
|
88
|
+
`inputTokens` is the cache-miss count and nothing is charged twice.
|
|
89
|
+
* `reasoningTokens` is a subset of `outputTokens` and is deliberately **not**
|
|
90
|
+
charged again.
|
|
91
|
+
* `cacheWrite` is reported by the API but not billed, so its rate is 0.
|
|
92
|
+
* The tariff instant for a request is its **`step/start` time**, not the
|
|
93
|
+
`assistant/message` time. The settlement message is appended after the stream
|
|
94
|
+
finishes, so a request dispatched at 11:59 Beijing and settled at 12:01 would
|
|
95
|
+
otherwise be billed in the wrong window. Without a `step/start` the settlement
|
|
96
|
+
time is used.
|
|
97
|
+
* A model with no published rate keeps its tokens but adds **no** money; the panel
|
|
98
|
+
then warns how many tokens were excluded instead of inventing a price.
|
|
99
|
+
* Attempt samples are replaced, not accumulated: a retried request is billed once
|
|
100
|
+
per attempt, and a repeat usage sample for the same attempt does not
|
|
101
|
+
double count.
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
Tunables live in the plugin row's `config`, which the profile's own patch layer
|
|
106
|
+
can override without touching this bundle:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
# ~/.dsh/profiles/<profile>/cordis.patch.yml
|
|
110
|
+
- id: session-cost
|
|
111
|
+
config:
|
|
112
|
+
currency: USD # CNY (default) or USD
|
|
113
|
+
holidays: # replaces the shipped calendar entirely
|
|
114
|
+
- 2027-01-01
|
|
115
|
+
- 2027-01-02
|
|
116
|
+
rates:
|
|
117
|
+
offPeakMultiplier: 0.5 # off-peak multiplier
|
|
118
|
+
models:
|
|
119
|
+
deepseek-flash:
|
|
120
|
+
output: 8 # override one rate
|
|
121
|
+
my-model:
|
|
122
|
+
cacheHit: 0.01
|
|
123
|
+
cacheMiss: 0.5
|
|
124
|
+
cacheWrite: 0
|
|
125
|
+
output: 1
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`Config` is a Standard Schema validator, so a misspelled key, a negative rate, a
|
|
129
|
+
malformed date or an out-of-range window fails the plugin **loudly at startup**
|
|
130
|
+
instead of silently pricing the session with defaults. Because it is not a
|
|
131
|
+
schemastery schema, the Plugin Manager page shows these fields as unknown
|
|
132
|
+
configuration (`status: unsupported`) rather than rendering a form; the values
|
|
133
|
+
still work.
|
|
134
|
+
|
|
135
|
+
⚠️ **The shipped calendar covers 2026 only.** The State Council publishes each
|
|
136
|
+
year's arrangement in the previous November, so around 2027-01-01 this list must
|
|
137
|
+
be extended (or replaced via `config.holidays`). Until then a 2027 statutory
|
|
138
|
+
holiday would be priced as a peak weekday, **doubling** the reported cost for
|
|
139
|
+
those days — and the panel says exactly that when the tariff clock leaves the
|
|
140
|
+
covered years.
|
|
141
|
+
|
|
142
|
+
## Install
|
|
143
|
+
|
|
144
|
+
From the registry, by bundle name:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
plugin_manager install_bundle target: dsh-session-cost-meter
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
or from a checkout / tarball, by absolute path:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
plugin_manager install_bundle target: <absolute path to this directory>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Either form runs `pnpm add <spec>` in the active profile, appends the bundle to
|
|
157
|
+
`dsh.profile.bundles`, and applies the change live (`application: applied`). The
|
|
158
|
+
same is available from the CLI as `dsh plugin --profile <name> add <spec>`, and
|
|
159
|
+
from the Web UI's Plugins settings page, which lists the bundle once installed and
|
|
160
|
+
lets you disable it. Add `- id: session-cost disabled: true` to the profile patch
|
|
161
|
+
layer to turn it off without uninstalling.
|
|
162
|
+
|
|
163
|
+
## Uninstall
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
plugin_manager remove_bundle target: dsh-session-cost-meter
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Releasing
|
|
170
|
+
|
|
171
|
+
The package is a **bundle**: an npm package whose manifest declares
|
|
172
|
+
`dsh.bundle` and whose `cordis.patch.yml` inserts the loader row. Publishing is
|
|
173
|
+
therefore an ordinary npm publish, and `private` must stay absent. The `prepack`
|
|
174
|
+
script runs the whole suite, so a broken package cannot be uploaded.
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
# authenticate once, against the registry that accepts new publishes
|
|
178
|
+
npm login --registry=https://registry.npmjs.org/
|
|
179
|
+
|
|
180
|
+
# then, from the repo root
|
|
181
|
+
pnpm pack # optional: inspect the tarball first
|
|
182
|
+
pnpm publish --registry=https://registry.npmjs.org/
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`pnpm publish --dry-run` performs the full flow without uploading and is the safe
|
|
186
|
+
way to check a change to the manifest. Pass `--no-git-checks` only when
|
|
187
|
+
publishing from a detached or dirty tree.
|
|
188
|
+
|
|
189
|
+
Use the Harness's bundled Node 24 (`load_workspace_dependencies`) rather than an
|
|
190
|
+
older shell Node: `npm@11.7` on Node 23.0.0 mangles paths inside `npm pack`
|
|
191
|
+
(`ib/client.js` ENOENT) and cannot produce a tarball at all. The configured
|
|
192
|
+
`~/.npmrc` here points at `registry.npmmirror.com`, a read-only mirror — always
|
|
193
|
+
name `--registry=https://registry.npmjs.org/` explicitly when publishing.
|
|
194
|
+
|
|
195
|
+
`tests/package.test.mjs` is the release gate. A package name appears in three
|
|
196
|
+
places — the manifest, the loader row in `cordis.patch.yml`, and the Client
|
|
197
|
+
bundle's `window.__ModuleLoader__.load({ id })` — and the suite fails unless all
|
|
198
|
+
three agree, so a rename cannot ship half-applied.
|
|
199
|
+
|
|
200
|
+
## Verification status
|
|
201
|
+
|
|
202
|
+
* 50 unit tests (`node --test "tests/*.test.mjs"`) cover the published windows
|
|
203
|
+
against fixed instants, a minute-by-minute sweep of the schedule the Client
|
|
204
|
+
reads against the host rules, the host/Client pure-function copies against each
|
|
205
|
+
other, the per-bucket arithmetic, replacement/retry semantics, reference
|
|
206
|
+
stability, schema validation, config validation, the shipped 2026 holiday
|
|
207
|
+
calendar (count, format, no duplicates, make-up weekends excluded) and the
|
|
208
|
+
panel's covered/stale/missing holiday reporting.
|
|
209
|
+
* A real `deepseek-flash` session log (193 billing-relevant events, metadata
|
|
210
|
+
only — no message content) folds to the totals computed independently from the
|
|
211
|
+
same log: ¥1.16035704 over 10,816,537 tokens in 96 attempts.
|
|
212
|
+
* Live in the running profile: the host fiber is `active`, the Client occupant
|
|
213
|
+
`session-cost` is registered on `conversation.composer.dock` beside `stats`, and
|
|
214
|
+
the `sessionCost` row is checkpointed in the projection cache with values whose
|
|
215
|
+
arithmetic checks out exactly (¥2.36912192 = 0.98939392 + 0.414256 + 0.965472).
|
|
216
|
+
An independent fold of the durable log reproduced that row exactly (160
|
|
217
|
+
attempts, 28,216,576 / 209,990 / 131,665 tokens, ¥2.60196304).
|
|
218
|
+
* The holiday calendar was confirmed live after re-applying the row: the running
|
|
219
|
+
projection reports all **33** published 2026 dates, `2026-01-01` through
|
|
220
|
+
`2026-10-07`, so the National Day closure now bills off-peak.
|
|
221
|
+
* **Not verified visually**: no browser control is available in this environment,
|
|
222
|
+
so the panel's rendered appearance has not been observed; only its syntax, its
|
|
223
|
+
manifest, its live slot registration and its data are verified.
|
|
224
|
+
* Host-side code changes need an app restart to take effect (client bundles
|
|
225
|
+
hot-reload). The first install is live immediately; a row toggle re-applies
|
|
226
|
+
patch-level config without a restart.
|
|
227
|
+
|
|
228
|
+
## Design notes
|
|
229
|
+
|
|
230
|
+
* The host half is a `sessionCost` **session projection**. It registers no
|
|
231
|
+
services, subscribes to nothing, and imports **no packages** — not even Cordis
|
|
232
|
+
or a schema library — so it cannot disagree with the host about a module
|
|
233
|
+
instance. `apply(state, event)` returns the same state reference for events it
|
|
234
|
+
ignores, and `wire.view` returns the same object when the view content is
|
|
235
|
+
unchanged, so an unrelated event never re-renders a client.
|
|
236
|
+
* The client half derives the current tier from a **schedule the host sends**
|
|
237
|
+
(the tier in force plus its flip instants) rather than re-implementing the
|
|
238
|
+
window rules; the only duplicated functions are a five-line parity lookup and a
|
|
239
|
+
bucket sum, and a test extracts both from the bundle and checks them directly.
|
|
240
|
+
* No Harness Client package is imported (`react` and `react-dom` from the browser
|
|
241
|
+
module table only), which is why the pill draws its own icon and copies the
|
|
242
|
+
shipped panel's spacing instead of importing primitives.
|
|
243
|
+
* **Text color convention**: `--dsw-alias-label-secondary` is the resting tone for
|
|
244
|
+
labels, section headings, notes and the pill; `--dsw-alias-label-primary` is
|
|
245
|
+
reserved for values and names. The `--dsw-alias-state-*` tokens are *indicator*
|
|
246
|
+
colors only (the tier dot, the unpriced warning) — using one for ordinary body
|
|
247
|
+
text renders a near-invisible gray, which is a mistake this plugin made in its
|
|
248
|
+
first revision.
|
|
249
|
+
* A per-model row's token total is summed from its buckets when the host did not
|
|
250
|
+
publish a `total`, so a running host that still holds its previous module
|
|
251
|
+
generation renders correctly.
|
|
252
|
+
|
|
253
|
+
## Known limitations
|
|
254
|
+
|
|
255
|
+
* **Holiday calendar** — DeepSeek never publishes it; the plugin ships the 2026
|
|
256
|
+
list from the State Council notice and must be extended for each new year once
|
|
257
|
+
the arrangement is published (see above). `config.holidays` replaces it.
|
|
258
|
+
* **Make-up working days** — treated as off-peak, following the rule's wording
|
|
259
|
+
that peak is Monday–Friday and weekends are off-peak. If a future DeepSeek
|
|
260
|
+
notice bills makeup workdays at peak, that cannot be expressed by a weekly
|
|
261
|
+
weekday mask and would need a per-date override.
|
|
262
|
+
* **`deepseek-v4-pro` routing conflict** — DeepSeek's news post (2026-09-10) says
|
|
263
|
+
V4-Pro requests route to V4.1-Flash at Flash rates from 2026-09-14, while the
|
|
264
|
+
changelog from the same date says V4-Pro keeps serving at unchanged billing.
|
|
265
|
+
This plugin prices V4-Pro at its own published rates; override
|
|
266
|
+
`rates.models.deepseek-v4-pro` if your account bills it as Flash.
|
|
267
|
+
* **Retired ids** — `deepseek-chat` and `deepseek-reasoner` were discontinued
|
|
268
|
+
(2026-07-24) and have no current published rate, so they are reported as
|
|
269
|
+
unpriced rather than guessed. `deepseek-v4-flash` and
|
|
270
|
+
`deepseek-v4-flash-vision-exp` are aliased to `deepseek-flash`, which is what
|
|
271
|
+
the provider bills them at.
|
|
272
|
+
* **Rate drift** — DeepSeek can change prices; this table is a snapshot. Override
|
|
273
|
+
it in config rather than editing `lib/tariff.js`.
|
|
274
|
+
* **Schedule horizon** — the Client's tariff schedule covers 14 days, refreshed on
|
|
275
|
+
every session event. Past that horizon with no new events at all, the pill falls
|
|
276
|
+
back to the host's last computed tier.
|
|
277
|
+
* **Prompt-caching effects** — cost follows provider-reported cache hits, so a
|
|
278
|
+
change in cache behaviour changes the bill, not the estimate. No request is
|
|
279
|
+
predicted or pre-priced.
|
|
280
|
+
|
|
281
|
+
## Prior art
|
|
282
|
+
|
|
283
|
+
`@goodandready/dsh-cost-meter` (MIT, third-party) is an independently published
|
|
284
|
+
DSH plugin with the same goal; this bundle was written from the running harness's
|
|
285
|
+
own APIs and does not depend on it.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Bundle patch for dsh-session-cost-meter.
|
|
2
|
+
#
|
|
3
|
+
# The bundle owns exactly one loader row: the host plugin that registers the
|
|
4
|
+
# `sessionCost` session projection.
|
|
5
|
+
#
|
|
6
|
+
# `holidays` repeats the calendar lib/tariff.js ships as `PRC_HOLIDAYS_2026`.
|
|
7
|
+
# Config is read when the profile recomposes, while a running Host keeps its
|
|
8
|
+
# loaded JavaScript generation until restart, so carrying the calendar here makes
|
|
9
|
+
# an already-installed plugin correct without waiting for a restart. A test
|
|
10
|
+
# asserts the two lists are identical, and the user's profile patch layer can
|
|
11
|
+
# still override any of it, e.g.
|
|
12
|
+
#
|
|
13
|
+
# - id: session-cost
|
|
14
|
+
# config:
|
|
15
|
+
# currency: USD
|
|
16
|
+
# holidays: ['2027-01-01']
|
|
17
|
+
#
|
|
18
|
+
# Source: 《国务院办公厅关于2026年部分节假日安排的通知》(郭办发明电〔2025〕7 号).
|
|
19
|
+
- insert:
|
|
20
|
+
- id: session-cost
|
|
21
|
+
name: 'dsh-session-cost-meter'
|
|
22
|
+
config:
|
|
23
|
+
holidays:
|
|
24
|
+
# New Year's Day: January 1 (Thu) - January 3 (Sat).
|
|
25
|
+
- '2026-01-01'
|
|
26
|
+
- '2026-01-02'
|
|
27
|
+
- '2026-01-03'
|
|
28
|
+
# Spring Festival: February 15 (Sun) - February 23 (Mon).
|
|
29
|
+
- '2026-02-15'
|
|
30
|
+
- '2026-02-16'
|
|
31
|
+
- '2026-02-17'
|
|
32
|
+
- '2026-02-18'
|
|
33
|
+
- '2026-02-19'
|
|
34
|
+
- '2026-02-20'
|
|
35
|
+
- '2026-02-21'
|
|
36
|
+
- '2026-02-22'
|
|
37
|
+
- '2026-02-23'
|
|
38
|
+
# Qingming Festival: April 4 (Sat) - April 6 (Mon).
|
|
39
|
+
- '2026-04-04'
|
|
40
|
+
- '2026-04-05'
|
|
41
|
+
- '2026-04-06'
|
|
42
|
+
# Labor Day: May 1 (Fri) - May 5 (Tue).
|
|
43
|
+
- '2026-05-01'
|
|
44
|
+
- '2026-05-02'
|
|
45
|
+
- '2026-05-03'
|
|
46
|
+
- '2026-05-04'
|
|
47
|
+
- '2026-05-05'
|
|
48
|
+
# Dragon Boat Festival: June 19 (Fri) - June 21 (Sun).
|
|
49
|
+
- '2026-06-19'
|
|
50
|
+
- '2026-06-20'
|
|
51
|
+
- '2026-06-21'
|
|
52
|
+
# Mid-Autumn Festival: September 25 (Fri) - September 27 (Sun).
|
|
53
|
+
- '2026-09-25'
|
|
54
|
+
- '2026-09-26'
|
|
55
|
+
- '2026-09-27'
|
|
56
|
+
# National Day: October 1 (Thu) - October 7 (Wed).
|
|
57
|
+
- '2026-10-01'
|
|
58
|
+
- '2026-10-02'
|
|
59
|
+
- '2026-10-03'
|
|
60
|
+
- '2026-10-04'
|
|
61
|
+
- '2026-10-05'
|
|
62
|
+
- '2026-10-06'
|
|
63
|
+
- '2026-10-07'
|