fix(schema): restore ParamDecl mappings and drop messages-send RequiredWhen

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
玉澜
2026-08-01 12:00:47 +08:00
co-authored by Cursor
parent 31cade34ff
commit 066094a68c
45 changed files with 2420 additions and 275 deletions
+2
View File
@@ -14,6 +14,8 @@ func TestCrossPlatformCoverageRuntimeAnnotationAPIsCoverage(t *testing.T) {
AnnotateRuntimeFlag(nil, "x", "x", "string", false, "")
AnnotateRuntimeFlagProperty(nil, "x", "x")
AnnotateRuntimeRequiredFlags(nil, "x")
AnnotateRuntimeFlagRequiredValue(nil, "x", false)
AnnotateRuntimeFlagInterfaceType(nil, "x", "string")
AnnotateRuntimeFlagRequiredWhen(nil, "x", "when")
AnnotateRuntimeFlagFormat(nil, "x", "uri")
AnnotateRuntimeFlagEnum(nil, "x", "a")
@@ -0,0 +1,53 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"io"
"os"
"strconv"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/catalogcodegen"
)
func TestCrossPlatformCoverageProbeMainPrintsToolTotals(t *testing.T) {
if len(catalogcodegen.Tools) == 0 {
t.Fatal("generated catalog tools must be non-empty")
}
old := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
main()
_ = w.Close()
os.Stdout = old
var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
t.Fatal(err)
}
_ = r.Close()
fields := strings.Fields(strings.TrimSpace(buf.String()))
if len(fields) != 2 {
t.Fatalf("main output = %q, want \"<tools> <total>\"", buf.String())
}
tools, err := strconv.Atoi(fields[0])
if err != nil || tools != len(catalogcodegen.Tools) {
t.Fatalf("tool count = %q, want %d", fields[0], len(catalogcodegen.Tools))
}
}
+26
View File
@@ -171,6 +171,32 @@ func TestCrossPlatformCoverageContractFinalNilCommandGuards(t *testing.T) {
ClearRuntimeContractFinalForTest(nil)
}
func TestCrossPlatformCoverageApplyParamDeclsSkipsBlankAndAnnotatesEnum(t *testing.T) {
cmd := &cobra.Command{Use: "apply-params"}
cmd.Flags().String("mode", "", "mode")
required := false
ApplyParamDecls(cmd, []ParamDecl{
{Name: " "}, // blank names are skipped
{
Name: "mode", Property: "mode", Required: &required,
InterfaceType: "string", Description: "mode desc",
RequiredWhen: "when create", Enum: []string{"a", "b"},
},
})
flag := cmd.Flags().Lookup("mode")
if flag == nil {
t.Fatal("missing mode flag")
}
if got := flag.Annotations["dws.schema.property"]; len(got) == 0 || got[0] != "mode" {
t.Fatalf("property = %#v", flag.Annotations["dws.schema.property"])
}
if got := flag.Annotations["x-cli-enum"]; len(got) != 2 || got[0] != "a" || got[1] != "b" {
t.Fatalf("enum = %#v", flag.Annotations["x-cli-enum"])
}
ApplyParamDecls(nil, []ParamDecl{{Name: "mode"}})
ApplyParamDecls(cmd, nil)
}
func TestCrossPlatformCoverageRuntimeContractFinalRejectsForeignStoredValue(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) })
+1 -1
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"source_hash": "sha256:d1cf513fe3999ea4affd2218ee54f0ce746c0b267ce156587b5836ea3a8dc611",
"source_hash": "sha256:26f44334e2182ab20b691e9e6b6221288d4ff9a3c9e13e436b4b52a98eae70b8",
"catalog": {
"agent_metadata": {
"products_with_metadata": 26,
+223 -61
View File
@@ -5010,8 +5010,14 @@
"required": {
"candidates": [
{
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": true
},
{
"precedence": "tool_schema_hint",
"selected": false,
"source": "tool_schema_hint",
"value": true
},
@@ -5034,9 +5040,9 @@
"value": false
}
],
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "tool_schema_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -14285,8 +14291,14 @@
"interface_type": {
"candidates": [
{
"precedence": "mcp_metadata",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "object"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "object"
},
@@ -14297,9 +14309,9 @@
"value": "string"
}
],
"precedence": "mcp_metadata",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "mcp_metadata",
"source": "native_annotation",
"value": "object"
},
"property": {
@@ -14319,8 +14331,14 @@
"required": {
"candidates": [
{
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": true
},
{
"precedence": "tool_schema_hint",
"selected": false,
"source": "tool_schema_hint",
"value": true
},
@@ -14337,9 +14355,9 @@
"value": false
}
],
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "tool_schema_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -39016,8 +39034,14 @@
"interface_type": {
"candidates": [
{
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "array"
},
{
"precedence": "tool_schema_hint",
"selected": false,
"source": "tool_schema_hint",
"value": "array"
},
@@ -39034,23 +39058,29 @@
"value": "string"
}
],
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "tool_schema_hint",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "fieldIds"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "fieldIds"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"source": "native_annotation",
"value": "fieldIds"
},
"required": {
@@ -39140,8 +39170,14 @@
"interface_type": {
"candidates": [
{
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "object"
},
{
"precedence": "tool_schema_hint",
"selected": false,
"source": "tool_schema_hint",
"value": "object"
},
@@ -39158,23 +39194,29 @@
"value": "string"
}
],
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "tool_schema_hint",
"source": "native_annotation",
"value": "object"
},
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "filters"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "filters"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"source": "native_annotation",
"value": "filters"
},
"required": {
@@ -39479,6 +39521,12 @@
"source": "versioned_parameter_binding",
"value": "keyword"
},
{
"precedence": "native_annotation",
"selected": false,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
@@ -39577,8 +39625,14 @@
"interface_type": {
"candidates": [
{
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "array"
},
{
"precedence": "tool_schema_hint",
"selected": false,
"source": "tool_schema_hint",
"value": "array"
},
@@ -39595,23 +39649,29 @@
"value": "string"
}
],
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "tool_schema_hint",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "recordIds"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "recordIds"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"source": "native_annotation",
"value": "recordIds"
},
"required": {
@@ -39707,8 +39767,14 @@
"interface_type": {
"candidates": [
{
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "array"
},
{
"precedence": "tool_schema_hint",
"selected": false,
"source": "tool_schema_hint",
"value": "array"
},
@@ -39725,23 +39791,29 @@
"value": "string"
}
],
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "tool_schema_hint",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "sort"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "sort"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"source": "native_annotation",
"value": "sort"
},
"required": {
@@ -42577,8 +42649,14 @@
"interface_type": {
"candidates": [
{
"precedence": "mcp_metadata",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "array"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "array"
},
@@ -42589,23 +42667,29 @@
"value": "string"
}
],
"precedence": "mcp_metadata",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "mcp_metadata",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "fieldIds"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "fieldIds"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"source": "native_annotation",
"value": "fieldIds"
},
"required": {
@@ -42689,8 +42773,14 @@
"interface_type": {
"candidates": [
{
"precedence": "mcp_metadata",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "array"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "array"
},
@@ -42701,23 +42791,29 @@
"value": "string"
}
],
"precedence": "mcp_metadata",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "mcp_metadata",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "recordIds"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "recordIds"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"source": "native_annotation",
"value": "recordIds"
},
"required": {
@@ -48026,8 +48122,14 @@
"interface_type": {
"candidates": [
{
"precedence": "mcp_metadata",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "array"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "array"
},
@@ -48038,9 +48140,9 @@
"value": "string"
}
],
"precedence": "mcp_metadata",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "mcp_metadata",
"source": "native_annotation",
"value": "array"
},
"property": {
@@ -48060,8 +48162,14 @@
"required": {
"candidates": [
{
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": true
},
{
"precedence": "tool_schema_hint",
"selected": false,
"source": "tool_schema_hint",
"value": true
},
@@ -48084,9 +48192,9 @@
"value": false
}
],
"precedence": "tool_schema_hint",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "tool_schema_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -57845,16 +57953,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -57899,7 +58013,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": false,
"type": "string"
},
@@ -61796,6 +61910,26 @@
"source": "cobra_usage",
"value": "结构化过滤条件 JSON(可选)"
},
"interface_type": {
"candidates": [
{
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "object"
},
{
"precedence": "fallback",
"selected": false,
"source": "cobra_flag_type",
"value": "string"
}
],
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "native_annotation",
"value": "object"
},
"property": {
"candidates": [
{
@@ -61853,6 +61987,7 @@
"value": "string"
}
},
"interface_type": "object",
"property": "filters",
"required": false,
"type": "string"
@@ -61973,16 +62108,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -62027,7 +62168,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": false,
"type": "string"
},
@@ -62138,6 +62279,26 @@
"source": "cobra_usage",
"value": "排序条件 JSON 数组(可选)"
},
"interface_type": {
"candidates": [
{
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "array"
},
{
"precedence": "fallback",
"selected": false,
"source": "cobra_flag_type",
"value": "string"
}
],
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
@@ -62195,6 +62356,7 @@
"value": "string"
}
},
"interface_type": "array",
"property": "sort",
"required": false,
"type": "string"
+41 -37
View File
@@ -54493,16 +54493,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -54553,7 +54559,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
}
@@ -66200,16 +66206,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -66260,7 +66272,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
}
@@ -77948,23 +77960,17 @@
},
"required_when": {
"candidates": [
{
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "identity=bot"
},
{
"precedence": "default",
"selected": false,
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "native_annotation",
"precedence": "default",
"resolution": "highest_precedence",
"source": "native_annotation",
"value": "identity=bot"
"source": "default",
"value": ""
},
"type": {
"candidates": [
@@ -77983,7 +77989,6 @@
},
"property": "robotCode",
"required": false,
"required_when": "identity=bot",
"type": "string"
},
"text": {
@@ -78465,23 +78470,17 @@
},
"required_when": {
"candidates": [
{
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "identity=webhook"
},
{
"precedence": "default",
"selected": false,
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "native_annotation",
"precedence": "default",
"resolution": "highest_precedence",
"source": "native_annotation",
"value": "identity=webhook"
"source": "default",
"value": ""
},
"type": {
"candidates": [
@@ -78500,7 +78499,6 @@
},
"property": "webhookToken",
"required": false,
"required_when": "identity=webhook",
"type": "string"
}
},
@@ -84012,16 +84010,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -84072,7 +84076,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": false,
"type": "string"
},
+11 -5
View File
@@ -17797,16 +17797,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -17863,7 +17869,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
}
+27 -21
View File
@@ -19394,19 +19394,13 @@
"parameter_count": 2,
"parameters": {
"name": {
"description": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名",
"description": "新名称 (必填;原样传给服务端,不做扩展名规范化;如需根据节点类型和当前后缀规范化,请使用 drive rename)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
},
{
"precedence": "cobra_contract",
"selected": false,
"selected": true,
"source": "cobra_usage",
"value": "新名称 (必填;原样传给服务端,不做扩展名规范化;如需根据节点类型和当前后缀规范化,请使用 drive rename)"
},
@@ -19423,10 +19417,10 @@
"value": ""
}
],
"precedence": "native_annotation",
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "native_annotation",
"value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
"source": "cobra_usage",
"value": "新名称 (必填;原样传给服务端,不做扩展名规范化;如需根据节点类型和当前后缀规范化,请使用 drive rename)"
},
"property": {
"candidates": [
@@ -27250,16 +27244,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -27316,7 +27316,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
}
@@ -29725,16 +29725,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -29779,7 +29785,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": false,
"type": "string"
},
+44 -20
View File
@@ -19620,13 +19620,19 @@
"parameter_count": 2,
"parameters": {
"name": {
"description": "新名称 (必填;实际执行时仅去掉与节点当前扩展名完全匹配的一层后缀)",
"description": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
},
{
"precedence": "cobra_contract",
"selected": false,
"source": "cobra_usage",
"value": "新名称 (必填;实际执行时仅去掉与节点当前扩展名完全匹配的一层后缀)"
},
@@ -19643,10 +19649,10 @@
"value": ""
}
],
"precedence": "cobra_contract",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "新名称 (必填;实际执行时仅去掉与节点当前扩展名完全匹配的一层后缀)"
"source": "native_annotation",
"value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
},
"property": {
"candidates": [
@@ -22966,16 +22972,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -23032,7 +23044,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
}
@@ -26074,16 +26086,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -26140,7 +26158,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
},
@@ -26731,16 +26749,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -26797,7 +26821,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
}
+22 -10
View File
@@ -13155,16 +13155,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "size"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "limit"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "limit"
"source": "native_annotation",
"value": "size"
},
"required": {
"candidates": [
@@ -13215,7 +13221,7 @@
"value": "string"
}
},
"property": "limit",
"property": "size",
"required": false,
"type": "string"
}
@@ -20954,16 +20960,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -21020,7 +21032,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
}
+33 -15
View File
@@ -18344,16 +18344,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -18398,7 +18404,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": false,
"type": "string"
}
@@ -18966,16 +18972,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -19020,7 +19032,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": false,
"type": "string"
}
@@ -19588,16 +19600,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -19642,7 +19660,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": false,
"type": "string"
}
+11 -5
View File
@@ -9794,16 +9794,22 @@
"property": {
"candidates": [
{
"precedence": "inference",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "keyword"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "query"
}
],
"precedence": "inference",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "flag_name_inference",
"value": "query"
"source": "native_annotation",
"value": "keyword"
},
"required": {
"candidates": [
@@ -9860,7 +9866,7 @@
"value": "string"
}
},
"property": "query",
"property": "keyword",
"required": true,
"type": "string"
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -305,6 +305,7 @@ func TestCrossPlatformCoverageSchemaDeclEmptyReportsEveryAuthoredSection(t *test
"title": {Title: "T"},
"description": {Description: "D"},
"positionals": {Positionals: []PositionalDecl{{Name: "id"}}},
"parameters": {Parameters: []ParamDecl{{Name: "mode"}}},
"dry_run": {DryRun: &DryRunDecl{PreviewKind: "request"}},
"interface": {Interface: &InterfaceDecl{Mode: "mcp"}},
"selection": {Selection: SelectionDecl{Tips: []string{"tip"}}},
+18 -1
View File
@@ -1919,7 +1919,13 @@ newFieldName、config、aiConfig 至少传入一项。
Examples: []string{"dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID>"},
},
Parameters: []corecmd.ParamDecl{
{Name: "record-ids", Required: boolPtr(false)},
// query→keyword is also in versioned bindings; keep a leaf-local
// ParamDecl so the mapping survives binding/hint churn.
{Name: "query", Property: "keyword"},
{Name: "record-ids", Property: "recordIds", Required: boolPtr(false), InterfaceType: "array"},
{Name: "field-ids", Property: "fieldIds", InterfaceType: "array"},
{Name: "filters", Property: "filters", InterfaceType: "object"},
{Name: "sort", Property: "sort", InterfaceType: "array"},
},
},
})
@@ -2424,6 +2430,9 @@ Windows 用户注意:如果 --records JSON 很长,请使用 --records-file
AvoidWhen: []string{"纯新建用 create;纯更新用 update"},
Examples: []string{"dws aitable record upsert --base-id <BASE_ID> --table-id <TABLE_ID> --records '[{\"cells\":{\"fldXXX\":\"值\"}}]'"},
},
Parameters: []corecmd.ParamDecl{
{Name: "records", Required: boolPtr(true), InterfaceType: "array"},
},
},
})
@@ -2607,6 +2616,11 @@ fieldId 必须是 primaryDoc 类型的字段。`,
AvoidWhen: []string{"禁止改用 drive upload;电子表格附件用 sheet media-upload"},
Examples: []string{"dws aitable attachment upload --base-id BASE_ID --file-name report.xlsx --size 204800"},
},
// size is validated in RunE (Int64); publish required via ParamDecl
// instead of relying on RegisterSchemaHints tool_schema_hint.
Parameters: []corecmd.ParamDecl{
{Name: "size", Required: boolPtr(true)},
},
},
})
@@ -4954,6 +4968,9 @@ layout 数组里每项含图表的新位置(row/col/width/height)。`,
"dws aitable chart update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID --layout '{\"x\":0,\"y\":4,\"w\":12,\"h\":4}'",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "config", Required: boolPtr(true), InterfaceType: "object"},
},
},
})
+3 -3
View File
@@ -1978,9 +1978,9 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。
},
Examples: []string{"dws doc rename --node <DOC_ID> --name \"新名称\" --format json"},
},
Parameters: []corecmd.ParamDecl{
{Name: "name", Description: "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"},
},
// No ParamDecl for --name here: the extension-stripping description
// belongs to drive rename (shared RPC rename_document). doc rename
// keeps the Cobra usage ("原样传给服务端").
},
})
+8
View File
@@ -976,6 +976,9 @@ func newDriveCommand() *cobra.Command {
"dws drive upload --file ./README.md --node <dentryUuid> --format json",
},
},
// Composite multi-step leaf (get_upload_info → PUT → commit_upload):
// no single RPCName / interface_ref. Keep --node→nodeId on this leaf
// only; do not hang it on upload-info or commit.
Parameters: []corecmd.ParamDecl{
{Name: "node", Property: "nodeId", Required: boolPtr(false)},
},
@@ -1451,6 +1454,11 @@ func newDriveCommand() *cobra.Command {
},
Examples: []string{"dws drive rename --node <ID> --name \"新名称\" --format json"},
},
// Shared RPC rename_document with doc rename; keep this description
// on the drive leaf only (doc rename does not strip extensions).
Parameters: []corecmd.ParamDecl{
{Name: "name", Description: "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"},
},
},
})
driveRenameCmd.Flags().String("node", "", "文档/文件 ID 或 URL (必填)")
+6
View File
@@ -327,6 +327,12 @@ func newMailCommand() *cobra.Command {
"dws mail message list --email user@company.com --folder-id 1 --limit 50",
},
},
// Composite wrapper maps --limit onto search_emails.size (same RPC as
// mail message search). Declare here so clearing hints does not fall
// back to flag_name_inference → "limit".
Parameters: []corecmd.ParamDecl{
{Name: "limit", Property: "size"},
},
},
})
+3 -1
View File
@@ -90,7 +90,9 @@ func newMarkdownFetchCmd() *cobra.Command {
Examples: []string{"dws markdown fetch --node <nodeId>"},
},
Parameters: []corecmd.ParamDecl{
{Name: "id", Property: "nodeId", Required: boolPtr(false)},
// --id remains a hidden Cobra compat alias (flagOrFallback), but
// runtime schema skips Hidden flags — do not ParamDecl it or it
// suggests a published Schema surface that 87910880 never had.
{Name: "node", Property: "nodeId", Required: boolPtr(true)},
{Name: "output", Property: "output", Required: boolPtr(false)},
{Name: "space-id", Property: "spaceId", Required: boolPtr(false)},
+7
View File
@@ -355,6 +355,13 @@ func newReportCommand() *cobra.Command {
RunE: withReportDeprecationWarning("create", "entry submit", runReportCreate),
}
addReportCreateFlags(createCmd)
// Mirror entry submit's reviewed --to-chat ParamDecl onto the deprecated
// create alias. Schema assembly still reads ContractFinal from the primary
// leaf; this keeps compatibility-equivalence NativeRequired annotations
// symmetric instead of relying only on one-sided tolerance.
cli.ApplyParamDecls(createCmd, []cli.ParamDecl{
{Name: "to-chat", Required: boolPtr(false)},
})
detailCmd := &cobra.Command{
Use: "detail",
+9
View File
@@ -598,6 +598,15 @@ var RecordQuery = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws aitable +record-query --base-id B --table-id T --query \"关键词\" --limit 50"},
},
// Execute maps --query → MCP keyword (not inferable); JSON flags keep
// wire types so Agents do not treat comma-strings as opaque text.
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
{Name: "filters", InterfaceType: "object"},
{Name: "sort", InterfaceType: "array"},
{Name: "record-ids", InterfaceType: "array"},
{Name: "field-ids", InterfaceType: "array"},
},
},
Flags: []shortcut.Flag{
{Name: "base-id", Type: shortcut.FlagString, Desc: "Base ID", Required: true},
@@ -0,0 +1,40 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package aitable
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageQueryToKeywordParamDecls(t *testing.T) {
// Force package-level Schema Parameter blocks into the coverage graph and
// pin Execute's --query → MCP keyword mapping for Schema publication.
assertQueryKeywordParamDecl(t, "+record-query", RecordQuery)
spec := shortcut.FromShortcut(RecordQuery)
if len(spec.Schema.Parameters) == 0 {
t.Fatal("+record-query Schema.Parameters empty after FromShortcut")
}
}
func assertQueryKeywordParamDecl(t *testing.T, name string, sc shortcut.Shortcut) {
t.Helper()
for _, p := range sc.Schema.Parameters {
if p.Name == "query" && p.Property == "keyword" {
return
}
}
t.Fatalf("%s missing ParamDecl query→keyword; got %#v", name, sc.Schema.Parameters)
}
+4
View File
@@ -154,6 +154,10 @@ var BotFind = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws chat +bot-find --query \"日报\""},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词"},
+4
View File
@@ -48,6 +48,10 @@ var ChatSearch = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws chat +chat-search --query \"项目冲刺\""},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "群名称关键词"},
@@ -0,0 +1,45 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chat
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageQueryToKeywordParamDecls(t *testing.T) {
for _, tc := range []struct {
name string
sc shortcut.Shortcut
}{
{"+chat-search", ChatSearch},
{"+bot-find", BotFind},
} {
assertQueryKeywordParamDecl(t, tc.name, tc.sc)
if len(shortcut.FromShortcut(tc.sc).Schema.Parameters) == 0 {
t.Fatalf("%s Schema.Parameters empty after FromShortcut", tc.name)
}
}
}
func assertQueryKeywordParamDecl(t *testing.T, name string, sc shortcut.Shortcut) {
t.Helper()
for _, p := range sc.Schema.Parameters {
if p.Name == "query" && p.Property == "keyword" {
return
}
}
t.Fatalf("%s missing ParamDecl query→keyword; got %#v", name, sc.Schema.Parameters)
}
+15 -7
View File
@@ -204,16 +204,26 @@ func validateResourceDownloadOutputFlag(output, flagName string) error {
if output == "" {
return apperrors.NewValidation(flagName + " 不能为空")
}
if filepath.IsAbs(output) {
// Reject OS-absolute paths and Unix-rooted forms ("/x") even on Windows,
// where filepath.IsAbs("/x") is false but the path is still not a cwd-relative output.
if filepath.IsAbs(output) || strings.HasPrefix(filepath.ToSlash(output), "/") {
return apperrors.NewValidation(flagName + " 只接受工作目录内的相对路径")
}
clean := filepath.Clean(output)
if clean == ".." || strings.HasPrefix(clean, ".."+string(os.PathSeparator)) {
clean := filepath.Clean(filepath.FromSlash(output))
if resourcePathEscapesBase(clean) {
return apperrors.NewValidation(flagName + " 不允许使用 .. 逃逸工作目录")
}
return nil
}
// resourcePathEscapesBase reports whether rel escapes its base directory.
// Rel values may use '/' or the OS separator (for example mocked filepath.Rel
// results), so normalize before comparing against "..".
func resourcePathEscapesBase(rel string) bool {
rel = filepath.Clean(filepath.FromSlash(strings.TrimSpace(rel)))
return rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator))
}
func resourceDownloadInfo(data map[string]any) (string, map[string]string, error) {
for {
result, ok := data["result"].(map[string]any)
@@ -364,8 +374,7 @@ func resolveResourceDownloadPath(
return "", "", apperrors.NewInternal(fmt.Sprintf("解析输出目录失败: %v", err))
}
parentRel, err := resourceRel(realBase, realParent)
if err != nil || parentRel == ".." ||
strings.HasPrefix(parentRel, ".."+string(os.PathSeparator)) {
if err != nil || resourcePathEscapesBase(parentRel) {
return "", "", apperrors.NewValidation("--output 解析后逃逸工作目录")
}
@@ -393,8 +402,7 @@ func resolveResourceDownloadPath(
func ensureResourceDownloadParent(baseDir, parent string) error {
relative, err := resourceRel(baseDir, parent)
if err != nil || relative == ".." ||
strings.HasPrefix(relative, ".."+string(os.PathSeparator)) {
if err != nil || resourcePathEscapesBase(relative) {
return apperrors.NewValidation("--output 解析后逃逸工作目录")
}
if relative == "." {
+5 -2
View File
@@ -61,8 +61,11 @@ var MessagesSend = shortcut.Shortcut{
{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊接收者 openDingTalkId(user);受发送身份能力矩阵约束"},
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 userId(bot);受发送身份能力矩阵约束"},
{Name: "open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 openDingTalkId(bot);受发送身份能力矩阵约束"},
{Name: "robot-code", Type: shortcut.FlagString, RequiredWhen: "identity=bot", Desc: "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束"},
{Name: "webhook-token", Type: shortcut.FlagString, RequiredWhen: "identity=webhook", Desc: "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束"},
// Keep required_when out of Schema: validateMessagesSend already enforces
// bot/webhook credentials, and publishing RequiredWhen breaks merge-base
// schema-compatibility (null → expression).
{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束"},
{Name: "webhook-token", Type: shortcut.FlagString, Desc: "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束"},
{Name: "title", Type: shortcut.FlagString, Desc: "消息标题(不传则从正文生成)"},
{Name: "text", Type: shortcut.FlagString, Desc: "纯文本正文"},
{Name: "markdown", Type: shortcut.FlagString, Desc: "Markdown 正文"},
+4
View File
@@ -122,6 +122,10 @@ var SearchUser = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws contact +search-user --query \"张三\""},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词", Required: true},
@@ -0,0 +1,37 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package contact
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageQueryToKeywordParamDecls(t *testing.T) {
assertQueryKeywordParamDecl(t, "+search-user", SearchUser)
if len(shortcut.FromShortcut(SearchUser).Schema.Parameters) == 0 {
t.Fatal("+search-user Schema.Parameters empty after FromShortcut")
}
}
func assertQueryKeywordParamDecl(t *testing.T, name string, sc shortcut.Shortcut) {
t.Helper()
for _, p := range sc.Schema.Parameters {
if p.Name == "query" && p.Property == "keyword" {
return
}
}
t.Fatalf("%s missing ParamDecl query→keyword; got %#v", name, sc.Schema.Parameters)
}
+4
View File
@@ -55,6 +55,10 @@ var Search = shortcut.Shortcut{
"dws doc +search --extensions pdf,docx",
},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词,不传返回最近访问的文档"},
@@ -0,0 +1,37 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package doc
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageQueryToKeywordParamDecls(t *testing.T) {
assertQueryKeywordParamDecl(t, "+search", Search)
if len(shortcut.FromShortcut(Search).Schema.Parameters) == 0 {
t.Fatal("+search Schema.Parameters empty after FromShortcut")
}
}
func assertQueryKeywordParamDecl(t *testing.T, name string, sc shortcut.Shortcut) {
t.Helper()
for _, p := range sc.Schema.Parameters {
if p.Name == "query" && p.Property == "keyword" {
return
}
}
t.Fatalf("%s missing ParamDecl query→keyword; got %#v", name, sc.Schema.Parameters)
}
+8
View File
@@ -226,6 +226,10 @@ var Search = shortcut.Shortcut{
"dws drive +search --query \"合同\" --target file --extensions pdf,docx",
},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词", Required: true},
@@ -374,6 +378,10 @@ var SearchDocs = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws drive +search-docs --query \"季度汇报\""},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词", Required: true},
@@ -0,0 +1,45 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drive
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageQueryToKeywordParamDecls(t *testing.T) {
for _, tc := range []struct {
name string
sc shortcut.Shortcut
}{
{"+search", Search},
{"+search-docs", SearchDocs},
} {
assertQueryKeywordParamDecl(t, tc.name, tc.sc)
if len(shortcut.FromShortcut(tc.sc).Schema.Parameters) == 0 {
t.Fatalf("%s Schema.Parameters empty after FromShortcut", tc.name)
}
}
}
func assertQueryKeywordParamDecl(t *testing.T, name string, sc shortcut.Shortcut) {
t.Helper()
for _, p := range sc.Schema.Parameters {
if p.Name == "query" && p.Property == "keyword" {
return
}
}
t.Fatalf("%s missing ParamDecl query→keyword; got %#v", name, sc.Schema.Parameters)
}
+12
View File
@@ -51,6 +51,10 @@ var ListMine = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws minutes +list-mine --query \"周会\" --limit 10"},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "关键字筛选"},
@@ -86,6 +90,10 @@ var ListShared = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws minutes +list-shared --limit 20"},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "关键字筛选"},
@@ -121,6 +129,10 @@ var ListAll = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws minutes +list-all --query \"周会\" --limit 20"},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "关键字筛选"},
@@ -0,0 +1,46 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package minutes
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageQueryToKeywordParamDecls(t *testing.T) {
for _, tc := range []struct {
name string
sc shortcut.Shortcut
}{
{"+list-mine", ListMine},
{"+list-shared", ListShared},
{"+list-all", ListAll},
} {
assertQueryKeywordParamDecl(t, tc.name, tc.sc)
if len(shortcut.FromShortcut(tc.sc).Schema.Parameters) == 0 {
t.Fatalf("%s Schema.Parameters empty after FromShortcut", tc.name)
}
}
}
func assertQueryKeywordParamDecl(t *testing.T, name string, sc shortcut.Shortcut) {
t.Helper()
for _, p := range sc.Schema.Parameters {
if p.Name == "query" && p.Property == "keyword" {
return
}
}
t.Fatalf("%s missing ParamDecl query→keyword; got %#v", name, sc.Schema.Parameters)
}
+4
View File
@@ -67,6 +67,10 @@ var FindDoc = shortcut.Shortcut{
"dws doc +find-doc --query 合同 --limit 10",
},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "按关键词搜索云文档(必填)", Required: true},
+4
View File
@@ -66,6 +66,10 @@ var FindFile = shortcut.Shortcut{
"dws drive +find-file --query 合同",
},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "文件名关键词(必填)", Required: true},
@@ -76,6 +76,10 @@ var FindMailUser = shortcut.Shortcut{
"dws mail +find-mail-user --query alice --limit 10",
},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词(姓名/花名/邮箱片段,必填)", Required: true},
+4
View File
@@ -62,6 +62,10 @@ var FindRecord = shortcut.Shortcut{
"dws aitable +find-record --base B --table T --query 张三",
},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "base", Type: shortcut.FlagString, Desc: "Base ID(多维表所属 base)", Required: true},
+23
View File
@@ -14,6 +14,8 @@
package smart
import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
@@ -41,6 +43,27 @@ var MinutesSearch = shortcut.Shortcut{
"内部按 --query 关键词列出你创建的听记(最多 20 条),再在本地投影出每条的标题、创建时间和 taskUuid。" +
"这是纯只读操作,只做搜索与本地投影,不会修改任何听记;若没有匹配的听记则提示「没搜到妙记」。",
Risk: shortcut.RiskRead,
Safety: cli.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Schema: corecmd.SchemaDecl{
Description: "按关键词搜索我的妙记并投影列表",
Interface: &corecmd.InterfaceDecl{
Mode: "composite", Availability: "available",
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
},
Selection: corecmd.SelectionDecl{
AgentSummary: "按关键词搜索我的妙记并投影列表",
UseWhen: []string{"当你想按关键词快速找回自己创建的会议听记(妙记),只需要看到匹配到的标题、创建时间和 taskUuid 列表、而不想拿到一大坨原始字段时使用;内部按 --query 关键词列出你创建的听记(最多 20 条),再在本地投影出每条的标题、创建时间和 taskUuid。这是纯只读操作,只做搜索与本地投影,不会修改任何听记;若没有匹配的听记则提示「没搜到妙记」。"},
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws minutes +minutes-search --query 周会"},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "按关键词搜索听记(必填)", Required: true},
},
@@ -0,0 +1,48 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package smart
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageQueryToKeywordParamDecls(t *testing.T) {
// Execute paths that feed --query into MCP "keyword" must declare the
// mapping; flag_name_inference alone would publish property=query.
for _, tc := range []struct {
name string
sc shortcut.Shortcut
}{
{"+find-doc", FindDoc},
{"+find-file", FindFile},
{"+find-mail-user", FindMailUser},
{"+minutes-search", MinutesSearch},
{"+search-msg", SearchMsg},
{"+find-record", FindRecord},
} {
var found bool
for _, p := range tc.sc.Schema.Parameters {
if p.Name == "query" && p.Property == "keyword" {
found = true
break
}
}
if !found {
t.Fatalf("%s missing ParamDecl query→keyword; got %#v",
tc.name, tc.sc.Schema.Parameters)
}
}
}
+4
View File
@@ -60,6 +60,10 @@ var SearchMsg = shortcut.Shortcut{
"dws chat +search-msg --group <openConversationId> --message-type file --download-resources --output-dir ./downloads",
},
},
// Execute maps --query (or hidden --keyword) → MCP keyword.
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: append([]shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词"},
@@ -0,0 +1,37 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wiki
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageQueryToKeywordParamDecls(t *testing.T) {
assertQueryKeywordParamDecl(t, "+space-search", SpaceSearch)
if len(shortcut.FromShortcut(SpaceSearch).Schema.Parameters) == 0 {
t.Fatal("+space-search Schema.Parameters empty after FromShortcut")
}
}
func assertQueryKeywordParamDecl(t *testing.T, name string, sc shortcut.Shortcut) {
t.Helper()
for _, p := range sc.Schema.Parameters {
if p.Name == "query" && p.Property == "keyword" {
return
}
}
t.Fatalf("%s missing ParamDecl query→keyword; got %#v", name, sc.Schema.Parameters)
}
+4
View File
@@ -150,6 +150,10 @@ var SpaceSearch = shortcut.Shortcut{
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws wiki +space-search --query \"产品文档\""},
},
// Execute maps --query → MCP keyword (not inferable from flag name).
Parameters: []corecmd.ParamDecl{
{Name: "query", Property: "keyword"},
},
},
Flags: []shortcut.Flag{
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词", Required: true},
+400 -86
View File
@@ -4,11 +4,16 @@
Usage: python3 scripts/dev/migrate_product_params.py <product>
For each tool with parameter overlays in schema_hints/metadata/<product>.json:
1. Finds the DeclareLeafMetadata block containing the tool's RPCName.
1. Finds the DeclareLeafMetadata block for the tool's reviewed cli_path.
2. Inserts Parameters: []corecmd.ParamDecl{...} into the Schema block.
3. Moves any overlaid flag registrations before the DeclareLeafMetadata call.
4. Clears the tool's parameters from the hints file.
Shared-RPC siblings (e.g. drive rename / doc rename both use rename_document,
mail message list / search both call search_emails) MUST be disambiguated by
cli_path. If cli_path cannot uniquely select a leaf, the script refuses to
insert or clear hints — never pick the first RPC hit.
Run for one product at a time; verify compilation and schema output between runs.
"""
import json
@@ -112,6 +117,366 @@ def find_declare_for_var(lines, var_name, after_line=0):
return -1
# Minimum score to accept a multi-leaf shared-RPC candidate.
# Example/Examples primary invoke = 100; Use+product file = 40.
SCORE_MULTI_LEAF_MIN = 40
def cli_path_tokens(cli_path):
"""Normalize a reviewed cli_path into path tokens (no flags)."""
if not cli_path:
return []
# "mail message list" or "minutes +replace-batch"
parts = []
for tok in str(cli_path).split():
if tok.startswith("-"):
break
parts.append(tok)
return parts
def example_blobs(block_text):
"""Extract Example / Examples string bodies (ignore UseWhen/AvoidWhen prose)."""
blobs = []
for m in re.finditer(r"Example:\s*`([^`]*)`", block_text, re.S):
blobs.append(m.group(1))
for m in re.finditer(r"Examples:\s*\[\]string\{([^{}]*)\}", block_text, re.S):
blobs.append(m.group(1))
return "\n".join(blobs)
def file_matches_cli_product(fp, product):
"""True when helper/shortcut file clearly belongs to the cli_path product."""
if not product or not fp:
return False
base = os.path.basename(fp)
norm = fp.replace("\\", "/")
if base == f"{product}.go" or base.startswith(f"{product}_"):
return True
if f"/shortcut/{product}/" in norm or norm.endswith(f"/shortcut/{product}.go"):
return True
return False
def score_block_for_cli_path(block_text, cli_path, fp=""):
"""Score how strongly a command/Declare block owns reviewed cli_path.
Sibling AvoidWhen/UseWhen often mention the other leaf's path (e.g. doc rename
says "prefer dws drive rename"). Bare substring matching is therefore unsafe.
Only Example/Examples primary invokes and Use+product-file evidence count.
"""
tokens = cli_path_tokens(cli_path)
if not tokens:
return 0
path = " ".join(tokens)
product = tokens[0]
leaf = tokens[-1]
score = 0
examples = example_blobs(block_text)
# Primary: this leaf's Example documents `dws <cli_path> ...`
if re.search(
r'(?:^|[\s"\'`])dws\s+' + re.escape(path) + r'(?:\s|--|["\'`]|$)',
examples,
re.M,
):
score += 100
if re.search(r'Use:\s*"' + re.escape(leaf) + r'"', block_text):
if file_matches_cli_product(fp, product):
score += 40
elif len(tokens) >= 2 and tokens[-2] in block_text:
# Same-file siblings (mail list/search): Use leaf differs; parent token
# is weak confirmation only and must not beat Example evidence.
score += 10
else:
score += 5
return score
def block_matches_cli_path(block_text, cli_path, fp=""):
"""True when score shows clear ownership of cli_path (not bare substring)."""
return score_block_for_cli_path(block_text, cli_path, fp) >= SCORE_MULTI_LEAF_MIN
def collect_rpc_sites(file_contents, rpc_name):
"""Return [(file, line_idx)] for every RPCName / callMCPTool site."""
rpc_pattern = re.compile(r'RPCName:\s*"' + re.escape(rpc_name) + '"')
call_patterns = [
re.compile(r'callMCPTool\("' + re.escape(rpc_name) + '"'),
re.compile(r'callMCPToolOnServer\([^,]+,\s*"' + re.escape(rpc_name) + '"'),
re.compile(r'callAitableTool\("' + re.escape(rpc_name) + '"'),
re.compile(r'rt\.CallMCP\w*\("' + re.escape(rpc_name) + '"'),
]
sites = []
for fp, src in file_contents.items():
lines_src = src.split("\n")
for i, line in enumerate(lines_src):
if rpc_pattern.search(line) or any(cp.search(line) for cp in call_patterns):
sites.append((fp, i))
return sites
def _dedupe_candidates(candidates):
"""Keep highest-scoring candidate per DeclareLeafMetadata site."""
best = {}
for fp, decl_start, cmd_var, score, call_line in candidates:
key = (fp, decl_start)
prev = best.get(key)
if prev is None or score > prev[3]:
best[key] = (fp, decl_start, cmd_var, score, call_line)
return list(best.values())
def _pick_unique_candidate(tool_name, rpc_name, cli_path, candidates, *, multi_leaf):
"""Return (fp, decl_start, cmd_var) or (None, -1, "") when unsafe."""
ranked = sorted(candidates, key=lambda c: (-c[3], c[0], c[1]))
if not ranked:
return None, -1, ""
if multi_leaf:
if not cli_path:
print(f" WARNING: {tool_name} — RPC '{rpc_name}' has multiple DeclareLeafMetadata "
f"candidates; reviewed cli_path is required for disambiguation")
return None, -1, ""
if ranked[0][3] < SCORE_MULTI_LEAF_MIN:
print(f" WARNING: {tool_name} — RPC '{rpc_name}' has {len(ranked)} leaf candidates; "
f"cli_path={cli_path!r} did not uniquely select a leaf; refusing to insert")
return None, -1, ""
top = [c for c in ranked if c[3] == ranked[0][3]]
if len(top) > 1:
print(f" WARNING: {tool_name} — ambiguous cli_path match for RPC '{rpc_name}' "
f"({len(top)} DeclareLeafMetadata candidates at score {ranked[0][3]}); "
f"refusing to insert")
return None, -1, ""
fp, decl_start, cmd_var, _, _ = top[0]
return fp, decl_start, cmd_var
# Single leaf: allow RPC-only resolution (no cli_path required).
fp, decl_start, cmd_var, _, _ = ranked[0]
return fp, decl_start, cmd_var
def resolve_declare_target(file_contents, tool_name, rpc_name, cli_path):
"""Resolve (file, decl_start, cmd_var) for a tool, or (None, -1, "").
Shared RPCs (e.g. mail list/search both call search_emails) must be
disambiguated by reviewed cli_path. Never return the first RPC hit blindly.
"""
sites = collect_rpc_sites(file_contents, rpc_name)
candidates = [] # (fp, decl_start, cmd_var, score, call_line)
for fp, call_line in sites:
lines = file_contents[fp].split("\n")
cmd_var, cmd_line = find_cmd_var_for_call(lines, call_line)
decl_start = find_declare_for_var(lines, cmd_var, after_line=cmd_line) if cmd_var else -1
if decl_start < 0:
continue
block_end = find_block_end(lines, decl_start)
# Include the command literal block for Use/Example matching.
window_start = cmd_line if cmd_line >= 0 else decl_start
block_text = "\n".join(lines[window_start:block_end + 1])
score = score_block_for_cli_path(block_text, cli_path, fp)
candidates.append((fp, decl_start, cmd_var, score, call_line))
candidates = _dedupe_candidates(candidates)
multi_leaf = len(candidates) > 1
if candidates:
picked = _pick_unique_candidate(
tool_name, rpc_name, cli_path, candidates, multi_leaf=multi_leaf)
if picked[0] is not None:
return picked
if multi_leaf:
return None, -1, ""
# Strategy: composite / shortcut helpers keyed by cli_path or Use leaf.
tokens = cli_path_tokens(cli_path)
use_name = tokens[-1] if tokens else tool_name.split(".", 1)[-1].replace("_", "-")
for prefix in ("shortcut-", "+"):
if use_name.startswith(prefix):
use_name = use_name[len(prefix):]
use_pat = re.compile(r'Use:\s*"' + re.escape(use_name) + '"')
use_hits = []
for fp, src in file_contents.items():
lines_src = src.split("\n")
for i, line in enumerate(lines_src):
if not use_pat.search(line):
continue
cmd_var, cmd_line = find_cmd_var_for_call(lines_src, i)
decl_start = find_declare_for_var(lines_src, cmd_var, after_line=cmd_line) if cmd_var else -1
if decl_start < 0:
# shortcut.FromShortcut path: Schema may be on Shortcut literal.
continue
block_end = find_block_end(lines_src, decl_start)
window_start = cmd_line if cmd_line >= 0 else decl_start
block_text = "\n".join(lines_src[window_start:block_end + 1])
score = score_block_for_cli_path(block_text, cli_path, fp)
use_hits.append((fp, decl_start, cmd_var, score, i))
use_hits = _dedupe_candidates(use_hits)
if use_hits:
picked = _pick_unique_candidate(
tool_name, rpc_name, cli_path, use_hits, multi_leaf=len(use_hits) > 1)
if picked[0] is not None:
return picked
return None, -1, ""
print(f" WARNING: {tool_name} — RPCName '{rpc_name}' not found in any helper file")
return None, -1, ""
def schema_has_param_decls(schema_text, params):
"""True when Schema already declares every overlay flag Name."""
if "Parameters:" not in schema_text:
return False
for pname in params:
if not re.search(r'Name:\s*"' + re.escape(pname) + r'"', schema_text):
return False
return True
def _self_test_fixtures():
"""Synthetic shared-RPC helpers mirroring mail list/search and doc/drive rename."""
mail_src = r'''
messageSearchCmd := &cobra.Command{
Use: "search",
Example: ` dws mail message search --email user@company.com --query "subject:周报"`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPTool("search_emails", toolArgs)
},
}
DeclareLeafMetadata(messageSearchCmd, LeafSpec{
Schema: LeafSchema{
Interface: &LeafInterfaceDecl{
ProductID: "mail", RPCName: "search_emails",
},
Selection: LeafSelectionDecl{
AvoidWhen: []string{
"只列某文件夹邮件且无需复杂条件时优先 mail message list",
},
Examples: []string{"dws mail message search --email user@company.com --query \"subject:周报\""},
},
},
})
messageListCmd := &cobra.Command{
Use: "list",
Example: ` dws mail message list --email user@company.com`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPTool("search_emails", toolArgs)
},
}
DeclareLeafMetadata(messageListCmd, LeafSpec{
Schema: LeafSchema{
Interface: &LeafInterfaceDecl{
Mode: "composite",
Reason: "wrapper around search_emails",
},
Selection: LeafSelectionDecl{
AvoidWhen: []string{"需要组合条件时用 mail message search"},
Examples: []string{
"dws mail message list --email user@company.com",
},
},
},
})
'''
doc_src = r'''
renameCmd := &cobra.Command{
Use: "rename",
Example: ` dws doc rename --node DOC_ID --name "新名称"`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPTool("rename_document", map[string]any{})
},
}
DeclareLeafMetadata(renameCmd, LeafSpec{
Schema: LeafSchema{
Interface: &LeafInterfaceDecl{
ProductID: "doc", RPCName: "rename_document",
},
Selection: LeafSelectionDecl{
AvoidWhen: []string{
"文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型",
},
Examples: []string{"dws doc rename --node <DOC_ID> --name \"新名称\" --format json"},
},
},
})
'''
drive_src = r'''
driveRenameCmd := &cobra.Command{
Use: "rename",
Example: ` dws drive rename --node DOC_ID --name "新名称"`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPToolOnServer("doc", "rename_document", map[string]any{})
},
}
DeclareLeafMetadata(driveRenameCmd, LeafSpec{
Schema: LeafSchema{
Interface: &LeafInterfaceDecl{
ProductID: "doc", RPCName: "rename_document",
},
Selection: LeafSelectionDecl{
AvoidWhen: []string{
"要改正文里的标题改用 dws doc block update,不要用 rename",
},
Examples: []string{"dws drive rename --node <ID> --name \"新名称\" --format json"},
},
Parameters: []corecmd.ParamDecl{
{Name: "name", Description: "drive-only extension stripping"},
},
},
})
'''
return {
"internal/helpers/mail.go": mail_src,
"internal/helpers/doc.go": doc_src,
"internal/helpers/drive.go": drive_src,
}
def run_self_test():
"""Verify shared-RPC cli_path disambiguation; exit 0/1."""
fc = _self_test_fixtures()
cases = [
("mail.list_emails", "search_emails", "mail message list", "mail.go", "messageListCmd"),
("mail.search_emails", "search_emails", "mail message search", "mail.go", "messageSearchCmd"),
("drive.rename_document", "rename_document", "drive rename", "drive.go", "driveRenameCmd"),
("doc.rename_document", "rename_document", "doc rename", "doc.go", "renameCmd"),
]
failed = 0
for tool, rpc, cli, want_file, want_var in cases:
fp, decl, var = resolve_declare_target(fc, tool, rpc, cli)
base = os.path.basename(fp) if fp else None
ok = base == want_file and var == want_var and decl >= 0
status = "OK" if ok else "FAIL"
print(f" {status}: {tool} cli_path={cli!r} -> {base}:{var} (want {want_file}:{want_var})")
if not ok:
failed += 1
# Bare AvoidWhen cross-links must not make block_matches true alone.
doc_block = fc["internal/helpers/doc.go"]
if block_matches_cli_path(doc_block, "drive rename", "internal/helpers/doc.go"):
print(" FAIL: doc rename AvoidWhen must not own cli_path 'drive rename'")
failed += 1
else:
print(" OK: doc AvoidWhen cross-link does not claim drive rename")
# Multi-leaf without cli_path must refuse.
fp, decl, var = resolve_declare_target(fc, "mail.list_emails", "search_emails", "")
if fp is not None:
print(f" FAIL: multi-leaf without cli_path should refuse, got {fp}:{var}")
failed += 1
else:
print(" OK: multi-leaf without cli_path refused")
if failed:
print(f"self-test: {failed} failure(s)")
return False
print("self-test: all passed")
return True
def migrate_product(product):
hint_path = os.path.join(HINTS_DIR, f"{product}.json")
hints = json.load(open(hint_path, encoding="utf-8"))
@@ -131,100 +496,38 @@ def migrate_product(product):
"devdoc.search_open_platform_docs_rag": "search_open_platform_docs",
}
# Load all helper files into memory
# Also search shortcut packages for composite / no-single-RPC commands.
file_contents = {}
for fp in helper_files_for_product(product):
file_contents[fp] = open(fp, encoding="utf-8").read()
shortcut_root = "internal/shortcut"
if os.path.isdir(shortcut_root):
for root, _, fns in os.walk(shortcut_root):
for fn in fns:
if fn.endswith(".go") and not fn.endswith("_test.go"):
fp = os.path.join(root, fn)
file_contents[fp] = open(fp, encoding="utf-8").read()
modified_files = set()
flags_to_move = []
migrated_tools = set() # only clear hints for tools we actually migrated
for tool_name, params in sorted(overlays.items()):
meta = tools.get(tool_name) or {}
cli_path = (meta.get("cli_path") or "").strip()
rpc_name = RPC_ALIASES.get(tool_name, tool_name.split(".")[-1])
if not cli_path:
# Shared RPCs cannot be safely resolved without reviewed cli_path.
print(f" WARNING: {tool_name} — parameters overlay missing cli_path; "
f"shared-RPC siblings will refuse multi-leaf resolution")
# Strategy 1: RPCName field in Schema.Interface
rpc_pattern = re.compile(r'RPCName:\s*"' + re.escape(rpc_name) + '"')
# Strategy 2: callMCPTool / CallMCP call sites
call_patterns = [
re.compile(r'callMCPTool\("' + re.escape(rpc_name) + '"'),
re.compile(r'callMCPToolOnServer\([^,]+,\s*"' + re.escape(rpc_name) + '"'),
re.compile(r'callAitableTool\("' + re.escape(rpc_name) + '"'),
re.compile(r'rt\.CallMCP\w*\("' + re.escape(rpc_name) + '"'),
]
found_file = None
found_line = -1
for fp, src in file_contents.items():
lines_src = src.split("\n")
# Try strategy 1 first
for i, line in enumerate(lines_src):
if rpc_pattern.search(line):
found_file = fp
found_line = i
break
if found_file:
break
# Try strategy 2
for i, line in enumerate(lines_src):
for cp in call_patterns:
if cp.search(line):
found_file = fp
found_line = i
break
if found_file:
break
if found_file:
break
if not found_file:
# Strategy 3: composite helpers keyed by cobra Use (e.g. markdown.create).
use_name = tool_name.split(".", 1)[-1].replace("_", "-")
# Prefer last segment after dropping common prefixes.
for prefix in ("shortcut-",):
if use_name.startswith(prefix):
use_name = use_name[len(prefix):]
use_pat = re.compile(r'Use:\s*"' + re.escape(use_name) + '"')
for fp, src in file_contents.items():
lines_src = src.split("\n")
for i, line in enumerate(lines_src):
if use_pat.search(line):
found_file = fp
found_line = i
break
if found_file:
break
if not found_file:
print(f" WARNING: {tool_name} — RPCName '{rpc_name}' not found in any helper file")
found_file, decl_start, cmd_var = resolve_declare_target(
file_contents, tool_name, rpc_name, cli_path)
if not found_file or decl_start < 0:
continue
lines = file_contents[found_file].split("\n")
# Prefer command-variable association: call site → cmd var → DeclareLeafMetadata(cmd)
decl_start = -1
cmd_var, cmd_line = find_cmd_var_for_call(lines, found_line)
if cmd_var:
decl_start = find_declare_for_var(lines, cmd_var, after_line=cmd_line)
# Fallback: any DeclareLeafMetadata whose block mentions the RPC name
if decl_start < 0:
for i, line in enumerate(lines):
if "DeclareLeafMetadata(" not in line:
continue
block_end = find_block_end(lines, i)
block_text = "\n".join(lines[i:block_end + 1])
if rpc_name in block_text or tool_name in block_text:
decl_start = i
break
if f'callMCPTool("{rpc_name}"' in block_text or \
(f'callMCPToolOnServer(' in block_text and rpc_name in block_text):
decl_start = i
break
if decl_start < 0:
print(f" WARNING: {tool_name} — no matching DeclareLeafMetadata near line {found_line + 1} in {os.path.basename(found_file)} (cmd_var={cmd_var!r})")
continue
# Find Schema block
schema_start = -1
for i in range(decl_start, min(decl_start + 80, len(lines))):
@@ -237,10 +540,16 @@ def migrate_product(product):
schema_end = find_block_end(lines, schema_start)
schema_text = "\n".join(lines[schema_start:schema_end + 1])
if "Parameters:" in schema_text:
print(f" SKIP: {tool_name} — Parameters already declared")
if schema_has_param_decls(schema_text, params):
# Only treat as migrated when THIS leaf already has the overlay names.
print(f" SKIP: {tool_name} — Parameters already declared on matched leaf "
f"(var={cmd_var}, cli_path={cli_path!r})")
migrated_tools.add(tool_name)
continue
if "Parameters:" in schema_text:
print(f" WARNING: {tool_name} — matched leaf already has Parameters but is missing "
f"overlay flags {sorted(params)}; refusing to clear hints (var={cmd_var})")
continue
# Build and insert Parameters
param_lines = ["\t\t\tParameters: []corecmd.ParamDecl{"]
@@ -260,7 +569,8 @@ def migrate_product(product):
modified_files.add(found_file)
file_contents[found_file] = "\n".join(lines)
migrated_tools.add(tool_name)
print(f" OK: {tool_name} — inserted {len(params)} param decls in {os.path.basename(found_file)} at line {insert_at + 1} (var={cmd_var})")
print(f" OK: {tool_name} — inserted {len(params)} param decls in {os.path.basename(found_file)} "
f"at line {insert_at + 1} (var={cmd_var}, cli_path={cli_path!r})")
# Check flag registration order
for pname in params:
@@ -271,7 +581,7 @@ def migrate_product(product):
flags_to_move.append((pname, i, decl_start, os.path.basename(found_file)))
break
# Clear hints for migrated/skipped tools (even if no source files changed)
# Clear hints ONLY for tools we migrated onto the matched leaf.
if migrated_tools:
for tool_name in migrated_tools:
if tool_name in hints.get("tools", {}):
@@ -281,7 +591,8 @@ def migrate_product(product):
if not hints.get("tools"):
hints["tools"] = {}
json.dump(hints, open(hint_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(f"{product}: cleared hints for {len(migrated_tools)} tools ({len(overlays) - len(migrated_tools)} remaining)")
print(f"{product}: cleared hints for {len(migrated_tools)} tools "
f"({len(overlays) - len(migrated_tools)} remaining)")
if not modified_files:
if not migrated_tools:
@@ -291,7 +602,7 @@ def migrate_product(product):
# Write modified files and ensure corecmd import
for fp in modified_files:
src = file_contents[fp]
if "internal/corecmd" not in src:
if fp.startswith(HELPER_DIR) and "internal/corecmd" not in src:
m = re.search(r'(import \(\n(?:.*\n)*?\))', src)
if m:
block = m.group(1)
@@ -309,8 +620,11 @@ def migrate_product(product):
if __name__ == "__main__":
if len(sys.argv) == 2 and sys.argv[1] in ("--self-test", "self-test"):
sys.exit(0 if run_self_test() else 1)
if len(sys.argv) != 2:
print("Usage: migrate_product_params.py <product>")
print(" migrate_product_params.py --self-test")
sys.exit(1)
ok = migrate_product(sys.argv[1])
sys.exit(0 if ok else 1)