sintbot 1.4.16 → 1.4.17
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 +31 -0
- package/CHANGELOG.json +31 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/sintbot-v2.js +17 -2
- package/docs/AI-ASSISTED-INSTALL.md +4 -4
- package/docs/install.md +43 -7
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +4 -4
- package/docs/zh-CN/install.md +43 -7
- package/install.sh +1 -1
- package/package.json +3 -1
- package/src/agent/claudeusage.js +188 -3
- package/src/agent/envconflicts.js +74 -0
- package/src/agent/privatecore.js +65 -4
- package/src/agent/privatecore.wasm +0 -0
- package/src/c2install.js +1 -1
- package/src/connect.js +488 -0
- package/src/platform/feishu.js +22 -7
- package/src/ui/console.html +154 -124
- package/src/ui/serverbase.js +19 -10
- package/src/usage.js +131 -38
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 }
|