@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,221 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
3
|
+
import {
|
|
4
|
+
MAX_WEB_PANELS, WEB_PANEL_LOAD_TIMEOUT_MS, WEB_PANEL_REFERRER_POLICY, WEB_PANEL_SANDBOX,
|
|
5
|
+
actionError, clientOrigin, framingMessage, isSelfOrigin, movedOrder, openExternally, panelsOf, validateWebPanelDraft,
|
|
6
|
+
} from './web-panel-model.mjs'
|
|
7
|
+
|
|
8
|
+
// A pinned page shown inside the workspace instead of the chat. The list is the
|
|
9
|
+
// Host's own directory (never this browser's), the frame is a real cross-origin
|
|
10
|
+
// iframe, and a page that refuses framing — or never loads — is answered with the
|
|
11
|
+
// reason plus a way to open it in the browser.
|
|
12
|
+
//
|
|
13
|
+
// Every async result is checked against the Host it was asked of: a replaced Host
|
|
14
|
+
// remounts this board, so a late answer from the previous one cannot name a page
|
|
15
|
+
// the new Host never served.
|
|
16
|
+
|
|
17
|
+
let scopeKey = 0
|
|
18
|
+
export function WebPanelsPanel(props) {
|
|
19
|
+
// One board per Host. A replaced product API gets a fresh board instead of a
|
|
20
|
+
// component that has to remember which answers are still allowed to land.
|
|
21
|
+
const key = useMemo(() => ++scopeKey, [props.productApi])
|
|
22
|
+
return <WebPanelBoard key={key} {...props} />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The sidebar's compact pin list: the same Host directory, one click to a page. */
|
|
26
|
+
export function WebPanelPins({ panels = [], activeId, onSelect, busy = false }) {
|
|
27
|
+
if (!panels.length) return null
|
|
28
|
+
return <nav className="bp-webpanel-pins" aria-label="固定网页面板">
|
|
29
|
+
<p className="bp-webpanel-pins-label">常用网页</p>
|
|
30
|
+
<ul>{panels.map(panel => <li key={panel.id}>
|
|
31
|
+
<button type="button" className="bp-webpanel-pin" aria-current={activeId === panel.id ? 'page' : undefined}
|
|
32
|
+
disabled={busy} title={`${panel.name} · ${panel.host}`} onClick={() => onSelect?.(panel.id)}>
|
|
33
|
+
<UiIcon name="board" size={15} />
|
|
34
|
+
<span className="bp-webpanel-pin-name">{panel.name}</span>
|
|
35
|
+
<span className="bp-webpanel-pin-host">{panel.host}</span>
|
|
36
|
+
</button>
|
|
37
|
+
</li>)}</ul>
|
|
38
|
+
</nav>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function WebPanelBoard({ productApi, dispatch, busy = false, snapshot, available = true, onLeave, selectedId: drivenId, onSelect }) {
|
|
42
|
+
const { status, items, revision, limits, reason, nextAction } = panelsOf(snapshot)
|
|
43
|
+
const [ownId, setOwnId] = useState(null)
|
|
44
|
+
const [reloads, setReloads] = useState(0)
|
|
45
|
+
const [probes, setProbes] = useState({})
|
|
46
|
+
const [draft, setDraft] = useState(null)
|
|
47
|
+
const [draftError, setDraftError] = useState('')
|
|
48
|
+
const [notice, setNotice] = useState('')
|
|
49
|
+
const [error, setError] = useState('')
|
|
50
|
+
const [frame, setFrame] = useState('loading')
|
|
51
|
+
const origin = clientOrigin()
|
|
52
|
+
const leave = useRef(onLeave)
|
|
53
|
+
leave.current = onLeave
|
|
54
|
+
// The selection is the board's own unless a parent drives it, so the sidebar pin
|
|
55
|
+
// list and the board can never disagree about which page is on screen.
|
|
56
|
+
const selectedId = typeof drivenId === 'string' && drivenId ? drivenId : ownId
|
|
57
|
+
const select = useCallback(id => { setOwnId(id); setReloads(0); setFrame('loading'); if (typeof onSelect === 'function') onSelect(id) }, [onSelect])
|
|
58
|
+
|
|
59
|
+
const panel = items.find(entry => entry.id === selectedId) ?? null
|
|
60
|
+
const full = limits ? items.length >= limits.maxPanels : false
|
|
61
|
+
// The board reads the panel it is showing through a ref, so an effect keyed on
|
|
62
|
+
// the panel's identity does not restart because the list was re-projected.
|
|
63
|
+
const shown = useRef(null)
|
|
64
|
+
shown.current = panel
|
|
65
|
+
|
|
66
|
+
useEffect(() => { if (!selectedId && items.length) setOwnId(items[0].id) }, [items, selectedId])
|
|
67
|
+
|
|
68
|
+
// The selected panel is gone: it was removed here, or this is a different
|
|
69
|
+
// Host's list. Either way nothing of the old page stays on screen.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (selectedId === null || items.some(entry => entry.id === selectedId)) return
|
|
72
|
+
setOwnId(null); setProbes({}); setReloads(0)
|
|
73
|
+
leave.current?.()
|
|
74
|
+
}, [items, selectedId])
|
|
75
|
+
|
|
76
|
+
// A page may also change under us (the list was edited elsewhere), so a reload
|
|
77
|
+
// key covers both an explicit refresh and a new URL.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!selectedId) return
|
|
80
|
+
setFrame('loading')
|
|
81
|
+
const timer = setTimeout(() => setFrame(current => (current === 'loading' ? 'failed' : current)), WEB_PANEL_LOAD_TIMEOUT_MS)
|
|
82
|
+
return () => clearTimeout(timer)
|
|
83
|
+
}, [selectedId, panel?.url, reloads])
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!available || !selectedId || typeof productApi?.dispatch !== 'function') return
|
|
87
|
+
const entry = shown.current
|
|
88
|
+
if (!entry) return
|
|
89
|
+
const controller = new AbortController()
|
|
90
|
+
setProbes(previous => ({ ...previous, [entry.id]: { state: 'checking' } }))
|
|
91
|
+
productApi.dispatch('webPanels.probe', { id: entry.id }, controller.signal)
|
|
92
|
+
.then(result => { if (!controller.signal.aborted) setProbes(previous => ({ ...previous, [entry.id]: { state: 'done', ...result } })) })
|
|
93
|
+
.catch(() => { if (!controller.signal.aborted) setProbes(previous => ({ ...previous, [entry.id]: { state: 'error' } })) })
|
|
94
|
+
return () => controller.abort()
|
|
95
|
+
}, [available, selectedId, panel?.url, reloads, productApi])
|
|
96
|
+
|
|
97
|
+
const run = useCallback(async (action, payload) => {
|
|
98
|
+
setError(''); setNotice('')
|
|
99
|
+
if (typeof dispatch !== 'function') { setError('当前连接暂不能修改固定页面。'); return null }
|
|
100
|
+
let result
|
|
101
|
+
try {
|
|
102
|
+
// The workspace's own dispatch answers a failure as `{ok:false}`; a host that
|
|
103
|
+
// throws instead is answered the same way rather than losing the message.
|
|
104
|
+
result = await dispatch(action, payload, { errorResult: true })
|
|
105
|
+
} catch (cause) { setError(actionError(cause).text); return null }
|
|
106
|
+
if (result?.ok === false) { setError(actionError(result.error).text); return null }
|
|
107
|
+
return result
|
|
108
|
+
}, [dispatch])
|
|
109
|
+
|
|
110
|
+
const startAdd = () => { setDraft({ id: null, name: '', url: '' }); setDraftError('') }
|
|
111
|
+
const startEdit = () => { if (panel) { setDraft({ id: panel.id, name: panel.name, url: panel.url }); setDraftError('') } }
|
|
112
|
+
|
|
113
|
+
const save = async () => {
|
|
114
|
+
if (!draft) return
|
|
115
|
+
// The draft is checked here only to answer while the person is still typing;
|
|
116
|
+
// the Host re-validates with the real embedding origin and its own limits.
|
|
117
|
+
const check = validateWebPanelDraft(draft, origin)
|
|
118
|
+
if (!check.ok) { setDraftError(actionError({ code: check.error === 'self' ? 'WEB_PANEL_SELF_URL' : check.error === 'name' ? 'WEB_PANEL_NAME' : 'WEB_PANEL_URL' }).text); return }
|
|
119
|
+
const saved = await run('webPanels.save', { ...(draft.id ? { id: draft.id } : {}), name: check.name, url: check.url, expectedRevision: revision })
|
|
120
|
+
if (saved) { setDraft(null); setNotice(draft.id ? '已保存修改。' : '已固定该页面。'); if (draft.id) setReloads(value => value + 1); else select(saved.panel.id) }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const remove = async panelId => {
|
|
124
|
+
const removed = await run('webPanels.remove', { id: panelId, expectedRevision: revision })
|
|
125
|
+
if (removed) setNotice(panelId === selectedId ? '已取消固定,返回工作空间。' : '已取消固定。')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const move = async (panelId, delta) => {
|
|
129
|
+
const ids = movedOrder(items, panelId, delta)
|
|
130
|
+
if (!ids) return
|
|
131
|
+
const result = await run('webPanels.order', { ids, expectedRevision: revision })
|
|
132
|
+
if (result) setNotice('顺序已保存。')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const probe = probes[panel?.id] ?? null
|
|
136
|
+
const verdict = available ? framingMessage(probe) : { tone: 'idle', text: '' }
|
|
137
|
+
const refused = probe?.state === 'done' && probe.framing === 'blocked'
|
|
138
|
+
const selfFramed = Boolean(panel && isSelfOrigin(panel.url, origin))
|
|
139
|
+
|
|
140
|
+
if (status !== 'available') {
|
|
141
|
+
return <section className="bp-webpanel" aria-label="固定网页面板">
|
|
142
|
+
<header className="bp-webpanel-bar"><h2>固定网页面板</h2></header>
|
|
143
|
+
<p className="bp-webpanel-note" role="status">{status === 'blocked' ? (reason || '宿主尚未提供固定网页面板目录。') : '当前连接暂时无法读取固定网页面板。恢复连接后会在这里显示。'}</p>
|
|
144
|
+
{status === 'blocked' && nextAction && <p className="bp-webpanel-note bp-webpanel-note-quiet">下一步:{nextAction}</p>}
|
|
145
|
+
</section>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return <section className="bp-webpanel" aria-label="固定网页面板">
|
|
149
|
+
<header className="bp-webpanel-bar">
|
|
150
|
+
<h2>固定网页面板</h2>
|
|
151
|
+
<span className="bp-webpanel-count" aria-label={`已固定 ${items.length} 个,上限 ${limits?.maxPanels ?? MAX_WEB_PANELS} 个`}>{items.length} / {limits?.maxPanels ?? MAX_WEB_PANELS}</span>
|
|
152
|
+
<button type="button" className="bp-webpanel-button" disabled={busy || !available || full} onClick={startAdd}>
|
|
153
|
+
<UiIcon name="plus" size={15} /><span>新增固定页面</span>
|
|
154
|
+
</button>
|
|
155
|
+
</header>
|
|
156
|
+
{notice && <p className="bp-webpanel-note bp-webpanel-note-ok" role="status">{notice}</p>}
|
|
157
|
+
{error && <p className="bp-webpanel-note bp-webpanel-note-bad" role="alert">{error}</p>}
|
|
158
|
+
{!available && <p className="bp-webpanel-note">当前连接暂不能修改固定页面。</p>}
|
|
159
|
+
{!items.length && <p className="bp-webpanel-note">还没有固定页面。把常用的本地仪表盘或看板固定在这里,之后在侧栏一键打开。</p>}
|
|
160
|
+
<div className="bp-webpanel-body">
|
|
161
|
+
{items.length > 0 && <aside className="bp-webpanel-list" aria-label="已固定的页面">
|
|
162
|
+
<ul>{items.map((entry, index) => <li key={entry.id} className={entry.id === selectedId ? 'is-active' : ''}>
|
|
163
|
+
<button type="button" className="bp-webpanel-pin" aria-current={entry.id === selectedId ? 'page' : undefined}
|
|
164
|
+
disabled={busy} title={`${entry.name} · ${entry.url}`} onClick={() => select(entry.id)}>
|
|
165
|
+
<UiIcon name="board" size={15} />
|
|
166
|
+
<span className="bp-webpanel-pin-text"><span className="bp-webpanel-pin-name">{entry.name}</span>
|
|
167
|
+
<span className="bp-webpanel-pin-host">{entry.host}</span></span>
|
|
168
|
+
</button>
|
|
169
|
+
<span className="bp-webpanel-pin-tools">
|
|
170
|
+
<button type="button" title="上移" aria-label={`把 ${entry.name} 上移`} disabled={busy || !available || index === 0} onClick={() => move(entry.id, -1)}><UiIcon name="chevron" size={14} className="bp-webpanel-chevron-up" /></button>
|
|
171
|
+
<button type="button" title="下移" aria-label={`把 ${entry.name} 下移`} disabled={busy || !available || index === items.length - 1} onClick={() => move(entry.id, 1)}><UiIcon name="chevron" size={14} className="bp-webpanel-chevron-down" /></button>
|
|
172
|
+
</span>
|
|
173
|
+
</li>)}</ul>
|
|
174
|
+
</aside>}
|
|
175
|
+
<div className="bp-webpanel-stage">
|
|
176
|
+
{!panel && <p className="bp-webpanel-note">从左侧选择一个固定页面。</p>}
|
|
177
|
+
{panel && <>
|
|
178
|
+
<header className="bp-webpanel-stage-bar">
|
|
179
|
+
<strong className="bp-webpanel-stage-name" title={panel.url}>{panel.name}</strong>
|
|
180
|
+
<span className="bp-webpanel-stage-host" title={panel.url}>{panel.host}</span>
|
|
181
|
+
<span className="bp-webpanel-stage-tools">
|
|
182
|
+
<button type="button" title="重新加载" aria-label="重新加载该页面" disabled={busy} onClick={() => setReloads(value => value + 1)}><UiIcon name="refresh" size={15} /></button>
|
|
183
|
+
<button type="button" title="修改名称或网址" disabled={busy || !available} onClick={startEdit}><UiIcon name="settings" size={15} /><span>修改</span></button>
|
|
184
|
+
<button type="button" className="bp-webpanel-button-danger" title="取消固定该页面" disabled={busy || !available} onClick={() => remove(panel.id)}><UiIcon name="close" size={15} /><span>取消固定</span></button>
|
|
185
|
+
<button type="button" className="bp-webpanel-button-primary" onClick={() => { if (!openExternally(panel.url)) setError('浏览器没有打开新窗口,请检查弹出窗口设置。') }}><UiIcon name="arrow" size={15} /><span>在浏览器中打开</span></button>
|
|
186
|
+
</span>
|
|
187
|
+
</header>
|
|
188
|
+
{verdict.text && <p className="bp-webpanel-verdict" data-tone={verdict.tone} role="status">{verdict.text}</p>}
|
|
189
|
+
{refused || selfFramed ? <div className="bp-webpanel-refused">
|
|
190
|
+
<p className="bp-webpanel-refused-title">{selfFramed ? '不能显示应用自身的页面' : '该网站不允许在这里显示'}</p>
|
|
191
|
+
<p className="bp-webpanel-refused-body">{selfFramed ? '固定应用自身的页面会让它获得应用的访问权限,因此从不嵌入显示。' : '网站在自己的响应头里拒绝了被其他应用嵌入。可以在浏览器中打开,或换一个允许嵌入的页面。'}</p>
|
|
192
|
+
<button type="button" className="bp-webpanel-button-primary" onClick={() => openExternally(panel.url)}><UiIcon name="arrow" size={15} /><span>在浏览器中打开</span></button>
|
|
193
|
+
</div> : <>
|
|
194
|
+
{frame === 'failed' && <p className="bp-webpanel-note bp-webpanel-note-bad" role="status">页面迟迟没有加载完成。可以在浏览器中打开,或点“重新加载”再试。</p>}
|
|
195
|
+
<iframe key={`${panel.id}:${panel.url}:${reloads}`} className="bp-webpanel-frame" src={panel.url} title={panel.name}
|
|
196
|
+
sandbox={WEB_PANEL_SANDBOX} referrerPolicy={WEB_PANEL_REFERRER_POLICY}
|
|
197
|
+
onLoad={() => setFrame('loaded')} onError={() => setFrame('failed')} />
|
|
198
|
+
</>}
|
|
199
|
+
</>}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
{draft && <div className="bp-webpanel-editor" role="group" aria-label={draft.id ? '修改固定页面' : '新增固定页面'}>
|
|
203
|
+
<h3>{draft.id ? '修改固定页面' : '新增固定页面'}</h3>
|
|
204
|
+
<label htmlFor="bp-webpanel-draft-name">名称</label>
|
|
205
|
+
<input id="bp-webpanel-draft-name" value={draft.name} maxLength={limits?.maxNameChars ?? 40} autoComplete="off"
|
|
206
|
+
onChange={event => { setDraft({ ...draft, name: event.target.value }); setDraftError('') }} />
|
|
207
|
+
<label htmlFor="bp-webpanel-draft-url">网址</label>
|
|
208
|
+
<input id="bp-webpanel-draft-url" value={draft.url} type="url" inputMode="url" autoComplete="off" spellCheck={false}
|
|
209
|
+
placeholder="http://127.0.0.1:7841/ 或 https://…"
|
|
210
|
+
onChange={event => { setDraft({ ...draft, url: event.target.value }); setDraftError('') }}
|
|
211
|
+
onKeyDown={event => { if (event.key === 'Enter') { event.preventDefault(); void save() } }} />
|
|
212
|
+
{draftError && <p className="bp-webpanel-note bp-webpanel-note-bad" role="alert">{draftError}</p>}
|
|
213
|
+
<p className="bp-webpanel-note bp-webpanel-note-quiet">只支持 http/https 地址;保存后才会向宿主确认该页面是否允许嵌入,输入过程中不会发送任何请求。</p>
|
|
214
|
+
<div className="bp-webpanel-editor-tools">
|
|
215
|
+
<button type="button" className="bp-webpanel-button-primary" disabled={busy || !available} onClick={() => void save()}><span>保存</span></button>
|
|
216
|
+
<button type="button" className="bp-webpanel-button" onClick={() => { setDraft(null); setDraftError('') }}><span>取消</span></button>
|
|
217
|
+
</div>
|
|
218
|
+
</div>}
|
|
219
|
+
</section>
|
|
220
|
+
}
|
|
221
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { WebPanelPins } from './web-panel-page.jsx'
|
|
3
|
+
|
|
4
|
+
/** The sidebar needs only the pin directory, never the entire usage/task snapshot. */
|
|
5
|
+
export function WebPanelPinsLive({ productApi, onSelect }) {
|
|
6
|
+
const [state, setState] = useState({ api: null, panels: [] })
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
let live = true, generation = 0, controller, timer
|
|
9
|
+
const read = async () => {
|
|
10
|
+
const mine = ++generation
|
|
11
|
+
controller?.abort(); clearTimeout(timer)
|
|
12
|
+
const request = new AbortController(); controller = request
|
|
13
|
+
timer = setTimeout(() => request.abort(), 8000)
|
|
14
|
+
try {
|
|
15
|
+
const value = await productApi.dispatch('webPanels.list', {}, request.signal)
|
|
16
|
+
if (live && mine === generation && !request.signal.aborted) setState({ api: productApi, panels: Array.isArray(value?.panels) ? value.panels : [] })
|
|
17
|
+
} catch {
|
|
18
|
+
if (live && mine === generation && !request.signal.aborted) setState({ api: productApi, panels: [] })
|
|
19
|
+
} finally { if (mine === generation) clearTimeout(timer) }
|
|
20
|
+
}
|
|
21
|
+
const changed = action => { if (['webPanels.save', 'webPanels.remove', 'webPanels.order'].includes(action)) void read() }
|
|
22
|
+
const off = productApi.subscribeActions?.(changed)
|
|
23
|
+
globalThis.window?.addEventListener('focus', read)
|
|
24
|
+
void read()
|
|
25
|
+
return () => { live = false; generation++; clearTimeout(timer); controller?.abort(); off?.(); globalThis.window?.removeEventListener('focus', read) }
|
|
26
|
+
}, [productApi])
|
|
27
|
+
return <WebPanelPins panels={state.api === productApi ? state.panels : []} onSelect={onSelect}/>
|
|
28
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pinned web panel styles. Colour lives in the --bp-wp-* tokens below and follows
|
|
3
|
+
* one appearance: light is the default, the dark block only names what changes.
|
|
4
|
+
*
|
|
5
|
+
* The frame itself is deliberately left to the page it shows: this file lays out
|
|
6
|
+
* the pins, the stage bar and the refusal state, and never paints inside the
|
|
7
|
+
* iframe, which is a cross-origin document the app has no reach into.
|
|
8
|
+
*/
|
|
9
|
+
html body[data-bot-product-ui] .bp-webpanel {
|
|
10
|
+
--bp-wp-canvas: #f4f5f7;
|
|
11
|
+
--bp-wp-surface: #ffffff;
|
|
12
|
+
--bp-wp-inset: #eef0f3;
|
|
13
|
+
--bp-wp-hover: #e6e9ee;
|
|
14
|
+
--bp-wp-line: #dde1e8;
|
|
15
|
+
--bp-wp-ink: #252a33;
|
|
16
|
+
--bp-wp-muted: #5f6875;
|
|
17
|
+
--bp-wp-accent: #4a6484;
|
|
18
|
+
--bp-wp-accent-ink: #ffffff;
|
|
19
|
+
--bp-wp-ok: #2e7d32;
|
|
20
|
+
--bp-wp-bad: #b3261e;
|
|
21
|
+
--bp-wp-urgent: #8a5a12;
|
|
22
|
+
display: flex; flex-direction: column; gap: 10px; min-height: 0; color: var(--bp-wp-ink);
|
|
23
|
+
background: var(--bp-wp-canvas);
|
|
24
|
+
}
|
|
25
|
+
html body[data-bot-product-ui][data-bp-appearance="dark"] .bp-webpanel,
|
|
26
|
+
html body[data-bot-product-ui][data-ds-dark-theme]:not([data-bp-appearance="light"]) .bp-webpanel {
|
|
27
|
+
--bp-wp-canvas: #131519;
|
|
28
|
+
--bp-wp-surface: #1c1f24;
|
|
29
|
+
--bp-wp-inset: #232730;
|
|
30
|
+
--bp-wp-hover: #2a2f39;
|
|
31
|
+
--bp-wp-line: #333a45;
|
|
32
|
+
--bp-wp-ink: #e8eaee;
|
|
33
|
+
--bp-wp-muted: #aab2bd;
|
|
34
|
+
--bp-wp-accent: #a9c4e0;
|
|
35
|
+
--bp-wp-accent-ink: #131519;
|
|
36
|
+
--bp-wp-ok: #7bd48a;
|
|
37
|
+
--bp-wp-bad: #f2b8b5;
|
|
38
|
+
--bp-wp-urgent: #f6c76b;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
html body[data-bot-product-ui] .bp-webpanel-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap }
|
|
42
|
+
html body[data-bot-product-ui] .bp-webpanel-bar h2 { margin: 0; font-size: 14px; font-weight: 600 }
|
|
43
|
+
html body[data-bot-product-ui] .bp-webpanel-count { font-size: 12px; color: var(--bp-wp-muted); font-variant-numeric: tabular-nums }
|
|
44
|
+
html body[data-bot-product-ui] .bp-webpanel-bar .bp-webpanel-button { margin-left: auto }
|
|
45
|
+
|
|
46
|
+
html body[data-bot-product-ui] .bp-webpanel-button,
|
|
47
|
+
html body[data-bot-product-ui] .bp-webpanel-button-primary,
|
|
48
|
+
html body[data-bot-product-ui] .bp-webpanel-button-danger {
|
|
49
|
+
display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0 12px;
|
|
50
|
+
border: 1px solid var(--bp-wp-line); border-radius: 8px; background: var(--bp-wp-surface);
|
|
51
|
+
color: var(--bp-wp-ink); font: inherit; font-size: 13px; cursor: pointer;
|
|
52
|
+
}
|
|
53
|
+
html body[data-bot-product-ui] .bp-webpanel-button-primary { border-color: transparent; background: var(--bp-wp-accent); color: var(--bp-wp-accent-ink) }
|
|
54
|
+
html body[data-bot-product-ui] .bp-webpanel-button-danger { color: var(--bp-wp-bad) }
|
|
55
|
+
html body[data-bot-product-ui] .bp-webpanel-button:hover:not(:disabled),
|
|
56
|
+
html body[data-bot-product-ui] .bp-webpanel-button-primary:hover:not(:disabled),
|
|
57
|
+
html body[data-bot-product-ui] .bp-webpanel-button-danger:hover:not(:disabled) { background: var(--bp-wp-hover) }
|
|
58
|
+
html body[data-bot-product-ui] .bp-webpanel-button:disabled,
|
|
59
|
+
html body[data-bot-product-ui] .bp-webpanel-button-primary:disabled,
|
|
60
|
+
html body[data-bot-product-ui] .bp-webpanel-button-danger:disabled { opacity: .5; cursor: not-allowed }
|
|
61
|
+
html body[data-bot-product-ui] .bp-webpanel :focus-visible { outline: 2px solid var(--bp-wp-accent); outline-offset: 2px }
|
|
62
|
+
|
|
63
|
+
html body[data-bot-product-ui] .bp-webpanel-note { margin: 0; font-size: 12px; line-height: 1.6; color: var(--bp-wp-muted) }
|
|
64
|
+
html body[data-bot-product-ui] .bp-webpanel-note-quiet { color: var(--bp-wp-muted); opacity: .85 }
|
|
65
|
+
html body[data-bot-product-ui] .bp-webpanel-note-ok { color: var(--bp-wp-ok) }
|
|
66
|
+
html body[data-bot-product-ui] .bp-webpanel-note-bad { color: var(--bp-wp-bad) }
|
|
67
|
+
|
|
68
|
+
html body[data-bot-product-ui] .bp-webpanel-body { display: grid; grid-template-columns: minmax(180px, 240px) minmax(0, 1fr); gap: 10px; align-items: start; min-height: 0 }
|
|
69
|
+
html body[data-bot-product-ui] .bp-webpanel-list { background: var(--bp-wp-surface); border: 1px solid var(--bp-wp-line); border-radius: 10px; overflow: hidden }
|
|
70
|
+
html body[data-bot-product-ui] .bp-webpanel-list ul { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none }
|
|
71
|
+
html body[data-bot-product-ui] .bp-webpanel-list li { display: flex; align-items: center; gap: 4px; padding: 0 6px 0 0; border-bottom: 1px solid var(--bp-wp-line) }
|
|
72
|
+
html body[data-bot-product-ui] .bp-webpanel-list li:last-child { border-bottom: 0 }
|
|
73
|
+
html body[data-bot-product-ui] .bp-webpanel-list li.is-active { background: var(--bp-wp-inset) }
|
|
74
|
+
html body[data-bot-product-ui] .bp-webpanel-pin {
|
|
75
|
+
display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; min-height: 44px; padding: 6px 10px;
|
|
76
|
+
border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
|
|
77
|
+
}
|
|
78
|
+
html body[data-bot-product-ui] .bp-webpanel-pin:hover { background: var(--bp-wp-hover) }
|
|
79
|
+
html body[data-bot-product-ui] .bp-webpanel-pin[aria-current="page"] { font-weight: 600 }
|
|
80
|
+
html body[data-bot-product-ui] .bp-webpanel-pin:disabled { opacity: .55; cursor: not-allowed }
|
|
81
|
+
html body[data-bot-product-ui] .bp-webpanel-pin-text { display: flex; flex-direction: column; min-width: 0 }
|
|
82
|
+
html body[data-bot-product-ui] .bp-webpanel-pin-name { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
|
|
83
|
+
html body[data-bot-product-ui] .bp-webpanel-pin-host { font-size: 11px; color: var(--bp-wp-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
|
|
84
|
+
html body[data-bot-product-ui] .bp-webpanel-pin-tools { display: flex; gap: 2px }
|
|
85
|
+
html body[data-bot-product-ui] .bp-webpanel-pin-tools button {
|
|
86
|
+
display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px;
|
|
87
|
+
border: 0; border-radius: 6px; background: none; color: var(--bp-wp-muted); cursor: pointer;
|
|
88
|
+
}
|
|
89
|
+
html body[data-bot-product-ui] .bp-webpanel-pin-tools button:hover:not(:disabled) { background: var(--bp-wp-hover); color: var(--bp-wp-ink) }
|
|
90
|
+
html body[data-bot-product-ui] .bp-webpanel-pin-tools button:disabled { opacity: .35; cursor: not-allowed }
|
|
91
|
+
html body[data-bot-product-ui] .bp-webpanel-chevron-up { transform: rotate(180deg) }
|
|
92
|
+
html body[data-bot-product-ui] .bp-webpanel-chevron-down { transform: rotate(90deg) }
|
|
93
|
+
|
|
94
|
+
html body[data-bot-product-ui] .bp-webpanel-stage { display: flex; flex-direction: column; gap: 8px; min-width: 0; min-height: 0 }
|
|
95
|
+
html body[data-bot-product-ui] .bp-webpanel-stage-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 8px 10px; background: var(--bp-wp-surface); border: 1px solid var(--bp-wp-line); border-radius: 10px }
|
|
96
|
+
html body[data-bot-product-ui] .bp-webpanel-stage-name { font-size: 13px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
97
|
+
html body[data-bot-product-ui] .bp-webpanel-stage-host { font-size: 11px; color: var(--bp-wp-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
98
|
+
html body[data-bot-product-ui] .bp-webpanel-stage-tools { display: flex; align-items: center; gap: 6px; margin-left: auto; flex-wrap: wrap }
|
|
99
|
+
html body[data-bot-product-ui] .bp-webpanel-stage-tools button { min-height: 30px; padding: 0 8px; font-size: 12px }
|
|
100
|
+
html body[data-bot-product-ui] .bp-webpanel-stage-tools button:first-child { width: 30px; padding: 0; justify-content: center }
|
|
101
|
+
|
|
102
|
+
html body[data-bot-product-ui] .bp-webpanel-verdict { margin: 0; font-size: 12px; line-height: 1.5; color: var(--bp-wp-muted) }
|
|
103
|
+
html body[data-bot-product-ui] .bp-webpanel-verdict[data-tone="blocked"] { color: var(--bp-wp-urgent) }
|
|
104
|
+
html body[data-bot-product-ui] .bp-webpanel-verdict[data-tone="ok"] { color: var(--bp-wp-ok) }
|
|
105
|
+
html body[data-bot-product-ui] .bp-webpanel-verdict[data-tone="unknown"] { color: var(--bp-wp-muted) }
|
|
106
|
+
|
|
107
|
+
html body[data-bot-product-ui] .bp-webpanel-refused,
|
|
108
|
+
html body[data-bot-product-ui] .bp-webpanel-editor {
|
|
109
|
+
display: flex; flex-direction: column; gap: 8px; padding: 16px; background: var(--bp-wp-surface);
|
|
110
|
+
border: 1px solid var(--bp-wp-line); border-radius: 10px;
|
|
111
|
+
}
|
|
112
|
+
html body[data-bot-product-ui] .bp-webpanel-refused { align-items: flex-start }
|
|
113
|
+
html body[data-bot-product-ui] .bp-webpanel-refused-title { margin: 0; font-size: 14px; font-weight: 600 }
|
|
114
|
+
html body[data-bot-product-ui] .bp-webpanel-refused-body { margin: 0; font-size: 12px; line-height: 1.6; color: var(--bp-wp-muted); max-width: 46em }
|
|
115
|
+
|
|
116
|
+
html body[data-bot-product-ui] .bp-webpanel-frame { width: 100%; min-height: 60vh; flex: 1; border: 1px solid var(--bp-wp-line); border-radius: 10px; background: var(--bp-wp-surface) }
|
|
117
|
+
|
|
118
|
+
html body[data-bot-product-ui] .bp-webpanel-editor h3 { margin: 0; font-size: 13px; font-weight: 600 }
|
|
119
|
+
html body[data-bot-product-ui] .bp-webpanel-editor label { font-size: 12px; color: var(--bp-wp-muted) }
|
|
120
|
+
html body[data-bot-product-ui] .bp-webpanel-editor input {
|
|
121
|
+
min-height: 34px; padding: 0 10px; border: 1px solid var(--bp-wp-line); border-radius: 8px;
|
|
122
|
+
background: var(--bp-wp-inset); color: var(--bp-wp-ink); font: inherit; font-size: 13px;
|
|
123
|
+
}
|
|
124
|
+
html body[data-bot-product-ui] .bp-webpanel-editor-tools { display: flex; gap: 8px }
|
|
125
|
+
|
|
126
|
+
/* The sidebar's compact pin list: one row per page, no tools, nothing to mis-tap. */
|
|
127
|
+
html body[data-bot-product-ui] .bp-webpanel-pins { display: flex; flex-direction: column; gap: 2px; margin-top: auto }
|
|
128
|
+
html body[data-bot-product-ui] .bp-webpanel-pins-label { margin: 0 0 2px; padding: 0 12px; font-size: 11px; color: var(--bp-muted, #76767e) }
|
|
129
|
+
html body[data-bot-product-ui] .bp-webpanel-pins ul { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; list-style: none }
|
|
130
|
+
html body[data-bot-product-ui] .bp-webpanel-pins .bp-webpanel-pin { min-height: 34px; padding: 4px 12px; border-radius: 8px }
|
|
131
|
+
html body[data-bot-product-ui] .bp-webpanel-pins .bp-webpanel-pin[aria-current="page"] { background: var(--bp-hover, #ededf0) }
|
|
132
|
+
|
|
133
|
+
@media (max-width: 900px) {
|
|
134
|
+
html body[data-bot-product-ui] .bp-webpanel-body { grid-template-columns: minmax(0, 1fr) }
|
|
135
|
+
html body[data-bot-product-ui] .bp-webpanel-list ul { flex-direction: row; overflow-x: auto }
|
|
136
|
+
html body[data-bot-product-ui] .bp-webpanel-list li { border-bottom: 0; border-right: 1px solid var(--bp-wp-line) }
|
|
137
|
+
html body[data-bot-product-ui] .bp-webpanel-list li:last-child { border-right: 0 }
|
|
138
|
+
html body[data-bot-product-ui] .bp-webpanel-frame { min-height: 70vh }
|
|
139
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { modelName, TASK_STATES } from './api.mjs'
|
|
2
|
+
import { taskProgress, taskIdOf, taskKey, taskTitle } from './task-progress.mjs'
|
|
3
|
+
import { WORKSPACE_SECTIONS } from './workspace-navigation.mjs'
|
|
4
|
+
|
|
5
|
+
const timestamp = value => value !== null && value !== undefined && value !== '' && Number.isFinite(new Date(value).getTime()) ? new Date(value).getTime() : null
|
|
6
|
+
const summary = (value, length = 140) => String(value ?? '').replace(/\s+/g, ' ').trim().slice(0, length)
|
|
7
|
+
|
|
8
|
+
export function workspaceOverview(snapshot) {
|
|
9
|
+
const progress = taskProgress(snapshot)
|
|
10
|
+
const bots = (snapshot?.bots ?? []).map(bot => {
|
|
11
|
+
const work = progress.bots.find(row => row.botId === bot.id)
|
|
12
|
+
return { ...bot, work, state: work?.attention ? 'attention' : work?.active ? 'active' : 'idle' }
|
|
13
|
+
})
|
|
14
|
+
const machines = snapshot?.machines?.status === 'available' ? (snapshot.machines.items ?? []).filter(row => row.status !== 'revoked') : []
|
|
15
|
+
const recent = [...progress.items].sort((a, b) => (timestamp(b.updatedAt ?? b.createdAt) ?? -1) - (timestamp(a.updatedAt ?? a.createdAt) ?? -1)).slice(0, 6)
|
|
16
|
+
return { progress, bots, machines, recent, onlineMachines: machines.filter(row => row.status === 'online').length,
|
|
17
|
+
workingBots: bots.filter(bot => bot.state === 'active').length,
|
|
18
|
+
tasksAvailable: snapshot?.tasks?.status === 'available', tasksTruncated: snapshot?.tasks?.truncated === true,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function taskBoard(progress, rows = progress.items) {
|
|
23
|
+
const lanes = [
|
|
24
|
+
{ id: 'queued', title: '待开始', icon: 'clock', items: [] },
|
|
25
|
+
{ id: 'running', title: '进行中', icon: 'activity', items: [] },
|
|
26
|
+
{ id: 'attention', title: '需要处理', icon: 'inbox', items: [] },
|
|
27
|
+
{ id: 'finished', title: '已结束', icon: 'check', items: [] },
|
|
28
|
+
{ id: 'other', title: '其他状态', icon: 'tasks', items: [] },
|
|
29
|
+
]
|
|
30
|
+
const byId = new Map(lanes.map(lane => [lane.id, lane]))
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const category = progress.category(row)
|
|
33
|
+
const id = category === 'attention' ? 'attention' : category === 'finished' ? 'finished'
|
|
34
|
+
: ['running', 'cancelling'].includes(row.state) ? 'running' : ['queued', 'ready', 'admitting'].includes(row.state) ? 'queued' : 'other'
|
|
35
|
+
byId.get(id).items.push(row)
|
|
36
|
+
}
|
|
37
|
+
return lanes.filter(lane => lane.id !== 'other' || lane.items.length > 0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Search only the current committed projection; no transcripts or credentials enter this index. */
|
|
41
|
+
export function workspaceSearch(snapshot, query = '', limit = 60) {
|
|
42
|
+
const terms = query.trim().toLocaleLowerCase().split(/\s+/u).filter(Boolean)
|
|
43
|
+
const rows = WORKSPACE_SECTIONS.map(row => ({ id: `page:${row.section}`, kind: 'page', label: row.label, detail: row.description,
|
|
44
|
+
icon: row.icon, target: { section: row.section }, keywords: row.title }))
|
|
45
|
+
for (const bot of snapshot?.bots ?? []) {
|
|
46
|
+
rows.push({ id: `bot:${bot.id}`, kind: 'bot', label: summary(bot.name), detail: summary(bot.role || modelName(bot.model)), icon: 'bot',
|
|
47
|
+
target: { section: 'bindings', botId: bot.id }, sessionId: bot.sessionId, keywords: `${bot.id} ${modelName(bot.model)}` })
|
|
48
|
+
}
|
|
49
|
+
for (const task of taskProgress(snapshot).items) {
|
|
50
|
+
const bot = snapshot?.bots?.find(row => row.id === task.botId)
|
|
51
|
+
rows.push({ id: `task:${taskKey(task)}`, kind: 'task', label: summary(taskTitle(task)), detail: `${bot?.name ?? task.botId ?? '未取得归属'} · ${TASK_STATES[task.state] ?? task.state}`,
|
|
52
|
+
icon: 'tasks', target: { section: 'tasks', botId: task.botId, taskId: taskIdOf(task) },
|
|
53
|
+
keywords: [taskIdOf(task), task.operationId, task.attemptId, task.planId].filter(Boolean).join(' ') })
|
|
54
|
+
}
|
|
55
|
+
if (snapshot?.machines?.status === 'available') for (const machine of snapshot.machines.items ?? []) {
|
|
56
|
+
if (machine.status === 'revoked') continue
|
|
57
|
+
rows.push({ id: `machine:${machine.id}`, kind: 'machine', label: summary(machine.name || machine.id), detail: '电脑', icon: 'computer',
|
|
58
|
+
target: { section: 'machines', machineId: machine.id }, keywords: machine.id })
|
|
59
|
+
}
|
|
60
|
+
const matches = rows.map((row, index) => {
|
|
61
|
+
const label = row.label.toLocaleLowerCase(), haystack = `${label} ${row.detail} ${row.keywords ?? ''}`.toLocaleLowerCase()
|
|
62
|
+
if (!terms.every(term => haystack.includes(term))) return null
|
|
63
|
+
return { row, rank: terms.length ? terms.every(term => label.startsWith(term)) ? 0 : terms.every(term => label.includes(term)) ? 1 : 2 : 0, index }
|
|
64
|
+
}).filter(Boolean).sort((a, b) => a.rank - b.rank || a.index - b.index)
|
|
65
|
+
const count = Math.max(1, Math.min(120, Number.isFinite(limit) ? Math.floor(limit) : 60))
|
|
66
|
+
return { items: matches.slice(0, count).map(match => match.row), total: matches.length, truncated: matches.length > count }
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const WORKSPACE_SECTIONS = Object.freeze([
|
|
2
|
+
{ section: 'home', title: '工作空间', label: '总览', icon: 'home', group: '工作', description: '从一个目标开始,和你的 Bot 团队一起把事情做好。' },
|
|
3
|
+
{ section: 'inbox', title: '需要你处理', label: '任务待办', icon: 'inbox', group: '工作', description: '集中查看需要确认、补充条件或核对结果的任务。' },
|
|
4
|
+
{ section: 'notifications', title: '通知中心', label: '通知', icon: 'bell', group: '工作', description: '查看 Bot 的任务进展,回到对应的执行记录。' },
|
|
5
|
+
{ section: 'threads', title: '会话线程', label: '线程', icon: 'list', group: '工作', description: '在同一个 Bot 下建立多条独立会话,命名、切换与归档互不串上下文。' },
|
|
6
|
+
{ section: 'tasks', title: '团队任务', label: '任务', icon: 'tasks', group: '工作', description: '从目标到结果,用列表或看板跟进每一项工作。' },
|
|
7
|
+
{ section: 'bindings', title: '我的团队', label: '团队', icon: 'team', group: '资源', description: '认识每位伙伴的职责,配置模型、工具和工作空间。' },
|
|
8
|
+
{ section: 'machines', title: '电脑', label: '远程电脑', icon: 'computer', group: '资源', description: '连接你的电脑,查看工作现场,或随时接过控制权。' },
|
|
9
|
+
{ section: 'library', title: '资料库', label: '资料库', icon: 'folder', group: '资源', description: '把文件和知识交给合适的 Bot,让协作有据可循。' },
|
|
10
|
+
{ section: 'data', title: '数据', label: '数据与图表', icon: 'chart', group: '资源', description: '把资料变成可查询的数据,制作图表并导出结果。' },
|
|
11
|
+
{ section: 'routine', title: '例行工作', label: '例行任务', icon: 'repeat', group: '资源', description: '演示、保存、再次运行,让重复的工作少一点。' },
|
|
12
|
+
{ section: 'usage', title: '费用与用量', label: '费用与用量', icon: 'chart', group: '管理', description: '用真实收据了解团队使用情况和每笔费用。' },
|
|
13
|
+
{ section: 'webPanel', title: '常用网页', label: '网页面板', icon: 'board', group: '工作', description: '把常用的本地仪表盘或看板固定在这里,一键打开;列表保存在宿主,重启后仍在。' },
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
export const PRODUCT_PANELS = Object.freeze(Object.fromEntries(WORKSPACE_SECTIONS.map(({ section }) => [section, `bot-product-${section}`])))
|
|
17
|
+
export const ADVANCED_PANEL = 'model-router-galgame'
|
|
18
|
+
const scopes = new WeakMap()
|
|
19
|
+
const fallbackScope = {}
|
|
20
|
+
const identity = value => typeof value === 'string' && value.length > 0 && value.length <= 512 ? value : undefined
|
|
21
|
+
|
|
22
|
+
/** UI focus belongs to one Host API. These addresses confer no execution authority. */
|
|
23
|
+
export function workspaceNavigation(scope) {
|
|
24
|
+
const key = scope && ['object', 'function'].includes(typeof scope) ? scope : fallbackScope
|
|
25
|
+
if (scopes.has(key)) return scopes.get(key)
|
|
26
|
+
const listeners = new Set()
|
|
27
|
+
let snapshot = null, version = 0, claimedDraft = 0, goalDraft = ''
|
|
28
|
+
const navigation = Object.freeze({
|
|
29
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
|
|
30
|
+
getSnapshot: () => snapshot,
|
|
31
|
+
getGoalDraft: () => goalDraft,
|
|
32
|
+
saveGoalDraft(text) { goalDraft = typeof text === 'string' ? text.slice(0, 20_000) : '' },
|
|
33
|
+
navigate(destination) {
|
|
34
|
+
if (!Object.hasOwn(PRODUCT_PANELS, destination?.section)) throw new TypeError('Unknown workspace section')
|
|
35
|
+
snapshot = Object.freeze({
|
|
36
|
+
version: ++version, section: destination.section,
|
|
37
|
+
botId: identity(destination.botId), taskId: identity(destination.taskId), machineId: identity(destination.machineId),
|
|
38
|
+
panelId: identity(destination.panelId),
|
|
39
|
+
attemptId: identity(destination.attemptId),
|
|
40
|
+
filter: ['all', 'active', 'attention', 'finished'].includes(destination.filter) ? destination.filter : undefined,
|
|
41
|
+
compose: destination.compose === true,
|
|
42
|
+
search: destination.search === true,
|
|
43
|
+
draft: typeof destination.draft === 'string' ? destination.draft.slice(0, 20_000) : undefined,
|
|
44
|
+
})
|
|
45
|
+
for (const listener of listeners) listener()
|
|
46
|
+
return snapshot
|
|
47
|
+
},
|
|
48
|
+
claimDraft(requestVersion) {
|
|
49
|
+
if (!snapshot || snapshot.version !== requestVersion || claimedDraft === requestVersion || typeof snapshot.draft !== 'string') return null
|
|
50
|
+
claimedDraft = requestVersion
|
|
51
|
+
return snapshot.draft
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
scopes.set(key, navigation)
|
|
55
|
+
return navigation
|
|
56
|
+
}
|