Files
larksuite__cli/shortcuts/base/app_block_create.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

107 lines
5.7 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package base
import (
"context"
"encoding/json"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
)
var BaseAppBlockCreate = common.Shortcut{
Service: "base",
Command: "+app-block-create",
Description: "Create a block on a BaseApp page",
Risk: "write",
Scopes: []string{"base:appmode_block:create", "base:appmode_block:read"},
AuthTypes: authTypes(),
Flags: []common.Flag{
appTokenFlag(true),
pageIDFlag(true),
{Name: "name", Desc: "block name", Required: true},
{Name: "type", Desc: "block type: chart(column|bar|line|pie|ring|area|combo|scatter|funnel|wordCloud|radar|statistics) | text | list. Read lark-base-app-block-data-config.md before creating.", Required: true, Enum: appBlockTypes()},
{Name: "sub-type", Desc: "list subtype: standard|grouped|collapsible|card|detail; defaults to standard", Enum: appListSubTypes},
{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 validation and normalization; send data_config as-is"},
},
Tips: []string{
`lark-cli base +app-block-create --app-token <app_token> --page-id <page_id> --name "Order Count" --type statistics --data-config '{"base_token":"basxxx","data_sources":[{"table_name":"Orders","count_all":true}]}'`,
`lark-cli base +app-block-create --app-token <app_token> --page-id <page_id> --name "Monthly sales" --type column --data-config '{"base_token":"basxxx","data_sources":[{"table_name":"Orders","series":[{"field_name":"Amount","rollup":"SUM"}],"group_by":[{"field_name":"Month","sort":{"type":"group","order":"asc"}}]}]}'`,
"Chart blocks use multi-datasource data_config: one top-level base_token shared by all sources, with table_name/series/count_all/group_by/filter inside each data_sources[] element (text needs none). App block commands carry no --base-token.",
`lark-cli base +app-block-create --app-token <app_token> --page-id <page_id> --name "Notes" --type text --data-config '{"text":"# Sales overview"}'`,
`lark-cli base +app-block-create --app-token <app_token> --page-id <page_id> --name "Open orders" --type list --sub-type standard --data-config '{"base_token":"basxxx","table_name":"Orders"}'`,
"For list creates, omit optional columns/fields to use the product defaults. The CLI sends them only when explicitly provided.",
"Before creating data-backed blocks, use +table-list and +field-list to confirm real table and field names.",
"A list accepts exactly one base_token, and that Base must be in the same Workspace as the App.",
"Read lark-base-app-block-data-config.md as the SSOT for chart, list and text config; do not invent data_config from natural language.",
"Block type cannot be changed after creation and this phase has no delete command, so a wrong --type can only be fixed in the UI. Confirm the type before creating.",
"Widget layout, position, size and display settings are not part of the public create/update protocol; the platform applies product defaults.",
"Record block_id for +app-block-update. For chart data reads, pass the returned chart_token to +app-block-get-data --block-id.",
"Block names must be unique within the page; the CLI checks every existing block before creation.",
"Create blocks sequentially; do not parallelize multiple block creates for the same page.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
blockType := strings.TrimSpace(runtime.Str("type"))
if !isAppBlockType(blockType) {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--type %q 不在支持的 block 类型内: %s", blockType, strings.Join(appBlockTypes(), ", ")).WithParam("--type")
}
raw := strings.TrimSpace(runtime.Str("data-config"))
noValidate := runtime.Bool("no-validate")
var cfg map[string]interface{}
if raw != "" && !noValidate {
var err error
cfg, err = parseJSONObject(newParseCtx(runtime), raw, "data-config")
if err != nil {
return err
}
}
if strings.EqualFold(blockType, "list") {
subType, ok := normalizeAppListSubType(runtime.Str("sub-type"))
if !ok {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--sub-type 仅支持 %s", strings.Join(appListSubTypes, "|")).WithParam("--sub-type")
}
if cfg != nil {
if problems := validateAppListDataConfig(subType, cfg); len(problems) > 0 {
return formatDataConfigErrors(problems)
}
}
} else if strings.TrimSpace(runtime.Str("sub-type")) != "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--sub-type 仅适用于 list 类型组件").WithParam("--sub-type")
}
if raw == "" {
if strings.EqualFold(blockType, "list") || isChartBlockType(blockType) {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s 类型组件必须提供 data-config", blockType).WithParam("--data-config")
}
return nil
}
if noValidate {
return nil
}
norm := cfg
if !strings.EqualFold(blockType, "list") {
// Chart blocks use the multi-datasource ChartDataConfig shape
// (base_token top-level, table_name/series/count_all/group_by/filter
// per data_sources[] element); text keeps the flat text shape.
if isChartBlockType(blockType) {
norm = normalizeAppChartDataConfig(cfg)
} else {
norm = normalizeDataConfig(cfg)
}
if problems := validateAppBlockDataConfig(blockType, norm); len(problems) > 0 {
return formatDataConfigErrors(problems)
}
}
b, _ := json.Marshal(norm)
_ = runtime.Cmd.Flags().Set("data-config", string(b))
return nil
},
DryRun: dryRunAppBlockCreate,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeAppBlockCreate(runtime)
},
}