quota-keepalive 0.1.1 → 0.1.3
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.en.md +284 -0
- package/README.md +94 -11
- package/index.js +97 -20
- package/package.json +3 -3
- package/src/config.js +10 -0
- package/src/keepalive.js +30 -6
- package/src/log.js +1 -0
- package/src/provider-chat.js +28 -8
- package/src/schedule.js +93 -0
- package/src/snapshot.js +2 -1
- package/src/state.js +15 -2
- package/src/workbuddy.js +80 -23
- package/ui/app.js +5 -1
- package/ui/index.html +11 -0
- package/ui/style.css +1 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# quota-keepalive
|
|
4
|
+
|
|
5
|
+
Multi-provider quota check-in, real-chat keepalive, run history, and quota trends for Magpie.
|
|
6
|
+
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://www.npmjs.com/package/quota-keepalive)
|
|
9
|
+
[](https://github.com/sljdxde/quota-keepalive)
|
|
10
|
+
[](#unofficial-plugin-notice)
|
|
11
|
+
|
|
12
|
+
[中文](./README.md) · English
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
quota-keepalive adds one provider-neutral page to Magpie for inspecting signed-in
|
|
19
|
+
providers, running check-ins, sending configurable real-chat keepalive requests,
|
|
20
|
+
reviewing run history, and watching quota changes.
|
|
21
|
+
|
|
22
|
+
It does not take over provider sign-in. Magpie and the installed provider plugins
|
|
23
|
+
keep owning credentials, model lists, quotas, and the provider's own `auth.checkin`
|
|
24
|
+
hook. This plugin only coordinates those capabilities and presents the results.
|
|
25
|
+
|
|
26
|
+
## Unofficial plugin notice
|
|
27
|
+
|
|
28
|
+
> This project is not an official Magpie plugin.
|
|
29
|
+
>
|
|
30
|
+
> Check-in uses the provider plugin's public `auth.checkin` hook whenever possible.
|
|
31
|
+
> Real-chat keepalive is not a provider-authorized or guaranteed feature. Keepalive
|
|
32
|
+
> requests use the signed provider adapter on your machine and are sent directly to
|
|
33
|
+
> the vendor, with no third-party relay and no token export. That reduces credential
|
|
34
|
+
> exposure, but it does not make vendor-risk zero.
|
|
35
|
+
>
|
|
36
|
+
> Automated synthetic messages may be classified as abnormal traffic and can lead to
|
|
37
|
+
> rate limits, credit freezes, account restrictions, or bans. Keepalive is disabled by
|
|
38
|
+
> default. Enable it only if you understand the risk and accept the consequences.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- Detect every signed-in Magpie provider, account, model, and quota window.
|
|
43
|
+
- Run check-in through each provider plugin's public `auth.checkin` hook.
|
|
44
|
+
- Respect provider-level and account-level switches; unsupported or signed-out
|
|
45
|
+
accounts are reported explicitly instead of being silently skipped.
|
|
46
|
+
- Send real-chat keepalive through each provider's signed adapter.
|
|
47
|
+
- Use short probes by default. Long-context probes are opt-in and respect
|
|
48
|
+
`inputTokens`.
|
|
49
|
+
- Prefer models that recently completed a real keepalive successfully, then fall
|
|
50
|
+
back by success rate, recency, and known failures. Provider model order is used
|
|
51
|
+
when there is no history.
|
|
52
|
+
- Search and select a model per provider. If the selected model is unavailable,
|
|
53
|
+
the runner falls back to another model available to that account.
|
|
54
|
+
- Schedule check-in, keepalive, or both from every 5 minutes to every 7 days.
|
|
55
|
+
- Persist the next scheduled run so an overdue task can catch up after a Magpie
|
|
56
|
+
restart or computer sleep.
|
|
57
|
+
- Show today's result, a five-day account history, run details, and quota trends.
|
|
58
|
+
- Keep refresh-only events out of the check-in/keepalive run list.
|
|
59
|
+
- Refresh quotas every minute for enabled providers, and support per-provider or
|
|
60
|
+
refresh-all actions.
|
|
61
|
+
- Keep only hashed account tags, sanitized errors, model names, and status fields in
|
|
62
|
+
logs. Tokens, full account identifiers, enterprise identifiers, and model replies
|
|
63
|
+
are never stored.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
Install from npm. This is the recommended path and supports Magpie update checks:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
magpie plugin add quota-keepalive
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Magpie checks installed npm/git plugins shortly after startup and then every hour.
|
|
74
|
+
When a newer version is available, Plugins shows an update badge and an update action.
|
|
75
|
+
|
|
76
|
+
Update every npm/git plugin:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
magpie plugin update
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Update only this plugin:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
magpie plugin add quota-keepalive
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Re-adding the same npm package replaces the old version while preserving plugin
|
|
89
|
+
options, provider preferences, and enabled/disabled state. Updating also unpins an
|
|
90
|
+
older `quota-keepalive@x.y.z` spec.
|
|
91
|
+
|
|
92
|
+
Install from GitHub without npm:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
magpie plugin add github:sljdxde/quota-keepalive
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Install from a local folder only for development:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
magpie plugin add "$(pwd)"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A folder plugin loads the working tree directly and is not included in Magpie's
|
|
105
|
+
version checks or `magpie plugin update`. Switch back to npm before shipping.
|
|
106
|
+
|
|
107
|
+
After installation, open **Plugins → Quota Keepalive** in Magpie. Reopen the page
|
|
108
|
+
after a UI-only update; restarting all of Magpie is not required.
|
|
109
|
+
|
|
110
|
+
## Quick start
|
|
111
|
+
|
|
112
|
+
1. Open the **Quota Keepalive** page.
|
|
113
|
+
2. Click **Detect** to load signed-in providers, accounts, models, and quotas.
|
|
114
|
+
3. Keep provider and account automatic switches in the desired state.
|
|
115
|
+
4. Use **Check in all** or **Send keepalive** for one manual run.
|
|
116
|
+
5. Configure **Schedule** if you want automatic execution.
|
|
117
|
+
6. Review **Recent runs**, account history, and quota changes.
|
|
118
|
+
|
|
119
|
+
Keepalive sends a real provider request and may consume credits. The plugin does not
|
|
120
|
+
enable automatic keepalive by default.
|
|
121
|
+
|
|
122
|
+
## Page controls
|
|
123
|
+
|
|
124
|
+
- **Detect**: refresh the provider/account/model/quota snapshot.
|
|
125
|
+
- **Check in all**: run check-in for every eligible provider.
|
|
126
|
+
- **Send keepalive**: confirm and send one keepalive request to every eligible account.
|
|
127
|
+
- Provider switch: enable or disable all automatic tasks for that provider.
|
|
128
|
+
- Account **Check-in** / **Keepalive** switches: control automatic participation only;
|
|
129
|
+
they do not send a request immediately.
|
|
130
|
+
- Account **Run check-in** / **Run keepalive** buttons: execute one account now.
|
|
131
|
+
Manual keepalive asks for confirmation and still requires the provider to be enabled.
|
|
132
|
+
- Circular refresh: refresh all enabled providers; each provider row also has a
|
|
133
|
+
provider-only refresh button.
|
|
134
|
+
- **Save schedule**: set the shared interval and action type.
|
|
135
|
+
- **Short probe / Long context**: choose the request shape used by keepalive.
|
|
136
|
+
|
|
137
|
+
Saving a schedule starts the next run after the configured interval. The next run
|
|
138
|
+
time is persisted, and a task already due when Magpie starts is run immediately.
|
|
139
|
+
|
|
140
|
+
## Scheduling and failures
|
|
141
|
+
|
|
142
|
+
Magpie's native daily check-in scheduler still runs independently. This plugin adds
|
|
143
|
+
an optional shared scheduler for more frequent check-in or keepalive.
|
|
144
|
+
|
|
145
|
+
- Default: disabled.
|
|
146
|
+
- Interval: 5 minutes to 7 days.
|
|
147
|
+
- Actions: `checkin`, `keepalive`, or `all`.
|
|
148
|
+
- A successful check-in or keepalive for the same account and Beijing date is
|
|
149
|
+
idempotent and will not be sent again.
|
|
150
|
+
- Keepalive retries are bounded by `maxAttempts`.
|
|
151
|
+
- HTTP 402/429 credit exhaustion stops automatic retry for that day.
|
|
152
|
+
- Manual actions can deliberately retry a failed or quota-exhausted account.
|
|
153
|
+
- Network errors, expired sign-in, unavailable models, empty replies, and provider
|
|
154
|
+
business errors are recorded with sanitized details.
|
|
155
|
+
|
|
156
|
+
## Model availability
|
|
157
|
+
|
|
158
|
+
Keepalive does not blindly choose the cheapest or a free model:
|
|
159
|
+
|
|
160
|
+
1. Intersect the provider's model list with the account's allowed models.
|
|
161
|
+
2. Prefer a model that recently completed a real keepalive successfully.
|
|
162
|
+
3. If the account has no history, prefer a model another account of the same
|
|
163
|
+
provider used successfully.
|
|
164
|
+
4. If there is still no history, follow the provider's model order.
|
|
165
|
+
5. Put known failures after unverified candidates.
|
|
166
|
+
6. Fall back to the next candidate on a missing model, credit rejection, upstream
|
|
167
|
+
outage, or empty reply.
|
|
168
|
+
|
|
169
|
+
For WorkBuddy CN, the keepalive path follows dsh probe semantics: an HTTP 2xx means
|
|
170
|
+
the upstream accepted and recorded a real chat, even when a one-token response only
|
|
171
|
+
contains `[DONE]`, usage data, or an empty visible body. JSON business errors, HTML
|
|
172
|
+
login pages, and timeouts are failures.
|
|
173
|
+
|
|
174
|
+
## Configuration
|
|
175
|
+
|
|
176
|
+
The page writes plugin-level options. The same values can be changed from the CLI:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
magpie plugin options quota-keepalive '{
|
|
180
|
+
"scheduleEnabled": true,
|
|
181
|
+
"scheduleMinutes": 60,
|
|
182
|
+
"scheduleAction": "all",
|
|
183
|
+
"modelPreferences": {
|
|
184
|
+
"workbuddy": "deepseek-v4.1-flash",
|
|
185
|
+
"codearts": "deepseek-v4.1-flash"
|
|
186
|
+
},
|
|
187
|
+
"modelCandidates": 4,
|
|
188
|
+
"probeMode": "short",
|
|
189
|
+
"probeMessage": "Magpie daily connectivity check. Reply with OK.",
|
|
190
|
+
"inputTokens": 25000,
|
|
191
|
+
"maxAttempts": 3,
|
|
192
|
+
"timeoutMs": 90000,
|
|
193
|
+
"logMaxBytes": 2097152
|
|
194
|
+
}'
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- `scheduleEnabled`: enable the plugin scheduler. Default `false`.
|
|
198
|
+
- `scheduleMinutes`: shared interval, default `60`, range 5 minutes to 7 days.
|
|
199
|
+
- `scheduleAction`: `checkin`, `keepalive`, or `all`. Default `checkin`.
|
|
200
|
+
- `model`: optional preferred model; it is not a hard allowlist.
|
|
201
|
+
- `modelPreferences`: preferred model per provider.
|
|
202
|
+
- `modelCandidates`: maximum fallback candidates per account, default `4`.
|
|
203
|
+
- `probeMode`: `short` by default; `context` sends the configured long input.
|
|
204
|
+
- `probeMessage`: short probe text, up to 500 characters.
|
|
205
|
+
- `inputTokens`: long-context input size, only used with `probeMode=context`.
|
|
206
|
+
- `maxAttempts`: maximum keepalive attempts per account per Beijing day.
|
|
207
|
+
- `timeoutMs`: overall timeout for one model request, default 90 seconds.
|
|
208
|
+
- `logMaxBytes`: rotate one previous log after this size, default 2 MiB.
|
|
209
|
+
- `statePath`, `logPath`, `snapshotPath`, `commandPath`, `schedulePath`: optional
|
|
210
|
+
absolute paths.
|
|
211
|
+
|
|
212
|
+
## Logs
|
|
213
|
+
|
|
214
|
+
The default log is:
|
|
215
|
+
|
|
216
|
+
```text
|
|
217
|
+
~/.config/magpie/workbuddy-daily.log.jsonl
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Read it with the bundled command:
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
node ./bin/daily-keepalive-log.js --limit 50
|
|
224
|
+
node ./bin/daily-keepalive-log.js --day 2026-10-30 --status failed
|
|
225
|
+
node ./bin/daily-keepalive-log.js --json
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Agents can use the `quota-keepalive_logs` tool. Logs contain stable hashes of account
|
|
229
|
+
identifiers, provider IDs, status, model names, and sanitized errors. They never
|
|
230
|
+
contain tokens, full account identifiers, enterprise identifiers, or model replies.
|
|
231
|
+
|
|
232
|
+
## Security
|
|
233
|
+
|
|
234
|
+
- State and log files are created with mode `0600`.
|
|
235
|
+
- Writes use temporary files and atomic rename.
|
|
236
|
+
- `plugin-auth.json` updates use a short lock, file fingerprint checks, and conflict
|
|
237
|
+
retries. The plugin re-reads and merges when Magpie or another provider plugin
|
|
238
|
+
changed the file after it was read.
|
|
239
|
+
- Model replies are not written to state or logs.
|
|
240
|
+
- Tokens, Bearer values, API keys, and account identifiers are redacted from page
|
|
241
|
+
output and logs.
|
|
242
|
+
- Missing provider plugins, signed-out accounts, and model-less providers produce
|
|
243
|
+
explicit skip or failure results.
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
```sh
|
|
248
|
+
bun test
|
|
249
|
+
bun test --coverage
|
|
250
|
+
npm pack --dry-run
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Tests use mocked requests. They do not contact vendors or consume credits. The core
|
|
254
|
+
JavaScript modules currently have 100% line and function coverage, including
|
|
255
|
+
performance-boundary cases.
|
|
256
|
+
|
|
257
|
+
## Publishing
|
|
258
|
+
|
|
259
|
+
The repository uses npm Trusted Publishing through
|
|
260
|
+
`.github/workflows/publish.yml`; no npm token is stored in the repository or in
|
|
261
|
+
GitHub Secrets.
|
|
262
|
+
|
|
263
|
+
1. Update the version in `package.json` and commit the changes.
|
|
264
|
+
2. Create a matching tag, for example `v0.1.2`.
|
|
265
|
+
3. Push `main` and the tag.
|
|
266
|
+
4. Publish a GitHub Release for that tag.
|
|
267
|
+
5. The workflow runs tests, syntax checks, `npm pack`, and publishes through OIDC.
|
|
268
|
+
6. Installed users see the update through Magpie's hourly plugin update check.
|
|
269
|
+
|
|
270
|
+
Manual npm publishing with two-factor authentication enabled requires the current
|
|
271
|
+
one-time code:
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
npm publish --access public --otp=<current-one-time-code>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Never put an OTP, npm token, or recovery code in a repository, README, or chat.
|
|
278
|
+
|
|
279
|
+
## Links
|
|
280
|
+
|
|
281
|
+
- [GitHub](https://github.com/sljdxde/quota-keepalive)
|
|
282
|
+
- [npm](https://www.npmjs.com/package/quota-keepalive)
|
|
283
|
+
- [Releases](https://github.com/sljdxde/quota-keepalive/releases)
|
|
284
|
+
- [License](./LICENSE)
|
package/README.md
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# quota-keepalive
|
|
2
4
|
|
|
3
5
|
面向 Magpie 全部已登录供应商的额度签到、真实对话保活、执行留痕和额度趋势页面。
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://www.npmjs.com/package/quota-keepalive)
|
|
9
|
+
[](https://github.com/sljdxde/quota-keepalive)
|
|
10
|
+
[](#非官方插件声明)
|
|
11
|
+
|
|
12
|
+
中文 · [English](./README.en.md)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
quota-keepalive 不接管任何供应商的登录,也不会把页面伪装成单一供应商专属功能。
|
|
19
|
+
登录、模型、额度和签到 Hook 仍由各供应商插件维护;本插件负责统一检测、调度、
|
|
20
|
+
执行和展示。
|
|
21
|
+
|
|
22
|
+
> **非官方插件声明**
|
|
23
|
+
>
|
|
24
|
+
> 本项目不是 Magpie 官方插件。签到优先调用供应商插件公开的签到接口;真实对话
|
|
25
|
+
> 保活不是供应商授权或保证支持的功能。保活请求通过本机已登录的供应商签名适配器
|
|
26
|
+
> 直接发往供应商,不经过第三方服务器,也不会导出令牌,因此降低了凭据泄露风险,
|
|
27
|
+
> 但不等于厂商风控风险为零。自动化合成消息仍可能被供应商识别为异常流量,并触发
|
|
28
|
+
> 限流、额度冻结、账号限制或封禁。默认关闭保活,仅在理解风险后按需启用,并由
|
|
29
|
+
> 使用者自行承担相关后果。
|
|
6
30
|
|
|
7
31
|
## 功能
|
|
8
32
|
|
|
@@ -10,6 +34,7 @@
|
|
|
10
34
|
- 一键签到:通过各供应商登录插件公开的 `auth.checkin` 接口执行,尊重供应商、
|
|
11
35
|
账号两级启停;页面会明确显示未适配或未登录的账号,不会静默跳过。
|
|
12
36
|
- 真实对话保活:通过各供应商自己的登录插件取得签名请求,发送一条真实消息。
|
|
37
|
+
默认使用“短探活”,只发送一条短消息;需要长上下文时才显式切换“长上下文”模式。
|
|
13
38
|
- 模型选择:只从该账号实际列出的模型中选择。默认优先近期真实保活成功过的模型,
|
|
14
39
|
再按成功率、成功时间和失败次数排序;没有历史记录时沿用供应商自己的模型顺序,
|
|
15
40
|
不再按免费或价格抢先选择。第一个模型不可用时自动换下一档,全部不可用才记失败。
|
|
@@ -39,18 +64,50 @@
|
|
|
39
64
|
|
|
40
65
|
## 安装
|
|
41
66
|
|
|
42
|
-
|
|
67
|
+
npm 安装(推荐,支持 Magpie 自动检查更新):
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
magpie plugin add quota-keepalive
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
安装后的插件规格会显示为 `quota-keepalive@latest`。Magpie 会在启动后和之后每小时
|
|
74
|
+
检查一次更新,并在插件页面提示新版本。更新方式:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
# 更新全部 npm/git 插件
|
|
78
|
+
magpie plugin update
|
|
79
|
+
|
|
80
|
+
# 只更新这个插件
|
|
81
|
+
magpie plugin add quota-keepalive
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
页面中的 `Update all` 与 `magpie plugin update` 等价。重新添加同一个 npm 包会替换
|
|
85
|
+
旧版本,并保留插件 options、启用/停用状态和供应商偏好。已固定版本
|
|
86
|
+
`quota-keepalive@0.1.x` 也会在更新时升级到最新版并解除固定。
|
|
87
|
+
|
|
88
|
+
从 GitHub 安装(不经过 npm):
|
|
43
89
|
|
|
44
90
|
```sh
|
|
45
91
|
magpie plugin add github:sljdxde/quota-keepalive
|
|
46
92
|
```
|
|
47
93
|
|
|
48
|
-
|
|
94
|
+
从本地目录安装(仅用于开发调试):
|
|
49
95
|
|
|
50
96
|
```sh
|
|
51
97
|
magpie plugin add "$(pwd)"
|
|
52
98
|
```
|
|
53
99
|
|
|
100
|
+
目录安装会直接读取当前源码,适合修改后立即调试,但不会进入 npm 的版本检查,也不会
|
|
101
|
+
被 `magpie plugin update` 更新。开发完成后应切换到 npm 安装。切换前先保存 npm
|
|
102
|
+
条目中的 options,再停用目录条目:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
magpie plugin add quota-keepalive
|
|
106
|
+
magpie plugin options quota-keepalive '<原来的 options JSON>'
|
|
107
|
+
magpie plugin off '<目录插件的 spec>'
|
|
108
|
+
magpie plugin rm '<目录插件的 spec>'
|
|
109
|
+
```
|
|
110
|
+
|
|
54
111
|
插件不需要接管 provider。供应商登录仍由 Magpie 已安装的对应社区插件提供;本插件
|
|
55
112
|
直接从 Magpie 的插件目录加载这些签名适配器,因此 Magpie 或供应商插件更新后,
|
|
56
113
|
只要其公开接口没有变化,本插件仍可继续使用。若供应商包改名为不兼容的接口,页面
|
|
@@ -59,12 +116,28 @@ magpie plugin add "$(pwd)"
|
|
|
59
116
|
安装后在 Magpie 左侧 `Plugins` 分组打开“额度签到”。如果页面文件刚更新,
|
|
60
117
|
重新打开一次插件页即可刷新清单,不需要重启 Magpie。
|
|
61
118
|
|
|
62
|
-
|
|
119
|
+
### 发布新版本
|
|
120
|
+
|
|
121
|
+
仓库包含 `.github/workflows/publish.yml`,使用 npm Trusted Publishing,不需要把
|
|
122
|
+
npm token 写进仓库或 GitHub Secrets:
|
|
123
|
+
|
|
124
|
+
1. 更新 `package.json` 版本并提交改动。
|
|
125
|
+
2. 创建同名 Git tag,例如版本 `0.1.2` 对应 `v0.1.2`。
|
|
126
|
+
3. 推送 `main` 和 tag。
|
|
127
|
+
4. 在 GitHub 创建并发布对应的 Release。
|
|
128
|
+
5. Release 发布后,工作流会重新跑测试、语法检查和 `npm pack`,再通过 OIDC
|
|
129
|
+
发布到 npm。
|
|
130
|
+
6. 本机等待 Magpie 的更新提示,或执行 `magpie plugin add quota-keepalive` /
|
|
131
|
+
`magpie plugin update`。
|
|
132
|
+
|
|
133
|
+
本地手动发布时,npm 如果启用了双因素认证,需要附加当前动态码:
|
|
63
134
|
|
|
64
135
|
```sh
|
|
65
|
-
|
|
136
|
+
npm publish --access public --otp=<6 位动态码>
|
|
66
137
|
```
|
|
67
138
|
|
|
139
|
+
不要把动态码、npm token 或恢复码写进仓库、README 或聊天记录。
|
|
140
|
+
|
|
68
141
|
## 页面操作
|
|
69
142
|
|
|
70
143
|
- `一键检测`:刷新供应商、账号、模型和额度快照。
|
|
@@ -76,7 +149,10 @@ magpie plugin add quota-keepalive
|
|
|
76
149
|
不会因为快速连续点击触发“每秒只能写入一次”的报错。
|
|
77
150
|
- 圆形刷新按钮:立即刷新全部启用供应商额度;供应商卡片内的刷新按钮只刷新该供应商。
|
|
78
151
|
- `保存计划`:设置统一执行间隔和任务类型。计划默认关闭,防止无意间消耗积分。
|
|
79
|
-
-
|
|
152
|
+
- `短探活/长上下文`:短探活风险、耗时和额度消耗更低;长上下文会按 `inputTokens`
|
|
153
|
+
填充请求,适合明确需要维持长上下文窗口的场景。
|
|
154
|
+
- 定时计划保存或修改后,从下一个间隔开始执行;下次执行时间会持久化,Magpie 重启或
|
|
155
|
+
电脑休眠后会补跑已经到期的任务。需要立即执行时使用页面顶部的手动按钮。
|
|
80
156
|
|
|
81
157
|
页面每 5 秒读取一次本地快照;插件后台每 1 分钟刷新启用中供应商的额度,停用项会
|
|
82
158
|
跳过。额度刷新仍写入后台诊断日志,但不会出现在页面的签到/保活执行记录中。
|
|
@@ -120,6 +196,8 @@ magpie plugin options quota-keepalive '{
|
|
|
120
196
|
"codearts": "deepseek-v4.1-flash"
|
|
121
197
|
},
|
|
122
198
|
"modelCandidates": 4,
|
|
199
|
+
"probeMode": "short",
|
|
200
|
+
"probeMessage": "Magpie daily connectivity check. Reply with OK.",
|
|
123
201
|
"inputTokens": 25000,
|
|
124
202
|
"maxAttempts": 3,
|
|
125
203
|
"timeoutMs": 90000,
|
|
@@ -136,21 +214,25 @@ magpie plugin options quota-keepalive '{
|
|
|
136
214
|
WorkBuddy CN 默认优先 `deepseek-v4.1-flash`,这是 dsh 实测较稳定的真实对话模型;
|
|
137
215
|
其他供应商按近 5 天可用性排序,没有成功记录时按供应商模型顺序选择,不按免费或倍率抢先选择。
|
|
138
216
|
- `modelCandidates`:每个账号每次最多尝试的模型数,默认 4,范围 1–12。
|
|
139
|
-
- `
|
|
217
|
+
- `probeMode`:`short`(默认)只发一条短消息;`context` 才使用长上下文填充。
|
|
218
|
+
- `probeMessage`:短探活消息,最多 500 字符;留空时使用默认连通性检查文本。
|
|
219
|
+
- `inputTokens`:仅 `probeMode=context` 时生效,默认 25000,范围 1000–25000。
|
|
140
220
|
- `maxAttempts`:每个账号每个北京时间日最多保活尝试轮数,默认 3。
|
|
141
221
|
- `timeoutMs`:单个模型请求的整体截止时间,默认 90 秒,范围 10–180 秒。WorkBuddy
|
|
142
222
|
CN 的 25k 输入请求可能先返回响应头、再继续流式读取;90 秒比 25 秒更接近 dsh 的
|
|
143
223
|
probe 容忍度,同时仍避免任务无限挂起。即使供应商
|
|
144
224
|
登录插件内部忽略中止信号,外层也会结束该候选并继续回退;账号较多时不要调得过大。
|
|
145
225
|
- `logMaxBytes`:单个日志上限,默认 2 MiB,超出后轮转一份 `.1`。
|
|
146
|
-
- `statePath`、`logPath`、`snapshotPath`、`commandPath`:可选绝对路径。
|
|
226
|
+
- `statePath`、`logPath`、`snapshotPath`、`commandPath`、`schedulePath`:可选绝对路径。
|
|
147
227
|
|
|
148
228
|
## 执行周期与失败
|
|
149
229
|
|
|
150
230
|
Magpie 自己的每日签到调度仍会在启动后约 2 分钟进行首轮,之后约每 30 分钟巡检;
|
|
151
231
|
失败约 30 分钟后重试。本插件的定时任务是额外的统一调度,默认关闭,适合需要更
|
|
152
232
|
频繁签到或保活的场景。本插件的定时任务不会在保存后立即执行,也不会在修改后
|
|
153
|
-
重启 Magpie
|
|
233
|
+
重启 Magpie;修改配置会替换运行中的定时器,并从新的间隔重新计时。调度状态保存在
|
|
234
|
+
`quota-keepalive-schedule.json`,Magpie 重启后会读取上次执行时间和下次执行时间,
|
|
235
|
+
已到期的任务会补跑,不会因为反复重启而一直错过执行。
|
|
154
236
|
|
|
155
237
|
保活失败会按 `maxAttempts` 重试;达到上限后当天不再自动发送。网络失败、登录过期、
|
|
156
238
|
模型不可用和空回复都会进入日志。遇到明确的 HTTP 402/429 额度耗尽或免费额度用尽时,
|
|
@@ -180,7 +262,8 @@ node ./bin/daily-keepalive-log.js --json
|
|
|
180
262
|
## 安全
|
|
181
263
|
|
|
182
264
|
- 状态和日志文件以 `0600` 创建,写入使用临时文件和原子 rename。
|
|
183
|
-
- `plugin-auth.json`
|
|
265
|
+
- `plugin-auth.json` 的更新使用短锁、文件指纹校验和冲突重试。若 Magpie 或其他
|
|
266
|
+
供应商插件在读取后改动了凭据文件,本插件会重新读取并合并,不会直接覆盖新内容。
|
|
184
267
|
- 模型回复不会写入状态或日志。
|
|
185
268
|
- 日志和页面显示会对令牌、Bearer、API key 和账号标识做脱敏。
|
|
186
269
|
- 未安装对应供应商插件、未登录或没有可用模型时,会明确记为跳过或失败。
|
package/index.js
CHANGED
|
@@ -11,6 +11,12 @@ import { snapshotWarning, writeDailySnapshot } from "./src/snapshot.js"
|
|
|
11
11
|
import { createProviderCheckin } from "./src/checkin-runner.js"
|
|
12
12
|
import { createWorkBuddyKeepalive } from "./src/workbuddy.js"
|
|
13
13
|
import { readCommand } from "./src/commands.js"
|
|
14
|
+
import {
|
|
15
|
+
nextRunAfter,
|
|
16
|
+
readSchedule,
|
|
17
|
+
scheduleSignature,
|
|
18
|
+
writeSchedule,
|
|
19
|
+
} from "./src/schedule.js"
|
|
14
20
|
|
|
15
21
|
const LOG_STATUSES = new Set(["started", "success", "failed", "skipped"])
|
|
16
22
|
const SCHEDULER = Symbol.for("quota-keepalive.scheduler")
|
|
@@ -20,32 +26,85 @@ const REFRESH_INTERVAL_MS = 60_000
|
|
|
20
26
|
|
|
21
27
|
export function stopScheduler() {
|
|
22
28
|
const previous = globalThis[SCHEDULER]
|
|
23
|
-
if (previous?.timer)
|
|
29
|
+
if (previous?.timer) clearTimeout(previous.timer)
|
|
30
|
+
if (previous) previous.stopped = true
|
|
24
31
|
if (previous) delete globalThis[SCHEDULER]
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
function warnSchedule(action, error) {
|
|
35
|
+
console.warn(`[quota-keepalive] ${action}: ${error?.message || error}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function startScheduler({
|
|
39
|
+
config,
|
|
40
|
+
checkin,
|
|
41
|
+
keepalive,
|
|
42
|
+
now = () => Date.now(),
|
|
43
|
+
read = readSchedule,
|
|
44
|
+
persist = writeSchedule,
|
|
45
|
+
run = runCommand,
|
|
46
|
+
log = appendDailyLog,
|
|
47
|
+
snapshot = writeDailySnapshot,
|
|
48
|
+
}) {
|
|
28
49
|
// Every behavior setting must participate in the signature. Otherwise a
|
|
29
50
|
// model or provider/account preference change would leave the running timer
|
|
30
51
|
// on the previous closures and configuration.
|
|
31
|
-
const signature =
|
|
52
|
+
const signature = scheduleSignature(config)
|
|
32
53
|
const previous = globalThis[SCHEDULER]
|
|
33
54
|
if (previous?.signature === signature) return
|
|
34
55
|
stopScheduler()
|
|
35
56
|
if (!config.scheduleEnabled) return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
running
|
|
57
|
+
const controller = {
|
|
58
|
+
signature,
|
|
59
|
+
timer: null,
|
|
60
|
+
running: false,
|
|
61
|
+
stopped: false,
|
|
62
|
+
nextRunAt: 0,
|
|
63
|
+
}
|
|
64
|
+
globalThis[SCHEDULER] = controller
|
|
65
|
+
|
|
66
|
+
const intervalMs = config.scheduleMinutes * 60_000
|
|
67
|
+
let saved
|
|
68
|
+
try {
|
|
69
|
+
saved = await read(config.schedulePath)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
warnSchedule("cannot read the persisted schedule", error)
|
|
72
|
+
saved = {}
|
|
73
|
+
}
|
|
74
|
+
controller.nextRunAt = nextRunAfter({
|
|
75
|
+
now: now(),
|
|
76
|
+
lastRunAt: saved.lastRunAt,
|
|
77
|
+
nextRunAt: saved.nextRunAt,
|
|
78
|
+
intervalMs,
|
|
79
|
+
storedSignature: saved.signature,
|
|
80
|
+
signature,
|
|
81
|
+
})
|
|
82
|
+
await persist(config.schedulePath, {
|
|
83
|
+
...saved,
|
|
84
|
+
signature,
|
|
85
|
+
nextRunAt: new Date(controller.nextRunAt).toISOString(),
|
|
86
|
+
}).catch((error) => warnSchedule("cannot save the schedule", error))
|
|
87
|
+
|
|
88
|
+
const arm = () => {
|
|
89
|
+
if (controller.stopped) return
|
|
90
|
+
const delay = Math.max(0, controller.nextRunAt - now())
|
|
91
|
+
controller.timer = setTimeout(tick, delay)
|
|
92
|
+
controller.timer?.unref?.()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function tick() {
|
|
96
|
+
if (controller.stopped || controller.running) return
|
|
97
|
+
controller.running = true
|
|
98
|
+
let result = null
|
|
40
99
|
try {
|
|
41
|
-
|
|
100
|
+
result = await run({
|
|
42
101
|
action: config.scheduleAction,
|
|
43
102
|
config,
|
|
44
103
|
checkin,
|
|
45
104
|
keepalive,
|
|
46
105
|
force: false,
|
|
47
106
|
})
|
|
48
|
-
await
|
|
107
|
+
await log({
|
|
49
108
|
logPath: config.logPath,
|
|
50
109
|
logMaxBytes: config.logMaxBytes,
|
|
51
110
|
entry: {
|
|
@@ -58,15 +117,15 @@ export function startScheduler({ config, checkin, keepalive }) {
|
|
|
58
117
|
outcome: result.status,
|
|
59
118
|
message: result.summary,
|
|
60
119
|
},
|
|
61
|
-
})
|
|
62
|
-
await
|
|
120
|
+
}).catch((error) => warnSchedule("cannot write the schedule log", error))
|
|
121
|
+
await snapshot({
|
|
63
122
|
...(config.configHome ? { configHome: config.configHome } : {}),
|
|
64
123
|
logPath: config.logPath,
|
|
65
124
|
snapshotPath: config.snapshotPath,
|
|
66
125
|
commandPath: config.commandPath,
|
|
67
|
-
})
|
|
126
|
+
}).catch((error) => warnSchedule(snapshotWarning(error), error))
|
|
68
127
|
} catch (error) {
|
|
69
|
-
await
|
|
128
|
+
await log({
|
|
70
129
|
logPath: config.logPath,
|
|
71
130
|
logMaxBytes: config.logMaxBytes,
|
|
72
131
|
entry: {
|
|
@@ -79,13 +138,31 @@ export function startScheduler({ config, checkin, keepalive }) {
|
|
|
79
138
|
outcome: "failed",
|
|
80
139
|
error: error instanceof Error ? error.message : String(error),
|
|
81
140
|
},
|
|
82
|
-
})
|
|
141
|
+
}).catch((logError) => warnSchedule("cannot write the failure log", logError))
|
|
83
142
|
} finally {
|
|
84
|
-
running = false
|
|
143
|
+
controller.running = false
|
|
144
|
+
const finishedAt = now()
|
|
145
|
+
controller.nextRunAt = nextRunAfter({
|
|
146
|
+
now: finishedAt,
|
|
147
|
+
lastRunAt: new Date(finishedAt).toISOString(),
|
|
148
|
+
intervalMs,
|
|
149
|
+
storedSignature: signature,
|
|
150
|
+
signature,
|
|
151
|
+
})
|
|
152
|
+
await persist(config.schedulePath, {
|
|
153
|
+
...saved,
|
|
154
|
+
signature,
|
|
155
|
+
lastRunAt: new Date(finishedAt).toISOString(),
|
|
156
|
+
nextRunAt: new Date(controller.nextRunAt).toISOString(),
|
|
157
|
+
lastStatus: result?.status || "failed",
|
|
158
|
+
lastSummary: result?.summary || "",
|
|
159
|
+
}).catch((error) => warnSchedule("cannot save the completed schedule", error))
|
|
160
|
+
arm()
|
|
85
161
|
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
arm()
|
|
165
|
+
return controller
|
|
89
166
|
}
|
|
90
167
|
|
|
91
168
|
export function stopOptionsWatcher() {
|
|
@@ -205,7 +282,7 @@ export function startOptionsWatcher({
|
|
|
205
282
|
const liveConfig = resolveKeepaliveOptions(liveRaw)
|
|
206
283
|
const checkin = makeCheckin({ input, config: liveConfig })
|
|
207
284
|
const keepalive = makeKeepalive({ input, config: liveConfig })
|
|
208
|
-
start({ config: liveConfig, checkin, keepalive })
|
|
285
|
+
await start({ config: liveConfig, checkin, keepalive })
|
|
209
286
|
startRefresh({ config: liveConfig })
|
|
210
287
|
await consume({
|
|
211
288
|
config: liveConfig,
|
|
@@ -303,7 +380,7 @@ export async function QuotaKeepalivePlugin(input, rawOptions = {}) {
|
|
|
303
380
|
const config = resolveKeepaliveOptions(rawOptions)
|
|
304
381
|
const checkin = createProviderCheckin({ input, config })
|
|
305
382
|
const keepalive = createWorkBuddyKeepalive({ input, config })
|
|
306
|
-
startScheduler({ config, checkin, keepalive })
|
|
383
|
+
await startScheduler({ config, checkin, keepalive })
|
|
307
384
|
startQuotaRefresher({ config })
|
|
308
385
|
startOptionsWatcher({ input, rawOptions, config })
|
|
309
386
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quota-keepalive",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Unofficial multi-provider quota inventory, check-in, keepalive scheduling, history, and quota trends for Magpie",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"magpie",
|
|
7
7
|
"magpie-plugin",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"main": "./index.js",
|
|
26
26
|
"exports": "./index.js",
|
|
27
27
|
"bin": {
|
|
28
|
-
"daily-keepalive-log": "
|
|
28
|
+
"daily-keepalive-log": "bin/daily-keepalive-log.js"
|
|
29
29
|
},
|
|
30
30
|
"magpie": {
|
|
31
31
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iOCIgeTE9IjQiIHgyPSI1NiIgeTI9IjYwIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMwQjIwMjkiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTczQzQ2Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJnb2xkIiB4MT0iMzgiIHkxPSIzOCIgeDI9IjU2IiB5Mj0iNTYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0ZGRDI3QSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNFNzlCMzgiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgPC9kZWZzPgogIDxyZWN0IHg9IjMiIHk9IjMiIHdpZHRoPSI1OCIgaGVpZ2h0PSI1OCIgcng9IjE4IiBmaWxsPSJ1cmwoI2JnKSIvPgogIDxjaXJjbGUgY3g9IjMwLjUiIGN5PSIzMSIgcj0iMTcuMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjhGMURFIiBzdHJva2Utd2lkdGg9IjcuNCIKICAgICAgICAgIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWRhc2hhcnJheT0iODggMjEiIHRyYW5zZm9ybT0icm90YXRlKDM4IDMwLjUgMzEpIi8+CiAgPHBhdGggZD0iTTM0LjUgMzYuNWM1LjMgNC40IDEwLjIgOS4yIDE5LjUgMTguMyIKICAgICAgICBmaWxsPSJub25lIiBzdHJva2U9InVybCgjZ29sZCkiIHN0cm9rZS13aWR0aD0iNi4yIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+Cg==",
|
package/src/config.js
CHANGED
|
@@ -7,16 +7,20 @@ import {
|
|
|
7
7
|
MIN_LOG_MAX_BYTES,
|
|
8
8
|
defaultLogPath,
|
|
9
9
|
} from "./log.js"
|
|
10
|
+
import { DEFAULT_PROBE_MESSAGE } from "./keepalive.js"
|
|
10
11
|
import {
|
|
11
12
|
normalizeAccountPreferences,
|
|
12
13
|
normalizeProviderPreferences,
|
|
13
14
|
} from "./preferences.js"
|
|
15
|
+
import { defaultSchedulePath } from "./schedule.js"
|
|
14
16
|
import { defaultSnapshotPath } from "./snapshot.js"
|
|
15
17
|
|
|
16
18
|
export const DEFAULT_KEEPALIVE = Object.freeze({
|
|
17
19
|
model: "",
|
|
18
20
|
modelCandidates: 4,
|
|
19
21
|
inputTokens: 25_000,
|
|
22
|
+
probeMode: "short",
|
|
23
|
+
probeMessage: DEFAULT_PROBE_MESSAGE,
|
|
20
24
|
maxAttempts: 3,
|
|
21
25
|
scheduleEnabled: false,
|
|
22
26
|
scheduleAction: "checkin",
|
|
@@ -54,6 +58,9 @@ function absolutePath(value, fallback) {
|
|
|
54
58
|
export function resolveKeepaliveOptions(raw) {
|
|
55
59
|
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}
|
|
56
60
|
const model = typeof source.model === "string" ? source.model.trim().slice(0, 200) : ""
|
|
61
|
+
const probeMessage = typeof source.probeMessage === "string"
|
|
62
|
+
? source.probeMessage.trim().slice(0, 500)
|
|
63
|
+
: ""
|
|
57
64
|
const scheduleAction = ["checkin", "keepalive", "all"].includes(source.scheduleAction)
|
|
58
65
|
? source.scheduleAction
|
|
59
66
|
: DEFAULT_KEEPALIVE.scheduleAction
|
|
@@ -68,6 +75,8 @@ export function resolveKeepaliveOptions(raw) {
|
|
|
68
75
|
scheduleMinutes: integerIn(source.scheduleMinutes, DEFAULT_KEEPALIVE.scheduleMinutes, 5, 10_080),
|
|
69
76
|
configHome: absolutePath(source.configHome, undefined),
|
|
70
77
|
inputTokens: integerIn(source.inputTokens, DEFAULT_KEEPALIVE.inputTokens, 1_000, 25_000),
|
|
78
|
+
probeMode: source.probeMode === "context" ? "context" : DEFAULT_KEEPALIVE.probeMode,
|
|
79
|
+
probeMessage: probeMessage || DEFAULT_PROBE_MESSAGE,
|
|
71
80
|
maxAttempts: integerIn(source.maxAttempts, DEFAULT_KEEPALIVE.maxAttempts, 1, 10),
|
|
72
81
|
timeoutMs: integerIn(source.timeoutMs, DEFAULT_KEEPALIVE.timeoutMs, 10_000, 180_000),
|
|
73
82
|
lockWaitMs: integerIn(source.lockWaitMs, DEFAULT_KEEPALIVE.lockWaitMs, 0, 10_000),
|
|
@@ -82,5 +91,6 @@ export function resolveKeepaliveOptions(raw) {
|
|
|
82
91
|
MAX_LOG_MAX_BYTES,
|
|
83
92
|
),
|
|
84
93
|
snapshotPath: absolutePath(source.snapshotPath, defaultSnapshotPath()),
|
|
94
|
+
schedulePath: absolutePath(source.schedulePath, defaultSchedulePath()),
|
|
85
95
|
}
|
|
86
96
|
}
|
package/src/keepalive.js
CHANGED
|
@@ -4,20 +4,36 @@ const ORIGIN = "https://copilot.tencent.com"
|
|
|
4
4
|
const CHAT_PATH = "/v2/chat/completions"
|
|
5
5
|
const FILLER = "The quick brown fox jumps over the lazy dog. "
|
|
6
6
|
const CHARS_PER_TOKEN = 4.5
|
|
7
|
+
export const DEFAULT_PROBE_MESSAGE = "Magpie daily connectivity check. Reply with OK."
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
function probeMessage(value) {
|
|
10
|
+
return sanitize(value, 500).trim() || DEFAULT_PROBE_MESSAGE
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildKeepaliveBody({
|
|
14
|
+
model,
|
|
15
|
+
inputTokens,
|
|
16
|
+
day,
|
|
17
|
+
probeMode = "short",
|
|
18
|
+
probeMessage: message = "",
|
|
19
|
+
}) {
|
|
20
|
+
const prompt = `${probeMessage(message)} Today is ${day}.`
|
|
21
|
+
let content = prompt
|
|
22
|
+
if (probeMode === "context") {
|
|
23
|
+
const targetChars = Math.ceil(inputTokens * CHARS_PER_TOKEN)
|
|
24
|
+
const filler = FILLER.repeat(Math.ceil(targetChars / FILLER.length))
|
|
25
|
+
content = `${filler}${prompt}`
|
|
26
|
+
}
|
|
11
27
|
return {
|
|
12
28
|
model,
|
|
13
29
|
messages: [
|
|
14
30
|
{
|
|
15
31
|
role: "system",
|
|
16
|
-
content: "You are a connectivity check. Reply with a single word.",
|
|
32
|
+
content: "You are a concise connectivity check. Reply with a single word.",
|
|
17
33
|
},
|
|
18
34
|
{
|
|
19
35
|
role: "user",
|
|
20
|
-
content
|
|
36
|
+
content,
|
|
21
37
|
},
|
|
22
38
|
],
|
|
23
39
|
max_tokens: 1,
|
|
@@ -152,6 +168,8 @@ export async function sendKeepalive({
|
|
|
152
168
|
origin = ORIGIN,
|
|
153
169
|
model,
|
|
154
170
|
inputTokens,
|
|
171
|
+
probeMode,
|
|
172
|
+
probeMessage,
|
|
155
173
|
timeoutMs,
|
|
156
174
|
day,
|
|
157
175
|
key,
|
|
@@ -167,7 +185,13 @@ export async function sendKeepalive({
|
|
|
167
185
|
accept: "text/event-stream",
|
|
168
186
|
"x-magpie-workbuddy-session": `workbuddy-keepalive:${key}:${day}`.slice(0, 200),
|
|
169
187
|
},
|
|
170
|
-
body: JSON.stringify(buildKeepaliveBody({
|
|
188
|
+
body: JSON.stringify(buildKeepaliveBody({
|
|
189
|
+
model,
|
|
190
|
+
inputTokens,
|
|
191
|
+
day,
|
|
192
|
+
probeMode,
|
|
193
|
+
probeMessage,
|
|
194
|
+
})),
|
|
171
195
|
signal: AbortSignal.timeout(timeoutMs),
|
|
172
196
|
})
|
|
173
197
|
return { response, raw: await response.text() }
|
package/src/log.js
CHANGED
|
@@ -227,6 +227,7 @@ export function formatLogRecord(record) {
|
|
|
227
227
|
["outcome", record.outcome],
|
|
228
228
|
["attempts", record.attempts],
|
|
229
229
|
["model", record.model],
|
|
230
|
+
["probeMode", record.probeMode],
|
|
230
231
|
["inputTokens", record.inputTokens],
|
|
231
232
|
["credit", record.credit],
|
|
232
233
|
["streak", record.streak],
|
package/src/provider-chat.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { sanitize, terminalQuotaFailure, text, withTimeout } from "./common.js"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PROBE_MESSAGE,
|
|
4
|
+
buildKeepaliveBody,
|
|
5
|
+
sendKeepalive,
|
|
6
|
+
} from "./keepalive.js"
|
|
3
7
|
|
|
4
8
|
const FILLER = "The quick brown fox jumps over the lazy dog. "
|
|
5
9
|
const CHARS_PER_TOKEN = 4.5
|
|
@@ -42,15 +46,27 @@ export function endpointFor(baseURL, protocol) {
|
|
|
42
46
|
return `${base}${protocol === "anthropic" ? "/messages" : "/chat/completions"}`
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
export function anthropicBody({
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
export function anthropicBody({
|
|
50
|
+
model,
|
|
51
|
+
inputTokens,
|
|
52
|
+
day,
|
|
53
|
+
probeMode = "short",
|
|
54
|
+
probeMessage = "",
|
|
55
|
+
}) {
|
|
56
|
+
const message = sanitize(probeMessage, 500).trim() || DEFAULT_PROBE_MESSAGE
|
|
57
|
+
const prompt = `${message} Today is ${day}.`
|
|
58
|
+
let content = prompt
|
|
59
|
+
if (probeMode === "context") {
|
|
60
|
+
const targetChars = Math.ceil(inputTokens * CHARS_PER_TOKEN)
|
|
61
|
+
const filler = FILLER.repeat(Math.ceil(targetChars / FILLER.length))
|
|
62
|
+
content = `${filler}${prompt}`
|
|
63
|
+
}
|
|
48
64
|
return {
|
|
49
65
|
model,
|
|
50
|
-
system: "You are a connectivity check. Reply with a short word.",
|
|
66
|
+
system: "You are a concise connectivity check. Reply with a short word.",
|
|
51
67
|
messages: [{
|
|
52
68
|
role: "user",
|
|
53
|
-
content
|
|
69
|
+
content,
|
|
54
70
|
}],
|
|
55
71
|
max_tokens: 8,
|
|
56
72
|
stream: true,
|
|
@@ -126,6 +142,8 @@ export async function sendProviderChat({
|
|
|
126
142
|
protocol,
|
|
127
143
|
loader,
|
|
128
144
|
inputTokens,
|
|
145
|
+
probeMode,
|
|
146
|
+
probeMessage,
|
|
129
147
|
timeoutMs,
|
|
130
148
|
day,
|
|
131
149
|
key,
|
|
@@ -148,6 +166,8 @@ export async function sendProviderChat({
|
|
|
148
166
|
origin,
|
|
149
167
|
model,
|
|
150
168
|
inputTokens,
|
|
169
|
+
probeMode,
|
|
170
|
+
probeMessage,
|
|
151
171
|
timeoutMs,
|
|
152
172
|
day,
|
|
153
173
|
key,
|
|
@@ -169,8 +189,8 @@ export async function sendProviderChat({
|
|
|
169
189
|
|
|
170
190
|
const url = endpointFor(loader.baseURL, protocol)
|
|
171
191
|
const body = protocol === "anthropic"
|
|
172
|
-
? anthropicBody({ model, inputTokens, day })
|
|
173
|
-
: buildKeepaliveBody({ model, inputTokens, day })
|
|
192
|
+
? anthropicBody({ model, inputTokens, day, probeMode, probeMessage })
|
|
193
|
+
: buildKeepaliveBody({ model, inputTokens, day, probeMode, probeMessage })
|
|
174
194
|
|
|
175
195
|
let response
|
|
176
196
|
let raw
|
package/src/schedule.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto"
|
|
2
|
+
import { chmod, mkdir, open, readFile, rename, unlink } from "node:fs/promises"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
import { errorText, magpieConfigHome, sanitize } from "./common.js"
|
|
5
|
+
|
|
6
|
+
const VERSION = 1
|
|
7
|
+
const FILE_OPS = { chmod, mkdir, open, readFile, rename, unlink }
|
|
8
|
+
|
|
9
|
+
export function defaultSchedulePath() {
|
|
10
|
+
return join(magpieConfigHome(), "quota-keepalive-schedule.json")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function emptySchedule() {
|
|
14
|
+
return {
|
|
15
|
+
version: VERSION,
|
|
16
|
+
signature: "",
|
|
17
|
+
lastRunAt: "",
|
|
18
|
+
nextRunAt: "",
|
|
19
|
+
lastStatus: "",
|
|
20
|
+
lastSummary: "",
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validDate(value) {
|
|
25
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function readSchedule(path, ops = FILE_OPS) {
|
|
29
|
+
let raw
|
|
30
|
+
try {
|
|
31
|
+
raw = await ops.readFile(path, "utf8")
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error?.code === "ENOENT") return emptySchedule()
|
|
34
|
+
throw new Error(`cannot read ${path}: ${errorText(error)}`)
|
|
35
|
+
}
|
|
36
|
+
let value
|
|
37
|
+
try {
|
|
38
|
+
value = JSON.parse(raw)
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`${path} is not valid JSON; it was left unchanged`)
|
|
41
|
+
}
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || value.version !== VERSION) {
|
|
43
|
+
throw new Error(`${path} has an unsupported shape; it was left unchanged`)
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
version: VERSION,
|
|
47
|
+
signature: sanitize(value.signature, 2_000),
|
|
48
|
+
lastRunAt: validDate(value.lastRunAt) ? value.lastRunAt : "",
|
|
49
|
+
nextRunAt: validDate(value.nextRunAt) ? value.nextRunAt : "",
|
|
50
|
+
lastStatus: sanitize(value.lastStatus, 24),
|
|
51
|
+
lastSummary: sanitize(value.lastSummary, 480),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function writeSchedule(path, value, ops = FILE_OPS) {
|
|
56
|
+
await ops.mkdir(dirname(path), { recursive: true, mode: 0o700 })
|
|
57
|
+
const tmp = `${path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`
|
|
58
|
+
let handle
|
|
59
|
+
try {
|
|
60
|
+
handle = await ops.open(tmp, "wx", 0o600)
|
|
61
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8")
|
|
62
|
+
await handle.sync()
|
|
63
|
+
await handle.close()
|
|
64
|
+
handle = null
|
|
65
|
+
await ops.rename(tmp, path)
|
|
66
|
+
await ops.chmod(path, 0o600)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (handle) await handle.close().catch(() => {})
|
|
69
|
+
await ops.unlink(tmp).catch(() => {})
|
|
70
|
+
throw error
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function nextRunAfter({
|
|
75
|
+
now = Date.now(),
|
|
76
|
+
lastRunAt = "",
|
|
77
|
+
nextRunAt = "",
|
|
78
|
+
intervalMs,
|
|
79
|
+
storedSignature = "",
|
|
80
|
+
signature = "",
|
|
81
|
+
}) {
|
|
82
|
+
const interval = Math.max(1_000, Number(intervalMs) || 60_000)
|
|
83
|
+
if (storedSignature !== signature) return now + interval
|
|
84
|
+
const saved = Date.parse(nextRunAt)
|
|
85
|
+
const last = Date.parse(lastRunAt)
|
|
86
|
+
if (Number.isFinite(saved)) return Math.max(saved, now)
|
|
87
|
+
if (Number.isFinite(last)) return Math.max(last + interval, now)
|
|
88
|
+
return now + interval
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function scheduleSignature(config = {}) {
|
|
92
|
+
return JSON.stringify(config)
|
|
93
|
+
}
|
package/src/snapshot.js
CHANGED
|
@@ -31,6 +31,7 @@ const FIELD_LIMITS = {
|
|
|
31
31
|
status: 12,
|
|
32
32
|
outcome: 24,
|
|
33
33
|
model: 160,
|
|
34
|
+
probeMode: 16,
|
|
34
35
|
account: 24,
|
|
35
36
|
run: 48,
|
|
36
37
|
}
|
|
@@ -47,7 +48,7 @@ function safeField(record, name) {
|
|
|
47
48
|
|
|
48
49
|
export function publicRecord(record) {
|
|
49
50
|
const out = {}
|
|
50
|
-
for (const name of ["ts", "day", "provider", "event", "status", "outcome", "model", "account", "run"]) {
|
|
51
|
+
for (const name of ["ts", "day", "provider", "event", "status", "outcome", "model", "probeMode", "account", "run"]) {
|
|
51
52
|
const value = safeField(record, name)
|
|
52
53
|
if (value) out[name] = value
|
|
53
54
|
}
|
package/src/state.js
CHANGED
|
@@ -232,8 +232,21 @@ export async function runDailyOnce({
|
|
|
232
232
|
return { kind: "unavailable", error: errorText(error) }
|
|
233
233
|
}
|
|
234
234
|
prune(doc, day)
|
|
235
|
-
|
|
235
|
+
let previous = newer(doc.accounts[account], inMemory)
|
|
236
|
+
let interrupted = false
|
|
236
237
|
if (previous?.day === day) {
|
|
238
|
+
const startedAt = Date.parse(previous.at || "")
|
|
239
|
+
const staleMs = Math.max(30_000, timeoutMs + 30_000)
|
|
240
|
+
if (previous.status === "running" && Number.isFinite(startedAt) &&
|
|
241
|
+
now.getTime() - startedAt > staleMs) {
|
|
242
|
+
interrupted = true
|
|
243
|
+
previous = {
|
|
244
|
+
...previous,
|
|
245
|
+
status: "failed",
|
|
246
|
+
at: new Date(startedAt).toISOString(),
|
|
247
|
+
error: "the previous attempt was interrupted",
|
|
248
|
+
}
|
|
249
|
+
}
|
|
237
250
|
remember(statePath, account, previous)
|
|
238
251
|
if (previous.status === "ok") {
|
|
239
252
|
return { kind: "skipped", reason: "done-today", attempts: previous.attempts }
|
|
@@ -246,7 +259,7 @@ export async function runDailyOnce({
|
|
|
246
259
|
error: previous.error || "",
|
|
247
260
|
}
|
|
248
261
|
}
|
|
249
|
-
if (previous.attempts >= maxAttempts && !force) {
|
|
262
|
+
if (previous.attempts >= maxAttempts && !force && !interrupted) {
|
|
250
263
|
return {
|
|
251
264
|
kind: "skipped",
|
|
252
265
|
reason: "exhausted",
|
package/src/workbuddy.js
CHANGED
|
@@ -181,31 +181,83 @@ export async function withAuthLock(authPath, work, waitMs = 2_000, ops = FILE_OP
|
|
|
181
181
|
export async function saveAuthEntry(authPath, key, next, ops = FILE_OPS) {
|
|
182
182
|
if (!text(key) || !object(next)) throw new Error("refusing to save an invalid provider sign-in")
|
|
183
183
|
return withAuthLock(authPath, async () => {
|
|
184
|
-
let
|
|
185
|
-
|
|
186
|
-
all =
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
184
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
185
|
+
const snapshot = await readAuthSnapshot(authPath, ops)
|
|
186
|
+
const all = snapshot.value
|
|
187
|
+
if (!object(all)) throw new Error("plugin-auth.json isn't an object")
|
|
188
|
+
if (JSON.stringify(all[key]) === JSON.stringify(next)) return true
|
|
189
|
+
|
|
190
|
+
all[key] = next
|
|
191
|
+
const serialized = `${JSON.stringify(all, null, 2)}\n`
|
|
192
|
+
const tmp = `${authPath}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`
|
|
193
|
+
let handle
|
|
194
|
+
try {
|
|
195
|
+
handle = await ops.open(tmp, "wx", 0o600)
|
|
196
|
+
await handle.writeFile(serialized, "utf8")
|
|
197
|
+
await handle.sync()
|
|
198
|
+
await handle.close()
|
|
199
|
+
handle = null
|
|
200
|
+
|
|
201
|
+
// Magpie and provider plugins do not use this lock. Refuse to replace
|
|
202
|
+
// a file that changed after our read, then merge against the new copy.
|
|
203
|
+
const current = await readAuthStamp(authPath, ops)
|
|
204
|
+
if (current !== snapshot.stamp) {
|
|
205
|
+
await ops.unlink(tmp).catch(() => {})
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
await ops.rename(tmp, authPath)
|
|
210
|
+
await ops.chmod(authPath, 0o600)
|
|
211
|
+
return true
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (handle) await handle.close().catch(() => {})
|
|
214
|
+
await ops.unlink(tmp).catch(() => {})
|
|
215
|
+
throw error
|
|
216
|
+
}
|
|
190
217
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
218
|
+
throw new Error("magpie's sign-in file changed repeatedly; the update was not saved")
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function authStamp(info) {
|
|
223
|
+
if (!info) return "missing"
|
|
224
|
+
return [
|
|
225
|
+
info.ino ?? "",
|
|
226
|
+
info.size ?? "",
|
|
227
|
+
info.mtimeMs ?? "",
|
|
228
|
+
info.ctimeMs ?? "",
|
|
229
|
+
].join(":")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function readAuthStamp(authPath, ops) {
|
|
233
|
+
try {
|
|
234
|
+
return authStamp(await ops.stat(authPath))
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error?.code === "ENOENT") return "missing"
|
|
237
|
+
throw error
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function readAuthSnapshot(authPath, ops) {
|
|
242
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
243
|
+
const before = await readAuthStamp(authPath, ops)
|
|
244
|
+
if (before === "missing") {
|
|
245
|
+
return { value: {}, stamp: before }
|
|
246
|
+
}
|
|
247
|
+
let raw
|
|
195
248
|
try {
|
|
196
|
-
|
|
197
|
-
await handle.writeFile(`${JSON.stringify(all, null, 2)}\n`, "utf8")
|
|
198
|
-
await handle.sync()
|
|
199
|
-
await handle.close()
|
|
200
|
-
handle = null
|
|
201
|
-
await ops.rename(tmp, authPath)
|
|
202
|
-
await ops.chmod(authPath, 0o600)
|
|
249
|
+
raw = await ops.readFile(authPath, "utf8")
|
|
203
250
|
} catch (error) {
|
|
204
|
-
if (
|
|
205
|
-
await ops.unlink(tmp).catch(() => {})
|
|
251
|
+
if (error?.code === "ENOENT") return { value: {}, stamp: "missing" }
|
|
206
252
|
throw error
|
|
207
253
|
}
|
|
208
|
-
|
|
254
|
+
const after = await readAuthStamp(authPath, ops)
|
|
255
|
+
if (before === after) {
|
|
256
|
+
return { value: JSON.parse(raw), stamp: after }
|
|
257
|
+
}
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
259
|
+
}
|
|
260
|
+
throw new Error("magpie's sign-in file kept changing while it was being read")
|
|
209
261
|
}
|
|
210
262
|
|
|
211
263
|
export function authIdentity(auth, key) {
|
|
@@ -215,6 +267,7 @@ export function authIdentity(auth, key) {
|
|
|
215
267
|
}
|
|
216
268
|
|
|
217
269
|
export function resultFields(result, config, model = "") {
|
|
270
|
+
const inputTokens = config?.probeMode === "context" ? Number(config.inputTokens) || 0 : 0
|
|
218
271
|
switch (result?.kind) {
|
|
219
272
|
case "skipped":
|
|
220
273
|
if (result.reason === "done-today") {
|
|
@@ -271,7 +324,7 @@ export function resultFields(result, config, model = "") {
|
|
|
271
324
|
: "exhausted",
|
|
272
325
|
attempts: result.attempts,
|
|
273
326
|
model: result.result?.model || model,
|
|
274
|
-
inputTokens
|
|
327
|
+
inputTokens,
|
|
275
328
|
retry: !result.ok && !terminal && result.attempts < config.maxAttempts,
|
|
276
329
|
persisted: result.persisted !== false,
|
|
277
330
|
error: result.error,
|
|
@@ -364,6 +417,8 @@ export async function sendWithCandidates({
|
|
|
364
417
|
protocol: candidate.npm === "@ai-sdk/anthropic" ? "anthropic" : adapter.protocol,
|
|
365
418
|
loader,
|
|
366
419
|
inputTokens: config.inputTokens,
|
|
420
|
+
probeMode: config.probeMode,
|
|
421
|
+
probeMessage: config.probeMessage,
|
|
367
422
|
timeoutMs: config.timeoutMs,
|
|
368
423
|
day,
|
|
369
424
|
key: accountTag(identity),
|
|
@@ -473,7 +528,8 @@ export function createProviderKeepalive(options = {}) {
|
|
|
473
528
|
run,
|
|
474
529
|
model: candidates[0].id,
|
|
475
530
|
models: candidates.map((candidate) => candidate.id).join(","),
|
|
476
|
-
inputTokens: config.inputTokens,
|
|
531
|
+
inputTokens: config.probeMode === "context" ? config.inputTokens : 0,
|
|
532
|
+
probeMode: config.probeMode,
|
|
477
533
|
keepalive: true,
|
|
478
534
|
})
|
|
479
535
|
|
|
@@ -482,7 +538,8 @@ export function createProviderKeepalive(options = {}) {
|
|
|
482
538
|
account: accountTag(`${provider}|${identity}`),
|
|
483
539
|
day,
|
|
484
540
|
model: candidates[0].id,
|
|
485
|
-
inputTokens: config.inputTokens,
|
|
541
|
+
inputTokens: config.probeMode === "context" ? config.inputTokens : 0,
|
|
542
|
+
probeMode: config.probeMode,
|
|
486
543
|
maxAttempts: config.maxAttempts,
|
|
487
544
|
lockWaitMs: config.lockWaitMs,
|
|
488
545
|
timeoutMs: config.timeoutMs,
|
package/ui/app.js
CHANGED
|
@@ -996,6 +996,7 @@
|
|
|
996
996
|
["供应商", record?.provider],
|
|
997
997
|
["账号", record?.account],
|
|
998
998
|
["模型", record?.model],
|
|
999
|
+
["保活方式", record?.probeMode === "context" ? "长上下文" : record?.probeMode === "short" ? "短探活" : ""],
|
|
999
1000
|
["输入", number(record?.inputTokens) ? `${formatNumber(record.inputTokens)} tokens` : ""],
|
|
1000
1001
|
["本次积分", number(record?.credit) ? `+${formatNumber(record.credit)}` : ""],
|
|
1001
1002
|
["连续签到", number(record?.streak) ? `${formatNumber(record.streak)} 天` : ""],
|
|
@@ -1192,7 +1193,8 @@
|
|
|
1192
1193
|
keepalive: "真实保活",
|
|
1193
1194
|
all: "签到 + 真实保活",
|
|
1194
1195
|
}[options.scheduleAction] || "签到"
|
|
1195
|
-
|
|
1196
|
+
const probe = options.probeMode === "context" ? "长上下文保活" : "短探活"
|
|
1197
|
+
return `每 ${number(options.scheduleMinutes) || 60} 分钟执行一次${action};${probe}。`
|
|
1196
1198
|
}
|
|
1197
1199
|
|
|
1198
1200
|
async function loadSchedule(showToast = false, refresh = false) {
|
|
@@ -1210,6 +1212,7 @@
|
|
|
1210
1212
|
$("schedule-action").value = ["checkin", "keepalive", "all"].includes(options.scheduleAction)
|
|
1211
1213
|
? options.scheduleAction
|
|
1212
1214
|
: "checkin"
|
|
1215
|
+
$("probe-mode").value = options.probeMode === "context" ? "context" : "short"
|
|
1213
1216
|
$("schedule-status").textContent = scheduleDescription(options)
|
|
1214
1217
|
if (showToast) toast("定时计划已保存")
|
|
1215
1218
|
} catch (error) {
|
|
@@ -1225,6 +1228,7 @@
|
|
|
1225
1228
|
scheduleEnabled: $("schedule-enabled").checked,
|
|
1226
1229
|
scheduleMinutes: minutes,
|
|
1227
1230
|
scheduleAction: $("schedule-action").value,
|
|
1231
|
+
probeMode: $("probe-mode").value,
|
|
1228
1232
|
})))
|
|
1229
1233
|
await loadSchedule(true)
|
|
1230
1234
|
}
|
package/ui/index.html
CHANGED
|
@@ -108,6 +108,17 @@
|
|
|
108
108
|
<path d="m4 6 4 4 4-4"></path>
|
|
109
109
|
</svg>
|
|
110
110
|
</label>
|
|
111
|
+
<label class="select-field probe-field"
|
|
112
|
+
title="短探活只发送一条短消息;长上下文会填充到配置的输入规模">
|
|
113
|
+
<select id="probe-mode" aria-label="保活方式">
|
|
114
|
+
<option value="short">短探活</option>
|
|
115
|
+
<option value="context">长上下文</option>
|
|
116
|
+
</select>
|
|
117
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
118
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
119
|
+
<path d="m4 6 4 4 4-4"></path>
|
|
120
|
+
</svg>
|
|
121
|
+
</label>
|
|
111
122
|
<button class="button schedule-save" id="schedule-save" type="button">保存计划</button>
|
|
112
123
|
</div>
|
|
113
124
|
</section>
|
package/ui/style.css
CHANGED