sintbot 1.4.1 → 1.4.3
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 +42 -0
- package/CHANGELOG.json +42 -0
- package/bin/sintbot-v2.js +33 -2
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +6 -4
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +6 -4
- package/install.sh +1 -1
- package/package.json +5 -1
- package/src/c2configcontrol.js +3 -2
- package/src/c2console.js +6 -3
- package/src/c2multigroupservice.js +19 -2
- package/src/c2updater.js +361 -0
- package/src/c2upgradeswitch.js +169 -0
- package/src/core/c2cardterminalpath.js +17 -3
- package/src/core/c2durableruntime.js +6 -0
- package/src/core/turnprogress.js +135 -0
- package/src/core/v2engine.js +141 -19
- package/src/ui/console.html +36 -15
- package/src/updater.js +243 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// 卡片进度:里程碑 · 「最近在做」· 脉搏文案(纯函数)。
|
|
4
|
+
// 发行宿主(v2engine)与卡片收尾共用一份。只做展示,不参与任何收据或效果判定。
|
|
5
|
+
// 卡片文案与旧引擎逐字一致,便于逐字对照。
|
|
6
|
+
|
|
7
|
+
// 长任务的「活着」信号与「做到哪了」。⚠ 只在卡里滚动,不另发消息 —— 群里多一条就是一次刷屏。
|
|
8
|
+
const T = {
|
|
9
|
+
// 迟迟没动静时先回一声:填进本来就要建的那张流式卡,答案到了原地替换 —— 不多发消息。
|
|
10
|
+
// 文案过零工具名,且不说「马上/很快」这类无法兑现的承诺。
|
|
11
|
+
ackWorking: '⏳ 已收到,正在处理…',
|
|
12
|
+
// 「最近动静」只在**停滞**时才有信息量 ⇒ 超过 cardPulseStaleMs(默认 2 分钟)没动静才追加。
|
|
13
|
+
// 还没调过任何工具时退回「处理中 · 已跑」。
|
|
14
|
+
pulse: (ran, doing, stale) => (doing && doing.length ? `⏳ 已跑 ${ran} · ${T.pulseDoing(doing)}` : `⏳ 处理中 · 已跑 ${ran}`) + (stale ? ` · 最近动静 ${stale}前` : ''),
|
|
15
|
+
// 由工具调用**确定性**推出(describeTool),模型一个字不说也有东西看。脉搏与 /status 共用。
|
|
16
|
+
// 只给文件名不给路径、不给命令内容 —— 群面零机器路径。
|
|
17
|
+
pulseDoing: (list) => `最近在做:${list.join(' · ')}`,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 脉搏「已跑多久」:分钟粒度够用(卡片底部那行的耗时另算到秒)。
|
|
21
|
+
function humanDur(ms) {
|
|
22
|
+
const s = Math.max(0, Math.round(ms / 1000))
|
|
23
|
+
if (s < 60) return `${s} 秒`
|
|
24
|
+
const m = Math.floor(s / 60)
|
|
25
|
+
if (m < 60) return `${m} 分钟`
|
|
26
|
+
const h = Math.floor(m / 60)
|
|
27
|
+
return m % 60 ? `${h} 小时 ${m % 60} 分钟` : `${h} 小时`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── 里程碑与「最近在做」─────────────────────────────────────────────────────
|
|
31
|
+
// 模型每完成一个阶段输出一行 `>> …`,引擎认它 —— 跑的时候随打字机进卡(显示成 ▶),收尾收进卡片
|
|
32
|
+
// 折叠区,不再被「只留最后一段」丢掉。判据收在这里,不散在三处(打字机 / 脉搏 / 收尾)。
|
|
33
|
+
const MILESTONE_MAX = 12
|
|
34
|
+
const MILESTONE_RE = /^>>\s*(.*\S)\s*$/
|
|
35
|
+
// 逐行扫、跳过代码围栏 —— shell 的 `>> file` 与 Python 的 `>>>` 都可能出现在代码块里。
|
|
36
|
+
function scanMilestoneLines(text, onLine) {
|
|
37
|
+
let fence = false
|
|
38
|
+
for (const line of String(text || '').split('\n')) {
|
|
39
|
+
if (/^\s*```/.test(line)) { fence = !fence; onLine(line, null); continue }
|
|
40
|
+
const m = fence ? null : MILESTONE_RE.exec(line)
|
|
41
|
+
onLine(line, m ? m[1] : null)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// 本轮全部里程碑(去重、封顶),从流式全量文本里取 —— 中途说的与收尾说的都算。
|
|
45
|
+
function milestonesOf(text) {
|
|
46
|
+
const out = []
|
|
47
|
+
scanMilestoneLines(text, (_l, m) => { if (m && !out.includes(m) && out.length < MILESTONE_MAX) out.push(m) })
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
// 打字机 / 脉搏用:`>> ` 原地换成 ▶(不换的话 markdown 会把 `>>` 渲染成嵌套引用)。
|
|
51
|
+
function renderMilestones(text) {
|
|
52
|
+
const lines = []
|
|
53
|
+
scanMilestoneLines(text, (l, m) => lines.push(m == null ? l : '▶ ' + m))
|
|
54
|
+
return lines.join('\n')
|
|
55
|
+
}
|
|
56
|
+
// 收尾用:正文里的里程碑行剔掉(它们进折叠区),只剩答案。
|
|
57
|
+
function stripMilestones(text) {
|
|
58
|
+
const lines = []
|
|
59
|
+
scanMilestoneLines(text, (l, m) => { if (m == null) lines.push(l) })
|
|
60
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
61
|
+
}
|
|
62
|
+
// 「最近在做」:一次工具调用 → 一句人话。只给文件名不给路径(群面零机器路径);命令内容不给
|
|
63
|
+
// (那是 agent 写的,可能带机器路径);插件按名字;认不出的返回 null,不拿框架里的工具名去凑数。
|
|
64
|
+
// codex 的工具名经 codexcode 映射后与 claude 同形(Bash / Write / MCP)。
|
|
65
|
+
const baseName = (p) => String(p || '').split(/[\\/]/).filter(Boolean).pop() || ''
|
|
66
|
+
function describeTool(name, input = {}) {
|
|
67
|
+
const n = String(name || '')
|
|
68
|
+
const inp = input && typeof input === 'object' ? input : {}
|
|
69
|
+
const file = baseName(inp.file_path || inp.path || inp.notebook_path)
|
|
70
|
+
const withFile = (verb) => (file ? `${verb} ${file}` : `${verb}文件`)
|
|
71
|
+
const mcp = /^mcp__(.+?)__/.exec(n)
|
|
72
|
+
if (mcp) return `用插件 ${mcp[1]}`
|
|
73
|
+
switch (n) {
|
|
74
|
+
case 'Read': return withFile('看')
|
|
75
|
+
case 'Edit': case 'MultiEdit': case 'NotebookEdit': return withFile('改')
|
|
76
|
+
case 'Write': return withFile('写')
|
|
77
|
+
case 'Delete': return withFile('删')
|
|
78
|
+
case 'Bash': return '跑命令'
|
|
79
|
+
case 'Glob': case 'Grep': case 'FileSearch': return '搜代码'
|
|
80
|
+
case 'WebFetch': case 'WebSearch': return '查网页'
|
|
81
|
+
case 'Task': case 'Agent': return '派子任务'
|
|
82
|
+
case 'MCP': return `用插件${inp.server ? ' ' + baseName(inp.server) : ''}`
|
|
83
|
+
default: return null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ── 确定性里程碑 ──────────────────────────────────────────────────────────────
|
|
87
|
+
// 「最近在做」只滚最近 3 条,回答的是「还活着吗」;这里回答「做到哪了」:从工具调用里只认**交付类**
|
|
88
|
+
// 动作(写 / 改文件 · 发到群 · 上画布 / 生图 / 预览 · git 提交推送 · 出 PDF),累积、去重、封顶,
|
|
89
|
+
// 与模型自己的 `>> ` 行并排进卡、收尾同进折叠区。只给文件名不给路径,临时目录里的脚本不算
|
|
90
|
+
// (那是它写给自己用的),命令内容不给 —— 群面零机器路径。一次 Bash 只记最重的一条。
|
|
91
|
+
const TMP_PATH_RE = /^(?:\/private)?\/tmp\/|^\/var\/folders\/|^\$TMPDIR|^%TEMP%|\\Temp\\/i
|
|
92
|
+
const unquote = (s) => String(s || '').trim().replace(/^(["'])(.*)\1$/, '$2')
|
|
93
|
+
const SH_ARG = '("[^"]+"|\'[^\']+\'|[^\\s;|&<>]+)'
|
|
94
|
+
function autoMilestone(name, input = {}) {
|
|
95
|
+
const n = String(name || '')
|
|
96
|
+
const inp = input && typeof input === 'object' ? input : {}
|
|
97
|
+
// 路径里带 shell 变量(`$OUT`)或命令替换 ⇒ 名字解不出来,只报动作不报名
|
|
98
|
+
const fileMs = (verb, raw) => { const p = unquote(raw); const f = baseName(p); return f && !TMP_PATH_RE.test(p) && !/[$`]/.test(p) ? `${verb} ${f}` : null }
|
|
99
|
+
const target = inp.file_path || inp.path || inp.notebook_path
|
|
100
|
+
switch (n) {
|
|
101
|
+
case 'Write': return fileMs('写了', target)
|
|
102
|
+
case 'Edit': case 'MultiEdit': case 'NotebookEdit': return fileMs('改了', target)
|
|
103
|
+
case 'Bash': break
|
|
104
|
+
default: return null
|
|
105
|
+
}
|
|
106
|
+
const cmd = String(inp.command || '')
|
|
107
|
+
const grab = (re) => { const m = re.exec(cmd); return m ? m[1] : '' }
|
|
108
|
+
let a
|
|
109
|
+
// 平台基础技能的命令名不写死(sbot feishu / sbot wecom …):里程碑只认动作名
|
|
110
|
+
if ((a = grab(new RegExp('\\bsbot\\s+[a-z]+\\s+send-file\\s+' + SH_ARG)))) return fileMs('已发文件', a) || '已发文件到群'
|
|
111
|
+
if ((a = grab(new RegExp('\\bsbot\\s+[a-z]+\\s+send-image\\s+' + SH_ARG)))) return fileMs('已发图片', a) || '已发图片到群'
|
|
112
|
+
if (/\bsbot\s+[a-z]+\s+send-notice\b/.test(cmd)) return '已发通知到群'
|
|
113
|
+
if (/\bsbot\s+[a-z]+\s+send-msg\b/.test(cmd)) return '已发消息到群'
|
|
114
|
+
if (/\bsbot\s+feishu\s+board-add-(?:image|diagram|nodes)\b/.test(cmd)) return '已上画布'
|
|
115
|
+
if (/\bsbot\s+imagen\s/.test(cmd) && /--out\b/.test(cmd)) return '已提交生图'
|
|
116
|
+
if (/\bsbot\s+preview\s+start\b/.test(cmd)) return '已开预览'
|
|
117
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?push\b/.test(cmd)) return '已推送远端'
|
|
118
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(cmd)) return '已提交代码'
|
|
119
|
+
if ((a = grab(new RegExp('--print-to-pdf=' + SH_ARG)))) return fileMs('已出 PDF', a) || '已出 PDF'
|
|
120
|
+
if (/--print-to-pdf\b/.test(cmd)) return '已出 PDF'
|
|
121
|
+
if ((a = grab(new RegExp('(?:^|[\\s;&|(])cat\\s*>{1,2}\\s*' + SH_ARG)))) return fileMs('写了', a)
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
// 最多留 3 条、去重、最新在最后(脉搏与 /status 共用同一个列表)。
|
|
125
|
+
function pushDoing(list, d) {
|
|
126
|
+
if (!d) return list
|
|
127
|
+
const i = list.indexOf(d)
|
|
128
|
+
if (i >= 0) list.splice(i, 1)
|
|
129
|
+
list.push(d)
|
|
130
|
+
while (list.length > 3) list.shift()
|
|
131
|
+
return list
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { T, humanDur, MILESTONE_MAX, milestonesOf, renderMilestones,
|
|
135
|
+
stripMilestones, describeTool, autoMilestone, pushDoing }
|
package/src/core/v2engine.js
CHANGED
|
@@ -15,6 +15,7 @@ const { createC2TurnProjection } = require('./c2turnprojection')
|
|
|
15
15
|
const { prepareC2RunnerInput } = require('./c2runnerinput')
|
|
16
16
|
const { continueC2Successor } = require('./c2successorbridge')
|
|
17
17
|
const { rejectC2Input } = require('./c2inputrejection')
|
|
18
|
+
const progress = require('./turnprogress')
|
|
18
19
|
const NO_REPLY = 'NO_REPLY'
|
|
19
20
|
const SYSTEM_APPENDIX = systemAppendix(NO_REPLY)
|
|
20
21
|
const V2_NORMAL_ROUTE_MISSING = Object.freeze(['complete durable v2 authority'])
|
|
@@ -47,8 +48,10 @@ const T = {
|
|
|
47
48
|
'/issue 一句话 记下问题反馈',
|
|
48
49
|
steer ? '直接发文字就是派任务;任务进行中再发的消息会补充进当前任务。'
|
|
49
50
|
: '直接发文字就是派任务;任务进行中再发的消息会排队,等这一轮结束再处理。'].join('\n'),
|
|
50
|
-
status: ({ model, project, state, queue, session }) => ['【本群状态】', `· 模型:${model}`,
|
|
51
|
-
`· 项目:${project}`, `· 状态:${state}`,
|
|
51
|
+
status: ({ model, project, state, doing = [], queue, session }) => ['【本群状态】', `· 模型:${model}`,
|
|
52
|
+
`· 项目:${project}`, `· 状态:${state}`,
|
|
53
|
+
...(doing.length ? [`· 最近在做:${doing.join(' · ')}`] : []),
|
|
54
|
+
`· 排队:${queue}`, `· 对话:${session}`].join('\n'),
|
|
52
55
|
freshSession: '✅ 已开始新的一段对话,之前聊的内容不再带着',
|
|
53
56
|
newBusy: '⚠ 手头还有任务在跑 —— 等它跑完,或先发 /stop 再发 /new',
|
|
54
57
|
quotaShow: (name, value) => `${name} 额度:${value}`,
|
|
@@ -68,6 +71,7 @@ const T = {
|
|
|
68
71
|
stopPending: '正在停止当前任务,这条消息没有处理,请稍后重发。',
|
|
69
72
|
stopUnavailable: '当前任务正在收尾,暂时无法停止。',
|
|
70
73
|
stoppedWithQueue: n => `已停止 — 连同排队的 ${n} 条一并取消`,
|
|
74
|
+
quotaFailure: 'Codex 订阅额度已用完,本次任务未完成。请恢复额度后重新发送;本次请求不会自动重试。',
|
|
71
75
|
cardFooter: ({ ms, tokens }) => {
|
|
72
76
|
const parts = [`耗时 ${Math.max(0, Math.round(ms / 1000))} 秒`]
|
|
73
77
|
const tokenLabel = n => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n)
|
|
@@ -351,11 +355,14 @@ async _durableV2Command(msg, cmd, rest) {
|
|
|
351
355
|
if (cmd === '/status') {
|
|
352
356
|
const rt = this.rts.get(msg.chatId)
|
|
353
357
|
const queued = this.durableV2Runtime.snapshot().state?.lanes?.queue?.[msg.chatId]?.queue?.length || 0
|
|
354
|
-
|
|
355
|
-
|
|
358
|
+
// "Running for long" is not "stuck": steps and the last activity tell them apart.
|
|
359
|
+
const busy = !!rt?.v2NormalRouteActive
|
|
360
|
+
const state = busy
|
|
361
|
+
? `运行中(已跑 ${humanDuration(Date.now() - (rt.v2StartedAt || Date.now()))}${rt.v2Steps ? ` · 已执行 ${rt.v2Steps} 步` : ''}${rt.v2AliveAt ? ` · 最近动静 ${humanDuration(Date.now() - rt.v2AliveAt)}前` : ''})`
|
|
356
362
|
: rt?.v2AdmissionPending ? '消息已登记,等待并发名额' : '空闲'
|
|
357
363
|
const project = String(group.workdir || '').split(/[\\/]/).filter(Boolean).pop() || '(未设)'
|
|
358
|
-
return reject('status', T.status({ model: label(name, provider), project, state,
|
|
364
|
+
return reject('status', T.status({ model: label(name, provider), project, state,
|
|
365
|
+
doing: busy && Array.isArray(rt.v2Doing) ? rt.v2Doing : [], queue: queued,
|
|
359
366
|
session: this.store.getSession(msg.chatId) ? '进行中(记着上下文)' : '未开始' }))
|
|
360
367
|
}
|
|
361
368
|
if (cmd === '/new') {
|
|
@@ -1258,6 +1265,13 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1258
1265
|
const generation = randomBytes(16).toString('hex')
|
|
1259
1266
|
rt.v2RunnerGeneration = generation
|
|
1260
1267
|
rt.v2StartedAt = Date.now()
|
|
1268
|
+
// Evidence that the turn is still moving, for /status and the card pulse;
|
|
1269
|
+
// every turn starts from zero.
|
|
1270
|
+
rt.v2Steps = 0
|
|
1271
|
+
rt.v2AliveAt = 0
|
|
1272
|
+
rt.v2Doing = []
|
|
1273
|
+
rt.v2AutoMilestones = []
|
|
1274
|
+
rt.v2CardQuotaFailure = false
|
|
1261
1275
|
rt.v2CardBinding = null
|
|
1262
1276
|
rt.v2StreamRequested = false
|
|
1263
1277
|
rt.v2StreamQuiescing = false
|
|
@@ -1271,6 +1285,13 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1271
1285
|
let accepted = false
|
|
1272
1286
|
let controller = null
|
|
1273
1287
|
let capTimer = null
|
|
1288
|
+
let ackTimer = null
|
|
1289
|
+
let ackDue = false
|
|
1290
|
+
let pulseTimer = null
|
|
1291
|
+
const stopProgressTimers = () => {
|
|
1292
|
+
if (ackTimer) { clearTimeout(ackTimer); ackTimer = null }
|
|
1293
|
+
if (pulseTimer) { clearInterval(pulseTimer); pulseTimer = null }
|
|
1294
|
+
}
|
|
1274
1295
|
try {
|
|
1275
1296
|
capability = authority.beginRunnerStart({ receiptId, chatId, turnToken,
|
|
1276
1297
|
generation, verifyGeneration })
|
|
@@ -1346,19 +1367,28 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1346
1367
|
rt.v2RunnerNotStarted = false
|
|
1347
1368
|
let streamChain = Promise.resolve()
|
|
1348
1369
|
let streamFailure = null
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
if (
|
|
1357
|
-
|
|
1370
|
+
// Card progress (same mechanism and wording as the legacy engine): deterministic
|
|
1371
|
+
// milestones and "recently doing" from tool calls, an early "received" card
|
|
1372
|
+
// and a pulse line. All of it is process-local card text; the card itself is
|
|
1373
|
+
// still opened through the durable terminal path below.
|
|
1374
|
+
const autoMs = []
|
|
1375
|
+
rt.v2AutoMilestones = autoMs
|
|
1376
|
+
const pushAuto = m => {
|
|
1377
|
+
if (m && !autoMs.includes(m) && autoMs.length < progress.MILESTONE_MAX) autoMs.push(m)
|
|
1378
|
+
}
|
|
1379
|
+
const autoBlock = () => (autoMs.length ? autoMs.map(m => '▶ ' + m).join('\n') + '\n\n' : '')
|
|
1380
|
+
const pulseFrom = Date.now()
|
|
1381
|
+
let cardTouchedAt = 0
|
|
1382
|
+
const progressClosed = () => rt.v2RunnerGeneration !== generation ||
|
|
1383
|
+
rt.v2TerminalStarted || controller.signal.aborted || rt.v2CancelControl ||
|
|
1384
|
+
rt.v2StreamQuiescing || !!authority.scopeState('stopIntent', chatId)
|
|
1385
|
+
const enqueueCardText = text => {
|
|
1358
1386
|
rt.v2StreamRequested = true
|
|
1359
1387
|
streamChain = streamChain.then(async () => {
|
|
1360
1388
|
if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
|
|
1361
|
-
|
|
1389
|
+
// An earlier frame or pulse may still await its platform response;
|
|
1390
|
+
// only a closed authority ends the stream.
|
|
1391
|
+
if (authority.closed) throw new Error('C2 v2 stream authority closed')
|
|
1362
1392
|
if (controller.signal.aborted || rt.v2CancelControl ||
|
|
1363
1393
|
authority.scopeState('stopIntent', chatId)) return
|
|
1364
1394
|
if (!rt.v2CardBinding) {
|
|
@@ -1368,8 +1398,10 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1368
1398
|
generation, verifyGeneration,
|
|
1369
1399
|
applyLocalEffect: this.durableV2ApplyLocalEffect,
|
|
1370
1400
|
assertWriterLock: this.durableV2AssertWriterLock,
|
|
1371
|
-
|
|
1372
|
-
|
|
1401
|
+
// An early card may end as the supported quota failure; its final
|
|
1402
|
+
// text then uses the same fixed notice as an unopened card.
|
|
1403
|
+
redactText: value => redact(rt.v2CardQuotaFailure === true
|
|
1404
|
+
? T.quotaFailure : value, { pii: job.group?.redactPii !== false }),
|
|
1373
1405
|
sendPlainReply: typeof job.platform.replyOnce === 'function'
|
|
1374
1406
|
? (target, value) => job.platform.replyOnce(target.replyCtx, value)
|
|
1375
1407
|
: null })
|
|
@@ -1385,6 +1417,82 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1385
1417
|
try { controller.abort() } catch {}
|
|
1386
1418
|
})
|
|
1387
1419
|
}
|
|
1420
|
+
const enqueueFrame = event => {
|
|
1421
|
+
if (event.type !== 'assistant') return
|
|
1422
|
+
// /status and the pulse read these; a late event from an older
|
|
1423
|
+
// generation must not write progress into this turn.
|
|
1424
|
+
if (rt.v2RunnerGeneration === generation) {
|
|
1425
|
+
rt.v2AliveAt = Date.now()
|
|
1426
|
+
if (event.tools && event.tools.length) {
|
|
1427
|
+
rt.v2Steps = (rt.v2Steps || 0) + event.tools.length
|
|
1428
|
+
for (const tool of event.tools) {
|
|
1429
|
+
progress.pushDoing(rt.v2Doing, progress.describeTool(tool.name, tool.input))
|
|
1430
|
+
pushAuto(progress.autoMilestone(tool.name, tool.input))
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (!this.durableV2StreamFrames) return
|
|
1435
|
+
if (!event.text || controller.signal.aborted || rt.v2CancelControl ||
|
|
1436
|
+
rt.v2StreamQuiescing ||
|
|
1437
|
+
authority.scopeState('stopIntent', chatId)) return
|
|
1438
|
+
const buffer = projection.streamSnapshot().bufferText
|
|
1439
|
+
const trimmed = buffer.trim()
|
|
1440
|
+
if (!trimmed || NO_REPLY.startsWith(trimmed)) return
|
|
1441
|
+
cardTouchedAt = Date.now()
|
|
1442
|
+
enqueueCardText(autoBlock() + progress.renderMilestones(buffer))
|
|
1443
|
+
}
|
|
1444
|
+
const cardsVisible = this.durableV2StreamFrames && !receipt.quiet &&
|
|
1445
|
+
typeof job.platform?.createStreamingCard === 'function'
|
|
1446
|
+
// No visible output after ackDelayMs: open the card that would be opened
|
|
1447
|
+
// anyway and fill it with "received"; the answer replaces it in place and
|
|
1448
|
+
// a turn that ends silent recalls it at the terminal.
|
|
1449
|
+
const ackMs = this.cfg.agent?.ackDelayMs == null ? 25_000 : Number(this.cfg.agent.ackDelayMs)
|
|
1450
|
+
// No card before the runner is accepted. A runner accepted after the
|
|
1451
|
+
// delay (Codex holds its events until its turn start returns) gets the
|
|
1452
|
+
// card as soon as it is accepted.
|
|
1453
|
+
const tryAck = () => {
|
|
1454
|
+
if (!accepted) { ackDue = true; return }
|
|
1455
|
+
if (progressClosed() || rt.v2CardBinding || rt.v2StreamRequested ||
|
|
1456
|
+
projection.streamSnapshot().bufferText.trim()) return
|
|
1457
|
+
cardTouchedAt = Date.now()
|
|
1458
|
+
enqueueCardText(progress.T.ackWorking)
|
|
1459
|
+
}
|
|
1460
|
+
const armAck = delayMs => {
|
|
1461
|
+
ackTimer = setTimeout(() => { ackTimer = null; tryAck() }, delayMs)
|
|
1462
|
+
if (ackTimer.unref) ackTimer.unref()
|
|
1463
|
+
}
|
|
1464
|
+
if (cardsVisible && ackMs > 0) armAck(ackMs)
|
|
1465
|
+
// Pulse: only rewrites the open card. Every PULSE_MS for the first
|
|
1466
|
+
// PULSE_SLOW_AFTER_MS, then every PULSE_SLOW_MS; "recent activity" is only
|
|
1467
|
+
// added after PULSE_STALE_MS without an assistant event.
|
|
1468
|
+
const agentCfg = this.cfg.agent || {}
|
|
1469
|
+
const PULSE_MS = agentCfg.cardPulseMs == null ? 15_000 : Number(agentCfg.cardPulseMs)
|
|
1470
|
+
const PULSE_SLOW_MS = agentCfg.cardPulseSlowMs == null ? 60_000 : Number(agentCfg.cardPulseSlowMs)
|
|
1471
|
+
const PULSE_SLOW_AFTER_MS = agentCfg.cardPulseSlowAfterMs == null ? 10 * 60 * 1000 : Number(agentCfg.cardPulseSlowAfterMs)
|
|
1472
|
+
const PULSE_STALE_MS = agentCfg.cardPulseStaleMs == null ? 2 * 60 * 1000 : Number(agentCfg.cardPulseStaleMs)
|
|
1473
|
+
if (cardsVisible && PULSE_MS > 0) {
|
|
1474
|
+
const pulseEvery = () => (Date.now() - pulseFrom >= PULSE_SLOW_AFTER_MS
|
|
1475
|
+
? Math.max(PULSE_MS, PULSE_SLOW_MS) : PULSE_MS)
|
|
1476
|
+
pulseTimer = setInterval(() => {
|
|
1477
|
+
const binding = rt.v2CardBinding
|
|
1478
|
+
if (progressClosed() || !binding?.opened || binding.generation !== generation) return
|
|
1479
|
+
if (Date.now() - cardTouchedAt < pulseEvery()) return
|
|
1480
|
+
cardTouchedAt = Date.now()
|
|
1481
|
+
const sinceAlive = Date.now() - (rt.v2AliveAt || pulseFrom)
|
|
1482
|
+
const stale = sinceAlive >= PULSE_STALE_MS ? progress.humanDur(sinceAlive) : ''
|
|
1483
|
+
// Same gate as the stream frame: text that may still become NO_REPLY
|
|
1484
|
+
// never reaches the card.
|
|
1485
|
+
const buffer = projection.streamSnapshot().bufferText
|
|
1486
|
+
const sofar = buffer.trim()
|
|
1487
|
+
const body = autoBlock() + (sofar && !NO_REPLY.startsWith(sofar)
|
|
1488
|
+
? progress.renderMilestones(buffer) + '\n\n' : '')
|
|
1489
|
+
try {
|
|
1490
|
+
binding.path.update(body + progress.T.pulse(
|
|
1491
|
+
progress.humanDur(Date.now() - pulseFrom), rt.v2Doing, stale))
|
|
1492
|
+
} catch { /* a closed card never affects the turn */ }
|
|
1493
|
+
}, PULSE_MS)
|
|
1494
|
+
if (pulseTimer.unref) pulseTimer.unref()
|
|
1495
|
+
}
|
|
1388
1496
|
const maxMins = Number(this.cfg.agent.maxTurnTimeMins) || 0
|
|
1389
1497
|
const timeoutMs = maxMins > 0 ? maxMins * 60_000 : 0
|
|
1390
1498
|
const timeoutDeadlineAt = timeoutMs > 0 ? Date.now() + timeoutMs : null
|
|
@@ -1416,6 +1524,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1416
1524
|
onTurnAccepted: value => {
|
|
1417
1525
|
const ack = capability.onTurnAccepted(value)
|
|
1418
1526
|
accepted = true
|
|
1527
|
+
if (ackDue) { ackDue = false; armAck(0) }
|
|
1419
1528
|
return ack
|
|
1420
1529
|
},
|
|
1421
1530
|
onSpawnFailed: value => capability.onSpawnFailed(value),
|
|
@@ -1440,6 +1549,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1440
1549
|
let result
|
|
1441
1550
|
try {
|
|
1442
1551
|
result = await running
|
|
1552
|
+
stopProgressTimers()
|
|
1443
1553
|
rt.v2SteerClosing = true
|
|
1444
1554
|
rt.v2SteerControl = null
|
|
1445
1555
|
if (rt.v2SteerDrain) await rt.v2SteerDrain
|
|
@@ -1462,6 +1572,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1462
1572
|
}
|
|
1463
1573
|
finally {
|
|
1464
1574
|
if (capTimer) clearTimeout(capTimer)
|
|
1575
|
+
stopProgressTimers()
|
|
1465
1576
|
if (rt.v2Controller === controller) rt.v2Controller = null
|
|
1466
1577
|
}
|
|
1467
1578
|
if (authority.blocked && rt.v2RunnerStopVerified !== true) {
|
|
@@ -1512,6 +1623,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1512
1623
|
return { effectId, accepted: true, outputHeld: true }
|
|
1513
1624
|
} catch (error) {
|
|
1514
1625
|
if (capTimer) clearTimeout(capTimer)
|
|
1626
|
+
stopProgressTimers()
|
|
1515
1627
|
if (controller && rt.v2Controller === controller) rt.v2Controller = null
|
|
1516
1628
|
const proofId = `sbot-c2-runner-not-started-v1:${receiptId}:${generation}`
|
|
1517
1629
|
const eventId = `sbot-c2-runner-not-started-commit-v1:${receiptId}:${turnToken}:${generation}`
|
|
@@ -1620,7 +1732,7 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1620
1732
|
verifyGeneration, applyLocalEffect: this.durableV2ApplyLocalEffect,
|
|
1621
1733
|
assertWriterLock: this.durableV2AssertWriterLock,
|
|
1622
1734
|
redactText: text => redact(quotaFailure
|
|
1623
|
-
?
|
|
1735
|
+
? T.quotaFailure
|
|
1624
1736
|
: text, { pii: job.group?.redactPii !== false }),
|
|
1625
1737
|
sendPlainReply: typeof job.platform.replyOnce === 'function'
|
|
1626
1738
|
? (target, text) => job.platform.replyOnce(target.replyCtx, text)
|
|
@@ -1631,7 +1743,15 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1631
1743
|
for (const text of updates) path.update(text)
|
|
1632
1744
|
const footer = T.cardFooter({ ms: Math.max(0, Date.now() - rt.v2StartedAt),
|
|
1633
1745
|
tokens: rt.v2UsageDecision?.tokens || null })
|
|
1634
|
-
|
|
1746
|
+
// Milestones from tool calls first, then the model's own `>> ` lines;
|
|
1747
|
+
// the card shows them in its "process" fold (display only).
|
|
1748
|
+
rt.v2CardQuotaFailure = !!quotaFailure
|
|
1749
|
+
const redactMilestone = value => redact(value, { pii: job.group?.redactPii !== false })
|
|
1750
|
+
const milestones = [...(rt.v2AutoMilestones || []),
|
|
1751
|
+
...progress.milestonesOf(payload.bufferText)]
|
|
1752
|
+
.filter((m, i, a) => a.indexOf(m) === i).map(redactMilestone)
|
|
1753
|
+
.filter(m => typeof m === 'string' && m.trim())
|
|
1754
|
+
const completed = await path.complete({ payload, replyText, paused, footer, milestones })
|
|
1635
1755
|
const after = authority.snapshot()
|
|
1636
1756
|
const queue = after.state?.lanes?.queue?.[chatId]?.queue
|
|
1637
1757
|
let successor = null
|
|
@@ -1668,6 +1788,8 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1668
1788
|
rt.v2TerminalProjection = null
|
|
1669
1789
|
rt.v2UsageDecision = null
|
|
1670
1790
|
rt.v2StartedAt = null
|
|
1791
|
+
rt.v2AutoMilestones = []
|
|
1792
|
+
rt.v2CardQuotaFailure = false
|
|
1671
1793
|
rt.v2CardBinding = null
|
|
1672
1794
|
rt.v2StreamRequested = false
|
|
1673
1795
|
rt.v2TimeoutProof = null
|
package/src/ui/console.html
CHANGED
|
@@ -2580,7 +2580,7 @@ function cronFullHtml(text){
|
|
|
2580
2580
|
}).join('');
|
|
2581
2581
|
}
|
|
2582
2582
|
const RUNTIME_UI_TEXT={
|
|
2583
|
-
"summary": "
|
|
2583
|
+
"summary": "当前支持多群消息、流式卡片回复、忙时跟进、停止任务、状态查看、认证管理、绑定新群、空闲群的模型(Codex / Claude Code)和项目目录切换,以及后台运行时的软件升级。新会话、解除绑定、控制台插话、定时任务、生图、画布与预览暂不可用。",
|
|
2584
2584
|
"groups": "可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务在「概览」。群里发 /model 也能切换模型。",
|
|
2585
2585
|
"skills": "当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。",
|
|
2586
2586
|
"cron": "当前不执行定时任务;已有定义仅供查看。",
|
|
@@ -3485,21 +3485,27 @@ function renderSettings(){
|
|
|
3485
3485
|
<div class="hint">代理第一次填,新的对话轮次就生效;改已有的地址要重启后台服务(sintbot service restart)。</div></div>
|
|
3486
3486
|
</div>`;
|
|
3487
3487
|
const updateJob=upd&&upd.job;
|
|
3488
|
-
|
|
3489
|
-
const
|
|
3488
|
+
// 1.4.x 多群服务的升级器带 mode:后台托管才能一键升级,前台与开发版只给说明;不在群里播报。
|
|
3489
|
+
const updMode=upd&&upd.mode||'';
|
|
3490
|
+
const updateBusy=!!(updateJob&&['queued','installing','switching','restarting','restart-needed'].includes(updateJob.state));
|
|
3491
|
+
const updCanInstall=!!(upd&&(updMode?upd.upgradeSupported:true));
|
|
3492
|
+
const updateText=!upd?'后台版本检查不可用':updateJob&&updateJob.state!=='done'||updateJob&&!upd.available&&updMode?esc(updateJob.message||updateJob.state):upd.available?`检测到新版本 v${esc(upd.latestVersion)} · 当前 v${esc(upd.currentVersion)}`:updMode==='dev'&&upd.latestVersion?`最新正式版 v${esc(upd.latestVersion)} · 当前开发版 v${esc(upd.currentVersion)}`:upd.checkedAt&&upd.latestVersion?`当前为最新版本 v${esc(upd.currentVersion)}`:upd.checkedAt?'暂时无法确认最新版本':'尚未检查新版';
|
|
3493
|
+
const updHint=updMode==='managed'?'只安装检测到的稳定版本:等所有群空闲、记录结清后装到单独的目录,再切换后台服务;新版本没在 90 秒内连上全部群会自动换回。旧版本保留用于回滚,不提权,不在群里播报。'
|
|
3494
|
+
:updMode==='foreground'?'前台运行不能在控制台升级:在运行 SintBot 的终端按 Ctrl-C 等待退出,重新运行一行安装命令 curl -fsSL https://sintbot.com/install.sh | bash,再用原来的启动命令启动。'
|
|
3495
|
+
:updMode==='dev'?'开发版不在控制台升级,请用部署脚本。':'只安装检测到的稳定版本;所有群任务与队列清空后才会安装。需要可写的全局安装目录,不会提权;后台服务会自动重启,前台运行需手动重启。';
|
|
3490
3496
|
const updateCard=`<div class="card" style="grid-column:1/-1" id="updateCard"><div class="lbl">软件更新</div>
|
|
3491
3497
|
<div id="updateStatus" class="hint" style="margin:0 0 12px;color:var(--ink)">${updateText}</div>
|
|
3492
3498
|
${upd&&upd.available?`<div class="hint" style="margin:-4px 0 12px">版本差异与发布说明:<a href="https://github.com/sintlab/sintbot/releases/tag/v${encodeURIComponent(upd.latestVersion)}" target="_blank" rel="noopener" style="color:var(--ember-soft)">查看 v${esc(upd.latestVersion)} 发布说明</a></div>`:''}
|
|
3493
3499
|
<div class="inline" style="gap:14px;flex-wrap:wrap;margin-bottom:12px">
|
|
3494
3500
|
<span class="dim">自动检查稳定版(启动时及每天)</span>
|
|
3495
|
-
|
|
3501
|
+
${upd&&upd.announceSupported===false?'':`<label class="inline" style="gap:6px;cursor:pointer"><input type="checkbox" id="updAnnounce" ${upd&&upd.announce!==false?'checked':''} style="width:auto"/> <span class="dim">升级后向群里播报(记住选择)</span></label>`}
|
|
3496
3502
|
</div>
|
|
3497
3503
|
<div class="inline" style="gap:8px;flex-wrap:wrap">
|
|
3498
3504
|
<button class="btn" id="updCheck" ${!upd?'disabled':''}>检查新版</button>
|
|
3499
|
-
|
|
3505
|
+
${updCanInstall?`<button class="btn primary" id="updInstall" ${!upd||!upd.available||updateBusy?'disabled':''}>现在升级</button>`:''}
|
|
3500
3506
|
<button class="btn" id="updIgnore" ${!upd||!upd.available?'disabled':''}>忽略此版本</button>
|
|
3501
3507
|
</div>
|
|
3502
|
-
<div class="hint" style="margin-top:10px"
|
|
3508
|
+
<div class="hint" style="margin-top:10px">${updHint}</div>
|
|
3503
3509
|
<div class="update-history"><div class="subhead">版本历史</div>
|
|
3504
3510
|
<div id="verhistBox"><div class="empty">加载中…</div></div></div>
|
|
3505
3511
|
</div>`;
|
|
@@ -3547,18 +3553,19 @@ function renderSettings(){
|
|
|
3547
3553
|
}catch(e){toast(e.message,'err');b.disabled=false;}
|
|
3548
3554
|
};
|
|
3549
3555
|
const saveUpdatePrefs=async(patch)=>{try{S=await api('/api/ui/update-settings','POST',patch);renderSettings();}catch(e){toast(e.message,'err');}};
|
|
3550
|
-
$('#updAnnounce').onchange=()=>saveUpdatePrefs({announce:$('#updAnnounce').checked});
|
|
3556
|
+
if($('#updAnnounce'))$('#updAnnounce').onchange=()=>saveUpdatePrefs({announce:$('#updAnnounce').checked});
|
|
3551
3557
|
$('#updCheck').onclick=async()=>{const b=$('#updCheck');b.disabled=true;b.textContent='检查中…';
|
|
3552
3558
|
try{await api('/api/update/check','POST',{});S=await api('/api/state');await refreshInbox();renderSettings();renderOverview();toast('版本检查完成','ok');}
|
|
3553
3559
|
catch(e){toast(e.message,'err');b.disabled=false;b.textContent='检查新版';}};
|
|
3554
|
-
$('#updInstall').onclick=async()=>{const u=S.update||{},version=u.latestVersion;if(!version)return;
|
|
3555
|
-
const announce=$('#updAnnounce').checked;
|
|
3556
|
-
|
|
3560
|
+
if($('#updInstall'))$('#updInstall').onclick=async()=>{const u=S.update||{},version=u.latestVersion;if(!version)return;
|
|
3561
|
+
const announce=$('#updAnnounce')?$('#updAnnounce').checked:false;
|
|
3562
|
+
const what=u.mode==='managed'?'等所有群空闲、记录结清后装到单独的目录,再切换后台服务;切换期间约一分钟不处理新消息,新版本没连上全部群会自动换回。':`${announce?'安装完成后会向未暂停的群播报;':'本次不向群播报。'}所有群任务结束后才开始安装,后台服务会重启。`;
|
|
3563
|
+
if(!confirm(`将从 v${u.currentVersion} 升级到 v${version}。${what}继续?`))return;
|
|
3557
3564
|
const b=$('#updInstall');b.disabled=true;
|
|
3558
|
-
try{const r=await api('/api/update/start','POST',{version,announce});S.update={...r,announce};await refreshInbox();renderSettings();renderOverview();toast('更新已排队,群任务空闲后开始','ok');}
|
|
3565
|
+
try{const r=await api('/api/update/start','POST',{version,announce});S.update={...r,announce};await refreshInbox();renderSettings();renderOverview();toast(u.mode==='managed'?'升级已排队,所有群空闲后开始':'更新已排队,群任务空闲后开始','ok');}
|
|
3559
3566
|
catch(e){toast(e.message,'err');b.disabled=false;}};
|
|
3560
3567
|
$('#updIgnore').onclick=async()=>{const version=(S.update||{}).latestVersion;if(!version)return;
|
|
3561
|
-
try{const r=await api('/api/update/ignore','POST',{version});S.update={...r,announce:$('#updAnnounce').checked};await refreshInbox();renderSettings();renderOverview();toast('已忽略此版本','ok');}
|
|
3568
|
+
try{const r=await api('/api/update/ignore','POST',{version});S.update={...r,announce:$('#updAnnounce')?$('#updAnnounce').checked:false};await refreshInbox();renderSettings();renderOverview();toast('已忽略此版本','ok');}
|
|
3562
3569
|
catch(e){toast(e.message,'err');}};
|
|
3563
3570
|
// 只开着控制台的守护进程(还没接平台):第一次接上平台时说一句接下来会发生什么(服务托管的会自己重启连上)
|
|
3564
3571
|
{const pl=S.platforms||{};if(S.setupMode&&(pl.feishu||pl.slack||pl.wecom)&&!window._sbRestartToast){window._sbRestartToast=1;toast(S.setupMode.supervised?'已接上聊天平台 —— 后台服务几秒后自动重启并连上':'已接上聊天平台 —— 在终端里重启 sintbot start --ui 连上','ok');}}
|
|
@@ -4032,7 +4039,7 @@ updateThemeBtn();
|
|
|
4032
4039
|
const I18N_EN=[
|
|
4033
4040
|
["需要处理", "Needs attention"],
|
|
4034
4041
|
["当前可用操作", "Available controls"],
|
|
4035
|
-
["
|
|
4042
|
+
["当前支持多群消息、流式卡片回复、忙时跟进、停止任务、状态查看、认证管理、绑定新群、空闲群的模型(Codex / Claude Code)和项目目录切换,以及后台运行时的软件升级。新会话、解除绑定、控制台插话、定时任务、生图、画布与预览暂不可用。", "Available: group messages, streaming reply cards, busy-turn followups, stopping tasks, live status, authentication, binding new groups, switching the model (Codex / Claude Code) or project of an idle group, and software upgrades in a background run. New sessions, unbinding, console messages, schedules, image generation, canvas and previews are unavailable."],
|
|
4036
4043
|
["可绑定新群,修改模型、项目目录、权限、数据输出和忙时跟进;该群须没有进行中的任务和未结记录。切模型或项目会开始新的一段对话。停止任务在「概览」。群里发 /model 也能切换模型。", "Bind new groups and change the model, project folder, permissions, data display and busy-turn followups of a group with no running task or unsettled record. Changing the model or project starts a new conversation. Stop tasks in Overview. Members can also switch models with /model in the group."],
|
|
4037
4044
|
["当前支持飞书文字消息、忙时跟进、任务回复和停止。定时任务、生图、群消息管理、画布与预览暂未接入。", "Feishu text messages, busy-turn followups, task replies and stopping are supported. Schedules, image generation, group message management, canvas and previews are unavailable."],
|
|
4038
4045
|
["当前不执行定时任务;已有定义仅供查看。", "Schedules are not executed. Existing definitions are shown for reference only."],
|
|
@@ -4765,6 +4772,11 @@ const I18N_EN=[
|
|
|
4765
4772
|
["更新已排队,群任务空闲后开始","Update queued; installation will start when all group tasks are idle"],
|
|
4766
4773
|
["已忽略此版本","This version was ignored"],
|
|
4767
4774
|
["版本检查完成","Version check complete"],
|
|
4775
|
+
["暂时无法确认最新版本","The latest version could not be confirmed yet"],
|
|
4776
|
+
["升级已排队,所有群空闲后开始","Upgrade queued; it starts when every group is idle"],
|
|
4777
|
+
["只安装检测到的稳定版本:等所有群空闲、记录结清后装到单独的目录,再切换后台服务;新版本没在 90 秒内连上全部群会自动换回。旧版本保留用于回滚,不提权,不在群里播报。","Installs only the detected stable version: once every group is idle and settled, it goes into its own folder and the background service switches to it; if the new version does not connect every group within 90 seconds, the previous one is restored. The previous version is kept for rollback. No elevated privileges and no group announcements."],
|
|
4778
|
+
["前台运行不能在控制台升级:在运行 SintBot 的终端按 Ctrl-C 等待退出,重新运行一行安装命令 curl -fsSL https://sintbot.com/install.sh | bash,再用原来的启动命令启动。","A foreground run cannot upgrade from the console: press Ctrl-C in the terminal running SintBot and wait for it to exit, rerun the one-line installer curl -fsSL https://sintbot.com/install.sh | bash, then start with your usual command."],
|
|
4779
|
+
["开发版不在控制台升级,请用部署脚本。","Development builds are not upgraded from the console; use the deployment scripts."],
|
|
4768
4780
|
["访问地址正在切换;页面即将重载","Changing the access address; this page will reload"],
|
|
4769
4781
|
["访问设置已保存","Access settings saved"],
|
|
4770
4782
|
["开启局域网访问前要设置至少 6 位访问密码","Set an access password of at least 6 characters before enabling LAN access"],
|
|
@@ -5380,16 +5392,25 @@ const I18N_EN_RX=[
|
|
|
5380
5392
|
[/^登录失败 \(HTTP (\d+)\)$/,"Sign-in failed (HTTP $1)"],
|
|
5381
5393
|
[/^检测到新版本 v([\d.]+) · 当前 v([\d.]+)$/,"New version v$1 · current v$2"],
|
|
5382
5394
|
[/^当前为最新版本 v([\d.]+)$/,"v$1 is the latest version"],
|
|
5395
|
+
[/^最新正式版 v([\d.]+) · 当前开发版 v(\S+)$/,"Latest release v$1 · this development build v$2"],
|
|
5383
5396
|
[/^等待所有群任务结束后更新$/,"Waiting for all group tasks to finish"],
|
|
5384
5397
|
[/^正在检查安装位置并安装指定版本$/,"Checking the install location and installing the selected version"],
|
|
5385
5398
|
[/^已安装,正在重启后台服务$/,"Installed; restarting the background service"],
|
|
5386
5399
|
[/^已安装新版;后台服务未能自动重启,请手动重启$/,"The new version is installed, but the background service did not restart; restart it manually"],
|
|
5387
5400
|
[/^已安装新版;当前不是受托管服务,请手动重启 SintBot$/,"The new version is installed; restart SintBot manually"],
|
|
5388
5401
|
[/^更新完成并已重启$/,"Update completed and restarted"],
|
|
5402
|
+
[/^等所有群空闲、记录结清后升级$/,"Waiting until every group is idle and settled"],
|
|
5403
|
+
[/^正在安装 v([\d.]+)$/,"Installing v$1"],
|
|
5404
|
+
[/^v([\d.]+) 已安装,等所有群空闲后切换$/,"v$1 is installed; switching when every group is idle"],
|
|
5405
|
+
[/^正在切换到 v([\d.]+);期间的消息会提示稍后重发$/,"Switching to v$1; messages meanwhile are asked to be resent later"],
|
|
5406
|
+
[/^已升级到 v([\d.]+)$/,"Upgraded to v$1"],
|
|
5407
|
+
[/^没有切换到 v([\d.]+):([\s\S]*);已换回 v([\d.]+)$/,"Did not switch to v$1 ($2); v$3 was restored"],
|
|
5408
|
+
[/^等待群空闲超时;可在空闲后重新发起$/,"Timed out waiting for idle groups; retry when they are idle"],
|
|
5409
|
+
[/^有暂停的群,升级已取消;先处理暂停的群$/,"A group is paused, so the upgrade was cancelled; handle the paused group first"],
|
|
5389
5410
|
[/^更新后未能自动重启,请手动重启服务$/,"The update could not restart automatically; restart the service manually"],
|
|
5390
5411
|
[/^等待群任务结束超时;可在任务空闲后重新发起$/,"Timed out waiting for group tasks; retry when they are idle"],
|
|
5391
5412
|
[/^当前运行的是开发目录或非全局副本;请在终端手动更新$/,"This is a development or non-global install; update it from a terminal"],
|
|
5392
|
-
[/^将从 v([\d.]+) 升级到 v([\d.]+)。([\s\S]*)$/,(a,from,to,what)=>`Upgrade from v${from} to v${to}. ${what.replace('所有群任务结束后才开始安装,后台服务会重启。','Installation waits until all group tasks finish, then restarts the background service.').replace('本次不向群播报。','This update will not be announced in groups.').replace('安装完成后会向未暂停的群播报;','After installation, it will be announced in active groups;')}`],
|
|
5413
|
+
[/^将从 v([\d.]+) 升级到 v([\d.]+)。([\s\S]*)$/,(a,from,to,what)=>`Upgrade from v${from} to v${to}. ${what.replace('等所有群空闲、记录结清后装到单独的目录,再切换后台服务;切换期间约一分钟不处理新消息,新版本没连上全部群会自动换回。','Once every group is idle and settled, it installs into its own folder and switches the background service; new messages wait about a minute during the switch, and the previous version is restored if the new one does not connect every group. ').replace('所有群任务结束后才开始安装,后台服务会重启。','Installation waits until all group tasks finish, then restarts the background service.').replace('本次不向群播报。','This update will not be announced in groups.').replace('安装完成后会向未暂停的群播报;','After installation, it will be announced in active groups;')}`],
|
|
5393
5414
|
];
|
|
5394
5415
|
/* I18N_DICT_END */
|
|
5395
5416
|
|
|
@@ -5590,7 +5611,7 @@ async function fetchCodexModels(){if(CX_MODELS)return CX_MODELS;try{CX_MODELS=aw
|
|
|
5590
5611
|
function cxDefEffortLabel(model){const m=CX_MODELS&&(CX_MODELS.models||[]).find(x=>x.model===model);return '默认'+(m&&m.defEffort?'('+m.defEffort+')':'');}
|
|
5591
5612
|
/* 有 codex provider 时启动即预热模型列表(server 缓存 10min)→ 卡片默认档标注补齐 + 面板秒开 */
|
|
5592
5613
|
function warmCodexModels(){if(!CX_MODELS&&S&&(S.providers||[]).some(x=>x.runner==='codex'&&!x.hidden))fetchCodexModels().then(()=>renderProviders());}
|
|
5593
|
-
const RUNTIME_DISABLED_SELECTORS='[data-osay],[data-onew],.gpv,[data-rm],[data-gcfg],[data-cronnew],[data-cronadd],[data-crontgl],[data-cronrm],[data-reposave],[data-repotest],[data-iconuse],[data-iconsys],[data-iconstyle],[data-stylecollect],#mNewChat,#addModel,.pcard [data-act="edit"],.pcard [data-act="test"],#amSave,#amDel,#mNewGo,#pgSend,#pgNew,#fSave,#slSave,#wSave,#bSave,#styleNew,[data-styleedit],[data-styledup],[data-styledel],#nsSave,#nsExtract,#clSave
|
|
5614
|
+
const RUNTIME_DISABLED_SELECTORS='[data-osay],[data-onew],.gpv,[data-rm],[data-gcfg],[data-cronnew],[data-cronadd],[data-crontgl],[data-cronrm],[data-reposave],[data-repotest],[data-iconuse],[data-iconsys],[data-iconstyle],[data-stylecollect],#mNewChat,#addModel,.pcard [data-act="edit"],.pcard [data-act="test"],#amSave,#amDel,#mNewGo,#pgSend,#pgNew,#fSave,#slSave,#wSave,#bSave,#styleNew,[data-styleedit],[data-styledup],[data-styledel],#nsSave,#nsExtract,#clSave,[data-pvstop],[data-lvkill],[data-lvkillall]';
|
|
5594
5615
|
function disableRuntimeControl(el,reason){
|
|
5595
5616
|
el.disabled=true;el.title=trText(reason);el.dataset.runtimeDisabled='1';
|
|
5596
5617
|
el.setAttribute('aria-disabled','true');
|