@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// Native tool activity: the honest observation surface.
|
|
2
|
+
//
|
|
3
|
+
// The Host records what a native engine actually did — a tool started, a tool
|
|
4
|
+
// returned, a turn closed, a stop was requested, a stop was confirmed. This
|
|
5
|
+
// module turns those observations into display records and nothing else:
|
|
6
|
+
// it never sends, stops, retries, settles or re-runs anything, and it has no
|
|
7
|
+
// access to a tool's parameters or its result, so it never invents one.
|
|
8
|
+
//
|
|
9
|
+
// The controller is read-only and epoch-guarded. One scope (API instance, Bot,
|
|
10
|
+
// mode, session/context) owns one controller; a reply that does not answer that
|
|
11
|
+
// scope is discarded instead of displayed, and retiring a controller aborts its
|
|
12
|
+
// in-flight read and stops its timer.
|
|
13
|
+
|
|
14
|
+
const failure = (code, message = code) => Object.assign(new Error(message), { code })
|
|
15
|
+
const BOT = /^[-A-Za-z0-9_.]{1,128}$/
|
|
16
|
+
// Session, context and call keys are Host-minted and opaque, so they are bounded
|
|
17
|
+
// by length and shape only: a format this module invents would refuse real records.
|
|
18
|
+
const ID = /^[^\s\u0000-\u001f\u007f]{1,128}$/
|
|
19
|
+
const KEY = /^[^\s\u0000-\u001f\u007f]{1,256}$/
|
|
20
|
+
const CONTROL = /[\u0000-\u001f\u007f]/
|
|
21
|
+
const OUTCOMES = new Set(['succeeded', 'failed', 'cancelled', 'unknown', 'refused', 'host-closed', 'host-lost'])
|
|
22
|
+
/** The owner action this surface is allowed to call. Nothing else exists here. */
|
|
23
|
+
export const NATIVE_ACTIVITY_ACTION = 'nativeActivity.list'
|
|
24
|
+
export const NATIVE_ACTIVITY_LIMIT_MAX = 50
|
|
25
|
+
export const NATIVE_ACTIVITY_LIMIT_DEFAULT = 20
|
|
26
|
+
/** A loaded page never grows without bound: older rows stop loading, they are never dropped silently. */
|
|
27
|
+
const LOADED_MAX = 200
|
|
28
|
+
const REQUEST_MS = 8000
|
|
29
|
+
|
|
30
|
+
/** The four states the interface is allowed to show. Nothing else is invented. */
|
|
31
|
+
export const NATIVE_ACTIVITY_STATES = Object.freeze(['running', 'cli-ok', 'cli-error', 'turn-unconfirmed'])
|
|
32
|
+
export const NATIVE_ACTIVITY_LABELS = Object.freeze({
|
|
33
|
+
running: '进行中',
|
|
34
|
+
'cli-ok': '已返回',
|
|
35
|
+
'cli-error': '返回错误',
|
|
36
|
+
'turn-unconfirmed': '待核对',
|
|
37
|
+
})
|
|
38
|
+
const OUTCOME_WORDS = Object.freeze({
|
|
39
|
+
succeeded: '已成功返回',
|
|
40
|
+
failed: '返回失败',
|
|
41
|
+
cancelled: '已取消',
|
|
42
|
+
unknown: '结果未知',
|
|
43
|
+
refused: '被拒绝',
|
|
44
|
+
'host-closed': '宿主已关闭',
|
|
45
|
+
'host-lost': '宿主未保留结果',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Friendly names for the tools a native engine reports. A name the Host did not
|
|
49
|
+
// report stays unreported: the row says "工具调用" and the expanded detail says
|
|
50
|
+
// the name is missing, rather than guessing one.
|
|
51
|
+
const TOOL_WORDS = Object.freeze([
|
|
52
|
+
{ match: /^(?:read|readfile|fileread|view)$/i, label: '读取文件', icon: 'file' },
|
|
53
|
+
{ match: /^(?:write|createfile|filewrite)$/i, label: '写入文件', icon: 'file' },
|
|
54
|
+
{ match: /^(?:edit|multiedit|notebookedit|replace)$/i, label: '修改文件', icon: 'file' },
|
|
55
|
+
{ match: /^(?:bash|shell|command|run|exec|terminal)$/i, label: '运行命令', icon: 'computer' },
|
|
56
|
+
{ match: /^(?:glob|ls|list|find)$/i, label: '查找文件', icon: 'search' },
|
|
57
|
+
{ match: /^(?:grep|search|ripgrep)$/i, label: '搜索内容', icon: 'search' },
|
|
58
|
+
{ match: /^(?:task|agent|subagent)$/i, label: '安排子任务', icon: 'team' },
|
|
59
|
+
{ match: /^(?:todowrite|todos|plan)$/i, label: '更新任务清单', icon: 'tasks' },
|
|
60
|
+
{ match: /^(?:webfetch|fetch|download)$/i, label: '读取网页', icon: 'arrow' },
|
|
61
|
+
{ match: /^(?:websearch|searchweb)$/i, label: '搜索网页', icon: 'search' },
|
|
62
|
+
{ match: /^(?:lsp|diagnostics|typecheck)$/i, label: '检查代码', icon: 'check' },
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
/** The label and icon one record shows, using only what the Host reported. */
|
|
66
|
+
export function nativeActivityTool(record) {
|
|
67
|
+
const raw = record?.toolNameRecorded !== false && typeof record?.toolName === 'string' ? record.toolName.trim() : ''
|
|
68
|
+
const name = raw.startsWith('mcp__') ? raw.split('__').at(-1) : raw
|
|
69
|
+
const family = {
|
|
70
|
+
data_load: ['导入数据', 'chart'], data_describe: ['查看数据结构', 'chart'], data_sql: ['查询数据', 'chart'],
|
|
71
|
+
data_show: ['展示数据', 'chart'], data_export: ['导出数据', 'chart'],
|
|
72
|
+
bot_file_read: ['读取文件', 'file'], bot_file_write: ['写入文件', 'file'],
|
|
73
|
+
browser_open: ['打开网页', 'computer'], browser_screenshot: ['网页截图', 'computer'],
|
|
74
|
+
desktop_screenshot: ['查看桌面', 'computer'], coordinate_bots: ['安排队友工作', 'team'],
|
|
75
|
+
}[name]
|
|
76
|
+
if (family) return { label: family[0], icon: family[1], reported: raw }
|
|
77
|
+
const known = TOOL_WORDS.find(row => row.match.test(name))
|
|
78
|
+
if (known) return { label: known.label, icon: known.icon, reported: raw }
|
|
79
|
+
return { label: raw === '' ? '工具调用' : raw, icon: 'bot', reported: raw }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The honest state of one observation.
|
|
84
|
+
*
|
|
85
|
+
* A CLI return is not an operation result: `cli-ok` only means the tool answered.
|
|
86
|
+
* An unknown outcome stays unknown even when a late CLI return arrived, so a late
|
|
87
|
+
* return can never paint an uncertain row green.
|
|
88
|
+
*/
|
|
89
|
+
export function nativeActivityState(record = {}) {
|
|
90
|
+
return NATIVE_ACTIVITY_STATES.includes(record?.state) ? record.state : 'turn-unconfirmed'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A bounded, readable payload for exactly one scope. */
|
|
94
|
+
export function nativeActivityScope({ botId, mode, sessionId, contextId } = {}) {
|
|
95
|
+
const hasSession = typeof sessionId === 'string' && sessionId !== ''
|
|
96
|
+
const hasContext = typeof contextId === 'string' && contextId !== ''
|
|
97
|
+
if (mode === 'integration') {
|
|
98
|
+
if (!BOT.test(botId ?? '') || !hasSession || !KEY.test(sessionId) || hasContext) throw failure('NATIVE_ACTIVITY_SCOPE_INVALID', '集成会话缺少有效的 Bot 或会话标识。')
|
|
99
|
+
return { botId, mode: 'integration', sessionId, contextId: null }
|
|
100
|
+
}
|
|
101
|
+
if (mode === 'independent') {
|
|
102
|
+
if (!BOT.test(botId ?? '') || !hasContext || !ID.test(contextId) || hasSession) throw failure('NATIVE_ACTIVITY_SCOPE_INVALID', '独立会话缺少有效的 Bot 或对话标识。')
|
|
103
|
+
return { botId, mode: 'independent', sessionId: null, contextId }
|
|
104
|
+
}
|
|
105
|
+
throw failure('NATIVE_ACTIVITY_SCOPE_INVALID', '工具记录读取模式无效。')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The stable identity of one scope: a change here is an epoch boundary. */
|
|
109
|
+
export function nativeActivityScopeKey({ botId, mode, sessionId, contextId } = {}) {
|
|
110
|
+
const scope = nativeActivityScope({ botId, mode, sessionId, contextId })
|
|
111
|
+
return `${scope.mode}:${scope.botId}:${scope.sessionId ?? ''}:${scope.contextId ?? ''}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const payloadOf = (scope, cursor, limit) => ({
|
|
115
|
+
botId: scope.botId,
|
|
116
|
+
mode: scope.mode,
|
|
117
|
+
...(scope.mode === 'integration' ? { sessionId: scope.sessionId } : { contextId: scope.contextId }),
|
|
118
|
+
limit,
|
|
119
|
+
...(cursor ? { cursor } : {}),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const text = (value, max) => typeof value === 'string' && value !== '' && value.length <= max && !CONTROL.test(value) ? value : null
|
|
123
|
+
const count = value => Number.isSafeInteger(value) && value >= 0 ? value : null
|
|
124
|
+
|
|
125
|
+
/** One display record. A field the Host did not send stays null; nothing is filled in. */
|
|
126
|
+
function readRecord(value, scope) {
|
|
127
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.schemaVersion !== 1) return null
|
|
128
|
+
const callKey = text(value.callKey, 128), callId = text(value.callId, 256)
|
|
129
|
+
if (callKey === null || !KEY.test(callKey) || callId === null || !KEY.test(callId)
|
|
130
|
+
|| text(value.turnId, 128) === null || text(value.processGeneration, 128) === null
|
|
131
|
+
|| !NATIVE_ACTIVITY_STATES.includes(value.state) || count(value.startedAt) === null || count(value.updatedAt) === null
|
|
132
|
+
|| value.endedAt !== null && count(value.endedAt) === null
|
|
133
|
+
|| value.cliOk !== null && typeof value.cliOk !== 'boolean'
|
|
134
|
+
|| value.turnOutcome !== null && !OUTCOMES.has(value.turnOutcome)) return null
|
|
135
|
+
if (['toolNameRecorded', 'turnSettled', 'cancelRequested', 'stopConfirmed', 'lateCliReturn'].some(key => typeof value[key] !== 'boolean')
|
|
136
|
+
|| value.state === 'cli-ok' && value.cliOk !== true || value.state === 'cli-error' && value.cliOk !== false
|
|
137
|
+
|| ['running', 'turn-unconfirmed'].includes(value.state) && value.cliOk !== null) return null
|
|
138
|
+
if (value.botId !== scope.botId || value.mode !== scope.mode || value.conversationId !== scope.conversationId
|
|
139
|
+
|| value.contextId !== scope.contextId) throw failure('NATIVE_ACTIVITY_SCOPE_MISMATCH', '工具记录属于其他会话。')
|
|
140
|
+
const record = {
|
|
141
|
+
schemaVersion: 1, callKey, callId,
|
|
142
|
+
botId: value.botId, mode: value.mode, conversationId: value.conversationId, contextId: value.contextId,
|
|
143
|
+
processGeneration: value.processGeneration, turnId: value.turnId,
|
|
144
|
+
toolName: value.toolNameRecorded === false ? null : text(value.toolName, 128),
|
|
145
|
+
toolNameRecorded: value.toolNameRecorded === true,
|
|
146
|
+
state: value.state, cliOk: value.cliOk, turnOutcome: value.turnOutcome, turnSettled: value.turnSettled === true,
|
|
147
|
+
startedAt: value.startedAt, endedAt: value.endedAt, updatedAt: value.updatedAt,
|
|
148
|
+
cancelRequested: value.cancelRequested === true,
|
|
149
|
+
stopConfirmed: value.stopConfirmed === true,
|
|
150
|
+
lateCliReturn: value.lateCliReturn === true,
|
|
151
|
+
lateCliOk: value.lateCliOk === true ? true : value.lateCliOk === false ? false : null,
|
|
152
|
+
duplicateStarts: count(value.duplicateStarts) ?? 0, duplicateCompletions: count(value.duplicateCompletions) ?? 0,
|
|
153
|
+
}
|
|
154
|
+
return record
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A closed page for exactly this scope. A page that cannot be read completely is
|
|
159
|
+
* refused instead of shown partially, so a gap can never look like finished work.
|
|
160
|
+
*/
|
|
161
|
+
export function readNativeActivityPage(value, scope) {
|
|
162
|
+
if (!value || typeof value !== 'object' || value.schemaVersion !== 1) throw failure('NATIVE_ACTIVITY_INVALID', '宿主返回的工具记录格式无效。')
|
|
163
|
+
const reply = value.scope
|
|
164
|
+
if (!reply || typeof reply !== 'object' || reply.botId !== scope.botId || reply.mode !== scope.mode) throw failure('NATIVE_ACTIVITY_SCOPE_MISMATCH', '宿主回答的不是当前会话。')
|
|
165
|
+
if (scope.mode === 'integration') {
|
|
166
|
+
if (reply.sessionId !== scope.sessionId || text(reply.conversationId, 128) === null || reply.contextId !== null) throw failure('NATIVE_ACTIVITY_SCOPE_MISMATCH', '宿主回答的不是当前集成会话。')
|
|
167
|
+
} else if (reply.sessionId !== null || reply.contextId !== scope.contextId || reply.conversationId !== scope.contextId) throw failure('NATIVE_ACTIVITY_SCOPE_MISMATCH', '宿主回答的不是当前对话。')
|
|
168
|
+
if (!Array.isArray(value.items) || value.items.length > NATIVE_ACTIVITY_LIMIT_MAX) throw failure('NATIVE_ACTIVITY_INVALID', '宿主返回的工具记录数量无效。')
|
|
169
|
+
const items = []
|
|
170
|
+
for (const row of value.items) {
|
|
171
|
+
const record = readRecord(row, reply)
|
|
172
|
+
if (record === null) throw failure('NATIVE_ACTIVITY_INVALID', '宿主返回的工具记录缺少稳定编号。')
|
|
173
|
+
items.push(record)
|
|
174
|
+
}
|
|
175
|
+
if (new Set(items.map(row => row.callKey)).size !== items.length) throw failure('NATIVE_ACTIVITY_INVALID', '宿主返回的工具记录编号重复。')
|
|
176
|
+
if (typeof value.nativeRoute !== 'boolean') throw failure('NATIVE_ACTIVITY_INVALID', '宿主没有说明这一会话是否记录工具活动。')
|
|
177
|
+
const recording = value.recording
|
|
178
|
+
if (!recording || typeof recording !== 'object' || !['ready', 'degraded'].includes(recording.status)
|
|
179
|
+
|| count(recording.failures) === null) throw failure('NATIVE_ACTIVITY_INVALID', '宿主没有说明工具记录写入状态。')
|
|
180
|
+
const nextCursor = value.nextCursor === null ? null : text(value.nextCursor, 4096)
|
|
181
|
+
if (value.nextCursor !== null && nextCursor === null) throw failure('NATIVE_ACTIVITY_INVALID', '宿主返回的记录页码无效。')
|
|
182
|
+
return {
|
|
183
|
+
conversationId: text(reply.conversationId, 128),
|
|
184
|
+
contextId: scope.mode === 'independent' ? scope.contextId : null,
|
|
185
|
+
items,
|
|
186
|
+
nextCursor,
|
|
187
|
+
nativeRoute: value.nativeRoute,
|
|
188
|
+
recording: { status: recording.status, failures: count(recording.failures), code: text(recording.code, 64) },
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Newest first, then by call key, exactly the order the Host commits to. The
|
|
193
|
+
// order is display only: every record keeps the identity it arrived with.
|
|
194
|
+
const compareRecords = (a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0)
|
|
195
|
+
|| (a.callKey < b.callKey ? 1 : a.callKey > b.callKey ? -1 : 0)
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The reported result is stable, while later cancellation, stop and settlement
|
|
199
|
+
* facts still arrive. Unknown never becomes green, and an older page cannot
|
|
200
|
+
* overwrite a newer observation.
|
|
201
|
+
*/
|
|
202
|
+
const mergeRecord = (held, next) => {
|
|
203
|
+
for (const key of ['botId', 'mode', 'conversationId', 'contextId', 'processGeneration', 'turnId', 'callId', 'startedAt']) {
|
|
204
|
+
if (held[key] !== next[key]) throw failure('NATIVE_ACTIVITY_SCOPE_MISMATCH', '工具记录的原始身份发生变化。')
|
|
205
|
+
}
|
|
206
|
+
return { ...next,
|
|
207
|
+
state: held.state === 'running' ? next.state : held.state,
|
|
208
|
+
cliOk: held.state === 'running' ? next.cliOk : held.cliOk,
|
|
209
|
+
toolName: held.toolNameRecorded ? held.toolName : next.toolName,
|
|
210
|
+
toolNameRecorded: held.toolNameRecorded || next.toolNameRecorded,
|
|
211
|
+
endedAt: held.endedAt ?? next.endedAt,
|
|
212
|
+
turnOutcome: held.turnSettled ? held.turnOutcome : next.turnOutcome,
|
|
213
|
+
turnSettled: held.turnSettled || next.turnSettled,
|
|
214
|
+
cancelRequested: held.cancelRequested || next.cancelRequested,
|
|
215
|
+
stopConfirmed: held.stopConfirmed || next.stopConfirmed,
|
|
216
|
+
lateCliReturn: held.lateCliReturn || next.lateCliReturn,
|
|
217
|
+
lateCliOk: held.lateCliReturn ? held.lateCliOk : next.lateCliOk,
|
|
218
|
+
duplicateStarts: Math.max(held.duplicateStarts, next.duplicateStarts),
|
|
219
|
+
duplicateCompletions: Math.max(held.duplicateCompletions, next.duplicateCompletions),
|
|
220
|
+
updatedAt: Math.max(held.updatedAt, next.updatedAt),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function mergeRecords(previous, incoming) {
|
|
225
|
+
const byKey = new Map(previous.map(row => [row.callKey, row]))
|
|
226
|
+
for (const row of incoming) {
|
|
227
|
+
const held = byKey.get(row.callKey)
|
|
228
|
+
byKey.set(row.callKey, held === undefined ? row : mergeRecord(held, row))
|
|
229
|
+
}
|
|
230
|
+
const items = [...byKey.values()].sort(compareRecords)
|
|
231
|
+
return { items: items.length > LOADED_MAX ? items.slice(0, LOADED_MAX) : items, truncated: items.length > LOADED_MAX }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The view crop. Paging reads the original records; this only chooses how many are shown. */
|
|
235
|
+
export const nativeActivityLatest = (items, count = 4) => items.slice(0, count)
|
|
236
|
+
|
|
237
|
+
/** What the compact surface says, using only loaded records. */
|
|
238
|
+
export function nativeActivitySummary(items) {
|
|
239
|
+
const rows = Array.isArray(items) ? items : []
|
|
240
|
+
const running = rows.find(row => row.state === 'running')
|
|
241
|
+
if (running) {
|
|
242
|
+
const tool = nativeActivityTool(running)
|
|
243
|
+
return { state: 'running', text: `正在进行:${tool.label}`, icon: tool.icon, record: running }
|
|
244
|
+
}
|
|
245
|
+
const first = rows[0]
|
|
246
|
+
if (!first) return null
|
|
247
|
+
const tool = nativeActivityTool(first)
|
|
248
|
+
return { state: first.state, text: `最近:${tool.label} · ${NATIVE_ACTIVITY_LABELS[first.state]}`, icon: tool.icon, record: first }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const nativeActivityOutcomeWord = outcome => (outcome && OUTCOME_WORDS[outcome]) || '宿主未记录结果'
|
|
252
|
+
|
|
253
|
+
/** A read failure in the owner's words. Raw transport detail never reaches the screen. */
|
|
254
|
+
export function nativeActivityNote(error) {
|
|
255
|
+
switch (error?.code) {
|
|
256
|
+
case 'NATIVE_ACTIVITY_SCOPE_INVALID': return '当前会话的工具记录范围无效,已停止读取。'
|
|
257
|
+
case 'NATIVE_ACTIVITY_SCOPE_MISMATCH': return '宿主切换了这一会话的工具记录范围,已停止显示之前的内容。'
|
|
258
|
+
case 'NATIVE_ACTIVITY_INVALID': return '宿主返回的工具记录无法核对,这一页已拒绝显示。'
|
|
259
|
+
case 'NATIVE_ACTIVITY_ABORTED': return '宿主没有在约定时间内回答,已保留上一次读到的记录。'
|
|
260
|
+
case 'NATIVE_ACTIVITY_UNAVAILABLE': return '当前宿主暂不支持读取工具记录。'
|
|
261
|
+
default: return '读取工具记录失败,已保留上一次读到的记录。'
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const nativeActivityRecordingNote = recording => recording?.status === 'degraded'
|
|
266
|
+
? '工具记录写入异常,下面只显示已经记下的部分,可能不完整。'
|
|
267
|
+
: ''
|
|
268
|
+
|
|
269
|
+
/** `14:20` today, `11-09 14:20` otherwise; an unrecorded time says so. */
|
|
270
|
+
export function formatActivityAt(value, now = Date.now()) {
|
|
271
|
+
if (!Number.isSafeInteger(value) || value <= 0) return '时间未记录'
|
|
272
|
+
const at = new Date(value), today = new Date(now)
|
|
273
|
+
const clock = `${String(at.getHours()).padStart(2, '0')}:${String(at.getMinutes()).padStart(2, '0')}`
|
|
274
|
+
const sameDay = at.getFullYear() === today.getFullYear() && at.getMonth() === today.getMonth() && at.getDate() === today.getDate()
|
|
275
|
+
return sameDay ? clock : `${String(at.getMonth() + 1).padStart(2, '0')}-${String(at.getDate()).padStart(2, '0')} ${clock}`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* One mounted, read-only observation of one scope.
|
|
280
|
+
*
|
|
281
|
+
* Polling exists only while this controller is live and only for this scope:
|
|
282
|
+
* dispose() clears the timer and aborts the in-flight read, and every reply is
|
|
283
|
+
* checked against the epoch that asked for it, so a late answer for a retired
|
|
284
|
+
* Host, API, Bot, session or context is dropped instead of displayed.
|
|
285
|
+
*/
|
|
286
|
+
export function createNativeActivityController({
|
|
287
|
+
api, botId, mode, sessionId, contextId,
|
|
288
|
+
pollMs = 1500, limit = NATIVE_ACTIVITY_LIMIT_DEFAULT, timeoutMs = REQUEST_MS,
|
|
289
|
+
} = {}) {
|
|
290
|
+
const scope = nativeActivityScope({ botId, mode, sessionId, contextId })
|
|
291
|
+
const readLimit = Number.isSafeInteger(limit) ? Math.min(Math.max(1, limit), NATIVE_ACTIVITY_LIMIT_MAX) : NATIVE_ACTIVITY_LIMIT_DEFAULT
|
|
292
|
+
if (typeof api?.dispatch !== 'function') throw failure('NATIVE_ACTIVITY_UNAVAILABLE', '宿主没有提供只读工具记录接口。')
|
|
293
|
+
const listeners = new Set(), inflight = new Set()
|
|
294
|
+
let live = false, retired = false, fenced = false, reading = false, paging = false, timer
|
|
295
|
+
let pollEpoch = 0, pageEpoch = 0, pagingStarted = false
|
|
296
|
+
let state = {
|
|
297
|
+
scope, conversationId: null, items: [], latest: [], nextCursor: null,
|
|
298
|
+
loading: true, loadingOlder: false, error: '', code: null, truncated: false, loaded: false,
|
|
299
|
+
nativeRoute: null, recording: { status: null, failures: 0, code: null }, revision: 0,
|
|
300
|
+
}
|
|
301
|
+
const publish = patch => {
|
|
302
|
+
if (retired) return
|
|
303
|
+
state = { ...state, ...patch, latest: nativeActivityLatest((patch.items ?? state.items)) , revision: state.revision + 1 }
|
|
304
|
+
for (const listener of listeners) listener()
|
|
305
|
+
}
|
|
306
|
+
const list = async (cursor, signal) => {
|
|
307
|
+
let cancel
|
|
308
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
309
|
+
cancel = () => reject(failure('NATIVE_ACTIVITY_ABORTED', '读取已中断。'))
|
|
310
|
+
signal.addEventListener('abort', cancel, { once: true })
|
|
311
|
+
if (signal.aborted) cancel()
|
|
312
|
+
})
|
|
313
|
+
try { return await Promise.race([Promise.resolve().then(() => api.dispatch(NATIVE_ACTIVITY_ACTION, payloadOf(scope, cursor, readLimit), signal)), interrupted]) }
|
|
314
|
+
finally { signal.removeEventListener('abort', cancel) }
|
|
315
|
+
}
|
|
316
|
+
const fail = error => {
|
|
317
|
+
const mismatched = error?.code === 'NATIVE_ACTIVITY_SCOPE_MISMATCH'
|
|
318
|
+
if (mismatched) fenced = true
|
|
319
|
+
publish({ loading: false, error: nativeActivityNote(error), code: typeof error?.code === 'string' ? error.code : null,
|
|
320
|
+
...(mismatched ? { items: [], nextCursor: null, loaded: false } : {}) })
|
|
321
|
+
}
|
|
322
|
+
const applyPage = (page, olderPage = false) => {
|
|
323
|
+
if (state.conversationId !== null && page.conversationId !== state.conversationId) {
|
|
324
|
+
fenced = true
|
|
325
|
+
fail(failure('NATIVE_ACTIVITY_SCOPE_MISMATCH'))
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
const merged = mergeRecords(state.items, page.items)
|
|
329
|
+
const truncated = state.truncated || merged.truncated
|
|
330
|
+
publish({
|
|
331
|
+
items: merged.items, truncated, nextCursor: truncated ? null : olderPage || !pagingStarted ? page.nextCursor : state.nextCursor,
|
|
332
|
+
conversationId: page.conversationId, nativeRoute: page.nativeRoute, recording: page.recording,
|
|
333
|
+
loading: false, loaded: true, error: '', code: null,
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
const refresh = async () => {
|
|
337
|
+
if (!live || fenced) return
|
|
338
|
+
if (reading || paging) { clearTimeout(timer); timer = setTimeout(refresh, pollMs); return }
|
|
339
|
+
reading = true
|
|
340
|
+
publish({ loading: true })
|
|
341
|
+
const mine = ++pollEpoch
|
|
342
|
+
const controller = new AbortController()
|
|
343
|
+
inflight.add(controller)
|
|
344
|
+
const alarm = setTimeout(() => controller.abort(), timeoutMs)
|
|
345
|
+
try {
|
|
346
|
+
const page = readNativeActivityPage(await list(null, controller.signal), scope)
|
|
347
|
+
if (!live || mine !== pollEpoch || controller.signal.aborted) return
|
|
348
|
+
applyPage(page)
|
|
349
|
+
} catch (error) { if (live && mine === pollEpoch) fail(error) }
|
|
350
|
+
finally {
|
|
351
|
+
clearTimeout(alarm); reading = false; inflight.delete(controller)
|
|
352
|
+
if (live && !fenced) { clearTimeout(timer); timer = setTimeout(refresh, pollMs) }
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const older = async () => {
|
|
356
|
+
if (!live || paging || reading || fenced || state.truncated || state.nextCursor === null) return
|
|
357
|
+
pagingStarted = true
|
|
358
|
+
paging = true
|
|
359
|
+
publish({ loadingOlder: true })
|
|
360
|
+
const mine = ++pageEpoch
|
|
361
|
+
const cursor = state.nextCursor
|
|
362
|
+
const controller = new AbortController()
|
|
363
|
+
inflight.add(controller)
|
|
364
|
+
const alarm = setTimeout(() => controller.abort(), timeoutMs)
|
|
365
|
+
try {
|
|
366
|
+
const page = readNativeActivityPage(await list(cursor, controller.signal), scope)
|
|
367
|
+
if (!live || mine !== pageEpoch || controller.signal.aborted) return
|
|
368
|
+
applyPage(page, true)
|
|
369
|
+
} catch (error) { if (live && mine === pageEpoch) fail(error) }
|
|
370
|
+
finally { clearTimeout(alarm); paging = false; inflight.delete(controller); if (live) publish({ loadingOlder: false }) }
|
|
371
|
+
}
|
|
372
|
+
return Object.freeze({
|
|
373
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
|
|
374
|
+
getSnapshot: () => state,
|
|
375
|
+
start() { if (retired || live || fenced) return; live = true; void refresh() },
|
|
376
|
+
inspect() { clearTimeout(timer); void refresh() },
|
|
377
|
+
older,
|
|
378
|
+
dispose() {
|
|
379
|
+
live = false; retired = true; fenced = true; clearTimeout(timer)
|
|
380
|
+
for (const controller of inflight) controller.abort()
|
|
381
|
+
inflight.clear(); listeners.clear()
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
3
|
+
|
|
4
|
+
// A read-only inbox over committed Task-journal events. It opens the original
|
|
5
|
+
// detail and tracks what the owner has seen; it never starts, stops or retries a
|
|
6
|
+
// Task, and it renders only the Host's structured projection (never task input,
|
|
7
|
+
// execution references or raw event detail).
|
|
8
|
+
const PAGE = 20
|
|
9
|
+
const VISIBLE_MS = 5000
|
|
10
|
+
const FAILED_MS = 15000
|
|
11
|
+
const MAX_ITEMS = 200
|
|
12
|
+
|
|
13
|
+
function abortable(work, signal) {
|
|
14
|
+
let cancel
|
|
15
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
16
|
+
cancel = () => reject(new Error('NOTIFICATION_REQUEST_ABORTED'))
|
|
17
|
+
signal.addEventListener('abort', cancel, { once: true })
|
|
18
|
+
if (signal.aborted) cancel()
|
|
19
|
+
})
|
|
20
|
+
return Promise.race([Promise.resolve().then(work), interrupted]).finally(() => signal.removeEventListener('abort', cancel))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CATEGORY_ICON = { succeeded: 'check', failed: 'close', reconcile: 'refresh', 'waiting-budget': 'clock', cancelled: 'close', blocked: 'lock', reconciled: 'check', update: 'activity' }
|
|
24
|
+
|
|
25
|
+
// Merge a freshly-read newest page into the rendered list. Keys are the durable
|
|
26
|
+
// event seq, so a row the owner is focused on keeps its DOM node across a poll.
|
|
27
|
+
function merge(existing, incoming) {
|
|
28
|
+
const map = new Map(existing.map(item => [item.seq, item]))
|
|
29
|
+
for (const item of incoming) map.set(item.seq, item)
|
|
30
|
+
return [...map.values()].sort((left, right) => right.seq - left.seq).slice(0, MAX_ITEMS)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Relative time from structured journal state; `now` is injected so it stays testable. */
|
|
34
|
+
export function formatAt(at, now = Date.now()) {
|
|
35
|
+
if (!Number.isSafeInteger(at) || at < 0) return ''
|
|
36
|
+
const seconds = Math.max(0, Math.round((now - at) / 1000))
|
|
37
|
+
if (seconds < 45) return '刚刚'
|
|
38
|
+
const minutes = Math.round(seconds / 60)
|
|
39
|
+
if (minutes < 60) return `${minutes} 分钟前`
|
|
40
|
+
const hours = Math.round(minutes / 60)
|
|
41
|
+
if (hours < 24) return `${hours} 小时前`
|
|
42
|
+
return `${Math.round(hours / 24)} 天前`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let scopeKey = 0
|
|
46
|
+
export function NotificationsPanel(props) {
|
|
47
|
+
const key = useMemo(() => ++scopeKey, [props.productApi, props.available])
|
|
48
|
+
return <NotificationFeed key={key} {...props} />
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function NotificationFeed({ productApi, onNavigate, available = true, botIds }) {
|
|
52
|
+
const [items, setItems] = useState([])
|
|
53
|
+
const [summary, setSummary] = useState(null)
|
|
54
|
+
const [status, setStatus] = useState(available ? 'loading' : 'unavailable')
|
|
55
|
+
const [loadingMore, setLoadingMore] = useState(false)
|
|
56
|
+
const [pending, setPending] = useState(false)
|
|
57
|
+
const [feedback, setFeedback] = useState('')
|
|
58
|
+
const session = useRef({ live: false, epoch: 0, busy: false, requests: new Set() })
|
|
59
|
+
|
|
60
|
+
// Each operation is retired with its Host. A read acknowledgement must arrive
|
|
61
|
+
// before the UI claims success; stale polls cannot overwrite that receipt.
|
|
62
|
+
const requestAction = async (action, payload, apply) => {
|
|
63
|
+
const current = session.current
|
|
64
|
+
if (!current.live || current.busy) return
|
|
65
|
+
current.busy = true; current.epoch++
|
|
66
|
+
for (const controller of current.requests) controller.abort()
|
|
67
|
+
const controller = new AbortController()
|
|
68
|
+
current.requests.add(controller)
|
|
69
|
+
setPending(true); setFeedback('')
|
|
70
|
+
const timeout = setTimeout(() => controller.abort(), 8000)
|
|
71
|
+
try {
|
|
72
|
+
const result = await abortable(() => productApi.dispatch(action, payload, controller.signal), controller.signal)
|
|
73
|
+
if (current.live && !controller.signal.aborted) apply(result)
|
|
74
|
+
} catch {
|
|
75
|
+
if (current.live) setFeedback(action === 'notifications.list' ? '较早通知暂时无法读取,请重试。' : '已读状态未确认,请重试。')
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timeout); current.requests.delete(controller); current.busy = false
|
|
78
|
+
if (current.live) { setPending(false); setLoadingMore(false) }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!available || typeof productApi?.dispatch !== 'function') { setStatus('unavailable'); return undefined }
|
|
84
|
+
let live = true, epoch = 0, timer, request
|
|
85
|
+
const currentSession = session.current
|
|
86
|
+
currentSession.live = true
|
|
87
|
+
const schedule = (delay) => { clearTimeout(timer); if (live && !document.hidden) timer = setTimeout(load, delay) }
|
|
88
|
+
const load = async () => {
|
|
89
|
+
clearTimeout(timer)
|
|
90
|
+
if (!live || document.hidden) return
|
|
91
|
+
if (currentSession.busy) { schedule(VISIBLE_MS); return }
|
|
92
|
+
const current = ++epoch, controller = new AbortController()
|
|
93
|
+
const revision = currentSession.epoch
|
|
94
|
+
request?.abort(); request = controller
|
|
95
|
+
currentSession.requests.add(controller)
|
|
96
|
+
const deadline = setTimeout(() => controller.abort(), 8000)
|
|
97
|
+
const valid = () => live && current === epoch && revision === currentSession.epoch && !controller.signal.aborted
|
|
98
|
+
let failed = false
|
|
99
|
+
try {
|
|
100
|
+
const page = await abortable(() => productApi.dispatch('notifications.list', { limit: PAGE }, controller.signal), controller.signal)
|
|
101
|
+
if (!valid()) return
|
|
102
|
+
setItems(previous => merge(previous, Array.isArray(page?.items) ? page.items : []))
|
|
103
|
+
setSummary(previous => ({ unreadCount: page?.unreadCount ?? 0, truncated: page?.truncated === true,
|
|
104
|
+
nextBefore: previous?.olderLoaded ? previous.nextBefore : page?.nextBefore ?? null, olderLoaded: previous?.olderLoaded ?? false, maxSeq: page?.maxSeq ?? 0 }))
|
|
105
|
+
setStatus('ready')
|
|
106
|
+
} catch {
|
|
107
|
+
failed = true
|
|
108
|
+
// A transient failure keeps the rows already on screen; only a first
|
|
109
|
+
// load that never succeeded reports an error state.
|
|
110
|
+
if (live && current === epoch && revision === currentSession.epoch) setStatus('error')
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(deadline)
|
|
113
|
+
currentSession.requests.delete(controller)
|
|
114
|
+
if (request === controller) request = undefined
|
|
115
|
+
if (live && current === epoch) schedule(failed ? FAILED_MS : VISIBLE_MS)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const visible = () => { if (document.hidden) { epoch++; clearTimeout(timer); request?.abort(); request = undefined } else void load() }
|
|
119
|
+
document.addEventListener('visibilitychange', visible)
|
|
120
|
+
void load()
|
|
121
|
+
return () => { live = false; currentSession.live = false; epoch++; clearTimeout(timer); for (const controller of currentSession.requests) controller.abort(); document.removeEventListener('visibilitychange', visible) }
|
|
122
|
+
}, [productApi, available])
|
|
123
|
+
|
|
124
|
+
const open = item => { if (typeof onNavigate === 'function') onNavigate({ section: 'tasks', botId: item.botId, taskId: item.taskId, attemptId: item.attemptId }) }
|
|
125
|
+
const markRead = item => requestAction('notifications.read', { seq: item.seq }, result => {
|
|
126
|
+
setItems(previous => previous.map(row => row.seq === item.seq ? { ...row, read: true } : row))
|
|
127
|
+
setSummary(previous => ({ ...previous, unreadCount: result.unreadCount, truncated: result.truncated === true }))
|
|
128
|
+
})
|
|
129
|
+
const markAll = () => requestAction('notifications.markAll', {}, result => {
|
|
130
|
+
setItems(previous => previous.map(row => row.seq <= result.cursorSeq ? { ...row, read: true } : row))
|
|
131
|
+
setSummary(previous => ({ ...previous, unreadCount: result.unreadCount, truncated: result.truncated === true }))
|
|
132
|
+
})
|
|
133
|
+
const loadMore = async () => {
|
|
134
|
+
if (loadingMore || summary?.nextBefore === null || summary?.nextBefore === undefined) return
|
|
135
|
+
setLoadingMore(true)
|
|
136
|
+
await requestAction('notifications.list', { before: summary.nextBefore, limit: PAGE }, page => {
|
|
137
|
+
setItems(previous => merge(previous, Array.isArray(page?.items) ? page.items : []))
|
|
138
|
+
setSummary(previous => ({ ...previous, nextBefore: page?.nextBefore ?? null, olderLoaded: true }))
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const unread = summary?.unreadCount ?? 0
|
|
143
|
+
const shown = Array.isArray(botIds) ? items.filter(item => botIds.includes(item.botId)) : items
|
|
144
|
+
return (
|
|
145
|
+
<section className="bp-notifications" aria-label="通知中心">
|
|
146
|
+
<header className="bp-notifications-bar">
|
|
147
|
+
<h2 className="bp-notifications-title">通知中心</h2>
|
|
148
|
+
{(unread > 0 || summary?.truncated) && <span className="bp-notifications-badge" aria-label={unread ? `${summary?.truncated ? '至少 ' : ''}${unread} 条未读` : '较早通知的未读状态尚未完整统计'}>{unread > 99 ? '99+' : unread ? `${unread}${summary?.truncated ? '+' : ''}` : '…'}</span>}
|
|
149
|
+
<div className="bp-notifications-bar-actions">
|
|
150
|
+
<button type="button" className="bp-notifications-markall" onClick={markAll} disabled={pending || (unread === 0 && !summary?.truncated)}>
|
|
151
|
+
<UiIcon name="check" size={16} /><span>全部已读</span>
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</header>
|
|
155
|
+
{feedback && <p className="bp-notifications-note" role="alert">{feedback}</p>}
|
|
156
|
+
{summary?.truncated && <p className="bp-notifications-note">未读数量仅统计最近 512 条事件,可继续查看历史通知。</p>}
|
|
157
|
+
{status === 'unavailable' && <p className="bp-notifications-note">当前连接暂时无法读取通知。恢复可用连接后,可在这里查看任务完成、失败与待核对事件。</p>}
|
|
158
|
+
{status === 'loading' && <p className="bp-notifications-note" role="status">正在读取通知…</p>}
|
|
159
|
+
{status === 'error' && <p className="bp-notifications-note" role="status">通知暂时没有更新,稍后会自动重试。</p>}
|
|
160
|
+
{status === 'ready' && shown.length === 0 && <p className="bp-notifications-note">暂无通知。任务完成、失败或需要核对时会出现在这里。</p>}
|
|
161
|
+
{shown.length > 0 && (
|
|
162
|
+
<ul className="bp-notifications-list">
|
|
163
|
+
{shown.map(item => (
|
|
164
|
+
<li key={item.seq} className={`bp-notification bp-notification-${item.tone}${item.read ? '' : ' is-unread'}`} data-category={item.category} data-seq={item.seq}>
|
|
165
|
+
<button type="button" className="bp-notification-open" onClick={() => open(item)}>
|
|
166
|
+
<span className="bp-notification-icon" aria-hidden="true"><UiIcon name={CATEGORY_ICON[item.category] ?? 'activity'} size={18} /></span>
|
|
167
|
+
<span className="bp-notification-body">
|
|
168
|
+
<span className="bp-notification-text">{item.text}</span>
|
|
169
|
+
<span className="bp-notification-meta">
|
|
170
|
+
{item.isNew && <span className="bp-notification-new">新</span>}
|
|
171
|
+
<span className="bp-notification-time">{formatAt(item.at)}</span>
|
|
172
|
+
</span>
|
|
173
|
+
</span>
|
|
174
|
+
{!item.read && <span className="bp-notification-dot" aria-label="未读" />}
|
|
175
|
+
</button>
|
|
176
|
+
{!item.read && <button type="button" className="bp-notification-read" disabled={pending} onClick={() => markRead(item)} aria-label="标为已读"><UiIcon name="check" size={16} /></button>}
|
|
177
|
+
</li>
|
|
178
|
+
))}
|
|
179
|
+
</ul>
|
|
180
|
+
)}
|
|
181
|
+
{summary?.nextBefore !== null && summary?.nextBefore !== undefined && items.length > 0 && (
|
|
182
|
+
<button type="button" className="bp-notifications-more" onClick={loadMore} disabled={pending || loadingMore || items.length >= MAX_ITEMS}>{loadingMore ? '加载中…' : items.length >= MAX_ITEMS ? '已显示最近 200 条通知' : '加载更早'}</button>
|
|
183
|
+
)}
|
|
184
|
+
</section>
|
|
185
|
+
)
|
|
186
|
+
}
|