sintbot 1.4.3 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.json +62 -0
- package/CHANGELOG.json +62 -0
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +10 -7
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +10 -7
- package/install.sh +1 -1
- package/package.json +1 -1
- package/src/agent/codexstatus.js +130 -5
- package/src/c2configcontrol.js +86 -10
- package/src/c2console.js +52 -4
- package/src/core/v2engine.js +148 -14
- package/src/ui/console.html +329 -24
- package/src/ui/serverbase.js +74 -19
package/src/ui/serverbase.js
CHANGED
|
@@ -303,6 +303,25 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
303
303
|
if (runtimePolicy) runtimePolicy.applyConfig(obj)
|
|
304
304
|
else if (onConfigChange) { try { onConfigChange() } catch (e) { log('reload hook error: ' + (e && e.message)) } }
|
|
305
305
|
}
|
|
306
|
+
// 默认模型的兜底。旧守护进程:默认模型隐藏了 / 在本进程看来没配好(如 key 是读不到的 env:X)/
|
|
307
|
+
// 文件里没写(跟随内置的 claude),都改成第一个可用的。持久处理流程里这会把跟随默认模型的群
|
|
308
|
+
// 悄悄换到另一个后端并重置会话(拖一下排序、加一个不相干的模型都会触发)⇒ 那里**只在默认模型
|
|
309
|
+
// 已不存在**(文件和内置都没有这个名字)时补一个,其余一律不动;隐藏默认模型由写入方直接拒绝。
|
|
310
|
+
// 返回 { from, to } = 这次自动改了默认模型(调用方据此如实报告),null = 没改。
|
|
311
|
+
const settleDefault = (raw) => {
|
|
312
|
+
if (!runtimePolicy) { fixDefault(raw); return null }
|
|
313
|
+
const current = raw.defaultProvider || DEFAULTS.defaultProvider
|
|
314
|
+
if ({ ...DEFAULTS.providers, ...(raw.providers || {}) }[current]) return null
|
|
315
|
+
const next = pickDefault(raw.providers || {})
|
|
316
|
+
if (!next || next === raw.defaultProvider) return null
|
|
317
|
+
const from = raw.defaultProvider || null
|
|
318
|
+
raw.defaultProvider = next
|
|
319
|
+
return { from, to: next }
|
|
320
|
+
}
|
|
321
|
+
// 持久处理流程下模型库写入的如实回执:这次写入重置了几个群的会话、默认模型有没有被自动补上。
|
|
322
|
+
const runtimeLibraryChange = (defaultChange) => runtimePolicy ? {
|
|
323
|
+
resetGroups: runtimePolicy.appliedResets ? runtimePolicy.appliedResets().length : 0,
|
|
324
|
+
...(defaultChange ? { defaultProvider: defaultChange.to } : {}) } : null
|
|
306
325
|
// 群内通知统一出口:暂停群不打扰(暂停语义=完全静默,配置变更通知也不例外 —— 2026-07-22
|
|
307
326
|
// 真机踩过:暂停群仍收到「模型已更新」;operator 在控制台自己改的,群成员恢复后清单会重注入,
|
|
308
327
|
// 静默无损)。唯 force:true 豁免 —— 暂停/恢复动作本身的那条告知必须送达。
|
|
@@ -344,6 +363,15 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
344
363
|
} catch { /* 回填是锦上添花,读不了就空着 */ }
|
|
345
364
|
const inboxDismiss = dashboard.createDismissStore({ file: inboxDismissPath || path.join(os.homedir(), '.sbot', 'inbox-dismissed.json'), log })
|
|
346
365
|
let lastHealth = null // 最近一次 buildState 的健康检查,给只算收件箱的 /api/inbox 用(那里不重跑探针)
|
|
366
|
+
// 升级排队时还有任务、排队、未结或暂停的群(只给名字,升级弹窗据此说明在等谁)
|
|
367
|
+
const upgradeWaitingGroups = () => {
|
|
368
|
+
let live = null, groupsRaw = {}
|
|
369
|
+
try { live = getLiveState ? getLiveState() : null } catch { return [] }
|
|
370
|
+
try { groupsRaw = readRaw().groups || {} } catch {}
|
|
371
|
+
return Object.entries((live && live.groups) || {})
|
|
372
|
+
.filter(([, g]) => g && (g.busy || g.finalizing || g.waitingForCapacity || g.queue > 0 || g.pending > 0 || g.held))
|
|
373
|
+
.map(([chatId, g]) => ({ name: (groupsRaw[chatId] && groupsRaw[chatId].name) || chatId.slice(-8), held: !!g.held }))
|
|
374
|
+
}
|
|
347
375
|
// 「需要你处理」:从 state 各区取数(反馈本 / 预览 / 残留进程 / 健康)+ 账本 + 接管票,不再各算一遍
|
|
348
376
|
const buildInboxFor = ({ issues, previews, leftovers, health }, raw) => {
|
|
349
377
|
try {
|
|
@@ -496,15 +524,19 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
496
524
|
const hasCodex = providers.some((p) => !p.hidden && p.runner === 'codex')
|
|
497
525
|
const cv = hasCodex ? await probeCodexCached() : null
|
|
498
526
|
// 「可用模型」= 能用的,不只是配了的(1.0.3;此前没登录的 Claude 订阅、试跑失败的 key 都算在内)。
|
|
499
|
-
// key 型看最近一次验证:失败的不算,还没验证的先算上;Claude
|
|
500
|
-
//
|
|
527
|
+
// key 型看最近一次验证:失败的不算,还没验证的先算上;Claude 订阅看本机登录态,探不出来(没装 / 超时)不扣。
|
|
528
|
+
// codex 用服务端判据(与 doctor、卡片「验证登录」同一个探针,按 bin 共用一份缓存):`codex login status`
|
|
529
|
+
// 只读本地文件,令牌被吊销后照样「已登录」。只有服务端明确拒绝(dead)才扣;探不到(没装 / 超时 / 连不上)不扣。
|
|
530
|
+
// ⚠ 概览**绝不等**这个探针(起进程出网,冷缓存 2s、连不上 12s):peekAuthProbe 只读 10 分钟内的缓存,
|
|
531
|
+
// 没有就按「未验证」算(不扣)并在后台刷新一次,下一次重建就是真结果。claude 那条是 60s 缓存的本地调用,照旧等。
|
|
501
532
|
const live = providers.filter((p) => !p.hidden && p.configured)
|
|
502
533
|
const subLogin = live.some((p) => !p.baseUrl && p.runner !== 'codex') ? await claudestatus.cachedLoginStatus(cfg.agent.bin).catch(() => null) : null
|
|
503
|
-
const cxLogin = live.some((p) => p.runner === 'codex') ?
|
|
534
|
+
const cxLogin = live.some((p) => p.runner === 'codex') ? codexstatus.peekAuthProbe('codex', 10 * 60_000) : null
|
|
504
535
|
const loggedOut = (st) => !!(st && st.ok && st.loggedIn === false)
|
|
505
536
|
const unusable = (p) => {
|
|
506
537
|
if (p.baseUrl) { const v = verify.lookup(verify.providerFp(cfg.providers[p.name] || {}), vopts); return !!(v && !v.ok) }
|
|
507
|
-
|
|
538
|
+
if (p.runner === 'codex') return !!(cxLogin && cxLogin.dead)
|
|
539
|
+
return loggedOut(subLogin)
|
|
508
540
|
}
|
|
509
541
|
const badProv = live.filter(unusable).map((p) => p.name)
|
|
510
542
|
const usableProv = live.length - badProv.length
|
|
@@ -516,7 +548,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
516
548
|
...(v ? [{ status: v.ok ? 'ok' : 'fail', label: 'Claude CLI', detail: v.ok ? v.version : v.error }] : []),
|
|
517
549
|
...(cv ? [{ status: cv.ok ? 'ok' : 'fail', label: 'Codex CLI', detail: cv.ok ? `${cv.version} · GPT 订阅` : cv.error }] : []),
|
|
518
550
|
{ status: !usableProv ? 'fail' : badProv.length ? 'warn' : 'ok', label: '可用模型',
|
|
519
|
-
detail: (usableProv ? `${usableProv} 个能用${
|
|
551
|
+
detail: (usableProv ? `${usableProv} 个能用${live.some((p) => p.runner === 'codex' && !badProv.includes(p.name)) ? '(含 Codex)' : ''}` : '无可用模型 — 去「模型 · 库」页添加 / 测试')
|
|
520
552
|
+ (badProv.length ? `;${badProv.join('、')} 没登录或验证失败` : '') },
|
|
521
553
|
{ status: usableGroups ? 'ok' : 'warn', label: '可用群 / 项目', detail: usableGroups ? `${usableGroups} 个已绑定` : '无绑定群/项目 — 去「群·项目」页创建' },
|
|
522
554
|
...connRows,
|
|
@@ -839,6 +871,12 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
839
871
|
if (p === '/api/changelog' && req.method === 'GET') return sendJSON(res, 200, { changelog: readChangelog() })
|
|
840
872
|
// 同一份版本历史的英文本(界面切英文时才拉;缺的版本前端按 version 回退中文)
|
|
841
873
|
if (p === '/api/changelog-en' && req.method === 'GET') return sendJSON(res, 200, { changelog: readChangelogEn() })
|
|
874
|
+
// 升级进度:控制台的升级弹窗在升级期间每一两秒读一次。只读,只给升级状态和还有任务的群名,
|
|
875
|
+
// 不重算整份 state(那里有健康探针);服务重启的那几秒连不上,由页面当作「正在重启」处理。
|
|
876
|
+
if (p === '/api/update' && req.method === 'GET') {
|
|
877
|
+
if (!updater) return sendJSON(res, 503, { error: '版本检查暂不可用' })
|
|
878
|
+
return sendJSON(res, 200, { ...updater.status(), waitingGroups: upgradeWaitingGroups() })
|
|
879
|
+
}
|
|
842
880
|
if (p === '/api/update/check' && req.method === 'POST') {
|
|
843
881
|
if (!updater) return sendJSON(res, 503, { error: '版本检查暂不可用' })
|
|
844
882
|
return sendJSON(res, 200, await updater.check())
|
|
@@ -900,6 +938,10 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
900
938
|
if (p === '/api/providers/add' && req.method === 'POST') {
|
|
901
939
|
const { name, baseUrl, model, apiKey, env, toolQuality, effort, reasoningEffort, hidden, vision, modelClear, runner } = await body(req)
|
|
902
940
|
if (!name || !/^[\w.-]+$/.test(name)) return sendJSON(res, 400, { error: '名称必填,且只用字母/数字/._-' })
|
|
941
|
+
// 持久处理流程只跑 Codex / Claude Code:别的后端名不能被下面的白名单悄悄改写成 Claude 存进去。
|
|
942
|
+
if (runtimePolicy && runner !== undefined && runner !== '' && !runtimePolicy.providerSupported({ runner })) {
|
|
943
|
+
return sendJSON(res, 400, { error: '当前只能添加或修改 Codex 或 Claude Code 模型' })
|
|
944
|
+
}
|
|
903
945
|
const raw = readRaw(); raw.providers = raw.providers || {}
|
|
904
946
|
// 编辑前快照:model / 推理强度变了要联动受影响群(通知 + model 变更重置会话)
|
|
905
947
|
const prev = raw.providers[name] ? { model: raw.providers[name].model || '', re: raw.providers[name].reasoningEffort || '' } : null
|
|
@@ -928,7 +970,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
928
970
|
// 不传 ⇒ 保持原值(编辑已有 codex provider 的常态,别把它降级成 claude 后端)。
|
|
929
971
|
if (runner !== undefined) { if (runner === 'codex') pr.runner = 'codex'; else delete pr.runner }
|
|
930
972
|
raw.providers[name] = pr
|
|
931
|
-
|
|
973
|
+
const defaultChange = settleDefault(raw); writeRaw(raw)
|
|
932
974
|
// 变更联动(仅编辑已有 provider 时):受影响群 = 用着它(显式或经 defaultProvider)
|
|
933
975
|
// 且无 per-group model 覆盖的群。model 变 = 换脑 → 按「切模型即重置」的既有拍板,
|
|
934
976
|
// 重置这些群会话 + 群内一行通知;仅推理强度变 → 通知不重置。daemon 未运行(无
|
|
@@ -952,7 +994,9 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
952
994
|
} catch (e) { log('providerChange notify: ' + (e && e.message)) }
|
|
953
995
|
}
|
|
954
996
|
}
|
|
955
|
-
|
|
997
|
+
// 持久处理流程:会话重置由配置写入方按「谁实际在用、型号/后端/端点是否变了」决定并已完成,
|
|
998
|
+
// 这里如实报它重置了几个群、默认模型有没有被补上(上面的 affected 是旧守护进程的通知口径)。
|
|
999
|
+
return sendJSON(res, 200, { ...(await buildState()), providerChange: { modelChanged: !!modelChanged, reChanged: !!reChanged, affected: affected.length, live, ...(runtimeLibraryChange(defaultChange) || {}) } })
|
|
956
1000
|
}
|
|
957
1001
|
if (p === '/api/providers' && req.method === 'DELETE') {
|
|
958
1002
|
const name = url.searchParams.get('name'); const raw = readRaw()
|
|
@@ -965,13 +1009,17 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
965
1009
|
return sendJSON(res, 400, { error: `「${name}」是内置默认模型,移不掉 —— 不想让它出现在列表里就用「隐藏」` })
|
|
966
1010
|
}
|
|
967
1011
|
if (raw.providers) delete raw.providers[name]
|
|
968
|
-
|
|
1012
|
+
const defaultChange = settleDefault(raw); writeRaw(raw)
|
|
1013
|
+
const change = runtimeLibraryChange(defaultChange)
|
|
1014
|
+
return sendJSON(res, 200, { ...(await buildState()), ...(change ? { providerChange: change } : {}) })
|
|
969
1015
|
}
|
|
970
1016
|
if (p === '/api/providers/hidden' && req.method === 'POST') {
|
|
971
1017
|
const { name, hidden } = await body(req); const raw = readRaw()
|
|
972
1018
|
if (!raw.providers || !raw.providers[name]) return sendJSON(res, 404, { error: 'no such provider' })
|
|
973
1019
|
if (hidden) raw.providers[name].hidden = true; else delete raw.providers[name].hidden
|
|
974
|
-
|
|
1020
|
+
const defaultChange = settleDefault(raw); writeRaw(raw)
|
|
1021
|
+
const change = runtimeLibraryChange(defaultChange)
|
|
1022
|
+
return sendJSON(res, 200, { ...(await buildState()), ...(change ? { providerChange: change } : {}) })
|
|
975
1023
|
}
|
|
976
1024
|
if (p === '/api/providers/order' && req.method === 'POST') {
|
|
977
1025
|
const { order } = await body(req); const raw = readRaw()
|
|
@@ -980,8 +1028,12 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
980
1028
|
for (const n of order) if (raw.providers[n]) next[n] = raw.providers[n]
|
|
981
1029
|
for (const [n, v] of Object.entries(raw.providers)) if (!(n in next)) next[n] = v // keep any not listed
|
|
982
1030
|
raw.providers = next
|
|
983
|
-
|
|
1031
|
+
// ordering doesn't set default; just keep the invisible fallback valid
|
|
1032
|
+
// (durable mode: only when the default no longer exists — see settleDefault)
|
|
1033
|
+
const defaultChange = settleDefault(raw)
|
|
984
1034
|
writeRaw(raw)
|
|
1035
|
+
const change = runtimeLibraryChange(defaultChange)
|
|
1036
|
+
if (change) return sendJSON(res, 200, { ...(await buildState()), providerChange: change })
|
|
985
1037
|
}
|
|
986
1038
|
return sendJSON(res, 200, await buildState())
|
|
987
1039
|
}
|
|
@@ -2235,19 +2287,22 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
2235
2287
|
}
|
|
2236
2288
|
return sendJSON(res, 200, { ...claudemodels.listModels({ loggedIn: !!(login && login.ok && login.loggedIn) }), login })
|
|
2237
2289
|
}
|
|
2238
|
-
// codex
|
|
2239
|
-
//
|
|
2290
|
+
// codex 卡片「验证登录」:带令牌问一次服务端(app-server account/rateLimits/read),登录态 + 订阅用量
|
|
2291
|
+
// 一次拿到。无需 API key;探针缓存 60s(app-server 有 spawn 开销)。
|
|
2240
2292
|
if (p === '/api/codex/verify' && req.method === 'POST') {
|
|
2241
2293
|
// force = 卡片额度行的 ↻:点按钮就是要此刻的数,绕过下面的 60s 去抖
|
|
2242
2294
|
// (去抖是给开局自动探测用的 —— app-server spawn 不便宜,但手点一次的开销该认)。
|
|
2243
2295
|
const { force } = await body(req).catch(() => ({}))
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2296
|
+
// 登录态与额度合成**一次**带令牌的服务端询问(额度接口)。此前先 `codex login status` 再取额度 ——
|
|
2297
|
+
// 前者只读本地文件,令牌被吊销后照样「已登录」,卡片一直绿着。现在服务端明确拒绝 ⇒ dead:true、loggedIn:false;
|
|
2298
|
+
// 探不到(超时)⇒ verified:false,保留上一份额度、文案「未验证」,不当成未登录。
|
|
2299
|
+
if (force) codexstatus.clearCache()
|
|
2300
|
+
const probe = await codexstatus.cachedAuthProbe('codex')
|
|
2301
|
+
const file = codexstatus.readAuthFile()
|
|
2302
|
+
const d = codexstatus.describeLogin(probe, file)
|
|
2303
|
+
const login = { ok: true, verified: !!probe.verified, loggedIn: !!(probe.ok || (!probe.verified && file.exists && !file.expired)), dead: !!probe.dead, mode: file.mode || 'chatgpt', level: d.level, text: d.text }
|
|
2304
|
+
let usage = probe.usage || null
|
|
2305
|
+
if (!usage && !probe.dead && codexStatusCache && codexStatusCache.usage) usage = codexStatusCache.usage // 探不到时保住旧额度(同前端那条「同一份状态两个消费者」的教训)
|
|
2251
2306
|
codexStatusCache = { at: Date.now(), login, usage } // cached into /api/state so the card survives a page reload
|
|
2252
2307
|
return sendJSON(res, 200, { login, usage })
|
|
2253
2308
|
}
|