@mrrisega/dsh-remote 0.6.0-beta.7 → 0.6.0-beta.9
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 +30 -0
- package/clients/dsh-remote/dsh-bridge.mjs +291 -4
- package/clients/dsh-remote/e2ee-client.mjs +711 -0
- package/clients/dsh-remote/e2ee-shim-script.js +697 -0
- package/clients/dsh-remote/e2ee-shim.mjs +158 -0
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +630 -0
- package/clients/dsh-remote/test/e2ee-client.test.mjs +365 -0
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +507 -0
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +200 -0
- package/clients/dsh-remote/test/lib-e2ee-shim.mjs +59 -0
- package/clients/dsh-remote/test/lib-native-e2ee.mjs +92 -0
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +214 -37
- package/packages/dsh-remote-web/lib/index.js +92 -2
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +79 -9
- package/packages/dsh-remote-web/test/e2ee-state.test.mjs +141 -0
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +174 -2
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +72 -3
|
@@ -146,6 +146,9 @@ window.__ModuleLoader__.load({
|
|
|
146
146
|
".dru-dev-sub{font-size:11px;color:#8c959f;margin-top:3px}",
|
|
147
147
|
".dru-dev-tag{font-size:10.5px;color:#8c959f;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
|
|
148
148
|
".dru-dev-tag-off{color:#cf222e;border-color:#ffb3b6;background:#fff0f1}",
|
|
149
|
+
// Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
|
|
150
|
+
".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:#57606a}",
|
|
151
|
+
".dru-e2ee-line.ok{color:#1a7f37}",
|
|
149
152
|
// 左下角「远程访问」快捷小手机图标(安装后常驻;首次点击前带红点)
|
|
150
153
|
".dru-fab{position:fixed;left:18px;bottom:18px;z-index:2147482100;width:46px;height:46px;border-radius:50%;background:#0969da;color:#fff;border:none;display:flex;align-items:center;justify-content:center;font-size:22px;line-height:1;box-shadow:0 6px 18px rgba(0,0,0,.28);cursor:pointer;font-family:inherit;padding:0}",
|
|
151
154
|
".dru-fab:hover{background:#0860bd}",
|
|
@@ -156,55 +159,131 @@ window.__ModuleLoader__.load({
|
|
|
156
159
|
// ── 侧边栏「远程访问」快捷入口(挂到官方设置按钮上方一行,用 dsh 自身的挂载位) ──
|
|
157
160
|
// 不用 fixed 悬浮层(会遮挡官方按钮);改为在左侧主菜单的「设置」按钮上方克隆一行同款导航项:
|
|
158
161
|
// 点击 = 打开 设置页 → 「远程访问」栏目;首次点击前该入口右上角带小红点(localStorage 一次)。
|
|
162
|
+
// 导航命中策略(兼容宿主差异):候选 = [class*=navCell] / [role=tab] / [role=menuitem] /
|
|
163
|
+
// [aria-label](纯图标项常只有 aria-label 可读);文本优先取 aria-label,再取 textContent,
|
|
164
|
+
// 因此 emoji/图标前缀(如 🖥/📱)不影响子串命中。命中「远程访问」栏目项后打 data-dru-remote
|
|
165
|
+
// 标记(同一设置页生命周期内二次命中直接走标记,无需重扫)。始终排除本插件注入的侧栏项
|
|
166
|
+
// (id=dru-nav-remote),避免点到自己造成递归。
|
|
159
167
|
var NAV_SEEN_KEY = "dsh-remote-nav-seen";
|
|
160
|
-
|
|
168
|
+
var NAV_ENTRY_ID = "dru-nav-remote";
|
|
169
|
+
var NAV_MARK = "data-dru-remote";
|
|
170
|
+
var NAV_CAND_SEL = '[class*="navCell"], [role="tab"], [role="menuitem"], [aria-label]';
|
|
171
|
+
var NAV_ANY_SEL = 'button, a, [role="button"], [class*="nav"] button, [class*="sidebar"] a';
|
|
172
|
+
|
|
173
|
+
/** 元素导航文本:优先 aria-label(纯图标导航常见),再拼 textContent;去空白差异。 */
|
|
174
|
+
function navTextOf(el) {
|
|
175
|
+
var t = "";
|
|
176
|
+
try {
|
|
177
|
+
var al = el.getAttribute && el.getAttribute("aria-label");
|
|
178
|
+
if (al) t += " " + al;
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
try { if (el.textContent) t += " " + el.textContent; } catch (e2) {}
|
|
181
|
+
return t.replace(/\s+/g, " ").trim();
|
|
182
|
+
}
|
|
183
|
+
/** 候选是否“可点导航项”:按钮/链接/tab/menuitem/navCell(排除无交互的装饰容器)。 */
|
|
184
|
+
function isNavClickable(el) {
|
|
185
|
+
try {
|
|
186
|
+
var tag = String(el.tagName || "").toLowerCase();
|
|
187
|
+
if (tag === "button" || tag === "a") return true;
|
|
188
|
+
var role = el.getAttribute && el.getAttribute("role") || "";
|
|
189
|
+
if (role === "tab" || role === "menuitem" || role === "button" || role === "link") return true;
|
|
190
|
+
return String(el.className || "").indexOf("navCell") !== -1;
|
|
191
|
+
} catch (e) { return false; }
|
|
192
|
+
}
|
|
193
|
+
/** 在候选列表里找文本含 token 的导航项并返回(命中「远程访问」时打标记)。 */
|
|
194
|
+
function pickNavHit(nodes, token, excludeId, preferMarked) {
|
|
161
195
|
try {
|
|
162
|
-
var nodes = document.querySelectorAll('button, [role="tab"], [role="menuitem"], [class*="navCell"], [class*="nav"], [class*="sidebar"] a, a');
|
|
163
196
|
for (var i = 0; i < nodes.length; i++) {
|
|
164
197
|
var el = nodes[i];
|
|
165
|
-
if (excludeId && el.id === excludeId) continue;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
198
|
+
if (excludeId && el.id === excludeId) continue; // 排除注入项自身,防递归
|
|
199
|
+
if (!isNavClickable(el)) continue;
|
|
200
|
+
var marked = !!el.getAttribute && el.getAttribute(NAV_MARK) === "1";
|
|
201
|
+
if (preferMarked && !marked) continue;
|
|
202
|
+
if (!preferMarked && marked) continue;
|
|
203
|
+
if (navTextOf(el).indexOf(token) === -1) continue;
|
|
204
|
+
if (token === "远程访问") { try { el.setAttribute(NAV_MARK, "1"); } catch (e) {} }
|
|
205
|
+
return el;
|
|
206
|
+
}
|
|
207
|
+
} catch (e) { /* 忽略 */ }
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 点击“文本含 token”的导航项:① 已打 data-dru-remote 标记的(远程栏目)优先;
|
|
212
|
+
* ② [class*=navCell]/[role=tab]/[role=menuitem]/[aria-label] 语义候选;
|
|
213
|
+
* ③ 任意按钮/链接兜底。全部失败返回 false(调用方决定是否退化进「设置」)。
|
|
214
|
+
*/
|
|
215
|
+
function clickNavToken(token, excludeId) {
|
|
216
|
+
try {
|
|
217
|
+
if (token === "远程访问") {
|
|
218
|
+
var all = document.querySelectorAll(NAV_CAND_SEL + ", " + NAV_ANY_SEL);
|
|
219
|
+
var hit = pickNavHit(all, token, excludeId, true);
|
|
220
|
+
if (hit) { try { hit.click(); } catch (e) {} return true; }
|
|
171
221
|
}
|
|
222
|
+
var sem = document.querySelectorAll(NAV_CAND_SEL);
|
|
223
|
+
var hit2 = pickNavHit(sem, token, excludeId, false);
|
|
224
|
+
if (hit2) { try { hit2.click(); } catch (e) {} return true; }
|
|
225
|
+
var any = document.querySelectorAll(NAV_ANY_SEL);
|
|
226
|
+
var hit3 = pickNavHit(any, token, excludeId, false);
|
|
227
|
+
if (hit3) { try { hit3.click(); } catch (e) {} return true; }
|
|
172
228
|
} catch (e) { /* 忽略 */ }
|
|
173
229
|
return false;
|
|
174
230
|
}
|
|
231
|
+
/** 「远程访问」栏目内容区是否已在 DOM(设置页已选中该栏目)。 */
|
|
232
|
+
function remoteSectionVisible() {
|
|
233
|
+
try {
|
|
234
|
+
var el = document.querySelector(".dru-settings-section");
|
|
235
|
+
return !!el && (!document.body || document.body.contains(el));
|
|
236
|
+
} catch (e) { return false; }
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 打开设置页「远程访问」栏目(侧栏注入项/面板内“去设置”等共用入口):
|
|
240
|
+
* 已在栏目内容区 → 不动;否则先找栏目项直接点;点不到 → 点「设置」进设置页,
|
|
241
|
+
* 再轮询(栏目项异步挂载)补点「远程访问」。全程排除注入项自身防递归。
|
|
242
|
+
*/
|
|
175
243
|
function openRemoteSettings() {
|
|
176
|
-
if (
|
|
177
|
-
if (
|
|
244
|
+
if (remoteSectionVisible()) return;
|
|
245
|
+
if (clickNavToken("远程访问", NAV_ENTRY_ID)) return;
|
|
246
|
+
clickNavToken("设置", NAV_ENTRY_ID); // 进设置页(导航文字可能不同名/纯图标,交给命中器)
|
|
178
247
|
var tries = 0;
|
|
179
248
|
var iv = setInterval(function () {
|
|
180
|
-
if (
|
|
249
|
+
if (remoteSectionVisible() || clickNavToken("远程访问", NAV_ENTRY_ID)) { clearInterval(iv); return; }
|
|
181
250
|
if (++tries > 40) clearInterval(iv);
|
|
182
251
|
}, 150);
|
|
183
252
|
if (typeof iv.unref === "function") iv.unref();
|
|
184
253
|
}
|
|
254
|
+
// 暴露给宿主/自动化(同一入口,避免重复实现;测试沙箱经此驱动点击流)
|
|
255
|
+
try { window.__dshRemoteNav = { open: openRemoteSettings }; } catch (e) {}
|
|
256
|
+
|
|
185
257
|
function injectSidebarRemoteEntry() {
|
|
186
258
|
try {
|
|
187
|
-
if (document.getElementById(
|
|
259
|
+
if (document.getElementById(NAV_ENTRY_ID)) return;
|
|
188
260
|
if (!document.body) { setTimeout(injectSidebarRemoteEntry, 300); return; }
|
|
189
261
|
var tries = 0;
|
|
190
262
|
var iv = setInterval(function () {
|
|
191
263
|
try {
|
|
192
|
-
if (document.getElementById(
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
var nodes = document.querySelectorAll(
|
|
264
|
+
if (document.getElementById(NAV_ENTRY_ID)) { clearInterval(iv); return; }
|
|
265
|
+
// 在左侧主导航找「设置」按钮:语义候选优先,文本开头为“设置”或 aria-label 恰为“设置”
|
|
266
|
+
// (兼容文字不同名/带 emoji 图标的情况:能读到的标识仍是“设置”)。
|
|
267
|
+
var nodes = document.querySelectorAll(NAV_CAND_SEL + ", " + NAV_ANY_SEL);
|
|
196
268
|
var settingsBtn = null;
|
|
197
269
|
for (var i = 0; i < nodes.length; i++) {
|
|
198
270
|
var el = nodes[i];
|
|
271
|
+
if (el.id === NAV_ENTRY_ID) continue;
|
|
272
|
+
if (!isNavClickable(el)) continue;
|
|
273
|
+
var label = (el.getAttribute && el.getAttribute("aria-label")) || "";
|
|
199
274
|
var t = (el.textContent || "").trim();
|
|
200
|
-
|
|
275
|
+
var hit = label === "设置" || label.indexOf("设置") === 0 ||
|
|
276
|
+
t === "设置" || t.indexOf("设置") === 0 || t.indexOf("设置 ") === 0;
|
|
277
|
+
if (!hit) continue;
|
|
278
|
+
settingsBtn = el;
|
|
279
|
+
break;
|
|
201
280
|
}
|
|
202
281
|
if (!settingsBtn || !settingsBtn.parentNode) {
|
|
203
282
|
if (++tries > 80) clearInterval(iv);
|
|
204
283
|
return;
|
|
205
284
|
}
|
|
206
285
|
var entry = settingsBtn.cloneNode(false);
|
|
207
|
-
entry.id =
|
|
286
|
+
entry.id = NAV_ENTRY_ID;
|
|
208
287
|
entry.removeAttribute("data-view");
|
|
209
288
|
entry.removeAttribute("href");
|
|
210
289
|
entry.textContent = "";
|
|
@@ -980,10 +1059,30 @@ window.__ModuleLoader__.load({
|
|
|
980
1059
|
selfMsg ? h("div", { className: "dru-msg dru-msg-" + selfMsg.kind }, selfMsg.text) : null,
|
|
981
1060
|
h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
982
1061
|
armed ? "⚠ 再次点击后即开始彻底卸载:① 移除 dsh web 配置中的插件引用与本地文件;② 停止并移除 bridge 自启动服务(macOS com.dshremote.bridge / Linux dsh-bridge)并结束残留进程;③ 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销,如需再次使用请在插件市场重新安装。" :
|
|
983
|
-
"
|
|
1062
|
+
"检测新版、一键在线更新、彻底卸载都在本卡片完成。")
|
|
984
1063
|
);
|
|
985
1064
|
}
|
|
986
1065
|
|
|
1066
|
+
// ── 端到端加密(E2EE,Phase-5):bridge 状态 → 可读文案映射 ─────────────
|
|
1067
|
+
// bridge 启停时把开关结果写入 <relayDir>/.e2ee-state.json({enabled,reason,profile,epoch,caps},
|
|
1068
|
+
// 见 clients/dsh-remote/e2ee-client.mjs),node 半随 /dsh-remote/status 以 service.e2ee 下发;
|
|
1069
|
+
// 此处只做“可读文案”映射(协议 docs/e2ee-protocol.md §2.3/§7.3)。
|
|
1070
|
+
var E2EE_DISABLED_COPY = {
|
|
1071
|
+
server_disabled: "端到端加密暂不可用(当前为普通安全连接 HTTPS)",
|
|
1072
|
+
params_unreachable: "当前为普通安全连接(HTTPS)",
|
|
1073
|
+
disabled_by_config: "当前为普通安全连接(HTTPS)",
|
|
1074
|
+
derive_failed: "账号密码已变更,需在「🔑 账号」重新登录后恢复端到端加密(当前为普通安全连接(HTTPS))",
|
|
1075
|
+
};
|
|
1076
|
+
var E2EE_DISABLED_FALLBACK = "当前为普通安全连接(HTTPS)";
|
|
1077
|
+
/** 把 service.e2ee 归一化为可读状态行;e2ee 缺失/旧 host 未下发 → null(不打扰)。 */
|
|
1078
|
+
function describeE2ee(e2ee) {
|
|
1079
|
+
if (!e2ee || typeof e2ee !== "object") return null;
|
|
1080
|
+
if (e2ee.enabled === true) return { kind: "ok", text: "🔒 端到端加密已启用(手机解锁后生效)" };
|
|
1081
|
+
var reason = typeof e2ee.reason === "string" && e2ee.reason ? e2ee.reason : "";
|
|
1082
|
+
var text = E2EE_DISABLED_COPY[reason] || E2EE_DISABLED_FALLBACK; // reason 未知 → 兜底
|
|
1083
|
+
return { kind: "off", text: text };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
987
1086
|
// ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
|
|
988
1087
|
function RemoteControlSection(props) {
|
|
989
1088
|
|
|
@@ -1031,7 +1130,9 @@ window.__ModuleLoader__.load({
|
|
|
1031
1130
|
var devOpenArr = useState(false); var devOpen = devOpenArr[0]; var setDevOpen = devOpenArr[1];
|
|
1032
1131
|
var devBusyArr = useState(""); var devBusy = devBusyArr[0]; var setDevBusy = devBusyArr[1];
|
|
1033
1132
|
var devMsgArr = useState(null); var devMsg = devMsgArr[0]; var setDevMsg = devMsgArr[1];
|
|
1034
|
-
var armedDevArr = useState(null); var armedDev = armedDevArr[0]; var setArmedDev = armedDevArr[1]; // 待二次确认的 session id
|
|
1133
|
+
var armedDevArr = useState(null); var armedDev = armedDevArr[0]; var setArmedDev = armedDevArr[1]; // 待二次确认的 session id(取消配对)
|
|
1134
|
+
var armedDelArr = useState(null); var armedDel = armedDelArr[0]; var setArmedDel = armedDelArr[1]; // 待二次确认的 session id(删除记录)
|
|
1135
|
+
var purgeArmedArr = useState(false); var purgeArmed = purgeArmedArr[0]; var setPurgeArmed = purgeArmedArr[1]; // 清理已解绑二次确认
|
|
1035
1136
|
|
|
1036
1137
|
var refresh = useCallback(function () {
|
|
1037
1138
|
setBusy("status");
|
|
@@ -1175,33 +1276,79 @@ window.__ModuleLoader__.load({
|
|
|
1175
1276
|
setDevMsg({ kind: "err", text: "加载已授权设备失败:" + e.message });
|
|
1176
1277
|
}).finally(function () { setDevBusy(""); });
|
|
1177
1278
|
}
|
|
1279
|
+
/** 操作(取消配对/删除记录/清理已解绑)成功后静默重拉列表,覆盖行内状态。 */
|
|
1280
|
+
function refreshDeviceList() {
|
|
1281
|
+
api("/dsh-remote/mobile-sessions").then(function (b) {
|
|
1282
|
+
if (b && b.ok) setDevSessions(Array.isArray(b.sessions) ? b.sessions : []);
|
|
1283
|
+
}).catch(function () {});
|
|
1284
|
+
}
|
|
1178
1285
|
|
|
1179
1286
|
var toggleDevices = function () {
|
|
1180
1287
|
var next = !devOpen;
|
|
1181
1288
|
setDevOpen(next);
|
|
1182
1289
|
if (next && devSessions === null && devBusy === "") loadDevices();
|
|
1183
|
-
if (!next) setArmedDev(null);
|
|
1290
|
+
if (!next) { setArmedDev(null); setArmedDel(null); setPurgeArmed(false); }
|
|
1184
1291
|
};
|
|
1185
1292
|
|
|
1186
1293
|
/** 取消配对:先点一次进入确认态,再点一次才 POST revoke(同 SelfManageCard 二次确认风格)。 */
|
|
1187
1294
|
var doRevokeDevice = function (id) {
|
|
1188
1295
|
if (!id) return;
|
|
1189
|
-
if (armedDev !== id) { setArmedDev(id); return; }
|
|
1296
|
+
if (armedDev !== id) { setArmedDev(id); setArmedDel(null); setPurgeArmed(false); return; }
|
|
1190
1297
|
setDevBusy("revoke:" + id);
|
|
1191
1298
|
post("/dsh-remote/mobile-sessions/revoke", { id: id }).then(function (b) {
|
|
1192
1299
|
if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "取消失败");
|
|
1193
1300
|
setArmedDev(null);
|
|
1194
1301
|
setDevMsg({ kind: "ok", text: "已取消,对方需重新扫码/登录" });
|
|
1195
|
-
|
|
1196
|
-
api("/dsh-remote/mobile-sessions").then(function (lb) {
|
|
1197
|
-
if (lb && lb.ok) setDevSessions(Array.isArray(lb.sessions) ? lb.sessions : []);
|
|
1198
|
-
}).catch(function () {});
|
|
1302
|
+
refreshDeviceList();
|
|
1199
1303
|
}).catch(function (e) {
|
|
1200
1304
|
setArmedDev(null);
|
|
1201
1305
|
setDevMsg({ kind: "err", text: "取消配对失败:" + e.message });
|
|
1202
1306
|
}).finally(function () { setDevBusy(""); });
|
|
1203
1307
|
};
|
|
1204
1308
|
|
|
1309
|
+
/**
|
|
1310
|
+
* 删除设备记录:任意行(含已取消/历史)都可用,整行删除并拉黑 jti——
|
|
1311
|
+
* DELETE /dsh-remote/mobile-sessions/delete(body {id} → 企业端 DELETE /api/mobile-sessions/:id)。
|
|
1312
|
+
* 先点一次进入确认态,再点一次才发请求。
|
|
1313
|
+
*/
|
|
1314
|
+
var doDeleteDevice = function (id) {
|
|
1315
|
+
if (!id) return;
|
|
1316
|
+
if (armedDel !== id) { setArmedDel(id); setArmedDev(null); setPurgeArmed(false); return; }
|
|
1317
|
+
setDevBusy("delete:" + id);
|
|
1318
|
+
api("/dsh-remote/mobile-sessions/delete", {
|
|
1319
|
+
method: "DELETE",
|
|
1320
|
+
headers: { "content-type": "application/json" },
|
|
1321
|
+
body: JSON.stringify({ id: id })
|
|
1322
|
+
}).then(function (b) {
|
|
1323
|
+
if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "删除失败");
|
|
1324
|
+
setArmedDel(null);
|
|
1325
|
+
setDevMsg({ kind: "ok", text: "已删除该设备的记录" });
|
|
1326
|
+
refreshDeviceList();
|
|
1327
|
+
}).catch(function (e) {
|
|
1328
|
+
setArmedDel(null);
|
|
1329
|
+
setDevMsg({ kind: "err", text: "删除记录失败:" + e.message });
|
|
1330
|
+
}).finally(function () { setDevBusy(""); });
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* 清理已解绑:删除本人全部 revoked 行——POST /dsh-remote/mobile-sessions/purge(企业端 purge)。
|
|
1335
|
+
* 先点一次进入确认态,再点一次才发请求;成功后刷新列表并提示清理条数。
|
|
1336
|
+
*/
|
|
1337
|
+
var doPurgeDevices = function () {
|
|
1338
|
+
if (!purgeArmed) { setPurgeArmed(true); setArmedDev(null); setArmedDel(null); return; }
|
|
1339
|
+
setDevBusy("purge");
|
|
1340
|
+
post("/dsh-remote/mobile-sessions/purge", {}).then(function (b) {
|
|
1341
|
+
if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "清理失败");
|
|
1342
|
+
setPurgeArmed(false);
|
|
1343
|
+
var n = b.removed != null ? Number(b.removed) : 0;
|
|
1344
|
+
setDevMsg({ kind: "ok", text: n > 0 ? ("已清理 " + n + " 条已解绑记录") : "已清理全部已解绑记录" });
|
|
1345
|
+
refreshDeviceList();
|
|
1346
|
+
}).catch(function (e) {
|
|
1347
|
+
setPurgeArmed(false);
|
|
1348
|
+
setDevMsg({ kind: "err", text: "清理失败:" + e.message });
|
|
1349
|
+
}).finally(function () { setDevBusy(""); });
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1205
1352
|
/** 升级/续费带登录态打开:取一次性访问 url,改写为 /app/promo?auth=… 后在手机端进入续费页。 */
|
|
1206
1353
|
var openUpgradeAuth = function () {
|
|
1207
1354
|
setBusy("upgrade");
|
|
@@ -1477,11 +1624,23 @@ window.__ModuleLoader__.load({
|
|
|
1477
1624
|
? "请先登录(下方账号卡片)后启用远程访问"
|
|
1478
1625
|
: serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
|
|
1479
1626
|
var dotCls = "dru-dot " + (loggedInSaaS && serviceRunning ? "dru-dot-on" : "dru-dot-off");
|
|
1627
|
+
// Phase-5:端到端加密(E2EE)状态行 —— 未登录/旧 host 未下发 e2ee 一律不渲染
|
|
1628
|
+
// (桌面宽屏与手机镜像共用同一面板:纯文字状态行、不弹层不打扰);启用=绿点绿字(🔒已启用),
|
|
1629
|
+
// 未启用=灰字 + 中性原因文案(回退普通 HTTPS 连接,不宣称“灰度等待”)。
|
|
1630
|
+
function renderE2eeBadge() {
|
|
1631
|
+
if (!loggedInSaaS) return null;
|
|
1632
|
+
var e = describeE2ee(st && st.service && st.service.e2ee);
|
|
1633
|
+
if (!e) return null;
|
|
1634
|
+
return h("div", { className: "dru-e2ee-line" + (e.kind === "ok" ? " ok" : "") },
|
|
1635
|
+
h("span", { className: "dru-dot " + (e.kind === "ok" ? "dru-dot-on" : "dru-dot-off") }),
|
|
1636
|
+
h("span", null, e.text));
|
|
1637
|
+
}
|
|
1480
1638
|
return card("📱 远程访问", [
|
|
1481
1639
|
h("div", { className: "dru-status-line" },
|
|
1482
1640
|
h("span", { className: dotCls }),
|
|
1483
1641
|
h("span", null, statusTxt)
|
|
1484
1642
|
),
|
|
1643
|
+
renderE2eeBadge(),
|
|
1485
1644
|
akeyMsg ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } }, akeyMsg.text) : null,
|
|
1486
1645
|
hasKey ? h("div", null, [
|
|
1487
1646
|
h("div", { className: "dru-url big", style: { marginTop: 8 } },
|
|
@@ -1498,15 +1657,15 @@ window.__ModuleLoader__.load({
|
|
|
1498
1657
|
),
|
|
1499
1658
|
h("div", { className: "dru-access-col" },
|
|
1500
1659
|
h("div", { className: "dru-key-note" },
|
|
1501
|
-
"
|
|
1660
|
+
"扫码即进入,30 分钟有效、用一次即失效。"),
|
|
1502
1661
|
h("div", { className: "dru-actions", style: { marginTop: 2 } },
|
|
1503
1662
|
h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: akeyBusy, onClick: openKeyUrl }, "直接打开"),
|
|
1504
1663
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: akeyBusy, onClick: loadAccessKey }, akeyBusy ? "生成中…" : "刷新二维码/访问链接")
|
|
1505
1664
|
),
|
|
1506
1665
|
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
1507
1666
|
!serviceRunning
|
|
1508
|
-
? "本机 bridge
|
|
1509
|
-
: "
|
|
1667
|
+
? "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。"
|
|
1668
|
+
: "打开链接/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。")
|
|
1510
1669
|
)
|
|
1511
1670
|
)
|
|
1512
1671
|
]) : h("div", null, [
|
|
@@ -1522,7 +1681,7 @@ window.__ModuleLoader__.load({
|
|
|
1522
1681
|
]);
|
|
1523
1682
|
}
|
|
1524
1683
|
|
|
1525
|
-
// ---------- 📲 已授权设备卡(展开列表 +
|
|
1684
|
+
// ---------- 📲 已授权设备卡(展开列表 + 行内 取消配对/删除记录 + 底部 清理已解绑) ----------
|
|
1526
1685
|
function deviceLabel(s) {
|
|
1527
1686
|
if (s && s.label) return String(s.label);
|
|
1528
1687
|
var parts = [];
|
|
@@ -1543,10 +1702,12 @@ window.__ModuleLoader__.load({
|
|
|
1543
1702
|
if (devSessions.length === 0) {
|
|
1544
1703
|
return h("div", { className: "dru-fb-empty" }, "暂无已授权设备(手机扫码后出现)");
|
|
1545
1704
|
}
|
|
1705
|
+
var revokedCount = devSessions.filter(function (s) { return !!(s && s.revoked_at); }).length;
|
|
1546
1706
|
return h("div", null, [
|
|
1547
1707
|
devSessions.map(function (s) {
|
|
1708
|
+
var id = s && s.id;
|
|
1548
1709
|
var revoked = !!(s && s.revoked_at);
|
|
1549
|
-
return h("div", { key:
|
|
1710
|
+
return h("div", { key: id, className: "dru-dev" },
|
|
1550
1711
|
h("div", { className: "dru-dev-top" },
|
|
1551
1712
|
h("span", { className: "dru-dev-name" }, deviceLabel(s)),
|
|
1552
1713
|
deviceMeta(s) ? h("span", { className: "dru-dev-meta" }, deviceMeta(s)) : null,
|
|
@@ -1557,18 +1718,34 @@ window.__ModuleLoader__.load({
|
|
|
1557
1718
|
(s && s.last_seen_at ? " · 最近活跃 " + fmtDT(s.last_seen_at) : "") +
|
|
1558
1719
|
(revoked ? " · 取消于 " + fmtDT(s.revoked_at) : "")
|
|
1559
1720
|
),
|
|
1560
|
-
|
|
1561
|
-
h("button", {
|
|
1721
|
+
h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
1722
|
+
revoked ? null : h("button", {
|
|
1562
1723
|
type: "button",
|
|
1563
1724
|
className: "dru-btn dru-btn-danger",
|
|
1564
1725
|
disabled: devBusy !== "",
|
|
1565
|
-
onClick: function () { doRevokeDevice(
|
|
1566
|
-
}, devBusy === "revoke:" +
|
|
1726
|
+
onClick: function () { doRevokeDevice(id); }
|
|
1727
|
+
}, devBusy === "revoke:" + id ? "取消中…" : armedDev === id ? "⚠ 再点一次确认取消配对" : "取消配对"),
|
|
1728
|
+
h("button", {
|
|
1729
|
+
type: "button",
|
|
1730
|
+
className: "dru-btn dru-btn-ghost",
|
|
1731
|
+
style: { color: "#cf222e", borderColor: "#cf222e" },
|
|
1732
|
+
disabled: devBusy !== "",
|
|
1733
|
+
onClick: function () { doDeleteDevice(id); }
|
|
1734
|
+
}, devBusy === "delete:" + id ? "删除中…" : armedDel === id ? "⚠ 再点一次确认删除记录" : "删除记录")
|
|
1567
1735
|
)
|
|
1568
1736
|
);
|
|
1569
1737
|
}),
|
|
1570
1738
|
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
1571
|
-
"取消配对后,对方需重新扫码/登录才能再次远程访问本机。")
|
|
1739
|
+
"取消配对后,对方需重新扫码/登录才能再次远程访问本机。"),
|
|
1740
|
+
h("div", { className: "dru-actions", style: { marginTop: 8, borderTop: "1px dashed #eaeef2", paddingTop: 8 } },
|
|
1741
|
+
h("button", {
|
|
1742
|
+
type: "button",
|
|
1743
|
+
className: "dru-btn dru-btn-ghost",
|
|
1744
|
+
disabled: devBusy !== "" || revokedCount === 0,
|
|
1745
|
+
onClick: doPurgeDevices,
|
|
1746
|
+
title: revokedCount > 0 ? ("清理 " + revokedCount + " 条已解绑记录") : "没有已解绑记录"
|
|
1747
|
+
}, devBusy === "purge" ? "清理中…" : purgeArmed ? "⚠ 再点一次确认清理已解绑" : "清理已解绑" + (revokedCount > 0 ? "(" + revokedCount + ")" : ""))
|
|
1748
|
+
)
|
|
1572
1749
|
]);
|
|
1573
1750
|
}
|
|
1574
1751
|
function renderDevicesCard() {
|
|
@@ -1692,7 +1869,7 @@ window.__ModuleLoader__.load({
|
|
|
1692
1869
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
|
|
1693
1870
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
|
|
1694
1871
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
|
|
1695
|
-
h("div", { className: "dru-hint" }, "🛡
|
|
1872
|
+
h("div", { className: "dru-hint" }, "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。")
|
|
1696
1873
|
]),
|
|
1697
1874
|
// 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
|
|
1698
1875
|
h(SelfManageCard, null),
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// - 读写配置目录下 .dsh-config.json(0600)
|
|
5
5
|
// - 查询/启停 bridge(launchctl,plist 缺失时自动生成,逻辑与 dsh-setup.mjs 一致)
|
|
6
6
|
// - 代理 relay API(captcha / register / login / public-config),直连、不走系统代理;
|
|
7
|
-
// 另代理企业端一次性访问密钥 / 授权设备(/api/auth-key、/api/mobile-sessions、…/revoke
|
|
7
|
+
// 另代理企业端一次性访问密钥 / 授权设备(/api/auth-key、/api/mobile-sessions、…/revoke、
|
|
8
|
+
// DELETE …/:id、POST …/purge,Bearer)供面板「📱 远程访问」卡使用
|
|
8
9
|
// - 自管理 self*(版本可见 / 新版检测 / 一键在线更新 / 彻底卸载):插件市场没有更新卸载按钮,
|
|
9
10
|
// 面板内即官方管理入口;更新=后台 npx 按 dist-tag(默认 latest,DSH_UPDATE_TAG 可切 beta/alpha)(幂等补齐运行环境并重启 bridge);
|
|
10
11
|
// 彻底卸载=profile 插件清理(uninstallSelf)+ 运行时清理(uninstallRuntime:停 bridge 自启动 /
|
|
@@ -802,8 +803,78 @@ async function proxyRevokeMobileSession(relayDir, req, res) {
|
|
|
802
803
|
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
803
804
|
}
|
|
804
805
|
|
|
806
|
+
/**
|
|
807
|
+
* DELETE /dsh-remote/mobile-sessions/delete(body {id})→ 删除本机该设备的授权记录
|
|
808
|
+
* (企业端 DELETE /api/mobile-sessions/:id,Bearer:本人整行删除并拉黑 jti)。
|
|
809
|
+
*/
|
|
810
|
+
async function proxyDeleteMobileSession(relayDir, req, res) {
|
|
811
|
+
const body = await readJsonBody(req);
|
|
812
|
+
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
813
|
+
const id = String(body.id ?? "").trim();
|
|
814
|
+
if (!id) return sendJson(res, 400, { ok: false, error: "缺少参数 id(会话 ID)" });
|
|
815
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
816
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
817
|
+
const r = await relayFetch(relayDir, `/api/mobile-sessions/${encodeURIComponent(id)}`, {
|
|
818
|
+
method: "DELETE",
|
|
819
|
+
headers: { authorization: `Bearer ${token}` },
|
|
820
|
+
});
|
|
821
|
+
const d = flattenRelayBody(r);
|
|
822
|
+
const ok = !!(r.ok && d.ok !== false);
|
|
823
|
+
if (!ok) {
|
|
824
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
825
|
+
}
|
|
826
|
+
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* POST /dsh-remote/mobile-sessions/purge → 清理本人所有已解绑(revoked)记录
|
|
831
|
+
* (企业端 POST /api/mobile-sessions/purge,Bearer)。
|
|
832
|
+
*/
|
|
833
|
+
async function proxyPurgeMobileSessions(relayDir, res) {
|
|
834
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
835
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
836
|
+
const r = await relayFetch(relayDir, "/api/mobile-sessions/purge", {
|
|
837
|
+
method: "POST",
|
|
838
|
+
headers: { authorization: `Bearer ${token}` },
|
|
839
|
+
});
|
|
840
|
+
const d = flattenRelayBody(r);
|
|
841
|
+
const ok = !!(r.ok && d.ok !== false);
|
|
842
|
+
if (!ok) {
|
|
843
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
844
|
+
}
|
|
845
|
+
const removed = Number.isInteger(d.removed) ? d.removed : null;
|
|
846
|
+
const payload = { ok: true, relayStatus: (r && r.status) || 200 };
|
|
847
|
+
if (removed !== null) payload.removed = removed;
|
|
848
|
+
return sendJson(res, 200, payload);
|
|
849
|
+
}
|
|
850
|
+
|
|
805
851
|
// ---------- 综合状态 ----------
|
|
806
852
|
|
|
853
|
+
/**
|
|
854
|
+
* 读取 bridge 写入的 E2EE 开关状态(<relayDir>/.e2ee-state.json,写入方见
|
|
855
|
+
* clients/dsh-remote/e2ee-client.mjs 的 writeE2eeStateFile:{enabled, reason, profile, epoch, caps, at})。
|
|
856
|
+
* 归一化后只透出固定展示字段(enabled/reason/profile/epoch/caps),at 等调试字段不外泄;
|
|
857
|
+
* 文件缺失/损坏一律视为「未启用(明文回退)」——面板据此显示普通安全连接。
|
|
858
|
+
*/
|
|
859
|
+
export function readE2eeStateFile(relayDir) {
|
|
860
|
+
const none = () => ({ enabled: false, reason: "no_state_file", profile: "", epoch: 0, caps: [] });
|
|
861
|
+
try {
|
|
862
|
+
const f = join(relayDir, ".e2ee-state.json");
|
|
863
|
+
if (!existsSync(f)) return none();
|
|
864
|
+
const s = JSON.parse(readFileSync(f, "utf8"));
|
|
865
|
+
if (!s || typeof s !== "object") return none();
|
|
866
|
+
return {
|
|
867
|
+
enabled: s.enabled === true,
|
|
868
|
+
reason: typeof s.reason === "string" && s.reason ? s.reason : "",
|
|
869
|
+
profile: typeof s.profile === "string" ? s.profile : "",
|
|
870
|
+
epoch: Number.isInteger(s.epoch) && s.epoch >= 0 ? s.epoch : 0,
|
|
871
|
+
caps: Array.isArray(s.caps) ? s.caps.filter((c) => typeof c === "string") : [],
|
|
872
|
+
};
|
|
873
|
+
} catch {
|
|
874
|
+
return none();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
807
878
|
async function composeStatus(relayDir) {
|
|
808
879
|
const cfg = loadConfig(relayDir);
|
|
809
880
|
const launchd = launchdStatus();
|
|
@@ -838,6 +909,9 @@ async function composeStatus(relayDir) {
|
|
|
838
909
|
manual,
|
|
839
910
|
running: launchd.running || manual.bridge.length > 0,
|
|
840
911
|
bindError,
|
|
912
|
+
// E2EE 开关状态(bridge 写 .e2ee-state.json;文件缺失 = 未启用明文):
|
|
913
|
+
// {enabled, reason, profile, epoch, caps},供面板「📱 远程访问」卡展示加密状态。
|
|
914
|
+
e2ee: readE2eeStateFile(relayDir),
|
|
841
915
|
},
|
|
842
916
|
host: hostname(),
|
|
843
917
|
};
|
|
@@ -946,7 +1020,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
946
1020
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
947
1021
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
948
1022
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
949
|
-
const PLUGIN_VERSION = "0.6.0-beta.
|
|
1023
|
+
const PLUGIN_VERSION = "0.6.0-beta.9";
|
|
950
1024
|
const UPDATE_LOG = ".dsh-update.log";
|
|
951
1025
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
952
1026
|
|
|
@@ -1222,6 +1296,22 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1222
1296
|
await proxyRevokeMobileSession(relayDir, req, res);
|
|
1223
1297
|
},
|
|
1224
1298
|
},
|
|
1299
|
+
// 删除已授权设备记录(整行删除并拉黑 jti;企业端 DELETE /api/mobile-sessions/:id,Bearer)
|
|
1300
|
+
{
|
|
1301
|
+
method: "DELETE",
|
|
1302
|
+
path: "/dsh-remote/mobile-sessions/delete",
|
|
1303
|
+
handler: async (req, res) => {
|
|
1304
|
+
await proxyDeleteMobileSession(relayDir, req, res);
|
|
1305
|
+
},
|
|
1306
|
+
},
|
|
1307
|
+
// 清理本人全部已解绑(revoked)记录(企业端 POST /api/mobile-sessions/purge,Bearer)
|
|
1308
|
+
{
|
|
1309
|
+
method: "POST",
|
|
1310
|
+
path: "/dsh-remote/mobile-sessions/purge",
|
|
1311
|
+
handler: async (_req, res) => {
|
|
1312
|
+
await proxyPurgeMobileSessions(relayDir, res);
|
|
1313
|
+
},
|
|
1314
|
+
},
|
|
1225
1315
|
{
|
|
1226
1316
|
method: "POST",
|
|
1227
1317
|
path: "/dsh-remote/config",
|