Files
dingtalk-workspace-cli/internal/shortcut/smart/resolve_table.go

181 lines
6.2 KiB
Go
Raw Permalink 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 smart
import (
"strconv"
"strings"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// ResolveTable: resolve a 数据表 (table) inside one Base by name keyword into a
// single tableId.
//
// This is the table-level analogue of "resolve a Base by name". Because there is
// no server tool that searches tables by name, it lists every table in the Base
// via get_tables (baseId ← --base, verbatim from list_tables / helpers
// tableGetCmd) and then matches --name locally:
// - project each table to {tableId, name} — field parsing is defensive
// (multiple candidate keys);
// - filter locally by a case-insensitive substring match on name;
// - exactly one match → return {resolved:true, tableId, name, base};
// multiple matches → return {resolved:false, count, candidates} and let the
// caller pick (never guesses);
// zero matches → report a validation error instead of an empty raw dump.
//
// Read-only: it only lists and reshapes, never mutates any table.
//
// dws aitable +resolve-table --base B --name 任务
var ResolveTable = shortcut.Shortcut{
Service: "aitable",
Command: "+resolve-table",
Product: "aitable",
Description: "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)",
Intent: "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、" +
"想把它解析成可直接用于后续工具的 tableId 时使用;" +
"内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name," +
"并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。" +
"如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。" +
"这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。",
Risk: shortcut.RiskRead,
Flags: []shortcut.Flag{
{Name: "base", Type: shortcut.FlagString, Desc: "Base ID(要在其内解析数据表的多维表)", Required: true},
{Name: "name", Type: shortcut.FlagString, Desc: "要匹配的数据表名称关键词(必填)", Required: true},
},
Tips: []string{
`dws aitable +resolve-table --base B --name 任务`,
},
Execute: func(rt *shortcut.RuntimeContext) error {
// baseId mirrors list_tables / helpers tableGetCmd (get_tables). Omitting
// tableIds asks the server for every table in the base.
data, err := rt.CallMCPData("aitable", "get_tables", map[string]any{
"baseId": rt.Str("base"),
})
if err != nil {
return err
}
// Project every table to {tableId, name}, defensively unwrapping the list.
items := resolveTableItems(data)
all := make([]map[string]any, 0, len(items))
for _, t := range items {
all = append(all, map[string]any{
"tableId": resolveTableID(t),
"name": resolveTableName(t),
})
}
// Filter locally by case-insensitive substring match on name.
needle := strings.ToLower(rt.Str("name"))
candidates := make([]map[string]any, 0, len(all))
for _, c := range all {
name, _ := c["name"].(string)
if strings.Contains(strings.ToLower(name), needle) {
candidates = append(candidates, c)
}
}
switch len(candidates) {
case 0:
return apperrors.NewValidation("Base 内没有名称包含 " + rt.Str("name") + " 的数据表")
case 1:
return rt.Output(map[string]any{
"resolved": true,
"tableId": candidates[0]["tableId"],
"name": candidates[0]["name"],
"base": rt.Str("base"),
})
default:
return rt.Output(map[string]any{
"resolved": false,
"count": len(candidates),
"candidates": candidates,
})
}
},
}
// resolveTableItems defensively unwraps the list of tables from a get_tables
// response, tolerating the common container keys the gateway may use.
func resolveTableItems(data map[string]any) []map[string]any {
if data == nil {
return nil
}
for _, key := range []string{"result", "data", "list", "items", "tables", "records"} {
raw, ok := data[key]
if !ok {
continue
}
if list, ok := raw.([]any); ok {
out := make([]map[string]any, 0, len(list))
for _, e := range list {
if m, ok := e.(map[string]any); ok {
out = append(out, m)
}
}
return out
}
// Nested container, e.g. {"data":{"list":[...]}}.
if nested, ok := raw.(map[string]any); ok {
if inner := resolveTableItems(nested); len(inner) > 0 {
return inner
}
}
}
return nil
}
// resolveTableID reads a table's identifier, tolerating the common id keys.
func resolveTableID(t map[string]any) string {
for _, key := range []string{"tableId", "table_id", "id"} {
if s := resolveTableString(t[key]); s != "" {
return s
}
}
return ""
}
// resolveTableName reads a table's display name, tolerating the common name keys.
func resolveTableName(t map[string]any) string {
for _, key := range []string{"name", "tableName", "table_name", "title"} {
if s := resolveTableString(t[key]); s != "" {
return s
}
}
return ""
}
// resolveTableString coerces a scalar JSON value to a trimmed string, returning
// "" for nil / non-scalar / empty values.
func resolveTableString(v any) string {
switch typed := v.(type) {
case string:
return strings.TrimSpace(typed)
case float64:
return strconv.FormatFloat(typed, 'f', -1, 64)
case int:
return strconv.Itoa(typed)
case int64:
return strconv.FormatInt(typed, 10)
default:
return ""
}
}
func init() {
shortcut.Register(ResolveTable)
}