sintbot 1.4.14 → 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 +38 -0
- package/CHANGELOG.json +38 -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 +5 -3
- package/src/agent/codexcode.js +6 -4
- package/src/agent/modelwindows.js +10 -3
- package/src/agent/runner.js +5 -0
- package/src/c2configcontrol.js +29 -18
- package/src/c2console.js +22 -6
- package/src/c2multigroupservice.js +22 -2
- package/src/config.js +18 -8
- package/src/core/brain.js +117 -0
- package/src/core/c2inputrejection.js +19 -6
- package/src/core/c2runnerinput.js +10 -5
- 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 +182 -48
- package/src/ui/serverbase.js +173 -18
- package/src/modelcontext.js +0 -13
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// Model metadata for console display and executor override validation.
|
|
4
|
+
// Official limits and active executor windows are
|
|
4
5
|
// separate values; neither changes the server-side capacity of a model.
|
|
5
6
|
const fs = require('node:fs')
|
|
6
7
|
const os = require('node:os')
|
|
@@ -38,7 +39,7 @@ function codexCatalogWindows(home = process.env.CODEX_HOME || path.join(os.homed
|
|
|
38
39
|
const percent = model.effective_context_window_percent
|
|
39
40
|
if (typeof id !== 'string' || !Number.isSafeInteger(full) || full <= 0 ||
|
|
40
41
|
(percent != null && (!Number.isSafeInteger(percent) || percent < 1 || percent > 100))) continue
|
|
41
|
-
windows.set(id, { tokens: full, usableTokens: Math.floor(full * (percent || 100) / 100) })
|
|
42
|
+
windows.set(id, { maxTokens: Number.isSafeInteger(model.max_context_window) && model.max_context_window >= full ? model.max_context_window : full, tokens: full, usableTokens: Math.floor(full * (percent || 100) / 100) })
|
|
42
43
|
}
|
|
43
44
|
return windows
|
|
44
45
|
}
|
|
@@ -49,7 +50,13 @@ function windowForProvider(provider, reported, codexWindows) {
|
|
|
49
50
|
const gateway = provider.baseUrl || provider.env?.ANTHROPIC_BASE_URL
|
|
50
51
|
const official = provider.runner === 'codex' ? OPENAI_API_WINDOWS.get(model)
|
|
51
52
|
: gateway ? null : CLAUDE_WINDOWS.get(model)
|
|
52
|
-
// The catalog
|
|
53
|
+
// The catalog window is a default, not proof that an explicit override is active.
|
|
54
|
+
// With an override, only a real runtime sample can describe the active window.
|
|
55
|
+
if (provider.runner === 'codex' && provider.contextWindowTokens > 0) return {
|
|
56
|
+
tokens: official || null, source: official ? 'official' : 'unknown',
|
|
57
|
+
...(Number.isSafeInteger(reported) && reported > 0 ? { activeTokens: reported } : {}),
|
|
58
|
+
}
|
|
59
|
+
// Runtime reports
|
|
53
60
|
// may instead contain its effective (reserved) portion.
|
|
54
61
|
if (catalog && (!Number.isSafeInteger(reported) || reported <= 0 ||
|
|
55
62
|
reported === catalog.tokens || reported === catalog.usableTokens)) return {
|
package/src/agent/runner.js
CHANGED
|
@@ -9,6 +9,11 @@ const codexApp = require('./codexapp')
|
|
|
9
9
|
const { routeBackend } = require('./privatecore')
|
|
10
10
|
|
|
11
11
|
function runTurn(opts) {
|
|
12
|
+
// One-off console trials use the same model settings as group turns.
|
|
13
|
+
// Explicit turn values (including zero) remain authoritative.
|
|
14
|
+
const compact = Number(opts.autocompactTokens ?? opts.provider?.autocompactTokens) || 0
|
|
15
|
+
opts = { ...opts, autocompactTokens: compact > 0
|
|
16
|
+
? Math.min(1_000_000, Math.max(100_000, Math.floor(compact))) : 0 }
|
|
12
17
|
const route = routeBackend(opts && opts.provider && opts.provider.runner, opts && opts.codexApp, opts && opts.codexSteer, opts && opts.codexProjectDocFallback)
|
|
13
18
|
if (route !== 'claude') {
|
|
14
19
|
const codexOpts = { ...opts, bin: (opts.provider && opts.provider.bin) || 'codex' }
|
package/src/c2configcontrol.js
CHANGED
|
@@ -8,20 +8,24 @@ const { loadInstallationFromRaw, assertJournalScope, assertRunnerExecutable,
|
|
|
8
8
|
MANIFEST, VALIDATORS, SUPPORTED_RUNNERS } = require('./c2install')
|
|
9
9
|
const { C2DurableJournal } = require('./core/c2durablejournal')
|
|
10
10
|
const { assertNoLegacyGroupState } = require('./c2migration')
|
|
11
|
+
const { applyBrainChange } = require('./groupbrain')
|
|
11
12
|
|
|
12
13
|
const GLOBAL_TASK_KEYS = ['agent', 'timezone', 'brand']
|
|
13
|
-
|
|
14
|
+
// A group's model and depth (providerPrefs, legacy model) are not here: the
|
|
15
|
+
// executors resume the same session with another model of the same backend.
|
|
16
|
+
const RESET_KEYS = ['provider', 'workdir', 'codexApp']
|
|
14
17
|
const EFFORT_ENV = 'CLAUDE_CODE_EFFORT_LEVEL'
|
|
15
18
|
|
|
16
|
-
// What a group's session belongs to: backend, executable, endpoint
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
19
|
+
// What a group's session belongs to: backend, executable, endpoint and the
|
|
20
|
+
// environment the backend starts with. Model, depth, tool quality, key, image
|
|
21
|
+
// support, visibility and order apply from the next turn in the same session:
|
|
22
|
+
// both executors resume a session with another model of the same backend.
|
|
23
|
+
function sessionIdentity(provider) {
|
|
20
24
|
if (!provider || typeof provider !== 'object') return null
|
|
21
25
|
const env = { ...(provider.env && typeof provider.env === 'object' ? provider.env : {}) }
|
|
22
26
|
delete env[EFFORT_ENV]
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
delete env.ANTHROPIC_MODEL
|
|
28
|
+
return { runner: provider.runner, bin: provider.bin || '', baseUrl: provider.baseUrl || '', env }
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
// Synchronous validation/application around one atomic configuration write.
|
|
@@ -134,8 +138,8 @@ function createC2ConfigControl({ installation, getBindings, getHeld = () => new
|
|
|
134
138
|
// A model library or default change reaches every group that runs
|
|
135
139
|
// the edited provider, even though its own settings stay the same.
|
|
136
140
|
const providerMoved = !isDeepStrictEqual(
|
|
137
|
-
sessionIdentity(cfg.providers?.[effective(route.chatId, route.group, cfg)]
|
|
138
|
-
sessionIdentity(next.cfg.providers?.[effective(route.chatId, target.group, next.cfg)]
|
|
141
|
+
sessionIdentity(cfg.providers?.[effective(route.chatId, route.group, cfg)]),
|
|
142
|
+
sessionIdentity(next.cfg.providers?.[effective(route.chatId, target.group, next.cfg)]))
|
|
139
143
|
if (heldGroups.has(route.chatId)) {
|
|
140
144
|
// Edits elsewhere proceed; the held group keeps its exact settings.
|
|
141
145
|
if (!isDeepStrictEqual(route.group, target.group)) {
|
|
@@ -272,18 +276,25 @@ function createC2ConfigControl({ installation, getBindings, getHeld = () => new
|
|
|
272
276
|
return raw
|
|
273
277
|
}
|
|
274
278
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
// Backend · model · depth of one group (console, /model, model card). The
|
|
280
|
+
// writer validates, resets the session only when the backend changes, and
|
|
281
|
+
// goes live. Returns the change summary; throws on refusal.
|
|
282
|
+
function setGroupBrain(chatId, change = {}) {
|
|
283
|
+
let summary = null
|
|
284
|
+
writeConfig(raw => {
|
|
285
|
+
if (!raw.groups?.[chatId]) throw new Error('Unknown group')
|
|
286
|
+
const result = applyBrainChange(raw, chatId, change)
|
|
287
|
+
if (result.error) throw new Error({ 'unknown-provider': 'Unknown model',
|
|
288
|
+
'bad-model': 'Invalid model id', 'bad-effort': 'Invalid thinking depth' }[result.error] || 'Unknown group')
|
|
289
|
+
summary = result
|
|
283
290
|
})
|
|
291
|
+
return summary
|
|
292
|
+
}
|
|
293
|
+
function switchGroupProvider(chatId, name) {
|
|
294
|
+
return setGroupBrain(chatId, { provider: name })
|
|
284
295
|
}
|
|
285
296
|
|
|
286
|
-
return { validateConfig, beforeWrite, applyConfig, writeConfig, switchGroupProvider,
|
|
297
|
+
return { validateConfig, beforeWrite, applyConfig, writeConfig, switchGroupProvider, setGroupBrain,
|
|
287
298
|
// Groups whose session the last applied write reset (for the console).
|
|
288
299
|
appliedResets: () => [...appliedResets] }
|
|
289
300
|
}
|
package/src/c2console.js
CHANGED
|
@@ -13,10 +13,11 @@ const READ_PATHS = ['/api/state', '/api/events', '/api/inbox', '/api/activity',
|
|
|
13
13
|
'/api/plugins', '/api/design/styles', '/api/design/icons', '/api/fs/listdir',
|
|
14
14
|
'/api/feishu/admin-candidates', '/api/feishu/org-users', '/api/feishu/resolve-contact',
|
|
15
15
|
'/api/feishu/chats', '/api/feishu/chat-members', '/api/update', '/api/claude/models',
|
|
16
|
+
'/api/groups/model-options',
|
|
16
17
|
START_PATH, CALLBACK_PATH]
|
|
17
18
|
const WRITE_PATHS = ['/api/login', '/api/logout', '/api/logout/all',
|
|
18
19
|
'/api/ui/network', '/api/ui/public-url', '/api/ui/update-settings', '/api/feishu/admins',
|
|
19
|
-
'/api/groups', '/api/groups/provider', '/api/fs/mkdir',
|
|
20
|
+
'/api/groups', '/api/groups/provider', '/api/groups/brain', '/api/fs/mkdir',
|
|
20
21
|
'/api/groups/order', '/api/groups/policy', '/api/groups/redact',
|
|
21
22
|
'/api/groups/codex-steer', '/api/groups/steer', '/api/groups/pause', '/api/groups/stop',
|
|
22
23
|
// A fresh session only for an idle, settled group (same rule as /new).
|
|
@@ -38,9 +39,18 @@ const WRITE_PATHS = ['/api/login', '/api/logout', '/api/logout/all',
|
|
|
38
39
|
// manages this exact entry (the updater refuses every other run mode).
|
|
39
40
|
'/api/update/check', '/api/update/start', '/api/update/ignore']
|
|
40
41
|
const SUPPORTED_RUNNERS = ['claude', 'codex']
|
|
41
|
-
const {
|
|
42
|
+
const { groupBrain } = require('./core/brain')
|
|
43
|
+
const { codexCatalogWindows, windowForProvider } = require('./agent/modelwindows')
|
|
42
44
|
const { GROUP_COMMANDS } = require('./runtimefacts')
|
|
43
45
|
|
|
46
|
+
// What a group runs on one backend, for console rows: configured values only;
|
|
47
|
+
// what the executor finally selected is reported by the run itself.
|
|
48
|
+
function brainFields(group, name, provider, cfg) {
|
|
49
|
+
const b = groupBrain(group, name, provider, { defaultProvider: cfg.defaultProvider })
|
|
50
|
+
return { model: b.model, reasoningEffort: b.effort, groupModel: b.groupModel, groupEffort: b.groupEffort,
|
|
51
|
+
source: b.groupModel ? 'group' : b.defaultModel ? 'provider' : 'default', actualModel: null }
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld = () => new Map(), activity = null, store = null,
|
|
45
55
|
assertWriterLock, validateConfig, applyConfig, beforeWrite = () => true,
|
|
46
56
|
appliedResets = () => [], now = Date.now,
|
|
@@ -222,8 +232,7 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
222
232
|
? { usedTokens: stats.ctx, windowTokens: null, measuredAt: null, quality: 'historical' } : null)
|
|
223
233
|
return { chatId, name: group.name || chatId.slice(-8), project: path.basename(group.workdir || '') || '—',
|
|
224
234
|
workdir: group.workdir || '', runner: provider.runner || 'claude',
|
|
225
|
-
...
|
|
226
|
-
reasoningEffort: (provider.runner === 'codex' ? provider.reasoningEffort : provider.env?.CLAUDE_CODE_EFFORT_LEVEL) || '',
|
|
235
|
+
...brainFields(group, providerName, provider, cfg),
|
|
227
236
|
paused: !!group.paused, ...live.groups[chatId], context }
|
|
228
237
|
})
|
|
229
238
|
const rows = getActivity().filter(row => (!query.chatId || row.chatId === query.chatId) && (!query.status || row.status === query.status))
|
|
@@ -249,8 +258,13 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
249
258
|
for (const group of state.groups || []) {
|
|
250
259
|
const cfg = getConfig(), source = cfg.groups?.[group.chatId] || {}
|
|
251
260
|
const provider = cfg.providers?.[group.effectiveProvider || source.provider || cfg.defaultProvider]
|
|
252
|
-
const
|
|
253
|
-
|
|
261
|
+
const name = group.effectiveProvider || source.provider || cfg.defaultProvider
|
|
262
|
+
const fields = brainFields(source, name, provider, cfg)
|
|
263
|
+
Object.assign(group, { configuredModel: fields.model, modelSource: fields.source,
|
|
264
|
+
actualModel: null, brain: { provider: name, model: fields.model, effort: fields.reasoningEffort,
|
|
265
|
+
groupModel: fields.groupModel, groupEffort: fields.groupEffort },
|
|
266
|
+
providerPrefs: source.providerPrefs || null,
|
|
267
|
+
contextWindow: provider ? windowForProvider({ ...provider, model: fields.model }, null, codexCatalogWindows()) : null })
|
|
254
268
|
}
|
|
255
269
|
state.previews = (state.previews || []).filter(item => getConfig().groups?.[item.chatId])
|
|
256
270
|
state.leftovers = { supported: false, rows: [], error: null }
|
|
@@ -290,6 +304,8 @@ function createC2Console(options = {}) {
|
|
|
290
304
|
throw new TypeError('C2 console requires explicit installation paths')
|
|
291
305
|
}
|
|
292
306
|
const facade = createC2ConsoleFacade(options)
|
|
307
|
+
// The process-wide model list (also used by /model), when the service has one.
|
|
308
|
+
facade.modelCatalog = typeof options.modelCatalog === 'function' ? options.modelCatalog : null
|
|
293
309
|
const ui = createUiServer({ configPath, host, log, getConnHealth, updater: options.updater || null,
|
|
294
310
|
onHostChange: options.onHostChange,
|
|
295
311
|
getLiveState: facade.getLiveState, control: facade.control, runtimePolicy: facade,
|
|
@@ -39,7 +39,7 @@ function createC2MultiGroupService({ routes, assertWriterLock,
|
|
|
39
39
|
createTransport, createBinding, onFatal = () => {}, closeCapacity = () => {},
|
|
40
40
|
quarantine = null, stopTimeoutMs = 2_000, drainTimeoutMs = 12_000,
|
|
41
41
|
createRecoveryAuthority = null, holds = new Map(), sharedFault = () => null,
|
|
42
|
-
ingressWaitMs = 60_000, now = Date.now, log = () => {} } = {}) {
|
|
42
|
+
ingressWaitMs = 60_000, onModelCard = null, now = Date.now, log = () => {} } = {}) {
|
|
43
43
|
if (!Array.isArray(routes) || !routes.length ||
|
|
44
44
|
new Set(routes.map(route => route.chatId)).size !== routes.length ||
|
|
45
45
|
routes.some(route => !route || typeof route.chatId !== 'string' ||
|
|
@@ -392,6 +392,26 @@ function createC2MultiGroupService({ routes, assertWriterLock,
|
|
|
392
392
|
throw error
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
|
+
// Taps on the /model card only. They write the configuration through the
|
|
396
|
+
// same validated writer as the console and never touch a journal, receipt
|
|
397
|
+
// or session; every other card callback stays unsupported. The return
|
|
398
|
+
// value is the platform's synchronous answer (toast and replacement card).
|
|
399
|
+
function cardAction(event) {
|
|
400
|
+
let value = event?.action?.value
|
|
401
|
+
if (typeof value === 'string') { try { value = JSON.parse(value) } catch { value = null } }
|
|
402
|
+
if (typeof onModelCard !== 'function' || !value || value.a !== 'mc' || !allowed(event?.chatId)) {
|
|
403
|
+
return unsupported('card action')(event)
|
|
404
|
+
}
|
|
405
|
+
if (maintenance) return { toast: { type: 'info', content: '正在升级,稍后再点' } }
|
|
406
|
+
if (suspended.has(event.chatId)) return { toast: { type: 'warning', content: '本群暂停处理中,请管理员处理后再改' } }
|
|
407
|
+
const binding = bindings.get(event.chatId)
|
|
408
|
+
if (!binding) return { ignored: true }
|
|
409
|
+
return Promise.resolve().then(() => onModelCard({ ...event, action: { ...event.action, value } }, binding))
|
|
410
|
+
.catch(error => {
|
|
411
|
+
log(`model card ${event.chatId}: ${error?.message || String(error)}`)
|
|
412
|
+
return { toast: { type: 'error', content: '没改成 —— 请稍后再试' } }
|
|
413
|
+
})
|
|
414
|
+
}
|
|
395
415
|
function unsupported(source) {
|
|
396
416
|
return event => {
|
|
397
417
|
// An event without group identity cannot safely be attributed. It has
|
|
@@ -507,7 +527,7 @@ function createC2MultiGroupService({ routes, assertWriterLock,
|
|
|
507
527
|
transportStarted = true
|
|
508
528
|
await transport.platform.start(routeMessage, { onFatal: fatal,
|
|
509
529
|
onRecall: recall, onReaction: unsupported('reaction'),
|
|
510
|
-
onCardAction:
|
|
530
|
+
onCardAction: cardAction, onBoardUpdated: unsupported('board update') })
|
|
511
531
|
held()
|
|
512
532
|
if (stopPromise) throw new Error('C2 service stopped during connection')
|
|
513
533
|
return { groups: bindings.size, connected: true, ...(suspended.size ? { held: heldGroups() } : {}) }
|
package/src/config.js
CHANGED
|
@@ -25,14 +25,9 @@ const DEFAULTS = {
|
|
|
25
25
|
// runs fully locked down (dontAsk ⇒ read-only safe set only), which is fail-closed.
|
|
26
26
|
// maxTurnTimeMins defaults to 30: an uncapped runaway turn would
|
|
27
27
|
// hold a concurrency slot and the group lock forever. 0 = explicitly off.
|
|
28
|
-
// autocompactTokens:传给 Claude --autocompact 的是压缩窗口,传给 Codex
|
|
29
|
-
// model_auto_compact_token_limit 的是请求阈值;两者均可能早于设置值触发。
|
|
30
|
-
// 多天连跑的群会话历史只增不减(2026-08-09 实测 4 天涨到
|
|
31
|
-
// 96 万 token 且两家 CLI 默认都没触发任何压缩),不设上限 = 每轮驮全量历史 + 闲置后整段重写缓存。
|
|
32
|
-
// 0 = 不指定 SintBot 阈值,交还 CLI 默认压缩行为。
|
|
33
28
|
// ackDelayMs:收到真人消息后迟迟没动静,先回一声「已收到」的等待毫秒数(0 = 不发;
|
|
34
29
|
// engine 侧另有 == null ? 25s 兜底,直构 Engine 不经本层合并也不失默认)。
|
|
35
|
-
agent: { bin: 'claude', permissionMode: 'dontAsk', maxTurnTimeMins: 30, maxConcurrent: 2,
|
|
30
|
+
agent: { bin: 'claude', permissionMode: 'dontAsk', maxTurnTimeMins: 30, maxConcurrent: 2, ackDelayMs: 25_000 },
|
|
36
31
|
// 时间口径(IANA 时区名):群内一切时间表述/消息时间戳/spawn 出去的 TZ 都按它,
|
|
37
32
|
// 默认北京时间 —— 主机系统时区不参与(海外主机部署照样说北京时间)。
|
|
38
33
|
timezone: DEFAULT_TZ,
|
|
@@ -87,6 +82,21 @@ function loadConfig(explicit) {
|
|
|
87
82
|
return normalizeConfig(user, p)
|
|
88
83
|
}
|
|
89
84
|
|
|
85
|
+
// Collapse an explicit legacy global compression value into model settings at
|
|
86
|
+
// the configuration boundary. Read-only until an authorized config write;
|
|
87
|
+
// preserve per-model overrides, secret references, and every unrelated field.
|
|
88
|
+
function migrateModelSettings(raw) {
|
|
89
|
+
if (!raw?.agent || !Object.hasOwn(raw.agent, 'autocompactTokens')) return raw
|
|
90
|
+
const n = Number(raw.agent.autocompactTokens) || 0
|
|
91
|
+
const compact = n > 0 ? Math.min(1_000_000, Math.max(100_000, Math.floor(n))) : 0
|
|
92
|
+
const agent = { ...raw.agent }
|
|
93
|
+
delete agent.autocompactTokens
|
|
94
|
+
const providers = Object.fromEntries(Object.entries({ ...DEFAULTS.providers, ...(raw.providers || {}) })
|
|
95
|
+
.map(([name, provider]) => [name, provider && typeof provider === 'object'
|
|
96
|
+
? { ...provider, autocompactTokens: provider.autocompactTokens ?? compact } : provider]))
|
|
97
|
+
return { ...raw, agent, providers }
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
// The console validates proposed JSON before replacing the live file. Keep
|
|
91
101
|
// normalization identical to disk loading without mutating the proposed object
|
|
92
102
|
// or changing the live redaction registry during validation.
|
|
@@ -94,7 +104,7 @@ function normalizeConfig(raw, p = null, { registerSecrets = true } = {}) {
|
|
|
94
104
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
95
105
|
throw new TypeError('Configuration must be a JSON object')
|
|
96
106
|
}
|
|
97
|
-
const user = JSON.parse(JSON.stringify(raw))
|
|
107
|
+
const user = migrateModelSettings(JSON.parse(JSON.stringify(raw)))
|
|
98
108
|
const cfg = {
|
|
99
109
|
agent: { ...DEFAULTS.agent, ...(user.agent || {}) },
|
|
100
110
|
timezone: normalizeTz(user.timezone || DEFAULTS.timezone), // 写错回退默认,不漂到主机时区
|
|
@@ -293,4 +303,4 @@ function chatPlatform(cfg, chatId) {
|
|
|
293
303
|
return live.length === 1 ? live[0] : null
|
|
294
304
|
}
|
|
295
305
|
|
|
296
|
-
module.exports = { loadConfig, normalizeConfig, findConfigPath, defaultConfigPath, DEFAULTS, applyProxyEnv, deriveNoProxy, resolveSecret, platformsConfigured, inferChatPlatform, normalizeGroupPlatforms, chatPlatform, PLATFORMS }
|
|
306
|
+
module.exports = { loadConfig, normalizeConfig, migrateModelSettings, findConfigPath, defaultConfigPath, DEFAULTS, applyProxyEnv, deriveNoProxy, resolveSecret, platformsConfigured, inferChatPlatform, normalizeGroupPlatforms, chatPlatform, PLATFORMS }
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Which model and thinking depth a group uses on one model card (backend).
|
|
3
|
+
// This is the single resolver: runner input, card footer, /help · /model ·
|
|
4
|
+
// /status, the agent manifest, the console and the activity rows all call it.
|
|
5
|
+
//
|
|
6
|
+
// Storage: groups[chatId].providerPrefs = { "<card name>": { model?, effort? } },
|
|
7
|
+
// kept per backend. A group that moves between backends keeps each backend's
|
|
8
|
+
// own choice: a model id from one backend is never passed to another, and
|
|
9
|
+
// switching back does not silently fall back to something else.
|
|
10
|
+
// A missing entry means "not chosen": the executor picks its own default.
|
|
11
|
+
// Legacy groups[chatId].model belongs only to the backend the group was on
|
|
12
|
+
// when it was written (group.provider, else defaultProvider).
|
|
13
|
+
//
|
|
14
|
+
// Only shapes are validated, never an allow list: model ids and depth names
|
|
15
|
+
// evolve upstream, and values travel as argv/env entries without a shell.
|
|
16
|
+
// A bad shape is treated as unset instead of failing the turn.
|
|
17
|
+
|
|
18
|
+
const MODEL_RE = /^[\w.\-[\]:/]{1,80}$/
|
|
19
|
+
const EFFORT_RE = /^[a-z]{2,16}$/
|
|
20
|
+
|
|
21
|
+
const isModel = (s) => typeof s === 'string' && MODEL_RE.test(s)
|
|
22
|
+
const isEffort = (s) => typeof s === 'string' && EFFORT_RE.test(s)
|
|
23
|
+
|
|
24
|
+
// Legacy card-level values. Subscription cards no longer carry them after the
|
|
25
|
+
// one-time migration; a third-party endpoint keeps its model as connection
|
|
26
|
+
// configuration (it has no model list to choose from).
|
|
27
|
+
function defaultModel(provider) {
|
|
28
|
+
const p = provider || {}
|
|
29
|
+
return p.model || (p.runner !== 'codex' && p.env && p.env.ANTHROPIC_MODEL) || ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function defaultEffort(provider) {
|
|
33
|
+
const p = provider || {}
|
|
34
|
+
return (p.runner === 'codex' ? p.reasoningEffort : p.env && p.env.CLAUDE_CODE_EFFORT_LEVEL) || ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// One entry → valid fields only; nothing valid ⇒ null.
|
|
38
|
+
function cleanPref(x) {
|
|
39
|
+
if (!x || typeof x !== 'object' || Array.isArray(x)) return null
|
|
40
|
+
const o = {}
|
|
41
|
+
if (isModel(x.model)) o.model = x.model
|
|
42
|
+
if (isEffort(x.effort)) o.effort = x.effort
|
|
43
|
+
return Object.keys(o).length ? o : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Whole table: drop bad shapes, empty entries and (when known is given)
|
|
47
|
+
// entries for cards that do not exist. An empty table becomes undefined.
|
|
48
|
+
function normalizePrefs(prefs, known) {
|
|
49
|
+
if (!prefs || typeof prefs !== 'object' || Array.isArray(prefs)) return undefined
|
|
50
|
+
const out = {}
|
|
51
|
+
for (const [name, v] of Object.entries(prefs)) {
|
|
52
|
+
if (!/^[\w.-]+$/.test(name)) continue
|
|
53
|
+
if (known && !Object.prototype.hasOwnProperty.call(known, name)) continue
|
|
54
|
+
const c = cleanPref(v)
|
|
55
|
+
if (c) out[name] = c
|
|
56
|
+
}
|
|
57
|
+
return Object.keys(out).length ? out : undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param group groups[chatId] (may be empty)
|
|
62
|
+
* @param name the card actually running this turn
|
|
63
|
+
* @param provider cfg.providers[name]
|
|
64
|
+
* @param opts.defaultProvider top-level defaultProvider, only used to decide
|
|
65
|
+
* which backend a legacy group.model belongs to
|
|
66
|
+
* @returns {{ model, effort, groupModel, groupEffort, defaultModel, defaultEffort }}
|
|
67
|
+
* model / effort are effective values ('' = executor default);
|
|
68
|
+
* groupX are the group's own choices ('' = not chosen).
|
|
69
|
+
*/
|
|
70
|
+
function groupBrain(group, name, provider, { defaultProvider = '' } = {}) {
|
|
71
|
+
const g = group || {}
|
|
72
|
+
const pref = cleanPref(g.providerPrefs && typeof g.providerPrefs === 'object' ? g.providerPrefs[name] : null) || {}
|
|
73
|
+
const home = g.provider || defaultProvider || ''
|
|
74
|
+
const legacy = (!pref.model && isModel(g.model) && (!home || home === name)) ? g.model : ''
|
|
75
|
+
const groupModel = pref.model || legacy
|
|
76
|
+
const groupEffort = pref.effort || ''
|
|
77
|
+
const dm = defaultModel(provider)
|
|
78
|
+
const de = defaultEffort(provider)
|
|
79
|
+
return { model: groupModel || dm, effort: groupEffort || de, groupModel, groupEffort, defaultModel: dm, defaultEffort: de }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Move legacy group.model under the backend it belonged to (mutates the raw
|
|
83
|
+
// group). Every writer that changes group.provider calls this first, so the
|
|
84
|
+
// legacy id never attaches itself to the new backend. Returns whether it changed.
|
|
85
|
+
function migrateLegacyModel(g, defaultProvider) {
|
|
86
|
+
if (!g || typeof g !== 'object' || g.model === undefined) return false
|
|
87
|
+
const home = g.provider || defaultProvider || ''
|
|
88
|
+
if (home && isModel(g.model)) {
|
|
89
|
+
const prefs = (g.providerPrefs && typeof g.providerPrefs === 'object' && !Array.isArray(g.providerPrefs)) ? g.providerPrefs : {}
|
|
90
|
+
const cur = cleanPref(prefs[home]) || {}
|
|
91
|
+
if (!cur.model) prefs[home] = { ...cur, model: g.model }
|
|
92
|
+
g.providerPrefs = prefs
|
|
93
|
+
}
|
|
94
|
+
delete g.model
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Active groups whose current backend lacks a choice (doctor and console use
|
|
99
|
+
// the same rule). A subscription card without a model and any card without a
|
|
100
|
+
// depth are listed. Paused groups are skipped.
|
|
101
|
+
// Returns [{ chatId, name, provider, lack: ['model' | 'effort'] }].
|
|
102
|
+
function missingPicks(cfg) {
|
|
103
|
+
const out = []
|
|
104
|
+
const provs = (cfg && cfg.providers) || {}
|
|
105
|
+
for (const [id, g] of Object.entries((cfg && cfg.groups) || {})) {
|
|
106
|
+
if (!g || typeof g !== 'object' || id === 'oc_example_chat_id' || g.paused === true) continue
|
|
107
|
+
const pn = g.provider || cfg.defaultProvider
|
|
108
|
+
const pv = provs[pn]
|
|
109
|
+
if (!pv) continue
|
|
110
|
+
const b = groupBrain(g, pn, pv, { defaultProvider: cfg.defaultProvider })
|
|
111
|
+
const lack = [!b.model && !pv.baseUrl ? 'model' : '', !b.effort ? 'effort' : ''].filter(Boolean)
|
|
112
|
+
if (lack.length) out.push({ chatId: id, name: g.name || '', provider: pn, lack })
|
|
113
|
+
}
|
|
114
|
+
return out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { groupBrain, normalizePrefs, cleanPref, migrateLegacyModel, missingPicks, defaultModel, defaultEffort, isModel, isEffort, MODEL_RE, EFFORT_RE }
|
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
const CODE = 'C2_V2_INPUT_REJECTED'
|
|
8
8
|
const FEISHU_MESSAGE_ID = /^om_[A-Za-z0-9_-]{1,124}$/
|
|
9
9
|
|
|
10
|
-
function rejectC2Input(message, { reason, notice = null } = {}) {
|
|
10
|
+
function rejectC2Input(message, { reason, notice = null, card = null } = {}) {
|
|
11
11
|
if (typeof message !== 'string' || !message ||
|
|
12
12
|
typeof reason !== 'string' || !/^[a-z][a-z-]{0,40}$/.test(reason) ||
|
|
13
|
-
notice !== null && (typeof notice !== 'string' || !notice)
|
|
13
|
+
notice !== null && (typeof notice !== 'string' || !notice) ||
|
|
14
|
+
card !== null && (typeof card !== 'object' || Array.isArray(card))) {
|
|
14
15
|
throw new TypeError('C2 input rejection needs a message, reason and optional notice')
|
|
15
16
|
}
|
|
16
17
|
const error = new Error(message)
|
|
17
18
|
error.code = CODE
|
|
18
19
|
error.reason = reason
|
|
19
20
|
error.notice = notice
|
|
21
|
+
// An optional interactive card (the /model card) sent instead of the text;
|
|
22
|
+
// the text stays the fallback when the card is not confirmed.
|
|
23
|
+
if (card) error.card = card
|
|
20
24
|
return error
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -33,10 +37,19 @@ async function answerC2InputRejection({ error, msg, platform, log = () => {} } =
|
|
|
33
37
|
if (error.notice && typeof platform?.replyOnce === 'function' &&
|
|
34
38
|
typeof msg?.chatId === 'string' && target?.chatId === msg.chatId &&
|
|
35
39
|
FEISHU_MESSAGE_ID.test(target?.messageId || '')) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
if (error.card && typeof platform.replyCardOnce === 'function') {
|
|
41
|
+
try {
|
|
42
|
+
const response = await platform.replyCardOnce(target, error.card)
|
|
43
|
+
notified = response?.code === 0
|
|
44
|
+
} catch {}
|
|
45
|
+
if (!notified) log(`input rejection card not confirmed (${error.reason}); sending text`)
|
|
46
|
+
}
|
|
47
|
+
if (!notified) {
|
|
48
|
+
try {
|
|
49
|
+
const response = await platform.replyOnce(target, error.notice)
|
|
50
|
+
notified = response?.code === 0
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
40
53
|
if (!notified) log(`input rejection notice not confirmed (${error.reason})`)
|
|
41
54
|
}
|
|
42
55
|
return Object.freeze({ rejected: true, reason: error.reason, notified })
|
|
@@ -6,7 +6,7 @@ const { followupEnabled } = require('../agent/privatecore')
|
|
|
6
6
|
// process-local arguments. It neither starts a child nor persists text,
|
|
7
7
|
// credentials, callback handles, or environment values.
|
|
8
8
|
function prepareC2RunnerInput({ prompt, appendSystemPrompt, msg, group, provider,
|
|
9
|
-
config, sessionId = null, signal, extraEnv, onTurnAccepted, onSpawnFailed, onDurableEvent,
|
|
9
|
+
config, brain = null, sessionId = null, signal, extraEnv, onTurnAccepted, onSpawnFailed, onDurableEvent,
|
|
10
10
|
onEvent = () => {}, onSteerReady, onSteerLanded, log = () => {} } = {}) {
|
|
11
11
|
if (typeof prompt !== 'string' || !prompt ||
|
|
12
12
|
typeof appendSystemPrompt !== 'string' || !appendSystemPrompt ||
|
|
@@ -14,6 +14,8 @@ function prepareC2RunnerInput({ prompt, appendSystemPrompt, msg, group, provider
|
|
|
14
14
|
!group || typeof group.workdir !== 'string' || !group.workdir ||
|
|
15
15
|
!provider || !['claude', 'codex'].includes(provider.runner) ||
|
|
16
16
|
!config || !config.agent || typeof config.agent !== 'object' ||
|
|
17
|
+
(brain !== null && (typeof brain !== 'object' || typeof brain.model !== 'string' ||
|
|
18
|
+
typeof brain.effort !== 'string')) ||
|
|
17
19
|
(sessionId !== null && (typeof sessionId !== 'string' || !sessionId)) ||
|
|
18
20
|
!signal || typeof signal.aborted !== 'boolean' ||
|
|
19
21
|
!extraEnv || typeof extraEnv !== 'object' || Array.isArray(extraEnv) ||
|
|
@@ -34,9 +36,9 @@ function prepareC2RunnerInput({ prompt, appendSystemPrompt, msg, group, provider
|
|
|
34
36
|
typeof onSteerLanded !== 'function'))) {
|
|
35
37
|
throw new TypeError('C2 follow-up needs durable control callbacks')
|
|
36
38
|
}
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
const rawCompact = Number(provider.autocompactTokens
|
|
39
|
+
// Model settings are resolved once at the configuration boundary.
|
|
40
|
+
// Missing or zero leaves compaction to the executor.
|
|
41
|
+
const rawCompact = Number(provider.autocompactTokens) || 0
|
|
40
42
|
const autocompactTokens = rawCompact > 0
|
|
41
43
|
? Math.min(1_000_000, Math.max(100_000, Math.floor(rawCompact))) : 0
|
|
42
44
|
return {
|
|
@@ -47,7 +49,10 @@ function prepareC2RunnerInput({ prompt, appendSystemPrompt, msg, group, provider
|
|
|
47
49
|
steer: !isCodex && enabled,
|
|
48
50
|
codexProjectDocFallback: isCodex && group.codexProjectDocFallback === true,
|
|
49
51
|
onSteerReady, onSteerLanded,
|
|
50
|
-
model
|
|
52
|
+
// The group's model and depth on this backend (core/brain.js); empty
|
|
53
|
+
// leaves the choice to the executor.
|
|
54
|
+
model: brain ? brain.model || undefined : group.model,
|
|
55
|
+
effort: brain ? brain.effort : '',
|
|
51
56
|
workdir: group.workdir,
|
|
52
57
|
sessionId,
|
|
53
58
|
permissionMode: group.permissionMode || 'dontAsk',
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// 群命令 /model 的词法。纯函数 —— core 不认 runner 名:哪家有什么型号 / 深度,由 bin 注入的
|
|
3
|
+
// modelCatalog 给出形状 { ok, models: [{ id, efforts? }] | null, aliases: [], efforts: [] }(models 为 null = 这家
|
|
4
|
+
// 没有可选清单,型号只能在后台改)。
|
|
5
|
+
//
|
|
6
|
+
// 写法:`/model` 后面的每个词各自**精确**匹配一类(大小写不敏感,顺序随意,每类最多一个):
|
|
7
|
+
// 后端(模型卡名)· 深度(档位名)· 型号(目标后端清单里的 id 或别名)。
|
|
8
|
+
// 没有「默认」可回(模型卡上不再有默认型号 / 深度,⇒「默认 / default」不再是重置词,当认不出的词整条拒绝。
|
|
9
|
+
// ⚠ **不做模糊匹配**(口径):认不出一个词就整条拒绝、一项都不改 —— 换错模型 = 换了花的钱,
|
|
10
|
+
// 宁可让人重发一条也不猜;此前「只认第一个词、后面的悄悄扔掉」正是要修的坑(`/model claude sonnet` 丢了 sonnet)。
|
|
11
|
+
// 型号可以照显示名打:`/model opus-5.5` · `/model Opus 5.5` · `/model claude-opus-5-5` · `/model GPT-6.1 Terra` 是对应的同一个 ——
|
|
12
|
+
// 比对键见 core/modelname.js modelKey(大小写、claude- 前缀、点 / 连字符 / 空格不算),仍是拿清单**逐个精确比对**;
|
|
13
|
+
// 两个清单项归一后撞了键 ⇒ 当认不出(不替人挑)。「Opus 5.5」被空格拆成两个词:一个词后面紧跟纯版本号(5.5 / 4.8)
|
|
14
|
+
// 或变体((1M) / [1m])就并回一个词。
|
|
15
|
+
|
|
16
|
+
const { modelKey } = require('./modelname')
|
|
17
|
+
|
|
18
|
+
// 识别为「深度」这一类的词(能不能用再按目标后端 / 型号查)。Claude Code 的五档 + Codex 服务端认的两档低档。
|
|
19
|
+
const EFFORT_WORDS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
20
|
+
const VERSION_RE = /^\d+(?:[.-]\d+)*$/
|
|
21
|
+
const VARIANT_RE = /^[[(][a-z0-9]+[\])]$/i
|
|
22
|
+
|
|
23
|
+
// 「Opus 5.5 (1M)」「GPT-6.1 Terra」这类被空格拆开的显示名并回一个词:字母结尾 + 纯版本号 · 数字结尾 + 变体 ·
|
|
24
|
+
// 数字结尾 + 纯字母词(Codex 官方名称「GPT-6.1 Terra」照着打被拆成两个词、整条拒绝)。
|
|
25
|
+
// 只在型号类的词之间并:后端名 / 深度词不并、也不被并(`/model gpt-6-luna low` 的 low 仍是深度)。
|
|
26
|
+
function joinVersionWords(words, isOther) {
|
|
27
|
+
const out = []
|
|
28
|
+
for (const raw of words || []) {
|
|
29
|
+
const w = String(raw || '').trim()
|
|
30
|
+
if (!w) continue
|
|
31
|
+
const prev = out[out.length - 1]
|
|
32
|
+
const glue = prev !== undefined && !isOther(prev) && !isOther(w) && (
|
|
33
|
+
VERSION_RE.test(w) ? /[a-z]$/i.test(prev)
|
|
34
|
+
: VARIANT_RE.test(w) ? /\d$/.test(prev)
|
|
35
|
+
: /^[a-z]+$/i.test(w) && /\d$/.test(prev))
|
|
36
|
+
if (glue) out[out.length - 1] = prev + ' ' + w
|
|
37
|
+
else out.push(w)
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param words 命令后面的词
|
|
44
|
+
* @param providerNames 群里可选的模型卡名(没隐藏的)
|
|
45
|
+
* @returns {{ provider?, model?, models?, effort? } | { error: { kind: 'dup', what, a, b } }}
|
|
46
|
+
* model 原样保留大小写(查清单时再归一成清单里的写法);型号类写了不止一个 ⇒ models(调用方先逐个认,再判重)
|
|
47
|
+
*/
|
|
48
|
+
function parseModelWords(words, providerNames = []) {
|
|
49
|
+
const names = new Map(providerNames.map((n) => [String(n).toLowerCase(), n]))
|
|
50
|
+
const out = {}
|
|
51
|
+
const isOther = (w) => { const lw = w.toLowerCase(); return names.has(lw) || EFFORT_WORDS.includes(lw) }
|
|
52
|
+
for (const w of joinVersionWords(words, isOther)) {
|
|
53
|
+
const lw = w.toLowerCase()
|
|
54
|
+
if (names.has(lw)) {
|
|
55
|
+
const n = names.get(lw)
|
|
56
|
+
if (out.provider && out.provider !== n) return { error: { kind: 'dup', what: '后端', a: out.provider, b: n } }
|
|
57
|
+
out.provider = n
|
|
58
|
+
} else if (EFFORT_WORDS.includes(lw)) {
|
|
59
|
+
if (out.effort && out.effort !== lw) return { error: { kind: 'dup', what: '深度', a: out.effort, b: lw } }
|
|
60
|
+
out.effort = lw
|
|
61
|
+
} else {
|
|
62
|
+
// 型号这一类先都收下,由调用方按清单逐个认:两个词里有认不出的 ⇒ 报「认不出」(多半是打错 / 隐藏的后端名),
|
|
63
|
+
// 都认得出才报「一次只能选一个型号」—— 否则 `/model claude sonnet`(claude 被隐藏)会得到一句莫名其妙的「写了两个型号」
|
|
64
|
+
const ws = out.models || (out.models = [])
|
|
65
|
+
if (!ws.some((x) => x.toLowerCase() === lw)) ws.push(w)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (out.models && out.models.length === 1) { out.model = out.models[0]; delete out.models }
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 词 → 清单里的规范写法(id 或别名);认不出 ⇒ null。先原样比(大小写不敏感),再按比对键比 —— 键撞了不止一项 ⇒ null(不替人挑)。
|
|
73
|
+
// 实时清单带 aliasOf(opus → claude-opus-5-5)⇒ 别名认成它此刻指向的**具体型号**存下(钉住,CLI 升级不悄悄换代);
|
|
74
|
+
// 官方显示名(「GPT-6.1 Terra」)也参与比对键 —— 照卡上 / 控制台看到的名字打就认。
|
|
75
|
+
function resolveModel(word, cat) {
|
|
76
|
+
if (!word || !cat) return null
|
|
77
|
+
const lw = String(word).toLowerCase()
|
|
78
|
+
const models = (cat.models || []).map((m) => ({ id: String(m.id), name: m.name ? String(m.name) : '' }))
|
|
79
|
+
const ids = [...models.map((m) => m.id), ...(cat.aliases || []).map(String)]
|
|
80
|
+
const hit = ids.find((x) => x.toLowerCase() === lw)
|
|
81
|
+
if (hit) return hit
|
|
82
|
+
const aliasOf = cat.aliasOf && typeof cat.aliasOf === 'object' ? cat.aliasOf : {} // 别名走下面的比对键(大小写不敏感,与原样比等价)
|
|
83
|
+
const k = modelKey(word)
|
|
84
|
+
if (!k) return null
|
|
85
|
+
const keyed = new Set(ids.filter((x) => modelKey(x) === k))
|
|
86
|
+
for (const m of models) if (m.name && modelKey(m.name) === k) keyed.add(m.id)
|
|
87
|
+
for (const a of Object.keys(aliasOf)) if (modelKey(a) === k && ids.includes(aliasOf[a])) keyed.add(aliasOf[a])
|
|
88
|
+
return keyed.size === 1 ? [...keyed][0] : null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 这家(某型号下)能选的深度:清单里该型号自带档位就用它的,否则用这家的通用档位
|
|
92
|
+
function effortsFor(cat, modelId) {
|
|
93
|
+
const m = cat && modelId ? (cat.models || []).find((x) => x.id === modelId) : null
|
|
94
|
+
return (m && Array.isArray(m.efforts) && m.efforts.length) ? m.efforts : ((cat && cat.efforts) || [])
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 群里列给人看的型号:别名在前(短、好打),再列完整 id
|
|
98
|
+
function modelChoices(cat) {
|
|
99
|
+
if (!cat || !Array.isArray(cat.models)) return []
|
|
100
|
+
return [...(cat.aliases || []), ...cat.models.map((m) => m.id)]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 某型号的推荐深度:清单里它自带默认档(Codex defaultReasoningEffort)就用它的;否则这家的推荐档
|
|
104
|
+
// (cat.recommended.effort,Claude 系 xhigh);都不在这型号可选的档里 ⇒ high ⇒ 第一档。没有清单 ⇒ ''。
|
|
105
|
+
function recommendEffort(cat, modelId) {
|
|
106
|
+
if (!cat) return ''
|
|
107
|
+
const list = effortsFor(cat, modelId)
|
|
108
|
+
const m = modelId && Array.isArray(cat.models) ? cat.models.find((x) => x.id === modelId) : null
|
|
109
|
+
for (const e of [m && m.defEffort, cat.recommended && cat.recommended.effort, 'high']) if (e && list.includes(e)) return e
|
|
110
|
+
return list[0] || ''
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 补齐本群在目标后端上缺的选择(;群里 /model、模型卡、控制台群卡同一份)。没有「默认」之后,缺项不再静静跟随模型卡:
|
|
114
|
+
// 换后端且那一家**实际生效的**型号是空的(没选过、也没有迁移前遗留的卡上默认;第三方除外)⇒ 推荐型号;
|
|
115
|
+
// 换后端 / 换型号时实际生效的深度是空的、或新型号没有这一档 ⇒ 这型号的推荐档。
|
|
116
|
+
// 只调深度不补型号(补了就换脑、开新对话,与「只调深度接着聊」冲突)。返回 { model, effort, filled: ['型号','深度'] }。
|
|
117
|
+
function fillPicks({ switching = false, model, effort, currentModel = '', currentEffort = '', third = false, cat = null } = {}) {
|
|
118
|
+
const filled = []
|
|
119
|
+
const rec = cat && cat.ok !== false && cat.recommended ? cat.recommended : null
|
|
120
|
+
if (switching && model === undefined && !currentModel && !third && rec && rec.model) { model = rec.model; filled.push('型号') }
|
|
121
|
+
const next = model === undefined ? currentModel : model
|
|
122
|
+
if (effort === undefined && cat && cat.ok !== false && (switching || model !== undefined)) {
|
|
123
|
+
const effs = effortsFor(cat, next)
|
|
124
|
+
if (!currentEffort || (effs.length && !effs.includes(currentEffort))) {
|
|
125
|
+
const e = recommendEffort(cat, next)
|
|
126
|
+
if (e && e !== currentEffort) { effort = e; if (!currentEffort) filled.push('深度') }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { model, effort, filled }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { parseModelWords, resolveModel, effortsFor, modelChoices, recommendEffort, fillPicks, EFFORT_WORDS }
|