dsh-peak-hours 1.10.1 → 1.11.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/CHANGELOG.md +34 -0
- package/README.md +19 -3
- package/client.js +174 -25
- package/package.json +13 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
## 1.11.0 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
### 修正
|
|
6
|
+
|
|
7
|
+
- **说明文案与实现对齐**:日程条的说明原来写「红色=高峰、绿色=空闲」,但条子实际是琥珀橙 / DeepSeek 蓝,现已改为「橙色=高峰、蓝色=空闲」(英文同步为 `amber` / `blue`)。
|
|
8
|
+
- 节假日说明不再暴露源码里的 `HOLIDAYS` 常量名,改成数据驱动:从已收录年份自动生成「节假日表已收录 2026 年中秋、国庆」。
|
|
9
|
+
- `package.json` 补充 `./package.json` 导出、`engines`、`scripts.test`,发布物更规范。
|
|
10
|
+
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- **点击复制状态**:点一下角标(或聚焦后按 Enter / 空格)即可把「高峰 ×2 · 3小时11分后转空闲(北京时间)」复制到剪贴板,1.5 秒内显示「已复制」。剪贴板不可用时回退到临时 textarea。
|
|
14
|
+
- **切换前 10 分钟强调**:距峰↔谷切换不足 10 分钟时,倒计时变琥珀色加粗,圆点轻微脉冲(`prefers-reduced-motion` 下自动关闭动画)。
|
|
15
|
+
- **更完整的 tooltip**:新增「明天」预告(明天周末 / 法定节假日 / 工作日),以及点击复制的提示。
|
|
16
|
+
- **可访问性**:角标改为 `role="button"` + `tabIndex`,带 `aria-label`(含状态、倒计时与操作提示),键盘 `:focus-visible` 有可见焦点环。
|
|
17
|
+
- **单元测试与 CI**:`npm test` 用 `node --test` 跑 15 个用例(时段边界 09:00/12:00/14:00/18:00、周末、节假日、跨日倒计时、日程条分段与宽度、游标位置、折叠态、复制、中英文 key 对齐),GitHub Actions 在 22.x 上跑测试与打包检查。
|
|
18
|
+
|
|
19
|
+
## 1.10.1 — 2026-10-28
|
|
20
|
+
|
|
21
|
+
- 修正 bundle patch 里的行名仍然指向改名前的 `@local/dsh-peak-hours`,导致重命名后客户端半不装载、角标消失。
|
|
22
|
+
|
|
23
|
+
## 1.10.0 — 2026-10-28
|
|
24
|
+
|
|
25
|
+
- 收窄空闲色渐变(`#4d6bfe → #6383ff`),让条子底色与侧栏 logo 的品牌蓝一致。
|
|
26
|
+
- 发布物整理:包名改为 `dsh-peak-hours`,补 LICENSE / README / 图标 / 截图 / `screenshots.json`。
|
|
27
|
+
|
|
28
|
+
## 1.9.0 — 2026-10-27
|
|
29
|
+
|
|
30
|
+
- 空闲色从绿色换成 DeepSeek 品牌蓝 `#4d6bfe`;高峰仍为琥珀色 `#f59e0b`。
|
|
31
|
+
|
|
32
|
+
## 1.4.0 — 2026-10-27
|
|
33
|
+
|
|
34
|
+
- 进度条改为「当天 00:00–24:00 的日程条」:红/绿分段 + 白色当前时刻标尺。
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
## 它显示什么
|
|
8
10
|
|
|
9
11
|
挂在侧栏底部动作行(`sidebar.footer.action`,「插件市场」下面),一行搞定三件事:
|
|
@@ -14,7 +16,12 @@
|
|
|
14
16
|
|
|
15
17
|
侧栏折叠成窄轨时自动退化为「圆点 + 一小截进度条」,不占地方。
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
**距切换不到 10 分钟**时,倒计时会变成琥珀色加粗、圆点轻微脉冲,提醒你「要涨价了」或「要降价了」(系统开启减少动态效果时动画自动关闭)。
|
|
20
|
+
|
|
21
|
+
**点一下角标**(或聚焦后按 Enter / 空格)会把当前状态复制到剪贴板,例如
|
|
22
|
+
`DeepSeek 高峰 ×2 · 3小时11分后转空闲(北京时间)`,方便贴给同事。
|
|
23
|
+
|
|
24
|
+
鼠标悬停能看到完整说明:当前段起止时间、明天是工作日还是休息、以及高峰/空闲的价格表。
|
|
18
25
|
|
|
19
26
|
## 计费规则
|
|
20
27
|
|
|
@@ -25,13 +32,13 @@
|
|
|
25
32
|
- **整天空闲**:周六、周日,以及中国的法定节假日
|
|
26
33
|
- 闲时价 = 高峰价的一半
|
|
27
34
|
|
|
28
|
-
节假日表是**内置的,不联网**:目前覆盖 2026 年(中秋 9/25–27、国庆 10/1–7)。2026 年 9/20(周日)和 10/10
|
|
35
|
+
节假日表是**内置的,不联网**:目前覆盖 2026 年(中秋 9/25–27、国庆 10/1–7)。2026 年 9/20(周日)和 10/10(周六)是调休上班日,按规则仍算空闲。表里没有的年份退化成「周末空闲」规则——这是保守选择:宁可把某天当成空闲(少标高峰),也不误报高峰;遇到新的节假日安排需要更新 `client.js` 里的 `HOLIDAYS`,tooltip 里显示的年份由该表自动推导。
|
|
29
36
|
|
|
30
37
|
时间一律按北京时间(UTC+8)算,和你浏览器的时区无关。
|
|
31
38
|
|
|
32
39
|
## 安装
|
|
33
40
|
|
|
34
|
-
从 npm
|
|
41
|
+
从 npm:
|
|
35
42
|
|
|
36
43
|
```sh
|
|
37
44
|
dsh plugin --profile web add dsh-peak-hours
|
|
@@ -57,6 +64,15 @@ dsh plugin --profile web remove dsh-peak-hours
|
|
|
57
64
|
- 纯客户端插件:不注册 host 工具、不写文件、不发网络请求、不需要任何配置或凭据
|
|
58
65
|
- 颜色是固定值(高峰 `#f59e0b`、空闲 `#4d6bfe`,即 DeepSeek 品牌蓝),不跟随主题 `brand-primary`,以免换皮肤后蓝色被改成别的色
|
|
59
66
|
|
|
67
|
+
## 开发
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm test # 15 个单元测试(node --test,零依赖)
|
|
71
|
+
npm run verify # npm pack --dry-run,检查发布清单
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
测试通过 `vm` 加载 `client.js`,用最小的 React / locale / document 替身渲染组件,因此不用起 GUI 也能验证时段判定、分段宽度、折叠态与复制逻辑。
|
|
75
|
+
|
|
60
76
|
## 许可
|
|
61
77
|
|
|
62
78
|
MIT
|
package/client.js
CHANGED
|
@@ -14,11 +14,14 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
[14 * 60, 18 * 60],
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
+
// 距峰↔谷切换不足该分钟数时,倒计时与圆点进入强调态。
|
|
18
|
+
const SOON_MINUTES = 10;
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* 中国法定节假日(全天空闲计价)。来源:2026-09-19 官方说明
|
|
19
22
|
* 「法定节假日全天、调休上班的周末全天均按空闲时段计费」。
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* 已核对 2026 年中秋、国庆;新一年的放假安排公布后按公告补充,
|
|
24
|
+
* 未收录的年份只按周末规则判定(周末本就全天空闲,因此不会判错方向)。
|
|
22
25
|
*/
|
|
23
26
|
const HOLIDAYS = {
|
|
24
27
|
2026: [
|
|
@@ -27,6 +30,7 @@ window.__ModuleLoader__.load({
|
|
|
27
30
|
'2026-10-05', '2026-10-06', '2026-10-07',
|
|
28
31
|
],
|
|
29
32
|
};
|
|
33
|
+
const HOLIDAY_YEARS = Object.keys(HOLIDAYS).sort();
|
|
30
34
|
const holidayCache = new Map();
|
|
31
35
|
function holidaySet(year) {
|
|
32
36
|
if (holidayCache.has(year)) return holidayCache.get(year);
|
|
@@ -73,6 +77,15 @@ window.__ModuleLoader__.load({
|
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/** 次日的计价形态:holiday / weekend / workday。 */
|
|
81
|
+
function kindAt(ms) {
|
|
82
|
+
const t = beijing(ms);
|
|
83
|
+
const holidays = holidaySet(t.year);
|
|
84
|
+
if (holidays && holidays.has(t.key)) return 'holiday';
|
|
85
|
+
if (t.dow === 0 || t.dow === 6) return 'weekend';
|
|
86
|
+
return 'workday';
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
/** 下一个峰↔谷切换的瞬间(分钟对齐),最多向前找 14 天。 */
|
|
77
90
|
function nextSwitch(ms) {
|
|
78
91
|
const current = stateAt(ms);
|
|
@@ -107,7 +120,7 @@ window.__ModuleLoader__.load({
|
|
|
107
120
|
return { start, end, progress };
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
/** 当天 00:00–24:00
|
|
123
|
+
/** 当天 00:00–24:00 的峰谷分段(分钟),画成橙蓝相间的日程条。 */
|
|
111
124
|
function daySchedule(ms) {
|
|
112
125
|
const st = stateAt(ms);
|
|
113
126
|
if (st.allDayOff) return [{ from: 0, to: 1440, peak: false }];
|
|
@@ -142,6 +155,10 @@ window.__ModuleLoader__.load({
|
|
|
142
155
|
return b.month + '-' + pad2(b.day) + ' ' + b.clock;
|
|
143
156
|
}
|
|
144
157
|
|
|
158
|
+
function holidayNote(t) {
|
|
159
|
+
return (t('tip.table') || '').replace('{years}', HOLIDAY_YEARS.join(' / '));
|
|
160
|
+
}
|
|
161
|
+
|
|
145
162
|
function tooltip(t, st, phase, now) {
|
|
146
163
|
const lines = [
|
|
147
164
|
t('tip.head'),
|
|
@@ -153,6 +170,14 @@ window.__ModuleLoader__.load({
|
|
|
153
170
|
];
|
|
154
171
|
if (st.isHoliday) lines.push(t('tip.holidayToday'));
|
|
155
172
|
else if (st.isWeekend) lines.push(t('tip.weekendToday'));
|
|
173
|
+
const tomorrow = kindAt(now + 1440 * MINUTE);
|
|
174
|
+
lines.push(
|
|
175
|
+
tomorrow === 'holiday'
|
|
176
|
+
? t('tip.tomorrowHoliday')
|
|
177
|
+
: tomorrow === 'weekend'
|
|
178
|
+
? t('tip.tomorrowWeekend')
|
|
179
|
+
: t('tip.tomorrowWorkday'),
|
|
180
|
+
);
|
|
156
181
|
if (phase.start !== null && phase.end !== null) {
|
|
157
182
|
lines.push(
|
|
158
183
|
t('tip.phase') + clockLabel(phase.start) + ' → ' + clockLabel(phase.end),
|
|
@@ -169,12 +194,70 @@ window.__ModuleLoader__.load({
|
|
|
169
194
|
t('tip.flash'),
|
|
170
195
|
t('tip.pro'),
|
|
171
196
|
'',
|
|
172
|
-
t
|
|
197
|
+
holidayNote(t),
|
|
173
198
|
t('tip.tz'),
|
|
199
|
+
t('tip.hint'),
|
|
174
200
|
);
|
|
175
201
|
return lines.join('\n');
|
|
176
202
|
}
|
|
177
203
|
|
|
204
|
+
function copySummary(t, st, phase, now) {
|
|
205
|
+
const pill = st.peak ? t('price.multiplier') : t('price.discount');
|
|
206
|
+
const rest =
|
|
207
|
+
phase.end !== null
|
|
208
|
+
? t('tip.next') +
|
|
209
|
+
fmtDuration(phase.end - now, t) +
|
|
210
|
+
(st.peak ? t('next.off') : t('next.peak'))
|
|
211
|
+
: '';
|
|
212
|
+
return (
|
|
213
|
+
'DeepSeek ' +
|
|
214
|
+
(st.peak ? t('status.peak') : t('status.off')) +
|
|
215
|
+
' ' +
|
|
216
|
+
pill +
|
|
217
|
+
' · ' +
|
|
218
|
+
rest +
|
|
219
|
+
' ' +
|
|
220
|
+
t('copy.suffix')
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 复制到剪贴板:Clipboard API 优先,失败退回临时 textarea。 */
|
|
225
|
+
function writeClipboard(text) {
|
|
226
|
+
try {
|
|
227
|
+
if (
|
|
228
|
+
typeof navigator !== 'undefined' &&
|
|
229
|
+
navigator.clipboard &&
|
|
230
|
+
typeof navigator.clipboard.writeText === 'function'
|
|
231
|
+
) {
|
|
232
|
+
return Promise.resolve(navigator.clipboard.writeText(text)).then(
|
|
233
|
+
() => true,
|
|
234
|
+
() => fallbackCopy(text),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
/* 落到兜底 */
|
|
239
|
+
}
|
|
240
|
+
return Promise.resolve(fallbackCopy(text));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function fallbackCopy(text) {
|
|
244
|
+
try {
|
|
245
|
+
if (typeof document === 'undefined' || !document.body) return false;
|
|
246
|
+
const area = document.createElement('textarea');
|
|
247
|
+
area.value = text;
|
|
248
|
+
area.setAttribute('readonly', '');
|
|
249
|
+
area.style.position = 'fixed';
|
|
250
|
+
area.style.opacity = '0';
|
|
251
|
+
document.body.appendChild(area);
|
|
252
|
+
area.select();
|
|
253
|
+
const ok = document.execCommand && document.execCommand('copy');
|
|
254
|
+
document.body.removeChild(area);
|
|
255
|
+
return !!ok;
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
178
261
|
// 字族跟随宿主,字号显式压到侧栏次级字号(0.8125rem ≈ 13px),
|
|
179
262
|
// 因为 footer 动作行从根节点继承的是 1rem,直接用 inherit 会比菜单项大一号。
|
|
180
263
|
const CSS = `
|
|
@@ -189,10 +272,14 @@ window.__ModuleLoader__.load({
|
|
|
189
272
|
font-family: inherit; font-size: 14px; font-size: .875rem;
|
|
190
273
|
font-weight: 400; line-height: 1.45;
|
|
191
274
|
color: var(--dsw-alias-label-primary);
|
|
192
|
-
user-select: none; cursor:
|
|
275
|
+
user-select: none; cursor: pointer; outline: none;
|
|
193
276
|
transition: background-color .15s ease;
|
|
194
277
|
}
|
|
195
278
|
.dshph-root:hover { background: var(--dsw-alias-bg-layer-2); }
|
|
279
|
+
.dshph-root:focus-visible {
|
|
280
|
+
outline: 2px solid var(--dsw-alias-brand-primary, #4d6bfe);
|
|
281
|
+
outline-offset: 1px;
|
|
282
|
+
}
|
|
196
283
|
.dshph-head { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
197
284
|
.dshph-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; }
|
|
198
285
|
.dshph-peak .dshph-dot {
|
|
@@ -203,6 +290,11 @@ window.__ModuleLoader__.load({
|
|
|
203
290
|
background: var(--dshph-off);
|
|
204
291
|
box-shadow: 0 0 0 3px rgba(77, 107, 254, .2);
|
|
205
292
|
}
|
|
293
|
+
.dshph-dot-soon { animation: dshph-pulse 1.6s ease-in-out infinite; }
|
|
294
|
+
@keyframes dshph-pulse {
|
|
295
|
+
0%, 100% { box-shadow: 0 0 0 2px rgba(245, 158, 11, .22); }
|
|
296
|
+
50% { box-shadow: 0 0 0 5px rgba(245, 158, 11, .06); }
|
|
297
|
+
}
|
|
206
298
|
.dshph-label,
|
|
207
299
|
.dshph-pill,
|
|
208
300
|
.dshph-next {
|
|
@@ -216,6 +308,8 @@ window.__ModuleLoader__.load({
|
|
|
216
308
|
flex: 0 1 auto; min-width: 0; overflow: hidden;
|
|
217
309
|
text-overflow: ellipsis; white-space: nowrap;
|
|
218
310
|
}
|
|
311
|
+
.dshph-soon .dshph-next { color: #f59e0b; font-weight: 600; }
|
|
312
|
+
.dshph-toast { color: var(--dsw-alias-label-secondary); }
|
|
219
313
|
.dshph-track {
|
|
220
314
|
position: relative; height: 8px; border-radius: 999px;
|
|
221
315
|
background: var(--dsw-alias-bg-layer-2); overflow: hidden;
|
|
@@ -240,6 +334,11 @@ window.__ModuleLoader__.load({
|
|
|
240
334
|
}
|
|
241
335
|
.dshph-rail .dshph-track { width: 26px; height: 6px; }
|
|
242
336
|
.dshph-rail .dshph-cursor { width: 1.5px; }
|
|
337
|
+
@media (prefers-reduced-motion: reduce) {
|
|
338
|
+
.dshph-root, .dshph-root * {
|
|
339
|
+
animation: none !important; transition: none !important;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
243
342
|
`;
|
|
244
343
|
|
|
245
344
|
function ensureStyle() {
|
|
@@ -270,18 +369,25 @@ window.__ModuleLoader__.load({
|
|
|
270
369
|
'tip.head': 'DeepSeek 峰谷计费',
|
|
271
370
|
'tip.current': '当前:',
|
|
272
371
|
'tip.window': '高峰时段:工作日 09:00–12:00、14:00–18:00(北京时间)',
|
|
273
|
-
'tip.bar': '日程条:当天 00:00–24:00
|
|
372
|
+
'tip.bar': '日程条:当天 00:00–24:00,橙色=高峰、蓝色=空闲,白线是当前时刻',
|
|
274
373
|
'tip.rule': '其余时间按空闲计价,闲时价为高峰价的一半',
|
|
275
374
|
'tip.weekend': '周末与法定节假日全天按空闲计价',
|
|
276
375
|
'tip.holidayToday': '今天:法定节假日,全天按空闲计价',
|
|
277
376
|
'tip.weekendToday': '今天:周末,全天按空闲计价',
|
|
377
|
+
'tip.tomorrowHoliday': '明天:法定节假日,全天按空闲计价',
|
|
378
|
+
'tip.tomorrowWeekend': '明天:周末,全天按空闲计价',
|
|
379
|
+
'tip.tomorrowWorkday': '明天:工作日,恢复按 09:00–12:00、14:00–18:00 计价',
|
|
278
380
|
'tip.phase': '本段:',
|
|
279
381
|
'tip.next': '距切换:',
|
|
280
382
|
'tip.prices': '价格(元/百万 tokens,空闲 → 高峰)',
|
|
281
383
|
'tip.flash': 'deepseek-flash:缓存命中 0.02 → 0.04|未命中 1 → 2|输出 4 → 8',
|
|
282
384
|
'tip.pro': 'deepseek-v4-pro:缓存命中 0.15 → 0.30|未命中 4.5 → 9.0|输出 13.5 → 27.0',
|
|
283
|
-
'tip.table': '
|
|
385
|
+
'tip.table': '节假日表已收录 {years} 年中秋、国庆;新一年安排公布后随版本更新',
|
|
284
386
|
'tip.tz': '判定时区:北京时间(UTC+8)',
|
|
387
|
+
'tip.hint': '点击复制当前状态',
|
|
388
|
+
'copy.ok': '已复制',
|
|
389
|
+
'copy.fail': '复制失败',
|
|
390
|
+
'copy.suffix': '(北京时间)',
|
|
285
391
|
};
|
|
286
392
|
|
|
287
393
|
const EN = {
|
|
@@ -299,18 +405,25 @@ window.__ModuleLoader__.load({
|
|
|
299
405
|
'tip.head': 'DeepSeek peak/off-peak pricing',
|
|
300
406
|
'tip.current': 'Now: ',
|
|
301
407
|
'tip.window': 'Peak: weekdays 09:00–12:00 and 14:00–18:00 (Beijing time)',
|
|
302
|
-
'tip.bar': 'Day bar: 00:00–24:00,
|
|
408
|
+
'tip.bar': 'Day bar: 00:00–24:00, amber = peak, blue = off-peak, white line = now',
|
|
303
409
|
'tip.rule': 'Everything else is off-peak, priced at half of the peak rate',
|
|
304
410
|
'tip.weekend': 'Weekends and public holidays are off-peak all day',
|
|
305
411
|
'tip.holidayToday': 'Today: public holiday, off-peak all day',
|
|
306
412
|
'tip.weekendToday': 'Today: weekend, off-peak all day',
|
|
413
|
+
'tip.tomorrowHoliday': 'Tomorrow: public holiday, off-peak all day',
|
|
414
|
+
'tip.tomorrowWeekend': 'Tomorrow: weekend, off-peak all day',
|
|
415
|
+
'tip.tomorrowWorkday': 'Tomorrow: weekday, peak windows apply again',
|
|
307
416
|
'tip.phase': 'This block: ',
|
|
308
417
|
'tip.next': 'Switch in ',
|
|
309
418
|
'tip.prices': 'Price (CNY / million tokens, off-peak → peak)',
|
|
310
419
|
'tip.flash': 'deepseek-flash: cache hit 0.02 → 0.04 | miss 1 → 2 | output 4 → 8',
|
|
311
420
|
'tip.pro': 'deepseek-v4-pro: cache hit 0.15 → 0.30 | miss 4.5 → 9.0 | output 13.5 → 27.0',
|
|
312
|
-
'tip.table': '
|
|
421
|
+
'tip.table': 'Holiday table covers {years} (Mid-Autumn, National Day); updated per release',
|
|
313
422
|
'tip.tz': 'Evaluated in Beijing time (UTC+8)',
|
|
423
|
+
'tip.hint': 'Click to copy the current state',
|
|
424
|
+
'copy.ok': 'Copied',
|
|
425
|
+
'copy.fail': 'Copy failed',
|
|
426
|
+
'copy.suffix': '(Beijing time)',
|
|
314
427
|
};
|
|
315
428
|
|
|
316
429
|
let translate = (key) => (EN[key] !== undefined ? EN[key] : key);
|
|
@@ -318,6 +431,8 @@ window.__ModuleLoader__.load({
|
|
|
318
431
|
function PeakBadge(props) {
|
|
319
432
|
const t = translate;
|
|
320
433
|
const [now, setNow] = React.useState(() => Date.now());
|
|
434
|
+
const [toast, setToast] = React.useState('');
|
|
435
|
+
|
|
321
436
|
React.useEffect(() => {
|
|
322
437
|
const id = setInterval(() => setNow(Date.now()), 15000);
|
|
323
438
|
return () => clearInterval(id);
|
|
@@ -330,12 +445,42 @@ window.__ModuleLoader__.load({
|
|
|
330
445
|
const wide = !props || props.wide !== false;
|
|
331
446
|
const stateText = off ? t('status.off') : t('status.peak');
|
|
332
447
|
const title = tooltip(t, st, phase, now);
|
|
333
|
-
const
|
|
448
|
+
const soon =
|
|
449
|
+
phase.end !== null && phase.end - now <= SOON_MINUTES * MINUTE;
|
|
450
|
+
const rootClass =
|
|
451
|
+
'dshph-root ' +
|
|
452
|
+
(off ? 'dshph-off' : 'dshph-peak') +
|
|
453
|
+
(soon ? ' dshph-soon' : '');
|
|
334
454
|
const cursorLeft = pct(st.beijing.minute);
|
|
335
455
|
const remaining = phase.end
|
|
336
456
|
? fmtDuration(phase.end - now, t) + (off ? t('next.peak') : t('next.off'))
|
|
337
457
|
: '';
|
|
338
458
|
|
|
459
|
+
const onCopy = () => {
|
|
460
|
+
const text = copySummary(t, st, phase, now);
|
|
461
|
+
let result;
|
|
462
|
+
try {
|
|
463
|
+
result = writeClipboard(text);
|
|
464
|
+
} catch {
|
|
465
|
+
result = Promise.resolve(false);
|
|
466
|
+
}
|
|
467
|
+
Promise.resolve(result)
|
|
468
|
+
.then((ok) => setToast(ok ? t('copy.ok') : t('copy.fail')))
|
|
469
|
+
.catch(() => setToast(t('copy.fail')));
|
|
470
|
+
setTimeout(() => setToast(''), 1500);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const onKeyDown = (event) => {
|
|
474
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
|
|
475
|
+
event.preventDefault();
|
|
476
|
+
onCopy();
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const headText = toast
|
|
481
|
+
? h('span', { className: 'dshph-next dshph-toast' }, toast)
|
|
482
|
+
: h('span', { className: 'dshph-next' }, remaining);
|
|
483
|
+
|
|
339
484
|
const track = h(
|
|
340
485
|
'div',
|
|
341
486
|
{ className: 'dshph-track' },
|
|
@@ -350,32 +495,36 @@ window.__ModuleLoader__.load({
|
|
|
350
495
|
h('span', { key: 'cursor', className: 'dshph-cursor', style: { left: cursorLeft } }),
|
|
351
496
|
);
|
|
352
497
|
|
|
498
|
+
const common = {
|
|
499
|
+
title,
|
|
500
|
+
role: 'button',
|
|
501
|
+
tabIndex: 0,
|
|
502
|
+
onClick: onCopy,
|
|
503
|
+
onKeyDown,
|
|
504
|
+
'aria-label':
|
|
505
|
+
t('label') + ': ' + stateText + ', ' + remaining + '. ' + t('tip.hint'),
|
|
506
|
+
};
|
|
507
|
+
|
|
353
508
|
if (!wide) {
|
|
354
509
|
return h(
|
|
355
510
|
'div',
|
|
356
|
-
{
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
'aria-label': t('label') + ': ' + stateText,
|
|
361
|
-
},
|
|
362
|
-
h('span', { className: 'dshph-dot' }),
|
|
511
|
+
{ ...common, className: rootClass + ' dshph-rail' },
|
|
512
|
+
h('span', {
|
|
513
|
+
className: 'dshph-dot' + (soon ? ' dshph-dot-soon' : ''),
|
|
514
|
+
}),
|
|
363
515
|
track,
|
|
364
516
|
);
|
|
365
517
|
}
|
|
366
518
|
|
|
367
519
|
return h(
|
|
368
520
|
'div',
|
|
369
|
-
{
|
|
370
|
-
className: rootClass,
|
|
371
|
-
title,
|
|
372
|
-
role: 'status',
|
|
373
|
-
'aria-label': t('label') + ': ' + stateText,
|
|
374
|
-
},
|
|
521
|
+
{ ...common, className: rootClass },
|
|
375
522
|
h(
|
|
376
523
|
'div',
|
|
377
524
|
{ className: 'dshph-head' },
|
|
378
|
-
h('span', {
|
|
525
|
+
h('span', {
|
|
526
|
+
className: 'dshph-dot' + (soon ? ' dshph-dot-soon' : ''),
|
|
527
|
+
}),
|
|
379
528
|
h('span', { className: 'dshph-label' }, stateText),
|
|
380
529
|
h(
|
|
381
530
|
'span',
|
|
@@ -383,7 +532,7 @@ window.__ModuleLoader__.load({
|
|
|
383
532
|
off ? t('price.discount') : t('price.multiplier'),
|
|
384
533
|
),
|
|
385
534
|
h('span', { className: 'dshph-spacer' }),
|
|
386
|
-
|
|
535
|
+
headText,
|
|
387
536
|
),
|
|
388
537
|
track,
|
|
389
538
|
);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-peak-hours",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Sidebar badge that marks the current DeepSeek peak-pricing window
|
|
5
|
+
"description": "Sidebar badge that marks the current DeepSeek peak-pricing window: 24-hour schedule bar, now-cursor, countdown to the next switch, click to copy the state.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "run666888 <zhirunli6@gmail.com>",
|
|
8
8
|
"homepage": "https://github.com/run666888/dsh-peak-hours#readme",
|
|
@@ -13,11 +13,19 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/run666888/dsh-peak-hours/issues"
|
|
15
15
|
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test",
|
|
21
|
+
"verify": "npm pack --dry-run"
|
|
22
|
+
},
|
|
16
23
|
"keywords": [
|
|
17
24
|
"dsh",
|
|
18
25
|
"dsh-plugin",
|
|
19
26
|
"deepseek-harness",
|
|
20
27
|
"peak-hours",
|
|
28
|
+
"off-peak",
|
|
21
29
|
"pricing",
|
|
22
30
|
"sidebar"
|
|
23
31
|
],
|
|
@@ -28,7 +36,8 @@
|
|
|
28
36
|
".": "./index.js",
|
|
29
37
|
"./client": "./client.js",
|
|
30
38
|
"./locale/*.json": "./locale/*.json",
|
|
31
|
-
"./icon": "./icon.svg"
|
|
39
|
+
"./icon": "./icon.svg",
|
|
40
|
+
"./package.json": "./package.json"
|
|
32
41
|
},
|
|
33
42
|
"files": [
|
|
34
43
|
"index.js",
|
|
@@ -40,6 +49,7 @@
|
|
|
40
49
|
"screenshots",
|
|
41
50
|
"screenshots.json",
|
|
42
51
|
"README.md",
|
|
52
|
+
"CHANGELOG.md",
|
|
43
53
|
"LICENSE"
|
|
44
54
|
],
|
|
45
55
|
"dsh": {
|