dsh-peak-status 1.0.1 → 1.0.3

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.
@@ -20,15 +22,32 @@
20
22
  // The host client-modules expects a bundle to register the id of the entry
21
23
  // it is mounted as — the package name ("@local/dsh-peak-status" for a
22
24
  // profile @local install, "dsh-peak-status" when installed from npm).
23
- // Derive it from the bundle URL at runtime so one artifact works in every
24
- // mount shape (bundles are loaded via <script>, so document.currentScript
25
- // is the plugin script while this top-level code runs).
25
+ //
26
+ // Single-file loads use /plugins/<id>/client.js, so the id is derivable
27
+ // from document.currentScript.src. Since dsh 0.1.2-alpha.3 the web shell
28
+ // may serve plugin bundles as one shared combo script
29
+ // (/plugins/??<id>/client.js,<id2>/client.js&rev=...) — currentScript.src
30
+ // then names every bundle in the batch, not this one. Fall back to
31
+ // scanning the combo URL for our own package leaf, which is present in
32
+ // both install shapes.
26
33
  var MODULE_ID = (function () {
34
+ var OWN = 'dsh-peak-status'
27
35
  try {
28
36
  var el = document.currentScript
29
37
  if (el && el.src) {
30
- var m = /\/plugins\/(.+?)\/client\.js/.exec(el.src)
31
- if (m) return decodeURIComponent(m[1])
38
+ var src = el.src
39
+ var single = /\/plugins\/([^/]+)\/client\.js(?:\?|$)/.exec(src)
40
+ if (single) return decodeURIComponent(single[1])
41
+ var combo = /\/plugins\/\?\?([^&]+)/.exec(src)
42
+ if (combo) {
43
+ var parts = combo[1].split(',')
44
+ for (var i = 0; i < parts.length; i++) {
45
+ var m = /^(.+?)\/client\.js$/.exec(parts[i])
46
+ if (m && decodeURIComponent(m[1]).split('/').pop() === OWN) {
47
+ return decodeURIComponent(m[1])
48
+ }
49
+ }
50
+ }
32
51
  }
33
52
  } catch (error) { /* ignore */ }
34
53
  return null
@@ -42,44 +61,84 @@
42
61
  var react = require('react')
43
62
 
44
63
  var TZ = 'Asia/Shanghai'
45
- // Peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
64
+ // Weekday peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
46
65
  var BOUNDS = [540, 720, 840, 1080]
66
+ // Weekday labels for "next boundary" display (ISO weekday 1..7).
67
+ var WEEKDAY_LABELS = ['一', '二', '三', '四', '五', '六', '日']
47
68
 
48
69
  var FONT = 'var(--dsw-font-family, system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif)'
49
70
  var DIM = 'var(--dsw-alias-label-secondary, var(--dsh-text-dim, #8a8f98))'
50
71
  var PEAK_COLOR = 'var(--dsw-alias-state-error-primary, #e5484d)'
51
72
  var OFF_COLOR = 'var(--dsw-alias-state-success-primary, #30a46c)'
52
73
 
74
+ /** Beijing time parts incl. ISO weekday (1=Mon … 7=Sun), timezone-proof. */
53
75
  function bjParts() {
54
76
  var parts = new Intl.DateTimeFormat('en-GB', {
55
- timeZone: TZ, hour12: false,
77
+ timeZone: TZ, hour12: false, weekday: 'short',
56
78
  hour: '2-digit', minute: '2-digit', second: '2-digit',
57
79
  }).formatToParts(new Date())
58
80
  var get = function (type) {
59
81
  for (var i = 0; i < parts.length; i++) {
60
- if (parts[i].type === type) return Number(parts[i].value)
82
+ if (parts[i].type === type) return parts[i].value
61
83
  }
62
- return 0
84
+ return ''
85
+ }
86
+ var wdMap = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 }
87
+ return {
88
+ h: Number(get('hour')),
89
+ m: Number(get('minute')),
90
+ s: Number(get('second')),
91
+ weekday: wdMap[get('weekday')] || 1,
63
92
  }
64
- return { h: get('hour'), m: get('minute'), s: get('second') }
65
93
  }
66
94
 
95
+ /**
96
+ * Peak/off-peak state at a Beijing-time instant.
97
+ * Weekends (Sat/Sun) are all-day off-peak; weekdays follow the peak
98
+ * windows 09:00–12:00 / 14:00–18:00. Returns the next state boundary as
99
+ * { nextMin, daysAhead } (daysAhead counts how many Beijing days away),
100
+ * seconds until it, and the progress through the current phase.
101
+ */
67
102
  function compute(t) {
68
103
  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 }
104
+ var weekend = t.weekday === 6 || t.weekday === 7
105
+ var peak, nextMin, daysAhead, phaseStartMin, phaseLenMin
106
+
107
+ if (weekend) {
108
+ peak = false
109
+ nextMin = BOUNDS[0] // Monday 09:00
110
+ daysAhead = ((1 - t.weekday + 7) % 7) || 7 // Sat 2, Sun 1
111
+ var daysSinceSat = (t.weekday - 6 + 7) % 7 // Sat=0, Sun=1
112
+ phaseStartMin = daysSinceSat * 1440
113
+ phaseLenMin = 2 * 1440 + BOUNDS[0] // Sat 00:00 → Mon 09:00
114
+ } else {
115
+ peak = (now >= BOUNDS[0] && now < BOUNDS[1]) || (now >= BOUNDS[2] && now < BOUNDS[3])
116
+ var next = null
117
+ for (var i = 0; i < BOUNDS.length; i++) {
118
+ if (BOUNDS[i] > now) { next = BOUNDS[i]; break }
119
+ }
120
+ if (next !== null) {
121
+ nextMin = next
122
+ daysAhead = 0
123
+ var ps = 0
124
+ for (var j = BOUNDS.length - 1; j >= 0; j--) {
125
+ if (BOUNDS[j] <= now) { ps = BOUNDS[j]; break }
126
+ }
127
+ phaseStartMin = ps
128
+ phaseLenMin = nextMin - ps
129
+ } else {
130
+ nextMin = BOUNDS[0]
131
+ // After 18:00: Friday → next Monday 09:00 (3 days), otherwise next day 09:00.
132
+ daysAhead = t.weekday === 5 ? 3 : 1
133
+ phaseStartMin = BOUNDS[3]
134
+ phaseLenMin = daysAhead * 1440 + BOUNDS[0] - phaseStartMin
135
+ }
79
136
  }
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 }
137
+
138
+ var secsToNext = daysAhead * 86400 + (nextMin - now) * 60 - t.s
139
+ var elapsedMin = weekend ? daysSinceSat * 1440 + now : now - phaseStartMin
140
+ var progress = phaseLenMin > 0 ? Math.min(100, Math.max(0, (elapsedMin / phaseLenMin) * 100)) : 0
141
+ return { peak: peak, nextMin: nextMin, daysAhead: daysAhead, secsToNext: secsToNext, progress: progress }
83
142
  }
84
143
 
85
144
  function pad(n) { return (n < 10 ? '0' : '') + n }
@@ -87,8 +146,10 @@
87
146
  function fmtBoundary(min) { return pad(Math.floor(min / 60)) + ':' + pad(min % 60) }
88
147
  function fmtDur(totalSecs) {
89
148
  var s = Math.max(0, Math.round(totalSecs))
90
- var h = Math.floor(s / 3600)
149
+ var d = Math.floor(s / 86400)
150
+ var h = Math.floor((s % 86400) / 3600)
91
151
  var m = Math.floor((s % 3600) / 60)
152
+ if (d > 0) return d + 'd ' + h + 'h'
92
153
  return (h > 0 ? h + 'h ' : '') + m + 'm'
93
154
  }
94
155
  function fmtDurFull(totalSecs) {
@@ -98,6 +159,13 @@
98
159
  var sec = s % 60
99
160
  return pad(h) + ':' + pad(m) + ':' + pad(sec)
100
161
  }
162
+ /** Human label for the next boundary: "18:00" / "明天 09:00" / "周一 09:00". */
163
+ function fmtBoundaryLabel(t, s) {
164
+ if (s.daysAhead === 0) return fmtBoundary(s.nextMin)
165
+ if (s.daysAhead === 1) return '明天 ' + fmtBoundary(s.nextMin)
166
+ var nextW = ((t.weekday - 1 + s.daysAhead) % 7) + 1
167
+ return '周' + WEEKDAY_LABELS[nextW - 1] + ' ' + fmtBoundary(s.nextMin)
168
+ }
101
169
 
102
170
  function PeakChip() {
103
171
  var tickState = react.useState(bjParts())
@@ -112,17 +180,18 @@
112
180
  }, [])
113
181
 
114
182
  var s = compute(tick)
183
+ var weekend = tick.weekday === 6 || tick.weekday === 7
115
184
  var color = s.peak ? PEAK_COLOR : OFF_COLOR
116
185
  var clock = fmtClock(tick)
117
- var boundaryLabel = fmtBoundary(s.next.min) + (s.next.tomorrow ? '(明天)' : '')
186
+ var boundaryLabel = fmtBoundaryLabel(tick, s)
118
187
 
119
188
  var chip = react.createElement(
120
189
  'div',
121
190
  {
122
191
  onClick: function () { setOpen(!open[0]) },
123
192
  title: s.peak
124
- ? '高峰期(北京时间 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
125
- : '空闲期:价格为高峰一半。下一高峰 ' + boundaryLabel + ',点击展开详情',
193
+ ? '高峰期(工作日 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
194
+ : '空闲期:价格为高峰一半' + (weekend ? '(周末全天谷价)' : '') + '。下一高峰 ' + boundaryLabel + ',点击展开详情',
126
195
  style: {
127
196
  display: 'inline-flex', alignItems: 'center', gap: 6,
128
197
  fontFamily: FONT, fontSize: 12, lineHeight: '20px',
@@ -135,7 +204,7 @@
135
204
  [
136
205
  react.createElement('span', { key: 'dot', style: { width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 } }),
137
206
  react.createElement('span', { key: 'state', style: { fontWeight: s.peak ? 700 : 500, color: color } },
138
- s.peak ? '高峰期' : '空闲期'),
207
+ s.peak ? '高峰期' : (weekend ? '周末谷价' : '空闲期')),
139
208
  react.createElement('span', { key: 'clock', style: { fontVariantNumeric: 'tabular-nums' } }, clock),
140
209
  react.createElement('span', { key: 'count', style: { fontVariantNumeric: 'tabular-nums' } },
141
210
  s.peak
@@ -164,14 +233,16 @@
164
233
  [
165
234
  react.createElement('div', { key: 'l1', style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
166
235
  react.createElement('span', { key: 'st', style: { fontWeight: 700, color: color } },
167
- s.peak ? '高峰时段' : '空闲时段'),
236
+ s.peak ? '高峰时段' : (weekend ? '周末谷价' : '空闲时段')),
168
237
  react.createElement('span', { key: 'tm', style: { fontVariantNumeric: 'tabular-nums' } },
169
238
  clock + '(北京)'),
170
239
  react.createElement('span', { key: 'price', style: { color: color, fontWeight: 500 } },
171
240
  s.peak ? '价格 ×2' : '半价'),
172
241
  ]),
173
242
  react.createElement('div', { key: 'l2' },
174
- '高峰窗口:09:00–12:00 / 14:00–18:00(每日)'),
243
+ '高峰窗口(工作日):09:00–12:00 / 14:00–18:00'),
244
+ react.createElement('div', { key: 'l2b', style: { color: weekend ? OFF_COLOR : DIM, fontWeight: weekend ? 600 : 400 } },
245
+ '周末(周六、周日):全天按谷价计费'),
175
246
  react.createElement('div', { key: 'l3', style: { fontVariantNumeric: 'tabular-nums' } },
176
247
  '下一转折:' + boundaryLabel + ' · 倒计时 ' + fmtDurFull(s.secsToNext)),
177
248
  react.createElement('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
@@ -185,7 +256,7 @@
185
256
  s.progress.toFixed(0) + '%'),
186
257
  ]),
187
258
  react.createElement('div', { key: 'foot', style: { fontSize: 10.5, lineHeight: 1.5, opacity: 0.85 } },
188
- '官方峰谷定价 2026-08-17 生效:空闲时段价格为高峰一半。'),
259
+ '官方峰谷定价 8/17 生效,8/23 起周末全天谷价;空闲价 = 高峰一半。'),
189
260
  ],
190
261
  )
191
262
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-peak-status",
3
- "version": "1.0.1",
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.3",
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.
@@ -20,15 +22,32 @@
20
22
  // The host client-modules expects a bundle to register the id of the entry
21
23
  // it is mounted as — the package name ("@local/dsh-peak-status" for a
22
24
  // profile @local install, "dsh-peak-status" when installed from npm).
23
- // Derive it from the bundle URL at runtime so one artifact works in every
24
- // mount shape (bundles are loaded via <script>, so document.currentScript
25
- // is the plugin script while this top-level code runs).
25
+ //
26
+ // Single-file loads use /plugins/<id>/client.js, so the id is derivable
27
+ // from document.currentScript.src. Since dsh 0.1.2-alpha.3 the web shell
28
+ // may serve plugin bundles as one shared combo script
29
+ // (/plugins/??<id>/client.js,<id2>/client.js&rev=...) — currentScript.src
30
+ // then names every bundle in the batch, not this one. Fall back to
31
+ // scanning the combo URL for our own package leaf, which is present in
32
+ // both install shapes.
26
33
  var MODULE_ID = (function () {
34
+ var OWN = 'dsh-peak-status'
27
35
  try {
28
36
  var el = document.currentScript
29
37
  if (el && el.src) {
30
- var m = /\/plugins\/(.+?)\/client\.js/.exec(el.src)
31
- if (m) return decodeURIComponent(m[1])
38
+ var src = el.src
39
+ var single = /\/plugins\/([^/]+)\/client\.js(?:\?|$)/.exec(src)
40
+ if (single) return decodeURIComponent(single[1])
41
+ var combo = /\/plugins\/\?\?([^&]+)/.exec(src)
42
+ if (combo) {
43
+ var parts = combo[1].split(',')
44
+ for (var i = 0; i < parts.length; i++) {
45
+ var m = /^(.+?)\/client\.js$/.exec(parts[i])
46
+ if (m && decodeURIComponent(m[1]).split('/').pop() === OWN) {
47
+ return decodeURIComponent(m[1])
48
+ }
49
+ }
50
+ }
32
51
  }
33
52
  } catch (error) { /* ignore */ }
34
53
  return null
@@ -42,44 +61,84 @@
42
61
  var react = require('react')
43
62
 
44
63
  var TZ = 'Asia/Shanghai'
45
- // Peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
64
+ // Weekday peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
46
65
  var BOUNDS = [540, 720, 840, 1080]
66
+ // Weekday labels for "next boundary" display (ISO weekday 1..7).
67
+ var WEEKDAY_LABELS = ['一', '二', '三', '四', '五', '六', '日']
47
68
 
48
69
  var FONT = 'var(--dsw-font-family, system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif)'
49
70
  var DIM = 'var(--dsw-alias-label-secondary, var(--dsh-text-dim, #8a8f98))'
50
71
  var PEAK_COLOR = 'var(--dsw-alias-state-error-primary, #e5484d)'
51
72
  var OFF_COLOR = 'var(--dsw-alias-state-success-primary, #30a46c)'
52
73
 
74
+ /** Beijing time parts incl. ISO weekday (1=Mon … 7=Sun), timezone-proof. */
53
75
  function bjParts() {
54
76
  var parts = new Intl.DateTimeFormat('en-GB', {
55
- timeZone: TZ, hour12: false,
77
+ timeZone: TZ, hour12: false, weekday: 'short',
56
78
  hour: '2-digit', minute: '2-digit', second: '2-digit',
57
79
  }).formatToParts(new Date())
58
80
  var get = function (type) {
59
81
  for (var i = 0; i < parts.length; i++) {
60
- if (parts[i].type === type) return Number(parts[i].value)
82
+ if (parts[i].type === type) return parts[i].value
61
83
  }
62
- return 0
84
+ return ''
85
+ }
86
+ var wdMap = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 }
87
+ return {
88
+ h: Number(get('hour')),
89
+ m: Number(get('minute')),
90
+ s: Number(get('second')),
91
+ weekday: wdMap[get('weekday')] || 1,
63
92
  }
64
- return { h: get('hour'), m: get('minute'), s: get('second') }
65
93
  }
66
94
 
95
+ /**
96
+ * Peak/off-peak state at a Beijing-time instant.
97
+ * Weekends (Sat/Sun) are all-day off-peak; weekdays follow the peak
98
+ * windows 09:00–12:00 / 14:00–18:00. Returns the next state boundary as
99
+ * { nextMin, daysAhead } (daysAhead counts how many Beijing days away),
100
+ * seconds until it, and the progress through the current phase.
101
+ */
67
102
  function compute(t) {
68
103
  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 }
104
+ var weekend = t.weekday === 6 || t.weekday === 7
105
+ var peak, nextMin, daysAhead, phaseStartMin, phaseLenMin
106
+
107
+ if (weekend) {
108
+ peak = false
109
+ nextMin = BOUNDS[0] // Monday 09:00
110
+ daysAhead = ((1 - t.weekday + 7) % 7) || 7 // Sat 2, Sun 1
111
+ var daysSinceSat = (t.weekday - 6 + 7) % 7 // Sat=0, Sun=1
112
+ phaseStartMin = daysSinceSat * 1440
113
+ phaseLenMin = 2 * 1440 + BOUNDS[0] // Sat 00:00 → Mon 09:00
114
+ } else {
115
+ peak = (now >= BOUNDS[0] && now < BOUNDS[1]) || (now >= BOUNDS[2] && now < BOUNDS[3])
116
+ var next = null
117
+ for (var i = 0; i < BOUNDS.length; i++) {
118
+ if (BOUNDS[i] > now) { next = BOUNDS[i]; break }
119
+ }
120
+ if (next !== null) {
121
+ nextMin = next
122
+ daysAhead = 0
123
+ var ps = 0
124
+ for (var j = BOUNDS.length - 1; j >= 0; j--) {
125
+ if (BOUNDS[j] <= now) { ps = BOUNDS[j]; break }
126
+ }
127
+ phaseStartMin = ps
128
+ phaseLenMin = nextMin - ps
129
+ } else {
130
+ nextMin = BOUNDS[0]
131
+ // After 18:00: Friday → next Monday 09:00 (3 days), otherwise next day 09:00.
132
+ daysAhead = t.weekday === 5 ? 3 : 1
133
+ phaseStartMin = BOUNDS[3]
134
+ phaseLenMin = daysAhead * 1440 + BOUNDS[0] - phaseStartMin
135
+ }
79
136
  }
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 }
137
+
138
+ var secsToNext = daysAhead * 86400 + (nextMin - now) * 60 - t.s
139
+ var elapsedMin = weekend ? daysSinceSat * 1440 + now : now - phaseStartMin
140
+ var progress = phaseLenMin > 0 ? Math.min(100, Math.max(0, (elapsedMin / phaseLenMin) * 100)) : 0
141
+ return { peak: peak, nextMin: nextMin, daysAhead: daysAhead, secsToNext: secsToNext, progress: progress }
83
142
  }
84
143
 
85
144
  function pad(n) { return (n < 10 ? '0' : '') + n }
@@ -87,8 +146,10 @@
87
146
  function fmtBoundary(min) { return pad(Math.floor(min / 60)) + ':' + pad(min % 60) }
88
147
  function fmtDur(totalSecs) {
89
148
  var s = Math.max(0, Math.round(totalSecs))
90
- var h = Math.floor(s / 3600)
149
+ var d = Math.floor(s / 86400)
150
+ var h = Math.floor((s % 86400) / 3600)
91
151
  var m = Math.floor((s % 3600) / 60)
152
+ if (d > 0) return d + 'd ' + h + 'h'
92
153
  return (h > 0 ? h + 'h ' : '') + m + 'm'
93
154
  }
94
155
  function fmtDurFull(totalSecs) {
@@ -98,6 +159,13 @@
98
159
  var sec = s % 60
99
160
  return pad(h) + ':' + pad(m) + ':' + pad(sec)
100
161
  }
162
+ /** Human label for the next boundary: "18:00" / "明天 09:00" / "周一 09:00". */
163
+ function fmtBoundaryLabel(t, s) {
164
+ if (s.daysAhead === 0) return fmtBoundary(s.nextMin)
165
+ if (s.daysAhead === 1) return '明天 ' + fmtBoundary(s.nextMin)
166
+ var nextW = ((t.weekday - 1 + s.daysAhead) % 7) + 1
167
+ return '周' + WEEKDAY_LABELS[nextW - 1] + ' ' + fmtBoundary(s.nextMin)
168
+ }
101
169
 
102
170
  function PeakChip() {
103
171
  var tickState = react.useState(bjParts())
@@ -112,17 +180,18 @@
112
180
  }, [])
113
181
 
114
182
  var s = compute(tick)
183
+ var weekend = tick.weekday === 6 || tick.weekday === 7
115
184
  var color = s.peak ? PEAK_COLOR : OFF_COLOR
116
185
  var clock = fmtClock(tick)
117
- var boundaryLabel = fmtBoundary(s.next.min) + (s.next.tomorrow ? '(明天)' : '')
186
+ var boundaryLabel = fmtBoundaryLabel(tick, s)
118
187
 
119
188
  var chip = react.createElement(
120
189
  'div',
121
190
  {
122
191
  onClick: function () { setOpen(!open[0]) },
123
192
  title: s.peak
124
- ? '高峰期(北京时间 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
125
- : '空闲期:价格为高峰一半。下一高峰 ' + boundaryLabel + ',点击展开详情',
193
+ ? '高峰期(工作日 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
194
+ : '空闲期:价格为高峰一半' + (weekend ? '(周末全天谷价)' : '') + '。下一高峰 ' + boundaryLabel + ',点击展开详情',
126
195
  style: {
127
196
  display: 'inline-flex', alignItems: 'center', gap: 6,
128
197
  fontFamily: FONT, fontSize: 12, lineHeight: '20px',
@@ -135,7 +204,7 @@
135
204
  [
136
205
  react.createElement('span', { key: 'dot', style: { width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 } }),
137
206
  react.createElement('span', { key: 'state', style: { fontWeight: s.peak ? 700 : 500, color: color } },
138
- s.peak ? '高峰期' : '空闲期'),
207
+ s.peak ? '高峰期' : (weekend ? '周末谷价' : '空闲期')),
139
208
  react.createElement('span', { key: 'clock', style: { fontVariantNumeric: 'tabular-nums' } }, clock),
140
209
  react.createElement('span', { key: 'count', style: { fontVariantNumeric: 'tabular-nums' } },
141
210
  s.peak
@@ -164,14 +233,16 @@
164
233
  [
165
234
  react.createElement('div', { key: 'l1', style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
166
235
  react.createElement('span', { key: 'st', style: { fontWeight: 700, color: color } },
167
- s.peak ? '高峰时段' : '空闲时段'),
236
+ s.peak ? '高峰时段' : (weekend ? '周末谷价' : '空闲时段')),
168
237
  react.createElement('span', { key: 'tm', style: { fontVariantNumeric: 'tabular-nums' } },
169
238
  clock + '(北京)'),
170
239
  react.createElement('span', { key: 'price', style: { color: color, fontWeight: 500 } },
171
240
  s.peak ? '价格 ×2' : '半价'),
172
241
  ]),
173
242
  react.createElement('div', { key: 'l2' },
174
- '高峰窗口:09:00–12:00 / 14:00–18:00(每日)'),
243
+ '高峰窗口(工作日):09:00–12:00 / 14:00–18:00'),
244
+ react.createElement('div', { key: 'l2b', style: { color: weekend ? OFF_COLOR : DIM, fontWeight: weekend ? 600 : 400 } },
245
+ '周末(周六、周日):全天按谷价计费'),
175
246
  react.createElement('div', { key: 'l3', style: { fontVariantNumeric: 'tabular-nums' } },
176
247
  '下一转折:' + boundaryLabel + ' · 倒计时 ' + fmtDurFull(s.secsToNext)),
177
248
  react.createElement('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
@@ -185,7 +256,7 @@
185
256
  s.progress.toFixed(0) + '%'),
186
257
  ]),
187
258
  react.createElement('div', { key: 'foot', style: { fontSize: 10.5, lineHeight: 1.5, opacity: 0.85 } },
188
- '官方峰谷定价 2026-08-17 生效:空闲时段价格为高峰一半。'),
259
+ '官方峰谷定价 8/17 生效,8/23 起周末全天谷价;空闲价 = 高峰一半。'),
189
260
  ],
190
261
  )
191
262