@suheugene/deepseek-peak-indicator 0.0.0-stage → 1.0.1
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 +40 -2
- package/package.json +29 -4
- package/src/index.ts +6 -0
- package/src/peak.ts +105 -0
- package/src/tui.tsx +34 -0
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @suheugene/deepseek-peak-indicator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An [OpenCode](https://opencode.ai) TUI plugin that shows a DeepSeek off-peak
|
|
4
|
+
badge in the sidebar:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
Deepseek Peak: in 02h 41m ← off-peak, next peak window in 2h 41m (green)
|
|
8
|
+
Deepseek Peak: 22m left ← peak window ends in 22 min (red)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`Deepseek Peak:` is blue; `in HHh MMm` is green; `MMm left` is red. Gaps
|
|
12
|
+
longer than a day show as `Xd Hh` (e.g. Friday evening shows the gap to
|
|
13
|
+
Monday 01:00 UTC).
|
|
14
|
+
|
|
15
|
+
## When is peak?
|
|
16
|
+
|
|
17
|
+
Per the [official DeepSeek pricing](https://api-docs.deepseek.com/quick_start/pricing):
|
|
18
|
+
peak hours are **01:00–04:00 and 06:00–10:00 UTC, Monday through Friday,
|
|
19
|
+
excluding Chinese public holidays**. All other hours are off-peak (half price),
|
|
20
|
+
including weekends and holidays in full.
|
|
21
|
+
|
|
22
|
+
Holidays are hardcoded in `src/peak.ts`: 2026 is official (State Council
|
|
23
|
+
notice, Nov 2025); 2027 is projected until the State Council publishes its
|
|
24
|
+
notice — update the table when it lands.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
// ~/.config/opencode/opencode.json(c)
|
|
30
|
+
{
|
|
31
|
+
"$schema": "https://opencode.ai/config.json",
|
|
32
|
+
"plugins": ["@suheugene/deepseek-peak-indicator"]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Develop
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun install
|
|
40
|
+
bunx tsc --noEmit
|
|
41
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suheugene/deepseek-peak-indicator",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "OpenCode TUI plugin that shows a DeepSeek off-peak badge in the sidebar",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/SuhEugene/deepseek-peak-indicator.git"
|
|
10
|
+
},
|
|
11
|
+
"files": ["src"],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.ts",
|
|
14
|
+
"./tui": "./src/tui.tsx"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@opencode/plugin": "latest"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@opencode/theme": "^2.3.0",
|
|
21
|
+
"@opentui/core": "^0.8.4",
|
|
22
|
+
"@opentui/solid": "^0.8.4",
|
|
23
|
+
"solid-js": ">=1.9.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/bun": "^1.4.4"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
package/src/peak.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek off-peak logic.
|
|
3
|
+
*
|
|
4
|
+
* Official (api-docs.deepseek.com): peak hours are 01:00–04:00 and 06:00–10:00
|
|
5
|
+
* UTC, Monday through Friday, excluding Chinese public holidays. All other
|
|
6
|
+
* hours are off-peak, including weekends and holidays in full.
|
|
7
|
+
*
|
|
8
|
+
* China is UTC+8 with no DST, and both peak windows (+8 = 09:00–12:00 and
|
|
9
|
+
* 14:00–18:00) fall on the same China calendar date as their UTC date — so the
|
|
10
|
+
* UTC date can be matched directly against the China holiday list.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Rest-day ranges (mmdd, inclusive) per year.
|
|
14
|
+
// 2026: official — 国办发明电〔2025〕7号 (gov.cn, 2025-11-04).
|
|
15
|
+
// 2027: projected — the State Council notice is not published yet (due
|
|
16
|
+
// Nov 2026); statutory lunar dates plus the codified bridging pattern.
|
|
17
|
+
// shortcut: 2027 dates are estimates, update when the official notice lands.
|
|
18
|
+
export const HOLIDAYS: Record<number, Array<[number, number]>> = {
|
|
19
|
+
2026: [
|
|
20
|
+
[101, 103],
|
|
21
|
+
[215, 223],
|
|
22
|
+
[404, 406],
|
|
23
|
+
[501, 505],
|
|
24
|
+
[619, 621],
|
|
25
|
+
[925, 927],
|
|
26
|
+
[1001, 1007],
|
|
27
|
+
],
|
|
28
|
+
2027: [
|
|
29
|
+
[101, 101],
|
|
30
|
+
[205, 214],
|
|
31
|
+
[403, 405],
|
|
32
|
+
[501, 505],
|
|
33
|
+
[609, 609],
|
|
34
|
+
[915, 915],
|
|
35
|
+
[1001, 1007],
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// UTC hours [start, end)
|
|
40
|
+
const PEAK_WINDOWS: Array<[number, number]> = [
|
|
41
|
+
[1, 4],
|
|
42
|
+
[6, 10],
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const md = (d: Date) => (d.getUTCMonth() + 1) * 100 + d.getUTCDate()
|
|
46
|
+
|
|
47
|
+
export function isChinaHoliday(d: Date): boolean {
|
|
48
|
+
const year = d.getUTCFullYear()
|
|
49
|
+
const day = md(d)
|
|
50
|
+
return (HOLIDAYS[year] ?? []).some(([a, b]) => day >= a && day <= b)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function peakWindows(d: Date): Array<[number, number]> {
|
|
54
|
+
const wd = d.getUTCDay()
|
|
55
|
+
if (wd === 0 || wd === 6 || isChinaHoliday(d)) return []
|
|
56
|
+
return PEAK_WINDOWS
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Milliseconds since that date's 00:00 UTC. */
|
|
60
|
+
function dayMs(d: Date): number {
|
|
61
|
+
return d.getUTCHours() * 3_600_000 + d.getUTCMinutes() * 60_000 + d.getUTCSeconds() * 1000 + d.getUTCMilliseconds()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type PeakStatus = { peak: boolean; ms: number }
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Current state and time until it flips: `ms` counts down to the end of the
|
|
68
|
+
* current peak window, or up to the start of the next one.
|
|
69
|
+
*/
|
|
70
|
+
export function peakStatus(now: Date): PeakStatus {
|
|
71
|
+
let day = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
|
|
72
|
+
const t = dayMs(now)
|
|
73
|
+
|
|
74
|
+
for (const [start, end] of peakWindows(day)) {
|
|
75
|
+
if (t >= start * 3_600_000 && t < end * 3_600_000) {
|
|
76
|
+
return { peak: true, ms: end * 3_600_000 - t }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Off-peak: find the next window start, at most ~3 weeks out (longest gap:
|
|
81
|
+
// a long Spring Festival break, ~12 days).
|
|
82
|
+
for (let i = 0; i < 21; i++) {
|
|
83
|
+
const windows = peakWindows(day)
|
|
84
|
+
for (const [start] of windows) {
|
|
85
|
+
const s = start * 3_600_000
|
|
86
|
+
if (i > 0 || s > t) return { peak: false, ms: s - t + i * 86_400_000 }
|
|
87
|
+
}
|
|
88
|
+
day = new Date(day.getTime() + 86_400_000)
|
|
89
|
+
}
|
|
90
|
+
return { peak: false, ms: 0 }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** "HHh MMm" (or "Xd Hh" over a day, "Mm" under an hour). */
|
|
94
|
+
export function fmt(ms: number): string {
|
|
95
|
+
const total = Math.max(0, Math.floor(ms / 60_000)) + 1
|
|
96
|
+
const d = Math.floor(total / 60 / 24)
|
|
97
|
+
const h = Math.floor(total / 60) % 24
|
|
98
|
+
const m = total % 60
|
|
99
|
+
|
|
100
|
+
if (d > 0) return `${d}d ${h}h`
|
|
101
|
+
|
|
102
|
+
if (h == 0) return `${m}m`
|
|
103
|
+
|
|
104
|
+
return `${String(h).padStart(2, "0")}h ${String(m).padStart(2, "0")}m`
|
|
105
|
+
}
|
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
2
|
+
import type { Context } from "@opencode/plugin/tui/plugin"
|
|
3
|
+
import { onCleanup, createSignal } from "solid-js"
|
|
4
|
+
import type { ResolvedTheme } from "@opencode/theme/tui"
|
|
5
|
+
import { fmt, peakStatus } from "./peak"
|
|
6
|
+
|
|
7
|
+
function Badge(props: { theme: ResolvedTheme }) {
|
|
8
|
+
const [now, setNow] = createSignal(new Date())
|
|
9
|
+
const tick = setInterval(() => setNow(new Date()), 1000)
|
|
10
|
+
onCleanup(() => clearInterval(tick))
|
|
11
|
+
|
|
12
|
+
const status = () => peakStatus(now())
|
|
13
|
+
return (
|
|
14
|
+
<box flexDirection="row">
|
|
15
|
+
{/* theme.hue.blue exists at runtime, just missing from @opencode/theme's SemanticHue type */}
|
|
16
|
+
{/* @ts-expect-error blue is a real theme hue */}
|
|
17
|
+
<text fg={props.theme.hue.blue[100]}>Deepseek Peak: </text>
|
|
18
|
+
<text fg={status().peak ? props.theme.text.feedback.error.base : props.theme.text.base}>
|
|
19
|
+
{status().peak ? `${fmt(status().ms)} left` : `in ${fmt(status().ms)}`}
|
|
20
|
+
</text>
|
|
21
|
+
</box>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Plugin.define({
|
|
26
|
+
id: "deepseek-peak-indicator",
|
|
27
|
+
setup(context: Context) {
|
|
28
|
+
const theme = context.theme
|
|
29
|
+
context.ui.slot({
|
|
30
|
+
append: "sidebar.content",
|
|
31
|
+
render: () => <Badge theme={theme} />,
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
})
|