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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// 「这张模型卡能选什么」(;两家都改成实时清单)。bin 注入给 engine.modelCatalog,控制台同一份;core 只认返回形状:
|
|
3
|
+
// { ok, models: [{ id, name?, efforts?, defEffort? }] | null, aliases: [], aliasOf?: { 别名: 具体型号 }, efforts: [],
|
|
4
|
+
// recommended: { model, effort }, source: 'live' | 'saved' | 'builtin' }
|
|
5
|
+
// models === null ⇒ 这家没有可选清单(第三方端点):群里只能调深度,型号到后台改 —— 打错一个字下一轮就报错,
|
|
6
|
+
// 比让人在群里瞎试更稳。ok === false ⇒ 有清单但这会儿取不到(codex 探测失败、也没存过):型号这一项整条拒绝,不猜。
|
|
7
|
+
//
|
|
8
|
+
// 清单与名称一律取两家 CLI 的**实时数据**:
|
|
9
|
+
// Codex = app-server model/list(官方显示名 GPT-6.1-Sol、★ 默认型号、各型号支持与默认的深度);
|
|
10
|
+
// Claude 订阅 = Claude Code 初始化握手报的 /model 选单(claudemodels.probeModels:官方显示名、各型号支持的深度、
|
|
11
|
+
// default 指向的推荐型号;别名 opus / sonnet … 打进来存成它此刻指向的具体型号)。
|
|
12
|
+
// 都是 10 分钟缓存、过期先给旧的后台再刷(模型卡按钮回调要 3 秒内回包,现场探测要好几秒);每次拿到就存盘(cacheFile),
|
|
13
|
+
// 重启后 / 探测失败时先用存盘的(source 'saved')—— 不再退回写死在代码里、早已过时的候选。Claude 从没拿到过才用内置候选。
|
|
14
|
+
// 拿到的显示名登记进 core/modelname.js(群里回执、卡片、控制台都按官方名称显示)。
|
|
15
|
+
// recommended(没有「模型卡默认」):新绑的群、换到没选过的后端、模型卡「测试」预填 / 兜底用的推荐项 ——
|
|
16
|
+
// Codex = ★ 型号 + 它自带的默认深度;Claude 订阅 = 握手里 default 指向的型号 + xhigh(Claude Code 自己的默认档);
|
|
17
|
+
// 第三方 = 型号由模型卡定(空)+ xhigh。只用于预填与迁移,运行时不拿它悄悄兜底。
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs')
|
|
20
|
+
const path = require('node:path')
|
|
21
|
+
const { recommendEffort } = require('./core/modelcmd') // 推荐深度的口径只此一份(engine 的 /model 与模型卡同用)
|
|
22
|
+
const { setModelNames } = require('./core/modelname')
|
|
23
|
+
|
|
24
|
+
const CLAUDE_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'] // Claude Code 的五档(code.claude.com model-config)
|
|
25
|
+
// Codex 服务端真正接受的档(与控制台 RE_LEGAL 同一份口径):model/list 会过报(把 ultra 列进去),先过一道
|
|
26
|
+
const CODEX_LEGAL = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
27
|
+
const CLAUDE_ALIASES = ['opus', 'sonnet', 'haiku'] // 只在拿不到实时清单、退回内置候选时用(原样交给 CLI)
|
|
28
|
+
const CLAUDE_EFFORT = 'xhigh'
|
|
29
|
+
const TTL = 600_000
|
|
30
|
+
|
|
31
|
+
function createModelCatalog({ codexstatus, claudemodels, claudeBin = 'claude', cacheFile = null, now = () => Date.now(), log = () => {} } = {}) {
|
|
32
|
+
const live = { codex: null, claude: null } // { at, data }
|
|
33
|
+
const inflight = { codex: null, claude: null }
|
|
34
|
+
const fetchers = {
|
|
35
|
+
codex: async () => { const l = await codexstatus.listModels('codex'); return Array.isArray(l) && l.length ? l : null },
|
|
36
|
+
claude: async () => (claudemodels && typeof claudemodels.probeModels === 'function' ? claudemodels.probeModels(claudeBin) : null),
|
|
37
|
+
}
|
|
38
|
+
const register = (kind, data) => {
|
|
39
|
+
try {
|
|
40
|
+
if (kind === 'codex') setModelNames(data.map((m) => ({ id: m.model, name: m.name })))
|
|
41
|
+
else setModelNames(data.models)
|
|
42
|
+
} catch { /* 名称登记失败只影响显示 */ }
|
|
43
|
+
}
|
|
44
|
+
// 存盘的上次结果:at 记 0 ⇒ 第一次用到就后台再刷一次
|
|
45
|
+
if (cacheFile) {
|
|
46
|
+
try {
|
|
47
|
+
const saved = JSON.parse(fs.readFileSync(cacheFile, 'utf8'))
|
|
48
|
+
for (const k of ['codex', 'claude']) if (saved && saved[k] && saved[k].data) { live[k] = { at: 0, data: saved[k].data, saved: true }; register(k, saved[k].data) }
|
|
49
|
+
} catch { /* 没有 / 坏了就当没有 */ }
|
|
50
|
+
}
|
|
51
|
+
const persist = () => {
|
|
52
|
+
if (!cacheFile) return
|
|
53
|
+
try {
|
|
54
|
+
const out = {}
|
|
55
|
+
for (const k of ['codex', 'claude']) if (live[k]) out[k] = { at: live[k].at || now(), data: live[k].data }
|
|
56
|
+
fs.mkdirSync(path.dirname(cacheFile), { recursive: true })
|
|
57
|
+
const tmp = cacheFile + '.tmp'
|
|
58
|
+
fs.writeFileSync(tmp, JSON.stringify(out))
|
|
59
|
+
fs.renameSync(tmp, cacheFile)
|
|
60
|
+
} catch (e) { log('可选型号存盘失败: ' + (e && e.message)) }
|
|
61
|
+
}
|
|
62
|
+
const refresh = (kind) => {
|
|
63
|
+
if (!inflight[kind]) {
|
|
64
|
+
inflight[kind] = Promise.resolve().then(() => fetchers[kind]()).catch(() => null)
|
|
65
|
+
.then((data) => {
|
|
66
|
+
if (data) { live[kind] = { at: now(), data }; register(kind, data); persist() }
|
|
67
|
+
else log(`${kind === 'codex' ? 'codex' : 'Claude'} 可选型号探测失败 —— 沿用上次结果(如有)`)
|
|
68
|
+
})
|
|
69
|
+
.finally(() => { inflight[kind] = null })
|
|
70
|
+
}
|
|
71
|
+
return inflight[kind]
|
|
72
|
+
}
|
|
73
|
+
// 有结果就先用、过期了后台再刷;从没取到过(也没存盘)才等这一次
|
|
74
|
+
async function get(kind) {
|
|
75
|
+
const c = live[kind]
|
|
76
|
+
if (c) { if (now() - c.at >= TTL) refresh(kind); return c.data }
|
|
77
|
+
await refresh(kind)
|
|
78
|
+
return live[kind] ? live[kind].data : null
|
|
79
|
+
}
|
|
80
|
+
const sourceOf = (kind) => (live[kind] && live[kind].saved && !live[kind].at ? 'saved' : 'live')
|
|
81
|
+
|
|
82
|
+
async function catalog(name, provider) {
|
|
83
|
+
const p = provider || {}
|
|
84
|
+
if (p.runner === 'codex') {
|
|
85
|
+
const list = await get('codex')
|
|
86
|
+
if (!list) return { ok: false, models: [], aliases: [], efforts: CODEX_LEGAL.slice(1), recommended: { model: '', effort: '' } }
|
|
87
|
+
const models = list.map((m) => ({
|
|
88
|
+
id: m.model,
|
|
89
|
+
...(m.name ? { name: m.name } : {}),
|
|
90
|
+
efforts: (m.efforts || []).filter((e) => CODEX_LEGAL.includes(e)),
|
|
91
|
+
...(m.defEffort && CODEX_LEGAL.includes(m.defEffort) ? { defEffort: m.defEffort } : {}),
|
|
92
|
+
}))
|
|
93
|
+
const star = list.find((m) => m.isDefault) || list[0]
|
|
94
|
+
const cat = { ok: true, models, aliases: [], efforts: CODEX_LEGAL.slice(1), source: sourceOf('codex') }
|
|
95
|
+
cat.recommended = { model: star.model, effort: '' }
|
|
96
|
+
cat.recommended.effort = recommendEffort(cat, star.model)
|
|
97
|
+
return cat
|
|
98
|
+
}
|
|
99
|
+
if (!p.baseUrl) {
|
|
100
|
+
const d = await get('claude')
|
|
101
|
+
if (d && Array.isArray(d.models) && d.models.length) {
|
|
102
|
+
const models = d.models.map((m) => ({ id: m.id, name: m.name, ...(m.effort !== false && m.efforts && m.efforts.length ? { efforts: m.efforts.slice() } : {}) }))
|
|
103
|
+
const cat = { ok: true, models, aliases: [], aliasOf: { ...(d.aliasOf || {}) }, efforts: CLAUDE_EFFORTS.slice(), source: sourceOf('claude') }
|
|
104
|
+
cat.recommended = { model: d.recommended || models[0].id, effort: CLAUDE_EFFORT }
|
|
105
|
+
cat.recommended.effort = recommendEffort(cat, cat.recommended.model) // xhigh 不在这型号的档里才换
|
|
106
|
+
return cat
|
|
107
|
+
}
|
|
108
|
+
// 从没拿到过实时清单(没登录 / CLI 太老 / 超时)⇒ 内置候选 + 本机账号额外项 + 官方别名
|
|
109
|
+
let ids = []
|
|
110
|
+
try { ids = claudemodels.mergeModels(claudemodels.readExtraModels()).map((m) => m.id).filter(Boolean) } catch { ids = claudemodels.BUILTIN.map((m) => m.id).filter(Boolean) }
|
|
111
|
+
return { ok: true, models: ids.map((id) => ({ id })), aliases: CLAUDE_ALIASES.slice(), efforts: CLAUDE_EFFORTS.slice(), recommended: { model: ids[0] || '', effort: CLAUDE_EFFORT }, source: 'builtin' }
|
|
112
|
+
}
|
|
113
|
+
return { ok: true, models: null, aliases: [], efforts: CLAUDE_EFFORTS.slice(), recommended: { model: '', effort: CLAUDE_EFFORT } }
|
|
114
|
+
}
|
|
115
|
+
// 守护进程启动时预热(有哪家的卡才探哪家):第一张模型卡 / 第一次打开群卡就不用等探测
|
|
116
|
+
catalog.warm = (kinds = ['codex']) => Promise.all(kinds.filter((k) => fetchers[k]).map((k) => refresh(k)))
|
|
117
|
+
// 原始清单(控制台 /api/codex/models · /api/claude/models 用:要 isDefault / 说明这些字段)+ 它是不是实时的
|
|
118
|
+
catalog.raw = async (kind) => ({ data: fetchers[kind] ? await get(kind) : null, source: live[kind] ? sourceOf(kind) : 'none' })
|
|
119
|
+
return catalog
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { createModelCatalog, CLAUDE_EFFORTS, CODEX_LEGAL, CLAUDE_ALIASES }
|
package/src/platform/feishu.js
CHANGED
|
@@ -848,6 +848,23 @@ function createFeishuPlatform({ appId, appSecret, botOpenId = null, onlyWhenMent
|
|
|
848
848
|
return sendText(chatId, content)
|
|
849
849
|
},
|
|
850
850
|
|
|
851
|
+
// One interactive reply under a group command (the /model card). Like
|
|
852
|
+
// replyOnce it is best-effort and never retried; the caller falls back
|
|
853
|
+
// to the text reply when this does not confirm.
|
|
854
|
+
async replyCardOnce(replyCtx, card) {
|
|
855
|
+
const mid = replyCtx && replyCtx.messageId
|
|
856
|
+
if (typeof mid !== 'string' || !/^om_[A-Za-z0-9_-]{1,124}$/.test(mid) ||
|
|
857
|
+
!card || typeof card !== 'object') {
|
|
858
|
+
throw new TypeError('Feishu card reply needs exact message target and a card')
|
|
859
|
+
}
|
|
860
|
+
return client.im.message.reply({ path: { message_id: mid },
|
|
861
|
+
data: { msg_type: 'interactive', content: JSON.stringify(card) } })
|
|
862
|
+
},
|
|
863
|
+
|
|
864
|
+
// open_id → display name (cached; null when unknown). The model card's
|
|
865
|
+
// receipt says who changed it; a card callback only carries the open_id.
|
|
866
|
+
nameOf: (openId) => senderNameOf(openId),
|
|
867
|
+
|
|
851
868
|
// 发一张静态富卡,返回 { msgId, cardId }(msgId=按钮/表情命中锚点;cardId=实体更新回填用)。
|
|
852
869
|
// 文本已由调用方在构卡时逐字段 redact(卡是 JSON 不能整体脱敏),故这里不再包 redact。
|
|
853
870
|
async sendCard(chatId, cardJson) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// 群里 /model 的「模型卡」。发 /model 不带参数 ⇒ 一张能点的卡:
|
|
3
|
+
// 后端一排按钮 · 型号一个下拉 · 思考深度一排按钮 + 「确认更新」「关闭」;确认后卡片收成一行回执(卡片本身就是回执,不另发消息);
|
|
4
|
+
// 10 分钟后过期翻只读(防止往上翻聊天记录时误点到几天前的老卡)。
|
|
5
|
+
// 纯函数、零依赖:飞书卡片 schema 2.0 的 JSON 形状只在此一处;样式与通知卡同一套(撑满宽 · 标题栏底色说类别 ·
|
|
6
|
+
// 注意事项与右下角小字用 notation 灰字;⚠ 2.0 不认 note 组件)。没有这种卡的平台(企微)由调用方退回文本回执。
|
|
7
|
+
// ⚠ 脱敏:型号 / 后端名来自配置与清单,没有用户输入;结果行里的人名来自平台,同样经 opts.redact。
|
|
8
|
+
|
|
9
|
+
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
10
|
+
const DEFAULT_VALUE = '__default__' // 下拉里「跟随默认」那一项的值(选项值不能是空串)
|
|
11
|
+
|
|
12
|
+
const grey = (s, size = 'notation') => ({ tag: 'markdown', text_size: size, content: `<font color='grey'>${esc(s)}</font>` })
|
|
13
|
+
|
|
14
|
+
function button(label, { selected = false, value, disabled = false } = {}) {
|
|
15
|
+
const b = { tag: 'button', text: { tag: 'plain_text', content: label }, type: selected ? 'primary_filled' : 'default', size: 'small', width: 'default' }
|
|
16
|
+
if (disabled) b.disabled = true
|
|
17
|
+
if (value) b.behaviors = [{ type: 'callback', value }]
|
|
18
|
+
return b
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 一排按钮:flow 布局,窄屏自动折行
|
|
22
|
+
function buttonRow(buttons) {
|
|
23
|
+
return { tag: 'column_set', flex_mode: 'flow', horizontal_spacing: '6px', horizontal_align: 'left', columns: buttons.map((b) => ({ tag: 'column', width: 'auto', elements: [b] })) }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 「先选、再确认」(点一下就生效容易点错):
|
|
28
|
+
* 点后端 / 选型号 / 点深度只改**卡上的草稿**(卡片重画选中状态,什么都没生效);草稿与当前不同时出一行「将改为 … —— 会不会开新对话」,
|
|
29
|
+
* 「确认更新」一次生效(只写一次、最多开一次新对话、审计一条),卡片收成一行回执;「关闭」什么都不改,卡片原地变成灰色「已关闭」。
|
|
30
|
+
* 草稿随每个回调值带回去(无状态,守护进程重启也不丢):{ a:'mc', f:'p'|'m'|'e'|'ok'|'x', v?, d:{p,m,e}, t }
|
|
31
|
+
* —— p 后端 · m 型号 · e 深度('' = 跟随默认);下拉的选中值在回调的 action.option 里。
|
|
32
|
+
* @param s {
|
|
33
|
+
* mode: 'edit' | 'receipt' | 'closed' | 'expired', current: 'claude · claude-opus-5 · xhigh'(当前生效的)
|
|
34
|
+
* failover: '', quota: '', —— 额度临时切换那一句 / 缓存额度(有才给)
|
|
35
|
+
* draft: { p, m, e }, —— edit:卡上正在选的
|
|
36
|
+
* backends: [{ name, selected }], model: { options: [{ value, label }], selected } | null(null = 这家型号到后台改),
|
|
37
|
+
* efforts: [{ value, label, selected }], —— label 里由调用方标「(默认)」
|
|
38
|
+
* preview: { to, note } | null, —— 草稿与当前不同才有;null ⇒「确认更新」灰着
|
|
39
|
+
* receipt: { line, before }, —— receipt:谁改成了什么 · 之前是什么
|
|
40
|
+
* issuedAt: 发卡 / 上次操作的时刻(写进回调值,10 分钟过期)
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
function buildModelCard(s, { redact } = {}) {
|
|
44
|
+
const r = typeof redact === 'function' ? (x) => String(redact(String(x))) : (x) => String(x)
|
|
45
|
+
const mode = s.mode || (s.expired ? 'expired' : 'edit')
|
|
46
|
+
const t = s.issuedAt || 0
|
|
47
|
+
const d = { p: '', m: '', e: '', ...(s.draft || {}) }
|
|
48
|
+
const cb = (f, v) => ({ a: 'mc', f, ...(v !== undefined ? { v } : {}), d, t })
|
|
49
|
+
const els = []
|
|
50
|
+
const head = (title, template, tag, tagColor) => ({
|
|
51
|
+
title: { tag: 'plain_text', content: title },
|
|
52
|
+
subtitle: { tag: 'plain_text', content: r(s.current || '') },
|
|
53
|
+
template, text_tag_list: [{ tag: 'text_tag', text: { tag: 'plain_text', content: tag }, color: tagColor }],
|
|
54
|
+
padding: '12px 12px 12px 12px',
|
|
55
|
+
})
|
|
56
|
+
let header
|
|
57
|
+
if (mode === 'receipt') {
|
|
58
|
+
// 确认之后:收成一行回执 —— 控件全收起(卡片就此「关闭」),群里留一条谁改成了什么
|
|
59
|
+
header = head('模型已更新', 'green', '已更新', 'green')
|
|
60
|
+
const rc = s.receipt || {}
|
|
61
|
+
els.push({ tag: 'markdown', text_size: 'normal', content: esc(r(rc.line || '')) })
|
|
62
|
+
if (rc.before) els.push(grey(r(rc.before)))
|
|
63
|
+
} else if (mode === 'closed') {
|
|
64
|
+
header = head('本群模型', 'grey', '已关闭', 'neutral')
|
|
65
|
+
els.push(grey('已关闭 · 模型没变', 'normal'))
|
|
66
|
+
} else if (mode === 'expired') {
|
|
67
|
+
header = head('本群模型', 'grey', '已过期', 'neutral')
|
|
68
|
+
els.push(grey('这张卡已过期,没有改任何东西 —— 发 /model 重新打开', 'normal'))
|
|
69
|
+
} else {
|
|
70
|
+
header = head('本群模型', 'indigo', '模型', 'indigo')
|
|
71
|
+
const info = []
|
|
72
|
+
if (s.failover) info.push(`**额度切换** ${esc(s.failover)}`)
|
|
73
|
+
if (s.quota) info.push(`**额度** ${esc(s.quota)}`)
|
|
74
|
+
if (info.length) els.push({ tag: 'markdown', text_size: 'normal', content: info.join('\n') })
|
|
75
|
+
const backendBtns = buttonRow((s.backends || []).map((b) => button(b.name, { selected: b.selected, value: b.selected ? null : cb('p', b.name) })))
|
|
76
|
+
let modelCtl
|
|
77
|
+
if (s.model) {
|
|
78
|
+
modelCtl = {
|
|
79
|
+
tag: 'select_static', width: 'fill', type: 'default',
|
|
80
|
+
placeholder: { tag: 'plain_text', content: '选一个型号' },
|
|
81
|
+
initial_option: s.model.selected || DEFAULT_VALUE,
|
|
82
|
+
options: s.model.options.map((o) => ({ text: { tag: 'plain_text', content: o.label }, value: o.value || DEFAULT_VALUE })),
|
|
83
|
+
behaviors: [{ type: 'callback', value: cb('m') }],
|
|
84
|
+
}
|
|
85
|
+
} else modelCtl = grey('这个后端的型号在控制台改', 'normal')
|
|
86
|
+
const effortBtns = buttonRow((s.efforts || []).map((e) => button(e.label || e.value, { selected: e.selected, value: e.selected ? null : cb('e', e.value || DEFAULT_VALUE) })))
|
|
87
|
+
// 标签在上、控件在下(手机上五个深度按钮一排放得下)
|
|
88
|
+
for (const [label, ctl] of [['后端', backendBtns], ['型号', modelCtl], ['思考深度', effortBtns]]) els.push(grey(label), ctl)
|
|
89
|
+
els.push({ tag: 'hr' })
|
|
90
|
+
if (s.preview) {
|
|
91
|
+
els.push({ tag: 'markdown', text_size: 'normal', content: `**将改为** ${esc(s.preview.to)}` })
|
|
92
|
+
if (s.preview.note) els.push(grey(s.preview.note))
|
|
93
|
+
}
|
|
94
|
+
els.push(buttonRow([
|
|
95
|
+
button('确认更新', { selected: !!s.preview, value: s.preview ? cb('ok') : null, disabled: !s.preview }),
|
|
96
|
+
button('关闭', { value: { a: 'mc', f: 'x', t } }),
|
|
97
|
+
]))
|
|
98
|
+
els.push(grey('选好后点「确认更新」才生效 · 换后端会开新对话,换型号或调深度接着当前对话 · 要等手头任务结束 · 10 分钟内有效 · 点不动时可直接发 /model high 这类文字'))
|
|
99
|
+
}
|
|
100
|
+
return { schema: '2.0', config: { width_mode: 'fill', update_multi: true }, header, body: { padding: '12px 12px 12px 12px', vertical_spacing: '8px', elements: els } }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { buildModelCard, DEFAULT_VALUE }
|
package/src/ui/console.html
CHANGED
|
@@ -1650,6 +1650,7 @@ function claudeChip(){
|
|
|
1650
1650
|
if(!s.loggedIn) return '<span class="chip none" title="这台机器还没登录 Claude —— 跑 claude auth login">未登录</span>';
|
|
1651
1651
|
return `<span class="chip set" title="claude auth status: ${esc(s.method||'')} · ${esc(s.email||'')}${s.org?' · '+esc(s.org):''}">订阅登录 ${IC.check}${s.plan?' · '+esc(s.plan):''}</span>`;
|
|
1652
1652
|
}
|
|
1653
|
+
function providerGroupCount(name){return (S.groups||[]).filter(g=>!g.paused&&(g.effectiveProvider||g.provider||S.defaultProvider)===name).length;}
|
|
1653
1654
|
function renderProviders(){
|
|
1654
1655
|
const cards=S.providers.filter(p=>showAll||(!p.hidden&&p.configured)).map(p=>{
|
|
1655
1656
|
const cs=p.runner==='codex'?codexStatus[p.name]:null;
|
|
@@ -1657,14 +1658,12 @@ function renderProviders(){
|
|
|
1657
1658
|
return `<div class="card pcard${p.hidden?' is-hidden':(!p.configured?' is-unconfig':'')}" data-prov="${esc(p.name)}">
|
|
1658
1659
|
<div class="ph"><div class="pn"><span class="drag" draggable="true" title="拖动排序">${IC.grip}</span>${esc(p.name)}${p.runner==='codex'?'<span class="cli-tag codex" title="GPT 经 Codex CLI 后端 · 订阅登录">Codex CLI</span>':'<span class="cli-tag claude" title="经 Claude Code CLI · 模型大脑按 env 切换">Claude Code CLI</span>'}</div>
|
|
1659
1660
|
<div class="inline" style="gap:8px">${chip}<button class="iconbtn cfg" data-act="edit" title="配置模型">${IC.settings}</button></div></div>
|
|
1660
|
-
|
|
1661
|
-
<div class="kv"><span class="kk">${p.contextWindow?.source==='official'?'模型上限':'当前窗口'}</span><span class="vv" title="${esc(providerWindowText(p,true))} · 只读,不会改变模型容量">${esc(providerWindowText(p))}</span></div
|
|
1662
|
-
${p.runner==='codex'?`<div class="kv"><span class="kk">窗口设置</span><span class="vv">${p.contextWindowTokens?esc(fmtContextTokens(p.contextWindowTokens)):'执行器默认'}</span></div
|
|
1661
|
+
${p.baseUrl?`<div class="kv"><span class="kk">型号</span><span class="vv" title="${esc(p.model||'')}">${esc(p.model?modelName(p.model):'—')}</span></div>
|
|
1662
|
+
<div class="kv"><span class="kk">${p.contextWindow?.source==='official'?'模型上限':'当前窗口'}</span><span class="vv" title="${esc(providerWindowText(p,true))} · 只读,不会改变模型容量">${esc(providerWindowText(p))}</span></div>`:`<div class="kv"><span class="kk">型号与深度</span><span class="vv" title="型号与思考深度按群在「群·项目」里选">按群选 · ${providerGroupCount(p.name)} 个群在用</span></div>`}
|
|
1663
|
+
${p.runner==='codex'?`<div class="kv"><span class="kk">窗口设置</span><span class="vv">${p.contextWindowTokens?esc(fmtContextTokens(p.contextWindowTokens)):'执行器默认'}</span></div>`:''}
|
|
1663
1664
|
<div class="kv"><span class="kk">${p.runner==='codex'?'压缩阈值':'压缩窗口'}</span><span class="vv" title="${esc(providerCompactText(p))} · 逐模型设置 · ${p.runner==='codex'?'Codex 可能按当前窗口提前压缩':'Claude Code 在压缩窗口满前触发,设置值受模型窗口限制'}">${esc(providerCompactText(p))}</span></div>
|
|
1664
1665
|
<div class="kv"><span class="kk">连接方式</span><span class="vv" title="${esc(p.runner==='codex'?'Codex CLI(codex exec · 订阅登录)':(p.baseUrl||'本机 Claude Code 的登录态 —— 无需 baseUrl / API key'))}">${esc(p.runner==='codex'?'Codex CLI (订阅登录)':(p.baseUrl?p.baseUrl.replace('https://',''):'Claude 订阅登录'))}</span></div>
|
|
1665
|
-
${p.effort?`<div class="kv"><span class="kk">思考深度</span><span class="vv">${esc(p.effort)}</span></div>`:''}
|
|
1666
1666
|
${p.vision===false?'<div class="kv"><span class="kk">识图</span><span class="vv" title="纯文本模型:送给它的图片(群里发的图/网页截图/读取的图片文件)自动降级成占位文本,不会 400 崩掉整轮">不支持 · 图片自动降级</span></div>':''}
|
|
1667
|
-
${p.runner==='codex'?`<div class="kv"><span class="kk">推理强度</span><span class="vv">${esc(p.reasoningEffort||cxDefEffortLabel(p.model))}</span></div>`:''}
|
|
1668
1667
|
${usage[p.name]?`<div class="kv"><span class="kk" style="flex:none">${esc(usage[p.name].label)}${usageRefreshBtn(false)}</span><span class="vv" title="${esc(usage[p.name].value)}${usage[p.name].label==='订阅额度'?' —— 百分比 = 已用,括号 = 距重置,与这台机器上交互式 Claude Code 共享的订阅窗口':''}" style="color:var(--ok);font-weight:600;white-space:normal;overflow:visible;max-width:none;flex:1;min-width:0;line-height:1.5">${esc(usage[p.name].value)}</span></div>`:''}
|
|
1669
1668
|
${(cs&&cs.usageText)?`<div class="kv"><span class="kk" style="flex:none">订阅额度${usageRefreshBtn(true)}</span><span class="vv" title="百分比 = 已用,括号 = 距重置 —— 与交互式 Codex 共享的订阅窗口(时长随官方口径)" style="color:var(--ok);font-weight:600;white-space:normal;overflow:visible;max-width:none;flex:1;min-width:0;line-height:1.5">${esc(cs.usageText)}</span></div>`:''}
|
|
1670
1669
|
<div class="acts"><button class="btn" data-act="test">测试</button>${p.hidden?'<span class="hint" style="align-self:center">已隐藏</span>':''}</div>
|
|
@@ -1674,7 +1673,7 @@ function renderProviders(){
|
|
|
1674
1673
|
const extra=S.providers.filter(p=>p.hidden||!p.configured).length;
|
|
1675
1674
|
const toggle=extra?`<button class="btn ghost" id="showAll">${showAll?'只看已配置':`显示全部 (${extra})`}</button>`:'';
|
|
1676
1675
|
$('#providers').innerHTML=`<div class="inline" style="justify-content:space-between;align-items:flex-start;margin:0 2px 14px;gap:10px">
|
|
1677
|
-
<div class="hint" style="flex:1">${S.runtimeCapabilities?esc(runtimeAllows('POST','/api/providers/add')?RUNTIME_UI_TEXT.providers:RUNTIME_UI_TEXT.providersView):`管理模型 · 拖 ${IC.grip} 排序 · ${IC.settings} 配端点/key
|
|
1676
|
+
<div class="hint" style="flex:1">${S.runtimeCapabilities?esc(runtimeAllows('POST','/api/providers/add')?RUNTIME_UI_TEXT.providers:RUNTIME_UI_TEXT.providersView):`管理模型 · 拖 ${IC.grip} 排序 · ${IC.settings} 配端点/key/压缩 · 型号与思考深度按群在「群·项目」选`}</div>
|
|
1678
1677
|
<div class="inline" style="gap:8px">${toggle}<button class="btn primary" id="addModel">${IC.plus} 添加模型</button></div></div>
|
|
1679
1678
|
<div class="grid cols2 stagger" id="pgrid">${cards}</div>
|
|
1680
1679
|
<h2 class="sec" style="margin-top:32px">插件库 · MCP</h2>
|
|
@@ -1805,21 +1804,26 @@ function openProviderModal(name,opts){
|
|
|
1805
1804
|
const preset=edit?'':`<div class="field"><label class="f">预设(自动填端点/模型/调优)</label><select id="amPreset">${Object.keys(PRESETS).map(k=>`<option value="${esc(k)}" ${opts.preset===k?'selected':''}>${esc(k)}</option>`).join('')}</select></div>`;
|
|
1806
1805
|
const keyPh=edit?(p.keyStatus==='set'?'已设置 · 留空保持':p.keyStatus==='env'?'env:'+esc(p.keyRef)+' · 留空保持':'粘贴 key'):'粘贴 key(存进本地 gitignored 配置)';
|
|
1807
1806
|
const codexNote=isCodex?'<div class="hint" style="margin-bottom:12px">使用本机 ChatGPT 订阅登录,额度与本机 Codex 共享。</div>':'';
|
|
1807
|
+
// Durable hosts: model and thinking depth are chosen per group (group card / /model);
|
|
1808
|
+
// only a third-party endpoint keeps its model here as connection configuration.
|
|
1809
|
+
const perGroup=!!S.runtimeCapabilities;
|
|
1810
|
+
const showModelSec=!edit||!perGroup||(!isCodex&&!isClaudeLogin);
|
|
1808
1811
|
$('#modal').innerHTML=`<h3>${edit?'配置 '+esc(name):'添加模型'}</h3>
|
|
1809
1812
|
${preset}
|
|
1810
1813
|
${codexNote}
|
|
1811
|
-
|
|
1814
|
+
${showModelSec?`<h4 class="sec">${perGroup?'名称与型号':'型号与推理'}</h4>`:''}
|
|
1812
1815
|
${edit?'':'<div class="field"><label class="f" for="amName">名称</label><input id="amName" placeholder="如 glm / my-model"/></div>'}
|
|
1813
|
-
${
|
|
1816
|
+
${perGroup?`<div class="hint" style="margin-bottom:12px">型号与思考深度按群在「群·项目」里选${(isCodex||isClaudeLogin)?'':';这个端点的型号在下面填,各群共用'}。</div>`:''}
|
|
1817
|
+
${isCodex&&perGroup?'':isCodex?`<div class="field"><label class="f">模型 model</label>
|
|
1814
1818
|
<select id="amModelSel"><option value="${edit?esc(p.model):''}" selected>${edit&&p.model?esc(p.model):'(未设 — 用 codex 默认)'}</option></select>
|
|
1815
1819
|
<input id="amModel" value="${edit?esc(p.model):''}" placeholder="如 gpt-5.6-sol" style="display:none;margin-top:8px"/>
|
|
1816
1820
|
<div class="hint" id="amModelSrc" style="margin-top:4px">加载模型列表…</div></div>`
|
|
1817
|
-
:isClaudeLogin?`<div class="field" id="clModelWrap"><label class="f">模型 model</label>
|
|
1821
|
+
:isClaudeLogin&&perGroup?'':isClaudeLogin?`<div class="field" id="clModelWrap"><label class="f">模型 model</label>
|
|
1818
1822
|
<div class="hint">检测登录中…</div></div>`
|
|
1819
1823
|
:`<div class="field"><label class="f">模型 model</label><input id="amModel" value="${edit?esc(p.model):''}" placeholder="如 glm-5.2[1m]"/></div>`}
|
|
1820
|
-
${isCodex?`<div class="field"><label class="f">推理强度</label><select id="amRE">${RE_CODEX.concat(edit&&p.reasoningEffort&&!RE_CODEX.includes(p.reasoningEffort)?[p.reasoningEffort]:[]).map(v=>`<option value="${v}" ${edit&&v===(p.reasoningEffort||'')?'selected':''}>${v||'默认(模型内置)'}</option>`).join('')}</select>
|
|
1824
|
+
${isCodex&&!perGroup?`<div class="field"><label class="f">推理强度</label><select id="amRE">${RE_CODEX.concat(edit&&p.reasoningEffort&&!RE_CODEX.includes(p.reasoningEffort)?[p.reasoningEffort]:[]).map(v=>`<option value="${v}" ${edit&&v===(p.reasoningEffort||'')?'selected':''}>${v||'默认(模型内置)'}</option>`).join('')}</select>
|
|
1821
1825
|
<div class="hint" style="margin-top:4px">默认沿用模型推荐强度;更高强度通常需要更多时间和额度。</div></div>`:''}
|
|
1822
|
-
${isCodex?'':`<div class="field"><label class="f">思考深度</label><select id="amEff">${EFFORTS.map(v=>`<option value="${v}" ${edit&&v===p.effort?'selected':''}>${v||'默认'}</option>`).join('')}</select>
|
|
1826
|
+
${isCodex||perGroup?'':`<div class="field"><label class="f">思考深度</label><select id="amEff">${EFFORTS.map(v=>`<option value="${v}" ${edit&&v===p.effort?'selected':''}>${v||'默认'}</option>`).join('')}</select>
|
|
1823
1827
|
<div class="hint" style="margin-top:5px">默认沿用模型推荐强度;更高强度通常需要更多时间和额度。</div></div>`}
|
|
1824
1828
|
<h4 class="sec">上下文与压缩</h4>
|
|
1825
1829
|
<details class="field"><summary class="hint" style="cursor:pointer">容量与运行信息</summary><div class="dim" id="amWindow" style="padding:9px 0">${edit?esc(providerWindowText(p,true)):'添加后按模型目录或会话上报显示'}</div></details>
|
|
@@ -1946,7 +1950,7 @@ function openProviderModal(name,opts){
|
|
|
1946
1950
|
const b={name:nm,autocompactTokens:$('#amCompMode').value==='default'?0:Number($('#amComp').value)};
|
|
1947
1951
|
if(!Number.isInteger(b.autocompactTokens)||b.autocompactTokens<0||($('#amCompMode').value==='custom'&&(b.autocompactTokens<100000||b.autocompactTokens>1000000))){toast('压缩阈值须为 0 或 10万–100万 token','err');return;}
|
|
1948
1952
|
if(selModel)b.model=selModel;
|
|
1949
|
-
if(isCodex){b.model=selModel||'';b.reasoningEffort=$('#amRE').value;
|
|
1953
|
+
if(isCodex){if(!perGroup){b.model=selModel||'';b.reasoningEffort=$('#amRE').value;}
|
|
1950
1954
|
const value=$('#amContextMode').value==='default'?'0':$('#amContext').value.trim();b.contextWindowTokens=Number(value);
|
|
1951
1955
|
if(!value||!Number.isSafeInteger(b.contextWindowTokens)||b.contextWindowTokens<0||($('#amContextMode').value==='custom'&&(b.contextWindowTokens<100000||b.contextWindowTokens>1050000))){toast('自定义窗口须为 10万–105万 token','err');return;}
|
|
1952
1956
|
// ⚠ 新建时必须显式带 runner —— 少了它建出来的是个"没有端点没有 key 的 claude provider",
|
|
@@ -1955,8 +1959,8 @@ function openProviderModal(name,opts){
|
|
|
1955
1959
|
else if(isClaudeLogin){
|
|
1956
1960
|
// 订阅登录:端点/key/识图三项面板里就没有 ⇒ 不回传(回传空串会把已有值清掉)。
|
|
1957
1961
|
// 型号可以被清空成「跟随 Claude Code 默认」,那要显式 modelClear —— 但只在控件真在时才发。
|
|
1958
|
-
b.effort=$('#amEff').value; if(hasModelCtl&&!selModel)b.modelClear=true;
|
|
1959
|
-
} else {b.model=selModel;b.baseUrl=$('#amBase').value.trim();b.apiKey=$('#amKey').value.trim();b.effort=$('#amEff').value;b.vision=!$('#amVision').checked;}
|
|
1962
|
+
if(!perGroup){b.effort=$('#amEff').value; if(hasModelCtl&&!selModel)b.modelClear=true;}
|
|
1963
|
+
} else {b.model=selModel;b.baseUrl=$('#amBase').value.trim();b.apiKey=$('#amKey').value.trim();if($('#amEff'))b.effort=$('#amEff').value;b.vision=!$('#amVision').checked;}
|
|
1960
1964
|
if(!edit)b.env=JSON.parse($('#amPreset').dataset.env||'{}');
|
|
1961
1965
|
if(edit)b.hidden=$('#amHide').checked;
|
|
1962
1966
|
try{const r=await api('/api/providers/add','POST',b);S=r;
|
|
@@ -2655,7 +2659,7 @@ function cronFullHtml(text){
|
|
|
2655
2659
|
}).join('');
|
|
2656
2660
|
}
|
|
2657
2661
|
const RUNTIME_UI_TEXT={
|
|
2658
|
-
"groups": "
|
|
2662
|
+
"groups": "可绑定新群,修改后端、型号、思考深度、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。换后端或项目会开始新的一段对话,换型号或调深度接着当前对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /model 也能改。",
|
|
2659
2663
|
"skills": "当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。",
|
|
2660
2664
|
"cron": "当前不执行定时任务;已有定义仅供查看。",
|
|
2661
2665
|
"board": "画布创建、同步与反馈暂未接入;已有链接仅供查看。",
|
|
@@ -2665,10 +2669,80 @@ const RUNTIME_UI_TEXT={
|
|
|
2665
2669
|
"resumeLabel": "恢复",
|
|
2666
2670
|
"steer": "Codex 与 Claude Code 默认开启忙时跟进;长命令执行期间的补充会在工具步骤之间读取。只有确认模型已读取才显示已加入;不支持此能力时排队。控制台插话同样支持忙时跟进;关闭或暂不支持时排队。",
|
|
2667
2671
|
"preview": "当前版本未接入预览服务。",
|
|
2668
|
-
"providers": "
|
|
2672
|
+
"providers": "可添加模型,修改端点、密钥、上下文与压缩,测试、隐藏或拖动排序;型号与思考深度按群在「群·项目」里选(第三方端点的型号在这里填)。改后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。",
|
|
2669
2673
|
"providersView": "当前可查看模型配置与登录状态;各群的模型在「群·项目」里切换。添加或修改模型暂不可用。",
|
|
2670
2674
|
"unavailable": "当前版本暂不支持此操作。"
|
|
2671
2675
|
};
|
|
2676
|
+
// Model display names (claude-opus-5-5 → Opus 5.5): the server injects the same
|
|
2677
|
+
// function the group replies use; stored and sent values stay the official ids.
|
|
2678
|
+
const modelNameRule=/*__SBOT_MODELNAME__*/(s=>String(s==null?'':s));
|
|
2679
|
+
const MODEL_OPTS={}; // provider name → /api/groups/model-options result
|
|
2680
|
+
const modelName=(id)=>{const k=String(id==null?'':id).trim();if(!k)return '';
|
|
2681
|
+
const listed=Object.values(MODEL_OPTS).map(c=>c&&Array.isArray(c.models)&&c.models.find(m=>m&&m.id===k)).find(Boolean);
|
|
2682
|
+
return (S&&S.modelNames&&S.modelNames[k])||(listed&&listed.name)||modelNameRule(k);};
|
|
2683
|
+
// Backend · model · thinking depth on the group card. Choices are stored per
|
|
2684
|
+
// backend; only a backend change starts a new conversation.
|
|
2685
|
+
const MODEL_OPTS_LOADING=new Set();
|
|
2686
|
+
function loadModelOpts(name){
|
|
2687
|
+
if(!name||MODEL_OPTS[name]||MODEL_OPTS_LOADING.has(name))return;
|
|
2688
|
+
MODEL_OPTS_LOADING.add(name);
|
|
2689
|
+
api('/api/groups/model-options?provider='+encodeURIComponent(name)).then(c=>{MODEL_OPTS[name]=c||{ok:false,models:[],efforts:[]};})
|
|
2690
|
+
.catch(()=>{MODEL_OPTS[name]={ok:false,models:[],efforts:[]};})
|
|
2691
|
+
.finally(()=>{MODEL_OPTS_LOADING.delete(name);if($('#groups'))renderGroups();});
|
|
2692
|
+
}
|
|
2693
|
+
function catEfforts(cat,model){
|
|
2694
|
+
if(!cat)return null;
|
|
2695
|
+
const m=Array.isArray(cat.models)?cat.models.find(x=>x.id===model):null;
|
|
2696
|
+
if(m&&Array.isArray(m.efforts))return m.efforts; // [] = this model has no depth setting
|
|
2697
|
+
return cat.efforts||[];
|
|
2698
|
+
}
|
|
2699
|
+
function groupBrainRows(g){
|
|
2700
|
+
const b=g.brain||{provider:g.effectiveProvider||g.provider||S.defaultProvider,model:'',effort:''};
|
|
2701
|
+
const name=b.provider, cat=MODEL_OPTS[name];
|
|
2702
|
+
if(!cat)loadModelOpts(name);
|
|
2703
|
+
const sel='style="width:auto;min-width:110px;max-width:100%;padding:5px 30px 5px 10px;font-size:11.5px"';
|
|
2704
|
+
const unset='<span style="color:var(--fail)">未选</span>';
|
|
2705
|
+
const provs=S.providers.filter(pp=>!pp.hidden&&pp.configured);
|
|
2706
|
+
if(!provs.some(pp=>pp.name===name))provs.unshift({name});
|
|
2707
|
+
const backend=`<select class="gprov" data-g="${esc(g.chatId)}" ${sel}>${provs.map(pp=>`<option value="${esc(pp.name)}" ${pp.name===name?'selected':''}>${esc(pp.name)}</option>`).join('')}</select>`;
|
|
2708
|
+
let model;
|
|
2709
|
+
if(!cat)model=`<span class="vv">${b.model?esc(modelName(b.model)):unset} <span class="faint">· 加载清单…</span></span>`;
|
|
2710
|
+
else if(cat.models===null)model=`<span class="vv" title="这个端点的型号在「模型」页设置,各群共用">${b.model?esc(modelName(b.model)):'—'} <span class="faint">· 在「模型」页改</span></span>`;
|
|
2711
|
+
else{
|
|
2712
|
+
const list=cat.models||[];
|
|
2713
|
+
const opts=list.map(m=>`<option value="${esc(m.id)}" ${m.id===b.model?'selected':''}>${esc(m.name||modelName(m.id))}${cat.recommended&&cat.recommended.model===m.id?' ★':''}</option>`);
|
|
2714
|
+
if(b.model&&!list.some(m=>m.id===b.model))opts.unshift(`<option value="${esc(b.model)}" selected>${esc(modelName(b.model))}</option>`);
|
|
2715
|
+
if(!b.model)opts.unshift('<option value="" selected disabled>未选</option>');
|
|
2716
|
+
opts.push('<option value="__custom__">自定义…</option>');
|
|
2717
|
+
model=`<span class="vv inline" style="gap:6px;flex-wrap:wrap;min-width:0"><select class="gmodel" data-g="${esc(g.chatId)}" ${sel}>${opts.join('')}</select>
|
|
2718
|
+
<span class="gcustom" data-g="${esc(g.chatId)}" hidden><input class="gcustomv" placeholder="型号 id" style="width:150px;padding:5px 8px;font-size:11.5px"/> <button class="btn" data-gcustom="${esc(g.chatId)}" style="padding:3px 9px;font-size:11px">保存</button></span>
|
|
2719
|
+
${cat.ok===false?'<span class="faint" style="font-size:11px">清单暂时取不到</span>':''}</span>`;
|
|
2720
|
+
}
|
|
2721
|
+
const effs=catEfforts(cat,b.model);
|
|
2722
|
+
let effort;
|
|
2723
|
+
if(effs&&!effs.length&&cat&&Array.isArray(cat.models)&&cat.models.some(m=>m.id===b.model))effort='<span class="vv faint">这个型号不支持</span>';
|
|
2724
|
+
else{
|
|
2725
|
+
const list=(effs&&effs.length?effs:['low','medium','high','xhigh','max']).slice();
|
|
2726
|
+
if(b.effort&&!list.includes(b.effort))list.push(b.effort);
|
|
2727
|
+
effort=`<select class="geffort" data-g="${esc(g.chatId)}" ${sel}>${b.effort?'':'<option value="" selected disabled>未选</option>'}${list.map(e=>`<option value="${esc(e)}" ${e===b.effort?'selected':''}>${esc(e)}</option>`).join('')}</select>`;
|
|
2728
|
+
}
|
|
2729
|
+
const pr=S.providers.find(pp=>pp.name===name)||{};
|
|
2730
|
+
const win=g.contextWindow&&g.contextWindow.tokens?providerWindowText({contextWindow:g.contextWindow,runner:pr.runner},true):'';
|
|
2731
|
+
// One "模型" row: backend and model side by side (they wrap on a narrow screen).
|
|
2732
|
+
return `<div class="kv"><span class="kk">模型</span><span class="vv inline" style="gap:6px;flex-wrap:wrap;min-width:0">${backend}${model}</span></div>
|
|
2733
|
+
<div class="kv"><span class="kk">思考深度</span>${effort}</div>
|
|
2734
|
+
${win?`<div class="kv"><span class="kk">上下文</span><span class="vv" title="${esc(win)}">${esc(win)}</span></div>`:''}
|
|
2735
|
+
<div class="kv"><span class="kk"></span><span class="faint" style="font-size:11px">换后端会开新对话;换型号或调深度接着当前对话 · 群空闲时才能改</span></div>`;
|
|
2736
|
+
}
|
|
2737
|
+
async function saveGroupBrain(chatId,change){
|
|
2738
|
+
try{
|
|
2739
|
+
const r=await api('/api/groups/brain','POST',{chatId,...change});S=r;renderOverview();renderGroups();
|
|
2740
|
+
const c=r.brainChange||{};
|
|
2741
|
+
const filled=c.filled&&c.filled.length?'(未选的'+c.filled.join('和')+'先用推荐)':'';
|
|
2742
|
+
const what=[c.model?modelName(c.model):'',c.effort||''].filter(Boolean).join(' · ');
|
|
2743
|
+
toast(!c.changed?'没有变化':c.switching?`已切到 ${c.provider}${what?' · '+what:''} · 会话已重置,下条消息生效${filled}`:c.modelChanged?`已改为 ${what} · 接着当前对话,下条消息生效${filled}`:`思考深度已调为 ${c.effort} · 下条消息生效`,'ok');
|
|
2744
|
+
}catch(e){toast(e.message,'err');renderGroups();}
|
|
2745
|
+
}
|
|
2672
2746
|
function renderGroups(){
|
|
2673
2747
|
const provOf=g=>S.providers.find(pp=>pp.name===(g.provider||S.defaultProvider));
|
|
2674
2748
|
const cards=S.groups.map(g=>{
|
|
@@ -2885,10 +2959,7 @@ function renderGroups(){
|
|
|
2885
2959
|
</label>
|
|
2886
2960
|
<span class="faint" style="font-size:11px">${g.redactPii===false?'当前:明文':'当前:遮盖'}</span>
|
|
2887
2961
|
</span></div>
|
|
2888
|
-
|
|
2889
|
-
<select class="gprov" data-g="${esc(g.chatId)}" style="width:auto;min-width:110px;padding:5px 30px 5px 10px;font-size:11.5px">
|
|
2890
|
-
${S.providers.filter(pp=>!pp.hidden&&pp.configured).map(pp=>`<option value="${esc(pp.name)}" ${pp.name===(g.provider||S.defaultProvider)?'selected':''}>${esc(pp.name)}${pp.model?' · '+esc(pp.model):''}</option>`).join('')}
|
|
2891
|
-
</select></div>
|
|
2962
|
+
${groupBrainRows(g)}
|
|
2892
2963
|
<div class="kv" style="align-items:flex-start"><span class="kk">忙时跟进</span><span class="vv" style="min-width:0">
|
|
2893
2964
|
<label class="inline" style="gap:6px;cursor:pointer" title="${esc(S.runtimeCapabilities?RUNTIME_UI_TEXT.steer:'开启后,处理任务期间:群成员的后续消息、控制台勾了「回复」的插话,都会加入当前任务 —— 相关要求及时吸收,独立请求按先后回应。定时任务和不回复的插话仍排在后面。')}">
|
|
2894
2965
|
<input type="checkbox" class="gsteer" data-g="${esc(g.chatId)}"${g.followupEnabled?' checked':''} style="width:auto;margin:0"/>
|
|
@@ -2932,7 +3003,14 @@ function renderGroups(){
|
|
|
2932
3003
|
const on=b.dataset.p!=='1';
|
|
2933
3004
|
if(on&&!confirm(S.runtimeCapabilities?'暂停本群?只有任务结束且记录全部结账后才能暂停;暂停期间不处理新消息,配置和会话保留。':'暂停本群?\n期间消息不处理(静默丢弃)、排队清空、定时任务与画布播报停发;进行中的任务会跑完。\n完全可逆:配置/会话/画布全保留,点「恢复」即原样回来。'))return;
|
|
2934
3005
|
try{S=await api('/api/groups/pause','POST',{chatId:b.dataset.gpause,paused:on});renderGroups();renderOverview();toast(S.runtimeCapabilities?(on?'已暂停':'已恢复'):(on?'已暂停 · 群内已通知':'已恢复 · 群内已通知'),'ok');}catch(e){toast(e.message,'err');}});
|
|
2935
|
-
$$('#groups .gprov').forEach(sel=>sel.onchange=
|
|
3006
|
+
$$('#groups .gprov').forEach(sel=>sel.onchange=()=>saveGroupBrain(sel.dataset.g,{provider:sel.value}));
|
|
3007
|
+
$$('#groups .gmodel').forEach(sel=>sel.onchange=()=>{const box=$('.gcustom[data-g="'+sel.dataset.g+'"]');
|
|
3008
|
+
if(sel.value==='__custom__'){if(box){box.hidden=false;box.querySelector('input').focus();}return;}
|
|
3009
|
+
if(box)box.hidden=true;saveGroupBrain(sel.dataset.g,{model:sel.value});});
|
|
3010
|
+
$$('#groups [data-gcustom]').forEach(b=>b.onclick=()=>{const box=$('.gcustom[data-g="'+b.dataset.gcustom+'"]'),v=box&&box.querySelector('input').value.trim();
|
|
3011
|
+
if(!v){toast('填写型号 id','err');return;}saveGroupBrain(b.dataset.gcustom,{model:v});});
|
|
3012
|
+
$$('#groups .geffort').forEach(sel=>sel.onchange=()=>saveGroupBrain(sel.dataset.g,{effort:sel.value}));
|
|
3013
|
+
|
|
2936
3014
|
$$('#groups .gsteer').forEach(c=>c.onchange=async()=>{const on=c.checked;
|
|
2937
3015
|
try{S=await api('/api/groups/steer','POST',{chatId:c.dataset.g,enabled:on});renderGroups();toast(on?'已开启 · 下轮生效':'已关闭 · 下轮恢复排队','ok');}
|
|
2938
3016
|
catch(e){c.checked=!on;toast(e.message,'err');}});
|
|
@@ -4350,7 +4428,7 @@ const I18N_EN=[
|
|
|
4350
4428
|
["等待模型读取,尚未确认加入", "Waiting for the model to read it; not yet confirmed joined"],
|
|
4351
4429
|
["处理状态请查看实时活动", "Check live activity for processing status"],
|
|
4352
4430
|
["需要处理", "Needs attention"],
|
|
4353
|
-
["
|
|
4431
|
+
["可绑定新群,修改后端、型号、思考深度、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。换后端或项目会开始新的一段对话,换型号或调深度接着当前对话。停止任务和开新会话在「概览」,开新会话须等该群空闲。群里发 /model 也能改。", "Bind new groups and change the backend, model, thinking depth, project folder, permissions, data display and busy-turn followups of a group with no running task or unsettled record. Changing the backend or project starts a new conversation; changing the model or depth continues it. Stop tasks and start new sessions in Overview; a new session needs the group to be idle. Members can also change these with /model in the group."],
|
|
4354
4432
|
["当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。", "Feishu text messages, busy-turn followups, task replies and stopping are supported. Schedules, image generation, group message management, canvas and previews are unavailable."],
|
|
4355
4433
|
["当前不执行定时任务;已有定义仅供查看。", "Schedules are not executed. Existing definitions are shown for reference only."],
|
|
4356
4434
|
["画布创建、同步与反馈暂未接入;已有链接仅供查看。", "Canvas creation, sync and feedback are unavailable. Existing links are view-only."],
|
|
@@ -4361,7 +4439,7 @@ const I18N_EN=[
|
|
|
4361
4439
|
["Codex 与 Claude Code 默认开启忙时跟进;长命令执行期间的补充会在工具步骤之间读取。只有确认模型已读取才显示已加入;不支持此能力时排队。控制台插话同样支持忙时跟进;关闭或暂不支持时排队。", "Busy-turn followups are on by default for Codex and Claude Code. Followups are read between tool steps; a long command must finish first. Joined means the model has read the message. Unsupported clients queue followups. Console messages also support busy-turn followups, or queue when followups are disabled or unavailable."],
|
|
4362
4440
|
["当前版本未接入预览服务。", "Preview services are unavailable in this version."],
|
|
4363
4441
|
["当前可查看模型配置与登录状态;各群的模型在「群·项目」里切换。添加或修改模型暂不可用。", "Model configuration and login status are shown here. Switch each group's model under Groups. Adding or changing models is unavailable."],
|
|
4364
|
-
["
|
|
4442
|
+
["可添加模型,修改端点、密钥、上下文与压缩,测试、隐藏或拖动排序;型号与思考深度按群在「群·项目」里选(第三方端点的型号在这里填)。改后端或端点时,用它的群须空闲,并会开始新的一段对话;默认模型和群正在用的模型不能隐藏。移除模型暂不可用。", "Add models; change their endpoint, key, context and compaction; test, hide or drag to reorder them. Model and thinking depth are chosen per group under Groups (a third-party endpoint's model is set here). Changing a backend or endpoint needs the groups using it to be idle and starts a new conversation for them. The default model and models in use by a group cannot be hidden. Removing models is unavailable."],
|
|
4365
4443
|
["默认开启 · 下轮生效", "On by default · Applies next turn"],
|
|
4366
4444
|
["当前只能添加或修改 Codex 或 Claude Code 模型", "Only Codex or Claude Code models can be added or changed right now."],
|
|
4367
4445
|
["模型上限", "Model limit"],
|
|
@@ -4579,6 +4657,24 @@ const I18N_EN=[
|
|
|
4579
4657
|
["管理模型 · 拖","Manage models · drag"],
|
|
4580
4658
|
["排序 ·","to reorder ·"],
|
|
4581
4659
|
["配端点/key/思考深度 · 模型按群在「群·项目」选","sets endpoint / key / thinking depth · models are chosen per group on the Groups · Projects page"],
|
|
4660
|
+
["配端点/key/压缩 · 型号与思考深度按群在「群·项目」选", "sets endpoint / key / compaction · model and thinking depth are chosen per group on the Groups · Projects page"],
|
|
4661
|
+
["按群选", "Per group"],
|
|
4662
|
+
["个群在用", "groups using it"],
|
|
4663
|
+
["型号与深度", "Model and depth"],
|
|
4664
|
+
["型号与思考深度按群在「群·项目」里选", "Model and thinking depth are chosen per group under Groups"],
|
|
4665
|
+
[";这个端点的型号在下面填,各群共用", "; this endpoint's model is set below and shared by its groups"],
|
|
4666
|
+
["名称与型号", "Name and model"],
|
|
4667
|
+
["后端", "Backend"],
|
|
4668
|
+
["未选", "Not chosen"],
|
|
4669
|
+
["加载清单…", "loading list…"],
|
|
4670
|
+
["在「模型」页改", "change on the Models page"],
|
|
4671
|
+
["清单暂时取不到", "List unavailable right now"],
|
|
4672
|
+
["这个型号不支持", "Not supported by this model"],
|
|
4673
|
+
["上下文", "Context"],
|
|
4674
|
+
["型号 id", "Model id"],
|
|
4675
|
+
["换后端会开新对话;换型号或调深度接着当前对话 · 群空闲时才能改", "A backend change starts a new conversation; a model or depth change continues it · only while the group is idle"],
|
|
4676
|
+
["没有变化", "No change"],
|
|
4677
|
+
["填写型号 id", "Enter a model id"],
|
|
4582
4678
|
["添加模型","Add model"],
|
|
4583
4679
|
["插件库 · MCP","Plugin library · MCP"],
|
|
4584
4680
|
["本机装好的 MCP · 按运行时分两套 · 命令行安装、即时刷新","MCP servers installed on this machine · two sets, one per runtime · installed from the command line, refreshes instantly"],
|