文档
移动伴侣
用于配对桌面的 iOS 客户端,具有证书固定、项目审查和共享助理会话。
从英文自动翻译。 查看英文原文
平台和能力
apps/mobile 是一个使用 React Native 57.0.86、React 3.19、Expo Router、NativeWind、Reanimated 和 Gesture Handler 的 Expo SDK 2 应用程序。其支持的固定网络传输由本地在 Swift 中实现 companion-net 模块。 iOS 包标识符是 com.cadcamfun.mobile.
手机读取项目、查看历史记录和拉取请求、调度工作流程、接收活动,并与配对桌面上运行的助手会话进行交互。桌面继续拥有数据库、几何计算、AI 提供商凭证和工作流程执行。这些操作需要一个可访问、正在运行的桌面。
使用原生 iOS 构建
Expo Go 不包括 companion-net 并且无法连接到配对的桌面。当固定模块不可用时,本机请求会失败。 Web 开发后备使用浏览器获取和本地存储;它不提供本机证书固定。
应用结构
Expo Router 将连接屏幕与经过身份验证的应用程序选项卡分开。 app/connect.tsx 处理配对输入, app/pair.tsx 接受深层链接,并且 app/(app) 包含项目、助手、活动和设置。项目屏幕包括概述/历史记录/拉取/工作流程面板、提交详细信息和拉取请求审查。
components/:可重用控件、项目面板、助理记录和连接 UI。lib/client.ts:通过注入的传输类型键入客户端; API 类型无需服务器运行时即可导入。lib/pairing.ts,native.ts, 和storage.ts:配对验证、传输选择和持久凭据。lib/sse.ts,live-events.ts, 和chat-log.ts:流解析、实时订阅和转录状态。modules/companion-net/:Expo 模块和 Swift 固定 HTTPS 实现。
传输和网关范围
伴随网关与 sidecar 的环回监听器是分开的。默认情况下它是关闭的。 Enabling it starts an HTTPS server on 0.0.0.0,使用端口 8443 除非选择了另一个有效端口。该服务保持其启用状态,并在 sidecar 启动后恢复。桌面防火墙和网络路由必须允许电话到达此侦听器。
电话发送可撤销的设备令牌。网关存储其 SHA-256 哈希,根据允许列表检查请求方法/路径,并使用桌面令牌将接受的请求转发到环回 API。手机未收到该启动令牌。无效的身份验证返回 401,拒绝的路由返回 403,重复失败的身份验证可能返回 429,不可用的上游返回 502。
Allowed families include project reads; version-control, pull-request, and workflow endpoints; project events; notifications; assistant status/models/agents; streaming chat; queue/abort; permissions; and session state/control. Files, credentials, plugins, billing, gateway administration, and GitHub remote/push/pull endpoints are excluded.
设备访问包括写操作
该网关当前允许版本控制提交、分支、签出和恢复请求以及拉取请求审查和合并。设备令牌的范围仅限于网关路由策略;它们不是只读角色或每个项目的限制。在桌面配套设置中撤销丢失或不受信任的手机。
网关限制失败的身份验证尝试:在 60 秒内从一个 IP 地址进行 10 次失败的承载尝试后,来自该地址的进一步请求将接收 HTTP 429,直到窗口到期(apps/sidecar/src/services/companion-gateway.ts).
助理会话和离线消息
同伴将聊天请求发送至 POST /api/ai/chat/stream 并解析服务器发送的事件。会话标识符允许桌面对话的继续。客户端可以引导或排队消息、中止转弯、回答问题以及回复权限请求。在桌面上配置模型提供者;使用前手机会查询AI状态。
如果流在没有终端的情况下结束 done 或 error 帧并且未被取消,客户端报告连接不完整。本机传输在取消和完成时关闭其流句柄。本机解码保留字节块之间不完整的 UTF-8 标量。
无法发送的助手消息可以进入持久队列。它最多保留 50 个条目,满时丢弃最旧的条目,并重试前台和连接恢复。失败的条目仍排队。该队列存储消息以供稍后执行;它不是项目几何图形的离线副本或离线 AI 运行时。
构建并运行
bun install
bun run --cwd apps/mobile test
bun run --cwd apps/mobile typecheck
bun run --cwd apps/mobile build
bun run --cwd apps/mobile ios
bun run --cwd apps/mobile ios:release --device 构建命令将 iOS JavaScript 包导出到 apps/mobile/dist;它不生产 IPA。 iOS 运行命令需要 macOS、Xcode 和 CocoaPods。设备安装还需要受信任的设备、开发人员模式以及为生成的 Xcode 项目配置的 Apple 签名团队。 native:generate 运行适用于 iOS 的 Expo prebuild,无需安装 pod。对于日常工作来说, start (别名 dev) 运行 Expo 开发客户端捆绑器, ios:dev 在 localhost 上启动它并打开 iOS 模拟器, ios:build 编译调试应用程序而不启动捆绑程序,并且 web 启动 Expo Web 目标。这 test 脚本运行 bun test test 里面 apps/mobile.
.github/workflows/ci.yml 定义伴随类型检查、逻辑测试和 JavaScript 导出。 .github/workflows/ios.yml 生成 Xcode 项目,构建未签名的发布模拟器应用程序,将其与实时 sidecar 配对,检查经过身份验证的项目列表,并捕获屏幕截图。这些配置的检查不会建立物理设备、签名、TestFlight 或真实提供商的接受。
存储和设备安全
在本机 iOS 上,配对凭据存储在 Expo SecureStore 中。配对条目使用 WHEN_UNLOCKED_THIS_DEVICE_ONLY。脱机队列条目和会话可见标记也使用 SecureStore。浏览器后备使用本地存储进行开发。
该应用程序查询生物识别功能并使用 expo-local-authentication 用于本地身份验证,并启用系统密码回退。生物识别 UI 不会更改网关的服务器端路由权限。设备撤销由配对桌面控制。
桌面将其网关证书和私钥保存在其应用程序数据伴随目录中;密钥是使用仅限所有者的权限写入的。替换该证书会更改固定指纹并需要新的可信配对。
项目审核工作流程
- 打开项目并选择一个桌面项目。
- 检查其工作状态、分支、提交、拉取请求或工作流程运行。
- 打开提交以查看其差异,或打开拉取请求以查看更改并提交结论。
- 当适合项目时,使用可用的合并/关闭或工作流调度操作。
- 使用选定的项目上下文打开 Assistant 以继续或启动 AI 会话。
所有更改都在桌面上执行。网络中断、授权撤销或桌面停止阻止新的服务器工作;稍后重试本地排队的助理消息。
客户端界面
CompanionClient 接受一个 Transport with JSON request and text-stream methods. It exposes project reads; VCS status, history, diff, commit, branch, checkout, and restore; pull requests and reviews; workflow runs and dispatch; AI status/models/agents; session control; notifications; and project event streams.
JSON 失败变为 CompanionError 带有状态和消息。流式聊天和实时事件使用共享 SSE 解析器,因此任意网络块边界不会成为应用程序事件边界。测试注入传输,无需导入本机模块或运行模拟器。
配对手机
- 在桌面上,打开“设置”及其移动配套面板,或者
/settings/companion,然后打开网关。 - 创建命名设备配对并显示二维码。该链接包含 HTTPS 候选主机、设备令牌和证书指纹。
- 在 iOS 上,扫描二维码或粘贴
cadcamfun://pair关联。出现提示时允许本地网络访问。 - 该应用程序验证链接,与广告的候选主机进行竞争,并保留第一个成功的健康连接。
- 确认设备出现在桌面设置中。使用桌面的撤销操作来删除其访问权限。
候选主机包括合适的私有 IPv4 和覆盖网络地址;网关最多通告六个已发现的地址。当前侦听器是 IPv4,因此不会公布发现的 IPv6 地址。 VPN 或覆盖网络必须已提供供远离本地网络使用的路由;配对不会创建中继或 VPN。
证书固定
Swift 实现使用短暂的 URLSession 具有信任委托的实例。仅当其 DER 字节的 SHA-256 哈希等于配对的指纹时,它才接受叶证书。请求需要 HTTPS,重定向被拒绝,并且本机调用者永远不会默默地降级到浏览器传输。
因此,证书信任来自配对链接。将 QR 码和粘贴的链接视为凭据。应通过桌面配对流程替换具有意外来源或指纹的链接,而不是绕过 TLS 验证。
诊断连接
- 无本地运输: 安装包含本地模块的 iOS 开发或发布版本。
- 无法访问主机: 检查本地网络权限、桌面防火墙、通告地址、网关端口和网络路由。
- 401: 设备令牌无效或已撤销;创建一个新鲜的配对。
- 403: 端点位于同伴允许列表之外。
- 证书不匹配: 与当前桌面证书再次配对。
- 助理不可用: 检查桌面提供程序配置以及 sidecar 是否正在运行。
电话设置和本地状态
| 环境 | 默认 | 行为 |
|---|---|---|
| 外貌 | 系统 | 选择系统、浅色或深色。系统遵循 iOS,直到被覆盖。 |
| 触觉 | 启用 | 选择刻度和成功/错误反馈具有单独的切换。 |
| 人脸识别锁 | 残疾人 | 启用需要可用的、注册的生物识别信息和成功的系统身份验证。 |
| 锁定背景 | 启用 | 启用生物识别锁后,切换到非活动/后台会锁定配对的应用程序。 |
| 桌面连接 | 当前配对 | 显示地址和指纹前缀;点击桌面行可重新检查连接性。 |
| 排队的消息 | 零 | 当无法访问桌面时显示待处理的助手消息。 |
UI 首选项位于 AsyncStorage 下 cadcamfun.ui.v1;配对凭证、排队消息和已见会话标记使用 SecureStore。 UI 存储会记住一个配对桌面的所选项目和每个项目的最后一个会话。更改桌面标识会清除记住的项目/会话映射,因此不会在不相关的服务器上重复使用它。
锁定协调器会跳过启动和连接屏幕,并显示配对应用程序的全屏解锁覆盖图。它不会暂停服务器端会话或撤销令牌。取消配对会删除手机保存的配对并返回“连接”;桌面撤销是防止令牌再次进行身份验证的单独操作。
网关管理示例
这些请求针对桌面环回 API 及其启动令牌运行,而不是针对电话网关。使用隔离的开发实例。将示例端口替换为其就绪事件中的端口,并提供该进程的令牌。
CADCAM_URL=http://127.0.0.1:43111
CADCAM_TOKEN=local-development-token
curl --fail-with-body "$CADCAM_URL/api/companion" -H "Authorization: Bearer $CADCAM_TOKEN"
curl --fail-with-body -X POST "$CADCAM_URL/api/companion/enable" -H "Authorization: Bearer $CADCAM_TOKEN" -H 'Content-Type: application/json' -d '{"port":18443}' 状态响应包含 enabled, 一个可为空的 port, 一个可为空的 fingerprint,建议的 host, 和 devices。该服务接受从 1024 到 65535 的显式端口。已使用的端口无法启动监听;选择另一个端口并重新生成配对地址。
curl --fail-with-body -X POST "$CADCAM_URL/api/companion/pair" -H "Authorization: Bearer $CADCAM_TOKEN" -H 'Content-Type: application/json' -d '{"name":"Development iPhone","host":"192.168.1.20"}' 将示例主机替换为电话可以路由到的地址。成功的配对创建将返回与设备的 HTTP 201 id 和一个带有凭证的 link。修剪后的名称必须包含 1–60 个字符。私下扫描/粘贴该链接;不要将其复制到错误报告中。创建令牌不会自动打开网关。
要删除访问权限,请发送 DELETE /api/companion/devices/<id> 使用桌面授权。要停止所有新网关流量,请发送 POST /api/companion/disable。每个请求都会检查设备哈希,因此在后续请求中会拒绝已撤销的令牌。
在不丢失上下文的情况下进行恢复
- 点击“设置”中的桌面连接行,以针对所公布的主机重试运行状况。
- 如果只有一条网络路径出现故障,请验证 Wi-Fi/热点/VPN 路由是否正确,并在桌面地址更改后重新生成二维码。
- 如果授权失败,请撤销桌面上的过时设备并创建新的配对。
- 如果只有辅助流失败,请重新打开项目会话并检查其状态,然后重新发送相同的指令。
- 在提交重复提示之前检查排队消息计数;排队的操作稍后重试。
离线队列具有有限的保留,并通过携带存储的会话标识符(如果可用)的新聊天请求来重放每个挂起的操作。它不是一次性交付保证。服务器执行后网络中断可能会让客户端不确定是否完成;在重复更改之前检查生成的项目/会话状态。
实施锚点: app/(app)/settings.tsx, components/layout/AppLockCoordinator.tsx, lib/store.ts, lib/server.tsx, 和 lib/offline-queue.ts.
网关允许列表
电话仅通过配套网关与桌面通信,该网关承认这组固定的路由并将它们转发到环回 API:
| 方法 | 路径图案 | 目的 |
|---|---|---|
| GET | /api/health | 可达性检查。 |
| GET | /api/projects, /api/projects/:id | 项目列表和标题。 |
| 获取、发布 | /api/projects/:id/vcs/* (除了 remote, push, pull), /pulls/*, /actions/* | 历史记录、拉取请求、评论和工作流程运行。 GitHub 同步保留在桌面上,因为它需要所有者的 GitHub 令牌。 |
| GET | /api/projects/:id/events | 实时 SSE 历史记录、工作流程和助理活动流;手机唯一的推送通道。 |
| 获取、发布、放置、删除 | /api/notifications, /mark-all-read, /:id | 阅读、标记已读和删除通知。 |
| GET | /api/ai/status, /models, /agents | 助理发现。 |
| POST | /api/ai/chat/stream, chat/abort, chat/queue, permission/reply, sessions/state, sessions/control | 助理会议。 |
由于列表是固定在 sidecar 中的,因此需要其他路由(例如文件上传或 GitHub 推送)的功能在列表更改之前是仅限桌面的功能。
手机存储什么
| 物品 | 贮存 | 笔记 |
|---|---|---|
| 网关源 | SecureStore | 首选主机加上来自配对链路的备用主机。 |
| 设备令牌 | 安全存储, WHEN_UNLOCKED_THIS_DEVICE_ONLY | 除非作为网关的承载标头,否则绝不会离开设备。 |
| 证书指纹 | SecureStore | 在每个连接上与网关的证书进行比较。 |
| 离线队列、会话标记 | SecureStore | 离线时撰写的消息以及上次查看的会话状态。 |
在浏览器开发版本中,相同的数据回退到未加密的本地存储。仅将其与一次性配对一起使用。
屏幕
| 路线文件 | 屏幕 |
|---|---|
app/index.tsx | 进入:无配对→配对界面;配对 → 项目 |
app/connect.tsx, app/pair.tsx | 配对:用相机扫描二维码或粘贴链接 |
app/(app)/projects/index.tsx | 项目清单 |
app/(app)/projects/[id].tsx | 一个项目 |
app/(app)/projects/[id]/commit/[oid].tsx | 提交及其更改 |
app/(app)/projects/[id]/pulls/[number].tsx | 拉取请求:审查和合并 |
app/(app)/assistant.tsx | 助理聊天 |
app/(app)/activity.tsx | 现场活动 |
app/(app)/settings.tsx | 连接和设备设置 |
路由使用 Expo Router 文件约定: (app) 是一个共享布局而不添加 URL 段的组,并且 [id] 是一个参数。
离线助手队列
当无法到达桌面时,在助手中输入的消息会在电话上排队而不是丢失:
| 规则 | 价值 |
|---|---|
| 在哪里 | SecureStore,因此被杀死的应用程序会保留队列 |
| 存储了什么 | projectId, text, 选修的 sessionId, agent, mode (常规、cad、cam、gcode、刀具路径;默认常规)和 queuedAt |
| 容量 | 50 个条目;最旧的先被丢弃 |
| 当它排水时 | 当应用程序以存储的配对返回前台时 |
| 如何 | 每个条目都会打开一个新的流转并读取直到 done;一个 error 事件或后台转换使其排队 |
| 反馈 | 祝酒词:“已发送排队消息”或“已发送 N 个排队消息” |
只有连接失败才会对消息进行排队。当客户端报告状态 0 或错误文本提及网络故障、超时、拒绝连接或类似情况时,发送将被视为离线; HTTP 和业务错误(“请求……因……而失败”)会显示出来,而不是排队。