dsh-peak-status 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-peak-status contributors
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 ADDED
@@ -0,0 +1,93 @@
1
+ # dsh-peak-status
2
+
3
+ English | [中文](README.zh-CN.md)
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
+ ![peak indicator preview](assets/peak-indicator.png)
8
+
9
+ ## What it shows
10
+
11
+ DeepSeek's official peak/valley API pricing took effect on **2026-08-17**:
12
+
13
+ | State | Beijing time (daily) | Price |
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 |
17
+
18
+ The chip (top-right of the conversation header, inside the native `conversation.session.header.utilities` strip) renders:
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
24
+
25
+ Everything is themed with the web UI's native `--dsw-alias-*` variables, so it blends into the interface — no floating overlays.
26
+
27
+ ## Install
28
+
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
+ ```sh
34
+ dsh plugin --profile web add dsh-peak-status
35
+ ```
36
+
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/`:
59
+
60
+ ```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
64
+ ```
65
+
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.
71
+
72
+ ## Repository layout
73
+
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
+ ```
88
+
89
+ Then `dsh plugin --profile web add dsh-peak-status` installs the published version.
90
+
91
+ ## License
92
+
93
+ [MIT](LICENSE)
@@ -0,0 +1,93 @@
1
+ # dsh-peak-status
2
+
3
+ [English](README.md) | 中文
4
+
5
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web GUI 的高峰时段指示灯。在会话头部右上角(原生 `conversation.session.header.utilities` 工具位)显示一个融入界面的小胶囊,实时告诉你当前**北京时间**是否处于 DeepSeek API 的**高峰时段**,带秒级时钟、下一转折倒计时和价格提示。
6
+
7
+ ![peak indicator 预览](assets/peak-indicator.png)
8
+
9
+ ## 功能
10
+
11
+ DeepSeek 官方峰谷定价于 **2026-08-17** 生效:
12
+
13
+ | 状态 | 北京时间(每日) | 价格 |
14
+ | :-- | :-- | :-- |
15
+ | 🔴 **高峰** | 09:00–12:00 和 14:00–18:00 | 高峰价(= 空闲 ×2) |
16
+ | 🟢 **空闲** | 其余时间 | 高峰的一半 |
17
+
18
+ 胶囊显示内容:
19
+
20
+ - 状态圆点——**红 = 高峰,绿 = 空闲**
21
+ - 实时北京时钟(`HH:MM:SS`,时区 `Asia/Shanghai`,与本机时区无关)
22
+ - 倒计时:`剩 3h 54m · 价格 ×2`(高峰)/ `距高峰 3h 54m · 半价`(空闲)
23
+ - 点击展开下拉详情:高峰窗口、下一转折(含 `HH:MM:SS` 倒计时)、当前阶段进度条、价格说明
24
+
25
+ 全部使用 Web UI 原生 `--dsw-alias-*` 主题变量,与界面完全融合,没有悬浮贴片。
26
+
27
+ ## 安装
28
+
29
+ 插件是经 profile 补丁层挂载的真实包,**重启 dsh web 不会消失**。
30
+
31
+ ### dsh CLI 安装(发布到 npm 后)
32
+
33
+ ```sh
34
+ dsh plugin --profile web add dsh-peak-status
35
+ ```
36
+
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/` 生成:
59
+
60
+ ```sh
61
+ npm run build # 把 src/*.js 复制到 lib/
62
+ npm run check # 两个半区的语法检查
63
+ npm test # 高峰判定算法边界测试
64
+ ```
65
+
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
+ ## 目录结构
73
+
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
+ ## 发布
84
+
85
+ ```sh
86
+ npm publish # prepack 会自动构建 lib/
87
+ ```
88
+
89
+ 之后 `dsh plugin --profile web add dsh-peak-status` 即可安装已发布版本。
90
+
91
+ ## 许可证
92
+
93
+ [MIT](LICENSE)
@@ -0,0 +1,4 @@
1
+ # dsh bundle patch: inserts the plugin row into a profile's layer stack.
2
+ - insert:
3
+ - id: dsh-peak-status
4
+ name: 'dsh-peak-status'
package/lib/client.js ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * dsh-peak-status — browser half.
3
+ *
4
+ * A compact status chip in the session header's right-aligned utilities
5
+ * strip (conversation.session.header.utilities) — the very top of the
6
+ * conversation — fully themed with the web UI's native --dsw-alias-*
7
+ * variables so it blends into the interface.
8
+ *
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)
13
+ *
14
+ * Red = peak (bold), green = off-peak; live Beijing clock, countdown to the
15
+ * next transition, click to expand a small popover with the full detail.
16
+ * Mounted persistently via the profile's cordis.patch.yml, so it survives
17
+ * restarts.
18
+ */
19
+ (function () {
20
+ // The host client-modules expects a bundle to register the id of the entry
21
+ // it is mounted as — the package name ("@local/dsh-peak-status" for a
22
+ // 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).
26
+ var MODULE_ID = (function () {
27
+ try {
28
+ var el = document.currentScript
29
+ if (el && el.src) {
30
+ var m = /\/plugins\/(.+?)\/client\.js/.exec(el.src)
31
+ if (m) return decodeURIComponent(m[1])
32
+ }
33
+ } catch (error) { /* ignore */ }
34
+ return null
35
+ })()
36
+
37
+ var MODULE_FACTORY = (require) => {
38
+ var module = { exports: {} }
39
+ var exports = module.exports
40
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
41
+
42
+ var react = require('react')
43
+
44
+ var TZ = 'Asia/Shanghai'
45
+ // Peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
46
+ var BOUNDS = [540, 720, 840, 1080]
47
+
48
+ var FONT = 'var(--dsw-font-family, system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif)'
49
+ var DIM = 'var(--dsw-alias-label-secondary, var(--dsh-text-dim, #8a8f98))'
50
+ var PEAK_COLOR = 'var(--dsw-alias-state-error-primary, #e5484d)'
51
+ var OFF_COLOR = 'var(--dsw-alias-state-success-primary, #30a46c)'
52
+
53
+ function bjParts() {
54
+ var parts = new Intl.DateTimeFormat('en-GB', {
55
+ timeZone: TZ, hour12: false,
56
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
57
+ }).formatToParts(new Date())
58
+ var get = function (type) {
59
+ for (var i = 0; i < parts.length; i++) {
60
+ if (parts[i].type === type) return Number(parts[i].value)
61
+ }
62
+ return 0
63
+ }
64
+ return { h: get('hour'), m: get('minute'), s: get('second') }
65
+ }
66
+
67
+ function compute(t) {
68
+ 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 }
79
+ }
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 }
83
+ }
84
+
85
+ function pad(n) { return (n < 10 ? '0' : '') + n }
86
+ function fmtClock(t) { return pad(t.h) + ':' + pad(t.m) + ':' + pad(t.s) }
87
+ function fmtBoundary(min) { return pad(Math.floor(min / 60)) + ':' + pad(min % 60) }
88
+ function fmtDur(totalSecs) {
89
+ var s = Math.max(0, Math.round(totalSecs))
90
+ var h = Math.floor(s / 3600)
91
+ var m = Math.floor((s % 3600) / 60)
92
+ return (h > 0 ? h + 'h ' : '') + m + 'm'
93
+ }
94
+ function fmtDurFull(totalSecs) {
95
+ var s = Math.max(0, Math.round(totalSecs))
96
+ var h = Math.floor(s / 3600)
97
+ var m = Math.floor((s % 3600) / 60)
98
+ var sec = s % 60
99
+ return pad(h) + ':' + pad(m) + ':' + pad(sec)
100
+ }
101
+
102
+ function PeakChip() {
103
+ var tickState = react.useState(bjParts())
104
+ var tick = tickState[0]
105
+ var setTick = tickState[1]
106
+ var open = react.useState(false)
107
+ var setOpen = open[1]
108
+
109
+ react.useEffect(function () {
110
+ var id = setInterval(function () { setTick(bjParts()) }, 1000)
111
+ return function () { clearInterval(id) }
112
+ }, [])
113
+
114
+ var s = compute(tick)
115
+ var color = s.peak ? PEAK_COLOR : OFF_COLOR
116
+ var clock = fmtClock(tick)
117
+ var boundaryLabel = fmtBoundary(s.next.min) + (s.next.tomorrow ? '(明天)' : '')
118
+
119
+ var chip = react.createElement(
120
+ 'div',
121
+ {
122
+ onClick: function () { setOpen(!open[0]) },
123
+ title: s.peak
124
+ ? '高峰期(北京时间 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
125
+ : '空闲期:价格为高峰一半。下一高峰 ' + boundaryLabel + ',点击展开详情',
126
+ style: {
127
+ display: 'inline-flex', alignItems: 'center', gap: 6,
128
+ fontFamily: FONT, fontSize: 12, lineHeight: '20px',
129
+ padding: '1px 10px', borderRadius: 999,
130
+ background: 'var(--dsw-alias-bg-layer-1, #f7f8fa)',
131
+ border: '1px solid var(--dsw-alias-border-l1, #e4e7ec)',
132
+ color: DIM, cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap',
133
+ },
134
+ },
135
+ [
136
+ react.createElement('span', { key: 'dot', style: { width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 } }),
137
+ react.createElement('span', { key: 'state', style: { fontWeight: s.peak ? 700 : 500, color: color } },
138
+ s.peak ? '高峰期' : '空闲期'),
139
+ react.createElement('span', { key: 'clock', style: { fontVariantNumeric: 'tabular-nums' } }, clock),
140
+ react.createElement('span', { key: 'count', style: { fontVariantNumeric: 'tabular-nums' } },
141
+ s.peak
142
+ ? ('剩 ' + fmtDur(s.secsToNext) + ' · 价格 ×2')
143
+ : ('距高峰 ' + fmtDur(s.secsToNext) + ' · 半价')),
144
+ react.createElement('span', { key: 'caret', style: { fontSize: 9, opacity: 0.7 } }, open[0] ? '▾' : '▸'),
145
+ ],
146
+ )
147
+
148
+ if (!open[0]) return chip
149
+
150
+ var popover = react.createElement(
151
+ 'div',
152
+ {
153
+ style: {
154
+ position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 264,
155
+ zIndex: 60, display: 'flex', flexDirection: 'column', gap: 5,
156
+ fontFamily: FONT, fontSize: 11.5, lineHeight: 1.6,
157
+ color: DIM,
158
+ background: 'var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-base, #ffffff))',
159
+ border: '1px solid var(--dsw-alias-border-l1, #e4e7ec)',
160
+ borderRadius: 10, padding: '8px 11px',
161
+ boxShadow: 'var(--dsw-shadow-lv2, 0 8px 24px rgba(15,23,42,.14))',
162
+ },
163
+ },
164
+ [
165
+ react.createElement('div', { key: 'l1', style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
166
+ react.createElement('span', { key: 'st', style: { fontWeight: 700, color: color } },
167
+ s.peak ? '高峰时段' : '空闲时段'),
168
+ react.createElement('span', { key: 'tm', style: { fontVariantNumeric: 'tabular-nums' } },
169
+ clock + '(北京)'),
170
+ react.createElement('span', { key: 'price', style: { color: color, fontWeight: 500 } },
171
+ s.peak ? '价格 ×2' : '半价'),
172
+ ]),
173
+ react.createElement('div', { key: 'l2' },
174
+ '高峰窗口:09:00–12:00 / 14:00–18:00(每日)'),
175
+ react.createElement('div', { key: 'l3', style: { fontVariantNumeric: 'tabular-nums' } },
176
+ '下一转折:' + boundaryLabel + ' · 倒计时 ' + fmtDurFull(s.secsToNext)),
177
+ react.createElement('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
178
+ react.createElement('div', { key: 'track', style: { flex: 1, height: 4, borderRadius: 2, background: 'var(--dsw-alias-border-l1, #e4e7ec)', overflow: 'hidden' } },
179
+ react.createElement('div', { key: 'fill', style: {
180
+ height: '100%', width: s.progress.toFixed(1) + '%',
181
+ background: color, borderRadius: 2, transition: 'width .4s ease',
182
+ } }),
183
+ ),
184
+ react.createElement('span', { key: 'pct', style: { fontSize: 10.5, fontVariantNumeric: 'tabular-nums' } },
185
+ s.progress.toFixed(0) + '%'),
186
+ ]),
187
+ react.createElement('div', { key: 'foot', style: { fontSize: 10.5, lineHeight: 1.5, opacity: 0.85 } },
188
+ '官方峰谷定价 2026-08-17 生效:空闲时段价格为高峰一半。'),
189
+ ],
190
+ )
191
+
192
+ return react.createElement(
193
+ 'div',
194
+ { style: { position: 'relative', display: 'inline-block' } },
195
+ [chip, popover],
196
+ )
197
+ }
198
+
199
+ function apply(ctx) {
200
+ try {
201
+ var slots = ctx.get('slots')
202
+ if (slots === undefined) return
203
+ slots.inject('conversation.session.header.utilities', function () {
204
+ return slots.register(
205
+ { name: 'conversation.session.header.utilities', id: 'dsh-peak-status', order: 1 },
206
+ PeakChip,
207
+ )
208
+ })
209
+ } catch (error) {
210
+ console.warn('[dsh-peak-status] apply failed:', error)
211
+ }
212
+ }
213
+
214
+ var inject = ['slots']
215
+
216
+ exports.apply = apply
217
+ exports.inject = inject
218
+ return module.exports
219
+ }
220
+
221
+ function registerModule(id) {
222
+ window.__ModuleLoader__.load({ id: id, factory: MODULE_FACTORY })
223
+ }
224
+
225
+ if (MODULE_ID !== null) {
226
+ registerModule(MODULE_ID)
227
+ } else {
228
+ // Script URL unavailable — cover both install shapes so neither mount
229
+ // fails the "loaded without registering" check.
230
+ registerModule('@local/dsh-peak-status')
231
+ registerModule('dsh-peak-status')
232
+ }
233
+ })()
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * dsh-peak-status — host half.
3
+ *
4
+ * No host behavior: the browser half renders the peak indicator chip.
5
+ * Kept as a real (no-op) plugin so the loader entry mounts — the
6
+ * client-modules scan only admits entries whose host fiber is active.
7
+ */
8
+
9
+ export const apply = function () {
10
+ // Intentionally empty.
11
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
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 高峰时段。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "src",
15
+ "cordis.patch.yml",
16
+ "README.md",
17
+ "README.zh-CN.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "node scripts/build.mjs",
22
+ "prepack": "npm run build",
23
+ "check": "node --check src/index.js && node --check src/client.js",
24
+ "test": "node scripts/test-peak.mjs"
25
+ },
26
+ "keywords": [
27
+ "dsh",
28
+ "deepseek",
29
+ "harness",
30
+ "dsh-plugin",
31
+ "peak",
32
+ "pricing",
33
+ "status",
34
+ "web-ui"
35
+ ],
36
+ "license": "MIT",
37
+ "author": "",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/xiaoyi-xx/dsh-peak-status.git"
41
+ },
42
+ "homepage": "https://github.com/xiaoyi-xx/dsh-peak-status",
43
+ "bugs": {
44
+ "url": "https://github.com/xiaoyi-xx/dsh-peak-status/issues"
45
+ },
46
+ "dsh": {
47
+ "bundle": {
48
+ "patch": "./cordis.patch.yml"
49
+ },
50
+ "client": {
51
+ "inject": [],
52
+ "platform": "web"
53
+ }
54
+ }
55
+ }
package/src/client.js ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * dsh-peak-status — browser half.
3
+ *
4
+ * A compact status chip in the session header's right-aligned utilities
5
+ * strip (conversation.session.header.utilities) — the very top of the
6
+ * conversation — fully themed with the web UI's native --dsw-alias-*
7
+ * variables so it blends into the interface.
8
+ *
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)
13
+ *
14
+ * Red = peak (bold), green = off-peak; live Beijing clock, countdown to the
15
+ * next transition, click to expand a small popover with the full detail.
16
+ * Mounted persistently via the profile's cordis.patch.yml, so it survives
17
+ * restarts.
18
+ */
19
+ (function () {
20
+ // The host client-modules expects a bundle to register the id of the entry
21
+ // it is mounted as — the package name ("@local/dsh-peak-status" for a
22
+ // 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).
26
+ var MODULE_ID = (function () {
27
+ try {
28
+ var el = document.currentScript
29
+ if (el && el.src) {
30
+ var m = /\/plugins\/(.+?)\/client\.js/.exec(el.src)
31
+ if (m) return decodeURIComponent(m[1])
32
+ }
33
+ } catch (error) { /* ignore */ }
34
+ return null
35
+ })()
36
+
37
+ var MODULE_FACTORY = (require) => {
38
+ var module = { exports: {} }
39
+ var exports = module.exports
40
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
41
+
42
+ var react = require('react')
43
+
44
+ var TZ = 'Asia/Shanghai'
45
+ // Peak window boundaries in minutes of day: 09:00, 12:00, 14:00, 18:00.
46
+ var BOUNDS = [540, 720, 840, 1080]
47
+
48
+ var FONT = 'var(--dsw-font-family, system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif)'
49
+ var DIM = 'var(--dsw-alias-label-secondary, var(--dsh-text-dim, #8a8f98))'
50
+ var PEAK_COLOR = 'var(--dsw-alias-state-error-primary, #e5484d)'
51
+ var OFF_COLOR = 'var(--dsw-alias-state-success-primary, #30a46c)'
52
+
53
+ function bjParts() {
54
+ var parts = new Intl.DateTimeFormat('en-GB', {
55
+ timeZone: TZ, hour12: false,
56
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
57
+ }).formatToParts(new Date())
58
+ var get = function (type) {
59
+ for (var i = 0; i < parts.length; i++) {
60
+ if (parts[i].type === type) return Number(parts[i].value)
61
+ }
62
+ return 0
63
+ }
64
+ return { h: get('hour'), m: get('minute'), s: get('second') }
65
+ }
66
+
67
+ function compute(t) {
68
+ 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 }
79
+ }
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 }
83
+ }
84
+
85
+ function pad(n) { return (n < 10 ? '0' : '') + n }
86
+ function fmtClock(t) { return pad(t.h) + ':' + pad(t.m) + ':' + pad(t.s) }
87
+ function fmtBoundary(min) { return pad(Math.floor(min / 60)) + ':' + pad(min % 60) }
88
+ function fmtDur(totalSecs) {
89
+ var s = Math.max(0, Math.round(totalSecs))
90
+ var h = Math.floor(s / 3600)
91
+ var m = Math.floor((s % 3600) / 60)
92
+ return (h > 0 ? h + 'h ' : '') + m + 'm'
93
+ }
94
+ function fmtDurFull(totalSecs) {
95
+ var s = Math.max(0, Math.round(totalSecs))
96
+ var h = Math.floor(s / 3600)
97
+ var m = Math.floor((s % 3600) / 60)
98
+ var sec = s % 60
99
+ return pad(h) + ':' + pad(m) + ':' + pad(sec)
100
+ }
101
+
102
+ function PeakChip() {
103
+ var tickState = react.useState(bjParts())
104
+ var tick = tickState[0]
105
+ var setTick = tickState[1]
106
+ var open = react.useState(false)
107
+ var setOpen = open[1]
108
+
109
+ react.useEffect(function () {
110
+ var id = setInterval(function () { setTick(bjParts()) }, 1000)
111
+ return function () { clearInterval(id) }
112
+ }, [])
113
+
114
+ var s = compute(tick)
115
+ var color = s.peak ? PEAK_COLOR : OFF_COLOR
116
+ var clock = fmtClock(tick)
117
+ var boundaryLabel = fmtBoundary(s.next.min) + (s.next.tomorrow ? '(明天)' : '')
118
+
119
+ var chip = react.createElement(
120
+ 'div',
121
+ {
122
+ onClick: function () { setOpen(!open[0]) },
123
+ title: s.peak
124
+ ? '高峰期(北京时间 9:00-12:00 / 14:00-18:00):价格 = 空闲 ×2。' + boundaryLabel + ' 结束,点击展开详情'
125
+ : '空闲期:价格为高峰一半。下一高峰 ' + boundaryLabel + ',点击展开详情',
126
+ style: {
127
+ display: 'inline-flex', alignItems: 'center', gap: 6,
128
+ fontFamily: FONT, fontSize: 12, lineHeight: '20px',
129
+ padding: '1px 10px', borderRadius: 999,
130
+ background: 'var(--dsw-alias-bg-layer-1, #f7f8fa)',
131
+ border: '1px solid var(--dsw-alias-border-l1, #e4e7ec)',
132
+ color: DIM, cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap',
133
+ },
134
+ },
135
+ [
136
+ react.createElement('span', { key: 'dot', style: { width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 } }),
137
+ react.createElement('span', { key: 'state', style: { fontWeight: s.peak ? 700 : 500, color: color } },
138
+ s.peak ? '高峰期' : '空闲期'),
139
+ react.createElement('span', { key: 'clock', style: { fontVariantNumeric: 'tabular-nums' } }, clock),
140
+ react.createElement('span', { key: 'count', style: { fontVariantNumeric: 'tabular-nums' } },
141
+ s.peak
142
+ ? ('剩 ' + fmtDur(s.secsToNext) + ' · 价格 ×2')
143
+ : ('距高峰 ' + fmtDur(s.secsToNext) + ' · 半价')),
144
+ react.createElement('span', { key: 'caret', style: { fontSize: 9, opacity: 0.7 } }, open[0] ? '▾' : '▸'),
145
+ ],
146
+ )
147
+
148
+ if (!open[0]) return chip
149
+
150
+ var popover = react.createElement(
151
+ 'div',
152
+ {
153
+ style: {
154
+ position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 264,
155
+ zIndex: 60, display: 'flex', flexDirection: 'column', gap: 5,
156
+ fontFamily: FONT, fontSize: 11.5, lineHeight: 1.6,
157
+ color: DIM,
158
+ background: 'var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-base, #ffffff))',
159
+ border: '1px solid var(--dsw-alias-border-l1, #e4e7ec)',
160
+ borderRadius: 10, padding: '8px 11px',
161
+ boxShadow: 'var(--dsw-shadow-lv2, 0 8px 24px rgba(15,23,42,.14))',
162
+ },
163
+ },
164
+ [
165
+ react.createElement('div', { key: 'l1', style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
166
+ react.createElement('span', { key: 'st', style: { fontWeight: 700, color: color } },
167
+ s.peak ? '高峰时段' : '空闲时段'),
168
+ react.createElement('span', { key: 'tm', style: { fontVariantNumeric: 'tabular-nums' } },
169
+ clock + '(北京)'),
170
+ react.createElement('span', { key: 'price', style: { color: color, fontWeight: 500 } },
171
+ s.peak ? '价格 ×2' : '半价'),
172
+ ]),
173
+ react.createElement('div', { key: 'l2' },
174
+ '高峰窗口:09:00–12:00 / 14:00–18:00(每日)'),
175
+ react.createElement('div', { key: 'l3', style: { fontVariantNumeric: 'tabular-nums' } },
176
+ '下一转折:' + boundaryLabel + ' · 倒计时 ' + fmtDurFull(s.secsToNext)),
177
+ react.createElement('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
178
+ react.createElement('div', { key: 'track', style: { flex: 1, height: 4, borderRadius: 2, background: 'var(--dsw-alias-border-l1, #e4e7ec)', overflow: 'hidden' } },
179
+ react.createElement('div', { key: 'fill', style: {
180
+ height: '100%', width: s.progress.toFixed(1) + '%',
181
+ background: color, borderRadius: 2, transition: 'width .4s ease',
182
+ } }),
183
+ ),
184
+ react.createElement('span', { key: 'pct', style: { fontSize: 10.5, fontVariantNumeric: 'tabular-nums' } },
185
+ s.progress.toFixed(0) + '%'),
186
+ ]),
187
+ react.createElement('div', { key: 'foot', style: { fontSize: 10.5, lineHeight: 1.5, opacity: 0.85 } },
188
+ '官方峰谷定价 2026-08-17 生效:空闲时段价格为高峰一半。'),
189
+ ],
190
+ )
191
+
192
+ return react.createElement(
193
+ 'div',
194
+ { style: { position: 'relative', display: 'inline-block' } },
195
+ [chip, popover],
196
+ )
197
+ }
198
+
199
+ function apply(ctx) {
200
+ try {
201
+ var slots = ctx.get('slots')
202
+ if (slots === undefined) return
203
+ slots.inject('conversation.session.header.utilities', function () {
204
+ return slots.register(
205
+ { name: 'conversation.session.header.utilities', id: 'dsh-peak-status', order: 1 },
206
+ PeakChip,
207
+ )
208
+ })
209
+ } catch (error) {
210
+ console.warn('[dsh-peak-status] apply failed:', error)
211
+ }
212
+ }
213
+
214
+ var inject = ['slots']
215
+
216
+ exports.apply = apply
217
+ exports.inject = inject
218
+ return module.exports
219
+ }
220
+
221
+ function registerModule(id) {
222
+ window.__ModuleLoader__.load({ id: id, factory: MODULE_FACTORY })
223
+ }
224
+
225
+ if (MODULE_ID !== null) {
226
+ registerModule(MODULE_ID)
227
+ } else {
228
+ // Script URL unavailable — cover both install shapes so neither mount
229
+ // fails the "loaded without registering" check.
230
+ registerModule('@local/dsh-peak-status')
231
+ registerModule('dsh-peak-status')
232
+ }
233
+ })()
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * dsh-peak-status — host half.
3
+ *
4
+ * No host behavior: the browser half renders the peak indicator chip.
5
+ * Kept as a real (no-op) plugin so the loader entry mounts — the
6
+ * client-modules scan only admits entries whose host fiber is active.
7
+ */
8
+
9
+ export const apply = function () {
10
+ // Intentionally empty.
11
+ }