dsh-zen-remote 1.0.2 → 1.1.0

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.
Files changed (51) hide show
  1. package/AGENTS.md +9 -1
  2. package/README.md +56 -16
  3. package/docs/interface.md +69 -6
  4. package/docs/remote-access.en.md +7 -5
  5. package/docs/remote-access.md +8 -6
  6. package/dsh-push.mjs +272 -64
  7. package/lib/client.js +550 -28
  8. package/lib/lan-gate-server.cjs +7 -1
  9. package/lib/types/client/MobileHome.d.ts.map +1 -1
  10. package/lib/types/client/MobileSessionHeader.d.ts +1 -1
  11. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
  12. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  13. package/lib/types/client/effects/modal-back.d.ts +22 -0
  14. package/lib/types/client/effects/modal-back.d.ts.map +1 -0
  15. package/lib/types/client/history-nav.d.ts +49 -0
  16. package/lib/types/client/history-nav.d.ts.map +1 -0
  17. package/lib/types/client/index.d.ts.map +1 -1
  18. package/lib/types/client/locales.d.ts +4 -2
  19. package/lib/types/client/locales.d.ts.map +1 -1
  20. package/lib/types/client/styles/base.css.d.ts +1 -1
  21. package/lib/types/client/styles/base.css.d.ts.map +1 -1
  22. package/lib/types/client/styles/chips.css.d.ts +1 -1
  23. package/lib/types/client/styles/chips.css.d.ts.map +1 -1
  24. package/lib/types/client/styles/compat.css.d.ts +1 -1
  25. package/lib/types/client/styles/compat.css.d.ts.map +1 -1
  26. package/lib/types/client/styles/composer.css.d.ts +1 -1
  27. package/lib/types/client/styles/composer.css.d.ts.map +1 -1
  28. package/lib/types/client/styles/content.css.d.ts +1 -1
  29. package/lib/types/client/styles/content.css.d.ts.map +1 -1
  30. package/lib/types/client/styles/header.css.d.ts +1 -1
  31. package/lib/types/client/styles/header.css.d.ts.map +1 -1
  32. package/lib/types/client/styles/home.css.d.ts +1 -1
  33. package/lib/types/client/styles/home.css.d.ts.map +1 -1
  34. package/llms.txt +1 -1
  35. package/package.json +23 -21
  36. package/pwa/inject.js +1 -0
  37. package/scripts/sync-doc-version.mjs +83 -0
  38. package/src/client/MobileHome.tsx +17 -1
  39. package/src/client/MobileSessionHeader.tsx +134 -1
  40. package/src/client/MobileSessionInfo.tsx +38 -8
  41. package/src/client/effects/modal-back.ts +98 -0
  42. package/src/client/history-nav.ts +121 -0
  43. package/src/client/index.tsx +2 -0
  44. package/src/client/locales.ts +8 -4
  45. package/src/client/styles/base.css.ts +6 -0
  46. package/src/client/styles/chips.css.ts +4 -1
  47. package/src/client/styles/compat.css.ts +8 -1
  48. package/src/client/styles/composer.css.ts +25 -0
  49. package/src/client/styles/content.css.ts +24 -0
  50. package/src/client/styles/header.css.ts +123 -0
  51. package/src/client/styles/home.css.ts +19 -10
package/AGENTS.md CHANGED
@@ -29,9 +29,16 @@
29
29
  pnpm install
30
30
  pnpm build # tsc host + tsc client + build-client.mjs → lib/(产物入库,改 src/ 必须重跑并提交 lib/)
31
31
  pnpm verify # 两个 tsconfig 的 --noEmit 类型检查
32
- pnpm test # 网关 43 个 node:test 用例 + 三个界面自检脚本,一条命令全跑
32
+ pnpm test # 网关 node:test 用例 + 三个界面自检脚本 + 文档版本号一致性检查,一条命令全跑
33
33
  ```
34
34
 
35
+ **版本号**:README 里的 release 徽章和 profile 依赖示例由
36
+ `scripts/sync-doc-version.mjs` 按 `package.json` 改写,挂在 `version` 生命周期
37
+ 脚本上——`npm version patch` 会把改好的 README 带进同一个发版提交,不用手改
38
+ (徽章曾经一路卡在 v1.0.0 到 1.0.2)。`pnpm test` 里的 `--check` 会在漏同步时
39
+ 把测试挂掉。文档里的锚点变了就更新那个脚本:找不到标记它直接非零退出,不会
40
+ 默默通过。
41
+
35
42
  ## 深度文档
36
43
 
37
44
  | 文件 | 内容 |
@@ -39,6 +46,7 @@ pnpm test # 网关 43 个 node:test 用例 + 三个界面自检脚本,一
39
46
  | [`docs/remote-access.md`](docs/remote-access.md) | 通道半边:反代配置(nginx/Caddy/Lucky)、配对流程、环境变量表、管理 API、推送、安全边界 |
40
47
  | [`docs/interface.md`](docs/interface.md) | 界面半边:断点策略、调试徽章、安全区体系、兼容插件清单 |
41
48
 
49
+
42
50
  ## 合仓后仍然成立的硬约束
43
51
 
44
52
  - **网关是子进程**:`lan-gate.mjs` 只负责 spawn + 生命周期,永远不要把
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  <p align="center">
5
5
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0B7285?style=flat-square" alt="MIT"></a>
6
- <img src="https://img.shields.io/badge/release-v1.0.0-5B4CF0?style=flat-square" alt="v1.0.0">
6
+ <img src="https://img.shields.io/badge/release-v1.1.0-5B4CF0?style=flat-square" alt="v1.1.0">
7
7
  <img src="https://img.shields.io/badge/DSH-Web%20Profile-5B4CF0?style=flat-square" alt="DSH Web Profile">
8
8
  </p>
9
9
 
@@ -27,7 +27,7 @@ dsh plugin add dsh-zen-remote
27
27
 
28
28
  装完重启 `dsh web`,手机界面与网关一起生效,不需要再手写任何配置行。
29
29
 
30
- > 兼容性:在 DSH `0.1.0-rc.6`(web profile)上开发并实测,最后验证 2026-08-18
30
+ > 兼容性:在 DSH `0.1.0-rc.7`(web profile)上开发并实测,最后验证 2026-08-20
31
31
 
32
32
  卸载:`dsh plugin remove dsh-zen-remote`(或从 profile 的 `dependencies` 与 `bundles` 里删掉那两行),重启 `dsh web` 即恢复原状;要清掉配对数据再删 `~/.dsh/lan-gate-state.json` 与 `~/.dsh/lan-gate.config.json`。
33
33
 
@@ -39,7 +39,7 @@ dsh plugin add dsh-zen-remote
39
39
  ```jsonc
40
40
  {
41
41
  "dependencies": {
42
- "dsh-zen-remote": "^1.0.0" // 本地开发换成 "link:/path/to/dsh-zen-remote"
42
+ "dsh-zen-remote": "^1.1.0" // 本地开发换成 "link:/path/to/dsh-zen-remote"
43
43
  },
44
44
  "dsh": { "profile": { "bundles": [
45
45
  "@deepseek-ai/dsh-base",
@@ -56,7 +56,6 @@ cd ~/.dsh/profiles/web && pnpm install
56
56
 
57
57
  不想走 profile 安装流程的静态挂载写法见 [`cordis.patch.yml.example`](cordis.patch.yml.example)。
58
58
 
59
- 从旧的两包结构(`dsh-mobile-pwa` + `@dsh-external/dsh-mobile-nav`)升级:两行依赖、两条 bundle 换成上面的一行一条,并把 profile 的 `cordis.patch.yml` 里手挂 `dsh-mobile-pwa/dsh-push.mjs` 的那行删掉——推送现在随包自带。
60
59
  </details>
61
60
 
62
61
  ---
@@ -65,7 +64,7 @@ cd ~/.dsh/profiles/web && pnpm install
65
64
 
66
65
  装完在本机 `127.0.0.1:3080` 就能用手机界面。要从外面访问,按下面三步走。
67
66
 
68
- ### 1. 配一个反代终结 HTTPS
67
+ ### 1. 配一个反代中继 HTTPS
69
68
 
70
69
  网关默认只监听 `127.0.0.1:3088`,必须由你自己的反代对外。**家宽没有公网 IP、或者不想开路由器端口**,就跳过 nginx/Caddy 直接看第三个块(Cloudflare Tunnel)。
71
70
 
@@ -184,15 +183,52 @@ open http://127.0.0.1:3088/lan-gate/admin
184
183
  | `LAN_GATE_RATE_LIMIT` | `120` | 未配对请求的每分钟上限(按真实客户端 IP) |
185
184
  | `LAN_GATE_TRUSTED_PROXIES` | 空 | 逗号分隔 IP;反代不在本机时必填 |
186
185
  | `LAN_GATE_VAPID_SUBJECT` | `mailto:admin@localhost` | 推送联系人。**iOS 必须改成真实邮箱或 https 网址**,否则 Apple 拒发 |
187
- | `DSH_PUSH_EVENTS` | `agent/turn-stopping` | 触发自动推送的事件名,逗号分隔 |
188
- | `DSH_PUSH_DEBOUNCE_MS` | `15000` | 两条自动推送的最小间隔 |
189
- | `DSH_PUSH_SUMMARY` | 关 | 设 `1` 让通知带上本回合最后一条回复(截 120 字) |
186
+ | `DSH_PUSH_TURN_END` | **关** | 设 `1` 让「回合结束」也推一条。默认不推——回合结束不代表需要你(1.0.3 之前是默认推的,这是行为变更)。等授权、等回答这两类通知不受它影响,永远推 |
187
+ | `DSH_PUSH_EVENTS` | `agent/turn-stopping` | 「回合结束」算哪些事件,逗号分隔;只在 `DSH_PUSH_TURN_END=1` 时有意义 |
188
+ | `DSH_PUSH_DEBOUNCE_MS` | `15000` | 两条自动推送的最小间隔;等授权/等回答的通知不受压制 |
189
+ | `DSH_PUSH_SUMMARY` | 关 | 设 `1` 让通知带上本回合的最终回复(只取正文,不含思考过程;截 120 字)和提问原文 |
190
190
  | `DSH_PUSH_TOOL` | 开 | 设 `0` 关掉模型可调用的 `push_notify` 工具 |
191
191
 
192
192
  上传大小上限(默认 20MB)在插件行的 `config.maxUploadBytes` 里改。
193
193
 
194
194
  ---
195
195
 
196
+ ## 通知什么时候会响
197
+
198
+ 默认只在**真正需要你**的时候响,分两条互不依赖的线。
199
+
200
+ **一、系统自己判断的(恒开,且不受最小间隔压制)**
201
+
202
+ | 情况 | 通知 |
203
+ | --- | --- |
204
+ | 某个工具在等你授权 | 「DSH 等你授权」,带工具名 |
205
+ | 模型调用 `ask_user_question` 在等你回答 | 「DSH 等你回答」 |
206
+
207
+ 这两类不看会话层级——子代理自己卡在授权上,照样喊你,因为等的还是你。也**不受
208
+ `DSH_PUSH_DEBOUNCE_MS` 压制**:「有操作等你点头」是最不能被吞掉的一条。
209
+
210
+ 策略自动放行的授权不会打扰你:请求发起后先等 1.5 秒,配对的「已决定」到了就取消,
211
+ 只有真正悬着没人管的才推。
212
+
213
+ **二、模型自己决定的**
214
+
215
+ `push_notify` 工具,模型在这些时候该调:你明确要求做完通知、需要你介入才能继续、
216
+ 出现你大概率想立刻知道的意外。不该调的场景(常规回合结束、进度汇报、它自己能推进
217
+ 的事)同样写在工具描述里——只写前者会让它每回合都调。会话开始还会注入一段同源的
218
+ 上下文强化,和工具描述共用一个常量,不会各改各的。
219
+
220
+ **默认不会响的**
221
+
222
+ - **普通跑完一轮不推**(1.0.3 起的行为变更,此前每回合都推)。干完活本身不等于
223
+ 需要你。想要旧行为设 `DSH_PUSH_TURN_END=1`。
224
+ - **子代理跑完永远不推**,无论上面那个开关。
225
+
226
+ **通知里写什么**:默认只有标题,不带对话内容。开 `DSH_PUSH_SUMMARY=1` 才带这一轮
227
+ 的最终回复——只取正文,不含思考过程;这一轮没说话就退回「最后执行了 xx 工具」,不拿思考内容凑数。
228
+ 推送 payload 是 aes128gcm 端到端加密的。
229
+
230
+ ---
231
+
196
232
  ## 功能
197
233
 
198
234
  - 会话列表主屏 + 独立会话页两级页面栈,横向推入推出
@@ -200,12 +236,12 @@ open http://127.0.0.1:3088/lan-gate/admin
200
236
  - composer 重排:控件图标化,权限/模型菜单变成底部 sheet
201
237
  - 会话信息卡:六格统计 + 导出日志 / 重命名 / Fork / 归档
202
238
  - 同一回合的推理与工具调用默认折叠成一条「过程 · N 步」
203
- - 手势:左边缘右滑返回、底部 sheet 下滑关闭
239
+ - 手势:左边缘右滑返回、底部 sheet 下滑关闭;安卓系统返回手势接管为「先关弹层 → 退回列表 → 退出应用」,不再一按就退出 PWA
204
240
  - 手机本地附件上传:落到会话工作目录 `.dsh-uploads/`,输入框追加 `@` 引用,发不发你说了算
205
241
  - 配对码换长期设备令牌,认令牌不认 IP,可随时吊销
206
242
  - 管理面(生成配对码 / 管理设备 / 触发推送)只认本机直连,经反代一律 403
207
243
  - 真 PWA:manifest + service worker,可装到主屏、可离线打开
208
- - 真 Web Push:VAPID + aes128gcm,通知默认不带对话正文
244
+ - 真 Web Push:VAPID + aes128gcm,通知默认不带对话正文;默认只在等授权/等回答时响,回合结束不再打扰(见上)
209
245
  - `push_notify` 工具:模型可在关键节点自己推一条,带限流
210
246
  - 「内测声明」弹窗注入「不再弹出」可选项:远程访问每次刷新都会重弹声明,点一次后本设备记住选择、以后自动关闭
211
247
 
@@ -220,11 +256,11 @@ open http://127.0.0.1:3088/lan-gate/admin
220
256
 
221
257
  | 插件 | 移动端适配内容 | 实测版本 |
222
258
  | --- | --- | --- |
223
- | [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) | 会话页头部提供工作台入口按钮;面板变手机全宽抽屉并避让刘海安全区;底部居中的关闭按钮 | 0.12.2 |
224
- | [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams) | AgentTeams 活动浮层挪到会话头部下方(原位置压住头部按钮)、会话列表页自动隐藏;子代理会话头部保留可点的父会话标题,一键切回主会话 | 0.1.6 |
225
- | [dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats) | 用量与余额入口收进主屏 chips 行 | |
226
- | [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins) | 定时任务入口收进主屏 chips 行 | 0.2.0 |
227
- | dsh-at-file | @文件引用,配合附件上传的 `@` 路径引用使用 | |
259
+ | [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) | 会话页头部提供工作台入口按钮;面板变手机全宽抽屉并避让刘海安全区;底部居中的关闭按钮 | 0.14.0 |
260
+ | [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams) | AgentTeams 活动浮层挪到会话头部下方(原位置压住头部按钮)、会话列表页自动隐藏;子代理会话头部保留可点的父会话标题,一键切回主会话 | 0.1.7 |
261
+ | [dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats) | 用量与余额入口收进主屏 chips 行 | 0.2.5 |
262
+ | [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins) | 定时任务入口收进主屏 chips 行 | 0.2.2 |
263
+ | dsh-at-file | @文件引用,配合附件上传的 `@` 路径引用使用 | 0.6.5 |
228
264
  | [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit) | 图像 Q&A/OCR,配合手机端附件上传使用 | — |
229
265
  | [dsh-web-ui 全家桶](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all) | 沿用上游 dsh-web-mobile 的兼容规则(文件树 / 预览浮层限宽居中等) | — |
230
266
 
@@ -236,7 +272,11 @@ open http://127.0.0.1:3088/lan-gate/admin
236
272
 
237
273
  **iOS 26.x 独立 PWA 视口缩水**:加到主屏后视口底部会少掉一条状态栏高度,普通 Safari 标签页正常。这是 iOS 系统缺陷,缺掉的区域在文档之外,CSS 够不着;本插件做了三层缓解(浅色 manifest 背景 + 安全区补偿 + 强制重排),能减轻但不保证复原。彻底恢复只能整个 App 退出重开。
238
274
 
239
- **经反代访问时设置页的插件配置列表空白**:直连 `127.0.0.1:3080` 正常。根因在 DSH 官方客户端的连接就绪超时判定,不在网关。绕法是要改插件配置时回本机浏览器改,配置存在后端,改完手机侧其它功能不受影响。
275
+ **经反代访问时设置页打不开(插件配置列表空白、模型卡片报「settings are unavailable in this browser」)**:直连 `127.0.0.1:3080/3088` 正常。
276
+
277
+ 根因是 DSH 官方的设计,不在网关:设置类 RPC **只对回环连接开放**。客户端按 `location.hostname` 判定(`dsh-client-connection` 的 `isLoopback`),非回环时 `dsh-client-ui-settings` 把持久化降级为 `memory`,设置镜像初始状态就是 `unavailable`——官方源码注释原话是「remote browsers remain process-local because settings RPCs are loopback-only」。所有依赖这个镜像的卡片(模型、插件配置)因此一起空白,与本插件、与 service worker 缓存都无关(2026-08-20 真机 USB 调试 + 本机对照实测)。
278
+
279
+ 绕法:要改配置就回跑 DSH 的那台机器上用本机浏览器改,配置存在后端,改完手机侧其它功能不受影响。想让远程也能改设置,得由上游放开这条限制。
240
280
 
241
281
  ---
242
282
 
package/docs/interface.md CHANGED
@@ -42,7 +42,8 @@ v1.0.0(fork 自 [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-
42
42
 
43
43
  - 底排控件全部图标化(附件 · 权限 · 模型 · 上下文环 · 发送),不再是挤在一起的文字按钮;
44
44
  - 权限、模型这两个官方弹出菜单被 CSS 整成从底部升起的 sheet 样式(结构还是官方的,只换了皮肤);
45
- - 分支/Todo 这类 mini chips 挪到输入卡外上方;
45
+ - `conversation.input.dock` 里的入口(git 分支等)压成一行可横滚的 mini chips,挪到输入卡外上方;
46
+ - 例外是 DSH 原生的待办卡片(`data-testid="todo-panel"`):它不是 chip 而是可就地展开的卡片,因此单独豁免出 26px 胶囊笼子,保持整行宽度与自然高度——展开后的列表由官方自己限高 180px 并内部滚动,不会顶到输入框;
46
47
  - composer 顶部不画分割线,改用渐变 mask——消息滚动到顶部/底部时自然淡入淡出,比一条硬边界更贴近原生 app。
47
48
 
48
49
  ### 会话信息卡
@@ -58,6 +59,24 @@ Chat 视图里,同一回合内的推理块、工具调用块默认折叠成一
58
59
  - 会话页左边缘往右滑 = 返回列表(有浮层或工作台开着的话先关掉它);
59
60
  - 各类底部 sheet(信息卡、chips 自定义单、权限/模型菜单)支持下滑关闭。
60
61
 
62
+ **安卓系统返回手势(2026-08-20)**:安卓边缘内滑就是浏览器的「后退」,而手机端的
63
+ 两级页面栈原本是纯 store 状态、从不压历史记录,于是后退发现无处可退 → 直接退出
64
+ PWA,再进来就是冷启动。插件自己的边缘手势也触发不了——那几十像素被系统手势先吃
65
+ 掉,网页收不到 touch。
66
+
67
+ 系统手势**拦不掉**(`systemGestureExclusionRects` 是原生 App 的 API),只能接住:
68
+ `history-nav.ts` 维护一个「可关闭层」栈并镜像进 `history`。进会话压一层、开信息卡
69
+ 再压一层,系统返回于是变成「先关信息卡、再退回列表」,在列表按返回才退出应用(根
70
+ 页面本该如此)。官方对话框、composer 的权限/模型 sheet、第三方面板由
71
+ `effects/modal-back.ts` 统一接管:它们的开关状态都锁在各自组件里没有公开 setter,
72
+ 但共享一个 DOM 契约——打开时是 `[aria-modal="true"]`、按 Escape 关闭,所以按契约
73
+ 兜底即可,不必为每个插件写适配。
74
+
75
+ 纪律:**层只能单向关闭**。组件不自己把状态改成关,而是调 `popLayer()` 回退历史,
76
+ 由 `popstate` 去执行 `close`。两头各改各的必然错位,错位表现为「返回手势没反应」。
77
+ 排除自有弹层(`:not([data-mobile-nav])`)也是同一个道理——信息卡曾经被显式层和
78
+ 通用观察器各登记一次,一次返回退两层,历史退到根而会话页还在屏幕上。
79
+
61
80
  ### 附件上传
62
81
 
63
82
  composer 最左的回形针打开的是**手机本地**的文件选择器(iOS 上会弹相册/拍照/选取文件三选一)——官方那套文件选择器是在跑 DSH 的电脑上弹窗,手机远程用不了。图片和文件一视同仁:都上传到会话工作目录的 `.dsh-uploads/`,composer 上方出现可删除的预览 chip(图片缩略图、文件图标+文件名),并把 `@.dsh-uploads/文件名` 追加到输入框——发不发、什么时候发,由你按官方发送键决定,不会替你自动发出。
@@ -93,6 +112,50 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
93
112
 
94
113
  ## 更新日志
95
114
 
115
+ > `v2.0.0` 及更早的小节标题用的是手机端界面的代次,与 npm 包版本号不是一条线;
116
+ > 两套编号并行容易看混,从 `1.1.0` 起本表统一按 **npm 包版本**记录。
117
+
118
+ ### 1.1.0
119
+
120
+ **新增**
121
+
122
+ - 安卓系统返回手势接管:页面栈与各类弹层镜像进 `history`,返回变成「先关弹层 →
123
+ 退回列表 → 退出应用」,不再一按就退出 PWA(见「手势」一节);
124
+ - 会话页头部活动 chip:子代理 / 后台任务的计数与状态点(蓝色脉动=运行中、绿色=
125
+ 已完成、琥珀=失败或被终止),点开直接弹官方原生列表;
126
+ - 推送时机重做:只在等授权 / 等回答时必推,回合结束改为 opt-in,子代理跑完不推
127
+ (详见根 README 的「通知什么时候会响」)。
128
+
129
+ **改进**
130
+
131
+ - 正文行距 28px → 1.65 倍、段间距 16px → 12px,手机上一屏能多看几行;
132
+ - 会话列表主屏与会话页统一为同一个 `bg-base` 背景,卡片与 chips 改用次级表面,
133
+ 切页面不再有明显色阶跳变。
134
+
135
+ **修复**
136
+
137
+ - DSH 原生待办卡片在手机端点了没反应:`conversation.input.dock` 的 mini chip
138
+ 规则给槽内所有条目套了 `max-height: 26px` + `overflow: hidden`,而官方待办
139
+ 是就地展开的卡片而非 chip。展开动作其实一直是成功的(`aria-expanded` 翻了、
140
+ 8 行待办以 216px 挂载完毕),只是整张卡被剪在 26px 里,看上去像点不动。已按
141
+ `data-testid="todo-panel"` 单独豁免(2026-08-20 真机视口实测)。
142
+ - 信息卡的缓存命中率升为主数据并保留一位小数,Token 收支降为副行——整数四舍五入
143
+ 会把 99.6% 显示成 100%,这一位不是装饰;
144
+ - 「关闭工作台」胶囊在深色下看不出边缘:填充是 `bg-base`、与身后表面同色,而黑色
145
+ 投影落在深色背景上等于不存在。改用内描边画边界,投影只负责浮起;
146
+ - 「不再弹出」按钮在手机上被官方确认按钮挤成两字宽:操作行是 flex,该按钮缺
147
+ `flex-shrink` 保护,补 `flex: 0 0 auto` + `min-width: max-content`。
148
+
149
+ **内部**
150
+
151
+ - 构建与实测基准从 DSH `0.1.0-rc.6` 升到 `0.1.0-rc.7`(peer 与 dev 依赖同步);
152
+ - README 的 release 徽章与依赖示例改由 `scripts/sync-doc-version.mjs` 跟着
153
+ `package.json` 走,挂在 `version` 生命周期脚本上,`pnpm test` 里带一致性检查。
154
+ - 已知问题更正:经反代访问时设置页空白,真实原因是 DSH 官方**设置 RPC 仅对回环
155
+ 连接开放**(客户端按 `location.hostname` 判定),不是此前记录的「连接就绪超时」,
156
+ 也与本插件无关(2026-08-20 真机 USB 调试 + 本机对照实测);
157
+ - 测试从 46 增至 73:新增返回手势层栈 7 条、推送策略 13 条、活动 chip 契约若干。
158
+
96
159
  ### v2.0.0
97
160
 
98
161
  **新增**
@@ -161,11 +224,11 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
161
224
 
162
225
  ## 兼容插件
163
226
 
164
- - [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(移动端全宽抽屉,本插件在会话页头部给它留入口位)——本机实测 **0.12.2**
165
- - [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams)(AgentTeams 活动浮层:手机端挪到会话头部下方、避开安全区,会话列表页隐藏;子代理会话头部保留可点的父会话面包屑用于切回)——本机实测 **0.1.6**
166
- - [dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)(用量与余额,主屏 chips 行可直接打开)
167
- - [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.0**
168
- - dsh-at-file(@文件引用)
227
+ - [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(移动端全宽抽屉,本插件在会话页头部给它留入口位)——本机实测 **0.14.0**
228
+ - [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams)(AgentTeams 活动浮层:手机端挪到会话头部下方、避开安全区,会话列表页隐藏;子代理会话头部保留可点的父会话面包屑用于切回)——本机实测 **0.1.7**
229
+ - [dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)(用量与余额,主屏 chips 行可直接打开)——本机实测 **0.2.5**
230
+ - [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.2**
231
+ - dsh-at-file(@文件引用)——本机实测 **0.6.5**
169
232
  - [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit)(图像 Q&A/OCR,配合手机端附件上传使用)
170
233
  - [dsh-web-ui 全家桶](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all)(文件树 / 预览 / 任务看板 / SSH / 宠物 / 会话统计 / 远程配对 / 设置)——沿用上游兼容规则,本次未扩展
171
234
 
@@ -20,7 +20,7 @@ Built on the MIT [dsh-mobile-gate](https://github.com/Bernardxu123/dsh-mobile-ga
20
20
  | 📱 **Real PWA** | `manifest.json` + service worker: once the proxy provides HTTPS, "Add to Home Screen" actually works — standalone full-screen app with icon, splash, theme-color, maskable assets |
21
21
  | 🌐 **Offline** | SW v3: only the true static shell (manifest/icons/offline page) is cache-first, everything else (DSH client bundle JS/CSS, API, page HTML) is network-first — a new deploy is picked up immediately instead of lingering behind stale cached CSS |
22
22
  | 👆 **Touch gestures** | Pinch-to-resize font (resettable); edge-swipe-back has been handed off to the interface half (see "Division of labor" below), pull-to-refresh has been removed entirely (an accidental overscroll used to fire a full reload mid-conversation) |
23
- | 🔔 **Agent-done push** | Real Web Push (VAPID-signed, aes128gcm-encrypted). Notified when the agent finishes, even from another app the notification never carries conversation content |
23
+ | 🔔 **Push when you're actually needed** | Real Web Push (VAPID-signed, aes128gcm-encrypted). By default it fires only when something is genuinely waiting on you: a tool needs authorization, or the model asked you a question. Plain turn-end is opt-in (`DSH_PUSH_TURN_END=1`). The notification carries no conversation content unless you ask for it |
24
24
  | 🛎️ **`push_notify` tool** | A model-callable push tool (registered by `dsh-push.mjs`): the model can decide mid-task that the user needs a decision, that a key milestone was reached, or that an error needs a human — and push straight to the lock screen instead of waiting for the turn to end. Usage discipline (don't call this often) is spelled out in the tool description; the host also enforces it with rate limits (max 1 per 60s per session, 20/hour globally) — over the limit, the call is silently dropped, never an error. Same aes128gcm end-to-end encryption, same lock-screen-only exposure. Turn it off entirely with `pushTool: false` in `lan-gate.config.json` (or `DSH_PUSH_TOOL=0`); it's also skipped automatically on hosts without a tool registry (`ctx.tools`), with no effect on the rest of the plugin |
25
25
  | 📐 **Touch layout** | This repo now only keeps shell-level rules (iOS input-zoom fix, safe-area scroll padding, horizontal-scrolling code) — layout rules (44px targets, dialogs, composer chrome) moved to the interface half, see "Division of labor" below — desktop never affected |
26
26
  | 🔒 **Desktop unaffected** | Every rule is rooted at `html:not([data-lan-device="desktop"])` (or an `@media(max-width:820px)` with the same exclusion) — an explicit "desktop" kind opts out, everything else (including a real phone's default "auto" kind) opts in |
@@ -223,8 +223,8 @@ The exception is `/lan-gate/pair/claim` (POST) — the one endpoint reachable fr
223
223
  - Push payloads carry only a title and a short body line (e.g. "DSH task complete") — **never any conversation content**. Delivery is standard Web Push (VAPID-signed, aes128gcm-encrypted); only the push service and your browser ever see the plaintext.
224
224
  - Revoking a device deletes its push subscription too; a 404/410 from the push endpoint (expired subscription) gets it auto-cleaned on the next send.
225
225
  - Mobile browsers require HTTPS before they'll register a service worker at all, so both push and offline support depend on step 2's reverse proxy — neither works on a real device until HTTPS is in place.
226
- - The "notify when the agent finishes" wiring lives in the optional host plugin `dsh-push.mjs`: it listens on the DSH event bus and calls the local `/pwa/push/send`. Event names come from `DSH_PUSH_EVENTS` (comma-separated); the default `agent/turn-stopping` is the official turn-close checkpoint (fires once per turn when the model owes no response and no tool calls are live). Override the env var if your DSH version names it differently. `DSH_PUSH_DEBOUNCE_MS` (default 15000) sets the minimum gap between notifications. Want the turn's outcome in the notification body? Set `DSH_PUSH_SUMMARY=1` and the body becomes the turn's final assistant message (truncated to 120 chars). The push payload is aes128gcm-encrypted end to end — Google/Apple push servers only ever see ciphertext; the remaining exposure is your own lock screen / notification center (both OSes can hide notification content on the lock screen if that matters to you). You can also skip the plugin entirely and trigger pushes yourself: `curl -X POST http://127.0.0.1:3088/pwa/push/send -H 'Content-Type: application/json' -d '{"title":"DSH task complete"}'`.
227
- - "The model pushes on its own" is the same `dsh-push.mjs` additionally registering a model tool, `push_notify` (`title` required, `body` optional), over the same encrypted `/pwa/push/send` path. It only shows up when the host has a tool registry (`ctx.tools`) and hasn't disabled it; `pushTool: false` in `lan-gate.config.json` (or `DSH_PUSH_TOOL=0`) turns it off entirely. Rate limiting is independent from the turn-close notifier above: at most 1 push per session per 60 seconds, 20 total per hour across all sessions — over the limit, the call is silently skipped (not sent, not an error), so a chatty model can't turn your phone into a notification firehose.
226
+ - "When do I get pushed" is decided by the optional host plugin `dsh-push.mjs`: it listens on the DSH event bus and calls the local `/pwa/push/send`. Two legs. **Event leg (on by default, always fires):** a tool is waiting for your authorization (session event `approval/asked` with no matching `approval/decided` within 1.5s), or the model called `ask_user_question` and is waiting for your answer (session event `tool/call`). Neither is suppressed by the debounce, and both fire for subagents too. **Turn end (off by default):** for the old "buzz me when it finishes" behaviour set `DSH_PUSH_TURN_END=1` (this was the default before 1.0.3 now it must be turned on explicitly); once on, only top-level sessions push and a subagent finishing never does. Which events count as a turn end still comes from `DSH_PUSH_EVENTS` (comma-separated), default `agent/turn-stopping` — the official turn-close checkpoint; override it if your DSH version names it differently. `DSH_PUSH_DEBOUNCE_MS` (default 15000) sets the minimum gap between notifications. Want the turn's outcome in the body? Set `DSH_PUSH_SUMMARY=1` and the body becomes the turn's final **text** output (`text` blocks only, so the model's reasoning never leaks; a turn that produced no prose falls back to the last tool name; truncated to 120 chars). The push payload is aes128gcm-encrypted end to end — Google/Apple push servers only ever see ciphertext; the remaining exposure is your own lock screen / notification center (both OSes can hide notification content on the lock screen if that matters to you). You can also skip the plugin entirely and trigger pushes yourself: `curl -X POST http://127.0.0.1:3088/pwa/push/send -H 'Content-Type: application/json' -d '{"title":"DSH task complete"}'`.
227
+ - "The model pushes on its own" is the same `dsh-push.mjs` additionally registering a model tool, `push_notify` (`title` required, `body` optional), over the same encrypted `/pwa/push/send` path. Its description spells out both when to call it **and when not to** (listing only the former turns it into a per-turn reflex), and the same text is also injected as standing session context via `ctx.systemPrompt` — both come from one shared constant, so they cannot drift apart. It only shows up when the host has a tool registry (`ctx.tools`) and hasn't disabled it; `pushTool: false` in `lan-gate.config.json` (or `DSH_PUSH_TOOL=0`) turns it off entirely. Rate limiting is independent from the automatic notifications above: at most 1 push per session per 60 seconds, 20 total per hour across all sessions — over the limit, the call is silently skipped (not sent, not an error), so a chatty model can't turn your phone into a notification firehose. A push it does send resets the shared debounce clock, so an automatic notification right behind it is suppressed.
228
228
 
229
229
  ---
230
230
 
@@ -248,7 +248,9 @@ The exception is `/lan-gate/pair/claim` (POST) — the one endpoint reachable fr
248
248
 
249
249
  On iOS 26.x, once DSH is added to the home screen and opened as a standalone PWA, the layout viewport loses a chunk of its bottom edge (measured on one iPhone on 26.5: 852px screen vs. 793px viewport — exactly one status-bar's worth) from cold start onward, until the app is fully quit and reopened. The same URL in a plain Safari tab is unaffected.
250
250
 
251
- This is not a bug in this plugin — it's a known iOS 26.x system defect (the layout viewport permanently shrinks the first time the on-screen keyboard is shown inside a standalone PWA; `innerHeight`, `visualViewport.height` and `100dvh` all shrink together). The missing strip sits outside the document, so no stylesheet can reach it — only the system paints it, using the manifest's `background_color`. This repo changed that value to a light `#f9fafb` (matching the interface half's light theme background) so the dead strip blends into the page instead of standing out as a dark bar.
251
+ This is not a bug in this plugin — it's a known iOS 26.x system defect (the layout viewport permanently shrinks the first time the on-screen keyboard is shown inside a standalone PWA; `innerHeight`, `visualViewport.height` and `100dvh` all shrink together). The missing strip sits outside the document, so no stylesheet can reach it — only the system paints it, using the manifest's `background_color`. This repo keeps that value at a light `#f9fafb` (matching the interface half's light theme background) so the dead strip blends into the page instead of standing out as a dark bar.
252
+
253
+ > The Android bottom band is **not** painted from this value — do not change it to chase that. It is the Android system navigation bar, which follows the system dark-mode setting and is out of the page's reach.
252
254
 
253
255
  That's a visual mitigation, not a fix: in dark theme the strip is actually more visible (the manifest color can't follow the page theme), and it's also the launch-splash color, so the splash went from dark to light. The underlying shrink can only be fixed by Apple. the interface half applies two further mitigation layers (detection + an active reflow "heal") on its own side — see that plugin's README for details.
254
256
 
@@ -318,7 +320,7 @@ See [`AGENTS.md`](../AGENTS.md) for development conventions.
318
320
  - Removed the gateway's dead inline `DEVICE_CSS` copy, whose fullscreen-dialog rule used to stretch the interface half's session-info card off-screen — long misdiagnosed as an iOS/Chromium engine difference;
319
321
  - CSS/gesture gating switched from the literal `"phone"` value to "not desktop" — a real paired device defaults to kind `"auto"`, so the old gate never actually fired on a real phone;
320
322
  - Removed pull-to-refresh (an accidental overscroll used to fire a full reload mid-conversation); removed edge-swipe-back, handing that 24px zone to the interface half's own gesture (the old handler was a no-op against DSH's client-side routing anyway);
321
- - `manifest.json`'s `background_color` switched to a light color as a visual mitigation for the iOS 26.x standalone-PWA viewport shrink dead strip (known OS defect, not a fix — see "Known issue" above).
323
+ - `manifest.json`'s `background_color` switched to a light color as a visual mitigation for the iOS 26.x standalone-PWA viewport shrink dead strip (known OS defect, not a fix — see "Known issue" above). Unrelated to the Android navigation bar.
322
324
 
323
325
  **Internal**
324
326
 
@@ -15,7 +15,7 @@
15
15
  | 🧩 **可安装** | 主屏图标、`standalone` 显示、`apple-touch-icon`、maskable 图标 |
16
16
  | 🌐 **离线可用** | service worker(v3):只有真正的静态壳(manifest/图标/离线页)缓存优先,其余(DSH 客户端 JS/CSS、API、页面 HTML)一律网络优先——装了新版本手机上立刻吃到,不会像早期版本那样在部署之后还残留旧 CSS |
17
17
  | 👆 **触屏手势** | 捏合缩放字体(可重置);左缘返回手势已让位给界面半边(见下方「分工」),下拉刷新已整体移除(误触发全页重载会把人从对话中间弹回列表) |
18
- | 🔔 **任务完成推送** | 真 Web Push(VAPID 签名 + aes128gcm 加密),agent 干完活推送到手机,通知里不带对话内容 |
18
+ | 🔔 **卡住了就推送** | 真 Web Push(VAPID 签名 + aes128gcm 加密)。默认只在**真正需要你**的时候推:某个工具在等授权、模型在等你回答问题。「回合结束」默认不推(设 `DSH_PUSH_TURN_END=1` 打开)。通知里默认不带对话内容 |
19
19
  | 🛎️ **`push_notify` 工具** | 模型可主动调用的推送工具(在 `dsh-push.mjs` 里注册):任务中途要用户拿主意、跑到关键节点、或出错需要人来处理时,模型自己决定推一条到锁屏。纪律写在工具描述里明确要求模型别高频用;宿主侧再兜底限流(同会话 60 秒最多 1 条、全局每小时最多 20 条),超额直接不发送、不报错。同样是 aes128gcm 端到端加密,推送服务器只见密文,暴露面只有你自己的锁屏。`lan-gate.config.json` 里 `pushTool: false`(或 `DSH_PUSH_TOOL=0`)可整体关掉;宿主没装工具注册服务(`ctx.tools`)时自动跳过,不影响插件其余功能 |
20
20
  | 📐 **触屏布局** | 本仓库只留壳级规则(iOS 输入框防缩放、安全区滚动补偿、代码横向滚动)——排版类规则(44px 触摸目标、弹窗、composer 外观等)已交给界面半边,见下方「分工」——桌面零影响 |
21
21
  | 🔒 **桌面不受影响** | 所有规则都以 `html:not([data-lan-device="desktop"])`(或带同样排除条件的 `@media`)为根——只有显式标成「桌面」才会被排除,其余(包括真机默认的「自动」)都生效 |
@@ -264,7 +264,7 @@ sudo cloudflared service install # 通了再装成常驻服务
264
264
  }
265
265
  ```
266
266
 
267
- 字段名 = 环境变量去掉前缀转小驼峰:`port` / `host` / `targetPort` / `rateLimit` / `trustedProxies` / `vapidSubject`,推送半边是 `pushEvents` / `pushDebounceMs` / `pushSummary` / `pushTool`(`push_notify` 工具开关,默认 `true`)。挂载行支持 Cordis config 的 DSH 版本也可以把网关配置写在 insert 行的 `config:` 下(同名小驼峰字段),效果等同。
267
+ 字段名 = 环境变量去掉前缀转小驼峰:`port` / `host` / `targetPort` / `rateLimit` / `trustedProxies` / `vapidSubject`,推送半边是 `pushTurnEnd`(回合结束是否推送,默认 `false`)/ `pushEvents` / `pushDebounceMs` / `pushSummary` / `pushTool`(`push_notify` 工具开关,默认 `true`)。挂载行支持 Cordis config 的 DSH 版本也可以把网关配置写在 insert 行的 `config:` 下(同名小驼峰字段),效果等同。
268
268
 
269
269
  推送半边(`dsh-push.mjs`)随包自带,装包即挂载,**不需要在 profile 的
270
270
  `cordis.patch.yml` 里手写任何一行**。从旧的两包结构升级上来的话,profile patch
@@ -294,8 +294,8 @@ sudo cloudflared service install # 通了再装成常驻服务
294
294
  - 推送内容只有标题和一句简短正文(比如「DSH 任务完成」),**不携带任何对话内容**——走的是标准 Web Push(VAPID 签名 + aes128gcm 加密),只有推送服务商和你的浏览器能看到密文。
295
295
  - 设备被吊销时,它的推送订阅一并删除;推送目标返回 404/410(订阅已失效)时网关会自动清掉这条订阅。
296
296
  - 手机浏览器要求页面必须是 HTTPS 才会注册 Service Worker,所以推送和离线能力都依赖第 2 步配好的反代——反代没配好之前,这两项在真机上都不会生效。
297
- - 「agent 干完活自动推送」由可选宿主插件 `dsh-push.mjs` 负责:它监听 DSH 事件总线并调用本机 `/pwa/push/send`。事件名通过 `DSH_PUSH_EVENTS`(逗号分隔)配置,默认 `agent/turn-stopping`——官方文档定义的「回合即将关闭」检查点(模型不再欠响应、无存活工具调用时触发,每回合一次);如果你的 DSH 版本更旧/更新导致事件名不同,用该环境变量覆盖即可。`DSH_PUSH_DEBOUNCE_MS`(默认 15000)控制两条通知的最小间隔。想让通知带上这回合的结果摘要?设 `DSH_PUSH_SUMMARY=1`,通知正文会换成本回合最后一条助手消息(截 120 字)——推送 payload 本身是 aes128gcm 端到端加密的,Google/Apple 的推送服务器只见密文,剩下的暴露面是你自己的锁屏和通知中心(两大系统都支持「锁屏隐藏通知内容」,介意就开)。不装它也可以自己在任何脚本里 `curl -X POST http://127.0.0.1:3088/pwa/push/send -H 'Content-Type: application/json' -d '{"title":"DSH 任务完成"}'` 手动触发。
298
- - 「模型主动推送」由同一个 `dsh-push.mjs` 额外注册一个模型工具 `push_notify`(`title` 必填、`body` 可选),走的是同一条 `/pwa/push/send` 加密发送通道。宿主装了工具注册服务(`ctx.tools`)且没关闭时才会出现;`lan-gate.config.json` 的 `pushTool: false`(或环境变量 `DSH_PUSH_TOOL=0`)可以整体关掉。宿主侧限流独立于上面的自动推送:同一会话 60 秒内最多发 1 条,全部会话合计每小时最多 20 条,超出直接跳过(不发送、不算错误),避免模型高频调用把你手机刷成消息轰炸。
297
+ - 「什么时候推」由可选宿主插件 `dsh-push.mjs` 决定:它监听 DSH 事件总线并调用本机 `/pwa/push/send`。分两条腿。**事件腿(默认开、必推)**:某个工具在等你授权(会话事件 `approval/asked` 一秒半内没等到配对的 `approval/decided`),或者模型调了 `ask_user_question` 在等你回答(会话事件 `tool/call`)——这两类不受去抖压制,子代理里发生的也照推。**回合结束(默认关)**:想要「干完活就响一下」的老行为,设 `DSH_PUSH_TURN_END=1`(1.0.3 之前这是默认行为,现在改成了要显式打开);打开之后只有顶层会话会推,子代理跑完永远不推。算哪些事件仍由 `DSH_PUSH_EVENTS`(逗号分隔)配置,默认 `agent/turn-stopping`——官方文档定义的「回合即将关闭」检查点;DSH 版本不同导致事件名对不上时用它覆盖。`DSH_PUSH_DEBOUNCE_MS`(默认 15000)控制两条通知的最小间隔。想让通知带上这回合的结果摘要?设 `DSH_PUSH_SUMMARY=1`,正文会换成本回合的**最终文本回复**(只取 `text` 内容块,思考过程不会漏出来;整回合没说话就退回「最后执行了 <工具名>」;截 120 字)——推送 payload 本身是 aes128gcm 端到端加密的,Google/Apple 的推送服务器只见密文,剩下的暴露面是你自己的锁屏和通知中心(两大系统都支持「锁屏隐藏通知内容」,介意就开)。不装它也可以自己在任何脚本里 `curl -X POST http://127.0.0.1:3088/pwa/push/send -H 'Content-Type: application/json' -d '{"title":"DSH 任务完成"}'` 手动触发。
298
+ - 「模型主动推送」由同一个 `dsh-push.mjs` 额外注册一个模型工具 `push_notify`(`title` 必填、`body` 可选),走的是同一条 `/pwa/push/send` 加密发送通道。工具描述里同时写清了**该调**和**不该调**的场景(只写前者会变成每回合都调),同一段文字还通过 `ctx.systemPrompt` 作为会话上下文再强化一遍,两处共用同一个常量、不会各改各的。宿主装了工具注册服务(`ctx.tools`)且没关闭时才会出现;`lan-gate.config.json` 的 `pushTool: false`(或环境变量 `DSH_PUSH_TOOL=0`)可以整体关掉。宿主侧限流独立于上面的自动推送:同一会话 60 秒内最多发 1 条,全部会话合计每小时最多 20 条,超出直接跳过(不发送、不算错误),避免模型高频调用把你手机刷成消息轰炸;发出去之后它会把去抖时钟拨到当下,所以紧跟其后的自动推送会被压掉。
299
299
 
300
300
  ---
301
301
 
@@ -319,7 +319,9 @@ sudo cloudflared service install # 通了再装成常驻服务
319
319
 
320
320
  现象:iPhone 上把 DSH 加到主屏、以独立 PWA 打开后,视口底部会凭空少掉一截(实测 iPhone 一台 26.5 系统上是 852 屏幕高度对 793 视口高度,少了正好一条状态栏的高度),从冷启动那一刻就在,直到你把整个 app 彻底退出重开才会恢复;在普通 Safari 标签页里打开同一个网址则完全正常。
321
321
 
322
- 这不是本插件的 bug,是 iOS 26.x 的系统级缺陷(独立 PWA 里第一次弹出软键盘后,布局视口永久性变矮,`innerHeight`/`visualViewport.height`/`100dvh` 三个值一起变小,社区已有记录)。少掉的那截视口在文档范围之外,任何 CSS 都够不着,只能由系统自己拿背景色画上——本仓库把 `manifest.json` 的 `background_color` 从深色改成了 `#f9fafb`(和界面半边浅色主题背景一致),让这条系统画的死区尽量看起来像页面背景的延伸,而不是一条突兀的黑条。
322
+ 这不是本插件的 bug,是 iOS 26.x 的系统级缺陷(独立 PWA 里第一次弹出软键盘后,布局视口永久性变矮,`innerHeight`/`visualViewport.height`/`100dvh` 三个值一起变小,社区已有记录)。少掉的那截视口在文档范围之外,任何 CSS 都够不着,只能由系统自己拿背景色画上——本仓库把 `manifest.json` 的 `background_color` 设成 `#f9fafb`(和界面半边浅色主题背景一致),让这条系统画的死区尽量看起来像页面背景的延伸,而不是一条突兀的黑条。
323
+
324
+ > 安卓底部那条**不是**这个值画的,别顺手改这里去治它:那是 Android 系统导航栏,跟随系统夜间模式,网页够不着(见下方「安卓导航栏」)。
323
325
 
324
326
  这只是视觉缓解,不是根治:深色主题下这条带反而会更显眼(系统画的是 manifest 里那个固定颜色,没法跟着页面主题切换),而且它同时也是启动闪屏的颜色,所以闪屏从深色变成了浅色。真正的坏行为(视口变矮本身)只能等苹果修复系统缺陷。界面半边那边另外做了两层缓解(检测 + 主动摘窗重排),细节见该插件的 README。
325
327
 
@@ -398,7 +400,7 @@ npm test # 起 mock 上游,跑 gateway/auth/push 三组测试:反代与注
398
400
  - 删掉网关内联的 `DEVICE_CSS` 死代码副本——其中一条全屏弹窗规则会把界面半边的会话信息卡撑满屏并溢出视口,此前一直被误判为「iOS 内核差异」;
399
401
  - CSS/手势的 `data-lan-device` 判定从字面量 `"phone"` 改成排除 `"desktop"`——真机配对后默认是 `"auto"`,此前这个判定条件让相关补丁在真机上从未生效过;
400
402
  - 下拉刷新移除(误触发全页重载会把人从对话中间弹回列表);边缘返回手势移除,让位给界面半边的左缘手势(原实现对 SPA 路由本就是空操作);
401
- - manifest 的 `background_color` 改成浅色,视觉缓解 iOS 26.x 独立 PWA 视口收缩留下的系统死区(已知系统缺陷,非根治,见「已知问题」一节)。
403
+ - manifest 的 `background_color` 改成浅色,视觉缓解 iOS 26.x 独立 PWA 视口收缩留下的系统死区(已知系统缺陷,非根治,见「已知问题」一节)。安卓底部导航栏与该值无关。
402
404
 
403
405
  **内部**
404
406