sintbot 1.3.0 → 1.4.1
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 +50 -0
- package/CHANGELOG.json +50 -0
- package/README.md +138 -20
- package/README.zh-CN.md +144 -20
- package/assets/design-styles/acid-pop.md +155 -0
- package/assets/design-styles/archive-index.md +161 -0
- package/assets/design-styles/art-deco.md +164 -0
- package/assets/design-styles/cinematic-noir.md +178 -0
- package/assets/design-styles/editorial.md +121 -0
- package/assets/design-styles/geometric-pop.md +122 -0
- package/assets/design-styles/hand-drawn.md +117 -0
- package/assets/design-styles/islamic-geometry.md +155 -0
- package/assets/design-styles/liquid-glass.md +183 -0
- package/assets/design-styles/neo-chinese.md +136 -0
- package/assets/design-styles/precision-mono.md +156 -0
- package/assets/design-styles/quiet-luxury.md +163 -0
- package/assets/design-styles/riso-print.md +116 -0
- package/assets/design-styles/soft-friendly.md +174 -0
- package/assets/design-styles/swiss-grid.md +156 -0
- package/assets/design-styles/tech-dark.md +182 -0
- package/bin/sintbot-v2.js +291 -263
- package/docs/AI-ASSISTED-INSTALL.md +8 -8
- package/docs/install.md +80 -48
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +8 -8
- package/docs/zh-CN/install.md +80 -48
- package/install.sh +34 -81
- package/package.json +60 -5
- package/src/agent/claudecode.js +32 -3
- package/src/agent/claudemodels.js +60 -0
- package/src/agent/claudestatus.js +74 -0
- package/src/agent/claudeusage.js +200 -0
- package/src/agent/codexapp.js +21 -4
- package/src/agent/codexcode.js +8 -3
- package/src/agent/codexstatus.js +181 -0
- package/src/agent/privatecore.js +1 -1
- package/src/agent/privatecore.wasm +0 -0
- package/src/c2configcontrol.js +214 -0
- package/src/c2console.js +149 -0
- package/src/c2incident.js +125 -0
- package/src/c2install.js +247 -37
- package/src/c2migration.js +100 -0
- package/src/c2multigroupcapacity.js +103 -0
- package/src/c2multigroupservice.js +418 -0
- package/src/cdp.js +254 -0
- package/src/config.js +13 -2
- package/src/core/c2durablejournal.js +26 -9
- package/src/core/c2durableruntime.js +104 -1
- package/src/core/c2inputrejection.js +45 -0
- package/src/core/c2localeffectexecutor.js +12 -1
- package/src/core/c2payloadhandles.js +11 -0
- package/src/core/c2persistrelation.js +20 -0
- package/src/core/c2persiststate.js +28 -2
- package/src/core/c2platformrecoverysender.js +1 -1
- package/src/core/c2sourcequarantine.js +56 -1
- package/src/core/c2terminalprune.js +2 -1
- package/src/core/engineslotsadapter.js +108 -0
- package/src/core/issues.js +185 -0
- package/src/core/ledger.js +30 -0
- package/src/core/proxy.js +424 -0
- package/src/core/store.js +12 -0
- package/src/core/v2engine.js +427 -31
- package/src/design.js +5161 -0
- package/src/gitrepo.js +150 -0
- package/src/leftover.js +174 -0
- package/src/platform/slack.js +1294 -0
- package/src/platform/wecom.js +961 -0
- package/src/platform/wecomcli.js +128 -0
- package/src/platform/wecomname.js +43 -0
- package/src/platform/wecompoll.js +241 -0
- package/src/preview.js +581 -0
- package/src/projectmemory.js +175 -0
- package/src/ui/auth.js +31 -0
- package/src/ui/browser.html +392 -0
- package/src/ui/browserroutes.js +234 -0
- package/src/ui/console.html +5719 -0
- package/src/ui/dashboard.js +168 -0
- package/src/ui/feishulogin.js +136 -0
- package/src/ui/serverbase.js +2371 -0
- package/src/ui/sessions.js +78 -0
- package/src/ui/takeover.js +108 -0
- package/src/usage.js +144 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sintbot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Drive your Mac projects from Feishu groups with Codex or Claude Code, using a built-in agent core.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sbot": "bin/sintbot-v2.js",
|
|
8
8
|
"sintbot": "bin/sintbot-v2.js"
|
|
@@ -21,6 +21,22 @@
|
|
|
21
21
|
"README.md",
|
|
22
22
|
"README.zh-CN.md",
|
|
23
23
|
"THIRD-PARTY-NOTICES.md",
|
|
24
|
+
"assets/design-styles/acid-pop.md",
|
|
25
|
+
"assets/design-styles/archive-index.md",
|
|
26
|
+
"assets/design-styles/art-deco.md",
|
|
27
|
+
"assets/design-styles/cinematic-noir.md",
|
|
28
|
+
"assets/design-styles/editorial.md",
|
|
29
|
+
"assets/design-styles/geometric-pop.md",
|
|
30
|
+
"assets/design-styles/hand-drawn.md",
|
|
31
|
+
"assets/design-styles/islamic-geometry.md",
|
|
32
|
+
"assets/design-styles/liquid-glass.md",
|
|
33
|
+
"assets/design-styles/neo-chinese.md",
|
|
34
|
+
"assets/design-styles/precision-mono.md",
|
|
35
|
+
"assets/design-styles/quiet-luxury.md",
|
|
36
|
+
"assets/design-styles/riso-print.md",
|
|
37
|
+
"assets/design-styles/soft-friendly.md",
|
|
38
|
+
"assets/design-styles/swiss-grid.md",
|
|
39
|
+
"assets/design-styles/tech-dark.md",
|
|
24
40
|
"bin/sintbot-v2.js",
|
|
25
41
|
"docs/AI-ASSISTED-INSTALL.md",
|
|
26
42
|
"docs/install.md",
|
|
@@ -28,15 +44,26 @@
|
|
|
28
44
|
"docs/zh-CN/install.md",
|
|
29
45
|
"install.sh",
|
|
30
46
|
"src/agent/claudecode.js",
|
|
47
|
+
"src/agent/claudemodels.js",
|
|
48
|
+
"src/agent/claudestatus.js",
|
|
49
|
+
"src/agent/claudeusage.js",
|
|
31
50
|
"src/agent/codexapp.js",
|
|
32
51
|
"src/agent/codexcode.js",
|
|
52
|
+
"src/agent/codexstatus.js",
|
|
33
53
|
"src/agent/errtag.js",
|
|
34
54
|
"src/agent/privatecore.js",
|
|
35
55
|
"src/agent/privatecore.wasm",
|
|
36
56
|
"src/agent/runner.js",
|
|
37
57
|
"src/agent/visionproxy.js",
|
|
38
58
|
"src/agent/winexec.js",
|
|
59
|
+
"src/c2configcontrol.js",
|
|
60
|
+
"src/c2console.js",
|
|
61
|
+
"src/c2incident.js",
|
|
39
62
|
"src/c2install.js",
|
|
63
|
+
"src/c2migration.js",
|
|
64
|
+
"src/c2multigroupcapacity.js",
|
|
65
|
+
"src/c2multigroupservice.js",
|
|
66
|
+
"src/cdp.js",
|
|
40
67
|
"src/config.js",
|
|
41
68
|
"src/core/c2cardeffectbridge.js",
|
|
42
69
|
"src/core/c2cardterminalpath.js",
|
|
@@ -45,6 +72,7 @@
|
|
|
45
72
|
"src/core/c2durableruntime.js",
|
|
46
73
|
"src/core/c2eventidentity.js",
|
|
47
74
|
"src/core/c2globalfault.js",
|
|
75
|
+
"src/core/c2inputrejection.js",
|
|
48
76
|
"src/core/c2localeffectexecutor.js",
|
|
49
77
|
"src/core/c2payloadhandles.js",
|
|
50
78
|
"src/core/c2persistrelation.js",
|
|
@@ -63,31 +91,57 @@
|
|
|
63
91
|
"src/core/datadir.js",
|
|
64
92
|
"src/core/enginecheckpoint.js",
|
|
65
93
|
"src/core/engineeffectcursor.js",
|
|
94
|
+
"src/core/engineslotsadapter.js",
|
|
66
95
|
"src/core/enginestepcoordinator.js",
|
|
67
96
|
"src/core/instancelock.js",
|
|
97
|
+
"src/core/issues.js",
|
|
98
|
+
"src/core/ledger.js",
|
|
68
99
|
"src/core/mdtable.js",
|
|
100
|
+
"src/core/proxy.js",
|
|
69
101
|
"src/core/redact.js",
|
|
70
102
|
"src/core/store.js",
|
|
71
103
|
"src/core/time.js",
|
|
72
104
|
"src/core/v2engine.js",
|
|
73
105
|
"src/core/verify.js",
|
|
106
|
+
"src/design.js",
|
|
107
|
+
"src/gitrepo.js",
|
|
108
|
+
"src/leftover.js",
|
|
74
109
|
"src/platform/board.js",
|
|
75
110
|
"src/platform/feishu.js",
|
|
76
111
|
"src/platform/multi.js",
|
|
112
|
+
"src/platform/slack.js",
|
|
113
|
+
"src/platform/wecom.js",
|
|
114
|
+
"src/platform/wecomcli.js",
|
|
115
|
+
"src/platform/wecomname.js",
|
|
116
|
+
"src/platform/wecompoll.js",
|
|
77
117
|
"src/platform/wswatchdog.js",
|
|
78
118
|
"src/plugins.js",
|
|
119
|
+
"src/preview.js",
|
|
120
|
+
"src/projectmemory.js",
|
|
79
121
|
"src/projectsroot.js",
|
|
80
|
-
"src/setupmodels.js"
|
|
122
|
+
"src/setupmodels.js",
|
|
123
|
+
"src/ui/auth.js",
|
|
124
|
+
"src/ui/browser.html",
|
|
125
|
+
"src/ui/browserroutes.js",
|
|
126
|
+
"src/ui/console.html",
|
|
127
|
+
"src/ui/dashboard.js",
|
|
128
|
+
"src/ui/feishulogin.js",
|
|
129
|
+
"src/ui/serverbase.js",
|
|
130
|
+
"src/ui/sessions.js",
|
|
131
|
+
"src/ui/takeover.js",
|
|
132
|
+
"src/usage.js"
|
|
81
133
|
],
|
|
82
134
|
"scripts": {
|
|
83
135
|
"start": "node bin/sintbot-v2.js start"
|
|
84
136
|
},
|
|
85
137
|
"keywords": [
|
|
86
138
|
"codex",
|
|
139
|
+
"claude-code",
|
|
87
140
|
"feishu",
|
|
88
141
|
"group-chat",
|
|
89
142
|
"chatops",
|
|
90
|
-
"self-hosted"
|
|
143
|
+
"self-hosted",
|
|
144
|
+
"agent"
|
|
91
145
|
],
|
|
92
146
|
"author": "WaterTian",
|
|
93
147
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -100,6 +154,7 @@
|
|
|
100
154
|
"url": "https://github.com/sintlab/sintbot/issues"
|
|
101
155
|
},
|
|
102
156
|
"dependencies": {
|
|
103
|
-
"@larksuiteoapi/node-sdk": "^1.76.0"
|
|
157
|
+
"@larksuiteoapi/node-sdk": "^1.76.0",
|
|
158
|
+
"ws": "^8.21.3"
|
|
104
159
|
}
|
|
105
160
|
}
|
package/src/agent/claudecode.js
CHANGED
|
@@ -29,6 +29,26 @@ const { IS_WIN, resolveSpawn } = require('./winexec')
|
|
|
29
29
|
const { tagError } = require('./errtag') // 出错原话 → quota / sessionMissing / transient 之一(优先级只在那一处定),与 codex runner 共用
|
|
30
30
|
const closedRunnerResults = new WeakSet()
|
|
31
31
|
const isClosedRunnerResult = result => !!result && typeof result === 'object' && closedRunnerResults.has(result)
|
|
32
|
+
// A rejection raised from the child's own 'close' event: the process has
|
|
33
|
+
// exited even though the turn has no result. It never proves the turn's outcome.
|
|
34
|
+
const closedRunnerErrors = new WeakSet()
|
|
35
|
+
const isClosedRunnerError = error => !!error && typeof error === 'object' && closedRunnerErrors.has(error)
|
|
36
|
+
const closedError = error => { closedRunnerErrors.add(error); return error }
|
|
37
|
+
const PRE_INIT_HOOK_EVENTS = new Set(['hook_started', 'hook_response'])
|
|
38
|
+
const DURABLE_EVENT_TYPES = new Set(['system', 'assistant', 'user', 'result', 'tool_result'])
|
|
39
|
+
// Tool output is completion evidence only, never answer text.
|
|
40
|
+
function durableToolResults(ev) {
|
|
41
|
+
const results = Array.isArray(ev.results) ? ev.results : []
|
|
42
|
+
return results.filter(item => item && item.type === 'tool_result').map(item => {
|
|
43
|
+
const content = typeof item.content === 'string' ? item.content
|
|
44
|
+
: Array.isArray(item.content) ? item.content
|
|
45
|
+
.filter(part => part && part.type === 'text' && typeof part.text === 'string')
|
|
46
|
+
.map(part => part.text).join('\n')
|
|
47
|
+
: ''
|
|
48
|
+
return { type: 'tool_result', ...(ev.sessionId ? { sessionId: ev.sessionId } : {}),
|
|
49
|
+
results: [{ type: 'tool_result', content, is_error: item.is_error === true }] }
|
|
50
|
+
})
|
|
51
|
+
}
|
|
32
52
|
|
|
33
53
|
// Build the child env from a resolved provider. `baseUrlOverride` (when given)
|
|
34
54
|
// replaces provider.baseUrl for ANTHROPIC_BASE_URL — used to point `claude` at
|
|
@@ -351,6 +371,9 @@ function runTurn({
|
|
|
351
371
|
let raw
|
|
352
372
|
try { raw = JSON.parse(line) } catch { continue } // skip non-JSON noise
|
|
353
373
|
if (onTurnAccepted && !turnAccepted) {
|
|
374
|
+
// Locally configured session hooks report their own lifecycle before
|
|
375
|
+
// init. They are not model output; skip them and keep waiting.
|
|
376
|
+
if (raw.type === 'system' && PRE_INIT_HOOK_EVENTS.has(raw.subtype)) continue
|
|
354
377
|
if (raw.type !== 'system' || raw.subtype !== 'init' || typeof raw.session_id !== 'string' || !raw.session_id) {
|
|
355
378
|
failTurnAcceptance(new Error('Claude turn emitted an event before start confirmation'))
|
|
356
379
|
return
|
|
@@ -400,7 +423,13 @@ function runTurn({
|
|
|
400
423
|
const last = lastMsgUsage || (ev.usage && Array.isArray(ev.usage.iterations) ? ev.usage.iterations[ev.usage.iterations.length - 1] : null)
|
|
401
424
|
if (last) ev.contextTokens = (last.input_tokens || 0) + (last.cache_read_input_tokens || 0) + (last.cache_creation_input_tokens || 0)
|
|
402
425
|
}
|
|
403
|
-
|
|
426
|
+
// Only transcript events reach the durable projection. Informational
|
|
427
|
+
// stream additions (for example rate_limit_event) carry no answer.
|
|
428
|
+
// Claude reports several tool results per event, with array content
|
|
429
|
+
// and an optional error flag; persist each in the shared shape.
|
|
430
|
+
if (onDurableEvent && ev.type === 'tool_result') {
|
|
431
|
+
for (const durable of durableToolResults(ev)) await persistEvent(durable)
|
|
432
|
+
} else if (onDurableEvent && DURABLE_EVENT_TYPES.has(ev.type)) await persistEvent(ev)
|
|
404
433
|
if (ev.type === 'result') { lastResult = ev; armCloseGrace() } // answer in → bound the wait for 'close'
|
|
405
434
|
if (onEvent) { try { onEvent(ev) } catch { /* renderer must not kill the turn */ } }
|
|
406
435
|
}
|
|
@@ -427,7 +456,7 @@ function runTurn({
|
|
|
427
456
|
|
|
428
457
|
const completeClose = (code) => finish(() => {
|
|
429
458
|
if (code !== 0 && !lastResult && !aborted) {
|
|
430
|
-
return reject(new Error(`claude exited ${code}: ${stderr.trim().slice(0, 800) || '(no stderr)'}`))
|
|
459
|
+
return reject(closedError(new Error(`claude exited ${code}: ${stderr.trim().slice(0, 800) || '(no stderr)'}`)))
|
|
431
460
|
}
|
|
432
461
|
resolveRun({ sessionId: capturedSessionId, result: lastResult, code, aborted })
|
|
433
462
|
})
|
|
@@ -500,4 +529,4 @@ function probeVersion(bin = 'claude') {
|
|
|
500
529
|
})
|
|
501
530
|
}
|
|
502
531
|
|
|
503
|
-
module.exports = { runTurn, isClosedRunnerResult, buildEnv, normalizeEvent, probeVersion, supportsFlag, BASE_SKILL_ALLOWS }
|
|
532
|
+
module.exports = { runTurn, isClosedRunnerResult, isClosedRunnerError, buildEnv, normalizeEvent, probeVersion, supportsFlag, BASE_SKILL_ALLOWS }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Claude 模型清单(控制台「模型」下拉用)—— **只在已登录时给**,未登录不列任何型号。
|
|
2
|
+
//
|
|
3
|
+
// ⚠ 为什么不是「登录后拉官方实时清单」:查过了,三条路都不通(2026-08-03 实测):
|
|
4
|
+
// 1. `GET /v1/models` + Claude Code 的登录态 token(Bearer + anthropic-beta: oauth-2025-04-20)
|
|
5
|
+
// —— curl 拿得到 200 与完整清单,但**同样的头从 Node 里发一律 403 `Request not allowed`**
|
|
6
|
+
// (node:https / fetch 都试过,补 accept、换 UA、换 HTTP/1.1 全无效)⇒ 这是按客户端指纹的
|
|
7
|
+
// 门禁,不是缺哪个头。它拦的是「登录态 token 被别的程序拿去用」,shell 出 curl 去绕正是
|
|
8
|
+
// 它要防的那件事,所以不做。
|
|
9
|
+
// 2. `claude` 没有 models 子命令(只有 auth / mcp / plugin / doctor …)。
|
|
10
|
+
// 3. `~/.claude.json` 的 `modelAccessCache` 实测是空数组、`orgModelDefaultCache` 是 null
|
|
11
|
+
// —— 不是清单来源。
|
|
12
|
+
// 所以清单 = 内置候选 + 本机账号的**额外可选项**(`additionalModelOptionsCache`,CLI 登录后
|
|
13
|
+
// 自己写的,合法本地读、best-effort),并在面板里说清「以 Claude Code 里 /model 为准」。
|
|
14
|
+
// 想用清单外的型号:面板留了「自定义…」直接填,不受这里限制。
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs')
|
|
17
|
+
const os = require('node:os')
|
|
18
|
+
const path = require('node:path')
|
|
19
|
+
|
|
20
|
+
// 当前主力档 + 上一代 Opus。刻意不铺满历史版本 —— 它是挑选快捷方式,不是权威清单;
|
|
21
|
+
// 4.8 留着是因为它仍在服务且额度池与 Opus 5 分开(operator 点名要能选,2026-08-05)。
|
|
22
|
+
// ⚠ id 是不带日期后缀的固定串(`claude-opus-4-8`,不是 `claude-opus-4-8-2026xxxx`)。
|
|
23
|
+
const BUILTIN = [
|
|
24
|
+
{ id: '', name: '默认(跟随 Claude Code)', note: '推荐 · 不指定型号' },
|
|
25
|
+
{ id: 'claude-opus-5', name: 'Claude Opus 5', note: '最强 · 复杂改码 / 长链路' },
|
|
26
|
+
{ id: 'claude-opus-4-8', name: 'Claude Opus 4.8', note: '上一代 Opus · 额度池独立' },
|
|
27
|
+
{ id: 'claude-sonnet-5', name: 'Claude Sonnet 5', note: '速度与智能平衡' },
|
|
28
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', note: '最快最省 · 简单任务' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
// CLI 登录后写在 ~/.claude.json 的账号额外可选模型(本机这台是 Fable 5)。
|
|
32
|
+
// 私有字段,形状可能变 ⇒ 全程防御式读取,取不到就当没有。纯函数,便于单测。
|
|
33
|
+
function parseExtraModels(rawJson) {
|
|
34
|
+
const arr = rawJson && Array.isArray(rawJson.additionalModelOptionsCache) ? rawJson.additionalModelOptionsCache : []
|
|
35
|
+
return arr
|
|
36
|
+
.filter((x) => x && typeof x.value === 'string' && x.value)
|
|
37
|
+
.map((x) => ({ id: x.value, name: x.label || x.value, note: String(x.description || '').slice(0, 60) }))
|
|
38
|
+
}
|
|
39
|
+
function readExtraModels(home) {
|
|
40
|
+
try { return parseExtraModels(JSON.parse(fs.readFileSync(path.join(home || os.homedir(), '.claude.json'), 'utf8'))) }
|
|
41
|
+
catch { return [] }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 合并:内置在前,账号额外项去重后追加(同 id 不重复列)。
|
|
45
|
+
function mergeModels(extra = []) {
|
|
46
|
+
const out = BUILTIN.slice()
|
|
47
|
+
const seen = new Set(out.map((m) => m.id))
|
|
48
|
+
for (const m of extra) if (m.id && !seen.has(m.id)) { seen.add(m.id); out.push({ ...m, extra: true }) }
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// { loggedIn, models, source }。未登录 ⇒ models 为空数组(面板据此整块不显示型号)。
|
|
53
|
+
// source: 'account' = 含本机账号的额外可选项 · 'builtin' = 只有内置候选。
|
|
54
|
+
function listModels({ loggedIn, home } = {}) {
|
|
55
|
+
if (!loggedIn) return { loggedIn: false, models: [], source: 'none' }
|
|
56
|
+
const extra = readExtraModels(home)
|
|
57
|
+
return { loggedIn: true, models: mergeModels(extra), source: extra.length ? 'account' : 'builtin' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { listModels, mergeModels, parseExtraModels, readExtraModels, BUILTIN }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Claude 订阅登录态探测 —— 控制台 Claude 模型卡 + doctor 用。
|
|
2
|
+
//
|
|
3
|
+
// `claude auth status --json` 是**官方的非交互子命令**(0.2s、不联网收费、不吃订阅额度),
|
|
4
|
+
// 返回 {loggedIn, authMethod, apiProvider, email, orgId, orgName, subscriptionType}。
|
|
5
|
+
// 它是 Claude 这侧对应 `codex login status` 的那件东西 —— 在它之前,sbot 只能按
|
|
6
|
+
// 「没填 baseUrl 就当成登录态」猜(卡片写「登录态·未验证」、doctor 写「Claude 登录态(未验证)」),
|
|
7
|
+
// 真没登录时一样显示正常,要到群里跑一轮才炸。
|
|
8
|
+
//
|
|
9
|
+
// 一律 best-effort:失败不抛,调用方显示「未验证」而不是把整块健康检查拖垮。
|
|
10
|
+
|
|
11
|
+
const { spawn } = require('node:child_process')
|
|
12
|
+
const { resolveSpawn } = require('./winexec')
|
|
13
|
+
|
|
14
|
+
// `claude auth status --json` 的 stdout → 归一化结果。纯函数,便于单测钉住字段映射。
|
|
15
|
+
// 老版本 CLI 没有这个子命令(stderr 报 unknown command / 非 0 退出)⇒ ok:false,
|
|
16
|
+
// 调用方据此显示「未验证」,**不能**当成「未登录」—— 二者要分开,同飞书应用描述那条教训。
|
|
17
|
+
function parseAuthStatus(stdout, code) {
|
|
18
|
+
const text = String(stdout || '').trim()
|
|
19
|
+
let j = null
|
|
20
|
+
try { j = JSON.parse(text) } catch { /* 非 JSON:老版本或报错文本 */ }
|
|
21
|
+
if (!j || typeof j !== 'object') {
|
|
22
|
+
return { ok: false, error: text.slice(0, 200) || `exit ${code}`, loggedIn: false }
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
loggedIn: !!j.loggedIn,
|
|
27
|
+
plan: j.subscriptionType || '', // max / pro / team …(未订阅时为空)
|
|
28
|
+
email: j.email || '',
|
|
29
|
+
org: j.orgName || '',
|
|
30
|
+
method: j.authMethod || '', // claude.ai = 订阅登录
|
|
31
|
+
apiProvider: j.apiProvider || '', // firstParty / bedrock / vertex
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loginStatus(bin = 'claude', timeoutMs = 8000) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const { file, spawnArgs } = resolveSpawn(bin, ['auth', 'status', '--json'])
|
|
38
|
+
let out = '', err = '', settled = false
|
|
39
|
+
const done = (v) => { if (settled) return; settled = true; clearTimeout(timer); resolve(v) }
|
|
40
|
+
let child
|
|
41
|
+
try { child = spawn(file, spawnArgs, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) }
|
|
42
|
+
catch (e) { return resolve({ ok: false, error: e.message, loggedIn: false }) }
|
|
43
|
+
const timer = setTimeout(() => { try { child.kill() } catch {} done({ ok: false, error: 'timeout', loggedIn: false }) }, timeoutMs)
|
|
44
|
+
child.stdout.on('data', (c) => { out += c.toString('utf8') })
|
|
45
|
+
child.stderr.on('data', (c) => { err += c.toString('utf8') })
|
|
46
|
+
child.on('error', (e) => done({ ok: false, loggedIn: false, error: e.code === 'ENOENT' ? "'claude' 不在 PATH 上" : e.message }))
|
|
47
|
+
child.on('close', (code) => done(parseAuthStatus(out || err, code)))
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// TTL 缓存:doctor 是一次性的,但控制台 /api/state 每次动作 + 60s 自动刷新都会重建健康行,
|
|
52
|
+
// 每次都 spawn 一个进程没必要。登录态分钟级变化足够,取 60s。
|
|
53
|
+
const CACHE = new Map() // bin -> { at, val }
|
|
54
|
+
const TTL = 60_000
|
|
55
|
+
async function cachedLoginStatus(bin = 'claude', ttl = TTL) {
|
|
56
|
+
const c = CACHE.get(bin)
|
|
57
|
+
if (c && Date.now() - c.at < ttl) return c.val
|
|
58
|
+
const val = await loginStatus(bin)
|
|
59
|
+
CACHE.set(bin, { at: Date.now(), val })
|
|
60
|
+
return val
|
|
61
|
+
}
|
|
62
|
+
function clearCache() { CACHE.clear() } // 「重新检测」按钮:绕过 TTL
|
|
63
|
+
|
|
64
|
+
// 登录态 → 一行人话(卡片 chip / doctor 详情共用,措辞单一事实源)。
|
|
65
|
+
function describeLogin(s) {
|
|
66
|
+
if (!s || !s.ok) return { level: 'warn', text: '未验证' + (s && s.error ? ' — ' + String(s.error).slice(0, 60) : '') }
|
|
67
|
+
if (!s.loggedIn) return { level: 'fail', text: '未登录 — 在这台机器上跑 claude auth login' }
|
|
68
|
+
const bits = ['订阅登录']
|
|
69
|
+
if (s.plan) bits.push(s.plan) // max / pro
|
|
70
|
+
if (s.email) bits.push(s.email)
|
|
71
|
+
return { level: 'ok', text: bits.join(' · ') }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { loginStatus, cachedLoginStatus, clearCache, parseAuthStatus, describeLogin }
|
|
@@ -0,0 +1,200 @@
|
|
|
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/codexapp.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
const { spawn } = require('node:child_process')
|
|
5
5
|
const closedRunnerResults = new WeakSet()
|
|
6
6
|
const isClosedRunnerResult = result => !!result && typeof result === 'object' && closedRunnerResults.has(result)
|
|
7
|
+
// A rejection raised from the child's own 'close' event: the process has
|
|
8
|
+
// exited even though the turn has no result. It never proves the turn's outcome.
|
|
9
|
+
const closedRunnerErrors = new WeakSet()
|
|
10
|
+
const isClosedRunnerError = error => !!error && typeof error === 'object' && closedRunnerErrors.has(error)
|
|
11
|
+
const closedError = error => { closedRunnerErrors.add(error); return error }
|
|
7
12
|
const { IS_WIN, resolveSpawn } = require('./winexec')
|
|
8
13
|
const { normalizeEvent, isEffortRejection } = require('./codexcode')
|
|
9
14
|
const { coreFailedResult, isCoreFailure } = require('./errtag')
|
|
@@ -142,6 +147,18 @@ function runTurn(opts = {}) {
|
|
|
142
147
|
})
|
|
143
148
|
const deactivate = () => { activeTurnId = null } // 完成/中止后立即拒绝晚到的补充。
|
|
144
149
|
const control = {
|
|
150
|
+
async steerDurable({ msgId, text }) {
|
|
151
|
+
const turnId = activeTurnId
|
|
152
|
+
if (!turnId || completed || aborted) return false
|
|
153
|
+
try {
|
|
154
|
+
await request('turn/steer', { threadId: capturedSessionId, expectedTurnId: turnId,
|
|
155
|
+
clientUserMessageId: msgId, input: [{ type: 'text', text }] })
|
|
156
|
+
return true
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error?.codexRpcRejected === true) return false
|
|
159
|
+
throw error
|
|
160
|
+
}
|
|
161
|
+
},
|
|
145
162
|
async steer({ msgId, text }) {
|
|
146
163
|
const turnId = activeTurnId
|
|
147
164
|
if (!turnId || completed || aborted) return false
|
|
@@ -321,7 +338,7 @@ function runTurn(opts = {}) {
|
|
|
321
338
|
}
|
|
322
339
|
if (raw.id != null) {
|
|
323
340
|
const p = requests.get(raw.id)
|
|
324
|
-
if (p) { requests.delete(raw.id); raw.error ? p.fail(new Error(raw.error.message || 'Codex RPC error')) : p.ok(raw.result) }
|
|
341
|
+
if (p) { requests.delete(raw.id); raw.error ? p.fail(Object.assign(new Error(raw.error.message || 'Codex RPC error'), { codexRpcRejected: [-32600, -32601, -32602].includes(raw.error.code) })) : p.ok(raw.result) }
|
|
325
342
|
return
|
|
326
343
|
}
|
|
327
344
|
if (onTurnAccepted && pendingTurnStart && !acceptedTurnId) {
|
|
@@ -348,7 +365,7 @@ function runTurn(opts = {}) {
|
|
|
348
365
|
}
|
|
349
366
|
} else if (action === 4) {
|
|
350
367
|
const item = params.item
|
|
351
|
-
if (appLandedAction(item && item.type, raw.method === 'item/completed', !!(item && item.clientId), !!(item && landed.has(item.clientId)))) {
|
|
368
|
+
if (params.turnId === activeTurnId && params.threadId === capturedSessionId && appLandedAction(item && item.type, raw.method === 'item/completed', !!(item && item.clientId), !!(item && landed.has(item.clientId)))) {
|
|
352
369
|
landed.add(item.clientId)
|
|
353
370
|
if (onSteerLanded) { try { onSteerLanded(item.clientId) } catch {} }
|
|
354
371
|
}
|
|
@@ -427,7 +444,7 @@ function runTurn(opts = {}) {
|
|
|
427
444
|
})
|
|
428
445
|
const completeClose = (code) => settle(() => {
|
|
429
446
|
if (onDurableEvent && !completed && !aborted) {
|
|
430
|
-
reject(new Error(`codex app-server exited ${code} before a durable result`))
|
|
447
|
+
reject(closedError(new Error(`codex app-server exited ${code} before a durable result`)))
|
|
431
448
|
return
|
|
432
449
|
}
|
|
433
450
|
if (!completed && !aborted) {
|
|
@@ -455,4 +472,4 @@ function runTurn(opts = {}) {
|
|
|
455
472
|
})
|
|
456
473
|
}
|
|
457
474
|
|
|
458
|
-
module.exports = { runTurn, isClosedRunnerResult, usageFromTotal, itemEvent, failedTurnEvent }
|
|
475
|
+
module.exports = { runTurn, isClosedRunnerResult, isClosedRunnerError, usageFromTotal, itemEvent, failedTurnEvent }
|
package/src/agent/codexcode.js
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
const { spawn } = require('node:child_process')
|
|
14
14
|
const closedRunnerResults = new WeakSet()
|
|
15
15
|
const isClosedRunnerResult = result => !!result && typeof result === 'object' && closedRunnerResults.has(result)
|
|
16
|
+
// A rejection raised from the child's own 'close' event: the process has
|
|
17
|
+
// exited even though the turn has no result. It never proves the turn's outcome.
|
|
18
|
+
const closedRunnerErrors = new WeakSet()
|
|
19
|
+
const isClosedRunnerError = error => !!error && typeof error === 'object' && closedRunnerErrors.has(error)
|
|
20
|
+
const closedError = error => { closedRunnerErrors.add(error); return error }
|
|
16
21
|
const { StringDecoder } = require('node:string_decoder')
|
|
17
22
|
const { IS_WIN, resolveSpawn } = require('./winexec')
|
|
18
23
|
const { tagError } = require('./errtag') // 出错原话 → quota / sessionMissing / transient 之一(优先级只在那一处定),与 claude runner 共用
|
|
@@ -398,11 +403,11 @@ function runOnce(opts, effort) {
|
|
|
398
403
|
|
|
399
404
|
const completeClose = (code) => {
|
|
400
405
|
if (onDurableEvent && code !== 0 && !lastResult && !aborted) {
|
|
401
|
-
done(() => reject(new Error(`codex exited ${code} before a durable result`)))
|
|
406
|
+
done(() => reject(closedError(new Error(`codex exited ${code} before a durable result`))))
|
|
402
407
|
return
|
|
403
408
|
}
|
|
404
409
|
if (durableStart && !acceptedStart && !aborted && !acceptanceFailed) {
|
|
405
|
-
done(() => reject(new Error('Codex exited before turn.started acceptance')))
|
|
410
|
+
done(() => reject(closedError(new Error('Codex exited before turn.started acceptance'))))
|
|
406
411
|
return
|
|
407
412
|
}
|
|
408
413
|
// Watch the answer→close (rollout-flush) lag. Healthy = ~2–3s (real-machine verified),
|
|
@@ -450,4 +455,4 @@ function probeVersion(bin = 'codex') {
|
|
|
450
455
|
})
|
|
451
456
|
}
|
|
452
457
|
|
|
453
|
-
module.exports = { runTurn, runOnce, isClosedRunnerResult, probeVersion, normalizeEvent, parseCodexError, isEffortRejection, sandboxMode, buildArgs, buildPrompt }
|
|
458
|
+
module.exports = { runTurn, runOnce, isClosedRunnerResult, isClosedRunnerError, probeVersion, normalizeEvent, parseCodexError, isEffortRejection, sandboxMode, buildArgs, buildPrompt }
|