@mrrisega/dsh-remote 0.4.1 → 0.4.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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",
|
|
@@ -124,6 +124,11 @@ window.__ModuleLoader__.load({
|
|
|
124
124
|
".dru-popup-foot button{border:none;background:none;color:#57606a;cursor:pointer;font-size:12px;font-family:inherit;padding:4px 6px}",
|
|
125
125
|
".dru-popup-foot button:hover{color:#0969da}",
|
|
126
126
|
".dru-popup .dru-msg{text-align:left}",
|
|
127
|
+
// ── 自管理:版本与更新(插件面板内提供在线更新/彻底卸载,市场无更新按钮) ──
|
|
128
|
+
".dru-ver-badge{display:inline-block;font-size:11px;border-radius:999px;padding:1px 8px;margin-left:6px;vertical-align:1px}",
|
|
129
|
+
".dru-ver-badge-new{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
|
|
130
|
+
".dru-ver-badge-ok{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
|
|
131
|
+
".dru-up-log{margin-top:8px;background:#0d1117;color:#e6edf3;border-radius:8px;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:150px;overflow:auto}",
|
|
127
132
|
].join("\n");
|
|
128
133
|
document.head.appendChild(styleEl);
|
|
129
134
|
|
|
@@ -607,6 +612,161 @@ window.__ModuleLoader__.load({
|
|
|
607
612
|
// 通用「远程控制」图标(🖥 风格 emoji,与面板内既有 emoji 图标体系一致):
|
|
608
613
|
// 栏目导航 label 与栏目头部均使用它。原侧边栏入口(电源图标按钮)已随入口迁移移除。
|
|
609
614
|
|
|
615
|
+
// ── 版本与更新卡片(自管理:市场没有更新按钮,这里提供在线一键更新/彻底卸载) ──
|
|
616
|
+
// 数据来自 node 半新增的 /dsh-remote/self* 路由;逻辑均在插件 node 半实现,
|
|
617
|
+
// 因此无论插件从「插件市场」还是 npx 安装,界面与行为完全一致。
|
|
618
|
+
function SelfManageCard() {
|
|
619
|
+
var verArr = useState(null); var ver = verArr[0]; var setVer = verArr[1]; // {version, runtimeReady}
|
|
620
|
+
var chkArr = useState(null); var chk = chkArr[0]; var setChk = chkArr[1]; // {current, latest, outdated}
|
|
621
|
+
var chkBusyArr = useState(false); var chkBusy = chkBusyArr[0]; var setChkBusy = chkBusyArr[1];
|
|
622
|
+
var upBusyArr = useState(false); var upBusy = upBusyArr[0]; var setUpBusy = upBusyArr[1];
|
|
623
|
+
var unBusyArr = useState(false); var unBusy = unBusyArr[0]; var setUnBusy = unBusyArr[1];
|
|
624
|
+
var logArr = useState(""); var log = logArr[0]; var setLog = logArr[1]; // 更新日志尾部
|
|
625
|
+
var updArr = useState(false); var updating = updArr[0]; var setUpdating = updArr[1]; // 更新任务是否仍在跑
|
|
626
|
+
var doneArr = useState(false); var updated = doneArr[0]; var setUpdated = doneArr[1]; // 本轮已更新完成(提示重启)
|
|
627
|
+
var armArr = useState(false); var armed = armArr[0]; var setArmed = armArr[1]; // 彻底卸载二次确认
|
|
628
|
+
var msgArr = useState(null); var selfMsg = msgArr[0]; var setSelfMsg = msgArr[1]; // {kind, text}
|
|
629
|
+
|
|
630
|
+
var loadVer = useCallback(function () {
|
|
631
|
+
api("/dsh-remote/self").then(function (b) {
|
|
632
|
+
if (b && b.ok) setVer(b);
|
|
633
|
+
}).catch(function () {});
|
|
634
|
+
}, []);
|
|
635
|
+
|
|
636
|
+
var doCheck = useCallback(function () {
|
|
637
|
+
setChkBusy(true);
|
|
638
|
+
api("/dsh-remote/self/update-check").then(function (b) {
|
|
639
|
+
if (b && b.ok) setChk(b);
|
|
640
|
+
else setSelfMsg({ kind: "err", text: "检查更新失败:" + ((b && (b.error || b.detail)) || "未知错误") });
|
|
641
|
+
}).catch(function (e) {
|
|
642
|
+
setSelfMsg({ kind: "err", text: "无法连接更新服务:" + e.message });
|
|
643
|
+
}).finally(function () { setChkBusy(false); });
|
|
644
|
+
}, []);
|
|
645
|
+
|
|
646
|
+
// 轮询更新日志:点击一键更新后,每 2s 拉一次日志;直到 running=false 视为完成
|
|
647
|
+
var logTimer = useCallback(function (force) {
|
|
648
|
+
if (!force && updating) return;
|
|
649
|
+
api("/dsh-remote/self/update-log").then(function (b) {
|
|
650
|
+
if (b && b.ok) {
|
|
651
|
+
setLog(b.log || "");
|
|
652
|
+
if (!b.running) {
|
|
653
|
+
setUpdating(false);
|
|
654
|
+
setUpdated(true);
|
|
655
|
+
loadVer();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
setUpdating(true);
|
|
660
|
+
}).catch(function () { setUpdating(false); });
|
|
661
|
+
}, [updating, loadVer]);
|
|
662
|
+
|
|
663
|
+
useEffect(function () { loadVer(); doCheck(); }, [loadVer, doCheck]);
|
|
664
|
+
useEffect(function () {
|
|
665
|
+
if (!updating) return;
|
|
666
|
+
var iv = setInterval(function () {
|
|
667
|
+
api("/dsh-remote/self/update-log").then(function (b) {
|
|
668
|
+
if (!b || !b.ok) return;
|
|
669
|
+
setLog(b.log || "");
|
|
670
|
+
if (!b.running) {
|
|
671
|
+
clearInterval(iv);
|
|
672
|
+
setUpdating(false);
|
|
673
|
+
setUpdated(true);
|
|
674
|
+
loadVer();
|
|
675
|
+
doCheck();
|
|
676
|
+
}
|
|
677
|
+
}).catch(function () {});
|
|
678
|
+
}, 2000);
|
|
679
|
+
return function () { clearInterval(iv); };
|
|
680
|
+
}, [updating, loadVer, doCheck]);
|
|
681
|
+
|
|
682
|
+
var doUpdate = function () {
|
|
683
|
+
setUpBusy(true);
|
|
684
|
+
setUpdated(false);
|
|
685
|
+
setSelfMsg(null);
|
|
686
|
+
post("/dsh-remote/self/update", {}).then(function (b) {
|
|
687
|
+
if (b && b.ok) {
|
|
688
|
+
setSelfMsg({ kind: "ok", text: "更新已在后台开始,正在下载安装…(本页会实时显示进度日志)" });
|
|
689
|
+
setUpdating(true);
|
|
690
|
+
logTimer(true);
|
|
691
|
+
} else {
|
|
692
|
+
var detail = String((b && (b.detail || b.error)) || "更新启动失败");
|
|
693
|
+
// 另一种常见情况:node 半返回 ok:false + “已有更新在进行中” → 转为跟踪进度而非报错
|
|
694
|
+
api("/dsh-remote/self/update-log").then(function (lb) {
|
|
695
|
+
if (lb && lb.ok && lb.running) {
|
|
696
|
+
setSelfMsg({ kind: "ok", text: "检测到已有一次更新正在进行,正在跟踪进度…" });
|
|
697
|
+
setUpdating(true);
|
|
698
|
+
logTimer(true);
|
|
699
|
+
} else {
|
|
700
|
+
setSelfMsg({ kind: "err", text: detail });
|
|
701
|
+
}
|
|
702
|
+
}).catch(function () { setSelfMsg({ kind: "err", text: detail }); });
|
|
703
|
+
}
|
|
704
|
+
}).catch(function (e) {
|
|
705
|
+
setSelfMsg({ kind: "err", text: "更新失败:" + e.message });
|
|
706
|
+
}).finally(function () { setUpBusy(false); });
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
var doUninstall = function () {
|
|
710
|
+
if (!armed) { setArmed(true); return; }
|
|
711
|
+
setUnBusy(true);
|
|
712
|
+
setSelfMsg(null);
|
|
713
|
+
post("/dsh-remote/self/uninstall", {}).then(function (b) {
|
|
714
|
+
if (b && b.ok) {
|
|
715
|
+
setArmed(false);
|
|
716
|
+
setSelfMsg({ kind: "ok", text: "已移除插件引用与本地文件。请重启 dsh web:插件将完全卸载(本栏目也会消失)。之后如需重新安装,直接在插件市场再次安装即可。" });
|
|
717
|
+
} else {
|
|
718
|
+
setArmed(false);
|
|
719
|
+
setSelfMsg({ kind: "err", text: "卸载失败:" + ((b && (b.error || b.detail)) || "未知错误") });
|
|
720
|
+
}
|
|
721
|
+
}).catch(function (e) {
|
|
722
|
+
setArmed(false);
|
|
723
|
+
setSelfMsg({ kind: "err", text: "卸载失败:" + e.message });
|
|
724
|
+
}).finally(function () { setUnBusy(false); });
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
var outdated = !!(chk && chk.outdated && chk.latest && chk.latest !== chk.current);
|
|
728
|
+
var currentV = (ver && ver.version) || (chk && chk.current) || "…";
|
|
729
|
+
var runtimeReady = ver ? !!ver.runtimeReady : null;
|
|
730
|
+
|
|
731
|
+
return h("div", { className: "dru-card", style: { marginTop: 2 } },
|
|
732
|
+
h("h3", null, "🔄 版本与更新"),
|
|
733
|
+
h("div", { className: "dru-status-line" },
|
|
734
|
+
h("span", null, "插件版本 v" + currentV),
|
|
735
|
+
chk === null && chkBusy ? h("span", { className: "dru-meta", style: { margin: 0 } }, "(检查新版本中…)") : null,
|
|
736
|
+
chk && outdated
|
|
737
|
+
? h("span", { className: "dru-ver-badge dru-ver-badge-new" }, "发现新版本 v" + chk.latest)
|
|
738
|
+
: chk && !outdated ? h("span", { className: "dru-ver-badge dru-ver-badge-ok" }, "已是最新版本") : null
|
|
739
|
+
),
|
|
740
|
+
h("div", { className: "dru-meta" },
|
|
741
|
+
runtimeReady === false ? "⚠ 桌面运行环境缺失(点击下方「一键更新」会自动补全并启动)" : runtimeReady === true ? "桌面运行环境正常" : "读取运行环境中…"
|
|
742
|
+
),
|
|
743
|
+
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
744
|
+
outdated
|
|
745
|
+
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
|
|
746
|
+
upBusy ? "更新启动中…" : updating ? "正在更新…" : "一键更新到 v" + chk.latest)
|
|
747
|
+
: h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
|
|
748
|
+
updating ? "正在更新…" : (ver && !ver.runtimeReady) ? "安装并启动(一键修复)" : "重新检查 / 修复"),
|
|
749
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: chkBusy || updating || upBusy, onClick: doCheck }, chkBusy ? "检查中…" : "检查更新"),
|
|
750
|
+
h("button", {
|
|
751
|
+
type: "button",
|
|
752
|
+
className: "dru-btn dru-btn-danger",
|
|
753
|
+
style: { marginLeft: "auto" },
|
|
754
|
+
disabled: unBusy || updating || upBusy,
|
|
755
|
+
onClick: doUninstall
|
|
756
|
+
}, unBusy ? "卸载中…" : armed ? "⚠ 再点一次确认彻底卸载" : "彻底卸载")
|
|
757
|
+
),
|
|
758
|
+
updated
|
|
759
|
+
? h("div", { className: "dru-msg dru-msg-ok" },
|
|
760
|
+
"✅ 更新已完成,最新代码已就位。请", h("strong", null, "重启 dsh web"), "后生效;桌面 bridge 会随系统自启自动运行新版本。")
|
|
761
|
+
: null,
|
|
762
|
+
log ? h("div", { className: "dru-up-log", title: "更新日志(尾部)" }, log) : null,
|
|
763
|
+
selfMsg ? h("div", { className: "dru-msg dru-msg-" + selfMsg.kind }, selfMsg.text) : null,
|
|
764
|
+
h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
765
|
+
armed ? "卸载会移除插件引用与本地文件;远程控制用的桌面 bridge 与数据目录保留,可随时重新安装。" :
|
|
766
|
+
"插件市场没有更新/卸载按钮(dsh 官方市场暂不提供),本卡片即官方管理入口:检测新版、一键在线更新、彻底卸载都在这里完成。")
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
610
770
|
// ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
|
|
611
771
|
function RemoteControlSection(props) {
|
|
612
772
|
|
|
@@ -1023,6 +1183,8 @@ window.__ModuleLoader__.load({
|
|
|
1023
1183
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "③ 也可以自建:项目完全开源,有服务器可自行部署,流量走自己的服务器,闭环自控。"),
|
|
1024
1184
|
h("div", { className: "dru-hint" }, "④ 一句话总结:简单省心用 SaaS,技术玩家可自建。")
|
|
1025
1185
|
]),
|
|
1186
|
+
// 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
|
|
1187
|
+
h(SelfManageCard, null),
|
|
1026
1188
|
message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
|
|
1027
1189
|
);
|
|
1028
1190
|
}
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
// - 读写 dsh-remote-open/.dsh-config.json(0600)
|
|
5
5
|
// - 查询/启停 bridge(launchctl,plist 缺失时自动生成,逻辑与 dsh-setup.mjs 一致)
|
|
6
6
|
// - 代理 relay API(captcha / register / login / public-config),直连、不走系统代理
|
|
7
|
+
// - 自管理 self*(版本可见 / 新版检测 / 一键在线更新 / 彻底卸载):插件市场没有更新卸载按钮,
|
|
8
|
+
// 面板内即官方管理入口;更新=后台 npx @mrrisega/dsh-remote@latest(幂等补齐运行环境并重启 bridge)
|
|
9
|
+
// - 运行时自愈:缺运行环境自动后台安装、登录后自动拉起 bridge(0.4.2 起)
|
|
10
|
+
// - 0.1.2+ ?token 浏览器鉴权会话代持(0.4.1 起)
|
|
7
11
|
//
|
|
8
12
|
// 不依赖任何第三方包:只使用 node 内置模块与 cordis 注入的 webServer 服务。
|
|
9
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync, accessSync, chmodSync, constants as fsConstants } from "node:fs";
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync, accessSync, chmodSync, openSync, closeSync, rmSync, constants as fsConstants } from "node:fs";
|
|
10
14
|
import { join, dirname } from "node:path";
|
|
11
|
-
import { execSync } from "node:child_process";
|
|
15
|
+
import { execSync, spawn } from "node:child_process";
|
|
12
16
|
import { homedir, hostname, platform } from "node:os";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
13
18
|
|
|
14
19
|
/** 本插件在 host 侧的服务依赖。 */
|
|
15
20
|
export const inject = ["webServer"];
|
|
@@ -250,6 +255,74 @@ function manualStatus() {
|
|
|
250
255
|
return { watcher, bridge };
|
|
251
256
|
}
|
|
252
257
|
|
|
258
|
+
// ---------- 插件市场一键全功能:缺桌面运行环境时自动后台安装 dsh-remote ----------
|
|
259
|
+
|
|
260
|
+
const PROVISION_MARKER = ".dsh-setup-installing";
|
|
261
|
+
const AUTO_INSTALL_LOG = ".dsh-setup-install.log";
|
|
262
|
+
const STALE_MARKER_MS = 30 * 60 * 1000; // 超过该时长视为上次进程残留,插件启动时清理
|
|
263
|
+
|
|
264
|
+
/** 向日志追加一行(多个子进程写同一日志用 append 模式,互不覆盖)。 */
|
|
265
|
+
function appendLogLine(relayDir, name, line) {
|
|
266
|
+
try {
|
|
267
|
+
const fd = openSync(join(relayDir, name), "a");
|
|
268
|
+
try {
|
|
269
|
+
writeFileSync(fd, `\n${line}\n`);
|
|
270
|
+
} finally {
|
|
271
|
+
closeSync(fd);
|
|
272
|
+
}
|
|
273
|
+
} catch { /* 非关键 */ }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 清理“进程残留”标记:dsh web 在后台安装/更新期间被重启/强杀时,
|
|
278
|
+
* 子进程的清理回调会随宿主进程一起丢失,marker 会永久卡住后续安装/更新。
|
|
279
|
+
* 插件每次启动时把超时的 marker 清掉(内容=创建时间戳)。
|
|
280
|
+
*/
|
|
281
|
+
function sweepStaleMarkers(relayDir) {
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
for (const name of [PROVISION_MARKER, UPDATE_MARKER]) {
|
|
284
|
+
const p = join(relayDir, name);
|
|
285
|
+
try {
|
|
286
|
+
const t = Number((readFileSync(p, "utf8") || "0").trim());
|
|
287
|
+
if (Number.isFinite(t) && t > 0 && now - t > STALE_MARKER_MS) {
|
|
288
|
+
rmSync(p, { force: true });
|
|
289
|
+
appendLogLine(relayDir, AUTO_INSTALL_LOG, `[dsh-remote-ui] 清理残留标记 ${name}(${new Date(t).toISOString()} 创建,已超时)`);
|
|
290
|
+
}
|
|
291
|
+
} catch { /* 无文件等 → 忽略 */ }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 插件市场只装了 UI 插件;若桌面缺 dsh-remote 运行环境(dsh-setup.mjs=bridge/自启动),
|
|
296
|
+
* 由插件在后台自动执行一次 `npx @mrrisega/dsh-remote` 补齐,用户无需手动跑命令。
|
|
297
|
+
* 已有环境(包括手动 npx 装过)直接跳过。返回 true=已就绪。 */
|
|
298
|
+
function ensureRuntime(relayDir) {
|
|
299
|
+
if (existsSync(join(relayDir, "dsh-setup.mjs"))) return true;
|
|
300
|
+
const marker = join(relayDir, PROVISION_MARKER);
|
|
301
|
+
if (existsSync(marker)) return false; // 正在安装中
|
|
302
|
+
try {
|
|
303
|
+
mkdirSync(relayDir, { recursive: true });
|
|
304
|
+
writeFileSync(marker, String(Date.now()), { mode: 0o600 });
|
|
305
|
+
const log = join(relayDir, AUTO_INSTALL_LOG);
|
|
306
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
307
|
+
const child = spawn(npx, ["--yes", "@mrrisega/dsh-remote"], {
|
|
308
|
+
detached: true,
|
|
309
|
+
stdio: ["ignore", openSync(log, "a"), openSync(log, "a")]
|
|
310
|
+
});
|
|
311
|
+
child.on("exit", (code) => {
|
|
312
|
+
// 安装结束即移除 marker(无论成败,失败由下一轮 scheduleRuntime 重试)
|
|
313
|
+
try { rmSync(marker, { force: true }); } catch { /* ignore */ }
|
|
314
|
+
appendLogLine(relayDir, AUTO_INSTALL_LOG, `[auto-install] npx 退出 code=${code ?? "?"}`);
|
|
315
|
+
});
|
|
316
|
+
child.unref();
|
|
317
|
+
console.log(`[dsh-remote-ui] 检测到缺少桌面运行环境,已在后台自动安装(日志: ${log}),完成后将自动启动 bridge`);
|
|
318
|
+
return false;
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.warn(`[dsh-remote-ui] 自动安装启动失败: ${e.message}`);
|
|
321
|
+
try { rmSync(marker, { force: true }); } catch { /* ignore */ }
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
253
326
|
/** 生成 plist(与 dsh-setup.mjs writeAutostartFile 同构),返回路径。 */
|
|
254
327
|
function writeAutostartFile(relayDir) {
|
|
255
328
|
const plistPath = launchAgentPath();
|
|
@@ -291,6 +364,29 @@ function startBridge(relayDir) {
|
|
|
291
364
|
return { ok: st.running, status: st.running ? "running" : "failed", pid: st.pid, detail: st.running ? void 0 : "服务未进入运行态" };
|
|
292
365
|
}
|
|
293
366
|
|
|
367
|
+
/** 运行时自愈 watcher:账号就绪后若环境缺失则自动安装;装好/重启后自动拉起 bridge。 */
|
|
368
|
+
function scheduleRuntime(relayDir) {
|
|
369
|
+
let done = false;
|
|
370
|
+
const iv = setInterval(() => {
|
|
371
|
+
if (done) { clearInterval(iv); return; }
|
|
372
|
+
try {
|
|
373
|
+
const cfg = loadConfig(relayDir);
|
|
374
|
+
const hasAcct = Boolean((cfg.phone || cfg.email) && cfg.password) || Boolean(cfg.local_key);
|
|
375
|
+
if (!hasAcct) return;
|
|
376
|
+
const setupUrl = join(relayDir, "dsh-setup.mjs");
|
|
377
|
+
if (!existsSync(setupUrl)) {
|
|
378
|
+
ensureRuntime(relayDir);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const st = launchdStatus();
|
|
382
|
+
if (st.running) { done = true; clearInterval(iv); return; }
|
|
383
|
+
startBridge(relayDir);
|
|
384
|
+
} catch { /* 下一轮再试 */ }
|
|
385
|
+
}, 12_000);
|
|
386
|
+
iv.unref?.();
|
|
387
|
+
return () => clearInterval(iv);
|
|
388
|
+
}
|
|
389
|
+
|
|
294
390
|
/** 停止 bridge:launchctl bootout。 */
|
|
295
391
|
function stopBridge() {
|
|
296
392
|
const target = launchTarget();
|
|
@@ -538,11 +634,167 @@ async function proxyFeedback(relayDir, req, res, pathname) {
|
|
|
538
634
|
}
|
|
539
635
|
}
|
|
540
636
|
|
|
637
|
+
// ---------- 自管理:版本 / 在线更新 / 彻底卸载(面板内“版本与更新”卡片) ----------
|
|
638
|
+
|
|
639
|
+
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
640
|
+
const PLUGIN_VERSION = "0.4.3";
|
|
641
|
+
const UPDATE_LOG = ".dsh-update.log";
|
|
642
|
+
const UPDATE_MARKER = ".dsh-update-running";
|
|
643
|
+
|
|
644
|
+
/** 查询 npm 最新版(官方源优先,失败回退 npmmirror;纯服务端无 CORS 限制)。 */
|
|
645
|
+
async function npmLatestVersion() {
|
|
646
|
+
for (const reg of ["https://registry.npmjs.org/@mrrisega/dsh-remote", "https://registry.npmmirror.com/@mrrisega/dsh-remote"]) {
|
|
647
|
+
try {
|
|
648
|
+
const res = await fetch(reg, { signal: AbortSignal.timeout(8000) });
|
|
649
|
+
if (!res.ok) continue;
|
|
650
|
+
const j = await res.json();
|
|
651
|
+
if (j && j["dist-tags"] && typeof j["dist-tags"].latest === "string") return j["dist-tags"].latest;
|
|
652
|
+
} catch { /* 试下一个源 */ }
|
|
653
|
+
}
|
|
654
|
+
return "";
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** 以 detached 子进程执行 `npx --yes @mrrisega/dsh-remote@latest`(env 可覆盖 npm 源)。 */
|
|
658
|
+
function spawnUpdater(relayDir, extraEnv) {
|
|
659
|
+
const log = join(relayDir, UPDATE_LOG);
|
|
660
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
661
|
+
return spawn(npx, ["--yes", "@mrrisega/dsh-remote@latest"], {
|
|
662
|
+
detached: true,
|
|
663
|
+
cwd: homedir(),
|
|
664
|
+
env: { ...process.env, ...(extraEnv || {}) },
|
|
665
|
+
stdio: ["ignore", openSync(log, "a"), openSync(log, "a")]
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* 后台执行在线一键更新:npx @mrrisega/dsh-remote@latest(幂等自愈:补运行环境/更新 bridge/重写 include)。
|
|
671
|
+
* 稳健性:
|
|
672
|
+
* - 默认 npx 源(国内常为 npmmirror)未同步到最新版导致失败时,自动用官方 npm 源重试一次;
|
|
673
|
+
* - marker 由子进程退出回调清理;若宿主 dsh web 在更新期间被重启,残留 marker 由启动时的 sweepStaleMarkers 兜底。
|
|
674
|
+
*/
|
|
675
|
+
function runOnlineUpdate(relayDir) {
|
|
676
|
+
try {
|
|
677
|
+
mkdirSync(relayDir, { recursive: true });
|
|
678
|
+
const marker = join(relayDir, UPDATE_MARKER);
|
|
679
|
+
if (existsSync(marker)) return { ok: false, detail: "已有更新在进行中,请稍候" };
|
|
680
|
+
writeFileSync(marker, String(Date.now()), { mode: 0o600 });
|
|
681
|
+
appendLogLine(relayDir, UPDATE_LOG, `[update] 开始在线更新 @mrrisega/dsh-remote@latest (${new Date().toISOString()})`);
|
|
682
|
+
|
|
683
|
+
let retried = false;
|
|
684
|
+
const run = () => {
|
|
685
|
+
const child = spawnUpdater(relayDir, retried ? { npm_config_registry: "https://registry.npmjs.org" } : {});
|
|
686
|
+
child.on("exit", (code) => {
|
|
687
|
+
if (!retried && code !== 0) {
|
|
688
|
+
retried = true;
|
|
689
|
+
appendLogLine(relayDir, UPDATE_LOG, `[update] 默认源安装失败(exit=${code}),改用官方 npm 源重试…`);
|
|
690
|
+
run();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
appendLogLine(relayDir, UPDATE_LOG, `[update] npx 退出 code=${code ?? "?"}(默认源${retried ? "/官方源" : ""})`);
|
|
694
|
+
try { rmSync(marker, { force: true }); } catch { /* ignore */ }
|
|
695
|
+
});
|
|
696
|
+
child.unref();
|
|
697
|
+
return child;
|
|
698
|
+
};
|
|
699
|
+
const child = run();
|
|
700
|
+
return { ok: true, pid: child.pid, log: join(relayDir, UPDATE_LOG) };
|
|
701
|
+
} catch (e) {
|
|
702
|
+
try { rmSync(join(relayDir, UPDATE_MARKER), { force: true }); } catch { /* ignore */ }
|
|
703
|
+
return { ok: false, detail: String(e.message || e) };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** 读日志尾部(更新进度展示)。 */
|
|
708
|
+
function tailOf(filePath, lines = 24) {
|
|
709
|
+
try {
|
|
710
|
+
const all = readFileSync(filePath, "utf8").split("\n");
|
|
711
|
+
return all.slice(-lines).join("\n");
|
|
712
|
+
} catch { return ""; }
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** 彻底卸载(兼容市场“拒绝改写用户补丁”的场景):移除 include、依赖、bundle、本地目录与链接。 */
|
|
716
|
+
function uninstallSelf(relayDir, profileDir, patchFile, pkgFile) {
|
|
717
|
+
const out = { removedPatch: false, removedDep: false, removedDir: false, removedBundle: false };
|
|
718
|
+
try {
|
|
719
|
+
const patch = readFileSync(patchFile, "utf8");
|
|
720
|
+
const cleaned = patch
|
|
721
|
+
.replace(/\n?# >>> dsh-remote-ui .*?# <<< dsh-remote-ui\s*/s, "\n")
|
|
722
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
723
|
+
.trimEnd() + "\n";
|
|
724
|
+
if (cleaned !== patch) { writeFileSync(patchFile, cleaned); out.removedPatch = true; }
|
|
725
|
+
} catch { /* 无 patch 忽略 */ }
|
|
726
|
+
try {
|
|
727
|
+
const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
|
|
728
|
+
if (pkg.dependencies && pkg.dependencies["dsh-remote-ui"]) { delete pkg.dependencies["dsh-remote-ui"]; out.removedDep = true; }
|
|
729
|
+
const bundles = pkg.dsh && pkg.dsh.profile && Array.isArray(pkg.dsh.profile.bundles) ? pkg.dsh.profile.bundles : null;
|
|
730
|
+
if (bundles) {
|
|
731
|
+
const i = bundles.indexOf("dsh-remote-ui");
|
|
732
|
+
if (i >= 0) { bundles.splice(i, 1); out.removedBundle = true; }
|
|
733
|
+
}
|
|
734
|
+
if (out.removedDep || out.removedBundle) writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
|
|
735
|
+
} catch { /* 无 package.json 忽略 */ }
|
|
736
|
+
try {
|
|
737
|
+
rmSync(join(profileDir, "dsh-remote-ui-plugin"), { recursive: true, force: true });
|
|
738
|
+
rmSync(join(profileDir, "node_modules", "dsh-remote-ui"), { recursive: true, force: true });
|
|
739
|
+
out.removedDir = true;
|
|
740
|
+
} catch { /* ignore */ }
|
|
741
|
+
return out;
|
|
742
|
+
}
|
|
743
|
+
|
|
541
744
|
// ---------- 路由 ----------
|
|
542
745
|
|
|
543
746
|
/** 路由表:{method, path, handler}。 */
|
|
544
747
|
function registerRoutes(ctx, relayDir) {
|
|
748
|
+
// 本插件所在 profile(由插件自身文件位置推导,覆盖市场 git 安装与本地 include 两种形态)
|
|
749
|
+
let profileDir = join(homedir(), ".dsh", "profiles", "web");
|
|
750
|
+
try {
|
|
751
|
+
const here = fileURLToPath(import.meta.url);
|
|
752
|
+
// 依次尝试两种安装布局;split()[0] 未命中时返回原串,需显式判断后再试下一种
|
|
753
|
+
let m = here.split("/dsh-remote-ui-plugin/")[0];
|
|
754
|
+
if (m === here) m = here.split("/node_modules/dsh-remote-ui/")[0];
|
|
755
|
+
if (m !== here) profileDir = m;
|
|
756
|
+
} catch { /* 保持默认 */ }
|
|
545
757
|
const routes = [
|
|
758
|
+
// 自管理:版本信息 / 检查更新 / 一键更新 / 更新日志 / 彻底卸载
|
|
759
|
+
{
|
|
760
|
+
method: "GET",
|
|
761
|
+
path: "/dsh-remote/self",
|
|
762
|
+
handler: async (_req, res) => {
|
|
763
|
+
const runtimeReady = existsSync(join(relayDir, "dsh-setup.mjs"));
|
|
764
|
+
sendJson(res, 200, { ok: true, version: PLUGIN_VERSION, runtimeReady, relayDir });
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
method: "GET",
|
|
769
|
+
path: "/dsh-remote/self/update-check",
|
|
770
|
+
handler: async (_req, res) => {
|
|
771
|
+
const latest = await npmLatestVersion();
|
|
772
|
+
const current = PLUGIN_VERSION;
|
|
773
|
+
sendJson(res, 200, { ok: true, current, latest, outdated: !!latest && latest !== current });
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
method: "POST",
|
|
778
|
+
path: "/dsh-remote/self/update",
|
|
779
|
+
handler: async (_req, res) => {
|
|
780
|
+
sendJson(res, 200, { ok: true, ...runOnlineUpdate(relayDir) });
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
method: "GET",
|
|
785
|
+
path: "/dsh-remote/self/update-log",
|
|
786
|
+
handler: async (_req, res) => {
|
|
787
|
+
sendJson(res, 200, { ok: true, running: existsSync(join(relayDir, UPDATE_MARKER)), log: tailOf(join(relayDir, UPDATE_LOG)) });
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
method: "POST",
|
|
792
|
+
path: "/dsh-remote/self/uninstall",
|
|
793
|
+
handler: async (_req, res) => {
|
|
794
|
+
const r = uninstallSelf(relayDir, profileDir, join(profileDir, "cordis.patch.yml"), join(profileDir, "package.json"));
|
|
795
|
+
sendJson(res, 200, { ok: true, ...r, detail: "已移除插件引用与本地文件,重启 dsh web 后完全卸载生效" });
|
|
796
|
+
},
|
|
797
|
+
},
|
|
546
798
|
{
|
|
547
799
|
method: "GET",
|
|
548
800
|
path: "/dsh-remote/status",
|
|
@@ -811,8 +1063,12 @@ function registerRoutes(ctx, relayDir) {
|
|
|
811
1063
|
*/
|
|
812
1064
|
export function apply(ctx, config = {}) {
|
|
813
1065
|
const relayDir = config.relayDir || process.env.DSH_RELAY_DIR || DEFAULT_RELAY_DIR;
|
|
1066
|
+
// 清理上次进程残留的安装/更新 marker(宿主被重启/强杀时子进程清理回调会丢失)
|
|
1067
|
+
sweepStaleMarkers(relayDir);
|
|
814
1068
|
ctx.effect(() => registerRoutes(ctx, relayDir), "dsh-remote-ui: /dsh-remote routes");
|
|
815
1069
|
// 0.1.2-rc.1+ 浏览器会话代持:换取 Harness 会话 Cookie 供 bridge 上游携带(手机点设备不再 401 白页)
|
|
816
1070
|
ctx.effect(() => scheduleHarnessMint(ctx, relayDir), "dsh-remote-ui: harness browser-session mint");
|
|
1071
|
+
// 插件市场一键全功能:缺桌面运行环境则自动安装,登录后自动拉起 bridge(不依赖用户跑 npx)
|
|
1072
|
+
ctx.effect(() => scheduleRuntime(relayDir), "dsh-remote-ui: runtime self-provision");
|
|
817
1073
|
ctx.logger?.info?.(`dsh-remote-ui: /dsh-remote routes ready (relayDir=${relayDir})`);
|
|
818
1074
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// 插件 node 半「自管理」回归:/dsh-remote/self | update-check | update | update-log | uninstall。
|
|
2
|
+
// 覆盖用户三项诉求对应的机制:
|
|
3
|
+
// 1) 插件市场没有「更新按钮」→ 面板内 self* 路由提供版本检测与一键在线更新;
|
|
4
|
+
// 2) 市场无法卸载(我们的 include 引用挡住)→ uninstall 路由移除 include/依赖/bundle/本地文件;
|
|
5
|
+
// 3) 版本可见 + 新版本检测 + 稳健更新(含残留 marker 清理兜底逻辑在 apply 时执行)。
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import http from "node:http";
|
|
8
|
+
import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import { apply } from "../lib/index.js";
|
|
14
|
+
|
|
15
|
+
const PLUGIN_BLOCK = `# >>> dsh-remote-ui (managed by dsh-remote plugin; do not edit)
|
|
16
|
+
- type: plugin
|
|
17
|
+
name: dsh-remote-ui
|
|
18
|
+
apply: dsh-remote-ui
|
|
19
|
+
# <<< dsh-remote-ui
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
/** 加载插件(tempHome 可覆盖 os.homedir(),供 uninstall 的默认 profile 推导用)。 */
|
|
23
|
+
function boot(tempHome, relayDir) {
|
|
24
|
+
const routes = new Map();
|
|
25
|
+
const previousHome = process.env.HOME;
|
|
26
|
+
if (tempHome) process.env.HOME = tempHome;
|
|
27
|
+
try {
|
|
28
|
+
apply({
|
|
29
|
+
webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
|
|
30
|
+
effect(register) { return register(); },
|
|
31
|
+
logger: { info() {}, warn() {} }
|
|
32
|
+
}, { relayDir });
|
|
33
|
+
} finally {
|
|
34
|
+
if (tempHome) process.env.HOME = previousHome;
|
|
35
|
+
}
|
|
36
|
+
return routes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 起一个指向 routes 的 http server,返回 base url。 */
|
|
40
|
+
async function serve(routes) {
|
|
41
|
+
const host = http.createServer((req, res) => {
|
|
42
|
+
const url = new URL(req.url, "http://x");
|
|
43
|
+
const handler = routes.get(url.pathname);
|
|
44
|
+
(handler || ((_r, rs) => { rs.writeHead(404); rs.end(); }))(req, res);
|
|
45
|
+
});
|
|
46
|
+
await new Promise((resolve) => host.listen(0, "127.0.0.1", resolve));
|
|
47
|
+
return { host, base: `http://127.0.0.1:${host.address().port}` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("self 路由:版本可见 + 运行环境状态(无 npx 环境时不谎报已就绪)", async () => {
|
|
51
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-self-"));
|
|
52
|
+
try {
|
|
53
|
+
const routes = boot(null, tempDir);
|
|
54
|
+
const { host, base } = await serve(routes);
|
|
55
|
+
try {
|
|
56
|
+
const self = await (await fetch(`${base}/dsh-remote/self`)).json();
|
|
57
|
+
assert.equal(self.ok, true);
|
|
58
|
+
assert.match(self.version, /^\d+\.\d+\.\d+$/, `version 应为 semver,实际 ${self.version}`);
|
|
59
|
+
assert.equal(self.runtimeReady, false, "temp 目录没有 dsh-setup.mjs → runtimeReady=false");
|
|
60
|
+
assert.equal(self.relayDir, tempDir);
|
|
61
|
+
} finally {
|
|
62
|
+
host.close();
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("update-check 路由:从 npm 检测新版本(dist-tags.latest 0.4.4 > 当前)", async () => {
|
|
70
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-upchk-"));
|
|
71
|
+
const origFetch = globalThis.fetch;
|
|
72
|
+
try {
|
|
73
|
+
const routes = boot(null, tempDir);
|
|
74
|
+
const { host, base } = await serve(routes);
|
|
75
|
+
// 假 npm 源:registry 请求回 0.4.4,其余请求(本测试自身的 HTTP 调用)走真实 fetch
|
|
76
|
+
globalThis.fetch = async (url, init) => {
|
|
77
|
+
if (String(url).startsWith("https://registry.")) {
|
|
78
|
+
return { ok: true, status: 200, json: async () => ({ "dist-tags": { latest: "0.4.4" } }) };
|
|
79
|
+
}
|
|
80
|
+
return origFetch(url, init);
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
const r = await (await fetch(`${base}/dsh-remote/self/update-check`)).json();
|
|
84
|
+
assert.equal(r.ok, true);
|
|
85
|
+
assert.equal(r.latest, "0.4.4");
|
|
86
|
+
assert.equal(r.outdated, true, "0.4.4 > 当前版本 → outdated 应为 true");
|
|
87
|
+
assert.notEqual(r.current, r.latest);
|
|
88
|
+
} finally {
|
|
89
|
+
host.close();
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
globalThis.fetch = origFetch;
|
|
93
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("update-log 路由:running 由 marker 决定,日志返回尾部", async () => {
|
|
98
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-uplog-"));
|
|
99
|
+
try {
|
|
100
|
+
const routes = boot(null, tempDir);
|
|
101
|
+
const { host, base } = await serve(routes);
|
|
102
|
+
try {
|
|
103
|
+
// 无 marker:不 running
|
|
104
|
+
let r = await (await fetch(`${base}/dsh-remote/self/update-log`)).json();
|
|
105
|
+
assert.equal(r.ok, true);
|
|
106
|
+
assert.equal(r.running, false);
|
|
107
|
+
|
|
108
|
+
// 写入 marker + 日志 → running=true 且返回日志尾部
|
|
109
|
+
const lines = Array.from({ length: 30 }, (_, i) => `line ${i}`);
|
|
110
|
+
await writeFile(path.join(tempDir, ".dsh-update-running"), String(Date.now()));
|
|
111
|
+
await writeFile(path.join(tempDir, ".dsh-update.log"), lines.join("\n"));
|
|
112
|
+
r = await (await fetch(`${base}/dsh-remote/self/update-log`)).json();
|
|
113
|
+
assert.equal(r.running, true);
|
|
114
|
+
assert.ok(r.log.includes("line 29"), "日志应含尾部内容");
|
|
115
|
+
assert.ok(!r.log.includes("line 0"), "日志过长时应只截尾部(tailOf 生效)");
|
|
116
|
+
} finally {
|
|
117
|
+
host.close();
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("update 路由:已有更新进行中时拒绝重复触发(防重入)", async () => {
|
|
125
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-up-"));
|
|
126
|
+
try {
|
|
127
|
+
const routes = boot(null, tempDir);
|
|
128
|
+
const { host, base } = await serve(routes);
|
|
129
|
+
try {
|
|
130
|
+
await writeFile(path.join(tempDir, ".dsh-update-running"), String(Date.now()));
|
|
131
|
+
const r = await (await fetch(`${base}/dsh-remote/self/update`, { method: "POST" })).json();
|
|
132
|
+
assert.equal(r.ok, false);
|
|
133
|
+
assert.match(String(r.detail), /进行中/, `detail 应提示进行中,实际: ${r.detail}`);
|
|
134
|
+
} finally {
|
|
135
|
+
host.close();
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("uninstall 路由:移除 include 块 + package.json 依赖/bundle + 本地目录(解锁市场卸载)", async () => {
|
|
143
|
+
const tempHome = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-home-"));
|
|
144
|
+
const relayDir = path.join(tempHome, "relay");
|
|
145
|
+
await mkdir(relayDir, { recursive: true });
|
|
146
|
+
// 模拟真实 profile 布局:cordis.patch.yml + package.json + 本地插件目录/链接
|
|
147
|
+
const profile = path.join(tempHome, ".dsh", "profiles", "web");
|
|
148
|
+
await mkdir(path.join(profile, "dsh-remote-ui-plugin"), { recursive: true });
|
|
149
|
+
await mkdir(path.join(profile, "node_modules", "dsh-remote-ui"), { recursive: true });
|
|
150
|
+
const patchFile = path.join(profile, "cordis.patch.yml");
|
|
151
|
+
const pkgFile = path.join(profile, "package.json");
|
|
152
|
+
await writeFile(patchFile, `base: .\ninclude:\n${PLUGIN_BLOCK} - other-plugin\n`);
|
|
153
|
+
await writeFile(pkgFile, JSON.stringify({
|
|
154
|
+
dependencies: { "dsh-remote-ui": "github:mrRisega/dsh-remote#path:/packages/dsh-remote-ui", "other": "^1.0.0" },
|
|
155
|
+
dsh: { profile: { bundles: ["dsh-remote-ui", "other-bundle"] } },
|
|
156
|
+
}));
|
|
157
|
+
await writeFile(path.join(profile, "dsh-remote-ui-plugin", "index.js"), "// stub");
|
|
158
|
+
await writeFile(path.join(profile, "node_modules", "dsh-remote-ui", "index.js"), "// stub");
|
|
159
|
+
await writeFile(path.join(relayDir, ".dsh-config.json"), "{}");
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// 关键:以 tempHome 为 HOME 启动,profileDir 默认推导才会落在 temp profile 而非真实 ~/.dsh
|
|
163
|
+
const routes = boot(tempHome, relayDir);
|
|
164
|
+
const { host, base } = await serve(routes);
|
|
165
|
+
try {
|
|
166
|
+
const r = await (await fetch(`${base}/dsh-remote/self/uninstall`, { method: "POST" })).json();
|
|
167
|
+
assert.equal(r.ok, true);
|
|
168
|
+
assert.equal(r.removedPatch, true, "应移除 patch 中的 include 块");
|
|
169
|
+
assert.equal(r.removedDep, true, "应移除 package.json 依赖");
|
|
170
|
+
assert.equal(r.removedBundle, true, "应移除 dsh.profile.bundles 条目");
|
|
171
|
+
assert.equal(r.removedDir, true, "应删除本地插件目录与 node_modules 链接");
|
|
172
|
+
|
|
173
|
+
const patch = await readFile(patchFile, "utf8");
|
|
174
|
+
assert.ok(!patch.includes("dsh-remote-ui"), "patch 不应再引用 dsh-remote-ui");
|
|
175
|
+
assert.ok(patch.includes("other-plugin"), "其他插件 include 不应被误伤");
|
|
176
|
+
|
|
177
|
+
const pkg = JSON.parse(await readFile(pkgFile, "utf8"));
|
|
178
|
+
assert.equal(pkg.dependencies["dsh-remote-ui"], undefined, "依赖应被删除");
|
|
179
|
+
assert.equal(pkg.dependencies["other"], "^1.0.0", "其他依赖应保留");
|
|
180
|
+
assert.deepEqual(pkg.dsh.profile.bundles, ["other-bundle"], "其他 bundle 应保留");
|
|
181
|
+
} finally {
|
|
182
|
+
host.close();
|
|
183
|
+
}
|
|
184
|
+
} finally {
|
|
185
|
+
await rm(tempHome, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("源码约束:浏览器半提供版本与更新卡片(含彻底卸载确认)", () => {
|
|
190
|
+
const source = readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
|
|
191
|
+
assert.match(source, /🔄 版本与更新/);
|
|
192
|
+
assert.match(source, /dsh-remote\/self\/update-check/);
|
|
193
|
+
assert.match(source, /一键更新/);
|
|
194
|
+
assert.match(source, /彻底卸载/);
|
|
195
|
+
assert.match(source, /dsh-remote\/self\/uninstall/);
|
|
196
|
+
assert.match(source, /再点一次确认彻底卸载/);
|
|
197
|
+
});
|