扩展 bk-cli System
使用目标
这个技能统一覆盖两类工作:
- 新增一个顶层 system 命令
- 给已有 system 新增一个或多个 action
- 给已有 system 新增一层 subsystem,并在 subsystem 下新增 YAML 或 Go actions
本技能同时提供创建/扩展 system 时必查的共享契约摘要;详细规则以 docs/design.md 为准。先判断 system 是否已存在,再决定走哪条分支;不要先写代码再回头补注册或补结构。
推荐读取顺序
- 先读
AGENTS.md和docs/design.md,确认仓库边界与设计基线。 - 再读本技能中的“扩展 system 必查摘要”,确认扩展 system 时最容易踩坑的共享规则;详细契约一律回到
docs/design.md。 - 最后按本技能的分支流程判断是“新建 system”还是“给已有 system 增加 action”。
第零步:先判断是否需要 subsystem
如果用户的需求、OpenAPI 列表、资源 tags 或业务描述天然分成多个模块,先暂停实现,给用户两个选项:
- 扁平 action:继续使用
bk-cli <system> <action>,通过 action 名区分模块,例如get_pipeline_build_list。 - 一层 subsystem:使用
bk-cli <system> <subsystem> <action>,例如bk-cli devops pipeline get_build_list、bk-cli devops codecc get_task_detail、bk-cli devops stream trigger。
当前只允许一层 subsystem,不允许 bk-cli <system> <subsystem> <sub_subsystem> <action>。如果用户需要更深层级,停止实现并给出公共契约变更 proposal 建议。
第一步:先判断 system 是否已存在
按下面顺序检查:
- 阅读
AGENTS.md、docs/design.md,并先通读本技能后面的“扩展 system 必查摘要” - 查看
cmd/system/register.go,确认systemCatalog()的注册方式 - 检查以下文件是否已经存在:
cmd/system/<system>.gocmd/system/<system>/spec.gocmd/system/<system>/actions.yamlcmd/system/<system>/<subsystem>/spec.gocmd/system/<system>/<subsystem>/actions.yaml
- 搜索
new<System>SystemSpec()或NewSystemSpec(),确认该 system 是否已接入 catalog
判断结果:
- system 不存在:先创建 system,再按需要补 YAML actions、Go-implemented actions、测试和文档
- system 已存在:只新增 action,按 action 粒度决定用 YAML 还是 Go,不要为了一个 Go action 迁移已有 YAML actions
- system 已存在但 subsystem 不存在:按“分支 C:新增 subsystem”创建 subsystem,再按 action 粒度决定 YAML 或 Go
扩展 system 必查摘要
文档分层以 AGENTS.md 为准;详细共享契约以 docs/design.md 为准。生成 system/action 时,至少先确认:
- context 表示独立 BlueKing 部署目标,不是 API Gateway stage;详细 context 规则以
docs/design.md为准。 --stage、timeout、tenant、--header的优先级不要自己重写;涉及这些行为时回查docs/design.md。- 即使用户用
--header覆盖X-Bkapi-Authorization,--dry-run/--verbose仍必须脱敏展示认证内容。 - YAML action 必须显式声明
authConfig;resourcePermissionRequired: true必须同时设置appVerifiedRequired: true。 - YAML
params只支持in: path、in: query和帮助用途的in: header;不要声明in: body。 - YAML action 统一获得共享输入:
--stage、--body '<json>'、重复的--header 'Key:Value';保留 flag 名冲突时应跳过 action 并给出 warning。 - 如果 OpenAPI request body 很复杂、需要 Agent 或调用方自行构造完整 JSON,不要强行把 body 字段拆成 command flags;继续使用共享
--body '<json>',请求体示例放进examples,并在 YAML action 中配置body_schema。如果 OpenAPI 标记 request body 为 required,同时配置body_required: true。默认 help 按Usage、Examples、schema 查看提示的顺序展示,完整 schema 通过bk-cli <system> [subsystem] <action> -h --body-schema查看。 - 路径占位符值会按单个 URL path segment 转义,不能依赖它注入
/或?。 - Go-implemented action 要通过
systemcmd.ResolveRuntime(deps)和systemcmd.ExecuteRequest(...)或syslib.ExecuteRequest(...)走共享执行路径,不要绕过 runtime / output / credential 逻辑。 - 当 Go-implemented action 同时支持命名 flags 和原始
--body时,把--body视为显式覆盖。
command group 形态
一个 command group 可以是以下形态之一:
- YAML-driven:所有 action 由
actions.yaml定义 - Go-implemented:所有 action 由 Go 代码实现
- mixed:同时保留 YAML actions 和 Go-implemented actions
- group-only:父 system 只作为分组,下面挂 subsystem
- parent actions + subsystems:父 system 有自己的 actions,同时下面挂 subsystem
一个 action 的实现方式只看它自己的复杂度,不看别的 action 已经用什么。
同一 parent 下的直接子命令名必须唯一。父 system 的 action 名不能和 subsystem 名冲突。
action 选型规则
优先选择满足需求的最简单实现。
优先用 YAML,当且仅当下面条件都满足:
- 本质上只是
cli args -> 一次 API 调用 - 不需要本地编排逻辑
- 不需要本地专属 flags, API 请求参数直接转换成 flags
- 不需要定制返回结构
- 即使有 request body,只要调用方可以通过
--body直接提供完整 JSON,且examples/body_schema足以指导 Agent 构造 body,也优先保持 YAML-driven
必须用 Go-implemented action,只要满足任一条件:
- 需要本地校验、分支、编排或分页聚合
- 需要零次、一次或多次 API 调用
- 需要本地专属 flags,例如
--bk_biz_id、--fields、--limit, 封装/处理/编排后再作为请求参数 - 需要请求前由 CLI 根据命名 flags 合成 body,或需要对 body 字段做本地专属校验/默认值处理
- 需要请求后用
mutate或手工 envelope 调整返回
YAML body schema / example 规则
body_schema 用于“body 很复杂,但 action 本身仍然只是一次 API 调用”的场景。典型例子是 OpenAPI 的 request body 有大量嵌套字段、数组或对象,Agent 需要根据 schema 自行构造完整 JSON。
规则:
- 不要为复杂 body 字段生成大量 command flags;否则 help 会膨胀,调用契约也容易和 OpenAPI 漂移。
- YAML action 只把
path/query参数转成 flags;body 继续通过共享--body '<json>'输入。 body_schema放精简后的 JSON schema 或字段结构说明;可直接作为--body起点的 JSON 示例放在 actionexamples中,避免和示例重复维护。- 如果 OpenAPI request body 是 required,配置
body_required: true;这样执行时缺少--body会在本地失败,不会把空 body 发送到上游。 - 默认
bk-cli <system> [subsystem] <action> --help必须先展示Usage和Examples,再展示 schema 查看提示;body_schema必须能通过bk-cli <system> [subsystem] <action> -h --body-schema看到。系统专属skills/*/SKILL.md只能放常用示例,不能作为唯一的 body 结构来源。 --body-schema是 help modifier,不是执行参数;不带-h单独使用时必须快速失败,不能进入认证或请求执行路径。- 只有当用户明确需要更友好的本地 flags、CLI 需要合成 body、或需要本地校验复杂字段时,才把该 action 单独做成 Go-implemented wrapper。
核心类型
systemcmd.SystemSpec
type SystemSpec struct {
Name string
Description string
YAMLFile string
RegisterGoActions RegisterGoActionsFunc
Subsystems []SystemSpec
}
systemcmd.BuildDeps
type BuildDeps struct {
GetContext func() string
IsDryRun func() bool
IsVerbose func() bool
WarnWriter io.Writer
}
syslib.RequestSpec
Go-implemented action 用它描述单次请求。常用字段:
GatewayNameMethodPathParamsJSONBodyJSONHeadersStageTimeoutAuthConfig
AuthConfig 必须显式设置,使用 &syslib.AuthConfig{...}。
共享 helper 地图
运行时与请求执行
| Helper | 用途 |
|---|---|
systemcmd.ResolveRuntime(deps) |
在 RunE 开头统一解析 context、dry-run、verbose、insecure |
systemcmd.ExecuteRequest(cmd, runtime, actionName, spec, mutate) |
单次请求 action 的标准执行路径 |
syslib.ExecuteRequest(runtime, spec) |
多次请求编排、分页聚合 |
systemcmd.EnsureEnvelope(actionName, env) |
防御空 envelope |
flags、校验与序列化
| Helper | 用途 |
|---|---|
systemcmd.AddCommonRequestFlags(cmd, &stage, &body, &headers) |
注册 --stage、--body、--header |
systemcmd.AddCommonRequestFlagsWithoutBody(cmd, &stage, &headers) |
注册 --stage、--header |
systemcmd.MarshalJSON(payload) |
统一序列化 body |
systemcmd.ValidatePositiveIntFlag(...) |
校验必填正整数 |
systemcmd.ValidatePositiveIntFlagIfChanged(...) |
校验可选正整数 |
systemcmd.ValidateNonNegativeIntFlag(...) |
校验非负整数 |
systemcmd.ValidateNonEmptyStringFlag(...) |
校验非空字符串 |
systemcmd.ParseJSONObjectFlag(flagName, raw) |
解析 JSON object 类型 flag |
测试 helper
| Helper | 用途 |
|---|---|
testutil.BuildDeps(dryRun bool) |
构造测试依赖 |
testutil.SetupTestContext(baseURL string) |
创建默认 context 与凭据 |
testutil.CaptureCommandStdout(fn) |
捕获 stdout |
testutil.BuildYAMLActionCmd(...) |
构造 YAML action 测试命令 |
不要自己重复实现这些共享能力。
分支 A:system 不存在,先创建 system
必须新增或更新的文件
始终需要:
| 文件 | 用途 |
|---|---|
cmd/system/<system>.go |
薄包装,调用 <system>.NewSystemSpec() |
cmd/system/<system>/spec.go |
NewSystemSpec() 实现 |
cmd/system/register.go |
在 systemCatalog() 中注册 |
按需新增:
| 文件 | 条件 |
|---|---|
cmd/system/<system>/actions.yaml |
该 system 需要 YAML actions |
cmd/system/<system>/<action>.go |
该 system 需要 Go-implemented actions |
cmd/system/<system>/common.go |
多个 Go actions 共享逻辑 |
cmd/system/<system>/<system>_suite_test.go |
该 system 有 Ginkgo 测试 |
skills/bk-cli-<system>/SKILL.md |
新增公开 system 时必须补齐,且内容用中文 |
薄包装模板
package system
import <system>system "github.com/TencentBlueKing/bk-cli/cmd/system/<system>"
func new<System>SystemSpec() SystemSpec {
return <system>system.NewSystemSpec()
}
spec.go 模板
YAML-driven
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<system>",
Description: "<system> system commands",
YAMLFile: "<system>/actions.yaml",
}
}
Go-implemented
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<system>",
Description: "<system> system commands",
RegisterGoActions: func(parent *cobra.Command, deps systemcmd.BuildDeps) error {
parent.AddCommand(newSomeActionCmd(deps))
return nil
},
}
}
mixed
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<system>",
Description: "<system> system commands",
YAMLFile: "<system>/actions.yaml",
RegisterGoActions: func(parent *cobra.Command, deps systemcmd.BuildDeps) error {
builders := []func(systemcmd.BuildDeps) *cobra.Command{
newActionOneCmd,
newActionTwoCmd,
}
for _, build := range builders {
parent.AddCommand(build(deps))
}
return nil
},
}
}
注册与 embed 约束
在 cmd/system/register.go 的 systemCatalog() 中加入新 system。
如果用了 YAML,文件必须放在 cmd/system/<system>/actions.yaml,因为仓库依赖 //go:embed */actions.yaml 自动嵌入。
common.go 建议
多个 Go-implemented actions 共享逻辑时,把下面内容放进 common.go:
- 网关名常量
- 共享类型
- body builder
- 共享校验函数
- factory function
Ginkgo suite
该 system 只要出现测试文件,就补 suite 文件:
package <system>_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSuite(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "<System> Suite")
}
分支 B:system 已存在,新增 action
YAML action 工作流
YAML 文件位置固定为:
cmd/system/<system>/actions.yaml
如果 SystemSpec.YAMLFile 已配置,只需要在 actions 列表中追加 action,不需要额外 Go wiring。
YAML 顶层结构
name: <system>
gateway_name: bk-<upstream>
description: "System description"
actions:
- ...
YAML action 字段
| 字段 | 必填 | 说明 |
|---|---|---|
name |
是 | action 命令名 |
description |
是 | Cobra Short |
method |
是 | HTTP 方法 |
path |
是 | API 路径,可带 {param} |
timeout |
否 | 覆盖 context timeout,例如 30s |
authConfig |
是 | 认证配置 |
params |
否 | 参数列表 |
examples |
否 | 命令示例 |
body_schema |
否 | 复杂 request body 的 schema 或字段结构说明;通过 -h --body-schema 帮助 Agent 构造 --body |
body_required |
否 | 执行时是否要求非空 --body;OpenAPI requestBody.required=true 时应设置为 true |
authConfig
| 字段 | 必填 | 说明 |
|---|---|---|
appVerifiedRequired |
是 | 是否需要应用认证 |
userVerifiedRequired |
是 | 是否需要用户认证 |
resourcePermissionRequired |
是 | 是否需要资源权限校验 |
约束:
resourcePermissionRequired: true时,appVerifiedRequired也必须为true- 如果 app/user 都不需要,CLI 不会生成
X-Bkapi-Authorization - 即使用户用
--header覆盖认证头,dry-run / verbose 也必须继续脱敏展示认证内容
params
| 字段 | 必填 | 说明 |
|---|---|---|
name |
是 | 参数名,也是 flag 名 |
in |
是 | path、query 或 header |
type |
是 | string、bool、int |
description |
否 | 帮助文本 |
required |
否 | 是否必填 |
default |
否 | 默认值 |
规则:
path与query会生成 CLI flagsheader仅用于帮助文本,不生成独立 flag- 不要写
in: body - action 额外共享
--stage、--body '<json>'和重复的--header 'Key:Value';有body_schema时额外支持 help modifier--body-schema;有body_required: true时--body是执行必填项 - param 名不能与保留 flag 冲突:
body、body-schema、header、stage、help、context、dry-run、format、verbose、insecure - 同一 action 内 path 和 query param 名不能重复
Go-implemented action 工作流
需要修改的文件
| 文件 | 用途 |
|---|---|
cmd/system/<system>/<action>.go |
action 构造函数 |
cmd/system/<system>/spec.go |
注册新 action |
cmd/system/<system>/<action>_test.go |
action 测试 |
cmd/system/<system>/common.go |
共享逻辑,可选 |
构造函数模板
func newSomeActionCmd(deps systemcmd.BuildDeps) *cobra.Command {
var (
bizID int
stage string
body string
headers []string
)
cmd := &cobra.Command{
Use: "some_action",
Short: "Short description",
RunE: func(cmd *cobra.Command, args []string) error {
runtime, err := systemcmd.ResolveRuntime(deps)
if err != nil {
return err
}
if err := systemcmd.ValidatePositiveIntFlag("bk_biz_id", bizID); err != nil {
return err
}
bodyJSON, err := buildSomeBody(body, bizID)
if err != nil {
return err
}
return systemcmd.ExecuteRequest(cmd, runtime, "some_action", syslib.RequestSpec{
GatewayName: gatewayName,
Method: "POST",
Path: "/api/v3/some/path/",
BodyJSON: bodyJSON,
Headers: headers,
Stage: stage,
AuthConfig: &syslib.AuthConfig{
AppVerifiedRequired: true,
UserVerifiedRequired: true,
ResourcePermissionRequired: false,
},
}, nil)
},
}
cmd.Flags().IntVar(&bizID, "bk_biz_id", 0, "Business ID")
systemcmd.AddCommonRequestFlags(cmd, &stage, &body, &headers)
return cmd
}
body 合成模式
优先使用 bodyOverride 守卫:
func buildSomeBody(bodyOverride string, bizID int) (string, error) {
if bodyOverride != "" {
return bodyOverride, nil
}
if err := systemcmd.ValidatePositiveIntFlag("bk_biz_id", bizID); err != nil {
return "", err
}
return systemcmd.MarshalJSON(map[string]any{
"bk_biz_id": bizID,
})
}
mutate 回调
需要在 stdout 前调整 envelope 时,传入 mutate:
return systemcmd.ExecuteRequest(cmd, runtime, "demo_action", spec,
func(env *output.Envelope) error {
if env.DryRun {
env.Data = map[string]any{"received": localData}
return nil
}
env.Data = map[string]any{
"received": localData,
"upstream": env.Data,
}
return nil
})
无共享 --body 的 action
如果 action 自己管理 body 语义:
- 使用
systemcmd.AddCommonRequestFlagsWithoutBody - 自己声明本地
--bodyflag RequestSpec.BodyJSON由本地逻辑决定
factory 模式
多个 action 共享同一套 flag/request 结构时,把公共部分收进 common.go,用 spec struct + factory function 生成命令,避免复制粘贴。
多次请求编排
需要分页聚合或多次调用时:
- 用
syslib.ExecuteRequest(runtime, spec)发每次请求 - 用
systemcmd.EnsureEnvelope(actionName, result.Envelope)校验结果 - 手工聚合数据
- 手工构造最终 envelope
在 spec.go 中挂载
builders := []func(systemcmd.BuildDeps) *cobra.Command{
newExistingActionCmd,
newSomeActionCmd,
}
for _, build := range builders {
parent.AddCommand(build(deps))
}
分支 C:system 已存在,新增一层 subsystem
必须新增或更新的文件
| 文件 | 用途 |
|---|---|
cmd/system/<system>/spec.go |
在 SystemSpec.Subsystems 中注册 subsystem |
cmd/system/<system>/<subsystem>/spec.go |
NewSystemSpec() 实现,描述 subsystem 自己的 YAML/Go actions |
cmd/system/<system>/<subsystem>/actions.yaml |
该 subsystem 需要 YAML actions 时使用 |
cmd/system/<system>/<subsystem>/<action>.go |
该 subsystem 需要 Go-implemented actions 时使用 |
cmd/system/<system>/<subsystem>/common.go |
多个 Go actions 共享逻辑,可选 |
cmd/system/<system>/<subsystem>/<subsystem>_suite_test.go |
该 subsystem 有测试时必须存在 |
subsystem spec.go 模板
package <subsystem>
import (
"github.com/spf13/cobra"
systemcmd "github.com/TencentBlueKing/bk-cli/internal/systemcmd"
)
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<subsystem>",
Description: "<subsystem> commands",
YAMLFile: "<system>/<subsystem>/actions.yaml",
RegisterGoActions: func(parent *cobra.Command, deps systemcmd.BuildDeps) error {
parent.AddCommand(newSomeActionCmd(deps))
return nil
},
}
}
parent system 注册模板
func NewSystemSpec() systemcmd.SystemSpec {
return systemcmd.SystemSpec{
Name: "<system>",
Description: "<system> commands",
YAMLFile: "<system>/actions.yaml",
RegisterGoActions: func(parent *cobra.Command, deps systemcmd.BuildDeps) error {
parent.AddCommand(newParentActionCmd(deps))
return nil
},
Subsystems: []systemcmd.SystemSpec{
<subsystem>.NewSystemSpec(),
},
}
}
父 system 的 YAMLFile 和 RegisterGoActions 都是可选的。不要为了挂 subsystem 创建无意义的父 action。
subsystem YAML 顶层结构
name: <subsystem>
gateway_name: <subsystem-gateway-name>
description: "<subsystem> commands"
actions:
- name: <action>
subsystem YAML 的 gateway_name 必须独立声明,不从父 system 继承。
测试与文档要求
至少补充或更新:
cmd/system/register_test.go:system 注册行为cmd/system/<system>/<action>_test.go:Go-implemented action 测试cmd/system/<system>/<subsystem>/<action>_test.go:subsystem Go-implemented action 测试cmd/system/<system>/<system>_suite_test.go:该 system 有测试时必须存在cmd/system/<system>/<subsystem>/<subsystem>_suite_test.go:该 subsystem 有测试时必须存在
如果新增公开 system,同步更新:
AGENTS.mdREADME.mdREADME_EN.mdtests/integration/AGENTS.mdskills/bk-cli-<system>/SKILL.md
如果只是改动已有命令或参数,也要同步更新 AGENTS.md、README.md、README_EN.md、tests/integration/AGENTS.md 和相关 skill。
如果变更影响公开 system 的可见行为,补充或更新:
tests/integration/cases/system/<system>/下的 YAML 集成用例tests/integration/mock_api/app.py(仅当httpbin不够表达该行为时)
最终检查清单
- 先确认目标 system 是否已存在,再选分支
cmd/system/<system>.go与cmd/system/<system>/spec.go都存在SystemSpec.Name、YAML 顶层name、注册项三者一致cmd/system/register.go的systemCatalog()已包含目标 system- YAML 文件路径是
cmd/system/<system>/actions.yaml - YAML action 都有完整
authConfig - Go-implemented action 使用
systemcmd.ResolveRuntime - Go-implemented action 使用
systemcmd.ExecuteRequest或syslib.ExecuteRequest - 共享 flags、校验、JSON 解析优先复用
systemcmdhelper - 测试优先复用
cmd/system/testutil - 公开 system 已补
skills/bk-cli-<system>/SKILL.md - 相关 system 的集成用例已在
tests/integration/cases/system/<system>/补齐或确认无需变更 - 如集成用例作者约定有变化,已更新
tests/integration/AGENTS.md - 如果新增 subsystem,确认只使用一层 subsystem
- 如果新增 subsystem,确认 subsystem action 路径是
cmd/system/<system>/<subsystem>/... - 如果新增 subsystem,确认 subsystem YAML 顶层
name是<subsystem>,并且独立声明gateway_name - 确认父 system 的 action 名没有和 subsystem 名冲突
- 完成后执行:
make fmt
make lint
make test
make build
make test-integration SCENARIO=<SCENARIO_ID>
排障顺序
- 先确认 context 与凭据是否正确。
- 再确认 stage、tenant、timeout、header、body 是否符合前面的必查摘要;细节以
docs/design.md为准。 - 使用
--dry-run查看最终请求构造。 - 仍有问题时,再回到具体 system 技能或
docs/design.md深挖。
常见错误
- 没先判断 system 是否已存在,结果重复创建结构
- 只写
actions.yaml,却没有补cmd/system/<system>.go与spec.go - 忘了把 system 加到
systemCatalog() - YAML 文件不在
cmd/system/<system>/actions.yaml - YAML action 缺少
authConfig - 在 YAML
params中写in: body - Go-implemented action 绕过
ResolveRuntime或ExecuteRequest - 在 system 目录里重复定义测试 helper
- 新增公开 system 却没补
skills/bk-cli-<system>/SKILL.md - 导入别名没用
syslib/systemcmd - 用户给出的 API 列表已经按 pipeline/codecc/stream 这类模块划分,agent 没先让用户选择扁平 action 还是 subsystem
- subsystem YAML 写成了父 system 的
name - subsystem YAML 试图省略
gateway_name并继承父 system - 创建了多层 subsystem
- 父 system action 和 subsystem 使用了同一个命令名