dsh-peak-status 1.0.1 → 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 +20 -63
- package/README.zh-CN.md +20 -63
- package/lib/client.js +83 -29
- package/package.json +2 -2
- package/src/client.js +83 -29
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
|

|
|
8
6
|
|
|
9
|
-
|
|
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
|
|
11
|
+
DeepSeek's official peak/valley API pricing (effective 2026-08-17, weekends adjusted 2026-08-23):
|
|
12
12
|
|
|
13
|
-
| State | Beijing time
|
|
13
|
+
| State | When (Beijing time) | Price |
|
|
14
14
|
| :-- | :-- | :-- |
|
|
15
|
-
| 🔴
|
|
16
|
-
| 🟢
|
|
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
|
-
|
|
18
|
+
Weekends (Saturday and Sunday) are charged at the valley price all day — no peak windows on weekends.
|
|
19
19
|
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|

|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web GUI 的高峰时段指示灯。在会话头部右上角显示一个融入界面的小胶囊,实时告诉你当前**北京时间**是否处于 DeepSeek API 的**高峰时段**,带秒级时钟、下一转折倒计时和价格提示。
|
|
8
|
+
|
|
9
|
+
## 高峰时段
|
|
10
10
|
|
|
11
|
-
DeepSeek
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
npm run check # 两个半区的语法检查
|
|
63
|
-
npm test # 高峰判定算法边界测试
|
|
37
|
+
dsh plugin --profile web add github:xiaoyi-xx/dsh-peak-status
|
|
64
38
|
```
|
|
65
39
|
|
|
66
|
-
|
|
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
|
-
|
|
86
|
-
npm publish # prepack 会自动构建 lib/
|
|
87
|
-
```
|
|
44
|
+
胶囊每秒自动更新。点击展开/收起详情卡,悬停有快捷提示。高峰/空闲状态严格按上面官方时刻表、以北京时间计算。
|
|
88
45
|
|
|
89
|
-
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
//
|
|
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
|
|
65
|
+
if (parts[i].type === type) return parts[i].value
|
|
61
66
|
}
|
|
62
|
-
return
|
|
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
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
var
|
|
82
|
-
|
|
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
|
|
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 =
|
|
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
|
-
? '
|
|
125
|
-
: '
|
|
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
|
-
'
|
|
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
|
-
'官方峰谷定价
|
|
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.
|
|
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 (
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
//
|
|
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
|
|
65
|
+
if (parts[i].type === type) return parts[i].value
|
|
61
66
|
}
|
|
62
|
-
return
|
|
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
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
var
|
|
82
|
-
|
|
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
|
|
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 =
|
|
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
|
-
? '
|
|
125
|
-
: '
|
|
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
|
-
'
|
|
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
|
-
'官方峰谷定价
|
|
242
|
+
'官方峰谷定价 8/17 生效,8/23 起周末全天谷价;空闲价 = 高峰一半。'),
|
|
189
243
|
],
|
|
190
244
|
)
|
|
191
245
|
|