sintbot 1.4.15 → 1.4.17
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 +50 -0
- package/CHANGELOG.json +50 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/sintbot-v2.js +104 -4
- package/docs/AI-ASSISTED-INSTALL.md +4 -4
- package/docs/install.md +46 -10
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +4 -4
- package/docs/zh-CN/install.md +46 -10
- package/install.sh +1 -1
- package/package.json +10 -3
- package/src/agent/claudecode.js +5 -2
- package/src/agent/claudemodels.js +79 -8
- package/src/agent/claudeusage.js +188 -3
- package/src/agent/codexapp.js +4 -3
- package/src/agent/codexcode.js +1 -1
- package/src/agent/envconflicts.js +74 -0
- package/src/agent/privatecore.js +65 -4
- package/src/agent/privatecore.wasm +0 -0
- package/src/c2configcontrol.js +29 -18
- package/src/c2console.js +22 -6
- package/src/c2install.js +1 -1
- package/src/c2multigroupservice.js +22 -2
- package/src/connect.js +488 -0
- 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 +39 -7
- package/src/platform/modelcard.js +103 -0
- package/src/ui/console.html +273 -147
- package/src/ui/serverbase.js +174 -17
- package/src/usage.js +131 -38
- package/src/modelcontext.js +0 -13
package/src/agent/privatecore.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// ABI
|
|
1
|
+
// ABI v36 adapter for the private binary decision core. Manifest assembly still stays in JS.
|
|
2
2
|
const fs = require('node:fs')
|
|
3
3
|
const path = require('node:path')
|
|
4
4
|
const { createHash } = require('node:crypto')
|
|
5
5
|
|
|
6
|
-
const EXPECTED_SHA256 = '
|
|
6
|
+
const EXPECTED_SHA256 = 'c79f12ceb1ef79aceb0ab54cf87052127325a1d8fdfd5c02e3509fef39f6f511'
|
|
7
7
|
let bytes
|
|
8
8
|
try {
|
|
9
9
|
bytes = fs.readFileSync(path.join(__dirname, 'privatecore.wasm'))
|
|
@@ -26,7 +26,7 @@ try {
|
|
|
26
26
|
} catch (cause) {
|
|
27
27
|
throw new Error('SintBot binary core cannot start on this runtime', { cause })
|
|
28
28
|
}
|
|
29
|
-
if (core.abi_version() !==
|
|
29
|
+
if (core.abi_version() !== 36) throw new Error('SintBot binary core ABI mismatch')
|
|
30
30
|
const encoder = new TextEncoder()
|
|
31
31
|
const decoder = new TextDecoder('utf-8', { fatal: true })
|
|
32
32
|
|
|
@@ -771,6 +771,67 @@ function manifestPolicy(permissionMode, isCodex, allowedTools, disallowedTools)
|
|
|
771
771
|
return value
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
+
// ABI 36 JSON decisions (sintbot-core docs/ABI36_CONTRACTS.md). Each wrapper only
|
|
775
|
+
// checks the envelope shape the host relies on; the core owns the rules and wording.
|
|
776
|
+
const isObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
|
|
777
|
+
|
|
778
|
+
function connectManifest(basic, userName, version) {
|
|
779
|
+
const value = coreJson('connect_manifest', { basic: !!basic, userName: String(userName || ''), version: String(version || '') }, 'build the Feishu connect manifest')
|
|
780
|
+
if (!isObject(value) || !isObject(value.appPreset) || !isObject(value.addons) ||
|
|
781
|
+
!Array.isArray(value.addons.scopes) || !Array.isArray(value.addons.events) ||
|
|
782
|
+
!Array.isArray(value.addons.callbacks) || typeof value.hint !== 'string') {
|
|
783
|
+
throw new Error('SintBot binary core returned an invalid connect manifest')
|
|
784
|
+
}
|
|
785
|
+
return value
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function connectPlan(step, { basic = false, status = null, code = null, msg = '', observed = {} } = {}) {
|
|
789
|
+
const value = coreJson('connect_plan', { step, basic: !!basic, status, code, msg: String(msg || ''), observed: isObject(observed) ? observed : {} }, `decide the connect step ${step}`)
|
|
790
|
+
if (!isObject(value) || !['ok', 'stop', 'wait', 'warn'].includes(value.verdict) || typeof value.notice !== 'string' ||
|
|
791
|
+
!(value.next === null || typeof value.next === 'string') ||
|
|
792
|
+
!(value.retryAfterMs === null || Number.isSafeInteger(value.retryAfterMs))) {
|
|
793
|
+
throw new Error('SintBot binary core returned an invalid connect decision')
|
|
794
|
+
}
|
|
795
|
+
return value
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function connectConfig(input) {
|
|
799
|
+
const value = coreJson('connect_config', input, 'build the Feishu connect configuration')
|
|
800
|
+
if (!isObject(value) || typeof value.ok !== 'boolean' ||
|
|
801
|
+
(value.ok && (!isObject(value.config) || typeof value.readme !== 'string')) ||
|
|
802
|
+
(!value.ok && typeof value.error !== 'string')) {
|
|
803
|
+
throw new Error('SintBot binary core returned an invalid connect configuration')
|
|
804
|
+
}
|
|
805
|
+
return value
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function qrMatrix(text, ec = 'M') {
|
|
809
|
+
const value = coreJson('qr_matrix', { text: String(text), ec }, 'encode a QR code')
|
|
810
|
+
if (!isObject(value) || !Number.isInteger(value.size) || !Array.isArray(value.rows) ||
|
|
811
|
+
value.rows.length !== value.size || value.rows.some(row => typeof row !== 'string' || row.length !== value.size)) {
|
|
812
|
+
throw new Error('SintBot binary core returned an invalid QR matrix')
|
|
813
|
+
}
|
|
814
|
+
return value
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function usageParse(provider, source, status, body, now = Date.now()) {
|
|
818
|
+
const value = coreJson('usage_parse', { provider, source, status, body, now }, `parse ${provider} ${source} usage`)
|
|
819
|
+
if (!isObject(value) || typeof value.ok !== 'boolean' ||
|
|
820
|
+
(value.ok && !Array.isArray(value.windows)) || (!value.ok && typeof value.error !== 'string')) {
|
|
821
|
+
throw new Error('SintBot binary core returned an invalid usage result')
|
|
822
|
+
}
|
|
823
|
+
return value
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function envConflicts(input) {
|
|
827
|
+
const value = coreJson('env_conflicts', input, 'check environment conflicts')
|
|
828
|
+
if (!isObject(value) || !Array.isArray(value.warnings) ||
|
|
829
|
+
value.warnings.some(w => !isObject(w) || typeof w.key !== 'string' || typeof w.where !== 'string' || typeof w.reason !== 'string')) {
|
|
830
|
+
throw new Error('SintBot binary core returned an invalid environment check')
|
|
831
|
+
}
|
|
832
|
+
return value
|
|
833
|
+
}
|
|
834
|
+
|
|
774
835
|
// A trap or exhausted stack can leave the instance's own stack pointer wrong, and
|
|
775
836
|
// C1 calls hold no state, so the next call simply gets a fresh instance.
|
|
776
837
|
function guarded(fn) {
|
|
@@ -798,7 +859,7 @@ function followupCall(name, value) {
|
|
|
798
859
|
function followupEnabled(group, runner) { return followupCall('followup_policy', { group, runner }) }
|
|
799
860
|
function claudeFollowupStep(state, event) { return followupCall('claude_followup_step', { state, event }) }
|
|
800
861
|
|
|
801
|
-
const entries = { followupEnabled, claudeFollowupStep, classifyError, routeBackend, sandboxTier, appErrorOverride, appNormalizeItem, appStreamTail, appNoticeAction, appDeltaNext, appLandedAction, appCompletionAction, appCompletionNoticeAction, manifestFeatures, manifestHeader, manifestContext, manifestSkills, manifestStyleBoard, manifestImagenAsset, manifestRuntime, manifestPolicy, systemAppendix, codexEffortRejection, wecomGatePrompt, imagenPrompt, turnPrompt, compactEvent, engineQueueStep, engineSlotsStep, engineTurnStep, engineSessionStep, engineHandoffStep, engineIntakeStep, engineLifecycleStep, engineTerminalStep, engineSteerStep, engineRecoveryStep, engineFailoverStep, parseResetFields, chooseResetEpoch, codexBuildPrompt, codexBuildArgs, appUsageFromTotal, settleUsage, appBuildRequest, claudeBaseSkillAllows, claudeBuildArgs, claudePluginAllowRules, claudeNormalizeEvent, codexNormalizeEvent, codexParseError }
|
|
862
|
+
const entries = { followupEnabled, claudeFollowupStep, classifyError, routeBackend, sandboxTier, appErrorOverride, appNormalizeItem, appStreamTail, appNoticeAction, appDeltaNext, appLandedAction, appCompletionAction, appCompletionNoticeAction, manifestFeatures, manifestHeader, manifestContext, manifestSkills, manifestStyleBoard, manifestImagenAsset, manifestRuntime, manifestPolicy, systemAppendix, codexEffortRejection, wecomGatePrompt, imagenPrompt, turnPrompt, compactEvent, engineQueueStep, engineSlotsStep, engineTurnStep, engineSessionStep, engineHandoffStep, engineIntakeStep, engineLifecycleStep, engineTerminalStep, engineSteerStep, engineRecoveryStep, engineFailoverStep, parseResetFields, chooseResetEpoch, codexBuildPrompt, codexBuildArgs, appUsageFromTotal, settleUsage, appBuildRequest, claudeBaseSkillAllows, claudeBuildArgs, claudePluginAllowRules, claudeNormalizeEvent, codexNormalizeEvent, codexParseError, connectManifest, connectPlan, connectConfig, qrMatrix, usageParse, envConflicts }
|
|
802
863
|
for (const name of Object.keys(entries)) entries[name] = guarded(entries[name])
|
|
803
864
|
|
|
804
865
|
module.exports = { ...entries, engineStep, resetBinaryCore, HUMAN_BY_KIND: HUMAN }
|
|
Binary file
|
package/src/c2configcontrol.js
CHANGED
|
@@ -8,20 +8,24 @@ const { loadInstallationFromRaw, assertJournalScope, assertRunnerExecutable,
|
|
|
8
8
|
MANIFEST, VALIDATORS, SUPPORTED_RUNNERS } = require('./c2install')
|
|
9
9
|
const { C2DurableJournal } = require('./core/c2durablejournal')
|
|
10
10
|
const { assertNoLegacyGroupState } = require('./c2migration')
|
|
11
|
+
const { applyBrainChange } = require('./groupbrain')
|
|
11
12
|
|
|
12
13
|
const GLOBAL_TASK_KEYS = ['agent', 'timezone', 'brand']
|
|
13
|
-
|
|
14
|
+
// A group's model and depth (providerPrefs, legacy model) are not here: the
|
|
15
|
+
// executors resume the same session with another model of the same backend.
|
|
16
|
+
const RESET_KEYS = ['provider', 'workdir', 'codexApp']
|
|
14
17
|
const EFFORT_ENV = 'CLAUDE_CODE_EFFORT_LEVEL'
|
|
15
18
|
|
|
16
|
-
// What a group's session belongs to: backend, executable, endpoint
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
19
|
+
// What a group's session belongs to: backend, executable, endpoint and the
|
|
20
|
+
// environment the backend starts with. Model, depth, tool quality, key, image
|
|
21
|
+
// support, visibility and order apply from the next turn in the same session:
|
|
22
|
+
// both executors resume a session with another model of the same backend.
|
|
23
|
+
function sessionIdentity(provider) {
|
|
20
24
|
if (!provider || typeof provider !== 'object') return null
|
|
21
25
|
const env = { ...(provider.env && typeof provider.env === 'object' ? provider.env : {}) }
|
|
22
26
|
delete env[EFFORT_ENV]
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
delete env.ANTHROPIC_MODEL
|
|
28
|
+
return { runner: provider.runner, bin: provider.bin || '', baseUrl: provider.baseUrl || '', env }
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
// Synchronous validation/application around one atomic configuration write.
|
|
@@ -134,8 +138,8 @@ function createC2ConfigControl({ installation, getBindings, getHeld = () => new
|
|
|
134
138
|
// A model library or default change reaches every group that runs
|
|
135
139
|
// the edited provider, even though its own settings stay the same.
|
|
136
140
|
const providerMoved = !isDeepStrictEqual(
|
|
137
|
-
sessionIdentity(cfg.providers?.[effective(route.chatId, route.group, cfg)]
|
|
138
|
-
sessionIdentity(next.cfg.providers?.[effective(route.chatId, target.group, next.cfg)]
|
|
141
|
+
sessionIdentity(cfg.providers?.[effective(route.chatId, route.group, cfg)]),
|
|
142
|
+
sessionIdentity(next.cfg.providers?.[effective(route.chatId, target.group, next.cfg)]))
|
|
139
143
|
if (heldGroups.has(route.chatId)) {
|
|
140
144
|
// Edits elsewhere proceed; the held group keeps its exact settings.
|
|
141
145
|
if (!isDeepStrictEqual(route.group, target.group)) {
|
|
@@ -272,18 +276,25 @@ function createC2ConfigControl({ installation, getBindings, getHeld = () => new
|
|
|
272
276
|
return raw
|
|
273
277
|
}
|
|
274
278
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
// Backend · model · depth of one group (console, /model, model card). The
|
|
280
|
+
// writer validates, resets the session only when the backend changes, and
|
|
281
|
+
// goes live. Returns the change summary; throws on refusal.
|
|
282
|
+
function setGroupBrain(chatId, change = {}) {
|
|
283
|
+
let summary = null
|
|
284
|
+
writeConfig(raw => {
|
|
285
|
+
if (!raw.groups?.[chatId]) throw new Error('Unknown group')
|
|
286
|
+
const result = applyBrainChange(raw, chatId, change)
|
|
287
|
+
if (result.error) throw new Error({ 'unknown-provider': 'Unknown model',
|
|
288
|
+
'bad-model': 'Invalid model id', 'bad-effort': 'Invalid thinking depth' }[result.error] || 'Unknown group')
|
|
289
|
+
summary = result
|
|
283
290
|
})
|
|
291
|
+
return summary
|
|
292
|
+
}
|
|
293
|
+
function switchGroupProvider(chatId, name) {
|
|
294
|
+
return setGroupBrain(chatId, { provider: name })
|
|
284
295
|
}
|
|
285
296
|
|
|
286
|
-
return { validateConfig, beforeWrite, applyConfig, writeConfig, switchGroupProvider,
|
|
297
|
+
return { validateConfig, beforeWrite, applyConfig, writeConfig, switchGroupProvider, setGroupBrain,
|
|
287
298
|
// Groups whose session the last applied write reset (for the console).
|
|
288
299
|
appliedResets: () => [...appliedResets] }
|
|
289
300
|
}
|
package/src/c2console.js
CHANGED
|
@@ -13,10 +13,11 @@ const READ_PATHS = ['/api/state', '/api/events', '/api/inbox', '/api/activity',
|
|
|
13
13
|
'/api/plugins', '/api/design/styles', '/api/design/icons', '/api/fs/listdir',
|
|
14
14
|
'/api/feishu/admin-candidates', '/api/feishu/org-users', '/api/feishu/resolve-contact',
|
|
15
15
|
'/api/feishu/chats', '/api/feishu/chat-members', '/api/update', '/api/claude/models',
|
|
16
|
+
'/api/groups/model-options',
|
|
16
17
|
START_PATH, CALLBACK_PATH]
|
|
17
18
|
const WRITE_PATHS = ['/api/login', '/api/logout', '/api/logout/all',
|
|
18
19
|
'/api/ui/network', '/api/ui/public-url', '/api/ui/update-settings', '/api/feishu/admins',
|
|
19
|
-
'/api/groups', '/api/groups/provider', '/api/fs/mkdir',
|
|
20
|
+
'/api/groups', '/api/groups/provider', '/api/groups/brain', '/api/fs/mkdir',
|
|
20
21
|
'/api/groups/order', '/api/groups/policy', '/api/groups/redact',
|
|
21
22
|
'/api/groups/codex-steer', '/api/groups/steer', '/api/groups/pause', '/api/groups/stop',
|
|
22
23
|
// A fresh session only for an idle, settled group (same rule as /new).
|
|
@@ -38,9 +39,18 @@ const WRITE_PATHS = ['/api/login', '/api/logout', '/api/logout/all',
|
|
|
38
39
|
// manages this exact entry (the updater refuses every other run mode).
|
|
39
40
|
'/api/update/check', '/api/update/start', '/api/update/ignore']
|
|
40
41
|
const SUPPORTED_RUNNERS = ['claude', 'codex']
|
|
41
|
-
const {
|
|
42
|
+
const { groupBrain } = require('./core/brain')
|
|
43
|
+
const { codexCatalogWindows, windowForProvider } = require('./agent/modelwindows')
|
|
42
44
|
const { GROUP_COMMANDS } = require('./runtimefacts')
|
|
43
45
|
|
|
46
|
+
// What a group runs on one backend, for console rows: configured values only;
|
|
47
|
+
// what the executor finally selected is reported by the run itself.
|
|
48
|
+
function brainFields(group, name, provider, cfg) {
|
|
49
|
+
const b = groupBrain(group, name, provider, { defaultProvider: cfg.defaultProvider })
|
|
50
|
+
return { model: b.model, reasoningEffort: b.effort, groupModel: b.groupModel, groupEffort: b.groupEffort,
|
|
51
|
+
source: b.groupModel ? 'group' : b.defaultModel ? 'provider' : 'default', actualModel: null }
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld = () => new Map(), activity = null, store = null,
|
|
45
55
|
assertWriterLock, validateConfig, applyConfig, beforeWrite = () => true,
|
|
46
56
|
appliedResets = () => [], now = Date.now,
|
|
@@ -222,8 +232,7 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
222
232
|
? { usedTokens: stats.ctx, windowTokens: null, measuredAt: null, quality: 'historical' } : null)
|
|
223
233
|
return { chatId, name: group.name || chatId.slice(-8), project: path.basename(group.workdir || '') || '—',
|
|
224
234
|
workdir: group.workdir || '', runner: provider.runner || 'claude',
|
|
225
|
-
...
|
|
226
|
-
reasoningEffort: (provider.runner === 'codex' ? provider.reasoningEffort : provider.env?.CLAUDE_CODE_EFFORT_LEVEL) || '',
|
|
235
|
+
...brainFields(group, providerName, provider, cfg),
|
|
227
236
|
paused: !!group.paused, ...live.groups[chatId], context }
|
|
228
237
|
})
|
|
229
238
|
const rows = getActivity().filter(row => (!query.chatId || row.chatId === query.chatId) && (!query.status || row.status === query.status))
|
|
@@ -249,8 +258,13 @@ function createC2ConsoleFacade({ getBindings, getCapacity, getConfig, getHeld =
|
|
|
249
258
|
for (const group of state.groups || []) {
|
|
250
259
|
const cfg = getConfig(), source = cfg.groups?.[group.chatId] || {}
|
|
251
260
|
const provider = cfg.providers?.[group.effectiveProvider || source.provider || cfg.defaultProvider]
|
|
252
|
-
const
|
|
253
|
-
|
|
261
|
+
const name = group.effectiveProvider || source.provider || cfg.defaultProvider
|
|
262
|
+
const fields = brainFields(source, name, provider, cfg)
|
|
263
|
+
Object.assign(group, { configuredModel: fields.model, modelSource: fields.source,
|
|
264
|
+
actualModel: null, brain: { provider: name, model: fields.model, effort: fields.reasoningEffort,
|
|
265
|
+
groupModel: fields.groupModel, groupEffort: fields.groupEffort },
|
|
266
|
+
providerPrefs: source.providerPrefs || null,
|
|
267
|
+
contextWindow: provider ? windowForProvider({ ...provider, model: fields.model }, null, codexCatalogWindows()) : null })
|
|
254
268
|
}
|
|
255
269
|
state.previews = (state.previews || []).filter(item => getConfig().groups?.[item.chatId])
|
|
256
270
|
state.leftovers = { supported: false, rows: [], error: null }
|
|
@@ -290,6 +304,8 @@ function createC2Console(options = {}) {
|
|
|
290
304
|
throw new TypeError('C2 console requires explicit installation paths')
|
|
291
305
|
}
|
|
292
306
|
const facade = createC2ConsoleFacade(options)
|
|
307
|
+
// The process-wide model list (also used by /model), when the service has one.
|
|
308
|
+
facade.modelCatalog = typeof options.modelCatalog === 'function' ? options.modelCatalog : null
|
|
293
309
|
const ui = createUiServer({ configPath, host, log, getConnHealth, updater: options.updater || null,
|
|
294
310
|
onHostChange: options.onHostChange,
|
|
295
311
|
getLiveState: facade.getLiveState, control: facade.control, runtimePolicy: facade,
|
package/src/c2install.js
CHANGED
|
@@ -323,6 +323,6 @@ function registerAddedGroups(installation) {
|
|
|
323
323
|
return addedRoutes.map(route => route.chatId)
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
module.exports = { registerAddedGroups, loadInstallation, loadInstallationFromRaw, initializeInstallation, assertPrivateDirectory,
|
|
326
|
+
module.exports = { registerAddedGroups, loadInstallation, loadInstallationFromRaw, initializeInstallation, installationStateDir, assertPrivateDirectory,
|
|
327
327
|
assertJournalScope, assertInstallationJournals, assertRunnerExecutable, SUPPORTED_RUNNERS, MANIFEST,
|
|
328
328
|
VALIDATORS }
|
|
@@ -39,7 +39,7 @@ function createC2MultiGroupService({ routes, assertWriterLock,
|
|
|
39
39
|
createTransport, createBinding, onFatal = () => {}, closeCapacity = () => {},
|
|
40
40
|
quarantine = null, stopTimeoutMs = 2_000, drainTimeoutMs = 12_000,
|
|
41
41
|
createRecoveryAuthority = null, holds = new Map(), sharedFault = () => null,
|
|
42
|
-
ingressWaitMs = 60_000, now = Date.now, log = () => {} } = {}) {
|
|
42
|
+
ingressWaitMs = 60_000, onModelCard = null, now = Date.now, log = () => {} } = {}) {
|
|
43
43
|
if (!Array.isArray(routes) || !routes.length ||
|
|
44
44
|
new Set(routes.map(route => route.chatId)).size !== routes.length ||
|
|
45
45
|
routes.some(route => !route || typeof route.chatId !== 'string' ||
|
|
@@ -392,6 +392,26 @@ function createC2MultiGroupService({ routes, assertWriterLock,
|
|
|
392
392
|
throw error
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
|
+
// Taps on the /model card only. They write the configuration through the
|
|
396
|
+
// same validated writer as the console and never touch a journal, receipt
|
|
397
|
+
// or session; every other card callback stays unsupported. The return
|
|
398
|
+
// value is the platform's synchronous answer (toast and replacement card).
|
|
399
|
+
function cardAction(event) {
|
|
400
|
+
let value = event?.action?.value
|
|
401
|
+
if (typeof value === 'string') { try { value = JSON.parse(value) } catch { value = null } }
|
|
402
|
+
if (typeof onModelCard !== 'function' || !value || value.a !== 'mc' || !allowed(event?.chatId)) {
|
|
403
|
+
return unsupported('card action')(event)
|
|
404
|
+
}
|
|
405
|
+
if (maintenance) return { toast: { type: 'info', content: '正在升级,稍后再点' } }
|
|
406
|
+
if (suspended.has(event.chatId)) return { toast: { type: 'warning', content: '本群暂停处理中,请管理员处理后再改' } }
|
|
407
|
+
const binding = bindings.get(event.chatId)
|
|
408
|
+
if (!binding) return { ignored: true }
|
|
409
|
+
return Promise.resolve().then(() => onModelCard({ ...event, action: { ...event.action, value } }, binding))
|
|
410
|
+
.catch(error => {
|
|
411
|
+
log(`model card ${event.chatId}: ${error?.message || String(error)}`)
|
|
412
|
+
return { toast: { type: 'error', content: '没改成 —— 请稍后再试' } }
|
|
413
|
+
})
|
|
414
|
+
}
|
|
395
415
|
function unsupported(source) {
|
|
396
416
|
return event => {
|
|
397
417
|
// An event without group identity cannot safely be attributed. It has
|
|
@@ -507,7 +527,7 @@ function createC2MultiGroupService({ routes, assertWriterLock,
|
|
|
507
527
|
transportStarted = true
|
|
508
528
|
await transport.platform.start(routeMessage, { onFatal: fatal,
|
|
509
529
|
onRecall: recall, onReaction: unsupported('reaction'),
|
|
510
|
-
onCardAction:
|
|
530
|
+
onCardAction: cardAction, onBoardUpdated: unsupported('board update') })
|
|
511
531
|
held()
|
|
512
532
|
if (stopPromise) throw new Error('C2 service stopped during connection')
|
|
513
533
|
return { groups: bindings.size, connected: true, ...(suspended.size ? { held: heldGroups() } : {}) }
|