feat:移除了弹窗,服务器添加sls

This commit is contained in:
2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 changed files with 1322534 additions and 0 deletions

View File

@ -0,0 +1,159 @@
---
name: lark-base
version: 1.2.3
description: "飞书多维表格Base操作建表、字段、记录、视图、统计、公式/lookup、表单、仪表盘、workflow、角色权限遇到 Base/多维表格/bitable 或 /base/ 链接时使用。文件导入转 lark-drive认证/授权转 lark-shared。"
metadata:
requires:
bins: ["lark-cli"]
cliHelp: "lark-cli base --help"
---
# base
## 何时使用
使用本 skill
- 用户明确提到 Base / 多维表格 / bitable或给出 `/base/` 链接。
- 用户要在 Base 内建表、改表、管理字段、写记录、查记录、配视图。
- 用户要在 Base 内做公式字段、lookup 字段、跨表计算、派生指标、筛选聚合、TopN、统计分析。
- 用户要管理 Base 表单、仪表盘、workflow、高级权限或角色。
- 用户要把旧 Base 聚合式命令或旧写法迁移到当前 `lark-cli base +...` shortcut。
不要使用本 skill
- 只是认证、初始化配置、切换身份、处理 scope 或权限授权恢复,转 `lark-shared`
- 把本地 Excel / CSV / `.base` 导入成 Base`lark-drive +import --type bitable`
- 泛化数据分析、字段设计、公式讨论,但没有 Base/多维表格上下文。
## 使用边界
- Base 业务操作只使用 `lark-cli base +...` shortcut不使用旧聚合式 `+table / +field / +record / +view / +history / +workspace`
- 执行 update 前必须先查当前 shortcut 的 `--help` 或对应 reference。若命令要求完整配置首次请求必须基于可信的当前配置执行 read-modify-write只修改用户明确指定的内容保留其他仍适用的可写配置并按命令要求的结构提交。若命令支持局部delta update按其契约提交最小合法 payload不得以不完整请求试错补参。
- 用户要把 Excel / CSV / `.base` 导入成 Base 时,先转 `lark-cli drive +import --type bitable`,导入完成后再回到 Base 命令。
- 认证、初始化、scope、身份切换、权限不足恢复属于 `lark-shared`Base 文档只保留会影响 Base 路径选择的权限规则。
## 先获取 Base Token 和所需 ID
进入任何需要目标 Base 的 shortcut 前,必须先拿到可用的 `base_token`,以及当前任务需要的 `table_id` / `view_id` / `record_id` / `form_id` / `dashboard_id` / `workflow_id` 等真实 ID不要把完整 URL、wiki token、workspace token 或孤立 raw token 直接当作 `--base-token`
- 用户输入 URL 或分享链接:先运行 `lark-cli base +url-resolve --url "<url>" --as user`,用返回的 `base_token` 和相关 ID 继续后续命令。
- 用户输入 Base 标题、关键词或不确定名称:先运行 `lark-cli base +title-resolve --title "<keyword>" --as user``--title` 传入标题中的短关键词,不超过 30 个字符;过长标题先取最有区分度的短关键词;多候选时先让用户消歧,不要猜。
- 文档嵌入 Base 标签:直接读取 `<bitable>` / `<base_refer>``token` 作为 `--base-token``table-id` 作为 `--table-id``view-id` 作为 `--view-id`;孤立 raw token 不走 `+url-resolve`
- 仍无法定位且用户不是要新建 Base 时,先反问用户要操作哪一个 Base用户要新建时才用 `+base-create`
## 快速路由
| 用户目标 | 优先命令 | 何时读 reference |
|---|---|---|
| 查 Base 本体 | `+base-get` | 用返回确认 Base 名称、owner、权限和可继续操作的 token |
| 创建/复制 Base | `+base-create` / `+base-copy` | 新建时强烈推荐用 `--table-name` + `--fields` 同时配置新 Base 里唯一一个初始数据表的 name 和 schema写入后报告新 Base 标识和 `permission_grant` |
| 查看 Base 内资源目录 | `+base-block-list` | 想先了解一个 Base 里有哪些 table/docx/dashboard/workflow/folder 时优先用它;返回 ID 关系和 fewshot 看 `--help` |
| 管理 Base 内资源目录 | `+base-block-create/move/rename/delete` | 创建或整理 Base 直接管理的 folder/table/docx/dashboard/workflow资源内容继续用对应命令 |
| 管理数据表 | `+table-list/get/create/update/delete` | 处理 table 的列出、详情、创建、重命名和删除 |
| 列/查/删字段 | `+field-list/get/delete/search-options` | 写入前用 list/get 确认字段类型、选项、ID删除前确认目标字段 |
| 创建/更新字段 | `+field-create` / `+field-update` | 必读 [lark-base-field-json.md](references/lark-base-field-json.md);公式读 [formula-field-guide.md](references/formula-field-guide.md)lookup 读 [lookup-field-guide.md](references/lookup-field-guide.md);命令细节读 [lark-base-field-create.md](references/lark-base-field-create.md) / [lark-base-field-update.md](references/lark-base-field-update.md) |
| 读记录明细 | `+record-get` / `+record-list` / `+record-search` | 涉及筛选、排序、Top/Bottom N、聚合、多表关联、全局结论时读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md) |
| 写记录 | `+record-upsert` / `+record-batch-create` / `+record-batch-update` | 必读 [lark-base-record-upsert.md](references/lark-base-record-upsert.md) / [lark-base-record-batch-create.md](references/lark-base-record-batch-create.md) / [lark-base-record-batch-update.md](references/lark-base-record-batch-update.md) 和 [lark-base-cell-value.md](references/lark-base-cell-value.md) |
| 附件字段 | `+record-upload-attachment` / `+record-download-attachment` / `+record-remove-attachment` | 附件不要伪造成普通 CellValue上传走本地文件下载/删除按 file token 或字段定位 |
| 删除记录 / 分享记录链接 / 历史 | `+record-delete` / `+record-share-link-create` / `+record-history-list` | 删除前确认 record分享链接最多 100 条;历史读 [lark-base-record-history-list.md](references/lark-base-record-history-list.md),只查单条记录,不做整表审计 |
| 管理视图 | `+view-*` | `+view-set-filter` 读 [lark-base-view-set-filter.md](references/lark-base-view-set-filter.md);其余配置先 get 现状,再按返回结构更新 |
| 一次性聚合统计 | `+data-query` | 必读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md) 和入口 [lark-base-data-query-guide.md](references/lark-base-data-query-guide.md);完整 DSL 再读 [lark-base-data-query.md](references/lark-base-data-query.md) |
| 公式字段 | `+field-create/update --json '{"type":"formula",...}'` | 必读 [formula-field-guide.md](references/formula-field-guide.md),读后再加隐藏确认 flag `--i-have-read-guide` |
| Lookup 字段 | `+field-create/update --json '{"type":"lookup",...}'` | 必读 [lookup-field-guide.md](references/lookup-field-guide.md),读后再加隐藏确认 flag `--i-have-read-guide` |
| 表单提交 | `+form-submit` | 先读 [lark-base-form-detail.md](references/lark-base-form-detail.md) 获取题目、filter 和附件所需 `base_token`;提交 JSON 读 [lark-base-form-submit.md](references/lark-base-form-submit.md) |
| 表单题目创建/更新 | `+form-questions-create` / `+form-questions-update` | 读 [lark-base-form-questions-create.md](references/lark-base-form-questions-create.md) / [lark-base-form-questions-update.md](references/lark-base-form-questions-update.md) |
| 其他表单管理 | `+form-list/get/detail/create/update/delete` / `+form-questions-list/delete` | `+form-detail` 读 [lark-base-form-detail.md](references/lark-base-form-detail.md);删除前确认目标表单 |
| 仪表盘与组件 | `+dashboard-*` / `+dashboard-block-*` | 提到图表/看板/block 时先读 [lark-base-dashboard.md](references/lark-base-dashboard.md);组件 `data_config` 读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md);读取图表计算结果用 `+dashboard-block-get-data` |
| Workflow | `+workflow-*` | 创建/更新或理解 steps 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md)list/get/enable/disable 只处理 workflow ID 与启停状态 |
| 高级权限与角色 | `+advperm-*` / `+role-*` | 角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);角色 create/update 或解读完整配置再读权限 JSON SSOT [role-config.md](references/role-config.md);系统角色不可删除;关闭高级权限会影响自定义角色 |
## Base 心智模型
- Base 曾用名 Bitable返回字段、错误或旧文档里的 `bitable` 多为历史兼容,不代表应改走裸 API 或另一套命令。
- `+base-block-list` 是查看一个 Base 内资源目录的新入口:它列出这个 Base 直接管理的 `folder/table/docx/dashboard/workflow`,适合先判断 Base 里有什么,再决定走 table、dashboard、workflow 或 docx 命令。
- `base-block` 只负责资源目录管理,包括创建资源、移动到 folder、重命名和删除具体资源内容仍走 table/dashboard/workflow 命令。
- 新建 Base 时,强烈推荐一次性执行 `lark-cli base +base-create --name "<base>" --table-name "<table>" --fields '<field-json-array>'`,同时配置新 Base 里唯一一个初始数据表的 name 和 schema使用 `--fields` 前先读 [lark-base-field-json.md](references/lark-base-field-json.md) 或复用 `+field-create` 的字段 JSON 形状,不要猜字段属性。
- `+base-create` 不传 `--table-name``--fields` 时,会创建一个默认 schema 的初始数据表。
- 表、字段、视图、workflow、dashboard block 的名称和 ID 必须来自真实返回,不要凭用户口述猜。
- 存储字段可写;系统字段、`formula``lookup` 只读;附件字段走专用 attachment 命令。
- 一次性原始记录查询优先用 `+record-list` / `+record-search` 的 filter/sort聚合分析优先用 `+data-query`;需要长期显示在表中时,才新增 `formula` / `lookup` 字段。
- `formula` 适合常规计算、条件判断、文本/日期处理和长期派生指标;`lookup` 适合明确的跨表查找、筛选后取值或聚合引用。
- 写入、分析、公式、lookup、workflow、dashboard 前,先读取真实结构:表、字段、视图、关联表和 dashboard block 名称都以命令返回为准。
- 跨表场景必须读取目标表结构link 单元格中的关联 `record_id` 只是连接键,最终回答要回查并展示用户可读字段。
## 身份与权限降级
- 默认显式使用 `--as user` 操作用户资源;只有用户明确要求应用身份时,才直接用 `--as bot`
- user 身份报 scope/授权不足,或错误中包含 `missing_scopes` / `hint`,先转 `lark-shared` 做用户授权恢复,不要直接降级 bot。
- user 身份报资源级无访问且无授权恢复提示时,才可用 `--as bot` 重试一次bot 仍失败就停止重试并按权限错误处理。
- `91403` 或明确不可访问错误不要循环换身份重试。
- `+base-create` / `+base-copy` 若用 bot 身份执行,关注返回中的 `permission_grant`,并把用户是否可打开新 Base 告知用户。
## 查询与统计规则
涉及查询、统计或判断结论时,先阅读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md),并遵守:
1. `+record-list` 的默认页、固定 `--limit` 和本地 `jq` 只能证明已读取范围内的事实不能直接支撑全局最值、全量计数、Top/Bottom N、异常识别或分组结论。
2. 能由 Base 表达的筛选、排序、投影、聚合、分组和限制,应在 Base 云端查询能力中执行;不要先拉原始记录到本地上下文再手工筛选排序。
3. `has_more=true` 或等价分页信号表示当前结果不是全量;除非用户只要样例/前 N 条,不能基于该页回答全局问题。
4. 多表查询必须先确认关系字段和连接键link 单元格里的 `record_id` 是关系键,不是用户可读答案。
5. 最终答案必须能追溯到真实表、真实字段、查询范围、筛选/排序/聚合条件和必要的连接键。
6. 一次性原始记录查询优先用 `+record-list` / `+record-search` 的 filter/sort聚合分析优先用 `+data-query`;要把结果长期显示在表里,才考虑新增 `formula` / `lookup` 字段。
7. `+data-query` 可返回聚合结果或维度字段行,但维度行按字段组合去重且不返回 `record_id`;需要逐条记录、记录定位或完整行级字段时,再用 `+record-list` / `+record-search` / `+record-get` 回查。
## 写入前置规则
- 优先用写入返回确认结果;返回信息不足或任务明确要求核验时,再读回。
- 写记录前先读字段结构;只写存储字段。系统字段、附件字段、`formula``lookup` 不作为普通记录写入目标。
- 附件上传、下载、删除走专用 `+record-*-attachment` 命令。
- 写字段前先读 [lark-base-field-json.md](references/lark-base-field-json.md);涉及 `formula` / `lookup` 时必须读 [formula-field-guide.md](references/formula-field-guide.md) / [lookup-field-guide.md](references/lookup-field-guide.md)。
- 表名、字段名、视图名、workflow 配置中的名称必须来自真实返回;跨表场景还要读取目标表结构。
- 删除、角色更新、字段更新、表单提交(`+form-submit`)等高风险操作遵循 CLI 的 confirmation gate必须带 `--yes`;目标不明确时先用 get/list 消歧。
- 批量写入单批最多 200 条;连续写同一表时串行执行,遇到 `1254291` 按短暂等待后重试处理。
- `select` 字段只支持写入字段中已有的选项;构造 CellValue 前先用 `+field-list``+field-search-options` 确认目标选项存在。
## 表单与视图细节
- `+form-submit` 是高风险写操作,必须带 `--yes` 确认;调用前必须先跑 `+form-detail`,读取 `questions[].type``required``filter` 和附件场景需要的 `base_token`;不要填写被 filter 隐藏的问题。
- 表单附件不要写进 `fields`,放在 `--json.attachments`;提交附件时必须同时传表单所属 Base 的 `--base-token`
- `+view-set-filter` 是唯一保留的 view referencesort/group/card/timebar/visible-fields 这类配置先用对应 get 命令读现状,保留未修改字段,只替换用户要求变更的配置。
- 视图适合持久化、共享和 UI 复用;一次性筛选/排序可先用 `+record-list` / `+record-search` 的 filter/sort 验证结果,再按需要沉淀为持久视图。
## Dashboard / Workflow / Role
- Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md),组件必须串行创建;`+dashboard-arrange` 是服务端智能布局,仅在用户明确要求重排/美化、或对本次会话从零新建的仪表盘做收尾整理时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`
- Workflow 的复杂点是 `steps` 结构。创建、更新或解释完整 workflow 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md)enable/disable/list 只需确认 workflow ID、当前启停状态和用户意图。
- Role 的复杂点是权限 JSON。角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md)`+role-create` 只支持自定义角色;`+role-update` 是 delta merge角色 create/update 或解读完整配置时读权限 JSON SSOT [role-config.md](references/role-config.md)。`+role-delete` 只适用于自定义角色,系统角色不可删除;删除角色和关闭高级权限前必须确认目标和影响。
## 常见恢复
| 错误 / 现象 | 恢复动作 |
|---|---|
| `param baseToken is invalid` / `base_token invalid` | 检查是否把 wiki token、workspace token 或完整 URL 当成了 `--base-token`;按入口规则重新获取真实 `base_token` |
| `not found` 且输入来自 Wiki 链接 | 优先检查是否把 wiki token 当成 base token不要立刻改走裸 API |
| `1254045` 字段名不存在 | 重新 `+field-list`,使用真实字段名或字段 ID注意空格、大小写和跨表字段 |
| `1254015` 字段值类型不匹配 | 先 `+field-list`,再按 [lark-base-cell-value.md](references/lark-base-cell-value.md) 构造 CellValue |
| `Invalid discriminator value`(字段写入缺 `type` | 按完整提交规则读取当前字段,只改目标内容后提交;不要只补 `type` 重试 |
| filter 报 `value of type array` / `Only string values` | 用 record/view 的 tuple `--filter-json`(非 `+data-query` 对象型value 按字段 type 选标量或数组;见 [lark-base-view-set-filter.md](references/lark-base-view-set-filter.md) |
| 日期 / 人员 / 超链接字段报格式错误 | 日期用 `YYYY-MM-DD HH:mm:ss`;人员用 `[{ "id": "ou_xxx" }]`;超链接用 URL 或 markdown link 字符串 |
| formula / lookup 创建失败 | 先读 [formula-field-guide.md](references/formula-field-guide.md) / [lookup-field-guide.md](references/lookup-field-guide.md),再按 guide 重建请求 |
| `ignored_fields` / `READONLY` | 移除只读字段,只写存储字段 |
| `1254104` | 批量超过 200分批调用 |
| `1254291` | 并发写冲突,串行写入并在批次间短暂等待 |
| `91403` | 无权限访问该 Base`lark-shared` 权限流程处理,不要盲目重试 |
## 保留 Reference
- [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md):查询/统计/全局结论的选路 SOP
- [lark-base-data-query-guide.md](references/lark-base-data-query-guide.md) / [lark-base-data-query.md](references/lark-base-data-query.md):聚合查询入口 fewshot 与 DSL SSOT
- [lark-base-cell-value.md](references/lark-base-cell-value.md):记录 CellValue 构造
- [lark-base-field-json.md](references/lark-base-field-json.md):字段 JSON 构造
- [formula-field-guide.md](references/formula-field-guide.md) / [lookup-field-guide.md](references/lookup-field-guide.md):公式与 lookup 字段
- [lark-base-field-create.md](references/lark-base-field-create.md) / [lark-base-field-update.md](references/lark-base-field-update.md):字段创建/更新命令级补充
- [lark-base-record-upsert.md](references/lark-base-record-upsert.md) / [lark-base-record-batch-create.md](references/lark-base-record-batch-create.md) / [lark-base-record-batch-update.md](references/lark-base-record-batch-update.md) / [lark-base-record-history-list.md](references/lark-base-record-history-list.md):记录写入 JSON 与历史返回解释
- [lark-base-view-set-filter.md](references/lark-base-view-set-filter.md):视图筛选 JSON
- [lark-base-form-detail.md](references/lark-base-form-detail.md) / [lark-base-form-submit.md](references/lark-base-form-submit.md) / [lark-base-form-questions-create.md](references/lark-base-form-questions-create.md) / [lark-base-form-questions-update.md](references/lark-base-form-questions-update.md):表单详情、提交和复杂 JSON
- [lark-base-dashboard.md](references/lark-base-dashboard.md) / [dashboard-block-data-config.md](references/dashboard-block-data-config.md) / [lark-base-dashboard-block-get-data.md](references/lark-base-dashboard-block-get-data.md):仪表盘、组件配置与图表结果协议
- [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) / [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md)workflow 入口与 steps JSON SSOT
- [lark-base-role-guide.md](references/lark-base-role-guide.md) / [role-config.md](references/role-config.md):角色入口与权限 JSON SSOT

View File

@ -0,0 +1,376 @@
# dashboard block data_config SSOT
Block 的 `data_config` 字段因 `type` 不同而变化。本文档是 dashboard block `data_config` 的单一事实来源SSOT包含组件类型、字段结构、筛选格式、约束和可复制模板。
## 支持的组件类型(`type` 枚举)
| type 值 | 说明 |
|---------|------|
| `column` | 柱状图 |
| `bar` | 条形图 |
| `line` | 折线图 |
| `pie` | 饼图 |
| `ring` | 环形图 |
| `area` | 面积图 |
| `combo` | 组合图 |
| `scatter` | 散点图 |
| `funnel` | 漏斗图 |
| `wordCloud` | 词云 |
| `radar` | 雷达图 |
| `statistics` | 指标卡 |
| `text` | 文本(支持 Markdown |
## 字段类型与操作符速查AI 决策用)
> 先用 `+field-list` / `+field-get` 确认字段 `type`;本节使用当前字段接口里的 canonical 类型名:`number`、`text`、`select`、`datetime`、`checkbox`、`user`。
```
text: is, isNot, contains, doesNotContain, isEmpty, isNotEmpty
number: is, isNot, isGreater, isGreaterEqual, isLess, isLessEqual, isEmpty, isNotEmpty
selectmultiple=false: is, isNot, isEmpty, isNotEmpty
selectmultiple=true: is, isNot, contains, doesNotContain, isEmpty, isNotEmpty
datetime: is, isGreater, isGreaterEqual, isLess, isLessEqual, isEmpty, isNotEmpty
checkbox: is (value: true/false)
user / created_by / updated_by: is, isNot, isEmpty, isNotEmpty
```
## data_config 通用结构
| 字段 | 类型 | 说明 |
|------|------|------|
| `table_name` | string | 关联数据表名称 |
| `series` | `[{ "field_name": "xxx", "rollup": "SUM" }]` | 指标/Y 轴(与 `count_all` 二选一。rollup 支持 `SUM` / `MAX` / `MIN` / `AVERAGE` |
| `count_all` | boolean | COUNTA 聚合,统计所有记录数(与 `series` 二选一) |
| `group_by` | `[{ "field_name": "xxx", "mode": "integrated", "sort": {...} }]` | X 轴分组维度。`mode` 必填,`sort` 可选,见下方说明 |
| `filter` | object | 筛选条件 |
| `filter.conjunction` | `"and"` / `"or"` | 筛选逻辑 |
| `filter.conditions` | `[{ "field_name", "operator", "value" }]` | 筛选条件数组value 类型因字段类型而异(见下方 filter 格式规则) |
### text 类型特殊结构
`text` 类型组件用于展示富文本内容,**不需要数据源配置**(无 `table_name``series``group_by``filter`)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `text` | string | **必填**。支持 Markdown 语法,详见下方说明 |
**支持的 Markdown 语法:**
| 语法 | 示例 | 效果 |
|------|------|------|
| 一级标题 | `# 标题` | 大标题 |
| 二级标题 | `## 标题` | 中标题 |
| 三级标题 | `### 标题` | 小标题 |
| 加粗 | `**文字**` | **文字** |
| 斜体 | `*文字*` | *文字* |
| 删除线 | `~~文字~~` | ~~文字~~ |
| 有序列表 | `1. 项目` | 1. 项目 |
| 无序列表 | `- 项目` | - 项目 |
> **注意**:以上未提及的 Markdown 语法(如链接、图片、代码块、表格等)均不支持。
## group_by 详细说明
### mode 枚举
| mode | 含义 | 适用场景 |
|------|------|----------|
| `integrated` | 聚合分组(默认) | 绝大部分场景,按字段值分组统计 |
| `enumerated` | 多值拆分统计 | 多选、人员等多值字段,将每个选项/人员拆开独立统计 |
> 多选、人员等多值字段默认用 `enumerated`;其他字段默认用 `integrated`。
### sort 排序
| sort.type | 含义 | 典型场景 |
|-----------|------|----------|
| `group` | 按横轴值排序 | 按月份升序、按品类名字母序 |
| `value` | 按纵轴值排序 | 按销售额从大到小 |
| `view` | 按数据源记录顺序 | 保持原表行序(不常用) |
`sort.order``asc`(升序)/ `desc`(降序)
只要写 `sort` 对象就需要明确排序方向。CLI 会把 `sort.type``group``view` 且缺少 `order` 的情况规范化为 `order:"asc"``sort.type:"value"` 必须显式写 `order:"asc"``order:"desc"`,因为指标值排序方向会改变业务含义。
如果表中行序就是业务顺序,首次创建 block 时就一次性设置 `sort:{"type":"view","order":"asc"}` 保留行序,避免创建后再二次更新排序条件。
示例 — 柱状图按销售额降序:
```json
{
"table_name": "订单表",
"series": [{ "field_name": "金额", "rollup": "SUM" }],
"group_by": [{ "field_name": "类别", "mode": "integrated", "sort": {"type": "value", "order": "desc"} }]
}
```
## filter 格式规则
**基本结构:**
```json
{
"filter": {
"conjunction": "and",
"conditions": [
{ "field_name": "字段名", "operator": "操作符", "value": "值" }
]
}
}
```
**多条件示例and/or**
```json
{
"filter": {
"conjunction": "and",
"conditions": [
{ "field_name": "状态", "operator": "is", "value": "已完成" },
{ "field_name": "金额", "operator": "isGreater", "value": 1000 }
]
}
}
```
**操作符:**
| 操作符 | 含义 | 是否需要 value |
|--------|------|---------------|
| `is` | 等于 | 是 |
| `isNot` | 不等于 | 是 |
| `contains` | 包含 | 是 |
| `doesNotContain` | 不包含 | 是 |
| `isEmpty` | 为空 | 否 |
| `isNotEmpty` | 不为空 | 否 |
| `isGreater` | 大于 | 是 |
| `isGreaterEqual` | 大于等于 | 是 |
| `isLess` | 小于 | 是 |
| `isLessEqual` | 小于等于 | 是 |
**各字段类型的 value 格式:**
| 字段类型 | value 类型 | 适用操作符 | 示例 |
|----------|-----------|-----------|------|
| `text` | string | is, isNot, contains, doesNotContain, isEmpty, isNotEmpty | `{"field_name":"姓名","operator":"contains","value":"张"}` |
| `number` | number | is, isNot, isGreater, isGreaterEqual, isLess, isLessEqual, isEmpty, isNotEmpty | `{"field_name":"金额","operator":"isGreater","value":0}` |
| `select` (`multiple=false`) | string选项名 | is, isNot, isEmpty, isNotEmpty | `{"field_name":"状态","operator":"is","value":"已完成"}` |
| `select` (`multiple=true`) | string[](选多个)/ string选单个 | is, isNot, contains, doesNotContain, isEmpty, isNotEmpty | 多选传数组如 `["标签1","标签2"]`;单选传单个字符串 |
| `datetime` / `created_at` / `updated_at` | numberUnix 毫秒时间戳13位 | is, isGreater, isGreaterEqual, isLess, isLessEqual, isEmpty, isNotEmpty | `{"field_name":"创建日期","operator":"isGreater","value":1704038400000}` |
| `checkbox` | boolean | is | `{"field_name":"已审核","operator":"is","value":true}` |
| `user` / `created_by` / `updated_by` | string 或 string[](用户 ID格式 `ou_xxx`)。不知道 `open_id` 时先用 `lark-cli contact +search-user --query "<姓名/邮箱/手机号>" --as user` 查 id。 | is, isNot, isEmpty, isNotEmpty | `{"field_name":"负责人","operator":"is","value":"ou_xxxxxxxxxxxxxxxx"}` |
| 所有类型(为空/不为空) | 不需要 value | isEmpty, isNotEmpty | `{"field_name":"备注","operator":"isEmpty"}` |
> `value` 类型为 `string | number | boolean | string[]`,需根据字段类型匹配正确格式
## 约束与本地校验
- 必填与互斥
- 图表类型必填:`table_name`
- text 类型必填:`text`
- 互斥:`series``count_all` 二选一,且至少提供其一(仅图表类型)
- text 类型**不支持**`series``count_all``group_by``filter`
- 长度/结构
- `group_by` 最多 2 个;每项 `field_name` 必填
- `group_by[].sort.type` 取值 `group|value|view``order` 取值 `asc|desc`
- 规范化CLI 自动处理;`--no-validate` 时不生效,`data_config` 原样透传给后端)
- `series[].rollup` 自动转成大写(如 `sum``SUM`
- `group_by[].sort.type/order` 自动转成小写
- `group_by[].sort.type``group``view` 且缺少 `order` 时,自动补 `order:"asc"``value` 排序不会自动补方向
- 本地校验(可通过 `--no-validate` 跳过)
- `+dashboard-block-create` 默认对 `data_config` 做轻量校验;失败会聚合错误并给出修复建议
- `+dashboard-block-update` 不做强类型校验,由后端验证具体字段
- 仅需传入合法 JSONCLI 不会擅自改写你的业务含义
## 可复制模板
**按意图选择模板:**
- 比较不同类别数值 → 柱状图 / 条形图
- 看趋势变化 → 折线图 / 面积图
- 看占比分布 → 饼图 / 环形图 / 词云
- 多指标对比 → 组合图
- 看两变量关系 → 散点图
- 看流程转化 → 漏斗图
- 看多维度评分 → 雷达图
- 显示单个指标 → 指标卡(统计数字或记录数)
最小柱状图:
```json
{
"table_name": "表名",
"series": [{ "field_name": "数值字段", "rollup": "SUM" }],
"group_by": [{ "field_name": "分组字段", "mode": "integrated" }]
}
```
最小饼图/环形图(按分类字段统计行数占比):
```json
{
"table_name": "表名",
"count_all": true,
"group_by": [{ "field_name": "分类字段", "mode": "integrated" }]
}
```
折线图(按月趋势):
```json
{
"table_name": "表名",
"series": [{ "field_name": "金额", "rollup": "SUM" }],
"group_by": [{ "field_name": "月份", "mode": "integrated", "sort": {"type":"group","order":"asc"} }]
}
```
条形图(横向柱状图):
```json
{
"table_name": "表名",
"series": [{ "field_name": "数值字段", "rollup": "SUM" }],
"group_by": [{ "field_name": "分组字段", "mode": "integrated" }]
}
```
面积图(趋势填充):
```json
{
"table_name": "表名",
"series": [{ "field_name": "数值字段", "rollup": "SUM" }],
"group_by": [{ "field_name": "时间字段", "mode": "integrated", "sort": {"type":"group","order":"asc"} }]
}
```
组合图(柱+线等多指标对比):
```json
{
"table_name": "表名",
"series": [
{ "field_name": "指标1", "rollup": "SUM" },
{ "field_name": "指标2", "rollup": "SUM" }
],
"group_by": [{ "field_name": "分类字段", "mode": "integrated" }]
}
```
散点图(两变量相关性):
```json
{
"table_name": "表名",
"series": [{ "field_name": "Y轴字段数值/指标)", "rollup": "SUM" }],
"group_by": [{ "field_name": "X轴字段分类/维度)", "mode": "integrated" }]
}
```
漏斗图(流程转化):
先判断用户要看的数值语义:
- **当前数量**:统计每个当前状态/阶段下有多少记录,例如“各环节当前数量”“当前阶段分布”。源表有状态/阶段字段时,直接用 `count_all:true` + `group_by`
- **累计数量**:统计到达该阶段及其后续阶段(后缀和)的累计数量,例如“流程转化”“从 A 到 B 各环节转化”。此口径假设流程单向、无跳阶/回退、记录不删除;不满足时须用状态变更历史,不能对当前快照累加。如果表中已有累计数量字段或阶段汇总表,直接用该字段画漏斗图;否则先计算累计数量,创建并写入 helper 汇总表后再画图。
当前数量:
```json
{
"table_name": "表名",
"count_all": true,
"group_by": [{ "field_name": "状态字段", "mode": "integrated" }]
}
```
累计数量:
```json
{
"table_name": "流程汇总表名",
"series": [{ "field_name": "累计数量", "rollup": "SUM" }],
"group_by": [{ "field_name": "阶段字段", "mode": "integrated", "sort": {"type":"view","order":"asc"} }]
}
```
如果只有当前状态数据但用户要看流程转化,需要先按业务阶段顺序计算每个阶段的累计数量,再创建 helper 汇总表(如:阶段、累计数量),用 `+record-batch-create` 一次写入后按“累计数量”模板创建漏斗图。helper 表行序就是业务顺序时,首次创建 block 时一次性设置好 `group_by.sort`
> ⚠️ 注意:helper 汇总表仅用于源表无法直接聚合出目标形态的场景(如上面的累计数量漏斗图)。只要能在源表上直接用 `group_by` + `rollup`(含 `AVERAGE`)算出,就不需要新建 helper 表。
词云(文本频率):
```json
{
"table_name": "表名",
"count_all": true,
"group_by": [{ "field_name": "文本字段", "mode": "integrated" }]
}
```
雷达图(多维度评分):
```json
{
"table_name": "表名",
"series": [
{ "field_name": "维度1", "rollup": "SUM" },
{ "field_name": "维度2", "rollup": "SUM" },
{ "field_name": "维度3", "rollup": "SUM" }
],
"group_by": [{ "field_name": "分类字段", "mode": "integrated" }]
}
```
指标卡(统计数字):
```json
{
"table_name": "数据表",
"series": [{ "field_name": "数字", "rollup": "SUM" }]
}
```
指标卡(统计记录数):
```json
{
"table_name": "数据表",
"count_all": true
}
```
文本组件Markdown 富文本):
```json
{
"text": "# 🚀 一级标题\n这是一个 **加粗** *斜体* ~~删除线~~ 的示例。\n\n## 📌 二级标题\n1. 有序列表项 1\n2. 有序列表项 2\n\n### 📌 三级标题\n- 无序列表项 1\n- 无序列表项 2"
}
```
> **注意**text 类型组件不需要 `table_name`、`series`、`group_by`、`filter` 等数据源相关字段。
## 常见错误与修复
- 同时存在 `series``count_all`
- 现象:后端/本地校验报互斥错误
- 修复:见「关键约束」章节的二选一规则
- 缺少 `table_name`
- 现象:本地校验缺少必填字段
- 修复:指定数据源表名(使用表名,非表 ID
- `series[].rollup` 大小写/取值不合法
- 现象:本地校验提示枚举不支持
- 修复:改为 `SUM|MAX|MIN|AVERAGE` 中之一不区分大小写CLI 会统一为大写;计数请使用 `count_all:true`
- `group_by` 超出 2 个或字段名为空
- 修复:保留前 2 个,或补齐 `field_name`
- 排序枚举不合法
- 修复:`group_by.sort.type` 仅能为 `group|value|view``order``asc|desc`
- filter 写法不规范
- 修复:`conjunction``and|or``conditions[].operator` 必须在本页表格列举的范围内;除 `isEmpty/isNotEmpty` 外需提供 `value`
## 坑点
- **`count_all``series` 二选一** — 两者不能同时使用
- **filter `value` 类型因字段而异** — 文本/单选为 string数字为 number日期为毫秒时间戳多选/人员可为 string[],复选框为 boolean`isEmpty`/`isNotEmpty` 不需要 value
- **`data_config` 结构随 `type` 变化** — 不同组件类型的字段不同,创建前务必确认类型对应的字段
- **表名用 name不是 ID** — `table_name` 对应的是表名称(如「订单表」),不是 `table_id`

View File

@ -0,0 +1,737 @@
# Base Formula Writing Guide
## Mandatory Read Acknowledgement
When creating or updating a formula field with `lark-cli base +field-create/+field-update --json ...` and `type` is `formula`, you should read this guide first and only then add `--i-have-read-guide` to the command.
Do **not** proactively add `--i-have-read-guide` before reading this guide. Without it, the CLI will fail fast and direct you back to this guide.
When using `+field-update`, also pass `--yes`: field update is a high-risk `PUT` operation because changing a field definition can affect the whole column.
## Default strategy
**All cross-table references, aggregations, and computed fields should use Formula fields by default.** Do NOT use Lookup fields unless the user explicitly requests it. Formula is a strict superset of Lookup — anything Lookup can do, Formula can do with a single expression.
## Usage
When creating a formula field, the Agent should:
1. Get all table names: `lark-cli base +table-list --base-token <base>` — returns `items[].table_name`
2. Get table structure: `lark-cli base +table-get --base-token <base> --table-id <table>` — returns `fields[]`
3. If the formula references other tables, also get those tables' structures
4. Write the formula expression following this guide
5. Construct the Formula field JSON and submit it to create or update the field
**Key constraints**:
- The JSON must include `"type": "formula"` — this field is required
- Table names and field names in the formula must **exactly match** those returned by `+table-list` / `+table-get`
- The `expression` value is a string containing the formula expression; double quotes inside the expression must be properly escaped in JSON (e.g. `\"text\"`)
---
## Section 1: Core Concepts — Scalar vs List
This is the foundation of formula logic. You must determine this before writing any formula.
| Syntax | Meaning | Return type | Example |
| --------------------- | -------------------------------------------- | ---------------------- | -------------------------------------------- |
| `[Field]` | Value of this field in the current row | Scalar (single value) | `[Name]``"Alice"` |
| `[TableName].[Field]` | All values of this field in the target table | List (multiple values) | `[Employees].[Name]``["Alice","Bob",...]` |
| `[TableName]` | The target table (entire table) | Table reference | Used as data range for FILTER/COUNTIF etc. |
**Rules**:
- Scalars can be used directly in operations: `[Price] * [Quantity]`
- Lists cannot be used as scalars — they must be processed first: use `SUM()` for sum, `ARRAYJOIN(",")` for joining, `FIRST()`/`LAST()`/`NTH()` for single value extraction
- Link field access `[LinkField].[TargetField]` returns a list (values of the target field for all linked records)
- **LISTCOMBINE flattening rule**: When a FILTER's result column is itself a multi-value field (`select` with `multiple=true`, `link`, etc.), it produces a 2D array and **must** be flattened with `.LISTCOMBINE()`; for single-value fields (`number`, `text`, etc.) it can be omitted, but adding it is never wrong:
```
[Table].FILTER(CurrentValue.[Field] = [Value]).[Tags].LISTCOMBINE() ← required for multi-value columns
[Table].FILTER(CurrentValue.[Field] = [Value]).[NumberCol].LISTCOMBINE() ← optional for single-value columns
```
---
## Section 2: Data Types and Type Conversion
### Field storage types
| Type | Description | Supported operations |
|------|-------------|----------------------|
| `number` | Stored as numeric value | Math operations, comparisons, auto-converts to string for concatenation |
| `text` | Stored as string | String operations; can participate in math if content is numeric, otherwise errors |
| `datetime` | Date object | Date functions, add/subtract with numbers; auto-converts to default format string when using `&` — use TEXT to format first for controlled output |
| `select` (`multiple=true`) | Data list | List functions, CONTAIN checks |
| `link` | Links to other table records | Chained access `[LinkField].[Field]`, result is a list |
| `checkbox` | TRUE/FALSE | Logical operations; auto-converts to number when compared with numbers |
### Implicit type conversion
| Scenario | Conversion rule |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Number + Float | → Float |
| Date + Number | → Date (adds/subtracts days). Use `+`/`-` for whole days, use `DURATION()` for hour/minute/second precision |
| Date - Date | → Duration |
| Boolean compared with Number | Boolean auto-converts to number (TRUE=1, FALSE=0) |
| `&` concatenation | Both sides auto-convert to string |
### Type consistency in comparisons
When using comparison operators (`>`, `>=`, `<`, `<=`, `=`, `!=`), **both sides should be the same type** to avoid semantic errors or unexpected results.
**Principle**: When types differ, explicitly convert one side rather than relying on implicit conversion:
- `number` vs `text` → use `VALUE()` to convert text to number
- `datetime` vs `text` → use `TEXT()` to convert date to text
- `datetime` vs `datetime` equality → dates include time components, so direct `=` comparison may fail due to different hours/minutes/seconds. For day-level equality, convert to text first: `TEXT([DateA], "YYYY/MM/DD") = TEXT([DateB], "YYYY/MM/DD")`
- `select` and `user` fields can be compared with both same-type values and text
- `text` fields in numeric aggregation (SUM/AVERAGE/MIN/MAX etc.) → convert to number with `VALUE()` first. For FILTER results, use `.MAP(VALUE(CurrentValue)).SUM()`
---
## Section 3: CurrentValue
**CurrentValue is the iteration variable in FILTER/MAP/COUNTIF/SUMIF functions, representing the "current item" being processed in the data range.**
### CurrentValue meaning in different contexts
| Data range type | CurrentValue represents | Access pattern | Example |
| ---------------------------- | ----------------------- | --------------------------- | --------------------------------------------------------- |
| Entire table `[TableName]` | A row in the table | `CurrentValue.[FieldName]` | `[Orders].FILTER(CurrentValue.[Amount] > 100).[Customer]` |
| Column `[TableName].[Field]` | A single field value | Use `CurrentValue` directly | `[Orders].[Amount].FILTER(CurrentValue > 100)` |
| `select` (`multiple=true`) field `[Tags]` | One option | Use `CurrentValue` directly | `[Tags].FILTER(CurrentValue = "Important")` |
| LIST-generated list | One element | Use `CurrentValue` directly | `LIST(1,2,3).MAP(CurrentValue * 2)` |
### Key rules
1. **When data range is a table**, use `CurrentValue.[FieldName]` to access row fields
2. **When data range is a column/list**, use `CurrentValue` directly for the element value — **cannot** use `CurrentValue.[FieldName]`
3. CurrentValue can **only** appear inside the condition/mapping parameters of FILTER/MAP/COUNTIF/SUMIF functions
4. To reference the current table's field value in a condition, write `[FieldName]` directly — it refers to the formula row's value, not a property of CurrentValue
### Anti-patterns
| Wrong | Reason | Correct |
| ---------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `[Table].[Col].FILTER(CurrentValue.[Col] > 0)` | Data range is a column; CurrentValue is a scalar, cannot use `.` to access fields | `[Table].[Col].FILTER(CurrentValue > 0)` |
| `[Table].FILTER(CurrentValue > 100)` | Data range is a table; CurrentValue is a row, cannot compare directly | `[Table].FILTER(CurrentValue.[Amount] > 100).[Amount]` |
| `CurrentValue + 1` (at top level) | CurrentValue can only be used inside iteration functions | Use inside MAP/FILTER etc. |
---
## Section 4: Operators
Base formulas **only allow** the following operators. `like`, `in`, `<>`, `**`, `^` etc. are prohibited.
| Category | Operators | Description |
| ------------- | -------------------------- | -------------------------------------------------------------------------- |
| Arithmetic | `+` `-` `*` `/` `%` | Add, subtract, multiply, divide, modulo (`%` is equivalent to `MOD()`) |
| Comparison | `>` `>=` `<` `<=` `=` `!=` | Greater than, greater or equal, less than, less or equal, equal, not equal |
| Logical | `&&` `\|\|` | AND, OR |
| Concatenation | `&` | Text concatenation; non-text values auto-convert to string |
**Important**:
- Equality uses `=` (single equals), not `==`
- Not-equal uses `!=`, not `<>`
- String concatenation uses `&`, not `+`
- Both `&&`/`||` and AND()/OR() functions are supported
---
## Section 5: Link Fields and Cross-Table References
### Link field description
When a field type is described as `FieldName: Link [target table: X, foreign key: Y]`, it links to target table X using field Y as the join key.
### Chained cross-table access
```
[LinkField].[TargetField]
```
Retrieves the target field values for all linked records as a list. Supports continued chaining: `[LinkA].[LinkB].[Field]`.
### Equivalent expanded form
- Multi-value link: `[TargetTableX].FILTER([LinkField].CONTAIN(CurrentValue.[Y])).[TargetField].LISTCOMBINE()`
- Single-value link: `[TargetTableX].FILTER(CurrentValue.[Y] = [LinkField]).[TargetField].LISTCOMBINE()`
(`.LISTCOMBINE()` is required when `[TargetField]` is a multi-value field; optional for single-value fields)
### Notes
- Link fields typically return **lists** (possibly empty)
- To output a single value, use aggregation (SUM/MAX), joining (ARRAYJOIN), or extraction (FIRST/LAST/NTH)
- Do not nest FILTER inside FILTER for cross-table queries — prefer link field chained access
---
## Section 6: Function Call Conventions
### Two calling styles
| Style | Format | Description |
| ---------- | ------------------ | ----------------------------------- |
| Functional | `FUNC(arg1, arg2)` | Works for all functions |
| Chained | `arg1.FUNC(arg2)` | Moves the first argument before `.` |
**Rules**:
- Zero-argument functions cannot be chained: `NOW()`, `TODAY()`, `PI()`, `TRUE()`, `FALSE()`
- SORTBY can **only** be chained: `[Table].SORTBY([Table].[SortCol]).[OutputCol]`. The sort column always uses the original table's column name (`[TableName].[Field]` format); the engine aligns rows internally, even when the data range is a FILTER result
- FILTER is recommended to be chained: `[Table].FILTER(condition).[OutputCol]`
### FILTER / SORTBY result column rules
- **When data range is a table** `[TableName]`, FILTER / SORTBY returns a table reference. The chain **must** end with `.[Field]` to specify the result column, otherwise the formula fails:
```
Correct: [Sales].FILTER(CurrentValue.[Amount] > 100).[Customer]
Correct: [Sales].FILTER(condition).SORTBY([Sales].[SortCol]).[Customer] ← result column at end of chain
Wrong: [Sales].FILTER(CurrentValue.[Amount] > 100) ← missing result column
```
- **When data range is a column** `[TableName].[Field]` or a list, FILTER returns the filtered list directly — **no** result column needed:
```
Correct: [Sales].[Amount].FILTER(CurrentValue > 100)
```
After the result column, it's recommended to flatten with `.LISTCOMBINE()` first (especially when the result column is a multi-value field), then chain aggregation functions:
```
[Sales].FILTER(CurrentValue.[Amount] > 100).[Amount].LISTCOMBINE().SUM()
```
---
## Section 7: Hard Constraints
1. **Nesting prohibition**: FILTER / SUMIF / COUNTIF / MAP **must not be nested** inside each other's condition/mapping expressions. None of these functions can appear inside the condition or mapping parameter of another.
- Prohibited: `[Table1].FILTER(CurrentValue.[Col] = [Table2].FILTER(...).[Col])` ← FILTER inside FILTER condition
- Prohibited: `[Table].MAP([Table2].MAP(...))` ← MAP inside MAP mapping
- **Allowed**: `[Table].FILTER(cond1).[Col].FILTER(cond2)` ← chained call; the first FILTER's output is the second's data range, not nesting
2. **Function whitelist**: Only use functions listed in Section 8. No unlisted functions.
3. **Exact name matching**: Table names and field names in formulas must **exactly match** those returned by `+table-get` — no renaming or adding spaces.
4. **Operator whitelist**: Only use operators listed in Section 4.
5. **Strings use double quotes**: Strings must be wrapped in double quotes `"`, single quotes are not supported.
6. **Do not use LOOKUP**: FILTER is a superset of LOOKUP. All LOOKUP formulas can be rewritten with FILTER. Use FILTER exclusively to reduce complexity.
---
## Section 8: Complete Function Reference
### 8.1 Logic functions
| Function | Signature | Return type | Description |
| ------------- | ------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------- |
| IF | `IF(condition, true_val, [false_val])` | Matches branch type | Returns true_val when TRUE, false_val otherwise; omitting false_val returns false (not null) |
| IFS | `IFS(cond1, val1, cond2, val2, ...)` | Matches branch type | Multi-condition branching; returns value for the first TRUE condition |
| SWITCH | `SWITCH(expr, match1, result1, [match2, result2, ...], [default])` | Matches branch type | Matches expression value and returns corresponding result |
| IFERROR | `IFERROR(expr, fallback)` | Matches branch type | Returns fallback when expression errors |
| IFBLANK | `IFBLANK(expr, fallback)` | Matches branch type | Returns fallback when expression is blank (blank = NULL/empty string/empty list) |
| AND | `AND(cond1, cond2, ...)` | Boolean | TRUE when all conditions are TRUE |
| OR | `OR(cond1, cond2, ...)` | Boolean | TRUE when any condition is TRUE |
| NOT | `NOT(condition)` | Boolean | Logical negation |
| ISBLANK | `ISBLANK(value)` | Boolean | Tests if blank (NULL/empty string/empty list are blank; 0 and FALSE are not) |
| ISNULL | `ISNULL(value)` | Boolean | Tests if NULL (only NULL is true; empty string is not) |
| ISERROR | `ISERROR(expr)` | Boolean | Tests if expression errors |
| ISNUMBER | `ISNUMBER(value)` | Boolean | Tests if value is a number |
| CONTAIN | `CONTAIN(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains the value; **does NOT do text substring matching** |
| CONTAINSALL | `CONTAINSALL(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains all specified values |
| CONTAINSONLY | `CONTAINSONLY(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains only the specified values |
| TRUE | `TRUE()` | Boolean | Returns TRUE |
| FALSE | `FALSE()` | Boolean | Returns FALSE |
| RECORD_ID | `RECORD_ID()` | Text | Returns the current row's record ID |
| RANDOMBETWEEN | `RANDOMBETWEEN(min_int, max_int, [keep_updating])` | Number | Random integer in the specified range |
| RANDOMITEM | `RANDOMITEM(list, [keep_updating])` | Matches element type | Randomly picks one element from a list |
### 8.2 Numeric functions
| Function | Signature | Return type | Description |
| --- | --- | --- | --- |
| SUM | `SUM(val1, val2, ...)` | Number | Sum; accepts multiple values or a list |
| AVERAGE | `AVERAGE(val1, val2, ...)` | Number | Average |
| MAX | `MAX(val1, val2, ...)` | Number | Maximum |
| MIN | `MIN(val1, val2, ...)` | Number | Minimum |
| MEDIAN | `MEDIAN(val1, val2, ...)` | Number | Median |
| COUNTA | `COUNTA(val1, val2, ...)` | Number | Count of non-blank values |
| COUNTIF | `COUNTIF(data_range, condition)` | Number | Count matching items. Data range can be a **table** (CurrentValue is a row, use `CurrentValue.[Field]`) or a **column** (CurrentValue is a scalar value) |
| SUMIF | `SUMIF(data_range, condition)` | Number | Sum matching values. Data range **must be a numeric column** (e.g. `[Table].[NumField]`); CurrentValue is each value in that column (scalar), cannot use `CurrentValue.[Field]` to access other fields. For cross-field conditions, use FILTER+SUM instead |
| ROUND | `ROUND(number, digits)` | Number | Round. digits: 1=one decimal, 0=integer, -1=tens place |
| ROUNDUP | `ROUNDUP(number, digits)` | Number | Round away from zero. Same digits semantics as ROUND |
| ROUNDDOWN | `ROUNDDOWN(number, digits)` | Number | Round toward zero. Same digits semantics as ROUND |
| FLOOR | `FLOOR(number, [base])` | Number | Round down to nearest multiple of base (default 1) |
| CEILING | `CEILING(number, [base])` | Number | Round up to nearest multiple of base (default 1) |
| ABS | `ABS(number)` | Number | Absolute value |
| INT | `INT(number)` | Integer | Truncate to integer |
| MOD | `MOD(dividend, divisor)` | Number | Modulo |
| POWER | `POWER(base, exponent)` | Number | Exponentiation |
| QUOTIENT | `QUOTIENT(dividend, divisor)` | Number | Integer division |
| VALUE | `VALUE(text)` | Number | Convert text to number |
| ISODD | `ISODD(number)` | Boolean | Tests if number is odd |
| RANK | `RANK(value, search_range, [ascending])` | Number | Rank of value in range; default descending |
| SEQUENCE | `SEQUENCE(start, end, [step])` | List | Generate number sequence |
| PI | `PI()` | Number | Pi constant |
| SIN/COS/TAN/ASIN/ACOS/ATAN/ATAN2/SINH/COSH/TANH/ASINH/ACOSH/ATANH | `func(radians_or_value)` | Number | Trigonometric and hyperbolic functions; arguments in radians |
### 8.3 Text functions
| Function | Signature | Return type | Description |
| --------------- | ---------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- |
| CONCATENATE | `CONCATENATE(text1, text2, ...)` | Text | Concatenate multiple texts; supports lists as input |
| LEN | `LEN(text)` | Number | Character count |
| LEFT | `LEFT(text, [count])` | Text | Extract from left; default 1 |
| RIGHT | `RIGHT(text, [count])` | Text | Extract from right; default 1 |
| MID | `MID(text, start, count)` | Text | Extract from middle |
| FIND | `FIND(search_val, search_range, [start])` | Number | Find substring position (case-sensitive); returns -1 if not found |
| REPLACE | `REPLACE(text, start, count, new_text)` | Text | Replace by position |
| SUBSTITUTE | `SUBSTITUTE(text, old_text, new_text, [occurrence])` | Text | Replace by content; can specify which occurrence |
| UPPER | `UPPER(text)` | Text | Convert to uppercase |
| LOWER | `LOWER(text)` | Text | Convert to lowercase |
| TRIM | `TRIM(text)` | Text | Remove leading/trailing spaces |
| TEXT | `TEXT(value, format)` | Text | Format output. Date formats: `"YYYY-MM-DD"`, `"YYYY/MM/DD hh:mm:ss"`; number formats: `"00"`, `"000.00"` |
| CONTAINTEXT | `CONTAINTEXT(text, search_text)` | Boolean | Tests if text contains substring (text substring matching) |
| SPLIT | `SPLIT(text, delimiter)` | List | Split text by delimiter |
| TODATE | `TODATE(value)` | Date | Convert date string to date type |
| CHAR | `CHAR(number)` | Text | ASCII code to character |
| FORMAT | `FORMAT(template, [val1, val2, ...])` | Text | Template string formatting; use `{1}`, `{2}` as placeholders |
| HYPERLINK | `HYPERLINK(url, [display_text])` | Hyperlink | Create a hyperlink |
| ENCODEURL | `ENCODEURL(text)` | Text | URL encode |
| REGEXMATCH | `REGEXMATCH(text, regex)` | Boolean | Regex match test |
| REGEXEXTRACT | `REGEXEXTRACT(text, regex)` | List | Extract first match's capture groups |
| REGEXEXTRACTALL | `REGEXEXTRACTALL(text, regex)` | 2D List | Extract all matches |
| REGEXREPLACE | `REGEXREPLACE(text, regex, replacement)` | Text | Regex replace |
### 8.4 Date functions
| Function | Signature | Return type | Description |
| ----------- | ----------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------- |
| NOW | `NOW()` | Date | Current date and time |
| TODAY | `TODAY()` | Date | Current date (midnight) |
| DATE | `DATE(year, month, day)` | Date | Construct a date |
| YEAR | `YEAR(date)` | Number | Extract year |
| MONTH | `MONTH(date)` | Number | Extract month |
| DAY | `DAY(date)` | Number | Extract day |
| HOUR | `HOUR(date)` | Number | Extract hour |
| MINUTE | `MINUTE(date)` | Number | Extract minute |
| SECOND | `SECOND(date)` | Number | Extract second |
| WEEKDAY | `WEEKDAY(date, [type])` | Number | Day of week |
| WEEKNUM | `WEEKNUM(date, [type])` | Number | Week number |
| DAYS | `DAYS(end_date, start_date)` | Number | Days between two dates (end - start), includes decimals. **Note parameter order: end date comes first** |
| DATEDIF | `DATEDIF(start_date, end_date, [unit])` | Number | Whole days/months/years between dates. Unit: `"D"`(default)/`"M"`/`"Y"`. **Start must be before end** |
| DURATION | `DURATION(days, [hours], [minutes], [seconds])` | Duration | Create a duration for date arithmetic |
| EDATE | `EDATE(date, months)` | Date | Date N months later |
| EOMONTH | `EOMONTH(date, [months])` | Date | End of month N months later; months default 0 |
| WORKDAY | `WORKDAY(start_date, days, [holidays])` | Date | Date N workdays later (skips weekends and holidays) |
| NETWORKDAYS | `NETWORKDAYS(start_date, end_date, [holidays])` | Number | Workdays between dates (inclusive) |
### 8.5 List functions
| Function | Signature | Return type | Description |
| --- | --- | --- | --- |
| LIST | `LIST(val1, val2, ...)` | List | Create a list |
| FIRST | `FIRST(list)` | Scalar | First element |
| LAST | `LAST(list)` | Scalar | Last element |
| NTH | `NTH(list, index)` | Scalar | Nth element (1-based) |
| FILTER | `[Table].FILTER(condition).[ResultCol]` or `[Table].[Col].FILTER(condition)` | List | Filter by condition. When data range is a table, result column is **required**; when it's a column/list, it's not needed. Use CurrentValue in conditions. Add `.LISTCOMBINE()` when result column is multi-value |
| MAP | `data_range.MAP(mapping_expr)` | List | Apply mapping to each element. Use CurrentValue in mapping |
| SORT | `SORT(list, [ascending])` | List | Sort; default ascending (TRUE) |
| SORTBY | `[Table].SORTBY([Table].[SortCol], [ascending]).[OutputCol]` | List | Sort by column then extract output column. **Chain-only, must include output column** |
| UNIQUE | `UNIQUE(list)` | List | Deduplicate |
| ARRAYJOIN | `ARRAYJOIN(list, [delimiter])` | Text | Join list elements as text; default comma-separated |
| LISTCOMBINE | `LISTCOMBINE(val1, [val2, ...])` or `list.LISTCOMBINE()` | List | Two uses: (1) merge values/lists into one list; (2) chained call to flatten 2D array (commonly used when FILTER result column is a multi-value field) |
| DISTANCE | `DISTANCE(location1, location2)` | Number | Distance between two geographic locations (km) |
---
## Section 9: Commonly Confused Functions
### CONTAIN vs CONTAINTEXT
| | CONTAIN | CONTAINTEXT |
| ----------- | -------------------------------------------------------------- | ---------------------------------------------------------- |
| Purpose | Tests if a **list / `select` (`multiple=true`)** contains a value | Tests if **text** contains a substring |
| Example | `[Tags].CONTAIN("Urgent")` | `[Notes].CONTAINTEXT("completed")` |
| Wrong usage | `CONTAIN([Notes], "completed")` — cannot do substring matching | `CONTAINTEXT([Tags], "Urgent")` — Tags is a list, not text |
### ISBLANK vs ISNULL
| | ISBLANK | ISNULL |
| ----------------- | ------- | ------ |
| NULL | TRUE | TRUE |
| `""` empty string | TRUE | FALSE |
| Empty list `[]` | TRUE | FALSE |
| `0` | FALSE | FALSE |
| `FALSE` | FALSE | FALSE |
### DAYS vs DATEDIF
| | DAYS | DATEDIF |
| --------------- | ------------------------------------------------------------ | ----------------------------------------- |
| Parameter order | `DAYS(end, start)` — end first | `DATEDIF(start, end, unit)` — start first |
| Precision | Includes decimals (hours/minutes/seconds as fractional days) | Integer only (whole days/months/years) |
| Negative values | Returns negative when start is after end | **Errors** when start is after end |
### SUM vs SUMIF
| | SUM | SUMIF |
| --------- | ---------------------------------------------- | -------------------------------------------------------------- |
| Purpose | Sum all values | Sum values **matching a condition** |
| Arguments | `SUM(val1, val2, ...)` or `SUM([Table].[Col])` | `SUMIF(data_range, condition)` with CurrentValue in condition |
| Example | `SUM([Orders].[Amount])` — sum all | `SUMIF([Orders].[Amount], CurrentValue > 100)` — sum only >100 |
### FILTER+aggregation vs COUNTIF/SUMIF
| | FILTER+aggregation | COUNTIF/SUMIF |
| ----------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ |
| Nature | Filter then aggregate (two steps) | One-step (syntactic sugar) |
| Equivalence | `[Table].FILTER(cond).[Col].LISTCOMBINE().SUM()` | `SUMIF([Table].[Col], cond)` (only when condition involves only column values) |
| When to use | Conditions span multiple fields, or multi-step needed | Conditions only involve column values (e.g. `CurrentValue > 100`) |
---
## Section 10: Decision Trees
### Cross-table queries: which approach?
```
Need data from another table?
├─ Current table has a link field to the target table?
│ ├─ Yes → Use chained access: [LinkField].[TargetField]
│ │ Need aggregation? → .SUM() / .ARRAYJOIN(",") / .FIRST()
│ └─ No → Need to match by field value?
│ ├─ Field matching or complex filtering → [TargetTable].FILTER(CurrentValue.[MatchField] = [Value]).[OutputCol]
│ └─ Only counting or summing → COUNTIF([TargetTable], condition) / FILTER+SUM
```
### Conditional logic: IF vs IFS vs SWITCH?
```
Need conditional logic?
├─ Single condition → IF(condition, true_val, false_val)
├─ Multiple mutually exclusive conditions (if-elseif-else) → IFS(cond1, val1, cond2, val2, ...)
├─ Matching a value against fixed options → SWITCH(expr, option1, result1, option2, result2, ..., default)
└─ Need error handling?
├─ Catch errors → IFERROR(expr, fallback)
└─ Catch blanks → IFBLANK(expr, fallback)
```
### Aggregation: which function?
```
Need to aggregate data?
├─ Sum/average/max/min for entire column → SUM/AVERAGE/MAX/MIN([Table].[Col])
├─ Count non-blank → COUNTA([Table].[Col])
├─ Conditional count → COUNTIF([Table], CurrentValue.[Field] = [Value])
├─ Conditional sum (column-only condition) → SUMIF([Table].[Col], CurrentValue > threshold)
├─ Conditional sum (cross-field condition) → [Table].FILTER(CurrentValue.[Field]=value).[NumCol].LISTCOMBINE().SUM()
├─ Count unique → [Table].[Col].UNIQUE().COUNTA()
└─ Ranking → RANK([Value], [Table].[Col])
```
---
## Section 11: Common Formula Patterns
### Pattern 1: Cross-table conditional count
Count rows in target table matching a condition:
```
[TargetTable].COUNTIF(CurrentValue.[MatchField] = [CurrentTableField])
```
### Pattern 2: Cross-table conditional sum
Filter target table by current row's value, then sum:
```
[TargetTable].FILTER(CurrentValue.[MatchField] = [CurrentTableField]).[NumCol].LISTCOMBINE().SUM()
```
SUMIF works when data range is a column and conditions only involve column values:
```
SUMIF([TargetTable].[NumCol], CurrentValue > 100)
```
Note: COUNTIF can use a table as data range (only counting, no specific column needed), but SUMIF's data range **must be a numeric column** (needs values to sum), so `CurrentValue` is each value in that column (scalar) — cannot use `CurrentValue.[OtherField]` to access other fields. For cross-field conditions, use FILTER with a table as data range.
### Pattern 3: Cross-table lookup
```
[TargetTable].FILTER(CurrentValue.[MatchCol] = [CurrentTableField]).[ReturnCol]
```
### Pattern 4: Link field values + aggregation
```
SUM([LinkField].[NumField])
[LinkField].[TextField].UNIQUE().ARRAYJOIN(",")
```
### Pattern 5: Conditional text concatenation
```
IF([Condition], "prefix" & [Field] & "suffix", "default text")
```
### Pattern 6: Date difference
```
DATEDIF([StartDate], [EndDate], "D") & " days"
DAYS([EndDate], [StartDate])
```
### Pattern 7: List element mapping
```
[SelectField(which multiple=true)].MAP(CurrentValue & " tag")
SPLIT([TextField], ",").MAP(TRIM(CurrentValue))
```
### Pattern 8: Cross-table with sorting
```
[TargetTable].SORTBY([TargetTable].[SortCol], FALSE).[OutputCol]
[TargetTable].FILTER(CurrentValue.[Field] = [Value]).SORTBY([TargetTable].[SortCol]).[OutputCol]
```
---
## Section 12: Anti-Pattern Collection
### Mistake 1: Extra argument in MAP
```
Wrong: [Table].[Col].MAP([Table2].[Col], CurrentValue + 1)
Correct: [Table].[Col].MAP(CurrentValue + 1)
```
Reason: MAP takes only two arguments (data range + mapping expression), no "lookup range".
### Mistake 2: Inverted FILTER syntax
```
Wrong: condition.[Table].FILTER()
Correct: [Table].FILTER(condition).[ResultCol] (result column required when data range is a table)
```
Reason: FILTER's data range comes first, condition is passed as the argument.
### Mistake 3: Using CurrentValue.[Field] on a column range
```
Wrong: SUMIF([Sales].[Revenue], CurrentValue.[Salesperson] = [Name])
Correct: [Sales].FILTER(CurrentValue.[Salesperson] = [Name]).[Revenue].LISTCOMBINE().SUM()
```
Reason: `SUMIF([Sales].[Revenue], ...)` uses "Revenue" column as data range. CurrentValue is each revenue value (scalar), not a row — cannot use `.` to access other fields. Use FILTER with the table as data range for cross-field conditions.
### Mistake 4: Missing result column after FILTER
```
Wrong: [Sales].FILTER(CurrentValue.[Amount] > 100)
Correct: [Sales].FILTER(CurrentValue.[Amount] > 100).[Customer]
```
Reason: FILTER on a table returns a table reference; must specify result column with `.[Field]` at the end.
### Mistake 5: Nested FILTER
```
Wrong: [Table1].FILTER(CurrentValue.[ID] = [Table2].FILTER(CurrentValue.[Status]="Done").[ID])
Correct: [Table1].FILTER(CurrentValue.[ID] = [CurrentRowField]).[OutputCol]
```
Reason: FILTER/MAP/SUMIF/COUNTIF cannot be nested inside each other's conditions. Split into multiple steps or use link fields.
### Mistake 6: SORTBY without output column
```
Wrong: [Table].SORTBY([Table].[Col])
Correct: [Table].SORTBY([Table].[Col]).[OutputCol]
```
Reason: SORTBY must have an output column at the end; otherwise the result cannot be represented as an array.
### Mistake 7: SORTBY sort column without table name
```
Wrong: [Table].SORTBY([Col]).[OutputCol]
Correct: [Table].SORTBY([Table].[Col]).[OutputCol]
```
Reason: SORTBY's sort column must use `[TableName].[FieldName]` format.
### Mistake 8: Using CONTAIN for text substring matching
```
Wrong: CONTAIN([Notes], "urgent")
Correct: CONTAINTEXT([Notes], "urgent")
```
Reason: CONTAIN checks if a list or `select` (`multiple=true`) contains a whole value, not substring matching. Use CONTAINTEXT for text substrings.
### Mistake 9: Date concatenation without formatting
```
Not recommended: "Deadline: " & [DateField] ← output format is uncontrolled
Recommended: "Deadline: " & TEXT([DateField], "YYYY-MM-DD")
```
Reason: Concatenating a date with `&` won't error, but uses the default format. Use TEXT to specify the format explicitly.
### Mistake 10: Reversed DAYS parameter order
```
Wrong: DAYS([StartDate], [EndDate]) → returns negative
Correct: DAYS([EndDate], [StartDate]) → returns positive
```
Reason: DAYS parameter order is end date first, start date second.
### Mistake 11: Chaining zero-argument functions
```
Wrong: TODAY.DAYS([Date])
Correct: TODAY().DAYS([Date])
```
Reason: NOW, TODAY, PI and other zero-argument functions must include parentheses.
---
## Section 13: Complete Examples
### Example 1: Employee sales summary
**Table structure** (from `+table-get`):
- Employees: EmployeeID (Text), Name (Text), Department (Text)
- Sales: ContractID (Number), SalespersonID (Text), Quantity (Number), Total (Number)
**Current table**: Employees
**Requirement**: For each employee, output "Sold XX orders" if they have sales records, otherwise "No sales records".
**Formula**:
```
IF(
[Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) >= 1,
"Sold " & [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) & " orders",
"No sales records"
)
```
**Field JSON**:
```json
{
"type": "formula",
"name": "Sales Summary",
"expression": "IF([Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) >= 1, \"Sold \" & [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) & \" orders\", \"No sales records\")"
}
```
**Explanation**: `[Sales].COUNTIF(...)` uses the entire Sales table as data range. CurrentValue represents each row in Sales, accessing `CurrentValue.[SalespersonID]` for that row's salesperson. `[EmployeeID]` refers to the current row in the Employees table (where the formula lives).
### Example 2: Chained cross-table access via link fields
**Table structure**:
- Orders: ID (`auto_number`), OrderItems (`link` [target: OrderItems, foreign key: ID])
- OrderItems: ID (`auto_number`), Product (`link` [target: Products, foreign key: ID])
- Products: ID (`auto_number`), ProductName (`text`)
**Current table**: Orders
**Requirement**: Deduplicate and comma-join all product names from linked order items.
**Formula**:
```
[OrderItems].[Product].[ProductName].UNIQUE().ARRAYJOIN(",")
```
**Field JSON**:
```json
{
"type": "formula",
"name": "Product List",
"expression": "[OrderItems].[Product].[ProductName].UNIQUE().ARRAYJOIN(\",\")"
}
```
**Explanation**: `[OrderItems]` gets linked order item records, `.[Product]` expands to each item's linked product, `.[ProductName]` gets all product names, `.UNIQUE()` deduplicates, `.ARRAYJOIN(",")` joins with commas.
### Example 3: Cross-table filter + sort
**Table structure**:
- Projects: ProjectName (Text), Status (Text), Owner (Text)
- Tasks: TaskName (Text), Project (Text), Priority (Number), DueDate (Date)
**Current table**: Projects
**Requirement**: Find the highest-priority (lowest number) task name for the current project.
**Formula**:
```
FIRST(
[Tasks].FILTER(CurrentValue.[Project] = [ProjectName]).SORTBY([Tasks].[Priority], TRUE).[TaskName]
)
```
**Field JSON**:
```json
{
"type": "formula",
"name": "Top Priority Task",
"expression": "FIRST([Tasks].FILTER(CurrentValue.[Project] = [ProjectName]).SORTBY([Tasks].[Priority], TRUE).[TaskName])"
}
```
**Explanation**: `[Tasks].FILTER(CurrentValue.[Project] = [ProjectName])` filters tasks belonging to the current project. `.SORTBY([Tasks].[Priority], TRUE)` sorts by priority ascending. `.[TaskName]` extracts task names. `FIRST(...)` gets the first one (highest priority).
---
## Section 14: Translating User Requirements to Formulas
When the user describes their formula need in natural language, follow these rules to convert it into a precise expression:
1. **Numbers must use precise values**: "less than 80%" → field value less than `0.8`. "above 1000" → `>= 1000`.
2. **Interval boundaries**: "above/below/within" = closed (inclusive); "less than/more than/outside" = open (exclusive).
3. **Branching logic** must be organized as an ordered list with a fallback branch. Each branch has a condition and output.
- Example: "return risk level for 1-3" → `IFS([Value] = 1, "low", [Value] = 2, "medium", [Value] = 3, "high")` with an `IFERROR` or trailing empty-string fallback.
4. **Multi-level branches must be flattened** to a single level. Nested if-else chains → flat IFS.
5. **Branch conditions must be mutually exclusive**. If the user's conditions overlap, rewrite to eliminate ambiguity.
6. **Reorder branches by logical priority** if the user's order is illogical (e.g., check specific conditions before catch-all).
---
## Section 15: Constraint Summary
- Request body must include `"type": "formula"` — this field is required
- Only use functions and operators listed in this document
- FILTER/SUMIF/COUNTIF/MAP must not be nested inside each other's conditions (chained calls are not nesting)
- Do not use LOOKUP — use FILTER exclusively
- Table and field names must exactly match `+table-get` output
- Strings must use double quotes `"`
- Format dates with TEXT before concatenating, to control output format
- SORTBY can only be chained and must include an output column
- Link fields return lists — aggregate or extract single values before output

View File

@ -0,0 +1,158 @@
# base CellValue 规范lark-base-cell-value
> 适用命令:`lark-cli base +record-upsert`、`lark-cli base +record-batch-create`、`lark-cli base +record-batch-update`
本文件定义 **shortcut 写记录**`CellValue` 的推荐格式,目标是让 AI 一次写对。不同命令的外层 JSON 形状不同,但每个 cell 都以本文为 source of truth。
## 1. 顶层规则(必须遵守)
- `--json` 必须是 JSON 对象。
- `+record-upsert`:顶层直接传字段映射:`{"字段名或字段ID": CellValue}`
- `+record-batch-create`:使用 `create_records`,其每个元素都是 `Map<FieldNameOrID, CellValue>`
- `+record-batch-update`:使用 `update_records`,其每个 value 都是 `Map<FieldNameOrID, CellValue>`
- 一次 payload 里同一字段只用一种 key字段名或字段 ID不要重复。
- 写入前先 `+field-list` 获取字段 `type/style/multiple`,再构造值。
- 需要清空字段时优先传 `null`(字段允许清空时)。
## 2. 各类型 CellValue
### 2.1 text
text 字段的 `style.type` 影响单元格检查逻辑:
`type=plain` 传 Markdown 格式的字符串。
`type=url` 传一个带 title 的 Markdown 格式链接,或单独传一个链接。
`type=phone` 传合法电话号码。
`type=email` 传合法邮箱字符串。
```json
{
"标题": "Hello, [lark-cli](https://github.com/larksuite/cli)",
"官网": "[官网](https://example.com)",
"联系电话": "1380000000000",
"邮箱": "owner@example.com"
}
```
### 2.2 number
用 JSON number不要用带单位或千分位的字符串。货币、百分比、进度、评分等数字类字段也按数字写入展示格式由字段配置决定。
```json
{
"工时": 12.5,
"预算": 3000,
"完成度": 0.65,
"评分": 4
}
```
### 2.3 select单选/多选)
`select` 字段用 `multiple` 区分单选和多选:`multiple=false` 时传选项名字符串,`multiple=true` 时传选项名数组。只支持写入字段中已有的选项;构造 CellValue 前先用 `+field-list``+field-search-options` 确认目标选项存在。
```json
{
"单选": "Todo",
"多选": ["后端", "高优"]
}
```
### 2.4 datetime
优先用 `YYYY-MM-DD HH:mm:ss` 字符串,这是最稳妥的写法,也和常见 API 输出更容易对齐。不要写相对时间(如“明天上午”)。
```json
{
"截止时间": "2026-03-24 10:00:00"
}
```
### 2.5 checkbox
用 JSON boolean`true``false`,不要用 `"true"``"是"``1`
```json
{
"已完成": true
}
```
### 2.6 user / group_chat
用对象数组,元素至少包含 `id`。人员字段传用户 ID`ou_xxx`),群字段传群 ID`oc_xxx`);单值/多值都统一使用数组。
> **人员字段:不要猜 ID。** 不知道 `open_id` 时,先用 `lark-contact` 查 id`lark-cli contact +search-user --query "<姓名/邮箱/手机号>" --as user`。
> **群组字段:不要猜 ID。** 不知道 `chat_id` 时,先用 `lark-im` 搜群:`lark-cli im +chat-search --query "<群名关键词>" --as user`;取结果里的 `oc_xxx`。
```json
{
"负责人": [
{ "id": "ou_xxx" },
{ "id": "ou_xxx2" }
],
"协作群": [
{ "id": "oc_xxx" }
]
}
```
### 2.7 link
用对象数组,元素包含 `id`,值为目标记录的 `record_id`。不要传记录标题;先用 `+record-list` / `+record-search` 找到目标记录 ID。
```json
{
"关联任务": [
{ "id": "<record_id>" }
]
}
```
### 2.8 location
写入对象必须使用 `{lng, lat}`,两者都是数字;`lng` 是经度,`lat` 是纬度。不需要手动传 `full_address`,平台会根据坐标解析地址。
```json
{
"坐标": {
"lng": 116.397428,
"lat": 39.90923
}
}
```
读取、筛选、转文本等场景使用 `full_address` 字符串;只有公式能访问坐标。如果用户只给地址文本,先获取或确认坐标后再写入;不要把仅有地址文本直接当作 location CellValue。
### 2.9 attachment不作为普通 CellValue 写入)
- 追加附件:使用 `lark-cli base +record-upload-attachment --record-id <record_id> --field-id <field_id> --file <path>`;可重复 `--file` 一次追加多个附件,不能用普通记录操作接口写附件值。
- 删除附件:使用 `lark-cli base +record-remove-attachment --record-id <record_id> --field-id <field_id> --file-token <file_token> --yes`;可重复 `--file-token` 一次删除同一单元格里的多个附件。
- 下载附件:使用 `lark-cli base +record-download-attachment --record-id <record_id> --file-token <file_token> --output <dir>`;不传 `--file-token` 时下载整行所有附件,也可重复 `--file-token` 只下载指定附件。Base 附件必须用这个命令下载,用其他下载入口可能失败。
## 3. 只读字段(不要写)
以下字段在写记录时应视为只读:
- `auto_number`
- `lookup`
- `formula`
- `created_at` / `updated_at`
- `created_by` / `updated_by`
写入只读字段通常不会更新数据;返回里可能出现 `ignored_fields`reason 会说明 `READONLY`。看到这种返回时,不要重试同一 payload应移除只读字段只写存储字段。
## 4. 完整示例
```json
{
"标题": "Created from shortcut",
"状态": "Todo",
"标签": ["高优", "外部依赖"],
"工时": 8,
"截止时间": "2026-03-24 10:00:00",
"已完成": false,
"负责人": [{ "id": "ou_123" }],
"关联任务": [{ "id": "rec_456" }],
"坐标": { "lng": 116.397428, "lat": 39.90923 }
}
```

View File

@ -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`
> - 文本组件(`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 <token>` | 是 | Base Token标识目标多维表格 |
| `--block-id <id>` | 是 | 图表 Block ID即目标组件的唯一标识 |
| `--format <fmt>` | 否 | 输出格式,遵循 CLI 全局输出格式规则 |
| `--dry-run` | 否 | 只预览 API 调用,不真正执行 |
> [!TIP]
> 这个命令**不需要** `--dashboard-id`。只要 `base_token + block_id` 即可定位并读取图表结果。
---
## 返回结构总览
CLI 成功输出使用标准 `{ok, identity, data}` 信封:
```json
{
"ok": true,
"identity": "user",
"data": {
"dimensions": [],
"measures": [],
"main_data": []
}
}
```
其中 `identity` 是本次调用实际使用的身份,`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> ...`
- 增强模板:`按 <维度字段> 统计,当前指标表示 <指标含义>。从结果看,<Top1维度值> 的值最高,为 <Top1值><Top2维度值> 和 <Top3维度值> 紧随其后。若按 Top N 看,前 <N> 项合计贡献了 ...;若看低值项,<低值维度值> 最低,为 <低值>。整体上,<一句总结>`
- 分组聚合图表:
- 基础模板:`按 <维度1> 统计,并以 <维度2> 分组,得到 <组合1>=<值1><组合2>=<值2> ...`
- 增强模板:`当前指标表示 <指标含义>。按 <维度1> 拆分后,不同 <维度2> 组之间存在明显差异:例如 <组合1> = <值1><组合2> = <值2>。如果按 <维度1> 汇总,<Top1维度1值> 总值最高,为 <汇总值>;如果看组内对比,<某组> 在 <某维度1值> 下表现最强 / 最弱。整体说明 <一句总结>`
- 词云:
- 基础模板:`按分词结果统计,当前指标表示 <指标含义>;其中 <词1>=<统计值1><词2>=<统计值2> ...`
- 增强模板:`当前词云反映的是“按词分组后的 <指标含义>”。从结果看,<Top1词> 的值最高,为 <值1>,说明它是当前最突出的关键词;<Top2词>、<Top3词> 处于第二梯队。如果按 Top N 看,主要关注词集中在 <主题A>、<主题B>;如果有多个词数值接近,可归为同一热点层级。整体上,这组词更适合用来总结 <主题/热点/关注点>`
- 指标卡:
- 基础模板:`当前主指标值为 <main_data>;当前周期值为 <comparison_data[0]>;对比周期值为 <comparison_data[1]>;趋势上 ...`
- 增强模板:`当前主指标表示 <指标含义>,主值为 <main_data>。若看周期比较,当前周期值为 <comparison_data[0]>,对比周期值为 <comparison_data[1]>,因此整体表现为 <上升/下降/持平>。若看趋势序列,最高点出现在 <日期>,值为 <峰值>;最低点出现在 <日期>,值为 <低值>;整体走势表现为 <持续增长/阶段波动/明显回落>。如果需要给出结论,可总结为:<一句总结>`
> [!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` 获取。
### 误区 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 模块总指引
- `+dashboard-block-get` — 获取 block 元数据
- [dashboard-block-data-config.md](dashboard-block-data-config.md) — data_config 结构和组件类型说明

View File

@ -0,0 +1,247 @@
# Dashboard仪表盘/数据看板)模块指引
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
Dashboard 是 Base 中的数据可视化看板,可以把表格数据变成**组件**(图表、指标卡等)进行展示。
## 核心概念
- **Dashboard仪表盘**:容器,包含多个组件
- **Block组件**:仪表盘中的单个可视化元素(柱状图、折线图、饼图、指标卡等)
- **data_config**:组件的数据源配置(表名、字段、分组等)
## 能力速览
| 你想做什么 | 用这些命令 | 关键文档 |
|------|-----------|---------|
| 创建/删除/改名称 | `+dashboard-create/delete/update` | 本页下方「仪表盘管理」 |
| 在仪表盘里添加组件 | `+dashboard-block-create` | 先定位 dashboard、表和字段再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 构造 `data_config` |
| 修改组件 | `+dashboard-block-update` | 先读 block 现状,再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 决定替换哪些顶层 key |
| 查看仪表盘有哪些组件 | `+dashboard-get``+dashboard-block-list` | 本页下方「查看仪表盘」 |
| 读取图表计算结果 | `+dashboard-block-get-data` | 返回图表最终数据协议;需要 block 元数据先用 `+dashboard-block-get` |
| 智能重排组件布局 | `+dashboard-arrange` | 用户明确要求重排,或本次会话新建仪表盘的收尾整理;无法指定精确位置 |
## 典型场景工作流
### 场景 1从 0 到 1 创建仪表盘
从 0 到 1 创建仪表盘时,按用户需求规划组件的类型和数量,并注意以下要点:
- 聚合方式:创建指标卡或分布图时优先把聚合写进 `data_config`,只有 Top N、字段取值探索、复杂筛选校验或 helper 汇总表场景才先用 `+data-query`
- Dry-run 边界:已按模板构造的简单指标卡、分布图、趋势图不需要逐个 `--dry-run` 后再真实创建;只有在调试 JSON、检查请求体、复杂自造 `data_config` 或处理 API validation 错误时才 dry-run。
- 验证方式:通过创建接口返回值确认创建成功与否,只在结果不确定时用 `+dashboard-get``+dashboard-block-list` 确认仪表盘和组件存在,或调用 `+dashboard-block-get-data`读取计算结果验证。
- 布局方式:`+dashboard-arrange` 仅两种情况使用:① 用户明确要求美化/重排;② 本次会话中从零新建的仪表盘,建完组件后做一次性布局整理。不是创建成功的必要步骤。
示例:搭建一个销售数据分析仪表盘
```bash
# 第 1 步:创建空白仪表盘
lark-cli base +dashboard-create --base-token xxx --name "销售数据分析"
# 记录返回的 dashboard_id
# 第 2 步:获取数据源信息
lark-cli base +table-list --base-token xxx
lark-cli base +field-list --base-token xxx --table-id <table_id>
# 第 3 步:规划应该创建哪些组件(根据用户需求确定组件类型和数量)
# 例如:总销售额(指标卡)、月度趋势(折线图)、品类占比(饼图)
# 第 4 步:顺序创建每个组件(必须串行执行,不能并发)
# 重要:创建组件前,先确定 dashboard_id、组件 name/type 和真实表字段
# 再阅读 dashboard-block-data-config.md 了解 data_config 结构、组件类型和 filter 规则
# 第 1 个组件
lark-cli base +dashboard-block-create \
--base-token xxx \
--dashboard-id blk_xxx \
--name "总销售额" \
--type statistics \
--data-config '{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}]}'
# 第 2 个组件(等上一个完成后再执行)
lark-cli base +dashboard-block-create \
--base-token xxx \
--dashboard-id blk_xxx \
--name "月度趋势" \
--type line \
--data-config '{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated"}]}'
# 继续创建其他组件...
# 第 5 步:组件创建完成后,使用 arrange 命令智能重排布局(可选但推荐)
# 默认布局可能不够美观arrange 会根据组件数量和类型自动优化布局
# 若用户没有要求美化/重排,可先跳过此步骤;这不影响仪表盘和组件是否已创建成功
lark-cli base +dashboard-arrange \
--base-token xxx \
--dashboard-id blk_xxx
```
### 场景 2在已有仪表盘上添加新组件
```bash
# 第 1 步:列出仪表盘,定位到当前仪表盘
lark-cli base +dashboard-list --base-token xxx
# 获取目标 dashboard_id
# 第 2 步:根据用户诉求规划组件类型和数据源
# 建议先查看当前仪表盘已有组件,避免重复创建,或作为参考
lark-cli base +dashboard-get --base-token xxx --dashboard-id blk_xxx
# 第 3 步:获取数据源信息
lark-cli base +table-list --base-token xxx
lark-cli base +field-list --base-token xxx --table-id <table_id>
# 第 4 步:顺序创建每个新组件(必须串行执行,不能并发)
# 重要:先确定 dashboard_id、组件 name/type 和真实表字段
# 再阅读 dashboard-block-data-config.md 了解 data_config 结构
lark-cli base +dashboard-block-create \
--base-token xxx \
--dashboard-id blk_xxx \
--name "新组件名" \
--type column \
--data-config '{...}'
```
### 场景 3编辑已有组件
> [!IMPORTANT]
> `+dashboard-block-update` **不能修改组件的 `type`**(图表类型),只能更新 `name` 和 `data_config`。
> 如需更换组件类型,必须先删除再重新创建。
```bash
# 第 1 步:列出仪表盘,定位到当前仪表盘
lark-cli base +dashboard-list --base-token xxx
# 第 2 步:列出组件,获取到目标组件
lark-cli base +dashboard-block-list --base-token xxx --dashboard-id blk_xxx
# 获取目标 block_id
# 提示:查看已有组件可作为参考,或检查是否重复创建相似组件
# 第 3 步:获取组件当前详情
lark-cli base +dashboard-block-get --base-token xxx --dashboard-id blk_xxx --block-id chtxxxxxxxx
# 第 4 步:根据用户编辑诉求准备更新
# 如果编辑诉求涉及数据源变更,需要先获取数据源信息
lark-cli base +table-list --base-token xxx
lark-cli base +field-list --base-token xxx --table-id <table_id>
# 第 5 步:执行更新
# 重要:先读取当前 block 的 name/type/data_config
# 再阅读 dashboard-block-data-config.md 了解 data_config 更新规则
lark-cli base +dashboard-block-update \
--base-token xxx \
--dashboard-id blk_xxx \
--block-id chtxxxxxxxx \
--data-config '{...}'
```
### 场景 4重排仪表盘布局
当用户明确要求对已有仪表盘进行布局重排或美化时使用(对本次会话从零新建的仪表盘,可在建完组件后直接做一次性整理,见场景 1
> [!CAUTION]
> - 排列结果是**服务端智能推荐**,不一定完全符合用户预期
> - 无法指定具体位置(如"第一排放 A第二排放 B"),排列逻辑是**自适应**的
> - **不建议**在已有仪表盘上自动调用,除非用户明确要求
```bash
# 第 1 步:列出仪表盘,定位到目标仪表盘
lark-cli base +dashboard-list --base-token xxx
# 第 2 步:执行智能重排
lark-cli base +dashboard-arrange \
--base-token xxx \
--dashboard-id blk_xxx
```
### 场景 5读取仪表盘或组件现状
**选择查询方式:**
- 想看仪表盘整体结构(含主题、所有组件名称和类型)→ 用 **方式 A**
- 只想快速查看有哪些组件 → 用 **方式 B**
- 想看某个组件的详细 data_config 配置 → 用 **方式 C**
- 想看某个图表/指标卡实际算出来的数据 → 用 **方式 D**
```bash
# 第 1 步:列出仪表盘,定位到当前仪表盘
lark-cli base +dashboard-list --base-token xxx
# 第 2 步:根据用户诉求查看详情
# 方式 A查看仪表盘整体情况包含所有组件列表
lark-cli base +dashboard-get --base-token xxx --dashboard-id blk_xxx
# 方式 B列出所有组件
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
# 最后:把获取到的现状信息整理好告诉用户
```
## 组件类型选择
组件 `type` 决定展示形式:
| 用户想看什么 | 选什么 type | 说明 |
|-------------|------------|------|
| 数据趋势(时间变化) | line | 折线图组件 |
| 类别比较(谁高谁低) | column | 柱状图组件 |
| 占比分布(各部分比例) | pie | 饼图组件 |
| 单个关键指标 | statistics | 指标卡组件 |
| 富文本说明/标题/注释 | text | 文本组件(支持 Markdown |
详细组件类型和 data_config 完整规则:[dashboard-block-data-config.md](dashboard-block-data-config.md)
## 常见问题
**Q: 创建组件的命令和 data_config 怎么写?**
A:
1. 先确定 `dashboard_id`、组件 `name`、组件 `type` 和真实表字段
2. 再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解:
- 全部组件类型的可复制模板
- filter 筛选条件格式
- 字段类型与操作符对应表
**Q: 为什么组件创建失败了?**
A: 常见原因:
- `table_name` 用了 table_id 而不是表名(必须用表名称,如「订单表」)
- `series``count_all` 同时存在(必须二选一,互斥)
- 字段名拼写错误(必须用 `+field-list` 获取的真实字段名,禁止猜测)
- 组件创建并发执行(必须串行,等上一个完成再执行下一个)
**Q: 可以一次创建多个组件吗?**
A: 不可以,必须串行执行。等上一个 `+dashboard-block-create` 完成后再执行下一个。
**Q: 组件的 `type` 创建后能改吗?**
A: 不能。`+dashboard-block-update` 只能修改 `name``data_config`,不能修改 `type`
**Q: 更新组件的命令和 data_config 怎么写?**
A:
1. 先读取当前 block确认 `block_id`、当前 `type` 和已有 `data_config`
2. 再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解 data_config 结构
**data_config 更新策略(顶层 key merge**
- 只传入需要修改的顶层字段(如 `series``filter`
- 未传的顶层字段(如 `group_by`)自动保留原值
- 但每个传入的字段内部是**全量替换**(如传新 `filter` 会完整覆盖旧 `filter`
**Q: 查看已有组件有什么用?**
A: 在「添加新组件」或「编辑组件」前查看已有组件可以:
- 了解当前仪表盘已有哪些可视化
- 避免重复创建相似的组件
- 参考已有组件的 data_config 结构作为模板
**Q: 我想直接拿图表算好的结果给 AI 分析,应该用什么?**
A: 用 `+dashboard-block-get-data`。它返回图表协议 JSON常见字段包括 `dimensions``measures``main_data`,指标卡可能还有 `comparison_data``trend_data`),不返回 block 名称、类型、布局或 `data_config`;需要这些元数据时先用 `+dashboard-block-get`
## 写入前检查
- 创建 block 前必须知道 `base_token``dashboard_id`、组件 `name/type``data_config`
- 更新 block 前必须知道 `base_token``dashboard_id``block_id`,并读过当前 block。
- `data_config` 中使用表名和字段名,不使用 table_id / field_id名称必须来自 `+table-list` / `+field-list` 的真实返回。

View File

@ -0,0 +1,210 @@
# Base data analysis SOP
Base 数据查询与分析任务的执行契约。覆盖记录读取、筛选、排序、Top/Bottom N、聚合统计、分组聚合、多表关联、临时分析和查询后写入前的目标定位。
本文只管查询选路和正确性边界;具体操作前先读真实结构和现状,复杂 JSON 再跳到 reference
- `+data-query`: entry guide [lark-base-data-query-guide.md](lark-base-data-query-guide.md), full DSL SSOT [lark-base-data-query.md](lark-base-data-query.md)
- 视图筛选: [lark-base-view-set-filter.md](lark-base-view-set-filter.md)
- 记录读取: `+record-list` / `+record-search` / `+record-get`,先确认字段 ID、字段名、分页和投影范围
## 0. Hard Rules
- 全局问题不能用默认 `+record-list --limit N` 片面地回答。
- `jq` / shell / 本地代码是在个人电脑或当前运行环境中处理已返回数据,只适合小范围结果;超过 200 行默认不推荐本地统计、排序或求极值,应改用 Base 云端查询服务的 filter/sort/aggregate。
- “最高、最低、最新、最早、Top、Bottom、总数、全部、异常、最大、最小、最多、最少、优先级最高”等全局语义必须在 Base 云端查询服务中完成筛选、排序或聚合。
- 一次性原始记录查询优先用 `+record-list` / `+record-search` 的 filter/sort聚合分析优先用 `+data-query`
- `+record-search` 用于关键词检索字段的展示文本;金额、状态、日期、空值、关联等结构化条件继续用 `--filter-json` 表达。
- 不要依赖已有视图,除非用户明确指定该视图,或你已读取并验证其 filter/sort/projection 符合当前问题。
- 交付输出必须使用用户可读的真实字段值;内部 ID、`record_id`、关联记录 ID、open_id、编码字段只可作为连接键或定位键不能替代最终输出除非用户明确要求输出这些键值。
- 每次读取必须做最小投影,并包含后续解释、回查或写入需要的业务 key。
## 1. Intent -> Tool Path
| 用户意图 | 首选路径 | 关键规则 |
| --- | --- | --- |
| 看几条、预览、示例 | `+record-list --limit N --field-id ...` | 保持局部语义;不要推广为全局结论 |
| 已知 `record_id` | `+record-get` | 直接读取;不要 search/list 反查 |
| 明确关键词 | `+record-search --keyword ... --search-field ... --field-id ...` | 必须显式指定 `--search-field`;可叠加 `--filter-json` |
| 按条件找原始记录 | `+record-list --filter-json ...` | `filter-json` 与视图筛选结构一致,支持文本、数字、日期、选项、人员、群组、关联等值 |
| 排序 / TopN 原始记录 | `+record-list --filter-json ... --sort-json ... --limit N` | 最高/最新用 `desc:true`,最低/最早用 `desc:false`;数组顺序表达优先级;最多 10 个排序条件 |
| 聚合 / 分组 / 分组排序 | `+data-query` | 使用 filters/dimensions/measures/sort/limit |
| 聚合后输出逐条记录 | `+data-query` 得到业务 key 或候选字段组合 -> `+record-list --filter-json` / `+record-get` 回查 | `+data-query` 维度行按字段组合去重且不返回 `record_id` |
| 多表 / 多跳关联 | 以候选数最小的事实表为驱动表,沿业务 key 或 link `record_id` 逐跳回查 | 读出 link 单元格里的关联 `record_id` 后,到被关联表批量 `+record-get` 展示字段 |
| 查询后写入 / 视图化 | 先用本 SOP 得到可复核的目标记录 id 集合 | 再进入记录写入或视图配置;高价值可复用查询可沉淀为持久视图 |
## 2. Execution Patterns
### 2.1 结构化原始记录与 TopN
使用 `+record-list` 的 filter/sort 路径:
1. `+field-list` 确认筛选字段、排序字段、展示字段、业务 key。
2. 筛选只用 `--filter-json``--filter-json @file`
3. 排序用 `--sort-json`
4. `--field-id` 做最小投影,`--limit` 控制返回数量。
Example: string/number 条件 + TopN
```bash
lark-cli base +record-list \
--base-token <base_token> \
--table-id <table_id> \
--filter-json '{"logic":"and","conditions":[["Title","==","Launch plan"],["Score",">=",80]]}' \
--sort-json '[{"field":"Updated","desc":true}]' \
--field-id Name \
--field-id Title \
--field-id Score \
--limit 20
```
Example: 复杂筛选从文件读取:
```bash
lark-cli base +record-list \
--base-token <base_token> \
--table-id <table_id> \
--filter-json @filter.json \
--sort-json '[{"field":"Priority","desc":true}]' \
--field-id Name \
--field-id Tags \
--limit 50
```
`filter-json` 与视图筛选结构一致。下面只列常用 fewshot字段类型、operator、value 形状拿不准或需要人员、群组、关联、空值、地理位置、formula / lookup 等完整筛选时,先读 [lark-base-view-set-filter.md](lark-base-view-set-filter.md),再把同样的 filter JSON 传给 `--filter-json`
文本 `==`:字段值等于目标文本。
```json
{"logic":"and","conditions":[["Title","==","Launch plan"]]}
```
文本包含 / like文本字段包含目标片段operator 写 `intersects`
```json
{"logic":"and","conditions":[["Title","intersects","urgent"]]}
```
数字 `==`:字段值等于目标数字。
```json
{"logic":"and","conditions":[["Score","==",95]]}
```
日期 `==`字段值等于目标日期datetime / created_at / updated_at 用 `ExactDate(...)`
```json
{"logic":"and","conditions":[["Due Date","==","ExactDate(2026-06-02)"]]}
```
选项 `==`:字段值匹配单个选项;选项值使用选项名数组,单个选项也写数组。
```json
{"logic":"and","conditions":[["Priority","==",["P0"]]]}
```
选项 `intersects`:字段值与给定选项集合有交集,常用于多选或“命中任一选项”。
```json
{"logic":"and","conditions":[["Tags","intersects",["P0","Blocked"]]]}
```
`--sort-json` 传排序数组,数组顺序就是优先级,`desc:true` 为降序,`desc:false` 为升序,最多 10 个排序条件。
### 2.2 关键词检索后叠加结构化条件
使用 `+record-search` 做关键词命中,结构化条件仍用 `--filter-json` 下推:
```bash
lark-cli base +record-search \
--base-token <base_token> \
--table-id <table_id> \
--keyword Alice \
--search-field Name \
--filter-json '{"logic":"and","conditions":[["Status","!=","Done"]]}' \
--sort-json '[{"field":"Updated","desc":true}]' \
--field-id Name \
--field-id Status \
--limit 20
```
不要把 `+record-search` 当成金额、状态、日期、空值、关联字段的结构化筛选入口;这些条件继续写成 `--filter-json`
### 2.3 聚合分析与 TopN
使用 `+data-query`
- 让 Base 云端查询服务完成 filters、dimensions、measures、sort、pagination.limit。
- `pagination.limit` 是 Base 云端查询服务中的结果限制,不是本地分页扫描。
- 常用聚合 fewshot 先读 [lark-base-data-query-guide.md](lark-base-data-query-guide.md);字段类型、日期 value、DSL shape 以 [lark-base-data-query.md](lark-base-data-query.md) 为准。
- `+data-query` 可返回聚合结果或维度字段行;维度字段行按字段组合去重且不返回 `record_id`,不能当逐条原始记录结果使用。
- 需要输出逐条记录、记录定位或完整行级字段时,先用 `+data-query` 得到业务 key、分组值或候选字段组合再用 `+record-list --filter-json` / `+record-get` 回查。
Example: 分组计数:
```bash
lark-cli base +data-query \
--base-token <base_token> \
--dsl '{"datasource":{"type":"table","table":{"tableId":"<table_id>"}},"dimensions":[{"field_name":"Status","alias":"status"}],"measures":[{"field_name":"Status","aggregation":"count","alias":"count"}],"shaper":{"format":"flat"}}'
```
Example: 过滤后汇总并取 TopN
```bash
lark-cli base +data-query \
--base-token <base_token> \
--dsl '{"datasource":{"type":"table","table":{"tableId":"<table_id>"}},"dimensions":[{"field_name":"Owner","alias":"owner"}],"measures":[{"field_name":"Amount","aggregation":"sum","alias":"total_amount"}],"filters":{"type":1,"conjunction":"and","conditions":[{"field_name":"Status","operator":"is","value":["Done"]}]},"sort":[{"field_name":"total_amount","order":"desc"}],"pagination":{"limit":10},"shaper":{"format":"flat"}}'
```
### 2.4 视图化与复用
一次性查询先用 `+record-list` / `+record-search` 的 filter/sort 验证。需要用户长期打开、共享或复用时,再把同一套 filter/sort 沉淀为视图。
Example: 将已验证的筛选排序写入视图:
```bash
lark-cli base +view-set-filter \
--base-token <base_token> \
--table-id <table_id> \
--view-id <view_id> \
--json @filter.json
lark-cli base +view-set-sort \
--base-token <base_token> \
--table-id <table_id> \
--view-id <view_id> \
--json '{"sort_config":[{"field":"Priority","desc":true}]}'
```
手动配置和视图配置的优先级:
1. `--filter-json` 覆盖 `--view-id` 保存的 view filter JSON。
2. `--sort-json` 覆盖 `--view-id` 保存的 view sort config。
3. 没有手动 filter/sort 时,`--view-id` 使用视图自身保存的 filter/sort。
### 2.5 关系查询与回查
- link 单元格通常是关联表 `record_id` 数组,不是用户可读内容,只是连接键。
- 先用 `+field-list` 确认 link 字段的 `link_table`、业务唯一键和展示字段。
- 从驱动表拿到候选记录后,用关联 `record_id` 到关联表 `+record-get` 批量读取记录内容。
- 多跳关系逐跳建立 `record_id/key -> 用户可读字段` 映射;最终用户可读的信息。
禁止:
- 把 link `record_id` 当最终输出。
-`+record-search` 搜 link `record_id`
- 基于 ID、自增编号、link 值做语义猜测;禁止依赖字段先验、样本记忆补全交付输出。
## 3. Range & Pagination Contract
- `+record-list` 默认页、固定 `--limit`、本地 `jq`、shell 管道、手工浏览输出,都只覆盖已读取范围;超过 200 行不要把本地处理当作推荐路径。
- `has_more=true`、存在下一页 offset/page token、或返回行数等于 page size都表示可能还有未读取数据。
- 对全局问题,只有 Base 云端查询服务已经通过 filter/sort/aggregate 收敛目标范围,或 `+data-query` 已在云端完成聚合、排序和限制时,才可以用有限返回形成结论。
- 必须全量导出时,按 `+record-list` 分页语义串行翻页;不要并发调用 `+record-list`
## 4. Final Answer Check
形成交付输出前必须能确认:
- 问题范围是局部样例、单点定位、全局原始记录、聚合分析、多表关联,还是查询后写入。
- 筛选、排序、聚合是否发生在 Base 云端查询服务中,而不是本地 `jq` / shell 中。
- 如果使用 `jq` / shell本地输入是否是 200 行以内的小范围结果;超过 200 行是否已改用 Base 云端查询服务查询。
- 如果使用 `+record-list` / `+record-search`,是否处理了 `has_more`,且投影包含业务 key 和解释字段。
- 如果涉及关系查询,是否按 `record_id` 或业务 key 精确回查,交付输出是否来自关联表真实字段。
- 交付输出能追溯到表、字段、筛选条件、排序/聚合条件和连接键。
任一项无法确认时,继续查询或明确说明只能得到局部结论。

View File

@ -0,0 +1,61 @@
# Base data-query guide
This guide is the entry point for `+data-query`. Use it for common aggregation fewshots and command selection. For the complete DSL fields, operators, limits, and response details, use [lark-base-data-query.md](lark-base-data-query.md) as the DSL SSOT.
Before using `+data-query`, also follow [lark-base-data-analysis-sop.md](lark-base-data-analysis-sop.md) to confirm that the task really needs aggregation instead of record listing or a temporary view.
## When to use
Use `+data-query` when the user asks for server-side:
- group by / aggregation
- sum, average, min, max, count, distinct count
- filtered aggregation
- sorted Top N or Bottom N
- global statistical conclusions
`+data-query` can return dimension field rows, but those rows are grouped by dimension values and do not include `record_id`. Use `+record-list`, `+record-search`, or `+record-get` for row-level output, record identity, or full raw record details.
## Common Fewshots
Count records by a category field:
```bash
lark-cli base +data-query \
--base-token <base_token> \
--dsl '{"datasource":{"type":"table","table":{"tableId":"<table_id>"}},"dimensions":[{"field_name":"Status","alias":"status"}],"measures":[{"field_name":"Status","aggregation":"count","alias":"count"}],"shaper":{"format":"flat"}}'
```
Sum a number field by category and return Top 10:
```bash
lark-cli base +data-query \
--base-token <base_token> \
--dsl '{"datasource":{"type":"table","table":{"tableId":"<table_id>"}},"dimensions":[{"field_name":"Region","alias":"region"}],"measures":[{"field_name":"Amount","aggregation":"sum","alias":"total_amount"}],"sort":[{"field_name":"total_amount","order":"desc"}],"pagination":{"limit":10},"shaper":{"format":"flat"}}'
```
Aggregate only records matching a filter:
```bash
lark-cli base +data-query \
--base-token <base_token> \
--dsl '{"datasource":{"type":"table","table":{"tableId":"<table_id>"}},"dimensions":[{"field_name":"Owner","alias":"owner"}],"measures":[{"field_name":"Amount","aggregation":"sum","alias":"total_amount"}],"filters":{"type":1,"conjunction":"and","conditions":[{"field_name":"Status","operator":"is","value":["Done"]}]},"shaper":{"format":"flat"}}'
```
Use `tableName` when the table ID is unavailable but the table name is known:
```bash
lark-cli base +data-query \
--base-token <base_token> \
--dsl '{"datasource":{"type":"table","table":{"tableName":"Orders"}},"measures":[{"field_name":"Amount","aggregation":"sum","alias":"total_amount"}],"shaper":{"format":"flat"}}'
```
## Routing to the DSL SSOT
Read [lark-base-data-query.md](lark-base-data-query.md) when you need:
- the full DSL field reference
- supported aggregations and field types
- filter operator details
- pagination and result limits
- response shape and error recovery

View File

@ -0,0 +1,454 @@
# Base data-query DSL SSOT
> **入口指南**: [lark-base-data-query-guide.md](lark-base-data-query-guide.md) | **前置条件**: 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
本文档是 `+data-query` JSON DSL 的单一事实来源SSOT用于说明完整字段、操作符、限制、返回和错误恢复。常用 fewshot 与命令选择先读 [lark-base-data-query-guide.md](lark-base-data-query-guide.md)。
查询类任务还必须先遵守 [`lark-base-data-analysis-sop.md`](lark-base-data-analysis-sop.md)。`+data-query` 适合让筛选、分组、聚合、排序和 TopN 在 Base 云端查询服务中执行;不要用默认分页的 `+record-list` 或本地 `jq` 替代聚合查询。
## 限制
- **权限要求**(按文档类型分流):
- **普通多维表格**:调用者拥有文档的**阅读权限**即可
- **高级权限多维表格**:调用者必须是文档管理员,拥有 **FAFull Access / 完全访问权限)**
权限不足时返回权限错误。
## 推荐命令
```bash
# 按字段分组计数
lark-cli base +data-query \
--base-token MAGObxxxxx \
--dsl '{
"datasource": {"type": "table", "table": {"tableId": "tblxxxxxxxx"}},
"dimensions": [{"field_name": "城市", "alias": "dim_city"}],
"measures": [{"field_name": "城市", "aggregation": "count", "alias": "count"}],
"shaper": {"format": "flat"}
}'
# 带过滤条件 + 排序 + 限制条数
lark-cli base +data-query \
--base-token MAGObxxxxx \
--dsl '{
"datasource": {"type": "table", "table": {"tableId": "tblxxxxxxxx"}},
"dimensions": [{"field_name": "城市", "alias": "dim_city"}],
"measures": [{"field_name": "金额", "aggregation": "sum", "alias": "total_amount"}],
"filters": {
"type": 1,
"conjunction": "and",
"conditions": [{"field_name": "城市", "operator": "isNot", "value": [""]}]
},
"sort": [{"field_name": "total_amount", "order": "desc"}],
"pagination": {"limit": 100},
"shaper": {"format": "flat"}
}'
# 使用 tableName表名代替 tableId
lark-cli base +data-query \
--base-token MAGObxxxxx \
--dsl '{
"datasource": {"type": "table", "table": {"tableName": "销售数据"}},
"measures": [{"field_name": "金额", "aggregation": "sum", "alias": "total"}],
"shaper": {"format": "flat"}
}'
# 聚合或维度查询后如需读取逐条记录,先让 data-query 返回可回查的业务 key
lark-cli base +data-query \
--base-token MAGObxxxxx \
--dsl '{
"datasource": {"type": "table", "table": {"tableId": "tblxxxxxxxx"}},
"dimensions": [{"field_name": "业务编号", "alias": "biz_key"}],
"measures": [{"field_name": "指标值", "aggregation": "max", "alias": "max_value"}],
"filters": {
"type": 1,
"conjunction": "and",
"conditions": [{"field_name": "状态", "operator": "is", "value": ["有效"]}]
},
"sort": [{"field_name": "max_value", "order": "desc"}],
"pagination": {"limit": 10},
"shaper": {"format": "flat"}
}'
```
## 参数
| 参数 | 必填 | 说明 |
|------------------------|------|------|
| `--base-token <token>` | 是 | Base Tokenbase_token |
| `--dsl <json>` | 是 | LiteQuery Protocol JSON DSL 查询语句 |
## 如何从链接中提取参数
用户通常会提供如下 URL
```
https://example.feishu.cn/base/<base_token>?table=<table_id>
```
- `--base-token`:取 `/base/` 后面的字符串
- DSL 中的 `tableId`:取 `table=` 后面的值
## API 入参详情
**HTTP 方法和路径:**
```
POST /open-apis/base/v3/bases/:base_token/data/query
```
**Path 参数:**
| 参数 | 必填 | 说明 |
|------|------|------|
| `base_token` | 是 | Base Token |
**Request Body — DSL 结构:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `datasource` | object | 是 | 数据源,包含 `type`(固定 `"table"`)和 `table` 对象 |
| `datasource.table.tableId` | string | 二选一 | 目标数据表 ID |
| `datasource.table.tableName` | string | 二选一 | 目标数据表名称 |
| `dimensions` | Dimension[] | 否* | 分组维度字段GROUP BY |
| `measures` | Measure[] | 否* | 聚合度量字段 |
| `filters` | FilterGroup | 否 | 过滤条件WHERE |
| `sort` | Sort[] | 否 | 排序规则 |
| `pagination` | object | 否 | 限制返回行数,`{limit: N}`,最大 5000 |
| `shaper` | object | 否 | 结果格式,固定 `{format: "flat"}` |
> \* `dimensions` 和 `measures` 至少填写一个。
**Dimension 字段:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `field_name` | string | 是 | 字段名称 |
| `alias` | string | 否 | 输出列别名,需全局唯一 |
**Measure 字段:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `field_name` | string | 是 | 字段名称 |
| `aggregation` | string | 是 | 聚合函数:`sum``avg``min``max``count``count_all``distinct_count` |
| `alias` | string | 否 | 输出列别名,需全局唯一 |
**聚合函数适用字段类型:**
| 聚合函数 | 适用字段类型 |
|----------|-------------|
| `sum` / `avg` | `number` |
| `min` / `max` | `number``datetime` |
| `count` | 全字段适用,计数非空值 |
| `count_all` | 全字段适用,计数所有行 |
| `distinct_count` | 全字段适用 |
> `number` 包含 `style.type` 为 `progress` / `currency` / `rating` 等所有子类型。
**FilterGroup**
```json
{
"filters": {
"type": 1,
"conjunction": "and",
"conditions": [
{"field_name": "城市", "operator": "is", "value": ["北京"]}
]
}
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `type` | int | 是 | 固定填 `1` |
| `conjunction` | string | 否 | 条件组合逻辑:`"and"``"or"`,默认 `"and"` |
| `conditions` | Condition[] | 否 | 条件列表 |
**Condition**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `field_name` | string | 是 | 字段名称(必须与表中字段名精确匹配) |
| `operator` | string | 是 | 运算符(见下方运算符表) |
| `value` | string[] | 是 | 条件值数组;`isEmpty`/`isNotEmpty` 时**必须**传空数组 `[]` |
**运算符:**
| 运算符 | 说明 |
|--------|------|
| `is` | 等于 |
| `isNot` | 不等于 |
| `contains` | 包含 |
| `doesNotContain` | 不包含 |
| `isEmpty` | 为空 |
| `isNotEmpty` | 不为空 |
| `isGreater` | 大于 |
| `isGreaterEqual` | 大于等于 |
| `isLess` | 小于 |
| `isLessEqual` | 小于等于 |
> 各运算符的适用字段类型见下方「按各字段类型筛选时 value 格式详解」。
**按各字段类型筛选时 value 格式详解:**
*`text`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `is` / `isNot` / `contains` / `doesNotContain` | `["文本内容"]` | 仅 1 个 | `["Hello"]` |
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
> **不支持** `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual`:文本无自然顺序,比较运算无意义。
> `text` 也覆盖电话、超链接、邮箱、条码字段;通过 `style.type` 区分(`plain`(默认)/ `phone` / `url` / `email` / `barcode`),运算符集合一致。
> 当 `style.type=url` 时value 筛选的是链接显示名称,而不是 URL 本身。
*`number`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `is` / `isNot` / `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual` | `["数字字符串"]` | 仅 1 个 | `["23.4"]``["-100"]` |
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
> value 必须为合法数字的字符串形式。
> `number` 也覆盖货币、进度、评分字段;通过 `style.type` 区分(`plain`(默认)/ `currency` / `progress` / `rating`),运算符集合一致,仅 value 解释不同:
> - 当 `style.type=progress` 时34% 对应 0.34 而不是 34。
> - 当 `style.type=rating` 时,必须输入整数,代表评分。
*`auto_number`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `is` / `isNot` / `contains` / `doesNotContain` | `["编号字符串"]` | 仅 1 个 | `["00001"]` |
| `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual` | `["编号字符串"]` | 仅 1 个 | `["00010"]` |
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
*`select`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `is` / `isNot` | `["选项名"]` | **仅 1 个** | `["选项A"]` |
| `contains` / `doesNotContain` | `["选项A", "选项B"]` | 可多个 | `["选项A", "选项B"]` |
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
> **不支持** `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual`:选项为枚举值,无自然顺序。
> 通过 `multiple` 区分单选(`multiple=false`,默认)/ 多选(`multiple=true`)。
*`user` / `created_by` / `updated_by`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------------------------|
| `is` / `isNot` | `["用户ID1", "用户ID2"]` | **可多个** | `["ou_aaa", "ou_bbb"]` |
| `contains` / `doesNotContain` | `["用户ID1", "用户ID2"]` | 可多个 | `["ou_aaa", "ou_bbb"]` |
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
> **不支持** `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual`:人员无法比大小。
> 用户 ID 使用 `open_id``ou_` 前缀),接口层会自动做 ID 转换。
*`group_chat`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `is` / `isNot` | `["群组ID1", "群组ID2"]` | 可多个 | `["oc_aaa", "oc_bbb"]` |
| `contains` / `doesNotContain` | `["群组ID1", "群组ID2"]` | 可多个 | `["oc_aaa", "oc_bbb"]` |
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
> **不支持** `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual`:群组无法比大小。
*`link`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `is` / `isNot` | `["recId1", "recId2"]` | 可多个 | `["recAAA", "recBBB"]` |
| `contains` / `doesNotContain` | `["recId1", "recId2"]` | 可多个 | `["recAAA", "recBBB"]` |
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
> **不支持** `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual`:关联记录无法比大小。
> value 传关联表记录的 `record_id`。
> 双向关联(创建时设 `bidirectional=true`)也属于 `link` 类型,运算符与单向关联一致。
*`location`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `is` / `isNot` / `contains` / `doesNotContain` | `["地址文本"]` | 仅 1 个 | `["北京市朝阳区..."]` |
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
> **不支持** `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual`:地理位置无自然顺序。
> location 按 `full_address` 字符串筛选,不支持经纬度空间筛选;查城市/片区时优先用 `contains`,避免用 `is` 匹配短地址词。
*`checkbox`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `is` | `["true"]``["false"]` | 仅 1 个 | `["true"]` |
> 仅支持 `is` 运算符,不支持其他运算符。
*`datetime` / `created_at` / `updated_at`*
日期字段仅支持 `is``isEmpty``isNotEmpty``isGreater``isLess` 五种运算符。
value 使用预定义关键字机制,第一个元素为字符串常量名称:
| 关键字 | 说明 | value 格式 | 支持的运算符 |
|--------|------|-----------|-------------|
| `ExactDate` | 精确日期 | `["ExactDate", "1773187200000"]`(毫秒时间戳) | `is``isGreater``isLess` |
| `Today` | 今天 | `["Today"]` | `is``isGreater``isLess` |
| `Tomorrow` | 明天 | `["Tomorrow"]` | `is``isGreater``isLess` |
| `Yesterday` | 昨天 | `["Yesterday"]` | `is``isGreater``isLess` |
| `CurrentWeek` | 本周 | `["CurrentWeek"]` | 仅 `is` |
| `LastWeek` | 上周 | `["LastWeek"]` | 仅 `is` |
| `CurrentMonth` | 本月 | `["CurrentMonth"]` | 仅 `is` |
| `LastMonth` | 上月 | `["LastMonth"]` | 仅 `is` |
| `TheLastWeek` | 过去七天 | `["TheLastWeek"]` | 仅 `is` |
| `TheNextWeek` | 未来七天 | `["TheNextWeek"]` | 仅 `is` |
| `TheLastMonth` | 过去三十天 | `["TheLastMonth"]` | 仅 `is` |
| `TheNextMonth` | 未来三十天 | `["TheNextMonth"]` | 仅 `is` |
> - **ExactDate 时区行为**:毫秒时间戳在实际筛选时会被转为**文档时区当天零点**,跨时区场景需注意日期可能偏移一天。
> - **范围型关键字**`CurrentWeek`、`LastWeek`、`CurrentMonth`、`LastMonth`、`TheLastWeek`、`TheNextWeek`、`TheLastMonth`、`TheNextMonth`)仅支持 `is` 运算符。
> - **关键字大小写敏感**`ExactDate`、`Today`、`CurrentWeek` 等首字母大写,写错大小写会导致校验失败。
*`attachment`*
| 运算符 | value 格式 | 元素个数 | 示例 |
|--------|-----------|---------|------|
| `isEmpty` / `isNotEmpty` | `[]` | 0 个 | `[]` |
> 附件字段仅支持 `isEmpty` 和 `isNotEmpty`,不支持其他运算符。
*`formula` / `lookup`*
公式和查找引用字段的运算符和 value 格式 **取决于其结果数据类型**,按结果类型参照上方对应字段类型的规则。例如:
- 公式结果为数字 → 按 `number` 规则
- 公式结果为日期 → 按 `datetime` 规则
- 公式结果为单选 → 按 `select` 规则
**Sort 字段:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `field_name` | string | 是 | 字段名称或 alias |
| `order` | string | 否 | `"asc"`(默认)或 `"desc"` |
**Pagination 字段:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `limit` | int | 否 | 返回记录数上限,必须为正整数,最大 5000不填时使用系统默认值。不支持 offset |
**Shaper 字段:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `format` | string | 是 | 固定为 `"flat"`,表示返回扁平化的对象数组 |
## CLI 出参详情
CLI 输出标准信封 `{ok, identity, data}`(失败时为 `{ok:false, identity, error}`)。
**成功时:**
```json
{"ok": true, "identity": "user", "data": {"main_data": [{"dim_city": {"value": "北京"}, "total_amount": {"value": 12345.00}}, ...]}}
```
**失败时:**
```json
{"ok": false, "identity": "user", "error": {"type": "api", "subtype": "unknown", "code": 800004006, "message": "...does not exist in table schema", "hint": "...", "log_id": "..."}}
```
**Response 字段:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `ok` | bool | 是否成功 |
| `identity` | string | 执行身份:`user` / `bot` |
| `data.main_data` | []object | 查询结果数组,每个元素为一行数据(成功时) |
| `error` | object | 失败时的 typed 错误,含 `type` / `subtype` / `code` / `message` / `hint` / `log_id` |
每行数据的字段值封装在 CellValue 中:
```json
{
"dim_city": {
"value": "北京"
},
"total_amount": {
"value": 12345.00
}
}
```
- `value`:展示值(人员名称、选项名称、格式化日期等)
## 返回值
命令成功后输出 `data` 字段的内容:
```json
{
"main_data": [
{
"dim_city": {"value": "直营"},
"measure_count": {"value": 1}
},
{
"dim_city": {"value": "加盟"},
"measure_count": {"value": 2}
}
]
}
```
## 工作流
1. 确认 base-token 和 table-id
2. **先查表结构**:执行 `lark-cli base +field-list --base-token <base_token> --table-id <table_id>`
3. 从返回的字段列表中获取 field_nameDSL 中使用的字段名称)
4. 根据字段信息构造 DSL JSON
5. 执行 +data-query
6. 解读返回结果:
- 结果在 `data.main_data` 数组中,每个元素代表一行
- 每行对象的 key 为 DSL 中指定的 `alias`;未指定 alias 时key 为自动生成的列名
- 每个 value 是 CellValue 对象,实际值在 `value` 字段中,如 `{"value": "北京"}``{"value": 12345.00}`
- 失败时结果在 `data.error` 中,包含具体错误码和信息
## 与记录读取组合
`+data-query` 可返回聚合结果,也可在只传 `dimensions` 时返回维度字段行;这些维度行按字段组合去重,不包含 `record_id`,不能等同于逐条原始记录。需要输出聚合结果对应的原始记录字段、展示值、记录定位信息或关联表字段时,按以下方式组合:
1.`+data-query` 在 Base 云端查询服务中完成全局筛选、分组、聚合、排序和 TopN得到业务 key、分组值或候选字段组合。
2. 如果已经拿到候选记录的 `record_id`,用 `+record-get` 读取逐条记录字段。
3. 如果拿到的是结构化业务 key例如编号、状态、日期、金额等`+record-list --filter-json` 做精确过滤后读取;不要用 `+record-search` 代替结构化条件。
4. 只有候选条件本身是文本展示值关键词时,才使用 `+record-search`,并用 `search_fields` 限定范围、`select_fields` 做投影。
5. 若候选记录包含 link 字段,提取关联 `record_id` 后到关联表用 `+record-get` 批量读取展示字段。
6. 最终回答业务字段,不要把内部 `record_id` 当作用户可读答案。
不要把 `data-query pagination.limit` 理解为分页扫描;它只限制 Base 云端查询服务返回的聚合结果行数,不支持 offset。需要全量原始记录导出时回到 data analysis SOP 的 `+record-list` 分页规则。
## 坑点
- ⚠️ **必须先查表结构**DSL 的 `field_name` 必须与表中字段名称精确匹配(区分大小写),不能凭猜测构造。先用 `lark-cli base +field-list --base-token <base_token> --table-id <table_id>` 获取真实字段名
- ⚠️ **权限要求按文档类型分流**:普通多维表格只需文档**阅读权限**;高级权限多维表格必须是文档管理员(**FA / Full Access**),否则返回权限错误
- ⚠️ **alias 不支持中文**dimensions 和 measures 的 alias 必须使用英文(如 `dim_city``total_amount`),中文 alias 会导致错误
- ⚠️ **API 路径是 `base/v3`**:本接口路径为 `/open-apis/base/v3/bases/:base_token/data/query`,不是 `bitable/v1`。两者完全不同,用错版本号会返回 `[2200] Internal Error`
- ⚠️ **`dimensions``measures` 至少填一个**:两个都不填会返回 DSL 校验错误
- ⚠️ **`shaper` 必须为 `{"format": "flat"}`**:不填或填其他值会导致结果格式不可预期,建议始终显式指定
- ⚠️ **数据表标识 `tableId` vs `tableName`**datasource 中可以用 `tableId`(如 `tblXXX`)或 `tableName`(数据表的用户自定义显示名称),二选一,不要混用
- ⚠️ **`pagination.limit` 最大 5000**:超过会报错,且不支持 offset只支持 limit
- ⚠️ **所有 alias 必须全局唯一**dimensions 和 measures 之间的 alias 也不能重名
- ⚠️ **不要用本地分页结果替代 data-query**:凡是全局计数、分组、聚合、排序 TopN优先让 `+data-query` 在 Base 云端查询服务中执行;默认页 `+record-list` 后本地统计只能得到已读取范围内的结果
## 参考
- [lark-base](../SKILL.md) — 多维表格全部命令
- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数
- [lark-base-data-analysis-sop.md](lark-base-data-analysis-sop.md) — 查询范围、选路、下推、分页、`+record-list` / `+record-search` 回查和关系查询 SOP
- [lark-base-cell-value.md](lark-base-cell-value.md) — CellValue 格式规范
- [lark-base-field-json.md](lark-base-field-json.md) — 字段类型与 JSON 结构

View File

@ -0,0 +1,109 @@
# base +field-create
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
创建一个字段。
## Agent 最小工作流
1. 先判断是不是 `formula` / `lookup`
2. 如果是:先读对应 guide。
3. 没读 guide 前,不要直接创建 formula / lookup 字段。
4. 读完 guide 后,再构造 `--json` 并创建字段。
5. 如果是跨表 formula / lookup再补查**目标表**的结构。
## 推荐命令
```bash
lark-cli base +field-create \
--base-token <base_token> \
--table-id <table_id> \
--json '{"name":"预算","type":"number","style":{"type":"plain","precision":2}}'
lark-cli base +field-create \
--base-token <base_token> \
--table-id <table_id> \
--json '{"name":"状态","type":"select","multiple":false,"default_value":["Todo"],"options":[{"name":"Todo","hue":"Blue","lightness":"Lighter"},{"name":"Done","hue":"Green","lightness":"Light"}]}'
lark-cli base +field-create \
--base-token <base_token> \
--table-id <table_id> \
--json '{"name":"负责人","type":"user","multiple":false,"default_value":[{"$slot":"current_user"}],"description":"用于标记记录的直接负责人;协作约定可参考[团队字段约定](https://example.com/field-spec)"}'
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--base-token <token>` | 是 | Base Token |
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
| `--json <body>` | 是 | 字段属性 JSON 对象 |
## API 入参详情
**HTTP 方法和路径:**
```
POST /open-apis/base/v3/bases/:base_token/tables/:table_id/fields
```
## JSON 值规范
- `--json` 必须是 **JSON 对象**,顶层直接传字段定义,不要再套一层。
- 顶层最少包含:`name``type`
- 所有字段类型都支持可选 `description`;支持纯文本,也支持 Markdown 链接,如 `协作约定可参考[团队字段约定](https://example.com/field-spec)`
- 需要字段默认值时传 `default_value`,直接使用字段对应 CellValue`datetime` / `user` 的动态填充用 `$slot`。完整规则见 [lark-base-field-json.md](lark-base-field-json.md)。
- `type` 不同,必填子字段不同:
- `select``multiple` 控制是否多选,`options` 定义静态选项,`dynamic_options_source` 定义动态选项来源。静态与动态选项配置二选一,不能同时传。
- `link`:必须有 `link_table`,可选 `bidirectional``bidirectional_link_field_name`
- `formula`:必须有 `expression`;先读 formula guide再创建。
- `lookup`:必须有 `from``select``where`;先读 lookup guide再创建。
**正确base +field-create**
```json
{
"name": "状态",
"type": "select",
"multiple": false,
"default_value": ["Todo"],
"options": [
{ "name": "Todo", "hue": "Blue", "lightness": "Lighter" },
{ "name": "Done", "hue": "Green", "lightness": "Light" }
]
}
```
**字段说明示例**
```json
{
"name": "负责人",
"type": "user",
"multiple": false,
"description": "用于标记记录的直接负责人;协作约定可参考[团队字段约定](https://example.com/field-spec)"
}
```
## 返回重点
- 返回 `field``created: true`
- 如果返回 `field_get_recommended:false``next_step:"done"`,表示本次是简单字段创建,通常不需要立刻执行 `+field-get`
- 如果返回 `field_get_recommended:true``next_step:"field_get"`,按 `verification_hint` 读回字段;`formula``lookup``link``auto_number` 等计算、关联或生成型字段更适合读回确认服务端最终结构。
## 工作流
1. formula / lookup 字段必须先阅读对应指南;没读之前不要直接创建。
2. 创建简单字段时,优先相信命令返回;只有用户要求精确核对额外属性,或返回建议读回时,才继续执行 `+field-get`
## 坑点
- ⚠️ 这是写入操作,执行前必须确认。
- ⚠️ 当 `type``formula``lookup` 时,先读对应 guide再创建。
- ⚠️ 不要把“每次创建后都 `+field-get`”当作固定流程;按返回里的 `field_get_recommended``next_step` 决定是否读回。
## 参考
- [lark-base-field-json.md](lark-base-field-json.md) — 字段 JSON 规范(推荐)
- [formula-field-guide.md](formula-field-guide.md) — formula 指南(创建公式必读)
- [lookup-field-guide.md](lookup-field-guide.md) — lookup 指南(创建查找引用必读)

View File

@ -0,0 +1,527 @@
# Base field JSON SSOT
> 适用命令:`lark-cli base +field-create`、`lark-cli base +field-update`
本文档定义 `+field-create` / `+field-update` 写字段时 `--json` 的推荐格式,是字段类型与字段 JSON 结构的 source of truth。目标不是复刻完整 schema而是让 agent 稳定产出正确 payload。
## 1. 顶层规则(必须遵守)
- `--json` 必须是 JSON 对象。
- 顶层统一使用:`type` + `name` + 类型特有字段。
- 所有字段类型都支持可选 `description`;支持纯文本,也支持 Markdown 链接。
- 字段默认值使用 `default_value`,直接传对应 CellValue支持范围只有 `text``number`、静态 `select``datetime``user`。清空默认值传 `null`;省略表示创建时不设置、更新时不修改。
- 不要使用旧结构:`field_name``property``ui_type`、数字枚举 `type`
- `+field-update` 使用同样的字段 JSON 结构,但语义是 `PUT`;这是高风险写入操作,建议先 `+field-get` 再按目标状态全量提交,并带 `--yes`
- `type=formula``type=lookup` 创建/更新前,必须先读对应 guide。
推荐示例:
```json
{
"type": "text",
"name": "需求背景",
"description": "记录需求背景与已知约束"
}
```
## 2. 字段速查
| 类型 | 最小必填字段 | 常见补充字段 |
|------|--------------|-------------|
| `text` | `type` `name` | `style.type` `default_value` |
| `number` | `type` `name` | `style` `default_value` |
| `select` | `type` `name` | `multiple` + `options` + 静态 `default_value`,或 `multiple` + `dynamic_options_source` |
| `datetime` | `type` `name` | `style.format` `default_value` |
| `created_at` / `updated_at` | `type` `name` | `style.format` |
| `user` / `group_chat` | `type` `name` | `multiple`;仅 `user` 支持 `default_value` |
| `created_by` / `updated_by` | `type` `name` | 无 |
| `link` | `type` `name` `link_table` | `bidirectional` `bidirectional_link_field_name` |
| `formula` | `type` `name` `expression` | 无 |
| `lookup` | `type` `name` `from` `select` `where` | `aggregate` |
| `auto_number` | `type` `name` | `style.rules` |
| `attachment` / `location` / `checkbox` | `type` `name` | 无 |
所有类型都可额外传 `description`;上表的“常见补充字段”只列类型特有配置。
## 3. 各类型写法
### 3.1 text
文本字段;电话、超链接、邮箱、条码也都属于 `text`,通过 `style.type` 区分。
支持 `default_value`:静态 Markdown 文本字符串;`phone` style 必须是合法电话号码;`url` style 传一个 Markdown 链接或裸 URL`email` style 必须是合法邮箱字符串,不要传 Markdown 链接或 `mailto:`
最小写法(默认 `style.type``plain`
```json
{
"type": "text",
"name": "标题",
"default_value": "默认标题"
}
```
常用写法:
默认值可以是 Markdown 文本
```json
{
"type": "text",
"name": "标题",
"description": "主标题字段",
"default_value": "未命名"
}
```
`style.type=phone` 时默认值是合法电话号码字符串。
```json
{
"type": "text",
"name": "联系电话",
"style": { "type": "phone" },
"default_value": "+8613800000000"
}
```
```json
{
"type": "text",
"name": "官网",
"style": { "type": "url" },
"default_value": "[官网](https://example.com)"
}
```
```json
{
"type": "text",
"name": "邮箱",
"style": { "type": "email" },
"default_value": "owner@example.com"
}
```
常用 `style.type``plain`(默认)、`phone``url``email``barcode`
### 3.2 number
数字字段;货币、进度、评分都属于 `number`,通过 `style.type` 区分。
支持 `default_value`:静态 JSON number所有 number style 都按这个规则写。
最小写法(默认 `style.type``plain`
```json
{
"type": "number",
"name": "工时",
"default_value": 8
}
```
`style` 是按 `type` 区分的对象;不同 `style.type` 的内部字段不一样,不要混传。
#### `plain`
支持字段:`precision``percentage``thousands_separator`
默认值 / 约束:
- `precision` 取值 `0..4`,默认 `2`
- `percentage` 默认 `false`
- `thousands_separator` 默认 `false`
```json
{
"type": "number",
"name": "工时",
"style": {
"type": "plain",
"precision": 2,
"percentage": false,
"thousands_separator": true
},
"default_value": 8
}
```
#### `currency`
支持字段:`precision``currency_code`
默认值 / 约束:
- `precision` 取值 `0..4`,默认 `2`
- `currency_code` 必填,如 `CNY``USD``EUR`
```json
{
"type": "number",
"name": "预算",
"style": { "type": "currency", "precision": 2, "currency_code": "CNY" }
}
```
#### `progress`
支持字段:`percentage``color`
默认值 / 约束:
- `percentage` 默认 `true`
- `color` 必填
- `color` 可用:`Blue``Purple``DarkGreen``Green``Cyan``Orange``Red``Gray``WhiteToBlueGradient``WhiteToPurpleGradient``WhiteToOrangeGradient``GreenToRedGradient``RedToGreenGradient``BlueToPinkGradient``PinkToBlueGradient``SpectralGradient`
```json
{
"type": "number",
"name": "完成度",
"style": { "type": "progress", "percentage": true, "color": "Blue" },
"default_value": 0.65
}
```
#### `rating`
支持字段:`icon``min``max`
默认值 / 已知平台范围:
- `icon` 默认 `star`
- `icon` 可用:`star``heart``thumbsup``fire``smile``lightning``flower``number`
- `min` 取值 `0..1`,默认 `1`
- `max` 默认 `5`;常见或已文档化的范围为 `1..10`,但 CLI 不强制上限为 `10`。如果用户明确需要更大评分范围,优先确认平台能力或用 `+field-create/update --dry-run` 检查请求形状;平台拒绝后再建议改用普通数字或进度字段。
```json
{
"type": "number",
"name": "评分",
"style": { "type": "rating", "icon": "star", "min": 1, "max": 5 }
}
```
### 3.3 select
单选和多选都使用 `select`;用 `multiple` 区分。`multiple` 默认 `false`。静态选项用 `options`,动态选项用 `dynamic_options_source`;两者不要同时传。
#### 静态选项
支持字段:`multiple``options`
支持 `default_value`:静态选项名数组;即使 `multiple=false` 也写数组,如 `["Todo"]`
默认值 / 约束:
- `multiple` 默认 `false`
- `options` 最多 `10000`
- `options[]` 结构是 `{name, hue?, lightness?}`
- `options[].name` 必填
- `options[].hue` 可用:`Red``Orange``Yellow``Lime``Green``Turquoise``Wathet``Blue``Carmine``Purple``Gray` 缺省值为 `Blue`
- `options[].lightness` 可用:`Lighter``Light``Standard``Dark``Darker` 缺省值为 `Lighter`
- 选项里没有 `id`,只有 `name`
- 支持 `default_value` 配置:填选项名数组。
```json
{
"type": "select",
"name": "状态",
"multiple": false,
"default_value": ["Todo"],
"options": [
{ "name": "Todo", "hue": "Blue", "lightness": "Lighter" },
{ "name": "Done", "hue": "Green", "lightness": "Light" }
]
}
```
#### 动态选项
支持字段:`multiple``dynamic_options_source`
动态选项不支持 `default_value`
默认值 / 约束:
- `multiple` 默认 `false`
- `dynamic_options_source` 结构是 `{table_id, field_id}`
- `dynamic_options_source.table_id` 填来源表 id 或表名
- `dynamic_options_source.field_id` 填来源字段 id 或字段名
- `dynamic_options_source` 仅创建支持;更新已有字段时不要传
- 引用选项条件 / 级联筛选条件:这个功能在 Base 前端支持,属于 UI-only 属性OpenAPI 里不支持CLI 不能读取、创建或更新;不要根据接口返回缺失判断未配置
- 动态选项不支持配置 `default_value`
```json
{
"type": "select",
"name": "动态状态",
"multiple": false,
"dynamic_options_source": {
"table_id": "选项表",
"field_id": "候选状态"
}
}
```
### 3.4 datetime
手动填写的日期/时间字段。系统时间用 `created_at` / `updated_at`
支持 `default_value`:静态时间字符串,或 `{ "$slot": "record_created_time" }``datetime + record_created_time` 是自动填充可编辑单元格;`created_at` 是只读创建时间元信息。
最小写法:
```json
{
"type": "datetime",
"name": "截止时间",
"default_value": "2026-03-24 10:00:00"
}
```
支持字段:`style.format`
默认值 / 约束:
- `style.format` 默认 `yyyy/MM/dd` 可用格式:`yyyy/MM/dd``yyyy/MM/dd HH:mm``yyyy/MM/dd HH:mm Z``yyyy-MM-dd``yyyy-MM-dd HH:mm``yyyy-MM-dd HH:mm Z``MM-dd``MM/dd/yyyy``dd/MM/yyyy`
- `style.format` 只控制前端显示格式;当前可配置格式最多显示到分钟,底层时间值仍可保留秒级精度。
常用写法:
```json
{
"type": "datetime",
"name": "截止时间",
"style": { "format": "yyyy-MM-dd HH:mm" },
"default_value": { "$slot": "record_created_time" }
}
```
### 3.5 created_at / updated_at
系统创建时间 / 系统更新时间字段;可配显示格式,但记录写入时应视为只读。
支持字段:`style.format`
默认值 / 约束:
- `style.format` 默认 `yyyy/MM/dd`
- 可用格式:`yyyy/MM/dd``yyyy/MM/dd HH:mm``yyyy/MM/dd HH:mm Z``yyyy-MM-dd``yyyy-MM-dd HH:mm``yyyy-MM-dd HH:mm Z``MM-dd``MM/dd/yyyy``dd/MM/yyyy`
```json
{ "type": "created_at", "name": "创建时间" }
```
```json
{ "type": "updated_at", "name": "更新时间", "style": { "format": "yyyy/MM/dd HH:mm" } }
```
### 3.6 user / group_chat
人员字段和群字段都支持 `multiple`
`user` 支持 `default_value`:人员 CellValue 数组,元素可用 `{ "id": "ou_xxx" }``{ "$slot": "current_user" }`;不要猜用户 ID。`group_chat` 不支持默认值。
默认值 / 约束:
- `multiple` 默认 `true`
- `user` 字段支持 `default_value` 配置,`group_chat` 字段不支持 `default_value` 配置。
```json
{
"type": "user",
"name": "负责人",
"multiple": true,
"default_value": [{ "$slot": "current_user" }, { "id": "ou_xxx" }]
}
```
```json
{ "type": "group_chat", "name": "负责群", "multiple": true }
```
### 3.7 created_by / updated_by
系统创建人 / 系统修改人字段;记录写入时应视为只读。
```json
{ "type": "created_by", "name": "创建人" }
```
```json
{ "type": "updated_by", "name": "更新人" }
```
### 3.8 link
关联字段;`link_table` 必填。
支持字段:`link_table``bidirectional``bidirectional_link_field_name`
默认值 / 约束:
- `link_table` 必填
- `link` 字段的单元格表示“当前记录关联到的对侧表记录集合”
- `bidirectional` 默认 `false`
- `bidirectional=true` 时,会在被关联表自动创建一个反向关联字段。任一侧记录的关联关系发生变更时,另一侧对应记录会自动同步更新
- `bidirectional_link_field_name` 仅在 `bidirectional=true` 时使用
- 关联字段筛选:这个功能在 Base 前端支持,属于 UI-only 属性OpenAPI 里不支持CLI 不能读取、创建或更新;不要根据接口返回缺失判断未配置
```json
{
"type": "link",
"name": "关联任务",
"link_table": "任务表"
}
```
双向关联:
```json
{
"type": "link",
"name": "关联任务",
"link_table": "任务表",
"bidirectional": true,
"bidirectional_link_field_name": "反向关联"
}
```
更新时注意:
- `link` 不允许转换为其他类型,其他类型也不能转换为 `link`
- 现有 `link` 字段的 `bidirectional` 不能改。
### 3.9 formula
公式字段;`expression` 必填。创建/更新前先读 [formula-field-guide.md](formula-field-guide.md) 学习公式语法。
```json
{
"type": "formula",
"name": "合计",
"expression": "1+1"
}
```
### 3.10 lookup
查找引用字段;`from``select``where` 必填,`aggregate` 可选。创建/更新前先读 [lookup-field-guide.md](lookup-field-guide.md)。
支持字段:`from``select``where``aggregate`
默认值 / 约束:
- `from``select``where` 必填
- `aggregate` 默认 `raw_value` 代表不进行聚合,直接返回 select 回的原始值
- `aggregate` 可用:`raw_value``sum``average``counta``unique_counta``max``min``unique`
- `where.logic` 默认 `and`,仅支持 `and` / `or`
- `where.conditions` 至少 1 条
- `conditions` 每项是三元组 `[field, op, value?]`
```json
{
"type": "lookup",
"name": "状态汇总",
"from": "任务表",
"select": "状态",
"where": {
"logic": "and",
"conditions": [
["负责人", "==", { "type": "field_ref", "field": "当前负责人" }],
["状态", "non_empty", null]
]
},
"aggregate": "raw_value"
}
```
### 3.11 auto_number
自动编号字段;创建时不写 `style.rules` 会使用默认规则:`NO.001`。更新已有自动编号字段时应显式提交目标 `style.rules`,因为 `+field-update` 会把新的编号规则重新应用到已有编号。
最小写法:
```json
{
"type": "auto_number",
"name": "编号"
}
```
支持字段:`style.rules`
默认值 / 约束:
- `style.rules` 是规则数组,数量 `1..9`
- 默认规则:
```json
{
"style": {
"rules": [
{ "type": "text", "text": "NO." },
{ "type": "incremental_number", "length": 3 }
]
}
}
```
#### `text`
支持字段:`text`
```json
{ "type": "text", "text": "TASK-" }
```
#### `incremental_number`
支持字段:`length`
默认值 / 约束:
- `length` 取值 `1..9`
```json
{ "type": "incremental_number", "length": 4 }
```
#### `created_time`
支持字段:`date_format`
默认值 / 约束:
- `date_format` 可用:`yyyyMMdd``yyyyMM``yyMM``MMdd``yyyy``MM``dd`
```json
{ "type": "created_time", "date_format": "yyyyMMdd" }
```
自定义规则:
```json
{
"type": "auto_number",
"name": "编号",
"style": {
"rules": [
{ "type": "text", "text": "TASK-" },
{ "type": "created_time", "date_format": "yyyyMMdd" },
{ "type": "incremental_number", "length": 4 }
]
}
}
```
### 3.12 attachment / location / checkbox
```json
{ "type": "attachment", "name": "附件" }
```
```json
{ "type": "location", "name": "位置" }
```
写入必须使用 `{lng,lat}`。location 读回会包含 `full_address`;筛选和 `location -> text` 类型转换按 `full_address` 字符串处理,只有公式能访问坐标。
```json
{ "type": "checkbox", "name": "完成" }
```
## 4. 创建与更新
- `+field-create`:按目标字段配置直接构造 `--json`
- `+field-update`:使用同样的 JSON 结构,但语义是 `PUT`;建议先 `+field-get`,再按目标完整状态提交,并带 `--yes`。当 `type``auto_number` 时,更新编号规则本身就会把新规则应用到已有编号,无需额外参数,也不要在 JSON 里塞额外的底层实现参数。
## 5. 暂不支持字段
Object对象字段、Button按钮字段、Stage流程字段暂时都没有被 CLI 支持。这些字段会展示为 `not_support` 字段并被保护:不允许修改,不允许读取内容。
## 6. 易错点
- `select` 只有一个类型;不要写 `single_select` / `multi_select`,用 `multiple` 控制是否多选。
- `number` 的精度、货币、进度、评分配置都放在 `style` 下,不要写顶层 `precision`
- `datetime` 是手动日期字段;系统时间请改用 `created_at` / `updated_at`
- `formula` / `lookup` 没读 guide 前不要直接写。
- 只有 `text``number`、静态 `select``datetime``user` 支持 `default_value`;清空统一传 `"default_value": null`。其他字段类型不要配置默认值。

View File

@ -0,0 +1,189 @@
# base +field-update
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
更新一个已有字段。
## 推荐命令
```bash
lark-cli base +field-update \
--base-token <base_token> \
--table-id <table_id> \
--field-id <field_id> \
--json '{"name":"状态","type":"select","multiple":false,"default_value":["Doing"],"options":[{"name":"Todo","hue":"Blue","lightness":"Lighter"},{"name":"Doing","hue":"Orange","lightness":"Light"},{"name":"Done","hue":"Green","lightness":"Light"}]}' \
--yes
lark-cli base +field-update \
--base-token <base_token> \
--table-id <table_id> \
--field-id <field_id> \
--json '{"name":"负责人","type":"user","multiple":false,"default_value":null,"description":"用于标记记录的直接负责人"}' \
--yes
lark-cli base +field-update \
--base-token <base_token> \
--table-id <table_id> \
--field-id <field_id> \
--json '{"name":"编号","type":"auto_number","style":{"rules":[{"type":"text","text":"TASK-"},{"type":"created_time","date_format":"yyyyMM"},{"type":"text","text":"-"},{"type":"incremental_number","length":4}]}}' \
--yes
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--base-token <token>` | 是 | Base Token |
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
| `--field-id <id_or_name>` | 是 | 字段 ID 或字段名 |
| `--json <body>` | 是 | 字段属性 JSON 对象 |
| `--yes` | 是 | 确认执行高风险字段更新 |
> 这是**高风险写入操作**。`+field-update` 使用 `PUT` 全量字段定义语义改变字段类型或关键配置可能影响整列已有数据的解释、展示或可用性。CLI 层要求显式传 `--yes`;如果用户已经明确目标和期望更新,可直接执行并带上 `--yes`。
## API 入参详情
**HTTP 方法和路径:**
```
PUT /open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id
```
`--json.type``auto_number` 时,仍然走同一个 v3 字段更新接口:更新自动编号规则后,接口现状就会把新规则应用到已有编号(这是接口默认行为,只是 agent 通常不知道),因此**不需要**任何额外开关或参数。只需要正常提交目标自动编号字段定义即可;如果用户要求“将修改用于已有编号”,直接执行这次 `+field-update` 就能达到效果,不要在 `--json` 里额外添加任何参数去“触发”重排。
## JSON 值规范
- `--json` 必须是 **JSON 对象**,顶层直接传字段定义。
- 更新语义是 `PUT`(全量字段配置更新),不要只传零散片段;至少显式包含 `name``type`,并补齐该类型所需关键配置。
- 所有字段类型都支持可选 `description`;支持纯文本,也支持 Markdown 链接。
- 需要字段默认值时传 `default_value`,直接使用字段对应 CellValue`null` 清空,省略表示不修改现有默认值。完整规则见 [lark-base-field-json.md](lark-base-field-json.md)。
- `select` 更新时:`options` 仍按对象数组传,避免混入无效字段。
- `link` 更新限制:
- 不能把非 `link` 字段改成 `link`,也不能把 `link` 改成非 `link`
- 现有 `link` 字段的 `bidirectional` 不能改。
- `auto_number` 更新的 `style.rules` 支持 `text``created_time``incremental_number`
**推荐更新示例**
```json
{
"name": "状态",
"type": "select",
"multiple": false,
"default_value": ["Doing"],
"options": [
{ "name": "Todo", "hue": "Blue", "lightness": "Lighter" },
{ "name": "Doing", "hue": "Orange", "lightness": "Light" },
{ "name": "Done", "hue": "Green", "lightness": "Light" }
]
}
```
**字段说明示例**
```json
{
"name": "负责人",
"type": "user",
"multiple": false,
"description": "用于标记记录的直接负责人"
}
```
## 返回重点
- 返回 `field``updated: true`
- `updated:true` 只表示更新请求成功,不表示字段结构、已有记录值或下游能力已经完成验证。`+field-update` 无法知道更新前的字段类型,因此成功响应会推荐执行 `+field-get`;若发生类型转换,还要抽样读取记录值。
- 如果响应中的 `field.type` 与提交的 `type` 不一致,必须把它当作待核验的类型不匹配;不能返回完成态,也不能只根据其中任一类型推断更新成功。
- 如果 API 报告本次更新没有产生任何变更no-op命令会如实返回该错误这通常说明目标字段已是期望状态不要机械重试同一份 `+field-update`。需要确认当前字段完整状态时执行 `+field-get`
- 如果返回 `field_get_recommended:true``next_step:"field_get"`,按提示读回字段;`auto_number` 更新后还应抽样读记录值确认编号已按新规则生成。
## 工作流
1. 建议先用 `+field-get` 拉现状,再做最小化修改。
2. `formula/lookup` 类型更新前先阅读对应指南。
3. 如果更新 `auto_number`,理解为“更新编号规则,同时把新规则应用到已有编号”;执行后按返回提示读回字段并在必要时抽样记录值。
4. 如果这次更新会改变字段 `type` 先按下方“字段类型变更规则”判断能否执行。如果不修改 `type`,大多数场景都相对安全。
## 字段类型变更规则
字段类型变更采用白名单机制:**只允许白名单转换**;未命中白名单时,**不建议用 CLI 转换字段类型** 除非用户明确知道风险并同意。
### 允许直接转换 type
`+field-get` / `+field-list` 看结构,再抽样读值;只有命中以下规则时,转换才是比较安全的。
#### 相对安全
| 目标类型 | 允许的源类型 | 说明 |
|------|------|------|
| `text` | `number``select``datetime``created_at``updated_at``location`(只保留 `full_address`)、`auto_number``checkbox` | 保留字符串表示;丢失原类型语义和结构化能力 |
| `number` | `text``number``datetime``created_at``updated_at``checkbox` | 保留可解析的数字值;无法解析的值会变空,原文本格式会丢失 |
| `datetime` | `text``number``datetime``created_at``updated_at` | 保留可解析的时间字符串和时间戳;无法解析的值会变空,原文本格式会丢失 |
| `select` | `text -> select``number -> select``single select -> multi select` | 只有完全匹配目标选项名的值会转成对应选项;没匹配上的值会被丢弃 |
#### 可执行但会截断 / 重算
- `select(multi) -> select(single)`: 只保留第一个值,其余值会被丢弃。
- `user(multi) -> user(single)`: 只保留第一个人员,其余值会被丢弃。
- `group_chat(multi) -> group_chat(single)`: 只保留第一个群,其余值会被丢弃。
#### 无状态字段可直接转换
- `created_at``created_by``updated_at``updated_by``formula``lookup`: 这类字段值由系统或计算逻辑生成,不承载独立存储数据;可以执行类型转换,不必担心破坏原始记录值,但仍要做下游读回验证。
### 一律不要用 CLI 转换
以下场景全部视为黑名单;默认要求用户改到 Web 页面手动完成,或改走“新建字段 + 数据迁移”。
- `any -> checkbox`
- `any -> user`
- `any -> group_chat`
- `any -> attachment`
- `any -> location`
- `link` 类型变更
- 任意涉及动态 / 静态选项来源切换的 `select` 类型变更
### 可例外继续执行的场景
只有在**整列数据丢失可接受**时,才允许对黑名单场景例外执行。
- `EmptyColumn`: 该列为空
- `FreshTableInit`: 新建空表初始化
- `PrimaryFieldBootstrap`: 主列不能删,只能更新完成初始化
- `ExplicitLossAccepted`: 用户明确接受整列数据丢失
不满足以上条件时,不要转换。
### 非白名单场景如何处理
- 命中白名单时:建议直接原地转换,再做读回验证。
- 未命中白名单时:先询问用户是否仍要执行转换,并明确说明风险:
- 无状态字段除外;这类字段可以直接转换
- 可能整列变空
- 可能只保留第一个值
- 可能只保留字符串表示,丢失原类型语义和结构化能力
- 可能影响视图 / 筛选 / 排序 / 公式 / lookup / 写入引用
- 如果用户不接受风险:不要执行转换。
### 完成态验证
- `FieldReadback`: 读回字段结构,确认 `type` / `multiple` / `style` / `options`
- `NoopReadback`: `+field-update` 返回 no-op 错误时,只能说明 API 报告没有产生变更;可以跳过重复 update但不能替代 `FieldReadback`
- `ValueReadback`: 抽样读回转换后的单元格值
- `DownstreamReadback`: 若涉及看板 / 分组 / 排序 / lookup / 公式,继续读回结果
- `CompletionRule`: 结构、值、下游能力都正确,才能回复“已完成”
## 坑点
- ⚠️ 这是全量字段属性更新语义,不是 patch。
- ⚠️ 这是高风险写入操作,执行时必须带 `--yes`
- ⚠️ 当 `type``formula``lookup` 时,先阅读对应指南再执行。
## 参考
- 更新前读取当前字段,确认现有 `type` 和具体配置细节,再决定是原地更新还是新建字段迁移。
- [lark-base-field-json.md](lark-base-field-json.md) — 字段 JSON 规范(推荐)
- [formula-field-guide.md](formula-field-guide.md) — formula 指南(更新公式前必读)
- [lookup-field-guide.md](lookup-field-guide.md) — lookup 指南(更新查找引用前必读)

View File

@ -0,0 +1,71 @@
# base +form-detail
通过表单分享 token 读取表单详情。只读操作,适合在提交表单前解析题目结构、必填项、显示条件和附件提交所需的 Base token。
## 何时使用
- 用户给出 `/share/base/form/{shareToken}` 表单分享链接,先提取最后一段作为 `--share-token`
- 准备调用 `+form-submit` 前,必须先用 `+form-detail` 读取 `questions[]`
- 只知道分享链接、还不知道 `base-token` / `table-id` / `form-id` 时,用 `+form-detail`;已在 Base 内部管理表单时,才用 `+form-get`
```bash
lark-cli base +form-detail --share-token <share_token> --format pretty
```
## 读取重点
`+form-detail` 返回的关键字段:
| 字段 | 用途 |
|---|---|
| `base_token` | 表单所属 Base提交附件时必须传给 `+form-submit --base-token` |
| `questions[].id` | 题目标识,通常对应字段 ID |
| `questions[].title` | 提交时使用的字段名/题目名,以真实返回为准 |
| `questions[].type` | 决定值格式;与字段类型和 `lark-base-cell-value.md` 对齐 |
| `questions[].required` | 判断必填项 |
| `questions[].filter` | 判断题目是否对当前提交可见;被隐藏的问题不要填写 |
题目除固定字段外,会按类型携带动态配置,例如 `select.options` / `select.multiple``number.style``datetime.style.format``user.multiple``link.link_table``formula.expression``lookup.from/select/where/aggregate`。提交前按返回结构构造值,不要猜题目类型或选项。
## filter 显示条件
`questions[].filter` 控制题目显示/隐藏:
```json
{
"conjunction": "and",
"conditions": [
{"field_name": "是否携带家属", "operator": "is", "value": ["是"]},
{"field_name": "参与人数", "operator": "isGreater", "value": [1]}
]
}
```
- `conjunction``and` / `or`,表示条件全部满足或任一满足。
- `conditions[].field_name` 引用其他题目的 `title`
- `conditions[].operator` 常见为 `is``isNot``contains``doesNotContain``isEmpty``isNotEmpty``isGreater``isGreaterEqual``isLess``isLessEqual`
- `isEmpty` / `isNotEmpty` 不需要 `value`
- 附件题目的 filter 只适合 `isEmpty` / `isNotEmpty`
如果当前已填写值不满足某题目的 `filter`,该题目视为隐藏,不应放入 `+form-submit --json.fields``--json.attachments`
## 与 form-submit 的关系
提交普通字段:
```bash
lark-cli base +form-submit \
--share-token <share_token> \
--json '{"fields":{"姓名":"张三","评分":5}}'
```
提交附件字段:
```bash
lark-cli base +form-submit \
--share-token <share_token> \
--base-token <base_token_from_form_detail> \
--json '{"fields":{"姓名":"张三"},"attachments":{"附件":["./report.pdf"]}}'
```
附件字段不要写进 `fields`;放在顶层 `attachments`,值为本地文件路径数组。

View File

@ -0,0 +1,118 @@
# base +form-questions-create
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
向多维表格表单/问卷中批量添加问题。
## 命令
```bash
# 添加一个文本必填问题
lark-cli base +form-questions-create \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[{"type":"text","title":"您的姓名是?","required":true}]'
# 添加多个问题(按顺序排列)
lark-cli base +form-questions-create \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[
{"type":"text","title":"您的姓名是?","required":true},
{"type":"text","title":"您的联系方式是?","required":false}
]'
# 添加单选题(带选项)
lark-cli base +form-questions-create \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[{"type":"select","title":"满意度评价","required":true,"multiple":false,"options":[{"name":"非常满意","hue":"Green"},{"name":"满意","hue":"Blue"},{"name":"一般","hue":"Yellow"}]}]'
# 添加评分题
lark-cli base +form-questions-create \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[{"type":"number","title":"服务评分","style":{"type":"rating","icon":"star","min":1,"max":5}}]'
# 添加带描述的问题(纯文本)
lark-cli base +form-questions-create \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[{"type":"text","title":"您的姓名","description":"请填写真实姓名"}]'
# 添加带描述的问题(含链接)
lark-cli base +form-questions-create \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[{"type":"text","title":"反馈建议","description":"更多详情请查看[帮助文档](https://example.com/help)"}]'
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--base-token <token>` | 是 | Base Tokenbase_token |
| `--table-id <id>` | 是 | 数据表 ID |
| `--form-id <id>` | 是 | 表单 ID |
| `--questions <json>` | 是 | 问题 JSON 数组,最多 10 个(见下方格式) |
| `--format` | 否 | 输出格式json默认\| pretty \| table \| ndjson \| csv |
| `--as` | 否 | 身份user默认\| bot |
| `--dry-run` | 否 | 预览 API 调用,不执行 |
## `--questions` 格式
每个问题对象支持以下字段:
| 字段 | 必填 | 说明 |
|-----------------------|------|------|
| `title` | **是** | 问题标题(字段名) |
| `type` | **是** | 题目类型:`text``number``select``datetime``user``attachment``location` |
| `description` | 否 | 问题描述(纯文本或 Markdown 链接,如 `[文本](https://example.com)` |
| `required` | 否 | 是否必填true/false |
| `option_display_mode` | 否 | 选项展示方式(仅 `select` 有效):`0`=下拉,`1`=纵向(默认),`2`=横向 |
| `multiple` | 否 | 是否多选(`select`/`user` 类型有效bool |
| `options` | 否 | 选项列表(仅 `select` 有效):`[{"name":"选项1","hue":"Blue"}]`hue 可选:`Red`/`Orange`/`Yellow`/`Green`/`Blue`/`Purple`/`Gray` |
| `style` | 否 | 字段样式配置(见下方说明) |
### `style` 字段说明
| 类型 | style 结构 | 说明 |
|------|------|------|
| `text` | `{"type":"plain"}` | 当前仅支持 `plain` |
| `number` | `{"type":"plain","precision":2}` | precision 为小数位数 |
| `number`(评分) | `{"type":"rating","icon":"star","min":1,"max":5}` | icon 可选:`star`/`heart`/`thumbsup`/`fire`/`smile`/`lightning`/`flower`/`number` |
| `datetime` | `{"format":"yyyy/MM/dd"}` | format 可选:`yyyy/MM/dd``yyyy/MM/dd HH:mm``MM-dd``MM/dd/yyyy``dd/MM/yyyy` |
## 输出格式
返回创建成功的问题列表:
```json
{
"ok": true,
"data": {
"items": [
{"id": "q_001", "title": "您的姓名是?", "required": true}
]
}
}
```
## 工作流
> [!CAUTION]
> 这是**写入操作** — 执行前必须向用户确认。
1. 先用 `+form-questions-list` 查看现有问题
2. 确认要添加的问题内容
3. 执行命令并报告新建的问题 ID
## 参考
- [lark-base](../SKILL.md) — 多维表格全部命令
- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数

View File

@ -0,0 +1,92 @@
# base +form-questions-update
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
批量更新多维表格表单/问卷中的问题(标题、描述、是否必填)。
## 命令
```bash
# 更新一个问题的标题
lark-cli base +form-questions-update \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[{"id":"q_001","title":"您的真实姓名是?"}]'
# 同时更新多个问题
lark-cli base +form-questions-update \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[
{"id":"q_001","title":"姓名(必填)","required":true},
{"id":"q_002","title":"联系方式","required":false}
]'
# 更新问题描述(纯文本)
lark-cli base +form-questions-update \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[{"id":"q_001","description":"请填写您的真实姓名"}]'
# 更新问题描述(含链接)
lark-cli base +form-questions-update \
--base-token <base_token> \
--table-id <table_id> \
--form-id <form_id> \
--questions '[{"id":"q_001","description":"更多说明请参考[帮助文档](https://example.com/help)"}]'
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--base-token <token>` | 是 | Base Tokenbase_token |
| `--table-id <id>` | 是 | 数据表 ID |
| `--form-id <id>` | 是 | 表单 ID |
| `--questions <json>` | 是 | 问题更新 JSON 数组,最多 10 个(见下方格式) |
| `--format` | 否 | 输出格式json默认\| pretty \| table \| ndjson \| csv |
| `--as` | 否 | 身份user默认\| bot |
| `--dry-run` | 否 | 预览 API 调用,不执行 |
## `--questions` 格式
每个问题对象必须包含 `id`,其余字段按需传入:
| 字段 | 必填 | 说明 |
|------|------|------|
| `id` | **是** | 问题 IDfield_id不可修改 |
| `title` | 否 | 新的问题标题 |
| `description` | 否 | 新的问题描述(纯文本或 Markdown 链接,如 `[文本](https://example.com)` |
| `required` | 否 | 是否必填 |
| `option_display_mode` | 否 | 选项展示方式(仅 `select` 有效):`0`=下拉,`1`=纵向(默认),`2`=横向 |
## 输出格式
返回更新后的问题列表:
```json
{
"ok": true,
"data": {
"items": [
{"id": "q_001", "title": "姓名(必填)", "required": true}
]
}
}
```
## 工作流
> [!CAUTION]
> 这是**写入操作** — 执行前必须向用户确认。
1. 先用 `+form-questions-list` 获取现有问题及其 `id`
2. 构造包含 `id` 的更新数组
3. 执行命令并报告更新结果
## 参考
- [lark-base](../SKILL.md) — 多维表格全部命令
- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数

View File

@ -0,0 +1,179 @@
# base +form-submit
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
通过表单分享链接填写并提交多维表格表单。仅支持分享模式share_token支持填写普通字段值和上传本地文件作为附件。
> **⚠️ 高风险写操作high-risk-write** 本命令会向表单写入并提交数据,属于高风险写操作,必须额外传递 `--yes` 进行确认,否则会返回 `confirmation_required` 错误并退出。当用户明确要求提交且目标表单无歧义时,直接附加 `--yes`,无需再次询问。
## 填写前必读:先获取表单详情
**在调用 `+form-submit` 之前,必须先使用 `+form-detail` 获取表单详情。** 原因如下:
1. **字段类型匹配**:每个题目的 `type` 决定了值的格式(文本、数字、选项、人员、日期等),需根据类型正确构造 `fields` 中的值
2. **必填校验**:通过 `questions[].required` 判断哪些题目为必填项,避免遗漏
3. **显示条件过滤**:部分题目带有 `filter`(显示/隐藏逻辑),需根据用户已填的其他题目值判断该题目是否应该出现——**不应填写被 filter 隐藏的题目**
4. **获取 base_token附件场景必用**`+form-detail` 返回的 `data.base_token` 是该表单所属的多维表格标识。当表单包含附件字段时,提交时必须通过 `--base-token` 传入此值,因为附件需要上传到该 Base 的 Drive Media 中
典型流程:
```bash
# 1⃣ 先获取表单详情,了解所有题目
lark-cli base +form-detail --share-token <share_token>
# 2⃣ 根据返回的 questions 列表,按 type 格式化值、检查 required、判断 filter 条件
# 3⃣ 再提交(高风险写操作,必须带 --yes
lark-cli base +form-submit \
--share-token <share_token> \
--json '{"fields":{...}}' \
--yes
```
`+form-detail` 的返回中要重点读取 `questions[].type``questions[].required`、题目 `filter` 和附件场景所需的 `data.base_token`
## 命令
```bash
# 基本提交(填写普通字段)
lark-cli base +form-submit \
--share-token <share_token> \
--json '{"fields":{"服务评分":5,"评价内容":"服务态度好"}}' \
--yes
# 带附件提交(需要额外提供 --base-token
lark-cli base +form-submit \
--share-token <share_token> \
--base-token <base_token> \
--json '{
"fields": {"服务评分": 5, "评价内容": "好"},
"attachments": {
"附件字段名": ["./report.pdf", "./photo.png"],
"另一个附件字段": ["./doc.docx"]
}
}' \
--yes
# 使用应用身份bot
lark-cli base +form-submit \
--share-token <share_token> \
--json '{"fields":{...}}' \
--as bot \
--yes
# 预览 API 调用不实际执行dry-run 无需 --yes
lark-cli base +form-submit \
--share-token <share_token> \
--json '{"fields":{...}}' \
--dry-run
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--share-token <token>` | 是 | 表单分享 Token必填从表单分享链接中提取 |
| `--base-token <token>` | 条件必填 | Base token**当 `--json` 包含 `attachments` 时必须提供**,用于将附件上传到 Base Drive Media |
| `--json <json>` | 是 | JSON 对象,包含 `"fields"`(普通字段值)和 `"attachments"`(附件上传),详见下方说明 |
| `--yes` | 是 | 确认高风险写操作。本命令为 high-risk-write不带 `--yes` 会返回 `confirmation_required` |
| `--format` | 否 | 输出格式json默认\| pretty \| table \| ndjson \| csv |
| `--as` | 否 | 身份user默认\| bot |
| `--dry-run` | 否 | 预览 API 调用,不执行 |
### --json 结构说明
`--json` 是一个 JSON 对象,包含两个部分:
#### fields普通字段
`fields` 中的单元格值写法与 [`lark-base-cell-value.md`](lark-base-cell-value.md) 完全对齐,填写前应先阅读该文档了解各类型的构造规则:
```json
{
"文本字段": "Hello World",
"电话字段": "13800000000",
"超链接字段": "https://example.com",
"数字字段": 12.5,
"单选字段": "选项A",
"多选字段": ["选项A", "选项B"],
"时间字段": "2026-04-27 14:30:00",
"复选框字段": true,
"人员字段": [{ "id": "ou_7094d131420c8749632145f08fbf114a" }],
"关联字段": [{ "id": "recXXXXXXXXXXXX" }],
"地理位置字段": { "lng": 116.397428, "lat": 39.90923 }
}
```
> **注意:附件类型字段不要写在 `fields` 里。** `fields` 中不包含附件附件有独立的填写方式见下方「attachments附件上传」章节。
> 自动编号、公式、创建/修改人、创建/修改时间等系统字段会自动填入,无需手动传入。
#### attachments附件上传
**附件字段的填写方式与 `fields` 中的普通单元格完全不同**,不能在 `fields` 里传 `file_token` 或其他附件格式。必须将附件字段单独放在 `--json` 的顶层 `attachments` 对象中,值为**本地文件路径数组**(不是 token
```json
{
"attachments": {
"附件字段名": ["./report.pdf", "./photo.png"],
"另一个附件字段": ["./doc.docx"]
}
}
```
CLI 收到路径后会自动完成以下流程:
1. 校验所有文件(存在性、大小 ≤2GB、常规文件
2. 并行上传到 Base Drive Media并发上限 5跨字段重复路径自动去重
3. 获取 `file_token` 后合并到最终表单提交内容中
> 与 [`lark-base-cell-value.md`](lark-base-cell-value.md) 中 Record 场景的附件写法不同Record 写入时附件走独立的 `+record-upload-attachment` 命令;而 `+form-submit` 只需在 `attachments` 中传本地路径,上传由 CLI 内部自动完成。
### 从分享链接提取 share-token
用户提供形如以下格式的表单分享链接时:
```
https://www.example.com/share/base/form/shrbcvST8eZy0vk8zjVZ1CAXNye
```
**提取方式:** 取 URL 路径最后一段作为 `--share-token`
以上述链接为例:
- `share-token` = `shrbcvST8eZy0vk8zjVZ1CAXNye`
```bash
lark-cli base +form-submit \
--share-token shrbcvST8eZy0vk8zjVZ1CAXNye \
--json '{"fields":{...}}' \
--yes
```
## 输出格式
| 字段 | 类型 | 说明 |
|------|------|------|
| `can_submit_again` | bool | 是否可以再次填写 |
```json
{
"ok": true,
"data": {
"can_submit_again": true
}
}
```
## 提示
- **本命令为高风险写操作high-risk-write必须额外传递 `--yes` 确认**,否则返回 `confirmation_required` 并以非零码退出;`--dry-run` 预览除外
- 本命令仅支持通过表单分享链接share_token提交不支持通过 base_token + table_id + view_id 方式提交
- **当 `--json` 包含 `attachments` 时,必须额外提供 `--base-token`**,因为附件上传到 Base Drive Media 需要指定目标 Base
- 附件字段只需在 `--json.attachments` 中提供本地路径即可CLI 自动完成校验、并行上传、Token 获取和合并写入
- 限流:单应用 20 QPS单用户 5 QPS
- 权限要求:`base:form:update`;使用 attachments 时还需 `docs:document.media:upload`
## 参考
- [lark-base](../SKILL.md) — 多维表格全部命令
- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数

View File

@ -0,0 +1,59 @@
# base +record-batch-create
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
批量创建记录。
## 适用场景(重点)
- 适合导入 CSV / Excel、外部系统一次性写入新数据。
- 先把每条输入数据映射为独立的字段对象,再组装到 `create_records`
## 推荐命令
```bash
lark-cli base +record-batch-create --base-token <base_token> --table-id <table_id> \
--json '{"create_records":[{"标题":"任务 A","状态":"Open"},{"标题":"任务 B","状态":"Done"}]}'
lark-cli base +record-batch-create --base-token <base_token> --table-id <table_id> --json @batch-create.json
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--base-token <token>` | 是 | Base Token |
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
| `--json <body>` | 是 | 批量创建请求体,必须是 JSON 对象。支持直接传 JSON 字符串,或 `@<file_path>` 从文件读取 |
## API
`POST /open-apis/base/v3/bases/:base_token/tables/:table_id/records/batch_create`
## `--json` 结构
本节只说明 `+record-batch-create` 的外层 JSON 形状CellValue 统一看 [lark-base-cell-value.md](lark-base-cell-value.md)。
对象形态:
```json
{"create_records":[{"标题":"任务 A","状态":"Open"},{"标题":"任务 B","状态":"Done"}]}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `create_records` | `Array<Map<FieldNameOrID, CellValue>>` | 是 | 记录字段对象数组;每条记录可以提交不同字段,单次最多 200 条 |
## 返回重点
返回 `record_id_list` 和可选的 `ignored_fields`
## 坑点
- 每个 `create_records` 元素都是独立的记录字段对象,只提交该记录需要写入的字段。
- 单次最多 200 条,超出需分批写入。
- `select` 字段只支持写入字段中已有的选项;构造 CellValue 前先用 `+field-list``+field-search-options` 确认目标选项存在。
## 参考
- [lark-base-cell-value.md](lark-base-cell-value.md) — CellValue 格式规范

View File

@ -0,0 +1,54 @@
# base +record-batch-update (batch update)
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
通过 `update_records` 为每条记录提交字段值。
## 推荐命令
```bash
lark-cli base +record-batch-update --base-token <base_token> --table-id <table_id> \
--json '{"update_records":{"<record_id_a>":{"状态":["完成"]},"<record_id_b>":{"分数":20}}}'
lark-cli base +record-batch-update --base-token <base_token> --table-id <table_id> --json @batch-update.json
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--base-token <token>` | 是 | Base Token |
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
| `--json <body>` | 是 | 批量更新请求体,必须是 JSON 对象。支持直接传 JSON 字符串,或 `@<file_path>` 从文件读取 |
## API
`POST /open-apis/base/v3/bases/:base_token/tables/:table_id/records/batch_update`
## `--json` 结构
本节只说明 `+record-batch-update` 的外层 JSON 形状CellValue 统一看 [lark-base-cell-value.md](lark-base-cell-value.md)。
对象形态:
```json
{"update_records":{"recA":{"状态":["完成"]},"recB":{"分数":20}}}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `update_records` | `Map<RecordID, Map<FieldNameOrID, CellValue>>` | 是 | record ID 到字段更新对象的映射(单次最多 200 条) |
## 返回重点
成功响应只包含可选的 `ignored_fields`;没有忽略字段时 `data` 为空对象。请求不会预先校验 record ID 是否存在,因此需要确认实际写入结果时,应再用 `+record-get` 读回目标记录。
## 坑点
- 单次最多更新 200 条记录,超过会被接口校验拒绝。
- 命令不会自动做字段/行映射转换,传什么就发什么。
- 如果字段映射包含只读字段,返回里可能出现 `ignored_fields`;这些字段不会被更新。
## 参考
- [lark-base-cell-value.md](lark-base-cell-value.md) — CellValue 格式规范

View File

@ -0,0 +1,43 @@
# base +record-history-list
查询单条记录的变更历史。它返回历史事件,不返回记录当前值,也不支持整表审计扫描。
## 推荐命令
```bash
lark-cli base +record-history-list \
--base-token <base_token> \
--table-id <table_id> \
--record-id <record_id>
lark-cli base +record-history-list \
--base-token <base_token> \
--table-id <table_id> \
--record-id <record_id> \
--page-size 30 \
--max-version <next_max_version>
```
## 返回解释
- 历史条目通常按版本号降序返回,最新在前。
- 每条历史包含版本号、操作人、操作时间、操作类型和字段变更。
- `create_time` 是秒级 Unix 时间戳。
- `field_changes` 描述字段变更,重点看字段名/字段类型、`before``after`
- `activity_type` 常见值:`create`(创建记录)、`update`(编辑记录)、`delete`(删除记录)。
以下字段类型的变化可能不会出现在 `field_changes` 中:
- 计算字段:`formula``lookup`
- 系统字段:自动编号、创建时间、创建人、修改时间、修改人
## 翻页
- 首次请求不传 `--max-version`
- 如果返回 `has_more=true`,取返回中的 `next_max_version` 作为下一次请求的 `--max-version`
- `--page-size` 默认 30最大 50。
## 注意
- `table-id``record-id` 必须来自同一张表。
- 这是单条记录历史,不是表级审计;需要查多条记录时串行调用。

View File

@ -0,0 +1,63 @@
# base +record-upsert
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
创建记录,或在带 `--record-id` 时更新记录。
## 推荐命令
```bash
# 创建记录
lark-cli base +record-upsert --base-token <base_token> --table-id <table_id> \
--json '{"项目名称":"Apollo","状态":"进行中"}'
# 更新记录
lark-cli base +record-upsert --base-token <base_token> --table-id <table_id> --record-id <record_id> \
--json '{"项目名称":"Apollo","状态":"完成","完成时间":"2026-03-24 10:00:00"}'
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--base-token <token>` | 是 | Base Token |
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
| `--record-id <id>` | 否 | 传入时走更新,不传时走创建 |
| `--json <body>` | 是 | 字段写入对象,类型 `Map<FieldNameOrID, CellValue>` |
## API
- 创建:`POST /open-apis/base/v3/bases/:base_token/tables/:table_id/records`
- 更新:带 `--record-id` 时改走 `PATCH /records/:record_id`
## `--json` 结构
- `--json` 必须是 **JSON object map**,形状是 `Map<FieldNameOrID, CellValue>`
- key 是字段名或字段 IDvalue 是该字段的 `CellValue`
- 一次请求里同一字段只用一种标识,避免重复写入冲突。
- 写入前先 `+field-list` 确认字段类型和字段名/ID。
- CellValue 统一看 [lark-base-cell-value.md](lark-base-cell-value.md)。
```json
{
"项目名称": "Apollo",
"状态": "进行中",
"完成时间": "2026-03-24 10:00:00"
}
```
## 返回重点
- 创建时返回 `record``created: true`
- 更新时返回 `record``updated: true`
- 如果写入了 `formula / lookup / created_at / updated_at / created_by / updated_by` 等只读字段,返回里可能出现 `ignored_fields`,这些字段不会被更新。
## 坑点
-`--record-id` 就一定更新;不传就一定创建,不会自动查重或按业务键 upsert。
- `select` 字段只支持写入字段中已有的选项;构造 CellValue 前先用 `+field-list``+field-search-options` 确认目标选项存在。
- 这是写入操作,执行前必须确认目标表和字段。
## 参考
- [lark-base-cell-value.md](lark-base-cell-value.md) — CellValue 格式规范

View File

@ -0,0 +1,65 @@
# Base advanced permission and role guide
This guide is the entry point for Base advanced permissions and roles. Use it to choose commands and understand safety boundaries. For the permission JSON itself, use [role-config.md](role-config.md) as the SSOT.
## Command selection
| Goal | Command | Notes |
|------|---------|-------|
| Enable advanced permissions | `+advperm-enable` | Required before creating or updating roles. Caller must be a Base admin. |
| Disable advanced permissions | `+advperm-disable` | High-risk write. Disabling invalidates existing custom roles. |
| Locate roles | `+role-list` | Returns role summaries. Use `+role-get` for full config. |
| Inspect one role | `+role-get` | Use before updating a role or deciding whether a role can be deleted. |
| Create a custom role | `+role-create` | Supports `custom_role` only. Read [role-config.md](role-config.md) before constructing `--json`. |
| Update a role | `+role-update` | Delta merge. Read current config first, then send only intended changes. |
| Delete a role | `+role-delete` | Custom roles only. System roles cannot be deleted. |
## Safety boundaries
- Role operations require advanced permissions to be enabled and the caller to be a Base admin.
- `+role-create` creates custom roles only.
- `+role-delete` is only for custom roles. System roles such as editor/reader can be configured within supported limits, but cannot be deleted.
- `+role-update` uses delta merge: omitted fields remain unchanged, but identity fields such as `role_name` and `role_type` should match the current target role.
- `+advperm-disable` invalidates existing custom roles; confirm the target Base and user intent before passing `--yes`.
## Common Fewshots
Use these fewshots for simple role changes. For table, field, record, dashboard, docx, or filter permission details, switch to [role-config.md](role-config.md).
Create a custom role that keeps copy/download disabled:
```bash
lark-cli base +role-create \
--base-token <base_token> \
--json '{"role_name":"Reviewer","role_type":"custom_role","base_rule_map":{"copy":false,"download":false}}'
```
Rename a role while preserving its type:
```bash
lark-cli base +role-update \
--base-token <base_token> \
--role-id <role_id> \
--json '{"role_name":"Finance Reviewer","role_type":"custom_role"}' \
--yes
```
Grant read-only access to one table:
```bash
lark-cli base +role-update \
--base-token <base_token> \
--role-id <role_id> \
--json '{"role_name":"Finance Reviewer","role_type":"custom_role","table_rule_map":{"Orders":{"perm":"read_only"}}}' \
--yes
```
## JSON SSOT
Use [role-config.md](role-config.md) for:
- `AdvPermBaseRoleConfig` top-level structure.
- `base_rule_map`, `table_rule_map`, `dashboard_rule_map`, and `docx_rule_map`.
- Table, view, field, record, dashboard, and docx permission values.
- Filter permission JSON.
- Default permission strategy and risk rules.

View File

@ -0,0 +1,191 @@
# base +view-set-filter
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
更新视图筛选配置。
## 1. 顶层规则
- `--json` 必须是 JSON 对象。
- 顶层结构是 `{logic?, conditions?}`
- `logic` 默认 `and`;推荐只用 canonical 值 `and` / `or`
- `conditions` 默认空数组。
- 每条条件写成 tuple`[field, operator, value?]`
- `empty` / `non_empty` 可写成 2 项:`[field, "empty"]``[field, "non_empty"]`
- 支持 `filter` 的视图类型:`grid``kanban``gallery``calendar``gantt`
## 2. operator
可用 operator
- `==`
- `!=`
- `>`
- `>=`
- `<`
- `<=`
- `intersects`
- `disjoint`
- `empty`
- `non_empty`
## 3. value 写法
### `text`
用字符串:
```json
["标题", "intersects", "发布"]
```
### `location`
location 筛选只按 `full_address` 字符串匹配,不能直接按经纬度筛选;优先使用 `intersects` 做包含匹配,例如查深圳:
```json
["位置", "intersects", "深圳"]
```
不推荐写 `["位置", "==", "深圳"]` 这类精确匹配,除非确保筛选值与完整 `full_address` 完全一致。
### `number` / `auto_number`
用数字:
```json
["工时", ">=", 3.5]
```
### `select`
用选项名数组:
```json
["状态", "intersects", ["Doing", "Blocked"]]
```
### `user` / `created_by` / `updated_by`
用对象数组:
> **人员筛选:不要猜 ID。** 不知道 `open_id` 时,先用 `lark-contact` 查 id`lark-cli contact +search-user --query "<姓名/邮箱/手机号>" --as user`。
```json
["负责人", "intersects", [{ "id": "ou_xxx" }]]
```
### `group_chat`
用对象数组:
> **群组筛选:不要猜 ID。** 不知道 `chat_id` 时,先用 `lark-im` 搜群:`lark-cli im +chat-search --query "<群名关键词>" --as user`;取结果里的 `oc_xxx`。
```json
["负责群", "intersects", [{ "id": "oc_xxx" }]]
```
### `link`
用记录 id 对象数组:
```json
["关联任务", "intersects", [{ "id": "rec_xxx" }]]
```
### `checkbox`
用布尔值:
```json
["完成", "==", true]
```
### `datetime` / `created_at` / `updated_at`
用相对时间关键字或 `ExactDate(...)`
```json
["截止时间", "==", "ExactDate(2026-01-01)"]
```
```json
["截止时间", "==", "ExactDate(2026-01-01 11:30)"]
```
```json
["截止时间", "==", "Today"]
```
可用关键字:
- `Today`
- `Yesterday`
- `Tomorrow`
### `formula` / `lookup`
- 筛选值类型由字段计算结果类型动态决定。
- 拿不准时,先把 `value` 当作单个字符串填入做一次尝试。
- 如果报错,再按错误提示把 `value` 改成对应类型。
字符串示例:
```json
["风险说明", "intersects", "高风险"]
```
数字示例:
```json
["汇总分", ">=", 80]
```
## 4. 推荐命令
```bash
lark-cli base +view-set-filter \
--base-token <base_token> \
--table-id <table_id> \
--view-id <view_id> \
--json '{"logic":"and","conditions":[["状态","intersects",["Doing"]],["负责人","intersects",[{"id":"ou_xxx"}]],["截止时间","empty"]]}'
```
## 5. JSON 写法
```json
{
"logic": "and",
"conditions": [
["状态", "intersects", ["Doing"]],
["负责人", "intersects", [{ "id": "ou_xxx" }]],
["截止时间", "empty"]
]
}
```
清空写法:
```json
{
"conditions": []
}
```
## 6. 使用建议
- 先读取当前筛选配置,理解现有 `logic``conditions` 的组合关系;只替换用户要求变更的条件,未提到的条件默认保留。
- 优先传字段 id不要依赖字段名。
- 拿不准字段 type 或真实取值时,先用 `+field-list` / `+record-list` 确认,再按对应字段类型的 value 写法构造条件;别按字段名猜 type、凭印象猜枚举取值。
- 需要清空全部筛选时,直接传 `{"conditions":[]}`
## 7. 易错点
- 本 tuple DSL 由 `+view-set-filter``+record-list` / `+record-search``--filter-json` 共用;不要写成 `+data-query` 的对象风格 `{"field_name":...,"operator":...}`(会报校验失败)。
- 标量类字段(`text` / `number` / `datetime` 等)的 value 用标量、别包成数组(各类型详见 value 写法一节)。
- `user` / `group_chat` / `link` 不要写成单个标量。
- `empty` / `non_empty` 不要硬塞无意义的 value。
- 日期条件稳定写法用 `ExactDate(...)``Today` / `Yesterday` / `Tomorrow`
- `formula` / `lookup` 的 value 形状不固定;拿不准时先读当前 filter 或字段定义,或根据错误提示修正类型。
## 8. 参考
- [lookup-field-guide.md](lookup-field-guide.md)

View File

@ -0,0 +1,830 @@
# Workflow guide
本文档是 Workflow 的入口指南,帮助选择步骤组合、理解创建/更新边界,并引导到 steps JSON SSOT。
> **配套文档**:
> - Workflow 的数据结构参考:[lark-base-workflow-schema.md](lark-base-workflow-schema.md)
> - 创建/更新时重点构造 `title`、`status` 和 `steps`;复杂度集中在 `steps[].type/data/next`
---
## 快速开始
### 最简单的 Workflow
新增记录时发送消息通知:
```json
{
"client_token": "1704067200",
"title": "新订单自动通知",
"steps": [
{
"id": "trigger_1",
"type": "AddRecordTrigger",
"title": "监控新订单",
"next": "action_1",
"data": {
"table_name": "订单表",
"watched_field_name": "订单号"
}
},
{
"id": "action_1",
"type": "LarkMessageAction",
"title": "发送通知",
"next": null,
"data": {
"receiver": [{ "value_type": "user", "value": {"id": "ou_xxxx", "name": "张三"} }],
"send_to_everyone": false,
"title": [{ "value_type": "text", "value": "新订单提醒" }],
"content": [
{ "value_type": "text", "value": "收到新订单" }
],
"btn_list": []
}
}
]
}
```
---
## 场景速查表
| 场景 | 步骤组合 | 示例 |
|------|---------|------|
| 新增触发+通知 | AddRecordTrigger → LarkMessageAction | [下方](#示例1-新增记录触发--发送消息) |
| 按钮点击+调用外部接口+写入日志 | ButtonTrigger → HTTPClientAction → AddRecordAction | [下方](#示例-6-按钮触发--调用外部接口--写入同步日志) |
| 定时+循环 | TimerTrigger → FindRecordAction → Loop → LarkMessageAction | [下方](#示例2-定时触发--查找记录--循环遍历--发送消息) |
| 条件判断 | ... → IfElseBranch → 分支处理 | [下方](#示例3-条件分支-ifelsebranch) |
| 多路分类 | ... → SwitchBranch → 多分支处理 | [下方](#示例4-多路分支-switchbranch) |
| 复杂组合 | 定时+查找+循环+分支+消息 | [下方](#示例5-组合场景-定时查找循环分支消息) |
---
## 完整示例
### 示例 1: 新增记录触发 + 发送消息
**场景**: 当订单表新增记录时,发送飞书消息通知负责人。
```json
{
"client_token": "1704067201",
"title": "新订单自动通知",
"steps": [
{
"id": "step_trigger",
"type": "AddRecordTrigger",
"title": "新增订单时触发",
"next": "step_notify",
"data": {
"table_name": "订单表",
"watched_field_name": "订单号",
"condition_list": null
}
},
{
"id": "step_notify",
"type": "LarkMessageAction",
"title": "发送订单通知",
"next": null,
"data": {
"receiver": [{ "value_type": "ref", "value": "$.step_trigger.fldManager" }],
"send_to_everyone": false,
"title": [{ "value_type": "text", "value": "新订单提醒" }],
"content": [
{ "value_type": "text", "value": "客户 " },
{ "value_type": "ref", "value": "$.step_trigger.fldCustomer" },
{ "value_type": "text", "value": " 创建了新订单,金额:¥" },
{ "value_type": "ref", "value": "$.step_trigger.fldAmount" }
],
"btn_list": [
{
"text": "查看订单",
"btn_action": "openLink",
"link": [{ "value_type": "ref", "value": "$.step_trigger.recordLink" }]
}
]
}
}
]
}
```
**关键点**:
- `AddRecordTrigger` 监控 `table_name` 表的 `watched_field_name` 字段
- 使用 `ref` 引用触发器输出的字段值(注意是 fieldId不是字段名
- `recordLink` 是触发器内置输出,表示记录链接
---
### 示例 2: 定时触发 + 查找记录 + 循环遍历 + 发送消息
**场景**: 每天早上 9 点,查找所有待处理订单,给每个客户发送提醒。
```json
{
"client_token": "1704067202",
"title": "每日待处理订单提醒",
"steps": [
{
"id": "step_timer",
"type": "TimerTrigger",
"title": "每天早上9点触发",
"next": "step_find_orders",
"data": {
"rule": "DAILY",
"start_time": "2025-01-01 09:00",
"is_never_end": true
}
},
{
"id": "step_find_orders",
"type": "FindRecordAction",
"title": "查找所有待处理订单",
"next": "step_loop_customers",
"data": {
"table_name": "订单表",
"field_names": ["客户名称", "订单金额", "客户联系方式"],
"should_proceed_when_no_results": false,
"filter_info": {
"conjunction": "and",
"conditions": [
{
"field_name": "状态",
"operator": "is",
"value": [{ "value_type": "option", "value": { "name": "待处理" } }]
}
]
}
}
},
{
"id": "step_loop_customers",
"type": "Loop",
"title": "遍历每个订单",
"children": {
"links": [
{ "kind": "loop_start", "to": "step_send_reminder" }
]
},
"next": null,
"data": {
"loop_mode": "continue",
"max_loop_times": 100,
"data": [{
"value_type": "ref",
"value": "$.step_find_orders.fieldRecords"
}]
}
},
{
"id": "step_send_reminder",
"type": "LarkMessageAction",
"title": "发送催办消息",
"next": null,
"data": {
"receiver": [{
"value_type": "ref",
"value": "$.step_loop_customers.item.fldContact"
}],
"send_to_everyone": false,
"title": [{ "value_type": "text", "value": "订单处理提醒" }],
"content": [
{ "value_type": "text", "value": "您好,您的订单 " },
{ "value_type": "ref", "value": "$.step_loop_customers.item.fldName" },
{ "value_type": "text", "value": " 金额 ¥" },
{ "value_type": "ref", "value": "$.step_loop_customers.item.fldAmount" },
{ "value_type": "text", "value": " 正在处理中。" }
],
"btn_list": []
}
}
]
}
```
**关键点**:
- `Loop.data` 必须传入 `ref` 类型的数据源(通常是 FindRecordAction 的 `fieldRecords`
- `Loop.children.links` 必须包含 `kind: "loop_start"` 的链接指向循环体
- 循环体内用 `$.{loopStepId}.item.{fieldId}` 引用当前遍历记录的字段
- `$.{loopStepId}.index` 获取当前索引(从 0 开始)
---
### 示例 3: 条件分支IfElseBranch
**场景**: 根据订单金额判断,大额订单通知主管审批,小额订单自动通过。
```json
{
"client_token": "1704067203",
"title": "订单金额自动判断",
"steps": [
{
"id": "step_trigger",
"type": "AddRecordTrigger",
"title": "新增订单时触发",
"next": "step_check_amount",
"data": {
"table_name": "订单表",
"watched_field_name": "订单金额"
}
},
{
"id": "step_check_amount",
"type": "IfElseBranch",
"title": "判断是否为大额订单",
"children": {
"links": [
{ "kind": "if_true", "to": "step_notify_manager", "label": "high", "desc": "金额>=10000" },
{ "kind": "if_false", "to": "step_auto_approve", "label": "normal", "desc": "金额<10000" }
]
},
"next": "step_log",
"data": {
"condition": {
"conjunction": "or",
"conditions": [
{
"conjunction": "and",
"conditions": [
{
"left_value": { "value_type": "ref", "value": "$.step_trigger.fldAmount" },
"operator": "isGreaterEqual",
"right_value": [{ "value_type": "number", "value": 10000 }]
}
]
}
]
}
}
},
{
"id": "step_notify_manager",
"type": "LarkMessageAction",
"title": "通知主管审批大额订单",
"next": "step_log",
"data": {
"receiver": [{ "value_type": "user", "value": {"id": "ou_manager", "name": "主管"} }],
"send_to_everyone": false,
"title": [{ "value_type": "text", "value": "大额订单待审批" }],
"content": [
{ "value_type": "text", "value": "有大额订单 ¥" },
{ "value_type": "ref", "value": "$.step_trigger.fldAmount" },
{ "value_type": "text", "value": " 需要您审批" }
],
"btn_list": []
}
},
{
"id": "step_auto_approve",
"type": "SetRecordAction",
"title": "自动标记小额订单为已审核",
"next": "step_log",
"data": {
"table_name": "订单表",
"ref_info": { "step_id": "step_trigger" },
"field_values": [
{
"field_name": "审批状态",
"value": [{ "value_type": "option", "value": { "name": "已自动审核" } }]
}
]
}
},
{
"id": "step_log",
"type": "GenerateAiTextAction",
"title": "生成订单处理日志",
"next": null,
"data": {
"prompt": [
{ "value_type": "text", "value": "请生成订单处理日志,金额:" },
{ "value_type": "ref", "value": "$.step_trigger.fldAmount" }
]
}
}
]
}
```
**关键点**:
- `IfElseBranch.children.links` 必须包含 `if_true``if_false` 两个分支
- `next` 指向两个分支汇合后的步骤(可选,为 null 则分支结束)
- `condition` 使用 OrGroup 结构,支持 `(A and B) or (C and D)` 的复杂条件
- 分支内可以用 `ref_info` 引用触发记录,用 `filter_info` 批量筛选记录
---
### 示例 4: 多路分支SwitchBranch
**场景**: 根据订单优先级P0/P1/P2执行不同的处理流程。
```json
{
"client_token": "1704067204",
"title": "按优先级分类处理订单",
"steps": [
{
"id": "step_trigger",
"type": "AddRecordTrigger",
"title": "新增订单时触发",
"next": "step_classify",
"data": {
"table_name": "订单表",
"watched_field_name": "优先级"
}
},
{
"id": "step_classify",
"type": "SwitchBranch",
"title": "按优先级分类",
"children": {
"links": [
{ "kind": "case", "to": "step_p0_handler", "label": "p0", "desc": "P0-紧急" },
{ "kind": "case", "to": "step_p1_handler", "label": "p1", "desc": "P1-高优先级" },
{ "kind": "case", "to": "step_p2_handler", "label": "p2", "desc": "P2-普通" },
{ "kind": "case", "to": "step_other_handler", "label": "other", "desc": "其他" }
]
},
"next": null,
"data": {
"mode": "exclusive",
"no_match_action": "classifyToOther",
"child_branch_list": [
{
"name": "P0-紧急",
"condition": {
"conjunction": "or",
"conditions": [
{
"conjunction": "and",
"conditions": [
{
"left_value": { "value_type": "ref", "value": "$.step_trigger.fldPriority" },
"operator": "is",
"right_value": [{ "value_type": "option", "value": { "name": "P0" } }]
}
]
}
]
}
},
{
"name": "P1-高优先级",
"condition": {
"conjunction": "or",
"conditions": [
{
"conjunction": "and",
"conditions": [
{
"left_value": { "value_type": "ref", "value": "$.step_trigger.fldPriority" },
"operator": "is",
"right_value": [{ "value_type": "option", "value": { "name": "P1" } }]
}
]
}
]
}
},
{
"name": "P2-普通",
"condition": {
"conjunction": "or",
"conditions": [
{
"conjunction": "and",
"conditions": [
{
"left_value": { "value_type": "ref", "value": "$.step_trigger.fldPriority" },
"operator": "is",
"right_value": [{ "value_type": "option", "value": { "name": "P2" } }]
}
]
}
]
}
}
]
}
},
{
"id": "step_p0_handler",
"type": "LarkMessageAction",
"title": "P0紧急处理",
"next": null,
"data": {
"receiver": [{ "value_type": "user", "value": {"id": "ou_director", "name": "总监"} }],
"send_to_everyone": false,
"title": [{ "value_type": "text", "value": "🚨 P0 紧急订单" }],
"content": [{ "value_type": "text", "value": "有新的 P0 紧急订单需要立即处理" }],
"btn_list": []
}
},
{
"id": "step_p1_handler",
"type": "SetRecordAction",
"title": "标记高优先级",
"next": null,
"data": {
"table_name": "订单表",
"ref_info": { "step_id": "step_trigger" },
"field_values": [
{ "field_name": "处理状态", "value": [{ "value_type": "text", "value": "高优先级待处理" }] }
]
}
},
{
"id": "step_p2_handler",
"type": "Delay",
"title": "普通订单延迟处理",
"next": null,
"data": { "duration": 60 }
},
{
"id": "step_other_handler",
"type": "SetRecordAction",
"title": "标记其他订单",
"next": null,
"data": {
"table_name": "订单表",
"ref_info": { "step_id": "step_trigger" },
"field_values": [
{ "field_name": "处理状态", "value": [{ "value_type": "text", "value": "待分类" }] }
]
}
}
]
}
```
**关键点**:
- `SwitchBranch` 适合 3 路及以上的分支场景(少于 3 路用 `IfElseBranch` 更简洁)
- `children.links``kind: "case"``label` 对应 `child_branch_list` 中的条件
- `mode: "exclusive"` 表示排他执行(第一个匹配的分支执行后停止)
- `no_match_action: "classifyToOther"` 表示无匹配时走最后一个 `case`(兜底分支)
---
### 示例 5: 组合场景(定时+查找+循环+分支+消息)
**场景**: 每天早上 9 点,查找昨天的订单,按金额分级,给不同级别的销售发送不同的通知。
```json
{
"client_token": "1704067205",
"title": "每日订单分级通知",
"steps": [
{
"id": "step_timer",
"type": "TimerTrigger",
"title": "每天早上9点触发",
"next": "step_find_orders",
"data": {
"rule": "DAILY",
"start_time": "2025-01-01 09:00",
"is_never_end": true
}
},
{
"id": "step_find_orders",
"type": "FindRecordAction",
"title": "查找昨天所有订单",
"next": "step_loop",
"data": {
"table_name": "订单表",
"field_names": ["订单号", "客户名称", "金额", "销售负责人"],
"should_proceed_when_no_results": false,
"filter_info": {
"conjunction": "and",
"conditions": [
{ "field_name": "创建时间", "operator": "isGreaterEqual", "value": [{ "value_type": "date", "value": "yesterday" }] }
]
}
}
},
{
"id": "step_loop",
"type": "Loop",
"title": "遍历每个订单",
"children": {
"links": [
{ "kind": "loop_start", "to": "step_classify" }
]
},
"next": "step_summary",
"data": {
"loop_mode": "continue",
"max_loop_times": 500,
"data": [{ "value_type": "ref", "value": "$.step_find_orders.fieldRecords" }]
}
},
{
"id": "step_classify",
"type": "SwitchBranch",
"title": "按金额分类",
"children": {
"links": [
{ "kind": "case", "to": "step_vip_notify", "label": "vip", "desc": "VIP >= 10万" },
{ "kind": "case", "to": "step_normal_notify", "label": "normal", "desc": "普通 < 10万" }
]
},
"next": null,
"data": {
"mode": "exclusive",
"no_match_action": "fail",
"child_branch_list": [
{
"name": "VIP订单",
"condition": {
"conjunction": "or",
"conditions": [
{
"conjunction": "and",
"conditions": [
{
"left_value": { "value_type": "ref", "value": "$.step_loop.item.fldAmount" },
"operator": "isGreaterEqual",
"right_value": [{ "value_type": "number", "value": 100000 }]
}
]
}
]
}
},
{
"name": "普通订单",
"condition": {
"conjunction": "or",
"conditions": [
{
"conjunction": "and",
"conditions": [
{
"left_value": { "value_type": "ref", "value": "$.step_loop.item.fldAmount" },
"operator": "isLess",
"right_value": [{ "value_type": "number", "value": 100000 }]
}
]
}
]
}
}
]
}
},
{
"id": "step_vip_notify",
"type": "LarkMessageAction",
"title": "VIP订单通知",
"next": null,
"data": {
"receiver": [{ "value_type": "ref", "value": "$.step_loop.item.fldSales" }],
"send_to_everyone": false,
"title": [{ "value_type": "text", "value": "🌟 VIP大额订单" }],
"content": [
{ "value_type": "text", "value": "恭喜!您有一笔 VIP 订单 ¥" },
{ "value_type": "ref", "value": "$.step_loop.item.fldAmount" },
{ "value_type": "text", "value": ",客户:" },
{ "value_type": "ref", "value": "$.step_loop.item.fldCustomer" }
],
"btn_list": []
}
},
{
"id": "step_normal_notify",
"type": "LarkMessageAction",
"title": "普通订单通知",
"next": null,
"data": {
"receiver": [{ "value_type": "ref", "value": "$.step_loop.item.fldSales" }],
"send_to_everyone": false,
"title": [{ "value_type": "text", "value": "新订单通知" }],
"content": [
{ "value_type": "text", "value": "您有一笔新订单 ¥" },
{ "value_type": "ref", "value": "$.step_loop.item.fldAmount" }
],
"btn_list": []
}
},
{
"id": "step_summary",
"type": "GenerateAiTextAction",
"title": "生成日报",
"next": null,
"data": {
"prompt": [
{ "value_type": "text", "value": "请生成昨日订单处理日报" }
]
}
}
]
}
```
---
### 示例 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 构造要点
1. **数据源**: `Loop.data` 必须传入 `ref` 类型,通常是 `FindRecordAction``fieldRecords`
2. **循环体**: `children.links` 必须包含 `kind: "loop_start"` 指向循环体入口
3. **引用**: 循环体内用 `$.{loopStepId}.item.{fieldId}` 引用当前元素
4. **索引**: 用 `$.{loopStepId}.index` 获取当前索引(从 0 开始)
### 分支构造要点
1. **IfElseBranch**:
- 适合二元判断(是/否、大于/小于)
- `children.links` 必须包含 `if_true``if_false`
- 可以用 `next` 指向汇合点
2. **SwitchBranch**:
- 适合多路分类3路及以上
- `label` 对应 `child_branch_list` 中的条件顺序
- 建议加一个兜底分支(其他)
### 字段值构造
| 字段类型 | value_type | 示例 |
|---------|------------|------|
| 文本 | `text` | `{"value_type": "text", "value": "张三"}` |
| 数字 | `number` | `{"value_type": "number", "value": 100}` |
| 单选 | `option` | `{"value_type": "option", "value": {"name": "已完成"}}` |
| 人员 | `user` | `{"value_type": "user", "value": {"id": "ou_xxxx"}}` |
| 引用 | `ref` | `{"value_type": "ref", "value": "$.step_1.fldxxx"}` |
---
## 常见错误避免
### Top 10 高频错误
| # | 错误信息 | 原因 | 解决方案 |
|---|---------|------|---------|
| 1 | `path "xxx" does not exist in the output path tree` | ref 引用路径错误或 stepId 不存在 | 检查 stepId 是否在 steps 数组中;使用 fieldId 而非字段名;确保路径以 `$.` 开头 |
| 2 | `recordInfo.conditions must be non-empty` | `condition_list` 为空数组 `[]` | 改用 `null` 或省略该字段 |
| 3 | `At least one of filter info and ref info is required` | SetRecordAction/FindRecordAction 缺少定位条件 | 必须提供 `filter_info``ref_info` 之一 |
| 4 | `client token is empty` | 缺少 `client_token` | 每次请求传入唯一值(时间戳或随机字符串) |
| 5 | `valueType 'text' not allowed for fieldType '3'` | select 类型字段值格式错误 | 改用 `option` 类型 |
| 6 | `Undefined Step Type` | 使用了不支持的 StepType | 使用 `AddRecordTrigger` 而非 `CreateRecordTrigger` |
| 7 | `prompt references an unknown reference from step` | 引用的 stepId 不存在 | 确保引用的 step 在同一 workflow 的 steps 数组中 |
| 8 | `[2200] Internal Error` | 1. steps[].id 重复 2. next/children.links 引用了不存在的 step | 确保所有 step id 唯一;检查引用关系 |
| 9 | 工作流结构不完整 | Branch/Loop 节点缺少 `children` | 仅 BranchIfElseBranch/SwitchBranch和 Loop 节点需要 `children`Trigger/Action 节点无需设置 |
| 10 | 嵌套分支过于复杂 | 多层 IfElseBranch 嵌套 | 3+ 路分支用 SwitchBranch 替代嵌套 IfElseBranch |
### 其他常见错误
**1. condition_list 为空数组**
```json
// ❌ 错误
{ "condition_list": [] }
// ✅ 正确
{ "condition_list": null }
// 或省略该字段
```
**2. filter_info 和 ref_info 同时提供**
```json
// ❌ 错误
{ "filter_info": {...}, "ref_info": {...} }
// ✅ 正确(二选一)
{ "filter_info": {...}, "ref_info": null }
{ "filter_info": null, "ref_info": {...} }
```
**3. 使用字段名而非 fieldId**
```json
// ❌ 错误
{ "value": "$.step_1.客户名称" }
// ✅ 正确
{ "value": "$.step_1.fldXXXXXXXX" }
```
---
## 参考
- [lark-base-workflow-schema.md](lark-base-workflow-schema.md) — 字段定义参考
- 创建/更新前先确认真实表名、字段名和目标 workflow ID`steps` 结构按 schema 构造,不凭自然语言猜 `type`

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,512 @@
# Base Lookup Field Configuration Guide
## Mandatory Read Acknowledgement
When creating or updating a lookup field with `lark-cli base +field-create/+field-update --json ...` and `type` is `lookup`, you should read this guide first and only then add `--i-have-read-guide` to the command.
Do **not** proactively add `--i-have-read-guide` before reading this guide. Without it, the CLI will fail fast and direct you back to this guide.
When using `+field-update`, also pass `--yes`: field update is a high-risk `PUT` operation because changing a field definition can affect the whole column.
## Default strategy
**Use Formula fields by default for cross-table references and aggregations.** Only use Lookup fields when the user explicitly requests a Lookup field. Formula is a strict superset of Lookup — anything Lookup can do, Formula can do with a single expression.
## Usage
When creating a lookup field, the Agent should:
1. Get all table names: `lark-cli base +table-list --base-token <base>` — returns `items[].table_name`
2. Get table structure: `lark-cli base +table-get --base-token <base> --table-id <table>` — returns `fields[]`
3. If the lookup references other tables, also get those tables' structures
4. Determine the four elements: from (source table), select (source field), where (filter), aggregate (aggregation)
5. Construct the Lookup field JSON and submit it to create or update the field
**Key constraints**:
- Table names and field names must **exactly match** those returned by `+table-list` / `+table-get`
- The `from` table must be in the same Base
---
## Section 1: Core Concepts — Four-Element Model
A Lookup field is defined by five fields:
| Field | Meaning | JSON key | Required |
|-------|---------|----------|----------|
| **type** | Must be `"lookup"` | `type` | Yes |
| **from** | Source table to pull data from | `from` | Yes |
| **select** | Field in the source table to retrieve | `select` | Yes |
| **where** | Filter conditions on the source table | `where` | Yes (at least one condition) |
| **aggregate** | How to aggregate multiple matching records | `aggregate` | No (default: `raw_value`) |
**SQL analogy**:
```
SELECT [select field]
FROM [from table]
WHERE [filter conditions]
GROUP BY [aggregate function]
```
**Row-level matching (most important concept)**:
A Lookup field is computed row-by-row — for each row in the current table, it filters the source table to find "related" records. **The filter defines what "related" means.**
```
Current table row 1 → filter source table → matching records → select field → aggregate → result
Current table row 2 → filter source table → matching records → select field → aggregate → result
...
```
**Rule: Whenever the current table and the source table have a row-level correspondence (matching by some field value), you must specify a filter.**
---
## Section 2: Lookup vs Link vs Formula
Lookup and Link serve **different purposes**. Creating a Lookup does NOT require a Link field to exist first.
| Dimension | Link | Lookup | Formula |
|-----------|------|--------|---------|
| Purpose | Establish record relationships (read-write) | Pull and aggregate data from another table (read-only) | Compute values from expressions (read-only) |
| When to use | "link" / "associate" / "bind" two tables | "look up" / "reference" / "aggregate" / "count" from another table | Calculations, text manipulation, conditional logic |
**Common mistake**: Creating a Link field just to create a Lookup. If two tables share a matching text/number field, Lookup can match directly — no Link required.
**Selection decision tree**:
```
What does the user need?
├─ "Link"/"associate"/"bind" records between tables → Link
├─ "Look up"/"reference"/"aggregate"/"count" from another table → Lookup
│ ├─ Needs aggregation (sum/count/average)? → Lookup + aggregate
│ └─ Just reference a value? → Lookup (aggregate = null)
├─ Calculations/text manipulation within current table → Formula
└─ Access linked record's field → Prefer Lookup (more intuitive), or Formula chain access
```
---
## Section 3: Filter Condition Rules
**You must provide a `where` with at least one condition.** Improper conditions cause every row to pull all records from the source table.
### The Iron Rule: field belongs to source table
```
filter condition:
field → must be a field in the FROM table (source table)
value → constant or reference to a field in the CURRENT table
```
### How to find the matching field pair
**With a Link field (most common)**: The match is between the **Link field** and the **target table's primary field**.
```
Link is in the source table → source.linkField matches current.primaryField
Link is in the current table → source.primaryField matches current.linkField
```
**Without a Link field**: Two tables share a field with the same meaning — match directly.
### Where condition structure
Each condition is a **tuple** (array) of 2 or 3 elements: `[field, operator, value?]`
```json
{
"logic": "and",
"conditions": [
["<source table field>", "<operator>", { "type": "constant", "value": "<val>" }]
]
}
```
For `empty` / `non_empty`, the value can be omitted (2-element tuple):
```json
["<source table field>", "empty"]
```
### Two value formats
**Constant value** — for fixed conditions (e.g., "status is completed"):
```json
["状态", "==", { "type": "constant", "value": "已完成" }]
```
**Field reference** — for dynamic per-row matching (e.g., "match current row's project"):
```json
["项目名", "==", { "type": "field_ref", "field": "项目名" }]
```
**Decision guide**: Fixed condition (e.g., "status is completed") → `constant`. Dynamic condition (e.g., "match current record's project ID") → `field_ref`.
### Constant value format by field type
The `value` inside `{ "type": "constant", "value": ... }` varies by field type:
| Field type | Constant value format | Example |
|-----------|----------------------|---------|
| `text` | String | `"已完成"` |
| `number` | Number | `100`, `0.8` |
| `datetime` / `created_at` / `updated_at` | String | `"ExactDate(2025-01-01)"`, `"ExactDate(2025-01-01 09:30)"`, `"Today"`, `"Yesterday"`, `"Tomorrow"` |
| `select` (`multiple=false/true`) | Option name array | `["Todo"]`, `["Todo", "Done"]` |
| `link` | Record reference array | `[{ "id": "rec_xxx" }]`, `[{ "id": "rec_xxx" }, { "id": "rec_yyy" }]` |
| `user` / `created_by` / `updated_by` | User reference array | `[{ "id": "ou_xxx" }]`, `[{ "id": "ou_xxx" }, { "id": "ou_yyy" }]` |
| `checkbox` | Boolean | `true`, `false` |
| `attachment` / `location` | Only `empty` / `non_empty` | value must be `null` or omitted |
| `auto_number` | Not supported for constant comparison | Use dynamic field\_ref instead |
| `formula` / `lookup` (exact type) | Follow the underlying type rules | — |
| `formula` / `lookup` (fuzzy type) | String | `"some text"` |
**`datetime` notes**:
- Supported datetime constant values are `ExactDate(...)`, `Today`, `Yesterday`, `Tomorrow`
- Date-only fields use `ExactDate(YYYY-MM-DD)`
- Fields that include time use `ExactDate(YYYY-MM-DD HH:mm)`
- For complex or relative date filtering, consider using a Formula field instead
### Dynamic field reference — set comparison semantics
When using `{ "type": "field_ref", "field": "..." }`, values from both sides are first **converted to sets** at runtime, then compared using set operations:
- **`==`**: Sets are exactly equal (strict matching)
- **`intersects`**: Sets have a non-empty intersection (most commonly used)
**Conversion rules by field type**:
| Field type | Converted to |
|-----------|-------------|
| `text` | Single-element string set |
| `number` / `auto_number` / `datetime` | Single-element number set |
| `select` (`multiple=false/true`) | Set of option name strings |
| `user` / `created_by` / `updated_by` | Set of user name strings |
| `link` | Set of linked records' primary field string representations |
| `formula` / `lookup` | The computed value set |
**Examples**:
- User field `["name1", "name2"]` **intersects** text `"name1"` → true; **==** text `"name1"` → false (sets not equal)
- User field `["name1"]` **==** text `"name1"` → true (single-element sets are equal)
- Link field referencing records → converted to primary field strings, then compared
### Supported operators
| Operator | Meaning | Applicable field types |
|----------|---------|-----------------|
| `==` | Equal (exact match) | All types |
| `!=` | Not equal | All types |
| `>` | Greater than | `number`, `datetime` |
| `>=` | Greater than or equal | `number`, `datetime` |
| `<` | Less than | `number`, `datetime` |
| `<=` | Less than or equal | `number`, `datetime` |
| `intersects` | Has intersection (non-empty overlap) | All types (most commonly used for dynamic field\_ref) |
| `disjoint` | No intersection | All types |
| `empty` | Field is empty | All types (value must be null or omitted) |
| `non_empty` | Field is not empty | All types (value must be null or omitted) |
### Constraints
- **Only one level of and/or** — nesting (e.g., `{ and: [{ or: [...] }] }`) is not supported
- **At least one condition** — empty conditions array will error
---
## Section 4: Aggregate Rules
| Aggregate | Common user phrasing | Select field should be | Result type |
|-----------|---------------------|----------------------|-------------|
| `sum` | "total" / "sum" / "cumulative amount" | `number` field (e.g., amount) | Number |
| `average` | "average" / "mean" | `number` field | Number |
| `max` | "maximum" / "latest" / "most recent" | `number` / `datetime` field | Same as source |
| `min` | "minimum" / "earliest" | `number` / `datetime` field | Same as source |
| `counta` | "count" / "how many" / "total number" | Any field | Number |
| `unique_counta` | "count distinct" / "how many different" | Field to deduplicate | Number |
| `unique` | "list distinct" / "which ones" / "show different" | Field to display | List |
| `raw_value` | "list all" / "show all values" (default) | Field to display | List |
**Common confusion**: `unique` returns a **deduplicated list**, `unique_counta` returns a **count**. "Which categories are involved" → `unique`; "How many categories" → `unique_counta`.
**Important**:
- Enum values are **snake_case lowercase**: `sum` not `Sum`, `average` not `Average`
- **Count is `counta`, NOT `count`** — this is the most common enum mistake
---
## Section 5: Hard Constraints
1. **Always write a filter**: The `where` field is required with at least one condition. Whenever the current table and source table have row-level correspondence, the condition should express that relationship.
2. **Lookup fields are read-only**: Cell values cannot be manually set.
3. **Create Lookup after all dependent fields exist**: The source table and referenced fields must exist before creating the Lookup field.
4. **Source table must be in the same Base**: Cross-Base lookups are not supported.
5. **Changing `from` requires changing `select`**: Updating the source table without updating the select field will error.
---
## Section 6: Decision Trees
### How to build the filter
```
Step 1: Analyze the filtering semantics in the user's request
"Count artworks per exhibition" → filter: belongs to exhibition = current exhibition
"Sum completed order amounts" → filter: status = completed AND project = current project
Step 2: Find the matching field pair
├─ Tables have a Link relationship?
│ ├─ Link is in source table → source.linkField matches current.primaryField
│ └─ Link is in current table → source.primaryField matches current.linkField
├─ Tables share same-meaning text/number field? → source.field matches current.field
└─ Also need constant filtering? → AND combination
```
### Which aggregate?
```
How to handle multiple matching records?
├─ Show all values as-is → raw_value (default)
├─ Show deduplicated list → unique
├─ Sum → sum
├─ Average → average
├─ Maximum / minimum → max / min
├─ Count records → counta
└─ Count distinct → unique_counta
```
---
## Section 7: Common Configuration Patterns
> Patterns are categorized by **filter matching method**. Aggregate choice is independent — see Section 4.
### Pattern 1: Aggregate from a linked table (Link is in the source table)
**Scenario**: "Count artworks per exhibition", "Sum order amounts per project"
When the source table has a Link pointing to the current table:
```
Exhibition table: ExhibitionName (primaryField) ← current table
Artwork table: ArtworkName (primaryField), ← source table (Link is here)
Exhibition (Link → Exhibition table)
```
```json
{
"type": "lookup",
"name": "Artwork Count",
"from": "Artwork table",
"select": "ArtworkName",
"aggregate": "counta",
"where": {
"logic": "and",
"conditions": [
["Exhibition", "intersects", { "type": "field_ref", "field": "ExhibitionName" }]
]
}
}
```
### Pattern 2: Reference a linked record's field (Link is in the current table)
**Scenario**: "Show supplier's contact person", "Display warehouse manager"
When the current table has a Link pointing to the source table:
```
Supplier table: SupplierName (primaryField), Contact (Text) ← source table
Inventory table: ProductName (primaryField), ← current table (Link is here)
Supplier (Link → Supplier table)
```
```json
{
"type": "lookup",
"name": "Supplier Contact",
"from": "Supplier table",
"select": "Contact",
"where": {
"logic": "and",
"conditions": [
["SupplierName", "intersects", { "type": "field_ref", "field": "Supplier" }]
]
}
}
```
### Pattern 3: Match by same-meaning field (no Link)
**Scenario**: "Sum order amounts per project" (tables share a "ProjectName" field but no Link)
```
Project table: ProjectName (primaryField) ← current table
Order table: OrderID (primaryField), ProjectName (Text), ← source table
Amount (Number)
```
```json
{
"type": "lookup",
"name": "Order Total",
"from": "Order table",
"select": "Amount",
"aggregate": "sum",
"where": {
"logic": "and",
"conditions": [
["ProjectName", "==", { "type": "field_ref", "field": "ProjectName" }]
]
}
}
```
### Pattern 4: Dynamic matching + constant filtering
**Scenario**: "Only count completed orders", "Only sum approved budgets"
Combine row-level matching with fixed-value filtering using `logic: "and"`:
```json
{
"type": "lookup",
"name": "Completed Order Amount",
"from": "Order table",
"select": "Amount",
"aggregate": "sum",
"where": {
"logic": "and",
"conditions": [
["Manager", "==", { "type": "field_ref", "field": "EmployeeName" }],
["Status", "==", { "type": "constant", "value": "Completed" }]
]
}
}
```
### Pattern 5: Date filtering with constant value
**Scenario**: "Look up orders created after 2025-01-01", "Sum today's sales"
```json
{
"type": "lookup",
"name": "Recent Orders",
"from": "Order table",
"select": "Amount",
"aggregate": "sum",
"where": {
"logic": "and",
"conditions": [
["ProjectName", "==", { "type": "field_ref", "field": "ProjectName" }],
["CreatedDate", ">=", { "type": "constant", "value": "ExactDate(2025-01-01)" }]
]
}
}
```
---
## Section 8: Anti-Pattern Collection
### Mistake 1: Omitting where (most common)
```json
// Wrong: no where, every row pulls all records
{ "type": "lookup", "name": "Artwork Count", "from": "Artwork table", "select": "ArtworkName", "aggregate": "counta" }
// Correct: where with Link relationship
{ "type": "lookup", "name": "Artwork Count", "from": "Artwork table", "select": "ArtworkName", "aggregate": "counta",
"where": { "logic": "and", "conditions": [
["Exhibition", "intersects", { "type": "field_ref", "field": "ExhibitionName" }]
]}}
```
### Mistake 2: Wrong value type — confusing constant vs field_ref
```json
// Wrong: using constant for a dynamic join
["ProjectName", "==", { "type": "constant", "value": "ProjectName" }]
// Correct: use field_ref for dynamic per-row matching
["ProjectName", "==", { "type": "field_ref", "field": "ProjectName" }]
```
### Mistake 3: Using `count` instead of `counta`
```json
// Wrong
{ "aggregate": "count" }
// Correct
{ "aggregate": "counta" }
```
### Mistake 4: Wrong case for aggregate values
```json
// Wrong
{ "aggregate": "SUM" }
{ "aggregate": "Sum" }
// Correct — snake_case lowercase
{ "aggregate": "sum" }
{ "aggregate": "average" }
```
### Mistake 5: Nested where conditions
```json
// Wrong: nesting not supported
{ "logic": "and", "conditions": [
{ "logic": "or", "conditions": [...] }
]}
// Correct: only one level
{ "logic": "and", "conditions": [cond1, cond2, cond3] }
```
### Mistake 6: Confusing Lookup with Link
The user says "aggregate order amounts" — use Lookup, not Link. Link establishes relationships; Lookup retrieves and aggregates data.
### Mistake 7: Using object format instead of tuple for conditions
```json
// Wrong: object format
{ "fieldRef": "Status", "operator": "is", "value": { "type": "constant", "value": "Done" } }
// Correct: tuple format [field, operator, value?]
["Status", "==", { "type": "constant", "value": "Done" }]
```
### Mistake 8: Missing `type` field
```json
// Wrong: no type field
{ "name": "Total", "from": "Orders", "select": "Amount", "aggregate": "sum", "where": { ... } }
// Correct: must include type
{ "type": "lookup", "name": "Total", "from": "Orders", "select": "Amount", "aggregate": "sum", "where": { ... } }
```
---
## Section 9: Constraint Summary
- `type` must be `"lookup"` — this field is required in the request body
- `where` is required with at least one condition — always specify a filter
- Conditions use **tuple format**: `[field, operator, value?]` — NOT object format
- Lookup fields are read-only — values cannot be manually set
- Source table and referenced fields must exist before creating the Lookup
- Condition field (first element of tuple) must reference a field in the source table, not the current table
- Where supports only one level of and/or — no nesting
- Aggregate values are snake_case lowercase: `sum`, `counta`, `unique_counta` (NOT `count`)
- Operators: `==`, `!=`, `>`, `>=`, `<`, `<=`, `intersects`, `disjoint`, `empty`, `non_empty`
- Table and field names must exactly match `+table-get` output
- `datetime` constant values use string format: `ExactDate(YYYY-MM-DD)` / `ExactDate(YYYY-MM-DD HH:mm)` / `Today` / `Yesterday` / `Tomorrow`
- `select` constant values use option names;
- `link` / `user` constant values use `{id}` object arrays

View File

@ -0,0 +1,549 @@
# Base role permission JSON SSOT
> **入口指南**: [lark-base-role-guide.md](lark-base-role-guide.md) | **相关命令**: `+role-create` · `+role-update` · `+role-get`
本文档是角色权限 JSONAdvPermBaseRoleConfig的单一事实来源SSOT`+role-create``+role-update` 构造 `--json` 参数时参考。
## 📋 目录
- [顶层结构 (AdvPermBaseRoleConfig)](#顶层结构-advpermbaseroleconfig)
- [角色类型 (RoleType)](#角色类型-roletype)
- [读取与更新角色](#读取与更新角色)
- [Base 级权限 (BaseRuleMap)](#base-级权限-baserulemap)
- [仪表盘权限 (DashboardRule)](#仪表盘权限-dashboardrule)
- [文档权限 (DocxRule)](#文档权限-docxrule)
- [数据表权限 (TableRule)](#数据表权限-tablerule)
- [表级权限 (TablePerm)](#表级权限-tableperm)
- [视图权限 (ViewRule)](#视图权限-viewrule)
- [字段权限 (FieldRule)](#字段权限-fieldrule)
- [记录权限 (RecordRule)](#记录权限-recordrule)
- [筛选条件 (FilterRuleGroup)](#筛选条件-filterrulegroup)
- [默认权限策略与风控规则](#默认权限策略与风控规则)
- [默认关闭项](#默认关闭项)
- [权限对象选择](#权限对象选择)
- [记录操作默认策略](#记录操作默认策略)
- [field_perms 构造 SOP](#field_perms-构造-sop)
- [视图权限默认策略](#视图权限默认策略)
---
## 顶层结构 (AdvPermBaseRoleConfig)
```json
{
"role_name": "财务审核员",
"role_type": "custom_role",
"base_rule_map": { "copy": false, "download": false },
"table_rule_map": { "订单表": { "perm": "edit", "...": "..." } },
"dashboard_rule_map": { "销售看板": { "perm": "read_only" } },
"docx_rule_map": { "文档A": { "perm": "edit", "allow_download": true } }
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|----|------|
| `role_name` | string | 是 | 角色名称,不能为空 |
| `role_type` | string | 是 | 角色类型,见 [RoleType](#角色类型-roletype) |
| `base_rule_map` | map\<string, bool\> | 是 | Base 级权限,见 [BaseRuleMap](#base-级权限-baserulemap) |
| `table_rule_map` | map\<string, TableRule\> | 否 | 数据表权限key 为表名 |
| `dashboard_rule_map` | map\<string, DashboardRule\> | 否 | 仪表盘权限key 为仪表盘名称 |
| `docx_rule_map` | map\<string, DocxRule\> | 否 | 文档权限仅单品模式key 为文档名称 |
---
## 角色类型 (RoleType)
| 值 | 说明 |
|------|------|
| `editor` | 系统角色:编辑者 |
| `reader` | 系统角色:阅读者 |
| `custom_role` | 自定义角色 |
**注意**:
- 创建接口(`+role-create`)仅支持 `custom_role`
- 更新接口(`+role-update`)支持 `editor` / `reader` / `custom_role`
---
## 读取与更新角色
- `+role-list` 用于定位角色,返回角色摘要;系统角色和自定义角色都可能出现在列表中。
- `+role-get` 返回完整权限配置。更新前先用它确认当前 `role_name``role_type` 和已有权限结构。
- `+role-update` 是 delta merge只提交需要变更的字段`role_name``role_type` 仍要带当前值,避免误改角色身份信息。
- `+role-delete` 仅适用于自定义角色;系统角色可以在权限上限内调整配置,但不可删除。
---
## Base 级权限 (BaseRuleMap)
1. 默认值均为 `false`,当需要启用时设置为 `true`
2. 在新增角色和修改角色时需要默认带上这个字段,**严禁**在用户未明确要求的情况下将其设置为 `true`
```json
{
"base_rule_map": {
"copy": true,
"download": false
}
}
```
| Key | 说明 |
|-----|------|
| `copy` | 允许复制多维表格内容 |
| `download` | 允许创建副本、下载、打印多维表格 |
---
## 仪表盘权限 (DashboardRule)
```json
{
"dashboard_rule_map": {
"销售看板": { "perm": "read_only" },
"内部数据": { "perm": "no_perm" }
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `perm` | string | 仪表盘权限 |
**perm 可选值**:
| 值 | 说明 |
|----|------|
| `read_only` | 仅可阅读 |
| `no_perm` | 无权限 |
---
## 文档权限 (DocxRule)
> ⚠️ 仅在单品模式(`is_base_solo = true`)下可用。
```json
{
"docx_rule_map": {
"文档A": { "perm": "edit", "allow_download": true },
"文档B": { "perm": "read_only" }
}
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `perm` | string | 是 | 文档权限 |
| `allow_download` | bool | 否 | 是否允许下载/导出 |
**perm 可选值**:
| 值 | 说明 |
|----|------|
| `edit` | 可编辑 |
| `read_only` | 仅可阅读 |
| `no_perm` | 无权限 |
---
## 数据表权限 (TableRule)
```json
{
"table_rule_map": {
"订单表": {
"perm": "edit",
"view_rule": { "..." : "..." },
"record_rule": { "..." : "..." },
"field_rule": { "..." : "..." }
},
"用户表": {
"perm": "read_only"
}
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `perm` | string | 表级权限,见 [TablePerm](#表级权限-tableperm) |
| `view_rule` | ViewRule | 视图权限配置 |
| `record_rule` | RecordRule | 记录权限配置 |
| `field_rule` | FieldRule | 字段权限配置 |
**注意**: 当 `perm``no_perm` 时,`view_rule``record_rule``field_rule` 均无须再设置。
---
### 表级权限 (TablePerm)
| 值 | 说明 |
|----|------|
| `manage` | 可管理 |
| `edit` | 可编辑 |
| `read_only` | 仅可阅读 |
| `no_perm` | 无权限(此时不能再设置视图、记录和字段的权限) |
---
### 视图权限 (ViewRule)
```json
{
"view_rule": {
"allow_edit": true,
"visibility": {
"all_visible": false,
"visible_views": ["表格视图", "看板视图"]
}
}
}
```
| 字段 | 类型 | 说明 |
|------|------|----------------------------|
| `allow_edit` | bool | 可新增、删除、修改视图;表权限为 `edit` 时默认为 `true`,表权限为 `read_only` 或用户明确限制时为 `false` |
| `visibility` | object | 可见的视图配置 |
| `visibility.all_visible` | bool | 是否全部可见 |
| `visibility.visible_views` | []string | 可见视图名称 列表 |
**⚠️ 核心规则:`view_rule` 必须同时包含 `allow_edit``visibility` 两个字段,缺一不可。**
输出 `view_rule` 时,**必须**使用以下完整结构,根据场景选择对应模板:
```json
// 情况 A表权限为 edit 且用户未明确限制 → allow_edit 默认为 true全部可见
{
"view_rule": {
"allow_edit": true,
"visibility": {
"all_visible": true
}
}
}
// 情况 B表权限为 read_only或用户明确说不可编辑视图 → 全部可见、不可编辑
{
"view_rule": {
"allow_edit": false,
"visibility": {
"all_visible": true
}
}
}
// 情况 C用户提及了具体视图 → 仅指定视图可见allow_edit 仍按 A/B 规则判断)
{
"view_rule": {
"allow_edit": true,
"visibility": {
"all_visible": false,
"visible_views": ["表格视图", "看板视图"]
}
}
}
```
**注意**:
-`all_visible``false` 时,`visible_views` 不可为空,必须指定至少一个可见视图
- `biz_type``query_form_view` 的视图不可放在 `visible_views` 中(不能配置可见性)
---
### 字段权限 (FieldRule)
```json
{
"field_rule": {
"field_perm_mode": "specify",
"field_perms": {
"金额": "edit",
"备注": "read",
"密码": "no_perm"
},
"allow_edit_and_modify_option_fields": [],
"allow_edit_and_download_file_fields": []
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `field_perm_mode` | string | 字段权限模式 |
| `field_perms` | map\<string, string\> | 字段名 → 权限,仅 `field_perm_mode``specify` 时有效 |
| `allow_edit_and_modify_option_fields` | []string | 允许增删改选项的字段名列表 |
| `allow_edit_and_download_file_fields` | []string | 允许下载附件的字段名列表 |
**field_perm_mode 可选值**:
| 值 | 说明 |
|----|------|
| `all_edit` | 所有字段可编辑,但选项不可增删改 |
| `all_read` | 所有字段可读 |
| `specify` | 指定字段权限(可进一步设置 `field_perms` 和选项增删改权限) |
| `no_perm` | 无权限 |
**field_perms 中单个字段的权限值**:
| 值 | 说明 |
|----|------|
| `edit` | 可编辑(含新增和阅读权限) |
| `create` | 可新增(含阅读权限) |
| `read` | 可阅读 |
| `no_perm` | 无权限 |
**⚠️ field_perms 重要规则**:
1. 写入前必须先查看字段的 `type`
2. `formula` / `lookup` / `auto_number` 类型字段**必须强制**降级为 `read``no_perm`**严禁**设为 `edit`
3. 必须输出除 4 个系统字段外的所有字段
4. `allow_edit_and_modify_option_fields`:仅当用户明确要求"允许增删改选项"时才配置,否则必须为空数组 `[]`。仅支持 `select` 类型字段
5. `allow_edit_and_download_file_fields`:用户没有要求时不要设置,且仅 `field_perm_mode``specify` 时才能设置
---
### 记录权限 (RecordRule)
```json
{
"record_rule": {
"record_operations": ["add"],
"edit_filter_rule_group": {
"conjunction": "and",
"filter_rules": [
{
"conjunction": "and",
"filters": [
{
"field_name": "部门",
"operator": "is",
"filter_values": ["财务部"]
}
]
}
]
},
"other_record_all_read": true
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `record_operations` | []string | 记录操作权限,仅 `TablePerm = edit` 时有效 |
| `edit_filter_rule_group` | FilterRuleGroup | 可编辑记录的筛选条件,范围为所有记录时此字段为空 |
| `other_record_all_read` | bool | 是否可阅读所有记录。都可读时为 `true`,其他情况为 `false` |
| `read_filter_rule_group` | FilterRuleGroup | 可阅读记录的额外筛选规则。仅当可阅读范围与可编辑范围不一致时设置(依赖 `other_record_all_read = false` |
**record_operations 可选值**:
| 值 | 说明 |
|----|------|
| `add` | 可新增记录 |
| `delete` | 可删除记录 |
---
### 筛选条件 (FilterRuleGroup)
```json
{
"conjunction": "and",
"filter_rules": [
{
"conjunction": "and",
"filters": [
{
"field_name": "部门",
"operator": "is",
"filter_values": ["财务部"]
}
]
}
]
}
```
**FilterRuleGroup 结构**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `conjunction` | string | 逻辑连接词:`and` / `or` |
| `filter_rules` | []FilterRule | 筛选规则数组 |
**FilterRule 结构**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `conjunction` | string | 逻辑连接词,默认 `and` |
| `filters` | []Filter | 筛选条件数组 |
**Filter 结构**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `field_name` | string | 是 | 字段名。仅限 `can_filter``true` 的字段。若服务端要求当前用户类条件,可按 API 返回结构处理 |
| `operator` | string | 是 | 操作符,见下表 |
| `field_type` | string | 否 | 通常由服务端 filterFiller 补全Agent 判断字段类型时以 `+field-list` / 字段操作接口的 `type` 为准,常见可筛选类型包括 `select``user``created_by``number` 及部分 `formula` / `lookup` |
| `reference_type` | string | 条件 | 引用类型。`field_type` 为公式或引用字段时必须赋值,其他情况不能赋值 |
| `filter_values` | []string | 条件 | 筛选值。`operator``isEmpty` / `isNotEmpty` 时不设置,字段类型为 `user` 时也无需设置,其他情况必须设置。值为选项的 `name` |
| `field_ui_type` | string | 条件 | 该字段有值时一定要填 |
| `is_invalid` | bool | 否 | 判断筛选条件是否有效 |
**operator 可选值**:
| 值 | 说明 |
|----|------|
| `is` | 等于 |
| `isNot` | 不等于 |
| `contains` | 包含 |
| `doesNotContain` | 不包含 |
| `isEmpty` | 为空 |
| `isNotEmpty` | 不为空 |
| `isGreater` | 大于 |
| `isGreaterEqual` | 大于等于 |
| `isLess` | 小于 |
| `isLessEqual` | 小于等于 |
**注意**:
- `field_type``field_ui_type``reference_type` 在创建/更新角色时由服务端 filterFiller 自动补全,客户端通常只需传 `field_name``operator``filter_values`
---
## 默认权限策略与风控规则
构造角色配置 JSON 时,采用 **默认拒绝与权限最小化** 策略。用户未明确提及的权限一律不开放,不因"合理猜测""常见做法"主动扩展权限范围。
### 默认关闭项
以下能力在用户未明确说明时**默认关闭**
| 能力 | 默认值 | 开启条件 |
|------|--------|----------|
| 未提及的数据表的任何访问 | `no_perm` | 用户明确提及该表 |
| 仪表盘访问 | 不配置 | 用户明确提及该仪表盘 |
| `base_rule_map.copy` | `false` | 用户明确要求"允许复制" |
| `base_rule_map.download` | `false` | 用户明确要求"允许下载/打印/副本" |
### 默认开启项(条件性)
以下能力在特定条件下**默认开启**,用户明确限制时才排除:
| 能力 | 默认值 | 排除条件 |
|------|--------|----------|
| `record_operations` 中的 `delete` | **包含**`perm = edit` 时) | 用户明确限制时才排除 |
| `view_rule.allow_edit` | **`true`**`perm = edit` 时) | 用户明确限制"不可编辑视图"或 `perm = read_only` 时设为 `false` |
---
### Editor / Reader 的权限上限规则
1. 对 Editor 与 Reader系统允许修改其权限配置但同时施加以下封顶约束
2. Reader 的任一权限项 不允许超过「仅可阅读」
3. Reader 不允许拥有任何可编辑、可新增、可删除相关权限; Editor 的权限可被修改,但其能力范围受高级权限能力封顶。
### 权限对象选择
**注意**:
- 仅对用户明确指向的权限对象生成配置(明确提及的表名、仪表盘名,或可解析为唯一对象的指代如"当前表""这张表"
- **严禁**基于业务常识、岗位职责、名称相似性或其他角色的历史配置推断或扩展权限对象
- 用户未明确提及的对象不生成任何权限配置,视为 `no_perm`
---
### 记录操作默认策略
**注意**:
- 用户未提及时,表权限为 `edit` 时默认同时包含 `add``delete`,默认不包含 `delete` 的情况仅适用于用户明确限制操作的场景
- 阅读范围默认对齐编辑范围:用户仅描述可编辑范围、未说明阅读范围时,可阅读范围与可编辑范围保持一致,不主动扩大
- 当可读范围与可编辑范围一致时,**不得**生成 `read_filter_rule_group`;应设置 `other_record_all_read = false``read_filter_rule_group = null`
**⚠️ 记录操作限制**:
1. `perm``read_only` 时,`record_rule.record_operations` **只能为空**
2. 同步表(`is_sync = true`**严禁**新增和删除记录
---
### field_perms 构造 SOP
在生成 `field_perms` 时,**严禁**依赖模糊的"继承"概念,必须按以下步骤执行:
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1. 基准设定 | `perm = edit` → 全部字段预设 `"edit"``perm = read_only` → 全部预设 `"read"` | 基于 `base_table_info` 中的全量字段 |
| 2. 物理降级 | `formula` / `lookup` / `auto_number` 及系统字段 → 强制降级为 `"read"` | 不可变字段严禁设为 `edit` |
| 3. 用户覆盖 | 仅对用户**显式指定**了特定权限的字段应用 `no_perm` / `read` / `create` | 未显式指定的保持基准值 |
| 4. 反筛选误判 | 用于 `filter_rules` 的字段,若基准为 `"edit"` 且用户未要求降级 → **保持 `"edit"`** | 筛选条件不影响字段可编辑性 |
| 5. 筛选依赖兜底 | 出现在 `filter_rules` 中的字段**不允许**遗漏,权限至少为 `"read"` | 最终校验步骤 |
**⚠️ field_perm_mode 选择规则**:
1. 用户以"所有字段""全字段"等整体性表述描述且不要求选项增删改时,**必须**使用 `all_edit` / `all_read`**严禁**变为逐字段 `specify`
2. 仅在以下情况使用 `specify`:用户明确提出字段级差异需求、不同字段权限目标存在显著差异、或明确要求配置选项增删改权限
3. 系统字段硬性约束导致的自动降级**不视为**差异,不触发 `specify`
4. 对"仅""只能""部分"等约束定语,范围外的字段按定语的反方向设置
**⚠️ 同步表限制**: `is_sync = true` 的表**严禁**设置字段为 `edit``create`
---
### 视图权限默认策略
**判断流程(必须按顺序执行,命中即停)**:
1. **先判断用户是否提及了具体视图名称**(如"看板视图可见""甘特图不可编辑"等)
- **是** → `all_visible = false``visible_views` 仅包含用户明确提及为"可见"的视图名称(非 viewID未提及的视图视为不可见
- **否**(用户完全未提及任何视图)→ `all_visible = true`
2. `allow_edit` 在表权限为 `edit` 时**默认为 `true`**;仅当用户明确限制"不可编辑视图"时才设为 `false`。设为 `true` 时仍**必须**包含 `visibility` 字段(参考视图权限 情况 A
3. `all_visible``false` 时,`visible_views` **不可为空**,必须至少包含一个视图
**❌ 常见错误 — 缺少 `visibility` 字段:**
```json
// 错误!缺少 visibility
{ "view_rule": { "allow_edit": false } }
```
**✅ 正确写法:**
```json
// 即使全部可见,也必须显式写出 visibility
{ "view_rule": { "allow_edit": false, "visibility": { "all_visible": true } } }
```
---
### 字段类型与筛选算子的强约束关系
当字段被用于记录筛选条件时,字段操作接口返回的 `type` 与可用算子存在固定绑定关系:
**`user` / `created_by` 类型字段:**
- 仅允许使用 `contains` 算子
- 不允许使用 `is``isNot` 等精确匹配算子
- 这是当前成员匹配模式,筛选条件中无需填写具体成员值;不要在 `filter_values` 中写入姓名或用户 ID
**`select` (`multiple=false`) 类型字段:**
- `is``isNot` 算子仅允许用于匹配**单一选项**,不得用于多个值
- 当用户表达"字段值等于/不等于某一个具体选项"(如"出勤状态不等于出勤"Agent 必须使用 `is` / `isNot`,且 filter_values 仅包含单一值。
- 当用户表达"字段值等于/不等于多个选项集合"(如"学历不是专科和其他"Agent 必须使用 `contains` / `doesNotContain`,并将多个选项填入 filter_values。
- `contains` / `doesNotContain`中的filter_values可包含多个值表示或关系
**`select` (`multiple=true`) 类型字段:**
- `is` / `isNot`filter_values 允许填写多个选项
- 当 operator = is 且勾选 A、B 时,语义为该字段**同时包含** A 和 BA&B不是"等于 A 或等于 B"
- 当用户表达"包含任一选项"时,除了可以使用 contains 实现外,也可以使用 is 并且配套通过 filter_rules.conjunction = or 实现
- `contains` / `doesNotContain`:用于表达"包含任一选项/不包含任一选项"filter_values 可填写多个选项(系统按"任一匹配"处理);若要表达"等于 A 或等于 B",应拆成多条筛选条件并用「或」组合。
**百分比字段**
- 对于 query 中“数字”的筛选条件时,如果涉及到百分比,要原封不动地还原用户给你的数值(百分比都变成小数)。比如“大于 20%”则变成“大于 0.2”、“xx 率小于 60”则变成“小于 0.6”。
### 被用于筛选的字段的 field_perms 权限强制要求
当某字段(系统字段没有此要求)被用于「满足特定条件的记录」中的筛选条件时,系统将根据当前数据表权限与记录权限,自动施加以下**不可变约束**
**筛选字段的读写一致性:**
- 若表权限为 edit且字段类型属于【可编辑字段】则筛选字段必须保持 edit 权限,除非用户显式要求降级。
- 严禁因为字段被用作筛选条件而将其降级为 read。筛选条件仅要求字段可见不要求字段只读。
**新增记录时的字段最低权限:**
- 当且仅当记录权限包含「可新增记录」时字段至少为可新增create用于保证在新增记录时筛选条件字段可被正确写入。
- 若当前记录权限为「仅可阅读」,则不触发该约束。
**字段是否可编辑edit不作强制要求**,由具体权限方案决定,不属于 infra 强制约束范围。
上述由系统自动施加的字段权限,不可被手动取消或降级。