@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,439 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Data panel: the human-facing half of the load → describe → sql → show →
|
|
6
|
+
* export loop. It is embeddable and chat-first; the parent wires its entry
|
|
7
|
+
* (see data-wiring.patch). It renders ONLY declared data — a semantic table or
|
|
8
|
+
* an inline SVG chart built from a declarative series — and never executes
|
|
9
|
+
* HTML or JavaScript (no dangerouslySetInnerHTML anywhere). Real data, real
|
|
10
|
+
* errors and real limits (truncated / row caps / byte caps) are always shown.
|
|
11
|
+
*
|
|
12
|
+
* Props mirror the other panels; `transport` is the raw owner dispatch so a
|
|
13
|
+
* page or query does not refresh the whole snapshot (the Machines pattern).
|
|
14
|
+
* `scope` is the parent's Host handle (productApi): when it changes — a new
|
|
15
|
+
* Host, a new connection — everything in flight is dropped and the panel starts
|
|
16
|
+
* over, so a late answer from the previous Host can never land in this one.
|
|
17
|
+
* `busy`, `dispatch` and `transport` stay exactly as they were; `scope` is
|
|
18
|
+
* optional and a panel without one behaves like before.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const SERIES_COLORS = ['#5b7cba', '#c07ab0', '#5fa38a', '#c99a5b', '#8a7fc0', '#b06a6a']
|
|
22
|
+
/** No bots, ever: a stable empty list keeps the effects from re-running. */
|
|
23
|
+
const NO_BOTS = []
|
|
24
|
+
/** A request that outlives this is reported as a timeout, never silently dropped. */
|
|
25
|
+
const REQUEST_TIMEOUT_MS = 45_000
|
|
26
|
+
const CANCEL_TIMEOUT_MS = 10_000
|
|
27
|
+
|
|
28
|
+
const errorMessage = value => {
|
|
29
|
+
if (value?.error) return value.error.message ?? '操作失败。'
|
|
30
|
+
return ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const formatCell = cell => {
|
|
34
|
+
if (cell === null || cell === undefined) return ''
|
|
35
|
+
if (typeof cell === 'boolean') return cell ? 'true' : 'false'
|
|
36
|
+
return String(cell)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A category label for a declared point; a null x is shown, never guessed. */
|
|
40
|
+
const categoryOf = point => (point?.x === null || point?.x === undefined ? '(空)' : String(point.x))
|
|
41
|
+
/** Only a finite number may become a coordinate. */
|
|
42
|
+
const finiteY = value => (typeof value === 'number' && Number.isFinite(value) ? value : null)
|
|
43
|
+
|
|
44
|
+
/** A semantic, read-only table. Cells are text nodes; nothing is interpreted. */
|
|
45
|
+
export function DataTable({ columns, rows, caption }) {
|
|
46
|
+
if (!columns?.length) return <p className="bp-muted">没有可显示的列。</p>
|
|
47
|
+
return (
|
|
48
|
+
<div className="bp-data-table-wrap">
|
|
49
|
+
<table className="bp-data-table">
|
|
50
|
+
{caption && <caption>{caption}</caption>}
|
|
51
|
+
<thead><tr>{columns.map((column, index) => <th key={`${column.name}-${index}`} scope="col">{column.name}<span className="bp-data-type">{column.type}</span></th>)}</tr></thead>
|
|
52
|
+
<tbody>
|
|
53
|
+
{rows.map((row, rowIndex) => (
|
|
54
|
+
<tr key={rowIndex}>{row.map((cell, cellIndex) => <td key={cellIndex}>{formatCell(cell)}</td>)}</tr>
|
|
55
|
+
))}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Inline SVG bar/line/area chart from a declarative series. No scripts run. */
|
|
63
|
+
export function DataChart({ chart }) {
|
|
64
|
+
const width = 640
|
|
65
|
+
const height = 260
|
|
66
|
+
const pad = { top: 12, right: 12, bottom: 44, left: 52 }
|
|
67
|
+
const plotW = width - pad.left - pad.right
|
|
68
|
+
const plotH = height - pad.top - pad.bottom
|
|
69
|
+
// NaN/Infinity are not data points: they are dropped before any coordinate is
|
|
70
|
+
// computed, so no NaN ever reaches an SVG attribute.
|
|
71
|
+
const series = (chart?.series ?? [])
|
|
72
|
+
.map(entry => ({ key: entry.key, points: (entry.points ?? []).filter(point => finiteY(point.y) !== null) }))
|
|
73
|
+
.filter(entry => entry.points.length)
|
|
74
|
+
if (!series.length) return <p className="bp-muted">图表没有可绘制的数据点。</p>
|
|
75
|
+
|
|
76
|
+
const categories = [...new Set(series.flatMap(entry => entry.points.map(categoryOf)))]
|
|
77
|
+
const values = series.flatMap(entry => entry.points.map(point => point.y))
|
|
78
|
+
const maxY = values.length ? Math.max(...values) : 0
|
|
79
|
+
// Zero is always on the scale, so the baseline below is always inside the plot.
|
|
80
|
+
const minY = values.length ? Math.min(...values, 0) : 0
|
|
81
|
+
const span = maxY - minY || 1
|
|
82
|
+
const xStep = categories.length ? plotW / categories.length : plotW
|
|
83
|
+
const yOf = value => pad.top + plotH - ((value - minY) / span) * plotH
|
|
84
|
+
const xOf = index => pad.left + index * xStep + xStep / 2
|
|
85
|
+
const baseline = yOf(0)
|
|
86
|
+
const yTicks = [0, 0.25, 0.5, 0.75, 1].map(fraction => minY + span * fraction)
|
|
87
|
+
// Show a bounded number of category labels so 375px stays readable.
|
|
88
|
+
const labelEvery = Math.max(1, Math.ceil(categories.length / 8))
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<figure className="bp-data-chart">
|
|
92
|
+
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`${chart.type} 图:${chart.xLabel} 对 ${chart.yLabel}`} preserveAspectRatio="xMidYMid meet">
|
|
93
|
+
<title>{`${chart.type} 图:${chart.xLabel} 对 ${chart.yLabel}`}</title>
|
|
94
|
+
{yTicks.map((tick, index) => (
|
|
95
|
+
<g key={`t${index}`}>
|
|
96
|
+
<line x1={pad.left} x2={width - pad.right} y1={yOf(tick)} y2={yOf(tick)} className="bp-data-grid" />
|
|
97
|
+
<text x={pad.left - 8} y={yOf(tick) + 4} textAnchor="end" className="bp-data-axis">{Number.isInteger(tick) ? tick : tick.toFixed(1)}</text>
|
|
98
|
+
</g>
|
|
99
|
+
))}
|
|
100
|
+
{categories.map((category, index) => (
|
|
101
|
+
index % labelEvery === 0
|
|
102
|
+
? <text key={`c${index}`} x={xOf(index)} y={height - pad.bottom + 18} textAnchor="middle" className="bp-data-axis">{category.length > 10 ? `${category.slice(0, 9)}…` : category}</text>
|
|
103
|
+
: null
|
|
104
|
+
))}
|
|
105
|
+
{chart.type === 'bar' && series.map((entry, sIndex) => entry.points.map((point, index) => {
|
|
106
|
+
const categoryIndex = categories.indexOf(categoryOf(point))
|
|
107
|
+
if (categoryIndex < 0) return null
|
|
108
|
+
const barW = Math.max(2, (xStep / series.length) - 2)
|
|
109
|
+
const x = pad.left + categoryIndex * xStep + (sIndex * barW) + (series.length > 1 ? 1 : (xStep - barW) / 2)
|
|
110
|
+
const value = yOf(point.y)
|
|
111
|
+
// A bar is drawn from the ZERO baseline: a negative value extends below
|
|
112
|
+
// it, a positive one above, and neither is clipped into the axis.
|
|
113
|
+
const top = Math.min(value, baseline)
|
|
114
|
+
const barHeight = Math.max(1, Math.abs(baseline - value))
|
|
115
|
+
return <rect key={`${sIndex}-${index}`} x={x} y={top} width={barW} height={barHeight} fill={SERIES_COLORS[sIndex % SERIES_COLORS.length]} rx="2" />
|
|
116
|
+
}))}
|
|
117
|
+
{(chart.type === 'line' || chart.type === 'area') && series.map((entry, sIndex) => {
|
|
118
|
+
const color = SERIES_COLORS[sIndex % SERIES_COLORS.length]
|
|
119
|
+
const drawn = entry.points.filter(point => categories.indexOf(categoryOf(point)) >= 0)
|
|
120
|
+
if (drawn.length < 1) return null
|
|
121
|
+
const path = drawn.map((point, index) => `${index === 0 ? 'M' : 'L'} ${xOf(categories.indexOf(categoryOf(point)))} ${yOf(point.y)}`).join(' ')
|
|
122
|
+
// An area closes on the ZERO baseline, not on the bottom of the plot,
|
|
123
|
+
// so the filled region is the value itself and stays readable with
|
|
124
|
+
// negative values on the same axis.
|
|
125
|
+
const areaPath = `${path} L ${xOf(categories.indexOf(categoryOf(drawn.at(-1))))} ${baseline} L ${xOf(categories.indexOf(categoryOf(drawn[0])))} ${baseline} Z`
|
|
126
|
+
return (
|
|
127
|
+
<g key={`s${sIndex}`}>
|
|
128
|
+
{chart.type === 'area' && <path d={areaPath} fill={color} opacity="0.18" />}
|
|
129
|
+
<path d={path} fill="none" stroke={color} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
|
|
130
|
+
{drawn.map((point, index) => <circle key={index} cx={xOf(categories.indexOf(categoryOf(point)))} cy={yOf(point.y)} r="2.5" fill={color} />)}
|
|
131
|
+
</g>
|
|
132
|
+
)
|
|
133
|
+
})}
|
|
134
|
+
</svg>
|
|
135
|
+
{series.length > 1 && <figcaption className="bp-data-legend">{series.map((entry, sIndex) => (
|
|
136
|
+
<span key={entry.key}><i style={{ background: SERIES_COLORS[sIndex % SERIES_COLORS.length] }} aria-hidden="true" />{entry.key}</span>
|
|
137
|
+
))}</figcaption>}
|
|
138
|
+
</figure>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const CHART_TYPES = [['bar', '柱形图'], ['line', '折线图'], ['area', '面积图']]
|
|
143
|
+
const CHART_AGGS = [['sum', '求和'], ['avg', '平均'], ['min', '最小'], ['max', '最大']]
|
|
144
|
+
const looksNumeric = type => /INT|REAL|NUM|DEC|FLOA|DOUB|BOOL/i.test(String(type ?? ''))
|
|
145
|
+
|
|
146
|
+
export function DataPanel({ snapshot, dispatch, busy, focus, transport, scope, requestTimeoutMs = REQUEST_TIMEOUT_MS }) {
|
|
147
|
+
const bots = snapshot?.bots ?? NO_BOTS
|
|
148
|
+
const call = useCallback(async (action, payload, { timeoutMs = requestTimeoutMs } = {}) => {
|
|
149
|
+
const send = typeof transport === 'function' ? transport : dispatch
|
|
150
|
+
if (typeof send !== 'function') throw new Error('宿主未连接,无法执行该操作。')
|
|
151
|
+
const controller = new AbortController()
|
|
152
|
+
inflight.current.add(controller)
|
|
153
|
+
let timer = null
|
|
154
|
+
try {
|
|
155
|
+
const value = await Promise.race([
|
|
156
|
+
Promise.resolve(send(action, payload, controller.signal)),
|
|
157
|
+
new Promise((resolve, reject) => {
|
|
158
|
+
timer = setTimeout(() => { controller.abort(); reject(new Error('宿主响应超时;本次结果已放弃,不会自动重放。')) }, timeoutMs)
|
|
159
|
+
timer?.unref?.()
|
|
160
|
+
}),
|
|
161
|
+
])
|
|
162
|
+
// No answer is not a success: the Host said nothing, so nothing is applied.
|
|
163
|
+
if (value === undefined) throw new Error('宿主没有返回结果;本次操作已放弃,不会自动重放。')
|
|
164
|
+
const message = errorMessage(value)
|
|
165
|
+
if (message) throw new Error(message)
|
|
166
|
+
return value
|
|
167
|
+
} finally {
|
|
168
|
+
if (timer !== null) clearTimeout(timer)
|
|
169
|
+
inflight.current.delete(controller)
|
|
170
|
+
}
|
|
171
|
+
}, [transport, dispatch, scope, requestTimeoutMs])
|
|
172
|
+
|
|
173
|
+
const [botId, setBotId] = useState(bots[0]?.id ?? '')
|
|
174
|
+
const [catalog, setCatalog] = useState(null)
|
|
175
|
+
const [error, setError] = useState('')
|
|
176
|
+
const [status, setStatus] = useState('')
|
|
177
|
+
const [working, setWorking] = useState(false)
|
|
178
|
+
const [sql, setSql] = useState('')
|
|
179
|
+
const [result, setResult] = useState(null)
|
|
180
|
+
const [card, setCard] = useState(null)
|
|
181
|
+
const [loadForm, setLoadForm] = useState({ from: 'library', path: '', content: '', format: 'csv', name: '' })
|
|
182
|
+
// The chart editor works from the REAL columns of a result the Host returned;
|
|
183
|
+
// nothing is guessed from the SQL text.
|
|
184
|
+
const [chartSource, setChartSource] = useState(null)
|
|
185
|
+
const [chartColumns, setChartColumns] = useState([])
|
|
186
|
+
const [chartDraft, setChartDraft] = useState({ type: 'bar', x: '', y: '', agg: 'sum' })
|
|
187
|
+
const sequence = useRef(0)
|
|
188
|
+
const inflight = useRef(new Set())
|
|
189
|
+
|
|
190
|
+
/** Re-read the catalog for the current Bot; a stale token applies nothing. */
|
|
191
|
+
const refresh = useCallback(async token => {
|
|
192
|
+
const value = await call('data.catalog', { botId })
|
|
193
|
+
if (token !== sequence.current) return
|
|
194
|
+
setCatalog(value)
|
|
195
|
+
setError('')
|
|
196
|
+
}, [botId, call])
|
|
197
|
+
|
|
198
|
+
/** Drop every answer still on the wire; the next request starts a new generation. */
|
|
199
|
+
const dropInflight = useCallback(() => {
|
|
200
|
+
sequence.current += 1
|
|
201
|
+
for (const controller of inflight.current) controller.abort()
|
|
202
|
+
inflight.current.clear()
|
|
203
|
+
}, [])
|
|
204
|
+
|
|
205
|
+
// A new Host (scope), a new Bot or a new transport: nothing from before may
|
|
206
|
+
// land here, so the generation moves and every in-flight request is aborted.
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
dropInflight()
|
|
209
|
+
setCatalog(null); setResult(null); setCard(null); setChartSource(null); setChartColumns([])
|
|
210
|
+
if (!botId) return
|
|
211
|
+
const token = sequence.current
|
|
212
|
+
void refresh(token).catch(failure => { if (token === sequence.current) setError(String(failure?.message ?? failure)) })
|
|
213
|
+
}, [botId, call, dropInflight, refresh])
|
|
214
|
+
|
|
215
|
+
// Hidden or unmounted: late answers are dropped instead of applied.
|
|
216
|
+
useEffect(() => () => { dropInflight() }, [dropInflight])
|
|
217
|
+
|
|
218
|
+
useEffect(() => { if (!botId && bots[0]) setBotId(bots[0].id) }, [botId, bots])
|
|
219
|
+
// A Bot that is no longer in the team must not keep serving requests: the
|
|
220
|
+
// selection is replaced (and said out loud) rather than silently reused.
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!botId || bots.some(bot => bot.id === botId)) return
|
|
223
|
+
const next = bots[0]?.id ?? ''
|
|
224
|
+
setBotId(next)
|
|
225
|
+
setStatus(next ? '之前选择的 Bot 已不在团队中,已切换到当前列表中的 Bot。' : '之前选择的 Bot 已不在团队中;请先创建一个 Bot。')
|
|
226
|
+
}, [bots, botId])
|
|
227
|
+
useEffect(() => { if (focus?.botId && bots.some(bot => bot.id === focus.botId)) setBotId(focus.botId) }, [focus?.version, focus?.botId])
|
|
228
|
+
|
|
229
|
+
const perform = async work => {
|
|
230
|
+
const token = ++sequence.current
|
|
231
|
+
setWorking(true); setError(''); setStatus('')
|
|
232
|
+
try { await work(token) } catch (failure) { if (token === sequence.current) setError(String(failure?.message ?? failure)) } finally { if (token === sequence.current) setWorking(false) }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Stop the statement at the Host. A cancel is a request, never a confirmation. */
|
|
236
|
+
const cancelWork = () => {
|
|
237
|
+
dropInflight()
|
|
238
|
+
setWorking(false)
|
|
239
|
+
const token = sequence.current
|
|
240
|
+
void (async () => {
|
|
241
|
+
try {
|
|
242
|
+
await call('data.cancel', { botId }, { timeoutMs: CANCEL_TIMEOUT_MS })
|
|
243
|
+
if (token === sequence.current) setStatus('已请求取消:宿主会中止正在执行的语句。取消请求不等于取消确认。')
|
|
244
|
+
} catch (failure) { if (token === sequence.current) setError(String(failure?.message ?? failure)) }
|
|
245
|
+
})()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const disabled = busy || working || !botId
|
|
249
|
+
const tables = catalog?.tables ?? []
|
|
250
|
+
const cards = catalog?.cards ?? []
|
|
251
|
+
|
|
252
|
+
/** Open the chart editor from the columns a real result actually returned. */
|
|
253
|
+
const beginChart = (columns, source) => {
|
|
254
|
+
const list = columns ?? []
|
|
255
|
+
const numeric = list.filter(column => looksNumeric(column.type)).map(column => column.name)
|
|
256
|
+
setChartSource(source); setChartColumns(list)
|
|
257
|
+
setChartDraft({ type: 'bar', x: list[0]?.name ?? '', y: numeric[0] ?? '', agg: 'sum' })
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!bots.length) return <p className="bp-empty">还没有 Bot。先创建一个 Bot,再为它加载数据。</p>
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<article className="bp-card bp-data">
|
|
264
|
+
<div className="bp-card-title">
|
|
265
|
+
<h2>数据</h2>
|
|
266
|
+
<select aria-label="数据 Bot" value={botId} disabled={working} onChange={event => { setBotId(event.target.value); setStatus('') }}>
|
|
267
|
+
{bots.map(bot => <option key={bot.id} value={bot.id}>{bot.name ?? bot.id}</option>)}
|
|
268
|
+
</select>
|
|
269
|
+
</div>
|
|
270
|
+
<p className="bp-muted">按 Bot 隔离的 SQLite 数据集:从资料库或内联内容加载,用 SQL 查询,渲染为表格或图表并导出 CSV。所有结果按行数、列数与字节设上限;超时查询会被取消,不会挂起界面。</p>
|
|
271
|
+
|
|
272
|
+
{error && <p className="bp-error" role="alert">{error}</p>}
|
|
273
|
+
{status && <p role="status">{status}</p>}
|
|
274
|
+
{working && <p className="bp-muted">正在等待宿主…<button className="bp-text-button" onClick={cancelWork}>取消执行</button></p>}
|
|
275
|
+
|
|
276
|
+
<section className="bp-data-load" aria-label="加载数据集">
|
|
277
|
+
<h3>加载数据集</h3>
|
|
278
|
+
<div className="bp-data-load-form">
|
|
279
|
+
<label>来源
|
|
280
|
+
<select value={loadForm.from} onChange={event => setLoadForm({ ...loadForm, from: event.target.value })}>
|
|
281
|
+
<option value="library">资料库文件</option>
|
|
282
|
+
<option value="content">内联 CSV / JSON</option>
|
|
283
|
+
</select>
|
|
284
|
+
</label>
|
|
285
|
+
{loadForm.from === 'library'
|
|
286
|
+
? <label>资料库路径<input value={loadForm.path} onChange={event => setLoadForm({ ...loadForm, path: event.target.value })} placeholder="data/sales.csv" /></label>
|
|
287
|
+
: <label>内容<textarea rows={4} value={loadForm.content} onChange={event => setLoadForm({ ...loadForm, content: event.target.value })} placeholder="col1,col2 a,1" /></label>}
|
|
288
|
+
<label>格式
|
|
289
|
+
<select value={loadForm.format} onChange={event => setLoadForm({ ...loadForm, format: event.target.value })}>
|
|
290
|
+
<option value="csv">CSV</option><option value="tsv">TSV</option><option value="json">JSON</option>
|
|
291
|
+
</select>
|
|
292
|
+
</label>
|
|
293
|
+
<label>表名<input value={loadForm.name} onChange={event => setLoadForm({ ...loadForm, name: event.target.value })} placeholder="默认取文件名" /></label>
|
|
294
|
+
<button className="bp-button-primary" disabled={disabled} onClick={() => { void perform(async token => {
|
|
295
|
+
const value = await call('data.load', { botId, from: loadForm.from, path: loadForm.path, content: loadForm.content, format: loadForm.format, name: loadForm.name || undefined })
|
|
296
|
+
if (token !== sequence.current) return
|
|
297
|
+
const loaded = value?.tables?.[0]
|
|
298
|
+
setStatus(loaded ? `已加载 ${loaded.name}:${loaded.rowCount} 行,${loaded.columns.length} 列。` : '已加载。')
|
|
299
|
+
await refresh(token)
|
|
300
|
+
}) }}>加载</button>
|
|
301
|
+
</div>
|
|
302
|
+
</section>
|
|
303
|
+
|
|
304
|
+
<section className="bp-data-catalog" aria-label="数据集目录">
|
|
305
|
+
<h3>已加载的表</h3>
|
|
306
|
+
{!tables.length ? <p className="bp-muted">还没有加载任何表。</p> : (
|
|
307
|
+
<ul className="bp-data-tables">
|
|
308
|
+
{tables.map(table => (
|
|
309
|
+
<li key={table.name}>
|
|
310
|
+
<button className="bp-text-button" disabled={disabled} onClick={() => { void perform(async token => {
|
|
311
|
+
setResult(null)
|
|
312
|
+
const value = await call('data.show', { botId, table: table.name, kind: 'table', title: table.name })
|
|
313
|
+
if (token !== sequence.current) return
|
|
314
|
+
setCard(value)
|
|
315
|
+
}) }}>{table.name}</button>
|
|
316
|
+
<span className="bp-muted">{table.columns.map(column => column.name).join(', ')}</span>
|
|
317
|
+
<span className="bp-row-actions">
|
|
318
|
+
<button disabled={disabled} onClick={() => { void perform(async token => {
|
|
319
|
+
const statement = `SELECT * FROM ${table.name} LIMIT 100`
|
|
320
|
+
const value = await call('data.sql', { botId, sql: statement })
|
|
321
|
+
if (token !== sequence.current) return
|
|
322
|
+
setSql(statement); setCard(null); setResult(value)
|
|
323
|
+
beginChart(value.columns, { table: table.name })
|
|
324
|
+
}) }}>查询</button>
|
|
325
|
+
<button disabled={disabled} onClick={() => { void perform(async token => {
|
|
326
|
+
beginChart(table.columns, { table: table.name })
|
|
327
|
+
if (token === sequence.current) setStatus(`已按 ${table.name} 的真实列打开图表编辑。`)
|
|
328
|
+
}) }}>图表</button>
|
|
329
|
+
<button disabled={disabled} onClick={() => { void perform(async token => {
|
|
330
|
+
const value = await call('data.export', { botId, table: table.name, filename: table.name })
|
|
331
|
+
if (token !== sequence.current) return
|
|
332
|
+
setStatus(exportStatus(value))
|
|
333
|
+
}) }}>导出 CSV</button>
|
|
334
|
+
</span>
|
|
335
|
+
</li>
|
|
336
|
+
))}
|
|
337
|
+
</ul>
|
|
338
|
+
)}
|
|
339
|
+
</section>
|
|
340
|
+
|
|
341
|
+
<section className="bp-data-query" aria-label="SQL 查询">
|
|
342
|
+
<h3>SQL 查询</h3>
|
|
343
|
+
<textarea rows={3} value={sql} onChange={event => setSql(event.target.value)} placeholder="SELECT region, SUM(sales) AS total FROM sales GROUP BY region" aria-label="SQL" />
|
|
344
|
+
<div className="bp-actions">
|
|
345
|
+
<button className="bp-button-primary" disabled={disabled || !sql.trim()} onClick={() => { void perform(async token => {
|
|
346
|
+
setCard(null)
|
|
347
|
+
const value = await call('data.sql', { botId, sql })
|
|
348
|
+
if (token !== sequence.current) return
|
|
349
|
+
setResult(value)
|
|
350
|
+
beginChart(value.columns, { sql })
|
|
351
|
+
}) }}>运行</button>
|
|
352
|
+
</div>
|
|
353
|
+
{result && <div className="bp-data-result">
|
|
354
|
+
<p className="bp-muted">{result.table ? `结果表 ${result.table} · ` : ''}{result.rowCount} 行{result.truncated ? ' · 已截断(结果受上限约束)' : ''}</p>
|
|
355
|
+
{result.error ? <p className="bp-error">{result.error.message}</p> : <DataTable columns={result.columns} rows={result.rows} />}
|
|
356
|
+
</div>}
|
|
357
|
+
</section>
|
|
358
|
+
|
|
359
|
+
{chartSource && chartColumns.length > 0 && <section className="bp-data-chart-editor" aria-label="图表编辑">
|
|
360
|
+
<h3>图表(按上面结果的真实列显式选择)</h3>
|
|
361
|
+
<div className="bp-data-load-form">
|
|
362
|
+
<label>类型
|
|
363
|
+
<select value={chartDraft.type} onChange={event => setChartDraft({ ...chartDraft, type: event.target.value })}>
|
|
364
|
+
{CHART_TYPES.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
|
|
365
|
+
</select>
|
|
366
|
+
</label>
|
|
367
|
+
<label>X 列(分组)
|
|
368
|
+
<select value={chartDraft.x} onChange={event => setChartDraft({ ...chartDraft, x: event.target.value })}>
|
|
369
|
+
{chartColumns.map(column => <option key={column.name} value={column.name}>{column.name}({column.type})</option>)}
|
|
370
|
+
</select>
|
|
371
|
+
</label>
|
|
372
|
+
<label>Y 列
|
|
373
|
+
<select value={chartDraft.y} onChange={event => setChartDraft({ ...chartDraft, y: event.target.value })}>
|
|
374
|
+
<option value="">不选:按行计数</option>
|
|
375
|
+
{chartColumns.map(column => <option key={column.name} value={column.name}>{column.name}({column.type})</option>)}
|
|
376
|
+
</select>
|
|
377
|
+
</label>
|
|
378
|
+
{chartDraft.y && <label>聚合
|
|
379
|
+
<select value={chartDraft.agg} onChange={event => setChartDraft({ ...chartDraft, agg: event.target.value })}>
|
|
380
|
+
{CHART_AGGS.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
|
|
381
|
+
</select>
|
|
382
|
+
</label>}
|
|
383
|
+
<button className="bp-button-primary" disabled={disabled || !chartDraft.x} onClick={() => { void perform(async token => {
|
|
384
|
+
const chart = { type: chartDraft.type, x: chartDraft.x, ...(chartDraft.y ? { y: chartDraft.y, agg: chartDraft.agg } : {}) }
|
|
385
|
+
const value = await call('data.show', { botId, ...chartSource, kind: 'chart', title: card?.title ?? '查询图表', chart })
|
|
386
|
+
if (token !== sequence.current) return
|
|
387
|
+
setCard(value)
|
|
388
|
+
}) }}>生成图表</button>
|
|
389
|
+
</div>
|
|
390
|
+
<p className="bp-muted">列名来自宿主返回的真实结果;若所选组合无法聚合,宿主会直接报错,不会替您猜测。</p>
|
|
391
|
+
</section>}
|
|
392
|
+
|
|
393
|
+
{card && <section className="bp-data-card" aria-label="结果卡片">
|
|
394
|
+
<div className="bp-card-title"><h3>{card.title ?? '结果'}</h3>
|
|
395
|
+
<span className="bp-muted">{card.rowCount} 行{card.truncated ? ' · 已截断' : ''}</span>
|
|
396
|
+
<button disabled={disabled} onClick={() => { void perform(async token => {
|
|
397
|
+
const value = await call('data.export', { botId, cardId: card.id, filename: card.title ?? 'card' })
|
|
398
|
+
if (token !== sequence.current) return
|
|
399
|
+
setStatus(exportStatus(value))
|
|
400
|
+
}) }}>导出 CSV</button>
|
|
401
|
+
</div>
|
|
402
|
+
{card.error ? <p className="bp-error">{card.error.message}</p> : card.chart
|
|
403
|
+
? <DataChart chart={card.chart} />
|
|
404
|
+
: <DataTable columns={card.columns} rows={card.rows} />}
|
|
405
|
+
</section>}
|
|
406
|
+
|
|
407
|
+
{cards.length > 0 && <section className="bp-data-cards" aria-label="已保存卡片">
|
|
408
|
+
<h3>已保存的卡片</h3>
|
|
409
|
+
<ul className="bp-data-card-list">
|
|
410
|
+
{cards.map(entry => (
|
|
411
|
+
<li key={entry.id}>
|
|
412
|
+
<button className="bp-text-button" disabled={disabled} onClick={() => { void perform(async token => {
|
|
413
|
+
setResult(null)
|
|
414
|
+
// A card is re-opened by its id — a chart card re-renders from
|
|
415
|
+
// its saved spec instead of being looked up as a table.
|
|
416
|
+
const value = await call('data.show', { botId, cardId: entry.id, kind: entry.kind })
|
|
417
|
+
if (token !== sequence.current) return
|
|
418
|
+
setCard(value)
|
|
419
|
+
}) }}>{entry.title}</button>
|
|
420
|
+
<span className="bp-muted">{entry.kind === 'chart' ? '图表' : '表格'}</span>
|
|
421
|
+
</li>
|
|
422
|
+
))}
|
|
423
|
+
</ul>
|
|
424
|
+
</section>}
|
|
425
|
+
</article>
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** A completed export says what was written; a failed one never reaches here. */
|
|
430
|
+
function exportStatus(value) {
|
|
431
|
+
const rows = `已导出 ${value.rowCount} 行`
|
|
432
|
+
const source = value.sourceRowCount !== undefined && value.sourceRowCount !== value.rowCount ? `(源共 ${value.sourceRowCount} 行)` : ''
|
|
433
|
+
const bytes = `,${value.bytes} 字节`
|
|
434
|
+
const where = value.path ? `到 ${value.path}` : '(未写入资料库,仅返回内容)'
|
|
435
|
+
const cut = value.truncated ? ' · 已截断' : ''
|
|
436
|
+
return `${rows}${source}${bytes}${where}${cut}。`
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export default DataPanel
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* Data panel surfaces. Colours come from the shared --bp-* tokens so the panel
|
|
2
|
+
* follows the workspace light/dark theme automatically; the parent concatenates
|
|
3
|
+
* this sheet with the others (see data-wiring.patch). Nothing here executes. */
|
|
4
|
+
|
|
5
|
+
.bp-data .bp-data-load-form,
|
|
6
|
+
.bp-data .bp-data-query {
|
|
7
|
+
display: grid;
|
|
8
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
9
|
+
gap: 10px 12px;
|
|
10
|
+
align-items: end;
|
|
11
|
+
margin: 10px 0 4px;
|
|
12
|
+
}
|
|
13
|
+
.bp-data .bp-data-query { grid-template-columns: 1fr; }
|
|
14
|
+
.bp-data .bp-data-load-form label,
|
|
15
|
+
.bp-data label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--bp-muted); }
|
|
16
|
+
.bp-data input,
|
|
17
|
+
.bp-data select,
|
|
18
|
+
.bp-data textarea {
|
|
19
|
+
font: inherit; font-size: 13px; color: var(--bp-ink); background: var(--bp-surface);
|
|
20
|
+
border: 1px solid var(--bp-border); border-radius: 8px; padding: 7px 9px; min-height: 24px; width: 100%;
|
|
21
|
+
}
|
|
22
|
+
.bp-data textarea { resize: vertical; font-family: var(--bp-font); }
|
|
23
|
+
.bp-data input:focus-visible,
|
|
24
|
+
.bp-data select:focus-visible,
|
|
25
|
+
.bp-data textarea:focus-visible { outline: 2px solid var(--bp-focus); outline-offset: 1px; }
|
|
26
|
+
.bp-data h3 { margin: 20px 0 6px; }
|
|
27
|
+
.bp-data section { margin-top: 4px; }
|
|
28
|
+
|
|
29
|
+
.bp-data-tables, .bp-data-card-list { list-style: none; margin: 8px 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
30
|
+
.bp-data-tables li, .bp-data-card-list li {
|
|
31
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
32
|
+
padding: 8px 10px; border: 1px solid var(--bp-border); border-radius: 10px; background: var(--bp-surface);
|
|
33
|
+
}
|
|
34
|
+
.bp-data-tables li > .bp-muted { flex: 1 1 160px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
|
35
|
+
.bp-data-card-list li { justify-content: space-between; }
|
|
36
|
+
.bp-data .bp-row-actions { display: inline-flex; gap: 6px; margin-left: auto; }
|
|
37
|
+
|
|
38
|
+
.bp-data-table-wrap { overflow: auto; border: 1px solid var(--bp-border); border-radius: 10px; margin: 12px 0; max-width: 100%; }
|
|
39
|
+
.bp-data-table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
40
|
+
.bp-data-table caption { text-align: left; padding: 8px 10px; color: var(--bp-muted); font-size: 12px; }
|
|
41
|
+
.bp-data-table th, .bp-data-table td { padding: 7px 10px; border-bottom: 1px solid var(--bp-border); text-align: left; white-space: nowrap; }
|
|
42
|
+
.bp-data-table th { position: sticky; top: 0; background: var(--bp-subtle); font-weight: 600; z-index: 1; }
|
|
43
|
+
.bp-data-table td { font-variant-numeric: tabular-nums; }
|
|
44
|
+
.bp-data-table tbody tr:last-child td { border-bottom: 0; }
|
|
45
|
+
.bp-data-type { display: block; font-weight: 400; font-size: 10px; color: var(--bp-quiet); text-transform: uppercase; letter-spacing: .03em; }
|
|
46
|
+
|
|
47
|
+
.bp-data-chart { margin: 12px 0; }
|
|
48
|
+
.bp-data-chart svg { width: 100%; height: auto; max-width: 720px; background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 10px; }
|
|
49
|
+
.bp-data-grid { stroke: var(--bp-border); stroke-width: 1; }
|
|
50
|
+
.bp-data-axis { fill: var(--bp-muted); font-size: 11px; }
|
|
51
|
+
.bp-data-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 12px; color: var(--bp-muted); }
|
|
52
|
+
.bp-data-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
53
|
+
.bp-data-legend i { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
|
|
54
|
+
|
|
55
|
+
.bp-data-result p, .bp-data-card .bp-card-title { margin: 4px 0; }
|
|
56
|
+
|
|
57
|
+
.bp-data .bp-data-chart-editor .bp-data-load-form { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
|
|
58
|
+
.bp-data .bp-actions { display: flex; gap: 8px; align-items: center; margin: 8px 0 4px; }
|
|
59
|
+
.bp-data .bp-actions button { font: inherit; font-size: 13px; padding: 7px 12px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-surface); color: var(--bp-ink); cursor: pointer; }
|
|
60
|
+
.bp-data .bp-actions .bp-button-primary { color: var(--bp-on-solid); background: var(--bp-solid); border-color: var(--bp-solid); }
|
|
61
|
+
.bp-data button:disabled { opacity: .55; cursor: not-allowed; }
|
|
62
|
+
.bp-data-chart-editor p.bp-muted { margin: 6px 0 0; }
|
|
63
|
+
|
|
64
|
+
@container (max-width: 600px) {
|
|
65
|
+
.bp-data .bp-data-load-form { grid-template-columns: 1fr; }
|
|
66
|
+
.bp-data-tables li { flex-direction: column; align-items: stretch; }
|
|
67
|
+
.bp-data .bp-row-actions { margin-left: 0; }
|
|
68
|
+
.bp-data-table th, .bp-data-table td { padding: 6px 8px; }
|
|
69
|
+
}
|
|
@@ -14,6 +14,7 @@ export function installDesktopBridge(api, renderPane) {
|
|
|
14
14
|
open: async (botId, machineId, options) => api.dispatch('machine.desktop.open', { botId, ...await target(machineId), ...options }),
|
|
15
15
|
renew: (botId, sessionId) => api.dispatch('machine.desktop.renew', { botId, sessionId }),
|
|
16
16
|
close: (botId, sessionId) => api.dispatch('machine.desktop.close', { botId, sessionId }),
|
|
17
|
+
transport: (action,payload,signal) => api.dispatch(action,payload,signal),
|
|
17
18
|
saveRoutine: (botId, routine) => api.dispatch('routine.save', { botId, routine }),
|
|
18
19
|
view: async (botId, machineId) => api.dispatch('machine.desktop.view', { botId, ...await target(machineId) }),
|
|
19
20
|
act: async (botId, machineId, screenshotId, action) => api.dispatch('machine.desktop.act', { botId, ...await target(machineId), screenshotId, action }),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Fetch uses the shell's existing authenticated bridge. Application writes are sent once, in order. */
|
|
2
|
+
export function createOwnerDesktopSocket({ticket,botId,dispatch}){
|
|
3
|
+
const socket={readyState:0,binaryType:'arraybuffer',onopen:null,onmessage:null,onclose:null,onerror:null}
|
|
4
|
+
const controller=new AbortController()
|
|
5
|
+
let cursor=0,authorized=false,sequence=0,pending=0,outbound=Promise.resolve()
|
|
6
|
+
const stop=(error)=>{
|
|
7
|
+
if(socket.readyState===3)return
|
|
8
|
+
socket.readyState=3;controller.abort()
|
|
9
|
+
if(error)socket.onerror?.({error})
|
|
10
|
+
socket.onclose?.({code:error?1011:1000,reason:error?'桌面连接中断,请检查电脑后重新连接。':''})
|
|
11
|
+
}
|
|
12
|
+
const input=(extra)=>({botId,sessionId:ticket.sessionId,token:ticket.token,...extra})
|
|
13
|
+
const poll=async()=>{
|
|
14
|
+
try{
|
|
15
|
+
while(socket.readyState===1){
|
|
16
|
+
const value=await dispatch('machine.desktop.poll',input({cursor}),controller.signal)
|
|
17
|
+
if(socket.readyState!==1)return
|
|
18
|
+
if(!value||!Array.isArray(value.events)||!Number.isSafeInteger(value.cursor)||value.cursor<cursor)throw new Error('DESKTOP_PROTOCOL_INVALID')
|
|
19
|
+
if(value.closed){stop(new Error(value.code??'DESKTOP_DISCONNECTED'));return}
|
|
20
|
+
for(const event of value.events){
|
|
21
|
+
if(!Number.isSafeInteger(event.sequence)||event.sequence<=cursor||event.sequence>value.cursor||typeof event.data!=='string'||event.data.length>12*1024*1024)throw new Error('DESKTOP_PROTOCOL_INVALID')
|
|
22
|
+
const bytes=Uint8Array.from(atob(event.data),char=>char.charCodeAt(0))
|
|
23
|
+
if(event.kind==='message')socket.onmessage?.({data:event.binary?bytes.buffer:new TextDecoder().decode(bytes)})
|
|
24
|
+
else if(event.kind!=='ping')throw new Error('DESKTOP_PROTOCOL_INVALID')
|
|
25
|
+
cursor=event.sequence
|
|
26
|
+
}
|
|
27
|
+
if(cursor!==value.cursor)throw new Error('DESKTOP_PROTOCOL_INVALID')
|
|
28
|
+
}
|
|
29
|
+
}catch(error){if(!controller.signal.aborted)stop(error)}
|
|
30
|
+
}
|
|
31
|
+
socket.send=text=>{
|
|
32
|
+
if(socket.readyState!==1)throw new Error('DESKTOP_DISCONNECTED')
|
|
33
|
+
const message=JSON.parse(text)
|
|
34
|
+
if(!authorized){
|
|
35
|
+
if(message.type!=='auth'||message.sessionId!==ticket.sessionId||message.token!==ticket.token)throw new Error('DESKTOP_AUTH_INVALID')
|
|
36
|
+
authorized=true;void poll();return
|
|
37
|
+
}
|
|
38
|
+
if(++pending>64){stop(new Error('DESKTOP_INPUT_LIMIT'));return}
|
|
39
|
+
const requestId=`http-desktop-${++sequence}`
|
|
40
|
+
outbound=outbound.then(async()=>{
|
|
41
|
+
if(socket.readyState!==1)return
|
|
42
|
+
const result=await dispatch('machine.desktop.send',input({requestId,message:text}),controller.signal)
|
|
43
|
+
if(result?.accepted!==true)throw new Error('DESKTOP_INPUT_UNKNOWN')
|
|
44
|
+
}).catch(error=>stop(error)).finally(()=>pending--)
|
|
45
|
+
}
|
|
46
|
+
socket.close=()=>stop()
|
|
47
|
+
queueMicrotask(()=>{if(socket.readyState!==0)return;socket.readyState=1;socket.onopen?.({})})
|
|
48
|
+
return socket
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const DESKTOP_SHORTCUTS=Object.freeze([
|
|
2
|
+
{label:'Tab',keys:[{code:'Tab',key:'Tab'}]},
|
|
3
|
+
{label:'Esc',keys:[{code:'Escape',key:'Escape'}]},
|
|
4
|
+
{label:'Enter',keys:[{code:'Enter',key:'Enter'}]},
|
|
5
|
+
{label:'Windows',keys:[{code:'MetaLeft',key:'Meta'}]},
|
|
6
|
+
{label:'全选',keys:[{code:'ControlLeft',key:'Control'},{code:'KeyA',key:'a'}]},
|
|
7
|
+
{label:'复制',keys:[{code:'ControlLeft',key:'Control'},{code:'KeyC',key:'c'}]},
|
|
8
|
+
{label:'粘贴远端剪贴板',keys:[{code:'ControlLeft',key:'Control'},{code:'KeyV',key:'v'}]},
|
|
9
|
+
{label:'任务管理器',keys:[{code:'ControlLeft',key:'Control'},{code:'ShiftLeft',key:'Shift'},{code:'Escape',key:'Escape'}]},
|
|
10
|
+
].map(row=>Object.freeze({...row,keys:Object.freeze(row.keys.map(key=>Object.freeze(key)))})))
|
|
11
|
+
|
|
12
|
+
/** Each transition waits for its original native ACK. Failure releases held input and never replays a key. */
|
|
13
|
+
export async function sendDesktopKeys(stream,keys,isCurrent=()=>true){
|
|
14
|
+
const check=()=>{if(!isCurrent())throw new Error('DESKTOP_SESSION_CHANGED')}
|
|
15
|
+
try{
|
|
16
|
+
check();await stream.drain();check()
|
|
17
|
+
for(const action of ['down','up'])for(const key of action==='up'?[...keys].reverse():keys){
|
|
18
|
+
check();if(!stream.input({type:'key',action,...key}))throw new Error('CONTROL_NOT_GRANTED')
|
|
19
|
+
await stream.drain();check()
|
|
20
|
+
}
|
|
21
|
+
}catch(error){await stream.releaseInput()?.catch(()=>{});throw error}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function sendDesktopText(stream,text,isCurrent=()=>true){
|
|
25
|
+
if(typeof text!=='string'||!text||text.length>8192)throw new Error('INPUT_LIMIT')
|
|
26
|
+
if(!isCurrent())throw new Error('DESKTOP_SESSION_CHANGED')
|
|
27
|
+
const released=await stream.releaseInput()
|
|
28
|
+
if(released?.ok!==true||!isCurrent())throw new Error('INPUT_RELEASE_UNCONFIRMED')
|
|
29
|
+
if(!stream.input({type:'text',text}))throw new Error('CONTROL_NOT_GRANTED')
|
|
30
|
+
await stream.drain()
|
|
31
|
+
if(!isCurrent())throw new Error('DESKTOP_SESSION_CHANGED')
|
|
32
|
+
}
|