dsh-bridge-gateway 0.1.3 → 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,6 +2,26 @@
2
2
 
3
3
  本项目 `dsh-bridge-gateway` 是基于 [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge) 的移植增强分支,在保留原版能力之外,重点新增「公网直连网关」。
4
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
21
+
22
+ ### 修复
23
+ - 修复「远程访问」面板在宿主连接未建立时无限卡在「加载中」的问题:为状态拉取增加兜底超时(15s),超时后释放 in-flight 锁并给出可用的「🔄 重试」按钮,避免面板永远无法自行恢复。
24
+
5
25
  ## [0.1.3] - 待发布
6
26
 
7
27
  ### 修复
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
@@ -2920,18 +2920,29 @@ function BridgePanel({ rpcCall }) {
2920
2920
  }, [rpcCall, adminToken]);
2921
2921
  const loadInFlightRef = React.useRef(false);
2922
2922
  const loadSeqRef = React.useRef(0);
2923
+ const LOAD_TIMEOUT_MS = 15e3;
2923
2924
  const load = React.useCallback(async (quiet = false) => {
2924
2925
  if (loadInFlightRef.current) return;
2925
2926
  loadInFlightRef.current = true;
2926
2927
  const currentSeq = ++loadSeqRef.current;
2928
+ const timedOut = { done: false };
2927
2929
  try {
2928
- const r = await authRpcCall(BRIDGE_ENDPOINTS.getStatus, {});
2930
+ const result = await Promise.race([
2931
+ authRpcCall(BRIDGE_ENDPOINTS.getStatus, {}),
2932
+ new Promise((resolve) => {
2933
+ setTimeout(() => {
2934
+ timedOut.done = true;
2935
+ resolve(null);
2936
+ }, LOAD_TIMEOUT_MS);
2937
+ })
2938
+ ]);
2939
+ if (timedOut.done) throw new Error("\u52A0\u8F7D\u8D85\u65F6\uFF08\u5BBF\u4E3B\u8FDE\u63A5\u53EF\u80FD\u5728\u5207\u6362\u6216\u672A\u5EFA\u7ACB\uFF09\u3002\u8BF7\u70B9\u51FB\u300C\u{1F504} \u91CD\u8BD5\u300D\u5237\u65B0\u3002");
2929
2940
  if (currentSeq !== loadSeqRef.current) return;
2930
- if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
2931
- setStatus(r.value);
2941
+ if (!result?.ok) throw new Error(result?.error?.message ?? "RPC failed");
2942
+ setStatus(result.value);
2932
2943
  if (!quiet) setErr(null);
2933
2944
  } catch (e) {
2934
- if (currentSeq === loadSeqRef.current) setErr(e.message);
2945
+ if (currentSeq === loadSeqRef.current) setErr(e.message || "\u52A0\u8F7D\u5931\u8D25");
2935
2946
  } finally {
2936
2947
  loadInFlightRef.current = false;
2937
2948
  }
@@ -3345,6 +3356,11 @@ function BridgePanel({ rpcCall }) {
3345
3356
  }
3346
3357
  },
3347
3358
  React.createElement("span", { style: { flex: "1 1 auto" } }, err),
3359
+ !isInterceptionErr && React.createElement("button", {
3360
+ type: "button",
3361
+ style: { ...s.btnGhost, height: 26, fontSize: 12, padding: "0 10px", flexShrink: 0 },
3362
+ onClick: () => load()
3363
+ }, "\u{1F504} \u91CD\u8BD5"),
3348
3364
  isInterceptionErr && React.createElement("button", {
3349
3365
  type: "button",
3350
3366
  style: { ...s.btnPri, background: "#dc2626", color: "#ffffff", height: 26, fontSize: 12, padding: "0 10px", flexShrink: 0 },
@@ -3813,6 +3829,54 @@ function injectMobileStyles() {
3813
3829
  display: none !important;
3814
3830
  }
3815
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
+
3816
3880
  /* \u5B50\u4EE3\u7406/\u667A\u80FD\u4F53\u6A21\u5F0F\u80F6\u56CA (Actions)\uFF1A\u7D27\u51D1\u5706\u89D2\u80F6\u56CA */
3817
3881
  div[class*="wSkVaW_headerActions"],
3818
3882
  div[class*="headerActions"] {
@@ -3923,6 +3987,136 @@ function injectMobileStyles() {
3923
3987
  box-sizing: border-box !important;
3924
3988
  }
3925
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
+
3926
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 */
3927
4121
  div[class*="uV2eYG_row"] {
3928
4122
  display: flex !important;
@@ -4017,8 +4211,16 @@ function injectMobileStyles() {
4017
4211
  position: fixed !important;
4018
4212
  left: 0 !important;
4019
4213
  top: 0 !important;
4020
- 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;
4021
4221
  height: 100dvh !important;
4222
+ max-height: 100dvh !important;
4223
+ margin: 0 !important;
4022
4224
  width: 290px !important;
4023
4225
  max-width: 82vw !important;
4024
4226
  z-index: 10000 !important;
@@ -4536,6 +4738,9 @@ function setupMobileExperience(rpcCall, ctx) {
4536
4738
  }
4537
4739
  const sessionRow = e.target.closest('a, div[class*="sessionRow"], div[role="treeitem"]');
4538
4740
  if (sessionRow) {
4741
+ if (sessionRow.hasAttribute("aria-expanded")) {
4742
+ return;
4743
+ }
4539
4744
  setTimeout(() => {
4540
4745
  if (document.body.classList.contains("dsh-drawer-open")) {
4541
4746
  document.body.classList.remove("dsh-drawer-open");
@@ -4546,6 +4751,31 @@ function setupMobileExperience(rpcCall, ctx) {
4546
4751
  document.body.classList.remove("dsh-drawer-open");
4547
4752
  }
4548
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();
4549
4779
  let longPressTimer = null;
4550
4780
  let touchStartX = 0;
4551
4781
  let touchStartY = 0;