@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0
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/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* - Versioned: a store written by a newer schema is refused instead of guessed at.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, openSync, closeSync, fsyncSync, unlinkSync } from 'node:fs'
|
|
17
17
|
import { dirname, join } from 'node:path'
|
|
18
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
19
|
+
import { LEGACY_CONTEXT, MAX_CONTEXTS, contextIdOk, contextRequestOk, contextFields, contextView } from './independent-context-state.mjs'
|
|
18
20
|
|
|
19
21
|
export const BOT_SESSION_SCHEMA = 1
|
|
20
22
|
export const MAX_MESSAGES = 2_000
|
|
@@ -28,17 +30,33 @@ const text = value => (typeof value === 'string' ? value : '')
|
|
|
28
30
|
const idOk = value => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,128}$/.test(value)
|
|
29
31
|
|
|
30
32
|
function emptySession(botId, now) {
|
|
31
|
-
return { schemaVersion: BOT_SESSION_SCHEMA, botId, createdAt: now, updatedAt: now, turns: 0, messages: [], truncated: false
|
|
33
|
+
return { schemaVersion: BOT_SESSION_SCHEMA, botId, createdAt: now, updatedAt: now, turns: 0, messages: [], truncated: false,
|
|
34
|
+
...contextFields({ createdAt: 0 }) }
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
function bounded(message) {
|
|
35
38
|
const content = text(message.content)
|
|
39
|
+
const model = message.modelSource
|
|
40
|
+
if (model !== undefined && (!model || model.kind !== 'model' ||
|
|
41
|
+
![model.provider, model.model].every(value => typeof value === 'string' && value.length > 0 && value.length <= 256 && !/[\x00-\x1f]/.test(value)))) fail('INVALID_BOT_MESSAGE_SOURCE')
|
|
42
|
+
if (message.toolCallId !== undefined && (typeof message.toolCallId !== 'string' || !message.toolCallId || message.toolCallId.length > 256)) fail('INVALID_BOT_TOOL_CALL_ID')
|
|
43
|
+
const calls = message.toolCalls
|
|
44
|
+
if (calls !== undefined && (!Array.isArray(calls) || calls.length > 8 || calls.some(call =>
|
|
45
|
+
!call || typeof call.id !== 'string' || !call.id || call.id.length > 256 || typeof call.name !== 'string' || !call.name || call.name.length > 128 ||
|
|
46
|
+
!call.args || typeof call.args !== 'object' || Array.isArray(call.args) || JSON.stringify(call.args).length > MAX_MESSAGE_CHARS) ||
|
|
47
|
+
new Set(calls.map(call => call.id)).size !== calls.length)) fail('INVALID_BOT_TOOL_CALLS')
|
|
36
48
|
return {
|
|
49
|
+
contextId: message.contextId ?? LEGACY_CONTEXT,
|
|
37
50
|
role: ROLES.includes(message.role) ? message.role : 'user',
|
|
38
51
|
content: content.length > MAX_MESSAGE_CHARS ? `${content.slice(0, MAX_MESSAGE_CHARS)}\n[truncated]` : content,
|
|
39
52
|
at: Number.isSafeInteger(message.at) ? message.at : 0,
|
|
40
53
|
...(typeof message.name === 'string' && message.name ? { name: message.name.slice(0, 128) } : {}),
|
|
41
54
|
...(message.toolCall ? { toolCall: message.toolCall } : {}),
|
|
55
|
+
...(typeof message.messageId === 'string' && message.messageId ? { messageId: message.messageId.slice(0, 256) } : {}),
|
|
56
|
+
...(model ? { modelSource: { kind: 'model', provider: model.provider, model: model.model } } : {}),
|
|
57
|
+
...(calls ? { toolCalls: structuredClone(calls) } : {}),
|
|
58
|
+
...(message.toolCallId ? { toolCallId: message.toolCallId } : {}),
|
|
59
|
+
...(message.isError === true ? { isError: true } : {}),
|
|
42
60
|
}
|
|
43
61
|
}
|
|
44
62
|
|
|
@@ -48,6 +66,7 @@ export function parseSession(value, botId) {
|
|
|
48
66
|
if (value.schemaVersion !== BOT_SESSION_SCHEMA) fail(value.schemaVersion > BOT_SESSION_SCHEMA ? 'BOT_SESSION_SCHEMA_NEWER' : 'INVALID_BOT_SESSION')
|
|
49
67
|
if (value.botId !== botId) fail('BOT_SESSION_MISMATCH')
|
|
50
68
|
if (!Array.isArray(value.messages)) fail('INVALID_BOT_SESSION')
|
|
69
|
+
if (value.messages.some(message => message.contextId !== undefined && !contextIdOk(message.contextId))) fail('INVALID_BOT_CONTEXT')
|
|
51
70
|
return {
|
|
52
71
|
schemaVersion: BOT_SESSION_SCHEMA,
|
|
53
72
|
botId,
|
|
@@ -55,7 +74,9 @@ export function parseSession(value, botId) {
|
|
|
55
74
|
updatedAt: Number.isSafeInteger(value.updatedAt) ? value.updatedAt : 0,
|
|
56
75
|
turns: Number.isSafeInteger(value.turns) && value.turns >= 0 ? value.turns : 0,
|
|
57
76
|
truncated: value.truncated === true,
|
|
58
|
-
|
|
77
|
+
...contextFields(value),
|
|
78
|
+
messages: value.messages.map((message, index) => bounded({ ...message,
|
|
79
|
+
messageId: message.messageId ?? `legacy-${createHash('sha256').update(JSON.stringify([botId, index, message])).digest('hex')}` })),
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
|
|
@@ -79,10 +100,32 @@ export function openBotSessionStore({ dir, now = Date.now, maxMessages = MAX_MES
|
|
|
79
100
|
const write = session => {
|
|
80
101
|
mkdirSync(dirname(fileOf(session.botId)), { recursive: true })
|
|
81
102
|
const target = fileOf(session.botId)
|
|
82
|
-
const temp = `${target}.${process.pid}.tmp`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`
|
|
104
|
+
let descriptor, renamed = false
|
|
105
|
+
try {
|
|
106
|
+
descriptor = openSync(temp, 'wx', 0o600)
|
|
107
|
+
writeFileSync(descriptor, JSON.stringify(session)); fsyncSync(descriptor)
|
|
108
|
+
closeSync(descriptor); descriptor = undefined
|
|
109
|
+
renameSync(temp, target); renamed = true
|
|
110
|
+
const verified = read(session.botId)
|
|
111
|
+
if (JSON.stringify(verified) !== JSON.stringify(parseSession(session, session.botId))) fail('BOT_SESSION_WRITE_NOT_CONFIRMED')
|
|
112
|
+
return verified
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// Do not overwrite a possibly committed file with an old snapshot.
|
|
115
|
+
error.outcome = renamed ? 'unknown' : 'refused'
|
|
116
|
+
throw error
|
|
117
|
+
} finally { if (descriptor !== undefined) closeSync(descriptor) }
|
|
118
|
+
}
|
|
119
|
+
// Exclusive filesystem admission covers the read-modify-rename in every process.
|
|
120
|
+
// A stale lock is refused; guessing that its owner died could overwrite a live writer.
|
|
121
|
+
const mutate = (botId, change) => {
|
|
122
|
+
const target = fileOf(botId)
|
|
123
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
124
|
+
let lock
|
|
125
|
+
try { lock = openSync(`${target}.lock`, 'wx', 0o600) }
|
|
126
|
+
catch (error) { if (error.code === 'EEXIST') fail('BOT_SESSION_WRITE_BUSY'); throw error }
|
|
127
|
+
try { return change(read(botId)) }
|
|
128
|
+
finally { closeSync(lock); unlinkSync(`${target}.lock`) }
|
|
86
129
|
}
|
|
87
130
|
|
|
88
131
|
return Object.freeze({
|
|
@@ -93,8 +136,12 @@ export function openBotSessionStore({ dir, now = Date.now, maxMessages = MAX_MES
|
|
|
93
136
|
/** Append messages and advance the turn counter atomically. */
|
|
94
137
|
append(botId, messages, { turn = false } = {}) {
|
|
95
138
|
if (!Array.isArray(messages) || messages.length === 0) fail('INVALID_BOT_SESSION_APPEND')
|
|
96
|
-
|
|
97
|
-
|
|
139
|
+
return mutate(botId, session => {
|
|
140
|
+
if (session.contextReset) fail('INDEPENDENT_CONTEXT_PENDING_RECONCILIATION')
|
|
141
|
+
for (const message of messages) session.messages.push(bounded({ ...message,
|
|
142
|
+
contextId: session.contextId,
|
|
143
|
+
...(!message.messageId ? { messageId: randomUUID() } : {}),
|
|
144
|
+
at: Number.isSafeInteger(message.at) ? message.at : now() }))
|
|
98
145
|
if (session.messages.length > maxMessages) {
|
|
99
146
|
session.messages.splice(0, session.messages.length - maxMessages)
|
|
100
147
|
session.truncated = true
|
|
@@ -102,6 +149,55 @@ export function openBotSessionStore({ dir, now = Date.now, maxMessages = MAX_MES
|
|
|
102
149
|
if (turn) session.turns += 1
|
|
103
150
|
session.updatedAt = now()
|
|
104
151
|
return write(session)
|
|
152
|
+
})
|
|
153
|
+
},
|
|
154
|
+
contextOf: botId => contextView(read(botId)),
|
|
155
|
+
beginContext(botId, { requestKey, contextId, expectedContextId } = {}) {
|
|
156
|
+
if (!contextRequestOk(requestKey) || !contextIdOk(contextId)) fail('INVALID_BOT_CONTEXT_RESET')
|
|
157
|
+
return mutate(botId, session => {
|
|
158
|
+
const existing = session.contexts.find(row => row.requestKey === requestKey)
|
|
159
|
+
if (existing) {
|
|
160
|
+
if (existing.contextId !== contextId) fail('INDEPENDENT_CONTEXT_REQUEST_CONFLICT')
|
|
161
|
+
return session
|
|
162
|
+
}
|
|
163
|
+
if (session.contextReset) fail('INDEPENDENT_CONTEXT_PENDING_RECONCILIATION')
|
|
164
|
+
if (contextId === session.contextId || session.messages.some(row => row.contextId === contextId)) fail('INDEPENDENT_CONTEXT_REQUEST_CONFLICT')
|
|
165
|
+
if (expectedContextId !== undefined && session.contextId !== expectedContextId) fail('INDEPENDENT_CONTEXT_STALE')
|
|
166
|
+
session.contextReset = { requestKey, contextId, previousContextId: session.contextId, createdAt: now() }
|
|
167
|
+
session.updatedAt = now()
|
|
168
|
+
return write(session)
|
|
169
|
+
})
|
|
170
|
+
},
|
|
171
|
+
commitContext(botId, { requestKey, contextId } = {}) {
|
|
172
|
+
return mutate(botId, session => {
|
|
173
|
+
const existing = session.contexts.find(row => row.requestKey === requestKey)
|
|
174
|
+
if (existing) {
|
|
175
|
+
if (existing.contextId !== contextId) fail('INDEPENDENT_CONTEXT_REQUEST_CONFLICT')
|
|
176
|
+
return session
|
|
177
|
+
}
|
|
178
|
+
const intent = session.contextReset
|
|
179
|
+
if (!intent || intent.requestKey !== requestKey || intent.contextId !== contextId) fail('INDEPENDENT_CONTEXT_REQUEST_CONFLICT')
|
|
180
|
+
const sequence = Math.max(...session.contexts.map(row => row.sequence)) + 1
|
|
181
|
+
if (!Number.isSafeInteger(sequence)) fail('INDEPENDENT_CONTEXT_HISTORY_LIMIT')
|
|
182
|
+
session.contexts.push({ ...intent, sequence, createdAt: now() })
|
|
183
|
+
if (session.contexts.length > MAX_CONTEXTS) { session.contexts = session.contexts.slice(-MAX_CONTEXTS); session.contextsTruncated = true }
|
|
184
|
+
session.contextId = contextId
|
|
185
|
+
delete session.contextReset
|
|
186
|
+
session.updatedAt = now()
|
|
187
|
+
return write(session)
|
|
188
|
+
})
|
|
189
|
+
},
|
|
190
|
+
abortContext(botId, { requestKey, contextId } = {}) {
|
|
191
|
+
return mutate(botId, session => {
|
|
192
|
+
if (!session.contextReset || session.contextReset.requestKey !== requestKey || session.contextReset.contextId !== contextId) fail('INDEPENDENT_CONTEXT_REQUEST_CONFLICT')
|
|
193
|
+
delete session.contextReset
|
|
194
|
+
session.updatedAt = now()
|
|
195
|
+
return write(session)
|
|
196
|
+
})
|
|
197
|
+
},
|
|
198
|
+
contextCommitted(botId, requestKey, contextId) {
|
|
199
|
+
const session = read(botId)
|
|
200
|
+
return session.contexts.some(row => row.requestKey === requestKey && row.contextId === contextId)
|
|
105
201
|
},
|
|
106
202
|
/** Forget one Bot's transcript (used when a Bot is deleted, never on a transient error). */
|
|
107
203
|
remove(botId) {
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { mkdirSync } from 'node:fs'
|
|
3
3
|
import { dirname, resolve } from 'node:path'
|
|
4
4
|
import { OFFICIAL_TOOLS } from '../../../.dsh-plugin/shared/official-tool-registry.mjs'
|
|
5
|
+
import { fundingForMainRoute, materializeMainRouteDefault, normalizeMainModelRoutes, preserveMainModelRoutes } from './main-model-routes.mjs'
|
|
5
6
|
|
|
6
7
|
export const BOT_SETTINGS_SCHEMA = 1
|
|
7
8
|
export const BOT_EXECUTION_MODES = Object.freeze(['read-only', 'workspace-write', 'cli-native'])
|
|
@@ -36,7 +37,7 @@ export function defaultBotSettings() {
|
|
|
36
37
|
|
|
37
38
|
/** Closed stored schema. No executable paths, installer URLs, credentials or arbitrary roots. */
|
|
38
39
|
export function normalizeBotSettings(input) {
|
|
39
|
-
keys(input, ['schemaVersion', 'enabled', 'model', 'officialTools', 'machineGrants', 'capabilities', 'workspaceRef', 'funding', 'maxConcurrentTasks', 'routing'], 'settings')
|
|
40
|
+
keys(input, ['schemaVersion', 'enabled', 'model', 'officialTools', 'machineGrants', 'capabilities', 'workspaceRef', 'funding', 'maxConcurrentTasks', 'routing', 'mainRoutes'], 'settings')
|
|
40
41
|
const value = { ...defaultBotSettings(), ...input }
|
|
41
42
|
if (value.schemaVersion !== 1) fail('BOT_SETTINGS_VERSION_UNSUPPORTED', 'Bot 绑定版本不受支持,请升级宿主。')
|
|
42
43
|
if (typeof value.enabled !== 'boolean') fail('BOT_SETTINGS_INVALID', 'enabled 必须是布尔值。', { field: 'enabled' })
|
|
@@ -82,8 +83,16 @@ export function normalizeBotSettings(input) {
|
|
|
82
83
|
if (!['local', 'remote'].includes(value.workspaceRef.kind) || !ref(value.workspaceRef.machineId) || !ref(value.workspaceRef.rootId)) fail('BOT_SETTINGS_INVALID', '工作区必须引用已登记机器和根目录。', { field: 'workspaceRef' })
|
|
83
84
|
value.workspaceRef = { kind: value.workspaceRef.kind, machineId: value.workspaceRef.machineId, rootId: value.workspaceRef.rootId }
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
value.funding = normalizeFunding(value.funding)
|
|
87
|
+
if (Object.hasOwn(input, 'mainRoutes')) value.mainRoutes = normalizeMainModelRoutes(input.mainRoutes, { normalizeFunding })
|
|
88
|
+
if (!Number.isSafeInteger(value.maxConcurrentTasks) || value.maxConcurrentTasks < 1 || value.maxConcurrentTasks > 32) fail('BOT_SETTINGS_INVALID', '最大并发任务数必须为 1 至 32。', { field: 'maxConcurrentTasks' })
|
|
89
|
+
json(value)
|
|
90
|
+
return clone(value)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeFunding(input) {
|
|
94
|
+
keys(input, ['mode', 'scopeId', 'pricing', 'request', 'dailyBudgetUsd', 'monthlyBudgetUsd', 'subscription'], 'funding')
|
|
95
|
+
const funding = { ...defaultBotSettings().funding, ...input }
|
|
87
96
|
if (!['api', 'subscription'].includes(funding.mode) || (funding.scopeId !== null && !ref(funding.scopeId))) fail('BOT_SETTINGS_INVALID', '请选择目录中的 API 或订阅资金范围。', { field: 'funding' })
|
|
88
97
|
if (funding.pricing !== null) {
|
|
89
98
|
keys(funding.pricing, ['input', 'output', 'cacheRead', 'cacheWrite'], 'funding.pricing')
|
|
@@ -101,17 +110,18 @@ export function normalizeBotSettings(input) {
|
|
|
101
110
|
funding.subscription = { dailyCalls: count(funding.subscription.dailyCalls, 'funding.subscription.dailyCalls'), monthlyCalls: count(funding.subscription.monthlyCalls, 'funding.subscription.monthlyCalls') }
|
|
102
111
|
}
|
|
103
112
|
if (funding.mode === 'api' && funding.subscription !== null) fail('BOT_SETTINGS_INVALID', 'API 资金不能携带订阅次数;不会隐式切换执行渠道。', { field: 'funding.subscription' })
|
|
104
|
-
|
|
105
|
-
if (!Number.isSafeInteger(value.maxConcurrentTasks) || value.maxConcurrentTasks < 1 || value.maxConcurrentTasks > 32) fail('BOT_SETTINGS_INVALID', '最大并发任务数必须为 1 至 32。', { field: 'maxConcurrentTasks' })
|
|
106
|
-
json(value)
|
|
107
|
-
return clone(value)
|
|
113
|
+
return funding
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
function catalogOf(input) {
|
|
111
117
|
if (!plain(input)) fail('BOT_SETTINGS_CATALOG_UNAVAILABLE', '宿主尚未提供可信模型、机器和资金目录。')
|
|
112
118
|
const models = list(input.models ?? [], 4096, 'catalog.models').map(item => {
|
|
113
119
|
if (!token(item.provider) || !token(item.model)) fail('BOT_SETTINGS_CATALOG_INVALID', '宿主模型目录无效。')
|
|
120
|
+
if (item.fundingModes !== undefined && (!Array.isArray(item.fundingModes) || !item.fundingModes.length || item.fundingModes.length > 2
|
|
121
|
+
|| item.fundingModes.some(mode => !['api', 'subscription'].includes(mode)) || new Set(item.fundingModes).size !== item.fundingModes.length)) fail('BOT_SETTINGS_CATALOG_INVALID', '宿主模型资金渠道目录无效。')
|
|
114
122
|
return { provider: item.provider, model: item.model, label: String(item.label ?? item.model).slice(0, 256),
|
|
123
|
+
...(item.fundingModes === undefined ? {} : { fundingModes: [...item.fundingModes] }),
|
|
124
|
+
...(item.pricing === undefined ? {} : { pricing: normalizeFunding({ pricing: item.pricing }).pricing }),
|
|
115
125
|
cliModels: (item.cliModels ?? []).filter(mapping => registry.has(mapping.toolId) && token(mapping.model)).map(mapping => ({ toolId: mapping.toolId, model: mapping.model })) }
|
|
116
126
|
})
|
|
117
127
|
unique(models.map(routeRef), 'catalog.models')
|
|
@@ -152,6 +162,7 @@ export function validateBotSettings(input, catalogInput) {
|
|
|
152
162
|
const denied = (message, field) => fail('BOT_SETTINGS_SCOPE_DENIED', message, { field })
|
|
153
163
|
const model = settings.model && catalog.models.find(item => routeRef(item) === routeRef(settings.model))
|
|
154
164
|
if (settings.model && !model) denied('此准确模型已不在宿主目录,请刷新后重新选择。', 'model')
|
|
165
|
+
if (model?.fundingModes && !model.fundingModes.includes(settings.funding.mode)) denied('此模型不支持所选资金渠道,请选择宿主列出的渠道。', 'funding.mode')
|
|
155
166
|
for (const tool of settings.officialTools) {
|
|
156
167
|
const entry = catalog.tools.find(item => item.toolId === tool.toolId)
|
|
157
168
|
if (!entry?.modes.includes(tool.mode)) denied(`${tool.toolId} 尚未获准使用该执行模式。`, 'officialTools')
|
|
@@ -187,10 +198,20 @@ function compiledGrant(settings, catalog) {
|
|
|
187
198
|
return { tools: [...new Set(tools)].sort(), modelRoutes: [{ ...settings.model }] }
|
|
188
199
|
}
|
|
189
200
|
|
|
201
|
+
export function checkMainRouteFunding(route, funding, catalog) {
|
|
202
|
+
const model = catalog.models.find(item => item.provider === route.provider && item.model === route.model)
|
|
203
|
+
const scope = catalog.fundingScopes.find(item => item.id === funding?.scopeId)
|
|
204
|
+
if (!funding || !model || (model.fundingModes && !model.fundingModes.includes(funding.mode))
|
|
205
|
+
|| !scope?.modes.includes(funding.mode) || (scope.models && !scope.models.some(item => item.provider === route.provider && item.model === route.model))) {
|
|
206
|
+
fail('MAIN_ROUTE_FUNDING_UNAVAILABLE', '此路线缺少可用模型、资金范围、渠道或完整限额。', { field: 'mainRoutes', route: routeRef(route) })
|
|
207
|
+
}
|
|
208
|
+
return funding
|
|
209
|
+
}
|
|
210
|
+
|
|
190
211
|
/** Isolated component tables, safe to colocate with other SQLite components. */
|
|
191
|
-
export function openBotSettings({ Database, file, catalogFor = () => ({}), botExists = () => false, onCommitted = () => {}, onSuspended = () => {}, now = Date.now }) {
|
|
212
|
+
export function openBotSettings({ Database, file, catalogFor = () => ({}), botExists = () => false, isMainBot = () => false, onCommitted = () => {}, onSuspended = () => {}, now = Date.now }) {
|
|
192
213
|
if (typeof Database !== 'function' || typeof file !== 'string' || !file) throw new TypeError('Database constructor and explicit file are required')
|
|
193
|
-
for (const [name, callback] of Object.entries({ catalogFor, botExists, onCommitted, onSuspended })) if (typeof callback !== 'function') throw new TypeError(`${name} must be a function`)
|
|
214
|
+
for (const [name, callback] of Object.entries({ catalogFor, botExists, isMainBot, onCommitted, onSuspended })) if (typeof callback !== 'function') throw new TypeError(`${name} must be a function`)
|
|
194
215
|
if (file !== ':memory:') { file = resolve(file); mkdirSync(dirname(file), { recursive: true }) }
|
|
195
216
|
const db = new Database(file)
|
|
196
217
|
try {
|
|
@@ -288,8 +309,26 @@ export function openBotSettings({ Database, file, catalogFor = () => ({}), botEx
|
|
|
288
309
|
const botId = botIdOf(request.botId)
|
|
289
310
|
if (!Number.isSafeInteger(request.expectedRevision) || request.expectedRevision < 0) fail('BOT_SETTINGS_INVALID', 'expectedRevision 必须是当前绑定修订号。')
|
|
290
311
|
if (!sync(botExists(botId), 'botExists')) fail('BOT_SETTINGS_UNKNOWN_BOT', '此 Bot 不在已提交的宿主目录。')
|
|
291
|
-
const
|
|
292
|
-
const
|
|
312
|
+
const previous = load(botId).settings
|
|
313
|
+
const incoming = normalizeBotSettings(request.settings)
|
|
314
|
+
const main = sync(isMainBot(botId), 'isMainBot') === true
|
|
315
|
+
if (!main && Object.hasOwn(incoming, 'mainRoutes') && json(incoming.mainRoutes) !== json(previous.mainRoutes ?? null)) {
|
|
316
|
+
fail('MAIN_ROUTES_ROLE_REQUIRED', '只有已提交的 Main Bot 可以修改多路线配置。')
|
|
317
|
+
}
|
|
318
|
+
let normalized = main ? preserveMainModelRoutes(incoming, previous) : incoming
|
|
319
|
+
if (!main && Object.hasOwn(previous, 'mainRoutes')) normalized.mainRoutes = previous.mainRoutes
|
|
320
|
+
const needsCatalog = normalized.enabled || (main && normalized.mainRoutes)
|
|
321
|
+
const hostCatalog = needsCatalog ? sync(catalogFor(botId), 'catalogFor') : null
|
|
322
|
+
if (main && normalized.mainRoutes) {
|
|
323
|
+
const catalog = catalogOf(hostCatalog)
|
|
324
|
+
for (const route of normalized.mainRoutes.routes) {
|
|
325
|
+
const known = catalog.models.find(item => item.provider === route.provider && item.model === route.model)
|
|
326
|
+
if (route.funding.mode === 'api' && route.funding.pricing === null && known?.pricing) route.funding.pricing = clone(known.pricing)
|
|
327
|
+
if (route.enabled) checkMainRouteFunding(route, fundingForMainRoute(normalized.mainRoutes, route), catalog)
|
|
328
|
+
}
|
|
329
|
+
normalized = materializeMainRouteDefault(normalized, { normalizeFunding })
|
|
330
|
+
}
|
|
331
|
+
const settings = normalized.enabled ? validateBotSettings(normalized, hostCatalog).settings : normalized
|
|
293
332
|
return commit(botId, request.expectedRevision, settings)
|
|
294
333
|
},
|
|
295
334
|
refresh() {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which model calls belong to a Bot thread, decided before anything else runs.
|
|
3
|
+
*
|
|
4
|
+
* The runtime's live owner index is a fast answer that goes stale the moment a thread is
|
|
5
|
+
* archived, a Bot is suspended or deleted, or a Bot is moved to the other runtime: an archived
|
|
6
|
+
* thread's Session leaves `chatOwners`, so the ordinary Harness bypass would answer for it as if
|
|
7
|
+
* it were the user's own chat — unguarded, unpoliced and unaccounted. The thread catalog is the
|
|
8
|
+
* record that survives all of that, and its gate is the one the send path already uses.
|
|
9
|
+
*
|
|
10
|
+
* So this guard asks the catalog first, for the exact Bot/thread/session triple, and only then
|
|
11
|
+
* the frozen runtime. Three outcomes, and only three:
|
|
12
|
+
*
|
|
13
|
+
* - `thread`: a committed Bot thread. The gate has just re-confirmed the Bot is committed and
|
|
14
|
+
* healthy, its grant still stands, the thread is open, and the Host still owns the Session.
|
|
15
|
+
* - `unbound`: nothing claims this Session. An ordinary Harness session — the user's own chat,
|
|
16
|
+
* another plugin — stays exactly as unguarded as it was.
|
|
17
|
+
* - a refusal: the Session is claimed but the claim does not stand. Nothing is replayed, and no
|
|
18
|
+
* identity is invented: a doubt about a thread never becomes Main, the owner, or a default
|
|
19
|
+
* Session.
|
|
20
|
+
*
|
|
21
|
+
* A Session the frozen runtime reports as a live thread while the catalog has no record for it
|
|
22
|
+
* is a refusal too. The catalog is the record; the runtime's index is the hint.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fail = (code, message) => {
|
|
26
|
+
const error = new Error(message ?? code)
|
|
27
|
+
error.code = code
|
|
28
|
+
error.outcome = 'refused'
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const safe = work => { try { return work() } catch { return null } }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {{
|
|
36
|
+
* threads?: object | (() => object | null) | null,
|
|
37
|
+
* nativeThreadOf?: ((sessionId: string) => { botId: string, threadId: string } | null | undefined) | null,
|
|
38
|
+
* }} options
|
|
39
|
+
*/
|
|
40
|
+
export function createBotThreadAdmission({ threads = null, nativeThreadOf = null } = {}) {
|
|
41
|
+
// The thread host is built after the product runtime, so the resolver is allowed to be lazy:
|
|
42
|
+
// the guard asks for it on every call rather than capturing a half-composed Host.
|
|
43
|
+
const host = () => {
|
|
44
|
+
const value = typeof threads === 'function' ? safe(() => threads()) : threads
|
|
45
|
+
// Either the thread host itself or its catalog: both are the same committed record.
|
|
46
|
+
const catalog = value?.service && typeof value.service === 'object' ? value.service : value
|
|
47
|
+
return catalog && typeof catalog.threadBySession === 'function' && typeof catalog.assertSendable === 'function' ? catalog : null
|
|
48
|
+
}
|
|
49
|
+
const nativeThread = sessionId => (typeof nativeThreadOf === 'function' ? safe(() => nativeThreadOf(sessionId)) : null) ?? null
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string | undefined | null} sessionId
|
|
53
|
+
* @returns {{ kind: 'absent' | 'unbound' } | { kind: 'thread', botId: string, threadId: string, sessionId: string }}
|
|
54
|
+
*/
|
|
55
|
+
const admit = sessionId => {
|
|
56
|
+
if (typeof sessionId !== 'string' || sessionId === '') return { kind: 'absent' }
|
|
57
|
+
const service = host()
|
|
58
|
+
const live = nativeThread(sessionId)
|
|
59
|
+
if (!service) {
|
|
60
|
+
// No catalog to ask. A Session the runtime still calls a live Bot thread is refused rather
|
|
61
|
+
// than handed to the ordinary bypass, which would answer for a Bot with no Bot policy.
|
|
62
|
+
if (live) fail('BOT_THREAD_ADMISSION_UNAVAILABLE', '线程会话服务未就绪,宿主无法确认该会话归属,本次请求被拒绝。')
|
|
63
|
+
return { kind: 'unbound' }
|
|
64
|
+
}
|
|
65
|
+
// A catalog that cannot be read is a doubt, not an answer: it may hold the very tombstoned
|
|
66
|
+
// identity this call belongs to, so the failure propagates instead of reading as "unbound".
|
|
67
|
+
const found = service.threadBySession(sessionId) ?? null
|
|
68
|
+
if (found && typeof found.botId === 'string' && typeof found.threadId === 'string') {
|
|
69
|
+
// The exact triple, re-confirmed now: archive, revocation, a deleted Bot, a moved runtime
|
|
70
|
+
// and a Session the Host no longer owns all refuse here.
|
|
71
|
+
const gate = service.assertSendable({ botId: found.botId, threadId: found.threadId })
|
|
72
|
+
// And the Session the gate answered for is the one that was asked about: a record that
|
|
73
|
+
// resolved to some other Session is a refusal, never a silent substitution.
|
|
74
|
+
if (gate?.sessionId !== sessionId) {
|
|
75
|
+
fail('THREAD_SESSION_CONFLICT', '该线程会话与请求的会话不一致,本次请求被拒绝。')
|
|
76
|
+
}
|
|
77
|
+
return { kind: 'thread', botId: found.botId, threadId: found.threadId, sessionId, grantVerified: gate?.grantVerified === true }
|
|
78
|
+
}
|
|
79
|
+
if (live) fail('BOT_THREAD_UNVERIFIED', '该会话是一条 Bot 线程会话,但线程目录没有它的记录,本次请求被拒绝。')
|
|
80
|
+
return { kind: 'unbound' }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Object.freeze({ admit })
|
|
84
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createBotThreads } from './bot-threads.mjs'
|
|
2
|
+
|
|
3
|
+
// The seam between the thread catalog and the real Host. Everything the catalog
|
|
4
|
+
// needs to know about authority is asked here, of services the product already
|
|
5
|
+
// owns: the frozen runtime's thread records, the lifecycle's committed Bots, the
|
|
6
|
+
// principal directory and the capability authority. Nothing is reimplemented,
|
|
7
|
+
// and nothing the Host does not confirm is treated as confirmed.
|
|
8
|
+
//
|
|
9
|
+
// The adapter also reports which Host hooks exist. A Host that has not applied
|
|
10
|
+
// the thread records (an older overlay, or a build without them) says so here,
|
|
11
|
+
// so the composition can surface `blocked` with the missing hook instead of a
|
|
12
|
+
// surface that looks wired and is not.
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{
|
|
16
|
+
* rt: object, // the product runtime (createBotProduct().rt)
|
|
17
|
+
* lifecycle: object, // attachDsLifecycle()
|
|
18
|
+
* authority: object, // capability-broker authority
|
|
19
|
+
* directory: object, // principal directory (principals.mjs)
|
|
20
|
+
* file: string, // where the catalog lives, beside the Host's own state
|
|
21
|
+
* runtimeMode?: (botId: string) => string | null,
|
|
22
|
+
* now?: () => number,
|
|
23
|
+
* }} options
|
|
24
|
+
*/
|
|
25
|
+
export function createBotThreadHost({ rt, lifecycle, authority, directory, file, runtimeMode = null, now = Date.now } = {}) {
|
|
26
|
+
if (!rt || typeof rt !== 'object') throw new TypeError('BOT_THREAD_HOST_RUNTIME_REQUIRED')
|
|
27
|
+
if (!lifecycle || typeof lifecycle !== 'object') throw new TypeError('BOT_THREAD_HOST_LIFECYCLE_REQUIRED')
|
|
28
|
+
if (!authority || typeof authority !== 'object') throw new TypeError('BOT_THREAD_HOST_AUTHORITY_REQUIRED')
|
|
29
|
+
if (!directory || typeof directory !== 'object') throw new TypeError('BOT_THREAD_HOST_DIRECTORY_REQUIRED')
|
|
30
|
+
if (typeof file !== 'string' || file === '') throw new TypeError('BOT_THREAD_HOST_FILE_REQUIRED')
|
|
31
|
+
|
|
32
|
+
// Which trusted hooks this Host actually has. `ready` is the honest gate for
|
|
33
|
+
// offering a send at all: without the runtime record and the ownership
|
|
34
|
+
// lookup, a thread Session could be made but never authorized.
|
|
35
|
+
const hooks = Object.freeze({
|
|
36
|
+
createSession: typeof rt.createSession === 'function',
|
|
37
|
+
addThreadSession: typeof rt.addThreadSession === 'function',
|
|
38
|
+
setThreadArchived: typeof rt.setThreadArchived === 'function',
|
|
39
|
+
renameThread: typeof rt.renameThread === 'function',
|
|
40
|
+
threadOf: typeof rt.threadOf === 'function',
|
|
41
|
+
threadsOf: typeof rt.threadsOf === 'function',
|
|
42
|
+
committedBot: typeof lifecycle.committedBot === 'function',
|
|
43
|
+
lifecycleStatus: typeof lifecycle.status === 'function',
|
|
44
|
+
forSession: typeof directory.forSession === 'function',
|
|
45
|
+
forBot: typeof directory.forBot === 'function',
|
|
46
|
+
describe: typeof authority.describe === 'function',
|
|
47
|
+
assertTool: typeof authority.assertTool === 'function',
|
|
48
|
+
})
|
|
49
|
+
const ready = hooks.createSession && hooks.addThreadSession && hooks.committedBot && hooks.forSession && hooks.describe
|
|
50
|
+
|
|
51
|
+
const committedBot = botId => (hooks.committedBot ? lifecycle.committedBot(botId) ?? null : null)
|
|
52
|
+
const lifecycleStatus = () => (hooks.lifecycleStatus ? lifecycle.status() ?? null : null)
|
|
53
|
+
|
|
54
|
+
// The Host's own record of a Bot's threads: identity and archive, read back
|
|
55
|
+
// from committed state. This is what a restart settles an interrupted create
|
|
56
|
+
// against, so it is read from the lifecycle, never from this process.
|
|
57
|
+
const committedThreads = botId => {
|
|
58
|
+
const bot = committedBot(botId)
|
|
59
|
+
if (!Array.isArray(bot?.threads)) return []
|
|
60
|
+
return bot.threads
|
|
61
|
+
.filter(thread => thread && typeof thread.threadId === 'string' && typeof thread.sessionId === 'string')
|
|
62
|
+
.map(thread => ({ threadId: thread.threadId, sessionId: thread.sessionId, archivedAt: thread.archivedAt ?? null }))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The principal directory is the Host's persistent confirmation of who a
|
|
66
|
+
// Session belongs to. It is asked before any policy runs, every time.
|
|
67
|
+
const ownsSession = (botId, sessionId) => {
|
|
68
|
+
if (!hooks.forSession || !hooks.describe) return false
|
|
69
|
+
try { return authority.describe(directory.forSession(sessionId)).botId === botId } catch { return false }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A suspended or disabled Bot has an empty committed grant. The authority
|
|
73
|
+
// says so through the one assertion a send always needs; a doubt is reported
|
|
74
|
+
// as unanswerable rather than assumed.
|
|
75
|
+
const botAuthorized = botId => {
|
|
76
|
+
if (!hooks.forBot || !hooks.assertTool) return null
|
|
77
|
+
try {
|
|
78
|
+
const subject = directory.forBot(botId)
|
|
79
|
+
authority.assertTool(subject, 'model/infer')
|
|
80
|
+
return true
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return error?.code === 'TOOL_DENIED' || error?.code === 'SUBJECT_REVOKED' ? false : null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Makes a thread's Session through the same factory every own chat uses, and
|
|
87
|
+
// records it in the Bot's committed state in one save — the write that makes
|
|
88
|
+
// the lifecycle compile the Session into this Bot's ownership. A refusal the
|
|
89
|
+
// Host names before any Session exists is reported as such, so the catalog
|
|
90
|
+
// does not keep a create intent for an operation that never started.
|
|
91
|
+
const createSession = async ({ botId, threadId, title, dir }) => {
|
|
92
|
+
if (!hooks.createSession) {
|
|
93
|
+
throw Object.assign(new Error('宿主运行时未提供会话工厂。'), { code: 'SESSION_FACTORY_REQUIRED', outcome: 'refused' })
|
|
94
|
+
}
|
|
95
|
+
const readOnly = typeof rt.readOnly === 'function' ? rt.readOnly() : undefined
|
|
96
|
+
if (readOnly !== undefined) throw Object.assign(new Error(readOnly), { code: 'PRODUCT_READ_ONLY', outcome: 'refused' })
|
|
97
|
+
const sessionId = await rt.createSession(title, { dir })
|
|
98
|
+
if (!hooks.addThreadSession) {
|
|
99
|
+
throw Object.assign(new Error('宿主运行时未提供线程会话记录(rt.addThreadSession)。'), { code: 'SESSION_HOST_HOOK_MISSING', outcome: 'refused' })
|
|
100
|
+
}
|
|
101
|
+
await rt.addThreadSession(botId, threadId, sessionId, { name: title })
|
|
102
|
+
return sessionId
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Archive is the Host's fact: dropping a thread's authority happens in the
|
|
106
|
+
// committed state, and only then in the catalog.
|
|
107
|
+
const setThreadArchived = async ({ botId, threadId, archivedAt }) => {
|
|
108
|
+
if (!hooks.setThreadArchived) return null
|
|
109
|
+
return rt.setThreadArchived(botId, threadId, archivedAt)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A thread's title is the Host's too: the rename lands in the committed state
|
|
113
|
+
// and answers with the record it wrote, which the catalog checks before it
|
|
114
|
+
// believes the rename happened.
|
|
115
|
+
const renameThread = async ({ botId, threadId, name }) => {
|
|
116
|
+
if (!hooks.renameThread) return null
|
|
117
|
+
return rt.renameThread(botId, threadId, name)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const service = createBotThreads({
|
|
121
|
+
file,
|
|
122
|
+
committedBot,
|
|
123
|
+
lifecycleStatus,
|
|
124
|
+
botAuthorized,
|
|
125
|
+
ownsSession,
|
|
126
|
+
committedThreads,
|
|
127
|
+
createSession,
|
|
128
|
+
setThreadArchived,
|
|
129
|
+
renameThread,
|
|
130
|
+
// No public Host session-inspect exists yet: reconcile then reports every
|
|
131
|
+
// unowned Session as 'unbound' rather than 'missing'.
|
|
132
|
+
inspectSession: null,
|
|
133
|
+
runtimeMode: typeof runtimeMode === 'function' ? runtimeMode : null,
|
|
134
|
+
now,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// The owner-console handlers. Each takes the RPC payload as a closed field
|
|
138
|
+
// set (the catalog refuses anything else) and returns JSON-safe state; the
|
|
139
|
+
// composition wraps every one of them in ownerOnly.
|
|
140
|
+
const actions = Object.freeze({
|
|
141
|
+
'threads.readModel': payload => service.readModel(payload ?? {}),
|
|
142
|
+
'threads.list': payload => service.list(payload?.botId),
|
|
143
|
+
'threads.create': payload => service.create(payload ?? {}),
|
|
144
|
+
'threads.rename': payload => service.rename(payload ?? {}),
|
|
145
|
+
'threads.switch': payload => service.switchThread(payload ?? {}),
|
|
146
|
+
'threads.open': payload => service.openThread(payload ?? {}),
|
|
147
|
+
'threads.archive': payload => service.archive(payload ?? {}),
|
|
148
|
+
'threads.unarchive': payload => service.unarchive(payload ?? {}),
|
|
149
|
+
'threads.ensure': payload => service.ensureSession(payload ?? {}),
|
|
150
|
+
'threads.recover': payload => service.recover(payload ?? {}),
|
|
151
|
+
'threads.reconcile': () => service.reconcile(),
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The gate a real send runs under, before any model or tool call. It answers
|
|
156
|
+
* with the Session the turn must use, or refuses with the reason.
|
|
157
|
+
*
|
|
158
|
+
* `create` defaults to false: dispatching a model request is not a request to
|
|
159
|
+
* make a Session. A caller that means "write in this thread" says so, and
|
|
160
|
+
* only a thread that has never had a Session — no id, no durable create
|
|
161
|
+
* intent, no outcome the Host failed to confirm — is given one, exactly once,
|
|
162
|
+
* through the same single-flight ensure every other path uses. Every other
|
|
163
|
+
* refusal is handed back untouched: an unknown, unbound, revoked, archived,
|
|
164
|
+
* foreign or moved-away thread is never quietly given a fresh identity.
|
|
165
|
+
*/
|
|
166
|
+
const sendGate = async ({ botId, threadId, create = false } = {}) => {
|
|
167
|
+
const gate = service.probeSendable({ botId, threadId })
|
|
168
|
+
if (gate.ok) return gate
|
|
169
|
+
if (create !== true) return gate
|
|
170
|
+
// The one path that may make a Session: never created, and never attempted.
|
|
171
|
+
if (gate.code !== 'SESSION_MISSING' || gate.sessionState !== 'none') return gate
|
|
172
|
+
await service.ensureSession({ botId, threadId })
|
|
173
|
+
return service.probeSendable({ botId, threadId })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
service,
|
|
178
|
+
actions,
|
|
179
|
+
hooks,
|
|
180
|
+
ready,
|
|
181
|
+
sendGate,
|
|
182
|
+
ownsSession,
|
|
183
|
+
committedThreads,
|
|
184
|
+
botAuthorized,
|
|
185
|
+
close: () => service.close(),
|
|
186
|
+
})
|
|
187
|
+
}
|