sintbot 1.4.16 → 1.4.18
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/CHANGELOG.en.json +62 -0
- package/CHANGELOG.json +62 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/sintbot-v2.js +82 -4
- package/docs/AI-ASSISTED-INSTALL.md +4 -4
- package/docs/install.md +62 -10
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +4 -4
- package/docs/zh-CN/install.md +62 -10
- package/install.sh +1 -1
- package/package.json +4 -1
- package/src/agent/claudecode.js +41 -3
- package/src/agent/claudeusage.js +239 -5
- package/src/agent/envconflicts.js +74 -0
- package/src/agent/privatecore.js +86 -4
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +9 -1
- package/src/c2configcontrol.js +7 -0
- package/src/c2console.js +3 -0
- package/src/c2install.js +1 -1
- package/src/c2multigroupservice.js +1 -1
- package/src/connect.js +566 -0
- package/src/core/c2runnerinput.js +47 -1
- package/src/core/v2engine.js +211 -11
- package/src/platform/feishu.js +22 -7
- package/src/setupserver.js +447 -0
- package/src/ui/console.html +161 -128
- package/src/ui/serverbase.js +19 -10
- package/src/usage.js +131 -38
package/src/ui/serverbase.js
CHANGED
|
@@ -24,6 +24,9 @@ const { codexCatalogWindows, windowForProvider } = require('../agent/modelwindow
|
|
|
24
24
|
const claudestatus = require('../agent/claudestatus') // Claude 订阅登录态(卡片「检测登录」· doctor 同源)
|
|
25
25
|
const claudemodels = require('../agent/claudemodels') // Claude 模型候选(仅登录后给)
|
|
26
26
|
const claudeusage = require('../agent/claudeusage') // Claude 订阅额度(oauth/usage,cc-hud 同源;v0.11.3)
|
|
27
|
+
const { collectEnvConflicts } = require('../agent/envconflicts') // 模型卡「环境冲突」(B5):守护进程 env / settings.json / config.toml,只回键名
|
|
28
|
+
// 探不出来给空数组 —— 这行告警绝不能拖垮登录检测本身。
|
|
29
|
+
const envWarningsFor = (runner, provider) => { try { return collectEnvConflicts({ runner, provider }) } catch { return [] } }
|
|
27
30
|
const codexAgent = require('../agent/codexcode') // codex --version 探针(概览健康,仅配了 codex provider 时)
|
|
28
31
|
const { globalPlugins, codexPlugins, projectPlugins, mcpReachability } = require('../plugins')
|
|
29
32
|
const gitrepo = require('../gitrepo') // 群项目 ↔ git 远端关联(「仓库地址」:init + origin,幂等)+ ls-remote 连通性探测
|
|
@@ -734,7 +737,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
734
737
|
|
|
735
738
|
leftovers: await leftoversState(),
|
|
736
739
|
|
|
737
|
-
codexStatus: codexStatusCache ? { login: codexStatusCache.login, usage: codexStatusCache.usage } : null, // 前端加载回填,codex 卡片刷新不丢
|
|
740
|
+
codexStatus: codexStatusCache ? { login: codexStatusCache.login, usage: codexStatusCache.usage, envWarningsByProvider: codexStatusCache.envWarningsByProvider || {} } : null, // 前端加载回填,codex 卡片刷新不丢
|
|
738
741
|
claudeStatus: claudeStatusCache ? claudeStatusCache.login : null, // 同上,Claude 订阅登录态
|
|
739
742
|
// 服务器时区/时间(顶栏时钟):tz/offsetMin = bot 工作时区(cfg.timezone,默认北京)——
|
|
740
743
|
// 群里/prompt 时间戳用的就是这个口径,运维看到的钟要和群里一致;hostTz 是主机系统时区
|
|
@@ -1033,7 +1036,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
1033
1036
|
codexModelsCache = { at: Date.now(), val }
|
|
1034
1037
|
return sendJSON(res, 200, val)
|
|
1035
1038
|
}
|
|
1036
|
-
if (p === '/api/usage' && req.method === 'GET') { const cfg = loadConfig(configPath); return sendJSON(res, 200, await getUsage(cfg.providers)) }
|
|
1039
|
+
if (p === '/api/usage' && req.method === 'GET') { const cfg = loadConfig(configPath); return sendJSON(res, 200, await getUsage(cfg.providers, { bin: cfg.agent.bin })) }
|
|
1037
1040
|
// 模型卡「额度」行的 ↻:只刷这一个 provider 且绕过两层缓存(见 usage.js refreshUsage)。
|
|
1038
1041
|
// 分工 = 整页 /api/usage 走 5 分钟缓存,手点才现拉;codex 走自家 /api/codex/verify 的 force。
|
|
1039
1042
|
if (p === '/api/usage/refresh' && req.method === 'POST') {
|
|
@@ -1041,7 +1044,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
1041
1044
|
const cfg = loadConfig(configPath)
|
|
1042
1045
|
const prov = cfg.providers && cfg.providers[name]
|
|
1043
1046
|
if (!prov) return sendJSON(res, 400, { error: `未知模型 '${name}'` })
|
|
1044
|
-
return sendJSON(res, 200, { name, usage: await refreshUsage(name, prov) })
|
|
1047
|
+
return sendJSON(res, 200, { name, usage: await refreshUsage(name, prov, { bin: cfg.agent.bin }) })
|
|
1045
1048
|
}
|
|
1046
1049
|
|
|
1047
1050
|
if (p === '/api/provider-key' && req.method === 'POST') {
|
|
@@ -2473,10 +2476,12 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
2473
2476
|
// 前端不显示额度行,不拖垮登录检测本身。
|
|
2474
2477
|
let usage = null
|
|
2475
2478
|
if (login && login.ok && login.loggedIn) {
|
|
2476
|
-
try { if (force) claudeusage.clearCache(); usage = await claudeusage.usageLine() } catch { usage = null }
|
|
2479
|
+
try { if (force) claudeusage.clearCache(); usage = await claudeusage.usageLine({ bin: cfg.agent.bin }) } catch { usage = null }
|
|
2477
2480
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2481
|
+
// 订阅卡 = 无 baseUrl、无 key 的直连形态;环境里的 ANTHROPIC_API_KEY 对它是合法鉴权,核心据此不报。
|
|
2482
|
+
const envWarnings = envWarningsFor('claude', { baseUrl: null, hasApiKey: false })
|
|
2483
|
+
claudeStatusCache = { at: Date.now(), login: { ...login, usage, envWarnings } }
|
|
2484
|
+
return sendJSON(res, 200, { ...login, usage, envWarnings })
|
|
2480
2485
|
}
|
|
2481
2486
|
// Claude 模型下拉的候选。**未登录不给任何型号**(产品决定:没登录就别列)。
|
|
2482
2487
|
// 已登录时 = 内置候选 + 本机账号的额外可选项;为什么不是官方实时清单,见 claudemodels.js 头部。
|
|
@@ -2498,8 +2503,9 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
2498
2503
|
// 会把 usage 字段洗掉,而 /api/claude/verify 的 60s 去抖随后会把这份缺 usage 的缓存原样
|
|
2499
2504
|
// 吐回去 —— 表现就是卡片上的额度行凭空消失,而登录态一切正常,没人查得出为什么。
|
|
2500
2505
|
if (login && login.ok) {
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2506
|
+
const prev = (claudeStatusCache && claudeStatusCache.login) || {}
|
|
2507
|
+
const keep = prev.usage
|
|
2508
|
+
claudeStatusCache = { at: Date.now(), login: { ...login, ...(keep ? { usage: keep } : {}), ...(prev.envWarnings ? { envWarnings: prev.envWarnings } : {}) } }
|
|
2503
2509
|
}
|
|
2504
2510
|
return sendJSON(res, 200, { ...claudemodels.listModels({ loggedIn: !!(login && login.ok && login.loggedIn) }), login })
|
|
2505
2511
|
}
|
|
@@ -2516,11 +2522,14 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
2516
2522
|
const probe = await codexstatus.cachedAuthProbe('codex')
|
|
2517
2523
|
const file = codexstatus.readAuthFile()
|
|
2518
2524
|
const d = codexstatus.describeLogin(probe, file)
|
|
2525
|
+
// B5:每张 codex 卡各算一份(baseUrl / key 不同,核心的判据不同);OPENAI_* / config.toml 的 model_provider,只有键名
|
|
2526
|
+
const envWarningsByProvider = Object.fromEntries(Object.entries(loadConfig(configPath).providers || {})
|
|
2527
|
+
.filter(([, x]) => x && x.runner === 'codex').map(([n, x]) => [n, envWarningsFor('codex', x)]))
|
|
2519
2528
|
const login = { ok: true, verified: !!probe.verified, loggedIn: !!(probe.ok || (!probe.verified && file.exists && !file.expired)), dead: !!probe.dead, mode: file.mode || 'chatgpt', level: d.level, text: d.text }
|
|
2520
2529
|
let usage = probe.usage || null
|
|
2521
2530
|
if (!usage && !probe.dead && codexStatusCache && codexStatusCache.usage) usage = codexStatusCache.usage // 探不到时保住旧额度(同前端那条「同一份状态两个消费者」的教训)
|
|
2522
|
-
codexStatusCache = { at: Date.now(), login, usage } // cached into /api/state so the card survives a page reload
|
|
2523
|
-
return sendJSON(res, 200, { login, usage })
|
|
2531
|
+
codexStatusCache = { at: Date.now(), login, usage, envWarningsByProvider } // cached into /api/state so the card survives a page reload
|
|
2532
|
+
return sendJSON(res, 200, { login, usage, envWarningsByProvider })
|
|
2524
2533
|
}
|
|
2525
2534
|
if (p === '/api/playground/run' && req.method === 'POST') {
|
|
2526
2535
|
const { provider: provName, workdir, prompt, sessionId } = await body(req)
|
package/src/usage.js
CHANGED
|
@@ -1,19 +1,44 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
// Per-provider account balance / plan usage — the numbers cc-hud surfaces, for
|
|
2
4
|
// the console. Server-side (keys never reach the browser), best-effort, cached
|
|
3
|
-
// 5 min
|
|
4
|
-
//
|
|
5
|
+
// 5 min. The host only moves bytes: which endpoint, which headers, how to render.
|
|
6
|
+
// **Every response body is judged by the binary core** (`usage_parse`, ABI 36):
|
|
7
|
+
// HTTP status + body → windows / balance / one of five error kinds. The host never
|
|
8
|
+
// re-reads a provider's JSON shape, so one rule set serves the console, /hud and
|
|
9
|
+
// the quota alert alike.
|
|
10
|
+
//
|
|
11
|
+
// Common rules (2026-11-12, cc-switch takeaways B1):
|
|
12
|
+
// · 15 s per request · real HTTP status goes to the core (network error ⇒ null)
|
|
13
|
+
// · `credentials` ⇒ the card shows「凭据失效」instead of going blank
|
|
14
|
+
// · `transient` ⇒ keep the last good value of that provider for 10 min
|
|
15
|
+
// · `malformed` / `none` ⇒ null (nothing to show)
|
|
16
|
+
// · never log a response body or a key; the last-good cache is keyed by a key hash
|
|
17
|
+
|
|
18
|
+
const crypto = require('node:crypto')
|
|
19
|
+
const { usageParse } = require('./agent/privatecore')
|
|
5
20
|
|
|
6
21
|
const CACHE = new Map() // provider name -> { val, ts }
|
|
7
22
|
const TTL = 5 * 60 * 1000
|
|
23
|
+
const REQUEST_TIMEOUT_MS = 15_000
|
|
24
|
+
const LAST_GOOD_TTL_MS = 10 * 60 * 1000
|
|
25
|
+
const LAST_GOOD = new Map() // kind|baseUrl|sha256(key)[:16] -> { val, ts }
|
|
8
26
|
|
|
9
|
-
|
|
27
|
+
const CREDENTIALS_VALUE = Object.freeze({ label: '额度', value: '凭据失效' })
|
|
28
|
+
|
|
29
|
+
// One request → { status, body }. Network error / timeout ⇒ status null (the core
|
|
30
|
+
// files it under `transient`). Non-JSON bodies travel as text so the core can still
|
|
31
|
+
// say `malformed`. The body is handed to the core and dropped — never logged.
|
|
32
|
+
async function request(url, headers, { fetchImpl = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
|
|
10
33
|
const ac = new AbortController()
|
|
11
|
-
const timer = setTimeout(() => ac.abort(),
|
|
34
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs)
|
|
12
35
|
try {
|
|
13
|
-
const r = await
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
const r = await fetchImpl(url, { headers, signal: ac.signal })
|
|
37
|
+
const text = await r.text()
|
|
38
|
+
let body = text
|
|
39
|
+
try { body = JSON.parse(text) } catch { /* 非 JSON:原文交给核心判 malformed */ }
|
|
40
|
+
return { status: r.status, body }
|
|
41
|
+
} catch { return { status: null, body: null } } finally { clearTimeout(timer) }
|
|
17
42
|
}
|
|
18
43
|
|
|
19
44
|
// baseUrl → 哪一家的余额接口(null = 这家根本没有公开余额接口,如火山 coding plan)。
|
|
@@ -25,40 +50,107 @@ const balanceKind = (base = '') =>
|
|
|
25
50
|
: base.includes('minimax') ? 'minimax'
|
|
26
51
|
: null
|
|
27
52
|
|
|
28
|
-
|
|
53
|
+
// 核心的 windows[] → 与 Claude 订阅额度同一种一行:`5h:42% │ 7d:10% (5d)`。
|
|
54
|
+
// 复用 claudeusage.formatClaudeUsage,窗口名映射:session=5h · weekly=7d · weekly_scoped=模型名。
|
|
55
|
+
function formatWindows(windows, now = Date.now()) {
|
|
56
|
+
const { formatClaudeUsage } = require('./agent/claudeusage')
|
|
57
|
+
const limits = (Array.isArray(windows) ? windows : []).map((w) => ({
|
|
58
|
+
kind: w.kind === 'weekly' ? 'weekly_all' : w.kind,
|
|
59
|
+
percent: w.percentUsed,
|
|
60
|
+
resetsAt: w.resetsAt,
|
|
61
|
+
scopeName: w.scopeName || '',
|
|
62
|
+
}))
|
|
63
|
+
return formatClaudeUsage(limits, now)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const CURRENCY_SIGN = { CNY: '¥', RMB: '¥', USD: '$' }
|
|
67
|
+
function formatBalance(balance) {
|
|
68
|
+
if (!balance || typeof balance.amount !== 'number' || !Number.isFinite(balance.amount)) return null
|
|
69
|
+
const sign = CURRENCY_SIGN[String(balance.currency || '').toUpperCase()]
|
|
70
|
+
return sign ? `${sign}${balance.amount.toFixed(2)}` : `${balance.amount.toFixed(2)} ${balance.currency || ''}`.trim()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 核心的成功结果 → 卡片一行;窗口优先(额度),否则余额;两者皆无 ⇒ null。
|
|
74
|
+
function render(parsed, now) {
|
|
75
|
+
const windows = formatWindows(parsed.windows, now)
|
|
76
|
+
if (windows) return { label: '额度', value: windows }
|
|
77
|
+
const balance = formatBalance(parsed.balance)
|
|
78
|
+
if (balance) return { label: '余额', value: balance }
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 每家的取数顺序。返回核心的 usage_parse 结果(成功或失败),不在这里解释。
|
|
83
|
+
async function probeGlm(base, key, io) {
|
|
84
|
+
const host = base.includes('api.z.ai') ? 'https://api.z.ai' : 'https://open.bigmodel.cn'
|
|
85
|
+
const now = io.now()
|
|
86
|
+
// coding plan 配额(5 小时 / 周窗口)。鉴权头不带 Bearer(智谱监控接口的口径)。
|
|
87
|
+
const quota = await request(`${host}/api/monitor/usage/quota/limit`,
|
|
88
|
+
{ Authorization: key, 'Accept-Language': 'en-US,en' }, io)
|
|
89
|
+
const q = usageParse('glm', 'quota', quota.status, quota.body, now)
|
|
90
|
+
if (q.ok || (q.error !== 'noplan' && q.error !== 'credentials' && q.error !== 'transient')) return q
|
|
91
|
+
// 没有 coding plan ⇒ 按量付费账户,退回余额;配额接口拒绝/抖动时也问一次余额 —— 两个接口
|
|
92
|
+
// 鉴权口径未必相同,只有**两边都**说凭据不对才显示「凭据失效」;任一边抖动按瞬时算(保上次好值)。
|
|
93
|
+
const bal = await request(`${host}/api/biz/account/query-customer-account-report`, { Authorization: `Bearer ${key}` }, io)
|
|
94
|
+
const b = usageParse('glm', 'balance', bal.status, bal.body, io.now())
|
|
95
|
+
if (b.ok) return b
|
|
96
|
+
if (q.error === 'credentials' && b.error === 'credentials') return q
|
|
97
|
+
if (q.error === 'transient' || b.error === 'transient') return { ok: false, error: 'transient', detail: 'glm quota/balance transient' }
|
|
98
|
+
return b.error === 'credentials' ? { ok: false, error: 'malformed', detail: 'glm balance credentials without quota agreement' } : b
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function probeMinimax(base, key, io) {
|
|
102
|
+
const host = base.includes('minimaxi.com') ? 'https://api.minimaxi.com' : 'https://api.minimax.io'
|
|
103
|
+
const headers = { Authorization: `Bearer ${key}` }
|
|
104
|
+
const plan = await request(`${host}/v1/api/openplatform/coding_plan/remains`, headers, io)
|
|
105
|
+
const p = usageParse('minimax', 'quota', plan.status, plan.body, io.now())
|
|
106
|
+
if (p.ok || (p.error !== 'malformed' && p.error !== 'none')) return p
|
|
107
|
+
// 旧接口(Token Plan):形状相同,只在新接口认不得时退回
|
|
108
|
+
const legacy = await request(`${host}/v1/token_plan/remains`, headers, io)
|
|
109
|
+
return usageParse('minimax', 'quota', legacy.status, legacy.body, io.now())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function probeDeepseek(_base, key, io) {
|
|
113
|
+
const r = await request('https://api.deepseek.com/user/balance', { Authorization: `Bearer ${key}` }, io)
|
|
114
|
+
return usageParse('deepseek', 'balance', r.status, r.body, io.now())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const PROBES = { glm: probeGlm, minimax: probeMinimax, deepseek: probeDeepseek }
|
|
118
|
+
|
|
119
|
+
const keyHash = (key) => crypto.createHash('sha256').update(String(key)).digest('hex').slice(0, 16)
|
|
120
|
+
|
|
121
|
+
// 单个 provider → { label, value } | null。`io`:bin(cfg.agent.bin,登录态 claude 的兜底要用)
|
|
122
|
+
// 与可测注入(fetchImpl / now / timeoutMs)。
|
|
123
|
+
async function providerUsage(provider, io = {}) {
|
|
29
124
|
const key = provider.apiKey
|
|
30
125
|
const base = provider.baseUrl || ''
|
|
31
126
|
// Claude 订阅登录形态(无 baseUrl、非 codex):额度走 oauth/usage(cc-hud 同源通道,
|
|
32
|
-
// v0.11.3;凭据取本机 Claude Code 登录态,拿不到/未登录 ⇒ null
|
|
127
|
+
// v0.11.3;凭据取本机 Claude Code 登录态,拿不到/未登录 ⇒ 走 claudeusage 的兜底,仍可能 null)。
|
|
33
128
|
if (!base && provider.runner !== 'codex') {
|
|
34
|
-
const v = await require('./agent/claudeusage').usageLine()
|
|
129
|
+
const v = await require('./agent/claudeusage').usageLine(io.bin ? { bin: io.bin } : {})
|
|
35
130
|
return v ? { label: '订阅额度', value: v } : null
|
|
36
131
|
}
|
|
37
132
|
if (!key) return null
|
|
38
133
|
|
|
39
134
|
const kind = balanceKind(base)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (v == null) return null
|
|
52
|
-
return { label: '余额', value: typeof v === 'number' ? `¥${v.toFixed(2)}` : `¥${String(v).replace(/^¥/, '')}` }
|
|
135
|
+
const probe = PROBES[kind]
|
|
136
|
+
if (!probe) return null // volcano / claude: no public balance API
|
|
137
|
+
const now = typeof io.now === 'function' ? io.now : Date.now
|
|
138
|
+
const ctx = { fetchImpl: io.fetchImpl || globalThis.fetch, timeoutMs: io.timeoutMs || REQUEST_TIMEOUT_MS, now }
|
|
139
|
+
const goodKey = `${kind}|${base}|${keyHash(key)}`
|
|
140
|
+
|
|
141
|
+
const parsed = await probe(base, key, ctx)
|
|
142
|
+
if (parsed.ok) {
|
|
143
|
+
const v = render(parsed, now())
|
|
144
|
+
if (v) LAST_GOOD.set(goodKey, { val: v, ts: now() })
|
|
145
|
+
return v
|
|
53
146
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return { label: '额度', value: `5h:${Math.min(100, Math.max(0, Math.round(100 - m.current_interval_remaining_percent)))}%` } // 口径统一为「已用」(v0.11.8);接口给的是剩余 ⇒ 换算
|
|
147
|
+
if (parsed.error === 'credentials') { LAST_GOOD.delete(goodKey); return { ...CREDENTIALS_VALUE } }
|
|
148
|
+
if (parsed.error === 'transient') {
|
|
149
|
+
const last = LAST_GOOD.get(goodKey)
|
|
150
|
+
if (last && now() - last.ts < LAST_GOOD_TTL_MS) return last.val
|
|
151
|
+
LAST_GOOD.delete(goodKey)
|
|
60
152
|
}
|
|
61
|
-
return null //
|
|
153
|
+
return null // malformed / none / stale transient
|
|
62
154
|
}
|
|
63
155
|
|
|
64
156
|
// 能不能给这个 provider 查额度 —— getUsage 与 refreshUsage 共用的单一判据(两处各写一份
|
|
@@ -83,13 +175,13 @@ const hasUsageApi = (p) => !!p && p.runner !== 'codex' && !(p.baseUrl && !p.apiK
|
|
|
83
175
|
&& (isClaudeLogin(p) || !!balanceKind(p.baseUrl || ''))
|
|
84
176
|
|
|
85
177
|
// Fetch balances for all keyed providers (resolved apiKey). Returns { name: {label,value} }.
|
|
86
|
-
async function getUsage(providers = {}) {
|
|
178
|
+
async function getUsage(providers = {}, { bin } = {}) {
|
|
87
179
|
const out = {}
|
|
88
180
|
await Promise.all(Object.entries(providers).map(async ([name, p]) => {
|
|
89
181
|
if (!queryable(p)) return
|
|
90
182
|
const c = CACHE.get(name)
|
|
91
183
|
if (c && Date.now() - c.ts < TTL) { out[name] = c.val; return }
|
|
92
|
-
const v = await providerUsage(p)
|
|
184
|
+
const v = await providerUsage(p, bin ? { bin } : {})
|
|
93
185
|
if (v) { CACHE.set(name, { val: v, ts: Date.now() }); out[name] = v }
|
|
94
186
|
else if (c) out[name] = c.val // stale fallback
|
|
95
187
|
}))
|
|
@@ -100,12 +192,12 @@ async function getUsage(providers = {}) {
|
|
|
100
192
|
// 本模块的 5 分钟 CACHE,以及登录态 claude 那条通道自己的 60s TTL(与 /api/claude/verify 同口径)。
|
|
101
193
|
// ⚠ 拉不到时把上一次的值留在缓存里 —— 手点刷新失败不该让卡片上原本显示着的额度行凭空消失
|
|
102
194
|
// (旧 ts 保留 ⇒ 它照样算过期,下一次 getUsage 仍会重拉)。probe/now 注入是为了可单测。
|
|
103
|
-
async function refreshUsage(name, provider, { probe = providerUsage, now = Date.now } = {}) {
|
|
195
|
+
async function refreshUsage(name, provider, { probe = providerUsage, now = Date.now, bin } = {}) {
|
|
104
196
|
if (!queryable(provider)) { CACHE.delete(name); return null }
|
|
105
197
|
const prev = CACHE.get(name)
|
|
106
198
|
CACHE.delete(name)
|
|
107
199
|
if (isClaudeLogin(provider)) { try { require('./agent/claudeusage').clearCache() } catch { /* best-effort */ } }
|
|
108
|
-
const v = await probe(provider).catch(() => null)
|
|
200
|
+
const v = await probe(provider, bin ? { bin } : {}).catch(() => null)
|
|
109
201
|
if (v) CACHE.set(name, { val: v, ts: now() })
|
|
110
202
|
else if (prev) CACHE.set(name, prev)
|
|
111
203
|
return v
|
|
@@ -121,7 +213,8 @@ async function refreshUsage(name, provider, { probe = providerUsage, now = Date.
|
|
|
121
213
|
// 而这两句话给群成员的下一步完全相反(一个别再问,一个过会儿再问)。
|
|
122
214
|
// 缓存:**成功压 60s,失败只压 10s**(与 claudeusage 同口径)—— 失败也压一分钟,等于一次
|
|
123
215
|
// 偶发超时让整整一分钟查不到;完全不压又让连点 /hud 直捅端点限流。
|
|
124
|
-
|
|
216
|
+
// `bin` = cfg.agent.bin:登录态 claude 的额度兜底要起它(见 claudeusage),不传就是 PATH 上的 claude。
|
|
217
|
+
function createUsageProbe({ codexUsage = async () => null, probe = providerUsage, ttl = 60_000, failTtl = 10_000, now = Date.now, bin } = {}) {
|
|
125
218
|
const cache = new Map() // provider name + '|' + baseUrl -> { v, ts }
|
|
126
219
|
return async (name, provider) => {
|
|
127
220
|
const isCodex = !!provider && provider.runner === 'codex'
|
|
@@ -134,11 +227,11 @@ function createUsageProbe({ codexUsage = async () => null, probe = providerUsage
|
|
|
134
227
|
let v = null
|
|
135
228
|
try {
|
|
136
229
|
if (isCodex) v = await codexUsage(provider)
|
|
137
|
-
else { const u = await probe(provider); v = u ? `${u.label} ${u.value}` : null }
|
|
230
|
+
else { const u = await probe(provider, bin ? { bin } : {}); v = u ? `${u.label} ${u.value}` : null }
|
|
138
231
|
} catch { v = null }
|
|
139
232
|
cache.set(key, { v: v || null, ts: now() })
|
|
140
233
|
return { supported: true, value: v || null }
|
|
141
234
|
}
|
|
142
235
|
}
|
|
143
236
|
|
|
144
|
-
module.exports = { getUsage, providerUsage, refreshUsage, queryable, hasUsageApi, createUsageProbe }
|
|
237
|
+
module.exports = { getUsage, providerUsage, refreshUsage, queryable, hasUsageApi, createUsageProbe, REQUEST_TIMEOUT_MS, LAST_GOOD_TTL_MS }
|