@wenbin_wb/dsh-bridge 2.10.3 → 2.10.5
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 +28 -0
- package/client/client.js +75 -14
- package/client/index.js +62 -15
- package/client/mobile-styles.js +834 -802
- package/lib/auth/login-template.js +417 -382
- package/lib/auth/manager.js +5 -4
- package/lib/bridge-rpc.js +21 -2
- package/lib/index.js +22 -5
- package/lib/session-strip.js +14 -4
- package/lib/telegram/gateway.js +28 -26
- package/lib/tunnel-client.mjs +123 -7
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v2.10.5] - 2026-09-06
|
|
8
|
+
|
|
9
|
+
### 🐞 修复
|
|
10
|
+
|
|
11
|
+
- **修复「仅密码登录」模式自我锁死**:`password_only` 模式在尚未设置任何密码时,服务端会拒绝一切登录(防空白登录),而管理员因进不去面板也无法设置密码 → 永久死锁。现未设密码时放行(与「扫码+密码」默认模式一致——没有可校验的哈希即无秘密可保护),让管理员能进入面板完成密码初始化;设置密码后门禁才真正生效(错误密码会被拒绝)。
|
|
12
|
+
- **修复无密码时找不到设置密码入口**:此前"外部访问门禁"卡片(含访客访问密码输入框)仅在安全防护已开启时渲染;未开启防护时面板没有密码输入框,用户先开防护再想设密码就会被卡。现只要系统**尚未设置任何密码**,密码设置区就始终显示。
|
|
13
|
+
- **切换「仅密码」模式 / 开启防护的引导**:UI 在切换到 `password_only` 或开启防护时若尚未设密码,会就地弹窗引导先设置访问密码;服务端 `authUpdateConfig` 同步增加守卫——无密码时禁止开启 `password_only` 防护(除非同请求携带新密码),双保险杜绝再次自我锁死。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [v2.10.4] - 2026-09-05
|
|
18
|
+
|
|
19
|
+
### 🐞 修复
|
|
20
|
+
|
|
21
|
+
- **全新安装未设密码时远程访问不再被假锁屏挡住**:此前远程打开设置面板会强制要求输入"管理密码",而系统尚无任何密码可校验(任意输入都放行);现仅当已配置密码或策略为「仅限本机管理」时才上锁,无密码时直接进入面板并引导先设置访问密码(issue 反馈)。
|
|
22
|
+
- **修复 Telegram 代理在 Node ≥ 24 下不生效**(issue #32):自定义 `createConnection` 作为 `https.Agent` 构造参数会被 Node ≥24 静默忽略(回退到直连),导致需代理才能访问 api.telegram.org 的网络环境下网关永远连不上且无任何报错。改为构造后赋值实例属性,CONNECT 隧道在 Node 22/24 均生效。
|
|
23
|
+
- **修复移动端抽屉误关**(issue #31):点击 workspace 分组名(projectRow 同样带 `role="treeitem"`)会误触发会话行收起逻辑,抽屉在 ~100ms 后自动关闭。closest 选择器移除 `div[role="treeitem"]`,会话行收起、分组展开/收起的交互恢复正常(长按检测同步修复)。
|
|
24
|
+
- **自建隧道会话剥离支持 gzip 响应**(PR #29):DSH web 默认 gzip 压缩,此前 `session.list`/`session.history` 的 gzip 响应直接 `JSON.parse` 失败导致大字段剥离失效、完整巨响应原样传输;现先 gunzip 再剥离。
|
|
25
|
+
- **目录选择弹窗深色模式适配**(PR #29):`--dsw-alias-brand-primary` 在深色模式下为白色导致按钮/提示背景错乱,改用静态蓝并补齐深色变量覆盖。
|
|
26
|
+
- **WebSocket Ping/Pong keepalive**(PR #29):自建隧道裸 TCP 转发不再透传 Ping 帧,DSH 每 2s 发 Ping 收不到 Pong 会 terminate;现于帧层自动回 Pong 保持长连接。
|
|
27
|
+
|
|
28
|
+
### ✨ 新功能
|
|
29
|
+
|
|
30
|
+
- **自建隧道 SSE 流式传输**(PR #29):隧道协议新增 `response-start/chunk/end` 流式分片,SSE 事件流不再因"永不 end"而超时截断;安装脚本同步支持。
|
|
31
|
+
- **远程/移动端禁用下拉刷新**(issue #30):注入 `overscroll-behavior-y: none`,避免移动浏览器下拉误触导致整页重载丢失会话与输入。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
7
35
|
## [v2.10.3] - 2026-09-03
|
|
8
36
|
|
|
9
37
|
### ✨ 新功能
|
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,23 @@ 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
|
+
if (mode === "password_only") {
|
|
1824
|
+
window.alert("\u300C\u4EC5\u5BC6\u7801 / PIN \u7801\u767B\u5F55\u300D\u6A21\u5F0F\u5FC5\u987B\u5148\u8BBE\u7F6E\u8BBF\u95EE\u5BC6\u7801\u624D\u80FD\u5F00\u542F\u5B89\u5168\u9632\u62A4\u3002\n\n\u8BF7\u5148\u5728\u4E0B\u65B9\u300C\u8BBE\u7F6E\u5916\u90E8\u8BBF\u5BA2\u8BBF\u95EE\u5BC6\u7801 / PIN \u7801\u300D\u8F93\u5165\u5BC6\u7801\u5E76\u70B9\u51FB\u300C\u4FDD\u5B58\u8BBF\u95EE\u5BC6\u7801\u300D\uFF0C\u7136\u540E\u518D\u5F00\u542F\u3002");
|
|
1825
|
+
setTopMsg({ ok: false, text: "\u8BF7\u5148\u8BBE\u7F6E\u8BBF\u5BA2\u8BBF\u95EE\u5BC6\u7801\uFF0C\u518D\u5F00\u542F\u5B89\u5168\u9632\u62A4" });
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const go = window.confirm(
|
|
1829
|
+
'\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'
|
|
1830
|
+
);
|
|
1831
|
+
if (!go) return;
|
|
1832
|
+
}
|
|
1789
1833
|
setEnabled(next);
|
|
1790
1834
|
try {
|
|
1791
1835
|
const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
|
|
1792
1836
|
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" });
|
|
1837
|
+
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
1838
|
onUpdate?.();
|
|
1795
1839
|
} catch (e) {
|
|
1796
1840
|
setEnabled(prev);
|
|
@@ -1812,6 +1856,11 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
|
|
|
1812
1856
|
}
|
|
1813
1857
|
};
|
|
1814
1858
|
const handleChangeMode = async (m) => {
|
|
1859
|
+
if (m === "password_only" && !auth?.hasPassword && !auth?.hasAdminPassword) {
|
|
1860
|
+
window.alert("\u300C\u4EC5\u5BC6\u7801 / PIN \u7801\u767B\u5F55\u300D\u9700\u8981\u5148\u8BBE\u7F6E\u8BBF\u95EE\u5BC6\u7801\u3002\n\n\u8BF7\u5728\u4E0B\u65B9\u300C\u8BBE\u7F6E\u5916\u90E8\u8BBF\u5BA2\u8BBF\u95EE\u5BC6\u7801 / PIN \u7801\u300D\u5904\u8F93\u5165\u5BC6\u7801\u5E76\u70B9\u51FB\u300C\u4FDD\u5B58\u8BBF\u95EE\u5BC6\u7801\u300D\uFF0C\u7136\u540E\u518D\u5207\u6362\u5230\u6B64\u6A21\u5F0F\u6216\u5F00\u542F\u5B89\u5168\u9632\u62A4\u3002");
|
|
1861
|
+
setTopMsg({ ok: false, text: "\u8BF7\u5148\u5728\u4E0B\u65B9\u8BBE\u7F6E\u8BBF\u5BA2\u8BBF\u95EE\u5BC6\u7801\uFF0C\u518D\u5207\u6362\u4E3A\u300C\u4EC5\u5BC6\u7801\u767B\u5F55\u300D\u6A21\u5F0F" });
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1815
1864
|
const prev = mode;
|
|
1816
1865
|
setMode(m);
|
|
1817
1866
|
try {
|
|
@@ -2025,7 +2074,10 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
|
|
|
2025
2074
|
// =========================================================================
|
|
2026
2075
|
// ---- 第一道防线:外部访问门禁(控制谁能进入 Web 界面使用 AI) ----
|
|
2027
2076
|
// =========================================================================
|
|
2028
|
-
|
|
2077
|
+
// 第一道防线卡片:已开启防护,或【尚未设置任何密码】时始终显示——
|
|
2078
|
+
// 未设密码时必须给出密码输入框,否则用户开启防护(尤其 password_only)后
|
|
2079
|
+
// 会因无密码被锁在登录墙外且找不到设密码入口(自我锁死,v2.10.5 修复)。
|
|
2080
|
+
(enabled || !auth?.hasPassword) && React.createElement(
|
|
2029
2081
|
"div",
|
|
2030
2082
|
{ style: s.card },
|
|
2031
2083
|
React.createElement(
|
|
@@ -4196,7 +4248,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
4196
4248
|
}
|
|
4197
4249
|
const auth = status?.auth;
|
|
4198
4250
|
const policy = auth?.adminPolicy ?? "password_unlock";
|
|
4199
|
-
const
|
|
4251
|
+
const hasAnyPassword = !!(auth?.hasPassword || auth?.hasAdminPassword);
|
|
4252
|
+
const isLocked = !isLocalhost && auth?.adminProtection !== false && policy !== "open" && !adminUnlocked && (policy === "local_only" || hasAnyPassword);
|
|
4200
4253
|
if (isLocked) {
|
|
4201
4254
|
return React.createElement(
|
|
4202
4255
|
"div",
|
|
@@ -4367,7 +4420,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
4367
4420
|
onClick: handleLockAdmin
|
|
4368
4421
|
}, "\u{1F512} \u91CD\u65B0\u9501\u5B9A\u540E\u53F0")
|
|
4369
4422
|
),
|
|
4370
|
-
//
|
|
4423
|
+
// 未解锁时的顶部引导条:未设密码 → 提示先设密码;已设密码 → 提示解锁
|
|
4371
4424
|
!isLocalhost && !adminUnlocked && auth?.enabled && policy !== "open" && React.createElement(
|
|
4372
4425
|
"div",
|
|
4373
4426
|
{
|
|
@@ -4384,12 +4437,20 @@ function BridgePanel({ rpcCall }) {
|
|
|
4384
4437
|
color: "var(--dsw-alias-state-warn-primary,#92400e)"
|
|
4385
4438
|
}
|
|
4386
4439
|
},
|
|
4387
|
-
React.createElement(
|
|
4388
|
-
|
|
4440
|
+
React.createElement(
|
|
4441
|
+
"span",
|
|
4442
|
+
null,
|
|
4443
|
+
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"
|
|
4444
|
+
),
|
|
4445
|
+
hasAnyPassword ? React.createElement("button", {
|
|
4389
4446
|
type: "button",
|
|
4390
4447
|
style: { ...s.btnPri, height: 24, fontSize: 11, padding: "0 10px", background: "#d97706" },
|
|
4391
4448
|
onClick: () => setShowUnlockModal(true)
|
|
4392
|
-
}, "\u{1F511} \u89E3\u9501\u7BA1\u7406\u6743\u9650")
|
|
4449
|
+
}, "\u{1F511} \u89E3\u9501\u7BA1\u7406\u6743\u9650") : React.createElement("button", {
|
|
4450
|
+
type: "button",
|
|
4451
|
+
style: { ...s.btnPri, height: 24, fontSize: 11, padding: "0 10px", background: "#d97706" },
|
|
4452
|
+
onClick: () => setActiveTab("security")
|
|
4453
|
+
}, "\u{1F510} \u7ACB\u5373\u8BBE\u7F6E\u5BC6\u7801")
|
|
4393
4454
|
),
|
|
4394
4455
|
React.createElement(VersionBanner, { rpcCall: authRpcCall }),
|
|
4395
4456
|
React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
|
|
@@ -4653,7 +4714,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
4653
4714
|
return;
|
|
4654
4715
|
}
|
|
4655
4716
|
}
|
|
4656
|
-
const sessionRow = e.target.closest('a, div[class*="sessionRow"]
|
|
4717
|
+
const sessionRow = e.target.closest('a, div[class*="sessionRow"]');
|
|
4657
4718
|
if (sessionRow) {
|
|
4658
4719
|
setTimeout(() => {
|
|
4659
4720
|
if (document.body.classList.contains("dsh-drawer-open")) {
|
|
@@ -4676,7 +4737,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
4676
4737
|
if (!document.body.classList.contains("dsh-drawer-open")) return;
|
|
4677
4738
|
const sidebar = document.querySelector('div[class*="_sidebarCol"]');
|
|
4678
4739
|
if (!sidebar || !sidebar.contains(e.target)) return;
|
|
4679
|
-
const sessionRow = e.target.closest('div[class*="sessionRow"]
|
|
4740
|
+
const sessionRow = e.target.closest('div[class*="sessionRow"]');
|
|
4680
4741
|
if (!sessionRow) return;
|
|
4681
4742
|
longPressTimer = setTimeout(() => {
|
|
4682
4743
|
lastLongPressTime = Date.now();
|
|
@@ -4829,7 +4890,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4829
4890
|
<form id="dsh-ws-unlock-form" style="display: flex; gap: 8px;">
|
|
4830
4891
|
<input id="dsh-ws-unlock-input" type="password" placeholder="\u8BF7\u8F93\u5165\u540E\u53F0\u7BA1\u7406\u5BC6\u7801" value="${escapeHtml(unlockInput)}"
|
|
4831
4892
|
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-
|
|
4893
|
+
<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
4894
|
</form>
|
|
4834
4895
|
${unlockErr ? `<div style="font-size: 11px; color: var(--dsw-alias-state-error-primary, #dc2626); margin-top: 6px;">${escapeHtml(unlockErr)}</div>` : ""}
|
|
4835
4896
|
</div>
|
|
@@ -4849,7 +4910,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4849
4910
|
${(drives || []).map((d) => {
|
|
4850
4911
|
const isActive = currentPath.startsWith(d.path) || currentPath === d.path;
|
|
4851
4912
|
return `
|
|
4852
|
-
<button class="dsh-ws-quick-btn" data-path="${escapeHtml(d.path)}" style="border: 1px solid ${isActive ? "var(--dsw-
|
|
4913
|
+
<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
4914
|
\u{1F4BE} ${escapeHtml(d.name)}
|
|
4854
4915
|
</button>
|
|
4855
4916
|
`;
|
|
@@ -4896,9 +4957,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4896
4957
|
<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
4958
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 6px;">
|
|
4898
4959
|
<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>
|
|
4960
|
+
<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
4961
|
</div>
|
|
4901
|
-
<button id="dsh-ws-add-current-btn" style="border: none; background: var(--dsw-
|
|
4962
|
+
<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
4963
|
${isSubmitting ? "\u6B63\u5728\u6DFB\u52A0\u5E76\u5207\u6362\u2026" : "\u{1F449} \u8BBE\u4E3A\u5F53\u524D\u5DE5\u4F5C\u533A\u5E76\u8FDB\u5165"}
|
|
4903
4964
|
</button>
|
|
4904
4965
|
</div>
|
|
@@ -4957,7 +5018,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4957
5018
|
<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
5019
|
\u524D\u5F80
|
|
4959
5020
|
</button>
|
|
4960
|
-
<button id="dsh-ws-manual-add-btn" style="border: none; background: var(--dsw-
|
|
5021
|
+
<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
5022
|
\u6DFB\u52A0\u5E76\u8FDB\u5165
|
|
4962
5023
|
</button>
|
|
4963
5024
|
</div>
|
package/client/index.js
CHANGED
|
@@ -758,11 +758,29 @@ 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
|
+
if (mode === 'password_only') {
|
|
765
|
+
// password_only 模式必须已有密码才能开启(服务端同样强制),就地引导设密码
|
|
766
|
+
window.alert('「仅密码 / PIN 码登录」模式必须先设置访问密码才能开启安全防护。\n\n请先在下方「设置外部访客访问密码 / PIN 码」输入密码并点击「保存访问密码」,然后再开启。');
|
|
767
|
+
setTopMsg({ ok: false, text: '请先设置访客访问密码,再开启安全防护' });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const go = window.confirm(
|
|
771
|
+
'⚠️ 您尚未设置任何访问密码或管理密码。\n\n开启安全防护后,任何知道局域网 IP / 隧道地址的访客仍可直接进入(当前相当于"免密开放"状态)。\n\n是否仍要开启?建议先关闭,在下方「设置外部访客访问密码」处设置密码后再开启。'
|
|
772
|
+
);
|
|
773
|
+
if (!go) return;
|
|
774
|
+
}
|
|
761
775
|
setEnabled(next);
|
|
762
776
|
try {
|
|
763
777
|
const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
|
|
764
778
|
if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
|
|
765
|
-
setTopMsg({ ok: true, text: next
|
|
779
|
+
setTopMsg({ ok: true, text: next
|
|
780
|
+
? (noPasswordYet
|
|
781
|
+
? '✓ 安全防护已开启(注意:尚未设置密码,访客仍可免密进入,请立即在下方设置访问密码)'
|
|
782
|
+
: '✓ 访问安全认证已开启(现有登录态已刷新)')
|
|
783
|
+
: '✓ 访问安全认证已关闭(访问免密,管理保护不受影响)' });
|
|
766
784
|
onUpdate?.();
|
|
767
785
|
} catch (e) {
|
|
768
786
|
setEnabled(prev);
|
|
@@ -786,6 +804,13 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
|
|
|
786
804
|
};
|
|
787
805
|
|
|
788
806
|
const handleChangeMode = async (m) => {
|
|
807
|
+
// 切换到「仅密码登录」但尚未设置任何密码:提前引导先设密码,避免切过去后
|
|
808
|
+
// 无密码可登录(服务端同样有守卫拒绝,双保险防自我锁死)
|
|
809
|
+
if (m === 'password_only' && !auth?.hasPassword && !auth?.hasAdminPassword) {
|
|
810
|
+
window.alert('「仅密码 / PIN 码登录」需要先设置访问密码。\n\n请在下方「设置外部访客访问密码 / PIN 码」处输入密码并点击「保存访问密码」,然后再切换到此模式或开启安全防护。');
|
|
811
|
+
setTopMsg({ ok: false, text: '请先在下方设置访客访问密码,再切换为「仅密码登录」模式' });
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
789
814
|
const prev = mode;
|
|
790
815
|
setMode(m);
|
|
791
816
|
try {
|
|
@@ -962,7 +987,10 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
|
|
|
962
987
|
// =========================================================================
|
|
963
988
|
// ---- 第一道防线:外部访问门禁(控制谁能进入 Web 界面使用 AI) ----
|
|
964
989
|
// =========================================================================
|
|
965
|
-
|
|
990
|
+
// 第一道防线卡片:已开启防护,或【尚未设置任何密码】时始终显示——
|
|
991
|
+
// 未设密码时必须给出密码输入框,否则用户开启防护(尤其 password_only)后
|
|
992
|
+
// 会因无密码被锁在登录墙外且找不到设密码入口(自我锁死,v2.10.5 修复)。
|
|
993
|
+
(enabled || !auth?.hasPassword) && React.createElement('div', { style: s.card },
|
|
966
994
|
React.createElement('div', { style: { marginBottom: 14 } },
|
|
967
995
|
React.createElement('div', { style: { ...s.label, fontSize: 14, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
968
996
|
'🛡️ 第一道防线:外部访问门禁(控制谁能使用 AI)',
|
|
@@ -2961,11 +2989,18 @@ function BridgePanel({ rpcCall }) {
|
|
|
2961
2989
|
|
|
2962
2990
|
const auth = status?.auth;
|
|
2963
2991
|
const policy = auth?.adminPolicy ?? 'password_unlock';
|
|
2992
|
+
// 系统是否已配置任何密码(访客访问密码 或 独立管理密码)。
|
|
2993
|
+
const hasAnyPassword = !!(auth?.hasPassword || auth?.hasAdminPassword);
|
|
2964
2994
|
// 锁屏条件:远程 + 管理保护开启(adminProtection)+ 未解锁 + 非宽松策略。
|
|
2965
2995
|
// 不依赖 auth.enabled:即使访问认证关闭,管理保护仍独立生效,上锁后必须显示锁屏。
|
|
2966
2996
|
// local_only 也锁定(显示"仅限本机管理"专属锁屏)。
|
|
2997
|
+
// 关键修正:仅当系统【已配置密码】时才需要输入密码解锁——全新安装(无任何密码)时
|
|
2998
|
+
// 服务端没有可校验的哈希,任何输入都会被放行(宽松降级),此时展示"输入管理密码"
|
|
2999
|
+
// 锁屏只会误导用户输一个假密码;应直接放行进入面板并提示先设置密码。
|
|
3000
|
+
// local_only 例外:无论是否已设密码,远程一律禁止管理(策略语义高于空密码豁免)。
|
|
2967
3001
|
const isLocked = !isLocalhost && auth?.adminProtection !== false
|
|
2968
|
-
&& policy !== 'open' && !adminUnlocked
|
|
3002
|
+
&& policy !== 'open' && !adminUnlocked
|
|
3003
|
+
&& (policy === 'local_only' || hasAnyPassword);
|
|
2969
3004
|
|
|
2970
3005
|
// 远程设备被锁定:全局展示锁定页面,阻断所有 Tab 的查看与操作
|
|
2971
3006
|
if (isLocked) {
|
|
@@ -3098,7 +3133,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3098
3133
|
}, '🔒 重新锁定后台'),
|
|
3099
3134
|
),
|
|
3100
3135
|
|
|
3101
|
-
//
|
|
3136
|
+
// 未解锁时的顶部引导条:未设密码 → 提示先设密码;已设密码 → 提示解锁
|
|
3102
3137
|
!isLocalhost && !adminUnlocked && auth?.enabled && policy !== 'open' && React.createElement('div', {
|
|
3103
3138
|
style: {
|
|
3104
3139
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
@@ -3107,12 +3142,20 @@ function BridgePanel({ rpcCall }) {
|
|
|
3107
3142
|
marginBottom: 14, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#92400e)',
|
|
3108
3143
|
},
|
|
3109
3144
|
},
|
|
3110
|
-
React.createElement('span', null,
|
|
3111
|
-
|
|
3145
|
+
React.createElement('span', null,
|
|
3146
|
+
hasAnyPassword
|
|
3147
|
+
? '🔒 后台管理权限未解锁(修改敏感配置需先解锁)'
|
|
3148
|
+
: '⚠️ 尚未设置任何访问密码 / 管理密码,远程访客可免密进入!'
|
|
3149
|
+
),
|
|
3150
|
+
hasAnyPassword ? React.createElement('button', {
|
|
3112
3151
|
type: 'button',
|
|
3113
3152
|
style: { ...s.btnPri, height: 24, fontSize: 11, padding: '0 10px', background: '#d97706' },
|
|
3114
3153
|
onClick: () => setShowUnlockModal(true),
|
|
3115
|
-
}, '🔑 解锁管理权限'),
|
|
3154
|
+
}, '🔑 解锁管理权限') : React.createElement('button', {
|
|
3155
|
+
type: 'button',
|
|
3156
|
+
style: { ...s.btnPri, height: 24, fontSize: 11, padding: '0 10px', background: '#d97706' },
|
|
3157
|
+
onClick: () => setActiveTab('security'),
|
|
3158
|
+
}, '🔐 立即设置密码'),
|
|
3116
3159
|
),
|
|
3117
3160
|
|
|
3118
3161
|
React.createElement(VersionBanner, { rpcCall: authRpcCall }),
|
|
@@ -3404,8 +3447,11 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3404
3447
|
}
|
|
3405
3448
|
}
|
|
3406
3449
|
|
|
3407
|
-
//
|
|
3408
|
-
|
|
3450
|
+
// 点击会话项后平滑收起抽屉。
|
|
3451
|
+
// 注意:不能匹配 div[role="treeitem"] —— DSH 的 workspace 分组行(projectRow)
|
|
3452
|
+
// 同样带 role="treeitem",会误伤"点击分组名展开/收起"的手势(issue #31)。
|
|
3453
|
+
// 会话行已被 div[class*="sessionRow"] 覆盖;搜索结果行是 <button>,不匹配 div。
|
|
3454
|
+
const sessionRow = e.target.closest('a, div[class*="sessionRow"]');
|
|
3409
3455
|
if (sessionRow) {
|
|
3410
3456
|
setTimeout(() => {
|
|
3411
3457
|
if (document.body.classList.contains('dsh-drawer-open')) {
|
|
@@ -3434,7 +3480,8 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3434
3480
|
const sidebar = document.querySelector('div[class*="_sidebarCol"]');
|
|
3435
3481
|
if (!sidebar || !sidebar.contains(e.target)) return;
|
|
3436
3482
|
|
|
3437
|
-
|
|
3483
|
+
// 长按会话项呼出操作菜单。同样不能匹配 div[role="treeitem"](分组行同标签,issue #31)。
|
|
3484
|
+
const sessionRow = e.target.closest('div[class*="sessionRow"]');
|
|
3438
3485
|
if (!sessionRow) return;
|
|
3439
3486
|
|
|
3440
3487
|
longPressTimer = setTimeout(() => {
|
|
@@ -3633,7 +3680,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
3633
3680
|
<form id="dsh-ws-unlock-form" style="display: flex; gap: 8px;">
|
|
3634
3681
|
<input id="dsh-ws-unlock-input" type="password" placeholder="请输入后台管理密码" value="${escapeHtml(unlockInput)}"
|
|
3635
3682
|
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-
|
|
3683
|
+
<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
3684
|
</form>
|
|
3638
3685
|
${unlockErr ? `<div style="font-size: 11px; color: var(--dsw-alias-state-error-primary, #dc2626); margin-top: 6px;">${escapeHtml(unlockErr)}</div>` : ''}
|
|
3639
3686
|
</div>
|
|
@@ -3653,7 +3700,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
3653
3700
|
${(drives || []).map(d => {
|
|
3654
3701
|
const isActive = currentPath.startsWith(d.path) || currentPath === d.path;
|
|
3655
3702
|
return `
|
|
3656
|
-
<button class="dsh-ws-quick-btn" data-path="${escapeHtml(d.path)}" style="border: 1px solid ${isActive ? 'var(--dsw-
|
|
3703
|
+
<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
3704
|
💾 ${escapeHtml(d.name)}
|
|
3658
3705
|
</button>
|
|
3659
3706
|
`;
|
|
@@ -3700,9 +3747,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
3700
3747
|
<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
3748
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 6px;">
|
|
3702
3749
|
<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>
|
|
3750
|
+
<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
3751
|
</div>
|
|
3705
|
-
<button id="dsh-ws-add-current-btn" style="border: none; background: var(--dsw-
|
|
3752
|
+
<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
3753
|
${isSubmitting ? '正在添加并切换…' : '👉 设为当前工作区并进入'}
|
|
3707
3754
|
</button>
|
|
3708
3755
|
</div>
|
|
@@ -3761,7 +3808,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
3761
3808
|
<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
3809
|
前往
|
|
3763
3810
|
</button>
|
|
3764
|
-
<button id="dsh-ws-manual-add-btn" style="border: none; background: var(--dsw-
|
|
3811
|
+
<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
3812
|
添加并进入
|
|
3766
3813
|
</button>
|
|
3767
3814
|
</div>
|