@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,114 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* 测试用内存实现。
|
|
5
|
+
* 不提供持久化或加密语义,不能用于生产;用于阶段 2 状态机单元测试。
|
|
6
|
+
*/
|
|
7
|
+
export class InMemoryCodingNsCredentialStore {
|
|
8
|
+
record = null;
|
|
9
|
+
async read() {
|
|
10
|
+
return this.record ? { ...this.record } : null;
|
|
11
|
+
}
|
|
12
|
+
async write(record) {
|
|
13
|
+
this.record = { ...record };
|
|
14
|
+
}
|
|
15
|
+
async clear() {
|
|
16
|
+
this.record = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Host 侧 refresh token 的文件存储;文件权限限制为当前用户可读写。 */
|
|
20
|
+
export class FileCodingNsCredentialStore {
|
|
21
|
+
filePath;
|
|
22
|
+
constructor(filePath) {
|
|
23
|
+
this.filePath = filePath;
|
|
24
|
+
if (!filePath.trim())
|
|
25
|
+
throw new TypeError('Codingns4DSH credential file path 不能为空');
|
|
26
|
+
}
|
|
27
|
+
async read() {
|
|
28
|
+
try {
|
|
29
|
+
const value = JSON.parse(await readFile(this.filePath, 'utf8'));
|
|
30
|
+
return parseHostCredential(value);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (isNodeError(error, 'ENOENT'))
|
|
34
|
+
return null;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async write(record) {
|
|
39
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
40
|
+
const temporary = `${this.filePath}.${process.pid}.tmp`;
|
|
41
|
+
await writeFile(temporary, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
42
|
+
await rename(temporary, this.filePath);
|
|
43
|
+
}
|
|
44
|
+
async clear() {
|
|
45
|
+
try {
|
|
46
|
+
const { unlink } = await import('node:fs/promises');
|
|
47
|
+
await unlink(this.filePath);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (!isNodeError(error, 'ENOENT'))
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class InMemoryDshDeviceCredentialStore {
|
|
56
|
+
record = null;
|
|
57
|
+
async read() { return this.record ? { ...this.record } : null; }
|
|
58
|
+
async write(record) { this.record = { ...record }; }
|
|
59
|
+
async clear() { this.record = null; }
|
|
60
|
+
}
|
|
61
|
+
export class FileDshDeviceCredentialStore {
|
|
62
|
+
filePath;
|
|
63
|
+
constructor(filePath) {
|
|
64
|
+
this.filePath = filePath;
|
|
65
|
+
if (!filePath.trim())
|
|
66
|
+
throw new TypeError('DSH device credential file path 不能为空');
|
|
67
|
+
}
|
|
68
|
+
async read() {
|
|
69
|
+
try {
|
|
70
|
+
const value = JSON.parse(await readFile(this.filePath, 'utf8'));
|
|
71
|
+
return parseDshCredential(value);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (isNodeError(error, 'ENOENT'))
|
|
75
|
+
return null;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async write(record) {
|
|
80
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
81
|
+
const temporary = `${this.filePath}.${process.pid}.tmp`;
|
|
82
|
+
await writeFile(temporary, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
83
|
+
await rename(temporary, this.filePath);
|
|
84
|
+
}
|
|
85
|
+
async clear() {
|
|
86
|
+
try {
|
|
87
|
+
const { unlink } = await import('node:fs/promises');
|
|
88
|
+
await unlink(this.filePath);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!isNodeError(error, 'ENOENT'))
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function parseHostCredential(value) {
|
|
97
|
+
if (!isRecord(value) || typeof value.controlBaseUrl !== 'string' || typeof value.accountId !== 'string' || typeof value.refreshToken !== 'string' || typeof value.refreshTokenExpiresAt !== 'string' || (typeof value.deviceId !== 'string' && value.deviceId !== null) || typeof value.savedAt !== 'string') {
|
|
98
|
+
throw new Error('Codingns4DSH credential 文件格式无效');
|
|
99
|
+
}
|
|
100
|
+
return { controlBaseUrl: value.controlBaseUrl, accountId: value.accountId, refreshToken: value.refreshToken, refreshTokenExpiresAt: value.refreshTokenExpiresAt, deviceId: value.deviceId, savedAt: value.savedAt };
|
|
101
|
+
}
|
|
102
|
+
function parseDshCredential(value) {
|
|
103
|
+
if (!isRecord(value) || typeof value.deviceId !== 'string' || typeof value.deviceCredential !== 'string' || typeof value.credentialVersion !== 'number' || !Number.isInteger(value.credentialVersion) || value.credentialVersion < 1 || typeof value.dtlsFingerprint !== 'string' || (typeof value.tunnelDomain !== 'string' && value.tunnelDomain !== null) || typeof value.displayName !== 'string' || typeof value.savedAt !== 'string') {
|
|
104
|
+
throw new Error('DSH device credential 文件格式无效');
|
|
105
|
+
}
|
|
106
|
+
return { deviceId: value.deviceId, deviceCredential: value.deviceCredential, credentialVersion: value.credentialVersion, dtlsFingerprint: value.dtlsFingerprint, tunnelDomain: value.tunnelDomain, displayName: value.displayName, savedAt: value.savedAt };
|
|
107
|
+
}
|
|
108
|
+
function isRecord(value) {
|
|
109
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
110
|
+
}
|
|
111
|
+
function isNodeError(error, code) {
|
|
112
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=credential-store.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { DebugConfig } from '../shared/contracts/debug-config.js';
|
|
2
|
+
import type { TerminalProcessInstance, TerminalProcessLaunchResult } from '../shared/contracts/terminal-process.js';
|
|
3
|
+
import type { TerminalProcessService } from './terminal/terminal-process-service.js';
|
|
4
|
+
export interface DebugPortProcess {
|
|
5
|
+
readonly pid: number;
|
|
6
|
+
readonly startToken: string;
|
|
7
|
+
readonly command: string | null;
|
|
8
|
+
readonly cwd: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface DebugPortInspector {
|
|
11
|
+
inspect(port: number): Promise<DebugPortProcess | null>;
|
|
12
|
+
terminate(process: DebugPortProcess): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface DebugProxyBinding {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly slug: string;
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readonly workspaceId: string;
|
|
19
|
+
readonly profileId: string;
|
|
20
|
+
readonly instanceId: string;
|
|
21
|
+
readonly port: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DebugWorkspaceServiceOptions {
|
|
24
|
+
readonly resolveWorkspaceRoot: (workspaceId: string) => string | null;
|
|
25
|
+
readonly terminalProcesses: TerminalProcessService;
|
|
26
|
+
readonly portInspector?: DebugPortInspector;
|
|
27
|
+
}
|
|
28
|
+
export interface DebugPortCheck {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly workspaceId: string;
|
|
31
|
+
readonly profileId: string;
|
|
32
|
+
readonly port: number;
|
|
33
|
+
readonly listening: boolean;
|
|
34
|
+
readonly process: DebugPortProcess | null;
|
|
35
|
+
readonly checkedAt: string;
|
|
36
|
+
}
|
|
37
|
+
/** Spec003 的最小 Host 服务:配置、PTY 启动、端口检查和代理绑定。 */
|
|
38
|
+
export declare class DebugWorkspaceService {
|
|
39
|
+
private readonly options;
|
|
40
|
+
private readonly portInspector;
|
|
41
|
+
private readonly checks;
|
|
42
|
+
private readonly bindings;
|
|
43
|
+
private readonly bindingProcesses;
|
|
44
|
+
constructor(options: DebugWorkspaceServiceOptions);
|
|
45
|
+
getConfig(workspaceId: string): Promise<DebugConfig>;
|
|
46
|
+
saveConfig(workspaceId: string, value: unknown): Promise<DebugConfig>;
|
|
47
|
+
/** 更新一个已有启动项;运行中的实例继续使用启动时的旧参数。 */
|
|
48
|
+
updateProfile(workspaceId: string, profileId: string, value: unknown): Promise<DebugConfig>;
|
|
49
|
+
/** 删除一个启动项;有活动实例时拒绝删除,避免留下无法管理的终端进程。 */
|
|
50
|
+
deleteProfile(workspaceId: string, profileId: string): Promise<DebugConfig>;
|
|
51
|
+
launch(input: {
|
|
52
|
+
workspaceId: string;
|
|
53
|
+
profileId: string;
|
|
54
|
+
dshSessionId?: string;
|
|
55
|
+
cols: number;
|
|
56
|
+
rows: number;
|
|
57
|
+
}): Promise<TerminalProcessLaunchResult>;
|
|
58
|
+
listInstances(workspaceId?: string): readonly TerminalProcessInstance[];
|
|
59
|
+
getInstance(instanceId: string): TerminalProcessInstance | undefined;
|
|
60
|
+
stop(instanceId: string): Promise<TerminalProcessInstance>;
|
|
61
|
+
checkPort(workspaceId: string, profileId: string): Promise<DebugPortCheck>;
|
|
62
|
+
terminatePort(workspaceId: string, checkId: string): Promise<DebugPortCheck>;
|
|
63
|
+
/** 只结束端口对应的业务进程,不触碰 tmux/ConPTY 终端运行时。 */
|
|
64
|
+
killPortProcess(workspaceId: string, checkId: string): Promise<DebugPortCheck>;
|
|
65
|
+
enableProxy(workspaceId: string, profileId: string, instanceId: string): Promise<DebugProxyBinding>;
|
|
66
|
+
disableProxy(workspaceId: string, bindingId: string): Promise<void>;
|
|
67
|
+
getProxy(bindingId: string): DebugProxyBinding | null;
|
|
68
|
+
/** 由插件自己的 Fetch 路由调用;不接受任意 Host、端口或 URL。 */
|
|
69
|
+
handleProxyRequest(request: Request): Promise<Response>;
|
|
70
|
+
private workspaceRoot;
|
|
71
|
+
private revokeBindings;
|
|
72
|
+
private configFilename;
|
|
73
|
+
}
|
|
74
|
+
/** Node 平台的最小端口观察器;具体系统命令只留在这一层。 */
|
|
75
|
+
export declare class NodeDebugPortInspector implements DebugPortInspector {
|
|
76
|
+
inspect(port: number): Promise<DebugPortProcess | null>;
|
|
77
|
+
terminate(processInfo: DebugPortProcess): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=debug.d.ts.map
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, isAbsolute, normalize } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { EMPTY_DEBUG_CONFIG, parseDebugConfig } from '../shared/contracts/debug-config.js';
|
|
6
|
+
/** Spec003 的最小 Host 服务:配置、PTY 启动、端口检查和代理绑定。 */
|
|
7
|
+
export class DebugWorkspaceService {
|
|
8
|
+
options;
|
|
9
|
+
portInspector;
|
|
10
|
+
checks = new Map();
|
|
11
|
+
bindings = new Map();
|
|
12
|
+
bindingProcesses = new Map();
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.portInspector = options.portInspector ?? new NodeDebugPortInspector();
|
|
16
|
+
}
|
|
17
|
+
async getConfig(workspaceId) {
|
|
18
|
+
const filename = this.configFilename(workspaceId);
|
|
19
|
+
try {
|
|
20
|
+
return parseDebugConfig(JSON.parse(await readFile(filename, 'utf8')));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (isMissingFile(error))
|
|
24
|
+
return EMPTY_DEBUG_CONFIG;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async saveConfig(workspaceId, value) {
|
|
29
|
+
const config = parseDebugConfig(value);
|
|
30
|
+
const filename = this.configFilename(workspaceId);
|
|
31
|
+
await mkdir(join(this.workspaceRoot(workspaceId), '.codingns'), { recursive: true, mode: 0o700 });
|
|
32
|
+
const temporary = `${filename}.${process.pid}.tmp`;
|
|
33
|
+
await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
34
|
+
await rename(temporary, filename);
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
/** 更新一个已有启动项;运行中的实例继续使用启动时的旧参数。 */
|
|
38
|
+
async updateProfile(workspaceId, profileId, value) {
|
|
39
|
+
const current = await this.getConfig(workspaceId);
|
|
40
|
+
if (!current.profiles.some((profile) => profile.id === profileId))
|
|
41
|
+
throw new Error(`Spec003 配置项不存在: ${profileId}`);
|
|
42
|
+
const parsed = parseDebugConfig({ version: 1, profiles: [value] });
|
|
43
|
+
const profile = parsed.profiles[0];
|
|
44
|
+
if (profile === undefined || profile.id !== profileId)
|
|
45
|
+
throw new Error('更新配置项 ID 不匹配');
|
|
46
|
+
return this.saveConfig(workspaceId, {
|
|
47
|
+
version: 1,
|
|
48
|
+
profiles: current.profiles.map((item) => item.id === profileId ? profile : item),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** 删除一个启动项;有活动实例时拒绝删除,避免留下无法管理的终端进程。 */
|
|
52
|
+
async deleteProfile(workspaceId, profileId) {
|
|
53
|
+
const current = await this.getConfig(workspaceId);
|
|
54
|
+
if (!current.profiles.some((profile) => profile.id === profileId))
|
|
55
|
+
throw new Error(`Spec003 配置项不存在: ${profileId}`);
|
|
56
|
+
if (this.options.terminalProcesses.listInstances(workspaceId).some((instance) => instance.profileId === profileId && isActive(instance.state))) {
|
|
57
|
+
throw new Error('配置项仍有活动进程,请先停止后再删除');
|
|
58
|
+
}
|
|
59
|
+
await this.options.terminalProcesses.deleteProfile(workspaceId, profileId);
|
|
60
|
+
return this.saveConfig(workspaceId, { version: 1, profiles: current.profiles.filter((profile) => profile.id !== profileId) });
|
|
61
|
+
}
|
|
62
|
+
async launch(input) {
|
|
63
|
+
const config = await this.getConfig(input.workspaceId);
|
|
64
|
+
const profile = findProfile(config, input.profileId);
|
|
65
|
+
await this.options.terminalProcesses.createProfile({
|
|
66
|
+
id: profile.id,
|
|
67
|
+
workspaceId: input.workspaceId,
|
|
68
|
+
name: profile.name,
|
|
69
|
+
cwdRelative: profile.cwdRelative,
|
|
70
|
+
command: profile.command,
|
|
71
|
+
args: profile.args,
|
|
72
|
+
env: profile.env,
|
|
73
|
+
shell: profile.shell,
|
|
74
|
+
runtimeType: profile.runtimeType,
|
|
75
|
+
runtimeMode: 'pty',
|
|
76
|
+
});
|
|
77
|
+
return this.options.terminalProcesses.launch({
|
|
78
|
+
workspaceId: input.workspaceId,
|
|
79
|
+
profileId: profile.id,
|
|
80
|
+
...(input.dshSessionId === undefined ? {} : { dshSessionId: input.dshSessionId }),
|
|
81
|
+
cols: input.cols,
|
|
82
|
+
rows: input.rows,
|
|
83
|
+
commandMode: 'shell-input',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
listInstances(workspaceId) { return this.options.terminalProcesses.listInstances(workspaceId); }
|
|
87
|
+
getInstance(instanceId) { return this.options.terminalProcesses.getInstance(instanceId); }
|
|
88
|
+
async stop(instanceId) {
|
|
89
|
+
const stopped = await this.options.terminalProcesses.stop(instanceId);
|
|
90
|
+
for (const binding of this.bindings.values()) {
|
|
91
|
+
if (binding.instanceId === instanceId) {
|
|
92
|
+
this.bindings.delete(binding.id);
|
|
93
|
+
this.bindingProcesses.delete(binding.id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return stopped;
|
|
97
|
+
}
|
|
98
|
+
async checkPort(workspaceId, profileId) {
|
|
99
|
+
const profile = findProfile(await this.getConfig(workspaceId), profileId);
|
|
100
|
+
if (profile.port === null)
|
|
101
|
+
throw new Error('配置项没有声明端口');
|
|
102
|
+
const process = await this.portInspector.inspect(profile.port);
|
|
103
|
+
const check = {
|
|
104
|
+
id: randomUUID(), workspaceId, profileId, port: profile.port,
|
|
105
|
+
listening: process !== null, process, checkedAt: new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
this.checks.set(check.id, check);
|
|
108
|
+
return check;
|
|
109
|
+
}
|
|
110
|
+
async terminatePort(workspaceId, checkId) {
|
|
111
|
+
return this.killPortProcess(workspaceId, checkId);
|
|
112
|
+
}
|
|
113
|
+
/** 只结束端口对应的业务进程,不触碰 tmux/ConPTY 终端运行时。 */
|
|
114
|
+
async killPortProcess(workspaceId, checkId) {
|
|
115
|
+
const check = this.checks.get(checkId);
|
|
116
|
+
if (check === undefined || check.workspaceId !== workspaceId)
|
|
117
|
+
throw new Error('端口检查结果不存在或已失效');
|
|
118
|
+
if (Date.now() - Date.parse(check.checkedAt) > 60_000) {
|
|
119
|
+
this.checks.delete(checkId);
|
|
120
|
+
throw new Error('端口检查结果已过期,请重新检查');
|
|
121
|
+
}
|
|
122
|
+
const current = await this.portInspector.inspect(check.port);
|
|
123
|
+
if (!sameProcess(check.process, current))
|
|
124
|
+
throw new Error('端口监听进程已变化,请重新检查');
|
|
125
|
+
if (current === null) {
|
|
126
|
+
this.checks.delete(checkId);
|
|
127
|
+
return { ...check, listening: false, process: null, checkedAt: new Date().toISOString() };
|
|
128
|
+
}
|
|
129
|
+
await this.portInspector.terminate(current);
|
|
130
|
+
this.checks.delete(checkId);
|
|
131
|
+
this.revokeBindings(workspaceId, check.profileId, check.port);
|
|
132
|
+
return { ...check, listening: false, process: null, checkedAt: new Date().toISOString() };
|
|
133
|
+
}
|
|
134
|
+
async enableProxy(workspaceId, profileId, instanceId) {
|
|
135
|
+
const profile = findProfile(await this.getConfig(workspaceId), profileId);
|
|
136
|
+
if (!profile.proxy.enabled || profile.port === null)
|
|
137
|
+
throw new Error('配置项未启用代理或没有端口');
|
|
138
|
+
const instance = this.getInstance(instanceId);
|
|
139
|
+
if (instance === undefined || instance.workspaceId !== workspaceId || instance.profileId !== profileId || !isActive(instance.state))
|
|
140
|
+
throw new Error('运行实例不存在或未运行');
|
|
141
|
+
const process = await this.portInspector.inspect(profile.port);
|
|
142
|
+
if (process === null)
|
|
143
|
+
throw new Error('配置端口当前未监听');
|
|
144
|
+
if (instance.pid !== null && process.pid !== instance.pid)
|
|
145
|
+
throw new Error('配置端口未由当前运行实例监听');
|
|
146
|
+
const slug = randomUUID().replaceAll('-', '');
|
|
147
|
+
const binding = {
|
|
148
|
+
id: randomUUID(), slug, url: `/api/codingns/debug-proxy?slug=${encodeURIComponent(slug)}`,
|
|
149
|
+
workspaceId, profileId, instanceId, port: profile.port,
|
|
150
|
+
};
|
|
151
|
+
this.bindings.set(binding.id, binding);
|
|
152
|
+
this.bindingProcesses.set(binding.id, process);
|
|
153
|
+
return binding;
|
|
154
|
+
}
|
|
155
|
+
async disableProxy(workspaceId, bindingId) {
|
|
156
|
+
const binding = this.bindings.get(bindingId) ?? null;
|
|
157
|
+
if (binding === null || binding.workspaceId !== workspaceId)
|
|
158
|
+
throw new Error('代理绑定不存在');
|
|
159
|
+
this.bindings.delete(bindingId);
|
|
160
|
+
this.bindingProcesses.delete(bindingId);
|
|
161
|
+
}
|
|
162
|
+
getProxy(bindingId) { return this.bindings.get(bindingId) ?? null; }
|
|
163
|
+
/** 由插件自己的 Fetch 路由调用;不接受任意 Host、端口或 URL。 */
|
|
164
|
+
async handleProxyRequest(request) {
|
|
165
|
+
const requestUrl = new URL(request.url);
|
|
166
|
+
const slug = requestUrl.searchParams.get('slug');
|
|
167
|
+
const binding = slug === null ? undefined : [...this.bindings.values()].find((item) => item.slug === slug);
|
|
168
|
+
if (binding === undefined)
|
|
169
|
+
return new Response('代理绑定不存在', { status: 404 });
|
|
170
|
+
if (isUpgradeRequest(request.headers))
|
|
171
|
+
return new Response('WebSocket 代理未由 DSH Fetch 接口提供', { status: 501 });
|
|
172
|
+
const instance = this.getInstance(binding.instanceId);
|
|
173
|
+
if (instance === undefined || instance.workspaceId !== binding.workspaceId || !isActive(instance.state)) {
|
|
174
|
+
this.bindings.delete(binding.id);
|
|
175
|
+
this.bindingProcesses.delete(binding.id);
|
|
176
|
+
return new Response('运行实例已停止', { status: 404 });
|
|
177
|
+
}
|
|
178
|
+
const process = await this.portInspector.inspect(binding.port);
|
|
179
|
+
const expectedProcess = this.bindingProcesses.get(binding.id);
|
|
180
|
+
if (process === null || expectedProcess === undefined || !sameProcess(expectedProcess, process) || (instance.pid !== null && process.pid !== instance.pid)) {
|
|
181
|
+
this.bindings.delete(binding.id);
|
|
182
|
+
this.bindingProcesses.delete(binding.id);
|
|
183
|
+
return new Response('端口监听身份已变化', { status: 404 });
|
|
184
|
+
}
|
|
185
|
+
const targetPath = requestUrl.searchParams.get('path') ?? '/';
|
|
186
|
+
let parsedPath;
|
|
187
|
+
try {
|
|
188
|
+
parsedPath = new URL(targetPath, 'http://127.0.0.1');
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return new Response('代理路径非法', { status: 400 });
|
|
192
|
+
}
|
|
193
|
+
if (parsedPath.origin !== 'http://127.0.0.1' || !parsedPath.pathname.startsWith('/'))
|
|
194
|
+
return new Response('代理路径非法', { status: 400 });
|
|
195
|
+
const upstreamHeaders = new Headers();
|
|
196
|
+
request.headers.forEach((value, key) => { if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase()) && key.toLowerCase() !== 'host')
|
|
197
|
+
upstreamHeaders.set(key, value); });
|
|
198
|
+
const init = { method: request.method, headers: upstreamHeaders, redirect: 'manual', signal: request.signal };
|
|
199
|
+
if (request.method !== 'GET' && request.method !== 'HEAD')
|
|
200
|
+
init.body = await request.arrayBuffer();
|
|
201
|
+
let response;
|
|
202
|
+
try {
|
|
203
|
+
response = await fetch(`http://127.0.0.1:${binding.port}${parsedPath.pathname}${parsedPath.search}`, init);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return new Response(error instanceof Error ? error.message : '上游服务不可达', { status: 502 });
|
|
207
|
+
}
|
|
208
|
+
const responseHeaders = new Headers();
|
|
209
|
+
response.headers.forEach((value, key) => { if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase()))
|
|
210
|
+
responseHeaders.set(key, value); });
|
|
211
|
+
const location = response.headers.get('location');
|
|
212
|
+
if (location !== null)
|
|
213
|
+
responseHeaders.set('location', rewriteLocation(location, binding.url));
|
|
214
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers: responseHeaders });
|
|
215
|
+
}
|
|
216
|
+
workspaceRoot(workspaceId) {
|
|
217
|
+
const root = this.options.resolveWorkspaceRoot(workspaceId);
|
|
218
|
+
if (root === null || !isAbsolute(root))
|
|
219
|
+
throw new Error('Workspace 根目录不可用');
|
|
220
|
+
return normalize(root);
|
|
221
|
+
}
|
|
222
|
+
revokeBindings(workspaceId, profileId, port) {
|
|
223
|
+
for (const binding of this.bindings.values()) {
|
|
224
|
+
if (binding.workspaceId !== workspaceId || binding.profileId !== profileId || binding.port !== port)
|
|
225
|
+
continue;
|
|
226
|
+
this.bindings.delete(binding.id);
|
|
227
|
+
this.bindingProcesses.delete(binding.id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
configFilename(workspaceId) { return join(this.workspaceRoot(workspaceId), '.codingns', 'debug.json'); }
|
|
231
|
+
}
|
|
232
|
+
const HOP_BY_HOP_HEADERS = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade']);
|
|
233
|
+
function isUpgradeRequest(headers) { return headers.get('upgrade')?.toLowerCase() === 'websocket'; }
|
|
234
|
+
function rewriteLocation(location, proxyUrl) {
|
|
235
|
+
try {
|
|
236
|
+
const parsed = new URL(location, 'http://127.0.0.1');
|
|
237
|
+
if (parsed.hostname !== '127.0.0.1' && parsed.hostname !== 'localhost')
|
|
238
|
+
return location;
|
|
239
|
+
return `${proxyUrl}&path=${encodeURIComponent(`${parsed.pathname}${parsed.search}`)}`;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return location;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Node 平台的最小端口观察器;具体系统命令只留在这一层。 */
|
|
246
|
+
export class NodeDebugPortInspector {
|
|
247
|
+
async inspect(port) {
|
|
248
|
+
const result = process.platform === 'win32'
|
|
249
|
+
? spawnSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf8', windowsHide: true })
|
|
250
|
+
: spawnSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpct'], { encoding: 'utf8' });
|
|
251
|
+
if (result.status !== 0 && result.stdout.trim() === '')
|
|
252
|
+
return null;
|
|
253
|
+
const pid = process.platform === 'win32' ? parseWindowsPid(result.stdout, port) : parsePosixPid(result.stdout);
|
|
254
|
+
if (pid === null)
|
|
255
|
+
return null;
|
|
256
|
+
const details = spawnSync(process.platform === 'win32' ? 'powershell' : 'ps', process.platform === 'win32'
|
|
257
|
+
? ['-NoProfile', '-Command', `(Get-Process -Id ${pid}).Path`]
|
|
258
|
+
: ['-o', 'lstart=,command=', '-p', String(pid)], { encoding: 'utf8', windowsHide: true });
|
|
259
|
+
const lines = details.stdout.trim().split(/\s{2,}|\n/u).map((line) => line.trim()).filter(Boolean);
|
|
260
|
+
const startToken = lines[0] ?? '';
|
|
261
|
+
if (startToken === '')
|
|
262
|
+
return null;
|
|
263
|
+
return { pid, startToken, command: lines.slice(1).join(' ') || null, cwd: null };
|
|
264
|
+
}
|
|
265
|
+
async terminate(processInfo) {
|
|
266
|
+
if (processInfo.startToken.trim() === '')
|
|
267
|
+
throw new Error('无法确认进程启动身份');
|
|
268
|
+
process.kill(processInfo.pid, 'SIGTERM');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function findProfile(config, profileId) {
|
|
272
|
+
const profile = config.profiles.find((item) => item.id === profileId);
|
|
273
|
+
if (profile === undefined)
|
|
274
|
+
throw new Error(`Spec003 配置项不存在: ${profileId}`);
|
|
275
|
+
return profile;
|
|
276
|
+
}
|
|
277
|
+
function sameProcess(left, right) {
|
|
278
|
+
return left === null ? right === null : right !== null && left.pid === right.pid && left.startToken === right.startToken;
|
|
279
|
+
}
|
|
280
|
+
function isActive(state) { return state === 'starting' || state === 'running' || state === 'stopping'; }
|
|
281
|
+
function isMissingFile(error) { return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT'; }
|
|
282
|
+
function parsePosixPid(output) {
|
|
283
|
+
const match = output.match(/(?:^|\n)p(\d+)(?:\n|$)/u);
|
|
284
|
+
return match?.[1] === undefined ? null : Number(match[1]);
|
|
285
|
+
}
|
|
286
|
+
function parseWindowsPid(output, port) {
|
|
287
|
+
const suffix = `:${port}`;
|
|
288
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
289
|
+
const fields = line.trim().split(/\s+/u);
|
|
290
|
+
if (fields[1]?.endsWith(suffix) && fields[3] === 'LISTENING')
|
|
291
|
+
return Number(fields[4]) || null;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=debug.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FeatureResourceScope } from '../shared/contracts/feature.js';
|
|
2
|
+
import type { DshDeviceCredentialRecord, DshDeviceSummary } from '../shared/contracts/dsh-device.js';
|
|
3
|
+
import type { CodingNsControlApiClient } from './control-api-client.js';
|
|
4
|
+
import { type DshDeviceCredentialStore } from './credential-store.js';
|
|
5
|
+
import { startHostRelayRuntime, type HostDtlsIdentityStore, type HostRelayRuntime } from './relay-tunnel-runtime.js';
|
|
6
|
+
import type { DshGatewayFeature } from '../transport/dsh-gateway.js';
|
|
7
|
+
import type { DshTransportDebugLogger } from '../transport/debug.js';
|
|
8
|
+
export interface DshHostDeviceRuntimeOptions {
|
|
9
|
+
readonly controlClient: Pick<CodingNsControlApiClient, 'registerDshDevice' | 'listDshDevices' | 'heartbeatDshDevice' | 'createDshRelayTicket'>;
|
|
10
|
+
readonly accessToken: string;
|
|
11
|
+
/** 认证续期后动态读取最新 access token;未提供时沿用 accessToken。 */
|
|
12
|
+
readonly accessTokenProvider?: () => string | null;
|
|
13
|
+
readonly credentialStore?: DshDeviceCredentialStore;
|
|
14
|
+
readonly dtlsStore?: HostDtlsIdentityStore;
|
|
15
|
+
readonly displayName?: string;
|
|
16
|
+
readonly protocolVersion?: string;
|
|
17
|
+
/** 当前 DSH 宿主的真实版本,用于 Relay Transport 握手。 */
|
|
18
|
+
readonly dshVersion?: string;
|
|
19
|
+
readonly capabilities?: readonly string[];
|
|
20
|
+
readonly resources?: Pick<FeatureResourceScope, 'add'>;
|
|
21
|
+
readonly signalingSocketFactory?: Parameters<typeof startHostRelayRuntime>[0]['signalingSocketFactory'];
|
|
22
|
+
readonly peerConnectionFactory?: Parameters<typeof startHostRelayRuntime>[0]['peerConnectionFactory'];
|
|
23
|
+
readonly gatewayFeatures?: readonly DshGatewayFeature[];
|
|
24
|
+
readonly heartbeatIntervalMs?: number;
|
|
25
|
+
readonly onRuntime?: (runtime: HostRelayRuntime) => void | Promise<void>;
|
|
26
|
+
readonly debug?: DshTransportDebugLogger;
|
|
27
|
+
}
|
|
28
|
+
export interface DshHostDeviceRuntime {
|
|
29
|
+
readonly credential: DshDeviceCredentialRecord;
|
|
30
|
+
readonly device: DshDeviceSummary;
|
|
31
|
+
readonly runtime: HostRelayRuntime;
|
|
32
|
+
stop(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 注册一个独立的 DSH 设备并启动 Host Relay runtime。
|
|
36
|
+
* 这里不读取或写入 Codingns4DSH binding,设备凭据也与 refresh token 分文件保存。
|
|
37
|
+
*/
|
|
38
|
+
export declare function startDshHostDeviceRuntime(options: DshHostDeviceRuntimeOptions): Promise<DshHostDeviceRuntime>;
|
|
39
|
+
//# sourceMappingURL=dsh-device-runtime.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { FileDshDeviceCredentialStore, } from './credential-store.js';
|
|
4
|
+
import { ensureHostDtlsIdentity, FileHostDtlsIdentityStore, startHostRelayRuntime, } from './relay-tunnel-runtime.js';
|
|
5
|
+
/**
|
|
6
|
+
* 注册一个独立的 DSH 设备并启动 Host Relay runtime。
|
|
7
|
+
* 这里不读取或写入 Codingns4DSH binding,设备凭据也与 refresh token 分文件保存。
|
|
8
|
+
*/
|
|
9
|
+
export async function startDshHostDeviceRuntime(options) {
|
|
10
|
+
if (!options.accessToken.trim())
|
|
11
|
+
throw new TypeError('DSH Host accessToken 不能为空');
|
|
12
|
+
const getAccessToken = () => {
|
|
13
|
+
const token = options.accessTokenProvider === undefined ? options.accessToken : options.accessTokenProvider();
|
|
14
|
+
if (typeof token !== 'string' || !token.trim())
|
|
15
|
+
throw new Error('DSH Host accessToken 已失效');
|
|
16
|
+
return token;
|
|
17
|
+
};
|
|
18
|
+
const credentialStore = options.credentialStore ?? new FileDshDeviceCredentialStore(defaultDshCredentialPath());
|
|
19
|
+
const dtlsStore = options.dtlsStore ?? new FileHostDtlsIdentityStore(defaultDtlsPath());
|
|
20
|
+
const identity = await ensureHostDtlsIdentity(dtlsStore);
|
|
21
|
+
let credential = await credentialStore.read();
|
|
22
|
+
let device;
|
|
23
|
+
if (credential === null || credential.dtlsFingerprint !== identity.fingerprint) {
|
|
24
|
+
const request = {
|
|
25
|
+
displayName: options.displayName?.trim() || 'DSH Host',
|
|
26
|
+
devicePublicKey: identity.certPem,
|
|
27
|
+
dtlsFingerprint: identity.fingerprint,
|
|
28
|
+
protocolVersion: options.protocolVersion ?? 'dsh-envelope-v1',
|
|
29
|
+
capabilities: [...(options.capabilities ?? ['rpc', 'pty', 'file', 'web'])],
|
|
30
|
+
};
|
|
31
|
+
const registered = await options.controlClient.registerDshDevice(getAccessToken(), request);
|
|
32
|
+
credential = {
|
|
33
|
+
deviceId: registered.device.dshDeviceId ?? registered.device.deviceId ?? (() => { throw new Error('DSH 注册响应缺少设备标识'); })(),
|
|
34
|
+
deviceCredential: registered.deviceCredential,
|
|
35
|
+
credentialVersion: registered.credentialVersion,
|
|
36
|
+
dtlsFingerprint: identity.fingerprint,
|
|
37
|
+
tunnelDomain: registered.device.tunnelDomain ?? null,
|
|
38
|
+
displayName: registered.device.displayName,
|
|
39
|
+
savedAt: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
await credentialStore.write(credential);
|
|
42
|
+
device = registered.device;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const listed = await options.controlClient.listDshDevices(getAccessToken());
|
|
46
|
+
device = listed.devices.find((candidate) => (candidate.dshDeviceId ?? candidate.deviceId) === credential.deviceId) ?? {
|
|
47
|
+
dshDeviceId: credential.deviceId,
|
|
48
|
+
deviceId: credential.deviceId,
|
|
49
|
+
displayName: credential.displayName,
|
|
50
|
+
protocolVersion: options.protocolVersion ?? 'dsh-envelope-v1',
|
|
51
|
+
capabilities: [...(options.capabilities ?? [])],
|
|
52
|
+
dtlsFingerprint: credential.dtlsFingerprint,
|
|
53
|
+
...(credential.tunnelDomain === null ? {} : { tunnelDomain: credential.tunnelDomain }),
|
|
54
|
+
status: 'active',
|
|
55
|
+
online: false,
|
|
56
|
+
lastHeartbeatAt: null,
|
|
57
|
+
createdAt: credential.savedAt,
|
|
58
|
+
updatedAt: credential.savedAt,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (credential === null)
|
|
62
|
+
throw new Error('DSH 设备凭据初始化失败');
|
|
63
|
+
const savedCredential = credential;
|
|
64
|
+
await options.controlClient.heartbeatDshDevice(getAccessToken(), savedCredential.deviceId, savedCredential.deviceCredential);
|
|
65
|
+
const runtimeOptions = {
|
|
66
|
+
controlClient: { createSignalingTicket: async () => { throw new Error('DSH runtime 必须使用 DSH ticket'); } },
|
|
67
|
+
createTicket: ({ identity: material, credentialVersion }) => requestDshTicket(options.controlClient, getAccessToken(), savedCredential, material.fingerprint, credentialVersion),
|
|
68
|
+
accessToken: options.accessToken,
|
|
69
|
+
bindingId: savedCredential.deviceId,
|
|
70
|
+
credentialVersion: savedCredential.credentialVersion,
|
|
71
|
+
dtlsStore,
|
|
72
|
+
hostId: savedCredential.deviceId,
|
|
73
|
+
...(options.resources === undefined ? {} : { resources: options.resources }),
|
|
74
|
+
...(options.signalingSocketFactory === undefined ? {} : { signalingSocketFactory: options.signalingSocketFactory }),
|
|
75
|
+
...(options.peerConnectionFactory === undefined ? {} : { peerConnectionFactory: options.peerConnectionFactory }),
|
|
76
|
+
...(options.gatewayFeatures === undefined ? {} : { gatewayFeatures: options.gatewayFeatures }),
|
|
77
|
+
...(options.dshVersion === undefined ? {} : { dshVersion: options.dshVersion }),
|
|
78
|
+
...(options.debug === undefined ? {} : { debug: options.debug }),
|
|
79
|
+
};
|
|
80
|
+
const runtime = await startHostRelayRuntime(runtimeOptions);
|
|
81
|
+
let stopped = false;
|
|
82
|
+
const interval = options.heartbeatIntervalMs === 0 ? null : setInterval(() => {
|
|
83
|
+
if (stopped)
|
|
84
|
+
return;
|
|
85
|
+
void Promise.resolve()
|
|
86
|
+
.then(() => options.controlClient.heartbeatDshDevice(getAccessToken(), savedCredential.deviceId, savedCredential.deviceCredential))
|
|
87
|
+
.catch(() => undefined);
|
|
88
|
+
}, options.heartbeatIntervalMs ?? 30_000);
|
|
89
|
+
const stop = async () => {
|
|
90
|
+
if (stopped)
|
|
91
|
+
return;
|
|
92
|
+
stopped = true;
|
|
93
|
+
if (interval !== null)
|
|
94
|
+
clearInterval(interval);
|
|
95
|
+
await runtime.close();
|
|
96
|
+
};
|
|
97
|
+
options.resources?.add(stop);
|
|
98
|
+
await options.onRuntime?.(runtime);
|
|
99
|
+
return { credential: savedCredential, device, runtime, stop };
|
|
100
|
+
}
|
|
101
|
+
async function requestDshTicket(client, accessToken, credential, fingerprint, credentialVersion) {
|
|
102
|
+
const response = await client.createDshRelayTicket(accessToken, {
|
|
103
|
+
dshDeviceId: credential.deviceId,
|
|
104
|
+
deviceCredential: credential.deviceCredential,
|
|
105
|
+
hostDtlsFingerprint: fingerprint,
|
|
106
|
+
credentialVersion: credentialVersion ?? credential.credentialVersion,
|
|
107
|
+
role: 'host',
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
...response,
|
|
111
|
+
bindingId: response.dshDeviceId,
|
|
112
|
+
tunnelDomain: response.tunnelDomain ?? response.dshDeviceId,
|
|
113
|
+
credentialVersion: response.credentialVersion,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function defaultDshCredentialPath() { return join(homedir(), '.config', 'codingns4dsh', 'device-credential.json'); }
|
|
117
|
+
function defaultDtlsPath() { return join(homedir(), '.config', 'codingns4dsh', 'dtls-identity.json'); }
|
|
118
|
+
//# sourceMappingURL=dsh-device-runtime.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CodingNsRpcTable } from './rpc-table.js';
|
|
2
|
+
import type { DshGatewayFeature } from '../transport/dsh-gateway.js';
|
|
3
|
+
/** 将 Host 已登记的 RPC 命名空间接到 DSH Gateway。 */
|
|
4
|
+
export declare function createDshRpcGatewayFeature(rpc: CodingNsRpcTable): DshGatewayFeature;
|
|
5
|
+
//# sourceMappingURL=dsh-gateway-feature.d.ts.map
|