opencode-deepseek-peak 0.1.0 → 0.2.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 +27 -3
- package/dist/index.js +17 -14
- package/dist/index.js.map +2 -2
- package/dist/tui-v1.js +48204 -0
- package/dist/tui-v1.js.map +7 -0
- package/dist/tui.js +47750 -16
- package/dist/tui.js.map +4 -4
- package/install.mjs +12 -8
- package/package.json +3 -1
- package/src/core/display.ts +62 -3
- package/src/i18n.ts +37 -2
- package/src/index.ts +23 -16
- package/src/panel/PeakPanel.tsx +6 -1
- package/src/schedule.ts +39 -0
- package/src/tui.tsx +27 -5
- package/src/types.ts +2 -0
- package/src/v1/tui.tsx +293 -0
- package/src/v2/commands.ts +9 -2
- package/src/v2/settings-menu.ts +19 -1
- package/tui/index.js +7 -2
package/install.mjs
CHANGED
|
@@ -24,28 +24,32 @@ async function install() {
|
|
|
24
24
|
await mkdir(directory, { recursive: true })
|
|
25
25
|
const jsoncPath = join(directory, "opencode.jsonc")
|
|
26
26
|
const jsonc = await readOptional(jsoncPath)
|
|
27
|
+
const tuiJsoncPath = join(directory, "tui.jsonc")
|
|
28
|
+
const tuiJsonc = await readOptional(tuiJsoncPath)
|
|
29
|
+
// [path, $schema, list key]: V1 TUI plugins live in tui.json under `plugin`.
|
|
27
30
|
const targets = [
|
|
28
|
-
[jsonc !== undefined ? jsoncPath : join(directory, "opencode.json"), "https://opencode.ai/config.json"],
|
|
29
|
-
[join(directory, "cli.json"), "https://opencode.ai/v2/cli.json"],
|
|
31
|
+
[jsonc !== undefined ? jsoncPath : join(directory, "opencode.json"), "https://opencode.ai/config.json", "plugins"],
|
|
32
|
+
[join(directory, "cli.json"), "https://opencode.ai/v2/cli.json", "plugins"],
|
|
33
|
+
[tuiJsonc !== undefined ? tuiJsoncPath : join(directory, "tui.json"), "https://opencode.ai/tui.json", "plugin"],
|
|
30
34
|
]
|
|
31
|
-
// Validate
|
|
35
|
+
// Validate every document before writing any of them. JSONC edits preserve
|
|
32
36
|
// comments, trailing commas and unrelated settings.
|
|
33
37
|
const updates = []
|
|
34
|
-
for (const [path, schema] of targets) {
|
|
35
|
-
const text = await readOptional(path) ?? JSON.stringify({ $schema: schema,
|
|
38
|
+
for (const [path, schema, key] of targets) {
|
|
39
|
+
const text = await readOptional(path) ?? JSON.stringify({ $schema: schema, [key]: [] }, null, 2) + "\n"
|
|
36
40
|
const errors = []
|
|
37
41
|
const config = parse(text, errors, { allowTrailingComma: true })
|
|
38
42
|
if (errors.length || !config || typeof config !== "object" || Array.isArray(config)) {
|
|
39
43
|
throw new Error(`Invalid configuration: ${path}`)
|
|
40
44
|
}
|
|
41
|
-
const plugins = config
|
|
42
|
-
if (!Array.isArray(plugins)) throw new Error(`Expected a
|
|
45
|
+
const plugins = config[key] ?? []
|
|
46
|
+
if (!Array.isArray(plugins)) throw new Error(`Expected a ${key} array: ${path}`)
|
|
43
47
|
const present = plugins.some((item) => {
|
|
44
48
|
const spec = typeof item === "string" ? item : item?.package
|
|
45
49
|
return spec === PLUGIN_SPEC || spec?.startsWith(`${PLUGIN_SPEC}@`)
|
|
46
50
|
})
|
|
47
51
|
if (present) continue
|
|
48
|
-
const result = applyEdits(text, modify(text, [
|
|
52
|
+
const result = applyEdits(text, modify(text, [key], [...plugins, PLUGIN_SPEC], {
|
|
49
53
|
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
50
54
|
}))
|
|
51
55
|
updates.push([path, result])
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-deepseek-peak",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "OpenCode V2 plugin showing DeepSeek peak and off-peak pricing periods",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
"jsonc-parser": "^3.3.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@opencode-ai/plugin": "^1.18.16",
|
|
55
|
+
"@opencode-ai/sdk": "^1.18.16",
|
|
54
56
|
"@opencode/plugin": "^2.0.16",
|
|
55
57
|
"@opentui/core": "^0.5.10",
|
|
56
58
|
"@opentui/solid": "^0.5.10",
|
package/src/core/display.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin/tui"
|
|
2
|
-
import type { T, TranslationKey } from "../i18n.js"
|
|
2
|
+
import type { LangCode, T, TranslationKey } from "../i18n.js"
|
|
3
|
+
import { nextTransition } from "../schedule.js"
|
|
3
4
|
import type { PeakStatus, TuiSettings } from "../types.js"
|
|
4
5
|
|
|
5
6
|
export const INITIAL_SETTINGS: TuiSettings = {
|
|
@@ -9,6 +10,7 @@ export const INITIAL_SETTINGS: TuiSettings = {
|
|
|
9
10
|
sidebarEvidence: true,
|
|
10
11
|
toastEveryRequest: false,
|
|
11
12
|
lang: "en",
|
|
13
|
+
timezone: "local",
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function statusLabel(status: PeakStatus, t: T) {
|
|
@@ -26,9 +28,65 @@ const HOLIDAY_KEYS: Record<string, TranslationKey> = {
|
|
|
26
28
|
"Labour Day": "holiday.labourDay",
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export interface LabelOptions {
|
|
32
|
+
/** Reference instant for the next transition; defaults to now. */
|
|
33
|
+
now?: Date
|
|
34
|
+
lang?: LangCode
|
|
35
|
+
/** IANA time zone; undefined uses the system zone, "UTC" shows the official times. */
|
|
36
|
+
timeZone?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function zoneDate(date: Date, timeZone: string | undefined): string {
|
|
40
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
41
|
+
timeZone,
|
|
42
|
+
year: "numeric",
|
|
43
|
+
month: "2-digit",
|
|
44
|
+
day: "2-digit",
|
|
45
|
+
}).formatToParts(date)
|
|
46
|
+
const value = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value ?? ""
|
|
47
|
+
return `${value("year")}-${value("month")}-${value("day")}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatClock(date: Date, timeZone: string | undefined): string {
|
|
51
|
+
const parts = new Intl.DateTimeFormat("en-GB", {
|
|
52
|
+
timeZone,
|
|
53
|
+
hour: "2-digit",
|
|
54
|
+
minute: "2-digit",
|
|
55
|
+
hourCycle: "h23",
|
|
56
|
+
}).formatToParts(date)
|
|
57
|
+
const value = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value ?? ""
|
|
58
|
+
return `${value("hour")}:${value("minute")}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** "10:00 UTC" in UTC mode, "07:00" in the local zone; adds a short weekday on another day. */
|
|
62
|
+
function formatTransitionAt(at: Date, now: Date, lang: LangCode, timeZone: string | undefined): string {
|
|
63
|
+
const clock = timeZone === "UTC" ? `${formatClock(at, timeZone)} UTC` : formatClock(at, timeZone)
|
|
64
|
+
if (zoneDate(at, timeZone) === zoneDate(now, timeZone)) return clock
|
|
65
|
+
const weekday = new Intl.DateTimeFormat(lang, { weekday: "short", timeZone }).format(at)
|
|
66
|
+
return `${weekday} ${clock}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Localized "peak ends 10:00 UTC" / "peak starts Mon 01:00" for the next change. */
|
|
70
|
+
export function transitionLabel(t: T, options: LabelOptions = {}): string {
|
|
71
|
+
const now = options.now ?? new Date()
|
|
72
|
+
const next = nextTransition(now)
|
|
73
|
+
return t(next.kind === "peakEnd" ? "evidence.peakEnds" : "evidence.peakStarts", {
|
|
74
|
+
when: formatTransitionAt(next.at, now, options.lang ?? "en", options.timeZone),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Compact sidebar form: ▲ peak starts / ▼ peak ends. Arrows need no translation. */
|
|
79
|
+
export function transitionCompact(options: LabelOptions = {}): string {
|
|
80
|
+
const now = options.now ?? new Date()
|
|
81
|
+
const next = nextTransition(now)
|
|
82
|
+
const arrow = next.kind === "peakEnd" ? "▼" : "▲"
|
|
83
|
+
return `${arrow} ${formatTransitionAt(next.at, now, options.lang ?? "en", options.timeZone)}`
|
|
84
|
+
}
|
|
85
|
+
|
|
29
86
|
/** Short, sidebar-friendly, localized version of the status evidence. */
|
|
30
|
-
export function evidenceLabel(status: PeakStatus, t: T): string {
|
|
87
|
+
export function evidenceLabel(status: PeakStatus, t: T, options: LabelOptions = {}): string {
|
|
31
88
|
let label: string
|
|
89
|
+
let transition = ""
|
|
32
90
|
// An API response header is the source of truth when present.
|
|
33
91
|
if (status.source === "api-header") {
|
|
34
92
|
label = t("evidence.apiHeader", { header: status.evidence.split(":")[0] })
|
|
@@ -37,10 +95,11 @@ export function evidenceLabel(status: PeakStatus, t: T): string {
|
|
|
37
95
|
label = t("evidence.holiday", { name: key ? t(key) : status.holiday })
|
|
38
96
|
} else {
|
|
39
97
|
label = t("evidence.schedule")
|
|
98
|
+
transition = ` · ${transitionCompact(options)}`
|
|
40
99
|
}
|
|
41
100
|
if (status.evidence.endsWith("(cached)")) label += ` ${t("evidence.cached")}`
|
|
42
101
|
if (status.evidence.includes("no request observed yet")) label += ` ${t("evidence.preview")}`
|
|
43
|
-
return label
|
|
102
|
+
return label + transition
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
/**
|
package/src/i18n.ts
CHANGED
|
@@ -10,11 +10,13 @@ export type LangCode = "en" | "pt" | "zh" | "ja" | "ko"
|
|
|
10
10
|
const EN_T = {
|
|
11
11
|
"period.peak": "PEAK",
|
|
12
12
|
"period.offPeak": "OFF-PEAK",
|
|
13
|
-
"evidence.schedule": "
|
|
13
|
+
"evidence.schedule": "Official schedule",
|
|
14
14
|
"evidence.apiHeader": "via {header}",
|
|
15
15
|
"evidence.cached": "(cached)",
|
|
16
16
|
"evidence.preview": "(no request yet)",
|
|
17
17
|
"evidence.holiday": "Holiday: {name}",
|
|
18
|
+
"evidence.peakEnds": "peak ends {when}",
|
|
19
|
+
"evidence.peakStarts": "peak starts {when}",
|
|
18
20
|
"label.mismatch": "mismatch",
|
|
19
21
|
"toast.title": "DeepSeek pricing",
|
|
20
22
|
"status.title": "DeepSeek Peak",
|
|
@@ -22,6 +24,8 @@ const EN_T = {
|
|
|
22
24
|
"status.noRequest": "No DeepSeek request observed in this session yet.",
|
|
23
25
|
"status.source": "Source",
|
|
24
26
|
"status.evidence": "Evidence",
|
|
27
|
+
"status.windows": "Peak: 01:00–04:00 and 06:00–10:00 UTC, Mon–Fri, except Chinese public holidays",
|
|
28
|
+
"status.nextChange": "Next change",
|
|
25
29
|
"status.unavailable": "DeepSeek Peak server plugin is unavailable.",
|
|
26
30
|
"command.status.title": "DeepSeek Peak: Show Status",
|
|
27
31
|
"command.status.description": "Show the last observed DeepSeek pricing period",
|
|
@@ -36,6 +40,9 @@ const EN_T = {
|
|
|
36
40
|
"settings.evidence": "Sidebar evidence",
|
|
37
41
|
"settings.frequency": "Toast frequency",
|
|
38
42
|
"settings.lang": "Language",
|
|
43
|
+
"settings.timezone": "Timezone",
|
|
44
|
+
"timezone.local": "Local",
|
|
45
|
+
"timezone.utc": "UTC",
|
|
39
46
|
"settings.on": "on",
|
|
40
47
|
"settings.off": "off",
|
|
41
48
|
"frequency.change": "on change",
|
|
@@ -54,11 +61,13 @@ export type TranslationKey = keyof Translation
|
|
|
54
61
|
const PT_T: Translation = {
|
|
55
62
|
"period.peak": "PICO",
|
|
56
63
|
"period.offPeak": "FORA DE PICO",
|
|
57
|
-
"evidence.schedule": "
|
|
64
|
+
"evidence.schedule": "Horário oficial",
|
|
58
65
|
"evidence.apiHeader": "via {header}",
|
|
59
66
|
"evidence.cached": "(cache)",
|
|
60
67
|
"evidence.preview": "(sem requisição ainda)",
|
|
61
68
|
"evidence.holiday": "Feriado: {name}",
|
|
69
|
+
"evidence.peakEnds": "pico termina {when}",
|
|
70
|
+
"evidence.peakStarts": "pico começa {when}",
|
|
62
71
|
"label.mismatch": "divergente",
|
|
63
72
|
"toast.title": "Preço DeepSeek",
|
|
64
73
|
"status.title": "DeepSeek Peak",
|
|
@@ -66,6 +75,8 @@ const PT_T: Translation = {
|
|
|
66
75
|
"status.noRequest": "Nenhuma requisição DeepSeek observada nesta sessão ainda.",
|
|
67
76
|
"status.source": "Fonte",
|
|
68
77
|
"status.evidence": "Evidência",
|
|
78
|
+
"status.windows": "Pico: 01:00–04:00 e 06:00–10:00 UTC, seg–sex, exceto feriados chineses",
|
|
79
|
+
"status.nextChange": "Próxima mudança",
|
|
69
80
|
"status.unavailable": "O plugin de servidor do DeepSeek Peak está indisponível.",
|
|
70
81
|
"command.status.title": "DeepSeek Peak: Mostrar status",
|
|
71
82
|
"command.status.description": "Mostra o último período de preço observado da DeepSeek",
|
|
@@ -80,6 +91,9 @@ const PT_T: Translation = {
|
|
|
80
91
|
"settings.evidence": "Evidência na barra lateral",
|
|
81
92
|
"settings.frequency": "Frequência do toast",
|
|
82
93
|
"settings.lang": "Idioma",
|
|
94
|
+
"settings.timezone": "Fuso horário",
|
|
95
|
+
"timezone.local": "Local",
|
|
96
|
+
"timezone.utc": "UTC",
|
|
83
97
|
"settings.on": "ativado",
|
|
84
98
|
"settings.off": "desativado",
|
|
85
99
|
"frequency.change": "ao mudar",
|
|
@@ -100,6 +114,8 @@ const ZH_T: Translation = {
|
|
|
100
114
|
"evidence.cached": "(缓存)",
|
|
101
115
|
"evidence.preview": "(尚无请求)",
|
|
102
116
|
"evidence.holiday": "节假日:{name}",
|
|
117
|
+
"evidence.peakEnds": "高峰于 {when} 结束",
|
|
118
|
+
"evidence.peakStarts": "高峰于 {when} 开始",
|
|
103
119
|
"label.mismatch": "不一致",
|
|
104
120
|
"toast.title": "DeepSeek 价格",
|
|
105
121
|
"status.title": "DeepSeek Peak",
|
|
@@ -107,6 +123,8 @@ const ZH_T: Translation = {
|
|
|
107
123
|
"status.noRequest": "此会话尚未观察到 DeepSeek 请求。",
|
|
108
124
|
"status.source": "来源",
|
|
109
125
|
"status.evidence": "证据",
|
|
126
|
+
"status.windows": "高峰时段:01:00–04:00、06:00–10:00 UTC,周一至周五,中国法定节假日除外",
|
|
127
|
+
"status.nextChange": "下次变化",
|
|
110
128
|
"status.unavailable": "DeepSeek Peak 服务端插件不可用。",
|
|
111
129
|
"command.status.title": "DeepSeek Peak:显示状态",
|
|
112
130
|
"command.status.description": "显示最近观察到的 DeepSeek 价格时段",
|
|
@@ -121,6 +139,9 @@ const ZH_T: Translation = {
|
|
|
121
139
|
"settings.evidence": "侧边栏证据",
|
|
122
140
|
"settings.frequency": "通知频率",
|
|
123
141
|
"settings.lang": "语言",
|
|
142
|
+
"settings.timezone": "时区",
|
|
143
|
+
"timezone.local": "本地",
|
|
144
|
+
"timezone.utc": "UTC",
|
|
124
145
|
"settings.on": "开",
|
|
125
146
|
"settings.off": "关",
|
|
126
147
|
"frequency.change": "变化时",
|
|
@@ -141,6 +162,8 @@ const JA_T: Translation = {
|
|
|
141
162
|
"evidence.cached": "(キャッシュ)",
|
|
142
163
|
"evidence.preview": "(まだリクエストなし)",
|
|
143
164
|
"evidence.holiday": "祝日:{name}",
|
|
165
|
+
"evidence.peakEnds": "ピークは {when} に終了",
|
|
166
|
+
"evidence.peakStarts": "ピークは {when} に開始",
|
|
144
167
|
"label.mismatch": "不一致",
|
|
145
168
|
"toast.title": "DeepSeek 料金",
|
|
146
169
|
"status.title": "DeepSeek Peak",
|
|
@@ -148,6 +171,8 @@ const JA_T: Translation = {
|
|
|
148
171
|
"status.noRequest": "このセッションではまだ DeepSeek リクエストがありません。",
|
|
149
172
|
"status.source": "ソース",
|
|
150
173
|
"status.evidence": "エビデンス",
|
|
174
|
+
"status.windows": "ピーク:01:00–04:00、06:00–10:00 UTC、月〜金、中国の祝日を除く",
|
|
175
|
+
"status.nextChange": "次の切り替え",
|
|
151
176
|
"status.unavailable": "DeepSeek Peak サーバープラグインが利用できません。",
|
|
152
177
|
"command.status.title": "DeepSeek Peak:ステータス表示",
|
|
153
178
|
"command.status.description": "最後に観測した DeepSeek 料金期間を表示",
|
|
@@ -162,6 +187,9 @@ const JA_T: Translation = {
|
|
|
162
187
|
"settings.evidence": "サイドバーのエビデンス",
|
|
163
188
|
"settings.frequency": "トースト頻度",
|
|
164
189
|
"settings.lang": "言語",
|
|
190
|
+
"settings.timezone": "タイムゾーン",
|
|
191
|
+
"timezone.local": "ローカル",
|
|
192
|
+
"timezone.utc": "UTC",
|
|
165
193
|
"settings.on": "オン",
|
|
166
194
|
"settings.off": "オフ",
|
|
167
195
|
"frequency.change": "変更時",
|
|
@@ -182,6 +210,8 @@ const KO_T: Translation = {
|
|
|
182
210
|
"evidence.cached": "(캐시)",
|
|
183
211
|
"evidence.preview": "(아직 요청 없음)",
|
|
184
212
|
"evidence.holiday": "공휴일: {name}",
|
|
213
|
+
"evidence.peakEnds": "피크 {when} 종료",
|
|
214
|
+
"evidence.peakStarts": "피크 {when} 시작",
|
|
185
215
|
"label.mismatch": "불일치",
|
|
186
216
|
"toast.title": "DeepSeek 요금",
|
|
187
217
|
"status.title": "DeepSeek Peak",
|
|
@@ -189,6 +219,8 @@ const KO_T: Translation = {
|
|
|
189
219
|
"status.noRequest": "이 세션에는 아직 DeepSeek 요청이 없습니다.",
|
|
190
220
|
"status.source": "출처",
|
|
191
221
|
"status.evidence": "근거",
|
|
222
|
+
"status.windows": "피크: 01:00–04:00, 06:00–10:00 UTC, 월–금, 중국 공휴일 제외",
|
|
223
|
+
"status.nextChange": "다음 전환",
|
|
192
224
|
"status.unavailable": "DeepSeek Peak 서버 플러그인을 사용할 수 없습니다.",
|
|
193
225
|
"command.status.title": "DeepSeek Peak: 상태 표시",
|
|
194
226
|
"command.status.description": "마지막으로 관측된 DeepSeek 요금 기간 표시",
|
|
@@ -203,6 +235,9 @@ const KO_T: Translation = {
|
|
|
203
235
|
"settings.evidence": "사이드바 근거",
|
|
204
236
|
"settings.frequency": "토스트 빈도",
|
|
205
237
|
"settings.lang": "언어",
|
|
238
|
+
"settings.timezone": "시간대",
|
|
239
|
+
"timezone.local": "로컬",
|
|
240
|
+
"timezone.utc": "UTC",
|
|
206
241
|
"settings.on": "켜짐",
|
|
207
242
|
"settings.off": "꺼짐",
|
|
208
243
|
"frequency.change": "변경 시",
|
package/src/index.ts
CHANGED
|
@@ -28,34 +28,41 @@ export default Plugin.define({
|
|
|
28
28
|
const status = statuses.get(sessionID)
|
|
29
29
|
return status ? { found: true, status } : { found: false }
|
|
30
30
|
},
|
|
31
|
-
//
|
|
32
|
-
//
|
|
31
|
+
// Re-synthesizes the current period on every call so an idle TUI can
|
|
32
|
+
// refresh itself without waiting for a request. A recent API header
|
|
33
|
+
// still wins over the official schedule.
|
|
33
34
|
preview: async (input) => {
|
|
34
35
|
const { sessionID } = input as { sessionID: string }
|
|
35
|
-
const observed = statuses.get(sessionID)
|
|
36
|
-
if (observed) return { found: true, status: observed }
|
|
37
36
|
try {
|
|
38
37
|
const session = await context.session.get({ sessionID })
|
|
39
38
|
const model = session.model
|
|
40
39
|
if (!model || !isDeepSeek(model.providerID)) return { found: false }
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const observed = statuses.get(sessionID)
|
|
41
|
+
const now = Date.now()
|
|
42
|
+
const schedule = scheduleInfo(new Date(now))
|
|
43
|
+
const cached = apiSignals.get(modelKey(model.providerID, model.id))
|
|
44
|
+
const api = cached && now - cached.observedAt <= apiSignalTtlMs ? cached : undefined
|
|
45
|
+
const evidence = api
|
|
46
|
+
? `${api.evidence} (cached)`
|
|
47
|
+
: schedule.holiday
|
|
48
|
+
? `Chinese public holiday: ${schedule.holiday}${observed ? "" : " (no request observed yet)"}`
|
|
49
|
+
: observed
|
|
50
|
+
? "DeepSeek official UTC pricing schedule"
|
|
51
|
+
: "Current schedule (no request observed yet)"
|
|
45
52
|
return {
|
|
46
53
|
found: true,
|
|
47
54
|
status: {
|
|
48
55
|
sessionID,
|
|
49
56
|
providerID: model.providerID,
|
|
50
57
|
modelID: model.id,
|
|
51
|
-
period: schedule.period,
|
|
58
|
+
period: api?.period ?? schedule.period,
|
|
52
59
|
scheduledPeriod: schedule.period,
|
|
53
|
-
source: "official-schedule",
|
|
54
|
-
phase: "request",
|
|
55
|
-
mismatch: false,
|
|
56
|
-
observedAt:
|
|
60
|
+
source: api ? "api-header" : "official-schedule",
|
|
61
|
+
phase: observed?.phase ?? "request",
|
|
62
|
+
mismatch: api ? api.period !== schedule.period : false,
|
|
63
|
+
observedAt: now,
|
|
57
64
|
evidence,
|
|
58
|
-
holiday: schedule.holiday,
|
|
65
|
+
...(schedule.holiday !== undefined ? { holiday: schedule.holiday } : {}),
|
|
59
66
|
},
|
|
60
67
|
}
|
|
61
68
|
} catch {
|
|
@@ -93,7 +100,7 @@ export default Plugin.define({
|
|
|
93
100
|
: schedule.holiday
|
|
94
101
|
? `Chinese public holiday: ${schedule.holiday}`
|
|
95
102
|
: "DeepSeek official UTC pricing schedule",
|
|
96
|
-
holiday: schedule.holiday,
|
|
103
|
+
...(schedule.holiday !== undefined ? { holiday: schedule.holiday } : {}),
|
|
97
104
|
})
|
|
98
105
|
})
|
|
99
106
|
|
|
@@ -118,7 +125,7 @@ export default Plugin.define({
|
|
|
118
125
|
mismatch: api.period !== schedule.period,
|
|
119
126
|
observedAt: now,
|
|
120
127
|
evidence: api.evidence,
|
|
121
|
-
holiday: schedule.holiday,
|
|
128
|
+
...(schedule.holiday !== undefined ? { holiday: schedule.holiday } : {}),
|
|
122
129
|
})
|
|
123
130
|
})
|
|
124
131
|
|
package/src/panel/PeakPanel.tsx
CHANGED
|
@@ -17,6 +17,7 @@ export interface IndicatorProps {
|
|
|
17
17
|
|
|
18
18
|
export interface PanelProps extends IndicatorProps {
|
|
19
19
|
evidence: () => boolean
|
|
20
|
+
timeZone: () => string | undefined
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function PeakPanel(props: PanelProps) {
|
|
@@ -51,7 +52,11 @@ export function PeakPanel(props: PanelProps) {
|
|
|
51
52
|
<For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
|
|
52
53
|
</text>
|
|
53
54
|
<Show when={props.evidence() ? props.status() : undefined}>
|
|
54
|
-
{(status) =>
|
|
55
|
+
{(status) => (
|
|
56
|
+
<text wrapMode="none" fg={themeColors(props.context.theme).muted}>
|
|
57
|
+
{evidenceLabel(status(), t, { lang: props.lang(), timeZone: props.timeZone() })}
|
|
58
|
+
</text>
|
|
59
|
+
)}
|
|
55
60
|
</Show>
|
|
56
61
|
</box>
|
|
57
62
|
</Show>
|
package/src/schedule.ts
CHANGED
|
@@ -56,6 +56,45 @@ export function scheduleInfo(
|
|
|
56
56
|
return { period: peakWindow(date) }
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export interface Transition {
|
|
60
|
+
kind: "peakStart" | "peakEnd"
|
|
61
|
+
/** Instant of the next change of the official schedule. */
|
|
62
|
+
at: Date
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function utcAt(date: Date, dayOffset: number, hour: number): Date {
|
|
66
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + dayOffset, hour))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPeakDay(date: Date, holidayName: (value: Date) => string | undefined): boolean {
|
|
70
|
+
const day = date.getUTCDay()
|
|
71
|
+
return day !== 0 && day !== 6 && holidayName(date) === undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Next instant at which the official schedule changes period, considering the
|
|
76
|
+
* two weekday peak windows and China's public holidays.
|
|
77
|
+
*/
|
|
78
|
+
export function nextTransition(
|
|
79
|
+
date: Date,
|
|
80
|
+
holidayName: (value: Date) => string | undefined = chinesePublicHoliday,
|
|
81
|
+
): Transition {
|
|
82
|
+
if (scheduleInfo(date, holidayName).period === "peak") {
|
|
83
|
+
return { kind: "peakEnd", at: utcAt(date, 0, date.getUTCHours() < 4 ? 4 : 10) }
|
|
84
|
+
}
|
|
85
|
+
// Off-peak: the earliest future peak start on a weekday without a holiday.
|
|
86
|
+
// A public holiday can run for several consecutive days, so scan further.
|
|
87
|
+
for (let offset = 0; offset < 15; offset += 1) {
|
|
88
|
+
if (!isPeakDay(utcAt(date, offset, 0), holidayName)) continue
|
|
89
|
+
for (const hour of [1, 6]) {
|
|
90
|
+
const candidate = utcAt(date, offset, hour)
|
|
91
|
+
if (candidate.getTime() > date.getTime()) return { kind: "peakStart", at: candidate }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// date-holidays always yields a working weekday within two weeks; keep a fallback.
|
|
95
|
+
return { kind: "peakStart", at: utcAt(date, 1, 1) }
|
|
96
|
+
}
|
|
97
|
+
|
|
59
98
|
export function officialPricePeriod(
|
|
60
99
|
date: Date,
|
|
61
100
|
holidayCheck: (value: Date) => boolean = isChinesePublicHoliday,
|
package/src/tui.tsx
CHANGED
|
@@ -21,6 +21,8 @@ const mod: Plugin.Definition = {
|
|
|
21
21
|
// Stored settings may predate the `lang` field.
|
|
22
22
|
const lang = () => (settings.lang ?? detectLang()) as LangCode
|
|
23
23
|
const t = createT(lang)
|
|
24
|
+
// Local by default; `utc` pins the official schedule times.
|
|
25
|
+
const timeZone = () => (settings.timezone === "utc" ? "UTC" : undefined)
|
|
24
26
|
const [statuses, setStatuses] = createSignal<Record<string, PeakStatus>>({})
|
|
25
27
|
const lastToast = new Map<string, string>()
|
|
26
28
|
|
|
@@ -40,13 +42,19 @@ const mod: Plugin.Definition = {
|
|
|
40
42
|
})
|
|
41
43
|
|
|
42
44
|
const readStatus = async (sessionID: string) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
// Preview re-synthesizes the current schedule, so an idle session stays
|
|
46
|
+
// fresh; it already folds in a recent API header when one exists.
|
|
45
47
|
try {
|
|
46
48
|
const preview = await client.preview({ sessionID }) as { found: boolean; status?: PeakStatus }
|
|
47
|
-
|
|
48
|
-
} catch {
|
|
49
|
+
if (preview.found && preview.status) return preview.status
|
|
49
50
|
return undefined
|
|
51
|
+
} catch {
|
|
52
|
+
try {
|
|
53
|
+
const result = await client.status({ sessionID }) as { found: boolean; status?: PeakStatus }
|
|
54
|
+
return result.found ? result.status : undefined
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
@@ -60,10 +68,22 @@ const mod: Plugin.Definition = {
|
|
|
60
68
|
.catch(() => {})
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
// Keep the period and the next-change label fresh while the session is idle.
|
|
72
|
+
const timer = setInterval(() => {
|
|
73
|
+
for (const sessionID of Object.keys(statuses())) refresh(sessionID)
|
|
74
|
+
}, 30_000)
|
|
75
|
+
|
|
63
76
|
const unregisterCommands = context.ui.slot({
|
|
64
77
|
append: "app",
|
|
65
78
|
render: () => (
|
|
66
|
-
<CommandRoot
|
|
79
|
+
<CommandRoot
|
|
80
|
+
context={context}
|
|
81
|
+
settings={settings}
|
|
82
|
+
update={update}
|
|
83
|
+
readStatus={readStatus}
|
|
84
|
+
lang={lang}
|
|
85
|
+
timeZone={timeZone}
|
|
86
|
+
/>
|
|
67
87
|
),
|
|
68
88
|
})
|
|
69
89
|
const unregisterSidebar = context.ui.slot({
|
|
@@ -77,6 +97,7 @@ const mod: Plugin.Definition = {
|
|
|
77
97
|
enabled={() => settings.sidebarIndicator !== false}
|
|
78
98
|
evidence={() => settings.sidebarEvidence !== false}
|
|
79
99
|
lang={lang}
|
|
100
|
+
timeZone={timeZone}
|
|
80
101
|
sessionID={sessionID}
|
|
81
102
|
refresh={refresh}
|
|
82
103
|
/>
|
|
@@ -101,6 +122,7 @@ const mod: Plugin.Definition = {
|
|
|
101
122
|
})
|
|
102
123
|
|
|
103
124
|
return () => {
|
|
125
|
+
clearInterval(timer)
|
|
104
126
|
unsubscribe()
|
|
105
127
|
unregisterCommands()
|
|
106
128
|
unregisterSidebar()
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { LangCode } from "./i18n.js"
|
|
|
3
3
|
export type PricePeriod = "peak" | "off-peak"
|
|
4
4
|
export type StatusSource = "official-schedule" | "api-header"
|
|
5
5
|
export type StatusPhase = "request" | "response"
|
|
6
|
+
export type TimezoneMode = "local" | "utc"
|
|
6
7
|
|
|
7
8
|
export interface PeakStatus {
|
|
8
9
|
sessionID: string
|
|
@@ -26,4 +27,5 @@ export interface TuiSettings {
|
|
|
26
27
|
sidebarEvidence: boolean
|
|
27
28
|
toastEveryRequest: boolean
|
|
28
29
|
lang: LangCode
|
|
30
|
+
timezone: TimezoneMode
|
|
29
31
|
}
|