mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
723f884e9b
* feat(base): add BaseApp workspace, page and block shortcuts Implement the CLI layer of the BaseApp CLI/OpenAPI protocol design: 17 new shortcuts covering workspace entities, blank app creation, page CRUD and page block CRUD, plus skill references and dry-run E2E for each. The data_config validator moves to a neutral block_data_config.go with chart logic unchanged; list and richText dispatch are new branches, so dashboard behaviour is untouched. Command spaces stay separate — dashboard commands never take --app-token and app block commands never take --dashboard-id. The one exception is +app-block-get-data, which shares the dashboard endpoint, execute and dry-run hooks and therefore takes --base-token instead of --app-token. This phase ships no +app-block-delete and no page arrange command; both the help text and the skill docs spell out that a block type cannot be changed after creation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(base): implement app mode shortcuts and list components * feat: support deleting BaseApp via drive delete * fix: 修复 workspace scope * fix: correct BaseApp permission scopes * feat: 新增 moveIn workspace 逻辑 * feat(base): support multi-datasource data_config for BaseApp charts BaseApp page charts follow section 8 图表协议 of the App CLI RPC 协议, which differs from dashboard charts by supporting multiple data sources: base_token is a single top-level value shared by every source, while table_name/series/count_all/group_by/filter move into each data_sources[] element (plus top-level data_source_mode and sort). The per-source value semantics are identical to dashboard charts, so each data_sources[] element reuses normalizeDataConfig / validateChartDataConfig; the wrapper only adds the top-level structure. Dashboard charts keep the flat shape; the list protocol is untouched. - block_data_config.go: add normalizeAppChartDataConfig / validateAppChartDataConfig / validateAppBlockDataConfig - app_block_create/update: route chart blocks to the multi-datasource normalize/validate; refresh tips and examples - reference doc: rewrite the chart section for the multi-datasource shape - unit + e2e tests: migrate chart cases to data_sources; add a multi-datasource combo case Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(base): map BaseApp richText block type to the wire type "text" The rich-text widget's API type is "text" (App CLI RPC 协议 §10), but the CLI exposes the friendlier "richText" alias and was sending it verbatim, so the backend rejected +app-block-create --type richText with "type is invalid". Map richText -> text when building the request body; the user-facing --type richText is unchanged. Add TestAppRichTextTypeMapsToText. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(base): remove internal protocol doc link/reference from skills The BaseApp skill references pointed at an internal Lark doc (deep link with a private token) as the source of truth, which must not ship in this repo. Drop the link and the doc name entirely from the reference markdown and from code comments; describe behavior in neutral terms ("服务端协议 / 服务端返回和校验") instead. No functional change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: create workspace for BaseApp when omitted * fix(base): align BaseApp block protocol * fix: 删掉废弃的 workspace-entity-remove * fix(base): align app get reference response * fix(base): align app shortcuts with API contract * fix(base): refine app mode shortcut contracts * fix: use entity_type for workspace entity filtering * fix: return workspace URLs for base workspace ops * fix(base): enforce unique app block names * fix(base): use chart token for app block data * 明确baseapp边界,不导向到dashboard-arrange * docs(base): clarify app copy is unsupported * docs(base): define unsupported app page operations * fix(base): align app block text type with dashboard AppMode 的文本组件此前对外叫 richText,发送时再映射成 wire 上的 text, 而读取方向没有反向映射:写进去用 richText、读回来是 text,同一个 CLI 表面自相矛盾,回填或幂等复建时会被枚举校验拒掉。 统一成 text,与 Dashboard 文本组件同名同义: - appBlockTypes/isAppBlockType/textBlockTypes 去掉 richText - 删除 appBlockBody 里的 richText → text 发送期映射 - help、枚举、示例、tips 与 baseapp block data_config reference 同步 - 新增回归测试,确保 richText 不再被接受也不再出现在枚举里 richText 不保留别名:+app-* 尚未随已发布版本对外,无存量调用方。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(base): resolve BaseApp URLs Migrate the net changes from bitable/base_cli!1308 onto the current BaseApp development branch. * fix(base): remove app block list type filter * docs: preserve explicit intent when reusing BaseApp blocks * fix(base): route +app-block-get-data to base_apps endpoint Move the shortcut off the dashboard route and onto the dedicated BaseApp block-data endpoint: - URL: /open-apis/base/v3/base_apps/:app_token/blocks/:block_id/data - base_token is passed as a required query parameter per the new IDL - Refresh --block-id description and tips to list all producers of the chart_token (create/list/get) and note the cht… prefix - Update the dryrun test to expect the new URL Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: clarify unsupported BaseApp copy paths * fix: front-load BaseApp copy stop rule * fix: surface unsupported PageGroup operations * fix: preserve PageGroup support boundary * docs(base): clarify unsupported app block handling * docs(base): clarify how to read text block content Text blocks have no /data endpoint; calling +app-block-get-data on one returns a generic server 500. Point readers at +app-block-get, whose data_config.text carries the Markdown source. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: clarify immutable BaseApp block types * docs(base): correct chart date filter format * docs(base): explain inaccessible app pages * docs: require workspace removal lookup * fix(base): return workspace move-in result faithfully * chore(base): adapt BaseApp changes to upstream main * fix(base): align app mode changes with upstream scope * fix(base): address app mode review feedback * test(base): assert app data transport error contract * fix(base): satisfy app mode merge requirements * refactor(base): align app mode filenames * docs: fix BaseApp rename guidance * fix(base): remove unsupported workspace icon * docs(base): clarify app mode concepts and config reuse --------- Co-authored-by: weibiao.x <weibiao.x@bytedance.com> Co-authored-by: zhangbinkai.zbk <zhangbinkai.zbk@bytedance.com> Co-authored-by: yurunjie <yurunjie.xx@bytedance.com> Co-authored-by: Codex <codex@example.com>
135 lines
5.2 KiB
Go
135 lines
5.2 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package base
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/larksuite/cli/errs"
|
||
"github.com/larksuite/cli/shortcuts/common"
|
||
)
|
||
|
||
var BaseAppBlockUpdate = common.Shortcut{
|
||
Service: "base",
|
||
Command: "+app-block-update",
|
||
Description: "Update a block on a BaseApp page",
|
||
Risk: "write",
|
||
Scopes: []string{"base:appmode_block:update", "base:appmode_block:read"},
|
||
AuthTypes: authTypes(),
|
||
Flags: []common.Flag{
|
||
appTokenFlag(true),
|
||
pageIDFlag(true),
|
||
appBlockIDFlag(true),
|
||
{Name: "name", Desc: "new block name"},
|
||
{Name: "data-config", Desc: "data_config JSON object; read lark-base-app-block-data-config.md for the SSOT"},
|
||
{Name: "no-validate", Type: "bool", Desc: "skip local data_config normalization; send data_config as-is"},
|
||
},
|
||
Tips: []string{
|
||
`lark-cli base +app-block-update --app-token <app_token> --page-id <page_id> --block-id <block_id> --name "Monthly sales"`,
|
||
`lark-cli base +app-block-update --app-token <app_token> --page-id <page_id> --block-id <block_id> --data-config '{"base_token":"basxxx","data_sources":[{"table_name":"Orders","count_all":true,"filter":{"conjunction":"and","conditions":[{"field_name":"Status","operator":"is","value":"Closed"}]}}]}'`,
|
||
"Do not call this command for a component whose +app-block-list result has type=unsupported; the API will return an error.",
|
||
"Read lark-base-app-block-data-config.md as the SSOT; do not invent data_config from natural language.",
|
||
"Use +app-block-get first to inspect the current data_config before replacing nested values.",
|
||
"The type and sub_type of an existing Block are immutable after creation and are not part of data_config; +app-block-update accepts only the name and data_config fields. If a user asks to change type/sub_type, read the current Block and always state this constraint in the final answer, even when it already matches and no write is needed; if it differs, it can only be fixed in the UI.",
|
||
"Only explicitly provided data_config fields are sent; omitted fields stay unchanged. For charts, passing data_sources replaces the whole ordered array, and changing base_token requires sending the full data_sources.",
|
||
"Widget layout, position, size and display settings are not part of the public create/update protocol.",
|
||
},
|
||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||
name := strings.TrimSpace(runtime.Str("name"))
|
||
raw := strings.TrimSpace(runtime.Str("data-config"))
|
||
if name == "" && raw == "" {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--name 与 --data-config 至少提供一个").WithParam("--name")
|
||
}
|
||
if runtime.Bool("no-validate") {
|
||
return nil
|
||
}
|
||
if raw == "" {
|
||
return nil
|
||
}
|
||
pc := newParseCtx(runtime)
|
||
cfg, err := parseJSONObject(pc, raw, "data-config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if containsJSONNull(cfg) {
|
||
return formatDataConfigErrors([]string{"Update 不接受 null 作为清空标记"})
|
||
}
|
||
if problems := validateAppBlockUpdateTopLevelFields(cfg); len(problems) > 0 {
|
||
return formatDataConfigErrors(problems)
|
||
}
|
||
// update 不传 type,无法做强类型校验;按多数据源图表结构归一化
|
||
// (data_sources[] 存在时逐项归一化,否则原样透传)。
|
||
norm := normalizeAppChartDataConfig(cfg)
|
||
if sources, exists := norm["data_sources"]; exists {
|
||
items, ok := sources.([]interface{})
|
||
if !ok || len(items) == 0 {
|
||
return formatDataConfigErrors([]string{"data_sources 一旦传入,必须是至少包含一项的完整有序数组"})
|
||
}
|
||
var problems []string
|
||
for i, rawSource := range items {
|
||
source, ok := rawSource.(map[string]interface{})
|
||
if !ok {
|
||
problems = append(problems, fmt.Sprintf("data_sources[%d] 必须是对象", i))
|
||
continue
|
||
}
|
||
for _, problem := range validateAppChartDataSourceConfig(source) {
|
||
problems = append(problems, fmt.Sprintf("data_sources[%d]: %s", i, problem))
|
||
}
|
||
}
|
||
if len(problems) > 0 {
|
||
return formatDataConfigErrors(problems)
|
||
}
|
||
}
|
||
b, _ := json.Marshal(norm)
|
||
_ = runtime.Cmd.Flags().Set("data-config", string(b))
|
||
return nil
|
||
},
|
||
DryRun: dryRunAppBlockUpdate,
|
||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||
return executeAppBlockUpdate(runtime)
|
||
},
|
||
}
|
||
|
||
func validateAppBlockUpdateTopLevelFields(cfg map[string]interface{}) []string {
|
||
allowed := map[string]bool{
|
||
// Chart.
|
||
"base_token": true, "data_sources": true, "data_source_mode": true, "sort": true,
|
||
// List.
|
||
"table_name": true, "filter": true, "sort_by": true, "columns": true,
|
||
"group_by": true, "fields": true, "card_config": true, "detail_config": true,
|
||
// Text.
|
||
"text": true,
|
||
}
|
||
var problems []string
|
||
for key := range cfg {
|
||
if !allowed[key] {
|
||
problems = append(problems, fmt.Sprintf("data_config 不支持字段 %s", key))
|
||
}
|
||
}
|
||
return problems
|
||
}
|
||
|
||
func containsJSONNull(value interface{}) bool {
|
||
switch typed := value.(type) {
|
||
case nil:
|
||
return true
|
||
case map[string]interface{}:
|
||
for _, item := range typed {
|
||
if containsJSONNull(item) {
|
||
return true
|
||
}
|
||
}
|
||
case []interface{}:
|
||
for _, item := range typed {
|
||
if containsJSONNull(item) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|