@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Identity and personality inside the Bot settings side window. The panel owns
|
|
2
|
+
no colour of its own: every surface, border, ink and focus ring is a product
|
|
3
|
+
token, so light, dark and the DS theme all follow the window around it. */
|
|
4
|
+
html body[data-bot-product-ui] .bp-profile { display: flex; flex-direction: column; gap: 12px; padding: 14px; border: 1px solid var(--bp-border); border-radius: 12px; background: var(--bp-surface); color: var(--bp-ink); font-family: var(--bp-font, system-ui); }
|
|
5
|
+
html body[data-bot-product-ui] .bp-profile-head { display: flex; align-items: center; gap: 11px; }
|
|
6
|
+
html body[data-bot-product-ui] .bp-profile-badge { display: grid; place-items: center; flex: none; width: 34px; height: 34px; border-radius: 10px; background: var(--bp-subtle); color: var(--bp-ink); }
|
|
7
|
+
html body[data-bot-product-ui] .bp-profile-title { min-width: 0; }
|
|
8
|
+
html body[data-bot-product-ui] .bp-profile-title h3 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
9
|
+
html body[data-bot-product-ui] .bp-profile-title p { margin: 3px 0 0; font-size: 12px; line-height: 1.5; color: var(--bp-muted); }
|
|
10
|
+
html body[data-bot-product-ui] .bp-profile-revision { flex: none; margin-left: auto; padding: 4px 9px; border: 1px solid var(--bp-border); border-radius: 999px; font-size: 11px; color: var(--bp-muted); white-space: nowrap; }
|
|
11
|
+
html body[data-bot-product-ui] .bp-profile-search { display: flex; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--bp-border); border-radius: 9px; background: var(--bp-bg); color: var(--bp-muted); }
|
|
12
|
+
html body[data-bot-product-ui] .bp-profile-search:focus-within { border-color: var(--bp-focus); }
|
|
13
|
+
html body[data-bot-product-ui] .bp-profile-search input { flex: 1; min-width: 0; height: 34px; border: 0; outline: none; background: transparent; color: var(--bp-ink); font: inherit; font-size: 13px; }
|
|
14
|
+
html body[data-bot-product-ui] .bp-profile-search input::placeholder { color: var(--bp-quiet); }
|
|
15
|
+
html body[data-bot-product-ui] .bp-profile-search button { display: grid; place-items: center; flex: none; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--bp-muted); cursor: pointer; }
|
|
16
|
+
html body[data-bot-product-ui] .bp-profile-search button:hover { background: var(--bp-hover); color: var(--bp-ink); }
|
|
17
|
+
html body[data-bot-product-ui] .bp-profile-status { margin: 0; font-size: 12px; }
|
|
18
|
+
html body[data-bot-product-ui] .bp-profile-error { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--bp-border); border-radius: 9px; background: var(--bp-danger-bg); color: var(--bp-danger); font-size: 12px; }
|
|
19
|
+
html body[data-bot-product-ui] .bp-profile-error p { margin: 0; flex: 1; min-width: 0; }
|
|
20
|
+
html body[data-bot-product-ui] .bp-profile-error button { flex: none; padding: 5px 10px; border: 1px solid currentcolor; border-radius: 7px; background: transparent; color: inherit; font: inherit; font-size: 12px; cursor: pointer; }
|
|
21
|
+
html body[data-bot-product-ui] .bp-profile-group { border: 1px solid var(--bp-border); border-radius: 10px; background: var(--bp-bg); overflow: hidden; }
|
|
22
|
+
html body[data-bot-product-ui] .bp-profile-group[data-open="false"] { background: var(--bp-surface); }
|
|
23
|
+
html body[data-bot-product-ui] .bp-profile-group-head { display: flex; align-items: center; gap: 8px; width: 100%; padding: 10px 12px; border: 0; background: transparent; color: var(--bp-ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
|
|
24
|
+
html body[data-bot-product-ui] .bp-profile-group-head:hover { background: var(--bp-hover); }
|
|
25
|
+
html body[data-bot-product-ui] .bp-profile-group-head svg:first-child { flex: none; color: var(--bp-muted); }
|
|
26
|
+
html body[data-bot-product-ui] .bp-profile-group-label { font-weight: 600; }
|
|
27
|
+
html body[data-bot-product-ui] .bp-profile-group-hint { margin-left: auto; font-size: 11px; color: var(--bp-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
28
|
+
html body[data-bot-product-ui] .bp-profile-group-head svg:last-child { flex: none; color: var(--bp-muted); transition: rotate .15s ease; }
|
|
29
|
+
html body[data-bot-product-ui] .bp-profile-group[data-open="true"] .bp-profile-group-head svg:last-child { rotate: 180deg; }
|
|
30
|
+
html body[data-bot-product-ui] .bp-profile-group-body { display: flex; flex-direction: column; gap: 12px; padding: 2px 12px 12px; }
|
|
31
|
+
html body[data-bot-product-ui] .bp-profile-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
|
32
|
+
html body[data-bot-product-ui] .bp-profile-field-head { display: flex; align-items: baseline; gap: 8px; }
|
|
33
|
+
html body[data-bot-product-ui] .bp-profile-field-label { font-size: 12px; font-weight: 600; color: var(--bp-ink); }
|
|
34
|
+
html body[data-bot-product-ui] .bp-profile-field-count { margin-left: auto; font-size: 11px; color: var(--bp-quiet); font-variant-numeric: tabular-nums; }
|
|
35
|
+
html body[data-bot-product-ui] .bp-profile-field input, html body[data-bot-product-ui] .bp-profile-field textarea { width: 100%; min-width: 0; padding: 8px 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-surface); color: var(--bp-ink); font: inherit; font-size: 13px; line-height: 1.55; resize: vertical; }
|
|
36
|
+
html body[data-bot-product-ui] .bp-profile-field input::placeholder, html body[data-bot-product-ui] .bp-profile-field textarea::placeholder { color: var(--bp-quiet); }
|
|
37
|
+
html body[data-bot-product-ui] .bp-profile-field input:focus-visible, html body[data-bot-product-ui] .bp-profile-field textarea:focus-visible { outline: 2px solid var(--bp-focus); outline-offset: -1px; border-color: var(--bp-focus); }
|
|
38
|
+
html body[data-bot-product-ui] .bp-profile-field textarea { min-height: 92px; }
|
|
39
|
+
html body[data-bot-product-ui] .bp-profile-field-help { font-size: 11px; line-height: 1.55; color: var(--bp-muted); }
|
|
40
|
+
html body[data-bot-product-ui] .bp-profile-field-problem { font-size: 11px; line-height: 1.5; color: var(--bp-danger); }
|
|
41
|
+
html body[data-bot-product-ui] .bp-profile-save { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding-top: 2px; }
|
|
42
|
+
html body[data-bot-product-ui] .bp-profile-pill { flex: none; padding: 4px 9px; border: 1px solid var(--bp-border); border-radius: 999px; background: var(--bp-subtle); font-size: 11px; color: var(--bp-muted); }
|
|
43
|
+
html body[data-bot-product-ui] .bp-profile-pill[data-tone="warn"] { border-color: var(--bp-warning); background: var(--bp-warning-bg); color: var(--bp-warning); }
|
|
44
|
+
html body[data-bot-product-ui] .bp-profile-save button { flex: none; margin-left: auto; }
|
|
45
|
+
html body[data-bot-product-ui] .bp-profile-feedback { margin: 0; font-size: 12px; line-height: 1.55; color: var(--bp-muted); }
|
|
46
|
+
html body[data-bot-product-ui] .bp-profile-feedback[data-kind="saved"] { color: var(--bp-solid); }
|
|
47
|
+
html body[data-bot-product-ui] .bp-profile-feedback[data-kind="error"] { color: var(--bp-danger); }
|
|
48
|
+
html body[data-bot-product-ui] .bp-profile-feedback[data-kind="notice"] { color: var(--bp-warning); }
|
|
49
|
+
html body[data-bot-product-ui] .bp-profile button:focus-visible, html body[data-bot-product-ui] .bp-profile input:focus-visible, html body[data-bot-product-ui] .bp-profile textarea:focus-visible { outline: 2px solid var(--bp-focus); outline-offset: 1px; }
|
|
50
|
+
/* A phone-width settings window keeps every control reachable without sideways scroll. */
|
|
51
|
+
@media (max-width: 520px) {
|
|
52
|
+
html body[data-bot-product-ui] .bp-profile { padding: 12px; gap: 10px; }
|
|
53
|
+
html body[data-bot-product-ui] .bp-profile-revision { width: 100%; margin-left: 0; align-self: flex-start; text-align: left; }
|
|
54
|
+
html body[data-bot-product-ui] .bp-profile-head { flex-wrap: wrap; }
|
|
55
|
+
html body[data-bot-product-ui] .bp-profile-group-hint { display: none; }
|
|
56
|
+
html body[data-bot-product-ui] .bp-profile-save button { margin-left: 0; width: 100%; }
|
|
57
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { BOT_PROFILE_LIMITS, botProfileNameProblem } from '../../packages/orchestration/src/bot-profile.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The identity slice's own model: which fields exist, what the Host persists,
|
|
5
|
+
* and the words the panel uses. The limits and the name rule come from the
|
|
6
|
+
* wiring service so the panel can never offer a value the Host would refuse;
|
|
7
|
+
* the Host still decides, this is only the pre-check that keeps a save from
|
|
8
|
+
* being sent to fail.
|
|
9
|
+
*
|
|
10
|
+
* What is deliberately absent: no soul column, no reply-style enum, no
|
|
11
|
+
* personality switch. The frozen roster persists name, role and instructions
|
|
12
|
+
* and nothing else about who a Bot is, so a control with no field behind it
|
|
13
|
+
* would be a lie the owner only discovers later.
|
|
14
|
+
*/
|
|
15
|
+
export const BOT_PROFILE_FIELDS = Object.freeze(['name', 'role', 'instructions'])
|
|
16
|
+
|
|
17
|
+
export const BOT_PROFILE_GROUPS = Object.freeze([
|
|
18
|
+
{ id: 'identity', label: '身份', icon: 'bot', hint: '名称与职责', fields: Object.freeze(['name', 'role']) },
|
|
19
|
+
{ id: 'voice', label: '对话指令与性格', icon: 'spark', hint: '每次对话都会读', fields: Object.freeze(['instructions']) },
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
export const BOT_PROFILE_FIELD_META = Object.freeze({
|
|
23
|
+
name: { label: '名称', limit: BOT_PROFILE_LIMITS.name, control: 'input', placeholder: '例如:研究伙伴',
|
|
24
|
+
help: '群聊与名单里显示的名字,不能与其他 Bot 同名。',
|
|
25
|
+
terms: Object.freeze(['名称', '名字', '昵称', 'name']) },
|
|
26
|
+
role: { label: '职责 / 描述', limit: BOT_PROFILE_LIMITS.role, control: 'input', placeholder: '例如:整理可借鉴的设计资料',
|
|
27
|
+
help: '一句话说明这个 Bot 负责什么;名单与群聊成员里显示这一句。',
|
|
28
|
+
terms: Object.freeze(['职责', '描述', '一句话', '定位', 'role', 'title']) },
|
|
29
|
+
instructions: { label: '对话指令 / 性格', limit: BOT_PROFILE_LIMITS.instructions, control: 'textarea', placeholder: '例如:先给结论,再给依据;不确定时说明还缺什么。',
|
|
30
|
+
help: '这些指令每次对话都会进入上下文,决定这个 Bot 怎么说话、怎么做事。它不授予任何工具权限:模型、工具与执行权限请在“模型与官方工具”里设置。',
|
|
31
|
+
terms: Object.freeze(['指令', '性格', '对话', '人设', 'instructions', 'personality', 'persona']) },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/** The editable draft: exactly the three fields the Host persists, nothing more. */
|
|
35
|
+
export function botProfileDraft(view) {
|
|
36
|
+
return { name: view?.bot?.name ?? '', role: view?.bot?.role ?? '', instructions: view?.bot?.instructions ?? '' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Only changed fields travel, so an untouched field is never re-sent to the Host. */
|
|
40
|
+
export function botProfilePatch(draft, view) {
|
|
41
|
+
const patch = {}
|
|
42
|
+
for (const field of BOT_PROFILE_FIELDS) if (draft[field] !== (view?.bot?.[field] ?? '')) patch[field] = draft[field]
|
|
43
|
+
return patch
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function botProfileDirty(draft, view) {
|
|
47
|
+
return Object.keys(botProfilePatch(draft, view)).length > 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Live problems that make a save pointless before it reaches the Host. */
|
|
51
|
+
export function botProfileProblems(draft) {
|
|
52
|
+
const problems = []
|
|
53
|
+
for (const field of BOT_PROFILE_FIELDS) {
|
|
54
|
+
const value = draft[field]
|
|
55
|
+
if (value.length > BOT_PROFILE_FIELD_META[field].limit) problems.push({ field, text: `${BOT_PROFILE_FIELD_META[field].label}不能超过 ${BOT_PROFILE_FIELD_META[field].limit} 个字符。` })
|
|
56
|
+
}
|
|
57
|
+
const name = botProfileNameProblem(draft.name.trim())
|
|
58
|
+
if (name !== undefined) problems.push({ field: 'name', text: name })
|
|
59
|
+
return problems
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Group visibility for a search: a group matches its own words or any field's. */
|
|
63
|
+
export function botProfileSearch(query) {
|
|
64
|
+
const needle = String(query ?? '').trim().toLowerCase()
|
|
65
|
+
if (needle === '') return null
|
|
66
|
+
const hit = text => String(text).toLowerCase().includes(needle)
|
|
67
|
+
const groups = {}
|
|
68
|
+
for (const group of BOT_PROFILE_GROUPS) {
|
|
69
|
+
groups[group.id] = hit(group.label) || hit(group.hint) || group.fields.some(field => {
|
|
70
|
+
const meta = BOT_PROFILE_FIELD_META[field]
|
|
71
|
+
return hit(meta.label) || hit(meta.help) || hit(meta.placeholder) || meta.terms.some(hit)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
return { needle, groups, any: Object.values(groups).some(Boolean) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function botProfileSavedText(result) {
|
|
78
|
+
const fields = Object.freeze({ name: '名称', role: '职责', instructions: '对话指令' })
|
|
79
|
+
const saved = (result?.saved ?? []).map(field => fields[field] ?? field)
|
|
80
|
+
if (saved.length === 0) return '与宿主一致,没有需要保存的修改。'
|
|
81
|
+
return `已保存:${saved.join('、')}。`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One place that turns a Host refusal into words the owner can act on. */
|
|
85
|
+
export function botProfileFailure(error) {
|
|
86
|
+
switch (error?.code) {
|
|
87
|
+
case 'BOT_PROFILE_CONFLICT': return '这个 Bot 的身份在别处已更新。请重新读取后再保存,你未保存的修改会保留。'
|
|
88
|
+
case 'BOT_PROFILE_UNKNOWN_BOT': return '这个 Bot 不在已提交的宿主目录中,无法修改它的身份。'
|
|
89
|
+
case 'BOT_PROFILE_UNCONFIRMED': return '宿主尚未确认这次保存,请重新读取后核对。'
|
|
90
|
+
case 'BOT_PROFILE_SEAM_UNAVAILABLE': return '当前宿主还不支持保存 Bot 身份,请更新完整插件。'
|
|
91
|
+
case 'BOT_PROFILE_NAME_INVALID': case 'BOT_PROFILE_NAME_TAKEN': case 'BOT_PROFILE_LIMIT': return error.message
|
|
92
|
+
case 'REMOTE_OWNER_REQUIRED': return '只有已认证的宿主拥有者可以修改 Bot 身份。'
|
|
93
|
+
case 'PRODUCT_ACTION_UNAVAILABLE': case 'INVALID_PRODUCT_ACTION': return '当前连接尚不支持身份与个性设置,请更新完整插件。'
|
|
94
|
+
case 'REQUEST_ABORTED': return '保存结果尚未确认,请重新读取后核对。'
|
|
95
|
+
default: return '暂时无法保存,请重新读取后核对。'
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
html body[data-bot-product-ui] .bp-bot-settings-backdrop { position: fixed; inset: 0; z-index: 9900; display: flex; justify-content: flex-end; background: #0005; color: var(--bp-ink); font-family: var(--bp-font, system-ui); }
|
|
2
|
+
html body[data-bot-product-ui] .bp-bot-settings-dialog { display: flex; flex-direction: column; width: min(620px, 100vw); min-width: 0; height: 100%; background: var(--bp-bg); box-shadow: -12px 0 45px #0002; border-left: 1px solid var(--bp-border); }
|
|
3
|
+
html body[data-bot-product-ui] .bp-bot-settings-header { display: flex; align-items: center; flex: none; gap: 12px; padding: 18px 22px; border-bottom: 1px solid var(--bp-border); background: var(--bp-surface); }
|
|
4
|
+
html body[data-bot-product-ui] .bp-bot-settings-header h2 { margin: 0; font-size: 17px; font-weight: 600; }
|
|
5
|
+
html body[data-bot-product-ui] .bp-bot-settings-header p { margin: 4px 0 0; font-size: 12px; color: var(--bp-muted); }
|
|
6
|
+
html body[data-bot-product-ui] .bp-bot-settings-header button { display: grid; place-items: center; flex: none; margin-left: auto; width: 36px; height: 36px; padding: 0; border: 0; border-radius: 9px; background: transparent; color: var(--bp-ink); cursor: pointer; }
|
|
7
|
+
html body[data-bot-product-ui] .bp-bot-settings-header button:hover { background: var(--bp-hover); }
|
|
8
|
+
html body[data-bot-product-ui] .bp-bot-settings-avatar { display: grid; place-items: center; flex: none; width: 42px; height: 42px; border-radius: 12px; background: var(--bp-subtle); }
|
|
9
|
+
html body[data-bot-product-ui] .bp-bot-settings-body { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 22px; }
|
|
10
|
+
html body[data-bot-product-ui] .bp-bot-settings-tabs { display: flex; flex: none; gap: 4px; padding: 10px 18px 0; border-bottom: 1px solid var(--bp-border); background: var(--bp-surface); }
|
|
11
|
+
html body[data-bot-product-ui] .bp-bot-settings-tabs button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 11px 12px; margin-bottom: -1px; color: var(--bp-muted); background: transparent; border: 0; border-bottom: 2px solid transparent; font: inherit; font-size: 13px; cursor: pointer; }
|
|
12
|
+
html body[data-bot-product-ui] .bp-bot-settings-tabs button[aria-selected=true] { color: var(--bp-ink); border-bottom-color: var(--bp-ink); font-weight: 600; }
|
|
13
|
+
html body[data-bot-product-ui] .bp-bot-settings-body > [hidden] { display: none; }
|
|
14
|
+
html body[data-bot-product-ui] .bp-bot-settings-error { padding: 12px; margin-bottom: 14px; border: 1px solid var(--bp-border); border-radius: 10px; color: var(--bp-ink); background: var(--bp-surface); font-size: 13px; }
|
|
15
|
+
html body[data-bot-product-ui] .bp-bot-settings-error button { padding: 6px 12px; color: inherit; background: var(--bp-subtle); border: 1px solid var(--bp-border); border-radius: 7px; cursor: pointer; }
|
|
16
|
+
html body[data-bot-product-ui] .bp-bot-settings-dialog button:focus-visible { outline: 2px solid var(--bp-focus); outline-offset: -2px; }
|
|
17
|
+
@media (max-width: 520px) { html body[data-bot-product-ui] .bp-bot-settings-body { padding: 16px; } html body[data-bot-product-ui] .bp-bot-settings-header { padding: 14px 16px; } }
|
|
18
|
+
html body[data-bot-product-ui]:has(.bp-bot-settings-backdrop) { overflow: hidden; }
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React, { useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { BotBindingEditor } from './binding-editor.jsx'
|
|
4
|
+
import { BotProfileIdentity } from './bot-profile-identity.jsx'
|
|
5
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
6
|
+
import css from './bot-settings-dialog.css'
|
|
7
|
+
|
|
8
|
+
let nextScope = 0
|
|
9
|
+
export function BotSettingsDialog(props) {
|
|
10
|
+
const key = useMemo(() => ++nextScope, [props.productApi, props.botId])
|
|
11
|
+
return <SettingsDialog key={key} {...props}/>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function SettingsDialog({ productApi, botId, onClose, onOpenTools }) {
|
|
15
|
+
const [view, setView] = useState(null), [error, setError] = useState(''), [loading, setLoading] = useState(true), [busy, setBusy] = useState(false)
|
|
16
|
+
const [tab, setTab] = useState('binding'), [identityVisited, setIdentityVisited] = useState(false)
|
|
17
|
+
const tabId = useId()
|
|
18
|
+
const surface = useRef(null), closeButton = useRef(null)
|
|
19
|
+
const session = useRef({ live: false, epoch: 0, busy: false, requests: new Set() })
|
|
20
|
+
const call = async (action, payload) => {
|
|
21
|
+
const current = session.current, controller = new AbortController()
|
|
22
|
+
current.requests.add(controller)
|
|
23
|
+
let cancel
|
|
24
|
+
const deadline = new Promise((_resolve, reject) => { cancel = () => reject(new Error('REQUEST_ABORTED')); controller.signal.addEventListener('abort', cancel, { once: true }) })
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), 10000)
|
|
26
|
+
try { return await Promise.race([Promise.resolve().then(() => productApi.dispatch(action, payload, controller.signal)), deadline]) }
|
|
27
|
+
finally { clearTimeout(timer); current.requests.delete(controller); controller.signal.removeEventListener('abort', cancel) }
|
|
28
|
+
}
|
|
29
|
+
const read = async () => {
|
|
30
|
+
const current = session.current, epoch = ++current.epoch
|
|
31
|
+
setLoading(true)
|
|
32
|
+
try {
|
|
33
|
+
const value = await call('binding.inspect', { botId })
|
|
34
|
+
if (!current.live || epoch !== current.epoch) return
|
|
35
|
+
if (value?.schemaVersion !== 1 || value.bot?.id !== botId || value.record?.botId !== botId) throw new Error('WRONG_BINDING_OWNER')
|
|
36
|
+
setView(value); setError('')
|
|
37
|
+
} catch (cause) {
|
|
38
|
+
if (current.live && epoch === current.epoch) setError(['PRODUCT_ACTION_UNAVAILABLE', 'INVALID_PRODUCT_ACTION'].includes(cause?.code) ? '当前连接尚不支持模型与工具设置,请更新完整插件。' : '暂时无法读取这个 Bot 的设置,请重试。')
|
|
39
|
+
} finally { if (current.live && epoch === current.epoch) setLoading(false) }
|
|
40
|
+
}
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const current = session.current; current.live = true
|
|
43
|
+
const restore = document.activeElement
|
|
44
|
+
const style = document.createElement('style'); style.dataset.botSettingsDialog = '1'; style.textContent = css; document.head.append(style)
|
|
45
|
+
closeButton.current?.focus()
|
|
46
|
+
void read()
|
|
47
|
+
return () => {
|
|
48
|
+
current.live = false; current.epoch++
|
|
49
|
+
for (const controller of current.requests) controller.abort()
|
|
50
|
+
style.remove()
|
|
51
|
+
if (restore?.isConnected && typeof restore.focus === 'function') restore.focus()
|
|
52
|
+
}
|
|
53
|
+
}, [productApi, botId])
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const keyboard = event => {
|
|
56
|
+
if (event.defaultPrevented || event.isComposing || event.keyCode === 229 || document.querySelector('.bp-model-popover')) return
|
|
57
|
+
const active = document.activeElement
|
|
58
|
+
if (!surface.current?.contains(active)) return
|
|
59
|
+
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); onClose?.() }
|
|
60
|
+
if (event.key === 'Tab') {
|
|
61
|
+
const controls = [...surface.current.querySelectorAll('*')].filter(node => ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY'].includes(node.tagName) && !node.disabled && node.getClientRects().length)
|
|
62
|
+
if (event.shiftKey && active === controls[0]) { event.preventDefault(); controls.at(-1)?.focus() }
|
|
63
|
+
else if (!event.shiftKey && active === controls.at(-1)) { event.preventDefault(); controls[0]?.focus() }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
document.addEventListener('keydown', keyboard)
|
|
67
|
+
return () => document.removeEventListener('keydown', keyboard)
|
|
68
|
+
}, [onClose])
|
|
69
|
+
const dispatch = async (action, payload) => {
|
|
70
|
+
const current = session.current
|
|
71
|
+
if (!current.live || current.busy || !['binding.save', 'funding.account.save'].includes(action) || (action === 'binding.save' && payload?.botId !== botId)) throw new Error('设置暂时无法保存,请重新读取。')
|
|
72
|
+
current.busy = true; setBusy(true)
|
|
73
|
+
try {
|
|
74
|
+
const result = await call(action, payload)
|
|
75
|
+
if (current.live) await read()
|
|
76
|
+
return result
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
if (cause?.message === 'REQUEST_ABORTED') {
|
|
79
|
+
if (current.live) setError('保存结果尚未确认,请重新读取后核对。')
|
|
80
|
+
throw new Error('保存结果尚未确认,请重新读取后核对。')
|
|
81
|
+
}
|
|
82
|
+
throw cause
|
|
83
|
+
} finally { current.busy = false; if (current.live) setBusy(false) }
|
|
84
|
+
}
|
|
85
|
+
const chooseTab = next => { setTab(next); if (next === 'identity') setIdentityVisited(true) }
|
|
86
|
+
const tabKeys = event => {
|
|
87
|
+
if (event.isComposing || event.keyCode === 229 || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
const next = event.key === 'Home' ? 'binding' : event.key === 'End' ? 'identity' : tab === 'binding' ? 'identity' : 'binding'
|
|
90
|
+
chooseTab(next)
|
|
91
|
+
event.currentTarget.querySelector(`[data-settings-tab="${next}"]`)?.focus()
|
|
92
|
+
}
|
|
93
|
+
return createPortal(<div className="bp-bot-settings-backdrop" onMouseDown={event => { if (event.target === event.currentTarget) onClose?.() }}>
|
|
94
|
+
<section ref={surface} className="bp-bot-settings-dialog" role="dialog" aria-modal="true" aria-label={`${view?.bot?.name ?? 'Bot'} 的设置`}>
|
|
95
|
+
<header className="bp-bot-settings-header"><span className="bp-bot-settings-avatar"><UiIcon name="bot" size={23}/></span><div><h2>{view?.bot?.name ?? 'Bot 设置'}</h2><p>模型、工具与 Bot 个性</p></div><button ref={closeButton} type="button" aria-label="关闭 Bot 设置" onClick={onClose}><UiIcon name="close" size={19}/></button></header>
|
|
96
|
+
<div className="bp-bot-settings-tabs" role="tablist" aria-label="Bot 设置分类" onKeyDown={tabKeys}>
|
|
97
|
+
{[['binding', '模型与工具', 'spark'], ['identity', '身份与个性', 'bot']].map(([id, label, icon]) => <button key={id} type="button" role="tab" data-settings-tab={id} id={`${tabId}-${id}`} aria-controls={`${tabId}-${id}-panel`} aria-selected={tab === id} tabIndex={tab === id ? 0 : -1} onClick={() => chooseTab(id)}><UiIcon name={icon} size={15}/>{label}</button>)}
|
|
98
|
+
</div>
|
|
99
|
+
<div className="bp-page bp-bot-settings-body">
|
|
100
|
+
<div role="tabpanel" id={`${tabId}-binding-panel`} aria-labelledby={`${tabId}-binding`} hidden={tab !== 'binding'}>
|
|
101
|
+
{loading && !view && <p className="bp-muted" role="status">正在读取 Bot 设置…</p>}
|
|
102
|
+
{error && <div className="bp-bot-settings-error" role="alert"><p>{error}</p><button type="button" disabled={busy || loading} onClick={() => void read()}>重新读取</button></div>}
|
|
103
|
+
{view && <BotBindingEditor bot={view.bot} record={view.record} accounts={view.accounts} dispatch={dispatch} hostScope={productApi} busy={busy || loading || Boolean(error)} onOpenTools={onOpenTools}/>}
|
|
104
|
+
</div>
|
|
105
|
+
<div role="tabpanel" id={`${tabId}-identity-panel`} aria-labelledby={`${tabId}-identity`} hidden={tab !== 'identity'}>
|
|
106
|
+
{identityVisited && <BotProfileIdentity productApi={productApi} botId={botId} busy={busy} onSaved={bot => setView(current => current && bot.id === botId ? { ...current, bot: { ...current.bot, name: bot.name, role: bot.role } } : current)}/>}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</section>
|
|
110
|
+
</div>, document.body)
|
|
111
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* The directory palette is scoped to .bp-threads. The header lives in the
|
|
2
|
+
existing chat toolbar and inherits that conversation's appearance instead. */
|
|
3
|
+
html body[data-bot-product-ui] .bp-threads-head-wrap {
|
|
4
|
+
--bp-thread-surface: var(--bp-chat-surface);
|
|
5
|
+
--bp-thread-inset: var(--bp-chat-inset);
|
|
6
|
+
--bp-thread-hover: var(--bp-chat-hover);
|
|
7
|
+
--bp-thread-line: var(--bp-chat-line);
|
|
8
|
+
--bp-thread-ink: var(--bp-chat-ink);
|
|
9
|
+
--bp-thread-muted: var(--bp-chat-muted);
|
|
10
|
+
--bp-thread-accent: var(--bp-chat-accent);
|
|
11
|
+
--bp-thread-accent-ink: var(--bp-chat-accent-ink);
|
|
12
|
+
--bp-thread-focus: var(--bp-chat-focus);
|
|
13
|
+
--bp-thread-wait: var(--bp-chat-warning);
|
|
14
|
+
}
|
|
15
|
+
html body[data-bot-product-ui] .bp-threads-head-wrap :is(button, input):focus-visible { outline: 2px solid var(--bp-chat-focus); outline-offset: 2px; }
|
|
16
|
+
@media (max-width: 640px) {
|
|
17
|
+
html body[data-bot-product-ui] .bp-threads-head-wrap { position: static; }
|
|
18
|
+
html body[data-bot-product-ui] .bp-threads-head-wrap .bp-threads-head { max-width: 76px; padding: 0 6px; }
|
|
19
|
+
html body[data-bot-product-ui] .bp-threads-head-wrap .bp-threads-head-name { max-width: 38px; }
|
|
20
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
3
|
+
import { THREAD_ACTIONS, formatThreadAt, isArchivedThread, sortedThreads, threadLabel } from './bot-threads-panel.jsx'
|
|
4
|
+
|
|
5
|
+
// The chat-header control for a Bot's threads.
|
|
6
|
+
//
|
|
7
|
+
// It answers two questions the native header cannot: which thread of this Bot
|
|
8
|
+
// is open, and what the Bot's other threads are. Everything else about the
|
|
9
|
+
// conversation — the Bot's model, its computer, its memory, its tasks — stays
|
|
10
|
+
// where the product already puts it, so this is one control, not a second
|
|
11
|
+
// toolbar.
|
|
12
|
+
//
|
|
13
|
+
// It owns no authority. Opening a thread asks the Host for that thread's real
|
|
14
|
+
// Session and only then hands the id to `onOpenSession` (the native
|
|
15
|
+
// `actions.openSession` by default); creating one is the same, after an
|
|
16
|
+
// explicit create. Nothing here sends a greeting, a message, a model request
|
|
17
|
+
// or a Task, and no thread parameter ever carries a model, a route, a folder
|
|
18
|
+
// or a credential — the Bot's committed grant decides what a turn may do.
|
|
19
|
+
//
|
|
20
|
+
// The Host is read through the product API with an abort, a deadline and an
|
|
21
|
+
// epoch per read: a late or withdrawn answer never rewrites the list, and the
|
|
22
|
+
// list is read for one Bot on a change or while the menu is open, never as a
|
|
23
|
+
// repeated full snapshot.
|
|
24
|
+
|
|
25
|
+
const REQUEST_MS = 8000
|
|
26
|
+
const MENU_MS = 2500
|
|
27
|
+
const MAX_NAME = 80
|
|
28
|
+
|
|
29
|
+
let headSeq = 0
|
|
30
|
+
|
|
31
|
+
// An IME composition is not a submission: Enter that confirms a candidate must
|
|
32
|
+
// never create a thread.
|
|
33
|
+
const submits = event => event.key === 'Enter' && event.nativeEvent?.isComposing !== true && event.keyCode !== 229
|
|
34
|
+
|
|
35
|
+
function abortable(work, signal) {
|
|
36
|
+
let cancel
|
|
37
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
38
|
+
cancel = () => reject(new Error('THREAD_REQUEST_ABORTED'))
|
|
39
|
+
signal.addEventListener('abort', cancel, { once: true })
|
|
40
|
+
if (signal.aborted) cancel()
|
|
41
|
+
})
|
|
42
|
+
return Promise.race([Promise.resolve().then(work), interrupted]).finally(() => signal.removeEventListener('abort', cancel))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The same Bot-scoped refusals the directory panel reports, in the owner's words. */
|
|
46
|
+
export function headerThreadNote(error) {
|
|
47
|
+
switch (error?.code) {
|
|
48
|
+
case 'BOT_THREADS_STORAGE_FAILED': return '线程目录无法读取,已停止写入与发送。请修复存储后重启宿主。'
|
|
49
|
+
case 'THREAD_ARCHIVED': return '这个线程已归档,只能只读打开。'
|
|
50
|
+
case 'SESSION_UNKNOWN': return '这个线程的会话建立结果未知,请在线程目录中核对后恢复。'
|
|
51
|
+
case 'SESSION_NOT_OWNED': return '宿主没有确认这个线程会话的归属。'
|
|
52
|
+
case 'BOT_GRANT_EMPTY': return '这个 Bot 的授权已被撤销或停用。'
|
|
53
|
+
case 'BOT_NOT_COMMITTED': return '这个 Bot 已不在已提交状态。'
|
|
54
|
+
case 'RUNTIME_MODE_MISMATCH': return '这个 Bot 已由另一个运行时提供服务。'
|
|
55
|
+
case 'THREAD_REQUEST_ABORTED': return '宿主没有在约定时间内回答。'
|
|
56
|
+
default: return '操作未得到宿主确认。'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function BotThreadHeader({ sessionId, roster, productApi, actions, onOpenSession, onOpenPanel, className }) {
|
|
61
|
+
// The thread that is open, and the Bot it belongs to. The roster indexes a
|
|
62
|
+
// thread Session under its Bot, so both identities are known here.
|
|
63
|
+
const thread = roster?.threadsById?.[sessionId] ?? null
|
|
64
|
+
const bot = roster?.byId?.[sessionId] ?? null
|
|
65
|
+
const botId = thread?.botId ?? bot?.id ?? null
|
|
66
|
+
const [open, setOpen] = useState(false)
|
|
67
|
+
const [rows, setRows] = useState(null)
|
|
68
|
+
const [pending, setPending] = useState(false)
|
|
69
|
+
const [note, setNote] = useState('')
|
|
70
|
+
const [draft, setDraft] = useState('')
|
|
71
|
+
const [place, setPlace] = useState(null)
|
|
72
|
+
const live = useRef({ mounted: false, requests: new Set(), queue: Promise.resolve(), loaded: false, inflight: null })
|
|
73
|
+
const root = useRef(null)
|
|
74
|
+
const chip = useRef(null)
|
|
75
|
+
// Two epochs, one per kind of read: only the newest open may navigate, and
|
|
76
|
+
// only the newest list may rewrite the rows.
|
|
77
|
+
const opening = useRef(0)
|
|
78
|
+
const reading = useRef(0)
|
|
79
|
+
// One create intent, one key, held until the Host answers (see createThread).
|
|
80
|
+
const createKey = useRef(null)
|
|
81
|
+
const navigate = id => (typeof onOpenSession === 'function' ? onOpenSession(id) : actions?.openSession?.(id))
|
|
82
|
+
// Closing gives the caret back to the chip that opened the menu, so a keyboard
|
|
83
|
+
// owner is never dropped on the page when the input disappears.
|
|
84
|
+
const closeMenu = () => {
|
|
85
|
+
setOpen(false)
|
|
86
|
+
if (root.current?.contains(document.activeElement)) chip.current?.focus()
|
|
87
|
+
}
|
|
88
|
+
// One guarded call at a time, in the order it was made; a reply counts only
|
|
89
|
+
// while this control is mounted and no newer call of the same kind began.
|
|
90
|
+
const call = (action, payload, epoch) => {
|
|
91
|
+
const mine = epoch === undefined ? 0 : epoch.current + 1
|
|
92
|
+
if (epoch !== undefined) epoch.current = mine
|
|
93
|
+
// The newest call retires the one before it, so a Host that never answers
|
|
94
|
+
// cannot hold the queue.
|
|
95
|
+
const idle = live.current.inflight === null
|
|
96
|
+
live.current.inflight?.abort()
|
|
97
|
+
const run = idle ? step(action, payload, epoch, mine) : live.current.queue.then(() => step(action, payload, epoch, mine))
|
|
98
|
+
live.current.queue = run.then(() => undefined, () => undefined)
|
|
99
|
+
return run
|
|
100
|
+
}
|
|
101
|
+
const step = async (action, payload, epoch, mine) => {
|
|
102
|
+
const current = live.current
|
|
103
|
+
if (!current.mounted || typeof productApi?.dispatch !== 'function') return { abandoned: true }
|
|
104
|
+
const controller = new AbortController()
|
|
105
|
+
current.inflight = controller
|
|
106
|
+
current.requests.add(controller)
|
|
107
|
+
let expired = false
|
|
108
|
+
const timeout = setTimeout(() => { expired = true; controller.abort() }, REQUEST_MS)
|
|
109
|
+
try {
|
|
110
|
+
const result = await abortable(() => productApi.dispatch(action, payload, controller.signal), controller.signal)
|
|
111
|
+
if (!current.mounted || controller.signal.aborted || (epoch !== undefined && epoch.current !== mine)) return { abandoned: true }
|
|
112
|
+
return { ok: true, result }
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (!current.mounted || (epoch !== undefined && epoch.current !== mine)) return { abandoned: true }
|
|
115
|
+
// A deadline the Host missed is reported; a call a newer one retired is not.
|
|
116
|
+
if (expired) return { ok: false, error: Object.assign(new Error('宿主没有在约定时间内回答。'), { code: 'THREAD_REQUEST_ABORTED' }) }
|
|
117
|
+
return controller.signal.aborted ? { abandoned: true } : { ok: false, error }
|
|
118
|
+
} finally { clearTimeout(timeout); current.requests.delete(controller); if (current.inflight === controller) current.inflight = null }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const read = useCallback(async () => {
|
|
122
|
+
const current = live.current
|
|
123
|
+
if (!current.mounted || !botId) return
|
|
124
|
+
const outcome = await call(THREAD_ACTIONS.list, { botId }, reading)
|
|
125
|
+
if (outcome.ok) { current.loaded = true; setRows(sortedThreads(outcome.result?.threads)) }
|
|
126
|
+
else if (outcome.error !== undefined && (!current.loaded || outcome.error.code === 'BOT_THREADS_STORAGE_FAILED')) setNote(headerThreadNote(outcome.error))
|
|
127
|
+
}, [botId, productApi])
|
|
128
|
+
|
|
129
|
+
// Read once per Bot, and again only while the menu is open: a closed control
|
|
130
|
+
// pulls nothing, and an open one follows the Host without a full snapshot.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const current = live.current
|
|
133
|
+
current.mounted = true
|
|
134
|
+
void read()
|
|
135
|
+
return () => { current.mounted = false; reading.current += 1; opening.current += 1; for (const controller of current.requests) controller.abort(); current.requests.clear() }
|
|
136
|
+
}, [read])
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!open) return undefined
|
|
139
|
+
const timer = setInterval(() => void read(), MENU_MS)
|
|
140
|
+
return () => clearInterval(timer)
|
|
141
|
+
}, [open, read])
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!open) return undefined
|
|
144
|
+
const away = event => { if (!root.current?.contains(event.target)) setOpen(false) }
|
|
145
|
+
// Escape closes the menu and gives the caret back to the chip that opened it,
|
|
146
|
+
// so a keyboard owner is never dropped on the page.
|
|
147
|
+
const key = event => {
|
|
148
|
+
if (event.key !== 'Escape') return
|
|
149
|
+
closeMenu()
|
|
150
|
+
}
|
|
151
|
+
document.addEventListener('mousedown', away)
|
|
152
|
+
document.addEventListener('keydown', key)
|
|
153
|
+
return () => { document.removeEventListener('mousedown', away); document.removeEventListener('keydown', key) }
|
|
154
|
+
}, [open])
|
|
155
|
+
// The menu is placed against the control's own box, not against whatever
|
|
156
|
+
// ancestor happens to be positioned: it sits in the product's chat toolbar,
|
|
157
|
+
// whose layout the toolbar itself owns.
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!open) { setPlace(null); return undefined }
|
|
160
|
+
const measure = () => {
|
|
161
|
+
const box = root.current?.getBoundingClientRect()
|
|
162
|
+
if (box === undefined) return
|
|
163
|
+
const width = Math.min(300, Math.max(200, window.innerWidth - 24))
|
|
164
|
+
const left = Math.min(Math.max(8, box.right - width), Math.max(8, window.innerWidth - width - 8))
|
|
165
|
+
const below = box.bottom + 6, above = box.top - 6
|
|
166
|
+
setPlace({ left, top: below + 320 < window.innerHeight || above < 320 ? below : undefined, bottom: below + 320 < window.innerHeight || above < 320 ? undefined : window.innerHeight - above })
|
|
167
|
+
}
|
|
168
|
+
measure()
|
|
169
|
+
window.addEventListener('resize', measure)
|
|
170
|
+
window.addEventListener('scroll', measure, true)
|
|
171
|
+
return () => { window.removeEventListener('resize', measure); window.removeEventListener('scroll', measure, true) }
|
|
172
|
+
}, [open])
|
|
173
|
+
|
|
174
|
+
const openThread = async (threadId, { create = false } = {}) => {
|
|
175
|
+
setNote('')
|
|
176
|
+
setPending(true)
|
|
177
|
+
const outcome = await call(THREAD_ACTIONS.open, { botId, threadId, ...(create ? { create: true } : {}) }, opening)
|
|
178
|
+
setPending(false)
|
|
179
|
+
if (!outcome.ok) { if (outcome.error !== undefined) setNote(headerThreadNote(outcome.error)); return }
|
|
180
|
+
const id = outcome.result?.sessionId ?? null
|
|
181
|
+
// Only a Session the Host confirmed is navigated to.
|
|
182
|
+
if (typeof id === 'string' && id !== '') { closeMenu(); setDraft(''); navigate(id) }
|
|
183
|
+
else if (create) setNote('宿主没有返回这个线程的会话。')
|
|
184
|
+
}
|
|
185
|
+
const createThread = async () => {
|
|
186
|
+
// One intent, one key, held until the Host answers: a timeout or an unknown
|
|
187
|
+
// outcome followed by another click is the same intent, so the Host answers
|
|
188
|
+
// with the thread it already made instead of a second one. The key is
|
|
189
|
+
// retired only when the create is confirmed or the owner abandons it.
|
|
190
|
+
if (createKey.current === null) createKey.current = `head-${botId}-${++headSeq}`
|
|
191
|
+
const requestKey = createKey.current
|
|
192
|
+
const name = draft.trim().slice(0, MAX_NAME)
|
|
193
|
+
setNote('')
|
|
194
|
+
setPending(true)
|
|
195
|
+
const made = await call(THREAD_ACTIONS.create, { botId, name: name || undefined, requestKey })
|
|
196
|
+
setPending(false)
|
|
197
|
+
const threadId = made.result?.thread?.threadId
|
|
198
|
+
if (typeof threadId !== 'string') {
|
|
199
|
+
if (made.error !== undefined) setNote(headerThreadNote(made.error))
|
|
200
|
+
// Unconfirmed: the key stands, so the next attempt is the same intent.
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
createKey.current = null
|
|
204
|
+
setDraft('')
|
|
205
|
+
await openThread(threadId, { create: true })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const list = rows ?? []
|
|
209
|
+
const current = list.find(row => row.sessionId === sessionId || row.threadId === thread?.threadId) ?? null
|
|
210
|
+
// The Bot's own default chat is not a thread and is never given a substitute
|
|
211
|
+
// Session: it is listed so a thread can be left again, and opening it hands
|
|
212
|
+
// the shell the Bot id it already trusts.
|
|
213
|
+
const defaultChat = { threadId: null, name: `${bot?.name ?? botId} 的默认聊天`, sessionId: bot?.sessionId ?? bot?.id ?? botId, state: 'open', archivedAt: null, sessionState: 'owned', updatedAt: 0, active: current === null }
|
|
214
|
+
// The thread's own name comes from the freshest source that has it: the
|
|
215
|
+
// directory answer for a thread the Host has not recorded yet, the roster for
|
|
216
|
+
// one it has. The Bot beside it is the identity the roster confirms.
|
|
217
|
+
const label = current !== null ? threadLabel(current) : thread !== null ? threadLabel({ name: thread.name }) : (bot?.name ?? '')
|
|
218
|
+
const botName = bot?.name ?? null
|
|
219
|
+
const title = botName !== null ? `${label} · ${botName}` : label
|
|
220
|
+
// Without a Bot there is nothing to switch: the default chat is not a
|
|
221
|
+
// thread, and this control stays out of the way.
|
|
222
|
+
if (botId === null) return null
|
|
223
|
+
return (
|
|
224
|
+
<div className={`bp-threads-head-wrap${open ? ' is-open' : ''}${className ? ` ${className}` : ''}`} ref={root}>
|
|
225
|
+
<button type="button" className="bp-threads-head" ref={chip} aria-haspopup="menu" aria-expanded={open}
|
|
226
|
+
aria-label={botName !== null ? `当前线程 ${label}(${botName}),打开这个 Bot 的线程列表` : `打开这个 Bot 的线程列表`}
|
|
227
|
+
title={title} onClick={() => { setOpen(!open); if (!open) void read() }}>
|
|
228
|
+
<UiIcon name="list" size={14} />
|
|
229
|
+
<span className="bp-threads-head-name">{label}</span>
|
|
230
|
+
{list.length > 0 && <span className="bp-threads-head-count">{list.length + 1}</span>}
|
|
231
|
+
</button>
|
|
232
|
+
{open && (
|
|
233
|
+
<div className="bp-threads-menu" role="menu" aria-label={`${bot?.name ?? botId} 的会话线程`} style={place === null ? undefined : { position: 'fixed', left: `${place.left}px`, top: place.top === undefined ? undefined : `${place.top}px`, bottom: place.bottom === undefined ? undefined : `${place.bottom}px` }}>
|
|
234
|
+
<p className="bp-threads-menu-title">{bot?.name ?? botId} 的会话</p>
|
|
235
|
+
{note && <p className="bp-threads-note" role="alert">{note}</p>}
|
|
236
|
+
{rows === null && <p className="bp-threads-note" role="status">正在读取线程目录…</p>}
|
|
237
|
+
<ul className="bp-threads-menu-list">
|
|
238
|
+
<li key="default">
|
|
239
|
+
<button type="button" role="menuitem" data-state="default" className={`bp-threads-menu-row${current === null ? ' is-active' : ''}`}
|
|
240
|
+
disabled={pending} onClick={() => { if (current !== null) { closeMenu(); navigate(defaultChat.sessionId) } }}>
|
|
241
|
+
<span className="bp-threads-menu-name">{defaultChat.name}</span>
|
|
242
|
+
<span className="bp-threads-menu-meta">
|
|
243
|
+
{current === null && <span className="bp-thread-badge">当前</span>}
|
|
244
|
+
<span className="bp-thread-badge is-quiet">默认聊天</span>
|
|
245
|
+
</span>
|
|
246
|
+
</button>
|
|
247
|
+
</li>
|
|
248
|
+
{list.map(row => (
|
|
249
|
+
<li key={row.threadId}>
|
|
250
|
+
<button type="button" role="menuitem" data-state={isArchivedThread(row) ? 'archived' : 'open'} className={`bp-threads-menu-row${current?.threadId === row.threadId ? ' is-active' : ''}`}
|
|
251
|
+
disabled={pending} onClick={() => { if (current?.threadId !== row.threadId) void openThread(row.threadId, { create: !isArchivedThread(row) }) }}>
|
|
252
|
+
<span className="bp-threads-menu-name">{threadLabel(row)}</span>
|
|
253
|
+
<span className="bp-threads-menu-meta">
|
|
254
|
+
{current?.threadId === row.threadId && <span className="bp-thread-badge">当前</span>}
|
|
255
|
+
{isArchivedThread(row) && <span className="bp-thread-badge is-quiet">归档 · 只读</span>}
|
|
256
|
+
{row.sessionState === 'unbound' && <span className="bp-thread-badge is-wait">待宿主确认</span>}
|
|
257
|
+
<span className="bp-thread-time">{formatThreadAt(row.updatedAt)}</span>
|
|
258
|
+
</span>
|
|
259
|
+
</button>
|
|
260
|
+
</li>
|
|
261
|
+
))}
|
|
262
|
+
</ul>
|
|
263
|
+
<div className="bp-threads-menu-new">
|
|
264
|
+
<input className="bp-threads-input" value={draft} maxLength={MAX_NAME} placeholder="新线程名称(可留空)"
|
|
265
|
+
aria-label="新线程名称" onChange={event => setDraft(event.target.value)}
|
|
266
|
+
onKeyDown={event => { if (submits(event) && !pending) void createThread() }} />
|
|
267
|
+
<button type="button" className="bp-threads-button" disabled={pending} onClick={() => void createThread()}>
|
|
268
|
+
<UiIcon name="plus" size={16} /><span>新建线程</span>
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
{typeof onOpenPanel === 'function' && (
|
|
272
|
+
<button type="button" className="bp-threads-menu-more" onClick={() => { closeMenu(); onOpenPanel() }}>在线程目录中管理归档与名称</button>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
}
|