mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
d7873bda96
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 与参数校验层。
110 lines
4.0 KiB
Go
110 lines
4.0 KiB
Go
// 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:]
|
||
}
|