fix(schema): restore ParamDecl mappings and drop messages-send RequiredWhen
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
|
||||
"source_hash": "sha256:d1cf513fe3999ea4affd2218ee54f0ce746c0b267ce156587b5836ea3a8dc611",
|
||||
"source_hash": "sha256:26f44334e2182ab20b691e9e6b6221288d4ff9a3c9e13e436b4b52a98eae70b8",
|
||||
"catalog": {
|
||||
"agent_metadata": {
|
||||
"products_with_metadata": 26,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"}}},
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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 ("原样传给服务端").
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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 (必填)")
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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: "搜索关键词"},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 == "." {
|
||||
|
||||
@@ -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 正文"},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user