quota-keepalive 0.1.9 → 0.1.10
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 +14 -4
- package/README.md +16 -8
- package/package.json +1 -1
- package/src/config.js +5 -1
- package/src/runner.js +8 -0
- package/ui/app.js +92 -55
- package/ui/index.html +3 -25
- package/ui/quota-format.js +96 -35
- package/ui/style.css +35 -6
package/README.en.md
CHANGED
|
@@ -23,6 +23,12 @@ It does not take over provider sign-in. Magpie and the installed provider plugin
|
|
|
23
23
|
keep owning credentials, model lists, quotas, and the provider's own `auth.checkin`
|
|
24
24
|
hook. This plugin only coordinates those capabilities and presents the results.
|
|
25
25
|
|
|
26
|
+
> **Current status: real-chat keepalive paused**
|
|
27
|
+
>
|
|
28
|
+
> The page hides the global keepalive action, account-level keepalive controls, and
|
|
29
|
+
> model selection. Scheduled jobs now run check-in only. Existing keepalive records
|
|
30
|
+
> remain available as history while the backend code stays in place for a later release.
|
|
31
|
+
|
|
26
32
|
## Unofficial plugin notice
|
|
27
33
|
|
|
28
34
|
> This project is not an official Magpie plugin.
|
|
@@ -57,6 +63,8 @@ hook. This plugin only coordinates those capabilities and presents the results.
|
|
|
57
63
|
- Persist the next scheduled run so an overdue task can catch up after a Magpie
|
|
58
64
|
restart or computer sleep.
|
|
59
65
|
- Show today's result, a five-day account history, run details, and quota trends.
|
|
66
|
+
- Label quota rows explicitly as remaining versus total, show used amounts and
|
|
67
|
+
percentages separately, and keep vendor-only displays uninterpreted.
|
|
60
68
|
- Keep refresh-only events out of the check-in/keepalive run list.
|
|
61
69
|
- Refresh quotas every minute for enabled providers, and support per-provider or
|
|
62
70
|
refresh-all actions.
|
|
@@ -185,7 +193,8 @@ The page writes plugin-level options. The same values can be changed from the CL
|
|
|
185
193
|
magpie plugin options quota-keepalive '{
|
|
186
194
|
"scheduleEnabled": true,
|
|
187
195
|
"scheduleMinutes": 60,
|
|
188
|
-
"scheduleAction": "
|
|
196
|
+
"scheduleAction": "checkin",
|
|
197
|
+
"keepaliveEnabled": false,
|
|
189
198
|
"modelPreferences": {
|
|
190
199
|
"workbuddy": "deepseek-v4.1-flash",
|
|
191
200
|
"codearts": "deepseek-v4.1-flash"
|
|
@@ -203,6 +212,8 @@ magpie plugin options quota-keepalive '{
|
|
|
203
212
|
- `scheduleEnabled`: enable the plugin scheduler. Default `false`.
|
|
204
213
|
- `scheduleMinutes`: shared interval, default `60`, range 5 minutes to 7 days.
|
|
205
214
|
- `scheduleAction`: `checkin`, `keepalive`, or `all`. Default `checkin`.
|
|
215
|
+
- `keepaliveEnabled`: backend keepalive switch. Default `false`. The page hides all
|
|
216
|
+
keepalive controls, and old `keepalive` / `all` schedules fall back to check-in.
|
|
206
217
|
- `model`: optional preferred model; it is not a hard allowlist.
|
|
207
218
|
- `modelPreferences`: preferred model per provider.
|
|
208
219
|
- `modelCandidates`: maximum fallback candidates per account, default `4`.
|
|
@@ -217,9 +228,8 @@ magpie plugin options quota-keepalive '{
|
|
|
217
228
|
- `statePath`, `logPath`, `snapshotPath`, `commandPath`, `schedulePath`: optional
|
|
218
229
|
absolute paths.
|
|
219
230
|
|
|
220
|
-
Upgrade note:
|
|
221
|
-
|
|
222
|
-
options) when you want WorkBuddy activity records to be written.
|
|
231
|
+
Upgrade note: old schedules saved as `all` or `keepalive` automatically fall back
|
|
232
|
+
to check-in, so no page change is required. Keepalive options remain for compatibility.
|
|
223
233
|
|
|
224
234
|
## Logs
|
|
225
235
|
|
package/README.md
CHANGED
|
@@ -19,6 +19,11 @@ quota-keepalive 不接管任何供应商的登录,也不会把页面伪装成
|
|
|
19
19
|
登录、模型、额度和签到 Hook 仍由各供应商插件维护;本插件负责统一检测、调度、
|
|
20
20
|
执行和展示。
|
|
21
21
|
|
|
22
|
+
> **当前状态:真实保活暂停**
|
|
23
|
+
>
|
|
24
|
+
> 页面已隐藏顶部“发送保活”、账号级保活开关和保活模型配置;定时任务只执行签到。
|
|
25
|
+
> 已有保活记录仍作为历史日志保留。保活后端代码暂不删除,后续确认稳定后再开放入口。
|
|
26
|
+
|
|
22
27
|
> **非官方插件声明**
|
|
23
28
|
>
|
|
24
29
|
> 本项目不是 Magpie 官方插件。签到优先调用供应商插件公开的签到接口;真实对话
|
|
@@ -51,9 +56,9 @@ quota-keepalive 不接管任何供应商的登录,也不会把页面伪装成
|
|
|
51
56
|
签到积分(如 `+100`)、已保活,以及设备受限、额度不足、待重试等具体原因。
|
|
52
57
|
跳过项不再计入“需要处理”,只有明确失败才进入待处理数量。
|
|
53
58
|
- 近 5 天历史:每个有执行记录的账号可展开查看最近 5 天的签到、积分和保活结果。
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
- 额度视图:主行明确标注“剩余 / 总计”,副行展示“已用数量 / 已用百分比”,
|
|
60
|
+
进度条只表示已用比例;余额单列显示。供应商只返回原始文本时标注“供应商显示”,
|
|
61
|
+
不推断“已用”或“剩余”,避免误读。
|
|
57
62
|
- 自动刷新额度:启用中的供应商每 1 分钟刷新一次,停用的供应商不刷新;顶部可刷新全部
|
|
58
63
|
启用供应商,每个供应商卡片也可单独立即刷新。供应商没有可读取额度卡时记为跳过;
|
|
59
64
|
Magpie 30 秒内已刷新过时使用缓存并记为跳过,只有网络、登录失效或空数据等真实
|
|
@@ -192,7 +197,8 @@ npm publish --access public --otp=<6 位动态码>
|
|
|
192
197
|
magpie plugin options quota-keepalive '{
|
|
193
198
|
"scheduleEnabled": true,
|
|
194
199
|
"scheduleMinutes": 60,
|
|
195
|
-
"scheduleAction": "
|
|
200
|
+
"scheduleAction": "checkin",
|
|
201
|
+
"keepaliveEnabled": false,
|
|
196
202
|
"modelPreferences": {
|
|
197
203
|
"workbuddy": "deepseek-v4.1-flash",
|
|
198
204
|
"codearts": "deepseek-v4.1-flash"
|
|
@@ -209,7 +215,10 @@ magpie plugin options quota-keepalive '{
|
|
|
209
215
|
|
|
210
216
|
- `scheduleEnabled`:是否启用定时任务,默认 `false`。
|
|
211
217
|
- `scheduleMinutes`:统一执行间隔,默认 60 分钟,范围 5 分钟到 7 天。
|
|
212
|
-
- `scheduleAction`:`checkin`、`keepalive` 或 `all`,默认 `checkin
|
|
218
|
+
- `scheduleAction`:`checkin`、`keepalive` 或 `all`,默认 `checkin`。只有当
|
|
219
|
+
`keepaliveEnabled` 为 `true` 时才会保留 `keepalive` / `all`;否则自动降为签到。
|
|
220
|
+
- `keepaliveEnabled`:真实保活后端开关,默认 `false`。当前页面已隐藏所有保活入口,
|
|
221
|
+
旧配置即使仍是 `all` 也只执行签到。
|
|
213
222
|
- `model`:可选的首选模型;不是白名单,候选仍会按可用性回退。
|
|
214
223
|
- `modelPreferences`:按供应商指定首选模型,例如
|
|
215
224
|
`{"workbuddy":"deepseek-v4.1-flash","codearts":"deepseek-v4.1-flash"}`。未配置时
|
|
@@ -228,9 +237,8 @@ magpie plugin options quota-keepalive '{
|
|
|
228
237
|
- `logMaxBytes`:单个日志上限,默认 2 MiB,超出后轮转一份 `.1`。
|
|
229
238
|
- `statePath`、`logPath`、`snapshotPath`、`commandPath`、`schedulePath`:可选绝对路径。
|
|
230
239
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
并保存计划,或者执行 `magpie plugin options quota-keepalive` 更新配置。
|
|
240
|
+
升级提示:旧版本如果保存过 `scheduleAction: "all"` 或 `"keepalive"`,升级后仍会
|
|
241
|
+
自动降为签到,不需要重新保存计划。保活相关配置仅为保留兼容性。
|
|
234
242
|
|
|
235
243
|
## 执行周期与失败
|
|
236
244
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -22,6 +22,7 @@ export const DEFAULT_KEEPALIVE = Object.freeze({
|
|
|
22
22
|
probeMode: "activity",
|
|
23
23
|
probeMessage: DEFAULT_PROBE_MESSAGE,
|
|
24
24
|
maxAttempts: 3,
|
|
25
|
+
keepaliveEnabled: false,
|
|
25
26
|
scheduleEnabled: false,
|
|
26
27
|
scheduleAction: "checkin",
|
|
27
28
|
scheduleMinutes: 60,
|
|
@@ -61,15 +62,18 @@ export function resolveKeepaliveOptions(raw) {
|
|
|
61
62
|
const probeMessage = typeof source.probeMessage === "string"
|
|
62
63
|
? source.probeMessage.trim().slice(0, 500)
|
|
63
64
|
: ""
|
|
64
|
-
const
|
|
65
|
+
const requestedScheduleAction = ["checkin", "keepalive", "all"].includes(source.scheduleAction)
|
|
65
66
|
? source.scheduleAction
|
|
66
67
|
: DEFAULT_KEEPALIVE.scheduleAction
|
|
68
|
+
const keepaliveEnabled = source.keepaliveEnabled === true
|
|
69
|
+
const scheduleAction = keepaliveEnabled ? requestedScheduleAction : "checkin"
|
|
67
70
|
return {
|
|
68
71
|
model,
|
|
69
72
|
modelPreferences: modelPreferences(source.modelPreferences),
|
|
70
73
|
providerPreferences: normalizeProviderPreferences(source.providerPreferences),
|
|
71
74
|
accountPreferences: normalizeAccountPreferences(source.accountPreferences),
|
|
72
75
|
modelCandidates: integerIn(source.modelCandidates, DEFAULT_KEEPALIVE.modelCandidates, 1, 12),
|
|
76
|
+
keepaliveEnabled,
|
|
73
77
|
scheduleEnabled: source.scheduleEnabled === true,
|
|
74
78
|
scheduleAction,
|
|
75
79
|
scheduleMinutes: integerIn(source.scheduleMinutes, DEFAULT_KEEPALIVE.scheduleMinutes, 5, 10_080),
|
package/src/runner.js
CHANGED
|
@@ -231,6 +231,14 @@ export function classifyRefreshResult(result, body) {
|
|
|
231
231
|
cards,
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
if (result?.ok && Array.isArray(body) && cards.length === 0) {
|
|
235
|
+
return {
|
|
236
|
+
status: "skipped",
|
|
237
|
+
outcome: "no-card",
|
|
238
|
+
message: "该供应商当前没有可刷新的额度卡,已跳过。",
|
|
239
|
+
cards,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
234
242
|
const empty = result?.ok
|
|
235
243
|
? Array.isArray(body) ? "Magpie 返回 0 张额度卡" : "Magpie 返回空额度数据(退出码 0)"
|
|
236
244
|
: ""
|
package/ui/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const $ = (id) => document.getElementById(id)
|
|
3
|
+
const KEEPALIVE_VISIBLE = false
|
|
3
4
|
const ACTIONS = {
|
|
4
5
|
detect: { title: "检测账号", detail: "正在读取 Magpie 当前已登录的供应商与账号。" },
|
|
5
6
|
checkin: { title: "全部签到", detail: "正在让已有签到能力的供应商执行签到。" },
|
|
@@ -265,7 +266,7 @@
|
|
|
265
266
|
|
|
266
267
|
function accountNotes(account, provider) {
|
|
267
268
|
const notes = []
|
|
268
|
-
for (const task of ["checkin", "keepalive"]) {
|
|
269
|
+
for (const task of KEEPALIVE_VISIBLE ? ["checkin", "keepalive"] : ["checkin"]) {
|
|
269
270
|
if (!provider?.[`${task}Supported`]) continue
|
|
270
271
|
const point = todayPoint(account, task)
|
|
271
272
|
if (!point || point.success) continue
|
|
@@ -295,7 +296,7 @@
|
|
|
295
296
|
function accountHistory(account, provider, index) {
|
|
296
297
|
const days = activityDays(account)
|
|
297
298
|
if (!days.length) return ""
|
|
298
|
-
if (!days.some((day) => day?.checkin || day?.keepalive)) return ""
|
|
299
|
+
if (!days.some((day) => day?.checkin || (KEEPALIVE_VISIBLE && day?.keepalive))) return ""
|
|
299
300
|
const summary = account?.activity?.summary || {}
|
|
300
301
|
const key = `${provider?.id || ""}|${account?.keyTag || ""}|${index}`
|
|
301
302
|
return `<details class="account-history" data-history-key="${escapeHtml(key)}"${
|
|
@@ -303,9 +304,11 @@
|
|
|
303
304
|
}>
|
|
304
305
|
<summary>
|
|
305
306
|
<span>近 5 天</span>
|
|
306
|
-
<small>签到 ${number(summary.checkinDays)}/${number(summary.days) || days.length}
|
|
307
|
-
number(summary.keepaliveDays)
|
|
308
|
-
|
|
307
|
+
<small>签到 ${number(summary.checkinDays)}/${number(summary.days) || days.length}${
|
|
308
|
+
KEEPALIVE_VISIBLE ? ` · 保活 ${number(summary.keepaliveDays)}/${
|
|
309
|
+
number(summary.days) || days.length
|
|
310
|
+
}` : ""
|
|
311
|
+
}</small>
|
|
309
312
|
<span class="history-chevron" aria-hidden="true">›</span>
|
|
310
313
|
</summary>
|
|
311
314
|
<div class="history-days">
|
|
@@ -320,11 +323,11 @@
|
|
|
320
323
|
} else if (checkin) {
|
|
321
324
|
chips.push(`<span class="history-chip muted">未签到</span>`)
|
|
322
325
|
}
|
|
323
|
-
if (keepalive?.success) {
|
|
326
|
+
if (KEEPALIVE_VISIBLE && keepalive?.success) {
|
|
324
327
|
chips.push(`<span class="history-chip good">保活</span>`)
|
|
325
|
-
} else if (keepalive?.status === "failed") {
|
|
328
|
+
} else if (KEEPALIVE_VISIBLE && keepalive?.status === "failed") {
|
|
326
329
|
chips.push(`<span class="history-chip bad">保活失败</span>`)
|
|
327
|
-
} else if (keepalive) {
|
|
330
|
+
} else if (KEEPALIVE_VISIBLE && keepalive) {
|
|
328
331
|
chips.push(`<span class="history-chip muted">未保活</span>`)
|
|
329
332
|
}
|
|
330
333
|
return `<div class="history-day">
|
|
@@ -348,15 +351,11 @@
|
|
|
348
351
|
const today = data?.summary?.today || {}
|
|
349
352
|
const accounts = number(today.accounts)
|
|
350
353
|
const checkinDone = number(today.checkinDone)
|
|
351
|
-
const keepaliveDone = number(today.keepaliveDone)
|
|
352
354
|
const checkinSuccess = number(today.checkin?.success)
|
|
353
355
|
const checkinSkipped = number(today.checkin?.skipped)
|
|
354
356
|
const checkinFailed = number(today.checkin?.failed)
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
const keepaliveFailed = number(today.keepalive?.failed)
|
|
358
|
-
const failed = checkinFailed + keepaliveFailed
|
|
359
|
-
const skipped = checkinSkipped + keepaliveSkipped
|
|
357
|
+
const failed = checkinFailed
|
|
358
|
+
const skipped = checkinSkipped
|
|
360
359
|
const taskDetail = (success, skippedCount, failedCount) => [
|
|
361
360
|
`${success} 成功`,
|
|
362
361
|
skippedCount ? `${skippedCount} 跳过` : "",
|
|
@@ -372,26 +371,20 @@
|
|
|
372
371
|
: "暂无签到记录",
|
|
373
372
|
checkinFailed ? "bad" : checkinSkipped ? "warn" : "",
|
|
374
373
|
),
|
|
375
|
-
stat(
|
|
376
|
-
"今日保活",
|
|
377
|
-
String(keepaliveDone),
|
|
378
|
-
keepaliveDone || keepaliveSkipped || keepaliveFailed
|
|
379
|
-
? taskDetail(keepaliveSuccess, keepaliveSkipped, keepaliveFailed)
|
|
380
|
-
: "等待手动触发",
|
|
381
|
-
keepaliveFailed ? "bad" : keepaliveSkipped ? "warn" : "",
|
|
382
|
-
),
|
|
383
374
|
stat(
|
|
384
375
|
"需要处理",
|
|
385
376
|
String(failed),
|
|
386
|
-
failed
|
|
387
|
-
? `签到 ${checkinFailed} · 保活 ${keepaliveFailed}`
|
|
388
|
-
: skipped ? `暂无失败 · ${skipped} 项跳过` : "暂无失败",
|
|
377
|
+
failed ? `签到 ${checkinFailed}` : skipped ? `暂无失败 · ${skipped} 项跳过` : "暂无失败",
|
|
389
378
|
failed ? "bad" : "",
|
|
390
379
|
),
|
|
391
380
|
].join("")
|
|
392
381
|
}
|
|
393
382
|
|
|
394
|
-
const
|
|
383
|
+
const quotaItems = window.QuotaKeepaliveFormat?.quotaItems || (() => [{
|
|
384
|
+
kind: "pending",
|
|
385
|
+
name: "额度",
|
|
386
|
+
text: "额度待检测",
|
|
387
|
+
}])
|
|
395
388
|
|
|
396
389
|
function quotaTone(account) {
|
|
397
390
|
const history = Array.isArray(account?.history) ? account.history : []
|
|
@@ -418,15 +411,60 @@
|
|
|
418
411
|
preserveAspectRatio="none" role="img" aria-label="最近额度变化"><polyline points="${points}"></polyline></svg>`
|
|
419
412
|
}
|
|
420
413
|
|
|
414
|
+
function quotaItemHtml(item) {
|
|
415
|
+
if (item.kind === "meter") {
|
|
416
|
+
const unit = item.unit ? ` ${item.unit}` : ""
|
|
417
|
+
const progress = Math.max(0, Math.min(100, number(item.progress)))
|
|
418
|
+
const label = `${item.name}已用 ${formatNumber(item.percent)}%`
|
|
419
|
+
return `<div class="quota-window">
|
|
420
|
+
<div class="quota-window-head">
|
|
421
|
+
<span class="quota-window-name">${escapeHtml(item.name)}</span>
|
|
422
|
+
<span class="quota-window-value"><b>剩余 ${escapeHtml(formatNumber(item.remaining))}</b> / ${
|
|
423
|
+
escapeHtml(formatNumber(item.total))
|
|
424
|
+
}${escapeHtml(unit)}</span>
|
|
425
|
+
</div>
|
|
426
|
+
<div class="quota-meter ${escapeHtml(item.level)}" role="progressbar"
|
|
427
|
+
aria-label="${escapeHtml(label)}" aria-valuemin="0" aria-valuemax="100"
|
|
428
|
+
aria-valuenow="${escapeHtml(formatNumber(progress))}">
|
|
429
|
+
<i style="width:${progress}%"></i>
|
|
430
|
+
</div>
|
|
431
|
+
<div class="quota-window-foot">
|
|
432
|
+
<span>已用 ${escapeHtml(formatNumber(item.used))}${escapeHtml(unit)}</span>
|
|
433
|
+
<span>${escapeHtml(formatNumber(item.percent))}%</span>
|
|
434
|
+
</div>
|
|
435
|
+
</div>`
|
|
436
|
+
}
|
|
437
|
+
const detail = item.kind === "display"
|
|
438
|
+
? "供应商原样显示,不推断已用或剩余"
|
|
439
|
+
: item.detail || ""
|
|
440
|
+
return `<div class="quota-window quota-window-plain">
|
|
441
|
+
<div class="quota-window-head">
|
|
442
|
+
<span class="quota-window-name">${escapeHtml(item.name)}</span>
|
|
443
|
+
<span class="quota-window-value">${escapeHtml(item.text)}</span>
|
|
444
|
+
</div>
|
|
445
|
+
${detail ? `<div class="quota-window-foot">${escapeHtml(detail)}</div>` : ""}
|
|
446
|
+
</div>`
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function accountQuota(account) {
|
|
450
|
+
const items = quotaItems(account)
|
|
451
|
+
return `<div class="account-quota ${quotaTone(account)}" aria-label="额度信息">
|
|
452
|
+
<div class="account-quota-head">
|
|
453
|
+
<span class="account-quota-title"><b>额度</b><span>进度条 = 已用比例</span></span>
|
|
454
|
+
${quotaSparkline(account)}
|
|
455
|
+
</div>
|
|
456
|
+
<div class="account-quota-windows">${items.map(quotaItemHtml).join("")}</div>
|
|
457
|
+
</div>`
|
|
458
|
+
}
|
|
459
|
+
|
|
421
460
|
function accountRow(account, provider, index) {
|
|
422
461
|
const latest = account?.latest
|
|
423
|
-
const quota = quotaView(account)
|
|
424
462
|
const actualModel = String(
|
|
425
463
|
account?.lastModel || account?.activity?.today?.keepalive?.model || "",
|
|
426
464
|
).trim()
|
|
427
465
|
const chosen = selectedModel(provider)
|
|
428
466
|
const automatic = provider?.defaultModel || provider?.cheapestModel
|
|
429
|
-
const modelLabel = provider?.keepaliveSupported
|
|
467
|
+
const modelLabel = KEEPALIVE_VISIBLE && provider?.keepaliveSupported
|
|
430
468
|
? actualModel
|
|
431
469
|
? `实际模型 ${modelName(provider, actualModel)}`
|
|
432
470
|
: chosen
|
|
@@ -435,10 +473,9 @@
|
|
|
435
473
|
? `自动首选 ${automatic.name || automatic.id}`
|
|
436
474
|
: "模型待选择"
|
|
437
475
|
: ""
|
|
438
|
-
const quotaToneClass = quotaTone(account)
|
|
439
476
|
const taskRows = [
|
|
440
477
|
provider.checkinSupported ? taskRow(provider, account, "checkin") : "",
|
|
441
|
-
provider.keepaliveSupported ? taskRow(provider, account, "keepalive") : "",
|
|
478
|
+
KEEPALIVE_VISIBLE && provider.keepaliveSupported ? taskRow(provider, account, "keepalive") : "",
|
|
442
479
|
].filter(Boolean).join("")
|
|
443
480
|
const detail = latest
|
|
444
481
|
? `${EVENT_NAMES[latest.event] || latest.event || "任务"} · ${relation(latest.at)}`
|
|
@@ -453,14 +490,15 @@
|
|
|
453
490
|
</div>
|
|
454
491
|
</div>
|
|
455
492
|
</div>
|
|
493
|
+
${accountQuota(account)}
|
|
456
494
|
<div class="account-meta">
|
|
457
|
-
<span class="quota ${quotaToneClass}">${escapeHtml(quota)}</span>
|
|
458
|
-
${quotaSparkline(account)}
|
|
459
495
|
${modelLabel ? `<span class="model" title="${escapeHtml(actualModel || chosen?.id || automatic?.id || "")}">${
|
|
460
496
|
escapeHtml(modelLabel)
|
|
461
497
|
}</span>` : ""}
|
|
462
498
|
<span>${provider.checkinSupported ? "支持签到" : "无签到能力"}</span>
|
|
463
|
-
|
|
499
|
+
${KEEPALIVE_VISIBLE
|
|
500
|
+
? `<span>${provider.keepaliveSupported ? "支持真实保活" : "暂不保活"}</span>`
|
|
501
|
+
: ""}
|
|
464
502
|
</div>
|
|
465
503
|
${taskRows ? `<div class="account-tasks">${taskRows}</div>` : ""}
|
|
466
504
|
${accountHistory(account, provider, index)}
|
|
@@ -595,10 +633,10 @@
|
|
|
595
633
|
const chosen = selectedModel(provider)
|
|
596
634
|
const capabilities = [
|
|
597
635
|
provider?.checkinSupported ? "签到" : "",
|
|
598
|
-
provider?.keepaliveSupported ? "保活" : "",
|
|
636
|
+
KEEPALIVE_VISIBLE && provider?.keepaliveSupported ? "保活" : "",
|
|
599
637
|
].filter(Boolean).join(" + ") || "仅展示"
|
|
600
638
|
const taskChips = []
|
|
601
|
-
for (const task of ["checkin", "keepalive"]) {
|
|
639
|
+
for (const task of KEEPALIVE_VISIBLE ? ["checkin", "keepalive"] : ["checkin"]) {
|
|
602
640
|
if (!provider?.[`${task}Supported`]) continue
|
|
603
641
|
const activeAccounts = accounts.filter((account) => taskOn(provider, account, task))
|
|
604
642
|
taskChips.push(taskChip(activeAccounts.map((account) => todayPoint(account, task)), task))
|
|
@@ -623,14 +661,22 @@
|
|
|
623
661
|
<span class="provider-mark">${mark}</span>
|
|
624
662
|
<span class="provider-title">
|
|
625
663
|
<strong>${escapeHtml(provider?.name || provider?.id || "未知供应商")}</strong>
|
|
626
|
-
<small>${accounts.length} 个账号 · ${escapeHtml(capabilities)}${
|
|
664
|
+
<small>${accounts.length} 个账号 · ${escapeHtml(capabilities)}${
|
|
665
|
+
KEEPALIVE_VISIBLE
|
|
666
|
+
? chosen
|
|
667
|
+
? ` · 指定模型 ${escapeHtml(chosen.name || chosen.id)}`
|
|
668
|
+
: model
|
|
669
|
+
? ` · 默认最可用 ${escapeHtml(model.name || model.id)}`
|
|
670
|
+
: ""
|
|
671
|
+
: ""
|
|
672
|
+
}</small>
|
|
627
673
|
</span>
|
|
628
674
|
<span class="provider-states">${enabled ? states : `<span class="status-chip muted">已停用</span>`}</span>
|
|
629
675
|
${providerRefresh(provider, id)}
|
|
630
676
|
${providerToggle(provider, id)}
|
|
631
677
|
<span class="chevron" aria-hidden="true">›</span>
|
|
632
678
|
</summary>
|
|
633
|
-
${modelPicker(provider, id)}
|
|
679
|
+
${KEEPALIVE_VISIBLE ? modelPicker(provider, id) : ""}
|
|
634
680
|
<div class="accounts">${accounts.map((account, accountIndex) =>
|
|
635
681
|
accountRow(account, provider, accountIndex)).join("")}</div>
|
|
636
682
|
</details>`
|
|
@@ -1108,7 +1154,7 @@
|
|
|
1108
1154
|
.reverse()
|
|
1109
1155
|
$("logs").innerHTML = records.length
|
|
1110
1156
|
? records.map((record) => logRecord(record, allRecords)).join("")
|
|
1111
|
-
: `<div class="empty compact"><b>还没有执行记录</b><span
|
|
1157
|
+
: `<div class="empty compact"><b>还没有执行记录</b><span>签到完成后,这里会显示每次结果。</span></div>`
|
|
1112
1158
|
$("log-count").textContent = records.length ? `最近 ${records.length} 条` : "暂无记录"
|
|
1113
1159
|
}
|
|
1114
1160
|
|
|
@@ -1163,7 +1209,10 @@
|
|
|
1163
1209
|
|
|
1164
1210
|
function setBusy(value, text = "") {
|
|
1165
1211
|
busy = value
|
|
1166
|
-
for (const id of ["detect", "checkin", "keepalive"])
|
|
1212
|
+
for (const id of ["detect", "checkin", "keepalive"]) {
|
|
1213
|
+
const button = $(id)
|
|
1214
|
+
if (button) button.disabled = value
|
|
1215
|
+
}
|
|
1167
1216
|
updateRefreshControls()
|
|
1168
1217
|
if (text) toast(text)
|
|
1169
1218
|
}
|
|
@@ -1190,15 +1239,7 @@
|
|
|
1190
1239
|
|
|
1191
1240
|
function scheduleDescription(options = {}) {
|
|
1192
1241
|
if (options.scheduleEnabled !== true) return "自动任务已关闭,可手动执行或在这里开启。"
|
|
1193
|
-
|
|
1194
|
-
checkin: "签到",
|
|
1195
|
-
keepalive: "真实保活",
|
|
1196
|
-
all: "签到 + 真实保活",
|
|
1197
|
-
}[options.scheduleAction] || "签到"
|
|
1198
|
-
const probe = options.probeMode === "activity" || options.probeMode === "context"
|
|
1199
|
-
? "真实活跃对话(会消耗积分)"
|
|
1200
|
-
: "短探活(不保证活跃记录)"
|
|
1201
|
-
return `每 ${number(options.scheduleMinutes) || 60} 分钟执行一次${action};${probe}。`
|
|
1242
|
+
return `每 ${number(options.scheduleMinutes) || 60} 分钟为启用中的供应商执行一次签到。`
|
|
1202
1243
|
}
|
|
1203
1244
|
|
|
1204
1245
|
async function loadSchedule(showToast = false, refresh = false) {
|
|
@@ -1213,10 +1254,7 @@
|
|
|
1213
1254
|
accountPreferences = normalizePreferences(options.accountPreferences)
|
|
1214
1255
|
$("schedule-enabled").checked = options.scheduleEnabled === true
|
|
1215
1256
|
$("schedule-minutes").value = String(options.scheduleMinutes || 60)
|
|
1216
|
-
$("schedule-action").value =
|
|
1217
|
-
? options.scheduleAction
|
|
1218
|
-
: "checkin"
|
|
1219
|
-
$("probe-mode").value = options.probeMode === "short" ? "short" : "activity"
|
|
1257
|
+
$("schedule-action").value = "checkin"
|
|
1220
1258
|
$("schedule-status").textContent = scheduleDescription(options)
|
|
1221
1259
|
if (showToast) toast("定时计划已保存")
|
|
1222
1260
|
} catch (error) {
|
|
@@ -1231,8 +1269,7 @@
|
|
|
1231
1269
|
...latest,
|
|
1232
1270
|
scheduleEnabled: $("schedule-enabled").checked,
|
|
1233
1271
|
scheduleMinutes: minutes,
|
|
1234
|
-
scheduleAction:
|
|
1235
|
-
probeMode: $("probe-mode").value,
|
|
1272
|
+
scheduleAction: "checkin",
|
|
1236
1273
|
})))
|
|
1237
1274
|
await loadSchedule(true)
|
|
1238
1275
|
}
|
|
@@ -1438,7 +1475,7 @@
|
|
|
1438
1475
|
}, true)
|
|
1439
1476
|
$("detect").addEventListener("click", () => queue("detect"))
|
|
1440
1477
|
$("checkin").addEventListener("click", () => queue("checkin"))
|
|
1441
|
-
$("keepalive")
|
|
1478
|
+
$("keepalive")?.addEventListener("click", () => openConfirm())
|
|
1442
1479
|
$("schedule-save").addEventListener("click", () => {
|
|
1443
1480
|
saveSchedule().catch((error) => toast(`保存失败:${error?.message || error}`, "bad"))
|
|
1444
1481
|
})
|
package/ui/index.html
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<div class="brand-copy">
|
|
20
20
|
<p class="eyebrow">QUOTA KEEPALIVE</p>
|
|
21
21
|
<h1>额度签到</h1>
|
|
22
|
-
<p class="lead"
|
|
22
|
+
<p class="lead">检测已登录账号,集中查看签到、额度变化和执行结果。</p>
|
|
23
23
|
</div>
|
|
24
24
|
</div>
|
|
25
25
|
<div class="hero-tools">
|
|
@@ -42,15 +42,6 @@
|
|
|
42
42
|
</span>
|
|
43
43
|
<span>全部签到</span>
|
|
44
44
|
</button>
|
|
45
|
-
<button class="button action-button action-warm" id="keepalive" type="button">
|
|
46
|
-
<span class="action-icon" aria-hidden="true">
|
|
47
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
48
|
-
<path d="M2.6 3.2h10.8v7.2H8l-3.1 2.7v-2.7H2.6z"></path>
|
|
49
|
-
<path d="M5 6.2h1.4L7.5 8l1.2-2.2 1.2 1.5h1.1"></path>
|
|
50
|
-
</svg>
|
|
51
|
-
</span>
|
|
52
|
-
<span>发送保活</span>
|
|
53
|
-
</button>
|
|
54
45
|
</div>
|
|
55
46
|
<button
|
|
56
47
|
class="button refresh-button"
|
|
@@ -85,7 +76,7 @@
|
|
|
85
76
|
<p class="eyebrow">AUTOMATION</p>
|
|
86
77
|
<b>定时任务</b>
|
|
87
78
|
</div>
|
|
88
|
-
<span id="schedule-status"
|
|
79
|
+
<span id="schedule-status">默认关闭;开启后会按间隔为全部供应商执行签到。</span>
|
|
89
80
|
</div>
|
|
90
81
|
<div class="schedule-controls">
|
|
91
82
|
<label class="switch-field">
|
|
@@ -103,19 +94,6 @@
|
|
|
103
94
|
<label class="select-field">
|
|
104
95
|
<select id="schedule-action" aria-label="定时任务内容">
|
|
105
96
|
<option value="checkin">只签到</option>
|
|
106
|
-
<option value="keepalive">只保活</option>
|
|
107
|
-
<option value="all">签到 + 保活</option>
|
|
108
|
-
</select>
|
|
109
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
110
|
-
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
111
|
-
<path d="m4 6 4 4 4-4"></path>
|
|
112
|
-
</svg>
|
|
113
|
-
</label>
|
|
114
|
-
<label class="select-field probe-field"
|
|
115
|
-
title="真实活跃对话会发送约 25k 输入并消耗积分;短探活只做低成本连通性检查">
|
|
116
|
-
<select id="probe-mode" aria-label="保活方式">
|
|
117
|
-
<option value="activity">真实活跃对话</option>
|
|
118
|
-
<option value="short">短探活</option>
|
|
119
97
|
</select>
|
|
120
98
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
121
99
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
@@ -159,7 +137,7 @@
|
|
|
159
137
|
</section>
|
|
160
138
|
|
|
161
139
|
<footer class="footnote">
|
|
162
|
-
<span
|
|
140
|
+
<span>签到状态来自本机插件日志,不包含令牌或账号凭据。</span>
|
|
163
141
|
</footer>
|
|
164
142
|
</main>
|
|
165
143
|
|
package/ui/quota-format.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
(function exposeQuotaFormat(global) {
|
|
2
2
|
const number = (value) => {
|
|
3
|
+
if (value === null || value === undefined || value === "") return null
|
|
3
4
|
const parsed = Number(value)
|
|
4
5
|
return Number.isFinite(parsed) ? parsed : null
|
|
5
6
|
}
|
|
@@ -12,55 +13,115 @@
|
|
|
12
13
|
: parsed.toFixed(2).replace(/\.?0+$/, "")
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const clampPercent = (value) => Math.max(0, Math.min(100, number(value)))
|
|
17
|
+
|
|
18
|
+
function quotaLevel(percent) {
|
|
19
|
+
if (percent >= 100) return "exhausted"
|
|
20
|
+
if (percent >= 85) return "warning"
|
|
21
|
+
return "normal"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function meterItem(name, unit, used, total, shownPercent) {
|
|
25
|
+
const safeTotal = Math.max(0, total)
|
|
26
|
+
const safeUsed = Math.max(0, used)
|
|
27
|
+
const remaining = Math.max(0, safeTotal - safeUsed)
|
|
28
|
+
const countedPercent = safeTotal > 0 ? (safeUsed / safeTotal) * 100 : 0
|
|
29
|
+
const percent = shownPercent !== null && Math.abs(shownPercent - countedPercent) < 0.5
|
|
30
|
+
? shownPercent
|
|
31
|
+
: countedPercent
|
|
32
|
+
return {
|
|
33
|
+
kind: "meter",
|
|
34
|
+
name,
|
|
35
|
+
unit,
|
|
36
|
+
remaining,
|
|
37
|
+
total: safeTotal,
|
|
38
|
+
used: safeUsed,
|
|
39
|
+
percent,
|
|
40
|
+
progress: clampPercent(percent),
|
|
41
|
+
level: quotaLevel(percent),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function windowItem(window) {
|
|
46
|
+
const name = String(window?.name || "额度").trim()
|
|
47
|
+
const unit = String(window?.unit || "").trim()
|
|
16
48
|
const limit = number(window?.limit)
|
|
17
49
|
const amount = number(window?.amount)
|
|
18
|
-
|
|
19
|
-
|
|
50
|
+
const remaining = number(window?.remaining)
|
|
51
|
+
const usedPercent = number(window?.used)
|
|
52
|
+
const display = typeof window?.display === "string" ? window.display.trim() : ""
|
|
53
|
+
if (limit !== null && limit > 0) {
|
|
54
|
+
if (amount !== null) {
|
|
55
|
+
return meterItem(name, unit, amount, limit, usedPercent)
|
|
56
|
+
}
|
|
57
|
+
if (remaining !== null) {
|
|
58
|
+
const sum = usedPercent !== null ? usedPercent + remaining : null
|
|
59
|
+
const left = sum !== null && Math.abs(sum - 100) < 0.5 && Math.abs(sum - limit) >= 0.5
|
|
60
|
+
? (limit * remaining) / 100
|
|
61
|
+
: remaining
|
|
62
|
+
const clampedLeft = Math.max(0, Math.min(limit, left))
|
|
63
|
+
return meterItem(name, unit, limit - clampedLeft, limit, usedPercent)
|
|
64
|
+
}
|
|
65
|
+
if (usedPercent !== null) {
|
|
66
|
+
return meterItem(name, unit, (limit * Math.max(0, usedPercent)) / 100, limit, usedPercent)
|
|
67
|
+
}
|
|
20
68
|
}
|
|
21
69
|
const balance = number(window?.balance)
|
|
22
|
-
if (balance !== null)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const shown = /^(-?\d+(?:\.\d+)?)\s*\/\s*(-?\d+(?:\.\d+)?)/.exec(display)
|
|
28
|
-
if (shown && used !== null && remaining !== null) {
|
|
29
|
-
const shownUsed = Number(shown[1])
|
|
30
|
-
const shownTotal = Number(shown[2])
|
|
31
|
-
const total = limit !== null && limit > 0 ? limit : used + remaining
|
|
32
|
-
if (
|
|
33
|
-
Math.abs(shownUsed - used) < 0.001 &&
|
|
34
|
-
Math.abs(shownTotal - total) < 0.001 &&
|
|
35
|
-
total > 0 &&
|
|
36
|
-
remaining >= 0
|
|
37
|
-
) {
|
|
38
|
-
return `${formatNumber(remaining)} / ${formatNumber(total)}`
|
|
39
|
-
}
|
|
70
|
+
if (balance !== null) {
|
|
71
|
+
return {
|
|
72
|
+
kind: "balance",
|
|
73
|
+
name,
|
|
74
|
+
text: `余额 ${formatNumber(balance)}${unit ? ` ${unit}` : ""}`,
|
|
40
75
|
}
|
|
41
|
-
return display
|
|
42
76
|
}
|
|
43
|
-
|
|
77
|
+
if (usedPercent !== null) {
|
|
78
|
+
return {
|
|
79
|
+
kind: "percent",
|
|
80
|
+
name,
|
|
81
|
+
text: `已用 ${formatNumber(usedPercent)}%`,
|
|
82
|
+
detail: display ? `供应商显示 ${display}` : "",
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (display) {
|
|
86
|
+
return { kind: "display", name, text: display }
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
44
89
|
}
|
|
45
90
|
|
|
46
|
-
function
|
|
91
|
+
function itemText(item) {
|
|
92
|
+
if (item.kind === "meter") {
|
|
93
|
+
const unit = item.unit ? ` ${item.unit}` : ""
|
|
94
|
+
return [
|
|
95
|
+
item.name,
|
|
96
|
+
`剩余 ${formatNumber(item.remaining)} / ${formatNumber(item.total)}${unit}`,
|
|
97
|
+
`已用 ${formatNumber(item.used)}${unit} · ${formatNumber(item.percent)}%`,
|
|
98
|
+
].join(" · ")
|
|
99
|
+
}
|
|
100
|
+
const text = item.kind === "display"
|
|
101
|
+
? `${item.name} · 供应商显示 ${item.text}`
|
|
102
|
+
: `${item.name} · ${item.text}`
|
|
103
|
+
return item.detail ? `${text} · ${item.detail}` : text
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function quotaItems(account) {
|
|
47
107
|
const windows = Array.isArray(account?.quota?.windows) ? account.quota.windows : []
|
|
48
|
-
const shown = windows
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return [name, value, unit].filter(Boolean).join(" ")
|
|
54
|
-
}).filter(Boolean)
|
|
55
|
-
if (shown.length) return shown.join(" · ")
|
|
108
|
+
const shown = windows
|
|
109
|
+
.slice(0, 2)
|
|
110
|
+
.map(windowItem)
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
if (shown.length) return shown
|
|
56
113
|
|
|
57
114
|
const balance = typeof account?.quota?.balance === "string"
|
|
58
115
|
? account.quota.balance.trim()
|
|
59
116
|
: ""
|
|
60
|
-
if (balance) return `余额 ${balance}`
|
|
61
|
-
return "额度待检测"
|
|
117
|
+
if (balance) return [{ kind: "balance", name: "额度", text: `余额 ${balance}` }]
|
|
118
|
+
return [{ kind: "pending", name: "额度", text: "额度待检测" }]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function quotaView(account) {
|
|
122
|
+
return quotaItems(account).map(itemText).join(" · ")
|
|
62
123
|
}
|
|
63
124
|
|
|
64
|
-
const api = Object.freeze({ number, formatNumber, quotaView })
|
|
125
|
+
const api = Object.freeze({ number, formatNumber, quotaItems, quotaView })
|
|
65
126
|
global.QuotaKeepaliveFormat = api
|
|
66
127
|
})(globalThis)
|
package/ui/style.css
CHANGED
|
@@ -80,7 +80,7 @@ h2 { margin-bottom: 2px; font-size: 16px; letter-spacing: -.015em; }
|
|
|
80
80
|
.hero-tools { display: flex; align-items: center; gap: 8px; }
|
|
81
81
|
.actions {
|
|
82
82
|
display: grid;
|
|
83
|
-
grid-template-columns: repeat(
|
|
83
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
84
84
|
gap: 6px;
|
|
85
85
|
flex: 1;
|
|
86
86
|
min-width: 0;
|
|
@@ -420,7 +420,7 @@ h2 { margin-bottom: 2px; font-size: 16px; letter-spacing: -.015em; }
|
|
|
420
420
|
|
|
421
421
|
.summary {
|
|
422
422
|
display: grid;
|
|
423
|
-
grid-template-columns: repeat(
|
|
423
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
424
424
|
gap: 10px;
|
|
425
425
|
margin-top: 14px;
|
|
426
426
|
}
|
|
@@ -662,13 +662,41 @@ h2 { margin-bottom: 2px; font-size: 16px; letter-spacing: -.015em; }
|
|
|
662
662
|
.account-name div { min-width: 0; }
|
|
663
663
|
.account-name strong { display: block; font-size: 12.5px; }
|
|
664
664
|
.account-name small { display: block; margin-top: 1px; color: var(--muted); font-size: 10.5px; }
|
|
665
|
+
.account-quota {
|
|
666
|
+
display: grid;
|
|
667
|
+
gap: 7px;
|
|
668
|
+
margin: 8px 0 0 36px;
|
|
669
|
+
padding: 9px 10px;
|
|
670
|
+
border: 1px solid color-mix(in srgb, var(--line) 88%, transparent);
|
|
671
|
+
border-radius: 9px;
|
|
672
|
+
background: color-mix(in srgb, var(--bg) 42%, var(--card));
|
|
673
|
+
}
|
|
674
|
+
.account-quota-head { display: flex; align-items: center; justify-content: space-between; min-height: 16px; }
|
|
675
|
+
.account-quota-title { display: flex; align-items: baseline; gap: 8px; color: var(--ink-2); font-size: 10.5px; }
|
|
676
|
+
.account-quota-title b { font-weight: 700; }
|
|
677
|
+
.account-quota-title span { color: var(--faint); font-size: 9.5px; font-weight: 500; }
|
|
678
|
+
.account-quota-windows { display: grid; gap: 8px; }
|
|
679
|
+
.quota-window { display: grid; gap: 4px; }
|
|
680
|
+
.quota-window-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; min-width: 0; }
|
|
681
|
+
.quota-window-name { overflow: hidden; color: var(--muted); font-size: 10.5px; text-overflow: ellipsis; white-space: nowrap; }
|
|
682
|
+
.quota-window-value { color: var(--muted); font-size: 10.5px; white-space: nowrap; }
|
|
683
|
+
.quota-window-value b { color: var(--ink); font-size: 12px; font-weight: 700; }
|
|
684
|
+
.quota-meter {
|
|
685
|
+
height: 5px;
|
|
686
|
+
overflow: hidden;
|
|
687
|
+
border-radius: 999px;
|
|
688
|
+
background: color-mix(in srgb, var(--faint) 20%, transparent);
|
|
689
|
+
}
|
|
690
|
+
.quota-meter i { display: block; height: 100%; border-radius: inherit; background: var(--green); transition: width .25s ease; }
|
|
691
|
+
.quota-meter.warning i { background: var(--amber); }
|
|
692
|
+
.quota-meter.exhausted i { background: var(--red); }
|
|
693
|
+
.quota-window-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--faint); font-size: 10px; }
|
|
694
|
+
.quota-window-plain .quota-window-value { white-space: normal; text-align: right; }
|
|
695
|
+
.quota-window-plain .quota-window-foot { justify-content: flex-start; }
|
|
696
|
+
.account-quota .quota-spark { width: 54px; height: 16px; padding: 0; background: transparent; overflow: visible; }
|
|
665
697
|
.account-meta { display: flex; flex-wrap: wrap; gap: 6px; margin: 7px 0 0 36px; color: var(--muted); font-size: 10.5px; }
|
|
666
698
|
.account-meta span { padding: 2px 6px; border-radius: 5px; background: color-mix(in srgb, var(--bg) 70%, var(--card)); }
|
|
667
|
-
.account-meta .quota { color: var(--ink-2); }
|
|
668
|
-
.account-meta .quota.up { color: var(--green); }
|
|
669
|
-
.account-meta .quota.down { color: var(--amber); }
|
|
670
699
|
.account-meta .model { color: var(--accent); background: var(--accent-soft); }
|
|
671
|
-
.account-meta .quota-spark { width: 54px; height: 16px; margin-left: -3px; padding: 0; background: transparent; overflow: visible; }
|
|
672
700
|
.quota-spark polyline { fill: none; stroke: var(--faint); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
|
673
701
|
.quota-spark.up polyline { stroke: var(--green); }
|
|
674
702
|
.quota-spark.down polyline { stroke: var(--amber); }
|
|
@@ -1161,6 +1189,7 @@ h2 { margin-bottom: 2px; font-size: 16px; letter-spacing: -.015em; }
|
|
|
1161
1189
|
.section-tools { flex-wrap: wrap; justify-content: flex-end; gap: 6px 10px; }
|
|
1162
1190
|
.provider-tools { align-items: stretch; }
|
|
1163
1191
|
.model-picker { width: 100%; }
|
|
1192
|
+
.account-quota { margin-left: 0; }
|
|
1164
1193
|
.account-meta { margin-left: 0; }
|
|
1165
1194
|
.account-tasks { margin-left: 0; }
|
|
1166
1195
|
.account-task-row { grid-template-columns: 34px minmax(0, 1fr); }
|