diff --git a/.agents/skills/lark-approval/SKILL.md b/.agents/skills/lark-approval/SKILL.md new file mode 100644 index 0000000..713330e --- /dev/null +++ b/.agents/skills/lark-approval/SKILL.md @@ -0,0 +1,52 @@ +--- +name: lark-approval +version: 1.0.0 +description: "飞书审批 API:审批实例、审批任务管理。" +metadata: + requires: + bins: ["lark-cli"] + cliHelp: "lark-cli approval --help" +--- + +# approval (v4) + +**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),其中包含认证、权限处理** + +## API Resources + +```bash +lark-cli schema approval.. # 调用 API 前必须先查看参数结构 +lark-cli approval [flags] # 调用 API +``` + +> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。 + +### instances + + - `get` — 获取单个审批实例详情 + - `cancel` — 撤回审批实例 + - `cc` — 抄送审批实例 + - `initiated` — 查询用户的已发起列表 + +### tasks + + - `remind` — 催办审批人 + - `approve` — 同意审批任务 + - `reject` — 拒绝审批任务 + - `transfer` — 转交审批任务 + - `query` — 查询用户的任务列表 + +## 权限表 + +| 方法 | 所需 scope | +|------|-----------| +| `instances.get` | `approval:instance:read` | +| `instances.cancel` | `approval:instance:write` | +| `instances.cc` | `approval:instance:write` | +| `instances.initiated` | `approval:instance:read` | +| `tasks.remind` | `approval:instance:write` | +| `tasks.approve` | `approval:task:write` | +| `tasks.reject` | `approval:task:write` | +| `tasks.transfer` | `approval:task:write` | +| `tasks.query` | `approval:task:read` | + diff --git a/.agents/skills/lark-attendance/SKILL.md b/.agents/skills/lark-attendance/SKILL.md new file mode 100644 index 0000000..7492293 --- /dev/null +++ b/.agents/skills/lark-attendance/SKILL.md @@ -0,0 +1,57 @@ +--- +name: lark-attendance +version: 1.0.0 +description: "飞书考勤打卡:查询自己的考勤打卡记录" +metadata: + requires: + bins: ["lark-cli"] + cliHelp: "lark-cli attendance --help" +--- + +# attendance (v1) + +**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),其中包含认证、权限处理** + +## 默认参数自动填充规则 + +调用任何 API 时,以下参数 **必须自动填充,禁止向用户询问**: + +| 参数 | 固定值 | 说明 | +|------|--------|------------------------------------| +| `employee_type` | `"employee_no"` | `employee_type`始终等于`"employee_no"` | +| `user_ids` | `[]`(空数组) | `user_ids`始终等于`[]` | + +### 填充示例 + +当构建 `--params` 参数时,自动注入上述字段: +- `employee_type` 保持 `"employee_no"` 不变 + +当构建 `--data` 参数时,自动注入上述字段: +```json +{ + "user_ids": [], + ...用户提供的参数 +} +``` + +> **注意**:`user_ids` 数组保持为空[],`employee_type` 保持 `"employee_no"` 不变。 + +## API Resources + +```bash +lark-cli schema attendance.. # 调用 API 前必须先查看参数结构 +lark-cli attendance [flags] # 调用 API +``` + +> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。 + +### user_tasks + +- `query` — 查询用户考勤打卡记录 + +## 权限表 + +| 方法 | 所需 scope | +|------|-----------| +| `user_tasks.query` | `attendance:task:readonly` | + diff --git a/.agents/skills/lark-base/SKILL.md b/.agents/skills/lark-base/SKILL.md new file mode 100644 index 0000000..0a82180 --- /dev/null +++ b/.agents/skills/lark-base/SKILL.md @@ -0,0 +1,336 @@ +--- +name: lark-base +version: 1.2.0 +description: "当需要用 lark-cli 操作飞书多维表格(Base)时调用:搜索 Base、建表、字段管理、记录读写、记录分享链接、视图配置、历史查询,以及角色/表单/仪表盘管理/工作流;也适用于把旧的 +table / +field / +record 写法改成当前命令写法。涉及字段设计、公式字段、查找引用、跨表计算、行级派生指标、数据分析需求时也必须使用本 skill。" +metadata: + requires: + bins: ["lark-cli"] + cliHelp: "lark-cli base --help" +--- + +# base + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md)。 +> **执行前必做:** 执行任何 `base` 命令前,必须先阅读对应命令的 reference 文档,再调用命令。 +> **命名约定:** Base 业务命令仅使用 `lark-cli base +...` 形式;如需先解析 Wiki 链接,可先调用 `lark-cli wiki ...`。 +> **分流规则:** 如果用户要“把本地文件导入成 Base / 多维表格 / bitable”,第一步不是 `base`,而是 `lark-cli drive +import --type bitable`;导入完成后再回到 `lark-cli base +...` 做表内操作。 + +## 1. 何时使用本 Skill + +### 1.1 触发条件 + +以下场景应使用本 skill: + +- 用户明确要操作飞书多维表格 / Base。 +- 用户要建表、改表、查表、删表,或管理字段、记录、视图。 +- 用户要做公式字段、lookup 字段、派生指标、跨表计算。 +- 用户要做临时统计、聚合分析、比较排序、求最值。 +- 用户要管理 workflow、dashboard、表单、角色权限。 +- 用户给出 `/base/{token}` 链接。 +- 用户给出 `/wiki/{token}` 链接,且最终解析为 `bitable`。 +- 用户要把旧的 Base 聚合式写法改成当前原子命令写法,例如把旧 `+table / +field / +record / +view / +history / +workspace` 改写成当前命令。 + +以下场景不应使用本 skill: + +- 用户只是做认证、初始化配置、切换 `--as user/bot`、处理 scope。此时先读 `../lark-shared/SKILL.md`。 +- 用户只是泛化地讨论“数据分析 / 字段设计”,但并不在 Base 场景中。不要因为提到“统计 / 公式 / lookup”就误触发。 + +### 1.2 前置约束 + +1. 先阅读 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md)。 +2. Base 业务命令仅使用 `lark-cli base +...` 形式的 shortcut 命令;如果输入是 Wiki 链接,可先调用 `lark-cli wiki spaces get_node` 解析真实 token。 +3. 定位到命令后,先读该命令对应的 reference,再执行命令。 +4. 如果用户要把本地 Excel / CSV / `.base` 快照导入成 Base / 多维表格 / bitable,第一步不是 `base`,而是 `lark-cli drive +import --type bitable`;导入完成后再回到 `lark-cli base +...` 做表内操作。 +5. 不要在 Base 场景改走 `lark-cli api /open-apis/bitable/v1/...`。 +6. 如果用户只给 Base 名称、关键词,或说“帮我找一个多维表格”,先通过 `lark-cli docs +search --query --filter '{"doc_types":["BITABLE"]}'` 搜索 `BITABLE` 资源;拿到 Base URL 后再使用本 skill 的 `base +...` 命令。复杂搜索再读 [`../lark-doc/references/lark-doc-search.md`](../lark-doc/references/lark-doc-search.md):标题精确匹配、限定创建者/群/文件夹/时间范围、只搜标题/评论、分页/全量搜索。 + +## 2. 模块与命令导航 + +本章按“先选模块,再选命令”的方式组织。先判断用户目标属于哪个大模块,再进入对应子模块,按要求阅读 reference 后执行命令。 + +### 2.1 模块地图 + +| 大模块 | 处理什么问题 | 包含的小模块 / 能力 | +|------|-------------|-------------------| +| Base 模块 | 管理 Base 本体,或从链接进入 Base 场景 | `base-create / base-get / base-copy`,Base / Wiki 链接解析 | +| 表与数据模块 | 管理 Base 内部结构与日常数据操作 | `table / field / record / view` | +| 公式 / Lookup 模块 | 处理派生字段、条件判断、跨表计算、固定查找引用 | `formula / lookup` 字段创建与更新 | +| 数据分析模块 | 做一次性筛选、分组、聚合分析 | `data-query` | +| Workflow 模块 | 管理自动化流程 | `workflow-list / get / create / update / enable / disable` | +| Dashboard 模块 | 管理仪表盘和图表组件 | `dashboard-* / dashboard-block-*` | +| 表单模块 | 管理表单和表单题目 | `form-* / form-questions-*` | +| 权限与角色模块 | 管理高级权限和自定义角色 | `advperm-* / role-*` | + +### 2.2 Base 模块 + +用于管理 Base 本体,或从用户给出的链接进入后续 Base 操作。 +模块索引:[`references/lark-base-workspace.md`](references/lark-base-workspace.md) + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `lark-cli docs +search --query --filter '{"doc_types":["BITABLE"]}'` | 按名称、关键词查找 Base / 多维表格 / bitable | 复杂搜索再读 [`../lark-doc/references/lark-doc-search.md`](../lark-doc/references/lark-doc-search.md) | 先定位资源,再回到 `base +...` 操作表内数据 | +| `+base-create` | 创建新的 Base | [`lark-base-base-create.md`](references/lark-base-base-create.md)、[`lark-base-workspace.md`](references/lark-base-workspace.md) | 写入操作;执行前先读 reference;`--folder-token`、`--time-zone` 都是可选项 | +| `+base-get` | 获取 Base 信息 | [`lark-base-base-get.md`](references/lark-base-base-get.md)、[`lark-base-workspace.md`](references/lark-base-workspace.md) | 适合确认 Base 本体信息,不替代表/字段结构读取 | +| `+base-copy` | 复制已有 Base | [`lark-base-base-copy.md`](references/lark-base-base-copy.md)、[`lark-base-workspace.md`](references/lark-base-workspace.md) | 写入操作;执行前先读 reference;复制成功后应主动返回新 Base 标识信息 | + +### 2.3 表与数据模块 + +这是最常用的大模块,包含 `table / field / record / view` 四类子模块。 +补充示例:[`references/examples.md`](references/examples.md),适合需要串联 table / record / view 完整操作链路时再读。 + +#### 2.3.1 Table 子模块 + +子模块索引:[`references/lark-base-table.md`](references/lark-base-table.md) + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+table-list / +table-get` | 列出数据表,或获取单个表详情 | [`lark-base-table-list.md`](references/lark-base-table-list.md)、[`lark-base-table-get.md`](references/lark-base-table-get.md) | `+table-list` 只能串行执行;`+table-get` 适合删除/修改前确认目标 | +| `+table-create / +table-update / +table-delete` | 创建、更新或删除数据表 | [`lark-base-table-create.md`](references/lark-base-table-create.md)、[`lark-base-table-update.md`](references/lark-base-table-update.md)、[`lark-base-table-delete.md`](references/lark-base-table-delete.md) | 创建适合一次性建表;更新前先确认目标表;删除时用户已明确目标可直接执行并带 `--yes` | + +#### 2.3.2 Field 子模块 + +普通字段管理走这里;如果字段类型是 `formula` 或 `lookup`,转到下方“公式 / Lookup 模块”。 +子模块索引:[`references/lark-base-field.md`](references/lark-base-field.md) + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+field-list / +field-get` | 列出字段结构,或获取单个字段详情 | [`lark-base-field-list.md`](references/lark-base-field-list.md)、[`lark-base-field-get.md`](references/lark-base-field-get.md) | 写记录、写字段、做分析前常先读 `+field-list`;`+field-list` 只能串行执行;`+field-get` 适合删除/更新前确认目标 | +| `+field-create / +field-update / +field-delete` | 创建、更新或删除普通字段 | [`lark-base-field-create.md`](references/lark-base-field-create.md)、[`lark-base-field-update.md`](references/lark-base-field-update.md)、[`lark-base-field-delete.md`](references/lark-base-field-delete.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 写字段前先看字段属性规范;如果涉及类型转换,直接按 `+field-update` 中的字段类型变更规则执行,只在安全白名单内考虑原地转换;如果类型是 `formula / lookup`,先转去读对应 guide;删除时用户已明确目标可直接执行并带 `--yes` | +| `+field-search-options` | 查询字段可选项 | [`lark-base-field-search-options.md`](references/lark-base-field-search-options.md) | 适合单选/多选等选项型字段 | + +#### 2.3.3 Record 子模块 + +子模块索引:[`references/lark-base-record.md`](references/lark-base-record.md)、[`references/lark-base-history.md`](references/lark-base-history.md) + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+record-search / +record-list / +record-get` | 按关键词检索记录、读取记录明细 / 分页导出,或按 ID 获取一条或多条记录 | [`lark-base-record-read-sop.md`](references/lark-base-record-read-sop.md) | 记录读取统一先读 read SOP guide:已知 `record_id` 用 `+record-get`;明确关键词用 `+record-search`;普通明细用 `+record-list`;明确筛选 / 排序 / Top N 用临时视图投影后 `+record-list --view-id`;统计聚合才分流到 `+data-query`;`+record-get` 支持重复 `--record-id` 或 `--json` 读取多条记录 | +| `+record-upsert / +record-batch-create / +record-batch-update` | 创建、更新或批量写入记录 | [`lark-base-record-upsert.md`](references/lark-base-record-upsert.md)、[`lark-base-record-batch-create.md`](references/lark-base-record-batch-create.md)、[`lark-base-record-batch-update.md`](references/lark-base-record-batch-update.md)、[`lark-base-cell-value.md`](references/lark-base-cell-value.md) | 写前先 `+field-list`;只写存储字段;`+record-batch-update` 为同值更新(同一 patch 应用到多条记录);批量单次不超过 `200` 条;附件不要走这里 | +| `+record-upload-attachment` | 给已有记录上传附件 | [`lark-base-record-upload-attachment.md`](references/lark-base-record-upload-attachment.md) | 附件上传专用链路,不要用 `+record-upsert` / `+record-batch-*` 伪造附件值 | +| `lark-cli docs +media-download` | 下载 Base 附件文件到本地 | [`../lark-doc/references/lark-doc-media-download.md`](../lark-doc/references/lark-doc-media-download.md) | Base 附件的 `file_token` 从 `+record-get` 返回的附件字段数组里取;**不要用 `lark-cli drive +download`**(对 Base 附件返回 403) | +| `+record-delete` | 删除一条或多条记录 | [`lark-base-record-delete.md`](references/lark-base-record-delete.md) | 删除多条时重复传 `--record-id` 指定多个记录;用户已明确目标可直接执行并带 `--yes` | +| `+record-history-list` | 查询指定记录的变更历史 | [`lark-base-record-history-list.md`](references/lark-base-record-history-list.md) | 按 `table-id + record-id` 查询,不支持整表扫描;`+record-history-list` 只能串行执行 | +| `+record-share-link-create` | 为一条或多条记录生成分享链接 | [`lark-base-record-share-link-create.md`](references/lark-base-record-share-link-create.md) | 单次最多 100 条;重复 record_id 会自动去重;适合分享单条记录或批量分享场景 | + +#### 2.3.4 View 子模块 + +子模块索引:[`references/lark-base-view.md`](references/lark-base-view.md) + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+view-list / +view-get` | 列出视图,或获取视图详情 | [`lark-base-view-list.md`](references/lark-base-view-list.md)、[`lark-base-view-get.md`](references/lark-base-view-get.md) | `+view-list` 只能串行执行;`+view-get` 适合查看已有视图配置 | +| `+view-create / +view-delete / +view-rename` | 创建、删除或重命名视图 | [`lark-base-view-create.md`](references/lark-base-view-create.md)、[`lark-base-view-delete.md`](references/lark-base-view-delete.md)、[`lark-base-view-rename.md`](references/lark-base-view-rename.md) | 创建前先确认表和视图类型;删除前先确认目标;用户已明确新名字时可直接重命名 | +| `+view-get-filter / +view-set-filter` | 读取或配置筛选条件 | [`lark-base-view-get-filter.md`](references/lark-base-view-get-filter.md)、[`lark-base-view-set-filter.md`](references/lark-base-view-set-filter.md)、[`lark-base-record-read-sop.md`](references/lark-base-record-read-sop.md) | 常与 `+record-list` 组合,用于按视图筛选读取 | +| `+view-get-sort / +view-set-sort` | 读取或配置排序 | [`lark-base-view-get-sort.md`](references/lark-base-view-get-sort.md)、[`lark-base-view-set-sort.md`](references/lark-base-view-set-sort.md) | 字段名必须来自真实结构 | +| `+view-get-group / +view-set-group` | 读取或配置分组 | [`lark-base-view-get-group.md`](references/lark-base-view-get-group.md)、[`lark-base-view-set-group.md`](references/lark-base-view-set-group.md) | 字段名必须来自真实结构 | +| `+view-get-visible-fields / +view-set-visible-fields` | 读取或配置视图可见字段 | [`lark-base-view-get-visible-fields.md`](references/lark-base-view-get-visible-fields.md)、[`lark-base-view-set-visible-fields.md`](references/lark-base-view-set-visible-fields.md) | 用于控制视图中的字段顺序与可见性;字段名必须来自真实结构 | +| `+view-get-card / +view-set-card` | 读取或配置卡片视图 | [`lark-base-view-get-card.md`](references/lark-base-view-get-card.md)、[`lark-base-view-set-card.md`](references/lark-base-view-set-card.md) | 适合卡片展示场景 | +| `+view-get-timebar / +view-set-timebar` | 读取或配置时间轴视图 | [`lark-base-view-get-timebar.md`](references/lark-base-view-get-timebar.md)、[`lark-base-view-set-timebar.md`](references/lark-base-view-set-timebar.md) | 适合时间线展示场景 | + +### 2.4 公式 / Lookup 模块 + +只要用户诉求涉及派生指标、条件判断、文本处理、日期差、跨表计算、跨表筛选后取值,都要先判断是否进入本模块。 + +默认优先考虑 `formula`:适合常规计算、条件判断、文本处理、日期差、跨表聚合,以及需要长期显示在表里的派生结果。 +只有当用户明确要求 `lookup`,或场景天然符合 `from / select / where / aggregate` 这种固定查找建模时,再使用 `lookup`。 + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+field-create`(`type=formula`) | 创建公式字段 | [`formula-field-guide.md`](references/formula-field-guide.md)、[`lark-base-field-create.md`](references/lark-base-field-create.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 没读 guide 前不要直接创建 | +| `+field-update`(`type=formula`) | 更新公式字段 | [`formula-field-guide.md`](references/formula-field-guide.md)、[`lark-base-field-update.md`](references/lark-base-field-update.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 先拿当前表结构 | +| `+field-create`(`type=lookup`) | 创建 lookup 字段 | [`lookup-field-guide.md`](references/lookup-field-guide.md)、[`lark-base-field-create.md`](references/lark-base-field-create.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 没读 guide 前不要直接创建 | +| `+field-update`(`type=lookup`) | 更新 lookup 字段 | [`lookup-field-guide.md`](references/lookup-field-guide.md)、[`lark-base-field-update.md`](references/lark-base-field-update.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 跨表时还要拿目标表结构 | + +### 2.5 数据分析模块 + +用于一次性分析和临时聚合查询。用户要的是“这次算出来的结果”,而不是把结果沉淀成字段时,优先进入本模块。 + +进入本模块前先确认几件事: + +- `+data-query` 只做聚合查询(分组、过滤、排序、聚合计算),不用于列出原始记录或逐条明细。 +- 调用者必须是目标多维表格的管理员,拥有目标多维表格的 FA(Full Access / 完全访问权限),否则会返回权限错误。 +- `+data-query` 只支持白名单字段类型;`formula`、`lookup`、附件、系统字段、关联等字段不能用于 `dimensions / measures / filters / sort`。 + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+data-query` | 做分组统计、SUM / AVG / COUNT / MAX / MIN、条件筛选后的聚合分析 | [`lark-base-data-query.md`](references/lark-base-data-query.md) | 字段名必须精确匹配真实字段名;不要用 `+record-list` / `+record-search` 拉全量再手算;`+data-query` 不返回原始记录;使用前先确认权限和字段类型是否受支持 | + +### 2.6 Workflow 模块 + +这是高约束模块。执行任何 workflow 命令前,都必须先读对应命令文档和 schema。 +模块索引:[`references/lark-base-workflow.md`](references/lark-base-workflow.md) + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+workflow-list / +workflow-get` | 列出 workflow,或获取完整 workflow 结构 | [`lark-base-workflow-list.md`](references/lark-base-workflow-list.md)、[`lark-base-workflow-get.md`](references/lark-base-workflow-get.md)、[`lark-base-workflow-schema.md`](references/lark-base-workflow-schema.md) | `+workflow-list` 只返回摘要且只能串行执行;需要完整结构时用 `+workflow-get` | +| `+workflow-create / +workflow-update` | 创建或更新 workflow | [`lark-base-workflow-create.md`](references/lark-base-workflow-create.md)、[`lark-base-workflow-update.md`](references/lark-base-workflow-update.md)、[`lark-base-workflow-schema.md`](references/lark-base-workflow-schema.md) | 先读 schema;禁止凭自然语言猜 `type`;先确认真实表名和字段名 | +| `+workflow-enable / +workflow-disable` | 启用或停用 workflow | [`lark-base-workflow-enable.md`](references/lark-base-workflow-enable.md)、[`lark-base-workflow-disable.md`](references/lark-base-workflow-disable.md)、[`lark-base-workflow-schema.md`](references/lark-base-workflow-schema.md) | 启用或停用前先确认目标 workflow;`workflow_id` 与 `table_id` 需按前缀区分 | + +### 2.7 Dashboard 模块 + +当用户提到“仪表盘、dashboard、数据看板、图表、可视化、block、组件、添加组件、创建图表”等关键词时,进入本模块,并先阅读 [`lark-base-dashboard.md`](references/lark-base-dashboard.md)。 + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+dashboard-list / +dashboard-get` | 列出仪表盘,或获取仪表盘详情 | [`lark-base-dashboard-list.md`](references/lark-base-dashboard-list.md)、[`lark-base-dashboard-get.md`](references/lark-base-dashboard-get.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md) | 进入仪表盘语义后先读 guide;`+dashboard-list` 只能串行执行 | +| `+dashboard-create / +dashboard-update / +dashboard-delete` | 创建、更新或删除仪表盘 | [`lark-base-dashboard-create.md`](references/lark-base-dashboard-create.md)、[`lark-base-dashboard-update.md`](references/lark-base-dashboard-update.md)、[`lark-base-dashboard-delete.md`](references/lark-base-dashboard-delete.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md) | 创建前先明确看板目标和展示场景;更新前先读取当前配置;删除前先确认目标 | +| `+dashboard-block-list / +dashboard-block-get` | 列出图表组件,或获取单个 block 详情 | [`lark-base-dashboard-block-list.md`](references/lark-base-dashboard-block-list.md)、[`lark-base-dashboard-block-get.md`](references/lark-base-dashboard-block-get.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md)、[`dashboard-block-data-config.md`](references/dashboard-block-data-config.md) | `+dashboard-block-list` 只能串行执行;查看配置细节时读 block config 文档 | +| `+dashboard-block-create / +dashboard-block-update / +dashboard-block-delete` | 创建、更新或删除图表组件 | [`lark-base-dashboard-block-create.md`](references/lark-base-dashboard-block-create.md)、[`lark-base-dashboard-block-update.md`](references/lark-base-dashboard-block-update.md)、[`lark-base-dashboard-block-delete.md`](references/lark-base-dashboard-block-delete.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md)、[`dashboard-block-data-config.md`](references/dashboard-block-data-config.md) | 涉及 `data_config`、图表类型、filter 时要读 block config 文档;删除前先确认目标 | + +### 2.8 表单模块 + +用于管理表单本体和表单题目。 +模块索引:[`references/lark-base-form.md`](references/lark-base-form.md)、[`references/lark-base-form-questions.md`](references/lark-base-form-questions.md) +表单问题相关操作依赖 `form-id`;具体获取方式见 `form-list` 和 `form-create` 的 reference。 + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+form-list / +form-get` | 列出表单,或获取单个表单 | [`lark-base-form-list.md`](references/lark-base-form-list.md)、[`lark-base-form-get.md`](references/lark-base-form-get.md) | `+form-list` 可用来获取 `form-id`;`+form-get` 适合查看已有表单配置 | +| `+form-create / +form-update / +form-delete` | 创建、更新或删除表单 | [`lark-base-form-create.md`](references/lark-base-form-create.md)、[`lark-base-form-update.md`](references/lark-base-form-update.md)、[`lark-base-form-delete.md`](references/lark-base-form-delete.md) | 创建后可继续进入表单问题相关操作;更新或删除前先确认目标表单 | +| `+form-questions-list` | 列出表单题目 | [`lark-base-form-questions-list.md`](references/lark-base-form-questions-list.md) | 适合查看已有题目结构 | +| `+form-questions-create / +form-questions-update / +form-questions-delete` | 创建、更新或删除题目 | [`lark-base-form-questions-create.md`](references/lark-base-form-questions-create.md)、[`lark-base-form-questions-update.md`](references/lark-base-form-questions-update.md)、[`lark-base-form-questions-delete.md`](references/lark-base-form-questions-delete.md) | 先确认 `form-id`;更新或删除前先确认题目目标 | + +### 2.9 权限与角色模块 + +用于启用高级权限,以及管理 Base 自定义角色。 +涉及 `+advperm-enable / +advperm-disable / +role-*` 时,操作用户必须为 Base 管理员,否则会返回权限错误。 + +| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | +|------|------------------|----------------|----------| +| `+advperm-enable / +advperm-disable` | 启用或停用高级权限 | [`lark-base-advperm-enable.md`](references/lark-base-advperm-enable.md)、[`lark-base-advperm-disable.md`](references/lark-base-advperm-disable.md) | 管理角色前必须先启用;停用是高风险操作,会使已有自定义角色失效 | +| `+role-list / +role-get` | 列出角色,或获取角色详情 | [`lark-base-role-list.md`](references/lark-base-role-list.md)、[`lark-base-role-get.md`](references/lark-base-role-get.md)、[`role-config.md`](references/role-config.md) | `+role-list` 只能串行执行;`+role-get` 适合查看完整权限配置 | +| `+role-create / +role-update / +role-delete` | 创建、更新或删除角色 | [`lark-base-role-create.md`](references/lark-base-role-create.md)、[`lark-base-role-update.md`](references/lark-base-role-update.md)、[`lark-base-role-delete.md`](references/lark-base-role-delete.md)、[`role-config.md`](references/role-config.md) | `+role-create` 仅支持 `custom_role`;`+role-update` 采用 Delta Merge,`role_name` 和 `role_type` 即使不改也必须传当前值;`+role-delete` 不可逆 | + +## 3. 多维表格通用知识 + +飞书多维表格英文名是 `Base`,曾用名 `Bitable`;因此旧文档、返回字段、参数名或错误信息里出现 `bitable` 多属历史兼容,不代表应改用另一套命令体系。 + +### 3.1 字段分类与可写性 + +| 字段类型 | 含义 | 能否直接作为 `+record-upsert / +record-batch-create / +record-batch-update` 写入目标 | 说明 | +|----------|------|-----------------------------------------------------------|------| +| 存储字段 | 真实存用户输入的数据 | 可以 | 常见如文本、数字、日期、单选、多选、人员、关联 | +| 附件字段 | 存储文件附件 | 不应直接按普通字段写 | 上传附件走 `+record-upload-attachment`;下载附件走 `lark-cli docs +media-download` | +| 系统字段 | 平台自动维护 | 不可以 | 常见如创建时间、更新时间、创建人、修改人、自动编号 | +| `formula` 字段 | 通过表达式计算 | 不可以 | 只读字段 | +| `lookup` 字段 | 通过跨表规则查找引用 | 不可以 | 只读字段 | + +### 3.2 任务选路心智模型 + +| 用户诉求 | 优先方案 | 不要误走 | +|---------|----------|----------| +| 一次性分析 / 临时统计 | `+data-query` | 不要用 `+record-list` / `+record-search` 拉全量后手算 | +| 要把结果长期显示在表里 | `formula` 字段 | 不要只给一次性手工分析结果 | +| 用户明确要求 lookup,或天然是固定查找配置 | `lookup` 字段 | 不要默认先上 lookup;先判断 formula 是否更合适 | +| 读取原始记录明细 / 关键词检索 / 导出 | `+record-search / +record-list / +record-get` | 不要拿 `+data-query` 当取数命令 | +| 上传附件到记录 | `+record-upload-attachment` | 不要用 `+record-upsert` / `+record-batch-*` 伪造附件值 | +| 下载记录里的附件文件 | `lark-cli docs +media-download --token --output ` | `file_token` 从 `+record-get` 返回的附件字段里取;用法见 [`../lark-doc/references/lark-doc-media-download.md`](../lark-doc/references/lark-doc-media-download.md) | +| 基于视图做筛选读取 | `+view-set-filter` + `+record-list` | 不要跳过视图筛选直接猜条件 | +| 本地 Excel / CSV / `.base` 导入为 Base | `lark-cli drive +import --type bitable` | 不要误走 `+base-create`、`+table-create` 或 `+record-upsert` | + +### 3.3 表名、字段名与表达式引用 + +1. 表名、字段名必须精确匹配真实返回,来源应是 `+table-list / +table-get / +field-list`。 +2. 不要凭自然语言猜名称,不要自行改写用户口述中的表名、字段名。 +3. `formula / lookup / data-query / workflow` 中出现的名称同样必须精确匹配;表达式引用、where 条件、DSL 字段名、workflow 配置都遵守同一规则。 +4. 跨表场景必须额外读取目标表结构,不能只看当前表。 + +### 3.4 Token 与链接 + +这是高优先级章节。只要用户输入里出现链接、token,或报错涉及 `baseToken` / `wiki_token` / `obj_token`,都应优先回到这里检查。 + +| 输入类型 | 正确处理方式 | 说明 | +|---------|--------------|------| +| 直接 Base 链接 `/base/{token}` | 直接提取 token 作为 `--base-token` | 不要把完整 URL 直接作为 `--base-token` | +| Wiki 链接 `/wiki/{token}` | 先 `wiki.spaces.get_node`,再取 `node.obj_token` | 不要把 `wiki_token` 直接当 `--base-token` | +| URL 中的 `?table={id}` | 先按前缀判断对象类型 | `tbl` 开头表示数据表 `table-id`,可作为 `--table-id`;`blk` 开头表示仪表盘 `dashboard-ID`;`wkf` 开头表示 `workflow-ID`;`ldx` 开头表示内嵌文档,不要一律当成 `--table-id` | +| URL 中的 `?view={id}` | 提取为 `--view-id` | 适合直接定位视图 | + +| `lark-cli wiki spaces get_node` 返回的 `obj_type` | 后续路线 | 说明 | +|-----------------------------------------------|----------|------| +| `bitable` | 优先走 `lark-cli base +...` | 如果 shortcut 不覆盖,再用 `lark-cli base `;不要改走 `lark-cli api /open-apis/bitable/v1/...` | +| `docx` | 转到文档 / Drive 相关 skill | 不继续使用本 skill 的 Base 命令 | +| `sheet` | 转到 Sheets 相关 skill | 不继续使用本 skill 的 Base 命令 | +| `slides` | 转到 Drive 相关 skill | 不继续使用本 skill 的 Base 命令 | +| `mindnote` | 转到 Drive 相关 skill | 不继续使用本 skill 的 Base 命令 | + +### 3.5 身份选择与权限降级策略 + +多维表格通常属于用户的个人或团队资源。**默认应优先使用 `--as user`(用户身份)执行所有 Base 操作**,始终显式指定身份。 + +- **`--as user`(推荐)**:以当前登录用户身份操作其有权访问的 Base。执行前先完成用户授权: + +```bash +lark-cli auth login --domain base +``` + +- **`--as bot`(降级)**:仅当 user 身份权限不足、且 bot 身份确实拥有目标 Base 的访问权限时,才降级使用。bot 看不到用户私有资源,行为以应用身份执行。 + +**执行规则**: + +1. 所有操作默认先用 `--as user`。 +2. 若 user 身份返回权限错误,先判断是否为**不可重试错误码**(如 `91403`)。若是,**立即停止**,不做任何重试或降级,直接按 `lark-shared` 权限不足处理流程引导用户解决。 +3. 非不可重试错误码时,检查错误响应中是否包含 `permission_violations` / `hint` 等提权引导信息: + - **有提权引导**:按 `lark-shared` 权限不足处理流程,先引导用户完成 user 身份提权(`auth login --scope`);确认提权成功后,以 `--as user` 重试。 + - **无提权引导**(如资源级无访问权限、非 scope 不足):切换到 `--as bot` 重试**一次**。 +4. 若 bot 身份仍然返回权限错误,**立即停止重试**,根据错误响应按 `lark-shared` 流程引导用户解决(引导去开发者后台开通 scope 或确认资源访问权限)。 +5. 只有在用户明确要求"用应用身份 / bot 身份操作",才跳过 user 直接使用 `--as bot`。 + +## 4. 执行规则 + +### 4.1 标准执行顺序 + +1. 先判断任务属于哪个模块,选对命令族。 +2. 如果用户给了链接,先解析 token,不要把 wiki token、完整 URL 或其他对象 ID 误当成 `base_token`。 +3. 先拿结构,再写命令,避免猜表名、字段名、表达式引用。 +4. 定位到命令后,先读对应 reference,再执行命令。 +5. 执行命令,并按返回结果判断下一步。 +6. 回复时返回关键结果和后续可继续操作的信息,方便 agent 链式执行下一步。 + +### 4.2 不可违反规则 + +1. 先拿结构,再写命令;至少先拿当前表结构,跨表时还要拿目标表结构。 +2. 不要猜表名、字段名、表达式引用,一律以真实返回为准。 +3. 只使用原子命令;不要回退到旧的聚合式 `+table / +field / +record / +view / +history / +workspace`。 +4. 写记录前先读字段结构;先 `+field-list`,再按 [`lark-base-cell-value.md`](references/lark-base-cell-value.md) 构造 CellValue。 +5. 写字段前先看字段属性规范;先读 `lark-base-shortcut-field-properties.md`,再构造 `+field-create / +field-update` 的 JSON。 +6. 只写可写字段;系统字段、附件字段、`formula`、`lookup` 默认不作为普通记录写入目标。 +7. 聚合分析与取数分流;统计走 `+data-query`,关键词检索走 `+record-search`,明细走 `+record-list / +record-get`。 +8. 筛选查询按视图能力执行;先用 `+view-set-filter` 配置筛选,再结合 `+record-list` 读取。 +9. Base 场景不要改走裸 API,不要切去 `lark-cli api /open-apis/bitable/v1/...`。 +10. 统一使用 `--base-token`。 +11. workflow 场景先读 schema,不要凭自然语言猜 `type`。 +12. dashboard 场景先读 guide;提到图表、看板、block 就先进入 dashboard 模块。 +13. formula / lookup 场景先读 guide;没读 guide 前不要直接创建或更新。 + +### 4.3 并发、分页与批量限制 + +- `+table-list / +field-list / +record-list / +view-list / +record-history-list / +role-list / +dashboard-list / +dashboard-block-list / +workflow-list` 禁止并发调用,只能串行执行。 +- `+record-list` 分页时,`--limit` 最大 `200`;先拉首批并检查 `has_more`,只有用户明确需要更多数据时再继续翻页。 +- 批量写入时,单批不超过 `200` 条。 +- 连续写入同一表时,必须串行写入,批次间延迟 `0.5–1` 秒。 + +### 4.4 确认与回复规则 + +- 视图重命名时,用户已明确“把哪个视图改成什么名字”时,`+view-rename` 直接执行即可。 +- 删除记录 / 字段 / 表时,如果用户已经明确说要删除,且目标明确,`+record-delete / +field-delete / +table-delete` 可直接执行,并带 `--yes`。 +- 删除目标仍有歧义时,先用 `+record-get / +field-get / +table-get` 或相应 list 命令确认。 +- `+base-create / +base-copy` 成功后,回复中必须主动返回新 Base 的标识信息;若结果带可访问链接,也应一并返回。 +- 若 Base 由 bot 身份创建或复制,shortcut 会自动尝试为当前 CLI 用户补授 `full_access`,并在输出中返回 `permission_grant`;agent 不需要再手动编排单独授权。owner 转移必须单独确认,禁止擅自执行。 + +## 5. 常见错误与恢复 + +| 错误 / 现象 | 含义 | 恢复动作 | +|-------------|------|----------| +| `1254064` | 日期格式错误 | 传 `YYYY-MM-DD HH:mm:ss` 字符串,不要写相对时间 | +| `1254068` | 超链接格式错误 | `"https://example.com"` 或 `"[文本](https://example.com)"` | +| `1254066` | 人员字段错误 | `[{ "id": "ou_xxx" }]` | +| `1254045` | 字段名不存在 | 检查字段名(含空格、大小写) | +| `1254015` | 字段值类型不匹配 | 先 `+field-list`,再按类型构造 | +| `param baseToken is invalid` / `base_token invalid` | 把 wiki token、workspace token 或其他 token 当成了 `base_token` | 如果输入来自 `/wiki/...`,先用 `lark-cli wiki spaces get_node` 取真实 `obj_token`;当 `obj_type=bitable` 时,用 `node.obj_token` 作为 `--base-token` 重试,不要改走 `bitable/v1` | +| `not found` 且用户给的是 wiki 链接 | 常见于把 wiki token 当成 base token | 优先回退检查 wiki 解析,而不是改走 `bitable/v1` | +| formula / lookup 创建失败 | 指南未读或结构不合法 | 先读 `formula-field-guide.md` / `lookup-field-guide.md`,再按 guide 重建请求 | +| `ignored_fields` / `READONLY` | 只读字段被当成可写字段,常见于系统字段、formula、lookup | 移除只读字段,只写存储字段;计算结果交给 formula / lookup / 系统字段自动产出 | +| `1254104` | 批量超 200 条 | 分批调用 | +| `1254291` | 并发写冲突 | 串行写入 + 批次间延迟 | +| `91403` | 无权限访问该 Base | **不要重试**。按 `lark-shared` 权限不足处理流程引导用户解决权限问题 | diff --git a/.agents/skills/lark-base/references/dashboard-block-data-config.md b/.agents/skills/lark-base/references/dashboard-block-data-config.md new file mode 100644 index 0000000..e5f400f --- /dev/null +++ b/.agents/skills/lark-base/references/dashboard-block-data-config.md @@ -0,0 +1,350 @@ +# dashboard block data_config 参考 + +Block 的 `data_config` 字段因 `type` 不同而变化。本文档描述所有共享结构。 + +## 支持的组件类型(`type` 枚举) + +| type 值 | 说明 | +|---------|------| +| `column` | 柱状图 | +| `bar` | 条形图 | +| `line` | 折线图 | +| `pie` | 饼图 | +| `ring` | 环形图 | +| `area` | 面积图 | +| `combo` | 组合图 | +| `scatter` | 散点图 | +| `funnel` | 漏斗图 | +| `wordCloud` | 词云 | +| `radar` | 雷达图 | +| `statistics` | 指标卡 | +| `text` | 文本(支持 Markdown) | + +## 字段类型与操作符速查(AI 决策用) + +> 先用 `+field-list` / `+field-get` 确认字段 `type`;本节使用当前字段接口里的 canonical 类型名:`number`、`text`、`select`、`datetime`、`checkbox`、`user`。 + +``` +text: is, isNot, contains, doesNotContain, isEmpty, isNotEmpty +number: is, isNot, isGreater, isGreaterEqual, isLess, isLessEqual, isEmpty, isNotEmpty +select(multiple=false): is, isNot, isEmpty, isNotEmpty +select(multiple=true): is, isNot, contains, doesNotContain, isEmpty, isNotEmpty +datetime: is, isGreater, isGreaterEqual, isLess, isLessEqual, isEmpty, isNotEmpty +checkbox: is (value: true/false) +user / created_by / updated_by: is, isNot, isEmpty, isNotEmpty +``` + +## data_config 通用结构 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `table_name` | string | 关联数据表名称 | +| `series` | `[{ "field_name": "xxx", "rollup": "SUM" }]` | 指标/Y 轴(与 `count_all` 二选一)。rollup 支持 `SUM` / `MAX` / `MIN` / `AVERAGE` | +| `count_all` | boolean | COUNTA 聚合,统计所有记录数(与 `series` 二选一) | +| `group_by` | `[{ "field_name": "xxx", "mode": "integrated", "sort": {...} }]` | X 轴分组维度。`mode` 必填,`sort` 可选,见下方说明 | +| `filter` | object | 筛选条件 | +| `filter.conjunction` | `"and"` / `"or"` | 筛选逻辑 | +| `filter.conditions` | `[{ "field_name", "operator", "value" }]` | 筛选条件数组,value 类型因字段类型而异(见下方 filter 格式规则) | + +### text 类型特殊结构 + +`text` 类型组件用于展示富文本内容,**不需要数据源配置**(无 `table_name`、`series`、`group_by`、`filter`)。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `text` | string | **必填**。支持 Markdown 语法,详见下方说明 | + +**支持的 Markdown 语法:** + +| 语法 | 示例 | 效果 | +|------|------|------| +| 一级标题 | `# 标题` | 大标题 | +| 二级标题 | `## 标题` | 中标题 | +| 三级标题 | `### 标题` | 小标题 | +| 加粗 | `**文字**` | **文字** | +| 斜体 | `*文字*` | *文字* | +| 删除线 | `~~文字~~` | ~~文字~~ | +| 有序列表 | `1. 项目` | 1. 项目 | +| 无序列表 | `- 项目` | - 项目 | + +> **注意**:以上未提及的 Markdown 语法(如链接、图片、代码块、表格等)均不支持。 + +## group_by 详细说明 + +### mode 枚举 + +| mode | 含义 | 适用场景 | +|------|------|----------| +| `integrated` | 聚合分组(默认) | 绝大部分场景,按字段值分组统计 | +| `enumerated` | 多值拆分统计 | 多选、人员等多值字段,将每个选项/人员拆开独立统计 | + +> 多选、人员等多值字段默认用 `enumerated`;其他字段默认用 `integrated`。 + +### sort 排序 + +| sort.type | 含义 | 典型场景 | +|-----------|------|----------| +| `group` | 按横轴值排序 | 按月份升序、按品类名字母序 | +| `value` | 按纵轴值排序 | 按销售额从大到小 | +| `view` | 按数据源记录顺序 | 保持原表行序(不常用) | + +`sort.order`:`asc`(升序)/ `desc`(降序) + +示例 — 柱状图按销售额降序: + +```json +{ + "table_name": "订单表", + "series": [{ "field_name": "金额", "rollup": "SUM" }], + "group_by": [{ "field_name": "类别", "mode": "integrated", "sort": {"type": "value", "order": "desc"} }] +} +``` + +## filter 格式规则 + +**基本结构:** + +```json +{ + "filter": { + "conjunction": "and", + "conditions": [ + { "field_name": "字段名", "operator": "操作符", "value": "值" } + ] + } +} +``` + +**多条件示例(and/or):** + +```json +{ + "filter": { + "conjunction": "and", + "conditions": [ + { "field_name": "状态", "operator": "is", "value": "已完成" }, + { "field_name": "金额", "operator": "isGreater", "value": 1000 } + ] + } +} +``` + +**操作符:** + +| 操作符 | 含义 | 是否需要 value | +|--------|------|---------------| +| `is` | 等于 | 是 | +| `isNot` | 不等于 | 是 | +| `contains` | 包含 | 是 | +| `doesNotContain` | 不包含 | 是 | +| `isEmpty` | 为空 | 否 | +| `isNotEmpty` | 不为空 | 否 | +| `isGreater` | 大于 | 是 | +| `isGreaterEqual` | 大于等于 | 是 | +| `isLess` | 小于 | 是 | +| `isLessEqual` | 小于等于 | 是 | + +**各字段类型的 value 格式:** + +| 字段类型 | value 类型 | 适用操作符 | 示例 | +|----------|-----------|-----------|------| +| `text` | string | is, isNot, contains, doesNotContain, isEmpty, isNotEmpty | `{"field_name":"姓名","operator":"contains","value":"张"}` | +| `number` | number | is, isNot, isGreater, isGreaterEqual, isLess, isLessEqual, isEmpty, isNotEmpty | `{"field_name":"金额","operator":"isGreater","value":0}` | +| `select` (`multiple=false`) | string(选项名) | is, isNot, isEmpty, isNotEmpty | `{"field_name":"状态","operator":"is","value":"已完成"}` | +| `select` (`multiple=true`) | string[](选多个)/ string(选单个) | is, isNot, contains, doesNotContain, isEmpty, isNotEmpty | 多选传数组如 `["标签1","标签2"]`;单选传单个字符串 | +| `datetime` / `created_at` / `updated_at` | number(Unix 毫秒时间戳,13位) | is, isGreater, isGreaterEqual, isLess, isLessEqual, isEmpty, isNotEmpty | `{"field_name":"创建日期","operator":"isGreater","value":1704038400000}` | +| `checkbox` | boolean | is | `{"field_name":"已审核","operator":"is","value":true}` | +| `user` / `created_by` / `updated_by` | string 或 string[](用户 ID,格式 `ou_xxx`)。不知道 `open_id` 时先用 `lark-cli contact +search-user --query "<姓名/邮箱/手机号>" --as user` 查 id。 | is, isNot, isEmpty, isNotEmpty | `{"field_name":"负责人","operator":"is","value":"ou_xxxxxxxxxxxxxxxx"}` | +| 所有类型(为空/不为空) | 不需要 value | isEmpty, isNotEmpty | `{"field_name":"备注","operator":"isEmpty"}` | + +> `value` 类型为 `string | number | boolean | string[]`,需根据字段类型匹配正确格式 + +## 约束与本地校验 + +- 必填与互斥 + - 图表类型必填:`table_name` + - text 类型必填:`text` + - 互斥:`series` 与 `count_all` 二选一,且至少提供其一(仅图表类型) + - text 类型**不支持**:`series`、`count_all`、`group_by`、`filter` +- 长度/结构 + - `group_by` 最多 2 个;每项 `field_name` 必填 + - `group_by[].sort.type` 取值 `group|value|view`;`order` 取值 `asc|desc` +- 规范化(CLI 自动处理) + - `series[].rollup` 自动转成大写(如 `sum` → `SUM`) + - `group_by[].sort.type/order` 自动转成小写 +- 本地校验(可通过 `--no-validate` 跳过) + - `+dashboard-block-create` 默认对 `data_config` 做轻量校验;失败会聚合错误并给出修复建议 + - `+dashboard-block-update` 不做强类型校验,由后端验证具体字段 + - 仅需传入合法 JSON;CLI 不会擅自改写你的业务含义 + +## 可复制模板 + +**按意图选择模板:** +- 比较不同类别数值 → 柱状图 / 条形图 +- 看趋势变化 → 折线图 / 面积图 +- 看占比分布 → 饼图 / 环形图 / 词云 +- 多指标对比 → 组合图 +- 看两变量关系 → 散点图 +- 看流程转化 → 漏斗图 +- 看多维度评分 → 雷达图 +- 显示单个指标 → 指标卡(统计数字或记录数) + +最小柱状图: + +```json +{ + "table_name": "表名", + "series": [{ "field_name": "数值字段", "rollup": "SUM" }], + "group_by": [{ "field_name": "分组字段", "mode": "integrated" }] +} +``` + +最小饼图/环形图(按分类字段统计行数占比): + +```json +{ + "table_name": "表名", + "count_all": true, + "group_by": [{ "field_name": "分类字段", "mode": "integrated" }] +} +``` + +折线图(按月趋势): + +```json +{ + "table_name": "表名", + "series": [{ "field_name": "金额", "rollup": "SUM" }], + "group_by": [{ "field_name": "月份", "mode": "integrated", "sort": {"type":"group","order":"asc"} }] +} +``` + +条形图(横向柱状图): + +```json +{ + "table_name": "表名", + "series": [{ "field_name": "数值字段", "rollup": "SUM" }], + "group_by": [{ "field_name": "分组字段", "mode": "integrated" }] +} +``` + +面积图(趋势填充): + +```json +{ + "table_name": "表名", + "series": [{ "field_name": "数值字段", "rollup": "SUM" }], + "group_by": [{ "field_name": "时间字段", "mode": "integrated", "sort": {"type":"group","order":"asc"} }] +} +``` + +组合图(柱+线等多指标对比): + +```json +{ + "table_name": "表名", + "series": [ + { "field_name": "指标1", "rollup": "SUM" }, + { "field_name": "指标2", "rollup": "SUM" } + ], + "group_by": [{ "field_name": "分类字段", "mode": "integrated" }] +} +``` + +散点图(两变量相关性): + +```json +{ + "table_name": "表名", + "series": [{ "field_name": "Y轴字段(数值/指标)", "rollup": "SUM" }], + "group_by": [{ "field_name": "X轴字段(分类/维度)", "mode": "integrated" }] +} +``` + +漏斗图(流程转化): + +```json +{ + "table_name": "表名", + "series": [{ "field_name": "数值字段", "rollup": "SUM" }], + "group_by": [{ "field_name": "状态字段", "mode": "integrated" }] +} +``` + +词云(文本频率): + +```json +{ + "table_name": "表名", + "count_all": true, + "group_by": [{ "field_name": "文本字段", "mode": "integrated" }] +} +``` + +雷达图(多维度评分): + +```json +{ + "table_name": "表名", + "series": [ + { "field_name": "维度1", "rollup": "SUM" }, + { "field_name": "维度2", "rollup": "SUM" }, + { "field_name": "维度3", "rollup": "SUM" } + ], + "group_by": [{ "field_name": "分类字段", "mode": "integrated" }] +} +``` + +指标卡(统计数字): + +```json +{ + "table_name": "数据表", + "series": [{ "field_name": "数字", "rollup": "SUM" }] +} +``` + +指标卡(统计记录数): + +```json +{ + "table_name": "数据表", + "count_all": true +} +``` + +文本组件(Markdown 富文本): + +```json +{ + "text": "# 🚀 一级标题\n这是一个 **加粗** *斜体* ~~删除线~~ 的示例。\n\n## 📌 二级标题\n1. 有序列表项 1\n2. 有序列表项 2\n\n### 📌 三级标题\n- 无序列表项 1\n- 无序列表项 2" +} +``` + +> **注意**:text 类型组件不需要 `table_name`、`series`、`group_by`、`filter` 等数据源相关字段。 + +## 常见错误与修复 + +- 同时存在 `series` 与 `count_all` + - 现象:后端/本地校验报互斥错误 + - 修复:见「关键约束」章节的二选一规则 +- 缺少 `table_name` + - 现象:本地校验缺少必填字段 + - 修复:指定数据源表名(使用表名,非表 ID) +- `series[].rollup` 大小写/取值不合法 + - 现象:本地校验提示枚举不支持 + - 修复:改为 `SUM|MAX|MIN|AVERAGE` 中之一(不区分大小写,CLI 会统一为大写;计数请使用 `count_all:true`) +- `group_by` 超出 2 个或字段名为空 + - 修复:保留前 2 个,或补齐 `field_name` +- 排序枚举不合法 + - 修复:`group_by.sort.type` 仅能为 `group|value|view`;`order` 为 `asc|desc` +- filter 写法不规范 + - 修复:`conjunction` 取 `and|or`;`conditions[].operator` 必须在本页表格列举的范围内;除 `isEmpty/isNotEmpty` 外需提供 `value` + +## 坑点 + +- **`count_all` 与 `series` 二选一** — 两者不能同时使用 +- **filter `value` 类型因字段而异** — 文本/单选为 string,数字为 number,日期为毫秒时间戳,多选/人员可为 string[],复选框为 boolean;`isEmpty`/`isNotEmpty` 不需要 value +- **`data_config` 结构随 `type` 变化** — 不同组件类型的字段不同,创建前务必确认类型对应的字段 +- **表名用 name,不是 ID** — `table_name` 对应的是表名称(如「订单表」),不是 `table_id` diff --git a/.agents/skills/lark-base/references/examples.md b/.agents/skills/lark-base/references/examples.md new file mode 100644 index 0000000..f98e956 --- /dev/null +++ b/.agents/skills/lark-base/references/examples.md @@ -0,0 +1,140 @@ +# 飞书多维表格使用场景完整示例(base) + +本文档提供基于 `lark-cli base +...` shortcut 的完整示例。 + +> **返回**: [SKILL.md](../SKILL.md) | **参考**: [shortcut 字段 JSON 规范](lark-base-shortcut-field-properties.md) · [CellValue 规范](lark-base-cell-value.md) + +--- + +## 场景 1:用 unified Shortcut 快速建表 + +适合已经明确字段结构、希望一次性完成建表的场景。 + +```bash +lark-cli base +table-create \ + --base-token bascnXXXXXXXX \ + --name "客户管理表" \ + --fields '[ + {"name":"客户名称","type":"text","description":"主标题字段"}, + {"name":"负责人","type":"user","multiple":false,"description":"用于标记客户跟进的直接负责人"}, + {"name":"签约日期","type":"datetime"}, + {"name":"状态","type":"select","multiple":false,"options":[{"name":"进行中"},{"name":"已完成"}]} + ]' +``` + +--- + +## 场景 2:创建数据表并查看字段 + +适合需要先建表、再确认字段结构的场景。 + +### 步骤 1:在已有 Base 中创建数据表 + +```bash +lark-cli base +table-create \ + --base-token bascnXXXXXXXX \ + --name "客户管理表" +``` + +### 步骤 2:列出字段 + +```bash +lark-cli base +field-list \ + --base-token bascnXXXXXXXX \ + --table-id tblXXXXXXXX \ + --limit 100 +``` + +> 提示:Base token 统一通过 `--base-token` 传入;表 ID 统一通过 `--table-id` 传入。 + +--- + +## 场景 3:创建、读取、更新单条记录 + +### 新增记录 + +```bash +lark-cli base +record-upsert \ + --base-token bascnXXXXXXXX \ + --table-id tblXXXXXXXX \ + --json '{ + "客户名称":"字节跳动", + "负责人":[{"id":"ou_xxx"}], + "状态":"进行中" + }' +``` + +### 列出记录 + +```bash +lark-cli base +record-list \ + --base-token bascnXXXXXXXX \ + --table-id tblXXXXXXXX \ + --limit 100 +``` + +### 更新记录 + +```bash +lark-cli base +record-upsert \ + --base-token bascnXXXXXXXX \ + --table-id tblXXXXXXXX \ + --record-id recXXXXXXXX \ + --json '{ + "状态":"已完成" + }' +``` + +### 删除记录 + +```bash +lark-cli base +record-delete \ + --base-token bascnXXXXXXXX \ + --table-id tblXXXXXXXX \ + --record-id recXXXXXXXX \ + --yes +``` + +--- + +## 场景 4:配置视图筛选后按视图读取记录 + +需要筛选查询时,推荐先写视图筛选,再通过 `view_id` 读取记录。 + +### 更新视图筛选条件 + +```bash +lark-cli base +view-set-filter \ + --base-token bascnXXXXXXXX \ + --table-id tblXXXXXXXX \ + --view-id vewXXXXXXXX \ + --json '{ + "logic":"and", + "conditions":[ + { + "field_name":"状态", + "operator":"is", + "value":["进行中"] + } + ] + }' +``` + +### 按视图读取记录 + +```bash +lark-cli base +record-list \ + --base-token bascnXXXXXXXX \ + --table-id tblXXXXXXXX \ + --view-id vewXXXXXXXX \ + --limit 100 +``` + +--- + +## 场景 5:什么时候优先用 Shortcut + +- 需要一次性建表并附带字段、视图时,优先 `lark-cli base +table-create` +- 需要按业务字段名做 upsert 时,优先 `lark-cli base +record-upsert` +- 需要配置筛选视图时,优先 `lark-cli base +view-set-filter` +- 需要记录历史时,优先 `lark-cli base +record-history-list` diff --git a/.agents/skills/lark-base/references/formula-field-guide.md b/.agents/skills/lark-base/references/formula-field-guide.md new file mode 100644 index 0000000..df89fdb --- /dev/null +++ b/.agents/skills/lark-base/references/formula-field-guide.md @@ -0,0 +1,735 @@ +# Base Formula Writing Guide + +## Mandatory Read Acknowledgement + +When creating or updating a formula field with `lark-cli base +field-create/+field-update --json ...` and `type` is `formula`, you should read this guide first and only then add `--i-have-read-guide` to the command. + +Do **not** proactively add `--i-have-read-guide` before reading this guide. Without it, the CLI will fail fast and direct you back to this guide. + +## Default strategy + +**All cross-table references, aggregations, and computed fields should use Formula fields by default.** Do NOT use Lookup fields unless the user explicitly requests it. Formula is a strict superset of Lookup — anything Lookup can do, Formula can do with a single expression. + +## Usage + +When creating a formula field, the Agent should: + +1. Get all table names: `lark-cli base +table-list --base-token ` — returns `items[].table_name` +2. Get table structure: `lark-cli base +table-get --base-token --table-id ` — returns `fields[]` +3. If the formula references other tables, also get those tables' structures +4. Write the formula expression following this guide +5. Construct the Formula field JSON and submit it to create or update the field + +**Key constraints**: + +- The JSON must include `"type": "formula"` — this field is required +- Table names and field names in the formula must **exactly match** those returned by `+table-list` / `+table-get` +- The `expression` value is a string containing the formula expression; double quotes inside the expression must be properly escaped in JSON (e.g. `\"text\"`) + +--- + +## Section 1: Core Concepts — Scalar vs List + +This is the foundation of formula logic. You must determine this before writing any formula. + +| Syntax | Meaning | Return type | Example | +| --------------------- | -------------------------------------------- | ---------------------- | -------------------------------------------- | +| `[Field]` | Value of this field in the current row | Scalar (single value) | `[Name]` → `"Alice"` | +| `[TableName].[Field]` | All values of this field in the target table | List (multiple values) | `[Employees].[Name]` → `["Alice","Bob",...]` | +| `[TableName]` | The target table (entire table) | Table reference | Used as data range for FILTER/COUNTIF etc. | + +**Rules**: + +- Scalars can be used directly in operations: `[Price] * [Quantity]` +- Lists cannot be used as scalars — they must be processed first: use `SUM()` for sum, `ARRAYJOIN(",")` for joining, `FIRST()`/`LAST()`/`NTH()` for single value extraction +- Link field access `[LinkField].[TargetField]` returns a list (values of the target field for all linked records) +- **LISTCOMBINE flattening rule**: When a FILTER's result column is itself a multi-value field (`select` with `multiple=true`, `link`, etc.), it produces a 2D array and **must** be flattened with `.LISTCOMBINE()`; for single-value fields (`number`, `text`, etc.) it can be omitted, but adding it is never wrong: + + ``` + [Table].FILTER(CurrentValue.[Field] = [Value]).[Tags].LISTCOMBINE() ← required for multi-value columns + [Table].FILTER(CurrentValue.[Field] = [Value]).[NumberCol].LISTCOMBINE() ← optional for single-value columns + ``` + +--- + +## Section 2: Data Types and Type Conversion + +### Field storage types + +| Type | Description | Supported operations | +|------|-------------|----------------------| +| `number` | Stored as numeric value | Math operations, comparisons, auto-converts to string for concatenation | +| `text` | Stored as string | String operations; can participate in math if content is numeric, otherwise errors | +| `datetime` | Date object | Date functions, add/subtract with numbers; auto-converts to default format string when using `&` — use TEXT to format first for controlled output | +| `select` (`multiple=true`) | Data list | List functions, CONTAIN checks | +| `link` | Links to other table records | Chained access `[LinkField].[Field]`, result is a list | +| `checkbox` | TRUE/FALSE | Logical operations; auto-converts to number when compared with numbers | + +### Implicit type conversion + +| Scenario | Conversion rule | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | +| Number + Float | → Float | +| Date + Number | → Date (adds/subtracts days). Use `+`/`-` for whole days, use `DURATION()` for hour/minute/second precision | +| Date - Date | → Duration | +| Boolean compared with Number | Boolean auto-converts to number (TRUE=1, FALSE=0) | +| `&` concatenation | Both sides auto-convert to string | + +### Type consistency in comparisons + +When using comparison operators (`>`, `>=`, `<`, `<=`, `=`, `!=`), **both sides should be the same type** to avoid semantic errors or unexpected results. + +**Principle**: When types differ, explicitly convert one side rather than relying on implicit conversion: + +- `number` vs `text` → use `VALUE()` to convert text to number +- `datetime` vs `text` → use `TEXT()` to convert date to text +- `datetime` vs `datetime` equality → dates include time components, so direct `=` comparison may fail due to different hours/minutes/seconds. For day-level equality, convert to text first: `TEXT([DateA], "YYYY/MM/DD") = TEXT([DateB], "YYYY/MM/DD")` +- `select` and `user` fields can be compared with both same-type values and text +- `text` fields in numeric aggregation (SUM/AVERAGE/MIN/MAX etc.) → convert to number with `VALUE()` first. For FILTER results, use `.MAP(VALUE(CurrentValue)).SUM()` + +--- + +## Section 3: CurrentValue + +**CurrentValue is the iteration variable in FILTER/MAP/COUNTIF/SUMIF functions, representing the "current item" being processed in the data range.** + +### CurrentValue meaning in different contexts + +| Data range type | CurrentValue represents | Access pattern | Example | +| ---------------------------- | ----------------------- | --------------------------- | --------------------------------------------------------- | +| Entire table `[TableName]` | A row in the table | `CurrentValue.[FieldName]` | `[Orders].FILTER(CurrentValue.[Amount] > 100).[Customer]` | +| Column `[TableName].[Field]` | A single field value | Use `CurrentValue` directly | `[Orders].[Amount].FILTER(CurrentValue > 100)` | +| `select` (`multiple=true`) field `[Tags]` | One option | Use `CurrentValue` directly | `[Tags].FILTER(CurrentValue = "Important")` | +| LIST-generated list | One element | Use `CurrentValue` directly | `LIST(1,2,3).MAP(CurrentValue * 2)` | + +### Key rules + +1. **When data range is a table**, use `CurrentValue.[FieldName]` to access row fields +2. **When data range is a column/list**, use `CurrentValue` directly for the element value — **cannot** use `CurrentValue.[FieldName]` +3. CurrentValue can **only** appear inside the condition/mapping parameters of FILTER/MAP/COUNTIF/SUMIF functions +4. To reference the current table's field value in a condition, write `[FieldName]` directly — it refers to the formula row's value, not a property of CurrentValue + +### Anti-patterns + +| Wrong | Reason | Correct | +| ---------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------ | +| `[Table].[Col].FILTER(CurrentValue.[Col] > 0)` | Data range is a column; CurrentValue is a scalar, cannot use `.` to access fields | `[Table].[Col].FILTER(CurrentValue > 0)` | +| `[Table].FILTER(CurrentValue > 100)` | Data range is a table; CurrentValue is a row, cannot compare directly | `[Table].FILTER(CurrentValue.[Amount] > 100).[Amount]` | +| `CurrentValue + 1` (at top level) | CurrentValue can only be used inside iteration functions | Use inside MAP/FILTER etc. | + +--- + +## Section 4: Operators + +Base formulas **only allow** the following operators. `like`, `in`, `<>`, `**`, `^` etc. are prohibited. + +| Category | Operators | Description | +| ------------- | -------------------------- | -------------------------------------------------------------------------- | +| Arithmetic | `+` `-` `*` `/` `%` | Add, subtract, multiply, divide, modulo (`%` is equivalent to `MOD()`) | +| Comparison | `>` `>=` `<` `<=` `=` `!=` | Greater than, greater or equal, less than, less or equal, equal, not equal | +| Logical | `&&` `\|\|` | AND, OR | +| Concatenation | `&` | Text concatenation; non-text values auto-convert to string | + +**Important**: + +- Equality uses `=` (single equals), not `==` +- Not-equal uses `!=`, not `<>` +- String concatenation uses `&`, not `+` +- Both `&&`/`||` and AND()/OR() functions are supported + +--- + +## Section 5: Link Fields and Cross-Table References + +### Link field description + +When a field type is described as `FieldName: Link [target table: X, foreign key: Y]`, it links to target table X using field Y as the join key. + +### Chained cross-table access + +``` +[LinkField].[TargetField] +``` + +Retrieves the target field values for all linked records as a list. Supports continued chaining: `[LinkA].[LinkB].[Field]`. + +### Equivalent expanded form + +- Multi-value link: `[TargetTableX].FILTER([LinkField].CONTAIN(CurrentValue.[Y])).[TargetField].LISTCOMBINE()` +- Single-value link: `[TargetTableX].FILTER(CurrentValue.[Y] = [LinkField]).[TargetField].LISTCOMBINE()` + +(`.LISTCOMBINE()` is required when `[TargetField]` is a multi-value field; optional for single-value fields) + +### Notes + +- Link fields typically return **lists** (possibly empty) +- To output a single value, use aggregation (SUM/MAX), joining (ARRAYJOIN), or extraction (FIRST/LAST/NTH) +- Do not nest FILTER inside FILTER for cross-table queries — prefer link field chained access + +--- + +## Section 6: Function Call Conventions + +### Two calling styles + +| Style | Format | Description | +| ---------- | ------------------ | ----------------------------------- | +| Functional | `FUNC(arg1, arg2)` | Works for all functions | +| Chained | `arg1.FUNC(arg2)` | Moves the first argument before `.` | + +**Rules**: + +- Zero-argument functions cannot be chained: `NOW()`, `TODAY()`, `PI()`, `TRUE()`, `FALSE()` +- SORTBY can **only** be chained: `[Table].SORTBY([Table].[SortCol]).[OutputCol]`. The sort column always uses the original table's column name (`[TableName].[Field]` format); the engine aligns rows internally, even when the data range is a FILTER result +- FILTER is recommended to be chained: `[Table].FILTER(condition).[OutputCol]` + +### FILTER / SORTBY result column rules + +- **When data range is a table** `[TableName]`, FILTER / SORTBY returns a table reference. The chain **must** end with `.[Field]` to specify the result column, otherwise the formula fails: + + ``` + Correct: [Sales].FILTER(CurrentValue.[Amount] > 100).[Customer] + Correct: [Sales].FILTER(condition).SORTBY([Sales].[SortCol]).[Customer] ← result column at end of chain + Wrong: [Sales].FILTER(CurrentValue.[Amount] > 100) ← missing result column + ``` + +- **When data range is a column** `[TableName].[Field]` or a list, FILTER returns the filtered list directly — **no** result column needed: + + ``` + Correct: [Sales].[Amount].FILTER(CurrentValue > 100) + ``` + +After the result column, it's recommended to flatten with `.LISTCOMBINE()` first (especially when the result column is a multi-value field), then chain aggregation functions: + +``` +[Sales].FILTER(CurrentValue.[Amount] > 100).[Amount].LISTCOMBINE().SUM() +``` + +--- + +## Section 7: Hard Constraints + +1. **Nesting prohibition**: FILTER / SUMIF / COUNTIF / MAP **must not be nested** inside each other's condition/mapping expressions. None of these functions can appear inside the condition or mapping parameter of another. + - Prohibited: `[Table1].FILTER(CurrentValue.[Col] = [Table2].FILTER(...).[Col])` ← FILTER inside FILTER condition + - Prohibited: `[Table].MAP([Table2].MAP(...))` ← MAP inside MAP mapping + - **Allowed**: `[Table].FILTER(cond1).[Col].FILTER(cond2)` ← chained call; the first FILTER's output is the second's data range, not nesting + +2. **Function whitelist**: Only use functions listed in Section 8. No unlisted functions. + +3. **Exact name matching**: Table names and field names in formulas must **exactly match** those returned by `+table-get` — no renaming or adding spaces. + +4. **Operator whitelist**: Only use operators listed in Section 4. + +5. **Strings use double quotes**: Strings must be wrapped in double quotes `"`, single quotes are not supported. + +6. **Do not use LOOKUP**: FILTER is a superset of LOOKUP. All LOOKUP formulas can be rewritten with FILTER. Use FILTER exclusively to reduce complexity. + +--- + +## Section 8: Complete Function Reference + +### 8.1 Logic functions + +| Function | Signature | Return type | Description | +| ------------- | ------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------- | +| IF | `IF(condition, true_val, [false_val])` | Matches branch type | Returns true_val when TRUE, false_val otherwise; omitting false_val returns false (not null) | +| IFS | `IFS(cond1, val1, cond2, val2, ...)` | Matches branch type | Multi-condition branching; returns value for the first TRUE condition | +| SWITCH | `SWITCH(expr, match1, result1, [match2, result2, ...], [default])` | Matches branch type | Matches expression value and returns corresponding result | +| IFERROR | `IFERROR(expr, fallback)` | Matches branch type | Returns fallback when expression errors | +| IFBLANK | `IFBLANK(expr, fallback)` | Matches branch type | Returns fallback when expression is blank (blank = NULL/empty string/empty list) | +| AND | `AND(cond1, cond2, ...)` | Boolean | TRUE when all conditions are TRUE | +| OR | `OR(cond1, cond2, ...)` | Boolean | TRUE when any condition is TRUE | +| NOT | `NOT(condition)` | Boolean | Logical negation | +| ISBLANK | `ISBLANK(value)` | Boolean | Tests if blank (NULL/empty string/empty list are blank; 0 and FALSE are not) | +| ISNULL | `ISNULL(value)` | Boolean | Tests if NULL (only NULL is true; empty string is not) | +| ISERROR | `ISERROR(expr)` | Boolean | Tests if expression errors | +| ISNUMBER | `ISNUMBER(value)` | Boolean | Tests if value is a number | +| CONTAIN | `CONTAIN(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains the value; **does NOT do text substring matching** | +| CONTAINSALL | `CONTAINSALL(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains all specified values | +| CONTAINSONLY | `CONTAINSONLY(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains only the specified values | +| TRUE | `TRUE()` | Boolean | Returns TRUE | +| FALSE | `FALSE()` | Boolean | Returns FALSE | +| RECORD_ID | `RECORD_ID()` | Text | Returns the current row's record ID | +| RANDOMBETWEEN | `RANDOMBETWEEN(min_int, max_int, [keep_updating])` | Number | Random integer in the specified range | +| RANDOMITEM | `RANDOMITEM(list, [keep_updating])` | Matches element type | Randomly picks one element from a list | + +### 8.2 Numeric functions + +| Function | Signature | Return type | Description | +| --- | --- | --- | --- | +| SUM | `SUM(val1, val2, ...)` | Number | Sum; accepts multiple values or a list | +| AVERAGE | `AVERAGE(val1, val2, ...)` | Number | Average | +| MAX | `MAX(val1, val2, ...)` | Number | Maximum | +| MIN | `MIN(val1, val2, ...)` | Number | Minimum | +| MEDIAN | `MEDIAN(val1, val2, ...)` | Number | Median | +| COUNTA | `COUNTA(val1, val2, ...)` | Number | Count of non-blank values | +| COUNTIF | `COUNTIF(data_range, condition)` | Number | Count matching items. Data range can be a **table** (CurrentValue is a row, use `CurrentValue.[Field]`) or a **column** (CurrentValue is a scalar value) | +| SUMIF | `SUMIF(data_range, condition)` | Number | Sum matching values. Data range **must be a numeric column** (e.g. `[Table].[NumField]`); CurrentValue is each value in that column (scalar), cannot use `CurrentValue.[Field]` to access other fields. For cross-field conditions, use FILTER+SUM instead | +| ROUND | `ROUND(number, digits)` | Number | Round. digits: 1=one decimal, 0=integer, -1=tens place | +| ROUNDUP | `ROUNDUP(number, digits)` | Number | Round away from zero. Same digits semantics as ROUND | +| ROUNDDOWN | `ROUNDDOWN(number, digits)` | Number | Round toward zero. Same digits semantics as ROUND | +| FLOOR | `FLOOR(number, [base])` | Number | Round down to nearest multiple of base (default 1) | +| CEILING | `CEILING(number, [base])` | Number | Round up to nearest multiple of base (default 1) | +| ABS | `ABS(number)` | Number | Absolute value | +| INT | `INT(number)` | Integer | Truncate to integer | +| MOD | `MOD(dividend, divisor)` | Number | Modulo | +| POWER | `POWER(base, exponent)` | Number | Exponentiation | +| QUOTIENT | `QUOTIENT(dividend, divisor)` | Number | Integer division | +| VALUE | `VALUE(text)` | Number | Convert text to number | +| ISODD | `ISODD(number)` | Boolean | Tests if number is odd | +| RANK | `RANK(value, search_range, [ascending])` | Number | Rank of value in range; default descending | +| SEQUENCE | `SEQUENCE(start, end, [step])` | List | Generate number sequence | +| PI | `PI()` | Number | Pi constant | +| SIN/COS/TAN/ASIN/ACOS/ATAN/ATAN2/SINH/COSH/TANH/ASINH/ACOSH/ATANH | `func(radians_or_value)` | Number | Trigonometric and hyperbolic functions; arguments in radians | + +### 8.3 Text functions + +| Function | Signature | Return type | Description | +| --------------- | ---------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- | +| CONCATENATE | `CONCATENATE(text1, text2, ...)` | Text | Concatenate multiple texts; supports lists as input | +| LEN | `LEN(text)` | Number | Character count | +| LEFT | `LEFT(text, [count])` | Text | Extract from left; default 1 | +| RIGHT | `RIGHT(text, [count])` | Text | Extract from right; default 1 | +| MID | `MID(text, start, count)` | Text | Extract from middle | +| FIND | `FIND(search_val, search_range, [start])` | Number | Find substring position (case-sensitive); returns -1 if not found | +| REPLACE | `REPLACE(text, start, count, new_text)` | Text | Replace by position | +| SUBSTITUTE | `SUBSTITUTE(text, old_text, new_text, [occurrence])` | Text | Replace by content; can specify which occurrence | +| UPPER | `UPPER(text)` | Text | Convert to uppercase | +| LOWER | `LOWER(text)` | Text | Convert to lowercase | +| TRIM | `TRIM(text)` | Text | Remove leading/trailing spaces | +| TEXT | `TEXT(value, format)` | Text | Format output. Date formats: `"YYYY-MM-DD"`, `"YYYY/MM/DD hh:mm:ss"`; number formats: `"00"`, `"000.00"` | +| CONTAINTEXT | `CONTAINTEXT(text, search_text)` | Boolean | Tests if text contains substring (text substring matching) | +| SPLIT | `SPLIT(text, delimiter)` | List | Split text by delimiter | +| TODATE | `TODATE(value)` | Date | Convert date string to date type | +| CHAR | `CHAR(number)` | Text | ASCII code to character | +| FORMAT | `FORMAT(template, [val1, val2, ...])` | Text | Template string formatting; use `{1}`, `{2}` as placeholders | +| HYPERLINK | `HYPERLINK(url, [display_text])` | Hyperlink | Create a hyperlink | +| ENCODEURL | `ENCODEURL(text)` | Text | URL encode | +| REGEXMATCH | `REGEXMATCH(text, regex)` | Boolean | Regex match test | +| REGEXEXTRACT | `REGEXEXTRACT(text, regex)` | List | Extract first match's capture groups | +| REGEXEXTRACTALL | `REGEXEXTRACTALL(text, regex)` | 2D List | Extract all matches | +| REGEXREPLACE | `REGEXREPLACE(text, regex, replacement)` | Text | Regex replace | + +### 8.4 Date functions + +| Function | Signature | Return type | Description | +| ----------- | ----------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------- | +| NOW | `NOW()` | Date | Current date and time | +| TODAY | `TODAY()` | Date | Current date (midnight) | +| DATE | `DATE(year, month, day)` | Date | Construct a date | +| YEAR | `YEAR(date)` | Number | Extract year | +| MONTH | `MONTH(date)` | Number | Extract month | +| DAY | `DAY(date)` | Number | Extract day | +| HOUR | `HOUR(date)` | Number | Extract hour | +| MINUTE | `MINUTE(date)` | Number | Extract minute | +| SECOND | `SECOND(date)` | Number | Extract second | +| WEEKDAY | `WEEKDAY(date, [type])` | Number | Day of week | +| WEEKNUM | `WEEKNUM(date, [type])` | Number | Week number | +| DAYS | `DAYS(end_date, start_date)` | Number | Days between two dates (end - start), includes decimals. **Note parameter order: end date comes first** | +| DATEDIF | `DATEDIF(start_date, end_date, [unit])` | Number | Whole days/months/years between dates. Unit: `"D"`(default)/`"M"`/`"Y"`. **Start must be before end** | +| DURATION | `DURATION(days, [hours], [minutes], [seconds])` | Duration | Create a duration for date arithmetic | +| EDATE | `EDATE(date, months)` | Date | Date N months later | +| EOMONTH | `EOMONTH(date, [months])` | Date | End of month N months later; months default 0 | +| WORKDAY | `WORKDAY(start_date, days, [holidays])` | Date | Date N workdays later (skips weekends and holidays) | +| NETWORKDAYS | `NETWORKDAYS(start_date, end_date, [holidays])` | Number | Workdays between dates (inclusive) | + +### 8.5 List functions + +| Function | Signature | Return type | Description | +| --- | --- | --- | --- | +| LIST | `LIST(val1, val2, ...)` | List | Create a list | +| FIRST | `FIRST(list)` | Scalar | First element | +| LAST | `LAST(list)` | Scalar | Last element | +| NTH | `NTH(list, index)` | Scalar | Nth element (1-based) | +| FILTER | `[Table].FILTER(condition).[ResultCol]` or `[Table].[Col].FILTER(condition)` | List | Filter by condition. When data range is a table, result column is **required**; when it's a column/list, it's not needed. Use CurrentValue in conditions. Add `.LISTCOMBINE()` when result column is multi-value | +| MAP | `data_range.MAP(mapping_expr)` | List | Apply mapping to each element. Use CurrentValue in mapping | +| SORT | `SORT(list, [ascending])` | List | Sort; default ascending (TRUE) | +| SORTBY | `[Table].SORTBY([Table].[SortCol], [ascending]).[OutputCol]` | List | Sort by column then extract output column. **Chain-only, must include output column** | +| UNIQUE | `UNIQUE(list)` | List | Deduplicate | +| ARRAYJOIN | `ARRAYJOIN(list, [delimiter])` | Text | Join list elements as text; default comma-separated | +| LISTCOMBINE | `LISTCOMBINE(val1, [val2, ...])` or `list.LISTCOMBINE()` | List | Two uses: (1) merge values/lists into one list; (2) chained call to flatten 2D array (commonly used when FILTER result column is a multi-value field) | +| DISTANCE | `DISTANCE(location1, location2)` | Number | Distance between two geographic locations (km) | + +--- + +## Section 9: Commonly Confused Functions + +### CONTAIN vs CONTAINTEXT + +| | CONTAIN | CONTAINTEXT | +| ----------- | -------------------------------------------------------------- | ---------------------------------------------------------- | +| Purpose | Tests if a **list / `select` (`multiple=true`)** contains a value | Tests if **text** contains a substring | +| Example | `[Tags].CONTAIN("Urgent")` | `[Notes].CONTAINTEXT("completed")` | +| Wrong usage | `CONTAIN([Notes], "completed")` — cannot do substring matching | `CONTAINTEXT([Tags], "Urgent")` — Tags is a list, not text | + +### ISBLANK vs ISNULL + +| | ISBLANK | ISNULL | +| ----------------- | ------- | ------ | +| NULL | TRUE | TRUE | +| `""` empty string | TRUE | FALSE | +| Empty list `[]` | TRUE | FALSE | +| `0` | FALSE | FALSE | +| `FALSE` | FALSE | FALSE | + +### DAYS vs DATEDIF + +| | DAYS | DATEDIF | +| --------------- | ------------------------------------------------------------ | ----------------------------------------- | +| Parameter order | `DAYS(end, start)` — end first | `DATEDIF(start, end, unit)` — start first | +| Precision | Includes decimals (hours/minutes/seconds as fractional days) | Integer only (whole days/months/years) | +| Negative values | Returns negative when start is after end | **Errors** when start is after end | + +### SUM vs SUMIF + +| | SUM | SUMIF | +| --------- | ---------------------------------------------- | -------------------------------------------------------------- | +| Purpose | Sum all values | Sum values **matching a condition** | +| Arguments | `SUM(val1, val2, ...)` or `SUM([Table].[Col])` | `SUMIF(data_range, condition)` with CurrentValue in condition | +| Example | `SUM([Orders].[Amount])` — sum all | `SUMIF([Orders].[Amount], CurrentValue > 100)` — sum only >100 | + +### FILTER+aggregation vs COUNTIF/SUMIF + +| | FILTER+aggregation | COUNTIF/SUMIF | +| ----------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ | +| Nature | Filter then aggregate (two steps) | One-step (syntactic sugar) | +| Equivalence | `[Table].FILTER(cond).[Col].LISTCOMBINE().SUM()` | `SUMIF([Table].[Col], cond)` (only when condition involves only column values) | +| When to use | Conditions span multiple fields, or multi-step needed | Conditions only involve column values (e.g. `CurrentValue > 100`) | + +--- + +## Section 10: Decision Trees + +### Cross-table queries: which approach? + +``` +Need data from another table? +├─ Current table has a link field to the target table? +│ ├─ Yes → Use chained access: [LinkField].[TargetField] +│ │ Need aggregation? → .SUM() / .ARRAYJOIN(",") / .FIRST() +│ └─ No → Need to match by field value? +│ ├─ Field matching or complex filtering → [TargetTable].FILTER(CurrentValue.[MatchField] = [Value]).[OutputCol] +│ └─ Only counting or summing → COUNTIF([TargetTable], condition) / FILTER+SUM +``` + +### Conditional logic: IF vs IFS vs SWITCH? + +``` +Need conditional logic? +├─ Single condition → IF(condition, true_val, false_val) +├─ Multiple mutually exclusive conditions (if-elseif-else) → IFS(cond1, val1, cond2, val2, ...) +├─ Matching a value against fixed options → SWITCH(expr, option1, result1, option2, result2, ..., default) +└─ Need error handling? + ├─ Catch errors → IFERROR(expr, fallback) + └─ Catch blanks → IFBLANK(expr, fallback) +``` + +### Aggregation: which function? + +``` +Need to aggregate data? +├─ Sum/average/max/min for entire column → SUM/AVERAGE/MAX/MIN([Table].[Col]) +├─ Count non-blank → COUNTA([Table].[Col]) +├─ Conditional count → COUNTIF([Table], CurrentValue.[Field] = [Value]) +├─ Conditional sum (column-only condition) → SUMIF([Table].[Col], CurrentValue > threshold) +├─ Conditional sum (cross-field condition) → [Table].FILTER(CurrentValue.[Field]=value).[NumCol].LISTCOMBINE().SUM() +├─ Count unique → [Table].[Col].UNIQUE().COUNTA() +└─ Ranking → RANK([Value], [Table].[Col]) +``` + +--- + +## Section 11: Common Formula Patterns + +### Pattern 1: Cross-table conditional count + +Count rows in target table matching a condition: + +``` +[TargetTable].COUNTIF(CurrentValue.[MatchField] = [CurrentTableField]) +``` + +### Pattern 2: Cross-table conditional sum + +Filter target table by current row's value, then sum: + +``` +[TargetTable].FILTER(CurrentValue.[MatchField] = [CurrentTableField]).[NumCol].LISTCOMBINE().SUM() +``` + +SUMIF works when data range is a column and conditions only involve column values: + +``` +SUMIF([TargetTable].[NumCol], CurrentValue > 100) +``` + +Note: COUNTIF can use a table as data range (only counting, no specific column needed), but SUMIF's data range **must be a numeric column** (needs values to sum), so `CurrentValue` is each value in that column (scalar) — cannot use `CurrentValue.[OtherField]` to access other fields. For cross-field conditions, use FILTER with a table as data range. + +### Pattern 3: Cross-table lookup + +``` +[TargetTable].FILTER(CurrentValue.[MatchCol] = [CurrentTableField]).[ReturnCol] +``` + +### Pattern 4: Link field values + aggregation + +``` +SUM([LinkField].[NumField]) +[LinkField].[TextField].UNIQUE().ARRAYJOIN(",") +``` + +### Pattern 5: Conditional text concatenation + +``` +IF([Condition], "prefix" & [Field] & "suffix", "default text") +``` + +### Pattern 6: Date difference + +``` +DATEDIF([StartDate], [EndDate], "D") & " days" +DAYS([EndDate], [StartDate]) +``` + +### Pattern 7: List element mapping + +``` +[SelectField(which multiple=true)].MAP(CurrentValue & " tag") +SPLIT([TextField], ",").MAP(TRIM(CurrentValue)) +``` + +### Pattern 8: Cross-table with sorting + +``` +[TargetTable].SORTBY([TargetTable].[SortCol], FALSE).[OutputCol] +[TargetTable].FILTER(CurrentValue.[Field] = [Value]).SORTBY([TargetTable].[SortCol]).[OutputCol] +``` + +--- + +## Section 12: Anti-Pattern Collection + +### Mistake 1: Extra argument in MAP + +``` +Wrong: [Table].[Col].MAP([Table2].[Col], CurrentValue + 1) +Correct: [Table].[Col].MAP(CurrentValue + 1) +``` + +Reason: MAP takes only two arguments (data range + mapping expression), no "lookup range". + +### Mistake 2: Inverted FILTER syntax + +``` +Wrong: condition.[Table].FILTER() +Correct: [Table].FILTER(condition).[ResultCol] (result column required when data range is a table) +``` + +Reason: FILTER's data range comes first, condition is passed as the argument. + +### Mistake 3: Using CurrentValue.[Field] on a column range + +``` +Wrong: SUMIF([Sales].[Revenue], CurrentValue.[Salesperson] = [Name]) +Correct: [Sales].FILTER(CurrentValue.[Salesperson] = [Name]).[Revenue].LISTCOMBINE().SUM() +``` + +Reason: `SUMIF([Sales].[Revenue], ...)` uses "Revenue" column as data range. CurrentValue is each revenue value (scalar), not a row — cannot use `.` to access other fields. Use FILTER with the table as data range for cross-field conditions. + +### Mistake 4: Missing result column after FILTER + +``` +Wrong: [Sales].FILTER(CurrentValue.[Amount] > 100) +Correct: [Sales].FILTER(CurrentValue.[Amount] > 100).[Customer] +``` + +Reason: FILTER on a table returns a table reference; must specify result column with `.[Field]` at the end. + +### Mistake 5: Nested FILTER + +``` +Wrong: [Table1].FILTER(CurrentValue.[ID] = [Table2].FILTER(CurrentValue.[Status]="Done").[ID]) +Correct: [Table1].FILTER(CurrentValue.[ID] = [CurrentRowField]).[OutputCol] +``` + +Reason: FILTER/MAP/SUMIF/COUNTIF cannot be nested inside each other's conditions. Split into multiple steps or use link fields. + +### Mistake 6: SORTBY without output column + +``` +Wrong: [Table].SORTBY([Table].[Col]) +Correct: [Table].SORTBY([Table].[Col]).[OutputCol] +``` + +Reason: SORTBY must have an output column at the end; otherwise the result cannot be represented as an array. + +### Mistake 7: SORTBY sort column without table name + +``` +Wrong: [Table].SORTBY([Col]).[OutputCol] +Correct: [Table].SORTBY([Table].[Col]).[OutputCol] +``` + +Reason: SORTBY's sort column must use `[TableName].[FieldName]` format. + +### Mistake 8: Using CONTAIN for text substring matching + +``` +Wrong: CONTAIN([Notes], "urgent") +Correct: CONTAINTEXT([Notes], "urgent") +``` + +Reason: CONTAIN checks if a list or `select` (`multiple=true`) contains a whole value, not substring matching. Use CONTAINTEXT for text substrings. + +### Mistake 9: Date concatenation without formatting + +``` +Not recommended: "Deadline: " & [DateField] ← output format is uncontrolled +Recommended: "Deadline: " & TEXT([DateField], "YYYY-MM-DD") +``` + +Reason: Concatenating a date with `&` won't error, but uses the default format. Use TEXT to specify the format explicitly. + +### Mistake 10: Reversed DAYS parameter order + +``` +Wrong: DAYS([StartDate], [EndDate]) → returns negative +Correct: DAYS([EndDate], [StartDate]) → returns positive +``` + +Reason: DAYS parameter order is end date first, start date second. + +### Mistake 11: Chaining zero-argument functions + +``` +Wrong: TODAY.DAYS([Date]) +Correct: TODAY().DAYS([Date]) +``` + +Reason: NOW, TODAY, PI and other zero-argument functions must include parentheses. + +--- + +## Section 13: Complete Examples + +### Example 1: Employee sales summary + +**Table structure** (from `+table-get`): + +- Employees: EmployeeID (Text), Name (Text), Department (Text) +- Sales: ContractID (Number), SalespersonID (Text), Quantity (Number), Total (Number) + +**Current table**: Employees + +**Requirement**: For each employee, output "Sold XX orders" if they have sales records, otherwise "No sales records". + +**Formula**: + +``` +IF( + [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) >= 1, + "Sold " & [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) & " orders", + "No sales records" +) +``` + +**Field JSON**: + +```json +{ + "type": "formula", + "name": "Sales Summary", + "expression": "IF([Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) >= 1, \"Sold \" & [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) & \" orders\", \"No sales records\")" +} +``` + +**Explanation**: `[Sales].COUNTIF(...)` uses the entire Sales table as data range. CurrentValue represents each row in Sales, accessing `CurrentValue.[SalespersonID]` for that row's salesperson. `[EmployeeID]` refers to the current row in the Employees table (where the formula lives). + +### Example 2: Chained cross-table access via link fields + +**Table structure**: + +- Orders: ID (`auto_number`), OrderItems (`link` [target: OrderItems, foreign key: ID]) +- OrderItems: ID (`auto_number`), Product (`link` [target: Products, foreign key: ID]) +- Products: ID (`auto_number`), ProductName (`text`) + +**Current table**: Orders + +**Requirement**: Deduplicate and comma-join all product names from linked order items. + +**Formula**: + +``` +[OrderItems].[Product].[ProductName].UNIQUE().ARRAYJOIN(",") +``` + +**Field JSON**: + +```json +{ + "type": "formula", + "name": "Product List", + "expression": "[OrderItems].[Product].[ProductName].UNIQUE().ARRAYJOIN(\",\")" +} +``` + +**Explanation**: `[OrderItems]` gets linked order item records, `.[Product]` expands to each item's linked product, `.[ProductName]` gets all product names, `.UNIQUE()` deduplicates, `.ARRAYJOIN(",")` joins with commas. + +### Example 3: Cross-table filter + sort + +**Table structure**: + +- Projects: ProjectName (Text), Status (Text), Owner (Text) +- Tasks: TaskName (Text), Project (Text), Priority (Number), DueDate (Date) + +**Current table**: Projects + +**Requirement**: Find the highest-priority (lowest number) task name for the current project. + +**Formula**: + +``` +FIRST( + [Tasks].FILTER(CurrentValue.[Project] = [ProjectName]).SORTBY([Tasks].[Priority], TRUE).[TaskName] +) +``` + +**Field JSON**: + +```json +{ + "type": "formula", + "name": "Top Priority Task", + "expression": "FIRST([Tasks].FILTER(CurrentValue.[Project] = [ProjectName]).SORTBY([Tasks].[Priority], TRUE).[TaskName])" +} +``` + +**Explanation**: `[Tasks].FILTER(CurrentValue.[Project] = [ProjectName])` filters tasks belonging to the current project. `.SORTBY([Tasks].[Priority], TRUE)` sorts by priority ascending. `.[TaskName]` extracts task names. `FIRST(...)` gets the first one (highest priority). + +--- + +## Section 14: Translating User Requirements to Formulas + +When the user describes their formula need in natural language, follow these rules to convert it into a precise expression: + +1. **Numbers must use precise values**: "less than 80%" → field value less than `0.8`. "above 1000" → `>= 1000`. +2. **Interval boundaries**: "above/below/within" = closed (inclusive); "less than/more than/outside" = open (exclusive). +3. **Branching logic** must be organized as an ordered list with a fallback branch. Each branch has a condition and output. + - Example: "return risk level for 1-3" → `IFS([Value] = 1, "low", [Value] = 2, "medium", [Value] = 3, "high")` with an `IFERROR` or trailing empty-string fallback. +4. **Multi-level branches must be flattened** to a single level. Nested if-else chains → flat IFS. +5. **Branch conditions must be mutually exclusive**. If the user's conditions overlap, rewrite to eliminate ambiguity. +6. **Reorder branches by logical priority** if the user's order is illogical (e.g., check specific conditions before catch-all). + +--- + +## Section 15: Constraint Summary + +- Request body must include `"type": "formula"` — this field is required +- Only use functions and operators listed in this document +- FILTER/SUMIF/COUNTIF/MAP must not be nested inside each other's conditions (chained calls are not nesting) +- Do not use LOOKUP — use FILTER exclusively +- Table and field names must exactly match `+table-get` output +- Strings must use double quotes `"` +- Format dates with TEXT before concatenating, to control output format +- SORTBY can only be chained and must include an output column +- Link fields return lists — aggregate or extract single values before output diff --git a/.agents/skills/lark-base/references/lark-base-advperm-disable.md b/.agents/skills/lark-base/references/lark-base-advperm-disable.md new file mode 100644 index 0000000..3781dd8 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-advperm-disable.md @@ -0,0 +1,83 @@ +# base +advperm-disable + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +停用指定 Base 的高级权限。停用后自定义角色等高级权限功能将不可用。 + +## 推荐命令 + +```bash +# 停用高级权限 +lark-cli base +advperm-disable \ + --base-token VwGhbYCXQaYGMzsWlEZcBbfMnod +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token,27 位字母数字字符串 | + +## API 入参详情 + +**HTTP 方法和路径:** + +``` +PUT /open-apis/base/v3/bases/:base_token/advperm/enable?enable=false +``` + +**Path 参数:** + +| 参数 | 必填 | 说明 | +|------|------|------| +| `base_token` | 是 | Base 的唯一标识,27 位字母数字字符串 | + +**Query 参数:** + +| 参数 | 必填 | 类型 | 说明 | +|------|------|------|------| +| `enable` | 是 | bool | 固定为 `false`,表示停用高级权限 | + +## API 出参详情 + +**Response:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | int32 | 错误码,0 表示成功 | +| `message` | string | 错误信息 | +| `data` | string | 成功时为空 | + +## 返回值 + +命令成功后输出 JSON: + +```json +{ + "ok": true, + "data": { + "success": true + } +} +``` + +## 工作流 + +> [!CAUTION] +> 这是**高风险写入操作** — 停用高级权限会影响所有已配置的自定义角色,执行前必须向用户确认。 + +1. 向用户确认 `--base-token`,并提醒停用会影响已有角色配置 +2. 执行命令 +3. 确认返回 `code: 0` 表示停用成功 + +## 坑点 + +- ⚠️ **操作用户必须为 Base 管理员**:非管理员调用会返回权限错误 +- ⚠️ **停用影响已有角色**:停用高级权限后,已创建的自定义角色将失效 +- ⚠️ **API 路径版本**:本接口使用 `base/v3`,路径必须从原始文档提取,不要用 WebSearch 补全 +- ⚠️ **data 字段是 JSON 字符串**:响应中 `data` 是 string 类型(非 object),需要双重解析 + +## 参考 + +- [lark-base](../SKILL.md) — 多维表格全部命令 +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/.agents/skills/lark-base/references/lark-base-advperm-enable.md b/.agents/skills/lark-base/references/lark-base-advperm-enable.md new file mode 100644 index 0000000..3dd2f4d --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-advperm-enable.md @@ -0,0 +1,80 @@ +# base +advperm-enable + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +启用指定 Base 的高级权限。启用后可使用自定义角色等高级权限功能。 + +## 推荐命令 + +```bash +# 启用高级权限 +lark-cli base +advperm-enable \ + --base-token VwGh**************Mnod +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token,27 位字母数字字符串 | + +## API 入参详情 + +**HTTP 方法和路径:** + +``` +PUT /open-apis/base/v3/bases/:base_token/advperm/enable?enable=true +``` + +**Path 参数:** + +| 参数 | 必填 | 说明 | +|------|------|------| +| `base_token` | 是 | Base 的唯一标识,27 位字母数字字符串 | + +**Query 参数:** + +| 参数 | 必填 | 类型 | 说明 | +|------|------|------|------| +| `enable` | 是 | bool | 固定为 `true`,表示启用高级权限 | + +## API 出参详情 + +**Response:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | int32 | 错误码,0 表示成功 | +| `message` | string | 错误信息 | +| `data` | string | 成功时为空 | + +## 返回值 + +命令成功后输出 JSON: + +```json +{ + "ok": true, + "data": { + "success": true + } +} +``` + +## 工作流 + +1. 向用户确认 `--base-token` +2. 执行命令 +3. 确认返回 `code: 0` 表示启用成功 + +## 坑点 + +- ⚠️ **操作用户必须为 Base 管理员**:非管理员调用会返回权限错误 +- ⚠️ **API 路径版本**:本接口使用 `base/v3`,路径必须从原始文档提取,不要用 WebSearch 补全 +- ⚠️ **data 字段是 JSON 字符串**:响应中 `data` 是 string 类型(非 object),需要双重解析 +- ⚠️ **启用后才能管理角色**:`+role-create / +role-update / +role-delete` 等角色操作需要先启用高级权限 + +## 参考 + +- [lark-base](../SKILL.md) — 多维表格全部命令 +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/.agents/skills/lark-base/references/lark-base-base-copy.md b/.agents/skills/lark-base/references/lark-base-base-copy.md new file mode 100644 index 0000000..33f6375 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-base-copy.md @@ -0,0 +1,74 @@ +# base +base-copy + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +复制一个已有 Base;可选只复制结构,不复制内容。 + +## 推荐命令 + +```bash +lark-cli base +base-copy \ + --base-token app_xxx \ + --name "Copied Base" + +lark-cli base +base-copy \ + --base-token app_xxx \ + --name "Copied Base" \ + --folder-token fld_xxx \ + --time-zone Asia/Shanghai \ + --without-content +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | 源 Base Token | +| `--name ` | 否 | 新 Base 名称 | +| `--folder-token ` | 否 | 目标文件夹 token | +| `--time-zone ` | 否 | 时区,如 `Asia/Shanghai` | +| `--without-content` | 否 | 只复制结构,不复制内容 | + +## API 入参详情 + +**HTTP 方法和路径:** + +``` +POST /open-apis/base/v3/bases/:base_token/copy +``` + +## 返回重点 + +- 返回 `base`。 +- CLI 会额外标记 `copied: true`。 +- 回复结果时,必须主动返回新 Base 的可访问链接: + - 优先使用返回结果中的 `base.url` + - 同时返回新 Base 的 token + - 如果本次返回没有 `url`,至少返回新 Base 的名称和 token + +> [!IMPORTANT] +> 如果 Base 是**以应用身份(bot)复制**出来的,shortcut 会在复制成功后自动尝试为当前 CLI 用户添加该 Base 的 `full_access`(管理员)权限,并在输出中附带 `permission_grant` 字段。 +> +> `permission_grant.status` 语义如下: +> - `granted`:当前 CLI 用户已获得该 Base 的管理员权限 +> - `skipped`:Base 已复制成功,但没有可授权的当前 CLI 用户,或复制结果缺少可授权 token +> - `failed`:Base 已复制成功,但自动授权失败;结果中会包含失败原因,用户可稍后重试授权,或继续使用应用身份(bot)处理该 Base +> +> 回复复制结果时,除 `base token` 和可访问链接外,还必须明确告知用户 `permission_grant` 的结果。 +> +> **仍然不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。 + +## 工作流 + +> [!CAUTION] +> 这是**写入操作** — 执行前必须向用户确认。 + +1. 先确认源 Base Token。 +2. `--name`、`--folder-token`、`--time-zone` 都是可选项;用户没要求时不要为这些可选参数额外追问。 +3. 只要结构时,显式传 `--without-content`。 +4. 复制成功后,整理并返回:新 Base 名称、token,以及响应中已有的可访问链接。 + +## 参考 + +- [lark-base-workspace.md](lark-base-workspace.md) — base / workspace 索引页 +- [lark-base-base-create.md](lark-base-base-create.md) — 创建全新 Base diff --git a/.agents/skills/lark-base/references/lark-base-base-create.md b/.agents/skills/lark-base/references/lark-base-base-create.md new file mode 100644 index 0000000..2544b03 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-base-create.md @@ -0,0 +1,68 @@ +# base +base-create + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +创建一个新的 Base;可选指定父文件夹和时区。 + +## 推荐命令 + +```bash +lark-cli base +base-create \ + --name "New Base" + +lark-cli base +base-create \ + --name "项目管理" \ + --folder-token fld_xxx \ + --time-zone Asia/Shanghai +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--name ` | 是 | 新 Base 名称 | +| `--folder-token ` | 否 | 目标文件夹 token | +| `--time-zone ` | 否 | 时区,如 `Asia/Shanghai` | + +## API 入参详情 + +**HTTP 方法和路径:** + +``` +POST /open-apis/base/v3/bases +``` + +## 返回重点 + +- 返回 `base`。 +- CLI 会额外标记 `created: true`。 +- 回复结果时,必须主动返回新 Base 的可访问链接: + - 优先使用返回结果中的 `base.url` + - 同时返回新 Base 的 token + - 如果本次返回没有 `url`,至少返回新 Base 的名称和 token + +> [!IMPORTANT] +> 如果 Base 是**以应用身份(bot)创建**的,shortcut 会在创建成功后自动尝试为当前 CLI 用户添加该 Base 的 `full_access`(管理员)权限,并在输出中附带 `permission_grant` 字段。 +> +> `permission_grant.status` 语义如下: +> - `granted`:当前 CLI 用户已获得该 Base 的管理员权限 +> - `skipped`:Base 已创建成功,但没有可授权的当前 CLI 用户,或创建结果缺少可授权 token +> - `failed`:Base 已创建成功,但自动授权失败;结果中会包含失败原因,用户可稍后重试授权,或继续使用应用身份(bot)处理该 Base +> +> 回复创建结果时,除 `base token` 和可访问链接外,还必须明确告知用户 `permission_grant` 的结果。 +> +> **仍然不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。 + +## 工作流 + +> [!CAUTION] +> 这是**写入操作** — 执行前必须向用户确认。 + +1. 先确认 Base 名称。 +2. `--folder-token`、`--time-zone` 都是可选项;用户没要求时不要为此额外追问。 +3. 创建成功后,整理并返回:Base 名称、token,以及响应中已有的可访问链接。 + +## 参考 + +- [lark-base-workspace.md](lark-base-workspace.md) — base / workspace 索引页 +- [lark-base-base-copy.md](lark-base-base-copy.md) — 复制 Base diff --git a/.agents/skills/lark-base/references/lark-base-base-get.md b/.agents/skills/lark-base/references/lark-base-base-get.md new file mode 100644 index 0000000..57791f3 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-base-get.md @@ -0,0 +1,39 @@ +# base +base-get + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +读取一个 Base 的详情。 + +## 推荐命令 + +```bash +lark-cli base +base-get \ + --base-token app_xxx +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | + +## API 入参详情 + +**HTTP 方法和路径:** + +``` +GET /open-apis/base/v3/bases/:base_token +``` + +## 返回重点 + +- 返回 `base`,通常包含 `base_token / name / url` 等信息。 + +## 坑点 + +- ⚠️ 先确认传入的是 `base_token`,不是 `workspace_token`。 +- ⚠️ 如果最初输入来自 `/wiki/...`,不要直接把 `wiki_token` 当 `--base-token`;若报 `param baseToken is invalid` / `base_token invalid`,先用 `lark-cli wiki spaces get_node` 取 `node.obj_token`,再重试 `+base-get`。 + +## 参考 + +- [lark-base-workspace.md](lark-base-workspace.md) — base 索引页 diff --git a/.agents/skills/lark-base/references/lark-base-cell-value.md b/.agents/skills/lark-base/references/lark-base-cell-value.md new file mode 100644 index 0000000..1da7677 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-cell-value.md @@ -0,0 +1,151 @@ +# base CellValue 规范(lark-base-cell-value) + +> 适用命令:`lark-cli base +record-upsert`、`lark-cli base +record-batch-create`、`lark-cli base +record-batch-update` + +本文件定义 **shortcut 写记录** 时 `CellValue` 的推荐格式,目标是让 AI 一次写对。不同命令的外层 JSON 形状不同,但每个 cell 都以本文为 source of truth。 + +## 1. 顶层规则(必须遵守) + +- `--json` 必须是 JSON 对象。 +- `+record-upsert`:顶层直接传字段映射:`{"字段名或字段ID": CellValue}`。 +- `+record-batch-create`:`rows` 是 `CellValue[][]`,列顺序由 `fields` 决定。 +- `+record-batch-update`:`patch` 是 `Map`,同一份 `patch` 会应用到所有 `record_id_list`。 +- 一次 payload 里同一字段只用一种 key(字段名或字段 ID),不要重复。 +- 写入前先 `+field-list` 获取字段 `type/style/multiple`,再构造值。 +- 需要清空字段时优先传 `null`(字段允许清空时)。 + +## 2. 各类型 CellValue + +### 2.1 text / phone / url + +用字符串。URL 字段也传 URL 字符串;普通文本里可以保留 Markdown 风格链接文本,平台会按字段类型处理。 + +```json +{ + "标题": "Hello", + "联系电话": "1380000000000", + "官网": "https://example.com" +} +``` + +### 2.2 number + +用 JSON number,不要用带单位或千分位的字符串。货币、百分比、进度、评分等数字类字段也按数字写入,展示格式由字段配置决定。 + +```json +{ + "工时": 12.5, + "预算": 3000, + "完成度": 0.65, + "评分": 4 +} +``` + +### 2.3 select(单选/多选) + +单选用选项名字符串;多选用选项名数组。选项名建议与字段配置一致;写入未知选项时平台可能自动新增选项,因此不要把自然语言近义词当成已有选项传入。 + +```json +{ + "单选": "Todo", + "多选": ["后端", "高优"] +} +``` + +### 2.4 datetime + +优先用 `YYYY-MM-DD HH:mm:ss` 字符串,这是最稳妥的写法,也和常见 API 输出更容易对齐。不要写相对时间(如“明天上午”)。 + +```json +{ + "截止时间": "2026-03-24 10:00:00" +} +``` + +### 2.5 checkbox + +用 JSON boolean:`true` 或 `false`,不要用 `"true"`、`"是"`、`1`。 + +```json +{ + "已完成": true +} +``` + +### 2.6 user / group_chat + +用对象数组,元素至少包含 `id`。人员字段传用户 ID(如 `ou_xxx`),群字段传群 ID(如 `oc_xxx`);单值/多值都统一使用数组。 + +> **人员字段:不要猜 ID。** 不知道 `open_id` 时,先用 `lark-contact` 查 id:`lark-cli contact +search-user --query "<姓名/邮箱/手机号>" --as user`。 + +> **群组字段:不要猜 ID。** 不知道 `chat_id` 时,先用 `lark-im` 搜群:`lark-cli im +chat-search --query "<群名关键词>" --as user`;取结果里的 `oc_xxx`。 + +```json +{ + "负责人": [ + { "id": "ou_xxx" }, + { "id": "ou_xxx2" } + ], + "协作群": [ + { "id": "oc_xxx" } + ] +} +``` + +### 2.7 link + +用对象数组,元素包含 `id`,值为目标记录的 `record_id`。不要传记录标题;先用 `+record-list` / `+record-search` 找到目标记录 ID。 + +```json +{ + "关联任务": [ + { "id": "" } + ] +} +``` + +### 2.8 location + +用对象 `{lng, lat}`,两者都是数字;`lng` 是经度,`lat` 是纬度。 + +```json +{ + "坐标": { + "lng": 116.397428, + "lat": 39.90923 + } +} +``` + +### 2.9 attachment(不作为普通 CellValue 写入) + +用户要把本地文件加到记录里时,必须使用 `lark-cli base +record-upload-attachment --file ` 上传到已有记录。不能用普通记录操作接口来上传附件。 + +`+record-get` 返回的附件字段单元格包含 `file_token` 和文件名,可以把 `file_token` 交给 `lark-cli docs +media-download` 进行附件下载。 + +## 3. 只读字段(不要写) + +以下字段在写记录时应视为只读: +- `auto_number` +- `lookup` +- `formula` +- `created_at` / `updated_at` +- `created_by` / `updated_by` + +写入只读字段通常不会更新数据;返回里可能出现 `ignored_fields`,reason 会说明 `READONLY`。看到这种返回时,不要重试同一 payload,应移除只读字段,只写存储字段。 + +## 4. 完整示例 + +```json +{ + "标题": "Created from shortcut", + "状态": "Todo", + "标签": ["高优", "外部依赖"], + "工时": 8, + "截止时间": "2026-03-24 10:00:00", + "已完成": false, + "负责人": [{ "id": "ou_123" }], + "关联任务": [{ "id": "rec_456" }], + "坐标": { "lng": 116.397428, "lat": 39.90923 } +} +``` diff --git a/.agents/skills/lark-base/references/lark-base-dashboard-arrange.md b/.agents/skills/lark-base/references/lark-base-dashboard-arrange.md new file mode 100644 index 0000000..7a79313 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-dashboard-arrange.md @@ -0,0 +1,83 @@ +# base +dashboard-arrange + +> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流 + +自动重新排列仪表盘组件布局。服务端根据组件数量和类型进行智能布局优化。 + +## 使用场景 + +| 场景 | 说明 | +|------|------| +| **从 0 到 1 搭建后** | 使用 `+dashboard-create` 和 `+dashboard-block-create` 创建仪表盘后,默认布局可能不够工整美观,推荐使用本命令做一次整体重排 | +| **用户明确要求** | 用户主动要求对已有仪表盘进行布局重排或美化时 | + +> [!CAUTION] +> - **不建议**在已有仪表盘上自动调用此命令,除非用户明确要求 +> - 排列结果是**服务端智能推荐**,不一定完全符合用户预期 +> - 无法指定具体位置(如"第一排放 A,第二排放 B"),排列逻辑是**自适应**的 + +## 推荐命令 + +```bash +# 基础用法 +lark-cli base +dashboard-arrange \ + --base-token xxx \ + --dashboard-id blk_xxx +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | +| `--dashboard-id ` | 是 | 仪表盘 ID | +| `--user-id-type ` | 否 | 用户 ID 类型:open_id / union_id / user_id | +| `--dry-run` | 否 | 预览 API 调用,不执行 | + +## 返回示例 + +```json +{ + "dashboard_id": "blk_xxx", + "name": "数据分析", + "blocks": [ + { + "block_id": "chtbxxxx", + "block_name": "总销售额", + "block_type": "statistics", + "layout": { + "x": 0, + "y": 0, + "w": 6, + "h": 6 + } + }, + { + "block_id": "chtbcrxxxx", + "block_name": "月度趋势", + "block_type": "column", + "layout": { + "x": 6, + "y": 0, + "w": 6, + "h": 6 + } + } + ], + "arranged": true +} +``` + +## 返回重点 + +| 字段 | 说明 | +|------|------| +| `blocks[].layout` | 重排后的布局信息,包含 x/y/w/h | +| `arranged` | 是否重排成功 | + +> [!CAUTION] +> 这是**写入操作** — 执行前必须向用户确认。 + +## 参考 + +- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 diff --git a/.agents/skills/lark-base/references/lark-base-dashboard-block-create.md b/.agents/skills/lark-base/references/lark-base-dashboard-block-create.md new file mode 100644 index 0000000..0e8aa1d --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-dashboard-block-create.md @@ -0,0 +1,108 @@ +# base +dashboard-block-create + +> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流 +> **关键:** 创建前必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解组件类型和 data_config 结构 + +在仪表盘中创建一个组件(Block)。 + +## 关键约束 + +- **`type` 创建后不可修改**,创建时务必选对 +- **`data_config` 结构随 `type` 变化**,不同组件类型字段不同,**⚠️ 必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解如何构造** +- **组件创建必须串行执行**,不能并发 + +## 推荐命令 + +```bash +# 简单示例:创建一个指标卡(统计记录数) +lark-cli base +dashboard-block-create \ + --base-token xxx \ + --dashboard-id blk_xxx \ + --name "总记录数" \ + --type statistics \ + --data-config '{"table_name":"订单表","count_all":true}' + +# 文本组件示例(Markdown 富文本) +lark-cli base +dashboard-block-create \ + --base-token xxx \ + --dashboard-id blk_xxx \ + --name "说明文字" \ + --type text \ + --data-config '{"text":"# 标题\n## 副标题\n**加粗** *斜体* ~~删除~~\n1. 列表1\n2. 列表2"}' + +# 复杂配置用文件传入 +lark-cli base +dashboard-block-create \ + --base-token xxx \ + --dashboard-id blk_xxx \ + --name "销售额趋势" \ + --type line \ + --data-config @config.json +``` + +完整流程参考 [lark-base-dashboard.md](lark-base-dashboard.md) 的「场景 1:从 0 到 1 创建仪表盘」 + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | +| `--dashboard-id ` | 是 | 仪表盘 ID(从 `+dashboard-list/get` 获取) | +| `--name ` | **是** | 组件名称(允许重名) | +| `--type ` | **是** | 组件类型,见下方枚举值。**不同 type 对应不同的 data_config 结构**,常用:`column`(柱状图)、`line`(折线图)、`pie`(饼图)、`statistics`(指标卡)、`text`(文本) | +| `--data-config ` | 否 | 数据配置 JSON,**结构随 type 变化**。**⚠️ 必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解如何构造**。创建时会做本地校验,更新时由后端校验 | +| `--user-id-type ` | 否 | 用户 ID 类型,filter 涉及人员字段时使用 | +| `--dry-run` | 否 | 预览 API 调用,不执行 | + +### type 枚举值 + +| 值 | 说明 | +|----|------| +| `column` | 柱状图 | +| `bar` | 条形图 | +| `line` | 折线图 | +| `pie` | 饼图 | +| `ring` | 环形图 | +| `area` | 面积图 | +| `combo` | 组合图 | +| `scatter` | 散点图 | +| `funnel` | 漏斗图 | +| `wordCloud` | 词云 | +| `radar` | 雷达图 | +| `statistics` | 指标卡 | +| `text` | 文本(支持 Markdown) | + +## 返回示例 + +```json +{ + "block": { + "block_id": "chtxxxxxxxx", + "name": "总记录数", + "type": "statistics", + "data_config": { + "table_name": "电商交易明细", + "count_all": true + } + }, + "created": true +} +``` + +## 返回重点 + +| 字段 | 说明 | +|------|------| +| `block.block_id` | 组件 ID,后续编辑/删除需要用到,务必记录 | +| `block.name` | 组件名称 | +| `block.type` | 组件类型 | +| `block.data_config` | 实际创建的数据配置(可能包含后端自动添加的默认值)| +| `created` | 是否创建成功 | + +> [!CAUTION] +> 这是**写入操作** — 执行前必须向用户确认。 + + +## 参考 + +- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 +- [dashboard-block-data-config.md](dashboard-block-data-config.md) — data_config 结构、图表类型、filter 规则 diff --git a/.agents/skills/lark-base/references/lark-base-dashboard-block-delete.md b/.agents/skills/lark-base/references/lark-base-dashboard-block-delete.md new file mode 100644 index 0000000..d989395 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-dashboard-block-delete.md @@ -0,0 +1,46 @@ +# base +dashboard-block-delete + +> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流。 + +删除仪表盘中的一个组件(Block),不可恢复。 + +## 推荐命令 + +```bash +lark-cli base +dashboard-block-delete \ + --base-token bascn***************CtadY \ + --dashboard-id blkxxx \ + --block-id chtxxxxxxxx +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | +| `--dashboard-id ` | 是 | 仪表盘 ID | +| `--block-id ` | 是 | Block ID | +| `--dry-run` | 否 | 预览 API 调用,不执行 | + +## 返回示例 + +```json +{ + "block_id": "chtxxxxxxxx", + "deleted": true +} +``` + +## 返回重点 + +| 字段 | 说明 | +|------|------| +| `block_id` | 被删除的组件 ID | +| `deleted` | 是否删除成功 | + +> [!CAUTION] +> 这是**写入操作**且**不可逆** — 执行前必须向用户确认。 + +## 参考 + +- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 diff --git a/.agents/skills/lark-base/references/lark-base-dashboard-block-get.md b/.agents/skills/lark-base/references/lark-base-dashboard-block-get.md new file mode 100644 index 0000000..0994f60 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-dashboard-block-get.md @@ -0,0 +1,57 @@ +# base +dashboard-block-get + +> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流。 + +获取仪表盘中单个组件的详情(包含 data_config 完整配置)。常用于:1) 查看组件的完整配置;2) 编辑组件前了解当前配置。 + +## 推荐命令 + +```bash +lark-cli base +dashboard-block-get \ + --base-token bascn***************CtadY \ + --dashboard-id blkxxx \ + --block-id chtxxxxxxxx +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | +| `--dashboard-id ` | 是 | 仪表盘 ID | +| `--block-id ` | 是 | Block ID | +| `--user-id-type ` | 否 | 用户 ID 类型:open_id / union_id / user_id | +| `--format ` | 否 | 输出格式 | +| `--dry-run` | 否 | 预览 API 调用,不执行 | + +## 返回示例 + +```json +{ + "block": { + "block_id": "chtxxxxxxxx", + "name": "柱状图", + "type": "column", + "data_config": { + "table_name": "电商交易明细", + "series": [{"field_name": "营销费用", "rollup": "SUM"}], + "group_by": [{"field_name": "品类", "mode": "integrated"}] + }, + "layout": {"x": 0, "y": 0, "w": 6, "h": 4} + } +} +``` + +## 返回重点 + +| 字段 | 说明 | +|------|-------------------------------| +| `block.block_id` | 组件 ID | +| `block.name` | 组件名称 | +| `block.type` | 组件类型(如 `column`/`line`/`pie`) | +| `block.data_config` | 数据配置(新建/编辑组件时可基于此字段修改) | +| `block.layout` | 布局信息(只读,x/y/w/h 坐标和尺寸) | + +## 参考 + +- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 diff --git a/.agents/skills/lark-base/references/lark-base-dashboard-block-list.md b/.agents/skills/lark-base/references/lark-base-dashboard-block-list.md new file mode 100644 index 0000000..ff8a015 --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-dashboard-block-list.md @@ -0,0 +1,53 @@ +# base +dashboard-block-list + +> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流。 + +分页列出仪表盘中的所有组件(Block)。常用于:1) 查看仪表盘有哪些组件;2) 获取组件 ID 和类型用于后续编辑/删除。 + +## 推荐命令 + +```bash +lark-cli base +dashboard-block-list \ + --base-token bascn***************CtadY \ + --dashboard-id blkxxx +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | +| `--dashboard-id ` | 是 | 仪表盘 ID | +| `--page-size ` | 否 | 每页数量,默认 20,最大 100 | +| `--page-token ` | 否 | 分页标记 | +| `--format ` | 否 | 输出格式:json / pretty / table / csv / ndjson | +| `--dry-run` | 否 | 预览 API 调用,不执行 | + +## 返回示例 + +```json +{ + "items": [ + {"block_id": "chtxxxxxxxx", "name": "图表", "type": "column"}, + {"block_id": "chtxxxxxxxx", "name": "总利润", "type": "statistics"} + ], + "total": 4, + "has_more": false +} +``` + +## 返回重点 + +| 字段 | 说明 | +|------|------| +| `items` | 组件列表,每项包含 `block_id`(ID)、`name`(名称)、`type`(类型)| +| `total` | 组件总数 | +| `has_more` | 是否有更多组件(为 `true` 时需用 `page_token` 继续获取)| + +## 坑点 + +- `+dashboard-block-list` 禁止并发调用;批量执行时只能串行。 + +## 参考 + +- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 diff --git a/.agents/skills/lark-base/references/lark-base-dashboard-block-update.md b/.agents/skills/lark-base/references/lark-base-dashboard-block-update.md new file mode 100644 index 0000000..38cc39e --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-dashboard-block-update.md @@ -0,0 +1,84 @@ +# base +dashboard-block-update + +> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流 +> **关键:** 更新前必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解 data_config 结构和更新规则 + +更新仪表盘中组件的名称或数据配置。 + +## 关键约束 + +- **不可修改 `type` 和 `layout`** — 只能更新 `name` 和 `data_config`。 +- **`data_config` 顶层按 key merge** — 只需传入要修改的顶层字段,未传的字段保留原值;但每个字段内部是全量替换(如传新 `filter` 会完整覆盖旧 `filter`)。 +- **`series` 与 `count_all` 二选一** — 且至少提供其一。 +- **表名用 name,不是 ID** — `table_name` 对应的是表名称(如「订单表」),不是 `table_id`。 +- **`user_id_type`** 仅在 filter 涉及人员字段时有意义。 + +> [!TIP] +> CLI 默认会对 `data_config` 做轻量校验与规范化;如需兼容特殊场景,可加 `--no-validate` 跳过。 + +## 推荐命令 + +```bash +# 示例 1:更新组件名称 +lark-cli base +dashboard-block-update \ + --base-token xxx \ + --dashboard-id blk_xxx \ + --block-id chtxxxxxxxx \ + --name "新名称" + +# 示例 2:更新数据配置(只传要改的字段,未传字段保留原值) +lark-cli base +dashboard-block-update \ + --base-token xxx \ + --dashboard-id blk_xxx \ + --block-id chtxxxxxxxx \ + --data-config '{"filter":{"conjunction":"and","conditions":[{"field_name":"状态","operator":"is","value":"已完成"}]}}' +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | +| `--dashboard-id ` | 是 | 仪表盘 ID | +| `--block-id ` | 是 | Block ID | +| `--name ` | 否 | 新名称 | +| `--data-config ` | 否 | 数据配置 JSON。**结构随 block 的 `type` 变化**。**⚠️ 必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解如何构造** | +| `--user-id-type ` | 否 | 用户 ID 类型,filter 涉及人员字段时使用 | +| `--no-validate` | 否 | 跳过 data_config 本地校验(用于兼容特殊场景) | +| `--dry-run` | 否 | 预览 API 调用,不执行 | + +## 返回示例 + +```json +{ + "block": { + "block_id": "chtxxxxxxxx", + "name": "新名称", + "type": "column", + "data_config": { + "table_name": "订单表", + "series": [{"field_name": "金额", "rollup": "SUM"}], + "group_by": [{"field_name": "类别", "mode": "integrated"}] + } + }, + "updated": true +} +``` + +## 返回重点 + +| 字段 | 说明 | +|------|------| +| `block.block_id` | 组件 ID | +| `block.name` | 更新后的名称 | +| `block.type` | 组件类型(不可修改)| +| `block.data_config` | 更新后的数据配置 | +| `updated` | 是否更新成功 | + +> [!CAUTION] +> 这是**写入操作** — 执行前必须向用户确认。 + +## 参考 + +- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 +- [dashboard-block-data-config.md](dashboard-block-data-config.md) — data_config 结构、图表类型、filter 规则 diff --git a/.agents/skills/lark-base/references/lark-base-dashboard-create.md b/.agents/skills/lark-base/references/lark-base-dashboard-create.md new file mode 100644 index 0000000..6215b4a --- /dev/null +++ b/.agents/skills/lark-base/references/lark-base-dashboard-create.md @@ -0,0 +1,73 @@ +# base +dashboard-create + +> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流。 + +创建空白仪表盘。创建成功后务必记录返回的 `dashboard_id`,后续添加组件和管理仪表盘都需要用到。 + +## 关键约束 + +- **dashboard_id** 在 create 返回中取得,后续 get/update/delete 使用。 + +## 推荐命令 + +```bash +# 创建仪表盘 +lark-cli base +dashboard-create \ + --base-token VwGhb**************fMnod \ + --name "销售报表" + +# 创建仪表盘(指定主题) +lark-cli base +dashboard-create \ + --base-token VwGhb**************fMnod \ + --name "销售报表" \ + --theme-style default +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | +| `--name ` | 是 | 仪表盘名称 | +| `--theme-style + + +
+ +
The page does not exist.
+ Go to homepage +
+ + + \ No newline at end of file diff --git a/.agents/skills/lark-doc/references/lark-doc-media-download.md b/.agents/skills/lark-doc/references/lark-doc-media-download.md new file mode 100644 index 0000000..73f4946 --- /dev/null +++ b/.agents/skills/lark-doc/references/lark-doc-media-download.md @@ -0,0 +1,50 @@ + +# docs +media-download(下载文档素材/画板缩略图) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +下载文档中的图片/文件素材(`file_token`),或下载画板缩略图(`whiteboard_id`)。当 `--output` 不带扩展名时,会根据响应的 `Content-Type` 自动补全扩展名。 + +## 选择规则 + +- 用户明确说“下载素材”时,使用 `docs +media-download` +- 用户只是想查看、预览图片或文件素材时,优先使用 [`docs +media-preview`](lark-doc-media-preview.md) +- 如果目标明确是画板 / whiteboard / 画板缩略图,继续使用 `docs +media-download --type whiteboard`;`+media-preview` 不支持画板 + +## 命令 + +```bash +# 下载图片/文件素材(默认 type=media) +lark-cli docs +media-download --token "Z1Fjxxxxxxxx" --output ./asset + +# 指定输出文件名(带扩展名则不会自动补全) +lark-cli docs +media-download --token "Z1Fjxxxxxxxx" --output ./asset.png + +# 下载画板缩略图(whiteboard token) +lark-cli docs +media-download --type whiteboard --token "wbcnxxxxxxxx" --output ./whiteboard +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--token ` | 是 | 资源 token:素材为 `file_token`,画板为 `whiteboard_id` | +| `--output ` | 是 | 本地保存路径;不带扩展名会自动补全 | +| `--type ` | 否 | `media`(默认)或 `whiteboard` | + +## token 从哪里来 + +- 若你是从文档内容里提取:`lark-doc-fetch` 返回的 Markdown 里可能包含: + - 图片:`` + - 文件:`` + - 画板:`` + +## 排障 + +- 如果报错返回的信息包含 `HTTP 403`,且目标是图片/文件素材,可以改成调用 [`docs +media-preview`](lark-doc-media-preview.md) 看是否能先预览内容 + +## 参考 + +- [lark-doc-fetch](lark-doc-fetch.md) — 获取文档内容(用于提取 token) +- [lark-doc-media-preview](lark-doc-media-preview.md) — 预览素材 +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/.agents/skills/lark-doc/references/lark-doc-media-insert.md b/.agents/skills/lark-doc/references/lark-doc-media-insert.md new file mode 100644 index 0000000..b5b8879 --- /dev/null +++ b/.agents/skills/lark-doc/references/lark-doc-media-insert.md @@ -0,0 +1,106 @@ + +# docs +media-insert(文档末尾插入图片/文件) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +把"创建空 block → 上传文件 → 设置 token"三步合并成一个命令,在**文档末尾**插入本地图片或文件。 + +## 来源选择(Agent 必读) + +> **最高优先级:用户明确指定了来源,就严格按用户的来。** 下面的启发式只在用户没表态时生效。 +> +> - 用户说"把这张截图插进去"、"用剪切板里的图"、"我刚复制的" → 无条件走 `--from-clipboard`。 +> - 用户说"用 `~/Downloads/foo.png`"、"插本地这个文件"、给了具体路径 → 无条件走 `--file`。 +> - 用户两者都没说清 → 按下表的启发式推断。 +> +> 即使推断看起来更"优"(比如用户说了路径但你觉得走剪切板更省事),也**不要自作主张**换来源。要换,先问。 + +按下列顺序判断,**不要反向做**: + +| 用户的图片来源 | 命令 | 禁止做法 | +|----------------|------|----------| +| 图片已经在剪切板里(截图快捷键、从飞书/浏览器复制、从设计稿复制) | `--from-clipboard` | ❌ 不要先把剪切板存到本地文件再用 `--file`。多一步文件 I/O,还得清理临时文件。 | +| 图片是磁盘上的真实文件 | `--file ` | — | +| 图片是 URL | 先下载到本地 → `--file`;或用 `drive` 相关命令 | — | + +`--from-clipboard` 走进程内存直传,不产生临时文件;macOS / Windows 内置支持,Linux 需要 `xclip` 或 `wl-paste` 或 `xsel` 任一。 + +### 剪切板为空时的 fallback + +`--from-clipboard` 失败(剪切板里不是图片 / 没有图片 / Linux 上三个工具都没装)时,命令会返回 `clipboard contains no image data`(或类似的平台错误)。**这不是错误退出理由,而是 fallback 信号。** + +**Agent 的标准处置顺序**(每一步失败再进下一步,不要并行): + +1. 先用 `--from-clipboard` 试一次。 +2. 如果返回"no image data"类错误,**向用户明确说明剪切板里没有可识别的图片**,请用户提供本地文件路径或重新复制一张图。 +3. 拿到本地路径后,用 `--file ` 重试**同一条插入命令**(其他参数如 `--doc` / `--align` / `--caption` 保持不变)。 + +**禁止做法**: +- ❌ 不要悄悄把空剪切板当"成功但没插入"处理。必须提示用户。 +- ❌ 不要在剪切板失败后自行瞎猜某个本地文件路径(比如最近修改的 png)。必须让用户给路径。 +- ❌ 不要用"先让用户保存剪切板到磁盘再 `--file`"的建议绕过 `--from-clipboard`,当且仅当剪切板确实没图片时才退回本地路径。 + +## 命令 + +```bash +# 🟢 推荐:从剪切板直接插入(无需先存盘) +lark-cli docs +media-insert --doc doxcnXXX --from-clipboard + +# 从本地文件插入 +# 除了上传本地文件,还可以在 `docs +update` 时直接通过网络 URL 插入图片,无需先下载到本地: +lark-cli docs +update --api-version v2 --doc "" --command block_insert_after \ + --block-id "目标 block_id" \ + --content '' + +# 插入图片(默认) +lark-cli docs +media-insert --doc doxcnXXX --file ./image.png + +# doc 支持直接传 docx URL(自动提取 document_id) +lark-cli docs +media-insert --doc "https://xxx.feishu.cn/docx/doxcnXXX" --from-clipboard + +# 如果上一步是 create-doc,优先传返回值里的 doc_id +# 不要把 /wiki/... 形式的 doc_url 直接传给 docs +media-insert +lark-cli docs +media-insert --doc doxcnReturnedByCreateDoc --file ./image.png + +# 插入文件(非图片) +lark-cli docs +media-insert --doc doxcnXXX --file ./spec.pdf --type file + +# 图片对齐与描述(caption) +lark-cli docs +media-insert --doc doxcnXXX --from-clipboard --align center --caption "架构图" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--doc ` | 是 | 文档 ID 或 docx URL(仅支持 `/docx/` 形式自动提取;**不支持 `/wiki/...` URL 自动提取**) | +| `--from-clipboard` | 二选一 | 从系统剪切板读取图片(与 `--file` 互斥)。macOS/Windows 内置支持,Linux 需要 `xclip` / `wl-paste` / `xsel` 之一。 | +| `--file ` | 二选一 | 本地文件路径(文件大于 20MB 时自动切换分片上传) | +| `--type ` | 否 | `image`(默认)或 `file`。`--from-clipboard` 目前只产出 image。 | +| `--align ` | 否 | 仅图片:`left` / `center`(默认)/ `right` | +| `--caption ` | 否 | 仅图片:图片描述 | + +> [!IMPORTANT] +> 如果上一步是 [`lark-doc-create`](lark-doc-create.md),并且它在知识库/知识空间场景下返回的是 `/wiki/...` 形式的 `doc_url`,后续调用 `docs +media-insert` 时应优先传 `doc_id`,不要直接传这个 `doc_url`。 + +## 平台注意(仅 `--from-clipboard`) + +| 平台 | 依赖 | 典型错误 | +|------|------|---------| +| macOS | osascript(内置) | 剪切板为空 / 不是图片 → "clipboard contains no image data" | +| Windows | PowerShell + System.Windows.Forms(内置) | 同上 | +| Linux | `xclip` 或 `wl-paste` 或 `xsel` 任一 | 都没安装 → 报错会提示用发行版包管理器安装 | + +命令不支持读取 TIFF 等非 PNG/JPEG/GIF/WebP/BMP 的冷门格式;遇到这类剪切板会返回 "contains no image data",此时才考虑先用系统工具转成文件再 `--file`。 + +## 输出 + +命令成功后会输出 JSON,包含:`document_id`、`block_id`、`file_token`、`file_name`(剪切板路径下为 `clipboard.png`)、`type`。 + +> [!CAUTION] +> 这是**写入操作**(会修改文档内容)—— 执行前必须确认用户意图。 + +## 参考 + +- [lark-doc-fetch](lark-doc-fetch.md) — 获取文档内容(可用于确认插入后的结果、以及提取媒体 token) +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/.agents/skills/lark-doc/references/lark-doc-media-preview.md b/.agents/skills/lark-doc/references/lark-doc-media-preview.md new file mode 100644 index 0000000..b29bebf --- /dev/null +++ b/.agents/skills/lark-doc/references/lark-doc-media-preview.md @@ -0,0 +1,41 @@ + +# docs +media-preview(预览文档素材) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +优先用于查看、预览文档中的图片或文件素材(`file_token`)。命令会把素材保存到本地路径,便于后续打开查看内容。 + +## 选择规则 + +- 用户说“看一下素材 / 图片 / 附件”“预览一下”时,优先使用 `docs +media-preview` +- 用户明确说“下载”时,使用 [`docs +media-download`](lark-doc-media-download.md) +- 如果目标明确是画板 / whiteboard / 画板缩略图,不要使用 `+media-preview`,改用 `docs +media-download --type whiteboard` + +## 命令 + +```bash +# 预览图片/文件素材 +lark-cli docs +media-preview --token "Z1Fjxxxxxxxx" --output ./asset + +# 指定输出文件名(带扩展名则不会自动补全) +lark-cli docs +media-preview --token "Z1Fjxxxxxxxx" --output ./asset.png +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--token ` | 是 | 素材 token,即 `file_token` | +| `--output ` | 是 | 本地保存路径;不带扩展名会自动补全 | + +## token 从哪里来 + +- 若你是从文档内容里提取:`lark-doc-fetch` 返回的 Markdown 里可能包含: + - 图片:`` + - 文件:`` + +## 参考 + +- [lark-doc-fetch](lark-doc-fetch.md) — 获取文档内容(用于提取 token) +- [lark-doc-media-download](lark-doc-media-download.md) — 明确下载素材,或下载画板缩略图 +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/.agents/skills/lark-doc/references/lark-doc-search.md b/.agents/skills/lark-doc/references/lark-doc-search.md new file mode 100644 index 0000000..6ca0df4 --- /dev/null +++ b/.agents/skills/lark-doc/references/lark-doc-search.md @@ -0,0 +1,217 @@ + +# docs +search(云空间搜索:文档 / Wiki / 电子表格) + +> ⚠️ **此命令进入维护期,后续会下线。新用法请使用 [`drive +search`](../../lark-drive/references/lark-drive-search.md)。** +> +> `drive +search` 把所有过滤条件扁平化为独立 flag(`--edited-since` / `--mine` / `--doc-types` 等),面向自然语言场景设计,同时新增了 `my_edit_time`(我编辑过)、`my_comment_time`(我评论过)等维度。除非要沿用老脚本里的 `--filter` JSON,否则**都应该切到 `drive +search`**。 +> +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +基于 Search v2 接口 `POST /open-apis/search/v2/doc_wiki/search`,以**用户身份**统一搜索云空间对象。 + +虽然接口名是 `doc_wiki/search`,但命中结果不只限于文档 / Wiki,也会返回 `SHEET`、`BITABLE`、`FOLDER` 等云空间对象。因此它适合作为云空间对象的资源发现入口:先定位文档、知识库节点、电子表格、多维表格、文件夹,以及用户以“表格 / 报表”方式描述的相关对象,再切回对应业务 skill 做对象内部操作。 + +该 shortcut 会: + +- 未指定范围字段时,自动补齐 `doc_filter` / `wiki_filter` +- 自动将 `--filter` 中的公共字段同步到搜索范围对应的 filter;`folder_tokens` 仅发到 `doc_filter`,`space_ids` 仅发到 `wiki_filter` +- 支持在 `filter.open_time` / `filter.create_time` 中使用 ISO 8601 时间,并自动转换为 Unix 秒 +- 在返回结果中为 `*_time` 字段补充 `*_time_iso`(便于阅读) +- `title_highlighted` / `summary_highlighted` 可能包含高亮标签(如 `` / ``) + +## 命令 + +> **关键约束:搜索关键词必须通过 `--query` 传递。** +> 正确:`lark-cli docs +search --query "方案"` +> 错误:`lark-cli docs +search 方案` +> `+search` 不接受“搜索词位置参数”这种写法;如果把关键词直接跟在命令后面,不会进入 `query`,会变成空搜或返回不符合预期的结果。 + +```bash +# 关键词搜索 +lark-cli docs +search --query "季度总结" + +# 搜标题里带“评测结果”的电子表格 / 文档 +lark-cli docs +search --query "评测结果" + +# 标题包含关键词(默认按关键词检索,不做精确标题匹配) +lark-cli docs +search --query "方案" + +# 使用服务端标题限定语法 +lark-cli docs +search --query 'intitle:方案' + +# 精确短语匹配 +lark-cli docs +search --query '"季度 总结"' + +# 逻辑或 / 排除 +lark-cli docs +search --query '方案 OR 草稿' +lark-cli docs +search --query '方案 -草稿' + +# 标题精确短语匹配 +lark-cli docs +search --query 'intitle:"季度总结"' + +# 按最近打开时间过滤 +lark-cli docs +search \ + --query "方案" \ + --filter '{"open_time":{"start":"2025-09-24T00:00:00+08:00","end":"2025-12-24T23:59:59+08:00"}}' + +# 按文档所有者过滤(creator_ids 传文档所有者 open_id,不是邮箱 / user_id) +lark-cli docs +search \ + --query "季度总结" \ + --filter '{"creator_ids":["ou_7890123456abcdef"]}' + +# 只搜索指定类型 +lark-cli docs +search \ + --query "评测结果" \ + --filter '{"doc_types":["SHEET","DOCX"]}' + +# 只在指定文件夹下搜索文档(folder_token 通常来自 /drive/folder/) +lark-cli docs +search \ + --query "方案" \ + --filter '{"folder_tokens":["fld_123456"]}' + +# 只搜标题,不搜正文 / 摘要 +lark-cli docs +search \ + --query "周报" \ + --filter '{"only_title":true}' + +# 只搜评论,不搜标题 / 正文 +lark-cli docs +search \ + --query "延期原因" \ + --filter '{"only_comment":true}' + +# 只搜索指定群会话里分享过的文档(chat_id 最多 20 个) +lark-cli docs +search \ + --query "方案" \ + --filter '{"chat_ids":["oc_1234567890abcdef"]}' + +# 只搜索指定分享者分享过的文档(sharer_ids 传分享者 open_id,最多 20 个) +lark-cli docs +search \ + --query "复盘" \ + --filter '{"sharer_ids":["ou_7890123456abcdef"]}' + +# 按创建时间过滤并指定排序方式 +lark-cli docs +search \ + --query "方案" \ + --filter '{"create_time":{"start":"2026-01-01","end":"2026-03-31"},"sort_type":"CREATE_TIME"}' + +# 组合多个筛选条件 +lark-cli docs +search \ + --query "项目复盘" \ + --filter '{"creator_ids":["ou_7890123456abcdef"],"doc_types":["DOCX","SHEET"],"only_title":true,"sort_type":"OPEN_TIME","open_time":{"start":"2026-01-01T00:00:00+08:00"}}' + +# 只在指定知识空间下搜 Wiki +lark-cli docs +search \ + --query "研发规范" \ + --filter '{"space_ids":["space_1234567890fedcba"]}' + +# 空搜(不传 query 或传空字符串):按最近浏览等默认规则返回 +lark-cli docs +search + +# 人类可读格式输出 +lark-cli docs +search --query "OKR" --format pretty + +# 返回原始 JSON,并用 page_token 翻页 +lark-cli docs +search --query "方案" --format json +lark-cli docs +search --query "方案" --format json --page-token '' +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--query ` | 否 | 搜索关键词。**支持高级 Boolean 语法**以提升搜索精度:
1. 使用空格表示 AND(如 `方案 设计`)。
2. 使用 `OR` 表示逻辑或(如 `方案 OR 草稿`)。
3. 使用 `-` 表示排除(如 `方案 -草稿`)。
4. 使用双引号 `""` 表示精确匹配短语。
5. 使用 `intitle:` 限定关键词出现在标题中(如 `intitle:总结` 或 `intitle:"季度 总结"`)。不传/空字符串表示空搜。**凡是有关键词,都要显式通过 `--query` 传递,不要写成位置参数。** | +| `--filter ` | 否 | JSON 对象。公共字段默认同时应用到 `doc_filter` / `wiki_filter`;若传 `folder_tokens`,则只发 `doc_filter`;若传 `space_ids`,则只发 `wiki_filter`;两者不能同时传 | +| `--page-size ` | 否 | 每页数量(默认 15,最大 20) | +| `--page-token ` | 否 | 翻页标记(配合 `has_more` 使用) | +| `--format` | 否 | 输出格式:json(默认) \| pretty | + +## `--query` 高级语法 + +以下语法由服务端搜索能力处理,适合把过滤逻辑尽量下推到搜索侧: + +- 空格表示 AND:`方案 设计` +- `OR` 表示逻辑或:`方案 OR 草稿` +- `-` 表示排除:`方案 -草稿` +- 双引号表示精确短语:`"季度 总结"` +- `intitle:` 表示标题限定:`intitle:总结` +- 标题精确短语:`intitle:"季度总结"` + +## `--filter` 字段速查 + +`--filter` 是一个 JSON 对象。大多数字段默认会同时作用于 `doc_filter` 和 `wiki_filter`;其中 `folder_tokens` 只用于文档侧,`space_ids` 只用于 Wiki 侧。 + +### 字段归属 + +- `doc_filter` / `wiki_filter` 公共字段:`creator_ids`、`doc_types`、`chat_ids`、`sharer_ids`、`only_title`、`only_comment`、`open_time`、`sort_type`、`create_time` +- `doc_filter` 独有字段:`folder_tokens` +- `wiki_filter` 独有字段:`space_ids` +- 如果传 `folder_tokens`,shortcut 只发送 `doc_filter` +- 如果传 `space_ids`,shortcut 只发送 `wiki_filter` +- 如果同时传 `folder_tokens` 和 `space_ids`,shortcut 直接报错,不支持同时查询文档文件夹范围和知识空间范围 + +| 字段 | 作用范围 | 类型 | 说明 | +|------|----------|------|------| +| `creator_ids` | 文档 + Wiki | `string[]` | 所有者列表,**必须传 open_id**,不是 `user_id` / `union_id` / 邮箱。比如 `["ou_xxx"]`。如果只有姓名,先用 `lark-contact` 查 open_id | +| `doc_types` | 文档 + Wiki | `string[]` | 资源类型过滤。常用值:`DOC`、`DOCX`、`SHEET`、`BITABLE`、`FILE`、`WIKI`、`SLIDES`、`FOLDER`、`CATALOG`、`SHORTCUT` | +| `chat_ids` | 文档 + Wiki | `string[]` | 群会话 ID 列表,只搜索这些会话里分享过的文档,最多 20 个。通常传群 `chat_id`(如 `oc_xxx`);如果用户只给群名,先用 `lark-im` 定位群 | +| `sharer_ids` | 文档 + Wiki | `string[]` | 分享者列表,**必须传分享者 open_id**,最多 20 个。适合“某人分享过的文档”;如果只有姓名,先用 `lark-contact` 查 open_id | +| `folder_tokens` | 仅文档 | `string[]` | 只搜索指定云空间文件夹下的文档;值通常来自文件夹 URL `/drive/folder/` | +| `space_ids` | 仅 Wiki | `string[]` | 只搜索指定知识空间下的 Wiki 节点 | +| `only_title` | 文档 + Wiki | `boolean` | 只搜标题。注意这不是“标题精确等于”,只是把搜索范围限制在标题 | +| `only_comment` | 文档 + Wiki | `boolean` | 只搜评论。用法类似 `only_title`,只是把搜索范围限制在评论区;默认 `false` | +| `open_time` | 文档 + Wiki | `object` | 最近打开时间范围,支持 `{ "start": "...", "end": "..." }`。shortcut 支持 ISO 8601 / `YYYY-MM-DD` / Unix 秒,并自动转成秒级时间戳 | +| `sort_type` | 文档 + Wiki | `string` | 排序方式。常用值:`DEFAULT_TYPE`、`OPEN_TIME`、`EDIT_TIME`、`EDIT_TIME_ASC`、`CREATE_TIME` | +| `create_time` | 文档 + Wiki | `object` | 文档 / Wiki 创建时间范围,结构与 `open_time` 相同 | + +### 字段使用建议 + +- `creator_ids`:适合“找某个人创建的文档 / 表格 / Wiki”。如果用户只给姓名,不要猜 ID,先查这个人的 `open_id`。 +- `doc_types`:只在用户**明确指定资源类型**时使用,适合先把资源类型缩小。显式类型词可按以下方式映射:`表格 / 电子表格 / spreadsheet -> ["SHEET"]`、`多维表格 / base / bitable -> ["BITABLE"]`、`知识库 / wiki -> ["WIKI"]`、`文件夹 -> ["FOLDER"]`、`普通文档` 或明确要求“只看文档类型、不要表格 / Wiki” -> `["DOC","DOCX"]`。不要因为用户口头说“文档”就默认补 `DOC` / `DOCX`,因为“文档”在很多场景里只是对云空间对象的泛称。 +- `chat_ids`:适合“搜某个群里分享过的文档”“看某个群会话里的方案”。如果用户只给群名,先切到 `lark-im` 用群搜索能力拿到 `chat_id`,再回到 `docs +search`。 +- `sharer_ids`:适合“找某人分享过的文档”“看某个同事转给我的资料”。如果用户只给姓名,不要猜 ID,先用 `lark-contact` 查分享者 `open_id`。 +- `folder_tokens`:适合“在某个云空间文件夹里搜文档”。它不是知识空间 `space_id`,两者不要混用。 +- `only_title`:适合“标题里包含某个词”的场景;如果用户明确表达标题限定,也可以直接在 `--query` 里使用 `intitle:`。如果用户要“标题精确等于”,优先使用 `intitle:"完整标题"`,必要时再做客户端精确确认。 +- `only_comment`:适合“评论里提到某个词”“只找评论区讨论过某件事”。它和 `only_title` 一样,都是把搜索范围缩小到特定区域,但这里限制到评论区。 +- `open_time`:适合“最近打开过 / 最近看过”的描述;如果用户说相对时间,先换算成明确绝对时间再传。 +- `sort_type`:`CREATE_TIME_ASC` 在协议里标注“暂不支持”,`ENTITY_CREATE_TIME_ASC` / `ENTITY_CREATE_TIME_DESC` 已废弃,默认不要主动使用。 +- `create_time`:适合“今年新建的”“上个月创建的”这类条件;不写 `start` / `end` 时,协议默认窗口是“请求时间往前 1 年”到“请求时间”。 + +### 常见 `--filter` JSON 片段 + +```json +{"creator_ids":["ou_7890123456abcdef"]} +{"doc_types":["SHEET","DOCX"]} +{"chat_ids":["oc_1234567890abcdef"]} +{"sharer_ids":["ou_7890123456abcdef"]} +{"folder_tokens":["fld_123456"]} +{"only_title":true} +{"only_comment":true} +{"open_time":{"start":"2026-01-01T00:00:00+08:00","end":"2026-03-31T23:59:59+08:00"},"sort_type":"OPEN_TIME"} +{"create_time":{"start":"2026-01-01","end":"2026-03-31"},"sort_type":"CREATE_TIME"} +{"space_ids":["space_1234567890fedcba"]} +``` + +## 结果判别 + +- `result_meta.doc_types == SHEET`:电子表格,后续切到 `lark-sheets` +- 其他类型:继续按对应 skill 或 API 处理 + +## 决策规则 + +- 参数传递:只要用户给了搜索关键词,就必须显式使用 `--query "<关键词>"`。不要生成 `lark-cli docs +search 方案`、`lark-cli docs +search xxx(搜索关键词)` 这种位置参数写法。 +- 查询语义:必须优先利用 --query 的高级语法(如 intitle:、""、-)将过滤逻辑下推给服务端。当用户要求“标题精确等于 X”时,直接使用 --query "intitle:\"X\"",严禁先进行模糊搜索再做客户端二次筛选。只有在遇到服务端语法无法覆盖的复杂本地比对场景时,才允许在客户端过滤,且比对前必须先去掉 title_highlighted 里的高亮标签。 +- 实体补全:如果用户要按“某个群里分享的文档”搜索,先用 `lark-im` 拿 `chat_id` 再填 `chat_ids`;如果用户要按“某人分享的文档”搜索,先用 `lark-contact` 拿 `open_id` 再填 `sharer_ids`。 +- 零结果回退:如果因为用户的显式类型约束加了 `doc_types` 且结果为 0,可以提示“按指定类型没搜到”;只有在不违背用户明确约束的前提下,才建议放宽类型重试。 +- 入口选择:用户说“找表格标题”“找名为 `X` 的电子表格”“搜某个报表”时,也默认走 `docs +search`。不要误用 `sheets +find` 做跨文件搜索。 +- 分页策略:默认只返回**第一页**,并说明 `has_more` / `page_token`。只有当用户明确要求“全部结果”“继续翻页”“全量扫描”“所有结果”“完整列表”时,才继续翻页。 +- 翻页上限:即使用户要求全量,单轮也最多先拉 **5 页**(按默认 `page-size=20` 约等于最多 100 条结果)。达到上限后,先回报当前进度和是否还有更多页,再让用户决定是否继续下一批。 +- 总数口径:`total` 是 OpenAPI 的搜索结果总数,不一定等于客户端二次筛选后的精确数量。凡是依赖本地过滤、去重、精确标题匹配的场景,都不要默认承诺“精确总数”。 +- 原始返回:如果用户要求“直接返回接口数据 / 原始返回”,优先使用 `--format json`,不要额外做精确标题过滤或摘要重写。 +- 时间表达:用户如果说“3 到 6 个月前”“最近半年内”等相对时间,先转换成明确的绝对时间,再写入 `filter.open_time` / `filter.create_time`。 +- 跨 skill handoff:如果搜索的目标是某个 spreadsheet,返回命中的标题、URL、token 等定位信息后,应切换到 `lark-sheets` 继续后续操作,不要把 `docs +search` 当成对象内部查询。 + +## 权限 + +| 操作 | 所需 scope | +|------|-----------| +| 搜索云空间对象(含文档 / Wiki / 表格资源发现) | `search:docs:read` | diff --git a/.agents/skills/lark-doc/references/lark-doc-update.md b/.agents/skills/lark-doc/references/lark-doc-update.md new file mode 100644 index 0000000..d746645 --- /dev/null +++ b/.agents/skills/lark-doc/references/lark-doc-update.md @@ -0,0 +1,252 @@ + +# docs +update(更新飞书云文档) + +> **前置条件(MUST READ):** 生成文档内容前,必须先用 Read 工具读取以下文件,缺一不可: +> 1. [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) — 认证、全局参数和安全规则 +> 2. [`lark-doc-xml.md`](lark-doc-xml.md) — XML 语法规则(使用 Markdown 格式时改读 [`lark-doc-md.md`](lark-doc-md.md)) +> 3. [`lark-doc-style.md`](style/lark-doc-style.md) — 排版指南(元素选择、丰富度规则、颜色语义) +> 4. [`lark-doc-update-workflow.md`](style/lark-doc-update-workflow.md) — 改写增强工作流(Code-Act Loop、并行执行策略) +> +> **未读完以上文件就生成内容会导致格式错误或样式不达标。** + +通过八种指令精确更新飞书云文档。支持字符串级别和 block 级别的操作。 + +> **⚠️ 格式选择规则:** +> - **局部精修**(`str_replace` / `block_insert_after` / `block_replace` / `block_delete` / `block_move_after`):优先使用 XML(默认)。XML 能稳定表达 block 结构和样式,精准编辑更可控;不要因为 Markdown 写起来更简单就自行切换。 +> - **整段写入**(`append` / `overwrite`):XML 和 Markdown 都可以。用户提供 `.md` 本地文件或明确要求 Markdown 时直接用 Markdown;否则默认 XML。 +> +> **Markdown 局限 & block ID 前提:** Markdown 不携带 block ID,也无样式(颜色、对齐、callout 等)。需要按 block ID 定位(`block_*` 指令的 `--block-id`)时,先 `docs +fetch --detail with-ids` **配合 `--scope`(`outline` / `range` / `keyword` / `section`)局部获取**目标段落,不要全量 fetch。拿到 block ID 后 `--content` 仍可用 Markdown,只是写入内容不带样式。 + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--api-version` | 是 | 固定传 `v2` | +| `--doc` | 是 | 文档 URL 或 token | +| `--command` | 是 | 操作指令(见下方指令速查表) | +| `--doc-format` | 否 | 内容格式:`xml`(默认,始终优先使用)\| `markdown`(仅用户明确要求时) | +| `--content` | 视指令 | 写入内容(`str_replace` 传空字符串可实现删除) | +| `--pattern` | 视指令 | 匹配文本(str_replace) | +| `--block-id` | 视指令 | 目标 block ID(block_* 操作),-1 表示末尾 | +| `--src-block-ids` | 视指令 | 源 block ID(逗号分隔),用于 block_copy_insert_after / block_move_after | +| `--revision-id` | 否 | 基准版本号,-1 = 最新(默认 `-1`) | + +## 指令速查表 + +| 指令 | 说明 | 必需参数 | +|------|------|----------| +| `str_replace` | 全文文本查找替换(replacement 支持富文本标签;`--content` 传空字符串即为删除) | `--pattern` `--content` | +| `block_insert_after` | 在指定 block 之后插入新内容 | `--block-id` `--content` | +| `block_copy_insert_after` | 复制源 block 并插入到锚点之后(源块不变) | `--block-id` `--src-block-ids` | +| `block_replace` | 替换指定 block(同一 block 仅限一次) | `--block-id` `--content` | +| `block_delete` | 删除指定 block(逗号分隔可批量) | `--block-id` | +| `overwrite` | ⚠️ 清空文档后全文重写(可能丢失图片、评论) | `--content` | +| `append` | 在文档末尾追加内容(等价于 `block_insert_after --block-id -1`) | `--content` | +| `block_move_after` | 移动已有 block 到指定位置 | `--block-id` + (`--content` 或 `--src-block-ids`) | + +## 指令示例 + +### str_replace — 全文文本替换 + +> **匹配范围:** +> - **XML 模式(默认)**:`--pattern` 只支持**行内匹配**,不能跨 block / 跨段落匹配。涉及整段或多 block 的改动,请改用 `block_replace`。 +> - **Markdown 模式**(`--doc-format markdown`):`--pattern` 同时支持**行内和跨行匹配**,可以用多行字符串匹配并替换一整段内容。 +> - 还支持**`前缀...后缀` 省略号语法**:用 `...`(三个英文句点)串联起始与结束片段,匹配从前缀到后缀之间的全部内容(含中间被省略部分)。适合一段很长、但首尾特征明显的文本,避免把整段都塞进 `--pattern`。 +> - 前缀、后缀本身仍遵循 Markdown 转义规则;省略号中间的内容**会被替换**为 `--content` 的完整文本,不会被保留。 + +```bash +# 简单文本替换 +lark-cli docs +update --api-version v2 --doc "" --command str_replace \ + --pattern "张三" --content "李四" + +# 替换为富文本(加粗 + 链接) +lark-cli docs +update --api-version v2 --doc "" --command str_replace \ + --pattern "旧链接" --content '新链接 点击查看' + +# 仅当用户明确要求时才使用 Markdown +lark-cli docs +update --api-version v2 --doc "" --command str_replace \ + --doc-format markdown --pattern "旧内容" --content "新内容" + +# Markdown 模式下支持跨行匹配(--pattern 与 --content 都需要真实换行;"..."/'...' 里的 \n 是字面量) +# 多行内容推荐 heredoc 或 --content @file.md,避免 shell 转义踩坑 +lark-cli docs +update --api-version v2 --doc "" --command str_replace \ + --doc-format markdown \ + --pattern "$(printf '## 旧标题\n\n第一段原文\n\n第二段原文')" \ + --content - <<'EOF' +## 新标题 + +改写后的第一段 + +改写后的第二段 +EOF + +# Markdown 模式下使用 `前缀...后缀` 省略号匹配首尾特征明显的大段内容 +# 下例会把「## 旧标题」到「结束语。」之间的所有内容整体替换 +lark-cli docs +update --api-version v2 --doc "" --command str_replace \ + --doc-format markdown \ + --pattern "## 旧标题...结束语。" \ + --content - <<'EOF' +## 新标题 + +重写后的正文... + +新的结束语。 +EOF + +# 删除文本:--content 传空字符串即可 +lark-cli docs +update --api-version v2 --doc "" --command str_replace \ + --pattern "废弃的内容" --content "" +``` + +### block_insert_after — 在指定 block 之后插入 + +```bash +lark-cli docs +update --api-version v2 --doc "" --command block_insert_after \ + --block-id "目标 block_id" \ + --content '

新章节

  • 要点 1
  • 要点 2
' +``` + +### block_replace — 替换指定 block + +```bash +lark-cli docs +update --api-version v2 --doc "" --command block_replace \ + --block-id "目标 block_id" \ + --content '

替换后的段落内容

' +``` + +### block_delete — 删除指定 block + +```bash +lark-cli docs +update --api-version v2 --doc "" --command block_delete \ + --block-id "目标 block_id" +``` + +### overwrite — 全文覆盖 + +```bash +lark-cli docs +update --api-version v2 --doc "" --command overwrite \ + --content '全新文档

概述

新的内容

' +``` + +> ⚠️ 会清空文档后重写,可能丢失图片、评论等。仅在需要完全重建文档时使用。 + +### append — 在文档末尾追加 + +```bash +lark-cli docs +update --api-version v2 --doc "" --command append \ + --content '

新增章节

追加的内容

' +``` + +> 等价于 `block_insert_after --block-id -1`,无需先获取 block ID。 + +### block_copy_insert_after — 复制块并插入 + +将一个或多个源块复制到锚点块之后,源块保持不变。`--src-block-ids` 为逗号分隔的源块 ID,按顺序依次插入到锚点之后。 + +```bash +# 复制多个块(按顺序插入:anchor → a → b → c) +lark-cli docs +update --api-version v2 --doc "" --command block_copy_insert_after \ + --block-id "锚点 block_id" \ + --src-block-ids "block_a,block_b,block_c" +``` + +### block_move_after — 移动已有 block + +将文档中已有的 block 移动到指定锚点之后。使用 `--src-block-ids` 指定要移动的块 ID,无需 `--content`。 + +```bash +# 移动到页面末尾 +lark-cli docs +update --api-version v2 --doc "" --command block_move_after \ + --block-id "-1表示末尾,page_id表示开头,blk" \ + --src-block-ids "block_a,block_b" +``` + +## 返回值 + +```json +{ + "ok": true, + "identity": "user", + "data": { + "document": { + "revision_id": 13, + "new_blocks": [ + { "block_id": "blkcnXXXX", "block_type": "whiteboard", "block_token": "boardXXXX" } + ] + }, + "result": "success", + "updated_blocks_count": 3, + "warnings": [] + } +} +``` + +| 字段 | 说明 | +|------|------| +| `result` | `success` \| `partial_success` \| `failed` | +| `updated_blocks_count` | 实际更新的 block 数量 | +| `warnings` | 警告信息列表 | +| `document.new_blocks` | 本次操作新增的 block 列表(如画板)。`block_id` 可用于后续精确编辑;`block_token` 是资源块 token(如画板)可交给 `lark-whiteboard` 等 skill 继续操作 | + +## 典型工作流 + +### 精确 block 级更新 + +1. **获取文档内容和 block ID**: + ```bash + lark-cli docs +fetch --api-version v2 --doc "" --detail with-ids + ``` + +2. **定位目标 block**:从返回的 XML 中找到要修改的 block 及其 `id` 属性 + +3. **执行更新**: + ```bash + # 替换特定 block + lark-cli docs +update --api-version v2 --doc "" --command block_replace \ + --block-id "blkcnXXXX" --content "

新内容

" + + # 在某 block 后插入 + lark-cli docs +update --api-version v2 --doc "" --command block_insert_after \ + --block-id "blkcnXXXX" --content "

追加的章节

" + ``` + +### 简单文本替换 + +不需要 block ID,直接匹配替换: + +```bash +lark-cli docs +update --api-version v2 --doc "" --command str_replace \ + --pattern "v1.0" --content "v2.0" +``` + +## 画板处理 + +> **`docs +update` 不能直接编辑已有画板的内容。** 本命令只能**新增**画板块;要修改已有画板,先用 `docs +fetch` 取到 ``,再切到 [`lark-whiteboard`](../../lark-whiteboard/SKILL.md) 用 `whiteboard +update` 写入。 + +画板的语法选型与插入示例见 [`lark-doc-style.md`](style/lark-doc-style.md) 的「画板语法与插入」章节。 + +## 最佳实践 + +- **精确操作优于全文覆盖**:使用 `block_replace`/`block_insert_after` 精确修改,避免 `overwrite` 全文覆盖 +- **str_replace 的匹配范围取决于格式**: + - **XML 模式(默认)**:`--pattern` 只支持**行内**匹配,不支持跨行 / 跨 block。段落、整块或容器级(列表、表格、分栏、引用块等)改动请改用 `block_replace` 指定 block_id 重建。 + - **Markdown 模式**(`--doc-format markdown`):`--pattern` 同时支持**行内和跨行**匹配,还支持 `前缀...后缀` 省略号语法(用 `...` 串联首尾片段匹配一大段内容),可以一次替换多行文本;但仍建议优先按最小片段匹配,跨 block 容器级重写仍优先用 `block_replace`,避免副作用。 +- **保护不可重建的内容**:图片、画板、电子表格等以 token 形式存储,替换时避开这些 block +- **str_replace 的 replacement 支持富文本**:可以用行内标签 ``、``、``、`` 等替换普通文本为富文本 +- **同一 block 只能被 replace 一次**:多次修改同一 block 请合并为一次 block_replace +- **block_delete 支持批量**:用逗号分隔多个 block_id 一次删除 +- **复杂结构重组**:将多个段落转换为 grid / table 等复杂布局时,分步操作比 overwrite 更安全: + 1. 用 `block_insert_after` 在目标位置插入新的富文本结构 + 2. 用 `block_delete` 批量删除旧的 block + 3. 这样可以保留文档中其他不相关的内容(图片、评论等) +- **视觉丰富度**:插入或替换内容时,同样遵循 [`lark-doc-style.md`](style/lark-doc-style.md) 中的样式指南,主动使用结构化 block + +## 参考 + +- [`lark-doc-update-workflow.md`](style/lark-doc-update-workflow.md) — 改写增强工作流(Code-Act Loop、并行执行策略) +- [`lark-doc-style.md`](style/lark-doc-style.md) — 文档样式指南(元素选择 + 丰富度规则 + 颜色语义) +- [`lark-doc-xml.md`](lark-doc-xml.md) — XML 语法规范 +- [`lark-doc-fetch.md`](lark-doc-fetch.md) — 获取文档 +- [`lark-doc-create.md`](lark-doc-create.md) — 创建文档 +- [`lark-doc-media-insert.md`](lark-doc-media-insert.md) — 插入图片/文件到文档 +- [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/.agents/skills/lark-doc/references/lark-doc-whiteboard.md b/.agents/skills/lark-doc/references/lark-doc-whiteboard.md new file mode 100644 index 0000000..5ef05f1 --- /dev/null +++ b/.agents/skills/lark-doc/references/lark-doc-whiteboard.md @@ -0,0 +1,66 @@ +# lark-doc 画板处理指南 + +> **前置条件:** 先阅读 [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +## 两个 Skill 的职责边界 + +| Skill | 核心职责 | 约束 | +|------|------|------| +| `lark-doc` | 文档内容读取/更新、插入空白画板占位、获取 board_token | 不能直接编辑画板内容;`docs +update` 的画板能力仅限插入空白占位 | +| `lark-whiteboard` | 查询/导出画板(+query);图表内容生成(Mermaid/DSL/SVG 路由、场景选型、渲染验证);写入画板(+update) | 图表内容生成由此 skill 完整执行,不依赖外部调度 | + +## 文档与画板协同流程 + +### 步骤 1:判断场景 + +| 场景 | 入口 | +|------|------| +| 文档中需要插入新画板 | 继续步骤 2 | +| 已有画板需要更新内容 | 先 `docs +fetch` 获取 `board_token`,跳至步骤 3 | +| 只查看 / 下载已有画板 | 切换至 `lark-whiteboard`,不走本流程 | + +### 步骤 2:在文档中创建空白画板 + +- 创建场景:`docs +create`;编辑场景:`docs +update` +- markdown 中使用 ``(不要转义) +- 多个画板时,在相应的地方插入各自的 whiteboard 标签 +- 从响应的 `data.board_tokens` 中读取 token 列表 + +### 步骤 3:生成并写入画板内容 + +读取 [`../../lark-whiteboard/SKILL.md`](../../lark-whiteboard/SKILL.md),跳至"渲染 & 写入画板"章节,按其完整流程为每个 board_token 生成并写入图表内容。 + +多个画板时依次处理,每个画板完成后再处理下一个。 + +### 步骤 4:完成校验 + +- 确认每个 token 对应的画板都已填充真实内容 +- 不保留空白占位画板;只有空白画板而无内容视为任务未完成 + +--- + +## 语义与画板类型映射 + +| 语义 | 画板类型 | +|------|------| +| 架构/分层/技术方案/模块依赖/调用关系 | 架构图 | +| 流程/审批/部署/业务流转/状态机 | 流程图 | +| 跨角色流程/跨系统交互/端到端链路 | 泳道图 | +| 组织/层级/汇报关系 | 组织架构图 | +| 时间线/里程碑/版本规划 | 里程碑图 | +| 因果/复盘/根因分析 | 鱼骨图 | +| 方案对比/技术选型/功能矩阵 | 对比图 | +| 循环/飞轮/闭环/增长链路 | 飞轮图 | +| 层级占比/能力模型/需求层次 | 金字塔图 | +| 矩形树图/层级面积占比 | 树状图 | +| 转化漏斗/销售漏斗 | 漏斗图 | +| 分类梳理/知识体系/思维导图/时序图/类图 | Mermaid | +| 数据分布/占比/饼图 | Mermaid | +| 柱状图/条形图/数据对比 | 柱状图 | +| 折线图/趋势图/时序数据 | 折线图 | + +--- + +## 关联参考 + +- 画板查询/创作/修改/渲染写入:[`../../lark-whiteboard/SKILL.md`](../../lark-whiteboard/SKILL.md) diff --git a/.agents/skills/lark-doc/references/lark-doc-xml.md b/.agents/skills/lark-doc/references/lark-doc-xml.md new file mode 100644 index 0000000..9dff841 --- /dev/null +++ b/.agents/skills/lark-doc/references/lark-doc-xml.md @@ -0,0 +1,169 @@ +基于 HTML 子集的 XML 格式描述飞书文档内容。 + +# 一、标准 HTML 标签 +p, h1-h9, ul, ol, li, table, thead, tbody, tr, th, td, blockquote, pre, code, hr, img, b, em, u, del, a, br, span 语义不变 + +# 二、扩展标签速查表 +## 块级标签 +|标签|说明|关键属性| +|-|-|-| +| `` | 文档标题(每篇唯一)| `align` | +| `<checkbox>` | 待办项| `done="true"\|"false"` | + +## 容器标签 +|标签|说明|关键属性| +|-|-|-| +| `<callout>` | 高亮框,子块仅支持文本、标题、列表、待办、引用 | `emoji`(默认 bulb), `background-color`, `border-color`, `text-color` | +| `<grid>` + `<column>` | 分栏布局,各列 width-ratio 之和为 1 | `width-ratio` | +| `<whiteboard>` | 嵌入画板 | `type`: `mermaid` \| `plantuml` \| `blank` | +| `<pre>` | (代码块,内含 `code`)| `lang`, `caption` | +| `<figure>` | 视图容器 | `view-type` | +| `<bookmark>` | 书签链接 | `<bookmark name="标题" href="https://..."></bookmark>`,必传 name 和 href | + +## 行内组件 +| 标签 | 说明 | 关键属性 | +|-|-|-| +| `<cite type="user">` | @人 | `<cite type="user" user-id="userID"></cite>` | +| `<cite type="doc">` | @文档 | `<cite type="doc" doc-id="docx_token"></cite>` | +| `<latex>` | 行内公式 | `<latex>E = mc^2</latex>` | +| `<img>` | 图片(可独立成块或内联) | `<img width="800" height="600" caption="说明" name="图.png" href="http 或 https"/>` | +| `<source>` | 文件附件(可独立成块或内联) | `<source name="报告.pdf"/>` | +| `<a type="url-preview">` | 预览卡片 | `<a type="url-preview" href="...">标题</a>` | +| `<button>` | 操作按钮 | `background-color`、`src`,必须包含 `action=OpenLink\|DuplicatePage\|FollowPage` | +| `<time>` | 提醒 | 必包含 `expire-time`、`notify-time`(毫秒时间戳)、`should-notify=true\|false` | + +## 文本块通用属性 +- `align` — `"left"`|`"center"`|`"right"`(适用于 p / h1-h9 / li / checkbox) +- 有序列表项用 `seq="auto"` 自动编号 + +# 三、资源块 + +文档中可嵌入外部资源块(属于容器标签的特殊形式),需要额外语法创建: + +- `<img>` — `<img href="https://..."/>` 上传网络图片 +- `<whiteboard>` — `<whiteboard type="blank"></whiteboard>` 空白;`<whiteboard type="mermaid|plantuml">内容</whiteboard>` 带内容; +- `<sheet>` — `<sheet type="blank"></sheet>` 空白;`<sheet sheet-id="SID" token="TOKEN"></sheet>` 复制已有 +- `<task>` — `<task task-id="GUID"></task>`,必传 task-id(任务 guid) +- `<chat_card>` — `<chat_card chat-id="CHAT_ID"></chat_card>`,必传 chat-id +- bitable、base_ref、synced_reference、synced_source、okr — 不可创建,仅支持移动 + +# 四、块级复制与移动 + +## 移动(block_move_after) +支持**所有**块类型(块级标签、容器标签、行内组件、资源块),使用 `docs +update --command block_move_after --block-id "<锚点>" --src-block-ids "id1,id2"`。 + +## 复制(block_copy_insert_after) +- **基础标签**(块级标签、容器标签、行内组件):均支持复制 +- **资源块**:仅 img、source、whiteboard、sheet、chat_card 支持复制;task、bitable、base_ref、synced_reference、synced_source、okr 不支持复制 + +使用 `docs +update --command block_copy_insert_after --block-id "<锚点>" --src-block-ids "id1,id2"`。 + +> 详见 [lark-doc-update.md](lark-doc-update.md)。 + +# 五、补充规则 + +## 富文本样式嵌套顺序 +- 行内样式标签必须按以下固定顺序嵌套(外 → 内),关闭顺序严格反转:`<a> → <b> → <em> → <del> → <u> → <code> → <span> → 文本内容` + +## 列表分组 +- 连续同类型列表项自动合并为一个 `<ul>` 或 `<ol>` +- 嵌套子列表放在 `<li>` 内部 +- 新增列表项必须包在 `<ul>` 或 `<ol>` 内: + ```xml + <ul> + <li>第一项</li> + <li>第二项</li> + </ul> + ``` + + +## 表格扩展 +标准 HTML table 结构不变,扩展点: +- `<colgroup>` / `<col>` 定义列宽,紧跟 `<table>` 之后:`<col span="2" width="100"/>` +- `<th>` / `<td>` 增加 `background-color` 和 `vertical-align`(top | middle | bottom) +- 有表头时第一行在 `<thead>` 用 `<th>`,其余在 `<tbody>` 用 `<td>` +- 合并单元格仅起始格输出 `colspan` / `rowspan`,被合并的格不出现 + +# 六、美化系统 +- 颜色优先使用命名色,也可写 `rgb(r,g,b)` / `rgba(r,g,b,a)`。**基础色(7 色)**:red, orange, yellow, green, blue, purple, gray + | 属性 | 支持的命名色 | + |-|-| + | 文字颜色 `<span text-color>` | 基础色 | + | 高亮框字色 `<callout text-color>` | 基础色 | + | 高亮框边框 `<callout border-color>` | 基础色 | + | 文字背景 `<span background-color>` | 基础色 + `light-{色}` + `medium-gray` | + | 高亮框填充 `<callout background-color>` | `gray` + `light-{色}` + `medium-{色}` | + | 单元格背景 `<th/td background-color>` | 同文字背景 | + | 按钮背景 `<button background-color>` | 同文字背景 | +- 常用 emoji: 💡(默认)✅❌⚠️📝❓❗👍❤️📌🏁⭐ + +# 七、**重要规则** +## 转义规则:标签本身 **禁止转义**,只有标签内部的文本内容才需要转义 + +**错误** ❌:`<p>内容</p>`(把标签也转义了) +**正确** ✅:`<p>A & B 的对比:1 < 2</p>`(标签保持原样,文本中的 `&` 和 `<` 才转义) + +转义字符表: +- `<` → `<` +- `>` → `>` +- `&` → `&` +- `\n`(换行符) → `<br/>` + + +# 八、完整示例 + +```xml +<title>文档标题 + +

一级标题

+ +

加粗文本绿色文本

+ + +

高亮框内容,子块仅支持文本/标题/列表/待办/引用

+
+ +已完成事项 +未完成事项 + + + +

左栏

+
+ +

右栏

+
+
+ +
+ + + +
表头表头
单元格单元格
+ +

+ +
  1. 第一项
  2. 第二项
+ +

链接标题

+ +

E = mc^2

+ +
fmt.Println("hello")
+ +
+ + + + + + + + + +引文标题 + + + + +``` \ No newline at end of file diff --git a/.agents/skills/lark-doc/references/style/lark-doc-create-workflow.md b/.agents/skills/lark-doc/references/style/lark-doc-create-workflow.md new file mode 100644 index 0000000..48bec7f --- /dev/null +++ b/.agents/skills/lark-doc/references/style/lark-doc-create-workflow.md @@ -0,0 +1,50 @@ +# 从零创作工作流 + +用户提供主题、需求或简要说明,需要生成一份新的飞书文档时,遵循本工作流。 + +## 核心方法论 — Code-Act Loop + +通过自适应的 **Code-Act Loop** 驱动文档创作,而非固定模板式的工作流。每次任务都循环执行: + +1. **Plan(规划)** — 根据用户目标和文档当前状态,评估下一步该做什么 +2. **Execute(执行)** — 运行相应的 `lark-cli docs` 命令,或 **spawn** Agent 子任务并行推进 +3. **Observe(观察)** — 检查命令输出,验证正确性,核查样式是否达标 +4. **Iterate(迭代)** — 如需调整,回到 Plan 继续循环 + +循环在文档达到质量标准且满足用户需求时结束。不要试图一次性产出完美内容——迭代打磨效果更好。根据用户实际需求灵活决定文档结构和版块,而不是套用固定模板。 + + +## 典型 Code-Act Loop 流程 + +### 第一波 — 规划与骨架(串行) + +1. 分析用户需求:受众、目的、范围 +2. 设计大纲——每个 h1/h2 章节至少规划 1 个非文本 block +3. `docs +create --api-version v2` **只建骨架**:标题 + 开头 `` + 各级标题 + 每节一句占位摘要 + - ⚠️ **不要**一次性把完整章节内容塞进 `--content`。超长 `--content` 容易触发字符/参数限制。 + - 完整内容留到第二波,由各 Agent 用 `docs +update --command append` 或 `block_insert_after` 分段写入。 + +### 第二波 — 内容撰写(并行 Agent) + +4. Spawn Agent 并行撰写各章节。每个 Agent 需收到: + - 文档 token、负责的章节范围、期望的 block 类型 + - `lark-doc-xml.md` 和 `lark-doc-style.md` 的完整路径(Agent 须先读取) + - 使用 `docs +update --command append` 或 `block_insert_after` 写入 + +### 第三波 — 整合审查 + 画板意图识别(串行) + +5. `docs +fetch --detail with-ids` 获取文档,审查整体效果 +6. 评估样式达标(富 block 密度、元素多样性、连续 `

` 数量) +7. **画板意图识别**:逐章节扫描,按 `lark-doc-style.md`「画板意图识别」表判断是否有段落适合用图表达。记录需要插图的章节及推荐的画板类型 + +### 第四波 — 润色与图表(并行 Agent) +8. Spawn Agent 定向改进:(结合 `lark-doc-style.md` 润色) + - **优先处理第三波识别出的画板需求**:简单图直接 ``,复杂图 spawn Agent 使用 **lark-whiteboard** skill + - 文字密集章节转为 ``/``/`` + - 主要章节间补充 `
` + - 本地图片使用 `docs +media-insert` 插入 + + +## Agent 子任务要求 + +Spawn Agent 时必须提供:文档 token、章节范围(标题/block ID)、`lark-doc-xml.md` 和 `lark-doc-style.md` 路径、具体的 `docs +update` command 和 `--block-id`。 diff --git a/.agents/skills/lark-doc/references/style/lark-doc-style.md b/.agents/skills/lark-doc/references/style/lark-doc-style.md new file mode 100644 index 0000000..321dfa2 --- /dev/null +++ b/.agents/skills/lark-doc/references/style/lark-doc-style.md @@ -0,0 +1,97 @@ +# 文档样式指南 + +创建或编辑文档时,必须遵循本指南,使用结构化 block 提升可读性和视觉层次。 + +## 一、核心原则 + +1. **结构优于文字**:能用结构化 block 表达的信息,不用纯文本段落 +2. **Front-load 结论**:文档以 `` 开头概括核心结论;每章节首段点明要旨 +3. **视觉节奏**:连续纯文本不超过 3 段;不同主题章节间用 `
` 分隔 +4. **最少惊讶**:同类信息使用同类元素,全篇风格统一 + +## 二、元素选择指南 + +涉及图表需求时,简单图用 `` 内嵌,复杂图使用 **lark-whiteboard** skill。 + +| 场景 | 推荐方案 | +|-|-| +| 核心结论 / 摘要 / 注意事项 | `` + emoji + 背景色 | +| 方案对比 / 优劣势 / Before vs After | `` 2 列分栏 | +| 3+ 属性的结构化数据 / 指标表 | `
` + 表头背景色 | +| 任务清单 / 检查项 | `` | +| 代码片段 | `
` |
+| 引用 / 公式 | `
` / `` | +| 操作入口 / 跳转链接 | `
`/`` + - **对第一波识别出的画板候选段落**:简单图直接 ``,复杂图 spawn Agent 使用 **lark-whiteboard** skill + - 添加流程图、对比分栏等富 block + +### 第三波 — 验证(串行) + +5. 获取更新后文档局部内容,重新检查样式指标 +6. 未达标则定向修正,向用户呈现结果 + +## Agent 子任务要求 + +Spawn Agent 时必须提供:文档 token、章节范围(标题/block ID)、`lark-doc-xml.md` 和 `lark-doc-style.md` 路径、具体的 `docs +update` command 和 `--block-id`。 + +**上下文节省提示**:Agent 如需在自己负责的章节内重新读取内容,优先用 `docs +fetch --api-version v2 --scope section --start-block-id <章节标题id>`(自动覆盖整节),或 `--scope range --start-block-id xxx --end-block-id yyy` 精确区间,只拉自己的章节,不要重复拉全文。 diff --git a/.agents/skills/lark-drive/SKILL.md b/.agents/skills/lark-drive/SKILL.md new file mode 100644 index 0000000..a23e097 --- /dev/null +++ b/.agents/skills/lark-drive/SKILL.md @@ -0,0 +1,328 @@ +--- +name: lark-drive +version: 1.0.0 +description: "飞书云空间:管理云空间中的文件和文件夹。上传和下载文件、创建文件夹、复制/移动/删除文件、查看文件元数据、管理文档评论、管理文档权限、订阅用户评论变更事件、修改文件标题(docx、sheet、bitable、file、folder、wiki);也负责把本地 Word/Markdown/Excel/CSV 以及 Base 快照(.base)导入为飞书在线云文档(docx、sheet、bitable)。当用户需要上传或下载文件、整理云空间目录、查看文件详情、管理评论、管理文档权限、修改文件标题、订阅用户评论变更事件,或要把本地文件导入成新版文档、电子表格、多维表格/Base 时使用。" +metadata: + requires: + bins: ["lark-cli"] + cliHelp: "lark-cli drive --help" +--- + +# drive (v1) + +**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),其中包含认证、权限处理** + +> **导入分流规则:** 如果用户要把本地 Excel / CSV / `.base` 快照导入成 Base / 多维表格 / bitable,必须优先使用 `lark-cli drive +import --type bitable`。不要先切到 `lark-base`;`lark-base` 只负责导入完成后的表内操作。 + +## 快速决策 + +- 用户要**搜文档 / Wiki / 电子表格 / 多维表格 / 云空间对象**,优先使用 `lark-cli drive +search`。自然语言里"最近我编辑过的"、"我创建的"、"最近一周我打开过的 xxx"、"某人创建的 docx" 等直接映射到扁平 flag,避免手写嵌套 JSON。老的 `docs +search` 进入维护期、后续会下线,不要新增对它的依赖。 +- 用户要把本地 `.xlsx` / `.csv` / `.base` 导入成 Base / 多维表格 / bitable,第一步必须使用 `lark-cli drive +import --type bitable`。 +- 用户要把本地 `.md` / `.docx` / `.doc` / `.txt` / `.html` 导入成在线文档,使用 `lark-cli drive +import --type docx`。 +- 用户要在 Drive 里上传、创建、读取、覆盖更新**原生 `.md` 文件**(不是导入成 docx),切到 [`lark-markdown`](../lark-markdown/SKILL.md)。 +- 用户要把本地 `.xlsx` / `.xls` / `.csv` 导入成电子表格,使用 `lark-cli drive +import --type sheet`。 +- 用户要在云空间里新建文件夹,优先使用 `lark-cli drive +create-folder`。 +- 用户要把本地文件上传到知识库 / 文档库里的某个 wiki 节点下时,仍然使用 `lark-cli drive +upload --wiki-token `;不要误切到 `wiki` 域命令。 +- `lark-base` 只负责导入完成后的 Base 内部操作(表、字段、记录、视图),不要在“本地文件 -> Base”这一步提前切到 `lark-base`。 + +## 修改标题 +- 使用 `drive files patch` 命令,通过new_title字段可以修改标题,支持 docx、sheet、bitable、file、wiki、folder 类型 + +## 核心概念 + +### 文档类型与 Token + +飞书开放平台中,不同类型的文档有不同的 URL 格式和 Token 处理方式。在进行文档操作(如添加评论、下载文件等)时,必须先获取正确的 `file_token`。 + +### 文档 URL 格式与 Token 处理 + +| URL 格式 | 示例 | Token 类型 | 处理方式 | +|----------|---------------------------------------------------------|-----------|----------| +| `/docx/` | `https://example.larksuite.com/docx/doxcnxxxxxxxxx` | `file_token` | URL 路径中的 token 直接作为 `file_token` 使用 | +| `/doc/` | `https://example.larksuite.com/doc/doccnxxxxxxxxx` | `file_token` | URL 路径中的 token 直接作为 `file_token` 使用 | +| `/wiki/` | `https://example.larksuite.com/wiki/wikcnxxxxxxxxx` | `wiki_token` | ⚠️ **不能直接使用**,需要先查询获取真实的 `obj_token` | +| `/sheets/` | `https://example.larksuite.com/sheets/shtcnxxxxxxxxx` | `file_token` | URL 路径中的 token 直接作为 `file_token` 使用 | +| `/drive/folder/` | `https://example.larksuite.com/drive/folder/fldcnxxxx` | `folder_token` | URL 路径中的 token 作为文件夹 token 使用 | + +### Wiki 链接特殊处理(关键!) + +知识库链接(`/wiki/TOKEN`)背后可能是云文档、电子表格、多维表格等不同类型的文档。**不能直接假设 URL 中的 token 就是 file_token**,必须先查询实际类型和真实 token。 + +#### 处理流程 + +1. **使用 `wiki.spaces.get_node` 查询节点信息** + ```bash + lark-cli wiki spaces get_node --params '{"token":"wiki_token"}' + ``` + +2. **从返回结果中提取关键信息** + - `node.obj_type`:文档类型(docx/doc/sheet/bitable/slides/file/mindnote) + - `node.obj_token`:**真实的文档 token**(用于后续操作) + - `node.title`:文档标题 + +3. **根据 `obj_type` 使用对应的 API** + + | obj_type | 说明 | 使用的 API | + |----------|------|-----------| + | `docx` | 新版云文档 | `drive file.comments.*`、`docx.*` | + | `doc` | 旧版云文档 | `drive file.comments.*` | + | `sheet` | 电子表格 | `sheets.*` | + | `bitable` | 多维表格 | `bitable.*` | + | `slides` | 幻灯片 | `drive.*` | + | `file` | 文件 | `drive.*` | + | `mindnote` | 思维导图 | `drive.*` | + +#### 查询示例 + +```bash +# 查询 wiki 节点 +lark-cli wiki spaces get_node --params '{"token":"wiki_token"}' +``` + +返回结果示例: +```json +{ + "node": { + "obj_type": "docx", + "obj_token": "xxxx", + "title": "标题", + "node_type": "origin", + "space_id": "12345678910" + } +} +``` + +### 资源关系 + +``` +Wiki Space (知识空间) +└── Wiki Node (知识库节点) + ├── obj_type: docx (新版文档) + │ └── obj_token (真实文档 token) + ├── obj_type: doc (旧版文档) + │ └── obj_token (真实文档 token) + ├── obj_type: sheet (电子表格) + │ └── obj_token (真实文档 token) + ├── obj_type: bitable (多维表格) + │ └── obj_token (真实文档 token) + └── obj_type: file/slides/mindnote + └── obj_token (真实文档 token) + +Drive Folder (云空间文件夹) +└── File (文件/文档) + └── file_token (直接使用) +``` + +### 常见操作 Token 需求 + +| 操作 | 需要的 Token | 说明 | +|------|-------------|------| +| 读取文档内容 | `file_token` / 通过 `docs +fetch --api-version v2` 自动处理 | `docs +fetch` 支持直接传入 URL | +| 添加局部评论(划词评论) | `file_token` | 传 `--block-id` 时,`drive +add-comment` 会创建局部评论;`docx` 支持文本定位或 block_id,`slides` 仅支持 block_id,且都支持最终解析到对应类型的 wiki URL | +| 添加全文评论 | `file_token` | 不传 `--block-id` 时,`drive +add-comment` 默认创建全文评论;支持 `docx`、旧版 `doc` URL,以及最终解析为 `doc`/`docx` 的 wiki URL | +| 下载文件 | `file_token` | 从文件 URL 中直接提取 | +| 上传文件 | `folder_token` / `wiki_node_token` | 目标位置的 token | +| 列出文档评论 | `file_token` | 同添加评论 | + +### 评论能力边界(关键!) + +- `drive +add-comment` 支持两种模式。 +- 全文评论:未传 `--block-id` 时默认启用,也可显式传 `--full-comment`;支持 `docx`、旧版 `doc` URL,以及最终解析为 `doc`/`docx` 的 wiki URL。 +- 局部评论:传 `--block-id` 时启用;仅支持 `docx`,以及最终解析为 `docx` 的 wiki URL。block ID 可通过 `docs +fetch --api-version v2 --detail with-ids` 获取。 +- `drive +add-comment` 的 `--content` 需要传 `reply_elements` JSON 数组字符串,例如 `--content '[{"type":"text","text":"正文"}]'`。 +- `slides` 评论要求显式传 `--block-id !`;CLI 会将其拆分后写入 `anchor.block_id` 和 `anchor.slide_block_type`。其中 `` 是 PPT XML 协议中的元素 `id`;不支持 `--selection-with-ellipsis` 和 `--full-comment`。 + +- 评论写入内容(添加评论、回复评论、编辑回复)里的文本不能直接出现 `<`、`>`;提交前必须先转义:`<` -> `<`,`>` -> `>`。 +- 使用 `drive +add-comment` 时,shortcut 会对 `type=text` 的文本元素自动做上述转义兜底;如果直接调用 `drive file.comments create_v2`、`drive file.comment.replys create`、`drive file.comment.replys update`,则需要在请求里自行传入已转义的内容。 +- 如果 wiki 解析后不是 `doc`/`docx`/`sheet`/`slides`,不要用 `+add-comment`。 +- 如果需要更底层地直接调用评论 V2 协议,再走原生 API:先执行 `lark-cli schema drive.file.comments.create_v2`,再执行 `lark-cli drive file.comments create_v2 ...`。全文评论省略 `anchor`,局部评论传 `anchor.block_id`。 + +### 评论查询与统计口径(关键!) + +**强制规则**:`drive file.comments list` 默认必须传 `is_solved:false`,即仅查询未解决评论。即使用户说“所有评论”“全部评论”“把评论都列出来”,只要没有明确提到要包含已解决评论,仍然按默认口径查询未解决评论。仅当用户明确要求包含已解决评论时,才可省略 `is_solved` 参数。 + +**正确示例:** + +```bash +# 默认查询:仅未解决评论(推荐) +lark-cli drive file.comments list --params '{"file_token": "xxx", "file_type": "docx", "is_solved": false}' + +# 查询所有评论(用户未明确要求包含已解决评论) +lark-cli drive file.comments list --params '{"file_token": "xxx", "file_type": "docx", "is_solved": false}' + +# 包含已解决评论(需用户明确要求) +lark-cli drive file.comments list --params '{"file_token": "xxx", "file_type": "docx"}' +``` + +**错误示例:** + +```bash +# 不推荐:用户未明确要求但查询所有评论 +lark-cli drive file.comments list --params '{"file_token": "xxx", "file_type": "docx"}' +``` + +- 查询文档评论时,使用 `drive file.comments list`。 +- `drive file.comments list` 返回的 `items` 应理解为"评论卡片"列表,每个 `item` 对应用户界面里看到的一张评论卡片,而不是平铺的互动消息列表。 +- 服务端语义上,创建第一条评论时会同时创建该卡片里的第一条 reply;因此真正承载正文的是每个 `item.reply_list.replies`,其中第一条 reply 在用户视角下就是这张卡片里的"评论本身"。 +- 当用户要统计"评论数"或"评论卡片数"时,统计 `items` 的长度即可;如果是全量统计,则对所有评论分页返回的 `items` 长度累加。 +- 当用户要统计"回复数"时,按用户视角应排除每张评论卡片里的首条评论,统计口径是所有 `item.reply_list.replies` 的长度之和减去 `items` 的长度。 +- 当用户要统计"总互动数"时,统计所有 `item.reply_list.replies` 的长度之和即可;这个口径包含每张评论卡片里的首条评论。 +- 如果某个 `item.has_more=true`,说明该评论卡片下还有更多回复未包含在当前返回中;此时需要继续调用 `drive file.comment.replys list` 拉全后,再做全量回复数 / 总互动数统计。 + +### 评论业务特性与引导(关键!) + +#### 评论排序引导 +- 一个文档通常有多个评论,评论按 `create_time`(创建时间)排序。 +- **重要**:只有当用户明确提到"最新评论"、"最后评论"、"最早评论"时,才需要根据 `create_time` 进行排序: + - **必须先获取所有评论(处理分页拉完所有数据)**,不能只获取一页就排序 + - "最新评论" / "最后评论":按 `create_time` 降序排列,取第一条 + - "最早评论":按 `create_time` 升序排列,取第一条 +- 如果用户只说"第一条评论",直接使用 `drive file.comments list` 返回的第一条即可,不需要额外排序。 + +#### 评论回复限制 +- **添加评论回复前先检查是否存在以下限制** +- **全文评论不支持回复**:`is_whole=true` 的评论(全文评论)无法添加回复,遇到此类评论应提示用户"全文评论不支持回复"。 +- **已解决评论不支持回复**:`is_solved=true` 的评论无法添加回复,遇到此类评论应提示用户"该评论已被解决,无法回复"。 +- **注意**:当用户要回复某条评论但该评论因上述限制不能回复时,只提示不能回复即可,**不要自动帮用户找其他可以回复的评论**,避免不符合用户预期。 + +#### 批量查询与列表查询的选择 +- 使用 `drive file.comments batch_query` 是**已知评论 ID 后**的批量查询,需要传入具体的评论 ID 列表。 +- 使用 `drive file.comments list` 用于分页获取评论列表,适合统计评论总数、遍历所有评论,或获取"最新/最后 N 条评论"等场景。 + +#### Reaction / 表情场景 +- 遇到评论 / 回复上的 reaction(表情、各表情数量、谁点了什么、添加/删除表情)相关问题时,**先阅读 [lark-drive-reactions.md](../../skills/lark-drive/references/lark-drive-reactions.md) 了解如何使用**。 + +### 典型错误与解决方案 + +| 错误信息 | 原因 | 解决方案 | +|----------|------|----------| +| `not exist` | 使用了错误的 token | 检查 token 类型,wiki 链接必须先查询获取 `obj_token` | +| `permission denied` | 没有相关操作权限 | 引导用户检查当前身份对文档/文件是否有相应操作权限;如果需要,可以授予相应权限 | +| `invalid file_type` | file_type 参数错误 | 根据 `obj_type` 传入正确的 file_type(docx/doc/sheet/slides) | + +### 授权当前应用访问文档 + +当需要将文档权限授予**当前应用(bot)自身**时,先通过 bot info 接口获取应用的 open_id,再调用权限接口授权: + +```bash +# 1. 获取当前应用的 open_id +lark-cli api GET /open-apis/bot/v3/info --as bot +# 从返回值中取 bot.open_id + +# 2. 授权当前应用访问文档 +lark-cli drive permission.members create \ + --params '{"token":"","type":""}' \ + --data '{"member_type":"openid","member_id":"","perm":"view","type":"user"}' +``` + +> **注意**:此方式仅适用于需要授权给**当前应用**的场景。授权给其他用户时,直接使用对方的 open_id 即可,无需调用 bot info 接口。 + +`` 可选值:`doc`、`docx`、`sheet`、`bitable`、`file`、`folder`、`wiki`、`slides`。 + +## Shortcuts(推荐优先使用) + +Shortcut 是对常用操作的高级封装(`lark-cli drive + [flags]`)。有 Shortcut 的操作优先使用。 + +| Shortcut | 说明 | +|----------|------| +| [`+search`](references/lark-drive-search.md) | Search Lark docs, Wiki, and spreadsheet files with flat filter flags (preferred over `docs +search`). Natural-language-friendly: `--edited-since`, `--mine`, `--doc-types`, etc. | +| [`+upload`](references/lark-drive-upload.md) | Upload a local file to a Drive folder or wiki node | +| [`+create-folder`](references/lark-drive-create-folder.md) | Create a Drive folder, optionally under a parent folder, with bot auto-grant support | +| [`+download`](references/lark-drive-download.md) | Download a file from Drive to local | +| [`+status`](references/lark-drive-status.md) | Compare a local directory with a Drive folder by SHA-256 content hash; reports `new_local` / `new_remote` / `modified` / `unchanged` (read-only diff primitive for sync workflows). `--local-dir` 必须是 cwd 内的相对路径,越界路径 CLI 会直接拒绝;目标在 cwd 外时引导用户切换 agent 工作目录,不要私自 `cd` 绕过。 | +| [`+pull`](references/lark-drive-pull.md) | One-way **file-level** mirror of a Drive folder onto a local directory (Drive → local). Supports `--if-exists` (overwrite/skip) and `--delete-local` for orphan cleanup; the destructive `--delete-local` requires `--yes` and only unlinks regular files — empty local directories left behind by remote folder deletes are NOT pruned. Item-level failures exit non-zero (`error.type=partial_failure`) and skip the `--delete-local` pass to avoid half-synced state. `--local-dir` is bounded to cwd by CLI path validation; tell the user to switch the agent's working directory if the target is outside cwd. | +| [`+create-shortcut`](references/lark-drive-create-shortcut.md) | Create a shortcut to an existing Drive file in another folder | +| [`+add-comment`](references/lark-drive-add-comment.md) | Add a comment to doc/docx/sheet/slides, also supports wiki URL resolving to doc/docx/sheet/slides | +| [`+export`](references/lark-drive-export.md) | Export a doc/docx/sheet/bitable to a local file with limited polling; supports `--file-name` for local naming | +| [`+export-download`](references/lark-drive-export-download.md) | Download an exported file by file_token | +| [`+import`](references/lark-drive-import.md) | Import a local file to Drive as a cloud document (docx, sheet, bitable) | +| [`+move`](references/lark-drive-move.md) | Move a file or folder to another location in Drive | +| [`+delete`](references/lark-drive-delete.md) | Delete a Drive file or folder with limited polling for folder deletes | +| [`+push`](references/lark-drive-push.md) | Mirror a local directory onto a Drive folder (local → Drive). Supports `--if-exists` (overwrite/skip) and `--delete-remote` for one-way mirror sync; the destructive `--delete-remote` requires `--yes`. `--local-dir` is bounded to cwd by CLI path validation; tell the user to switch the agent's working directory if the source is outside cwd. | +| [`+task_result`](references/lark-drive-task-result.md) | Poll async task result for import, export, move, or delete operations | +| [`+apply-permission`](references/lark-drive-apply-permission.md) | Apply to the document owner for view/edit access (user-only; 5/day per document) | + +## API Resources + +```bash +lark-cli schema drive.. # 调用 API 前必须先查看参数结构 +lark-cli drive [flags] # 调用 API +``` + +> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。 + +### files + + - `copy` — 复制文件 + - `create_folder` — 新建文件夹 + - `list` — 获取文件夹下的清单 + - `patch` — 修改文件标题 + +### file.comments + + - `batch_query` — 批量获取评论 + - `create_v2` — 添加全文/局部(划词)评论 + - `list` — 分页获取文档评论 + - `patch` — 解决/恢复 评论 + +### file.comment.replys + + - `create` — 添加回复 + - `delete` — 删除回复 + - `list` — 获取回复 + - `update` — 更新回复 + +### permission.members + + - `auth` — + - `create` — 增加协作者权限 + - `transfer_owner` — + +### metas + + - `batch_query` — 获取文档元数据 + +### user + + - `remove_subscription` — 取消订阅用户、应用维度事件 + - `subscription` — 订阅用户、应用维度事件(本次开放评论添加事件) + - `subscription_status` — 查询用户、应用对指定事件的订阅状态 + +### file.statistics + + - `get` — 获取文件统计信息 + +### file.view_records + + - `list` — 获取文档的访问者记录 + +### file.comment.reply.reactions + + - `update_reaction` — 添加/删除 reaction + +## 权限表 + +| 方法 | 所需 scope | +|------|-----------| +| `files.copy` | `docs:document:copy` | +| `files.create_folder` | `space:folder:create` | +| `files.list` | `space:document:retrieve` | +| `files.patch` | `docx:document:write_only` | +| `file.comments.batch_query` | `docs:document.comment:read` | +| `file.comments.create_v2` | `docs:document.comment:create` | +| `file.comments.list` | `docs:document.comment:read` | +| `file.comments.patch` | `docs:document.comment:update` | +| `file.comment.replys.create` | `docs:document.comment:create` | +| `file.comment.replys.delete` | `docs:document.comment:delete` | +| `file.comment.replys.list` | `docs:document.comment:read` | +| `file.comment.replys.update` | `docs:document.comment:update` | +| `permission.members.auth` | `docs:permission.member:auth` | +| `permission.members.create` | `docs:permission.member:create` | +| `permission.members.transfer_owner` | `docs:permission.member:transfer` | +| `metas.batch_query` | `drive:drive.metadata:readonly` | +| `user.remove_subscription` | `docs:event:subscribe` | +| `user.subscription` | `docs:event:subscribe` | +| `user.subscription_status` | `docs:event:subscribe` | +| `file.statistics.get` | `drive:drive.metadata:readonly` | +| `file.view_records.list` | `drive:file:view_record:readonly` | +| `file.comment.reply.reactions.update_reaction` | `docs:document.comment:create` | diff --git a/.agents/skills/lark-drive/references/lark-drive-add-comment.md b/.agents/skills/lark-drive/references/lark-drive-add-comment.md new file mode 100644 index 0000000..ae009e4 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-add-comment.md @@ -0,0 +1,169 @@ + +# drive +add-comment + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +给文档、电子表格或飞书幻灯片添加评论。底层统一走 `/open-apis/drive/v1/files/:file_token/new_comments`(`create_v2`)接口;未指定位置时省略 `anchor` 创建全文评论,指定 `--block-id` 时传入 `anchor.block_id` 创建局部评论。支持直接传 docx URL/token、旧版 doc URL(仅全文评论)、sheet URL、slides URL,也支持传最终可解析为 doc/docx/sheet/slides 的 wiki URL。 + +## 命令 + +```bash +# 默认:未指定位置时添加全文评论 +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/docx/" \ + --content '[{"type":"text","text":"请补充发布说明"}]' + +# 也可以显式指定为全文评论;旧版 doc URL 仅支持全文评论 +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/doc/" \ + --full-comment \ + --content '[{"type":"text","text":"请补充旧版文档的背景信息"}]' + +# wiki 链接也可以,shortcut 会先解析到真实 doc/docx token +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/wiki/" \ + --content '[{"type":"text","text":"这里需要一段全文评论"}]' + +# 给 docx 文档的指定 block 添加局部评论(block_id 可通过 docs +fetch --api-version v2 --detail with-ids 获取) +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/docx/" \ + --block-id "" \ + --content '[{"type":"text","text":"请补充流程说明"}]' + +# wiki 链接也支持局部评论,但解析结果必须是 docx +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/wiki/" \ + --block-id "" \ + --content '[{"type":"text","text":"请补充更细的开发步骤"}]' + +# 组合文本、@用户、链接元素 +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/docx/" \ + --block-id "" \ + --content '[{"type":"text","text":"请 "},{"type":"mention_user","text":"ou_xxx"},{"type":"text","text":" 处理,参考 "},{"type":"link","text":"https://example.com"}]' + +# 给电子表格单元格添加评论(--block-id 格式为 !) +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/sheets/" \ + --block-id "!D6" \ + --content '[{"type":"text","text":"请检查此单元格数据"}]' + +# wiki 链接指向的 sheet 也支持 +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/wiki/" \ + --block-id "!A1" \ + --content '[{"type":"text","text":"请 "},{"type":"mention_user","text":"ou_xxx"},{"type":"text","text":" 确认"}]' + +# 给幻灯片元素添加评论(--block-id 格式为 !) +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/slides/" \ + --block-id "!" \ + --content '[{"type":"text","text":"请调整这个元素的位置"}]' + +# 例如:给整页 slide 添加评论 +# ... => --block-id slide!pkk +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/slides/" \ + --block-id "slide!pkk" \ + --content '[{"type":"text","text":"这一页需要补充过渡说明"}]' + +# 例如:给图片元素添加评论 +# => --block-id img!bPk +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/slides/" \ + --block-id "img!bPk" \ + --content '[{"type":"text","text":"这张图片建议换成更清晰的版本"}]' + +# 例如:给文本 shape 添加评论 +# ... => --block-id shape!bPq +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/slides/" \ + --block-id "shape!bPq" \ + --content '[{"type":"text","text":"这段文案可以再精简"}]' + +# wiki 链接指向的 slides 也支持 +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/wiki/" \ + --block-id "!" \ + --content '[{"type":"text","text":"这里需要补充说明"}]' + +# 传裸 token 时需要 --type 指定文档类型 +lark-cli drive +add-comment \ + --doc "" --type sheet \ + --block-id "!D6" \ + --content '[{"type":"text","text":"请检查"}]' + +lark-cli drive +add-comment \ + --doc "" --type docx \ + --content '[{"type":"text","text":"全文评论"}]' + +# 裸 token + 已知 block_id 的局部评论 +lark-cli drive +add-comment \ + --doc "" --type slides \ + --block-id "!" \ + --content '[{"type":"text","text":"slide block comment"}]' + +# 裸 token + 已知 block_id 的局部评论 +lark-cli drive +add-comment \ + --doc "" --type docx \ + --block-id "" \ + --content '[{"type":"text","text":"请 "},{"type":"mention_user","text":"ou_xxx"},{"type":"text","text":" 处理,参考 "},{"type":"link","text":"https://example.com"}]' + +# 如果需要更底层的原生 API,也可以直接调用 V2 协议 +lark-cli schema drive.file.comments.create_v2 + +lark-cli drive file.comments create_v2 \ + --params '{"file_token":""}' \ + --data '{"file_type":"docx","reply_elements":[{"type":"text","text":"全文评论内容"}]}' + +# 预览底层调用链 +lark-cli drive +add-comment \ + --doc "https://example.larksuite.com/docx/" \ + --block-id "" \ + --content '[{"type":"text","text":"请补充流程说明"}]' \ + --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--doc` | 是 | 文档 URL / token、sheet / slides URL,或可解析到 `doc`/`docx`/`sheet`/`slides` 的 wiki URL | +| `--type` | 裸 token 时必填 | 文档类型:`doc`、`docx`、`sheet`、`slides`。URL 输入时自动识别,无需传 | +| `--content` | 是 | `reply_elements` JSON 数组字符串。示例:`'[{"type":"text","text":"文本"},{"type":"mention_user","text":"ou_xxx"},{"type":"link","text":"https://example.com"}]'` | +| `--full-comment` | 否 | 显式指定创建全文评论;未传 `--block-id` 时也会默认走全文评论(不适用于 sheet) | +| `--block-id` | 局部评论时必填 | 目标块 ID,可通过 `docs +fetch --api-version v2 --detail with-ids` 获取。**Sheet 评论**:格式为 `!`(如 `a281f9!D6`) | + +## 行为说明 + +- **局部评论需要先获取 block ID**:先调用 `docs +fetch --api-version v2 --doc --detail with-ids` 获取带有 block ID 的文档内容,然后使用 `--block-id` 指定目标块。 +- 未传 `--block-id` 时,shortcut 默认创建**全文评论**;也可以显式传 `--full-comment`。全文评论支持 `docx`、旧版 `doc` URL,以及最终可解析为 `doc`/`docx` 的 wiki URL。 +- 传 `--block-id` 时,shortcut 创建**局部评论(划词评论)**;该模式支持 `docx`、`slides`,以及最终可解析为这些类型的 wiki URL。 +- **Sheet 评论**:当 `--doc` 为 sheet URL 或 wiki 解析为 sheet 时,使用 `--block-id "!"` 指定单元格(如 `a281f9!D6`);sheet 没有全文评论,`--full-comment` 不可用。 +- **Slide 评论**:当 `--doc` 为 slides URL、`--type slides`,或 wiki 解析为 slides 时,必须传 `--block-id "!"`。CLI 会将其拆分映射到 `anchor.block_id` / `anchor.slide_block_type`。此时 `--full-comment` 和 `--selection-with-ellipsis` 不可用。 +- **Slide 参数映射示例**:`--block-id` 由 PPT XML 元素类型和元素 `id` 组成。例如: + - `` 对应 `--block-id slide!pkk`,表示给整页评论。 + - `` 对应 `--block-id img!bPk`,表示给图片元素评论。 + - `...` 对应 `--block-id shape!bPq`,表示给文本 shape 评论。 + +- `--content` 接收结构化评论元素数组;`type` 支持 `text`、`mention_user`、`link`。为便于书写,`mention_user` / `link` 元素可以直接把用户 ID 或链接地址放在 `text` 字段中,shortcut 会转换成 OpenAPI 所需字段。 +- `type=text` 的评论文本不能直接包含 `<`、`>`;应优先传 `<`、`>`。shortcut 在发送前也会自动将 `<`、`>` 转义为 `<`、`>` 作为兜底。 +- **所有 `type=text` 元素的字符总和 ≤ 10000**(按字符算,中英文 / 符号一视同仁)。超过会被 shortcut 在发送前拒绝,并指出累计超长的元素。**拆成多个 text element 不能绕过这个上限**——上限是总额,不是每元素。需要更长内容就缩短或拆成多条评论。 +- 长度限制只对 `type=text` 生效,`mention_user` / `link` 不计入。 +- 局部评论走 `locate-doc` 时,内部固定使用 `limit=10`。 +- 当 `locate-doc` 命中多处时,shortcut 会中止并提示用户继续收窄 `--selection-with-ellipsis`,不支持手动指定匹配序号。 +- 写入评论前会自动生成符合 OpenAPI 定义的请求体: + - 统一接口:`POST /new_comments` + - 统一字段:`file_type` + `reply_elements` + - 全文评论:省略 `anchor` + - 局部评论:传入 `anchor.block_id` +- `--dry-run` 仅预览调用链和请求体,不会实际写入。 +- 如果需要更底层的控制,仍可改用 `lark-cli schema drive.file.comments.create_v2` + `lark-cli drive file.comments create_v2`。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。 + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-apply-permission.md b/.agents/skills/lark-drive/references/lark-drive-apply-permission.md new file mode 100644 index 0000000..700366a --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-apply-permission.md @@ -0,0 +1,77 @@ + +# drive +apply-permission(申请文档权限) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli drive +apply-permission`。 + +向云文档 **Owner** 发起 `view` 或 `edit` 权限申请。申请会以卡片形式推送给 Owner,由 Owner 决定是否通过。 + +> [!CAUTION] +> 这是**写入操作** —— 会给 Owner 发推送通知,不要批量或自动化调用。可以先用 `--dry-run` 预览。 + +## 身份要求 + +- **仅支持 `user` 身份**(使用 `user_access_token`),不支持 `bot` / `tenant_access_token`;shortcut 已在 `AuthTypes` 中强制限定为 `user`,使用 bot 会被拒。 +- 所需 scope:`docs:permission.member:apply`(若用户缺权限会走统一的 permission 错误路径)。 + +## 命令 + +```bash +# 通过 URL 申请(type 自动从 URL 推断) +lark-cli drive +apply-permission \ + --token "https://example.larksuite.com/docx/doxcnxxxxxxxxx" \ + --perm view \ + --remark "安全评估:需查看需求文档内容" --as user + +# 通过 bare token + 显式 --type +lark-cli drive +apply-permission \ + --token "doxcnxxxxxxxxx" --type docx \ + --perm edit --as user +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--token` | 是 | 目标文档 token 或完整 URL(`/docx/`、`/sheets/`、`/base/`、`/bitable/`、`/file/`、`/wiki/`、`/doc/`、`/mindnote/`、`/slides/` 路径里的 token 会被自动提取) | +| `--type` | 否 | 目标类型,可选值 `doc` / `sheet` / `file` / `wiki` / `bitable` / `docx` / `mindnote` / `slides`。传 URL 时可由 shortcut 自动推断;bare token 必须显式传 | +| `--perm` | 是 | 申请的权限,仅支持 `view` 或 `edit`(**不支持 `full_access`**,CLI 侧会直接拒绝) | +| `--remark` | 否 | 备注,会显示在权限申请卡片上 | +| `--dry-run` | 否 | 仅打印请求内容,不实际发送 | + +## 输出 + +API 成功时返回空 `data`(仅 `code: 0, msg: "success"`),对应 CLI 输出: + +```json +{ + "ok": true, + "identity": "user", + "data": {} +} +``` + +## 频率限制 + +- **应用级**:每应用每租户每分钟最多 10 次。 +- **用户级**:同一用户对**同一篇文档**一天不超过 5 次。 + +## 常见错误 + +| 错误码 | 含义 | CLI 处理 | +|---|---|---| +| `1063006` | 申请次数已达上限(5 次/日) | CLI 自动加 hint:`permission-apply quota reached: each user may request access on the same document at most 5 times per day` | +| `1063007` | 当前文档无法申请(如:文档禁用外部申请、申请者已拥有对应权限、目标类型不支持 apply) | CLI 自动加 hint:`this document does not accept a permission-apply request ... contact the owner directly` | +| `1063002` | 无操作权限(如该租户关闭了外部申请) | 由统一 permission 错误路径处理 | +| `1063004` | 用户所在组织无分享权限 | 由统一 permission 错误路径处理 | +| `1063005` | 资源已删除 | 需要确认目标文档/节点是否仍存在 | +| `1066001/1066002` | 服务端异常 / 并发冲突 | 稍后重试 | + +## 与 wiki URL 的关系 + +传入 `/wiki/` 时,shortcut 会直接用 `node_token` 作为路径参数并以 `type=wiki` 调用接口。如果需要先把 wiki 节点解析成 `obj_token`(例如想显式对底层 docx 申请),自行先调 `wiki spaces get_node` 拿 `obj_token + obj_type`,再用 bare token + `--type docx` 调本命令。 + +## 参考 + +- OpenAPI 端点:`POST /open-apis/drive/v1/permissions/:token/members/apply` diff --git a/.agents/skills/lark-drive/references/lark-drive-create-folder.md b/.agents/skills/lark-drive/references/lark-drive-create-folder.md new file mode 100644 index 0000000..cb67d34 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-create-folder.md @@ -0,0 +1,73 @@ +# drive +create-folder(创建云空间文件夹) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +在飞书云空间中创建一个新文件夹。该 shortcut 对原生 `drive files create_folder` 做了一层更适合日常使用的封装:`--folder-token` 可省略,此时会在调用者根目录创建;如果使用 `--as bot`,创建成功后 CLI 会尝试把新文件夹的可管理权限自动授予当前 CLI 用户。 + +## 命令 + +```bash +# 在根目录创建文件夹 +lark-cli drive +create-folder \ + --name "周报归档" + +# 在指定父文件夹下创建子文件夹 +lark-cli drive +create-folder \ + --folder-token \ + --name "2026-W16" + +# 预览底层调用 +lark-cli drive +create-folder \ + --folder-token \ + --name "分析资料" \ + --dry-run +``` + +## 返回值 + +成功后会返回一个 JSON 对象,常见字段包括: + +- `folder_token`:新建文件夹 token,可直接用于后续 `drive +move`、`drive +upload` 等命令 +- `url`:新建文件夹链接(如果接口返回) +- `name`:文件夹名称 +- `parent_folder_token`:父文件夹 token;为空字符串表示创建在根目录 +- `permission_grant`(可选):仅 `--as bot` 时返回,说明是否已自动为当前 CLI 用户授予可管理权限 + +> [!IMPORTANT] +> 如果文件夹是**以应用身份(bot)创建**的,如 `lark-cli drive +create-folder --as bot`,在创建成功后 CLI 会**尝试为当前 CLI 用户自动授予该文件夹的 `full_access`(可管理权限)**。 +> +> 以应用身份创建时,结果里会额外返回 `permission_grant` 字段,明确说明授权结果: +> - `status = granted`:当前 CLI 用户已获得该文件夹的可管理权限 +> - `status = skipped`:本地没有可用的当前用户 `open_id`,因此不会自动授权;可提示用户先完成 `lark-cli auth login`,再让 AI / agent 继续使用应用身份(bot)授予当前用户权限 +> - `status = failed`:文件夹已创建成功,但自动授权用户失败;会带上失败原因,并提示稍后重试或继续使用 bot 身份处理该文件夹 +> +> `permission_grant.perm = full_access` 表示该资源已授予“可管理权限”。 +> +> **不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。 + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--name` | 是 | 文件夹名称,不能为空,最长 256 字节 | +| `--folder-token` | 否 | 父文件夹 token;省略时表示在调用者根目录创建 | + +## 行为说明 + +- **根目录创建**:不传 `--folder-token` 时,shortcut 会向 API 显式传空字符串 `folder_token=""`,让后端按“根目录”语义创建 +- **bot 自动授权**:只有在 `--as bot` 时,结果才会额外带上 `permission_grant` +- **原生 API 仍可用**:如果用户明确要求按底层 API 字段调用,仍可继续使用 `lark-cli drive files create_folder` + +## 推荐场景 + +- 用户说“在云空间新建一个文件夹 / 目录”时,优先使用 `drive +create-folder` +- 用户给了父文件夹链接或 token,需要在其下继续分层建目录时,传 `--folder-token` +- 如果后续还要上传文件、移动文件、建子目录,优先复用返回值里的 `folder_token` + +> [!CAUTION] +> `drive +create-folder` 是**写入操作**,执行前必须确认用户意图。 + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-create-shortcut.md b/.agents/skills/lark-drive/references/lark-drive-create-shortcut.md new file mode 100644 index 0000000..f2a892b --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-create-shortcut.md @@ -0,0 +1,103 @@ + +# drive +create-shortcut + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +在目标文件夹中为一个现有 Drive 文件创建快捷方式。 + +## 命令 + +```bash +# 为普通文件创建快捷方式 +lark-cli drive +create-shortcut \ + --folder-token \ + --file-token \ + --type file + +# 为新版文档创建快捷方式 +lark-cli drive +create-shortcut \ + --folder-token \ + --file-token \ + --type docx + +# 为电子表格创建快捷方式 +lark-cli drive +create-shortcut \ + --folder-token \ + --file-token \ + --type sheet + +# 仅预览即将发起的请求,不真正执行 +lark-cli drive +create-shortcut \ + --folder-token \ + --file-token \ + --type docx \ + --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--folder-token` | 是 | 目标父文件夹 token | +| `--file-token` | 是 | 源文件 token,表示被引用的原始文件 | +| `--type` | 是 | 源文件类型,推荐值:`file`、`docx`、`doc`、`sheet`、`bitable`、`mindnote`、`slides` | + +## 输入规则 + +- 该 shortcut 的最小输入是 `--folder-token` + `--file-token` + `--type` +- CLI 层会把 `--file-token` 和 `--type` 组装为底层 API 所需的 `refer_entity` +- `--file-token` 必须是 Drive 文件 token,不要直接传 wiki 节点 token +- 如果来源是 `/wiki/...` 链接,必须先按 [`lark-drive`](../SKILL.md) 中的 wiki 解析流程拿到真实 `obj_token`,再创建快捷方式 +- 目标位置必须是云空间文件夹;这个 shortcut 不是“复制文件内容”,而是“在另一个文件夹里挂一个引用入口” + +## 类型说明 + +| 类型 | 说明 | +|------|------| +| `file` | 普通文件 | +| `docx` | 新版云文档 | +| `doc` | 旧版云文档 | +| `sheet` | 电子表格 | +| `bitable` | 多维表格 | +| `mindnote` | 思维笔记 | +| `slides` | 幻灯片 | + +## 行为说明 + +- 成功时会调用 `POST /open-apis/drive/v1/files/create_shortcut` +- 该 shortcut 继承通用能力,可配合 `--as user|bot|auto`、`--format`、`--jq`、`--dry-run` 使用 +- `--dry-run` 只输出请求方法、路径、身份和请求体预览,不会真正创建快捷方式 +- 这是写入操作;执行前应确认目标文件夹和源文件都准确无误 + +## 限制 + +- 该接口不支持并发调用 +- 调用频率上限为 5 QPS,且 10000 次/天 +- 不支持跨租户、跨地域创建快捷方式 +- 不支持跨品牌创建快捷方式 +- 如果目标父文件夹单层挂载数量超过限制,会返回 `1062507` + +## 权限要求 + +- 当前调用身份需要能访问源文件 +- 当前调用身份需要对目标文件夹有编辑权限 +- 如果权限不足,常见表现为 `1061004 forbidden` + +## 常见错误 + +| 错误码 / 错误信息 | 原因 | 处理建议 | +|------|------|------| +| `1061002 params error` | 缺少必填参数,或 `--file-token` / `--type` 组合无法构成有效源文件信息 | 检查 `--file-token`、`--type` 是否完整且匹配;如显式传了 `--folder-token`,再确认其值有效 | +| `1061003 not found` | 源文件或目标文件夹不存在 | 重新确认 token 是否正确、资源是否已删除 | +| `1061004 forbidden` | 对源文件没有访问权限,或对目标文件夹没有编辑权限 | 切换到有权限的身份,或先授予文档 / 文件夹权限 | +| `1061005 auth failed` | 身份类型或 access token 不正确 | 检查 `--as` 使用的身份及当前登录态 | +| `1061007 file has been delete` | 源文件已删除 | 确认原文件仍存在,再重新执行 | +| `1062507 parent node out of sibling num` | 目标文件夹单层挂载数超过上限 | 清理目标目录,或换一个父文件夹 | +| `1061045 resource contention occurred, please retry` | 平台内部资源争抢 | 稍后重试,不要并发重复调用 | +| `1064510 cross tenant and unit not support` | 跨租户或跨地域请求 | 改为在同租户、同地域范围内操作 | +| `1064511 cross brand not support` | 跨品牌请求 | 改为在同品牌环境内操作 | + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-delete.md b/.agents/skills/lark-drive/references/lark-drive-delete.md new file mode 100644 index 0000000..0048364 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-delete.md @@ -0,0 +1,79 @@ + +# drive +delete + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +删除云空间内的文件或文件夹。删除后资源会进入回收站。 + +> [!CAUTION] +> 这是**高风险写操作**。CLI 层要求显式传 `--yes`;如果用户已经明确要求删除且目标明确,直接执行并带上 `--yes`。 + +## 命令 + +```bash +# 删除普通文件 +lark-cli drive +delete \ + --file-token \ + --type file \ + --yes + +# 删除在线文档 +lark-cli drive +delete \ + --file-token \ + --type docx \ + --yes + +# 删除文件夹(异步操作,会自动有限轮询任务状态) +lark-cli drive +delete \ + --file-token \ + --type folder \ + --yes +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--file-token` | 是 | 需要删除的文件或文件夹 token | +| `--type` | 是 | 文件类型,可选值:`file`、`docx`、`bitable`、`doc`、`sheet`、`mindnote`、`folder`、`shortcut`、`slides` | +| `--yes` | 是 | 确认执行高风险删除操作 | + +## 行为说明 + +- **普通文件删除**:同步操作,成功时直接返回 `deleted=true` +- **文件夹删除**:异步操作,接口返回 `task_id`,shortcut 会先做有限轮询;如果在轮询窗口内完成,则直接返回成功结果 +- **轮询超时不是失败**:文件夹删除内置最多轮询 30 次、每次间隔 2 秒;如果轮询结束任务仍未完成,会返回 `task_id`、`status`、`ready=false`、`timed_out=true` 和 `next_command` +- **继续查询**:当看到 `next_command` 时,改用 `lark-cli drive +task_result --scenario task_check --task-id ` 继续查询 +- **状态值**:`task_check` 的服务端状态通常是 `success`、`fail`、`process` + +## 推荐续跑方式 + +```bash +# 第一步:先直接删除文件夹 +lark-cli drive +delete \ + --file-token \ + --type folder \ + --yes + +# 如果返回 ready=false / timed_out=true,再继续查 +lark-cli drive +task_result \ + --scenario task_check \ + --task-id +``` + +## 限制 + +- 该 shortcut 仅支持云空间文件或文件夹,不支持 wiki 文档 +- 该接口不支持并发调用 +- 调用频率上限为 5 QPS 且 10000 次/天 + +## 权限要求 + +- 删除文件时,调用身份需要满足以下其一: +- 是文件所有者,并且拥有该文件所在父文件夹的编辑权限 +- 不是文件所有者,但拥有该父文件夹的 owner 或 full access 权限 + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-download.md b/.agents/skills/lark-drive/references/lark-drive-download.md new file mode 100644 index 0000000..a7ce629 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-download.md @@ -0,0 +1,31 @@ + +# drive +download + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +从飞书云空间下载文件到本地。 + +## 命令 + +```bash +# 下载到指定路径 +lark-cli drive +download --file-token boxbc_xxx --output ./report.pdf + +# 只提供 token,默认保存为当前目录下同名文件 +lark-cli drive +download --file-token boxbc_xxx +``` + +## URL 解析 + +从飞书文件 URL 提取 token: + +``` +https://xxx.feishu.cn/drive/file/boxbc_xxx + ^^^^^^^^^ + file_token +``` + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-export-download.md b/.agents/skills/lark-drive/references/lark-drive-export-download.md new file mode 100644 index 0000000..42c4fdc --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-export-download.md @@ -0,0 +1,50 @@ + +# drive +export-download + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +根据导出任务产物的 `file_token` 下载本地文件。通常与 `drive +task_result --scenario export` 配合使用。 + +## 命令 + +```bash +# 使用服务端返回的文件名下载到当前目录 +lark-cli drive +export-download \ + --file-token "" + +# 下载到指定目录 +lark-cli drive +export-download \ + --file-token "" \ + --output-dir ./exports + +# 指定本地文件名 +lark-cli drive +export-download \ + --file-token "" \ + --file-name "weekly-report.pdf" \ + --output-dir ./exports + +# 允许覆盖 +lark-cli drive +export-download \ + --file-token "" \ + --overwrite +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--file-token` | 是 | 导出完成后的产物 token | +| `--file-name` | 否 | 覆盖默认文件名 | +| `--output-dir` | 否 | 本地输出目录,默认当前目录 | +| `--overwrite` | 否 | 覆盖已存在文件 | + +## 使用顺序 + +1. 用 `drive +export` 发起导出 +2. 如果返回 `ticket` / `next_command`,用 `drive +task_result --scenario export --ticket --file-token ` 继续查 +3. 查到 `file_token` 后,用 `drive +export-download` 下载 + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-export.md b/.agents/skills/lark-drive/references/lark-drive-export.md new file mode 100644 index 0000000..4805e73 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-export.md @@ -0,0 +1,119 @@ + +# drive +export + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +把 `doc` / `docx` / `sheet` / `bitable` 导出到本地文件。这个 shortcut 内置有限轮询: + +- 如果导出任务在轮询窗口内完成,会直接下载到本地目录 +- 如果轮询结束仍未完成,会返回 `ticket`、`ready=false`、`timed_out=true` 和 `next_command` +- 后续继续查结果时,改用 `drive +task_result --scenario export` +- 拿到 `file_token` 后,改用 `drive +export-download` + +## 命令 + +```bash +# 导出新版文档为 pdf,默认保存到当前目录 +lark-cli drive +export \ + --token "" \ + --doc-type docx \ + --file-extension pdf + +# 导出旧版文档为 docx +lark-cli drive +export \ + --token "" \ + --doc-type doc \ + --file-extension docx + +# 导出 docx 为 markdown +# 注意:markdown 只支持 docx,底层走 /open-apis/docs/v1/content +lark-cli drive +export \ + --token "" \ + --doc-type docx \ + --file-extension markdown + +# 导出电子表格为 xlsx +lark-cli drive +export \ + --token "" \ + --doc-type sheet \ + --file-extension xlsx \ + --output-dir ./exports + +# 指定本地文件名(会按导出格式自动补扩展名) +lark-cli drive +export \ + --token "" \ + --doc-type docx \ + --file-extension pdf \ + --file-name "weekly-report.pdf" \ + --output-dir ./exports + +# 导出电子表格或多维表格为 csv 时,必须传 sub_id +lark-cli drive +export \ + --token "" \ + --doc-type "" \ + --file-extension csv \ + --sub-id "" \ + --output-dir ./exports + +# 导出多维表格为 .base 快照(只支持 bitable) +lark-cli drive +export \ + --token "" \ + --doc-type bitable \ + --file-extension base \ + --output-dir ./exports + +# 允许覆盖已存在文件 +lark-cli drive +export \ + --token "" \ + --doc-type docx \ + --file-extension pdf \ + --overwrite +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--token` | 是 | 源文档 token | +| `--doc-type` | 是 | 源文档类型:`doc` / `docx` / `sheet` / `bitable` | +| `--file-extension` | 是 | 导出格式:`docx` / `pdf` / `xlsx` / `csv` / `markdown` / `base` | +| `--sub-id` | 条件必填 | 当 `sheet` / `bitable` 导出为 `csv` 时必填 | +| `--file-name` | 否 | 覆盖默认本地文件名;如未带扩展名,会按 `--file-extension` 自动补齐 | +| `--output-dir` | 否 | 本地输出目录,默认当前目录 | +| `--overwrite` | 否 | 覆盖已存在文件 | + +## 关键约束 + +- `markdown` 只支持 `docx` +- `base` 只支持 `bitable` +- `sheet` / `bitable` 导出为 `csv` 时必须带 `--sub-id` +- shortcut 内部固定有限轮询:最多 10 次,每次间隔 5 秒 +- 轮询超时不是失败;会返回 `ticket`、`timed_out=true` 和 `next_command`,供后续继续查询 + +## 推荐续跑方式 + +```bash +# 第一步:先尝试直接导出 +lark-cli drive +export \ + --token "" \ + --doc-type docx \ + --file-extension pdf \ + --file-name "weekly-report.pdf" + +# 如果返回 ready=false / timed_out=true,再继续查 +lark-cli drive +task_result \ + --scenario export \ + --ticket "" \ + --file-token "" + +# 查到 file_token 后下载 +lark-cli drive +export-download \ + --file-token "" \ + --file-name "weekly-report.pdf" \ + --output-dir ./exports +``` + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-import.md b/.agents/skills/lark-drive/references/lark-drive-import.md new file mode 100644 index 0000000..feb581f --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-import.md @@ -0,0 +1,154 @@ +# drive +import + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +将本地文件(如 Word、TXT、Markdown、Excel 等)导入并转换为飞书在线云文档(docx、sheet、bitable)。底层统一通过 `POST /open-apis/drive/v1/import_tasks` 接口创建导入任务,并在 shortcut 内做有限次数轮询 `GET /open-apis/drive/v1/import_tasks/:ticket`。 + +> [!IMPORTANT] +> 当用户说“把本地 Excel / CSV / `.base` 快照导入成 Base / 多维表格 / bitable 文档”时,第一步必须使用 `drive +import --type bitable`。 +> 这是 Drive 导入场景,不是 `lark-base` 的建表 / 写记录场景。 +> 只有导入完成并拿到新文档的 `token` / `url` 后,后续字段、记录、视图等表内操作才切换到 `lark-cli base +...`。 + +## 命令 + +```bash +# 导入 Word 为新版文档 (docx) +lark-cli drive +import --file ./report.docx --type docx +lark-cli drive +import --file ./legacy.doc --type docx + +# 导入 Markdown 为新版文档 (docx) +lark-cli drive +import --file ./README.md --type docx + +# 导入纯文本为新版文档 (docx) +lark-cli drive +import --file ./notes.txt --type docx + +# 导入 HTML 为新版文档 (docx) +lark-cli drive +import --file ./page.html --type docx + +# 导入 Excel 为电子表格 (sheet) +lark-cli drive +import --file ./data.xlsx --type sheet + +# 导入 Excel 97-2003 (.xls) 为电子表格 (sheet) +lark-cli drive +import --file ./legacy.xls --type sheet + +# 导入 CSV 为电子表格 (sheet) +lark-cli drive +import --file ./data.csv --type sheet + +# 导入 Excel 为多维表格 / Base (bitable) +lark-cli drive +import --file ./crm.xlsx --type bitable --name "客户台账" + +# 导入 .base 快照为多维表格 / Base (bitable)(文件不能超过 20MB) +lark-cli drive +import --file ./snapshot.base --type bitable --name "快照还原" + +# 导入到指定文件夹,并指定导入后的文件名 +lark-cli drive +import --file ./data.csv --type bitable --folder-token --name "导入数据表" + +# 预览底层调用链(上传 -> 创建任务 -> 轮询) +lark-cli drive +import --file ./README.md --type docx --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--file` | 是 | 本地文件路径,根据文件后缀名自动推断 `file_extension`;文件需满足对应格式的导入大小限制,超过 20MB 且仍在允许范围内时会自动切换分片上传 | +| `--type` | 是 | 导入目标云文档格式。可选值:`docx` (新版文档)、`sheet` (电子表格)、`bitable` (多维表格) | +| `--folder-token` | 否 | 目标文件夹 token,不传则请求中的 `point.mount_key` 为空字符串,Import API 会将其解释为导入到云空间根目录 | +| `--name` | 否 | 导入后的在线云文档名称,不传默认使用本地文件名去掉扩展名后的结果 | + +## 行为说明 + +- **完整执行流程**:此 shortcut 内部封装了完整流程: + 1. 自动上传源文件获取 `file_token`: + - 20MB 及以下:调用素材上传接口 `POST /open-apis/drive/v1/medias/upload_all` + - 超过 20MB:自动切换为分片上传 `upload_prepare -> upload_part -> upload_finish` + 2. 调用 `import_tasks` 接口发起导入任务,自动根据本地文件提取扩展名并构造挂载点(`mount_point`)参数 + 3. 自动轮询查询导入任务状态;如果在内置轮询窗口内完成,则直接返回导入结果;如果仍未完成,则返回 `ticket`、当前状态和后续查询命令 +- **默认根目录行为**:不传 `--folder-token` 时,shortcut 会保留空的 `point.mount_key`,Lark Import API 会将其视为“导入到调用者根目录”。 + +### 支持的文件类型转换 + +本地文件扩展名与目标云文档类型的对应关系如下: + +| 本地文件扩展名 | 可导入为 | 说明 | +|--------------|---------|------| +| `.docx`, `.doc` | `docx` | Microsoft Word 文档 | +| `.txt` | `docx` | 纯文本文件 | +| `.md`, `.markdown`, `.mark` | `docx` | Markdown 文档 | +| `.html` | `docx` | HTML 文档 | +| `.xlsx` | `sheet`, `bitable` | Microsoft Excel 表格 | +| `.xls` | `sheet` | Microsoft Excel 97-2003 表格 | +| `.csv` | `sheet`, `bitable` | CSV 数据文件 | +| `.base` | `bitable` | 多维表格快照文件 | + +> [!IMPORTANT] +> 用户口头说的 “Base” / “多维表格” / “bitable”,在命令里统一对应 `--type bitable`。 +> +> 文件扩展名与目标文档类型必须匹配,否则会返回验证错误: +> - 文档类文件(.docx, .doc, .txt, .md, .html)**只能**导入为 `docx` +> - `.xlsx` / `.csv` 文件**只能**导入为 `sheet` 或 `bitable` +> - `.xls` 文件**只能**导入为 `sheet` +> - `.base` 文件**只能**导入为 `bitable` +> - 例如:`.csv` 文件不能导入为 `docx`,`.md` 文件不能导入为 `sheet` + +> [!IMPORTANT] +> 如果在线文档是**以应用身份(bot)导入创建**的,如 `lark-cli drive +import --as bot`,当某次结果**已经返回最终在线文档目标**后,CLI 会**尝试为当前 CLI 用户自动授予该资源的 `full_access`(可管理权限)**。 +> +> 这个自动授权有两种触发时机: +> - `drive +import` 的内置轮询窗口内已经完成,直接在 `+import` 中进行自动授权 +> - `drive +import` 先返回 `ready=false` / `timed_out=true`,之后你再执行 `lark-cli drive +task_result --scenario import --ticket `,当该查询第一次拿到最终在线文档目标时会自动授权 +> +> 只有在已经拿到最终在线文档目标的那次结果里,才会返回 `permission_grant` 字段,明确说明授权结果: +> - `status = granted`:当前 CLI 用户已获得该导入结果的可管理权限 +> - `status = skipped`:本地没有可用的当前用户 `open_id`,或当前结果还没有可授权目标,因此不会自动授权;可提示用户先完成 `lark-cli auth login`,再让 AI / agent 继续使用应用身份(bot)授予当前用户权限 +> - `status = failed`:导入已成功返回最终在线文档,但自动授权用户失败;会带上失败原因,并提示稍后重试或继续使用 bot 身份处理该文档 +> +> `permission_grant.perm = full_access` 表示该资源已授予“可管理权限”。 +> +> **不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。 + +### 文件大小限制 + +除扩展名与目标类型匹配外,`drive +import` 还会在本地上传前校验格式级大小限制: + +| 本地文件扩展名 | 导入目标 | 大小上限 | +|--------------|---------|---------| +| `.docx`, `.doc` | `docx` | 600MB | +| `.txt` | `docx` | 20MB | +| `.md`, `.mark`, `.markdown` | `docx` | 20MB | +| `.html` | `docx` | 20MB | +| `.xlsx` | `sheet`, `bitable` | 800MB | +| `.csv` | `sheet` | 20MB | +| `.csv` | `bitable` | 100MB | +| `.xls` | `sheet` | 20MB | +| `.base` | `bitable` | 20MB | + +- 如果文件超出对应上限,shortcut 会在真正上传前直接返回验证错误。 +- “超过 20MB 自动切换分片上传”只表示上传链路会切到 multipart,不代表所有格式都允许导入超过 20MB 的文件。 + +- 若导入任务执行失败,会返回失败时的 `job_status` 及错误信息。 +- 若内置轮询超时但任务仍在处理中,shortcut 会成功返回,并带上: + - `ready=false` + - `timed_out=true` + - `next_command`:可直接复制执行的后续查询命令,例如 `lark-cli drive +task_result --scenario import --ticket ` +- 若使用 `--as bot` 且内置轮询窗口内已经拿到最终在线文档,输出还会额外带上 `permission_grant`,用于说明是否已自动为当前 CLI 用户授予可管理权限。 +- 若使用 `--as bot` 但当前只返回 `ready=false`,此时还不会返回 `permission_grant`;应继续执行返回值里的 `next_command`,等 `drive +task_result --scenario import` 拿到最终文档后再触发自动授权。 +- 如果文件扩展名不被支持,执行时将抛出验证错误。 + +### 超时后的继续查询 + +当 `+import` 的内置轮询窗口结束但任务尚未完成时,使用返回结果中的 `ticket` 继续查询: + +```bash +lark-cli drive +task_result --scenario import --ticket +``` + +如果这里最终返回 `ready=true` 且使用的是 `--as bot`,结果还会额外带上 `permission_grant`,用于说明是否已自动为当前 CLI 用户授予可管理权限。 + +> [!CAUTION] +> `drive +import` 是**写入操作** —— 执行前必须确认用户意图。 + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-move.md b/.agents/skills/lark-drive/references/lark-drive-move.md new file mode 100644 index 0000000..1a11bc7 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-move.md @@ -0,0 +1,120 @@ + +# drive +move + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +将文件或文件夹移动到用户云空间的其他位置。 + +## 与 `wiki +move` 的区别 + +- `drive +move` 只处理 **Drive 文件夹树内部** 的位置调整,目标位置用 `--folder-token` 表示 +- `wiki +move` 处理的是 **Wiki 知识空间 / 页面层级**:要么移动已有 Wiki 节点,要么把 Drive 文档迁入 Wiki +- 如果用户说“移动到某个文件夹”“移动到我的空间根目录”,应使用 `drive +move` +- 如果用户说“移动到某个知识库 / 页面下”“迁入 Wiki / 知识空间”,应使用 `wiki +move` +- 如果用户说“移动到我的文档库 / 我的知识库 / 个人知识库 / my_library”,不要使用 `drive +move`;先按 Wiki 目标处理 +- `我的文档库` 不是 Drive root folder,也不是 `--folder-token` 省略后的默认目的地 +- `drive +move` 不支持 wiki 文档;如果目标是 Wiki,不要尝试用 `drive +move` 代替 + +## 不要误用到 `我的文档库` + +下面几种说法都**不应该**触发 `drive +move`: + +- `移动到我的文档库` +- `放到我的知识库` +- `迁入个人知识库` +- `move to My Document Library` + +这些目标都应该先走 Wiki 解析流程: + +```bash +lark-cli wiki spaces get --params '{"space_id":"my_library"}' +``` + +拿到真实 `space_id` 后,再改用 `wiki +move`。不要因为 `drive +move` 可以省略 `--folder-token` 就把它当作“我的文档库”的近似目标。 + +## 命令 + +```bash +# 移动文件到指定文件夹 +lark-cli drive +move \ + --file-token \ + --type file \ + --folder-token + +# 移动文档到指定文件夹 +lark-cli drive +move \ + --file-token \ + --type docx \ + --folder-token + +# 移动文件夹(异步操作,会自动有限轮询任务状态) +lark-cli drive +move \ + --file-token \ + --type folder \ + --folder-token + +# 移动到根文件夹(不指定 --folder-token) +lark-cli drive +move \ + --file-token \ + --type file +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--file-token` | 是 | 需要移动的文件或文件夹 token | +| `--type` | 是 | 文件类型,可选值:`file` (普通文件)、`docx` (新版文档)、`bitable` (多维表格)、`doc` (旧版文档)、`sheet` (电子表格)、`mindnote` (思维笔记)、`folder` (文件夹)、`slides` (幻灯片) | +| `--folder-token` | 否 | 目标文件夹 token,不指定则移动到根文件夹 | + +## 文件类型说明 + +| 类型 | 说明 | +|------|------| +| `file` | 普通文件 | +| `docx` | 新版云文档 | +| `doc` | 旧版云文档 | +| `sheet` | 电子表格 | +| `bitable` | 多维表格 | +| `mindnote` | 思维笔记 | +| `slides` | 幻灯片 | +| `folder` | 文件夹(移动文件夹是异步操作) | + +## 行为说明 + +- **普通文件移动**:同步操作,立即完成 +- **文件夹移动**:异步操作,接口返回 `task_id`,shortcut 会先做有限轮询;如果在轮询窗口内完成,则直接返回成功结果 +- **轮询超时不是失败**:文件夹移动内置最多轮询 30 次、每次间隔 2 秒;如果轮询结束任务仍未完成,会返回 `task_id`、`status`、`ready=false`、`timed_out=true` 和 `next_command` +- **继续查询**:当看到 `next_command` 时,改用 `lark-cli drive +task_result --scenario task_check --task-id ` 继续查询 +- **目标文件夹**:如果不指定 `--folder-token`,文件将被移动到用户的根文件夹("我的空间") +- **不要混淆产品概念**:这里的“根文件夹 / 我的空间”仅属于 Drive 文件夹树,不等于 Wiki 的“我的文档库” +- **权限要求**:需要被移动文件的可管理权限、被移动文件所在位置的编辑权限、目标位置的编辑权限 + +## 推荐续跑方式 + +```bash +# 第一步:先直接移动文件夹 +lark-cli drive +move \ + --file-token \ + --type folder \ + --folder-token + +# 如果返回 ready=false / timed_out=true,再继续查 +lark-cli drive +task_result \ + --scenario task_check \ + --task-id +``` + +## 限制 + +- 被移动的文件不支持 wiki 文档 +- 该接口不支持并发调用 +- 调用频率上限为 5 QPS 且 10000 次/天 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。 + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-pull.md b/.agents/skills/lark-drive/references/lark-drive-pull.md new file mode 100644 index 0000000..5ef290f --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-pull.md @@ -0,0 +1,113 @@ + +# drive +pull + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +把飞书云空间的某个文件夹**单向、文件级**镜像到本地目录(Drive → 本地)。命令递归列出 `--folder-token` 下所有 `type=file` 的文件,逐一下载到 `--local-dir` 对应的相对路径,子文件夹自动复刻为本地目录。 + +> ⚠️ **不是 directory-level mirror**:`--delete-local` 只删除本地"多余"的常规文件,不删除空目录。如果云端把整个子文件夹删了,对应的本地子目录会留空(里面的文件被清掉,目录本身保留);想精确同步目录结构请自己 `rmdir` 处理空壳。 + +输出按"动作"分类: + +| 字段 | 含义 | +|------|------| +| `summary.downloaded` | 成功下载的文件数 | +| `summary.skipped` | 按 `--if-exists=skip` 跳过的文件数 | +| `summary.failed` | 下载或写盘失败的文件数 | +| `summary.deleted_local` | 启用 `--delete-local --yes` 时删除的本地文件数 | +| `items[]` | 每个文件的明细(`rel_path` / `file_token` / `action` / 失败时的 `error`) | + +`summary.failed > 0` 时命令以 **非零状态码**(`exit=1`,`error.type=partial_failure`)退出,且同一份 `summary + items` 会在 `error.detail` 里返回;脚本/agent 直接通过 exit code 判断成败即可,不需要再去解 `summary.failed`。 + +## 命令 + +```bash +# 基础用法 —— 把云端 fldcXXX 镜像到 ./repo +lark-cli drive +pull --local-dir ./repo --folder-token fldcnxxxxxxxxx + +# 已存在的本地文件保持不动 +lark-cli drive +pull --local-dir ./repo --folder-token fldcnxxxxxxxxx \ + --if-exists skip + +# 文件级镜像:下载新文件 + 删除云端没有的本地文件(不删空目录) +# (--delete-local 必须搭配 --yes,否则会被 Validate 直接拒绝) +lark-cli drive +pull --local-dir ./repo --folder-token fldcnxxxxxxxxx \ + --delete-local --yes +``` + +## 参数 + +| 标志 | 必填 | 类型 | 说明 | +|------|------|------|------| +| `--local-dir` | 是 | path | 本地根目录(**必须是 cwd 的相对路径**;绝对路径或逃出 cwd 的相对路径会被 CLI 直接拒绝) | +| `--folder-token` | 是 | string | 源 Drive 文件夹 token | +| `--if-exists` | 否 | enum | 本地文件已存在时的策略:`overwrite`(默认)/ `skip` | +| `--delete-local` | 否 | bool | 删除本地"云端没有的常规文件"(**不删空目录**,因此是 file-level mirror);**必须配合 `--yes`** | +| `--yes` | 否 | bool | 确认 `--delete-local`;不传时该破坏性操作在 Validate 阶段被拒绝 | + +## 比较与下载范围 + +- **只下载 Drive `type=file` 的二进制文件**。在线文档(`docx` / `sheet` / `bitable` / `mindnote` / `slides`)和快捷方式(`shortcut`)会被跳过 —— 它们没有等价的本地二进制可写盘,否则会变成产生噪声的"假"下载。 +- 子文件夹会递归遍历;rel_path 形如 `sub1/sub2/file.txt`,本地缺失的父目录会被自动创建。 +- 已存在的本地文件按 `--if-exists` 决定 `overwrite` 还是 `skip`,没有第三种选择 —— 想做 `keep-both` 这类的请自己改名再 pull。 + +## --delete-local 的安全行为 + +`--delete-local` 是命令里**唯一的破坏性 flag**,会按"本地有但云端没有"清理本地常规文件。设计上把它跟 `--yes` 强绑定,且与下载阶段的失败联动: + +- `--delete-local`(无 `--yes`)→ Validate 直接报错:`--delete-local requires --yes`,没有任何下载、列表请求或删除发生。 +- `--delete-local --yes`,**且下载阶段全部成功** → 扫一遍 `--local-dir` 下所有常规文件,把不在云端清单里的逐个 `os.Remove`。**只删常规文件,不删目录**:远端文件夹被删除后,对应本地目录会保留空壳。 +- `--delete-local --yes`,**但下载阶段有任何条目失败** → **跳过整个删除阶段**,命令以 `partial_failure` 非零退出。设计意图:避免出现"前面下载失败、后面继续删本地文件"的半同步状态;操作者修好下载错误后再重跑即可。 +- 不传 `--delete-local` → `summary.deleted_local` 永远是 0;命令对本地"多余"文件视而不见。 + +第 6 章里把 `+pull --delete-local` 标了 `high-risk-write`,CLI 这边的实现等价于"未传 `--yes` 时拒绝执行",符合该约束的精神。 + +## 输出 schema + +```json +{ + "summary": { + "downloaded": 0, + "skipped": 0, + "failed": 0, + "deleted_local": 0 + }, + "items": [ + {"rel_path": "...", "file_token": "...", "action": "downloaded"}, + {"rel_path": "...", "file_token": "...", "action": "skipped"}, + {"rel_path": "...", "file_token": "...", "action": "failed", "error": "..."}, + {"rel_path": "...", "action": "deleted_local"}, + {"rel_path": "...", "action": "delete_failed", "error": "..."} + ] +} +``` + +`rel_path` 始终用 `/` 作为分隔符(跨平台一致)。删除条目(`deleted_local` / `delete_failed`)没有 `file_token`,因为该文件本来就只在本地。 + +## 性能注意 + +- 下载流量 ≈ 云端待下载文件的总字节数。pull 是**全量**写盘 —— 跟 `+status` 不一样,不会跳过"内容相同"的文件(status 是按 hash 比较,pull 是按 `--if-exists`),所以一次跑可能很重。 +- 想避免重跑全量,可以先 `+status` 找出 `new_remote` 和 `modified`,再只对这些文件单独 `+download`。 +- 大文件会用 SDK 的流式下载(不会把整个 body 读进内存),但本地磁盘空间需要够。 + +## 所需 scope + +| 操作 | scope | +|------|-------| +| 列出文件夹 / 子目录 | `drive:drive.metadata:readonly` | +| 下载文件 | `drive:file:download` | + +如果当前 token 缺这些 scope,命令会直接报 `missing_scope` 并提示重新登录。`drive:drive` 在部分企业被策略禁用,所以 +pull 故意只声明上面这两个细粒度 scope。 + +## 范围限制 + +`--local-dir` 只接受 cwd 内的相对路径。CLI 会先 `EvalSymlinks` 整条路径,再判断它是否仍落在 cwd 内 —— **指向 cwd 外的符号链接也会被拒**,"在 cwd 内放一条软链指向外面" 这条捷径走不通,会直接撞上 `unsafe file path`。 + +如果用户想 pull 到 cwd 之外的目录,**不要 agent 自己 `cd` 绕过**。可以选:让用户在外部把 agent 工作目录切换到目标的祖先后重启会话;或者把目标整体物理移动 / 拷贝到 cwd 内(不是软链);或者直接放弃这次同步,改用别的方式。 + +## 参考 + +- [lark-drive](../SKILL.md) —— 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) —— 认证和全局参数 +- [lark-drive-status](lark-drive-status.md) —— 下载前先看差异 +- [lark-drive-download](lark-drive-download.md) —— 单文件按需拉取 diff --git a/.agents/skills/lark-drive/references/lark-drive-push.md b/.agents/skills/lark-drive/references/lark-drive-push.md new file mode 100644 index 0000000..31fbb57 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-push.md @@ -0,0 +1,137 @@ + +# drive +push + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +把本地目录**单向、文件级**镜像到飞书云空间的某个文件夹(本地 → Drive)。命令递归列出 `--folder-token` 下的远端清单,遍历 `--local-dir` 的所有常规文件,按相对路径在 Drive 上新建、覆盖或跳过;可选地(`--delete-remote --yes`)删除云端"本地没有"的 `type=file`。 + +> **"文件级镜像"≠"目录镜像"。** 命令只在文件维度收敛差异:本地多了文件就上传,本地少了文件且开了 `--delete-remote --yes` 就删远端文件。**远端只有的空目录、本地已删除的目录**都不会被收敛,云端目录树的多余结构不会被清理。如果需要"目录也要保持完全一致",得自行先 `+status` 找差异、再手动处理多余目录。 + +输出按"动作"分类: + +| 字段 | 含义 | +|------|------| +| `summary.uploaded` | 成功新建或覆盖的文件数 | +| `summary.skipped` | 按 `--if-exists=skip` 跳过的文件数 | +| `summary.failed` | 上传 / 覆盖 / 建目录 / 删除失败的条目数;**只要不为 0,命令就以非零状态退出**(结构化 `items[]` 仍在 stdout 上) | +| `summary.deleted_remote` | 启用 `--delete-remote --yes` 时删除的云端文件数 | +| `items[]` | 每个条目的明细(`rel_path` / `file_token` / `action` / 覆盖时的 `version` / `size_bytes` / 失败时的 `error`) | + +`items[].action` 取值:`uploaded` / `overwritten` / `skipped` / `folder_created` / `deleted_remote` / `failed` / `delete_failed`。 + +> 本地目录(包括空目录)会被镜像到 Drive;新建的子目录会以 `action: "folder_created"` 出现在 `items[]` 里,但**不计入** `summary.uploaded`(该字段只数文件)。已存在的远端目录复用其 token,不会重复 `create_folder`,也不会出现在 `items[]` 里。 + +## 命令 + +```bash +# 基础用法 —— 把本地 ./repo 增量推送到云端 fldcXXX +# 默认 --if-exists=skip:已经存在的远端文件保持不动,只新增、不覆盖。 +lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx + +# 显式覆盖远端同名文件(依赖 upload_all 的灰度协议字段,详见下文"覆盖语义") +lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \ + --if-exists overwrite + +# 文件级镜像同步:上传 / 覆盖 + 删除本地不存在的远端文件 +# (--delete-remote 必须搭配 --yes,否则会被 Validate 直接拒绝; +# 且 Validate 阶段会动态检查 space:document:delete scope,缺权限会立刻失败, +# 不会出现"上传成功了但是后面删除阶段挂了"的半同步状态) +lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \ + --if-exists overwrite --delete-remote --yes +``` + +## 参数 + +| 标志 | 必填 | 类型 | 说明 | +|------|------|------|------| +| `--local-dir` | 是 | path | 本地根目录(**必须是 cwd 的相对路径**;绝对路径或逃出 cwd 的相对路径会被 CLI 直接拒绝) | +| `--folder-token` | 是 | string | 目标 Drive 文件夹 token | +| `--if-exists` | 否 | enum | 远端文件已存在时的策略:`skip`(**默认**,安全)/ `overwrite`(依赖灰度后端协议,详见"覆盖语义") | +| `--delete-remote` | 否 | bool | 删除云端本地不存在的文件(文件级镜像;**不会**清理远端只有的目录);**必须配合 `--yes`**,且 Validate 阶段会动态检查 `space:document:delete` scope | +| `--yes` | 否 | bool | 确认 `--delete-remote`;不传时该破坏性操作在 Validate 阶段被拒绝 | + +## 上传与目录复刻范围 + +- **只上传 / 覆盖 / 删除 Drive `type=file`**。在线文档(`docx` / `sheet` / `bitable` / `mindnote` / `slides`)和快捷方式(`shortcut`)即使在同一 rel_path 下出现,也不会被覆盖或删除 —— 它们没有等价的本地二进制。 +- **本地目录结构整体被镜像**:所有子目录(含**空目录**)会按需在 Drive 上 `create_folder`;同名远端目录复用其 token,不重建。空目录不计入 `summary.uploaded`,但会在 `items[]` 里以 `folder_created` 形式留痕。 +- 已存在的远端文件按 `--if-exists` 决定 `overwrite` 还是 `skip`,没有第三种选择 —— 想做 `keep-both` 这类的请自行改名再 push。 + +## 覆盖语义 + +`--if-exists=overwrite` 走 `POST /open-apis/drive/v1/files/upload_all`,并在 form 中带上现有文件的 `file_token`,由后端原地更新内容并返回新版本号。`items[].version` 字段会回填该版本号。 + +> **为什么默认是 `skip` 而不是 `overwrite`:** `upload_all` 接受 `file_token` 字段、并在响应里返回 `version` 是设计文档(Drive 同步盘)规定的协议;此后端尚在灰度发布。在还未开通该字段的 tenant 上,`--if-exists=overwrite` 会因"无 version 返回"而把对应文件标成 `failed`,整次 `+push` 也会因此非零退出。所以默认值故意定为 `skip`:第一次往一个已经有内容的目录里 push,不会因为协议没到位就把整次运行打挂;要真的覆盖远端,必须显式带 `--if-exists overwrite`。新建上传不依赖该字段,不受影响。 + +大文件(>20MB)会自动切到三段式 `upload_prepare` / `upload_part` / `upload_finish`;该路径下 `version` 暂未在响应中返回,覆盖结果中 `items[].version` 会留空,但 `file_token` 与 `action: overwritten` 仍会正确产出。 + +## --delete-remote 的安全行为 + +`--delete-remote` 是命令里**唯一的破坏性 flag**,会按"远端有但本地没有"逐个 `DELETE /open-apis/drive/v1/files/?type=file` 清理云端副本。设计上把它跟 `--yes` 强绑定: + +- `--delete-remote`(无 `--yes`)→ Validate 直接报错:`--delete-remote requires --yes`,不会发起任何列表 / 上传 / 删除请求。 +- `--delete-remote --yes` → Validate 阶段还会**动态做一次** `space:document:delete` 的 scope 预检:缺这条 scope 时整次运行立刻失败、不发任何上传请求,避免出现"上传都成功了,但删除阶段才报 missing_scope"的半同步状态。 +- `--delete-remote --yes`(且 scope 已授权)→ 正常执行:先把本地文件 push 上去,再扫一遍远端 `type=file` 列表,把不在本地清单里的逐个删除。**任何上传 / 覆盖 / 建目录失败时,整段 `--delete-remote` 阶段会被跳过**(stderr 上有提示),命令以非零状态退出,远端不会被破坏。 +- 不传 `--delete-remote` → `summary.deleted_remote` 永远是 0;命令对远端"多余"文件视而不见。 +- 在线文档(docx / sheet / bitable / ...)和快捷方式即使本地完全没有同名文件,也**不会**进入删除候选,因为它们从来不进 `summary.uploaded` 的对齐域。 +- **远端只有的空目录、本地已删除的目录**也不会被清理 —— 这是"文件级镜像"的语义边界,命令不会对目录结构做主动收敛。 + +第 6 章里把 `+push --delete-remote` 标了 `high-risk-write`,CLI 这边的实现等价于"未传 `--yes` 时拒绝执行 + 动态 scope 预检",符合该约束的精神。 + +## 输出 schema + +```json +{ + "summary": { + "uploaded": 0, + "skipped": 0, + "failed": 0, + "deleted_remote": 0 + }, + "items": [ + {"rel_path": "...", "file_token": "...", "action": "folder_created"}, + {"rel_path": "...", "file_token": "...", "action": "uploaded", "size_bytes": 0}, + {"rel_path": "...", "file_token": "...", "action": "overwritten", "version": "...", "size_bytes": 0}, + {"rel_path": "...", "file_token": "...", "action": "skipped", "size_bytes": 0}, + {"rel_path": "...", "action": "failed", "size_bytes": 0, "error": "..."}, + {"rel_path": "...", "file_token": "...", "action": "deleted_remote"}, + {"rel_path": "...", "file_token": "...", "action": "delete_failed", "error": "..."} + ] +} +``` + +`rel_path` 始终用 `/` 作为分隔符(跨平台一致)。 + +## 性能注意 + +- 上传流量 ≈ 本地待上传文件的总字节数。push 是**全量**上传 —— 跟 `+status` 不一样,不会按 hash 跳过"内容相同"的文件(status 是按 hash 比较,push 是按 `--if-exists`),所以一次跑可能很重。 +- 想避免重跑全量,可以先 `+status` 找出 `new_local` 和 `modified`,再只对这些文件单独上传 / 覆盖。 +- 大文件会用三段式分片上传(不会把整个 body 读进内存),但本地磁盘和上行带宽需要够。 + +## 所需 scope + +| 操作 | scope | 是否在命令上预声明 | +|------|-------|-------------------| +| 列出文件夹 / 子目录 | `drive:drive.metadata:readonly` | ✅ 预声明 | +| 上传 / 覆盖文件 | `drive:file:upload` | ✅ 预声明 | +| 新建子目录(`create_folder`) | `space:folder:create` | ✅ 预声明 | +| 删除文件(仅 `--delete-remote --yes`) | `space:document:delete` | ⚙️ 不在命令默认 Scopes 里,但在 `--delete-remote --yes` 时由 Validate 动态预检 | + +`drive:drive` 在部分企业被策略禁用,所以 +push 故意只声明上面这几条细粒度 scope。 + +> **关于 `space:document:delete`:** 框架的 scope 预检(`runner.go: checkShortcutScopes`)会在 `Validate` 和 `--dry-run` 之前就把命令上声明的 scope 全检查一遍;如果把删除 scope 也预声明,**普通上传或 dry-run** 都会因为没授权删除权限而被拦下来。所以这一项不放在命令的默认 Scopes 里,而是在 Validate 中**条件触发**:只有 `--delete-remote --yes` 同时打开时才会调用 `runtime.EnsureScopes([]string{"space:document:delete"})` 做一次动态前置校验。这样既保留了"普通上传不需要删除权限"的便利,又能在真要做镜像删除前把 scope 缺失暴露出来,避免出现"上传成功 → 删除阶段才挂"的半同步状态。 +> +> 想一次性把权限补齐:`lark-cli auth login --scope "drive:drive.metadata:readonly drive:file:upload space:folder:create space:document:delete"`。 + +## 范围限制 + +`--local-dir` 只接受 cwd 内的相对路径。CLI 会先 `EvalSymlinks` 整条路径,再判断它是否仍落在 cwd 内 —— **指向 cwd 外的符号链接也会被拒**,"在 cwd 内放一条软链指向外面" 这条捷径走不通,会直接撞上 `unsafe file path`。 + +如果用户想 push cwd 之外的目录,**不要 agent 自己 `cd` 绕过**。可以选:让用户在外部把 agent 工作目录切换到目标的祖先后重启会话;或者把目标整体物理移动 / 拷贝到 cwd 内(不是软链);或者直接放弃这次同步,改用别的方式。 + +## 参考 + +- [lark-drive](../SKILL.md) —— 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) —— 认证和全局参数 +- [lark-drive-status](lark-drive-status.md) —— 上传前先看差异(避免全量回写) +- [lark-drive-pull](lark-drive-pull.md) —— Drive → 本地的对称命令 +- [lark-drive-upload](lark-drive-upload.md) —— 单文件按需上传 diff --git a/.agents/skills/lark-drive/references/lark-drive-reactions.md b/.agents/skills/lark-drive/references/lark-drive-reactions.md new file mode 100644 index 0000000..1ddc682 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-reactions.md @@ -0,0 +1,113 @@ +# drive reactions + +> **前置条件:** 先阅读 [`../SKILL.md`](../SKILL.md) 了解 Drive 评论卡片模型、评论数/回复数统计口径、`file_token` / `file_type` 规则;再阅读 [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +处理文档评论 / 回复上的 reaction(点赞、表情、各表情数量、谁点了什么、添加/删除表情)。这个场景不常见,但规则比较集中:查询时只有在用户明确需要 reaction 信息时才带 `need_reaction=true`;写入时统一使用 `drive file.comment.reply.reactions update_reaction`,操作对象始终是 `reply_id`。 + +> [!IMPORTANT] +> **`reaction_type` 只能使用本文下方“完整 `reaction_type` 列表”中定义的枚举值。** +> 不要自由填写、不要根据自然语言临时编造、也不要把列表里的 mixed-case 值改写成别的大小写形式。需要写入时,只能从下方枚举中原样选择并传参。 + +## 何时使用 + +- 用户明确要求查看评论 / 回复上的 reaction(表情)。 +- 用户要统计某条评论卡片有哪些表情、各表情数量,或要看谁点了什么。 +- 用户要给评论或回复添加 / 删除 reaction。 + +## 查询规则 + +- `drive file.comments list`、`drive file.comments batch_query`、`drive file.comment.replys list` 都支持通过指定`need_reaction`查询reaction信息。 +- `need_reaction` 只在用户明确需要 reaction 信息时再带;如果用户只关心评论正文、回复正文、评论数 / 回复数,默认不要加。 +- 遍历评论卡片并顺带拿 reaction:使用 `drive file.comments list`。 +- 已知评论 ID,批量查看 reaction:使用 `drive file.comments batch_query`,并在请求体里带 `need_reaction=true`。 +- 某张评论卡片下继续翻页拉 reply reaction:使用 `drive file.comment.replys list`。 +- 如果 `drive file.comments list` 返回的某个 `item.has_more=true`,且用户要完整的 reply reaction 数据,后续每一页 `drive file.comment.replys list` 都要持续带 `need_reaction=true`。 + +## 查询示例 + +```bash +# 遍历评论卡片,并把 reaction 一起拿回来 +lark-cli drive file.comments list \ + --params '{"file_token":"","file_type":"docx","need_reaction":true}' + +# 已知 comment_id,批量查询评论卡片 reaction +lark-cli drive file.comments batch_query \ + --params '{"file_token":"","file_type":"docx"}' \ + --data '{"comment_ids":[""],"need_reaction":true}' + +# 继续翻某张评论卡片下的 replies,并把 reaction 一起拿回来 +lark-cli drive file.comment.replys list \ + --params '{"file_token":"","comment_id":"","file_type":"docx","need_reaction":true}' +``` + +## 写入规则 + +- 添加 / 删除 reaction 时,使用 `drive file.comment.reply.reactions update_reaction`。 +- 请求里必须带正确的 `file_type`,并在 body 中传 `action=add|delete`、`reply_id`、`reaction_type`。 +- `update_reaction` 的操作对象是 `reply_id`,不是 `comment_id`。 +- 如果用户说要给“这条评论”加 / 删 reaction,通常需要定位到该评论卡片首条 reply 的 `reply_id` 再操作。 + +## 写入示例 + +```bash +# 给某条 reply 添加一个点赞 reaction +lark-cli drive file.comment.reply.reactions update_reaction \ + --params '{"file_token":"","file_type":"docx"}' \ + --data '{"action":"add","reply_id":"","reaction_type":"THUMBSUP"}' + +# 删除某条 reply 上已有的 DONE reaction +lark-cli drive file.comment.reply.reactions update_reaction \ + --params '{"file_token":"","file_type":"docx"}' \ + --data '{"action":"delete","reply_id":"","reaction_type":"DONE"}' +``` + +> [!CAUTION] +> `update_reaction` 是写入操作。执行前必须确认用户意图,不要默认替用户点表情。 + +## `reaction_type` 使用规则 + +- `reaction_type` 必须传平台定义的枚举字符串,大小写敏感。 +- 不要擅自把 mixed-case 值改成全大写,例如 `Yes`、`No`、`Get`、`EatingFood`、`CheckMark`、`CrossMark` 都要按原值传。 +- **不要编造列表外的 `reaction_type`,也不要把自然语言描述臆造成平台未定义的新枚举**。 +- 如果用户给的是自然语言语义(如“点赞”“在处理中”“确认一下”),可以在下方枚举列表内选择语义最接近的现有值;如果是近似映射,应在执行时明确告知用户。 + +## 常见语义联想 + +- `Yes`:确认 / 同意 / 批准。 +- `No`:拒绝 / 不同意 / 否定。 +- `DONE`:已完成 / 已处理。 +- `Typing`:正在输入 / 正在处理中 / 正在跟进(近似语义)。 +- `OK`:好的 / 收到 / 确认一下。 +- `THUMBSUP`:点赞 / 认可。 +- `LGTM`:看起来没问题 / 可以继续。 + +## 完整 `reaction_type` 列表 + +以下枚举按当前 Drive 评论 reaction 指引维护,使用时请保持原样: + +```text +ANGRY, APPLAUSE, ATTENTION, AWESOME, BEAR, BEER, BETRAYED, BIGKISS +BLACKFACE, BLUBBER, BLUSH, BOMB, CAKE, CHUCKLE, CLAP, CLEAVER +COMFORT, CRAZY, CRY, CUCUMBER, DETERGENT, DIZZY, DONE, DONNOTGO +DROOL, DROWSY, DULL, DULLSTARE, EATING, EMBARRASSED, ENOUGH, ERROR +EYESCLOSED, FACEPALM, FINGERHEART, FISTBUMP, FOLLOWME, FROWN, GIFT, GLANCE +GOODJOB, HAMMER, HAUGHTY, HEADSET, HEART, HEARTBROKEN, HIGHFIVE, HUG +HUSKY, INNOCENTSMILE, JIAYI, JOYFUL, KISS, LAUGH, LIPS, LOL +LOOKDOWN, LOVE, MONEY, MUSCLE, NOSEPICK, OBSESSED, OK, PARTY +PETRIFIED, POOP, PRAISE, PROUD, PUKE, RAINBOWPUKE, ROSE, SALUTE +SCOWL, SHAKE, SHHH, SHOCKED, SHOWOFF, SHY, SICK, SILENT +SKULL, SLAP, SLEEP, SLIGHT, SMART, SMILE, SMIRK, SMOOCH +SMUG, SOB, SPEECHLESS, SPITBLOOD, STRIVE, SWEAT, TEARS, TEASE +TERROR, THANKS, THINKING, THUMBSUP, TOASTED, TONGUE, TRICK, UPPERLEFT +WAIL, WAVE, WELLDONE, WHAT, WHIMPER, WINK, WITTY, WOW +WRONGED, XBLUSH, YAWN, YEAH, FIREWORKS, BULL, CALF, AWESOMEN +2021, CANDIEDHAWS, REDPACKET, FORTUNE, LUCK, FIRECRACKER, Yes, No +Get, LGTM, Lemon, EatingFood, Hundred, MinusOne, ThumbsDown, Fire +OKR, Drumstick, BubbleTea, Loudspeaker, Pin, Coffee, Alarm, Trophy +Music, Typing, Pepper, CheckMark, CrossMark +``` + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-search.md b/.agents/skills/lark-drive/references/lark-drive-search.md new file mode 100644 index 0000000..3b02459 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-search.md @@ -0,0 +1,239 @@ + +# drive +search(云空间搜索:扁平 flag,面向自然语言场景) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +基于 Search v2 接口 `POST /open-apis/search/v2/doc_wiki/search`,以**用户身份**统一搜索云空间对象。 + +和老的 `docs +search` 相比: + +- 把常用过滤条件全部**扁平化为独立 flag**(`--edited-since`、`--mine`、`--doc-types`、`--folder-tokens` 等),不再要求用户或 AI 手写嵌套 `--filter` JSON +- 额外暴露了 4 个"我"维度:`my_edit_time`(我编辑过)、`my_comment_time`(我评论过)、`open_time`(我打开过)、`create_time`(文档创建时间)——直接对应用户自然语言里的"最近我编辑过的"、"我评论过的"等表达 +- 自动处理 `my_edit_time` / `my_comment_time` 的小时级聚合(服务端存储粒度):亚小时输入会向整点 snap,并在 stderr 打出提示 +- `--mine` 一键从当前登录用户的 open_id 填 `creator_ids`,不必再先去查 contact + +> **资源发现入口统一**:`drive +search` 同样返回 `SHEET` / `Base` / `FOLDER` 等全部云空间对象,不只是文档 / Wiki。用户说"找一个表格"、"找报表"、"最近打开的表格"时,也从这里开始;定位后再切到对应业务 skill(如 `lark-sheets`)做对象内部操作。 + +## 命令 + +> **关键约束:搜索关键词必须通过 `--query` 传递。** +> 正确:`lark-cli drive +search --query "方案"` +> 错误:`lark-cli drive +search 方案` +> `+search` 不接受位置参数;空 `--query` 或省略 `--query` 表示纯靠 filter 浏览(合法)。 + +### 自然语言 → 命令映射速查 + +| 用户说 | 命令 | +|---|---| +| 最近一个月我编辑过的文档 | `lark-cli drive +search --query "" --edited-since 1m` | +| 最近一个月我编辑过 且 我评论过的 | `lark-cli drive +search --query "" --edited-since 1m --commented-since 1m` | +| 最近一周我打开过的表格 | `lark-cli drive +search --query "" --opened-since 7d --doc-types sheet` | +| 我创建的所有文档 | `lark-cli drive +search --query "" --mine` | +| 我 30-60 天前创建的文档(粗略"上个月",按 30 天滑窗算) | `lark-cli drive +search --query "" --mine --created-since 2m --created-until 1m` | +| 我 2026 年 3 月创建的文档(精确日历月) | `lark-cli drive +search --query "" --mine --created-since 2026-03-01 --created-until 2026-04-01` | +| 关键词"预算",最近一周我打开过,按编辑时间降序 | `lark-cli drive +search --query 预算 --opened-since 7d --sort edit_time` | +| 某个 wiki space 下、我 30-60 天前创建的 | `lark-cli drive +search --query "" --mine --space-ids space_xxx --created-since 2m --created-until 1m` | +| 张三创建的文档 | `lark-cli drive +search --query "" --creator-ids ou_zhangsan` | +| 我最近 3 个月评论过的 docx | `lark-cli drive +search --query "" --commented-since 3m --doc-types docx` | + +### 更多示例 + +```bash +# 纯关键词搜索 +lark-cli drive +search --query "季度总结" + +# 使用服务端 query 高级语法(和 docs +search 一致) +lark-cli drive +search --query 'intitle:方案' +lark-cli drive +search --query '"季度 总结"' +lark-cli drive +search --query '方案 OR 草稿' +lark-cli drive +search --query '方案 -草稿' + +# 只搜某个文件夹下的文档 +lark-cli drive +search --query 方案 --folder-tokens fld_123456 + +# 只搜某个知识空间下的 Wiki +lark-cli drive +search --query 研发规范 --space-ids space_1234567890fedcba + +# 指定群内分享过的文档 +lark-cli drive +search --query 方案 --chat-ids oc_1234567890abcdef + +# 只搜标题 / 只搜评论 +lark-cli drive +search --query 周报 --only-title +lark-cli drive +search --query 延期原因 --only-comment + +# 人类可读格式 +lark-cli drive +search --query OKR --format pretty + +# 翻页(--format json 先拿 page_token) +lark-cli drive +search --query 方案 --format json +lark-cli drive +search --query 方案 --page-token '' +``` + +## 参数 + +### 核心 + +| 参数 | 必填 | 说明 | +|---|---|---| +| `--query ` | 否 | 搜索关键词;支持服务端高级语法(`intitle:`、`""`、`OR`、`-`)。空字符串或省略表示纯 filter 浏览 | +| `--page-size ` | 否 | 每页数量,默认 15,最大 20。超过 20 自动 clamp;非正数(≤0)回落 15;**非数字值直接返回 validation 错误** | +| `--page-token ` | 否 | 上一次响应里的 `page_token`,用于翻页 | +| `--format` | 否 | `json`(默认)/ `pretty` | + +### 身份(creator 维度) + +| 参数 | 映射 | 说明 | +|---|---|---| +| `--mine` | `creator_ids = [当前用户 open_id]` | bool。一键"我创建的";从当前登录用户身份(`runtime.UserOpenId()`)解析 open_id,取不到直接报错(提示运行 `lark-cli auth login`) | +| `--creator-ids ou_x,ou_y` | `creator_ids = [...]` | 显式 open_id 列表,逗号分隔;**与 `--mine` 互斥** | + +### 时间维度(每个维度一对 since/until) + +| 参数 | 映射 API 字段 | 是否小时 snap | +|---|---|---| +| `--edited-since` / `--edited-until` | `my_edit_time.start` / `.end` | ✅ start 向下取整,end 向上取整 | +| `--commented-since` / `--commented-until` | `my_comment_time.start` / `.end` | ✅ 同上 | +| `--opened-since` / `--opened-until` | `open_time.start` / `.end` | ❌ 原样透传 | +| `--created-since` / `--created-until` | `create_time.start` / `.end` | ❌ 原样透传(文档创建时间,非"我"语义)| + +### 作用域 + +| 参数 | 映射 | 说明 | +|---|---|---| +| `--doc-types docx,sheet` | `doc_types` | 逗号分隔。允许值:`doc,sheet,bitable,mindnote,file,wiki,docx,folder,catalog,slides,shortcut` | +| `--folder-tokens fld_a,fld_b` | `folder_tokens`(仅 doc_filter) | 存在时只发 `doc_filter`;**与 `--space-ids` 互斥** | +| `--space-ids sp_x` | `space_ids`(仅 wiki_filter) | 存在时只发 `wiki_filter`;**与 `--folder-tokens` 互斥** | +| `--chat-ids oc_x` | `chat_ids` | 逗号分隔 | +| `--sharer-ids ou_x` | `sharer_ids` | 逗号分隔,open_id | + +### 其他 + +| 参数 | 映射 | 说明 | +|---|---|---| +| `--only-title` | `only_title: true` | bool | +| `--only-comment` | `only_comment: true` | bool | +| `--sort ` | `sort_type`(转大写枚举) | 允许值:`default, edit_time, edit_time_asc, open_time, create_time` | + +> `--sort`:CLI 只暴露服务端**正式支持**的 5 个值。服务端 enum 里 `CREATE_TIME_ASC` 协议标注"暂不支持",`ENTITY_CREATE_TIME_ASC` / `ENTITY_CREATE_TIME_DESC` 已废弃,CLI 直接不放出来,传了会被 cobra enum 校验拒掉。 + +## 时间值格式 + +所有 `--*-since` / `--*-until` 共用: + +| 输入 | 含义 | +|---|---| +| `7d` / `30d` | N 天前的当前时刻 | +| `1m` | 30 天前(固定 30 天,**不是**日历月)| +| `3m` / `6m` | 90 / 180 天前 | +| `1y` | 365 天前 | +| `2026-04-01` | 本地时区 00:00:00 | +| `2026-04-01 10:00:00` / `2026-04-01T10:00:00` | 本地时区具体时刻 | +| `2026-04-01T10:00:00+08:00` | RFC3339 带时区 | +| `1743523200`(≥ 10 位纯数字)| Unix 秒直接透传 | + +> `m` 绑定 month(30 天),不支持 minute——因为 `my_edit_time` / `my_comment_time` 在服务端是小时聚合,分钟粒度没意义。 + +## 小时聚合(my_edit_time / my_comment_time) + +服务端对这两个字段按整点聚合,亚小时输入会被 CLI 向整点对齐: + +```text +start: floor 到整点 16:23:45 → 16:00:00 +end: ceil 到整点 16:23:45 → 17:00:00 +``` + +发生对齐时,stderr 会打印一条 notice,例如: + +```text +notice: my_edit_time has hour-level granularity server-side; + start 2026-04-22 16:23:00 → 2026-04-22 16:00:00 + end 2026-04-22 16:28:00 → 2026-04-22 17:00:00 +``` + +stdout 的 JSON 输出不受影响。`open_time` / `create_time` 不做 snap。 + +## 输出 + +- `--format json`(默认):`{ total, has_more, page_token, results: [...] }`;所有 `*_time` 字段递归补 `*_time_iso` +- `--format pretty`:4 列 table —— `type | title | edit_time | url` +- `title_highlighted` / `summary_highlighted` 可能包含 `` / `` 高亮标签,客户端对比前需先剥离 + +> **注意**:返回体里的 `total` 字段不够准确(官方确认,仅供参考)。需要精确统计的场景,按实际 `results` 做去重和累加,不要把 `total` 当结果数承诺。 + +## 决策规则 + +- **和 `docs +search` 的选择**:优先使用 `drive +search`(本指令),不要再用 `docs +search`。`docs +search` 进入维护期、后续会下线。 +- **身份快捷方式**:只要用户说"我创建的",直接 `--mine` 即可,不需要先查 contact 拿 open_id。 +- **时间维度选择**: + - "我编辑的"、"我修改的" → `--edited-since` / `--edited-until` + - "我评论的"、"我回复过的" → `--commented-since` / `--commented-until` + - "我看过的"、"我打开过的"、"最近看过的" → `--opened-since` / `--opened-until` + - "创建于"、"新建的"(文档整体维度,与"我"无关)→ `--created-since` / `--created-until` +- **作用域选择**: + - "某个文件夹下" → `--folder-tokens`(doc-only) + - "某个 wiki 空间下" → `--space-ids`(wiki-only) + - 两者不能同时使用,混用会报错 +- **身份 flag 互斥**:`--mine` 和 `--creator-ids` 不要同时传,会直接报错。"我和张三创建的" 用 `--creator-ids ou_me,ou_zhangsan`(需要先拿到自己 open_id,但这种场景少见)。 +- **实体补全**: + - 用户说"某个群里",先用 `lark-im` 查 `chat_id` + - 用户说"某人创建/分享的"(非自己),先用 `lark-contact` 查 open_id,再填 `--creator-ids` / `--sharer-ids` +- **查询语义下推**:`--query` 支持的服务端高级语法(`intitle:`、`""`、`OR`、`-`)优先使用,不要先模糊搜再在客户端二次过滤。 +- **时间表达**: + - 模糊相对时间("最近半年"、"过去 30 天"、"最近一周")→ `--*-since 6m` / `--*-since 30d` / `--*-since 7d`,不展开成 ISO 时间 + - **日历表达**("上个月"、"上周"、"本月"、"前年"、"今年 3 月"等明确日历单位)→ **必须算出绝对 `YYYY-MM-DD` 边界**(如"上个月" = 上一个日历月的 1 号 → 当月 1 号),**不要近似成 `1m`/`2m`**:CLI 里 `m` 是固定 30 天、`y` 固定 365 天,跟日历差 0-3 天,月末月初尤其容易偏出去 + - 绝对日期 → 直接 `YYYY-MM-DD` 或 RFC3339 +- **分页策略**:默认只返回第一页,并说明 `has_more` 和下一页命令。只有用户明确要"全部 / 全量 / 继续翻"才继续。单轮翻页上限 5 页。 +- **原始返回**:用户要求"原始数据"、"接口返回"时用 `--format json`,不做客户端精确过滤或摘要重写。 + +## 权限 + +| 操作 | 所需 scope | +|---|---| +| 搜索云空间对象(文档 / Wiki / 表格等资源发现) | `search:docs:read` | + +## 常见错误 + +| code | 含义 | 处理 | +|---|---|---| +| `99992351` | `--creator-ids` / `--sharer-ids` 里有 open_id 超出**应用的通讯录可见范围**,服务端拒绝识别 | 让管理员在开发者后台把这些用户加进应用的"通讯录可见性"授权里;或把超出范围的 open_id 从参数里去掉。这和 `search:docs:read` scope 不是一回事 —— 是"应用能看见哪些人"而不是"应用能调用哪个接口" | + +## 时间范围自动裁剪(`--opened-*` 专有) + +服务端对 `open_time` 过滤**每次请求最多支持 3 个月**(90 天)窗口。其他三个时间维度(`--edited-*` / `--commented-*` / `--created-*`)**不受影响**。 + +CLI 在发请求前会检查 `--opened-since` 到有效 `--opened-until`(没传则取 `now`)的跨度: + +| 跨度 | 行为 | +|---|---| +| ≤ 90 天 | 原样透传 | +| 91 ~ 365 天 | **自动裁剪**到"最近一个 90 天 slice",stderr 打一条 notice 列出所有剩余 slice 的 `--opened-since` / `--opened-until` 参数值 | +| > 365 天 | 直接报 validation 错,要求缩小范围或自行拆分多次查询 | + +Notice 示例(用户原本要求"过去 8 个月",会被拆成 3 个 slice): + +```text +notice: --opened-* window spans 240 days (~8 months), exceeds the server-side 3-month (90-day) limit. + this query was narrowed to the most recent slice; 3 slices total: + [slice 1/3 current] --opened-since 2026-01-24T21:54:02+08:00 --opened-until 2026-04-24T21:54:02+08:00 + [slice 2/3] --opened-since 2025-10-26T21:54:02+08:00 --opened-until 2026-01-24T21:54:02+08:00 + [slice 3/3] --opened-since 2025-08-27T21:54:02+08:00 --opened-until 2025-10-26T21:54:02+08:00 + pagination: paginate within a slice via --page-token using that slice's --opened-since / --opened-until values verbatim (NOT the original relative time like '1y' / '8m' — relative times re-resolve against time.Now() and would mismatch the page_token); switch to the next slice's --opened-* flags only after has_more=false, and do not carry --page-token across slices. +``` + +### Agent 看到 notice 时的处理 + +**标准流程(分页 × slice 的先后顺序):** + +1. **跑 slice 1**(本次请求已自动裁剪到这个窗口),把结果呈现给用户 +2. **先在当前 slice 内翻页**:返回的 `has_more = true` 且用户想看更多时,把 `--opened-since` / `--opened-until` 改成 notice 里 `[slice 1/N current]` 行给出的**具体时间值**(**不要继续用原始的 `--opened-since 1y` 这种相对值**——CLI 每次调用都按 `time.Now()` 重算窗口,相对值 + `--page-token` 一起跑会让 page_token 绑到一个漂移的窗口上、结果静默失真),加 `--page-token` 继续翻,直到 `has_more = false` +3. **再切换到下一个 slice**:当前 slice 翻完后,如果用户还要"更老的",用 notice 里列的 slice 2 的 `--opened-since` / `--opened-until` 值,**其他 flag(`--query`、`--doc-types`、`--page-size`、`--sort`……)保持原样,`--page-token` 不带**,重新发请求 +4. **依次递推**:slice 2 翻完后切 slice 3,以此类推 +5. 用户只对最近一段感兴趣时,跳过第 3 步及以后 —— 避免无意义的 API 调用 + +> `--page-token` 只在单 slice 上下文内有效;切 slice 时不要把上一个 slice 的 `page_token` 带过去。 + +### 注意事项 + +- `--sort` 在**单 slice 内部**是正确的。跨 slice 的全局 sort(例如"过去一年我打开过的,按 edit_time desc 排")不被 CLI 保证,需要 agent 自行拉完多个 slice 后在客户端 re-sort 再呈现 +- 裁剪只改 request 发出去的 `open_time` 范围,`--query` / 其他 filter 不动 +- 最后一个(最老的)slice 常常不足 90 天,这是正常的截断 diff --git a/.agents/skills/lark-drive/references/lark-drive-status.md b/.agents/skills/lark-drive/references/lark-drive-status.md new file mode 100644 index 0000000..a872442 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-status.md @@ -0,0 +1,89 @@ + +# drive +status + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +按 SHA-256 内容哈希比较本地目录与飞书云空间文件夹,输出四类差异: + +| 字段 | 含义 | +|------|------| +| `new_local` | 仅本地存在 | +| `new_remote` | 仅云端存在 | +| `modified` | 双端都存在但 hash 不一致 | +| `unchanged` | 双端都存在且 hash 一致 | + +只读命令:流式 hash,不下载落盘;但双端都有的文件会从云端拉一份字节流过来在内存里算 hash,大目录 / 大文件会有可观的网络流量。 + +## 命令 + +```bash +# 基础用法 —— 两个必填参数 +lark-cli drive +status \ + --local-dir ./repo \ + --folder-token fldcnxxxxxxxxx + +# 只看 hash 不一致的项(结合 --jq 过滤) +lark-cli drive +status \ + --local-dir ./repo \ + --folder-token fldcnxxxxxxxxx \ + --jq '.modified' +``` + +## 参数 + +| 标志 | 必填 | 类型 | 说明 | +|------|------|------|------| +| `--local-dir` | 是 | path | 本地根目录(**必须是 cwd 的相对路径**;绝对路径或逃逸到 cwd 外的相对路径会被 CLI 直接拒绝) | +| `--folder-token` | 是 | string | Drive 文件夹 token | + +## 输出 schema + +```json +{ + "new_local": [{"rel_path": "..."}], + "new_remote": [{"rel_path": "...", "file_token": "..."}], + "modified": [{"rel_path": "...", "file_token": "..."}], + "unchanged": [{"rel_path": "...", "file_token": "..."}] +} +``` + +`rel_path` 始终用 `/` 作为分隔符(跨平台一致),相对于 `--local-dir` 或 `--folder-token` 的根。仅本地存在时没有 `file_token` 字段。 + +## 比较范围 + +- **只比对 Drive `type=file` 的二进制文件**。在线文档(`docx` / `sheet` / `bitable` / `mindnote` / `slides`)和快捷方式(`shortcut`)都被跳过 —— 它们没有等价的本地二进制可对齐,否则会在 `new_remote` 里产生大量误报。 +- 子文件夹会递归遍历;rel_path 形如 `sub1/sub2/file.txt`。 +- 本地侧只比对常规文件(regular file);符号链接、设备文件等被忽略。 + +## 范围限制 + +`+status` 的本地侧只接受 cwd 下的相对路径。如果用户想比对的目录在 cwd 之外,**不要 agent 自己 `cd` 绕过**;让用户在合适的祖先目录重新启动 agent 后再跑。注意:把目标软链接到 cwd 内**也不行**——路径校验会先 `EvalSymlinks` 再判定是否越界,链接最终指向的真实目录如果在 cwd 之外,仍然会被 `unsafe file path` 拒掉。CLI 会在路径越界时直接报错,无需在 skill 这一层提前手动校验。 + +## 典型用法 + +把 +status 当作"先看差异、再决定怎么同步"的只读探针。常见接驳场景: + +- 想知道云端有什么本地没有的内容 → 看 `new_remote`,按需选择性拉取(`drive +download --file-token `)。 +- 想把本地新增的内容推到云端 → 看 `new_local`,再 `drive +upload --file --folder-token `(注意 +upload 不接受 0 字节文件)。 +- 想知道哪些文件在云端被同事改过 → 看 `modified`,逐个 `drive +download` 查内容差异。 + +## 性能注意 + +- `unchanged` + `modified` 的总字节数 = 本次需从云端下载的流量。100GB 的双端共享内容意味着 100GB 网络往返。 +- 仅一侧存在的文件不会被下载。 +- Hash 计算在内存里流式做(io.Copy → sha256.New),不会把云端文件落到磁盘。 + +## 所需 scope + +| 操作 | scope | +|------|-------| +| 列出文件夹 / 子目录 | `drive:drive.metadata:readonly` | +| 下载并 hash 文件 | `drive:file:download` | + +如果当前 token 缺这些 scope,命令会直接报 `missing_scope` 并提示重新登录。`drive:drive` 在部分企业被策略禁用,所以 +status 故意只声明上面这两个细粒度 scope。 + +## 参考 + +- [lark-drive](../SKILL.md) —— 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) —— 认证和全局参数 +- [lark-drive-upload](lark-drive-upload.md) / [lark-drive-download](lark-drive-download.md) —— 把 +status 输出接到推/拉动作上 diff --git a/.agents/skills/lark-drive/references/lark-drive-task-result.md b/.agents/skills/lark-drive/references/lark-drive-task-result.md new file mode 100644 index 0000000..018e093 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-task-result.md @@ -0,0 +1,302 @@ + +# drive +task_result + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +查询异步任务结果。该 shortcut 聚合了导入、导出、移动/删除文件夹、Wiki 节点 / 文档迁入 Wiki 等多种异步任务的结果查询,统一接口方便调用。 + +> [!IMPORTANT] +> 对于 `import` 场景,如果使用 `--as bot` 且这次查询**已经拿到最终在线文档目标**(`ready=true` 且返回了最终 `token` / `url`),CLI 会**再次尝试为当前 CLI 用户自动授予该资源的 `full_access`(可管理权限)**。 +> +> 此时结果里会额外返回 `permission_grant` 字段,明确说明授权结果: +> - `status = granted`:当前 CLI 用户已获得该导入结果的可管理权限 +> - `status = skipped`:本地没有可用的当前用户 `open_id`,或最终结果缺少可授权的在线文档目标,因此不会自动授权;可提示用户先完成 `lark-cli auth login`,再让 AI / agent 继续使用应用身份(bot)授予当前用户权限 +> - `status = failed`:导入结果已就绪,但自动授权用户失败;会带上失败原因,并提示稍后重试或继续使用 bot 身份处理该文档 +> +> `permission_grant.perm = full_access` 表示该资源已授予“可管理权限”。 +> +> **不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。 + +## 命令 + +```bash +# 查询导入任务结果 +lark-cli drive +task_result \ + --scenario import \ + --ticket + +# 查询导出任务结果 +lark-cli drive +task_result \ + --scenario export \ + --ticket \ + --file-token + +# 查询移动/删除文件夹任务状态 +lark-cli drive +task_result \ + --scenario task_check \ + --task-id + +# 查询 Wiki 移动任务结果(wiki +move 异步超时后的续跑) +lark-cli drive +task_result \ + --scenario wiki_move \ + --task-id + +# 查询 Wiki 删除知识空间任务结果(wiki +delete-space 异步超时后的续跑) +lark-cli drive +task_result \ + --scenario wiki_delete_space \ + --task-id +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--scenario` | 是 | 任务场景,可选值:`import` (导入任务)、`export` (导出任务)、`task_check` (移动/删除文件夹任务)、`wiki_move` (Wiki 移动任务)、`wiki_delete_space` (Wiki 删除知识空间任务) | +| `--ticket` | 条件必填 | 异步任务 ticket,**import/export 场景必填** | +| `--task-id` | 条件必填 | 异步任务 ID,**task_check / wiki_move / wiki_delete_space 场景必填** | +| `--file-token` | 条件必填 | 导出任务对应的源文档 token,**export 场景必填** | + +## 场景说明 + +| 场景 | 说明 | 所需参数 | +|------|------|----------| +| `import` | 文档导入任务(如将本地文件导入为云文档) | `--ticket` | +| `export` | 文档导出任务(如云文档导出为 PDF/Word) | `--ticket`、`--file-token` | +| `task_check` | 文件夹移动/删除任务 | `--task-id` | +| `wiki_move` | Wiki 移动任务(`wiki +move` 的 docs-to-wiki 异步流程,超时后续跑用) | `--task-id` | +| `wiki_delete_space` | Wiki 删除知识空间任务(`wiki +delete-space` 的异步流程,超时后续跑用) | `--task-id` | + +## 返回结果 + +### Import 场景返回 + +```json +{ + "scenario": "import", + "ticket": "", + "type": "sheet", + "ready": true, + "failed": false, + "job_status": 0, + "job_status_label": "success", + "job_error_msg": "success", + "token": "", + "url": "https://example.feishu.cn/sheets/", + "extra": ["2000"], + "permission_grant": { + "status": "granted", + "perm": "full_access", + "member_type": "openid", + "user_open_id": "", + "message": "Granted the current CLI user full_access (可管理权限) on the new spreadsheet." + } +} +``` + +**字段说明:** +- `ready`: 是否已经导入完成,可直接使用 `token` / `url` +- `failed`: 是否已经失败 +- `job_status`: 服务端返回的原始状态码 +- `job_status_label`: 便于阅读的状态标签,例如 `success` / `processing` +- `token`: 导入后的文档 token +- `url`: 导入后的文档链接 +- `permission_grant`: 仅 `--as bot` 且这次查询已经拿到最终在线文档目标时返回,用于说明是否已自动为当前 CLI 用户授予可管理权限;如果当前仍是 `ready=false`,则不会返回这个字段 + +### Export 场景返回 + +```json +{ + "scenario": "export", + "ticket": "", + "ready": true, + "failed": false, + "file_extension": "pdf", + "type": "doc", + "file_name": "docName", + "file_token": "", + "file_size": 34356, + "job_error_msg": "success", + "job_status": 0, + "job_status_label": "success" +} +``` + +**字段说明:** +- `ready`: 是否已经完成导出,可直接使用 `file_token` +- `failed`: 是否已经失败 +- `job_status`: 服务端返回的原始状态码 +- `job_status_label`: 便于阅读的状态标签,例如 `success` / `processing` +- `file_token`: 导出文件的 token,用于下载 +- `file_extension`: 导出文件扩展名 +- `file_size`: 导出文件大小(字节) + +### Task_check 场景返回 + +```json +{ + "scenario": "task_check", + "task_id": "", + "status": "success", + "ready": true, + "failed": false +} +``` + +**字段说明:** +- `status`: 任务状态,`success`=成功,`failed`=失败,`pending`=处理中 +- `ready`: 是否已经完成 +- `failed`: 是否已经失败 + +### Wiki_move 场景返回 + +```json +{ + "scenario": "wiki_move", + "task_id": "", + "ready": true, + "failed": false, + "status": 0, + "status_msg": "success", + "wiki_token": "wikcnXXX", + "node_token": "wikcnXXX", + "space_id": "", + "obj_token": "", + "obj_type": "docx", + "parent_node_token": "", + "node_type": "origin", + "origin_node_token": "", + "title": "项目计划", + "has_child": false, + "node": { + "space_id": "", + "node_token": "wikcnXXX", + "obj_token": "", + "obj_type": "docx", + "parent_node_token": "", + "node_type": "origin", + "origin_node_token": "", + "title": "项目计划", + "has_child": false + }, + "move_results": [ + { + "status": 0, + "status_msg": "success", + "node": { "...": "同上" } + } + ] +} +``` + +**字段说明:** +- `ready`: 所有 `move_results[].status` 都为 `0` 时为 `true` +- `failed`: 任一 `move_results[].status` 小于 `0` 时为 `true` +- `status` / `status_msg`: 第一个 move_result 的状态码 / 标签(无结果时回退为 `1` / `processing`) +- `wiki_token` / `node_token`: 移入 Wiki 后的目标节点 token(首个结果有 `node.node_token` 时镜像到顶层,便于下游脚本使用) +- `space_id`、`obj_token`、`obj_type`、`title` 等:从首个 `move_results[0].node` 平铺到顶层,方便直接引用 +- `move_results`: 保留完整列表(适用于一次任务移动多个文档的场景) + +### Wiki_delete_space 场景返回 + +```json +{ + "scenario": "wiki_delete_space", + "task_id": "", + "ready": true, + "failed": false, + "status": "success", + "status_msg": "success" +} +``` + +**字段说明:** +- `ready`: `status=success` 时为 `true` +- `failed`: `status=failure` 或 `failed` 时为 `true`;未知非成功状态(如 `processing`)视为进行中 +- `status`: 服务端返回的原始 `delete_space_result.status` +- `status_msg`: 优先使用 `delete_space_result.status_msg`,否则回落到 `status`,再回落到 `processing` + +## 使用场景 + +### 配合 +import 使用 + +```bash +# 1. 创建导入任务 +lark-cli drive +import --file ./data.xlsx --type sheet +# 若任务很快完成:直接返回 token / url +# 若内置轮询超时:返回 ready=false、ticket 和 next_command + +# 2. 轮询导入结果 +lark-cli drive +task_result --scenario import --ticket +# 如果这里返回 ready=true 且使用 --as bot,结果还会包含 permission_grant +``` + +### 配合 +move 使用 + +```bash +# 1. 移动文件夹(异步操作) +lark-cli drive +move --file-token --type folder --folder-token +# 若轮询窗口内完成:直接返回 ready=true +# 若内置轮询结束仍未完成:返回 ready=false、task_id 和 next_command + +# 2. 轮询移动结果 +lark-cli drive +task_result --scenario task_check --task-id +``` + +### 配合 wiki +move 使用 + +```bash +# 1. 把 Drive 文档迁入 Wiki(异步任务可能返回 task_id) +lark-cli wiki +move --obj-type docx --obj-token --target-space-id +# 若内置轮询窗口内完成:直接返回 ready=true 和 wiki_token +# 若轮询窗口结束仍未完成:返回 ready=false、task_id、timed_out=true 和 next_command + +# 2. 续跑查询 Wiki 移动结果(next_command 即下面这条) +lark-cli drive +task_result --scenario wiki_move --task-id --as user +``` + +> **身份保持一致**:续跑命令的 `--as` 必须与原 `wiki +move` 调用一致;`wiki +move` 的 `next_command` 已自动带上正确的 `--as`。 + +### 配合 wiki +delete-space 使用 + +```bash +# 1. 删除知识空间(高风险写操作,必须显式带 --yes;接口可能同步返回空 task_id,也可能返回异步 task_id) +lark-cli wiki +delete-space --space-id --yes +# 若同步返回:直接 ready=true +# 若轮询窗口结束仍未完成:返回 ready=false、task_id、timed_out=true 和 next_command + +# 2. 续跑查询 Wiki 删除结果(next_command 即下面这条) +lark-cli drive +task_result --scenario wiki_delete_space --task-id --as user +``` + +### 配合 +export 使用 + +```bash +# 1. 发起导出 +lark-cli drive +export --token --doc-type docx --file-extension pdf +# 若轮询窗口内完成:直接下载本地文件 +# 若内置轮询结束仍未完成:返回 ready=false、ticket 和 next_command + +# 2. 继续查询导出结果 +lark-cli drive +task_result --scenario export --ticket --file-token + +# 3. 拿到 file_token 后下载 +lark-cli drive +export-download --file-token +``` + +## 权限要求 + +| 场景 | 所需 scope | +|------|-----------| +| import | `drive:drive.metadata:readonly` | +| export | `drive:drive.metadata:readonly` | +| task_check | `drive:drive.metadata:readonly` | +| wiki_move | `wiki:space:read` | +| wiki_delete_space | `wiki:space:read` | + +> [!NOTE] +> `import` 场景在 `--as bot` 且任务最终就绪时,还可能额外尝试一次协作者授权;如果 `permission_grant.status = failed`,请根据失败信息检查应用是否具备相应的文档协作者授权能力。 + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-drive/references/lark-drive-upload.md b/.agents/skills/lark-drive/references/lark-drive-upload.md new file mode 100644 index 0000000..a509b32 --- /dev/null +++ b/.agents/skills/lark-drive/references/lark-drive-upload.md @@ -0,0 +1,67 @@ + +# drive +upload + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +上传本地文件到飞书云空间。 + +## 命令 + +```bash +# 推荐:使用 shortcut 一步上传 +lark-cli drive +upload --file ./report.pdf --folder-token fldbc_xxx + +# 自定义上传后的文件名 +lark-cli drive +upload --file ./report.pdf --name "季度总结.pdf" + +# 生成可用临时下载链接的上传方式(素材上传,适用于后续用 curl 下载) +# 注意:需要可写 docx 文档 ID(用于挂载素材 block),且文件最大 20MB +lark-cli drive +upload --as-media --doc docx_xxx --file ./report.pdf + +# 取出 tmp_download_url 后可直接 curl 下载 +curl -L -o report.pdf "" + +# 原生命令(高级/分片上传):预上传 + 完成上传 +lark-cli drive files upload_prepare --data '{ + "file_name": "report.pdf", + "parent_type": "explorer", + "parent_node": "fldbc_xxx", + "size": 1048576 +}' +lark-cli drive files upload_finish --data '{ + "upload_id": "", + "block_num": 1 +}' + +# 查看完整参数定义 +lark-cli schema drive.files.upload_prepare +``` + +> [!IMPORTANT] +> 如果文件是**以应用身份(bot)上传**的,如 `lark-cli drive +upload --as bot` 在上传成功后,CLI 会**尝试为当前 CLI 用户自动授予该文件的 `full_access`(可管理权限)**。 +> +> 以应用身份上传时,结果里会额外返回 `permission_grant` 字段,明确说明授权结果: +> - `status = granted`:当前 CLI 用户已获得该文件的可管理权限 +> - `status = skipped`:本地没有可用的当前用户 `open_id`,因此不会自动授权;可提示用户先完成 `lark-cli auth login`,再让 AI / agent 继续使用应用身份(bot)授予当前用户权限 +> - `status = failed`:文件已上传成功,但自动授权用户失败;会带上失败原因,并提示稍后重试或继续使用 bot 身份处理该文件 +> +> `permission_grant.perm = full_access` 表示该资源已授予“可管理权限”。 +> +> **不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。 + +参数(预上传 `--data` JSON body): + +| 字段 | 必填 | 说明 | +|------|------|------| +| `file_name` | 是 | 文件名 | +| `parent_type` | 是 | 父节点类型,如 `"explorer"` | +| `parent_node` | 是 | 父节点 token(文件夹 token) | +| `size` | 是 | 文件大小(字节) | + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。 + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数 diff --git a/.agents/skills/lark-event/SKILL.md b/.agents/skills/lark-event/SKILL.md new file mode 100644 index 0000000..c015fe8 --- /dev/null +++ b/.agents/skills/lark-event/SKILL.md @@ -0,0 +1,145 @@ +--- +name: lark-event +version: 1.0.0 +description: "Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume ` (covers IM message receive, reactions, chat member changes, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses." +metadata: + requires: + bins: ["lark-cli"] + cliHelp: "lark-cli event --help" +--- + +# Lark Events + +> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md) first for authentication, `--as user/bot` switching, `Permission denied` handling, and safety rules. + +## Core commands + +| Command | Purpose | +|------|------| +| `lark-cli event list [--json]` | List all subscribable EventKeys | +| `lark-cli event schema [--json]` | Show an EventKey's params and output schema | +| `lark-cli event consume [flags]` | Blocking consume; events → stdout NDJSON | +| `lark-cli event status [--json] [--fail-on-orphan]` | Inspect the local bus daemon status | +| `lark-cli event stop [--all] [--force]` | Stop the bus daemon | + + +## Common flags + +| Flag | Description | +|---|---| +| `--param key=value` / `-p` | Business params (repeatable; comma-separated for multi-value). Unknown keys fail with valid names listed inline | +| `--jq ` | jq expression to filter / transform each event; empty output skips the event | +| `--max-events N` | Exit after N events. Default 0 = unlimited | +| `--timeout D` | Exit after duration D (e.g. `30s`, `2m`). Default 0 = no timeout. Whichever of `--max-events` / `--timeout` fires first wins | +| `--output-dir ` | Write each event as a file (relative paths only; prevents traversal) | +| `--quiet` | Suppress stderr diagnostics. **AI should not use this** — it silences the ready marker | +| `--as user\|bot\|auto` | Identity for the session (see lark-shared) | + + +## Examples + +```bash +# Default: stream every event for the key (no filter, no projection) +lark-cli event consume im.message.receive_v1 --as bot + +# Grab one sample event to inspect payload shape +lark-cli event consume im.message.receive_v1 --max-events 1 --timeout 30s --as bot + +# Run for 10 minutes then auto-exit +lark-cli event consume im.message.receive_v1 --timeout 10m --as bot + +# Consume multiple EventKeys concurrently (one shape per process, no dispatcher) +lark-cli event consume im.message.receive_v1 --as bot > receive.ndjson & +lark-cli event consume im.message.reaction.created_v1 --as bot > reaction.ndjson & +wait + +``` + +## Call flow + +1. `lark-cli event list --json` → pick a legal key +2. `lark-cli event schema --json` → read `resolved_output_schema` + `jq_root_path` to determine field paths +3. `lark-cli event consume [--jq '']` → consume + +## Subprocess contract + +### Ready marker + +`event consume`'s stderr emits a fixed line `[event] ready event_key=`. **Parent processes should block on stderr until this line appears, then start reading stdout.** Do not fall back to `sleep`. + +### stdin EOF = graceful exit + +`event consume` treats stdin close as a shutdown signal (wired for AI subprocess callers). `< /dev/null` / `nohup` / systemd's default `StandardInput=null` will cause an immediate graceful exit (stderr `reason: signal`). To keep running: + +- Feed stdin a source that never EOFs: `< <(tail -f /dev/null)` +- Or run bounded: `--max-events N` / `--timeout D` + +### Exit codes & reason + +On exit, the last stderr line is `[event] exited — received N event(s) in Xs (reason: ...)`. + +| exit code | reason | Trigger | +|---|---|---| +| 0 | `reason: limit` | `--max-events` reached | +| 0 | `reason: timeout` | `--timeout` reached | +| 0 | `reason: signal` | Ctrl+C / SIGTERM / stdin EOF | +| non-0 | `Error: ...` (no `exited` line) | Startup / runtime failure (permissions, network, params, config) | + +Orchestrators should treat `reason: limit/timeout/signal` (all exit 0) as "business completion" and non-zero as "failure". + +### Never `kill -9` + +**Avoid `kill -9` on consume processes**: for EventKeys with a **PreConsume hook** (those that register server-side subscriptions via OAPI), `kill -9` skips the OAPI unsubscribe and leaks server-side subscriptions (symptoms: "subscription already exists" on restart, duplicate event delivery). Prefer SIGTERM or closing stdin. + +### One consume, one EventKey (multi-key = multi-shell) + +The command takes exactly one positional argument; `k1,k2` and wildcards are unsupported. Listening to N keys means N subprocesses — this is **intentional**: + +- One shape per process stdout; no dispatcher logic required in the AI +- Fault isolation (one key failing doesn't affect others) +- Independent `--as` / `--jq` / `--max-events` / `--timeout` per key + +All N consumers share a single bus daemon (UDS local IPC), so the overhead is small + +## Writing jq via schema + +`event schema --json` is the source of truth for writing `--jq`. Four things to look at: + +**(1) Where fields start** — see `jq_root_path` + +- Value `"."` → fields are at the top level, write `.chat_id` +- Value `".event"` → fields are inside a V2 envelope, write `.event.chat_id` + +**(2) Field list and types** — see `resolved_output_schema.properties.` + +Each field carries `type` / `description`, and some also have `format`. Snippet (from `event schema im.message.receive_v1 --json`): + +```json +{ + "chat_id": {"type":"string", "format":"chat_id", "description":"Chat ID, prefixed with oc_"}, + "sender_id": {"type":"string", "format":"open_id", "description":"Sender open_id, prefixed with ou_"}, + "create_time": {"type":"string", "format":"timestamp_ms", "description":"Send time as ms-epoch string"} +} +``` + +**(3) Field semantics** — see the `format` tag + +Lark-defined semantic tags (**not** JSON Schema's standard `format`). Common values: `open_id` / `chat_id` / `message_id` / `timestamp_ms` / `email`. Purpose: distinguish "same string type, different meanings" fields so you can reverse-lookup via API or convert formats. + +**(4) Decoded state** — read the field's `description` + +`event consume` runs Process hooks that may pre-decode some payload fields (flattening V2 envelopes, rendering `.content` to plain text, etc.) — behavior differs from raw OAPI. **Always read the field's `description` before writing jq**, especially for generic field names like `content` / `data` / `body` / `payload`. + +**Why it matters**: blindly applying `fromjson` to an already-decoded text field makes jq error on every event and silently drop it — the consumer looks alive but emits nothing, with only a single `WARN` line buried on stderr. (This is the general behavior: any jq runtime error skips the event with a one-line WARN; the loop does not abort.) + +**Don't shortcut the schema**: when projecting `event schema --json` with jq, do not strip `.description` from `properties` — that's the field that tells you whether a field is already decoded. Dump the full property objects, not just keys. + +--- + +**Aside**: `--param`'s valid parameters also live in the schema — the `params` section lists `name` / `type` / `required` / `enum` / `default` / `description`; **section missing = this key accepts no `--param`**. + +## Topic index + +| Topic | Reference | Coverage | +|---|---|---| +| IM | [`references/lark-event-im.md`](references/lark-event-im.md) | Catalog of 11 IM EventKeys + shape notes (flat vs V2 envelope) + `im.message.receive_v1` field gotchas (`sender_id` is open_id only; `.content` is plain text except for `interactive` cards) + common jq recipes (filter by chat_type / message_type / sender) | diff --git a/.agents/skills/lark-event/references/lark-event-im.md b/.agents/skills/lark-event/references/lark-event-im.md new file mode 100644 index 0000000..113a877 --- /dev/null +++ b/.agents/skills/lark-event/references/lark-event-im.md @@ -0,0 +1,86 @@ +# IM Events + +> **Prerequisite:** Read [`../SKILL.md`](../SKILL.md) first for the `event consume` essentials (commands, subprocess contract, jq usage). +> +> **Heads-up for AI agents**: this key's `.content` is **NOT** the raw OAPI payload shape your training data may suggest. `lark-cli` runs a Process hook (`convertlib`) that flattens the V2 envelope and **pre-renders** `.content` to human-readable text for `text` / `post` / `image` / `file` / `audio` / etc. Only `interactive` (cards) keeps the raw JSON string. Don't blindly `fromjson`. + +## Key catalog (11) + +| EventKey | Purpose | +|---|---| +| `im.message.receive_v1` | Receive IM messages | +| `im.message.message_read_v1` | User read a bot's **p2p** message (group messages don't fire this) | +| `im.message.reaction.created_v1` | Reaction added to a message | +| `im.message.reaction.deleted_v1` | Reaction removed from a message | +| `im.chat.updated_v1` | Chat settings changed (owner, avatar, name, permissions, etc.) | +| `im.chat.disbanded_v1` | Chat disbanded | +| `im.chat.member.bot.added_v1` | Bot added to a chat | +| `im.chat.member.bot.deleted_v1` | Bot removed from a chat | +| `im.chat.member.user.added_v1` | User joined a chat (including topic chats) | +| `im.chat.member.user.deleted_v1` | User left voluntarily **or** was removed | +| `im.chat.member.user.withdrawn_v1` | Pending chat invite withdrawn (inviter canceled; user never actually joined) | + +> **Shape**: `im.message.receive_v1` is the only flat key (fields at `.xxx`); the other 10 are V2-enveloped (fields at `.event.xxx`). + +## Gotchas (`im.message.receive_v1`) + +**sender_id is open_id only**: the event payload carries no display name. Call the contact API separately if you need the sender's name. + +**`.content` shape depends on `message_type`** (this key uses a flat Custom schema; see [`events/im/message_receive.go`](../../../events/im/message_receive.go)): + +| message_type | `.content` shape | How to read | +|---|---|---| +| `text` / `post` / `image` / `file` / `audio` / `sticker` / `share_chat` / `share_user` / `media` / `system` | Human-readable text (convertlib-processed; `@mentions` resolved to display names) | Use `.content` directly | +| `interactive` (card) | Raw card JSON string (structured actions can't be losslessly flattened) | `.content \| fromjson` to get the card object | + +**Do not blindly `fromjson`** — for non-interactive messages it fails with `jq: fromjson cannot be applied to "hello"` because `.content` isn't JSON-encoded. + +```bash +# text: .content is plain text — no fromjson needed +lark-cli event consume im.message.receive_v1 --as bot \ + --jq 'select(.message_type=="text") | .content' + +# interactive: .content is a JSON string — fromjson to parse +lark-cli event consume im.message.receive_v1 --as bot \ + --jq 'select(.message_type=="interactive") | .content | fromjson' +``` + +## On-demand filter recipes + +> **Default = no `--jq`.** Run `lark-cli event consume im.message.receive_v1 --as bot` to see every message. The recipes below are only for cases where the user has asked to narrow the stream. + +### 1. Filter by chat type (p2p vs group) + +`chat_type` is an enum with values `p2p` / `group`. + +```bash +# p2p only (direct messages) +lark-cli event consume im.message.receive_v1 --as bot \ + --jq 'select(.chat_type=="p2p") | {from: .sender_id, msg: .content}' + +# group only +lark-cli event consume im.message.receive_v1 --as bot \ + --jq 'select(.chat_type=="group") | {chat: .chat_id, from: .sender_id, msg: .content}' +``` + +### 2. Filter by message type + +```bash +# text only — content is plain human-readable text +lark-cli event consume im.message.receive_v1 --as bot \ + --jq 'select(.message_type=="text") | .content' + +# interactive (card) only — parse the card body +lark-cli event consume im.message.receive_v1 --as bot \ + --jq 'select(.message_type=="interactive") | .content | fromjson' +``` + +### 3. Filter by sender (only one user's messages) + +```bash +# example: only messages from the given open_id +lark-cli event consume im.message.receive_v1 --as bot\ + --jq 'select(.sender_id=="ou_xxxxxxxxxxxxxxxxxxxxxxxxxx") | {msg_id: .message_id, text: .content}' +``` + +Get your own open_id via `lark-cli contact +get-user --as user`; other users' via `lark-cli contact +search-user`. \ No newline at end of file diff --git a/.agents/skills/lark-im/SKILL.md b/.agents/skills/lark-im/SKILL.md new file mode 100644 index 0000000..7388067 --- /dev/null +++ b/.agents/skills/lark-im/SKILL.md @@ -0,0 +1,142 @@ +--- +name: lark-im +version: 1.0.0 +description: "飞书即时通讯:收发消息和管理群聊。发送和回复消息、搜索聊天记录、管理群聊成员、上传下载图片和文件(支持大文件分片下载)、管理表情回复。当用户需要发消息、查看或搜索聊天记录、下载聊天中的文件、查看群成员时使用。" +metadata: + requires: + bins: ["lark-cli"] + cliHelp: "lark-cli im --help" +--- + +# im (v1) + +**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),其中包含认证、权限处理** + +## Core Concepts + +- **Message**: A single message in a chat, identified by `message_id` (om_xxx). Supports types: text, post, image, file, audio, video, sticker, interactive (card), share_chat, share_user, merge_forward, etc. +- **Chat**: A group chat or P2P conversation, identified by `chat_id` (oc_xxx). +- **Thread**: A reply thread under a message, identified by `thread_id` (om_xxx or omt_xxx). +- **Reaction**: An emoji reaction on a message. + +## Resource Relationships + +``` +Chat (oc_xxx) +├── Message (om_xxx) +│ ├── Thread (reply thread) +│ ├── Reaction (emoji) +│ └── Resource (image / file / video / audio) +└── Member (user / bot) +``` + +## Important Notes + +### Identity and Token Mapping + +- `--as user` means **user identity** and uses `user_access_token`. Calls run as the authorized end user, so permissions depend on both the app scopes and that user's own access to the target chat/message/resource. +- `--as bot` means **bot identity** and uses `tenant_access_token`. Calls run as the app bot, so behavior depends on the bot's membership, app visibility, availability range, and bot-specific scopes. +- If an IM API says it supports both `user` and `bot`, the token type changes who the operator is. The same API can succeed with one identity and fail with the other because owner/admin status, chat membership, tenant boundary, or app availability are checked against the current caller. + +### Sender Name Resolution with Bot Identity + +When using bot identity (`--as bot`) to fetch messages (e.g. `+chat-messages-list`, `+threads-messages-list`, `+messages-mget`), sender names may not be resolved (shown as open_id instead of display name). This happens when the bot cannot access the user's contact info. + +**Root cause**: The bot's app visibility settings do not include the message sender, so the contact API returns no name. + +**Solution**: Check the app's visibility settings in the Lark Developer Console — ensure the app's visible range covers the users whose names need to be resolved. Alternatively, use `--as user` to fetch messages with user identity, which typically has broader contact access. + +### Card Messages (Interactive) + +Card messages (`interactive` type) are not yet supported for compact conversion in event subscriptions. The raw event data will be returned instead, with a hint printed to stderr. + +## Shortcuts(推荐优先使用) + +Shortcut 是对常用操作的高级封装(`lark-cli im + [flags]`)。有 Shortcut 的操作优先使用。 + +| Shortcut | 说明 | +|----------|------| +| [`+chat-create`](references/lark-im-chat-create.md) | Create a group chat; user/bot; creates private/public chats, invites users/bots, optionally sets bot manager | +| [`+chat-messages-list`](references/lark-im-chat-messages-list.md) | List messages in a chat or P2P conversation; user/bot; accepts --chat-id or --user-id, resolves P2P chat_id, supports time range/sort/pagination | +| [`+chat-search`](references/lark-im-chat-search.md) | Search visible group chats by keyword and/or member open_ids (e.g. look up chat_id by group name); user/bot; supports member/type filters, sorting, and pagination | +| [`+chat-update`](references/lark-im-chat-update.md) | Update group chat name or description; user/bot; updates a chat's name or description | +| [`+messages-mget`](references/lark-im-messages-mget.md) | Batch get messages by IDs; user/bot; fetches up to 50 om_ message IDs, formats sender names, expands thread replies | +| [`+messages-reply`](references/lark-im-messages-reply.md) | Reply to a message (supports thread replies); user/bot; supports text/markdown/post/media replies, reply-in-thread, idempotency key | +| [`+messages-resources-download`](references/lark-im-messages-resources-download.md) | Download images/files from a message; user/bot; supports automatic chunked download for large files (8MB chunks), auto-detects file extension from Content-Type | +| [`+messages-search`](references/lark-im-messages-search.md) | Search messages across chats (supports keyword, sender, time range filters) with user identity; user-only; filters by chat/sender/attachment/time, supports auto-pagination via `--page-all` / `--page-limit`, enriches results via batched mget and chats batch_query | +| [`+messages-send`](references/lark-im-messages-send.md) | Send a message to a chat or direct message; user/bot; sends to chat-id or user-id with text/markdown/post/media, supports idempotency key | +| [`+threads-messages-list`](references/lark-im-threads-messages-list.md) | List messages in a thread; user/bot; accepts om_/omt_ input, resolves message IDs to thread_id, supports sort/pagination | + +## API Resources + +```bash +lark-cli schema im.. # 调用 API 前必须先查看参数结构 +lark-cli im [flags] # 调用 API +``` + +> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。 + +### chats + + - `create` — 创建群。Identity: `bot` only (`tenant_access_token`). + - `get` — 获取群信息。Identity: supports `user` and `bot`; the caller must be in the target chat to get full details, and must belong to the same tenant for internal chats. + - `link` — 获取群分享链接。Identity: supports `user` and `bot`; the caller must be in the target chat, must be an owner or admin when chat sharing is restricted to owners/admins, and must belong to the same tenant for internal chats. + - `list` — 获取用户或机器人所在的群列表。Identity: supports `user` and `bot`. + - `update` — 更新群信息。Identity: supports `user` and `bot`. + +### chat.members + + - `bots` — 获取群内机器人列表。 Identity: supports `user` and `bot`; the caller must be in the target chat and must belong to the same tenant for internal chats. + - `create` — 将用户或机器人拉入群聊。Identity: supports `user` and `bot`; the caller must be in the target chat; for `bot` calls, added users must be within the app's availability; for internal chats the operator must belong to the same tenant; if only owners/admins can add members, the caller must be an owner/admin, or a chat-creator bot with `im:chat:operate_as_owner`. + - `delete` — 将用户或机器人移出群聊。Identity: supports `user` and `bot`; only group owner, admin, or creator bot can remove others; max 50 users or 5 bots per request. + - `get` — 获取群成员列表。Identity: supports `user` and `bot`; the caller must be in the target chat and must belong to the same tenant for internal chats. + +### messages + + - `delete` — 撤回消息。Identity: supports `user` and `bot`; for `bot` calls, the bot must be in the chat to revoke group messages; to revoke another user's group message, the bot must be the owner, an admin, or the creator; for user P2P recalls, the target user must be within the bot's availability. + - `forward` — 转发消息。Identity: `bot` only (`tenant_access_token`). + - `merge_forward` — 合并转发消息。Identity: `bot` only (`tenant_access_token`). + - `read_users` — 查询消息已读信息。Identity: `bot` only (`tenant_access_token`); the bot must be in the chat, and can only query read status for messages it sent within the last 7 days. + +### reactions + + - `batch_query` — 批量获取消息表情。Identity: supports `user` and `bot`.[Must-read](references/lark-im-reactions.md) + - `create` — 添加消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message.[Must-read](references/lark-im-reactions.md) + - `delete` — 删除消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message, and can only delete reactions added by itself.[Must-read](references/lark-im-reactions.md) + - `list` — 获取消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message.[Must-read](references/lark-im-reactions.md) + +### images + + - `create` — 上传图片。Identity: `bot` only (`tenant_access_token`). + +### pins + + - `create` — Pin 消息。Identity: supports `user` and `bot`. + - `delete` — 移除 Pin 消息。Identity: supports `user` and `bot`. + - `list` — 获取群内 Pin 消息。Identity: supports `user` and `bot`. + +## 权限表 + +| 方法 | 所需 scope | +|------|-----------| +| `chats.create` | `im:chat:create` | +| `chats.get` | `im:chat:read` | +| `chats.link` | `im:chat:read` | +| `chats.list` | `im:chat:read` | +| `chats.update` | `im:chat:update` | +| `chat.members.bots` | `im:chat.members:read` | +| `chat.members.create` | `im:chat.members:write_only` | +| `chat.members.delete` | `im:chat.members:write_only` | +| `chat.members.get` | `im:chat.members:read` | +| `messages.delete` | `im:message:recall` | +| `messages.forward` | `im:message` | +| `messages.merge_forward` | `im:message` | +| `messages.read_users` | `im:message:readonly` | +| `reactions.batch_query` | `im:message.reactions:read` | +| `reactions.create` | `im:message.reactions:write_only` | +| `reactions.delete` | `im:message.reactions:write_only` | +| `reactions.list` | `im:message.reactions:read` | +| `images.create` | `im:resource` | +| `pins.create` | `im:message.pins:write_only` | +| `pins.delete` | `im:message.pins:write_only` | +| `pins.list` | `im:message.pins:read` | diff --git a/.agents/skills/lark-im/references/lark-im-chat-create.md b/.agents/skills/lark-im/references/lark-im-chat-create.md new file mode 100644 index 0000000..2a9fda2 --- /dev/null +++ b/.agents/skills/lark-im/references/lark-im-chat-create.md @@ -0,0 +1,156 @@ +# im +chat-create + +> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules. + +Create a group chat. Supports both user identity (`--as user`) and bot identity (`--as bot`). You can specify the group name, description, members (users/bots), owner, and chat type (private/public). + +This skill maps to the shortcut: `lark-cli im +chat-create` (internally calls `POST /open-apis/im/v1/chats`). + +- `--as bot` requires the `im:chat:create` scope. +- `--as user` requires the `im:chat:create_by_user` scope. + +## Commands + +```bash +# Create a private group (default) +lark-cli im +chat-create --name "My Group" + +# Create a public group (name is required and must be at least 2 characters) +lark-cli im +chat-create --name "Public Group" --type public + +# Specify the group owner +lark-cli im +chat-create --name "My Group" --owner ou_xxx + +# Invite user members (comma-separated open_ids, up to 50) +lark-cli im +chat-create --name "My Group" --users "ou_aaa,ou_bbb" + +# Invite bot members (comma-separated app IDs, up to 5) +lark-cli im +chat-create --name "My Group" --bots "cli_aaa,cli_bbb" + +# Invite both users and bots +lark-cli im +chat-create --name "My Group" --users "ou_aaa" --bots "cli_aaa" + +# Make the creating bot a group manager (bot identity only) +lark-cli im +chat-create --name "My Group" --set-bot-manager --as bot + +# JSON output +lark-cli im +chat-create --name "My Group" --format json + +# Create a group with bot identity +lark-cli im +chat-create --name "My Group" --users "ou_aaa" --as bot + +# Create a group with user identity +lark-cli im +chat-create --name "My Group" --users "ou_aaa,ou_bbb" --as user + +# Preview the request without creating anything +lark-cli im +chat-create --name "My Group" --dry-run +``` + +## Parameters + +| Parameter | Required | Limits | Description | +|------|------|------|------| +| `--name ` | Required for public groups | Max 60 characters; at least 2 characters for public groups | Group name (`"(no subject)"` for private groups if omitted) | +| `--description ` | No | Max 100 characters | Group description | +| `--users ` | No | Up to 50, format `ou_xxx` | Comma-separated user open_ids | +| `--bots ` | No | Up to 5, format `cli_xxx` | Comma-separated bot app IDs | +| `--owner ` | No | Format `ou_xxx` | Owner open_id (defaults to the bot when using `--as bot`, or the authorized user when using `--as user`) | +| `--type ` | No | `private` (default) or `public` | Group type | +| `--set-bot-manager` | No | - | Set the creating bot as a group manager (only effective with `--as bot`) | +| `--format json` | No | - | Output as JSON | +| `--as ` | No | `bot` or `user` | Identity type | +| `--dry-run` | No | - | Preview the request without executing it | + +## AI Usage Guidance + +### When using `--as bot` + +Bot may fail to invite users who are mutually invisible to it during group creation (error 232043). To avoid this, use the **two-step flow** below instead of passing other users' open_ids in `--users`. + +1. **Get the current user's open_id:** Run `lark-cli contact +search-user --query ""` to retrieve it. +2. **Create the group — by default include the current user:** + + ```bash + lark-cli im +chat-create --name "" \ + --users "" --as bot + ``` + + **Default behavior:** Always add the current user to the group, unless the user explicitly says "do not add me" or "bot-only group" — only then omit `--users`. + +3. **Add other members via user identity** (requires the current user to be in the group): + + ```bash + lark-cli im chat.members create \ + --params '{"chat_id":"","member_id_type":"open_id","succeed_type":1}' \ + --data '{"id_list":["ou_aaa","ou_bbb"]}' \ + --as user + ``` + + `succeed_type=1` ensures reachable users are added successfully; unreachable ones are returned in `invalid_id_list` instead of failing the whole request. + +4. **Check `invalid_id_list`** in the response. If non-empty, report to the user which members could not be added. + +### When using `--as user` + +User identity does not have the bot visibility limitation, so you can create the group and invite members in one step: + +```bash +lark-cli im +chat-create --name "" --users "ou_aaa,ou_bbb" --as user +``` + +The authorized user is automatically the group creator and member. + +## Output Fields + +| Field | Description | +|------|------| +| `chat_id` | The new group's ID (`oc_xxx` format) | +| `name` | Group name | +| `chat_type` | Group type (`private` / `public`) | +| `owner_id` | Owner ID (may be empty when a bot creates the group and `--owner` is not specified) | +| `external` | Whether the group is external | +| `share_link` | Group share link (omitted if retrieval fails) | + +## Usage Scenarios + +### Scenario 1: Create a group and specify the owner + +```bash +lark-cli im +chat-create --name "Project Discussion Group" --owner ou_xxx +``` + +### Scenario 2: Create a group and invite users and a bot + +```bash +lark-cli im +chat-create --name "Project Discussion Group" \ + --owner ou_xxx \ + --users "ou_aaa,ou_bbb" \ + --bots "cli_aaa" +``` + +### Scenario 3: Create a group and send a welcome message + +```bash +CHAT_ID=$(lark-cli im +chat-create --name "New Group" --format json | jq -r '.data.chat_id') +lark-cli im +messages-send --chat-id "$CHAT_ID" --text "Welcome, everyone!" +``` + +## Common Errors and Troubleshooting + +| Symptom | Root Cause | Solution | +|---------|---------|---------| +| Permission denied (99991672) | The app does not have `im:chat:create` (bot) or `im:chat:create_by_user` (user) permission enabled | Enable the required permission for the app in the Open Platform console | +| `--name is required for public groups and must be at least 2 characters` | A public group was created without a name or with a name shorter than 2 characters | Provide a name with at least 2 characters | +| `--name exceeds the maximum of 60 characters` | The group name is too long | Shorten the name to 60 characters or fewer | +| `--description exceeds the maximum of 100 characters` | The group description is too long | Shorten the description to 100 characters or fewer | +| `--users exceeds the maximum of 50` | Too many user members were provided | Split the operation into batches and add more members later | +| `--bots exceeds the maximum of 5` | Too many bot members were provided | Invite at most 5 bots at once | +| `invalid user id: expected open_id (ou_xxx)` | Invalid user ID format | Use the `ou_xxx` format for users | +| `invalid bot id: expected app ID (cli_xxx)` | Invalid bot ID format | Use the `cli_xxx` format for bots | +| `invalid --owner: expected open_id (ou_xxx)` | Invalid owner ID format | Use the `ou_xxx` format for the owner | +| `bot is invisible to user` (232043) | The bot and target users are mutually invisible | Follow the two-step flow in AI Usage Guidance above — do not pass other users in `--users` during creation | + +## References + +- [lark-im](../SKILL.md) - all IM commands +- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters diff --git a/.agents/skills/lark-im/references/lark-im-chat-identity.md b/.agents/skills/lark-im/references/lark-im-chat-identity.md new file mode 100644 index 0000000..53a9f63 --- /dev/null +++ b/.agents/skills/lark-im/references/lark-im-chat-identity.md @@ -0,0 +1,55 @@ +# Group Chat Identity Rules + +> Warning: The most common source of failure in group operations is choosing the wrong identity. Confirm the identity before performing the action. + +Group-chat operations support both `--as user` (UAT user identity) and `--as bot` (TAT bot identity). Choosing the correct identity is critical for success. + +## Basic Principles + +- **If the user explicitly specifies an identity:** use exactly what the user requested (`--as user` or `--as bot`) without guessing. +- **If the user does not specify an identity:** infer the correct identity from context instead of relying on the default. + +## Identity Selection by Operation + +| Operation | Recommended Identity | Why | +|------|---------|-----------------------------------| +| Create group (`+chat-create`) | Depends on the scenario | Infer from context | +| Add members (member-management flow) | `--as user` | Bot visibility is limited and often fails when the target user is mutually invisible to the bot (232024) | +| Update group (`+chat-update`) | Owner identity | Permission changes require owner/admin privileges; owner transfer requires owner identity | + +## Inferring the Owner + +When an owner-level action is needed and the owner is unknown, infer in this order: + +1. A bot created the group and `--owner` was **not** specified -> the owner is the bot (`--as bot`) +2. A bot created the group and `--owner ou_xxx` **was** specified -> the owner is that user (`--as user`) +3. A user created the group and `--owner` was **not** specified -> the owner is the current user (`--as user`) +4. Still unclear -> ask the user to confirm who owns the group before making owner-level changes + +### When the Owner Is Neither the Current User Nor the Bot + +If the query shows that the owner is a third-party user (`owner_id` is neither the currently authorized user nor the bot), the current identity does not have owner privileges. In that case: + +- **Permission/setting changes:** if the bot is an admin of the group, `--as bot` can still perform admin-level operations such as renaming the group or changing permissions. +- **Owner-only actions such as owner transfer:** require the actual owner to complete UAT authorization via `lark-cli auth login`, then perform the action as that owner. +- Explain the limitation clearly to the user instead of retrying blindly. + +## Common Pitfalls + +### Inviting Members During Group Creation + +If a bot creates a group and `--users` includes users who are mutually invisible to the bot, the entire request fails with 232043. Use two steps instead: + +1. Create the group with the bot first, excluding invisible users: `lark-cli im +chat-create --name "Group Name"` +2. Add users later with a user-identity member-management flow + +### Insufficient Privileges + +- **232016 / 232002 / 232017:** the current identity is not the owner or an admin -> switch to the owner identity +- **232011:** the current user is not in the group -> use a group-member identity, or join the group first +- **232024:** the bot and the target user are mutually invisible -> switch to `--as user` + +## References + +- [lark-im](../SKILL.md) - all IM commands +- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters diff --git a/.agents/skills/lark-im/references/lark-im-chat-messages-list.md b/.agents/skills/lark-im/references/lark-im-chat-messages-list.md new file mode 100644 index 0000000..a3f61f0 --- /dev/null +++ b/.agents/skills/lark-im/references/lark-im-chat-messages-list.md @@ -0,0 +1,142 @@ +# im +chat-messages-list + +> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules. + +Fetch the message list for a conversation. Supports both group chats and direct messages. + +This skill maps to the shortcut: `lark-cli im +chat-messages-list` (internally calls `GET /open-apis/im/v1/messages`, and automatically resolves the p2p chat_id when needed). + +## Commands + +```bash +# Get group chat messages (json output by default) +lark-cli im +chat-messages-list --chat-id oc_xxx + +# Get direct messages with a user (pass open_id and resolve p2p chat_id automatically) +lark-cli im +chat-messages-list --user-id ou_xxx + +# Specify a time range (ISO 8601) +lark-cli im +chat-messages-list --chat-id oc_xxx --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" + +# Specify a time range (date only) +lark-cli im +chat-messages-list --chat-id oc_xxx --start 2026-03-10 --end 2026-03-11 + +# Control sort order and page size (max 50) +lark-cli im +chat-messages-list --chat-id oc_xxx --sort asc --page-size 20 + +# Pagination +lark-cli im +chat-messages-list --chat-id oc_xxx --page-token "xxx" + +# JSON output +lark-cli im +chat-messages-list --chat-id oc_xxx --format json +``` + +## Parameters + +| Parameter | Required | Description | +|------|------|------| +| `--chat-id ` | One of two | Specify the conversation by its chat_id directly (e.g., group chat `oc_xxx`) | +| `--user-id ` | One of two | Specify a DM conversation by the other user's open_id (`ou_xxx`); p2p chat_id is resolved automatically. Requires user identity (`--as user`); not supported with bot identity | +| `--start