From 1bb37f85d43068968ccc9b0dc2084e479d8cf6f7 Mon Sep 17 00:00:00 2001 From: mobaijie Date: Mon, 25 May 2026 14:26:46 +0800 Subject: [PATCH] feat(base): add dashboard block data shortcut and workflow docs Change-Id: I52c471886bdb2d4b7be021ce86c34bbb78385017 --- shortcuts/base/base_dashboard_execute_test.go | 46 ++ shortcuts/base/base_shortcuts_test.go | 2 +- shortcuts/base/dashboard_block_get_data.go | 36 + shortcuts/base/dashboard_ops.go | 18 + shortcuts/base/shortcuts.go | 1 + shortcuts/register_test.go | 8 + skills/lark-base/SKILL.md | 3 +- .../lark-base-dashboard-block-get-data.md | 717 ++++++++++++++++++ .../references/lark-base-dashboard.md | 9 + .../references/lark-base-workflow-guide.md | 114 +++ .../references/lark-base-workflow-schema.md | 149 +++- .../references/lark-base-workflow-update.md | 2 +- ...se_dashboard_block_get_data_dryrun_test.go | 61 ++ 13 files changed, 1157 insertions(+), 9 deletions(-) create mode 100644 shortcuts/base/dashboard_block_get_data.go create mode 100644 skills/lark-base/references/lark-base-dashboard-block-get-data.md create mode 100644 tests/cli_e2e/base/base_dashboard_block_get_data_dryrun_test.go diff --git a/shortcuts/base/base_dashboard_execute_test.go b/shortcuts/base/base_dashboard_execute_test.go index 845446779..62f56d0d9 100644 --- a/shortcuts/base/base_dashboard_execute_test.go +++ b/shortcuts/base/base_dashboard_execute_test.go @@ -268,6 +268,39 @@ func TestBaseDashboardBlockExecuteGet(t *testing.T) { }) } +// TestBaseDashboardBlockExecuteGetData tests the +dashboard-block-get-data command. +func TestBaseDashboardBlockExecuteGetData(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/dashboards/blocks/blk_chart/data", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "dimensions": []interface{}{ + map[string]interface{}{"field_name": "文本", "alias": "dim_text"}, + }, + "measures": []interface{}{ + map[string]interface{}{"field_name": "Bitable_Dashboard_Count", "aggregation": "count_all", "alias": "me_count"}, + }, + "main_data": []interface{}{ + map[string]interface{}{ + "dim_text": map[string]interface{}{"value": "A"}, + "me_count": map[string]interface{}{"value": 3}, + }, + }, + }, + }, + }) + if err := runShortcut(t, BaseDashboardBlockGetData, []string{"+dashboard-block-get-data", "--base-token", "app_x", "--block-id", "blk_chart"}, factory, stdout); err != nil { + t.Fatalf("err=%v", err) + } + got := stdout.String() + if !strings.Contains(got, `"dimensions"`) || !strings.Contains(got, `"main_data"`) || !strings.Contains(got, `"dim_text"`) { + t.Fatalf("stdout=%s", got) + } +} + // TestBaseDashboardBlockExecuteCreate tests the +dashboard-block-create command. func TestBaseDashboardBlockExecuteCreate(t *testing.T) { t.Run("with data-config", func(t *testing.T) { @@ -537,6 +570,19 @@ func TestBaseDashboardBlockDryRun_Get(t *testing.T) { } } +// TestBaseDashboardBlockDryRun_GetData tests the +dashboard-block-get-data --dry-run flag. +func TestBaseDashboardBlockDryRun_GetData(t *testing.T) { + factory, stdout, _ := newExecuteFactory(t) + args := []string{"+dashboard-block-get-data", "--base-token", "app_x", "--block-id", "blk_a", "--dry-run", "--format", "pretty"} + if err := runShortcut(t, BaseDashboardBlockGetData, args, factory, stdout); err != nil { + t.Fatalf("err=%v", err) + } + got := stdout.String() + if !strings.Contains(got, "GET /open-apis/base/v3/bases/app_x/dashboards/blocks/blk_a/data") || !strings.Contains(got, "blk_a") { + t.Fatalf("stdout=%s", got) + } +} + // TestBaseDashboardBlockDryRun_Create tests the +dashboard-block-create --dry-run flag. func TestBaseDashboardBlockDryRun_Create(t *testing.T) { factory, stdout, _ := newExecuteFactory(t) diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index 9f75ac763..3e3926566 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -146,7 +146,7 @@ func TestShortcutsCatalog(t *testing.T) { "+form-questions-create", "+form-questions-delete", "+form-questions-update", "+form-questions-list", "+form-submit", "+dashboard-list", "+dashboard-get", "+dashboard-create", "+dashboard-update", "+dashboard-delete", "+dashboard-arrange", - "+dashboard-block-list", "+dashboard-block-get", "+dashboard-block-create", "+dashboard-block-update", "+dashboard-block-delete", + "+dashboard-block-list", "+dashboard-block-get", "+dashboard-block-get-data", "+dashboard-block-create", "+dashboard-block-update", "+dashboard-block-delete", } if len(shortcuts) != len(want) { t.Fatalf("len(shortcuts)=%d want=%d", len(shortcuts), len(want)) diff --git a/shortcuts/base/dashboard_block_get_data.go b/shortcuts/base/dashboard_block_get_data.go new file mode 100644 index 000000000..184e6353f --- /dev/null +++ b/shortcuts/base/dashboard_block_get_data.go @@ -0,0 +1,36 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + + "github.com/larksuite/cli/shortcuts/common" +) + +var BaseDashboardBlockGetData = common.Shortcut{ + Service: "base", + Command: "+dashboard-block-get-data", + Description: "Get computed data for a dashboard chart block", + Risk: "read", + Scopes: []string{"base:dashboard:read"}, + AuthTypes: authTypes(), + HasFormat: true, + Flags: []common.Flag{ + baseTokenFlag(true), + blockIDFlag(true), + }, + Tips: []string{ + "lark-cli base +dashboard-block-get-data --base-token --block-id ", + "Use +dashboard-block-get first when you need block metadata like name, type, or data_config.", + "This command returns computed chart protocol JSON directly, not wrapped block metadata.", + "Text blocks do not have computed chart data; this shortcut is for chart/statistics blocks.", + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + return dryRunDashboardBlockGetData(ctx, runtime) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeDashboardBlockGetData(runtime) + }, +} diff --git a/shortcuts/base/dashboard_ops.go b/shortcuts/base/dashboard_ops.go index 3a05d771d..3d32099f0 100644 --- a/shortcuts/base/dashboard_ops.go +++ b/shortcuts/base/dashboard_ops.go @@ -104,6 +104,14 @@ func dryRunDashboardBlockGet(_ context.Context, runtime *common.RuntimeContext) Params(params) } +// dryRunDashboardBlockGetData returns a DryRunAPI for getting computed data for a dashboard block. +func dryRunDashboardBlockGetData(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + return common.NewDryRunAPI(). + GET("/open-apis/base/v3/bases/:base_token/dashboards/blocks/:block_id/data"). + Set("base_token", runtime.Str("base-token")). + Set("block_id", runtime.Str("block-id")) +} + // dryRunDashboardBlockCreate returns a DryRunAPI for creating a dashboard block. func dryRunDashboardBlockCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { pc := newParseCtx(runtime) @@ -261,6 +269,16 @@ func executeDashboardBlockGet(runtime *common.RuntimeContext) error { return nil } +// executeDashboardBlockGetData retrieves computed data for a dashboard chart block. +func executeDashboardBlockGetData(runtime *common.RuntimeContext) error { + data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "dashboards", "blocks", runtime.Str("block-id"), "data"), nil, nil) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil +} + // executeDashboardBlockCreate creates a new dashboard block. func executeDashboardBlockCreate(runtime *common.RuntimeContext) error { pc := newParseCtx(runtime) diff --git a/shortcuts/base/shortcuts.go b/shortcuts/base/shortcuts.go index c98ccff7e..13f2a9210 100644 --- a/shortcuts/base/shortcuts.go +++ b/shortcuts/base/shortcuts.go @@ -84,6 +84,7 @@ func Shortcuts() []common.Shortcut { BaseDashboardArrange, BaseDashboardBlockList, BaseDashboardBlockGet, + BaseDashboardBlockGetData, BaseDashboardBlockCreate, BaseDashboardBlockUpdate, BaseDashboardBlockDelete, diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 3d791759f..82b067c62 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -97,6 +97,14 @@ func TestRegisterShortcutsMountsBaseCommands(t *testing.T) { if workspaceCmd == nil || workspaceCmd.Name() != "+base-get" { t.Fatalf("base workspace shortcut not mounted: %#v", workspaceCmd) } + + blockDataCmd, _, err := program.Find([]string{"base", "+dashboard-block-get-data"}) + if err != nil { + t.Fatalf("find dashboard block get-data shortcut: %v", err) + } + if blockDataCmd == nil || blockDataCmd.Name() != "+dashboard-block-get-data" { + t.Fatalf("base dashboard block get-data shortcut not mounted: %#v", blockDataCmd) + } } // Service-level cobra commands created by RegisterShortcuts must carry diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index 87549b19e..727a9819f 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -1,6 +1,6 @@ --- name: lark-base -version: 1.2.1 +version: 1.2.2 description: "当需要用 lark-cli 操作飞书多维表格(Base)时调用:搜索 Base、建表、字段管理、记录读写、记录分享链接、视图配置、历史查询,以及角色/表单/仪表盘管理/工作流;也适用于把旧的 +table / +field / +record 写法改成当前命令写法。涉及字段设计、公式字段、查找引用、跨表计算、行级派生指标、数据分析需求时也必须使用本 skill。" metadata: requires: @@ -178,6 +178,7 @@ metadata: | `+dashboard-list / +dashboard-get` | 列出仪表盘,或获取仪表盘详情 | [`lark-base-dashboard-list.md`](references/lark-base-dashboard-list.md)、[`lark-base-dashboard-get.md`](references/lark-base-dashboard-get.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md) | 进入仪表盘语义后先读 guide;`+dashboard-list` 只能串行执行 | | `+dashboard-create / +dashboard-update / +dashboard-delete` | 创建、更新或删除仪表盘 | [`lark-base-dashboard-create.md`](references/lark-base-dashboard-create.md)、[`lark-base-dashboard-update.md`](references/lark-base-dashboard-update.md)、[`lark-base-dashboard-delete.md`](references/lark-base-dashboard-delete.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md) | 创建前先明确看板目标和展示场景;更新前先读取当前配置;删除前先确认目标 | | `+dashboard-block-list / +dashboard-block-get` | 列出图表组件,或获取单个 block 详情 | [`lark-base-dashboard-block-list.md`](references/lark-base-dashboard-block-list.md)、[`lark-base-dashboard-block-get.md`](references/lark-base-dashboard-block-get.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md)、[`dashboard-block-data-config.md`](references/dashboard-block-data-config.md) | `+dashboard-block-list` 只能串行执行;查看配置细节时读 block config 文档 | +| `+dashboard-block-get-data` | 获取图表组件的计算结果 | [`lark-base-dashboard-block-get-data.md`](references/lark-base-dashboard-block-get-data.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md)、[`dashboard-block-data-config.md`](references/dashboard-block-data-config.md) | 适合读取图表组件的最终计算结果;此命令不返回 block 元数据,只返回计算结果 | | `+dashboard-block-create / +dashboard-block-update / +dashboard-block-delete` | 创建、更新或删除图表组件 | [`lark-base-dashboard-block-create.md`](references/lark-base-dashboard-block-create.md)、[`lark-base-dashboard-block-update.md`](references/lark-base-dashboard-block-update.md)、[`lark-base-dashboard-block-delete.md`](references/lark-base-dashboard-block-delete.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md)、[`dashboard-block-data-config.md`](references/dashboard-block-data-config.md) | 涉及 `data_config`、图表类型、filter 时要读 block config 文档;删除前先确认目标 | ### 2.8 表单模块 diff --git a/skills/lark-base/references/lark-base-dashboard-block-get-data.md b/skills/lark-base/references/lark-base-dashboard-block-get-data.md new file mode 100644 index 000000000..757b5dd79 --- /dev/null +++ b/skills/lark-base/references/lark-base-dashboard-block-get-data.md @@ -0,0 +1,717 @@ +# base +dashboard-block-get-data + +> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解 dashboard 整体工作流。 + +获取仪表盘图表组件(block)的**最终计算结果**,返回一份适合 AI 直接消费的图表协议 JSON。 + +这个命令适合以下场景: + +1. 读取柱状图 / 条形图 / 折线图 / 饼图 / 环形图 / 面积图 / 组合图 / 散点图 / 漏斗图 / 雷达图 / 词云 / 指标卡的**实际计算结果**; +2. 把图表结果交给 AI 做后续总结、趋势解释、同比/环比说明、异常点提取; +3. 在**不读取原始记录**的前提下,直接消费图表层已经聚合好的结果; +4. 验证某个图表当前展示的数据是否符合预期。 + +> [!IMPORTANT] +> - 本命令返回的是**图表结果协议**,不是 block 元数据; +> - 如果你需要 `name`、`type`、`layout`、`data_config` 等配置,请先用 [`+dashboard-block-get`](lark-base-dashboard-block-get.md); +> - 文本组件(`text`)不涉及计算,不适用本命令; + +## 一句话理解 + +`+dashboard-block-get-data` = **拿图表“算出来的结果”**,而不是拿图表“怎么配置的”。 + +--- + +## 支持的图表类型 + +当前支持以下图表类型的数据计算与返回: + +### 二维图表(10 种) + +- 柱状图 +- 条形图 +- 折线图 +- 饼图 +- 环形图 +- 面积图 +- 组合图 +- 散点图 +- 漏斗图 +- 雷达图 + +### 特殊类型(2 种) + +- 词云 +- 指标卡(statistics) + +> [!CAUTION] +> 文本组件虽然也属于 dashboard block,但它不产生可计算数据,因此不会返回本协议。 + +--- + +## 推荐命令 + +```bash +lark-cli base +dashboard-block-get-data \ + --base-token bascn***************CtadY \ + --block-id chtxxxxxxxx +``` + +如果你还不知道目标 block 的 ID,典型顺序是: + +```bash +# 先看仪表盘里有哪些组件 +lark-cli base +dashboard-block-list \ + --base-token bascn***************CtadY \ + --dashboard-id blkxxxxxxxx + +# 再读取某个组件的最终计算结果 +lark-cli base +dashboard-block-get-data \ + --base-token bascn***************CtadY \ + --block-id chtxxxxxxxx +``` + +如果你需要先确认组件类型、名称或 `data_config`,请先执行: + +```bash +lark-cli base +dashboard-block-get \ + --base-token bascn***************CtadY \ + --dashboard-id blkxxxxxxxx \ + --block-id chtxxxxxxxx +``` + +--- + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token,标识目标多维表格 | +| `--block-id ` | 是 | 图表 Block ID,即目标组件的唯一标识 | +| `--format ` | 否 | 输出格式,遵循 CLI 全局输出格式规则 | +| `--dry-run` | 否 | 只预览 API 调用,不真正执行 | + +> [!TIP] +> 这个命令**不需要** `--dashboard-id`。只要 `base_token + block_id` 即可定位并读取图表结果。 + +--- + +## 返回结构总览 + +服务端响应外层仍然是标准 OpenAPI 包装: + +```json +{ + "code": 0, + "msg": "success", + "data": { + "dimensions": [...], + "measures": [...], + "main_data": [...] + } +} +``` + +其中 `data` 就是 CLI 图表协议本体。不同图表类型的 `data` 结构略有不同: + +| 图表类型 | 一定有 | 可能有 | +|----------|--------|--------| +| 二维图表 | `dimensions` / `measures` / `main_data` | 无 | +| 词云 | `dimensions` / `measures` / `main_data` | 无 | +| 指标卡 | `dimensions` / `measures` / `main_data` | `comparison_data` / `trend_data` | + +--- + +## 协议字段说明 + +### 1) `dimensions` + +维度定义数组,告诉你主结果里每个 `dim_*` key 代表什么字段。 + +```json +[ + { + "field_name": "文本", + "alias": "dim_5bKp" + } +] +``` + +字段含义: + +| 字段 | 说明 | +|------|------| +| `field_name` | 维度字段显示名称 | +| `alias` | 维度别名,在 `main_data` / `trend_data` 中作为 key 使用 | + +### 2) `measures` + +指标定义数组,告诉你每个 `me_*` key 代表什么聚合指标。 + +```json +[ + { + "field_name": "Count", + "aggregation": "count_all", + "alias": "me_Y291bnRfYWxsX0NvdW50" + } +] +``` + +字段含义: + +| 字段 | 说明 | +|------|------| +| `field_name` | 统计该指标时所使用的字段名称;当 `aggregation = count_all` 时固定为 `Count`,表示统计记录总数 | +| `aggregation` | 聚合方式,常见值:`count_all` / `count` / `sum` / `avg` / `min` / `max` | +| `alias` | 指标别名,在 `main_data` / `comparison_data` / `trend_data` 中作为 key 使用 | + +例如: + +- 如果统计“销售额”的求和,则 `field_name = 销售额`、`aggregation = sum` +- 如果统计记录总数,则 `field_name = Count`、`aggregation = count_all` + +### 3) `main_data` + +主结果集。每一行都是一个对象,key 不是字段名本身,而是 `dimensions` / `measures` 中声明过的 `alias`。 + +```json +[ + { + "dim_5bKp": {"value": "A"}, + "me_Y291bnRfYWxsX0NvdW50": {"value": 3} + } +] +``` + +### 4) `comparison_data` + +仅指标卡可能返回。表示同/环比的两个值,顺序固定为: + +1. 当前周期值 +2. 对比周期值 + +> [!NOTE] +> 原始协议里通常**不直接展示周期名称**,只提供对应的值。因此解释“同比”还是“环比”、以及比较窗口具体是什么,通常要结合组件配置或 UI 上下文理解。 + +### 5) `trend_data` + +仅指标卡可能返回。表示时间序列趋势,每一行通常包含一个时间维度和一个指标值。 + +--- + +## alias 规则与读取方式 + +你不应该把 alias 当成人类可读字段名,而应把它视为**结果表里的列 ID**。 + +常见生成规则: + +- 维度 alias:`dim_` + `base64(field_name)` +- 指标 alias:`me_` + `base64(aggregation + "_" + field_name)` + +> [!NOTE] +> 为了便于阅读,本文档中的部分示例会使用**简化后的 alias**(例如 `dim_xxx`、`me_xxx` 或较短的示例值),不保证和真实返回值逐字符一致。 +> 在实际读取结果时,应始终以 `dimensions` / `measures` 中声明的 alias 为准,而不要假设所有示例都严格展开成完整编码值。 + +例如: + +```json +{ + "dimensions": [ + {"field_name": "文本", "alias": "dim_5bKp"} + ], + "measures": [ + {"field_name": "Count", "aggregation": "count_all", "alias": "me_xxx"} + ], + "main_data": [ + { + "dim_5bKp": {"value": "A"}, + "me_xxx": {"value": 3} + } + ] +} +``` + +应解读为: + +- `dim_5bKp` 对应字段“文本”,取值是 `A` +- `me_xxx` 对应指标 `count_all(Count)`,取值是 `3` + +> [!TIP] +> 读取结果时,**先看 `dimensions` / `measures`,再解 `main_data`**。不要仅凭 alias 名字猜含义。 + +--- + +## 各图表类型的协议细节 + +### 一、二维图表 + +适用于:柱状图、条形图、折线图、饼图、环形图、面积图、组合图、散点图、漏斗图、雷达图。 + +#### 结构特征 + +- `dimensions`:通常有 `1~2` 个维度 + - 不分组聚合时:通常 1 个维度 + - 开启分组聚合时:通常 2 个维度 +- `measures`:指标定义数组 +- `main_data`:按“维度组合”展开后的行数据 + +#### 这类数据代表什么 + +二维图表返回的本质上是一张**聚合结果表**: + +- 每一行代表一个维度值,或一组维度组合; +- 每一个 measure 值代表该维度下算出来的指标结果; +- 如果图表开启了分组聚合,那么每一行表示“主维度 + 分组维度”的一个组合结果; +- 如果图表是折线图、面积图这类带时间轴的图,通常可以把第一维理解为横轴、把 measure 理解为纵轴数值; +- 如果图表是饼图、环形图这类占比图,通常可以把每一行理解为一个扇区对应的分类及其数值。 + +换句话说,AI 在读取这类结果时,可以把它当作“按某些维度聚合后的统计明细表”,适合进一步做排序、Top N、占比解释、分组对比和趋势总结。 + +#### 示例 1:普通二维图表(无分组聚合) + +```json +{ + "dimensions": [ + { + "field_name": "文本", + "alias": "dim_5bKp" + } + ], + "measures": [ + { + "aggregation": "count_all", + "field_name": "Count", + "alias": "me_Y291bnRfYWxsX0NvdW50" + } + ], + "main_data": [ + { + "dim_5bKp": {"value": "A"}, + "me_Y291bnRfYWxsX0NvdW50": {"value": 3} + }, + { + "dim_5bKp": {"value": "B"}, + "me_Y291bnRfYWxsX0NvdW50": {"value": 2} + }, + { + "dim_5bKp": {"value": "C"}, + "me_Y291bnRfYWxsX0NvdW50": {"value": 2} + } + ] +} +``` + +可解读为: + +- 维度字段是“文本” +- 指标是“按记录总数统计” +- 当“文本”字段为 `A` 时,对应的 `Count` 指标值是 `3` +- 当“文本”字段为 `B` 时,对应的 `Count` 指标值是 `2` +- 当“文本”字段为 `C` 时,对应的 `Count` 指标值是 `2` + +#### 示例 2:二维图表(开启分组聚合) + +```json +{ + "dimensions": [ + { + "field_name": "文本", + "alias": "dim_5bKp" + }, + { + "field_name": "单选", + "alias": "dim_5aSl" + } + ], + "measures": [ + { + "aggregation": "count_all", + "field_name": "Count", + "alias": "me_YW91bnR" + } + ], + "main_data": [ + { + "dim_5bKp": {"value": "A"}, + "dim_5aSl": {"value": "a-1"}, + "me_YW91bnR": {"value": 2} + }, + { + "dim_5bKp": {"value": "A"}, + "dim_5aSl": {"value": "a-2"}, + "me_YW91bnR": {"value": 1} + }, + { + "dim_5bKp": {"value": "B"}, + "dim_5aSl": {"value": "b-1"}, + "me_YW91bnR": {"value": 1} + }, + { + "dim_5bKp": {"value": "C"}, + "dim_5aSl": {"value": "c-1"}, + "me_YW91bnR": {"value": 2} + } + ] +} +``` + +可解读为: + +- 第一维是“文本”,第二维是“单选”,指标是“按记录总数统计” +- 当“文本”字段为 `A`、且“单选”字段为 `a-1` 时,对应的指标值是 `2` +- 当“文本”字段为 `A`、且“单选”字段为 `a-2` 时,对应的指标值是 `1` +- 当“文本”字段为 `B`、且“单选”字段为 `b-1` 时,对应的指标值是 `1` +- 当“文本”字段为 `C`、且“单选”字段为 `c-1` 时,对应的指标值是 `2` +- 如果按“文本”字段汇总,那么“文本”字段为 `A` 时总指标值是 `3`;为 `B` 时总指标值是 `1`;为 `C` 时总指标值是 `2` + +--- + +### 二、词云 + +#### 结构特征 + +词云协议仍然沿用 `dimensions + measures + main_data` 的结构,但语义稍有不同: + +- `dimensions` 对应被分词的字段; +- `main_data` 每一行代表一个词; +- `measure` 的 value 表示按该词分组后计算出来的统计值。 + +#### 这类数据代表什么 + +词云返回的不是“原文列表”,而是**按词分组后的聚合统计结果**: + +- `dimensions` 定义的是被分词的来源字段; +- `measure` 对应的是该词在当前图表统计范围内对应的统计值,具体含义取决于聚合方式和指标字段; +- `main_data` 的每一行都可以理解成“某个词 + 该词对应的统计结果”,其中该维度的具体 value 就是拆分出来的词; +- 返回结果通常已经结合图表当前过滤条件、时间范围、数据权限等上下文计算完成。 + +因此,AI 读取词云数据时,更适合做“关键词排序”“热点词解释”“按词聚合结果分析”“主题归纳”,而不是把它当成逐条文本记录去理解。 + +#### 示例 + +```json +{ + "dimensions": [ + { + "field_name": "文本", + "alias": "dim_5bKp" + } + ], + "measures": [ + { + "aggregation": "count_all", + "field_name": "Count", + "alias": "me_YW91bnR" + } + ], + "main_data": [ + { + "dim_5bKp": {"value": "A"}, + "me_YW91bnR": {"value": 3} + }, + { + "dim_5bKp": {"value": "B"}, + "me_YW91bnR": {"value": 2} + }, + { + "dim_5bKp": {"value": "C"}, + "me_YW91bnR": {"value": 2} + } + ] +} +``` + +可解读为: + +- 被统计的分词字段是“文本” +- 当前示例里的 measure 是 `count_all(Count)`,所以这里的统计值可以理解为“按词分组后的记录总数” +- 当分词结果为 `A` 时,对应的统计值是 `3` +- 当分词结果为 `B` 时,对应的统计值是 `2` +- 当分词结果为 `C` 时,对应的统计值是 `2` +- 按统计值排序,分词结果 `A` 对应的值最高 +- 分词结果 `B` 和 `C` 的统计值相同,说明它们处于同一梯队 + +--- + +### 三、指标卡(statistics) + +指标卡除了主值外,还可能包含同/环比与趋势结果,是本命令里结构最特殊的一类。 + +#### 结构特征 + +- `measures`:**有且仅有一个指标** +- `main_data`:通常只有一行,表示总指标值 +- `comparison_data`:可选,表示当前周期值与对比周期值 +- `trend_data`:可选,表示趋势序列 +- `dimensions`:可能包含同/环比日期字段、趋势日期字段 + +#### 这类数据代表什么 + +指标卡返回的核心是一个**主指标摘要**,外加可选的比较信息和趋势信息: + +- `main_data` 表示当前卡片最核心、最醒目的那个主值;它通常是某个表的记录总数,或某个字段的聚合值,本身**不带时间周期概念**; +- `comparison_data` 表示用于同/环比展示的两个数值,通常是“当前周期值”和“对比周期值”;它们表示某个时间周期下的记录总数,或某个字段的聚合值; +- `trend_data` 表示这个指标在一段时间内的变化轨迹,用来支持走势判断; +- `dimensions` 在指标卡里通常不是拿来做主分组展示,而是给 `trend_data` 或同/环比相关日期字段提供语义说明。 + +例如: + +- `main_data = 7` 可以理解为当前卡片展示的主数据,比如某张表当前总记录数是 `7`; +- `comparison_data[0] = 6` 则表示某个比较周期下的当前值,比如“本月记录总数 = 6”; +- 因此,`main_data` 与 `comparison_data[0]` **不一定相等**,因为两者表达的口径并不完全相同。 + +因此,AI 在解读指标卡时,应该优先回答这几个问题: + +1. 当前主值是多少; +2. 和对比周期相比是上升、下降还是持平; +3. 趋势整体是增长、波动还是下滑; +4. 是否存在明显的异常峰值或低谷。 + +> [!NOTE] +> 当指标卡**同时指定同/环比和趋势**时,`dimensions` 中日期维度的顺序是固定的: +> 1. 第一个元素是**趋势**对应的日期维度; +> 2. 第二个元素是**同/环比**对应的日期维度。 +> +> 另外要注意:`comparison_data` 自身通常**不直接携带日期字段**,它只给出“当前周期值 / 对比周期值”。 +> `dimensions` 中的第一个日期维度会直接出现在 `trend_data` 中,作为趋势序列的时间列; +> 第二个日期维度则主要用于补充“该卡片配置了哪类比较相关日期字段”的语义。 + +#### 示例 + +```json +{ + "dimensions": [ + { + "field_name": "日期", + "alias": "dim_ZGF0ZQ" + }, + { + "field_name": "日期2", + "alias": "dim_ZGF0ZTI" + } + ], + "measures": [ + { + "aggregation": "count_all", + "field_name": "Count", + "alias": "me_YW91b" + } + ], + "main_data": [ + { + "me_YW91b": {"value": 7} + } + ], + "comparison_data": [ + { + "me_YW91b": {"value": 6} + }, + { + "me_YW91b": {"value": 0} + } + ], + "trend_data": [ + { + "dim_ZGF0ZQ": {"value": "2026-01-15"}, + "me_YW91b": {"value": 1} + }, + { + "dim_ZGF0ZQ": {"value": "2026-01-17"}, + "me_YW91b": {"value": 1} + }, + { + "dim_ZGF0ZQ": {"value": "2026-03-22"}, + "me_YW91b": {"value": 1} + }, + { + "dim_ZGF0ZQ": {"value": "2026-04-24"}, + "me_YW91b": {"value": 2} + }, + { + "dim_ZGF0ZQ": {"value": "2026-05-01"}, + "me_YW91b": {"value": 1} + } + ] +} +``` + +可解读为: + +- 当前主指标值 = `7` +- 当前主指标值不带时间周期概念,可理解为当前卡片主数据 +- comparison_data[0] = 当前周期值 `6`,例如某个时间周期(如本月)下的统计值 +- comparison_data[1] = 对比周期值 `0` +- `dimensions[0]` 对应趋势日期维度,因此实际出现在 `trend_data` 里 +- `dimensions[1]` 对应同/环比相关的日期维度,用来补充比较语义 +- trend_data 展示该指标随时间的变化序列 +- 从 comparison_data 看,当前周期相较对比周期是上升的,并且对比周期值为 0 +- 从 trend_data 看,这个指标并不是每天都有值,而是在若干离散日期出现 +- 趋势序列里的最高点出现在 `2026-04-24`,值为 `2` +- 其余出现的日期大多为 `1`,说明整体上有波动,但暂时没有持续快速增长的趋势 + +> [!NOTE] +> `comparison_data` 只告诉你“当前值 / 对比值”,**不额外标出日期区间文本**。如果用户需要完整说明“和上周比”还是“和上月比”,通常要结合组件配置或界面上下文进一步判断。 + +--- + +## 如何正确解读返回值 + +建议按下面顺序阅读: + +1. **先看 `dimensions`**:确认每个 `dim_*` alias 对应哪个字段; +2. **再看 `measures`**:确认每个 `me_*` alias 是什么聚合方式; +3. **最后读 `main_data` / `comparison_data` / `trend_data`**:把 alias 还原成“字段名 + 指标名”再做解释。 + +### 推荐解释模板 + +如果要把结果转成自然语言,建议不要只“复述数值”,而应尽量覆盖下面几个层次: + +1. **先解释指标含义**:说明 measure 代表“记录总数”“某字段求和”“平均值”等; +2. **再给出核心结果**:明确当前主值、主要分类、主要组合或主要词项; +3. **做排序或 Top N 提炼**:指出最高、最低、前几名、同一梯队; +4. **补充分组/对比关系**:如果有第二维或 comparison_data,就说明比较对象和差异; +5. **分析趋势或异常点**:如果有时间序列,指出上升、下降、波动、峰值、低谷; +6. **最后给一句结论**:总结最值得关注的信息。 + +可参考下面模板: + +- 二维图表: + - 基础模板:`按 <维度字段> 统计,当前指标 <指标含义>;其中 <维度值1>=<指标值1>,<维度值2>=<指标值2> ...` + - 增强模板:`按 <维度字段> 统计,当前指标表示 <指标含义>。从结果看, 的值最高,为 紧随其后。若按 Top N 看,前 项合计贡献了 ...;若看低值项,<低值维度值> 最低,为 <低值>。整体上,<一句总结>` + +- 分组聚合图表: + - 基础模板:`按 <维度1> 统计,并以 <维度2> 分组,得到 <组合1>=<值1>,<组合2>=<值2> ...` + - 增强模板:`当前指标表示 <指标含义>。按 <维度1> 拆分后,不同 <维度2> 组之间存在明显差异:例如 <组合1> = <值1>,<组合2> = <值2>。如果按 <维度1> 汇总, 总值最高,为 <汇总值>;如果看组内对比,<某组> 在 <某维度1值> 下表现最强 / 最弱。整体说明 <一句总结>` + +- 词云: + - 基础模板:`按分词结果统计,当前指标表示 <指标含义>;其中 <词1>=<统计值1>,<词2>=<统计值2> ...` + - 增强模板:`当前词云反映的是“按词分组后的 <指标含义>”。从结果看, 的值最高,为 <值1>,说明它是当前最突出的关键词; 处于第二梯队。如果按 Top N 看,主要关注词集中在 <主题A>、<主题B>;如果有多个词数值接近,可归为同一热点层级。整体上,这组词更适合用来总结 <主题/热点/关注点>` + +- 指标卡: + - 基础模板:`当前主指标值为 ;当前周期值为 ;对比周期值为 ;趋势上 ...` + - 增强模板:`当前主指标表示 <指标含义>,主值为 。若看周期比较,当前周期值为 ,对比周期值为 ,因此整体表现为 <上升/下降/持平>。若看趋势序列,最高点出现在 <日期>,值为 <峰值>;最低点出现在 <日期>,值为 <低值>;整体走势表现为 <持续增长/阶段波动/明显回落>。如果需要给出结论,可总结为:<一句总结>` + +> [!TIP] +> 当用户明确要求“帮我分析”“帮我总结”“帮我找异常 / Top N / 趋势”时,优先采用增强模板,而不是只逐条复述原始数值。 + +--- + +## 常见工作流 + +### 场景 1:用户要“拿这个图表当前展示的数据” + +```bash +# 如果已知 block_id,直接读结果 +lark-cli base +dashboard-block-get-data \ + --base-token xxx \ + --block-id chtxxxxxxxx +``` + +### 场景 2:用户说“帮我分析这个图表”,但你还不知道它是什么组件 + +```bash +# 先看组件配置,确认它是不是支持计算的图表类型 +lark-cli base +dashboard-block-get \ + --base-token xxx \ + --dashboard-id blk_xxx \ + --block-id chtxxxxxxxx + +# 再读最终计算结果 +lark-cli base +dashboard-block-get-data \ + --base-token xxx \ + --block-id chtxxxxxxxx +``` + +### 场景 3:用户要找“仪表盘里哪个图的结果异常” + +```bash +# 先列组件 +lark-cli base +dashboard-block-list \ + --base-token xxx \ + --dashboard-id blk_xxx + +# 再针对可疑 block 逐个取结果 +lark-cli base +dashboard-block-get-data \ + --base-token xxx \ + --block-id chtxxxxxxxx +``` + +--- + +## 何时优先用这个命令 + +- 用户说“帮我拿这个图表算出来的数据 / 结果 / 指标” +- 用户已经知道 `block_id`,目标是**读取结果**而不是看配置 +- 用户后续还要让 AI 对图表结果做解释、归纳、比较、总结 +- 你只关心图表层的聚合产出,不需要回到底表逐条读记录 + +## 何时不要误用 + +- 想看 block 的 `data_config`、名称、类型、布局 → 用 `+dashboard-block-get` +- 想列出仪表盘里有哪些组件 → 用 `+dashboard-block-list` +- 想修改或新建组件 → 用 `+dashboard-block-update` / `+dashboard-block-create` +- 想看原始记录明细,而不是图表聚合结果 → 回到 `record-*` +- 目标是文本组件 → 本命令不适用 + +--- + +## 常见误区 + +### 误区 1:把这个命令当成“获取 block 详情” + +不是。这个命令不返回: + +- block 名称 +- block 类型 +- layout +- `data_config` +- 所属 dashboard 信息 + +这些都应该通过 [`+dashboard-block-get`](lark-base-dashboard-block-get.md) 获取。 + +### 误区 2:以为它返回的是原始记录 + +不是。它返回的是**图表聚合后的最终结果**。如果图表本身做了过滤、分组、聚合、时间窗口限制,返回值反映的是图表视角,不是原始表全量明细。 + +### 误区 3:直接把 alias 当真实字段名读 + +不应该。alias 只是协议里的键,必须结合 `dimensions` / `measures` 还原语义。 + +### 误区 4:看到指标卡的 `comparison_data` 就以为已经知道“同比/环比周期文本” + +不一定。它只给出比较值,不一定给出周期标签。若要精确解释比较窗口,通常还需要组件配置或 UI 上下文。 + +--- + +## dry-run 用途 + +可用来确认最终会调用的接口路径: + +```bash +lark-cli base +dashboard-block-get-data \ + --base-token bascn_example_token \ + --block-id chtxxxxxxxx \ + --dry-run \ + --format pretty +``` + +你应能看到类似: + +```text +GET /open-apis/base/v3/bases/bascn_example_token/dashboards/blocks/chtxxxxxxxx/data +``` + +适合在以下场景使用: + +- 校验 `base_token` / `block_id` 是否传对; +- 调试 agent 生成的命令; +- 编写自动化测试时确认请求结构。 + +--- + +## 参考 + +- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块总指引 +- [lark-base-dashboard-block-get.md](lark-base-dashboard-block-get.md) — 获取 block 元数据 +- [dashboard-block-data-config.md](dashboard-block-data-config.md) — data_config 结构和组件类型说明 diff --git a/skills/lark-base/references/lark-base-dashboard.md b/skills/lark-base/references/lark-base-dashboard.md index f988ba0e2..3dfb0d827 100644 --- a/skills/lark-base/references/lark-base-dashboard.md +++ b/skills/lark-base/references/lark-base-dashboard.md @@ -18,6 +18,7 @@ Dashboard 是 Base 中的数据可视化看板,可以把表格数据变成** | 在仪表盘里添加组件 | `+dashboard-block-create` | 先读 [lark-base-dashboard-block-create.md](lark-base-dashboard-block-create.md),再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) | | 修改组件 | `+dashboard-block-update` | 先读 [lark-base-dashboard-block-update.md](lark-base-dashboard-block-update.md),再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) | | 查看仪表盘有哪些组件 | `+dashboard-get` 或 `+dashboard-block-list` | 本页下方「查看仪表盘」 | +| 读取图表最终计算结果 | `+dashboard-block-get-data` | [lark-base-dashboard-block-get-data.md](lark-base-dashboard-block-get-data.md) | | 智能重排组件布局 | `+dashboard-arrange` | [lark-base-dashboard-arrange.md](lark-base-dashboard-arrange.md) | ## 典型场景工作流 @@ -151,6 +152,7 @@ lark-cli base +dashboard-arrange \ - 想看仪表盘整体结构(含主题、所有组件名称和类型)→ 用 **方式 A** - 只想快速查看有哪些组件 → 用 **方式 B** - 想看某个组件的详细 data_config 配置 → 用 **方式 C** +- 想看某个图表/指标卡实际算出来的数据 → 用 **方式 D** ```bash # 第 1 步:列出仪表盘,定位到当前仪表盘 @@ -167,6 +169,9 @@ lark-cli base +dashboard-block-list --base-token xxx --dashboard-id blk_xxx # 方式 C:查看某个组件的详细配置 lark-cli base +dashboard-block-get --base-token xxx --dashboard-id blk_xxx --block-id chtxxxxxxxx +# 方式 D:查看某个图表组件的计算结果(AI 友好的 chart protocol) +lark-cli base +dashboard-block-get-data --base-token xxx --block-id chtxxxxxxxx + # 最后:把获取到的现状信息整理好告诉用户 ``` @@ -223,6 +228,9 @@ A: 在「添加新组件」或「编辑组件」前查看已有组件可以: - 避免重复创建相似的组件 - 参考已有组件的 data_config 结构作为模板 +**Q: 我想直接拿图表算好的结果给 AI 分析,应该用什么?** +A: 用 `+dashboard-block-get-data`。它直接返回图表协议 JSON(`dimensions / measures / main_data`,指标卡还可能有 `comparison_data / trend_data`),适合让 AI 做后续总结、比对和解释。注意它不返回 block 名称、类型、layout、data_config 等元数据;如果你还需要这些信息,先用 `+dashboard-block-get`。 + ## 命令详细文档 | CLI 命令 | 说明 | 详细文档 | @@ -235,6 +243,7 @@ A: 在「添加新组件」或「编辑组件」前查看已有组件可以: | `+dashboard-arrange` | 智能重排布局 | [lark-base-dashboard-arrange.md](lark-base-dashboard-arrange.md) | | `+dashboard-block-list` | 列出组件 | [lark-base-dashboard-block-list.md](lark-base-dashboard-block-list.md) | | `+dashboard-block-get` | 获取单个组件详情 | [lark-base-dashboard-block-get.md](lark-base-dashboard-block-get.md) | +| `+dashboard-block-get-data` | 获取图表组件计算结果 | [lark-base-dashboard-block-get-data.md](lark-base-dashboard-block-get-data.md) | | `+dashboard-block-create` | 创建组件 | [lark-base-dashboard-block-create.md](lark-base-dashboard-block-create.md) | | `+dashboard-block-update` | 更新组件 | [lark-base-dashboard-block-update.md](lark-base-dashboard-block-update.md) | | `+dashboard-block-delete` | 删除组件 | [lark-base-dashboard-block-delete.md](lark-base-dashboard-block-delete.md) | diff --git a/skills/lark-base/references/lark-base-workflow-guide.md b/skills/lark-base/references/lark-base-workflow-guide.md index b67321b68..eb2a235ba 100644 --- a/skills/lark-base/references/lark-base-workflow-guide.md +++ b/skills/lark-base/references/lark-base-workflow-guide.md @@ -56,6 +56,7 @@ | 场景 | 步骤组合 | 示例 | |------|---------|------| | 新增触发+通知 | AddRecordTrigger → LarkMessageAction | [下方](#示例1-新增记录触发--发送消息) | +| 按钮点击+调用外部接口+写入日志 | ButtonTrigger → HTTPClientAction → AddRecordAction | [下方](#示例-6-按钮触发--调用外部接口--写入同步日志) | | 定时+循环 | TimerTrigger → FindRecordAction → Loop → LarkMessageAction | [下方](#示例2-定时触发--查找记录--循环遍历--发送消息) | | 条件判断 | ... → IfElseBranch → 分支处理 | [下方](#示例3-条件分支-ifelsebranch) | | 多路分类 | ... → SwitchBranch → 多分支处理 | [下方](#示例4-多路分支-switchbranch) | @@ -628,6 +629,119 @@ --- +### 示例 6: 按钮触发 + 调用外部接口 + 写入同步日志 + +**场景**: 在「客户线索表」里给每条记录配置一个“同步到 CRM”按钮。销售点击按钮后,Workflow 调用外部 CRM 接口同步当前线索,再在「同步日志表」新增一条记录,方便后续审计和排查。 + +```json +{ + "client_token": "1704067206", + "title": "线索一键同步到 CRM", + "steps": [ + { + "id": "step_button_trigger", + "type": "ButtonTrigger", + "title": "点击同步到 CRM 按钮时触发", + "next": "step_call_crm_api", + "data": { + "button_type": "buttonField", + "table_name": "客户线索表" + } + }, + { + "id": "step_call_crm_api", + "type": "HTTPClientAction", + "title": "调用 CRM 同步接口", + "next": "step_add_sync_log", + "data": { + "method": "POST", + "url": [ + { "value_type": "text", "value": "https://api.example-crm.com/v1/leads/sync" } + ], + "headers": [ + { "key": "Content-Type", "value": [{ "value_type": "text", "value": "application/json" }] }, + { "key": "X-System", "value": [{ "value_type": "text", "value": "lark_base_workflow" }] } + ], + "body_type": "raw", + "raw_body": [ + { "value_type": "text", "value": "{\"lead_name\":\"" }, + { "value_type": "ref", "value": "$.step_button_trigger.fldLeadName" }, + { "value_type": "text", "value": "\",\"mobile\":\"" }, + { "value_type": "ref", "value": "$.step_button_trigger.fldMobile" }, + { "value_type": "text", "value": "\",\"company\":\"" }, + { "value_type": "ref", "value": "$.step_button_trigger.fldCompany" }, + { "value_type": "text", "value": "\",\"owner\":\"" }, + { "value_type": "ref", "value": "$.step_button_trigger.fldOwner" }, + { "value_type": "text", "value": "\",\"source_record_id\":\"" }, + { "value_type": "ref", "value": "$.step_button_trigger.recordId" }, + { "value_type": "text", "value": "\"}" } + ], + "response_type": "json", + "response_value": "{\"success\":true,\"message\":\"lead synced successfully\"}" + } + }, + { + "id": "step_add_sync_log", + "type": "AddRecordAction", + "title": "写入同步日志", + "next": null, + "data": { + "table_name": "同步日志表", + "field_values": [ + { + "field_name": "线索名称", + "value": [{ "value_type": "ref", "value": "$.step_button_trigger.fldLeadName" }] + }, + { + "field_name": "手机号", + "value": [{ "value_type": "ref", "value": "$.step_button_trigger.fldMobile" }] + }, + { + "field_name": "公司名称", + "value": [{ "value_type": "ref", "value": "$.step_button_trigger.fldCompany" }] + }, + { + "field_name": "负责人", + "value": [{ "value_type": "ref", "value": "$.step_button_trigger.fldOwner" }] + }, + { + "field_name": "来源记录ID", + "value": [{ "value_type": "ref", "value": "$.step_button_trigger.recordId" }] + }, + { + "field_name": "同步状态", + "value": [{ "value_type": "text", "value": "已提交 CRM 同步" }] + }, + { + "field_name": "同步是否成功", + "value": [{ "value_type": "ref", "value": "$.step_call_crm_api.body.success" }] + }, + { + "field_name": "同步结果说明", + "value": [{ "value_type": "ref", "value": "$.step_call_crm_api.body.message" }] + }, + { + "field_name": "备注", + "value": [{ "value_type": "text", "value": "由按钮触发自动发起同步请求" }] + } + ] + } + } + ] +} +``` + +**关键点**: +- `ButtonTrigger` 适合“人工确认后再执行”的场景,比如同步 CRM、推送 ERP、发起审批等 +- `button_type: "buttonField"` 表示按钮挂在记录上,因此可以直接引用当前记录的字段和值 +- `HTTPClientAction.raw_body` 可以通过 `text + ref + text` 的方式动态拼接 JSON 请求体 +- `HTTPClientAction` 的输出引用规则是:`response_type=none` 时不可引用;`response_type=text` 时只能用 `$.stepId` 引整个文本;`response_type=json` 时用 `$.stepId.body` 引整个 body、用 `$.stepId.body.字段名` 引 body 中字段,同时 `$.stepId.status_code` 表示 HTTP 返回状态码 +- `HTTPClientAction.response_value` 中声明了哪些字段,后续节点就只能引用这些字段;例如 `$.step_call_crm_api.body.success`、`$.step_call_crm_api.body.message` +- `AddRecordAction` 常用于写日志表、操作审计表、同步结果表,便于追踪谁在什么时候触发了外部调用 +- 示例里的 `fldLeadName` / `fldMobile` / `fldCompany` / `fldOwner` 只是占位的 fieldId,请以实际表字段 ID 为准 + +--- + ## 构造技巧 ### Loop 构造要点 diff --git a/skills/lark-base/references/lark-base-workflow-schema.md b/skills/lark-base/references/lark-base-workflow-schema.md index c4a58364c..3dc3a327e 100644 --- a/skills/lark-base/references/lark-base-workflow-schema.md +++ b/skills/lark-base/references/lark-base-workflow-schema.md @@ -98,6 +98,7 @@ | `ChangeRecordTrigger` | 记录满足条件时触发 | | `TimerTrigger` | 定时触发 | | `ReminderTrigger` | 日期提醒触发 | +| `ButtonTrigger` | 按钮点击触发 | | `LarkMessageTrigger` | 接收飞书消息触发 | > 所有 Trigger 节点**请勿设置** `children` ,通过 `next` 串联后继。 @@ -120,6 +121,7 @@ | `AddRecordAction` | 新增记录 | | `SetRecordAction` | 更新记录 | | `FindRecordAction` | 查找记录 | +| `HTTPClientAction` | HTTP 请求 | | `Delay` | 延迟 | | `LarkMessageAction` | 发送飞书消息 | | `GenerateAiTextAction` | AI 生成文本 | @@ -256,6 +258,23 @@ | `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 | +### ButtonTrigger + +```json +{ + "button_type": "buttonField", + "table_name": "审批表" +} +``` + +| 字段 | 必填 | 说明 | +|------|------|------| +| `button_type` | 是 | 按钮类型:`buttonField`(表格里的按钮,可操作当前记录数据)/ `buttonElement`(仪表盘、应用页面上的按钮,可执行整体操作) | +| `table_name` | 否 | 绑定的数据表名,仅 `button_type=buttonField` 时填写 | + +> `buttonField` 和 `buttonElement` 的输出能力不同,详见下方「ButtonTrigger(按钮触发器)」输出说明。 + + ### LarkMessageTrigger ```json @@ -351,6 +370,48 @@ | `filter_info` | 否* | RecordFilterInfo(与 `ref_info` 互斥) | | `ref_info` | 否* | RefInfo(与 `filter_info` 互斥) | +### HTTPClientAction + +```json +{ + "method": "POST", + "url": [{ "value_type": "text", "value": "https://api.example.com/webhook" }], + "queries": [ + { "key": "source", "value": [{ "value_type": "text", "value": "workflow" }] } + ], + "headers": [ + { "key": "Content-Type", "value": [{ "value_type": "text", "value": "application/json" }] } + ], + "body_type": "raw", + "raw_body": [ + { "value_type": "text", "value": "{\"record_id\":\"" }, + { "value_type": "ref", "value": "$.step_1.recordId" }, + { "value_type": "text", "value": "\"}" } + ], + "response_type": "json", + "response_value": "{\"success\":true,\"message\":\"data fetched successfully\"}" +} +``` + +| 字段 | 必填 | 说明 | +|------|-----|------| +| `method` | 否 | 请求方法:`GET` / `POST` / `PUT` / `PATCH` / `DELETE`,默认 `POST` | +| `url` | 是 | ValueInfo[],请求 URL,支持 `text` / `ref` 拼接 | +| `queries` | 否 | KeyValue[],查询参数 | +| `headers` | 否 | KeyValue[],请求头 | +| `body_type` | 否 | 请求体类型:`none` / `raw` / `form-data` / `form-urlencoded`,默认 `raw` | +| `raw_body` | 否 | ValueInfo[],原始请求体,仅 `body_type=raw` 时使用 | +| `form_body` | 否 | KeyValue[],表单数据,仅 `body_type=form-data` 或 `body_type=form-urlencoded` 时使用 | +| `response_type` | 否 | 响应类型:`none` / `text` / `json`,默认 `json` | +| `response_value` | 否 | string,JSON 字符串形式的响应结果示例;仅当 `response_type=json` 时必填 | + +`KeyValue`: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `key` | string | 参数名 / 请求头名 | +| `value` | ValueInfo[] | 参数值 / 请求头值,支持 `text` / `ref` | + ### Delay ```json @@ -564,8 +625,8 @@ $.{stepId}.{pathId}.{childPathId}.{grandChildPathId} | pathId | 说明 | 引用示例 | |--------|------|----------| | `{fieldId}` | 字段id,从配置表的所有字段或者指定字段id生成,可下钻字段属性 | `$.{stepId}.{fieldId}` | -| `{fieldId}.fieldId` | 字段id属性 | `$.{stepId}.{fieldId}.fieldId}` | -| `{fieldId}.fieldName` | 字段名属性 | `$.{stepId}.{fieldId}.fieldName}` | +| `{fieldId}.fieldId` | 字段id属性 | `$.{stepId}.{fieldId}.fieldId` | +| `{fieldId}.fieldName` | 字段名属性 | `$.{stepId}.{fieldId}.fieldName` | | `startTime` | 触发时间戳 | `$.{stepId}.startTime` | | `recordId` | 记录 ID | `$.{stepId}.recordId` | | `recordLink` | 记录链接 | `$.{stepId}.recordLink` | @@ -583,6 +644,34 @@ $.{stepId}.{pathId}.{childPathId}.{grandChildPathId} **recordLink 的 children**:如果配置了数据表,则为该表所有视图的列表,每个视图 `{ pathId: viewId, pathName: viewName, pathType: 'string' }`。引用示例:`$.{stepId}.recordLink.{viewId}`。 +##### ButtonTrigger(按钮触发器) + +`ButtonTrigger` 的输出取决于 `button_type`: + +#### `button_type = buttonField` + +| pathId | 说明 | 引用示例 | +|--------|------|----------| +| `{fieldId}` | 字段id,从配置表的所有字段或者指定字段id生成,可下钻字段属性 | `$.{stepId}.{fieldId}` | +| `{fieldId}.fieldId` | 字段id属性 | `$.{stepId}.{fieldId}.fieldId` | +| `{fieldId}.fieldName` | 字段名属性 | `$.{stepId}.{fieldId}.fieldName` | +| `recordId` | 记录 ID | `$.{stepId}.recordId` | +| `recordLink` | 记录链接 | `$.{stepId}.recordLink` | +| `recordCreatedUser` | 记录创建者 | `$.{stepId}.recordCreatedUser` | +| `recordModifiedUser` | 最后修改者 | `$.{stepId}.recordModifiedUser` | +| `recordModifiedTime` | 最后修改时间 | `$.{stepId}.recordModifiedTime` | +| `time` | 触发时间 | `$.{stepId}.time` | +| `user` | 触发人 | `$.{stepId}.user` | +| `buttonName` | 触发的按钮名称 | `$.{stepId}.buttonName` | + +#### `button_type = buttonElement` + +| pathId | 说明 | 引用示例 | +|--------|------|----------| +| `time` | 触发时间 | `$.{stepId}.time` | +| `user` | 触发人 | `$.{stepId}.user` | +| `buttonName` | 触发的按钮名称 | `$.{stepId}.buttonName` | + ##### TimerTrigger(定时触发器) | pathId | 说明 | 引用示例 | @@ -633,8 +722,8 @@ $.{stepId}.{pathId}.{childPathId}.{grandChildPathId} | pathId | 说明 | 引用示例 | |--------|------|----------| | `{fieldId}` | 用户配置的字段值,可下钻字段属性 | `$.{stepId}.{fieldId}` | -| `{fieldId}.fieldId` | 用户配置的字段id | `$.{stepId}.{fieldId}.fieldId}` | -| `{fieldId}.fieldName` | 用户配置的字段名 | `$.{stepId}.{fieldId}.fieldName}` | +| `{fieldId}.fieldId` | 用户配置的字段id | `$.{stepId}.{fieldId}.fieldId` | +| `{fieldId}.fieldName` | 用户配置的字段名 | `$.{stepId}.{fieldId}.fieldName` | | `recordId` | 新增的记录 ID | `$.{stepId}.recordId` | | `recordLink` | 新增的记录 URL | `$.{stepId}.recordLink` | @@ -643,10 +732,56 @@ $.{stepId}.{pathId}.{childPathId}.{grandChildPathId} | pathId | 说明 | 引用示例 | |--------|------|----------| | `{fieldId}` | 用户配置的字段值,可下钻字段属性 | `$.{stepId}.{fieldId}` | -| `{fieldId}.fieldId` | 用户配置的字段id | `$.{stepId}.{fieldId}.fieldId}` | -| `{fieldId}.fieldName` | 用户配置的字段名 | `$.{stepId}.{fieldId}.fieldName}` | +| `{fieldId}.fieldId` | 用户配置的字段id | `$.{stepId}.{fieldId}.fieldId` | +| `{fieldId}.fieldName` | 用户配置的字段名 | `$.{stepId}.{fieldId}.fieldName` | | `recordId` | 记录 ID 数组(因可能更新多条记录) | `$.{stepId}.recordId` | +##### HTTPClientAction(HTTP 请求) + +HTTPClientAction 的输出取决于 `response_type`: + +| response_type | 是否可引用 | 输出说明 | 引用示例 | +|--------------|-----------|----------|----------| +| `none` | 否 | 无任何可引用输出 | 不支持引用 | +| `text` | 是 | 整个响应文本作为节点整体输出 | `$.{stepId}` | +| `json` | 是 | 响应体整体挂在 `body` 下,同时返回 `status_code`;仅可引用 `response_value` 中声明的字段 | `$.{stepId}.body`、`$.{stepId}.body.success`、`$.{stepId}.body.message`、`$.{stepId}.status_code` | + +**补充说明**: + +- 当 `response_type = none` 时,后续节点无法引用 HTTPClientAction 的任何输出 +- 当 `response_type = text` 时,`$.{stepId}` 表示整个响应文本 +- 当 `response_type = json` 时,`$.{stepId}.body` 表示整个 JSON body,`$.{stepId}.body.字段名` 表示 body 中某个字段 +- 仅当 `response_type = json` 时,`$.{stepId}.status_code` 表示请求该 HTTP URL 后返回的 HTTP 状态码 +- 仅当 `response_type = json` 时,`response_value` 必填 +- 当 `response_type = json` 时,后续节点只能引用 `response_value` 中声明过的字段 + +**案例**: + +假设某个 `HTTPClientAction` 的配置如下: + +```json +{ + "id": "step_http_1", + "type": "HTTPClientAction", + "data": { + "response_type": "json", + "response_value": "{\"success\":true,\"message\":\"ok\"}" + } +} +``` + +则后续节点仅可以引用: + +- `$.step_http_1.body` +- `$.step_http_1.body.success` +- `$.step_http_1.body.message` +- `$.step_http_1.status_code` + +但**不能**引用未在 `response_value` 中声明的字段,例如: + +- `$.step_http_1.body.data` +- `$.step_http_1.body.request_id` + ##### GenerateAiTextAction(AI 生成文本) | pathId | 说明 | 引用示例 | @@ -744,11 +879,13 @@ $.{stepId}.{fieldId}.fileToken → 文件 Token 列表(array,仅 | ChangeRecordTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) | | SetRecordTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) | | ReminderTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) | +| ButtonTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性;buttonElement 仅基础触发属性) | | TimerTrigger | 触发器 | ✅ | 静态(仅 scheduleTime) | | LarkMessageTrigger | 触发器 | ✅ | 静态(消息属性列表) | | FindRecordAction | 动作 | ✅ | 动态(用户选择的字段) | | AddRecordAction | 动作 | ✅ | 动态(用户配置的字段) | | SetRecordAction | 动作 | ✅ | 动态(用户配置的字段) | +| HTTPClientAction | 动作 | ✅ | 动态(取决于用户配置的 HTTP 响应输出) | | GenerateAiTextAction | 动作 | ✅ | 静态(单 string) | | Delay | 动作 | ❌ | 无输出 | | LarkMessageAction | 动作 | ❌ | 无输出 | diff --git a/skills/lark-base/references/lark-base-workflow-update.md b/skills/lark-base/references/lark-base-workflow-update.md index 28a9c97fd..cdca5fecb 100644 --- a/skills/lark-base/references/lark-base-workflow-update.md +++ b/skills/lark-base/references/lark-base-workflow-update.md @@ -151,7 +151,7 @@ PUT /open-apis/base/v3/bases/:base_token/workflows/:workflow_id ## 坑点 -- ⚠️ **PUT 是全量覆盖**:传什么就写什么;如果只传 `title` 不传 `steps`,原有 steps 会被清空;如需只改标题,使用 PATCH 接口(目前无对应 shortcut,可参考 API 文档直接调用) +- ⚠️ **PUT 是全量覆盖**:传什么就写什么;如果只传 `title` 不传 `steps`,原有 steps 会被清空 - ⚠️ **workflow_id 前缀**:以 `wkf` 开头,从 URL 的 `?table=wkf...` 提取;和 table_id(`tbl` 开头)混淆会导致 `[2200] Internal Error` - ⚠️ **steps 中 id 字段必须唯一**:每个步骤的 `id` 在同一工作流内必须唯一;`next` 和 `children.links[].to` 引用的 ID 必须在 steps 数组中存在 - ⚠️ **更新不影响 enabled 状态**:`+workflow-update` 不会改变工作流的 `enabled/disabled` 状态;需要另外调用 `+workflow-enable` / `+workflow-disable` diff --git a/tests/cli_e2e/base/base_dashboard_block_get_data_dryrun_test.go b/tests/cli_e2e/base/base_dashboard_block_get_data_dryrun_test.go new file mode 100644 index 000000000..d70822c8d --- /dev/null +++ b/tests/cli_e2e/base/base_dashboard_block_get_data_dryrun_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "strings" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBaseDashboardBlockGetDataDryRun(t *testing.T) { + setBaseDryRunConfigEnv(t) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "base", "+dashboard-block-get-data", + "--base-token", "app_x", + "--block-id", "blk_chart", + "--dry-run", + }, + BinaryPath: "../../../lark-cli", + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + output := strings.TrimSpace(result.Stdout) + assert.Contains(t, output, "/open-apis/base/v3/bases/app_x/dashboards/blocks/blk_chart/data") + assert.Contains(t, output, `"method": "GET"`) + assert.Contains(t, output, `"block_id": "blk_chart"`) + assert.Contains(t, output, `"base_token": "app_x"`) +} + +func TestBaseDashboardBlockGetDataDryRun_MissingRequiredFlags(t *testing.T) { + setBaseDryRunConfigEnv(t) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "base", "+dashboard-block-get-data", + "--dry-run", + }, + BinaryPath: "../../../lark-cli", + DefaultAs: "bot", + }) + require.NoError(t, err) + assert.NotEqual(t, 0, result.ExitCode) + assert.Contains(t, result.Stderr, "base-token") + assert.Contains(t, result.Stderr, "block-id") +}