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/core/v2engine.js
CHANGED
|
@@ -18,6 +18,8 @@ const { prepareC2RunnerInput } = require('./c2runnerinput')
|
|
|
18
18
|
const { continueC2Successor } = require('./c2successorbridge')
|
|
19
19
|
const { rejectC2Input } = require('./c2inputrejection')
|
|
20
20
|
const progress = require('./turnprogress')
|
|
21
|
+
const { modelContext } = require('../modelcontext')
|
|
22
|
+
const { GROUP_COMMANDS, RUNTIME_ENTRY, runtimeCommand } = require('../runtimefacts')
|
|
21
23
|
const NO_REPLY = 'NO_REPLY'
|
|
22
24
|
const SYSTEM_APPENDIX = systemAppendix(NO_REPLY)
|
|
23
25
|
const V2_NORMAL_ROUTE_MISSING = Object.freeze(['complete durable v2 authority'])
|
|
@@ -25,7 +27,7 @@ const V2_NORMAL_ROUTE_MISSING = Object.freeze(['complete durable v2 authority'])
|
|
|
25
27
|
// the Codex read-only sandbox, 3 the fail-closed default. 0 is full access
|
|
26
28
|
// and 2 an allow-listed tool set, which may still change files.
|
|
27
29
|
const READ_ONLY_TIERS = Object.freeze([1, 3])
|
|
28
|
-
const DURABLE_V2_COMMANDS = new Set(
|
|
30
|
+
const DURABLE_V2_COMMANDS = new Set(GROUP_COMMANDS.filter(cmd => !['/skip', '/stop'].includes(cmd)))
|
|
29
31
|
function fmtWan(n) {
|
|
30
32
|
n = Number(n) || 0
|
|
31
33
|
if (n >= 100_000_000) return `${(n / 100_000_000).toFixed(1).replace(/\.0$/, '')}亿`
|
|
@@ -51,8 +53,7 @@ function cardDuration(ms) {
|
|
|
51
53
|
}
|
|
52
54
|
function cardModelInfo(group, provider) {
|
|
53
55
|
if (!provider) return { model: '', effort: '' }
|
|
54
|
-
const model = group
|
|
55
|
-
(provider.runner === 'codex' ? '' : provider.env?.ANTHROPIC_MODEL) || ''
|
|
56
|
+
const { model } = modelContext(group, provider)
|
|
56
57
|
const effort = provider.runner === 'codex'
|
|
57
58
|
? provider.reasoningEffort : provider.env?.CLAUDE_CODE_EFFORT_LEVEL
|
|
58
59
|
const clean = value => String(value || '').replace(/\s+/g, ' ').trim()
|
|
@@ -224,10 +225,11 @@ function v2QueueSelection(queue) {
|
|
|
224
225
|
function releaseSkillLines(platform) {
|
|
225
226
|
const features = manifestFeatures({ ...(platform?.features || {}),
|
|
226
227
|
boards: false, imagen: false, takeoverCard: false })
|
|
227
|
-
const lines = manifestSkills({ features,
|
|
228
|
+
const lines = manifestSkills({ features: { ...features, localAnswers: true },
|
|
228
229
|
platformName: String(platform?.chatLabel || '飞书群').replace(/群$/, '') })
|
|
229
230
|
const cron = lines.findIndex(line => line.startsWith('【定时任务】'))
|
|
230
|
-
return cron < 0 ? lines : lines.slice(0, cron)
|
|
231
|
+
return (cron < 0 ? lines : lines.slice(0, cron))
|
|
232
|
+
.map(line => line.replace(/sbot feishu/g, `${runtimeCommand()} feishu`))
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
// The group skills call `sbot feishu`. A machine can carry another `sbot` on
|
|
@@ -256,7 +258,7 @@ class Engine {
|
|
|
256
258
|
durableV2NormalRoute = true, durableV2StreamFrames = false,
|
|
257
259
|
durableV2AcquireCapacity = null, durableV2SealCapacity = null,
|
|
258
260
|
durableV2SwitchProvider = null, durableV2UsageProbe = null, durableV2Issues = null,
|
|
259
|
-
durableV2Version = '' } = options
|
|
261
|
+
durableV2Version = '', durableV2DataDir = '', durableV2ConsoleEnabled = false } = options
|
|
260
262
|
const sync = f => typeof f === 'function' && f.constructor?.name !== 'AsyncFunction'
|
|
261
263
|
if (!cfg || !cfg.agent || !cfg.groups || !cfg.providers ||
|
|
262
264
|
options.receiptJournal || options.unifiedStepHost ||
|
|
@@ -289,13 +291,21 @@ class Engine {
|
|
|
289
291
|
durableV2StartTurn, durableV2ApplyLocalEffect, durableV2VerifyRunnerClosed,
|
|
290
292
|
durableV2AssertWriterLock, durableV2Journal, durableV2NormalRoute,
|
|
291
293
|
durableV2StreamFrames, durableV2AcquireCapacity, durableV2SealCapacity,
|
|
292
|
-
durableV2SwitchProvider, durableV2UsageProbe, durableV2Issues, durableV2Version
|
|
294
|
+
durableV2SwitchProvider, durableV2UsageProbe, durableV2Issues, durableV2Version,
|
|
295
|
+
durableV2DataDir, durableV2ConsoleEnabled })
|
|
293
296
|
this.projectMemory = options.projectMemory || null
|
|
294
297
|
this.repoInfo = options.repoInfo || null
|
|
298
|
+
this.activity = options.activity || null
|
|
295
299
|
this.tz = normalizeTz(cfg.timezone)
|
|
296
300
|
this.rts = new Map()
|
|
297
301
|
}
|
|
298
302
|
|
|
303
|
+
// Read-only diagnostics must never change admission, delivery or settlement.
|
|
304
|
+
_observeActivity(method, value) {
|
|
305
|
+
try { this.activity?.[method]?.(value) }
|
|
306
|
+
catch { this.log('Activity display update unavailable') }
|
|
307
|
+
}
|
|
308
|
+
|
|
299
309
|
shutdown() {
|
|
300
310
|
this.durableV2Runtime.close()
|
|
301
311
|
let aborted = 0
|
|
@@ -426,6 +436,8 @@ baseSkillEnv(chatId) {
|
|
|
426
436
|
const g = (this.cfg.groups || {})[chatId]
|
|
427
437
|
const bin = releaseBinDir()
|
|
428
438
|
return { ...(bin ? { PATH: [bin, process.env.PATH].filter(Boolean).join(path.delimiter) } : {}),
|
|
439
|
+
SBOT_RUNTIME_ENTRY: RUNTIME_ENTRY, SBOT_RUNTIME_VERSION: this.durableV2Version || '',
|
|
440
|
+
...(this.durableV2DataDir ? { SBOT_DATA_DIR: this.durableV2DataDir } : {}),
|
|
429
441
|
SBOT_CHAT_ID: chatId, ...(g && g.platform ? { SBOT_PLATFORM: g.platform } : {}), TZ: this.tz, ...(this.cfg._path ? { SBOT_CONFIG: this.cfg._path } : {}), ...(this.uiLocalUrl ? { SBOT_UI_URL: this.uiLocalUrl } : {}) }
|
|
430
442
|
}
|
|
431
443
|
|
|
@@ -456,7 +468,7 @@ async _durableV2Command(msg, cmd, rest) {
|
|
|
456
468
|
if (!group || group.paused) return reject('unsupported-command', T.unsupportedCommandWithModel)
|
|
457
469
|
const { name, provider } = this._resolveProvider(msg.chatId, group)
|
|
458
470
|
// A group may pin its own model; the runner uses it before the provider default.
|
|
459
|
-
const label = (n, p) => (group
|
|
471
|
+
const label = (n, p) => { const { model } = modelContext(group, p); return model ? `${n} · ${model}` : n }
|
|
460
472
|
if (cmd === '/model') return this._durableV2ModelCommand(msg, rest, group)
|
|
461
473
|
if (cmd === '/help') {
|
|
462
474
|
const isCodex = provider?.runner === 'codex'
|
|
@@ -525,12 +537,15 @@ async _durableV2Command(msg, cmd, rest) {
|
|
|
525
537
|
_durableV2ModelCommand(msg, want, group) {
|
|
526
538
|
const reject = (reason, notice) =>
|
|
527
539
|
rejectC2Input('C2 v2 model command answered before receipt', { reason, notice })
|
|
528
|
-
const label = (name, provider
|
|
540
|
+
const label = (name, provider, scope = {}) => {
|
|
541
|
+
const { model } = modelContext(scope, provider)
|
|
542
|
+
return model ? `${name} · ${model}` : name
|
|
543
|
+
}
|
|
529
544
|
const available = Object.entries(this.cfg.providers || {})
|
|
530
545
|
.filter(([, provider]) => provider && !provider.hidden &&
|
|
531
|
-
['claude', 'codex'].includes(provider.runner))
|
|
546
|
+
['claude', 'codex'].includes(provider.runner || 'claude'))
|
|
532
547
|
const { name: currentName, provider: currentProvider } = this._resolveProvider(msg.chatId, group)
|
|
533
|
-
const current = label(currentName, currentProvider)
|
|
548
|
+
const current = label(currentName, currentProvider, group)
|
|
534
549
|
const list = available.map(([name, provider]) => label(name, provider)).join(' / ')
|
|
535
550
|
if (!want) {
|
|
536
551
|
const example = available.find(([name]) => name !== currentName)?.[0] || currentName
|
|
@@ -741,6 +756,7 @@ async runDurableV2Message(msg, platform) {
|
|
|
741
756
|
} catch (error) {
|
|
742
757
|
if (controller.signal.aborted && !rt.v2DispatchEffect &&
|
|
743
758
|
error?.code === 'C2_CAPACITY_WAIT_CANCELLED') {
|
|
759
|
+
this._observeActivity('unknown', { chatId: msg.chatId })
|
|
744
760
|
return { receiptId, status: 'interruptedBeforeDispatch',
|
|
745
761
|
queued: true, ingressClosed: true }
|
|
746
762
|
}
|
|
@@ -763,6 +779,7 @@ async runDurableV2Message(msg, platform) {
|
|
|
763
779
|
// siblings must not dispatch in the microtask before the service sees
|
|
764
780
|
// this rejection and begins its process-wide stop.
|
|
765
781
|
failed = true
|
|
782
|
+
this._observeActivity('unknown', { chatId: msg.chatId })
|
|
766
783
|
this.durableV2SealCapacity?.(error)
|
|
767
784
|
throw error
|
|
768
785
|
} finally {
|
|
@@ -823,6 +840,7 @@ async _runDurableV2Steer(rt, receiptId) {
|
|
|
823
840
|
invoke: () => control.steerDurable({ msgId: job.msg.msgId, text }) })
|
|
824
841
|
entry.accepted = accepted
|
|
825
842
|
if (accepted) {
|
|
843
|
+
try { this.activity?.merge(chatId, identity.parentReceiptId, [receiptId], entry.landed ? 'landed' : 'accepted') } catch {}
|
|
826
844
|
if (entry.landed) {
|
|
827
845
|
authority.commitSteerObservation({ ...identity, type: 'landed' })
|
|
828
846
|
}
|
|
@@ -1100,6 +1118,7 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1100
1118
|
}
|
|
1101
1119
|
const started = await this.startDurableV2Runner(identity)
|
|
1102
1120
|
if (started.startFailureRecovered === true) {
|
|
1121
|
+
this._observeActivity('finish', { ...identity, status: 'not-started', err: '任务未启动,已通知群内' })
|
|
1103
1122
|
settled.push({ receiptId: identity.receiptId, status: 'startupNotified' })
|
|
1104
1123
|
return { receiptId: firstReceiptId, status: settled[0].status,
|
|
1105
1124
|
settledReceiptIds: settled.map(item => item.receiptId),
|
|
@@ -1170,6 +1189,8 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1170
1189
|
}
|
|
1171
1190
|
for (const settledReceiptId of cancelled.cancelledReceiptIds ||
|
|
1172
1191
|
[identity.receiptId]) {
|
|
1192
|
+
this._observeActivity('finish', { chatId: identity.chatId, receiptId: settledReceiptId,
|
|
1193
|
+
status: control === 'recall' ? 'recalled' : 'stopped' })
|
|
1173
1194
|
settled.push({ receiptId: settledReceiptId,
|
|
1174
1195
|
status: control === 'recall' ? 'recalled' : 'stopped',
|
|
1175
1196
|
terminalEffectId: null, cancellationEventId: cancelled.eventId })
|
|
@@ -1230,6 +1251,8 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1230
1251
|
rt.v2TurnToken = null
|
|
1231
1252
|
for (const settledReceiptId of released.settledReceiptIds ||
|
|
1232
1253
|
[identity.receiptId]) {
|
|
1254
|
+
this._observeActivity('finish', { chatId: identity.chatId, receiptId: settledReceiptId,
|
|
1255
|
+
status: 'timeout', err: '任务超过运行时限,已停止' })
|
|
1233
1256
|
settled.push({ receiptId: settledReceiptId,
|
|
1234
1257
|
status: 'timeout', terminalEffectId: null,
|
|
1235
1258
|
cancellationEventId: released.eventId })
|
|
@@ -1361,6 +1384,9 @@ stageDurableV2Message(msg, platform) {
|
|
|
1361
1384
|
const rt = this._rt(msg.chatId)
|
|
1362
1385
|
if (!rt.v2QueueReceiptIds) rt.v2QueueReceiptIds = []
|
|
1363
1386
|
rt.v2QueueReceiptIds.push(receipt.id)
|
|
1387
|
+
this._observeActivity('receive', { chatId: msg.chatId, receiptId: receipt.id,
|
|
1388
|
+
sourceMessageId: msg.msgId,
|
|
1389
|
+
text: msg.text, sender: msg.senderName || '', acceptedAt })
|
|
1364
1390
|
return { receiptId: receipt.id, cursor: result.cursor, staged: true,
|
|
1365
1391
|
ingressClosed: !!authority.blocked }
|
|
1366
1392
|
}
|
|
@@ -1534,6 +1560,9 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1534
1560
|
throw new Error('C2 v2 runner provider unavailable')
|
|
1535
1561
|
}
|
|
1536
1562
|
rt.v2CardModelInfo = cardModelInfo(job.group, provider)
|
|
1563
|
+
this._observeActivity('start', { chatId, receiptId, receiptIds: cohort.receiptIds, generation,
|
|
1564
|
+
sessionId: this.store.getSession(chatId), model: rt.v2CardModelInfo.model,
|
|
1565
|
+
runner: provider.runner, project: projectName(job.group) })
|
|
1537
1566
|
rt.v2FollowupEnabled = followupEnabled(job.group, provider.runner)
|
|
1538
1567
|
rt.v2SteerControl = null
|
|
1539
1568
|
rt.v2SteerEntries = new Map()
|
|
@@ -1579,6 +1608,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1579
1608
|
if (rt.v2CardBinding?.opened) await rt.v2CardBinding.path.flushForTerminal()
|
|
1580
1609
|
if (rt.v2RunnerGeneration !== generation) return
|
|
1581
1610
|
authority.commitSteerObservation({ ...steerIdentity, receiptId: entry.identity.receiptId, type: 'landed' })
|
|
1611
|
+
try { this.activity?.merge(chatId, receiptId, [entry.identity.receiptId], 'landed') } catch {}
|
|
1582
1612
|
await rt.v2MoveCard?.(entry)
|
|
1583
1613
|
})
|
|
1584
1614
|
rt.v2SteerLandedChain = observation
|
|
@@ -1757,18 +1787,22 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1757
1787
|
(!Number.isSafeInteger(timeoutDeadlineAt) || timeoutDeadlineAt <= 0)) {
|
|
1758
1788
|
throw new Error('C2 v2 hard timeout deadline invalid')
|
|
1759
1789
|
}
|
|
1760
|
-
|
|
1761
|
-
? setTimeout(() => {
|
|
1790
|
+
const onHardDeadline = () => {
|
|
1762
1791
|
const elapsedAt = Date.now()
|
|
1763
1792
|
if (rt.v2RunnerGeneration !== generation ||
|
|
1764
1793
|
rt.v2Controller !== controller || rt.v2CancelControl ||
|
|
1765
1794
|
rt.v2StreamQuiescing) return
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1795
|
+
// Timers and Date.now need not tick together. An early callback (or
|
|
1796
|
+
// a backward wall-clock adjustment) is not proof of elapsed time.
|
|
1797
|
+
if (elapsedAt < timeoutDeadlineAt) {
|
|
1798
|
+
capTimer = setTimeout(onHardDeadline, timeoutDeadlineAt - elapsedAt)
|
|
1799
|
+
return
|
|
1769
1800
|
}
|
|
1801
|
+
rt.v2TimeoutProof = { receiptId, turnToken, generation,
|
|
1802
|
+
deadlineAt: timeoutDeadlineAt, elapsedAt }
|
|
1770
1803
|
controller.abort()
|
|
1771
|
-
|
|
1804
|
+
}
|
|
1805
|
+
capTimer = timeoutDeadlineAt !== null ? setTimeout(onHardDeadline, timeoutMs) : null
|
|
1772
1806
|
const input = prepareC2RunnerInput({ prompt,
|
|
1773
1807
|
appendSystemPrompt: SYSTEM_APPENDIX, msg: job.msg, group: job.group,
|
|
1774
1808
|
provider, config: this.cfg,
|
|
@@ -1777,6 +1811,12 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1777
1811
|
onDurableEvent: event => {
|
|
1778
1812
|
projection.accept(event)
|
|
1779
1813
|
enqueueFrame(event)
|
|
1814
|
+
this._observeActivity('observe', { chatId, receiptId, generation, event,
|
|
1815
|
+
doing: rt.v2Doing, milestones: event.text?.includes('>>') ? progress.milestonesOf(projection.streamSnapshot().bufferText) : undefined })
|
|
1816
|
+
},
|
|
1817
|
+
onEvent: event => {
|
|
1818
|
+
if (event.type === 'system' && event.subtype === 'context')
|
|
1819
|
+
this._observeActivity('observe', { chatId, receiptId, generation, event })
|
|
1780
1820
|
},
|
|
1781
1821
|
onTurnAccepted: value => {
|
|
1782
1822
|
const ack = capability.onTurnAccepted(value)
|
|
@@ -2049,8 +2089,14 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
2049
2089
|
...progress.milestonesOf(payload.bufferText)]
|
|
2050
2090
|
.filter((m, i, a) => a.indexOf(m) === i).map(redactMilestone)
|
|
2051
2091
|
.filter(m => typeof m === 'string' && m.trim())
|
|
2092
|
+
this._observeActivity('sending', { chatId, receiptId })
|
|
2052
2093
|
const completed = await path.complete({ payload, replyText, paused,
|
|
2053
2094
|
footer, footerDetail, milestones })
|
|
2095
|
+
this._observeActivity('finish', { chatId, receiptId,
|
|
2096
|
+
status: quotaFailure || closedFailure ? 'error' : payload.quiet || projected.display.silent ? 'silent' : 'answered',
|
|
2097
|
+
reply: redact(quotaFailure ? T.quotaFailure : replyText || '', { pii: job.group?.redactPii !== false }),
|
|
2098
|
+
err: quotaFailure ? T.quotaFailure : closedFailure ? terminalReplyText(payload) : '',
|
|
2099
|
+
tokens: rt.v2UsageDecision?.tokens || null, sessionId: projected.display.sessionId })
|
|
2054
2100
|
const after = authority.snapshot()
|
|
2055
2101
|
const queue = after.state?.lanes?.queue?.[chatId]?.queue
|
|
2056
2102
|
let successor = null
|
|
@@ -2180,21 +2226,25 @@ classifyDurableV2UnresolvedTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
2180
2226
|
const policy = manifestPolicy(group.permissionMode, provider.runner === 'codex',
|
|
2181
2227
|
group.allowedTools, group.disallowedTools)
|
|
2182
2228
|
const brand = this.cfg.brand || 'SintBot'
|
|
2229
|
+
const groupTools = group.platform === 'feishu' && manifestFeatures(platform.features || {}).skills
|
|
2183
2230
|
const info = group.repo && this.repoInfo ? this.repoInfo(group.workdir, group.repo) : null
|
|
2184
2231
|
const manifest = [
|
|
2185
2232
|
...manifestHeader({ restrictedV2: true, brand, chatLabel: platform.chatLabel || '项目群',
|
|
2186
2233
|
chatId: msg.chatId, groupName: group.name || msg.chatId,
|
|
2187
2234
|
workdir: group.workdir, repo: group.repo || '', repoState: info?.state || '',
|
|
2188
2235
|
repoOrigin: info?.origin || '未知', pushDenied: policy.pushDenied,
|
|
2189
|
-
providerName, model: group
|
|
2236
|
+
providerName, model: modelContext(group, provider).model,
|
|
2237
|
+
runtimeVersion: this.durableV2Version || '', runtimeCommand: runtimeCommand(),
|
|
2190
2238
|
noVision: provider.vision === false, platformName: platform.chatLabel || '项目群',
|
|
2191
2239
|
permissionTier: policy.tier, tz: this.tz, publicUrl: '', uiUrl: '', lanUrl: '' }),
|
|
2192
2240
|
...manifestContext({ restrictedV2: true, brand, isCodex: provider.runner === 'codex',
|
|
2241
|
+
groupCommands: GROUP_COMMANDS, groupTools,
|
|
2242
|
+
consoleEnabled: this.durableV2ConsoleEnabled === true,
|
|
2193
2243
|
pmem: this.projectMemory ? this.projectMemory(group.workdir,
|
|
2194
2244
|
{ full: policy.full, isCodex: provider.runner === 'codex' }) : null,
|
|
2195
2245
|
unredactedPii: group.redactPii === false,
|
|
2196
2246
|
cardPinned: platform.features?.cardPinned === true, plainSend: false }),
|
|
2197
|
-
...releaseSkillLines(platform),
|
|
2247
|
+
...(groupTools ? releaseSkillLines(platform) : []),
|
|
2198
2248
|
].join('\n')
|
|
2199
2249
|
// Reinject current supported context each turn. No legacy feature menu,
|
|
2200
2250
|
// external-message commands or v1 manifest refresh scheduler is included.
|
|
@@ -2269,6 +2319,7 @@ requestDurableV2Stop(chatId) {
|
|
|
2269
2319
|
payloadHandles: this.durableV2Payloads,
|
|
2270
2320
|
assertWriterLock: this.durableV2AssertWriterLock })
|
|
2271
2321
|
rt.v2QueueReceiptIds = rt.v2QueueReceiptIds.filter(id => id !== receiptId)
|
|
2322
|
+
this._observeActivity('finish', { chatId, receiptId, status: 'recalled' })
|
|
2272
2323
|
return true
|
|
2273
2324
|
} catch (error) { authority?.close(); this.log(error.message); return false }
|
|
2274
2325
|
}
|
package/src/feishucli.js
CHANGED
|
@@ -20,7 +20,7 @@ const { redact } = require('./core/redact')
|
|
|
20
20
|
const { fmtStamp, normalizeTz } = require('./core/time')
|
|
21
21
|
|
|
22
22
|
const FEISHU_MESSAGE_ACTIONS = Object.freeze(['send-msg', 'send-notice', 'send-file', 'send-image',
|
|
23
|
-
'history', 'reply', 'edit', 'recall', 'delete', 'download', 'react'])
|
|
23
|
+
'history', 'answers', 'reply', 'edit', 'recall', 'delete', 'download', 'react'])
|
|
24
24
|
|
|
25
25
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`
|
|
26
26
|
const green = (s) => `\x1b[32m${s}\x1b[0m`
|
|
@@ -44,6 +44,8 @@ function parseFeishuArgs(action, rawArgs) {
|
|
|
44
44
|
else if (a.startsWith('--chat=')) o.chat = a.slice(7)
|
|
45
45
|
else if (a === '--config') o.config = rawArgs[++i]
|
|
46
46
|
else if (a.startsWith('--config=')) o.config = a.slice(9)
|
|
47
|
+
else if (a === '--data-dir') o.dataDir = rawArgs[++i]
|
|
48
|
+
else if (a.startsWith('--data-dir=')) o.dataDir = a.slice(11)
|
|
47
49
|
else if (action === 'send-notice' && a === '--sub') o.subtitle = rawArgs[++i]
|
|
48
50
|
else if (action === 'send-notice' && a === '--tone') o.tone = rawArgs[++i]
|
|
49
51
|
else if (action === 'send-notice' && a === '--warn') o.warnings.push(rawArgs[++i])
|
|
@@ -102,6 +104,15 @@ async function runFeishuAction(args, deps = {}) {
|
|
|
102
104
|
const out = deps.stdout || ((s) => process.stdout.write(s))
|
|
103
105
|
const err = deps.stderr || ((s) => process.stderr.write(s))
|
|
104
106
|
const p = opts.positionals
|
|
107
|
+
if (action === 'answers') {
|
|
108
|
+
if (!cfg.groups?.[chatId]) refuse('目标群没有绑定,不能读取本机答复记录')
|
|
109
|
+
const { readRecordedAnswers } = require('./recordedanswers')
|
|
110
|
+
const result = readRecordedAnswers({ dataDir: opts.dataDir || (deps.env || process.env).SBOT_DATA_DIR,
|
|
111
|
+
chatId, id: p[0] || '' })
|
|
112
|
+
out(JSON.stringify(result, null, 2) + '\n')
|
|
113
|
+
if (!result.answers.length) err('未找到本群匹配记录;可能已过保留范围或尚未落盘,不能推断原卡正文。\n')
|
|
114
|
+
return result
|
|
115
|
+
}
|
|
105
116
|
// Usage errors before any network call, worded as the legacy entry did.
|
|
106
117
|
let notice
|
|
107
118
|
if (action === 'send-msg' && !p.join(' ').trim()) refuse('缺少消息内容 — sbot feishu send-msg "<文本>"')
|
|
@@ -200,6 +211,7 @@ function feishuUsage(cmd = 'sintbot') {
|
|
|
200
211
|
+ ' send-notice "<标题>" "<正文>" 发群通知卡(可加 --sub / --tone / --warn / --from)\n'
|
|
201
212
|
+ ' send-file <路径> · send-image <路径> 发文件 / 图片\n'
|
|
202
213
|
+ ' history [n] 拉最近 n 条(默认 20,最多 50):每条带 message_id,并标 →@某某·未@bot / →@bot\n'
|
|
214
|
+
+ ' answers [消息或轮次ID] 只读本群本机答复记录;默认最近 10 轮,不代表完整飞书历史\n'
|
|
203
215
|
+ ' reply <message_id> "<文本>" 回复某条\n'
|
|
204
216
|
+ ' edit <message_id> "<文本>" 改自己发过的消息\n'
|
|
205
217
|
+ ' recall <message_id> 撤回自己发过的消息(别名 delete)\n'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Configuration projection shared by chat, cards and the console. A configured
|
|
4
|
+
// model is not proof of what an executor ultimately selected or fell back to.
|
|
5
|
+
function modelContext(group = {}, provider = {}) {
|
|
6
|
+
group ||= {}; provider ||= {}
|
|
7
|
+
const environment = provider.runner === 'codex' ? '' : provider.env?.ANTHROPIC_MODEL
|
|
8
|
+
const model = group.model || provider.model || environment || ''
|
|
9
|
+
return { model, source: group.model ? 'group' : provider.model ? 'provider' : environment ? 'environment' : 'default',
|
|
10
|
+
actualModel: null }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { modelContext }
|
package/src/platform/feishu.js
CHANGED
|
@@ -181,9 +181,10 @@ function createCardWriter({
|
|
|
181
181
|
client, cardId, elementId = 'answer', log = () => {}, now = () => Date.now(),
|
|
182
182
|
throttleMs = 0, streamWindowMs = STREAM_WINDOW_MS, setTimer = setTimeout, clearTimer = clearTimeout,
|
|
183
183
|
operationBoundary = null, operationKey = null,
|
|
184
|
+
initialText = '', initialSequence = 1,
|
|
184
185
|
} = {}) {
|
|
185
186
|
const openedAt = now()
|
|
186
|
-
let seq =
|
|
187
|
+
let seq = initialSequence // Includes any confirmed settings call before this writer opens.
|
|
187
188
|
let streamOpen = true // 飞书侧流式模式还开着吗
|
|
188
189
|
// ⚠ 「已收尾」与「已冻结」是两件事,合成一个标记会吃掉一条兜底路径:撤回失败时调用方
|
|
189
190
|
// 要退一句中性收尾(engine 的 dropAckCard),而 recall 若把 finish 一并封死,那张卡就
|
|
@@ -195,8 +196,9 @@ function createCardWriter({
|
|
|
195
196
|
let pushes = 0 // 流式帧 cardElement.content
|
|
196
197
|
let fulls = 0 // 整卡更新 card.update
|
|
197
198
|
let closes = 0 // 关流式 card.settings
|
|
198
|
-
|
|
199
|
-
let
|
|
199
|
+
const resumes = initialText ? 1 : 0 // Confirmed pre-writer streaming enable.
|
|
200
|
+
let lastSeen = initialText // Seed only after the snapshot's create/post have been confirmed.
|
|
201
|
+
let confirmedText = initialText || null // 只记录平台确认过的正文,不能把已派发当成已送达
|
|
200
202
|
let chain = Promise.resolve()
|
|
201
203
|
let finishResult = null
|
|
202
204
|
let boundaryError = null
|
|
@@ -363,7 +365,7 @@ function createCardWriter({
|
|
|
363
365
|
// ⚠ 这行必须打在**收尾之后**:收尾自己那一次调用(整卡更新 / 关流式)就发生在这个
|
|
364
366
|
// 回调里。原先它打在回调外、且只报节流器的派发次数 ⇒ 降级后的整卡更新与收尾那次
|
|
365
367
|
// 全在账外(实测漏计 ≈44 次/天)。注释写着「配额账可实测」,那就得真能对上账。
|
|
366
|
-
log(`card ops: ${pushes + fulls + closes} 次调用(流式 ${pushes} · 整卡 ${fulls} · 关流式 ${closes})`)
|
|
368
|
+
log(`card ops: ${pushes + fulls + closes + resumes} 次调用(流式 ${pushes} · 整卡 ${fulls} · 关流式 ${closes}${resumes ? ` · 恢复流式 ${resumes}` : ''})`)
|
|
367
369
|
}
|
|
368
370
|
}, { terminal: true })
|
|
369
371
|
return finishResult
|
|
@@ -371,7 +373,7 @@ function createCardWriter({
|
|
|
371
373
|
// recall:先断掉后续流式帧再去删消息。⚠ 只冻结不收尾 —— 撤不掉时调用方还要退一句中性收尾。
|
|
372
374
|
markClosed() { frozen = true; flushedRevision = -1 },
|
|
373
375
|
settle: () => chain.then((result) => { if (boundaryError) throw boundaryError; return result }),
|
|
374
|
-
stats: () => ({ pushes, fullUpdates: fulls, closes, calls: pushes + fulls + closes, streamOpen, seq }),
|
|
376
|
+
stats: () => ({ pushes, fullUpdates: fulls, closes, calls: pushes + fulls + closes + resumes, streamOpen, seq }),
|
|
375
377
|
}
|
|
376
378
|
}
|
|
377
379
|
|
|
@@ -858,9 +860,10 @@ function createFeishuPlatform({ appId, appSecret, botOpenId = null, onlyWhenMent
|
|
|
858
860
|
// effect. `finish` flips streaming_mode off so the card settles.
|
|
859
861
|
// All ops are serialized so the per-card `sequence` stays strictly increasing.
|
|
860
862
|
async createStreamingCard(replyCtx, { title = 'sbot', operationBoundary = null,
|
|
861
|
-
operationKey = null } = {}) {
|
|
863
|
+
operationKey = null, initialText = '' } = {}) {
|
|
862
864
|
const chatId = replyCtx && replyCtx.chatId
|
|
863
865
|
if (!chatId) return null
|
|
866
|
+
if (typeof initialText !== 'string') throw new TypeError('card initial text must be a string')
|
|
864
867
|
if (operationBoundary && (typeof operationBoundary !== 'function' ||
|
|
865
868
|
typeof operationKey !== 'string' || !operationKey ||
|
|
866
869
|
Buffer.byteLength(operationKey) > 256)) {
|
|
@@ -870,8 +873,8 @@ function createFeishuPlatform({ appId, appSecret, botOpenId = null, onlyWhenMent
|
|
|
870
873
|
// No header — the reply card shows just the answer body (no "sbot" title line).
|
|
871
874
|
const cardJson = {
|
|
872
875
|
schema: '2.0',
|
|
873
|
-
config: { streaming_mode:
|
|
874
|
-
body: { elements: [{ tag: 'markdown', content:
|
|
876
|
+
config: { streaming_mode: !initialText, summary: { content: '' }, streaming_config: { print_strategy: 'fast' } },
|
|
877
|
+
body: { elements: [{ tag: 'markdown', content: initialText, element_id: ELEM }] },
|
|
875
878
|
}
|
|
876
879
|
|
|
877
880
|
let cardId
|
|
@@ -900,6 +903,22 @@ function createFeishuPlatform({ appId, appSecret, botOpenId = null, onlyWhenMent
|
|
|
900
903
|
const posted = await postCard(client, chatId, replyCtx && replyCtx.messageId, cardId,
|
|
901
904
|
{ operationBoundary, operationKey })
|
|
902
905
|
msgId = posted
|
|
906
|
+
// A moved card is first delivered with its complete, static snapshot.
|
|
907
|
+
// Enable streaming only afterward: old text never enters the typewriter
|
|
908
|
+
// endpoint, and the next cumulative frame continues from this baseline.
|
|
909
|
+
if (initialText) {
|
|
910
|
+
const sequence = 1
|
|
911
|
+
const uuid = crypto.randomUUID()
|
|
912
|
+
await cardOperation(operationBoundary, operationKey, 'card.settings',
|
|
913
|
+
{ cardId, sequence, uuid }, async () => {
|
|
914
|
+
const response = assertCardOk(await client.cardkit.v1.card.settings({
|
|
915
|
+
path: { card_id: cardId },
|
|
916
|
+
data: { settings: JSON.stringify({ config: { streaming_mode: true } }), sequence, uuid },
|
|
917
|
+
}), '恢复流式')
|
|
918
|
+
if (response?.code !== 0) throw new Error('恢复流式未确认:未返回 code=0')
|
|
919
|
+
return response
|
|
920
|
+
})
|
|
921
|
+
}
|
|
903
922
|
} catch (e) {
|
|
904
923
|
log('feishu card create error: ' + (e && e.message))
|
|
905
924
|
if (operationBoundary) throw e
|
|
@@ -908,7 +927,8 @@ function createFeishuPlatform({ appId, appSecret, botOpenId = null, onlyWhenMent
|
|
|
908
927
|
|
|
909
928
|
// 全部写操作交给写入器(流式窗口判断、序号分配、错误码检查都在它一处)。
|
|
910
929
|
const writer = createCardWriter({ client, cardId, elementId: ELEM, log,
|
|
911
|
-
throttleMs: cardThrottleMs, operationBoundary, operationKey
|
|
930
|
+
throttleMs: cardThrottleMs, operationBoundary, operationKey,
|
|
931
|
+
initialText, initialSequence: initialText ? 2 : 1 })
|
|
912
932
|
|
|
913
933
|
return {
|
|
914
934
|
durableTarget() {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const { redact } = require('./core/redact')
|
|
5
|
+
|
|
6
|
+
// Read only the bounded observation file. Never open/rewrite execution journals
|
|
7
|
+
// or infer that a stored draft reached Feishu.
|
|
8
|
+
function readRecordedAnswers({ dataDir, chatId, id = '' }) {
|
|
9
|
+
if (!path.isAbsolute(dataDir || '') || !chatId) throw new Error('缺少当前服务的数据目录或群信息,无法读取本机答复记录')
|
|
10
|
+
const file = path.join(dataDir, 'activity-v1.json')
|
|
11
|
+
let data
|
|
12
|
+
try {
|
|
13
|
+
if (fs.statSync(file).size > 12 * 1024 * 1024) throw new Error('size')
|
|
14
|
+
data = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
15
|
+
if (data.version !== 1 || !Array.isArray(data.rows) || data.rows.length > 256) throw new Error('format')
|
|
16
|
+
} catch { throw new Error('本机答复记录不存在或不可读;不能据此判断机器人没有回复') }
|
|
17
|
+
const rows = data.rows.filter(row => row && row.chatId === chatId && typeof row.id === 'string' &&
|
|
18
|
+
(!id || row.id === id || Array.isArray(row.receiptIds) && row.receiptIds.includes(id) ||
|
|
19
|
+
Array.isArray(row.messages) && row.messages.some(message => message?.sourceMessageId === id)))
|
|
20
|
+
.sort((a, b) => (b.ts || 0) - (a.ts || 0)).slice(0, id ? 1 : 10)
|
|
21
|
+
return { source: 'local-activity-v1', retention: '全服务最多保留最近 256 轮;每轮答复最多 16384 字符;旧记录可能没有消息 ID',
|
|
22
|
+
chatId, answers: rows.map(row => ({ id: row.id, ts: row.ts,
|
|
23
|
+
status: row.status, deliveryConfirmed: row.status === 'answered',
|
|
24
|
+
truncated: !!row.truncated, reply: redact(String(row.reply || ''), { pii: false }).slice(0, 16384),
|
|
25
|
+
sourceMessageIds: (Array.isArray(row.messages) ? row.messages : []).map(message => message?.sourceMessageId).filter(id => typeof id === 'string') })) }
|
|
26
|
+
}
|
|
27
|
+
module.exports = { readRecordedAnswers }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Supported surfaces of the release host. These are facts passed to the core
|
|
4
|
+
// renderer, not an alternative implementation of core policy.
|
|
5
|
+
const GROUP_COMMANDS = Object.freeze(['/help', '/status', '/model', '/new', '/hud', '/issue', '/skip', '/stop'])
|
|
6
|
+
const RUNTIME_ENTRY = require('node:path').join(__dirname, '..', 'bin', 'sintbot-v2.js')
|
|
7
|
+
const quoteShell = value => "'" + String(value).replace(/'/g, "'\\''") + "'"
|
|
8
|
+
function runtimeCommand(entry = RUNTIME_ENTRY, node = process.execPath) {
|
|
9
|
+
return `${quoteShell(node)} ${quoteShell(entry)}`
|
|
10
|
+
}
|
|
11
|
+
module.exports = { GROUP_COMMANDS, RUNTIME_ENTRY, runtimeCommand }
|