Files
larksuite__cli/shortcuts/base/app_block_update.go
xiaomi-bytedance 723f884e9b feat(base): support BaseApp application mode (#2231)
* 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>
2026-08-13 16:20:34 +08:00

135 lines
5.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 (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
}