fix: align agent schema parameter contracts
This commit is contained in:
1 parent
391e761b59
commit
ddad2f648c
26 files changed
+766
-176
No files matched your search
@@ -593,6 +593,73 @@ func TestRuntimeSchemaUsesMCPMetadataAnnotations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeSchemaEmitsCrossParameterConstraintsAndPositionals(t *testing.T) {
|
||||
root := &cobra.Command{Use: "dws"}
|
||||
send := &cobra.Command{Use: "send", RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
send.Flags().String("group", "", "群聊必填")
|
||||
send.Flags().String("users", "", "单聊必填")
|
||||
AttachRuntimeSchema(send, "chat", "send_robot_message", "hardcoded:chat")
|
||||
AnnotateRuntimeConstraints(send, RuntimeSchemaConstraints{
|
||||
MutuallyExclusive: [][]string{{"group", "users"}},
|
||||
RequireOneOf: [][]string{{"group", "users"}},
|
||||
})
|
||||
AnnotateRuntimePositionals(send, RuntimeSchemaPositional{
|
||||
Name: "scope", Type: "array", Variadic: true, Index: 0,
|
||||
})
|
||||
chat := &cobra.Command{Use: "chat"}
|
||||
chat.AddCommand(send)
|
||||
root.AddCommand(chat)
|
||||
|
||||
payload, err := runtimeSchemaPayload(root, []string{"chat.send_robot_message"})
|
||||
if err != nil {
|
||||
t.Fatalf("runtimeSchemaPayload() error = %v", err)
|
||||
}
|
||||
params, _ := payload["parameters"].(map[string]any)
|
||||
for _, name := range []string{"group", "users"} {
|
||||
param, _ := params[name].(map[string]any)
|
||||
if param["required"] != false {
|
||||
t.Fatalf("%s required = %#v, want conditional false", name, param["required"])
|
||||
}
|
||||
}
|
||||
constraints, _ := payload["constraints"].(map[string]any)
|
||||
requireOneOf, _ := constraints["require_one_of"].([][]string)
|
||||
if len(requireOneOf) != 1 || !equalStrings(requireOneOf[0], []string{"group", "users"}) {
|
||||
t.Fatalf("require_one_of = %#v", constraints["require_one_of"])
|
||||
}
|
||||
positionals, _ := payload["positionals"].([]RuntimeSchemaPositional)
|
||||
if len(positionals) != 1 || positionals[0].Name != "scope" || !positionals[0].Variadic {
|
||||
t.Fatalf("positionals = %#v", payload["positionals"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeSchemaAnnotationsDoNotChangeCobraValidation(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "read", RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
cmd.Flags().String("node", "", "node ID")
|
||||
cmd.Flags().String("url", "", "node URL alias")
|
||||
AnnotateRuntimeRequiredFlags(cmd, "node")
|
||||
cmd.SetArgs([]string{"--url", "https://example.com/doc"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("schema-only required annotation rejected alias invocation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeSchemaExcludesHumanOnlyHints(t *testing.T) {
|
||||
root := &cobra.Command{Use: "dws"}
|
||||
product := &cobra.Command{Use: "sheet"}
|
||||
real := &cobra.Command{Use: "range-read", RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
hint := &cobra.Command{Use: "read", RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
AttachRuntimeSchema(real, "sheet", "range_read", "runtime:sheet")
|
||||
AttachRuntimeSchema(hint, "sheet", "read", "runtime:sheet")
|
||||
ExcludeFromRuntimeSchema(hint)
|
||||
product.AddCommand(real, hint)
|
||||
root.AddCommand(product)
|
||||
|
||||
entries := collectRuntimeSchemaEntries(root)
|
||||
if len(entries) != 1 || entries[0].CLIPath != "sheet range-read" {
|
||||
t.Fatalf("entries = %#v, want only executable leaf", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeSchemaUsesCuratedRequiredSemantics(t *testing.T) {
|
||||
root := &cobra.Command{Use: "dws"}
|
||||
|
||||
|
||||
@@ -116,7 +116,8 @@ func helperLocalLeafSchema(cmd *cobra.Command) map[string]any {
|
||||
canonicalPath = productID + "." + toolName
|
||||
}
|
||||
hint := schemaHintForCanonicalPath(canonicalPath)
|
||||
parameters := runtimeCommandParameters(cmd, hint.Parameters, nil)
|
||||
constraints := runtimeCommandConstraints(cmd)
|
||||
parameters := runtimeCommandParameters(cmd, hint.Parameters, nil, constraints)
|
||||
if parameters == nil {
|
||||
parameters = map[string]any{}
|
||||
}
|
||||
@@ -141,6 +142,12 @@ func helperLocalLeafSchema(cmd *cobra.Command) map[string]any {
|
||||
"has_parameters": len(parameters) > 0,
|
||||
"parameter_count": len(parameters),
|
||||
}
|
||||
if rendered := runtimeConstraintsPayload(constraints); len(rendered) > 0 {
|
||||
payload["constraints"] = rendered
|
||||
}
|
||||
if positionals := runtimeCommandPositionals(cmd); len(positionals) > 0 {
|
||||
payload["positionals"] = positionals
|
||||
}
|
||||
if len(meta.Aliases) > 0 {
|
||||
payload["aliases"] = meta.Aliases
|
||||
}
|
||||
|
||||
@@ -34,13 +34,36 @@ const (
|
||||
runtimeSchemaTitleAnnotation = "dws.schema.title"
|
||||
runtimeSchemaDescAnnotation = "dws.schema.description"
|
||||
runtimeSchemaMetaAnnotation = "dws.schema.metadata_source"
|
||||
runtimeSchemaExcludeAnnotation = "dws.schema.exclude"
|
||||
runtimeSchemaRulesAnnotation = "dws.schema.constraints"
|
||||
runtimeSchemaArgsAnnotation = "dws.schema.positionals"
|
||||
|
||||
runtimeSchemaFlagPropertyAnnotation = "dws.schema.property"
|
||||
runtimeSchemaFlagTypeAnnotation = "dws.schema.type"
|
||||
runtimeSchemaFlagRequiredAnnotation = "dws.schema.required"
|
||||
runtimeSchemaFlagDefaultAnnotation = "dws.schema.default"
|
||||
runtimeSchemaFlagExampleAnnotation = "dws.schema.example"
|
||||
)
|
||||
|
||||
// RuntimeSchemaConstraints describes cross-parameter rules that cannot be
|
||||
// represented by an individual parameter's required bit.
|
||||
type RuntimeSchemaConstraints struct {
|
||||
MutuallyExclusive [][]string `json:"mutually_exclusive,omitempty"`
|
||||
RequireOneOf [][]string `json:"require_one_of,omitempty"`
|
||||
RequireTogether [][]string `json:"require_together,omitempty"`
|
||||
}
|
||||
|
||||
// RuntimeSchemaPositional describes one ordered CLI argument. Name is also
|
||||
// used by RuntimeSchemaConstraints when a one-of group mixes flags and args.
|
||||
type RuntimeSchemaPositional struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
Variadic bool `json:"variadic,omitempty"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
type embeddedMCPMetadata struct {
|
||||
Version int `json:"version"`
|
||||
Source string `json:"source"`
|
||||
@@ -155,6 +178,127 @@ func AnnotateRuntimeFlag(cmd *cobra.Command, flagName, propertyName, paramType s
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeRequiredFlags records schema-only required semantics. Unlike
|
||||
// cobra.MarkFlagRequired, it does not require the primary flag itself to be
|
||||
// changed, so helper commands can keep accepting hidden --url/--id aliases.
|
||||
func AnnotateRuntimeRequiredFlags(cmd *cobra.Command, flagNames ...string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
for _, name := range flagNames {
|
||||
flag := cmd.Flags().Lookup(strings.TrimSpace(name))
|
||||
if flag != nil {
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, "true")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagFormat records a machine-readable value format without
|
||||
// changing the Cobra flag type.
|
||||
func AnnotateRuntimeFlagFormat(cmd *cobra.Command, flagName, format string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := cmd.Flags().Lookup(strings.TrimSpace(flagName)); flag != nil {
|
||||
setFlagAnnotation(flag, "x-cli-format", strings.TrimSpace(format))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagEnum records the accepted values for a flag.
|
||||
func AnnotateRuntimeFlagEnum(cmd *cobra.Command, flagName string, values ...string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
flag := cmd.Flags().Lookup(strings.TrimSpace(flagName))
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
clean := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
clean = append(clean, value)
|
||||
}
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return
|
||||
}
|
||||
if flag.Annotations == nil {
|
||||
flag.Annotations = map[string][]string{}
|
||||
}
|
||||
flag.Annotations["x-cli-enum"] = clean
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagExample records a valid representative CLI value.
|
||||
func AnnotateRuntimeFlagExample(cmd *cobra.Command, flagName, example string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := cmd.Flags().Lookup(strings.TrimSpace(flagName)); flag != nil {
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagExampleAnnotation, strings.TrimSpace(example))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeConstraints records command-level parameter relationships.
|
||||
func AnnotateRuntimeConstraints(cmd *cobra.Command, constraints RuntimeSchemaConstraints) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
constraints = normalizeRuntimeSchemaConstraints(constraints)
|
||||
if runtimeSchemaConstraintsEmpty(constraints) {
|
||||
return
|
||||
}
|
||||
if existing := runtimeCommandConstraints(cmd); !runtimeSchemaConstraintsEmpty(existing) {
|
||||
constraints.MutuallyExclusive = append(existing.MutuallyExclusive, constraints.MutuallyExclusive...)
|
||||
constraints.RequireOneOf = append(existing.RequireOneOf, constraints.RequireOneOf...)
|
||||
constraints.RequireTogether = append(existing.RequireTogether, constraints.RequireTogether...)
|
||||
constraints = normalizeRuntimeSchemaConstraints(constraints)
|
||||
}
|
||||
encoded, _ := json.Marshal(constraints)
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaRulesAnnotation, string(encoded))
|
||||
}
|
||||
|
||||
// AnnotateRuntimePositionals records ordered positional arguments for agents.
|
||||
func AnnotateRuntimePositionals(cmd *cobra.Command, positionals ...RuntimeSchemaPositional) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
clean := make([]RuntimeSchemaPositional, 0, len(positionals))
|
||||
for _, positional := range positionals {
|
||||
positional.Name = strings.TrimSpace(positional.Name)
|
||||
positional.Type = strings.TrimSpace(positional.Type)
|
||||
positional.Description = strings.TrimSpace(positional.Description)
|
||||
if positional.Name == "" || positional.Index < 0 {
|
||||
continue
|
||||
}
|
||||
if positional.Type == "" {
|
||||
positional.Type = "string"
|
||||
}
|
||||
clean = append(clean, positional)
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return
|
||||
}
|
||||
sort.SliceStable(clean, func(i, j int) bool { return clean[i].Index < clean[j].Index })
|
||||
encoded, _ := json.Marshal(clean)
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaArgsAnnotation, string(encoded))
|
||||
}
|
||||
|
||||
// ExcludeFromRuntimeSchema keeps a human-facing hint or redirect in --help
|
||||
// while preventing it from being advertised as an executable agent tool.
|
||||
func ExcludeFromRuntimeSchema(cmd *cobra.Command) {
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaExcludeAnnotation, "true")
|
||||
}
|
||||
|
||||
func setRuntimeCommandAnnotation(cmd *cobra.Command, key, value string) {
|
||||
if cmd == nil || strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
cmd.Annotations[key] = value
|
||||
}
|
||||
|
||||
func setFlagAnnotation(flag *pflag.Flag, key, value string) {
|
||||
if flag == nil || strings.TrimSpace(value) == "" {
|
||||
return
|
||||
@@ -206,6 +350,9 @@ func collectRuntimeSchemaEntries(root *cobra.Command) []runtimeSchemaEntry {
|
||||
entries := []runtimeSchemaEntry{}
|
||||
seen := map[string]bool{}
|
||||
walkLeafCommands(root, func(leaf *cobra.Command) {
|
||||
if runtimeSchemaExcluded(leaf) {
|
||||
return
|
||||
}
|
||||
productID, toolName, source := runtimeSchemaAnnotations(leaf)
|
||||
if productID == "" || toolName == "" {
|
||||
return
|
||||
@@ -248,6 +395,9 @@ func collectRuntimeSchemaEntries(root *cobra.Command) []runtimeSchemaEntry {
|
||||
}
|
||||
productName := strings.TrimSpace(productRoot.Short)
|
||||
walkLeafCommands(productRoot, func(leaf *cobra.Command) {
|
||||
if runtimeSchemaExcluded(leaf) {
|
||||
return
|
||||
}
|
||||
parts := commandPathParts(leaf)
|
||||
if len(parts) == 0 {
|
||||
return
|
||||
@@ -426,6 +576,11 @@ func runtimeSchemaAnnotations(cmd *cobra.Command) (productID, toolName, source s
|
||||
return productID, toolName, source
|
||||
}
|
||||
|
||||
func runtimeSchemaExcluded(cmd *cobra.Command) bool {
|
||||
return cmd != nil && cmd.Annotations != nil &&
|
||||
strings.EqualFold(strings.TrimSpace(cmd.Annotations[runtimeSchemaExcludeAnnotation]), "true")
|
||||
}
|
||||
|
||||
func runtimeCommandTitle(cmd *cobra.Command) string {
|
||||
if cmd == nil {
|
||||
return ""
|
||||
@@ -741,7 +896,8 @@ func runtimeToolPayload(entry runtimeSchemaEntry) map[string]any {
|
||||
hint := runtimeSchemaHintForEntry(entry)
|
||||
embeddedMeta, hasEmbeddedMeta := embeddedMCPMetadataForEntry(entry)
|
||||
title, description, metadataSource := runtimeToolTextMetadata(entry)
|
||||
parameters := runtimeCommandParameters(entry.Command, hint.Parameters, embeddedMeta.Parameters)
|
||||
constraints := runtimeCommandConstraints(entry.Command)
|
||||
parameters := runtimeCommandParameters(entry.Command, hint.Parameters, embeddedMeta.Parameters, constraints)
|
||||
if parameters == nil {
|
||||
parameters = map[string]any{}
|
||||
}
|
||||
@@ -776,13 +932,19 @@ func runtimeToolPayload(entry runtimeSchemaEntry) map[string]any {
|
||||
if len(entry.Aliases) > 0 {
|
||||
payload["aliases"] = entry.Aliases
|
||||
}
|
||||
if rendered := runtimeConstraintsPayload(constraints); len(rendered) > 0 {
|
||||
payload["constraints"] = rendered
|
||||
}
|
||||
if positionals := runtimeCommandPositionals(entry.Command); len(positionals) > 0 {
|
||||
payload["positionals"] = positionals
|
||||
}
|
||||
paths := []string{entry.PrimaryCLIPath, entry.CLIPath, canonicalPath}
|
||||
paths = append(paths, entry.Aliases...)
|
||||
applyAgentToolMetadata(payload, true, paths...)
|
||||
return payload
|
||||
}
|
||||
|
||||
func runtimeCommandParameters(cmd *cobra.Command, hints map[string]ParameterSchemaHint, embeddedParams map[string]embeddedMCPParamMeta) map[string]any {
|
||||
func runtimeCommandParameters(cmd *cobra.Command, hints map[string]ParameterSchemaHint, embeddedParams map[string]embeddedMCPParamMeta, constraints RuntimeSchemaConstraints) map[string]any {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -853,20 +1015,120 @@ func runtimeCommandParameters(cmd *cobra.Command, hints map[string]ParameterSche
|
||||
entry["format"] = format
|
||||
} else if hasEmbeddedParam && strings.TrimSpace(embeddedParam.Format) != "" {
|
||||
entry["format"] = strings.TrimSpace(embeddedParam.Format)
|
||||
} else if format := inferredRuntimeFlagFormat(flag); format != "" {
|
||||
entry["format"] = format
|
||||
}
|
||||
if enum := runtimeFlagEnum(flag); len(enum) > 0 {
|
||||
entry["enum"] = enum
|
||||
} else if hasEmbeddedParam && len(embeddedParam.Enum) > 0 {
|
||||
entry["enum"] = append([]string(nil), embeddedParam.Enum...)
|
||||
}
|
||||
if example := firstFlagAnnotation(flag, runtimeSchemaFlagExampleAnnotation); example != "" {
|
||||
entry["example"] = example
|
||||
}
|
||||
params[flagName] = entry
|
||||
})
|
||||
// A member of a require-one-of group is conditionally required, not
|
||||
// individually required. This also corrects usage text such as "群聊必填"
|
||||
// that would otherwise make every alternative look globally mandatory.
|
||||
for _, group := range constraints.RequireOneOf {
|
||||
for _, name := range group {
|
||||
if entry, ok := params[name].(map[string]any); ok {
|
||||
entry["required"] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(params) == 0 {
|
||||
return nil
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func runtimeCommandConstraints(cmd *cobra.Command) RuntimeSchemaConstraints {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return RuntimeSchemaConstraints{}
|
||||
}
|
||||
raw := strings.TrimSpace(cmd.Annotations[runtimeSchemaRulesAnnotation])
|
||||
if raw == "" {
|
||||
return RuntimeSchemaConstraints{}
|
||||
}
|
||||
var constraints RuntimeSchemaConstraints
|
||||
if json.Unmarshal([]byte(raw), &constraints) != nil {
|
||||
return RuntimeSchemaConstraints{}
|
||||
}
|
||||
return normalizeRuntimeSchemaConstraints(constraints)
|
||||
}
|
||||
|
||||
func runtimeCommandPositionals(cmd *cobra.Command) []RuntimeSchemaPositional {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return nil
|
||||
}
|
||||
raw := strings.TrimSpace(cmd.Annotations[runtimeSchemaArgsAnnotation])
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var positionals []RuntimeSchemaPositional
|
||||
if json.Unmarshal([]byte(raw), &positionals) != nil {
|
||||
return nil
|
||||
}
|
||||
sort.SliceStable(positionals, func(i, j int) bool { return positionals[i].Index < positionals[j].Index })
|
||||
return positionals
|
||||
}
|
||||
|
||||
func normalizeRuntimeSchemaConstraints(constraints RuntimeSchemaConstraints) RuntimeSchemaConstraints {
|
||||
constraints.MutuallyExclusive = normalizeRuntimeSchemaGroups(constraints.MutuallyExclusive, 2)
|
||||
constraints.RequireOneOf = normalizeRuntimeSchemaGroups(constraints.RequireOneOf, 1)
|
||||
constraints.RequireTogether = normalizeRuntimeSchemaGroups(constraints.RequireTogether, 2)
|
||||
return constraints
|
||||
}
|
||||
|
||||
func normalizeRuntimeSchemaGroups(groups [][]string, minimum int) [][]string {
|
||||
out := make([][]string, 0, len(groups))
|
||||
seenGroups := map[string]bool{}
|
||||
for _, group := range groups {
|
||||
clean := make([]string, 0, len(group))
|
||||
seenNames := map[string]bool{}
|
||||
for _, name := range group {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || seenNames[name] {
|
||||
continue
|
||||
}
|
||||
seenNames[name] = true
|
||||
clean = append(clean, name)
|
||||
}
|
||||
if len(clean) < minimum {
|
||||
continue
|
||||
}
|
||||
key := strings.Join(clean, "\x00")
|
||||
if seenGroups[key] {
|
||||
continue
|
||||
}
|
||||
seenGroups[key] = true
|
||||
out = append(out, clean)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func runtimeSchemaConstraintsEmpty(constraints RuntimeSchemaConstraints) bool {
|
||||
return len(constraints.MutuallyExclusive) == 0 &&
|
||||
len(constraints.RequireOneOf) == 0 &&
|
||||
len(constraints.RequireTogether) == 0
|
||||
}
|
||||
|
||||
func runtimeConstraintsPayload(constraints RuntimeSchemaConstraints) map[string]any {
|
||||
payload := map[string]any{}
|
||||
if len(constraints.MutuallyExclusive) > 0 {
|
||||
payload["mutually_exclusive"] = constraints.MutuallyExclusive
|
||||
}
|
||||
if len(constraints.RequireOneOf) > 0 {
|
||||
payload["require_one_of"] = constraints.RequireOneOf
|
||||
}
|
||||
if len(constraints.RequireTogether) > 0 {
|
||||
payload["require_together"] = constraints.RequireTogether
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func lookupEmbeddedMCPParam(params map[string]embeddedMCPParamMeta, property, flagName string) (embeddedMCPParamMeta, bool) {
|
||||
if len(params) == 0 {
|
||||
return embeddedMCPParamMeta{}, false
|
||||
@@ -920,10 +1182,7 @@ func runtimeFlagRequired(flag *pflag.Flag) bool {
|
||||
return true
|
||||
}
|
||||
usage := strings.ToLower(strings.TrimSpace(flag.Usage))
|
||||
if usageImpliesRequired(usage) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return usageImpliesRequired(usage)
|
||||
}
|
||||
|
||||
func usageImpliesRequired(usage string) bool {
|
||||
@@ -1012,6 +1271,16 @@ func runtimeFlagEnum(flag *pflag.Flag) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func inferredRuntimeFlagFormat(flag *pflag.Flag) string {
|
||||
if flag == nil {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(strings.ToLower(strings.TrimSpace(flag.Usage)), "a1") {
|
||||
return "a1-range"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func strconvQuote(value string) string {
|
||||
return "\"" + strings.ReplaceAll(value, "\"", "\\\"") + "\""
|
||||
}
|
||||
@@ -61,19 +61,6 @@
|
||||
"skills/mono/references/products/contact.md"
|
||||
]
|
||||
},
|
||||
"contact get": {
|
||||
"use_when": [
|
||||
"查用户详情/部门/主管/管理员"
|
||||
],
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws contact user get --ids userId1,userId2"
|
||||
],
|
||||
"source_refs": [
|
||||
"skills/mono/references/products/contact.md"
|
||||
]
|
||||
},
|
||||
"contact relation list-my-followings": {
|
||||
"use_when": [
|
||||
"我关注了谁/我的特别关注列表/我的星标联系人/特别关注的人有哪些"
|
||||
@@ -84,20 +71,6 @@
|
||||
"skills/mono/references/products/contact.md"
|
||||
]
|
||||
},
|
||||
"contact search": {
|
||||
"use_when": [
|
||||
"2. 按名字搜索同事或好友 — 可提取 同事的userId,或好友的openDingTalkId"
|
||||
],
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws contact user search --query \"张三\"",
|
||||
"dws contact user search --query \"张三\" --format json"
|
||||
],
|
||||
"source_refs": [
|
||||
"skills/mono/references/products/contact.md"
|
||||
]
|
||||
},
|
||||
"contact user dismission search": {
|
||||
"use_when": [
|
||||
"13. 查询全部离职员工",
|
||||
@@ -114,6 +87,19 @@
|
||||
"skills/mono/references/products/contact.md"
|
||||
]
|
||||
},
|
||||
"contact user get": {
|
||||
"use_when": [
|
||||
"查用户详情/部门/主管/管理员"
|
||||
],
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws contact user get --ids userId1,userId2"
|
||||
],
|
||||
"source_refs": [
|
||||
"skills/mono/references/products/contact.md"
|
||||
]
|
||||
},
|
||||
"contact user get-self": {
|
||||
"use_when": [
|
||||
"1. 查看自己的信息 — 提取 userId",
|
||||
@@ -157,6 +143,20 @@
|
||||
"skills/mono/references/products/contact.md"
|
||||
]
|
||||
},
|
||||
"contact user search": {
|
||||
"use_when": [
|
||||
"2. 按名字搜索同事或好友 — 可提取 同事的userId,或好友的openDingTalkId"
|
||||
],
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws contact user search --query \"张三\"",
|
||||
"dws contact user search --query \"张三\" --format json"
|
||||
],
|
||||
"source_refs": [
|
||||
"skills/mono/references/products/contact.md"
|
||||
]
|
||||
},
|
||||
"contact user search-mobile": {
|
||||
"examples": [
|
||||
"dws contact user search-mobile --mobile 13800138000"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:2a8994db5beb9389d5b0a2cf201e67c7160f3d23156996e5747b85bc8474ff74",
|
||||
"surface_hash": "sha256:2499ae17d61030fa3aec1d4adc2efd9d0644d93bc277c4616a8dc327fe893fae",
|
||||
"surface_hash": "sha256:bd65a90baa1568a7126eac505e3e1c7f943583333fb7ed6ef56413ca98d2c83f",
|
||||
"coverage": {
|
||||
"surface_products": 23,
|
||||
"products_with_metadata": 19,
|
||||
"surface_tools": 518,
|
||||
"surface_tools": 511,
|
||||
"tools_with_metadata": 304,
|
||||
"unmatched_skill_tools": 86
|
||||
},
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
{
|
||||
"product_id": "wiki",
|
||||
"tools": {
|
||||
"wiki doc list": {
|
||||
"use_when": [
|
||||
"2. 列出根目录节点",
|
||||
"3. 进入子目录",
|
||||
"知识库下的文件/知识库里有哪些文档/浏览知识库内容",
|
||||
"读某个知识库里的某篇文档"
|
||||
],
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws wiki node list --workspace \u003cworkspaceId\u003e --folder \u003cparentNodeId\u003e --format json",
|
||||
"dws wiki node list --workspace \u003cworkspaceId\u003e --format json"
|
||||
],
|
||||
"source_refs": [
|
||||
"skills/mono/references/products/wiki.md"
|
||||
]
|
||||
},
|
||||
"wiki member add": {
|
||||
"use_when": [
|
||||
"2. 添加成员",
|
||||
@@ -124,6 +107,23 @@
|
||||
"skills/mono/references/products/wiki.md"
|
||||
]
|
||||
},
|
||||
"wiki node list": {
|
||||
"use_when": [
|
||||
"2. 列出根目录节点",
|
||||
"3. 进入子目录",
|
||||
"知识库下的文件/知识库里有哪些文档/浏览知识库内容",
|
||||
"读某个知识库里的某篇文档"
|
||||
],
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws wiki node list --workspace \u003cworkspaceId\u003e --folder \u003cparentNodeId\u003e --format json",
|
||||
"dws wiki node list --workspace \u003cworkspaceId\u003e --format json"
|
||||
],
|
||||
"source_refs": [
|
||||
"skills/mono/references/products/wiki.md"
|
||||
]
|
||||
},
|
||||
"wiki node move": {
|
||||
"use_when": [
|
||||
"移动知识库里的文档",
|
||||
|
||||
@@ -754,24 +754,14 @@
|
||||
{
|
||||
"cli_path": "contact dept search"
|
||||
},
|
||||
{
|
||||
"cli_path": "contact get",
|
||||
"aliases": [
|
||||
"contact user get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cli_path": "contact relation list-my-followings"
|
||||
},
|
||||
{
|
||||
"cli_path": "contact search",
|
||||
"aliases": [
|
||||
"contact user list",
|
||||
"contact user search"
|
||||
]
|
||||
"cli_path": "contact user dismission search"
|
||||
},
|
||||
{
|
||||
"cli_path": "contact user dismission search"
|
||||
"cli_path": "contact user get"
|
||||
},
|
||||
{
|
||||
"cli_path": "contact user get-self"
|
||||
@@ -782,6 +772,9 @@
|
||||
{
|
||||
"cli_path": "contact user profile get"
|
||||
},
|
||||
{
|
||||
"cli_path": "contact user search"
|
||||
},
|
||||
{
|
||||
"cli_path": "contact user search-mobile"
|
||||
}
|
||||
@@ -901,34 +894,19 @@
|
||||
"id": "devdoc",
|
||||
"tools": [
|
||||
{
|
||||
"cli_path": "devdoc article legacy-search",
|
||||
"aliases": [
|
||||
"devdoc article-legacy-search"
|
||||
]
|
||||
"cli_path": "devdoc article legacy-search"
|
||||
},
|
||||
{
|
||||
"cli_path": "devdoc article legacy-search-open-platform",
|
||||
"aliases": [
|
||||
"devdoc article-legacy-search-open-platform"
|
||||
]
|
||||
"cli_path": "devdoc article legacy-search-open-platform"
|
||||
},
|
||||
{
|
||||
"cli_path": "devdoc article rag-pretest",
|
||||
"aliases": [
|
||||
"devdoc article-rag-pretest"
|
||||
]
|
||||
"cli_path": "devdoc article rag-pretest"
|
||||
},
|
||||
{
|
||||
"cli_path": "devdoc article search",
|
||||
"aliases": [
|
||||
"devdoc article-search"
|
||||
]
|
||||
"cli_path": "devdoc article search"
|
||||
},
|
||||
{
|
||||
"cli_path": "devdoc error diagnose",
|
||||
"aliases": [
|
||||
"devdoc error-diagnose"
|
||||
]
|
||||
"cli_path": "devdoc error diagnose"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1126,10 +1104,7 @@
|
||||
"id": "live",
|
||||
"tools": [
|
||||
{
|
||||
"cli_path": "live list",
|
||||
"aliases": [
|
||||
"live stream list"
|
||||
]
|
||||
"cli_path": "live stream list"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1199,15 +1174,11 @@
|
||||
{
|
||||
"cli_path": "mail message search",
|
||||
"aliases": [
|
||||
"mail message list",
|
||||
"mail search"
|
||||
"mail message list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cli_path": "mail message send",
|
||||
"aliases": [
|
||||
"mail send"
|
||||
]
|
||||
"cli_path": "mail message send"
|
||||
},
|
||||
{
|
||||
"cli_path": "mail tag list"
|
||||
@@ -1398,8 +1369,7 @@
|
||||
{
|
||||
"cli_path": "report entry get",
|
||||
"aliases": [
|
||||
"report detail",
|
||||
"report get"
|
||||
"report detail"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1411,14 +1381,12 @@
|
||||
{
|
||||
"cli_path": "report entry submit",
|
||||
"aliases": [
|
||||
"report create",
|
||||
"report submit"
|
||||
"report create"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cli_path": "report inbox list",
|
||||
"aliases": [
|
||||
"report inbox-list",
|
||||
"report list"
|
||||
]
|
||||
},
|
||||
@@ -1426,22 +1394,17 @@
|
||||
"cli_path": "report outbox list",
|
||||
"aliases": [
|
||||
"report created",
|
||||
"report outbox-list",
|
||||
"report sent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cli_path": "report template get",
|
||||
"aliases": [
|
||||
"report template detail",
|
||||
"report template-get"
|
||||
"report template detail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cli_path": "report template list",
|
||||
"aliases": [
|
||||
"report template-list"
|
||||
]
|
||||
"cli_path": "report template list"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1544,9 +1507,6 @@
|
||||
{
|
||||
"cli_path": "sheet find"
|
||||
},
|
||||
{
|
||||
"cli_path": "sheet get"
|
||||
},
|
||||
{
|
||||
"cli_path": "sheet get-dropdown"
|
||||
},
|
||||
@@ -1604,9 +1564,6 @@
|
||||
{
|
||||
"cli_path": "sheet range update"
|
||||
},
|
||||
{
|
||||
"cli_path": "sheet read"
|
||||
},
|
||||
{
|
||||
"cli_path": "sheet replace"
|
||||
},
|
||||
@@ -1655,10 +1612,7 @@
|
||||
"cli_path": "todo task add-reminder"
|
||||
},
|
||||
{
|
||||
"cli_path": "todo task create",
|
||||
"aliases": [
|
||||
"todo create"
|
||||
]
|
||||
"cli_path": "todo task create"
|
||||
},
|
||||
{
|
||||
"cli_path": "todo task create-sub"
|
||||
@@ -1667,19 +1621,13 @@
|
||||
"cli_path": "todo task delete"
|
||||
},
|
||||
{
|
||||
"cli_path": "todo task done",
|
||||
"aliases": [
|
||||
"todo done"
|
||||
]
|
||||
"cli_path": "todo task done"
|
||||
},
|
||||
{
|
||||
"cli_path": "todo task get"
|
||||
},
|
||||
{
|
||||
"cli_path": "todo task list",
|
||||
"aliases": [
|
||||
"todo list"
|
||||
]
|
||||
"cli_path": "todo task list"
|
||||
},
|
||||
{
|
||||
"cli_path": "todo task remove-executor"
|
||||
@@ -1698,24 +1646,6 @@
|
||||
{
|
||||
"id": "wiki",
|
||||
"tools": [
|
||||
{
|
||||
"cli_path": "wiki doc list",
|
||||
"aliases": [
|
||||
"wiki node list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki doc read"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki doc search",
|
||||
"aliases": [
|
||||
"wiki file search"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki file list"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki member add"
|
||||
},
|
||||
@@ -1738,46 +1668,28 @@
|
||||
"cli_path": "wiki node delete"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki node info"
|
||||
"cli_path": "wiki node list"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki node move"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki node read"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki node search"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki space create",
|
||||
"aliases": [
|
||||
"wiki create"
|
||||
]
|
||||
"cli_path": "wiki space create"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki space delete",
|
||||
"aliases": [
|
||||
"wiki delete"
|
||||
]
|
||||
"cli_path": "wiki space delete"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki space get",
|
||||
"aliases": [
|
||||
"wiki get"
|
||||
]
|
||||
"cli_path": "wiki space get"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki space list",
|
||||
"aliases": [
|
||||
"wiki list"
|
||||
]
|
||||
"cli_path": "wiki space list"
|
||||
},
|
||||
{
|
||||
"cli_path": "wiki space search",
|
||||
"aliases": [
|
||||
"wiki search"
|
||||
]
|
||||
"cli_path": "wiki space search"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -47,7 +47,10 @@ func init() {
|
||||
"contact search": "search_contact_by_key_word",
|
||||
"contact user list": "search_contact_by_key_word",
|
||||
},
|
||||
nil,
|
||||
map[string]string{
|
||||
"get_user_info_by_user_ids": "contact user get",
|
||||
"search_contact_by_key_word": "contact user search",
|
||||
},
|
||||
))
|
||||
|
||||
RegisterRuntimeSchemaRoot("devdoc", newRuntimeAliasRoot("hardcoded:devdoc",
|
||||
|
||||
@@ -222,6 +222,7 @@ func BuildDynamicCommands(servers []market.ServerDescriptor, runner executor.Run
|
||||
route.OutputTransform = buildOutputTransform(override.OutputFormat)
|
||||
|
||||
cmd := NewDirectCommand(route, runner)
|
||||
annotateRuntimePositionals(cmd, bindings)
|
||||
|
||||
// Enrich flags with typed parameters from Detail API toolRequest JSON Schema.
|
||||
if dt, ok := detailIndex[toolName]; ok {
|
||||
@@ -994,6 +995,7 @@ func buildRedirectCommand(name, description, target string) *cobra.Command {
|
||||
Use: name,
|
||||
Short: short,
|
||||
Long: fmt.Sprintf("This command has moved. Please use: %s", target),
|
||||
Hidden: true,
|
||||
DisableFlagParsing: true,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -1001,6 +1003,7 @@ func buildRedirectCommand(name, description, target string) *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
schemacli.ExcludeFromRuntimeSchema(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1028,6 +1031,7 @@ func buildHintCommand(name string, def market.CLIHintDef) *cobra.Command {
|
||||
Use: name,
|
||||
Short: short,
|
||||
Long: short,
|
||||
Hidden: true,
|
||||
DisableFlagParsing: true,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -1039,6 +1043,7 @@ func buildHintCommand(name string, def market.CLIHintDef) *cobra.Command {
|
||||
return apperrors.NewValidation(fmt.Sprintf("use: %s", target))
|
||||
},
|
||||
}
|
||||
schemacli.ExcludeFromRuntimeSchema(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1062,12 +1067,14 @@ func applyFlagConstraints(cmd *cobra.Command, override market.CLIToolOverride) {
|
||||
return valid
|
||||
}
|
||||
|
||||
constraints := schemacli.RuntimeSchemaConstraints{}
|
||||
for _, group := range override.MutuallyExclusive {
|
||||
names := validate(group)
|
||||
if len(names) < 2 {
|
||||
continue
|
||||
}
|
||||
cmd.MarkFlagsMutuallyExclusive(names...)
|
||||
constraints.MutuallyExclusive = append(constraints.MutuallyExclusive, names)
|
||||
}
|
||||
for _, group := range override.RequireOneOf {
|
||||
names := validate(group)
|
||||
@@ -1075,6 +1082,55 @@ func applyFlagConstraints(cmd *cobra.Command, override market.CLIToolOverride) {
|
||||
continue
|
||||
}
|
||||
cmd.MarkFlagsOneRequired(names...)
|
||||
constraints.RequireOneOf = append(constraints.RequireOneOf, names)
|
||||
}
|
||||
for _, group := range override.RequireTogether {
|
||||
names := validate(group)
|
||||
if len(names) < 2 {
|
||||
continue
|
||||
}
|
||||
constraints.RequireTogether = append(constraints.RequireTogether, names)
|
||||
}
|
||||
schemacli.AnnotateRuntimeConstraints(cmd, constraints)
|
||||
}
|
||||
|
||||
func annotateRuntimePositionals(cmd *cobra.Command, bindings []FlagBinding) {
|
||||
positionals := make([]schemacli.RuntimeSchemaPositional, 0)
|
||||
for _, binding := range bindings {
|
||||
if !binding.Positional {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(binding.Property)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(binding.FlagName)
|
||||
}
|
||||
hasFlagAlias := strings.TrimSpace(binding.Alias) != "" ||
|
||||
strings.TrimSpace(binding.FlagName) != "" || len(binding.Aliases) > 0
|
||||
positionals = append(positionals, schemacli.RuntimeSchemaPositional{
|
||||
Name: name,
|
||||
Type: runtimeSchemaBindingType(binding.Kind),
|
||||
Description: strings.TrimSpace(binding.Usage),
|
||||
Required: binding.Required || !hasFlagAlias,
|
||||
Index: binding.PositionalIndex,
|
||||
})
|
||||
}
|
||||
schemacli.AnnotateRuntimePositionals(cmd, positionals...)
|
||||
}
|
||||
|
||||
func runtimeSchemaBindingType(kind ValueKind) string {
|
||||
switch kind {
|
||||
case ValueInt:
|
||||
return "integer"
|
||||
case ValueFloat:
|
||||
return "number"
|
||||
case ValueBool:
|
||||
return "boolean"
|
||||
case ValueStringSlice, ValueIntSlice, ValueFloatSlice, ValueBoolSlice:
|
||||
return "array"
|
||||
case ValueJSON:
|
||||
return "object"
|
||||
default:
|
||||
return "string"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -979,6 +979,9 @@ func TestBuildDynamicCommands_RedirectTo(t *testing.T) {
|
||||
if history == nil {
|
||||
t.Fatal("history stub not found")
|
||||
}
|
||||
if !history.Hidden {
|
||||
t.Fatal("redirect stub must be hidden from command help")
|
||||
}
|
||||
out := &strings.Builder{}
|
||||
history.SetOut(out)
|
||||
history.SetErr(out)
|
||||
@@ -1037,6 +1040,9 @@ func TestBuildDynamicCommands_Hints(t *testing.T) {
|
||||
if history == nil {
|
||||
t.Fatal("hint 'history' missing under chat root")
|
||||
}
|
||||
if !history.Hidden {
|
||||
t.Fatal("hint command must be hidden from command help")
|
||||
}
|
||||
if history.Short != "migrated to `message list`" {
|
||||
t.Fatalf("hint description not applied, got %q", history.Short)
|
||||
}
|
||||
|
||||
@@ -324,6 +324,8 @@ func newAttendanceSummaryCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("user", "", "钉钉用户 ID(必填)")
|
||||
cmd.Flags().String("date", "", "工作日期,格式 yyyy-MM-dd HH:mm:ss,如 2026-03-12 15:00:00(必填)")
|
||||
cmd.Flags().String("stats-type", "", "统计类型:week(周统计)或 month(月统计)(必填,钉钉服务端业务层强制要求)")
|
||||
annotateFlagExample(cmd, "date", "2026-03-12 15:00:00")
|
||||
annotateFlagEnum(cmd, "stats-type", "week", "month")
|
||||
preferLegacyLeaf(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -555,6 +555,11 @@ func newChatMessageSendByBotCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("text", "", "消息内容 Markdown (必填)")
|
||||
cmd.Flags().String("title", "", "消息标题 (必填)")
|
||||
cmd.Flags().String("users", "", "接收者 userId 列表,逗号分隔,最多 20 个 (单聊必填)")
|
||||
annotateFlagConstraints(cmd,
|
||||
[][]string{{"group", "users"}},
|
||||
[][]string{{"group", "users"}},
|
||||
nil,
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -595,11 +600,14 @@ func newChatGroupCreateCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
threadEnabled, _ := cmd.Flags().GetBool("thread")
|
||||
|
||||
currentUserID, err := getCurrentUserID(cmd.Context(), runner)
|
||||
if err != nil {
|
||||
return err
|
||||
allMembers := memberUserIDs
|
||||
if !commandDryRun(cmd) {
|
||||
currentUserID, err := getCurrentUserID(cmd.Context(), runner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allMembers = prependOwner(currentUserID, memberUserIDs)
|
||||
}
|
||||
allMembers := prependOwner(currentUserID, memberUserIDs)
|
||||
|
||||
// create_group_conversation (multi-type + thread support) and the
|
||||
// legacy create_internal_group live on two different MCP servers
|
||||
|
||||
@@ -174,6 +174,18 @@ func newChatFileUploadCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("file-name", "", "文件名(可选)")
|
||||
cmd.Flags().String("md5", "", "文件 MD5(可选,本地不传自动计算)")
|
||||
cmd.Flags().String("uuid", "", "幂等 UUID(可选)")
|
||||
annotateFlagFormat(cmd, "file", "file-path")
|
||||
annotateFlagConstraints(cmd,
|
||||
[][]string{
|
||||
{"group", "user", "open-dingtalk-id"},
|
||||
{"file", "url"},
|
||||
},
|
||||
[][]string{
|
||||
{"group", "user", "open-dingtalk-id"},
|
||||
{"file", "url"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,30 @@ func preferLegacyLeaf(cmd *cobra.Command) {
|
||||
cli.SetOverridePriority(cmd, 100)
|
||||
}
|
||||
|
||||
func annotateRequiredFlags(cmd *cobra.Command, names ...string) {
|
||||
cli.AnnotateRuntimeRequiredFlags(cmd, names...)
|
||||
}
|
||||
|
||||
func annotateFlagFormat(cmd *cobra.Command, name, format string) {
|
||||
cli.AnnotateRuntimeFlagFormat(cmd, name, format)
|
||||
}
|
||||
|
||||
func annotateFlagEnum(cmd *cobra.Command, name string, values ...string) {
|
||||
cli.AnnotateRuntimeFlagEnum(cmd, name, values...)
|
||||
}
|
||||
|
||||
func annotateFlagExample(cmd *cobra.Command, name, example string) {
|
||||
cli.AnnotateRuntimeFlagExample(cmd, name, example)
|
||||
}
|
||||
|
||||
func annotateFlagConstraints(cmd *cobra.Command, mutuallyExclusive, requireOneOf, requireTogether [][]string) {
|
||||
cli.AnnotateRuntimeConstraints(cmd, cli.RuntimeSchemaConstraints{
|
||||
MutuallyExclusive: mutuallyExclusive,
|
||||
RequireOneOf: requireOneOf,
|
||||
RequireTogether: requireTogether,
|
||||
})
|
||||
}
|
||||
|
||||
// resolveStringFlag reads a string flag, resolves @file/@- input sources,
|
||||
// and falls back to stdin pipe when the flag is the designated primary
|
||||
// content flag and the user did not provide an explicit value.
|
||||
|
||||
@@ -497,6 +497,7 @@ func newDevAppRobotConnectStatusCommand() *cobra.Command {
|
||||
preferLegacyLeaf(cmd)
|
||||
cmd.Flags().String("robot-client-id", "", "机器人 clientId(定位守护进程)")
|
||||
cmd.Flags().String("unified-app-id", "", "统一应用 ID(当未用 clientId 起守护进程时定位)")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"robot-client-id", "unified-app-id"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -520,6 +521,7 @@ func newDevAppRobotConnectStopCommand() *cobra.Command {
|
||||
preferLegacyLeaf(cmd)
|
||||
cmd.Flags().String("robot-client-id", "", "机器人 clientId(定位守护进程)")
|
||||
cmd.Flags().String("unified-app-id", "", "统一应用 ID(当未用 clientId 起守护进程时定位)")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"robot-client-id", "unified-app-id"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,7 @@ func newDevAppEventSubscribeCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
addDevAppUnifiedIDFlag(cmd)
|
||||
cmd.Flags().String("event-codes", "", "事件码,多个用逗号或分号分隔")
|
||||
annotateRequiredFlags(cmd, "event-codes")
|
||||
preferLegacyLeaf(cmd)
|
||||
annotateDevAppTool(cmd, devAppEventSubscribeTool)
|
||||
return cmd
|
||||
@@ -328,6 +329,7 @@ func newDevAppEventUnsubscribeCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
addDevAppUnifiedIDFlag(cmd)
|
||||
cmd.Flags().String("event-codes", "", "事件码,多个用逗号或分号分隔")
|
||||
annotateRequiredFlags(cmd, "event-codes")
|
||||
preferLegacyLeaf(cmd)
|
||||
annotateDevAppTool(cmd, devAppEventUnsubscribeTool)
|
||||
return cmd
|
||||
@@ -738,6 +740,7 @@ func newDevAppPermissionAddCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
addDevAppUnifiedIDFlag(cmd)
|
||||
cmd.Flags().String("scope-values", "", "权限点 scopeValue,多个用逗号或分号分隔")
|
||||
annotateRequiredFlags(cmd, "scope-values")
|
||||
preferLegacyLeaf(cmd)
|
||||
annotateDevAppTool(cmd, devAppPermissionAddTool)
|
||||
return cmd
|
||||
@@ -769,6 +772,7 @@ func newDevAppPermissionRemoveCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
addDevAppUnifiedIDFlag(cmd)
|
||||
cmd.Flags().String("scope-values", "", "待取消权限点 scopeValue,多个用逗号或分号分隔")
|
||||
annotateRequiredFlags(cmd, "scope-values")
|
||||
preferLegacyLeaf(cmd)
|
||||
annotateDevAppTool(cmd, devAppPermissionRmTool)
|
||||
return cmd
|
||||
|
||||
@@ -210,6 +210,9 @@ func newDevdocErrorDiagnoseCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().Int("page", 1, "分页页码 (从 1 开始,默认 1)")
|
||||
cmd.Flags().String("cursor", "", "分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page")
|
||||
cmd.Flags().Int("size", 10, "分页大小 (默认 10)")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{
|
||||
"query", "request-id", "error-code", "error-message", "context",
|
||||
}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -577,6 +577,13 @@ func newDocUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("content-format", "", i18n.T("内容格式: markdown / jsonml"))
|
||||
cmd.Flags().Int("revision", 0, i18n.T("文档编辑版本号(JSONML 场景透传)"))
|
||||
addDocJSONMLControlFlags(cmd)
|
||||
annotateRequiredFlags(cmd, "mode")
|
||||
annotateFlagEnum(cmd, "mode", "append", "overwrite")
|
||||
annotateFlagConstraints(cmd,
|
||||
[][]string{{"content", "content-file"}},
|
||||
[][]string{{"content", "content-file"}},
|
||||
nil,
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1059,6 +1066,7 @@ func newDocBlockInsertCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("content-format", "", i18n.T("输入格式: element / jsonml"))
|
||||
cmd.Flags().String("parent-block", "", i18n.T("父容器块 ID(JSONML 场景透传)"))
|
||||
addDocJSONMLControlFlags(cmd)
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"text", "heading", "element", "type"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1122,6 +1130,7 @@ func newDocBlockUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().Int("level", 1, i18n.T("标题级别"))
|
||||
cmd.Flags().String("content-format", "", i18n.T("输入格式: element / jsonml"))
|
||||
addDocJSONMLControlFlags(cmd)
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"text", "heading", "element", "type"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1534,6 +1543,7 @@ func addDocNodeFlags(cmd *cobra.Command) {
|
||||
addDocHiddenStringFlag(cmd, "node-id", "--node alias")
|
||||
addDocHiddenStringFlag(cmd, "doc-id", "--node alias")
|
||||
addDocHiddenStringFlag(cmd, "file-id", "--node alias")
|
||||
annotateRequiredFlags(cmd, "node")
|
||||
}
|
||||
|
||||
func addDocJSONMLControlFlags(cmd *cobra.Command) {
|
||||
@@ -2014,6 +2024,7 @@ func newDocExportGetCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("job-id", "", i18n.T("导出任务 ID (必填)"))
|
||||
cmd.Flags().String("output", "", i18n.T("本地落盘路径(可选,提供则自动下载 docx 到本地)"))
|
||||
cmd.Flags().Int("timeout-sec", 300, i18n.T("整体轮询超时(秒),默认 300"))
|
||||
annotateFlagFormat(cmd, "job-id", "numeric-id")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -2151,6 +2162,7 @@ func newDocPermissionAddCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("user", "", i18n.T("被授权用户 userId 列表,逗号分隔,单次最多 30 (必填)"))
|
||||
addDocHiddenStringFlag(cmd, "users", "--user alias")
|
||||
cmd.Flags().String("role", "", i18n.T("权限角色: MANAGER / EDITOR / DOWNLOADER / READER (必填,大小写不敏感)"))
|
||||
annotateFlagEnum(cmd, "role", "MANAGER", "EDITOR", "DOWNLOADER", "READER")
|
||||
cmd.Flags().String("workspace", "", i18n.T("目标知识库 ID 或 URL(选填,辅助构造返回的 docUrl)"))
|
||||
addDocHiddenStringFlag(cmd, "workspace-id", "--workspace alias")
|
||||
return cmd
|
||||
@@ -2180,6 +2192,7 @@ func newDocPermissionUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
addDocHiddenStringFlag(cmd, "users", "--user alias")
|
||||
addDocHiddenStringFlag(cmd, "uid", "--user alias")
|
||||
cmd.Flags().String("role", "", i18n.T("新权限角色: MANAGER / EDITOR / DOWNLOADER / READER (必填)"))
|
||||
annotateFlagEnum(cmd, "role", "MANAGER", "EDITOR", "DOWNLOADER", "READER")
|
||||
cmd.Flags().String("workspace", "", i18n.T("目标知识库 ID 或 URL(选填)"))
|
||||
addDocHiddenStringFlag(cmd, "workspace-id", "--workspace alias")
|
||||
return cmd
|
||||
|
||||
@@ -847,6 +847,7 @@ func newDrivePermissionMutationCommand(runner executor.Runner, use, tool string,
|
||||
addDriveHiddenStringFlag(cmd, "uid", "--users 的兼容别名")
|
||||
if requireRole {
|
||||
cmd.Flags().String("role", "", "权限角色: MANAGER / EDITOR / DOWNLOADER / READER (必填)")
|
||||
annotateFlagEnum(cmd, "role", "MANAGER", "EDITOR", "DOWNLOADER", "READER")
|
||||
}
|
||||
cmd.Flags().String("workspace", "", "知识库 ID (选填)")
|
||||
addDriveHiddenStringFlag(cmd, "workspace-id", "--workspace 的兼容别名")
|
||||
|
||||
@@ -257,6 +257,9 @@ func newSheetUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().Int("frozen-row-count", 0, "冻结行数")
|
||||
cmd.Flags().Int("frozen-column-count", 0, "冻结列数")
|
||||
cmd.Flags().String("tab-color", "", "工作表标签颜色,Hex 格式如 #FF0000;传空字符串清除颜色")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{
|
||||
"name", "index", "hidden", "frozen-row-count", "frozen-column-count", "tab-color",
|
||||
}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -617,6 +620,8 @@ func newSheetDimensionPositionCommand(runner executor.Runner, use, short, tool s
|
||||
cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)")
|
||||
cmd.Flags().String("position", "", "位置,A1 表示法 (必填)")
|
||||
cmd.Flags().String("length", "", "数量,正整数 (必填)")
|
||||
annotateFlagEnum(cmd, "dimension", "ROWS", "COLUMNS")
|
||||
annotateFlagFormat(cmd, "length", "positive-integer")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -664,6 +669,9 @@ func newSheetUpdateDimensionCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("length", "", "数量,正整数 (必填)")
|
||||
cmd.Flags().Bool("hidden", false, "是否隐藏")
|
||||
cmd.Flags().Int("pixel-size", 0, "行高或列宽")
|
||||
annotateFlagEnum(cmd, "dimension", "ROWS", "COLUMNS")
|
||||
annotateFlagFormat(cmd, "length", "positive-integer")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"hidden", "pixel-size"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -690,6 +698,7 @@ func newSheetMoveDimensionCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("start-index", "", "源起始位置 (必填)")
|
||||
cmd.Flags().String("end-index", "", "源结束位置 (必填)")
|
||||
cmd.Flags().String("destination-index", "", "目标位置 (必填)")
|
||||
annotateFlagEnum(cmd, "dimension", "ROWS", "COLUMNS")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -716,6 +725,7 @@ func newSheetAddDimensionCommand(runner executor.Runner) *cobra.Command {
|
||||
addSheetBaseFlags(cmd)
|
||||
cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)")
|
||||
cmd.Flags().Int("length", 0, "追加数量,正整数 (必填)")
|
||||
annotateFlagEnum(cmd, "dimension", "ROWS", "COLUMNS")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -774,6 +784,7 @@ func newSheetSetDropdownCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("range", "", "目标单元格范围 (必填)")
|
||||
cmd.Flags().String("options", "", "下拉选项 JSON 数组 (必填)")
|
||||
cmd.Flags().Bool("multi-select", false, "是否允许多选")
|
||||
annotateFlagExample(cmd, "options", `[{"value":"SchemaSmoke"}]`)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -960,6 +971,7 @@ func newSheetFilterViewUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("name", "", "筛选视图新名称")
|
||||
cmd.Flags().String("range", "", "筛选视图新范围")
|
||||
cmd.Flags().String("criteria", "", "筛选条件 JSON 数组")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"name", "range", "criteria"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1031,6 +1043,9 @@ func newSheetFilterViewReadCommand(runner executor.Runner, use, short, mode stri
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if commandDryRun(cmd) {
|
||||
return writeCommandPayload(cmd, result)
|
||||
}
|
||||
filterViews := sheetResultFilterViews(result.Response)
|
||||
// 不再对空列表提前返回成功:请求了具体 filter-view-id 却找不到(含一个都没有)
|
||||
// 应报"未找到"错误,而非静默成功(与 wukong 对齐)。
|
||||
@@ -1095,6 +1110,7 @@ func newSheetCondFormatCreateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("condition", "", "条件类型及参数 JSON 对象 (必填)")
|
||||
cmd.Flags().String("cell-style", "", "单元格样式 JSON 对象")
|
||||
cmd.Flags().String("data-bar-style", "", "数据条样式 JSON 对象")
|
||||
annotateFlagExample(cmd, "ranges", `["A1:B2"]`)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1115,6 +1131,10 @@ func newSheetCondFormatUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("condition", "", "条件类型及参数 JSON 对象")
|
||||
cmd.Flags().String("cell-style", "", "单元格样式 JSON 对象")
|
||||
cmd.Flags().String("data-bar-style", "", "数据条样式 JSON 对象")
|
||||
annotateFlagExample(cmd, "ranges", `["A1:B2"]`)
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{
|
||||
"ranges", "condition", "cell-style", "data-bar-style",
|
||||
}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1217,6 +1237,9 @@ func newSheetUpdateFloatImageCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().Int("height", 0, "新的图片高度")
|
||||
cmd.Flags().Int("offset-x", 0, "新的水平偏移量")
|
||||
cmd.Flags().Int("offset-y", 0, "新的垂直偏移量")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{
|
||||
"src", "range", "width", "height", "offset-x", "offset-y",
|
||||
}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1410,21 +1433,25 @@ func addSheetNodeFlags(cmd *cobra.Command) {
|
||||
addSheetHiddenStringFlag(cmd, "node-id", "--node alias")
|
||||
addSheetHiddenStringFlag(cmd, "doc-id", "--node alias")
|
||||
addSheetHiddenStringFlag(cmd, "file-id", "--node alias")
|
||||
annotateRequiredFlags(cmd, "node")
|
||||
}
|
||||
|
||||
func addSheetBaseFlags(cmd *cobra.Command) {
|
||||
addSheetNodeFlags(cmd)
|
||||
cmd.Flags().String("sheet-id", "", "工作表 ID 或名称")
|
||||
annotateRequiredFlags(cmd, "sheet-id")
|
||||
}
|
||||
|
||||
func addSheetFilterViewBaseFlags(cmd *cobra.Command) {
|
||||
addSheetBaseFlags(cmd)
|
||||
cmd.Flags().String("filter-view-id", "", "筛选视图 ID")
|
||||
annotateRequiredFlags(cmd, "filter-view-id")
|
||||
}
|
||||
|
||||
func addSheetFloatImageBaseFlags(cmd *cobra.Command) {
|
||||
addSheetBaseFlags(cmd)
|
||||
cmd.Flags().String("float-image-id", "", "浮动图片 ID")
|
||||
annotateRequiredFlags(cmd, "float-image-id")
|
||||
}
|
||||
|
||||
func addSheetRangeTransferFlags(cmd *cobra.Command) {
|
||||
@@ -1963,6 +1990,8 @@ func newSheetRangeSetStyleCommand(runner executor.Runner) *cobra.Command {
|
||||
addSheetBaseFlags(cmd)
|
||||
cmd.Flags().String("range", "", "目标单元格区域地址 (必填)")
|
||||
bindSheetStyleFlags(cmd)
|
||||
annotateFlagFormat(cmd, "range", "a1-range")
|
||||
annotateSheetStyleConstraints(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -2034,9 +2063,30 @@ func newSheetRangeBatchSetStyleCommand(runner executor.Runner) *cobra.Command {
|
||||
addSheetNodeFlags(cmd)
|
||||
cmd.Flags().String("batch", "", "批次配置 JSON 文件路径 (必填)")
|
||||
cmd.Flags().Bool("continue-on-error", false, "遇到失败时继续执行")
|
||||
annotateFlagFormat(cmd, "batch", "file-path")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func annotateSheetStyleConstraints(cmd *cobra.Command) {
|
||||
annotateFlagConstraints(cmd,
|
||||
[][]string{
|
||||
{"bg-color", "bg-colors-json"},
|
||||
{"font-size", "font-sizes-json"},
|
||||
{"h-align", "h-aligns-json"},
|
||||
{"v-align", "v-aligns-json"},
|
||||
{"font-color", "font-colors-json"},
|
||||
{"font-weight", "font-weights-json"},
|
||||
},
|
||||
[][]string{{
|
||||
"bg-color", "bg-colors-json", "font-size", "font-sizes-json",
|
||||
"h-align", "h-aligns-json", "v-align", "v-aligns-json",
|
||||
"font-color", "font-colors-json", "font-weight", "font-weights-json",
|
||||
"word-wrap", "number-format",
|
||||
}},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func bindSheetStyleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String("bg-color", "", "背景色")
|
||||
cmd.Flags().String("bg-colors-json", "", "背景色二维 JSON 数组")
|
||||
|
||||
@@ -226,6 +226,8 @@ func newWikiSpaceSearchCommand(runner executor.Runner) *cobra.Command {
|
||||
addWikiHiddenStringFlag(cmd, "keyword", "--query 的兼容别名")
|
||||
cmd.Flags().String("type", "", "知识库类型;仅 --type myWikiSpace 支持无 keyword 查询个人知识库")
|
||||
cmd.Flags().String("limit", "", "返回数量 1-20 (默认 10)")
|
||||
annotateFlagEnum(cmd, "type", "myWikiSpace")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"query", "type"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -54,12 +55,16 @@ func addWikiProxyCommands(root *cobra.Command, runner executor.Runner) {
|
||||
)
|
||||
|
||||
file := newWikiProxyGroup("file", "知识库文件兼容入口")
|
||||
file.Hidden = true
|
||||
cli.ExcludeFromRuntimeSchema(file)
|
||||
file.AddCommand(
|
||||
newWikiProxyLeaf(runner, "list", wikiProxyTargetDoc, []string{"list"}, wikiProxyOptions{}),
|
||||
newWikiProxyLeaf(runner, "search", wikiProxyTargetDoc, []string{"search"}, wikiProxyOptions{workspaceToWorkspaceIDs: true}),
|
||||
)
|
||||
|
||||
doc := newWikiProxyGroup("doc", "知识库文档兼容入口")
|
||||
doc.Hidden = true
|
||||
cli.ExcludeFromRuntimeSchema(doc)
|
||||
doc.AddCommand(
|
||||
newWikiProxyLeaf(runner, "list", wikiProxyTargetDoc, []string{"list"}, wikiProxyOptions{}),
|
||||
newWikiProxyLeaf(runner, "read", wikiProxyTargetDoc, []string{"read"}, wikiProxyOptions{}),
|
||||
@@ -88,6 +93,7 @@ func newWikiProxyLeaf(runner executor.Runner, use string, target wikiProxyTarget
|
||||
cmd := &cobra.Command{
|
||||
Use: use,
|
||||
Short: "兼容入口,透明转发到新命令",
|
||||
Hidden: true,
|
||||
DisableFlagParsing: true,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -99,6 +105,7 @@ func newWikiProxyLeaf(runner executor.Runner, use string, target wikiProxyTarget
|
||||
},
|
||||
}
|
||||
preferLegacyLeaf(cmd)
|
||||
cli.ExcludeFromRuntimeSchema(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -187,4 +194,7 @@ func configureWikiProxyTargetRoot(source, root *cobra.Command) {
|
||||
if root.PersistentFlags().Lookup("dry-run") == nil {
|
||||
root.PersistentFlags().Bool("dry-run", false, "仅打印即将发送的请求,不真正执行")
|
||||
}
|
||||
if root.PersistentFlags().Lookup("yes") == nil {
|
||||
root.PersistentFlags().BoolP("yes", "y", false, "跳过确认提示 (AI Agent 模式)")
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,30 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWikiCompatibilityProxiesAreHidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := wikiHandler{}.Command(&wikiCommandRunner{})
|
||||
for _, name := range []string{"list", "search", "create", "get", "delete"} {
|
||||
proxy, _, err := root.Find([]string{name})
|
||||
if err != nil || proxy == nil || proxy.Name() != name {
|
||||
t.Fatalf("proxy %q not found: command=%v error=%v", name, proxy, err)
|
||||
}
|
||||
if !proxy.Hidden {
|
||||
t.Fatalf("proxy %q must be hidden from command help", name)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"file", "doc"} {
|
||||
group, _, err := root.Find([]string{name})
|
||||
if err != nil || group == nil || group.Name() != name {
|
||||
t.Fatalf("proxy group %q not found: command=%v error=%v", name, group, err)
|
||||
}
|
||||
if !group.Hidden {
|
||||
t.Fatalf("proxy group %q must be hidden from command help", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiTopLevelListProxyExecutesSpaceList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
@@ -224,5 +225,6 @@ func newBrowserPolicyCommand() *cobra.Command {
|
||||
|
||||
cmd.Flags().Bool("enabled", false, "PAT 撞墙时是否允许本地打开浏览器")
|
||||
cmd.Flags().String("agentCode", "", "Agent 唯一标识(可选;不填则写入全局默认策略,不从 env DINGTALK_DWS_AGENTCODE 回退)")
|
||||
cli.AnnotateRuntimeRequiredFlags(cmd, "enabled")
|
||||
return cmd
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
)
|
||||
@@ -412,6 +413,18 @@ agentCode 配置:
|
||||
chmodCmd.Flags().StringSliceVar(&domainsFlag, "domains", nil, "产品域/产品编码列表,逗号分隔;执行批量授权需 --yes")
|
||||
chmodCmd.Flags().BoolVar(&recommend, "recommend", false, "使用推荐 scope 集合批量授权;执行授权需 --yes")
|
||||
|
||||
cli.AnnotateRuntimeFlagEnum(chmodCmd, "grant-type", "once", "session", "permanent")
|
||||
cli.AnnotateRuntimePositionals(chmodCmd, cli.RuntimeSchemaPositional{
|
||||
Name: "scope",
|
||||
Type: "array",
|
||||
Description: "权限 scope,格式为 <product>.<entity>:<permission>;可重复",
|
||||
Variadic: true,
|
||||
Index: 0,
|
||||
})
|
||||
cli.AnnotateRuntimeConstraints(chmodCmd, cli.RuntimeSchemaConstraints{
|
||||
RequireOneOf: [][]string{{"scope", "product", "products", "domain", "domains", "recommend"}},
|
||||
})
|
||||
|
||||
return chmodCmd
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run every `dws schema` leaf command in dry-run mode.
|
||||
"""Run every executable `dws schema --all` leaf in dry-run mode.
|
||||
|
||||
The script treats `dws schema --format json` as the command inventory, expands
|
||||
The script treats `dws schema --all --format json` as the command inventory, expands
|
||||
each leaf schema, synthesizes flag values from parameter metadata, and executes
|
||||
the runnable CLI path with `--dry-run --yes --format json`.
|
||||
"""
|
||||
@@ -74,6 +74,21 @@ def kebab_to_words(value: str) -> str:
|
||||
|
||||
|
||||
def scalar_value(flag: str, param: dict[str, Any], canonical_path: str) -> str:
|
||||
enum = param.get("enum") or []
|
||||
if enum:
|
||||
return str(enum[0])
|
||||
|
||||
if param.get("example") not in (None, ""):
|
||||
return str(param["example"])
|
||||
|
||||
value_format = str(param.get("format", "")).lower()
|
||||
if value_format in {"numeric-id", "positive-integer"}:
|
||||
return "1"
|
||||
if value_format == "a1-range":
|
||||
return "A1:B2"
|
||||
if value_format == "file-path":
|
||||
return str(Path(tempfile.gettempdir()) / "dws-schema-smoke-fixture.txt")
|
||||
|
||||
if "default" in param and param["default"] not in (None, ""):
|
||||
return str(param["default"])
|
||||
|
||||
@@ -106,6 +121,8 @@ def scalar_value(flag: str, param: dict[str, Any], canonical_path: str) -> str:
|
||||
return "leave"
|
||||
if canonical_path == "attendance.save_self_setting" and flag == "setting-scene":
|
||||
return "checkResultNotify"
|
||||
if canonical_path == "attendance.get_attendance_summary" and flag == "date":
|
||||
return "2026-07-09 10:00:00"
|
||||
if "json" in haystack:
|
||||
if "array" in haystack or "数组" in haystack:
|
||||
return "[]"
|
||||
@@ -160,6 +177,11 @@ def scalar_value(flag: str, param: dict[str, Any], canonical_path: str) -> str:
|
||||
|
||||
|
||||
def value_for(flag: str, param: dict[str, Any], canonical_path: str) -> str | None:
|
||||
if canonical_path == "pat.batch_grant" and flag == "scope":
|
||||
return "aitable.record:read"
|
||||
if canonical_path == "sheet.range_batch_set_style" and flag == "batch":
|
||||
return str(Path(tempfile.gettempdir()) / "dws-schema-smoke-style.json")
|
||||
|
||||
typ = str(param.get("type", "string")).lower()
|
||||
prop = str(param.get("property", ""))
|
||||
haystack = " ".join([kebab_to_words(flag), kebab_to_words(prop)])
|
||||
@@ -195,15 +217,88 @@ def smoke_extra_flags(canonical_path: str) -> set[str]:
|
||||
}.get(canonical_path, set())
|
||||
|
||||
|
||||
def constraint_groups(leaf: dict[str, Any], name: str) -> list[list[str]]:
|
||||
constraints = leaf.get("constraints") or {}
|
||||
groups = constraints.get(name) or []
|
||||
return [[str(item) for item in group] for group in groups if group]
|
||||
|
||||
|
||||
def selected_inputs(leaf: dict[str, Any], include_optional: bool) -> tuple[set[str], set[str]]:
|
||||
params = leaf.get("parameters") or {}
|
||||
positionals = {
|
||||
str(item.get("name")): item
|
||||
for item in (leaf.get("positionals") or [])
|
||||
if item.get("name")
|
||||
}
|
||||
selected_params = {
|
||||
name for name, param in params.items()
|
||||
if include_optional or bool((param or {}).get("required"))
|
||||
}
|
||||
selected_positionals = {
|
||||
name for name, positional in positionals.items()
|
||||
if bool((positional or {}).get("required"))
|
||||
}
|
||||
|
||||
def is_selected(name: str) -> bool:
|
||||
return name in selected_params or name in selected_positionals
|
||||
|
||||
def select(name: str) -> bool:
|
||||
if name in params:
|
||||
selected_params.add(name)
|
||||
return True
|
||||
if name in positionals:
|
||||
selected_positionals.add(name)
|
||||
return True
|
||||
return False
|
||||
|
||||
for group in constraint_groups(leaf, "require_one_of"):
|
||||
if any(is_selected(name) for name in group):
|
||||
continue
|
||||
for name in group:
|
||||
if select(name):
|
||||
break
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for group in constraint_groups(leaf, "require_together"):
|
||||
if not any(is_selected(name) for name in group):
|
||||
continue
|
||||
for name in group:
|
||||
if not is_selected(name) and select(name):
|
||||
changed = True
|
||||
|
||||
for group in constraint_groups(leaf, "mutually_exclusive"):
|
||||
selected = [name for name in group if is_selected(name)]
|
||||
for name in selected[1:]:
|
||||
selected_params.discard(name)
|
||||
selected_positionals.discard(name)
|
||||
|
||||
return selected_params, selected_positionals
|
||||
|
||||
|
||||
def build_command(binary: str, leaf: dict[str, Any], include_optional: bool) -> list[str]:
|
||||
cli_path = str(leaf["cli_path"])
|
||||
canonical_path = str(leaf.get("canonical_path", ""))
|
||||
extra_flags = smoke_extra_flags(canonical_path)
|
||||
argv = [binary, *shlex.split(cli_path)]
|
||||
params = leaf.get("parameters") or {}
|
||||
selected_params, selected_positionals = selected_inputs(leaf, include_optional)
|
||||
|
||||
positionals = sorted(
|
||||
(leaf.get("positionals") or []),
|
||||
key=lambda item: int(item.get("index", 0)),
|
||||
)
|
||||
for positional in positionals:
|
||||
name = str(positional.get("name", ""))
|
||||
if name not in selected_positionals:
|
||||
continue
|
||||
value = value_for(name, positional, canonical_path)
|
||||
argv.append("" if value is None else value)
|
||||
|
||||
for flag in sorted(params):
|
||||
param = params[flag] or {}
|
||||
if not include_optional and not bool(param.get("required")) and flag not in extra_flags:
|
||||
if flag not in selected_params and flag not in extra_flags:
|
||||
continue
|
||||
if str(param.get("type", "")).lower() == "boolean":
|
||||
argv.append(f"--{flag}")
|
||||
@@ -361,11 +456,16 @@ def main() -> int:
|
||||
cwd = Path.cwd()
|
||||
fixture = Path(tempfile.gettempdir()) / "dws-schema-smoke-fixture.txt"
|
||||
fixture.write_text("schema smoke fixture\n", encoding="utf-8")
|
||||
style_fixture = Path(tempfile.gettempdir()) / "dws-schema-smoke-style.json"
|
||||
style_fixture.write_text(
|
||||
'[{"sheetId":"sheet-smoke","range":"A1","fontSize":12}]\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
if args.path:
|
||||
paths = args.path
|
||||
else:
|
||||
listing = run_json([args.binary, "schema", "--format", "json"], cwd, args.timeout)
|
||||
listing = run_json([args.binary, "schema", "--all", "--format", "json"], cwd, args.timeout)
|
||||
paths = [tool["canonical_path"] for product in listing["products"] for tool in product["tools"]]
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as executor:
|
||||
|
||||
Reference in new issue
Block a user