@qzsy/dsh-bridge 0.0.0-stage → 3.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # 更新日志
2
+
3
+ ## [v3.0.0] - 2026-11-01
4
+
5
+ > DSH 远程隧道云官方客户端插件的**首个独立实现版本**。包内所有代码均为本仓库自研,不包含或依赖任何第三方 dsh-bridge 代码。
6
+
7
+ ### ✨ 新增
8
+
9
+ - **插件骨架**:cordis 插件(id/name `qzsy-dsh-bridge`)、RPC 通道 `/qzsy-dsh-bridge`、设置页条目与 provide 服务均使用独立命名;数据目录 `~/.dsh/qzsy-dsh-bridge`,默认本地端口 3092(可配置)。
10
+ - **本地门禁代理**:监听本机端口转发 DSH 本体并注入 DSH 原生 loopback 会话 cookie;回环直连免认证;隧道来源使用进程内随机密钥 `x-dsh-internal-tunnel` 标记;局域网来源需本地口令会话;`/__qzsy_bridge__/loopback-token` 仅对物理回环签发,隧道来源 403。
11
+ - **SaaS 隧道客户端**:自研 v2 控制通道(hello / hello-ack、heartbeat / heartbeat-ack、route-update、drain、request/response、response-start|chunk|end、ws-open/accept/frame/close),支持重连退避与 4001/4003/4004 终态。
12
+ - **设置面板**:状态、网关地址、设备码获取与绑定、解绑、隧道启停、公网地址(host/path)复制、二维码、在线态与最近错误;中英文词典。
13
+ - **测试**:node:test 覆盖协议握手/心跳/转发/SSE/WebSocket/吊销/drain/重连、门禁三类来源、打包身份与全包无第三方残留扫描。
14
+
15
+ ### ⚠️ 已知限制
16
+
17
+ - `getStatus` 免 admin 引导但只返回脱敏状态;adminToken 进程级随机、仅物理回环可领;登录页暂无速率限制;`access.token` 固定可复用(泄露后解绑重绑轮换设备密钥)。
18
+
19
+ ### 🔧 工程
20
+
21
+ - 包内不包含 NOTICE / 上游版权文件;LICENSE 为本仓库(qzsy)MIT。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qzsy (DSH 远程隧道云)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,106 @@
1
- # Temporary Holding Version
1
+ # DSH 远程隧道云 · 官方客户端插件
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **@qzsy/dsh-bridge** 是 **DSH 远程隧道云**的官方客户端插件:一条命令把本机 DeepSeek Harness 接入云端隧道,在手机、平板或任意设备上安全远程使用。
4
+
5
+ - 独立实现:包内所有代码均为本仓库自研(MIT,版权见 LICENSE),不依赖、不包含任何第三方 dsh 隧道插件。
6
+ - 独立身份:插件 id/name `qzsy-dsh-bridge`、RPC 通道 `/qzsy-dsh-bridge`、设置项与 provide 服务均为本插件命名;数据目录 `~/.dsh/qzsy-dsh-bridge/`,默认本地端口 `3092`(可配置)。
7
+ - 轻量:只做 SaaS 隧道 + 本地门禁代理 + 设置面板,不包含 IM 机器人、Cloudflare、自建隧道、多网卡等额外功能。
8
+
9
+ ## 安装
10
+
11
+ `<name>` 换成你的 DSH profile(如 `web` / `desktop`):
12
+
13
+ ```bash
14
+ dsh plugin --profile <name> add @qzsy/dsh-bridge
15
+ ```
16
+
17
+ 安装后在 **设置 → 远程访问** 打开面板(插件 id 为 `qzsy-dsh-bridge`,DSH 组件列表会显示本插件而非其它隧道插件)。
18
+
19
+ ## 绑定设备(接入 DSH 远程隧道云)
20
+
21
+ 0. **首次使用**:在面板设置访问口令(远程访客的门禁凭据)。
22
+ 1. 面板中确认 **SaaS 网关地址**:官方云为
23
+ `wss://dsh.b.nps.qzsyzn.com/_gw/connect`(自建部署填自己的网关地址),点「保存网关」。
24
+ 2. 点「获取设备码」,面板显示 8 位设备码(10 分钟内有效)。
25
+ 3. 打开 [DSH 远程隧道云控制台](https://dsh.b.nps.qzsyzn.com/user/tunnels) →「添加设备」,
26
+ 输入设备码并确认。
27
+ 4. 面板会自动轮询并在批准后领取设备密钥;显示「已绑定」后点「开启隧道」。
28
+ 5. 公网地址出现后,可一键复制子域地址 / 路径地址,或扫码打开。
29
+
30
+ 绑定信息(`deviceId` / `deviceSecret`)只保存在本机
31
+ `~/.dsh/qzsy-dsh-bridge/config.json`(权限 0600);面板只显示掩码。解绑会清除本机凭证,
32
+ 设备本身需在控制台吊销。
33
+
34
+ ## 公网地址形态
35
+
36
+ - **Host(主)**:`https://<publicId>.b.nps.qzsyzn.com/` —— 多租户生产形态,根路径资源天然隔离。
37
+ - **Path(兼容)**:`https://<平台域名>/<prefix>/t/<publicId>/...` —— 单隧道演示/无泛域名环境。
38
+
39
+ 面板会把 Host 路由(存在时)显示为主地址,并同时列出 Path 路由。
40
+
41
+ ## 本地门禁与安全语义
42
+
43
+ 插件会在本机监听一个代理端口(默认 `3092`),把请求转发给 DSH 本体并注入 DSH 原生
44
+ loopback 会话 cookie。来源判定:
45
+
46
+ | 来源 | 判定 | 结果 |
47
+ | --- | --- | --- |
48
+ | 物理本机回环 | 连接来自 `127.0.0.1` / `::1` 且未带隧道标记 | 免认证,享有本机特权 |
49
+ | SaaS 隧道 | 携带插件隧道客户端注入的 `x-dsh-internal-tunnel`(进程内随机密钥,公网不可伪造) | 仅代表来源是隧道;访客仍必须通过本地口令 / `?auth=` 会话,否则 401 |
50
+ | 局域网 / 其它直连 | 非回环且无有效隧道标记 | 必须通过本地口令登录或 `?auth=` 令牌换取会话,否则 401 |
51
+
52
+ - `/__qzsy_bridge__/loopback-token` 只对物理回环签发;经隧道访问一律 **403**。
53
+ - **首次使用必须在面板设置访问口令**(至少 6 位);未设置时所有远程/隧道访客都会被拒绝并返回初始化引导页。
54
+ - 面板「带访问令牌的链接」包含固定 `?auth=` 令牌,首次打开后换取 30 天会话 cookie;令牌可重复使用,请只发给可信设备,泄露后请在控制台吊销设备并解绑重绑。
55
+ - 隧道标记密钥每次进程启动重新生成,不落盘。
56
+
57
+ ## 配置
58
+
59
+ 配置文件:`~/.dsh/qzsy-dsh-bridge/config.json`
60
+
61
+ | 字段 | 默认 | 说明 |
62
+ | --- | --- | --- |
63
+ | `port` | `3092` | 本地门禁代理端口 |
64
+ | `gatewayUrl` | `wss://dsh.b.nps.qzsyzn.com/_gw/connect` | SaaS 网关地址(也接受 http(s)) |
65
+ | `autoStart` | `false` | 随 DSH 启动自动开启隧道 |
66
+ | `deviceId` / `deviceSecret` | — | 绑定后写入,勿手工复制到其它设备 |
67
+ | `tunnel` / `routesVersion` | — | 云端下发的只读缓存 |
68
+ | `access.token` / `access.sessionSecret` | 自动生成 | 本地访问令牌与会话签名密钥 |
69
+
70
+ 也支持 cordis 配置(见 `cordis.example.yml`):`port`、`targetPort`、`gatewayUrl`。
71
+
72
+ ## 故障排查
73
+
74
+ | 现象 | 排查 |
75
+ | --- | --- |
76
+ | 面板显示「未绑定」 | 确认网关地址可访问;重新获取设备码;确认控制台批准的是同一设备码 |
77
+ | 设备码过期 | 设备码 10 分钟有效,重新「获取设备码」 |
78
+ | 开启隧道一直「连接中」 | 检查 `wss://.../_gw/connect` 是否可直连(公司代理/防火墙可能拦截 WebSocket) |
79
+ | 握手失败 `auth_failed` / `revoked` | 设备已吊销或令牌失效:解绑后重新绑定;被吊销需在控制台重新添加设备 |
80
+ | 打开公网地址 401 | 先确认已在面板设置访问口令;用「带访问令牌的链接」打开一次换取会话;或确认控制台访问模式为 password |
81
+ | 本机 3092 被占用 | 在 `config.json` 或 cordis 配置里改 `port` |
82
+ | 请求 502 | DSH 本体不可达:确认 `dsh web` 在运行,或配置 `targetPort` |
83
+ | WebSocket 打不开 | 确认网关与反代放行 WS Upgrade;插件会透传 `ws-open/accept/frame/close` |
84
+
85
+ ## 已知限制(v3.0.0)
86
+
87
+ - `getStatus` 为面板引导接口,免 adminToken,只返回脱敏状态(设备 id/secret 均掩码,不含访问令牌与二维码)。
88
+ - adminToken 每次进程启动随机生成,只经物理回环 `/__qzsy_bridge__/loopback-token` 签发;重启 DSH 后需重新领取(面板自动完成)。
89
+ - 访问口令登录页暂无失败速率限制;请使用足够强的口令,并只在内网/可信网络暴露本地端口。
90
+ - `access.token` 为固定本地令牌(`?auth=` 链接可重复使用),README 已说明泄露处置方式;吊销设备后请解绑并重新绑定以轮换设备密钥。
91
+
92
+ ## 开发与测试
93
+
94
+ ```bash
95
+ npm install
96
+ node client/build.mjs # 重建 client/client.js
97
+ node --test test/*.test.mjs # 全量单测
98
+ ```
99
+
100
+ 测试覆盖:v2 握手、心跳与心跳超时、HTTP/SSE/WebSocket 转发、route-update、drain、
101
+ 4003 吊销终态、断线重连、门禁三类来源、打包身份(全包无第三方包名残留)。
102
+
103
+ ## 协议与许可
104
+
105
+ - 控制通道协议 v2 契约见仓库 `docs/architecture/saas-tunnel.md`,共享类型见 `packages/tunnel-protocol`。
106
+ - 本包以 MIT 许可发布,版权归本仓库(qzsy)所有,详见 [LICENSE](./LICENSE)。
@@ -0,0 +1,41 @@
1
+ // 客户端打包:client/index.js → client/client.js(DSH __ModuleLoader__ 约定)
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { build } from 'esbuild';
7
+
8
+ const sourceDir = dirname(fileURLToPath(import.meta.url));
9
+ const packageRoot = resolve(sourceDir, '..');
10
+ const outputPath = resolve(packageRoot, 'client/client.js');
11
+ const loaderId = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8')).name;
12
+
13
+ const result = await build({
14
+ entryPoints: [resolve(sourceDir, 'index.js')],
15
+ bundle: true,
16
+ format: 'cjs',
17
+ platform: 'browser',
18
+ target: ['chrome100'],
19
+ external: ['react', 'react/jsx-runtime'],
20
+ write: false,
21
+ minify: process.env.NODE_ENV === 'production',
22
+ legalComments: 'none',
23
+ });
24
+
25
+ const bundled = result.outputFiles?.[0]?.text;
26
+ if (!bundled) throw new Error('esbuild did not produce a client bundle');
27
+
28
+ const wrapped = `window.__ModuleLoader__.load({
29
+ id: ${JSON.stringify(loaderId)},
30
+ factory: (require) => {
31
+ var module = { exports: {} };
32
+ var exports = module.exports;
33
+ var React = require("react");
34
+ ${bundled}
35
+ return module.exports;
36
+ }
37
+ });
38
+ `;
39
+ await mkdir(dirname(outputPath), { recursive: true });
40
+ await writeFile(outputPath, wrapped, 'utf8');
41
+ console.log(`Wrote ${outputPath}`);