first commit

This commit is contained in:
2026-09-02 11:44:52 +08:00
commit 0c8fa2653e
309 changed files with 57278 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
---
name: dingtalk-aitable
description: 钉钉 AI 表格(多维表)。Use when 用户说 AI表格/多维表/数据表/base/table/建表/查记录/写数据/字段/记录增删改查/筛选/排序/公式/模板搜索/批量导入CSV或JSON/导出/仪表盘/图表/上传附件到表格/按字段类型建表/数据源/创建数据源/更新数据源配置/触发数据源同步/按任务 ID 查询同步状态/获取数据源配置/列出数据源可用来源/获取数据源可同步字段/审批数据同步。不做电子表格单元格读写(走 dingtalk-misc)、文档编辑(走 dingtalk-doc);听记待办入表先用 dingtalk-minutes 提取,再由本 skill 写入。命令前缀:dws aitable。
metadata:
cli_version: ">=0.2.14"
category: product
requires:
bins:
- dws
---
# 钉钉 AI 表格 Skill
<!-- DWS_RUNTIME_CONTRACT_START -->
## 最小 DWS 执行契约
- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。
- 已知 leaf 直接执行。只有参数不确定时,最多读取一次 `dws schema --cli-path "aitable <leaf>" --compact --format json`;仅当该 compact leaf Schema 与 Cobra 实际不一致时,才读取同一 leaf 的 `dws aitable <leaf> --help`。禁止通过父级 Help、`dws aitable --help` 或完整 Catalog 探索命令。
- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。
- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。
- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。
- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;本轮用户已明确要求执行、目标与影响无歧义的非破坏性写操作时,该明确指令就是本次确认,首次调用直接携带 Runtime 所需的 `--yes`,不先制造 `confirmation_required`。删除、停用自动化等破坏性或高风险动作仍须先说明对象、动作与影响并取得独立确认。
- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。
- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。
- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dingtalk-shared` 中对应 reference;不要连续猜测替代命令。
<!-- DWS_RUNTIME_CONTRACT_END -->
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcut 发现(按需)
`aitable` 当前有 100 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知 leaf 直接执行。只有参数不确定时,最多读取一次 `dws schema --cli-path "aitable <leaf>" --compact --format json`;仅当该 compact leaf Schema 与 Cobra 实际不一致时,才读取同一 leaf 的 `dws aitable <leaf> --help`。禁止用父级 Help、产品 Help 或完整 Catalog 探索命令;一个 Case 一旦读取 Reference,就不再读取 Help 或第二个 Reference。
仅当根路由、精确 task reference 和 `references/aitable.md` 的低频原子索引都无法定位能力时,才执行 `dws shortcut list --service aitable --format json` 做最终回退;不要为已知意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## Golden Route
已有 ID 直接使用;完整 URL 先解析;名称先唯一解析为稳定 ID。零命中或多候选时停止,不默认选第一项。
| 用户意图 | 唯一推荐入口 | 关键边界 |
|---|---|---|
| 从 URL 解析稳定 ID | `dws aitable +url-resolve --url <URL>` | 只解析 URL 中已有的 baseId/tableId/viewId/recordId,不做远端名称搜索 |
| 按名称唯一定位并操作 Base/Table | `dws aitable +resolve-base --name <名称>``dws aitable +resolve-table --base <ID> --name <表名>` | 默认精确匹配;只有用户明确接受模糊匹配时才加 `--fuzzy` |
| 浏览 Base 下的数据表 | `dws aitable +list-tables --base <ID>` | 只返回 tableId/tableName,不加载字段 |
| 搜索 Base 候选或检查是否存在 | `dws aitable +base-search --query <关键词>` | 用户说“搜索/找一下/候选/如果没有就创建”时直接走本入口,不先调用 `+resolve-base`AITable 上下文中的 Base 名称不得路由到 `dws aisearch person` |
| 新建 Base 与整套表字段 | `dws aitable +base-bootstrap --name <名称> --tables '[{"name":"<表名>","fields":[{"fieldName":"<字段名>","type":"text"}]}]'` | 表对象键必须是 `name`,不是 `tableName`;字段使用 `fieldName/type/config`;参数已足够时直接执行,不读 Reference 或 Help |
| 已有 Base 新建一张表与字段 | `dws aitable +table-bootstrap --base-id <ID> --name <表名> --fields '<JSON数组>'` | 字段使用 `fieldName/type/config`;自动按 15 个字段分片并读回验证 |
| 读取字段目录或完整配置 | `dws aitable field list --base-id <B> --table-id <T>` / `dws aitable +field-get --base-id <B> --table-id <T>` | 只需 fieldId/name/type 用 `field list`;需要 config 用 `+field-get`;不存在 `+field-list``+list-fields` |
| 查询、筛选、排序或字段投影 | `dws aitable +record-query --base-id <ID> --table-id <ID> [--record-ids <IDs>] [--field-ids <IDs>] [--filters <JSON>] [--sort <JSON>] [--query <关键词>]` | 用户要求“只返回/仅查看”指定字段时必须传对应 `--field-ids`,不能只在最终文本删列;明确要求全量时改用原子 `record query --all --page-limit <N>` |
| 查询一条记录的变更历史 | `dws aitable +record-history-list --base-id <ID> --table-id <ID> --record-id <ID>` | 已知 recordId 时直接执行;不要调用 Help、产品 Catalog 或全量 Schema 寻找 history 命令 |
| 新增单条或批量记录 | `dws aitable record create --base-id <ID> --table-id <ID> --records <JSON>` | 当前无 `+record-create`;写前取字段定义,写后按新 ID 回读 |
| 更新已知 recordId | `dws aitable +record-update --base-id <ID> --table-id <ID> --records <JSON>` | 自动分片并读回;只传需修改字段 |
| 按业务唯一键同步 | `dws aitable +record-upsert-by-key --base-id <ID> --table-id <ID> --key-field-id <ID> --key-value <值> --cells <JSON>` | 0 条创建、1 条更新、多条停止;非字符串键改用 `--key-value-json` |
| 按条件批量修改 | `dws aitable +record-bulk-patch --base-id <ID> --table-id <ID> --query <关键词> --patch <JSON> --max-matches <N>` | 也可用 filters/record-ids 选范围;禁止无边界整表写 |
| 删除整个 Base | `dws aitable +base-delete --base-id <ID>` | 先通过只读命令确认真实 ID;按 Runtime confirmation 执行,不用 Drive 删除同名节点 |
| 删除字段 | `dws aitable +field-delete --base-id <ID> --table-id <ID> --field-id <ID>` | 先读取字段目录并确认非主字段;按 Runtime confirmation 执行 |
| 查询/创建记录主键文档 | `dws aitable +record-primary-doc-get|+record-primary-doc-create ...` | create 必须传 primaryDoc 类型的 `--field-id`;正文操作切到 Doc |
| 生成记录分享链接并发送给联系人 | `dws aitable +record-share-links --base <B> --table <T> --record-ids <IDs>``dws chat +dm --to <姓名> --text <完整链接文本>` | AITable 只生成链接;用户要求“发送”时必须加载 `dingtalk-chat` 并对每位收件人完成真实发送,不能停在联系人解析 |
| 创建 View / Dashboard / Chart 或导入文件 | 对应 leaf / `+import-*` | 根 Skill 参数足够则直接执行;复杂配置最多读取一个对应操作 Reference,不读取通用索引 |
| 调整视图列顺序 | `dws aitable view update visible-fields --base-id <ID> --table-id <ID> --view-id <ID> --field-ids <完整有序IDs>` | 先读取字段和当前完整列数组,固定主字段在首位,写后回读精确校验 |
| 创建/修改图表前取配置 | `dws aitable +chart-widgets-example` | 命令返回所有图表类型示例;已有合法 config 时直接 create/update |
| Base 内 Section/节点移动 | `dws aitable +section-*` | Table/Dashboard/Section 是 Base 内 nsheet 节点,不是独立 Drive 节点 |
| 接入外部数据源(审批等) | `dws aitable +datasource-list-sources --base-id <ID> --datasource-type OA` → 解析 result 构造 sourceConfig → `dws aitable +datasource-create --base-id <ID> --datasource-type OA --source-config '<JSON>'` | 当前仅支持 OA 审批;sourceConfig 中 processCode/name/iconUrl/url 须从 list-sources 原样透传;创建后用 `+datasource-sync-status` 查同步结果 |
### 常用 leaf 直达
参数已知时直接执行,不探测 Help/CatalogBase 查看/改名用 `+base-get` / `+base-update`;模板搜索用 `+template-search`,再把真实 templateId 交给 `base create --template-id`Table 查看/更新用 `+table-get` / `+table-update`;视图创建/复制用 `view create` / `+view-duplicate`;仪表盘创建/更新/读回用 `dashboard create` / `+dashboard-update` / `+dashboard-get`;表单分享用 `+form-share-update` / `+form-share-get`;查看自动化用 `+workflow-list`;数据源查看来源用 `+datasource-list-sources`,获取字段用 `+datasource-get-fields`,创建/更新/同步/查状态/查配置用 `+datasource-create` / `+datasource-update` / `+datasource-sync` / `+datasource-sync-status` / `+datasource-get-config`
### 低频入口
字段配置用 `+field-*`;删记录用 `+record-delete`;附件用 `+attachment-*`。批量分享记录用 `+record-share-links --base <B> --table <T> --record-ids <IDs>`。其余能力使用同名前缀 leaf。
## 当前最短路径
- 已有 ID 直接使用;URL 只解析一次;“唯一定位并操作”用 `+resolve-base` / `+resolve-table`,“搜索候选/存在性检查”直接用 `+base-search`,两条路径不要串行探测。filters/sort 缺 fieldId 时才读取字段目录。
- Golden Route 已给出准确命令和参数时直接执行;不预读或默认读取通用 `references/aitable.md`。只有操作参数、JSON 结构或恢复语义确实缺失时,才读取下方一个精确操作 Reference。
- Shortcut 已含分片或验证时不重复拆步;已有 Base 新建完整表结构直接用 `+table-bootstrap`
- 单产品线性任务直接执行,不创建 TodoWrite;只有跨产品或多个独立分支的长任务才建计划,并且只在阶段切换时更新,不在每条 CLI 后刷新状态。
- 用户要求资源名带当前时间戳时只取一次并在 Base、Table、Dashboard 等名称中复用同一值;不要为每个资源分别取时间。
- JSON 已返回所需字段时立即复用;不得为寻找同一字段改用 `--verbose``raw``pretty` 重复请求。
- 数据源创建前必须先 `+datasource-list-sources` 获取 processCode 等透传字段,不要凭记忆或猜测构造 sourceConfig。
## 记录输入与结果
- `cells` key 用当前 fieldId;大 JSON 用相对 `--records-file`。filters 顶层为 `and|or`sort 使用 `direction`;复杂条件读 [filter-sort](references/aitable/aitable-filter-sort.md)。
- 建表字段类型使用真实枚举:单选为 `singleSelect`;人民币货币字段使用 `type:"currency"``config:{"currencyType":"CNY","formatter":"FLOAT_2"}`,不要猜 `select``config.symbol`
- 用户限定返回字段时,先复用当前字段目录中的真实 fieldId,最终 `+record-query` 必须带 `--field-ids <ID1,ID2>`;工具层投影是业务要求和 token 控制的一部分,不能用最终答复二次过滤替代。
- 按真实字段类型写值,只读字段不得写入。
- 新建从 `data.newRecordIds[]` 取 ID,再用 `+record-query --record-ids` 回读;若用户同时限定列,回读命令一并传 `--field-ids`
- 批量结果检查 completed/failed、verification、checkpoint`partial_success` 不是完成。全量查询使用原子 `record query --all` 并检查 `hasMore`;只有 `hasMore=false`,或按指定 ID 全命中时,才声称结果完整。
- 写入效果未知时回读,不重放成功批次。
## 安全边界
- 删除不可逆,按 Runtime confirmation 核对真实目标;`base list` 只是最近访问。字段零/多候选、类型不明时停止;多批写保留已完成批次和续跑位置。
- 数据源 `+datasource-create` / `+datasource-update` 会触发真实数据同步(全量),执行前确认目标 Base 和 sourceConfig 无误。`+datasource-sync` 同理,单次最多 5 张表。
## 按需加载
每个 Case 最多读取一个操作 Reference。Golden Route 参数足够时读取零个并直接执行;一旦读取了一个 Reference,本 Case 不再读取第二个 Reference、通用 `aitable.md`、产品级 Catalog 或 Help。
| 触发条件 | Reference |
|---|---|
| 记录 CRUD、字段值格式 | [record-ops](references/aitable-record-ops.md) |
| 记录主键文档 | [primary-doc](references/aitable/aitable-primary-doc.md) |
| filters/sort/date 操作符 | [filter-sort](references/aitable/aitable-filter-sort.md) |
| 字段创建或复杂配置 | [field](references/aitable/aitable-field.md) |
| 导入导出任务恢复 | [export-import](references/aitable/aitable-export-import.md) |
| 视图列顺序、筛选、排序、冻结 | [view-config](references/aitable/aitable-view-config.md) |
| Base 内 Section/节点移动或清理 | [section](references/aitable-section.md) |
| 图表配置 | [dashboard-chart](references/aitable/aitable-dashboard-chart.md) |
| 附件、表单、工作流 | 读取 `references/aitable/` 下对应的一个精确文件 |
| 数据源接入、同步管理、sourceConfig 构造、同步审批数据到 AI 表格 | [datasource](references/aitable/aitable-datasource.md) |
| 产品边界不明确 | [intent-guide](references/intent-guide.md) |
| 无法匹配上述任何精确 reference 的原子能力 | [aitable.md](references/aitable.md) 的对应章节 |
不要预加载这些 reference。`references/aitable.md` 只在根路由和精确 task reference 都无法定位原子能力时读取对应章节;完整 Shortcut Catalog 仅在该索引仍无法定位时使用。每个 Case 最多读取一个 Reference,禁止连读。
## 错误最短路径
1. 零/多候选、字段歧义或分页不完整:停止并返回证据;需要后续页时只透传真实 `nextCursor`
2. 类型错误只复核目标字段,不删字段或丢输入;`partial_success` 从 checkpoint 续跑,未知写入先回读。
3. 错误包含 `actions` / `available_flags` 时只执行其中的 `next_command`;同一操作最多做一次有证据的参数修正。`retryable=false` 或目标 ID 类型不符时停止,不把 Drive/Wiki/Space/子节点 ID 轮流代入试错。
4. 数据源同步 `errorCode=4014` 为幂等冲突(同步运行中重复触发),标记 FAILED 但可稍后重试;非数据源表(sync=false)触发 sync 会返回参数错误,先用 `+base-get` 确认 sync=true。
## 跨产品边界
- Excel 式单元格、区域和公式操作 → `dingtalk-misc` 的 Sheet。
- Base 作为整体在普通文件夹间移动或做外层存储重命名 → Drive;Base 结构复制/删除,以及 Base 内 Table、Dashboard、Section 的创建、复制、移动、重命名、删除 → AITable。
- 记录主键文档正文 → 取得真实 nodeId 后切 `dingtalk-doc`
@@ -0,0 +1,11 @@
# 数据分析
> 本场景所有 recipe 均为 full。
| Recipe | 行动指南(固定路线) |
|--------|-------------------|
| read-aitable | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId`<br>3. `aitable record query --base-id <baseId> --table-id <tableId>` → 取记录(分页)<br>  需要筛选时 `--filters` 格式见 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md),根节点必须是 `{"operator":"and\|or","operands":[...]}`<br>4. 总结数据 |
| generate-data-report | 1. 同 read-aitable 步骤 1-3<br>2. 按[「多源并行采集」](recipes/conventions.md#多源并行采集公共模式)执行 → 补充背景<br>3. `doc create --name "<报告名>" --content "<分析报告>"` |
| create-aitable-record | **批量导入优先**`python scripts/import_records.py <baseId> <tableId> data.csv\|data.json [batch_size]`(自动分批创建)<br>单条/少量:1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId` 与类型<br>3. `aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":"值"}}]'` |
| update-aitable-record | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable record query --base-id <baseId> --table-id <tableId>` → 取 `recordId`**先展示让用户确认**<br>3. `aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<recordId>","cells":{...}}]'` |
| search-aitable-template | 1. `aitable template search --query "<关键词>"` → 取 `templateId`<br>2. 用户选定<br>3. `aitable base create --name "<表格名>" --template-id <templateId>` → 取 `baseId` |
@@ -0,0 +1,78 @@
# AI 表格记录操作
仅在根 Skill 的记录 Golden Route 参数不足,或需要字段值格式、删除、历史、分享、附件细节时读取。本文件不负责 Base/Table 选路。
## 查询
```bash
dws aitable +record-query --base-id <B> --table-id <T> --record-ids <R1,R2>
dws aitable +record-query --base-id <B> --table-id <T> --record-ids <R1,R2> --field-ids <F_NAME,F_STATUS>
dws aitable +record-query --base-id <B> --table-id <T> --query "关键词" --limit 100
dws aitable +record-query --base-id <B> --table-id <T> --filters '<JSON>' --sort '<JSON>'
dws aitable record query --base-id <B> --table-id <T> --filters '<JSON>' --all --page-limit 50
```
- `record-ids` 用于稳定 ID 精确读取;`query` 用于全文搜索;复杂条件用 `filters`
- 用户要求“只返回/仅查看”指定列时,查询必须在工具层传 `--field-ids <ID1,ID2>`。不要先拉取全部字段再只在最终答复中删列;字段投影既是结果契约,也是降低响应 token 的手段。
- filters 使用 `{"operator":"and|or","operands":[...]}`,字段引用使用 fieldId。若 Case 明确需要复杂操作符,应一开始把 [filter-sort](aitable/aitable-filter-sort.md) 选为唯一 Reference,而不是先读本文件后继续加载。
- `+record-query` 必须传真实 `base-id``table-id`。URL 先用 `+url-resolve`;名称先用 `+resolve-base` / `+resolve-table` 唯一解析,禁止自动选第一项。
- 单页 `limit` 为 1-100。返回 `data.records``data.hasMore`,存在后续页时还会返回 `data.nextCursor`;把该值原样传给下一次 `--cursor`
- `+record-query` 不提供 `--all`;明确要求全量时改用原子 `record query --all --page-limit <N>`。达到页上限后仍有 `hasMore=true` 代表截断,应从返回 cursor 续跑;只有 `hasMore=false`,或按 `record-ids` 查询且所有请求 ID 均已返回时,才能声称结果完整。
## 新增
当前没有 `+record-create`,使用原子命令:
```bash
dws aitable record create --base-id <B> --table-id <T> \
--records '[{"cells":{"fldText":"内容"}}]' --format json
```
长 JSON 写到 cwd 内相对文件后使用 `--records-file ./records.json`。从真实返回的 `data.newRecordIds[]` 取 recordId,再用 `+record-query --record-ids` 回读;用户限定返回列时同时传 `--field-ids`。不要从输入顺序、名称或行号推断 ID。
## 更新、同步与批量修改
已知 recordId
```bash
dws aitable +record-update --base-id <B> --table-id <T> \
--records '[{"recordId":"<R>","cells":{"fldStatus":"完成"}}]'
```
`+record-update` 自动按 100 条分片并逐批回读;该 shortcut 只接受 `--records`。超长文件输入需要改用原子 `record update --records-file`,不要给 shortcut 猜造 flag。
单选、多选等字段的写入值可能是名称字符串,读回则是 `{id,name}` 或对象数组。若 shortcut 因原始类型不同返回 commit-unknown/read-back mismatch,禁止重放更新;只按返回的 recordId 做一次 `+record-query`,把单选按 `name`、多选按名称集合归一化比较。归一化后与目标一致时,按“独立读回已确认写入”报告,并保留 shortcut 的误报信息供排障。
按业务唯一键同步使用 `+record-upsert-by-key`:0 条创建、1 条更新、多条冲突停止。按条件批改使用 `+record-bulk-patch`,必须提供 filters/query/record-ids 中至少一种选择条件,或显式 `--all`,并设置合理 `--max-matches`
## 删除
```bash
dws aitable +record-delete --base-id <B> --table-id <T> --record-ids <R1,R2>
```
删除不可逆。只使用已确认的真实 recordId;shortcut 自动分片并验证记录已不存在。未知结果按 recordId 回读,不重放已完成批次。
## 常用字段值
| 字段类型 | 写入值 |
|---|---|
| 文本、单选 | 字符串;单选使用已有选项名称 |
| 多选 | 选项名称数组 |
| 数字、评分 | JSON number |
| 复选框 | boolean |
| 日期 | 按字段配置要求的时间值;不凭展示文本猜格式 |
| URL | 按当前字段 Schema 要求的对象或字符串 |
| 人员、关联记录 | 使用真实 userId/recordId,不用姓名代替 |
| 附件 | 先用 `+attachment-put` 获得 AITable 附件 token,再写字段 |
公式、查找引用、创建人/时间、修改人/时间等只读字段不得写入。字段类型不明时只读取目标字段配置一次。
## 历史、分享与主键文档
- 记录历史:`dws aitable +record-history-list --base-id <B> --table-id <T> --record-id <R>`。已有真实 recordId 直接执行,不扫描 Help 或产品 Catalog。
- 批量记录分享:`dws aitable +record-share-links --base <B> --table <T> --record-ids <R1,R2>`;单条也可用 `+record-share-url`
- 用户要求把分享链接“发给”联系人时,AITable 的职责在链接生成后结束;随后加载 `dingtalk-chat`,用 `dws chat +dm --to <姓名> --text <包含全部链接的文本>` 对每位收件人分别发送并检查真实回执。只解析联系人或只生成 URL 都不算完成。
- 主键文档:`+record-primary-doc-get` / `+record-primary-doc-create`
创建主键文档必须显式传 primaryDoc 类型的 `--field-id`;字段类型不明时先读取目标字段。正文读写切到 Doc;这里仅管理记录与文档关联。
@@ -0,0 +1,28 @@
# AI 表格 Section 与内部节点
只在用户操作 Base 内文件夹(Section)或把 Table/Dashboard 移入、移出 Section 时读取。这里的节点是 nsheet 业务节点,不是独立 Drive dentry;不要加载 Drive Skill 或尝试 Drive move。
## 高频闭环
创建 Section 并移动节点:
```bash
dws aitable +section-create --base-id <B> --name "归档区" --format json
dws aitable +section-move-node --base-id <B> --node-id <TABLE_OR_DASHBOARD_ID> --new-parent-section-id <S> --format json
dws aitable +section-list-nodes --base-id <B> --format json
```
移动回 Base 根目录时显式传空字符串,不能省略该参数或改用 Drive:
```bash
dws aitable +section-move-node --base-id <B> --node-id <N> --new-parent-section-id '' --format json
```
## 删除空 Section
1.`+section-list-nodes` 核对目标 Section 内节点;需要移出的节点逐个 `+section-move-node`
2.`+section-list-empty --base-id <B>` 验证目标 sectionId 确实为空。
3. 执行 `+section-delete --base-id <B> --section-id <S>`;按 Runtime confirmation 处理。
4. 再次 `+section-list-nodes``+section-list-empty`,确认 Section 已不存在且被移动节点仍在预期父级。
Table 本身的创建、复制、改名、删除分别使用 `+table-*`Section 只管理 Base 内目录关系。
@@ -0,0 +1,138 @@
# AITable 低频原子能力索引
> 返回入口:[DingTalk AITable Skill](../SKILL.md)
本文件只用于根 Skill 和精确 task reference 都未覆盖的低频底层能力。Base/Table
定位、建表、记录查询与写入、字段配置、视图编排、导入导出等常见任务必须回到根 Skill
的 Golden Route 或对应 task reference,不在这里重新选路。
## 使用边界
1. 先确认任务确实需要 Shortcut 未发布的底层字段、原始响应或运维控制;
2. 只读取精确原子 leaf Schema/Help,不加载产品级 Catalog 猜参数;
3. URL、名称和自然目标仍须解析为唯一稳定 ID,禁止选第一项;
4. 原子写 leaf 的 confirmation 若与对应 Golden Shortcut 不一致,停止并报告交付漂移;
5. 后续 ID 只使用当前 profile 的真实返回,不跨组织复用;
6. 完成后保留 verification、partial failure、checkpoint 和可继续编排的稳定 ID。
## 高频任务返回表
| 用户终点 | 返回入口 |
|---|---|
| URL/名称解析、Base 搜索、建 Base/Table、记录查询与 CRUD | 根 Skill Golden Route |
| 记录值格式、批量写、历史、分享和统计 | [record-ops](aitable-record-ops.md) |
| 筛选、排序和日期操作符 | [filter-sort](aitable/aitable-filter-sort.md) |
| 字段类型、创建与复杂配置 | [field](aitable/aitable-field.md) |
| 视图列顺序、筛选、排序、冻结和展示配置 | [view-config](aitable/aitable-view-config.md) |
| 表单字段、题目与分享 | [form](aitable/aitable-form.md) |
| Dashboard 与 Chart 配置 | [dashboard-chart](aitable/aitable-dashboard-chart.md) |
| 导入、导出和异步任务恢复 | [export-import](aitable/aitable-export-import.md) |
| 附件、工作流与高级权限 | 对应的 [attachment](aitable/aitable-attachment.md)、[workflow](aitable/aitable-workflow.md) 或 [advperm](aitable/aitable-advperm.md) |
| 记录主键文档 | [primary-doc](aitable/aitable-primary-doc.md) |
| Base 内 Section/节点编排 | [section](aitable-section.md) |
| 相邻产品或低频意图仍需消歧 | [intent-guide](intent-guide.md) |
## Base、Table 与 Field 底层能力
| 原子命令 | 仅用于 |
|---|---|
| `aitable base get` / `list` / `search` | Shortcut 未投影的 Base 原始详情、最近访问列表或原始搜索响应 |
| `aitable base create` / `copy` / `update` / `delete` | Shortcut 未发布的底层创建、复制或变更字段;普通整套创建回根 Skill |
| `aitable base get-primary-doc-id` | 需要 Base 视角的记录主键文档 ID |
| `aitable table get` / `list` | 需要原始 Table 结构或目录响应 |
| `aitable table create` / `update` / `delete` | Shortcut 未发布的底层 Table 字段;完整建表回根 Skill |
| `aitable field get` / `list` / `search-options` | 字段原始配置、目录或选项搜索 |
| `aitable field create` / `update` / `delete` | 精确字段原子写入,且字段配置已由 task reference 校验 |
| `aitable template search` | 需要模板原始响应;普通模板检索使用 `+template-search` |
`base list` 仅表示最近访问,不是组织内全量 Base。Table、Field 和记录 ID 均属于指定
Base;同名对象零命中或多候选时停止,不能把名称、URL 末段或其他产品节点 ID 直接当稳定 ID。
## Record 底层能力
| 原子命令 | 仅用于 |
|---|---|
| `aitable record get` / `list` / `query` | Shortcut 未投影的原始记录响应、显式 continuation 或窄 ID 查询 |
| `aitable record query-empty` | 查找未填写用户字段的空行 |
| `aitable record history-list` | 已知 recordId 的原始变更历史 |
| `aitable record share-url` | 已知记录的原始分享链接响应 |
| `aitable record create` / `update` / `batch-update` / `upsert` | Shortcut 未发布的底层写参数;写前须有真实 fieldId 和字段类型 |
| `aitable record delete` | 删除已唯一确认的记录,按最终 Runtime gate 执行 |
| `aitable record primary-doc-get` / `primary-doc-create` | 取得或创建记录主键文档;正文编辑转交 Doc |
`cells` 使用真实 fieldId;公式、查找引用、创建人和修改时间等只读字段不得写入。批量结果
必须检查 completed/failed/checkpoint,写入效果未知时先按稳定 ID 或业务唯一键回读,不盲目重放。
## View、Form 与可视化底层能力
| 原子命令 | 用途 |
|---|---|
| `aitable view list` / `get` | 原始视图目录或完整配置 |
| `aitable view create` / `duplicate` / `delete` / `lock` | Shortcut 未发布的视图生命周期与锁定字段 |
| `aitable view get aggregate` / `card` / `field-widths` / `fill-color-rule` / `filter` / `frozen-cols` / `group` / `lock` / `row-height` / `sort` / `timebar` / `visible-fields` | 读取单个视图配置面 |
| `aitable view update aggregate` / `card` / `field-widths` / `fill-color-rule` / `filter` / `frozen-cols` / `group` / `name` / `row-height` / `sort` / `timebar` / `visible-fields` | 更新一个已完整读取的视图配置面 |
| `aitable form list` / `get` / `create` / `update` / `delete` | 表单视图原子生命周期 |
| `aitable form field list` / `update` / `hide` | 表单字段顺序、展示和隐藏 |
| `aitable form questions create` / `delete` | 表单题目原子写入 |
| `aitable form share get` / `update` | 表单分享配置 |
| `aitable dashboard get` / `create` / `update` / `delete` / `arrange` | 仪表盘原始配置和布局 |
| `aitable dashboard share get` / `update` | 仪表盘分享配置 |
| `aitable chart get` / `create` / `update` / `delete` | 图表原始配置和生命周期 |
| `aitable chart share get` / `update` | 图表分享配置 |
视图更新是配置面写入,不是字段本体修改。调整可见列前读取完整有序 fieldId 数组并固定主字段;
创建或更新图表前使用 `aitable chart widgets-example` 获取当前合法配置,不猜 config。
## 导入导出、附件与自动化
| 原子命令 | 用途 |
|---|---|
| `aitable import upload` / `data` | 申请导入上传凭证并用真实 importId 发起导入 |
| `aitable export data` | 发起或恢复底层导出任务 |
| `aitable attachment upload` | 准备 AI 表格附件上传;不是 Drive 文件上传 |
| `aitable workflow list` / `get` / `edit-example` | 工作流目录、详情与当前 DSL 示例 |
| `aitable workflow create` / `update` | 校验完整 DSL 后创建或全量更新工作流 |
| `aitable workflow enable` / `disable` | 启停已唯一确认的工作流 |
上传、导入、导出和工作流可能异步完成;accepted/pending 不等于成功。保留 taskId/importId、轮询状态、
超时和真实 next command。非幂等创建在提交状态未知时只核对,不自动重试。
旧版 Runtime 缺少当前导出或分片建字段能力时,才分别使用
[aitable_export_via_task.py](../scripts/aitable_export_via_task.py) 或
[bulk_add_fields.py](../scripts/bulk_add_fields.py);当前 Runtime 已有对应 Shortcut 时不得绕回脚本。
## 权限与 Base 内节点
| 原子命令 | 用途 |
|---|---|
| `aitable advperm role-list` / `role-get` | 高级权限角色读取 |
| `aitable advperm enable` / `disable` | Base 高级权限总开关 |
| `aitable advperm role-create` / `role-update` / `role-delete` | 自定义角色原子管理 |
| `aitable section list-nodes` / `list-empty` | Base 导航树和空 Section 读取 |
| `aitable section create` / `rename` / `reorder` | Section 创建、重命名和排序 |
| `aitable section move-node` | 在 Base 内移动 Table、Dashboard 等 nsheet 节点 |
| `aitable section delete` | 删除已确认的 Section |
高级权限角色 ID、Section ID 与 Drive 节点 ID 不可互换。整个 Base 在普通文件夹中的外层移动或重命名
归 DriveBase 内 Table、Dashboard、Section 的结构操作仍归 AITable。
## 稳定 ID 传递
| 来源 | 只可用于 |
|---|---|
| `+url-resolve` / 唯一 Base 解析 | 当前 profile 下的 baseId,以及 URL 实际携带的 tableId/viewId/recordId |
| Base/Table/Field 读取 | 同一 Base 下后续命令的 tableId、fieldId |
| Record 查询或写入回执 | recordId、主键文档 nodeId、分享链接与历史查询 |
| View/Form/Dashboard/Chart 创建或读取 | 对应对象自己的稳定 ID,不以名称或列表序号替代 |
| 导入导出回执 | importId/taskId 及其 continuation;不能当 Base/Table ID |
| Workflow/AdvPerm/Section 读取 | workflowId、roleId、sectionId,仅限原资源与 profile |
## 故障处理
- `unknown command` / `unknown flag`:读取精确 leaf Help,最多修正一次;
- confirmation 或参数约束不清:读取精确 leaf Schema,以最终 Runtime gate 为准;
- 自然目标零命中、多候选或类型不明:停止并展示候选,不选择第一项;
- `partial_success`:保留已完成项、失败 ledger 和 checkpoint,只从真实 continuation 继续;
- commit unknown:按稳定 ID 或业务唯一键核对远端效果,未确认前不重放写入;
- 权限、认证或 profile:按 `dingtalk-shared` 对应 reference 分流;
- 本索引仍无法定位命令时,才用 `dws shortcut list --service aitable --format json` 做最终回退。
@@ -0,0 +1,251 @@
# advperm — 高级权限管理
控制 Base 的高级权限总开关,并管理自定义角色(增删改查 + 子角色权限规则)。
适用场景:"如何控制谁能看/改 Base 数据"、"开启/关闭高级权限"、"新建/修改/删除角色"、"按字段或行配置权限"。
## 命令一览
| 命令 | 用途 |
|------|------|
| `advperm enable` | 开启 Base 高级权限总开关 |
| `advperm disable` | 关闭 Base 高级权限总开关(高危) |
| `advperm role-list` | 列出 Base 下全部角色 |
| `advperm role-get` | 获取单角色完整配置 |
| `advperm role-create` | 创建自定义角色 |
| `advperm role-update` | 增量更新自定义角色(PATCH 语义) |
| `advperm role-delete` | 删除自定义角色(不可逆) |
> 所有子命令的 `--base-id` 必填,可用隐藏别名 `--base`。
## 命令详情
### advperm enable — 开启高级权限
```bash
dws aitable advperm enable --base-id BASE_ID --format json
```
返回 `{baseId, enabled: true}`
只有开启后角色配置才会真正限制成员的可访问范围;关闭状态下角色配置仍可读但不生效。
### advperm disable — 关闭高级权限(高危)
```bash
dws aitable advperm disable --base-id BASE_ID --yes --format json
```
返回 `{baseId, enabled: false}`。关闭后所有角色配置即刻失效,全员回退到默认权限。涉及多人协作或敏感数据务必和用户二次确认,建议先 `role-list` 留底。
### advperm role-list — 列出全部角色
```bash
dws aitable advperm role-list --base-id BASE_ID --format json
```
返回结构:
```json
{
"data": {
"enabled": true,
"defaultRole": { "mode": 0 },
"roles": [
{
"roleId": "10685308981",
"name": "可查看角色",
"roleType": "custom",
"system": false,
"subRoles": [
{
"authLevel": "read",
"targetId": "HMEaRQ4",
"targetType": "sheet",
"config": { "actions": 268435455 },
"display": {
"authLevelLabel": "仅查看",
"targetTypeLabel": "数据表",
"permissionScopeNote": "...",
"actionsLabels": ["新增视图", "删除视图", "修改视图"],
"actionsNote": "..."
}
}
]
}
]
}
}
```
关键字段:
- `roleType``custom`(自定义) / `system_editor` / `system_reader` / `5000`owner / `4000`manager)。
- `system`boolean,true 表示系统角色(不可删)。
- `subRoles[].display.*`:服务端返回的人类可读标签,可直接拼接给用户阅读,无需自行映射枚举。
- 不返回角色成员列表;如需"成员-角色"映射请去 AI 表格 Web 端。
- 新建 Base 默认 `enabled=false`,开启后只有 `owner` / `manager` 两个 meta 角色;`system_editor` / `system_reader` 需要在 Web UI 给成员授权"可编辑/可查看"后才会被服务端自动生成。
`role-list` / `role-get` 不需要管理员权限,普通成员也可读。
### advperm role-get — 获取单角色配置
```bash
dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID --format json
```
返回结构同 `role-list` 中单个 role 对象(含完整 `subRoles[].config` 字段/行级规则与 `display.*` 标签)。
### advperm role-create — 创建自定义角色
```bash
# 仅指定 name,子角色由服务端按默认(none)填充
dws aitable advperm role-create --base-id BASE_ID --name "市场可读" --format json
# 创建时即指定 sub-roles(推荐——避免再走一次 role-update
dws aitable advperm role-create --base-id BASE_ID --name "市场可读" \
--sub-roles '[{"targetId":"<sheetId>","targetType":"sheet","authLevel":"read"}]' --format json
```
| flag | 必填 | 说明 |
|------|:---:|------|
| `--name` | ✅ | 角色名称 |
| `--role-type` | | 角色类型字符串(留空由服务端决定默认值,如 `custom` |
| `--flow-type` | | 流程类型字符串(按业务需要) |
| `--sub-roles` | | JSON 数组:`[{targetId, targetType, authLevel, appId?, config?}]`,详见下方"sub-roles 子字段"段 |
返回新建角色的完整配置(同 `role-get` 出参格式,含自动生成的 default subRoles)。
系统角色无法通过本命令创建。
### advperm role-update — 增量更新自定义角色(PATCH 语义)
```bash
# 只改名
dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name "新名字"
# 只改 sheet 子角色 authLevelname 不传保持不变
dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID \
--sub-roles '[{"targetId":"<sheetId>","targetType":"sheet","authLevel":"edit-own"}]'
```
| flag | 必填 | 说明 |
|------|:---:|------|
| `--role-id` | ✅ | 目标自定义角色 ID(数字 long 字符串) |
| `--name` | | 新角色名称;不传不修改 |
| `--role-type` / `--flow-type` | | 可选 |
| `--sub-roles` | | JSON 数组,**PATCH 合并语义**:按 `(targetId, targetType)` 合并到现有 subRoles,入参中的 sub 整体替换该 sub,**入参未提及的 sub 保留不变**(无需先调 `role-get` 自行 merge |
**系统角色禁止更新**(包括 owner / manager / system_editor / system_reader)。
### sub-roles 子字段
每个 sub-role 描述「角色对某个权限目标的访问粒度」:
| 字段 | 类型 | 说明 |
|------|------|------|
| `targetId` | string | 目标资源 ID(数据表 → `tableId`;仪表盘 → `dashboardId`;应用 → `appId` |
| `targetType` | string | `sheet` / `dashboard` / `app` |
| `authLevel` | string | `manage` / `edit-own` / `edit-custom-field` / `edit-field-range` / `read` / `none` |
| `appId` | string(可选) | 仅 `targetType=app` 时使用 |
| `config` | object(可选) | 字段/行级细化规则;含 `actions`(位图)/ `rows` / `cells`。结构与 `role-get` 出参 `subRoles[].config` 对齐 |
### advperm role-delete — 删除自定义角色(不可逆)
```bash
dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --format json
```
要求同时满足:
1. 该 Base 已开启高级权限(`role-list` 返回 `enabled=true`)。
2. 当前 dws 登录用户是该 Base 的管理员/Owner。
3. `--role-id``role-list` 返回的数字 long 字符串(如 `"10685308981"`),且对应角色 `system=false`
不可逆,删前先 `role-get` 留底。
## 能力边界
| 能力 | 状态 |
|------|------|
| 开/关高级权限 | ✅ 需管理员 |
| 列出 / 读取角色 | ✅ 普通成员也可读 |
| 创建自定义角色 | ✅ 需管理员 |
| 增量修改角色(PATCH 语义,不清空未传字段) | ✅ 需管理员 |
| 删除自定义角色 | ✅ 需管理员 |
| 修改/删除系统角色 | ❌ 服务端禁止;只能在 AI 表格 Web 端操作 |
| 角色 ↔ 成员绑定 | ❌ CLI 暂不支持,需在 AI 表格 Web 端 → Base 设置 → 高级权限 → 角色管理面板手动完成 |
## 错误码速查
| 场景 | code | type | message |
|------|------|------|---------|
| advperm 关闭时调用写接口(如 `role-delete` / `role-create` / `role-update` | `ADVANCED_PERMISSION_DISABLED` | `USER_ERROR` | `Advanced permission is disabled for base <BASE>, please enable it via setAdvancedPermission before managing roles` |
| 非管理员调用 `enable` / `disable` / `role-create` / `role-update` / `role-delete` | `401` | `AUTH_ERROR` | `the current user must be a manager (administrator) of this base to manage roles or advanced permission` |
| 删除/更新系统角色(`system=true` | `600` | `USER_ERROR` | `Illegal argument` |
| 操作不存在的数字 roleIdget/update/delete | `600` | `USER_ERROR` | `Illegal argument` |
| 传非数字 roleId(如 `owner` / `manager` | `INVALID_PARAMS` | `INPUT_ERROR` | `roleId is required` |
| `role-create``--name` | `INVALID_PARAMS` | `INPUT_ERROR` | `name is required` |
| `--sub-roles` JSON 不是数组 / 解析失败 | (CLI 层拦截) | — | `--sub-roles 解析失败 ...` / `--sub-roles 必须是 JSON 数组` |
| `--base-id` 无法解析 | `INVALID_BASE_ID` | `INPUT_ERROR` | `baseId cannot be resolved to docId` |
> `600 / Illegal argument` 同时覆盖"操作系统角色"和"操作不存在 roleId"两种情况。拿到 `600` 时先 `role-list` 自查目标 roleId 是否存在、是否 `system=true`,再据此引导用户。
## 典型工作流
### 排查"成员看不到某些字段/记录"
```bash
dws aitable advperm role-list --base-id BASE_ID --format json
# 若 enabled=false:高级权限未开,所有规则不生效,与用户确认是否需要 enable
dws aitable advperm enable --base-id BASE_ID --format json
dws aitable advperm role-list --base-id BASE_ID --format json
# 看 roles[] 里有哪些自定义角色
dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID --format json
# 检查 subRoles[].config 中的字段/行级权限规则
```
### 新建一个"市场可读"角色
```bash
# 1. 确保高级权限已开
dws aitable advperm enable --base-id BASE_ID --format json
# 2. 拿目标 sheet 的 tableId
dws aitable table get --base-id BASE_ID --format json
# 3. 创建角色 + 指定 sheet 子角色 authLevel=read
dws aitable advperm role-create --base-id BASE_ID --name "市场可读" \
--sub-roles '[{"targetId":"<tableId>","targetType":"sheet","authLevel":"read"}]' \
--format json
# → 返回新角色完整配置,含 roleId,记下后续 patch / delete 使用
```
### 升级角色权限(read → edit-own),保留其他配置
```bash
# 只传 sub-rolesname 等其他字段保持不变(PATCH 语义)
dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID \
--sub-roles '[{"targetId":"<tableId>","targetType":"sheet","authLevel":"edit-own"}]' \
--format json
```
### 改角色名(不影响权限规则)
```bash
dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name "新名字"
```
### 清理废弃角色
```bash
dws aitable advperm role-list --base-id BASE_ID --format json
dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --format json
```
### 关闭高级权限(恢复全员可见)
```bash
dws aitable advperm role-list --base-id BASE_ID --format json > /tmp/roles-backup.json
dws aitable advperm disable --base-id BASE_ID --yes --format json
```
@@ -0,0 +1,49 @@
# attachment — 附件上传
> **STOP — 不要使用钉盘 (drive) 上传!** 钉盘 fileId 无法写入 attachment 字段。必须使用以下流程。
>
> **STOP — 严禁在 record create/update 的 cells 里直接传图片 URL** 直传 `{"url":"https://..."}` 会导致服务端同步下载图片,批量写入时触发 TIMEOUT_ERROR。正确做法:先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。
## 准备附件上传
```
Usage:
dws aitable attachment upload [flags]
Example:
dws aitable attachment upload --base-id <BASE_ID> --file-name report.xlsx --size 204800
dws aitable attachment upload --base-id <BASE_ID> --file-name photo.png --size 1024 --mime-type image/png
Flags:
--base-id string Base ID (必填)
--file-name string 文件名,必须含扩展名 (必填)
--size int 文件大小(字节),>0 (必填)
--mime-type string MIME type(不传时根据扩展名推断)
```
## 附件上传完整流程(推荐:使用脚本,2 步完成)
```bash
# 步骤 1: 使用脚本一键上传(内部自动完成 prepare + PUT
python3 scripts/upload_attachment.py <BASE_ID> /path/to/report.pdf
# 输出: { "fileToken": "ft_xxx", "fileName": "report.pdf", "size": 204800 }
# 步骤 2: 在 record create/update 中使用 fileToken 写入
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
```
> `uploadUrl` 有时效性(`expiresAt`),脚本会自动在获取后立即上传。
## 手动流程(不使用脚本)
```bash
# 1. 获取上传凭证
dws aitable attachment upload --base-id <BASE_ID> --file-name report.pdf --size 204800 --format json
# → 返回 uploadUrl、fileToken
# 2. PUT 上传(Content-Type 必须是文件的具体 MIME type)
curl -X PUT "<uploadUrl>" -H "Content-Type: application/pdf" --data-binary @report.pdf
# 3. 写入记录
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
```
@@ -0,0 +1,48 @@
# AI 表格最佳实践
## 1. 字段可写性分类
| 字段类型 | 可写 | 正确方式 |
|----------|------|----------|
| 文本/数字/日期/单选/多选/复选框/URL | ✅ | record create/update |
| 附件 | ⚠️ | 必须先走 [attachment upload 流程](./aitable-attachment.md) |
| 创建人/修改人/创建时间/修改时间 | ❌ | 系统字段,只读 |
| 公式/查找引用 | ❌ | 只读,由系统计算 |
| AI 字段 | ❌ | 只读,由 AI 自动计算 |
## 2. 查询执行契约
1. **不要拉全量后在 context 里手动统计** — 标量聚合用 `record stats`,分组/去重用 `record group-stats`
2. **has_more=true 时不能做全局结论** — 数据可能不完整
3. **优先用 `--filters` 在服务端过滤** — 不要拉全量后在本地 jq/grep
4. **fieldId 必须来自 `field get` 真实返回** — 不要猜测 fieldId
5. **减少响应体积** — 用 `--field-ids` 仅返回需要的字段
## 3. 任务选路
| 用户诉求 | 优先方案 | 不要误走 |
|---------|----------|----------|
| 查看几条数据 | `record query` | 不要用 `--all` |
| 全量拉取明细 | `record query --all` | 不要手动循环 cursor |
| 标量统计 | `record stats` | 不要先拉全量再本地计算 |
| 分组/去重统计 | `record group-stats` | 不要先拉全量再本地 groupby |
| 全量导出为文件 | `export data` | 不要 `--all` 拉全量再写文件 |
| 批量写入 | `record create`(分批 100 条) | 不要一次传超过 100 条 |
| 附件/图片上传 | `attachment upload` 获取 fileToken → `record create/update` 用 fileToken 写入 | **严禁直接传图片 URL 到附件字段**(服务端同步下载会超时) |
| 文件级导入 | `import upload` + `import data` | 不要手动解析 xlsx 再逐条写入 |
## 4. 创建/修改后回读确认
执行写操作后,建议立即回读确认结果:
| 写操作 | 建议回读命令 | 确认内容 |
|--------|-------------|----------|
| `table create` | `table get --table-ids <新tableId>` | 表名、字段列表是否符合预期 |
| `field create` | `field get --table-id <tableId>` | 新字段是否出现在字段列表中 |
| `record create/update` | `record query --record-ids <新recordId>` | 写入值是否正确 |
## 5. AI 字段注意事项
- AI 字段的 prompt **必须至少包含一个 `fieldRef` 引用**,纯文本 prompt 会被后端拒绝
- 先创建/确认被引用字段的 fieldId,再在 prompt 中引用
- `outputType` 必须与字段类型一致(如 `outputType=text``--type text`
@@ -0,0 +1,346 @@
# cells 写入/读取格式规范(cellValue 数据结构)
> 适用命令:`dws aitable record create --records`、`dws aitable record update --records`、`dws aitable record query` 返回
>
> 本文件是 DWS AI 表格 cellValue 的 **source of truth**。写入记录时,必须严格按此格式构造 cells 对象。
## 顶层规则
- cells 的 key **必须是 fieldId**(如 `fldXXX`),不是字段名称
- fieldId 必须从 `field get` 返回中获取
- 不同字段类型的 value 格式不同,混用会报错
- 系统只读字段(creator/lastModifier/createdTime/lastModifiedTime/formula)不可写入
## 各字段类型详解
### text(文本)
**写入**:字符串
```json
{"fldTextId": "这是一段文本"}
```
**读取**:字符串
```json
{"fldTextId": "这是一段文本"}
```
---
### number(数字)
**写入**:数字或数字字符串
```json
{"fldNumId": 123.45}
{"fldNumId": "123.45"}
```
**读取**:字符串形式的数字
```json
{"fldNumId": "123.45"}
```
---
### singleSelect(单选)
**写入**:选项名称字符串(推荐),或对象形式 `{id, name}`
```json
{"fldSelectId": "进行中"}
{"fldSelectId": {"id": "opt_xxx", "name": "进行中"}}
```
> 写入不存在的选项名称时,系统会自动创建该选项。
> 对象写入时 id 为准,服务端会校验 id 是否存在。
**读取**:对象 `{id, name}`
```json
{"fldSelectId": {"id": "opt_abc123", "name": "进行中"}}
```
---
### multipleSelect(多选)
**写入**:选项名称数组(推荐),或对象数组
```json
{"fldMultiId": ["标签A", "标签B"]}
{"fldMultiId": [{"id": "opt_a", "name": "标签A"}, {"id": "opt_b", "name": "标签B"}]}
```
> 写入时每项需带 id(对象模式)或直接传 name 字符串。不存在的 name 会自动补入选项配置。
**读取**:对象数组
```json
{"fldMultiId": [{"id": "opt_a", "name": "标签A"}, {"id": "opt_b", "name": "标签B"}]}
```
---
### date(日期)
**写入**:日期字符串、RFC3339 字符串、或毫秒时间戳
```json
{"fldDateId": "2026-03-15"}
{"fldDateId": "2026-03-15 09:00"}
{"fldDateId": "2026-03-15T09:00+08:00"}
```
**读取**RFC3339 字符串(带时区)
```json
{"fldDateId": "2026-03-15T09:00:00+08:00"}
```
**过滤**`record query --filters`):日期字段**只能用日期专用操作符** `date_eq` / `before` / `after` / `not_before` / `not_after` / `exist` / `un_exist`,比较值用日期字符串(如 `"2026-03-15"`)。
- ❌ 通用 `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `contain` 对日期字段无效,会静默返回 0 条;
- ❌ 不支持区间 `date_between` 与相对 `from_now`CLI 会直接拒绝),范围查询用 `not_before` + `not_after` 组合。
- 详见 [aitable-filter-sort.md](./aitable-filter-sort.md) §日期字段过滤。
---
### currency(货币)
**写入**:数字(与 number 相同)
```json
{"fldCurrencyId": 99.5}
```
**读取**:字符串形式的数字(小数位数取决于 formatter 配置)
```json
{"fldCurrencyId": "99.5"}
```
---
### progress(进度)
**写入**:0~1 之间的浮点数(0 表示 0%,1 表示 100%)
```json
{"fldProgressId": 0.75}
```
> ⚠️ **常见错误**:写入 75 不会报错,但会被存储为 7500%(因为系统将其理解为 75 倍)。
> 正确做法:75% 应写入 0.75。API 不会拒绝超出 [0,1] 的值,但显示会异常。
> 如果字段配置了 `customizeRange`,则按自定义范围传值。
**读取**:字符串形式的数字
```json
{"fldProgressId": "0.75"}
```
---
### rating(评分)
**写入**:整数,必须在字段配置的 min~max 范围内
```json
{"fldRatingId": 4}
```
> ⚠️ 超出 max 范围的值(如 max=5 时写入 6)会被服务端拒绝并返回错误。
**读取**:数字(字符串形式)
```json
{"fldRatingId": "4"}
```
---
### checkbox(勾选)
**写入**:布尔值
```json
{"fldCheckId": true}
{"fldCheckId": false}
```
**读取**:布尔值
```json
{"fldCheckId": true}
```
---
### user(人员)
**写入**:对象数组,每项必须含 `userId``corpId`
```json
{"fldUserId": [{"userId": "staff_001", "corpId": "dingxxxxxxxx"}]}
```
> 单选字段(`multiple=false`)也必须传数组,只是数组长度为 1。
> 如果目标用户不在当前请求组织内,回退为 `[{"userRef": "ur_0AaZ19"}]`。
**读取**:对象数组
```json
{"fldUserId": [{"userId": "staff_001", "corpId": "dingxxxxxxxx"}]}
```
---
### department(部门)
**写入**:对象数组,每项含 `deptId`
```json
{"fldDeptId": [{"deptId": "52528700"}]}
```
**读取**:对象数组
```json
{"fldDeptId": [{"deptId": "52528700"}]}
```
---
### group(群组)
**写入**:对象数组,每项含 `cid`
```json
{"fldGroupId": [{"cid": "74577067501"}]}
```
> ⚠️ key 是 **`cid`**,不是 `openConversationId`
**读取**:对象数组
```json
{"fldGroupId": [{"cid": "74577067501"}]}
```
---
### url(链接)
**写入**:对象 `{text, link}` 或纯 URL 字符串
```json
{"fldUrlId": {"text": "钉钉官网", "link": "https://dingtalk.com"}}
{"fldUrlId": "https://dingtalk.com"}
```
> 纯字符串写入时,服务端自动补齐为 `{"text":"原字符串","link":"原字符串"}`
**读取**:对象 `{text, link}`
```json
{"fldUrlId": {"text": "钉钉官网", "link": "https://dingtalk.com"}}
```
---
### richText(富文本)
**写入**:对象 `{markdown: "..."}`
```json
{"fldRichId": {"markdown": "**加粗**\n普通文字\n"}}
```
**读取**:对象 `{markdown: "..."}`(有损,颜色/@人等信息可能丢失
```json
{"fldRichId": {"markdown": "**加粗**\n普通文字\n"}}
```
---
### attachment(附件)
**写入**:对象数组,**必须使用 `fileToken`**
```json
{"fldAttachId": [{"fileToken": "ft_xxx"}]}
```
> ⚠️ **必须先通过 [attachment upload 流程](./aitable-attachment.md) 上传文件获取 `fileToken`,再将 `fileToken` 写入 cells。**
> ❌ **严禁直接传 `{"url": "https://..."}` 形式写入附件/图片字段** — 服务端会同步下载图片,10 条记录即触发 TIMEOUT_ERROR 超时。
> 写入会**整体覆盖**原附件列表,不是追加。
**读取**:对象数组(含下载链接、文件名、大小)
```json
{"fldAttachId": [{"url": "https://...", "filename": "report.pdf", "size": 204800}]}
```
---
### telephone / email / barcode / idCard(电话/邮箱/条码/身份证)
**写入**:字符串
```json
{"fldPhoneId": "13800138000"}
{"fldEmailId": "test@example.com"}
{"fldBarcodeId": "978-3-16-148410-0"}
{"fldIdCardId": "520402196001067498"}
```
> idCard 必须是后端认可的合法身份证号格式
**读取**:字符串
```json
{"fldPhoneId": "13800138000"}
```
---
### geolocation(地理位置)
**写入**:对象,包含 `address``name``location`
```json
{
"fldGeoId": {
"address": "浙江省杭州市思凯路与爱橙街交叉口东南200米",
"name": "阿里中心·未科D1幢",
"location": ["120.007852", "30.271194"]
}
}
```
> `location` 按 **[经度, 纬度]** 传**字符串数组**
**读取**:对象(含额外的 `fullAddress` 字段,由服务端自动拼接)
```json
{
"fldGeoId": {
"address": "浙江省杭州市",
"fullAddress": "阿里中心-浙江省杭州市",
"name": "阿里中心",
"location": ["120.007852", "30.271194"]
}
}
```
---
### unidirectionalLink / bidirectionalLink(关联字段)
**写入**:对象 `{linkedRecordIds: [...]}`
```json
{"fldLinkId": {"linkedRecordIds": ["recXXX", "recYYY"]}}
```
**读取**:对象 `{linkedRecordIds: [...]}`
```json
{"fldLinkId": {"linkedRecordIds": ["recXXX", "recYYY"]}}
```
---
### 只读字段(禁止写入)
以下字段类型由系统自动填充,`record create/update` 时**禁止传入**
| 类型 | 说明 |
|------|------|
| `creator` | 创建人 |
| `lastModifier` | 最后编辑人 |
| `createdTime` | 创建时间 |
| `lastModifiedTime` | 最后编辑时间 |
| `formula` | 公式字段(系统计算) |
| AI 字段 | 由 AI 自动计算 |
## 常见错误速查
| 错误 | 正确做法 |
|------|----------|
| cells key 用字段名称 `"课程名称"` | 用 fieldId `"fldXXX"` |
| progress 写入 `75` | 写入 `0.75`(范围 0~1 |
| attachment 直接传文件路径或图片 URL | 必须先 `attachment upload` 获取 fileToken,再用 fileToken 写入(直传 URL 会超时) |
| user 字段传用户名字符串 | 传对象数组 `[{"userId":"...", "corpId":"..."}]` |
| group 字段用 `openConversationId` | 用 `cid` |
| singleSelect 传 option id 字符串 | 传 name 字符串或 `{"id":"...", "name":"..."}` 对象 |
| 对只读字段写入值 | 不传该字段,由系统自动填充 |
@@ -0,0 +1,55 @@
# dashboard & chart — 仪表盘与图表
## 建议操作顺序
```bash
# 1) 只在缺少配置结构时读取模板
dws aitable dashboard config-example --format json
dws aitable +chart-widgets-example --format json
# 2) 先拿 dashboard,再拿 chart 详情
dws aitable dashboard get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --format json
```
只按名称创建、改名并确认时,不需要读取配置示例或 Help:
```bash
dws aitable dashboard create --base-id <BASE_ID> --name <名称> --format json
dws aitable +dashboard-update --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --name <新名称> --format json
dws aitable +dashboard-get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
```
部分服务端更新回执可能仍回显更新前名称;只做一次 `+dashboard-get` 读回,以该后续权威读回为最终状态。读回已是目标名称时判定更新完成,不重放写操作,也不因旧回执继续探测 Help。
## 要点
- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用
- 删除 dashboard 会级联删除其全部 chart;确认前必须说明该影响
- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错
- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断
## dashboard 子命令
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `dashboard get` | 获取仪表盘详情(含 charts 列表) | `--base-id` `--dashboard-id` | — |
| `dashboard create` | 创建仪表盘 | `--base-id` + (`--config``--name`) | `--name` 简化版创建空看板;`--config` 传完整 JSON |
| `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config``--name`) | `--name` 仅改名;`--config` 更新完整配置 |
| `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` | 级联删除全部 chart,不可逆;由 Runtime 请求确认,Reference 不携带确认绕过参数 |
| `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 |
| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 把图表按行铺满网格,避免某行只占半幅、留下大片空白;返回 `{totalColumns, layout, alignedChartCount}` |
## chart 子命令
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` | 不可逆;由 Runtime 请求确认,Reference 不携带确认绕过参数 |
| `+chart-widgets-example` | 查看所有图表类型的 widgets 模板 | 无 |
## 配置获取流程
已有符合当前 leaf Schema 的合法 config 时直接创建/更新,不读取模板。只有缺少结构时调用一次 `+chart-widgets-example`;该命令当前返回所有图表类型示例,随后只使用目标类型,并按真实 tableId/fieldId 填充后执行。
@@ -0,0 +1,131 @@
# AI 表格数据分析 SOP
> 当用户诉求涉及查询、筛选、排序、统计、Top/Bottom N、分组聚合、判断全局结论时,必须先读本文档再执行。
## 1. 查询决策树
```
用户要做什么?
├─ 查看/导出原始记录明细
│ → record query [--filters] [--sort] [--field-ids] [--limit]
├─ 按条件筛选记录(如"状态=进行中的记录"
│ → record query --filters '{"operator":"and","operands":[...]}'
├─ 取 Top N / Bottom N(如"销售额最高的5条"
│ → record query --sort '[{"fieldId":"xxx","direction":"desc"}]' --limit 5
├─ 全量统计(如"一共多少条"、"所有记录的总销售额")
│ → record stats --stats '[{"fieldId":"...","statsType":"COUNT|SUM|AVG|..."}]'
├─ 分组统计(如"每个状态各有多少条")
│ → record group-stats --group '[...]' --stats '[{"fieldId":"...","statsType":"count"}]'
├─ 条件唯一实体数(如"有索赔单的门店共有多少家")
│ → record group-stats --filters '{...}' --stats '[{"fieldId":"<实体字段>","statsType":"distinct"}]'
└─ 判断全局结论(如"是否所有记录都满足条件")
→ record stats 对反向过滤结果 COUNT;只有需要行级证据时再 record query
```
## 2. 核心规则
### 2.1 禁止基于默认分页下全局结论
`record query` 默认返回 100 条。如果返回 JSON 中 `data.nextCursor` 非空,表示还有后续数据,当前结果**不是全量**。
```json
{"data": {"nextCursor": "3hf5MtLbLZ", "records": [...]}}
```
- ❌ 错误:只查了默认 100 条就说"共 100 条记录"
- ✅ 正确:使用 `--all` 自动翻页拿全量,再统计
### 2.2 能在服务端过滤的,不要拉到本地再过滤
| 需求 | 正确做法 | 错误做法 |
|------|---------|---------|
| 筛选"状态=已完成" | `--filters '{"operator":"and","operands":[{"operator":"eq","operands":["fldXXX","已完成"]}]}'` | `--all` 拉全量再本地 filter |
| 按日期降序取最新5条 | `--sort '[...]' --limit 5` | `--all` 拉全量再本地 sort + slice |
| 模糊搜索标题含"Q1" | `--filters``contain` 操作符 | 全量拉取再本地 grep |
### 2.3 聚合必须优先在服务端完成
以下场景均有服务端聚合入口,不得先用 `record query --all` 下载全表计算:
- `record stats`:不分组的 COUNT / SUM / AVG / MAX / MIN / MEDIAN / DISTINCT / 完整率等
- `record group-stats`:分组统计,以及不带 group 的条件 DISTINCT 唯一计数
- 任意条件比率:分别聚合分子与分母,再对齐范围和 `dataVersion` 计算
只有用户要求记录明细、少量样本校验、精确分位数输入,或聚合接口明确返回不支持/错误时,才允许使用 `record query`。需要先逐行运算再聚合的指标(例如两个日期相减)必须使用表内已有且可聚合的公式字段;没有该字段时停止并请用户先在 AI 表格页面创建,不能下载全表本地二次计算。
### 2.4 --all 使用注意
```bash
dws aitable record query \
--base-id <baseId> \
--table-id <tableId> \
--all \
--field-ids <只取需要的字段> \
--format json
```
- **必须配合 `--field-ids`** 限制返回字段,减少数据量
- 对于大表(>1000条),先告知用户可能耗时
- `--all` 会自动处理分页,无需手动翻页
## 3. filters 快速参考
详细语法见 [aitable-filter-sort.md](./aitable-filter-sort.md)。
### 常用操作符速查
| 操作符 | 适用类型 | 含义 | 示例 operands |
|--------|---------|------|-------------|
| `eq` | 通用 | 等于 | `["fldXXX", "值"]` |
| `ne` | 通用 | 不等于 | `["fldXXX", "值"]` |
| `gt` / `lt` | 数值/日期 | 大于/小于 | `["fldXXX", "25"]` |
| `gte` / `lte` | 数值/日期 | 大于等于/小于等于 | `["fldXXX", "100"]` |
| `contain` | 文本 | 包含 | `["fldXXX", "关键词"]` |
| `exist` / `un_exist` | 通用 | 有值/为空 | `["fldXXX"]`(无第二参数) |
| `any_of` | 多选 | 包含任一 | `["fldXXX", "选项A"]` |
### filters 结构模板
```json
{
"operator": "and",
"operands": [
{"operator": "eq", "operands": ["<fieldId>", "<值>"]},
{"operator": "gt", "operands": ["<fieldId>", "<数值>"]}
]
}
```
## 4. 分析结果呈现规范
### 4.1 必须包含的信息
- **数据范围**:基于哪个表、哪些筛选条件、查询了多少条记录
- **计算方法**:用了什么聚合方式(sum/count/avg 等)
- **结果值**:精确到合理小数位
### 4.2 示例
> 基于「销售数据」表,筛选条件:日期 ≥ 2026-01-01,共查询到 342 条记录。
> - 总销售额:¥1,234,567.89SUM
> - 平均单价:¥3,610.46AVG
> - 最大单笔:¥89,000.00MAX
## 5. 任务选路心智模型
| 用户诉求 | 优先方案 | 不要误走 |
|---------|---------|---------|
| 一次性标量统计 | `record stats` | 不要 `record query --all` 后本地聚合 |
| 分组/去重统计 | `record group-stats` | 不要下载全表 groupby / 去重 |
| 长期展示派生指标 | 创建 formula 字段(见 [formula-guide](./aitable-formula-guide.md) | 不要每次手算再手动写入 |
| 按条件筛选记录 | `record query --filters` | 不要 `--all` 拉全量再本地 filter |
| 取最新/最大/前N | `--sort + --limit` | 不要 `--all` 再本地排序取前N |
| 关键词检索 | `record query --filters``contain` | 不要把表格当搜索引擎全文检索 |
| 验证"是否全部满足" | 反向 filters(筛不满足的),看是否有结果 | 不要 `--all` 逐条遍历 |
@@ -0,0 +1,272 @@
# datasource — 数据源同步管理
将外部数据源(当前仅支持 OA 审批)同步到 AI 表格。完整链路:list-sources → (OA) 选择模板 → get-fields → create → sync-status → sync / update / get-config。
## 命令一览
| 命令 | 用途 | 读/写 |
|------|------|-------|
| `+datasource-list-sources` | 列出可用数据源条目,获取 processCode/name/iconUrl/url | 读 |
| `+datasource-get-fields` | 获取可同步字段列表,用于决定 field-ids | 读 |
| `+datasource-create` | 创建数据源表并触发首次全量同步 | 写 |
| `+datasource-update` | 更新已有数据源表的同步配置 | 写 |
| `+datasource-sync` | 手动触发一次同步(最多 5 张表) | 写 |
| `+datasource-sync-status` | 查询同步任务状态(RUNNING/FINISHED/FAILED | 读 |
| `+datasource-get-config` | 获取数据源表当前同步配置 | 读 |
## 典型工作流
```
Step 0 列出可用来源 +datasource-list-sources --base-id <B> --datasource-type OA
→ 返回 approvals 数组(通常含多个审批模板)
Step 0.5 (OA 审批) 选择模板 list-sources 返回的 approvals 数组通常含多个审批模板,需确定目标:
- 用户已指定名称(如"采购申请")→ 按 name 精确/模糊匹配
· 唯一命中 → 提取该条的 processCode/name/iconUrl/url,继续
· 多候选 → 列出匹配项让用户消歧
· 零命中 → 停止,提示用户确认名称或从完整列表选择
- 用户未指定 → 列出候选模板清单(name + processCode),等用户选择
- 禁止:未匹配直接选第一项、或凭记忆猜 processCode
注:此步骤针对 OA 审批数据源(多模板场景);其他数据源类型的
list-sources 返回结构可能不同,按实际 result 解析即可,
不一定需要选择步骤。
Step 1 (可选) 获取可同步字段 +datasource-get-fields --base-id <B> --datasource-type OA --source-config '<JSON>'
→ 决定需要同步哪些字段,得到 field-ids
Step 2 创建数据源 +datasource-create --base-id <B> --datasource-type OA --source-config '<JSON>'
→ sourceConfig 中的 processCode/name/iconUrl/url 来自 Step 0.5 选中的模板
→ 返回 tableId + taskId
Step 3 查询同步结果 +datasource-sync-status --base-id <B> --table-id <T> --task-ids <TASK_ID>
→ FINISHED=完成,FAILED=看 errorCode 排查,RUNNING=轮询
Step 4 (后续) 手动触发同步 +datasource-sync --base-id <B> --table-ids <T1>,<T2>
→ 返回新 taskId,再用 sync-status 查结果
Step 5 (后续) 更新配置 +datasource-update --base-id <B> --table-id <T> --source-config '<JSON>' --auto
→ 更新后自动触发一次同步
查看当前配置 +datasource-get-config --base-id <B> --table-id <T>
```
## sourceConfig 字段协议
以下字段协议仅适用于 OA 审批数据源(datasourceType=OA),其他数据源类型待后续开放。
### 须从 list-sources 原样透传(必填)
| 字段 | 类型 | 说明 |
|------|------|------|
| processCode | String | OA 审批流程编码 |
| name | String | 展示名称 |
| iconUrl | String | 图标 URL |
| url | String | 跳转链接 |
### 调用方自行设置
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| dataType | String | 是 | 数据范围类型:`time_range` / `start_time` / `recent_time` |
| recentDays | String | 条件 | dataType=recent_time 时有效,取值 `7d`/`30d`/`1y`,默认 `30d` |
| startDate | String | 条件 | dataType=time_range 或 start_time 时有效,`yyyy-MM-dd`,默认 30 天前 |
| endDate | String | 条件 | dataType=time_range 时有效,`yyyy-MM-dd`,默认当天 |
| keepRemovedFields | Boolean | 否 | 是否保留已删除字段,默认 false |
| splitParentTableField | Boolean | 否 | 是否拆分父表字段 |
> list-sources 返回的 keepRemovedFields / splitParentTableField / enableDataSyncOaDetailList 不要透传,由调用方按需设置。
### 三种 dataType 最小示例
```json
// recent_time — 最近一段时间
{"processCode":"PROC-xxxx","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"...","url":"..."}
// time_range — 指定起止日期
{"processCode":"PROC-xxxx","name":"采购申请","dataType":"time_range","startDate":"2025-01-01","endDate":"2025-12-31","iconUrl":"...","url":"..."}
// start_time — 从某日期至今
{"processCode":"PROC-xxxx","name":"采购申请","dataType":"start_time","startDate":"2025-06-01","iconUrl":"...","url":"..."}
```
## autoSyncSetting 频率配置
仅在 `--auto=true` 时生效。不传时使用下游默认自动同步策略。
| 字段 | 必填 | 说明 |
|------|------|------|
| syncType | 是 | `hourly`(按小时间隔)/ `scheduled`(定时触发) |
| hourlyInterval | hourly 时 | 正整数,小时间隔 |
| scheduleType | scheduled 时 | `daily` / `weekly` / `monthly` |
| timeValue | scheduled 时 | `HH:mm` 触发时间 |
| selectedMonthDays | monthly 时必填 | 每月几号触发,1-31 |
| selectedWeekdays | weekly 时必填 | 每周哪几天触发,1=周一…7=周日 |
| skipNonWorkingDay | 否 | 是否跳过非工作日,默认 false |
示例:`{"syncType":"scheduled","scheduleType":"daily","timeValue":"09:00"}`
## 命令详情
### +datasource-list-sources — 列出可用数据源条目
```bash
dws aitable +datasource-list-sources --base-id BASE_ID --datasource-type OA --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--datasource-type` | 是 | 数据源类型,当前仅支持 `OA` |
返回每条条目包含 `result`(下游原始 JSON 字符串)和 `sourceType`(OA 审批对应 2)。OA 审批场景下 result 为 approvals 数组:
```json
{
"approvals": [
{
"processCode": "PROC-xxxx",
"name": "采购申请",
"iconUrl": "https://...",
"url": "https://...",
"keepRemovedFields": false,
"splitParentTableField": false,
"enableDataSyncOaDetailList": false
}
]
}
```
调用方应自行解析 result,提取目标模板字段后构造 sourceConfig。
### +datasource-get-fields — 获取可同步字段列表
```bash
dws aitable +datasource-get-fields --base-id BASE_ID --datasource-type OA \
--source-config '{"processCode":"PROC-xxxx","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"...","url":"..."}' \
--format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--datasource-type` | 是 | 数据源类型,当前仅支持 `OA` |
| `--source-config` | 是 | 源配置 JSON 字符串,结构同 create 的 --source-config |
返回字段列表(字段 ID、名称、类型等),用于在 create/update 中指定 `--field-ids`
### +datasource-create — 创建数据源表
```bash
dws aitable +datasource-create --base-id BASE_ID --datasource-type OA \
--source-config '{"processCode":"PROC-xxxx","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"...","url":"..."}' \
--format json
# 开启自动同步 + 自定义频率
dws aitable +datasource-create --base-id BASE_ID --datasource-type OA \
--source-config '...' --auto \
--auto-sync-setting '{"syncType":"scheduled","scheduleType":"daily","timeValue":"09:00"}' \
--format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--datasource-type` | 是 | 数据源类型,当前仅支持 `OA` |
| `--source-config` | 是 | 源配置 JSON 字符串(见上方字段协议) |
| `--auto` | 否 | 是否开启自动同步,默认 false;无论是否传入,CLI 都会把该字段下发给下游 |
| `--auto-sync-setting` | 否 | 自动同步频率配置 JSON 字符串,仅 --auto=true 时生效 |
| `--field-ids` | 否 | 需要同步的字段 ID 列表,不传时同步全部字段 |
返回新建数据源表 tableId 和同步任务 taskId。创建后自动触发一次全量同步,需用 `+datasource-sync-status` 查最终结果。
### +datasource-update — 更新数据源配置
```bash
# 仅开启自动同步
dws aitable +datasource-update --base-id BASE_ID --table-id TABLE_ID --auto --format json
# 更新源配置
dws aitable +datasource-update --base-id BASE_ID --table-id TABLE_ID \
--source-config '{"processCode":"PROC-yyyy","name":"出差申请","dataType":"recent_time","recentDays":"30d","iconUrl":"...","url":"..."}' \
--format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--table-id` | 是 | 已有数据源表 IDsync=true |
| `--source-config` | 否 | 新的源配置 JSON 字符串,不传时保持原配置;传入时整体覆盖 |
| `--auto` | 否 | 是否开启自动同步,不传时保持原设置 |
| `--auto-sync-setting` | 否 | 自动同步频率配置 JSON 字符串,仅 --auto=true 时生效;不传时保持原频率配置 |
| `--field-ids` | 否 | 需要同步的字段 ID 列表,不传时保持现有字段配置 |
更新后自动触发一次全量同步,返回新 taskId。
### +datasource-sync — 手动触发同步
```bash
dws aitable +datasource-sync --base-id BASE_ID --table-ids TBL1,TBL2 --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--table-ids` | 是 | 待同步的数据源表 ID 列表(sync=true),1-5 个 |
返回结果包含文档链接,可打开查看同步进度。每张表独立提交,部分失败不影响其他表。
### +datasource-sync-status — 按任务 ID 查询同步状态
```bash
dws aitable +datasource-sync-status --base-id BASE_ID --table-id TABLE_ID --task-ids TASK1,TASK2 --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--table-id` | 是 | 数据源表 IDsync=true |
| `--task-ids` | 是 | 同步任务 ID 列表(由 create/update/sync 返回),1-5 个 |
任务状态:`RUNNING`(进行中)、`FINISHED`(完成)、`FAILED`(失败,含 errorCode + errorMessage)。
### +datasource-get-config — 获取数据源配置
```bash
dws aitable +datasource-get-config --base-id BASE_ID --table-id TABLE_ID --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--table-id` | 是 | 数据源表 IDsync=true |
返回当前同步配置详情(sourceConfig、是否自动同步、同步状态等)。仅适用于数据源表,普通表会报错。
## 错误码与排查
| 场景 | 表现 | 排查 |
|------|------|------|
| 同步运行中重复触发 | errorCode=4014status=FAILED | 幂等冲突,稍后重试即可 |
| 非数据源表触发 sync | 参数错误返回 | 确认 table 的 sync=true,用 `+base-get` / `+table-list` 检查 |
| sourceConfig 缺必填字段 | 创建/更新失败 | 检查 processCode/name/iconUrl/url 是否从 list-sources 原样透传 |
| dataType 与时间字段不匹配 | 创建失败 | recent_time 需 recentDaystime_range 需 startDate+endDatestart_time 需 startDate |
## 能力边界
| 能力 | 状态 |
|------|------|
| OA 审批数据源 | 已支持 |
| 其他数据源类型 | 待后续开放 |
| 全量同步 | 已支持 |
| 增量同步 | 待后续开放 |
| 自动同步 | 已支持(--auto + autoSyncSetting |
| 删除数据源表 | 走普通表删除,不走 datasource 命令 |
## 注意事项
- sourceConfig 是 **JSON 字符串**(不是 JSON 对象),CLI flag 传入时需要用单引号包裹
- list-sources 返回的 keepRemovedFields / splitParentTableField / enableDataSyncOaDetailList 不要透传,由调用方按需设置
- create/update 后自动触发一次同步,返回 taskId;用 sync-status 查最终结果
- sync 单次最多 5 张表,超出拆分多次调用
- sync-status 单次最多 5 个 taskId,超出拆分多次调用
- get-config 仅适用于数据源表(sync=true),普通表会报错
@@ -0,0 +1,128 @@
# AI 表格错误恢复指南
> 当 CLI 命令返回错误时,按本文档的映射表判断恢复动作。
## 1. 错误响应结构
```json
{
"status": "error",
"summary": "Failed to create records",
"trace_id": "2104a64c17790723347215232e085e"
}
```
- `status: "error"` 表示操作失败
- `summary` 包含错误摘要信息
- `trace_id` 用于问题追踪
## 2. 常见错误与恢复动作
### 2.1 记录操作错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| `Failed to create records` | cellValue 格式错误或字段类型不匹配 | 先 `field get` 确认字段类型,再按 [cell-value](./aitable-cell-value.md) 规范重构值 |
| `record not found` | record-id 不存在或已删除 | 用 `record query` 重新查询确认目标记录 |
| rating 字段写入超出 max | 值超出字段配置范围 | 检查字段 config 的 min/max,确保值在范围内 |
| singleSelect 写入对象格式但 id 不存在 | option id 无效 | 改用 name 字符串写入(推荐),或先 `field get` 获取有效 option id |
### 2.2 字段操作错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| `Failed to create field` | config 格式错误或必填项缺失 | 检查 [field-properties](./aitable-field-properties.md) 中该类型的必填 config |
| `field not found` | field-id 不存在 | 用 `field get` 获取最新字段列表 |
| formula 创建失败 | 公式语法错误或引用字段名不匹配 | 先 `field get` 确认字段精确名称,再检查公式语法(见 [formula-guide](./aitable-formula-guide.md) |
| 删除主字段失败 | 主字段(第一列)不可删除 | 改为更新字段名或类型,不能删除 |
### 2.3 Base/Table 操作错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| `base not found` | base-id 错误或无权限 | 确认 base-id 正确;尝试 `base list``base search` 重新定位 |
| `table not found` | table-id 错误 | 用 `table get --base-id <baseId>` 不带 table-ids 查看所有表 |
| 表名重复 | 同 Base 下已存在同名表 | 系统会自动续号(如"原名 1"),无需额外处理 |
### 2.4 视图操作错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| `view not found` | view-id 错误 | 用 `view get --base-id <baseId> --table-id <tableId>` 查看所有视图 |
| 删除最后一个视图 | 表至少保留一个视图 | 不可删除唯一视图 |
### 2.5 filters/sort 错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| filters 无效被忽略 | 根节点不是 and/or,或 operands 格式错误 | 确保 filters 根节点是 `{"operator":"and"/"or", "operands":[...]}` 结构 |
| sort 无效 | fieldId 不存在 | 先 `field get` 确认字段 ID |
| 筛选结果为空 | 条件过严或字段值不匹配 | 放宽条件验证;注意 singleSelect 筛选值用 option name 或 id |
### 2.6 导入导出错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --task-id <taskId>` 轮询直到完成 |
| 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 |
## 3. 重试策略
### 3.1 可重试的错误
| 错误类型 | 重试方式 | 最大重试次数 |
|---------|---------|------------|
| 网络超时 / 5xx | 等待 2s 后原样重试 | 2 |
| 导出任务未完成 | 轮询 task-id | 5(间隔 3s |
| 并发写入冲突 | 串行重试 | 1 |
### 3.2 不可重试的错误(立即停止)
| 错误类型 | 原因 | 处理方式 |
|---------|------|---------|
| 权限不足 / 403 | 用户对该 Base 无权限 | 停止操作,提示用户确认权限 |
| 参数格式错误 | 请求结构不合法 | 修正参数后重试,不要原样重试 |
| 资源不存在 / 404 | ID 错误或资源已删除 | 重新查询定位资源 |
| 配额超限 / 429 | API 调用频率过高 | 等待后重试,并降低并发 |
### 3.3 重试前检查清单
在重试前,先确认:
1. ❓ 错误是暂时性的还是永久性的?
2. ❓ 参数有没有明显错误需要修正?
3. ❓ 是否需要先查询最新状态再重试?
## 4. 调试技巧
### 4.1 使用 --verbose 获取详细信息
```bash
dws aitable record create \
--base-id <baseId> \
--table-id <tableId> \
--records '[...]' \
--verbose --format json
```
`--verbose` 会输出请求/响应的详细信息,帮助定位问题。
### 4.2 使用 --dry-run 预览
```bash
dws aitable record create \
--base-id <baseId> \
--table-id <tableId> \
--records '[...]' \
--dry-run --format json
```
`--dry-run` 只预览不执行,适合在不确定参数是否正确时先验证。
## 5. 错误预防最佳实践
1. **写记录前先读字段结构**`field get` 确认字段类型和 ID
2. **写字段前先读 field-properties** — 确认 config 的必填项和格式
3. **formula 字段先确认引用字段名**`[字段名]` 必须精确匹配
4. **options 更新传完整列表** — 更新 singleSelect/multipleSelect 的 options 是全量覆盖
5. **大批量操作分批执行** — 单次最多 100 条记录
6. **使用 --format json** — 确保输出可解析,方便错误判断
@@ -0,0 +1,113 @@
# export & import — 导入导出
## 导出数据(两阶段轮询)
`export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。
> ⚠️ **`--format` 冲突警告**`export data` 的 `--format` 是**导出格式**excel/attachment 等),不是全局输出格式。**此命令禁止追加全局 `--format json`**,否则会覆盖导出格式导致 `INVALID_EXPORT_FORMAT` 错误。输出默认就是 JSON,无需额外指定。
```bash
# 第一步:创建任务(按 scope 传必要参数)——注意:不要加 --format json
dws aitable export data --base-id <BASE_ID> --scope table --table-id <TABLE_ID> --format excel --timeout-ms 1000
# 第二步:拿 taskId 继续轮询,直到返回 downloadUrl
dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 3000
```
### 参数约束
| scope | 必传参数 |
|-------|----------|
| `all` | 只需 `--base-id` |
| `table` | 必须 `--table-id` |
| `view` | 必须 `--table-id` + `--view-id` |
## 导入文件(三步流程)
当用户要求将 Excel`.xlsx`)或 CSV 文件完整导入 AI 表格时,**不需要自己解析文件内容**,直接使用文件级导入。
> **无需手动解析 CSV/Excel 再逐条 record create**,效率极低且容易出错。
```bash
# 第 1 步:申请上传凭证
dws aitable import upload --base-id <BASE_ID> \
--file-name data.xlsx --file-size <字节数> --format json
# → 返回 uploadUrl 和 importId
# 第 2 步:上传文件到 OSS(注意:Content-Type 必须设为空)
curl -X PUT "<uploadUrl>" -H "Content-Type:" --data-binary @data.xlsx
# 第 3 步:触发导入(新建表模式)
dws aitable import data --import-id <importId> --format json
# → 返回 status: success 和新建的 tableIds
# 第 3 步(替代):追加到已有表
dws aitable import data --import-id <importId> --table-id <TABLE_ID> --format json
# → 数据作为新行追加到指定表中
```
### 步骤说明
| 步骤 | 命令 | 说明 |
|------|------|------|
| 申请上传凭证 | `import upload --base-id <ID> --file-name <名称> --file-size <字节>` | `--file-size` 必须与实际文件大小一致 |
| 上传文件 | HTTP PUTcurl 等) | **必须**`-H "Content-Type:"` 将 Content-Type 设为空,否则 OSS 返回 403 |
| 触发导入 | `import data --import-id <ID> [--table-id <TABLE_ID>]` | 同步等待,大多一次调用即返回结果;超时可用相同 importId 重试 |
### import data 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--import-id` | ✅ | `import upload` 返回的 importId |
| `--table-id` | ❌ | 传入时数据追加到该已有表;不传则每个 Sheet 新建独立的数据表 |
| `--timeout` | ❌ | 最长等待秒数,默认且推荐 30 |
| `--header-row` | ❌ | 表头所在行号(从 1 开始),数据从下一行读取。不传则自动识别 |
| `--src-sheet-name` | ❌ | 源文件中的 Sheet 名称,多 Sheet 文件时指定。不传则用第一个 Sheet |
| `--field-mapping` | ❌ | 字段映射 JSON`{"目标字段名":"源列名"}`)。不传则按列名自动匹配 |
### 两种导入模式
| 模式 | 触发条件 | 效果 |
|------|----------|------|
| **新建表导入** | 不传 `--table-id` | 每个 Sheet 自动新建为独立数据表 |
| **追加导入** | 传入 `--table-id` | 数据作为新行追加到指定已有表,按列名自动匹配字段 |
### 支持的文件格式:xlsx vs csv
| 特性 | xlsx | csv |
|------|------|-----|
| 新建表导入 | ✅ | ✅ |
| 追加导入(`--table-id` | ✅ | ✅ |
| `--header-row` | ✅ | ❌ 不支持 |
| `--src-sheet-name` | ✅(多 Sheet 支持) | ❌ 无 Sheet 概念 |
| `--field-mapping` | ✅ | ✅ |
> **CSV 限制**CSV 没有 Sheet 概念,且表头固定为第一行,因此 `--header-row` 和 `--src-sheet-name` 对 CSV 均不可用。
>
> **建议**:需要指定表头行或多 Sheet 选择时,**必须使用 xlsx 格式**。CSV 仅适用于表头在第一行的简单导入场景。
### 追加导入的字段匹配规则
追加导入时,系统按以下规则将 Excel 列映射到目标表字段:
1. **不传 `--field-mapping`(自动匹配)**:按字段名**精确匹配** Excel 列名和目标表字段名。如果没有任何一列匹配上,导入会失败。
2. **传 `--field-mapping`(显式映射)**:按映射关系指定对应关系,key 为目标表字段名,value 为 Excel 列名。
> **追加导入失败常见原因**:Excel 列名与目标表字段名不一致(如 Excel 是"销售姓名"但表字段是"姓名"),导致自动匹配 0 个字段,报错 `"Failed to build import sheet infos from preview data"`。
>
> **解决方案**
> 1. **首选**:创建目标表时,字段名与 Excel 表头列名**保持完全一致**
> 2. **备选**:传 `--field-mapping '{"目标字段名":"Excel列名"}'` 手动指定映射
> 3. **兜底**:如果 import data 多次失败,改用 `record create` 逐条写入
### 适用场景
- **新建表导入**:首次导入 Excel/CSV,让系统自动建表建字段
- **追加到已有表**:已有数据表结构,需要把 Excel 数据批量写入 → 传 `--table-id`(推荐 xlsx
- **需要指定表头行**:源文件前几行非数据(如注释行)→ `--header-row`(必须用 xlsx
- **多 Sheet 文件**:只导入特定 Sheet → `--src-sheet-name`(必须用 xlsx
- **不适用**:需要复杂字段级控制(如只导入部分列、数据转换)→ 解析后用 `record create`
> **导入数据无法整体撤销**:文件一旦导入成功,数据即写入表中,没有"撤销导入"操作。如需清理导入的测试数据,只能手动通过 `record delete` 逐条或批量删除记录;如果是新建表模式导入的,可以直接 `table delete` 删除整张表。因此:
> - 测试/验证场景建议导入到**独立的测试表或测试 Base**,用完后整体删除
> - 如果用户明确表示不想导入测试数据或要求先预览内容再决定,应先解析文件内容展示给用户确认,而非直接导入
@@ -0,0 +1,238 @@
# 字段类型 config 规范(field create / table create / field update
> 适用命令:`dws aitable field create`、`dws aitable table create --fields`、`dws aitable field update --config`
>
> 本文件是 DWS AI 表格字段 config 的 **source of truth**。创建/更新字段时,必须严格按此规范构造 JSON。
## 1. 顶层规则
- `table create --fields``field create --fields` 中每个字段对象:`{"fieldName":"xxx", "type":"xxx", "config":{...}}`
- `field create --name --type --config` 中 config 单独传 JSON 字符串
- `field update --config` 只传 config 部分
- 不需要 config 的类型(如 text、checkbox、attachment)可省略 config 字段
## 2. 字段类型速查
| type | 需要 config | config 核心字段 | 说明 |
|------|-------------|----------------|------|
| `text` | ❌ | — | 纯文本 |
| `number` | 可选 | `formatter` | 数字格式 |
| `singleSelect` | ✅ | `options` | 单选 |
| `multipleSelect` | ✅ | `options` | 多选 |
| `date` | 可选 | `formatter` | 日期格式 |
| `currency` | 可选 | `currencyType`, `formatter` | 货币 |
| `progress` | 可选 | `formatter`, `min`, `max`, `customizeRange` | 进度条 |
| `rating` | 可选 | `min`, `max`, `icon` | 评分 |
| `checkbox` | ❌ | — | 勾选框 |
| `user` | 可选 | `multiple` | 人员 |
| `department` | 可选 | `multiple` | 部门 |
| `group` | 可选 | `multiple` | 群组 |
| `url` | ❌ | — | 链接 |
| `richText` | ❌ | — | 富文本 |
| `telephone` | ❌ | — | 电话 |
| `email` | ❌ | — | 邮箱 |
| `attachment` | ❌ | — | 附件 |
| `geolocation` | ❌ | — | 地理位置 |
| `formula` | ✅ | `formula` | 公式(只读字段) |
| `unidirectionalLink` | ✅ | `linkedTableId`, `multiple` | 单向关联 |
| `bidirectionalLink` | ✅ | `linkedTableId`, `multiple` | 双向关联 |
| `creator` | ❌ | — | 系统字段:创建人(只读) |
| `lastModifier` | ❌ | — | 系统字段:最后编辑人(只读) |
| `createdTime` | ❌ | — | 系统字段:创建时间(只读) |
| `lastModifiedTime` | ❌ | — | 系统字段:最后编辑时间(只读) |
## 3. 各类型 config 详解
### 3.1 number(数字)
config 字段:`formatter`
可选值:
- `INT` — 整数
- `FLOAT_1` — 1 位小数
- `FLOAT_2` — 2 位小数(默认)
- `FLOAT_3` — 3 位小数
- `FLOAT_4` — 4 位小数
- `THOUSAND` — 千分位整数
- `THOUSAND_FLOAT` — 千分位 + 小数
- `PERCENT` — 百分比(整数)
- `PERCENT_FLOAT` — 百分比(小数)
```json
{"fieldName": "工时", "type": "number", "config": {"formatter": "FLOAT_2"}}
```
```json
{"fieldName": "完成率", "type": "number", "config": {"formatter": "PERCENT"}}
```
### 3.2 singleSelect / multipleSelect(单选 / 多选)
config 字段:`options`(必填)
options 结构:
- `options` 是数组,每项至少包含 `name`
- 创建时只传 `name``id` 由系统生成
- **更新时**:已有选项必须回传原 `id`(从 `field get` 获取),新增选项不传 id
```json
{
"fieldName": "优先级",
"type": "singleSelect",
"config": {
"options": [
{"name": "紧急"},
{"name": "高"},
{"name": "中"},
{"name": "低"}
]
}
}
```
更新已有字段时(保留原选项 + 新增):
```json
{
"options": [
{"id": "opt_existing_1", "name": "紧急"},
{"id": "opt_existing_2", "name": "高"},
{"id": "opt_existing_3", "name": "中"},
{"name": "极低"}
]
}
```
> 更新 options 是**全量覆盖**,不是追加!不传的旧选项会被删除,关联的单元格数据丢失。
### 3.3 date(日期)
config 字段:`formatter`
可选值:
- `YYYY-MM-DD`(默认)
- `YYYY-MM-DD HH:mm`
- `YYYY-MM-DD HH:mm:ss`
- `YYYY/MM/DD`
- `YYYY/MM/DD HH:mm`
```json
{"fieldName": "截止日期", "type": "date", "config": {"formatter": "YYYY-MM-DD"}}
```
```json
{"fieldName": "创建时间", "type": "date", "config": {"formatter": "YYYY-MM-DD HH:mm"}}
```
### 3.4 currency(货币)
config 字段:`currencyType`(必填)、`formatter`(可选)
currencyType 可选值:
`CNY` | `HKD` | `USD` | `EUR` | `GBP` | `MOP` | `VND` | `JPY` | `KRW` | `AED` | `AUD` | `BRL` | `CAD` | `CHF` | `INR` | `IDR` | `MXN` | `MYR` | `PHP` | `PLN` | `RUB` | `SGD` | `THB` | `TRY` | `TWD`
formatter 可选值(控制小数位):`INT` | `FLOAT_1` | `FLOAT_2`(默认)| `FLOAT_3` | `FLOAT_4`
```json
{"fieldName": "预算", "type": "currency", "config": {"currencyType": "CNY", "formatter": "FLOAT_2"}}
```
### 3.5 progress(进度)
config 字段:`formatter`(固定为 `PERCENT`)、`customizeRange``min``max`
- 默认范围:0~1(即 0%~100%
- 自定义范围时 `customizeRange` 必须为 `true`
```json
{"fieldName": "完成度", "type": "progress", "config": {"formatter": "PERCENT"}}
```
自定义范围:
```json
{"fieldName": "进度", "type": "progress", "config": {"formatter": "PERCENT", "customizeRange": true, "min": 0, "max": 1}}
```
### 3.6 rating(评分)
config 字段:`min``max``icon`
- `min`:固定为 `1`
- `max`1~10,默认 `5`
- `icon`:默认 `star`
```json
{"fieldName": "满意度", "type": "rating", "config": {"min": 1, "max": 5, "icon": "star"}}
```
### 3.7 user / department / group(人员 / 部门 / 群组)
config 字段:`multiple`
- `multiple``true`(多选,默认)| `false`(单选)
```json
{"fieldName": "负责人", "type": "user", "config": {"multiple": false}}
```
```json
{"fieldName": "协作部门", "type": "department", "config": {"multiple": true}}
```
### 3.8 formula(公式)
config 字段:`formula`(必填)
- 公式中引用字段使用**方括号 + 字段名**:`[字段名]`
- 支持的函数:参考钉钉 AI 表格公式文档
```json
{"fieldName": "合计", "type": "formula", "config": {"formula": "[单价] * [数量]"}}
```
```json
{"fieldName": "是否逾期", "type": "formula", "config": {"formula": "IF([截止日期] < NOW(), \"是\", \"否\")"}}
```
> ⚠️ formula 字段创建后为**只读**,不能通过 record create/update 写入值。
### 3.9 unidirectionalLink(单向关联)
config 字段:`linkedTableId`(必填)、`multiple`
- `linkedTableId`:目标表的 tableId
- `multiple``true`(多选,默认)| `false`(单选)
```json
{"fieldName": "关联项目", "type": "unidirectionalLink", "config": {"linkedTableId": "tblXXXXXX", "multiple": true}}
```
### 3.10 bidirectionalLink(双向关联)
config 字段:`linkedTableId`(必填)、`multiple`
- 与单向关联参数相同
- 创建后系统会**自动**在被关联表创建反向字段
```json
{"fieldName": "关联任务", "type": "bidirectionalLink", "config": {"linkedTableId": "tblYYYYYY", "multiple": true}}
```
## 4. AI 字段(ai-config
AI 字段不使用 config,而使用独立的 `--ai-config` 参数。详见 [aitable-field.md](./aitable-field.md) 中的 AI 字段创建示例。
核心规则:
- `outputType` 必须与 `--type` 对应:text→text, select→singleSelect, multiSelect→multipleSelect, number→number, currency→currency, image/video→attachment
- `prompt` 中必须至少包含一个 `fieldRef` 引用
- 纯文本 prompt 会被后端拒绝
## 5. 常见错误
| 错误 | 说明 |
|------|------|
| options 更新时不传已有选项的 id | 会被视为新选项,旧选项被删除,关联数据丢失 |
| options 更新时只传新增项 | 全量覆盖,旧选项全部丢失 |
| formula 字段尝试写入值 | 只读字段,record create/update 会报错 |
| linkedTableId 传表名而非 ID | 必须传 tableId(如 `tblXXX`),不接受表名 |
| progress 值写入 50 表示 50% | 实际应写入 0.5range 0~1 |
| rating 值超出 max | 写入会报错 |
@@ -0,0 +1,174 @@
# field — 字段管理
## field get — 获取字段详情
```
Usage:
dws aitable field get [flags]
Example:
dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID>
dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID> --field-ids fld1,fld2
Flags:
--base-id string Base ID (必填)
--field-ids string 字段 ID 列表,逗号分隔,单次最多 10 个
--table-id string Table ID (必填)
```
返回字段的完整配置(含 options 等)。不要假设未指定 `--table-ids``table get` 枚举结果含字段;字段目录和配置以 `field get` 返回为准。
## field create — 创建字段
```
Usage:
dws aitable field create [flags]
Example:
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "状态" --type "singleSelect" --config '{"options":[{"name":"待办"},{"name":"进行中"},{"name":"已完成"}]}'
# 或者使用批量创建模式:
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--fields '[{"fieldName":"状态","type":"singleSelect","config":{"options":[{"name":"待办"}]}}]'
Flags:
--base-id string Base ID (必填)
--name string 要创建的单字段名称(与 --type 配合使用,替代 --fields
--type string 要创建的单字段类型(需要配合 --name,参考 table create 的内置类型)
--config string 单字段配置 JSON(需要配合 --name/--type,结构参考 table create
--ai-config string 单字段 AI 配置 JSON(需要配合 --name/--type
--fields string 批量新增字段 JSON 数组,单次最多 15 个;每个字段的配置写在其 config/aiConfig 内
--table-id string Table ID (必填)
```
`field create` 有且只有两种输入模式:
- 单字段模式:必须同时传 `--name``--type``--config``--ai-config` 只作为该字段的附加配置。
- 批量模式:只传 `--fields`;字段配置写在数组内各对象的 `config` / `aiConfig` 中。
两种模式严格互斥。`--fields` 不能与 `--name``--type``--config``--ai-config` 混用;单独传 `--config` 也会报错,不会被静默忽略。
例如创建单选字段时,单字段模式的 `--config` 是一个配置对象;批量模式则把同一对象放入对应字段元素的 `config`
```bash
# 单字段模式
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "部门" --type singleSelect \
--config '{"options":[{"name":"技术部"},{"name":"产品部"}]}'
# 批量模式
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--fields '[{"fieldName":"部门","type":"singleSelect","config":{"options":[{"name":"技术部"},{"name":"产品部"}]}}]'
```
允许部分成功,返回结果逐项标明成功/失败状态。
### AI 字段创建示例
```bash
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "AI摘要" --type text \
--ai-config '{
"outputType":"text",
"prompt":[
{"type":"text","value":"请将下面内容总结成不超过80字的中文摘要:"},
{"type":"fieldRef","fieldId":"fld_content"}
],
"autoRecompute":true,
"enableWebSearch":false,
"enableThinking":true
}' --format json
```
说明:
- `outputType` 与字段类型需一致(如 `outputType=text``--type text`
- `prompt` 里通过 `fieldRef` 引用已有字段
- `autoRecompute=true` 表示引用字段变化后自动重算
- **AI 字段的 prompt 必须至少包含一个 `fieldRef` 引用**,纯文本 prompt 会被后端拒绝
### 关联字段与跨表引用字段
创建 `lookup`(关联引用)和 `filterUp`(查找引用)字段时,config 格式有严格要求:
#### bidirectionalLink / unidirectionalLink(关联字段)
```bash
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "关联客户" --type bidirectionalLink \
--config '{"linkedTableId":"<目标表tableId>","multiple":true}' --format json
```
#### lookup(关联引用,通过已有关联字段取值)
**前置条件**:本表必须已有一个 bidirectionalLink 或 unidirectionalLink 类型的关联字段。
```bash
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "客户城市" --type lookup \
--config '{"associateField":"<本表关联字段的fieldId>","valuesField":"<关联目标表中要取值的字段fieldId>","aggregator":"CONCATENATE"}' --format json
```
config 必填字段:
- `associateField`:**本表中**已有的关联字段(bidirectionalLink/unidirectionalLink)的 fieldId
- `valuesField`:**关联目标表中**要取值的字段 fieldId
- `aggregator`:聚合方式,可选 `SUM`|`AVERAGE`|`COUNT`|`MAX`|`MIN`|`CONCATENATE`
> 常见错误:`associateField` 不是目标表的 tableId,也不是目标表的字段 ID,而是**本表中关联字段自身的 fieldId**。
#### filterUp(查找引用,无需关联字段,直接跨表取值)
```bash
# 基本用法:字段对常量匹配
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "客户总金额" --type filterUp \
--config '{"targetSheet":"<目标表tableId>","filters":[{"fieldId":"<目标表字段Id>","operator":"equal","value":"匹配值","link":"AND"}],"valuesField":"<目标表中要取值的字段Id>","aggregator":"SUM"}' --format json
# 进阶用法:字段对字段动态匹配(currentSheetFieldId
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "本城市订单金额" --type filterUp \
--config '{"targetSheet":"<目标表tableId>","filters":[{"fieldId":"<目标表字段Id>","operator":"equal","currentSheetFieldId":"<本表字段Id>","link":"AND"}],"valuesField":"<目标表中要取值的字段Id>","aggregator":"SUM"}' --format json
```
config 必填字段:
- `targetSheet`:目标表的 tableId
- `filters`:至少一条筛选规则
- `fieldId`:目标表中用于匹配的字段 fieldId
- `operator`:仅支持 `equal``contain`(不支持 not_equal/not_contain
- `value`:常量匹配值(与 `currentSheetFieldId` 二选一)
- `currentSheetFieldId`:本表中用于动态匹配的字段 fieldId(与 `value` 二选一,实现每行按本表字段值去目标表筛选)
- `link`:多条件时的逻辑关系,`AND``OR`(单条件时可省略,多条件时建议显式指定;所有 filter 的 link 必须统一)
- `valuesField`:目标表中要取值的字段 fieldId
- `aggregator`:聚合方式,可选 `SUM`|`AVERAGE`|`COUNT`|`MAX`|`MIN`|`CONCATENATE`
## field update — 更新字段
```
Usage:
dws aitable field update [flags]
Example:
dws aitable field update --base-id <BASE_ID> --table-id <TABLE_ID> --field-id <FIELD_ID> --name "新字段名"
dws aitable field update --base-id <BASE_ID> --table-id <TABLE_ID> --field-id <FIELD_ID> --config '{"options":[{"name":"A"},{"name":"B"}]}'
Flags:
--base-id string Base ID (必填)
--config string 字段配置 JSON (不修改时省略)
--ai-config string AI 配置 JSON (不修改时省略)
--field-id string Field ID (必填)
--name string 新字段名称 (不修改时省略)
--table-id string Table ID (必填)
```
- 不可变更字段类型
- 更新 singleSelect/multipleSelect 的 options 时需传入完整列表,已有选项应回传原 id
- `--name` / `--config` / `--ai-config` 至少传一个
## field delete — 删除字段
```
Usage:
dws aitable field delete [flags]
Example:
dws aitable field delete --base-id <BASE_ID> --table-id <TABLE_ID> --field-id <FIELD_ID> --yes
Flags:
--base-id string Base ID (必填)
--field-id string 待删除字段 ID (必填)
--table-id string Table ID (必填)
```
不可逆。禁止删除主字段和最后一个字段。
@@ -0,0 +1,169 @@
# filters & sort — 筛选排序语法参考
> 视图(view)配置的 filter/sort/group **整体写入**请优先用 `view update filter` / `view update sort` / `view update group` 子命令,详见 [aitable-view-config.md](./aitable-view-config.md)。本文件聚焦于 `record query --filters` 与 view config filter 的语法和差异。
## filters 结构规范
### 强制规则
1. **根节点必须是逻辑操作符**`"operator"` 必须是 `"and"``"or"`,不能是 `"eq"` 等比较操作符
2. 比较操作必须放在根节点的 `"operands"` 数组内的对象中
3. `singleSelect``multipleSelect` 字段,推荐使用 **选项的 exact String 名称 (name)** 作为比较值
4. fieldId 必须通过 `field get` 获取,不能直接用字段名称
### 精简防呆模板
CLI 同时兼容两种子条件写法(推荐格式 A):
**格式 Aoperands 数组,推荐):**
```json
{
"operator": "and",
"operands": [
{"operator": "eq", "operands": ["fld_state", "进行中"]}
]
}
```
**格式 BfieldId/value 对象,CLI 自动转换):**
```json
{
"operator": "and",
"operands": [
{"fieldId": "fld_state", "operator": "eq", "value": "进行中"}
]
}
```
4 种衍生:
- **OR 查询**:根节点 `"operator"` 改为 `"or"`
- **多条件 AND**:在 `"operands"` 数组中增加对象
- **文本包含**:内层 `"operator"` 改为 `"contain"`
- **为空判断**`"operator":"un_exist"`operands 只需 `["fieldId"]`
### 支持的操作符(已验证完整列表)
| 操作符 | 含义 | operands 格式 |
|--------|------|--------------|
| `eq` / `ne` | 等于 / 不等于 | `["fieldId", "value"]` |
| `contain` / `exclusive` | 包含 / 不包含(文本模糊) | `["fieldId", "value"]` |
| `gt` / `gte` / `lt` / `lte` | 大于 / ≥ / 小于 / ≤ | `["fieldId", "numStr"]` |
| `exist` / `un_exist` | 有值 / 为空 | `["fieldId"]`(无需第二项) |
| `any_of` / `none_of` / `all_of` | 包含任一 / 不包含任一 / 全包含(多选字段) | `["fieldId", "optionName"]` |
| `date_eq` / `before` / `after` | 日期等于 / 早于 / 晚于 | `["fieldId", "dateStr"]` |
| `not_before` / `not_after` | 不早于(≥) / 不晚于(≤) | `["fieldId", "2026-05-22"]` |
> **操作符拼写必须严格匹配上表**,CLI 会在调用前校验,错误拼写会被拒绝。
>
> **没有 `date_between`(区间)操作符**,也**不支持 `from_now`**——date 字段不支持区间/相对过滤,传了会被 CLI 拒绝。范围查询用 `not_before` + `not_after` 组合,见下方专节。
### 日期字段过滤(date / 创建时间 / 修改时间)
日期类字段的过滤规则与其它字段**不同**,是线上反馈最高频的踩坑点。**经集成测试实测**确认的规则:
1. **只能用日期专用操作符**`date_eq` / `before` / `after` / `not_before` / `not_after` / `exist` / `un_exist`(与前端筛选 UI 的「等于 / 早于 / 晚于 / 早于或等于 / 晚于或等于 / 不为空 / 为空」一一对应)。
2. **比较值用日期字符串**,如 `"2026-05-22"`(也接受 RFC3339 / 毫秒时间戳,内部统一转成毫秒比较)。读取返回的是带时区 RFC3339(如 `"2026-05-22T00:00:00+08:00"`)。
3. **通用操作符 `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `contain` 对 date 字段无效**——无论传 ISO 字符串还是毫秒时间戳,都会**静默返回 0 条**。这是后端 date 字段的比较规则,不是 bug,CLI 也无法在本地拦截(不知道字段类型),务必用对操作符。
4. **没有区间操作符 `date_between`**,也**不支持 `from_now`(相对天数)**——均会静默返回 0 条,CLI 已直接拒绝。范围查询用 `not_before`(≥起点)+ `not_after`(≤终点)两个条件 `and` 组合。
| 需求 | 操作符 | 示例 operands |
|------|--------|--------------|
| 等于某天 | `date_eq` | `["fldDate", "2026-05-22"]` |
| 早于 / 晚于(不含当天) | `before` / `after` | `["fldDate", "2026-05-22"]` |
| 不早于(≥) / 不晚于(≤) | `not_before` / `not_after` | `["fldDate", "2026-05-22"]` |
| 有值 / 为空 | `exist` / `un_exist` | `["fldDate"]` |
**日期区间查询(替代 between)**——查 `2026-05-01 ~ 2026-05-31`(含端点):
```bash
dws aitable record query --base-id X --table-id Y \
--filters '{"operator":"and","operands":[{"operator":"not_before","operands":["fldDate","2026-05-01"]},{"operator":"not_after","operands":["fldDate","2026-05-31"]}]}'
```
### 常见错误拼写(CLI 会自动提示纠正)
| 错误写法 | 正确写法 | 说明 |
|------------|-----------|------|
| `equal` / `equals` / `is` / `==` | `eq` | 等于 |
| `not_equal` / `not_equals` / `is_not` / `!=` | `ne` | 不等于 |
| `like` / `contains` / `include` | `contain` | 文本包含 |
| `greater_than` | `gt` | 大于 |
| `less_than` | `lt` | 小于 |
| `not_eq` / `not_contain` / `is_empty` | `ne` / `exclusive` / `un_exist` | 其他易混淆 |
### 错误示例
**缺失根节点 and/or**(API 将忽略该 filter,返回全表):
```json
{"operator":"eq","operands":["fldXXX","本科"]}
```
**传入选项 ID 而非名称**(可能导致匹配不到 0 记录):
```json
{"operator":"and","operands":[{"operator":"eq","operands":["fldXXX","CXzrOHK9JI"]}]}
```
### 完整示例
单条件:
```bash
dws aitable record query --base-id X --table-id Y \
--filters '{"operator":"and","operands":[{"operator":"eq","operands":["fldStatusId","进行中"]}]}'
```
多条件 AND
```bash
dws aitable record query --base-id X --table-id Y \
--filters '{"operator":"and","operands":[{"operator":"eq","operands":["fldStatusId","进行中"]},{"operator":"gt","operands":["fldStockId","0"]}]}'
```
## sort 结构规范
`--sort` 传 JSON 数组,排序方向字段**必须是 `direction`**,不要使用 `order`
```bash
--sort '[{"fieldId":"fldXXX","direction":"desc"}]'
```
多字段排序:
```bash
--sort '[{"fieldId":"fldPriority","direction":"desc"},{"fieldId":"fldCreatedAt","direction":"asc"}]'
```
---
## view update --config 中的 filter / sort 格式
> **重要区分**`record query --filters` 和 `view update --config` 中的 filter **格式不同**
| 场景 | filter 格式 | 说明 |
|------|-------------|------|
| `record query --filters` | **对象**`{"operator":"and","operands":[...]}` | 直接传最外层逻辑对象 |
| `view update --config` 的 filter | **数组**`[{"operator":"and","operands":[...]}]` | 外面多一层数组包裹 |
| `view update --config` 的 sort | **数组**`[{"fieldId":"X","direction":"asc"}]` | 与 record query --sort 一致 |
### 正确示例
```bash
# view update 设置筛选(filter 是数组)
dws aitable view update --base-id X --table-id Y --view-id Z \
--config '{"filter":[{"operator":"and","operands":[{"operator":"eq","operands":["fldStatus","待处理"]}]}]}'
# view update 设置排序(sort 是数组)
dws aitable view update --base-id X --table-id Y --view-id Z \
--config '{"sort":[{"fieldId":"fldPriority","direction":"desc"}]}'
# 同时设置 filter + sort + visibleFieldIds
dws aitable view update --base-id X --table-id Y --view-id Z \
--config '{"filter":[{"operator":"and","operands":[{"operator":"eq","operands":["fldStatus","进行中"]}]}],"sort":[{"fieldId":"fldDate","direction":"asc"}],"visibleFieldIds":["fld1","fld2","fld3"]}'
```
### CLI 自动容错
CLI 会自动修正以下常见错误格式(不会报错,但建议直接使用正确格式):
| 错误写法 | CLI 自动修正为 |
|----------|---------------|
| `"filter":{"operator":"and",...}` (对象) | `"filter":[{"operator":"and",...}]` (数组) |
| `"sort":{"fieldId":"X","direction":"asc"}` (对象) | `"sort":[{"fieldId":"X","direction":"asc"}]` (数组) |
| 子条件用 MCP 简写 `{"fieldId":"X","operator":"eq","value":"Y"}` | 自动转为 `{"operator":"eq","operands":["X","Y"]}` |
@@ -0,0 +1,130 @@
# form — 表单管理
## 命令一览
| 命令 | 用途 |
|------|------|
| `form list` | 列出数据表下所有表单视图 |
| `form get` | 按 viewId 取单个表单详情(list_form_views + viewIds 过滤) |
| `form create` | 创建表单视图(等价于 `view create --view-type FormDesigner` |
| `form update` | 更新表单标题或描述 |
| `form delete` | 删除表单视图(不可逆) |
| `form field list` | 列出表单可见字段 |
| `form field update` | 更新字段必填/描述 |
| `form field hide` | 在表单中隐藏/显示字段(不影响底层数据表字段) |
| `form share get` | 获取分享配置 |
| `form share update` | 开启/关闭分享 |
| `form questions create` | 添加题目(等价于 `field create`,命令位置上的别名) |
| `form questions delete` | 删除题目(等价于 `field delete`,命令位置上的别名) |
## 建议操作顺序
```bash
# 1) 列出数据表下的表单视图
dws aitable form list --base-id BASE_ID --table-id TABLE_ID --format json
# 2) 查看单个表单详情
dws aitable form get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json
# 3) 查看表单字段配置
dws aitable form field list --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json
# 4) 查看分享配置
dws aitable form share get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json
```
开启并取得可发送链接的最短闭环使用 canonical shortcut
```bash
dws aitable form create --base-id BASE_ID --table-id TABLE_ID --name "表单名" --format json
dws aitable +form-share-update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --enabled true --format json
dws aitable +form-share-get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json
```
从最后一次返回直接读取 `data.shareFormUuid`,分享地址为 `https://alidocs.dingtalk.com/i/form/{shareFormUuid}`。字段已存在时不要再调用 Help、Catalog、`form get`,也不要换 `--verbose``raw``pretty` 重复请求。用户还要求发送时,把该完整 URL 交给 Chat 的发送命令并检查真实发送回执。
## 要点
- **创建表单**有两种等价方式:
- `form create --name "表单名"`(推荐,语义清晰)
- `view create --view-type FormDesigner --name "表单名"`(底层一致)
- `form update` 支持 `--title``--name` 两个等价参数;至少需传一项
- `form field update` 必须传 `--required``--field-description` 至少一项
- `form field hide` 仅控制字段在表单中的可见性,不影响底层数据表字段
- **题目管理**与字段管理本质相同(题目 = 表格字段):
- `form questions create``field create` 入参完全一致(`--fields` JSON 或 `--name --type`
- `form questions delete``field delete` 入参完全一致(必传 `--field-id`
- 设置必填要在 create 后用 `form field update --required true` 单独调一次
## form 子命令
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `form list` | 列出表单视图 | `--base-id` `--table-id` | 返回 viewId/name/title/createdAt |
| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 内部基于 list_form_views 过滤 |
| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | viewType=FormDesigner |
| `form update` | 更新表单 | `--base-id` `--table-id` `--view-id` | `--title`/`--name`(等价)和 `--description` 至少传一项;同时传 title/name 时 title 优先 |
| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` `--yes` | 不可逆 |
## form field 子命令
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `form field list` | 列出表单字段 | `--base-id` `--table-id` `--view-id` | 返回 fieldId/name/type/required/hidden/descriptionhidden=true 的字段不在此返回) |
| `form field update` | 更新表单字段 | `--base-id` `--table-id` `--view-id` `--field-id` | `--required``--field-description` 至少一项 |
| `form field hide` | 切换字段隐藏 | `--base-id` `--table-id` `--view-id` `--field-id` `--hidden` | `--hidden true` 隐藏 / `--hidden false` 显示 |
## form questions 子命令
`form questions create/delete``field create/delete` 入参、行为完全一致,只是命令位置归属于 `form` 命令组,方便从表单视角操作题目。
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `form questions create` | 添加题目 | `--base-id` `--table-id` + (`--fields``--name --type`) | 入参与 `field create` 完全一致 |
| `form questions delete` | 删除题目 | `--base-id` `--table-id` `--field-id` `--yes` | 入参与 `field delete` 完全一致;不可逆;批量需多次调用 |
## form share 子命令
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `form share get` | 获取分享配置 | `--base-id` `--table-id` `--view-id` | 返回 enabled/status/shareFormUuid |
| `form share update` | 开启/关闭分享 | `--base-id` `--table-id` `--view-id` `--enabled` | `--enabled true` 开启 / `--enabled false` 关闭。注意:UI 上"发布并分享"按钮是另一概念,本命令只切换内部 enabled 标志,开启后需在 UI 刷新页面才会看到分享面板 |
## 完整工作流示例
> **占位符约定**
> - `BASE_ID` 来自 `dws aitable base list` / `base search` 返回的 `data.bases[].baseId`
> - `TABLE_ID` 来自 `dws aitable base get --base-id BASE_ID` 返回的 `data.tables[].tableId`
> - `VIEW_ID` 来自步骤 1 `form create` 返回的 `data.viewId`
> - `FIELD_ID` 来自步骤 2 `form questions create` 返回的 `data.results[].fieldId`
```bash
# 1) 创建表单 → 取返回的 data.viewId 作为 VIEW_ID
dws aitable form create --base-id BASE_ID --table-id TABLE_ID --name "员工信息收集" --format json
# 2) 添加题目 → 取返回的 data.results[].fieldId 作为 FIELD_ID
dws aitable form questions create --base-id BASE_ID --table-id TABLE_ID \
--fields '[{"fieldName":"姓名","type":"text"},{"fieldName":"邮箱","type":"text"}]' --format json
# 3) 配置表单标题与描述
dws aitable form update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \
--title "员工信息收集" --description "请填写您的基本信息" --format json
# 4) 设置题目必填(FIELD_ID 来自步骤 2)
dws aitable form field update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \
--field-id FIELD_ID --required true --format json
# 5) 隐藏不需要的题目
dws aitable form field hide --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \
--field-id FIELD_ID --hidden true --format json
# 6) 开启分享(注意:开启后需 UI 刷新页面才会看到分享面板)
dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \
--enabled true --format json
```
## 返回结构补充
- `form list` 返回 `data.formViews[]`**每条仅含** `viewId/name/title/createdAt``shareFormUuid` 不在此返回,请用 `form share get` 单独获取。
- `form get` 返回结构与 `form list` 完全一致(`data.formViews[]`),仅含一条记录(与请求 viewId 一致)。Agent 提取时仍走 `data.formViews[0]`
- `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`
@@ -0,0 +1,224 @@
# AI 表格公式字段指南
> 当用户要创建 formula 类型字段、编写表内计算公式、做派生指标时,必须先读本文档。
## 1. 何时使用 formula 字段
| 场景 | 用 formula | 不用 formula |
|------|-----------|-------------|
| 长期展示在表中的派生值(如"总价=单价×数量" | ✅ | |
| 条件标记(如"超期=IF(截止日期<TODAY(),'是','否')" | ✅ | |
| 文本拼接(如"全名=姓&名" | ✅ | |
| 一次性统计分析(如"本月总销售额" | | ✅ 用 record stats 服务端聚合 |
| 跨表查找引用 | | ✅ 用 lookup 字段(见下方说明) |
## 2. 创建 formula 字段
```bash
dws aitable field create \
--base-id <baseId> \
--table-id <tableId> \
--name "总价" \
--type formula \
--config '{"formula": "[单价] * [数量]"}' \
--format json
```
### config 结构
```json
{
"formula": "<公式表达式>"
}
```
- `formula` 是唯一必填字段
- 表达式中引用字段使用 **方括号 + 字段名**`[字段名]`
- 字段名必须精确匹配(含空格、大小写)
## 3. 公式语法
### 3.1 引用规则
| 引用方式 | 语法 | 说明 |
|---------|------|------|
| 引用本表字段 | `[字段名]` | 字段名必须精确匹配 |
| 引用关联表字段 | 不支持 | 需要用 lookup 字段 |
### 3.2 常用函数分类
#### 数值计算
| 函数 | 用途 | 示例 |
|------|------|------|
| `+` `-` `*` `/` | 四则运算 | `[单价] * [数量]` |
| `SUM(...)` | 求和 | `SUM([Q1], [Q2], [Q3], [Q4])` |
| `ROUND(value, digits)` | 四舍五入 | `ROUND([金额] * 0.1, 2)` |
| `ABS(value)` | 绝对值 | `ABS([差额])` |
| `MAX(a, b, ...)` | 最大值 | `MAX([成绩1], [成绩2])` |
| `MIN(a, b, ...)` | 最小值 | `MIN([报价1], [报价2])` |
#### 文本处理
| 函数 | 用途 | 示例 |
|------|------|------|
| `&` | 文本拼接 | `[姓] & [名]` |
| `CONCATENATE(...)` | 拼接多个值 | `CONCATENATE([城市], "-", [区])` |
| `LEFT(text, n)` | 取左侧 n 字符 | `LEFT([编号], 4)` |
| `RIGHT(text, n)` | 取右侧 n 字符 | `RIGHT([手机], 4)` |
| `LEN(text)` | 文本长度 | `LEN([备注])` |
| `UPPER(text)` / `LOWER(text)` | 大小写转换 | `UPPER([代码])` |
#### 逻辑判断
| 函数 | 用途 | 示例 |
|------|------|------|
| `IF(条件, 真值, 假值)` | 条件判断 | `IF([金额] > 1000, "大额", "普通")` |
| `AND(a, b, ...)` | 逻辑与 | `IF(AND([状态]="完成", [评分]>=4), "优秀", "")` |
| `OR(a, b, ...)` | 逻辑或 | `IF(OR([等级]="A", [等级]="B"), "通过", "未通过")` |
| `NOT(expr)` | 逻辑非 | `NOT([已归档])` |
| `SWITCH(expr, v1, r1, v2, r2, ..., default)` | 多条件匹配 | `SWITCH([状态], "待办","🔴", "进行中","🟡", "完成","🟢", "")` |
#### 日期函数
| 函数 | 用途 | 示例 |
|------|------|------|
| `TODAY()` | 当前日期 | `IF([截止日期] < TODAY(), "已逾期", "正常")` |
| `NOW()` | 当前时间 | `NOW()` |
| `YEAR(date)` / `MONTH(date)` / `DAY(date)` | 提取年/月/日 | `YEAR([创建时间])` |
| `DATEDIF(start, end, unit)` | 日期差 | `DATEDIF([开始], [结束], "d")` 返回天数 |
| `DATEADD(date, count, unit)` | 日期加减 | `DATEADD([创建时间], 7, "d")` |
> `DATEDIF` 的 unit 参数:`"y"`=年, `"m"`=月, `"d"`=天
#### 空值处理
| 函数 | 用途 | 示例 |
|------|------|------|
| `BLANK()` | 空值常量 | `IF([备注] = BLANK(), "无", [备注])` |
| `IF(field, ...)` | 字段为空时视为 false | `IF([评分], [评分], 0)` |
## 4. 常见公式模板
### 4.1 计算类
```
// 含税价格
[不含税价] * (1 + [税率])
// 完成率百分比
[已完成数] / [总数]
// 折扣后价格
[原价] * (1 - [折扣率])
```
### 4.2 状态标记类
```
// 逾期标记
IF([截止日期] < TODAY(), "⚠️ 已逾期", "正常")
// 优先级标签
SWITCH([优先级], "紧急","🔴P0", "高","🟠P1", "中","🟡P2", "低","🟢P3", "")
// 进度状态
IF([进度] >= 1, "✅ 已完成", IF([进度] > 0, "🔄 进行中", "⏳ 未开始"))
```
### 4.3 文本拼接类
```
// 编号生成
"PRJ-" & [项目编码] & "-" & [序号]
// 地址拼接
[省] & [市] & [区] & [详细地址]
```
## 5. 注意事项与限制
### 5.1 formula 字段是只读的
- formula 字段的值由系统自动计算,**不能通过 `record create/update` 写入**
- 如果用户要"设置某个计算结果",应引导其修改源字段
### 5.2 字段名必须精确
- 公式中的 `[字段名]` 必须与表中实际字段名完全一致
- 创建 formula 字段前,先通过 `field get` 确认字段名
### 5.3 循环引用
- formula 字段不能引用自身
- 不能形成 A→B→A 的循环引用
### 5.4 与跨表引用字段的区别
钉钉 AI 表格有两种跨表取值方式:`lookup`(关联引用)和 `filterUp`(查找引用)。
| 维度 | formula | lookup (关联引用) | filterUp (查找引用) |
|------|---------|-----------------|-------------------|
| 字段类型 | `formula` | `lookup` | `filterUp` |
| 数据来源 | 本表字段 | 通过已有关联字段(bidirectionalLink/unidirectionalLink)取关联表字段 | 直接指定目标表 + 筛选条件取值 |
| 前置条件 | 无 | 必须先有关联字段 | 无需关联字段 |
| 适用场景 | 本表内计算、条件判断 | "我关联了某条记录,取它的某个字段值" | "在另一张表里按条件查找记录并聚合取值" |
#### lookup config(已验证)
```json
{
"associateField": "<本表中的关联字段 fieldIdbidirectionalLink/unidirectionalLink 类型)>",
"valuesField": "<关联目标表中要取值的字段 fieldId>",
"aggregator": "SUM|AVERAGE|COUNT|MAX|MIN|CONCATENATE"
}
```
创建示例:
```bash
dws aitable field create --base-id <baseId> --table-id <tableId> \
--name "关联名称" --type lookup \
--config '{"associateField":"<linkFieldId>","valuesField":"<targetFieldId>","aggregator":"CONCATENATE"}'
```
#### filterUp config(已验证)
```json
{
"targetSheet": "<目标表 tableId>",
"filters": [
{
"fieldId": "<目标表字段Id>",
"operator": "equal|contain",
"value": "<匹配值>",
"link": "AND"
}
],
"valuesField": "<目标表中要取值的字段Id>",
"aggregator": "SUM|AVERAGE|COUNT|MAX|MIN|CONCATENATE"
}
```
> `filters` 必须非空(至少一条筛选规则)。
> `filters[].operator` 仅支持:`equal`、`contain``not_equal`/`not_contain`/`is_empty` 等均不支持)。
> `filters[].link` 统一为 `"AND"` 或 `"OR"`。
### 5.5 创建前检查清单
1. 已通过 `field get` 确认所有引用字段的精确名称
2. 引用字段不包含 formula/lookup 等只读字段(可能导致二次计算延迟)
3. 公式语法正确(括号匹配、函数名正确)
4. 字段类型兼容(数值运算的字段确实是 number 类型)
## 6. 更新 formula 字段
```bash
dws aitable field update \
--base-id <baseId> \
--table-id <tableId> \
--field-id <fieldId> \
--config '{"formula": "[新字段A] + [新字段B]"}' \
--format json
```
更新时只需传新的 `formula` 表达式,系统会自动重新计算所有记录。
@@ -0,0 +1,56 @@
# 主键文档管理
## 适用场景
当需要为 AI 表格中的记录创建或查询关联的主键文档时使用。主键文档是 primaryDoc 类型字段对应的钉钉在线文档,可通过 `dws doc` 进行内容读写。
## 命令
### 查询主键文档
```bash
dws aitable +record-primary-doc-get --base-id BASE_ID --table-id TABLE_ID --record-id RECORD_ID
```
**参数:**
- `--base-id`(必填):Base ID
- `--table-id`(必填):Table ID
- `--record-id`(必填):Record ID
**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update``--node` 参数。若该记录尚未创建主键文档,`nodeId` 为 null。
### 创建主键文档
```bash
dws aitable +record-primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID
```
**参数:**
- `--base-id`(必填):Base ID
- `--table-id`(必填):Table ID
- `--field-id`(必填):主键字段 ID,必须是 primaryDoc 类型(通过 `dws aitable field get` 查看字段类型)
- `--record-id`(必填):Record ID
**返回:** `data.nodeId` — 创建或已存在的主键文档 nodeId。
**幂等性:** 若该记录已有主键文档,直接返回已有文档的 nodeId,不会重复创建。
## 注意事项
- `fieldId` 必须是 primaryDoc 类型,否则返回 `INVALID_FIELD_TYPE` 错误
- `primaryDoc` 是建表时的首字段能力,不能在已有普通首字段之后补建,也不能把普通字段改成 primaryDoc。需要该能力时应新建以 primaryDoc 为首字段的数据表并迁移数据;未经用户明确授权不要自动迁移。
- 传入不存在的 `recordId` 会返回 `RECORD_NOT_FOUND` 错误
- 创建后可通过 `dws doc update --node <nodeId>` 写入文档内容,或 `dws doc read --node <nodeId>` 读取
## 典型工作流
```bash
# 1. 查询字段目录,拿到 primaryDoc 字段的 fieldId
dws aitable field get --base-id BASE_ID --table-id TABLE_ID
# 2. 为记录创建主键文档
dws aitable +record-primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID
# 3. 拿到返回的 nodeId,用 dws doc 写入内容
dws doc update --node <data.nodeId> --content "# 项目方案\n\n文档正文内容..."
```
@@ -0,0 +1,52 @@
# record create — 新增记录
## 命令格式
```
Usage:
dws aitable record create [flags]
Example:
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldTextId":"文本内容","fldNumId":123}}]'
Flags:
--base-id string Base ID (必填)
--records string 记录列表 JSON 数组,单次最多 100 条 (必填,与 --records-file 二选一)
--records-file string 从文件读取 records JSON(替代 --records,适合超长数据或 Windows 环境)
--table-id string Table ID (必填)
```
## Windows / 超长 JSON 推荐
将 records JSON 写入文件,用 `--records-file ./records.json` 传入,避免命令行截断和引号转义问题。
## 常见错误(严格避免)
| 错误 | 说明 |
|------|------|
| 参数名用 `--data` | ❌ 参数名是 `--records`,不是 `--data` |
| cells key 用字段名 | ❌ cells key 必须是 fieldId(如 `fldXXX`),不是字段名称(如 `"课程名称"` |
| 不先获取 fieldId | ❌ 必须先 `field get` 获取 fieldId,再写入记录 |
| 单次超 100 条 | ❌ 单次最多 100 条,超过需分批 |
| 附件/图片字段直传 URL | ❌ 严禁 `{"url":"https://..."}` — 会触发 TIMEOUT_ERROR。必须先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。详见 [aitable-attachment.md](./aitable-attachment.md) |
## 正确流程
```bash
# 先获取 fieldId
dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID> --format json
# 从返回中提取 fieldId(如 fldABC123
# 再用 fieldId 写入记录
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldABC123":"Python入门"}}]' --format json
# 从创建响应的 data.newRecordIds[] 提取新 ID,并回读确认真实写入值
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> \
--record-ids <NEW_RECORD_ID> --format json
```
创建成功以 `data.newRecordIds[]` 为 ID 来源;不要把整个 `data` 当作单个 recordId,也不要只以退出码作为写入成功证据。
## cells 写入格式
各字段类型的写入格式见 [aitable-cell-value.md](./aitable-cell-value.md)。
@@ -0,0 +1,20 @@
# record delete — 删除记录
## 命令格式
```
Usage:
dws aitable record delete [flags]
Example:
dws aitable record delete --base-id <BASE_ID> --table-id <TABLE_ID> --record-ids rec1,rec2 --yes
Flags:
--base-id string Base ID (必填)
--record-ids string 待删除记录 ID 列表,逗号分隔,最多 100 条 (必填)
--table-id string Table ID (必填)
```
## 注意事项
- **不可逆操作**,调用前建议先 `record query` 确认目标记录
- 需要先通过 `record query` 获取 recordId
- 单次最多删除 100 条记录
@@ -0,0 +1,97 @@
# 行记录变更历史(record history-list
按 recordId 查询单条记录的全部变更历史,用于审计、回溯字段变更、定位操作人。
## 命令
```
dws aitable record history-list \
--base-id BASE_ID --table-id TABLE_ID --record-id REC_ID \
[--offset N] [--limit M]
```
| flag | 说明 |
|------|------|
| `--base-id` | 所属 Base ID(必填,可用 `--base` 别名) |
| `--table-id` | 所属 Table ID(必填) |
| `--record-id` | 目标记录 ID(必填,单条;不支持批量) |
| `--offset` | 分页偏移量,默认 0 |
| `--limit` | 每页返回数量,范围 [1, 50],默认 20 |
## 返回结构
```jsonc
{
"data": {
"histories": [
{
"type": "field_change", // 变更类型
"action": "update", // 操作动作: create / update / delete
"newValue": "{\"...\":\"...\"}", // 变更后的值(JSON 字符串)
"oldValue": "{\"...\":\"...\"}", // 变更前的值(JSON 字符串)
"operateTime": 1733123456789, // 操作时间(毫秒级时间戳)
"typeChangedFields": "{...}", // 类型变更的字段信息(JSON 字符串)
"version": 7 // 版本号(单调递增)
}
]
}
}
```
`newValue` / `oldValue` / `typeChangedFields` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值。
## 字段含义速查
| 字段 | 用途 |
|------|------|
| `type` | 高层分类:`record_create` / `field_change` / `record_delete` 等。先按 type 过滤大类。 |
| `action` | 三态:`create` / `update` / `delete`。比 type 粗,但便于按"动作"统计。 |
| `version` | 单调递增整数;同一 record 越新值越大。**用作"上一条 vs 这一条"的稳定排序键**。 |
| `operateTime` | 毫秒时间戳;可格式化成可读时间。多条同 version 的极端场景用 operateTime 兜底排序。 |
## 典型用法
### 1. 看一条记录被改过几次
```bash
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \
| jq '.data.histories[] | {version, action, operateTime}'
```
### 2. 翻页拉全量历史
```bash
# 第 1 页(最新 20 条)
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --offset 0
# 第 2 页
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --offset 50
```
`limit` 上限 50,需要更多请增加 `offset` 翻页。
### 3. 回溯某字段最近一次值
```bash
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --format json \
| jq '[.data.histories[] | select(.action == "update")][0].oldValue'
```
### 4. 找出删除事件(如果存在 delete history
```bash
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \
| jq '.data.histories[] | select(.action == "delete") | {version, operateTime}'
```
## 注意事项
- 一次只能查一条 record;如需批量审计多条记录请循环调用。
- 仅返回**字段值变更**与**记录生命周期事件**;视图、字段定义、表结构变更不在此 history 里。
- 历史保留时长由 server 决定,过老的记录可能不再返回。
## 与其他 record 命令的关系
- 想看记录"现在长什么样" → `record query` / `record get`
- 想看记录"过去长什么样、什么时候改的" → `record history-list`(本命令)
- 想看"这张表整体改过什么" → 当前 CLI 不支持表级 history;只能逐 record 查
@@ -0,0 +1,47 @@
# 行命名规则枚举键(recordNameKey)映射
`dws aitable table update --record-name-key <枚举键>` 用于设置数据表的"行命名规则"——卡片/详情页里"行"的展示别名。**取值是固定枚举,不是字段 ID**;传非法值服务端返回 `INVALID_RECORD_NAME_KEY`
## 中文 → 枚举键(按 UI 下拉顺序)
| 用户说 | --record-name-key | 用户说 | --record-name-key |
|---|---|---|---|
| 记录 | `ji_lu`(默认) | 项目 | `project` |
| 任务 | `task` | 事件 | `event` |
| 请求 | `request` | 活动 | `campaign` |
| 目标 | `objective` | 交付物 | `deliverable` |
| 资产 | `asset` | 客户 | `customer` |
| 订单 | `order` | 联系人 | `contact` |
| 物料/物品 | `item` | 问题 | `question``issue` |
| 工单 | `ticket` | 候选人 | `candidate` |
| 商机/机会 | `opportunity` | 会议 | `meeting` |
| 成员 | `member` | OKR | `okr` |
## 其他常用键(按场景分组)
- **业务流程**`approval` / `application` / `case` / `decision` / `delivery` / `payment` / `purchase_order` / `quote` / `release`
- **HR / 财务**`employee` / `expense` / `budget` / `invoice`
- **产品 / 研发**`feature` / `feedback` / `idea` / `bug` / `requirement` / `risk` / `sprint` / `story` / `subtask` / `epic`
- **CRM**`account` / `lead` / `prospect` / `deal`
- **运营 / 支持**`note` / `report` / `topic` / `session` / `service`
- **资源 / 通用**`file` / `document` / `product` / `team` / `user` / `vendor` / `key_result` / `metric`
完整集合较大(共 273 个),服务端校验;以上未列出的合法键也可直接传(如 `goal` / `okr` / `pillar` / `phase` / `milestone` 等)。
## 使用示例
```bash
# 用户说"把这张表的行叫'任务'吧" → 传 task
dws aitable table update --base-id BASE --table-id TBL --record-name-key task
# 用户说"换成项目" → 传 project
dws aitable table update --base-id BASE --table-id TBL --record-name-key project
# 用户说"恢复成默认(记录)" → 传 ji_lu
dws aitable table update --base-id BASE --table-id TBL --record-name-key ji_lu
```
## 注意
- recordNameKey **不会在 `table get` 响应里回显**`get_tables` DTO 设计上不暴露该字段);写入是否成功以 `table update` 的 set response 是否回填 `recordNameKey` 字段为准。
- 中文别名是 server 内置 i18n,UI 显示用户对应的国际化文案,CLI 必须传英文枚举键。
@@ -0,0 +1,120 @@
# record query — 查询记录
## 命令格式
```
Usage:
dws aitable record query [flags]
Example:
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID>
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --record-ids rec1,rec2
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --query "关键词" --limit 50
Flags:
--base-id string Base ID (必填)
--cursor string 分页游标,首次不传
--field-ids string 返回字段 ID 列表,逗号分隔,单次最多 100 个
--filters string 结构化过滤条件 JSON
--query string 全文关键词搜索
--limit int 单次最大记录数,默认 100,最大 100
--record-ids string 指定记录 ID 列表,逗号分隔,单次最多 100 个
--sort string 排序条件 JSON 数组
--table-id string Table ID (必填)
--all 启用自动翻页,循环获取并合并所有记录后统一输出
--page-limit int 自动翻页最大页数(仅 --all 时生效)。默认 50,设为 0 表示无限制
```
两种模式: 按 ID 取(传 record-ids,忽略 filters/sort)或条件查(filters+sort+cursor 分页)。
## 自动翻页(--all + --page-limit
- 传入 `--all` 启用自动翻页,CLI 自动循环获取并合并所有记录后统一输出
- `--page-limit` 控制最大翻页次数,默认 50 页(5000 条),设为 0 表示无限制
- 页间间隔 200ms,中途网络错误会 graceful stop 并输出已获取的数据
- **被截断时**(达到 page-limit 但仍有数据):输出中包含 `"hasMore": true``"cursor": "..."` 字段,可通过 `--cursor` 从断点继续拉取
- 适用于需要一次性获取全量数据的场景(如导出、统计、批量处理)
```bash
# 默认(最多 50 页 = 5000 条)
dws aitable record query --base-id X --table-id Y --all
# 无限制(拉完为止)
dws aitable record query --base-id X --table-id Y --all --page-limit 0
# 从上次断点继续
dws aitable record query --base-id X --table-id Y --all --cursor "上次返回的cursor"
```
## 排序参数规范
`--sort` 需要传 JSON 数组,排序方向字段必须是 `direction``asc``desc`),不要使用 `order`
正确示例:
```bash
--sort '[{"fieldId":"wm8ns9bw2vmucb45xj3ix","direction":"desc"}]'
```
## filters 结构
详细语法见 [aitable-filter-sort.md](./aitable-filter-sort.md)。
快速模板:
```json
{"operator":"and","operands":[{"operator":"eq","operands":["<fieldId>","<value>"]}]}
```
> **singleSelect/multipleSelect 过滤**filters 中可传 option id 或 option name,但建议优先用 **option id**(通过 `field get` 获取),更可靠。
## 减少响应体积
字段较多时,用 `--field-ids` 仅返回需要的字段,可显著减少返回数据量。
## 常见错误
- `--filters` 根节点直接用 `"operator":"eq"` → API 静默忽略,返回全表
- `--sort``"order":"desc"` → 必须用 `"direction":"desc"`
- 不加 `--field-ids` 拉全字段 → 大表响应体积过大
- 全量拉取后在 context 里手动统计 → 应优先用 `--filters` 服务端过滤
## record query-empty — 找空行
`record query-empty` 是与 `record query` 平行的独立子命令,专门按表内顺序扫描出"完全没填用户字段"的空行。
```bash
dws aitable record query-empty --base-id BASE_ID --table-id TABLE_ID
```
| flag | 说明 |
|------|------|
| `--base-id` / `--base` | 必填 |
| `--table-id` | 必填 |
| `--limit` | 单次**扫描预算**(不是返回数);范围 [1, 100],默认 100 |
| `--cursor` | 分页游标。响应中 `nextCursor` 非空 → 用它翻页继续扫;nextCursor 为空(或不存在)→ 已扫完整表 |
返回结构:
```jsonc
{ "data": { "records": [...], "nextCursor": "..." } }
```
### 关键语义
1. **`--limit` 是扫描预算不是返回数**:可能扫了 100 条但全部非空,本页 `records: []`
2. **本页空 records ≠ 全表无空行**:必须看 `nextCursor`nextCursor 还在就要继续翻。
3. **空行定义**:除系统字段(recordId / 创建人 / 创建时间 / 修改人 / 修改时间)外,所有 cell 都是 null、空字符串、空集合或空 Map。一般是用户在 UI 上"插入空行"产生的。
### 典型用法
```bash
# 扫一页,看本页有没有空行
dws aitable record query-empty --base-id BASE --table-id TBL
# 翻页
dws aitable record query-empty --base-id BASE --table-id TBL --cursor <上次的nextCursor>
# 把整表扫完(手动循环 cursor)
NC=""
while : ; do
R=$(dws aitable record query-empty --base-id BASE --table-id TBL ${NC:+--cursor "$NC"} --format json)
echo "$R" | jq '.data.records[] | .recordId'
NC=$(echo "$R" | jq -r '.data.nextCursor // empty')
[ -z "$NC" ] && break
done
```
@@ -0,0 +1,54 @@
# 行记录分享链接(record share-url
按 recordId 批量获取记录的分享链接,把某行单独发给同事查看。
## 命令
```
dws aitable record share-url \
--base-id BASE_ID --table-id TABLE_ID \
--record-ids rec1,rec2,rec3 \
[--view-id VIEW_ID]
```
| flag | 说明 |
|------|------|
| `--base-id` | 所属 Base ID(必填,可用 `--base` 别名) |
| `--table-id` | 所属 Table ID(必填) |
| `--record-ids` | 目标 Record ID 列表,CSV 逗号分隔,**单次最多 20 条**(必填) |
| `--view-id` | 视图 ID(可选)。带上后链接打开会落在该视图上下文里 |
## 返回结构
```jsonc
{
"data": {
"items": [
{ "recordId": "rec1", "shareUrl": "https://..." },
{ "recordId": "rec2", "shareUrl": "https://..." }
]
}
}
```
`shareUrl` 为 null 表示该条获取失败(不影响其他条目)。
## 典型用法
```bash
# 一次拿一条记录的链接
dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1
# 批量拿,配合 jq 过滤出 url
dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1,rec2,rec3 --format json \
| jq '.data.items[] | {recordId, shareUrl}'
# 带视图上下文(链接打开时落在指定视图)
dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1 --view-id viw_VIP
```
## 注意事项
- **单次最多 20 条**,超出请客户端拆批。
- 该链接是分享链接(不是源文档链接),打开后看到的是该 record 的只读详情页。
- 取消单条分享 / 关闭整表分享当前 CLI 不支持,需要在 AI 表格 Web 端操作。
@@ -0,0 +1,59 @@
# record stats / group-stats — 服务端聚合统计
统计任务优先使用服务端聚合,不要先用 `record query --all` 下载全表再计算。
## 命令选择
| 需求 | 命令 | 底层接口 |
|------|------|----------|
| 总数、求和、平均值、最大/最小值、中位数、完整率等标量统计 | `record stats` | `query_records_stats` |
| 按字段分组统计 | `record group-stats` + `--group` | `query_stats` |
| 满足条件的唯一门店/客户/商品数量 | `record group-stats` + `distinct`,不传 `--group` | `query_stats` |
## 不分组统计
```bash
dws aitable record stats \
--base-id <BASE_ID> \
--table-id <TABLE_ID> \
--stats '[{"fieldId":"<FIELD_ID>","statsType":"COUNT"}]' \
--format json
```
- `statsType` 必须大写。
- `--stats` 单次最多 20 项,同一 `fieldId` 不得重复;同字段多个指标拆成多次调用。
- 支持基础类型 `COUNT``COUNT_COLUMN``SUM``AVG``MAX``MIN`,以及运行时支持的 `MEDIAN``STANDARD_DEVIATION``RANGE``DISTINCT``DISTINCT_RATIO`、完整率、勾选率和日期统计类型。
- 统计全部匹配记录时省略 `--limit`;传入 limit 会改变统计范围。
- 可选参数:`--filters``--sort``--keyword``--search-field-ids``--data-version`
## 分组或去重统计
```bash
dws aitable record group-stats \
--base-id <BASE_ID> \
--table-id <TABLE_ID> \
--group '[{"fieldId":"<GROUP_FIELD_ID>","direction":"ASC","fieldConfig":null,"arraySplitMode":true}]' \
--stats '[{"fieldId":"<VALUE_FIELD_ID>","statsType":"avg"}]' \
--limit 1000 \
--format json
```
- `statsType` 必须小写;基础类型为 `sum``avg``count``max``min`,后端还可能支持 `median``distinct``distinct_ratio` 等高级类型。
- `--group``--sort` 都是 JSON 数组编码后的字符串,CLI 会原样映射到 MCP 的 `group` / `sortDsl`
- 分组结果最多 1000 行。不要依赖服务端 limit 选择 Top N;应在基数不超过 1000 时取完整分组结果后排序。
- 条件唯一实体计数不传 `--group`,对实体字段使用 `distinct`
## 过滤条件
```bash
--filters '{"operator":"and","operands":[{"operator":"gt","operands":["fldAmount",0]}]}'
```
- 根节点必须是 `and` / `or`
- `lt``gt``lte``gte` 的值必须是 JSON 数字,不能写成数字字符串。
- 单选/多选字段建议使用 `field get` 返回的 option ID。
- 所有 Base、table、field 和 option ID 都必须从当前目标 Base 的实时元数据取得,不能复用示例或历史 ID。
## 降级边界
只有用户要求记录明细、少量校验样本、精确分位数输入,或聚合接口明确失败时,才使用 `record query`。需要先逐行运算再聚合的指标必须依赖表内已有且可直接聚合的公式字段;没有该字段时停止并请用户先在 AI 表格页面创建,不能遍历全表本地二次计算。
@@ -0,0 +1,66 @@
# record update — 更新记录
## 命令格式
```
Usage:
dws aitable record update [flags]
Example:
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"fldStatusId":"已完成"}}]'
Flags:
--base-id string Base ID (必填)
--records string 待更新记录 JSON 数组,单次最多 100 条;cells key 支持 fieldId 或当前表内唯一字段名,推荐 fieldId (必填,与 --records-file 二选一)
--records-file string 从文件读取 records JSON(替代 --records,适合超长数据或 Windows 环境)
--table-id string Table ID (必填)
```
只需传入需修改的字段,未传入的保持原值。每条记录必须含 recordId 和 cells。
## cells key:优先使用 fieldId,也支持唯一字段名
`cells` 的 key 有两种写法:
- fieldId(推荐):不受字段重命名或重名影响,通过 `field get` 获取。
- 当前表内唯一的字段名:按名称精确匹配;如果存在同名字段,必须改用 fieldId。
同一字段同时通过 fieldId 和字段名传入时,fieldId 对应的值优先。
```bash
# 推荐:fieldId
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"fldStatusId":"已完成"}}]' --format json
# 便捷写法:当前表内唯一字段名
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"状态":"已完成"}}]' --format json
```
## 推荐参数形式
公开、稳定的批量入口是 `--records`(或 `--records-file`),格式为 JSON 数组;即使只改一条记录,推荐也包在数组里。CLI 仍保留隐藏的 `--record-id` + `--cells` 兼容入口,但它不会出现在常规帮助中,自动化脚本应优先使用 `--records`
| 不推荐或无效写法 | 推荐写法 |
|---|---|
| `--record-id recXXX --cells '{"fldX":"值"}'`(隐藏兼容入口) | `--records '[{"recordId":"recXXX","cells":{"fldX":"值"}}]'` |
| `--id recXXX --data '{"fldX":"值"}'` | 同上 |
| `--record-id recXXX --field fldX --value "新值"` | 同上 |
## 单条更新模板(直接复制)
```bash
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"<RECORD_ID>","cells":{"<FIELD_ID>":"新值"}}]' --format json
# 从更新响应的 data.recordIds[] 提取成功记录 ID,并回读确认真实值
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> \
--record-ids <RECORD_ID> --format json
```
更新响应不返回“受影响字段”;以 `data.recordIds[]` 确定成功记录,再用查询回读验证。
## 引号转义提示
- Linux/macOS:外层用单引号 `'[...]'`,内部 JSON 用双引号即可
- Windows PowerShell:外层用双引号 `"[...]"`,内部双引号需转义为 `\"`
- 或将 JSON 写入临时文件,用 `--records-file ./records.json` 规避转义
@@ -0,0 +1,89 @@
# 行记录 Upsertrecord upsert
`recordId` 是否存在,自动把入参拆分到 update 链路或 create 链路:批次混合"已存在改 + 新出现建"时用,省掉客户端按 ID 分批的逻辑。
## 命令
```
dws aitable record upsert \
--base-id BASE_ID --table-id TABLE_ID \
--records '[{"recordId":"<可选>","cells":{...}}, ...]'
```
| flag | 说明 |
|------|------|
| `--base-id` | 必填(可用 `--base` 别名) |
| `--table-id` | 必填 |
| `--records` | 待 upsert 的记录 JSON 数组,**单次最多 100 条**(必填)|
| `--records-file` | 从文件读入(命令行 JSON 太长时用),与 `--records` 互斥优先级更高 |
## --records 结构
每项 JSON
```jsonc
{
"recordId": "rec1", // 可选;带 → update,缺省 → create
"cells": { // 必填;key 是 fieldIdvalue 按字段类型
"fldTitleId": "新标题",
"fldNumberId": 42
}
}
```
`cells` 写入格式与 `record create` / `record update` **完全一致**key 必须是 fieldId 不是字段名;按字段类型见 [aitable-cell-value.md](./aitable-cell-value.md))。
## 返回结构
```jsonc
{
"data": {
"createdRecordIds": ["recX", "recY"], // 不带 recordId 的项产出
"updatedRecordIds": ["recA", "recB"] // 带 recordId 的项产出
}
}
```
`createdRecordIds` 顺序对应入参里**不带 recordId**的项(按出现顺序汇总),同理 `updatedRecordIds` 对应**带 recordId**的项。
## 典型用法
```bash
# 1) 全部新建:所有项都不带 recordId
dws aitable record upsert --base-id BASE --table-id TBL --records '[
{"cells":{"fldTitleId":"任务1","fldStatusId":"待办"}},
{"cells":{"fldTitleId":"任务2","fldStatusId":"待办"}}
]'
# 2) 全部更新:所有项都带 recordId
dws aitable record upsert --base-id BASE --table-id TBL --records '[
{"recordId":"rec1","cells":{"fldStatusId":"已完成"}},
{"recordId":"rec2","cells":{"fldStatusId":"已完成"}}
]'
# 3) 混合:第 1 条更新(带 recordId),第 2 条创建(不带)
dws aitable record upsert --base-id BASE --table-id TBL --records '[
{"recordId":"rec1","cells":{"fldStatusId":"已完成"}},
{"cells":{"fldTitleId":"新增任务","fldStatusId":"待办"}}
]'
# 4) 长 JSON 用文件
dws aitable record upsert --base-id BASE --table-id TBL --records-file ./batch.json
```
## 与 record create / record update 的关系
| 场景 | 命令 |
|------|------|
| 确定全是新增 | `record create` |
| 确定全是更新(每条独立 cells) | `record update` |
| 确定全是更新(共享同一 cells) | `record batch-update` |
| **不确定有没有,按 recordId 自动分流** | `record upsert`(本命令) |
`record upsert``--records` 入参格式与 `record update` 完全相同,唯一差别是 `recordId` 字段在 upsert 里是可选的。如果批次确定全是更新或全是新建,用专用命令更清晰;批次混合时(典型场景:定时同步外部数据,源里既有已存在的也有新出现的),用 upsert。
## 注意事项
- **单次最多 100 条**(创建 + 更新合计),超出请客户端拆批。
- `cells` 的 key 必须是 fieldId 不是字段名(先用 `record query``field get` 拿 fieldId)。
- 只读字段(formula / lookup / 系统字段)不能写入 — upsert 链路与 update 链路同样限制。
@@ -0,0 +1,243 @@
# 视图配置(view get/update <attr>
按属性局部读/写视图配置。每个属性独立子命令,typed flag 友好,agent 不必拼 JSON。
向后兼容:`view update --config '{...}'` 一次多属性入口仍可用。
## viewType × 支持矩阵
| viewType | card | timebar | aggregate | filter / sort / group | visible-fields | field-widths | name |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Grid | | | ✅ | ✅ | ✅ | ✅ | ✅ |
| Kanban | ✅ | | | ✅ | ✅ | | ✅ |
| Gallery | ✅ | | | ✅ | ✅ | | ✅ |
| Gantt | | ✅ | | ✅ | ✅ | | ✅ |
| Calendar | | | | ✅ | ✅ | | ✅ |
| FormDesigner | (走 `form` 系列命令) | | | | | | |
> card 在 Kanban 走 `kanbanCard`,在 Gallery 走 `galleryCard`CLI 自动按 viewType dispatchpreflight 1 次 `get_views`)。timebar 仅 Gantt 支持;Calendar 服务端未暴露任何 timebar 配置。
> **Gantt 视图必须两步创建**`view create --view-type Gantt` 只创建空壳(`ganttTimebar: {}`),**必须**紧跟 `view update timebar --start-field <日期字段ID>` 绑定时间轴字段,否则视图打开是空白。`view create --config` 不接受 `ganttTimebar`,请在创建后使用专属子命令。
## 创建:view create
`--view-type` 支持 `Grid``Kanban``Gantt``Calendar``Gallery``FormDesigner`。创建时通过 `--config` JSON 设置可见字段:
```bash
# --config JSON:可同时配置可见字段、筛选、排序和分组;主字段必须排第一
dws aitable view create --base-id BASE_ID --table-id TABLE_ID \
--view-type Grid --name "任务视图" \
--config '{"visibleFieldIds":["fldPrimary","fldStatus","fldOwner"],"sort":[{"fieldId":"fldStatus","direction":"asc"}]}'
```
创建阶段的 `--config` 是 JSON 对象,并且只接受以下 4 个 key:
| key | 类型 | 说明 |
|---|---|---|
| `visibleFieldIds` | `string[]` | fieldId 数组,不接受字段名;至少一个,主字段必须排第一 |
| `filter` | `object[]` | 筛选规则数组;兼容单个 object,CLI 会自动包装为数组 |
| `sort` | `object[]` | 排序规则数组;兼容单个 object,CLI 会自动包装为数组 |
| `group` | `object[]` | 分组规则数组;兼容单个 object,CLI 会自动包装为数组 |
描述使用独立的 `--desc '{"content":[]}'``description``fieldWidths``aggregate``kanbanCard``ganttTimebar``galleryCard` 等其他 key 会在调用服务端前被拒绝,并提示对应的 `view update` 子命令。
## 读取:view get <attr>
所有 `view get <attr>` 共用 `--base-id` / `--table-id` / `--view-id`,输出是该属性子块的 JSON(不存在时输出 `{}`)。viewType 不匹配会报错并指明应该选哪种视图。
```bash
dws aitable view get card --view-id VIEW_ID --format json # Kanban / Gallery
dws aitable view get timebar --view-id VIEW_ID --format json # Gantt
dws aitable view get aggregate --view-id VIEW_ID --format json # Grid
dws aitable view get filter --view-id VIEW_ID --format json # 所有
dws aitable view get sort --view-id VIEW_ID --format json
dws aitable view get group --view-id VIEW_ID --format json
dws aitable view get visible-fields --view-id VIEW_ID --format json
dws aitable view get field-widths --view-id VIEW_ID --format json # Grid
```
## 写入:view update <attr>
所有 `view update <attr>` 共用 `--base-id` / `--table-id` / `--view-id`
**typed flag + `--json` 可混用**;冲突时 typed flag 优先并 stderr 提示。
card / timebar / aggregate 三类写入有 viewType 校验(preflight 1 次 get_views)。
### view update cardKanban / Gallery
服务端按 viewType 分发到 `kanbanCard``galleryCard`。typed flag 共享。
| flag | 类型 | 说明 |
|------|------|------|
| `--cover-field-id` | string | 封面字段 IDKanban / Gallery 通用),与 `--no-cover` 互斥 |
| `--no-cover` | bool | 清除封面(等价 `coverFieldId="NONE"` |
| `--cover-resize-mode` | string | `cover` / `contain` / `stretch` |
| `--hidden-field-title` | bool | 隐藏字段名标题(仅 Kanban 生效) |
| `--cover-mode` | string | `none` / `auto` / `custom`(仅 Gallery 生效) |
| `--display-field-name` | bool | 是否显示字段名(仅 Gallery 生效) |
| `--json` | JSON | 完整 card 子块对象 |
```bash
dws aitable view update card --view-id KANBAN_ID --cover-field-id fldAttachment --cover-resize-mode contain
dws aitable view update card --view-id KANBAN_ID --no-cover
dws aitable view update card --view-id GALLERY_ID --cover-mode auto
dws aitable view update card --view-id GALLERY_ID --json '{"coverMode":"custom","coverFieldId":"fldX","displayFieldName":true}'
```
### view update timebar(仅 Gantt
| flag | 类型 | 说明 |
|------|------|------|
| `--start-field` | string (date fieldId) | 开始日期字段 |
| `--end-field` | string (date fieldId) | 结束日期字段 |
| `--display-field-id` | string | 时间条上显示的标题字段 |
| `--timeline-scale` | string | `year` / `quarter` / `month` / `weeks` |
| `--color-configs` | JSON 数组 | 颜色配置数组(结构由下游协议定义;清空传 `[]` |
| `--official-holiday` | bool | 是否标注法定节假日 |
| `--json` | JSON | 完整 ganttTimebar 子块 |
```bash
dws aitable view update timebar --view-id GANTT_ID --start-field fldStart --end-field fldEnd --timeline-scale month
dws aitable view update timebar --view-id GANTT_ID --official-holiday=true
```
### view update aggregate(仅 Grid
值是 `map[fieldId]→AggregateAction string`;传 null 清除某个字段聚合。
| flag | 类型 | 说明 |
|------|------|------|
| `--field-id` | string | 配合 `--action` 设置**单字段**聚合 |
| `--action` | string | `SUM`/`AVG`/`MAX`/`MIN`/`MEDIAN`/`RANGE`/`TOTAL`/`DISTINCT`/`EXIST`/`UN_EXIST`/`CHECKED`/`EARLIEST_DATE` 等(按字段类型可用) |
| `--clear-field-id` | string (CSV) | 一/多个字段 ID,清除其聚合 |
| `--json` | JSON | 完整 aggregate map |
```bash
dws aitable view update aggregate --view-id GRID_ID --field-id fldX --action SUM
dws aitable view update aggregate --view-id GRID_ID --clear-field-id fldA,fldB
dws aitable view update aggregate --view-id GRID_ID --json '{"fldX":"AVG","fldY":null}'
```
### view update field-widths(仅 Grid
| flag | 类型 |
|------|------|
| `--field-id` + `--width` | string + int(单字段) |
| `--json` | `{fldId: width, ...}` |
```bash
dws aitable view update field-widths --view-id GRID_ID --field-id fldX --width 200
dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB":200}'
```
### view update visible-fields(通用)
整组替换可见字段列表与顺序。`field get` 返回的第一个字段是系统行索引/主字段;无论它显示为 text 还是 primaryDoc,都必须保留在数组第一位,且不能隐藏。不要仅凭字段类型猜主字段。
> ⚠️ 注意:服务端**只接受 reorder,不接受真"隐藏字段"**——如果传入的列表比当前 columns 短,缺失的字段不会被隐藏。需要真正隐藏字段请到 AI 表格 Web UI。
| flag | 类型 |
|------|------|
| `--field-ids` | string (CSV) |
| `--json` | string 数组 JSON(与 `--field-ids` 同传时 `--json` 优先) |
```bash
dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA,fldB
dws aitable view update visible-fields --view-id VIEW_ID --json '["fldPrimary","fldA","fldB"]'
```
### 列顺序最短闭环
用户说“客户名称最左、状态在金额前”时,不要用通用 `+view-update --config` 探索:
1. `dws aitable field get --base-id <B> --table-id <T> --format json` 取字段有序列表;第一个 fieldId 固定为数组第 1 项。目标 viewId 从真实上下文或 `view get` 返回中取得。
2. `dws aitable view get visible-fields ...` 取当前完整列数组;必须保留全部现有字段,因为该接口只支持 reorder,不是真隐藏。
3. 只重排目标:`[主字段, 客户名称, ..., 状态, 金额, ...]`,其他字段保持相对顺序;一次执行 `view update visible-fields`
4. 再次 `view get visible-fields`,数组完全一致才算完成。遇到 `PRIMARY_FIELD_CANNOT_BE_MOVED/HIDDEN` 立即停止,重新按步骤 1 构造一次;禁止继续猜排列。
“固定/冻结左侧列”与“放到最左边”不是同一操作。只有 Grid 支持冻结;若要冻结主字段后的目标列,需要冻结前 N 列(例如目标位于第 2 列则 count=2):
```bash
dws aitable +view-set-frozen-cols --base-id <B> --table-id <T> --view-id <V> --count <N>
dws aitable +view-get-frozen-cols --base-id <B> --table-id <T> --view-id <V>
```
Kanban/Gallery 等视图只调整列顺序,不尝试冻结。
### view update filter / sort / group(通用,纯 --json
```bash
dws aitable view update filter --view-id VIEW_ID --json '[{"operator":"and","operands":[{"operator":"eq","operands":["fldX","value"]}]}]'
dws aitable view update sort --view-id VIEW_ID --json '[{"fieldId":"fldX","direction":"asc"}]'
dws aitable view update group --view-id VIEW_ID --json '[{"fieldId":"fldX","direction":"asc"}]'
```
> filter/sort/group 入参格式与 `record query --filters`(对象格式)**不同**view config 这边外层必须是数组。传对象 CLI 会自动 wrap,建议直接用数组。详见 [aitable-filter-sort.md](./aitable-filter-sort.md)。
### view update name(重命名)
```bash
dws aitable view update name --view-id VIEW_ID --name "新视图名"
```
等价于 `dws aitable view update --view-id VIEW_ID --name "新视图名"`,无 `config` 参数。
## 服务端字段速查(与 dws CLI 关系)
| dws 子命令 | 服务端 `update_view.config` 子键 | 服务端 Java 模型 |
|---|---|---|
| `view update card`Kanban | `kanbanCard` | `KanbanCardUpdateInput` |
| `view update card`Gallery | `galleryCard` | `GalleryCardUpdateInput` |
| `view update timebar` | `ganttTimebar` | `GanttTimebarUpdateInput` |
| `view update aggregate` | `aggregate` | `Map<fieldId, AggregateAction>` |
| `view update visible-fields` | `visibleFieldIds` | `List<String>` |
| `view update filter / sort / group` | `filter` / `sort` / `group` | `List<Object>` |
| `view update field-widths` | `fieldWidths` | `Map<String, Object>` |
| `view update name` | (不在 config 内)`newViewName` 顶层 | — |
## 典型工作流
### 排查"Kanban 卡片为啥不显示封面"
```bash
dws aitable view get card --view-id KANBAN_ID --format json
# → 看 coverFieldId 是不是 "NONE" 或缺失;不是再看 coverResizeMode 是不是 contain 导致裁掉
```
### 创建可用的 Gantt 视图(必须两步)
```bash
# 第 1 步:创建 Gantt 视图
dws aitable view create --base-id BASE_ID --table-id TABLE_ID \
--view-type Gantt --name "项目甘特图" -f json
# → 记录返回的 viewId
# 第 2 步(必须):绑定日期字段,否则视图为空
dws aitable view update timebar --base-id BASE_ID --table-id TABLE_ID \
--view-id VIEW_ID --start-field fldDateStart
# 可选:加结束日期、标题字段、时间尺度
# --end-field fldDateEnd --display-field-id fldName --timeline-scale month
```
### 把 Gantt 时间轴改成季度尺度并加节假日
```bash
dws aitable view update timebar --view-id GANTT_ID \
--timeline-scale quarter --official-holiday=true
```
### 用 dws 脚本批量替换 Kanban 封面字段
```bash
for v in viw1 viw2 viw3; do
dws aitable view update card --view-id $v --cover-field-id fldNewCover --cover-resize-mode cover --format json | jq .status
done
```
### 一次性多属性更新(仍走 legacy --config
```bash
dws aitable view update --view-id VIEW_ID --config '{
"visibleFieldIds":["fldPrimary","fldA","fldB"],
"filter":[{"operator":"and","operands":[]}],
"kanbanCard":{"coverFieldId":"fldImg","coverResizeMode":"contain"}
}'
```
@@ -0,0 +1,198 @@
# 视图扩展操作(lock / frozen-cols / row-height / fill-color-rule / duplicate
本文档讲 5 项视图操作命令:
- 锁定 / 解锁视图:`view lock` / `view get lock`
- 冻结列:`view update frozen-cols` / `view get frozen-cols`
- 行高:`view update row-height` / `view get row-height`
- 数据高亮规则(条件填色):`view update fill-color-rule` / `view get fill-color-rule`
- 复制视图:`view duplicate`
> **与 [aitable-view-config.md](./aitable-view-config.md) 的分工**
> - `aitable-view-config.md` 讲 `view get/update <attr>` 中 8 个属性:filter / sort / group / visible-fields / field-widths / aggregate / card / timebar。
> - 本文档讲上面 5 项额外能力(包括 attr 形式的 frozen-cols / row-height / fill-color-rule,以及顶层独立的 lock / duplicate)。
> 这 5 项**不能**通过 `view update --config '{...}'` 写入,必须用各自专属子命令。
## 命令矩阵
| 子命令 | 用途 | 必填参数 | 适用 viewType |
|---|---|---|---|
| `view lock [--off]` | 锁定(默认)/ 解锁视图 | `--base-id --table-id --view-id` | 全部 |
| `view get lock` | 读取锁定状态 | `--base-id --table-id --view-id` | 全部 |
| `view update frozen-cols --count N` | 冻结左侧 N 列(0 取消) | `--base-id --table-id --view-id --count` | Grid |
| `view get frozen-cols` | 读取冻结列数 | `--base-id --table-id --view-id` | Grid |
| `view update row-height --cell-height N` | 设置单元格高度(像素) | `--base-id --table-id --view-id --cell-height` | Grid |
| `view get row-height` | 读取单元格高度 | `--base-id --table-id --view-id` | Grid |
| `view update fill-color-rule --json '[...]'` | 全量覆盖条件填色规则 | `--base-id --table-id --view-id --json` | Grid |
| `view get fill-color-rule` | 读取条件填色规则 | `--base-id --table-id --view-id` | 全部(其他视图返回 `[]` |
| `view duplicate [--new-name X]` | 复制视图 | `--base-id --table-id --view-id` | 全部 |
## 视图锁定 / 解锁
```bash
# 锁定(默认)
dws aitable view lock --view-id VIEW_ID
# 解锁
dws aitable view lock --view-id VIEW_ID --off
# 查询当前是否锁定
dws aitable view get lock --view-id VIEW_ID --format json
# → {"data": {"baseId": ..., "tableId": ..., "viewId": ..., "locked": true|false}}
```
锁定的视图禁止他人修改其配置(filter/sort/group/字段顺序等),但记录读写不受影响。锁定状态可重复 set,幂等。
## 冻结列(仅 Grid
```bash
# 冻结从首列起 1 列
dws aitable view update frozen-cols --view-id VIEW_ID --count 1
# 取消冻结
dws aitable view update frozen-cols --view-id VIEW_ID --count 0
# 查询当前冻结列数
dws aitable view get frozen-cols --view-id VIEW_ID --format json
# → {"data": {..., "count": 1}} count 为 null 表示视图未显式设置
```
`--count` 必须 ≥ 0;负数会被拒绝。
## 行高(仅 Grid
⚠️ **`--cell-height` 只接受 4 档枚举:32 / 56 / 88 / 128**(与前端 CELL_HEIGHTS 约定一致),其他值会被拒绝。默认值为 32。
```bash
# 设置行高 — 推荐档位 32 / 56 / 88 / 128
dws aitable view update row-height --view-id VIEW_ID --cell-height 56
# 查询当前行高
dws aitable view get row-height --view-id VIEW_ID --format json
# → {"data": {..., "cellHeight": 56}} cellHeight 为 null 表示视图未显式设置(前端按 32 渲染)
```
## 数据高亮规则(条件填色,仅 Grid)
`view update fill-color-rule` **整组覆盖**,传 `--json '[]'` 清空所有规则。
### 规则结构
每条规则 JSON 结构:
```jsonc
{
"type": "cell" | "row" | "column" | "preRow",
"formatFieldId": "fldX", // 命中规则后被高亮的字段(cell/column 类型有意义)
"format": { "color": "firstLine5" }, // ⚠️ 必须用 FORMAT_COLORS 代号,不接受 hex
"filters": [ // 当前固定 1 条
{
"fieldId": "fldX", // ⚠️ 不是 operands[0]
"symbol": "GT", // ⚠️ 不是 operator;大写枚举
"value": 100 // 部分 symbolEXIST/UN_EXIST)不需要 value
}
]
}
```
### color 合法值(FORMAT_COLORS
`firstLine1` `firstLine11`(共 11 档色码,对应前端调色盘)。**不接受 `#FF0000` 这种 hex**。
### filter.symbol 合法值
| 类别 | symbol |
|---|---|
| 数值/通用比较 | `GT` / `LT` / `GTE` / `LTE` / `EQ` / `NE` |
| 文本 | `CONTAIN` / `EXCLUSIVE` |
| 存在性(无 value | `EXIST` / `UN_EXIST` |
| 多选 / 集合 | `ALL_OF` / `ANY_OF` / `NONE_OF` |
| 日期 | `BEFORE` / `AFTER` / `NOT_BEFORE` / `NOT_AFTER` / `DATE_EQ` / `FROM_NOW` / `DATE_BETWEEN` |
> **与 `record query --filters` / `view update filter` 的格式不同**:那两处用 `{operator, operands}` 结构;这里是 `{fieldId, symbol, value}`。不要混用。
### 典型用法
```bash
# 1) 给金额字段 > 100 的单元格上 firstLine5 色
dws aitable view update fill-color-rule --view-id GRID_ID --json '[
{
"type":"cell",
"formatFieldId":"fldAmount",
"format":{"color":"firstLine5"},
"filters":[{"fieldId":"fldAmount","symbol":"GT","value":100}]
}
]'
# 2) 清空所有规则
dws aitable view update fill-color-rule --view-id GRID_ID --json '[]'
# 3) 查询当前规则
dws aitable view get fill-color-rule --view-id GRID_ID --format json
# → {"data": [...]} 数组
```
> **写入后请用 `view get fill-color-rule` 二次确认实际生效**,以读到的 `data` 数组为准。
## 复制视图
```bash
# 显式命名
dws aitable view duplicate --view-id VIEW_ID --new-name "副本视图"
# 系统自动命名(一般是 "原视图名 (副本)"
dws aitable view duplicate --view-id VIEW_ID --format json
# → {"data": {..., "viewId": "<新视图ID>", "sourceViewId": "<原视图ID>", "viewName": "..."}}
```
复制会保留源视图的 filter / sort / group / visible-fields / card / timebar 等全部配置;新视图的 viewId 与源视图独立。
## 这些字段不能用 `view update --config '{...}'` 写
下列字段必须用对应的专属子命令;如果错塞进 `view update --config`CLI 会在 stderr 提示对应子命令并拒绝把字段当 view config 处理:
| 错误用法 | 应改用 |
|---|---|
| `--config '{"flags":1}'` | `view lock` / `view lock --off` |
| `--config '{"frozenColCount":2}'` | `view update frozen-cols --count N` |
| `--config '{"cellHeight":56}'` | `view update row-height --cell-height N` |
| `--config '{"rowHeightLevel":"tall"}'` | `view update row-height --cell-height N`(合法档位 32/56/88/128 |
| `--config '{"conditionalFormats":[...]}'` | `view update fill-color-rule --json '[...]'` |
## 典型工作流
### 配置一个"金额超阈值红色高亮"的 Grid 视图
```bash
BASE=baseXXX; TABLE=tblYYY; VIEW=viwGridZZ; FLD=fldAmount
# 1) 关键字段冻结,避免横向滚动看不到
dws aitable view update frozen-cols --base-id $BASE --table-id $TABLE --view-id $VIEW --count 1
# 2) 加大行高让数据更易读
dws aitable view update row-height --base-id $BASE --table-id $TABLE --view-id $VIEW --cell-height 56
# 3) 金额 > 100 的单元格上色
dws aitable view update fill-color-rule --base-id $BASE --table-id $TABLE --view-id $VIEW --json "[
{\"type\":\"cell\",\"formatFieldId\":\"$FLD\",\"format\":{\"color\":\"firstLine5\"},
\"filters\":[{\"fieldId\":\"$FLD\",\"symbol\":\"GT\",\"value\":100}]}
]"
# 4) 锁定视图,防止他人改坏
dws aitable view lock --base-id $BASE --table-id $TABLE --view-id $VIEW
```
### 复制一个"金牌客户"视图给销售团队
```bash
dws aitable view duplicate --view-id viw_VIP_template --new-name "金牌客户-华东区"
# 取返回里 data.viewId 进一步定制
```
### 排查"我设置了高亮规则为啥没生效"
```bash
# 看实际生效的 conditionalFormats
dws aitable view get fill-color-rule --view-id VIEW_ID --format json
# → 如果是 [] 说明上次写入失败;常见原因:color 用了 hex(必须 firstLineN/ filter 用了 operator(必须 symbol
```
@@ -0,0 +1,384 @@
# workflow — 自动化工作流管理
创建 / 更新 / 启停 / 手动执行 / 查询执行历史 / 查看 / 列出 Base 下的自动化工作流("当 X 时自动 Y" 流程)。
适用场景:用户要求创建自动化、修改流程、停掉或恢复流程、立即执行流程、核对执行结果或查询已有流程。
## 命令一览
| 命令 | 用途 |
|------|------|
| `workflow edit-example` | 获取工作流编辑文档与 workflow-dsl/v1 示例 |
| `workflow create` | 创建并发布自动化工作流 |
| `workflow update` | 更新并发布已有自动化工作流 |
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
| `workflow get` | 获取单个工作流详情(含 flowSchema 完整节点定义) |
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
| `workflow run` | 立即执行指定工作流(会产生真实副作用,需确认) |
| `workflow history` | 按状态、时间和分页条件查询工作流执行历史 |
> `workflow edit-example` 无参数;其他子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
## DSL 入参格式与最小 Demo
先运行 `workflow edit-example` 获取服务端提供的最新编辑文档和示例。`workflow create/update``--dsl` 接收钉钉 AI 表格 `workflow-dsl/v1` JSON object。
复杂工作流还应注意:
1. 使用 `workflow edit-example` 获取最新 DSL Guide、结构和示例。
2. 涉及数据表、字段或视图的节点,先用 `table get` / `field get` / `view list` 确认真实 `sheetId``fieldId``viewId`
3. create 和 update 都提交完整的 workflow-dsl/v1 JSON object,并检查所有 `next``loopEntry`、branch `to` 和 ref。
以下 Demo 表示“每天 09:00 触发,并向 Base 所有者发送消息”,不依赖数据表、字段或视图 ID:
```json
{
"version": "workflow-dsl/v1",
"name": "每日提醒",
"description": "可选说明",
"trigger": "start",
"steps": {
"start": {
"type": "Scheduled",
"next": "send",
"data": {
"mode": "daily",
"time": "09:00",
"timezone": "GMT+08:00"
}
},
"send": {
"type": "SendMessage",
"data": {
"title": "定时任务已触发",
"to": {
"users": [{"ref": "$.system_node.ownerUserId"}]
}
}
}
}
}
```
将上述 JSON 保存为 `workflow.json` 后创建工作流:
```bash
dws aitable workflow create \
--base-id BASE_ID \
--dsl @workflow.json \
--locale zh-CN \
--format json
```
保存创建结果中的 `data.flowId`。更新时修改 `workflow.json` 中的完整目标定义,例如修改 `name``description` 或消息 `title`,然后调用:
```bash
dws aitable workflow update \
--base-id BASE_ID \
--workflow-id FLOW_ID \
--dsl @workflow.json \
--locale zh-CN \
--format json
```
create 和 update 都必须同时满足 `status=success``data.valid=true``data.issues=[]` 才表示发布成功;update 返回的 `data.flowId` 应与传入的 `FLOW_ID` 一致。以上仅为最小 Demo,复杂节点的 `type``data` 结构以钉钉 AI 表格 MCP 最新 DSL 文档为准。
## 命令详情
### workflow edit-example — 获取编辑文档与示例
```bash
dws aitable workflow edit-example --format json
```
该命令无业务参数,调用 `aitable/edit_workflow_example` 返回服务端提供的工作流编辑文档和示例。创建或更新复杂工作流前优先调用它,避免依赖可能过期的本地 DSL 结构。
### workflow create — 创建并发布工作流
```bash
# 大 DSL 推荐从文件读取
dws aitable workflow create \
--base-id BASE_ID \
--dsl @workflow.json \
--locale zh-CN \
--format json
# 也支持 stdin
cat workflow.json | dws aitable workflow create --base-id BASE_ID --dsl - --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 所属 Base ID |
| `--dsl` | 是 | workflow-dsl/v1 JSON object;支持内联 JSON、`@filepath``-` stdin |
| `--locale` | 否 | 请求语言,如 `zh-CN` / `zh_CN` |
创建成功返回发布结果:
```json
{
"status": "success",
"data": {
"valid": true,
"flowId": "G-FLOW-XXXXXX",
"flowSchema": {},
"stepNodeIds": {},
"referenceMap": {},
"issues": []
}
}
```
关键语义:
- `create` 非幂等,CLI 不自动重试。若网络中断导致结果不确定,先 `workflow list` 按名称确认是否已创建,再决定是否重试。
- `status=success` 只说明 workflow-edit 正常返回;如果 `data.valid=false`,仍表示 DSL 未通过校验或发布,必须读取 `issues` 修正。
- 创建并发布后,用 `workflow list` 确认 `status`;需要运行但状态为 `STOP` 时再调用 `workflow enable`
### workflow update — 更新并发布工作流
```bash
# 先留底现有详情,再提交完整目标 DSL
dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json > /tmp/workflow-backup.json
dws aitable workflow update \
--base-id BASE_ID \
--workflow-id WORKFLOW_ID \
--dsl @workflow.json \
--locale zh_CN \
--format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 所属 Base ID |
| `--workflow-id` | 是 | 目标工作流 ID,对应 list 的 `flowId` |
| `--dsl` | 是 | 完整目标 workflow-dsl/v1 JSON object;支持内联、`@filepath``-` stdin |
| `--locale` | 否 | 请求语言,如 `zh-CN` / `zh_CN` |
返回结构与 create 相同,成功时 `flowId` 应为目标工作流。update 使用 AI 表格瞬态错误重试;最终仍必须检查 `data.valid``issues`,并用 `workflow get/list` 验证发布结果与运行状态。
### workflow run — 立即执行工作流
```bash
# 记录类触发器
dws aitable workflow run --base-id BASE_ID --workflow-id WORKFLOW_ID \
--table-id TABLE_ID --record-ids RECORD_ID_1,RECORD_ID_2
# 定时触发器不传 table-id / record-ids
dws aitable workflow run --base-id BASE_ID --workflow-id WORKFLOW_ID
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 所属 Base ID |
| `--workflow-id` | 是 | 目标工作流 ID |
| `--table-id` | 条件必填 | 记录类触发器绑定的数据表;必须与触发器配置一致 |
| `--record-ids` | 条件必填 | 记录类触发器的记录 ID,1–5 个、逗号分隔且不可重复 |
`run` 启动真实异步执行,工作流中的发消息、写记录等动作会实际发生;执行前必须取得用户确认。返回项中的 `executionId` 是本次执行标识,可与 `workflow history` 项目的 `instanceId` 匹配。网络结果不确定时不要直接重复执行,先按该标识查询历史。
### workflow history — 查询执行历史
```bash
dws aitable workflow history --base-id BASE_ID --workflow-id WORKFLOW_ID \
--status failed --after-time 1786000000000 --before-time 1787000000000 \
--page 0 --size 50
```
| flag | 说明 |
|------|------|
| `--base-id` | 必填 |
| `--workflow-id` | 必填;CLI 会映射为 MCP 的 `flowId` |
| `--status` | 可选:`success` / `failed` / `running` / `break` / `untrigger` |
| `--after-time` | 可选,Unix 毫秒开始时间 |
| `--before-time` | 可选,Unix 毫秒结束时间;与 after-time 同传时必须更大 |
| `--page` | 可选,从 0 开始,默认 0 |
| `--size` | 可选,默认 20,范围 `[1, 100]` |
返回 `totalCount``list``running` 是非终态;`success``failed``break``untrigger` 是终态。
### workflow list — 列出工作流
```bash
dws aitable workflow list --base-id BASE_ID --format json
dws aitable workflow list --base-id BASE_ID --limit 50 --offset 100
```
| flag | 说明 |
|------|------|
| `--base-id` | 必填 |
| `--limit` | 可选,分页大小 `[1, 100]`,不传走服务端默认 20 |
| `--offset` | 可选,分页偏移量 `>= 0`,不传走服务端默认 0 |
返回结构:
```json
{
"data": {
"list": [
{
"flowId": "G-FLOW-XXXXXX", // ★ 注意字段名是 flowId
"name": "流程1",
"description": "当创建记录时,就更新记录",
"status": "RUNNING", // RUNNING / STOP
"creatorStaffId": "281493",
"lastModifier": { "name": "李普阳", "staffId": "281493" },
"gmtModified": 1780318540000,
"versionId": "G-FLOW-VER-XXXXXX",
"icons": ["..."], // 触发器+动作的图标
"isSubFlow": false,
"opPermissions": { "canEdit": true }
}
],
"recordCount": 1, // Base 下总数
"runningCount": 1 // RUNNING 状态的数量
}
}
```
**注意**
- 标识字段服务端在 `list` 里叫 **`flowId`**,但在 `enable` / `disable` 出参里叫 **`workflowId`**。CLI `--workflow-id` 传任一即可(同值)。
- `status` 是字符串枚举:`RUNNING`(启用中)/ `STOP`(已禁用),**不是** boolean。
- `runningCount` 是当前 Base 下 status=RUNNING 的工作流数,方便快速判断「有几个流程在跑」。
### workflow get — 获取单个工作流详情
```bash
dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json
```
| flag | 说明 |
|------|------|
| `--base-id` | 必填 |
| `--workflow-id` | 必填,对应 list 出参里的 `flowId` |
返回完整工作流配置:
```json
{
"data": {
"name": "流程1",
"namespace": "...",
"status": "RUNNING",
"versionId": "G-FLOW-VER-XXXXXX",
"versionNo": 14,
"versionStatus": "...",
"accessor": {...}, // 访问者信息
"corpId": "...",
"flowAttribute": {...}, // 流程顶层属性
"flowSchema": {...}, // ★ 流程节点定义(触发器/动作/分支等)
"gmtCreate": 1780317804000,
"gmtModified": 1780318540000
}
}
```
`flowSchema` 是完整的节点 DAG,结构因流程而异(条件触发器 vs 定时触发器、单分支 vs 多分支等)。agent 应按需读取关心字段,不要试图建静态 schema。
### workflow enable — 启用工作流
```bash
dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID --format json
```
返回 `{workflowId, enabled: true}` —— **`enabled: true` 是动作确认,不是当前状态查询**。要确认真启用了,必须再 `workflow list``status` 是否变成 `"RUNNING"``runningCount` 是否加 1。
### workflow disable — 禁用工作流(高危)
```bash
dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes --format json
```
返回 `{workflowId, disabled: true}` —— 同样是动作确认。禁用后该工作流不再自动触发。
**风险**:直接影响业务自动化(如停掉「记录创建后自动发通知」会让通知断流)。建议:
- 操作前先 `workflow get` 留底当前配置
- 脚本场景显式传 `--yes`;交互场景让用户在 prompt 中再次确认
## 能力边界
| 能力 | 状态 |
|------|------|
| 新建工作流 | ✅ 创建并发布 |
| 修改工作流配置 | ✅ 更新并发布 |
| 列出工作流 | ✅ |
| 看工作流详情(含 flowSchema | ✅ |
| 启用/禁用 | ✅ |
| 查看运行历史/执行日志 | ✅ `workflow history` |
| 手动触发/单次运行 | ✅ `workflow run`(需确认) |
| 删除工作流 | ❌ 暂未开放 |
## 错误码速查
| 场景 | code | type | 备注 |
|------|------|------|------|
| create/update 返回 `valid=false` | — | success envelope | 读取 `data.issues` 修正 DSL,不能当作发布成功 |
| create 下游失败 | `CREATE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | create 不自动重试;先 list 排查是否已创建 |
| update 下游失败 | `UPDATE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | update 会重试瞬态错误,最终失败时保留 DSL 和 workflowId 排查 |
| `workflow-id` 不存在调 get | `GET_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 可能为 null,先 `workflow list` 核对 ID |
| `workflow-id` 不存在调 enable | `ENABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 含 "场域中不存在该 namespace" |
| `workflow-id` 不存在调 disable | `DISABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | 同上 |
| `--limit` < 1 或 > 100 | CLI 层拦截) | — | `--limit 必须在 [1, 100] 范围内,got N` |
| `--offset` < 0 | CLI 层拦截) | — | `--offset 必须 >= 0got N` |
> 拿到 `*_WORKFLOW_ERROR / SYSTEM_ERROR` 时,先 `workflow list` 自查目标 ID 是否还存在、是否在当前 Base 下。
## 典型工作流
### 创建并确认一个工作流
```bash
# 1. 按本文 DSL Demo 生成 /tmp/workflow.json
dws aitable workflow create --base-id BASE_ID --dsl @/tmp/workflow.json --locale zh-CN --format json \
| tee /tmp/workflow-result.json
# 2. valid 必须为 true;保存 flowId
jq '{valid: .data.valid, flowId: .data.flowId, issues: .data.issues}' /tmp/workflow-result.json
# 3. 确认运行状态,需要时显式启用
FLOW_ID=$(jq -r '.data.flowId' /tmp/workflow-result.json)
dws aitable workflow list --base-id BASE_ID --format json \
| jq --arg id "$FLOW_ID" '.data.list[] | select(.flowId == $id) | {flowId, name, status}'
```
### 看看 Base 里有哪些自动化在跑
```bash
dws aitable workflow list --base-id BASE_ID --format json | jq '.data | {total: .recordCount, running: .runningCount, items: .list | map({name, status, flowId})}'
```
### 临时停掉某个流程做调试
```bash
# 1. 留底当前状态
dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json > /tmp/wf-backup.json
# 2. 禁用
dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes --format json
# 3. 调试做完后重启
dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID --format json
# 4. 确认 status=RUNNING
dws aitable workflow list --base-id BASE_ID --format json | jq '.data.list[] | select(.flowId == "WORKFLOW_ID") | .status'
```
### 批量关掉某个 Base 下所有 workflow(调试 / 迁移前清场)
```bash
for WF in $(dws aitable workflow list --base-id BASE_ID --limit 100 --format json | jq -r '.data.list[] | select(.status == "RUNNING") | .flowId'); do
dws aitable workflow disable --base-id BASE_ID --workflow-id "$WF" --yes --format json | jq .status
done
```
## 注意事项
- `--workflow-id` 接受的就是 `list` 返回里的 `flowId`(同值,CLI 屏蔽了服务端字段名差异)。
- create / update 的 `--dsl` 必须是 JSON object,不能传数组、字符串化的二次 JSON 或 FlowSchema。
- 本文 Demo 可直接用于最小定时消息工作流;复杂节点应以钉钉 AI 表格 MCP 最新 DSL 文档为准。
- `status=success``data.valid=false` 仍是 DSL 校验失败;`issues` 才是下一步修复依据。
- create 不自动重试;update 仅对网络/5xx/`retryable:true` 瞬态错误自动重试。
- enable / disable 出参里的 `enabled` / `disabled`**动作确认 flag**,不是当前状态字段。要确认真生效请走 `workflow list``status`
- `workflow get``flowSchema` 结构随触发器/动作类型变化,不要假设固定字段。
- `workflow run` 不自动重试;结果不确定时用 `workflow history` 按 executionId / instanceId 核对。
- 删除工作流当前仍未开放。
@@ -0,0 +1,104 @@
# 易混淆操作与字段规则
## 易混淆操作 (高风险场景必读)
| 用户说的 | 正确命令 | 不是这个 |
|---------|----------|---------|
| "创建一个新表格 (Base)" | `base create` | 不是 `table create` |
| "在表格里加一个数据表" | `table create` | 不是 `base create` |
| "看看表格里有哪些表" | `base get` | 不是 `field get` |
| "看看表里有哪些列" | `field get` | 不是 `base get` |
| "搜索表格" (找 Base) | `base search` | 不是 `record query` |
| "搜索记录" (查表内数据) | `record query` | 不是 `base search` |
| "删掉这个数据表" | `table delete` | 不是 `record delete` |
| "删掉这条数据" | `record delete` | 不是 `table delete` |
| "删掉这个列" | `field delete` | 不是 `record delete` |
| "改字段类型" | 先 `field delete``field create` | `field update` **不能改类型** |
| "移动字段/调整字段顺序" | `view update --config '{"visibleFieldIds":[...]}'`(视图层重排,首列主字段必须保留在第一位) | 没有 `field reorder`/`field move` 命令;不能改字段在元数据里的"原始定义顺序" |
## field 子命令总览
> ⚠️ field 有且仅有以下 **4 个** 子命令,没有 `list`、`reorder`、`move`
| 子命令 | 用途 |
|-------|------|
| `field get` | 获取字段详情(含完整 config/options)。**不是 `field list`** |
| `field create` | **创建字段(支持通过 config.options 设置选项)** |
| `field update` | 更新字段名称或配置(**不能改类型**,**不能改顺序**) |
| `field delete` | 删除字段(不可逆) |
> **想"调整字段顺序"?请使用 `view update`**(视图层操作,不属于 `field` 子命令):
> - 通过 `--config '{"visibleFieldIds":["fld1","fld2",...]}'` 传入 fieldId 数组,数组顺序即视图中的字段显示顺序
> - 仅影响**该视图**的列排列,同一 table 的其他视图与字段元数据原始顺序不变
> - **首列字段(主字段)必须保留在数组第一位**,不能移动到非首位
> - **漏传的字段不会被隐藏**,而是会被 API 自动追加到列表末尾
> - **读写命名不一致**:写入键名 `visibleFieldIds`,但读出(`view get`)时该字段在 view 里叫 `columns`——校验顺序时请看 `views[].columns`
## 字段创建时设置 config(重要)
创建 singleSelect/multipleSelect 字段时,**必须设置选项 (options)**
```bash
# 创建带选项的单选字段 (推荐新语法: --name/--type/--config)
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "优先级" --type "singleSelect" \
--config '{"options":[{"name":"高"},{"name":"中"},{"name":"低"}]}' \
--format json
# 建表时也可以直接通过 --fields 批量带选项字段
dws aitable table create --base-id <BASE_ID> --name "任务表" \
--fields '[{"fieldName":"任务","type":"text"},{"fieldName":"状态","type":"singleSelect","config":{"options":[{"name":"待办"},{"name":"进行中"},{"name":"已完成"}]}}]' \
--format json
```
> ⚠️ **不要混淆**
> - **字段创建**`field create` 的 `--config` 或 `table create` 的 `--fields`):创建时就指定 `options`
> - **记录写入**`record create` / `record update` 的 `--records`):只能写入已存在的选项名称
## 主字段约束(table create 必读)
> ⚠️ `table create` 的 `--fields` 中,**第一个字段自动成为主字段**。
> 主字段只能是 **text** 类型,不能是 attachment、checkbox、formula 等。
**实际影响**:当用户要求创建的字段不适合做主字段时(如附件、复选框),必须:
1. 先放一个 text 字段作为第一个字段(主字段)
2. 再放用户要求的字段
3. **告知用户**为何多了一个字段
```bash
# 例: 用户要求只创建附件字段 → 附件不能做主字段,必须先加 text 主字段
dws aitable table create --base-id <BASE_ID> --name "产品图片" \
--fields '[{"fieldName":"名称","type":"text"},{"fieldName":"产品图片","type":"attachment"}]' \
--format json
```
## 只读字段 (不可写入)
以下类型的字段不可写入, 执行 `field get` 后识别并跳过:
- 创建时间 / 修改时间 (系统自动)
- 创建人 / 修改人 (系统自动)
- 自动编号
- 公式字段
- 引用字段
## 记录写入格式(record create / record update
> 各字段类型的完整写入/读取格式规范请参考:[aitable-cell-value.md](./aitable/aitable-cell-value.md)
>
> 该文件是 cellValue 格式的 **source of truth**,包含所有字段类型的详细示例和注意事项。
## ⚠️ 附件上传完整流程(必读!)
> **不要**使用钉盘 (drive) 上传来替代此流程!钉盘 fileId **无法**写入 attachment 字段。
附件字段写入使用 `upload_attachment.py` 脚本,**2 步**完成:
```bash
# 步骤 1: 一键上传文件(脚本内部自动完成 prepare + PUT to OSS
python3 scripts/upload_attachment.py <BASE_ID> /path/to/photo.png
# 输出: { "fileToken": "ft_xxx", "fileName": "photo.png", "size": 1024 }
# 步骤 2: 在 record create/update 中使用 fileToken
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
```
@@ -0,0 +1,17 @@
# AITable 局部意图消歧
| 用户表达 | 归属 | 理由 |
|---|---|---|
| AI 表格、多维表、Base、Table、字段、记录、视图、表单、仪表盘、自动化 | AITable | 操作 AITable 的业务数据与配置 |
| 搜索 Base 候选、按关键词找 Base、检查某 Base 是否存在 | AITable | 直接使用 `+base-search --query`;即使关键词像人名,只要对象是 Base,也不得改走 `aisearch person` |
| 表格链接,需要读取记录 | AITable | 先用 `+url-resolve` 取稳定 ID,再用 `+record-query` |
| 只有 Base/Table 名称,需要读取记录 | AITable | 先用 `+resolve-base` / `+resolve-table` 唯一解析,再查询记录 |
| 只复制 Base 结构、删除整个 Base | AITable | 复制到已知文档文件夹用 `+base-copy --target-folder-id ... --only-struct`;删除用真实 baseId。不要 Drive 完整复制后逐表删数据 |
| Base 整体移动到普通文件夹、外层存储重命名 | Drive | 这是 Base 作为单个存储节点的外层位置/名称动作 |
| Base 内 Table、Dashboard、Section 的复制/移动/重命名/删除 | AITable | 这些是 Base 内 nsheet/业务结构,不是独立 Drive dentry |
| Base 角色、高级权限 | AITable | `+role-*` / `+advperm-*`;仅普通文件 ACL 才走 Drive |
| 记录主键文档正文 | Doc | AITable 只取/建关联,正文由 Doc 处理 |
| Excel 式单元格、区域、工作表、公式 | Sheetdingtalk-misc | 二维电子表格,不是多维表记录模型 |
| CSV/JSON 数据进入现有 AI 表格 | AITable import 或 record create | 需要保留导入任务语义时用 import;已映射字段时直接写记录 |
若链接类型不明确,先做 URL 类型预检;不要仅凭 URL 文本猜产品。明确是 AI 表格后才加载本 Skill。
@@ -0,0 +1,44 @@
# 业务域通用规范
> 仅服务本 skill 已迁入的行动指南。安全门控、危险操作确认、`--format json` 等已在本 skill 的 `SKILL.md` 中定义,此处不重复。
## 批量查询规范
| # | 规范 |
|---|------|
| 1 | **并行查详情**:拿到多个 ID 后,用 `&` 合并到同一条 Shell 命令并行执行 + `wait`**严禁逐条串行** |
| 2 | **翻页**:分页接口须拉全直至无更多 |
| 3 | **优先批量 API**:有批量接口则用批量;无则按 #1 并行 |
| 4 | **群消息**:必须先 `chat search --query``openConversationId`,再 `chat message list --group <openConversationId> --time "<yyyy-MM-dd HH:mm:ss>" --direction older`;多群同条命令并行 |
| 5 | **列表少轮次**:带条件搜索/列表 → 一次采全详情;**禁止**无新参数时重复同一 `list` / `search` |
## 多源并行采集(公共模式)
> recipe 引用方式:`按「多源并行采集」执行(关键词=<X>,时间=<Y>至<Z>`。
- 同条 Shell`&` 并行 + `wait`;分页须采全。
- 只保留与主题相关的数据,无关丢弃。
- 有批量详情接口优先;否则并行拉详情(见上表 #1)。
- 具体采哪些产品列表由对应 **行动指南 recipe** 与当前产品参考决定;不要引入本文档未覆盖的产品路线。
## 字段术语与 ID 传递
> list 返回 JSON 后,必须提取下表字段传给后续命令。**禁止用其他字段替代。**
| 字段 | 来源 | 传递给 |
|------|------|--------|
| `taskUuid` | `minutes list` | `minutes get summary/info/batch --id(s)` |
| `userId` | `aisearch person` / `contact user search` / `contact dept list-members` | `contact user get --ids``todo --executors``calendar --users` |
| `deptId` | `contact dept search` | `contact dept list-members --ids <deptId1,deptId2...>`;多子部门时对每个子部门分别 `dept search` 取 id |
| `nodeId` | `drive search` / `wiki node search` | `doc read/update --node``drive copy/move/rename/delete --node` |
| `nodeId` | `wiki node list` 中的 folder 类型节点 / `wiki node create --type folder` | `wiki node list --folder``wiki node create --folder``drive upload --folder``drive copy/move --folder` |
| `eventId` | `calendar event list` | `calendar event get/update --id` |
| `processInstanceId` | `oa approval list-*` | `oa approval detail/approve --instance-id` |
| `openConversationId` | `chat search` | `chat message list/send --group` |
| `todoTaskId` | `todo task list` | `todo task update/done --task-id` |
| `reportId` | `report inbox list` / `report outbox list` | `report entry get/stats --report-id` |
| `baseId` / `tableId` | `aitable base search` | `aitable record query --base-id --table-id` |
| `dentryUuid` | `drive list` / `drive mkdir` | `drive info/download/copy/move/rename/delete --node``drive list/mkdir/upload/copy/move --folder` |
| `dentryId` | `drive info` 的数字字段 | 仅用于 `chat message send --dentry-id` |
**ID 边界硬约束**`dentryId` 通常是纯数字,只表示聊天文件消息需要的钉盘条目数字 ID;它不是父目录 ID。遇到 `drive --node/--folder``doc --node``wiki node --folder` 时,只能使用 `dentryUuid` / `nodeId` / 文档 URL。若当前上下文只有数字型 `dentryId`,必须先重新 `drive list` / `drive search` / `wiki node list` 获取正确 ID,不能把该数字直接代入后续命令。
@@ -0,0 +1,144 @@
# URL 格式与处理规范
## 路由第 0 步:意图直达(优先级高于 URL 探测)
用户已经明确表达某产品的内容意图时,直接进入对应产品场域,不要先做 URL 类型
探测。尤其:
- 明确提到 Markdown / `.md` 文件的读取或修改,按普通文件走 `drive` 场域:
`dws drive download` 下载到本地处理,再用 `dws drive upload` 回传。
- 明确“读这篇文档 / 编辑文档正文”进入 `doc`;明确“看这个在线表格数据”进入
`sheet`
仅当用户只粘贴 URL、没有明确产品意图,或意图与链接类型可能冲突时,才执行下方
类型探测。
## alidocs URL 分流决策(意图不明确时执行)
收到 `alidocs.dingtalk.com` URL 且无法从指令判断产品时,必须按以下顺序判断:
1. URL 路径含 `/i/p/`**分享短链**,禁止调用 `dws doc` 任何子命令 → 按下方 [分享短链处理](#分享短链处理) 执行
2. URL 路径含 `/i/nodes/`**节点链接**,需探测类型 → 按下方 [alidocs URL 类型探测流程](#alidocs-url-类型探测流程) 执行
3. URL 路径含 `/spreadsheetv2/`**电子表格直链**,直接路由到 `sheet`,将完整 URL 原样传给 `--node` 参数
4. URL 路径含 `/document/edit``/document/preview` 且 query 参数包含 `dentryKey`**文档链接**,直接路由到 `doc`,将完整 URL 原样传给 `--node` 参数(URL 中不一定有 `type=d`,只需匹配路径和 `dentryKey` 参数即可)
5. 其他 alidocs URL 格式 → 告知用户当前暂不支持该链接格式
---
## 已知 URL 格式
需要自行拼接链接时,只能使用以下模板:
| 产品 | 用途 | URL 格式 | ID 来源 |
|------|------|----------|---------|
| `aitable` | AI表格 Base 链接 | `https://alidocs.dingtalk.com/i/nodes/{baseId}` | `base list/search/create/get` 返回的 `baseId` |
| `aitable` | AI表格指定数据表链接 | `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}` | `baseId` + `table create/get``base get` 返回的 `tableId` |
| `aitable` | AI表格指定数据表+视图链接 | `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}%26viewId%3D{viewId}` | `baseId` + `tableId` + `view create/get` 返回的 `viewId` |
| `aitable` | AI表格模板预览 | `https://docs.dingtalk.com/table/template/{templateId}` | `template search` 返回的 `templateId` |
| `doc` | 文档链接 | `https://alidocs.dingtalk.com/i/nodes/{dentryUuid}` | `doc` 命令返回的 `dentryUuid` |
| `sheet` | 电子表格链接 | `https://alidocs.dingtalk.com/i/nodes/{dentryUuid}` | `sheet create` 返回的 `dentryUuid` |
| `sheet` | 电子表格直链 | `https://alidocs.dingtalk.com/spreadsheetv2/{key}/...?dentryKey={key}&type=s` | 用户提供的完整 URL,直接传给 `--node` |
| `doc` | 文档链接(edit/preview | `https://alidocs.dingtalk.com/document/{edit\|preview}?...&dentryKey={key}` | 用户提供的完整 URL,直接传给 `--node` |
| `minutes` | 听记链接 | `https://shanji.dingtalk.com/app/transcribes/{taskUuid}` | `list mine/shared` 返回的 `taskUuid` |
不在此表中的产品,禁止自行拼接 URL。命令返回中包含完整链接时直接使用,否则告知用户无法提供。
## 分享短链处理
`alidocs.dingtalk.com/i/p/{shortKey}` 是钉钉文档的**对外分享短链**`dws doc` 命令无法解析此格式。
### 识别规则
URL 路径中包含 `/i/p/` 即为分享短链(无论后面是否还有子路径),例如:
- `https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2`
- `https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2/docs/AY39rGpMPmeVNpXZevZm8OZkXKnaoNQ7`
- `https://alidocs.dingtalk.com/i/p/AbCdEfGh1234`
- `https://alidocs.dingtalk.com/i/p/AbCdEfGh1234/sheets/XYZ789`
> **关键**:只要 URL 中出现 `/i/p/`,无论后面跟什么子路径(`/docs/...`、`/sheets/...` 等),都属于分享短链,一律禁止调用 `dws doc`。
### 处理方式
**不要调用 `dws doc` 任何子命令**(包括 `doc info``doc read` 等),`dws` 无法解析此格式。
- **需要获取文档内容时**:使用 `read_url` 工具直接读取该链接
- **其他操作(如移动、复制、权限管理等)**:告知用户此链接为分享短链,无法直接执行复制、移动、权限管理等操作。如需保存该文档内容,建议用户在钉钉客户端中打开该页面,手动复制文本内容,然后可通过 `dws doc create` 创建一篇新文档并将内容写入
```
# 需要读取文档内容时(无论 /i/p/ 后面有没有子路径,都用 read_url)
read_url("https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2")
read_url("https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2/docs/AY39rGpMPmeVNpXZevZm8OZkXKnaoNQ7")
# 禁止(以下全部会失败,dws 无法解析任何含 /i/p/ 的 URL)
dws doc info --node "https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2" --format json
dws doc read --node "https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2/docs/AY39rGpMPmeVNpXZevZm8OZkXKnaoNQ7" --format json
```
### 当 `read_url` 返回内容不完整时
钉钉文档分享页是动态渲染的,`read_url` 可能只能获取到页面标题等有限信息,无法获取文档正文。此时**禁止猜测原因**(如"权限不足""文档为空""文档已删除"等),**禁止建议用户"提供 `/i/nodes/` 格式链接"**(分享短链和节点链接是不同体系,普通用户无法自行转换)。应直接告知用户:
> 这个链接是钉钉文档的分享短链,由于页面是动态渲染的,我无法通过该链接直接获取文档的完整正文内容。
>
> 你可以:
> 1. 在钉钉客户端中打开该文档,将正文内容复制粘贴给我
> 2. 如果文档已保存在你的文档空间中,可以告诉我文档名称,我通过 `dws drive search` 搜索后再读取
---
## alidocs URL 类型探测流程
`alidocs.dingtalk.com/i/nodes/{id}` 是钉钉文档空间的统一 URL,可能指向**文档、电子表格、多维表、文件、文件夹**等不同类型。**禁止仅凭 URL 就假定为文档**,必须先探测类型再路由到正确的产品。
### 探测步骤
```
Step 1 → dws drive info --node "<URL>" --format json
Step 2 → 从返回中提取 extension、nodeType 字段
Step 3 → 按下方路由规则映射到对应产品
```
> 路由依据是 `extension`,不是 `contentType`。`drive info` 检测到
> `adoc` / `axls` / `able` 时会自动补充在线文档信息。
### 路由映射表
| 条件 | 路由到产品 | 后续操作 |
|------|-----------|---------|
| `extension=adoc` | `doc` | 加载 `dingtalk-doc` 操作内容 |
| `extension=axls` | `sheet` | 加载 `dingtalk-misc``references/sheet.md` 操作(仅 `axls` 在线电子表格) |
| `extension=able` | `aitable` | 将 nodeId 作为 baseId,加载 `dingtalk-aitable` 操作 |
| `extension=xlsx` / `xls` / `xlsm` / `csv` | `drive` | 必须用 `dws drive download` 下载到本地处理,禁止走 `sheet` |
| `nodeType=file`(非在线文档扩展名,含 `md` | `drive` | 下载用 `dws drive download --node <ID> --output <PATH> --format json`;上传/覆盖用 `dws drive upload` |
| `nodeType=folder` | `drive` / `wiki` | 调用 `dws drive list --workspace <WS_ID>``dws wiki node list` 列出子节点 |
| 以上均不匹配 | — | 告知用户当前暂不支持该类型 |
> axls vs xlsx 关键区分:
> - `axls`(钉钉在线电子表格,`contentType=ALIDOC`)→ 走 `sheet` 产品线(读/写/筛选/导出等服务端原子操作)
> - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws drive download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错)
> - 用户想把在线表格导出为 xlsx 文件 → 用 `dws sheet export`(输入是 `axls`,输出是 xlsx,这是 axls → xlsx 的格式转换,不属于 xlsx 读取场景)
### 示例
```bash
# 用户传入: https://alidocs.dingtalk.com/i/nodes/abc123
dws drive info --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
# 返回 extension=axls → 在线电子表格,路由到 sheet
dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
# 返回 extension=xlsx/xls/csv → 本地表格文件,必须下载处理(禁止走 sheet)
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/xlsx456" --output <PATH> --format json
# 返回 nodeType=file → 普通文件,下载
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/def456" --output <PATH> --format json
# 返回 nodeType=folder → 文件夹,列出子节点
dws drive list --workspace <WS_ID> --format json
```
### 何时可跳过探测
当用户指令中已明确指定产品(如"帮我读这个文档"、"看下这个表格的数据"),可结合用户意图**跳过探测**直接路由。仅在以下情况**必须执行探测**:
- 用户只粘贴 URL,无其他上下文
- 用户指令与 URL 实际类型可能不一致(如说"文档"但实际是表格)
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""
通过 MCP 导出任务(export_data)导出 AI 表格,并可自动下载文件。
与普通命令的区别:
- 自动处理 taskId 轮询(直到拿到 downloadUrl 或达到轮询上限)。
- 自动保存导出文件到本地(可选 --output)。
用法:
python scripts/aitable_export_via_task.py <baseId> --scope all
python scripts/aitable_export_via_task.py <baseId> --scope table --table-id <tableId>
python scripts/aitable_export_via_task.py <baseId> --scope view --table-id <tableId> --view-id <viewId>
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
ALLOWED_FORMATS = {"excel", "attachment", "excel_and_attachment", "excel_with_inline_images"}
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
def run_dws(dws_bin: str, args: list[str], timeout_sec: int = 120) -> Tuple[int, str, str]:
cmd = [dws_bin] + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_sec)
return result.returncode, result.stdout.strip(), result.stderr.strip()
except subprocess.TimeoutExpired:
return 124, "", f"dws command timeout after {timeout_sec}s"
except FileNotFoundError:
return 127, "", f"dws binary not found: {dws_bin}"
def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
try:
obj = json.loads(raw)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
return None
def normalize_download_url(url: str) -> str:
if url.startswith("http://") or url.startswith("https://"):
return url
return f"https://{url}"
def download_file(url: str, output_path: Path) -> Tuple[bool, str]:
req = Request(url, method="GET")
try:
with urlopen(req, timeout=180) as resp:
if resp.status != 200:
return False, f"download http status: {resp.status}"
output_path.write_bytes(resp.read())
return True, ""
except HTTPError as e:
body = e.read().decode("utf-8", "ignore")
return False, f"HTTP {e.code}: {body[:300]}"
except URLError as e:
return False, f"URL error: {e.reason}"
def fail(msg: str, code: int = 1) -> None:
print(f"错误:{msg}", file=sys.stderr)
sys.exit(code)
def build_start_args(args: argparse.Namespace) -> list[str]:
cmd = [
"aitable",
"export",
"data",
"--base-id",
args.base_id,
"--scope",
args.scope,
"--format",
args.export_format,
"--timeout-ms",
str(args.timeout_ms),
]
if args.table_id:
cmd.extend(["--table-id", args.table_id])
if args.view_id:
cmd.extend(["--view-id", args.view_id])
return cmd
def main() -> None:
parser = argparse.ArgumentParser(description="通过 MCP 导出任务导出 AI 表格")
parser.add_argument("base_id", help="目标 AI 表格 baseId")
parser.add_argument("--scope", choices=["all", "table", "view"], required=True, help="导出范围")
parser.add_argument("--table-id", help="scope=table/view 时必填")
parser.add_argument("--view-id", help="scope=view 时必填")
parser.add_argument("--export-format", default="excel", choices=sorted(ALLOWED_FORMATS), help="导出格式")
parser.add_argument("--timeout-ms", type=int, default=1000, help="单次等待毫秒数,默认 1000")
parser.add_argument("--poll-timeout-ms", type=int, default=3000, help="轮询等待毫秒数,默认 3000")
parser.add_argument("--max-polls", type=int, default=10, help="最大轮询次数,默认 10")
parser.add_argument("--output", help="本地保存路径(不传则按 fileName 保存到当前目录)")
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径,默认 dws")
parser.add_argument("--no-download", action="store_true", help="仅返回 downloadUrl,不下载文件")
args = parser.parse_args()
if not validate_resource_id(args.base_id):
fail("无效的 baseId 格式")
if args.scope in ("table", "view") and not args.table_id:
fail("scope=table/view 时必须传 --table-id")
if args.scope == "view" and not args.view_id:
fail("scope=view 时必须传 --view-id")
print("[1/2] start export task", file=sys.stderr)
rc, out, err = run_dws(args.dws, build_start_args(args), timeout_sec=120)
if rc != 0:
fail(f"export_data 启动失败: {err or out}", rc)
obj = parse_json_output(out)
if not obj:
fail(f"export_data 返回非 JSON: {out[:300]}")
data = obj.get("data", {}) or {}
status = obj.get("status")
if status == "error":
fail(f"export_data 返回失败: {json.dumps(obj, ensure_ascii=False)}")
download_url = data.get("downloadUrl")
task_id = data.get("taskId")
file_name = data.get("fileName") or "export_result.bin"
polls = 0
while not download_url and task_id and polls < args.max_polls:
polls += 1
print(f"[2/2] polling task ({polls}/{args.max_polls})", file=sys.stderr)
rc2, out2, err2 = run_dws(
args.dws,
[
"aitable",
"export",
"data",
"--base-id",
args.base_id,
"--task-id",
task_id,
"--timeout-ms",
str(args.poll_timeout_ms),
],
timeout_sec=max(120, int(args.poll_timeout_ms / 1000) + 60),
)
if rc2 != 0:
fail(f"export_data 轮询失败: {err2 or out2}", rc2)
obj2 = parse_json_output(out2)
if not obj2:
fail(f"export_data 轮询返回非 JSON: {out2[:300]}")
if obj2.get("status") == "error":
fail(f"export_data 轮询返回失败: {json.dumps(obj2, ensure_ascii=False)}")
d2 = obj2.get("data", {}) or {}
download_url = d2.get("downloadUrl") or download_url
file_name = d2.get("fileName") or file_name
task_id = d2.get("taskId") or task_id
if not download_url:
time.sleep(0.2)
result: Dict[str, Any] = {
"baseId": args.base_id,
"scope": args.scope,
"exportFormat": args.export_format,
"taskId": task_id,
"fileName": file_name,
"downloadUrl": download_url,
"polledTimes": polls,
}
if not download_url:
result["status"] = "pending"
result["summary"] = "导出任务仍在处理中,请继续用 taskId 轮询。"
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(3)
if args.no_download:
result["status"] = "success"
result["summary"] = "导出完成(未下载文件)。"
print(json.dumps(result, ensure_ascii=False, indent=2))
return
norm_url = normalize_download_url(download_url)
output_path = Path(args.output).expanduser().resolve() if args.output else Path.cwd() / file_name
ok, dl_err = download_file(norm_url, output_path)
if not ok:
fail(f"downloadUrl 下载失败: {dl_err}")
result["status"] = "success"
result["summary"] = "导出完成并已下载。"
result["savedPath"] = str(output_path)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""
通过 MCP 文件导入任务(prepare_import_upload -> PUT -> import_data)导入 AI 表格。
与 import_records.py 的区别:
- 本脚本:走“文件导入任务”链路,通常会新建导入数据表。
- import_records.py:走 create_records,写入已有 table。
用法:
python scripts/aitable_import_via_task.py <baseId> <filePath>
python scripts/aitable_import_via_task.py <baseId> <filePath> --timeout 30
python scripts/aitable_import_via_task.py <baseId> <filePath> --dws /tmp/dws
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
ALLOWED_EXTENSIONS = {".csv", ".xlsx", ".xls"}
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
def run_dws(dws_bin: str, args: list[str], timeout_sec: int = 120) -> Tuple[int, str, str]:
cmd = [dws_bin] + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_sec)
return result.returncode, result.stdout.strip(), result.stderr.strip()
except subprocess.TimeoutExpired:
return 124, "", f"dws command timeout after {timeout_sec}s"
except FileNotFoundError:
return 127, "", f"dws binary not found: {dws_bin}"
def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
try:
obj = json.loads(raw)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
return None
def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]:
payload = file_path.read_bytes()
req = Request(upload_url, data=payload, method="PUT")
# 关键:清空 Content-Type,避免 SignatureDoesNotMatch。
req.add_header("Content-Type", "")
try:
with urlopen(req, timeout=180) as resp:
if resp.status == 200:
return True, ""
return False, f"unexpected HTTP status: {resp.status}"
except HTTPError as e:
body = e.read().decode("utf-8", "ignore")
return False, f"HTTP {e.code}: {body[:300]}"
except URLError as e:
return False, f"URL error: {e.reason}"
def fail(msg: str, exit_code: int = 1) -> None:
print(f"错误:{msg}", file=sys.stderr)
sys.exit(exit_code)
def main() -> None:
parser = argparse.ArgumentParser(description="通过文件导入任务导入 AI 表格")
parser.add_argument("base_id", help="目标 AI 表格 baseId")
parser.add_argument("file_path", help="待导入文件路径(.csv/.xlsx/.xls")
parser.add_argument("--timeout", type=int, default=30, help="import_data 等待秒数,默认 30")
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径,默认 dws")
args = parser.parse_args()
base_id = args.base_id.strip()
file_path = Path(args.file_path).expanduser().resolve()
if not validate_resource_id(base_id):
fail("无效的 baseId 格式")
if not file_path.exists() or not file_path.is_file():
fail(f"文件不存在或不可读: {file_path}")
if file_path.suffix.lower() not in ALLOWED_EXTENSIONS:
fail(f"仅支持 {sorted(ALLOWED_EXTENSIONS)},当前文件: {file_path.name}")
file_size = file_path.stat().st_size
if file_size <= 0:
fail("文件为空")
print(f"[1/3] prepare import upload: {file_path.name} ({file_size} bytes)", file=sys.stderr)
rc, out, err = run_dws(
args.dws,
[
"aitable",
"import",
"upload",
"--base-id",
base_id,
"--file-name",
file_path.name,
"--file-size",
str(file_size),
"--format",
"json",
],
)
if rc != 0:
fail(f"prepare_import_upload 失败: {err or out}", rc)
prepare_obj = parse_json_output(out)
if not prepare_obj:
fail(f"prepare_import_upload 返回非 JSON: {out[:300]}")
if prepare_obj.get("status") != "success":
fail(f"prepare_import_upload 返回失败: {json.dumps(prepare_obj, ensure_ascii=False)}")
pdata = prepare_obj.get("data") or {}
upload_url = pdata.get("uploadUrl")
import_id = pdata.get("importId")
if not upload_url or not import_id:
fail(f"prepare_import_upload 缺少 uploadUrl/importId: {json.dumps(pdata, ensure_ascii=False)}")
print("[2/3] upload file bytes via PUT", file=sys.stderr)
ok, put_err = put_file(upload_url, file_path)
if not ok:
fail(f"PUT 上传失败: {put_err}")
print("[3/3] trigger import_data", file=sys.stderr)
rc2, out2, err2 = run_dws(
args.dws,
[
"aitable",
"import",
"data",
"--import-id",
import_id,
"--timeout",
str(args.timeout),
"--format",
"json",
],
timeout_sec=max(120, args.timeout + 30),
)
if rc2 != 0:
fail(f"import_data 调用失败: {err2 or out2}", rc2)
import_obj = parse_json_output(out2)
if not import_obj:
fail(f"import_data 返回非 JSON: {out2[:300]}")
result = {
"baseId": base_id,
"fileName": file_path.name,
"fileSize": file_size,
"importId": import_id,
"status": import_obj.get("status"),
"summary": import_obj.get("summary"),
"data": import_obj.get("data", {}),
"error": import_obj.get("error", {}),
}
print(json.dumps(result, ensure_ascii=False, indent=2))
if import_obj.get("status") != "success":
sys.exit(2)
if __name__ == "__main__":
main()
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""
批量添加字段到钉钉 AI 表格数据表(新版 schema)
用法:
python bulk_add_fields.py <baseId> <tableId> fields.json
fields.json 格式:
[
{"fieldName": "字段 1", "type": "text"},
{"fieldName": "字段 2", "type": "number", "config": {"formatter": "INT"}},
{"fieldName": "字段 3", "type": "singleSelect", "config": {"options": [{"name": ""}]}}
]
兼容写法:
- name 会自动映射为 fieldName
- phone 会自动映射为 telephone
"""
import sys
import json
import subprocess
import os
import re
from pathlib import Path
from typing import Union, List, Dict, Any, Optional, Tuple
JsonData = Union[List[Any], Dict[str, Any]]
MAX_FILE_SIZE = 10 * 1024 * 1024
ALLOWED_FILE_EXTENSIONS = ['.json']
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
ALLOWED_FIELD_TYPES = {
'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency',
'user', 'department', 'group', 'progress', 'rating', 'checkbox',
'attachment', 'url', 'richText', 'telephone', 'email', 'idCard',
'barcode', 'geolocation', 'address', 'primaryDoc', 'formula',
'unidirectionalLink', 'bidirectionalLink', 'lookup', 'filterUp',
'creator', 'lastModifier', 'createdTime', 'lastModifiedTime',
}
FIELD_TYPE_ALIASES = {
'phone': 'telephone',
}
def resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path:
if allowed_root is None:
allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
allowed_root = Path(allowed_root).resolve()
target_path = (
Path(path).resolve()
if Path(path).is_absolute()
else (Path.cwd() / path).resolve()
)
try:
target_path.relative_to(allowed_root)
return target_path
except ValueError:
raise ValueError(
f"路径超出允许范围:{path}\n"
f"目标路径:{target_path}\n"
f"允许根目录:{allowed_root}\n"
f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内"
)
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
def validate_file_extension(filename: str, allowed_extensions: list) -> bool:
return any(filename.lower().endswith(ext) for ext in allowed_extensions)
def safe_json_load(file_path: Path, max_size: int = MAX_FILE_SIZE) -> JsonData:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def normalize_field_config(field: Dict[str, Any]) -> Dict[str, Any]:
normalized = dict(field)
if 'fieldName' not in normalized and 'name' in normalized:
normalized['fieldName'] = normalized.pop('name')
normalized['type'] = FIELD_TYPE_ALIASES.get(
normalized.get('type', 'text'), normalized.get('type', 'text')
)
return normalized
def validate_field_config(field: Dict[str, Any]) -> Tuple[bool, str]:
if not isinstance(field, dict):
return False, '字段配置必须是对象'
field = normalize_field_config(field)
if 'fieldName' not in field:
return False, '缺少必需字段:fieldName'
if not isinstance(field['fieldName'], str) or not field['fieldName'].strip():
return False, 'fieldName 必须是非空字符串'
field_type = field.get('type', 'text')
if field_type not in ALLOWED_FIELD_TYPES:
return False, f"不支持的字段类型:{field_type}"
config = field.get('config')
if config is not None and not isinstance(config, dict):
return False, 'config 必须是对象'
if field_type in {'singleSelect', 'multipleSelect'}:
options = (config or {}).get('options')
if not options or not isinstance(options, list):
return False, (
'singleSelect / multipleSelect 必须提供 config.options 数组'
)
if field_type in {'unidirectionalLink', 'bidirectionalLink'}:
linked_table_id = (config or {}).get('linkedTableId')
if not linked_table_id or not validate_resource_id(linked_table_id):
return False, (
'关联字段必须提供合法的 config.linkedTableId(目标 Table ID'
)
if field_type == 'lookup':
cfg = config or {}
if not cfg.get('associateField'):
return False, 'lookup 必须提供 config.associateField(本表关联字段的 fieldId'
if not cfg.get('valuesField'):
return False, 'lookup 必须提供 config.valuesField(关联目标表中要取值的字段 fieldId)'
if not cfg.get('aggregator'):
return False, 'lookup 必须提供 config.aggregatorSUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE'
if field_type == 'filterUp':
cfg = config or {}
if not cfg.get('targetSheet'):
return False, 'filterUp 必须提供 config.targetSheet(目标 Table ID'
filters = cfg.get('filters')
if not filters or not isinstance(filters, list):
return False, 'filterUp 必须提供 config.filters(至少一条筛选规则)'
if not cfg.get('valuesField'):
return False, 'filterUp 必须提供 config.valuesField(目标表中要取值的字段 fieldId)'
if not cfg.get('aggregator'):
return False, 'filterUp 必须提供 config.aggregatorSUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE'
return True, ''
def build_fields_json(fields: List[Dict[str, Any]]) -> str:
"""构建 --fields 参数的 JSON 字符串。"""
payload_fields = []
for field in fields:
normalized = normalize_field_config(field)
item: Dict[str, Any] = {
'fieldName': normalized['fieldName'].strip(),
'type': normalized.get('type', 'text'),
}
if 'config' in normalized and normalized['config'] is not None:
item['config'] = normalized['config']
payload_fields.append(item)
return json.dumps(payload_fields, ensure_ascii=False)
def run_dws(args: List[str]) -> Optional[Dict[str, Any]]:
if not args:
print('错误:空命令')
return None
cmd = ['dws'] + args
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}")
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
print(f"无法解析响应:{result.stdout[:200]}...")
print(f"JSON 解析错误:{e}")
return None
except subprocess.TimeoutExpired:
print('错误:命令执行超时(60 秒)')
return None
except FileNotFoundError:
print('错误:未找到 dws 命令,请确认已安装')
return None
def bulk_add_fields(
base_id: str, table_id: str, fields_file: str
) -> bool:
try:
safe_path = resolve_safe_path(fields_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
if not validate_file_extension(fields_file, ALLOWED_FILE_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_FILE_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
fields = safe_json_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except json.JSONDecodeError as e:
print(f"错误:JSON 格式无效:{e}")
return False
if not isinstance(fields, list) or not fields:
print('错误:fields.json 必须是非空 JSON 数组')
return False
if len(fields) > 15:
print('错误:单次最多创建 15 个字段,请拆分后重试')
return False
for i, field in enumerate(fields):
valid, error = validate_field_config(field)
if not valid:
print(f"错误:字段 #{i+1} 配置无效:{error}")
return False
fields_json = build_fields_json(fields)
result = run_dws([
'aitable', 'field', 'create',
'--base-id', base_id,
'--table-id', table_id,
'--fields', fields_json,
'--format', 'json',
])
if not result:
return False
print(json.dumps(result, ensure_ascii=False, indent=2))
return True
def main():
if len(sys.argv) != 4:
print(__doc__)
print('用法示例:')
print(' python bulk_add_fields.py basexxx tablexxx fields.json')
sys.exit(1)
base_id = sys.argv[1]
table_id = sys.argv[2]
fields_file = sys.argv[3]
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式')
sys.exit(1)
if not validate_resource_id(table_id):
print('错误:无效的 tableId 格式')
sys.exit(1)
success = bulk_add_fields(base_id, table_id, fields_file)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()
@@ -0,0 +1,333 @@
#!/usr/bin/env python3
"""
从 CSV / JSON 批量导入记录到钉钉 AI 表格(新版 schema)
用法:
python import_records.py <baseId> <tableId> data.csv [batch_size]
python import_records.py <baseId> <tableId> data.json [batch_size]
说明:
- CSV 表头默认视为 fieldId
- JSON 支持两种格式:
1. [{"cells": {"fldxxx": "value"}}, ...]
2. [{"fldxxx": "value"}, ...] # 会自动包装成 cells
⚠️ CSV 自动类型转换风险:
CSV 读入的所有 cell 都是 string,本脚本会尝试自动识别 'true'/'false'/数字
并转成对应类型(避免 text 字段塞入纯文本数字)。但当 fieldId 对应的字段是
text / telephone / idCard / barcode 这类"字符串形数字"字段时,自动转 int / float
会让 server 拒绝(字段类型不匹配)。这种情况建议改用 JSON 格式(自己显式控制类型),
或在 CSV 写入前给字段值前缀加引号 / 改为非纯数字。
"""
import sys
import csv
import json
import subprocess
import os
import re
from pathlib import Path
from typing import Union, List, Dict, Any, Optional, Tuple
JsonData = Union[List[Any], Dict[str, Any]]
RecordDict = Dict[str, str]
MAX_FILE_SIZE = 50 * 1024 * 1024
ALLOWED_CSV_EXTENSIONS = ['.csv']
ALLOWED_JSON_EXTENSIONS = ['.json']
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
MAX_RECORDS_PER_BATCH = 100
DEFAULT_BATCH_SIZE = 50
def resolve_safe_path(
path: str, allowed_root: Optional[str] = None
) -> Path:
if allowed_root is None:
allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
allowed_root = Path(allowed_root).resolve()
target_path = (
Path(path).resolve()
if Path(path).is_absolute()
else (Path.cwd() / path).resolve()
)
try:
target_path.relative_to(allowed_root)
return target_path
except ValueError:
raise ValueError(
f"路径超出允许范围:{path}\n"
f"目标路径:{target_path}\n"
f"允许根目录:{allowed_root}\n"
f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内"
)
def validate_resource_id(resource_id: str) -> bool:
return bool(
resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip())
)
def validate_file_extension(
filename: str, allowed_extensions: list
) -> bool:
return any(filename.lower().endswith(ext) for ext in allowed_extensions)
def safe_csv_load(
file_path: Path, max_size: int = MAX_FILE_SIZE
) -> List[RecordDict]:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8', newline='') as f:
return list(csv.DictReader(f))
def safe_json_load(
file_path: Path, max_size: int = MAX_FILE_SIZE
) -> JsonData:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def sanitize_record_value(
value: Any,
) -> Optional[Union[str, int, float, bool, list, dict]]:
if value is None:
return None
if isinstance(value, (bool, int, float, list, dict)):
return value
if not isinstance(value, str):
return value
if not value.strip():
return None
value = value.strip()
if value.lower() == 'true':
return True
if value.lower() == 'false':
return False
try:
if '.' in value:
return float(value)
return int(value)
except ValueError:
return value
def normalize_record(record: Dict[str, Any]) -> Dict[str, Any]:
if 'cells' in record and isinstance(record['cells'], dict):
cells = record['cells']
else:
cells = record
normalized = {}
for key, value in cells.items():
sanitized = sanitize_record_value(value)
if sanitized is not None:
normalized[key] = sanitized
return {'cells': normalized}
def validate_record(record: Dict[str, Any]) -> Tuple[bool, str]:
if not isinstance(record, dict):
return False, '记录必须是对象'
normalized = normalize_record(record)
cells = normalized.get('cells', {})
if not cells or not isinstance(cells, dict):
return False, '记录必须包含非空 cells 对象'
return True, ''
def run_dws(args: List[str]) -> Optional[Dict[str, Any]]:
if not args:
print('错误:空命令')
return None
cmd = ['dws'] + args
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=120
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}")
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
print(f"无法解析响应:{result.stdout[:200]}...")
print(f"JSON 解析错误:{e}")
return None
except subprocess.TimeoutExpired:
print('错误:命令执行超时(120 秒)')
return None
except FileNotFoundError:
print('错误:未找到 dws 命令,请确认已安装')
return None
def import_from_csv(
base_id: str, table_id: str, csv_file: str,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> bool:
try:
safe_path = resolve_safe_path(csv_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
if not validate_file_extension(csv_file, ALLOWED_CSV_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_CSV_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
rows = safe_csv_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except csv.Error as e:
print(f"错误:CSV 格式无效:{e}")
return False
if not rows:
print('错误:CSV 文件为空或没有有效数据行')
return False
records = [
normalize_record(row)
for row in rows
if normalize_record(row)['cells']
]
return import_records(base_id, table_id, records, batch_size)
def import_from_json(
base_id: str, table_id: str, json_file: str,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> bool:
try:
safe_path = resolve_safe_path(json_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
if not validate_file_extension(json_file, ALLOWED_JSON_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_JSON_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
records = safe_json_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except json.JSONDecodeError as e:
print(f"错误:JSON 格式无效:{e}")
return False
if not isinstance(records, list) or not records:
print('错误:JSON 文件必须是非空数组')
return False
for i, record in enumerate(records):
valid, error = validate_record(record)
if not valid:
print(f"错误:记录 #{i+1} 格式无效:{error}")
return False
return import_records(
base_id, table_id,
[normalize_record(r) for r in records], batch_size,
)
def import_records(
base_id: str, table_id: str,
records: List[Dict[str, Any]], batch_size: int,
) -> bool:
if batch_size <= 0:
print('错误:batch_size 必须大于 0')
return False
if batch_size > MAX_RECORDS_PER_BATCH:
batch_size = MAX_RECORDS_PER_BATCH
total_batches = (len(records) + batch_size - 1) // batch_size
success = True
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
batch_num = (i // batch_size) + 1
records_json = json.dumps(batch, ensure_ascii=False)
result = run_dws([
'aitable', 'record', 'create',
'--base-id', base_id,
'--table-id', table_id,
'--records', records_json,
'--format', 'json',
])
if result:
print(
f"[{batch_num}/{total_batches}] "
f"✓ 已提交 {len(batch)} 条记录"
)
else:
print(f"[{batch_num}/{total_batches}] ✗ 导入失败")
success = False
return success
def main():
if len(sys.argv) < 4 or len(sys.argv) > 5:
print(__doc__)
print('用法示例:')
print(
' python import_records.py basexxx tablexxx data.csv 50'
)
sys.exit(1)
base_id = sys.argv[1]
table_id = sys.argv[2]
input_file = sys.argv[3]
batch_size = (
int(sys.argv[4]) if len(sys.argv) == 5
else DEFAULT_BATCH_SIZE
)
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式')
sys.exit(1)
if not validate_resource_id(table_id):
print('错误:无效的 tableId 格式')
sys.exit(1)
if input_file.lower().endswith('.csv'):
success = import_from_csv(
base_id, table_id, input_file, batch_size
)
elif input_file.lower().endswith('.json'):
success = import_from_json(
base_id, table_id, input_file, batch_size
)
else:
print('错误:仅支持 .csv 或 .json 文件')
sys.exit(1)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
上传附件到钉钉 AI 表格 attachment 字段
完整流程(内部自动执行 3 步):
1. dws aitable attachment upload → 获取 uploadUrl + fileToken
2. HTTP PUT 上传文件到 OSS
3. 返回 fileToken,可直接用于 record create/update
用法:
python upload_attachment.py <baseId> <filePath>
输出 (JSON):
{ "fileToken": "ft_xxx", "fileName": "report.pdf", "size": 204800 }
然后在 record create/update 中使用:
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
"""
import sys
import json
import subprocess
import os
import mimetypes
import re
from pathlib import Path
from typing import Optional, Dict, Any
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
def detect_mime_type(file_path: Path) -> str:
"""根据文件扩展名推断 MIME type。"""
mime_type, _ = mimetypes.guess_type(str(file_path))
return mime_type or 'application/octet-stream'
def run_dws(args: list) -> Optional[Dict[str, Any]]:
"""调用 dws 命令并返回解析后的 JSON 结果。"""
cmd = ['dws'] + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
print(f"错误:dws 命令失败: {result.stderr.strip()}", file=sys.stderr)
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
print(f"错误:无法解析 dws 响应: {result.stdout[:300]}", file=sys.stderr)
return None
except subprocess.TimeoutExpired:
print('错误:dws 命令超时(60 秒)', file=sys.stderr)
return None
except FileNotFoundError:
print('错误:未找到 dws 命令,请确认已安装并在 PATH 中', file=sys.stderr)
return None
def upload_to_oss(upload_url: str, file_path: Path, mime_type: str) -> bool:
"""通过 HTTP PUT 上传文件到 OSS。"""
file_data = file_path.read_bytes()
req = Request(upload_url, data=file_data, method='PUT')
req.add_header('Content-Type', mime_type)
try:
with urlopen(req, timeout=120) as resp:
if resp.status == 200:
return True
print(f"错误:OSS 上传失败,HTTP {resp.status}", file=sys.stderr)
return False
except HTTPError as e:
print(f"错误:OSS 上传 HTTP 错误 {e.code}: {e.reason}", file=sys.stderr)
return False
except URLError as e:
print(f"错误:OSS 上传网络错误: {e.reason}", file=sys.stderr)
return False
def upload_attachment(base_id: str, file_path_str: str) -> Optional[Dict[str, Any]]:
"""
执行完整的附件上传流程:
1. prepare_attachment_upload → uploadUrl + fileToken
2. PUT 文件到 OSS
3. 返回 fileToken 信息
"""
# 验证文件
file_path = Path(file_path_str).resolve()
if not file_path.exists():
print(f"错误:文件不存在: {file_path}", file=sys.stderr)
return None
if not file_path.is_file():
print(f"错误:不是文件: {file_path}", file=sys.stderr)
return None
file_size = file_path.stat().st_size
if file_size <= 0:
print("错误:文件为空", file=sys.stderr)
return None
if file_size > MAX_FILE_SIZE:
print(f"错误:文件过大 ({file_size:,} 字节,限制 {MAX_FILE_SIZE:,} 字节)", file=sys.stderr)
return None
file_name = file_path.name
mime_type = detect_mime_type(file_path)
# 步骤 1: prepare_attachment_upload
print(f"步骤 1/3: 准备上传 {file_name} ({file_size:,} 字节, {mime_type})...", file=sys.stderr)
dws_args = [
'aitable', 'attachment', 'upload',
'--base-id', base_id,
'--file-name', file_name,
'--size', str(file_size),
'--mime-type', mime_type,
'--format', 'json',
]
result = run_dws(dws_args)
if not result:
return None
status = result.get('status', '')
if status != 'success':
error = result.get('error', {})
print(f"错误:准备上传失败: {error.get('message', json.dumps(error, ensure_ascii=False))}", file=sys.stderr)
return None
data = result.get('data', {})
upload_url = data.get('uploadUrl', '')
file_token = data.get('fileToken', '')
if not upload_url or not file_token:
print(f"错误:返回数据缺少 uploadUrl 或 fileToken: {json.dumps(data, ensure_ascii=False)}", file=sys.stderr)
return None
# 步骤 2: PUT 文件到 OSS
print(f"步骤 2/3: 上传文件到 OSS...", file=sys.stderr)
if not upload_to_oss(upload_url, file_path, mime_type):
return None
# 步骤 3: 返回 fileToken
print(f"步骤 3/3: 上传完成!", file=sys.stderr)
output = {
"fileToken": file_token,
"fileName": file_name,
"size": file_size,
"mimeType": mime_type,
}
return output
def main():
if len(sys.argv) != 3:
print(__doc__)
print('用法:')
print(' python upload_attachment.py <baseId> <filePath>')
print()
print('示例:')
print(' python upload_attachment.py G1DKw2zgV2bEk6PMSBooNxlEVB5r9YAn ./report.pdf')
print()
print('然后在 record create 中使用返回的 fileToken:')
print(' dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \\')
print(' --records \'[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]\' --format json')
sys.exit(1)
base_id = sys.argv[1]
file_path = sys.argv[2]
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式', file=sys.stderr)
sys.exit(1)
result = upload_attachment(base_id, file_path)
if result is None:
sys.exit(1)
# 正常输出到 stdout(JSON 格式,方便解析)
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0)
if __name__ == '__main__':
main()