@wenbin_wb/dsh-bridge 2.10.3 → 2.10.4

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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v2.10.4] - 2026-09-05
8
+
9
+ ### 🐞 修复
10
+
11
+ - **全新安装未设密码时远程访问不再被假锁屏挡住**:此前远程打开设置面板会强制要求输入"管理密码",而系统尚无任何密码可校验(任意输入都放行);现仅当已配置密码或策略为「仅限本机管理」时才上锁,无密码时直接进入面板并引导先设置访问密码(issue 反馈)。
12
+ - **修复 Telegram 代理在 Node ≥ 24 下不生效**(issue #32):自定义 `createConnection` 作为 `https.Agent` 构造参数会被 Node ≥24 静默忽略(回退到直连),导致需代理才能访问 api.telegram.org 的网络环境下网关永远连不上且无任何报错。改为构造后赋值实例属性,CONNECT 隧道在 Node 22/24 均生效。
13
+ - **修复移动端抽屉误关**(issue #31):点击 workspace 分组名(projectRow 同样带 `role="treeitem"`)会误触发会话行收起逻辑,抽屉在 ~100ms 后自动关闭。closest 选择器移除 `div[role="treeitem"]`,会话行收起、分组展开/收起的交互恢复正常(长按检测同步修复)。
14
+ - **自建隧道会话剥离支持 gzip 响应**(PR #29):DSH web 默认 gzip 压缩,此前 `session.list`/`session.history` 的 gzip 响应直接 `JSON.parse` 失败导致大字段剥离失效、完整巨响应原样传输;现先 gunzip 再剥离。
15
+ - **目录选择弹窗深色模式适配**(PR #29):`--dsw-alias-brand-primary` 在深色模式下为白色导致按钮/提示背景错乱,改用静态蓝并补齐深色变量覆盖。
16
+ - **WebSocket Ping/Pong keepalive**(PR #29):自建隧道裸 TCP 转发不再透传 Ping 帧,DSH 每 2s 发 Ping 收不到 Pong 会 terminate;现于帧层自动回 Pong 保持长连接。
17
+
18
+ ### ✨ 新功能
19
+
20
+ - **自建隧道 SSE 流式传输**(PR #29):隧道协议新增 `response-start/chunk/end` 流式分片,SSE 事件流不再因"永不 end"而超时截断;安装脚本同步支持。
21
+ - **远程/移动端禁用下拉刷新**(issue #30):注入 `overscroll-behavior-y: none`,避免移动浏览器下拉误触导致整页重载丢失会话与输入。
22
+
23
+ ---
24
+
7
25
  ## [v2.10.3] - 2026-09-03
8
26
 
9
27
  ### ✨ 新功能
package/client/client.js CHANGED
@@ -823,6 +823,38 @@ var MOBILE_STYLES_CSS = `
823
823
  to { transform: translateY(0); }
824
824
  }
825
825
 
826
+ /* \u6DF1\u8272\u6A21\u5F0F\u9002\u914D\uFF1Adsh-bridge \u76EE\u5F55\u6D4F\u89C8\u5668\u5F39\u7A97\u4F7F\u7528\u4E86 DSH \u4E3B\u9898\u7CFB\u7EDF\u672A\u5B9A\u4E49\u7684
827
+ state-*-bg / state-*-border / state-*-primary \u53D8\u91CF\uFF0C\u8865\u4E0A\u6DF1\u8272\u6A21\u5F0F\u503C\uFF0C\u907F\u514D\u6D45\u8272 fallback \u6C38\u8FDC\u751F\u6548 */
828
+ body[data-ds-dark-theme] {
829
+ --dsw-alias-state-info-bg: rgba(65, 118, 230, 0.12);
830
+ --dsw-alias-state-info-border: rgba(65, 118, 230, 0.25);
831
+ --dsw-alias-state-info-primary: #60a5fa;
832
+ --dsw-alias-state-success-bg: rgba(34, 197, 94, 0.12);
833
+ --dsw-alias-state-success-border: rgba(34, 197, 94, 0.25);
834
+ --dsw-alias-state-success-primary: #4ade80;
835
+ --dsw-alias-state-warn-bg: rgba(245, 158, 11, 0.12);
836
+ --dsw-alias-state-warn-border: rgba(245, 158, 11, 0.25);
837
+ --dsw-alias-state-warn-primary: #fbbf24;
838
+ --dsw-alias-state-error-bg: rgba(239, 68, 68, 0.12);
839
+ --dsw-alias-state-error-border: rgba(239, 68, 68, 0.25);
840
+ --dsw-alias-state-error-primary: #f87171;
841
+ }
842
+
843
+ /* \u6DF1\u8272\u6A21\u5F0F\uFF1A\u76F4\u63A5\u8986\u76D6\u5F39\u7A97\u5185\u6240\u6709\u4F7F\u7528 #fff/#ffffff fallback \u7684\u5185\u8054\u80CC\u666F\uFF0C
844
+ \u786E\u4FDD\u5373\u4F7F CSS \u53D8\u91CF\u672A\u6B63\u786E\u7EE7\u627F\uFF0C\u5F39\u7A97\u4E5F\u4E0D\u4F1A\u663E\u793A\u767D\u8272\u80CC\u666F */
845
+ body[data-ds-dark-theme] #dsh-remote-workspace-modal .dsh-ws-dialog-card,
846
+ body[data-ds-dark-theme] #dsh-remote-workspace-modal .dsh-ws-dialog-card * {
847
+ --dsw-alias-bg-layer-1: #1b1b1c;
848
+ --dsw-alias-bg-layer-2: #2c2c2e;
849
+ --dsw-alias-bg-layer-3: #353638;
850
+ --dsw-alias-border-l2: #3c3c3d;
851
+ --dsw-alias-label-primary: #f9fafb;
852
+ --dsw-alias-label-secondary: #adb2b8;
853
+ --dsw-alias-label-tertiary: #81858c;
854
+ --dsw-alias-brand-primary: #f9fafb;
855
+ --dsw-alias-label-primary-foreground: #0f1115;
856
+ }
857
+
826
858
  @media (min-width: 769px) {
827
859
  .dsh-mobile-app-header,
828
860
  .dsh-mobile-backdrop,
@@ -1786,11 +1818,18 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
1786
1818
  const handleToggleEnabled = async () => {
1787
1819
  const prev = enabled;
1788
1820
  const next = !enabled;
1821
+ const noPasswordYet = !auth?.hasPassword && !auth?.hasAdminPassword;
1822
+ if (next && noPasswordYet && mode !== "token_only") {
1823
+ const go = window.confirm(
1824
+ '\u26A0\uFE0F \u60A8\u5C1A\u672A\u8BBE\u7F6E\u4EFB\u4F55\u8BBF\u95EE\u5BC6\u7801\u6216\u7BA1\u7406\u5BC6\u7801\u3002\n\n\u5F00\u542F\u5B89\u5168\u9632\u62A4\u540E\uFF0C\u4EFB\u4F55\u77E5\u9053\u5C40\u57DF\u7F51 IP / \u96A7\u9053\u5730\u5740\u7684\u8BBF\u5BA2\u4ECD\u53EF\u76F4\u63A5\u8FDB\u5165\uFF08\u5F53\u524D\u76F8\u5F53\u4E8E"\u514D\u5BC6\u5F00\u653E"\u72B6\u6001\uFF09\u3002\n\n\u662F\u5426\u4ECD\u8981\u5F00\u542F\uFF1F\u5EFA\u8BAE\u5148\u5173\u95ED\uFF0C\u5728\u4E0B\u65B9\u300C\u8BBE\u7F6E\u5916\u90E8\u8BBF\u5BA2\u8BBF\u95EE\u5BC6\u7801\u300D\u5904\u8BBE\u7F6E\u5BC6\u7801\u540E\u518D\u5F00\u542F\u3002'
1825
+ );
1826
+ if (!go) return;
1827
+ }
1789
1828
  setEnabled(next);
1790
1829
  try {
1791
1830
  const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
1792
1831
  if (!res?.ok) throw new Error(res?.error?.message || "\u66F4\u65B0\u5931\u8D25");
1793
- setTopMsg({ ok: true, text: next ? "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5F00\u542F\uFF08\u73B0\u6709\u767B\u5F55\u6001\u5DF2\u5237\u65B0\uFF09" : "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5173\u95ED\uFF08\u8BBF\u95EE\u514D\u5BC6\uFF0C\u7BA1\u7406\u4FDD\u62A4\u4E0D\u53D7\u5F71\u54CD\uFF09" });
1832
+ setTopMsg({ ok: true, text: next ? noPasswordYet ? "\u2713 \u5B89\u5168\u9632\u62A4\u5DF2\u5F00\u542F\uFF08\u6CE8\u610F\uFF1A\u5C1A\u672A\u8BBE\u7F6E\u5BC6\u7801\uFF0C\u8BBF\u5BA2\u4ECD\u53EF\u514D\u5BC6\u8FDB\u5165\uFF0C\u8BF7\u7ACB\u5373\u5728\u4E0B\u65B9\u8BBE\u7F6E\u8BBF\u95EE\u5BC6\u7801\uFF09" : "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5F00\u542F\uFF08\u73B0\u6709\u767B\u5F55\u6001\u5DF2\u5237\u65B0\uFF09" : "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5173\u95ED\uFF08\u8BBF\u95EE\u514D\u5BC6\uFF0C\u7BA1\u7406\u4FDD\u62A4\u4E0D\u53D7\u5F71\u54CD\uFF09" });
1794
1833
  onUpdate?.();
1795
1834
  } catch (e) {
1796
1835
  setEnabled(prev);
@@ -4196,7 +4235,8 @@ function BridgePanel({ rpcCall }) {
4196
4235
  }
4197
4236
  const auth = status?.auth;
4198
4237
  const policy = auth?.adminPolicy ?? "password_unlock";
4199
- const isLocked = !isLocalhost && auth?.adminProtection !== false && policy !== "open" && !adminUnlocked;
4238
+ const hasAnyPassword = !!(auth?.hasPassword || auth?.hasAdminPassword);
4239
+ const isLocked = !isLocalhost && auth?.adminProtection !== false && policy !== "open" && !adminUnlocked && (policy === "local_only" || hasAnyPassword);
4200
4240
  if (isLocked) {
4201
4241
  return React.createElement(
4202
4242
  "div",
@@ -4367,7 +4407,7 @@ function BridgePanel({ rpcCall }) {
4367
4407
  onClick: handleLockAdmin
4368
4408
  }, "\u{1F512} \u91CD\u65B0\u9501\u5B9A\u540E\u53F0")
4369
4409
  ),
4370
- // 未解锁时的顶部引导条
4410
+ // 未解锁时的顶部引导条:未设密码 → 提示先设密码;已设密码 → 提示解锁
4371
4411
  !isLocalhost && !adminUnlocked && auth?.enabled && policy !== "open" && React.createElement(
4372
4412
  "div",
4373
4413
  {
@@ -4384,12 +4424,20 @@ function BridgePanel({ rpcCall }) {
4384
4424
  color: "var(--dsw-alias-state-warn-primary,#92400e)"
4385
4425
  }
4386
4426
  },
4387
- React.createElement("span", null, "\u{1F512} \u540E\u53F0\u7BA1\u7406\u6743\u9650\u672A\u89E3\u9501\uFF08\u4FEE\u6539\u654F\u611F\u914D\u7F6E\u9700\u5148\u89E3\u9501\uFF09"),
4388
- React.createElement("button", {
4427
+ React.createElement(
4428
+ "span",
4429
+ null,
4430
+ hasAnyPassword ? "\u{1F512} \u540E\u53F0\u7BA1\u7406\u6743\u9650\u672A\u89E3\u9501\uFF08\u4FEE\u6539\u654F\u611F\u914D\u7F6E\u9700\u5148\u89E3\u9501\uFF09" : "\u26A0\uFE0F \u5C1A\u672A\u8BBE\u7F6E\u4EFB\u4F55\u8BBF\u95EE\u5BC6\u7801 / \u7BA1\u7406\u5BC6\u7801\uFF0C\u8FDC\u7A0B\u8BBF\u5BA2\u53EF\u514D\u5BC6\u8FDB\u5165\uFF01"
4431
+ ),
4432
+ hasAnyPassword ? React.createElement("button", {
4389
4433
  type: "button",
4390
4434
  style: { ...s.btnPri, height: 24, fontSize: 11, padding: "0 10px", background: "#d97706" },
4391
4435
  onClick: () => setShowUnlockModal(true)
4392
- }, "\u{1F511} \u89E3\u9501\u7BA1\u7406\u6743\u9650")
4436
+ }, "\u{1F511} \u89E3\u9501\u7BA1\u7406\u6743\u9650") : React.createElement("button", {
4437
+ type: "button",
4438
+ style: { ...s.btnPri, height: 24, fontSize: 11, padding: "0 10px", background: "#d97706" },
4439
+ onClick: () => setActiveTab("security")
4440
+ }, "\u{1F510} \u7ACB\u5373\u8BBE\u7F6E\u5BC6\u7801")
4393
4441
  ),
4394
4442
  React.createElement(VersionBanner, { rpcCall: authRpcCall }),
4395
4443
  React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
@@ -4653,7 +4701,7 @@ function setupMobileExperience(rpcCall, ctx) {
4653
4701
  return;
4654
4702
  }
4655
4703
  }
4656
- const sessionRow = e.target.closest('a, div[class*="sessionRow"], div[role="treeitem"]');
4704
+ const sessionRow = e.target.closest('a, div[class*="sessionRow"]');
4657
4705
  if (sessionRow) {
4658
4706
  setTimeout(() => {
4659
4707
  if (document.body.classList.contains("dsh-drawer-open")) {
@@ -4676,7 +4724,7 @@ function setupMobileExperience(rpcCall, ctx) {
4676
4724
  if (!document.body.classList.contains("dsh-drawer-open")) return;
4677
4725
  const sidebar = document.querySelector('div[class*="_sidebarCol"]');
4678
4726
  if (!sidebar || !sidebar.contains(e.target)) return;
4679
- const sessionRow = e.target.closest('div[class*="sessionRow"], div[role="treeitem"]');
4727
+ const sessionRow = e.target.closest('div[class*="sessionRow"]');
4680
4728
  if (!sessionRow) return;
4681
4729
  longPressTimer = setTimeout(() => {
4682
4730
  lastLongPressTime = Date.now();
@@ -4829,7 +4877,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4829
4877
  <form id="dsh-ws-unlock-form" style="display: flex; gap: 8px;">
4830
4878
  <input id="dsh-ws-unlock-input" type="password" placeholder="\u8BF7\u8F93\u5165\u540E\u53F0\u7BA1\u7406\u5BC6\u7801" value="${escapeHtml(unlockInput)}"
4831
4879
  style="flex: 1; font: inherit; font-size: 13px; padding: 7px 10px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, currentColor); outline: none; box-sizing: border-box;" />
4832
- <button type="submit" style="border: none; background: var(--dsw-alias-brand-primary, #4f6ef7); color: #fff; border-radius: 8px; padding: 0 14px; font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0;" ${unlocking ? "disabled" : ""}>${unlocking ? "\u89E3\u9501\u4E2D\u2026" : "\u89E3\u9501"}</button>
4880
+ <button type="submit" style="border: none; background: var(--dsw-static-blue-600, #4f6ef7); color: #fff; border-radius: 8px; padding: 0 14px; font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0;" ${unlocking ? "disabled" : ""}>${unlocking ? "\u89E3\u9501\u4E2D\u2026" : "\u89E3\u9501"}</button>
4833
4881
  </form>
4834
4882
  ${unlockErr ? `<div style="font-size: 11px; color: var(--dsw-alias-state-error-primary, #dc2626); margin-top: 6px;">${escapeHtml(unlockErr)}</div>` : ""}
4835
4883
  </div>
@@ -4849,7 +4897,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4849
4897
  ${(drives || []).map((d) => {
4850
4898
  const isActive = currentPath.startsWith(d.path) || currentPath === d.path;
4851
4899
  return `
4852
- <button class="dsh-ws-quick-btn" data-path="${escapeHtml(d.path)}" style="border: 1px solid ${isActive ? "var(--dsw-alias-brand-primary, #4f6ef7)" : "var(--dsw-alias-border-l2, #d1d5db)"}; background: ${isActive ? "var(--dsw-alias-brand-primary, #4f6ef7)" : "var(--dsw-alias-bg-layer-2, #f9fafb)"}; color: ${isActive ? "#fff" : "var(--dsw-alias-label-primary, #111827)"}; border-radius: 14px; padding: 4px 10px; font-size: 11px; cursor: pointer; font-weight: 500; flex-shrink: 0; transition: all 0.1s;">
4900
+ <button class="dsh-ws-quick-btn" data-path="${escapeHtml(d.path)}" style="border: 1px solid ${isActive ? "var(--dsw-static-blue-600, #4f6ef7)" : "var(--dsw-alias-border-l2, #d1d5db)"}; background: ${isActive ? "var(--dsw-static-blue-600, #4f6ef7)" : "var(--dsw-alias-bg-layer-2, #f9fafb)"}; color: ${isActive ? "#fff" : "var(--dsw-alias-label-primary, #111827)"}; border-radius: 14px; padding: 4px 10px; font-size: 11px; cursor: pointer; font-weight: 500; flex-shrink: 0; transition: all 0.1s;">
4853
4901
  \u{1F4BE} ${escapeHtml(d.name)}
4854
4902
  </button>
4855
4903
  `;
@@ -4896,9 +4944,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4896
4944
  <div style="background: var(--dsw-alias-state-info-bg, #eff6ff); border: 1px solid var(--dsw-alias-state-info-border, #bfdbfe); border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px;">
4897
4945
  <div style="display: flex; align-items: center; justify-content: space-between; gap: 6px;">
4898
4946
  <span style="font-size: 11px; font-weight: 600; color: var(--dsw-alias-brand-primary, #2563eb); flex-shrink: 0;">\u5F53\u524D\u76EE\u5F55:</span>
4899
- <span style="font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--dsw-alias-label-primary, #1e3a8a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; font-weight: 600;">${escapeHtml(currentPath)}</span>
4947
+ <span style="font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--dsw-alias-label-primary, var(--dsw-alias-brand-primary, #1e3a8a)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; font-weight: 600;">${escapeHtml(currentPath)}</span>
4900
4948
  </div>
4901
- <button id="dsh-ws-add-current-btn" style="border: none; background: var(--dsw-alias-brand-primary, #2563eb); color: #fff; height: 36px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; box-shadow: 0 2px 4px rgba(37,99,235,0.25); transition: opacity 0.1s;" ${isSubmitting ? "disabled" : ""}>
4949
+ <button id="dsh-ws-add-current-btn" style="border: none; background: var(--dsw-static-blue-600, #2563eb); color: #fff; height: 36px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; box-shadow: 0 2px 4px rgba(37,99,235,0.25); transition: opacity 0.1s;" ${isSubmitting ? "disabled" : ""}>
4902
4950
  ${isSubmitting ? "\u6B63\u5728\u6DFB\u52A0\u5E76\u5207\u6362\u2026" : "\u{1F449} \u8BBE\u4E3A\u5F53\u524D\u5DE5\u4F5C\u533A\u5E76\u8FDB\u5165"}
4903
4951
  </button>
4904
4952
  </div>
@@ -4957,7 +5005,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4957
5005
  <button id="dsh-ws-manual-jump-btn" style="border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-2, #f9fafb); color: var(--dsw-alias-label-primary, #111827); padding: 0 10px; border-radius: 8px; font-size: 11px; cursor: pointer; white-space: nowrap;">
4958
5006
  \u524D\u5F80
4959
5007
  </button>
4960
- <button id="dsh-ws-manual-add-btn" style="border: none; background: var(--dsw-alias-brand-primary, #4f6ef7); color: #fff; padding: 0 12px; border-radius: 8px; font-size: 11px; font-weight: 500; cursor: pointer; white-space: nowrap;">
5008
+ <button id="dsh-ws-manual-add-btn" style="border: none; background: var(--dsw-static-blue-600, #4f6ef7); color: #fff; padding: 0 12px; border-radius: 8px; font-size: 11px; font-weight: 500; cursor: pointer; white-space: nowrap;">
4961
5009
  \u6DFB\u52A0\u5E76\u8FDB\u5165
4962
5010
  </button>
4963
5011
  </div>
package/client/index.js CHANGED
@@ -758,11 +758,23 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
758
758
  const handleToggleEnabled = async () => {
759
759
  const prev = enabled;
760
760
  const next = !enabled;
761
+ // 开启安全防护但尚未设置任何密码:明确提示当前是"裸奔"状态,避免用户误以为已受密码保护
762
+ const noPasswordYet = !auth?.hasPassword && !auth?.hasAdminPassword;
763
+ if (next && noPasswordYet && mode !== 'token_only') {
764
+ const go = window.confirm(
765
+ '⚠️ 您尚未设置任何访问密码或管理密码。\n\n开启安全防护后,任何知道局域网 IP / 隧道地址的访客仍可直接进入(当前相当于"免密开放"状态)。\n\n是否仍要开启?建议先关闭,在下方「设置外部访客访问密码」处设置密码后再开启。'
766
+ );
767
+ if (!go) return;
768
+ }
761
769
  setEnabled(next);
762
770
  try {
763
771
  const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
764
772
  if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
765
- setTopMsg({ ok: true, text: next ? '✓ 访问安全认证已开启(现有登录态已刷新)' : '✓ 访问安全认证已关闭(访问免密,管理保护不受影响)' });
773
+ setTopMsg({ ok: true, text: next
774
+ ? (noPasswordYet
775
+ ? '✓ 安全防护已开启(注意:尚未设置密码,访客仍可免密进入,请立即在下方设置访问密码)'
776
+ : '✓ 访问安全认证已开启(现有登录态已刷新)')
777
+ : '✓ 访问安全认证已关闭(访问免密,管理保护不受影响)' });
766
778
  onUpdate?.();
767
779
  } catch (e) {
768
780
  setEnabled(prev);
@@ -2961,11 +2973,18 @@ function BridgePanel({ rpcCall }) {
2961
2973
 
2962
2974
  const auth = status?.auth;
2963
2975
  const policy = auth?.adminPolicy ?? 'password_unlock';
2976
+ // 系统是否已配置任何密码(访客访问密码 或 独立管理密码)。
2977
+ const hasAnyPassword = !!(auth?.hasPassword || auth?.hasAdminPassword);
2964
2978
  // 锁屏条件:远程 + 管理保护开启(adminProtection)+ 未解锁 + 非宽松策略。
2965
2979
  // 不依赖 auth.enabled:即使访问认证关闭,管理保护仍独立生效,上锁后必须显示锁屏。
2966
2980
  // local_only 也锁定(显示"仅限本机管理"专属锁屏)。
2981
+ // 关键修正:仅当系统【已配置密码】时才需要输入密码解锁——全新安装(无任何密码)时
2982
+ // 服务端没有可校验的哈希,任何输入都会被放行(宽松降级),此时展示"输入管理密码"
2983
+ // 锁屏只会误导用户输一个假密码;应直接放行进入面板并提示先设置密码。
2984
+ // local_only 例外:无论是否已设密码,远程一律禁止管理(策略语义高于空密码豁免)。
2967
2985
  const isLocked = !isLocalhost && auth?.adminProtection !== false
2968
- && policy !== 'open' && !adminUnlocked;
2986
+ && policy !== 'open' && !adminUnlocked
2987
+ && (policy === 'local_only' || hasAnyPassword);
2969
2988
 
2970
2989
  // 远程设备被锁定:全局展示锁定页面,阻断所有 Tab 的查看与操作
2971
2990
  if (isLocked) {
@@ -3098,7 +3117,7 @@ function BridgePanel({ rpcCall }) {
3098
3117
  }, '🔒 重新锁定后台'),
3099
3118
  ),
3100
3119
 
3101
- // 未解锁时的顶部引导条
3120
+ // 未解锁时的顶部引导条:未设密码 → 提示先设密码;已设密码 → 提示解锁
3102
3121
  !isLocalhost && !adminUnlocked && auth?.enabled && policy !== 'open' && React.createElement('div', {
3103
3122
  style: {
3104
3123
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
@@ -3107,12 +3126,20 @@ function BridgePanel({ rpcCall }) {
3107
3126
  marginBottom: 14, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#92400e)',
3108
3127
  },
3109
3128
  },
3110
- React.createElement('span', null, '🔒 后台管理权限未解锁(修改敏感配置需先解锁)'),
3111
- React.createElement('button', {
3129
+ React.createElement('span', null,
3130
+ hasAnyPassword
3131
+ ? '🔒 后台管理权限未解锁(修改敏感配置需先解锁)'
3132
+ : '⚠️ 尚未设置任何访问密码 / 管理密码,远程访客可免密进入!'
3133
+ ),
3134
+ hasAnyPassword ? React.createElement('button', {
3112
3135
  type: 'button',
3113
3136
  style: { ...s.btnPri, height: 24, fontSize: 11, padding: '0 10px', background: '#d97706' },
3114
3137
  onClick: () => setShowUnlockModal(true),
3115
- }, '🔑 解锁管理权限'),
3138
+ }, '🔑 解锁管理权限') : React.createElement('button', {
3139
+ type: 'button',
3140
+ style: { ...s.btnPri, height: 24, fontSize: 11, padding: '0 10px', background: '#d97706' },
3141
+ onClick: () => setActiveTab('security'),
3142
+ }, '🔐 立即设置密码'),
3116
3143
  ),
3117
3144
 
3118
3145
  React.createElement(VersionBanner, { rpcCall: authRpcCall }),
@@ -3404,8 +3431,11 @@ function setupMobileExperience(rpcCall, ctx) {
3404
3431
  }
3405
3432
  }
3406
3433
 
3407
- // 点击会话项后平滑收起抽屉
3408
- const sessionRow = e.target.closest('a, div[class*="sessionRow"], div[role="treeitem"]');
3434
+ // 点击会话项后平滑收起抽屉。
3435
+ // 注意:不能匹配 div[role="treeitem"] —— DSH 的 workspace 分组行(projectRow)
3436
+ // 同样带 role="treeitem",会误伤"点击分组名展开/收起"的手势(issue #31)。
3437
+ // 会话行已被 div[class*="sessionRow"] 覆盖;搜索结果行是 <button>,不匹配 div。
3438
+ const sessionRow = e.target.closest('a, div[class*="sessionRow"]');
3409
3439
  if (sessionRow) {
3410
3440
  setTimeout(() => {
3411
3441
  if (document.body.classList.contains('dsh-drawer-open')) {
@@ -3434,7 +3464,8 @@ function setupMobileExperience(rpcCall, ctx) {
3434
3464
  const sidebar = document.querySelector('div[class*="_sidebarCol"]');
3435
3465
  if (!sidebar || !sidebar.contains(e.target)) return;
3436
3466
 
3437
- const sessionRow = e.target.closest('div[class*="sessionRow"], div[role="treeitem"]');
3467
+ // 长按会话项呼出操作菜单。同样不能匹配 div[role="treeitem"](分组行同标签,issue #31)。
3468
+ const sessionRow = e.target.closest('div[class*="sessionRow"]');
3438
3469
  if (!sessionRow) return;
3439
3470
 
3440
3471
  longPressTimer = setTimeout(() => {
@@ -3633,7 +3664,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
3633
3664
  <form id="dsh-ws-unlock-form" style="display: flex; gap: 8px;">
3634
3665
  <input id="dsh-ws-unlock-input" type="password" placeholder="请输入后台管理密码" value="${escapeHtml(unlockInput)}"
3635
3666
  style="flex: 1; font: inherit; font-size: 13px; padding: 7px 10px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, currentColor); outline: none; box-sizing: border-box;" />
3636
- <button type="submit" style="border: none; background: var(--dsw-alias-brand-primary, #4f6ef7); color: #fff; border-radius: 8px; padding: 0 14px; font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0;" ${unlocking ? 'disabled' : ''}>${unlocking ? '解锁中…' : '解锁'}</button>
3667
+ <button type="submit" style="border: none; background: var(--dsw-static-blue-600, #4f6ef7); color: #fff; border-radius: 8px; padding: 0 14px; font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0;" ${unlocking ? 'disabled' : ''}>${unlocking ? '解锁中…' : '解锁'}</button>
3637
3668
  </form>
3638
3669
  ${unlockErr ? `<div style="font-size: 11px; color: var(--dsw-alias-state-error-primary, #dc2626); margin-top: 6px;">${escapeHtml(unlockErr)}</div>` : ''}
3639
3670
  </div>
@@ -3653,7 +3684,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
3653
3684
  ${(drives || []).map(d => {
3654
3685
  const isActive = currentPath.startsWith(d.path) || currentPath === d.path;
3655
3686
  return `
3656
- <button class="dsh-ws-quick-btn" data-path="${escapeHtml(d.path)}" style="border: 1px solid ${isActive ? 'var(--dsw-alias-brand-primary, #4f6ef7)' : 'var(--dsw-alias-border-l2, #d1d5db)'}; background: ${isActive ? 'var(--dsw-alias-brand-primary, #4f6ef7)' : 'var(--dsw-alias-bg-layer-2, #f9fafb)'}; color: ${isActive ? '#fff' : 'var(--dsw-alias-label-primary, #111827)'}; border-radius: 14px; padding: 4px 10px; font-size: 11px; cursor: pointer; font-weight: 500; flex-shrink: 0; transition: all 0.1s;">
3687
+ <button class="dsh-ws-quick-btn" data-path="${escapeHtml(d.path)}" style="border: 1px solid ${isActive ? 'var(--dsw-static-blue-600, #4f6ef7)' : 'var(--dsw-alias-border-l2, #d1d5db)'}; background: ${isActive ? 'var(--dsw-static-blue-600, #4f6ef7)' : 'var(--dsw-alias-bg-layer-2, #f9fafb)'}; color: ${isActive ? '#fff' : 'var(--dsw-alias-label-primary, #111827)'}; border-radius: 14px; padding: 4px 10px; font-size: 11px; cursor: pointer; font-weight: 500; flex-shrink: 0; transition: all 0.1s;">
3657
3688
  💾 ${escapeHtml(d.name)}
3658
3689
  </button>
3659
3690
  `;
@@ -3700,9 +3731,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
3700
3731
  <div style="background: var(--dsw-alias-state-info-bg, #eff6ff); border: 1px solid var(--dsw-alias-state-info-border, #bfdbfe); border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px;">
3701
3732
  <div style="display: flex; align-items: center; justify-content: space-between; gap: 6px;">
3702
3733
  <span style="font-size: 11px; font-weight: 600; color: var(--dsw-alias-brand-primary, #2563eb); flex-shrink: 0;">当前目录:</span>
3703
- <span style="font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--dsw-alias-label-primary, #1e3a8a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; font-weight: 600;">${escapeHtml(currentPath)}</span>
3734
+ <span style="font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--dsw-alias-label-primary, var(--dsw-alias-brand-primary, #1e3a8a)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; font-weight: 600;">${escapeHtml(currentPath)}</span>
3704
3735
  </div>
3705
- <button id="dsh-ws-add-current-btn" style="border: none; background: var(--dsw-alias-brand-primary, #2563eb); color: #fff; height: 36px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; box-shadow: 0 2px 4px rgba(37,99,235,0.25); transition: opacity 0.1s;" ${isSubmitting ? 'disabled' : ''}>
3736
+ <button id="dsh-ws-add-current-btn" style="border: none; background: var(--dsw-static-blue-600, #2563eb); color: #fff; height: 36px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; box-shadow: 0 2px 4px rgba(37,99,235,0.25); transition: opacity 0.1s;" ${isSubmitting ? 'disabled' : ''}>
3706
3737
  ${isSubmitting ? '正在添加并切换…' : '👉 设为当前工作区并进入'}
3707
3738
  </button>
3708
3739
  </div>
@@ -3761,7 +3792,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
3761
3792
  <button id="dsh-ws-manual-jump-btn" style="border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-2, #f9fafb); color: var(--dsw-alias-label-primary, #111827); padding: 0 10px; border-radius: 8px; font-size: 11px; cursor: pointer; white-space: nowrap;">
3762
3793
  前往
3763
3794
  </button>
3764
- <button id="dsh-ws-manual-add-btn" style="border: none; background: var(--dsw-alias-brand-primary, #4f6ef7); color: #fff; padding: 0 12px; border-radius: 8px; font-size: 11px; font-weight: 500; cursor: pointer; white-space: nowrap;">
3795
+ <button id="dsh-ws-manual-add-btn" style="border: none; background: var(--dsw-static-blue-600, #4f6ef7); color: #fff; padding: 0 12px; border-radius: 8px; font-size: 11px; font-weight: 500; cursor: pointer; white-space: nowrap;">
3765
3796
  添加并进入
3766
3797
  </button>
3767
3798
  </div>