quota-keepalive 0.1.10 → 0.1.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quota-keepalive",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Unofficial multi-provider quota inventory, check-in, keepalive scheduling, history, and quota trends for Magpie",
5
5
  "keywords": [
6
6
  "magpie",
package/src/inventory.js CHANGED
@@ -261,12 +261,37 @@ function historyByAccount(history) {
261
261
  return out
262
262
  }
263
263
 
264
- export function quotaByAccount(quotas) {
264
+ function refreshedQuotaCard(card) {
265
+ if (!object(card) || object(card.quota)) return null
266
+ const provider = sanitize(card.provider, 80).trim()
267
+ const user = sanitize(card.user, 240).trim()
268
+ if (!provider || !user || sanitize(card.error, 300).trim()) return null
269
+ const windows = (Array.isArray(card.windows) ? card.windows : []).map(publicWindow)
270
+ const plan = sanitize(card.plan, 80)
271
+ const balance = typeof card.balance === "number" && Number.isFinite(card.balance)
272
+ ? String(card.balance)
273
+ : sanitize(card.balance, 80)
274
+ if (!windows.length && !plan && !balance) return null
275
+ const readAt = sanitize(card.readAt || card.asOf || card.at, 40)
276
+ return {
277
+ key: cardKey(provider, user).toLowerCase(),
278
+ entry: {
279
+ at: readAt,
280
+ quota: { plan, balance, windows, readAt },
281
+ },
282
+ }
283
+ }
284
+
285
+ export function quotaByAccount(quotas, quotaCards = []) {
265
286
  const out = new Map()
266
287
  for (const [key, entry] of Object.entries(object(quotas) ? quotas : {})) {
267
288
  if (!object(entry?.quota)) continue
268
289
  out.set(key.toLowerCase(), entry)
269
290
  }
291
+ for (const card of Array.isArray(quotaCards) ? quotaCards : []) {
292
+ const refreshed = refreshedQuotaCard(card)
293
+ if (refreshed) out.set(refreshed.key, refreshed.entry)
294
+ }
270
295
  return out
271
296
  }
272
297
 
@@ -581,6 +606,7 @@ export async function readInventory({
581
606
  logPath = "",
582
607
  records = [],
583
608
  accountsData = null,
609
+ quotaCards = [],
584
610
  } = {}) {
585
611
  const [providers, checkins, quotas, histories, auth] = await Promise.all([
586
612
  readJson(join(configHome, "plugin-providers.json"), []),
@@ -589,7 +615,7 @@ export async function readInventory({
589
615
  readJson(join(configHome, "quota-history.json"), {}),
590
616
  readJson(join(configHome, "plugin-auth.json"), {}),
591
617
  ])
592
- const cards = quotaByAccount(quotas)
618
+ const cards = quotaByAccount(quotas, quotaCards)
593
619
  const history = historyByAccount(histories)
594
620
  const activity = activityIndex(records)
595
621
  const sourceProviders = Array.isArray(providers) ? providers : []
package/src/runner.js CHANGED
@@ -290,6 +290,7 @@ export async function runRefresh({
290
290
  }
291
291
 
292
292
  const accounts = []
293
+ const quotaCards = []
293
294
  for (const id of providers) {
294
295
  const { result, body } = await runJsonCommand({
295
296
  args: ["quota", "refresh", id, "--json"],
@@ -298,6 +299,7 @@ export async function runRefresh({
298
299
  timeoutMs: Math.max(30_000, Math.min(50_000, Number(config.timeoutMs) || 50_000)),
299
300
  })
300
301
  const classified = classifyRefreshResult(result, body)
302
+ if (Array.isArray(classified.cards)) quotaCards.push(...classified.cards)
301
303
  accounts.push({
302
304
  provider: id,
303
305
  account: "",
@@ -324,7 +326,7 @@ export async function runRefresh({
324
326
  console.warn(`[quota-keepalive] cannot write quota refresh log: ${errorText(error)}`)
325
327
  })
326
328
  }
327
- await refreshSnapshot(config, null)
329
+ await refreshSnapshot(config, null, quotaCards)
328
330
  const success = accounts.filter((item) => item.status === "success").length
329
331
  const failed = accounts.filter((item) => item.status === "failed").length
330
332
  const skipped = accounts.filter((item) => item.status === "skipped").length
@@ -358,13 +360,14 @@ async function logCommand(config, entry, run = newRunId()) {
358
360
  })
359
361
  }
360
362
 
361
- async function refreshSnapshot(config, accountsData) {
363
+ async function refreshSnapshot(config, accountsData, quotaCards = []) {
362
364
  await writeDailySnapshot({
363
365
  ...(config.configHome ? { configHome: config.configHome } : {}),
364
366
  logPath: config.logPath,
365
367
  snapshotPath: config.snapshotPath,
366
368
  commandPath: config.commandPath,
367
369
  accountsData,
370
+ quotaCards,
368
371
  }).catch((error) => {
369
372
  console.warn(`[daily-keepalive] ${snapshotWarning(error)}`)
370
373
  })
package/src/snapshot.js CHANGED
@@ -278,6 +278,7 @@ export async function writeDailySnapshot(options = {}) {
278
278
  snapshotPath = defaultSnapshotPath(),
279
279
  configHome = undefined,
280
280
  accountsData = null,
281
+ quotaCards = [],
281
282
  commandPath = undefined,
282
283
  } = options
283
284
  const writeCompanion = options.writeCompanion ??
@@ -302,6 +303,7 @@ export async function writeDailySnapshot(options = {}) {
302
303
  logPath,
303
304
  records,
304
305
  accountsData,
306
+ quotaCards,
305
307
  })
306
308
  if (!Array.isArray(accountsData)) {
307
309
  inventory = carryForwardQuotas(inventory, await readPreviousSnapshot(snapshotPath))
package/ui/app.js CHANGED
@@ -434,6 +434,22 @@
434
434
  </div>
435
435
  </div>`
436
436
  }
437
+ if (item.kind === "percent") {
438
+ const progress = Math.max(0, Math.min(100, number(item.progress)))
439
+ const label = `${item.name}已用 ${formatNumber(item.percent)}%`
440
+ return `<div class="quota-window">
441
+ <div class="quota-window-head">
442
+ <span class="quota-window-name">${escapeHtml(item.name)}</span>
443
+ <span class="quota-window-value">已用 ${escapeHtml(formatNumber(item.percent))}%</span>
444
+ </div>
445
+ <div class="quota-meter ${escapeHtml(item.level)}" role="progressbar"
446
+ aria-label="${escapeHtml(label)}" aria-valuemin="0" aria-valuemax="100"
447
+ aria-valuenow="${escapeHtml(formatNumber(progress))}">
448
+ <i style="width:${progress}%"></i>
449
+ </div>
450
+ ${item.detail ? `<div class="quota-window-foot">${escapeHtml(item.detail)}</div>` : ""}
451
+ </div>`
452
+ }
437
453
  const detail = item.kind === "display"
438
454
  ? "供应商原样显示,不推断已用或剩余"
439
455
  : item.detail || ""
@@ -15,6 +15,27 @@
15
15
 
16
16
  const clampPercent = (value) => Math.max(0, Math.min(100, number(value)))
17
17
 
18
+ function displayAmount(value) {
19
+ const match = String(value || "").trim().match(
20
+ /^(\d+(?:\.\d*)?|\.\d+)(e[+-]?\d+)?([kmb])?$/i,
21
+ )
22
+ if (!match) return null
23
+ const scale = { k: 1e3, m: 1e6, b: 1e9 }[String(match[3] || "").toLowerCase()] || 1
24
+ const parsed = Number(`${match[1]}${match[2] || ""}`) * scale
25
+ return Number.isFinite(parsed) ? parsed : null
26
+ }
27
+
28
+ function displayPair(display) {
29
+ const pair = String(display || "").split("·", 1)[0].match(
30
+ /^\s*([0-9.,]+(?:e[+-]?\d+)?[kmb]?)\s*\/\s*([0-9.,]+(?:e[+-]?\d+)?[kmb]?)(?:\s+([a-zA-Z]+))?\s*$/i,
31
+ )
32
+ if (!pair) return null
33
+ const first = displayAmount(pair[1].replace(/,/g, ""))
34
+ const total = displayAmount(pair[2].replace(/,/g, ""))
35
+ if (first === null || total === null || total <= 0) return null
36
+ return { first, total, unit: pair[3] || "" }
37
+ }
38
+
18
39
  function quotaLevel(percent) {
19
40
  if (percent >= 100) return "exhausted"
20
41
  if (percent >= 85) return "warning"
@@ -66,6 +87,22 @@
66
87
  return meterItem(name, unit, (limit * Math.max(0, usedPercent)) / 100, limit, usedPercent)
67
88
  }
68
89
  }
90
+ const pair = limit === null && usedPercent !== null ? displayPair(display) : null
91
+ if (pair) {
92
+ const usedShare = (pair.first / pair.total) * 100
93
+ const usedMatches = Math.abs(usedShare - usedPercent) < 0.75
94
+ const remainingMatches = Math.abs(100 - usedShare - usedPercent) < 0.75
95
+ if (usedMatches || !remainingMatches) {
96
+ return meterItem(name, unit || pair.unit, pair.first, pair.total, usedPercent)
97
+ }
98
+ return meterItem(
99
+ name,
100
+ unit || pair.unit,
101
+ pair.total - pair.first,
102
+ pair.total,
103
+ usedPercent,
104
+ )
105
+ }
69
106
  const balance = number(window?.balance)
70
107
  if (balance !== null) {
71
108
  return {
@@ -75,11 +112,15 @@
75
112
  }
76
113
  }
77
114
  if (usedPercent !== null) {
115
+ const percent = clampPercent(usedPercent)
78
116
  return {
79
117
  kind: "percent",
80
118
  name,
81
119
  text: `已用 ${formatNumber(usedPercent)}%`,
82
120
  detail: display ? `供应商显示 ${display}` : "",
121
+ percent,
122
+ progress: percent,
123
+ level: quotaLevel(percent),
83
124
  }
84
125
  }
85
126
  if (display) {