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 +16 -1
- package/README.md +64 -29
- package/README.zh.md +110 -0
- package/client/client.js +215 -1
- package/client/index.js +223 -1
- package/lib/bridge-rpc.js +132 -8
- package/lib/feishu/lark-bundled.mjs +156 -38
- package/package.json +3 -2
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.
|
|
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
|
-
|
|
3
|
+
English | [简体中文](./README.zh.md)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
37
|
-
-
|
|
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
|
-
```
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
78
|
-
logger.warn('dsh-bridge: Connection
|
|
187
|
+
if (!ctx?.connection) {
|
|
188
|
+
logger.warn('dsh-bridge: Connection service unavailable — UI will not work');
|
|
79
189
|
return () => {};
|
|
80
190
|
}
|
|
81
191
|
|
|
82
|
-
|
|
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
|
-
|
|
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 = {}) =>
|
|
26866
|
-
|
|
26867
|
-
|
|
26868
|
-
|
|
26869
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121540
|
-
|
|
121541
|
-
|
|
121542
|
-
|
|
121543
|
-
|
|
121544
|
-
|
|
121545
|
-
|
|
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
|
-
|
|
121568
|
-
|
|
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
|
-
|
|
121592
|
-
|
|
121593
|
-
|
|
121594
|
-
|
|
121595
|
-
|
|
121596
|
-
|
|
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
|
-
|
|
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
|
|
122231
|
+
const succeed = (result) => {
|
|
122232
|
+
if (settled) {
|
|
122233
|
+
return;
|
|
122234
|
+
}
|
|
122235
|
+
settled = true;
|
|
122136
122236
|
cleanup();
|
|
122137
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122310
|
+
scheduleNextPoll();
|
|
122192
122311
|
} catch (e) {
|
|
122193
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "以 dsh-bridge 为根基、融合 remote-gateway 能力的 DSH 远程访问网关:局域网 / Cloudflare 隧道 / 自建隧道之外,新增「公网直连网关」(0.0.0.0 监听 + 自带 HTTPS 自签证书 + 强制登录门禁),解决移动端外网直连操控电脑。",
|
|
5
|
-
"releaseNotes": "【dsh-bridge-gateway
|
|
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"
|