quota-keepalive 0.1.0 → 0.1.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 +77 -9
- 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.md
CHANGED
|
@@ -4,12 +4,22 @@
|
|
|
4
4
|
插件不接管任何供应商的登录,也不会把页面伪装成单一供应商专属功能;登录、模型、
|
|
5
5
|
额度和签到 Hook 仍由各供应商插件维护,本插件负责统一检测、调度、执行和展示。
|
|
6
6
|
|
|
7
|
+
> **非官方插件声明**
|
|
8
|
+
>
|
|
9
|
+
> 本项目不是 Magpie 官方插件。签到优先调用供应商插件公开的签到接口;真实对话
|
|
10
|
+
> 保活不是供应商授权或保证支持的功能。保活请求通过本机已登录的供应商签名适配器
|
|
11
|
+
> 直接发往供应商,不经过第三方服务器,也不会导出令牌,因此降低了凭据泄露风险,
|
|
12
|
+
> 但不等于厂商风控风险为零。自动化合成消息仍可能被供应商识别为异常流量,并触发
|
|
13
|
+
> 限流、额度冻结、账号限制或封禁。默认关闭保活,仅在理解风险后按需启用,并由
|
|
14
|
+
> 使用者自行承担相关后果。
|
|
15
|
+
|
|
7
16
|
## 功能
|
|
8
17
|
|
|
9
18
|
- 一键检测 Magpie 当前已登录的供应商、账号、模型和额度窗口。
|
|
10
19
|
- 一键签到:通过各供应商登录插件公开的 `auth.checkin` 接口执行,尊重供应商、
|
|
11
20
|
账号两级启停;页面会明确显示未适配或未登录的账号,不会静默跳过。
|
|
12
21
|
- 真实对话保活:通过各供应商自己的登录插件取得签名请求,发送一条真实消息。
|
|
22
|
+
默认使用“短探活”,只发送一条短消息;需要长上下文时才显式切换“长上下文”模式。
|
|
13
23
|
- 模型选择:只从该账号实际列出的模型中选择。默认优先近期真实保活成功过的模型,
|
|
14
24
|
再按成功率、成功时间和失败次数排序;没有历史记录时沿用供应商自己的模型顺序,
|
|
15
25
|
不再按免费或价格抢先选择。第一个模型不可用时自动换下一档,全部不可用才记失败。
|
|
@@ -39,18 +49,50 @@
|
|
|
39
49
|
|
|
40
50
|
## 安装
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
npm 安装(推荐,支持 Magpie 自动检查更新):
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
magpie plugin add quota-keepalive
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
安装后的插件规格会显示为 `quota-keepalive@latest`。Magpie 会在启动后和之后每小时
|
|
59
|
+
检查一次更新,并在插件页面提示新版本。更新方式:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# 更新全部 npm/git 插件
|
|
63
|
+
magpie plugin update
|
|
64
|
+
|
|
65
|
+
# 只更新这个插件
|
|
66
|
+
magpie plugin add quota-keepalive
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
页面中的 `Update all` 与 `magpie plugin update` 等价。重新添加同一个 npm 包会替换
|
|
70
|
+
旧版本,并保留插件 options、启用/停用状态和供应商偏好。已固定版本
|
|
71
|
+
`quota-keepalive@0.1.x` 也会在更新时升级到最新版并解除固定。
|
|
72
|
+
|
|
73
|
+
从 GitHub 安装(不经过 npm):
|
|
43
74
|
|
|
44
75
|
```sh
|
|
45
76
|
magpie plugin add github:sljdxde/quota-keepalive
|
|
46
77
|
```
|
|
47
78
|
|
|
48
|
-
|
|
79
|
+
从本地目录安装(仅用于开发调试):
|
|
49
80
|
|
|
50
81
|
```sh
|
|
51
82
|
magpie plugin add "$(pwd)"
|
|
52
83
|
```
|
|
53
84
|
|
|
85
|
+
目录安装会直接读取当前源码,适合修改后立即调试,但不会进入 npm 的版本检查,也不会
|
|
86
|
+
被 `magpie plugin update` 更新。开发完成后应切换到 npm 安装。切换前先保存 npm
|
|
87
|
+
条目中的 options,再停用目录条目:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
magpie plugin add quota-keepalive
|
|
91
|
+
magpie plugin options quota-keepalive '<原来的 options JSON>'
|
|
92
|
+
magpie plugin off '<目录插件的 spec>'
|
|
93
|
+
magpie plugin rm '<目录插件的 spec>'
|
|
94
|
+
```
|
|
95
|
+
|
|
54
96
|
插件不需要接管 provider。供应商登录仍由 Magpie 已安装的对应社区插件提供;本插件
|
|
55
97
|
直接从 Magpie 的插件目录加载这些签名适配器,因此 Magpie 或供应商插件更新后,
|
|
56
98
|
只要其公开接口没有变化,本插件仍可继续使用。若供应商包改名为不兼容的接口,页面
|
|
@@ -59,12 +101,28 @@ magpie plugin add "$(pwd)"
|
|
|
59
101
|
安装后在 Magpie 左侧 `Plugins` 分组打开“额度签到”。如果页面文件刚更新,
|
|
60
102
|
重新打开一次插件页即可刷新清单,不需要重启 Magpie。
|
|
61
103
|
|
|
62
|
-
|
|
104
|
+
### 发布新版本
|
|
105
|
+
|
|
106
|
+
仓库包含 `.github/workflows/publish.yml`,使用 npm Trusted Publishing,不需要把
|
|
107
|
+
npm token 写进仓库或 GitHub Secrets:
|
|
108
|
+
|
|
109
|
+
1. 更新 `package.json` 版本并提交改动。
|
|
110
|
+
2. 创建同名 Git tag,例如版本 `0.1.2` 对应 `v0.1.2`。
|
|
111
|
+
3. 推送 `main` 和 tag。
|
|
112
|
+
4. 在 GitHub 创建并发布对应的 Release。
|
|
113
|
+
5. Release 发布后,工作流会重新跑测试、语法检查和 `npm pack`,再通过 OIDC
|
|
114
|
+
发布到 npm。
|
|
115
|
+
6. 本机等待 Magpie 的更新提示,或执行 `magpie plugin add quota-keepalive` /
|
|
116
|
+
`magpie plugin update`。
|
|
117
|
+
|
|
118
|
+
本地手动发布时,npm 如果启用了双因素认证,需要附加当前动态码:
|
|
63
119
|
|
|
64
120
|
```sh
|
|
65
|
-
|
|
121
|
+
npm publish --access public --otp=<6 位动态码>
|
|
66
122
|
```
|
|
67
123
|
|
|
124
|
+
不要把动态码、npm token 或恢复码写进仓库、README 或聊天记录。
|
|
125
|
+
|
|
68
126
|
## 页面操作
|
|
69
127
|
|
|
70
128
|
- `一键检测`:刷新供应商、账号、模型和额度快照。
|
|
@@ -76,7 +134,10 @@ magpie plugin add quota-keepalive
|
|
|
76
134
|
不会因为快速连续点击触发“每秒只能写入一次”的报错。
|
|
77
135
|
- 圆形刷新按钮:立即刷新全部启用供应商额度;供应商卡片内的刷新按钮只刷新该供应商。
|
|
78
136
|
- `保存计划`:设置统一执行间隔和任务类型。计划默认关闭,防止无意间消耗积分。
|
|
79
|
-
-
|
|
137
|
+
- `短探活/长上下文`:短探活风险、耗时和额度消耗更低;长上下文会按 `inputTokens`
|
|
138
|
+
填充请求,适合明确需要维持长上下文窗口的场景。
|
|
139
|
+
- 定时计划保存或修改后,从下一个间隔开始执行;下次执行时间会持久化,Magpie 重启或
|
|
140
|
+
电脑休眠后会补跑已经到期的任务。需要立即执行时使用页面顶部的手动按钮。
|
|
80
141
|
|
|
81
142
|
页面每 5 秒读取一次本地快照;插件后台每 1 分钟刷新启用中供应商的额度,停用项会
|
|
82
143
|
跳过。额度刷新仍写入后台诊断日志,但不会出现在页面的签到/保活执行记录中。
|
|
@@ -120,6 +181,8 @@ magpie plugin options quota-keepalive '{
|
|
|
120
181
|
"codearts": "deepseek-v4.1-flash"
|
|
121
182
|
},
|
|
122
183
|
"modelCandidates": 4,
|
|
184
|
+
"probeMode": "short",
|
|
185
|
+
"probeMessage": "Magpie daily connectivity check. Reply with OK.",
|
|
123
186
|
"inputTokens": 25000,
|
|
124
187
|
"maxAttempts": 3,
|
|
125
188
|
"timeoutMs": 90000,
|
|
@@ -136,21 +199,25 @@ magpie plugin options quota-keepalive '{
|
|
|
136
199
|
WorkBuddy CN 默认优先 `deepseek-v4.1-flash`,这是 dsh 实测较稳定的真实对话模型;
|
|
137
200
|
其他供应商按近 5 天可用性排序,没有成功记录时按供应商模型顺序选择,不按免费或倍率抢先选择。
|
|
138
201
|
- `modelCandidates`:每个账号每次最多尝试的模型数,默认 4,范围 1–12。
|
|
139
|
-
- `
|
|
202
|
+
- `probeMode`:`short`(默认)只发一条短消息;`context` 才使用长上下文填充。
|
|
203
|
+
- `probeMessage`:短探活消息,最多 500 字符;留空时使用默认连通性检查文本。
|
|
204
|
+
- `inputTokens`:仅 `probeMode=context` 时生效,默认 25000,范围 1000–25000。
|
|
140
205
|
- `maxAttempts`:每个账号每个北京时间日最多保活尝试轮数,默认 3。
|
|
141
206
|
- `timeoutMs`:单个模型请求的整体截止时间,默认 90 秒,范围 10–180 秒。WorkBuddy
|
|
142
207
|
CN 的 25k 输入请求可能先返回响应头、再继续流式读取;90 秒比 25 秒更接近 dsh 的
|
|
143
208
|
probe 容忍度,同时仍避免任务无限挂起。即使供应商
|
|
144
209
|
登录插件内部忽略中止信号,外层也会结束该候选并继续回退;账号较多时不要调得过大。
|
|
145
210
|
- `logMaxBytes`:单个日志上限,默认 2 MiB,超出后轮转一份 `.1`。
|
|
146
|
-
- `statePath`、`logPath`、`snapshotPath`、`commandPath`:可选绝对路径。
|
|
211
|
+
- `statePath`、`logPath`、`snapshotPath`、`commandPath`、`schedulePath`:可选绝对路径。
|
|
147
212
|
|
|
148
213
|
## 执行周期与失败
|
|
149
214
|
|
|
150
215
|
Magpie 自己的每日签到调度仍会在启动后约 2 分钟进行首轮,之后约每 30 分钟巡检;
|
|
151
216
|
失败约 30 分钟后重试。本插件的定时任务是额外的统一调度,默认关闭,适合需要更
|
|
152
217
|
频繁签到或保活的场景。本插件的定时任务不会在保存后立即执行,也不会在修改后
|
|
153
|
-
重启 Magpie
|
|
218
|
+
重启 Magpie;修改配置会替换运行中的定时器,并从新的间隔重新计时。调度状态保存在
|
|
219
|
+
`quota-keepalive-schedule.json`,Magpie 重启后会读取上次执行时间和下次执行时间,
|
|
220
|
+
已到期的任务会补跑,不会因为反复重启而一直错过执行。
|
|
154
221
|
|
|
155
222
|
保活失败会按 `maxAttempts` 重试;达到上限后当天不再自动发送。网络失败、登录过期、
|
|
156
223
|
模型不可用和空回复都会进入日志。遇到明确的 HTTP 402/429 额度耗尽或免费额度用尽时,
|
|
@@ -180,7 +247,8 @@ node ./bin/daily-keepalive-log.js --json
|
|
|
180
247
|
## 安全
|
|
181
248
|
|
|
182
249
|
- 状态和日志文件以 `0600` 创建,写入使用临时文件和原子 rename。
|
|
183
|
-
- `plugin-auth.json`
|
|
250
|
+
- `plugin-auth.json` 的更新使用短锁、文件指纹校验和冲突重试。若 Magpie 或其他
|
|
251
|
+
供应商插件在读取后改动了凭据文件,本插件会重新读取并合并,不会直接覆盖新内容。
|
|
184
252
|
- 模型回复不会写入状态或日志。
|
|
185
253
|
- 日志和页面显示会对令牌、Bearer、API key 和账号标识做脱敏。
|
|
186
254
|
- 未安装对应供应商插件、未登录或没有可用模型时,会明确记为跳过或失败。
|
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.2",
|
|
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