| name | create-bk-cli-system |
| description | 当需要为 bk-cli 扩展 system 能力时使用:无论是新增顶层 system、给已有 system 增加 action、还是为大 system 增加一层 subsystem,都应先使用此技能。技能会先检查目标 system/subsystem 是否已存在,遇到多模块 API 列表时先让用户在扁平 action 与一层 subsystem 两种命令形态中选择,再复用共享的 YAML/Go 选型、注册、测试和文档约定。 |
扩展 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>.go
cmd/system/<system>/spec.go
cmd/system/<system>/actions.yaml
cmd/system/<system>/<subsystem>/spec.go
cmd/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 示例放在 action examples 中,避免和示例重复维护。
- 如果 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 用它描述单次请求。常用字段:
GatewayName
Method
Path
ParamsJSON
BodyJSON
Headers
Stage
Timeout
AuthConfig
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 flags
header 仅用于帮助文本,不生成独立 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
- 自己声明本地
--body flag
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.md
README.md
README_EN.md
tests/integration/AGENTS.md
skills/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 解析优先复用
systemcmd helper
- 测试优先复用
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 使用了同一个命令名