@mrrisega/dsh-remote 0.4.8 → 0.5.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 CHANGED
@@ -63,12 +63,9 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
63
63
  npx @mrrisega/dsh-remote
64
64
  ```
65
65
 
66
- 安装完成后运行下面命令打开设置页,用手机号+密码登录即可(注册在手机端完成,
67
- 登录后 bridge 自动启动):
68
-
69
- ```bash
70
- npx @mrrisega/dsh-remote settings
71
- ```
66
+ 安装完成后**无需任何命令**:打开 dsh web → 设置 → 「远程控制」,注册/登录手机号即可
67
+ (注册在手机端完成,登录后 bridge 自动启动;自建用户在同一面板切「自建服务」标签)。
68
+ 原来的独立设置页(`dsh-remote settings`)已移除,避免与插件面板重复造成困惑。
72
69
 
73
70
  自建模式(自己部署了 relay-router,无需账号体系):
74
71
 
@@ -76,11 +73,11 @@ npx @mrrisega/dsh-remote settings
76
73
  npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访问密钥>
77
74
  ```
78
75
 
79
- 其他命令:`settings`(设置页)、`status`(查看状态)、`run`(前台调试)、
76
+ 其他命令:`status`(查看状态)、`run`(前台调试)、`settings`(仅显示登录指引)、
80
77
  `plugin`(重装/卸载 dsh web 插件)。运行 `npx @mrrisega/dsh-remote --help` 查看完整说明。
81
78
 
82
79
  > **版本与更新 / 卸载**:dsh 官方插件市场目前不提供更新按钮,也不会改写用户补丁(因此市场
83
- > 卸载会提示「仍通过 insert 引用 dsh-remote-ui」而拒绝)。插件设置面板里已内置管理入口
80
+ > 卸载会提示「仍通过 insert 引用 dsh-remote-web」而拒绝)。插件设置面板里已内置管理入口
84
81
  > (dsh web → 设置 → 「远程控制」→「🔄 版本与更新」卡片):显示当前版本、自动检测 npm 新版、
85
82
  > **一键在线更新**(后台补运行环境并重启 bridge,完成后重启 dsh web 生效)、以及**彻底卸载**
86
83
  > (移除补丁 include / 依赖 / bundle 与本地文件,之后市场卸载或直接重启均可完成卸载)。
@@ -112,7 +109,7 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
112
109
  | relay-router | `packages/relay-router/` | 中继服务器:bridge 注册表、实时设备列表、HTTP/WS 透明代理、配额 |
113
110
  | bridge | `clients/dsh-remote/` | 电脑端守护进程:连 router 注册,把转发帧代理到本地 `dsh web`;心跳自愈 |
114
111
  | PWA | `clients/dsh-web/native.html` | 手机端:登录 / 注册 / 设备选择(单文件,零构建) |
115
- | dsh web 插件 | `packages/dsh-remote-ui/` | 设置页「远程控制」面板:连接模式 / 账号 / bridge 启停 / 反馈 |
112
+ | dsh web 插件 | `packages/dsh-remote-web/` | 设置页「远程控制」面板:连接模式 / 账号 / bridge 启停 / 反馈 |
116
113
  | 部署脚本 | `deploy/` | `install-open.sh` 自建引导、nginx 参考配置、Dockerfile / compose |
117
114
 
118
115
  测试:`npm test`(router 契约 + 插件 + bridge 全部单测与回归)。
@@ -137,7 +137,7 @@ function saveLocalConfig(cfg) {
137
137
  }
138
138
 
139
139
  /**
140
- * Harness 浏览器会话 Cookie(由 dsh-remote-ui 插件在进程内换取后写入
140
+ * Harness 浏览器会话 Cookie(由 dsh-remote-web 插件在进程内换取后写入
141
141
  * <relayDir>/.harness-cookie.json)。新版 dsh web(0.1.2+)对每个请求校验该 Cookie,
142
142
  * 不带则 401 → 手机端白页;bridge 对所有上游 HTTP/WS 请求自动携带,让手机表现为已授权浏览器。
143
143
  */
package/dsh-setup.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * 用法:
6
6
  * dsh-remote [setup] [选项] 一键安装(默认命令,无需任何参数)
7
- * dsh-remote settings 打开本地设置页(登录账号 / 自建配置)
7
+ * dsh-remote settings 提示如何登录(独立设置页已移除,见 dsh web 面板)
8
8
  * dsh-remote run 前台运行 bridge(调试/守护)
9
9
  * dsh-remote status 查看配置与服务状态
10
10
  * dsh-remote plugin [--uninstall] 手动安装/卸载 dsh web 远程控制插件
@@ -19,10 +19,10 @@
19
19
  * 1. 写入配置 <CONFIG_DIR>/.dsh-config.json(0600;npm 安装时为 ~/.dsh-remote/)
20
20
  * 2. 生成自启动服务(macOS launchd / Linux systemd),随 dsh web(3080) 存活自动保活
21
21
  * 3. 自动把远程控制插件装进 dsh web 设置页(若检测到 profile)
22
- * 4. 登录在设置页完成: dsh-remote settings(手机号+密码,或自建密钥)
22
+ * 4. 登录/连接配置在 dsh web → 设置 → 「远程控制」面板完成(SaaS 注册登录 / 自建切换)
23
+ * 自建 CLI 用户也可用 `dsh-remote setup --server … --key …`(无需再打开设置页)
23
24
  */
24
25
 
25
- import http from "node:http";
26
26
  import fs from "node:fs";
27
27
  import os from "node:os";
28
28
  import path from "node:path";
@@ -35,7 +35,6 @@ const IS_NPM_INSTALL = THIS_DIR.includes(`${path.sep}node_modules${path.sep}`);
35
35
  // 配置目录:npm 安装时放在用户目录(node_modules 内不可写);仓库开发时放在仓库根。
36
36
  const CONFIG_DIR = process.env.DSH_RELAY_DIR || (IS_NPM_INSTALL ? path.join(os.homedir(), ".dsh-remote") : THIS_DIR);
37
37
  const CONFIG_PATH = path.join(CONFIG_DIR, ".dsh-config.json");
38
- const SETTINGS_PORT = 3499;
39
38
  // 默认云端服务地址(服务商 SaaS 入口;自建用户用 --server/--key 指向自己的 router)
40
39
  const DEFAULT_API = "https://n.risegao.cn:13443/relay-api";
41
40
  const DEFAULT_APP_URL = "https://n.risegao.cn:13443/app/";
@@ -45,7 +44,7 @@ const REPO_URL = "https://github.com/mrRisega/dsh-remote";
45
44
  // npx 每次安装的缓存目录(~/.npm/_npx/<hash>)不固定:缓存一旦清理,指向它的自启动服务
46
45
  // 就会像“找不到模块”一样崩溃。因此 npm 形态安装时把 dsh-setup.mjs + clients + 依赖(ws)
47
46
  // 固化到 CONFIG_DIR(~/.dsh-remote),自启动服务只指向这个稳定路径。
48
- // 插件(dsh-remote-ui)的“运行环境已就绪”判断同样以 CONFIG_DIR/dsh-setup.mjs 为准。
47
+ // 插件(dsh-remote-web;2026-09 前为 dsh-remote-ui)的“运行环境已就绪”判断同样以 CONFIG_DIR/dsh-setup.mjs 为准。
49
48
 
50
49
  function pkgVersion() {
51
50
  try {
@@ -281,97 +280,20 @@ function installAutostart() {
281
280
  return { path: svcPath, status: r };
282
281
  }
283
282
 
284
- // ---------- 设置页(本地 HTTP:远程地址 + 账号/自建配置) ----------
285
- function serveSettings() {
286
- const cfg = loadConfig();
287
- const server = http.createServer(async (req, res) => {
288
- const url = new URL(req.url, `http://127.0.0.1:${SETTINGS_PORT}`);
289
- const send = (code, body, type = "text/html") => {
290
- res.writeHead(code, { "Content-Type": type + "; charset=utf-8" });
291
- res.end(body);
292
- };
293
-
294
- if (req.method === "POST" && url.pathname === "/api/config") {
295
- let raw = "";
296
- for await (const c of req) raw += c;
297
- try {
298
- const body = JSON.parse(raw);
299
- const local = Boolean(body.local_key || body.server);
300
- if (local) {
301
- if (!body.server || !body.local_key) {
302
- return send(400, JSON.stringify({ ok: false, message: "自建模式需要服务器地址与访问密钥" }), "application/json");
303
- }
304
- cfg.tunnel_url = normalizeTunnelUrl(body.server);
305
- cfg.local_key = String(body.local_key).trim();
306
- delete cfg.phone; delete cfg.password;
307
- } else {
308
- if (!body.phone || !body.password) {
309
- return send(400, JSON.stringify({ ok: false, message: "SaaS 模式需要手机号与密码" }), "application/json");
310
- }
311
- cfg.phone = body.phone; cfg.password = body.password;
312
- applySaaSMode(cfg); // 清自建残留,api/tunnel 权威重算
313
- }
314
- // 自动获取服务端下发的 bridge_secret(device-login 共享密钥,一键安装开箱即用)
315
- if (!cfg.bridge_secret && !cfg.local_key) {
316
- const pub = await fetchPublicConfig(cfg.api_url || DEFAULT_API);
317
- if (pub.bridge_secret) cfg.bridge_secret = String(pub.bridge_secret);
318
- }
319
- saveConfig(cfg);
320
- const r = restartBridgeService();
321
- return send(200, JSON.stringify({ ok: true, service: r.ok ? "running" : (r.status || "failed") }), "application/json");
322
- } catch { return send(400, JSON.stringify({ ok: false, message: "JSON 解析失败" }), "application/json"); }
323
- }
283
+ // ---------- settings:旧独立设置页已移除(引导到 dsh web 插件面板) ----------
284
+ // 云服务用户在 dsh web → 设置 → 「远程控制」面板即可注册/登录(自建切换也在此面板);
285
+ // 独立设置页(127.0.0.1:3499)与面板功能完全重复、且暴露自建表单给普通用户造成困惑,已移除。
286
+ // 保留 `dsh-remote settings` 命令为“引导提示”,避免旧脚本/旧文档直接调它时报错。
287
+ function settingsHint() {
288
+ console.log(`
289
+ dsh-remote:独立的本地设置页已移除。
324
290
 
325
- const mode = cfg.local_key ? "自建服务" : "SaaS 云端服务";
326
- const pub = await fetchPublicConfig(cfg.api_url || DEFAULT_API);
327
- const remoteUrl = pub.app_url || (cfg.local_key ? (cfg.tunnel_url || "") : DEFAULT_APP_URL);
328
-
329
- const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"/>
330
- <meta name="viewport" content="width=device-width,initial-scale=1"/>
331
- <title>dsh-remote 设置</title>
332
- <style>
333
- body{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;background:#0d1117;color:#e6edf3;max-width:520px;margin:0 auto;padding:24px}
334
- h1{font-size:18px}label{display:block;font-size:13px;color:#8b949e;margin:14px 0 6px}
335
- input{width:100%;padding:11px 13px;border-radius:8px;border:1px solid #30363d;background:#161b22;color:#e6edf3;font-size:15px;box-sizing:border-box}
336
- button{width:100%;padding:13px;border-radius:8px;border:none;background:#2f81f7;color:#fff;font-size:15px;font-weight:600;cursor:pointer;margin-top:16px}
337
- .card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:18px;margin-bottom:16px}
338
- .url{background:#010409;border:1px solid #30363d;border-radius:8px;padding:12px;font-family:monospace;font-size:14px;word-break:break-all}
339
- .msg{font-size:13px;margin-top:10px;min-height:18px}.ok{color:#3fb950}.err{color:#f85149}
340
- </style></head><body>
341
- <h1>dsh-remote 设置</h1>
342
- <div class="card"><h3 style="margin:0 0 8px">📱 远程控制地址(当前模式:${mode})</h3>
343
- <div class="url">${remoteUrl || "(未配置)"}</div>
344
- <div style="font-size:12px;color:#8b949e;margin-top:8px">手机浏览器打开此地址,即可远程控制本机 dsh web。</div></div>
345
- <div class="card"><h3 style="margin:0 0 4px">🔑 连接配置</h3>
346
- <div style="font-size:12px;color:#8b949e;margin-bottom:8px">二选一:填手机号密码(SaaS),或填服务器地址+访问密钥(自建)。</div>
347
- <label>SaaS 手机号</label><input id="phone" value="${cfg.phone || ""}" autocomplete="tel"/>
348
- <label>SaaS 密码</label><input id="pass" type="password" placeholder="••••••••" autocomplete="current-password"/>
349
- <div style="height:1px;background:#30363d;margin:16px 0"></div>
350
- <label>自建服务器地址(wss://host:port)</label><input id="server" value="${cfg.tunnel_url || ""}" placeholder="wss://relay.example.com"/>
351
- <label>自建访问密钥</label><input id="lkey" type="password" value="${cfg.local_key || ""}" placeholder="访问密钥"/>
352
- <button onclick="save()">保存并生效</button>
353
- <div class="msg" id="msg"></div></div>
354
- <script>
355
- async function save(){
356
- const m=document.getElementById("msg");m.className="msg";m.textContent="保存中...";
357
- const local = document.getElementById("lkey").value.trim() !== "" || document.getElementById("server").value.trim() !== "";
358
- try{
359
- const r=await fetch("/api/config",{method:"POST",headers:{"content-type":"application/json"},
360
- body:JSON.stringify(local
361
- ? {server:document.getElementById("server").value.trim(),local_key:document.getElementById("lkey").value.trim()}
362
- : {phone:document.getElementById("phone").value.trim(),password:document.getElementById("pass").value})});
363
- const d=await r.json();
364
- if(d.ok){m.className="msg ok";m.textContent = d.service==="running" ? "✅ 已保存并生效,无需手动重启" : "✅ 已保存(服务状态: "+(d.service||"未知")+",可运行 dsh-remote setup 修复)";}
365
- else{m.className="msg err";m.textContent=d.message||"保存失败";}
366
- }catch(e){m.className="msg err";m.textContent="保存失败: "+e.message;}
367
- }
368
- </script></body></html>`;
369
- return send(200, html);
370
- });
371
- server.listen(SETTINGS_PORT, "127.0.0.1", () => {
372
- console.log(`\n✅ 设置页已打开: http://127.0.0.1:${SETTINGS_PORT}`);
373
- console.log(" 在浏览器中配置账号或自建连接,查看远程控制地址。Ctrl-C 关闭。\n");
374
- });
291
+ 请直接在 dsh web 里完成登录/连接配置(无需任何命令):
292
+ 打开 dsh web 设置 「远程控制」→ 注册或登录手机号即可(自建模式切换也在该面板)。
293
+
294
+ 自建用户若偏好命令行,可用:
295
+ dsh-remote setup --server wss://你的域名:端口 --key 访问密钥
296
+ `);
375
297
  }
376
298
 
377
299
  // ---------- run:前台跑 bridge(带配置 + 自启动 watcher) ----------
@@ -400,7 +322,7 @@ async function runBridge() {
400
322
  if (!saas && !local) {
401
323
  if (!warnedNoLogin) {
402
324
  warnedNoLogin = true;
403
- console.log("⚠️ 尚未登录:打开设置页 `dsh-remote settings` 登录账号(或配置自建密钥)后自动启动。");
325
+ console.log("⚠️ 尚未登录:请打开 dsh web → 设置 → 「远程控制」,注册/登录手机号(或切到自建模式)后自动启动。");
404
326
  }
405
327
  return;
406
328
  }
@@ -540,31 +462,39 @@ async function setup(argv) {
540
462
  }
541
463
 
542
464
  // ---------- plugin:安装/卸载 dsh web 远程控制插件 ----------
543
- const PLUGIN_MARKER_START = "# >>> dsh-remote-ui (managed by dsh-remote plugin; do not edit)";
544
- const PLUGIN_MARKER_END = "# <<< dsh-remote-ui";
465
+ /** 当前插件名(2026-09 dsh-remote-ui 更名;dsh-remote-web = dsh-remote dsh web 插件半)。 */
466
+ const PLUGIN_ID = "dsh-remote-web";
467
+ /** 更名前的历史插件名(≤0.4.9):升级安装/卸载时一并清理,避免旧 id 残留导致重复激活。 */
468
+ const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
469
+ /** 当前名 + 全部历史名。 */
470
+ const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
471
+ const PLUGIN_MARKER_START = `# >>> ${PLUGIN_ID} (managed by dsh-remote plugin; do not edit)`;
472
+ const PLUGIN_MARKER_END = `# <<< ${PLUGIN_ID}`;
545
473
  /** 插件在 profile 内的固定本地目录(安装时整目录拷贝)。 */
546
- const PLUGIN_LOCAL_DIR = "dsh-remote-ui-plugin";
474
+ const PLUGIN_LOCAL_DIR = `${PLUGIN_ID}-plugin`;
475
+ /** 片段是否涉及本插件(当前名或任一历史名)。 */
476
+ const pluginRef = (s) => PLUGIN_ALL_IDS.some((id) => s.includes(id));
547
477
 
548
478
  /**
549
- * include 块。name 必须是【裸包名】'dsh-remote-ui'
550
- * - 加载器据此 import 节点半(dsh-remote-ui 由 node_modules 链接解析);
479
+ * include 块。name 必须是【裸包名】'${PLUGIN_ID}'(≤0.4.9 为 'dsh-remote-ui'):
480
+ * - 加载器据此 import 节点半(${PLUGIN_ID} 由 node_modules 链接解析);
551
481
  * - dsh 客户端模块系统(@deepseek-ai/dsh-client-modules)用 require.resolve(
552
482
  * '<name>/package.json') 从 profile 解析该包并读取 package.json 的
553
483
  * dsh.client 声明 → 注入浏览器半(设置面板 UI)。相对文件路径入口只会加载
554
484
  * 节点半、不会注入 UI —— 0.3.7/0.3.8 曾因此丢设置页。
555
- * 链接由安装器自建(node_modules/dsh-remote-ui → 拷贝出的目录),不依赖 pnpm/npm。
485
+ * 链接由安装器自建(node_modules/${PLUGIN_ID} → 拷贝出的目录),不依赖 pnpm/npm。
556
486
  */
557
487
  function pluginBlock(relayDir) {
558
488
  return `${PLUGIN_MARKER_START}
559
489
  - insert:
560
- - id: dsh-remote-ui
561
- name: 'dsh-remote-ui'
490
+ - id: ${PLUGIN_ID}
491
+ name: '${PLUGIN_ID}'
562
492
  config:
563
493
  relayDir: '${relayDir}'
564
494
  ${PLUGIN_MARKER_END}`;
565
495
  }
566
496
 
567
- /** 移除 patch 中所有引用 dsh-remote-ui 的条目块(含其前置注释),返回剩余内容。 */
497
+ /** 移除 patch 中所有引用本插件(当前或历史名)的条目块(含其前置注释),返回剩余内容。 */
568
498
  function stripPluginEntries(patch) {
569
499
  const lines = patch.split("\n");
570
500
  const out = [];
@@ -579,10 +509,10 @@ function stripPluginEntries(patch) {
579
509
  block.push(lines[j]);
580
510
  j++;
581
511
  }
582
- if (block.join("\n").includes("dsh-remote-ui")) {
512
+ if (pluginRef(block.join("\n"))) {
583
513
  // 连带删除块前的连续注释(旧版条目说明 / 管理标记),以及块后的收尾标记行
584
514
  while (out.length && /^\s*#/.test(out[out.length - 1])) out.pop();
585
- if (j < lines.length && /^\s*#/.test(lines[j]) && lines[j].includes("dsh-remote-ui")) j++;
515
+ if (j < lines.length && /^\s*#/.test(lines[j]) && pluginRef(lines[j])) j++;
586
516
  i = j;
587
517
  continue;
588
518
  }
@@ -613,10 +543,14 @@ function normalizePatchBase(patch) {
613
543
 
614
544
  /**
615
545
  * 把插件整目录拷贝进 profile 的固定子目录并返回该目录。
616
- * 源(本包 packages/dsh-remote-ui)可能在 npx 临时缓存里,因此必须拷到 profile 内持久化,
546
+ * 源(本包 packages/${PLUGIN_ID})可能在 npx 临时缓存里,因此必须拷到 profile 内持久化,
617
547
  * include 用相对路径直接指向拷贝出的入口,不写依赖、不跑任何包管理器。
618
548
  */
619
549
  function copyPluginIntoProfile(profileDir, pluginDir) {
550
+ // 清理历史名残留目录(≤0.4.9 的 dsh-remote-ui-plugin),避免与新版并存
551
+ for (const id of PLUGIN_LEGACY_IDS) {
552
+ try { fs.rmSync(path.join(profileDir, `${id}-plugin`), { recursive: true, force: true }); } catch { /* ignore */ }
553
+ }
620
554
  const dest = path.join(profileDir, PLUGIN_LOCAL_DIR);
621
555
  fs.rmSync(dest, { recursive: true, force: true });
622
556
  fs.cpSync(pluginDir, dest, {
@@ -633,11 +567,15 @@ function copyPluginIntoProfile(profileDir, pluginDir) {
633
567
  return dest;
634
568
  }
635
569
 
636
- /** 把插件挂到 <profile>/node_modules/dsh-remote-ui(裸包名解析需要),指向拷贝目录;缺失/指错时重建。 */
570
+ /** 把插件挂到 <profile>/node_modules/<PLUGIN_ID>(裸包名解析需要),指向拷贝目录;缺失/指错时重建。 */
637
571
  function ensurePluginLinked(profileDir, pluginLocalDir) {
638
572
  const nmDir = path.join(profileDir, "node_modules");
639
- const nmPlugin = path.join(nmDir, "dsh-remote-ui");
573
+ const nmPlugin = path.join(nmDir, PLUGIN_ID);
640
574
  fs.mkdirSync(nmDir, { recursive: true });
575
+ // 清理历史名链接(node_modules/dsh-remote-ui),避免解析到旧拷贝
576
+ for (const id of PLUGIN_LEGACY_IDS) {
577
+ try { fs.rmSync(path.join(nmDir, id), { recursive: true, force: true }); } catch { /* ignore */ }
578
+ }
641
579
  try {
642
580
  if (fs.realpathSync(nmPlugin) === pluginLocalDir) return false;
643
581
  } catch { /* 缺失/悬空 → 重建 */ }
@@ -654,50 +592,64 @@ function ensurePluginLinked(profileDir, pluginLocalDir) {
654
592
  function declarePluginDep(pkgFile) {
655
593
  const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
656
594
  pkg.dependencies = pkg.dependencies || {};
657
- pkg.dependencies["dsh-remote-ui"] = `file:./${PLUGIN_LOCAL_DIR}`;
595
+ for (const id of PLUGIN_LEGACY_IDS) { delete pkg.dependencies[id]; }
596
+ pkg.dependencies[PLUGIN_ID] = `file:./${PLUGIN_LOCAL_DIR}`;
658
597
  fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
659
598
  }
660
599
 
661
- /** 把 dsh-remote-ui 加入 dsh.profile.bundles(幂等)。返回是否发生变更。 */
600
+ /** 把 PLUGIN_ID 加入 dsh.profile.bundles(幂等;顺带移除历史名条目)。返回是否发生变更。 */
662
601
  function ensureBundleEntry(pkgFile) {
663
602
  const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
664
603
  const bundles = pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles)
665
604
  ? pkg.dsh.profile.bundles
666
605
  : null;
667
- if (bundles && bundles.includes("dsh-remote-ui")) return false;
606
+ if (bundles && bundles.includes(PLUGIN_ID)) {
607
+ const hadLegacy = bundles.some((b) => PLUGIN_LEGACY_IDS.includes(b));
608
+ if (hadLegacy) {
609
+ pkg.dsh.profile.bundles = bundles.filter((b) => !PLUGIN_LEGACY_IDS.includes(b));
610
+ fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
611
+ return true;
612
+ }
613
+ return false;
614
+ }
668
615
  pkg.dsh = pkg.dsh || {};
669
616
  pkg.dsh.profile = pkg.dsh.profile || {};
670
- pkg.dsh.profile.bundles = bundles || [];
671
- pkg.dsh.profile.bundles.push("dsh-remote-ui");
617
+ pkg.dsh.profile.bundles = (bundles || []).filter((b) => !PLUGIN_LEGACY_IDS.includes(b) && b !== PLUGIN_ID);
618
+ pkg.dsh.profile.bundles.push(PLUGIN_ID);
672
619
  fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
673
620
  return true;
674
621
  }
675
622
 
676
- /** 从 dsh.profile.bundles 移除 dsh-remote-ui(幂等)。返回是否发生变更。 */
623
+ /** 从 dsh.profile.bundles 移除本插件(当前或历史名,幂等)。返回是否发生变更。 */
677
624
  function removeBundleEntry(pkgFile) {
678
625
  const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
679
626
  const bundles = pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles)
680
627
  ? pkg.dsh.profile.bundles
681
628
  : null;
682
- if (!bundles || !bundles.includes("dsh-remote-ui")) return false;
683
- pkg.dsh.profile.bundles = bundles.filter((b) => b !== "dsh-remote-ui");
629
+ if (!bundles) return false;
630
+ const filtered = bundles.filter((b) => !PLUGIN_ALL_IDS.includes(b));
631
+ if (filtered.length === bundles.length) return false;
632
+ pkg.dsh.profile.bundles = filtered;
684
633
  fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
685
634
  return true;
686
635
  }
687
636
 
688
637
  /**
689
- * 插件安装(pluginCmd 非卸载分支)收敛策略 —— 2026-09-06「重复 ID 崩溃」根治:
690
- * dsh-remote-ui 是带 dsh.bundle.patch 的 bundle:package.json 的 dsh.profile.bundles
638
+ * 插件安装(pluginCmd 非卸载分支)收敛策略 —— 2026-09-06「重复 ID 崩溃」根治;
639
+ * 2026-09 插件由 dsh-remote-ui 更名 dsh-remote-web,本函数同时兼容清理旧名残留:
640
+ * dsh-remote-web 是带 dsh.bundle.patch 的 bundle:package.json 的 dsh.profile.bundles
691
641
  * 声明它后,加载器会自动应用插件自带的 cordis.patch.yml(节点半+浏览器半的唯一激活点)。
692
642
  * 若用户级 cordis.patch.yml 再手工 insert 同一个 id,dsh web 启动即报“重复 ID”崩溃。
693
643
  * 因此本命令【绝不写用户 include】,只负责:让插件以 bundle 形态可解析,
694
- * 并清理历史遗留的 include 块。市场形态(github:/npm 依赖)则完全交由市场管理,只清理 include。
644
+ * 并清理历史遗留的 include 块与旧名(≤0.4.9 dsh-remote-ui)痕迹。
645
+ * 市场形态(github:/npm 依赖)则完全交由市场管理,只清理 include。
695
646
  */
696
647
  function convergePluginActivation(profileDir, pkgFile, patchFile, pluginDir, patch) {
697
648
  const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
698
- const dep = pkg.dependencies && pkg.dependencies["dsh-remote-ui"];
649
+ const legacyDep = PLUGIN_LEGACY_IDS.map((id) => pkg.dependencies && pkg.dependencies[id]).find((v) => v !== undefined);
650
+ const dep = (pkg.dependencies && pkg.dependencies[PLUGIN_ID]) ?? legacyDep;
699
651
  const inBundles = !!(pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles)
700
- && pkg.dsh.profile.bundles.includes("dsh-remote-ui"));
652
+ && pkg.dsh.profile.bundles.some((b) => PLUGIN_ALL_IDS.includes(b)));
701
653
  const marketManaged = dep && !String(dep).startsWith("file:"); // github:/npm: 等由市场/包管理器管源码
702
654
  const managedByUs = !dep || String(dep).startsWith("file:"); // 无依赖或 file: 拷贝 → 我们管
703
655
 
@@ -729,17 +681,17 @@ function convergePluginActivation(profileDir, pkgFile, patchFile, pluginDir, pat
729
681
  const linked = ensurePluginLinked(profileDir, pluginLocalDir); // 自建 node_modules 链接
730
682
  stripInclude("bundle patch 已是唯一激活点"); // 清历史 include
731
683
  console.log(addedBundle
732
- ? "✅ 已加入 dsh.profile.bundles(dsh-remote-ui 自带 patch 自动生效)"
733
- : "ℹ dsh.profile.bundles 已含 dsh-remote-ui");
684
+ ? `✅ 已加入 dsh.profile.bundles(${PLUGIN_ID} 自带 patch 自动生效)`
685
+ : `ℹ dsh.profile.bundles 已含 ${PLUGIN_ID}`);
734
686
  console.log(linked
735
- ? `✅ 已建立 node_modules/dsh-remote-ui → ${pluginLocalDir}(免包管理器即可解析)`
736
- : "✅ node_modules/dsh-remote-ui 已就绪");
687
+ ? `✅ 已建立 node_modules/${PLUGIN_ID} → ${pluginLocalDir}(免包管理器即可解析)`
688
+ : `✅ node_modules/${PLUGIN_ID} 已就绪`);
737
689
  console.log(`✅ 插件安装完成(bundle 形态,无用户 include)。配置目录: ${CONFIG_DIR}`);
738
690
  return;
739
691
  }
740
692
 
741
693
  // 异常形态:有非 file: 依赖但不在 bundles(无法靠 bundle patch 激活)
742
- console.log(`ℹ 检测到依赖 dsh-remote-ui(${dep}) 但未声明在 dsh.profile.bundles——插件不会激活。`);
694
+ console.log(`ℹ 检测到依赖 ${PLUGIN_ID}(${dep}) 但未声明在 dsh.profile.bundles——插件不会激活。`);
743
695
  console.log(" 请在 dsh 插件市场重新添加该插件,或先执行 `dsh-remote plugin --uninstall` 再一键安装。");
744
696
  }
745
697
 
@@ -751,7 +703,7 @@ async function pluginCmd(argv) {
751
703
  : process.env.DSH_PROFILE_DIR || path.join(os.homedir(), ".dsh", "profiles", "web");
752
704
  const pkgFile = path.join(profileDir, "package.json");
753
705
  const patchFile = path.join(profileDir, "cordis.patch.yml");
754
- const pluginDir = path.join(THIS_DIR, "packages/dsh-remote-ui");
706
+ const pluginDir = path.join(THIS_DIR, "packages", PLUGIN_ID);
755
707
 
756
708
  if (!fs.existsSync(pkgFile) || !fs.existsSync(patchFile)) {
757
709
  console.error(`❌ 未找到 dsh web profile(${profileDir})。`);
@@ -759,31 +711,37 @@ async function pluginCmd(argv) {
759
711
  process.exit(1);
760
712
  }
761
713
  if (!fs.existsSync(pluginDir)) {
762
- console.error(`❌ 本包缺少 packages/dsh-remote-ui(${pluginDir})。`);
714
+ console.error(`❌ 本包缺少 packages/${PLUGIN_ID}(${pluginDir})。`);
763
715
  process.exit(1);
764
716
  }
765
717
 
766
718
  const patch = fs.readFileSync(patchFile, "utf8");
767
719
 
768
720
  if (uninstall) {
769
- // 移除 patch 中的插件条目(兼容旧版无标记条目)
721
+ // 移除 patch 中的插件条目(兼容旧版无标记条目;旧名 dsh-remote-ui 一并清理)
770
722
  const newPatch = stripPluginEntries(patch);
771
723
  if (newPatch !== patch) {
772
724
  fs.writeFileSync(patchFile, newPatch);
773
725
  console.log(`✅ 已从 ${patchFile} 移除插件条目`);
774
726
  } else {
775
- console.log("ℹ patch 中未发现 dsh-remote-ui 条目。");
727
+ console.log(`ℹ patch 中未发现 ${PLUGIN_ID}(或历史名)条目。`);
728
+ }
729
+ // 清理本地目录与链接:当前名 + 历史名(dsh-remote-ui-plugin / node_modules/dsh-remote-ui)
730
+ for (const id of PLUGIN_ALL_IDS) {
731
+ fs.rmSync(path.join(profileDir, `${id}-plugin`), { recursive: true, force: true });
732
+ fs.rmSync(path.join(profileDir, "node_modules", id), { recursive: true, force: true });
776
733
  }
777
- fs.rmSync(path.join(profileDir, PLUGIN_LOCAL_DIR), { recursive: true, force: true });
778
- fs.rmSync(path.join(profileDir, "node_modules", "dsh-remote-ui"), { recursive: true, force: true });
779
734
  try {
780
735
  const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
781
- if (pkg.dependencies) delete pkg.dependencies["dsh-remote-ui"];
782
- fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
736
+ let removed = false;
737
+ for (const id of PLUGIN_ALL_IDS) {
738
+ if (pkg.dependencies && pkg.dependencies[id] !== undefined) { delete pkg.dependencies[id]; removed = true; }
739
+ }
740
+ if (removed) fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
783
741
  } catch { /* ignore */ }
784
742
  // 同步移除 bundles 声明,避免“bundles 引用已删除包 → dsh web 启动报错”
785
743
  try {
786
- if (removeBundleEntry(pkgFile)) console.log("✅ 已从 dsh.profile.bundles 移除 dsh-remote-ui");
744
+ if (removeBundleEntry(pkgFile)) console.log(`✅ 已从 dsh.profile.bundles 移除 ${PLUGIN_ID}`);
787
745
  } catch { /* ignore */ }
788
746
  console.log("✅ 卸载完成。重启 dsh web 生效。");
789
747
  return;
@@ -802,7 +760,7 @@ const raw = process.argv[2];
802
760
  const cmd = raw && !raw.startsWith("-") ? raw : "setup";
803
761
  const args = raw && !raw.startsWith("-") ? process.argv.slice(3) : process.argv.slice(2);
804
762
  if (cmd === "setup" || cmd === "install") await setup(args);
805
- else if (cmd === "settings") serveSettings();
763
+ else if (cmd === "settings") settingsHint();
806
764
  else if (cmd === "run") await runBridge();
807
765
  else if (cmd === "plugin") await pluginCmd(process.argv.slice(3));
808
766
  else if (cmd === "status") {
@@ -811,13 +769,13 @@ else if (cmd === "status") {
811
769
  console.log("配置文件:", CONFIG_PATH);
812
770
  console.log("连接模式:", local ? `自建服务(${cfg.tunnel_url || "未设置服务器地址"})` : `SaaS 云端服务(${cfg.phone || "未配置账号"})`);
813
771
  console.log("API:", cfg.api_url || (local ? "(自建模式无需账号 API)" : DEFAULT_API));
814
- console.log("远程地址: 运行 settings 查看最新");
772
+ console.log("远程地址/登录: 打开 dsh web → 设置 → 「远程控制」查看与操作");
815
773
  } else {
816
774
  console.log(`dsh-remote — 手机远程控制 dsh web(隧道模式)
817
775
 
818
776
  用法:
819
777
  dsh-remote 一键安装(默认命令,无需任何参数;含插件与自启动)
820
- dsh-remote settings 打开本地设置页(登录账号 / 自建配置)
778
+ dsh-remote settings 显示登录/连接配置指引(独立设置页已移除)
821
779
  dsh-remote run 前台运行 bridge(调试)
822
780
  dsh-remote status 查看配置与服务状态
823
781
  dsh-remote plugin 手动安装 dsh web 远程控制插件(--uninstall 卸载)
@@ -825,7 +783,8 @@ else if (cmd === "status") {
825
783
  自建模式(可选):
826
784
  dsh-remote setup --server wss://你的域名:端口 --key 访问密钥
827
785
 
828
- 登录: 安装后运行 \`dsh-remote settings\`,用手机号+密码登录(或配置自建密钥)。
786
+ 登录/连接配置: 打开 dsh web → 设置 → 「远程控制」→ 注册或登录手机号即可
787
+ (自建用户切「自建服务」标签或直接用上方 setup 命令,无需另开页面)。
829
788
  文档: ${REPO_URL}
830
789
  `);
831
790
  process.exit(cmd === "help" ? 0 : 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -37,7 +37,7 @@
37
37
  "files": [
38
38
  "dsh-setup.mjs",
39
39
  "clients/dsh-remote",
40
- "packages/dsh-remote-ui",
40
+ "packages/dsh-remote-web",
41
41
  "docs/self-hosting.md"
42
42
  ],
43
43
  "workspaces": [
@@ -50,9 +50,9 @@
50
50
  "scripts": {
51
51
  "test": "npm run test:router && npm run test:plugin && npm run test:bridge",
52
52
  "test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/quotas.test.mjs",
53
- "test:plugin": "node --test --test-concurrency=1 packages/dsh-remote-ui/test/*.test.mjs",
53
+ "test:plugin": "node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
54
54
  "test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
55
- "check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-ui/lib/index.js && node --check packages/dsh-remote-ui/lib/client.js && bash -n deploy/install-open.sh"
55
+ "check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && bash -n deploy/install-open.sh"
56
56
  },
57
57
  "dependencies": {
58
58
  "ws": "^8.18.0"
@@ -1,4 +1,8 @@
1
- # dsh-remote-ui — 公网远程控制 dsh web 的插件
1
+ # dsh-remote-web — 公网远程控制 dsh web 的插件
2
+
3
+ > 曾用名 **dsh-remote-ui**(≤0.4.9);2026-09 更名 **dsh-remote-web** —— 本插件是
4
+ > dsh-remote 全家桶的 **dsh web 插件半**(远程控制宿主插件,不是纯 UI/皮肤插件),
5
+ > 名称与功能定位一致。旧名安装可由新版 `dsh-setup.mjs` 自动迁移/清理。
2
6
 
3
7
  > **给 DeepSeek Harness(dsh web)一个「随时随地」的公网入口。**
4
8
  > 安装后在 dsh web 的设置页登录账号,即得到一个专属的**加密远程地址**:人在办公室外、
@@ -57,8 +61,8 @@ npx @mrrisega/dsh-remote plugin --uninstall
57
61
 
58
62
  What it does:
59
63
 
60
- - `package.json` dependency: `"dsh-remote-ui": "link:<包目录>/packages/dsh-remote-ui"`
61
- - `cordis.patch.yml` insert: `{ id: dsh-remote-ui, name: 'dsh-remote-ui', config: { relayDir: '~/.dsh-remote' } }`
64
+ - `package.json` dependency: `"dsh-remote-web": "link:<包目录>/packages/dsh-remote-web"`
65
+ - `cordis.patch.yml` insert: `{ id: dsh-remote-web, name: 'dsh-remote-web', config: { relayDir: '~/.dsh-remote' } }`
62
66
  - runs `pnpm install` (fallback `npm install`) in the profile
63
67
 
64
68
  Restart `dsh web` afterwards. Manual alternative: edit the two files above and
@@ -75,7 +79,7 @@ restart.
75
79
  dsh --profile web --port 3090 --no-open # verify on a separate port, 3080 untouched
76
80
  ```
77
81
 
78
- Verify: `/` `__DSH_BOOT__` contains `dsh-remote-ui`; `/plugins/dsh-remote-ui/client.js`
82
+ Verify: `/` `__DSH_BOOT__` contains `dsh-remote-web`; `/plugins/dsh-remote-web/client.js`
79
83
  200; `/dsh-remote/status` returns JSON; panel renders in headless Chrome.
80
84
 
81
85
  ## Known limitations
@@ -1,7 +1,7 @@
1
- # dsh-remote-ui — dsh web 远程控制插件(dsh-remote 的插件半)
1
+ # dsh-remote-web — dsh web 远程控制插件(dsh-remote 的插件半)
2
2
  # 提供设置页「远程控制」面板与同源 /dsh-remote/* 路由。
3
3
  # relayDir 默认取 ~/.dsh-remote(与 npx @mrrisega/dsh-remote 写入的
4
4
  # .dsh-config.json 相同),也可在面板或 entry config 中覆盖。
5
5
  - insert:
6
- - id: dsh-remote-ui
7
- name: 'dsh-remote-ui'
6
+ - id: dsh-remote-web
7
+ name: 'dsh-remote-web'
@@ -1,4 +1,4 @@
1
- // dsh-remote-ui — browser half(手写 bundle,无需构建)
1
+ // dsh-remote-web — browser half(手写 bundle,无需构建)
2
2
  //
3
3
  // 格式遵循 dsh 浏览器插件约定(双半插件,bundle 手写无构建):
4
4
  // window.__ModuleLoader__.load({ id: <包名>, factory: (require) => {...} })
@@ -14,7 +14,7 @@
14
14
  // - shell.overlay:满意度弹窗(安装体验至少 1 小时后弹出,只弹一次)
15
15
  // 所有数据经同源 /dsh-remote/* 宿主路由读写(node 半提供)。
16
16
  window.__ModuleLoader__.load({
17
- id: "dsh-remote-ui",
17
+ id: "dsh-remote-web",
18
18
  factory: (require) => {
19
19
  var module = { exports: {} };
20
20
  var exports = module.exports;
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
31
31
  // 面板主体内嵌于 DSH Web「设置」页的 settings.section 栏目(官方扩展点),
32
32
  // 不再使用侧边栏入口与全窗浮动层,故不再需要 .dru-entry/.dru-backdrop/.dru-panel。
33
33
  var styleEl = document.createElement("style");
34
- styleEl.setAttribute("data-plugin", "dsh-remote-ui");
34
+ styleEl.setAttribute("data-plugin", "dsh-remote-web");
35
35
  styleEl.textContent = [
36
36
  // 设置页栏目容器(nav 选中后渲染在 settings.section 内容区)
37
37
  ".dru-settings-section{max-width:720px;display:flex;flex-direction:column;gap:14px;padding-top:2px}",
@@ -1,4 +1,4 @@
1
- // dsh-remote-ui — node half (host plugin)
1
+ // dsh-remote-web — node half (host plugin)(2026-09 由 dsh-remote-web 更名;卸载/清理兼容旧名)
2
2
  //
3
3
  // 提供 /dsh-remote/* 同源 HTTP 路由,供浏览器半的配置面板调用:
4
4
  // - 读写 dsh-remote-open/.dsh-config.json(0600)
@@ -144,7 +144,7 @@ function sweepStaleMarkers(relayDir) {
144
144
  if (dead === false || (dead === null && expired)) {
145
145
  try { rmSync(p, { force: true }); } catch { /* ignore */ }
146
146
  appendLogLine(relayDir, AUTO_INSTALL_LOG,
147
- `[dsh-remote-ui] 清理残留标记 ${name}(pid=${info.pid ?? "?"}, at=${new Date(info.at).toISOString()}${dead === false ? ", 进程已死" : ", 已超时"})`);
147
+ `[dsh-remote-web] 清理残留标记 ${name}(pid=${info.pid ?? "?"}, at=${new Date(info.at).toISOString()}${dead === false ? ", 进程已死" : ", 已超时"})`);
148
148
  }
149
149
  }
150
150
  }
@@ -392,13 +392,13 @@ function ensureRuntime(relayDir) {
392
392
  child.on("error", (e) => {
393
393
  clear();
394
394
  appendLogLine(relayDir, AUTO_INSTALL_LOG, `[auto-install] 启动失败: ${e.message}`);
395
- console.warn(`[dsh-remote-ui] 自动安装子进程启动失败: ${e.message}`);
395
+ console.warn(`[dsh-remote-web] 自动安装子进程启动失败: ${e.message}`);
396
396
  });
397
397
  child.unref();
398
- console.log(`[dsh-remote-ui] 检测到缺少桌面运行环境,已在后台自动安装(日志: ${log}),完成后将自动启动 bridge`);
398
+ console.log(`[dsh-remote-web] 检测到缺少桌面运行环境,已在后台自动安装(日志: ${log}),完成后将自动启动 bridge`);
399
399
  return false;
400
400
  } catch (e) {
401
- console.warn(`[dsh-remote-ui] 自动安装启动失败: ${e.message}`);
401
+ console.warn(`[dsh-remote-web] 自动安装启动失败: ${e.message}`);
402
402
  try { rmSync(marker, { force: true }); } catch { /* ignore */ }
403
403
  return false;
404
404
  }
@@ -789,7 +789,7 @@ async function proxyFeedback(relayDir, req, res, pathname) {
789
789
  const url = new URL(suffix.replace(/^\//, ""), base);
790
790
  const headers = {
791
791
  "x-dsh-device": cfg.device_id || "",
792
- "x-dsh-client": "dsh-remote-ui/0.1.0",
792
+ "x-dsh-client": `dsh-remote-web/${PLUGIN_VERSION}`,
793
793
  };
794
794
  if (cfg.phone) headers["x-dsh-phone"] = String(cfg.phone);
795
795
  const auth = req.headers.authorization;
@@ -836,8 +836,13 @@ async function proxyFeedback(relayDir, req, res, pathname) {
836
836
 
837
837
  // ---------- 自管理:版本 / 在线更新 / 彻底卸载(面板内“版本与更新”卡片) ----------
838
838
 
839
+ /** 插件 id / 包名(2026-09 由 dsh-remote-ui 更名)。 */
840
+ const PLUGIN_ID = "dsh-remote-web";
841
+ /** 更名前 id(≤0.4.9):彻底卸载/清理时一并移除,防旧拷贝残留。 */
842
+ const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
843
+ const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
839
844
  /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
840
- const PLUGIN_VERSION = "0.4.8";
845
+ const PLUGIN_VERSION = "0.5.0";
841
846
  const UPDATE_LOG = ".dsh-update.log";
842
847
  const UPDATE_MARKER = ".dsh-update-running";
843
848
 
@@ -925,25 +930,37 @@ function uninstallSelf(relayDir, profileDir, patchFile, pkgFile) {
925
930
  const out = { removedPatch: false, removedDep: false, removedDir: false, removedBundle: false };
926
931
  try {
927
932
  const patch = readFileSync(patchFile, "utf8");
933
+ // 兼容当前与历史(dsh-remote-ui)两种管理标记
928
934
  const cleaned = patch
929
- .replace(/\n?# >>> dsh-remote-ui .*?# <<< dsh-remote-ui\s*/s, "\n")
935
+ .replace(/\n?# >>> dsh-remote-(?:web|ui) .*?# <<< dsh-remote-(?:web|ui)\s*/s, "\n")
930
936
  .replace(/\n{3,}/g, "\n\n")
931
937
  .trimEnd() + "\n";
932
938
  if (cleaned !== patch) { writeFileSync(patchFile, cleaned); out.removedPatch = true; }
933
939
  } catch { /* 无 patch 忽略 */ }
934
940
  try {
935
941
  const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
936
- if (pkg.dependencies && pkg.dependencies["dsh-remote-ui"]) { delete pkg.dependencies["dsh-remote-ui"]; out.removedDep = true; }
942
+ if (pkg.dependencies) {
943
+ let removed = false;
944
+ for (const id of PLUGIN_ALL_IDS) {
945
+ if (pkg.dependencies[id] !== undefined) { delete pkg.dependencies[id]; removed = true; }
946
+ }
947
+ if (removed) out.removedDep = true;
948
+ }
937
949
  const bundles = pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles) ? pkg.dsh.profile.bundles : null;
938
950
  if (bundles) {
939
- const i = bundles.indexOf("dsh-remote-ui");
940
- if (i >= 0) { bundles.splice(i, 1); out.removedBundle = true; }
951
+ const filtered = bundles.filter((b) => !PLUGIN_ALL_IDS.includes(b));
952
+ if (filtered.length !== bundles.length) {
953
+ pkg.dsh.profile.bundles = filtered;
954
+ out.removedBundle = true;
955
+ }
941
956
  }
942
957
  if (out.removedDep || out.removedBundle) writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
943
958
  } catch { /* 无 package.json 忽略 */ }
944
959
  try {
945
- rmSync(join(profileDir, "dsh-remote-ui-plugin"), { recursive: true, force: true });
946
- rmSync(join(profileDir, "node_modules", "dsh-remote-ui"), { recursive: true, force: true });
960
+ for (const id of PLUGIN_ALL_IDS) {
961
+ rmSync(join(profileDir, `${id}-plugin`), { recursive: true, force: true });
962
+ rmSync(join(profileDir, "node_modules", id), { recursive: true, force: true });
963
+ }
947
964
  out.removedDir = true;
948
965
  } catch { /* ignore */ }
949
966
  return out;
@@ -957,8 +974,10 @@ function registerRoutes(ctx, relayDir) {
957
974
  let profileDir = join(homedir(), ".dsh", "profiles", "web");
958
975
  try {
959
976
  const here = fileURLToPath(import.meta.url);
960
- // 依次尝试两种安装布局;split()[0] 未命中时返回原串,需显式判断后再试下一种
961
- let m = here.split("/dsh-remote-ui-plugin/")[0];
977
+ // 依次尝试两种安装布局(当前名优先,历史名兜底);split()[0] 未命中时返回原串,需显式判断后再试下一种
978
+ let m = here.split("/dsh-remote-web-plugin/")[0];
979
+ if (m === here) m = here.split("/node_modules/dsh-remote-web/")[0];
980
+ if (m === here) m = here.split("/dsh-remote-ui-plugin/")[0];
962
981
  if (m === here) m = here.split("/node_modules/dsh-remote-ui/")[0];
963
982
  if (m !== here) profileDir = m;
964
983
  } catch { /* 保持默认 */ }
@@ -1272,7 +1291,7 @@ function registerRoutes(ctx, relayDir) {
1272
1291
  return;
1273
1292
  }
1274
1293
  Promise.resolve(route.handler(req, res)).catch((e) => {
1275
- ctx.logger?.warn?.(`dsh-remote-ui: ${route.method} ${route.path} failed: ${e?.stack || e}`);
1294
+ ctx.logger?.warn?.(`dsh-remote-web: ${route.method} ${route.path} failed: ${e?.stack || e}`);
1276
1295
  if (!res.headersSent) sendJson(res, 500, { ok: false, error: String(e?.message || e) });
1277
1296
  else res.end();
1278
1297
  });
@@ -1297,10 +1316,10 @@ export function apply(ctx, config = {}) {
1297
1316
  UNINSTALLED_DIRS.delete(relayDir);
1298
1317
  // 清理上次进程残留的安装/更新 marker(宿主被重启/强杀时子进程清理回调会丢失)
1299
1318
  sweepStaleMarkers(relayDir);
1300
- ctx.effect(() => registerRoutes(ctx, relayDir), "dsh-remote-ui: /dsh-remote routes");
1319
+ ctx.effect(() => registerRoutes(ctx, relayDir), "dsh-remote-web: /dsh-remote routes");
1301
1320
  // 0.1.2-rc.1+ 浏览器会话代持:换取 Harness 会话 Cookie 供 bridge 上游携带(手机点设备不再 401 白页)
1302
- ctx.effect(() => scheduleHarnessMint(ctx, relayDir), "dsh-remote-ui: harness browser-session mint");
1321
+ ctx.effect(() => scheduleHarnessMint(ctx, relayDir), "dsh-remote-web: harness browser-session mint");
1303
1322
  // 插件市场一键全功能:缺桌面运行环境则自动安装,登录后自动拉起 bridge(不依赖用户跑 npx)
1304
- ctx.effect(() => scheduleRuntime(relayDir), "dsh-remote-ui: runtime self-provision");
1305
- ctx.logger?.info?.(`dsh-remote-ui: /dsh-remote routes ready (relayDir=${relayDir})`);
1323
+ ctx.effect(() => scheduleRuntime(relayDir), "dsh-remote-web: runtime self-provision");
1324
+ ctx.logger?.info?.(`dsh-remote-web: /dsh-remote routes ready (relayDir=${relayDir})`);
1306
1325
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "dsh-remote-ui",
3
- "version": "0.1.0",
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).",
2
+ "name": "dsh-remote-web",
3
+ "version": "0.5.0",
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",
7
7
  "exports": {
@@ -33,11 +33,11 @@
33
33
  "repository": {
34
34
  "type": "git",
35
35
  "url": "git+https://github.com/mrRisega/dsh-remote.git",
36
- "directory": "packages/dsh-remote-ui"
36
+ "directory": "packages/dsh-remote-web"
37
37
  },
38
38
  "bugs": {
39
39
  "url": "https://github.com/mrRisega/dsh-remote/issues"
40
40
  },
41
- "homepage": "https://github.com/mrRisega/dsh-remote/tree/main/packages/dsh-remote-ui#readme",
41
+ "homepage": "https://github.com/mrRisega/dsh-remote/tree/main/packages/dsh-remote-web#readme",
42
42
  "license": "PolyForm-Noncommercial-1.0.0"
43
43
  }
@@ -58,7 +58,7 @@ test("反馈代理:附加设备身份/手机号,透传 thread_token", async
58
58
  assert.equal(echo.url, "/api/feedback");
59
59
  assert.equal(echo.headers["x-dsh-device"], "dev-testproxy123");
60
60
  assert.equal(echo.headers["x-dsh-phone"], "13800000000");
61
- assert.equal(echo.headers["x-dsh-client"], "dsh-remote-ui/0.1.0");
61
+ assert.equal(echo.headers["x-dsh-client"], "dsh-remote-web/0.5.0");
62
62
  assert.equal(echo.headers.authorization, "Bearer thread-token-abc");
63
63
  assert.equal(echo.body.category, "bug");
64
64
 
@@ -1,4 +1,4 @@
1
- // dsh-remote-ui 浏览器半回归:面板入口迁移进「设置」页 settings.section 官方扩展点。
1
+ // dsh-remote-web 浏览器半回归(2026-09 前名 dsh-remote-ui):面板入口迁移进「设置」页 settings.section 官方扩展点。
2
2
  // 覆盖:栏目注册(id/order/label)、侧边栏入口与浮动面板移除、账号区无「切换账号」、
3
3
  // 「关于 dsh-remote」说明卡片、退出登录/切换连接账号清理反馈线程凭据、首次安装红点引导。
4
4
  import assert from "node:assert/strict";