diff --git a/shortcuts/apps/apps_db_audit_list.go b/shortcuts/apps/apps_db_audit_list.go index 8d7e3d62c..1b3545bf7 100644 --- a/shortcuts/apps/apps_db_audit_list.go +++ b/shortcuts/apps/apps_db_audit_list.go @@ -145,7 +145,10 @@ func fetchExistingTables(rctx *common.RuntimeContext, appID, env string) (map[st existing := map[string]bool{} token := "" for { - params := map[string]interface{}{"env": env, "page_size": 100} + params := map[string]interface{}{"page_size": 100} + if env != "" { + params["env"] = env + } if token != "" { params["page_token"] = token } @@ -168,7 +171,11 @@ func fetchExistingTables(rctx *common.RuntimeContext, appID, env string) (map[st // fetchAuditEnabledTables 拉审计状态,返回当前已开启审计的表名集合(status 命令同源接口)。 func fetchAuditEnabledTables(rctx *common.RuntimeContext, appID, env string) (map[string]bool, error) { - data, err := rctx.CallAPITyped("GET", appAuditStatusPath(appID), map[string]interface{}{"env": env}, nil) + statusParams := map[string]interface{}{} + if env != "" { + statusParams["env"] = env + } + data, err := rctx.CallAPITyped("GET", appAuditStatusPath(appID), statusParams, nil) if err != nil { return nil, err } @@ -208,11 +215,10 @@ func auditListTables(rctx *common.RuntimeContext) []string { // buildAuditListParams 组装 audit_list 查询参数:env / tables(逗号拼接) / page_size 及可选 since/until/page_token。 func buildAuditListParams(rctx *common.RuntimeContext, tables []string) map[string]interface{} { - params := map[string]interface{}{ - "env": dbEnv(rctx), + params := dbEnvParams(rctx, map[string]interface{}{ "tables": strings.Join(tables, ","), "page_size": rctx.Int("page-size"), - } + }) addStr := func(flag, key string) { if v := strings.TrimSpace(rctx.Str(flag)); v != "" { params[key] = v diff --git a/shortcuts/apps/apps_db_audit_set.go b/shortcuts/apps/apps_db_audit_set.go index 4cb842c36..5833d3a86 100644 --- a/shortcuts/apps/apps_db_audit_set.go +++ b/shortcuts/apps/apps_db_audit_set.go @@ -47,7 +47,7 @@ var AppsDBAuditEnable = common.Shortcut{ return common.NewDryRunAPI(). POST(appAuditSetPath(appID)). Desc("Enable table audit"). - Params(map[string]interface{}{"env": dbEnv(rctx)}). + Params(dbEnvParams(rctx, map[string]interface{}{})). Body(map[string]interface{}{"table": strings.TrimSpace(rctx.Str("table")), "enabled": true, "retention": rctx.Str("retention")}) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { @@ -60,7 +60,7 @@ var AppsDBAuditEnable = common.Shortcut{ stop := rctx.StartSpinner("Enabling audit logging for " + table) defer stop() data, err := rctx.CallAPITyped("POST", appAuditSetPath(appID), - map[string]interface{}{"env": dbEnv(rctx)}, + dbEnvParams(rctx, map[string]interface{}{}), map[string]interface{}{"table": table, "enabled": true, "retention": retention}) stop() if err != nil { @@ -108,7 +108,7 @@ var AppsDBAuditDisable = common.Shortcut{ return common.NewDryRunAPI(). POST(appAuditSetPath(appID)). Desc("Disable table audit"). - Params(map[string]interface{}{"env": dbEnv(rctx)}). + Params(dbEnvParams(rctx, map[string]interface{}{})). Body(map[string]interface{}{"table": strings.TrimSpace(rctx.Str("table")), "enabled": false}) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { @@ -118,7 +118,7 @@ var AppsDBAuditDisable = common.Shortcut{ } table := strings.TrimSpace(rctx.Str("table")) data, err := rctx.CallAPITyped("POST", appAuditSetPath(appID), - map[string]interface{}{"env": dbEnv(rctx)}, + dbEnvParams(rctx, map[string]interface{}{}), map[string]interface{}{"table": table, "enabled": false}) if err != nil { return withAppsHint(err, dbAuditSetHint) diff --git a/shortcuts/apps/apps_db_audit_status.go b/shortcuts/apps/apps_db_audit_status.go index 7f5b8faeb..6221eea36 100644 --- a/shortcuts/apps/apps_db_audit_status.go +++ b/shortcuts/apps/apps_db_audit_status.go @@ -75,7 +75,7 @@ var AppsDBAuditStatus = common.Shortcut{ // buildAuditStatusParams 组装 audit_status 查询参数:env 及可选 table(单表查询)。 func buildAuditStatusParams(rctx *common.RuntimeContext) map[string]interface{} { - params := map[string]interface{}{"env": dbEnv(rctx)} + params := dbEnvParams(rctx, map[string]interface{}{}) if t := strings.TrimSpace(rctx.Str("table")); t != "" { params["table"] = t } diff --git a/shortcuts/apps/apps_db_changelog_list.go b/shortcuts/apps/apps_db_changelog_list.go index 1b9011960..d007b4b02 100644 --- a/shortcuts/apps/apps_db_changelog_list.go +++ b/shortcuts/apps/apps_db_changelog_list.go @@ -77,10 +77,9 @@ var AppsDBChangelogList = common.Shortcut{ // buildChangelogParams 组装 changelog_list 查询参数:env / page_size 及可选 table/change_id/since/until/page_token。 func buildChangelogParams(rctx *common.RuntimeContext) map[string]interface{} { - params := map[string]interface{}{ - "env": dbEnv(rctx), + params := dbEnvParams(rctx, map[string]interface{}{ "page_size": rctx.Int("page-size"), - } + }) addStr := func(flag, key string) { if v := strings.TrimSpace(rctx.Str(flag)); v != "" { params[key] = v diff --git a/shortcuts/apps/apps_db_data_export.go b/shortcuts/apps/apps_db_data_export.go index fa8ce12a1..9ac45e689 100644 --- a/shortcuts/apps/apps_db_data_export.go +++ b/shortcuts/apps/apps_db_data_export.go @@ -75,10 +75,10 @@ var AppsDBDataExport = common.Shortcut{ return common.NewDryRunAPI(). GET(appDataExportPath(appID)). Desc("Export Miaoda app table data (raw bytes)"). - Params(map[string]interface{}{ - "env": dbEnv(rctx), "table": strings.TrimSpace(rctx.Str("table")), + Params(dbEnvParams(rctx, map[string]interface{}{ + "table": strings.TrimSpace(rctx.Str("table")), "format": format, "limit": rctx.Int("limit"), - }) + })) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID, err := requireAppID(rctx.Str("app-id")) @@ -95,15 +95,18 @@ var AppsDBDataExport = common.Shortcut{ // total 查询失败不阻断导出——回退到按导出文件内容数行。 total, totalErr := queryExportTotal(rctx, appID, dbEnv(rctx), table) + exportQuery := larkcore.QueryParams{ + "table": []string{table}, + "format": []string{format}, + "limit": []string{strconv.Itoa(rctx.Int("limit"))}, + } + if env := dbEnv(rctx); env != "" { + exportQuery["env"] = []string{env} + } resp, err := rctx.DoAPI(&larkcore.ApiReq{ - HttpMethod: http.MethodGet, - ApiPath: appDataExportPath(appID), - QueryParams: larkcore.QueryParams{ - "env": []string{dbEnv(rctx)}, - "table": []string{table}, - "format": []string{format}, - "limit": []string{strconv.Itoa(rctx.Int("limit"))}, - }, + HttpMethod: http.MethodGet, + ApiPath: appDataExportPath(appID), + QueryParams: exportQuery, }) if err != nil { return withAppsHint(errs.NewNetworkError(errs.SubtypeNetworkTransport, "export request failed").WithCause(err).WithRetryable(), dbDataExportHint) @@ -157,8 +160,11 @@ var AppsDBDataExport = common.Shortcut{ // queryExportTotal 调 GetAppTableRecordList(page_size=1)取 total(符合条件的记录总数)。 // 该接口与 +db-data-export 同为 spark:app:read scope,避免导出命令被迫升级到写权限。 func queryExportTotal(rctx *common.RuntimeContext, appID, env, table string) (int, error) { - raw, err := rctx.CallAPITyped("GET", appTableRecordsPath(appID, table), - map[string]interface{}{"env": env, "page_size": 1}, nil) + params := map[string]interface{}{"page_size": 1} + if env != "" { + params["env"] = env + } + raw, err := rctx.CallAPITyped("GET", appTableRecordsPath(appID, table), params, nil) if err != nil { return 0, err } diff --git a/shortcuts/apps/apps_db_data_import.go b/shortcuts/apps/apps_db_data_import.go index 56762dff2..ac94906a0 100644 --- a/shortcuts/apps/apps_db_data_import.go +++ b/shortcuts/apps/apps_db_data_import.go @@ -76,7 +76,7 @@ var AppsDBDataImport = common.Shortcut{ return common.NewDryRunAPI(). POST(appDataImportPath(appID)). Desc("Import data file into Miaoda app table (multipart upload)"). - Params(map[string]interface{}{"env": dbEnv(rctx), "table": importTableName(rctx)}). + Params(dbEnvParams(rctx, map[string]interface{}{"table": importTableName(rctx)})). Body(map[string]interface{}{"file_name": fileName, "file": ""}) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { @@ -100,10 +100,14 @@ var AppsDBDataImport = common.Shortcut{ fd.AddField("file_name", fileName) fd.AddFile("file", bytes.NewReader(content)) + importQuery := larkcore.QueryParams{"table": []string{table}} + if env := dbEnv(rctx); env != "" { + importQuery["env"] = []string{env} + } resp, err := rctx.DoAPI(&larkcore.ApiReq{ HttpMethod: http.MethodPost, ApiPath: appDataImportPath(appID), - QueryParams: larkcore.QueryParams{"env": []string{dbEnv(rctx)}, "table": []string{table}}, + QueryParams: importQuery, Body: fd, }, larkcore.WithFileUpload()) if err != nil { diff --git a/shortcuts/apps/apps_db_execute.go b/shortcuts/apps/apps_db_execute.go index ba5785476..ceae294ca 100644 --- a/shortcuts/apps/apps_db_execute.go +++ b/shortcuts/apps/apps_db_execute.go @@ -291,10 +291,9 @@ func parseErrorSentinel(data string) (int, string) { // // CLI 永远走 DBA 模式,原子性由用户在 SQL 内显式 BEGIN/COMMIT 控制;不暴露 transactional flag 给用户。 func buildDBSQLParams(rctx *common.RuntimeContext) map[string]interface{} { - return map[string]interface{}{ - "env": dbEnv(rctx), + return dbEnvParams(rctx, map[string]interface{}{ "transactional": false, - } + }) } // resolveExecuteSQL 返回要执行的 SQL,在用时(DryRun/Execute)现读,使 --file 的内容 diff --git a/shortcuts/apps/apps_db_quota_get.go b/shortcuts/apps/apps_db_quota_get.go index 379b830f7..ab7abca33 100644 --- a/shortcuts/apps/apps_db_quota_get.go +++ b/shortcuts/apps/apps_db_quota_get.go @@ -41,14 +41,14 @@ var AppsDBQuotaGet = common.Shortcut{ return common.NewDryRunAPI(). GET(appDbQuotaPath(appID)). Desc("Get Miaoda app database storage usage"). - Params(map[string]interface{}{"env": dbEnv(rctx)}) + Params(dbEnvParams(rctx, map[string]interface{}{})) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID, err := requireAppID(rctx.Str("app-id")) if err != nil { return err } - data, err := rctx.CallAPITyped("GET", appDbQuotaPath(appID), map[string]interface{}{"env": dbEnv(rctx)}, nil) + data, err := rctx.CallAPITyped("GET", appDbQuotaPath(appID), dbEnvParams(rctx, map[string]interface{}{}), nil) if err != nil { return withAppsHint(err, appIDListHint) } diff --git a/shortcuts/apps/apps_db_table_get.go b/shortcuts/apps/apps_db_table_get.go index fe659e40e..91f6a8841 100644 --- a/shortcuts/apps/apps_db_table_get.go +++ b/shortcuts/apps/apps_db_table_get.go @@ -80,7 +80,7 @@ var AppsDBTableGet = common.Shortcut{ // CLI 检测 rctx.Format == "pretty" 时给 server 带 format=ddl,要求返 CREATE 语句文本; // 其他 format(含默认 json)不传该参数,让 server 返默认结构化字段。 func buildDBTableGetParams(rctx *common.RuntimeContext) map[string]interface{} { - params := map[string]interface{}{"env": dbEnv(rctx)} + params := dbEnvParams(rctx, map[string]interface{}{}) if rctx.Format == "pretty" { params["format"] = "ddl" } diff --git a/shortcuts/apps/apps_db_table_list.go b/shortcuts/apps/apps_db_table_list.go index 3a8818016..e5bcded2e 100644 --- a/shortcuts/apps/apps_db_table_list.go +++ b/shortcuts/apps/apps_db_table_list.go @@ -110,10 +110,9 @@ func projectTableListItems(raw interface{}) []dbTableListItem { } func buildDBTableListParams(rctx *common.RuntimeContext) map[string]interface{} { - params := map[string]interface{}{ - "env": dbEnv(rctx), + params := dbEnvParams(rctx, map[string]interface{}{ "page_size": rctx.Int("page-size"), - } + }) if token := strings.TrimSpace(rctx.Str("page-token")); token != "" { params["page_token"] = token } diff --git a/shortcuts/apps/db_common.go b/shortcuts/apps/db_common.go index 982c9bee2..3dc6b30ee 100644 --- a/shortcuts/apps/db_common.go +++ b/shortcuts/apps/db_common.go @@ -34,6 +34,16 @@ func dbEnv(rctx *common.RuntimeContext) string { return rctx.Str("environment") } +// dbEnvParams 把 env 并入 params:仅当显式指定了环境(非空)才带 env 键;未指定(空)时 +// 省略该键,由服务端按应用多环境状态自动选分支(多环境→dev,单环境→online)。与家族对 +// 空可选参数的 omit-empty 约定一致——不发空串,wire 上真正不带 env。原样返回同一个 map 便于链式。 +func dbEnvParams(rctx *common.RuntimeContext, params map[string]interface{}) map[string]interface{} { + if env := dbEnv(rctx); env != "" { + params["env"] = env + } + return params +} + // rejectLegacyEnvFlag 在 Validate 阶段拦截已移除的 --env:显式传了就报清晰的 validation 错,指向 --environment。 func rejectLegacyEnvFlag(rctx *common.RuntimeContext) error { if rctx.Changed("env") { diff --git a/skills/lark-apps/references/lark-apps-db.md b/skills/lark-apps/references/lark-apps-db.md index 6ecf31117..88f6e40c1 100644 --- a/skills/lark-apps/references/lark-apps-db.md +++ b/skills/lark-apps/references/lark-apps-db.md @@ -28,7 +28,7 @@ ## 约定(先读) -- **环境 `--environment dev|online`(不传则自动适配)**:看表、看结构、数据导入导出、变更追溯、审计、配额都按环境区分。**不传 `--environment` 时 CLI 不指定环境,由服务端按应用是否开启多环境自动选择——开启多环境的应用用 `dev`、未开启多环境的应用用 `online`**,因此单库应用不带 `--environment` 也能正常访问其 `online` 库(不会再因默认 `dev` 分支不存在而报错)。要固定环境就显式传 `--environment dev|online`;写操作建议先在 `dev` 验(仅多环境应用有 `dev`)。**注意:只有开启了多环境(`+db-env-create`)的应用才有 `dev` 分支——对未开启多环境的应用显式传 `--environment dev` 会报错,这类应用请不传 `--environment`(走 `online`)或显式 `--environment online`**。旧名 `--env` 已**移除**:传入会报 validation 错(提示改用 `--environment`),一律用 `--environment`。`+db-env-diff`/`+db-env-migrate` 是「dev→online 发布」语义、`+db-recovery-*` 作用于当前库,二者**没有** `--environment`。 +- **环境 `--environment dev|online`(可省略)**:看表、看结构、数据导入导出、变更追溯、审计、配额都按环境区分。省略 `--environment` 时 CLI 不带该参数、由服务端按应用形态自动选分支——多环境应用走 `dev`、未开多环境的走 `online`;要固定环境就显式传。唯一会报错的组合:对未开多环境的应用显式传 `--environment dev`(无 `dev` 分支)。写操作建议先在 `dev` 验(仅多环境应用有 `dev`)。旧名 `--env` 已**移除**:传入会报 validation 错(提示改用 `--environment`),一律用 `--environment`。`+db-env-diff`/`+db-env-migrate` 是「dev→online 发布」语义、`+db-recovery-*` 作用于当前库,二者**没有** `--environment`。 - **本地文件 / `--output` 用工作目录内相对路径**:导入 `--file ./orders.csv`、导出 `--output ./out.csv`;绝对路径、或经 `..`/符号链接越出工作目录的 `--output` 会被拒(validation / exit 2)。路径在别处先 `cd` 过去或改成相对路径。 - **高危操作必须带 `--yes`**:`+db-env-create`、`+db-data-import`、`+db-env-migrate`、`+db-recovery-apply` 缺省会被确认关卡拦下;动手前先用对应的预览命令或 `--dry-run` 看清影响。 - **时间参数按口语自然传**(`--since`/`--until`/`--target`),格式见末尾。 @@ -154,7 +154,7 @@ lark-cli apps +db-quota-get --app-id app_xxx --environment dev ## Agent 规则 -- 用户说「本地 / 开发库 / 调试库」优先 `--environment dev`,线上排查用 `--environment online`;数据面写操作(导入 / 审计开关)默认先在 `dev` 验再动 `online`。 +- 用户说「本地 / 开发库 / 调试库」优先 `--environment dev`,线上排查用 `--environment online`;数据面写操作(导入 / 审计开关)建议先在 `dev` 验再动 `online`。**注意省略 `--environment` 时写操作会落到服务端选中的分支——单环境应用即 `online`(生产)**:不确定应用是否多环境时,写操作显式传 `--environment`;显式 `dev` 在单环境应用上会安全报错(无 dev 分支),正好当「是否多环境」的探针用。 - 看表用 `+db-table-list`,看结构用 `+db-table-get`(要建表语句加 `--format pretty`);`+db-env-create` 仅用于存量单库拆多环境,新建的 full_stack 应用一般不需要。 - 四个高危命令(`+db-env-create`、`+db-data-import`、`+db-env-migrate`、`+db-recovery-apply`)动手前先看清影响再带 `--yes`:发布 / 恢复先跑对应预览 `+db-env-diff` / `+db-recovery-diff`,导入无预览命令、可先 `--dry-run` 看请求或先在 `--environment dev` 验;不要静默追加 `--yes`,遇 confirmation_required(exit 10)按 lark-shared 协议向用户确认不可逆风险后再补 `--yes` 重试。 - 导入 / 导出的本地路径用工作目录内相对路径;超大表导出会被行数 / 体积上限拒,改用 `+db-execute` 分批。 diff --git a/tests/cli_e2e/apps/apps_db_execute_dryrun_test.go b/tests/cli_e2e/apps/apps_db_execute_dryrun_test.go index ae7721c78..f801ebd29 100644 --- a/tests/cli_e2e/apps/apps_db_execute_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_db_execute_dryrun_test.go @@ -37,8 +37,8 @@ func TestAppsDBExecuteDryRun(t *testing.T) { "CLI is DBA mode → must send transactional=false in query") assert.False(t, gjson.Get(result.Stdout, "api.0.body.transactional").Exists(), "transactional should be in query, not body") - assert.Equal(t, "", gjson.Get(result.Stdout, "api.0.params.env").String(), - "default: no --environment → CLI sends empty env, server picks workspace default branch") + assert.False(t, gjson.Get(result.Stdout, "api.0.params.env").Exists(), + "default: no --environment → env key must be omitted (server picks workspace default branch)") }) t.Run("OnlineEnvSwitch", func(t *testing.T) { diff --git a/tests/cli_e2e/apps/apps_db_table_list_dryrun_test.go b/tests/cli_e2e/apps/apps_db_table_list_dryrun_test.go index bb63a424b..28e1e2eae 100644 --- a/tests/cli_e2e/apps/apps_db_table_list_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_db_table_list_dryrun_test.go @@ -32,8 +32,8 @@ func TestAppsDBTableListDryRun(t *testing.T) { assert.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String()) assert.Equal(t, "/open-apis/spark/v1/apps/app_x/tables", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "", gjson.Get(result.Stdout, "api.0.params.env").String(), - "default: no --environment → CLI sends empty env, server picks workspace default branch") + assert.False(t, gjson.Get(result.Stdout, "api.0.params.env").Exists(), + "default: no --environment → env key must be omitted (server picks workspace default branch)") assert.Equal(t, "20", gjson.Get(result.Stdout, "api.0.params.page_size").String()) assert.False(t, gjson.Get(result.Stdout, "api.0.params.page_token").Exists(), "empty page_token must be omitted")