Appearance
功能规格说明: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 走既有上传通道)。
验收场景:
- 假设 页面被加到主屏幕,则 产物提供全部元信息: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 各取其一。 - 假设 浏览器要取 manifest 与图标,则 CSP 必须放行:
manifest-src缺省回落到default-src 'none',必须显式加'self';图标走img-src 'self'(已有)。 - 假设 产物走既有上传通道,则 不需要收端配合:
.png与.json(manifest 以manifest.json命名,application/json浏览器接受)都已在运营平台上传接口的扩展名白名单与 MIME 表里(2026-10-15 对收端源码核实)。 - 假设 用户在电脑侧开启了远程控制、或在页面上看到机器列表,则 两处各有一句引导:用相机扫码或在 Safari 里打开,再「添加到主屏幕」;不要用控制中心的扫码器——它把链接开在沙箱容器里,没有 Safari 的会话存储,既不保留页面也加不了主屏(2026-10-14 真机实测)。iOS 没有
beforeinstallprompt,引导只能是文案。
用户故事:锁屏也能知道——会话等着我、或者跑完了(优先级:P1)
作为把页面加到主屏、然后锁屏揣兜里的人,我希望「某条对话在等我确认」和「某条对话已经跑完」这两件事能作为系统通知推到我手机上,点开直达那条对话,以便不必一直盯着页面。
为什么是这个优先级:页面在后台会被 iOS 冻结、长连必断(remote-channel.md 的「断线自愈」故事已把接回做掉),没有推送就只剩「主动打开页面才知道」。而「人不在电脑前也能把卡住的会话推下去」正是远程控制不可替代的那一半——中继侧的推送基建(VAPID 密钥、订阅登记、requires_action 触发与限流)已经就绪,缺的只是页面这半。跨仓依赖:两条都已在中继侧落地——待确认那条本来就有;「已完成」那条 2026-10-16 放行(判据=真正的 running → idle 跳变,文案与载荷见 codechat !193,已按该分支构建部署到测试环境、MR 未合),属 codechat 仓库的改动。
独立测试:手机把页面添加到主屏并授权通知 → 电脑侧触发一次工具审批 → 锁屏收到「有会话等待确认」,点开直达该对话的审批卡;批准后这一轮跑完,再收到「有会话已完成」,点开仍直达该对话;页面在前台且正显示该对话时,同样两件事都不弹横幅(页面内的卡片与会话列表标记照常)。
验收场景:
- 假设 某会话进入待确认——工具审批、
AskUserQuestion提问、或ExitPlanMode计划待批准,则 手机收到一条推送「<机器名>· 有会话等待确认」,点开直达该对话。载荷不含会话标题、也不含命令或消息内容:中继只拿得到「哪个会话、什么状态」这两个非内容字段(见边界情况「不解析内容」)。这三类共用同一条信号——requires_action的判据就是该会话有待确认请求(listSessionStatuses),而提问与计划批准即使在bypassPermissions下也照样提示(permissionManager的USER_INTERACTION_REQUIRED_TOOLS)。 - 假设 某会话从「运行中」变为「空闲」(一轮生成结束、后台任务收尾——
running本就含后台任务),则 收到一条推送「<机器名>· 有会话已完成」,点开直达该对话。依据:桌面端把「已完成」交给侧栏绿点(2026-09-10 拍板移除「已完成」toast),但那条理由在这里不成立——页面没有已读跟踪(remote-web.md的「待确认只在它自己的对话里弹」故事场景 6 已明确不做绿点),且页面不可见时用户眼前什么都没有,「跑完了」与「等着我」一样是只有推送能送达的信息。这是与桌面端的刻意不一致,理由在此。 - 假设 同一会话重复上报同一状态(仍是
requires_action、或仍是空闲),则 不重复推送——只在「进入」该状态的那一次推。假设 同一台机器 60 秒内推送超过 30 次,则 超出部分只更新状态、不投递(中继既有兜底:PUSH_RATE_MAX = 30/PUSH_RATE_WINDOW_SECONDS = 60;机器级天花板不变,只是「已完成」另有一层更严的子限=一半预算,免得一轮一条的它把「待确认」挤掉——判据在被挡掉时不消耗共用桶)。 - 假设 页面此刻可见(Service Worker 的
clients里有可见页面),则 不弹系统横幅,只在页面内呈现(该弹的卡照弹、会话列表标记照标);页面不可见(后台、锁屏、已关闭)时才弹。依据:与桌面端「会话已显示在某个分屏就不弹 toast」同款口径(../desktop/desktop-sessions.md「后台会话确认提醒」场景 2)——用户正看着那一页时,横幅是重复打扰。 - 假设 用户点开推送,则 打开
/remote/?host=<hostId>&session=<sessionId>并落在该对话上(与扫码入口、中继落地页模板同一个形状);该会话若已不存在,则 落在会话列表并给出明确状态,不白屏。 - 假设 用户在页面上订阅或注销通知,则 浏览器订阅经中继登记(
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',这一行是把那条回落写明白而不是放宽;注册不依赖通知权限、也不依赖中继或登录态,所以它在页面启动时完成,不带任何请求。 - 假设 运行在 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注销(收端三端点与载荷形状已对codechatorigin/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 未合),本机那半仍待把页面产物上传后才能一起验。