dsh-offpeak-guard 0.1.0 → 0.3.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 +19 -0
- package/README.en.md +82 -10
- package/README.md +85 -11
- package/client.js +221 -4
- package/cordis.patch.yml +15 -3
- package/docs/settings-panel.png +0 -0
- package/index.js +201 -3
- package/lib/guard.js +447 -39
- package/lib/holidays.js +60 -0
- package/lib/peak.js +131 -34
- package/lib/store.js +40 -3
- package/lib/tools.js +78 -14
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-11-06
|
|
4
|
+
|
|
5
|
+
- 新增峰时策略 `pauseMode`:`pause-now` 立即暂停,`finish-turn` 完成当前轮后暂停;设置面板可切换。
|
|
6
|
+
- 子代理感知:会话有正在运行的子代理时先放行,子代理结束后再暂停父会话并记录,谷价开始自动恢复。
|
|
7
|
+
- 精确模型识别:按 `provider/model` 精确匹配,`peakPricedModels` 精确列表 + `exemptModels` 豁免;设置面板可逐模型开关。
|
|
8
|
+
- 新增设置面板(点击左栏计时打开):峰时策略、模型管理、会话豁免、节假日同步状态。
|
|
9
|
+
- 会话豁免:`offpeak_guard exempt/unexempt` 或设置面板,可随时取消。
|
|
10
|
+
- 节假日动态识别:默认从公开节假日 API 同步当前/下一年,缓存到 `holidays.json`,失败回退内置 2026 列表;`holidaySource: builtin` 可完全离线。
|
|
11
|
+
- 新增环境变量:`DSH_OFFPEAK_PAUSE_MODE`、`DSH_OFFPEAK_HOLIDAY_SOURCE`、`DSH_OFFPEAK_HOLIDAY_API`、`DSH_OFFPEAK_PEAK_MODELS`。
|
|
12
|
+
- 状态 schema 扩展:等待中的任务、会话豁免、模型规则、峰时策略;兼容旧 `state.json`。
|
|
13
|
+
|
|
14
|
+
## 0.2.0 — 2026-11-05
|
|
15
|
+
|
|
16
|
+
- 默认规则改为 DeepSeek 官方峰谷:工作日 09:00–12:00、14:00–18:00 峰时,周末与**中国法定节假日全天**谷价;内置 2026 年节假日列表。
|
|
17
|
+
- 新增 `preset`:`auto` / `deepseek-official` / `workbuddy` / `custom`,按 provider 自动选择(WorkBuddy/CodeBuddy → workbuddy,其余 → 官方)。
|
|
18
|
+
- 新增 `Config` schema:DSH 设置页可直接修改时区、窗口、豁免模型、提醒阈值等。
|
|
19
|
+
- 新增 `DSH_OFFPEAK_*` 环境变量覆盖(优先级:环境变量 > 配置 > 默认),便于部署方统一注入。
|
|
20
|
+
- 状态工具与界面显示当前规则和节假日状态。
|
|
21
|
+
|
|
3
22
|
## 0.1.0 — 2026-11-05
|
|
4
23
|
|
|
5
24
|
- 首个公开版本。
|
package/README.en.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# Off-peak guard (dsh-offpeak-guard)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-offpeak-guard)
|
|
4
|
+
|
|
5
|
+
Published on npm: install it from the DSH Plugins page, or run
|
|
6
|
+
`dsh plugin --profile <profile> add dsh-offpeak-guard`.
|
|
7
|
+
|
|
3
8
|
Schedule work around **DeepSeek peak/off-peak pricing** inside DeepSeek Harness:
|
|
4
9
|
|
|
5
|
-
- **Time awareness**: weekdays `09:00–12:00` and `14:00–18:00` are peak
|
|
6
|
-
- **Model awareness**:
|
|
10
|
+
- **Time awareness**: per DeepSeek's official pricing, Beijing-time weekdays (excluding Chinese statutory holidays) `09:00–12:00` and `14:00–18:00` are peak; every other hour (weekends, holidays, lunch, night) is off-peak at 50%. Holidays sync from a public API with a built-in 2026 fallback.
|
|
11
|
+
- **Model awareness**: exact `provider / model` detection; models containing `deepseek` are managed by default, and the settings panel can toggle each model. `peakPricedModels` / `exemptModels` provide exact control.
|
|
12
|
+
- **Peak strategy**: choose immediate pause or finish the current turn first; if a session has running subagents, they are allowed to finish and the parent is paused afterwards, then resumed at off-peak.
|
|
13
|
+
- **Session exemptions**: exempt any session from the guard (settings panel or `offpeak_guard exempt/unexempt`), and un-exempt it at any time.
|
|
7
14
|
- **Work planning**:
|
|
8
15
|
- peak starts → running tasks are paused (the pending inbox is kept, no message is lost);
|
|
9
16
|
- a task started during peak → stopped immediately, no peak-priced tokens spent;
|
|
@@ -21,6 +28,10 @@ Red blink within the alert window before peak (sample):
|
|
|
21
28
|
|
|
22
29
|

|
|
23
30
|
|
|
31
|
+
Click the clock to open the settings panel:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
24
35
|
## Tools
|
|
25
36
|
|
|
26
37
|
| Tool | Purpose |
|
|
@@ -31,16 +42,22 @@ Red blink within the alert window before peak (sample):
|
|
|
31
42
|
|
|
32
43
|
## Configuration
|
|
33
44
|
|
|
34
|
-
The `
|
|
45
|
+
The `offpeak-guard` row's `config` in `cordis.patch.yml`, or the plugin settings form in DSH (a Config schema is declared):
|
|
35
46
|
|
|
36
47
|
| Key | Default | Meaning |
|
|
37
48
|
| --- | --- | --- |
|
|
38
49
|
| `enabled` | `true` | Master switch |
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
50
|
+
| `preset` | `auto` | `auto` picks by provider: WorkBuddy/CodeBuddy → `workbuddy`, otherwise `deepseek-official`; can be pinned to `deepseek-official` / `workbuddy` / `custom` |
|
|
51
|
+
| `pauseMode` | `pause-now` | `pause-now` pauses immediately; `finish-turn` pauses after the current turn |
|
|
52
|
+
| `timezone` | `Asia/Shanghai` | Pricing time zone (DeepSeek bills in Beijing time) |
|
|
53
|
+
| `peakWeekdays` | `[1,2,3,4,5]` | Weekdays with peak windows (0 = Sunday); only used by `custom` |
|
|
54
|
+
| `peakWindows` | 09:00–12:00, 14:00–18:00 | Peak windows; only used by `custom` |
|
|
55
|
+
| `discount` | `0.5` | Off-peak discount, display only; only used by `custom` |
|
|
56
|
+
| `holidays` | `[]` | Extra statutory holidays (`YYYY-MM-DD`) appended to synced/built-in dates |
|
|
57
|
+
| `holidaySource` | `auto` | `auto` syncs from the public holiday API; `builtin` stays offline |
|
|
58
|
+
| `holidayApi` | timor.tech | Dynamic holiday API prefix |
|
|
59
|
+
| `modelPattern` | `deepseek` | Fallback match when no exact model list is set |
|
|
60
|
+
| `peakPricedModels` | `[]` | Exact peak-priced models (`provider/model` or `model`); takes priority over `modelPattern` |
|
|
44
61
|
| `exemptModels` | `[]` | Model ids exempted one by one |
|
|
45
62
|
| `pauseRunningOnPeak` | `true` | Pause running tasks when peak starts |
|
|
46
63
|
| `deferNewTurnsOnPeak` | `true` | Stop tasks started during peak immediately |
|
|
@@ -50,12 +67,62 @@ The `config` of the `offpeak-guard` row in `cordis.patch.yml`:
|
|
|
50
67
|
| `bypassMinutes` | `10` | Grace period after a manual resume |
|
|
51
68
|
| `alertMinutes` | `10` | Blink the sidebar clock within this many minutes of a switch (0 = never) |
|
|
52
69
|
|
|
70
|
+
### Environment variables
|
|
71
|
+
|
|
72
|
+
Deployments can inject settings with environment variables. Priority: **env > config > default**. Invalid values are ignored.
|
|
73
|
+
|
|
74
|
+
| Variable | Example | Config field |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `DSH_OFFPEAK_ENABLED` | `0` / `1` | `enabled` |
|
|
77
|
+
| `DSH_OFFPEAK_PRESET` | `custom` | `preset` |
|
|
78
|
+
| `DSH_OFFPEAK_PAUSE_MODE` | `finish-turn` | `pauseMode` |
|
|
79
|
+
| `DSH_OFFPEAK_TIMEZONE` | `Asia/Shanghai` | `timezone` |
|
|
80
|
+
| `DSH_OFFPEAK_PEAK_WEEKDAYS` | `1,2,3,4,5` | `peakWeekdays` |
|
|
81
|
+
| `DSH_OFFPEAK_PEAK_WINDOWS` | `09:00-12:00,14:00-18:00` | `peakWindows` |
|
|
82
|
+
| `DSH_OFFPEAK_DISCOUNT` | `0.5` | `discount` |
|
|
83
|
+
| `DSH_OFFPEAK_HOLIDAYS` | `2026-10-01,2026-10-02` | `holidays` |
|
|
84
|
+
| `DSH_OFFPEAK_HOLIDAY_SOURCE` | `builtin` | `holidaySource` |
|
|
85
|
+
| `DSH_OFFPEAK_HOLIDAY_API` | `https://.../year` | `holidayApi` |
|
|
86
|
+
| `DSH_OFFPEAK_MODEL_PATTERN` | `deepseek` | `modelPattern` |
|
|
87
|
+
| `DSH_OFFPEAK_PEAK_MODELS` | `deepseek-flash,vendor/model` | `peakPricedModels` |
|
|
88
|
+
| `DSH_OFFPEAK_EXEMPT_MODELS` | `deepseek-v4-pro,deepseek-flash` | `exemptModels` |
|
|
89
|
+
| `DSH_OFFPEAK_PAUSE_RUNNING` | `0` / `1` | `pauseRunningOnPeak` |
|
|
90
|
+
| `DSH_OFFPEAK_DEFER_NEW` | `0` / `1` | `deferNewTurnsOnPeak` |
|
|
91
|
+
| `DSH_OFFPEAK_RESUME` | `0` / `1` | `resumeOnOffPeak` |
|
|
92
|
+
| `DSH_OFFPEAK_POLL_SECONDS` | `15` | `pollSeconds` |
|
|
93
|
+
| `DSH_OFFPEAK_RESUME_DELAY_SECONDS` | `3` | `resumeDelaySeconds` |
|
|
94
|
+
| `DSH_OFFPEAK_BYPASS_MINUTES` | `10` | `bypassMinutes` |
|
|
95
|
+
| `DSH_OFFPEAK_ALERT_MINUTES` | `10` | `alertMinutes` |
|
|
96
|
+
|
|
97
|
+
### Presets
|
|
98
|
+
|
|
99
|
+
- `deepseek-official` (default): official weekday peak windows plus built-in 2026 Chinese holidays (holidays are off-peak all day).
|
|
100
|
+
- `workbuddy`: same peak windows, labeled as WorkBuddy credit pricing; selected automatically for WorkBuddy/CodeBuddy providers.
|
|
101
|
+
- `custom`: use `peakWeekdays` / `peakWindows` / `discount` / `holidays` exactly, for other providers or self-hosted pricing.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## Settings panel
|
|
105
|
+
|
|
106
|
+
Click the sidebar clock:
|
|
107
|
+
|
|
108
|
+
- **Peak strategy**: pause immediately or finish the current turn first;
|
|
109
|
+
- **Models**: provider-grouped catalog with a checkbox per model (persisted as `modelRules`, overriding `modelPattern`);
|
|
110
|
+
- **Session exemptions**: exempt/un-exempt live root sessions;
|
|
111
|
+
- **Holidays**: show the dynamic sync state.
|
|
112
|
+
|
|
113
|
+
## Subagents and waiting
|
|
114
|
+
|
|
115
|
+
- Running subagents are allowed to finish; the parent is paused after the last child ends and resumed off-peak;
|
|
116
|
+
- In `finish-turn` mode the current turn finishes; a queued next turn is stopped and recorded;
|
|
117
|
+
- A finished turn is marked `completed`, so no spurious turn is started at off-peak.
|
|
118
|
+
|
|
53
119
|
## Data
|
|
54
120
|
|
|
55
121
|
State and snapshots live in `$DSH_HOME/offpeak-guard/`:
|
|
56
122
|
|
|
57
|
-
- `state.json`: paused tasks,
|
|
58
|
-
- `status.json`: read-only snapshot for the UI (mirrored to `~/.dsh/offpeak-guard/status.json`)
|
|
123
|
+
- `state.json`: paused/waiting tasks, plans, session exemptions, exact model rules, peak strategy;
|
|
124
|
+
- `status.json`: read-only snapshot for the UI (mirrored to `~/.dsh/offpeak-guard/status.json`);
|
|
125
|
+
- `holidays.json`: cached dynamic holiday dates.
|
|
59
126
|
|
|
60
127
|
Nothing is written into user workspaces.
|
|
61
128
|
|
|
@@ -65,6 +132,11 @@ Pause uses `sessionController.cancel`, which ends only the current turn and keep
|
|
|
65
132
|
|
|
66
133
|
## Boundaries
|
|
67
134
|
|
|
135
|
+
- Dynamic holidays come from a third-party public API (default timor.tech; only the year is sent). Offline failures fall back to the `holidays.json` cache and the built-in 2026 list; set `holidaySource: builtin` for fully offline operation.
|
|
136
|
+
- Built-in statutory holidays cover 2026 only; the dynamic sync fills later years once published, or append dates with `holidays`.
|
|
137
|
+
- Named presets have fixed windows; use `preset: custom` for other providers or self-hosted pricing.
|
|
68
138
|
- Only root sessions with a working directory are handled; subagent sessions are skipped so parents do not hang.
|
|
139
|
+
- Subagent results are consumed by the parent after it resumes; the guard stops the parent at child completion but cannot intercept messages already written into the parent history.
|
|
140
|
+
- A session exemption affects future pauses/resumes only; use `resume_now` to resume an already paused session.
|
|
69
141
|
- A plugin restart does not cancel tasks that are already running (so installing the plugin cannot kill the current conversation); the next peak or new task follows the rules again.
|
|
70
142
|
- Manual `resume_now` grants those sessions a 10-minute grace period so they are not paused again immediately.
|
package/README.md
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# 错峰调度 · 峰谷守卫(dsh-offpeak-guard)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-offpeak-guard)
|
|
4
|
+
|
|
5
|
+
已发布到 npm:在 DSH 的插件页搜索 `dsh-offpeak-guard` 安装,或运行
|
|
6
|
+
`dsh plugin --profile <profile> add dsh-offpeak-guard`。
|
|
7
|
+
|
|
3
8
|
在 DeepSeek Harness 里按 **DeepSeek 峰谷计价** 安排工作:
|
|
4
9
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
10
|
+
- **时间识别**:按 DeepSeek 官方规则,北京时间周一至周五(不含中国法定节假日)`09:00–12:00`、`14:00–18:00` 为峰值原价;其余时间(含周末、法定节假日全天)为谷价,按 5 折计费。节假日默认从公开 API 动态同步,失败时回退内置 2026 列表。
|
|
11
|
+
- **模型识别**:按会话真实 `provider / model` 精确判断;默认名字含 `deepseek` 的模型受管理,可在设置面板逐模型开关,或用 `peakPricedModels` / `exemptModels` 精确控制。
|
|
12
|
+
- **峰时策略**:可选「立即暂停」或「完成当前轮后暂停」;会话有子代理在跑时先放行子代理,子代理结束后再暂停父会话,谷价开始时恢复。
|
|
13
|
+
- **会话豁免**:可让某个会话不参与插件的暂停/恢复(设置面板或 `offpeak_guard exempt/unexempt`),随时取消豁免。
|
|
7
14
|
- **工作规划**:
|
|
8
15
|
- 峰时开始 → 暂停正在运行的任务(保留待处理队列,不丢消息);
|
|
9
16
|
- 峰时新起的任务 → 立即停住,不花峰时 token;
|
|
10
17
|
- 谷价开始 → 自动把被打断的任务交还会话继续执行;
|
|
11
18
|
- 另提供 `offpeak_plan` 工具,把「12 点再跑」这类事排进下一个谷价窗口。
|
|
12
|
-
-
|
|
19
|
+
- **界面**:输入框下方一条状态条,显示当前峰/谷、距下次切换的倒计时、当前模型,以及暂停/规划数量;左侧栏底部另有一枚常驻计时,临近切换时闪色提醒(峰价快开始闪红、谷价快开始闪绿)。**点击左栏计时打开设置面板**:切换峰时策略、逐模型管理、会话豁免、查看节假日同步状态。
|
|
13
20
|
|
|
14
21
|

|
|
15
22
|
|
|
@@ -21,6 +28,10 @@
|
|
|
21
28
|
|
|
22
29
|

|
|
23
30
|
|
|
31
|
+
点击左栏计时打开设置面板:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
24
35
|
## 工具
|
|
25
36
|
|
|
26
37
|
| 工具 | 用途 |
|
|
@@ -31,16 +42,22 @@
|
|
|
31
42
|
|
|
32
43
|
## 配置
|
|
33
44
|
|
|
34
|
-
`cordis.patch.yml` 里那一行 `offpeak-guard` 的 `config
|
|
45
|
+
`cordis.patch.yml` 里那一行 `offpeak-guard` 的 `config`,或在 DSH 设置页的插件配置里修改(已声明 Config schema):
|
|
35
46
|
|
|
36
47
|
| 键 | 默认 | 说明 |
|
|
37
48
|
| --- | --- | --- |
|
|
38
49
|
| `enabled` | `true` | 总开关 |
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
50
|
+
| `preset` | `auto` | `auto` 按 provider 自动选:WorkBuddy/CodeBuddy → `workbuddy`,其余 → `deepseek-official`;也可固定为 `deepseek-official` / `workbuddy` / `custom` |
|
|
51
|
+
| `pauseMode` | `pause-now` | `pause-now` 峰时立即暂停;`finish-turn` 当前轮跑完后再暂停 |
|
|
52
|
+
| `timezone` | `Asia/Shanghai` | 计价时区(DeepSeek 计费按北京时间) |
|
|
53
|
+
| `peakWeekdays` | `[1,2,3,4,5]` | 峰时适用的星期(0=周日);仅 `custom` 预设使用 |
|
|
54
|
+
| `peakWindows` | 09:00–12:00、14:00–18:00 | 峰时窗口;仅 `custom` 预设使用,官方调价时改这里 |
|
|
55
|
+
| `discount` | `0.5` | 谷价折扣,仅用于显示「5 折」;仅 `custom` 预设使用 |
|
|
56
|
+
| `holidays` | `[]` | 追加的法定节假日(`YYYY-MM-DD`),在动态/内置列表上追加 |
|
|
57
|
+
| `holidaySource` | `auto` | `auto` 从公开节假日 API 动态同步;`builtin` 只用内置/配置列表 |
|
|
58
|
+
| `holidayApi` | timor.tech | 动态节假日 API 前缀 |
|
|
59
|
+
| `modelPattern` | `deepseek` | 未配置精确列表时,模型名/供应商名包含它即视为峰谷计价模型 |
|
|
60
|
+
| `peakPricedModels` | `[]` | 精确的峰谷计价模型列表(`provider/model` 或 `model`);非空时优先于 `modelPattern` |
|
|
44
61
|
| `exemptModels` | `[]` | 逐项豁免的模型 id |
|
|
45
62
|
| `pauseRunningOnPeak` | `true` | 峰时开始时暂停正在运行的任务 |
|
|
46
63
|
| `deferNewTurnsOnPeak` | `true` | 峰时新起的任务立即停住 |
|
|
@@ -50,12 +67,64 @@
|
|
|
50
67
|
| `bypassMinutes` | `10` | 手动恢复后的豁免期,避免立刻又被暂停 |
|
|
51
68
|
| `alertMinutes` | `10` | 左栏计时在距切换多少分钟内开始闪烁(0 = 不闪) |
|
|
52
69
|
|
|
70
|
+
### 环境变量
|
|
71
|
+
|
|
72
|
+
部署方可以用环境变量统一注入,优先级:**环境变量 > 配置 > 默认**。无效值会被忽略。
|
|
73
|
+
|
|
74
|
+
| 环境变量 | 例子 | 对应配置 |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `DSH_OFFPEAK_ENABLED` | `0` / `1` | `enabled` |
|
|
77
|
+
| `DSH_OFFPEAK_PRESET` | `custom` | `preset` |
|
|
78
|
+
| `DSH_OFFPEAK_PAUSE_MODE` | `finish-turn` | `pauseMode` |
|
|
79
|
+
| `DSH_OFFPEAK_TIMEZONE` | `Asia/Shanghai` | `timezone` |
|
|
80
|
+
| `DSH_OFFPEAK_PEAK_WEEKDAYS` | `1,2,3,4,5` | `peakWeekdays` |
|
|
81
|
+
| `DSH_OFFPEAK_PEAK_WINDOWS` | `09:00-12:00,14:00-18:00` | `peakWindows` |
|
|
82
|
+
| `DSH_OFFPEAK_DISCOUNT` | `0.5` | `discount` |
|
|
83
|
+
| `DSH_OFFPEAK_HOLIDAYS` | `2026-10-01,2026-10-02` | `holidays` |
|
|
84
|
+
| `DSH_OFFPEAK_HOLIDAY_SOURCE` | `builtin` | `holidaySource` |
|
|
85
|
+
| `DSH_OFFPEAK_HOLIDAY_API` | `https://.../year` | `holidayApi` |
|
|
86
|
+
| `DSH_OFFPEAK_MODEL_PATTERN` | `deepseek` | `modelPattern` |
|
|
87
|
+
| `DSH_OFFPEAK_PEAK_MODELS` | `deepseek-flash,vendor/model` | `peakPricedModels` |
|
|
88
|
+
| `DSH_OFFPEAK_EXEMPT_MODELS` | `deepseek-v4-pro,deepseek-flash` | `exemptModels` |
|
|
89
|
+
| `DSH_OFFPEAK_PAUSE_RUNNING` | `0` / `1` | `pauseRunningOnPeak` |
|
|
90
|
+
| `DSH_OFFPEAK_DEFER_NEW` | `0` / `1` | `deferNewTurnsOnPeak` |
|
|
91
|
+
| `DSH_OFFPEAK_RESUME` | `0` / `1` | `resumeOnOffPeak` |
|
|
92
|
+
| `DSH_OFFPEAK_POLL_SECONDS` | `15` | `pollSeconds` |
|
|
93
|
+
| `DSH_OFFPEAK_RESUME_DELAY_SECONDS` | `3` | `resumeDelaySeconds` |
|
|
94
|
+
| `DSH_OFFPEAK_BYPASS_MINUTES` | `10` | `bypassMinutes` |
|
|
95
|
+
| `DSH_OFFPEAK_ALERT_MINUTES` | `10` | `alertMinutes` |
|
|
96
|
+
|
|
97
|
+
### 预设
|
|
98
|
+
|
|
99
|
+
- `deepseek-official`(默认):官方工作日峰时 + 内置 2026 法定节假日(节假日全天谷价)。
|
|
100
|
+
- `workbuddy`:WorkBuddy 积分规则,峰时窗口与官方相同;按 provider 自动选择时用于标注来源。
|
|
101
|
+
- `custom`:完全使用 `peakWeekdays` / `peakWindows` / `discount` / `holidays`,适合其他供应商或自建计价。
|
|
102
|
+
|
|
103
|
+
## 设置面板
|
|
104
|
+
|
|
105
|
+
点击左栏计时打开:
|
|
106
|
+
|
|
107
|
+
- **峰时策略**:立即暂停 / 完成当前轮后暂停;
|
|
108
|
+
- **模型管理**:按 provider 分组列出模型目录,勾选即精确纳入峰谷管理(写入 `state.json` 的 `modelRules`,优先于 `modelPattern`);
|
|
109
|
+
- **会话豁免**:勾选某个会话后,它不再被暂停/恢复;取消勾选立即恢复管理;
|
|
110
|
+
- **节假日**:显示动态同步状态。
|
|
111
|
+
|
|
112
|
+
面板里的修改立即生效并持久化。
|
|
113
|
+
|
|
114
|
+
## 子代理与等待
|
|
115
|
+
|
|
116
|
+
- 峰时开始时若会话有**正在运行的子代理**,先放行父会话与子代理;
|
|
117
|
+
- 子代理结束后,父会话按当前策略暂停(`waiting-children`),谷价开始后自动恢复;
|
|
118
|
+
- 「完成当前轮后暂停」模式下,当前轮允许跑完;如果随后又排入了下一轮,下一轮会在峰时被拦下并记录,谷价开始恢复;
|
|
119
|
+
- 当前轮跑完且没有后续轮次时,记录标记为 `completed`,不会在谷价时无意义地再起一轮。
|
|
120
|
+
|
|
53
121
|
## 数据
|
|
54
122
|
|
|
55
123
|
状态与快照都在 `$DSH_HOME/offpeak-guard/`:
|
|
56
124
|
|
|
57
|
-
- `state.json
|
|
58
|
-
- `status.json
|
|
125
|
+
- `state.json`:暂停/等待中的任务、错峰规划、会话豁免、精确模型规则、峰时策略;
|
|
126
|
+
- `status.json`:给输入框状态条/设置面板读的只读快照(同时镜像到 `~/.dsh/offpeak-guard/status.json`);
|
|
127
|
+
- `holidays.json`:动态同步的节假日缓存。
|
|
59
128
|
|
|
60
129
|
不写用户工作区。
|
|
61
130
|
|
|
@@ -65,6 +134,11 @@
|
|
|
65
134
|
|
|
66
135
|
## 边界
|
|
67
136
|
|
|
137
|
+
- 动态节假日来自第三方公开 API(默认 timor.tech,只发送年份);离线或接口失败时回退 `holidays.json` 缓存与内置 2026 列表。可设 `holidaySource: builtin` 完全离线。
|
|
138
|
+
- 内置法定节假日目前覆盖 2026 年;2027 年安排公布后动态同步会自动补,或用 `holidays` / `DSH_OFFPEAK_HOLIDAYS` 追加,否则节假日会被当作普通工作日。
|
|
139
|
+
- 命名预设的窗口是固定的;需要其他供应商/自建计价时用 `preset: custom` 配 `peakWindows`。
|
|
68
140
|
- 只处理有工作目录、非子代理的根会话,避免打断子代理让父会话卡住。
|
|
141
|
+
- 子代理结果在父会话恢复后由模型继续消费;插件能在子代理结束时停住父会话,但无法在传输层拦截已经进入父会话历史的消息。
|
|
142
|
+
- 「会话豁免」只影响后续暂停/恢复,不会自动把已经暂停的任务立即恢复;需要时用 `resume_now`。
|
|
69
143
|
- 插件自身重启时不会主动打断正在跑的任务(避免刚安装就停掉当前对话);之后进入峰时或新起任务仍按规则处理。
|
|
70
144
|
- 手动「立即恢复」会给这些会话 10 分钟豁免,避免峰时刚恢复又被停回去。
|
package/client.js
CHANGED
|
@@ -149,7 +149,7 @@ window.__ModuleLoader__.load({
|
|
|
149
149
|
};
|
|
150
150
|
}, [load]);
|
|
151
151
|
|
|
152
|
-
return { snapshot, error };
|
|
152
|
+
return { snapshot, error, refresh: load };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
function makeStrip(pluginCtx) {
|
|
@@ -266,7 +266,7 @@ window.__ModuleLoader__.load({
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
const clockStyle = `
|
|
269
|
-
.opg-clock{box-sizing:border-box;display:inline-flex;flex:0 0 auto;align-items:center;gap:5px;width:auto;max-width:100%;min-height:32px;margin-left:auto;padding:4px 8px;border-radius:8px;color:var(--dsw-alias-label-primary,#252936);font:500 12px/18px var(--dsw-font-family,system-ui,sans-serif);white-space:nowrap;-webkit-app-region:no-drag;}
|
|
269
|
+
.opg-clock{box-sizing:border-box;display:inline-flex;flex:0 0 auto;align-items:center;gap:5px;width:auto;max-width:100%;min-height:32px;margin-left:auto;padding:4px 8px;border-radius:8px;color:var(--dsw-alias-label-primary,#252936);font:500 12px/18px var(--dsw-font-family,system-ui,sans-serif);white-space:nowrap;-webkit-app-region:no-drag;cursor:pointer;}
|
|
270
270
|
.opg-clock[data-compact="true"]{width:32px;min-height:32px;justify-content:center;padding:6px;gap:0;}
|
|
271
271
|
.opg-clock-dot{flex:none;width:7px;height:7px;border-radius:50%;background:currentColor;}
|
|
272
272
|
.opg-clock-state{flex:none;}
|
|
@@ -278,6 +278,27 @@ window.__ModuleLoader__.load({
|
|
|
278
278
|
@keyframes opg-blink-red{0%,100%{color:#e0554e;opacity:1;}50%{color:#ff8f8a;opacity:.4;}}
|
|
279
279
|
@keyframes opg-blink-green{0%,100%{color:#3f9d7a;opacity:1;}50%{color:#69d3aa;opacity:.4;}}
|
|
280
280
|
@media (prefers-reduced-motion: reduce){.opg-clock[data-blink]{animation:none;}}
|
|
281
|
+
.opg-panel{border:none;border-radius:14px;padding:0;width:min(600px,92vw);max-height:84vh;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#252936);box-shadow:0 24px 80px #0006;font:13px/1.6 var(--dsw-font-family,system-ui,sans-serif);}
|
|
282
|
+
.opg-panel::backdrop{background:#0006;}
|
|
283
|
+
.opg-panel-shell{display:flex;flex-direction:column;max-height:84vh;}
|
|
284
|
+
.opg-panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l1,#dde0e6);}
|
|
285
|
+
.opg-panel-head strong{font-size:15px;}
|
|
286
|
+
.opg-panel-close{border:1px solid var(--dsw-alias-border-l1,#dde0e6);background:transparent;color:inherit;border-radius:8px;padding:4px 10px;cursor:pointer;font:inherit;}
|
|
287
|
+
.opg-panel-body{padding:16px 20px;overflow:auto;}
|
|
288
|
+
.opg-panel-meta{color:var(--dsw-alias-label-secondary,#6c7769);font-size:12px;margin-bottom:14px;}
|
|
289
|
+
.opg-section{margin-bottom:18px;}
|
|
290
|
+
.opg-section h4{margin:0 0 8px;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#6c7769);}
|
|
291
|
+
.opg-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--dsw-alias-border-l1,#eef0f3);min-width:0;}
|
|
292
|
+
.opg-row:first-of-type{border-top:0;}
|
|
293
|
+
.opg-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
294
|
+
.opg-row-sub{color:var(--dsw-alias-label-secondary,#8a8d94);font-size:11px;margin-left:4px;}
|
|
295
|
+
.opg-group-name{font-size:11px;color:var(--dsw-alias-label-secondary,#8a8d94);margin:8px 0 2px;}
|
|
296
|
+
.opg-check{width:15px;height:15px;accent-color:var(--dsw-alias-state-business-primary,#4d6bfe);flex:none;}
|
|
297
|
+
.opg-seg{display:inline-flex;border:1px solid var(--dsw-alias-border-l1,#dde0e6);border-radius:8px;overflow:hidden;}
|
|
298
|
+
.opg-seg button{border:0;background:transparent;color:inherit;padding:5px 10px;font:inherit;cursor:pointer;}
|
|
299
|
+
.opg-seg button[data-active="true"]{background:var(--dsw-alias-state-business-primary,#4d6bfe);color:#fff;}
|
|
300
|
+
.opg-note{color:var(--dsw-alias-label-secondary,#8a8d94);font-size:11px;line-height:1.6;}
|
|
301
|
+
.opg-error{color:#e0554e;font-size:12px;margin-top:8px;}
|
|
281
302
|
`;
|
|
282
303
|
|
|
283
304
|
function ClockIcon() {
|
|
@@ -299,15 +320,188 @@ window.__ModuleLoader__.load({
|
|
|
299
320
|
);
|
|
300
321
|
}
|
|
301
322
|
|
|
323
|
+
function useCatalog(ctx, enabled) {
|
|
324
|
+
const [groups, setGroups] = React.useState([]);
|
|
325
|
+
React.useEffect(() => {
|
|
326
|
+
if (!enabled) return undefined;
|
|
327
|
+
let alive = true;
|
|
328
|
+
(async () => {
|
|
329
|
+
try {
|
|
330
|
+
const result = await ctx.remote.session.modelCatalog();
|
|
331
|
+
const list = result?.value?.groups ?? result?.groups ?? [];
|
|
332
|
+
if (alive && Array.isArray(list)) setGroups(list);
|
|
333
|
+
} catch {
|
|
334
|
+
/* 目录不可用时留空 */
|
|
335
|
+
}
|
|
336
|
+
})();
|
|
337
|
+
return () => {
|
|
338
|
+
alive = false;
|
|
339
|
+
};
|
|
340
|
+
}, [enabled]);
|
|
341
|
+
return groups;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function SettingsPanel({ ctx, snapshot, refresh, groups, onClose }) {
|
|
345
|
+
const [busy, setBusy] = React.useState('');
|
|
346
|
+
const [error, setError] = React.useState('');
|
|
347
|
+
const post = async (body) => {
|
|
348
|
+
setBusy(`${body.action}:${body.sessionId ?? body.provider ?? body.mode ?? ''}`);
|
|
349
|
+
setError('');
|
|
350
|
+
try {
|
|
351
|
+
const response = await fetch('/plugins/dsh-offpeak-guard/action', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers: { 'content-type': 'application/json' },
|
|
354
|
+
body: JSON.stringify(body),
|
|
355
|
+
});
|
|
356
|
+
const json = await response.json();
|
|
357
|
+
if (!response.ok || json.ok === false) throw new Error(json.error ?? `HTTP ${response.status}`);
|
|
358
|
+
await refresh();
|
|
359
|
+
} catch (caught) {
|
|
360
|
+
setError(String(caught?.message ?? caught));
|
|
361
|
+
} finally {
|
|
362
|
+
setBusy('');
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const modelManaged = (provider, model) => {
|
|
366
|
+
const key = `${provider}/${model}`;
|
|
367
|
+
if (Object.prototype.hasOwnProperty.call(snapshot.modelRules ?? {}, key)) return snapshot.modelRules[key] === true;
|
|
368
|
+
if ((snapshot.peakPricedModels ?? []).length > 0) {
|
|
369
|
+
return snapshot.peakPricedModels.includes(key) || snapshot.peakPricedModels.includes(model);
|
|
370
|
+
}
|
|
371
|
+
if ((snapshot.exemptModels ?? []).includes(model)) return false;
|
|
372
|
+
const needle = String(snapshot.modelPattern ?? 'deepseek').toLowerCase();
|
|
373
|
+
return model.toLowerCase().includes(needle) || provider.toLowerCase().includes(needle);
|
|
374
|
+
};
|
|
375
|
+
const sessions = Object.values(snapshot.sessions ?? {});
|
|
376
|
+
return h('div', { className: 'opg-panel-shell' }, [
|
|
377
|
+
h('div', { className: 'opg-panel-head', key: 'head' }, [
|
|
378
|
+
h('strong', { key: 'title' }, '峰谷守卫设置'),
|
|
379
|
+
h('button', { key: 'close', className: 'opg-panel-close', onClick: onClose }, '关闭'),
|
|
380
|
+
]),
|
|
381
|
+
h('div', { className: 'opg-panel-body', key: 'body' }, [
|
|
382
|
+
h(
|
|
383
|
+
'div',
|
|
384
|
+
{ className: 'opg-panel-meta', key: 'meta' },
|
|
385
|
+
`${snapshot.presetLabel ?? ''} · ${snapshot.phaseLabel ?? ''} · ${snapshot.holiday ? '今天法定节假日(全天谷价)' : '按工作日峰时'}`,
|
|
386
|
+
),
|
|
387
|
+
h('div', { className: 'opg-section', key: 'mode' }, [
|
|
388
|
+
h('h4', { key: 'h' }, '峰时策略'),
|
|
389
|
+
h('div', { className: 'opg-seg' }, [
|
|
390
|
+
h(
|
|
391
|
+
'button',
|
|
392
|
+
{
|
|
393
|
+
key: 'now',
|
|
394
|
+
'data-active': snapshot.pauseMode !== 'finish-turn' ? 'true' : 'false',
|
|
395
|
+
disabled: busy !== '',
|
|
396
|
+
onClick: () => post({ action: 'pauseMode', mode: 'pause-now' }),
|
|
397
|
+
},
|
|
398
|
+
'立即暂停',
|
|
399
|
+
),
|
|
400
|
+
h(
|
|
401
|
+
'button',
|
|
402
|
+
{
|
|
403
|
+
key: 'finish',
|
|
404
|
+
'data-active': snapshot.pauseMode === 'finish-turn' ? 'true' : 'false',
|
|
405
|
+
disabled: busy !== '',
|
|
406
|
+
onClick: () => post({ action: 'pauseMode', mode: 'finish-turn' }),
|
|
407
|
+
},
|
|
408
|
+
'完成当前轮后暂停',
|
|
409
|
+
),
|
|
410
|
+
]),
|
|
411
|
+
h('div', { className: 'opg-note', key: 'note' }, '有子代理在运行时,会先放行子代理;子代理结束后再暂停父会话。'),
|
|
412
|
+
]),
|
|
413
|
+
h('div', { className: 'opg-section', key: 'models' }, [
|
|
414
|
+
h('h4', { key: 'h' }, '模型管理(精确到 provider/model)'),
|
|
415
|
+
groups.length === 0
|
|
416
|
+
? h('div', { className: 'opg-note', key: 'empty' }, '模型目录暂不可用;仍可在配置里用 modelPattern / exemptModels 管理。')
|
|
417
|
+
: groups.map((group) =>
|
|
418
|
+
h('div', { key: `g:${group.id}` }, [
|
|
419
|
+
h('div', { className: 'opg-group-name', key: 'name' }, group.name ?? group.id),
|
|
420
|
+
...(Array.isArray(group.models) ? group.models : []).map((model) =>
|
|
421
|
+
h('label', { className: 'opg-row', key: `m:${group.id}/${model.id}` }, [
|
|
422
|
+
h('input', {
|
|
423
|
+
className: 'opg-check',
|
|
424
|
+
type: 'checkbox',
|
|
425
|
+
checked: modelManaged(String(group.id), String(model.id)),
|
|
426
|
+
disabled: busy !== '',
|
|
427
|
+
onChange: (event) =>
|
|
428
|
+
post({
|
|
429
|
+
action: 'model',
|
|
430
|
+
provider: String(group.id),
|
|
431
|
+
model: String(model.id),
|
|
432
|
+
peakPriced: event.target.checked,
|
|
433
|
+
}),
|
|
434
|
+
}),
|
|
435
|
+
h('span', { className: 'opg-row-label' }, [
|
|
436
|
+
model.name ?? model.id,
|
|
437
|
+
h('span', { className: 'opg-row-sub', key: 'id' }, model.id),
|
|
438
|
+
]),
|
|
439
|
+
]),
|
|
440
|
+
),
|
|
441
|
+
]),
|
|
442
|
+
),
|
|
443
|
+
]),
|
|
444
|
+
h('div', { className: 'opg-section', key: 'sessions' }, [
|
|
445
|
+
h('h4', { key: 'h' }, '会话豁免(不参与暂停/恢复)'),
|
|
446
|
+
sessions.length === 0
|
|
447
|
+
? h('div', { className: 'opg-note', key: 'empty' }, '当前没有运行中的根会话。')
|
|
448
|
+
: sessions.map((session) =>
|
|
449
|
+
h('label', { className: 'opg-row', key: `s:${session.id}` }, [
|
|
450
|
+
h('input', {
|
|
451
|
+
className: 'opg-check',
|
|
452
|
+
type: 'checkbox',
|
|
453
|
+
checked: session.exempt === true,
|
|
454
|
+
disabled: busy !== '',
|
|
455
|
+
onChange: (event) =>
|
|
456
|
+
post({ action: 'exempt', sessionId: String(session.id), exempt: event.target.checked }),
|
|
457
|
+
}),
|
|
458
|
+
h('span', { className: 'opg-row-label' }, [
|
|
459
|
+
session.title ?? session.id,
|
|
460
|
+
h('span', { className: 'opg-row-sub', key: 'model' }, session.id),
|
|
461
|
+
]),
|
|
462
|
+
]),
|
|
463
|
+
),
|
|
464
|
+
]),
|
|
465
|
+
h('div', { className: 'opg-section', key: 'holidays' }, [
|
|
466
|
+
h('h4', { key: 'h' }, '节假日'),
|
|
467
|
+
h(
|
|
468
|
+
'div',
|
|
469
|
+
{ className: 'opg-note', key: 'note' },
|
|
470
|
+
snapshot.holidaySource === 'auto'
|
|
471
|
+
? `动态识别:已同步(${snapshot.holidaySyncedAt ? new Date(snapshot.holidaySyncedAt).toLocaleString() : '尚未同步'}),失败时回退内置 2026 列表。`
|
|
472
|
+
: '使用内置节假日(可在配置 holidays 追加)。',
|
|
473
|
+
),
|
|
474
|
+
]),
|
|
475
|
+
error === '' ? null : h('div', { className: 'opg-error', key: 'error' }, error),
|
|
476
|
+
]),
|
|
477
|
+
]);
|
|
478
|
+
}
|
|
479
|
+
|
|
302
480
|
function makeClock(pluginCtx) {
|
|
303
481
|
return function Clock(props) {
|
|
304
482
|
const t = useLocale(pluginCtx);
|
|
305
|
-
const { snapshot } = useSnapshot(pluginCtx, null);
|
|
483
|
+
const { snapshot, refresh } = useSnapshot(pluginCtx, null);
|
|
484
|
+
const [open, setOpen] = React.useState(false);
|
|
485
|
+
const dialogRef = React.useRef(null);
|
|
486
|
+
const groups = useCatalog(pluginCtx, open);
|
|
306
487
|
const [, force] = React.useState(0);
|
|
307
488
|
React.useEffect(() => {
|
|
308
489
|
const timer = setInterval(() => force((value) => value + 1), 1000);
|
|
309
490
|
return () => clearInterval(timer);
|
|
310
491
|
}, []);
|
|
492
|
+
React.useEffect(() => {
|
|
493
|
+
const dialog = dialogRef.current;
|
|
494
|
+
if (dialog === null) return;
|
|
495
|
+
if (open && !dialog.open) dialog.showModal();
|
|
496
|
+
if (!open && dialog.open) dialog.close();
|
|
497
|
+
}, [open]);
|
|
498
|
+
React.useEffect(() => {
|
|
499
|
+
const dialog = dialogRef.current;
|
|
500
|
+
if (dialog === null) return undefined;
|
|
501
|
+
const onClose = () => setOpen(false);
|
|
502
|
+
dialog.addEventListener('close', onClose);
|
|
503
|
+
return () => dialog.removeEventListener('close', onClose);
|
|
504
|
+
}, []);
|
|
311
505
|
if (snapshot === null) return null;
|
|
312
506
|
|
|
313
507
|
const phase = snapshot.phase === 'peak' ? 'peak' : 'offpeak';
|
|
@@ -335,6 +529,7 @@ window.__ModuleLoader__.load({
|
|
|
335
529
|
t('offpeakNote'),
|
|
336
530
|
`${t('model')}: ${modelId || '—'}`,
|
|
337
531
|
t('clockHint'),
|
|
532
|
+
'点击打开设置 / Click to open settings',
|
|
338
533
|
]
|
|
339
534
|
.filter(Boolean)
|
|
340
535
|
.join('\n');
|
|
@@ -347,7 +542,7 @@ window.__ModuleLoader__.load({
|
|
|
347
542
|
]
|
|
348
543
|
: [ClockIcon()];
|
|
349
544
|
|
|
350
|
-
|
|
545
|
+
const clock = h(
|
|
351
546
|
'div',
|
|
352
547
|
{
|
|
353
548
|
className: 'opg-clock',
|
|
@@ -357,9 +552,31 @@ window.__ModuleLoader__.load({
|
|
|
357
552
|
'data-tone': tone,
|
|
358
553
|
'data-blink': blink ?? undefined,
|
|
359
554
|
'data-compact': wide ? 'false' : 'true',
|
|
555
|
+
onClick: () => setOpen(true),
|
|
360
556
|
},
|
|
361
557
|
[h('style', { key: 'style' }, clockStyle)].concat(inner),
|
|
362
558
|
);
|
|
559
|
+
const dialog = h(
|
|
560
|
+
'dialog',
|
|
561
|
+
{
|
|
562
|
+
key: 'dialog',
|
|
563
|
+
ref: dialogRef,
|
|
564
|
+
className: 'opg-panel',
|
|
565
|
+
onClick: (event) => {
|
|
566
|
+
if (event.target === event.currentTarget) setOpen(false);
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
open
|
|
570
|
+
? h(SettingsPanel, {
|
|
571
|
+
ctx: pluginCtx,
|
|
572
|
+
snapshot,
|
|
573
|
+
refresh,
|
|
574
|
+
groups,
|
|
575
|
+
onClose: () => setOpen(false),
|
|
576
|
+
})
|
|
577
|
+
: null,
|
|
578
|
+
);
|
|
579
|
+
return h(React.Fragment, null, [clock, dialog]);
|
|
363
580
|
};
|
|
364
581
|
}
|
|
365
582
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# dsh-offpeak-guard:DeepSeek 峰谷计价守卫。
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# 默认按 DeepSeek 官方「模型 & 价格」规则:
|
|
3
|
+
# 北京时间周一至周五(不含中国法定节假日)09:00–12:00、14:00–18:00 为高峰;
|
|
4
|
+
# 其余时段(含周末、法定节假日全天)为空闲,空闲价为高峰价的一半。
|
|
5
|
+
# preset:
|
|
6
|
+
# auto 按 provider 自动选(WorkBuddy/CodeBuddy 走 workbuddy,其余走 deepseek-official)
|
|
7
|
+
# deepseek-official 官方规则(含内置 2026 法定节假日)
|
|
8
|
+
# workbuddy WorkBuddy 积分规则(时段相同)
|
|
9
|
+
# custom 用下面 peakWeekdays / peakWindows / discount / holidays
|
|
10
|
+
# 环境变量(部署方注入,优先级高于本文件):DSH_OFFPEAK_*,详见 README。
|
|
5
11
|
- insert:
|
|
6
12
|
- id: offpeak-guard
|
|
7
13
|
name: 'dsh-offpeak-guard'
|
|
8
14
|
config:
|
|
9
15
|
enabled: true
|
|
16
|
+
preset: 'auto'
|
|
17
|
+
pauseMode: 'pause-now'
|
|
10
18
|
timezone: 'Asia/Shanghai'
|
|
11
19
|
peakWeekdays: [1, 2, 3, 4, 5]
|
|
12
20
|
peakWindows:
|
|
@@ -15,7 +23,11 @@
|
|
|
15
23
|
- start: '14:00'
|
|
16
24
|
end: '18:00'
|
|
17
25
|
discount: 0.5
|
|
26
|
+
holidays: []
|
|
27
|
+
holidaySource: 'auto'
|
|
28
|
+
holidayApi: 'https://timor.tech/api/holiday/year'
|
|
18
29
|
modelPattern: 'deepseek'
|
|
30
|
+
peakPricedModels: []
|
|
19
31
|
exemptModels: []
|
|
20
32
|
pauseRunningOnPeak: true
|
|
21
33
|
deferNewTurnsOnPeak: true
|
|
Binary file
|