dsh-peak-status 1.0.0 → 1.0.2

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
@@ -2,91 +2,48 @@
2
2
 
3
3
  English | [中文](README.zh-CN.md)
4
4
 
5
- A peak-hour indicator for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web GUI. A small native chip in the session header shows whether the current **Beijing time** falls inside DeepSeek's API **peak window**, with a live clock, a countdown to the next transition, and a pricing hint.
6
-
7
5
  ![peak indicator preview](assets/peak-indicator.png)
8
6
 
9
- ## What it shows
7
+ A peak-hour indicator for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web GUI. A small native chip in the session header shows whether the current **Beijing time** is inside DeepSeek's API **peak window**, with a live clock, a countdown to the next transition, and a pricing hint.
8
+
9
+ ## Peak windows
10
10
 
11
- DeepSeek's official peak/valley API pricing took effect on **2026-08-17**:
11
+ DeepSeek's official peak/valley API pricing (effective 2026-08-17, weekends adjusted 2026-08-23):
12
12
 
13
- | State | Beijing time (daily) | Price |
13
+ | State | When (Beijing time) | Price |
14
14
  | :-- | :-- | :-- |
15
- | 🔴 **高峰 / peak** | 09:00–12:00 and 14:00–18:00 | peak price (= off-peak ×2) |
16
- | 🟢 **空闲 / off-peak** | everything else | half of peak |
15
+ | 🔴 **Peak** | weekdays 09:00–12:00 and 14:00–18:00 | peak price (= off-peak ×2) |
16
+ | 🟢 **Off-peak** | everything else — including all of Saturday & Sunday | half of peak |
17
17
 
18
- The chip (top-right of the conversation header, inside the native `conversation.session.header.utilities` strip) renders:
18
+ Weekends (Saturday and Sunday) are charged at the valley price all day — no peak windows on weekends.
19
19
 
20
- - a colored status dot — **red = peak, green = off-peak**
21
- - the live Beijing clock (`HH:MM:SS`, timezone `Asia/Shanghai`, independent of the machine's timezone)
22
- - a countdown: `剩 3h 54m · 价格 ×2` (peak) / `距高峰 3h 54m · 半价` (off-peak)
23
- - click to expand a small popover with the full detail: peak windows, next transition with `HH:MM:SS` countdown, a phase progress bar, and the pricing note
20
+ ## What you get
24
21
 
25
- Everything is themed with the web UI's native `--dsw-alias-*` variables, so it blends into the interface no floating overlays.
22
+ - A colored status dot **red = peak, green = off-peak** plus the live Beijing clock (`HH:MM:SS`, timezone `Asia/Shanghai`, independent of your machine's timezone)
23
+ - A countdown: `剩 3h 54m · 价格 ×2` during peak / `距高峰 3h 54m · 半价` off-peak
24
+ - Click the chip to expand a detail card: peak windows, the next transition with a `HH:MM:SS` countdown, and a progress bar of the current phase
25
+ - Fully themed with the web UI's native `--dsw-alias-*` variables — it blends into the interface instead of floating over it
26
+ - Mounted persistently: it survives dsh web restarts
26
27
 
27
28
  ## Install
28
29
 
29
- The plugin is a real package mounted through the profile's patch layer, so it **survives dsh web restarts**.
30
-
31
- ### Via the dsh CLI (after publishing to npm)
32
-
33
30
  ```sh
34
31
  dsh plugin --profile web add dsh-peak-status
35
32
  ```
36
33
 
37
- ### Manual / from source
38
-
39
- 1. Copy (or link) the `dsh-peak-status` folder into the profile's `node_modules`:
40
-
41
- ```sh
42
- # from the repo root
43
- cp -R . "$HOME/.dsh/profiles/web/node_modules/dsh-peak-status"
44
- ```
45
-
46
- 2. Add one insert to `~/.dsh/profiles/web/cordis.patch.yml`:
47
-
48
- ```yaml
49
- - insert:
50
- - id: dsh-peak-status
51
- name: 'dsh-peak-status'
52
- ```
53
-
54
- 3. Refresh the page (the patch file is watched and hot-reloads); restart `dsh web` if the chip does not appear.
55
-
56
- ## Build
57
-
58
- The shipped `lib/` is generated from `src/`:
34
+ Prefer installing straight from GitHub:
59
35
 
60
36
  ```sh
61
- npm run build # copies src/*.js -> lib/
62
- npm run check # syntax-check both halves
63
- npm test # boundary tests for the peak algorithm
37
+ dsh plugin --profile web add github:xiaoyi-xx/dsh-peak-status
64
38
  ```
65
39
 
66
- `npm publish` runs `prepack` (build) automatically.
67
-
68
- ## Peak window policy
69
-
70
- The peak windows are hard-coded in `src/client.js` (`BOUNDS = [540, 720, 840, 1080]` — 09:00, 12:00, 14:00, 18:00 in minutes of day). If DeepSeek ever changes the schedule, update that one constant and rebuild. Open an issue or PR in this repository and the community plugin list ([awesome-dsh-plugin](https://awesome-dsh-plugin.com)) will pick it up.
40
+ Then restart `dsh web` (or refresh the page). The chip appears at the top-right of the conversation header.
71
41
 
72
- ## Repository layout
42
+ ## Usage
73
43
 
74
- ```
75
- dsh-peak-status/
76
- ├── src/ # source of truth (host half + browser half)
77
- ├── lib/ # built output, committed for zero-friction installs
78
- ├── scripts/ # build + tests
79
- ├── cordis.patch.yml # bundle patch used by `dsh plugin add`
80
- └── package.json # dsh.bundle / dsh.client declarations
81
- ```
82
-
83
- ## Publish
84
-
85
- ```sh
86
- npm publish # prepack builds lib/ automatically
87
- ```
44
+ The chip updates every second. Click it to expand/collapse the detail card; hover for a quick tooltip. Peak/off-peak state follows the official schedule above, computed in Beijing time.
88
45
 
89
- Then `dsh plugin --profile web add dsh-peak-status` installs the published version.
46
+ If DeepSeek ever changes the peak schedule, a new release will ship with the updated windows — open an issue on this repository if you notice a mismatch.
90
47
 
91
48
  ## License
92
49
 
package/README.zh-CN.md CHANGED
@@ -2,91 +2,48 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web GUI 的高峰时段指示灯。在会话头部右上角(原生 `conversation.session.header.utilities` 工具位)显示一个融入界面的小胶囊,实时告诉你当前**北京时间**是否处于 DeepSeek API 的**高峰时段**,带秒级时钟、下一转折倒计时和价格提示。
6
-
7
5
  ![peak indicator 预览](assets/peak-indicator.png)
8
6
 
9
- ## 功能
7
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web GUI 的高峰时段指示灯。在会话头部右上角显示一个融入界面的小胶囊,实时告诉你当前**北京时间**是否处于 DeepSeek API 的**高峰时段**,带秒级时钟、下一转折倒计时和价格提示。
8
+
9
+ ## 高峰时段
10
10
 
11
- DeepSeek 官方峰谷定价于 **2026-08-17** 生效:
11
+ DeepSeek 官方峰谷定价(2026-08-17 生效,2026-08-23 起周末调整):
12
12
 
13
- | 状态 | 北京时间(每日) | 价格 |
13
+ | 状态 | 时段(北京时间) | 价格 |
14
14
  | :-- | :-- | :-- |
15
- | 🔴 **高峰** | 09:00–12:00 和 14:00–18:00 | 高峰价(= 空闲 ×2) |
16
- | 🟢 **空闲** | 其余时间 | 高峰的一半 |
15
+ | 🔴 **高峰** | 工作日 09:00–12:00 和 14:00–18:00 | 高峰价(= 空闲 ×2) |
16
+ | 🟢 **空闲** | 其余时间——包括周六、周日全天 | 高峰的一半 |
17
17
 
18
- 胶囊显示内容:
18
+ 周末(周六、周日)全天按谷价计费,不设高峰时段。
19
19
 
20
- - 状态圆点——**红 = 高峰,绿 = 空闲**
21
- - 实时北京时钟(`HH:MM:SS`,时区 `Asia/Shanghai`,与本机时区无关)
22
- - 倒计时:`剩 3h 54m · 价格 ×2`(高峰)/ `距高峰 3h 54m · 半价`(空闲)
23
- - 点击展开下拉详情:高峰窗口、下一转折(含 `HH:MM:SS` 倒计时)、当前阶段进度条、价格说明
20
+ ## 功能
24
21
 
25
- 全部使用 Web UI 原生 `--dsw-alias-*` 主题变量,与界面完全融合,没有悬浮贴片。
22
+ - 状态圆点——**红 = 高峰,绿 = 空闲**,外加实时北京时钟(`HH:MM:SS`,时区 `Asia/Shanghai`,与本机时区无关)
23
+ - 倒计时:高峰时显示 `剩 3h 54m · 价格 ×2`,空闲时显示 `距高峰 3h 54m · 半价`
24
+ - 点击胶囊展开详情卡:高峰窗口、下一转折(含 `HH:MM:SS` 倒计时)、当前阶段进度条
25
+ - 全部使用 Web UI 原生 `--dsw-alias-*` 主题变量,与界面完全融合,没有悬浮贴片
26
+ - 持久挂载:重启 dsh web 后依然存在
26
27
 
27
28
  ## 安装
28
29
 
29
- 插件是经 profile 补丁层挂载的真实包,**重启 dsh web 不会消失**。
30
-
31
- ### dsh CLI 安装(发布到 npm 后)
32
-
33
30
  ```sh
34
31
  dsh plugin --profile web add dsh-peak-status
35
32
  ```
36
33
 
37
- ### 手动 / 源码安装
38
-
39
- 1. 把 `dsh-peak-status` 目录复制(或链接)进 profile 的 `node_modules`:
40
-
41
- ```sh
42
- # 在仓库根目录执行
43
- cp -R . "$HOME/.dsh/profiles/web/node_modules/dsh-peak-status"
44
- ```
45
-
46
- 2. 在 `~/.dsh/profiles/web/cordis.patch.yml` 里加一行插入:
47
-
48
- ```yaml
49
- - insert:
50
- - id: dsh-peak-status
51
- name: 'dsh-peak-status'
52
- ```
53
-
54
- 3. 刷新页面(补丁文件有监听、支持热加载);没出现就重启一次 `dsh web`。
55
-
56
- ## 构建
57
-
58
- 发布的 `lib/` 由 `src/` 生成:
34
+ 也可以直接从 GitHub 安装:
59
35
 
60
36
  ```sh
61
- npm run build # src/*.js 复制到 lib/
62
- npm run check # 两个半区的语法检查
63
- npm test # 高峰判定算法边界测试
37
+ dsh plugin --profile web add github:xiaoyi-xx/dsh-peak-status
64
38
  ```
65
39
 
66
- `npm publish` 会自动执行 `prepack`(构建)。
67
-
68
- ## 高峰时段策略
69
-
70
- 高峰窗口硬编码在 `src/client.js` 的 `BOUNDS = [540, 720, 840, 1080]`(一天中的分钟:09:00、12:00、14:00、18:00)。如果 DeepSeek 以后调整时段,改这一个常量重新构建即可;也可以在本仓库提 Issue/PR,社区插件列表([awesome-dsh-plugin](https://awesome-dsh-plugin.com))会自动收录。
71
-
72
- ## 目录结构
40
+ 然后重启 `dsh web`(或刷新页面),胶囊会出现在会话头部右上角。
73
41
 
74
- ```
75
- dsh-peak-status/
76
- ├── src/ # 源码(宿主半区 + 浏览器半区)
77
- ├── lib/ # 构建产物(已提交,零门槛安装)
78
- ├── scripts/ # 构建与测试脚本
79
- ├── cordis.patch.yml # dsh plugin add 使用的 bundle 补丁
80
- └── package.json # dsh.bundle / dsh.client 声明
81
- ```
82
-
83
- ## 发布
42
+ ## 使用
84
43
 
85
- ```sh
86
- npm publish # prepack 会自动构建 lib/
87
- ```
44
+ 胶囊每秒自动更新。点击展开/收起详情卡,悬停有快捷提示。高峰/空闲状态严格按上面官方时刻表、以北京时间计算。
88
45
 
89
- 之后 `dsh plugin --profile web add dsh-peak-status` 即可安装已发布版本。
46
+ 如果 DeepSeek 以后调整高峰时段,我们会发布包含新时段的新版本——发现不一致欢迎在仓库提 Issue。
90
47
 
91
48
  ## 许可证
92
49
 
package/lib/client.js CHANGED
@@ -7,9 +7,11 @@
7
7
  * variables so it blends into the interface.
8
8
  *
9
9
  * Shows whether the current Beijing time falls in DeepSeek's API peak
10
- * window. Official definition (effective 2026-08-17):
11
- * peak: daily 09:00–12:00 and 14:00–18:00 (Beijing time)
12
- * off-peak: everything else (priced at half of peak)
10
+ * window. Official pricing (peak/valley, effective 2026-08-17, weekends
11
+ * adjusted 2026-08-23):
12
+ * weekdays peak: 09:00–12:00 and 14:00–18:00 (Beijing time)
13
+ * weekends: Saturday & Sunday all day at the valley price
14
+ * off-peak: everything else (priced at half of peak)
13
15
  *
14
16
  * Red = peak (bold), green = off-peak; live Beijing clock, countdown to the
15
17
  * next transition, click to expand a small popover with the full detail.
@@ -42,44 +44,84 @@
42
44
  var react = require('react')
43
45
 
44
46
  var TZ = 'Asia/Shanghai'
45
- // Peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
47
+ // Weekday peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
46
48
  var BOUNDS = [540, 720, 840, 1080]
49
+ // Weekday labels for "next boundary" display (ISO weekday 1..7).
50
+ var WEEKDAY_LABELS = ['一', '二', '三', '四', '五', '六', '日']
47
51
 
48
52
  var FONT = 'var(--dsw-font-family, system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif)'
49
53
  var DIM = 'var(--dsw-alias-label-secondary, var(--dsh-text-dim, #8a8f98))'
50
54
  var PEAK_COLOR = 'var(--dsw-alias-state-error-primary, #e5484d)'
51
55
  var OFF_COLOR = 'var(--dsw-alias-state-success-primary, #30a46c)'
52
56
 
57
+ /** Beijing time parts incl. ISO weekday (1=Mon … 7=Sun), timezone-proof. */
53
58
  function bjParts() {
54
59
  var parts = new Intl.DateTimeFormat('en-GB', {
55
- timeZone: TZ, hour12: false,
60
+ timeZone: TZ, hour12: false, weekday: 'short',
56
61
  hour: '2-digit', minute: '2-digit', second: '2-digit',
57
62
  }).formatToParts(new Date())
58
63
  var get = function (type) {
59
64
  for (var i = 0; i < parts.length; i++) {
60
- if (parts[i].type === type) return Number(parts[i].value)
65
+ if (parts[i].type === type) return parts[i].value
61
66
  }
62
- return 0
67
+ return ''
68
+ }
69
+ var wdMap = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 }
70
+ return {
71
+ h: Number(get('hour')),
72
+ m: Number(get('minute')),
73
+ s: Number(get('second')),
74
+ weekday: wdMap[get('weekday')] || 1,
63
75
  }
64
- return { h: get('hour'), m: get('minute'), s: get('second') }
65
76
  }
66
77
 
78
+ /**
79
+ * Peak/off-peak state at a Beijing-time instant.
80
+ * Weekends (Sat/Sun) are all-day off-peak; weekdays follow the peak
81
+ * windows 09:00–12:00 / 14:00–18:00. Returns the next state boundary as
82
+ * { nextMin, daysAhead } (daysAhead counts how many Beijing days away),
83
+ * seconds until it, and the progress through the current phase.
84
+ */
67
85
  function compute(t) {
68
86
  var now = t.h * 60 + t.m
69
- var peak = (now >= BOUNDS[0] && now < BOUNDS[1]) || (now >= BOUNDS[2] && now < BOUNDS[3])
70
- var next = null
71
- for (var i = 0; i < BOUNDS.length; i++) {
72
- if (BOUNDS[i] > now) { next = { min: BOUNDS[i], tomorrow: false }; break }
73
- }
74
- if (next === null) next = { min: BOUNDS[0], tomorrow: true }
75
- var secsToNext = (next.min - now) * 60 - t.s + (next.tomorrow ? 24 * 3600 : 0)
76
- var phaseStart = 0
77
- for (var j = BOUNDS.length - 1; j >= 0; j--) {
78
- if (BOUNDS[j] <= now) { phaseStart = BOUNDS[j]; break }
87
+ var weekend = t.weekday === 6 || t.weekday === 7
88
+ var peak, nextMin, daysAhead, phaseStartMin, phaseLenMin
89
+
90
+ if (weekend) {
91
+ peak = false
92
+ nextMin = BOUNDS[0] // Monday 09:00
93
+ daysAhead = ((1 - t.weekday + 7) % 7) || 7 // Sat 2, Sun 1
94
+ var daysSinceSat = (t.weekday - 6 + 7) % 7 // Sat=0, Sun=1
95
+ phaseStartMin = daysSinceSat * 1440
96
+ phaseLenMin = 2 * 1440 + BOUNDS[0] // Sat 00:00 → Mon 09:00
97
+ } else {
98
+ peak = (now >= BOUNDS[0] && now < BOUNDS[1]) || (now >= BOUNDS[2] && now < BOUNDS[3])
99
+ var next = null
100
+ for (var i = 0; i < BOUNDS.length; i++) {
101
+ if (BOUNDS[i] > now) { next = BOUNDS[i]; break }
102
+ }
103
+ if (next !== null) {
104
+ nextMin = next
105
+ daysAhead = 0
106
+ var ps = 0
107
+ for (var j = BOUNDS.length - 1; j >= 0; j--) {
108
+ if (BOUNDS[j] <= now) { ps = BOUNDS[j]; break }
109
+ }
110
+ phaseStartMin = ps
111
+ phaseLenMin = nextMin - ps
112
+ } else {
113
+ nextMin = BOUNDS[0]
114
+ // After 18:00: Friday → next Monday 09:00 (3 days), otherwise next day 09:00.
115
+ daysAhead = t.weekday === 5 ? 3 : 1
116
+ phaseStartMin = BOUNDS[3]
117
+ phaseLenMin = daysAhead * 1440 + BOUNDS[0] - phaseStartMin
118
+ }
79
119
  }
80
- var phaseLenMin = (next.min - now) + (next.tomorrow ? 1440 : 0) + (now - phaseStart)
81
- var progress = phaseLenMin > 0 ? Math.min(100, Math.max(0, ((now - phaseStart) / phaseLenMin) * 100)) : 0
82
- return { peak: peak, next: next, secsToNext: secsToNext, progress: progress }
120
+
121
+ var secsToNext = daysAhead * 86400 + (nextMin - now) * 60 - t.s
122
+ var elapsedMin = weekend ? daysSinceSat * 1440 + now : now - phaseStartMin
123
+ var progress = phaseLenMin > 0 ? Math.min(100, Math.max(0, (elapsedMin / phaseLenMin) * 100)) : 0
124
+ return { peak: peak, nextMin: nextMin, daysAhead: daysAhead, secsToNext: secsToNext, progress: progress }
83
125
  }
84
126
 
85
127
  function pad(n) { return (n < 10 ? '0' : '') + n }
@@ -87,8 +129,10 @@
87
129
  function fmtBoundary(min) { return pad(Math.floor(min / 60)) + ':' + pad(min % 60) }
88
130
  function fmtDur(totalSecs) {
89
131
  var s = Math.max(0, Math.round(totalSecs))
90
- var h = Math.floor(s / 3600)
132
+ var d = Math.floor(s / 86400)
133
+ var h = Math.floor((s % 86400) / 3600)
91
134
  var m = Math.floor((s % 3600) / 60)
135
+ if (d > 0) return d + 'd ' + h + 'h'
92
136
  return (h > 0 ? h + 'h ' : '') + m + 'm'
93
137
  }
94
138
  function fmtDurFull(totalSecs) {
@@ -98,6 +142,13 @@
98
142
  var sec = s % 60
99
143
  return pad(h) + ':' + pad(m) + ':' + pad(sec)
100
144
  }
145
+ /** Human label for the next boundary: "18:00" / "明天 09:00" / "周一 09:00". */
146
+ function fmtBoundaryLabel(t, s) {
147
+ if (s.daysAhead === 0) return fmtBoundary(s.nextMin)
148
+ if (s.daysAhead === 1) return '明天 ' + fmtBoundary(s.nextMin)
149
+ var nextW = ((t.weekday - 1 + s.daysAhead) % 7) + 1
150
+ return '周' + WEEKDAY_LABELS[nextW - 1] + ' ' + fmtBoundary(s.nextMin)
151
+ }
101
152
 
102
153
  function PeakChip() {
103
154
  var tickState = react.useState(bjParts())
@@ -112,17 +163,18 @@
112
163
  }, [])
113
164
 
114
165
  var s = compute(tick)
166
+ var weekend = tick.weekday === 6 || tick.weekday === 7
115
167
  var color = s.peak ? PEAK_COLOR : OFF_COLOR
116
168
  var clock = fmtClock(tick)
117
- var boundaryLabel = fmtBoundary(s.next.min) + (s.next.tomorrow ? '(明天)' : '')
169
+ var boundaryLabel = fmtBoundaryLabel(tick, s)
118
170
 
119
171
  var chip = react.createElement(
120
172
  'div',
121
173
  {
122
174
  onClick: function () { setOpen(!open[0]) },
123
175
  title: s.peak
124
- ? '高峰期(北京时间 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
125
- : '空闲期:价格为高峰一半。下一高峰 ' + boundaryLabel + ',点击展开详情',
176
+ ? '高峰期(工作日 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
177
+ : '空闲期:价格为高峰一半' + (weekend ? '(周末全天谷价)' : '') + '。下一高峰 ' + boundaryLabel + ',点击展开详情',
126
178
  style: {
127
179
  display: 'inline-flex', alignItems: 'center', gap: 6,
128
180
  fontFamily: FONT, fontSize: 12, lineHeight: '20px',
@@ -135,7 +187,7 @@
135
187
  [
136
188
  react.createElement('span', { key: 'dot', style: { width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 } }),
137
189
  react.createElement('span', { key: 'state', style: { fontWeight: s.peak ? 700 : 500, color: color } },
138
- s.peak ? '高峰期' : '空闲期'),
190
+ s.peak ? '高峰期' : (weekend ? '周末谷价' : '空闲期')),
139
191
  react.createElement('span', { key: 'clock', style: { fontVariantNumeric: 'tabular-nums' } }, clock),
140
192
  react.createElement('span', { key: 'count', style: { fontVariantNumeric: 'tabular-nums' } },
141
193
  s.peak
@@ -164,14 +216,16 @@
164
216
  [
165
217
  react.createElement('div', { key: 'l1', style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
166
218
  react.createElement('span', { key: 'st', style: { fontWeight: 700, color: color } },
167
- s.peak ? '高峰时段' : '空闲时段'),
219
+ s.peak ? '高峰时段' : (weekend ? '周末谷价' : '空闲时段')),
168
220
  react.createElement('span', { key: 'tm', style: { fontVariantNumeric: 'tabular-nums' } },
169
221
  clock + '(北京)'),
170
222
  react.createElement('span', { key: 'price', style: { color: color, fontWeight: 500 } },
171
223
  s.peak ? '价格 ×2' : '半价'),
172
224
  ]),
173
225
  react.createElement('div', { key: 'l2' },
174
- '高峰窗口:09:00–12:00 / 14:00–18:00(每日)'),
226
+ '高峰窗口(工作日):09:00–12:00 / 14:00–18:00'),
227
+ react.createElement('div', { key: 'l2b', style: { color: weekend ? OFF_COLOR : DIM, fontWeight: weekend ? 600 : 400 } },
228
+ '周末(周六、周日):全天按谷价计费'),
175
229
  react.createElement('div', { key: 'l3', style: { fontVariantNumeric: 'tabular-nums' } },
176
230
  '下一转折:' + boundaryLabel + ' · 倒计时 ' + fmtDurFull(s.secsToNext)),
177
231
  react.createElement('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
@@ -185,7 +239,7 @@
185
239
  s.progress.toFixed(0) + '%'),
186
240
  ]),
187
241
  react.createElement('div', { key: 'foot', style: { fontSize: 10.5, lineHeight: 1.5, opacity: 0.85 } },
188
- '官方峰谷定价 2026-08-17 生效:空闲时段价格为高峰一半。'),
242
+ '官方峰谷定价 8/17 生效,8/23 起周末全天谷价;空闲价 = 高峰一半。'),
189
243
  ],
190
244
  )
191
245
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-peak-status",
3
- "version": "1.0.0",
4
- "description": "Peak-hour indicator for the DSH web GUI: a native header chip showing whether the current Beijing time is inside DeepSeek's API peak window (daily 09:00–12:00 / 14:00–18:00), with a live clock, countdown, and ×2/off-peak pricing hints. · DSH Web GUI 高峰指示灯:原生会话头部胶囊,实时显示当前北京时间是否处于 DeepSeek API 高峰时段。",
3
+ "version": "1.0.2",
4
+ "description": "Peak-hour indicator for the DSH web GUI: a native header chip showing whether the current Beijing time is inside DeepSeek's API peak window (weekdays 09:00–12:00 / 14:00–18:00; weekends all-day valley price), with a live clock, countdown, and ×2/off-peak pricing hints. · DSH Web GUI 高峰指示灯:原生会话头部胶囊,实时显示当前北京时间是否处于 DeepSeek API 高峰时段(工作日 9:00-12:00 / 14:00-18:00,周末全天谷价)。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
package/src/client.js CHANGED
@@ -7,9 +7,11 @@
7
7
  * variables so it blends into the interface.
8
8
  *
9
9
  * Shows whether the current Beijing time falls in DeepSeek's API peak
10
- * window. Official definition (effective 2026-08-17):
11
- * peak: daily 09:00–12:00 and 14:00–18:00 (Beijing time)
12
- * off-peak: everything else (priced at half of peak)
10
+ * window. Official pricing (peak/valley, effective 2026-08-17, weekends
11
+ * adjusted 2026-08-23):
12
+ * weekdays peak: 09:00–12:00 and 14:00–18:00 (Beijing time)
13
+ * weekends: Saturday & Sunday all day at the valley price
14
+ * off-peak: everything else (priced at half of peak)
13
15
  *
14
16
  * Red = peak (bold), green = off-peak; live Beijing clock, countdown to the
15
17
  * next transition, click to expand a small popover with the full detail.
@@ -42,44 +44,84 @@
42
44
  var react = require('react')
43
45
 
44
46
  var TZ = 'Asia/Shanghai'
45
- // Peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
47
+ // Weekday peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
46
48
  var BOUNDS = [540, 720, 840, 1080]
49
+ // Weekday labels for "next boundary" display (ISO weekday 1..7).
50
+ var WEEKDAY_LABELS = ['一', '二', '三', '四', '五', '六', '日']
47
51
 
48
52
  var FONT = 'var(--dsw-font-family, system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif)'
49
53
  var DIM = 'var(--dsw-alias-label-secondary, var(--dsh-text-dim, #8a8f98))'
50
54
  var PEAK_COLOR = 'var(--dsw-alias-state-error-primary, #e5484d)'
51
55
  var OFF_COLOR = 'var(--dsw-alias-state-success-primary, #30a46c)'
52
56
 
57
+ /** Beijing time parts incl. ISO weekday (1=Mon … 7=Sun), timezone-proof. */
53
58
  function bjParts() {
54
59
  var parts = new Intl.DateTimeFormat('en-GB', {
55
- timeZone: TZ, hour12: false,
60
+ timeZone: TZ, hour12: false, weekday: 'short',
56
61
  hour: '2-digit', minute: '2-digit', second: '2-digit',
57
62
  }).formatToParts(new Date())
58
63
  var get = function (type) {
59
64
  for (var i = 0; i < parts.length; i++) {
60
- if (parts[i].type === type) return Number(parts[i].value)
65
+ if (parts[i].type === type) return parts[i].value
61
66
  }
62
- return 0
67
+ return ''
68
+ }
69
+ var wdMap = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 }
70
+ return {
71
+ h: Number(get('hour')),
72
+ m: Number(get('minute')),
73
+ s: Number(get('second')),
74
+ weekday: wdMap[get('weekday')] || 1,
63
75
  }
64
- return { h: get('hour'), m: get('minute'), s: get('second') }
65
76
  }
66
77
 
78
+ /**
79
+ * Peak/off-peak state at a Beijing-time instant.
80
+ * Weekends (Sat/Sun) are all-day off-peak; weekdays follow the peak
81
+ * windows 09:00–12:00 / 14:00–18:00. Returns the next state boundary as
82
+ * { nextMin, daysAhead } (daysAhead counts how many Beijing days away),
83
+ * seconds until it, and the progress through the current phase.
84
+ */
67
85
  function compute(t) {
68
86
  var now = t.h * 60 + t.m
69
- var peak = (now >= BOUNDS[0] && now < BOUNDS[1]) || (now >= BOUNDS[2] && now < BOUNDS[3])
70
- var next = null
71
- for (var i = 0; i < BOUNDS.length; i++) {
72
- if (BOUNDS[i] > now) { next = { min: BOUNDS[i], tomorrow: false }; break }
73
- }
74
- if (next === null) next = { min: BOUNDS[0], tomorrow: true }
75
- var secsToNext = (next.min - now) * 60 - t.s + (next.tomorrow ? 24 * 3600 : 0)
76
- var phaseStart = 0
77
- for (var j = BOUNDS.length - 1; j >= 0; j--) {
78
- if (BOUNDS[j] <= now) { phaseStart = BOUNDS[j]; break }
87
+ var weekend = t.weekday === 6 || t.weekday === 7
88
+ var peak, nextMin, daysAhead, phaseStartMin, phaseLenMin
89
+
90
+ if (weekend) {
91
+ peak = false
92
+ nextMin = BOUNDS[0] // Monday 09:00
93
+ daysAhead = ((1 - t.weekday + 7) % 7) || 7 // Sat 2, Sun 1
94
+ var daysSinceSat = (t.weekday - 6 + 7) % 7 // Sat=0, Sun=1
95
+ phaseStartMin = daysSinceSat * 1440
96
+ phaseLenMin = 2 * 1440 + BOUNDS[0] // Sat 00:00 → Mon 09:00
97
+ } else {
98
+ peak = (now >= BOUNDS[0] && now < BOUNDS[1]) || (now >= BOUNDS[2] && now < BOUNDS[3])
99
+ var next = null
100
+ for (var i = 0; i < BOUNDS.length; i++) {
101
+ if (BOUNDS[i] > now) { next = BOUNDS[i]; break }
102
+ }
103
+ if (next !== null) {
104
+ nextMin = next
105
+ daysAhead = 0
106
+ var ps = 0
107
+ for (var j = BOUNDS.length - 1; j >= 0; j--) {
108
+ if (BOUNDS[j] <= now) { ps = BOUNDS[j]; break }
109
+ }
110
+ phaseStartMin = ps
111
+ phaseLenMin = nextMin - ps
112
+ } else {
113
+ nextMin = BOUNDS[0]
114
+ // After 18:00: Friday → next Monday 09:00 (3 days), otherwise next day 09:00.
115
+ daysAhead = t.weekday === 5 ? 3 : 1
116
+ phaseStartMin = BOUNDS[3]
117
+ phaseLenMin = daysAhead * 1440 + BOUNDS[0] - phaseStartMin
118
+ }
79
119
  }
80
- var phaseLenMin = (next.min - now) + (next.tomorrow ? 1440 : 0) + (now - phaseStart)
81
- var progress = phaseLenMin > 0 ? Math.min(100, Math.max(0, ((now - phaseStart) / phaseLenMin) * 100)) : 0
82
- return { peak: peak, next: next, secsToNext: secsToNext, progress: progress }
120
+
121
+ var secsToNext = daysAhead * 86400 + (nextMin - now) * 60 - t.s
122
+ var elapsedMin = weekend ? daysSinceSat * 1440 + now : now - phaseStartMin
123
+ var progress = phaseLenMin > 0 ? Math.min(100, Math.max(0, (elapsedMin / phaseLenMin) * 100)) : 0
124
+ return { peak: peak, nextMin: nextMin, daysAhead: daysAhead, secsToNext: secsToNext, progress: progress }
83
125
  }
84
126
 
85
127
  function pad(n) { return (n < 10 ? '0' : '') + n }
@@ -87,8 +129,10 @@
87
129
  function fmtBoundary(min) { return pad(Math.floor(min / 60)) + ':' + pad(min % 60) }
88
130
  function fmtDur(totalSecs) {
89
131
  var s = Math.max(0, Math.round(totalSecs))
90
- var h = Math.floor(s / 3600)
132
+ var d = Math.floor(s / 86400)
133
+ var h = Math.floor((s % 86400) / 3600)
91
134
  var m = Math.floor((s % 3600) / 60)
135
+ if (d > 0) return d + 'd ' + h + 'h'
92
136
  return (h > 0 ? h + 'h ' : '') + m + 'm'
93
137
  }
94
138
  function fmtDurFull(totalSecs) {
@@ -98,6 +142,13 @@
98
142
  var sec = s % 60
99
143
  return pad(h) + ':' + pad(m) + ':' + pad(sec)
100
144
  }
145
+ /** Human label for the next boundary: "18:00" / "明天 09:00" / "周一 09:00". */
146
+ function fmtBoundaryLabel(t, s) {
147
+ if (s.daysAhead === 0) return fmtBoundary(s.nextMin)
148
+ if (s.daysAhead === 1) return '明天 ' + fmtBoundary(s.nextMin)
149
+ var nextW = ((t.weekday - 1 + s.daysAhead) % 7) + 1
150
+ return '周' + WEEKDAY_LABELS[nextW - 1] + ' ' + fmtBoundary(s.nextMin)
151
+ }
101
152
 
102
153
  function PeakChip() {
103
154
  var tickState = react.useState(bjParts())
@@ -112,17 +163,18 @@
112
163
  }, [])
113
164
 
114
165
  var s = compute(tick)
166
+ var weekend = tick.weekday === 6 || tick.weekday === 7
115
167
  var color = s.peak ? PEAK_COLOR : OFF_COLOR
116
168
  var clock = fmtClock(tick)
117
- var boundaryLabel = fmtBoundary(s.next.min) + (s.next.tomorrow ? '(明天)' : '')
169
+ var boundaryLabel = fmtBoundaryLabel(tick, s)
118
170
 
119
171
  var chip = react.createElement(
120
172
  'div',
121
173
  {
122
174
  onClick: function () { setOpen(!open[0]) },
123
175
  title: s.peak
124
- ? '高峰期(北京时间 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
125
- : '空闲期:价格为高峰一半。下一高峰 ' + boundaryLabel + ',点击展开详情',
176
+ ? '高峰期(工作日 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
177
+ : '空闲期:价格为高峰一半' + (weekend ? '(周末全天谷价)' : '') + '。下一高峰 ' + boundaryLabel + ',点击展开详情',
126
178
  style: {
127
179
  display: 'inline-flex', alignItems: 'center', gap: 6,
128
180
  fontFamily: FONT, fontSize: 12, lineHeight: '20px',
@@ -135,7 +187,7 @@
135
187
  [
136
188
  react.createElement('span', { key: 'dot', style: { width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 } }),
137
189
  react.createElement('span', { key: 'state', style: { fontWeight: s.peak ? 700 : 500, color: color } },
138
- s.peak ? '高峰期' : '空闲期'),
190
+ s.peak ? '高峰期' : (weekend ? '周末谷价' : '空闲期')),
139
191
  react.createElement('span', { key: 'clock', style: { fontVariantNumeric: 'tabular-nums' } }, clock),
140
192
  react.createElement('span', { key: 'count', style: { fontVariantNumeric: 'tabular-nums' } },
141
193
  s.peak
@@ -164,14 +216,16 @@
164
216
  [
165
217
  react.createElement('div', { key: 'l1', style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
166
218
  react.createElement('span', { key: 'st', style: { fontWeight: 700, color: color } },
167
- s.peak ? '高峰时段' : '空闲时段'),
219
+ s.peak ? '高峰时段' : (weekend ? '周末谷价' : '空闲时段')),
168
220
  react.createElement('span', { key: 'tm', style: { fontVariantNumeric: 'tabular-nums' } },
169
221
  clock + '(北京)'),
170
222
  react.createElement('span', { key: 'price', style: { color: color, fontWeight: 500 } },
171
223
  s.peak ? '价格 ×2' : '半价'),
172
224
  ]),
173
225
  react.createElement('div', { key: 'l2' },
174
- '高峰窗口:09:00–12:00 / 14:00–18:00(每日)'),
226
+ '高峰窗口(工作日):09:00–12:00 / 14:00–18:00'),
227
+ react.createElement('div', { key: 'l2b', style: { color: weekend ? OFF_COLOR : DIM, fontWeight: weekend ? 600 : 400 } },
228
+ '周末(周六、周日):全天按谷价计费'),
175
229
  react.createElement('div', { key: 'l3', style: { fontVariantNumeric: 'tabular-nums' } },
176
230
  '下一转折:' + boundaryLabel + ' · 倒计时 ' + fmtDurFull(s.secsToNext)),
177
231
  react.createElement('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
@@ -185,7 +239,7 @@
185
239
  s.progress.toFixed(0) + '%'),
186
240
  ]),
187
241
  react.createElement('div', { key: 'foot', style: { fontSize: 10.5, lineHeight: 1.5, opacity: 0.85 } },
188
- '官方峰谷定价 2026-08-17 生效:空闲时段价格为高峰一半。'),
242
+ '官方峰谷定价 8/17 生效,8/23 起周末全天谷价;空闲价 = 高峰一半。'),
189
243
  ],
190
244
  )
191
245