@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,224 @@
|
|
|
1
|
+
const failure = (code, message = code) => Object.assign(new Error(message), { code })
|
|
2
|
+
const BOT = /^[-A-Za-z0-9_.]{1,128}$/
|
|
3
|
+
const HOST = /^host-[0-9a-f]{16}$/
|
|
4
|
+
const KEY = /^[-A-Za-z0-9_]{16,128}$/
|
|
5
|
+
const STATES = new Set(['accepted', 'running', 'succeeded', 'unknown', 'refused', 'cancelled'])
|
|
6
|
+
const withoutProgress = status => {
|
|
7
|
+
if (!status) return status
|
|
8
|
+
const { progress, ...stored } = status
|
|
9
|
+
return stored
|
|
10
|
+
}
|
|
11
|
+
export const independentChatStorageKey = (hostId, botId) => `bot-product:independent-chat:1:${hostId}:${botId}`
|
|
12
|
+
export const independentChatNonce = () => {
|
|
13
|
+
if (typeof globalThis.crypto?.randomUUID !== 'function') throw failure('CHAT_NONCE_UNAVAILABLE', '浏览器无法生成安全请求编号。')
|
|
14
|
+
return globalThis.crypto.randomUUID()
|
|
15
|
+
}
|
|
16
|
+
export function validateChatRequest(value, botId, requestKey) {
|
|
17
|
+
if (value?.schemaVersion !== 1 || value.botId !== botId || !KEY.test(value.requestKey ?? '') || (requestKey && value.requestKey !== requestKey)
|
|
18
|
+
|| !STATES.has(value.state) || typeof value.pending !== 'boolean' || typeof value.finished !== 'boolean' || value.stopConfirmed !== false) throw failure('CHAT_IDENTITY_MISMATCH', '宿主返回的请求身份或状态无效,请保留原编号核对。')
|
|
19
|
+
const progress = value.progress
|
|
20
|
+
if (value.operationKind !== undefined && !['send', 'new-conversation'].includes(value.operationKind)) throw failure('CHAT_IDENTITY_MISMATCH')
|
|
21
|
+
if (progress != null && (value.operationKind === 'new-conversation' || !value.pending || !['accepted', 'running'].includes(value.state) || !Number.isSafeInteger(progress.step) || progress.step < 1 || progress.step > 64
|
|
22
|
+
|| !Number.isSafeInteger(progress.sequence) || progress.sequence < 1 || typeof progress.text !== 'string' || progress.text.length > 64000 || typeof progress.truncated !== 'boolean')) throw failure('CHAT_PROGRESS_INVALID', '宿主返回的生成预览格式无效。')
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
export function validateChatPage(value, botId) {
|
|
26
|
+
if (value?.schemaVersion !== 1 || value.botId !== botId || !Array.isArray(value.items) || value.items.length > 100
|
|
27
|
+
|| (value.nextCursor !== null && typeof value.nextCursor !== 'string')
|
|
28
|
+
|| value.items.some(row => !row || typeof row.messageId !== 'string' || !row.messageId || typeof row.content !== 'string'
|
|
29
|
+
|| !['user', 'assistant', 'tool', 'system'].includes(row.role) || row.contextId !== undefined && (typeof row.contextId !== 'string' || !row.contextId || row.contextId.length > 128))
|
|
30
|
+
|| new Set(value.items.map(row => row.messageId)).size !== value.items.length) throw failure('CHAT_PAGE_INVALID', '会话页缺少稳定消息编号,已拒绝显示可能错位的内容。')
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One mounted, validated Host/Bot. This metadata never authenticates an owner.
|
|
35
|
+
* A persisted uncertain key is inspected only. send() is reachable solely from
|
|
36
|
+
* the owner's explicit submit, after its nonce and exact text are durable.
|
|
37
|
+
*/
|
|
38
|
+
export function createIndependentChatController({ api, hostId, botId, storage = globalThis.localStorage, pollMs = 1500, timeoutMs = 8000,
|
|
39
|
+
nonce = independentChatNonce, onHostChanged } = {}) {
|
|
40
|
+
if (!HOST.test(hostId ?? '') || !BOT.test(botId ?? '') || typeof api?.dispatch !== 'function') throw failure('CHAT_SCOPE_INVALID')
|
|
41
|
+
const key = independentChatStorageKey(hostId, botId), listeners = new Set(), calls = new Set()
|
|
42
|
+
let live = false, retired = false, timer, polling = false, lock = false, pageLock = false, revision = 0, pageLoaded = false, exhausted = false
|
|
43
|
+
let nextNonce = nonce()
|
|
44
|
+
if (!KEY.test(nextNonce ?? '')) throw failure('CHAT_NONCE_INVALID')
|
|
45
|
+
let state = { hostId, botId, draft: '', pending: null, lastRequest: null, view: null, items: [], nextCursor: null,
|
|
46
|
+
loading: true, sending: false, loadingOlder: false, error: '', storageError: '', acknowledgedRequestKey: null }
|
|
47
|
+
const publish = patch => { if (retired) return; state = { ...state, ...patch }; for (const listener of listeners) listener() }
|
|
48
|
+
const persist = patch => {
|
|
49
|
+
const next = { ...state, ...patch }
|
|
50
|
+
try {
|
|
51
|
+
const pending = next.pending ? { ...next.pending, status: withoutProgress(next.pending.status) } : null
|
|
52
|
+
storage.setItem(key, JSON.stringify({ schemaVersion: 1, hostId, botId, draft: next.draft, pending, acknowledgedRequestKey: next.acknowledgedRequestKey }))
|
|
53
|
+
publish({ ...patch, storageError: '' }); return true
|
|
54
|
+
} catch { publish({ storageError: '无法保存本机草稿与请求编号,请恢复浏览器存储后再发送。' }); return false }
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const saved = JSON.parse(storage.getItem(key) ?? 'null')
|
|
58
|
+
if (saved?.schemaVersion === 1 && saved.hostId === hostId && saved.botId === botId) {
|
|
59
|
+
const pending = saved.pending && KEY.test(saved.pending.requestKey ?? '') && typeof saved.pending.input === 'string' && saved.pending.input.length <= 64000
|
|
60
|
+
? { ...saved.pending, status: withoutProgress(saved.pending.status) } : null
|
|
61
|
+
state = { ...state, draft: typeof saved.draft === 'string' ? saved.draft.slice(0, 64000) : '', pending, acknowledgedRequestKey: KEY.test(saved.acknowledgedRequestKey ?? '') ? saved.acknowledgedRequestKey : null }
|
|
62
|
+
}
|
|
63
|
+
} catch { state.storageError = '无法读取本机草稿与请求编号;请修复浏览器存储后再发送。' }
|
|
64
|
+
const call = async (action, payload) => {
|
|
65
|
+
const controller = new AbortController(); calls.add(controller)
|
|
66
|
+
let alarm, abort
|
|
67
|
+
const stopped = new Promise((_, reject) => {
|
|
68
|
+
abort = () => reject(failure('CHAT_TRANSPORT_UNKNOWN', '连接中断,发送结果尚未确认;只核对原请求,不会自动重发。'))
|
|
69
|
+
controller.signal.addEventListener('abort', abort, { once: true })
|
|
70
|
+
alarm = setTimeout(() => controller.abort(), timeoutMs)
|
|
71
|
+
})
|
|
72
|
+
try { return await Promise.race([Promise.resolve().then(() => api.dispatch(action, payload, controller.signal)), stopped]) }
|
|
73
|
+
finally { clearTimeout(alarm); calls.delete(controller); controller.signal.removeEventListener('abort', abort) }
|
|
74
|
+
}
|
|
75
|
+
const assertHost = async () => {
|
|
76
|
+
const namespace = await call('ui.namespace', {})
|
|
77
|
+
if (!live) throw failure('CHAT_SCOPE_RETIRED')
|
|
78
|
+
if (namespace?.schemaVersion !== 1 || !HOST.test(namespace.hostId ?? '')) throw failure('CHAT_SCOPE_INVALID', '宿主没有确认界面存储标识。')
|
|
79
|
+
if (namespace.hostId !== hostId) {
|
|
80
|
+
publish({ view: null, error: '宿主连接已切换,正在重新读取联系人与草稿。' })
|
|
81
|
+
live = false; revision++; clearTimeout(timer)
|
|
82
|
+
for (const controller of calls) controller.abort()
|
|
83
|
+
onHostChanged?.()
|
|
84
|
+
throw failure('CHAT_SCOPE_RETIRED')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const applyRequest = (value, requestKey) => {
|
|
88
|
+
const request = validateChatRequest(value, botId, requestKey)
|
|
89
|
+
const held = state.pending
|
|
90
|
+
if (held && held.requestKey !== request.requestKey) return
|
|
91
|
+
const operationKind = request.operationKind ?? held?.operationKind ?? 'send'
|
|
92
|
+
if (held?.operationKind && held.operationKind !== operationKind) throw failure('CHAT_IDENTITY_MISMATCH')
|
|
93
|
+
const pending = request.state === 'succeeded' ? null : { requestKey: request.requestKey,
|
|
94
|
+
operationKind, input: held?.input ?? request.recoveryInput ?? '', accepted: request.durableInput === true, status: request }
|
|
95
|
+
const clearDraft = operationKind === 'send' && request.durableInput === true && held && state.draft === held.input
|
|
96
|
+
persist({ pending, ...(clearDraft ? { draft: '' } : {}), lastRequest: request,
|
|
97
|
+
...(request.state === 'succeeded' ? { acknowledgedRequestKey: request.requestKey } : {}) })
|
|
98
|
+
if (request.state === 'succeeded') nextNonce = nonce()
|
|
99
|
+
}
|
|
100
|
+
const loadMessages = async (cursor, older = false) => {
|
|
101
|
+
const page = validateChatPage(await call('independentChat.messages', { botId, limit: 40, ...(cursor ? { cursor } : {}) }), botId)
|
|
102
|
+
if (!live) return
|
|
103
|
+
const seen = new Set()
|
|
104
|
+
const combined = older ? [...page.items, ...state.items] : [...state.items.filter(row => !page.items.some(next => next.messageId === row.messageId)), ...page.items]
|
|
105
|
+
publish({ items: combined.filter(row => { if (seen.has(row.messageId)) return false; seen.add(row.messageId); return true }),
|
|
106
|
+
...((older || !pageLoaded) ? { nextCursor: exhausted ? null : page.nextCursor } : {}) })
|
|
107
|
+
pageLoaded = true
|
|
108
|
+
if (older && page.nextCursor === null) exhausted = true
|
|
109
|
+
}
|
|
110
|
+
const refresh = async () => {
|
|
111
|
+
if (!live || polling || lock) return
|
|
112
|
+
polling = true
|
|
113
|
+
const mine = revision
|
|
114
|
+
try {
|
|
115
|
+
await assertHost()
|
|
116
|
+
if (!live || mine !== revision) return
|
|
117
|
+
const view = await call('independentChat.view', { botId })
|
|
118
|
+
if (!live || mine !== revision) return
|
|
119
|
+
if (view?.schemaVersion !== 1 || view.botId !== botId || !['independent', 'integration', null].includes(view.mode) || typeof view.available !== 'boolean') throw failure('CHAT_VIEW_INVALID', '宿主聊天状态无效。')
|
|
120
|
+
publish({ view, error: '' })
|
|
121
|
+
if (state.pending) {
|
|
122
|
+
const requestKey = state.pending.requestKey
|
|
123
|
+
try {
|
|
124
|
+
const value = await call('independentChat.inspect', { botId, requestKey })
|
|
125
|
+
if (!live || mine !== revision) return
|
|
126
|
+
applyRequest(value, requestKey)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (live && mine === revision && error?.code === 'INDEPENDENT_CHAT_REQUEST_NOT_FOUND' && state.pending?.requestKey === requestKey) persist({ pending: { ...state.pending, notFound: true } })
|
|
129
|
+
throw error
|
|
130
|
+
}
|
|
131
|
+
} else if (view.pendingRequest || (view.latestRequest && view.latestRequest.requestKey !== state.acknowledgedRequestKey)) {
|
|
132
|
+
applyRequest(view.pendingRequest ?? view.latestRequest)
|
|
133
|
+
}
|
|
134
|
+
if (live && mine === revision) await loadMessages()
|
|
135
|
+
if (live && mine === revision) publish({ loading: false })
|
|
136
|
+
} catch (error) { if (live && mine === revision) publish({ loading: false, view: null, error: String(error?.message ?? error) }) }
|
|
137
|
+
finally {
|
|
138
|
+
polling = false
|
|
139
|
+
if (live) { clearTimeout(timer); timer = setTimeout(refresh, pollMs) }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
|
|
144
|
+
getSnapshot: () => state,
|
|
145
|
+
start() { if (retired || live) return; live = true; void refresh() },
|
|
146
|
+
dispose() { live = false; retired = true; revision++; clearTimeout(timer); for (const controller of calls) controller.abort(); listeners.clear() },
|
|
147
|
+
setDraft(text) { if (!retired && typeof text === 'string') persist({ draft: text.slice(0, 64000) }) },
|
|
148
|
+
async send() {
|
|
149
|
+
if (!live || lock || state.pending || !state.view?.available || !state.draft.trim()) return
|
|
150
|
+
if (new TextEncoder().encode(state.draft).length > 64000) { publish({ error: '消息超过 64,000 字节,请缩短后发送。' }); return }
|
|
151
|
+
lock = true; revision++
|
|
152
|
+
let requestKey
|
|
153
|
+
try {
|
|
154
|
+
await assertHost()
|
|
155
|
+
if (!live) return
|
|
156
|
+
requestKey = nextNonce
|
|
157
|
+
if (!KEY.test(requestKey)) throw failure('CHAT_NONCE_INVALID')
|
|
158
|
+
const pending = { requestKey, input: state.draft, accepted: false, status: null }
|
|
159
|
+
// Must complete before the first network send. Double click and Enter
|
|
160
|
+
// share this synchronous lock and this one durable nonce.
|
|
161
|
+
if (!persist({ pending, sending: true, error: '' })) return
|
|
162
|
+
const value = await call('independentChat.send', { botId, requestKey, input: pending.input })
|
|
163
|
+
if (live) applyRequest(value, requestKey)
|
|
164
|
+
} catch (error) { if (live) publish({ error: String(error?.message ?? error) }) }
|
|
165
|
+
finally { lock = false; if (live) { publish({ sending: false }); clearTimeout(timer); timer = setTimeout(refresh, 0) } }
|
|
166
|
+
},
|
|
167
|
+
async cancel() {
|
|
168
|
+
if (!live || lock || !state.pending) return
|
|
169
|
+
lock = true; revision++
|
|
170
|
+
const requestKey = state.pending.requestKey
|
|
171
|
+
try { await assertHost(); if (!live) return; const value = await call('independentChat.cancel', { botId, requestKey }); if (live) applyRequest(value, requestKey) }
|
|
172
|
+
catch (error) { if (live) publish({ error: String(error?.message ?? error) }) }
|
|
173
|
+
finally { lock = false; if (live) { clearTimeout(timer); timer = setTimeout(refresh, 0) } }
|
|
174
|
+
},
|
|
175
|
+
async newConversation() {
|
|
176
|
+
if (!live || lock || state.pending || !state.view?.newConversationAvailable || state.storageError) return
|
|
177
|
+
lock = true; revision++
|
|
178
|
+
try {
|
|
179
|
+
await assertHost()
|
|
180
|
+
if (!live) return
|
|
181
|
+
const requestKey = nextNonce
|
|
182
|
+
if (!KEY.test(requestKey)) throw failure('CHAT_NONCE_INVALID')
|
|
183
|
+
if (!persist({ pending: { requestKey, input: '', operationKind: 'new-conversation', accepted: false, status: null }, sending: true, error: '' })) return
|
|
184
|
+
const value = await call('independentChat.newConversation', { botId, requestKey })
|
|
185
|
+
if (live) applyRequest(value, requestKey)
|
|
186
|
+
} catch (error) { if (live) publish({ error: String(error?.message ?? error) }) }
|
|
187
|
+
finally { lock = false; if (live) { publish({ sending: false }); clearTimeout(timer); timer = setTimeout(refresh, 0) } }
|
|
188
|
+
},
|
|
189
|
+
inspect() { clearTimeout(timer); void refresh() },
|
|
190
|
+
restore() {
|
|
191
|
+
const held = state.pending
|
|
192
|
+
if (!live || lock || !held || held.status?.pending !== false || held.status?.finished !== true) return
|
|
193
|
+
if (held.operationKind === 'new-conversation') return false
|
|
194
|
+
const recovered = held.status.recoveryInput ?? held.input
|
|
195
|
+
if (state.draft && state.draft !== recovered) {
|
|
196
|
+
publish({ error: '输入框已有未发送文字,请先复制或清空,再恢复原消息。' })
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
// Restore is a human choice. It only fills the composer. The next send
|
|
200
|
+
// is a new explicit intent, after the old outcome is shown to the owner.
|
|
201
|
+
persist({ draft: recovered, pending: null, lastRequest: held.status, acknowledgedRequestKey: held.requestKey })
|
|
202
|
+
nextNonce = nonce()
|
|
203
|
+
},
|
|
204
|
+
releaseNotFound() {
|
|
205
|
+
// An unacknowledged transport request can be explicitly released only
|
|
206
|
+
// after the authoritative journal answers not-found, never on a timeout.
|
|
207
|
+
if (state.pending?.notFound === true) {
|
|
208
|
+
if (state.pending.operationKind === 'new-conversation') return false
|
|
209
|
+
if (state.draft && state.draft !== state.pending.input) {
|
|
210
|
+
publish({ error: '输入框已有未发送文字,请先复制或清空,再恢复原消息。' })
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
persist({ draft: state.pending.input, pending: null, acknowledgedRequestKey: state.pending.requestKey }); nextNonce = nonce()
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async older() {
|
|
217
|
+
if (!live || pageLock || !state.nextCursor) return
|
|
218
|
+
pageLock = true; publish({ loadingOlder: true })
|
|
219
|
+
try { await loadMessages(state.nextCursor, true) } catch (error) { if (live) publish({ error: String(error?.message ?? error) }) }
|
|
220
|
+
finally { pageLock = false; if (live) publish({ loadingOlder: false }) }
|
|
221
|
+
},
|
|
222
|
+
reloadMessages() { pageLoaded = false; exhausted = false; publish({ items: [], nextCursor: null }); clearTimeout(timer); void refresh() },
|
|
223
|
+
})
|
|
224
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
3
|
+
import { openIndependentChat } from './independent-chat-navigation.mjs'
|
|
4
|
+
|
|
5
|
+
let nextScope = 0
|
|
6
|
+
export function readWithDeadline(work, controller, ms = 8000) {
|
|
7
|
+
let timer, abort
|
|
8
|
+
const expired = new Promise((_, reject) => {
|
|
9
|
+
abort = () => reject(new Error('当前宿主没有及时回答,请重试连接。'))
|
|
10
|
+
controller.signal.addEventListener('abort', abort, { once: true })
|
|
11
|
+
timer = setTimeout(() => controller.abort(), ms)
|
|
12
|
+
})
|
|
13
|
+
return Promise.race([Promise.resolve().then(work), expired]).finally(() => { clearTimeout(timer); controller.signal.removeEventListener('abort', abort) })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Native navigation remains usable without loading the independent transcript renderer. */
|
|
17
|
+
export function IndependentChatEntry({ productApi, botId, onOpen, openPanel }) {
|
|
18
|
+
const scope = useMemo(() => ++nextScope, [productApi, botId])
|
|
19
|
+
return <ChatEntry key={scope} productApi={productApi} botId={botId} onOpen={onOpen} openPanel={openPanel}/>
|
|
20
|
+
}
|
|
21
|
+
function ChatEntry({ productApi, botId, onOpen, openPanel }) {
|
|
22
|
+
const [independent, setIndependent] = useState(false)
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!botId || !productApi?.dispatch) return
|
|
25
|
+
let live = true, timer, controller
|
|
26
|
+
const load = async () => {
|
|
27
|
+
controller = new AbortController()
|
|
28
|
+
try { const view = await readWithDeadline(() => productApi.dispatch('independentChat.view', { botId }, controller.signal), controller); if (live) setIndependent(view?.botId === botId && view.mode === 'independent') }
|
|
29
|
+
catch { if (live) setIndependent(false) }
|
|
30
|
+
finally { if (live) timer = setTimeout(load, 4000) }
|
|
31
|
+
}
|
|
32
|
+
void load()
|
|
33
|
+
return () => { live = false; controller?.abort(); clearTimeout(timer) }
|
|
34
|
+
}, [productApi, botId])
|
|
35
|
+
if (!independent) return null
|
|
36
|
+
return <button className="ic-native-entry" type="button" title="此 Bot 已使用独立运行时,请在独立聊天页继续发送" onClick={() => typeof onOpen === 'function' ? onOpen(botId) : openIndependentChat(productApi, openPanel, botId)}><UiIcon name="bot" size={16}/><span>打开独立聊天</span><UiIcon name="arrow" size={14}/></button>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function BotConversationLink({ productApi, bot, openSession, onOpenIndependent, available }) {
|
|
40
|
+
const [error, setError] = useState(''), [busy, setBusy] = useState(false)
|
|
41
|
+
const current = useRef({ live: false, epoch: 0, controller: null, pending: false })
|
|
42
|
+
useEffect(() => { const held = current.current; held.live = true; held.epoch++; held.pending = false; setError(''); setBusy(false); return () => { held.live = false; held.epoch++; held.controller?.abort() } }, [productApi, bot.id])
|
|
43
|
+
const open = async () => {
|
|
44
|
+
const held = current.current, epoch = held.epoch
|
|
45
|
+
if (held.pending || !held.live) return
|
|
46
|
+
held.pending = true
|
|
47
|
+
const controller = new AbortController(); held.controller = controller
|
|
48
|
+
setBusy(true); setError('')
|
|
49
|
+
try {
|
|
50
|
+
if (!available) { openSession?.(bot.sessionId); return }
|
|
51
|
+
const view = await readWithDeadline(() => productApi.dispatch('independentChat.view', { botId: bot.id }, controller.signal), controller)
|
|
52
|
+
if (!held.live || held.epoch !== epoch) return
|
|
53
|
+
if (view?.botId !== bot.id) throw new Error('宿主返回的 Bot 身份不匹配。')
|
|
54
|
+
if (view.mode === 'independent') onOpenIndependent?.(bot.id)
|
|
55
|
+
else if (view.mode === 'integration') openSession?.(bot.sessionId)
|
|
56
|
+
else throw new Error('宿主未确认当前运行时,请检查连接后重试。')
|
|
57
|
+
} catch (cause) { if (held.live && held.epoch === epoch) setError(String(cause?.message ?? cause)) }
|
|
58
|
+
finally { if (held.live && held.epoch === epoch) { held.pending = false; setBusy(false) } }
|
|
59
|
+
}
|
|
60
|
+
return <div><button className="bp-button-primary" type="button" disabled={busy} onClick={() => { void open() }}>{busy ? '正在打开…' : '打开私聊'}<UiIcon name="arrow" size={15}/></button>{error && <p className="bp-error" role="alert">{error}</p>}</div>
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const INDEPENDENT_CHAT_PANEL = 'bot-product-independentChat'
|
|
2
|
+
const scopes = new WeakMap()
|
|
3
|
+
export function independentChatNavigation(api) {
|
|
4
|
+
if (scopes.has(api)) return scopes.get(api)
|
|
5
|
+
let snapshot = null, version = 0
|
|
6
|
+
const listeners = new Set()
|
|
7
|
+
const navigation = Object.freeze({
|
|
8
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
|
|
9
|
+
getSnapshot: () => snapshot,
|
|
10
|
+
select(botId) {
|
|
11
|
+
if (typeof botId !== 'string' || !/^[-A-Za-z0-9_.]{1,128}$/.test(botId)) throw new TypeError('CHAT_BOT_REQUIRED')
|
|
12
|
+
snapshot = Object.freeze({ botId, version: ++version })
|
|
13
|
+
for (const listener of listeners) listener()
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
scopes.set(api, navigation); return navigation
|
|
17
|
+
}
|
|
18
|
+
export function openIndependentChat(api, openPanel, botId) {
|
|
19
|
+
independentChatNavigation(api).select(botId)
|
|
20
|
+
return openPanel?.(INDEPENDENT_CHAT_PANEL)
|
|
21
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
|
2
|
+
import { ComposerDraftHost } from './composer-draft-host.jsx'
|
|
3
|
+
import { createIndependentChatController } from './independent-chat-controller.mjs'
|
|
4
|
+
import { independentChatNavigation } from './independent-chat-navigation.mjs'
|
|
5
|
+
import { readWithDeadline } from './independent-chat-entry.jsx'
|
|
6
|
+
export { IndependentChatEntry, BotConversationLink } from './independent-chat-entry.jsx'
|
|
7
|
+
import { BotSettingsDialog } from './bot-settings-dialog.jsx'
|
|
8
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
9
|
+
import { botModelStatus } from './bot-model-status.mjs'
|
|
10
|
+
import { requestWorkbenchView } from '../../.dsh-plugin/client/workbench-state.mjs'
|
|
11
|
+
import { IndependentRichMessage } from './independent-chat-rich.jsx'
|
|
12
|
+
import { independentToolRelations } from './independent-chat-rich.mjs'
|
|
13
|
+
import { NativeActivityCardView, useNativeActivity } from './native-activity.jsx'
|
|
14
|
+
|
|
15
|
+
let nextScope = 0
|
|
16
|
+
export function IndependentChatPage(props) {
|
|
17
|
+
const scope = useMemo(() => ++nextScope, [props.productApi])
|
|
18
|
+
return <ChatDirectory key={scope} {...props}/>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ChatDirectory({ productApi, openPanel, openSession }) {
|
|
22
|
+
const navigation = useMemo(() => independentChatNavigation(productApi), [productApi])
|
|
23
|
+
const route = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot, navigation.getSnapshot)
|
|
24
|
+
const [directory, setDirectory] = useState(null), [error, setError] = useState(''), [attempt, setAttempt] = useState(0)
|
|
25
|
+
const reconnect = useCallback(() => { setDirectory(null); setAttempt(value => value + 1) }, [])
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let live = true, timer, controller
|
|
28
|
+
const load = async () => {
|
|
29
|
+
controller = new AbortController()
|
|
30
|
+
try {
|
|
31
|
+
const snapshot = await readWithDeadline(() => productApi.snapshot(controller.signal), controller)
|
|
32
|
+
if (!live) return
|
|
33
|
+
if (snapshot?.schemaVersion !== 1 || !Array.isArray(snapshot.bots)) throw new Error('宿主未返回 Bot 联系人。')
|
|
34
|
+
setDirectory(snapshot); setError('')
|
|
35
|
+
} catch (cause) { if (live) setError(String(cause?.message ?? cause)) }
|
|
36
|
+
finally { if (live) timer = setTimeout(load, 4000) }
|
|
37
|
+
}
|
|
38
|
+
void load()
|
|
39
|
+
return () => { live = false; controller?.abort(); clearTimeout(timer) }
|
|
40
|
+
}, [productApi, attempt])
|
|
41
|
+
const bots = directory?.bots ?? [], selected = route?.botId ?? directory?.mainBotId ?? bots[0]?.id
|
|
42
|
+
const bot = bots.find(row => row.id === selected)
|
|
43
|
+
return <section className="ic-shell" aria-label="独立 Bot 聊天">
|
|
44
|
+
<aside className="ic-contacts"><div className="ic-contact-title"><UiIcon name="team" size={19}/><strong>Bot 伙伴</strong></div>
|
|
45
|
+
<nav aria-label="独立聊天联系人">{bots.map(row => <button key={row.id} type="button" aria-current={row.id === selected ? 'page' : undefined} onClick={() => navigation.select(row.id)}><span className="ic-avatar"><UiIcon name="bot" size={21}/></span><span><strong>{row.name || row.id}</strong><small>{row.role || '和你的 Bot 一起工作'}</small></span></button>)}</nav>
|
|
46
|
+
<button className="ic-team-link" type="button" onClick={() => openPanel?.('bot-product-bindings')}>管理团队<UiIcon name="chevron" size={14}/></button>
|
|
47
|
+
</aside>
|
|
48
|
+
<div className="ic-conversation">{error ? <div className="ic-alert" role="alert">无法读取当前宿主联系人:{error}<button type="button" onClick={() => { setDirectory(null); setAttempt(value => value + 1) }}>重试连接</button></div>
|
|
49
|
+
: !directory ? <p className="ic-placeholder" role="status">正在连接 Bot 伙伴…</p>
|
|
50
|
+
: !bot ? <p className="ic-placeholder" role="status">当前宿主中没有这个 Bot。请从联系人重新选择。</p>
|
|
51
|
+
: <ComposerDraftHost productApi={productApi} attempt={attempt}>{({ hostId, status, error: namespaceError }) => status === 'ready'
|
|
52
|
+
? <ChatConversation key={`${hostId}:${bot.id}`} productApi={productApi} hostId={hostId} bot={bot} readOnly={directory.readOnly} nativeActivityAvailable={directory.availableActions?.includes('nativeActivity.list') === true} openSession={openSession} openPanel={openPanel} onHostChanged={reconnect}/>
|
|
53
|
+
: <div className="ic-placeholder" role="status">{status === 'error' ? <>{namespaceError}<button type="button" onClick={() => setAttempt(value => value + 1)}>重试连接</button></> : '正在确认当前宿主…'}</div>}</ComposerDraftHost>}
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const requestText = request => {
|
|
59
|
+
if (request?.operationKind === 'new-conversation') {
|
|
60
|
+
if (request.state === 'succeeded') return '新对话已开启,旧记录已保留。'
|
|
61
|
+
if (['accepted', 'running'].includes(request.state)) return '正在开启新对话,核对旧会话的停止状态…'
|
|
62
|
+
return '新对话结果尚未确认,请核对原操作;旧聊天记录保留。'
|
|
63
|
+
}
|
|
64
|
+
if (!request) return '发送结果尚未确认。请核对原请求;不会自动重发。'
|
|
65
|
+
if (request.state === 'succeeded') return request.cancelRequested ? '回复已提交;之前已请求停止。' : '回复已提交。'
|
|
66
|
+
if (request.cancelRequested) return request.finished ? '本地回合已结算,已请求停止;工具停止尚未确认。' : '已请求停止,正在核对回合结果;工具停止尚未确认。'
|
|
67
|
+
if (request.state === 'accepted') return '消息已保存,等待 Bot 回复…'
|
|
68
|
+
if (request.state === 'running') return 'Bot 正在处理…'
|
|
69
|
+
if (request.state === 'unknown') return request.pending ? '原宿主的回合可能仍在运行,请核对原请求。' : '回合结果未知,请检查已有消息与工具记录后再决定是否恢复文字。'
|
|
70
|
+
if (request.state === 'refused') return '宿主拒绝了这次发送,可以恢复文字后修改。'
|
|
71
|
+
return '本地回合已结算,可以恢复文字;工具停止尚未确认。'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ChatConversation({ productApi, hostId, bot, readOnly, nativeActivityAvailable, openSession, openPanel, onHostChanged }) {
|
|
75
|
+
const controller = useMemo(() => createIndependentChatController({ api: productApi, hostId, botId: bot.id, onHostChanged }), [productApi, hostId, bot.id, onHostChanged])
|
|
76
|
+
const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot)
|
|
77
|
+
const [settings, setSettings] = useState(false), [following, setFollowing] = useState(true)
|
|
78
|
+
const scroll = useRef(null), input = useRef(null), composition = useRef(false), olderPosition = useRef(null)
|
|
79
|
+
useEffect(() => { controller.start(); return () => controller.dispose() }, [controller])
|
|
80
|
+
const pending = state.pending, request = pending?.status ?? state.lastRequest
|
|
81
|
+
const progress = !state.loading && !state.error && state.view?.available && pending?.status?.state === 'running' && !pending.status.cancelRequested ? pending.status.progress : null
|
|
82
|
+
const contextOperation = pending?.operationKind === 'new-conversation' || request?.operationKind === 'new-conversation'
|
|
83
|
+
const knownPending = pending && ['accepted', 'running'].includes(pending.status?.state) && !pending.status.cancelRequested
|
|
84
|
+
const composingNext = knownPending && !contextOperation
|
|
85
|
+
const showRequest = Boolean(pending || (request && (request.state !== 'succeeded' || request.cancelRequested)))
|
|
86
|
+
const model = botModelStatus(bot)
|
|
87
|
+
const toolRelations = useMemo(() => independentToolRelations(state.items), [state.items])
|
|
88
|
+
const canSend = state.view?.available === true && !state.loading && !readOnly && !pending && !state.sending && !state.storageError
|
|
89
|
+
const context = state.view?.context
|
|
90
|
+
// Native tool activity for the context that is actually open. A context change
|
|
91
|
+
// retires the previous read, so another conversation's records never appear here.
|
|
92
|
+
const nativeActivity = useNativeActivity({ productApi, botId: bot.id, mode: 'independent', contextId: context?.contextId, enabled: nativeActivityAvailable })
|
|
93
|
+
const contextBoundary = () => <div className="ic-context-boundary" role="separator" aria-label="新对话,旧聊天记录保留"><span>新对话 · 旧记录保留</span></div>
|
|
94
|
+
useLayoutEffect(() => {
|
|
95
|
+
const node = scroll.current
|
|
96
|
+
if (!node) return
|
|
97
|
+
if (olderPosition.current !== null && !state.loadingOlder) { node.scrollTop += node.scrollHeight - olderPosition.current; olderPosition.current = null }
|
|
98
|
+
else if (following) node.scrollTop = node.scrollHeight
|
|
99
|
+
}, [state.items, state.loadingOlder, progress?.sequence, following, nativeActivity.activity?.revision ?? 0])
|
|
100
|
+
useLayoutEffect(() => {
|
|
101
|
+
const node = scroll.current
|
|
102
|
+
if (!node || typeof ResizeObserver === 'undefined') return
|
|
103
|
+
const observer = new ResizeObserver(() => {
|
|
104
|
+
if (following && olderPosition.current === null) node.scrollTop = node.scrollHeight
|
|
105
|
+
})
|
|
106
|
+
observer.observe(node)
|
|
107
|
+
if (node.firstElementChild) observer.observe(node.firstElementChild)
|
|
108
|
+
return () => observer.disconnect()
|
|
109
|
+
}, [following])
|
|
110
|
+
const submit = () => { if (canSend && !composition.current) void controller.send() }
|
|
111
|
+
return <>
|
|
112
|
+
<header className="ic-header"><span className="ic-avatar"><UiIcon name="bot" size={22}/></span><div className="ic-heading"><strong>{bot.name || bot.id}</strong><button type="button" className="ic-model" title={`独立聊天 · ${model.title}`} aria-label={`宿主已保存模型:${model.selected ?? '未选择'},打开模型与工具设置`} data-model-different={model.different ? '' : undefined} onClick={() => setSettings(true)}><UiIcon name="spark" size={12}/><span>宿主已选:{model.selectedLabel ?? '未选择模型'}</span><UiIcon name="chevron" size={10}/></button></div><div className="ic-header-actions">
|
|
113
|
+
{typeof state.view?.newConversationAvailable === 'boolean' && <button type="button" title="开启新对话,保留旧聊天记录" aria-label="开启新对话" disabled={!canSend || !state.view.newConversationAvailable} onClick={() => void controller.newConversation()}><UiIcon name="plus" size={18}/></button>}
|
|
114
|
+
{bot.sessionId && <button type="button" title="打开此 Bot 的原生会话" aria-label="打开此 Bot 的原生会话" onClick={() => openSession?.(bot.sessionId)}><UiIcon name="list" size={18}/></button>}
|
|
115
|
+
<button type="button" title="Bot 设置" aria-label="Bot 设置" onClick={() => setSettings(true)}><UiIcon name="settings" size={18}/></button>
|
|
116
|
+
</div></header>
|
|
117
|
+
{readOnly && <div className="ic-alert" role="alert">当前宿主为只读:{String(readOnly)}</div>}
|
|
118
|
+
{state.view && !state.view.available && <div className="ic-alert" role="status">{state.view.mode === 'integration' ? '这个 Bot 使用原生会话。独立聊天记录保留为只读。' : '当前宿主未完整启用独立聊天,请检查运行时配置。'}{state.view.mode === 'integration' && bot.sessionId && <button type="button" onClick={() => openSession?.(bot.sessionId)}>回到原生会话</button>}</div>}
|
|
119
|
+
<div ref={scroll} className="ic-transcript" role="log" aria-label={`${bot.name || bot.id} 的独立聊天记录`} aria-live="polite" onScroll={() => { const node = scroll.current; setFollowing(node.scrollHeight - node.scrollTop - node.clientHeight < 90) }}>
|
|
120
|
+
<div className="ic-message-column">{state.nextCursor && <button type="button" className="ic-load-older" disabled={state.loadingOlder} onClick={() => { olderPosition.current = scroll.current.scrollHeight; void controller.older() }}>{state.loadingOlder ? '正在读取…' : '加载较早消息'}</button>}
|
|
121
|
+
{state.items.length === 0 && <div className="ic-empty"><span className="ic-avatar ic-avatar-large"><UiIcon name="bot" size={33}/></span><strong>{bot.name || bot.id}</strong><p>{state.loading ? '正在读取聊天记录…' : '告诉我你想做什么,我们一起开始。'}</p></div>}
|
|
122
|
+
{state.items.map((message, index) => <React.Fragment key={message.messageId}>{index > 0 && message.contextId && message.contextId !== state.items[index - 1].contextId && contextBoundary()}<IndependentRichMessage message={message} botName={bot.name} calls={toolRelations.get(message.messageId)}/></React.Fragment>)}
|
|
123
|
+
{context?.sequence > 0 && !context.resetPending && state.items.at(-1)?.contextId !== context.contextId && contextBoundary()}
|
|
124
|
+
<NativeActivityCardView activity={nativeActivity.activity} controller={nativeActivity.controller} announce={false}/>
|
|
125
|
+
{progress?.text && <article className="ic-streaming" aria-label="正在生成的回复" aria-busy="true"><div className="ic-streaming-caption"><span className="ic-streaming-dot"/>正在生成 · 第 {progress.step} 步 · 尚未完成</div><div className="ic-streaming-text">{progress.text}</div>{progress.truncated && <small>预览已达到显示上限,完整回复会在完成后显示。</small>}</article>}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
{!following && <button type="button" className="ic-scroll-latest" onClick={() => { setFollowing(true); scroll.current?.scrollTo({ top: scroll.current.scrollHeight, behavior: 'smooth' }) }}>回到最新<UiIcon name="chevron" size={13}/></button>}
|
|
129
|
+
<div className="ic-composer-area">
|
|
130
|
+
{(state.error || state.storageError) && <div className="ic-alert" role="alert"><span>{state.storageError || state.error}</span><button type="button" onClick={() => controller.inspect()}>核对原请求</button><button type="button" onClick={() => controller.reloadMessages()}>刷新聊天记录</button></div>}
|
|
131
|
+
{showRequest && <div className={`ic-request${knownPending ? ' ic-request-working' : ''}`} role="status"><span>{requestText(pending ? { ...pending.status, operationKind: pending.operationKind } : request)}</span>{!knownPending && <small>请求编号:{pending?.requestKey ?? request.requestKey}</small>}
|
|
132
|
+
{pending?.status?.errorCode && <small>{pending.status.errorCode}</small>}
|
|
133
|
+
<div className="ic-request-actions">{pending && !knownPending && <button type="button" onClick={() => controller.inspect()}>核对原请求</button>}
|
|
134
|
+
{!contextOperation && pending?.status?.finished === true && pending.status.pending === false && <button type="button" onClick={() => { controller.restore(); input.current?.focus() }}>恢复文字到输入框</button>}
|
|
135
|
+
{!contextOperation && pending?.notFound && <button type="button" onClick={() => controller.releaseNotFound()}>宿主无此请求,恢复文字</button>}
|
|
136
|
+
{!contextOperation && pending?.status?.pending === true && !pending.status.cancelRequested && <button type="button" className="ic-stop" disabled={state.sending} onClick={() => { void controller.cancel() }}><UiIcon name="stop" size={13}/>请求停止</button>}
|
|
137
|
+
</div></div>}
|
|
138
|
+
<form className="ic-composer" onSubmit={event => { event.preventDefault(); submit() }}>
|
|
139
|
+
<label className="ic-sr-only" htmlFor={`independent-message-${bot.id}`}>发消息给 {bot.name || bot.id}</label>
|
|
140
|
+
<textarea ref={input} id={`independent-message-${bot.id}`} value={state.draft} placeholder={composingNext ? 'Bot 正在回复,可先写下一条…' : pending ? '原请求尚未核对,已暂停新的发送' : '发消息…'} rows={2} disabled={!state.view?.available || Boolean(readOnly)} onChange={event => controller.setDraft(event.target.value)} onCompositionStart={() => { composition.current = true }} onCompositionEnd={() => { composition.current = false }} onKeyDown={event => { if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && !composition.current) { event.preventDefault(); submit() } }}/>
|
|
141
|
+
<div className="ic-composer-footer"><span>文字消息 · Shift + Enter 换行</span><button type="submit" aria-label="发送消息" disabled={!canSend || !state.draft.trim()}><UiIcon name="arrow" size={18}/><span>发送</span></button></div>
|
|
142
|
+
</form>
|
|
143
|
+
<p className="ic-capability-note">此入口发送文字;附件、语音与记忆视图请打开原生会话。<button type="button" onClick={() => openPanel?.('bot-product-machines')}>电脑</button></p>
|
|
144
|
+
</div>
|
|
145
|
+
{settings && <BotSettingsDialog productApi={productApi} botId={bot.id} onClose={() => setSettings(false)} onOpenTools={typeof openPanel === 'function' ? async () => { requestWorkbenchView('tools'); await openPanel('model-router-galgame'); setSettings(false) } : undefined}/>}
|
|
146
|
+
</>
|
|
147
|
+
}
|
|
148
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { memo, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { MarkdownText, MarkdownDelegateProvider, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
4
|
+
import { copyRichText, INDEPENDENT_MARKDOWN_LABELS, richToolArguments } from './independent-chat-rich.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The public Host renderer owns parsing, GFM, math, safe URLs and highlighting.
|
|
8
|
+
* An empty nested delegate deliberately replaces enclosing file capabilities:
|
|
9
|
+
* an authored path never gains an opener or image resolver from another panel.
|
|
10
|
+
*/
|
|
11
|
+
export const IndependentRichMessage = memo(function IndependentRichMessage({ message, botName, calls = [] }) {
|
|
12
|
+
const text = typeof message.content === 'string' ? message.content : ''
|
|
13
|
+
const [feedback, setFeedback] = useState(null), [codeFeedback, setCodeFeedback] = useState(null)
|
|
14
|
+
const body = useRef(null), scope = useRef({ live: false, revision: 0, copying: false })
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const current = scope.current; current.live = true; current.revision++; current.copying = false
|
|
17
|
+
setFeedback(null); setCodeFeedback(null)
|
|
18
|
+
return () => { current.live = false; current.revision++; current.copying = false }
|
|
19
|
+
}, [message.messageId, text])
|
|
20
|
+
const copy = async (source, codeTarget = null) => {
|
|
21
|
+
const current = scope.current, revision = current.revision
|
|
22
|
+
if (!current.live || current.copying) return
|
|
23
|
+
current.copying = true
|
|
24
|
+
const focused = document.activeElement
|
|
25
|
+
const ok = await copyRichText(source, writeClipboard)
|
|
26
|
+
if (!current.live || current.revision !== revision) return
|
|
27
|
+
current.copying = false
|
|
28
|
+
const value = { ok, text: ok ? codeTarget ? '代码已复制' : '消息原文已复制' : '复制失败,请展开消息原文后手动复制。' }
|
|
29
|
+
if (codeTarget?.isConnected) setCodeFeedback({ ...value, target: codeTarget })
|
|
30
|
+
else setFeedback(value)
|
|
31
|
+
// The public helper's execCommand fallback temporarily focuses a textarea.
|
|
32
|
+
// Return to this message's own control rather than disturbing the composer.
|
|
33
|
+
if (focused?.isConnected && typeof focused.focus === 'function') focused.focus({ preventScroll: true })
|
|
34
|
+
}
|
|
35
|
+
const copyCode = event => {
|
|
36
|
+
if (!(event.target instanceof Element)) return
|
|
37
|
+
const button = event.target.closest('button'), block = button?.closest('.md-code-block')
|
|
38
|
+
if (!block || !body.current?.contains(block)) return
|
|
39
|
+
// These are public SDK 0.2.1-rc.4's observable code-card hooks. If a future
|
|
40
|
+
// Host changes them, do not guess from translated button text: the always
|
|
41
|
+
// present exact-message copy/raw controls still work independently.
|
|
42
|
+
const banner = button.closest('[data-code-block-banner]'), source = block.querySelector('[data-code-block-content] pre')
|
|
43
|
+
if (!banner || !source) return
|
|
44
|
+
event.preventDefault(); event.stopPropagation()
|
|
45
|
+
void copy(source.textContent ?? '', banner)
|
|
46
|
+
}
|
|
47
|
+
const roleLabel = message.role === 'user' ? '你' : message.role === 'assistant' ? botName || 'Bot' : message.role === 'tool' ? message.name || '工具结果' : '上下文记录'
|
|
48
|
+
return <article className={`ic-message ic-message-${message.role}`} data-message-id={message.messageId}>
|
|
49
|
+
<div className="ic-message-meta"><strong>{roleLabel}</strong>{message.at != null && <time>{new Date(message.at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}</time>}</div>
|
|
50
|
+
{message.role === 'tool' ? <details className="ic-rich-tool-result"><summary>工具结果{message.toolCallId ? ` · ${message.toolCallId}` : ' · 未记录调用编号'}</summary><pre>{text}</pre>{message.isError === true && <p className="ic-rich-tool-error">记录为错误结果</p>}</details>
|
|
51
|
+
: message.role === 'system' ? <details className="ic-rich-context"><summary>查看上下文记录</summary><pre>{text}</pre></details>
|
|
52
|
+
: text ? <div ref={body} className="ic-bubble ic-rich-body" onClickCapture={copyCode}>
|
|
53
|
+
<MarkdownDelegateProvider><MarkdownText text={text} labels={INDEPENDENT_MARKDOWN_LABELS} variant="body"/></MarkdownDelegateProvider>
|
|
54
|
+
{codeFeedback?.target.isConnected && createPortal(<span className="ic-rich-code-feedback" role="status" data-copy-ok={codeFeedback.ok}>{codeFeedback.text}</span>, codeFeedback.target)}
|
|
55
|
+
</div> : null}
|
|
56
|
+
{calls.length > 0 && <details className="ic-rich-tool-calls"><summary>工具调用 · {calls.length} 项</summary><p className="ic-rich-tool-note">这里显示已记录的调用与返回;结果记录不代表任务完成或已停止。</p>
|
|
57
|
+
{calls.map((call, index) => <section key={`${call.id ?? 'unrecorded'}:${index}`} data-tool-call-id={call.id}>
|
|
58
|
+
<h4>{call.name || '未记录名称'}</h4><dl><dt>调用编号</dt><dd>{call.id || '未记录'}</dd><dt>参数</dt><dd><pre>{richToolArguments(call)}</pre></dd><dt>返回记录</dt><dd>{call.results?.length ? call.results.map(result => <div key={result.messageId} data-tool-result-id={result.messageId}><small>{result.isError === true ? '记录为错误结果' : '已记录返回'} · {result.messageId}</small><pre>{result.content}</pre></div>) : <span>本页尚未记录对应返回;不能据此判断执行结果。</span>}</dd></dl>
|
|
59
|
+
</section>)}
|
|
60
|
+
</details>}
|
|
61
|
+
{message.modelSource && <small className="ic-message-source">{typeof message.modelSource === 'string' ? message.modelSource : `${message.modelSource.provider ?? ''}/${message.modelSource.model ?? ''}`}</small>}
|
|
62
|
+
<div className="ic-rich-message-actions"><button type="button" aria-label="复制消息原文" onClick={() => { void copy(text) }}>复制原文</button><details className="ic-rich-raw"><summary>消息原文</summary><textarea readOnly aria-label="消息原文(可选择复制)" value={text} spellCheck={false}/></details>{feedback && <span role="status" data-copy-ok={feedback.ok}>{feedback.text}</span>}</div>
|
|
63
|
+
</article>
|
|
64
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Match only the nearest preceding recorded call, including reused ids in later batches. */
|
|
2
|
+
export function independentToolRelations(messages = []) {
|
|
3
|
+
const owners = new Map(), result = new Map()
|
|
4
|
+
for (const message of messages) {
|
|
5
|
+
if (message.role === 'assistant') {
|
|
6
|
+
const calls = (Array.isArray(message.toolCalls) ? message.toolCalls : message.toolCall ? [message.toolCall] : []).map(call => ({ ...call, results: [] }))
|
|
7
|
+
result.set(message.messageId, calls)
|
|
8
|
+
for (const call of calls) if (typeof call.id === 'string' && call.id) owners.set(call.id, call)
|
|
9
|
+
}
|
|
10
|
+
const callId = message.toolCallId ?? message.toolCall?.id
|
|
11
|
+
if (message.role === 'tool' && typeof callId === 'string') owners.get(callId)?.results.push(message)
|
|
12
|
+
}
|
|
13
|
+
return result
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function richToolArguments(call) {
|
|
17
|
+
const value = call && Object.hasOwn(call, 'args') ? call.args : call?.arguments
|
|
18
|
+
if (value === undefined) return '未记录参数'
|
|
19
|
+
if (typeof value === 'string') return value
|
|
20
|
+
try { return JSON.stringify(value, null, 2) ?? '未记录参数' } catch { return '参数无法显示' }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Success is established only by the actual clipboard operation's return value. */
|
|
24
|
+
export async function copyRichText(text, writer) {
|
|
25
|
+
try { return await writer(text) === true } catch { return false }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const INDEPENDENT_MARKDOWN_LABELS = Object.freeze({
|
|
29
|
+
code: Object.freeze({ copyLabel: '复制代码', copiedLabel: '代码已复制' }), footnotes: '注释',
|
|
30
|
+
})
|