@wenbin_wb/dsh-bridge 2.10.2 → 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 +45 -0
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/client/client.js +86 -14
- package/client/index.js +58 -14
- package/client/mobile-styles.js +834 -802
- package/lib/auth/login-template.js +417 -382
- package/lib/bridge-rpc.js +2 -2
- package/lib/feishu/node.js +10 -4
- package/lib/index.js +109 -15
- package/lib/platform/conversation-bridge.js +8 -3
- package/lib/platform/message-split.js +39 -1
- package/lib/qq/node.js +539 -532
- package/lib/session-strip.js +67 -0
- package/lib/telegram/gateway.js +28 -26
- package/lib/telegram/node.js +0 -27
- package/lib/tunnel-client.mjs +130 -35
- package/lib/wechat/gateway.js +17 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@
|
|
|
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
|
+
|
|
25
|
+
## [v2.10.3] - 2026-09-03
|
|
26
|
+
|
|
27
|
+
### ✨ 新功能
|
|
28
|
+
|
|
29
|
+
- **兼容 DSH 原生端口直连(3080)**(issue #28):此前本机用 `http://127.0.0.1:3080`(DSH 原生端口)打开面板时,因 loopback-token 端点仅暴露在代理端口 3082、且 CORS 白名单不含原生端口来源,管理功能会被误判为「远程」而锁定(local_only 策略下)。现在原生端口页面可跨域回读管理员令牌,直连与走代理行为一致;CORS 仍收敛,仅回显白名单来源。
|
|
30
|
+
|
|
31
|
+
### 🐞 修复
|
|
32
|
+
|
|
33
|
+
- **修复代理响应头透传问题**:反向代理与 WebSocket 升级路径现会剥离 hop-by-hop 头(transfer-encoding/connection 等),并对 WS 101 响应做白名单过滤,避免上游注入任意头或客户端解析异常。
|
|
34
|
+
- **修复 QQ 流式消息发送**:首片失败时不再补发「全新流式消息」造成重复,直接降级单条 Markdown;引用机器人消息自动开新会话不再依赖固定 100ms 等待(消除竞态)。
|
|
35
|
+
- **修复微信网关轮询竞态**:stop/restart 时通过 AbortController 立即中断进行中的长轮询,避免新旧 poller 并存触发 iLink 独占锁(403)。
|
|
36
|
+
- **修复飞书群聊审批按钮被误拦**:群聊按「群」整体授权(与 QQ 群模型一致),群内成员可点按钮决议;单聊仍严格校验发起者本人。
|
|
37
|
+
- **会话列表/历史大字段剥离覆盖全部公网入口**:session.list / session.history 的大字段剥离不再仅限自建隧道,Cloudflare 隧道、局域网、外部登记隧道同样生效(社区 PR #27 扩展)。
|
|
38
|
+
|
|
39
|
+
### 🔒 安全
|
|
40
|
+
|
|
41
|
+
- **[SEND_FILE] 外发白名单**:AI 输出携带的 `[SEND_FILE: 路径]` 指令现仅允许发送会话工作目录内的文件,并拦截 `.ssh`/`.credentials`/`.env` 等敏感路径,防止模型被诱导后外发任意本地文件。
|
|
42
|
+
|
|
43
|
+
### 🧹 内部优化
|
|
44
|
+
|
|
45
|
+
- **未设密码高危告警**:开启访问认证但尚未设置任何密码时,设置页顶部显示醒目风险提示。
|
|
46
|
+
- **自建隧道安全须知**:README(中/英)明确隧道公网入口无独立认证、必须配合本地访问认证使用。
|
|
47
|
+
- 版本探测失败改为 10 分钟退避重试;版本检查结果 10 分钟 TTL 缓存,减少外网请求。
|
|
48
|
+
- 清理 Telegram 死代码(无调用点的审批卡片方法)。
|
|
49
|
+
- 新增 issue #28 回归测试,全量单测通过。
|
|
50
|
+
---
|
|
51
|
+
|
|
7
52
|
## [v2.10.2] - 2026-09-01
|
|
8
53
|
|
|
9
54
|
### ✨ 新功能
|
package/README.en.md
CHANGED
|
@@ -138,6 +138,8 @@ Access DeepSeek Harness from anywhere outside your home network without public I
|
|
|
138
138
|
- **Mode 3: Custom WebSocket Tunnel**:
|
|
139
139
|
- Connect to your personal VPS reverse proxy server ([View Setup Guide](docs/custom-tunnel.md)), equipped with per-message gzip and SSE optimization.
|
|
140
140
|
|
|
141
|
+
> **Custom tunnel security note**: The tunnel server (`scripts/install-tunnel-server.sh`) only authenticates the *tunnel client control channel* with `TOKEN`; public HTTP/WebSocket requests forwarded through the tunnel domain are **not independently authenticated** — security relies entirely on the plugin's local Access Auth (the `x-dsh-internal-tunnel` marker prevents tunnel traffic from using the loopback exemption). Always enable **Access Auth** with a password/QR Token in the plugin settings (especially with `scope=all` or when exposed publicly); with no password set, anyone who knows the tunnel URL can reach your DSH.
|
|
142
|
+
|
|
141
143
|
---
|
|
142
144
|
|
|
143
145
|
### 3. 📱 Mobile Experience & Standalone PWA
|
package/README.md
CHANGED
|
@@ -146,6 +146,8 @@ dsh plugin --profile web add @wenbin_wb/dsh-bridge@latest
|
|
|
146
146
|
- **模式 3:自建 WebSocket 隧道**
|
|
147
147
|
* 支持连接个人 VPS 隧道中转服务器([查看自建隧道部署教程](docs/custom-tunnel.md)),具备数据端到端 gzip 压缩与 SSE 响应优化。
|
|
148
148
|
|
|
149
|
+
> **自建隧道安全须知**:隧道服务端(`scripts/install-tunnel-server.sh`)只对「隧道客户端控制通道」校验 `TOKEN`;公网访客对隧道域名的 HTTP/WebSocket 转发**不再做独立认证**,安全完全依赖插件本地的「访问认证」(`x-dsh-internal-tunnel` 标识使隧道流量无法享受本机回环保留)。请务必在插件设置中开启「安全认证」并设置访问密码/二维码 Token(尤其 `scope=all` 或公网使用时);未设置任何密码时,任何知道隧道地址的访客都能直接访问您的 DSH。
|
|
150
|
+
|
|
149
151
|
---
|
|
150
152
|
|
|
151
153
|
### 3. 📱 移动端交互与 PWA 独立全屏 App
|
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);
|
|
@@ -1993,7 +2032,31 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
|
|
|
1993
2032
|
background: topMsg.ok ? "var(--dsw-alias-state-success-bg,#ecfdf5)" : "var(--dsw-alias-state-error-bg,#fef2f2)",
|
|
1994
2033
|
color: topMsg.ok ? "var(--dsw-alias-state-success-primary,#059669)" : "var(--dsw-alias-state-error-primary,#dc2626)"
|
|
1995
2034
|
}
|
|
1996
|
-
}, topMsg.text)
|
|
2035
|
+
}, topMsg.text),
|
|
2036
|
+
// 访问认证已开启但尚未设置任何密码/Token 保护:局域网/公网访客可免密进入,高危提示
|
|
2037
|
+
enabled && !auth?.hasPassword && !auth?.hasAdminPassword && React.createElement(
|
|
2038
|
+
"div",
|
|
2039
|
+
{
|
|
2040
|
+
style: {
|
|
2041
|
+
marginTop: 12,
|
|
2042
|
+
padding: "10px 14px",
|
|
2043
|
+
borderRadius: 6,
|
|
2044
|
+
fontSize: 12,
|
|
2045
|
+
background: "var(--dsw-alias-state-error-bg,#fef2f2)",
|
|
2046
|
+
border: "1px solid var(--dsw-alias-state-error-border,#fecaca)",
|
|
2047
|
+
color: "var(--dsw-alias-state-error-primary,#dc2626)",
|
|
2048
|
+
lineHeight: 1.6
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
"\u26A0\uFE0F ",
|
|
2052
|
+
React.createElement("strong", null, "\u5C1A\u672A\u8BBE\u7F6E\u4EFB\u4F55\u8BBF\u95EE\u5BC6\u7801\u6216\u7BA1\u7406\u5BC6\u7801"),
|
|
2053
|
+
" \u2014\u2014 \u6B64\u65F6\u201C\u8BBF\u95EE\u8BA4\u8BC1\u5DF2\u5F00\u542F\u201D\u4F46\u4EFB\u4F55\u77E5\u9053\u5C40\u57DF\u7F51 IP / \u96A7\u9053\u5730\u5740\u7684\u4EBA\u90FD\u80FD\u76F4\u63A5\u8FDB\u5165\uFF08\u96A7\u9053\u5165\u53E3\u81EA\u8EAB\u4E0D\u8BBE\u9632\uFF0C\u8BE6\u89C1\u4E0B\u65B9\u5B89\u5168\u987B\u77E5\uFF09\u3002",
|
|
2054
|
+
"\u5EFA\u8BAE\u7ACB\u5373\u5728\u4E0B\u65B9\u8BBE\u7F6E",
|
|
2055
|
+
React.createElement("strong", null, "\u8BBF\u5BA2\u8BBF\u95EE\u5BC6\u7801"),
|
|
2056
|
+
"\u6216",
|
|
2057
|
+
React.createElement("strong", null, "\u7BA1\u7406\u5BC6\u7801"),
|
|
2058
|
+
"\u540E\u518D\u5BF9\u5916\u5F00\u653E\u3002"
|
|
2059
|
+
)
|
|
1997
2060
|
),
|
|
1998
2061
|
React.createElement(
|
|
1999
2062
|
React.Fragment,
|
|
@@ -4172,7 +4235,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
4172
4235
|
}
|
|
4173
4236
|
const auth = status?.auth;
|
|
4174
4237
|
const policy = auth?.adminPolicy ?? "password_unlock";
|
|
4175
|
-
const
|
|
4238
|
+
const hasAnyPassword = !!(auth?.hasPassword || auth?.hasAdminPassword);
|
|
4239
|
+
const isLocked = !isLocalhost && auth?.adminProtection !== false && policy !== "open" && !adminUnlocked && (policy === "local_only" || hasAnyPassword);
|
|
4176
4240
|
if (isLocked) {
|
|
4177
4241
|
return React.createElement(
|
|
4178
4242
|
"div",
|
|
@@ -4343,7 +4407,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
4343
4407
|
onClick: handleLockAdmin
|
|
4344
4408
|
}, "\u{1F512} \u91CD\u65B0\u9501\u5B9A\u540E\u53F0")
|
|
4345
4409
|
),
|
|
4346
|
-
//
|
|
4410
|
+
// 未解锁时的顶部引导条:未设密码 → 提示先设密码;已设密码 → 提示解锁
|
|
4347
4411
|
!isLocalhost && !adminUnlocked && auth?.enabled && policy !== "open" && React.createElement(
|
|
4348
4412
|
"div",
|
|
4349
4413
|
{
|
|
@@ -4360,12 +4424,20 @@ function BridgePanel({ rpcCall }) {
|
|
|
4360
4424
|
color: "var(--dsw-alias-state-warn-primary,#92400e)"
|
|
4361
4425
|
}
|
|
4362
4426
|
},
|
|
4363
|
-
React.createElement(
|
|
4364
|
-
|
|
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", {
|
|
4365
4433
|
type: "button",
|
|
4366
4434
|
style: { ...s.btnPri, height: 24, fontSize: 11, padding: "0 10px", background: "#d97706" },
|
|
4367
4435
|
onClick: () => setShowUnlockModal(true)
|
|
4368
|
-
}, "\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")
|
|
4369
4441
|
),
|
|
4370
4442
|
React.createElement(VersionBanner, { rpcCall: authRpcCall }),
|
|
4371
4443
|
React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
|
|
@@ -4629,7 +4701,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
4629
4701
|
return;
|
|
4630
4702
|
}
|
|
4631
4703
|
}
|
|
4632
|
-
const sessionRow = e.target.closest('a, div[class*="sessionRow"]
|
|
4704
|
+
const sessionRow = e.target.closest('a, div[class*="sessionRow"]');
|
|
4633
4705
|
if (sessionRow) {
|
|
4634
4706
|
setTimeout(() => {
|
|
4635
4707
|
if (document.body.classList.contains("dsh-drawer-open")) {
|
|
@@ -4652,7 +4724,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
4652
4724
|
if (!document.body.classList.contains("dsh-drawer-open")) return;
|
|
4653
4725
|
const sidebar = document.querySelector('div[class*="_sidebarCol"]');
|
|
4654
4726
|
if (!sidebar || !sidebar.contains(e.target)) return;
|
|
4655
|
-
const sessionRow = e.target.closest('div[class*="sessionRow"]
|
|
4727
|
+
const sessionRow = e.target.closest('div[class*="sessionRow"]');
|
|
4656
4728
|
if (!sessionRow) return;
|
|
4657
4729
|
longPressTimer = setTimeout(() => {
|
|
4658
4730
|
lastLongPressTime = Date.now();
|
|
@@ -4805,7 +4877,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4805
4877
|
<form id="dsh-ws-unlock-form" style="display: flex; gap: 8px;">
|
|
4806
4878
|
<input id="dsh-ws-unlock-input" type="password" placeholder="\u8BF7\u8F93\u5165\u540E\u53F0\u7BA1\u7406\u5BC6\u7801" value="${escapeHtml(unlockInput)}"
|
|
4807
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;" />
|
|
4808
|
-
<button type="submit" style="border: none; background: var(--dsw-
|
|
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>
|
|
4809
4881
|
</form>
|
|
4810
4882
|
${unlockErr ? `<div style="font-size: 11px; color: var(--dsw-alias-state-error-primary, #dc2626); margin-top: 6px;">${escapeHtml(unlockErr)}</div>` : ""}
|
|
4811
4883
|
</div>
|
|
@@ -4825,7 +4897,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4825
4897
|
${(drives || []).map((d) => {
|
|
4826
4898
|
const isActive = currentPath.startsWith(d.path) || currentPath === d.path;
|
|
4827
4899
|
return `
|
|
4828
|
-
<button class="dsh-ws-quick-btn" data-path="${escapeHtml(d.path)}" style="border: 1px solid ${isActive ? "var(--dsw-
|
|
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;">
|
|
4829
4901
|
\u{1F4BE} ${escapeHtml(d.name)}
|
|
4830
4902
|
</button>
|
|
4831
4903
|
`;
|
|
@@ -4872,9 +4944,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4872
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;">
|
|
4873
4945
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 6px;">
|
|
4874
4946
|
<span style="font-size: 11px; font-weight: 600; color: var(--dsw-alias-brand-primary, #2563eb); flex-shrink: 0;">\u5F53\u524D\u76EE\u5F55:</span>
|
|
4875
|
-
<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>
|
|
4876
4948
|
</div>
|
|
4877
|
-
<button id="dsh-ws-add-current-btn" style="border: none; background: var(--dsw-
|
|
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" : ""}>
|
|
4878
4950
|
${isSubmitting ? "\u6B63\u5728\u6DFB\u52A0\u5E76\u5207\u6362\u2026" : "\u{1F449} \u8BBE\u4E3A\u5F53\u524D\u5DE5\u4F5C\u533A\u5E76\u8FDB\u5165"}
|
|
4879
4951
|
</button>
|
|
4880
4952
|
</div>
|
|
@@ -4933,7 +5005,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4933
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;">
|
|
4934
5006
|
\u524D\u5F80
|
|
4935
5007
|
</button>
|
|
4936
|
-
<button id="dsh-ws-manual-add-btn" style="border: none; background: var(--dsw-
|
|
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;">
|
|
4937
5009
|
\u6DFB\u52A0\u5E76\u8FDB\u5165
|
|
4938
5010
|
</button>
|
|
4939
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);
|
|
@@ -943,6 +955,19 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
|
|
|
943
955
|
color: topMsg.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
|
|
944
956
|
},
|
|
945
957
|
}, topMsg.text),
|
|
958
|
+
|
|
959
|
+
// 访问认证已开启但尚未设置任何密码/Token 保护:局域网/公网访客可免密进入,高危提示
|
|
960
|
+
enabled && !auth?.hasPassword && !auth?.hasAdminPassword && React.createElement('div', {
|
|
961
|
+
style: {
|
|
962
|
+
marginTop: 12, padding: '10px 14px', borderRadius: 6, fontSize: 12,
|
|
963
|
+
background: 'var(--dsw-alias-state-error-bg,#fef2f2)',
|
|
964
|
+
border: '1px solid var(--dsw-alias-state-error-border,#fecaca)',
|
|
965
|
+
color: 'var(--dsw-alias-state-error-primary,#dc2626)',
|
|
966
|
+
lineHeight: 1.6,
|
|
967
|
+
},
|
|
968
|
+
}, '⚠️ ', React.createElement('strong', null, '尚未设置任何访问密码或管理密码'),
|
|
969
|
+
' —— 此时“访问认证已开启”但任何知道局域网 IP / 隧道地址的人都能直接进入(隧道入口自身不设防,详见下方安全须知)。',
|
|
970
|
+
'建议立即在下方设置', React.createElement('strong', null, '访客访问密码'), '或', React.createElement('strong', null, '管理密码'), '后再对外开放。'),
|
|
946
971
|
),
|
|
947
972
|
|
|
948
973
|
React.createElement(React.Fragment, null,
|
|
@@ -2948,11 +2973,18 @@ function BridgePanel({ rpcCall }) {
|
|
|
2948
2973
|
|
|
2949
2974
|
const auth = status?.auth;
|
|
2950
2975
|
const policy = auth?.adminPolicy ?? 'password_unlock';
|
|
2976
|
+
// 系统是否已配置任何密码(访客访问密码 或 独立管理密码)。
|
|
2977
|
+
const hasAnyPassword = !!(auth?.hasPassword || auth?.hasAdminPassword);
|
|
2951
2978
|
// 锁屏条件:远程 + 管理保护开启(adminProtection)+ 未解锁 + 非宽松策略。
|
|
2952
2979
|
// 不依赖 auth.enabled:即使访问认证关闭,管理保护仍独立生效,上锁后必须显示锁屏。
|
|
2953
2980
|
// local_only 也锁定(显示"仅限本机管理"专属锁屏)。
|
|
2981
|
+
// 关键修正:仅当系统【已配置密码】时才需要输入密码解锁——全新安装(无任何密码)时
|
|
2982
|
+
// 服务端没有可校验的哈希,任何输入都会被放行(宽松降级),此时展示"输入管理密码"
|
|
2983
|
+
// 锁屏只会误导用户输一个假密码;应直接放行进入面板并提示先设置密码。
|
|
2984
|
+
// local_only 例外:无论是否已设密码,远程一律禁止管理(策略语义高于空密码豁免)。
|
|
2954
2985
|
const isLocked = !isLocalhost && auth?.adminProtection !== false
|
|
2955
|
-
&& policy !== 'open' && !adminUnlocked
|
|
2986
|
+
&& policy !== 'open' && !adminUnlocked
|
|
2987
|
+
&& (policy === 'local_only' || hasAnyPassword);
|
|
2956
2988
|
|
|
2957
2989
|
// 远程设备被锁定:全局展示锁定页面,阻断所有 Tab 的查看与操作
|
|
2958
2990
|
if (isLocked) {
|
|
@@ -3085,7 +3117,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3085
3117
|
}, '🔒 重新锁定后台'),
|
|
3086
3118
|
),
|
|
3087
3119
|
|
|
3088
|
-
//
|
|
3120
|
+
// 未解锁时的顶部引导条:未设密码 → 提示先设密码;已设密码 → 提示解锁
|
|
3089
3121
|
!isLocalhost && !adminUnlocked && auth?.enabled && policy !== 'open' && React.createElement('div', {
|
|
3090
3122
|
style: {
|
|
3091
3123
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
@@ -3094,12 +3126,20 @@ function BridgePanel({ rpcCall }) {
|
|
|
3094
3126
|
marginBottom: 14, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#92400e)',
|
|
3095
3127
|
},
|
|
3096
3128
|
},
|
|
3097
|
-
React.createElement('span', null,
|
|
3098
|
-
|
|
3129
|
+
React.createElement('span', null,
|
|
3130
|
+
hasAnyPassword
|
|
3131
|
+
? '🔒 后台管理权限未解锁(修改敏感配置需先解锁)'
|
|
3132
|
+
: '⚠️ 尚未设置任何访问密码 / 管理密码,远程访客可免密进入!'
|
|
3133
|
+
),
|
|
3134
|
+
hasAnyPassword ? React.createElement('button', {
|
|
3099
3135
|
type: 'button',
|
|
3100
3136
|
style: { ...s.btnPri, height: 24, fontSize: 11, padding: '0 10px', background: '#d97706' },
|
|
3101
3137
|
onClick: () => setShowUnlockModal(true),
|
|
3102
|
-
}, '🔑 解锁管理权限'),
|
|
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
|
+
}, '🔐 立即设置密码'),
|
|
3103
3143
|
),
|
|
3104
3144
|
|
|
3105
3145
|
React.createElement(VersionBanner, { rpcCall: authRpcCall }),
|
|
@@ -3391,8 +3431,11 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3391
3431
|
}
|
|
3392
3432
|
}
|
|
3393
3433
|
|
|
3394
|
-
//
|
|
3395
|
-
|
|
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"]');
|
|
3396
3439
|
if (sessionRow) {
|
|
3397
3440
|
setTimeout(() => {
|
|
3398
3441
|
if (document.body.classList.contains('dsh-drawer-open')) {
|
|
@@ -3421,7 +3464,8 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3421
3464
|
const sidebar = document.querySelector('div[class*="_sidebarCol"]');
|
|
3422
3465
|
if (!sidebar || !sidebar.contains(e.target)) return;
|
|
3423
3466
|
|
|
3424
|
-
|
|
3467
|
+
// 长按会话项呼出操作菜单。同样不能匹配 div[role="treeitem"](分组行同标签,issue #31)。
|
|
3468
|
+
const sessionRow = e.target.closest('div[class*="sessionRow"]');
|
|
3425
3469
|
if (!sessionRow) return;
|
|
3426
3470
|
|
|
3427
3471
|
longPressTimer = setTimeout(() => {
|
|
@@ -3620,7 +3664,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
3620
3664
|
<form id="dsh-ws-unlock-form" style="display: flex; gap: 8px;">
|
|
3621
3665
|
<input id="dsh-ws-unlock-input" type="password" placeholder="请输入后台管理密码" value="${escapeHtml(unlockInput)}"
|
|
3622
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;" />
|
|
3623
|
-
<button type="submit" style="border: none; background: var(--dsw-
|
|
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>
|
|
3624
3668
|
</form>
|
|
3625
3669
|
${unlockErr ? `<div style="font-size: 11px; color: var(--dsw-alias-state-error-primary, #dc2626); margin-top: 6px;">${escapeHtml(unlockErr)}</div>` : ''}
|
|
3626
3670
|
</div>
|
|
@@ -3640,7 +3684,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
3640
3684
|
${(drives || []).map(d => {
|
|
3641
3685
|
const isActive = currentPath.startsWith(d.path) || currentPath === d.path;
|
|
3642
3686
|
return `
|
|
3643
|
-
<button class="dsh-ws-quick-btn" data-path="${escapeHtml(d.path)}" style="border: 1px solid ${isActive ? 'var(--dsw-
|
|
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;">
|
|
3644
3688
|
💾 ${escapeHtml(d.name)}
|
|
3645
3689
|
</button>
|
|
3646
3690
|
`;
|
|
@@ -3687,9 +3731,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
3687
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;">
|
|
3688
3732
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 6px;">
|
|
3689
3733
|
<span style="font-size: 11px; font-weight: 600; color: var(--dsw-alias-brand-primary, #2563eb); flex-shrink: 0;">当前目录:</span>
|
|
3690
|
-
<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>
|
|
3691
3735
|
</div>
|
|
3692
|
-
<button id="dsh-ws-add-current-btn" style="border: none; background: var(--dsw-
|
|
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' : ''}>
|
|
3693
3737
|
${isSubmitting ? '正在添加并切换…' : '👉 设为当前工作区并进入'}
|
|
3694
3738
|
</button>
|
|
3695
3739
|
</div>
|
|
@@ -3748,7 +3792,7 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
3748
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;">
|
|
3749
3793
|
前往
|
|
3750
3794
|
</button>
|
|
3751
|
-
<button id="dsh-ws-manual-add-btn" style="border: none; background: var(--dsw-
|
|
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;">
|
|
3752
3796
|
添加并进入
|
|
3753
3797
|
</button>
|
|
3754
3798
|
</div>
|