dsh-bridge-gateway 0.1.4 → 0.1.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 CHANGED
@@ -2,7 +2,22 @@
2
2
 
3
3
  本项目 `dsh-bridge-gateway` 是基于 [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge) 的移植增强分支,在保留原版能力之外,重点新增「公网直连网关」。
4
4
 
5
- ## [0.1.4] - 待发布
5
+ ## [0.1.5] - 2026-09-12
6
+
7
+ ### 变更
8
+ - 适配 DSH 0.1.5:RPC 通道从 `connection.rpc.handle` 迁到 webServer 直注册 + `connection.requestRejection` 鉴权(0.1.5 inject 守卫使旧入口不可用,wire 格式不变)。
9
+
10
+ ### 修复(移动端深度适配批次,基于 390px/320px 真机实测坐标)
11
+ - 顶栏「标准模式」与「对话管理」水平重叠(390px 下重叠 11px):titleRow 允许换行,titleCluster/headerActions/headerUtilities 各自可收缩并限宽(45vw/50vw),内部按钮限宽省略号。
12
+ - 输入框被撑到 780px 而消息列仅 318px:从会话根节点收窄 `--dsh-chat-content-width`/`--dsh-composer-card-max-width` 变量,并补上此前遗漏的 `uV2eYG_root` 约束层,输入框祖先链整体不超视口。
13
+ - 320px 下 git 分支 chip 溢出屏幕右侧 39px 被裁切:chip 限宽 `min(120px, 30vw)`,其 `position:absolute` 浮层锚点改用 `right: 8px` 覆盖内联 `left`。
14
+ - 主要控件(模型/权限/工作区/分支 chip/会话操作等 10 类按钮)点击热区经伪元素 `inset: -8px` 扩至约 44px,视觉尺寸不变。
15
+ - 侧边栏关闭时仅被 transform 移出屏幕、内部 7 个按钮仍可被 Tab 聚焦:改用 `visibility: hidden` + `pointer-events: none`,抽屉打开时恢复。
16
+ - 隐藏 dsh-better-sidebar 注入的全屏 `panel-host` 遮罩(z-index 25 压住输入框并拦截点击)与主题装饰性 `glass-fade` 渐变条(右下角小方块)。
17
+ - 输入框底部工具栏(权限选择器/模型选择器/添加命令/发送)弹性自适应互不重叠。
18
+ - 侧边栏抽屉底部溢出屏幕 38px:`box-sizing: border-box` + `100dvh` 高度约束。
19
+
20
+ ## [0.1.4] - 2026-08-30
6
21
 
7
22
  ### 修复
8
23
  - 修复「远程访问」面板在宿主连接未建立时无限卡在「加载中」的问题:为状态拉取增加兜底超时(15s),超时后释放 in-flight 锁并给出可用的「🔄 重试」按钮,避免面板永远无法自行恢复。
package/README.md CHANGED
@@ -1,47 +1,82 @@
1
1
  # dsh-bridge-gateway
2
2
 
3
- DeepSeek Harness 远程访问插件:以 [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge) 为基础,额外加入 **「公网直连网关」**,让手机/外网设备能直接访问操作电脑上的 Harness。
3
+ English | [简体中文](./README.zh.md)
4
4
 
5
- 局域网、Cloudflare 隧道、自建隧道、IM 机器人等原有能力保持不变。
5
+ Remote-access plugin for DeepSeek Harness (DSH). Built on top of [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge), it adds a **public direct-connect gateway** so phones and other out-of-network devices can open and operate the Harness running on your computer — no tunnel required.
6
6
 
7
- ## 一键安装
7
+ LAN access, Cloudflare tunnels, custom tunnels, and the IM bots all keep working exactly as before. The plugin is fully inert until you enable the features you want.
8
8
 
9
- ```bash
9
+ ## Features
10
+
11
+ - **Public direct-connect gateway.** The computer listens on `0.0.0.0:<port>` with its own HTTPS self-signed certificate and a forced login gate, so outside devices connect straight to your machine. The link is terminated at the gateway and proxied to the DSH loopback, so plugin pages and RPC channels work unchanged.
12
+ - **LAN access.** Scan a QR code from the remote-access panel on the same Wi-Fi and you are in.
13
+ - **Public tunnels.** Cloudflare (temporary or fixed hostname) or a self-built tunnel, selectable per need alongside the direct gateway.
14
+ - **Remote workspace.** Browse the host's directory tree and pick workspace conversations from the phone.
15
+ - **Security.** Password gate + QR-based password-free token + admin unlock/lock for sensitive config + rate limiting against brute force. Client-claimed `isLocalhost` is never trusted.
16
+ - **IM bots.** WeChat / QQ / Feishu / Telegram bots connect through their own gateway links — no public IP needed. Per-platform guides live in [docs/](./docs).
17
+ - **Mobile-friendly web.** The remote web UI is deeply adapted for phone screens: drawer sidebar (fully hidden when closed, removing keyboard/screen-reader focus traps), top-bar mode and conversation-management controls that wrap instead of overlapping, git branch chips anchored to the viewport edge instead of being clipped, composer width aligned with the conversation column and stuck to the bottom, ~44px touch targets on the main controls via pseudo-element hit areas, and removal of full-screen overlays and decorative gradient strips that covered the input box.
18
+ - **All configuration in the UI.** Every feature above is configured from DSH Web's remote-access panel; nothing is edited by hand.
19
+
20
+ ## Requirements
21
+
22
+ - Node `^22.19.0 || >=24.0.0`.
23
+ - DSH with a web profile (built and tested against `0.1.5-rc.2`; adapted for the 0.1.5 RPC/webServer entry points).
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ # Preferred: install straight from GitHub (no npm involved)
29
+ dsh plugin --profile web add github:lament-z/dsh-bridge-gateway
30
+
31
+ # Alternative: from npm
10
32
  dsh plugin --profile web add dsh-bridge-gateway
33
+
34
+ # From a local clone / working copy
35
+ dsh plugin --profile web add link:<this directory>
11
36
  ```
12
37
 
13
- > 源码安装:`git clone https://github.com/lament-z/dsh-bridge-gateway && dsh plugin --profile web add ./dsh-bridge-gateway`
38
+ Upgrade to the newest build: `dsh plugin --profile web add github:lament-z/dsh-bridge-gateway` again (or append `@latest` for the npm source). Then restart `dsh web`.
39
+
40
+ Nothing changes on install: the gateway is off by default and the original LAN/tunnel/IM behavior is untouched.
41
+
42
+ ## Usage
43
+
44
+ ### Public direct-connect gateway (core feature)
45
+
46
+ No tunnel needed — the computer exposes a port itself and outside devices connect directly.
47
+
48
+ 1. Install, then open DSH Web -> Settings -> **Public Access** tab.
49
+ 2. In the **Direct Gateway** card, set the port (default `7443`) and click save.
50
+ 3. Click **Enable Direct Gateway**.
51
+ 4. Map that port to this machine on your router / cloud server, then open `https://<public-IP-or-domain>:<port>` from outside.
52
+
53
+ First visit shows a certificate warning because the HTTPS certificate is self-signed — choose "always allow" to continue. External visitors must pass the login gate configured under **Security**; the gate policy is independent of your LAN settings.
54
+
55
+ ### LAN access
14
56
 
15
- 升级到最新版:`dsh plugin --profile web add dsh-bridge-gateway@latest`
57
+ Open the remote-access panel in DSH Web and scan the QR code with the phone on the same Wi-Fi.
16
58
 
17
- ## 核心功能:公网直连网关
59
+ ### Public tunnel
18
60
 
19
- 不需要隧道,让电脑直接监听公网端口,外网直连访问。
61
+ In the same panel choose Cloudflare (temporary or fixed hostname) or a self-built tunnel. See [docs/custom-tunnel.md](./docs/custom-tunnel.md) for the custom-tunnel protocol.
20
62
 
21
- - **HTTPS 自签证书**:本机自动生成,公网传输加密。
22
- - **强制登录门禁**:外部访问必须输入你在「安全认证」里设置的密码,与局域网策略相互独立。
23
- - **全界面配置**:端口、随 DSH 自动启动等,都在「公网访问」设置页里配置。
63
+ ### Security configuration
24
64
 
25
- ### 开启方式
65
+ Settings -> Security: enable protection, set the password, manage the password-free token, and unlock/lock the admin surface. Unauthorized visitors are rejected before any page content is served.
26
66
 
27
- 1. 安装后进入 DSH Web 设置 → 「公网访问」Tab。
28
- 2. 在「直连网关」卡片设置端口(默认 `7443`),点「保存端口」。
29
- 3. 点击「开启直连网关」。
30
- 4. 在路由器/云服务器把该端口映射到本机,即可通过 `https://<公网IP或域名>:端口` 访问。
67
+ ### IM bots
31
68
 
32
- 浏览器首次访问会提示证书不安全,勾选「始终允许」即可继续。
69
+ WeChat / QQ / Feishu / Telegram guides: [docs/wechat-usage.md](./docs/wechat-usage.md), [docs/qq-usage.md](./docs/qq-usage.md), [docs/feishu-usage.md](./docs/feishu-usage.md), [docs/telegram-usage.md](./docs/telegram-usage.md).
33
70
 
34
- ## 其他能力
71
+ ## Security notes
35
72
 
36
- - **局域网访问**:同一 Wi-Fi 扫码即可。
37
- - **公网隧道**:Cloudflare(临时/固定域名)或自建隧道二选一,与直连网关按需选用。
38
- - **远程工作区**:手机端网页目录树直接浏览并选择工作区对话。
39
- - **安全认证**:密码 + 扫码免密 Token + 管理后台防篡改 + 防暴力破解。
40
- - **IM 机器人**:微信 / QQ / 飞书 / Telegram 扫码直连,无需公网。
73
+ - The direct gateway always enforces `public_only`-style gating: external visitors authenticate through the same AuthManager as LAN access, and loopback-only resources stay loopback-only.
74
+ - RPC channels are registered through the host web server and pass request-rejection authentication, so forged or unauthenticated channel calls are dropped at the entry.
75
+ - The self-signed certificate encrypts transport; it does not add identity. Anyone with the password can log in — keep the password strong and enable admin lock.
41
76
 
42
- ## 开发
77
+ ## Development
43
78
 
44
- ```bash
79
+ ```sh
45
80
  git clone https://github.com/lament-z/dsh-bridge-gateway
46
81
  cd dsh-bridge-gateway
47
82
  npm install
@@ -50,10 +85,10 @@ npm test
50
85
  dsh plugin --profile web add .
51
86
  ```
52
87
 
53
- ## 发布说明
88
+ ## Release notes
54
89
 
55
- `v*` 标签推送到 GitHub 即自动 `npm publish`(GitHub Actions,需仓库配置 `NPM_TOKEN` secret)。
90
+ Pushing a `v*` tag to GitHub triggers automatic `npm publish` (GitHub Actions; the repository needs an `NPM_TOKEN` secret).
56
91
 
57
- ## 开源协议
92
+ ## License
58
93
 
59
- [MIT](./LICENSE)
94
+ [MIT](./LICENSE)
package/README.zh.md ADDED
@@ -0,0 +1,110 @@
1
+ # dsh-bridge-gateway
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ DeepSeek Harness(DSH)远程访问插件。以 [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge)
6
+ 为根基,额外加入**「公网直连网关」**,让手机等外网设备不借助隧道就能直接访问、操作电脑上的
7
+ Harness。
8
+
9
+ 局域网访问、Cloudflare 隧道、自建隧道、IM 机器人等原有能力保持不变;插件默认完全不启用,
10
+ 装上后原有行为零改动,需要什么再开什么。
11
+
12
+ ## 功能
13
+
14
+ - **公网直连网关。** 电脑直接监听 `0.0.0.0:<端口>`,自带 HTTPS 自签证书与强制登录门禁,
15
+ 外网设备直连访问。链路在网关处终结并转发到 DSH loopback,插件页面与 RPC 通道原样可用。
16
+ - **局域网访问。** 同一 Wi-Fi 下用手机扫远程访问面板里的二维码即可进入。
17
+ - **公网隧道。** Cloudflare(临时/固定域名)或自建隧道,与直连网关按需二选一或并存。
18
+ - **远程工作区。** 手机端网页目录树直接浏览并选择工作区对话。
19
+ - **安全认证。** 密码门禁 + 扫码免密 Token + 敏感配置的管理员解锁/锁定 + 防暴力破解限频;
20
+ 永不信任客户端自称的 `isLocalhost`。
21
+ - **IM 机器人。** 微信 / QQ / 飞书 / Telegram 机器人走各自网关链路,无需公网 IP;
22
+ 分平台指南见 [docs/](./docs)。
23
+ - **移动端适配。** 远程网页 UI 针对手机屏幕深度适配:抽屉化侧边栏(隐藏后消除键盘/读屏
24
+ 焦点陷阱)、顶栏「标准模式/对话管理」换行不再重叠、git 分支 chip 按视口右边界锚定不再
25
+ 被裁切、输入框宽度与对话列对齐且 sticky 固定于底部、主要控件以伪元素把点击热区扩到
26
+ 约 44px、隐藏会遮挡输入框的全屏遮罩与装饰性渐变条。
27
+ - **全界面配置。** 以上全部功能都在 DSH Web 远程访问面板里配置,无需手改任何文件。
28
+
29
+ ## 要求
30
+
31
+ - Node `^22.19.0 || >=24.0.0`。
32
+ - DSH web profile(构建与测试基于 `0.1.5-rc.2`,已适配 0.1.5 的 RPC/webServer 入口)。
33
+
34
+ ## 安装
35
+
36
+ ```sh
37
+ # 推荐:直接从 GitHub 安装(不经 npm)
38
+ dsh plugin --profile web add github:lament-z/dsh-bridge-gateway
39
+
40
+ # 备选:从 npm 安装
41
+ dsh plugin --profile web add dsh-bridge-gateway
42
+
43
+ # 从本地 clone / 工作副本安装
44
+ dsh plugin --profile web add link:<本目录>
45
+ ```
46
+
47
+ 升级到最新版:再次执行上面的 GitHub 安装命令(npm 来源可加 `@latest`)。然后重启 `dsh web`。
48
+
49
+ 安装后默认不启用:直连网关关闭,原有局域网/隧道/IM 行为完全不变。
50
+
51
+ ## 使用方法
52
+
53
+ ### 公网直连网关(核心功能)
54
+
55
+ 不需要隧道,让电脑直接监听公网端口,外网直连访问。
56
+
57
+ 1. 安装后进入 DSH Web 设置 -> **公网访问** Tab。
58
+ 2. 在**直连网关**卡片设置端口(默认 `7443`),点「保存端口」。
59
+ 3. 点击「开启直连网关」。
60
+ 4. 在路由器/云服务器把该端口映射到本机,即可通过 `https://<公网IP或域名>:端口` 访问。
61
+
62
+ HTTPS 证书为自签,浏览器首次访问会提示不安全,勾选「始终允许」即可继续。外部访客必须通过
63
+ **安全认证**里配置的登录门禁;门禁策略与局域网设置相互独立。
64
+
65
+ ### 局域网访问
66
+
67
+ 在 DSH Web 打开远程访问面板,用同一 Wi-Fi 下的手机扫二维码。
68
+
69
+ ### 公网隧道
70
+
71
+ 同一面板里选择 Cloudflare(临时/固定域名)或自建隧道。自建隧道协议见
72
+ [docs/custom-tunnel.md](./docs/custom-tunnel.md)。
73
+
74
+ ### 安全配置
75
+
76
+ 设置 -> 安全认证:开启防护、设置密码、管理免密 Token、解锁/锁定管理面。未认证访客在
77
+ 任何页面内容返回之前就会被拒绝。
78
+
79
+ ### IM 机器人
80
+
81
+ 微信 / QQ / 飞书 / Telegram 使用指南:
82
+ [docs/wechat-usage.md](./docs/wechat-usage.md)、[docs/qq-usage.md](./docs/qq-usage.md)、
83
+ [docs/feishu-usage.md](./docs/feishu-usage.md)、[docs/telegram-usage.md](./docs/telegram-usage.md)。
84
+
85
+ ## 安全说明
86
+
87
+ - 直连网关始终强制 `public_only` 式门禁:外部访客与局域网访问共用同一 AuthManager 认证,
88
+ 仅限 loopback 的资源绝不经网关暴露。
89
+ - RPC 通道经由宿主 web server 注册并通过请求拒绝(requestRejection)鉴权,
90
+ 伪造或未认证的通道调用在入口即被丢弃。
91
+ - 自签证书只加密传输、不提供身份:拿到密码的人就能登录——请设置强密码并开启管理员锁定。
92
+
93
+ ## 开发
94
+
95
+ ```sh
96
+ git clone https://github.com/lament-z/dsh-bridge-gateway
97
+ cd dsh-bridge-gateway
98
+ npm install
99
+ npm run build:client
100
+ npm test
101
+ dsh plugin --profile web add .
102
+ ```
103
+
104
+ ## 发布说明
105
+
106
+ 打 `v*` 标签推送到 GitHub 即自动 `npm publish`(GitHub Actions,需仓库配置 `NPM_TOKEN` secret)。
107
+
108
+ ## 开源协议
109
+
110
+ [MIT](./LICENSE)
package/client/client.js CHANGED
@@ -3829,6 +3829,54 @@ function injectMobileStyles() {
3829
3829
  display: none !important;
3830
3830
  }
3831
3831
 
3832
+ /* ---- \u9876\u680F\u300C\u6807\u51C6\u6A21\u5F0F\u300D\u4E0E\u300C\u5BF9\u8BDD\u7BA1\u7406\u300D\u91CD\u53E0\u4FEE\u590D ----
3833
+ \u5B9E\u6D4B\uFF08390px\uFF09\uFF1A\u6807\u51C6\u6A21\u5F0F(\u5728 titleCluster \u5185) \u53F3\u8FB9\u754C 102\uFF0C
3834
+ \u5BF9\u8BDD\u7BA1\u7406(\u5728 headerUtilities \u5185) \u5DE6\u8FB9\u754C 91 \u2192 \u6C34\u5E73\u91CD\u53E0 11px\u3002
3835
+ \u6839\u56E0\uFF1AtitleCluster \u53EA\u6709 46px \u5BBD\uFF08[33,79]\uFF09\uFF0C\u4F46\u5185\u90E8\u7684\u300C\u6807\u51C6\u6A21\u5F0F\u300D\u80F6\u56CA\u56FA\u5B9A 69px\uFF0C
3836
+ \u6491\u7834\u7236\u5BB9\u5668\u540E\u4E0E\u53F3\u4FA7 headerUtilities \u76F8\u649E\u3002
3837
+ \u4FEE\u590D\uFF1A\u5141\u8BB8 titleRow \u6362\u884C + \u8BA9\u4E24\u4FA7\u5404\u81EA\u53EF\u6536\u7F29\u5E76\u9650\u5BBD\u3002 */
3838
+ div[class*="wSkVaW_titleRow"],
3839
+ div[class*="_titleRow"] {
3840
+ flex-wrap: wrap !important;
3841
+ row-gap: 6px !important;
3842
+ column-gap: 8px !important;
3843
+ overflow: visible !important;
3844
+ }
3845
+ div[class*="wSkVaW_titleCluster"],
3846
+ div[class*="_titleCluster"] {
3847
+ flex: 1 1 auto !important;
3848
+ min-width: 0 !important;
3849
+ max-width: 100% !important;
3850
+ }
3851
+ div[class*="wSkVaW_headerActions"],
3852
+ div[class*="_headerActions"] {
3853
+ flex: 0 0 auto !important;
3854
+ min-width: 0 !important;
3855
+ max-width: 45vw !important;
3856
+ overflow: hidden !important;
3857
+ }
3858
+ div[class*="wSkVaW_headerUtilities"],
3859
+ div[class*="_headerUtilities"] {
3860
+ flex: 0 0 auto !important;
3861
+ min-width: 0 !important;
3862
+ max-width: 50vw !important;
3863
+ margin-left: auto !important;
3864
+ overflow: hidden !important;
3865
+ }
3866
+ /* \u4E24\u8005\u5185\u90E8\u7684\u65E0 class \u6309\u94AE\uFF08\u6A21\u5F0F\u80F6\u56CA / \u5BF9\u8BDD\u7BA1\u7406 / \u5220\u9664\u672C\u5BF9\u8BDD\uFF09\u7EDF\u4E00\u9650\u5BBD\u5E76\u5141\u8BB8\u6536\u7F29 */
3867
+ div[class*="wSkVaW_headerActions"] > *,
3868
+ div[class*="wSkVaW_headerActions"] button,
3869
+ div[class*="wSkVaW_headerActions"] span,
3870
+ div[class*="wSkVaW_headerUtilities"] > *,
3871
+ div[class*="wSkVaW_headerUtilities"] button,
3872
+ div[class*="wSkVaW_headerUtilities"] span {
3873
+ max-width: 100% !important;
3874
+ min-width: 0 !important;
3875
+ overflow: hidden !important;
3876
+ text-overflow: ellipsis !important;
3877
+ white-space: nowrap !important;
3878
+ }
3879
+
3832
3880
  /* \u5B50\u4EE3\u7406/\u667A\u80FD\u4F53\u6A21\u5F0F\u80F6\u56CA (Actions)\uFF1A\u7D27\u51D1\u5706\u89D2\u80F6\u56CA */
3833
3881
  div[class*="wSkVaW_headerActions"],
3834
3882
  div[class*="headerActions"] {
@@ -3939,6 +3987,136 @@ function injectMobileStyles() {
3939
3987
  box-sizing: border-box !important;
3940
3988
  }
3941
3989
 
3990
+ /* ---- \u8F93\u5165\u6846\u5BBD\u5EA6\u4E0E\u5BF9\u8BDD\u5217\u5BF9\u9F50\u4FEE\u590D ----
3991
+ \u95EE\u9898\uFF1A\u8FDB\u5165\u771F\u5B9E\u5BF9\u8BDD\u540E\u8F93\u5165\u6846\u88AB\u6491\u5230 780px\uFF08= 748px \u5185\u5BB9\u5BBD + 32px\uFF09\uFF0C
3992
+ \u800C\u6D88\u606F\u5217\u4EC5 318px\uFF0C\u8F93\u5165\u6846\u6BD4\u5BF9\u8BDD\u5185\u5BB9\u5BBD\u7EA6 2.45 \u500D\u3002
3993
+ \u6839\u56E0\uFF1A\u5361\u7247\u7684\u7236\u7EA7 uV2eYG_root \u81EA\u5E26 max-width: var(--dsh-composer-card-max-width)\uFF0C
3994
+ \u5361\u7247\u81EA\u8EAB\u7684 max-width:100% \u662F\u76F8\u5BF9\u7236\u7EA7 780px \u8BA1\u7B97\u7684\uFF0C\u56E0\u6B64\u4ECD\u662F 780px\u3002
3995
+ \u4FEE\u590D\uFF1A\u4ECE\u6839\u53D8\u91CF\u6536\u7A84 + \u8865\u4E0A uV2eYG_root \u8FD9\u4E00\u5C42\u7EA6\u675F\u3002 */
3996
+
3997
+ /* 1. \u4ECE\u4F1A\u8BDD\u6839\u8282\u70B9\u6536\u7A84\u5185\u5BB9\u5BBD\u5EA6\u53D8\u91CF\uFF08\u540C\u65F6\u7EA6\u675F\u6D88\u606F\u5217\u4E0E\u8F93\u5165\u6846\uFF09 */
3998
+ div[class*="wSkVaW_root"] {
3999
+ --dsh-chat-content-width: min(748px, 100vw - 32px) !important;
4000
+ --dsh-composer-card-max-width: min(780px, 100vw - 24px) !important;
4001
+ }
4002
+
4003
+ /* 2. \u8865\u4E0A\u63D2\u4EF6\u6B64\u524D\u9057\u6F0F\u7684 uV2eYG_root\uFF08\u8F93\u5165\u6846\u7684\u76F4\u63A5\u7F29\u5BB9\u5668\uFF09 */
4004
+ div[class*="uV2eYG_root"] {
4005
+ max-width: 100% !important;
4006
+ min-width: 0 !important;
4007
+ box-sizing: border-box !important;
4008
+ }
4009
+
4010
+ /* 3. \u8F93\u5165\u6846\u7956\u5148\u94FE\u6574\u4F53\u4E0D\u8D85\u51FA\u89C6\u53E3 */
4011
+ div[class*="wSkVaW_composerStack"],
4012
+ div[class*="wSkVaW_composerSeat"],
4013
+ div[class*="uV2eYG_scroll"],
4014
+ div[class*="uV2eYG_grow"] {
4015
+ max-width: 100% !important;
4016
+ min-width: 0 !important;
4017
+ box-sizing: border-box !important;
4018
+ }
4019
+
4020
+ /* ---- \u9876\u90E8\u63A7\u4EF6\u9632\u6EA2\u51FA\u88C1\u5207\uFF08320px \u4E0B main \u5206\u652F chip \u6EA2\u51FA 39px \u88AB\u88C1\uFF09 ---- */
4021
+ div[class*="wSkVaW_titleRow"],
4022
+ div[class*="_titleRow"] {
4023
+ flex-wrap: wrap !important;
4024
+ row-gap: 6px !important;
4025
+ overflow: visible !important;
4026
+ }
4027
+ button[class*="_7rgC5q_chip"],
4028
+ button[class*="cubgiG_seat"],
4029
+ div[class*="_7rgC5q_chipWrap"] {
4030
+ max-width: min(120px, 30vw) !important;
4031
+ min-width: 0 !important;
4032
+ overflow: hidden !important;
4033
+ text-overflow: ellipsis !important;
4034
+ }
4035
+
4036
+ /* git-graph \u7684\u5206\u652F chip \u5916\u5C42\u662F position:absolute \u6D6E\u5C42\uFF08_7rgC5q_anchor / _7rgC5q_anchorHero\uFF09\uFF0C
4037
+ \u4E14\u901A\u8FC7\u5185\u8054\u6837\u5F0F\u5199\u6B7B left\uFF08\u5982 left: 271.484px\uFF09\uFF0C\u8131\u79BB flex \u6D41\u4E14\u4E0D\u53D7\u7236\u5BB9\u5668\u9650\u5BBD\u7EA6\u675F\uFF0C
4038
+ \u5728 320px \u4E0B\u6EA2\u51FA\u5C4F\u5E55\u53F3\u4FA7\u88AB\u88C1\u5207\uFF08\u53F3\u8FB9\u754C 351 > \u89C6\u53E3 320\uFF09\u3002
4039
+ \u5185\u8054\u6837\u5F0F\u4F18\u5148\u7EA7\u9AD8\u4E8E\u666E\u901A CSS\uFF0C\u5FC5\u987B\u7528 !important \u8986\u76D6 left\uFF0C\u6539\u4E3A\u6309\u89C6\u53E3\u53F3\u8FB9\u754C\u951A\u5B9A\u3002 */
4040
+ div[class*="_7rgC5q_anchor"] {
4041
+ left: auto !important;
4042
+ right: 8px !important;
4043
+ max-width: calc(100vw - 16px) !important;
4044
+ min-width: 0 !important;
4045
+ overflow: hidden !important;
4046
+ }
4047
+ button[class*="_7rgC5q_chip"],
4048
+ div[class*="_7rgC5q_chipWrap"] {
4049
+ width: auto !important;
4050
+ max-width: 100% !important;
4051
+ }
4052
+
4053
+ /* ---- \u70B9\u51FB\u76EE\u6807\u6269\u5230 44px\uFF08\u4F2A\u5143\u7D20\u6269\u70ED\u533A\uFF0C\u89C6\u89C9\u5C3A\u5BF8\u4E0D\u53D8\uFF09 ---- */
4054
+ button[class*="cubgiG_seat"],
4055
+ button[class*="_7rgC5q_chip"],
4056
+ button[class*="pXSMma_workspace"],
4057
+ button[class*="Sh0Q9G_trigger"],
4058
+ button[class*="_7KE1Ra_trigger"],
4059
+ button[class*="uV2eYG_add"],
4060
+ button[class*="qDHVXG_iconButton"],
4061
+ button[class*="qDHVXG_searchButton"],
4062
+ button[class*="hHd-Xa_iconButton"],
4063
+ button[class*="YDXeBa_iconButton"] {
4064
+ position: relative !important;
4065
+ }
4066
+ button[class*="cubgiG_seat"]::after,
4067
+ button[class*="_7rgC5q_chip"]::after,
4068
+ button[class*="pXSMma_workspace"]::after,
4069
+ button[class*="Sh0Q9G_trigger"]::after,
4070
+ button[class*="_7KE1Ra_trigger"]::after,
4071
+ button[class*="uV2eYG_add"]::after,
4072
+ button[class*="qDHVXG_iconButton"]::after,
4073
+ button[class*="qDHVXG_searchButton"]::after,
4074
+ button[class*="hHd-Xa_iconButton"]::after,
4075
+ button[class*="YDXeBa_iconButton"]::after {
4076
+ content: "" !important;
4077
+ position: absolute !important;
4078
+ inset: -8px !important;
4079
+ z-index: 1 !important;
4080
+ }
4081
+
4082
+ /* ---- \u4FA7\u8FB9\u680F\u771F\u6B63\u9690\u85CF\uFF0C\u6D88\u9664\u952E\u76D8/\u8BFB\u5C4F\u7126\u70B9\u9677\u9631 ----
4083
+ \u6B64\u524D\u4FA7\u8FB9\u680F\u4EC5\u88AB transform \u79FB\u5230\u5C4F\u5E55\u5916\uFF08visibility \u4ECD\u4E3A visible\uFF09\uFF0C
4084
+ \u5185\u90E8 7 \u4E2A\u6309\u94AE\u4ECD\u53EF\u88AB Tab \u805A\u7126\u3002 */
4085
+ div[class*="_sidebarCol"] {
4086
+ visibility: hidden !important;
4087
+ pointer-events: none !important;
4088
+ }
4089
+ body.dsh-drawer-open div[class*="_sidebarCol"] {
4090
+ visibility: visible !important;
4091
+ pointer-events: auto !important;
4092
+ }
4093
+
4094
+ /* ---- \u8F93\u5165\u6846 sticky \u5B9A\u4F4D\u4E0E\u906E\u6321\u4FEE\u590D ----
4095
+ \u73B0\u8C61\uFF1A\u53D1\u9001\u540E\u8F93\u5165\u6846\u8DF3\u5230 tabs(\u5BF9\u8BDD/\u8F68\u8FF9/\u4E0A\u4E0B\u6587/\u8BB0\u5FC6) \u4E0B\u65B9\uFF0C\u4E0B\u65B9\u7559\u51FA\u5927\u7247\u7A7A\u767D\uFF1B
4096
+ \u6EDA\u52A8\u65F6\u53F3\u4E0B\u89D2\u51FA\u73B0\u4E00\u4E2A\u5C0F\u65B9\u5757\u3002
4097
+ \u6839\u56E0\uFF1A
4098
+ 1) dsh-better-sidebar \u63D2\u4EF6\u6CE8\u5165\u7684 div[data-dsh-panel-host] \u662F\u5168\u5C4F fixed \u906E\u7F69
4099
+ \uFF08390x844\uFF0Cz-index 25\uFF09\uFF0C\u5728\u79FB\u52A8\u7AEF\u672A\u88AB\u9690\u85CF\uFF0C\u538B\u5728 composerSeat(z-index 7) \u4E4B\u4E0A\uFF1B
4100
+ 2) \u4E3B\u9898\u88C5\u9970\u5C42 span[data-dsh-glass-fade="bottom"] \u662F\u5168\u5BBD fixed \u6761\uFF08390x13\uFF0Cz-index 7\uFF09\uFF0C
4101
+ \u5E38\u9A7B\u5C4F\u5E55\u53F3\u4E0B\u89D2\uFF0C\u89C6\u89C9\u4E0A\u5C31\u662F\u90A3\u4E2A"\u5C0F\u65B9\u5757"\u3002
4102
+ \u4FEE\u590D\uFF1A\u79FB\u52A8\u7AEF\u9690\u85CF panel-host \u906E\u7F69\u4E0E glass \u6E10\u53D8\u88C5\u9970\u6761\uFF0C\u5E76\u9501\u5B9A\u8F93\u5165\u6846 sticky \u4E8E\u5E95\u90E8\u3002 */
4103
+
4104
+ /* \u9690\u85CF better-sidebar \u7684\u5168\u5C4F\u906E\u7F69\uFF08\u907F\u514D\u8986\u76D6\u8F93\u5165\u6846\u5E76\u62E6\u622A\u70B9\u51FB\uFF09 */
4105
+ div[data-dsh-panel-host],
4106
+ div[data-dsh-better-sidebar] {
4107
+ display: none !important;
4108
+ }
4109
+ /* \u9690\u85CF\u4E3B\u9898\u88C5\u9970\u6027\u6E10\u53D8\u6761\uFF08\u53F3\u4E0B\u89D2\u5C0F\u65B9\u5757\uFF09 */
4110
+ span[data-dsh-glass-fade] {
4111
+ display: none !important;
4112
+ }
4113
+ /* \u6CE8\u610F\uFF1A\u4EE5\u4E0B\u4E24\u5904\u6539\u52A8\u5747\u5DF2\u5B9E\u6D4B\u5931\u8D25\u5E76\u64A4\u56DE\uFF0C\u5207\u52FF\u91CD\u8BD5\u2014\u2014
4114
+ (1) \u628A viewArea \u6539\u4E3A\u53EF\u6536\u7F29\uFF08flex:1 1 auto / min-height:0\uFF09\uFF1A
4115
+ sticky \u5931\u53BB\u6B63\u786E\u6EDA\u52A8\u4E0A\u4E0B\u6587\uFF0C\u8F93\u5165\u6846\u88AB\u9876\u5230\u6574\u4E2A\u5BF9\u8BDD\u5386\u53F2\u6700\u4E0A\u65B9\u3002
4116
+ (2) \u628A composerSeat \u6539\u4E3A absolute\uFF08\u4EFF\u6E90\u7801 composer-overlay \u5206\u652F\uFF09\uFF1A
4117
+ absolute \u8131\u79BB\u6587\u6863\u6D41\u540E\u968F\u6EDA\u52A8\u5185\u5BB9\u79FB\u52A8\uFF0C\u5B9E\u6D4B\u8F93\u5165\u6846\u8DD1\u5230 y=-8972\u3002
4118
+ \u7ED3\u8BBA\uFF1A\u4FDD\u7559\u6E90\u7801\u7684 sticky \u673A\u5236\uFF0C\u4E0D\u505A\u7ED3\u6784\u6027\u6539\u52A8\u3002 */
4119
+
3942
4120
  /* \u8F93\u5165\u6846\u5E95\u90E8\u5DE5\u5177\u680F\uFF1A\u5F39\u6027\u81EA\u9002\u5E94\uFF0C\u5F7B\u5E95\u675C\u7EDD\u6743\u9650\u9009\u62E9\u5668(Full access)\u4E0E\u6A21\u578B\u9009\u62E9\u5668\u91CD\u53E0\u78B0\u649E */
3943
4121
  div[class*="uV2eYG_row"] {
3944
4122
  display: flex !important;
@@ -4033,8 +4211,16 @@ function injectMobileStyles() {
4033
4211
  position: fixed !important;
4034
4212
  left: 0 !important;
4035
4213
  top: 0 !important;
4036
- bottom: 0 !important;
4214
+ bottom: auto !important;
4215
+ /* \u4FA7\u8FB9\u680F\u5E95\u90E8\u6EA2\u51FA\u5C4F\u5E55\u4FEE\u590D\uFF1A
4216
+ \u5B9E\u6D4B 390x844 \u4E0B\uFF0C\u4FA7\u8FB9\u680F\u5B9E\u9645\u76D2\u5B50\u4E3A T=12 B=882\uFF08\u9AD8\u5EA6 870 > \u89C6\u53E3 844\uFF09\uFF0C\u5E95\u90E8\u6EA2\u51FA 38px\u3002
4217
+ \u6839\u56E0\u662F\u63D2\u4EF6\u6B64\u524D\u672A\u58F0\u660E box-sizing\uFF0C\u5143\u7D20\u7EE7\u627F\u4E3A content-box\uFF0C
4218
+ \u4E8E\u662F padding(10px+14px) \u4E0E margin(12px) \u88AB\u52A0\u5728 height:100dvh \u4E4B\u5916\u3002
4219
+ \u6539\u4E3A border-box \u5E76\u7528 100dvh \u76F4\u63A5\u7EA6\u675F\u9AD8\u5EA6\uFF0Cpadding \u5373\u88AB\u5305\u542B\u5728\u9AD8\u5EA6\u5185\u3002 */
4220
+ box-sizing: border-box !important;
4037
4221
  height: 100dvh !important;
4222
+ max-height: 100dvh !important;
4223
+ margin: 0 !important;
4038
4224
  width: 290px !important;
4039
4225
  max-width: 82vw !important;
4040
4226
  z-index: 10000 !important;
@@ -4552,6 +4738,9 @@ function setupMobileExperience(rpcCall, ctx) {
4552
4738
  }
4553
4739
  const sessionRow = e.target.closest('a, div[class*="sessionRow"], div[role="treeitem"]');
4554
4740
  if (sessionRow) {
4741
+ if (sessionRow.hasAttribute("aria-expanded")) {
4742
+ return;
4743
+ }
4555
4744
  setTimeout(() => {
4556
4745
  if (document.body.classList.contains("dsh-drawer-open")) {
4557
4746
  document.body.classList.remove("dsh-drawer-open");
@@ -4562,6 +4751,31 @@ function setupMobileExperience(rpcCall, ctx) {
4562
4751
  document.body.classList.remove("dsh-drawer-open");
4563
4752
  }
4564
4753
  }, true);
4754
+ const syncSidebarInert = () => {
4755
+ const sidebar = document.querySelector('div[class*="_sidebarCol"]');
4756
+ if (!sidebar) return;
4757
+ const open = document.body.classList.contains("dsh-drawer-open");
4758
+ const wantInert = window.innerWidth <= 768 && !open;
4759
+ if (wantInert) {
4760
+ if (!sidebar.hasAttribute("inert")) sidebar.setAttribute("inert", "");
4761
+ } else if (sidebar.hasAttribute("inert")) {
4762
+ sidebar.removeAttribute("inert");
4763
+ }
4764
+ };
4765
+ new MutationObserver(syncSidebarInert).observe(document.body, {
4766
+ attributes: true,
4767
+ attributeFilter: ["class"],
4768
+ childList: true,
4769
+ subtree: true
4770
+ });
4771
+ window.addEventListener("resize", syncSidebarInert);
4772
+ let inertRetry = 0;
4773
+ const inertTimer = setInterval(() => {
4774
+ syncSidebarInert();
4775
+ const sb = document.querySelector('div[class*="_sidebarCol"]');
4776
+ if (sb && sb.hasAttribute("inert") || ++inertRetry > 40) clearInterval(inertTimer);
4777
+ }, 250);
4778
+ syncSidebarInert();
4565
4779
  let longPressTimer = null;
4566
4780
  let touchStartX = 0;
4567
4781
  let touchStartY = 0;
package/client/index.js CHANGED
@@ -3426,6 +3426,54 @@ function injectMobileStyles() {
3426
3426
  display: none !important;
3427
3427
  }
3428
3428
 
3429
+ /* ---- 顶栏「标准模式」与「对话管理」重叠修复 ----
3430
+ 实测(390px):标准模式(在 titleCluster 内) 右边界 102,
3431
+ 对话管理(在 headerUtilities 内) 左边界 91 → 水平重叠 11px。
3432
+ 根因:titleCluster 只有 46px 宽([33,79]),但内部的「标准模式」胶囊固定 69px,
3433
+ 撑破父容器后与右侧 headerUtilities 相撞。
3434
+ 修复:允许 titleRow 换行 + 让两侧各自可收缩并限宽。 */
3435
+ div[class*="wSkVaW_titleRow"],
3436
+ div[class*="_titleRow"] {
3437
+ flex-wrap: wrap !important;
3438
+ row-gap: 6px !important;
3439
+ column-gap: 8px !important;
3440
+ overflow: visible !important;
3441
+ }
3442
+ div[class*="wSkVaW_titleCluster"],
3443
+ div[class*="_titleCluster"] {
3444
+ flex: 1 1 auto !important;
3445
+ min-width: 0 !important;
3446
+ max-width: 100% !important;
3447
+ }
3448
+ div[class*="wSkVaW_headerActions"],
3449
+ div[class*="_headerActions"] {
3450
+ flex: 0 0 auto !important;
3451
+ min-width: 0 !important;
3452
+ max-width: 45vw !important;
3453
+ overflow: hidden !important;
3454
+ }
3455
+ div[class*="wSkVaW_headerUtilities"],
3456
+ div[class*="_headerUtilities"] {
3457
+ flex: 0 0 auto !important;
3458
+ min-width: 0 !important;
3459
+ max-width: 50vw !important;
3460
+ margin-left: auto !important;
3461
+ overflow: hidden !important;
3462
+ }
3463
+ /* 两者内部的无 class 按钮(模式胶囊 / 对话管理 / 删除本对话)统一限宽并允许收缩 */
3464
+ div[class*="wSkVaW_headerActions"] > *,
3465
+ div[class*="wSkVaW_headerActions"] button,
3466
+ div[class*="wSkVaW_headerActions"] span,
3467
+ div[class*="wSkVaW_headerUtilities"] > *,
3468
+ div[class*="wSkVaW_headerUtilities"] button,
3469
+ div[class*="wSkVaW_headerUtilities"] span {
3470
+ max-width: 100% !important;
3471
+ min-width: 0 !important;
3472
+ overflow: hidden !important;
3473
+ text-overflow: ellipsis !important;
3474
+ white-space: nowrap !important;
3475
+ }
3476
+
3429
3477
  /* 子代理/智能体模式胶囊 (Actions):紧凑圆角胶囊 */
3430
3478
  div[class*="wSkVaW_headerActions"],
3431
3479
  div[class*="headerActions"] {
@@ -3536,6 +3584,136 @@ function injectMobileStyles() {
3536
3584
  box-sizing: border-box !important;
3537
3585
  }
3538
3586
 
3587
+ /* ---- 输入框宽度与对话列对齐修复 ----
3588
+ 问题:进入真实对话后输入框被撑到 780px(= 748px 内容宽 + 32px),
3589
+ 而消息列仅 318px,输入框比对话内容宽约 2.45 倍。
3590
+ 根因:卡片的父级 uV2eYG_root 自带 max-width: var(--dsh-composer-card-max-width),
3591
+ 卡片自身的 max-width:100% 是相对父级 780px 计算的,因此仍是 780px。
3592
+ 修复:从根变量收窄 + 补上 uV2eYG_root 这一层约束。 */
3593
+
3594
+ /* 1. 从会话根节点收窄内容宽度变量(同时约束消息列与输入框) */
3595
+ div[class*="wSkVaW_root"] {
3596
+ --dsh-chat-content-width: min(748px, 100vw - 32px) !important;
3597
+ --dsh-composer-card-max-width: min(780px, 100vw - 24px) !important;
3598
+ }
3599
+
3600
+ /* 2. 补上插件此前遗漏的 uV2eYG_root(输入框的直接缩容器) */
3601
+ div[class*="uV2eYG_root"] {
3602
+ max-width: 100% !important;
3603
+ min-width: 0 !important;
3604
+ box-sizing: border-box !important;
3605
+ }
3606
+
3607
+ /* 3. 输入框祖先链整体不超出视口 */
3608
+ div[class*="wSkVaW_composerStack"],
3609
+ div[class*="wSkVaW_composerSeat"],
3610
+ div[class*="uV2eYG_scroll"],
3611
+ div[class*="uV2eYG_grow"] {
3612
+ max-width: 100% !important;
3613
+ min-width: 0 !important;
3614
+ box-sizing: border-box !important;
3615
+ }
3616
+
3617
+ /* ---- 顶部控件防溢出裁切(320px 下 main 分支 chip 溢出 39px 被裁) ---- */
3618
+ div[class*="wSkVaW_titleRow"],
3619
+ div[class*="_titleRow"] {
3620
+ flex-wrap: wrap !important;
3621
+ row-gap: 6px !important;
3622
+ overflow: visible !important;
3623
+ }
3624
+ button[class*="_7rgC5q_chip"],
3625
+ button[class*="cubgiG_seat"],
3626
+ div[class*="_7rgC5q_chipWrap"] {
3627
+ max-width: min(120px, 30vw) !important;
3628
+ min-width: 0 !important;
3629
+ overflow: hidden !important;
3630
+ text-overflow: ellipsis !important;
3631
+ }
3632
+
3633
+ /* git-graph 的分支 chip 外层是 position:absolute 浮层(_7rgC5q_anchor / _7rgC5q_anchorHero),
3634
+ 且通过内联样式写死 left(如 left: 271.484px),脱离 flex 流且不受父容器限宽约束,
3635
+ 在 320px 下溢出屏幕右侧被裁切(右边界 351 > 视口 320)。
3636
+ 内联样式优先级高于普通 CSS,必须用 !important 覆盖 left,改为按视口右边界锚定。 */
3637
+ div[class*="_7rgC5q_anchor"] {
3638
+ left: auto !important;
3639
+ right: 8px !important;
3640
+ max-width: calc(100vw - 16px) !important;
3641
+ min-width: 0 !important;
3642
+ overflow: hidden !important;
3643
+ }
3644
+ button[class*="_7rgC5q_chip"],
3645
+ div[class*="_7rgC5q_chipWrap"] {
3646
+ width: auto !important;
3647
+ max-width: 100% !important;
3648
+ }
3649
+
3650
+ /* ---- 点击目标扩到 44px(伪元素扩热区,视觉尺寸不变) ---- */
3651
+ button[class*="cubgiG_seat"],
3652
+ button[class*="_7rgC5q_chip"],
3653
+ button[class*="pXSMma_workspace"],
3654
+ button[class*="Sh0Q9G_trigger"],
3655
+ button[class*="_7KE1Ra_trigger"],
3656
+ button[class*="uV2eYG_add"],
3657
+ button[class*="qDHVXG_iconButton"],
3658
+ button[class*="qDHVXG_searchButton"],
3659
+ button[class*="hHd-Xa_iconButton"],
3660
+ button[class*="YDXeBa_iconButton"] {
3661
+ position: relative !important;
3662
+ }
3663
+ button[class*="cubgiG_seat"]::after,
3664
+ button[class*="_7rgC5q_chip"]::after,
3665
+ button[class*="pXSMma_workspace"]::after,
3666
+ button[class*="Sh0Q9G_trigger"]::after,
3667
+ button[class*="_7KE1Ra_trigger"]::after,
3668
+ button[class*="uV2eYG_add"]::after,
3669
+ button[class*="qDHVXG_iconButton"]::after,
3670
+ button[class*="qDHVXG_searchButton"]::after,
3671
+ button[class*="hHd-Xa_iconButton"]::after,
3672
+ button[class*="YDXeBa_iconButton"]::after {
3673
+ content: "" !important;
3674
+ position: absolute !important;
3675
+ inset: -8px !important;
3676
+ z-index: 1 !important;
3677
+ }
3678
+
3679
+ /* ---- 侧边栏真正隐藏,消除键盘/读屏焦点陷阱 ----
3680
+ 此前侧边栏仅被 transform 移到屏幕外(visibility 仍为 visible),
3681
+ 内部 7 个按钮仍可被 Tab 聚焦。 */
3682
+ div[class*="_sidebarCol"] {
3683
+ visibility: hidden !important;
3684
+ pointer-events: none !important;
3685
+ }
3686
+ body.dsh-drawer-open div[class*="_sidebarCol"] {
3687
+ visibility: visible !important;
3688
+ pointer-events: auto !important;
3689
+ }
3690
+
3691
+ /* ---- 输入框 sticky 定位与遮挡修复 ----
3692
+ 现象:发送后输入框跳到 tabs(对话/轨迹/上下文/记忆) 下方,下方留出大片空白;
3693
+ 滚动时右下角出现一个小方块。
3694
+ 根因:
3695
+ 1) dsh-better-sidebar 插件注入的 div[data-dsh-panel-host] 是全屏 fixed 遮罩
3696
+ (390x844,z-index 25),在移动端未被隐藏,压在 composerSeat(z-index 7) 之上;
3697
+ 2) 主题装饰层 span[data-dsh-glass-fade="bottom"] 是全宽 fixed 条(390x13,z-index 7),
3698
+ 常驻屏幕右下角,视觉上就是那个"小方块"。
3699
+ 修复:移动端隐藏 panel-host 遮罩与 glass 渐变装饰条,并锁定输入框 sticky 于底部。 */
3700
+
3701
+ /* 隐藏 better-sidebar 的全屏遮罩(避免覆盖输入框并拦截点击) */
3702
+ div[data-dsh-panel-host],
3703
+ div[data-dsh-better-sidebar] {
3704
+ display: none !important;
3705
+ }
3706
+ /* 隐藏主题装饰性渐变条(右下角小方块) */
3707
+ span[data-dsh-glass-fade] {
3708
+ display: none !important;
3709
+ }
3710
+ /* 注意:以下两处改动均已实测失败并撤回,切勿重试——
3711
+ (1) 把 viewArea 改为可收缩(flex:1 1 auto / min-height:0):
3712
+ sticky 失去正确滚动上下文,输入框被顶到整个对话历史最上方。
3713
+ (2) 把 composerSeat 改为 absolute(仿源码 composer-overlay 分支):
3714
+ absolute 脱离文档流后随滚动内容移动,实测输入框跑到 y=-8972。
3715
+ 结论:保留源码的 sticky 机制,不做结构性改动。 */
3716
+
3539
3717
  /* 输入框底部工具栏:弹性自适应,彻底杜绝权限选择器(Full access)与模型选择器重叠碰撞 */
3540
3718
  div[class*="uV2eYG_row"] {
3541
3719
  display: flex !important;
@@ -3630,8 +3808,16 @@ function injectMobileStyles() {
3630
3808
  position: fixed !important;
3631
3809
  left: 0 !important;
3632
3810
  top: 0 !important;
3633
- bottom: 0 !important;
3811
+ bottom: auto !important;
3812
+ /* 侧边栏底部溢出屏幕修复:
3813
+ 实测 390x844 下,侧边栏实际盒子为 T=12 B=882(高度 870 > 视口 844),底部溢出 38px。
3814
+ 根因是插件此前未声明 box-sizing,元素继承为 content-box,
3815
+ 于是 padding(10px+14px) 与 margin(12px) 被加在 height:100dvh 之外。
3816
+ 改为 border-box 并用 100dvh 直接约束高度,padding 即被包含在高度内。 */
3817
+ box-sizing: border-box !important;
3634
3818
  height: 100dvh !important;
3819
+ max-height: 100dvh !important;
3820
+ margin: 0 !important;
3635
3821
  width: 290px !important;
3636
3822
  max-width: 82vw !important;
3637
3823
  z-index: 10000 !important;
@@ -4193,8 +4379,16 @@ function setupMobileExperience(rpcCall, ctx) {
4193
4379
  }
4194
4380
 
4195
4381
  // 点击会话项后平滑收起抽屉
4382
+ // 注意:工作区「文件夹」与「会话」都是 role="treeitem",但语义完全不同:
4383
+ // 文件夹 → 带 aria-expanded,onClick 是 onToggle(展开/折叠)
4384
+ // 会话 → 无 aria-expanded,onClick 是切换会话
4385
+ // 此前不区分二者,导致点击文件夹(只是想展开)也会立刻收起抽屉。
4196
4386
  const sessionRow = e.target.closest('a, div[class*="sessionRow"], div[role="treeitem"]');
4197
4387
  if (sessionRow) {
4388
+ // 文件夹:点击是为了展开/折叠,保持抽屉打开
4389
+ if (sessionRow.hasAttribute('aria-expanded')) {
4390
+ return;
4391
+ }
4198
4392
  setTimeout(() => {
4199
4393
  if (document.body.classList.contains('dsh-drawer-open')) {
4200
4394
  document.body.classList.remove('dsh-drawer-open');
@@ -4206,6 +4400,34 @@ function setupMobileExperience(rpcCall, ctx) {
4206
4400
  }
4207
4401
  }, true);
4208
4402
 
4403
+ // 3b. 抽屉关闭时,用 inert 彻底移除侧边栏内部控件的可聚焦性
4404
+ // CSS 的 visibility:hidden 不足以阻止 Tab 聚焦(实测仍可聚焦到屏幕外按钮),
4405
+ // inert 能同时阻断键盘焦点、指针事件与屏幕阅读器访问。
4406
+ const syncSidebarInert = () => {
4407
+ const sidebar = document.querySelector('div[class*="_sidebarCol"]');
4408
+ if (!sidebar) return;
4409
+ const open = document.body.classList.contains('dsh-drawer-open');
4410
+ const wantInert = window.innerWidth <= 768 && !open;
4411
+ if (wantInert) {
4412
+ if (!sidebar.hasAttribute('inert')) sidebar.setAttribute('inert', '');
4413
+ } else if (sidebar.hasAttribute('inert')) {
4414
+ sidebar.removeAttribute('inert');
4415
+ }
4416
+ };
4417
+ new MutationObserver(syncSidebarInert).observe(document.body, {
4418
+ attributes: true, attributeFilter: ['class'],
4419
+ childList: true, subtree: true,
4420
+ });
4421
+ window.addEventListener('resize', syncSidebarInert);
4422
+ // 侧边栏由 React 异步挂载,首帧执行时可能尚未存在,需轮询重试若干次
4423
+ let inertRetry = 0;
4424
+ const inertTimer = setInterval(() => {
4425
+ syncSidebarInert();
4426
+ const sb = document.querySelector('div[class*="_sidebarCol"]');
4427
+ if ((sb && sb.hasAttribute('inert')) || ++inertRetry > 40) clearInterval(inertTimer);
4428
+ }, 250);
4429
+ syncSidebarInert();
4430
+
4209
4431
  // 4. 移动端抽屉长按(Long Press >= 380ms)呼出操作菜单,以及左右滑动手势
4210
4432
  let longPressTimer = null;
4211
4433
  let touchStartX = 0;
package/lib/bridge-rpc.js CHANGED
@@ -73,15 +73,123 @@ function checkAdminAuth(authManager, payload) {
73
73
  return fail('bad-request', '操作已被拦截:需要管理员权限,请先在控制台输入管理密码解锁');
74
74
  }
75
75
 
76
+ // ---- RPC wire 助手(对齐 @deepseek-ai/dsh-client-connection 0.1.5 协议)----
77
+ // DSH 0.1.5 起 connection.rpc.handle 会在宿主 connection 上下文上访问 webServer
78
+ // (inject 守卫),第三方插件无法再从插件上下文调用;改为在自身 inject 的
79
+ // webServer 作用域注册等价 prefix 路由,认证走 connection.requestRejection
80
+ // (oh-my-dsh 升级卡 DSH-0.1.2-A1-08 的自定义通道配方),wire 格式保持不变。
81
+
82
+ const MAX_RPC_BODY_BYTES = 314572800;
83
+ const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.~-]+$/;
84
+
85
+ function endpointFromChannelPath(channel, pathname) {
86
+ if (!pathname.startsWith(`${channel}/`)) return undefined;
87
+ const endpoint = pathname.slice(channel.length + 1);
88
+ if (endpoint.split('/').some((s) => s === '' || s === '.' || s === '..' || !ENDPOINT_SEGMENT_PATTERN.test(s))) {
89
+ return undefined;
90
+ }
91
+ return endpoint;
92
+ }
93
+
94
+ function fullResponse(rpcId, result) {
95
+ return Response.json({ type: 'server-response', rpcId, result });
96
+ }
97
+
98
+ function errorResponse(rpcId, error) {
99
+ return fullResponse(rpcId, { ok: false, error });
100
+ }
101
+
102
+ async function bridgeRpcFetch(channel, handler, request) {
103
+ const endpoint = endpointFromChannelPath(channel, new URL(request.url).pathname);
104
+ if (request.method !== 'POST' || endpoint === undefined) {
105
+ return new Response('not found', { status: 404 });
106
+ }
107
+ if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
108
+ return new Response('content type must be application/json', { status: 415 });
109
+ }
110
+ let body;
111
+ try {
112
+ body = await request.json();
113
+ } catch {
114
+ return new Response('body is not JSON', { status: 400 });
115
+ }
116
+ const rpcId = typeof body?.rpcId === 'string' ? body.rpcId : 'invalid-request';
117
+ if (body?.type !== 'client-request' || body?.method !== endpoint) {
118
+ return errorResponse(rpcId, {
119
+ code: 'gateway/bad-request',
120
+ message: 'invalid client-request message',
121
+ details: { issues: [] },
122
+ });
123
+ }
124
+ try {
125
+ const result = await handler(endpoint, body.payload, request.signal);
126
+ return fullResponse(rpcId, result);
127
+ } catch (error) {
128
+ return new Response(`handler failure: ${String(error)}`, { status: 500 });
129
+ }
130
+ }
131
+
132
+ async function bridgeChannelRequest(req, res, connection, channel, handler) {
133
+ const rejection = typeof connection?.requestRejection === 'function' ? connection.requestRejection(req) : undefined;
134
+ if (rejection !== undefined) {
135
+ res.writeHead(rejection);
136
+ res.end(rejection === 401 ? 'unauthorized' : 'forbidden');
137
+ return;
138
+ }
139
+ const abort = new AbortController();
140
+ res.on('close', () => {
141
+ if (!res.writableEnded) abort.abort();
142
+ });
143
+ const url = new URL(req.url ?? '/', 'http://dsh.internal');
144
+ const headers = Object.fromEntries(Object.entries(req.headers).filter(([, v]) => typeof v === 'string'));
145
+ const chunks = [];
146
+ let received = 0;
147
+ for await (const chunk of req) {
148
+ received += chunk.byteLength;
149
+ if (received > MAX_RPC_BODY_BYTES) {
150
+ res.writeHead(413, { connection: 'close' });
151
+ res.end();
152
+ req.destroy();
153
+ return;
154
+ }
155
+ chunks.push(chunk);
156
+ }
157
+ const request = new Request(url, {
158
+ method: req.method ?? 'GET',
159
+ headers,
160
+ ...(chunks.length > 0 ? { body: Buffer.concat(chunks) } : {}),
161
+ signal: abort.signal,
162
+ });
163
+ const response = await bridgeRpcFetch(channel, handler, request);
164
+ const responseHeaders = Object.fromEntries(response.headers.entries());
165
+ res.writeHead(response.status, responseHeaders);
166
+ if (response.body === null) {
167
+ res.end();
168
+ return;
169
+ }
170
+ for await (const chunk of response.body) {
171
+ if (!res.write(chunk)) {
172
+ await new Promise((resolve) => {
173
+ const done = () => {
174
+ res.off('drain', done);
175
+ res.off('close', done);
176
+ resolve();
177
+ };
178
+ res.once('drain', done);
179
+ res.once('close', done);
180
+ });
181
+ }
182
+ }
183
+ res.end();
184
+ }
185
+
76
186
  export function installBridgeRpc(ctx, { service, authManager, wechat, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup }) {
77
- if (!ctx?.connection?.rpc?.handle) {
78
- logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
187
+ if (!ctx?.connection) {
188
+ logger.warn('dsh-bridge: Connection service unavailable — UI will not work');
79
189
  return () => {};
80
190
  }
81
191
 
82
- return ctx.connection.rpc.handle(
83
- BRIDGE_RPC_CHANNEL,
84
- async (endpoint, payload = {}, signal) => {
192
+ const handleBridgeRpc = async (endpoint, payload = {}, signal) => {
85
193
  if (signal?.aborted) return fail('cancelled', 'Request was cancelled');
86
194
 
87
195
  try {
@@ -546,8 +654,24 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
546
654
  logger.error('RPC endpoint %s failed: %s', endpoint, err.message);
547
655
  return fail('bad-request', err.message);
548
656
  }
549
- },
550
- { authority: 'loopback' }
551
- );
657
+ };
658
+
659
+ return ctx.inject(['connection', 'webServer'], (c) => {
660
+ const connection = c.connection;
661
+ return c.effect(() => {
662
+ const unregister = c.webServer.register({
663
+ kind: 'prefix',
664
+ path: BRIDGE_RPC_CHANNEL,
665
+ handler: (req, res) => {
666
+ void bridgeChannelRequest(req, res, connection, BRIDGE_RPC_CHANNEL, handleBridgeRpc);
667
+ },
668
+ });
669
+ return () => {
670
+ try {
671
+ unregister?.();
672
+ } catch {}
673
+ };
674
+ }, 'dsh-bridge: rpc channel');
675
+ });
552
676
  }
553
677
 
@@ -26862,12 +26862,19 @@ var require_index = __commonJS({
26862
26862
  return domain;
26863
26863
  }
26864
26864
  };
26865
- var fillApiPath = (apiPath, pathSupplement = {}) => apiPath.replace(/:([^/]+)/g, (_, $1) => {
26866
- if (pathSupplement[$1] !== void 0) {
26867
- return pathSupplement[$1];
26868
- }
26869
- throw new Error(`request miss ${$1} path argument`);
26870
- });
26865
+ var fillApiPath = (apiPath, pathSupplement = {}) => (
26866
+ // Only treat `:name` as a path parameter when it starts a path segment
26867
+ // (string start or right after `/`). Colons inside the URL authority,
26868
+ // e.g. `http://localhost:3000` or `user:pw@host`, must be left untouched.
26869
+ // The separator is captured and re-emitted instead of using a lookbehind
26870
+ // because the compile target (es6) has no lookbehind support.
26871
+ apiPath.replace(/(^|\/):([^/]+)/g, (_, pathPrefix, pathKey) => {
26872
+ if (pathSupplement[pathKey] !== void 0) {
26873
+ return `${pathPrefix}${pathSupplement[pathKey]}`;
26874
+ }
26875
+ throw new Error(`request miss ${pathKey} path argument`);
26876
+ })
26877
+ );
26871
26878
  var assert = (predication, callback) => __awaiter(void 0, void 0, void 0, function* () {
26872
26879
  const isInvoke = typeof predication === "function" ? predication() : predication;
26873
26880
  if (isInvoke) {
@@ -121095,6 +121102,9 @@ var require_index = __commonJS({
121095
121102
  }
121096
121103
  mergeData(params) {
121097
121104
  const { message_id, sum, seq, trace_id, data } = params;
121105
+ if (!Number.isInteger(sum) || sum <= 0 || !Number.isInteger(seq) || seq < 0 || seq >= sum) {
121106
+ throw new Error(`invalid event fragment metadata (sum: ${sum}, seq: ${seq})`);
121107
+ }
121098
121108
  const cache = this.cache.get(message_id);
121099
121109
  if (!cache) {
121100
121110
  const buffer = new Array(sum).fill(void 0);
@@ -121106,6 +121116,9 @@ var require_index = __commonJS({
121106
121116
  create_time: Date.now()
121107
121117
  });
121108
121118
  } else {
121119
+ if (sum !== cache.buffer.length) {
121120
+ throw new Error(`invalid event fragment metadata (sum: ${sum} differs from first fragment's ${cache.buffer.length}, seq: ${seq})`);
121121
+ }
121109
121122
  cache.buffer[seq] = data;
121110
121123
  }
121111
121124
  const mergedCache = this.cache.get(message_id);
@@ -121196,12 +121209,17 @@ var require_index = __commonJS({
121196
121209
  HttpStatusCode2[HttpStatusCode2["ok"] = 200] = "ok";
121197
121210
  HttpStatusCode2[HttpStatusCode2["internal_server_error"] = 500] = "internal_server_error";
121198
121211
  })(HttpStatusCode || (HttpStatusCode = {}));
121212
+ var describeError = (err) => {
121213
+ const message = err === null || err === void 0 ? void 0 : err.message;
121214
+ return message ? message : String(err);
121215
+ };
121199
121216
  var WSClient = class {
121200
121217
  constructor(params) {
121201
121218
  var _a;
121202
121219
  this.wsConfig = new WSConfig();
121203
121220
  this.reconnectGeneration = 0;
121204
121221
  this.isConnecting = false;
121222
+ this.closed = false;
121205
121223
  this.reconnectInfo = {
121206
121224
  lastConnectTime: 0,
121207
121225
  nextConnectTime: 0
@@ -121270,6 +121288,53 @@ var require_index = __commonJS({
121270
121288
  this.livenessTimer = void 0;
121271
121289
  }
121272
121290
  }
121291
+ /**
121292
+ * Detach a socket we are about to throw away: drop every listener, but
121293
+ * leave one 'error' sink behind.
121294
+ *
121295
+ * `ws` reports the abort of a non-OPEN socket asynchronously — both
121296
+ * `terminate()` and `close()` route through `abortHandshake()`, which
121297
+ * emits 'error' on a later tick — and a non-force `close()` on an OPEN
121298
+ * socket keeps the connection alive until the peer answers (up to `ws`'s
121299
+ * 30s closeTimeout), during which a TCP reset still surfaces as 'error'.
121300
+ * An EventEmitter with no 'error' listener throws instead of emitting, and
121301
+ * that throw lands outside every try/catch, killing the host process.
121302
+ * See https://github.com/larksuite/node-sdk/issues/197
121303
+ *
121304
+ * NOTE: not for the liveness watchdog — that one terminates *on purpose*
121305
+ * and needs its listeners intact so 'close' drives the reconnect.
121306
+ */
121307
+ detachSocket(socket) {
121308
+ socket.removeAllListeners();
121309
+ socket.on("error", (err) => {
121310
+ this.logger.debug("[ws]", "ignored error from discarded socket", err === null || err === void 0 ? void 0 : err.message);
121311
+ });
121312
+ }
121313
+ /**
121314
+ * Throw away whatever the current attempt established — socket *and* the
121315
+ * timers its 'open' handler armed. A handshake that completes after
121316
+ * `close()` runs `pingLoop()`, and that timer is not unref'd: leaving it
121317
+ * behind keeps the event loop alive forever on a client the caller closed.
121318
+ *
121319
+ * Only safe when {@link closed} is set: otherwise a newer session may
121320
+ * already own `wsConfig`'s instance.
121321
+ */
121322
+ discardConnection() {
121323
+ if (this.pingInterval) {
121324
+ clearTimeout(this.pingInterval);
121325
+ this.pingInterval = void 0;
121326
+ }
121327
+ this.clearLiveness();
121328
+ const socket = this.wsConfig.getWSInstance();
121329
+ if (!socket)
121330
+ return;
121331
+ this.detachSocket(socket);
121332
+ try {
121333
+ socket.terminate();
121334
+ } catch (_a) {
121335
+ }
121336
+ this.wsConfig.setWSInstance(null);
121337
+ }
121273
121338
  /**
121274
121339
  * Invoke a user-supplied callback safely: no-op if undefined, swallow any
121275
121340
  * exception to avoid breaking the WS state machine.
@@ -121376,7 +121441,7 @@ var require_index = __commonJS({
121376
121441
  if (this.handshakeTimeoutMs && this.handshakeTimeoutMs > 0) {
121377
121442
  timer = setTimeout(() => {
121378
121443
  this.logger.error("[ws]", `handshake timeout after ${this.handshakeTimeoutMs}ms`);
121379
- wsInstance.removeAllListeners();
121444
+ this.detachSocket(wsInstance);
121380
121445
  try {
121381
121446
  wsInstance.terminate();
121382
121447
  } catch (_a) {
@@ -121432,6 +121497,13 @@ var require_index = __commonJS({
121432
121497
  } finally {
121433
121498
  this.isConnecting = false;
121434
121499
  }
121500
+ if (this.closed) {
121501
+ this.discardConnection();
121502
+ return;
121503
+ }
121504
+ if (currentGeneration !== this.reconnectGeneration) {
121505
+ return;
121506
+ }
121435
121507
  if (result.ok) {
121436
121508
  this.hasEverConnected = true;
121437
121509
  this.safeInvoke("onReady", this.onReady);
@@ -121473,6 +121545,10 @@ var require_index = __commonJS({
121473
121545
  count++;
121474
121546
  this.currentReconnectAttempts = count;
121475
121547
  const result = yield tryConnect();
121548
+ if (this.closed) {
121549
+ this.discardConnection();
121550
+ return;
121551
+ }
121476
121552
  if (currentGeneration !== this.reconnectGeneration) {
121477
121553
  return;
121478
121554
  }
@@ -121536,13 +121612,17 @@ var require_index = __commonJS({
121536
121612
  const wsInstance = this.wsConfig.getWSInstance();
121537
121613
  wsInstance === null || wsInstance === void 0 ? void 0 : wsInstance.on("message", (buffer) => __awaiter(this, void 0, void 0, function* () {
121538
121614
  this.clearLiveness();
121539
- const data = decode(buffer);
121540
- const { method } = data;
121541
- if (method === FrameType.control) {
121542
- yield this.handleControlData(data);
121543
- }
121544
- if (method === FrameType.data) {
121545
- yield this.handleEventData(data);
121615
+ try {
121616
+ const data = decode(buffer);
121617
+ const { method } = data;
121618
+ if (method === FrameType.control) {
121619
+ yield this.handleControlData(data);
121620
+ }
121621
+ if (method === FrameType.data) {
121622
+ yield this.handleEventData(data);
121623
+ }
121624
+ } catch (err) {
121625
+ this.logger.error("[ws]", `failed to handle inbound frame: ${describeError(err)}`);
121546
121626
  }
121547
121627
  }));
121548
121628
  wsInstance === null || wsInstance === void 0 ? void 0 : wsInstance.on("error", (e) => {
@@ -121564,8 +121644,15 @@ var require_index = __commonJS({
121564
121644
  }
121565
121645
  if (type === MessageType.pong && payload) {
121566
121646
  this.logger.trace("[ws]", "receive pong");
121567
- const dataString = new TextDecoder("utf-8").decode(payload);
121568
- const { PingInterval, ReconnectCount, ReconnectInterval, ReconnectNonce } = JSON.parse(dataString);
121647
+ let pong;
121648
+ try {
121649
+ const dataString = new TextDecoder("utf-8").decode(payload);
121650
+ pong = JSON.parse(dataString);
121651
+ } catch (err) {
121652
+ this.logger.error("[ws]", `invalid pong payload: ${describeError(err)}`);
121653
+ return;
121654
+ }
121655
+ const { PingInterval, ReconnectCount, ReconnectInterval, ReconnectNonce } = pong;
121569
121656
  this.wsConfig.updateWs({
121570
121657
  pingInterval: PingInterval * 1e3,
121571
121658
  reconnectCount: ReconnectCount,
@@ -121588,13 +121675,19 @@ var require_index = __commonJS({
121588
121675
  if (type !== MessageType.event) {
121589
121676
  return;
121590
121677
  }
121591
- const mergedData = this.dataCache.mergeData({
121592
- message_id,
121593
- sum: Number(sum),
121594
- seq: Number(seq),
121595
- trace_id,
121596
- data: payload
121597
- });
121678
+ let mergedData;
121679
+ try {
121680
+ mergedData = this.dataCache.mergeData({
121681
+ message_id,
121682
+ sum: Number(sum),
121683
+ seq: Number(seq),
121684
+ trace_id,
121685
+ data: payload
121686
+ });
121687
+ } catch (err) {
121688
+ this.logger.error("[ws]", `failed to merge event fragments, message_id: ${message_id}; trace_id: ${trace_id}; error: ${describeError(err)}`);
121689
+ return;
121690
+ }
121598
121691
  if (!mergedData) {
121599
121692
  return;
121600
121693
  }
@@ -121663,6 +121756,7 @@ var require_index = __commonJS({
121663
121756
  */
121664
121757
  close(params = {}) {
121665
121758
  const { force = false } = params;
121759
+ this.closed = true;
121666
121760
  this.reconnectGeneration++;
121667
121761
  if (this.pingInterval) {
121668
121762
  clearTimeout(this.pingInterval);
@@ -121678,7 +121772,7 @@ var require_index = __commonJS({
121678
121772
  this.currentReconnectAttempts = 0;
121679
121773
  const wsInstance = this.wsConfig.getWSInstance();
121680
121774
  if (wsInstance) {
121681
- wsInstance.removeAllListeners();
121775
+ this.detachSocket(wsInstance);
121682
121776
  if (force) {
121683
121777
  wsInstance.terminate();
121684
121778
  } else {
@@ -121701,6 +121795,7 @@ var require_index = __commonJS({
121701
121795
  return;
121702
121796
  }
121703
121797
  this.terminalError = false;
121798
+ this.closed = false;
121704
121799
  this.currentReconnectAttempts = 0;
121705
121800
  this.dataCache.clearAtInterval();
121706
121801
  this.logger.info("[ws]", `receive events or callbacks through persistent connection only available in self-build & Feishu app, Configured in:
@@ -122120,6 +122215,7 @@ var require_index = __commonJS({
122120
122215
  let domainSwitched = false;
122121
122216
  let pollTimer = null;
122122
122217
  let expireTimer = null;
122218
+ let settled = false;
122123
122219
  const cleanup = () => {
122124
122220
  var _a2;
122125
122221
  if (pollTimer !== null) {
@@ -122132,25 +122228,51 @@ var require_index = __commonJS({
122132
122228
  }
122133
122229
  (_a2 = ctx.signal) === null || _a2 === void 0 ? void 0 : _a2.removeEventListener("abort", onAbort);
122134
122230
  };
122135
- const onAbort = () => {
122231
+ const succeed = (result) => {
122232
+ if (settled) {
122233
+ return;
122234
+ }
122235
+ settled = true;
122136
122236
  cleanup();
122137
- reject(createError("abort", "Registration was aborted"));
122237
+ resolve(result);
122238
+ };
122239
+ const fail = (err) => {
122240
+ if (settled) {
122241
+ return;
122242
+ }
122243
+ settled = true;
122244
+ cleanup();
122245
+ reject(err);
122246
+ };
122247
+ const scheduleNextPoll = () => {
122248
+ if (settled) {
122249
+ return;
122250
+ }
122251
+ pollTimer = setTimeout(poll, interval);
122252
+ };
122253
+ const onAbort = () => {
122254
+ fail(createError("abort", "Registration was aborted"));
122138
122255
  };
122139
122256
  if ((_a = ctx.signal) === null || _a === void 0 ? void 0 : _a.aborted) {
122140
- return reject(createError("abort", "Registration was aborted"));
122257
+ return fail(createError("abort", "Registration was aborted"));
122141
122258
  }
122142
122259
  (_b = ctx.signal) === null || _b === void 0 ? void 0 : _b.addEventListener("abort", onAbort, { once: true });
122143
122260
  expireTimer = setTimeout(() => {
122144
- cleanup();
122145
- reject(createError("expired_token", "Polling timed out"));
122261
+ fail(createError("expired_token", "Polling timed out"));
122146
122262
  }, ctx.expireIn);
122147
122263
  const poll = () => __awaiter(this, void 0, void 0, function* () {
122148
122264
  var _c, _d, _e, _f, _g, _h;
122265
+ if (settled) {
122266
+ return;
122267
+ }
122149
122268
  try {
122150
122269
  const pollRes = yield requestRegistration(baseUrl, {
122151
122270
  action: "poll",
122152
122271
  device_code: ctx.deviceCode
122153
122272
  });
122273
+ if (settled) {
122274
+ return;
122275
+ }
122154
122276
  if (((_c = pollRes.user_info) === null || _c === void 0 ? void 0 : _c.tenant_brand) === "lark" && !domainSwitched) {
122155
122277
  baseUrl = ctx.larkBaseUrl;
122156
122278
  domainSwitched = true;
@@ -122159,8 +122281,7 @@ var require_index = __commonJS({
122159
122281
  return;
122160
122282
  }
122161
122283
  if (pollRes.client_id && pollRes.client_secret) {
122162
- cleanup();
122163
- resolve({
122284
+ succeed({
122164
122285
  client_id: pollRes.client_id,
122165
122286
  client_secret: pollRes.client_secret,
122166
122287
  user_info: pollRes.user_info
@@ -122177,21 +122298,18 @@ var require_index = __commonJS({
122177
122298
  break;
122178
122299
  case "access_denied":
122179
122300
  case "expired_token":
122180
- cleanup();
122181
- reject(createError(pollRes.error, (_g = pollRes.error_description) !== null && _g !== void 0 ? _g : "Unknown error"));
122301
+ fail(createError(pollRes.error, (_g = pollRes.error_description) !== null && _g !== void 0 ? _g : "Unknown error"));
122182
122302
  return;
122183
122303
  default:
122184
122304
  if (pollRes.error) {
122185
- cleanup();
122186
- reject(createError(pollRes.error, (_h = pollRes.error_description) !== null && _h !== void 0 ? _h : "Unknown error"));
122305
+ fail(createError(pollRes.error, (_h = pollRes.error_description) !== null && _h !== void 0 ? _h : "Unknown error"));
122187
122306
  return;
122188
122307
  }
122189
122308
  break;
122190
122309
  }
122191
- pollTimer = setTimeout(poll, interval);
122310
+ scheduleNextPoll();
122192
122311
  } catch (e) {
122193
- cleanup();
122194
- reject(e);
122312
+ fail(e);
122195
122313
  }
122196
122314
  });
122197
122315
  poll();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-bridge-gateway",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "以 dsh-bridge 为根基、融合 remote-gateway 能力的 DSH 远程访问网关:局域网 / Cloudflare 隧道 / 自建隧道之外,新增「公网直连网关」(0.0.0.0 监听 + 自带 HTTPS 自签证书 + 强制登录门禁),解决移动端外网直连操控电脑。",
5
- "releaseNotes": "【dsh-bridge-gateway】以 dsh-bridge 为根基、融合 remote-gateway 能力的 DSH 远程访问网关\n• 公网访问区:隧道 / 直连网关 二选一,网关全部配置项可在界面配置\n• ➕ 直连网关:0.0.0.0:<端口> 自带 HTTPS(自签证书) + 强制 public_only 门禁,复用 AuthManager,链路打通到 DSH loopback\n• 📱 修复移动端选择模型时界面溢出屏幕的问题\n• 默认不启用,安装后原有局域网/隧道/IM 行为完全不变",
5
+ "releaseNotes": "【dsh-bridge-gateway 0.1.5】\n• 🔧 适配 DSH 0.1.5:RPC 通道迁移到 webServer 直注册 + requestRejection 鉴权\n• 📱 移动端深度适配:顶栏防重叠、分支 chip 不再裁切、输入框宽度对齐并 sticky 固定、44px 点击热区、侧边栏焦点陷阱消除\n• 默认不启用,安装后原有局域网/隧道/IM 行为完全不变",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
8
8
  "exports": {
@@ -17,6 +17,7 @@
17
17
  "client",
18
18
  "cordis.patch.yml",
19
19
  "README.md",
20
+ "README.zh.md",
20
21
  "CHANGELOG.md",
21
22
  "LICENSE",
22
23
  "docs"