@ljwei-stak/dsh-model-route-bot 0.66.4 → 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 +18 -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/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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createElement as h, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { COMPOSER_DRAFT_LIMITS, createComposerDraftStore } from './composer-draft.mjs'
|
|
3
|
+
import { ComposerDraftBound } from './composer-draft-bound.jsx'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Host namespace, read from the Host, and the slot wrapper the wiring uses.
|
|
7
|
+
*
|
|
8
|
+
* Why an adapter at all: a browser origin is not a Host identity. Two Hosts can
|
|
9
|
+
* serve the same `dsh-app://app` origin, so a namespace minted in the page would
|
|
10
|
+
* be shared by both and their drafts would mix. The Host therefore mints and
|
|
11
|
+
* persists its own id, and this component asks for it on every mount:
|
|
12
|
+
*
|
|
13
|
+
* productApi.dispatch('ui.namespace', {}, signal)
|
|
14
|
+
* -> { schemaVersion: 1, hostId: 'host-<16 hex>' }
|
|
15
|
+
*
|
|
16
|
+
* The Host keeps that value in its own SQLite
|
|
17
|
+
* (`packages/orchestration/src/ui-host-namespace.mjs`), so it survives restarts
|
|
18
|
+
* and differs per Host. Nothing here generates a namespace of its own: a read
|
|
19
|
+
* that fails, times out, is aborted or answers late leaves the drafts switched
|
|
20
|
+
* off rather than guessing an identity.
|
|
21
|
+
*
|
|
22
|
+
* Rules this holds:
|
|
23
|
+
* - until a validated namespace arrives, no read, no write, no restore;
|
|
24
|
+
* - a Host replacement aborts the read in flight and drops the old store
|
|
25
|
+
* before the new one is used, so a late answer cannot overwrite the new
|
|
26
|
+
* Host's drafts;
|
|
27
|
+
* - a failure is reported, not retried in the background; the caller re-reads
|
|
28
|
+
* explicitly by raising `attempt`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const NAMESPACE_PATTERN = /^host-[0-9a-f]{16}$/
|
|
32
|
+
|
|
33
|
+
/** The one shape the Host answers with; anything else is a refusal. */
|
|
34
|
+
export function validateNamespace(value) {
|
|
35
|
+
if (!value || typeof value !== 'object') throw new Error('宿主没有返回界面存储标识。')
|
|
36
|
+
if (value.schemaVersion !== 1) throw new Error('宿主界面存储标识版本不被此客户端支持。')
|
|
37
|
+
if (typeof value.hostId !== 'string' || !NAMESPACE_PATTERN.test(value.hostId)) throw new Error('宿主界面存储标识格式无效。')
|
|
38
|
+
return value.hostId
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const messageOf = error => (error?.message ? String(error.message) : String(error ?? '未知错误'))
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Asks the Host for its namespace and renders the draft once it answers.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} props
|
|
47
|
+
* @param {{ dispatch: (action: string, payload: unknown, signal?: AbortSignal) => Promise<unknown> }} props.productApi
|
|
48
|
+
* @param {number} [props.attempt] raising it re-reads the namespace explicitly
|
|
49
|
+
* @param {(status: { status: 'pending'|'ready'|'error', hostId: string, error: string }) => void} [props.onStatus]
|
|
50
|
+
* @param {(store: ReturnType<import('./composer-draft.mjs').createComposerDraftStore> | undefined) => void} [props.onStore]
|
|
51
|
+
* the store this Host namespace owns, or undefined while the drafts are off
|
|
52
|
+
* @param {Record<string, unknown>} [props.storage] defaults to the window's localStorage
|
|
53
|
+
*/
|
|
54
|
+
export function ComposerDraftHost({ productApi, attempt = 0, namespaceTimeoutMs = 8000, onStatus, onStore, storage, children, ...slot }) {
|
|
55
|
+
const [state, setState] = useState({ status: 'pending', hostId: '', error: '' })
|
|
56
|
+
const [retry, setRetry] = useState(0)
|
|
57
|
+
// Effects run after rendering. Never expose A's ready store on the first
|
|
58
|
+
// render of B, even when the replacement RPC has not started yet.
|
|
59
|
+
const current = state.api === productApi && state.attempt === attempt && state.retry === retry ? state : { status: 'pending', hostId: '', error: '' }
|
|
60
|
+
const generation = useRef(0)
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const mine = (generation.current += 1)
|
|
64
|
+
const controller = new AbortController()
|
|
65
|
+
// A replacement Host retires the old store first: until its own namespace
|
|
66
|
+
// answers, this window serves no drafts at all, and a late answer for the
|
|
67
|
+
// Host it replaced cannot install itself.
|
|
68
|
+
const scope = { api: productApi, attempt, retry }
|
|
69
|
+
setState({ ...scope, status: 'pending', hostId: '', error: '' })
|
|
70
|
+
let timer
|
|
71
|
+
const read = async () => {
|
|
72
|
+
try {
|
|
73
|
+
const value = await Promise.race([
|
|
74
|
+
productApi.dispatch('ui.namespace', {}, controller.signal),
|
|
75
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('读取宿主草稿标识超时,请重试。')), namespaceTimeoutMs) }),
|
|
76
|
+
])
|
|
77
|
+
// A later request (a Host replacement, a remount) owns the answer now.
|
|
78
|
+
if (generation.current !== mine || controller.signal.aborted) return
|
|
79
|
+
setState({ ...scope, status: 'ready', hostId: validateNamespace(value), error: '' })
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (generation.current !== mine || controller.signal.aborted) return
|
|
82
|
+
setState({ ...scope, status: 'error', hostId: '', error: messageOf(error) })
|
|
83
|
+
controller.abort()
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timer)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
void read()
|
|
89
|
+
return () => { clearTimeout(timer); controller.abort() }
|
|
90
|
+
}, [productApi, attempt, retry, namespaceTimeoutMs])
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (typeof onStatus === 'function') onStatus({ status: current.status, hostId: current.hostId, error: current.error })
|
|
94
|
+
}, [state, onStatus, productApi, attempt, retry])
|
|
95
|
+
// One store per Host namespace. A replacement flushes what the retired Host
|
|
96
|
+
// still had pending and lets it go before the new one takes over.
|
|
97
|
+
const store = useMemo(() => (current.hostId
|
|
98
|
+
? createComposerDraftStore({ storage: storage ?? globalThis.localStorage, hostId: current.hostId, limits: COMPOSER_DRAFT_LIMITS })
|
|
99
|
+
: undefined), [current.hostId, storage])
|
|
100
|
+
const held = useRef(undefined)
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (typeof onStore === 'function') onStore(store ?? null)
|
|
103
|
+
}, [store, onStore])
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const previous = held.current
|
|
106
|
+
held.current = store
|
|
107
|
+
if (previous !== undefined && previous !== store) previous.flush()
|
|
108
|
+
}, [store])
|
|
109
|
+
useEffect(() => () => { held.current?.flush() }, [])
|
|
110
|
+
|
|
111
|
+
if (typeof children === 'function') return children({ store, status: current.status, hostId: current.hostId, error: current.error })
|
|
112
|
+
return h(Fragment, null, h(ComposerDraftBound, { ...slot, store, hostStatus: current.status }),
|
|
113
|
+
current.status === 'error' ? h('span', { className: 'bp-draft-recovery' },
|
|
114
|
+
h('span', { role: 'status' }, '草稿恢复暂不可用'),
|
|
115
|
+
h('button', { type: 'button', onClick: () => setRetry(value => value + 1) }, '重试草稿恢复')) : null)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The wrapper the wiring puts in the Host's composer slot. It keeps the original
|
|
120
|
+
* registration's component and its props untouched — the Host still renders the
|
|
121
|
+
* very component it registered — and adds the draft beside it.
|
|
122
|
+
*
|
|
123
|
+
* @param {React.ComponentType} component the component the slot already had
|
|
124
|
+
* @param {{ productApi: object }} options
|
|
125
|
+
*/
|
|
126
|
+
export function withComposerDraft(component, options = {}) {
|
|
127
|
+
const { productApi } = options
|
|
128
|
+
const Wrapped = props => h(Fragment, null,
|
|
129
|
+
h(component, props),
|
|
130
|
+
h(ComposerDraftHost, { ...props, ...options, productApi, sessionBinding: options.sessionBindingOf?.(props.sessionId) }),
|
|
131
|
+
)
|
|
132
|
+
// Anything that probes the slot for the component it registered still finds it.
|
|
133
|
+
Wrapped.composerDraftOriginal = component
|
|
134
|
+
return Wrapped
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A stable re-read handle for a caller that wants to retry explicitly. */
|
|
138
|
+
export function useNamespaceRetry() {
|
|
139
|
+
const [attempt, setAttempt] = useState(0)
|
|
140
|
+
const retry = useCallback(() => setAttempt(value => value + 1), [])
|
|
141
|
+
return { attempt, retry }
|
|
142
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { createElement as h, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { threadKeyFromRoster } from './composer-draft.mjs'
|
|
3
|
+
import { matchSubmission, newQueueRows } from './composer-draft-witness.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The composer draft of one conversation, bound to the Host composer.
|
|
7
|
+
*
|
|
8
|
+
* The Host owns the composer: its text lives in the input state a composer slot
|
|
9
|
+
* receives (`useInput`), and text goes back in through the insertion face the
|
|
10
|
+
* same slot receives (`inputActions.captureInsertion()` / `insertText()`). Those
|
|
11
|
+
* are the only two doors used here — the vendor's own composer extras use the
|
|
12
|
+
* same pair (vendor/ds-bot/src/client/composer.js:70,79 and enter-key.js:18), and
|
|
13
|
+
* this file adds no DOM query, no hashed class selector and no global
|
|
14
|
+
* contenteditable lookup, because none of those is a contract.
|
|
15
|
+
*
|
|
16
|
+
* What this never does: send. No submit, no turn, no model call, no approval, no
|
|
17
|
+
* attachment re-add. Restoring a draft fills an empty composer and stops.
|
|
18
|
+
*
|
|
19
|
+
* When a draft is dropped, and why nothing else may drop it:
|
|
20
|
+
* - the witness says this exact send was accepted — the session's own
|
|
21
|
+
* submission identity, read in composer-draft-witness.mjs. That is the only
|
|
22
|
+
* "yes" there is;
|
|
23
|
+
* - the composer was emptied with no send in flight, which is a person
|
|
24
|
+
* deleting what they typed, so nothing may come back later.
|
|
25
|
+
* A composer that emptied while a send was in flight keeps its draft until the
|
|
26
|
+
* witness answers, and remembers that a send is still open so a reload asks
|
|
27
|
+
* again. An unobserved outcome is never a success: no phase, no empty input and
|
|
28
|
+
* no elapsed time is read as one.
|
|
29
|
+
*
|
|
30
|
+
* What the Host has to provide for any of it to work: `useInput` (to read the
|
|
31
|
+
* draft) and `inputActions` (to put text back). Without the insertion face this
|
|
32
|
+
* component does nothing at all — it does not read, write or restore — because a
|
|
33
|
+
* half-wired draft is worse than none.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const readDraft = state => (typeof state?.draft === 'string' ? state.draft : '')
|
|
37
|
+
const readPhase = state => (typeof state?.phase === 'string' ? state.phase : '')
|
|
38
|
+
/** User-sourced queue rows as one stable string, for a store selector. */
|
|
39
|
+
const readQueue = state => (Array.isArray(state?.queue)
|
|
40
|
+
? state.queue.filter(row => row?.source?.kind === 'user').map(row => String(row?.id ?? '')).join(',')
|
|
41
|
+
: '')
|
|
42
|
+
const readSubmissions = snapshot => {
|
|
43
|
+
const rows = Array.isArray(snapshot?.pendingSubmissions) ? snapshot.pendingSubmissions : []
|
|
44
|
+
// One stable string for a store selector, parsed back into the echo shape the
|
|
45
|
+
// witness helpers read.
|
|
46
|
+
return JSON.stringify(rows.map(row => ({ requestId: row?.requestId, text: row?.text, time: Number(row?.time ?? 0) })))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Phases in which the Host has taken the composer's text for a send. */
|
|
50
|
+
const IN_FLIGHT_PHASES = new Set(['submitting', 'adjudicating', 'claimed'])
|
|
51
|
+
|
|
52
|
+
const idsOf = value => (typeof value === 'string' && value ? value.split(',') : [])
|
|
53
|
+
|
|
54
|
+
const partsOf = key => {
|
|
55
|
+
const thread = key.indexOf(':thread:')
|
|
56
|
+
if (thread !== -1) {
|
|
57
|
+
const kind = key.slice(0, key.indexOf(':'))
|
|
58
|
+
return { kind, ownerId: key.slice(kind.length + 1, thread), threadId: key.slice(thread + ':thread:'.length) }
|
|
59
|
+
}
|
|
60
|
+
const colon = key.indexOf(':')
|
|
61
|
+
return colon === -1 ? null : { kind: key.slice(0, colon), ownerId: key.slice(colon + 1) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The conversation a mounted session belongs to, as the Host roster names it. A
|
|
66
|
+
* session the roster does not know (an unowned or an Agent session) has no draft.
|
|
67
|
+
* `threadOf` maps a session to the stable threadId the Host directory gave it; a
|
|
68
|
+
* session it does not name is the Bot's default conversation, which every part of
|
|
69
|
+
* that Bot shares.
|
|
70
|
+
*/
|
|
71
|
+
export function useComposerDraftKey(sessionId, useRoster, threadOf) {
|
|
72
|
+
return typeof useRoster === 'function'
|
|
73
|
+
? useRoster(value => threadKeyFromRoster(sessionId, value, threadOf))
|
|
74
|
+
: ''
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A Host that publishes no insertion face is a different component, not a
|
|
78
|
+
// different hook order: the vendor's composer slots keep one hook order for a
|
|
79
|
+
// whole mount, and so does this. NoInput touches nothing — no read, no write, no
|
|
80
|
+
// restore, not even the conversation directory.
|
|
81
|
+
const NoInput = ({ onNote }) => {
|
|
82
|
+
const said = useRef(false)
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (said.current) return
|
|
85
|
+
said.current = true
|
|
86
|
+
if (typeof onNote === 'function') onNote('no-input-face')
|
|
87
|
+
}, [onNote])
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ReadInput = ({ useInput, useSubmissions, ...rest }) => h(DraftBody, {
|
|
92
|
+
...rest,
|
|
93
|
+
draft: useInput(readDraft),
|
|
94
|
+
phase: useInput(readPhase),
|
|
95
|
+
queue: useInput(readQueue),
|
|
96
|
+
submissions: typeof useSubmissions === 'function' ? useSubmissions(readSubmissions) : '',
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {object} props
|
|
101
|
+
* @param {ReturnType<import('./composer-draft.mjs').createComposerDraftStore>} [props.store]
|
|
102
|
+
* @param {string} props.sessionId the mounted part's session
|
|
103
|
+
* @param {(select: (state: unknown) => unknown) => unknown} [props.useRoster]
|
|
104
|
+
* @param {(select: (state: unknown) => unknown) => unknown} [props.useInput]
|
|
105
|
+
* @param {(select: (state: unknown) => unknown) => unknown} [props.useSubmissions]
|
|
106
|
+
* the Host's session hook, when this slot carries one; it is what makes a send
|
|
107
|
+
* identifiable instead of merely suspicious
|
|
108
|
+
* @param {{ captureInsertion?: () => unknown, insertText?: (text: string, span: unknown) => unknown }} [props.inputActions]
|
|
109
|
+
* @param {(context: { text: string, at: number, requestId?: string }) => 'accepted'|'failed'|undefined} [props.witness]
|
|
110
|
+
* @param {(sessionId: string) => string | undefined} [props.threadOf]
|
|
111
|
+
* @param {(note: string) => void} [props.onNote]
|
|
112
|
+
*/
|
|
113
|
+
export function ComposerDraft(props) {
|
|
114
|
+
const face = props.inputActions
|
|
115
|
+
const usable = typeof face?.captureInsertion === 'function' && typeof face?.insertText === 'function'
|
|
116
|
+
return usable && typeof props.useInput === 'function' ? h(ReadInput, props) : h(NoInput, props)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function DraftBody({ store, sessionId, useRoster, inputActions, witness, threadOf, onNote, draft, phase, queue, submissions, evidenceRevision }) {
|
|
120
|
+
const key = useComposerDraftKey(sessionId, useRoster, threadOf)
|
|
121
|
+
const [status, setStatus] = useState('idle')
|
|
122
|
+
const shown = useRef('idle')
|
|
123
|
+
const latest = useRef({ draft, phase, queue, submissions, witness })
|
|
124
|
+
latest.current = { draft, phase, queue, submissions, witness }
|
|
125
|
+
const local = useRef({ key: '', revision: 0, seen: '', saved: '', restored: '', queueBefore: '', submissionsBefore: [], foreign: false, flight: null, timer: 0 })
|
|
126
|
+
|
|
127
|
+
const note = value => {
|
|
128
|
+
if (typeof onNote === 'function') onNote(value)
|
|
129
|
+
if (shown.current === value) return
|
|
130
|
+
shown.current = value
|
|
131
|
+
setStatus(value)
|
|
132
|
+
}
|
|
133
|
+
const face = useRef(inputActions)
|
|
134
|
+
face.current = inputActions
|
|
135
|
+
|
|
136
|
+
// Dropping one draft. Only a witness that says this send was accepted, or a
|
|
137
|
+
// deletion this conversation can be held to, gets here.
|
|
138
|
+
const drop = (parts, row) => {
|
|
139
|
+
row.flight = null
|
|
140
|
+
row.revision += 1
|
|
141
|
+
const dropped = store?.clear({ ...parts, revision: row.revision })
|
|
142
|
+
note(dropped?.ok ? 'cleared' : `kept:${dropped?.reason}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// A conversation is mounted: the surface serving it makes its draft reachable,
|
|
146
|
+
// and an unsent line from this same conversation goes back into an empty
|
|
147
|
+
// composer. What the person already typed always wins over a stored read.
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const row = local.current
|
|
150
|
+
if (!key || !store) {
|
|
151
|
+
row.key = ''
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
const parts = partsOf(key)
|
|
155
|
+
row.key = key
|
|
156
|
+
row.saved = ''
|
|
157
|
+
row.restored = ''
|
|
158
|
+
row.flight = null
|
|
159
|
+
clearTimeout(row.timer)
|
|
160
|
+
row.timer = 0
|
|
161
|
+
row.queueBefore = latest.current.queue
|
|
162
|
+
row.submissionsBefore = JSON.parse(latest.current.submissions || '[]').map(row => row.requestId)
|
|
163
|
+
row.seen = latest.current.draft
|
|
164
|
+
store.see({ ...parts, sessionId })
|
|
165
|
+
const record = store.read(parts)
|
|
166
|
+
row.revision = record?.revision ?? 0
|
|
167
|
+
// Text this conversation never saved — another conversation's, or another
|
|
168
|
+
// Host's — is not ours to drop when it goes: only a deletion of text we can
|
|
169
|
+
// attribute to this composer clears this conversation's draft.
|
|
170
|
+
row.foreign = Boolean(latest.current.draft) && latest.current.draft !== (record?.text ?? '')
|
|
171
|
+
if (!record?.text) { note('empty'); return () => store.flush() }
|
|
172
|
+
// A send that was still unanswered when this window closed: the witness gets
|
|
173
|
+
// the first word, so an accepted send does not come back as text.
|
|
174
|
+
if (record.pendingAt) {
|
|
175
|
+
const pending = { text: record.text, at: record.pendingAt, requestId: record.pendingRequestId || undefined }
|
|
176
|
+
const answer = latest.current.witness?.(pending)
|
|
177
|
+
if (answer === 'accepted') {
|
|
178
|
+
row.revision += 1
|
|
179
|
+
const dropped = store.clear({ ...parts, revision: row.revision })
|
|
180
|
+
note(dropped?.ok ? 'cleared' : `kept:${dropped?.reason}`)
|
|
181
|
+
return () => store.flush()
|
|
182
|
+
}
|
|
183
|
+
row.flight = pending
|
|
184
|
+
note('kept-unknown')
|
|
185
|
+
return () => store.flush()
|
|
186
|
+
}
|
|
187
|
+
if (latest.current.draft) { note('kept-typing'); return () => store.flush() }
|
|
188
|
+
if (face.current.insertText(record.text, face.current.captureInsertion()) === false) {
|
|
189
|
+
note('restore-refused')
|
|
190
|
+
return () => store.flush()
|
|
191
|
+
}
|
|
192
|
+
row.restored = record.text
|
|
193
|
+
row.seen = record.text
|
|
194
|
+
row.revision = record.revision
|
|
195
|
+
note('restored')
|
|
196
|
+
return () => { store.flush() }
|
|
197
|
+
}, [key, store, sessionId])
|
|
198
|
+
|
|
199
|
+
// Every change of the composer's text, and every new piece of evidence about
|
|
200
|
+
// the send that emptied it, lands here.
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const row = local.current
|
|
203
|
+
if (!row.key || !store) return
|
|
204
|
+
const parts = partsOf(row.key)
|
|
205
|
+
if (draft) {
|
|
206
|
+
if (draft === row.seen) return
|
|
207
|
+
// Typing again cancels whatever the previous empty might have meant, and
|
|
208
|
+
// remembers the queue this text was typed beside.
|
|
209
|
+
row.seen = draft
|
|
210
|
+
row.foreign = false
|
|
211
|
+
row.flight = null
|
|
212
|
+
row.queueBefore = latest.current.queue
|
|
213
|
+
row.submissionsBefore = JSON.parse(latest.current.submissions || '[]').map(row => row.requestId)
|
|
214
|
+
clearTimeout(row.timer)
|
|
215
|
+
row.timer = 0
|
|
216
|
+
row.revision += 1
|
|
217
|
+
const stored = store.save({ ...parts, sessionId, text: draft, revision: row.revision })
|
|
218
|
+
if (stored?.ok) row.saved = draft
|
|
219
|
+
note(stored?.ok ? 'saved' : `refused:${stored?.reason}`)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (row.flight) {
|
|
223
|
+
// New evidence about a send that is still open.
|
|
224
|
+
const answer = latest.current.witness?.(row.flight)
|
|
225
|
+
if (answer === 'accepted') { drop(parts, row); return }
|
|
226
|
+
if (answer === 'failed') { row.flight = null; note('kept-failed'); return }
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
if (draft === row.seen) return
|
|
230
|
+
const emptied = row.seen
|
|
231
|
+
row.seen = ''
|
|
232
|
+
if (!emptied) return
|
|
233
|
+
|
|
234
|
+
const at = Date.now()
|
|
235
|
+
const candidates = JSON.parse(latest.current.submissions || '[]')
|
|
236
|
+
const echo = matchSubmission(candidates, { text: emptied, at, before: row.submissionsBefore })
|
|
237
|
+
const tookQueue = newQueueRows(idsOf(latest.current.queue), idsOf(row.queueBefore)).length > 0
|
|
238
|
+
const inFlight = echo !== null || candidates.some(item => !row.submissionsBefore.includes(item.requestId)) || tookQueue ||
|
|
239
|
+
(evidenceRevision === undefined && IN_FLIGHT_PHASES.has(latest.current.phase))
|
|
240
|
+
if (inFlight) {
|
|
241
|
+
// The Host took the text. Whether it was accepted is the witness's word.
|
|
242
|
+
row.flight = { text: emptied, at, requestId: echo?.requestId }
|
|
243
|
+
const answer = latest.current.witness?.(row.flight)
|
|
244
|
+
if (answer === 'accepted') { drop(parts, row); return }
|
|
245
|
+
if (answer === 'failed') { row.flight = null; note('kept-failed'); return }
|
|
246
|
+
// Unknown: keep the text, and remember that a send is still open so a
|
|
247
|
+
// reload asks again instead of quietly showing the sent line back.
|
|
248
|
+
row.revision += 1
|
|
249
|
+
const marked = store.save({ ...parts, sessionId, text: emptied, revision: row.revision, pendingAt: at, pendingRequestId: echo?.requestId })
|
|
250
|
+
note(marked?.ok ? 'kept-unknown' : `refused:${marked?.reason}`)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (row.foreign) { note('kept-foreign'); return }
|
|
254
|
+
// A person deleted what they typed. Nothing may come back later — but the
|
|
255
|
+
// deletion is attributed to this conversation first, because a Host that
|
|
256
|
+
// clears its composer while switching sessions must not take the draft of
|
|
257
|
+
// the conversation it is leaving down with it.
|
|
258
|
+
const owner = row.key
|
|
259
|
+
clearTimeout(row.timer)
|
|
260
|
+
row.timer = setTimeout(() => {
|
|
261
|
+
row.timer = 0
|
|
262
|
+
if (local.current.key !== owner || latest.current.draft) return
|
|
263
|
+
drop(partsOf(owner), row)
|
|
264
|
+
}, 0)
|
|
265
|
+
}, [draft, store, sessionId, queue, submissions, phase, evidenceRevision])
|
|
266
|
+
|
|
267
|
+
// A window that goes away writes what is pending before it does.
|
|
268
|
+
useEffect(() => () => { clearTimeout(local.current.timer); store?.flush() }, [store])
|
|
269
|
+
|
|
270
|
+
if (status !== 'kept-unknown') return null
|
|
271
|
+
const recover = () => {
|
|
272
|
+
const row = local.current, parts = partsOf(row.key), record = store?.read(parts)
|
|
273
|
+
if (!record?.text || latest.current.draft) return
|
|
274
|
+
if (face.current.insertText(record.text, face.current.captureInsertion()) === false) return
|
|
275
|
+
row.flight = null
|
|
276
|
+
note('restored')
|
|
277
|
+
}
|
|
278
|
+
return h('span', { className: 'bp-draft-recovery' },
|
|
279
|
+
h('span', { role: 'status' }, '发送结果待确认,草稿已保留'),
|
|
280
|
+
h('button', { type: 'button', onClick: recover }, '恢复草稿'),
|
|
281
|
+
h('button', { type: 'button', onClick: () => drop(partsOf(local.current.key), local.current) }, '清除草稿'))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const COMPOSER_DRAFT_STATUSES = Object.freeze([
|
|
285
|
+
'idle', 'empty', 'kept-typing', 'no-input-face', 'restore-refused',
|
|
286
|
+
'restored', 'saved', 'cleared', 'kept-unknown', 'kept-failed', 'kept-foreign',
|
|
287
|
+
])
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did this exact send reach the Host? The only honest answers are the ones the
|
|
3
|
+
* session contract already publishes.
|
|
4
|
+
*
|
|
5
|
+
* The contract (verified against the installed SDK, not guessed):
|
|
6
|
+
* - `ISession.beginSubmission()` registers one local echo in
|
|
7
|
+
* `snapshot.pendingSubmissions` synchronously, before the prompt is
|
|
8
|
+
* serialized, and mints the `requestId` the prompt must carry.
|
|
9
|
+
* `@deepseek-ai/dsh-api-session-controller/lib/types/client/contract/session.d.ts:63-84`
|
|
10
|
+
* - `ISession.prompt(content, mode, signal, requestId)` answers
|
|
11
|
+
* `{accepted:true}` or the business error, and mirrors a failure into
|
|
12
|
+
* `snapshot.promptError` with `op:'send'`. (same file, 74-84)
|
|
13
|
+
* - A pending echo retires with `reason:'observed'` once its durable
|
|
14
|
+
* `user/message` event or its host queue occurrence arrived, and with
|
|
15
|
+
* `reason:'failed'` when the prompt was rejected, threw, or was aborted
|
|
16
|
+
* before acceptance. (same file, 18-28)
|
|
17
|
+
* - `PendingSubmission.requestId` is "the prompt RPC identity; the durable
|
|
18
|
+
* `user/message` source echoes it as `rpcId`".
|
|
19
|
+
* `.../client/contract/snapshot.d.ts:38-49`
|
|
20
|
+
*
|
|
21
|
+
* So the identity of one send is the requestId, and acceptance is that requestId
|
|
22
|
+
* showing up as a durable user message. Nothing else counts: a composer phase,
|
|
23
|
+
* an empty input, an elapsed timeout, a historical message with the same text, or
|
|
24
|
+
* a Bot reply. This module only reads those published fields.
|
|
25
|
+
*
|
|
26
|
+
* Everything here is pure and offline; the React side passes the snapshots in.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** How long after the composer emptied an echo may still be matched to it. */
|
|
30
|
+
export const SUBMISSION_MATCH_WINDOW_MS = 5000
|
|
31
|
+
|
|
32
|
+
const textOf = value => (typeof value === 'string' ? value : '')
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The echo this send produced, if one is there. The text and the moment only
|
|
36
|
+
* *find* the echo; the identity that comes back is the requestId, which is what
|
|
37
|
+
* later evidence has to match. A historical message with the same text is not an
|
|
38
|
+
* echo: it is not in `pendingSubmissions` at all.
|
|
39
|
+
*
|
|
40
|
+
* @param {readonly {requestId?: unknown, text?: unknown, time?: unknown}[]} submissions
|
|
41
|
+
* @param {{ text: string, at: number, windowMs?: number }} send
|
|
42
|
+
*/
|
|
43
|
+
export function matchSubmission(submissions, { text, at, windowMs = SUBMISSION_MATCH_WINDOW_MS, before = [] }) {
|
|
44
|
+
const wanted = textOf(text)
|
|
45
|
+
const moment = Number(at)
|
|
46
|
+
if (!wanted || !Number.isFinite(moment) || !Array.isArray(submissions)) return null
|
|
47
|
+
const known = new Set(before)
|
|
48
|
+
const matches = new Map()
|
|
49
|
+
for (const echo of submissions) {
|
|
50
|
+
if (typeof echo?.requestId !== 'string' || !echo.requestId || known.has(echo.requestId)) continue
|
|
51
|
+
if (textOf(echo?.text) !== wanted) continue
|
|
52
|
+
const started = Number(echo?.time ?? 0)
|
|
53
|
+
if (!Number.isFinite(started)) continue
|
|
54
|
+
// The echo belongs to this send when it began with it, not minutes later.
|
|
55
|
+
if (started < moment - windowMs || started > moment + windowMs) continue
|
|
56
|
+
matches.set(echo.requestId, { time: started, requestId: echo.requestId })
|
|
57
|
+
}
|
|
58
|
+
if (matches.size !== 1) return null
|
|
59
|
+
const found = [...matches.values()][0]
|
|
60
|
+
return { requestId: found.requestId, text: wanted, time: found.time }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The requestIds the durable conversation already carries. A user message node
|
|
65
|
+
* echoes its prompt identity as `source.rpcId`; a Bot's own reply carries none,
|
|
66
|
+
* so a reply can never be mistaken for this send.
|
|
67
|
+
*/
|
|
68
|
+
export function observedRequestIds(events) {
|
|
69
|
+
const seen = new Set()
|
|
70
|
+
if (!Array.isArray(events)) return seen
|
|
71
|
+
for (const event of events) {
|
|
72
|
+
if (event?.type !== 'user/message' || event?.data?.source?.kind !== 'user') continue
|
|
73
|
+
const rpcId = event.data.source.rpcId
|
|
74
|
+
if (typeof rpcId === 'string' && rpcId) seen.add(rpcId)
|
|
75
|
+
}
|
|
76
|
+
return seen
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A send failure the session published: `promptError.op === 'send'`. */
|
|
80
|
+
export const sendFailed = promptError => promptError?.op === 'send'
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The outcome of one identified send, or `undefined` when the Host has not said.
|
|
84
|
+
* `undefined` is the common case and it means "keep the draft": an outcome that
|
|
85
|
+
* was never observed is not a success.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} state
|
|
88
|
+
* @param {{requestId:string}} state.pending the identity captured when the send began
|
|
89
|
+
* @param {readonly object[]} [state.events] durable conversation events
|
|
90
|
+
* @param {readonly string[]} [state.acceptedRequestIds] exact durable admissions
|
|
91
|
+
* @param {readonly string[]} [state.failedRequestIds] identified failure receipts
|
|
92
|
+
*/
|
|
93
|
+
export function readSendOutcome({ pending, events, acceptedRequestIds = [], failedRequestIds = [] } = {}) {
|
|
94
|
+
if (!pending || typeof pending.requestId !== 'string' || !pending.requestId) return undefined
|
|
95
|
+
if (observedRequestIds(events).has(pending.requestId) || acceptedRequestIds.includes(pending.requestId)) return 'accepted'
|
|
96
|
+
// promptError has no request id. A stale send error cannot settle this send.
|
|
97
|
+
if (failedRequestIds.includes(pending.requestId)) return 'failed'
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** User-sourced rows the input face reports as queued right now. */
|
|
102
|
+
export function queuedUserRows(queue) {
|
|
103
|
+
if (!Array.isArray(queue)) return []
|
|
104
|
+
return queue.filter(row => row?.source?.kind === 'user')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The queue rows this read has that the previous one did not. Used only to decide
|
|
109
|
+
* "a send is in flight, so do not clear yet" — it never clears anything on its
|
|
110
|
+
* own, and a queue row is never read as acceptance of a specific send.
|
|
111
|
+
*/
|
|
112
|
+
export function newQueueRows(current, before) {
|
|
113
|
+
const known = new Set((Array.isArray(before) ? before : []).filter(id => typeof id === 'string'))
|
|
114
|
+
return (Array.isArray(current) ? current : []).filter(id => typeof id === 'string' && !known.has(id))
|
|
115
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
body[data-bot-product-ui] .bp-draft-recovery { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; max-width: 100%; font-size: 12px; color: var(--bp-chat-muted, #687486); }
|
|
2
|
+
body[data-bot-product-ui] .bp-draft-recovery button { border: 1px solid var(--bp-chat-line-strong, #7f8691); border-radius: 8px; padding: 5px 8px; background: var(--bp-chat-surface, #fff); color: var(--bp-chat-ink, #253247); cursor: pointer; }
|
|
3
|
+
body[data-bot-product-ui] .bp-draft-recovery button:focus-visible { outline: 2px solid var(--bp-chat-accent, #5174a0); outline-offset: 2px; }
|
|
4
|
+
@media (max-width: 600px) { body[data-bot-product-ui] .bp-draft-recovery button { min-height: 40px; } }
|