Repository navigation
[P3][Proposal] 控制面统一签名 JSON 传输,移除 typed SDK 路线选择 #150
Description
问题
控制面 controlplane.SDK.Call 对同一个 Action 有两条传输路线:typed SDK(调用 *ags.Client 方法)和契约校验后的签名 JSON(dynamic)。选哪条路线由两份手写清单(internal/controlplane/compatibility.go 的 sdkShapes、internal/controlplane/sdk.go 的分发 switch)加运行时契约兼容检查共同决定。此外,internal/cli/cloud_calls.go 绕过 controlplane,直接调用 typed SDK,构成第三条路线。
问题出在“选路线”这件事本身:
- 路线会随 SDK 版本、契约变化和 stable/preview channel 静默切换,而两条路线的可观察输出并不相同。
- 两份清单加第三条路线需要同步维护,容易漂移。
已确认的现状
基线为当前 main(5bd4a49,AGS/common SDK v1.3.188)。以下结论来自运行时 catalog 与 *ags.Client 方法的反射核对:
| Channel | 契约 Action | 实际走 typed | 已登记但被判为 dynamic |
|---|---|---|---|
| stable | 54 | 19 | DescribeSandboxInstanceList、DescribeSandboxToolList、StartSandboxInstance |
| preview | 62 | 18 | 上述 3 个 + CreateSandboxTool |
- 被判为 dynamic 的原因:契约中
TimeoutSeconds/DefaultTimeoutSeconds为 int,SDK 为*uint64;preview 的CreateSandboxTool.StorageMounts[].Volume在 SDK 中缺失。 - 因此
sdk.go中StartSandboxInstance的 typed 分支及 typed token 缓存,在两个 channel 的生产路径上都不可达。创建实例后的 token 缓存实际只由 dynamic 路径的cacheResourceToken执行。 - 同一个 Action
CreateSandboxTool,stable 走 typed,preview 走 dynamic。 - SDK 中另有 31 个与当前契约兼容的 Action 未登记,一直走 dynamic(Registry、Session、Event 等);feat(instance): add sandbox snapshots and source cloning #148 引入的 3 个 Snapshot Action 同理。目前没有观察到这些 Action 因此出错。
两条路线差异造成的可观察影响(代码证据):
- 返回类型不同:typed 返回
*ags.XxxResponseParams,dynamic 返回map[string]any。instance pause的文本输出(pauseResultText)只识别 typed 类型;一旦PauseSandboxInstance切到 dynamic,即使服务端返回InstanceStatus=PAUSED,文本也始终是Pause requested: <id>。当前两个 channel 都走 typed,该问题处于潜伏状态。tool fork的createdToolID已经为两种类型各写了一个分支。 - JSON 形状不同:typed 结果经 SDK struct 重新序列化(
omitnil,omitempty),契约外的扩展字段会丢失,null字段会被省略;dynamic 保留服务端原始字段、null和整数精度。省略null这一条来自源码推断,尚未用真实响应对比。
两条路线的传输能力等价:都基于 SDK common 包签名,网络与限流重试都是 0,错误都经 ClassifyCloudError 分类;请求在 Call 入口统一执行契约 ValidateRequest。typed 路线不提供额外的校验或传输能力。
方案
所有控制面 Action 统一走“契约校验 + 签名 JSON”传输(即现有 dynamic 路线),取消路线选择:
SDK.Call固定返回apivalue.Object。- 删除
sdkShapes、needsDynamic及兼容判定、typed 分发switch、call*/fillRequest、按 Action 注入的函数字段,以及Client/NewClient/cloudClient/cacheInstanceToken。测试统一通过已有的RawSender注入响应。 StartSandboxInstance成功后的 token 缓存只保留一份实现(cacheResourceToken)。workflow Action 移除RequestId、错误分类和诊断上下文的行为保持不变。- 读取 typed 返回值的使用方改为读 Object,例如
instance pause的文本输出、tool fork的 ToolId 提取。 internal/cli中直接调用 typed SDK 的路径改为经controlplane.SDK.Call:overlay 临时实例的创建和删除、doctor连通性探测、实例 token 获取。删除cloud_calls.go及对应的Cloud*/SetCloud*ForTest测试 seam。
依赖边界:
- 保留
tencentcloud/common:签名、临时凭据和错误类型由它提供,不自行实现。 tencentcloud/ags不再用于传输,但仍用于展示投影(通过apivalue.Project投影到ags.*struct,例如 instance、deployment、apikey 视图)。是否移除该依赖见“后续”,不在本 Issue 范围。
建议拆成两个 PR:① controlplane 收敛加使用方修正;② internal/cli 的 typed 路线切换。
兼容性
原来走 typed 的 Action(stable 19 个、preview 18 个)的 -o json 输出,可能从“字段缺失”变为保留服务端原样的 null 和扩展字段(推断,未比对真实响应)。依赖字段是否存在的 --jq 脚本可能受影响。实施时需要:
- 用包含
null、空数组和契约外字段的固定 mock 响应,对上述 Action 生成新旧输出对比,在 PR 中列出差异; Release impact选择 Stable and preview,并在 Changelog 中说明。
验收标准
- 生产代码不再构造
ags.Client,也不再调用其方法;internal/controlplane不再 importtencentcloud/ags,并由 lint 规则(如 depguard)约束,不依赖手写清单。 - 新增契约驱动的全量路由测试:遍历 stable/preview 当前契约中的每个 Action,用最小合法请求调用
SDK.Call,断言恰好发送一次签名 JSON、Action 正确、返回apivalue.Object。新增契约 Action 时无需修改测试即被覆盖。 -
instance pause在响应InstanceStatus=PAUSED时,文本输出为Instance paused: <id>(回归测试)。 -
instance create的普通创建、--source-instance-id和--snapshot-id(feat(instance): add sandbox snapshots and source cloning #148 合入后)各自恰好缓存一次 token;AuthMode=NONE时不获取 token;token 获取失败只输出警告,不使命令失败。 - overlay 临时实例(
instance exec、instance code run)的创建与清理,以及doctor连通性检查,经统一路径执行且行为不变。 - 保留上下文取消的错误分类、云 API 错误码与 RequestId、超出 int64 范围的整数精度和临时凭据支持。
- 新旧 JSON 输出差异已列入 PR 并写入 Changelog。
- stable 和 preview 下
go test -race ./...、golangci-lint、go run ./cmd/internal/cobragen check、go run ./cmd/internal/apipatch check-all以及 mock HTTPS 集成测试全部通过。 - 对传输路线实际发生变化的资源族完成真实 E2E 和清理回查:tool create/update/delete、instance update/pause/resume/stop/token、apikey create/list/delete、deployment 生命周期(含 proxy token)、pre-cache image task create/get;第二个 PR 补 overlay 临时实例和
doctor。未覆盖项如实报告。
范围与关联
- 替代 [Proposal] 自动发现 Go SDK 能力并生成 SDK 优先调用路由 #149:“SDK 优先、不兼容时回退”的方向不再推进。
- 落地 [P3][Tracking] 命令契约单一来源与内部架构渐进收敛 #146 第 3 步中的传输统一部分;该步中“token 缓存移回 instance 工作流”不在本 Issue 范围。
- 与 feat(instance): add sandbox snapshots and source cloning #148 的交集:feat(instance): add sandbox snapshots and source cloning #148 修改了
internal/controlplane/sdk_tool_request_test.go,实施时以它合入后的main为基线。 - 不包含:移除
tencentcloud/common、自行实现签名、修改api.json、新增命令、自动重试、基于契约的响应校验。
后续(不在本 Issue)
展示投影仍依赖 tencentcloud/ags 的 struct:SDK 落后于契约时,新字段会被丢弃,或者投影失败、降级为原样打印。是否改为契约生成的类型或直接读取 Object,并据此移除 tencentcloud/ags 依赖,与 #146 第 4 步一起决策。
Activity