Skip to content

功能规格说明:PWA 集成 ​

创建日期:2026-10-13(2026-10-30 从 ui/remote-control.md 拆出)

本文只讲装到主屏之后的那一层:主屏上的图标与名字、接收推送的 Service Worker、以及锁屏时也能到达的两条通知。页面本身见 remote-web.md,触控设备的尺寸与字号见 remote-mobile.md。

用户场景与测试 (必填) ​

用户故事:PWA 主屏元信息——真实图标与具名(优先级:P1) ​

作为把页面「添加到主屏幕」的用户,我希望主屏上出现的是一个可辨识的图标和名字,而不是灰色默认图标加一段截断的域名。

为什么是这个优先级:主屏入口是这个功能在手机上的主要形态;没有元信息,「添加到主屏幕」这个动作产出的东西既认不出也不愿点。图标只有一份、不分机器——机器的区分是页面内机器列表的事(见 remote-machine.md 的「机器列表与切换」故事),这正是「一个图标管所有机器」的字面义。

独立测试:在 iPhone Safari 打开页面地址 → 分享 → 「添加到主屏幕」,得到具名图标(非灰默认)与名字;点开以 standalone(无浏览器工具栏)形态打开;构建产物里含 manifest 与真实 PNG 图标(内容哈希命名,随 zip 走既有上传通道)。

验收场景:

  1. 假设 页面被加到主屏幕,则 产物提供全部元信息:manifest(name / short_name、display: standalone、start_url 指页面根——不带查询参数,机器的选择属于页面内的状态而不是入口的身份;主题色取页面深色主题)、真实 PNG 图标(以品牌应用图标为源,一份内容哈希文件同时充当 favicon 与 apple-touch-icon,替换掉现在的空图标 data:,)、apple-mobile-web-app-title(短名,主屏宽度内不截断,取品牌名)。iOS 用 apple-touch-icon 与这个短名渲染主屏条目;standalone 用 manifest 与 meta 两条路都声明——各代 iOS 各取其一。
  2. 假设 浏览器要取 manifest 与图标,则 CSP 必须放行:manifest-src 缺省回落到 default-src 'none',必须显式加 'self';图标走 img-src 'self'(已有)。
  3. 假设 产物走既有上传通道,则 不需要收端配合:.png 与 .json(manifest 以 manifest.json 命名,application/json 浏览器接受)都已在运营平台上传接口的扩展名白名单与 MIME 表里(2026-10-15 对收端源码核实)。
  4. 假设 用户在电脑侧开启了远程控制、或在页面上看到机器列表,则 两处各有一句引导:用相机扫码或在 Safari 里打开,再「添加到主屏幕」;不要用控制中心的扫码器——它把链接开在沙箱容器里,没有 Safari 的会话存储,既不保留页面也加不了主屏(2026-10-14 真机实测)。iOS 没有 beforeinstallprompt,引导只能是文案。

用户故事:锁屏也能知道——会话等着我、或者跑完了(优先级:P1) ​

作为把页面加到主屏、然后锁屏揣兜里的人,我希望「某条对话在等我确认」和「某条对话已经跑完」这两件事能作为系统通知推到我手机上,点开直达那条对话,以便不必一直盯着页面。

为什么是这个优先级:页面在后台会被 iOS 冻结、长连必断(remote-channel.md 的「断线自愈」故事已把接回做掉),没有推送就只剩「主动打开页面才知道」。而「人不在电脑前也能把卡住的会话推下去」正是远程控制不可替代的那一半——中继侧的推送基建(VAPID 密钥、订阅登记、requires_action 触发与限流)已经就绪,缺的只是页面这半。跨仓依赖:两条都已在中继侧落地——待确认那条本来就有;「已完成」那条 2026-10-16 放行(判据=真正的 running → idle 跳变,文案与载荷见 codechat !193,已按该分支构建部署到测试环境、MR 未合),属 codechat 仓库的改动。

独立测试:手机把页面添加到主屏并授权通知 → 电脑侧触发一次工具审批 → 锁屏收到「有会话等待确认」,点开直达该对话的审批卡;批准后这一轮跑完,再收到「有会话已完成」,点开仍直达该对话;页面在前台且正显示该对话时,同样两件事都不弹横幅(页面内的卡片与会话列表标记照常)。

验收场景:

  1. 假设 某会话进入待确认——工具审批、AskUserQuestion 提问、或 ExitPlanMode 计划待批准,则 手机收到一条推送「<机器名> · 有会话等待确认」,点开直达该对话。载荷不含会话标题、也不含命令或消息内容:中继只拿得到「哪个会话、什么状态」这两个非内容字段(见边界情况「不解析内容」)。这三类共用同一条信号——requires_action 的判据就是该会话有待确认请求(listSessionStatuses),而提问与计划批准即使在 bypassPermissions 下也照样提示(permissionManager 的 USER_INTERACTION_REQUIRED_TOOLS)。
  2. 假设 某会话从「运行中」变为「空闲」(一轮生成结束、后台任务收尾——running 本就含后台任务),则 收到一条推送「<机器名> · 有会话已完成」,点开直达该对话。依据:桌面端把「已完成」交给侧栏绿点(2026-09-10 拍板移除「已完成」toast),但那条理由在这里不成立——页面没有已读跟踪(remote-web.md 的「待确认只在它自己的对话里弹」故事场景 6 已明确不做绿点),且页面不可见时用户眼前什么都没有,「跑完了」与「等着我」一样是只有推送能送达的信息。这是与桌面端的刻意不一致,理由在此。
  3. 假设 同一会话重复上报同一状态(仍是 requires_action、或仍是空闲),则 不重复推送——只在「进入」该状态的那一次推。假设 同一台机器 60 秒内推送超过 30 次,则 超出部分只更新状态、不投递(中继既有兜底:PUSH_RATE_MAX = 30 / PUSH_RATE_WINDOW_SECONDS = 60;机器级天花板不变,只是「已完成」另有一层更严的子限=一半预算,免得一轮一条的它把「待确认」挤掉——判据在被挡掉时不消耗共用桶)。
  4. 假设 页面此刻可见(Service Worker 的 clients 里有可见页面),则 不弹系统横幅,只在页面内呈现(该弹的卡照弹、会话列表标记照标);页面不可见(后台、锁屏、已关闭)时才弹。依据:与桌面端「会话已显示在某个分屏就不弹 toast」同款口径(../desktop/desktop-sessions.md「后台会话确认提醒」场景 2)——用户正看着那一页时,横幅是重复打扰。
  5. 假设 用户点开推送,则 打开 /remote/?host=<hostId>&session=<sessionId> 并落在该对话上(与扫码入口、中继落地页模板同一个形状);该会话若已不存在,则 落在会话列表并给出明确状态,不白屏。
  6. 假设 用户在页面上订阅或注销通知,则 浏览器订阅经中继登记(POST /api/relay/push/subscriptions)与删除(DELETE /api/relay/push/subscriptions/:id),VAPID 公钥取自 GET /api/relay/push/public-key。页面同时自己注册那个接收推送的 Service Worker(与其余资源一样哈希命名、由构建把名字填进去,scope 落在页面自己的目录),因此 CSP 要显式写 worker-src 'self'——缺省时它回落到 script-src 'self',这一行是把那条回落写明白而不是放宽;注册不依赖通知权限、也不依赖中继或登录态,所以它在页面启动时完成,不带任何请求。
  7. 假设 运行在 iOS 且页面尚未添加到主屏,则 页面给出「添加到主屏幕后才能开启通知」的引导——iOS 16.4 起 Web Push 只对主屏 web app 可用,普通 Safari 标签页拿不到 PushManager,不静默失败;假设 浏览器不支持推送、或用户拒绝通知权限,则 同样明确呈现,页面其余功能不受影响。

边界:不做 APNs、不做原生 App(形状已定)。推送里只有机器名、会话 id 与状态词这些非内容信息——会话标题不进推送:要带标题就得让本机把内容上报给中继,与「中继不落内容」的隐私边界冲突(2026-10-16 拍板)。

未落地与待决 ​

  • **Web Push 推送(2026-10-16 拍板并实现页面侧,真机未验):页面侧已落地——remote/sw.js(Service Worker:只做推送,不缓存页面)由 shim 在 boot() 里注册(名字经 build.mjs 填成哈希名),MoreMenu 的「通知」入口按 shim 回的四个事实渲染(可开 / 可关 / 三条「用不了」的说明),订阅经 GET /api/relay/push/public-key + POST /api/relay/push/subscriptions 登记、在 DELETE /api/relay/push/subscriptions/:id 注销(收端三端点与载荷形状已对 codechat origin/main 核实:{publicKey}、{endpoint, keys{p256dh,auth}} → {id}、载荷 {title, body, url, data{hostId, sessionId, state}})。用例见 packages/webview/tests/remote/shimPush.test.ts(订阅生命周期)、packages/webview/tests/remote/sw.test.ts(可见页面不弹横幅、点击直达会话)与 packages/webview/tests/webview/remotePagePushToggle.test.tsx(入口文案:四档可操作的开关态 + 三条「用不了」的说明)。口径见「锁屏也能知道」故事:四类待确认(工具审批 / 提问 / 计划批准)共用一条推送、「已完成」也推、不带会话标题、页面可见时不弹横幅。仍未做的:ⓐ 真机(真中继 + 真浏览器 + 锁屏)确认整条链路,尤其 iOS 只在「添加到主屏幕」后才拿得到 PushManager——中继那半 2026-10-16 已放行(running → idle 也推、data.state 改传真实状态,属 codechat !193,已按该分支构建部署到测试环境、MR 未合),本机那半仍待把页面产物上传后才能一起验。