sintbot 1.0.16 → 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.
Files changed (137) hide show
  1. package/CHANGELOG.en.json +99 -0
  2. package/CHANGELOG.json +99 -0
  3. package/README.md +20 -158
  4. package/README.zh-CN.md +20 -166
  5. package/THIRD-PARTY-NOTICES.md +394 -0
  6. package/bin/sintbot-v2.js +363 -0
  7. package/docs/AI-ASSISTED-INSTALL.md +11 -35
  8. package/docs/install.md +61 -167
  9. package/docs/zh-CN/AI-ASSISTED-INSTALL.md +11 -35
  10. package/docs/zh-CN/install.md +61 -165
  11. package/install.sh +84 -0
  12. package/package.json +73 -37
  13. package/src/agent/claudecode.js +149 -108
  14. package/src/agent/codexapp.js +458 -0
  15. package/src/agent/codexcode.js +210 -107
  16. package/src/agent/errtag.js +34 -0
  17. package/src/agent/privatecore.js +790 -0
  18. package/src/agent/privatecore.wasm +0 -0
  19. package/src/agent/runner.js +10 -8
  20. package/src/agent/winexec.js +35 -5
  21. package/src/c2install.js +117 -0
  22. package/src/config.js +2 -1
  23. package/src/core/c2cardeffectbridge.js +51 -0
  24. package/src/core/c2cardterminalpath.js +216 -0
  25. package/src/core/c2durableingress.js +148 -0
  26. package/src/core/c2durablejournal.js +1560 -0
  27. package/src/core/c2durableruntime.js +2361 -0
  28. package/src/core/c2eventidentity.js +90 -0
  29. package/src/core/c2globalfault.js +401 -0
  30. package/src/core/c2localeffectexecutor.js +193 -0
  31. package/src/core/c2payloadhandles.js +333 -0
  32. package/src/core/c2persistrelation.js +179 -0
  33. package/src/core/c2persiststate.js +342 -0
  34. package/src/core/c2platformrecoverysender.js +76 -0
  35. package/src/core/c2recoverygate.js +385 -0
  36. package/src/core/c2runnerinput.js +63 -0
  37. package/src/core/c2servicestartgate.js +103 -0
  38. package/src/core/c2sessionbridge.js +72 -0
  39. package/src/core/c2sourcequarantine.js +30 -0
  40. package/src/core/c2startuprecovery.js +105 -0
  41. package/src/core/c2successorbridge.js +151 -0
  42. package/src/core/c2terminaleffectbridge.js +154 -0
  43. package/src/core/c2terminalprune.js +108 -0
  44. package/src/core/c2turnprojection.js +177 -0
  45. package/src/core/enginecheckpoint.js +211 -0
  46. package/src/core/engineeffectcursor.js +394 -0
  47. package/src/core/enginestepcoordinator.js +183 -0
  48. package/src/core/store.js +52 -8
  49. package/src/core/v2engine.js +1469 -0
  50. package/src/platform/feishu.js +324 -67
  51. package/src/plugins.js +1 -9
  52. package/assets/design-styles/acid-pop.md +0 -155
  53. package/assets/design-styles/archive-index.md +0 -161
  54. package/assets/design-styles/art-deco.md +0 -164
  55. package/assets/design-styles/cinematic-noir.md +0 -178
  56. package/assets/design-styles/editorial.md +0 -121
  57. package/assets/design-styles/extra/README.md +0 -23
  58. package/assets/design-styles/extra/constructivism.md +0 -146
  59. package/assets/design-styles/extra/neo-brutalism.md +0 -175
  60. package/assets/design-styles/extra/new-wave.md +0 -166
  61. package/assets/design-styles/extra/nordic-material.md +0 -162
  62. package/assets/design-styles/extra/paris-couture.md +0 -162
  63. package/assets/design-styles/extra/tropical-modern.md +0 -168
  64. package/assets/design-styles/extra/ulm-functional.md +0 -182
  65. package/assets/design-styles/extra/wa-shikimen.md +0 -160
  66. package/assets/design-styles/extra/y2k-chrome.md +0 -172
  67. package/assets/design-styles/geometric-pop.md +0 -122
  68. package/assets/design-styles/hand-drawn.md +0 -117
  69. package/assets/design-styles/islamic-geometry.md +0 -155
  70. package/assets/design-styles/liquid-glass.md +0 -183
  71. package/assets/design-styles/neo-chinese.md +0 -136
  72. package/assets/design-styles/precision-mono.md +0 -156
  73. package/assets/design-styles/quiet-luxury.md +0 -163
  74. package/assets/design-styles/riso-print.md +0 -116
  75. package/assets/design-styles/soft-friendly.md +0 -174
  76. package/assets/design-styles/swiss-grid.md +0 -156
  77. package/assets/design-styles/tech-dark.md +0 -182
  78. package/assets/favicon.svg +0 -6
  79. package/assets/logo.svg +0 -24
  80. package/bin/sbot.js +0 -4716
  81. package/docs/slack-app-manifest.yaml +0 -104
  82. package/sbot.config.example.json +0 -235
  83. package/src/agent/askonce.js +0 -76
  84. package/src/agent/claudemodels.js +0 -60
  85. package/src/agent/claudestatus.js +0 -74
  86. package/src/agent/claudeusage.js +0 -200
  87. package/src/agent/codexstatus.js +0 -181
  88. package/src/agent/imagen.js +0 -458
  89. package/src/agent/quotaerr.js +0 -44
  90. package/src/agent/transienterr.js +0 -32
  91. package/src/cdp.js +0 -254
  92. package/src/cli-secret.js +0 -36
  93. package/src/clilang.js +0 -31
  94. package/src/core/cron.js +0 -182
  95. package/src/core/cronfire.js +0 -120
  96. package/src/core/cronrun.js +0 -116
  97. package/src/core/engine.js +0 -2758
  98. package/src/core/interfaces.js +0 -114
  99. package/src/core/issues.js +0 -185
  100. package/src/core/ledger.js +0 -30
  101. package/src/core/opsalert.js +0 -214
  102. package/src/core/pending.js +0 -123
  103. package/src/core/proxy.js +0 -424
  104. package/src/core/quotaalert.js +0 -79
  105. package/src/core/quotaerr.js +0 -55
  106. package/src/core/scheduler.js +0 -59
  107. package/src/design.js +0 -5161
  108. package/src/gitrepo.js +0 -150
  109. package/src/health.js +0 -353
  110. package/src/installdeps.js +0 -72
  111. package/src/leftover.js +0 -174
  112. package/src/platform/boardnotice.js +0 -77
  113. package/src/platform/boardops.js +0 -36
  114. package/src/platform/boardwatch.js +0 -447
  115. package/src/platform/cli.js +0 -45
  116. package/src/platform/slack.js +0 -1289
  117. package/src/platform/takeovercard.js +0 -37
  118. package/src/platform/wecom.js +0 -938
  119. package/src/platform/wecomcli.js +0 -128
  120. package/src/platform/wecomgate.js +0 -108
  121. package/src/platform/wecomname.js +0 -43
  122. package/src/platform/wecompoll.js +0 -241
  123. package/src/preview.js +0 -581
  124. package/src/projectmemory.js +0 -175
  125. package/src/service.js +0 -317
  126. package/src/ui/auth.js +0 -31
  127. package/src/ui/browser.html +0 -392
  128. package/src/ui/browserroutes.js +0 -234
  129. package/src/ui/console.html +0 -5526
  130. package/src/ui/dashboard.js +0 -168
  131. package/src/ui/feishulogin.js +0 -136
  132. package/src/ui/server.js +0 -2314
  133. package/src/ui/sessions.js +0 -78
  134. package/src/ui/takeover.js +0 -108
  135. package/src/updater.js +0 -243
  136. package/src/upgrade.js +0 -35
  137. package/src/usage.js +0 -144
@@ -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 }
@@ -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 }