sintbot 1.4.11 → 1.4.13
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 +54 -0
- package/CHANGELOG.json +54 -0
- package/bin/sintbot-v2.js +12 -3
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +14 -4
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +12 -4
- package/install.sh +1 -1
- package/package.json +7 -1
- package/src/agent/claudecode.js +6 -0
- package/src/agent/codexapp.js +5 -1
- package/src/agent/privatecore.js +1 -1
- package/src/agent/privatecore.wasm +0 -0
- package/src/agent/runner.js +3 -1
- package/src/c2console.js +85 -15
- package/src/c2updater.js +13 -5
- package/src/consolesay.js +6 -6
- package/src/consoletrials.js +43 -0
- package/src/core/c2cardterminalpath.js +2 -1
- package/src/core/c2durablejournal.js +10 -0
- package/src/core/v2engine.js +70 -19
- package/src/feishucli.js +13 -1
- package/src/modelcontext.js +13 -0
- package/src/platform/feishu.js +29 -9
- package/src/recordedanswers.js +27 -0
- package/src/runtimefacts.js +11 -0
- package/src/ui/console.html +49 -29
- package/src/ui/liveactivity-client.js +276 -0
- package/src/ui/liveactivity.js +194 -0
- package/src/ui/serverbase.js +41 -14
package/src/c2console.js
CHANGED
|
@@ -8,7 +8,7 @@ const { createDurableConsoleSay } = require('./consolesay')
|
|
|
8
8
|
|
|
9
9
|
// A host facade over existing v2 authorities, not a second execution engine.
|
|
10
10
|
// The allowlist applies after the existing console's authentication boundary.
|
|
11
|
-
const READ_PATHS = ['/api/state', '/api/events', '/api/inbox', '/api/activity',
|
|
11
|
+
const READ_PATHS = ['/api/state', '/api/events', '/api/inbox', '/api/activity', '/api/activity/detail',
|
|
12
12
|
'/api/changelog', '/api/changelog-en', '/api/codex/models', '/api/usage',
|
|
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',
|
|
@@ -30,7 +30,7 @@ const WRITE_PATHS = ['/api/login', '/api/logout', '/api/logout/all',
|
|
|
30
30
|
// one-off agent run); it does not take a group concurrency slot.
|
|
31
31
|
'/api/test',
|
|
32
32
|
'/api/inbox/dismiss', '/api/issues/status',
|
|
33
|
-
// Console injection
|
|
33
|
+
// Console injection through the ordinary busy-followup ingress, the line shown in
|
|
34
34
|
// the group and the reply delivered there. The posted line is the platform
|
|
35
35
|
// message that drives the turn (see consolesay.createDurableConsoleSay).
|
|
36
36
|
'/api/groups/say',
|
|
@@ -38,13 +38,16 @@ const WRITE_PATHS = ['/api/login', '/api/logout', '/api/logout/all',
|
|
|
38
38
|
// manages this exact entry (the updater refuses every other run mode).
|
|
39
39
|
'/api/update/check', '/api/update/start', '/api/update/ignore']
|
|
40
40
|
const SUPPORTED_RUNNERS = ['claude', 'codex']
|
|
41
|
+
const { modelContext } = require('./modelcontext')
|
|
42
|
+
const { GROUP_COMMANDS } = require('./runtimefacts')
|
|
41
43
|
|
|
42
|
-
function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld = () => new Map(),
|
|
44
|
+
function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld = () => new Map(), activity = null, store = null,
|
|
43
45
|
assertWriterLock, validateConfig, applyConfig, beforeWrite = () => true,
|
|
44
46
|
appliedResets = () => [], now = Date.now,
|
|
45
47
|
// Console injection reaches a group only through the service's own ingress
|
|
46
48
|
// and that group's real platform adapter. Without them it is refused.
|
|
47
49
|
getPlatform = null, routeMessage = null, isServing = () => false, isFrozen = () => false,
|
|
50
|
+
acquireCapacity = null,
|
|
48
51
|
log = () => {} } = {}) {
|
|
49
52
|
for (const callback of [getBindings, getCapacity, getConfig, getHeld, assertWriterLock,
|
|
50
53
|
validateConfig, applyConfig, beforeWrite, appliedResets, now, isServing, isFrozen, log]) {
|
|
@@ -58,9 +61,12 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
58
61
|
throw new TypeError('C2 console configuration hooks must be synchronous')
|
|
59
62
|
}
|
|
60
63
|
const startedAt = now()
|
|
64
|
+
const trials = require('./consoletrials').createConsoleTrials({ getCapacity, acquireCapacity,
|
|
65
|
+
assertWriterLock, isServing, isFrozen })
|
|
61
66
|
const capabilities = Object.freeze({ mode: 'durable-v2',
|
|
67
|
+
groupCommands: GROUP_COMMANDS,
|
|
62
68
|
reason: '此操作尚未接入当前版本的持久处理流程,未执行。',
|
|
63
|
-
summary: '
|
|
69
|
+
summary: '实时状态、认证管理、停止任务、空闲群开新会话、支持忙时跟进的控制台插话、绑定新群、空闲群的模型切换(Codex / Claude Code)和项目目录修改、模型的添加与设置,以及后台运行时的软件升级已接入;该群有任务或未结记录时不能修改,也不能开新会话;改模型的型号、后端或端点时,用它的群须空闲,并会开始新的一段对话。插话会把这句话(标着「管理员」)和回复都发进群,群忙时按该群的忙时跟进设置尝试加入当前任务,关闭或暂不支持时排队;确认模型读取后才显示已加入;静默插话和不回复的插话暂不支持。解除绑定、移除模型、定时任务和画布暂不可用。',
|
|
64
70
|
allowed: { GET: READ_PATHS, POST: WRITE_PATHS } })
|
|
65
71
|
function held() {
|
|
66
72
|
if (assertWriterLock() !== true) throw new Error('服务写入权限已失效,操作未执行')
|
|
@@ -76,7 +82,7 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
76
82
|
// flight keeps the current session.
|
|
77
83
|
function groupBusy(chatId, binding) {
|
|
78
84
|
const rt = binding.engine?.rts?.get(chatId)
|
|
79
|
-
const snapshot = binding.journal?.snapshot()
|
|
85
|
+
const snapshot = binding.journal?.consoleSnapshot?.() || binding.journal?.snapshot()
|
|
80
86
|
return !!(rt?.v2NormalRouteActive || rt?.v2AdmissionPending || rt?.v2Controller ||
|
|
81
87
|
rt?.v2QueueReceiptIds?.length || snapshot?.receipts?.pending?.length ||
|
|
82
88
|
snapshot?.state?.lanes?.queue?.[chatId]?.queue?.length ||
|
|
@@ -97,7 +103,7 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
97
103
|
binding.engine.store.resetSessionAndStats(chatId)
|
|
98
104
|
return true
|
|
99
105
|
}
|
|
100
|
-
// Console injection
|
|
106
|
+
// Console injection: an open group, idle or running. Checked
|
|
101
107
|
// before the line is posted, so a refused group shows nothing, and again
|
|
102
108
|
// right after; the service ingress remains the final authority.
|
|
103
109
|
function sayGate(chatId) {
|
|
@@ -110,8 +116,13 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
110
116
|
const binding = bindings().get(chatId)
|
|
111
117
|
if (!binding || binding.runtime?.closed === true) return 'not-serving'
|
|
112
118
|
const rt = binding.engine?.rts?.get(chatId)
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
const snapshot = binding.journal?.consoleSnapshot?.() || binding.journal?.snapshot()
|
|
120
|
+
if (rt?.v2StopControl || rt?.v2SkipWaiter || snapshot?.state?.lanes?.stopIntent?.[chatId]) return 'busy'
|
|
121
|
+
// A normal active turn may accept ordinary messages. Unowned pending work
|
|
122
|
+
// still refuses; it must not become a new execution path or bypass recovery.
|
|
123
|
+
if (!rt?.v2NormalRouteActive && !rt?.v2AdmissionPending && (groupBusy(chatId, binding) || rt?.v2RunningJobs)) return 'busy'
|
|
124
|
+
const queued = snapshot?.state?.lanes?.queue?.[chatId]?.queue?.length || 0
|
|
125
|
+
if (queued >= (rt?.v2AdmissionPending ? 3 : 2)) return 'queue-full'
|
|
115
126
|
return null
|
|
116
127
|
}
|
|
117
128
|
function receiptState(chatId, receiptId) {
|
|
@@ -120,9 +131,22 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
120
131
|
if (receipts?.completed?.some(item => item.id === receiptId)) return 'completed'
|
|
121
132
|
return null
|
|
122
133
|
}
|
|
134
|
+
function locateSay(chatId, receiptId) {
|
|
135
|
+
const binding = bindings().get(chatId)
|
|
136
|
+
const rt = binding?.engine?.rts?.get(chatId)
|
|
137
|
+
const state = receiptState(chatId, receiptId)
|
|
138
|
+
if (state === 'completed') return { state: 'done' }
|
|
139
|
+
const steer = rt?.v2SteerEntries?.get(receiptId)
|
|
140
|
+
if (steer) return { state: steer.accepted && steer.landed ? 'joined' : 'following' }
|
|
141
|
+
const queue = (binding?.journal?.consoleSnapshot?.() || binding?.journal?.snapshot())?.state?.lanes?.queue?.[chatId]?.queue || []
|
|
142
|
+
const index = queue.findIndex(item => item.id === `receipt:job:${receiptId}`)
|
|
143
|
+
if (index >= 0) return { state: 'queued', ahead: index, join: false }
|
|
144
|
+
if (rt?.v2RunningJobs?.some(job => job.msg?._c2ReceiptId === receiptId)) return { state: 'started' }
|
|
145
|
+
return { state: 'accepted' }
|
|
146
|
+
}
|
|
123
147
|
const say = getPlatform ? createDurableConsoleSay({ getPlatform,
|
|
124
148
|
getGroup: chatId => getConfig().groups?.[chatId] || null,
|
|
125
|
-
gate: sayGate, route: routeMessage, receiptState, log, now }) : null
|
|
149
|
+
gate: sayGate, route: routeMessage, receiptState, locate: locateSay, log, now }) : null
|
|
126
150
|
function getLiveState() {
|
|
127
151
|
const cfg = getConfig()
|
|
128
152
|
const capacity = getCapacity()
|
|
@@ -133,11 +157,13 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
133
157
|
const binding = bindings().get(chatId)
|
|
134
158
|
const hold = heldGroups.get(chatId)
|
|
135
159
|
const rt = binding?.engine?.rts.get(chatId)
|
|
136
|
-
const snapshot = binding?.journal?.snapshot()
|
|
160
|
+
const snapshot = binding?.journal?.consoleSnapshot?.() || binding?.journal?.snapshot()
|
|
137
161
|
const pending = snapshot?.receipts?.pending || []
|
|
138
162
|
const queued = snapshot?.state?.lanes?.queue?.[chatId]?.queue?.length || 0
|
|
139
163
|
queue += queued
|
|
140
164
|
groups[chatId] = {
|
|
165
|
+
context: activity?.context(chatId, (binding?.engine?.store || store)?.getSession(chatId),
|
|
166
|
+
!!rt?.v2NormalRouteActive, rt?.v2RunnerGeneration) || null,
|
|
141
167
|
busy: !!rt?.v2NormalRouteActive,
|
|
142
168
|
finalizing: !!rt?.v2TerminalStarted,
|
|
143
169
|
followups: !!rt?.v2SteerControl && !rt?.v2SteerClosing && !rt?.v2TerminalStarted,
|
|
@@ -145,7 +171,7 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
145
171
|
queueFaulted: !group.paused && (!binding || (!rt?.v2NormalRouteActive && !!binding.gate?.blocked)),
|
|
146
172
|
waitingForCapacity: !!rt?.v2AdmissionPending,
|
|
147
173
|
sinceMs: rt?.v2StartedAt ? Math.max(0, now() - rt.v2StartedAt) : 0,
|
|
148
|
-
sessionId: binding?.engine?.store?.getSession(chatId) || '',
|
|
174
|
+
sessionId: (binding?.engine?.store || store)?.getSession(chatId) || '',
|
|
149
175
|
// Why a console injection would be refused right now (null = accepted).
|
|
150
176
|
sayBlocked: say ? sayGate(chatId) : 'not-serving',
|
|
151
177
|
// A held group needs the operator: its last result is unknown.
|
|
@@ -163,29 +189,56 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
163
189
|
function getActivity() {
|
|
164
190
|
const cfg = getConfig(), items = []
|
|
165
191
|
for (const [chatId, binding] of bindings()) {
|
|
166
|
-
const snap = binding.journal.snapshot()
|
|
192
|
+
const snap = binding.journal.consoleSnapshot?.() || binding.journal.snapshot()
|
|
167
193
|
const rt = binding.engine.rts.get(chatId)
|
|
168
194
|
const group = cfg.groups[chatId]?.name || chatId.slice(-8)
|
|
169
195
|
for (const receipt of snap.receipts.pending) {
|
|
196
|
+
if (activity?.has(chatId, receipt.id)) continue
|
|
170
197
|
const running = rt?.v2RunningJobs?.some(job => job.msg?._c2ReceiptId === receipt.id)
|
|
171
198
|
items.push({ id: receipt.id, chatId, group, ts: receipt.acceptedAt,
|
|
172
199
|
kind: running ? 'start' : 'done', status: running ? 'running' : '等待处理',
|
|
173
200
|
text: running ? '正在处理群消息' : '消息已登记,等待处理', sender: '' })
|
|
174
201
|
}
|
|
175
202
|
for (const receipt of snap.receipts.completed.slice(-30)) {
|
|
203
|
+
if (activity?.has(chatId, receipt.id)) continue
|
|
176
204
|
// A settled receipt alone does not prove successful answer delivery.
|
|
177
205
|
items.push({ id: receipt.id, chatId, group, ts: receipt.settledAt,
|
|
178
206
|
kind: 'done', status: '已结账', text: '消息处理记录已结账', sender: '' })
|
|
179
207
|
}
|
|
180
208
|
}
|
|
181
|
-
|
|
209
|
+
const recent = activity?.list({ limit: 100 }).activity || []
|
|
210
|
+
return [...recent, ...items].filter(item => cfg.groups[item.chatId])
|
|
211
|
+
.sort((a, b) => Number(a.kind !== 'start') - Number(b.kind !== 'start') || b.ts - a.ts).slice(0, 100)
|
|
212
|
+
}
|
|
213
|
+
function activityPage(query = {}) {
|
|
214
|
+
const cfg = getConfig(), live = getLiveState()
|
|
215
|
+
const meta = activity?.list({ limit: 1 }) || { epoch: 'receipts', revision: 0 }
|
|
216
|
+
const groups = Object.entries(cfg.groups || {}).map(([chatId, group]) => {
|
|
217
|
+
const binding = bindings().get(chatId), groupStore = binding?.engine?.store || store
|
|
218
|
+
const providerName = groupStore?.getProvider?.(chatId) || group.provider || cfg.defaultProvider
|
|
219
|
+
const provider = cfg.providers?.[providerName] || {}
|
|
220
|
+
const stats = groupStore?.getSessionStats?.(chatId), session = groupStore?.getSession?.(chatId)
|
|
221
|
+
const context = live.groups[chatId].context || (session && stats?.sessionId === session && Number.isFinite(stats.ctx)
|
|
222
|
+
? { usedTokens: stats.ctx, windowTokens: null, measuredAt: null, quality: 'historical' } : null)
|
|
223
|
+
return { chatId, name: group.name || chatId.slice(-8), project: path.basename(group.workdir || '') || '—',
|
|
224
|
+
workdir: group.workdir || '', runner: provider.runner || 'claude',
|
|
225
|
+
...modelContext(group, provider),
|
|
226
|
+
reasoningEffort: (provider.runner === 'codex' ? provider.reasoningEffort : provider.env?.CLAUDE_CODE_EFFORT_LEVEL) || '',
|
|
227
|
+
paused: !!group.paused, ...live.groups[chatId], context }
|
|
228
|
+
})
|
|
229
|
+
const rows = getActivity().filter(row => (!query.chatId || row.chatId === query.chatId) && (!query.status || row.status === query.status))
|
|
230
|
+
const offset = Math.max(0, Math.min(100, Number(query.offset) || 0)), limit = Math.max(1, Math.min(100, Number(query.limit) || 30))
|
|
231
|
+
return { epoch: meta.epoch, revision: meta.revision, storageError: !!meta.storageError, groups,
|
|
232
|
+
total: rows.length, activity: rows.slice(offset, offset + limit) }
|
|
182
233
|
}
|
|
183
234
|
return {
|
|
184
235
|
capabilities,
|
|
236
|
+
beginTrial: controller => trials.begin(controller),
|
|
185
237
|
allows: (method, pathname) => (capabilities.allowed[method] || []).includes(pathname),
|
|
186
238
|
// A provider without a runner field is Claude Code, as everywhere else.
|
|
187
239
|
providerSupported: provider => SUPPORTED_RUNNERS.includes(provider?.runner || 'claude'),
|
|
188
|
-
getLiveState, getActivity,
|
|
240
|
+
getLiveState, getActivity, activityPage,
|
|
241
|
+
activityDetail(chatId, id) { return getConfig().groups?.[chatId] ? activity?.detail(chatId, id) || null : null },
|
|
189
242
|
validateConfig(raw) { held(); validateConfig(raw) },
|
|
190
243
|
beforeWrite(raw) { held(); beforeWrite(raw) },
|
|
191
244
|
applyConfig(raw) { held(); applyConfig(raw) },
|
|
@@ -193,6 +246,12 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
193
246
|
appliedResets: () => appliedResets(),
|
|
194
247
|
decorateState(state) {
|
|
195
248
|
state.runtimeCapabilities = capabilities
|
|
249
|
+
for (const group of state.groups || []) {
|
|
250
|
+
const cfg = getConfig(), source = cfg.groups?.[group.chatId] || {}
|
|
251
|
+
const provider = cfg.providers?.[group.effectiveProvider || source.provider || cfg.defaultProvider]
|
|
252
|
+
const model = modelContext(source, provider)
|
|
253
|
+
Object.assign(group, { configuredModel: model.model, modelSource: model.source, actualModel: model.actualModel })
|
|
254
|
+
}
|
|
196
255
|
state.previews = (state.previews || []).filter(item => getConfig().groups?.[item.chatId])
|
|
197
256
|
state.leftovers = { supported: false, rows: [], error: null }
|
|
198
257
|
},
|
|
@@ -241,7 +300,18 @@ function createC2Console(options = {}) {
|
|
|
241
300
|
inboxDismissPath: path.join(stateDir, 'inbox-dismissed.json'),
|
|
242
301
|
verifyFile: path.join(stateDir, 'verify.json'),
|
|
243
302
|
})
|
|
244
|
-
|
|
303
|
+
let previous = null
|
|
304
|
+
const unsubscribe = options.activity?.subscribe(() => {
|
|
305
|
+
const page = facade.activityPage({ limit: 100 })
|
|
306
|
+
const before = new Map((previous?.activity || []).map(row => [row.chatId + ':' + row.id, JSON.stringify(row)]))
|
|
307
|
+
const current = new Set(page.activity.map(row => row.chatId + ':' + row.id))
|
|
308
|
+
const changed = page.activity.filter(row => before.get(row.chatId + ':' + row.id) !== JSON.stringify(row))
|
|
309
|
+
const groups = page.groups.filter(group => JSON.stringify(previous?.groups.find(g => g.chatId === group.chatId)) !== JSON.stringify(group))
|
|
310
|
+
ui.pushEvent('activity-update', { epoch: page.epoch, revision: page.revision, storageError: page.storageError,
|
|
311
|
+
activity: changed, groups, removed: [...before.keys()].filter(id => !current.has(id)) })
|
|
312
|
+
previous = page
|
|
313
|
+
})
|
|
314
|
+
return { ...ui, facade, close() { unsubscribe?.(); ui.close() } }
|
|
245
315
|
}
|
|
246
316
|
|
|
247
317
|
module.exports = { createC2Console, createC2ConsoleFacade }
|
package/src/c2updater.js
CHANGED
|
@@ -289,16 +289,24 @@ function createC2Updater({ currentVersion, runMode, stateFile, dataDir, configPa
|
|
|
289
289
|
const blocked = gate()
|
|
290
290
|
if (blocked.includes('held')) return fail('有暂停的群,升级已取消;先处理暂停的群')
|
|
291
291
|
if (blocked.length) { schedule(); return status() }
|
|
292
|
-
|
|
292
|
+
let prefix = job.installed ? job.prefix : path.join(releasesRoot, job.to)
|
|
293
293
|
if (!job.installed) {
|
|
294
294
|
setJob({ state: 'installing', prefix }, `正在安装 v${job.to}`)
|
|
295
295
|
try {
|
|
296
|
+
let reusable = false
|
|
296
297
|
if (fs.existsSync(prefix)) {
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
await verifyInstall({ version: job.to, prefix })
|
|
300
|
-
|
|
298
|
+
// Never repair an existing tree in place: another process may still
|
|
299
|
+
// use it. A failed check leaves it intact as evidence.
|
|
300
|
+
try { await verifyInstall({ version: job.to, prefix }); reusable = true }
|
|
301
|
+
catch { /* retry in an independent installation directory */ }
|
|
302
|
+
}
|
|
303
|
+
if (!reusable) {
|
|
301
304
|
fs.mkdirSync(releasesRoot, { recursive: true, mode: 0o700 })
|
|
305
|
+
// The directory becomes eligible for switching only after validation.
|
|
306
|
+
// Keeping its unique path also avoids overwriting a concurrently
|
|
307
|
+
// installed or running release when a download is retried.
|
|
308
|
+
prefix = fs.mkdtempSync(path.join(releasesRoot, `${job.to}-`))
|
|
309
|
+
setJob({ prefix })
|
|
302
310
|
await installVersion({ version: job.to, prefix, registry: job.registry })
|
|
303
311
|
await verifyInstall({ version: job.to, prefix })
|
|
304
312
|
}
|
package/src/consolesay.js
CHANGED
|
@@ -86,7 +86,7 @@ function createConsoleSay({ getPlatform, getGroup, handle, locate, log = () => {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// ── 持久处理流程(sintbot-v2)
|
|
89
|
+
// ── 持久处理流程(sintbot-v2)的插话,这句话与回复进群,忙时沿用普通群消息的跟进/排队流程 ──
|
|
90
90
|
//
|
|
91
91
|
// 群里那行「【管理员】…」就是驱动这一轮的那条平台消息:它的 om_ 消息 id 就是这笔任务的来源身份
|
|
92
92
|
// (source 'message'),与群成员自己发的一条消息同形 —— 收据、回复锚点、/stop、重启后的中断告知
|
|
@@ -97,7 +97,6 @@ function createConsoleSay({ getPlatform, getGroup, handle, locate, log = () => {
|
|
|
97
97
|
// 第 1 版不做的,都在发群消息**之前**就拒,群里不留痕:
|
|
98
98
|
// · 静默插话(这句不进群):没有平台消息可锚,需要另一种来源和重启告知路径;
|
|
99
99
|
// · 不回复的插话(quiet):持久路径还不接静默收据;
|
|
100
|
-
// · 群正忙:不排队、也不加入当前任务;
|
|
101
100
|
// · 斜杠命令:群命令不走插话(停止、开新会话在概览里有按钮)。
|
|
102
101
|
//
|
|
103
102
|
// requestId:控制台每次提交带的 32 位十六进制幂等键(经 syntheticSource 校验并折成键)。发出那行字
|
|
@@ -128,9 +127,10 @@ function refusalOf(value) {
|
|
|
128
127
|
// route(msg, platform) 服务的统一入口(routeMessage):暂停 / 待人工 / 收尾中 / 升级中都在那里把关
|
|
129
128
|
// receiptState(chatId, id) 该群日志里这笔收据:'pending' | 'completed' | null
|
|
130
129
|
function createDurableConsoleSay({ getPlatform, getGroup, gate, route, receiptState,
|
|
130
|
+
locate = () => ({ state: 'accepted' }),
|
|
131
131
|
log = () => {}, now = () => Date.now(), sendTimeoutMs = SEND_TIMEOUT_MS,
|
|
132
132
|
confirmMs = CONFIRM_MS, pollMs = POLL_MS, maxRequests = MAX_REQUESTS } = {}) {
|
|
133
|
-
for (const hook of [getPlatform, getGroup, gate, route, receiptState, log, now]) {
|
|
133
|
+
for (const hook of [getPlatform, getGroup, gate, route, receiptState, locate, log, now]) {
|
|
134
134
|
if (typeof hook !== 'function') throw new TypeError('durable console say needs explicit hooks')
|
|
135
135
|
}
|
|
136
136
|
const posted = new Map() // 请求键 → 群里那行字的消息 id(按插入顺序,超过上限丢最早的)
|
|
@@ -171,7 +171,7 @@ function createDurableConsoleSay({ getPlatform, getGroup, gate, route, receiptSt
|
|
|
171
171
|
return { ok: false, reason: 'shown-failed' }
|
|
172
172
|
}
|
|
173
173
|
remember(key, shownId)
|
|
174
|
-
//
|
|
174
|
+
// 发送期间再次检查暂停、停止、维护及容量,正常忙时仍走统一入口
|
|
175
175
|
const late = gate(chatId)
|
|
176
176
|
if (late) return { ok: false, reason: late === 'busy' ? 'busy-after-post' : late }
|
|
177
177
|
}
|
|
@@ -186,12 +186,12 @@ function createDurableConsoleSay({ getPlatform, getGroup, gate, route, receiptSt
|
|
|
186
186
|
if (first.admitted || first.timeout) {
|
|
187
187
|
// 这一轮在后台继续;它之后的成败由该群自己的持久路径收尾,这里只留日志
|
|
188
188
|
settled.then(({ error }) => { if (error) log('say: ' + (error && error.message)) })
|
|
189
|
-
return first.admitted ? { ok: true, where:
|
|
189
|
+
return first.admitted ? { ok: true, where: locate(chatId, receiptId) } : { ok: false, reason: 'unconfirmed' }
|
|
190
190
|
}
|
|
191
191
|
const { value, error } = first
|
|
192
192
|
if (value && value.duplicate) return { ok: true, where: { state: 'duplicate' } }
|
|
193
193
|
const state = receiptState(chatId, receiptId)
|
|
194
|
-
if (state) return { ok: true, where:
|
|
194
|
+
if (state) return { ok: true, where: state === 'completed' ? { state: 'done' } : locate(chatId, receiptId) }
|
|
195
195
|
if (error) {
|
|
196
196
|
log('say: ' + (error && error.message))
|
|
197
197
|
return { ok: false, reason: 'inject-failed' }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// One bounded console trial, sharing the real model capacity. A timeout or
|
|
4
|
+
// lease loss asks the runner to stop; only its close proof releases capacity.
|
|
5
|
+
function createConsoleTrials({ getCapacity, acquireCapacity, assertWriterLock, isServing, isFrozen }) {
|
|
6
|
+
let active = false
|
|
7
|
+
function assertWritable() {
|
|
8
|
+
if (assertWriterLock() !== true) throw new Error('服务写入权限已失效,试运行结果未保存')
|
|
9
|
+
}
|
|
10
|
+
function assertAdmission() {
|
|
11
|
+
assertWritable()
|
|
12
|
+
if (!isServing() || isFrozen()) throw new Error('服务未就绪或正在维护,暂不能试运行')
|
|
13
|
+
}
|
|
14
|
+
async function begin(controller) {
|
|
15
|
+
assertAdmission()
|
|
16
|
+
const capacity = getCapacity()
|
|
17
|
+
if (active || capacity.closed || capacity.active + capacity.waiting >= capacity.maxConcurrent) {
|
|
18
|
+
throw new Error('试运行或模型并发名额已满,请稍后重试')
|
|
19
|
+
}
|
|
20
|
+
if (typeof acquireCapacity !== 'function') throw new Error('当前服务未接入试运行并发准入')
|
|
21
|
+
active = true
|
|
22
|
+
let release
|
|
23
|
+
try {
|
|
24
|
+
release = await acquireCapacity({ signal: controller.signal })
|
|
25
|
+
assertAdmission()
|
|
26
|
+
} catch (error) { active = false; release?.(); throw error }
|
|
27
|
+
let finished = false
|
|
28
|
+
const monitor = setInterval(() => {
|
|
29
|
+
try { assertAdmission() } catch (error) { controller.abort(error) }
|
|
30
|
+
}, 100)
|
|
31
|
+
monitor.unref?.()
|
|
32
|
+
return { assertWritable,
|
|
33
|
+
finish(closed) {
|
|
34
|
+
if (finished) return
|
|
35
|
+
finished = true; clearInterval(monitor)
|
|
36
|
+
// Unknown closure keeps the shared slot occupied and blocks upgrades.
|
|
37
|
+
if (closed) { active = false; release() }
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { begin, active: () => active }
|
|
42
|
+
}
|
|
43
|
+
module.exports = { createConsoleTrials }
|
|
@@ -129,7 +129,7 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
129
129
|
const nextCtx = landedAnchor(anchor)
|
|
130
130
|
if (phase !== 'open' || !card) return false
|
|
131
131
|
if (activeAnchorCtx.messageId === nextCtx.messageId) return true
|
|
132
|
-
if (!lastCardText) throw new Error('C2 card move has no visible text to
|
|
132
|
+
if (!lastCardText) throw new Error('C2 card move has no visible text to preserve')
|
|
133
133
|
phase = 'relocating'
|
|
134
134
|
try {
|
|
135
135
|
if (await card.flush() !== true) throw new Error('C2 old card flush unconfirmed')
|
|
@@ -138,6 +138,7 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
138
138
|
const fresh = await platform.createStreamingCard(nextCtx, {
|
|
139
139
|
operationKey: `${receiptId}:followup:${anchor.receiptId}`,
|
|
140
140
|
operationBoundary,
|
|
141
|
+
initialText: lastCardText,
|
|
141
142
|
})
|
|
142
143
|
if (!fresh || typeof fresh.update !== 'function' ||
|
|
143
144
|
typeof fresh.flush !== 'function' || typeof fresh.recall !== 'function') {
|
|
@@ -646,6 +646,16 @@ class C2DurableJournal {
|
|
|
646
646
|
effects: new Map(this.#effects), notices: new Map(this.#notices) }
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
+
// Bounded diagnostics: the console must not clone all effects and history
|
|
650
|
+
// at progress-event frequency. This is never accepted as recovery authority.
|
|
651
|
+
consoleSnapshot() {
|
|
652
|
+
return { cursor: this.#cursor, state: { lanes: {
|
|
653
|
+
queue: structuredClone(this.#state.lanes?.queue || {}),
|
|
654
|
+
stopIntent: structuredClone(this.#state.lanes?.stopIntent || {}) } },
|
|
655
|
+
receipts: { pending: structuredClone(this.#receipts.pending),
|
|
656
|
+
completed: structuredClone(this.#receipts.completed.slice(-30)) } }
|
|
657
|
+
}
|
|
658
|
+
|
|
649
659
|
#settlementAuthority() {
|
|
650
660
|
return { state: this.#state, receipts: this.#receipts, effects: this.#effects,
|
|
651
661
|
attempts: this.#effectAttempts, descriptors: this.#effectDescriptors,
|