dsh-mobile 0.5.0 → 0.5.1
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 +10 -0
- package/CONTRIBUTORS.md +7 -3
- package/README.en.md +31 -30
- package/README.md +35 -33
- package/SECURITY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +4 -4
- package/docs/ATTACH_EXISTING_FRPS.en.md +2 -0
- package/docs/ATTACH_EXISTING_FRPS.md +4 -0
- package/docs/CLOUDFLARE_TUNNEL.en.md +15 -20
- package/docs/CLOUDFLARE_TUNNEL.md +15 -20
- package/docs/DSH_0.1.5_LAN.md +1 -1
- package/docs/README.md +2 -2
- package/docs/SELF_HOSTED_FRP.en.md +7 -10
- package/docs/SELF_HOSTED_FRP.md +7 -10
- package/docs/SELF_HOSTED_ORIGIN.en.md +1 -1
- package/docs/SELF_HOSTED_ORIGIN.md +1 -1
- package/lib/client.js +196 -25
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +2 -0
- package/lib/index.mjs +25 -6
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +22 -5
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Cloudflare named tunnel (a stable public hostname)
|
|
2
2
|
|
|
3
|
+
[中文指南](CLOUDFLARE_TUNNEL.md)
|
|
4
|
+
|
|
3
5
|
The built-in cloudflared provider runs in one of two modes, chosen in the panel under **Mobile access → Remote → cloudflared → Tunnel type**:
|
|
4
6
|
|
|
5
7
|
| | Quick tunnel (default) | Named tunnel |
|
|
@@ -52,11 +54,11 @@ Moving to a fixed hostname means **an already paired phone must pair again**: a
|
|
|
52
54
|
>
|
|
53
55
|
> Scanning is optional: the full link (`https://your-domain/mobile-access/pair#instance=…&token=…`) can be copied to the phone and pasted, because the app accepts a complete link in its input field.
|
|
54
56
|
|
|
55
|
-
The old
|
|
57
|
+
The old address may show **Address may have changed** or temporarily unreachable before re-pairing. After pairing the same computer again, the app updates its existing remote-device record by device identity and keeps the custom name; there is no old entry to delete.
|
|
56
58
|
|
|
57
59
|
## Security boundary
|
|
58
60
|
|
|
59
|
-
- The token is written only into the DSH Mobile private directory (
|
|
61
|
+
- The token is written only into the DSH Mobile private directory (by default `$DSH_HOME/mobile-access/remote/cloudflared/tunnel.json`; Unix mode `0600`, restricted ACL on Windows) and is passed to `cloudflared` **only** through the `TUNNEL_TOKEN` environment variable, never on the command line.
|
|
60
62
|
- The token is never returned to a browser or a phone. Panel status carries only whether it is configured, plus the hostname and port.
|
|
61
63
|
- Behind the tunnel sits DSH Mobile's own authenticated gateway: the public hostname exposes that gateway, and DSH still requires paired-device credentials.
|
|
62
64
|
- The plugin adds no system service, startup item, registry entry or PATH entry. Disabling the channel ends the process.
|
|
@@ -90,24 +92,17 @@ The old remote entry in the device list will show **Address may have changed** o
|
|
|
90
92
|
|
|
91
93
|
## Troubleshooting
|
|
92
94
|
|
|
93
|
-
- **Stuck on "connecting"**:
|
|
95
|
+
- **Stuck on "connecting"**: the named tunnel becomes ready only after the connector registers with Cloudflare. Check cloudflared output in the DSH log and test DNS, UDP/QUIC and TCP reachability from the computer to a Cloudflare edge.
|
|
94
96
|
- **Public access returns 1033**: Cloudflare believes no connector is healthy for that hostname. Check the tunnel shows Healthy in Zero Trust, and that its ingress port matches the panel.
|
|
95
97
|
- **`cloudflared` reports `Unauthorized`, or the tunnel id does not exist**: the token belongs to a different tunnel, for example one that was deleted and recreated. Copy the token again.
|
|
96
|
-
- **
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
prepend-rules:
|
|
107
|
-
- PROCESS-NAME,cloudflared.exe,DIRECT
|
|
108
|
-
- DOMAIN-SUFFIX,argotunnel.com,DIRECT
|
|
109
|
-
- DOMAIN-SUFFIX,trycloudflare.com,DIRECT
|
|
110
|
-
```
|
|
111
|
-
Then make cloudflared **reconnect**: hot-reloading the core does not migrate long-lived connections (use the panel's reconnect action, or toggle the provider off and on).
|
|
112
|
-
- To confirm it is this and not the phone: fetch the boot bundle from the computer with the same session and watch the rate. If the computer is just as slow, the phone and pairing are innocent.
|
|
98
|
+
- **The phone stalls on "Loading plugins" or the remote page is slow while a TUN or transparent proxy is active**: check whether a catch-all rule sends `cloudflared` connections to `*.argotunnel.com` through a proxy node. This routing has caused slow boot-resource transfers before; it does not by itself indicate a pairing failure. This Clash-rule example is for Windows; on Linux, adapt the process name and syntax to your proxy client, and put direct-route rules before catch-all rules:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
prepend-rules:
|
|
102
|
+
- PROCESS-NAME,cloudflared.exe,DIRECT
|
|
103
|
+
- DOMAIN-SUFFIX,argotunnel.com,DIRECT
|
|
104
|
+
- DOMAIN-SUFFIX,trycloudflare.com,DIRECT
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then make cloudflared **reconnect** so existing connections take the new route (use the panel's reconnect action, or toggle the provider off and on). Compare the same resource through the LAN and remote entries. If the remote entry is also slow on the computer, inspect the computer-to-Cloudflare route first; if only the phone is slow, inspect its network.
|
|
113
108
|
- **The domain still resolves to the old address**: after the nameserver change Cloudflare must move the zone from Pending to Active, and records in a pending zone are not served publicly.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Cloudflare 命名隧道(固定公网域名)
|
|
2
2
|
|
|
3
|
+
[English guide](CLOUDFLARE_TUNNEL.en.md)
|
|
4
|
+
|
|
3
5
|
内置的 cloudflared 通道有两种模式,在面板 **移动访问 → 远程 → cloudflared → 隧道类型** 中切换:
|
|
4
6
|
|
|
5
7
|
| | 快速隧道(默认) | 命名隧道 |
|
|
@@ -52,11 +54,11 @@
|
|
|
52
54
|
>
|
|
53
55
|
> 不想扫码时,完整链接(`https://你的域名/mobile-access/pair#instance=…&token=…`)可以整条复制到手机上粘贴,App 的输入框接受完整链接。
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
旧地址在重新配对前可能显示「地址可能已变化」或暂不可达。重新配对同一台电脑后,App 会按设备标识更新原远程记录,保留自定义名称;无需删除它。
|
|
56
58
|
|
|
57
59
|
## 安全边界
|
|
58
60
|
|
|
59
|
-
- 令牌只写入 DSH Mobile
|
|
61
|
+
- 令牌只写入 DSH Mobile 私有目录(默认 `$DSH_HOME/mobile-access/remote/cloudflared/tunnel.json`;Unix 权限 `0600`,Windows 使用受限 ACL),并且**只**通过子进程环境变量 `TUNNEL_TOKEN` 传给 `cloudflared`,不出现在命令行里。
|
|
60
62
|
- 令牌从不回传给浏览器或手机端;面板状态里只有「是否已配置」、域名和端口。
|
|
61
63
|
- 隧道背后是 DSH Mobile 自己的认证网关:公网主机名只暴露该网关,DSH 本体仍然需要配对设备凭据。
|
|
62
64
|
- 插件不会创建系统服务、开机启动项、注册表项或 PATH 项;关闭通道即结束进程。
|
|
@@ -90,24 +92,17 @@
|
|
|
90
92
|
|
|
91
93
|
## 排错
|
|
92
94
|
|
|
93
|
-
-
|
|
95
|
+
- **连接一直停在「正在连接」**:命名隧道只有在 connector 向 Cloudflare 注册后才显示就绪。检查 DSH 日志中的 cloudflared 输出,以及本机到 Cloudflare 边缘的 DNS、UDP/QUIC 和 TCP 连通性。
|
|
94
96
|
- **公网访问返回 1033**:Cloudflare 认为该主机名没有健康的 connector。确认隧道在 Zero Trust 里显示 Healthy,且 ingress 指向的端口与面板一致。
|
|
95
97
|
- **`cloudflared` 报 `Unauthorized` 或隧道 ID 不存在**:令牌与控制台里的隧道不匹配(例如隧道被删除后重建)。重新复制令牌。
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
prepend-rules:
|
|
107
|
-
- PROCESS-NAME,cloudflared.exe,DIRECT
|
|
108
|
-
- DOMAIN-SUFFIX,argotunnel.com,DIRECT
|
|
109
|
-
- DOMAIN-SUFFIX,trycloudflare.com,DIRECT
|
|
110
|
-
```
|
|
111
|
-
改完要让 cloudflared **重连**才会换路由:内核热重载不会迁移已建立的长连接(面板里点重连,或开关一次提供方)。
|
|
112
|
-
- 判断方法:如果手机端一直卡在加载插件,先在**本机**用同一份会话拉一次 boot bundle 看速率。若本机也很慢,就不是手机或配对的问题。
|
|
98
|
+
- **手机卡在「正在加载插件」或远程页面明显慢,而本机开着 TUN/透明代理**:先检查 `cloudflared` 到 `*.argotunnel.com` 的连接是否被兜底规则送进代理节点。曾有这种路由导致启动资源传输很慢;它不等于配对失败。以下是 Windows 上 Clash 客户端的规则示例;Linux 的进程名与具体规则语法应按所用客户端调整,并让直连规则排在兜底规则前:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
prepend-rules:
|
|
102
|
+
- PROCESS-NAME,cloudflared.exe,DIRECT
|
|
103
|
+
- DOMAIN-SUFFIX,argotunnel.com,DIRECT
|
|
104
|
+
- DOMAIN-SUFFIX,trycloudflare.com,DIRECT
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
改完后让 cloudflared **重连**,使已建立的连接使用新路由(面板里点重连,或开关一次提供方)。对比同一资源经局域网与远程入口的加载速度;若远程入口在电脑上也慢,优先检查电脑到 Cloudflare 的路径,若只有手机慢,再检查手机网络。
|
|
113
108
|
- **域名解析还是旧地址**:改完 NS 后 Cloudflare 需要把 zone 从 Pending 变为 Active;A/CNAME 在 zone 激活前不会对外生效。
|
package/docs/DSH_0.1.5_LAN.md
CHANGED
package/docs/README.md
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
|
|
20
20
|
| 文档 | 内容 |
|
|
21
21
|
| --- | --- |
|
|
22
|
-
| [DSH 0.1.5 局域网验证记录](DSH_0.1.5_LAN.md) | DSH Mobile 0.3.15
|
|
22
|
+
| [DSH 0.1.5 局域网验证记录](DSH_0.1.5_LAN.md) | DSH Mobile 0.3.15 的历史局域网实测;当前版本组合请看[兼容性表](../README.md#兼容性) |
|
|
23
23
|
|
|
24
24
|
## 面向维护者 / Maintainer notes
|
|
25
25
|
|
|
26
26
|
维护者文档面向改代码的人,**不随 npm 包发布**,只在仓库中阅读:
|
|
27
27
|
|
|
28
|
-
-
|
|
28
|
+
- [自建 FRP 维护记录](HANDOFF_SELF_HOSTED_FRP.md) —— 0.4.6 开发时的代码地图、安全检查点与历史真机验证;当前使用步骤以本页上方的用户指南为准。
|
|
@@ -20,7 +20,7 @@ Android app
|
|
|
20
20
|
- **Domain mode**: use your own public domain; Caddy obtains and renews the certificate automatically.
|
|
21
21
|
- **Public IPv4 mode**: use the VPS public IPv4 address directly. The plugin uses Certbot to request a roughly six-day Let's Encrypt IP certificate and installs a daily renewal timer. Documentation ranges such as `203.0.113.10`, private addresses, and other reserved addresses are rejected; enter a real routable public IP.
|
|
22
22
|
|
|
23
|
-
The publicly trusted entry in this guide requires Android app 0.3.3 or later for custom remote origins.
|
|
23
|
+
The publicly trusted entry in this guide requires Android app 0.3.3 or later for custom remote origins. The separate self-signed TCP passthrough in the [existing-frps attachment guide](ATTACH_EXISTING_FRPS.en.md) is attach-only and requires the 0.4.6 or later app with remote CA pinning; older apps cannot use that entry and continue to work with LAN, cpolar, and Tailscale.
|
|
24
24
|
|
|
25
25
|
## Manual deployment vs automatic deployment
|
|
26
26
|
|
|
@@ -53,12 +53,14 @@ Public IPv4 mode also removes the corresponding Let's Encrypt IP certificate. Do
|
|
|
53
53
|
On the VPS:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
sudo systemctl status dsh-mobile-frps.service caddy
|
|
56
|
+
sudo systemctl status dsh-mobile-frps.service caddy
|
|
57
57
|
sudo journalctl -u dsh-mobile-frps.service -u caddy -n 200 --no-pager
|
|
58
58
|
sudo ss -lntp
|
|
59
|
-
curl -
|
|
59
|
+
curl -v https://PUBLIC_HOST/mobile-access/discovery
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Only public IPv4 mode installs `dsh-mobile-cert-renew.timer`; in that mode, also run `sudo systemctl status dsh-mobile-cert-renew.timer`. Replace `PUBLIC_HOST` with your actual domain or public IPv4 address. Do not add `-k`: verify the public certificate as well as the response. Check the same address from an independent external network and confirm discovery reports this computer's installation identifier; success on the VPS alone does not prove the phone's network can reach it.
|
|
63
|
+
|
|
62
64
|
On the computer, start with the plugin log at `$DSH_HOME/mobile-access/logs/dsh-mobile.log` (JSONL, 5 MB rotation, tokens and keys redacted) and the panel diagnostic report. Windows antivirus software can quarantine frpc; if it does, create the smallest possible exception for the verified component directory only.
|
|
63
65
|
|
|
64
66
|
## Limits
|
|
@@ -69,11 +71,6 @@ On the computer, start with the plugin log at `$DSH_HOME/mobile-access/logs/dsh-
|
|
|
69
71
|
- VPS SSH targets currently cannot use IPv6.
|
|
70
72
|
- The frps plaintext vhost must bind to `127.0.0.1`. The plugin rejects a publicly reachable plaintext port and reports readiness only after public discovery identifies the current computer.
|
|
71
73
|
|
|
72
|
-
##
|
|
74
|
+
## Acceptance check
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
- [ ] Ubuntu domain mode: fresh VPS deployment → public domain ready → App pairing → send and receive messages.
|
|
76
|
-
- [x] Existing Caddy coexistence: deploy into a Caddyfile containing an existing site and import line; verify user content is byte-for-byte preserved, the import occurs exactly once, and validation passes (2026-09-03, Tencent Cloud; concurrent writes use `flock`).
|
|
77
|
-
- [x] Host-key change abort: an unconfirmed key set stops deployment/cleanup with `vps_host_key_mismatch` before network authentication (unit-test coverage plus full device fingerprint review).
|
|
78
|
-
- [x] One-click cleanup: after deployment, services, files, timers, marked firewall rules, the owned system user, and the LE certificate were removed while the user's Caddy installation remained (2026-09-03, Tencent Cloud).
|
|
79
|
-
- [x] Uninstall-script review: only DSH Mobile-owned paths and rules are touched (unit assertions plus device review).
|
|
76
|
+
After deployment, check the certificate, public discovery endpoint, app pairing, and a live conversation on your own VPS and phone network. The repository's historical tests are recorded in the [self-hosted FRP handoff](HANDOFF_SELF_HOSTED_FRP.md); they cannot replace your own acceptance test.
|
package/docs/SELF_HOSTED_FRP.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- **域名模式**:公开地址是自己的域名,Caddy 自动申请并续期证书。
|
|
25
25
|
- **公网 IPv4 模式**:公开地址直接是 VPS 公网 IPv4(例如你自己的 VPS 地址),插件用 Certbot 申请约 6 天有效的 Let's Encrypt IP 证书,并安装每日自动续期定时器。注意:文档示例地址(如 `203.0.113.10`)和内网、保留地址会被拒绝,必须填写真实可路由的公网 IP。
|
|
26
26
|
|
|
27
|
-
本页的公开证书入口需要 Android App 0.3.3
|
|
27
|
+
本页的公开证书入口需要 Android App 0.3.3 或更高版本(远程自定义入口)。另见[接入既有 frps](ATTACH_EXISTING_FRPS.md)中的自签 TCP 穿透档:它仅适用于接入模式,需要 0.4.6 或更新的 Android App 固定远程 CA;旧版 App 不支持该档,仍可使用局域网、cpolar 和 Tailscale。
|
|
28
28
|
|
|
29
29
|
## 手动部署 vs 自动部署
|
|
30
30
|
|
|
@@ -57,12 +57,14 @@ SSH 用户、端口和私钥路径只保存在当前浏览器的 `localStorage`
|
|
|
57
57
|
VPS 上:
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
sudo systemctl status dsh-mobile-frps.service caddy
|
|
60
|
+
sudo systemctl status dsh-mobile-frps.service caddy
|
|
61
61
|
sudo journalctl -u dsh-mobile-frps.service -u caddy -n 200 --no-pager
|
|
62
62
|
sudo ss -lntp
|
|
63
|
-
curl -
|
|
63
|
+
curl -v https://PUBLIC_HOST/mobile-access/discovery
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
只有公网 IPv4 模式会安装 `dsh-mobile-cert-renew.timer`,该模式另查 `sudo systemctl status dsh-mobile-cert-renew.timer`。将 `PUBLIC_HOST` 换成实际域名或公网 IPv4;不加 `-k`,以便同时验证公开证书。还应从独立外部网络检查同一地址,并确认 discovery 返回当前电脑的安装标识;只在 VPS 本机成功不代表手机网络可达。
|
|
67
|
+
|
|
66
68
|
本机优先看插件日志(`$DSH_HOME/mobile-access/logs/dsh-mobile.log`,JSONL,超 5 MB 轮转,自动脱敏 Token 与密钥)与面板诊断报告。Windows 杀毒软件可能隔离 frpc;如确有拦截,只为已校验的组件目录设置最小范围例外。
|
|
67
69
|
|
|
68
70
|
## 限制
|
|
@@ -73,11 +75,6 @@ curl -vk https://PUBLIC_HOST/
|
|
|
73
75
|
- VPS 的 SSH 目前不支持 IPv6 地址。
|
|
74
76
|
- frps 明文 vhost 必须只监听 `127.0.0.1`;插件会拒绝公网可达的明文端口,并在公开发现接口确认是当前电脑后才显示“已就绪”。
|
|
75
77
|
|
|
76
|
-
##
|
|
78
|
+
## 上线前验收
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
- [ ] Ubuntu 域名模式:全新 VPS 一键部署 → 公网域名就绪 → App 远程配对 → 收发消息。
|
|
80
|
-
- [x] 既有 Caddy 共存:在已有自有站点 + import 行的 Caddyfile 上部署,确认自有内容逐字节保留、import 恰好一行、validate 通过(2026-09-03,腾讯云实测;并发写有 flock 互斥)。
|
|
81
|
-
- [x] 指纹变更中止:未确认密钥集合部署/清理直接 `vps_host_key_mismatch` 中止且零网络碰触(单元测试锁定;真机指纹全量核对通过)。
|
|
82
|
-
- [x] 一键清理:部署后执行一键清理,服务、文件、定时器、标记防火墙规则、系统用户、LE 证书均已移除,自有 Caddy 本体保留(2026-09-03,腾讯云实测)。
|
|
83
|
-
- [x] 卸载脚本审阅:仅触碰 DSH Mobile 拥有的路径与规则(单元测试断言 + 真机复核)。
|
|
80
|
+
部署后,请在你的实际 VPS 与手机网络上检查证书、外网发现接口、App 配对和实时对话;本仓库的历史实测记录见[自建 FRP 交接记录](HANDOFF_SELF_HOSTED_FRP.md),不能替代你自己的验收。
|
|
@@ -13,7 +13,7 @@ This provider appears under **Mobile Access → Remote → Self-hosted connectio
|
|
|
13
13
|
- Separate LAN proxy: listen on the DSH computer's private IPv4, e.g. `192.168.50.10:3444`, and allow the proxy's actual direct source, e.g. `192.168.50.1/32`.
|
|
14
14
|
- Only explicit loopback/RFC1918 IPv4 binds are supported. Wildcard, public and IPv6 binds are rejected. The port must be 1024–65535 and may not be 3443 (the LAN gateway) or 3080 (DSH's own WebServer); note that 3444 is also the cloudflared named tunnel's default forward port. Source CIDRs must be canonical private/loopback networks, at most 16; prefer a single-host /32. Forwarded headers do not determine source authorization.
|
|
15
15
|
|
|
16
|
-
Select **Save and start backend**, then point the proxy at the displayed HTTP backend. **Never expose/port-forward that HTTP listener publicly**, and never bypass it by proxying to DSH
|
|
16
|
+
Select **Save and start backend**, then point the proxy at the displayed HTTP backend. **Never expose/port-forward that HTTP listener publicly**, and never bypass it by proxying to DSH's WebServer (default port 3080) or the LAN 3443 gateway. HTTP between separate devices is suitable only for a trusted private network. Pairing, authentication, normalized-exact Host/Origin enforcement, CSRF, Secure cookies and WebSocket path policy remain active. Do not open the plain HTTP backend in a browser to verify it: it only accepts requests proxied from the configured public HTTPS origin; direct access has the wrong Host, and its cookie/CSRF flow may fail.
|
|
17
17
|
|
|
18
18
|
## Proxy contract
|
|
19
19
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
- **只有第一段允许暴露公网。HTTP 后端不能做公网端口映射。** HTTP 段仅适用于可信私网;局域网其他设备上的代理也会接触明文流量。
|
|
18
|
-
- 后端不是裸 DSH:仍执行设备配对、认证、归一化后精确匹配的 Host/Origin 校验、CSRF、Secure Cookie 和 WebSocket 路径策略。没有配对的访问不能直接进入 DSH。**不要用浏览器直接打开这个明文 HTTP
|
|
18
|
+
- 后端不是裸 DSH:仍执行设备配对、认证、归一化后精确匹配的 Host/Origin 校验、CSRF、Secure Cookie 和 WebSocket 路径策略。没有配对的访问不能直接进入 DSH。**不要用浏览器直接打开这个明文 HTTP 后端做验证**:它只支持由指定 HTTPS 公网地址代理的请求;直接打开时 Host 不匹配,Cookie 与 CSRF 流程也可能失效。
|
|
19
19
|
- 不要把公网反代目标改成 DSH 的 WebServer 端口(默认 3080),也不要反代现有 LAN HTTPS 3443。新后端默认 3444,**明确拒绝 3443 和 3080**,并拒绝 1024 以下需要特权的端口。注意 3444 同时是 cloudflared 命名隧道的默认转发端口:同一时刻只有一个远程提供方能运行,但切换到本提供方时请确认该端口已释放。DSH 与 LAN 的原有设置不变。
|
|
20
20
|
- 公网 TLS、证书续期、DNS、路由和代理由你管理。插件不会安装隧道组件、部署 VPS、修改代理/防火墙/路由器,也不会为该提供方发起公网探测。
|
|
21
21
|
|
package/lib/client.js
CHANGED
|
@@ -2792,6 +2792,48 @@ window.__ModuleLoader__.load({
|
|
|
2792
2792
|
function firstByClassSuffix(root, suffix) {
|
|
2793
2793
|
return Array.from(root.querySelectorAll("[class]")).find((element) => classToken(element, suffix));
|
|
2794
2794
|
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Whether this composer is the landing hero rather than the composer of an open
|
|
2797
|
+
* conversation. DSH marks the hero composer's root with a `_hero` class and drops
|
|
2798
|
+
* it once a session owns the surface, which is the only difference available
|
|
2799
|
+
* without the layout's session id.
|
|
2800
|
+
*/
|
|
2801
|
+
function composerIsLandingHero(composer, depth = 4) {
|
|
2802
|
+
let node = composer;
|
|
2803
|
+
for (let step = 0; step < depth && node !== null; step += 1) {
|
|
2804
|
+
if (classToken(node, "_hero")) return true;
|
|
2805
|
+
node = node.parentElement;
|
|
2806
|
+
}
|
|
2807
|
+
return false;
|
|
2808
|
+
}
|
|
2809
|
+
/** Resolve only identities published by the current conversation or selected session row. */
|
|
2810
|
+
function resolveComposerSessionOrigin(composer, selectedRow, selectedRowToken) {
|
|
2811
|
+
const conversationRoot = composer?.closest("[data-conversation-session]") ?? null;
|
|
2812
|
+
const conversationId = conversationRoot?.getAttribute("data-conversation-session");
|
|
2813
|
+
if (conversationRoot !== null && conversationId) return {
|
|
2814
|
+
sessionRoot: conversationRoot,
|
|
2815
|
+
sessionId: conversationId
|
|
2816
|
+
};
|
|
2817
|
+
const dedicatedRoot = composer?.closest("[data-dsh-mobile-session]") ?? null;
|
|
2818
|
+
const dedicatedId = dedicatedRoot?.getAttribute("data-dsh-mobile-session");
|
|
2819
|
+
if (dedicatedRoot !== null && dedicatedId) return {
|
|
2820
|
+
sessionRoot: dedicatedRoot,
|
|
2821
|
+
sessionId: dedicatedId
|
|
2822
|
+
};
|
|
2823
|
+
if (selectedRow === null) return {
|
|
2824
|
+
sessionRoot: null,
|
|
2825
|
+
sessionId: null
|
|
2826
|
+
};
|
|
2827
|
+
const identity = selectedRow.getAttribute("data-session-id") ?? selectedRow.getAttribute("aria-label") ?? selectedRow.textContent?.trim() ?? "";
|
|
2828
|
+
return {
|
|
2829
|
+
sessionRoot: selectedRow,
|
|
2830
|
+
sessionId: `${selectedRowToken(selectedRow)}:${identity}`
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
/** An id-less non-hero composer still accepts App soft-keyboard line breaks, but not camera results. */
|
|
2834
|
+
function activeSessionForSoftEnter(composer, sessionId) {
|
|
2835
|
+
return sessionId !== null || composer !== null && composer.closest(".dshm-main") !== null && !composerIsLandingHero(composer);
|
|
2836
|
+
}
|
|
2795
2837
|
/** Find the stock DSH application frame without mistaking a feature card for the shell. */
|
|
2796
2838
|
function resolveNativeMobileFrame(root, dedicatedCenter) {
|
|
2797
2839
|
if (dedicatedCenter !== void 0) return void 0;
|
|
@@ -2962,6 +3004,38 @@ window.__ModuleLoader__.load({
|
|
|
2962
3004
|
function drawerScrimVisible(sidebarCollapsed, overlayActive) {
|
|
2963
3005
|
return !sidebarCollapsed && overlayActive;
|
|
2964
3006
|
}
|
|
3007
|
+
/** Let the stock page close one visible layer before Android navigates its WebView. */
|
|
3008
|
+
function installStockMobileBack(view, current, actions) {
|
|
3009
|
+
const onBack = (event) => {
|
|
3010
|
+
if (!event.cancelable || event.defaultPrevented) return;
|
|
3011
|
+
const layers = current();
|
|
3012
|
+
if (layers === null) return;
|
|
3013
|
+
if (layers.detailsOpen) {
|
|
3014
|
+
event.preventDefault();
|
|
3015
|
+
actions.closeDetails();
|
|
3016
|
+
} else if (layers.drawerOpen) {
|
|
3017
|
+
event.preventDefault();
|
|
3018
|
+
actions.closeDrawer();
|
|
3019
|
+
} else if (layers.mainPanelOpen) {
|
|
3020
|
+
event.preventDefault();
|
|
3021
|
+
actions.closeMainPanel();
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
view.addEventListener("dsh-mobile:native-back", onBack);
|
|
3025
|
+
return () => {
|
|
3026
|
+
view.removeEventListener("dsh-mobile:native-back", onBack);
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
function stockLayoutBackControl(value) {
|
|
3030
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
3031
|
+
}
|
|
3032
|
+
/** Whether the stock layout can return from a selected main panel. */
|
|
3033
|
+
function stockMainPanelOpen(value) {
|
|
3034
|
+
const layout = stockLayoutBackControl(value);
|
|
3035
|
+
if (typeof layout?.selectPanel !== "function" || typeof layout.panelInfo?.getSnapshot !== "function") return false;
|
|
3036
|
+
const snapshot = layout.panelInfo.getSnapshot();
|
|
3037
|
+
return typeof snapshot === "object" && snapshot !== null && typeof snapshot.activePanelId === "string";
|
|
3038
|
+
}
|
|
2965
3039
|
/** Only a known on-screen keyboard in an active App composer may change Enter into a line break. */
|
|
2966
3040
|
function isSoftKeyboardEnterLineBreak(event, context) {
|
|
2967
3041
|
if (context.nativeState?.imeVisible !== true || context.nativeState.noHardwareKeyboard !== true || !context.editable || !context.activeSession || context.commandMenuOpen || context.recentlyComposing) return false;
|
|
@@ -3009,7 +3083,7 @@ window.__ModuleLoader__.load({
|
|
|
3009
3083
|
return !current.disposed && origin.generation === current.generation && origin.href === current.href && origin.composer !== null && current.composerConnected && origin.composer === current.composer && origin.sessionRoot !== null && origin.sessionRoot === current.sessionRoot && origin.sessionId !== null && origin.sessionId === current.sessionId;
|
|
3010
3084
|
}
|
|
3011
3085
|
/** Add mobile semantics without replacing feature trees. */
|
|
3012
|
-
function installNativeMobileSurface() {
|
|
3086
|
+
function installNativeMobileSurface(backServices) {
|
|
3013
3087
|
document.documentElement.classList.add("dsh-native-mobile-active");
|
|
3014
3088
|
const browserLanguages = navigator.languages.length > 0 ? navigator.languages : [navigator.language];
|
|
3015
3089
|
const language = resolveNativeMobileLanguage(document.documentElement.lang, browserLanguages);
|
|
@@ -3106,27 +3180,15 @@ window.__ModuleLoader__.load({
|
|
|
3106
3180
|
const sessionTokens = /* @__PURE__ */ new WeakMap();
|
|
3107
3181
|
let nextSessionToken = 0;
|
|
3108
3182
|
const currentSessionOrigin = () => {
|
|
3109
|
-
const dedicatedRoot = boundComposer?.closest("[data-dsh-mobile-session]") ?? null;
|
|
3110
|
-
const dedicatedId = dedicatedRoot?.getAttribute("data-dsh-mobile-session");
|
|
3111
|
-
if (dedicatedRoot !== null && typeof dedicatedId === "string" && dedicatedId !== "") return {
|
|
3112
|
-
sessionRoot: dedicatedRoot,
|
|
3113
|
-
sessionId: dedicatedId
|
|
3114
|
-
};
|
|
3115
3183
|
const selectedRow = document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
token
|
|
3123
|
-
|
|
3124
|
-
}
|
|
3125
|
-
const identity = selectedRow.getAttribute("data-session-id") ?? selectedRow.getAttribute("aria-label") ?? selectedRow.textContent?.trim() ?? "";
|
|
3126
|
-
return {
|
|
3127
|
-
sessionRoot: selectedRow,
|
|
3128
|
-
sessionId: `${token}:${identity}`
|
|
3129
|
-
};
|
|
3184
|
+
return resolveComposerSessionOrigin(boundComposer, selectedRow, (row) => {
|
|
3185
|
+
let token = sessionTokens.get(row);
|
|
3186
|
+
if (token === void 0) {
|
|
3187
|
+
token = `stock-${String(++nextSessionToken)}`;
|
|
3188
|
+
sessionTokens.set(row, token);
|
|
3189
|
+
}
|
|
3190
|
+
return token;
|
|
3191
|
+
});
|
|
3130
3192
|
};
|
|
3131
3193
|
const mediaRequestContext = () => {
|
|
3132
3194
|
const session = currentSessionOrigin();
|
|
@@ -3472,7 +3534,7 @@ window.__ModuleLoader__.load({
|
|
|
3472
3534
|
disposeEnterBinding = editor === null || composerCard === null ? void 0 : bindComposerSoftEnter(editor, () => ({
|
|
3473
3535
|
nativeState: window.__DSH_MOBILE_NATIVE__ === void 0 ? null : window.__DSH_MOBILE_KEYBOARD_STATE__,
|
|
3474
3536
|
editable: editor.isConnected && editor.getAttribute("contenteditable") === "true" && editor.getAttribute("aria-disabled") !== "true" && editor.getAttribute("aria-haspopup") !== "menu" && composerCard.getAttribute("aria-busy") !== "true",
|
|
3475
|
-
activeSession: currentSessionOrigin().sessionId
|
|
3537
|
+
activeSession: activeSessionForSoftEnter(composerCard, currentSessionOrigin().sessionId),
|
|
3476
3538
|
commandMenuOpen: composerCard.querySelector("[data-trigger-menu],button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]") !== null
|
|
3477
3539
|
}));
|
|
3478
3540
|
};
|
|
@@ -3621,6 +3683,33 @@ window.__ModuleLoader__.load({
|
|
|
3621
3683
|
if (sidebar?.dataset.open === "true") toggle?.click();
|
|
3622
3684
|
});
|
|
3623
3685
|
sync();
|
|
3686
|
+
const stopStockBack = installStockMobileBack(window, () => {
|
|
3687
|
+
if (frame?.isConnected !== true) return null;
|
|
3688
|
+
const lastColumn = frame.style.gridTemplateColumns.trim().split(/\s+/).at(-1);
|
|
3689
|
+
return {
|
|
3690
|
+
detailsOpen: overlayQuery.matches && lastColumn !== void 0 && lastColumn !== "" && lastColumn !== "0px" && lastColumn !== "0",
|
|
3691
|
+
drawerOpen: overlayQuery.matches && sidebarRoot?.isConnected === true && toggle?.isConnected === true && !classToken(sidebarRoot, "_collapsed"),
|
|
3692
|
+
mainPanelOpen: stockMainPanelOpen(backServices.getLayout())
|
|
3693
|
+
};
|
|
3694
|
+
}, {
|
|
3695
|
+
closeDetails: () => {
|
|
3696
|
+
const layout = stockLayoutBackControl(backServices.getLayout());
|
|
3697
|
+
if (typeof layout?.closeRightbar !== "function") return;
|
|
3698
|
+
const sidebarRight = backServices.getSidebarRight();
|
|
3699
|
+
if (typeof sidebarRight === "object" && sidebarRight !== null) {
|
|
3700
|
+
const control = sidebarRight;
|
|
3701
|
+
if (typeof control.isExpanded === "function" && typeof control.toggleExpanded === "function" && control.isExpanded()) control.toggleExpanded();
|
|
3702
|
+
}
|
|
3703
|
+
layout.closeRightbar();
|
|
3704
|
+
},
|
|
3705
|
+
closeDrawer: () => {
|
|
3706
|
+
toggle?.click();
|
|
3707
|
+
},
|
|
3708
|
+
closeMainPanel: () => {
|
|
3709
|
+
const layout = stockLayoutBackControl(backServices.getLayout());
|
|
3710
|
+
if (typeof layout?.selectPanel === "function") layout.selectPanel(null);
|
|
3711
|
+
}
|
|
3712
|
+
});
|
|
3624
3713
|
const disposeTaskWatcher = installTaskCompletionWatcher({
|
|
3625
3714
|
completionFallback: false,
|
|
3626
3715
|
label: (kind, sessionLabel) => kind === "done" ? {
|
|
@@ -3633,6 +3722,7 @@ window.__ModuleLoader__.load({
|
|
|
3633
3722
|
});
|
|
3634
3723
|
return () => {
|
|
3635
3724
|
disposed = true;
|
|
3725
|
+
stopStockBack();
|
|
3636
3726
|
disposeTaskWatcher();
|
|
3637
3727
|
mediaRequestGeneration += 1;
|
|
3638
3728
|
mediaPickerAbortController.abort();
|
|
@@ -3737,6 +3827,64 @@ window.__ModuleLoader__.load({
|
|
|
3737
3827
|
function parseMobileRemoteProvider(value) {
|
|
3738
3828
|
return value === "cpolar" || value === "cloudflared" || value === "frp" || value === "origin" ? value : "tailscale";
|
|
3739
3829
|
}
|
|
3830
|
+
/**
|
|
3831
|
+
* Set a surface's visibility; opening a page or overlay brings it to the front.
|
|
3832
|
+
* @param container - the registered surface element.
|
|
3833
|
+
* @param open - whether to show it.
|
|
3834
|
+
*/
|
|
3835
|
+
function setMobileSurfaceOpen(container, open) {
|
|
3836
|
+
container.hidden = !open;
|
|
3837
|
+
if (open && container.isConnected && (container.dataset.dshMobileSurfacePlacement === "page" || container.dataset.dshMobileSurfacePlacement === "overlay")) container.parentElement?.append(container);
|
|
3838
|
+
}
|
|
3839
|
+
function visibleBackLayer(view, element) {
|
|
3840
|
+
if (!element.isConnected || element.hidden || element.closest("[hidden],[inert],[aria-hidden=\"true\"]") !== null) return false;
|
|
3841
|
+
const style = view.getComputedStyle(element);
|
|
3842
|
+
return style.display !== "none" && style.visibility !== "hidden" && element.getClientRects().length > 0;
|
|
3843
|
+
}
|
|
3844
|
+
/**
|
|
3845
|
+
* Consume page-level surfaces and dismissible DSH overlays before layout navigation.
|
|
3846
|
+
* @param view - the page window receiving the native Back request.
|
|
3847
|
+
* @param page - the current DSH document.
|
|
3848
|
+
* @param blocked - feedback for a foreground layer without a safe close action.
|
|
3849
|
+
* @returns a disposer for the capture listener.
|
|
3850
|
+
*/
|
|
3851
|
+
function installMobileNativeBack(view, page, blocked) {
|
|
3852
|
+
const onBack = (event) => {
|
|
3853
|
+
if (!event.cancelable || event.defaultPrevented) return;
|
|
3854
|
+
const claim = () => {
|
|
3855
|
+
event.preventDefault();
|
|
3856
|
+
event.stopImmediatePropagation();
|
|
3857
|
+
};
|
|
3858
|
+
const surface = [...page.querySelectorAll("[data-dsh-mobile-surface-placement=\"page\"], [data-dsh-mobile-surface-placement=\"overlay\"]")].filter((element) => visibleBackLayer(view, element)).at(-1);
|
|
3859
|
+
if (surface !== void 0) {
|
|
3860
|
+
setMobileSurfaceOpen(surface, false);
|
|
3861
|
+
claim();
|
|
3862
|
+
return;
|
|
3863
|
+
}
|
|
3864
|
+
const layers = [...page.querySelectorAll("[role=\"dialog\"][aria-modal=\"true\"], [role=\"menu\"]")].filter((element) => visibleBackLayer(view, element));
|
|
3865
|
+
const top = layers.at(-1);
|
|
3866
|
+
if (top === void 0) return;
|
|
3867
|
+
claim();
|
|
3868
|
+
if (top.getAttribute("role") === "dialog") {
|
|
3869
|
+
const wrapper = top.parentElement;
|
|
3870
|
+
const mask = wrapper?.firstElementChild;
|
|
3871
|
+
const maskStyle = mask === void 0 || mask === null ? void 0 : view.getComputedStyle(mask);
|
|
3872
|
+
if (wrapper?.getAttribute("role") === "presentation" && wrapper.children.length === 2 && wrapper.lastElementChild === top && mask?.tagName === "DIV" && mask.getAttribute("aria-hidden") === "true" && mask.isConnected && !mask.hidden && maskStyle?.display !== "none" && maskStyle?.visibility !== "hidden" && mask.getClientRects().length > 0) {
|
|
3873
|
+
mask.click();
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
} else if (top.getAttribute("data-menu-material") === "translucent" && layers.filter((layer) => layer.getAttribute("role") === "menu").length === 1) {
|
|
3877
|
+
page.dispatchEvent(new Event("pointerdown", { bubbles: true }));
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
blocked();
|
|
3881
|
+
};
|
|
3882
|
+
const capture = { capture: true };
|
|
3883
|
+
view.addEventListener("dsh-mobile:native-back", onBack, capture);
|
|
3884
|
+
return () => {
|
|
3885
|
+
view.removeEventListener("dsh-mobile:native-back", onBack, capture);
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3740
3888
|
const queuedDefinitions = [];
|
|
3741
3889
|
let queuedLegacyMount;
|
|
3742
3890
|
if (typeof window !== "undefined" && window.dshMobile === void 0) window.dshMobile = {
|
|
@@ -3757,6 +3905,18 @@ window.__ModuleLoader__.load({
|
|
|
3757
3905
|
function selectedMobileControlLocale() {
|
|
3758
3906
|
return selectMobileControlLocale(document.documentElement.lang, navigator.languages?.length ? navigator.languages : [navigator.language]);
|
|
3759
3907
|
}
|
|
3908
|
+
/**
|
|
3909
|
+
* Guidance when an open third-party layer has no safe close action.
|
|
3910
|
+
* @param locale - current Mobile UI language.
|
|
3911
|
+
* @returns localized guidance.
|
|
3912
|
+
*/
|
|
3913
|
+
function mobileBackBlockedMessage(locale) {
|
|
3914
|
+
return {
|
|
3915
|
+
en: "Close the open panel first.",
|
|
3916
|
+
it: "Chiudi prima il pannello aperto.",
|
|
3917
|
+
zh: "请先关闭当前弹层。"
|
|
3918
|
+
}[locale];
|
|
3919
|
+
}
|
|
3760
3920
|
/** Remount one plugin-owned surface when DSH changes the document language. */
|
|
3761
3921
|
function installDshLanguageBoundSurface(install) {
|
|
3762
3922
|
let locale = selectedMobileControlLocale();
|
|
@@ -7880,6 +8040,9 @@ window.__ModuleLoader__.load({
|
|
|
7880
8040
|
shellLayer().append(node);
|
|
7881
8041
|
window.setTimeout(() => node.remove(), 2600);
|
|
7882
8042
|
};
|
|
8043
|
+
const stopNativeBack = installMobileNativeBack(window, document, () => {
|
|
8044
|
+
toast(mobileBackBlockedMessage(selectedMobileControlLocale()));
|
|
8045
|
+
});
|
|
7883
8046
|
const materializeNativeFile = (value) => {
|
|
7884
8047
|
if (typeof value !== "object" || value === null || !("base64" in value) || !("name" in value)) return value;
|
|
7885
8048
|
const candidate = value;
|
|
@@ -8012,6 +8175,7 @@ window.__ModuleLoader__.load({
|
|
|
8012
8175
|
return registerUniqueDisposable(surfaces, surfaceIds, surface.id, () => {
|
|
8013
8176
|
const container = element("section");
|
|
8014
8177
|
container.dataset.dshMobileSurface = surface.id;
|
|
8178
|
+
container.dataset.dshMobileSurfacePlacement = surface.placement;
|
|
8015
8179
|
container.hidden = surface.placement === "page" || surface.placement === "overlay";
|
|
8016
8180
|
container.style.cssText = surface.placement === "page" || surface.placement === "overlay" ? "position:absolute;inset:0;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);padding:16px;pointer-events:auto" : "pointer-events:auto";
|
|
8017
8181
|
const host = () => surface.placement === "page" || surface.placement === "overlay" ? shellLayer() : surfaceHost(surface.placement) ?? shellLayer();
|
|
@@ -8064,12 +8228,12 @@ window.__ModuleLoader__.load({
|
|
|
8064
8228
|
open: (surfaceId) => {
|
|
8065
8229
|
ensureCurrent();
|
|
8066
8230
|
const entry = surfaces.get(surfaceId);
|
|
8067
|
-
if (entry !== void 0) entry.container
|
|
8231
|
+
if (entry !== void 0) setMobileSurfaceOpen(entry.container, true);
|
|
8068
8232
|
},
|
|
8069
8233
|
close: (surfaceId) => {
|
|
8070
8234
|
ensureCurrent();
|
|
8071
8235
|
const entry = surfaces.get(surfaceId);
|
|
8072
|
-
if (entry !== void 0) entry.container
|
|
8236
|
+
if (entry !== void 0) setMobileSurfaceOpen(entry.container, false);
|
|
8073
8237
|
},
|
|
8074
8238
|
toast: (message) => {
|
|
8075
8239
|
ensureCurrent();
|
|
@@ -8441,6 +8605,7 @@ window.__ModuleLoader__.load({
|
|
|
8441
8605
|
});
|
|
8442
8606
|
return () => {
|
|
8443
8607
|
disposed = true;
|
|
8608
|
+
stopNativeBack();
|
|
8444
8609
|
stopEvents();
|
|
8445
8610
|
stopRefresh();
|
|
8446
8611
|
started = false;
|
|
@@ -8560,7 +8725,10 @@ window.__ModuleLoader__.load({
|
|
|
8560
8725
|
};
|
|
8561
8726
|
});
|
|
8562
8727
|
const removeCustom = installCustomAssets();
|
|
8563
|
-
const removeSurface = installDshLanguageBoundSurface(installNativeMobileSurface
|
|
8728
|
+
const removeSurface = installDshLanguageBoundSurface(() => installNativeMobileSurface({
|
|
8729
|
+
getLayout: () => ctx.get("layout"),
|
|
8730
|
+
getSidebarRight: () => ctx.get("sidebarRight")
|
|
8731
|
+
}));
|
|
8564
8732
|
return () => {
|
|
8565
8733
|
removeSettingsAction();
|
|
8566
8734
|
removeCustom();
|
|
@@ -8643,6 +8811,8 @@ window.__ModuleLoader__.load({
|
|
|
8643
8811
|
exports.handleMissingExtensionManifest = handleMissingExtensionManifest;
|
|
8644
8812
|
exports.inject = inject;
|
|
8645
8813
|
exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
|
|
8814
|
+
exports.installMobileNativeBack = installMobileNativeBack;
|
|
8815
|
+
exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
|
|
8646
8816
|
exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
|
|
8647
8817
|
exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;
|
|
8648
8818
|
exports.parseMobileExtensionManifest = parseMobileExtensionManifest;
|
|
@@ -8653,6 +8823,7 @@ window.__ModuleLoader__.load({
|
|
|
8653
8823
|
exports.renderDiagnosticPayloadSafely = renderDiagnosticPayloadSafely;
|
|
8654
8824
|
exports.selectMobileControlLocale = selectMobileControlLocale;
|
|
8655
8825
|
exports.selectedMobileControlLocale = selectedMobileControlLocale;
|
|
8826
|
+
exports.setMobileSurfaceOpen = setMobileSurfaceOpen;
|
|
8656
8827
|
exports.startExtensionChangeStream = startExtensionChangeStream;
|
|
8657
8828
|
exports.startLifecycleRefreshScheduler = startLifecycleRefreshScheduler;
|
|
8658
8829
|
exports.trustAuthenticatedGatewayConnection = trustAuthenticatedGatewayConnection;
|