sintbot 1.4.15 → 1.4.16
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 +19 -0
- package/CHANGELOG.json +19 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/sintbot-v2.js +87 -2
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +5 -5
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +5 -5
- package/install.sh +1 -1
- package/package.json +8 -3
- package/src/agent/claudecode.js +5 -2
- package/src/agent/claudemodels.js +79 -8
- package/src/agent/codexapp.js +4 -3
- package/src/agent/codexcode.js +1 -1
- package/src/c2configcontrol.js +29 -18
- package/src/c2console.js +22 -6
- package/src/c2multigroupservice.js +22 -2
- package/src/core/brain.js +117 -0
- package/src/core/c2inputrejection.js +19 -6
- package/src/core/c2runnerinput.js +7 -2
- package/src/core/modelcmd.js +132 -0
- package/src/core/modelname.js +61 -0
- package/src/core/v2engine.js +304 -47
- package/src/groupbrain.js +135 -0
- package/src/modelcatalog.js +122 -0
- package/src/platform/feishu.js +17 -0
- package/src/platform/modelcard.js +103 -0
- package/src/ui/console.html +119 -23
- package/src/ui/serverbase.js +155 -7
- package/src/modelcontext.js +0 -13
package/src/ui/serverbase.js
CHANGED
|
@@ -43,6 +43,9 @@ const { platformsConfigured, chatPlatform, inferChatPlatform, PLATFORMS } = requ
|
|
|
43
43
|
const { feishuClient, fetchBotOpenId, fetchAppIdentity, fetchChats, createChat, fetchChatMembers, addChatMembers, deleteChat, sendChatText, larkErrMsg, feishuPrecheck, fetchAdminCandidates, resolveContactToOpenId, fetchOrgUsers } = require('../platform/feishu')
|
|
44
44
|
const { slackPrecheck, fetchSlackChannels, sendSlackText } = require('../platform/slack') // Slack:控制台侧只用这三个助手(收发在 daemon 里)
|
|
45
45
|
const { getUsage, refreshUsage } = require('../usage')
|
|
46
|
+
const brainMod = require('../core/brain') // a group's model and depth on one backend: the single resolver
|
|
47
|
+
const { applyBrainChange, auditRow } = require('../groupbrain')
|
|
48
|
+
const { effortsFor, fillPicks } = require('../core/modelcmd')
|
|
46
49
|
|
|
47
50
|
// 反馈本默认落点(与 CLI / 群命令同一个文件 —— 三个入口一本账,不许各记各的)
|
|
48
51
|
const DEFAULT_ISSUES_FILE = () => path.join(os.homedir(), '.sbot', 'issues.jsonl')
|
|
@@ -50,6 +53,11 @@ const DEFAULT_ISSUES_FILE = () => path.join(os.homedir(), '.sbot', 'issues.jsonl
|
|
|
50
53
|
const DEFAULT_SESSIONS_FILE = () => path.join(os.homedir(), '.sbot', 'ui-sessions.json')
|
|
51
54
|
|
|
52
55
|
const CONSOLE_HTML = path.join(__dirname, 'console.html')
|
|
56
|
+
// Model display names: the same function as group replies and cards, injected
|
|
57
|
+
// into the page (with a plain fallback when the page is opened directly).
|
|
58
|
+
const MODELNAME_SLOT = "/*__SBOT_MODELNAME__*/(s=>String(s==null?'':s))"
|
|
59
|
+
const MODELNAME_SRC = '(' + require('../core/modelname').modelDisplayName.toString() + ')'
|
|
60
|
+
const { knownModelNames } = require('../core/modelname')
|
|
53
61
|
const LIVE_ACTIVITY_JS = fs.readFileSync(path.join(__dirname, 'liveactivity-client.js'), 'utf8')
|
|
54
62
|
const BROWSER_HTML = path.join(__dirname, 'browser.html') // 接管浏览器页(自包含单文件,群成员凭一次性链接也能进)
|
|
55
63
|
const takeover = require('./takeover') // 接管链接的票据与 cookie(纯逻辑)
|
|
@@ -311,6 +319,71 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
311
319
|
// 悄悄换到另一个后端并重置会话(拖一下排序、加一个不相干的模型都会触发)⇒ 那里**只在默认模型
|
|
312
320
|
// 已不存在**(文件和内置都没有这个名字)时补一个,其余一律不动;隐藏默认模型由写入方直接拒绝。
|
|
313
321
|
// 返回 { from, to } = 这次自动改了默认模型(调用方据此如实报告),null = 没改。
|
|
322
|
+
// ── Backend · model · depth of one group (group card; the old provider route
|
|
323
|
+
// forwards here). The same change logic as the chat /model command
|
|
324
|
+
// (src/groupbrain.js). Only a backend change resets the session.
|
|
325
|
+
const catalogFor = async (name, prov) => {
|
|
326
|
+
const c = runtimePolicy && runtimePolicy.modelCatalog
|
|
327
|
+
if (!c || !prov) return null
|
|
328
|
+
try { return await c(name, prov) } catch { return null }
|
|
329
|
+
}
|
|
330
|
+
// The target backend of a change as the file says now ({} when unknown).
|
|
331
|
+
const brainTarget = (raw, chatId, change) => {
|
|
332
|
+
const g = raw.groups && raw.groups[chatId]
|
|
333
|
+
if (!g) return {}
|
|
334
|
+
const defName = raw.defaultProvider || DEFAULTS.defaultProvider
|
|
335
|
+
const home = g.provider || defName
|
|
336
|
+
const target = change.provider || home
|
|
337
|
+
return { g, defName, home, target, tProv: { ...DEFAULTS.providers, ...(raw.providers || {}) }[target] }
|
|
338
|
+
}
|
|
339
|
+
async function changeGroupBrain(chatId, change, by = 'console') {
|
|
340
|
+
// The model list may need a CLI handshake: fetch it first, then read,
|
|
341
|
+
// decide and write with no await in between, so a /model change made
|
|
342
|
+
// meanwhile is never overwritten by a stale copy.
|
|
343
|
+
const first = brainTarget(readRaw(), chatId, change)
|
|
344
|
+
const cat = first.tProv ? await catalogFor(first.target, first.tProv) : null
|
|
345
|
+
const raw = readRaw()
|
|
346
|
+
const { g, defName, home, target, tProv } = brainTarget(raw, chatId, change)
|
|
347
|
+
if (!g) return { status: 404, body: { error: 'no such group' } }
|
|
348
|
+
if (!tProv) return { status: 400, body: { error: `未知模型 '${target}'` } }
|
|
349
|
+
if (target !== first.target) return { status: 409, body: { error: '这个群的后端刚被改过,请刷新后再改' } }
|
|
350
|
+
if (change.provider && change.provider !== home && (tProv.hidden || (runtimePolicy && !runtimePolicy.providerSupported(tProv)))) {
|
|
351
|
+
return { status: 400, body: { error: `「${target}」已隐藏或不受支持,不能选` } }
|
|
352
|
+
}
|
|
353
|
+
const switching = target !== home
|
|
354
|
+
const tBrain = brainMod.groupBrain(g, target, tProv, { defaultProvider: defName })
|
|
355
|
+
let model = change.model, effort = change.effort
|
|
356
|
+
if (model !== undefined && model !== '' && tProv.baseUrl) return { status: 400, body: { error: '这个模型的型号是连接设置,请在「模型」页修改' } }
|
|
357
|
+
if (model !== undefined && model !== '' && !brainMod.isModel(model)) return { status: 400, body: { error: '型号格式不对' } }
|
|
358
|
+
if (effort !== undefined && effort !== '' && !brainMod.isEffort(effort)) return { status: 400, body: { error: '思考深度格式不对' } }
|
|
359
|
+
if (model === tBrain.model) model = undefined
|
|
360
|
+
if (effort === tBrain.effort && model === undefined) effort = undefined
|
|
361
|
+
const f = fillPicks({ switching, model, effort, currentModel: tBrain.model, currentEffort: tBrain.effort, third: !!tProv.baseUrl, cat })
|
|
362
|
+
model = f.model; effort = f.effort
|
|
363
|
+
const nextModel = model === undefined ? tBrain.model : model
|
|
364
|
+
if (effort) {
|
|
365
|
+
const known = cat && Array.isArray(cat.models) ? cat.models.find((m) => m.id === nextModel) : null
|
|
366
|
+
if (known && Array.isArray(known.efforts) && !known.efforts.length) return { status: 400, body: { error: '这个型号不支持调思考深度' } }
|
|
367
|
+
const effs = cat ? effortsFor(cat, nextModel) : []
|
|
368
|
+
if (change.effort !== undefined && effs.length && !effs.includes(effort)) return { status: 400, body: { error: `这个型号没有「${effort}」这档深度(可选:${effs.join(' · ')})` } }
|
|
369
|
+
}
|
|
370
|
+
if (tProv.runner === 'codex' && tProv.contextWindowTokens > 0 && nextModel) {
|
|
371
|
+
const maximum = codexCatalogWindows().get(nextModel)?.maxTokens
|
|
372
|
+
if (maximum && tProv.contextWindowTokens > maximum) return { status: 400, body: { error: `「${target}」的自定义上下文窗口超过这个型号的上限 ${maximum} token,先在「模型」页调小` } }
|
|
373
|
+
}
|
|
374
|
+
const r = applyBrainChange(raw, chatId, { provider: switching ? target : undefined, model, effort })
|
|
375
|
+
if (r.error) return { status: 400, body: { error: r.error } }
|
|
376
|
+
if (!r.changed) return { status: 200, body: { ...(await buildState()), brainChange: { changed: false } } }
|
|
377
|
+
try { writeRaw(raw) } catch (e) {
|
|
378
|
+
const m = (e && e.message) || String(e)
|
|
379
|
+
return { status: 400, body: { error: /settled|unresolved|未结/.test(m) ? '该群有任务进行中或记录未结账,等它结束后再改' : m } }
|
|
380
|
+
}
|
|
381
|
+
audit.append(auditRow(chatId, r, { source: 'console', by }))
|
|
382
|
+
const resets = runtimePolicy && runtimePolicy.appliedResets ? runtimePolicy.appliedResets() : []
|
|
383
|
+
return { status: 200, body: { ...(await buildState()), brainChange: { changed: true, switching: r.switching,
|
|
384
|
+
modelChanged: r.modelChanged, effortChanged: r.effortChanged, filled: f.filled, provider: target,
|
|
385
|
+
model: r.after.model, effort: r.after.effort, reset: resets.includes(chatId) } } }
|
|
386
|
+
}
|
|
314
387
|
const settleDefault = (raw) => {
|
|
315
388
|
if (!runtimePolicy) { fixDefault(raw); return null }
|
|
316
389
|
const current = raw.defaultProvider || DEFAULTS.defaultProvider
|
|
@@ -625,6 +698,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
625
698
|
health,
|
|
626
699
|
providers,
|
|
627
700
|
defaultProvider: raw.defaultProvider || cfg.defaultProvider,
|
|
701
|
+
modelNames: knownModelNames(), // official names registered from the live model lists
|
|
628
702
|
groups,
|
|
629
703
|
// 项目根目录:绑群弹窗的路径提示用 projectsRoots(生效值);设置页那张卡用 projectsRootsInfo
|
|
630
704
|
//(每条根在不在 · 配置原文 · 被忽略的值与原因 · 哪些群在根外)。与围栏同一份判据。
|
|
@@ -699,6 +773,14 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
699
773
|
return state
|
|
700
774
|
}
|
|
701
775
|
|
|
776
|
+
// A card without its own model (subscription) is tried with the catalog's
|
|
777
|
+
// recommendation, the same one a newly bound group starts with.
|
|
778
|
+
async function recommendedModel(name, provider) {
|
|
779
|
+
if (!provider || provider.baseUrl) return undefined
|
|
780
|
+
const cat = await catalogFor(name, provider)
|
|
781
|
+
return (cat && cat.recommended && cat.recommended.model) || undefined
|
|
782
|
+
}
|
|
783
|
+
|
|
702
784
|
async function testProvider(name, prompt) {
|
|
703
785
|
const cfg = loadConfig(configPath); applyProxyEnv(cfg)
|
|
704
786
|
const provider = cfg.providers[name]
|
|
@@ -718,7 +800,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
718
800
|
bin: cfg.agent.bin,
|
|
719
801
|
prompt: prompt || verify.TRIAL_PROMPT,
|
|
720
802
|
provider,
|
|
721
|
-
model: provider.model,
|
|
803
|
+
model: provider.model || await recommendedModel(name, provider),
|
|
722
804
|
permissionMode: cfg.agent.permissionMode,
|
|
723
805
|
signal: ac.signal, // a one-line reply — needs no tools, so no allow list
|
|
724
806
|
})
|
|
@@ -728,7 +810,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
728
810
|
lease?.assertWritable()
|
|
729
811
|
const ok = !(result && result.isError)
|
|
730
812
|
verify.recordTurn(provider, ok, (result && result.text) || '', vopts) // doctor / 概览据此报「能用」或「key 不对」
|
|
731
|
-
return { ok, model: provider.model, reply: (result && result.text) || '(no text)', ms: Date.now() - t0 }
|
|
813
|
+
return { ok, model: provider.model || '', reply: (result && result.text) || '(no text)', ms: Date.now() - t0 }
|
|
732
814
|
} catch (e) {
|
|
733
815
|
closed ||= isClosedRunnerError(e)
|
|
734
816
|
if (!lease) verify.recordTurn(provider, false, (e && e.message) || String(e), vopts)
|
|
@@ -873,7 +955,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
873
955
|
// 隧道对端哪天被写成 127.0.0.1:4600,也不会退化成「公网全员免密」;伪造这两个头只会让自己失去免密,方向安全。
|
|
874
956
|
const localBypass = !loopbackBind && isLoopbackReq(req) && !req.headers['x-forwarded-for'] && !req.headers['x-forwarded-proto']
|
|
875
957
|
const injectTok = (authConfigured() && !localBypass) ? '' : token
|
|
876
|
-
return fs.readFile(CONSOLE_HTML, 'utf8', (e, html) => e ? (res.writeHead(500), res.end('console.html missing')) : (res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }), res.end(html.replace('__SBOT_TOKEN__', injectTok).replace('__SBOT_VERSION__', PKG_VERSION).replace('__SBOT_FEISHU_LOGIN__', feishuLoginInfo().ready ? '1' : '').replace('/*__LIVE_ACTIVITY__*/', () => LIVE_ACTIVITY_JS))))
|
|
958
|
+
return fs.readFile(CONSOLE_HTML, 'utf8', (e, html) => e ? (res.writeHead(500), res.end('console.html missing')) : (res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }), res.end(html.replace('__SBOT_TOKEN__', injectTok).replace('__SBOT_VERSION__', PKG_VERSION).replace('__SBOT_FEISHU_LOGIN__', feishuLoginInfo().ready ? '1' : '').replace('/*__LIVE_ACTIVITY__*/', () => LIVE_ACTIVITY_JS).replace(MODELNAME_SLOT, () => MODELNAME_SRC))))
|
|
877
959
|
}
|
|
878
960
|
// 接管浏览器页。群成员的一次性链接是 /browser#k=<票>:票在 URL 片段里,服务端根本收不到 ⇒ 不进任何日志、链接预览的
|
|
879
961
|
// 裸 GET 也抓不走;页面脚本读片段、人点「开始接管」后 POST /api/browser/claim 换 cookie。这里只分发页面:
|
|
@@ -975,7 +1057,7 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
975
1057
|
}
|
|
976
1058
|
// upsert a provider (add or edit — the ⚙ config panel + 添加模型 both use this)
|
|
977
1059
|
if (p === '/api/providers/add' && req.method === 'POST') {
|
|
978
|
-
|
|
1060
|
+
let { name, baseUrl, model, apiKey, env, toolQuality, effort, reasoningEffort, autocompactTokens, contextWindowTokens, hidden, vision, modelClear, runner } = await body(req)
|
|
979
1061
|
if (!name || !/^[\w.-]+$/.test(name)) return sendJSON(res, 400, { error: '名称必填,且只用字母/数字/._-' })
|
|
980
1062
|
// 持久处理流程只跑 Codex / Claude Code:别的后端名不能被下面的白名单悄悄改写成 Claude 存进去。
|
|
981
1063
|
if (runtimePolicy && runner !== undefined && runner !== '' && !runtimePolicy.providerSupported({ runner })) {
|
|
@@ -986,6 +1068,13 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
986
1068
|
const prev = raw.providers[name] ? { model: raw.providers[name].model || '', re: raw.providers[name].reasoningEffort || '' } : null
|
|
987
1069
|
const pr = { ...(raw.providers[name] || {}) }
|
|
988
1070
|
if (baseUrl !== undefined) pr.baseUrl = baseUrl || undefined
|
|
1071
|
+
// Durable hosts: model and depth belong to each group (group card / /model).
|
|
1072
|
+
// A subscription card takes neither; a third-party endpoint keeps its
|
|
1073
|
+
// model as connection configuration and takes no depth.
|
|
1074
|
+
if (runtimePolicy) {
|
|
1075
|
+
effort = undefined; reasoningEffort = undefined
|
|
1076
|
+
if (!pr.baseUrl) { model = undefined; modelClear = undefined }
|
|
1077
|
+
}
|
|
989
1078
|
if (model) pr.model = model
|
|
990
1079
|
// 「默认(跟随 Claude Code)」= 不指定型号 ⇒ spawn 时不设 ANTHROPIC_MODEL,用 CLI 自己的默认。
|
|
991
1080
|
// 空 model 与「没传 model」要分开,所以用显式的 modelClear,而不是 `model !== undefined`
|
|
@@ -1025,8 +1114,16 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
1025
1114
|
}
|
|
1026
1115
|
if (pr.runner !== 'codex') delete pr.contextWindowTokens
|
|
1027
1116
|
if (pr.contextWindowTokens > 0) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1117
|
+
// Check every model a group runs on this card (and the card's own, if any).
|
|
1118
|
+
const windows = codexCatalogWindows()
|
|
1119
|
+
const defName = raw.defaultProvider || DEFAULTS.defaultProvider
|
|
1120
|
+
const models = new Set([pr.model, ...Object.entries(raw.groups || {})
|
|
1121
|
+
.filter(([id, g]) => id !== 'oc_example_chat_id' && g && (g.provider || defName) === name)
|
|
1122
|
+
.map(([, g]) => brainMod.groupBrain(g, name, pr, { defaultProvider: defName }).model)].filter(Boolean))
|
|
1123
|
+
for (const m of models) {
|
|
1124
|
+
const maximum = windows.get(m)?.maxTokens
|
|
1125
|
+
if (maximum && pr.contextWindowTokens > maximum) return sendJSON(res, 400, { error: `上下文窗口超过型号 ${m} 的目录上限 ${maximum} token` })
|
|
1126
|
+
}
|
|
1030
1127
|
}
|
|
1031
1128
|
raw.providers[name] = pr
|
|
1032
1129
|
const defaultChange = settleDefault(raw); writeRaw(raw)
|
|
@@ -1733,6 +1830,18 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
1733
1830
|
// Fail here, not at the first task: a binding to a missing
|
|
1734
1831
|
// directory would only surface when a turn tries to run in it.
|
|
1735
1832
|
if (!fs.existsSync(workdir)) return sendJSON(res, 400, { error: `workdir 不存在: ${workdir}` })
|
|
1833
|
+
// The model list is fetched before the read below, so no await sits
|
|
1834
|
+
// between reading and writing the file (a /model change made meanwhile
|
|
1835
|
+
// must not be overwritten).
|
|
1836
|
+
let preName = '', preCat = null
|
|
1837
|
+
if (runtimePolicy) {
|
|
1838
|
+
try {
|
|
1839
|
+
const r0 = readRaw()
|
|
1840
|
+
preName = provider || (r0.groups && r0.groups[chatId] && r0.groups[chatId].provider) || r0.defaultProvider || DEFAULTS.defaultProvider
|
|
1841
|
+
const p0 = { ...DEFAULTS.providers, ...(r0.providers || {}) }[preName]
|
|
1842
|
+
if (p0) preCat = await catalogFor(preName, p0)
|
|
1843
|
+
} catch { /* the read below reports a broken file */ }
|
|
1844
|
+
}
|
|
1736
1845
|
const raw = readRaw(); raw.groups = raw.groups || {}; delete raw.groups.oc_example_chat_id
|
|
1737
1846
|
// 已绑定的群重存:旧对象在下面整个展开再覆盖(见 next),这里只是取出来比目录有没有改
|
|
1738
1847
|
const prev = raw.groups[chatId] || {}
|
|
@@ -1762,6 +1871,20 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
1762
1871
|
// ⚠ 不按「那个平台配了凭据没」拒绝:纯配置模式(先绑群、后填凭据)一直是允许的;没配的平台由 doctor 的群行报「收不到消息」
|
|
1763
1872
|
next.platform = PLATFORMS.includes(platform) ? platform : chatPlatformOf(raw, chatId)
|
|
1764
1873
|
raw.groups[chatId] = next
|
|
1874
|
+
if (runtimePolicy) {
|
|
1875
|
+
// A new binding or another backend starts with the recommended model
|
|
1876
|
+
// and depth where the group has not chosen (same rule as /model).
|
|
1877
|
+
const defName = raw.defaultProvider || DEFAULTS.defaultProvider
|
|
1878
|
+
const tName = next.provider || defName
|
|
1879
|
+
const tProv = { ...DEFAULTS.providers, ...(raw.providers || {}) }[tName]
|
|
1880
|
+
if (tProv && (isNewBinding || tName !== (prev.provider || defName))) {
|
|
1881
|
+
brainMod.migrateLegacyModel(next, defName)
|
|
1882
|
+
const b = brainMod.groupBrain(next, tName, tProv, { defaultProvider: defName })
|
|
1883
|
+
const f = fillPicks({ switching: true, currentModel: b.model, currentEffort: b.effort,
|
|
1884
|
+
third: !!tProv.baseUrl, cat: tName === preName ? preCat : null })
|
|
1885
|
+
if (f.model !== undefined || f.effort !== undefined) applyBrainChange(raw, chatId, { model: f.model, effort: f.effort })
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1765
1888
|
writeRaw(raw)
|
|
1766
1889
|
// First-time binding: drop a confirmation into the group (best-effort) so a
|
|
1767
1890
|
// fresh chat is immediately alive and members know how to use it.
|
|
@@ -2268,6 +2391,19 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
2268
2391
|
}
|
|
2269
2392
|
// real-time per-group model switch: persist to config (+ hot-reload) AND set
|
|
2270
2393
|
// the live runtime override so it takes effect on the very next turn.
|
|
2394
|
+
if (p === '/api/groups/brain' && req.method === 'POST') {
|
|
2395
|
+
const { chatId, provider, model, effort } = await body(req)
|
|
2396
|
+
const pick = (v) => (v === undefined || v === null ? undefined : String(v))
|
|
2397
|
+
const out = await changeGroupBrain(chatId, { provider: pick(provider) || undefined, model: pick(model), effort: pick(effort) })
|
|
2398
|
+
return sendJSON(res, out.status, out.body)
|
|
2399
|
+
}
|
|
2400
|
+
if (p === '/api/groups/provider' && req.method === 'POST' && runtimePolicy) {
|
|
2401
|
+
// Older console pages: same writer as the group card.
|
|
2402
|
+
const { chatId, provider } = await body(req)
|
|
2403
|
+
const out = await changeGroupBrain(chatId, { provider: provider ? String(provider) : undefined })
|
|
2404
|
+
if (out.status === 200 && out.body.brainChange) out.body.switched = { chatId, provider, live: true }
|
|
2405
|
+
return sendJSON(res, out.status, out.body)
|
|
2406
|
+
}
|
|
2271
2407
|
if (p === '/api/groups/provider' && req.method === 'POST') {
|
|
2272
2408
|
const { chatId, provider } = await body(req); const raw = readRaw()
|
|
2273
2409
|
if (!raw.groups || !raw.groups[chatId]) return sendJSON(res, 404, { error: 'no such group' })
|
|
@@ -2344,6 +2480,17 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
2344
2480
|
}
|
|
2345
2481
|
// Claude 模型下拉的候选。**未登录不给任何型号**(产品决定:没登录就别列)。
|
|
2346
2482
|
// 已登录时 = 内置候选 + 本机账号的额外可选项;为什么不是官方实时清单,见 claudemodels.js 头部。
|
|
2483
|
+
// What one backend offers on the group card: models (official names,
|
|
2484
|
+
// supported depths), recommendation. models:null = a third-party
|
|
2485
|
+
// endpoint whose model is set on the model card.
|
|
2486
|
+
if (p === '/api/groups/model-options' && req.method === 'GET') {
|
|
2487
|
+
const name = url.searchParams.get('provider') || ''
|
|
2488
|
+
const raw = readRaw()
|
|
2489
|
+
const prov = { ...DEFAULTS.providers, ...(raw.providers || {}) }[name]
|
|
2490
|
+
if (!prov) return sendJSON(res, 404, { error: `未知模型 '${name}'` })
|
|
2491
|
+
const cat = await catalogFor(name, prov)
|
|
2492
|
+
return sendJSON(res, 200, cat || { ok: false, models: prov.baseUrl ? null : [], efforts: [], recommended: { model: '', effort: '' } })
|
|
2493
|
+
}
|
|
2347
2494
|
if (p === '/api/claude/models' && req.method === 'GET') {
|
|
2348
2495
|
const cfg = loadConfig(configPath)
|
|
2349
2496
|
const login = await claudestatus.cachedLoginStatus(cfg.agent.bin)
|
|
@@ -2396,7 +2543,8 @@ function createUiServer({ configPath, getLiveState = null, getConnHealth = null,
|
|
|
2396
2543
|
bin: cfg.agent.bin,
|
|
2397
2544
|
prompt,
|
|
2398
2545
|
provider,
|
|
2399
|
-
model: provider.model,
|
|
2546
|
+
model: (pgGroup && brainMod.groupBrain(pgGroup, provName, provider, { defaultProvider: cfg.defaultProvider }).model) || provider.model || await recommendedModel(provName, provider),
|
|
2547
|
+
effort: (pgGroup && brainMod.groupBrain(pgGroup, provName, provider, { defaultProvider: cfg.defaultProvider }).effort) || '',
|
|
2400
2548
|
workdir: workdir || undefined,
|
|
2401
2549
|
sessionId: sessionId || undefined,
|
|
2402
2550
|
permissionMode: (pgGroup && pgGroup.permissionMode) || 'dontAsk', // 试跑继承匹配群的模式(无全局默认,unset ⇒ dontAsk)
|
package/src/modelcontext.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// Configuration projection shared by chat, cards and the console. A configured
|
|
4
|
-
// model is not proof of what an executor ultimately selected or fell back to.
|
|
5
|
-
function modelContext(group = {}, provider = {}) {
|
|
6
|
-
group ||= {}; provider ||= {}
|
|
7
|
-
const environment = provider.runner === 'codex' ? '' : provider.env?.ANTHROPIC_MODEL
|
|
8
|
-
const model = group.model || provider.model || environment || ''
|
|
9
|
-
return { model, source: group.model ? 'group' : provider.model ? 'provider' : environment ? 'environment' : 'default',
|
|
10
|
-
actualModel: null }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
module.exports = { modelContext }
|