refactor(leaf): declare params on LeafSpec; keep Call as execution

Lift business flags/const params out of Call/PostMount, add typed
flag defaults and policy gates, and realign the RFC acceptance bar to
"no Execute/Call body exists only to assemble params".

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
玉澜
2026-07-30 20:32:08 +08:00
co-authored by Cursor
parent ceca98c573
commit 41fdf79aea
8 changed files with 1999 additions and 493 deletions
File diff suppressed because it is too large Load Diff
+31 -9
View File
@@ -76,7 +76,7 @@ type FlagSpec struct {
Name string // flag name (kebab-case)
Usage string // registration usage text
Kind FlagKind // value type, defaults to KindString
Default string // registration default (only KindString uses it at registration; as a fallback-chain tail it applies to all kinds without shadowing aliases/env)
Default string // registration default for every Kind; also the fallback-chain tail when aliases/env are empty
// Required, when true, validates a non-empty effective value in RunE. Plain
// Required flags aggregate into a cmdutil.ValidateRequiredFlags-compatible
@@ -91,8 +91,10 @@ type FlagSpec struct {
Aliases []string // hidden aliases, registered with the main flag's Kind; used in order when the main flag is not explicitly provided
EnvVar string // environment variable consulted when the effective value is empty (an integer flag's env value must be parseable)
// ArgDefault covers the case where the registration default is empty but
// toolArgs still needs a fallback; used as the arg value when the effective
// value is empty.
// toolArgs still needs a fallback. For KindString it is used when the
// effective value is empty. For KindInt it is also the floor: when the
// resolved integer is < 1, ArgDefault is emitted instead (cursor page-size
// semantics).
ArgDefault string
// Bind is the toolArgs key; empty uses Name.
Bind string
@@ -181,12 +183,16 @@ type CommandSpec struct {
Flags []FlagSpec
Constraints []Constraint
Risk Risk
// ConstParams are fixed toolArgs merged after flag assembly (e.g. precheckOnly).
// They are not flags and never satisfy Required.
ConstParams map[string]any
// Validate is the cross-flag validation hook, run after required/constraint
// checks and before args assembly; nil skips it.
Validate func(cmd *cobra.Command, args []string) error
// PostMount adjusts the built command after flag registration and before
// RunE is set (Args/DisableAutoGenTag/annotate/…); always runs.
// RunE is set (Args/DisableAutoGenTag/annotate/…); always runs. Business
// flags belong in Flags, not here.
PostMount func(cmd *cobra.Command)
// RunE fully replaces the generated body (escape hatch).
RunE func(cmd *cobra.Command, args []string) error
@@ -326,6 +332,9 @@ func NewCommand(spec CommandSpec) *cobra.Command {
if err != nil {
return err
}
for key, value := range spec.ConstParams {
toolArgs[key] = value
}
if !ConfirmRisk(cmd, spec.Risk) {
return apperrors.NewValidation("用户取消了操作")
}
@@ -373,15 +382,20 @@ func RegisterFlags(cmd *cobra.Command, flags []FlagSpec) {
}
}
// RegisterFlag registers one flag by Kind; the registration default is only used
// by KindString (other kinds treat Default purely as a fallback-chain tail,
// matching the existing KindInt behavior).
// RegisterFlag registers one flag by Kind. Default is applied at registration
// for every kind so --help DefValue matches the declared fallback.
func RegisterFlag(cmd *cobra.Command, kind FlagKind, name, def, usage string) {
switch kind {
case KindInt:
cmd.Flags().Int(name, 0, usage)
defInt := 0
if def != "" {
if v, err := strconv.Atoi(def); err == nil {
defInt = v
}
}
cmd.Flags().Int(name, defInt, usage)
case KindBool:
cmd.Flags().Bool(name, false, usage)
cmd.Flags().Bool(name, def == "true", usage)
case KindStringSlice:
cmd.Flags().StringSlice(name, nil, usage)
default:
@@ -478,6 +492,14 @@ func BuildArgs(cmd *cobra.Command, flags []FlagSpec) (map[string]any, error) {
if err != nil {
return nil, err
}
// ArgDefault floors values < 1 (cursor page-size: 0/-1 → default).
if v < 1 && flag.ArgDefault != "" {
parsed, err := strconv.ParseInt(flag.ArgDefault, 10, 64)
if err != nil {
return nil, fmt.Errorf("flag --%s: invalid ArgDefault %q", flag.Name, flag.ArgDefault)
}
v = parsed
}
// Keep "non-zero only" (putInt semantics).
if v != 0 {
toolArgs[bind] = int(v)
+64
View File
@@ -1001,3 +1001,67 @@ func TestCrossPlatformCoverageNewCommandOrchestrateHonorsConfirmation(t *testing
t.Fatal("confirmed orchestrate did not run")
}
}
func TestRegisterFlagTypedDefaults(t *testing.T) {
cmd := newTestCommand()
RegisterFlag(cmd, KindInt, "page-size", "20", "page")
RegisterFlag(cmd, KindBool, "flag", "true", "bool")
if def, _ := cmd.Flags().GetInt("page-size"); def != 20 {
t.Fatalf("int default = %d, want 20", def)
}
if def, _ := cmd.Flags().GetBool("flag"); !def {
t.Fatal("bool default = false, want true")
}
if got := cmd.Flags().Lookup("page-size").DefValue; got != "20" {
t.Fatalf("page-size DefValue = %q, want 20", got)
}
}
func TestBuildArgsIntArgDefaultFloor(t *testing.T) {
flags := []FlagSpec{{
Name: "page-size", Usage: "P", Kind: KindInt,
Default: "20", ArgDefault: "20", Bind: "pageSize",
}}
cmd := newTestCommand()
RegisterFlags(cmd, flags)
args, err := BuildArgs(cmd, flags)
if err != nil {
t.Fatal(err)
}
if args["pageSize"] != 20 {
t.Fatalf("unset pageSize = %#v, want 20", args["pageSize"])
}
_ = cmd.Flags().Set("page-size", "0")
args, err = BuildArgs(cmd, flags)
if err != nil {
t.Fatal(err)
}
if args["pageSize"] != 20 {
t.Fatalf("zero pageSize = %#v, want floor 20", args["pageSize"])
}
}
func TestNewCommandMergesConstParams(t *testing.T) {
var got map[string]any
cmd := NewCommand(CommandSpec{
Use: "pub",
Flags: []FlagSpec{{Name: "id", Usage: "ID", Bind: "versionId", Trim: true}},
ConstParams: map[string]any{"precheckOnly": false},
Invoke: func(_ *Ctx, toolArgs map[string]any) error {
got = toolArgs
return nil
},
})
_ = cmd.Flags().Set("id", "V1")
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatal(err)
}
if got["versionId"] != "V1" {
t.Fatalf("versionId = %#v", got["versionId"])
}
if got["precheckOnly"] != false {
t.Fatalf("precheckOnly = %#v, want false", got["precheckOnly"])
}
}
+69 -127
View File
@@ -249,7 +249,7 @@ func newDevAppEventListCommand(runner executor.Runner) *cobra.Command {
Use: "list",
Short: "查询应用已订阅的事件列表",
Example: " dws dev app event list --unified-app-id UNIFIED_APP_ID --page-size 20 --format json",
Tool: devAppEventListTool,
Tool: devAppEventListTool,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "keyword", Usage: "事件搜索关键词,支持按事件码或事件名称模糊匹配", Bind: "keyword", Trim: true, OmitEmpty: true},
@@ -258,12 +258,8 @@ func newDevAppEventListCommand(runner executor.Runner) *cobra.Command {
_, err := requiredDevAppUnifiedID(cmd)
return err
},
// cursor/pageSize 由 devAppApplyCursorParams 注入(page-size 默认 20、floor 20)。
Call: devAppCallCursor(runner),
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppEventListTool)
registerDevAppCursorFlags(cmd)
},
Call: devAppCallCursor(runner),
PostMount: devAppMetaCursor(devAppEventListTool),
})
}
@@ -275,6 +271,7 @@ func newDevAppEventSubscribeCommand(runner executor.Runner) *cobra.Command {
Tool: devAppEventSubscribeTool,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "event-codes", Usage: "事件码,多个用逗号或分号分隔", Bind: "eventCodes", Trim: true, Transform: transformDevAppListParam},
},
Validate: func(cmd *cobra.Command, args []string) error {
if err := devAppRequireWriteGuard(cmd, "event subscribe"); err != nil {
@@ -288,15 +285,8 @@ func newDevAppEventSubscribeCommand(runner executor.Runner) *cobra.Command {
}
return nil
},
// eventCodes 数组由 Call 解析注入(与手写 params["eventCodes"] 等价)。
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
params["eventCodes"] = parseDevAppListFlag(cmd, "event-codes")
return runDevAppTool(runner, cmd, tool, params)
},
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppEventSubscribeTool)
cmd.Flags().String("event-codes", "", "事件码,多个用逗号或分号分隔")
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppEventSubscribeTool),
})
}
@@ -308,6 +298,7 @@ func newDevAppEventUnsubscribeCommand(runner executor.Runner) *cobra.Command {
Tool: devAppEventUnsubscribeTool,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "event-codes", Usage: "事件码,多个用逗号或分号分隔", Bind: "eventCodes", Trim: true, Transform: transformDevAppListParam},
},
Validate: func(cmd *cobra.Command, args []string) error {
if err := devAppRequireWriteGuard(cmd, "event unsubscribe"); err != nil {
@@ -321,14 +312,8 @@ func newDevAppEventUnsubscribeCommand(runner executor.Runner) *cobra.Command {
}
return nil
},
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
params["eventCodes"] = parseDevAppListFlag(cmd, "event-codes")
return runDevAppTool(runner, cmd, tool, params)
},
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppEventUnsubscribeTool)
cmd.Flags().String("event-codes", "", "事件码,多个用逗号或分号分隔")
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppEventUnsubscribeTool),
})
}
@@ -337,7 +322,7 @@ func newDevAppListCommand(runner executor.Runner) *cobra.Command {
Use: "list",
Short: "查询开放平台企业内部应用列表",
Example: " dws dev app list --name DemoApp --page-size 20 --format json",
Tool: devAppListTool,
Tool: devAppListTool,
Flags: []LeafFlag{
{Name: "name", Usage: "应用名称关键词", Bind: "name", Trim: true, OmitEmpty: true, Aliases: []string{"keyword"}},
{Name: "app-key", Usage: "按 appKey/clientId 过滤", Bind: "appKey", Trim: true, OmitEmpty: true},
@@ -349,13 +334,8 @@ func newDevAppListCommand(runner executor.Runner) *cobra.Command {
{Name: "sort-type", Usage: "排序字段,如 gmt_modified", Bind: "sortType", Trim: true, OmitEmpty: true},
{Name: "sort-order", Usage: "排序方向 asc 或 desc", Bind: "sortOrder", Trim: true, OmitEmpty: true},
},
// cursor/pageSize 由 devAppApplyCursorParams 注入(退役 devAppApplyCursorParams 之外,
// 本命令同时退役 devAppPutString/devAppPutInt/devAppFlagOrFallback 全套手搓 helper)。
Call: devAppCallCursor(runner),
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppListTool)
registerDevAppCursorFlags(cmd)
},
Call: devAppCallCursor(runner),
PostMount: devAppMetaCursor(devAppListTool),
})
}
@@ -629,7 +609,7 @@ func newDevAppPermissionListCommand(runner executor.Runner) *cobra.Command {
Use: "list",
Short: "查询开放平台应用权限列表",
Example: " dws dev app permission list --unified-app-id UNIFIED_APP_ID --keyword 通讯录 --page-size 20 --format json",
Tool: devAppPermissionListTool,
Tool: devAppPermissionListTool,
// 命令级别名 "search" 由 PostMount 设回(LeafSpec 无 Command Aliases 字段)。
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
@@ -646,8 +626,7 @@ func newDevAppPermissionListCommand(runner executor.Runner) *cobra.Command {
Call: devAppCallCursor(runner),
PostMount: func(cmd *cobra.Command) {
cmd.Aliases = []string{"search"}
devAppLeafMeta(cmd, devAppPermissionListTool)
registerDevAppCursorFlags(cmd)
devAppMetaCursor(devAppPermissionListTool)(cmd)
},
})
}
@@ -660,6 +639,7 @@ func newDevAppPermissionAddCommand(runner executor.Runner) *cobra.Command {
Tool: devAppPermissionAddTool,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "scope-values", Usage: "权限点 scopeValue,多个用逗号或分号分隔", Bind: "scopeValues", Trim: true, Transform: transformDevAppScopeValues},
},
Validate: func(cmd *cobra.Command, args []string) error {
if err := devAppRequireWriteGuard(cmd, "permission add"); err != nil {
@@ -673,15 +653,8 @@ func newDevAppPermissionAddCommand(runner executor.Runner) *cobra.Command {
}
return nil
},
// scope-values 经 devAppPermissionScopes 解析为 []string,由 Call 注入。
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
params["scopeValues"] = devAppPermissionScopes(cmd)
return runDevAppTool(runner, cmd, tool, params)
},
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppPermissionAddTool)
cmd.Flags().String("scope-values", "", "权限点 scopeValue,多个用逗号或分号分隔")
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppPermissionAddTool),
})
}
@@ -693,6 +666,7 @@ func newDevAppPermissionRemoveCommand(runner executor.Runner) *cobra.Command {
Tool: devAppPermissionRmTool,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "scope-values", Usage: "待取消权限点 scopeValue,多个用逗号或分号分隔", Bind: "scopeValues", Trim: true, Transform: transformDevAppScopeValues},
},
Validate: func(cmd *cobra.Command, args []string) error {
if err := devAppRequireWriteGuard(cmd, "permission remove"); err != nil {
@@ -706,14 +680,8 @@ func newDevAppPermissionRemoveCommand(runner executor.Runner) *cobra.Command {
}
return nil
},
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
params["scopeValues"] = devAppPermissionScopes(cmd)
return runDevAppTool(runner, cmd, tool, params)
},
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppPermissionRmTool)
cmd.Flags().String("scope-values", "", "待取消权限点 scopeValue,多个用逗号或分号分隔")
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppPermissionRmTool),
})
}
@@ -745,6 +713,7 @@ func newDevAppMemberAddCommand(runner executor.Runner) *cobra.Command {
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "member-type", Usage: "成员类型,如 DEVELOPER (必填)", Bind: "memberType", Trim: true},
{Name: "user-ids", Usage: "成员 userId 列表,多个用逗号分隔 (必填)", Bind: "userIds", Trim: true, Aliases: []string{"member-user-ids"}, Transform: transformDevAppListParam},
},
Validate: func(cmd *cobra.Command, args []string) error {
if err := devAppRequireWriteGuard(cmd, op); err != nil {
@@ -761,18 +730,8 @@ func newDevAppMemberAddCommand(runner executor.Runner) *cobra.Command {
}
return nil
},
// userIds 由 Call 解析注入(user-ids / member-user-ids 别名回退)。
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
users, _ := requiredDevAppUsers(cmd)
params["userIds"] = users
return runDevAppTool(runner, cmd, tool, params)
},
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppMemberAddTool)
cmd.Flags().String("user-ids", "", "成员 userId 列表,多个用逗号分隔 (必填)")
cmd.Flags().String("member-user-ids", "", "成员 userId 列表,多个用逗号分隔 (兼容旧参数)")
_ = cmd.Flags().MarkHidden("member-user-ids")
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppMemberAddTool),
})
}
@@ -786,6 +745,7 @@ func newDevAppMemberRemoveCommand(runner executor.Runner) *cobra.Command {
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "member-type", Usage: "成员类型,如 DEVELOPER (必填)", Bind: "memberType", Trim: true},
{Name: "user-ids", Usage: "成员 userId 列表,多个用逗号分隔 (必填)", Bind: "userIds", Trim: true, Aliases: []string{"member-user-ids"}, Transform: transformDevAppListParam},
},
Validate: func(cmd *cobra.Command, args []string) error {
if err := devAppRequireWriteGuard(cmd, op); err != nil {
@@ -802,17 +762,8 @@ func newDevAppMemberRemoveCommand(runner executor.Runner) *cobra.Command {
}
return nil
},
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
users, _ := requiredDevAppUsers(cmd)
params["userIds"] = users
return runDevAppTool(runner, cmd, tool, params)
},
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppMemberRemoveTool)
cmd.Flags().String("user-ids", "", "成员 userId 列表,多个用逗号分隔 (必填)")
cmd.Flags().String("member-user-ids", "", "成员 userId 列表,多个用逗号分隔 (兼容旧参数)")
_ = cmd.Flags().MarkHidden("member-user-ids")
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppMemberRemoveTool),
})
}
@@ -826,6 +777,9 @@ func newDevAppSecurityConfigCommand(runner executor.Runner) *cobra.Command {
Tool: devAppSecurityConfigTool,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "ip-whitelist", Usage: "出口 IP 白名单,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "ipWhitelist", Trim: true, Transform: transformDevAppListParam},
{Name: "redirect-urls", Usage: "登录重定向 URL,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "redirectUrls", Trim: true, Transform: transformDevAppListParam},
{Name: "sso-urls", Usage: "端内免登地址,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "ssoUrls", Trim: true, Transform: transformDevAppListParam},
},
Validate: func(cmd *cobra.Command, args []string) error {
if err := devAppRequireWriteGuard(cmd, op); err != nil {
@@ -842,25 +796,8 @@ func newDevAppSecurityConfigCommand(runner executor.Runner) *cobra.Command {
}
return nil
},
// 三个列表 flag 经 parseDevAppListFlag 解析,非空才入参(与手写一致)。
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
if v := parseDevAppListFlag(cmd, "ip-whitelist"); len(v) > 0 {
params["ipWhitelist"] = v
}
if v := parseDevAppListFlag(cmd, "redirect-urls"); len(v) > 0 {
params["redirectUrls"] = v
}
if v := parseDevAppListFlag(cmd, "sso-urls"); len(v) > 0 {
params["ssoUrls"] = v
}
return runDevAppTool(runner, cmd, tool, params)
},
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppSecurityConfigTool)
cmd.Flags().String("ip-whitelist", "", "出口 IP 白名单,多个用逗号或分号分隔(整组覆盖,非追加)")
cmd.Flags().String("redirect-urls", "", "登录重定向 URL,多个用逗号或分号分隔(整组覆盖,非追加)")
cmd.Flags().String("sso-urls", "", "端内免登地址,多个用逗号或分号分隔(整组覆盖,非追加)")
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppSecurityConfigTool),
})
}
@@ -1173,7 +1110,7 @@ func newDevAppVersionListCommand(runner executor.Runner) *cobra.Command {
Use: "list",
Short: "分页查询应用版本列表",
Example: " dws dev app version list --unified-app-id UNIFIED_APP_ID --page-size 20 --format json",
Tool: devAppVersionListTool,
Tool: devAppVersionListTool,
Flags: []LeafFlag{
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
},
@@ -1181,11 +1118,8 @@ func newDevAppVersionListCommand(runner executor.Runner) *cobra.Command {
_, err := requiredDevAppUnifiedID(cmd)
return err
},
Call: devAppCallCursor(runner),
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppVersionListTool)
registerDevAppCursorFlags(cmd)
},
Call: devAppCallCursor(runner),
PostMount: devAppMetaCursor(devAppVersionListTool),
})
}
@@ -1218,15 +1152,12 @@ func newDevAppVersionCheckApprovalCommand(runner executor.Runner) *cobra.Command
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "version-id", Usage: "版本 ID (必填)", Bind: "versionId", Trim: true},
},
ConstParams: map[string]any{"precheckOnly": true},
Validate: func(cmd *cobra.Command, args []string) error {
_, err := devAppVersionLocator(cmd)
return err
},
// 复用 publish 工具的服务端预检模式:precheckOnly=true 只返回审批要求,不发布。
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
params["precheckOnly"] = true
return runDevAppTool(runner, cmd, tool, params)
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppVersionPublishTool),
})
}
@@ -1241,7 +1172,9 @@ func newDevAppVersionPublishCommand(runner executor.Runner) *cobra.Command {
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
{Name: "version-id", Usage: "版本 ID (必填)", Bind: "versionId", Trim: true},
{Name: "approver-user-id", Usage: "灰度选人模式下指定审批人 userId", Bind: "approverUserId", Trim: true, OmitEmpty: true},
{Name: "confirmed-sensitive", Usage: "确认发布包含高敏权限的版本", Kind: LeafBool, Bind: "confirmedSensitive"},
},
ConstParams: map[string]any{"precheckOnly": false},
Validate: func(cmd *cobra.Command, args []string) error {
if err := devAppRequireWriteGuard(cmd, "version publish"); err != nil {
return err
@@ -1251,20 +1184,8 @@ func newDevAppVersionPublishCommand(runner executor.Runner) *cobra.Command {
}
return nil
},
// precheckOnly=false(真发布)+ confirmed-sensitive 的 Changed() 语义由 Call 处理
//(框架无 LeafBool/Changed 语义,Bool flag 在 PostMount 注册)。
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
params["precheckOnly"] = false
if cmd.Flags().Changed("confirmed-sensitive") {
value, _ := cmd.Flags().GetBool("confirmed-sensitive")
params["confirmedSensitive"] = value
}
return runDevAppTool(runner, cmd, tool, params)
},
PostMount: func(cmd *cobra.Command) {
devAppLeafMeta(cmd, devAppVersionPublishTool)
cmd.Flags().Bool("confirmed-sensitive", false, "确认发布包含高敏权限的版本")
},
Call: devAppCall(runner),
PostMount: devAppMeta(devAppVersionPublishTool),
})
}
@@ -1338,20 +1259,13 @@ func devAppLeafMeta(cmd *cobra.Command, tool string) {
}
// devAppCall 返回统一派发闭包(替代各命令重复的 Call: runDevAppTool 透传)。
// 参数装配由 Flags/ConstParams 完成;本闭包只负责执行与响应处理。
func devAppCall(runner executor.Runner) func(*cobra.Command, string, map[string]any) error {
return func(cmd *cobra.Command, tool string, params map[string]any) error {
return runDevAppTool(runner, cmd, tool, params)
}
}
// devAppCallCursor 同上,但先经 devAppApplyCursorParams 注入 cursor/pageSize。
func devAppCallCursor(runner executor.Runner) func(*cobra.Command, string, map[string]any) error {
return func(cmd *cobra.Command, tool string, params map[string]any) error {
devAppApplyCursorParams(cmd, params)
return runDevAppTool(runner, cmd, tool, params)
}
}
// devAppMeta 返回纯收尾 PostMount 闭包(无额外 flag 的命令用)。
func devAppMeta(tool string) func(*cobra.Command) {
return func(cmd *cobra.Command) { devAppLeafMeta(cmd, tool) }
@@ -1866,6 +1780,34 @@ func splitDevAppList(raw string) []string {
return values
}
// transformDevAppListParam splits a comma/semicolon list for LeafFlag.Transform.
// Empty input returns nil so the key is omitted from toolArgs.
func transformDevAppListParam(raw string) (any, error) {
values := splitDevAppList(raw)
if len(values) == 0 {
return nil, nil
}
return values, nil
}
// transformDevAppScopeValues preserves the double-split used by
// devAppPermissionScopes (each comma-separated token may itself be a list).
func transformDevAppScopeValues(raw string) (any, error) {
values := splitDevAppList(raw)
out := make([]string, 0, len(values))
for _, value := range values {
for _, part := range splitDevAppList(value) {
if part != "" {
out = append(out, part)
}
}
}
if len(out) == 0 {
return nil, nil
}
return out, nil
}
// 应用定位:写操作统一只用 --unified-app-id;dev app get 额外支持只读 --app-key。
// --name 定位已下线(列表搜索的 --name/--app-key 是过滤参数、不在此列)。
// 写操作与其它 app 作用域命令共用 addDevAppUnifiedIDFlag + requiredDevAppUnifiedID。
+30 -10
View File
@@ -13,16 +13,20 @@
package helpers
import "github.com/spf13/cobra"
import (
"github.com/spf13/cobra"
// devapp_cursor wires cursor pagination as a PASS-THROUGH: the CLI forwards
// --cursor and --page-size straight into the upstream params, and the upstream
// returns nextCursor/hasMore in its response. No synthesis, no offset/page
// translation, no encode/decode in the CLI — the upstream tools own the cursor
// contract (see docs/cursor-pagination-design.md and docs/upstream-todo.md).
// Until the upstream tools accept `cursor`/`pageSize` and return `nextCursor`,
// paging is in the "pending integration" state, same as the precheckOnly
// rename — no anti-corruption layer bridges the gap.
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
)
// Cursor pagination is a devapp list tool, not a LeafSpec declaration.
// List factories compose registerDevAppCursorFlags (PostMount) with
// devAppCallCursor (Call). LeafSpec itself has no Cursor field and does not
// list --cursor/--page-size in Flags.
//
// Semantics stay PASS-THROUGH: the CLI forwards cursor/pageSize into upstream
// params and the upstream returns nextCursor/hasMore. No synthesis, no
// offset/page translation — see docs/cursor-pagination-design.md.
// registerDevAppCursorFlags adds the two cursor flags every list/search command
// exposes. pageSize defaults to 20.
@@ -32,7 +36,7 @@ func registerDevAppCursorFlags(cmd *cobra.Command) {
}
// devAppApplyCursorParams forwards cursor/pageSize into params as-is. cursor is
// only set when non-empty (first page omits it).
// only set when non-empty (first page omits it). size < 1 floors to 20.
func devAppApplyCursorParams(cmd *cobra.Command, params map[string]any) {
if cur := devAppStringFlag(cmd, "cursor"); cur != "" {
params["cursor"] = cur
@@ -43,3 +47,19 @@ func devAppApplyCursorParams(cmd *cobra.Command, params map[string]any) {
}
params["pageSize"] = size
}
// devAppCallCursor is the list execution body: apply cursor params, then run.
func devAppCallCursor(runner executor.Runner) func(*cobra.Command, string, map[string]any) error {
return func(cmd *cobra.Command, tool string, params map[string]any) error {
devAppApplyCursorParams(cmd, params)
return runDevAppTool(runner, cmd, tool, params)
}
}
// devAppMetaCursor is PostMount for list leaves: surface meta + cursor flags.
func devAppMetaCursor(tool string) func(*cobra.Command) {
return func(cmd *cobra.Command) {
devAppLeafMeta(cmd, tool)
registerDevAppCursorFlags(cmd)
}
}
+4 -4
View File
@@ -283,7 +283,7 @@ func TestDevAppListLeaf(t *testing.T) {
}
}
// TestDevAppEventListLeaf 验证 cursor/pageSize 经 devAppApplyCursorParams 注入。
// TestDevAppEventListLeaf 验证 cursor/pageSize 经 devAppCallCursor 工具注入。
func TestDevAppEventListLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := newDevAppEventListCommand(r)
@@ -294,9 +294,9 @@ func TestDevAppEventListLeaf(t *testing.T) {
if v, _ := r.got.Params["unifiedAppId"].(string); v != "APP-E" {
t.Fatalf("unifiedAppId = %q, want APP-E", v)
}
// page-size 默认 20,经 devAppApplyCursorParams 注入为 float64(20)。
if _, present := r.got.Params["pageSize"]; !present {
t.Fatal("pageSize missing, want injected by devAppApplyCursorParams")
// page-size 默认 20,由 registerDevAppCursorFlags + CallCursor 注入。
if v, present := r.got.Params["pageSize"]; !present || v != 20 {
t.Fatalf("pageSize = %v, want 20 from cursor tool", v)
}
// cursor 默认空:不入参。
if _, present := r.got.Params["cursor"]; present {
+11 -5
View File
@@ -117,10 +117,14 @@ type LeafSpec struct {
// 则中止且不派发。
Risk LeafRisk
// Call 是可插拔派发函数,非空时替代默认的 callMCPTool/callMCPToolOnServer。
// 供非 MCP 直连命令(如 devapp 走 executor.Runner)复用本框架:调用方用
// 闭包捕获自己的 runner/派发器即可。签名与默认路径一致——收到框架装配好
// 的 toolArgs,自行派发。
// ConstParams 是与 flag 无关的固定载荷(如 precheckOnly),在 flag 装配
// 之后并入 toolArgs。它们从不满足 Required。
ConstParams map[string]any
// Call 是执行体切面:非空时替代默认的 callMCPTool/callMCPToolOnServer。
// 供非 MCP 直连命令(如 devapp 走 executor.Runner)注入派发与响应处理。
// 收到的 toolArgs 已由 Flags/ConstParams 装配完成;Call 不应再写业务参数。
// 分页等横切能力由领域工具(如 devAppCallCursor)在执行侧处理,不进声明。
Call func(cmd *cobra.Command, tool string, args map[string]any) error
// Validate 是跨 flag 校验钩子(如时间区间、互斥关系),在 required
@@ -133,7 +137,8 @@ type LeafSpec struct {
// PostMount 在 flag 注册完成之后、RunE 设定之前对构建好的 cmd 做最终
// 调整(设置 Args/DisableAutoGenTag、调用 annotate/preferLegacy 等)。
// 无论是否使用 RunE 逃生舱都会执行。对标 lark shortcut 的 PostMount。
// 业务 flag 必须写在 Flags 里;分页 flag 由领域工具在 PostMount 注入,
// 不作为 LeafSpec 声明字段。
PostMount func(cmd *cobra.Command)
}
@@ -160,6 +165,7 @@ func FromLeafSpec(spec LeafSpec) cmdcore.CommandSpec {
Flags: spec.Flags,
Constraints: spec.Constraints,
Risk: spec.Risk,
ConstParams: spec.ConstParams,
Validate: spec.Validate,
PostMount: spec.PostMount,
RunE: spec.RunE,
+165
View File
@@ -0,0 +1,165 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
// TestLeafSpecPostMountDoesNotRegisterBusinessFlags keeps business flags in
// Flags. PostMount may set surface metadata and may call domain tools such as
// registerDevAppCursorFlags (pagination is a tool, not a LeafSpec field).
func TestLeafSpecPostMountDoesNotRegisterBusinessFlags(t *testing.T) {
for _, call := range leafSpecCompositeLits(t) {
post := fieldExpr(call, "PostMount")
if post == nil {
continue
}
ast.Inspect(post, func(n ast.Node) bool {
expr, ok := n.(*ast.CallExpr)
if !ok {
return true
}
fun, ok := expr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
switch fun.Sel.Name {
case "String", "Bool", "Int", "StringSlice", "Int64", "Float64":
inner, ok := fun.X.(*ast.CallExpr)
if !ok {
return true
}
innerSel, ok := inner.Fun.(*ast.SelectorExpr)
if ok && innerSel.Sel.Name == "Flags" {
t.Errorf("%s: PostMount registers business flag via Flags().%s; declare it in Flags instead (cursor tools use registerDevAppCursorFlags)",
call.pos, fun.Sel.Name)
}
}
return true
})
}
}
// TestLeafSpecCallDoesNotAssembleBusinessParams keeps Call as an execution body.
// Business binding belongs in Flags/ConstParams. Named helpers like
// devAppCall / devAppCallCursor may apply domain tooling (including cursor).
func TestLeafSpecCallDoesNotAssembleBusinessParams(t *testing.T) {
for _, call := range leafSpecCompositeLits(t) {
body := fieldExpr(call, "Call")
if body == nil {
continue
}
// Named helpers (devAppCall / devAppCallCursor) are execution tools.
if _, ok := body.(*ast.CallExpr); ok {
continue
}
if ident, ok := body.(*ast.Ident); ok {
if strings.HasPrefix(ident.Name, "devAppCall") {
continue
}
}
ast.Inspect(body, func(n ast.Node) bool {
assign, ok := n.(*ast.AssignStmt)
if !ok {
return true
}
for _, lhs := range assign.Lhs {
index, ok := lhs.(*ast.IndexExpr)
if !ok {
continue
}
if ident, ok := index.X.(*ast.Ident); ok && ident.Name == "params" {
t.Errorf("%s: Call assigns params[...]; move business binding to Flags/ConstParams", call.pos)
}
}
return true
})
}
}
type leafSpecLit struct {
pos string
lit *ast.CompositeLit
}
func leafSpecCompositeLits(t *testing.T) []leafSpecLit {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
dir := filepath.Dir(file)
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool {
name := info.Name()
return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go")
}, 0)
if err != nil {
t.Fatalf("parse helpers: %v", err)
}
var out []leafSpecLit
for _, pkg := range pkgs {
for _, f := range pkg.Files {
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
fun, ok := call.Fun.(*ast.Ident)
if !ok || fun.Name != "NewLeafCommand" || len(call.Args) != 1 {
return true
}
lit, ok := call.Args[0].(*ast.CompositeLit)
if !ok {
return true
}
if typ, ok := lit.Type.(*ast.Ident); !ok || typ.Name != "LeafSpec" {
return true
}
out = append(out, leafSpecLit{
pos: fset.Position(lit.Pos()).String(),
lit: lit,
})
return true
})
}
}
if len(out) == 0 {
t.Fatal("no NewLeafCommand(LeafSpec{...}) literals found")
}
return out
}
func fieldExpr(call leafSpecLit, name string) ast.Expr {
for _, elt := range call.lit.Elts {
kv, ok := elt.(*ast.KeyValueExpr)
if !ok {
continue
}
key, ok := kv.Key.(*ast.Ident)
if !ok || key.Name != name {
continue
}
return kv.Value
}
return nil
}