From 753866867f90c16e7668d98a3a610af679ff230f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=89=E6=BE=9C?= Date: Sat, 11 Jul 2026 11:21:44 +0800 Subject: [PATCH] feat(schema): complete catalog contract and smoke gates --- docs/architecture.md | 2 +- docs/schema-dynamic-endpoint-design.md | 2 +- internal/app/schema_contract_test.go | 88 ++++- internal/cli/runtime_schema.go | 126 +++++- internal/cli/schema_catalog.go | 1 + internal/cli/schema_catalog.json | 359 ++++++++++++++++-- internal/cli/schema_constraints_runtime.go | 14 +- internal/cli/schema_parameter_bindings.go | 3 +- internal/cli/schema_parameter_bindings.json | 26 +- .../cli/schema_parameter_bindings_test.go | 23 +- internal/cli/schema_parameters_runtime.go | 256 +++++++++++++ internal/helpers/catalog_fallback.go | 1 + internal/helpers/chat.go | 2 +- scripts/dev/schema_command_smoke.py | 29 +- scripts/policy/check-schema-catalog.sh | 18 +- test/scripts/package_script_test.go | 59 ++- 16 files changed, 924 insertions(+), 85 deletions(-) create mode 100644 internal/cli/schema_parameters_runtime.go diff --git a/docs/architecture.md b/docs/architecture.md index 31c970a0..ef1fe134 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,7 +10,7 @@ 4. `internal/executor` and `internal/transport` execute MCP JSON-RPC calls; `internal/output` formats responses. 5. `internal/auth` manages login state, PAT tokens, and agent-code detection. 6. Schema generation combines the reviewed command surface, current Cobra flags, strong typed CLI constraints, sanitized MCP snapshots, Agent hints, and Skills at build time. Startup and Schema queries do not call MCP `tools/list`. -7. The embedded Catalog may backfill canonical identity for real commands, but it cannot replay parameter metadata into the next generation. Stable flag-to-RPC property bindings come from the versioned `schema_parameter_bindings.json` (308 active bindings audited from a 311-row historical seed); CLI `required` and constraints come from Cobra/typed annotations. MCP `required` remains interface-only metadata. +7. The embedded Catalog may backfill canonical identity for real commands, but it cannot replay parameter metadata into the next generation. Stable flag-to-RPC property bindings come from the versioned `schema_parameter_bindings.json` (316 active bindings: a 311-row historical seed, minus 3 exclusions, plus 8 current inherited-flag additions); CLI `required` and constraints come from Cobra/typed annotations. MCP `required` remains interface-only metadata. ## Repository Structure diff --git a/docs/schema-dynamic-endpoint-design.md b/docs/schema-dynamic-endpoint-design.md index 22c34fdd..44420b56 100644 --- a/docs/schema-dynamic-endpoint-design.md +++ b/docs/schema-dynamic-endpoint-design.md @@ -88,7 +88,7 @@ Catalog 是发布版本内的统一命令中间表示,位于 `internal/cli/sch 内嵌 Catalog 对真实 Go 命令只允许回填稳定 canonical identity,禁止回灌任何参数元数据。flag-to-RPC property binding 固定在版本化 `internal/cli/schema_parameter_bindings.json`,每次人工执行 `make generate-schema` 都先应用该输入;required 和 constraints 则从当前 Cobra 树及强类型注解重算。这样 generator 不依赖上一次 Catalog 自回放。完整旧契约只允许用于 `-100` 优先级的 fallback leaf。 -parameter binding 快照由历史 Catalog 的 311 条非默认映射一次性审计得到。当前公开命令面保留 308 条 active binding:5 条旧 alias 已迁移到现行主 flag,3 条与当前 helper/MCP 接口不一致的旧参数被显式排除。迁移和排除原因都写在同一 JSON 中,生成器不得从上一版 Catalog 自动补回。policy 同时校验 active 数量、历史审计记录和连续两次 Catalog 生成 byte-identical。 +parameter binding 快照由历史 Catalog 的 311 条非默认映射一次性审计得到。当前公开命令面保留 316 条 active binding:5 条旧 alias 已迁移到现行主 flag,3 条与当前 helper/MCP 接口不一致的旧参数被显式排除,另补入 8 条当前 calendar attendee 父级 persistent flag 到 MCP property 的映射。迁移、排除和新增原因都写在同一 JSON 中,生成器不得从上一版 Catalog 自动补回。policy 同时校验 active 数量、历史审计记录和连续两次 Catalog 生成 byte-identical。 ## 5. Agent 元数据 diff --git a/internal/app/schema_contract_test.go b/internal/app/schema_contract_test.go index bd14bb60..27d7af58 100644 --- a/internal/app/schema_contract_test.go +++ b/internal/app/schema_contract_test.go @@ -5,6 +5,7 @@ package app import ( + "reflect" "strings" "testing" @@ -44,7 +45,7 @@ func TestEmbeddedSchemaContractMapsToExecutableTree(t *testing.T) { continue } for _, parameter := range definition.Parameters { - flag := command.Flags().Lookup(parameter.Name) + flag := schemaContractCommandFlag(command, parameter.Name) if flag == nil { t.Errorf("%s maps parameter %q to missing flag on %q", definition.CanonicalPath, parameter.Name, command.CommandPath()) continue @@ -54,7 +55,7 @@ func TestEmbeddedSchemaContractMapsToExecutableTree(t *testing.T) { } } for flagName, propertyName := range bindings[definition.CanonicalPath] { - flag := command.Flags().Lookup(flagName) + flag := schemaContractCommandFlag(command, flagName) if flag == nil || flag.Hidden { t.Errorf("%s binding --%s references a missing or hidden public flag", definition.CanonicalPath, flagName) continue @@ -73,6 +74,74 @@ func TestEmbeddedSchemaContractMapsToExecutableTree(t *testing.T) { } } +func TestRuntimeSchemaParameterMetadataMapsToGeneratedCatalog(t *testing.T) { + root := NewRootCommand() + cli.AnnotateEmbeddedSchemaCommands(root) + allowed := map[string]bool{} + for _, definition := range cli.EmbeddedSchemaCommandDefinitions() { + allowed[definition.CanonicalPath] = true + } + snapshot, err := cli.BuildSchemaCatalogSnapshot(root, cli.SchemaCatalogBuildOptions{AllowedCanonicalPaths: allowed}) + if err != nil { + t.Fatal(err) + } + + for canonicalPath, metadata := range cli.RuntimeSchemaParameterMetadataDefinitions() { + tool := snapshot.Tools[canonicalPath] + if tool == nil { + t.Errorf("parameter metadata references unknown tool %q", canonicalPath) + continue + } + parameters, _ := tool["parameters"].(map[string]any) + parameter := func(flagName string) map[string]any { + value, _ := parameters[flagName].(map[string]any) + if value == nil { + t.Errorf("%s parameter metadata references unknown flag --%s", canonicalPath, flagName) + } + return value + } + for _, flagName := range metadata.Inherited { + parameter(flagName) + } + for _, flagName := range metadata.Required { + if value := parameter(flagName); value != nil && value["required"] != true { + t.Errorf("%s --%s required = %#v", canonicalPath, flagName, value["required"]) + } + } + for flagName, want := range metadata.RequiredWhen { + if value := parameter(flagName); value != nil && value["required_when"] != want { + t.Errorf("%s --%s required_when = %#v, want %q", canonicalPath, flagName, value["required_when"], want) + } + } + for flagName, want := range metadata.Formats { + if value := parameter(flagName); value != nil && value["format"] != want { + t.Errorf("%s --%s format = %#v, want %q", canonicalPath, flagName, value["format"], want) + } + } + for flagName, want := range metadata.Examples { + if value := parameter(flagName); value != nil && value["example"] != want { + t.Errorf("%s --%s example = %#v, want %q", canonicalPath, flagName, value["example"], want) + } + } + for flagName, want := range metadata.Enums { + if value := parameter(flagName); value != nil { + var gotStrings []string + switch got := value["enum"].(type) { + case []string: + gotStrings = append([]string(nil), got...) + case []any: + for _, item := range got { + gotStrings = append(gotStrings, item.(string)) + } + } + if !reflect.DeepEqual(gotStrings, want) { + t.Errorf("%s --%s enum = %#v, want %#v", canonicalPath, flagName, gotStrings, want) + } + } + } + } +} + func schemaContractFlagDefault(flag *pflag.Flag) string { if flag == nil { return "" @@ -95,6 +164,21 @@ func schemaContractFlagDefault(flag *pflag.Flag) string { return value } +func schemaContractCommandFlag(command *cobra.Command, name string) *pflag.Flag { + if command == nil { + return nil + } + if flag := command.Flags().Lookup(name); flag != nil { + return flag + } + for current := command; current != nil; current = current.Parent() { + if flag := current.PersistentFlags().Lookup(name); flag != nil { + return flag + } + } + return nil +} + func TestChatSchemaSeparatesSendAndReply(t *testing.T) { definitions := map[string]cli.CatalogCommandDefinition{} for _, definition := range cli.EmbeddedSchemaCommandDefinitions() { diff --git a/internal/cli/runtime_schema.go b/internal/cli/runtime_schema.go index 6014ebf9..77d3f499 100644 --- a/internal/cli/runtime_schema.go +++ b/internal/cli/runtime_schema.go @@ -39,10 +39,11 @@ const ( runtimeSchemaRulesAnnotation = "dws.schema.constraints" runtimeSchemaArgsAnnotation = "dws.schema.positionals" - runtimeSchemaFlagPropertyAnnotation = "dws.schema.property" - runtimeSchemaFlagTypeAnnotation = "dws.schema.type" - runtimeSchemaFlagRequiredAnnotation = "dws.schema.required" - runtimeSchemaFlagExampleAnnotation = "dws.schema.example" + runtimeSchemaFlagPropertyAnnotation = "dws.schema.property" + runtimeSchemaFlagTypeAnnotation = "dws.schema.type" + runtimeSchemaFlagRequiredAnnotation = "dws.schema.required" + runtimeSchemaFlagRequiredWhenAnnotation = "dws.schema.required_when" + runtimeSchemaFlagExampleAnnotation = "dws.schema.example" ) // RuntimeSchemaConstraints describes cross-parameter rules that cannot be @@ -204,7 +205,7 @@ func AnnotateRuntimeFlag(cmd *cobra.Command, flagName, propertyName, paramType s if flagName == "" { return } - flag := cmd.Flags().Lookup(flagName) + flag := runtimeCommandFlag(cmd, flagName) if flag == nil { return } @@ -220,7 +221,7 @@ func AnnotateRuntimeFlagProperty(cmd *cobra.Command, flagName, propertyName stri if cmd == nil { return } - if flag := cmd.Flags().Lookup(strings.TrimSpace(flagName)); flag != nil { + if flag := runtimeCommandFlag(cmd, flagName); flag != nil { setFlagAnnotation(flag, runtimeSchemaFlagPropertyAnnotation, strings.TrimSpace(propertyName)) } } @@ -233,20 +234,31 @@ func AnnotateRuntimeRequiredFlags(cmd *cobra.Command, flagNames ...string) { return } for _, name := range flagNames { - flag := cmd.Flags().Lookup(strings.TrimSpace(name)) + flag := runtimeCommandFlag(cmd, name) if flag != nil { setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, "true") } } } +// AnnotateRuntimeFlagRequiredWhen records a conditional CLI requirement. The +// expression is descriptive metadata and does not alter Cobra validation. +func AnnotateRuntimeFlagRequiredWhen(cmd *cobra.Command, flagName, expression string) { + if cmd == nil { + return + } + if flag := runtimeCommandFlag(cmd, flagName); flag != nil { + setFlagAnnotation(flag, runtimeSchemaFlagRequiredWhenAnnotation, strings.TrimSpace(expression)) + } +} + // 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 { + if flag := runtimeCommandFlag(cmd, flagName); flag != nil { setFlagAnnotation(flag, "x-cli-format", strings.TrimSpace(format)) } } @@ -256,7 +268,7 @@ func AnnotateRuntimeFlagEnum(cmd *cobra.Command, flagName string, values ...stri if cmd == nil { return } - flag := cmd.Flags().Lookup(strings.TrimSpace(flagName)) + flag := runtimeCommandFlag(cmd, flagName) if flag == nil { return } @@ -280,7 +292,7 @@ func AnnotateRuntimeFlagExample(cmd *cobra.Command, flagName, example string) { if cmd == nil { return } - if flag := cmd.Flags().Lookup(strings.TrimSpace(flagName)); flag != nil { + if flag := runtimeCommandFlag(cmd, flagName); flag != nil { setFlagAnnotation(flag, runtimeSchemaFlagExampleAnnotation, strings.TrimSpace(example)) } } @@ -405,8 +417,10 @@ func collectRuntimeSchemaEntries(root *cobra.Command) []runtimeSchemaEntry { if productID == "" || toolName == "" { return } - applyRuntimeSchemaParameterBindings(leaf, productID+"."+toolName) - AnnotateRuntimeConstraints(leaf, runtimeSchemaConstraintsByCanonical[productID+"."+toolName]) + canonicalPath := productID + "." + toolName + applyRuntimeSchemaParameterBindings(leaf, canonicalPath) + applyRuntimeSchemaParameterMetadata(leaf, canonicalPath) + AnnotateRuntimeConstraints(leaf, runtimeSchemaConstraintsByCanonical[canonicalPath]) parts := commandPathParts(leaf) if len(parts) == 0 { return @@ -470,8 +484,10 @@ func collectRuntimeSchemaEntries(root *cobra.Command) []runtimeSchemaEntry { if toolName == "" { toolName = derivedRuntimeToolName(parts) } - applyRuntimeSchemaParameterBindings(leaf, productID+"."+toolName) - AnnotateRuntimeConstraints(leaf, runtimeSchemaConstraintsByCanonical[productID+"."+toolName]) + canonicalPath := productID + "." + toolName + applyRuntimeSchemaParameterBindings(leaf, canonicalPath) + applyRuntimeSchemaParameterMetadata(leaf, canonicalPath) + AnnotateRuntimeConstraints(leaf, runtimeSchemaConstraintsByCanonical[canonicalPath]) group := "" if len(parts) > 2 { group = strings.Join(parts[1:len(parts)-1], ".") @@ -980,7 +996,7 @@ func runtimeToolPayload(entry runtimeSchemaEntry) map[string]any { embeddedMeta, hasEmbeddedMeta := embeddedMCPMetadataForEntry(entry) title, description, metadataSource := runtimeToolTextMetadata(entry) constraints := runtimeCommandConstraints(entry.Command) - parameters := runtimeCommandParameters(entry.Command, hint.Parameters, embeddedMeta.Parameters, constraints) + parameters := runtimeCommandParameters(entry.Command, canonicalPath, hint.Parameters, embeddedMeta.Parameters, constraints) if parameters == nil { parameters = map[string]any{} } @@ -1046,12 +1062,13 @@ func applyRuntimeInterfaceRef(target map[string]any, entry runtimeSchemaEntry, m } } -func runtimeCommandParameters(cmd *cobra.Command, hints map[string]ParameterSchemaHint, embeddedParams map[string]embeddedMCPParamMeta, constraints RuntimeSchemaConstraints) map[string]any { +func runtimeCommandParameters(cmd *cobra.Command, canonicalPath string, hints map[string]ParameterSchemaHint, embeddedParams map[string]embeddedMCPParamMeta, constraints RuntimeSchemaConstraints) map[string]any { if cmd == nil { return nil } params := map[string]any{} - cmd.Flags().VisitAll(func(flag *pflag.Flag) { + inherited := runtimeSchemaParameterMetadataByCanonical[canonicalPath].Inherited + visitRuntimeCommandFlags(cmd, inherited, func(flag *pflag.Flag) { if flag == nil || flag.Hidden || flag.Name == "help" || isGenericPayloadFlag(flag) { return } @@ -1104,8 +1121,8 @@ func runtimeCommandParameters(cmd *cobra.Command, hints map[string]ParameterSche if interfaceType != "" && interfaceType != paramType { entry["interface_type"] = interfaceType } - requiredWhen := "" - if hasEmbeddedParam { + requiredWhen := firstFlagAnnotation(flag, runtimeSchemaFlagRequiredWhenAnnotation) + if requiredWhen == "" && hasEmbeddedParam { requiredWhen = strings.TrimSpace(embeddedParam.RequiredWhen) } if hasHint && strings.TrimSpace(hint.RequiredWhen) != "" { @@ -1156,6 +1173,77 @@ func runtimeCommandParameters(cmd *cobra.Command, hints map[string]ParameterSche return params } +// runtimeCommandFlag resolves local flags plus product/group persistent flags. +// Root persistent flags are intentionally available only when explicitly +// requested; they are global execution controls, not tool parameters. +func runtimeCommandFlag(cmd *cobra.Command, name string) *pflag.Flag { + if cmd == nil { + return nil + } + name = strings.TrimSpace(name) + if name == "" { + return nil + } + if flag := cmd.Flags().Lookup(name); flag != nil { + return flag + } + for current := cmd; current != nil; current = current.Parent() { + if flag := current.PersistentFlags().Lookup(name); flag != nil { + return flag + } + } + return nil +} + +func visitRuntimeCommandFlags(cmd *cobra.Command, inheritedNames []string, visit func(*pflag.Flag)) { + if cmd == nil || visit == nil { + return + } + root := cmd.Root() + rootPersistent := map[*pflag.Flag]bool{} + ancestorPersistent := map[*pflag.Flag]bool{} + if root != nil { + root.PersistentFlags().VisitAll(func(flag *pflag.Flag) { + rootPersistent[flag] = true + }) + } + for parent := cmd.Parent(); parent != nil && parent != root; parent = parent.Parent() { + parent.PersistentFlags().VisitAll(func(flag *pflag.Flag) { + ancestorPersistent[flag] = true + }) + } + allowedInherited := map[string]bool{} + for _, name := range inheritedNames { + if name = strings.TrimSpace(name); name != "" { + allowedInherited[name] = true + } + } + seen := map[string]bool{} + visitSet := func(flags *pflag.FlagSet) { + if flags == nil { + return + } + flags.VisitAll(func(flag *pflag.Flag) { + if flag == nil || rootPersistent[flag] || seen[flag.Name] || + (ancestorPersistent[flag] && !allowedInherited[flag.Name]) { + return + } + seen[flag.Name] = true + visit(flag) + }) + } + visitSet(cmd.Flags()) + visitSet(cmd.PersistentFlags()) + for parent := cmd.Parent(); parent != nil && parent != root; parent = parent.Parent() { + parent.PersistentFlags().VisitAll(func(flag *pflag.Flag) { + if flag != nil && allowedInherited[flag.Name] && !seen[flag.Name] { + seen[flag.Name] = true + visit(flag) + } + }) + } +} + func runtimeCommandConstraints(cmd *cobra.Command) RuntimeSchemaConstraints { if cmd == nil || cmd.Annotations == nil { return RuntimeSchemaConstraints{} diff --git a/internal/cli/schema_catalog.go b/internal/cli/schema_catalog.go index 674bb4a6..d36e0603 100644 --- a/internal/cli/schema_catalog.go +++ b/internal/cli/schema_catalog.go @@ -352,6 +352,7 @@ func AnnotateEmbeddedSchemaCommands(root *cobra.Command) EmbeddedSchemaAnnotatio for _, parameter := range definition.Parameters { parameterType := firstNonEmptySchemaString(parameter.InterfaceType, parameter.Type, "string") AnnotateRuntimeFlag(cmd, parameter.Name, parameter.Property, parameterType, parameter.Required, parameter.Default) + AnnotateRuntimeFlagRequiredWhen(cmd, parameter.Name, parameter.RequiredWhen) AnnotateRuntimeFlagFormat(cmd, parameter.Name, parameter.Format) AnnotateRuntimeFlagEnum(cmd, parameter.Name, parameter.Enum...) } diff --git a/internal/cli/schema_catalog.json b/internal/cli/schema_catalog.json index 28d26eed..51ad1c0e 100644 --- a/internal/cli/schema_catalog.json +++ b/internal/cli/schema_catalog.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:ac9f6328bcf1c95aa23483dc0a79c3511c3f5a13fec44697dae2d20b140e2b7f", + "source_hash": "sha256:faa338ecc8292450d41b10115ea13aafe3ff5e86bde442b544bc0bd7ac08d4a9", "surface_hash": "sha256:c0a9b75b4e38934db73f4ce1d337b4c64aa5e541257dd9e84c4947da9c9ab664", "catalog": { "agent_metadata": { @@ -12553,7 +12553,7 @@ "dws aisearch person --keyword \"张三\" --dimension name --format json", "dws aisearch person --keyword \"产品部\" --dimension department --format json" ], - "has_parameters": false, + "has_parameters": true, "idempotency": "idempotent", "interface_ref": { "product_id": "aisearch", @@ -12562,8 +12562,25 @@ "is_alias": false, "metadata_source": "embedded-mcp-metadata", "name": "enterprise_person_search", - "parameter_count": 0, - "parameters": {}, + "parameter_count": 2, + "parameters": { + "dimension": { + "default": "all", + "description": "查询维度: all/name/department/position/duty/supervisor/subordinate/phone/jobNumber,多个用逗号分隔", + "interface_description": "查询维度,根据用户意图自动映射。可选值:\\n- all: 默认,全部维度\\n- name: 姓名(用户提到'姓名'、'叫什么'、'是谁'时使用)\\n- department: 部门(用户提到'部门'、'团队'时使用)\\n- position: 职位(用户提到'职位'、'岗位'时使用)\\n- duty: 职责/技能(用户提到'负责'、'职责'、'技能'时使用)\\n- supervisor: 上级(用户提到'上级'、'领导'、'主管'时使用)\\n- subordinate: 下级(用户提到'下级'、'下属'时使用)\\n- phone: 手机号(用户提到'手机号'、'电话'、'手机'时使用)\\n- jobNumber: 工号(用户提到'工号'、'员工编号'时使用)\\n\\n【重要】:从用户问题中识别维度词并映射到此字段,不要将维度词放入keyword中。", + "interface_type": "array", + "property": "dimension", + "required": false, + "type": "string" + }, + "keyword": { + "description": "搜索关键词 (必填,如人名、技能关键词等)", + "interface_description": "搜索关键词。仅填入实际的搜索目标(人名、技能关键词等),不包含查询维度词。 【关键词提取规则】: 以下维度词必须从keyword中排除,改为映射到dimension: - '上级'、'领导'、'主管' → dimension: ['supervisor'] - '下级'、'下属'、'团队成员' → dimension: ['subordinate'] - '部门'、'团队'、'组织' → dimension: ['department'] - '职位'、'岗位'、'职级' → dimension: ['position'] - '职责'、'负责'、'技能' → dimension: ['duty'] - '姓名'、'叫什么' → dimension: ['name'] 【示例】: - 用户问'五道的上级是谁' → keyword: '五道', dimension: ['supervisor'] - 用户问'张三负责什么' → keyword: '张三', dimension: ['duty'] - 用户问'AI搜问的负责人是谁' → keyword: 'AI搜问', dimension: ['duty'] - 用户问'产品部有谁' → keyword: '产品部', dimension: ['department'] - 用户问'李四是哪个部门的' → keyword: '李四', dimension: ['department']- 用户问'手机号13800138000是谁' → keyword: '13800138000', dimension: ['phone']\\n- 用户问'工号A12345是谁' → keyword: 'A12345', dimension: ['jobNumber']", + "property": "keyword", + "required": true, + "type": "string" + } + }, "path": "aisearch.enterprise_person_search", "primary_cli_path": "aisearch person", "product_id": "aisearch", @@ -15592,7 +15609,7 @@ "interface_description": "可选。指定要获取的记录 ID 列表,单次最多 100 个。传入时直接按 ID 返回,忽略 filters 和 sort。\n适用于已知 recordId(如关联字段中的 linkedRecordIds)时的精准取数。", "interface_type": "array", "property": "recordIds", - "required": false, + "required": true, "type": "string" }, "sort": { @@ -15828,7 +15845,7 @@ "interface_description": "可选。指定要获取的记录 ID 列表,单次最多 100 个。传入时直接按 ID 返回,忽略 filters 和 sort。\n适用于已知 recordId(如关联字段中的 linkedRecordIds)时的精准取数。", "interface_type": "array", "property": "recordIds", - "required": false, + "required": true, "type": "string" }, "table-id": { @@ -17091,6 +17108,15 @@ "cli_name": "update", "cli_path": "aitable table update", "confirmation": "not_required", + "constraints": { + "require_one_of": [ + [ + "name", + "description", + "record-name-key" + ] + ] + }, "description": "重命名指定 Table(数据表)。若新名称不符合命名要求、与同一 Base 下其他表重名或无权限,将返回错误。", "display": "AI 表格操作", "effect": "write", @@ -18718,8 +18744,10 @@ }, "json": { "description": "filter 数组 JSON", + "example": "[{\"operator\":\"and\",\"operands\":[{\"operator\":\"eq\",\"operands\":[\"fldX\",\"value\"]}]}]", + "format": "json", "property": "json", - "required": false, + "required": true, "type": "string" }, "table-id": { @@ -18862,8 +18890,10 @@ }, "json": { "description": "group 数组 JSON", + "example": "[{\"fieldId\":\"fldX\",\"direction\":\"asc\"}]", + "format": "json", "property": "json", - "required": false, + "required": true, "type": "string" }, "table-id": { @@ -19073,8 +19103,10 @@ }, "json": { "description": "sort 数组 JSON", + "example": "[{\"fieldId\":\"fldX\",\"direction\":\"asc\"}]", + "format": "json", "property": "json", - "required": false, + "required": true, "type": "string" }, "table-id": { @@ -19564,12 +19596,14 @@ "parameters": { "end": { "description": "结束日期,格式 YYYY-MM-DD (必填)", + "format": "date", "property": "end", "required": true, "type": "string" }, "start": { "description": "开始日期,格式 YYYY-MM-DD (必填)", + "format": "date", "property": "start", "required": true, "type": "string" @@ -19814,6 +19848,8 @@ }, "files": { "description": "附件列表,格式 \u003cfileId\u003e:\u003cname\u003e,多项逗号分隔 (必填)", + "example": "file-smoke:report.pdf", + "format": "file-id-name-list", "interface_description": "attachments", "interface_type": "array", "property": "attachments", @@ -19867,11 +19903,27 @@ "is_alias": false, "metadata_source": "embedded-mcp-metadata", "name": "add_calendar_participant", - "parameter_count": 2, + "parameter_count": 4, "parameters": { "attendees": { "description": "参会人 userId 列表,逗号分隔 (必填,最多500人)", - "property": "attendees", + "interface_description": "需要添加的参与人uid列表。", + "interface_type": "array", + "property": "attendeesToAdd", + "required": true, + "type": "string" + }, + "calendar-id": { + "description": "日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 book list 获取)。注意:订阅日历下的日程无参会人", + "interface_description": "日历本id,可选,默认值为 \"primary\",表示主日历本。", + "property": "calendarId", + "required": false, + "type": "string" + }, + "event": { + "description": "日程 ID (必填)", + "interface_description": "日程ID,可调用创建日程接口或查询日程列表接口获取id参数值", + "property": "eventId", "required": true, "type": "string" }, @@ -20437,7 +20489,7 @@ "dws calendar participant list --event EVENT_ID # 查询 eventId: dws calendar event list" ], "group": "attendee", - "has_parameters": false, + "has_parameters": true, "idempotency": "idempotent", "interface_ref": { "product_id": "calendar", @@ -20446,8 +20498,23 @@ "is_alias": false, "metadata_source": "embedded-mcp-metadata", "name": "get_calendar_participants", - "parameter_count": 0, - "parameters": {}, + "parameter_count": 2, + "parameters": { + "calendar-id": { + "description": "日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 book list 获取)。注意:订阅日历下的日程无参会人", + "interface_description": "日历本id,可选,默认值为 \"primary\",表示主日历本。", + "property": "calendarId", + "required": false, + "type": "string" + }, + "event": { + "description": "日程 ID (必填)", + "interface_description": "日程ID,可调用创建日程接口或查询日程列表接口获取id参数值。", + "property": "eventId", + "required": true, + "type": "string" + } + }, "path": "calendar.get_calendar_participants", "primary_cli_path": "calendar attendee list", "product_id": "calendar", @@ -20980,11 +21047,27 @@ "is_alias": false, "metadata_source": "embedded-mcp-metadata", "name": "remove_calendar_participant", - "parameter_count": 1, + "parameter_count": 3, "parameters": { "attendees": { "description": "参会人 userId 列表,逗号分隔 (必填)", - "property": "attendees", + "interface_description": "需要被删除的日程参与者userId列表。", + "interface_type": "array", + "property": "attendeesToRemove", + "required": true, + "type": "string" + }, + "calendar-id": { + "description": "日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 book list 获取)。注意:订阅日历下的日程无参会人", + "interface_description": "日历本id,可选,默认值为 \"primary\",表示在 主日历本", + "property": "calendarId", + "required": false, + "type": "string" + }, + "event": { + "description": "日程 ID (必填)", + "interface_description": "日程ID,可调用创建日程接口或查询日程列表接口获取id参数值。", + "property": "eventId", "required": true, "type": "string" } @@ -21047,6 +21130,12 @@ }, "status": { "description": "响应状态: needsAction(未操作)|accepted(接受)|declined(拒绝)|tentative(暂定) (必填)", + "enum": [ + "needsAction", + "accepted", + "declined", + "tentative" + ], "interface_description": "设置响应状态,可取值和对应说明如下:\n1. needsAction:未操作(默认值)\n2. accepted:接受\n3. declined:拒绝\n4. tentative:暂定", "property": "responseStatus", "required": true, @@ -21763,6 +21852,20 @@ "cli_name": "send-card", "cli_path": "chat message send-card", "confirmation": "not_required", + "constraints": { + "mutually_exclusive": [ + [ + "group", + "receiver" + ] + ], + "require_one_of": [ + [ + "group", + "receiver" + ] + ] + }, "description": "创建并推送流式卡片", "display": "群聊 / 消息 / 机器人", "effect": "write", @@ -22049,6 +22152,9 @@ }, "type": { "description": "资源类型: mediaId (必填)", + "enum": [ + "mediaId" + ], "property": "type", "required": true, "type": "string" @@ -22280,6 +22386,22 @@ "cli_name": "conversation-info", "cli_path": "chat conversation-info", "confirmation": "not_required", + "constraints": { + "mutually_exclusive": [ + [ + "group", + "user", + "open-dingtalk-id" + ] + ], + "require_one_of": [ + [ + "group", + "user", + "open-dingtalk-id" + ] + ] + }, "description": "获取会话的基础信息", "display": "群聊 / 消息 / 机器人", "effect": "read", @@ -22456,6 +22578,22 @@ "cli_name": "list", "cli_path": "chat message list", "confirmation": "not_required", + "constraints": { + "mutually_exclusive": [ + [ + "group", + "user", + "open-dingtalk-id" + ] + ], + "require_one_of": [ + [ + "group", + "user", + "open-dingtalk-id" + ] + ] + }, "description": "拉取指定群聊的会话消息内容", "display": "群聊 / 消息 / 机器人", "effect": "read", @@ -22708,6 +22846,20 @@ "cli_name": "list-direct", "cli_path": "chat message list-direct", "confirmation": "not_required", + "constraints": { + "mutually_exclusive": [ + [ + "user", + "open-dingtalk-id" + ] + ], + "require_one_of": [ + [ + "user", + "open-dingtalk-id" + ] + ] + }, "description": "拉取指定用户的单聊会话消息内容", "display": "群聊 / 消息 / 机器人", "effect": "read", @@ -25061,6 +25213,11 @@ "group", "user", "open-dingtalk-id" + ], + [ + "text", + "content", + "msg-type" ] ] }, @@ -25109,6 +25266,7 @@ "format": "file-path", "property": "filePath", "required": false, + "required_when": "msg-type is file or audio or video", "type": "string" }, "group": { @@ -25119,8 +25277,10 @@ }, "media-id": { "description": "图片 mediaId(通过 dt_media_upload 上传后用 extract_media_id.py 提取,仅 msgType=image)", + "example": "@lADP_schema_smoke", "property": "mediaId", "required": false, + "required_when": "msg-type is image", "type": "string" }, "msg-type": { @@ -25173,7 +25333,7 @@ { "description": "消息内容(也可使用 --text;富媒体消息可省略)", "index": 0, - "name": "text", + "name": "content", "required": false, "type": "string" } @@ -25412,6 +25572,14 @@ "cli_name": "group-mute-member", "cli_path": "chat group-mute-member", "confirmation": "not_required", + "constraints": { + "require_one_of": [ + [ + "users", + "user" + ] + ] + }, "description": "管理群禁言成员", "display": "群聊 / 消息 / 机器人", "effect": "write", @@ -25436,10 +25604,18 @@ }, "mute-time": { "description": "禁言时长(毫秒),支持 300000/3600000/86400000/604800000/2592000000", + "enum": [ + "300000", + "3600000", + "86400000", + "604800000", + "2592000000" + ], "interface_description": "muteTime", "interface_type": "number", "property": "muteTime", "required": false, + "required_when": "off is false", "type": "integer" }, "off": { @@ -25666,6 +25842,20 @@ "cli_name": "transfer-owner", "cli_path": "chat group transfer-owner", "confirmation": "not_required", + "constraints": { + "mutually_exclusive": [ + [ + "new-owner", + "user" + ] + ], + "require_one_of": [ + [ + "new-owner", + "user" + ] + ] + }, "description": "群主转让", "display": "群聊 / 消息 / 机器人", "effect": "write", @@ -25858,6 +26048,14 @@ "cli_name": "set-admin", "cli_path": "chat group set-admin", "confirmation": "not_required", + "constraints": { + "require_one_of": [ + [ + "users", + "user" + ] + ] + }, "description": "设置/取消群成员为管理员", "display": "群聊 / 消息 / 机器人", "effect": "write", @@ -26020,6 +26218,8 @@ }, "icon-media-id": { "description": "群头像 mediaId (必填)", + "example": "@lADP_schema_smoke", + "format": "dingtalk-media-id", "interface_description": "iconMediaId", "property": "iconMediaId", "required": true, @@ -26287,6 +26487,11 @@ }, "option": { "description": "可见范围: FORBIDDEN | RECENT_100 | ALL (必填)", + "enum": [ + "FORBIDDEN", + "RECENT_100", + "ALL" + ], "interface_description": "可选值: \"FORBIDDEN\"-禁止查看历史消息;\"RECENT_100\"-可查看最近100条消息;\"ALL\"-可查看全部历史消息", "property": "option", "required": true, @@ -27544,6 +27749,7 @@ "parameters": { "dept": { "description": "部门 ID (必填)", + "format": "numeric-id", "interface_description": "部门Id", "interface_type": "number", "property": "deptId", @@ -27657,6 +27863,7 @@ "parameters": { "dept": { "description": "部门 ID (必填)", + "format": "numeric-id", "interface_description": "部门Id", "interface_type": "number", "property": "deptId", @@ -28154,7 +28361,7 @@ "scope-values": { "description": "权限点 scopeValue,多个用逗号或分号分隔", "property": "scopeValues", - "required": false, + "required": true, "type": "string" }, "unified-app-id": { @@ -29455,7 +29662,7 @@ "scope-values": { "description": "待取消权限点 scopeValue,多个用逗号或分号分隔", "property": "scopeValues", - "required": false, + "required": true, "type": "string" }, "unified-app-id": { @@ -29821,7 +30028,7 @@ "event-codes": { "description": "事件码,多个用逗号或分号分隔", "property": "eventCodes", - "required": false, + "required": true, "type": "string" }, "unified-app-id": { @@ -29871,7 +30078,7 @@ "event-codes": { "description": "事件码,多个用逗号或分号分隔", "property": "eventCodes", - "required": false, + "required": true, "type": "string" }, "unified-app-id": { @@ -31486,8 +31693,7 @@ [ "text", "heading", - "element", - "type" + "element" ] ] }, @@ -32627,8 +32833,7 @@ [ "text", "heading", - "element", - "type" + "element" ] ] }, @@ -36341,6 +36546,13 @@ }, "policy": { "description": "权限类型: 0=管理员, 1=所有者, 2=可编辑, 3=可查看/下载, 4=仅查看 (必填)", + "enum": [ + "0", + "1", + "2", + "3", + "4" + ], "interface_description": "策略id:MANAGER(0L, \"管理员\"), OWNER(1L, \"所有者\"), EDITOR(2L, \"可编辑\"), READ_DOWNLOAD(3L, \"可查看/下载\"), READ(4L, \"仅查看\");", "interface_type": "number", "property": "policyId", @@ -39114,8 +39326,12 @@ }, "enabled": { "description": "PAT 撞墙时是否允许本地打开浏览器", + "enum": [ + "true", + "false" + ], "property": "enabled", - "required": false, + "required": true, "type": "boolean" } }, @@ -39149,6 +39365,14 @@ "cli_name": "submit", "cli_path": "report entry submit", "confirmation": "not_required", + "constraints": { + "require_one_of": [ + [ + "contents", + "contents-file" + ] + ] + }, "description": "创建日志", "display": "钉钉日志(OA 周报应用 / 日志模版填报)", "effect": "write", @@ -39171,10 +39395,12 @@ "parameters": { "contents": { "description": "日志内容 JSON 数组 (必填,或用 --contents-file),每项含 key/sort/content/contentType/type;传 - 表示从 stdin 读取", + "example": "[{\"content\":\"schema smoke\",\"sort\":\"0\",\"key\":\"work\",\"contentType\":\"markdown\",\"type\":\"1\"}]", + "format": "json", "interface_description": "日志内容列表", "interface_type": "array", "property": "contents", - "required": true, + "required": false, "type": "string" }, "contents-file": { @@ -39676,6 +39902,10 @@ "parameters": { "dimension": { "description": "维度类型: ROWS 或 COLUMNS (必填)", + "enum": [ + "ROWS", + "COLUMNS" + ], "interface_description": "追加维度(必填)。ROWS 表示追加行,COLUMNS 表示追加列。", "property": "dimension", "required": true, @@ -39978,6 +40208,8 @@ "parameters": { "cell-style": { "description": "单元格样式 JSON 对象 (可选),如 '{\"backgroundColor\":\"#FF0000\",\"bold\":true}'", + "example": "{\"backgroundColor\":\"#FFCDD2\",\"bold\":true}", + "format": "json", "interface_description": "条件命中时应用的单元格样式。对 numberCondition、textCondition、emptyCondition、errorCondition、duplicateCondition、formulaCondition、rankCondition、averageCondition、stdevCondition 类型有效。", "interface_type": "object", "property": "cellStyle", @@ -39986,12 +40218,15 @@ }, "condition": { "description": "条件类型及参数 JSON 对象 (必填),如 '{\"numberCondition\":{\"operator\":\"greater\",\"value1\":\"80\"}}'", + "example": "{\"numberCondition\":{\"operator\":\"greater\",\"value1\":\"80\"}}", + "format": "json", "property": "condition", "required": true, "type": "string" }, "data-bar-style": { "description": "数据条样式 JSON 对象 (可选,仅数据条类型),如 '{\"fill\":[\"#4CAF50\",\"#F44336\"],\"isGradient\":true}'", + "format": "json", "interface_description": "数据条的显示样式配置。仅 dataBarCondition 类型时有效。", "interface_type": "object", "property": "dataBarStyle", @@ -40006,7 +40241,8 @@ }, "ranges": { "description": "应用范围 JSON 数组 (必填),如 '[\"A1:E10\"]'", - "format": "a1-range", + "example": "[\"A1:B2\"]", + "format": "json", "interface_description": "条件格式规则的应用范围(必填),A1 表示法的数组。示例:[\"A1:E10\"]", "interface_type": "array", "property": "ranges", @@ -40540,6 +40776,10 @@ "parameters": { "dimension": { "description": "删除维度: ROWS 或 COLUMNS (必填)", + "enum": [ + "ROWS", + "COLUMNS" + ], "interface_description": "删除维度:ROWS 表示删除行,COLUMNS 表示删除列。必填。", "property": "dimension", "required": true, @@ -40547,6 +40787,7 @@ }, "length": { "description": "删除数量,正整数 (必填),最大 5000", + "format": "positive-integer", "interface_description": "要删除的行/列数量,必须为正整数(\u003e= 1)。必填。", "interface_type": "number", "property": "length", @@ -40561,6 +40802,7 @@ }, "position": { "description": "删除起始位置,A1 表示法 (必填)。ROWS 时为行号如 \"3\";COLUMNS 时为列字母如 \"A\"", + "example": "1", "format": "a1-range", "interface_description": "删除起始位置的 A1 表示法。dimension=ROWS 时为 1-based 行号字符串,如 \"3\" 表示从第 3 行开始删除;dimension=COLUMNS 时为列字母,如 \"A\" 表示从 A 列开始删除、\"AB\" 表示从 AB 列开始删除。从该位置开始连续删除 length 个行/列。允许携带工作表前缀,如 \"Sheet1!3\" / \"Sheet1!A\",此时将忽略 sheetId 参数。必填。", "property": "position", @@ -42064,6 +42306,10 @@ "parameters": { "dimension": { "description": "插入维度: ROWS 或 COLUMNS (必填)", + "enum": [ + "ROWS", + "COLUMNS" + ], "interface_description": "插入维度:ROWS 表示插入行,COLUMNS 表示插入列。必填。", "property": "dimension", "required": true, @@ -42071,6 +42317,7 @@ }, "length": { "description": "插入数量,正整数 (必填),最大 5000", + "format": "positive-integer", "interface_description": "要插入的行/列数量,必须为正整数(\u003e= 1)。必填。", "interface_type": "number", "property": "length", @@ -42085,6 +42332,7 @@ }, "position": { "description": "插入位置,A1 表示法 (必填)。ROWS 时为行号如 \"3\";COLUMNS 时为列字母如 \"A\"", + "example": "1", "format": "a1-range", "interface_description": "插入位置的 A1 表示法。dimension=ROWS 时为 1-based 行号字符串,如 \"3\" 表示在第 3 行之前插入;dimension=COLUMNS 时为列字母,如 \"A\" 表示在 A 列之前插入、\"AB\" 表示在 AB 列之前插入。允许携带工作表前缀,如 \"Sheet1!3\" / \"Sheet1!A\",此时将忽略 sheetId 参数。必填。", "property": "position", @@ -42367,6 +42615,10 @@ }, "dimension": { "description": "维度类型: ROWS 或 COLUMNS (必填)", + "enum": [ + "ROWS", + "COLUMNS" + ], "interface_description": "移动维度(必填)。ROWS 表示移动行,COLUMNS 表示移动列。", "property": "dimension", "required": true, @@ -42823,6 +43075,8 @@ }, "bg-colors-json": { "description": "背景色二维 JSON 数组,维度需与 --range 一致", + "example": "[[\"#ffffff\",\"#ffffff\"],[\"#ffffff\",\"#ffffff\"]]", + "format": "json", "property": "bgColorsJson", "required": false, "type": "string" @@ -42835,6 +43089,8 @@ }, "font-colors-json": { "description": "字体颜色二维 JSON 数组", + "example": "[[\"#000000\",\"#000000\"],[\"#000000\",\"#000000\"]]", + "format": "json", "property": "fontColorsJson", "required": false, "type": "string" @@ -42847,30 +43103,46 @@ }, "font-sizes-json": { "description": "字号二维 JSON 数组,维度需与 --range 一致", + "example": "[[12,12],[12,12]]", + "format": "json", "property": "fontSizesJson", "required": false, "type": "string" }, "font-weight": { "description": "字体粗细(bold/normal),一键刷整个 range", + "enum": [ + "bold", + "normal" + ], "property": "fontWeight", "required": false, "type": "string" }, "font-weights-json": { "description": "字体粗细二维 JSON 数组", + "example": "[[\"normal\",\"normal\"],[\"normal\",\"normal\"]]", + "format": "json", "property": "fontWeightsJson", "required": false, "type": "string" }, "h-align": { "description": "水平对齐(left/center/right/general),一键刷整个 range", + "enum": [ + "left", + "center", + "right", + "general" + ], "property": "hAlign", "required": false, "type": "string" }, "h-aligns-json": { "description": "水平对齐二维 JSON 数组", + "example": "[[\"left\",\"left\"],[\"left\",\"left\"]]", + "format": "json", "property": "hAlignsJson", "required": false, "type": "string" @@ -42904,18 +43176,30 @@ }, "v-align": { "description": "垂直对齐(top/middle/bottom),一键刷整个 range", + "enum": [ + "top", + "middle", + "bottom" + ], "property": "vAlign", "required": false, "type": "string" }, "v-aligns-json": { "description": "垂直对齐二维 JSON 数组", + "example": "[[\"top\",\"top\"],[\"top\",\"top\"]]", + "format": "json", "property": "vAlignsJson", "required": false, "type": "string" }, "word-wrap": { "description": "换行方式(overflow/clip/autoWrap),整个 range 共用", + "enum": [ + "overflow", + "clip", + "autoWrap" + ], "interface_description": "单元格换行方式,单值字符串,整个 range 共用(非二维数组)。取值枚举:overflow / clip / autoWrap(驼峰,非下划线)。", "property": "wordWrap", "required": false, @@ -43194,6 +43478,8 @@ }, "options": { "description": "下拉选项 JSON 数组 (必填),如 '[{\"value\":\"选项1\",\"color\":\"#ff0000\"}]'", + "example": "[{\"value\":\"option1\"},{\"value\":\"option2\"}]", + "format": "json", "interface_description": "下拉选项数组(必填),至少包含 1 个选项。每个选项包含 value(必填)和 color(可选)。选项值不能包含英文逗号。", "interface_type": "array", "property": "options", @@ -43636,6 +43922,8 @@ "parameters": { "cell-style": { "description": "单元格样式 JSON 对象 (可选)", + "example": "{\"backgroundColor\":\"#FFCDD2\",\"bold\":true}", + "format": "json", "interface_description": "条件命中时应用的单元格样式。", "interface_type": "object", "property": "cellStyle", @@ -43644,12 +43932,15 @@ }, "condition": { "description": "条件类型及参数 JSON 对象 (可选),传入后替换原有条件类型", + "example": "{\"numberCondition\":{\"operator\":\"greater\",\"value1\":\"80\"}}", + "format": "json", "property": "condition", "required": false, "type": "string" }, "data-bar-style": { "description": "数据条样式 JSON 对象 (可选,仅数据条类型)", + "format": "json", "interface_description": "数据条的显示样式配置。仅 dataBarCondition 类型时有效。", "interface_type": "object", "property": "dataBarStyle", @@ -43664,7 +43955,8 @@ }, "ranges": { "description": "应用范围 JSON 数组 (可选),如 '[\"A1:E10\"]'", - "format": "a1-range", + "example": "[\"A1:B2\"]", + "format": "json", "interface_description": "条件格式规则的应用范围(可选)。", "interface_type": "array", "property": "ranges", @@ -43742,6 +44034,10 @@ "parameters": { "dimension": { "description": "更新维度: ROWS 或 COLUMNS (必填)", + "enum": [ + "ROWS", + "COLUMNS" + ], "interface_description": "更新维度:ROWS 表示行(对应行高/行显隐),COLUMNS 表示列(对应列宽/列显隐)。必填。", "property": "dimension", "required": true, @@ -43756,6 +44052,7 @@ }, "length": { "description": "更新数量,正整数 (必填),最大 5000", + "format": "positive-integer", "interface_description": "要更新的连续行/列数量,必须为正整数(\u003e= 1),行列均最多 5000。必填。", "interface_type": "number", "property": "length", @@ -43785,6 +44082,7 @@ }, "start-index": { "description": "起始位置,A1 表示法 (必填)。ROWS 时为行号如 \"3\";COLUMNS 时为列字母如 \"A\"", + "example": "1", "format": "a1-range", "interface_description": "起始位置的 A1 表示法。dimension=ROWS 时为 1-based 行号字符串,如 \"3\" 表示从第 3 行开始;dimension=COLUMNS 时为列字母,如 \"A\" 表示从 A 列开始、\"AB\" 表示从 AB 列开始。从该位置起的连续 length 行/列将被更新。允许携带工作表前缀,如 \"Sheet1!3\" / \"Sheet1!A\",此时将忽略 sheetId 参数。必填。", "property": "startIndex", @@ -46345,6 +46643,9 @@ }, "type": { "description": "知识库类型: myWikiSpace 时直接返回「我的文档」,省略则搜索组织知识库", + "enum": [ + "myWikiSpace" + ], "property": "type", "required": false, "type": "string" diff --git a/internal/cli/schema_constraints_runtime.go b/internal/cli/schema_constraints_runtime.go index 9ad1b211..8dc66438 100644 --- a/internal/cli/schema_constraints_runtime.go +++ b/internal/cli/schema_constraints_runtime.go @@ -5,19 +5,29 @@ package cli func init() { + registerRequireOneOf("aitable.table_update", "name", "description", "record-name-key") registerRequireTogether("calendar.create_calendar_event", "recurrence-type", "recurrence-interval", "recurrence-range-type") registerRequireOneOf("calendar.query_busy_status", "users", "rooms") registerRequireTogether("calendar.update_calendar_event", "recurrence-type", "recurrence-interval", "recurrence-range-type") registerExclusiveOneOf("chat.search_messages_by_sender", "sender-user-id", "sender-open-dingtalk-id") + registerExclusiveOneOf("chat.create_and_send_card", "group", "receiver") + registerExclusiveOneOf("chat.get_conversation_info", "group", "user", "open-dingtalk-id") + registerExclusiveOneOf("chat.list_conversation_message_v2", "group", "user", "open-dingtalk-id") + registerExclusiveOneOf("chat.list_individual_chat_message", "user", "open-dingtalk-id") + registerRequireOneOf("chat.send_personal_message", "text", "content", "msg-type") registerExclusiveOneOf("chat.send_robot_message", "group", "users") + registerRequireOneOf("chat.set_group_member_mute_list", "users", "user") + registerExclusiveOneOf("chat.transfer_group_owner", "new-owner", "user") + registerRequireOneOf("chat.update_conv_member_roles", "users", "user") registerRequireTogether("contact.query_dismission_employee_list", "start", "end") registerRequireOneOf("dev.connect_status", "robot-client-id", "unified-app-id") registerRequireOneOf("dev.connect_stop", "robot-client-id", "unified-app-id") registerRequireOneOf("devdoc.search_open_error_code_rag", "query", "request-id", "error-code", "error-message", "context") - registerRequireOneOf("doc.insert_document_block", "text", "heading", "element", "type") + registerRequireOneOf("doc.insert_document_block", "text", "heading", "element") registerExclusiveOneOf("doc.update_document", "content", "content-file") - registerRequireOneOf("doc.update_document_block", "text", "heading", "element", "type") + registerRequireOneOf("doc.update_document_block", "text", "heading", "element") registerRequireOneOf("pat.batch_grant", "scope", "product", "products", "domain", "domains", "recommend") + registerRequireOneOf("report.create_report", "contents", "contents-file") RegisterRuntimeSchemaConstraints("sheet.range_set_style", RuntimeSchemaConstraints{ MutuallyExclusive: [][]string{ {"bg-color", "bg-colors-json"}, diff --git a/internal/cli/schema_parameter_bindings.go b/internal/cli/schema_parameter_bindings.go index 3d215159..430c0585 100644 --- a/internal/cli/schema_parameter_bindings.go +++ b/internal/cli/schema_parameter_bindings.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" ) -const schemaParameterBindingsVersion = 1 +const schemaParameterBindingsVersion = 2 //go:embed schema_parameter_bindings.json var embeddedSchemaParameterBindingsJSON []byte @@ -32,6 +32,7 @@ type schemaParameterBindingSnapshot struct { HistoricalBindingCount int `json:"historical_binding_count"` Migrations map[string]string `json:"migrations"` Excluded map[string]string `json:"excluded"` + Added map[string]string `json:"added"` Bindings map[string]map[string]string `json:"bindings"` } diff --git a/internal/cli/schema_parameter_bindings.json b/internal/cli/schema_parameter_bindings.json index 4d3d9580..0c4d91d5 100644 --- a/internal/cli/schema_parameter_bindings.json +++ b/internal/cli/schema_parameter_bindings.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "source_catalog_hash": "sha256:6dc63141d35119ff6095d189ef9d8994cd35162ecf9fff9ad23b2ab68e4e2b7f", "historical_binding_count": 311, "migrations": { @@ -14,6 +14,16 @@ "calendar.query_available_meeting_room --available": "hidden compatibility flag explicitly marked as a non-MCP parameter", "minutes.query_user_tag_list --limit": "current helper and MCP interface have no pagination input" }, + "added": { + "calendar.add_calendar_participant --attendees": "attendeesToAdd (current inherited flag and MCP interface)", + "calendar.add_calendar_participant --calendar-id": "calendarId (current inherited flag and MCP interface)", + "calendar.add_calendar_participant --event": "eventId (current inherited flag and MCP interface)", + "calendar.get_calendar_participants --calendar-id": "calendarId (current inherited flag and MCP interface)", + "calendar.get_calendar_participants --event": "eventId (current inherited flag and MCP interface)", + "calendar.remove_calendar_participant --attendees": "attendeesToRemove (current inherited flag and MCP interface)", + "calendar.remove_calendar_participant --calendar-id": "calendarId (current inherited flag and MCP interface)", + "calendar.remove_calendar_participant --event": "eventId (current inherited flag and MCP interface)" + }, "bindings": { "aisearch.search_enterprise": { "types": "searchTypes" @@ -25,6 +35,11 @@ "event": "eventId", "files": "attachments" }, + "calendar.add_calendar_participant": { + "attendees": "attendeesToAdd", + "calendar-id": "calendarId", + "event": "eventId" + }, "calendar.add_meeting_room": { "event": "eventId", "rooms": "roomIds" @@ -61,6 +76,10 @@ "calendar.get_calendar_detail": { "id": "eventId" }, + "calendar.get_calendar_participants": { + "calendar-id": "calendarId", + "event": "eventId" + }, "calendar.list_calendar_events": { "end": "endTime", "start": "startTime" @@ -80,6 +99,11 @@ "start": "startTime", "users": "userIds" }, + "calendar.remove_calendar_participant": { + "attendees": "attendeesToRemove", + "calendar-id": "calendarId", + "event": "eventId" + }, "calendar.respond": { "id": "eventId", "status": "responseStatus" diff --git a/internal/cli/schema_parameter_bindings_test.go b/internal/cli/schema_parameter_bindings_test.go index 24fe6060..eaa58cda 100644 --- a/internal/cli/schema_parameter_bindings_test.go +++ b/internal/cli/schema_parameter_bindings_test.go @@ -26,13 +26,13 @@ func TestSchemaParameterBindingsMatchEmbeddedCatalog(t *testing.T) { } } } - if count != 308 { - t.Fatalf("active parameter binding count = %d, want 308", count) + if count != 316 { + t.Fatalf("active parameter binding count = %d, want 316", count) } snapshot := runtimeSchemaParameterBindingSnapshot - if snapshot.HistoricalBindingCount != 311 || len(snapshot.Migrations) != 5 || len(snapshot.Excluded) != 3 { - t.Fatalf("binding seed audit = historical:%d migrations:%d excluded:%d", - snapshot.HistoricalBindingCount, len(snapshot.Migrations), len(snapshot.Excluded)) + if snapshot.HistoricalBindingCount != 311 || len(snapshot.Migrations) != 5 || len(snapshot.Excluded) != 3 || len(snapshot.Added) != 8 { + t.Fatalf("binding seed audit = historical:%d migrations:%d excluded:%d added:%d", + snapshot.HistoricalBindingCount, len(snapshot.Migrations), len(snapshot.Excluded), len(snapshot.Added)) } if snapshot.SourceCatalogHash != "sha256:6dc63141d35119ff6095d189ef9d8994cd35162ecf9fff9ad23b2ab68e4e2b7f" { t.Fatalf("binding source catalog hash = %q", snapshot.SourceCatalogHash) @@ -41,11 +41,14 @@ func TestSchemaParameterBindingsMatchEmbeddedCatalog(t *testing.T) { t.Fatalf("calendar.get_calendar --id property = %q, want calendarId", got) } for canonical, flagName := range map[string]string{ - "contact.get_dept_info_by_dept_id": "dept", - "contact.get_dept_members_by_deptId": "depts", - "contact.get_sub_depts_by_dept_id": "dept", - "oa.list_pending_approvals": "limit", - "oa.list_user_visible_process": "limit", + "calendar.add_calendar_participant": "event", + "calendar.get_calendar_participants": "event", + "calendar.remove_calendar_participant": "event", + "contact.get_dept_info_by_dept_id": "dept", + "contact.get_dept_members_by_deptId": "depts", + "contact.get_sub_depts_by_dept_id": "dept", + "oa.list_pending_approvals": "limit", + "oa.list_user_visible_process": "limit", } { if runtimeSchemaParameterBindings[canonical][flagName] == "" { t.Errorf("migrated binding %s --%s is missing", canonical, flagName) diff --git a/internal/cli/schema_parameters_runtime.go b/internal/cli/schema_parameters_runtime.go new file mode 100644 index 00000000..3b9d4fa6 --- /dev/null +++ b/internal/cli/schema_parameters_runtime.go @@ -0,0 +1,256 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package cli + +import ( + "strings" + + "github.com/spf13/cobra" +) + +// RuntimeSchemaParameterMetadata contains reviewed CLI parameter semantics +// that Cobra cannot represent by itself. It is an independent generation +// input: generated Catalog data is never read back into this registry. +type RuntimeSchemaParameterMetadata struct { + Inherited []string + Required []string + RequiredWhen map[string]string + Formats map[string]string + Enums map[string][]string + Examples map[string]string +} + +var runtimeSchemaParameterMetadataByCanonical = map[string]RuntimeSchemaParameterMetadata{} + +// RegisterRuntimeSchemaParameterMetadata records strong, code-owned parameter +// semantics for one canonical command path. +func RegisterRuntimeSchemaParameterMetadata(canonicalPath string, metadata RuntimeSchemaParameterMetadata) { + canonicalPath = strings.TrimSpace(canonicalPath) + if canonicalPath == "" { + return + } + if _, exists := runtimeSchemaParameterMetadataByCanonical[canonicalPath]; exists { + panic("duplicate runtime schema parameter metadata: " + canonicalPath) + } + runtimeSchemaParameterMetadataByCanonical[canonicalPath] = metadata +} + +func applyRuntimeSchemaParameterMetadata(cmd *cobra.Command, canonicalPath string) { + metadata, ok := runtimeSchemaParameterMetadataByCanonical[strings.TrimSpace(canonicalPath)] + if !ok { + return + } + AnnotateRuntimeRequiredFlags(cmd, metadata.Required...) + for flagName, expression := range metadata.RequiredWhen { + AnnotateRuntimeFlagRequiredWhen(cmd, flagName, expression) + } + for flagName, format := range metadata.Formats { + AnnotateRuntimeFlagFormat(cmd, flagName, format) + } + for flagName, values := range metadata.Enums { + AnnotateRuntimeFlagEnum(cmd, flagName, values...) + } + for flagName, example := range metadata.Examples { + AnnotateRuntimeFlagExample(cmd, flagName, example) + } +} + +// RuntimeSchemaParameterMetadataDefinitions returns a defensive copy for +// build-time contract validation. +func RuntimeSchemaParameterMetadataDefinitions() map[string]RuntimeSchemaParameterMetadata { + out := make(map[string]RuntimeSchemaParameterMetadata, len(runtimeSchemaParameterMetadataByCanonical)) + for canonicalPath, metadata := range runtimeSchemaParameterMetadataByCanonical { + copyMetadata := RuntimeSchemaParameterMetadata{ + Inherited: append([]string(nil), metadata.Inherited...), + Required: append([]string(nil), metadata.Required...), + RequiredWhen: cloneRuntimeSchemaStringMap(metadata.RequiredWhen), + Formats: cloneRuntimeSchemaStringMap(metadata.Formats), + Examples: cloneRuntimeSchemaStringMap(metadata.Examples), + Enums: make(map[string][]string, len(metadata.Enums)), + } + for flagName, values := range metadata.Enums { + copyMetadata.Enums[flagName] = append([]string(nil), values...) + } + out[canonicalPath] = copyMetadata + } + return out +} + +func cloneRuntimeSchemaStringMap(source map[string]string) map[string]string { + if source == nil { + return nil + } + out := make(map[string]string, len(source)) + for key, value := range source { + out[key] = value + } + return out +} + +func init() { + RegisterRuntimeSchemaParameterMetadata("aisearch.enterprise_person_search", RuntimeSchemaParameterMetadata{ + Inherited: []string{"keyword", "dimension"}, + Required: []string{"keyword"}, + }) + RegisterRuntimeSchemaParameterMetadata("aitable.record_get", RuntimeSchemaParameterMetadata{ + Required: []string{"record-ids"}, + }) + RegisterRuntimeSchemaParameterMetadata("aitable.view_update_filter", RuntimeSchemaParameterMetadata{ + Required: []string{"json"}, + Formats: map[string]string{"json": "json"}, + Examples: map[string]string{"json": `[{"operator":"and","operands":[{"operator":"eq","operands":["fldX","value"]}]}]`}, + }) + RegisterRuntimeSchemaParameterMetadata("aitable.view_update_group", RuntimeSchemaParameterMetadata{ + Required: []string{"json"}, + Formats: map[string]string{"json": "json"}, + Examples: map[string]string{"json": `[{"fieldId":"fldX","direction":"asc"}]`}, + }) + RegisterRuntimeSchemaParameterMetadata("aitable.view_update_sort", RuntimeSchemaParameterMetadata{ + Required: []string{"json"}, + Formats: map[string]string{"json": "json"}, + Examples: map[string]string{"json": `[{"fieldId":"fldX","direction":"asc"}]`}, + }) + RegisterRuntimeSchemaParameterMetadata("attendance.batch_get_employee_shifts", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"start": "date", "end": "date"}, + }) + RegisterRuntimeSchemaParameterMetadata("calendar.add_attachments", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"files": "file-id-name-list"}, + Examples: map[string]string{"files": "file-smoke:report.pdf"}, + }) + RegisterRuntimeSchemaParameterMetadata("calendar.add_calendar_participant", RuntimeSchemaParameterMetadata{ + Inherited: []string{"event", "calendar-id"}, + }) + RegisterRuntimeSchemaParameterMetadata("calendar.get_calendar_participants", RuntimeSchemaParameterMetadata{ + Inherited: []string{"event", "calendar-id"}, + }) + RegisterRuntimeSchemaParameterMetadata("calendar.remove_calendar_participant", RuntimeSchemaParameterMetadata{ + Inherited: []string{"event", "calendar-id"}, + }) + RegisterRuntimeSchemaParameterMetadata("calendar.respond", RuntimeSchemaParameterMetadata{ + Enums: map[string][]string{"status": {"needsAction", "accepted", "declined", "tentative"}}, + }) + RegisterRuntimeSchemaParameterMetadata("chat.download_media", RuntimeSchemaParameterMetadata{ + Enums: map[string][]string{"type": {"mediaId"}}, + }) + RegisterRuntimeSchemaParameterMetadata("chat.send_personal_message", RuntimeSchemaParameterMetadata{ + RequiredWhen: map[string]string{ + "media-id": "msg-type is image", + "file-path": "msg-type is file or audio or video", + }, + Examples: map[string]string{"media-id": "@lADP_schema_smoke"}, + }) + RegisterRuntimeSchemaParameterMetadata("chat.set_group_member_mute_list", RuntimeSchemaParameterMetadata{ + RequiredWhen: map[string]string{"mute-time": "off is false"}, + Enums: map[string][]string{ + "mute-time": {"300000", "3600000", "86400000", "604800000", "2592000000"}, + }, + }) + RegisterRuntimeSchemaParameterMetadata("chat.update_group_icon", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"icon-media-id": "dingtalk-media-id"}, + Examples: map[string]string{"icon-media-id": "@lADP_schema_smoke"}, + }) + RegisterRuntimeSchemaParameterMetadata("chat.update_show_history_msg_option", RuntimeSchemaParameterMetadata{ + Enums: map[string][]string{"option": {"FORBIDDEN", "RECENT_100", "ALL"}}, + }) + RegisterRuntimeSchemaParameterMetadata("contact.get_dept_info_by_dept_id", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"dept": "numeric-id"}, + }) + RegisterRuntimeSchemaParameterMetadata("contact.get_sub_depts_by_dept_id", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"dept": "numeric-id"}, + }) + RegisterRuntimeSchemaParameterMetadata("dev.apply_dev_app_permissions", RuntimeSchemaParameterMetadata{ + Required: []string{"scope-values"}, + }) + RegisterRuntimeSchemaParameterMetadata("dev.remove_dev_app_permissions", RuntimeSchemaParameterMetadata{ + Required: []string{"scope-values"}, + }) + RegisterRuntimeSchemaParameterMetadata("dev.subscribe_dev_app_events", RuntimeSchemaParameterMetadata{ + Required: []string{"event-codes"}, + }) + RegisterRuntimeSchemaParameterMetadata("dev.unsubscribe_dev_app_events", RuntimeSchemaParameterMetadata{ + Required: []string{"event-codes"}, + }) + RegisterRuntimeSchemaParameterMetadata("minutes.add_member_permission", RuntimeSchemaParameterMetadata{ + Enums: map[string][]string{"policy": {"0", "1", "2", "3", "4"}}, + }) + RegisterRuntimeSchemaParameterMetadata("pat.browser_policy", RuntimeSchemaParameterMetadata{ + Required: []string{"enabled"}, + Enums: map[string][]string{"enabled": {"true", "false"}}, + }) + RegisterRuntimeSchemaParameterMetadata("report.create_report", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"contents": "json"}, + Examples: map[string]string{ + "contents": `[{"content":"schema smoke","sort":"0","key":"work","contentType":"markdown","type":"1"}]`, + }, + }) + + for _, canonicalPath := range []string{ + "sheet.add_dimension", + "sheet.delete_dimension", + "sheet.insert_dimension", + "sheet.move_dimension", + "sheet.update_dimension", + } { + metadata := RuntimeSchemaParameterMetadata{ + Enums: map[string][]string{"dimension": {"ROWS", "COLUMNS"}}, + } + switch canonicalPath { + case "sheet.delete_dimension", "sheet.insert_dimension": + metadata.Formats = map[string]string{"length": "positive-integer"} + metadata.Examples = map[string]string{"position": "1"} + case "sheet.update_dimension": + metadata.Formats = map[string]string{"length": "positive-integer"} + metadata.Examples = map[string]string{"start-index": "1"} + } + RegisterRuntimeSchemaParameterMetadata(canonicalPath, metadata) + } + RegisterRuntimeSchemaParameterMetadata("sheet.create_cond_format", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"ranges": "json", "condition": "json", "cell-style": "json", "data-bar-style": "json"}, + Examples: map[string]string{ + "ranges": `["A1:B2"]`, + "condition": `{"numberCondition":{"operator":"greater","value1":"80"}}`, + "cell-style": `{"backgroundColor":"#FFCDD2","bold":true}`, + }, + }) + RegisterRuntimeSchemaParameterMetadata("sheet.update_cond_format", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"ranges": "json", "condition": "json", "cell-style": "json", "data-bar-style": "json"}, + Examples: map[string]string{ + "ranges": `["A1:B2"]`, + "condition": `{"numberCondition":{"operator":"greater","value1":"80"}}`, + "cell-style": `{"backgroundColor":"#FFCDD2","bold":true}`, + }, + }) + RegisterRuntimeSchemaParameterMetadata("sheet.range_set_style", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{ + "bg-colors-json": "json", + "font-colors-json": "json", + "font-sizes-json": "json", + "font-weights-json": "json", + "h-aligns-json": "json", + "v-aligns-json": "json", + }, + Enums: map[string][]string{ + "font-weight": {"bold", "normal"}, + "h-align": {"left", "center", "right", "general"}, + "v-align": {"top", "middle", "bottom"}, + "word-wrap": {"overflow", "clip", "autoWrap"}, + }, + Examples: map[string]string{ + "bg-colors-json": `[["#ffffff","#ffffff"],["#ffffff","#ffffff"]]`, + "font-colors-json": `[["#000000","#000000"],["#000000","#000000"]]`, + "font-sizes-json": `[[12,12],[12,12]]`, + "font-weights-json": `[["normal","normal"],["normal","normal"]]`, + "h-aligns-json": `[["left","left"],["left","left"]]`, + "v-aligns-json": `[["top","top"],["top","top"]]`, + }, + }) + RegisterRuntimeSchemaParameterMetadata("sheet.set_dropdown_lists", RuntimeSchemaParameterMetadata{ + Formats: map[string]string{"options": "json"}, + Examples: map[string]string{"options": `[{"value":"option1"},{"value":"option2"}]`}, + }) + RegisterRuntimeSchemaParameterMetadata("wiki.search_wikiSpaces", RuntimeSchemaParameterMetadata{ + Enums: map[string][]string{"type": {"myWikiSpace"}}, + }) +} diff --git a/internal/helpers/catalog_fallback.go b/internal/helpers/catalog_fallback.go index 2372a794..cead889a 100644 --- a/internal/helpers/catalog_fallback.go +++ b/internal/helpers/catalog_fallback.go @@ -166,6 +166,7 @@ func registerCatalogFallbackFlag(cmd *cobra.Command, parameter cli.CatalogParame interfaceType = parameter.Type } cli.AnnotateRuntimeFlag(cmd, parameter.Name, parameter.Property, interfaceType, parameter.Required, parameter.Default) + cli.AnnotateRuntimeFlagRequiredWhen(cmd, parameter.Name, parameter.RequiredWhen) if parameter.Format != "" { cli.AnnotateRuntimeFlagFormat(cmd, parameter.Name, parameter.Format) } diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 5b00bc64..9c7af9a7 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -2475,7 +2475,7 @@ func newChatCommand() *cobra.Command { RequireOneOf: [][]string{{"group", "user", "open-dingtalk-id"}}, }) cli.AnnotateRuntimePositionals(chatMessageSendCmd, cli.RuntimeSchemaPositional{ - Name: "text", + Name: "content", Type: "string", Description: "消息内容(也可使用 --text;富媒体消息可省略)", Required: false, diff --git a/scripts/dev/schema_command_smoke.py b/scripts/dev/schema_command_smoke.py index bf01dea3..ddb5a894 100755 --- a/scripts/dev/schema_command_smoke.py +++ b/scripts/dev/schema_command_smoke.py @@ -85,6 +85,8 @@ def scalar_value(flag: str, param: dict[str, Any], canonical_path: str) -> str: return str(enum[0]) value_format = str(param.get("format", "")).lower() + + is_end = flag == "end" or flag.startswith("end-") or flag.endswith("-end") if value_format in {"numeric-id", "positive-integer"}: return "1" if value_format == "a1-range": @@ -92,9 +94,9 @@ def scalar_value(flag: str, param: dict[str, Any], canonical_path: str) -> str: if value_format == "file-path": return str(Path(tempfile.gettempdir()) / "dws-schema-smoke-fixture.txt") if value_format == "date-time": - return "2027-01-15T10:00:00+08:00" + return "2027-01-15T11:00:00+08:00" if is_end else "2027-01-15T10:00:00+08:00" if value_format == "date": - return "2027-01-15" + return "2027-01-16" if is_end else "2027-01-15" if "default" in param and param["default"] not in (None, ""): return str(param["default"]) @@ -108,7 +110,7 @@ def scalar_value(flag: str, param: dict[str, Any], canonical_path: str) -> str: ) if flag in {"start", "end", "start-time", "end-time"}: - return "2027-01-15T10:00:00+08:00" + return "2027-01-15T11:00:00+08:00" if is_end else "2027-01-15T10:00:00+08:00" if flag == "fields": return '[{"fieldName":"Name","type":"text"}]' @@ -191,6 +193,8 @@ def value_for(flag: str, param: dict[str, Any], canonical_path: str) -> str | No 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") + if canonical_path == "report.create_report" and flag == "contents-file": + return "tmp/dws-schema-smoke-report.json" if param.get("example") not in (None, ""): return str(param["example"]) @@ -284,6 +288,8 @@ def required_when_matches( actual_value = "true" if actual is None else str(actual) elif controller_param.get("default") not in (None, ""): actual_value = str(controller_param["default"]) + elif str(controller_param.get("type", "")).lower() == "boolean": + actual_value = "false" else: return False expected = [item.strip() for item in re.split(r"\s+or\s+", expected_raw)] @@ -424,12 +430,12 @@ def help_flags(binary: str, cwd: Path, cli_path: str, timeout: int) -> set[str]: f"{binary} {cli_path} --help exited {proc.returncode}: " f"{(proc.stderr or proc.stdout).strip()}" ) - flags_match = re.search( - r"(?ms)^Flags:\s*\n(?P.*?)(?:^Global Flags:\s*$|\Z)", - proc.stdout, + return set( + re.findall( + r"(?m)^\s*(?:-[a-zA-Z0-9],\s*)?--([a-zA-Z0-9][a-zA-Z0-9_.-]*)(?:[ =]|$)", + proc.stdout, + ) ) - flags_body = flags_match.group("body") if flags_match else "" - return set(re.findall(r"--([a-zA-Z0-9][a-zA-Z0-9_.-]*)", flags_body)) def is_auth_required(output: str) -> bool: @@ -660,6 +666,13 @@ def main() -> int: '[{"sheetId":"sheet-smoke","range":"A1","fontSize":12}]\n', encoding="utf-8", ) + report_fixture = cwd / "tmp" / "dws-schema-smoke-report.json" + report_fixture.parent.mkdir(parents=True, exist_ok=True) + report_fixture.write_text( + '[{"content":"schema smoke","sort":"0","key":"work",' + '"contentType":"markdown","type":"1"}]\n', + encoding="utf-8", + ) if args.path: paths = args.path diff --git a/scripts/policy/check-schema-catalog.sh b/scripts/policy/check-schema-catalog.sh index dc17a2b6..e4f3aba6 100755 --- a/scripts/policy/check-schema-catalog.sh +++ b/scripts/policy/check-schema-catalog.sh @@ -71,18 +71,28 @@ if ! jq -e ' exit 1 fi -if [ "$(jq '[.tools[] | select(.constraints != null)] | length' internal/cli/schema_catalog.json)" != "21" ]; then - printf '%s\n' 'schema command constraints are incomplete' >&2 +if ! jq -e ' + [.tools[] | select(.constraints != null)] as $tools | + ($tools | length) >= 21 and + all($tools[]; + (((.parameters // {}) | keys) + ((.positionals // []) | map(.name))) as $names | + all((.constraints.mutually_exclusive // [])[]; length > 1 and all(.[]; IN($names[]))) and + all((.constraints.require_one_of // [])[]; length > 1 and all(.[]; IN($names[]))) and + all((.constraints.require_together // [])[]; length > 1 and all(.[]; IN($names[]))) + ) +' internal/cli/schema_catalog.json >/dev/null; then + printf '%s\n' 'schema command constraints are incomplete or reference unknown parameters' >&2 exit 1 fi binding_count="$(jq '[.bindings[] | length] | add' internal/cli/schema_parameter_bindings.json)" -if [ "$binding_count" != "308" ] || ! jq -e --slurpfile bindings internal/cli/schema_parameter_bindings.json ' +if [ "$binding_count" != "316" ] || ! jq -e --slurpfile bindings internal/cli/schema_parameter_bindings.json ' . as $catalog | - $bindings[0].version == 1 and + $bindings[0].version == 2 and $bindings[0].historical_binding_count == 311 and ($bindings[0].migrations | length) == 5 and ($bindings[0].excluded | length) == 3 and + ($bindings[0].added | length) == 8 and ([$bindings[0].bindings | to_entries[] | .key as $tool | .value | to_entries[] | {tool: $tool, flag: .key, property: .value} diff --git a/test/scripts/package_script_test.go b/test/scripts/package_script_test.go index 9809673c..227bd770 100644 --- a/test/scripts/package_script_test.go +++ b/test/scripts/package_script_test.go @@ -1,6 +1,9 @@ package scripts_test import ( + "archive/tar" + "archive/zip" + "compress/gzip" "os" "os/exec" "path/filepath" @@ -28,9 +31,8 @@ var expectedPackagedSkillTargets = []string{ ".hermes/skills/dws", } -// seedDistArtifacts creates fake goreleaser output archives (empty tar.gz/zip -// files) and a checksums.txt stub so that post-goreleaser.sh can run without -// an actual goreleaser build. +// seedDistArtifacts creates minimal valid goreleaser archives and a checksum +// stub so post-goreleaser can exercise unpacking and Darwin ad-hoc signing. func seedDistArtifacts(t *testing.T, distDir string, targets []string) { t.Helper() if err := os.MkdirAll(distDir, 0o755); err != nil { @@ -39,9 +41,7 @@ func seedDistArtifacts(t *testing.T, distDir string, targets []string) { for _, target := range targets { p := filepath.Join(distDir, target) - if err := os.WriteFile(p, []byte("fake-archive"), 0o644); err != nil { - t.Fatalf("WriteFile(%s) error = %v", p, err) - } + seedDistArchive(t, p) } // Create empty checksums.txt (goreleaser creates this) @@ -55,6 +55,50 @@ func seedDistArtifacts(t *testing.T, distDir string, targets []string) { } } +func seedDistArchive(t *testing.T, path string) { + t.Helper() + file, err := os.Create(path) + if err != nil { + t.Fatalf("Create(%s) error = %v", path, err) + } + defer file.Close() + + content := []byte("#!/bin/sh\nexit 0\n") + switch { + case strings.HasSuffix(path, ".tar.gz"): + gzipWriter := gzip.NewWriter(file) + tarWriter := tar.NewWriter(gzipWriter) + if err := tarWriter.WriteHeader(&tar.Header{Name: "dws", Mode: 0o755, Size: int64(len(content))}); err != nil { + t.Fatalf("WriteHeader(%s) error = %v", path, err) + } + if _, err := tarWriter.Write(content); err != nil { + t.Fatalf("Write(%s) error = %v", path, err) + } + if err := tarWriter.Close(); err != nil { + t.Fatalf("Close tar(%s) error = %v", path, err) + } + if err := gzipWriter.Close(); err != nil { + t.Fatalf("Close gzip(%s) error = %v", path, err) + } + case strings.HasSuffix(path, ".zip"): + zipWriter := zip.NewWriter(file) + header := &zip.FileHeader{Name: "dws.exe", Method: zip.Store} + header.SetMode(0o755) + entry, err := zipWriter.CreateHeader(header) + if err != nil { + t.Fatalf("CreateHeader(%s) error = %v", path, err) + } + if _, err := entry.Write(content); err != nil { + t.Fatalf("Write(%s) error = %v", path, err) + } + if err := zipWriter.Close(); err != nil { + t.Fatalf("Close zip(%s) error = %v", path, err) + } + default: + t.Fatalf("unsupported archive path %s", path) + } +} + func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { t.Parallel() @@ -79,6 +123,7 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+distDir, + "DWS_PACKAGE_VERSION=1.2.3", "DWS_RELEASE_BASE_URL=https://downloads.example.com/dws/releases/v1.2.3", ) output, err := cmd.CombinedOutput() @@ -201,6 +246,7 @@ func TestPostGoreleaserAllPlatformNpmAssets(t *testing.T) { cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+distDir, + "DWS_PACKAGE_VERSION=9.9.9", "DWS_RELEASE_BASE_URL=https://downloads.example.com/dws/releases/v9.9.9", ) output, err := cmd.CombinedOutput() @@ -281,6 +327,7 @@ func TestPostGoreleaserSkillsZipLayout(t *testing.T) { cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+distDir, + "DWS_PACKAGE_VERSION=0.0.0", "DWS_RELEASE_BASE_URL=https://downloads.example.com/dws/releases/v0.0.0", ) output, err := cmd.CombinedOutput()