sintbot 1.0.15 → 1.3.0
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 +118 -0
- package/CHANGELOG.json +118 -0
- package/README.md +20 -156
- package/README.zh-CN.md +20 -164
- package/THIRD-PARTY-NOTICES.md +394 -0
- package/bin/sintbot-v2.js +363 -0
- package/docs/AI-ASSISTED-INSTALL.md +19 -0
- package/docs/install.md +93 -0
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +19 -0
- package/docs/zh-CN/install.md +93 -0
- package/install.sh +84 -0
- package/package.json +74 -34
- package/src/agent/claudecode.js +149 -108
- package/src/agent/codexapp.js +458 -0
- package/src/agent/codexcode.js +210 -107
- package/src/agent/errtag.js +34 -0
- package/src/agent/privatecore.js +790 -0
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +10 -8
- package/src/agent/winexec.js +35 -5
- package/src/c2install.js +117 -0
- package/src/config.js +2 -1
- package/src/core/c2cardeffectbridge.js +51 -0
- package/src/core/c2cardterminalpath.js +216 -0
- package/src/core/c2durableingress.js +148 -0
- package/src/core/c2durablejournal.js +1560 -0
- package/src/core/c2durableruntime.js +2361 -0
- package/src/core/c2eventidentity.js +90 -0
- package/src/core/c2globalfault.js +401 -0
- package/src/core/c2localeffectexecutor.js +193 -0
- package/src/core/c2payloadhandles.js +333 -0
- package/src/core/c2persistrelation.js +179 -0
- package/src/core/c2persiststate.js +342 -0
- package/src/core/c2platformrecoverysender.js +76 -0
- package/src/core/c2recoverygate.js +385 -0
- package/src/core/c2runnerinput.js +63 -0
- package/src/core/c2servicestartgate.js +103 -0
- package/src/core/c2sessionbridge.js +72 -0
- package/src/core/c2sourcequarantine.js +30 -0
- package/src/core/c2startuprecovery.js +105 -0
- package/src/core/c2successorbridge.js +151 -0
- package/src/core/c2terminaleffectbridge.js +154 -0
- package/src/core/c2terminalprune.js +108 -0
- package/src/core/c2turnprojection.js +177 -0
- package/src/core/enginecheckpoint.js +211 -0
- package/src/core/engineeffectcursor.js +394 -0
- package/src/core/enginestepcoordinator.js +183 -0
- package/src/core/store.js +52 -8
- package/src/core/v2engine.js +1469 -0
- package/src/platform/feishu.js +324 -67
- package/src/plugins.js +1 -9
- package/assets/design-styles/acid-pop.md +0 -155
- package/assets/design-styles/archive-index.md +0 -161
- package/assets/design-styles/art-deco.md +0 -164
- package/assets/design-styles/cinematic-noir.md +0 -178
- package/assets/design-styles/editorial.md +0 -121
- package/assets/design-styles/extra/README.md +0 -23
- package/assets/design-styles/extra/constructivism.md +0 -146
- package/assets/design-styles/extra/neo-brutalism.md +0 -175
- package/assets/design-styles/extra/new-wave.md +0 -166
- package/assets/design-styles/extra/nordic-material.md +0 -162
- package/assets/design-styles/extra/paris-couture.md +0 -162
- package/assets/design-styles/extra/tropical-modern.md +0 -168
- package/assets/design-styles/extra/ulm-functional.md +0 -182
- package/assets/design-styles/extra/wa-shikimen.md +0 -160
- package/assets/design-styles/extra/y2k-chrome.md +0 -172
- package/assets/design-styles/geometric-pop.md +0 -122
- package/assets/design-styles/hand-drawn.md +0 -117
- package/assets/design-styles/islamic-geometry.md +0 -155
- package/assets/design-styles/liquid-glass.md +0 -183
- package/assets/design-styles/neo-chinese.md +0 -136
- package/assets/design-styles/precision-mono.md +0 -156
- package/assets/design-styles/quiet-luxury.md +0 -163
- package/assets/design-styles/riso-print.md +0 -116
- package/assets/design-styles/soft-friendly.md +0 -174
- package/assets/design-styles/swiss-grid.md +0 -156
- package/assets/design-styles/tech-dark.md +0 -182
- package/assets/favicon.svg +0 -6
- package/assets/logo.svg +0 -24
- package/bin/sbot.js +0 -4711
- package/docs/slack-app-manifest.yaml +0 -104
- package/sbot.config.example.json +0 -235
- package/src/agent/askonce.js +0 -76
- package/src/agent/claudemodels.js +0 -60
- package/src/agent/claudestatus.js +0 -74
- package/src/agent/claudeusage.js +0 -200
- package/src/agent/codexstatus.js +0 -181
- package/src/agent/imagen.js +0 -458
- package/src/agent/quotaerr.js +0 -44
- package/src/agent/transienterr.js +0 -32
- package/src/cdp.js +0 -254
- package/src/clilang.js +0 -31
- package/src/core/cron.js +0 -182
- package/src/core/cronfire.js +0 -120
- package/src/core/cronrun.js +0 -116
- package/src/core/engine.js +0 -2762
- package/src/core/interfaces.js +0 -114
- package/src/core/issues.js +0 -185
- package/src/core/ledger.js +0 -30
- package/src/core/opsalert.js +0 -214
- package/src/core/pending.js +0 -123
- package/src/core/proxy.js +0 -424
- package/src/core/quotaalert.js +0 -79
- package/src/core/quotaerr.js +0 -55
- package/src/core/scheduler.js +0 -59
- package/src/design.js +0 -5161
- package/src/gitrepo.js +0 -150
- package/src/health.js +0 -353
- package/src/installdeps.js +0 -72
- package/src/leftover.js +0 -174
- package/src/platform/boardnotice.js +0 -77
- package/src/platform/boardops.js +0 -36
- package/src/platform/boardwatch.js +0 -447
- package/src/platform/cli.js +0 -45
- package/src/platform/slack.js +0 -1289
- package/src/platform/takeovercard.js +0 -37
- package/src/platform/wecom.js +0 -938
- package/src/platform/wecomcli.js +0 -128
- package/src/platform/wecomgate.js +0 -108
- package/src/platform/wecomname.js +0 -43
- package/src/platform/wecompoll.js +0 -241
- package/src/preview.js +0 -581
- package/src/projectmemory.js +0 -175
- package/src/service.js +0 -317
- package/src/ui/auth.js +0 -31
- package/src/ui/browser.html +0 -392
- package/src/ui/browserroutes.js +0 -234
- package/src/ui/console.html +0 -5494
- package/src/ui/dashboard.js +0 -168
- package/src/ui/feishulogin.js +0 -136
- package/src/ui/server.js +0 -2314
- package/src/ui/sessions.js +0 -78
- package/src/ui/takeover.js +0 -108
- package/src/updater.js +0 -243
- package/src/upgrade.js +0 -35
- package/src/usage.js +0 -144
package/src/agent/claudeusage.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
// Claude 订阅额度(v0.11.3)—— 控制台 Claude 卡「订阅额度」行 + 群内 /hud 用。
|
|
2
|
-
//
|
|
3
|
-
// 通道与 cc-hud(operator 自家 statusline 项目)同源,2026-08-03 拍板采用:
|
|
4
|
-
// 端点 = `https://api.anthropic.com/api/oauth/usage`(交互式 /usage 面板同一数据源),
|
|
5
|
-
// `Authorization: Bearer <claudeAiOauth.accessToken>` + `anthropic-beta: oauth-2025-04-20`。
|
|
6
|
-
// `limits[]` 三档(本机 2026-08-03 实测):kind=session(5h 窗口)/ weekly_all / weekly_scoped
|
|
7
|
-
// (带 scope.model.display_name,= 顶级模型周窗口,如 Fable);percent 为**已用**百分比。
|
|
8
|
-
//
|
|
9
|
-
// ⚠ 三条纪律(照 cc-hud 踩通的路,别走样):
|
|
10
|
-
// 1. Cloudflare WAF 对 /api/oauth/* 按 **TLS 指纹** 403 —— Node 的 fetch **恒被拒**、curl 放行
|
|
11
|
-
// (与网络环境无关,换机器换代理都一样)。所以这条路只走 curl,native fetch 不再试
|
|
12
|
-
// (那一发必 403 还白占端点的限流计数);`fetchNative` 保留不删 —— 哪天官方放行,
|
|
13
|
-
// 回接只是 fetchRaw 里加一行。curl 的 config 走 **stdin**,token 绝不进 argv(ps 可见)、
|
|
14
|
-
// 绝不落日志。
|
|
15
|
-
// 2. 凭据**只读不刷新**:accessToken 过期就当拿不到(返回 null 显示「未验证」),token 轮换
|
|
16
|
-
// 是 Claude Code 自己的事 —— 代刷会跟它抢 refresh token,两边都掉登录。
|
|
17
|
-
// 3. 端点又慢又娇:走本机代理的好路也要 1.1~2.7s,限流阈值低(十几分钟十几次就 429 且持续
|
|
18
|
-
// 数分钟)。故超时给足 5s(调用方都是异步回复,多等两秒无感)、失败隔一下重试一次、
|
|
19
|
-
// 失败**不长缓存**(见 fetchRaw)。
|
|
20
|
-
//
|
|
21
|
-
// 全程 best-effort:任何一步失败返回 null,调用方不显示额度行,绝不拖垮健康检查/卡片渲染。
|
|
22
|
-
// 但**失败要留一行痕**:此前 curl 带 `--fail` 把状态码连同 body 一起吞掉,超时/429/500 在
|
|
23
|
-
// 日志里长得一模一样(什么都没有),排查只能靠猜。
|
|
24
|
-
|
|
25
|
-
const fs = require('node:fs')
|
|
26
|
-
const os = require('node:os')
|
|
27
|
-
const path = require('node:path')
|
|
28
|
-
const { execFile, execFileSync } = require('node:child_process')
|
|
29
|
-
|
|
30
|
-
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage'
|
|
31
|
-
const TIMEOUT_MS = 5000 // curl 预算(纪律 3);native fetch 已不走这条路
|
|
32
|
-
const RETRY_WAIT_MS = 1500 // 失败后隔多久重试(一次为限)
|
|
33
|
-
const FAIL_TTL_MS = 10_000 // 失败缓存窗口,见 fetchRaw
|
|
34
|
-
|
|
35
|
-
// ~/.claude/.credentials.json(Windows / Linux)或 macOS Keychain 的 JSON → accessToken。
|
|
36
|
-
// 过期 ⇒ null(纪律 2)。纯函数,单测钉住。
|
|
37
|
-
function parseCredentials(raw, now = Date.now()) {
|
|
38
|
-
let j = null
|
|
39
|
-
try { j = JSON.parse(String(raw || '')) } catch { return null }
|
|
40
|
-
const o = j && j.claudeAiOauth
|
|
41
|
-
if (!o || typeof o.accessToken !== 'string' || !o.accessToken) return null
|
|
42
|
-
if (typeof o.expiresAt === 'number' && o.expiresAt <= now) return null
|
|
43
|
-
return o.accessToken
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function readAccessToken(home = os.homedir()) {
|
|
47
|
-
// 凭据文件优先(win/linux;存在即终局 —— 文件过期意味着同账号同过期,不再翻 Keychain)
|
|
48
|
-
let raw = null
|
|
49
|
-
try { raw = fs.readFileSync(path.join(home, '.claude', '.credentials.json'), 'utf8') } catch { /* 无文件 */ }
|
|
50
|
-
if (raw !== null) return parseCredentials(raw)
|
|
51
|
-
if (process.platform === 'darwin') {
|
|
52
|
-
try {
|
|
53
|
-
const out = execFileSync('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'],
|
|
54
|
-
{ encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
55
|
-
return parseCredentials(out)
|
|
56
|
-
} catch { /* 无条目 / 拒绝访问 */ }
|
|
57
|
-
}
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// limits[] → [{kind, percent(已用), resetsAt(ms|null), scopeName}]。防御式:形状不对给 []。
|
|
62
|
-
function parseLimits(data) {
|
|
63
|
-
const arr = data && Array.isArray(data.limits) ? data.limits : []
|
|
64
|
-
const out = []
|
|
65
|
-
for (const l of arr) {
|
|
66
|
-
if (!l || typeof l.kind !== 'string' || typeof l.percent !== 'number') continue
|
|
67
|
-
const t = Date.parse(l.resets_at || '')
|
|
68
|
-
out.push({
|
|
69
|
-
kind: l.kind,
|
|
70
|
-
percent: l.percent,
|
|
71
|
-
resetsAt: Number.isFinite(t) ? t : null,
|
|
72
|
-
scopeName: (l.scope && l.scope.model && typeof l.scope.model.display_name === 'string') ? l.scope.model.display_name : '',
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
return out
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 紧凑一行(v0.11.8 口径改版,operator 拍板):`5h:98% (1.8h) │ 7d:12% (6.1d) │ Fable:12%`
|
|
79
|
-
// ⚠ **百分比是「已用」不是「剩余」** —— 与 v0.11.3~0.11.7 的「剩 x%」正好相反,四处落点
|
|
80
|
-
// (本函数 / console 的 codex 卡 / usage.js 的 minimax 行 / 群内 /hud)必须同版改齐,
|
|
81
|
-
// 否则同屏两种口径,「98%」会被读成还剩很多。窗口名前缀 + 括号内距重置,不留裸百分比。
|
|
82
|
-
// 分隔符 │(U+2502),两侧空格。
|
|
83
|
-
// 一位小数的紧凑时长(4.0h ⇒ 4h:整数不留尾零;≥10 直接取整,两位数带小数太挤)
|
|
84
|
-
const fmtDurDecimal = (ms) => {
|
|
85
|
-
const trim = (v) => (v < 10 ? String(Number(v.toFixed(1))) : String(Math.round(v)))
|
|
86
|
-
const h = ms / 3600e3
|
|
87
|
-
if (h >= 24) return `${trim(h / 24)}d`
|
|
88
|
-
if (h >= 1) return `${trim(h)}h`
|
|
89
|
-
return `${Math.max(1, Math.round(ms / 60e3))}m`
|
|
90
|
-
}
|
|
91
|
-
const QUOTA_SEP = ' │ '
|
|
92
|
-
|
|
93
|
-
function formatClaudeUsage(limits, now = Date.now()) {
|
|
94
|
-
if (!Array.isArray(limits) || !limits.length) return null
|
|
95
|
-
const reset = (l) => (l.resetsAt && l.resetsAt > now && l.resetsAt - now < 9 * 864e5) ? ` (${fmtDurDecimal(l.resetsAt - now)})` : ''
|
|
96
|
-
// 窗口名:session=5h(官方就是 5 小时滚动窗)· weekly_all=7d · weekly_scoped=该模型名
|
|
97
|
-
const label = (l) => l.kind === 'session' ? '5h' : l.kind === 'weekly_all' ? '7d' : l.kind === 'weekly_scoped' ? (l.scopeName || '顶级模型') : ''
|
|
98
|
-
const parts = []
|
|
99
|
-
for (const l of limits) {
|
|
100
|
-
const lb = label(l)
|
|
101
|
-
if (!lb) continue // 未知窗口类型不猜(官方加新档时宁缺毋滥)
|
|
102
|
-
parts.push(`${lb}:${Math.min(100, Math.max(0, Math.round(l.percent)))}%${reset(l)}`)
|
|
103
|
-
}
|
|
104
|
-
return parts.join(QUOTA_SEP) || null
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ⚠ 当前**不走**这条腿(纪律 1:Node 的 TLS 指纹被 Cloudflare 恒 403)。保留函数是为了
|
|
108
|
-
// 哪天官方放行时回接成本近似为零 —— 别因为「没人调」把它删掉。
|
|
109
|
-
async function fetchNative(token) {
|
|
110
|
-
const ac = new AbortController()
|
|
111
|
-
const timer = setTimeout(() => ac.abort(), TIMEOUT_MS)
|
|
112
|
-
try {
|
|
113
|
-
const r = await fetch(USAGE_URL, { headers: { Authorization: `Bearer ${token}`, 'anthropic-beta': 'oauth-2025-04-20' }, signal: ac.signal })
|
|
114
|
-
if (!r.ok) return null
|
|
115
|
-
return await r.json()
|
|
116
|
-
} catch { return null } finally { clearTimeout(timer) }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// curl 收尾判读(纯函数,单测钉死):(err, stdout) → { data, note }。
|
|
120
|
-
// stdout 的**最后一行**是 `--write-out` 写的 HTTP 状态码 —— 不再用 `--fail`,它把状态码和 body
|
|
121
|
-
// 一起吞了,超时/限流/服务端错在日志里长得一模一样。note = 该记的一行人话(成功给 null,
|
|
122
|
-
// 成功不打日志);note 里只放状态码与本地错因,绝不带 token 或响应头。
|
|
123
|
-
function readCurlResult(err, stdout) {
|
|
124
|
-
if (err) {
|
|
125
|
-
// 退出码 28 = 取数命令自己的 max-time 到点;killed/SIGTERM = 外层兜底超时掐的
|
|
126
|
-
const exit = typeof err.code === 'number' ? err.code : null
|
|
127
|
-
if (err.killed || err.signal === 'SIGTERM' || exit === 28 || String(err.code || '') === 'ETIMEDOUT')
|
|
128
|
-
return { data: null, note: `额度接口超时(${TIMEOUT_MS}ms 未返回)` }
|
|
129
|
-
const why = exit != null ? `退出码 ${exit}` : String((err.message || '').split('\n')[0] || '未知错误').slice(0, 120)
|
|
130
|
-
return { data: null, note: `额度接口调用失败(${why})` }
|
|
131
|
-
}
|
|
132
|
-
const raw = String(stdout == null ? '' : stdout)
|
|
133
|
-
const cut = raw.lastIndexOf('\n')
|
|
134
|
-
const status = Number((cut >= 0 ? raw.slice(cut + 1) : raw).trim())
|
|
135
|
-
const body = cut >= 0 ? raw.slice(0, cut) : ''
|
|
136
|
-
if (!Number.isFinite(status) || !status) return { data: null, note: '额度接口没给出状态码(响应形状不认识)' }
|
|
137
|
-
if (status === 429) return { data: null, note: '额度接口限流(429),等一会儿才会恢复' }
|
|
138
|
-
if (status !== 200) return { data: null, note: `额度接口返回 ${status}` }
|
|
139
|
-
try { return { data: JSON.parse(body), note: null } } catch { return { data: null, note: '额度接口返回的不是 JSON(解析失败)' } }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const warn = (m) => { try { process.stderr.write('· ' + m + '\n') } catch { /* stderr 关了也不能崩 */ } }
|
|
143
|
-
|
|
144
|
-
// 唯一的取数腿:config 经 stdin(token 不进 argv);token 字符集先校验,防 config 注入。
|
|
145
|
-
function fetchCurl(token, { log = warn, exec = execFile } = {}) {
|
|
146
|
-
if (!/^[\w.\-~+/=]+$/.test(token)) return Promise.resolve(null)
|
|
147
|
-
return new Promise((resolve) => {
|
|
148
|
-
try {
|
|
149
|
-
const child = exec('curl', ['--config', '-'], { timeout: TIMEOUT_MS + 1000, windowsHide: true }, (err, stdout) => {
|
|
150
|
-
const { data, note } = readCurlResult(err, stdout)
|
|
151
|
-
if (note) log(note)
|
|
152
|
-
resolve(data)
|
|
153
|
-
})
|
|
154
|
-
child.stdin.on('error', () => { /* curl 可能在 stdin 写完前退出 */ })
|
|
155
|
-
child.stdin.end([
|
|
156
|
-
`url = "${USAGE_URL}"`,
|
|
157
|
-
`header = "Authorization: Bearer ${token}"`,
|
|
158
|
-
'header = "anthropic-beta: oauth-2025-04-20"',
|
|
159
|
-
'silent', `max-time = ${Math.ceil(TIMEOUT_MS / 1000)}`,
|
|
160
|
-
'write-out = "\\n%{http_code}"', // 末行状态码:失败要留痕,见 readCurlResult
|
|
161
|
-
].join('\n'))
|
|
162
|
-
} catch (e) { log('额度接口起不来:' + ((e && e.message) || '未知错误')); resolve(null) }
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
167
|
-
|
|
168
|
-
// 原始额度 JSON。**缓存与 usageLine 共用同一份** —— 额度告警每轮结算都会问一次,不共用
|
|
169
|
-
// 就是每轮多打一次接口(v0.11.8 新增,给 quotaalert 判定用)。
|
|
170
|
-
// ⚠ **只有成功才缓存 60s;失败只压 10 秒**:失败也缓存一分钟,等于一次偶发超时就让整整一分钟
|
|
171
|
-
// 里所有人都查不到额度(而这接口恰恰偶发超时/限流);完全不压又让连点几下就把限流捅穿。
|
|
172
|
-
// 10 秒 = 人再问一次就真的重试,机器连点仍被挡住。
|
|
173
|
-
const RAW = { at: 0, data: null }
|
|
174
|
-
async function fetchRaw(token, { ttl = 60_000, now = Date.now, log = warn, curl = fetchCurl, sleep = sleepMs } = {}) {
|
|
175
|
-
if (RAW.at && now() - RAW.at < (RAW.data ? ttl : FAIL_TTL_MS)) return RAW.data
|
|
176
|
-
// 只走 curl(纪律 1);失败隔一下再问一次 —— 超时与短限流重试一次往往就通,再不通就认了。
|
|
177
|
-
let data = await curl(token, { log })
|
|
178
|
-
if (!data) { await sleep(RETRY_WAIT_MS); data = await curl(token, { log }) }
|
|
179
|
-
RAW.at = now(); RAW.data = data
|
|
180
|
-
return data
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 拉一次 → 一行人话;拿不到 null。走 fetchRaw ⇒ 与告警共享那份缓存。
|
|
184
|
-
// 缓存口径与 fetchRaw 一致:成功 60s、失败 10s(两层都压死一分钟就白搭下层的重试了)。
|
|
185
|
-
const CACHE = { at: 0, val: null }
|
|
186
|
-
const TTL = 60_000
|
|
187
|
-
async function usageLine({ ttl = TTL, now = Date.now, readToken = readAccessToken, ...io } = {}) {
|
|
188
|
-
if (CACHE.at && now() - CACHE.at < (CACHE.val ? ttl : FAIL_TTL_MS)) return CACHE.val
|
|
189
|
-
let val = null
|
|
190
|
-
const token = readToken()
|
|
191
|
-
if (token) {
|
|
192
|
-
const data = await fetchRaw(token, { ttl, now, ...io })
|
|
193
|
-
val = formatClaudeUsage(parseLimits(data), now())
|
|
194
|
-
}
|
|
195
|
-
CACHE.at = now(); CACHE.val = val
|
|
196
|
-
return val
|
|
197
|
-
}
|
|
198
|
-
function clearCache() { CACHE.at = 0; CACHE.val = null; RAW.at = 0; RAW.data = null }
|
|
199
|
-
|
|
200
|
-
module.exports = { usageLine, fetchRaw, clearCache, parseCredentials, parseLimits, formatClaudeUsage, readAccessToken, fetchCurl, fetchNative, readCurlResult, fmtDurDecimal, QUOTA_SEP, TIMEOUT_MS, FAIL_TTL_MS, RETRY_WAIT_MS }
|
package/src/agent/codexstatus.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
// Codex subscription introspection — login status + rate-limit (quota) usage, for the
|
|
2
|
-
// console's codex model card. Two headless probes, neither needs an API key:
|
|
3
|
-
// loginStatus() → `codex login status` (exit 0 = logged in; prints the auth mode)
|
|
4
|
-
// rateLimits() → short-lived `codex app-server` JSON-RPC (newline-framed over stdio):
|
|
5
|
-
// initialize → initialized → account/rateLimits/read → quit.
|
|
6
|
-
// The exec event stream can't carry rate limits (exec responses lack the x-codex-*
|
|
7
|
-
// headers, openai/codex#14728), so app-server is the only headless source. Protocol
|
|
8
|
-
// shape confirmed against a working driver; exact behaviour is verified on the target
|
|
9
|
-
// machine. Both return best-effort and never throw.
|
|
10
|
-
|
|
11
|
-
const { spawn } = require('node:child_process')
|
|
12
|
-
const { IS_WIN, resolveSpawn } = require('./winexec')
|
|
13
|
-
|
|
14
|
-
// `codex login status` output → { loggedIn, mode }. The line typically reads
|
|
15
|
-
// "Logged in using ChatGPT" (subscription) or mentions an API key.
|
|
16
|
-
function parseLoginStatus(stdout, code) {
|
|
17
|
-
const text = (stdout || '').trim()
|
|
18
|
-
let mode = ''
|
|
19
|
-
if (/chatgpt/i.test(text)) mode = 'chatgpt'
|
|
20
|
-
else if (/api[\s-]?key/i.test(text)) mode = 'apikey'
|
|
21
|
-
return { loggedIn: code === 0, mode, text: text.slice(0, 200) }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function loginStatus(bin = 'codex', timeoutMs = 8000) {
|
|
25
|
-
return new Promise((resolve) => {
|
|
26
|
-
const { file, spawnArgs } = resolveSpawn(bin, ['login', 'status'])
|
|
27
|
-
let out = '', err = '', settled = false
|
|
28
|
-
const done = (v) => { if (settled) return; settled = true; clearTimeout(timer); resolve(v) }
|
|
29
|
-
const child = spawn(file, spawnArgs, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
30
|
-
const timer = setTimeout(() => { try { child.kill() } catch {} done({ ok: false, error: 'timeout' }) }, timeoutMs)
|
|
31
|
-
child.stdout.on('data', (c) => { out += c.toString('utf8') })
|
|
32
|
-
child.stderr.on('data', (c) => { err += c.toString('utf8') })
|
|
33
|
-
child.on('error', (e) => done({ ok: false, error: e.code === 'ENOENT' ? "'codex' not found on PATH" : e.message }))
|
|
34
|
-
child.on('close', (code) => done({ ok: true, ...parseLoginStatus(out || err, code) }))
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// TTL 缓存(与 claudestatus 同款):doctor 是一次性的,但控制台 /api/state 每次动作 + 60s 自动刷新
|
|
39
|
-
// 都会重建健康行,每次都 spawn 一个进程没必要。
|
|
40
|
-
const LOGIN_CACHE = new Map() // bin -> { at, val }
|
|
41
|
-
async function cachedLoginStatus(bin = 'codex', ttl = 60_000) {
|
|
42
|
-
const c = LOGIN_CACHE.get(bin)
|
|
43
|
-
if (c && Date.now() - c.at < ttl) return c.val
|
|
44
|
-
const val = await loginStatus(bin)
|
|
45
|
-
LOGIN_CACHE.set(bin, { at: Date.now(), val })
|
|
46
|
-
return val
|
|
47
|
-
}
|
|
48
|
-
function clearLoginCache() { LOGIN_CACHE.clear() }
|
|
49
|
-
|
|
50
|
-
// account/rateLimits/read response → compact usage. Windows: primary = 5h rolling
|
|
51
|
-
// window, secondary = weekly (see docs/CODEX-RUNNER.md). resetsAt is an epoch (secs).
|
|
52
|
-
function mapRateLimits(result) {
|
|
53
|
-
const rl = result && result.rateLimits
|
|
54
|
-
if (!rl) return null
|
|
55
|
-
const w = (x) => (x && typeof x.usedPercent === 'number')
|
|
56
|
-
? { usedPercent: x.usedPercent, windowMins: x.windowDurationMins, resetsAt: x.resetsAt } : null
|
|
57
|
-
const out = { primary: w(rl.primary), secondary: w(rl.secondary), planType: rl.planType || '' }
|
|
58
|
-
if (rl.credits) out.credits = { balance: rl.credits.balance, hasCredits: !!rl.credits.hasCredits, unlimited: !!rl.credits.unlimited }
|
|
59
|
-
return (out.primary || out.secondary || out.credits) ? out : null
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function killTree(child) {
|
|
63
|
-
if (!child || !child.pid) return
|
|
64
|
-
if (IS_WIN) { try { spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true }) } catch {} }
|
|
65
|
-
else { try { child.kill('SIGKILL') } catch {} }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Generic short-lived `codex app-server` JSON-RPC call: initialize → initialized →
|
|
69
|
-
// <method> → map → quit. One JSON object per line. Returns mapped value or null
|
|
70
|
-
// (never throws). Cache results upstream — each call spawns a process and may hit
|
|
71
|
-
// the ChatGPT backend.
|
|
72
|
-
function appServerCall(bin, method, params, mapFn, timeoutMs = 12000) {
|
|
73
|
-
return new Promise((resolve) => {
|
|
74
|
-
let child
|
|
75
|
-
try {
|
|
76
|
-
const { file, spawnArgs } = resolveSpawn(bin, ['app-server'])
|
|
77
|
-
child = spawn(file, spawnArgs, { stdio: ['pipe', 'pipe', 'ignore'], windowsHide: true })
|
|
78
|
-
} catch { return resolve(null) }
|
|
79
|
-
|
|
80
|
-
let settled = false
|
|
81
|
-
const finish = (v) => { if (settled) return; settled = true; clearTimeout(timer); killTree(child); resolve(v) }
|
|
82
|
-
const timer = setTimeout(() => finish(null), timeoutMs)
|
|
83
|
-
child.on('error', () => finish(null))
|
|
84
|
-
child.stdin.on('error', () => {})
|
|
85
|
-
|
|
86
|
-
const send = (obj) => { try { child.stdin.write(JSON.stringify(obj) + '\n') } catch {} }
|
|
87
|
-
const INIT_ID = 1, CALL_ID = 2
|
|
88
|
-
send({ jsonrpc: '2.0', id: INIT_ID, method: 'initialize', params: { clientInfo: { name: 'sbot', title: 'sbot', version: '0.1.0' }, capabilities: { experimentalApi: true } } })
|
|
89
|
-
|
|
90
|
-
let buf = ''
|
|
91
|
-
child.stdout.on('data', (chunk) => {
|
|
92
|
-
buf += chunk.toString('utf8')
|
|
93
|
-
let nl
|
|
94
|
-
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
95
|
-
const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1)
|
|
96
|
-
if (!line) continue
|
|
97
|
-
let msg; try { msg = JSON.parse(line) } catch { continue }
|
|
98
|
-
// Responses carry id + result/error and NO method; ignore notifications /
|
|
99
|
-
// server→client requests (none expected without a running turn).
|
|
100
|
-
if (msg.method) continue
|
|
101
|
-
if (msg.id === INIT_ID) { send({ jsonrpc: '2.0', method: 'initialized' }); send({ jsonrpc: '2.0', id: CALL_ID, method, params: params || {} }) }
|
|
102
|
-
else if (msg.id === CALL_ID) return finish(msg.error ? null : mapFn(msg.result))
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Subscription rate limits (5h/weekly windows) — see mapRateLimits.
|
|
109
|
-
function rateLimits(bin = 'codex', timeoutMs = 12000) {
|
|
110
|
-
return appServerCall(bin, 'account/rateLimits/read', {}, mapRateLimits, timeoutMs)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// model/list 响应 → 精简模型清单(过滤 hidden;efforts 兼容字符串/对象两形)。
|
|
114
|
-
// 空/坏返回 → null(上游回退内置预设)。协议 v2,codex 0.144.3 真机验证(2026-07)。
|
|
115
|
-
function mapModels(result) {
|
|
116
|
-
const data = (result && result.data) || []
|
|
117
|
-
const out = []
|
|
118
|
-
for (const x of data) {
|
|
119
|
-
if (!x || x.hidden || !(x.model || x.id)) continue
|
|
120
|
-
out.push({
|
|
121
|
-
model: x.model || x.id,
|
|
122
|
-
name: x.displayName || x.model || x.id,
|
|
123
|
-
desc: x.description || '',
|
|
124
|
-
isDefault: !!x.isDefault,
|
|
125
|
-
efforts: Array.isArray(x.supportedReasoningEfforts)
|
|
126
|
-
? x.supportedReasoningEfforts.map((e) => (e && e.reasoningEffort) || e).filter((e) => typeof e === 'string')
|
|
127
|
-
: [],
|
|
128
|
-
defEffort: x.defaultReasoningEffort || '',
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
return out.length ? out : null
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 当前账号可用的 codex 模型(控制台 codex 面板下拉的数据源)。
|
|
135
|
-
function listModels(bin = 'codex', timeoutMs = 12000) {
|
|
136
|
-
return appServerCall(bin, 'model/list', {}, mapModels, timeoutMs)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 探测不到(未登录/老版本/超时)时的内置预设 —— 2026-07 实测自 codex 0.144.3 的
|
|
140
|
-
// model/list 真实返回;随 sbot 版本按需更新。UI 恒留「自定义」逃生口。
|
|
141
|
-
// ⚠ efforts 已剔除 'ultra':model/list 把它列进 sol/terra,但服务端不认 —— 2026-07-20
|
|
142
|
-
// 真机用非法值触发参数校验拿到权威合法集:'none','minimal','low','medium','high','xhigh','max'。
|
|
143
|
-
// 前端另有 RE_LEGAL 兜底过滤(live 探测结果同样会带 ultra,源头改不了)。
|
|
144
|
-
const PRESET_MODELS = [
|
|
145
|
-
{ model: 'gpt-5.6-sol', name: 'GPT-5.6-Sol', desc: 'Latest frontier agentic coding model.', isDefault: true, efforts: ['low', 'medium', 'high', 'xhigh', 'max'], defEffort: 'low' },
|
|
146
|
-
{ model: 'gpt-5.6-terra', name: 'GPT-5.6-Terra', desc: 'Balanced agentic coding model for everyday work.', isDefault: false, efforts: ['low', 'medium', 'high', 'xhigh', 'max'], defEffort: 'medium' },
|
|
147
|
-
{ model: 'gpt-5.6-luna', name: 'GPT-5.6-Luna', desc: 'Fast and affordable agentic coding model.', isDefault: false, efforts: ['low', 'medium', 'high', 'xhigh', 'max'], defEffort: 'medium' },
|
|
148
|
-
{ model: 'gpt-5.5', name: 'GPT-5.5', desc: 'Frontier model for complex coding and research.', isDefault: false, efforts: ['low', 'medium', 'high', 'xhigh'], defEffort: 'medium' },
|
|
149
|
-
{ model: 'gpt-5.4', name: 'GPT-5.4', desc: 'Strong model for everyday coding.', isDefault: false, efforts: ['low', 'medium', 'high', 'xhigh'], defEffort: 'medium' },
|
|
150
|
-
{ model: 'gpt-5.4-mini', name: 'GPT-5.4-Mini', desc: 'Small, fast, and cost-efficient.', isDefault: false, efforts: ['low', 'medium', 'high', 'xhigh'], defEffort: 'medium' },
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
// A window's label DERIVED from its duration, not its position. Codex changed the shape:
|
|
154
|
-
// a window that used to sit in `primary` as 5h (≈300min) now arrives as the weekly window
|
|
155
|
-
// (10080min) with `secondary: null`, so the old positional `primary='5h'` mislabeled a
|
|
156
|
-
// weekly window as "5h". Read windowMins instead: ~300 ⇒ "5h", 10080 ⇒ "周".
|
|
157
|
-
// 窗口名(v0.11.8 起用于紧凑格式 `7d:12%`,故周窗口标 7d 而非「周」,与 Claude 卡一致)
|
|
158
|
-
function windowLabel(mins) {
|
|
159
|
-
if (!mins) return '窗口'
|
|
160
|
-
if (mins >= 1440) return `${Math.round(mins / 1440)}d`
|
|
161
|
-
return `${Math.round(mins / 60)}h`
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Mapped rate-limits → a compact one-line string for the group's /hud reply.
|
|
165
|
-
// Mirrors the console's fmtUsage (client-side) but runs in the daemon.
|
|
166
|
-
// Date.now() is real here (Node), so the reset countdown is live.
|
|
167
|
-
// 紧凑一行(v0.11.8 与 Claude 卡同口径):`5h:98% (1.8h) │ 7d:12% (6.1d)`。
|
|
168
|
-
// ⚠ **百分比是「已用」不是「剩余」** —— 这里是群内 /hud 对 codex 群的实际渲染,与
|
|
169
|
-
// claudeusage.formatClaudeUsage / console 的 fmtUsage / usage.js 的 minimax 行必须同口径,
|
|
170
|
-
// 漏改任何一处就会出现同一个数字两种含义(98% 既可能是快满也可能是还剩很多)。
|
|
171
|
-
function formatUsage(u) {
|
|
172
|
-
if (!u) return null
|
|
173
|
-
const dur = (ms) => { const trim = (v) => (v < 10 ? String(Number(v.toFixed(1))) : String(Math.round(v))); const h = ms / 3600e3; if (h >= 24) return `${trim(h / 24)}d`; if (h >= 1) return `${trim(h)}h`; return `${Math.max(1, Math.round(ms / 60e3))}m` }
|
|
174
|
-
const reset = (w) => { if (!w || w.resetsAt == null) return ''; const ms = w.resetsAt > 1e9 ? w.resetsAt * 1000 - Date.now() : w.resetsAt * 1000; return (ms > 0 && ms < 9 * 864e5) ? ` (${dur(ms)})` : '' }
|
|
175
|
-
const win = (w) => w ? `${windowLabel(w.windowMins)}:${Math.min(100, Math.max(0, Math.round(w.usedPercent || 0)))}%${reset(w)}` : ''
|
|
176
|
-
const parts = [win(u.primary), win(u.secondary)].filter(Boolean)
|
|
177
|
-
if (u.credits && u.credits.hasCredits && u.credits.balance != null) parts.push(`credits ${u.credits.balance}`)
|
|
178
|
-
return parts.join(' │ ') || null
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
module.exports = { loginStatus, cachedLoginStatus, clearLoginCache, rateLimits, listModels, parseLoginStatus, mapRateLimits, mapModels, formatUsage, windowLabel, PRESET_MODELS }
|