Switches the discovery version code bamboo -> cedar and aligns the open edition CLI with dws-wukong across communication (calendar book/acl/attendee, minutes tag, mail folder/template/contact, chat file upload, todo add-attachment, attendance transforms) and structured-office (aitable advperm/view/section/workflow/record, sheet/drive/wiki/doc) domains. Includes output-envelope parity, parse_bool/attendance_class_check_time transforms, --calendar-id support, CHANGELOG 1.0.43 and README command-index refresh. cedar config validated on pre and prod endpoints.
204 lines
5.9 KiB
Go
204 lines
5.9 KiB
Go
// 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 app
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/ir"
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
// toolMappingParam 描述一个 MCP 参数到 CLI flag + 中文友好名的映射。
|
||
type toolMappingParam struct {
|
||
Flag string `json:"flag"`
|
||
Label string `json:"label"`
|
||
Type string `json:"type,omitempty"`
|
||
}
|
||
|
||
// toolMappingEntry 是单个 MCP 工具的映射条目。key 用 RPCName,对齐 SLS 日志的 tool 字段。
|
||
type toolMappingEntry struct {
|
||
Product string `json:"product"`
|
||
CLICommand string `json:"cliCommand"`
|
||
DisplayName string `json:"displayName"`
|
||
Params map[string]toolMappingParam `json:"params,omitempty"`
|
||
}
|
||
|
||
// toolMapping 是给开放平台日志页渲染用的全量映射契约。
|
||
type toolMapping struct {
|
||
Version string `json:"version"`
|
||
Count int `json:"count"`
|
||
Tools map[string]toolMappingEntry `json:"tools"`
|
||
}
|
||
|
||
// newCatalogCommand 提供 `dws catalog export`:把已发现的工具目录投影成
|
||
// tool→指令 映射 JSON,供开放平台 MCP/DWS 日志页把 tool/args 渲染成中文友好名。
|
||
// 复用 root 注入的带 auth 的 loader(缓存优先;建议先 `dws cache refresh`)。
|
||
func newCatalogCommand(loader cli.CatalogLoader) *cobra.Command {
|
||
catalogCmd := &cobra.Command{
|
||
Use: "catalog",
|
||
Short: "导出已发现的工具目录(内部用)",
|
||
Hidden: true,
|
||
}
|
||
|
||
var out string
|
||
var version string
|
||
exportCmd := &cobra.Command{
|
||
Use: "export",
|
||
Short: "导出 tool→指令 映射 JSON(供开放平台日志页渲染)",
|
||
Args: cobra.NoArgs,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
catalog, err := loader.Load(cmd.Context())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
mapping := projectToolMapping(catalog, version)
|
||
data, err := json.MarshalIndent(mapping, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
data = append(data, '\n')
|
||
if strings.TrimSpace(out) == "" {
|
||
_, werr := os.Stdout.Write(data)
|
||
return werr
|
||
}
|
||
return os.WriteFile(out, data, 0o644)
|
||
},
|
||
}
|
||
exportCmd.Flags().StringVar(&out, "out", "", "输出文件路径(默认 stdout)")
|
||
exportCmd.Flags().StringVar(&version, "version", "dev", "版本号标记")
|
||
|
||
catalogCmd.AddCommand(exportCmd)
|
||
return catalogCmd
|
||
}
|
||
|
||
// projectToolMapping 把 ir.Catalog 投影成 toolMapping 契约。
|
||
func projectToolMapping(catalog ir.Catalog, version string) toolMapping {
|
||
mapping := toolMapping{Version: version, Tools: make(map[string]toolMappingEntry)}
|
||
for _, product := range catalog.Products {
|
||
command := ""
|
||
if product.CLI != nil {
|
||
command = strings.TrimSpace(product.CLI.Command)
|
||
}
|
||
if command == "" {
|
||
command = product.ID
|
||
}
|
||
for _, tool := range product.Tools {
|
||
if tool.Hidden {
|
||
continue
|
||
}
|
||
entry := toolMappingEntry{
|
||
Product: command,
|
||
CLICommand: tmBuildCLICommand(command, tool),
|
||
DisplayName: tmFirstNonEmpty(tool.Title, tmFirstNonEmpty(tmFirstLine(tool.Description), tool.RPCName)),
|
||
Params: make(map[string]toolMappingParam),
|
||
}
|
||
for name, raw := range tmSchemaProperties(tool.InputSchema) {
|
||
prop, _ := raw.(map[string]any)
|
||
overlay, hasOverlay := tool.FlagOverlay[name]
|
||
if hasOverlay && overlay.Hidden {
|
||
continue
|
||
}
|
||
flag := tmKebab(name)
|
||
if hasOverlay && strings.TrimSpace(overlay.Alias) != "" {
|
||
flag = strings.TrimSpace(overlay.Alias)
|
||
}
|
||
label := tmMapStr(prop, "title")
|
||
if label == "" {
|
||
label = tmFirstLine(tmMapStr(prop, "description"))
|
||
}
|
||
entry.Params[name] = toolMappingParam{
|
||
Flag: flag,
|
||
Label: label,
|
||
Type: tmMapStr(prop, "type"),
|
||
}
|
||
}
|
||
if len(entry.Params) == 0 {
|
||
entry.Params = nil
|
||
}
|
||
mapping.Tools[tool.RPCName] = entry
|
||
}
|
||
}
|
||
mapping.Count = len(mapping.Tools)
|
||
return mapping
|
||
}
|
||
|
||
// tmBuildCLICommand 拼出 CLI 命令路径,如 chat + message + list -> "chat message list"。
|
||
func tmBuildCLICommand(command string, tool ir.ToolDescriptor) string {
|
||
parts := make([]string, 0, 3)
|
||
if command != "" {
|
||
parts = append(parts, command)
|
||
}
|
||
if g := strings.TrimSpace(tool.Group); g != "" {
|
||
parts = append(parts, g)
|
||
}
|
||
name := strings.TrimSpace(tool.CLIName)
|
||
if name == "" {
|
||
name = tool.RPCName
|
||
}
|
||
parts = append(parts, name)
|
||
return strings.Join(parts, " ")
|
||
}
|
||
|
||
func tmSchemaProperties(schema map[string]any) map[string]any {
|
||
if schema == nil {
|
||
return nil
|
||
}
|
||
props, _ := schema["properties"].(map[string]any)
|
||
return props
|
||
}
|
||
|
||
func tmMapStr(m map[string]any, key string) string {
|
||
if m == nil {
|
||
return ""
|
||
}
|
||
s, _ := m[key].(string)
|
||
return strings.TrimSpace(s)
|
||
}
|
||
|
||
// tmFirstLine 取第一句中文/换行前的片段,作为长描述的短标签兜底。
|
||
func tmFirstLine(s string) string {
|
||
s = strings.TrimSpace(s)
|
||
if i := strings.IndexAny(s, "\n。"); i >= 0 {
|
||
return strings.TrimSpace(s[:i])
|
||
}
|
||
return s
|
||
}
|
||
|
||
func tmFirstNonEmpty(a, b string) string {
|
||
if strings.TrimSpace(a) != "" {
|
||
return strings.TrimSpace(a)
|
||
}
|
||
return strings.TrimSpace(b)
|
||
}
|
||
|
||
// tmKebab 把 camelCase 参数名转 kebab-case 作为默认 flag。
|
||
func tmKebab(s string) string {
|
||
var b strings.Builder
|
||
for i, r := range s {
|
||
if r >= 'A' && r <= 'Z' {
|
||
if i > 0 {
|
||
b.WriteByte('-')
|
||
}
|
||
b.WriteRune(r - 'A' + 'a')
|
||
continue
|
||
}
|
||
b.WriteRune(r)
|
||
}
|
||
return b.String()
|
||
}
|