@mrrisega/dsh-remote 0.6.0-beta.10 → 0.6.0-beta.11
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.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.11",
|
|
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",
|
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
// factory 内只能 require shell 种子模块(react / react/jsx-runtime 等)。
|
|
6
6
|
//
|
|
7
7
|
// 功能:
|
|
8
|
-
// - settings.section
|
|
8
|
+
// - 入口=官方「设置」页内的 settings.section「远程访问」栏目(位于「Agent 预设」下方,
|
|
9
|
+
// 官方扩展点);不向官方侧边栏注入任何按钮,官方「设置」按钮原样、不被遮挡
|
|
9
10
|
// - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
|
|
10
11
|
// · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册 tabs
|
|
11
|
-
// · 🔒
|
|
12
|
+
// · 🔒 修改密码(账号卡):已登录账号经 图形验证码+短信验证码 重置(/dsh-remote/password/reset 代理);
|
|
12
13
|
// 成功后全部已授权设备/会话失效 → 本地登出并提示用新密码重新登录(覆盖本机 config 密码)
|
|
14
|
+
// · 忘记密码(登录卡):未登录时登录表单内「忘记密码?」→ 同一套
|
|
15
|
+
// 图形验证码+短信验证码 表单(手机号预填当前输入、可改),成功后本地登出并提示「请用新密码登录」
|
|
13
16
|
// · 登录要求图形验证码;注册要求两次密码 + 图形验证码
|
|
14
17
|
// · 📱 远程访问卡:一次性访问密钥(扫码/直接打开/复制 + 到期倒计时自动刷新)与已授权设备管理
|
|
15
18
|
// · bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
|
|
16
|
-
// -
|
|
19
|
+
// - 首次安装引导:设置页「远程访问」栏目(官方导航 navCell)旁小红点(localStorage dsh-remote-seen-dot 控制)
|
|
17
20
|
// - shell.overlay:满意度弹窗(安装体验至少 1 小时后弹出,只弹一次)
|
|
18
21
|
// 所有数据经同源 /dsh-remote/* 宿主路由读写(node 半提供)。
|
|
19
22
|
window.__ModuleLoader__.load({
|
|
@@ -63,6 +66,8 @@ window.__ModuleLoader__.load({
|
|
|
63
66
|
".dru-btn-ghost:hover:not(:disabled){background:#f6f8fa}",
|
|
64
67
|
".dru-btn-danger{background:#ffffff;color:#cf222e;border-color:#cf222e}",
|
|
65
68
|
".dru-btn-danger:hover:not(:disabled){background:#fff0f1}",
|
|
69
|
+
".dru-linkbtn{display:inline-block;padding:0;border:none;background:none;color:#0969da;font-size:12.5px;line-height:1.6;cursor:pointer;font-family:inherit;text-decoration:none}",
|
|
70
|
+
".dru-linkbtn:hover{text-decoration:underline}",
|
|
66
71
|
".dru-tabs{display:flex;gap:8px;margin-bottom:12px}",
|
|
67
72
|
".dru-tab{flex:1;padding:7px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de;user-select:none}",
|
|
68
73
|
".dru-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
|
|
@@ -151,306 +156,9 @@ window.__ModuleLoader__.load({
|
|
|
151
156
|
// Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
|
|
152
157
|
".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:#57606a}",
|
|
153
158
|
".dru-e2ee-line.ok{color:#1a7f37}",
|
|
154
|
-
// 左下角「远程访问」快捷小手机图标(安装后常驻;首次点击前带红点)
|
|
155
|
-
".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}",
|
|
156
|
-
".dru-fab:hover{background:#0860bd}",
|
|
157
|
-
".dru-fab-dot{position:absolute;top:2px;right:2px;width:9px;height:9px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px #fff;pointer-events:none}",
|
|
158
159
|
].join("\n");
|
|
159
160
|
document.head.appendChild(styleEl);
|
|
160
161
|
|
|
161
|
-
// ── 侧边栏「远程访问」快捷入口(独立按钮,挂在官方「设置」上方;Bug A 修复) ──
|
|
162
|
-
// 用户实测 Bug A:入口视觉在左侧(官方「设置」上方),点击却落在工作区会话行——
|
|
163
|
-
// 会话标题含「远程访问」(如“分析竞品远程访问功能”)被旧逻辑当成导航项点中。
|
|
164
|
-
// 根因:旧实现用 document 级 querySelectorAll + textContent「包含匹配」扫全文档,
|
|
165
|
-
// 会话标题也命中;且注入项 clone 官方按钮可能继承官方事件委托/点击热区。
|
|
166
|
-
// 修复(本段全部导航点击逻辑):
|
|
167
|
-
// 1) 候选扫描一律限定在「侧栏/主导航容器」内([role=navigation]/<nav>/class 含
|
|
168
|
-
// sidebar|navRail|navPanel|navigation|sidenav|appnav 等;兜底:含「设置」导航项或
|
|
169
|
-
// navCell 的元素向上找最近容器)。绝不 document 全量扫可点元素去点击。
|
|
170
|
-
// 2) 命中收紧:aria-label/title 等于或 startsWith(token);textContent 仅当短文本
|
|
171
|
-
// (trim ≤ NAV_TEXT_MAX=10 且不含换行——长会话标题天然被拒)且 等于/以 token 开头/
|
|
172
|
-
// 含 token 但短;同时排除 class 带 menu/context/more/kebab 等“菜单/更多”语义的节点。
|
|
173
|
-
// 3) 注入项改为自有独立 <button id=dru-nav-remote>(不再复制官方按钮元素,避免继承
|
|
174
|
-
// 官方委托与热区),自带 click → openRemoteSettings()。
|
|
175
|
-
// 4) openRemoteSettings:栏目已在 DOM 直接返回 → 各容器内点「远程访问」栏目项 →
|
|
176
|
-
// 容器内点「设置」→ 轮询 ≤3s(20×150ms)容器内补点「远程访问」;绝不点工作区内容。
|
|
177
|
-
var NAV_SEEN_KEY = "dsh-remote-nav-seen";
|
|
178
|
-
var NAV_ENTRY_ID = "dru-nav-remote";
|
|
179
|
-
var NAV_MARK = "data-dru-remote";
|
|
180
|
-
/** 导航候选(仅在导航容器内查询):navCell(shell hashed 类)/tab/menuitem/button/a/aria-label。 */
|
|
181
|
-
var NAV_CAND_SEL = '[class*="navCell"], [role="tab"], [role="menuitem"], [role="button"], button, a, [aria-label]';
|
|
182
|
-
/** 侧栏/主导航容器定位选择器(先定位容器,再在容器内做候选查询)。 */
|
|
183
|
-
var NAV_CONTAINER_SEL = '[role="navigation"], nav, [class*="sidebar"], [class*="navRail"], [class*="navPanel"], [class*="sidenav"], [class*="appnav"], [class*="navigation"]';
|
|
184
|
-
/** 候选节点 class 命中这些语义(菜单/更多/下拉…)→ 视为会话行“⋯”等,绝不点击。 */
|
|
185
|
-
var NAV_BAD_CLS_RE = /(^|[^a-z0-9])(menu|context|more|kebab|dropdown|popover|toolbar)($|[^a-z0-9])/i;
|
|
186
|
-
/** textContent 短文本上限:超过即视为长文本(会话标题等),textContent 命中一律拒绝。 */
|
|
187
|
-
var NAV_TEXT_MAX = 10;
|
|
188
|
-
/** openRemoteSettings 轮询上限:150ms × 20 = 3s(≤3s),到时放弃,不再无限轮询。 */
|
|
189
|
-
var NAV_POLL_MAX = 20;
|
|
190
|
-
/** 向上找“最近容器”的最大步数(防误上到整页)。 */
|
|
191
|
-
var NAV_UP_MAX = 8;
|
|
192
|
-
|
|
193
|
-
/** 候选是否“可点导航项”:按钮/链接/tab/menuitem/navCell(排除无交互的装饰容器)。 */
|
|
194
|
-
function isNavClickable(el) {
|
|
195
|
-
try {
|
|
196
|
-
var tag = String(el.tagName || "").toLowerCase();
|
|
197
|
-
if (tag === "button" || tag === "a") return true;
|
|
198
|
-
var role = el.getAttribute && el.getAttribute("role") || "";
|
|
199
|
-
if (role === "tab" || role === "menuitem" || role === "button" || role === "link") return true;
|
|
200
|
-
return String(el.className || "").indexOf("navCell") !== -1;
|
|
201
|
-
} catch (e) { return false; }
|
|
202
|
-
}
|
|
203
|
-
/** 节点 class 是否带“菜单/更多”语义(会话行的 ⋯/右键菜单等)→ 不可作为导航候选。 */
|
|
204
|
-
function isBadNavClass(el) {
|
|
205
|
-
try { return NAV_BAD_CLS_RE.test(String(el.className || "")); } catch (e) { return false; }
|
|
206
|
-
}
|
|
207
|
-
/** 是否语义化导航容器:role=navigation/menu 或 class 含 sidebar/navRail/navPanel 等。 */
|
|
208
|
-
function isNavContainerEl(el) {
|
|
209
|
-
try {
|
|
210
|
-
var role = el.getAttribute && el.getAttribute("role") || "";
|
|
211
|
-
if (role === "navigation" || role === "menu") return true;
|
|
212
|
-
return NAV_CONTAINER_RE.test(String(el.className || ""));
|
|
213
|
-
} catch (e) { return false; }
|
|
214
|
-
}
|
|
215
|
-
var NAV_CONTAINER_RE = /sidebar|navRail|navPanel|sidenav|appnav|navigation/i;
|
|
216
|
-
/** 候选可用性:可点 + 非“菜单/更多”语义 class。 */
|
|
217
|
-
function isUsableNavCandidate(el) {
|
|
218
|
-
try {
|
|
219
|
-
if (!isNavClickable(el)) return false;
|
|
220
|
-
if (isBadNavClass(el)) return false;
|
|
221
|
-
return true;
|
|
222
|
-
} catch (e) { return false; }
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* 收紧后的命中规则(aria/title 与可见短文本任一命中即可,语义更贴近官方按钮:
|
|
226
|
-
* 官方按钮可能带 aria-label/title,也可能只有可见文字):
|
|
227
|
-
* - aria-label/title:等于 token 或以 token 开头(长度不限,纯图标导航常见);
|
|
228
|
-
* - textContent:仅短文本(trim ≤10 且不含换行——长会话标题被拒)且
|
|
229
|
-
* 等于 / 以 token 开头 / 含 token 但文本短。
|
|
230
|
-
*/
|
|
231
|
-
function navTextHits(el, token) {
|
|
232
|
-
try {
|
|
233
|
-
var al = el.getAttribute && el.getAttribute("aria-label");
|
|
234
|
-
var attr = al && String(al).trim() ? String(al).trim() : "";
|
|
235
|
-
if (!attr) { var tt = el.getAttribute && el.getAttribute("title"); if (tt && String(tt).trim()) attr = String(tt).trim(); }
|
|
236
|
-
if (attr && (attr === token || attr.indexOf(token) === 0)) return true;
|
|
237
|
-
} catch (e) {}
|
|
238
|
-
var t = "";
|
|
239
|
-
try { t = (el.textContent || "").replace(/\s+/g, " ").trim(); } catch (e2) {}
|
|
240
|
-
if (!t || /\r|\n/.test(t) || t.length > NAV_TEXT_MAX) return false; // 长文本/多行 → 拒绝
|
|
241
|
-
return t === token || t.indexOf(token) === 0 || t.indexOf(token) !== -1;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* 在候选列表里找命中 token 的导航项(命中「远程访问」时打 data-dru-remote 标记,
|
|
245
|
-
* 二次命中直接走标记,无需重扫)。始终排除注入项自身(id=dru-nav-remote)防递归。
|
|
246
|
-
*/
|
|
247
|
-
function pickNavHit(nodes, token, excludeId, preferMarked) {
|
|
248
|
-
try {
|
|
249
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
250
|
-
var el = nodes[i];
|
|
251
|
-
if (excludeId && el.id === excludeId) continue; // 排除注入项自身,防递归
|
|
252
|
-
if (!isUsableNavCandidate(el)) continue;
|
|
253
|
-
var marked = !!el.getAttribute && el.getAttribute(NAV_MARK) === "1";
|
|
254
|
-
if (preferMarked && !marked) continue;
|
|
255
|
-
if (!preferMarked && marked) continue;
|
|
256
|
-
if (!navTextHits(el, token)) continue;
|
|
257
|
-
if (token === "远程访问") { try { el.setAttribute(NAV_MARK, "1"); } catch (e) {} }
|
|
258
|
-
return el;
|
|
259
|
-
}
|
|
260
|
-
} catch (e) { /* 忽略 */ }
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
/** 统计容器内可用导航候选数量(用于向上定位“最近容器”)。 */
|
|
264
|
-
function navLikeCount(container) {
|
|
265
|
-
try {
|
|
266
|
-
var nodes = container.querySelectorAll(NAV_CAND_SEL) || [];
|
|
267
|
-
var n = 0;
|
|
268
|
-
for (var i = 0; i < nodes.length; i++) { if (isUsableNavCandidate(nodes[i])) n++; }
|
|
269
|
-
return n;
|
|
270
|
-
} catch (e) { return 0; }
|
|
271
|
-
}
|
|
272
|
-
/** 从某导航叶子向上找“最近容器”(语义容器或 ≥2 个可用候选的层)。 */
|
|
273
|
-
function navContainerAbove(leaf) {
|
|
274
|
-
var p = leaf && leaf.parentNode ? leaf.parentNode : null;
|
|
275
|
-
var steps = 0;
|
|
276
|
-
while (p && p !== document.documentElement && steps < NAV_UP_MAX) {
|
|
277
|
-
if (isNavContainerEl(p)) return p;
|
|
278
|
-
if (navLikeCount(p) >= 2) return p;
|
|
279
|
-
p = p.parentNode;
|
|
280
|
-
steps++;
|
|
281
|
-
}
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
/** 全文档找一个命中 token 的短导航叶子(仅用于“定位容器”,不点击)。 */
|
|
285
|
-
function navLeafByToken(token) {
|
|
286
|
-
try {
|
|
287
|
-
var all = document.querySelectorAll(NAV_CAND_SEL) || [];
|
|
288
|
-
for (var i = 0; i < all.length; i++) {
|
|
289
|
-
var el = all[i];
|
|
290
|
-
if (el.id === NAV_ENTRY_ID) continue;
|
|
291
|
-
if (!isUsableNavCandidate(el)) continue;
|
|
292
|
-
if (!navTextHits(el, token)) continue;
|
|
293
|
-
return el;
|
|
294
|
-
}
|
|
295
|
-
} catch (e) {}
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
/** 全文档找一个 navCell 叶子(shell 设置页导航按钮;用于兜底定位容器)。 */
|
|
299
|
-
function navCellLeaf() {
|
|
300
|
-
try {
|
|
301
|
-
var all = document.querySelectorAll('[class*="navCell"]') || [];
|
|
302
|
-
for (var i = 0; i < all.length; i++) {
|
|
303
|
-
var el = all[i];
|
|
304
|
-
if (el.id === NAV_ENTRY_ID) continue;
|
|
305
|
-
if (!isUsableNavCandidate(el)) continue;
|
|
306
|
-
return el;
|
|
307
|
-
}
|
|
308
|
-
} catch (e) {}
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* 定位侧栏/主导航容器(此后所有候选命中都在这些容器内进行,绝不扫 main/内容区/会话列表):
|
|
313
|
-
* ① 语义容器:[role=navigation]/<nav>/class 含 sidebar|navRail|navPanel|sidenav|appnav|navigation;
|
|
314
|
-
* ② 兜底:从全文档命中「设置」的短导航叶子向上找最近容器;
|
|
315
|
-
* ③ 再兜底:从任意 navCell 叶子向上找最近容器(设置页栏目导航)。
|
|
316
|
-
* 返回去嵌套后的容器数组(保持调用次序,document 不在容器内)。
|
|
317
|
-
*/
|
|
318
|
-
function navContainers() {
|
|
319
|
-
var out = [];
|
|
320
|
-
var push = function (el) {
|
|
321
|
-
if (!el || typeof el.querySelectorAll !== "function") return;
|
|
322
|
-
if (out.indexOf(el) !== -1) return;
|
|
323
|
-
var nested = false;
|
|
324
|
-
for (var j = 0; j < out.length; j++) {
|
|
325
|
-
try { if (out[j].contains && out[j].contains(el)) { nested = true; break; } } catch (e) {}
|
|
326
|
-
}
|
|
327
|
-
if (!nested) out.push(el);
|
|
328
|
-
};
|
|
329
|
-
try {
|
|
330
|
-
var sem = document.querySelectorAll(NAV_CONTAINER_SEL) || [];
|
|
331
|
-
for (var i = 0; i < sem.length; i++) push(sem[i]);
|
|
332
|
-
} catch (e) {}
|
|
333
|
-
var seed = navLeafByToken("设置");
|
|
334
|
-
if (seed) push(navContainerAbove(seed));
|
|
335
|
-
var cell = navCellLeaf();
|
|
336
|
-
if (cell) push(navContainerAbove(cell));
|
|
337
|
-
return out;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* 在全部侧栏/导航容器内点击“命中 token”的导航项(②:导航文字可能不同名/纯图标,
|
|
341
|
-
* 交给命中器)。只查容器内候选,绝不 document 全量点击。全部失败返回 false。
|
|
342
|
-
*/
|
|
343
|
-
function clickNavToken(token, excludeId) {
|
|
344
|
-
var containers = navContainers();
|
|
345
|
-
for (var c = 0; c < containers.length; c++) {
|
|
346
|
-
var nodes = null;
|
|
347
|
-
try { nodes = containers[c].querySelectorAll(NAV_CAND_SEL); } catch (e) { nodes = null; }
|
|
348
|
-
if (!nodes) continue;
|
|
349
|
-
if (token === "远程访问") {
|
|
350
|
-
var hitMarked = pickNavHit(nodes, token, excludeId, true);
|
|
351
|
-
if (hitMarked) { try { hitMarked.click(); } catch (e2) {} return true; }
|
|
352
|
-
}
|
|
353
|
-
var hit = pickNavHit(nodes, token, excludeId, false);
|
|
354
|
-
if (hit) { try { hit.click(); } catch (e3) {} return true; }
|
|
355
|
-
}
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
|
-
/** 「远程访问」栏目内容区是否已在 DOM(设置页已选中该栏目)。 */
|
|
359
|
-
function remoteSectionVisible() {
|
|
360
|
-
try {
|
|
361
|
-
var el = document.querySelector(".dru-settings-section");
|
|
362
|
-
if (!el) return false;
|
|
363
|
-
if (document.body && !document.body.contains(el)) return false;
|
|
364
|
-
if (el.getAttribute && (el.getAttribute("aria-hidden") === "true" || el.getAttribute("hidden") !== null)) return false;
|
|
365
|
-
return true;
|
|
366
|
-
} catch (e) { return false; }
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* 打开设置页「远程访问」栏目(侧栏注入项/面板内“去设置”等共用入口):
|
|
370
|
-
* 栏目已在 DOM → 直接返回;否则容器内点「远程访问」栏目项 → 容器内点「设置」进设置页,
|
|
371
|
-
* 再轮询(≤3s:20×150ms,NAV_POLL_MAX)容器内补点「远程访问」。全程不点工作区内容。
|
|
372
|
-
*/
|
|
373
|
-
function openRemoteSettings() {
|
|
374
|
-
if (remoteSectionVisible()) return;
|
|
375
|
-
if (clickNavToken("远程访问", NAV_ENTRY_ID)) return;
|
|
376
|
-
clickNavToken("设置", NAV_ENTRY_ID); // 进设置页(导航文字可能不同名/纯图标,交给命中器)
|
|
377
|
-
var tries = 0;
|
|
378
|
-
var iv = setInterval(function () {
|
|
379
|
-
if (remoteSectionVisible() || clickNavToken("远程访问", NAV_ENTRY_ID)) { clearInterval(iv); return; }
|
|
380
|
-
if (++tries >= NAV_POLL_MAX) clearInterval(iv); // ≤3s 轮询上限
|
|
381
|
-
}, 150);
|
|
382
|
-
if (typeof iv.unref === "function") iv.unref();
|
|
383
|
-
}
|
|
384
|
-
// 暴露给宿主/自动化(同一入口,避免重复实现;测试沙箱经此驱动点击流)
|
|
385
|
-
try { window.__dshRemoteNav = { open: openRemoteSettings }; } catch (e) {}
|
|
386
|
-
|
|
387
|
-
function injectSidebarRemoteEntry() {
|
|
388
|
-
try {
|
|
389
|
-
if (document.getElementById(NAV_ENTRY_ID)) return;
|
|
390
|
-
if (!document.body) { setTimeout(injectSidebarRemoteEntry, 300); return; }
|
|
391
|
-
var tries = 0;
|
|
392
|
-
var iv = setInterval(function () {
|
|
393
|
-
try {
|
|
394
|
-
if (document.getElementById(NAV_ENTRY_ID)) { clearInterval(iv); return; }
|
|
395
|
-
// 只在导航容器内找「设置」按钮(可点 + 短文本/aria 命中),绝不 document 全量找。
|
|
396
|
-
var containers = navContainers();
|
|
397
|
-
var settingsBtn = null;
|
|
398
|
-
for (var c = 0; c < containers.length; c++) {
|
|
399
|
-
var nodes = null;
|
|
400
|
-
try { nodes = containers[c].querySelectorAll(NAV_CAND_SEL); } catch (e) { nodes = null; }
|
|
401
|
-
if (!nodes) continue;
|
|
402
|
-
var hit = pickNavHit(nodes, "设置", NAV_ENTRY_ID, false);
|
|
403
|
-
if (hit) { settingsBtn = hit; break; }
|
|
404
|
-
}
|
|
405
|
-
if (!settingsBtn || !settingsBtn.parentNode) {
|
|
406
|
-
if (++tries > 80) clearInterval(iv);
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
// 独立按钮:不复用官方按钮元素(避免继承官方事件委托/点击热区 → Bug A 误点会话行)。
|
|
410
|
-
var entry = document.createElement("button");
|
|
411
|
-
entry.id = NAV_ENTRY_ID;
|
|
412
|
-
entry.setAttribute("type", "button");
|
|
413
|
-
entry.setAttribute("role", "button");
|
|
414
|
-
entry.setAttribute("tabindex", "0");
|
|
415
|
-
entry.setAttribute("aria-label", "远程访问");
|
|
416
|
-
entry.textContent = "📱 远程访问";
|
|
417
|
-
entry.style.cssText = "display:flex;align-items:center;gap:6px;box-sizing:border-box;width:100%;" +
|
|
418
|
-
"padding:8px 10px;margin:2px 0;border:1px solid transparent;background:transparent;" +
|
|
419
|
-
"border-radius:8px;color:inherit;font-family:inherit;font-size:13px;line-height:1.4;" +
|
|
420
|
-
"cursor:pointer;text-align:left;position:relative";
|
|
421
|
-
var seen = false;
|
|
422
|
-
try { seen = !!localStorage.getItem(NAV_SEEN_KEY); } catch (e) {}
|
|
423
|
-
if (!seen) {
|
|
424
|
-
var dot = document.createElement("span");
|
|
425
|
-
dot.setAttribute("aria-hidden", "true");
|
|
426
|
-
dot.style.cssText = "position:absolute;top:4px;right:10px;width:8px;height:8px;border-radius:50%;background:#e5484d;pointer-events:none";
|
|
427
|
-
entry.appendChild(dot);
|
|
428
|
-
}
|
|
429
|
-
entry.addEventListener("click", function () {
|
|
430
|
-
try { localStorage.setItem(NAV_SEEN_KEY, "1"); } catch (e) {}
|
|
431
|
-
var dots = entry.children || [];
|
|
432
|
-
for (var i = 0; i < dots.length; i++) {
|
|
433
|
-
var d = dots[i];
|
|
434
|
-
if (d.getAttribute && d.getAttribute("aria-hidden") === "true" && /background:#e5484d/.test(d.style && d.style.cssText || "")) {
|
|
435
|
-
try { entry.removeChild(d); } catch (e2) {}
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
openRemoteSettings();
|
|
440
|
-
});
|
|
441
|
-
settingsBtn.parentNode.insertBefore(entry, settingsBtn); // 官方「设置」在入口下方
|
|
442
|
-
clearInterval(iv);
|
|
443
|
-
} catch (e) { /* 忽略单次失败,继续重试 */ }
|
|
444
|
-
}, 300);
|
|
445
|
-
if (typeof iv.unref === "function") iv.unref();
|
|
446
|
-
} catch (e) { /* 非关键 */ }
|
|
447
|
-
}
|
|
448
|
-
if (document.readyState === "loading") {
|
|
449
|
-
document.addEventListener("DOMContentLoaded", injectSidebarRemoteEntry);
|
|
450
|
-
} else {
|
|
451
|
-
setTimeout(injectSidebarRemoteEntry, 600);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
162
|
// ── 首次安装引导小红点(设置页「远程访问」栏目,localStorage 控制) ────
|
|
455
163
|
// 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
|
|
456
164
|
// 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
|
|
@@ -1313,8 +1021,9 @@ window.__ModuleLoader__.load({
|
|
|
1313
1021
|
|
|
1314
1022
|
useEffect(function () { refresh(); }, [refresh]);
|
|
1315
1023
|
useEffect(function () {
|
|
1316
|
-
|
|
1317
|
-
|
|
1024
|
+
// 展开「忘记密码」重置表单时不用预载登录表单验证码(收起的 reset 表单用独立 pwdCap)
|
|
1025
|
+
if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !pwdOpen && !lcap) loadCaptcha("login");
|
|
1026
|
+
}, [st, loggedIn, mode, authTab, pwdOpen, lcap]);
|
|
1318
1027
|
|
|
1319
1028
|
// ── 📱 远程访问卡:常量 / 轮询 / 一次性访问密钥 / 已授权设备 ─────────────────
|
|
1320
1029
|
var KEY_AUTO_REFRESH_MS = 25000; // 停留栏目时约每 25s 自动轮换一把新的一次性密钥(防已用/过期)
|
|
@@ -1538,6 +1247,7 @@ window.__ModuleLoader__.load({
|
|
|
1538
1247
|
return post("/dsh-remote/config", { phone: phone.trim(), password: pass }).then(function (cfg) {
|
|
1539
1248
|
// 切换连接账号(手机号与之前不同):清除旧账号留下的反馈线程凭据
|
|
1540
1249
|
if (prevPhone !== phone.trim()) fbClearThreads();
|
|
1250
|
+
setPwdOpen(false); // 登录成功(可能从「忘记密码」返回)→ 收起重置表单
|
|
1541
1251
|
setSt(cfg); setPass(""); setLcapTxt(""); setLcap(null);
|
|
1542
1252
|
setMsg("ok", "✅ 登录成功,账号已保存");
|
|
1543
1253
|
});
|
|
@@ -1588,6 +1298,7 @@ window.__ModuleLoader__.load({
|
|
|
1588
1298
|
.then(function (body) {
|
|
1589
1299
|
if (body.ok || body.status === 201 || (body.body && body.body.token)) {
|
|
1590
1300
|
return post("/dsh-remote/config", { phone: rphone.trim(), password: rpass }).then(function (cfg) {
|
|
1301
|
+
setPwdOpen(false);
|
|
1591
1302
|
setSt(cfg);
|
|
1592
1303
|
setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRcap(null); setRcapTxt(""); setRInvite("");
|
|
1593
1304
|
setMsg("ok", "✅ 注册成功,已自动登录");
|
|
@@ -1609,17 +1320,23 @@ window.__ModuleLoader__.load({
|
|
|
1609
1320
|
// 退出登录:清除本机保存的用户反馈线程凭据(thread_token 见 FB_THREADS_KEY),
|
|
1610
1321
|
// 反馈历史保留在服务端(按账号校验),账号身份变化后本机不再可见旧线程。
|
|
1611
1322
|
fbClearThreads();
|
|
1323
|
+
setPwdOpen(false); // 退出登录后回到登录卡,收起重置/修改密码表单
|
|
1612
1324
|
if (clearForm) { setPhone(""); setPass(""); setLcapTxt(""); setLcap(null); setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRsmsBtn("获取验证码"); setRcapTxt(""); setRcap(null); }
|
|
1613
1325
|
setMessage(null);
|
|
1614
1326
|
}).catch(function (e) { setMsg("err", "退出失败: " + e.message); })
|
|
1615
1327
|
.finally(function () { setBusy(""); });
|
|
1616
1328
|
};
|
|
1617
1329
|
|
|
1618
|
-
// ---------- 🔒
|
|
1330
|
+
// ---------- 🔒 修改密码 / 忘记密码(共用:短信验证码重置;企业端公开 POST /api/password/reset) ----------
|
|
1619
1331
|
// 图形验证码 + 短信验证码复用既有 /dsh-remote/captcha、/dsh-remote/sms-code 防刷路径;
|
|
1620
|
-
// 新密码 ≥8 位。成功后企业端使全部授权设备/会话失效(含 E2EE
|
|
1621
|
-
// 且桌面端配置 config.phone/password 里的旧口令已不可用 → 本地登出(清 threads/会话),
|
|
1332
|
+
// 新密码 ≥8 位。成功后企业端使全部授权设备/会话失效(含 E2EE 派生口令)→ 本地登出,
|
|
1622
1333
|
// 提示用新密码重新登录(登录成功会把新密码写回本地 config,覆盖旧口令并重建 E2EE 派生)。
|
|
1334
|
+
// 两处入口共用同一组字段/函数(pwdOpen/pwdCap/pwdCapTxt/pwdSms/pwdSmsBtn/pwdNew 与
|
|
1335
|
+
// togglePwdForm/sendPwdSms/doResetPwd/renderResetPwdForm,避免重复实现):
|
|
1336
|
+
// - 账号卡「🔒 修改密码」:fromLogin=false,手机号以当前登录账号为准(只读);
|
|
1337
|
+
// - 登录卡「忘记密码?」:fromLogin=true,手机号预填登录输入、可改;成功后请用新密码登录。
|
|
1338
|
+
var PWD_RESET_WARN = "修改后所有已授权设备/会话将失效,需重新登录与解锁";
|
|
1339
|
+
|
|
1623
1340
|
function pwdLoadCaptcha() {
|
|
1624
1341
|
setPwdCap({ id: null, svg: '<span class="dru-hint">加载中</span>' });
|
|
1625
1342
|
fetch("/dsh-remote/captcha").then(function (r) {
|
|
@@ -1632,10 +1349,18 @@ window.__ModuleLoader__.load({
|
|
|
1632
1349
|
});
|
|
1633
1350
|
}).catch(function () { setPwdCap({ id: null, svg: null }); });
|
|
1634
1351
|
}
|
|
1635
|
-
/**
|
|
1636
|
-
var
|
|
1637
|
-
var
|
|
1638
|
-
|
|
1352
|
+
/** 展开/收起重置密码表单(账号卡「修改密码」与登录卡「忘记密码」共用开关)。 */
|
|
1353
|
+
var togglePwdForm = function () {
|
|
1354
|
+
var next = !pwdOpen;
|
|
1355
|
+
setPwdOpen(next);
|
|
1356
|
+
setMessage(null);
|
|
1357
|
+
if (next && !pwdCap) pwdLoadCaptcha();
|
|
1358
|
+
};
|
|
1359
|
+
/** 发送短信验证码(图形验证码防刷;captcha_invalid → 重载验证码提示重试)。ph 由调用方给出:
|
|
1360
|
+
* 账号卡=当前登录手机号;登录卡「忘记密码」=表单输入手机号。 */
|
|
1361
|
+
var sendPwdSms = function (ph) {
|
|
1362
|
+
ph = String(ph || "").trim();
|
|
1363
|
+
if (!/^1\d{10}$/.test(ph)) { setMsg("err", "请输入正确的手机号"); return; }
|
|
1639
1364
|
if (!pwdCap || !pwdCap.id || !pwdCapTxt.trim()) { setMsg("err", "请输入图中验证码(点击图片可刷新)"); return; }
|
|
1640
1365
|
setBusy("pwd-sms");
|
|
1641
1366
|
post("/dsh-remote/sms-code", { phone: ph, captcha_id: pwdCap.id, captcha_answer: pwdCapTxt.trim() })
|
|
@@ -1656,10 +1381,14 @@ window.__ModuleLoader__.load({
|
|
|
1656
1381
|
})
|
|
1657
1382
|
.finally(function () { setBusy(""); });
|
|
1658
1383
|
};
|
|
1659
|
-
/**
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1384
|
+
/**
|
|
1385
|
+
* 确认重置密码:POST /dsh-remote/password/reset → 成功后本地登出,提示用新密码登录。
|
|
1386
|
+
* @param {string} ph - 手机号(账号卡=当前登录账号;登录卡「忘记密码」=表单输入)。
|
|
1387
|
+
* @param {boolean} fromLogin - true=登录卡「忘记密码」;false=账号卡「修改密码」。
|
|
1388
|
+
*/
|
|
1389
|
+
var doResetPwd = function (ph, fromLogin) {
|
|
1390
|
+
ph = String(ph || "").trim();
|
|
1391
|
+
if (!/^1\d{10}$/.test(ph)) { setMsg("err", fromLogin ? "请输入正确的手机号" : "请先登录手机号账号(修改密码以当前账号为准)"); return; }
|
|
1663
1392
|
if (!pwdSms.trim()) { setMsg("err", "请填写短信验证码"); return; }
|
|
1664
1393
|
if (pwdNew.length < 8) { setMsg("err", "新密码至少 8 位"); return; }
|
|
1665
1394
|
setBusy("pwd-reset");
|
|
@@ -1670,7 +1399,7 @@ window.__ModuleLoader__.load({
|
|
|
1670
1399
|
var relayBody = (body && body.body) || body || {};
|
|
1671
1400
|
var errText = (relayBody.error && (relayBody.error.message || relayBody.error))
|
|
1672
1401
|
|| relayBody.detail || ("修改失败(" + ((body && body.status) || "?") + ")");
|
|
1673
|
-
setMsg("err", "修改失败:" + String(errText));
|
|
1402
|
+
setMsg("err", (fromLogin ? "重置失败:" : "修改失败:") + String(errText));
|
|
1674
1403
|
setPwdSms(""); // 短信验证码一次性:失败后需重新获取
|
|
1675
1404
|
return undefined;
|
|
1676
1405
|
}
|
|
@@ -1682,13 +1411,19 @@ window.__ModuleLoader__.load({
|
|
|
1682
1411
|
setAkey(null); setAkeyMsg(null);
|
|
1683
1412
|
fbClearThreads();
|
|
1684
1413
|
setPwdOpen(false); setPwdSms(""); setPwdNew(""); setPwdCap(null); setPwdCapTxt(""); setPwdSmsBtn("获取验证码");
|
|
1685
|
-
|
|
1686
|
-
|
|
1414
|
+
if (fromLogin) {
|
|
1415
|
+
// 「忘记密码」成功:本机登出完成 → 回到登录表单提示用新密码登录(手机号保留,便于直接重登)
|
|
1416
|
+
setPass(""); setLcapTxt(""); setLcap(null); loadCaptcha("login");
|
|
1417
|
+
setMsg("ok", "✅ 密码已重置成功:" + PWD_RESET_WARN + ",本机已退出登录。请用新密码登录。");
|
|
1418
|
+
} else {
|
|
1419
|
+
setMsg("ok", "✅ 密码已修改成功:所有已授权设备与会话已失效,本机已退出登录。" +
|
|
1420
|
+
"请在下方账号卡用「新密码」重新登录——登录会更新本机保存的密码并重新启用端到端加密(E2EE)。");
|
|
1421
|
+
}
|
|
1687
1422
|
}).catch(function (e) {
|
|
1688
|
-
setMsg("err", "
|
|
1423
|
+
setMsg("err", "密码已" + (fromLogin ? "重置" : "修改") + "成功,但本机退出登录失败:" + e.message + "(建议手动「退出登录」后用新密码重新登录,以更新本机配置密码)");
|
|
1689
1424
|
});
|
|
1690
1425
|
})
|
|
1691
|
-
.catch(function (e) { setMsg("err", "修改失败: " + e.message); })
|
|
1426
|
+
.catch(function (e) { setMsg("err", (fromLogin ? "重置失败: " : "修改失败: ") + e.message); })
|
|
1692
1427
|
.finally(function () { setBusy(""); });
|
|
1693
1428
|
};
|
|
1694
1429
|
|
|
@@ -1710,6 +1445,7 @@ window.__ModuleLoader__.load({
|
|
|
1710
1445
|
.then(function (body) {
|
|
1711
1446
|
// 切换到自建服务(连接账号上下文变为本地无账号):清除 SaaS 账号的反馈线程凭据
|
|
1712
1447
|
fbClearThreads();
|
|
1448
|
+
setPwdOpen(false);
|
|
1713
1449
|
setSt(body); setSelfHost(""); setLocalKey("");
|
|
1714
1450
|
setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,bridge 已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
|
|
1715
1451
|
})
|
|
@@ -1771,29 +1507,30 @@ window.__ModuleLoader__.load({
|
|
|
1771
1507
|
busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
|
|
1772
1508
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友赚会员"),
|
|
1773
1509
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
|
|
1774
|
-
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick:
|
|
1775
|
-
var next = !pwdOpen;
|
|
1776
|
-
setPwdOpen(next);
|
|
1777
|
-
setMessage(null);
|
|
1778
|
-
if (next && !pwdCap) pwdLoadCaptcha();
|
|
1779
|
-
} }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
|
|
1510
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
|
|
1780
1511
|
h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
|
|
1781
1512
|
),
|
|
1782
1513
|
h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
1783
1514
|
"升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
|
|
1784
1515
|
(endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : "")),
|
|
1785
|
-
pwdOpen ?
|
|
1516
|
+
pwdOpen ? renderResetPwdForm(false) : null
|
|
1786
1517
|
);
|
|
1787
1518
|
}
|
|
1788
1519
|
|
|
1789
|
-
// ---------- 🔒
|
|
1790
|
-
|
|
1791
|
-
|
|
1520
|
+
// ---------- 🔒 修改密码 / 忘记密码 共用重置表单(图形验证码 + 短信验证码 + 新密码≥8) ----------
|
|
1521
|
+
// fromLogin=false:账号卡「修改密码」手机号以当前登录账号为准(只读);
|
|
1522
|
+
// fromLogin=true:登录卡「忘记密码」手机号预填登录输入、可改(成功后返回登录表单请用新密码登录)。
|
|
1523
|
+
function renderResetPwdForm(fromLogin) {
|
|
1524
|
+
var ph = fromLogin ? phone : ((st && st.config && st.config.phone) || "");
|
|
1792
1525
|
return h("div", { className: "dru-card", style: { marginTop: 10, border: "1px dashed #d0d7de" } },
|
|
1793
|
-
h("h3", null, "🔒 修改密码(短信验证)"),
|
|
1526
|
+
h("h3", null, fromLogin ? "忘记密码(短信重置)" : "🔒 修改密码(短信验证)"),
|
|
1794
1527
|
h("div", { className: "dru-hint", style: { marginBottom: 8 } },
|
|
1795
|
-
|
|
1796
|
-
|
|
1528
|
+
fromLogin
|
|
1529
|
+
? "手机号预填当前输入,可改为其它账号;" + PWD_RESET_WARN + "。"
|
|
1530
|
+
: PWD_RESET_WARN + "(E2EE 用新密码重新派生)。"),
|
|
1531
|
+
field("手机号", fromLogin
|
|
1532
|
+
? input({ type: "tel", value: ph, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })
|
|
1533
|
+
: h("input", { className: "dru-input", type: "tel", value: ph, disabled: true, readOnly: true, title: "以当前登录账号为准" })),
|
|
1797
1534
|
field("图形验证码", h("div", { className: "dru-captcha" },
|
|
1798
1535
|
input({ value: pwdCapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setPwdCapTxt(e.target.value); } }),
|
|
1799
1536
|
h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { pwdLoadCaptcha(); }, dangerouslySetInnerHTML: pwdCap && pwdCap.svg && pwdCap.svg.indexOf("<svg") === 0 ? { __html: pwdCap.svg } : void 0 },
|
|
@@ -1801,13 +1538,19 @@ window.__ModuleLoader__.load({
|
|
|
1801
1538
|
)),
|
|
1802
1539
|
field("短信验证码", h("div", { className: "dru-captcha" },
|
|
1803
1540
|
input({ value: pwdSms, placeholder: "6 位验证码", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setPwdSms(e.target.value); } }),
|
|
1804
|
-
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: sendPwdSms }, pwdSmsBtn)
|
|
1541
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: function () { sendPwdSms(ph); } }, pwdSmsBtn)
|
|
1805
1542
|
)),
|
|
1806
1543
|
field("新密码(至少 8 位)", input({ type: "password", value: pwdNew, minLength: 8, autoComplete: "new-password", placeholder: "至少 8 位", onChange: function (e) { setPwdNew(e.target.value); } })),
|
|
1807
1544
|
h("div", { className: "dru-hint", style: { marginBottom: 8 } },
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1545
|
+
fromLogin
|
|
1546
|
+
? "重置成功后本机会退出登录(如有已保存的旧账号),请用新密码登录。"
|
|
1547
|
+
: "修改成功后,本机保存的旧密码会被清除并退出登录;请用新密码重新登录(登录会更新本机配置密码并重新启用远程访问与端到端加密)。"),
|
|
1548
|
+
h("button", { type: "button", className: "dru-btn dru-btn-danger", style: { width: "100%" }, disabled: busy !== "", onClick: function () { doResetPwd(ph, fromLogin); } },
|
|
1549
|
+
busy === "pwd-reset" ? "提交中…" : busy === "pwd-logout" ? "已重置,正在退出本地登录…" : (fromLogin ? "确认重置密码" : "确认修改密码")),
|
|
1550
|
+
fromLogin
|
|
1551
|
+
? h("div", { style: { textAlign: "right", marginTop: 6 } },
|
|
1552
|
+
h("button", { type: "button", className: "dru-linkbtn", onClick: function () { togglePwdForm(); } }, "← 返回登录"))
|
|
1553
|
+
: null
|
|
1811
1554
|
);
|
|
1812
1555
|
}
|
|
1813
1556
|
|
|
@@ -2039,19 +1782,26 @@ window.__ModuleLoader__.load({
|
|
|
2039
1782
|
? renderAccount()
|
|
2040
1783
|
: h("div", null,
|
|
2041
1784
|
h("div", { className: "dru-tabs" },
|
|
2042
|
-
h("div", { className: "dru-tab" + (authTab === "login" ? " active" : ""), onClick: function () { setAuthTab("login"); setMessage(null); } }, "登录"),
|
|
2043
|
-
h("div", { className: "dru-tab" + (authTab === "register" ? " active" : ""), onClick: function () { setAuthTab("register"); setMessage(null); } }, "注册")
|
|
1785
|
+
h("div", { className: "dru-tab" + (authTab === "login" ? " active" : ""), onClick: function () { setAuthTab("login"); setPwdOpen(false); setMessage(null); } }, "登录"),
|
|
1786
|
+
h("div", { className: "dru-tab" + (authTab === "register" ? " active" : ""), onClick: function () { setAuthTab("register"); setPwdOpen(false); setMessage(null); } }, "注册")
|
|
2044
1787
|
),
|
|
2045
1788
|
authTab === "login"
|
|
2046
|
-
?
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
1789
|
+
? (pwdOpen
|
|
1790
|
+
// 「忘记密码」展开:与账号卡「修改密码」共用同一重置表单(手机号可编辑、预填登录输入)
|
|
1791
|
+
? renderResetPwdForm(true)
|
|
1792
|
+
: h("div", null,
|
|
1793
|
+
field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
|
|
1794
|
+
field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
|
|
1795
|
+
field("验证码", h("div", { className: "dru-captcha" },
|
|
1796
|
+
input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
|
|
1797
|
+
h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("login"); }, dangerouslySetInnerHTML: lcap && lcap.svg && lcap.svg.indexOf("<svg") === 0 ? { __html: lcap.svg } : void 0 },
|
|
1798
|
+
lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
|
|
1799
|
+
)),
|
|
1800
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "login" ? "登录中…" : "登录"),
|
|
1801
|
+
// 忘记密码:小字入口,点击展开短信验证码重置表单(不抢占任何官方按钮)
|
|
1802
|
+
h("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 } },
|
|
1803
|
+
h("button", { type: "button", className: "dru-linkbtn", disabled: busy !== "", onClick: togglePwdForm }, "忘记密码?"))
|
|
1804
|
+
)
|
|
2055
1805
|
)
|
|
2056
1806
|
: h("div", null,
|
|
2057
1807
|
field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
|
|
@@ -1020,7 +1020,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
1020
1020
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
1021
1021
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
1022
1022
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
1023
|
-
const PLUGIN_VERSION = "0.6.0-beta.
|
|
1023
|
+
const PLUGIN_VERSION = "0.6.0-beta.11";
|
|
1024
1024
|
const UPDATE_LOG = ".dsh-update.log";
|
|
1025
1025
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
1026
1026
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// 🔒
|
|
1
|
+
// 🔒 修改密码 / 忘记密码(短信验证码重置):
|
|
2
2
|
// - node 半代理:POST /dsh-remote/password/reset → 企业端公开 /api/password/reset(无需 Bearer,透传 ok/error);
|
|
3
|
-
// -
|
|
3
|
+
// - 浏览器半:账号卡「🔒 修改密码」小表单 + 登录卡「忘记密码?」共用同一重置表单
|
|
4
|
+
// (手机号[账号卡只读/登录卡可编辑预填] + 图形验证码 + 短信验证码 + 新密码≥8),
|
|
4
5
|
// 成功后本地登出(清反馈线程凭据)并提示用新密码重新登录(登录会覆盖桌面端 config 密码)。
|
|
5
6
|
import assert from "node:assert/strict";
|
|
6
7
|
import http from "node:http";
|
|
@@ -238,8 +239,8 @@ async function runResetFlow(opts = {}) {
|
|
|
238
239
|
await flush();
|
|
239
240
|
tree = plugin.render();
|
|
240
241
|
|
|
241
|
-
// 小表单展开:图形验证码 + 手机号(当前账号,不可改) +
|
|
242
|
-
assert.ok(textHas(tree, "
|
|
242
|
+
// 小表单展开:图形验证码 + 手机号(当前账号,不可改) + 新密码提示(与登录卡「忘记密码」共用表单/文案)
|
|
243
|
+
assert.ok(textHas(tree, "修改后所有已授权设备/会话将失效,需重新登录与解锁"), "应提示设备/会话失效与重新登录解锁");
|
|
243
244
|
const phoneBox = find(tree, (n) => n.props?.value === "13800000000" && n.props?.disabled === true);
|
|
244
245
|
assert.ok(phoneBox, "手机号应预填当前账号且不可修改");
|
|
245
246
|
|
|
@@ -299,19 +300,126 @@ test("修改密码失败路径:企业端错误透传为可读提示,不登
|
|
|
299
300
|
assert.ok(textHas(tree, "修改失败"), "错误应带「修改失败」前缀");
|
|
300
301
|
});
|
|
301
302
|
|
|
302
|
-
test("
|
|
303
|
-
|
|
303
|
+
test("忘记密码(登录卡):「忘记密码?」展开共用重置表单(预填手机号)→ sms-code → password/reset → 本地登出并提示请用新密码登录", async () => {
|
|
304
|
+
const plugin = loadPlugin({ localStorageSeed: { "dsh-feedback-threads": '[{"id":"fb_1","token":"tok-abc","at":1}]' } });
|
|
305
|
+
plugin.states[0] = { config: { phone: "", deviceId: "dev-x", mode: "saas" }, service: { running: false } };
|
|
306
|
+
let tree = plugin.render();
|
|
307
|
+
|
|
308
|
+
// 登录 tab(SaaS 登录表单)应提供小字「忘记密码?」入口(紧邻登录按钮,不抢占官方按钮)
|
|
309
|
+
const forgetLink = find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("忘记密码")));
|
|
310
|
+
assert.ok(forgetLink, "登录表单内应提供「忘记密码?」入口");
|
|
311
|
+
assert.ok(!textHas(tree, "确认重置密码"), "未点击前不应展开重置表单");
|
|
312
|
+
|
|
313
|
+
// 先在登录手机号输入当前号码 → 展开后应预填
|
|
314
|
+
find(tree, (n) => n.props?.placeholder === "11 位手机号").props.onChange({ target: { value: "13800000000" } });
|
|
315
|
+
tree = plugin.render();
|
|
316
|
+
find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("忘记密码"))).props.onClick();
|
|
317
|
+
await flush();
|
|
318
|
+
await flush();
|
|
319
|
+
tree = plugin.render();
|
|
320
|
+
|
|
321
|
+
// 展开表单与账号卡「修改密码」共用字段/文案/函数
|
|
322
|
+
assert.ok(textHas(tree, "修改后所有已授权设备/会话将失效,需重新登录与解锁"), "应提示设备/会话失效与重新登录解锁");
|
|
323
|
+
assert.ok(find(tree, (n) => n.props?.value === "13800000000"), "重置表单手机号应预填当前输入(可改)");
|
|
324
|
+
const capInput = find(tree, (n) => n.props?.placeholder === "图中数字");
|
|
325
|
+
assert.ok(capInput, "应展示图形验证码输入框(现有 captcha 通道)");
|
|
326
|
+
capInput.props.onChange({ target: { value: "654321" } });
|
|
327
|
+
tree = plugin.render();
|
|
328
|
+
find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("获取验证码"))).props.onClick();
|
|
329
|
+
await flush();
|
|
330
|
+
await flush();
|
|
331
|
+
|
|
332
|
+
const smsReq = plugin.requests.find((r) => r.path === "/dsh-remote/sms-code");
|
|
333
|
+
assert.ok(smsReq, "应请求 /dsh-remote/sms-code(与修改密码同一短信防刷路径)");
|
|
334
|
+
assert.deepEqual(smsReq.body, { phone: "13800000000", captcha_id: "cap-pwd-1", captcha_answer: "654321" });
|
|
335
|
+
|
|
336
|
+
tree = plugin.render();
|
|
337
|
+
find(tree, (n) => n.props?.placeholder === "6 位验证码").props.onChange({ target: { value: "123456" } });
|
|
338
|
+
tree = plugin.render();
|
|
339
|
+
find(tree, (n) => n.props?.placeholder === "至少 8 位").props.onChange({ target: { value: "newpass123" } });
|
|
340
|
+
tree = plugin.render();
|
|
341
|
+
find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("确认重置密码"))).props.onClick();
|
|
342
|
+
await flush();
|
|
343
|
+
await flush();
|
|
344
|
+
await flush();
|
|
345
|
+
await flush();
|
|
346
|
+
|
|
347
|
+
const resetReq = plugin.requests.find((r) => r.path === "/dsh-remote/password/reset");
|
|
348
|
+
assert.ok(resetReq, "应 POST /dsh-remote/password/reset(复用已有 node 半代理)");
|
|
349
|
+
assert.equal(resetReq.method, "POST");
|
|
350
|
+
assert.deepEqual(resetReq.body, { phone: "13800000000", sms_code: "123456", new_password: "newpass123" },
|
|
351
|
+
"应提交 {phone,sms_code,new_password}");
|
|
352
|
+
|
|
353
|
+
assert.ok(plugin.requests.some((r) => r.path === "/dsh-remote/logout"), "重置成功后应本地登出");
|
|
354
|
+
assert.ok(plugin.removed.includes("dsh-feedback-threads"), "重置成功登出应清除本机反馈线程凭据");
|
|
355
|
+
|
|
356
|
+
tree = plugin.render();
|
|
357
|
+
assert.ok(textHas(tree, "密码已重置成功"), "应给出重置成功提示");
|
|
358
|
+
assert.ok(textHas(tree, "请用新密码登录"), "应提示「请用新密码登录」");
|
|
359
|
+
assert.ok(!find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("确认重置密码"))),
|
|
360
|
+
"成功后重置表单应收起");
|
|
361
|
+
assert.ok(find(tree, (n) => n.props?.placeholder === "密码"), "应回到登录表单(可直接输入新密码登录)");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("忘记密码失败路径:企业端错误透传为可读提示(重置失败前缀),不本地登出", async () => {
|
|
365
|
+
const plugin = loadPlugin({ resetFail: true });
|
|
366
|
+
plugin.states[0] = { config: { phone: "", deviceId: "dev-x", mode: "saas" }, service: { running: false } };
|
|
367
|
+
let tree = plugin.render();
|
|
368
|
+
|
|
369
|
+
find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("忘记密码"))).props.onClick();
|
|
370
|
+
await flush();
|
|
371
|
+
await flush();
|
|
372
|
+
tree = plugin.render();
|
|
373
|
+
find(tree, (n) => n.props?.placeholder === "11 位手机号").props.onChange({ target: { value: "13800000000" } });
|
|
374
|
+
tree = plugin.render();
|
|
375
|
+
find(tree, (n) => n.props?.placeholder === "图中数字").props.onChange({ target: { value: "654321" } });
|
|
376
|
+
tree = plugin.render();
|
|
377
|
+
find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("获取验证码"))).props.onClick();
|
|
378
|
+
await flush();
|
|
379
|
+
await flush();
|
|
380
|
+
tree = plugin.render();
|
|
381
|
+
find(tree, (n) => n.props?.placeholder === "6 位验证码").props.onChange({ target: { value: "123456" } });
|
|
382
|
+
tree = plugin.render();
|
|
383
|
+
find(tree, (n) => n.props?.placeholder === "至少 8 位").props.onChange({ target: { value: "newpass123" } });
|
|
384
|
+
tree = plugin.render();
|
|
385
|
+
find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("确认重置密码"))).props.onClick();
|
|
386
|
+
await flush();
|
|
387
|
+
await flush();
|
|
388
|
+
await flush();
|
|
389
|
+
await flush();
|
|
390
|
+
|
|
391
|
+
assert.ok(!plugin.requests.some((r) => r.path === "/dsh-remote/logout"), "失败时不应本地登出");
|
|
392
|
+
tree = plugin.render();
|
|
393
|
+
assert.ok(textHas(tree, "短信验证码错误或已过期"), "应展示企业端透传的可读错误");
|
|
394
|
+
assert.ok(textHas(tree, "重置失败"), "错误应带「重置失败」前缀");
|
|
395
|
+
assert.ok(find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("确认重置密码"))),
|
|
396
|
+
"失败后重置表单保留,可修改后重试");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("源码约束:账号区含「修改密码」/登录卡含「忘记密码」入口、共用重置逻辑、reset 代理路由、新密码≥8 与重新登录提示", () => {
|
|
400
|
+
// 浏览器半:账号卡修改密码入口
|
|
304
401
|
assert.match(SOURCE, /修改密码/);
|
|
305
402
|
assert.match(SOURCE, /🔒 修改密码/);
|
|
403
|
+
assert.match(SOURCE, /确认修改密码/);
|
|
404
|
+
// 浏览器半:登录卡「忘记密码」小字入口 → 展开共用重置表单(复用同一组 pwd* 字段/函数)
|
|
405
|
+
assert.match(SOURCE, /忘记密码/);
|
|
406
|
+
assert.match(SOURCE, /忘记密码?/);
|
|
407
|
+
assert.match(SOURCE, /确认重置密码/);
|
|
408
|
+
assert.match(SOURCE, /请用新密码登录/);
|
|
409
|
+
assert.match(SOURCE, /修改后所有已授权设备\/会话将失效,需重新登录与解锁/);
|
|
410
|
+
// 共用重置:同一 doResetPwd/sendPwdSms/renderResetPwdForm(fromLogin 分流手机号来源与提示)
|
|
411
|
+
assert.match(SOURCE, /doResetPwd\(ph, fromLogin\)/);
|
|
412
|
+
assert.match(SOURCE, /sendPwdSms\(ph\)/);
|
|
413
|
+
assert.match(SOURCE, /function renderResetPwdForm\(fromLogin\)/);
|
|
306
414
|
assert.match(SOURCE, /dsh-remote\/password\/reset/);
|
|
307
415
|
assert.match(SOURCE, /new_password/);
|
|
308
416
|
assert.match(SOURCE, /新密码至少 8 位/);
|
|
309
417
|
assert.match(SOURCE, /pwdNew\.length < 8/);
|
|
310
|
-
assert.match(SOURCE,
|
|
418
|
+
assert.match(SOURCE, /所有已授权设备与会话已失效/);
|
|
311
419
|
assert.match(SOURCE, /重新登录/);
|
|
312
420
|
assert.match(SOURCE, /E2EE/);
|
|
313
421
|
assert.match(SOURCE, /fbClearThreads\(\)/);
|
|
314
|
-
//
|
|
422
|
+
// 修改密码/忘记密码的短信验证码复用 /dsh-remote/sms-code(含图形验证码防刷)
|
|
315
423
|
assert.match(SOURCE, /post\("\/dsh-remote\/sms-code"/);
|
|
316
424
|
assert.match(SOURCE, /captcha_invalid/);
|
|
317
425
|
// node 半代理路由
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-remote-web 浏览器半回归(2026-09 前名 dsh-remote-ui):面板入口迁移进「设置」页 settings.section 官方扩展点。
|
|
2
|
-
// 覆盖:栏目注册(id/order/label
|
|
3
|
-
//
|
|
2
|
+
// 覆盖:栏目注册(id/order/label)、侧边栏入口与浮动面板移除(不再向官方侧栏注入任何按钮/点击机器)、
|
|
3
|
+
// 账号区无「切换账号」、「关于 dsh-remote」说明卡片、退出登录/切换连接账号清理反馈线程凭据、首次安装红点引导。
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import test from "node:test";
|
|
@@ -98,7 +98,6 @@ function loadPlugin(opts = {}) {
|
|
|
98
98
|
},
|
|
99
99
|
querySelectorAll(sel) {
|
|
100
100
|
if (sel === "button") return opts.navCells || [];
|
|
101
|
-
if (typeof opts.navQuery === "function") return opts.navQuery(sel);
|
|
102
101
|
return [];
|
|
103
102
|
},
|
|
104
103
|
};
|
|
@@ -158,7 +157,6 @@ function loadPlugin(opts = {}) {
|
|
|
158
157
|
return {
|
|
159
158
|
registered, metas, injects, requests, removed, states, localStorage,
|
|
160
159
|
intervals,
|
|
161
|
-
navWindow: sandbox.window,
|
|
162
160
|
lastObserver: () => lastObserver,
|
|
163
161
|
renderSection() { hook = 0; return registered.get("dsh-remote")({ close() {} }); },
|
|
164
162
|
};
|
|
@@ -284,116 +282,49 @@ test("红点已看过(localStorage 有 key)时不注入,重启 DSH Web 不
|
|
|
284
282
|
assert.equal(navCell.children.length, 0, "已看过时不应再注入红点");
|
|
285
283
|
});
|
|
286
284
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
_attrs: {},
|
|
297
|
-
_clicked: 0,
|
|
298
|
-
getAttribute(k) { return k in this._attrs ? this._attrs[k] : null; },
|
|
299
|
-
setAttribute(k, v) { this._attrs[k] = String(v); },
|
|
300
|
-
click() { this._clicked++; },
|
|
301
|
-
};
|
|
302
|
-
if (aria) node._attrs["aria-label"] = aria;
|
|
303
|
-
if (role) node._attrs.role = role;
|
|
304
|
-
return node;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** 简易“导航容器”:候选查询(NAV_CAND_SEL)一律返回给定候选列表(容器内行为仿真)。 */
|
|
308
|
-
function navContainer(candidates) {
|
|
309
|
-
return { querySelectorAll() { return candidates; } };
|
|
310
|
-
}
|
|
311
|
-
/** 容器定位选择器(NAV_CONTAINER_SEL)特征:client 只在语义容器选择器上全文档扫描。 */
|
|
312
|
-
const isContainerSel = (sel) => /navigation|sidebar|navRail|navPanel|sidenav|appnav/.test(sel);
|
|
313
|
-
/** 建一个“侧栏容器已存在”的沙箱导航环境:document 语义容器查询 → [container],其余 → []。 */
|
|
314
|
-
function navHarness(candidates) {
|
|
315
|
-
const container = navContainer(candidates);
|
|
316
|
-
return { container, navQuery: (sel) => (isContainerSel(sel) ? [container] : []) };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
test("侧栏入口点击流:远程栏目项已存在(navCell/aria-label,含 emoji 前缀)→ 容器内直接命中栏目,不点注入项自身", () => {
|
|
320
|
-
const injected = navNode({ id: "dru-nav-remote", text: "📱 远程访问", cls: "navCell-clone" }); // 注入的侧栏项
|
|
321
|
-
const settingsNav = navNode({ text: "设置", cls: "navCell-set" });
|
|
322
|
-
const remoteCell = navNode({ aria: "远程访问", cls: "VOzbGW_navCell", tag: "button" }); // 纯图标+aria-label 的栏目项
|
|
323
|
-
const candidates = [injected, settingsNav, remoteCell];
|
|
324
|
-
const env = navHarness(candidates);
|
|
325
|
-
const plugin = loadPlugin({ navQuery: env.navQuery });
|
|
326
|
-
plugin.navWindow.__dshRemoteNav.open();
|
|
327
|
-
assert.equal(remoteCell._clicked, 1, "应命中「远程访问」栏目项(aria-label 亦可读)");
|
|
328
|
-
assert.equal(remoteCell.getAttribute("data-dru-remote"), "1", "命中后应打 data-dru-remote 标记(加速二次命中)");
|
|
329
|
-
assert.equal(injected._clicked, 0, "不得点击注入的侧栏项自身(防递归)");
|
|
330
|
-
assert.equal(settingsNav._clicked, 0, "栏目已存在时无需退化点「设置」");
|
|
331
|
-
assert.equal(plugin.intervals.filter((x) => x.ms === 150).length, 0, "直接命中后不应启动轮询");
|
|
332
|
-
});
|
|
285
|
+
test("红点只挂在设置页「远程访问」栏目导航(navCell):普通按钮即使文本含「远程访问」也不注入,不遮挡官方入口", () => {
|
|
286
|
+
const navCells = [];
|
|
287
|
+
const officialCell = makeEl("button");
|
|
288
|
+
officialCell.className = "VOzbGW_navCell"; // shell hashed 类名(含 navCell 子串)=设置页栏目导航
|
|
289
|
+
officialCell.textContent = "📱 远程访问";
|
|
290
|
+
const plainBtn = makeEl("button");
|
|
291
|
+
plainBtn.className = "some-other-btn"; // 非 navCell(官方侧栏/内容区按钮):即使含「远程访问」也不得注入
|
|
292
|
+
plainBtn.textContent = "远程访问";
|
|
293
|
+
const plugin = loadPlugin({ navCells });
|
|
333
294
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const settingsNav = navNode({ text: "设置", cls: "navCell-set" });
|
|
337
|
-
const remoteCell = navNode({ text: "📱 远程访问", cls: "VOzbGW_navCell" }); // 设置页打开后才挂载
|
|
338
|
-
const candidates = [injected, settingsNav]; // 初始只有注入项 + 「设置」
|
|
339
|
-
const env = navHarness(candidates);
|
|
340
|
-
const plugin = loadPlugin({ navQuery: env.navQuery });
|
|
341
|
-
plugin.navWindow.__dshRemoteNav.open();
|
|
342
|
-
|
|
343
|
-
assert.equal(settingsNav._clicked, 1, "栏目不可达时应点「设置」进入设置页");
|
|
344
|
-
assert.equal(injected._clicked, 0, "不得点击注入的侧栏项自身");
|
|
345
|
-
const poll = plugin.intervals.find((x) => x.ms === 150);
|
|
346
|
-
assert.ok(poll, "应启动 150ms 轮询等待栏目挂载");
|
|
347
|
-
|
|
348
|
-
candidates.push(remoteCell); // 设置页已打开,栏目项挂载
|
|
349
|
-
poll.cb();
|
|
350
|
-
assert.equal(remoteCell._clicked, 1, "轮询到点后应补点「远程访问」栏目");
|
|
351
|
-
assert.equal(remoteCell.getAttribute("data-dru-remote"), "1", "栏目命中后应打标记");
|
|
352
|
-
assert.equal(settingsNav._clicked, 1, "「设置」只应点一次");
|
|
353
|
-
assert.equal(injected._clicked, 0, "轮询过程也不得点注入项自身");
|
|
354
|
-
});
|
|
295
|
+
navCells.push(officialCell, plainBtn);
|
|
296
|
+
plugin.lastObserver().cb([]);
|
|
355
297
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const settingsNav = navNode({ text: "设置", cls: "navCell-set" });
|
|
360
|
-
const sessionLike = navNode({ text: "分析竞品远程访问功能完整报告", cls: "chat-session-title", tag: "button" }); // 13 字会话标题(可点 button,考验文本长度规则)
|
|
361
|
-
const candidates = [injected, settingsNav, sessionLike];
|
|
362
|
-
const env = navHarness(candidates);
|
|
363
|
-
const plugin = loadPlugin({ navQuery: env.navQuery });
|
|
364
|
-
plugin.navWindow.__dshRemoteNav.open();
|
|
365
|
-
|
|
366
|
-
assert.equal(sessionLike._clicked, 0, "长文本(会话标题)候选绝不能被点击");
|
|
367
|
-
assert.equal(settingsNav._clicked, 1, "只能退化到容器内「设置」");
|
|
368
|
-
const poll = plugin.intervals.find((x) => x.ms === 150);
|
|
369
|
-
assert.ok(poll, "进「设置」后应轮询等待远程栏目");
|
|
370
|
-
// 轮询上限(20 次 × 150ms = 3s):跑满上限后不再产生任何点击
|
|
371
|
-
for (let i = 0; i < 20; i++) poll.cb();
|
|
372
|
-
assert.equal(sessionLike._clicked, 0, "长文本候选在轮询全程都不应被点击");
|
|
373
|
-
assert.equal(injected._clicked, 0, "注入项全程不应被点击");
|
|
374
|
-
assert.equal(settingsNav._clicked, 1, "「设置」只点一次(轮询只找「远程访问」,不重复点设置)");
|
|
298
|
+
assert.equal(officialCell.children.length, 1, "官方设置页「远程访问」栏目按钮应注入红点");
|
|
299
|
+
assert.equal(officialCell.children[0].className, "dru-reddot");
|
|
300
|
+
assert.equal(plainBtn.children.length, 0, "非栏目导航按钮不得注入红点(不遮挡官方按钮)");
|
|
375
301
|
});
|
|
376
302
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
assert.
|
|
386
|
-
assert.
|
|
387
|
-
assert.
|
|
388
|
-
|
|
389
|
-
assert.doesNotMatch(SOURCE, /
|
|
390
|
-
assert.
|
|
391
|
-
assert.
|
|
392
|
-
|
|
393
|
-
assert.
|
|
394
|
-
assert.
|
|
395
|
-
assert.
|
|
396
|
-
|
|
303
|
+
// ---------- 入口收敛:不再向官方侧栏注入任何按钮 / 导航点击机器 ----------
|
|
304
|
+
// 用户从官方「设置」进入 → 点 settings.section「远程访问」栏目(官方扩展点,天然不冲突),
|
|
305
|
+
// 官方「设置」按钮保持原样、不被遮挡。旧 Bug A 侧栏注入实现(容器限定点击流 + 独立按钮 +
|
|
306
|
+
// 红点 + openRemoteSettings/__dshRemoteNav)已整段移除,红点只保留在设置页栏目导航 navCell 上。
|
|
307
|
+
|
|
308
|
+
test("源码约束:侧栏注入按钮/导航点击机器已移除,官方「设置」不被抢、红点仅保留在设置页栏目导航", () => {
|
|
309
|
+
// 不再存在:侧栏 DOM 注入 / 导航容器点击 / 暴露给宿主与自动化的 open 入口
|
|
310
|
+
assert.doesNotMatch(SOURCE, /injectSidebarRemoteEntry/);
|
|
311
|
+
assert.doesNotMatch(SOURCE, /dru-nav-remote/);
|
|
312
|
+
assert.doesNotMatch(SOURCE, /dsh-remote-nav-seen/);
|
|
313
|
+
assert.doesNotMatch(SOURCE, /openRemoteSettings/);
|
|
314
|
+
assert.doesNotMatch(SOURCE, /clickNavToken/);
|
|
315
|
+
assert.doesNotMatch(SOURCE, /remoteSectionVisible/);
|
|
316
|
+
assert.doesNotMatch(SOURCE, /__dshRemoteNav/);
|
|
317
|
+
assert.doesNotMatch(SOURCE, /data-dru-remote/);
|
|
318
|
+
assert.doesNotMatch(SOURCE, /NAV_ENTRY_ID/);
|
|
319
|
+
assert.doesNotMatch(SOURCE, /NAV_CONTAINER_SEL/);
|
|
320
|
+
assert.doesNotMatch(SOURCE, /NAV_TEXT_MAX/);
|
|
321
|
+
assert.doesNotMatch(SOURCE, /dru-fab/);
|
|
322
|
+
// 官方按钮不被克隆 / 包装 / 挪位(克隆曾继承官方事件委托与热区 → Bug A)
|
|
323
|
+
assert.doesNotMatch(SOURCE, /cloneNode/);
|
|
324
|
+
// 唯一保留的 DOM 注入 = 设置页栏目导航红点(.dru-reddot,navCell 右上角,pointer-events:none)
|
|
325
|
+
assert.match(SOURCE, /dru-reddot/);
|
|
326
|
+
assert.match(SOURCE, /navCell/);
|
|
327
|
+
assert.match(SOURCE, /dsh-remote-seen-dot/);
|
|
397
328
|
});
|
|
398
329
|
|
|
399
330
|
test("源码约束:无侧边栏入口/浮动面板/切换账号;命名统一为「远程访问」;新增访问密钥/设备路由", () => {
|
|
@@ -413,13 +344,6 @@ test("源码约束:无侧边栏入口/浮动面板/切换账号;命名统一
|
|
|
413
344
|
assert.match(SOURCE, /dsh-remote\/access-key/);
|
|
414
345
|
assert.match(SOURCE, /dsh-remote\/mobile-sessions/);
|
|
415
346
|
assert.match(SOURCE, /order: 30/);
|
|
416
|
-
// 侧栏入口导航加固:aria-label/role 扫描、data-dru-remote 标记、排除注入项防递归、进设置后轮询
|
|
417
|
-
assert.match(SOURCE, /data-dru-remote/);
|
|
418
|
-
assert.match(SOURCE, /dru-nav-remote/);
|
|
419
|
-
assert.match(SOURCE, /aria-label/);
|
|
420
|
-
assert.match(SOURCE, /remoteSectionVisible/);
|
|
421
|
-
assert.match(SOURCE, /openRemoteSettings/);
|
|
422
|
-
assert.match(SOURCE, /__dshRemoteNav/);
|
|
423
347
|
// 清理时机:退出登录成功回调内、登录账号变化时、切换自建服务时
|
|
424
348
|
assert.match(SOURCE, /post\("\/dsh-remote\/logout"\)\.then\(function \(body\) \{[\s\S]*?fbClearThreads\(\);/);
|
|
425
349
|
assert.match(SOURCE, /if \(prevPhone !== phone\.trim\(\)\) fbClearThreads\(\);/);
|