fix: align agent schema parameter contracts

This commit is contained in:
玉澜 committed 2026-07-10 15:12:49 +08:00
1 parent 391e761b59
commit ddad2f648c
26 files changed
+766 -176

No files matched your search

+67
View File
@@ -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"}
+8 -1
View File
@@ -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
}
+275 -6
View File
@@ -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, "\"", "\\\"") + "\""
}
+27 -27
View File
@@ -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
},
+17 -17
View File
@@ -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": [
"移动知识库里的文档",
+26 -114
View File
@@ -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"
}
]
}
+4 -1
View File
@@ -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",
+56
View File
@@ -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"
}
}
+6
View File
@@ -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)
}
+2
View File
@@ -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
}
+12 -4
View File
@@ -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
+12
View File
@@ -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
}
+24
View File
@@ -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.
+2
View File
@@ -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
}
+4
View File
@@ -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
+3
View File
@@ -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
}
+13
View File
@@ -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
+1
View File
@@ -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 的兼容别名")
+50
View File
@@ -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 数组")
+2
View File
@@ -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
}
+10
View File
@@ -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 模式)")
}
}
+24
View File
@@ -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()
+2
View File
@@ -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
}
+13
View File
@@ -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
}
+104 -4
View File
@@ -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: