@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 +6 -9
- package/clients/dsh-remote/dsh-bridge.mjs +1 -1
- package/dsh-setup.mjs +100 -141
- package/package.json +4 -4
- package/packages/{dsh-remote-ui → dsh-remote-web}/README.md +8 -4
- package/packages/{dsh-remote-ui → dsh-remote-web}/cordis.patch.yml +3 -3
- package/packages/{dsh-remote-ui → dsh-remote-web}/lib/client.js +3 -3
- package/packages/{dsh-remote-ui → dsh-remote-web}/lib/index.js +39 -20
- package/packages/{dsh-remote-ui → dsh-remote-web}/package.json +5 -5
- package/packages/{dsh-remote-ui → dsh-remote-web}/test/feedback-proxy.test.mjs +1 -1
- package/packages/{dsh-remote-ui → dsh-remote-web}/test/settings-entry.test.mjs +1 -1
- /package/packages/{dsh-remote-ui → dsh-remote-web}/screenshots.json +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/account-info.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/account-switch.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/register-proxy.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/self-manage.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/sms-captcha-ui.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/uninstall-runtime.test.mjs +0 -0
package/README.md
CHANGED
|
@@ -63,12 +63,9 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
|
|
|
63
63
|
npx @mrrisega/dsh-remote
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
其他命令:`
|
|
76
|
+
其他命令:`status`(查看状态)、`run`(前台调试)、`settings`(仅显示登录指引)、
|
|
80
77
|
`plugin`(重装/卸载 dsh web 插件)。运行 `npx @mrrisega/dsh-remote --help` 查看完整说明。
|
|
81
78
|
|
|
82
79
|
> **版本与更新 / 卸载**:dsh 官方插件市场目前不提供更新按钮,也不会改写用户补丁(因此市场
|
|
83
|
-
> 卸载会提示「仍通过 insert 引用 dsh-remote-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
-
// ----------
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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("⚠️
|
|
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
|
-
|
|
544
|
-
const
|
|
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 =
|
|
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 节点半(
|
|
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
|
|
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:
|
|
561
|
-
name: '
|
|
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
|
|
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")
|
|
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]
|
|
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
|
|
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
|
|
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,
|
|
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[
|
|
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
|
-
/** 把
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
683
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
?
|
|
733
|
-
:
|
|
684
|
+
? `✅ 已加入 dsh.profile.bundles(${PLUGIN_ID} 自带 patch 自动生效)`
|
|
685
|
+
: `ℹ dsh.profile.bundles 已含 ${PLUGIN_ID}`);
|
|
734
686
|
console.log(linked
|
|
735
|
-
? `✅ 已建立 node_modules
|
|
736
|
-
:
|
|
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(`ℹ 检测到依赖
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
782
|
-
|
|
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(
|
|
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")
|
|
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("
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
61
|
-
- `cordis.patch.yml` insert: `{ id: 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-
|
|
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-
|
|
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-
|
|
7
|
-
name: 'dsh-remote-
|
|
6
|
+
- id: dsh-remote-web
|
|
7
|
+
name: 'dsh-remote-web'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-remote-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
395
|
+
console.warn(`[dsh-remote-web] 自动安装子进程启动失败: ${e.message}`);
|
|
396
396
|
});
|
|
397
397
|
child.unref();
|
|
398
|
-
console.log(`[dsh-remote-
|
|
398
|
+
console.log(`[dsh-remote-web] 检测到缺少桌面运行环境,已在后台自动安装(日志: ${log}),完成后将自动启动 bridge`);
|
|
399
399
|
return false;
|
|
400
400
|
} catch (e) {
|
|
401
|
-
console.warn(`[dsh-remote-
|
|
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":
|
|
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.
|
|
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
|
|
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
|
|
940
|
-
if (
|
|
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
|
-
|
|
946
|
-
|
|
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
|
-
//
|
|
961
|
-
let m = here.split("/dsh-remote-
|
|
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-
|
|
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-
|
|
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-
|
|
1321
|
+
ctx.effect(() => scheduleHarnessMint(ctx, relayDir), "dsh-remote-web: harness browser-session mint");
|
|
1303
1322
|
// 插件市场一键全功能:缺桌面运行环境则自动安装,登录后自动拉起 bridge(不依赖用户跑 npx)
|
|
1304
|
-
ctx.effect(() => scheduleRuntime(relayDir), "dsh-remote-
|
|
1305
|
-
ctx.logger?.info?.(`dsh-remote-
|
|
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-
|
|
3
|
-
"version": "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-
|
|
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-
|
|
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-
|
|
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
|
|
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";
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|