quota-keepalive 0.1.7 → 0.1.8
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 +22 -10
- package/README.md +19 -12
- package/package.json +1 -1
- package/src/config.js +4 -2
- package/src/keepalive.js +132 -13
- package/src/log.js +1 -0
- package/src/provider-chat.js +1 -1
- package/src/snapshot.js +1 -1
- package/src/workbuddy.js +15 -5
- package/ui/app.js +9 -4
- package/ui/index.html +3 -3
package/README.en.md
CHANGED
|
@@ -44,8 +44,10 @@ hook. This plugin only coordinates those capabilities and presents the results.
|
|
|
44
44
|
- Respect provider-level and account-level switches; unsupported or signed-out
|
|
45
45
|
accounts are reported explicitly instead of being silently skipped.
|
|
46
46
|
- Send real-chat keepalive through each provider's signed adapter.
|
|
47
|
-
- Use
|
|
48
|
-
|
|
47
|
+
- Use a real activity conversation by default. For WorkBuddy CN this follows
|
|
48
|
+
dsh's measured probe shape: about 25k input tokens and one output token, which
|
|
49
|
+
consumes credits. Use the short probe only when a low-cost connectivity check
|
|
50
|
+
is enough.
|
|
49
51
|
- Prefer models that recently completed a real keepalive successfully, then fall
|
|
50
52
|
back by success rate, recency, and known failures. Provider model order is used
|
|
51
53
|
when there is no history.
|
|
@@ -132,7 +134,9 @@ enable automatic keepalive by default.
|
|
|
132
134
|
- **Refresh quota**: refresh all enabled providers; each provider row also has a
|
|
133
135
|
provider-only refresh button.
|
|
134
136
|
- **Save schedule**: set the shared interval and action type.
|
|
135
|
-
- **
|
|
137
|
+
- **Real activity / Short probe**: real activity fills the configured input and
|
|
138
|
+
can leave an activity record, but consumes credits. A short probe is cheaper
|
|
139
|
+
and faster, but does not guarantee an activity record.
|
|
136
140
|
|
|
137
141
|
Saving a schedule starts the next run after the configured interval. The next run
|
|
138
142
|
time is persisted, and a task already due when Magpie starts is run immediately.
|
|
@@ -166,10 +170,12 @@ Keepalive does not blindly choose the cheapest or a free model:
|
|
|
166
170
|
6. Fall back to the next candidate on a missing model, credit rejection, upstream
|
|
167
171
|
outage, or empty reply.
|
|
168
172
|
|
|
169
|
-
For WorkBuddy CN, the
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
login pages, and timeouts are
|
|
173
|
+
For WorkBuddy CN, the real activity path follows dsh probe semantics and sends
|
|
174
|
+
about 25k input tokens with a one-token output cap. Activity mode requires either
|
|
175
|
+
visible model content or usage/credit/prompt-token evidence in the response. A bare
|
|
176
|
+
`[DONE]`, an empty 200, JSON business errors, HTML login pages, and timeouts are
|
|
177
|
+
failures. The short-probe mode remains available for low-cost checks, but it does
|
|
178
|
+
not claim that an activity record was created.
|
|
173
179
|
|
|
174
180
|
## Configuration
|
|
175
181
|
|
|
@@ -185,7 +191,7 @@ magpie plugin options quota-keepalive '{
|
|
|
185
191
|
"codearts": "deepseek-v4.1-flash"
|
|
186
192
|
},
|
|
187
193
|
"modelCandidates": 4,
|
|
188
|
-
"probeMode": "
|
|
194
|
+
"probeMode": "activity",
|
|
189
195
|
"probeMessage": "Magpie daily connectivity check. Reply with OK.",
|
|
190
196
|
"inputTokens": 25000,
|
|
191
197
|
"maxAttempts": 3,
|
|
@@ -200,15 +206,21 @@ magpie plugin options quota-keepalive '{
|
|
|
200
206
|
- `model`: optional preferred model; it is not a hard allowlist.
|
|
201
207
|
- `modelPreferences`: preferred model per provider.
|
|
202
208
|
- `modelCandidates`: maximum fallback candidates per account, default `4`.
|
|
203
|
-
- `probeMode`: `
|
|
209
|
+
- `probeMode`: `activity` by default and sends the configured long activity input;
|
|
210
|
+
`short` sends only a short message; `context` is the backwards-compatible alias
|
|
211
|
+
for `activity`.
|
|
204
212
|
- `probeMessage`: short probe text, up to 500 characters.
|
|
205
|
-
- `inputTokens`: long-
|
|
213
|
+
- `inputTokens`: long-input size for `activity`/`context`, default 25000.
|
|
206
214
|
- `maxAttempts`: maximum keepalive attempts per account per Beijing day.
|
|
207
215
|
- `timeoutMs`: overall timeout for one model request, default 90 seconds.
|
|
208
216
|
- `logMaxBytes`: rotate one previous log after this size, default 2 MiB.
|
|
209
217
|
- `statePath`, `logPath`, `snapshotPath`, `commandPath`, `schedulePath`: optional
|
|
210
218
|
absolute paths.
|
|
211
219
|
|
|
220
|
+
Upgrade note: an old installation that explicitly saved `probeMode: "short"` keeps
|
|
221
|
+
that choice. Select **Real activity** and save the schedule (or update plugin
|
|
222
|
+
options) when you want WorkBuddy activity records to be written.
|
|
223
|
+
|
|
212
224
|
## Logs
|
|
213
225
|
|
|
214
226
|
The default log is:
|
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@ quota-keepalive 不接管任何供应商的登录,也不会把页面伪装成
|
|
|
34
34
|
- 一键签到:通过各供应商登录插件公开的 `auth.checkin` 接口执行,尊重供应商、
|
|
35
35
|
账号两级启停;页面会明确显示未适配或未登录的账号,不会静默跳过。
|
|
36
36
|
- 真实对话保活:通过各供应商自己的登录插件取得签名请求,发送一条真实消息。
|
|
37
|
-
|
|
37
|
+
默认使用“真实活跃对话”,WorkBuddy CN 会按 dsh 实测语义填充约 25k 输入 token
|
|
38
|
+
并消耗额度;只想做低成本连通性检查时才切换到“短探活”。
|
|
38
39
|
- 模型选择:只从该账号实际列出的模型中选择。默认优先近期真实保活成功过的模型,
|
|
39
40
|
再按成功率、成功时间和失败次数排序;没有历史记录时沿用供应商自己的模型顺序,
|
|
40
41
|
不再按免费或价格抢先选择。第一个模型不可用时自动换下一档,全部不可用才记失败。
|
|
@@ -149,8 +150,8 @@ npm publish --access public --otp=<6 位动态码>
|
|
|
149
150
|
不会因为快速连续点击触发“每秒只能写入一次”的报错。
|
|
150
151
|
- `刷新额度`按钮:立即刷新全部启用供应商额度;供应商卡片内的刷新按钮只刷新该供应商。
|
|
151
152
|
- `保存计划`:设置统一执行间隔和任务类型。计划默认关闭,防止无意间消耗积分。
|
|
152
|
-
-
|
|
153
|
-
|
|
153
|
+
- `真实活跃对话/短探活`:真实活跃对话按 `inputTokens` 填充请求,能增加上活跃记录
|
|
154
|
+
的概率并可能消耗积分;短探活风险、耗时和额度消耗更低,但不保证供应商记录为一次对话。
|
|
154
155
|
- 定时计划保存或修改后,从下一个间隔开始执行;下次执行时间会持久化,Magpie 重启或
|
|
155
156
|
电脑休眠后会补跑已经到期的任务。需要立即执行时使用页面顶部的手动按钮。
|
|
156
157
|
|
|
@@ -170,9 +171,10 @@ npm publish --access public --otp=<6 位动态码>
|
|
|
170
171
|
3. 账号没有记录时,优先该供应商其他账号成功过的模型;仍无记录则跟随供应商模型列表顺序。
|
|
171
172
|
4. 已知失败的模型排在未验证模型之后;单个账号最多尝试配置数量的候选模型。
|
|
172
173
|
5. 模型不存在、额度拒绝、上游不可用或空回复时切换下一个候选。
|
|
173
|
-
6. WorkBuddy CN
|
|
174
|
-
|
|
175
|
-
HTML
|
|
174
|
+
6. WorkBuddy CN 的真实活跃对话按 dsh 的 probe 语义发送约 25k 输入和 1 个输出 token。
|
|
175
|
+
活动模式要求响应里出现模型正文,或 usage/credit/prompt token 证据;只有 `[DONE]`、
|
|
176
|
+
空 200、JSON 业务错误、HTML 登录页和超时都不算成功。短探活仍保留低成本兼容模式,
|
|
177
|
+
但不会声称已经留下真实活跃记录。
|
|
176
178
|
|
|
177
179
|
账号卡片会优先显示最近一次真实保活实际使用的模型(`实际模型`);没有执行历史时,
|
|
178
180
|
依次显示用户指定的 `所选模型` 或自动排序出的 `自动首选`。供应商设置行也会汇总
|
|
@@ -196,7 +198,7 @@ magpie plugin options quota-keepalive '{
|
|
|
196
198
|
"codearts": "deepseek-v4.1-flash"
|
|
197
199
|
},
|
|
198
200
|
"modelCandidates": 4,
|
|
199
|
-
"probeMode": "
|
|
201
|
+
"probeMode": "activity",
|
|
200
202
|
"probeMessage": "Magpie daily connectivity check. Reply with OK.",
|
|
201
203
|
"inputTokens": 25000,
|
|
202
204
|
"maxAttempts": 3,
|
|
@@ -214,9 +216,10 @@ magpie plugin options quota-keepalive '{
|
|
|
214
216
|
WorkBuddy CN 默认优先 `deepseek-v4.1-flash`,这是 dsh 实测较稳定的真实对话模型;
|
|
215
217
|
其他供应商按近 5 天可用性排序,没有成功记录时按供应商模型顺序选择,不按免费或倍率抢先选择。
|
|
216
218
|
- `modelCandidates`:每个账号每次最多尝试的模型数,默认 4,范围 1–12。
|
|
217
|
-
- `probeMode`:`short
|
|
219
|
+
- `probeMode`:`activity`(默认)发送真实活跃对话,约 25k 输入;`short` 只发短消息;
|
|
220
|
+
`context` 是旧版配置的兼容别名,行为与 `activity` 相同。
|
|
218
221
|
- `probeMessage`:短探活消息,最多 500 字符;留空时使用默认连通性检查文本。
|
|
219
|
-
- `inputTokens
|
|
222
|
+
- `inputTokens`:`activity`/`context` 模式的长输入规模,默认 25000,范围 1000–25000。
|
|
220
223
|
- `maxAttempts`:每个账号每个北京时间日最多保活尝试轮数,默认 3。
|
|
221
224
|
- `timeoutMs`:单个模型请求的整体截止时间,默认 90 秒,范围 10–180 秒。WorkBuddy
|
|
222
225
|
CN 的 25k 输入请求可能先返回响应头、再继续流式读取;90 秒比 25 秒更接近 dsh 的
|
|
@@ -225,6 +228,10 @@ magpie plugin options quota-keepalive '{
|
|
|
225
228
|
- `logMaxBytes`:单个日志上限,默认 2 MiB,超出后轮转一份 `.1`。
|
|
226
229
|
- `statePath`、`logPath`、`snapshotPath`、`commandPath`、`schedulePath`:可选绝对路径。
|
|
227
230
|
|
|
231
|
+
升级提示:旧版本如果已经显式保存过 `probeMode: "short"`,新版本会尊重这项用户设置,
|
|
232
|
+
不会静默改成 `activity`。需要点亮 WorkBuddy 活跃记录时,请在页面选择“真实活跃对话”
|
|
233
|
+
并保存计划,或者执行 `magpie plugin options quota-keepalive` 更新配置。
|
|
234
|
+
|
|
228
235
|
## 执行周期与失败
|
|
229
236
|
|
|
230
237
|
Magpie 自己的每日签到调度仍会在启动后约 2 分钟进行首轮,之后约每 30 分钟巡检;
|
|
@@ -236,9 +243,9 @@ Magpie 自己的每日签到调度仍会在启动后约 2 分钟进行首轮,
|
|
|
236
243
|
|
|
237
244
|
保活失败会按 `maxAttempts` 重试;达到上限后当天不再自动发送。网络失败、登录过期、
|
|
238
245
|
模型不可用和空回复都会进入日志。遇到明确的 HTTP 402/429 额度耗尽或免费额度用尽时,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
246
|
+
当天停止自动重试,避免持续发送无效请求。当天已经成功后,自动任务和手动触发都会
|
|
247
|
+
跳过,不会重复发送;手动“发送保活”只能在当天尚未成功时重试失败账号。供应商整体
|
|
248
|
+
停用时,账号级手动按钮也会禁用。
|
|
242
249
|
|
|
243
250
|
## 日志
|
|
244
251
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -19,7 +19,7 @@ export const DEFAULT_KEEPALIVE = Object.freeze({
|
|
|
19
19
|
model: "",
|
|
20
20
|
modelCandidates: 4,
|
|
21
21
|
inputTokens: 25_000,
|
|
22
|
-
probeMode: "
|
|
22
|
+
probeMode: "activity",
|
|
23
23
|
probeMessage: DEFAULT_PROBE_MESSAGE,
|
|
24
24
|
maxAttempts: 3,
|
|
25
25
|
scheduleEnabled: false,
|
|
@@ -75,7 +75,9 @@ export function resolveKeepaliveOptions(raw) {
|
|
|
75
75
|
scheduleMinutes: integerIn(source.scheduleMinutes, DEFAULT_KEEPALIVE.scheduleMinutes, 5, 10_080),
|
|
76
76
|
configHome: absolutePath(source.configHome, undefined),
|
|
77
77
|
inputTokens: integerIn(source.inputTokens, DEFAULT_KEEPALIVE.inputTokens, 1_000, 25_000),
|
|
78
|
-
probeMode:
|
|
78
|
+
probeMode: ["short", "context", "activity"].includes(source.probeMode)
|
|
79
|
+
? source.probeMode
|
|
80
|
+
: DEFAULT_KEEPALIVE.probeMode,
|
|
79
81
|
probeMessage: probeMessage || DEFAULT_PROBE_MESSAGE,
|
|
80
82
|
maxAttempts: integerIn(source.maxAttempts, DEFAULT_KEEPALIVE.maxAttempts, 1, 10),
|
|
81
83
|
timeoutMs: integerIn(source.timeoutMs, DEFAULT_KEEPALIVE.timeoutMs, 10_000, 180_000),
|
package/src/keepalive.js
CHANGED
|
@@ -4,6 +4,9 @@ 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
|
+
const DEFAULT_ACTIVITY_INPUT_TOKENS = 25_000
|
|
8
|
+
const ACTIVITY_PROMPT = "Reply with a single word."
|
|
9
|
+
const ACTIVITY_SYSTEM_PROMPT = "You are a connectivity check. Reply with a single word."
|
|
7
10
|
export const DEFAULT_PROBE_MESSAGE = "Magpie daily connectivity check. Reply with OK."
|
|
8
11
|
|
|
9
12
|
function probeMessage(value) {
|
|
@@ -18,18 +21,26 @@ export function buildKeepaliveBody({
|
|
|
18
21
|
probeMessage: message = "",
|
|
19
22
|
}) {
|
|
20
23
|
const prompt = `${probeMessage(message)} Today is ${day}.`
|
|
24
|
+
const activity = probeMode === "activity"
|
|
25
|
+
const context = activity || probeMode === "context"
|
|
21
26
|
let content = prompt
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
let system = "You are a concise connectivity check. Reply with a single word."
|
|
28
|
+
if (context) {
|
|
29
|
+
const tokens = Number(inputTokens) || (activity ? DEFAULT_ACTIVITY_INPUT_TOKENS : 0)
|
|
30
|
+
const targetChars = Math.ceil(tokens * CHARS_PER_TOKEN)
|
|
24
31
|
const filler = FILLER.repeat(Math.ceil(targetChars / FILLER.length))
|
|
25
|
-
|
|
32
|
+
// WorkBuddy's CN gateway measures a real request by its input size. The
|
|
33
|
+
// activity shape mirrors dsh's probe body so the request is recorded as
|
|
34
|
+
// a conversation instead of a short connectivity check.
|
|
35
|
+
content = activity ? `${filler}${ACTIVITY_PROMPT}` : `${filler}${prompt}`
|
|
36
|
+
if (activity) system = ACTIVITY_SYSTEM_PROMPT
|
|
26
37
|
}
|
|
27
38
|
return {
|
|
28
39
|
model,
|
|
29
40
|
messages: [
|
|
30
41
|
{
|
|
31
42
|
role: "system",
|
|
32
|
-
content:
|
|
43
|
+
content: system,
|
|
33
44
|
},
|
|
34
45
|
{
|
|
35
46
|
role: "user",
|
|
@@ -53,6 +64,64 @@ function parsedJson(value) {
|
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
function numberValue(value) {
|
|
68
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : undefined
|
|
69
|
+
if (typeof value !== "string" || !value.trim()) return undefined
|
|
70
|
+
const n = Number(value.trim())
|
|
71
|
+
return Number.isFinite(n) ? n : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasKey(value, name) {
|
|
75
|
+
return object(value) && Object.prototype.hasOwnProperty.call(value, name)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function numberField(value, names) {
|
|
79
|
+
for (const name of names) {
|
|
80
|
+
if (!hasKey(value, name)) continue
|
|
81
|
+
const n = numberValue(value[name])
|
|
82
|
+
if (n !== undefined) return n
|
|
83
|
+
}
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function usageEvidence(payloads) {
|
|
88
|
+
const result = {
|
|
89
|
+
sawUsage: false,
|
|
90
|
+
credit: undefined,
|
|
91
|
+
promptTokens: undefined,
|
|
92
|
+
completionTokens: undefined,
|
|
93
|
+
totalTokens: undefined,
|
|
94
|
+
}
|
|
95
|
+
for (const payload of Array.isArray(payloads) ? payloads : []) {
|
|
96
|
+
const candidates = [
|
|
97
|
+
payload,
|
|
98
|
+
payload?.usage,
|
|
99
|
+
payload?.data?.usage,
|
|
100
|
+
payload?.data,
|
|
101
|
+
].filter(object)
|
|
102
|
+
for (const candidate of candidates) {
|
|
103
|
+
const credit = numberField(candidate, ["credit", "credits"])
|
|
104
|
+
const promptTokens = numberField(candidate, ["prompt_tokens", "promptTokens", "input_tokens", "inputTokens"])
|
|
105
|
+
const completionTokens = numberField(candidate, [
|
|
106
|
+
"completion_tokens",
|
|
107
|
+
"completionTokens",
|
|
108
|
+
"output_tokens",
|
|
109
|
+
"outputTokens",
|
|
110
|
+
])
|
|
111
|
+
const totalTokens = numberField(candidate, ["total_tokens", "totalTokens"])
|
|
112
|
+
if ([credit, promptTokens, completionTokens, totalTokens].every((value) => value === undefined)) {
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
result.sawUsage = true
|
|
116
|
+
result.credit ??= credit
|
|
117
|
+
result.promptTokens ??= promptTokens
|
|
118
|
+
result.completionTokens ??= completionTokens
|
|
119
|
+
result.totalTokens ??= totalTokens
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result
|
|
123
|
+
}
|
|
124
|
+
|
|
56
125
|
function businessFailure(body) {
|
|
57
126
|
if (!object(body)) return ""
|
|
58
127
|
if (body.success === false) {
|
|
@@ -71,6 +140,26 @@ function businessFailure(body) {
|
|
|
71
140
|
return ""
|
|
72
141
|
}
|
|
73
142
|
|
|
143
|
+
function contentValue(value) {
|
|
144
|
+
if (typeof value === "string") return value
|
|
145
|
+
if (Array.isArray(value)) return value.map((part) => contentValue(part?.text)).join("")
|
|
146
|
+
return ""
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function payloadReply(payload) {
|
|
150
|
+
const choices = Array.isArray(payload?.choices) ? payload.choices : []
|
|
151
|
+
for (const choice of choices) {
|
|
152
|
+
if (!object(choice)) continue
|
|
153
|
+
const value = choice.delta?.content ?? choice.message?.content ?? choice.text
|
|
154
|
+
const content = contentValue(value)
|
|
155
|
+
if (content) return content
|
|
156
|
+
}
|
|
157
|
+
const blocks = Array.isArray(payload?.content) ? payload.content : []
|
|
158
|
+
const blockText = blocks.map((block) => contentValue(block?.text)).join("")
|
|
159
|
+
if (blockText) return blockText
|
|
160
|
+
return contentValue(payload?.output_text)
|
|
161
|
+
}
|
|
162
|
+
|
|
74
163
|
export function responseMessage(raw) {
|
|
75
164
|
const body = parsedJson(raw)
|
|
76
165
|
if (object(body)) {
|
|
@@ -107,9 +196,10 @@ export function assistantReply(raw) {
|
|
|
107
196
|
choice.delta?.content ??
|
|
108
197
|
choice.message?.content ??
|
|
109
198
|
choice.text
|
|
110
|
-
|
|
199
|
+
const piece = contentValue(value)
|
|
200
|
+
if (piece) {
|
|
111
201
|
sawReply = true
|
|
112
|
-
content +=
|
|
202
|
+
content += piece
|
|
113
203
|
}
|
|
114
204
|
}
|
|
115
205
|
}
|
|
@@ -121,7 +211,7 @@ export function assistantReply(raw) {
|
|
|
121
211
|
}
|
|
122
212
|
}
|
|
123
213
|
|
|
124
|
-
export function workbuddyProbeResult(response, raw) {
|
|
214
|
+
export function workbuddyProbeResult(response, raw, { probeMode = "short" } = {}) {
|
|
125
215
|
const source = text(raw)
|
|
126
216
|
const body = source.trim()
|
|
127
217
|
const contentType = text(response?.headers?.get?.("content-type")).toLowerCase()
|
|
@@ -153,13 +243,37 @@ export function workbuddyProbeResult(response, raw) {
|
|
|
153
243
|
}
|
|
154
244
|
}
|
|
155
245
|
|
|
156
|
-
// WorkBuddy can accept a real conversation without emitting assistant text:
|
|
157
|
-
// max_tokens is one and the signed gateway may finish with only [DONE].
|
|
158
|
-
// dsh treats HTTP 2xx as a successful probe for that reason.
|
|
159
246
|
const reply = assistantReply(source)
|
|
247
|
+
let content = reply.content
|
|
248
|
+
let hasReply = reply.ok
|
|
249
|
+
for (const payload of payloads) {
|
|
250
|
+
if (content) break
|
|
251
|
+
const direct = payloadReply(payload)
|
|
252
|
+
if (direct) {
|
|
253
|
+
content = direct
|
|
254
|
+
hasReply = true
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const evidence = usageEvidence(payloads)
|
|
259
|
+
const activity = probeMode === "activity" || probeMode === "context"
|
|
260
|
+
const activityVerified = activity && (hasReply || evidence.sawUsage)
|
|
261
|
+
if (activity && !activityVerified) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
content,
|
|
265
|
+
activityVerified: false,
|
|
266
|
+
error: "WorkBuddy's keepalive chat returned 200 without a model reply or usage evidence; no active conversation was confirmed",
|
|
267
|
+
}
|
|
268
|
+
}
|
|
160
269
|
return {
|
|
161
270
|
ok: true,
|
|
162
|
-
content:
|
|
271
|
+
content: sanitize(content, 120),
|
|
272
|
+
activityVerified,
|
|
273
|
+
...(evidence.credit === undefined ? {} : { credit: evidence.credit }),
|
|
274
|
+
...(evidence.promptTokens === undefined ? {} : { promptTokens: evidence.promptTokens }),
|
|
275
|
+
...(evidence.completionTokens === undefined ? {} : { completionTokens: evidence.completionTokens }),
|
|
276
|
+
...(evidence.totalTokens === undefined ? {} : { totalTokens: evidence.totalTokens }),
|
|
163
277
|
}
|
|
164
278
|
}
|
|
165
279
|
|
|
@@ -168,7 +282,7 @@ export async function sendKeepalive({
|
|
|
168
282
|
origin = ORIGIN,
|
|
169
283
|
model,
|
|
170
284
|
inputTokens,
|
|
171
|
-
probeMode,
|
|
285
|
+
probeMode = "activity",
|
|
172
286
|
probeMessage,
|
|
173
287
|
timeoutMs,
|
|
174
288
|
day,
|
|
@@ -207,12 +321,17 @@ export async function sendKeepalive({
|
|
|
207
321
|
error.retryToday = !terminal
|
|
208
322
|
throw error
|
|
209
323
|
}
|
|
210
|
-
const probe = workbuddyProbeResult(response, raw)
|
|
324
|
+
const probe = workbuddyProbeResult(response, raw, { probeMode })
|
|
211
325
|
if (!probe.ok) {
|
|
212
326
|
throw new Error(probe.error)
|
|
213
327
|
}
|
|
214
328
|
return {
|
|
215
329
|
model,
|
|
216
330
|
reply: probe.content,
|
|
331
|
+
activityVerified: probe.activityVerified,
|
|
332
|
+
...(probe.credit === undefined ? {} : { credit: probe.credit }),
|
|
333
|
+
...(probe.promptTokens === undefined ? {} : { promptTokens: probe.promptTokens }),
|
|
334
|
+
...(probe.completionTokens === undefined ? {} : { completionTokens: probe.completionTokens }),
|
|
335
|
+
...(probe.totalTokens === undefined ? {} : { totalTokens: probe.totalTokens }),
|
|
217
336
|
}
|
|
218
337
|
}
|
package/src/log.js
CHANGED
|
@@ -230,6 +230,7 @@ export function formatLogRecord(record) {
|
|
|
230
230
|
["probeMode", record.probeMode],
|
|
231
231
|
["inputTokens", record.inputTokens],
|
|
232
232
|
["credit", record.credit],
|
|
233
|
+
["activityVerified", record.activityVerified],
|
|
233
234
|
["streak", record.streak],
|
|
234
235
|
["durationMs", record.durationMs],
|
|
235
236
|
["retry", record.retry],
|
package/src/provider-chat.js
CHANGED
|
@@ -56,7 +56,7 @@ export function anthropicBody({
|
|
|
56
56
|
const message = sanitize(probeMessage, 500).trim() || DEFAULT_PROBE_MESSAGE
|
|
57
57
|
const prompt = `${message} Today is ${day}.`
|
|
58
58
|
let content = prompt
|
|
59
|
-
if (probeMode === "context") {
|
|
59
|
+
if (probeMode === "context" || probeMode === "activity") {
|
|
60
60
|
const targetChars = Math.ceil(inputTokens * CHARS_PER_TOKEN)
|
|
61
61
|
const filler = FILLER.repeat(Math.ceil(targetChars / FILLER.length))
|
|
62
62
|
content = `${filler}${prompt}`
|
package/src/snapshot.js
CHANGED
|
@@ -53,7 +53,7 @@ export function publicRecord(record) {
|
|
|
53
53
|
if (value) out[name] = value
|
|
54
54
|
}
|
|
55
55
|
if (out.account && !ACCOUNT.test(out.account)) delete out.account
|
|
56
|
-
for (const name of ["attempts", "inputTokens", "credit", "streak", "durationMs", "retry", "persisted", "keepalive"]) {
|
|
56
|
+
for (const name of ["attempts", "inputTokens", "credit", "streak", "durationMs", "retry", "persisted", "keepalive", "activityVerified"]) {
|
|
57
57
|
const value = record?.[name]
|
|
58
58
|
if (value !== undefined && value !== null && (typeof value === "number" || typeof value === "boolean")) {
|
|
59
59
|
out[name] = value
|
package/src/workbuddy.js
CHANGED
|
@@ -267,7 +267,8 @@ export function authIdentity(auth, key) {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
export function resultFields(result, config, model = "") {
|
|
270
|
-
const
|
|
270
|
+
const longProbe = config?.probeMode === "context" || config?.probeMode === "activity"
|
|
271
|
+
const configuredInputTokens = longProbe ? Number(config.inputTokens) || 0 : 0
|
|
271
272
|
switch (result?.kind) {
|
|
272
273
|
case "skipped":
|
|
273
274
|
if (result.reason === "done-today") {
|
|
@@ -313,6 +314,8 @@ export function resultFields(result, config, model = "") {
|
|
|
313
314
|
}
|
|
314
315
|
case "sent":
|
|
315
316
|
const terminal = result.cause?.retryToday === false
|
|
317
|
+
const reply = result.result || {}
|
|
318
|
+
const inputTokens = Number(reply.promptTokens) || configuredInputTokens
|
|
316
319
|
return {
|
|
317
320
|
status: result.ok ? "success" : "failed",
|
|
318
321
|
outcome: result.ok
|
|
@@ -323,8 +326,11 @@ export function resultFields(result, config, model = "") {
|
|
|
323
326
|
? "retry"
|
|
324
327
|
: "exhausted",
|
|
325
328
|
attempts: result.attempts,
|
|
326
|
-
model:
|
|
329
|
+
model: reply.model || model,
|
|
330
|
+
probeMode: config?.probeMode,
|
|
327
331
|
inputTokens,
|
|
332
|
+
...(reply.credit === undefined ? {} : { credit: reply.credit }),
|
|
333
|
+
...(reply.activityVerified === undefined ? {} : { activityVerified: reply.activityVerified }),
|
|
328
334
|
retry: !result.ok && !terminal && result.attempts < config.maxAttempts,
|
|
329
335
|
persisted: result.persisted !== false,
|
|
330
336
|
error: result.error,
|
|
@@ -528,17 +534,21 @@ export function createProviderKeepalive(options = {}) {
|
|
|
528
534
|
run,
|
|
529
535
|
model: candidates[0].id,
|
|
530
536
|
models: candidates.map((candidate) => candidate.id).join(","),
|
|
531
|
-
inputTokens: config.probeMode
|
|
537
|
+
inputTokens: ["context", "activity"].includes(config.probeMode) ? config.inputTokens : 0,
|
|
532
538
|
probeMode: config.probeMode,
|
|
533
539
|
keepalive: true,
|
|
534
540
|
})
|
|
535
541
|
|
|
542
|
+
const activityMode = config.probeMode === "short" ? "short" : "activity"
|
|
536
543
|
const result = await runDailyOnce({
|
|
537
544
|
statePath: config.statePath,
|
|
538
|
-
|
|
545
|
+
// A legacy short probe and a real activity conversation are not the
|
|
546
|
+
// same task. Keeping them separate lets a user switch to activity
|
|
547
|
+
// after a short probe has already been recorded today.
|
|
548
|
+
account: accountTag(`${provider}|${identity}|keepalive:${activityMode}`),
|
|
539
549
|
day,
|
|
540
550
|
model: candidates[0].id,
|
|
541
|
-
inputTokens: config.probeMode
|
|
551
|
+
inputTokens: ["context", "activity"].includes(config.probeMode) ? config.inputTokens : 0,
|
|
542
552
|
probeMode: config.probeMode,
|
|
543
553
|
maxAttempts: config.maxAttempts,
|
|
544
554
|
lockWaitMs: config.lockWaitMs,
|
package/ui/app.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const ACTIONS = {
|
|
4
4
|
detect: { title: "检测账号", detail: "正在读取 Magpie 当前已登录的供应商与账号。" },
|
|
5
5
|
checkin: { title: "全部签到", detail: "正在让已有签到能力的供应商执行签到。" },
|
|
6
|
-
keepalive: { title: "发送保活", detail: "
|
|
6
|
+
keepalive: { title: "发送保活", detail: "正在为全部已适配供应商账号发送真实活跃对话;会消耗额度。" },
|
|
7
7
|
refresh: { title: "刷新额度", detail: "正在刷新全部启用供应商的实时额度。" },
|
|
8
8
|
}
|
|
9
9
|
const EVENT_NAMES = {
|
|
@@ -995,9 +995,12 @@
|
|
|
995
995
|
["供应商", record?.provider],
|
|
996
996
|
["账号", record?.account],
|
|
997
997
|
["模型", record?.model],
|
|
998
|
-
["保活方式", record?.probeMode === "
|
|
998
|
+
["保活方式", record?.probeMode === "activity" || record?.probeMode === "context"
|
|
999
|
+
? "真实活跃对话"
|
|
1000
|
+
: record?.probeMode === "short" ? "短探活" : ""],
|
|
999
1001
|
["输入", number(record?.inputTokens) ? `${formatNumber(record.inputTokens)} tokens` : ""],
|
|
1000
1002
|
["本次积分", number(record?.credit) ? `+${formatNumber(record.credit)}` : ""],
|
|
1003
|
+
["活跃确认", record?.activityVerified === true ? "已验证" : ""],
|
|
1001
1004
|
["连续签到", number(record?.streak) ? `${formatNumber(record.streak)} 天` : ""],
|
|
1002
1005
|
["耗时", durationText(record?.durationMs)],
|
|
1003
1006
|
["日期", record?.day],
|
|
@@ -1192,7 +1195,9 @@
|
|
|
1192
1195
|
keepalive: "真实保活",
|
|
1193
1196
|
all: "签到 + 真实保活",
|
|
1194
1197
|
}[options.scheduleAction] || "签到"
|
|
1195
|
-
const probe = options.probeMode === "
|
|
1198
|
+
const probe = options.probeMode === "activity" || options.probeMode === "context"
|
|
1199
|
+
? "真实活跃对话(会消耗积分)"
|
|
1200
|
+
: "短探活(不保证活跃记录)"
|
|
1196
1201
|
return `每 ${number(options.scheduleMinutes) || 60} 分钟执行一次${action};${probe}。`
|
|
1197
1202
|
}
|
|
1198
1203
|
|
|
@@ -1211,7 +1216,7 @@
|
|
|
1211
1216
|
$("schedule-action").value = ["checkin", "keepalive", "all"].includes(options.scheduleAction)
|
|
1212
1217
|
? options.scheduleAction
|
|
1213
1218
|
: "checkin"
|
|
1214
|
-
$("probe-mode").value = options.probeMode === "
|
|
1219
|
+
$("probe-mode").value = options.probeMode === "short" ? "short" : "activity"
|
|
1215
1220
|
$("schedule-status").textContent = scheduleDescription(options)
|
|
1216
1221
|
if (showToast) toast("定时计划已保存")
|
|
1217
1222
|
} catch (error) {
|
package/ui/index.html
CHANGED
|
@@ -112,10 +112,10 @@
|
|
|
112
112
|
</svg>
|
|
113
113
|
</label>
|
|
114
114
|
<label class="select-field probe-field"
|
|
115
|
-
title="
|
|
115
|
+
title="真实活跃对话会发送约 25k 输入并消耗积分;短探活只做低成本连通性检查">
|
|
116
116
|
<select id="probe-mode" aria-label="保活方式">
|
|
117
|
+
<option value="activity">真实活跃对话</option>
|
|
117
118
|
<option value="short">短探活</option>
|
|
118
|
-
<option value="context">长上下文</option>
|
|
119
119
|
</select>
|
|
120
120
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
121
121
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
<section class="confirm-card" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
|
|
168
168
|
<p class="eyebrow">REAL REQUEST</p>
|
|
169
169
|
<h2 id="confirm-title">发送真实保活消息?</h2>
|
|
170
|
-
<p id="confirm-detail"
|
|
170
|
+
<p id="confirm-detail">这会为全部已适配供应商的账号发送真实活跃对话,优先尝试近期验证可用且能成功回复的模型;请求可能消耗积分。</p>
|
|
171
171
|
<div class="confirm-actions">
|
|
172
172
|
<button class="button" id="confirm-cancel" type="button">取消</button>
|
|
173
173
|
<button class="button caution" id="confirm-send" type="button">确认发送</button>
|