Files
larksuite__cli/shortcuts/apps/db_mcp_common.go
陈兴炀 d7873bda96 feat(apps): add standalone database commands and extend the db family
Aily × PaaS 独立数据库的 CLI 落地:9 条新命令 + 5 条存量命令支持独立库。
命令面全部对齐设计文档 https://bytedance.larkoffice.com/docx/UwqJd0yt6oCB2Ox8pglcQe2BnEe

新增 9 条
- 生命周期:+db-create / +db-list / +db-delete
- 权限:    +db-permission-list / -set / -revoke(只认 --database-id)
- MCP:     +db-mcp-enable / -disable / -get(--app-id 或 --database-id 二选一)

存量 5 条支持独立库(+db-execute / +db-table-list / +db-table-get /
+db-data-import / +db-data-export)
- 新增 --database-id,与 --app-id 二选一,统一走 requireDBResource;--app-id 的
  Required 随之去掉,否则 cobra 会在二选一判定之前拒掉只传 --database-id 的调用
- 路由按标识分流:两套 path 函数并列,不在存量函数里加 if
- App 支路行为逐字不变(BOE e2e 291 用例验证,见下)

几个刻意的设计点
- 独立库无环境概念:这些命令不带 env 键,显式传 --environment 在 Validate 阶段拒;
  判据用 Changed() 而非取值,否则「吃到默认值」会被误判成显式传参
- 表名不进路径段:独立库走 /table?table=xxx。url.PathEscape 挡不住 "..",让它进
  路径段会被规范化到上一级、打到别的接口上(App 支路的 /tables/{table} 属存量债)
- DryRun 与 Execute 共用同一个分派函数:dry-run 是 agent 唯一的自检手段
- +db-execute 独立库支路不发 transactional:该接口固定 DBA 模式,给个恒为 false 的
  参数只会让人以为传 true 能拿到事务
- +db-mcp-* 两支路径域段不对称:/databases/{id}/mcp vs /apps/{id}/db/mcp(多一段 db/)
- 输出不伪造服务端状态:+db-delete 的 deleted、+db-mcp-* 的 enabled 都取服务端结果

errclass 登记 3 个业务码(400002900 / 400002901 / 500002901)。这批是设计文档里的
占位码,服务端发号后需回来对齐真值;未命中时框架兜底成 api/unknown。

测试:新增 10 个测试文件(一命令一测),apps 域命令数断言 96 → 105。
BOE e2e(存量套件、只走 App 支路)291 用例 287 通过,2 个失败是 BOE 审计链路的
服务端故障(dataloom 调火山 ModifyTransmissionTask 返 400 及其级联),与本次改动无关。

服务端接口尚未实现(IDL 见 idl-larkgw 的 feat/standalone_db),故 --database-id
支路目前只能验到 dry-run 与参数校验层。
2026-08-13 21:08:51 +08:00

110 lines
4.0 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 (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package apps
import (
"context"
"fmt"
"io"
"strings"
"github.com/larksuite/cli/shortcuts/common"
)
// DB MCP 三条命令(+db-mcp-enable / -disable / -get)共用的接线。
//
// 三条都收「--app-id 或 --database-id」二选一。
// 【两支路径的域段不对称】:独立 DB 是 /databases/{id}/mcp[/action],妙搭 App 是
// /apps/{id}/db/mcp[/action] —— 多一段 db/。所以分派靠 dbMcpPath 而不是拼一个通用 suffix。
const (
dbMcpAppIDDesc = "Miaoda app id (mutually exclusive with --database-id)"
dbMcpDatabaseIDDesc = "standalone database id (mutually exclusive with --app-id)"
dbMcpHint = "verify the identifier: --app-id for a Miaoda app, --database-id for a standalone database (`lark-cli apps +db-list`)"
)
// dbMcpTarget 把二选一的解析结果收敛成「URL + 回显字段名 + 回显值」。
// 回显字段名随资源类型变化(app_id / database_id),这是设计文档要求的输出形态。
//
// DryRun 与 Execute 都经这一个函数取 URL —— dry-run 是 agent 唯一的自检手段,
// 预览与真实请求不一致会让这个手段失效。
func dbMcpTarget(rctx *common.RuntimeContext, action string) (url, idKey, idValue string, err error) {
kind, id, err := requireDBResource(rctx)
if err != nil {
return "", "", "", err
}
if kind == dbResourceDatabase {
return databaseMcpPath(id, action), "database_id", id, nil
}
return appDbMcpPath(id, action), "app_id", id, nil
}
// dbMcpSwitchShortcut 造 +db-mcp-enable / +db-mcp-disable —— 两条命令除动作词与文案外完全同构,
// 各自手写只会让它们随时间长歪(一边改了 hint / 输出形态另一边忘改)。
func dbMcpSwitchShortcut(action, pastTense string) common.Shortcut {
verb := capitalizeFirst(action)
return common.Shortcut{
Service: appsService,
Command: "+db-mcp-" + action,
Description: verb + " DB MCP for a Miaoda app or a standalone database",
Risk: "write",
Tips: []string{
fmt.Sprintf("Example: lark-cli apps +db-mcp-%s --database-id <database_id>", action),
"Switch semantics are idempotent: repeating the call returns the current state.",
},
Scopes: []string{"spark:app:write"},
AuthTypes: []string{"user"},
HasFormat: true,
Flags: dbResourceFlags(dbMcpAppIDDesc, dbMcpDatabaseIDDesc),
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
_, _, _, err := dbMcpTarget(rctx, action)
return err
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
url, idKey, _, _ := dbMcpTarget(rctx, action)
return common.NewDryRunAPI().POST(url).Desc(verb + " DB MCP for " + dbResourceLabel(idKey))
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
url, idKey, idValue, err := dbMcpTarget(rctx, action)
if err != nil {
return err
}
data, err := rctx.CallAPITyped("POST", url, nil, nil)
if err != nil {
return withAppsHint(err, dbMcpHint)
}
// enabled 取服务端结果,不按动作词伪造:这是开关命令,输出必须反映服务端实际状态,
// 否则「以为开了其实没开」在使用时查不出来。
out := map[string]interface{}{idKey: idValue, "enabled": data["enabled"]}
rctx.OutFormat(out, nil, func(w io.Writer) {
fmt.Fprintf(w, "✓ DB MCP %s: %s\n", pastTense, idValue)
})
return nil
},
}
}
// dbResourceLabel 供 dry-run 的 desc 用,措辞随资源类型分支。
func dbResourceLabel(idKey string) string {
if idKey == "app_id" {
return "a Miaoda app database"
}
return "a standalone database"
}
// flagNameForIDKey 把回显字段名换回 flag 名(app_id → app-id),供 hint 给出可直接执行的命令。
func flagNameForIDKey(idKey string) string {
if idKey == "app_id" {
return "app-id"
}
return "database-id"
}
func capitalizeFirst(s string) string {
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
}