Files
dingtalk-workspace-cli/internal/cli/canonical.go
T

318 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 cli
import (
"context"
"fmt"
"sort"
"strings"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline"
"github.com/spf13/cobra"
)
var schemaCommandCatalogError = embeddedSchemaCatalogError
type FlagKind string
const (
flagString FlagKind = "string"
flagInteger FlagKind = "integer"
flagNumber FlagKind = "number"
flagBoolean FlagKind = "boolean"
flagStringArray FlagKind = "string_array"
flagIntegerList FlagKind = "integer_array"
flagNumberList FlagKind = "number_array"
flagBooleanList FlagKind = "boolean_array"
flagJSON FlagKind = "json"
)
type FlagSpec struct {
PropertyName string
FlagName string
Alias string
Shorthand string
Kind FlagKind
Description string
}
// NewMCPCommand returns a stub command since the canonical discovery
// surface has been removed. The command tree is now built from plugins
// and static endpoint registration only.
func NewMCPCommand(_ context.Context, _ CatalogLoader, _ executor.Runner, _ *pipeline.Engine) *cobra.Command {
cmd := &cobra.Command{
Use: "mcp",
Short: "Canonical MCP-derived CLI surface (static mode)",
Long: "The canonical MCP command surface is disabled. Commands are now registered via plugins and static endpoints.",
Hidden: true,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
return cmd
}
// NewSchemaCommand serves the versioned embedded typed contract. A malformed
// release snapshot fails closed; falling back to the live Cobra tree would
// hide a broken delivery artifact and reintroduce a second Schema data path.
func NewSchemaCommand(_ CatalogLoader) *cobra.Command {
cmd := &cobra.Command{
Use: "schema [path]",
Short: "渐进查看命令 Schema (产品 / 分组 / 工具参数)",
Long: `查看当前可运行命令的 Schema 元数据。
不带参数时列出产品和工具数量;传产品或分组路径逐层展开;传具体工具路径输出扁平参数 Schema(对齐 GWS:parameters 内联 required,键为 CLI flag)。--all 输出全部工具的完整 leaf Schema(包括参数和约束,用于审计/CI)。--compact 去除 provenance / debug 字段,仅保留 Agent 选参所需信息(适合 Agent 上下文)。helper、MCP 与本地 Cobra 命令均须先进入 reviewed Registry,并从同一内嵌 ToolSpec 投影;查询不执行服务发现或临时合成第二份 Schema。`,
Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
all, _ := cmd.Flags().GetBool("all")
compact, _ := cmd.Flags().GetBool("compact")
cliPath, _ := cmd.Flags().GetString("cli-path")
cliPath = strings.TrimSpace(cliPath)
if cliPath != "" && len(args) > 0 {
return apperrors.NewValidation("--cli-path and positional argument are mutually exclusive")
}
if len(args) == 1 && strings.EqualFold(strings.TrimSpace(args[0]), "list") {
args = nil
}
if all && (cliPath != "" || len(args) > 0) {
return apperrors.NewValidation("--all cannot be combined with a schema path")
}
if cliPath != "" {
args = []string{cliPath}
}
if err := schemaCommandCatalogError(); err != nil {
return fmt.Errorf("load embedded typed Schema registry: %w", err)
}
var payload map[string]any
var err error
if all {
payload, err = embeddedSchemaAllPayload()
} else if len(args) == 0 {
payload, err = embeddedSchemaOverviewPayload()
} else {
payload, err = embeddedSchemaPayload(args)
}
if err != nil {
return err
}
if compact {
payload = stripSchemaPayloadCompact(payload)
}
return output.WriteFiltered(cmd.OutOrStdout(), output.ResolveFormat(cmd, output.FormatJSON), payload, output.ResolveFields(cmd), output.ResolveJQ(cmd))
},
}
cmd.Flags().Bool("all", false, "输出全部工具的完整 leaf Schema(包括参数和约束,用于审计/CI)")
cmd.Flags().Bool("compact", false, "去除 provenance/debug 字段,仅保留 Agent 选参所需信息")
cmd.Flags().String("cli-path", "", "按 CLI 命令路径查询")
return cmd
}
func BuildFlagSpecs(schema map[string]any, hints map[string]CLIFlagHint) []FlagSpec {
properties, ok := nestedMap(schema, "properties")
if !ok {
return nil
}
keys := make([]string, 0, len(properties))
for key := range properties {
keys = append(keys, key)
}
sort.Strings(keys)
specs := make([]FlagSpec, 0, len(keys))
for _, key := range keys {
propertySchema, ok := properties[key].(map[string]any)
if !ok {
continue
}
kind, ok := flagKindForSchema(propertySchema)
if !ok {
continue
}
specs = append(specs, FlagSpec{
PropertyName: key,
FlagName: strings.ReplaceAll(key, "_", "-"),
Alias: strings.TrimSpace(hints[key].Alias),
Shorthand: strings.TrimSpace(hints[key].Shorthand),
Kind: kind,
Description: schemaDescription(propertySchema),
})
}
return specs
}
// canRegisterToolFlag reports whether a long flag named name can be
// registered on cmd without panicking pflag ("flag redefined").
func canRegisterToolFlag(cmd *cobra.Command, name string) bool {
if name == "" || name == "json" || name == "params" {
return false
}
return cmd.Flags().Lookup(name) == nil
}
// safeToolShorthand returns short when it is a single-character shorthand not
// yet bound on cmd; otherwise "" (drop the shorthand, keep the long flag).
func safeToolShorthand(cmd *cobra.Command, short string) string {
short = strings.TrimSpace(short)
if len(short) != 1 {
return ""
}
if cmd.Flags().ShorthandLookup(short) != nil {
return ""
}
return short
}
func applyFlagSpecs(cmd *cobra.Command, specs []FlagSpec) {
for _, spec := range specs {
usage := spec.Description
if usage == "" {
usage = fmt.Sprintf("Override %s", spec.PropertyName)
}
primary := strings.TrimSpace(spec.FlagName)
if !canRegisterToolFlag(cmd, primary) {
continue
}
shorthand := safeToolShorthand(cmd, spec.Shorthand)
alias := strings.TrimSpace(spec.Alias)
if alias == primary || !canRegisterToolFlag(cmd, alias) {
alias = ""
}
switch spec.Kind {
case flagString, flagJSON:
cmd.Flags().StringP(primary, shorthand, "", usage)
if alias != "" {
cmd.Flags().String(alias, "", usage+" (alias)")
_ = cmd.Flags().MarkHidden(alias)
}
case flagInteger:
cmd.Flags().IntP(primary, shorthand, 0, usage)
if alias != "" {
cmd.Flags().Int(alias, 0, usage+" (alias)")
_ = cmd.Flags().MarkHidden(alias)
}
case flagNumber:
cmd.Flags().Float64P(primary, shorthand, 0, usage)
if alias != "" {
cmd.Flags().Float64(alias, 0, usage+" (alias)")
_ = cmd.Flags().MarkHidden(alias)
}
case flagBoolean:
cmd.Flags().BoolP(primary, shorthand, false, usage)
if alias != "" {
cmd.Flags().Bool(alias, false, usage+" (alias)")
_ = cmd.Flags().MarkHidden(alias)
}
case flagStringArray, flagIntegerList, flagNumberList, flagBooleanList:
cmd.Flags().StringSliceP(primary, shorthand, nil, usage)
if alias != "" {
cmd.Flags().StringSlice(alias, nil, usage+" (alias)")
_ = cmd.Flags().MarkHidden(alias)
}
}
}
}
func nestedMap(root map[string]any, key string) (map[string]any, bool) {
if root == nil {
return nil, false
}
value, ok := root[key]
if !ok {
return nil, false
}
out, ok := value.(map[string]any)
return out, ok
}
func flagKindForSchema(schema map[string]any) (FlagKind, bool) {
if _, ok := schema["enum"].([]any); ok {
return flagString, true
}
switch schema["type"] {
case "string":
return flagString, true
case "integer":
return flagInteger, true
case "number":
return flagNumber, true
case "boolean":
return flagBoolean, true
case "object":
return flagJSON, true
case "array":
items, ok := schema["items"].(map[string]any)
if !ok {
return flagJSON, true
}
if _, ok := items["enum"].([]any); ok {
return flagStringArray, true
}
switch items["type"] {
case "string":
return flagStringArray, true
case "integer":
return flagIntegerList, true
case "number":
return flagNumberList, true
case "boolean":
return flagBooleanList, true
case "object":
return flagJSON, true
}
}
return "", false
}
func schemaDescription(schema map[string]any) string {
value, _ := schema["description"].(string)
return strings.TrimSpace(value)
}
// splitSchemaPathTokens splits a CLI path on dots, slashes, and
// whitespace, returning only non-empty tokens.
func splitSchemaPathTokens(raw string) []string {
fields := strings.FieldsFunc(raw, func(r rune) bool {
return r == '.' || r == '/' || r == ' ' || r == '\t'
})
out := fields[:0]
for _, f := range fields {
if s := strings.TrimSpace(f); s != "" {
out = append(out, s)
}
}
return out
}
// normalizeSchemaQueryCLIPath accepts the historical query spellings while
// keeping authored Registry CLI paths strict and space-separated. Canonical
// identity lookup still runs before this compatibility normalization.
func normalizeSchemaQueryCLIPath(path string) string {
parts := splitSchemaPathTokens(strings.TrimSpace(path))
if len(parts) > 0 && parts[0] == "dws" {
parts = parts[1:]
}
return strings.Join(parts, " ")
}