十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Paseo 协议兼容性工程实践:App 与 Daemon 跨版本共存的契约设计

Paseo 协议兼容性工程实践:App 与 Daemon 跨版本共存的契约设计 Paseo 协议兼容性工程实践App 与 Daemon 跨版本共存的契约设计【免费下载链接】paseoOrchestrate multiple coding agents from desktop and mobile项目地址: https://gitcode.com/gh_mirrors/pa/paseo导读Paseo 的桌面端/移动端 App 与后台 Daemon 是分开发布的两个产品用户通过应用商店或桌面自动更新升级 App而 Daemon 则按自己的节奏升级因此在真实环境中新 App 配旧 Daemon旧 App 配新 Daemon甚至两端相差数月的组合都会出现。本文基于仓库中的协议兼容性规范文档系统讲解 Paseo 如何通过协议契约永远可解析与特性契约按特性一次性门控双轨机制保证任意版本组合下既有功能不回归、新功能优雅降级并给出COMPAT兼容垫片标记、客户端能力宣告、自有订阅owned subscriptions协商等可落地的工程细则与仓库源码佐证。读完本文你将掌握在多端异步发布架构下维护长生命周期协议的正确姿势。一、问题背景App 与 Daemon 永远存在版本组合Paseo 中App 与 Daemon 是两个独立产品App通过应用商店或桌面自动更新机制升级Daemon由用户按需自行升级。这导致开发环境与生产环境的一个关键差异在开发过程中两端永远是同版本这正是贡献者最容易忽略约束的地方——开发时同版本发布后不同版本才是常态。任何一端发布新功能另一端可能落后数月。因此协议层必须同时满足两个方向上的兼容性。从这一前提推导出两条必须遵守的契约协议契约schema 变更不得破坏任何方向的解析特性契约新特性按特性单独门控旧 Daemon 不支持就明确告知用户升级而不是降级模拟。二、协议契约schema 永远双向可解析2.1 核心规则一条 schema 变更必须保证旧 App 仍能解析新 Daemon 发来的消息新 Daemon 仍能解析旧 App 发来的消息。具体约束如下新字段必须声明为.optional()并带有合理默认值禁止将 optional 翻转为 required、删除字段或收窄类型——string收窄为enum、nullable 收窄为 non-null 都属于收窄某个字段你停止发送后接收端仍要继续接受它——停止写入不等于停止读取wire schema 必须是纯结构化声明WebSocket 消息 schema 上不允许.transform()、.catch()或.preprocess()归一化逻辑必须在校验之后的显式 pass 中完成。原因见 协议校验文档入站校验器是生成的而生成器只编译纯 schema当所有分支共享字面量 tag 时禁止使用普通z.union()必须使用z.discriminatedUnion().default()只能放在原始类型叶子字段上绝不能放在大数组内的条目 schema 或大型入站容器上。2.2 提交 schema 变更前回答两个问题在提交任何 schema 变更前必须能对以下两个问题同时回答是一个六个月前的旧 App 还能解析这条消息吗一个六个月前的旧 Daemon 发来的内容这个 App 还能接受吗两个都答是变更才算完成。2.3 Schema 与 RPC 命名规范所有协议 schema 集中在 packages/protocol/src/messages.ts。以server_info为例其features对象中每一个布尔标志都带COMPAT注释说明引入版本与移除日期例如// COMPAT(sessionPermissions): optional while clients support older daemons. permissions: z.array(DaemonPermissionSchema).optional(), // COMPAT(providersSnapshot): added in v0.1.48, remove gating when all clients use snapshot providersSnapshot: z.boolean().optional(),新增 RPC 的命名必须遵循 RPC 命名空间规范使用点号不是斜杠分层命名方向作为最后一段例如checkout.forge.set_auto_merge.request/checkout.forge.set_auto_merge.response普通请求必须有同前缀的成对响应requestId同时保留在请求与响应中作为关联键不要新增扁平命名如旧的checkout_pr_merge_request旧名称在兼容窗口内保持接受。2.4 测试佐证wire 兼容回归仓库用 packages/protocol/src/messages.wire-compat.test.ts 固化这一契约例如hello消息在有无 project update 能力时都能解析server_info能剥离未知的遗留 features同时接受旧的 turn identity旧的sub_agenttool-call payload 仍能按 v0.1.65-beta.3 的 schema 解析旧客户端解析带 rewind 能力的 agent snapshot、新客户端解析不带 rewind 能力的 snapshot 均成功。这些用例正是六个月前旧 App 仍可解析这一要求在测试层的落地。三、特性契约按特性门控一次绝不降级协议契约保证的是既有功能跨版本不回归而新特性通常需要新的 Daemon 能力旧 Daemon 并不具备。因此特性的策略是不建降级路径不要为旧 Daemon 构建劣化版特性不要通过打散到遗留 RPC 来模拟不存在的能力。用户要么升级主机要么没有这个特性不把防御分支散布在特性代码里能力检测只发生在一处下游所有代码读取的都是一个干净的形状能力标志集中在server_info消息的features字段中定义见 packages/protocol/src/messages.ts 的ServerInfoStatusPayloadSchema。features中每个布尔标志都代表一项可由 App 检测的 Daemon 能力例如directorySync、workspaceLabels、plugins、checkoutForgeSetAutoMerge、forgeSearch等每个都带COMPAT注释标注引入版本与移除门槛。App 在连接时读取这些标志决定运行新特性还是提示用户更新主机。值得强调的是特性门控永远不能替代协议契约。既有功能继续工作靠的是协议契约新特性靠门控两者分工明确。四、客户端能力归属能力默认值全量宣告4.1 能力清单与默认值客户端包负责宣告自己实现的协议行为。每个新能力都要加入其穷尽式默认值并在该处实现对应的订阅或解码行为App、CLI 和插件继承这些默认值只补充主机资源如浏览器自动化或显式覆盖。能力枚举定义在 packages/protocol/src/client-capabilities.tsexport const CLIENT_CAPS { ownedSubscriptions: owned_subscriptions, explicitEventSubscriptions: explicit_event_subscriptions, allProviders: all_providers, selectiveAgentTimeline: selective_agent_timeline, reasoningMergeEnum: reasoning_merge_enum, customModeIcons: custom_mode_icons, terminalReflowableSnapshot: terminal_reflowable_snapshot, providerSubagents: provider_subagents, projectUpdates: project_updates, compactProviderSnapshots: compact_provider_snapshots, timelineReplacementInvalidation: timeline_replacement_invalidation, timelineNotifications: timeline_notifications, pluginTimelineItems: plugin_timeline_items, workspaceSetupBlocked: workspace_setup_blocked, browserHost: browser_host, } as const;每个能力都带有精确的引入版本与淘汰日期注释例如customModeIcons是因为旧客户端把AgentModeIcon钉死在封闭枚举上、遇到未知值会崩溃所以 Daemon 在该能力缺失时把图标降级为ShieldCheck。4.2 为什么 schema 接受不等于支持一个关键原则schema 能接受某条消息并不代表客户端支持其投递语义。因此客户端必须显式宣告能力。默认宣告位于 packages/client/src/connection/index.ts// Protocol support belongs to the installed client. Only browser hosting needs // a resource supplied by the caller. Keep this exhaustive as the protocol evolves. export const DEFAULT_CLIENT_CAPABILITIES { [CLIENT_CAPS.ownedSubscriptions]: true, [CLIENT_CAPS.allProviders]: true, [CLIENT_CAPS.selectiveAgentTimeline]: true, [CLIENT_CAPS.reasoningMergeEnum]: true, [CLIENT_CAPS.customModeIcons]: true, [CLIENT_CAPS.terminalReflowableSnapshot]: true, [CLIENT_CAPS.providerSubagents]: true, [CLIENT_CAPS.projectedSubagentTimeline]: true, [CLIENT_CAPS.projectUpdates]: true, [CLIENT_CAPS.compactProviderSnapshots]: true, [CLIENT_CAPS.providerSnapshotReferences]: true, [CLIENT_CAPS.timelineReplacementInvalidation]: true, [CLIENT_CAPS.timelineNotifications]: true, [CLIENT_CAPS.pluginTimelineItems]: true, [CLIENT_CAPS.workspaceSetupBlocked]: true, [CLIENT_CAPS.explicitEventSubscriptions]: true, } satisfies RecordExcludeClientCapability, typeof CLIENT_CAPS.browserHost, true;注意browserHost被排除在默认值之外——它需要调用方提供真实的主机资源属于主机资源而非协议行为。连接测试 packages/client/src/connection.test.ts 中有专门用例断言普通客户端宣告全部协议能力且不宣告 browser host且注释明确每个新能力都需要一个有意的默认值或主机资源例外。4.3 订阅成员关系归属在有能力capable的 Daemon 上连接本身不产生任何 timeline 或 event 需求客户端订阅拥有自己的网络成员关系取消订阅时释放重连后恢复原始消息观察者raw message observers只检视流量不请求流应用缓存、可见的 agents 集合等由调用方持有不归连接层管理。五、自有订阅Owned Observations能力协商与遗留路径5.1 协商机制owned_subscriptions与server_info.features.ownedSubscriptions共同协商源自有契约底层 WebSocket 协议描述见 架构文档。客户端在连接边界处一次性选定投递行为App 工作流在两种模式下使用同一个观察接口。协商逻辑在 packages/client/src/connection/index.ts 中一目了然const owned info.features?.ownedSubscriptions true clientCapabilities[CLIENT_CAPS.ownedSubscriptions] true;即只有当 Daemon 宣告能力且客户端宣告能力时才启用自有订阅否则回退到遗留订阅LegacySubscriptions。5.2 与旧 Daemon 交互的遗留行为面对旧 Daemon 时客户端使用现有连接与遗留 RPC并保持以下语义目录订阅保持共享、last-query-wins最后一次查询生效本地 handle ID 仅标识监听者不承诺独立的服务器过滤器timeline 与 event 成员关系保持既有共享行为释放 handle 即分离其监听器并在存在旧 unsubscribe 操作时使用它仅广播broadcast的主机继续广播此时就绪只代表本地监听已挂载而非 Daemon 确认不引入额外 socket 或复用模拟multiplexing emulation。connection/index.ts中observeRequest的实现正是如此在遗留模式下广播时代的主机没有确认机制就绪状态直接本地接受快照请求失败时通过legacy.release发送旧式释放消息。相关能力在 packages/client/src/connection/legacy.ts 中以// COMPAT(ownedSubscriptions): added in v0.8.0; remove after 2027-03-11 once daemon floor v0.8.0.标记。5.3 边界与职责划分保持既有工作流独立过滤器independent filters与安静连接quiet connections需要有能力 Daemon而打开 App、读取历史、使用终端则不需要预注册pre-registry的工作区分组与遗留事件归一化属于客户端边界内部职责旧客户端在 Daemon 源边界保留其既有 wire 形状与槽位slot行为适配器按物理 socket给遗留槽位定键因此即使两个连接使用相同逻辑 client ID旧连接也不能顶替现代同级的观察可选的 wire ID 为解析兼容继续被接受但现代请求不能选择自己的订阅 ID。六、每个兼容垫片都要打标签并注明日期兼容垫片shim如果是为了支持旧 App 或旧 Daemon 而存在必须携带注释注明名称、引入版本与可移除时间// COMPAT(workspaceFileEditing): added in v0.2.0, remove after 2027-01-18 once daemon floor v0.2.0.rg COMPAT\(就是完整的清理积压清单backlog因此要求每个垫片一个标签放在必须被删除的代码位置标签包含名称、版本、移除条件或日期——通常以六个月为默认窗口绝不允许把兼容逻辑埋在未打标签的??兜底或可选链隧道里——未打标签的兼容代码永远不会被删除因为没人能找到它。当标签条件满足时在同一处变更中同时删除垫片与标签。仓库中COMPAT(遍布 App 与协议层例如 packages/app/src/components/add-project-flow.tsx 中的// COMPAT(stableProjectIdentity): added in v0.1.109, remove gate after 2027-01-15.以及desktopManaged字段的// COMPAT(desktopManaged): added in v0.1.X, remove optional parsing after 2027-01-16.都是这一规范的实例。七、QA 要求测试永远无法完全覆盖兼容性测试无法穷尽所有版本组合因此兼容性最终靠人工论证。规范要求只要改动涉及 packages/protocol 包就必须在 Pull Request 中说明为什么旧 App 仍能解析你新增/修改的消息为什么旧 Daemon 仍能满足你的 App。详细 QA 流程见 qa.md。配合 协议校验文档 中入站校验器由 zod-aot 在构建期生成、schema 必须保持纯净的约束这一论证通常可以落实到具体字段的 optional/默认值设置与生成代码回归测试上。八、实践清单综合全文为 Paseo 贡献协议相关代码时的自检清单schema 变更新字段 optional 默认值不删字段、不收窄类型不在 wire schema 上用 transform/catch/preprocess共享 tag 的 union 用z.discriminatedUnion()default 只放叶子。双问题自检六个月前的 App 还能解析吗六个月前的 Daemon 还能被接受吗新特性在server_info.features加布尔标志客户端检测一次不建降级路径不散布防御分支。新能力加入 client-capabilities.ts 的CLIENT_CAPS与 connection/index.ts 的DEFAULT_CLIENT_CAPABILITIES并实现对应订阅/解码行为。兼容垫片打COMPAT(name)标签注明版本与移除日期默认六个月条件满足时连同标签一起删除。命名新 RPC 按 rpc-namespacing.md 的点号分层命名不新增扁平名称。QA改动packages/protocol时在 PR 中书面论证双向兼容。这套协议契约保底、特性契约门控、能力显式宣告、垫片限期清理的组合拳就是 Paseo 能在 App 与 Daemon 各自异步发布的现实约束下长期保持任意版本组合可用性的核心工程方法论。【免费下载链接】paseoOrchestrate multiple coding agents from desktop and mobile项目地址: https://gitcode.com/gh_mirrors/pa/paseo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表