- Segment streaming Markdown so completed blocks are reused and only the growing tail is re-parsed; fall back to whole-document parsing for document-wide constructs - Memoize conversation sidebar rows and cache per-conversation derived activity, ordering and pending approvals - Activity page loads on entry, refreshes every 15 s while visible, and has a single header refresh button - Include pending settings, model download source, device sharing and knowledge gateway changes in the working tree - Add Electron streaming input benchmark and regression tests
248 lines
11 KiB
Markdown
248 lines
11 KiB
Markdown
# 设备发现与共享技术设计
|
||
|
||
## 文档信息
|
||
|
||
| 项目 | 内容 |
|
||
| --- | --- |
|
||
| 状态 | 本地元数据首期已接入;完整共享网络为后续设计 |
|
||
| 版本 | 0.1 |
|
||
| 日期 | 2026-09-03 |
|
||
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
|
||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||
| 共享协议 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
|
||
|
||
## 当前首期实现
|
||
|
||
本节实现 [FR-M1 至 FR-M5](./prd.md#当前首期本地元数据)。后文模块树、SQLite 表、共享协议包、
|
||
局域网与网关协议都是后续设计,当前不创建这些基础设施。
|
||
|
||
`src/shared/device-sharing-contracts.ts` 定义严格输入和返回类型;`DesktopApi.sharing` 暴露
|
||
`getSettings`、`saveSettings`、`registerDevice`、`getCatalog`、`publish`、`revoke` 六个方法。
|
||
生产 preload 对应六个固定 IPC 通道,`device-sharing-ipc.ts` 对每次调用检查可信主窗口主帧,
|
||
对变更输入进行 Zod 校验,不暴露通用网络入口。
|
||
|
||
`DeviceSharingService` 在 Main 内使用 `fetch`,每个请求含响应体读取采用 10 秒超时,非成功
|
||
响应保留 HTTP 状态;响应再经 Zod 校验。请求只在页面读取或用户动作时发出,不启动后台发现。
|
||
URL 接受 HTTP/HTTPS,禁止内嵌凭据、查询和片段。没有令牌或 Agent/Runtime 改动。
|
||
|
||
| 方法 | HTTP 请求 |
|
||
| --- | --- |
|
||
| 注册 | `POST /api/v1/sharing/devices/register`,字段为 `id,name,platform,appVersion` |
|
||
| 设备列表 | `GET /api/v1/sharing/devices`,读取 `{devices:[]}` |
|
||
| 发布列表 | `GET /api/v1/sharing/publications`,读取 `{publications:[]}` |
|
||
| 发布 | `POST /api/v1/sharing/publications`,Main 生成条目 ID 并填入本机设备 ID |
|
||
| 撤销 | 先按 `?deviceId=` 获取本机条目,再 `POST /api/v1/sharing/publications/:id/revoke` |
|
||
|
||
设备 ID、名称与服务 URL 存在用户数据目录的 `device-sharing-settings.json`,版本为 1;
|
||
复用 `writeJsonFileAtomically`,加载 Promise 去重、保存串行。损坏设置报错而不重置 ID。
|
||
此文件不保存目录副本或凭据。导航沿用已有存储归一化,把 `device-sharing` 追加到旧顺序,
|
||
保留既有应用相对顺序与设置;新入口可排序,不增加常驻字段。应用设置中的
|
||
`deviceSharingEnabled` 使用共享 schema 的 `false` 默认值补齐缺失字段,显式 `true` 保留;
|
||
沿用现有版本与原子持久化、变更事件。更新 schema 将该字段设为可选,避免无关更新重置选择。
|
||
菜单复用启用过滤,App 收到关闭设置时返回对话并卸载页面。共享服务和目录数据不受此开关修改。
|
||
|
||
服务测试使用真实 loopback HTTP,验证契约、独立权限、识别范围、持久化、错误和超时;
|
||
Electron 测试通过环境变量选择相邻真实 ShareServer 源码,使用临时服务数据与桌面设置。
|
||
具体命令和实测边界见[实施进度](./progress.md)。
|
||
|
||
## 1. 进程边界
|
||
|
||
```text
|
||
Renderer
|
||
-> 窄 Preload API
|
||
Main
|
||
-> DeviceSharingService
|
||
-> LanDiscoveryService
|
||
-> PeerPairingService
|
||
-> GatewayConnectionService
|
||
-> CapabilityCatalogService
|
||
-> CapabilityPublicationService
|
||
-> RemoteTaskService
|
||
-> PackageAcquisitionService
|
||
-> SharingAuditStore
|
||
-> 既有 Capability / Runtime / Skill / MCP 服务
|
||
```
|
||
|
||
Renderer 只取得脱敏快照和调用显式动作。设备私钥、Peer 证书、网关凭据、网络 socket、原始
|
||
任务令牌、任意文件读取和包缓存路径只存在于 Main。
|
||
|
||
## 2. 模块职责
|
||
|
||
| 模块 | 职责 |
|
||
| --- | --- |
|
||
| `LanDiscoveryService` | mDNS browse/advertise、手动地址握手、接口变化 |
|
||
| `PeerPairingService` | 临时握手、验证码、双端确认、配对凭据与撤销 |
|
||
| `GatewayConnectionService` | 浏览器登录、设备注册、WebSocket、重连和策略同步 |
|
||
| `CapabilityCatalogService` | 合并本机、Peer 和网关目录,保留来源身份 |
|
||
| `CapabilityPublicationService` | 校验本机可共享能力并向目标范围发布 |
|
||
| `RemoteTaskService` | 授权、确认、provider 冻结、传输、取消和终态 |
|
||
| `PackageAcquisitionService` | 固定来源下载、校验、安装协调和权限差异 |
|
||
| `SharingAuditStore` | 本机有界审计查询、筛选和清理 |
|
||
|
||
目录合并只生成 UI 视图,不能把不同 provider 的授权、版本和执行位置压成一个身份。
|
||
|
||
## 3. 共享契约
|
||
|
||
实现前建立独立 `packages/share-protocol`。Desktop 只从该包导入 Zod schema 和 TypeScript
|
||
类型。协议输入在网络边界和 IPC 边界分别验证,不能因已经过网络校验而跳过 Renderer 输入
|
||
校验。
|
||
|
||
建议导出:
|
||
|
||
```text
|
||
DeviceSummary
|
||
PairingChallenge
|
||
GatewayDescriptor
|
||
CapabilityManifest
|
||
Publication
|
||
GrantConstraint
|
||
AuthorizationDecision
|
||
RemoteTaskSnapshot
|
||
RemoteTaskEvent
|
||
PackageDescriptor
|
||
AuditEventSummary
|
||
ProtocolError
|
||
```
|
||
|
||
错误只向 Renderer 返回稳定 code、可本地化参数和可恢复动作,不转发远端堆栈、响应正文或
|
||
内部地址。
|
||
|
||
## 4. 持久化
|
||
|
||
建议在现有 Desktop SQLite 中增加:
|
||
|
||
```text
|
||
device_identity_metadata
|
||
peer_pairings
|
||
gateway_connections
|
||
capability_publications
|
||
local_grants
|
||
remote_tasks
|
||
sharing_audit_events
|
||
installed_shared_packages
|
||
```
|
||
|
||
- 私钥和 refresh credential 存系统安全存储;SQLite 只存引用和脱敏元数据。
|
||
- 附近广播、在线状态、配对验证码、短期任务令牌和目录候选只存在于内存。
|
||
- 网关能力目录可有界缓存,用于离线说明,不作为离线调用授权。
|
||
- 审计正文不落库,字段和保留上限在实现前固定。
|
||
- 数据库迁移只覆盖已发布 schema;未发布设计不预建兼容读取器。
|
||
|
||
## 5. 局域网网络实现
|
||
|
||
- 使用系统支持的 mDNS/DNS-SD 库 browse `_goodbuddy-share._tcp.local`。
|
||
- 广播绑定用户允许的网络接口;接口切换时重建监听,不保留旧地址为身份。
|
||
- 服务端口由 Main 持有,使用设备证书建立 TLS。
|
||
- 未配对握手仅暴露配对所需字段,并限制并发、频率、消息大小和有效时间。
|
||
- 手动地址只替代发现,不替代证书指纹与双端确认。
|
||
- 停止广播时关闭未完成配对监听;已配对活动连接按任务生命周期处理。
|
||
|
||
## 6. 网关连接
|
||
|
||
1. Main 校验 HTTPS URL,并取得服务公开描述。
|
||
2. 生成带 PKCE、随机 state 和设备挑战的系统浏览器登录请求。
|
||
3. 本机 loopback 或已注册应用链接只接收一次性回调 code。
|
||
4. Main 交换设备凭据,展示组织身份,用户确认后持久化。
|
||
5. Main 建立带设备身份的 WebSocket 出站连接。
|
||
6. 心跳更新在线状态;断线按有界指数退避重连。
|
||
|
||
服务器返回的跳转 URL 必须符合已取得服务描述和 HTTPS 限制。Renderer 不能提交认证头、设备
|
||
证书、WebSocket URL 或任意回调地址。
|
||
|
||
## 7. 本机能力适配
|
||
|
||
不是所有现有 Capability 都自动可共享。每种可共享类型需要显式 adapter:
|
||
|
||
```ts
|
||
interface ShareableCapabilityAdapter {
|
||
describe(): CapabilityManifest
|
||
validatePublication(input: PublicationDraft): ValidationResult
|
||
invoke(input: unknown, context: ProviderTaskContext): AsyncIterable<TaskEvent>
|
||
cancel(taskId: string): Promise<CancelResult>
|
||
}
|
||
```
|
||
|
||
adapter 负责把共享输入映射到既有生产服务,并保留原权限边界。首批类型在实现前根据真实
|
||
产品需求确定;未有 adapter 的 Skill、MCP、知识库和 Runtime 不出现在共享列表。
|
||
|
||
## 8. 远端调用生命周期
|
||
|
||
```text
|
||
Renderer 选择能力和输入
|
||
-> Main 解析候选并取得当前授权
|
||
-> 必要时返回确认描述
|
||
-> 用户批准
|
||
-> Main 冻结 task/provider/route/policy
|
||
-> 建立数据通道并流式发送
|
||
-> Provider 再校验并接受
|
||
-> Main 转发有界进度和结果
|
||
-> 终态与审计事务保存
|
||
```
|
||
|
||
每次任务使用稳定 `taskId` 和 attempt ID。Provider 未接受前的连接失败可以明确失败;已接受
|
||
后的断线先查询同一任务,不新建 attempt。无法恢复权威终态时保存 `outcome_unknown`。
|
||
|
||
附件从 Main 已授权的用户选择结果读取,按声明大小流式发送,不把任意本机路径交给远端。
|
||
接收成果先写 GoodBuddy-owned 暂存并校验,再通过现有 Artifact 边界进入产品。
|
||
|
||
## 9. IPC
|
||
|
||
Preload 只暴露按产品动作命名的方法,例如:
|
||
|
||
```text
|
||
sharing.getSnapshot
|
||
sharing.updateLanSettings
|
||
sharing.startPairing / confirmPairing / cancelPairing
|
||
sharing.revokePeer
|
||
sharing.addGateway / completeGatewayLogin / removeGateway
|
||
sharing.listCapabilities
|
||
sharing.savePublication / pausePublication / deletePublication
|
||
sharing.prepareInvocation / confirmInvocation / cancelTask
|
||
sharing.listAuditEvents / getAuditEvent
|
||
```
|
||
|
||
所有变更方法校验 trusted sender、严格 schema、对象当前 revision 和归属关系。不得暴露通用
|
||
HTTP、WebSocket、mDNS、证书、文件传输或数据库查询 IPC。
|
||
|
||
## 10. 生命周期与资源边界
|
||
|
||
- 应用启动按保存设置启动主动发现;不因保存了网关而阻塞窗口创建。
|
||
- 广播只有在用户开启后启动。
|
||
- 网关连接在后台恢复,认证过期只影响对应连接。
|
||
- 应用退出停止接收新任务,取消未接受任务,并有界通知活动 Provider 后关闭连接。
|
||
- 单个 Peer/网关/任务设置连接数、帧、字节、并发和重连上限。
|
||
- 大文件全程流式处理,不进入单个 Main `Buffer` 或 Renderer state。
|
||
|
||
## 11. 与现有模块集成
|
||
|
||
- `capability-service` 提供本机能力事实;共享 adapter 明确选择可公开子集。
|
||
- 已安装共享 Skill/MCP 继续使用现有配置、启停和 Runtime 分配,不建立第二套执行器。
|
||
- SSH Host 保持独立;除非未来提供专门 adapter 和用户发布,否则不进入目录。
|
||
- 远端 Task 的公开活动复用现有工具/任务展示语义,但保留 provider 和 route 字段。
|
||
- 应用更新源、模型下载源、Agent 下载源和工具下载源不因添加 ShareServer 自动改变。
|
||
|
||
## 12. 验证策略
|
||
|
||
### 12.1 自动化
|
||
|
||
- schema 边界、未知枚举和协议版本协商。
|
||
- 三个独立开关及默认值。
|
||
- mDNS 去重、接口变化、广播停止和手动地址。
|
||
- 配对双方确认、拒绝、超时、身份变化和撤销。
|
||
- 多网关隔离、认证过期、重连和移除。
|
||
- provider 排序、冻结、授权变化和失败不切换。
|
||
- 调用确认最小输入、附件上限、取消和结果未知。
|
||
- Publication 校验、暂停、撤销和 adapter 消失。
|
||
- 包签名、权限差异、来源冻结和失败保留旧安装。
|
||
- IPC sender、revision、归属和无通用网络/文件接口。
|
||
|
||
### 12.2 真实环境
|
||
|
||
- Windows、macOS、Linux 至少两种组合完成同网段发现和配对。
|
||
- mDNS 可用、被防火墙阻断、网络接口切换和设备离线场景。
|
||
- 真实 ShareServer 登录、设备出站连接、目录、调用和认证过期。
|
||
- 小型 JSON、文件流、取消、Provider 进程失败和网络中断。
|
||
- 确认日志、Renderer、诊断和审计中没有凭据或文件正文。
|
||
|
||
设计文档完成不代表功能实施或上述验证通过。
|