@mrrisega/dsh-remote 0.6.6 → 0.6.7-beta.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.6.6",
3
+ "version": "0.6.7-beta.1",
4
4
  "description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -1,4 +1,4 @@
1
- // 交流群二维码(运营配置)在插件侧的两处展示回归:
1
+ // 交流群二维码(后台配置)在插件侧的两处展示回归:
2
2
  // ① 设置面板:「💬 加入交流群」按钮 → 点击弹出二维码大图(未配置后台码 → 按钮不存在);
3
3
  // ② 用户反馈页:底部直接展示企微交流群二维码 + 文案。
4
4
  // 数据来源:node 半 GET /dsh-remote/community(读公开配置 community.qrcode 并拼绝对地址)。
@@ -1,6 +1,6 @@
1
1
  // 「登录后自动闭环」回归(node 半,0.6.4)。
2
2
  //
3
- // 现场(2026-09 生产数据,id 42~52 共 11 个真实注册用户):只有 3 人最终绑定了设备,其余 8 人的
3
+ // 现场观察:新注册用户里只有少数最终绑定了设备,其余的
4
4
  // 电脑端 bridge 从未连上过中继;多人反复点「生成访问链接」(auth.key_create 10~23 次)而
5
5
  // device.bind 始终为 0。典型路径:注册 → 手机端 /api/devices 拿到空列表 → 反复轮询 → 放弃。
6
6
  //
@@ -1,8 +1,8 @@
1
1
  // 「登录后自动闭环」UI 回归(浏览器半,0.6.4;真实 useEffect + 可控假定时器 + 可控 document.hidden)。
2
2
  //
3
- // 现场(2026-09 生产数据):注册后手机端一直是空设备列表,用户在设置面板反复点「生成访问链接」
4
- // (auth.key_create 10~23 次)却始终没有 device.bind——他们看不到「到底卡在哪一步」,也不知道
5
- // 要不要刷新页面。本用例锁死面板侧的自动闭环:
3
+ // 现场:注册后手机端一直是空设备列表,用户在设置面板反复点「生成访问链接」
4
+ // 却始终没有 device.bind——他们看不到「到底卡在哪一步」,也不知道要不要刷新页面。
5
+ // 本用例锁死面板侧的自动闭环:
6
6
  // ① 登录后立刻进入启动/连接流程(2.5s 短轮询自动推进,不需要点任何按钮);
7
7
  // ② 阶段推进到 online → UI 自动变成「已连接 ✅ 现在可以用手机扫码访问」,并自动重取二维码与
8
8
  // 已授权设备列表(全程不刷新页面);
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 安装器命令行分派契约测试(行为级,真起进程)。
3
+ *
4
+ * 背景(2026-09-17 实测发现):`dsh-remote --help` 过去会**直接开始安装** ——
5
+ * 分派逻辑是「首个参数以 `-` 开头就当成 setup 的参数」,而 `--help` 不在任何分支里,
6
+ * 于是落到默认的 setup 路径:往 ~/.dsh-remote 同步运行时脚本、装插件、登记自启动。
7
+ * 用户想先看一眼用法,结果被装了一整套东西。
8
+ *
9
+ * 本用例锁死两条:
10
+ * ① `--help` / `-h` / `help` → 打印用法、退出码 0、**不碰磁盘**;
11
+ * ② 参数拼错(不认识的 flag)→ 明确报错、退出码 1、**也不碰磁盘**。
12
+ *
13
+ * ⚠️ 隔离:所有用例都伪造 HOME + DSH_PROFILE_DIR,并设 DSH_RELAY_SKIP_SERVICE=1,
14
+ * 否则会污染真实的自启动服务与 ~/.dsh-remote(见 setup-output.test.mjs 的事故复盘)。
15
+ */
16
+ import assert from "node:assert/strict";
17
+ import { execFile } from "node:child_process";
18
+ import fs from "node:fs";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+ import test from "node:test";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
25
+ const SETUP = path.join(HERE, "..", "..", "..", "dsh-setup.mjs");
26
+
27
+ /** 在隔离 HOME 下运行安装器,返回 { code, stdout, stderr, touched }。 */
28
+ function runInstaller(args) {
29
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "dsh-cli-"));
30
+ return new Promise((resolve) => {
31
+ execFile(
32
+ process.execPath,
33
+ [SETUP, ...args],
34
+ {
35
+ env: {
36
+ ...process.env,
37
+ HOME: home,
38
+ DSH_RELAY_SKIP_SERVICE: "1",
39
+ DSH_PROFILE_DIR: path.join(home, "prof"),
40
+ DSH_RELAY_DIR: path.join(home, ".dsh-remote")
41
+ },
42
+ timeout: 30_000
43
+ },
44
+ (err, stdout, stderr) => {
45
+ const touched = fs.existsSync(path.join(home, ".dsh-remote"));
46
+ fs.rmSync(home, { recursive: true, force: true });
47
+ resolve({ code: err ? err.code ?? 1 : 0, stdout, stderr, touched });
48
+ }
49
+ );
50
+ });
51
+ }
52
+
53
+ test("--help / -h / help:打印用法、退出码 0、且不落盘", async () => {
54
+ for (const flag of ["--help", "-h", "help"]) {
55
+ const r = await runInstaller([flag]);
56
+ assert.equal(r.code, 0, `${flag} 应以 0 退出`);
57
+ assert.match(r.stdout, /用法:/, `${flag} 应打印用法`);
58
+ assert.match(r.stdout, /dsh-remote status/, `${flag} 的用法应列出子命令`);
59
+ assert.equal(r.touched, false, `${flag} 不得在用户主目录创建任何东西`);
60
+ }
61
+ });
62
+
63
+ test("参数拼错:明确报错、退出码 1、也不落盘", async () => {
64
+ for (const flag of ["--bogus", "--dry-run", "--hepl"]) {
65
+ const r = await runInstaller([flag]);
66
+ assert.equal(r.code, 1, `${flag} 应以 1 退出`);
67
+ assert.match(r.stderr, /未知参数/, `${flag} 应明确说是未知参数`);
68
+ assert.match(r.stderr, /--help/, `${flag} 的报错应告诉用户怎么看用法`);
69
+ assert.equal(r.touched, false, `${flag} 不得开始安装`);
70
+ }
71
+ });
72
+
73
+ test("源码级:用法文本只有一处实现(避免提前退出与末尾分派两处漂移)", () => {
74
+ const src = fs.readFileSync(SETUP, "utf8");
75
+ const hits = src.match(/dsh-remote — 手机远程控制 dsh web/g) || [];
76
+ assert.equal(hits.length, 1, "用法文案应只出现一次(共用 printHelp())");
77
+ assert.match(src, /function printHelp\(\)/, "应有 printHelp()");
78
+ // 关键:提前退出必须挡在 ensureRuntimeCopy() 之前
79
+ const iHelp = src.indexOf("printHelp();\n process.exit(0);");
80
+ const iSync = src.indexOf("try { ensureRuntimeCopy(); }");
81
+ assert.ok(iHelp > -1 && iSync > -1 && iHelp < iSync, "--help 的提前退出必须在运行时同步之前");
82
+ });
@@ -122,7 +122,9 @@ test("失败归因:客户端白名单必须与服务端逐字一致,且事
122
122
  for (const c of codes) assert.ok(out.queued.includes(c), `客户端 failCodes 应含 ${c}`);
123
123
 
124
124
  // ② 与服务端白名单**集合级**一致(少一个码 = 上报被丢弃、多一个 = 服务端拒收)
125
- const serverPath = join(REPO, "..", "dsh-relay-enterprise", "relay-enterprise", "src", "api.js");
125
+ // 可选:把服务端 api.js 的路径通过环境变量给出,即可做「客户端↔服务端白名单」集合级对拍。
126
+ // 未设置时跳过(跨仓库对拍属于本地/CI 场景,不应把另一仓库的位置硬编码进本仓库)。
127
+ const serverPath = process.env.DSH_TELEMETRY_SERVER_API || "";
126
128
  if (existsSync(serverPath)) {
127
129
  const apiSrc = readFileSync(serverPath, "utf8");
128
130
  const block = /const TELEMETRY_FAIL_CODES = new Set\(\[([\s\S]*?)\]\);/.exec(apiSrc);
@@ -0,0 +1,94 @@
1
+ // 「中继不提供额度接口」时的降级回归。
2
+ //
3
+ // 背景:插件是**开源侧产物,自建与 SaaS 两种模式共用**。自建中继不限制带宽与流量,
4
+ // 也不提供 `/_quota`;此时面板必须:
5
+ // ① 代理不报错(返回 {ok:true, quota:null} 而不是 5xx);
6
+ // ② 不显示任何**服务端并未下发的**限制文案(例如硬编码的“本月流量限额 1GB”)。
7
+ //
8
+ // 曾在 2026-09 修过一次:面板在拿不到额度数据时硬编码回退成
9
+ // 「免费额度: 带宽 ≈1Mbps · 本月流量限额 1GB」——对自建用户就是凭空捏造限制。
10
+ import assert from "node:assert/strict";
11
+ import http from "node:http";
12
+ import { readFileSync } from "node:fs";
13
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import test from "node:test";
17
+ import { fileURLToPath } from "node:url";
18
+ import { apply } from "../lib/index.js";
19
+
20
+ const CLIENT_SRC = readFileSync(
21
+ path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "lib", "client.js"),
22
+ "utf8"
23
+ );
24
+
25
+ /** 假 relay:只提供 device-login;其余 404(模拟极简自建账号服务)。 */
26
+ function startFakeRelay() {
27
+ const srv = http.createServer(async (req, res) => {
28
+ const url = new URL(req.url, "http://x");
29
+ const send = (code, obj) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
30
+ if (req.method === "POST" && url.pathname === "/api/device-login") return send(200, { token: "jwt-self" });
31
+ send(404, { error: { code: "not_found" } });
32
+ });
33
+ return new Promise((r) => srv.listen(0, "127.0.0.1", () => r({ srv, port: srv.address().port })));
34
+ }
35
+
36
+ /** 假中继:**没有 `/_quota`**(干净自建中继就是这样)。 */
37
+ function startFakeRelayRouter() {
38
+ const srv = http.createServer((_req, res) => { res.writeHead(404); res.end(); });
39
+ return new Promise((r) => srv.listen(0, "127.0.0.1", () => r({ srv, port: srv.address().port })));
40
+ }
41
+
42
+ test("中继无 /_quota:代理返回 {ok:true,quota:null} 且不报错", async () => {
43
+ const relay = await startFakeRelay();
44
+ const router = await startFakeRelayRouter();
45
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-noquota-"));
46
+ await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({
47
+ phone: "13800000000",
48
+ password: "pw",
49
+ device_id: "dev-selfhost0001",
50
+ api_url: `http://127.0.0.1:${relay.port}`,
51
+ tunnel_url: `ws://127.0.0.1:${router.port}`
52
+ }));
53
+
54
+ const routes = new Map();
55
+ apply({
56
+ webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
57
+ effect(register) { return register(); },
58
+ logger: { info() {}, warn() {} }
59
+ }, { relayDir: tempDir });
60
+
61
+ const host = http.createServer((req, res) => {
62
+ const url = new URL(req.url, "http://x");
63
+ const handler = routes.get(url.pathname);
64
+ (handler || ((_r, rs) => { rs.writeHead(404); rs.end(); }))(req, res);
65
+ });
66
+ await new Promise((r) => host.listen(0, "127.0.0.1", r));
67
+
68
+ try {
69
+ const r = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/quota`);
70
+ assert.equal(r.status, 200, "拿不到额度不应变成 5xx");
71
+ const body = await r.json();
72
+ assert.equal(body.ok, true);
73
+ assert.equal(body.quota, null, "中继没有额度接口 → quota 必须是 null(而不是编造一份)");
74
+ } finally {
75
+ host.close(); relay.srv.close(); router.srv.close();
76
+ await rm(tempDir, { recursive: true, force: true });
77
+ }
78
+ });
79
+
80
+ test("面板文案:额度数据缺失时不得断言任何限制", () => {
81
+ // 有额度数据 + 限量 → 才允许出现带宽/用量文案,且带宽值必须来自服务端
82
+ assert.match(CLIENT_SRC, /if \(quota && quota\.limit_enabled\) \{/, "有额度且限量时才展示用量");
83
+ assert.doesNotMatch(CLIENT_SRC, /带宽 ≈1Mbps/, "不得硬编码带宽数值,应从服务端下发");
84
+ assert.match(CLIENT_SRC, /quota\.max_mbps \? "带宽 ≈" \+ quota\.max_mbps/, "带宽值服务端有才写");
85
+ // 无额度数据 → 只说明是免费用户,不得出现硬编码的限额
86
+ assert.doesNotMatch(
87
+ CLIENT_SRC,
88
+ /quotaPct !== null \?[^:]*:[^"]*本月流量限额 1GB/,
89
+ "不得在拿不到额度时硬编码“本月流量限额 1GB”"
90
+ );
91
+ assert.doesNotMatch(CLIENT_SRC, /:\s*" · 本月流量限额 1GB"/, "硬编码限额回退已移除");
92
+ // 中继报告「不限量」时也不该说限额
93
+ assert.match(CLIENT_SRC, /免费用户 · 当前不限速、不限流量/, "不限量应如实说明");
94
+ });
@@ -207,7 +207,7 @@ test("登录态账号区:无「切换账号」,有「退出登录」,头
207
207
  const points = [
208
208
  "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。",
209
209
  "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。",
210
- "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。",
210
+ "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表。",
211
211
  "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。",
212
212
  ];
213
213
  for (const p of points) {
@@ -1,7 +1,7 @@
1
1
  // 匿名装机/连接遥测(客户端半)回归 —— 契约 + 隐私边界(2026-09)。
2
2
  //
3
- // 现场(2026-09 生产数据):11 个新注册用户里只有 3 人把设备连上——6 人电脑端从未安装、2 人装了但
4
- // bridge 没连上;而「装不上」的人没有任何账号、也就没有任何数据,「本地 OK、新机器失败」无法归因。
3
+ // 现场:新注册用户里只有少数把设备连上,多数卡在电脑端安装这一步;而「装不上」的人
4
+ // 往往没有账号、也就没有任何数据,「本地 OK、新机器失败」无法归因。
5
5
  // 本用例锁死这条补上的通道:
6
6
  // ① 触发点正确:plugin_loaded / install_started / install_failed(fail_code) / runtime_ready /
7
7
  // bridge_started / bridge_registered / tunnel_disconnected / first_remote_ok(只一次) / panel_opened;