@kanyun-ai-infra/agenthub 0.4.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -6
- package/dist/index.cjs +3855 -1788
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6999 -315
- package/dist/index.d.ts +6999 -315
- package/dist/index.js +4065 -2021
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,9 +24,9 @@ pnpm add @kanyun-ai-infra/agenthub
|
|
|
24
24
|
|
|
25
25
|
需要 Node 18+,因为 SDK 使用全局 `fetch`。
|
|
26
26
|
|
|
27
|
-
如果调用方是 Java、Python、Go 或 Shell,请优先看
|
|
28
|
-
[`docs/user-guide/agenthub-
|
|
29
|
-
|
|
27
|
+
如果调用方是 Java、Python、Go 或 Shell,请优先看 REST 参考及其多语言快速开始:
|
|
28
|
+
[`docs/user-guide/agenthub-rest-api.md`](../../docs/user-guide/agenthub-rest-api.md)
|
|
29
|
+
(curl / Python / Java 示例见 [§9 多语言快速开始](../../docs/user-guide/agenthub-rest-api.md#9-多语言快速开始))。
|
|
30
30
|
|
|
31
31
|
## 快速开始
|
|
32
32
|
|
|
@@ -80,15 +80,22 @@ const version = await agenthub.agents.publishVersion({
|
|
|
80
80
|
},
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
// 4. Session
|
|
83
|
+
// 4. Session:通过 stage 槽位分配一次运行时。setDefault:true 首次发布已自动
|
|
84
|
+
// 创建 test 槽位,直接用即可(stage 与 versionId 互斥,见下方 API 参考)。
|
|
84
85
|
const session = await agenthub.sessions.create({
|
|
85
86
|
projectId: project.id,
|
|
86
|
-
agentId: version.agentId,
|
|
87
|
+
agentId: version.agentId,
|
|
88
|
+
stage: "test",
|
|
87
89
|
});
|
|
88
90
|
|
|
89
91
|
const ready = await agenthub.sessions.wait(session.sessionId); // status === "ready" 时返回
|
|
90
92
|
```
|
|
91
93
|
|
|
94
|
+
> Environment(k8s 配置底座:资源规格 / envVars / TTL)上面还没有登场——test 槽位
|
|
95
|
+
> 目前落在裸 revision 快照上,本地联调、smoke test 够用。真实上线到 production,
|
|
96
|
+
> 要额外给槽位绑一个 Environment,才有资源规格定制、canary 灰度、drift 检测、
|
|
97
|
+
> 一键回滚这些能力:完整走一遍见下方 [发布闭环](#发布闭环)。
|
|
98
|
+
|
|
92
99
|
## 核心模型
|
|
93
100
|
|
|
94
101
|
```text
|
|
@@ -102,6 +109,12 @@ Project → Agent → Version → Session
|
|
|
102
109
|
标记 project/agent 默认版本。
|
|
103
110
|
- **Session**:基于某个 Version 发起的一次 startup/runtime allocation。
|
|
104
111
|
|
|
112
|
+
上面这条链路是"定义"层面的(发布了什么);Session 已经通过 **Stage** 槽位
|
|
113
|
+
(`test`/`production`,指向当前生效的 Version,可选绑定 **Environment**)
|
|
114
|
+
跑起来了——但这个槽位目前没绑 Environment(k8s 配置底座:资源规格 / envVars /
|
|
115
|
+
TTL),落的是裸 revision 快照。给槽位绑定 Environment、晋级到 production、
|
|
116
|
+
canary 灰度、drift 检测、一键回滚——完整闭环见 [发布闭环](#发布闭环)。
|
|
117
|
+
|
|
105
118
|
Agents 和 runtime templates 都是 **project-scoped**。Version 必须在同一个 Project
|
|
106
119
|
内解析到 Agent 和 runtime source;唯一例外是下文的 platform-shared templates。
|
|
107
120
|
|
|
@@ -137,12 +150,16 @@ Agents 和 runtime templates 都是 **project-scoped**。Version 必须在同一
|
|
|
137
150
|
| `list()` | 列出 token 有效 scope 内的 Project。 |
|
|
138
151
|
| `get(projectId)` | 查询单个 Project。 |
|
|
139
152
|
| `delete(projectId)` | 软删(停用)Project:清空 owner / members、关闭集成。**owner-only**(需 `project:settings`)。系统项目不可删。返回 `{ action: "deactivated", projectId }`。 |
|
|
153
|
+
| `getWorkbench(projectId)` | 工作台聚合(AgentHub lifecycle console 同款读模型):`workloadCount` / `sessionStats` / `stageDrift` / `recentChanges`。 |
|
|
154
|
+
| `getReleaseBoard(projectId)` | 发布板聚合:每个 Agent 一行,含 test/production 槽位摘要与 `drift`。 |
|
|
140
155
|
|
|
141
156
|
### `agenthub.agents`(别名 `agenthub.workloads`)
|
|
142
157
|
|
|
143
158
|
| 方法 | 说明 |
|
|
144
159
|
| --- | --- |
|
|
145
160
|
| `create(req, { dryRun? })` | 创建 Agent。返回 `{ mode, status, agentId?/workloadId?, workload }`。 |
|
|
161
|
+
| `get({ projectId, agentSlug \| workloadSlug })` | 读回单个 Agent(含 `defaultRevisionId` 与内嵌 `defaultRevision`;发布过 `setDefault` Version 前为 `null`)。不存在返回 `404 WORKLOAD_NOT_FOUND`;`projectId` 超出 token scope 返回 `403 PROJECT_NOT_IN_TOKEN_SCOPE`。 |
|
|
162
|
+
| `getVersion({ projectId, agentSlug \| workloadSlug, version })` | 读回单个 Version(别名 `getRevision`)。返回 `{ id, projectId, workloadId, workloadSlug, version, runtimeSnapshotHash, createdAt }`(SDK 额外附 `agentId` 镜像)。不存在返回 `404 WORKLOAD_NOT_FOUND / REVISION_NOT_FOUND`。 |
|
|
146
163
|
| `publishVersion(req, { dryRun? })` | 发布 Version(别名 `publishRevision`,用 `workloadSlug`)。返回 `{ mode, status, agentId/workloadId, versionId?/revisionId?, runtimeTemplateId, resolved }`。 |
|
|
147
164
|
| `applyManifest({ projectId, manifest, dryRun? })` | 高级 / GitOps:apply raw agent manifest。 |
|
|
148
165
|
| `applyRevisionManifest({ projectId, agentSlug \| workloadSlug, manifest, dryRun? })` | 高级 / GitOps:apply raw version manifest。两个 slug 拼写都接受(互斥)。 |
|
|
@@ -161,14 +178,64 @@ Agents 和 runtime templates 都是 **project-scoped**。Version 必须在同一
|
|
|
161
178
|
|
|
162
179
|
`apply*` 方法里的 `manifest` 可以是 manifest 对象、`{ yaml }` 或 `{ json }`。
|
|
163
180
|
|
|
181
|
+
#### `agenthub.agents.stages` + `previewStage`
|
|
182
|
+
|
|
183
|
+
Stage 槽位(`test` / `production`)——把一个 Version 部署到某个槽位,槽位把
|
|
184
|
+
「version 指针」和「environment 绑定」当成两个独立维度组合在一起。概念全貌与
|
|
185
|
+
完整走一遍的例子见 [发布闭环](#发布闭环)。
|
|
186
|
+
|
|
187
|
+
| 方法 | 说明 |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `stages.list(workloadId)` | 列出该 Agent 已部署的槽位;从未部署的 stage 直接不出现(无隐式创建)。 |
|
|
190
|
+
| `stages.set(workloadId, stage, { revisionId, environmentId?, runtimeOverrides?, configValues?, expectedUpdatedAt? })` | 创建/更新槽位(upsert,部署动作)。 |
|
|
191
|
+
| `stages.promote(workloadId, { environmentId?, expectedUpdatedAt?, sourceRevisionId?, expectedTestUpdatedAt? }?)` | 把 production 指针挪到 test 槽位当前指向的 revision(原子操作;只挪指针,production 自己的 overrides/config 不变)。 |
|
|
192
|
+
| `previewStage({ agentId \| workloadId, versionId \| revisionId, runtimeOverrides?, configValues?, environmentId? })` | UI-5 解析预览:只读计算部署后会解析成什么,不写任何行。晋级 / 部署前的确认步骤。 |
|
|
193
|
+
|
|
194
|
+
`set` / `promote` 的 `environmentId` 都是**三态**语义:
|
|
195
|
+
|
|
196
|
+
- **省略**(`undefined`)—— 保留槽位当前绑定;槽位第一次创建时没有"现值"可保留,
|
|
197
|
+
于是落成 `environmentId: null`(未绑定)。
|
|
198
|
+
- **`null`** —— 清除绑定(槽位回退到 revision 快照,不叠加任何 environment 字段组)。
|
|
199
|
+
- **UUID** —— 绑定该 Environment(服务端校验:存在 / 同项目或 platform-shared /
|
|
200
|
+
未 archived),并钉住它当前的 tip 版本。
|
|
201
|
+
|
|
202
|
+
⚠️ **`environmentId` "必须显式传,没有继承 revision 环境这回事"这条规则,只适用于
|
|
203
|
+
`stages.set` / `stages.promote` 这两个显式调用——`publishVersion` / `publishRevision`
|
|
204
|
+
本身没有 `environmentId` 这个入参,传不了。** `publishVersion({ setDefault: true })`
|
|
205
|
+
首次发布时自动创建的那条 **test** 槽位(2026-07-13 起的新行为,`production` 不会
|
|
206
|
+
被自动创建)同样落地成未绑定状态,但那是 upsert 时"省略"分支的结果,不是你对
|
|
207
|
+
publishVersion 传了什么参数。给这条自动创建的 test 槽位补环境绑定,要另外显式调用
|
|
208
|
+
`agenthub.agents.stages.set(workloadId, "test", { revisionId, environmentId })`。
|
|
209
|
+
见下方 [发布闭环](#发布闭环) 的 ⚠️ 说明。
|
|
210
|
+
|
|
211
|
+
### `agenthub.environments`
|
|
212
|
+
|
|
213
|
+
Environment = agent 运行的 **k8s 配置底座**(provider / 资源规格 / envVars /
|
|
214
|
+
TTL)——它本身不是"部署目标";`test`/`production` 才是部署目标(stage 槽位,
|
|
215
|
+
`agents.stages`),槽位钉住一对 `(environmentId, environmentVersion)`。
|
|
216
|
+
|
|
217
|
+
| 方法 | 说明 |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `list(projectId, { includeArchived? })` | 列出项目可见的 Environment(含 platform-shared)。 |
|
|
220
|
+
| `get(id, projectId)` | 查询单个 Environment(含 config 全貌 + `referencedBy`——当前所有引用它的槽位)。 |
|
|
221
|
+
| `create(projectId, { name, base, description?, config? })` | 创建(从 platform-shared 底座拷贝 provider 字段)。定义域操作,日常建议在管理门户完成;这里是给 CI/IaC 场景开的口子。 |
|
|
222
|
+
| `update(id, projectId, { name?, description?, config?, repointReferences? })` | 编辑。`config` 面变更(`resources`/`envVars`/`defaultTtlSeconds`)冻结一个新版本,默认(`repointReferences` 省略或 `true`)批量把所有引用槽位拨到新版本;传 `false` 只建版本、槽位不动(配合 `repoint` 手动灰度)。返回 `{ environment, createdVersion, repointedSlotCount }`;改名/描述这类 metadata-only 写不产生新版本(`createdVersion: null`)。 |
|
|
223
|
+
| `archive(id, projectId)` | 归档(终态,无法撤销)。存量引用继续解析;任何新引用一律拒绝。 |
|
|
224
|
+
| `versions(id, projectId)` | 版本历史(每条自带完整 config 快照,不需要单独的详情调用)+ repoint 审计账(哪个槽位、何时、被谁从哪个版本拨到哪个版本)。 |
|
|
225
|
+
| `repoint(id, projectId, { toVersion, slots?, expectedSlots? })` | 拨动槽位的版本指针——回滚=拨回旧版本,灰度=只拨 `slots` 里点的槽位。省略 `slots` = 项目内所有引用该 Environment 的槽位。槽位选择器接受 `agentId`(同 `workloadId`)。 |
|
|
226
|
+
|
|
227
|
+
`repoint` 同步执行——单事务内的有界原子操作(行锁 + CAS + 批量 UPDATE),不是
|
|
228
|
+
长任务,调用直接拿到结果,不需要轮询。
|
|
229
|
+
|
|
164
230
|
### `agenthub.sessions`
|
|
165
231
|
|
|
166
232
|
| 方法 | 说明 |
|
|
167
233
|
| --- | --- |
|
|
168
|
-
| `create({ projectId, agentId, versionId?, configValues?, metadata?, idempotencyKey?, resources?, ... })` | 启动 Session
|
|
234
|
+
| `create({ projectId, agentId, stage?, versionId?, configValues?, metadata?, idempotencyKey?, resources?, ... })` | 启动 Session。**推荐传 `stage`**(`"test"` \| `"production"`)——透过槽位解析出当前部署的 revision + environment;槽位不存在时抛 `STAGE_NOT_DEPLOYED`。`stage` 与 `versionId`/`revisionId` 互斥(同传抛 `STAGE_REVISION_MUTEX`)——后者是钉死某个 revision、绕开槽位解析的特殊路径。都省略时退化为 Agent 的 `defaultRevisionId`。`agentId`/`versionId` 也可写成 `workloadId`/`revisionId`(互斥)。`resources` 注入 Model A 只读输入文件(见下)。返回 `{ sessionId, status }`。 |
|
|
169
235
|
| `get(sessionId)` | 读取 Session 快照(`{ session: { id, status, ... } }`,字段为服务端原样返回)。 |
|
|
170
236
|
| `wait(sessionId, { intervalMs?, timeoutMs?, signal? })` | 轮询到 `status === "ready"`;遇到 `failed` 或超时会抛错。 |
|
|
171
237
|
| `releaseSandbox(sessionId)` | 释放 Session 当前 sandbox(保留 session,下一轮 turn 自动 revive 到新 sandbox)。需 `runs:create`。返回 `{ released, sandboxId, sandboxDestroyed, destroyError? }`;`sandboxDestroyed:false` 不代表失败(watcher 会兜底销毁)。 |
|
|
238
|
+
| `getDiagnosis(sessionId)` | 会话诊断聚合(AgentHub lifecycle console 同款读模型):`session` / `failure` / `events` / `sandbox`,任一区块读取失败时该块为 `null` 且 `blockErrors` 带错误说明,不影响其它区块。 |
|
|
172
239
|
|
|
173
240
|
Session `status` 取值为 `created`、`provisioning`、`starting`、`ready`、`failed`。
|
|
174
241
|
|
|
@@ -212,6 +279,183 @@ const { sessionId } = await agenthub.sessions.create({
|
|
|
212
279
|
约束:每 Session ≤ 20 个文件;`mountPath` 必须是 `.session/inputs/` 下的相对子路径
|
|
213
280
|
(拒绝绝对路径与 `..`);只读。
|
|
214
281
|
|
|
282
|
+
## 发布闭环
|
|
283
|
+
|
|
284
|
+
**正名**:Environment 对应 k8s 的「配置」——agent 跑在什么样的容器里(provider /
|
|
285
|
+
资源规格 / envVars / TTL),是**底座**,不是部署目标本身。`test` 和
|
|
286
|
+
`production` 才是部署目标,叫 **stage**(部署槽位)。槽位
|
|
287
|
+
= `{ version 指针, environment 绑定 }` 两个互相独立的维度:一个槽位可以只钉了
|
|
288
|
+
version、没绑 environment(回退到 revision 快照),也可以两者都钉。**"分别创建
|
|
289
|
+
测试环境和线上环境"**这个诉求,落到 API 上不是造两个 Environment,而是给
|
|
290
|
+
`test` 和 `production` 两个槽位各自 `stages.set` 一次——两个槽位可以指向
|
|
291
|
+
**同一个** Environment(同配置跑两份),也可以指向**不同**的 Environment
|
|
292
|
+
(比如 production 用更大的资源规格)。
|
|
293
|
+
|
|
294
|
+
下面完整走一遍"发布 → 部署 → 预览 → 晋级 → canary 灰度出真实 drift → repoint
|
|
295
|
+
回滚",全程用 agent 词汇;每段代码都是照实测过的 j8 SDK 旅程
|
|
296
|
+
(`tests/e2e/j8-agenthub-sdk-journey.spec.ts`)抄的真实调用序列,不是省略号带过
|
|
297
|
+
的伪代码——复制到 strict TS 项目里应该能直接编译。
|
|
298
|
+
|
|
299
|
+
### 第一幕:创建 Agent,发布 v1(setDefault:true 自动落地 test)
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const project = await agenthub.projects.create({ slug: "acme", displayName: "Acme" });
|
|
303
|
+
await agenthub.agents.create({
|
|
304
|
+
projectId: project.id,
|
|
305
|
+
slug: "support-bot",
|
|
306
|
+
displayName: "Support Bot",
|
|
307
|
+
kind: "managed_agent",
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// setDefault:true —— 首发用它是安全的:2026-07-13 起的新行为只会自动创建
|
|
311
|
+
// **test** 槽位(environmentId 仍是 null,未绑定)+ 翻转 defaultRevisionId,
|
|
312
|
+
// 不会误建一个"看起来已经上线其实没配置"的 production(见文末 ⚠️)。存量
|
|
313
|
+
// 在此之前已经发布过、已经有 production 槽位的老 workload 走的是另一条
|
|
314
|
+
// 分支,不受这次改动影响(同样见文末 ⚠️ 的存量说明)。
|
|
315
|
+
const v1 = await agenthub.agents.publishVersion({
|
|
316
|
+
projectId: project.id,
|
|
317
|
+
agentSlug: "support-bot",
|
|
318
|
+
version: "v1",
|
|
319
|
+
workerContract: { kind: "managed_agent" },
|
|
320
|
+
setDefault: true,
|
|
321
|
+
});
|
|
322
|
+
// `revisionId` 在 RevisionApplyResult 里是 optional(dry-run / manifest 面共用
|
|
323
|
+
// 同一个类型);status:"created" 时必然有值,但 strict TS 下仍需要先断言/守卫
|
|
324
|
+
// 才能当必填参数传下去——不要在类型上偷懒直接 `v1.revisionId!` 到处传。
|
|
325
|
+
if (!v1.revisionId) throw new Error("publishVersion did not return a revisionId");
|
|
326
|
+
const revisionId = v1.revisionId;
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 第二幕:给已存在的 test 槽位补上环境绑定
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
const env = await agenthub.environments.create(project.id, {
|
|
333
|
+
name: "acme-standard",
|
|
334
|
+
base: { platformTemplateName: "pilot-v2-pod-agent" },
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// test 槽位在第一幕已经由 setDefault:true 自动创建(environmentId 是 null,
|
|
338
|
+
// 未绑定)——这里不是"首次部署",是确认/补上环境绑定。set 是 upsert,同一个
|
|
339
|
+
// revisionId 再传一次不会产生新状态,只补 environmentId 这一个字段。
|
|
340
|
+
// production 槽位此刻还不存在,留到第四幕用 promote 显式创建它。
|
|
341
|
+
await agenthub.agents.stages.set(v1.workloadId, "test", {
|
|
342
|
+
revisionId,
|
|
343
|
+
environmentId: env.id,
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### 第三幕:晋级前先 preview
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
const preview = await agenthub.agents.previewStage({
|
|
351
|
+
agentId: v1.workloadId,
|
|
352
|
+
versionId: revisionId,
|
|
353
|
+
environmentId: env.id,
|
|
354
|
+
});
|
|
355
|
+
// 把 preview.runtimeSnapshot / preview.environment 展示给操作者确认,
|
|
356
|
+
// 无误后再提交第四幕的晋级请求。
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 第四幕:晋级 production(第一次创建它)
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
await agenthub.agents.stages.promote(v1.workloadId, {
|
|
363
|
+
// TOCTOU 防护:确保确认弹窗打开期间 test 槽位没有被别人改过。
|
|
364
|
+
sourceRevisionId: revisionId,
|
|
365
|
+
// production 此刻并不存在(新行为下首发只写 test)——promote 把它的指针
|
|
366
|
+
// 挪到 test 当前指向的 revision,但 environmentId 是独立三态、不会从
|
|
367
|
+
// test 继承绑定:首次创建 production 必须显式传 environmentId,否则会
|
|
368
|
+
// 带着 environmentId:null 落地(同一个坑,见文末 ⚠️)。
|
|
369
|
+
environmentId: env.id,
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
此刻 production 才第一次出现,且与 test 完全同步(同 revision、同
|
|
374
|
+
environment、同 environment 版本)——`getReleaseBoard` 这一行的 `drift` 是
|
|
375
|
+
`false`,这是正常稳态,不是 bug。
|
|
376
|
+
|
|
377
|
+
### 第五幕:环境升级到 v2,canary 灰度到 test(这才是真实 drift 的由来)
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
// defaultTtlSeconds 是"永不物化"字段(不需要 Kanyun template gateway)——
|
|
381
|
+
// config 面变更冻结出 v2;repointReferences:false 让两个槽位暂时都留在 v1,
|
|
382
|
+
// 不要一步就把所有槽位都拨过去。
|
|
383
|
+
const updated = await agenthub.environments.update(env.id, project.id, {
|
|
384
|
+
config: { defaultTtlSeconds: 7200 },
|
|
385
|
+
repointReferences: false,
|
|
386
|
+
});
|
|
387
|
+
// `createdVersion` is `number | null` — metadata-only writes create no
|
|
388
|
+
// version. A config-face write always does, but the type doesn't know that;
|
|
389
|
+
// guard before treating it as a required `toVersion`.
|
|
390
|
+
if (updated.createdVersion === null) throw new Error("update() did not create a new version");
|
|
391
|
+
const canaryVersion = updated.createdVersion;
|
|
392
|
+
|
|
393
|
+
// canary:只把 test 槽位拨到 v2,production 留在 v1。
|
|
394
|
+
const canary = await agenthub.environments.repoint(env.id, project.id, {
|
|
395
|
+
toVersion: canaryVersion,
|
|
396
|
+
slots: [{ agentId: v1.workloadId, stage: "test" }],
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// 这才是 drift 真正出现的时刻——不是假设,是两次 get 读回来的真实状态。
|
|
400
|
+
const afterCanary = await agenthub.environments.get(env.id, project.id);
|
|
401
|
+
const testRef = afterCanary.referencedBy.find((ref) => ref.stage === "test");
|
|
402
|
+
const productionRef = afterCanary.referencedBy.find((ref) => ref.stage === "production");
|
|
403
|
+
// testRef.environmentVersion === 2, productionRef.environmentVersion === 1
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### 第六幕:canary 效果不理想,回滚(真实 409 重试,不是省略号带过)
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
import { PilotPlatformApiError } from "@kanyun-ai-infra/agenthub";
|
|
410
|
+
|
|
411
|
+
async function rollbackTestSlot(): Promise<void> {
|
|
412
|
+
// 重新拉一次,把 test 槽位的当前状态当成"我现在看到的影响面"传给
|
|
413
|
+
// expectedSlots——不要复用第五幕观察时的旧快照。
|
|
414
|
+
const environment = await agenthub.environments.get(env.id, project.id);
|
|
415
|
+
const testRef = environment.referencedBy.find((ref) => ref.stage === "test");
|
|
416
|
+
if (!testRef) return; // 槽位已经不再引用这个 environment,没什么可回滚的。
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
await agenthub.environments.repoint(env.id, project.id, {
|
|
420
|
+
toVersion: 1, // 回到 canary 之前的版本
|
|
421
|
+
slots: [{ workloadId: testRef.workloadId, stage: "test" }],
|
|
422
|
+
expectedSlots: [{ workloadId: testRef.workloadId, stage: "test" }],
|
|
423
|
+
});
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (err instanceof PilotPlatformApiError && err.code === "ENVIRONMENT_REPOINT_IMPACT_SET_CHANGED") {
|
|
426
|
+
// 有人在你上面这次 get 之后又动过这个槽位——重新拉最新状态再试,
|
|
427
|
+
// 不能对着同一份 expectedSlots 硬重试。
|
|
428
|
+
return rollbackTestSlot();
|
|
429
|
+
}
|
|
430
|
+
throw err;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
await rollbackTestSlot();
|
|
435
|
+
// production 全程没有被 canary 影响过——回滚只是撤销 test 侧的灰度尝试。
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
⚠️ **`publishVersion({ setDefault: true })` 首次发布只会自动写 `test` 槽位
|
|
441
|
+
(`environmentId` 仍是 `null`,未绑定)+ 翻转 `defaultRevisionId`;`production`
|
|
442
|
+
永远不会被自动创建,必须显式 `stages.set` / `stages.promote`。** 这是
|
|
443
|
+
2026-07-13 拍板落地的行为(spec `pilot-v2-agent-stage-environment.md` §5.1
|
|
444
|
+
STG-M5~M8;读代码 + 实测确认,不是猜的):`applySetDefault`
|
|
445
|
+
(`packages/control-plane/src/workload/workload-manifest-apply.ts`)判定
|
|
446
|
+
"该 workload 此刻一个 stage 槽位都没有"(首发)时,只 upsert `test` 槽位
|
|
447
|
+
(`environmentId: undefined` 落在还不存在的槽位上,等价于插入 `null`——没有
|
|
448
|
+
"现值"可继承)+ 调用新写端点 `setWorkloadDefaultRevisionPointer` 挪
|
|
449
|
+
`default_revision_id`;`production` 完全不碰。**非首发**(该 workload 已经
|
|
450
|
+
有 ≥1 个槽位,不管是 `test` 还是 `production`)的 `setDefault` 写只挪
|
|
451
|
+
`default_revision_id` 指针,同样不碰任何槽位——上面六幕如果给第二次发布也传
|
|
452
|
+
`setDefault: true`,也只是指针翻转,不会自动同步任何槽位。
|
|
453
|
+
|
|
454
|
+
> **存量兼容**:2026-07-13 之前已经首发过、因而已经有 `production` 槽位的老
|
|
455
|
+
> workload 判定为"非首发",行为不受这次改动影响、不做迁移——`production`
|
|
456
|
+
> 槽位继续存在,只是后续 `setDefault` 写不会再挪动它的 revision 指针(这本身
|
|
457
|
+
> 是一处行为变化:改动前"非首发"的 `setDefault` 也会挪 `production` 指针)。
|
|
458
|
+
|
|
215
459
|
## Client 配置
|
|
216
460
|
|
|
217
461
|
`new PilotPlatformClient(config)`
|