@mrrisega/dsh-remote 0.4.9 → 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 +2 -2
- package/clients/dsh-remote/dsh-bridge.mjs +1 -1
- package/dsh-setup.mjs +78 -42
- 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
|
@@ -77,7 +77,7 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
|
|
|
77
77
|
`plugin`(重装/卸载 dsh web 插件)。运行 `npx @mrrisega/dsh-remote --help` 查看完整说明。
|
|
78
78
|
|
|
79
79
|
> **版本与更新 / 卸载**:dsh 官方插件市场目前不提供更新按钮,也不会改写用户补丁(因此市场
|
|
80
|
-
> 卸载会提示「仍通过 insert 引用 dsh-remote-
|
|
80
|
+
> 卸载会提示「仍通过 insert 引用 dsh-remote-web」而拒绝)。插件设置面板里已内置管理入口
|
|
81
81
|
> (dsh web → 设置 → 「远程控制」→「🔄 版本与更新」卡片):显示当前版本、自动检测 npm 新版、
|
|
82
82
|
> **一键在线更新**(后台补运行环境并重启 bridge,完成后重启 dsh web 生效)、以及**彻底卸载**
|
|
83
83
|
> (移除补丁 include / 依赖 / bundle 与本地文件,之后市场卸载或直接重启均可完成卸载)。
|
|
@@ -109,7 +109,7 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
|
|
|
109
109
|
| relay-router | `packages/relay-router/` | 中继服务器:bridge 注册表、实时设备列表、HTTP/WS 透明代理、配额 |
|
|
110
110
|
| bridge | `clients/dsh-remote/` | 电脑端守护进程:连 router 注册,把转发帧代理到本地 `dsh web`;心跳自愈 |
|
|
111
111
|
| PWA | `clients/dsh-web/native.html` | 手机端:登录 / 注册 / 设备选择(单文件,零构建) |
|
|
112
|
-
| dsh web 插件 | `packages/dsh-remote-
|
|
112
|
+
| dsh web 插件 | `packages/dsh-remote-web/` | 设置页「远程控制」面板:连接模式 / 账号 / bridge 启停 / 反馈 |
|
|
113
113
|
| 部署脚本 | `deploy/` | `install-open.sh` 自建引导、nginx 参考配置、Dockerfile / compose |
|
|
114
114
|
|
|
115
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
|
@@ -44,7 +44,7 @@ const REPO_URL = "https://github.com/mrRisega/dsh-remote";
|
|
|
44
44
|
// npx 每次安装的缓存目录(~/.npm/_npx/<hash>)不固定:缓存一旦清理,指向它的自启动服务
|
|
45
45
|
// 就会像“找不到模块”一样崩溃。因此 npm 形态安装时把 dsh-setup.mjs + clients + 依赖(ws)
|
|
46
46
|
// 固化到 CONFIG_DIR(~/.dsh-remote),自启动服务只指向这个稳定路径。
|
|
47
|
-
// 插件(dsh-remote-ui)的“运行环境已就绪”判断同样以 CONFIG_DIR/dsh-setup.mjs 为准。
|
|
47
|
+
// 插件(dsh-remote-web;2026-09 前为 dsh-remote-ui)的“运行环境已就绪”判断同样以 CONFIG_DIR/dsh-setup.mjs 为准。
|
|
48
48
|
|
|
49
49
|
function pkgVersion() {
|
|
50
50
|
try {
|
|
@@ -462,31 +462,39 @@ async function setup(argv) {
|
|
|
462
462
|
}
|
|
463
463
|
|
|
464
464
|
// ---------- plugin:安装/卸载 dsh web 远程控制插件 ----------
|
|
465
|
-
|
|
466
|
-
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}`;
|
|
467
473
|
/** 插件在 profile 内的固定本地目录(安装时整目录拷贝)。 */
|
|
468
|
-
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));
|
|
469
477
|
|
|
470
478
|
/**
|
|
471
|
-
* include 块。name 必须是【裸包名】'dsh-remote-ui'
|
|
472
|
-
* - 加载器据此 import 节点半(
|
|
479
|
+
* include 块。name 必须是【裸包名】'${PLUGIN_ID}'(≤0.4.9 为 'dsh-remote-ui'):
|
|
480
|
+
* - 加载器据此 import 节点半(${PLUGIN_ID} 由 node_modules 链接解析);
|
|
473
481
|
* - dsh 客户端模块系统(@deepseek-ai/dsh-client-modules)用 require.resolve(
|
|
474
482
|
* '<name>/package.json') 从 profile 解析该包并读取 package.json 的
|
|
475
483
|
* dsh.client 声明 → 注入浏览器半(设置面板 UI)。相对文件路径入口只会加载
|
|
476
484
|
* 节点半、不会注入 UI —— 0.3.7/0.3.8 曾因此丢设置页。
|
|
477
|
-
* 链接由安装器自建(node_modules
|
|
485
|
+
* 链接由安装器自建(node_modules/${PLUGIN_ID} → 拷贝出的目录),不依赖 pnpm/npm。
|
|
478
486
|
*/
|
|
479
487
|
function pluginBlock(relayDir) {
|
|
480
488
|
return `${PLUGIN_MARKER_START}
|
|
481
489
|
- insert:
|
|
482
|
-
- id:
|
|
483
|
-
name: '
|
|
490
|
+
- id: ${PLUGIN_ID}
|
|
491
|
+
name: '${PLUGIN_ID}'
|
|
484
492
|
config:
|
|
485
493
|
relayDir: '${relayDir}'
|
|
486
494
|
${PLUGIN_MARKER_END}`;
|
|
487
495
|
}
|
|
488
496
|
|
|
489
|
-
/** 移除 patch
|
|
497
|
+
/** 移除 patch 中所有引用本插件(当前或历史名)的条目块(含其前置注释),返回剩余内容。 */
|
|
490
498
|
function stripPluginEntries(patch) {
|
|
491
499
|
const lines = patch.split("\n");
|
|
492
500
|
const out = [];
|
|
@@ -501,10 +509,10 @@ function stripPluginEntries(patch) {
|
|
|
501
509
|
block.push(lines[j]);
|
|
502
510
|
j++;
|
|
503
511
|
}
|
|
504
|
-
if (block.join("\n")
|
|
512
|
+
if (pluginRef(block.join("\n"))) {
|
|
505
513
|
// 连带删除块前的连续注释(旧版条目说明 / 管理标记),以及块后的收尾标记行
|
|
506
514
|
while (out.length && /^\s*#/.test(out[out.length - 1])) out.pop();
|
|
507
|
-
if (j < lines.length && /^\s*#/.test(lines[j]) && lines[j]
|
|
515
|
+
if (j < lines.length && /^\s*#/.test(lines[j]) && pluginRef(lines[j])) j++;
|
|
508
516
|
i = j;
|
|
509
517
|
continue;
|
|
510
518
|
}
|
|
@@ -535,10 +543,14 @@ function normalizePatchBase(patch) {
|
|
|
535
543
|
|
|
536
544
|
/**
|
|
537
545
|
* 把插件整目录拷贝进 profile 的固定子目录并返回该目录。
|
|
538
|
-
* 源(本包 packages
|
|
546
|
+
* 源(本包 packages/${PLUGIN_ID})可能在 npx 临时缓存里,因此必须拷到 profile 内持久化,
|
|
539
547
|
* include 用相对路径直接指向拷贝出的入口,不写依赖、不跑任何包管理器。
|
|
540
548
|
*/
|
|
541
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
|
+
}
|
|
542
554
|
const dest = path.join(profileDir, PLUGIN_LOCAL_DIR);
|
|
543
555
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
544
556
|
fs.cpSync(pluginDir, dest, {
|
|
@@ -555,11 +567,15 @@ function copyPluginIntoProfile(profileDir, pluginDir) {
|
|
|
555
567
|
return dest;
|
|
556
568
|
}
|
|
557
569
|
|
|
558
|
-
/** 把插件挂到 <profile>/node_modules
|
|
570
|
+
/** 把插件挂到 <profile>/node_modules/<PLUGIN_ID>(裸包名解析需要),指向拷贝目录;缺失/指错时重建。 */
|
|
559
571
|
function ensurePluginLinked(profileDir, pluginLocalDir) {
|
|
560
572
|
const nmDir = path.join(profileDir, "node_modules");
|
|
561
|
-
const nmPlugin = path.join(nmDir,
|
|
573
|
+
const nmPlugin = path.join(nmDir, PLUGIN_ID);
|
|
562
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
|
+
}
|
|
563
579
|
try {
|
|
564
580
|
if (fs.realpathSync(nmPlugin) === pluginLocalDir) return false;
|
|
565
581
|
} catch { /* 缺失/悬空 → 重建 */ }
|
|
@@ -576,50 +592,64 @@ function ensurePluginLinked(profileDir, pluginLocalDir) {
|
|
|
576
592
|
function declarePluginDep(pkgFile) {
|
|
577
593
|
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
578
594
|
pkg.dependencies = pkg.dependencies || {};
|
|
579
|
-
pkg.dependencies[
|
|
595
|
+
for (const id of PLUGIN_LEGACY_IDS) { delete pkg.dependencies[id]; }
|
|
596
|
+
pkg.dependencies[PLUGIN_ID] = `file:./${PLUGIN_LOCAL_DIR}`;
|
|
580
597
|
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
|
|
581
598
|
}
|
|
582
599
|
|
|
583
|
-
/** 把
|
|
600
|
+
/** 把 PLUGIN_ID 加入 dsh.profile.bundles(幂等;顺带移除历史名条目)。返回是否发生变更。 */
|
|
584
601
|
function ensureBundleEntry(pkgFile) {
|
|
585
602
|
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
586
603
|
const bundles = pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles)
|
|
587
604
|
? pkg.dsh.profile.bundles
|
|
588
605
|
: null;
|
|
589
|
-
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
|
+
}
|
|
590
615
|
pkg.dsh = pkg.dsh || {};
|
|
591
616
|
pkg.dsh.profile = pkg.dsh.profile || {};
|
|
592
|
-
pkg.dsh.profile.bundles = bundles || [];
|
|
593
|
-
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);
|
|
594
619
|
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
|
|
595
620
|
return true;
|
|
596
621
|
}
|
|
597
622
|
|
|
598
|
-
/** 从 dsh.profile.bundles
|
|
623
|
+
/** 从 dsh.profile.bundles 移除本插件(当前或历史名,幂等)。返回是否发生变更。 */
|
|
599
624
|
function removeBundleEntry(pkgFile) {
|
|
600
625
|
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
601
626
|
const bundles = pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles)
|
|
602
627
|
? pkg.dsh.profile.bundles
|
|
603
628
|
: null;
|
|
604
|
-
if (!bundles
|
|
605
|
-
|
|
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;
|
|
606
633
|
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
|
|
607
634
|
return true;
|
|
608
635
|
}
|
|
609
636
|
|
|
610
637
|
/**
|
|
611
|
-
* 插件安装(pluginCmd 非卸载分支)收敛策略 —— 2026-09-06「重复 ID
|
|
612
|
-
* 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
|
|
613
641
|
* 声明它后,加载器会自动应用插件自带的 cordis.patch.yml(节点半+浏览器半的唯一激活点)。
|
|
614
642
|
* 若用户级 cordis.patch.yml 再手工 insert 同一个 id,dsh web 启动即报“重复 ID”崩溃。
|
|
615
643
|
* 因此本命令【绝不写用户 include】,只负责:让插件以 bundle 形态可解析,
|
|
616
|
-
* 并清理历史遗留的 include
|
|
644
|
+
* 并清理历史遗留的 include 块与旧名(≤0.4.9 dsh-remote-ui)痕迹。
|
|
645
|
+
* 市场形态(github:/npm 依赖)则完全交由市场管理,只清理 include。
|
|
617
646
|
*/
|
|
618
647
|
function convergePluginActivation(profileDir, pkgFile, patchFile, pluginDir, patch) {
|
|
619
648
|
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
620
|
-
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;
|
|
621
651
|
const inBundles = !!(pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles)
|
|
622
|
-
&& pkg.dsh.profile.bundles.includes(
|
|
652
|
+
&& pkg.dsh.profile.bundles.some((b) => PLUGIN_ALL_IDS.includes(b)));
|
|
623
653
|
const marketManaged = dep && !String(dep).startsWith("file:"); // github:/npm: 等由市场/包管理器管源码
|
|
624
654
|
const managedByUs = !dep || String(dep).startsWith("file:"); // 无依赖或 file: 拷贝 → 我们管
|
|
625
655
|
|
|
@@ -651,17 +681,17 @@ function convergePluginActivation(profileDir, pkgFile, patchFile, pluginDir, pat
|
|
|
651
681
|
const linked = ensurePluginLinked(profileDir, pluginLocalDir); // 自建 node_modules 链接
|
|
652
682
|
stripInclude("bundle patch 已是唯一激活点"); // 清历史 include
|
|
653
683
|
console.log(addedBundle
|
|
654
|
-
?
|
|
655
|
-
:
|
|
684
|
+
? `✅ 已加入 dsh.profile.bundles(${PLUGIN_ID} 自带 patch 自动生效)`
|
|
685
|
+
: `ℹ dsh.profile.bundles 已含 ${PLUGIN_ID}`);
|
|
656
686
|
console.log(linked
|
|
657
|
-
? `✅ 已建立 node_modules
|
|
658
|
-
:
|
|
687
|
+
? `✅ 已建立 node_modules/${PLUGIN_ID} → ${pluginLocalDir}(免包管理器即可解析)`
|
|
688
|
+
: `✅ node_modules/${PLUGIN_ID} 已就绪`);
|
|
659
689
|
console.log(`✅ 插件安装完成(bundle 形态,无用户 include)。配置目录: ${CONFIG_DIR}`);
|
|
660
690
|
return;
|
|
661
691
|
}
|
|
662
692
|
|
|
663
693
|
// 异常形态:有非 file: 依赖但不在 bundles(无法靠 bundle patch 激活)
|
|
664
|
-
console.log(`ℹ 检测到依赖
|
|
694
|
+
console.log(`ℹ 检测到依赖 ${PLUGIN_ID}(${dep}) 但未声明在 dsh.profile.bundles——插件不会激活。`);
|
|
665
695
|
console.log(" 请在 dsh 插件市场重新添加该插件,或先执行 `dsh-remote plugin --uninstall` 再一键安装。");
|
|
666
696
|
}
|
|
667
697
|
|
|
@@ -673,7 +703,7 @@ async function pluginCmd(argv) {
|
|
|
673
703
|
: process.env.DSH_PROFILE_DIR || path.join(os.homedir(), ".dsh", "profiles", "web");
|
|
674
704
|
const pkgFile = path.join(profileDir, "package.json");
|
|
675
705
|
const patchFile = path.join(profileDir, "cordis.patch.yml");
|
|
676
|
-
const pluginDir = path.join(THIS_DIR, "packages
|
|
706
|
+
const pluginDir = path.join(THIS_DIR, "packages", PLUGIN_ID);
|
|
677
707
|
|
|
678
708
|
if (!fs.existsSync(pkgFile) || !fs.existsSync(patchFile)) {
|
|
679
709
|
console.error(`❌ 未找到 dsh web profile(${profileDir})。`);
|
|
@@ -681,31 +711,37 @@ async function pluginCmd(argv) {
|
|
|
681
711
|
process.exit(1);
|
|
682
712
|
}
|
|
683
713
|
if (!fs.existsSync(pluginDir)) {
|
|
684
|
-
console.error(`❌ 本包缺少 packages
|
|
714
|
+
console.error(`❌ 本包缺少 packages/${PLUGIN_ID}(${pluginDir})。`);
|
|
685
715
|
process.exit(1);
|
|
686
716
|
}
|
|
687
717
|
|
|
688
718
|
const patch = fs.readFileSync(patchFile, "utf8");
|
|
689
719
|
|
|
690
720
|
if (uninstall) {
|
|
691
|
-
// 移除 patch
|
|
721
|
+
// 移除 patch 中的插件条目(兼容旧版无标记条目;旧名 dsh-remote-ui 一并清理)
|
|
692
722
|
const newPatch = stripPluginEntries(patch);
|
|
693
723
|
if (newPatch !== patch) {
|
|
694
724
|
fs.writeFileSync(patchFile, newPatch);
|
|
695
725
|
console.log(`✅ 已从 ${patchFile} 移除插件条目`);
|
|
696
726
|
} else {
|
|
697
|
-
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 });
|
|
698
733
|
}
|
|
699
|
-
fs.rmSync(path.join(profileDir, PLUGIN_LOCAL_DIR), { recursive: true, force: true });
|
|
700
|
-
fs.rmSync(path.join(profileDir, "node_modules", "dsh-remote-ui"), { recursive: true, force: true });
|
|
701
734
|
try {
|
|
702
735
|
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
703
|
-
|
|
704
|
-
|
|
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");
|
|
705
741
|
} catch { /* ignore */ }
|
|
706
742
|
// 同步移除 bundles 声明,避免“bundles 引用已删除包 → dsh web 启动报错”
|
|
707
743
|
try {
|
|
708
|
-
if (removeBundleEntry(pkgFile)) console.log(
|
|
744
|
+
if (removeBundleEntry(pkgFile)) console.log(`✅ 已从 dsh.profile.bundles 移除 ${PLUGIN_ID}`);
|
|
709
745
|
} catch { /* ignore */ }
|
|
710
746
|
console.log("✅ 卸载完成。重启 dsh web 生效。");
|
|
711
747
|
return;
|
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
|