@jingyi0605/codingns4dsh 0.1.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/README.md +1114 -0
- package/assets/provider-icons/claude-code.png +0 -0
- package/assets/provider-icons/codex.png +0 -0
- package/assets/provider-icons/command-code.svg +5 -0
- package/assets/provider-icons/deepseek-harness.svg +9 -0
- package/assets/provider-icons/gemini.png +0 -0
- package/assets/provider-icons/grok.png +0 -0
- package/assets/provider-icons/kimi.png +0 -0
- package/assets/provider-icons/opencode.png +0 -0
- package/assets/provider-icons/pi.svg +7 -0
- package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +97 -0
- package/data/build/dist/bootstrap/dsh-connection-adapter.js +169 -0
- package/data/build/dist/bootstrap/index.d.ts +38 -0
- package/data/build/dist/bootstrap/index.js +44 -0
- package/data/build/dist/client/account-bar.d.ts +9 -0
- package/data/build/dist/client/account-bar.js +398 -0
- package/data/build/dist/client/bundle.js +26167 -0
- package/data/build/dist/client/cli-catalog.d.ts +24 -0
- package/data/build/dist/client/cli-catalog.js +108 -0
- package/data/build/dist/client/cli-slots.d.ts +38 -0
- package/data/build/dist/client/cli-slots.js +297 -0
- package/data/build/dist/client/debug/ui.d.ts +12 -0
- package/data/build/dist/client/debug/ui.js +451 -0
- package/data/build/dist/client/dsh-h5-bootstrap.d.ts +64 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +293 -0
- package/data/build/dist/client/dsh-runtime-version.d.ts +3 -0
- package/data/build/dist/client/dsh-runtime-version.js +11 -0
- package/data/build/dist/client/external-tool-stream.d.ts +24 -0
- package/data/build/dist/client/external-tool-stream.js +114 -0
- package/data/build/dist/client/features/cli-adapters.d.ts +7 -0
- package/data/build/dist/client/features/cli-adapters.js +215 -0
- package/data/build/dist/client/features/debug.d.ts +4 -0
- package/data/build/dist/client/features/debug.js +24 -0
- package/data/build/dist/client/features/index.d.ts +33 -0
- package/data/build/dist/client/features/index.js +44 -0
- package/data/build/dist/client/features/lan-access-panel.d.ts +5 -0
- package/data/build/dist/client/features/lan-access-panel.js +146 -0
- package/data/build/dist/client/features/lan-access.d.ts +9 -0
- package/data/build/dist/client/features/lan-access.js +30 -0
- package/data/build/dist/client/features/login-protection-panel.d.ts +5 -0
- package/data/build/dist/client/features/login-protection-panel.js +91 -0
- package/data/build/dist/client/features/login-protection-session.d.ts +6 -0
- package/data/build/dist/client/features/login-protection-session.js +25 -0
- package/data/build/dist/client/features/login-protection.d.ts +4 -0
- package/data/build/dist/client/features/login-protection.js +23 -0
- package/data/build/dist/client/features/reverse-proxy-panel.d.ts +9 -0
- package/data/build/dist/client/features/reverse-proxy-panel.js +159 -0
- package/data/build/dist/client/features/reverse-proxy.d.ts +12 -0
- package/data/build/dist/client/features/reverse-proxy.js +112 -0
- package/data/build/dist/client/features/terminal-enhancement-panel.d.ts +5 -0
- package/data/build/dist/client/features/terminal-enhancement-panel.js +160 -0
- package/data/build/dist/client/features/terminal-enhancement.d.ts +4 -0
- package/data/build/dist/client/features/terminal-enhancement.js +24 -0
- package/data/build/dist/client/features/types.d.ts +58 -0
- package/data/build/dist/client/features/types.js +2 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.d.ts +5 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +66 -0
- package/data/build/dist/client/features/workspace-session-enhancement.d.ts +4 -0
- package/data/build/dist/client/features/workspace-session-enhancement.js +101 -0
- package/data/build/dist/client/h5-bootstrap-entry.d.ts +2 -0
- package/data/build/dist/client/h5-bootstrap-entry.js +17 -0
- package/data/build/dist/client/index.d.ts +31 -0
- package/data/build/dist/client/index.js +98 -0
- package/data/build/dist/client/lan-access.d.ts +28 -0
- package/data/build/dist/client/lan-access.js +68 -0
- package/data/build/dist/client/locale.d.ts +20 -0
- package/data/build/dist/client/locale.js +393 -0
- package/data/build/dist/client/provider-icon-assets.d.ts +2 -0
- package/data/build/dist/client/provider-icon-assets.js +23 -0
- package/data/build/dist/client/provider-icons.d.ts +19 -0
- package/data/build/dist/client/provider-icons.js +39 -0
- package/data/build/dist/client/remote-web-context.d.ts +57 -0
- package/data/build/dist/client/remote-web-context.js +939 -0
- package/data/build/dist/client/session-adapter-cache.d.ts +18 -0
- package/data/build/dist/client/session-adapter-cache.js +84 -0
- package/data/build/dist/client/settings-bridge.d.ts +32 -0
- package/data/build/dist/client/settings-bridge.js +126 -0
- package/data/build/dist/client/settings-section.d.ts +30 -0
- package/data/build/dist/client/settings-section.js +93 -0
- package/data/build/dist/client/subscription-slot.d.ts +18 -0
- package/data/build/dist/client/subscription-slot.js +223 -0
- package/data/build/dist/client/terminal/index.d.ts +4 -0
- package/data/build/dist/client/terminal/index.js +3 -0
- package/data/build/dist/client/terminal/model.d.ts +116 -0
- package/data/build/dist/client/terminal/model.js +435 -0
- package/data/build/dist/client/terminal/styles.d.ts +21 -0
- package/data/build/dist/client/terminal/styles.js +51 -0
- package/data/build/dist/client/terminal/ui.d.ts +19 -0
- package/data/build/dist/client/terminal/ui.js +257 -0
- package/data/build/dist/client/terminal/xterm-view.d.ts +13 -0
- package/data/build/dist/client/terminal/xterm-view.js +213 -0
- package/data/build/dist/client/theme.d.ts +56 -0
- package/data/build/dist/client/theme.js +186 -0
- package/data/build/dist/client/workspace-session-archive-dom.d.ts +29 -0
- package/data/build/dist/client/workspace-session-archive-dom.js +525 -0
- package/data/build/dist/client/workspace-session-fiber.d.ts +3 -0
- package/data/build/dist/client/workspace-session-fiber.js +33 -0
- package/data/build/dist/client/workspace-session-logo-dom.d.ts +26 -0
- package/data/build/dist/client/workspace-session-logo-dom.js +128 -0
- package/data/build/dist/features/index.d.ts +3 -0
- package/data/build/dist/features/index.js +2 -0
- package/data/build/dist/features/registry.d.ts +74 -0
- package/data/build/dist/features/registry.js +382 -0
- package/data/build/dist/features/resource-scope/index.d.ts +39 -0
- package/data/build/dist/features/resource-scope/index.js +161 -0
- package/data/build/dist/host/auth-session.d.ts +42 -0
- package/data/build/dist/host/auth-session.js +186 -0
- package/data/build/dist/host/cli-adapters/claude-driver.d.ts +14 -0
- package/data/build/dist/host/cli-adapters/claude-driver.js +103 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +45 -0
- package/data/build/dist/host/cli-adapters/codex-driver.js +534 -0
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +38 -0
- package/data/build/dist/host/cli-adapters/command-code-driver.js +394 -0
- package/data/build/dist/host/cli-adapters/command-code-subscription.d.ts +19 -0
- package/data/build/dist/host/cli-adapters/command-code-subscription.js +126 -0
- package/data/build/dist/host/cli-adapters/driver.d.ts +41 -0
- package/data/build/dist/host/cli-adapters/driver.js +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +37 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +180 -0
- package/data/build/dist/host/cli-adapters/dsh-tool-history.d.ts +48 -0
- package/data/build/dist/host/cli-adapters/dsh-tool-history.js +373 -0
- package/data/build/dist/host/cli-adapters/feature.d.ts +7 -0
- package/data/build/dist/host/cli-adapters/feature.js +335 -0
- package/data/build/dist/host/cli-adapters/gemini-driver.d.ts +24 -0
- package/data/build/dist/host/cli-adapters/gemini-driver.js +427 -0
- package/data/build/dist/host/cli-adapters/grok-driver.d.ts +38 -0
- package/data/build/dist/host/cli-adapters/grok-driver.js +372 -0
- package/data/build/dist/host/cli-adapters/http-sse-client.d.ts +22 -0
- package/data/build/dist/host/cli-adapters/http-sse-client.js +99 -0
- package/data/build/dist/host/cli-adapters/interaction-events.d.ts +11 -0
- package/data/build/dist/host/cli-adapters/interaction-events.js +80 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +65 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.js +256 -0
- package/data/build/dist/host/cli-adapters/kimi-driver.d.ts +21 -0
- package/data/build/dist/host/cli-adapters/kimi-driver.js +382 -0
- package/data/build/dist/host/cli-adapters/model-catalog.d.ts +22 -0
- package/data/build/dist/host/cli-adapters/model-catalog.js +89 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.d.ts +55 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.js +728 -0
- package/data/build/dist/host/cli-adapters/pi-driver.d.ts +43 -0
- package/data/build/dist/host/cli-adapters/pi-driver.js +312 -0
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +16 -0
- package/data/build/dist/host/cli-adapters/process-utils.js +27 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +82 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.js +610 -0
- package/data/build/dist/host/cli-adapters/registry.d.ts +95 -0
- package/data/build/dist/host/cli-adapters/registry.js +659 -0
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.d.ts +24 -0
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +119 -0
- package/data/build/dist/host/cli-adapters/session-probe.d.ts +20 -0
- package/data/build/dist/host/cli-adapters/session-probe.js +139 -0
- package/data/build/dist/host/cli-adapters/session-store.d.ts +68 -0
- package/data/build/dist/host/cli-adapters/session-store.js +180 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +45 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.js +205 -0
- package/data/build/dist/host/cli-adapters/stream-normalizer.d.ts +25 -0
- package/data/build/dist/host/cli-adapters/stream-normalizer.js +83 -0
- package/data/build/dist/host/cli-adapters/tool-observation.d.ts +9 -0
- package/data/build/dist/host/cli-adapters/tool-observation.js +39 -0
- package/data/build/dist/host/control-api-client.d.ts +84 -0
- package/data/build/dist/host/control-api-client.js +130 -0
- package/data/build/dist/host/credential-store.d.ts +54 -0
- package/data/build/dist/host/credential-store.js +114 -0
- package/data/build/dist/host/debug.d.ts +79 -0
- package/data/build/dist/host/debug.js +295 -0
- package/data/build/dist/host/dsh-device-runtime.d.ts +39 -0
- package/data/build/dist/host/dsh-device-runtime.js +118 -0
- package/data/build/dist/host/dsh-gateway-feature.d.ts +5 -0
- package/data/build/dist/host/dsh-gateway-feature.js +55 -0
- package/data/build/dist/host/dsh-runtime-version.d.ts +12 -0
- package/data/build/dist/host/dsh-runtime-version.js +69 -0
- package/data/build/dist/host/features/auth.d.ts +11 -0
- package/data/build/dist/host/features/auth.js +273 -0
- package/data/build/dist/host/features/debug.d.ts +5 -0
- package/data/build/dist/host/features/debug.js +90 -0
- package/data/build/dist/host/features/host-status.d.ts +12 -0
- package/data/build/dist/host/features/host-status.js +54 -0
- package/data/build/dist/host/features/index.d.ts +19 -0
- package/data/build/dist/host/features/index.js +29 -0
- package/data/build/dist/host/features/lan-access-dsh.d.ts +8 -0
- package/data/build/dist/host/features/lan-access-dsh.js +45 -0
- package/data/build/dist/host/features/terminal-process.d.ts +5 -0
- package/data/build/dist/host/features/terminal-process.js +110 -0
- package/data/build/dist/host/features/terminal-status.d.ts +18 -0
- package/data/build/dist/host/features/terminal-status.js +56 -0
- package/data/build/dist/host/features/types.d.ts +41 -0
- package/data/build/dist/host/features/types.js +2 -0
- package/data/build/dist/host/index.d.ts +43 -0
- package/data/build/dist/host/index.js +135 -0
- package/data/build/dist/host/lan-access-dsh.d.ts +83 -0
- package/data/build/dist/host/lan-access-dsh.js +831 -0
- package/data/build/dist/host/native-session-bridge.d.ts +130 -0
- package/data/build/dist/host/native-session-bridge.js +317 -0
- package/data/build/dist/host/relay-tunnel-runtime.d.ts +97 -0
- package/data/build/dist/host/relay-tunnel-runtime.js +361 -0
- package/data/build/dist/host/remote-web-runtime.d.ts +78 -0
- package/data/build/dist/host/remote-web-runtime.js +559 -0
- package/data/build/dist/host/rpc-table.d.ts +32 -0
- package/data/build/dist/host/rpc-table.js +47 -0
- package/data/build/dist/host/rpc.d.ts +18 -0
- package/data/build/dist/host/rpc.js +248 -0
- package/data/build/dist/host/settings.d.ts +19 -0
- package/data/build/dist/host/settings.js +80 -0
- package/data/build/dist/host/terminal/attachment-registry.d.ts +17 -0
- package/data/build/dist/host/terminal/attachment-registry.js +49 -0
- package/data/build/dist/host/terminal/backends/conpty-backend.d.ts +39 -0
- package/data/build/dist/host/terminal/backends/conpty-backend.js +160 -0
- package/data/build/dist/host/terminal/backends/local-pty-backend.d.ts +36 -0
- package/data/build/dist/host/terminal/backends/local-pty-backend.js +178 -0
- package/data/build/dist/host/terminal/backends/tmux-backend.d.ts +40 -0
- package/data/build/dist/host/terminal/backends/tmux-backend.js +191 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-client.d.ts +27 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-client.js +154 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-process.d.ts +10 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-process.js +147 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-protocol.d.ts +92 -0
- package/data/build/dist/host/terminal/broker/conpty-broker-protocol.js +134 -0
- package/data/build/dist/host/terminal/controller-factory.d.ts +37 -0
- package/data/build/dist/host/terminal/controller-factory.js +82 -0
- package/data/build/dist/host/terminal/index.d.ts +17 -0
- package/data/build/dist/host/terminal/index.js +17 -0
- package/data/build/dist/host/terminal/runtime-adapter.d.ts +77 -0
- package/data/build/dist/host/terminal/runtime-adapter.js +18 -0
- package/data/build/dist/host/terminal/runtime-manager.d.ts +41 -0
- package/data/build/dist/host/terminal/runtime-manager.js +179 -0
- package/data/build/dist/host/terminal/shell-detection.d.ts +23 -0
- package/data/build/dist/host/terminal/shell-detection.js +105 -0
- package/data/build/dist/host/terminal/startup.d.ts +33 -0
- package/data/build/dist/host/terminal/startup.js +100 -0
- package/data/build/dist/host/terminal/terminal-controller.d.ts +78 -0
- package/data/build/dist/host/terminal/terminal-controller.js +324 -0
- package/data/build/dist/host/terminal/terminal-process-service.d.ts +30 -0
- package/data/build/dist/host/terminal/terminal-process-service.js +194 -0
- package/data/build/dist/host/terminal/terminal-process-store.d.ts +45 -0
- package/data/build/dist/host/terminal/terminal-process-store.js +183 -0
- package/data/build/dist/host/terminal/terminal-service.d.ts +87 -0
- package/data/build/dist/host/terminal/terminal-service.js +528 -0
- package/data/build/dist/host/terminal/terminal-store.d.ts +49 -0
- package/data/build/dist/host/terminal/terminal-store.js +242 -0
- package/data/build/dist/index.d.ts +37 -0
- package/data/build/dist/index.js +26 -0
- package/data/build/dist/shared/contracts/auth.d.ts +122 -0
- package/data/build/dist/shared/contracts/auth.js +8 -0
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +192 -0
- package/data/build/dist/shared/contracts/cli-adapter.js +2 -0
- package/data/build/dist/shared/contracts/command-code.d.ts +5 -0
- package/data/build/dist/shared/contracts/command-code.js +2 -0
- package/data/build/dist/shared/contracts/config.d.ts +113 -0
- package/data/build/dist/shared/contracts/config.js +86 -0
- package/data/build/dist/shared/contracts/debug-config.d.ts +28 -0
- package/data/build/dist/shared/contracts/debug-config.js +78 -0
- package/data/build/dist/shared/contracts/debug.d.ts +75 -0
- package/data/build/dist/shared/contracts/debug.js +121 -0
- package/data/build/dist/shared/contracts/dsh-device.d.ts +72 -0
- package/data/build/dist/shared/contracts/dsh-device.js +2 -0
- package/data/build/dist/shared/contracts/errors.d.ts +33 -0
- package/data/build/dist/shared/contracts/errors.js +41 -0
- package/data/build/dist/shared/contracts/feature.d.ts +71 -0
- package/data/build/dist/shared/contracts/feature.js +2 -0
- package/data/build/dist/shared/contracts/host-status.d.ts +14 -0
- package/data/build/dist/shared/contracts/host-status.js +2 -0
- package/data/build/dist/shared/contracts/lan-access-dsh.d.ts +30 -0
- package/data/build/dist/shared/contracts/lan-access-dsh.js +2 -0
- package/data/build/dist/shared/contracts/peer-host.d.ts +37 -0
- package/data/build/dist/shared/contracts/peer-host.js +9 -0
- package/data/build/dist/shared/contracts/signaling.d.ts +67 -0
- package/data/build/dist/shared/contracts/signaling.js +2 -0
- package/data/build/dist/shared/contracts/subscription.d.ts +69 -0
- package/data/build/dist/shared/contracts/subscription.js +2 -0
- package/data/build/dist/shared/contracts/terminal-process.d.ts +65 -0
- package/data/build/dist/shared/contracts/terminal-process.js +2 -0
- package/data/build/dist/shared/contracts/terminal.d.ts +127 -0
- package/data/build/dist/shared/contracts/terminal.js +2 -0
- package/data/build/dist/shared/contracts/transport.d.ts +45 -0
- package/data/build/dist/shared/contracts/transport.js +3 -0
- package/data/build/dist/shared/contracts/version.d.ts +19 -0
- package/data/build/dist/shared/contracts/version.js +74 -0
- package/data/build/dist/shared/index.d.ts +22 -0
- package/data/build/dist/shared/index.js +9 -0
- package/data/build/dist/transport/carrier.d.ts +38 -0
- package/data/build/dist/transport/carrier.js +268 -0
- package/data/build/dist/transport/debug.d.ts +22 -0
- package/data/build/dist/transport/debug.js +57 -0
- package/data/build/dist/transport/dsh-envelope.d.ts +41 -0
- package/data/build/dist/transport/dsh-envelope.js +107 -0
- package/data/build/dist/transport/dsh-gateway.d.ts +64 -0
- package/data/build/dist/transport/dsh-gateway.js +254 -0
- package/data/build/dist/transport/dsh-session.d.ts +55 -0
- package/data/build/dist/transport/dsh-session.js +256 -0
- package/data/build/dist/transport/dsh-transport.d.ts +57 -0
- package/data/build/dist/transport/dsh-transport.js +125 -0
- package/data/build/dist/transport/frame.d.ts +132 -0
- package/data/build/dist/transport/frame.js +168 -0
- package/data/build/dist/transport/index.d.ts +11 -0
- package/data/build/dist/transport/index.js +11 -0
- package/data/build/dist/transport/multiplexer.d.ts +63 -0
- package/data/build/dist/transport/multiplexer.js +323 -0
- package/data/build/dist/transport/webrtc-client.d.ts +65 -0
- package/data/build/dist/transport/webrtc-client.js +255 -0
- package/data/build/dist/transport/webrtc-host.d.ts +58 -0
- package/data/build/dist/transport/webrtc-host.js +252 -0
- package/data/build/dist/typert.host.d.ts +25 -0
- package/data/build/dist/typert.host.js +150 -0
- package/dsh.bundle.patch +12 -0
- package/package.json +135 -0
- package/profile/README.md +27 -0
- package/profile/cordis.patch.yml +2 -0
- package/profile/package.json +20 -0
- package/profile/scripts/check-dsh-install.mjs +71 -0
- package/profile/version.json +5 -0
- package/scripts/check-dsh-install.mjs +88 -0
- package/version.json +6 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { HttpSseClient } from './http-sse-client.js';
|
|
3
|
+
import { isProviderDefaultModel } from './model-catalog.js';
|
|
4
|
+
import { firstToolText, normalizeToolStatus, serializeToolValue } from './tool-observation.js';
|
|
5
|
+
import { isQuestionEvent, questionAnswersList, readAgentQuestions } from './interaction-events.js';
|
|
6
|
+
import { usageChunk } from './rpc-driver-utils.js';
|
|
7
|
+
import { terminateChildProcess } from './process-utils.js';
|
|
8
|
+
const WINDOWS = process.platform === 'win32';
|
|
9
|
+
const DEFAULT_BINARIES = WINDOWS ? ['opencode.exe', 'opencode'] : ['opencode'];
|
|
10
|
+
const DEFAULT_URLS = ['http://127.0.0.1:4096'];
|
|
11
|
+
/** OpenCode 的 server/SSE 适配器,向上只暴露 Codingns4DSH 标准流。 */
|
|
12
|
+
export class OpenCodeDriver {
|
|
13
|
+
descriptor = { id: 'opencode', name: 'OpenCode', protocol: 'http-sse', capabilities: ['models', 'stream', 'resume', 'interrupt', 'tool-events', 'reasoning', 'usage', 'permission', 'questions'] };
|
|
14
|
+
binaries;
|
|
15
|
+
serverUrls;
|
|
16
|
+
runSpawnSync;
|
|
17
|
+
runSpawn;
|
|
18
|
+
serverArgs;
|
|
19
|
+
http;
|
|
20
|
+
cachedBinary = null;
|
|
21
|
+
cachedServer = null;
|
|
22
|
+
managedServers = new Map();
|
|
23
|
+
sessions = new Map();
|
|
24
|
+
/** Provider 会话的实际工作目录;目录变化时禁止复用旧会话。 */
|
|
25
|
+
sessionCwds = new Map();
|
|
26
|
+
interactionTargets = new Map();
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.binaries = options.binaries ?? DEFAULT_BINARIES;
|
|
29
|
+
this.serverUrls = options.serverUrls ?? (process.env.OPENCODE_SERVER_URL ? [process.env.OPENCODE_SERVER_URL] : DEFAULT_URLS);
|
|
30
|
+
this.runSpawnSync = options.spawnSync ?? spawnSync;
|
|
31
|
+
this.runSpawn = options.spawn ?? spawn;
|
|
32
|
+
this.serverArgs = options.serverArgs ?? ['serve'];
|
|
33
|
+
this.http = new HttpSseClient(options.fetch === undefined ? {} : { fetch: options.fetch });
|
|
34
|
+
}
|
|
35
|
+
async detect() {
|
|
36
|
+
const server = await this.findServer();
|
|
37
|
+
const binary = this.findBinary();
|
|
38
|
+
if (server !== null)
|
|
39
|
+
return { installed: true, version: server.version, command: server.url };
|
|
40
|
+
if (binary !== null)
|
|
41
|
+
return { installed: true, version: binary.version, command: binary.command };
|
|
42
|
+
return { installed: false, version: null, command: null };
|
|
43
|
+
}
|
|
44
|
+
async listModels() {
|
|
45
|
+
const server = await this.ensureServer(false, undefined);
|
|
46
|
+
if (server === null)
|
|
47
|
+
return emptyCatalog();
|
|
48
|
+
const paths = ['/config/providers', '/provider', '/models'];
|
|
49
|
+
for (const path of paths) {
|
|
50
|
+
try {
|
|
51
|
+
const response = await this.http.json(`${server}${path}`);
|
|
52
|
+
if (!response.data || response.status < 200 || response.status >= 300)
|
|
53
|
+
continue;
|
|
54
|
+
const catalog = parseModelCatalog(response.data);
|
|
55
|
+
if (catalog.groups.length > 0)
|
|
56
|
+
return catalog;
|
|
57
|
+
}
|
|
58
|
+
catch { /* OpenCode 版本间接口不同,继续尝试其他路径。 */ }
|
|
59
|
+
}
|
|
60
|
+
return emptyCatalog();
|
|
61
|
+
}
|
|
62
|
+
async probeSession(input) {
|
|
63
|
+
const providerSessionId = input.providerSessionId?.trim();
|
|
64
|
+
if (!providerSessionId)
|
|
65
|
+
return { state: 'unknown', reason: '缺少 Provider 会话标识' };
|
|
66
|
+
const server = await this.ensureServer(true, input.cwd);
|
|
67
|
+
if (server === null)
|
|
68
|
+
return { state: 'unreachable', reason: 'OpenCode server 当前不可达' };
|
|
69
|
+
const rawStoreRef = `${server}/session/${encodeURIComponent(providerSessionId)}`;
|
|
70
|
+
try {
|
|
71
|
+
const response = await this.http.json(rawStoreRef, input.signal === undefined ? {} : { signal: input.signal });
|
|
72
|
+
if (response.status === 404)
|
|
73
|
+
return { state: 'missing', reason: 'OpenCode server 确认该会话不存在' };
|
|
74
|
+
if (response.status >= 200 && response.status < 300) {
|
|
75
|
+
const record = asRecord(response.data);
|
|
76
|
+
const id = typeof record?.id === 'string' ? record.id : typeof record?.sessionID === 'string' ? record.sessionID : null;
|
|
77
|
+
const directory = readProviderDirectory(record);
|
|
78
|
+
if (input.cwd !== undefined && directory !== undefined && directory !== input.cwd.trim()) {
|
|
79
|
+
return { state: 'corrupt', reason: 'OpenCode 会话工作目录与当前 DSH 会话不一致', rawStoreRef };
|
|
80
|
+
}
|
|
81
|
+
return id === null || id === providerSessionId
|
|
82
|
+
? { state: 'available', reason: 'OpenCode 原始会话可用', rawStoreRef }
|
|
83
|
+
: { state: 'corrupt', reason: 'OpenCode 会话响应与绑定标识不一致', rawStoreRef };
|
|
84
|
+
}
|
|
85
|
+
if (response.status === 401 || response.status === 403 || response.status === 408 || response.status === 429 || response.status >= 500) {
|
|
86
|
+
return { state: 'unreachable', reason: `OpenCode server 暂时无法验证会话(HTTP ${response.status})` };
|
|
87
|
+
}
|
|
88
|
+
return { state: 'unknown', reason: `OpenCode server 无法确认会话状态(HTTP ${response.status})` };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { state: 'unreachable', reason: 'OpenCode server 会话探测请求失败' };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async *executeTurn(input) {
|
|
95
|
+
const server = await this.ensureServer(false, input.cwd);
|
|
96
|
+
if (server === null)
|
|
97
|
+
throw new Error('OpenCode server 未运行,请先启动 `opencode serve`');
|
|
98
|
+
let sessionId = input.providerSessionId ?? this.sessions.get(input.sessionId);
|
|
99
|
+
if (sessionId !== undefined && !(await this.providerSessionMatchesDirectory(server, sessionId, input.cwd, input.signal))) {
|
|
100
|
+
sessionId = undefined;
|
|
101
|
+
}
|
|
102
|
+
if (sessionId !== undefined)
|
|
103
|
+
this.sessions.set(input.sessionId, sessionId);
|
|
104
|
+
const createdSession = sessionId === undefined;
|
|
105
|
+
if (sessionId === undefined) {
|
|
106
|
+
sessionId = await this.createSession(server, input);
|
|
107
|
+
this.sessions.set(input.sessionId, sessionId);
|
|
108
|
+
}
|
|
109
|
+
if (input.cwd?.trim())
|
|
110
|
+
this.sessionCwds.set(sessionId, input.cwd.trim());
|
|
111
|
+
this.interactionTargets.set(input.sessionId, server);
|
|
112
|
+
const streamController = new AbortController();
|
|
113
|
+
let aborted = false;
|
|
114
|
+
const abort = () => {
|
|
115
|
+
aborted = true;
|
|
116
|
+
streamController.abort();
|
|
117
|
+
void this.abortSession(server, sessionId);
|
|
118
|
+
if (input.cwd !== undefined)
|
|
119
|
+
this.stopManagedServer(input.cwd);
|
|
120
|
+
};
|
|
121
|
+
input.signal?.addEventListener('abort', abort, { once: true });
|
|
122
|
+
const eventStream = this.http.sse(withOpenCodeDirectory(server, '/event', input.cwd), { signal: streamController.signal });
|
|
123
|
+
const eventIterator = eventStream[Symbol.asyncIterator]();
|
|
124
|
+
// 先调用 next() 让 SSE 请求真正建立,再发送 prompt,避免首个事件竞态丢失。
|
|
125
|
+
let pendingEvent = eventIterator.next();
|
|
126
|
+
let sendError = null;
|
|
127
|
+
const send = this.sendPrompt(server, sessionId, input).catch((error) => {
|
|
128
|
+
sendError = error;
|
|
129
|
+
// message 请求失败时,OpenCode 的全局 SSE 通常不会自行结束;主动中止,
|
|
130
|
+
// 否则调用方会一直等不到错误,只看到没有任何输出。
|
|
131
|
+
streamController.abort();
|
|
132
|
+
return null;
|
|
133
|
+
});
|
|
134
|
+
let emitted = false;
|
|
135
|
+
let finished = false;
|
|
136
|
+
const cumulative = new Map();
|
|
137
|
+
const partTypes = new Map();
|
|
138
|
+
const assistantMessageIds = new Set();
|
|
139
|
+
const knownMessageIds = new Set();
|
|
140
|
+
const pendingMessageParts = new Map();
|
|
141
|
+
try {
|
|
142
|
+
if (createdSession || input.providerSessionId !== undefined)
|
|
143
|
+
yield { type: 'session-binding', providerSessionId: sessionId };
|
|
144
|
+
// sendPrompt 的返回体可能包含完整消息;SSE 仍然是首选,返回体作为兜底。
|
|
145
|
+
try {
|
|
146
|
+
while (true) {
|
|
147
|
+
const result = await pendingEvent;
|
|
148
|
+
if (result.done)
|
|
149
|
+
break;
|
|
150
|
+
pendingEvent = eventIterator.next();
|
|
151
|
+
const parsed = parseEvent(result.value);
|
|
152
|
+
if (parsed === null)
|
|
153
|
+
continue;
|
|
154
|
+
const eventSession = eventSessionId(parsed);
|
|
155
|
+
// /event 是全局 SSE;没有会话字段的旧版事件仍允许通过,
|
|
156
|
+
// 但明确属于其他会话的事件绝不能结束或污染当前轮次。
|
|
157
|
+
if (eventSession !== undefined && eventSession !== sessionId)
|
|
158
|
+
continue;
|
|
159
|
+
const messageRole = readMessageRole(parsed);
|
|
160
|
+
if (messageRole !== null) {
|
|
161
|
+
knownMessageIds.add(messageRole.id);
|
|
162
|
+
if (messageRole.role === 'assistant') {
|
|
163
|
+
assistantMessageIds.add(messageRole.id);
|
|
164
|
+
const pending = pendingMessageParts.get(messageRole.id) ?? [];
|
|
165
|
+
pendingMessageParts.delete(messageRole.id);
|
|
166
|
+
for (const pendingPart of pending) {
|
|
167
|
+
const chunk = eventToChunk(pendingPart, cumulative, assistantMessageIds, partTypes);
|
|
168
|
+
if (chunk !== null) {
|
|
169
|
+
emitted = true;
|
|
170
|
+
yield chunk;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
pendingMessageParts.delete(messageRole.id);
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const messageId = eventMessageId(parsed);
|
|
180
|
+
if (messageId !== undefined && !knownMessageIds.has(messageId)) {
|
|
181
|
+
// OpenCode 的 message.part.updated 经常早于 message.updated 到达。
|
|
182
|
+
// 工具调用是当前轮次最重要的实时事件,不能像正文一样等到
|
|
183
|
+
// assistant 消息结算后才投影,否则原生工具节点会被追加到正文底部。
|
|
184
|
+
// 用户消息没有 tool 字段,因此仍然暂存并等待 role 校验。
|
|
185
|
+
if (isOpenCodeToolEvent(parsed)) {
|
|
186
|
+
const chunk = eventToChunk(parsed, cumulative, undefined, partTypes);
|
|
187
|
+
if (chunk !== null) {
|
|
188
|
+
emitted = true;
|
|
189
|
+
yield chunk;
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const pending = pendingMessageParts.get(messageId) ?? [];
|
|
194
|
+
pending.push(parsed);
|
|
195
|
+
pendingMessageParts.set(messageId, pending);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (messageId !== undefined && !assistantMessageIds.has(messageId))
|
|
199
|
+
continue;
|
|
200
|
+
const chunk = eventToChunk(parsed, cumulative, assistantMessageIds, partTypes);
|
|
201
|
+
if (chunk !== null) {
|
|
202
|
+
emitted = true;
|
|
203
|
+
yield chunk;
|
|
204
|
+
}
|
|
205
|
+
if (isFinishedEvent(parsed, emitted)) {
|
|
206
|
+
finished = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (sendError !== null && !aborted)
|
|
213
|
+
throw sendError;
|
|
214
|
+
if (!aborted)
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
if (!finished && !aborted) {
|
|
218
|
+
const response = await send;
|
|
219
|
+
if (sendError !== null && !aborted)
|
|
220
|
+
throw sendError;
|
|
221
|
+
for (const chunk of responseChunks(response, cumulative, partTypes)) {
|
|
222
|
+
emitted = true;
|
|
223
|
+
yield chunk;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (aborted || input.signal?.aborted)
|
|
227
|
+
yield { type: 'finish', reason: 'cancel' };
|
|
228
|
+
else if (finished || emitted)
|
|
229
|
+
yield { type: 'finish', reason: 'stop' };
|
|
230
|
+
else
|
|
231
|
+
throw new Error('OpenCode 未返回可识别的事件');
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
if (this.interactionTargets.get(input.sessionId) === server)
|
|
235
|
+
this.interactionTargets.delete(input.sessionId);
|
|
236
|
+
input.signal?.removeEventListener('abort', abort);
|
|
237
|
+
streamController.abort();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async respondPermission(sessionId, response) {
|
|
241
|
+
const server = this.interactionTargets.get(sessionId);
|
|
242
|
+
if (server === undefined)
|
|
243
|
+
throw new Error('OpenCode 权限请求已结束');
|
|
244
|
+
const result = await this.http.json(`${server}/permission/${encodeURIComponent(response.requestId)}/reply`, {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers: { 'content-type': 'application/json' },
|
|
247
|
+
body: JSON.stringify({ reply: response.approved ? 'once' : 'reject' }),
|
|
248
|
+
});
|
|
249
|
+
if (result.status < 200 || result.status >= 300)
|
|
250
|
+
throw new Error(`OpenCode 权限回复失败(HTTP ${result.status})`);
|
|
251
|
+
}
|
|
252
|
+
async respondQuestion(sessionId, response) {
|
|
253
|
+
const server = this.interactionTargets.get(sessionId);
|
|
254
|
+
if (server === undefined)
|
|
255
|
+
throw new Error('OpenCode 问题请求已结束');
|
|
256
|
+
const result = await this.http.json(`${server}/question/${encodeURIComponent(response.requestId)}/reply`, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'content-type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({ answers: questionAnswersList(response) }),
|
|
260
|
+
});
|
|
261
|
+
if (result.status < 200 || result.status >= 300)
|
|
262
|
+
throw new Error(`OpenCode 问题回复失败(HTTP ${result.status})`);
|
|
263
|
+
}
|
|
264
|
+
dispose() {
|
|
265
|
+
this.sessions.clear();
|
|
266
|
+
this.sessionCwds.clear();
|
|
267
|
+
for (const managed of this.managedServers.values())
|
|
268
|
+
terminateChildProcess(managed.child);
|
|
269
|
+
this.managedServers.clear();
|
|
270
|
+
this.cachedBinary = null;
|
|
271
|
+
this.cachedServer = null;
|
|
272
|
+
}
|
|
273
|
+
async sendPrompt(server, sessionId, input) {
|
|
274
|
+
const body = { parts: [{ type: 'text', text: input.prompt }] };
|
|
275
|
+
const model = parseOpenCodeModel(input.modelId);
|
|
276
|
+
if (model !== null)
|
|
277
|
+
body.model = model;
|
|
278
|
+
if (input.effortId)
|
|
279
|
+
body.variant = input.effortId;
|
|
280
|
+
const response = await this.http.json(withOpenCodeDirectory(server, `/session/${encodeURIComponent(sessionId)}/message`, input.cwd), {
|
|
281
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), ...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
282
|
+
});
|
|
283
|
+
if (response.status < 200 || response.status >= 300)
|
|
284
|
+
throw new Error(`OpenCode message 请求失败(HTTP ${response.status})`);
|
|
285
|
+
return response.data;
|
|
286
|
+
}
|
|
287
|
+
async createSession(server, input) {
|
|
288
|
+
const body = { title: input.sessionId, ...(input.cwd === undefined ? {} : { directory: input.cwd }) };
|
|
289
|
+
const response = await this.http.json(withOpenCodeDirectory(server, '/session', input.cwd), {
|
|
290
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), ...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
291
|
+
});
|
|
292
|
+
const record = asRecord(response.data);
|
|
293
|
+
const id = typeof record?.id === 'string' ? record.id : typeof record?.sessionID === 'string' ? record.sessionID : null;
|
|
294
|
+
if (response.status < 200 || response.status >= 300 || id === null)
|
|
295
|
+
throw new Error(`OpenCode 创建会话失败(HTTP ${response.status})`);
|
|
296
|
+
return id;
|
|
297
|
+
}
|
|
298
|
+
async providerSessionMatchesDirectory(server, sessionId, cwd, signal) {
|
|
299
|
+
if (cwd === undefined || cwd.trim() === '')
|
|
300
|
+
return true;
|
|
301
|
+
const normalizedCwd = cwd.trim();
|
|
302
|
+
const knownCwd = this.sessionCwds.get(sessionId);
|
|
303
|
+
if (knownCwd !== undefined)
|
|
304
|
+
return knownCwd === normalizedCwd;
|
|
305
|
+
try {
|
|
306
|
+
const response = await this.http.json(`${server}/session/${encodeURIComponent(sessionId)}`, signal === undefined ? {} : { signal });
|
|
307
|
+
if (response.status === 404)
|
|
308
|
+
return false;
|
|
309
|
+
if (response.status < 200 || response.status >= 300)
|
|
310
|
+
return true;
|
|
311
|
+
const directory = readProviderDirectory(asRecord(response.data));
|
|
312
|
+
if (directory === undefined)
|
|
313
|
+
return true;
|
|
314
|
+
return directory === normalizedCwd;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// 目录校验失败时保留旧兼容行为,避免临时网络故障导致会话被无故重建。
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async abortSession(server, sessionId) {
|
|
322
|
+
try {
|
|
323
|
+
await this.http.json(`${server}/session/${encodeURIComponent(sessionId)}/abort`, { method: 'POST' });
|
|
324
|
+
}
|
|
325
|
+
catch { /* 取消请求尽力而为 */ }
|
|
326
|
+
}
|
|
327
|
+
async ensureServer(probeOnly, cwd) {
|
|
328
|
+
const workspace = cwd ?? process.cwd();
|
|
329
|
+
const managed = this.managedServers.get(workspace);
|
|
330
|
+
if (managed !== undefined)
|
|
331
|
+
return managed.url;
|
|
332
|
+
if (this.cachedServer !== null)
|
|
333
|
+
return this.cachedServer;
|
|
334
|
+
const server = await this.findServer();
|
|
335
|
+
if (server !== null) {
|
|
336
|
+
this.cachedServer = server.url;
|
|
337
|
+
return server.url;
|
|
338
|
+
}
|
|
339
|
+
if (!probeOnly)
|
|
340
|
+
return this.startServer(workspace);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
async startServer(cwd) {
|
|
344
|
+
const command = this.cachedBinary ?? this.findBinary()?.command;
|
|
345
|
+
if (command === null || command === undefined)
|
|
346
|
+
return null;
|
|
347
|
+
const port = 4096 + this.managedServers.size;
|
|
348
|
+
const url = `http://127.0.0.1:${port}`;
|
|
349
|
+
let child;
|
|
350
|
+
try {
|
|
351
|
+
child = this.runSpawn(command, [...this.serverArgs, '--port', String(port)], {
|
|
352
|
+
cwd,
|
|
353
|
+
env: { ...process.env },
|
|
354
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
355
|
+
windowsHide: true,
|
|
356
|
+
shell: WINDOWS,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
child.stdout.on('data', () => undefined);
|
|
363
|
+
child.stderr.on('data', () => undefined);
|
|
364
|
+
this.managedServers.set(cwd, { url, child });
|
|
365
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
366
|
+
try {
|
|
367
|
+
const health = await this.http.json(`${url}/global/health`);
|
|
368
|
+
if (health.status >= 200 && health.status < 300)
|
|
369
|
+
return url;
|
|
370
|
+
}
|
|
371
|
+
catch { /* 服务尚未监听 */ }
|
|
372
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
373
|
+
}
|
|
374
|
+
this.stopManagedServer(cwd);
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
stopManagedServer(cwd) {
|
|
378
|
+
const managed = this.managedServers.get(cwd);
|
|
379
|
+
if (managed === undefined)
|
|
380
|
+
return;
|
|
381
|
+
this.managedServers.delete(cwd);
|
|
382
|
+
terminateChildProcess(managed.child);
|
|
383
|
+
}
|
|
384
|
+
async findServer() {
|
|
385
|
+
for (const raw of this.serverUrls) {
|
|
386
|
+
const url = raw.replace(/\/$/u, '');
|
|
387
|
+
for (const path of ['/global/health', '/health']) {
|
|
388
|
+
try {
|
|
389
|
+
const response = await this.http.json(`${url}${path}`);
|
|
390
|
+
if (response.status < 200 || response.status >= 300)
|
|
391
|
+
continue;
|
|
392
|
+
const record = asRecord(response.data);
|
|
393
|
+
const version = typeof record?.version === 'string' ? record.version : null;
|
|
394
|
+
return { url, version };
|
|
395
|
+
}
|
|
396
|
+
catch { /* 服务未监听或端口不可达 */ }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
findBinary() {
|
|
402
|
+
if (this.cachedBinary !== null)
|
|
403
|
+
return { command: this.cachedBinary, version: null };
|
|
404
|
+
for (const command of this.binaries) {
|
|
405
|
+
try {
|
|
406
|
+
const result = this.runSpawnSync(command, ['--version'], { encoding: 'utf8', timeout: 3_000, windowsHide: true, shell: WINDOWS });
|
|
407
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
408
|
+
if (result.status === 0) {
|
|
409
|
+
this.cachedBinary = command;
|
|
410
|
+
return { command, version: output.match(/\d+\.\d+\.\d+/u)?.[0] ?? null };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch { /* PATH 中没有命令 */ }
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function parseOpenCodeModel(modelId) {
|
|
419
|
+
if (isProviderDefaultModel(modelId))
|
|
420
|
+
return null;
|
|
421
|
+
const separator = modelId.indexOf('/');
|
|
422
|
+
if (separator <= 0 || separator === modelId.length - 1)
|
|
423
|
+
return null;
|
|
424
|
+
return { providerID: modelId.slice(0, separator), modelID: modelId.slice(separator + 1) };
|
|
425
|
+
}
|
|
426
|
+
function readProviderDirectory(record) {
|
|
427
|
+
const value = record?.directory ?? record?.cwd;
|
|
428
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
429
|
+
}
|
|
430
|
+
function isReasoningField(value) {
|
|
431
|
+
if (value === undefined)
|
|
432
|
+
return false;
|
|
433
|
+
const normalized = value.trim().toLowerCase();
|
|
434
|
+
return normalized === 'reasoning'
|
|
435
|
+
|| normalized === 'thinking'
|
|
436
|
+
|| normalized === 'reasoning_content'
|
|
437
|
+
|| normalized === 'reasoning_details';
|
|
438
|
+
}
|
|
439
|
+
function withOpenCodeDirectory(server, pathname, cwd) {
|
|
440
|
+
const url = new URL(pathname, `${server.replace(/\/$/u, '')}/`);
|
|
441
|
+
if (cwd?.trim())
|
|
442
|
+
url.searchParams.set('directory', cwd.trim());
|
|
443
|
+
return url.toString();
|
|
444
|
+
}
|
|
445
|
+
function parseEvent(event) {
|
|
446
|
+
try {
|
|
447
|
+
const value = JSON.parse(event.data);
|
|
448
|
+
const record = asRecord(value);
|
|
449
|
+
if (record === null)
|
|
450
|
+
return null;
|
|
451
|
+
return event.event === null || typeof record.type === 'string' ? record : { ...record, type: event.event };
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function eventToChunk(event, cumulative, assistantMessageIds, partTypes) {
|
|
458
|
+
const type = typeof event.type === 'string' ? event.type : '';
|
|
459
|
+
const properties = asRecord(event.properties);
|
|
460
|
+
const part = asRecord(event.part) ?? asRecord(properties?.part) ?? properties ?? event;
|
|
461
|
+
const messageId = firstToolText(part.messageID, part.messageId, properties?.messageID, properties?.messageId);
|
|
462
|
+
if (messageId !== undefined && assistantMessageIds !== undefined && !assistantMessageIds.has(messageId))
|
|
463
|
+
return null;
|
|
464
|
+
if (isQuestionEvent(type)) {
|
|
465
|
+
const requestId = firstToolText(part.id, part.requestID, part.requestId, properties?.id);
|
|
466
|
+
const questions = readAgentQuestions(part.questions ?? properties?.questions ?? part);
|
|
467
|
+
if (requestId !== undefined && questions.length > 0)
|
|
468
|
+
return { type: 'question-request', requestId, questions };
|
|
469
|
+
}
|
|
470
|
+
if (type.toLowerCase().includes('permission')) {
|
|
471
|
+
const requestId = firstToolText(part.id, part.requestID, part.requestId, properties?.id);
|
|
472
|
+
if (requestId !== undefined) {
|
|
473
|
+
const kind = firstToolText(part.permission, part.kind, part.type) ?? 'unknown';
|
|
474
|
+
const detail = serializeToolValue(part.patterns ?? part.metadata ?? part.detail);
|
|
475
|
+
return {
|
|
476
|
+
type: 'permission-request',
|
|
477
|
+
requestId,
|
|
478
|
+
kind,
|
|
479
|
+
toolName: firstToolText(part.tool, part.toolName) ?? kind,
|
|
480
|
+
...(detail === undefined ? {} : { detail }),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const partId = firstToolText(part.id, part.partID, properties?.partID, properties?.partId);
|
|
485
|
+
const rawPartType = typeof part.type === 'string' ? part.type : '';
|
|
486
|
+
if (partId !== undefined && rawPartType !== '')
|
|
487
|
+
partTypes?.set(partId, rawPartType.trim().toLowerCase());
|
|
488
|
+
const partType = rawPartType.trim().toLowerCase() || (partId === undefined ? '' : partTypes?.get(partId) ?? '');
|
|
489
|
+
const key = partId ?? `${type}:${partType}`;
|
|
490
|
+
// OpenCode 1.18 的 delta 把 field 放在 properties 顶层;兼容旧版的顶层 field。
|
|
491
|
+
// field 是最具体的通道声明,必须优先于 part.type,避免 reasoning 被投影成正文。
|
|
492
|
+
const field = firstToolText(part.field, properties?.field, event.field);
|
|
493
|
+
const reasoning = isReasoningField(field) || partType === 'reasoning';
|
|
494
|
+
const text = typeof part.text === 'string' ? part.text : typeof part.content === 'string' ? part.content : typeof part.delta === 'string' ? part.delta : null;
|
|
495
|
+
if (text !== null && (partType === 'text' || partType === 'reasoning' || type.includes('part'))) {
|
|
496
|
+
const eventDelta = typeof properties?.delta === 'string'
|
|
497
|
+
? properties.delta
|
|
498
|
+
: typeof event.delta === 'string' ? event.delta : undefined;
|
|
499
|
+
if (eventDelta !== undefined) {
|
|
500
|
+
if (!eventDelta)
|
|
501
|
+
return null;
|
|
502
|
+
cumulative.set(key, (cumulative.get(key) ?? 0) + eventDelta.length);
|
|
503
|
+
return reasoning ? { type: 'reasoning-delta', text: eventDelta } : { type: 'text-delta', text: eventDelta };
|
|
504
|
+
}
|
|
505
|
+
const previous = cumulative.get(key) ?? 0;
|
|
506
|
+
const snapshotDelta = text.slice(previous);
|
|
507
|
+
cumulative.set(key, text.length);
|
|
508
|
+
if (!snapshotDelta)
|
|
509
|
+
return null;
|
|
510
|
+
return reasoning ? { type: 'reasoning-delta', text: snapshotDelta } : { type: 'text-delta', text: snapshotDelta };
|
|
511
|
+
}
|
|
512
|
+
const toolName = firstToolText(part.tool, part.name);
|
|
513
|
+
if (partType === 'tool' && toolName !== undefined) {
|
|
514
|
+
const state = parseToolRecord(part.state) ?? part;
|
|
515
|
+
const callId = firstToolText(part.callID, part.callId, part.toolCallId, part.id);
|
|
516
|
+
const input = serializeToolValue(readOpenCodeToolInput(event, properties, part, state));
|
|
517
|
+
const output = serializeToolValue(state.output ?? state.result);
|
|
518
|
+
const error = serializeToolValue(state.error);
|
|
519
|
+
const fallback = error !== undefined
|
|
520
|
+
? 'failed'
|
|
521
|
+
: output !== undefined
|
|
522
|
+
? 'completed'
|
|
523
|
+
: 'running';
|
|
524
|
+
const agentId = firstToolText(state.agentId, state.agent_id, part.agentId, part.agent_id);
|
|
525
|
+
const detail = serializeToolValue(state.detail ?? part.detail);
|
|
526
|
+
return {
|
|
527
|
+
type: 'tool-event',
|
|
528
|
+
toolName,
|
|
529
|
+
status: normalizeToolStatus(state.status, fallback),
|
|
530
|
+
...(callId ? { callId } : {}),
|
|
531
|
+
...(input !== undefined ? { input } : {}),
|
|
532
|
+
...(output !== undefined ? { output } : {}),
|
|
533
|
+
...(output !== undefined ? { outputMode: 'snapshot' } : {}),
|
|
534
|
+
...(error !== undefined ? { error } : {}),
|
|
535
|
+
...(agentId ? { agentId } : {}),
|
|
536
|
+
...(detail !== undefined ? { detail } : {}),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const usage = asRecord(event.usage) ?? asRecord(part.usage) ?? readOpenCodeTokenUsage(properties);
|
|
540
|
+
if (usage !== null)
|
|
541
|
+
return usageChunk(usage);
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
/** OpenCode 将最终用量放在 message.updated.properties.info.tokens。 */
|
|
545
|
+
function readOpenCodeTokenUsage(properties) {
|
|
546
|
+
const info = asRecord(properties?.info);
|
|
547
|
+
const tokens = asRecord(info?.tokens);
|
|
548
|
+
if (tokens === null)
|
|
549
|
+
return null;
|
|
550
|
+
const cache = asRecord(tokens.cache);
|
|
551
|
+
return {
|
|
552
|
+
input_tokens: tokens.input ?? tokens.input_tokens,
|
|
553
|
+
output_tokens: tokens.output ?? tokens.output_tokens,
|
|
554
|
+
cache_read_tokens: cache?.read ?? tokens.cache_read_tokens ?? tokens.cacheReadTokens,
|
|
555
|
+
cache_creation_tokens: cache?.write ?? tokens.cache_write_tokens ?? tokens.cacheWriteTokens,
|
|
556
|
+
total_tokens: tokens.total ?? tokens.total_tokens ?? tokens.totalTokens,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function isFinishedEvent(event, emitted) {
|
|
560
|
+
const type = typeof event.type === 'string' ? event.type.toLowerCase() : '';
|
|
561
|
+
if (type.includes('error') || type.includes('completed') || type.includes('done'))
|
|
562
|
+
return true;
|
|
563
|
+
const properties = asRecord(event.properties);
|
|
564
|
+
const rawStatus = event.status ?? properties?.status;
|
|
565
|
+
const statusRecord = asRecord(rawStatus);
|
|
566
|
+
const status = typeof rawStatus === 'string' ? rawStatus.toLowerCase() : typeof statusRecord?.type === 'string' ? statusRecord.type.toLowerCase() : '';
|
|
567
|
+
return status === 'idle' || status === 'completed' || status === 'success';
|
|
568
|
+
}
|
|
569
|
+
function responseChunks(value, cumulative, partTypes) {
|
|
570
|
+
const record = asRecord(value);
|
|
571
|
+
if (record === null)
|
|
572
|
+
return [];
|
|
573
|
+
const chunk = eventToChunk(record, cumulative, undefined, partTypes);
|
|
574
|
+
return chunk === null ? [] : [chunk];
|
|
575
|
+
}
|
|
576
|
+
function readMessageRole(event) {
|
|
577
|
+
if (event.type !== 'message.updated')
|
|
578
|
+
return null;
|
|
579
|
+
const properties = asRecord(event.properties);
|
|
580
|
+
const info = asRecord(properties?.info) ?? properties;
|
|
581
|
+
if (info === null)
|
|
582
|
+
return null;
|
|
583
|
+
const messageId = firstToolText(info.id, info.messageID, info.messageId);
|
|
584
|
+
if (messageId === undefined || (info?.role !== 'assistant' && info?.role !== 'user'))
|
|
585
|
+
return null;
|
|
586
|
+
return { id: messageId, role: info.role };
|
|
587
|
+
}
|
|
588
|
+
function eventMessageId(event) {
|
|
589
|
+
const properties = asRecord(event.properties);
|
|
590
|
+
const part = asRecord(event.part) ?? asRecord(properties?.part);
|
|
591
|
+
return firstToolText(part?.messageID, part?.messageId, properties?.messageID, properties?.messageId);
|
|
592
|
+
}
|
|
593
|
+
/** 判断尚未关联到 role 的 SSE 是否已经明确是工具 part。 */
|
|
594
|
+
function isOpenCodeToolEvent(event) {
|
|
595
|
+
const properties = asRecord(event.properties);
|
|
596
|
+
const part = asRecord(event.part) ?? asRecord(properties?.part) ?? properties;
|
|
597
|
+
if (part === null)
|
|
598
|
+
return false;
|
|
599
|
+
const type = typeof part.type === 'string' ? part.type.trim().toLowerCase() : '';
|
|
600
|
+
return type === 'tool' || typeof part.tool === 'string' || part.callID !== undefined || part.toolCallId !== undefined;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* OpenCode 不同版本把工具参数放在不同层级:稳定格式是 state.input,
|
|
604
|
+
* 旧版/代理层还会放到 part.input、arguments 或 metadata.input。优先取
|
|
605
|
+
* 非空值,避免先到达的空快照把真正参数覆盖成 `{}`。
|
|
606
|
+
*/
|
|
607
|
+
function readOpenCodeToolInput(event, properties, part, state) {
|
|
608
|
+
const stateMetadata = asRecord(state.metadata);
|
|
609
|
+
const stateData = asRecord(state.data);
|
|
610
|
+
const partMetadata = asRecord(part.metadata);
|
|
611
|
+
const partData = asRecord(part.data);
|
|
612
|
+
const toolCall = asRecord(part.toolCall) ?? asRecord(part.tool_call) ?? asRecord(part.call);
|
|
613
|
+
const values = [
|
|
614
|
+
state.input, state.arguments, state.args, state.parameters,
|
|
615
|
+
state.raw,
|
|
616
|
+
stateMetadata?.input, stateMetadata?.arguments, stateMetadata?.args,
|
|
617
|
+
stateData?.input, stateData?.arguments, stateData?.args,
|
|
618
|
+
part.input, part.arguments, part.args, part.parameters,
|
|
619
|
+
part.raw,
|
|
620
|
+
partMetadata?.input, partMetadata?.arguments, partMetadata?.args,
|
|
621
|
+
partData?.input, partData?.arguments, partData?.args,
|
|
622
|
+
toolCall?.input, toolCall?.arguments, toolCall?.args, toolCall?.parameters, toolCall?.raw,
|
|
623
|
+
properties?.input, properties?.arguments, properties?.args,
|
|
624
|
+
event.input, event.arguments, event.args,
|
|
625
|
+
];
|
|
626
|
+
let fallback;
|
|
627
|
+
for (const value of values) {
|
|
628
|
+
if (value === undefined || value === null)
|
|
629
|
+
continue;
|
|
630
|
+
if (fallback === undefined)
|
|
631
|
+
fallback = value;
|
|
632
|
+
if (!isEmptyToolValue(value))
|
|
633
|
+
return value;
|
|
634
|
+
}
|
|
635
|
+
return fallback;
|
|
636
|
+
}
|
|
637
|
+
function isEmptyToolValue(value) {
|
|
638
|
+
if (typeof value === 'string')
|
|
639
|
+
return value.trim() === '' || value.trim() === '{}';
|
|
640
|
+
return isRecord(value) && Object.keys(value).length === 0;
|
|
641
|
+
}
|
|
642
|
+
function eventSessionId(event) {
|
|
643
|
+
const properties = asRecord(event.properties);
|
|
644
|
+
const info = asRecord(properties?.info);
|
|
645
|
+
const part = asRecord(event.part) ?? asRecord(properties?.part);
|
|
646
|
+
return firstToolText(event.sessionID, event.sessionId, properties?.sessionID, properties?.sessionId, info?.sessionID, info?.sessionId, part?.sessionID, part?.sessionId);
|
|
647
|
+
}
|
|
648
|
+
function parseModelCatalog(value) {
|
|
649
|
+
const root = asRecord(value);
|
|
650
|
+
if (Array.isArray(root?.providers)) {
|
|
651
|
+
const groups = root.providers.flatMap((rawProvider) => {
|
|
652
|
+
const provider = asRecord(rawProvider);
|
|
653
|
+
if (provider === null)
|
|
654
|
+
return [];
|
|
655
|
+
const providerId = typeof provider.id === 'string' ? provider.id : null;
|
|
656
|
+
if (!providerId)
|
|
657
|
+
return [];
|
|
658
|
+
const models = asRecord(provider.models);
|
|
659
|
+
if (models === null)
|
|
660
|
+
return [];
|
|
661
|
+
const items = Object.entries(models).map(([id, model]) => {
|
|
662
|
+
const info = asRecord(model);
|
|
663
|
+
return {
|
|
664
|
+
id: `${providerId}/${id}`,
|
|
665
|
+
name: typeof info?.name === 'string' ? info.name : id,
|
|
666
|
+
...(typeof info?.description === 'string' ? { description: info.description } : {}),
|
|
667
|
+
efforts: parseOpenCodeEfforts(info),
|
|
668
|
+
};
|
|
669
|
+
});
|
|
670
|
+
return items.length > 0 ? [{ id: providerId, name: typeof provider.name === 'string' ? provider.name : providerId, models: items }] : [];
|
|
671
|
+
});
|
|
672
|
+
return { groups, currentModel: null, currentEffort: null };
|
|
673
|
+
}
|
|
674
|
+
const providers = asRecord(root?.providers) ?? root;
|
|
675
|
+
const groups = [];
|
|
676
|
+
if (providers !== null)
|
|
677
|
+
for (const [providerId, raw] of Object.entries(providers)) {
|
|
678
|
+
const provider = asRecord(raw);
|
|
679
|
+
const models = asRecord(provider?.models) ?? (Array.isArray(raw) ? Object.fromEntries(raw.map((item) => [String(item), {}])) : null);
|
|
680
|
+
if (models === null)
|
|
681
|
+
continue;
|
|
682
|
+
const items = Object.entries(models).map(([id, model]) => {
|
|
683
|
+
const info = asRecord(model);
|
|
684
|
+
return {
|
|
685
|
+
id: `${providerId}/${id}`,
|
|
686
|
+
name: typeof info?.name === 'string' ? info.name : id,
|
|
687
|
+
...(typeof info?.description === 'string' ? { description: info.description } : {}),
|
|
688
|
+
efforts: parseOpenCodeEfforts(info),
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
if (items.length > 0)
|
|
692
|
+
groups.push({ id: providerId, name: providerId, models: items });
|
|
693
|
+
}
|
|
694
|
+
return { groups, currentModel: null, currentEffort: null };
|
|
695
|
+
}
|
|
696
|
+
function parseOpenCodeEfforts(value) {
|
|
697
|
+
if (value === null)
|
|
698
|
+
return [];
|
|
699
|
+
const variants = Array.isArray(value.variants)
|
|
700
|
+
? value.variants
|
|
701
|
+
: asRecord(value.variants) !== null ? Object.keys(value.variants) : [];
|
|
702
|
+
const allowed = new Set(['off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
703
|
+
return [...new Set(variants.flatMap((variant) => {
|
|
704
|
+
const variantRecord = asRecord(variant);
|
|
705
|
+
const raw = typeof variant === 'string' ? variant : variantRecord !== null ? variantRecord.id ?? variantRecord.value ?? variantRecord.name : null;
|
|
706
|
+
if (typeof raw !== 'string')
|
|
707
|
+
return [];
|
|
708
|
+
const normalized = raw.trim().toLowerCase();
|
|
709
|
+
return allowed.has(normalized) ? [normalized === 'none' ? 'off' : normalized] : [];
|
|
710
|
+
}))];
|
|
711
|
+
}
|
|
712
|
+
function asRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null; }
|
|
713
|
+
function isRecord(value) { return asRecord(value) !== null; }
|
|
714
|
+
function parseToolRecord(value) {
|
|
715
|
+
const record = asRecord(value);
|
|
716
|
+
if (record !== null)
|
|
717
|
+
return record;
|
|
718
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
719
|
+
return null;
|
|
720
|
+
try {
|
|
721
|
+
return asRecord(JSON.parse(value));
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function emptyCatalog() { return { groups: [], currentModel: null, currentEffort: null }; }
|
|
728
|
+
//# sourceMappingURL=opencode-driver.js.map
|