Zurück zu Skills
larksuite/cliPrüfung bestanden

SKILL DETAIL

lark-im

larksuite/cli/lark-im

The lark-im skill is an instant messaging capability provided by the Lark Open Platform, enabling operations such as sending and receiving messages, managing group chats, searching chat history, uploading and downloading files, managing emoji reactions, sending urgent messages, and sending and processing interactive cards via the lark-cli command-line tool. This skill covers core IM features including sending and replying to messages (text, rich text, images, files, audio, video, interactive cards, etc.), searching chat records, managing group members, creating group or topic chats, managing flags, managing feed shortcuts, and handling card callback events. It supports both user and bot identities and provides a set of convenient shortcuts to streamline common operations.

Installationen · 523Quelle ansehen

Installation

npx skills add https://github.com/larksuite/cli --skill lark-im

Skill-Dateien

SKILL.md

Zuletzt synchronisiert · 29.08.2026

references/card/card-2.0-schema.md
# 卡片 2.0 组件大纲

Card 2.0 组件按**容器 / 展示 / 交互**三类,均通过 `tag` 字段声明。先在下表按用途选组件,再点明细看字段:有明细文件的点 `components/<tag>.md`(完整字段+示例+易错点),低频组件点链接看官方文档。

## 根结构

顶层固定四字段,先搭骨架再往 `body.elements` 填组件。以下为**推荐完整骨架**(含 type scale、light/dark color token、header 三件套):

```json
{
  "schema": "2.0",
  "config": {
    "update_multi": true,
    "width_mode": "default",
    "style": {
      "text_size": {
        "title":   { "default": "heading-2", "pc": "heading-2", "mobile": "heading-3" },
        "body":    { "default": "normal",    "pc": "normal",    "mobile": "normal"    },
        "caption": { "default": "notation",  "pc": "notation",  "mobile": "notation"  }
      },
      "color": {
        "cus-primary":    { "light_mode": "rgba(30,120,255,1)",    "dark_mode": "rgba(80,150,255,1)"   },
        "cus-primary-bg": { "light_mode": "rgba(30,120,255,0.08)", "dark_mode": "rgba(80,150,255,0.12)" },
        "cus-muted":      { "light_mode": "rgba(100,106,115,1)",   "dark_mode": "rgba(150,155,163,1)"  }
      }
    }
  },
  "header": {
    "title":    { "tag": "plain_text", "content": "卡片标题" },
    "subtitle": { "tag": "plain_text", "content": "副标题:一句上下文(时间/来源/状态)" },
    "template": "blue",
    "icon": { "tag": "standard_icon", "token": "lark-logo_colorful" },
    "text_tag_list": [
      { "tag": "text_tag", "text": { "tag": "plain_text", "content": "状态标签" }, "color": "blue" }
    ]
  },
  "body": { "direction": "vertical", "padding": "12px 12px 20px 12px", "elements": [] }
}
```

> **按需裁剪**:`subtitle` / `text_tag_list` / color token 按实际诉求取舍,不强制全用。组件里用 `"text_size": "title"` / `"caption"` 引用 token,用 `"font_color": "cus-muted"` 引用颜色 token;主色系变化时只需改 config 里的 RGBA,全卡自动跟随。

- `schema` 必须显式为 `"2.0"`,否则按 1.0 渲染。`header` 详见 `components/header.md`。
- **元素通用字段**(所有 `elements[]` 组件):`tag`(必填) · `element_id`(卡内唯一,字母开头、≤20 字符) · `margin`(外边距 [-99,99]px)。
- `card_link`(整卡跳转):`{url, pc_url, ios_url, android_url}`,至少填 `url`;某端禁跳设 `lark://msgcard/unsupported_action`。
- 硬限制:单卡 ≤ **200** 元素;需客户端 **≥ 7.20**(旧版仅显示 header)。
- 颜色 / 图标枚举见 `resource/colors.md` · `resource/icons.md`。

**config**(全局行为,可整体省略):

| 字段 | 默认 | 说明 |
|---|---|---|
| `update_multi` | true | 共享卡片,v2 仅支持 true |
| `width_mode` | default | `default`(≤600px) / `compact`(400px) / `fill`(撑满) |
| `enable_forward` | true | 是否允许转发 |
| `summary` | — | 会话列表预览:`{content, i18n_content:{zh_cn,en_us,…}}` |
| `streaming_mode` | false | 流式更新模式(配 `streaming_config`) |
| `style.text_size` | — | 自定义字号 token,格式 `{"<名称>":{default,pc,mobile}}`;名称可自定义(如 `title`/`caption`),组件 `text_size` 引用该名称 |
| `style.color` | — | 自定义颜色 token,格式 `{"<名称>":{light_mode,dark_mode}}`(RGBA);名称可自定义(如 `cus-primary`),组件 `font_color`/`background_style` 等字段引用 |

> 多语言:`config.locales` 限定生效语种、`use_custom_translation` 优先用自带 i18n。

**body 布局字段**(均 v2 新增):`direction`(vertical/horizontal) · `padding`([0,99]px) · `horizontal_spacing`/`vertical_spacing`(`small`4/`medium`8/`large`12/`extra_large`16 或 px) · `horizontal_align`/`vertical_align`。

---

## 容器类(布局 / 组织交互)

| 组件 | 用途 |
|---|---|
| [column_set](components/column_set.md) | 横向分栏,多列图文对齐(数据表、字段对、列表) |
| [collapsible_panel](components/collapsible_panel.md) | 折叠面板,收纳备注/长文本等次要信息 |
| [form](components/form.md) | 表单容器,批量录入表单项后一次提交 |
| [interactive_container](components/interactive_container.md) | 整块可点击区域,可统一定义样式与交互 |
| [循环容器](components/recycling_container.md) | 批量渲染同版式不同数据(仅搭建工具) |

## 展示类(无交互)

| 组件 | 用途 |
|---|---|
| [header](components/header.md) | 卡片标题区:主/副标题、后缀标签、主题色 |
| [div](components/div.md) | 普通文本,带前缀图标、字段对 |
| [markdown](components/markdown.md) | 富文本,最常用;@人、彩色、链接、列表、表格等 |
| [img](components/img.md) | 单图 |
| [img_combination](components/img_combination.md) | 多图拼排(双图/三图/宫格) |
| [person](components/person.md) | 单个人员头像/姓名 |
| [person_list](components/person_list.md) | 多个人员头像/姓名 |
| [chart](components/chart.md) | VChart 图表(折线/柱/饼/词云等) |
| [table](components/table.md) | 多列数据表(只能放根节点) |
| [hr](components/hr.md) | 分割线 |

## 交互类

| 组件 | 用途 |
|---|---|
| [button](components/button.md) | 按钮:回调 / 跳转 / 表单提交 |
| [input](components/input.md) | 文本输入框(多嵌在 form 内) |
| [overflow](components/overflow.md) | 折叠按钮组,收纳多个操作 |
| [select_static](components/select_static.md) | 下拉单选 |
| [multi_select_static](components/multi_select_static.md) | 下拉多选 |
| [select_person](components/select_person.md) | 人员单选 |
| [multi_select_person](components/multi_select_person.md) | 人员多选 |
| [date_picker](components/date_picker.md) | 日期选择器 |
| [picker_time](components/picker_time.md) | 时间选择器 |
| [picker_datetime](components/picker_datetime.md) | 日期时间选择器 |
| [select_img](components/select_img.md) | 图片选择(单/多选) |
| [checker](components/checker.md) | 勾选器,任务勾选回调 |
references/card/components/button.md
# 按钮 `button`

交互按钮,支持跳转 / 回调 / 表单提交三类行为。**Card 2.0**。

## 最小示例

```json
{
  "tag": "button",
  "text": { "tag": "plain_text", "content": "确定" },
  "type": "primary",
  "behaviors": [{ "type": "callback", "value": { "action": "ok" } }]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `button` |
| `text` | 否 | Object | / | `{tag:"plain_text", content}`,≤100 字符 |
| `type` | 否 | String | default | 见下方 type 枚举 |
| `size` | 否 | String | medium | `tiny` / `small` / `medium` / `large` |
| `width` | 否 | String | default | `default` / `fill` / `[100,∞)px` |
| `behaviors` | 是* | Array | / | 交互行为,见下;表单内按钮不用 behaviors 而用 `form_action_type` |
| `icon` | 否 | Object | / | 前缀图标(同 `div.icon`) |
| `hover_tips` | 否 | Object | / | PC 端悬浮提示,plain_text |
| `disabled` | 否 | Boolean | false | 是否禁用 |
| `disabled_tips` | 否 | Object | / | 禁用后悬浮提示,plain_text |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}`(均 plain_text,title 必填) |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

**type 枚举**:`default`(黑字描边) / `primary`(蓝字描边) / `danger`(红字描边) / `text` / `primary_text` / `danger_text`(无边框) / `primary_filled`(蓝底白字) / `danger_filled`(红底白字) / `laser`(镭射)。

## 按钮主次(强制)

- 全卡仅 1 个按钮 → `type: "primary_filled"`,并 `width: "fill"` 撑满成强焦点。
- 多个并列按钮 → 第一个(主操作)`primary_filled`,其余一律 `default`,形成「一主多次」层级。
- 删除 / 拒绝等危险操作用 `danger` 系(`danger` 或 `danger_filled`)。

## behaviors(交互行为)

```json
// 1. 服务端回调
{ "type": "callback", "value": { "key": "v" } }
// 2. 跳转链接(可与 callback 同数组共存)
{ "type": "open_url", "default_url": "https://x", "pc_url": "", "ios_url": "", "android_url": "" }
```

表单容器内的按钮 **不用 behaviors**,改用根字段:

| 字段 | 必填 | 说明 |
|---|---|---|
| `name` | 是 | 表单内唯一标识 |
| `form_action_type` | 是 | `submit`(提交表单)/ `reset`(重置) |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / interactive_container 内。
- 2.0 已废弃 `action` 交互模块,按钮直接放 `elements`,用间距控制排列。
- 旧式 `url`/`value` 顶层字段是 1.0 写法;2.0 一律用 `behaviors`。
- 点击触发 `card.action.trigger`,回传 `action.tag="button"` + `action.value`(即 callback 的 value)。
references/card/components/chart.md
# 图表 `chart`

基于 VChart 的可视化图表(折线/柱/饼/词云等)。**Card 2.0**。

## 最小示例

```json
{
  "tag": "chart",
  "chart_spec": {
    "type": "line",
    "title": { "text": "趋势" },
    "data": { "values": [
      { "time": "周一", "value": 8 },
      { "time": "周二", "value": 14 }
    ] },
    "xField": "time",
    "yField": "value"
  }
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `chart` |
| `chart_spec` | 是 | Object | / | VChart 图表定义,见下 |
| `aspect_ratio` | 否 | String | 16:9(PC)/1:1(移动) | `1:1` / `2:1` / `4:3` / `16:9` |
| `color_theme` | 否 | String | brand | `brand` / `rainbow` / `complementary` / `converse` / `primary`;chart_spec 里声明了样式则此项无效 |
| `height` | 否 | String | auto | `auto`(按宽高比) 或 `[1,999]px`(设固定高则 aspect_ratio 失效) |
| `preview` | 否 | Boolean | true | 是否可独立窗口/全屏查看 |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

## chart_spec 常用类型

`chart_spec` 是标准 VChart spec。核心字段:`type`、`data.values`(数据数组)、`xField`/`yField`(轴字段)、`seriesField`(分组)、`title.text`、`legends`。

| 图表 | type | 关键字段 |
|---|---|---|
| 折线 | `line` | `xField`, `yField` |
| 面积 | `area` | `xField`, `yField` |
| 柱状 | `bar` | `xField`, `yField`,分组加 `seriesField` |
| 条形(横向) | `bar` | `direction:"horizontal"`,`xField`=值,`yField`=类别 |
| 饼/环 | `pie` | `valueField`, `categoryField`,环图加 `innerRadius` |
| 散点 | `scatter` | `xField`, `yField` |
| 词云 | `wordCloud` | `nameField`, `valueField` |

完整属性参考 [VChart 官方文档](https://www.visactor.io/vchart/option/barChart)。

## 易错点

- 不支持 JavaScript 语法,`chart_spec` 必须是纯 JSON。
- 单卡建议 ≤5 个图表。
- 移动端不支持部分 VChart 属性(纹理 texture、conical 渐变、grid 词云布局等),用了会在移动端加载失败。
- 平台默认给 chart_spec 追加 media query 自适应;要自控可设 `"media": []`。
references/card/components/checker.md
# 勾选器 `checker`

任务勾选场景的交互组件,支持配置回调响应。仅支持手写 JSON,搭建工具不支持构建。**Card 2.0**。

## 最小示例

```json
{
  "tag": "checker",
  "name": "check_1",
  "checked": false,
  "text": { "tag": "plain_text", "content": "完成新品上市计划报告" },
  "behaviors": [{ "type": "callback", "value": { "key": "todo1" } }]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `checker` |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `checked` | 否 | Boolean | false | 初始勾选状态 |
| `text` | 否 | Object | / | `{tag:"plain_text"\|"lark_md", content, text_size?, text_color?, text_align?}`(text_color 见 `../resource/colors.md`) |
| `overall_checkable` | 否 | Boolean | true | 悬浮时整体是否有阴影效果 |
| `button_area` | 否 | Object | / | `{pc_display_rule:"always"|"on_hover", buttons:[<=3 个 button]}` |
| `checked_style` | 否 | Object | / | `{show_strikethrough, opacity}`,勾选后的内容样式 |
| `disabled` / `disabled_tips` | 否 | Boolean/Object | false / 空 | 禁用及禁用提示 |
| `hover_tips` | 否 | Object | 空 | 悬浮提示;与 `disabled_tips` 同配时后者生效 |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]`;**未配置时仅本地勾选生效,不触发回调** |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `padding`/`margin` | 否 | String | 0 | [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 form / 交互容器 / column_set / collapsible_panel 内。
- 不配置 `behaviors` 时勾选仅前端本地生效,不会触发服务端回调——需要业务侧感知必须显式配置。
- 回调:`action.tag="checker"` + `action.checked`(布尔值);form 内则读 `form_value[name]`。
references/card/components/collapsible_panel.md
# 折叠面板 `collapsible_panel`

折叠次要内容(备注、长文本),点标题展开/收起。**Card 2.0**。

## 最小示例

```json
{
  "tag": "collapsible_panel",
  "expanded": false,
  "header": { "title": { "tag": "plain_text", "content": "面板标题" } },
  "elements": [{ "tag": "markdown", "content": "折叠的内容" }]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `collapsible_panel` |
| `header` | 是 | Object | / | 标题区,见下 |
| `elements` | 否 | Array | / | 面板内组件;**不能放 `form`** |
| `expanded` | 否 | Boolean | false | 是否默认展开 |
| `background_color` | 否 | String | 透明 | 面板背景,颜色枚举(见 `../resource/colors.md`) |
| `border` | 否 | Object | / | `{ color, corner_radius }` |
| `direction` | 否 | String | vertical | `vertical` / `horizontal` |
| `vertical_spacing`/`horizontal_spacing` | 否 | String | 8px | 间距枚举或 [0,99]px |
| `padding` | 否 | String | 0 | 内边距 [0,99]px |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |

**header 字段**:

| 字段 | 必填 | 说明 |
|---|---|---|
| `title` | 否 | `{tag:"plain_text"\|"markdown", content}` |
| `background_color` | 否 | 标题区背景,颜色枚举 |
| `width` | 否 | `fill` / `auto` / `auto_when_fold`(收起时自适应) |
| `vertical_align` | 否 | `top`/`center`/`bottom` |
| `icon` | 否 | 图标 `{tag, token, color, size}`(同 `div.icon`,多 `size`) |
| `icon_position` | 否 | `left` / `right` / `follow_text` |
| `icon_expanded_angle` | 否 | 展开时图标旋转角:`-180`/`-90`/`90`/`180` |

## 嵌套 / 易错点

- 内部不支持 `form`;容器最多嵌套 5 层。
- 仅支持写 JSON,搭建工具不支持。
references/card/components/column_set.md
# 分栏 `column_set` + `column`

横向多列布局容器。`column_set` 装若干 `column`,每个 `column` 内再放组件。**Card 2.0**。

## 最小示例

```json
{
  "tag": "column_set",
  "flex_mode": "none",
  "columns": [
    { "tag": "column", "width": "weighted", "weight": 1,
      "elements": [{ "tag": "markdown", "content": "左列" }] },
    { "tag": "column", "width": "weighted", "weight": 1,
      "elements": [{ "tag": "markdown", "content": "右列" }] }
  ]
}
```

## column_set 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `column_set` |
| `columns` | 是 | column[] | / | 列数组,子节点只能是 `column` |
| `flex_mode` | 否 | String | none | 窄屏自适应:`none`(按比例压缩) / `stretch`(变上下堆叠) / `flow`(自动换行) / `bisect`(两等分) / `trisect`(三等分) |
| `horizontal_spacing` | 否 | String | 8px | `small`(4)/`medium`(8)/`large`(12)/`extra_large`(16) 或 `[0,99]px` |
| `horizontal_align` | 否 | String | left | `left` / `center` / `right` |
| `background_style` | 否 | String | default | `default` 或颜色枚举/RGBA(见 `../resource/colors.md`);嵌套时上层覆盖下层 |
| `action` | 否 | Object | / | 整块点击跳转 `{ multi_url:{url,pc_url,ios_url,android_url} }` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |

## column 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `column` |
| `elements` | 否 | Element[] | / | 列内组件;**不能放 `form` 和 `table`**,可放 `column_set` |
| `width` | 否 | String | auto | 仅 `flex_mode:none` 生效:`auto` / `weighted`(配 weight) / `[16,600]px` |
| `weight` | 否 | Number | 1 | `width:weighted` 时的宽度占比,1~5 整数 |
| `vertical_align` | 否 | String | top | `top` / `center` / `bottom` |
| `direction` | 否 | String | vertical | `vertical` / `horizontal` |
| `horizontal_spacing`/`vertical_spacing` | 否 | String | 8px | 同上间距枚举或 `[0,99]px` |
| `padding` | 否 | String | 0 | 内边距 [0,99]px |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `background_style` | 否 | String | default | 同上 |
| `action` | 否 | Object | / | 点击列跳转,同 column_set.action |

## 嵌套 / 易错点

- **column_set 的直接子节点只能是 `column`**;不能 `column_set → column_set`。二级分栏要走 `column_set → column → column_set`。
- column 内可放除 `form` / `table` 外的所有组件。
- 最多嵌套 5 层,过深会压缩展示空间。
references/card/components/date_picker.md
# 日期选择器 `date_picker`

提供日期选项的交互组件,默认拥有交互能力(无需显式 `behaviors` 也会回调)。**Card 2.0**。

## 最小示例

```json
{
  "tag": "date_picker",
  "placeholder": { "tag": "plain_text", "content": "请选择" },
  "initial_date": "2024-01-01"
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `date_picker` |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `required` | 否 | Boolean | false | 是否必选(form 内生效) |
| `initial_date` | 否 | String | / | 初始值,格式 `yyyy-MM-dd`,会覆盖 `placeholder` |
| `placeholder` | 否 | Object | / | 占位文本,plain_text;未设 `initial_date` 时必填 |
| `width` | 否 | String | default | `default`/`fill`/`[100,∞)px` |
| `disabled` | 否 | Boolean | false | 是否禁用(需端版本 V7.4+) |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `margin` | 否 | String | 0 | [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / 交互容器内;搭建工具中暂不支持嵌套在交互容器中。
- 提醒用户注意时区语境(如预定海外酒店用酒店所在地时区);服务端只返回用户当前时区作为参考,不代表用户选的就是该时区。
- 回调:`action.tag="date_picker"` + `action.option`(日期字符串,如 `"2025-06-10 +0800"`)+ `action.timezone`;form 内则读 `form_value[name]`。
references/card/components/div.md
# 普通文本 `div`

带样式的文本块,支持前缀图标和 label-value 字段对。**Card 2.0**。富文本用 `markdown` 组件。

## 最小示例

```json
{
  "tag": "div",
  "text": { "tag": "plain_text", "content": "示例文本" }
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `div` |
| `text` | 否 | Object | / | 文本对象,见下 |
| `text.tag` | 是 | String | plain_text | `plain_text` 或 `lark_md`(部分 Markdown,语法见 `markdown.md`) |
| `text.content` | 是 | String | / | 文本内容 |
| `text.text_size` | 否 | String | normal | `heading-0`~`heading-4` / `normal`(14px) / `notation`(12px) 等;可在 `config.style.text_size` 自定义 pc/mobile 不同字号 |
| `text.text_color` | 否 | String | default | 颜色枚举(见 `../resource/colors.md`),仅 `plain_text` 生效 |
| `text.text_align` | 否 | String | left | `left` / `center` / `right` |
| `text.lines` | 否 | Int | / | 最大显示行数,超出 `...` 省略 |
| `icon` | 否 | Object | / | 前缀图标,见下 |
| `icon.tag` | 否 | String | / | `standard_icon`(用 `token`+`color`,token 见 `../resource/icons.md`)或 `custom_icon`(用 `img_key`) |
| `width` | 否 | String | fill | `fill` / `auto` / `[16,999]px` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符;流式更新时 `text.element_id` 指定文本 |

> `fields` 字段(多列 label-value):数组,每项 `{ is_short, text:{tag,content} }`,`is_short:true` 可并排。

## 易错点

- `text_color` 只在 `text.tag` 为 `plain_text` 时生效;`lark_md` 用内联 `<font color=red>` 着色。
references/card/components/form.md
# 表单容器 `form`

批量录入表单项后一次提交:用户在前端填写多个表单项,点击提交按钮后将所有值打包一次性回调到服务端。**Card 2.0**。

## 最小示例

```json
{
  "tag": "form",
  "name": "form_1",
  "elements": [
    { "tag": "input", "name": "reason", "required": true },
    {
      "tag": "button",
      "text": { "tag": "plain_text", "content": "提交" },
      "type": "primary",
      "form_action_type": "submit",
      "name": "Button_submit"
    }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `form` |
| `name` | 是 | String | / | 表单容器唯一标识,卡片内全局唯一,用于识别提交数据归属 |
| `elements` | 是 | Element[] | [] | 子节点,支持除 `table` 和 `form` 外的所有组件 |
| `direction` | 否 | String | vertical | `vertical` / `horizontal` |
| `horizontal_spacing`/`vertical_spacing` | 否 | String | 8px/12px | 间距枚举 `small`(4)/`medium`(8)/`large`(12)/`extra_large`(16) 或 `[0,99]px` |
| `horizontal_align` | 否 | String | left | `left`/`center`/`right` |
| `vertical_align` | 否 | String | top | `top`/`center`/`bottom` |
| `padding`/`margin` | 否 | String | 0 | [-99,99]px,支持单值/双值/四值写法 |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

### 子组件内嵌字段(交互组件嵌在 form 内时生效)

| 字段 | 必填 | 说明 |
|---|---|---|
| `name` | 是 | 表单内组件唯一标识,卡片全局唯一,否则提交失败 |
| `required` | 否 | 是否必填;为 true 且未填时点提交会本地拦截,不发起回调 |
| `form_action_type` | 是(按钮) | `submit`(提交)/ `reset`(重置初始值);表单内按钮**不用** `behaviors` |

## 嵌套 / 易错点

- `form` 不支持嵌套 `table` 和 `form`;且 `form` 本身只能放卡片根节点下,不能被其他组件嵌套。
- form 内所有交互组件的 `name` 必须填且全局唯一,否则提交失败。
- 表单内必须包含一个 `form_action_type: submit` 的按钮。
- 回调来源:`card.action.trigger` 中 `action.tag="button"` + `action.form_value`(按组件 `name` 映射各字段值)。
references/card/components/header.md
# 标题 `header`

卡片顶部标题区(主/副标题、后缀标签、图标、主题色)。**Card 2.0**。挂在卡片根的 `header` 键下,不在 `body.elements` 内,单卡仅一个。

## 最小示例

```json
{
  "header": {
    "title": { "tag": "plain_text", "content": "卡片标题" },
    "template": "blue"
  }
}
```

## 字段

| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| `title` | 是 | Object | 主标题,`{tag:"plain_text"\|"lark_md", content}`,最多 4 行 |
| `subtitle` | 否 | Object | 副标题,同 title,最多 1 行;只配副标题会按主标题展示 |
| `template` | 否 | String | 主题色枚举,见下;默认 `default` |
| `text_tag_list` | 否 | Array | 后缀标签,最多 3 个,每项 `{tag:"text_tag", text:{tag:"plain_text",content}, color}` |
| `i18n_text_tag_list` | 否 | Object | 多语言后缀标签;与 `text_tag_list` 二选一,同配以多语言为准 |
| `icon` | 否 | Object | 前缀图标(同 `div.icon`) |
| `padding` | 否 | String | 内边距,默认 12px,[0,99]px |

**template 枚举**(13 色):`blue` / `wathet` / `turquoise` / `green` / `yellow` / `orange` / `red` / `carmine` / `violet` / `purple` / `indigo` / `grey` / `default`。

**标签 color 枚举**:`neutral`/`blue`/`turquoise`/`lime`/`orange`/`violet`/`indigo`/`wathet`/`green`/`yellow`/`red`/`purple`/`carmine`。深浅档位及 RGBA 见 `../resource/colors.md`。

## 选色建议

按场景选 template 颜色见 `../lark-im-card-style.md` 意图表。常见语义:green=成功/完成,orange=警告,red=错误/危险,grey=失效/归档,blue=通用信息。
references/card/components/hr.md
# 分割线 `hr`

分隔卡片内容的水平线。**Card 2.0**(1.0 同名 `hr`)。

## 最小示例

```json
{ "tag": "hr" }
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `hr` |
| `margin` | 否 | String | 0 | 外边距,范围 [-99,99]px,如 `"8px 0"` |
| `element_id` | 否 | String | / | 组件唯一标识,字母开头、≤20 字符 |
references/card/components/img_combination.md
# 多图混排 `img_combination`

多张图片按预设版式拼排。**Card 2.0**。

## 最小示例

```json
{
  "tag": "img_combination",
  "combination_mode": "double",
  "img_list": [{ "img_key": "img_v3_a" }, { "img_key": "img_v3_b" }]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `img_combination` |
| `combination_mode` | 是 | String | / | `double`(≤2) / `triple`(≤3) / `bisect`(双列,≤6) / `trisect`(三列,≤9) |
| `img_list` | 是 | Array | / | 每项 `{ img_key }`,顺序即排列顺序 |
| `combination_transparent` | 否 | Boolean | false | 是否透明底 |
| `corner_radius` | 否 | String | / | 圆角,`[0,∞]px` 或 `[0,100]%` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

## 易错点

- 图片数超过 mode 上限:只显示靠前的,其余丢弃;不足则留空白。
- 上传规范:≤10M、≤1500×3000px、高:宽 ≤16:9。
references/card/components/img.md
# 图片 `img`

展示图片。需先调上传图片接口拿 `img_key`。**Card 2.0**。

## 最小示例

```json
{
  "tag": "img",
  "img_key": "img_v3_xxx",
  "alt": { "tag": "plain_text", "content": "" }
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `img` |
| `img_key` | 是 | String | / | 图片 key,上传图片接口获取 |
| `alt` | 是 | Object | / | hover 说明,`{tag:"plain_text", content:""}`,不需要传空 |
| `title` | 否 | Object | / | 图片标题,plain_text 对象 |
| `scale_type` | 否 | String | crop_center | `crop_center` / `crop_top` / `fit_horizontal`(不裁剪) |
| `size` | 否 | String | / | 仅 `crop_*` 生效:`stretch`/`large`(160)/`medium`(80)/`small`(40)/`tiny`(16),或 `"100px 100px"` |
| `corner_radius` | 否 | String | / | 圆角,`[0,∞]px` 或 `[0,100]%` |
| `transparent` | 否 | Boolean | false | 是否透明底 |
| `preview` | 否 | Boolean | true | 点击是否放大;配 `card_link` 跳转时设 false |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

## 易错点

- 通栏效果:2.0 不再支持 `size: stretch_without_padding`,改用负 `margin`(如 `"4px -12px"`)。
- 上传规范:≤10M、尺寸 ≤1500×3000px、高:宽 ≤16:9。
references/card/components/input.md
# 输入框 `input`

收集用户文本输入。常嵌在 `form` 内配合提交按钮使用。**Card 2.0**。

## 最小示例

```json
{
  "tag": "input",
  "name": "comment",
  "placeholder": { "tag": "plain_text", "content": "请输入" },
  "label": { "tag": "plain_text", "content": "备注:" }
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `input` |
| `name` | 否* | String | / | 唯一标识;**在 form 内必填且全局唯一**,用于识别提交数据 |
| `required` | 否 | Boolean | false | 是否必填(仅 form 内生效) |
| `placeholder` | 否 | Object | / | 占位文本,plain_text,≤100 字符 |
| `default_value` | 否 | String | / | 预填内容 |
| `label` | 否 | Object | / | 描述文本,plain_text |
| `label_position` | 否 | String | top | `top` / `left`(窄屏自动转 top) |
| `input_type` | 否 | String | text | `text` / `multiline_text`(多行,回调含 `\n`) / `password` |
| `rows` | 否 | Number | 5 | 多行时默认行数 |
| `auto_resize` | 否 | Boolean | false | 多行时高度自适应(仅 PC) |
| `max_rows` | 否 | Number | / | `auto_resize` 时最大行数 |
| `max_length` | 否 | Number | 1000 | 最大字符数,[1,1000] |
| `show_icon` | 否 | Boolean | true | password 时是否显示前缀图标 |
| `width` | 否 | String | default | `default` / `fill` / `[100,∞)px` |
| `disabled` | 否 | Boolean | false | 是否禁用(配 `disabled_tips` plain_text) |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / interactive_container 内。
- 在 form 内为**异步提交**:用户填完点提交按钮才一次性回调全部表单数据。
- 回调里 `action.tag="input"` + `action.input_value`(用户输入值);form 提交则值在 `form_value` 内。
references/card/components/interactive_container.md
# 交互容器 `interactive_container`

整块可点击区域,统一定义内嵌内容的样式和交互(callback/open_url),适合卡片内的列表项、可点击卡片块。**Card 2.0**。

## 最小示例

```json
{
  "tag": "interactive_container",
  "width": "fill",
  "has_border": true,
  "border_color": "grey",
  "corner_radius": "8px",
  "padding": "4px 12px 4px 12px",
  "behaviors": [{ "type": "callback", "value": { "key": "value" } }],
  "elements": [
    { "tag": "markdown", "content": "帮我生成一篇产品方案的框架" }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `interactive_container` |
| `elements` | 是 | Element[] | [] | 子节点,支持除 `form`/`table` 外的所有组件 |
| `behaviors` | 是 | Array | / | 点击整容器的交互:`callback`(回传)/ `open_url`(跳转),可同数组共存 |
| `width` | 否 | String | fill | `fill`/`auto`/`[16,999]px` |
| `height` | 否 | String | auto | `auto`/`[10,999]px` |
| `direction` | 否 | String | vertical | `vertical`/`horizontal` |
| `horizontal_align`/`vertical_align` | 否 | String | left/top | 对齐方式 |
| `background_style` | 否 | String | default | `default`/`laser`/颜色枚举/RGBA(见 `../resource/colors.md`) |
| `has_border` | 否 | Boolean | false | 是否展示 1px 边框 |
| `border_color` | 否 | String | grey | `has_border` 为 true 时生效 |
| `corner_radius` | 否 | String | 0px | `[0,∞]px` 或 `[0,100]%` |
| `padding`/`margin` | 否 | String | 4px,12px / 0px | 同间距写法 |
| `disabled` / `disabled_tips` | 否 | Boolean/Object | false / 空 | 禁用整容器及禁用提示 |
| `hover_tips` | 否 | Object | 空 | PC 端悬浮提示 |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |

## 嵌套 / 易错点

- 可嵌套除 `form`/`table` 外的所有组件,包括嵌套自身(列表项常见写法)。
- 若容器内有交互组件(如内部 `button`),优先响应该子组件的交互,容器级 `behaviors` 不会触发。
- 回调来源:`card.action.trigger`,`action.tag` 取决于内部触发的具体组件;容器本身被点击时 `action.value` 即容器 `behaviors.value`。
references/card/components/markdown.md
# 富文本 `markdown`

支持 Markdown + 部分 HTML 的富文本。最常用的内容组件。**Card 2.0**。

## 最小示例

```json
{
  "tag": "markdown",
  "content": "**标题**\n正文,<font color='red'>红字</font>,[链接](https://x)"
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `markdown` |
| `content` | 是 | String | / | Markdown 文本;JSON 里用 `\n` 换行 |
| `text_size` | 否 | String | normal | `heading-0`~`heading-4` / `normal`(14px) / `notation`(12px) 等;可在 `config.style.text_size` 自定义 pc/mobile 字号 |
| `text_align` | 否 | String | left | `left` / `center` / `right` |
| `icon` | 否 | Object | / | 前缀图标(同 `div.icon`) |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

## 常用语法

| 效果 | 语法 |
|---|---|
| 粗 / 斜 / 删除线 | `**粗**`、`*斜*`、`~~删~~`(前后留空格更稳) |
| 换行 | JSON 内 `\n`;或 `<br>` |
| 文字链接 | `[文字](https://x)`(必须带 http/https) |
| 带图标链接 | `<link icon='chat_outlined' …>文案</link>`(icon token 见 `../resource/icons.md`) |
| 彩色文本 | `<font color='red'>红字</font>`(color 枚举见 `../resource/colors.md`;链接文本不可着色) |
| 标签 | `<text_tag color='blue'>标签</text_tag>`(color:neutral/blue/turquoise/lime/orange/violet/indigo/wathet/green/yellow/red/purple/carmine) |
| @ 人 | `<at id=ou_xxx></at>`、`<at id=all></at>`、`<at ids=id1,id2></at>` |
| @所有人 | `<at id=all></at>`(需群主开权限,否则发送失败) |
| 人员卡片 | `<person id='ou_xxx' show_name=true show_avatar=true style='normal'></person>` |
| 数字角标 | `<number_tag>1</number_tag>`(0-99,可加 background_color/font_color/url) |
| 国际化时间 | `<local_datetime millisecond='' format_type='date_num'></local_datetime>` |
| 标题 | `# 一级` ~ `###### 六级`(大标题显丑,正文优先用加粗,见易错点) |
| 列表 | `- 项`(无序)/ `1. 项`(有序),4 空格一层缩进 |
| 引用 | `> 引用文字` |
| 行内/块代码 | `` `code` `` / ```` ```go ... ``` ````(可指定语言) |
| 分割线 | `<hr>` 或 `---`(需单独一行) |
| 图片 | `![hover文案](img_key)` |
| 表格 | 标准 MD 表格;除标题最多 5 行(超出分页),单组件 ≤4 表 |
| 飞书表情 | `:DONE:`、`:OK:` |

## 易错点

- **慎用大标题**:`#` / `##` / `###` 一~三级标题字号过大、显丑,正文里一律用 `**加粗**` 替代来突出重点。**唯一例外**是「指标卡」里用 `##` 放大数值(见 `../lark-im-card-style.md` 视觉规范)。
- **少用 `markdown` 的 `margin`**:间距优先交给父容器的 `vertical_spacing` / `padding`,多数情况置 `0px`;仅精细缩进时设非零值(见 `../lark-im-card-style.md` 间距纪律)。
- 2.0 不再支持旧的 `[xx]($urlVal)` + `href` 差异化跳转语法,改用 `<link>`。
- 要展示 Markdown 特殊字符(`* ~ > < [ ] ( ) # : _` 等)须 HTML 转义,如 `<`→`&#60;`、`*`→`&#42;`。
- `content` 里的引号注意与 JSON 转义;属性值用单引号可减少冲突。
references/card/components/multi_select_person.md
# 人员选择-多选 `multi_select_person`

从候选人员中多选。**Card 2.0**。字段与 `select_person` 基本一致,差别在多选默认值。

## 最小示例

```json
{
  "tag": "multi_select_person",
  "name": "reviewers",
  "placeholder": { "tag": "plain_text", "content": "请选择" },
  "options": [
    { "value": "ou_xxx" },
    { "value": "ou_yyy" }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `multi_select_person` |
| `options` | 否 | Array | / | 候选人 `{value: open_id}`;为空或全无效时候选项为会话全体成员 |
| `selected_values` | 否 | String[] | / | 默认选中的 open_id 数组 |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `required` | 否 | Boolean | false | 是否必选(form 内生效) |
| `type` | 否 | String | default | `default`(带框) / `text`(纯文本) |
| `placeholder` | 否 | Object | / | 占位文本,plain_text |
| `width` | 否 | String | default | `default` / `fill` / `[100,∞)px` |
| `disabled` | 否 | Boolean | false | 是否禁用 |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / interactive_container 内。
- `options[].value` 只接受 **open_id**;默认选中用 `selected_values`(数组)。
- 回调返回选中的多个 open_id。
references/card/components/multi_select_static.md
# 下拉多选 `multi_select_static`

下拉菜单多选。**Card 2.0**。字段与 `select_static` 基本一致,差别在多选默认值。

## 最小示例

```json
{
  "tag": "multi_select_static",
  "name": "tags",
  "placeholder": { "tag": "plain_text", "content": "请选择" },
  "options": [
    { "text": { "tag": "plain_text", "content": "选项1" }, "value": "1" },
    { "text": { "tag": "plain_text", "content": "选项2" }, "value": "2" }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `multi_select_static` |
| `options` | 否 | Array | / | 选项 `{text:{plain_text}, value, icon?}`,`value` 不可重复 |
| `selected_values` | 否 | String[] | / | 默认选中的 value 数组 |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `required` | 否 | Boolean | false | 是否必选(form 内生效) |
| `type` | 否 | String | default | `default`(带框) / `text`(纯文本) |
| `placeholder` | 否 | Object | / | 占位文本,plain_text |
| `width` | 否 | String | default | `default`(带框固定282px) / `fill` / `[100,∞)px` |
| `disabled` | 否 | Boolean | false | 是否禁用 |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / interactive_container 内。
- 选项 `value` 唯一;默认选中用 `selected_values`(数组)而非单选的 `initial_*`。
- 回调返回选中的多个值。
references/card/components/overflow.md
# 折叠按钮组 `overflow`

折叠多个选项按钮,点击展开。适用于操作较多的场景。**Card 2.0**。

## 最小示例

```json
{
  "tag": "overflow",
  "options": [
    { "text": { "tag": "plain_text", "content": "选项A" }, "value": "a" },
    { "text": { "tag": "plain_text", "content": "选项B" }, "value": "b" }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `overflow` |
| `options` | 是 | Array | / | 选项按钮,见下 |
| `options[].text` | 否 | Object | / | `{tag:"plain_text", content}`,≤100 字符 |
| `options[].value` | 否 | String | / | 点击回传值,用于区分点了哪个选项(回调 `action.option`) |
| `options[].multi_url` | 否 | Object | / | 跳转链接 `{url, pc_url, ios_url, android_url}` |
| `behaviors` | 否 | Array | / | 额外回传:`[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}`(均 plain_text) |
| `width` | 否 | String | default | `default` / `fill` / `[100,∞)px` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

## 嵌套 / 易错点

- 可嵌套在 form / collapsible_panel / 循环容器 / interactive_container / column_set 内。
- 多按钮时务必给每个 `options[].value`,否则回调无法区分点了哪个。
- 点击触发 `card.action.trigger`,回传 `action.tag = "overflow"` + `action.option`。
references/card/components/person_list.md
# 人员列表 `person_list`

展示多个用户的头像/姓名。**Card 2.0**。

## 最小示例

```json
{
  "tag": "person_list",
  "persons": [{ "id": "ou_xxx" }, { "id": "ou_yyy" }]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `person_list` |
| `persons` | 是 | Array | / | 每项 `{ id }`,id 支持 open_id / union_id / user_id |
| `show_name` | 否 | Boolean | true | 是否显示姓名;关掉且多人时为"葫芦串"叠头像样式 |
| `show_avatar` | 否 | Boolean | false | 是否显示头像 |
| `size` | 否 | String | medium | `extra_small` / `small` / `medium` / `large` |
| `lines` | 否 | Int | / | 最大行数,不可为 0 |
| `drop_invalid_user_id` | 否 | Boolean | false | true 忽略无效 ID;false 则有无效 ID 时报错 |
| `icon` / `ud_icon` | 否 | Object | / | 前缀图标(同 `div.icon`);两者同设以 `icon` 为准 |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

## 易错点

- 发卡应用需有访问用户 ID 的权限,否则无法展示人员信息。
references/card/components/person.md
# 人员 `person`

展示单个用户的头像/姓名,点击可看名片。**Card 2.0**。

## 最小示例

```json
{
  "tag": "person",
  "user_id": "ou_xxx",
  "show_name": true
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `person` |
| `user_id` | 是 | String | / | 人员 ID,支持 open_id / union_id / user_id |
| `size` | 否 | String | medium | `extra_small` / `small` / `medium` / `large` |
| `show_avatar` | 否 | Boolean | true | 是否显示头像 |
| `show_name` | 否 | Boolean | false | 是否显示姓名 |
| `style` | 否 | String | normal | `normal` / `capsule`(胶囊) |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |
| `element_id` | 否 | String | / | 唯一标识,字母开头 ≤20 字符 |

## 易错点

- 发卡应用需有访问用户 ID 的权限,否则人员信息无法展示。
references/card/components/picker_datetime.md
# 日期时间选择器 `picker_datetime`

提供日期+时间选项的交互组件,默认拥有交互能力。**Card 2.0**。

## 最小示例

```json
{
  "tag": "picker_datetime",
  "placeholder": { "tag": "plain_text", "content": "请选择" },
  "initial_datetime": "2024-01-01 08:00"
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `picker_datetime` |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `required` | 否 | Boolean | false | 是否必选(form 内生效) |
| `initial_datetime` | 否 | String | / | 初始值,格式 `yyyy-MM-dd HH:mm`,会覆盖 `placeholder` |
| `placeholder` | 否 | Object | / | 占位文本,plain_text;未设 `initial_datetime` 时必填 |
| `width` | 否 | String | default | `default`/`fill`/`[100,∞)px` |
| `disabled` | 否 | Boolean | false | 是否禁用(需端版本 V7.4+) |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `margin` | 否 | String | 0 | [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / 交互容器内;搭建工具中暂不支持嵌套在交互容器中。
- 提醒用户注意时区语境;服务端只返回用户当前时区作为参考,不代表用户选的就是该时区。
- 回调:`action.tag="picker_datetime"` + `action.option`(如 `"2025-06-10 19:19 +0800"`)+ `action.timezone`;form 内则读 `form_value[name]`。
references/card/components/picker_time.md
# 时间选择器 `picker_time`

提供时间选项的交互组件,默认拥有交互能力。**Card 2.0**。

## 最小示例

```json
{
  "tag": "picker_time",
  "placeholder": { "tag": "plain_text", "content": "请选择" },
  "initial_time": "09:00"
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `picker_time` |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `required` | 否 | Boolean | false | 是否必选(form 内生效) |
| `initial_time` | 否 | String | / | 初始值,格式 `HH:mm`,会覆盖 `placeholder` |
| `placeholder` | 否 | Object | / | 占位文本,plain_text;未设 `initial_time` 时必填 |
| `width` | 否 | String | default | `default`/`fill`/`[100,∞)px` |
| `disabled` | 否 | Boolean | false | 是否禁用(需端版本 V7.4+) |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `margin` | 否 | String | 0 | [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / 交互容器内;搭建工具中暂不支持嵌套在交互容器中。
- 提醒用户注意时区语境;服务端只返回用户当前时区作为参考,不代表用户选的就是该时区。
- 回调:`action.tag="picker_time"` + `action.option`(时间字符串,如 `"05:05 +0800"`)+ `action.timezone`;form 内则读 `form_value[name]`。
references/card/components/recycling_container.md
# 循环容器(搭建工具专属,无 JSON tag)

批量渲染同版式不同数据的列表(如商品列表、推荐列表)。**仅支持在飞书卡片搭建工具中可视化构建,不支持手写卡片 JSON 代码实现**——因此没有 `tag` 字段可直接编排。

## 使用方式

1. 在[卡片搭建工具](https://open.feishu.cn/cardkit)中添加循环容器组件,绑定一个对象数组变量。
2. 在容器内添加任意展示/交互/分栏组件,并将其字段绑定到对象数组的子变量。
3. 发布卡片模板后,发送时通过 `template_variable` 传入实际数据数组,数组每个元素对应一条循环项。

## 发送示例(模板 + 变量赋值)

```json
{
  "type": "template",
  "data": {
    "template_id": "AAqi6xJ8rabcd",
    "template_version_name": "1.0.0",
    "template_variable": {
      "looping": [
        { "title": "**和风陶韵**", "description": "...", "image": { "img_key": "img_v3_xxx" } },
        { "title": "**匠心之作**", "description": "...", "image": { "img_key": "img_v3_yyy" } }
      ]
    }
  }
}
```

将以上 JSON 压缩转义后作为 `messages.create` 的 `content`,`msg_type` 为 `interactive`。

## 嵌套 / 易错点

- 不支持再嵌套循环容器(对象数组变量不支持嵌套对象数组类型)。
- 数组元素个数即渲染条数,可直接控制列表长度。
- 若循环容器内嵌表单容器的交互组件(如 input),交互组件的 `name`(表单项标识)必须绑定到不重复的子变量,否则预览/发送报错。
references/card/components/select_img.md
# 多图选择 `select_img`

以图片为选项的交互组件,支持单选/多选(如商品图、模板图、AI 生成图)。仅支持手写 JSON,搭建工具不支持。**Card 2.0**。

## 最小示例

```json
{
  "tag": "select_img",
  "name": "select_img_1",
  "layout": "bisect",
  "aspect_ratio": "16:9",
  "options": [
    { "img_key": "img_v2_xxx", "value": "picture1" },
    { "img_key": "img_v2_yyy", "value": "picture2" }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `select_img` |
| `options` | 是 | Array | / | 选项,每项 `{img_key, value, disabled?, disabled_tips?, hover_tips?}` |
| `multi_select` | 否 | Boolean | false | 多选仅支持异步提交,**必须**内嵌在 form 中,否则报错 |
| `layout` | 否 | String | bisect | 图片布局:`stretch`(撑满)/`bisect`(二等分)/`trisect`(三等分) |
| `aspect_ratio` | 否 | String | 16:9 | `1:1`/`16:9`/`4:3` |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `required` | 否 | Boolean | false | 是否必选(form 内生效) |
| `can_preview` | 否 | Boolean | true | 点击图片是否弹窗放大(仅 form 内生效) |
| `disabled` | 否 | Boolean | false | 是否禁用整组件 |
| `value` | 否 | String/Object | / | 自定义回传参数 |
| `behaviors` | 是 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |

## 嵌套 / 易错点

- 可嵌套在根节点 / column_set / form / 交互容器(搭建工具暂不支持嵌套交互容器)。
- **不在 form 内**:仅支持单选,点击立即提交触发回调,不支持多选/异步提交。
- **在 form 内**:支持单选/多选 + 异步提交(随表单一起提交)。
- 回调(非 form):`action.tag="select_img"` + `action.options`(单选时仍是该字段);form 内则读 `form_value[name]`。
references/card/components/select_person.md
# 人员选择-单选 `select_person`

从候选人员中单选一人。**Card 2.0**。

## 最小示例

```json
{
  "tag": "select_person",
  "placeholder": { "tag": "plain_text", "content": "请选择" },
  "options": [
    { "value": "ou_xxx" },
    { "value": "ou_yyy" }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `select_person` |
| `options` | 否 | Array | / | 候选人,每项 `{value: open_id}`;**为空或全无效时,候选项为会话内全体成员** |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `required` | 否 | Boolean | false | 是否必选(form 内生效) |
| `type` | 否 | String | default | `default`(带框) / `text`(纯文本) |
| `placeholder` | 否 | Object | / | 占位文本,plain_text |
| `initial_option` | 否 | String | / | 初始选中的 open_id,须在 options 内 |
| `width` | 否 | String | default | `default` / `fill` / `[100,∞)px` |
| `disabled` | 否 | Boolean | false | 是否禁用 |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / interactive_container 内。
- `options[].value` 只接受 **open_id**。
- 回调 `action.tag="select_person"` + `action.option`(选中人的 open_id)。
references/card/components/select_static.md
# 下拉单选 `select_static`

下拉菜单单选。**Card 2.0**。

## 最小示例

```json
{
  "tag": "select_static",
  "placeholder": { "tag": "plain_text", "content": "请选择" },
  "options": [
    { "text": { "tag": "plain_text", "content": "选项1" }, "value": "1" },
    { "text": { "tag": "plain_text", "content": "选项2" }, "value": "2" }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `select_static` |
| `options` | 否 | Array | / | 选项,见下 |
| `options[].text` | 是 | Object | / | 选项名,plain_text |
| `options[].value` | 是 | String | / | 选项回调值,**同组件内不可重复** |
| `options[].icon` | 否 | Object | / | 选项前缀图标(同 `div.icon`) |
| `name` | 否* | String | / | 唯一标识;**form 内必填且全局唯一** |
| `required` | 否 | Boolean | false | 是否必选(form 内生效) |
| `type` | 否 | String | default | `default`(带框) / `text`(纯文本) |
| `placeholder` | 否 | Object | / | 占位文本,plain_text |
| `initial_option` | 否 | String | / | 初始选中内容(覆盖 placeholder 和 initial_index) |
| `initial_index` | 否 | Int | / | 初始选中序号,0=不选,1=第一个 |
| `width` | 否 | String | default | `default` / `fill` / `[100,∞)px` |
| `disabled` | 否 | Boolean | false | 是否禁用 |
| `behaviors` | 否 | Array | / | `[{type:"callback", value:{...}}]` |
| `confirm` | 否 | Object | / | 二次确认弹窗 `{title, text}` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |

## 嵌套 / 易错点

- 可嵌套在 column_set / form / collapsible_panel / 循环容器 / interactive_container 内。
- 选项 `value` 必须唯一,否则交互异常、服务端无法区分选了哪个。
- 回调 `action.tag="select_static"` + `action.option`(选中项的 value)。
references/card/components/table.md
# 表格 `table`

多列数据表,支持文本/数字/选项/人员/日期等列类型。**Card 2.0**。

## 最小示例

```json
{
  "tag": "table",
  "columns": [
    { "name": "city", "display_name": "城市", "data_type": "text" },
    { "name": "qty", "display_name": "数量", "data_type": "number" }
  ],
  "rows": [
    { "city": "北京", "qty": 12 },
    { "city": "上海", "qty": 8 }
  ]
}
```

## 字段

| 字段 | 必填 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| `tag` | 是 | String | / | 固定 `table` |
| `columns` | 是 | column[] | / | 列定义,≤50 列,见下 |
| `rows` | 是 | Object[] | / | 行数据,按 `列name: 值` 填充 |
| `page_size` | 否 | Number | 5 | 每页行数,[1,10] |
| `row_height` | 否 | String | low | `low`/`middle`/`high`/`auto`/`[32,124]px` |
| `row_max_height` | 否 | String | 124px | `row_height:auto` 时最大行高 [32,999]px |
| `freeze_first_column` | 否 | Boolean | false | 冻结首列 |
| `header_style` | 否 | Object | / | 表头样式:`{text_align, text_size, background_style:grey\|none, text_color, bold, lines}` |
| `margin` | 否 | String | 0 | 外边距 [-99,99]px |

**column 字段**:`name`(必填,键名) / `display_name`(表头名) / `data_type`(见下) / `width`(`auto`/`[80,600]px`/`%`) / `horizontal_align` / `vertical_align`;`number` 列可加 `format:{precision, symbol, separator}`;`date` 列可加 `date_format`(如 `YYYY/MM/DD`)。

**data_type 与行值结构**:

| data_type | 行值 |
|---|---|
| `text` | `"飞书"` |
| `lark_md` | `"[链接](https://x)"` |
| `number` | `168.23` |
| `options` | `[{text:"S2", color:"blue"}]`(颜色枚举见 `../resource/colors.md`,文本勿过长) |
| `persons` | `"ou_xxx"` 或 `["ou_a","ou_b"]` |
| `date` | `1699341315000`(毫秒时间戳,按本地时区显示) |
| `markdown` | `"![img](img_key)"` 完整 Markdown |

## 嵌套 / 易错点

- **table 只能放卡片根 `body.elements`**:不能被任何容器嵌套,自身也不能嵌别的组件。
- 单卡最多 5 个 table(多语言每语言 5 个)。
- `rows` 的键必须与 `columns[].name` 对应。
references/card/lark-im-card-create.md
# 发送 Interactive 卡片工作流

用户需要发送一张飞书互动卡片时,遵循本工作流。每次都必须严格按步骤执行。

---

## 入口分支:文字 / 图片 / 图片+文字组合

判断用户输入类型:

- **纯文字诉求**(无图片)→ 跳到 Step 1「文字诉求路径」。
- **纯图片**(截图 / 设计稿,无额外文字说明内容)→ 走「以图片为输入」路径,图片既是内容源也是风格源。
- **图片 + 文字组合** → 走「以图片为输入」路径,但**图片仅当样式/布局参考,内容来源以文字为准**(见第 5 点)。

### 以图片为输入时的处理

1. **分析图片**:从图片中提取视觉风格信息——
   - 配色方案(色环定位)、间距节奏、层级关系、分组方式、组件类型
   - 图片类型(见第 2 点)

2. **判断图片类型**,决定保真策略:

   | 图片类型 | 判断依据 | 构造策略 |
   |---|---|---|
   | **飞书卡片截图** | 能识别出 header / body / components 等飞书卡片结构特征 | **高保真复刻**:将截图中每一块视觉结构映射到相近的卡片 2.0 组件;复刻后仍需过 P0–P7 硬 Gate |
   | **其它设计稿 / 海报 / 网页 UI** | 无飞书卡片结构特征 | **风格萃取 + 按原则重构**:提取配色、间距、层级关系等风格 token;布局按 P0–P7 原则重构(**不像素级仿制**),产出说明偏差 |

3. **确定内容来源**:
   - **纯图片**:从图片提取内容/信息点(文字、字段、操作)作为诉求(喂给 P0)
   - **图片 + 文字组合**:**以文字/文档为内容来源**,图片仅提供样式和布局参考。将文字内容按图片风格组织进卡片

4. **冲突处理**:当图片样式与 P0–P7 或卡片组件能力冲突时——
   - 飞书卡片截图:在组件能力允许范围内**尽量保真**,冲突处微调并告知用户偏差原因
   - 其它设计稿 / 海报 / UI:**以 P0–P7 为准**,图片仅当风格参考,冲突时不硬搬

5. 分析明确后,向用户简要说明你的**类型判断结论 + 保真策略 + 内容来源方案**。然后进入 Step 2 加载组件文档,进入构造。

---

## Step 1(文字诉求路径):分析意图,输出设计方案

**目标**:在动手写 JSON 之前,先明确所有决策并告知用户。Step 2 的文档加载量取决于这里的组件列表,所以要尽量在这一步想清楚。

分析以下内容并向用户简要说明:

1. **版本**:Card 2.0 支持的组件更丰富,**推荐使用 Card 2.0**;仅当用户明确要求 1.0 时才用 1.0。
2. **组件组合**:在 `lark-im-card-style.md` 「意图 → 组件组合」表里匹配最接近的意图行,参考推荐组件组合和该行的 `header.template` 颜色(部分行为"无 header")。推荐组合仅供参考,**最终选型以符合用户意图为准**;使用 Card 2.0 时,可同时参考 `card-2.0-schema.md` 中的组件概述来补充或调整组件选择。
3. **交互类型**(若有):是否含会回调服务端的交互组件,以及是否有纯跳转(open_url)。回调分两类:① `select` / `multi_select` / `input` / `picker` / `overflow` 操作即默认回调;② `button` / `checker` / `interactive_container` 需显式配置 `behaviors`;`form` 提交统一回调。细则见 Step 5。
4. **宽度模式**:`compact`(400px) 适合通知/祝福/轻提醒(内容精简、单焦点);`default`(≤600px) 适合大多数场景;`fill`(撑满) 适合数据看板、含 `table` 的宽表格。默认 `default`,有明确理由才偏离。

> 输出示例:"Card 2.0,header green,`default`,组件:`column_set` / `column` / `markdown`,无交互。"

---

## Step 2:按需加载组件文档

> ⚠️ **仅 Card 2.0 适用**:`card-2.0-schema.md`、`components/` 明细都是 2.0 结构。若 Step 1 定为 **Card 1.0**(含 Step 4 降级场景),这些**不可参考**,跳过本步,直接按 1.0 结构构造。

**目标**:读组件明细 + 「好看的标准」,不全量加载。

> 组件列表来源:**文字路径** = Step 1 的设计方案;**图片路径** = 入口分支图片分析阶段确定的组件列表。

1. 阅读 `card-2.0-schema.md` —— 同时满足两个目的:① 了解组件概述,辅助组件选型;② 找到各组件的明细文档路由链接。**仅读一次,不重复加载。**
2. 按路由逐个读取 `components/<tag>.md`(如 `components/column_set.md`、`components/button.md`)
3. 阅读 `lark-im-card-style.md` 开头的「**好看的标准(P0–P7)**」和「视觉规范」——这是 Step 3 构造和自检的裁判基准,**构造前先内化**。

---

## Step 3:构造卡片 JSON

按 Step 2 中对应版本的根结构骨架构造卡片,组件选型遵循 Step 1(或图片分析阶段)的设计方案。

- Card 2.0 必须有 `"schema": "2.0"`,否则卡片不渲染
- `form` 容器内按钮用 `form_action_type: "submit"`,不写 `behaviors`
- `column_set` 的子节点只能是 `column`,不能直接放其他组件
- `table` **只能放 body 根节点**,不能嵌套进 `column_set` / `interactive_container` 等容器
- `collapsible_panel` 内**不能包含 `form`**;`interactive_container` 内**不能包含 `form`/`table`**

### 发送前硬 Gate(按 P0–P7 自检,不过不许进 Step 4)

构造完成后,逐条用 `lark-im-card-style.md` 的「好看的标准」做**结构化自检**。**P0 + P1–P3 是阻断项,任一不过必须回到本步修正后重判**,不得带病发送。

**阻断项(必须全过):**
- [ ] **P0 符合诉求**:把用户诉求拆成信息点清单,逐点在 JSON 里找到承载组件;需要的操作(按钮/表单/跳转)都齐备;无与诉求无关的填充
- [ ] **P1 层级**:body 内有且仅有**一个**最强焦点;标题用 `**加粗**` 与正文拉开,次要信息用 grey
- [ ] **P2 分组**:同主题字段收进同一容器,不同主题分容器;**没有「一路 hr 平铺」或多主题挤在同一 markdown**
- [ ] **P3 复杂度适中**:视觉块 **2–5** 个、主色系 ≤3;且 >1 个块、至少含一个非纯文本结构元素(背景块/指标卡/图标/表格)——**既不能纯文字流水账,也不能堆砌过载**

**基础卫生(应满足):**
- [ ] **P4 对比**:标题与正文在字号或粗细上至少差一档;正文不滥用 `#/##/###`(指标卡数值放大除外)
- [ ] **P5 对齐**:不滥用散设 `margin`,间距优先交容器;间距取值种类 ≤4;非末尾顶级容器间距一致

**加分项(尽量满足):**
- [ ] **P6 语义一致**:同色同义(红=降/警、绿=升/成、grey=次要);主色系起始色与 header 一致、取邻近色环
- [ ] **P7 健壮**:并列/指标列默认 `weighted`/`none`、慎用 `stretch`;必要时配 `config.style.color` light/dark

---

## Step 4:发送卡片

```bash
# 发送到群聊
lark-cli im +messages-send --chat-id oc_xxx --msg-type interactive --content '<card_json>'

# 发送给指定用户(私聊)
lark-cli im +messages-send --user-id ou_xxx --msg-type interactive --content '<card_json>'
```

**发送失败时**:先对照下方常见失败列表排查,若能匹配则按对应处理方式修复后重新发送;否则根据错误信息修复 JSON 后重新发送。最多尝试 **3 次**。若 3 次后仍失败,**降级为 Card 1.0 卡片**重新构造并发送。**不参考之前发送 2.0 的记忆**,完全根据用户意图重新构造 1.0 卡片。1.0 无本地参考文档(components/、resource/ 均为 2.0)。
**常见失败列表**

| # | 错误信息 | 处理方式 |
|---|---|---|
| 1 | `there is an invalid user resource (at/person) in your card` | 卡片中含有 at/person 组件,但使用了无效的用户 ID。询问用户其真实的 open_id / user_id,替换后重新发送。 |

---

## Step 5:交互回调(可选)

若卡片含**会回调服务端的交互组件**,则**支持**监听 `card.action.trigger` 回调(是否监听由实际需求决定,非必须):

**需显式配置 `behaviors: [{type:"callback"}]` 才会回调:**
- `button`(带 callback behavior)
- `checker` —— 未配置 behaviors 时仅本地勾选生效,不触发服务端回调
- `interactive_container` —— behaviors 为必填,支持 callback / open_url

**选中 / 输入即默认回调,无需显式 `behaviors`:**
- `select_static` / `multi_select_static` / `select_person` / `multi_select_person`
- `overflow` / `input` / `date_picker` / `picker_time` / `picker_datetime`

**form 提交统一回调(按钮用 `form_action_type: "submit"`,无需 behaviors):**
- form 内所有表单组件的值通过 `action.form_value` 一次性回传

> 纯 `open_url` 跳转按钮在客户端本地跳转,不回调服务端。

如需处理回调(监听事件、读取字段、更新卡片),见 `../lark-im-card-action-reply.md`。

---

## Step 6:用户反馈修正(按需进入)

用户看到已发送卡片后提出修改意见时,遵循以下流程。**不要整卡重做,外科手术式修改。**

### 1. 定位改动范围

把用户意见逐条映射到具体组件和字段:

| 用户反馈类型 | 映射目标 |
|---|---|
| 文案/措辞不满意 | 对应 `markdown.content` / `button.text` / `header.title` |
| 颜色/风格不满意 | 对应 `background_style` / `font_color` / `header.template` / config color token |
| 布局/排列不满意 | 对应 `column_set.flex_mode` / `width` / `weight` / `padding` |
| 缺少某个字段/信息 | 新增 `div.fields` 条目或 `markdown` 行 |
| 某个块太拥挤/太空 | 调整 `padding` / `vertical_spacing` / `margin` |
| 交互行为问题 | 对应 `behaviors` / `confirm` / `disabled` |

### 2. 最小改动原则

- 只改被指出的组件,不动周边结构。
- 改完后**只对被修改组件所涉及的 P 项重新自检**(改颜色 → 过 P6;改分组 → 过 P1+P2;改间距 → 过 P5)。

### 3. 重发

修正完成后,重新发送一张新卡(同 Step 4),告知用户"已重新发送修正版"。

### 4. 执行前告知

向用户复述"我将修改 ×××",确认后再执行,不要静默改动。

---

## 执行清单

- [ ] 入口:判断是文字诉求(→ Step 1)还是图片输入(→ 图片分支 → 判断类型→保真策略→组件映射)
- [ ] Step 1:分析意图,输出设计方案(版本 / 宽度模式 / 颜色 / 组件)
- [ ] Step 2:读 schema.md + 组件明细 + 「好看的标准 P0–P7」
- [ ] Step 3:构造 JSON → 过 P0–P7 硬 Gate(P0+P1–P3 阻断),不过先修
- [ ] Step 4:发送,失败按常见失败表排查重试(≤3 次);仍失败则降级 Card 1.0 重构发送
- [ ] Step 5:若有交互,参考 ../lark-im-card-action-reply.md
- [ ] Step 6:用户提出修改意见时,定位组件→最小改动→原地更新或重发
references/card/lark-im-card-style.md
# Card Style Guide

选择组件组合和视觉样式的决策指南。字段写法见 `card-2.0-schema.md`。

---

## 好看的标准(P0–P7,唯一裁判基准)

**先读这一节。** 下面的「意图→组件」表和「视觉规范」都是为这套标准服务的手段;构造和自检卡片时**以 P0–P7 为准**。

**目标函数**:一张好卡片 = 让收件人在**约 2 秒一瞥**内 get 到「这是什么 + 最重要的是什么 + 要不要操作」,且观感**有序、克制、不嘈杂**。高效传达与视觉舒适在此统一。

**用力分配**:P0 必过(前置闸)→ P1–P3 强约束(阻断)→ P4–P5 基础卫生 → P6–P7 加分。

每条都附**结构化验证句**——卡片不能渲染成图,只能对 JSON 结构推理,所以验证靠「数结构」而非「眯眼看」。

| | 准则 | 可操作要求 | 结构化验证(自检句) |
|---|---|---|---|
| **P0** | **符合诉求**(前置闸·阻断) | 精确承载用户要的信息/意图/操作,不缺、不多、不跑题;意图类型与组件组合匹配 | 把诉求拆成信息点清单,逐点在 JSON 里找到承载组件;操作诉求逐个找到交互组件。有缺=不过 |
| **P1** | **层级**(强约束·阻断) | header 承载「这是什么」;body 内**有且仅有一个**最强焦点(最大字号/最重色/指标卡大数字),其余为支撑;标题用 `**加粗**`、次要信息用 grey | 列出所有文本的「字号+粗细+颜色」三元组,能否排出主>次>辅三层;焦点是否唯一 |
| **P2** | **分组**(强约束·阻断) | 同主题字段收进同一容器(`column_set`/`interactive_container`/背景块),不同主题分容器;块边界靠容器底色/描边/间距,**而非一路 `hr` 平铺** | 数顶层视觉块个数;是否存在「多主题挤在同一无分隔 markdown / 一路 hr 平铺」反模式 |
| **P3** | **复杂度适中**(强约束·阻断·双边带) | 下限:不得纯文字流水账,至少有分块+层级+适度色彩/图标;上限:视觉块 2–5、主色系 ≤3、组件不堆砌、焦点唯一 | ①是否 >1 个视觉块且含≥1 个非纯文本结构元素(背景块/指标卡/图标/表格);②块数 ≤5、主色系 ≤3。两端都满足才过 |
| **P4** | **对比**(基础卫生) | 标题与正文字号或粗细至少差一档;强调用色/放大;正文不滥用 `#/##/###`(数值焦点放大除外,见 P1) | 标题与正文是否在「字号或粗细」上至少差一档 |
| **P5** | **对齐**(基础卫生) | 间距优先交容器 `vertical_spacing`/`horizontal_spacing`/`padding`,**不滥用散设 margin 造成疏密无规律**;间距值收敛到一套档位(2/4/8/12px);顶层容器间距一致 | 是否存在无规律的散落 margin;间距取值种类是否 ≤4 |
| **P6** | **语义一致**(加分) | 红=降/警/失败、绿=升/成/通过、grey=次要;主色系起始色由 header 决定、取邻近色环;同色同义 | 同一颜色是否对应同一语义;header 模板色与块色是否同色系 |
| **P7** | **健壮**(加分) | 并列/指标列默认 `weighted` 或 `none`、**慎用 `stretch`**(防移动端拉伸);需要时配 `config.style.color` 的 light/dark;不靠固定像素宽硬排 | 是否存在 stretch 拉伸风险;深浅色是否都可读 |

---

## 意图 → 组件组合

### 通知类(无交互或只读)

| 用户意图 | 推荐组件组合 | header.template |
|---|---|---|
| 纯文字通知 / 系统公告 | `column_set`(通知正文,带 `blue-50` 背景)+ `button(open_url)` | `blue` |
| 活动公告(带主视觉图) | `img`(主图)+ `markdown`(时间/地点)+ `column_set`(详情对)+ `button(open_url)` | `turquoise` / `blue` |
| 成功 / 完成状态通知 | `column_set`(关键字段,带 `green-50` 背景)+ `markdown`(结论加粗) | `green` |
| 审批结果反馈(已通过 / 已拒绝) | `column_set`(申请信息)+ `column_set`(审批结论 + icon,带 `green-50`/`red-50` 背景) | `green` / `red` |
| 生日 / 节日祝福 | `img`(主图)+ `column_set`(人名/日期)+ `button(open_url)` | `orange` |
| 产品 / 功能上线推广 | `img`(主图)+ `markdown`(亮点)+ `column_set`(功能高亮块)+ `button(open_url)` | `blue` / `violet` |
| 多图展示(图集、AI 生成图) | `img_combination` 或 多个 `img` + `markdown`(说明)+ `button(callback)` | `default` |

### 提醒 + 操作类

| 用户意图 | 推荐组件组合 | header.template |
|---|---|---|
| 提醒 + 一键操作 | `column_set`(详情,带 `yellow-50` 背景)+ `button(callback)` | `yellow` |
| 任务清单 / 待办跟踪 | `checker` × N(每项带 `behaviors: callback`)+ `button(callback)`(全部完成操作) | `blue` |
| 告警触发(需立即处理) | `column_set`(告警指标,带 `red-50` 背景)+ `column_set`(描述 + input 快速备注)+ `button(callback)` | `red` |
| 告警已解决 / 状态变更 | `column_set`(解决时间 / 负责人,带 `green-50` 背景)+ `markdown`(结论加粗) | `green` |
| 审批待处理(含备注输入) | `column_set`(申请信息,带 `grey-50` 背景)+ `column_set`(input 审批意见)+ `button(callback)` × 2(通过 / 拒绝) | `default` |
| 日历 / 日程提醒(含参与人) | `column_set`(时间 / 地点,带 `yellow-50` 背景)+ `person_list`(参与人)+ `button(callback)` | `yellow` |
| 危险操作确认 | `column_set`(说明,带 `red-50` 背景)+ `button(callback)` + `confirm` 弹窗配置 | `red` |

### 数据 / 报告类

| 用户意图 | 推荐组件组合 | header.template |
|---|---|---|
| 日报 / 工作汇报 | `column_set`(指标,带背景色)+ `interactive_container`(进展分块,带描边)× N;内容过长的块用 `collapsible_panel` 折叠次要细节 | `blue` / `default` |
| 数据看板(含图表) | `column_set`(指标,带 `blue-50` 背景)+ `chart` + `table`(根节点,不可嵌套)+ `markdown`(说明) | `blue` |
| 排行榜 | `column_set` 固定列宽(序号 + 头像 `img` + 名字 + 指标)循环条目 | `grey` |
| 订单 / 工单详情 | `div.fields`(字段对)或 `column_set`(需彩色背景块时)+ `button(callback)` | `orange` |

### 表单 / 收集类

| 用户意图 | 推荐组件组合 | header.template |
|---|---|---|
| 纯文字表单收集 | `form`(内含 `input` + `button(form_action_type: submit)`) | `blue` |
| 带下拉选择的表单(单选) | `form`(内含 `select_static` / `select_person` + `input` + `button`) | `wathet` |
| 带多选的表单 | `form`(内含 `multi_select_static` / `multi_select_person` + `input` + `button`) | `wathet` |
| 含日期 / 时间的表单 | `form`(内含 `date_picker` / `picker_time` / `picker_datetime` + `input` + `button`) | `blue` |
| 设备 / 服务反馈 | `form`(内含 `select_static`(满意度)+ `input`(备注)+ `button`) | `yellow` |
| 多步骤进度 / 引导 | `column_set`(横向步骤,带 `blue-50` 背景)+ `markdown`(当前状态)+ `button` | `blue` |

### 推荐 / 选择类

| 用户意图 | 推荐组件组合 | header.template |
|---|---|---|
| 推荐列表(带图卡片,可点击) | `interactive_container`(内含 `img` + `markdown`)× N + `button(open_url)` | `blue` |
| AI 引导选项 / 功能菜单 | `markdown`(欢迎语)+ `interactive_container`(内含 `markdown` 选项说明)× N | 无 header |
| Bot 功能引导 / 教程 | `column_set`(步骤说明,带背景)+ `button` × 2(主操作 / 次操作) | `blue` |
| 服务台 / 多操作入口 | `column_set`(说明,带背景)+ `button` × N(≤3 个主操作,`type` 区分主次);次要操作超过 3 个时改用 `overflow`(折叠菜单) | 无 header |

### 社交 / 互动类

| 用户意图 | 推荐组件组合 | header.template |
|---|---|---|
| 工作圈 / 社交分享 | `img_combination`(多图)+ `markdown`(正文)+ `button(open_url)` × 2 | `blue` |
| 成交 / 业绩公告 | `img`(庆祝图)+ `markdown`(成绩)+ `column_set`(关键数字) | `green` |

---

## 视觉规范(实现 P0–P7 的具体战术)

组件选型只解决「有没有」,下面各条是落地上面 P0–P7 的具体手段,括号标注它主要服务的原则。

> **P3 特例 — 数据看板类**:`chart + table + column_set + markdown` 是四种不同组件各出现一次,不算「堆砌」,P3 上限照常满足;但仍须保证每类只出现一次。

### 0. Header 图标(服务 P3 · 视觉质感底线)

**几乎所有卡片都应配 header icon**——这是提升「精致感」成本最低的一步,缺失会让 header 显得空洞、平价。

```json
"header": {
  "title": { "tag": "plain_text", "content": "卡片标题" },
  "template": "blue",
  "icon": { "tag": "standard_icon", "token": "calendar_colorful" }
}
```

- `token` 必须从 `resource/icons.md` 的精确枚举中选择;禁止根据名称规律自行拼接 token。没有合适的 token 时省略 icon。
- 场景速查:日历 `calendar_colorful`、待办 `todo_colorful`、投票 `vote_colorful`、妙记 `file-lark-minutes_colorful`、多维表格 `wiki-bitable_colorful`、表单 `file-form_colorful`、社区 `larkcommunity_colorful`、招聘 `hirelogo_colorful`、飞书品牌 `lark-logo_colorful`、Meego `meego_colorful`、AI `myai_colorful`、aPaaS `apaas_colorful`、审批 `approval_colorful`、通用 AI `ai-common_colorful`。

### 1. 配色纪律(服务 P6 语义一致)

- **邻近色环**:`Red → Carmine → Orange → Yellow → Green → Turquoise → Wathet → Blue → Violet → Purple →(回到)Red`。一张卡只能取色环上**相邻**的颜色,严禁跳跃(❌ blue + green + red)。
- **最多 3 种主色系**(不含 grey / white)。
- **起始色由 header 决定**:
  - header `blue` → blue / violet / purple
  - header `green` → green / turquoise / wathet
  - header `red` → red / carmine / orange
  - 无 header → 默认 blue / violet / purple
- **深浅语义**(写法 `blue-50`、`blue-600`、`grey-500`):
  - `-50` 区块背景 · `-100` 标签背景 · `-500` 正文文字 · `-600`/`-700` 强调文字

### 2. 间距纪律(服务 P5 对齐 · 视觉决定性因素)

- **body padding 推荐**:`"padding": "12px 12px 20px 12px"`(上右下左;底部 20px 留白更舒适)。
- **优先不用 `markdown` / `column` 的 `margin` 控间距**:交给父容器的 `vertical_spacing` / `horizontal_spacing` / `padding` 统一管理,多数情况显式置 `0px`;仅在需要精细缩进(如层级左缩进)时才设非零值。
- 容器内 `vertical_spacing` 推荐值:`2px`(高亮块内标题↔正文)/ `4px`(正文段落、列表项)/ `8px`(需拉开的元素)。
- **容器间智能 margin**:某个顶级容器若**不是** body 最后一个元素 → 设 `"margin": "0px 0px 12px 0px"`;若**是**最后一个 → `"0px"` 或不设,避免卡片底部多余留白。

### 3. 指标卡模式(服务 P1 焦点 · 出现 KPI / 数值 / 统计词时强制使用)

触发:内容含 `KPI/ROI/CTR/UV/PV/DAU/GMV/转化率/增长率/总数/营收` 等数值类信息。

- 多个指标并列放进一个 `column_set`,`flex_mode` **默认用 `"none"`、慎用 `"stretch"`**(防移动端拉伸变形,P7);仅在各列内容等宽、确认移动端不变形时才用 stretch。
- 数值:用 `##` 放大(**唯一允许用 markdown 标题的特例**),可配 `<font>` 上色。
- 描述:`<font color='grey'>` + `text_size: "notation"`。
- 居中 `text_align: "center"`;列背景 `background_style: "grey-50"`;`padding: "12px"`;`vertical_spacing: "2px"`。

```json
{
  "tag": "column_set",
  "flex_mode": "none",
  "horizontal_spacing": "12px",
  "columns": [
    { "tag": "column", "width": "weighted", "weight": 1,
      "background_style": "grey-50", "corner_radius": "8px",
      "padding": "12px", "vertical_spacing": "2px",
      "elements": [
        { "tag": "markdown", "content": "## <font color='blue'>5,483</font>", "text_align": "center" },
        { "tag": "markdown", "content": "<font color='grey'>GMV($)</font>", "text_align": "center", "text_size": "notation" }
      ] }
  ]
}
```

### 4. 描边卡片模式(服务 P2 分组 · 进展 / 事项 / 列表项分块展示)

用 `interactive_container` 给每个事项块加描边 + 圆角,视觉上比彩色底色更轻盈,适合进展/工单/任务列表等「多条目」场景。

```json
{
  "tag": "interactive_container",
  "width": "fill",
  "has_border": true,
  "border_color": "blue-100",
  "corner_radius": "8px",
  "background_style": "blue-50",
  "padding": "12px 12px 12px 12px",
  "vertical_spacing": "4px",
  "margin": "0px 0px 12px 0px",
  "elements": [
    {
      "tag": "markdown",
      "content": "**<font color='blue'>事项标题</font>**"
    },
    {
      "tag": "markdown",
      "content": "事项正文内容……",
      "text_size": "normal"
    }
  ]
}
```

- `border_color` 跟随主色系(蓝系用 `blue-100`,绿系用 `green-100`)。
- 不需要交互时可省略 `behaviors`;需要点击回调时加 `"behaviors": [{"type":"callback","value":{...}}]`。
- **不能在内部放 `form` 或 `table`**。

### 5. 高亮块模式(服务 P2 分组 · 多分类信息成块展示)

两层结构:外层 `column_set` 管布局,内层 `column` 管样式(彩色背景)。

- 每个 `column` 设 `background_style` 用浅色(如 `blue-50` / `green-50`),`padding: "12px 12px 12px 12px"`,`vertical_spacing: "4px"`,`weight: 1`。
- 块内首行用 `**<font color='blue'>分类标题</font>**` 着色加粗,正文紧随。
- **布局选择**:分类 ≤ 3 个且内容简短 → 水平,优先用 `flex_mode: "bisect"`(2 列)或 `"trisect"`(3 列);各列字数严格等宽且已确认移动端不变形时才用 `stretch`(慎用,见 §9);**分类 ≥ 4 个、奇数、或任一块内容 > 3 行 → 垂直**(每块独占一行)。配色按上面第 1 条邻近色环依次取色。
- ⚠️ **版本依赖**:`column.background_style` 需客户端 **≥ v7.9**,旧版静默丢背景。要求强健壮性时改用 `interactive_container` 的 `background_style`(无版本限制)替代 column 背景色。

### 6. Header 三件套(服务 P1 层级 · 语境补全)

header 有三层能力,**尽量用满**(至少用 `title` + `icon`;`subtitle` 和 `text_tag_list` 按实际诉求取舍)——这是成本最低、语境最清晰的一步:

- `title`:这是什么(必填)
- `subtitle`:一句上下文(谁发 / 什么时间 / 什么状态),≤1 行,`plain_text`
- `text_tag_list`:状态标签,≤3 个,颜色语义与 P6 保持一致(`blue`=信息、`yellow`=待处理、`red`=紧急、`green`=完成)

```json
"header": {
  "title":    { "tag": "plain_text", "content": "发版审批" },
  "subtitle": { "tag": "plain_text", "content": "2026-06-25 · 后端服务" },
  "template": "blue",
  "icon": { "tag": "standard_icon", "token": "approval_colorful" },
  "text_tag_list": [
    { "tag": "text_tag", "text": { "tag": "plain_text", "content": "待审批" }, "color": "yellow" }
  ]
}
```

**禁止**:在 `header.title` 里写 emoji;把 subtitle 信息改塞进 body 第一行 markdown,让 header 空洞;严肃场景(审批/告警/财务)在 title 或 body 标题里用装饰性 emoji。

### 7. 字段对用 `div.fields`,不要用 `column_set` 模拟(服务 P5 对齐)

详情型"label: value"(订单字段、审批信息、日程详情)首选 `div.fields`——原生对齐,结构更轻:

```json
{
  "tag": "div",
  "fields": [
    { "is_short": true, "text": { "tag": "lark_md", "content": "**提交人**\n张三" } },
    { "is_short": true, "text": { "tag": "lark_md", "content": "**部门**\n研发中台" } },
    { "is_short": true, "text": { "tag": "lark_md", "content": "**提交时间**\n2026-06-25 10:30" } },
    { "is_short": true, "text": { "tag": "lark_md", "content": "**优先级**\n<font color='red'>P0</font>" } }
  ]
}
```

`is_short: true` 的字段自动两两并排,对齐由组件保证。`column_set` 留给**需要彩色背景块 / 不等宽 / 嵌套复杂结构**的场景,不要用它模拟简单字段对。

### 8. 长文本必须设 `lines` 截断(服务 P3 复杂度上限)

凡接收动态数据的文本字段,必须设最大行数避免卡片被撑爆:

| 位置 | 字段 | 推荐上限 |
|---|---|---|
| `div.text` | `lines` | 正文 ≤4,次要说明 ≤2 |
| `person_list` | `lines` | ≤2 |
| `table.header_style` | `lines` | ≤1 |
| `collapsible_panel` | 默认折叠 | 长文本优先用折叠面板而非截断 |

不设 `lines` 的动态文本 = P3 上限的隐患。

### 9. `flex_mode` 决策表(服务 P7 健壮)

| 场景 | 推荐 flex_mode | 原因 |
|---|---|---|
| 指标卡并列(内容不等长) | `none` + `width: weighted` | 防移动端拉伸;各列按比例压缩 |
| 2 列等宽内容(字数相近) | `bisect` | 语义最清晰的两等分 |
| 3 列等宽内容 | `trisect` | 三等分,不写 weight |
| 多 tag / 多图标横排,允许换行 | `flow` | 窄屏自动折行,不挤压 |
| 明确要求两端对齐撑满且内容等宽 | `stretch` | 慎用:移动端窄屏内容过长时会拉伸变形 |

> `stretch` 只在各列字数高度相近、且已确认移动端不变形时使用;其余场景默认 `none`。

### 10. `chart` 配色纳入 P6 纪律

`chart.color_theme` 必须与全卡色系保持一致:

- **默认**:`brand`(单色系,跟随飞书品牌色)或 `primary`(主色单色系),安全选项。
- **禁止**:`rainbow`——会把色环上的跳跃色全打进图表,直接击穿 P6 的"主色系 ≤3 + 邻近色环"约束。
- **例外**:数据维度 ≥4 个系列、且各系列无主次关系(如区域对比图)时,可用 `complementary` 或在 `chart_spec` 里自定义与主色系邻近的颜色数组。

### 11. `laser` 样式的克制规则(服务 P6 语义一致)

`button.type: "laser"` 和 `background_style: "laser"` 是高饱和渐变效果:

- **允许**:AI 生成类、节日庆祝类、营销推广类,每卡 **≤1 处**,且位置在主操作按钮或视觉焦点块。
- **禁止**:审批、告警、财务、工单、日程等严肃场景——laser 在这些场景里显得轻浮廉价。
- **默认不用**;Step 1 设计方案里若要用,需显式说明"×× 场景适合 laser 风格"并得到确认。
references/card/resource/colors.md
# 颜色枚举

卡片所有颜色字段(`font_color` / `text_color` / `background_style` / `border_color` / icon `color` 等)共用同一套枚举,按属性名区分用途,无单独的文字/背景色表。

## 基础色名(14 色系)

`blue` `carmine` `green` `indigo` `lime` `orange` `purple` `red` `sunflower` `turquoise` `violet` `wathet` `yellow` `grey`

> **标签例外**:`text_tag` / `<text_tag>` 的灰色用 `neutral`(不是 `grey`);标签枚举无 `grey`。

## 深浅后缀

- 彩色系(13 个非 grey):`-50 -100 -200 -300 -350 -400 -500 -600 -700 -800 -900`,数字越大越深。
- **无后缀基础名(如 `blue`)= `-600`**(同色值)。
- grey 范围更细:`-00 -50 -100 … -650 … -950 -1000`。
- 用法语义:`-50` 区块背景 · `-100` 标签背景 · `-500` 正文 · `-600/-700` 强调文字。

## 特殊值

`white`(白)· `bg-white`(背景白:浅色 #ffffff / 深色 #1A1A1A)。无 `transparent` 枚举。

## 自定义 RGBA

在 `config.style.color` 定义 token 再引用:

```json
"config": { "style": { "color": {
  "cus-0": { "light_mode": "rgba(5,157,178,0.52)", "dark_mode": "rgba(...)" }
} } }
```

组件里写 `"font_color": "cus-0"`。RGBA 支持的属性同枚举(font/text_color、background_style、border_color、icon color 等)。

> `column` 的 `background_style` 需客户端 v7.9+。配色搭配规则见 `../lark-im-card-style.md` 视觉规范。
references/card/resource/icons.md
# 图标枚举

用于 `header.icon`、`div.icon`、`markdown` 的 `<link icon=...>` 等。

## 结构

```json
// 系统图标(推荐):用 token
{ "tag": "standard_icon", "token": "info_outlined", "color": "blue" }
// 自定义图标:用上传的 img_key
{ "tag": "custom_icon", "img_key": "img_v3_xxx" }
```

`color` 取颜色枚举(见 `colors.md`),仅对 `standard_icon` 生效。

## token 命名

- 线性:后缀 `_outlined`;面性(实心):后缀 `_filled`。
- 主体 kebab-case,如 `calendar-add_outlined`、`delete-trash_outlined`。

## 常用 token(业务卡片)

| 含义 | token | 含义 | token |
|---|---|---|---|
| 完成/对勾 | `done_outlined` | 关闭/叉 | `close_outlined` |
| 新增 | `add_outlined` | 编辑 | `edit_outlined` |
| 删除 | `delete-trash_outlined` | 搜索 | `search_outlined` |
| 设置 | `setting_outlined` | 信息 | `info_outlined` |
| 警告 | `warning_outlined` | 时间 | `time_outlined` |
| 日历 | `calendar_outlined` | 成员 | `member_outlined` |
| 群组 | `group_outlined` | 会话 | `chat_outlined` |
| 邮件 | `mail_outlined` | 链接 | `link-copy_outlined` |
| 分享 | `share_outlined` | 下载 | `download_outlined` |
| 通知/铃铛 | `bell_outlined` | 定位 | `pin_outlined` |
| 附件 | `attachment_outlined` | 审批 | `approval_outlined` |

## 彩色图标(精确 token)

彩色图标必须从下表按**完整字符串**选择,禁止根据名称规律自行拼接。彩色 token 自带颜色,不要再推导其他后缀或变体。

| 含义 | token | 含义 | token |
|---|---|---|---|
| 日历 | `calendar_colorful` | 待办 | `todo_colorful` |
| 投票 | `vote_colorful` | 飞书妙记 | `file-lark-minutes_colorful` |
| 多维表格 | `wiki-bitable_colorful` | 表单 | `file-form_colorful` |
| 飞书社区 | `larkcommunity_colorful` | 招聘 | `hirelogo_colorful` |
| 飞书品牌 | `lark-logo_colorful` | Meego | `meego_colorful` |
| AI | `myai_colorful` | aPaaS | `apaas_colorful` |
| 审批 | `approval_colorful` | 通用 AI | `ai-common_colorful` |

> token 必须与官方完全一致,否则图标不渲染。上表为常用项,全量(数百个,分系统/商务/沟通/用户/媒体/文档等类目)以官方图标库为准:
> https://open.larkoffice.com/document/feishu-cards/enumerations-for-icons
references/lark-im-card-action-reply.md
# card.action.trigger

> **Prerequisite:** Read [`../../lark-event/SKILL.md`](../../lark-event/SKILL.md) first for `event consume` essentials.

Fires when a user interacts with an interactive card — button click, form submit, dropdown select,
checkbox toggle, date/time pick, etc.

## Setup (required)

> **Console configuration required**: In the Feishu Developer Console, go to
> **App → Events & Callbacks → Callback Configuration** (应用--事件与回调--回调配置) and enable it.
> The consumer starts without errors even when not configured, but **no events will be received**.
> There is no preflight check for this setting.

After enabling, events are delivered over the existing WebSocket long connection — no additional
URL configuration needed.

## Scopes & auth

| Scope | Required for |
|---|---|
| `im:message:readonly` | Auto-fetch `card_content` via message get API (covers both p2p and group messages) |

Auth: `bot` only.

## Output fields

| Field | Type | Description |
|---|---|---|
| `type` | string | Always `card.action.trigger` |
| `event_id` | string | Unique event ID; safe for deduplication |
| `timestamp` | string (timestamp_ms) | Event delivery time (ms since epoch) |
| `operator_id` | string (open_id) | Open ID of the user who interacted |
| `message_id` | string (message_id) | Message ID of the card (`om_xxx`) |
| `chat_id` | string (chat_id) | Chat ID (`oc_xxx`) |
| `host` | string | `im_message` (chat card) or `im_top_notice` (top banner) |
| `token` | string | Delayed-update token; valid 30 min, max 2 uses |
| `action_tag` | string | Component type that was triggered (see decision table) |
| `action_value` | string | Developer-defined value on the component; serialized to JSON string |
| `action_name` | string | `name` attribute of the component |
| `timezone` | string | User timezone, e.g. `Asia/Shanghai`; only populated for date/time picker interactions |
| `form_value` | string (JSON) | All form field values as JSON string, keyed by component `name`; only present when a button inside a form container is clicked |
| `input_value` | string | Input text; only for standalone `input` components (not inside a form) |
| `option` | string | Selected value for standalone single-select: `select_static`, `select_person`, `overflow`, `date_picker`, `picker_time`, `picker_datetime` |
| `options` | string | Comma-separated selected values for standalone multi-select: `multi_select_static`, `multi_select_person` |
| `checked` | bool | Checkbox state for standalone `checker` elements |
| `card_content` | string | Original card content (userDSL text format) from when the card was sent; auto-fetched via message get API at consume time; empty if `message_id` absent or fetch fails — skip if empty |

## `card_content` — what it is and how to use it

`card_content` is the `user_dsl` field extracted from the card message content, auto-fetched
at event consume time. It represents the card's original definition — use it as the starting
point to understand the current card structure and construct the updated card JSON.

No extra API call is needed — the consumer fetches it automatically. If empty, skip — no fallback required.

## action_tag decision table

> **Form container rule**: when a component is inside a `form` container, its value appears in
> `form_value[name]` instead of the standalone fields (`option`, `options`, `input_value`,
> `checked`). There is no `form_submit` tag — form submission comes through as `button` with
> `form_value` populated.

| `action_tag` | Read field(s) | Notes |
|---|---|---|
| `button` | `action_value` (fromjson if object); `form_value` if inside a form | Most common; `form_value` non-empty = form submit |
| `overflow` | `option` | Collapsible button group selection |
| `select_static` | `option` (standalone) or `form_value[name]` (in form) | Single-select dropdown |
| `multi_select_static` | `options` (standalone) or `form_value[name]` (in form) | Multi-select dropdown |
| `select_person` | `option` — open_id of selected user | Single-select person |
| `multi_select_person` | `options` — comma-separated open_ids | Multi-select person |
| `input` | `input_value` (standalone) or `form_value[name]` (in form) | Text input |
| `checker` | `checked` (standalone) or `form_value[name]` (in form) | Checkbox |
| `date_picker` | `option` (date string) + `timezone` | e.g. `"2024-04-01 +0800"` |
| `picker_time` | `option` (time string) + `timezone` | e.g. `"08:30 +0800"` |
| `picker_datetime` | `option` (datetime string) + `timezone` | e.g. `"2024-04-29 07:07 +0800"` |
| `select_img` | `option` (single) or `options` (multi) | Image picker |

## Key constraints

1. Token **valid 30 minutes**, **max 2 uses** — if update fails after exhaustion, inform the user
2. Delayed-update API requires **complete new card JSON** — partial updates are not supported
3. SDK auto-responds `{"code":200}` within 3 s — your update call can be sent any time within 30 min
4. `card_content` is auto-populated — no extra API call needed; if empty, skip it

## After starting the listener

Once the listener is running, check whether your agent runtime supports background event
monitoring (i.e. can receive and process stdout lines from a running subprocess while
continuing to respond to the user). If it does, prompt the user:

> "Card callback listener is now active. Do you want me to automatically handle card
> interactions and update the card based on user actions?"

Only enter the auto-update workflow below if the user confirms. If your runtime does not
support background monitoring, inform the user that automatic card updates are not available
and they will need to handle interactions manually.

## Agent workflow

When a `card.action.trigger` event arrives (**each stdout JSON line is one event — process it immediately**):

```
1. Read action fields to understand what the user did:
   - action_tag: which component was triggered
   - action_value / option / options / checked / input_value / form_value: what value was set

2. Decide: does this interaction require a card update?
   - e.g. button click with a business action → yes
   - e.g. navigation / pagination → no (just record, no update needed)
   - Not every callback requires a card update — decide based on business semantics
   - Before updating, explicitly state what visual change the action requires. If you cannot articulate one, skip the update.

3. If update is needed:
   a. If card_content is empty: inform the user that the original card could not be fetched,
      so it is not possible to determine whether an update is needed — do not guess
   b. Determine the new card state based on the action
   c. Use card_content as the structural basis to construct the updated card JSON
   d. Detect card version: if card_content contains `"schema":"2.0"` or `"schema": "2.0"` it is Card 2.0; otherwise assume Card 1.0
   e. For Card 1.0: include `"open_ids": ["<operator_id>"]` inside the `card` object, or the API returns code 300090
   f. Call the delayed update API with the token and new card JSON

4. If no update: end (the SDK has already acknowledged the callback)
```

## Updating the card

```bash
lark-cli api POST /open-apis/interactive/v1/card/update --as bot \
  --data '{"token":"<token>","card":<new_card_json>}'
```

`--data` parameters:

| Field | Required | Description |
|---|---|---|
| `token` | Yes | Delayed-update token from the event |
| `card` | Yes | Complete new card JSON — construct based on `card_content` from the event, modified to reflect the new state |
| `card.open_ids` | No | **Card 1.0 only.** Array of `open_id`s defining which users see the updated card. Must contain at least one open_id (e.g. the operator's); passing `[]` or omitting the key both cause "openid empty" (code 300090). |

## Examples

```bash
# Stream all card interactions
lark-cli event consume card.action.trigger --as bot

# Grab one callback to inspect shape (debugging only — do not use in production workflows)
lark-cli event consume card.action.trigger --as bot --max-events 1 --timeout 60s

# Button clicks only (not form submit), with action value
lark-cli event consume card.action.trigger --as bot \
  --jq 'select(.action_tag == "button" and .form_value == "") | {op: .operator_id, val: (.action_value | fromjson?), token: .token}'

# Form submits (button with form_value present)
lark-cli event consume card.action.trigger --as bot \
  --jq 'select(.action_tag == "button" and .form_value != "") | {op: .operator_id, form: (.form_value | fromjson), token: .token}'

# Date picker interactions
lark-cli event consume card.action.trigger --as bot \
  --jq 'select(.action_tag == "date_picker") | {op: .operator_id, date: .option, tz: .timezone}'

# Filter to one chat
lark-cli event consume card.action.trigger --as bot \
  --jq 'select(.chat_id == "oc_xxx")'
```

## Gotchas

- **No `form_submit` tag**: form submission comes as `action_tag = "button"` with `form_value`
  populated. Check `form_value != ""` to distinguish from a standalone button click.
- **`action_value` type is developer-defined**: the original may be an object or a plain string.
  Use `fromjson?` (with `?` to swallow errors) or check before parsing.
- **Standalone vs form fields**: `input_value`, `option`, `options`, `checked` are only populated
  for components **not** inside a form container. Inside a form, all values appear in `form_value`.
- **WebSocket delivery**: no separate callback URL needed; uses the existing WS connection.
references/lark-im-chat-create.md
# 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, chat type (private/public), and group mode. Set `--chat-mode topic` to create a topic chat.

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

# Create a topic chat
lark-cli im +chat-create --name "Topic Group" --chat-mode topic

# 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 <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 <text>` | No | Max 100 characters | Group description |
| `--users <ids>` | No | Up to 50, format `ou_xxx` | Comma-separated user open_ids |
| `--bots <ids>` | No | Up to 5, format `cli_xxx` | Comma-separated bot app IDs |
| `--owner <open_id>` | No | Format `ou_xxx` | Owner open_id (defaults to the bot when using `--as bot`, or the authorized user when using `--as user`) |
| `--type <type>` | No | `private` (default) or `public` | Group type. Default to `private`; pass `public` only when the user explicitly asks for a discoverable/public group. |
| `--chat-mode <mode>` | No | `group` (default) or `topic` | Group mode; `topic` creates a topic chat (not the same as `group_message_type=thread`). When the user asks for a topic chat, pass `topic` explicitly — do not rely on the default. |
| `--set-bot-manager` | No | - | Set the creating bot as a group manager (only effective with `--as bot`) |
| `--format json` | No | - | Output as JSON |
| `--as <identity>` | No | `bot` or `user` | Identity type |
| `--dry-run` | No | - | Preview the request without executing it |

> **`--chat-mode topic` vs "normal group with topic-message mode"**: `--chat-mode topic` here creates a 话题群 — the entire group is a topic chat. This is different from "normal group (`chat_mode=group`) + topic-message mode (`group_message_type=thread`)". This CLI exposes only `chat_mode`; `group_message_type` is intentionally not surfaced.

## 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 "<name or email>"` to retrieve it.
2. **Create the group — by default include the current user:**

   ```bash
   lark-cli im +chat-create --name "<group name>" \
     --users "<current user open_id>" --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":"<chat_id from step 2>","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 "<group 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
references/lark-im-chat-identity.md
# 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
references/lark-im-chat-list.md
# im +chat-list

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

List chats the current user (or bot, with `--as bot`) is a member of. **Not a search API — there is no `--query` parameter; the call always returns the full member list, paginated.** For keyword-based lookup (e.g. find a group by name or by member), use [`+chat-search`](lark-im-chat-search.md) instead.

**Defaults to groups only**; pass `--types=p2p,group` (or `--types p2p --types group`) to also include p2p single chats (user identity only — see ["Bot identity and p2p"](#bot-identity-and-p2p)). Supports pagination, sort order, and (user identity only) muted-chat filtering.

This skill maps to the shortcut: `lark-cli im +chat-list` (internally calls `GET /open-apis/im/v1/chats`).

## Commands

```bash
# List the user's chats (default sort: create_time, ascending)
lark-cli im +chat-list

# Sort by recent activity (most recently active first)
lark-cli im +chat-list --sort active_time

# Limit page size
lark-cli im +chat-list --page-size 50

# Pagination
lark-cli im +chat-list --page-token "xxx"

# Fetch multiple pages automatically, up to 10 pages by default
lark-cli im +chat-list --page-all

# Drop muted chats (user identity only)
lark-cli im +chat-list --exclude-muted

# JSON output
lark-cli im +chat-list --format json

# Preview the request without executing it
lark-cli im +chat-list --dry-run

# Include p2p single chats (user identity only) — comma form
lark-cli im +chat-list --as user --types p2p,group

# Same, using repeat flag instead of CSV
lark-cli im +chat-list --as user --types p2p --types group

# Only p2p single chats (user identity only)
lark-cli im +chat-list --as user --types p2p
```

## Parameters

| Parameter | Required | Limits | Description |
|------|------|------|------|
| `--user-id-type <type>` | No | `open_id` (default), `union_id`, `user_id` | ID type used for `owner_id` in the response |
| `--types <strings>` | No | `group`, `p2p` (comma-separated or repeated) | Chat types to include. Omitted = groups only (backward compatible). `p2p` requires user identity (`--as user`); under `--as bot`, `--types=p2p` alone is rejected and `--types=p2p,group` is silently downgraded to `group` |
| `--sort <field>` | No | `create_time` (default, ascending), `active_time` (descending) | Result ordering |
| `--page-size <n>` | No | 1-100, default 20 | Number of results per page |
| `--page-token <token>` | No | - | Starting cursor, normally returned by a previous response |
| `--page-all` | No | - | Automatically fetch and merge subsequent pages; capped by `--page-limit` |
| `--page-limit <n>` | No | 1-1000, default 10 | Maximum pages fetched by `--page-all` |
| `--exclude-muted` | No | User identity only | Drop chats the current user has muted (do-not-disturb). Under `--as bot`, the flag is silently inactive; see "Filtering muted chats" below |
| `--format json` | No | - | Output as JSON |
| `--dry-run` | No | - | Preview the request without executing it |

> **Note:** Supports both `--as user` (default) and `--as bot`. When using bot identity, the app must have bot capability enabled.

With `--page-all`, `--page-token` sets the starting cursor. If `meta.pagination.complete=false`, resume from `meta.pagination.next_token` or raise `--page-limit`.

## Output Fields

| Field | Description |
|------|------|
| `chat_id` | Chat ID (`oc_xxx` format) |
| `name` | Chat name |
| `description` | Chat description |
| `owner_id` | Owner ID (type controlled by `--user-id-type`) |
| `external` | Whether the chat is external |
| `chat_status` | Chat status (`normal` / `dissolved` / `dissolved_save`) |
| `chat_mode` | Chat mode discriminator: `group` (regular) / `topic` (topic group) / `p2p` (single chat) |
| `p2p_target_type` | Peer type, e.g., `user` |
| `p2p_target_id` | Peer ID (type controlled by `--user-id-type`) |

## Including p2p single chats

Default behavior lists groups only — same as before this feature. To include p2p, pass `--types`:

| User intent | Call | Identity |
|---|---|---|
| "list my groups" / 我的群 / 我加入了哪些群 | (default, omit `--types`) | user or bot |
| "list my p2p chats" / 我的单聊 / 我跟谁有 1v1 | `--types p2p` | **user only** |
| "all my chats" / 全部聊天 / 所有会话 (ambiguous) | `--types p2p,group` | **user only** |

For p2p rows in the response: `name` is the peer's display name, `owner_id` follows group semantics, `chat_mode = "p2p"`, and `p2p_target_type` / `p2p_target_id` identify the peer.

## Bot identity and p2p

`tenant_access_token` cannot list p2p chats — to protect user privacy, bot identity is not permitted to enumerate p2p single chats. Behavior under `--as bot`:

- `--as bot --types=p2p` → rejected at validation time with an actionable error; no request is sent.
- `--as bot --types=p2p,group` → CLI strips `p2p` and sends `types=group`. Request proceeds; only groups are returned. The strip is a **request-level adjustment**, surfaced two ways so neither humans nor agents miss it:
    - **stderr**: `warning: bot_strip_p2p: To protect user privacy, bot identity cannot list p2p chats; --types=p2p,group was sent as types=group. Use --as user to include p2p.` (matches the `warning: <code>: <message>` convention in `shortcuts/common/runner.go`)
    - **stdout JSON**: a top-level `notices` array gains a structured entry:
      ```json
      {
        "chats": [...],
        "notices": [
          { "code": "bot_strip_p2p", "message": "To protect user privacy, bot identity cannot list p2p chats; …" }
        ]
      }
      ```
    - The `filter` slot stays scoped to `--exclude-muted`; `notices` is a separate top-level key, so the two never collide and no priority is needed when both fire.
    - DryRun emits the same stderr warning so a previewed request truthfully reflects what Execute will send (parity with `shortcuts/drive/drive_search.go`).
- `--as bot --types=group` → accepted, returns groups normally.
- `--as bot` (no `--types`) → unchanged, returns groups.

To include p2p single chats, switch to user identity: `--as user --types=p2p,group`.

## Filtering muted chats

`--exclude-muted` (user identity only) drops chats the current user has set to do-not-disturb. After the list call, the CLI batches the page's chat_ids through `POST /open-apis/im/v1/chat_user_setting/batch_get_mute_status` and filters client-side. Under `--as bot`, the mute API is UAT-only and the filter is silently skipped.

When the flag is set, the JSON envelope gains a `filter` sub-object (absent otherwise, so existing consumers are unaffected); `fetched_count == returned_count + filtered_count` always holds:

```json
{
  "chats": [...],
  "filter": {
    "applied": "exclude_muted",
    "fetched_count": 20,
    "returned_count": 17,
    "filtered_count": 3,
    "hint": "Filtered out 3 muted chat(s) on this page (17 remaining); use --page-token to fetch more."
  }
}
```

## Usage Scenarios

### Scenario 1: List my recent chats

```bash
lark-cli im +chat-list --sort active_time --page-size 10
```

### Scenario 2: List my non-muted chats sorted by activity

```bash
lark-cli im +chat-list --sort active_time --exclude-muted
```

### Scenario 3: Iterate all my chats programmatically

```bash
TOKEN=""
while :; do
  RESP=$(lark-cli im +chat-list --page-size 100 --page-token "$TOKEN" --format json)
  echo "$RESP" | jq -r '.data.chats[].chat_id'
  HAS_MORE=$(echo "$RESP" | jq -r '.data.has_more')
  [ "$HAS_MORE" = "true" ] || break
  TOKEN=$(echo "$RESP" | jq -r '.data.page_token')
done
```

## Common Errors and Troubleshooting

| Symptom | Root Cause | Solution |
|---------|---------|---------|
| `invalid --page-size 101: must be between 1 and 100` | page-size is out of range | Use an integer between 1 and 100 |
| Permission denied (99991672) | The bot app does not have `im:chat:read` TAT permission enabled | Enable the permission for the app in the Open Platform console |
| Permission denied (99991679) with `--as user` | UAT is not authorized for `im:chat:read` | Run `lark-cli auth login --scope "im:chat:read"` |
| `Bot ability is not activated` (232025) | The app does not have bot capability enabled | Enable bot capability in the Open Platform console |
| `--exclude-muted` returns all chats unfiltered and `hint` says "no effect under bot identity" | Running under `--as bot` (mute API is UAT-only) | Switch to `--as user` for mute filtering |
| `--types=p2p (single chats) is only supported with user identity` | `--as bot` + `--types=p2p` (single-value only; mixed `--types=p2p,group` is downgraded to `group` and surfaces a `bot_strip_p2p` notice via stderr + `outData["notices"]` — see "Bot identity and p2p") | Use `--as user`, or include `group` in `--types` (the bot proceeds with `group` only and emits the `bot_strip_p2p` notice) |

> Full error message of the row above: `--types=p2p (single chats) is only supported with user identity (--as user). To protect user privacy, bot identity cannot list p2p chats. Use --as user, or include "group" in --types.`
references/lark-im-chat-members-list.md
# im +chat-members-list

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

List the members of a chat. Users and bots are returned in **separate buckets** — `users[]` and `bots[]` — with per-bucket totals (`user_total` / `bot_total`). Use `--member-types` to return only one kind.

This skill maps to the shortcut: `lark-cli im +chat-members-list` (internally calls `GET /open-apis/im/v1/chats/{chat_id}/members/list`).

## Commands

```bash
# Single page (default)
lark-cli im +chat-members-list --chat-id oc_xxx

# Only users, or only bots
lark-cli im +chat-members-list --chat-id oc_xxx --member-types user
lark-cli im +chat-members-list --chat-id oc_xxx --member-types user,bot

# Walk every page (capped by --page-limit; 0 = unlimited)
lark-cli im +chat-members-list --chat-id oc_xxx --page-all --page-limit 0

# Fetch one page starting at a specific cursor
lark-cli im +chat-members-list --chat-id oc_xxx --page-token "xxx"

# Continue automatically from a specific cursor
lark-cli im +chat-members-list --chat-id oc_xxx --page-token "xxx" --page-all

# JSON output / preview the request
lark-cli im +chat-members-list --chat-id oc_xxx --format json
lark-cli im +chat-members-list --chat-id oc_xxx --dry-run
```

## Parameters

| Parameter | Required | Limits | Description |
|------|------|------|------|
| `--chat-id <id>` | Yes | `oc_xxx` | Target chat |
| `--member-types <strings>` | No | `user`, `bot` (comma-separated or repeated) | Member types to return. Omitted = all |
| `--member-id-type <type>` | No | `open_id` (default), `union_id`, `user_id` | ID type for `member_id` in the response |
| `--page-size <n>` | No | 1-100, default 20 | Results per page. With `--page-all` and no explicit `--page-size`, the max (100) is used automatically to minimize round-trips |
| `--page-token <token>` | No | - | Starting cursor, normally returned by a previous response |
| `--page-all` | No | - | Automatically walk every page (capped by `--page-limit`) |
| `--page-limit <n>` | No | default 10, `0` = unlimited | Max pages to fetch with `--page-all` |
| `--page-delay <ms>` | No | default 200, `0` = no delay | Delay between pages during `--page-all` (throttle to avoid rate limits on large lists) |
| `--format json` | No | - | Output as JSON |
| `--dry-run` | No | - | Preview the request without executing it |

> Supports both `--as user` (default) and `--as bot`. The caller must be in the target chat, and must belong to the same tenant for internal chats.

## Output Fields

| Field | Description |
|------|------|
| `chat_id` | The queried chat ID |
| `users` | Array of user members (`member_id`, `name`, `tenant_key`, …) |
| `bots` | Array of bot members (`member_id`, `app_id`, `name`, …) |
| `user_total` / `bot_total` | Server-reported totals for each bucket |
| `truncations` | Non-empty when the server **capped a bucket** due to security config — see below |
| `has_more` / `page_token` | Paging signals from the final page fetched |

## Truncation: the result may be incomplete

The server applies a security cap to large member lists. When a bucket is capped, the response carries a `truncations[]` entry (e.g. `[{"limit": 100, "member_type": "user"}]`) **on the final page only**. The shortcut surfaces this two ways so it is never missed:

- **stderr**: `⚠️  member list truncated by server security config: user bucket capped at 100 — the list is INCOMPLETE.`
- **stdout JSON**: the `truncations` array is preserved verbatim in the output.

A truncated result is *not* fixable by paging further — it is a server-side cap. Treat `users`/`bots` as a partial list whenever `truncations` is non-empty.

## Pagination notes

- Default fetches a single page. Pass `--page-all` to drain every page.
- With `--page-all` and no explicit `--page-size`, the shortcut uses the maximum page size (100) so a full walk takes the fewest round-trips. An explicit `--page-size` is always honored.
- `--page-all` sleeps `--page-delay` ms (default 200) between pages to avoid hammering the API when a tenant has no server-side member cap and the list spans many pages. Set `--page-delay 0` to disable.
- `--page-all` stops at `--page-limit` pages (default 10). When it stops early, `has_more` stays `true` so you know the result is incomplete; re-run with `--page-limit 0` for everything.
- `--page-token` and `--page-all` together: automatic pagination starts at the supplied cursor and continues until exhaustion or `--page-limit`.
- Across pages, `users[]` and `bots[]` are concatenated; `truncations` / `has_more` / `page_token` come from the last page fetched.

## Common Errors and Troubleshooting

| Symptom | Root Cause |   | Solution |
|---------|---------|---|---------|
| `--chat-id is required` | `--chat-id` omitted |   | Provide the `oc_xxx` chat ID |
| `invalid --page-size 101: must be between 1 and 100` | out of range |   | Use 1-100 |
| `--member-types contains invalid value` | value other than `user`/`bot` |   | Use `user`, `bot`, or both |
| Permission denied | missing `im:chat.members:read` |   | Bot: enable the scope in the console. User: `lark-cli auth login --scope "im:chat.members:read"` |
references/lark-im-chat-messages-list.md
# 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.

By default the response carries a `reactions` block (counts + details from `im.reactions.batch_query`) on every message that has reactions, and `update_time` on messages that were actually edited. Thread replies expanded via auto-`thread_replies` participate in the same batched enrichment. Pass `--no-reactions` to skip the extra round-trip. Pass `--download-resources` to additionally download message resources (image/file/audio/video/media + post-embedded, excluding stickers) into `./lark-im-resources/` and attach a `resources` block — off by default. See [message enrichment](lark-im-message-enrichment.md) for the full contract.

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 --order asc --page-size 20

# Pagination
lark-cli im +chat-messages-list --chat-id oc_xxx --page-token "xxx"

# Fetch multiple pages automatically, up to 10 pages by default
lark-cli im +chat-messages-list --chat-id oc_xxx --page-all

# JSON output
lark-cli im +chat-messages-list --chat-id oc_xxx --format json
```

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--chat-id <id>` | One of two | Specify the conversation by its chat_id directly (e.g., group chat `oc_xxx`) |
| `--user-id <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 <time>` | No | Start time (ISO 8601 or date only) |
| `--end <time>` | No | End time (ISO 8601 or date only) |
| `--order <order>` | No | Sort order: `asc` / `desc` (default `desc`) |
| `--page-size <n>` | No | Page size (default 50, max 50) |
| `--page-token <token>` | No | Starting cursor, normally returned by a previous response |
| `--page-all` | No | Automatically fetch and merge subsequent pages; capped by `--page-limit` |
| `--page-limit <n>` | No | Maximum pages fetched by `--page-all` (default 10, range 1-1000) |
| `--no-reactions` | No | Skip auto-fetching the `reactions` block |
| `--download-resources` | No | Download message resources (image/file/audio/video/media + post-embedded, excluding stickers) into `./lark-im-resources/` and attach a `resources` block. Off by default; no extra requests when omitted |

> Rule: `--chat-id` and `--user-id` are mutually exclusive. You must provide exactly one of them.

> **CAUTION:** `--order` is the only sort axis — messages are always ordered by creation time, `asc` or `desc`. There is no field axis: the command cannot sort by sender or any other field, so do **not** attempt `--sort sender` or similar (it is rejected). If the user asks to group or sort by sender, fetch with `--order` and aggregate client-side, and tell them this is local post-processing, not a CLI/API sort capability.

## Resource Rendering

Messages are rendered into human-readable text for inspection. Image messages are shown as placeholders such as `![Image](img_xxx)`; files, audio, and videos are rendered with resource keys in the content (e.g. `<audio key="file_xxx" duration="Xs"/>`). By default resource binaries are **not** downloaded.

Two ways to get the binaries:
- **In one pass:** add `--download-resources` to this command — every eligible resource (image/file/audio/video/media + post-embedded, excluding stickers) is downloaded into `./lark-im-resources/` and a `resources` block (`{message_id, key, type, local_path, size_bytes}`) is attached to each message. See [message enrichment](lark-im-message-enrichment.md#resource-auto-download---download-resources-opt-in).
- **One at a time:** use [lark-im-messages-resources-download](lark-im-messages-resources-download.md).

| Resource Type | Marker in Content | Behavior |
|---------|-------------|------|
| Image | `![Image](img_xxx)` | `--download-resources`, or manually `im +messages-resources-download --type image` |
| File | `<file key="file_xxx" .../>` | `--download-resources`, or manually `im +messages-resources-download --type file` |
| Audio | `<audio key="file_xxx" duration="Xs"/>` | `--download-resources`, or manually `im +messages-resources-download --type file` |
| Video | `<video key="file_xxx" .../>` | `--download-resources`, or manually `im +messages-resources-download --type file` |
| Sticker | `[Sticker]` | Not downloadable (Feishu does not support fetching sticker resources) |

## Thread Expansion (`thread_id`)

In JSON output, a message may contain a `thread_id` (`omt_xxx`) field, which means the message has replies in a thread. Use [`im +threads-messages-list`](lark-im-threads-messages-list.md) to inspect replies in that thread:

```bash
lark-cli im +threads-messages-list --thread omt_xxx
```

| Scenario | Recommendation |
|------|------|
| You need context | Call `im +threads-messages-list --order desc --page-size 10` for the discovered thread_id to inspect recent replies |
| The user asks for the "full discussion" | Use `im +threads-messages-list --order asc --page-size 50`, then paginate if needed |
| You only need an overview | Skip thread expansion |

## Output Fields

| Field | Description |
|------|------|
| `messages` | Message array |
| `total` | Number of messages in the current page |
| `has_more` | Whether additional pages are available |
| `page_token` | Pagination token for the next page |

Each message contains:

| Field | Description |
|------|------|
| `message_id` | Message ID |
| `msg_type` | Message type: `text`, `image`, `file`, `interactive`, `post`, `audio`, `video`, `system`, etc. |
| `create_time` | Creation time |
| `sender` | Sender information (includes `name` for user senders) |
| `content` | Message content |
| `deleted` | Whether the message has been recalled (always present, `true` = recalled) |
| `updated` | Whether the message has been edited after sending |
| `mentions` | Array of @mentions in the message; each item contains `{id, key, name}`. Present only when the message contains @mentions |
| `thread_id` | Thread ID (`omt_xxx`) if the message has replies in a thread. Present only when replies exist |

## Pagination (`has_more` / `page_token`)

By default, `im +chat-messages-list` fetches one page. It returns `has_more` and `page_token` when more data is available. Use `--page-token` to continue:

```bash
lark-cli im +chat-messages-list --chat-id oc_xxx --page-token <PAGE_TOKEN>
```

With `--page-all`, `--page-token` sets the starting cursor. If `meta.pagination.complete=false`, resume from `meta.pagination.next_token` or raise `--page-limit`.

You can also fall back to the generic API:

```bash
lark-cli api GET /open-apis/im/v1/messages \
  --params 'container_id_type=chat&container_id=oc_xxx&page_size=50&page_token=<PAGE_TOKEN>'
```

## Common Errors and Troubleshooting

| Symptom | Root Cause | Solution |
|---------|---------|---------|
| `specify --chat-id <chat_id> or --user-id <open_id>` | Neither `--chat-id` nor `--user-id` was provided | You must provide exactly one |
| `--chat-id and --user-id cannot be specified together` | Both parameters were provided | Use only one |
| `--user-id requires user identity (--as user); use --chat-id when calling with bot identity` | `--user-id` was used with bot identity | The p2p resolution endpoint requires user identity. Either pass `--as user` or look up the p2p `chat_id` separately and pass it via `--chat-id` |
| `P2P chat not found for this user` | `--user-id` was used but no p2p chat exists for the current identity and that user | Confirm the target direct-message relationship exists for the current identity |
| `--start: invalid time format` | Invalid time format | Use ISO 8601 or date-only format such as `2026-03-10` |
| Permission denied | Message read permissions are missing | Ensure the app has `im:message:readonly` and `im:chat:read` enabled |

## AI Usage Guidance

1. **Resolving chat_id from a chat name:** When the user refers to a chat by name and you don't have the `chat_id`, use [`+chat-search`](lark-im-chat-search.md) first:
   ```bash
   # Find chat_id by name, then list messages
   lark-cli im +chat-search --query "<chat name keyword>" --format json
   lark-cli im +chat-messages-list --chat-id <chat_id>
   ```
   **Do not use `im chats search` or `+chat-list` — always use the `+chat-search` shortcut.**
2. **Prefer `--chat-id` when available:** if the chat_id is already known, use it directly to avoid extra API calls.
3. **For direct messages:** use `--user-id` to resolve the p2p chat automatically instead of looking it up manually. This requires user identity (`--as user`); with bot identity, resolve the p2p `chat_id` yourself and pass it via `--chat-id`.
4. **For time ranges:** both ISO 8601 and date-only inputs are supported. Date-only is usually simpler.
5. **For full content:** table output truncates content. Use `--format json` when you need the complete message body.
6. **For sender info:** the command already resolves sender names, so you do not need a separate lookup.
7. **Application/bot identity + named group history:** If the user says "使用应用身份/以 bot 身份" and asks to list or read historical messages for a named group, use bot identity for both steps:
   ```bash
   lark-cli im +chat-search --as bot --query "<chat name keyword>" --format json
   lark-cli im +chat-messages-list --as bot --chat-id <chat_id> --page-size 50 --format json
   ```
   If the request is keyword search across message content, `im +messages-search --as bot` is also supported. Continue with `--page-token` if `has_more=true`.

## References

- [lark-im](../SKILL.md) - all IM commands
- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters
references/lark-im-chat-search.md
# im +chat-search

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Search the list of group chats visible to a user or bot, including chats the user or bot belongs to and public chats visible to them. Supports keyword matching on chat names and member names, including pinyin and prefix fuzzy search.

This skill maps to the shortcut: `lark-cli im +chat-search` (internally calls `POST /open-apis/im/v2/chats/search`).

## Commands

```bash
# Search chats by keyword
lark-cli im +chat-search --query "project"

# Restrict by search types
lark-cli im +chat-search --query "project" --search-types "private,public_joined"

# Filter by chat mode (group = regular group, topic = topic/thread group)
lark-cli im +chat-search --query "project" --chat-modes "topic"

# Filter by member open_ids (with keyword)
lark-cli im +chat-search --query "project" --member-ids "ou_xxx,ou_yyy"

# Search by member open_ids only
lark-cli im +chat-search --member-ids "ou_xxx,ou_yyy"

# Only show chats you created or manage
lark-cli im +chat-search --query "project" --is-manager

# Set page size
lark-cli im +chat-search --query "project" --page-size 10

# Pagination
lark-cli im +chat-search --query "project" --page-token "xxx"

# Fetch multiple pages automatically, up to 10 pages by default
lark-cli im +chat-search --query "project" --page-all

# JSON output
lark-cli im +chat-search --query "project" --format json

# Preview the request without executing it
lark-cli im +chat-search --query "project" --dry-run
```

## Parameters

| Parameter | Required | Limits | Description |
|------|------|------|------|
| `--query <keyword>` | No (at least one of `--query` / `--member-ids` required) | Max 64 characters | Search keyword. Supports matching localized chat names, member names, multilingual search, pinyin, and prefix fuzzy search. If the query contains `-`, it is automatically wrapped in quotes |
| `--search-types <types>` | No | Comma-separated: `private`, `external`, `public_joined`, `public_not_joined` | Restrict the visible chat types returned by search |
| `--chat-modes <modes>` | No | Comma-separated: `group`, `topic` | Filter by chat mode (server-side): `group` = regular group, `topic` = topic/thread group |
| `--member-ids <ids>` | No (at least one of `--query` / `--member-ids` required) | Up to 50, format `ou_xxx` | Filter by member open_ids; can be used alone or combined with `--query` |
| `--is-manager` | No | - | Only show chats you created or manage |
| `--disable-search-by-user` | No | - | Disable member-name-based matching and search by group name only |
| `--sort <field>` | No | `create_time`, `update_time`, `member_count` | Sort field (always descending) |
| `--page-size <n>` | No | 1-100, default 20 | Number of results per page |
| `--page-token <token>` | No | - | Starting cursor, normally returned by a previous response |
| `--page-all` | No | - | Automatically fetch and merge subsequent pages; capped by `--page-limit` |
| `--page-limit <n>` | No | 1-1000, default 10 | Maximum pages fetched by `--page-all` |
| `--exclude-muted` | No | User identity only | Drop chats the current user has muted (do-not-disturb). Under `--as bot`, the flag is silently inactive (mute is a per-user setting); see "Filtering muted chats" below |
| `--format json` | No | - | Output as JSON |
| `--dry-run` | No | - | Preview the request without executing it |

> **Note:** Supports both `--as user` (default) and `--as bot`. When using bot identity, the app must have bot capability enabled.

With `--page-all`, `--page-token` sets the starting cursor. If `meta.pagination.complete=false`, resume from `meta.pagination.next_token` or raise `--page-limit`.

> **CAUTION:** `--sort` is **always descending** — the search API only ranks the chosen field high-to-low (e.g. `member_count` = most members first). There is no ascending option. If the user asks for "fewest first / ascending / 从少到多", tell them the search API does not support ascending order; any low-to-high view requires re-sorting the fetched page client-side and is not an upstream sort. Do **not** invent values like `member_count_asc` or pass `asc` (they are rejected).

## Output Fields

| Field | Description |
|------|------|
| `chat_id` | Chat ID (`oc_xxx` format) |
| `name` | Chat name |
| `description` | Chat description |
| `owner_id` | Owner ID |
| `external` | Whether the chat is external |
| `chat_status` | Chat status (`normal` / `dissolved` / `dissolved_save`) |

## Filtering muted chats

`--exclude-muted` (user identity only) drops chats the current user has set to do-not-disturb. After the search call, the CLI batches the page's chat_ids through `POST /open-apis/im/v1/chat_user_setting/batch_get_mute_status` and filters client-side. Under `--as bot`, the mute API is UAT-only and the filter is silently skipped.

When the flag is set, the JSON envelope gains a `filter` sub-object (absent otherwise, so existing consumers are unaffected); `fetched_count == returned_count + filtered_count` always holds:

```json
{
  "chats": [...],
  "filter": {
    "applied": "exclude_muted",
    "fetched_count": 20,
    "returned_count": 19,
    "filtered_count": 1,
    "hint": "Filtered out 1 muted chat(s) on this page (19 remaining, including 2 non-member public group(s)); use --page-token to fetch more."
  }
}
```

Note: only confirmed-muted chats count toward `filtered_count`; non-member public groups are retained and surfaced in `hint`. For strict member-only results, combine with `--search-types "private,public_joined,external"`.

## Usage Scenarios

### Scenario 1: Search chats that contain a keyword

```bash
lark-cli im +chat-search --query "design review"
```

### Scenario 2: Search a chat and list recent messages

```bash
CHAT_ID=$(lark-cli im +chat-search --query "project" --format json | jq -r '.data.chats[0].chat_id')
lark-cli im +chat-messages-list --chat-id "$CHAT_ID"
```

### Scenario 3: Search a chat and send a message

```bash
CHAT_ID=$(lark-cli im +chat-search --query "daily report" --format json | jq -r '.data.chats[0].chat_id')
lark-cli im +messages-send --chat-id "$CHAT_ID" --text "Today's progress update"
```

## Common Errors and Troubleshooting

| Symptom | Root Cause | Solution |
|---------|---------|---------|
| `--query and --member-ids cannot both be empty` | Both were omitted | Provide at least `--query` or `--member-ids` |
| Empty results | No visible chats matched the keyword or filters | Relax the keyword or filters and try again |
| `invalid --page-size 101: must be between 1 and 100` | page-size is out of range | Use an integer between 1 and 100 |
| Permission denied (99991672) | The bot app does not have `im:chat:read` TAT permission enabled | Enable the permission for the app in the Open Platform console |
| Permission denied (99991679) with `--as user` | UAT is not authorized for `im:chat:read` | Run `lark-cli auth login --scope "im:chat:read"` |
| `Bot ability is not activated` (232025) | The app does not have bot capability enabled | Enable bot capability in the Open Platform console |

## AI Usage Guidance

When the user asks to search chats, follow these rules:

1. **At least one filter required:** `--query` and `--member-ids` cannot both be empty. Either alone or combined together are valid.
2. **Search scope is limited:** only chats visible to the current user or bot can be found (joined chats plus public chats). This is not a global search over all chats.
3. **Control result volume:** the result set may be large. Use `--page-size` deliberately.
4. **Suggest follow-up actions:** after finding a chat, common next steps include listing recent messages (`im +chat-messages-list`) or sending a message (`im +messages-send`).
5. **NEVER fall back to chats list:** If `+chat-search` returns empty results, do NOT attempt to use `+chat-list` or `GET /open-apis/im/v1/chats` as a fallback. The list API is not a search API — it returns all chats without keyword filtering and will not help locate the target chat. Instead, ask the user to refine the keyword or check whether the chat is visible to the current identity.

## References

- [lark-im](../SKILL.md) - all IM commands
- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters
references/lark-im-chat-update.md
# im +chat-update

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Update a group's name or description. Supports both **TAT (bot)** and **UAT (user)** identity.

This skill maps to the shortcut: `lark-cli im +chat-update` (internally calls `PUT /open-apis/im/v1/chats/:chat_id`).

## Commands

```bash
# Update the group name
lark-cli im +chat-update --chat-id oc_xxx --name "New Group Name"

# Update the group description
lark-cli im +chat-update --chat-id oc_xxx --description "Updated group description"

# Update multiple fields at once
lark-cli im +chat-update --chat-id oc_xxx \
  --name "Q2 Project Team" \
  --description "Owns Q2 goal tracking"

# Preview the request without executing it
lark-cli im +chat-update --chat-id oc_xxx --name "Test" --dry-run
```

## Parameters

### Required

| Parameter | Description |
|------|------|
| `--chat-id <oc_xxx>` | Group ID |

### Optional Fields

| Parameter | Limits | Description |
|------|------|------|
| `--name <name>` | Max 60 characters | Group name |
| `--description <text>` | Max 100 characters | Group description |

### Global Parameters

| Parameter | Description |
|------|------|
| `--format json` | Output as JSON (default) |
| `--dry-run` | Preview the request without executing it |

## Usage Scenarios

### Scenario 1: Rename a group and update its description

```bash
lark-cli im +chat-update --chat-id oc_xxx \
  --name "Q2 Project Team" \
  --description "Owns Q2 goal tracking"
```

## Common Errors and Troubleshooting

| Symptom | Root Cause | Solution |
|---------|---------|---------|
| `invalid --chat-id: expected chat ID (oc_xxx)` | Invalid chat_id format | Use a valid `oc_xxx` chat ID |
| `--name exceeds the maximum of 60 characters` | Group name too long | Shorten the name to 60 characters or fewer |
| `--description exceeds the maximum of 100 characters` | Group description too long | Shorten the description to 100 characters or fewer |
| `at least one field must be specified to update` | No update field was provided | Specify at least one field to update |
| Permission denied (99991679) | Missing `im:chat:update` permission | Run `lark-cli auth login --scope "im:chat:update"` |
| Non-owner/admin cannot update (232016/232002/232017) | Current identity is not the owner/admin | Try switching identity with `--as bot` or `--as user` |
| Not in the group (232011) | The current user is not a member of the group | Use a member identity (`--as bot`) or join the group first |

## AI Usage Guidance

### Identity Selection

`+chat-update` supports both user and bot identity (`--as user` / `--as bot`).

Infer the group owner from context whenever possible (for example, if a bot just created the group, the owner is the bot) and use the matching identity directly. If ownership is unclear, query the group first and confirm `owner_id`.

Identity choice should follow [Group Chat Identity Rules](lark-im-chat-identity.md): if the user explicitly specifies an identity, use it directly; otherwise infer the owner identity from context.

## References

- [lark-im](../SKILL.md) - all IM commands
- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters
references/lark-im-feed-group-list-item.md
# +feed-group-list-item

> Shortcut for `lark-cli im +feed-group-list-item`. List the feed cards inside one feed group (tag), enriched with a readable `chat_name`.

`+feed-group-list-item` is the only CLI surface for the `feed.groups.list_item` read API — there is no raw `feed.groups list_item` command. It resolves a human-readable `chat_name` for every feed card it returns: a v1 feed card's `feed_id` is always a chat ID (`oc_xxx`), so the shortcut issues a follow-up `POST /open-apis/im/v1/chats/batch_query` and injects `chat_name` into each entry of both `items[]` and `deleted_items[]`.

## Identity

User-only. Run with `--as user`.

## Scopes

Because chat-name resolution always runs, this shortcut needs **two** user scopes unconditionally:

- `im:feed_group_v1:read` — to read the items
- `im:chat:read` — to resolve names

`chat_name` resolution always runs, so there is no single-scope, un-enriched path. For the other raw `feed.groups.*` methods, see [lark-im-feed-groups.md](lark-im-feed-groups.md).

## Usage

```bash
# First page, enriched with chat names
lark-cli im +feed-group-list-item --as user --feed-group-id ofg_xxx

# Auto-paginate through everything within a time window
lark-cli im +feed-group-list-item --as user --feed-group-id ofg_xxx \
  --page-all --start-time 1767196800000 --end-time 1767200000000
```

## Flags

| Flag | Required | Description |
|---|---|---|
| `--feed-group-id` | Yes | Feed group ID (`ofg_xxx`); path parameter |
| `--page-size` | No | Records per page, 1–50 (default 50) |
| `--page-token` | No | Starting cursor, normally returned by a previous response |
| `--page-all` | No | Auto-paginate and merge all pages |
| `--page-limit` | No | Max pages when `--page-all` is set, 1–1000 (default 20) |
| `--start-time` | No | Update-time window start (Unix milliseconds as a decimal string) |
| `--end-time` | No | Update-time window end (Unix milliseconds as a decimal string) |

When `--page-token` and `--page-all` are supplied together, automatic pagination starts at that cursor and continues until exhaustion or `--page-limit`.

## Output

JSON keeps the raw envelope and adds `chat_name` to each resolvable item:

```json
{
  "items": [
    { "feed_id": "oc_abc", "feed_type": "chat", "update_time": "1767196800000", "chat_name": "Release Team" }
  ],
  "deleted_items": [
    { "feed_id": "oc_def", "feed_type": "chat", "update_time": "1767196800000", "chat_name": "Old Channel" }
  ],
  "page_token": "",
  "has_more": false
}
```

A feed card whose chat cannot be resolved (soft-deleted or no permission) simply omits `chat_name` — the command still exits 0. p2p (direct) chats also omit `chat_name`: the server returns an empty `name` for them (the client UI shows the partner's display name instead); if a label is needed, fetch the chat via `chats/batch_query`, read `p2p_target_id`, and resolve it with a contact lookup.

## See also

- [lark-im-feed-groups.md](lark-im-feed-groups.md) — raw `feed.groups.*` APIs, enums, and rule guidance
- [lark-im-feed-group-list.md](lark-im-feed-group-list.md) — list your feed groups
- [lark-im-feed-group-query-item.md](lark-im-feed-group-query-item.md) — look up specific feed cards by ID
references/lark-im-feed-group-list.md
# +feed-group-list

> Shortcut for `lark-cli im +feed-group-list`. List the caller's feed groups (tags) with auto-pagination that correctly merges both the live and soft-deleted lists.

`+feed-group-list` is the only CLI surface for listing feed groups — there is no raw `feed.groups list` command. The list response carries two parallel arrays — `groups` (live) and `deleted_groups` (soft-deleted). The shortcut paginates this dual-list response correctly: its `--page-all` merges **both** arrays across pages (a naive single-array pager would silently drop one list's later pages). It adds no enrichment.

## Identity

User-only. Run with `--as user`.

## Scopes

- `im:feed_group_v1:read`

## Usage

```bash
# First page
lark-cli im +feed-group-list --as user

# Auto-paginate through all your feed groups (both live and deleted)
lark-cli im +feed-group-list --as user --page-all

# Within an update-time window
lark-cli im +feed-group-list --as user --page-all \
  --start-time 1767196800000 --end-time 1767200000000
```

## Flags

| Flag | Required | Description |
|---|---|---|
| `--page-size` | No | Records per page, 1–50 (default 50). Caps the combined `groups` + `deleted_groups` count, so a page may hold fewer live groups than the size suggests |
| `--page-token` | No | Starting cursor, normally returned by a previous response |
| `--page-all` | No | Auto-paginate and merge all pages (both lists) |
| `--page-limit` | No | Max pages when `--page-all` is set, 1–1000 (default 20) |
| `--start-time` | No | Update-time window start (Unix milliseconds as a decimal string) |
| `--end-time` | No | Update-time window end (Unix milliseconds as a decimal string) |

When `--page-token` and `--page-all` are supplied together, automatic pagination starts at that cursor and continues until exhaustion or `--page-limit`.

## Output

JSON keeps the raw envelope; with `--page-all` both lists are returned fully merged:

```json
{
  "groups": [
    { "group_id": "ofg_xxx", "type": "normal", "name": "Releases", "rules": { "rules": [] } }
  ],
  "deleted_groups": [
    { "group_id": "ofg_yyy", "type": "rule", "name": "Old", "rules": { "rules": [] } }
  ],
  "page_token": "",
  "has_more": false
}
```

> `page_size` counts live and deleted groups together, and the per-page count can be smaller still when entries are filtered — so never infer completeness from counts. Pagination is governed solely by `has_more`.

## See also

- [lark-im-feed-groups.md](lark-im-feed-groups.md) — raw `feed.groups.*` APIs, enums, and rule guidance
- [lark-im-feed-group-list-item.md](lark-im-feed-group-list-item.md) — list the feed cards inside one group
- [lark-im-feed-group-query-item.md](lark-im-feed-group-query-item.md) — look up specific feed cards by ID
references/lark-im-feed-group-query-item.md
# +feed-group-query-item

> Shortcut for `lark-cli im +feed-group-query-item`. Look up specific feed cards inside one feed group (tag) by ID, enriched with a readable `chat_name`.

`+feed-group-query-item` is the only CLI surface for the `feed.groups.batch_query_item` read API — there is no raw `feed.groups batch_query_item` command. It resolves a human-readable `chat_name` for every feed card it returns: a v1 feed card's `feed_id` is always a chat ID (`oc_xxx`), so the shortcut issues a follow-up `POST /open-apis/im/v1/chats/batch_query` and injects `chat_name` into each entry of both `items[]` and `deleted_items[]`.

## Identity

User-only. Run with `--as user`.

## Scopes

Because chat-name resolution always runs, this shortcut needs **two** user scopes unconditionally:

- `im:feed_group_v1:read` — to read the items
- `im:chat:read` — to resolve names

`chat_name` resolution always runs, so there is no single-scope, un-enriched path. For the other raw `feed.groups.*` methods, see [lark-im-feed-groups.md](lark-im-feed-groups.md).

## Usage

```bash
lark-cli im +feed-group-query-item --as user \
  --feed-group-id ofg_xxx --feed-id oc_a,oc_b
```

## Flags

| Flag | Required | Description |
|---|---|---|
| `--feed-group-id` | Yes | Feed group ID (`ofg_xxx`); path parameter |
| `--feed-id` | Yes | Comma-separated chat IDs (`oc_xxx`); `feed_type` is fixed to `chat` |

## Output

The command sends `{"items":[{"feed_id":"oc_a","feed_type":"chat"},{"feed_id":"oc_b","feed_type":"chat"}]}`, then enriches the response (`items[]` and `deleted_items[]`) with `chat_name` exactly as `+feed-group-list-item` does. There is no pagination for this method.

A feed card whose chat cannot be resolved (soft-deleted or no permission) simply omits `chat_name` — the command still exits 0. p2p (direct) chats also omit `chat_name`: the server returns an empty `name` for them (the client UI shows the partner's display name instead); if a label is needed, fetch the chat via `chats/batch_query`, read `p2p_target_id`, and resolve it with a contact lookup.

## See also

- [lark-im-feed-groups.md](lark-im-feed-groups.md) — raw `feed.groups.*` APIs, enums, and rule guidance
- [lark-im-feed-group-list.md](lark-im-feed-group-list.md) — list your feed groups
- [lark-im-feed-group-list-item.md](lark-im-feed-group-list-item.md) — list all feed cards in a group (paginated)
references/lark-im-feed-groups.md
# im feed.groups

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

This reference is the shared annotation target for the IM feed-group (tag) APIs: it documents what each method does, the `--params` / `--data` request and response shapes, and the enum surface used in payloads. The full method list is in [Command Overview](#command-overview) below.

> **Important:** The six raw commands (`create`, `update`, `delete`, `batch_query`, `batch_add_item`, `batch_remove_item`) take structured input through `--params '<json>'` and `--data '<json>'` rather than typed flags. The three read methods (`list`, `list_item`, `batch_query_item`) are exposed only as typed `+` shortcut wrappers — see [Shortcuts](#shortcuts). All methods are user-only; see [Common Notes](#common-notes).

> **Picking a read method:** `batch_query` / `+feed-group-query-item` are lightweight ID lookups; `+feed-group-list` / `+feed-group-list-item` paginate the whole set and are much heavier. When you already hold the IDs (`group_id` from `create`, the `feed_id`s you passed to `batch_add_item`), prefer the lightweight lookup. Reserve the list methods for when you actually need to discover IDs you don't have.

## Command Overview

| Method | Purpose |
|---|---|
| `feed.groups.create` | Create a new feed group (tag) |
| `feed.groups.update` | Update a feed group's name and/or rules |
| `feed.groups.delete` | Delete one feed group |
| `feed.groups.batch_query` | Look up feed groups by ID list |
| `feed.groups.list` | List the caller's feed groups with optional time-range filter — **CLI: only via `+feed-group-list` shortcut** |
| `feed.groups.batch_add_item` | Add feed cards (chats) into a feed group |
| `feed.groups.batch_remove_item` | Remove feed cards from a feed group |
| `feed.groups.batch_query_item` | Look up feed cards inside a group by ID list — **CLI: only via `+feed-group-query-item` shortcut** |
| `feed.groups.list_item` | List feed cards inside one feed group — **CLI: only via `+feed-group-list-item` shortcut** |

> HTTP method and path are not duplicated here. For the six raw methods, inspect them with `lark-cli schema im.feed.groups.<method>` when needed; the three shortcut-only read methods (`list`, `list_item`, `batch_query_item`) use typed flags (see their `--help`).

## Shortcuts

Three typed `+` shortcuts cover the feed-group read paths. All are user-only.

| Shortcut | Purpose | Notes |
|---|---|---|
| [`+feed-group-list`](lark-im-feed-group-list.md) | List your feed groups | Its `--page-all` correctly merges the live and soft-deleted lists. No enrichment |
| [`+feed-group-list-item`](lark-im-feed-group-list-item.md) | List the feed cards inside a group | Enriches each card with `chat_name` |
| [`+feed-group-query-item`](lark-im-feed-group-query-item.md) | Look up feed cards in a group by ID | Enriches each card with `chat_name` |

The two `*-item` shortcuts resolve `chat_name` via a follow-up `chats/batch_query`, so they need `im:chat:read` in addition to `im:feed_group_v1:read`; `+feed-group-list` needs only `im:feed_group_v1:read`. All three are the **only** CLI surface for their methods — `list`, `list_item`, and `batch_query_item` have no raw command; full flags and response shapes live in the shortcut docs linked above.

## Common Notes

- `feed_group_id` is the feed-group identifier returned by `create`, typically formatted as `ofg_xxx`. It is an opaque string — the group's stable ID.
- `feed_id` is the identifier of one feed card inside a group. In v1 only the `chat` feed card type is supported (see `feed_card_type` below), so `feed_id` is currently a chat ID such as `oc_xxx`.
- All `feed.groups.*` methods require `user_access_token`. Run with `--as user`; bot/tenant tokens are rejected.
- Read APIs (`batch_query`, `list`, `batch_query_item`, `list_item`) return **two parallel lists**: a live list (`groups[]` or `items[]`) and a soft-deleted list (`deleted_groups[]` or `deleted_items[]`). Consumers tracking incremental sync should consume both.
- Time-range fields (`start_time`, `end_time`, `update_time`) are Unix timestamps **in milliseconds**, encoded as decimal strings (e.g. `1767196800000`).
- Rule-based feed groups (`type=rule`) auto-populate from the rules declared in `feed_group_creator.rules`. Normal feed groups (`type=normal`) are managed explicitly via `batch_add_item` / `batch_remove_item`.

> **Choose the simplest group that fits** — it keeps `create` / `update` fast and predictable. Apply these in order:
> 1. **Prefer `type=normal`.** When the target chats are known up front, set membership explicitly with `batch_add_item` / `batch_remove_item`. Use `type=rule` only when membership must be derived automatically.
> 2. **Keep the rule set smallest.** Use the fewest `rules[]` and `condition_items[]` that express the intent (one condition is ideal). This outranks the style rules below — never split a rule or add conditions just to satisfy them (e.g. one `match_any` rule beats two single-condition rules for "A or B").
> 3. **Within that, make each condition precise.** Prefer positive, specific conditions (`is`, or `contain` with a distinctive keyword) over exclusion (`is_not`, `not_contain`) or broad keywords, which capture more than intended. For a multi-condition rule, prefer `match_all` (narrower) over `match_any` (wider).

## Inspect Schema

```bash
lark-cli schema im.feed.groups
lark-cli schema im.feed.groups.create --format pretty
lark-cli schema im.feed.groups.batch_add_item --format pretty
```

> `list`, `list_item`, and `batch_query_item` have no raw method schema (they are shortcut-only). Inspect their flags with `lark-cli im +feed-group-list --help` / `+feed-group-list-item --help` / `+feed-group-query-item --help` instead.

## create

Create a new feed group. Returns the new `group_id` on success.

> **Prefer `type=normal`.** Use `type=rule` only when membership must be derived automatically, and keep the rule set small and precise — see the guidance under [Common Notes](#common-notes).

```bash
# Normal (empty) group
lark-cli im feed.groups create --as user \
  --data '{"feed_group_creator":{"type":"normal","name":"Releases"}}'

# Rule-based group: auto-add p2p chats with "release" in their name
lark-cli im feed.groups create --as user \
  --data '{
    "feed_group_creator":{
      "type":"rule",
      "name":"Auto: release chats",
      "rules":{
        "rules":[
          {
            "condition":{
              "match_type":"match_all",
              "condition_items":[
                {"type":"chat_type","operator":"is","chat_type":"p2p"},
                {"type":"keyword","operator":"contain","keyword":"release"}
              ]
            },
            "action":"add"
          }
        ]
      }
    }
  }'
```

### Request

#### `--params`

| Parameter | Required | Description |
|---|---|---|
| `user_id_type` | No | ID type used when the request body contains `user_id` references inside rules. One of `open_id`, `union_id`, `user_id` |

#### `--data`

| Field | Required | Description |
|---|---|---|
| `feed_group_creator.type` | Yes | `normal` (empty group) or `rule` (auto-populated by rules) |
| `feed_group_creator.name` | Yes | Display name, e.g. `"标签名称测试"` |
| `feed_group_creator.rules` | No | Rule object (required when `type=rule`). See `feed_group_rules` section below |

### Response

```json
{
  "group_id": "ofg_xxx"
}
```

## update

Update a feed group's name and/or rules. The `update_fields` array tells the server which fields are being updated.

> **Scope each update to what actually changed.** If you only need to rename, pass `update_fields:[1]` so the rules are left untouched. When you do change rules, the same guidance under [Common Notes](#common-notes) applies to the resulting set.

```bash
# Rename only
lark-cli im feed.groups update --as user \
  --params '{"feed_group_id":"ofg_xxx"}' \
  --data '{"feed_group_updater":{"name":"测试标签名称","update_fields":[1]}}'

# Replace rules only (rules array uses the feed_group_rules shape — see that section)
lark-cli im feed.groups update --as user \
  --params '{"feed_group_id":"ofg_xxx"}' \
  --data '{
    "feed_group_updater":{
      "rules":{"rules":[]},
      "update_fields":[2]
    }
  }'
```

### Request

#### `--params`

| Parameter | Required | Description |
|---|---|---|
| `feed_group_id` | Yes | Path parameter — the feed group to update |
| `user_id_type` | No | ID type for any `user_id` fields inside `rules` |

#### `--data`

| Field | Required | Description |
|---|---|---|
| `feed_group_updater.name` | No | New display name |
| `feed_group_updater.rules` | No | Replacement rule object. Same structure as `create.feed_group_creator.rules` |
| `feed_group_updater.update_fields` | No | Array of integer update markers: `1` = name, `2` = rules. Server applies only the listed fields |

### Response

Empty body on success. Inspect the CLI exit code for status.

## delete

Delete one feed group.

```bash
lark-cli im feed.groups delete --as user \
  --params '{"feed_group_id":"ofg_xxx"}'
```

### Request

| Parameter | Required | Description |
|---|---|---|
| `feed_group_id` | Yes | Path parameter — the feed group to delete |

### Response

Empty body on success.

## batch_query

Look up feed groups by an explicit list of IDs. Returns both live and soft-deleted matches.

```bash
lark-cli im feed.groups batch_query --as user \
  --params '{"user_id_type":"open_id"}' \
  --data '{"group_ids":["ofg_xxx","ofg_yyy"]}'
```

### Request

#### `--params`

| Parameter | Required | Description |
|---|---|---|
| `user_id_type` | No | ID type used when the response includes `user_id` references inside `groups[].rules` |

#### `--data`

| Field | Required | Description |
|---|---|---|
| `group_ids` | Yes | Array of feed group IDs to look up |

### Response

```json
{
  "groups": [
    {
      "group_id": "ofg_xxx",
      "type": "normal",
      "name": "test",
      "rules": { "rules": [] }
    }
  ],
  "deleted_groups": [
    {
      "group_id": "ofg_yyy",
      "type": "rule",
      "name": "test",
      "rules": { "rules": [] }
    }
  ]
}
```

Each `rules.rules[]` element follows the `feed_group_rules` shape — see that section for the full structure.

### Top-Level Fields

| Field | Type | Meaning |
|---|---|---|
| `groups` | `array<object>` | Live feed groups for the requested IDs |
| `deleted_groups` | `array<object>` | Soft-deleted matches, returned for incremental-sync clients |

Each element carries `group_id`, `type`, `name`, and (when defined) `rules`.

## list

Shortcut-only: [`+feed-group-list`](lark-im-feed-group-list.md). Lists the caller's feed groups, optionally filtered by an update-time window. Its `--page-all` correctly merges the live (`groups`) and soft-deleted (`deleted_groups`) lists across pages. There is no raw command — flags and response shape are in the linked shortcut doc.

## batch_add_item

Add feed cards (chats) into one feed group. Partial failures are reported in `failed_items`.

```bash
lark-cli im feed.groups batch_add_item --as user \
  --params '{"feed_group_id":"ofg_xxx"}' \
  --data '{
    "items":[
      {"feed_id":"oc_xxx","feed_type":"chat"},
      {"feed_id":"oc_yyy","feed_type":"chat"}
    ]
  }'
```

### Request

| Source | Field | Required | Description |
|---|---|---|---|
| `--params` | `feed_group_id` | Yes | Path parameter — the target feed group |
| `--data` | `items[]` | Yes | Array of feed cards to add |
| `--data` | `items[].feed_id` | No | The chat ID to add (e.g. `oc_xxx`) |
| `--data` | `items[].feed_type` | Yes (`"chat"` only) | Wire-typed as an open string. v1 OAPI service accepts only `chat`; anything else is rejected at runtime. See the Enums section. |

> Note: `items[].feed_id` is not marked as required in the API schema, but every element of `items` must set it — a missing field yields an unusable entry. Always pass `{"feed_id": "oc_xxx", "feed_type": "chat"}` per item.

### Response

```json
{
  "failed_items": [
    {
      "item": { "feed_id": "oc_xxx", "feed_type": "chat" },
      "error_code": 240001,
      "error_message": "feed_id is invalid"
    }
  ]
}
```

| Field | Type | Meaning |
|---|---|---|
| `failed_items` | `array<object>` | Items that failed; absent or empty means all succeeded |
| `failed_items[].item` | `object` | The original `{feed_id, feed_type}` element |
| `failed_items[].error_code` | `integer` | Numeric error code |
| `failed_items[].error_message` | `string` | Human-readable failure reason |

## batch_remove_item

Remove feed cards from one feed group. Same request and response shape as `batch_add_item`.

```bash
lark-cli im feed.groups batch_remove_item --as user \
  --params '{"feed_group_id":"ofg_xxx"}' \
  --data '{
    "items":[
      {"feed_id":"oc_xxx","feed_type":"chat"}
    ]
  }'
```

### Request

| Source | Field | Required | Description |
|---|---|---|---|
| `--params` | `feed_group_id` | Yes | Path parameter — the target feed group |
| `--data` | `items[]` | Yes | Array of feed cards to remove |
| `--data` | `items[].feed_id` | No | The chat ID to remove |
| `--data` | `items[].feed_type` | Yes (`"chat"` only) | Wire-typed as an open string. v1 OAPI service accepts only `chat`; anything else is rejected at runtime. See the Enums section. |

> Note: same caveat as `batch_add_item` — `items[].feed_id` is optional per the API schema but must be present in practice.

### Response

Identical shape to `batch_add_item` — `failed_items[]` lists rows that did not remove cleanly.

## batch_query_item

Shortcut-only: [`+feed-group-query-item`](lark-im-feed-group-query-item.md). Looks up feed cards in a group by an explicit ID list and enriches each with `chat_name`. There is no raw command — flags and response shape are in the linked shortcut doc.

## list_item

Shortcut-only: [`+feed-group-list-item`](lark-im-feed-group-list-item.md). Lists the feed cards inside a group (paginated, `--page-all` supported) and enriches each with `chat_name`. There is no raw command — flags and response shape are in the linked shortcut doc.

## Enums

All enum values listed here are exhaustive.

### `feed_group_type`

Used in `feed_group_creator.type` and the response `groups[].type`.

- `normal` — empty group; members managed explicitly via `batch_add_item` / `batch_remove_item`.
- `rule` — auto-populated; `feed_group_creator.rules` must be supplied.

### `feed_card_type`

Used in `items[].feed_type` everywhere a feed card appears. Wire type is an open string.

- `chat` — the only value the v1 OAPI service accepts. `feed_id` is therefore a chat ID such as `oc_xxx`.

The CLI does not pre-validate this field — passing anything other than `chat` reaches the server and is rejected at runtime. Treat `chat` as effectively required.

### `feed_group_rule_action`

Used inside `feed_group_rules.rules[].action`.

- `add` — when the condition matches, add the matching feed into this group.
- `remove` — when the condition matches, remove the matching feed from this group.

### `feed_group_rule_cond_match_type`

Used inside `feed_group_rules.rules[].condition.match_type`.

- `match_all` — every condition item must match.
- `match_any` — at least one condition item must match.

### `feed_group_rule_cond_item_type`

Used inside `feed_group_rules.rules[].condition.condition_items[].type`. Determines which sibling field of the item is consulted.

- `keyword` — match against a keyword; consult the `keyword` field.
- `chatter` — match against a user; consult the `user_id` field (interpreted per the request's `user_id_type`).
- `chat_type` — match against a chat type; consult the `chat_type` field.

### `feed_group_rule_cond_item_operator`

Used inside `feed_group_rules.rules[].condition.condition_items[].operator`. Typically paired with the relevant `type`:

- `contain` — substring match; typically paired with `keyword`.
- `not_contain` — substring non-match; typically paired with `keyword`.
- `is` — equality; typically paired with `chatter` or `chat_type`.
- `is_not` — non-equality; typically paired with `chatter` or `chat_type`.

### `feed_group_rule_cond_item_chat_type`

Used inside `feed_group_rules.rules[].condition.condition_items[].chat_type` when `type=chat_type`.

- `p2p`
- `group`
- `thread_group`
- `helpdesk`
- `bot`
- `mute`
- `flag`
- `cross_tenant`
- `any`

### `update_fields`

Used inside `feed_group_updater.update_fields`. Multiple values may be listed.

- `1` — update name only.
- `2` — update rules only.

Wire form: integers (`1` = name, `2` = rules). The server rejects the lowercase string forms (`"name"`, `"rules"`) with `9499 Invalid parameter value`. Omit the array (or pass an empty array) to make no field updates.

## feed_group_rules

The same nested object is used in `feed_group_creator.rules` (create), `feed_group_updater.rules` (update), and in read responses under `groups[].rules`. Shape:

```json
{
  "rules": [
    {
      "condition": {
        "match_type": "match_all",
        "condition_items": [
          { "type": "chat_type", "operator": "is", "chat_type": "group" },
          { "type": "keyword",   "operator": "contain", "keyword": "release" }
        ]
      },
      "action": "add"
    }
  ]
}
```

Per-`type` required-field legend:

- `type=keyword` → `keyword` is required; `user_id` and `chat_type` are ignored.
- `type=chatter` → `user_id` is required; the request's `user_id_type` query parameter tells the server how to interpret it.
- `type=chat_type` → `chat_type` is required.

## Permissions

| Method | Scope |
|---|---|
| `feed.groups.create` | `im:feed_group_v1:write` |
| `feed.groups.update` | `im:feed_group_v1:write` |
| `feed.groups.delete` | `im:feed_group_v1:write` |
| `feed.groups.batch_query` | `im:feed_group_v1:read` |
| `feed.groups.batch_add_item` | `im:feed_group_v1:write` |
| `feed.groups.batch_remove_item` | `im:feed_group_v1:write` |

The three read methods are shortcut-only:

- [`+feed-group-list`](lark-im-feed-group-list.md) — `im:feed_group_v1:read`
- [`+feed-group-list-item`](lark-im-feed-group-list-item.md) / [`+feed-group-query-item`](lark-im-feed-group-query-item.md) — `im:feed_group_v1:read` **plus** `im:chat:read` (they always resolve `chat_name`)

If a required scope is missing, the CLI surfaces a hint such as `lark-cli auth login --scope "im:feed_group_v1:write"`.

## References

- [lark-im](../SKILL.md) — all IM commands
- [lark-shared](../../lark-shared/SKILL.md) — authentication and global parameters
references/lark-im-feed-shortcut-create.md
# im +feed-shortcut-create

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) for authentication, global parameters, and security rules.

This skill maps to shortcut: `lark-cli im +feed-shortcut-create`. Underlying API: `POST /open-apis/im/v2/feed_shortcuts`.

## What it does

Adds one or more chats to the **current user's** feed shortcuts — equivalent to right-clicking a chat in the Feishu client and pinning it to the feed sidebar.

- Only **CHAT-type** shortcuts are exposed by the OpenAPI gateway right now (`feed_card_id` must be an `oc_xxx` open_chat_id).
- Batch up to **10 chat IDs per call**; pass more by issuing multiple calls.
- Currently only supports **user identity** (`--as user`); bot identity is not allowed by the server.
- If you only know a group name, resolve its `oc_xxx` first with `im +chat-search` or `im +chat-list`.

## Commands

```bash
# Add a single chat as a feed shortcut (defaults to head/top insertion)
lark-cli im +feed-shortcut-create --as user --chat-id oc_xxx

# Add multiple chats; comma-separated or repeated flag both work
lark-cli im +feed-shortcut-create --as user --chat-id oc_a,oc_b,oc_c
lark-cli im +feed-shortcut-create --as user --chat-id oc_a --chat-id oc_b

# Append at the bottom of the shortcut list instead of the top
lark-cli im +feed-shortcut-create --as user --chat-id oc_xxx --tail

# Preview the request without sending
lark-cli im +feed-shortcut-create --as user --chat-id oc_xxx --dry-run
```

## Parameters

| Parameter | Default | Description |
|------|------|------|
| `--chat-id <oc_xxx>` | required | open_chat_id to add as a feed shortcut; repeatable or comma-separated; **max 10 per call** |
| `--head` | true (implied) | Insert at the top of the shortcut list; mutually exclusive with `--tail` |
| `--tail` | false | Append at the bottom of the shortcut list |
| `--as user` | required | Server only accepts user_access_token for this API |

## Response

The response is a batch ledger. A full success exits `0` with `ok:true`. Any non-empty `failed_shortcuts` is a partial failure: the process exits non-zero (currently exit `1`), stdout carries `ok:false`, and the full ledger remains machine-readable:

| Field | Meaning |
|------|------|
| `total` | Number of requested shortcuts |
| `success_count` | Number of requested shortcuts not reported in `failed_shortcuts` |
| `failure_count` | Number of requested shortcuts reported as failed; `failed_shortcuts` preserves the raw server failure list |
| `succeeded_shortcuts` | Requested shortcut entries that succeeded |
| `failed_shortcuts` | Per-item failures returned by the server, enriched with `reason_label` |

The shortcut adds a `reason_label` field next to each numeric `reason`:

| `reason` | `reason_label` | Meaning |
|---:|------|------|
| 1 | `no_permission` | User has no permission on the feed card |
| 2 | `invalid_item` | `feed_card_id` is invalid or type doesn't match |
| 3 | `has_pending_delete` | The chat is being deleted |
| 4 | `type_not_support` | Type is not whitelisted (only CHAT is open now) |
| 5 | `internal_error` | Server internal error |

Example:

```json
{
  "ok": false,
  "data": {
    "total": 2,
    "success_count": 1,
    "failure_count": 1,
    "succeeded_shortcuts": [
      { "feed_card_id": "oc_good", "type": 1 }
    ],
    "failed_shortcuts": [
      {
        "shortcut": { "feed_card_id": "oc_bad", "type": 1 },
        "reason": 2,
        "reason_label": "invalid_item"
      }
    ]
  }
}
```

## Permissions

- Required scope: `im:feed.shortcut:write`
- Only available with user identity (`--as user`). The CLI will reject `--as bot` for this shortcut.

## Note

- The shortcut list is **per user**: the call adds shortcuts for the currently authenticated user only.
- Adding the same chat twice is **idempotent at the user level** (re-adding an existing shortcut is a no-op rather than an error).
- Scripts should check the process exit code, top-level `ok`, and ledger counts. Partial failures intentionally keep machine-readable success and failure details on stdout.
- To inspect the current shortcut list, use [`+feed-shortcut-list`](lark-im-feed-shortcut-list.md). To remove a shortcut, use [`+feed-shortcut-remove`](lark-im-feed-shortcut-remove.md).
references/lark-im-feed-shortcut-list.md
# im +feed-shortcut-list

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) for authentication, global parameters, and security rules.

This skill maps to shortcut: `lark-cli im +feed-shortcut-list`. Underlying API: `GET /open-apis/im/v2/feed_shortcuts`.

## What it does

Lists **one page** of the **current user's** feed shortcuts.

- Only **CHAT-type** shortcuts are exposed via OpenAPI today (others in the IDL are not yet whitelisted).
- The shortcut is a **thin one-page wrapper** — there is no built-in auto-pagination. Callers drive their own loop when they actually need to paginate.
- Server-side page size is controlled by the service, so this command has no `--page-size` flag; in normal use one page usually covers the list.
- Pagination tokens are opaque. If a token is rejected because the shortcut list changed, restart by omitting `--page-token`.

## Commands

```bash
# First page (the only call most users ever need — --page-token omitted)
lark-cli im +feed-shortcut-list --as user

# Continue from the previous response's page_token
lark-cli im +feed-shortcut-list --as user --page-token <token-from-previous-response>

# Skip detail enrichment when only IDs are needed; avoids the extra im:chat:read lookup
lark-cli im +feed-shortcut-list --as user --no-detail -q '.data.shortcuts[].feed_card_id'
```

> If you need to walk every page, write the loop yourself: read `data.page_token` from each response and pass it back in until `has_more=false`. The shortcut intentionally does not auto-walk because page-token errors require the caller to decide whether to restart from the first page.

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--page-token <token>` | no | Opaque pagination token from the previous response. **Omit it for the first page.** |
| `--no-detail` | no (default `false`) | Skip fetching each entry's full info object. By default enrichment is enabled: CHAT-type entries call `im.chats.batch_query`, need `im:chat:read`, and attach the object under the `detail` field. Pass `--no-detail` to skip the extra call and scope. |
| `--as user` | yes | Server only accepts user_access_token for this API |

## Response Structure

| Field | Type | Description |
|------|------|------|
| `shortcuts` | array | Feed shortcut entries; each has `feed_card_id` (oc_xxx) and `type` (1=CHAT). By default (without `--no-detail`), each entry also has a `detail` field with the full per-type info object. |
| `has_more` | boolean | Whether more pages exist |
| `page_token` | string | Opaque token to pass to the next call when continuing pagination |

Example (with detail enrichment, CHAT type):

```json
{
  "data": {
    "shortcuts": [
      {
        "feed_card_id": "oc_092f0100fe59c35995727db1039777a8",
        "type": 1,
        "detail": {
          "chat_id": "oc_092f0100fe59c35995727db1039777a8",
          "chat_mode": "group",
          "name": "Engineering",
          "avatar": "https://...",
          "description": "",
          "external": false,
          "owner_id": "ou_xxx",
          "owner_id_type": "open_id",
          "tenant_key": "..."
        }
      },
      {
        "feed_card_id": "oc_c82061d126a06635aa3569587b134bb1",
        "type": 1,
        "detail": {
          "chat_id": "oc_c82061d126a06635aa3569587b134bb1",
          "chat_mode": "p2p",
          "name": "",
          "p2p_target_id": "ou_xxx",
          "p2p_target_type": "user",
          "avatar": "",
          "description": "",
          "external": false,
          "tenant_key": "..."
        }
      }
    ],
    "has_more": false,
    "page_token": "v1.example-opaque-token"
  }
}
```

## Detail Enrichment

The `detail` payload is dispatched **per `type`**. Today only CHAT is wired in; future shortcut types can attach different object shapes. Callers should `switch` on `type` before parsing `detail`. For CHAT (`type=1`):

- **Source**: `POST /open-apis/im/v1/chats/batch_query` (50 ids per call, server limit).
- **Payload**: the **full chat object** is passed through verbatim — `chat_id`, `chat_mode` (`group` / `p2p` / `topic`), `name`, `avatar`, `description`, `external`, `tenant_key`, plus type-specific fields (`owner_id*` for groups, `p2p_target_*` for p2p).
- **P2P chats** return an empty `name` because the Feishu client renders the partner's display name there. The rest of the object (especially `p2p_target_id`) still flows through, so callers can resolve the partner via `+contact-search` if a display title is needed.
- **Lookup failure** (missing scope, network error) → the list still returns successfully; a warning is printed to stderr, the data payload carries a `_notice` field (`"detail enrichment skipped: ..."`), and affected entries simply lack the `detail` field. Check `_notice` to tell "enrichment skipped" from "nothing to enrich".

## Permissions

- Required scope: `im:feed.shortcut:read`
- Conditional scope (default detail path only): `im:chat:read`; pass `--no-detail` to avoid this extra scope and lookup.
- Only available with user identity (`--as user`).
references/lark-im-feed-shortcut-remove.md
# im +feed-shortcut-remove

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) for authentication, global parameters, and security rules.

This skill maps to shortcut: `lark-cli im +feed-shortcut-remove`. Underlying API: `POST /open-apis/im/v2/feed_shortcuts/remove`.

## What it does

Removes one or more chats from the **current user's** feed shortcuts.

- Only **CHAT-type** shortcuts are supported (`feed_card_id` must be an `oc_xxx`).
- Batch up to **10 chat IDs per call**.
- Currently only supports **user identity** (`--as user`).
- Removing a chat that is not currently in the shortcut list is idempotent success: the call returns `ok:true`, `failure_count=0`, and no `failed_shortcuts` entry for that chat.

## Commands

```bash
# Remove a single feed shortcut
lark-cli im +feed-shortcut-remove --as user --chat-id oc_xxx

# Remove multiple feed shortcuts in one call
lark-cli im +feed-shortcut-remove --as user --chat-id oc_a,oc_b
lark-cli im +feed-shortcut-remove --as user --chat-id oc_a --chat-id oc_b

# Preview the request
lark-cli im +feed-shortcut-remove --as user --chat-id oc_xxx --dry-run
```

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--chat-id <oc_xxx>` | yes | open_chat_id to remove from feed shortcuts; repeatable or comma-separated; max 10 per call |
| `--as user` | yes | Server only accepts user_access_token for this API |

## Response

The response uses the same batch ledger as [`+feed-shortcut-create`](lark-im-feed-shortcut-create.md#response): `total`, `success_count`, `failure_count`, `succeeded_shortcuts`, and `failed_shortcuts`. A non-empty `failed_shortcuts` is a partial failure: stdout carries `ok:false` with the full ledger and the process exits non-zero (currently exit `1`).

## Permissions

- Required scope: `im:feed.shortcut:write`
- Only available with user identity (`--as user`).

## Note

- To see what is currently in the shortcut list before removing, run [`+feed-shortcut-list`](lark-im-feed-shortcut-list.md). Use `--no-detail` when you only need the `feed_card_id` values.
references/lark-im-flag-cancel.md
# im +flag-cancel

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) for authentication, global parameters, and security rules.

This skill maps to shortcut: `lark-cli im +flag-cancel`. Underlying API: `POST /open-apis/im/v1/flags/cancel`.

## Double-Cancel Behavior (Important)

A message can have flags on both layers simultaneously:
- Message layer: `(default, message)`
- Feed layer: `(thread, feed)` or `(msg_thread, feed)` depending on chat type

**When no `--flag-type` is specified, the shortcut performs best-effort double-cancel**: the message-layer flag is always removed; the feed-layer flag is also removed when the chat type can be determined (otherwise a warning is printed on stderr and the feed layer is skipped). The server handles cancel requests for non-existent flags idempotently, so this is safe.

**Feed layer item_type is determined by chat_mode**:
- Topic-style chat (`chat_mode=topic`) → `item_type=thread`
- Regular chat (`chat_mode=group`) → `item_type=msg_thread`

## Commands

```bash
# Double-cancel both layers (recommended default)
lark-cli im +flag-cancel --as user --message-id om_xxx

# Only cancel message layer
lark-cli im +flag-cancel --as user --message-id om_xxx --flag-type message

# Only cancel feed layer (need to specify item-type)
lark-cli im +flag-cancel --as user --message-id om_xxx --item-type thread --flag-type feed

# Preview request
lark-cli im +flag-cancel --as user --message-id om_xxx --dry-run
```

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--message-id <om_xxx>` | Required | Message ID |
| `--flag-type <name>` | No | `message` or `feed`; **when omitted, best-effort double-cancel of both layers** |
| `--item-type <name>` | No | `default\|thread\|msg_thread`; required when `--flag-type feed` |
| `--as user` | Required | Currently only supports user identity |

## Idempotency

The server doesn't return an error for cancel requests when the flag doesn't exist, so repeated `+cancel` calls are idempotent.

## Permissions

- Required scopes: `im:feed.flag:write`, `im:message.group_msg:get_as_user`, `im:message.p2p_msg:get_as_user`, `im:chat:read`
- The message/chat read scopes are used by the default double-cancel path to auto-detect the feed-layer item type.

## Note

- **Do not call +flag-list for verification**: If the cancel API returns success, the flag is removed. Calling +flag-list to verify is expensive (requires full pagination) and unnecessary.

## Finding Message ID Efficiently

If you have message content but not the message ID:

1. **Use `+messages-search`** to find the message by content, then extract `message_id` from the result
2. **Do NOT use `+flag-list`** to find the message — it requires full pagination and is very inefficient

```bash
# Search by message content to find message_id
lark-cli im +messages-search --as user --query "message content here" -q '.data.items[0].message_id'
```
references/lark-im-flag-create.md
# im +flag-create

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) for authentication, global parameters, and security rules.

This skill maps to shortcut: `lark-cli im +flag-create`. Underlying API: `POST /open-apis/im/v1/flags`.

## Default Behavior

- **Message-layer flag** (default): `item_type=default, flag_type=message`
- **Feed-layer flag**: Use `--flag-type feed` — automatically detects chat type to determine `item_type`:
  - Topic-style chat (`chat_mode=topic`) → `item_type=thread`
  - Regular chat (`chat_mode=group`) → `item_type=msg_thread`

## Commands

```bash
# Flag a message (default: message-layer)
lark-cli im +flag-create --as user --message-id om_xxx

# Create feed-layer flag (auto-detects chat type)
lark-cli im +flag-create --as user --message-id om_xxx --flag-type feed

# Explicit item-type override (rarely needed)
lark-cli im +flag-create --as user --message-id om_xxx --item-type thread --flag-type feed

# Preview request (dry-run, doesn't send)
lark-cli im +flag-create --as user --message-id om_xxx --dry-run
```

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--message-id <om_xxx>` | Required | Message ID |
| `--flag-type <name>` | No | `message` (default) or `feed` |
| `--item-type <name>` | No | Override auto-detection: `default\|thread\|msg_thread` (rarely needed) |
| `--as user` | Required | Currently only supports user identity |

## Valid Combinations

The server only accepts these `(item_type, flag_type)` pairs:

- `(default, message)` — regular message flag
- `(thread, feed)` — feed flag in topic-style chat
- `(msg_thread, feed)` — feed flag in regular chat

## Permissions

- Required scopes: `im:feed.flag:write`, `im:message.group_msg:get_as_user`, `im:message.p2p_msg:get_as_user`, `im:chat:read`
- The message/chat read scopes are used when `--flag-type feed` is used without explicit `--item-type` so the CLI can auto-detect chat type.
- If missing, CLI will prompt with `lark-cli auth login --scope "..."`

## Note

- **Do not call +flag-list for verification**: If the create API returns success, the flag is created. Calling +flag-list to verify is expensive (requires full pagination) and unnecessary.

## Finding Message ID Efficiently

If you have message content but not the message ID:

1. **Use `+messages-search`** to find the message by content, then extract `message_id` from the result
2. **Do NOT use `+flag-list`** to find the message — it requires full pagination and is very inefficient

```bash
# Search by message content to find message_id
lark-cli im +messages-search --as user --query "message content here" -q '.data.items[0].message_id'
```
references/lark-im-flag-list.md
# im +flag-list

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) for authentication, global parameters, and security rules.

This skill maps to shortcut: `lark-cli im +flag-list`. Underlying API: `GET /open-apis/im/v1/flags`.

## Sorting Rules (Important)

The API returns data sorted by `update_time` in **ascending order**, meaning **oldest first, newest last**. When `has_more=true`, continue pagination until `has_more=false`; only then is the last item in the merged result authoritative as the newest flag. If pagination stops while `has_more=true`, the last item is only the newest observed flag.

`--page-all` enables automatic pagination but is still capped by `--page-limit`. When `--page-token` is also supplied, it sets the starting cursor and pagination continues from there. The default cap is 20 pages; **20 is not the hard maximum**. Set `--page-limit` between 1 and 1000 when a larger scan is required. A response with `has_more=true` is incomplete, even when `flag_items` is empty; increase the limit or resume from the returned `page_token` before reporting an authoritative latest item or count.

## Commands

```bash
# Fetch first page (default page-size=50)
lark-cli im +flag-list --as user

# Manual pagination with custom page size
lark-cli im +flag-list --as user --page-size 30 --page-token <page_token>

# Auto-paginate, capped at the default 20 pages
lark-cli im +flag-list --as user --page-all

# Auto-paginate + get the latest flag
lark-cli im +flag-list --as user --page-all -q '.data.flag_items[-1]'

# Auto-paginate + get only item_id list
lark-cli im +flag-list --as user --page-all -q '.data.flag_items[].item_id'

# Disable auto-enrichment of message content (enabled by default)
lark-cli im +flag-list --as user --page-all --enrich-feed-thread=false

# Use the largest supported page limit for a broader scan
lark-cli im +flag-list --as user --page-all --page-limit 1000
```

## Parameters

| Parameter | Default | Description |
|------|------|------|
| `--page-size <n>` | 50 | Range 1-50 (server max is 50) |
| `--page-token <token>` | empty | Starting cursor from a previous response; an empty cursor still selects the first page |
| `--page-all` | false | Auto-paginate and merge results, capped by `--page-limit` |
| `--page-limit <n>` | 20 | Max pages in `--page-all` mode; configurable range 1-1000 (20 is only the default) |
| `--enrich-feed-thread` | true | Auto-enrich feed-layer thread entries with message content (calls `im.messages.mget`) |
| `--as user` | Required | Currently only supports user identity |

## Response Structure

The response has `data` as the main body, with fields described below:

| Field | Type | Description |
|------|------|------|
| `flag_items` | array | List of currently existing (not canceled) flags, sorted by `update_time` ascending |
| `delete_flag_items` | array | List of previously canceled flags, sorted by `update_time` ascending |
| `messages` | array | Message content inlined by the server for `(default, message)` type flags |
| `has_more` | boolean | Whether there's a next page |
| `page_token` | string | Pagination token for the next page |

Note: `(thread, feed)` / `(msg_thread, feed)` entries are automatically enriched via `mget` by the shortcut, and written to the corresponding entry's `message` field.

## Limitations

- **Auto-pagination is bounded**: `--page-all` fetches at most 20 pages by default. If the response still has `has_more=true`, the result is incomplete; increase `--page-limit` up to 1000 or resume with `page_token`. Never interpret `flag_items: []` as an authoritative zero while more pages remain. Historical `delete_flag_items` may occupy early pages and push active flags to later pages.
- **delete_flag_items are not enriched**: Message content is only fetched for active flags (`flag_items`), not canceled flags (`delete_flag_items`). If you need message content for a canceled flag, query the message separately using `+messages-mget --message-ids <item_id>`.

## Response Example (Sanitized)

```json
{
  "data": {
    "delete_flag_items": [
      {
        "create_time": "xxx",
        "flag_type": "xxx",
        "item_id": "xxx",
        "item_type": "xxx",
        "update_time": "xxx"
      }
    ],
    "flag_items": [
      {
        "create_time": "xxx",
        "flag_type": "xxx",
        "item_id": "xxx",
        "item_type": "xxx",
        "update_time": "xxx"
      }
    ],
    "has_more": false,
    "messages": [],
    "page_token": "xxx"
  }
}
```

## Permissions

- Base scope: `im:feed.flag:read`
- Additional scopes only when `--enrich-feed-thread=true` needs to fetch missing message content: `im:message.group_msg:get_as_user`, `im:message.p2p_msg:get_as_user`
references/lark-im-message-enrichment.md
# im default message enrichment (reactions / update_time)

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

This is the single source of truth for the automatic message-enrichment contract shared by the four message-pulling shortcuts — [`+messages-mget`](lark-im-messages-mget.md), [`+chat-messages-list`](lark-im-chat-messages-list.md), [`+messages-search`](lark-im-messages-search.md), [`+threads-messages-list`](lark-im-threads-messages-list.md). They automatically attach `reactions` and `update_time` to each returned message, so callers do **not** need to invoke the raw [`im.reactions.batch_query`](lark-im-reactions.md) API separately.

- **`reactions`** — populated from `im.reactions.batch_query` as `{counts, details}`. The field is only attached when the server actually returns data; messages with no reactions omit it. Replies inside `thread_replies` are enriched alongside their parent (collected into the same id set), so outer and inner messages follow identical semantics. The id set is split into batches of <= 20 (server-side cap) and the batches are dispatched with bounded concurrency (up to 4 in flight), so high-N pulls — e.g. page 50 + ~500 expanded thread replies = 550 ids → ⌈550 / 20⌉ = **28 batches** — finish in a few round-trips instead of serializing into tens of seconds.
- **`update_time`** — emitted only when `updated == true` (message was actually edited). The server echoes `update_time == create_time` for unedited messages too, but the CLI gates that output away so consumers don't misread every message as "edited".
- **Opt-out** — each shortcut accepts `--no-reactions` to skip the extra round-trip when the caller only needs message bodies.

## Thread replies expansion

`+messages-mget` and `+chat-messages-list` also auto-expand thread replies: any returned message that carries a `thread_id` triggers a fetch of that thread's replies, which are attached as a `thread_replies` array on the host. Fetches across distinct threads run with bounded concurrency (up to 4 in flight). Two caps gate the result:

- **`perThread` (default 50)** — max replies fetched for any single thread.
- **`totalLimit` (default 500)** — max cumulative replies across all threads on the page.

`totalLimit` is enforced **post-fetch against actual returned reply counts**, not against the planned per-thread ceiling — so a chat with many short threads (e.g. 12 threads × 3 actual replies = 36 ≪ 500) attaches every thread, even though the planned sum (12 × 50 = 600) would exceed the budget. When a thread's actual replies push the running total across `totalLimit`, that thread is truncated to fit the remaining budget and its host is flagged with `thread_has_more: true` so consumers know the server has more.

On per-thread fetch failure the host gets `thread_replies_error: true` (mirrors the reactions data contract); budget-truncated or budget-skipped threads do NOT carry that flag.

## Resource auto-download (`--download-resources`, opt-in)

`+chat-messages-list`, `+messages-mget`, and `+threads-messages-list` accept an **opt-in** `--download-resources` flag. It is **off by default** — when omitted, output and the request count are identical to before (no `resources` block, no extra round-trips).

When enabled:

- Each message that carries downloadable resources gets a `resources` array. Eligible types: `image`, `file`, `audio`, `video`, `media`, and post-embedded `img` / `media`. **Stickers are excluded** (Feishu does not support fetching sticker resources).
- Each ref is `{message_id, key, type, local_path, size_bytes}` — `type` is `image` or `file`; `message_id` is the id used to fetch the resource. For a standalone message that is its own id; for a resource inside a **merge_forward** it is the **top-level container** `message_id`, not the sub-item's own id (the download endpoint rejects sub-item ids with `234003 File not in msg` and can only fetch a forwarded resource through the container). Thread replies each get their own block.
- Files download into `./lark-im-resources/` under the current working directory. Each distinct `(message_id, file_key)` is downloaded once (deduped) with bounded concurrency (up to 3 in flight).
- **Fail-silent isolation**: a single resource that fails to download is flagged `"error": true` with one stderr line (`warning: resource_download_failed: <message_id>/<key>: ...`); the main message and the other resources are unaffected.
- Output paths are confined to `./lark-im-resources/` by the same guards as [`+messages-resources-download`](lark-im-messages-resources-download.md) (abnormal `file_key` with path separators / `..` / absolute paths is rejected).
- **Scope**: the download uses `GET /open-apis/im/v1/messages/:message_id/resources/:file_key`, which requires `im:message:readonly` — already declared in each listing command's `Scopes`, so `--download-resources` needs **no extra scope** beyond what's required to read the messages (user identity also needs `im:message.group_msg:get_as_user` / `im:message.p2p_msg:get_as_user`; bot identity needs `im:message.group_msg` / `im:message.p2p_msg:readonly`, all already declared). Works under both user and bot identity. If a bot was registered before `im:message:readonly` was granted, a single resource will fail-silently (`error: true` + stderr warning) rather than aborting the pull.

Use `--download-resources` when you want the binaries on disk in one pass; otherwise the message content keeps the inline resource markers (e.g. `![Image](img_xxx)`, `<file .../>`, `<audio key="..." duration="Xs"/>`) and you can fetch individual resources later with [`+messages-resources-download`](lark-im-messages-resources-download.md).

## Scope requirement

The default enrichment requires `im:message.reactions:read`, already declared in each shortcut's `UserScopes` / `BotScopes` (or `Scopes` for the search command), so the framework's pre-flight check surfaces a `missing_scope` error before the request is sent. Bots that were registered before this scope was added need an incremental authorization in the Feishu developer console; users can run:

```bash
lark-cli auth login --scope "im:message.reactions:read"
```

## Data contract — missing field ≠ fetch failure

| Situation | Output |
|---|---|
| Message has no reactions | `reactions` field is omitted (not `{}`, not an empty list) |
| Message was never edited | `update_time` field is omitted |
| Whole batch failed | Messages in that batch carry no `reactions`; one line on stderr: `warning: reactions_batch_query_failed: ...` |
| Some message IDs failed | Failed IDs go to stderr: `warning: reactions_partial_failed: N message(s) failed (...)` |

When deciding "has the user already reacted?", branch on the **presence of the `reactions` field plus its `counts` contents**, not on whether a value is `null` — the field's absence means "no data attached" (which usually means "no reactions exist"), not "fetch failed".
references/lark-im-message-read-status.md
# IM message read status

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first for authentication and global parameters.

Use two focused shortcuts for message read-status queries:

- `im +messages-read-status` queries whether the current user has read 1–50 messages.
- `im +message-read-users` lists users who have read one message and supports automatic pagination.

Both underlying OpenAPIs support user identity through a user access token (UAT). `+message-read-users` additionally supports bot identity through a tenant access token (TAT).

## Identity and scopes

| Shortcut | Identity | Scope |
|---|---|---|
| `+messages-read-status` | user only | `im:message:readonly` (recommended), `im:message`, or `im:message:get_as_user` |
| `+message-read-users` | user | `im:message:readonly` (recommended), `im:message`, `im:message:basic`, or `im:message:get_as_user` |
| `+message-read-users` | bot | `im:message:readonly` |

For `+message-read-users`, the caller must still be in the chat. A user can query only messages they sent within the last seven days, while a bot can query only messages sent by that bot within the last seven days.
The user scopes in the table are alternatives; the CLI preflight uses `im:message:readonly` because it is the least-privileged regular OAuth scope supported by this endpoint.

## Batch query the current user's read status

```bash
# Preview one request
lark-cli im +messages-read-status \
  --message-ids om_xxx,om_yyy \
  --as user \
  --dry-run

# Execute with a user access token
lark-cli im +messages-read-status \
  --message-ids om_xxx,om_yyy \
  --as user \
  --json
```

The command accepts 1–50 comma-separated `om_` message IDs. The three scopes above are alternatives; any one is sufficient, and the CLI recommends the least-privileged OAuth scope `im:message:readonly`. The response keeps the OpenAPI response unchanged:

- `items[].message_id` and `items[].is_read` contain statuses the server could determine.
- `invalid_message_ids` contains messages that do not exist, are not visible to the current user, or do not support this query. The API deliberately does not expose a more specific reason.

## List users who read one message

```bash
# Fetch one page as the current user
lark-cli im +message-read-users \
  --message-id om_xxx \
  --as user \
  --json

# Fetch every page as a bot, bounded to ten pages by default
lark-cli im +message-read-users \
  --message-id om_xxx \
  --user-id-type open_id \
  --page-all \
  --as bot \
  --json
```

Pagination flags:

- `--page-size`: 1–100, default 100.
- `--page-token`: start from a known cursor.
- `--page-all`: continue until the endpoint is exhausted.
- `--page-limit`: maximum pages with `--page-all`; default 10, range 1–1000.
- `--page-delay`: delay in milliseconds between pages; default 200, and 0 disables the delay.

The command preserves each server item, including `user_id_type`, `user_id`, `timestamp`, and `tenant_key`. Pagination metadata reports whether the endpoint was exhausted and retains the next token when a bounded run can be resumed.

## Raw API commands

When Registry MR !128 is published, the corresponding raw commands remain available:

```bash
lark-cli im messages read_status --data '{"message_ids":["om_xxx"]}' --as user
lark-cli im messages read_users --params '{"message_id":"om_xxx","user_id_type":"open_id"}' --as user
```

Prefer the shortcuts for flag validation, identity-specific scope hints, and read-users auto-pagination.

## Troubleshooting

| Symptom | Meaning | Action |
|---|---|---|
| `--as bot is not supported` for read status | The batch endpoint requires user identity | Switch to `--as user` |
| Missing `im:message:readonly` or `im:message` | A regular OAuth scope has not been granted | Follow the CLI authorization hint to grant one supported scope |
| Missing a user read scope | No supported regular OAuth scope has been granted | Grant `im:message:readonly` and retry |
| Bot permission denied | The application lacks a bot scope | Open the `console_url` from the typed error and enable the requested scope |
| Empty read-user list | No user has read the message, or sender/time constraints are not met | Verify chat membership, the message sender, and the seven-day window |

## References

- [lark-im](../SKILL.md)
- [lark-shared](../../lark-shared/SKILL.md)
references/lark-im-messages-edit.md
# im +messages-edit

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Edit an already-sent message's content. **Bot identity only** — the edit API does not accept user tokens. Only messages the bot sent can be edited.

This skill maps to the shortcut: `lark-cli im +messages-edit` (PUT on the message edit endpoint).

## Safety Constraints

Editing rewrites a message visible to other people. Before calling it, you **must** confirm with the user:

1. Which message to edit (its `message_id`)
2. The new content

The bot must be the original sender — editing another identity's message fails. Identity is always the bot: user identity is rejected server-side (`user access token not support`).

**Do not** edit a message without explicit user approval.

## Choose The Right Content Flag

| Need | Recommended flag | Why |
|------|------|------|
| Edit to headings, lists, links, summaries, or Markdown-looking content | `--markdown` | Best default for lightweight formatting; converted to Feishu `post` JSON |
| Edit to exact plain text | `--text` | Preserves literal text; no Markdown conversion |
| Precisely control the new payload | `--content` | You provide the exact JSON for `text` / `post` |
| Attach files/folders to the edited message's attachment zone | `--set-attachments` | Repeatable, as bare `file_key` (`file_xxx`); **replaces** the post content's `files` array (flag values are the final list, discarding any `files` in `--content`). Requires a post message (`--markdown` or `--msg-type post`). Name/metadata are filled by the server, not the client |
| Clear the edited message's attachment zone | `--clear-attachments` | Sets `files:[]` on the post content. Requires a post message; mutually exclusive with `--set-attachments` |
| Keep the existing attachment zone while rewriting the body | *(no attachment flag)* | **Default.** Editing with only `--markdown` / `--text` / `--content` leaves the current `files` array untouched — a body-only edit never drops attachments |

## Editing the Attachment Zone

`post` messages can carry an attachment zone — a top-level `files` array that renders files/folders under the rich-text body.

**Default: no attachment flag preserves the attachment zone.** Editing with only `--markdown` / `--text` / `--content` (i.e. passing neither `--set-attachments` nor `--clear-attachments`) rewrites the body and keeps the existing `files` array unchanged. This is the safe default — fixing a typo must not drop the files you attached. Only pass `--set-attachments` to replace the zone, or `--clear-attachments` to remove it.

To edit a message so it attaches (or re-attaches) files:

```bash
lark-cli im +messages-edit --as bot --message-id om_xxx --markdown "Updated content" --set-attachments file_xxx --set-attachments file_yyy
```

- `--set-attachments` accepts a bare file/folder key (`file_xxx`), and may be repeated.
- **`--set-attachments` is a replace, not an append:** the flag values become the final `files` array. Send/reply's `--attachment` merges; edit's `--set-attachments` replaces.
- **Mutually exclusive with `--content` carrying files:** when `--content` already contains a `files` array, `--set-attachments` and `--clear-attachments` are rejected — declare the attachment zone either via `--content` or via the attachment flags, not both. Use `--markdown` (which never emits a `files` array) or a `--content` without `files` together with the attachment flags.
- The server fills name/size/mime/is_folder from file service metadata; the client does not (and cannot) override the display name.
- When `--set-attachments` is present the effective `msg_type` is forced to `post`. Pair it with `--markdown` (or `--content` with post JSON plus `--msg-type post`); `--text` cannot carry an attachment zone.
- The edited content replaces the whole message content, so include every file you want to keep in the new attachment zone.

To **clear** the attachment zone entirely, pass `--clear-attachments` instead of `--set-attachments`:

```bash
lark-cli im +messages-edit --as bot --message-id om_xxx --markdown "Updated content" --clear-attachments
```

- `--clear-attachments` sets the post content's `files` array to `[]`, telling the server to remove all file/folder attachments.
- It cannot be used together with `--set-attachments`.
- Like `--set-attachments`, it forces the effective `msg_type` to `post`, so pair it with `--markdown` or `--msg-type post --content <post-json>`.

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--message-id <id>` | Yes | Message ID (`om_xxx`) to edit |
| `--text <string>` | One content option | Plain text content |
| `--markdown <string>` | One content option | Markdown text, converted to `post` JSON |
| `--content <json>` | One content option | Exact message content JSON; must match the effective `--msg-type` |
| `--set-attachments <key>` | One content option | Repeatable bare file/folder key (`file_xxx`); **replaces** the post attachment zone — the flag values become the final `files` array, discarding any `files` written in `--content`, and duplicate keys are sent once. Name/size/mime/is_folder are filled by the server |
| `--clear-attachments` | One content option | Clear the post attachment zone by setting `files:[]` |
| `--msg-type <type>` | No | Message type (default `text`). When `--markdown`/`--set-attachments`/`--clear-attachments` is used the effective type is inferred automatically |
| `--as <identity>` | No | Identity type: `bot` only (user identity is rejected by the server) |
| `--dry-run` | No | Print the request only, do not execute it |

## Return Value

```json
{
  "message_id": "om_xxx",
  "chat_id": "oc_xxx",
  "update_time": "1234567890"
}
```

## Common Mistakes

- Editing a message the calling identity did not send — the API rejects it.
- Using `--set-attachments` with `--text`. The attachment zone only exists on `post` messages; use `--markdown` or `--msg-type post`.
- Supplying only the files you want to keep, then losing the text. Editing replaces the entire content; pass the full new content (text + attachments) in one call.
- Assuming a body-only edit clears the attachment zone. It does not — without `--set-attachments` / `--clear-attachments` the existing attachments are preserved.
references/lark-im-messages-mget.md
# im +messages-mget

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Fetch message details in batch. Given a list of message IDs, this returns the full content for multiple messages in one call and automatically resolves sender names.

By default the response also carries a `reactions` block (counts + details from `im.reactions.batch_query`) on every message that has reactions, and `update_time` on messages that were actually edited. Replies inside `thread_replies` participate in the same batched enrichment. Pass `--no-reactions` to skip the extra round-trip. Pass `--download-resources` to additionally download message resources (image/file/audio/video/media + post-embedded, excluding stickers) into `./lark-im-resources/` and attach a `resources` block — off by default, no extra requests when omitted. See [message enrichment](lark-im-message-enrichment.md) for the full contract.

> **Supports both `--as user` (default) and `--as bot`.**

This skill maps to the shortcut: `lark-cli im +messages-mget` (internally calls `GET /open-apis/im/v1/messages/mget`).

## Commands

```bash
# Fetch a single message
lark-cli im +messages-mget --message-ids om_xxx

# Fetch multiple messages in batch (comma-separated)
lark-cli im +messages-mget --message-ids "om_aaa,om_bbb,om_ccc"

# JSON output
lark-cli im +messages-mget --message-ids "om_aaa,om_bbb" --format json

# Preview the request without executing it
lark-cli im +messages-mget --message-ids "om_aaa" --dry-run
```

## Parameters

| Parameter | Required | Limits | Description |
|------|------|------|------|
| `--message-ids <ids>` | Yes | At least one, max 50, `om_xxx` format, comma-separated | Message ID list |
| `--no-reactions` | No | — | Skip auto-fetching the `reactions` block |
| `--download-resources` | No | — | Download message resources (image/file/audio/video/media + post-embedded, excluding stickers) into `./lark-im-resources/` and attach a `resources` block. Off by default |

## Output Fields

| Field | Description |
|------|------|
| `messages` | Message array |
| `total` | Number of messages returned |

Each message contains:

| Field | Description |
|------|------|
| `message_id` | Message ID |
| `msg_type` | Message type (`text`, `image`, `file`, etc.) |
| `create_time` | Creation time |
| `sender` | Sender information (includes `name`) |
| `content` | Message content |

For `post` messages, the attachment zone (top-level `files` array) is rendered as trailing lines in `content`, one per attachment:

- `<file key="file_xxx" name="report.pdf"/>` — a file with a display name (same tag style as a standalone `file` message)
- `<file key="file_xxx"/>` — a file with an empty display name (the server always backfills names, so this branch is rare but valid on the wire)
- `<folder key="file_xxx" name="assets"/>` — a folder (`is_folder: true`, same tag style as a standalone `folder` message)

Use `--format json` to see the full content without table truncation — note the content is the rendered text (including the `<file>`/`<folder>` lines above), not the raw post JSON. Attachment file keys rendered in the tags are eligible for [`+messages-resources-download`](lark-im-messages-resources-download.md) via `--download-resources`.

## Usage Scenarios

### Scenario 1: Fetch the full content of a specific message

```bash
lark-cli im +messages-mget --message-ids om_xxx --format json
```

### Scenario 2: Fetch multiple messages in one batch

```bash
lark-cli im +messages-mget --message-ids "om_aaa,om_bbb,om_ccc"
```

### Scenario 3: Use together with the message list command

First get message IDs via `+chat-messages-list`, then fetch full content via `+messages-mget`:

```bash
# Get the message list
lark-cli im +chat-messages-list --chat-id oc_xxx --format json

# Fetch specific message details
lark-cli im +messages-mget --message-ids "om_aaa,om_bbb"
```

## Common Errors and Troubleshooting

| Symptom | Root Cause | Solution |
|---------|---------|---------|
| `--message-ids requires at least one message ID` | No message ID was provided | Provide at least one message ID |
| `invalid message ID: must start with om_` | Invalid message ID format | Message IDs must start with `om_` |
| Permission denied | Message read permission is missing | Ensure the app has `im:message:readonly` and `contact:user.base:readonly` enabled |
| Empty result | Message IDs do not exist or are not accessible | Verify the IDs and access permissions |

## AI Usage Guidance

1. **Use JSON for full content:** table output truncates content. Use `--format json` when the full body matters.
2. **Sender names are already enriched:** the command resolves sender names automatically, so no extra lookup is required.
3. **Images are rendered as placeholders:** image messages appear as placeholders such as `![Image](img_xxx)`. Use `+messages-resources-download` when you need the binary resource.
4. **Batching is more efficient:** fetching multiple IDs in one request is better than calling the API repeatedly.

## References

- [lark-im](../SKILL.md) - all IM commands
- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters
references/lark-im-messages-reply.md
# im +messages-reply

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Reply to a specific message. Supports both user identity (`--as user`) and bot identity (`--as bot`). Also supports thread replies.

This skill maps to the shortcut: `lark-cli im +messages-reply` (internally calls `POST /open-apis/im/v1/messages/:message_id/reply`).

## Safety Constraints

Replies sent by this tool are visible to other people. Before calling it, you **must** confirm with the user:

1. Which message to reply to
2. The reply content
3. Which identity to use (user or bot)

**Do not** send a reply without explicit user approval.

When using `--as bot`, the reply is sent in the app's name, so make sure the app has already been added to the target chat.

When using `--as user`, the reply is sent as the authorized end user and requires the `im:message.send_as_user` and `im:message` scopes.

## Choose The Right Content Flag

### Default Selection Rule For Agents

- Prefer `--markdown` for headings, lists, links, summaries, investigation notes, or Markdown-looking content.
- Use `--text` for exact plain text: logs, code, indentation-sensitive text, or literal Markdown.
- Use `--content` for exact `post` JSON, titles, multiple locales, cards, or unsupported structures.

| Need | Recommended flag | Why |
|------|------|------|
| Reply with headings, lists, links, summaries, or investigation notes | `--markdown` | Best default for lightweight formatting; converted to Feishu `post` JSON |
| Reply with plain text exactly as written | `--text` | Preserves literal text; no Markdown conversion |
| Precisely control the reply payload | `--content` | You provide the exact JSON |
| Reply with media | `--image` / `--file` / `--video` / `--audio` | Shortcut uploads URLs, or cwd-relative local files automatically |

### `--text` vs `--markdown`

- Use `--markdown` for lightweight formatted replies.
- Use `--text` for exact plain text, especially logs, code, indentation, or literal Markdown characters.
- Use `--content` when you need exact `post` JSON, a card, a title, multiple locales, or any structure that `--markdown` cannot express reliably.

## What `--markdown` Really Does

`--markdown` accepts Markdown-like input and converts it to the Feishu `post` payload required by the reply API.

The shortcut:

1. Forces `msg_type=post`
2. Resolves remote Markdown images like `![x](https://...)`
3. Normalizes the Markdown for Feishu post rendering
4. Wraps the final content as:

```json
{"zh_cn":{"content":[[{"tag":"md","text":"..."}]]}}
```

This makes `--markdown` the simplest path for lightweight formatted replies.

### Markdown Boundaries

- It does **not** promise full CommonMark / GitHub Flavored Markdown support.
- It always becomes a `post` payload with a single `zh_cn` locale.
- It does **not** let you set a `post` title.
- Headings are rewritten:
    - `# Title` becomes `#### Title`
    - `##` to `######` are normalized to `#####` when the content contains H1-H3
- Consecutive headings are separated with blank lines after heading normalization.
- Block spacing and line breaks may be normalized during conversion.
- Code blocks are preserved as code blocks.
- Excess blank lines are compressed.
- Already-uploaded `img_xxx` image keys are the most reliable Markdown image input.
- Local paths (e.g. `![x](./a.png)`) are **not** supported directly in `--markdown` and will not be auto-uploaded.
- Remote URLs (`https://...`) will be auto-downloaded and uploaded at runtime; if the download or upload fails, the image is removed with a warning.

If you need a title, multiple locales, cards, unsupported rich structures, or byte-for-byte post JSON control, use `--msg-type post --content ...`.

### Image Constraint for `--markdown`

When using `--markdown` with images, prefer pre-uploading via `images.create` and referencing `![alt](img_xxx)` for predictable results. Remote URLs may work but are not guaranteed.

**Steps:**

```bash
# 1. Upload image to get image_key
lark-cli im images create --data '{"image_type":"message"}' --file ./diagram.png
# Returns: {"image_key":"img_v3_xxxx"}

# 2. Use image_key in --markdown reply
lark-cli im +messages-reply --message-id om_xxx --markdown $'## Result\n\n![diagram](img_v3_xxxx)\n\nSee above for details.'
```

## Preserving Formatting

If the reply contains multiple lines, code blocks, indentation, tabs, or a lot of escaping, prefer `$'...'` for either `--markdown` or `--text`.

### When formatting must be preserved

Use `--text` plus `$'...'`:

```bash
lark-cli im +messages-reply --message-id om_xxx --text $'Received\nI will check this today.\nOwner: alice'
```

```bash
lark-cli im +messages-reply --message-id om_xxx --text $'```sql\nselect * from jobs;\n```'
```

This keeps the reply as plain text instead of converting it to a `post`.

## Commands

```bash
# Reply with a formatted update
lark-cli im +messages-reply --message-id om_xxx --markdown $'## Reply\n\n- item 1\n- item 2'

# Reply with a plain one-line message
lark-cli im +messages-reply --message-id om_xxx --text "Received"

# Equivalent manual JSON
lark-cli im +messages-reply --message-id om_xxx --content '{"text":"Received"}'

# Reply as a bot
lark-cli im +messages-reply --message-id om_xxx --text "bot reply" --as bot

# Reply with preserved multi-line text
lark-cli im +messages-reply --message-id om_xxx --text $'Line 1\nLine 2\n  indented line'

# Reply inside the thread (message appears in the target thread)
lark-cli im +messages-reply --message-id om_xxx --text "Let's discuss this" --reply-in-thread

# Reply with Markdown containing an image (must pre-upload via images.create)
lark-cli im images create --data '{"image_type":"message"}' --file ./screenshot.png
# Use the returned image_key
lark-cli im +messages-reply --message-id om_xxx --markdown $'## Screenshot\n\n![screenshot](img_v3_xxxx)\n\nConfirmed.'

# If you need exact post structure, send JSON directly
lark-cli im +messages-reply --message-id om_xxx --msg-type post --content '{"zh_cn":{"title":"Reply","content":[[{"tag":"text","text":"Detailed content"}]]}}'

# Reply with a local image (uploaded automatically before sending)
lark-cli im +messages-reply --message-id om_xxx --image ./photo.png

# Reply with a local file (uploaded automatically before sending)
lark-cli im +messages-reply --message-id om_xxx --file ./report.pdf

# Reply with a local video (--video-cover is required as the video cover)
lark-cli im +messages-reply --message-id om_xxx --video ./demo.mp4 --video-cover ./cover.png

# Reply with a voice message
lark-cli im +messages-reply --message-id om_xxx --audio ./voice.opus

# With an idempotency key
lark-cli im +messages-reply --message-id om_xxx --text "Received" --idempotency-key my-unique-id

# Preview the request without executing it
lark-cli im +messages-reply --message-id om_xxx --markdown $'## Test\n\nhello' --dry-run

# ===== Interactive Card =====
# 🚫 STOP — before constructing ANY interactive card JSON, you MUST read
#    card/lark-im-card-create.md and follow its workflow. Do NOT
#    hand-write or copy a card payload. The JSON passed to --content must be
#    the OUTPUT of that workflow. This is non-negotiable.

# Once the workflow has produced the card JSON, reply with it:
lark-cli im +messages-reply --message-id om_xxx --msg-type interactive --content '<card_json_from_workflow>'
```

## Media Input Rules

- Media flags accept an existing key (`img_xxx` / `file_xxx`), an `http://` or `https://` URL, or a local file path.
- Local paths must be relative to the current working directory and stay within it after resolving `..` and symlinks.
- Absolute paths such as `/tmp/photo.png` are rejected. Run the command from the file's directory and pass `./photo.png`, or copy the file into the current directory first.
- `--audio` sends a voice message and accepts only Opus audio (`.opus` or Ogg Opus `.ogg`) for local paths and URLs. For `mp3`, `wav`, or other non-Opus audio, convert to `.opus` before using `--audio`, or use `--file` to send the original audio as an attachment.

## Parameters

| Parameter | Required | Description                                                                                                                                                                                   |
|------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--message-id <id>` | Yes | ID of the message being replied to (`om_xxx`)                                                                                                                                                 |
| `--msg-type <type>` | No | Message type (default `text`). If you use `--text` / `--markdown` / media flags, the effective type is inferred automatically. Explicitly setting a conflicting `--msg-type` fails validation |
| `--content <json>` | One content option | Exact reply content as JSON. The JSON must match the effective `--msg-type`                                                                                                                   |
| `--text <string>` | One content option | Plain text reply. Use when exact text and formatting preservation matter                                                                                                                      |
| `--markdown <string>` | One content option | Best default for lightweight formatted replies such as headings, lists, links, summaries, and investigation notes. Internally converted to `post` JSON with Feishu-specific normalization |
| `--image <path\|url\|key>` | One content option | Cwd-relative local image path, URL, or `image_key` (`img_xxx`)                                                                                                                                |
| `--file <path\|url\|key>` | One content option | Cwd-relative local file path, URL, or `file_key` (`file_xxx`)                                                                                                                                 |
| `--video <path\|url\|key>` | One content option | Cwd-relative local video path, URL, or `file_key` (`file_xxx`); **must be used together with `--video-cover`**                                                                                |
| `--video-cover <path\|url\|key>` | **Required with `--video`** | Cwd-relative local cover image path, URL, or `image_key` (`img_xxx`)                                                                                                                          |
| `--audio <path\|url\|key>` | One content option | Voice-message audio key, URL, or cwd-relative local path. Local paths and URLs must be Opus (`.opus` or Ogg Opus `.ogg`) |
| `--attachment <key>` | One content option | Repeatable bare file/folder key (`file_xxx`); merges into the post message's attachment zone. Requires a post message (`--markdown` or `--msg-type post`). Name/size/mime/is_folder are filled by the server from file service metadata, not taken from the client. Use this instead of `--file` when the file should render inside a rich-text message's attachment area |
| `--reply-in-thread` | No | Reply inside the thread. The reply appears in the target message's thread instead of the main chat stream                                                                                     |
| `--idempotency-key <key>` | No | Idempotency key, max 50 characters; the same key sends only one reply within 1 hour                                                                                                          |
| `--as <identity>` | No | Identity type: `bot` or `user` (default `bot`)                                                                                                                                                |
| `--dry-run` | No | Print the request only, do not execute it                                                                                                                                                     |

> **Mutual exclusivity rule:** `--text`, `--markdown`, `--content`, and `--image`/`--file`/`--video`/`--audio` cannot be used together. Media flags are also mutually exclusive with each other.
>
> **Video cover rule:** `--video` **must** be accompanied by `--video-cover`. Omitting `--video-cover` when using `--video` will fail validation. `--video-cover` cannot be used without `--video`.

## Common Mistakes

- Choosing `--text` for headings, lists, links, summaries, or investigation notes. Use `--markdown`.
- Choosing `--markdown` when you actually need exact plain text. If exact line breaks, spacing, logs, code, or literal Markdown characters matter, use `--text`, usually with `$'...'`.
- Assuming `--markdown` supports every Markdown feature. It is converted into a Feishu `post` payload and normalized first.
- Putting local image paths inside Markdown like `![x](./a.png)`. `--markdown` does not auto-upload those paths.
- **Using local file paths inside Markdown image syntax** (e.g. `![x](./a.png)`) with `--markdown`. Local paths are not auto-uploaded and will not render as an image. Pre-upload via `images.create` to get an `image_key` instead.
- Using `--content` without making the JSON match the effective `--msg-type`.
- Explicitly setting `--msg-type` to something that conflicts with `--text`, `--markdown`, or media flags.
- Mixing `--text`, `--markdown`, or `--content` with media flags in one command.
- Using `--attachment` with `--text` or a media flag. The attachment zone only exists on `post` messages — pair `--attachment` with `--markdown` or `--msg-type post`.

## Return Value

```json
{
  "message_id": "om_xxx",
  "chat_id": "oc_xxx",
  "create_time": "1234567890"
}
```

## Usage Scenarios

### Scenario 1: Reply in the main chat stream

```bash
lark-cli im +messages-reply --message-id om_xxx --text "OK, I will handle it"
```

The reply appears in the main chat stream and references the target message.

### Scenario 2: Reply inside a thread

```bash
lark-cli im +messages-reply --message-id om_xxx --text "Let me take a look at this" --reply-in-thread
```

The reply appears in the target message's thread and does not show up in the main chat stream.

## @Mention Format

The `<at>` syntax differs by message type. The shortcut only normalizes mentions for `text` and `post`; `interactive` card content is passed through verbatim, so cards must use the card-native syntax below.

### `text`

- `<at user_id="ou_xxx">name</at>` — the inner text is the mentioned user's display name and is optional (`<at user_id="ou_xxx"></at>` also works)
- @all: `<at user_id="all"></at>`

### `post`

- Inside a `text` or `md` element, the same inline form as `text` works: `<at user_id="ou_xxx">name</at>`
- Or use a dedicated `at` element node: `{"tag":"at","user_id":"ou_xxx"}` (use `"all"` to mention everyone)

### `interactive` (card)

Card content is **not** normalized — use the card-native `<at>` syntax inside a `lark_md` / `markdown` element:

- single user by open_id: `<at id=ou_xxx></at>`
- multiple users: `<at ids=ou_xxx1,ou_xxx2></at>`
- by email: `<at [email protected]></at>`

## Notes

- `--message-id` must be a valid message ID in `om_xxx` format
- `--content` must be valid JSON
- When using `--content`, you are responsible for making the JSON structure match the effective `msg_type`
- `--reply-in-thread` adds `reply_in_thread=true` to the API request
- `--reply-in-thread` is mainly meaningful in chats that support thread replies
- `--image`/`--file`/`--video`/`--audio`/`--video-cover` support existing keys, URLs, and cwd-relative local file paths; the shortcut uploads local paths and URLs first, then sends the reply; both the upload and send steps use the same identity (UAT when `--as user`, TAT when `--as bot`)
- If the provided media value starts with `img_` or `file_`, it is treated as an existing key and used directly
- `--markdown` always sends `msg_type=post`
- If you explicitly set `--msg-type` and it conflicts with the chosen content flag, validation fails
- When using `--video`, `--video-cover` is required as the video cover
- `--dry-run` uses placeholder image keys for remote Markdown images and placeholder media keys for local uploads
- Failures return error codes and messages
- `--as user` uses a user access token (UAT) and requires the `im:message.send_as_user` and `im:message` scopes; the reply is sent as the authorized end user
- `--as bot` uses a tenant access token (TAT), and requires the `im:message:send_as_bot` scope
- When using `--markdown` with images, pre-uploading via `images.create` to obtain an `image_key` is recommended for reliability; remote URLs may be auto-resolved at runtime, but if download/upload fails the image is removed with a warning; local paths are not supported
- **Interactive cards are gated:** you MUST read and follow the [`card/lark-im-card-create.md`](card/lark-im-card-create.md) workflow to produce the card JSON *before* replying. Do not hand-write or copy a card payload — the JSON given to `--msg-type interactive --content` must be the workflow's output. This applies every time, with no exception
references/lark-im-messages-resources-download.md
# im +messages-resources-download

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Download an image or file attached to a message. Use the `message_id` and resource key returned by a message-reading command; do not guess or combine identifiers from different messages.

> **Note:** read-only message commands render resource keys in message content, but they do not download binaries automatically. Use this command whenever you need to fetch the actual image/file bytes or save them to a specific path.

Shortcut: `lark-cli im +messages-resources-download`.

## Commands

```bash
# Download an image (save to the current directory)
lark-cli im +messages-resources-download --message-id om_xxx --file-key img_v3_xxx --type image

# Download a file
lark-cli im +messages-resources-download --message-id om_xxx --file-key file_v3_xxx --type file

# Specify the output path
lark-cli im +messages-resources-download --message-id om_xxx --file-key img_v3_xxx --type image --output ./photo.png

# Download as a bot
lark-cli im +messages-resources-download --message-id om_xxx --file-key img_v3_xxx --type image --as bot

# Preview the request without executing it
lark-cli im +messages-resources-download --message-id om_xxx --file-key img_v3_xxx --type image --dry-run
```

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--message-id <id>` | Yes | Message ID (`om_xxx` format) |
| `--file-key <key>` | Yes | Resource key (`img_xxx` or `file_xxx`) |
| `--type <type>` | Yes | Resource type: `image` or `file` |
| `--output <path>` | No | Relative output path; absolute paths and `..` traversal are rejected. When omitted, the command uses the attachment name when available and otherwise falls back to the resource key |
| `--as <identity>` | No | Identity type: `user` (default) or `bot` |
| `--dry-run` | No | Print the request only, do not execute it |

## Choose `--type`

Different resource markers in message content correspond to different `file_key` and `type` values:

| Message Type | Marker in Content | `file_key` Format | `--type` |
|---------|-------------|---------------|--------|
| Image | `img_xxx` | `img_xxx` | `image` |
| File | `file_xxx` | `file_xxx` | `file` |
| Audio | `file_xxx` | `file_xxx` | `file` |
| Video | `file_xxx` | `file_xxx` | `file` |

Stickers cannot be downloaded with this command.

## Output

On success, read:

| Field | Meaning |
|------|---------|
| `data.saved_path` | Saved local path |
| `data.size_bytes` | Saved byte count |

## Usage Scenario

### Scenario: Extract and download an image from a message

```bash
# Step 1: Fetch messages and find one containing an image
lark-cli im +chat-messages-list --chat-id oc_xxx
# In the response you see: { "msg_type": "image", "content": "{\"image_key\":\"img_v3_xxx\"}" }

# Step 2: Download the image
lark-cli im +messages-resources-download --message-id om_xxx --file-key img_v3_xxx --type image
```

## Common Errors and Troubleshooting

| Symptom | Root Cause | Solution |
|---------|---------|---------|
| Resource does not match the message | `file_key` and `message_id` came from different messages | Read the message again and use its matching identifiers |
| Permission denied | `im:message:readonly` is not authorized | For user identity, run `lark-cli auth login --scope "im:message:readonly"`; for bot identity, grant the scope to the app in the developer console |
| Attachment unavailable | The message or resource is deleted, hidden, restricted, or inaccessible to the caller | Do not retry unchanged; report the exact CLI error |
| Retryable network error | The transfer did not complete | Retry the same command |

## References

- [lark-im](../SKILL.md) - all message-related commands
- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters
references/lark-im-messages-search.md
# im +messages-search

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Search Feishu messages across conversations. This shortcut automatically performs a multi-step workflow: search for message IDs, batch fetch message details, then enrich the results with chat context.

By default each result message also carries a `reactions` block (counts + details from `im.reactions.batch_query`) when the server has reactions for it, and `update_time` for messages that were actually edited. With `--page-all`, every page is enriched; pass `--no-reactions` to skip the extra round-trip. See [message enrichment](lark-im-message-enrichment.md) for the full contract.

This skill maps to the shortcut: `lark-cli im +messages-search` (internally calls `POST /open-apis/im/v1/messages/search` + batched `GET /open-apis/im/v1/messages/mget`, then batch-fetches chat context).

## Commands

```bash
# Search by keyword
lark-cli im +messages-search --query "project progress"

# Restrict search to a specific group chat
lark-cli im +messages-search --query "weekly report" --chat-id oc_xxx

# Filter by sender (comma-separated)
lark-cli im +messages-search --query "requirement" --sender ou_xxx,ou_yyy

# Filter by attachment type
lark-cli im +messages-search --query "report" --include-attachment-type file

# Filter by chat type (group / p2p)
lark-cli im +messages-search --query "progress" --chat-type group

# Filter by sender type (user / bot)
lark-cli im +messages-search --query "reminder" --sender-type bot

# Exclude bot senders
lark-cli im +messages-search --query "reminder" --exclude-sender-type bot

# Only messages that @me
lark-cli im +messages-search --query "announcement" --is-at-me

# Only messages that @mention specific users (results also include messages that @all)
lark-cli im +messages-search --query "release" --at-chatter-ids ou_xxx,ou_yyy

# Combined filters + time range
lark-cli im +messages-search --query "meeting" --sender ou_xxx --chat-type group --start "2026-03-13T00:00:00+08:00" --end "2026-03-20T23:59:59+08:00"

# Specific time range (ISO 8601)
lark-cli im +messages-search --query "release" --start "2026-03-01T00:00:00+08:00" --end "2026-03-10T00:00:00+08:00"

# Output format options
lark-cli im +messages-search --query "test" --format pretty
lark-cli im +messages-search --query "test" --format table
lark-cli im +messages-search --query "test" --format csv

# Pagination
lark-cli im +messages-search --query "test" --page-token <PAGE_TOKEN>

# Auto-pagination across multiple pages
lark-cli im +messages-search --query "test" --page-all --format json

# Auto-pagination with an explicit page cap
lark-cli im +messages-search --query "test" --page-limit 5 --format json

# Preview the request without executing it
lark-cli im +messages-search --query "test" --dry-run
```

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--query <text>` | No | Search keyword (may be empty when used with other filters) |
| `--chat-id <id>` | No | Restrict to chat IDs, comma-separated (`oc_xxx,oc_yyy`) |
| `--sender <ids>` | No | Sender open_ids, comma-separated (`ou_xxx`) |
| `--include-attachment-type <type>` | No | Attachment filter: `file` / `image` / `video` / `link` |
| `--chat-type <type>` | No | Chat type: `group` / `p2p` |
| `--sender-type <type>` | No | Sender type: `user` / `bot` |
| `--exclude-sender-type <type>` | No | Exclude messages from `user` or `bot` senders |
| `--is-at-me` | No | Only return messages that mention `@me` |
| `--at-chatter-ids <ids>` | No | Filter by @mentioned user open_ids, comma-separated (`ou_xxx,ou_yyy`). Matched results also include messages that `@all` |
| `--start <time>` | No | Start time with local timezone offset required (e.g. `2026-03-24T00:00:00+08:00`) |
| `--end <time>` | No | End time with local timezone offset required (e.g. `2026-03-25T23:59:59+08:00`) |
| `--page-size <n>` | No | Page size (default 20, range 1-50) |
| `--page-token <token>` | No | Starting cursor, normally returned by a previous response |
| `--page-all` | No | Automatically paginate through all result pages (up to 40 pages) |
| `--page-limit <n>` | No | Max pages to fetch when auto-pagination is enabled (default 20, max 40). Setting it explicitly also enables auto-pagination |
| `--format <fmt>` | No | Output format: `json` (default) / `pretty` / `table` / `ndjson` / `csv` |
| `--as <identity>` | No | Identity type: `user` or `bot` |
| `--dry-run` | No | Print the request only, do not execute it |

## Core Constraints

### 1. Provide at least one filter whenever possible

All parameters are optional, but you should usually provide at least one filter (`--query`, `--sender`, `--chat-id`, etc.). Otherwise the search scope may be too broad and return low-signal results.

### 2. Two-step orchestration is automatic

The shortcut automatically performs:

1. The **search API** returns matching `message_id` values
2. The **mget API** fetches full message content for those message IDs in batch
3. Chat context lookup is fetched in batch and attached to each message

The user does not need to manage the orchestration manually. When search results span multiple pages, the shortcut can also paginate automatically with `--page-all` or `--page-limit`.

### 3. Conversation context is enriched automatically

In JSON output, each message automatically includes conversation context:

| Field | Description |
|------|------|
| `chat_type` | Conversation type: `p2p` / `group` |
| `chat_name` | Group name (for groups) or the other participant's name (for p2p chats) |
| `chat_partner` | For p2p only: the other participant's `open_id` and `name` |

In pretty output, the `chat` column shows the chat name for groups, or `"p2p"` for direct messages.

Each message in JSON output contains:

| Field | Description |
|------|------|
| `message_id` | Message ID |
| `msg_type` | Message type: `text`, `image`, `file`, `interactive`, `post`, `audio`, `video`, `system`, etc. |
| `create_time` | Creation time |
| `sender` | Sender information (includes `name` for user senders) |
| `content` | Message content |
| `chat_id` | ID of the conversation the message belongs to |
| `deleted` | Whether the message has been recalled (`true` = recalled) |
| `updated` | Whether the message has been edited after sending |
| `mentions` | Array of @mentions in the message; each item contains `{id, key, name}`. Present only when the message contains @mentions |
| `thread_id` | Thread ID (`omt_xxx`) if the message has replies in a thread. Present only when replies exist |

### 4. Pagination behavior

- Default behavior is still **single-page**.
- `--page-token` is the manual continuation mechanism when you already have a token from a previous response.
- `--page-all` enables auto-pagination and uses a default cap of **40 pages**.
- With both flags, auto-pagination starts at `--page-token` and continues from that cursor.
- `--page-limit <n>` enables auto-pagination with an explicit cap. If you pass `--page-limit` without `--page-all`, auto-pagination is still enabled.
- When auto-pagination stops because of the configured page cap, the response still includes the last `has_more` / `page_token` so you can continue manually.

### 5. Search results contain follow-up clues

In JSON output, each message includes `chat_id` and `thread_id` (when present). Use them with other shortcuts for deeper inspection:

```bash
# View the full message stream for the conversation that contains the search result
lark-cli im +chat-messages-list --chat-id <chat_id>

# View replies in the thread that contains the search result
lark-cli im +threads-messages-list --thread <thread_id>
```

## Resource Rendering

Search results reuse the same content formatter as other read commands. Image messages are rendered as placeholders such as `![Image](img_xxx)`; resource binaries are **not** downloaded automatically.

Use `im +messages-resources-download` if you need to fetch the underlying image or file bytes from a specific message.

## AI Usage Guidance

### Query boundary for activity review

Use `--query` only for real message keywords. If the user asks for activity review such as "最近一周我和哪些 Bot 有过交互" or "整理我和某人的聊天记录", and the useful constraints are sender type, chat, person, or time range, keep `--query ""` and rely on those filters. Do not put generic instruction words such as "看看", "总结", "交互内容", or "聊天记录" into `--query`; those words often over-constrain message search and hide the relevant messages.

This guidance applies to both user and bot identity. If the user explicitly asks for application/bot identity, run `im +messages-search --as bot`; for named-group history/listing intents where search is not needed, resolving the group with `im +chat-search --as bot` and listing messages with `im +chat-messages-list --as bot --chat-id <chat_id>` is still a good narrower path.

```bash
# Review recent bot interactions without forcing a keyword
lark-cli im +messages-search --query "" --sender-type bot --start "<YYYY-MM-DDT00:00:00+08:00>" --end "<YYYY-MM-DDT23:59:59+08:00>" --page-all --format json
```

Replace the time placeholders at execution time. For example, "最近一周" means computing the start date and end date from the current day before running the command; do not copy date literals from this reference into answers for relative requests.

For activity summaries, validate evidence by message IDs and chat context. The final answer should cite or retain the `message_id`, sender, chat, and create time for each important item. If the row's source data contains concrete `om_...` message IDs or `ou_...` user IDs, treat those IDs as strong recall targets during verification; do not rely only on a high-level keyword match.

### Resolving chat_id from a chat name

When the user refers to a chat by name and you need its `chat_id` for the `--chat-id` filter, use [`+chat-search`](lark-im-chat-search.md) first:

```bash
# Step 1: Find the chat_id by name
lark-cli im +chat-search --query "<chat name keyword>" --format json

# Step 2: Use the chat_id to narrow down message search
lark-cli im +messages-search --query "keyword" --chat-id <chat_id>
```

**Do not use `im chats search` or `+chat-list` — always use the `+chat-search` shortcut.**

## Work Summary / Report Generation

When the user asks you to summarize work, generate a weekly report, or compile activity from chat messages, you should **paginate through all available results** to get a complete picture. A single page is rarely enough for thorough summarization.

### Strategy

1. **Start with targeted filters** — use `--chat-id`, `--sender`, `--start`, `--end` to narrow the scope as much as possible before paginating.
2. **Prefer auto-pagination** — for report and summary tasks, use `--page-all --format json` by default. If you need a bounded run, use `--page-limit <n> --format json`.
3. **Accumulate before summarizing** — collect all pages of messages first, then analyze and summarize. Do not summarize after the first page alone — you will miss important context.
4. **Fall back to `--page-token` when resuming** — if auto-pagination hits the configured page cap and the response still has `has_more=true`, continue from the returned `page_token`.
5. **Use `--format json`** — JSON output includes `has_more` and `page_token` fields needed for pagination. `pretty` and `table` formats are useful for reading but not for resuming pagination reliably.

### Example: Weekly work summary from a project chat

```bash
# Preferred: fetch automatically
lark-cli im +messages-search --query "" --chat-id oc_xxx --sender ou_me --start "2026-03-18T00:00:00+08:00" --end "2026-03-25T23:59:59+08:00" --page-size 50 --page-all --format json

# If you need to cap the run explicitly
lark-cli im +messages-search --query "" --chat-id oc_xxx --sender ou_me --start "2026-03-18T00:00:00+08:00" --end "2026-03-25T23:59:59+08:00" --page-size 50 --page-limit 5 --format json

# If the bounded run still returns has_more=true, continue manually
lark-cli im +messages-search --query "" --chat-id oc_xxx --sender ou_me --start "2026-03-18T00:00:00+08:00" --end "2026-03-25T23:59:59+08:00" --page-size 50 --page-token <token_from_previous_run> --format json
```

### Key points

- **Always paginate exhaustively** for summary tasks. A single page of 20-50 messages is usually insufficient for a meaningful work summary.
- Prefer `--page-all`; use `--page-limit` only when you need to bound runtime or output volume.
- If the user does not specify a time range, default to the current week (Monday to today) for weekly reports, or ask for clarification.
- When summarizing, group messages by topic/thread rather than by chronological order for better readability.

## Common Errors and Troubleshooting

| Symptom | Root Cause | Solution |
|---------|---------|---------|
| Too few results | The time range is too narrow or the keyword is too specific | Expand the time range and try broader keywords |
| No results | Missing permission or no match | Confirm `search:message` is authorized and relax the filters |
| Permission denied | Search scope not authorized | Run `auth login --scope "search:message"` |

## References

- [lark-im](../SKILL.md) - all message-related commands
- [lark-im-threads-messages-list](lark-im-threads-messages-list.md) - inspect thread replies
- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters
references/lark-im-messages-send.md
# im +messages-send

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Send a message to a group chat or a direct message conversation. Supports both user identity (`--as user`) and bot identity (`--as bot`).

This skill maps to the shortcut: `lark-cli im +messages-send` (internally calls `POST /open-apis/im/v1/messages`).

## Safety Constraints

Messages sent by this tool are visible to other people. Before calling it, you **must** confirm with the user:

1. The recipient (which person or which group)
2. The message content
3. The sending identity (user or bot)

**Do not** send messages without explicit user approval.

When using `--as bot`, the message is sent in the app's name, so make sure the app has already been added to the target chat.

When using `--as user`, the message is sent as the authorized end user and requires the `im:message.send_as_user` and `im:message` scopes.

## Choose The Right Content Flag

### Default Selection Rule For Agents

- Prefer `--markdown` for headings, lists, links, summaries, reports, or Markdown-looking content.
- Use `--text` for exact plain text: logs, code, indentation-sensitive text, or literal Markdown.
- Use `--content` for exact `post` JSON, titles, multiple locales, cards, or unsupported structures.

| Need | Recommended flag | Why |
|------|------|------|
| Send headings, lists, links, summaries, or reports | `--markdown` | Best default for lightweight formatting; converted to Feishu `post` JSON |
| Send plain text exactly as written | `--text` | Preserves literal text; no Markdown conversion |
| Precisely control the final payload | `--content` | You provide the exact JSON for `text` / `post` / `interactive` / `share_*` / media payloads |
| Send image / file / video / audio | `--image` / `--file` / `--video` / `--audio` | Shortcut uploads URLs, or cwd-relative local files automatically |
| Attach files/folders to a post message's attachment zone | `--attachment` | Repeatable, as bare `file_key` (`file_xxx`); merges into the post content's `files` array. Requires a post message (`--markdown` or `--msg-type post`). Name/metadata are filled by the server, not the client |

### `--text` vs `--markdown`

- Use `--markdown` for lightweight formatted messages.
- Use `--text` for exact plain text, especially logs, code, indentation, or Markdown characters that should **not** render.
- Use `--content` when `--markdown` is not enough, especially if you need exact `post` JSON, a title, multiple locales, cards, or unsupported rich structures.

## What `--markdown` Really Does

`--markdown` accepts Markdown-like input and converts it to the Feishu `post` payload required by the message API.

The shortcut does all of the following before sending:

1. Forces `msg_type=post`
2. Resolves remote Markdown images like `![x](https://...)` by downloading and uploading them first
3. Normalizes the Markdown for Feishu post rendering
4. Wraps the result as:

```json
{"zh_cn":{"content":[[{"tag":"md","text":"..."}]]}}
```

This makes `--markdown` the simplest path for lightweight formatted messages.

### Markdown Boundaries

- It does **not** promise full CommonMark / GitHub Flavored Markdown support.
- It always becomes a `post` payload with a single `zh_cn` locale.
- It does **not** let you set a `post` title. If you need a title, use `--msg-type post --content ...`.
- Headings are rewritten:
    - `# Title` becomes `#### Title`
    - `##` to `######` are normalized to `#####` when the content contains H1-H3
- Consecutive headings are separated with blank lines after heading normalization.
- Block spacing and line breaks may be normalized during conversion.
- Code blocks are preserved as code blocks.
- Excess blank lines are compressed.
- Already-uploaded `img_xxx` image keys are the most reliable Markdown image input.
- Local paths in Markdown image syntax like `![x](./a.png)` are **not** supported and will not be auto-uploaded.
- Remote URLs (`https://...`) will be auto-downloaded and uploaded at runtime; if the download or upload fails, the image is removed with a warning.

If you need a title, multiple locales, cards, unsupported rich structures, or byte-for-byte post JSON control, use `--content` and provide the final JSON yourself.

### Image Constraint for `--markdown`

When using `--markdown` with images, prefer pre-uploading via `images.create` and referencing `![alt](img_xxx)` for predictable results. Remote URLs may work but are not guaranteed.

**Steps:**

```bash
# 1. Upload image to get image_key
lark-cli im images create --data '{"image_type":"message"}' --file ./diagram.png
# Returns: {"image_key":"img_v3_xxxx"}

# 2. Use image_key in --markdown
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Report\n\n![diagram](img_v3_xxxx)\n\nSee above for details.'
```

## Preserving Formatting

If the message has multiple lines, indentation, code blocks, tabs, or many quotes/backslashes, prefer shell ANSI-C quoting with `$'...'` for either `--markdown` or `--text`.

This is especially useful in `zsh` / `bash` because it lets you write `\n` explicitly instead of relying on the shell to preserve literal newlines.

### When formatting must be preserved

Use `--text` plus `$'...'`:

```bash
lark-cli im +messages-send --chat-id oc_xxx --text $'Build failed\nBranch: feature/im-docs\nAction: please check logs'
```

```bash
lark-cli im +messages-send --chat-id oc_xxx --text $'```bash\nmake test\nmake lint\n```'
```

Use this path when you want the receiver to see the text exactly as entered, not a converted Markdown post.

## Commands

```bash
# Send a formatted update
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Update\n\n- item 1\n- item 2'

# Send a plain one-line message
lark-cli im +messages-send --chat-id oc_xxx --text "Hello"

# Equivalent manual JSON
lark-cli im +messages-send --chat-id oc_xxx --content '{"text":"Hello"}'

# Send to a direct message (pass open_id)
lark-cli im +messages-send --user-id ou_xxx --text "Hello"

# Send multi-line text while preserving formatting
lark-cli im +messages-send --chat-id oc_xxx --text $'Line 1\nLine 2\n  indented line'

# Send Markdown with an image (must pre-upload via images.create)
lark-cli im images create --data '{"image_type":"message"}' --file ./screenshot.png
# Use the returned image_key in the markdown content
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Status\n\n![screenshot](img_v3_xxxx)\n\nDone.'

# If you need exact post structure, send JSON directly
lark-cli im +messages-send --chat-id oc_xxx --msg-type post --content '{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"Body"}]]}}'

# Send a local image (uploaded automatically before sending)
lark-cli im +messages-send --chat-id oc_xxx --image ./photo.png

# Or send directly with an existing image_key
lark-cli im +messages-send --chat-id oc_xxx --image img_xxx

# Send a local file (uploaded automatically before sending)
lark-cli im +messages-send --chat-id oc_xxx --file ./report.pdf

# Send a video (--video-cover is required as the cover)
lark-cli im +messages-send --chat-id oc_xxx --video ./demo.mp4 --video-cover ./cover.png
lark-cli im +messages-send --chat-id oc_xxx --video ./demo.mp4 --video-cover img_xxx

# Send a voice message
lark-cli im +messages-send --chat-id oc_xxx --audio ./voice.opus

# Use an idempotency key (same key sends only once within 1 hour)
lark-cli im +messages-send --chat-id oc_xxx --text "Hello" --idempotency-key my-unique-id

# Preview the request without executing it
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry-run

# ===== Interactive Card =====
# 🚫 STOP — before constructing ANY interactive card JSON, you MUST read
#    card/lark-im-card-create.md and follow its workflow. Do NOT
#    hand-write or copy a card payload from the examples below. The JSON passed
#    to --content must be the OUTPUT of that workflow. This is non-negotiable.

# Once the workflow has produced the card JSON, send it:
lark-cli im +messages-send --chat-id oc_xxx --msg-type interactive --content '<card_json_from_workflow>'
```

## Media Input Rules

- Media flags accept an existing key (`img_xxx` / `file_xxx`), an `http://` or `https://` URL, or a local file path.
- Local paths must be relative to the current working directory and stay within it after resolving `..` and symlinks.
- Absolute paths such as `/tmp/photo.png` are rejected. Run the command from the file's directory and pass `./photo.png`, or copy the file into the current directory first.
- `--audio` sends a voice message and accepts only Opus audio (`.opus` or Ogg Opus `.ogg`) for local paths and URLs. For `mp3`, `wav`, or other non-Opus audio, convert to `.opus` before using `--audio`, or use `--file` to send the original audio as an attachment.

## Parameters

| Parameter | Required | Description                                                                                                                                                                                   |
|------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--chat-id <id>` | One of two | Group chat ID (`oc_xxx`)                                                                                                                                                                      |
| `--user-id <id>` | One of two | User open_id (`ou_xxx`) for direct messages                                                                                                                                                   |
| `--text <string>` | One content option | Plain text message. Use when exact text and formatting preservation matter. Automatically wrapped as `{"text":"..."}`                                                                         |
| `--markdown <string>` | One content option | Best default for lightweight formatted messages such as headings, lists, links, summaries, and reports. Internally converted to `post` JSON with Feishu-specific normalization               |
| `--content <json>` | One content option | Exact message content JSON string; use this when you need full control over `msg_type` and payload. The JSON must match the effective `--msg-type`                                            |
| `--image <path\|url\|key>` | One content option | Cwd-relative local image path, URL, or `image_key` (`img_xxx`). Local paths and URLs are uploaded automatically                                                                               |
| `--file <path\|url\|key>` | One content option | Cwd-relative local file path, URL, or `file_key` (`file_xxx`). Local paths and URLs are uploaded automatically                                                                                |
| `--video <path\|url\|key>` | One content option | Cwd-relative local video path, URL, or `file_key` (`file_xxx`). Local paths and URLs are uploaded automatically. **Must be paired with `--video-cover`**                                      |
| `--video-cover <path\|url\|key>` | **Required with `--video`** | Cwd-relative local cover image path, URL, or `image_key` (`img_xxx`). Local paths and URLs are uploaded automatically                                                                         |
| `--audio <path\|url\|key>` | One content option | Voice-message audio key, URL, or cwd-relative local path. Local paths and URLs must be Opus (`.opus` or Ogg Opus `.ogg`) |
| `--attachment <key>` | One content option | Repeatable bare file/folder key (`file_xxx`); merges into the post message's attachment zone. Requires a post message (`--markdown` or `--msg-type post`). Name/size/mime/is_folder are filled by the server from file service metadata, not taken from the client. Use this instead of `--file` when the file should render inside a rich-text message's attachment area |
| `--msg-type <type>` | No | Message type (default `text`). If you use `--text` / `--markdown` / media flags, the effective type is inferred automatically. Explicitly setting a conflicting `--msg-type` fails validation |
| `--idempotency-key <key>` | No | Idempotency key, max 50 characters; the same key sends only one message within 1 hour                                                                                                        |
| `--as <identity>` | No | Identity type: `bot` or `user` (default `bot`)                                                                                                                                                |
| `--dry-run` | No | Print the request only, do not execute it                                                                                                                                                     |

> **Mutual exclusivity rule:** `--text`, `--markdown`, `--content`, and `--image`/`--file`/`--video`/`--audio` cannot be used together. Media flags are also mutually exclusive with each other. `--attachment` cannot be combined with a `--content` that already contains a `files` array (the attachment zone is declared either via `--content` or via `--attachment`, not both).
>
> **Video cover rule:** `--video` **must** be accompanied by `--video-cover`. Omitting `--video-cover` when using `--video` will fail validation. `--video-cover` cannot be used without `--video`.

## Common Mistakes

- Choosing `--text` for headings, lists, links, summaries, or reports. Use `--markdown`.
- Choosing `--markdown` when you actually need exact plain text. If exact line breaks, spacing, logs, code, or literal Markdown characters matter, use `--text`, usually with `$'...'`.
- Assuming `--markdown` supports every Markdown feature. It is converted into a Feishu `post` payload and normalized first.
- Putting local image paths inside Markdown like `![x](./a.png)`. `--markdown` does not auto-upload those paths.
- **Using local file paths inside Markdown image syntax** (e.g. `![x](./a.png)`) with `--markdown`. Local paths are not auto-uploaded and will not render as an image. Pre-upload via `images.create` to get an `image_key` instead.
- Using `--content` without making the JSON match the effective `--msg-type`.
- Explicitly setting `--msg-type` to something that conflicts with `--text`, `--markdown`, or media flags.
- Mixing `--text`, `--markdown`, or `--content` with media flags in one command.
- Using `--attachment` with `--text` or a media flag. The attachment zone only exists on `post` messages — pair `--attachment` with `--markdown` or `--msg-type post`.
- Using `--file` when the file should sit inside a rich-text message's attachment area. `--file` sends a standalone `file`-type message; use `--attachment` (with `--markdown` or post `--content`) to attach files/folders inside a post message.

## `content` Format Reference

| `msg_type` | Example `content` |
|----------|-------------|
| `text` | `{"text":"Hello <at user_id=\"ou_xxx\">name</at>"}` |
| `post` | `{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"Body"}]]},"files":[{"key":"file_xxx"}]}` — the top-level `files` array is the attachment zone; each entry carries a file/folder `key` (name/metadata are backfilled by the server from file service metadata — a client-supplied `name` has no effect) |
| `image` | `{"image_key":"img_xxx"}` |
| `file` | `{"file_key":"file_xxx"}` |
| `audio` | `{"file_key":"file_xxx"}` |
| `media` | `{"file_key":"file_xxx","image_key":"img_xxx"}` (video; `image_key` is the cover from `--video-cover` — **required**) |
| `share_chat` | `{"chat_id":"oc_xxx"}` |
| `share_user` | `{"user_id":"ou_xxx"}` |
| `interactive` | Card JSON — **MUST** be produced by the [`card/lark-im-card-create.md`](card/lark-im-card-create.md) workflow. Read it before writing any card; never hand-craft the JSON here |

> **`post` vs `interactive`:** `post` is a static rich-text message (title, paragraphs, @mentions, links, inline images) — content is fixed once sent. `interactive` is a card with structured layout and UI components (buttons, forms, selects, date pickers, charts) — content can be updated after sending and supports user-action callbacks. Use `post` for read-only content; use `interactive` when the message needs user interaction or dynamic updates.

`interactive` cards support callback events (`card.action.trigger`) — see [`lark-im-card-action-reply.md`](lark-im-card-action-reply.md).

## Return Value

```json
{
  "message_id": "om_xxx",
  "chat_id": "oc_xxx",
  "create_time": "1234567890"
}
```

## @Mention Format

The `<at>` syntax differs by message type. The shortcut only normalizes mentions for `text` and `post`; `interactive` card content is passed through verbatim, so cards must use the card-native syntax below.

### `text`

- `<at user_id="ou_xxx">name</at>` — the inner text is the mentioned user's display name and is optional (`<at user_id="ou_xxx"></at>` also works)
- @all: `<at user_id="all"></at>`

### `post`

- Inside a `text` or `md` element, the same inline form as `text` works: `<at user_id="ou_xxx">name</at>`
- Or use a dedicated `at` element node: `{"tag":"at","user_id":"ou_xxx"}` (use `"all"` to mention everyone)

### `interactive` (card)

Card content is **not** normalized — use the card-native `<at>` syntax inside a `lark_md` / `markdown` element:

- single user by open_id: `<at id=ou_xxx></at>`
- multiple users: `<at ids=ou_xxx1,ou_xxx2></at>`
- by email: `<at [email protected]></at>`

## Notes

- `--chat-id` and `--user-id` are mutually exclusive; you must provide exactly one
- `--content` must be valid JSON
- When using `--content`, you are responsible for making the JSON structure match the effective `msg_type`
- `--image`/`--file`/`--video`/`--audio` support existing keys, URLs, and cwd-relative local file paths; the shortcut uploads local paths and URLs first, then sends the message; both the upload and send steps use the same identity (UAT when `--as user`, TAT when `--as bot`)
- If the provided media value starts with `img_` or `file_`, it is treated as an existing key and used directly
- `--markdown` always sends `msg_type=post`, even if you do not explicitly set `--msg-type post`
- If you explicitly set `--msg-type` and it conflicts with the chosen content flag, validation fails
- When using `--video`, `--video-cover` is required as the video cover
- `--dry-run` uses placeholder image keys for remote Markdown images and placeholder media keys for local uploads
- Failures return an error code and message
- `--as user` uses a user access token (UAT) and requires the `im:message.send_as_user` and `im:message` scopes; the message is sent as the authorized end user
- `--as bot` uses a tenant access token (TAT) and requires the `im:message:send_as_bot` scope
- When sending as a bot, the app must already be in the target group or already have a direct-message relationship with the target user
- When using `--markdown` with images, pre-uploading via `images.create` to obtain an `image_key` is recommended for reliability; remote URLs may be auto-resolved at runtime, but if download/upload fails the image is removed with a warning; local paths are not supported
- **Interactive cards are gated:** you MUST read and follow the [`card/lark-im-card-create.md`](card/lark-im-card-create.md) workflow to produce the card JSON *before* sending. Do not hand-write or copy a card payload — the JSON given to `--msg-type interactive --content` must be the workflow's output. This applies every time, with no exception
references/lark-im-reactions.md
# im reactions

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

> **Heads-up — don't reach for `batch_query` by default.** The four message-pulling shortcuts (`+messages-mget`, `+chat-messages-list`, `+messages-search`, `+threads-messages-list`) already call `im.reactions.batch_query` automatically and attach the result as a `reactions` block on each message (replies inside `thread_replies` included). Use those shortcuts for any "read reactions of messages I'm already pulling" task. Reach for the raw `batch_query` API only when you have a standalone `message_id` outside that pull flow. See the main [message enrichment](lark-im-message-enrichment.md) for the contract.

This reference is the shared annotation target for the IM reaction APIs:

- `im.reactions.create`
- `im.reactions.list`
- `im.reactions.delete`
- `im.reactions.batch_query`

It focuses on:

- What each reaction method does
- The request/response shape you need when calling the raw API commands
- The complete `emoji_type` list used in reaction payloads and filters

> **Important:** These raw API commands accept structured input through `--params '<json>'` and `--data '<json>'`. They do not expose typed flags such as `--message-id` or `--reaction-type` directly.

## Command Overview

| Method | HTTP | Path | Purpose |
|---|---|---|---|
| `im.reactions.create` | `POST` | `/open-apis/im/v1/messages/{message_id}/reactions` | Add a reaction to one message |
| `im.reactions.list` | `GET` | `/open-apis/im/v1/messages/{message_id}/reactions` | List reaction records on one message |
| `im.reactions.delete` | `DELETE` | `/open-apis/im/v1/messages/{message_id}/reactions/{reaction_id}` | Delete one specific reaction record |
| `im.reactions.batch_query` | `POST` | `/open-apis/im/v1/messages/reactions/batch_query` | Query reactions for multiple messages in one request |

## Common Notes

- `message_id` is always an IM message ID such as `om_xxx`
- `reaction_id` is the unique record ID returned after a reaction is added
- `reaction_type.emoji_type` is the enum-like emoji identifier used by both write and read APIs
- Reaction APIs return **reaction records**, not only aggregated counts
- When the operator is a human user, the returned ID type may depend on `user_id_type`

## Inspect Schema

```bash
lark-cli schema im.reactions
lark-cli schema im.reactions.create --format pretty
lark-cli schema im.reactions.list --format pretty
lark-cli schema im.reactions.delete --format pretty
```

If your local build has already exposed the batch API in `schema`, also check:

```bash
lark-cli schema im.reactions.batch_query --format pretty
```

## create

Add a reaction to one message.

```bash
lark-cli im reactions create \
  --params '{"message_id":"om_xxx"}' \
  --data '{"reaction_type":{"emoji_type":"SMILE"}}'
```

### Request

- `--params.message_id`: required message ID
- `--data.reaction_type.emoji_type`: required emoji type

### Response

```json
{
  "reaction_id": "ZCaCIjUBVVWSrm5L-3ZTw_xxx",
  "operator": {
    "operator_id": "ou_xxx",
    "operator_type": "user"
  },
  "action_time": "1663054162546",
  "reaction_type": {
    "emoji_type": "SMILE"
  }
}
```

## list

List reaction records on one message.

```bash
lark-cli im reactions list --params '{"message_id":"om_xxx"}'
lark-cli im reactions list --params '{"message_id":"om_xxx","reaction_type":"SMILE"}'
lark-cli im reactions list --params '{"message_id":"om_xxx","page_size":50}'
lark-cli im reactions list --params '{"message_id":"om_xxx","page_token":"<PAGE_TOKEN>"}'
lark-cli im reactions list --params '{"message_id":"om_xxx","user_id_type":"open_id"}'
```

### Request Parameters (`--params`)

| Parameter | Required | Description |
|---|---|---|
| `message_id` | Yes | Message ID (`om_xxx`) |
| `reaction_type` | No | Filter by one emoji type such as `SMILE` or `LAUGH` |
| `page_size` | No | Number of records per page. Default is 20 |
| `page_token` | No | Pagination token from the previous page |
| `user_id_type` | No | Returned operator ID type when `operator_type=user`: `open_id`, `union_id`, or `user_id` |

### Response Shape

```json
{
  "items": [
    {
      "reaction_id": "ZCaCIjUBVVWSrm5L-3ZTw_xxx",
      "operator": {
        "operator_id": "ou_xxx",
        "operator_type": "user"
      },
      "action_time": "1663054162546",
      "reaction_type": {
        "emoji_type": "SMILE"
      }
    }
  ],
  "has_more": true,
  "page_token": "YhljsPiGfUgnVAg9urvRFd-BvSqRLxxxx"
}
```

### Top-Level Fields

| Field | Type | Meaning |
|---|---|---|
| `items` | `array<object>` | Reaction records for the current page |
| `has_more` | `boolean` | Whether more pages are available |
| `page_token` | `string` | Token for the next page when `has_more=true` |

### `items[]` Fields

| Field | Type | Meaning |
|---|---|---|
| `reaction_id` | `string` | Unique ID of this reaction record |
| `operator` | `object` | Identity of the user or app that added the reaction |
| `action_time` | `string` | Unix timestamp in milliseconds |
| `reaction_type` | `object` | Reaction payload. The key field is `emoji_type` |

### `operator` Fields

| Field | Type | Meaning |
|---|---|---|
| `operator.operator_id` | `string` | Operator ID. If `operator_type=user`, the returned ID type follows `user_id_type`; if `operator_type=app`, this is the app ID |
| `operator.operator_type` | `string` | `user` or `app` |

## delete

Delete one specific reaction record from one message.

```bash
lark-cli im reactions delete \
  --params '{"message_id":"om_xxx","reaction_id":"ZCaCIjUBVVWSrm5L-3ZTw_xxx"}'
```

### Request

- `--params.message_id`: required message ID
- `--params.reaction_id`: required reaction record ID

### Response

The response shape is similar to `create`, and usually echoes:

- `reaction_id`
- `operator`
- `action_time`
- `reaction_type.emoji_type`

## batch_query

Query reactions for multiple messages in one request.

```bash
lark-cli im reactions batch_query \
  --params '{"user_id_type":"open_id"}' \
  --data '{
    "queries":[
      {"message_id":"om_xxx"},
      {"message_id":"om_yyy","page_token":"<PAGE_TOKEN>"}
    ],
    "page_size_per_message":10,
    "reaction_type":"LAUGH"
  }'
```

### Request

#### `--params`

| Parameter | Required | Description |
|---|---|---|
| `user_id_type` | No | Returned user ID type in operator info: `open_id`, `union_id`, or `user_id` |

#### `--data`

| Field | Required | Description |
|---|---|---|
| `queries` | Yes | Array of target messages |
| `queries[].message_id` | No | Message ID to query |
| `queries[].page_token` | No | Continuation token for that message |
| `page_size_per_message` | No | Max reactions returned per message |
| `reaction_type` | No | Filter by one emoji type |

### Response

The meta definition contains three top-level result groups:

| Field | Meaning |
|---|---|
| `success_msg_reaction_details` | Per-message reaction detail records |
| `success_msg_reaction_counts` | Per-message aggregated reaction counts |
| `fail_msg_reaction_details` | Query failures for individual messages |

#### `success_msg_reaction_details`

Each `message_reaction_items[]` element includes:

- `reaction_id`
- `operator`
- `action_time`
- `emoji_type`

#### `success_msg_reaction_counts`

Each aggregated count record includes:

- `message_id`
- `reaction_count[].reaction_type`
- `reaction_count[].count`

#### `fail_msg_reaction_details`

Each failed message record includes:

- `message_id`
- `fail_reason`

Supported `fail_reason` values from meta:

- `invalid`
- `invalid_page_token`
- `no_permission`

## `emoji_type` Field

Reaction emoji identifiers are used in slightly different field names across the APIs:

- `im.reactions.create`: request and response use `reaction_type.emoji_type`
- `im.reactions.list`: request filter uses `reaction_type`, response uses `reaction_type.emoji_type`
- `im.reactions.delete`: response uses `reaction_type.emoji_type`
- `im.reactions.batch_query`: request filter uses top-level `reaction_type`, detail results use `message_reaction_items[].emoji_type`, aggregated results use `reaction_count[].reaction_type`

## Complete `emoji_type` List

The following list is synchronized from the official Feishu reaction emoji documentation:

- Source page: `https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message-reaction/emojis-introduce`
- Markdown source: `https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message-reaction/emojis-introduce.md`

Current count in the fetched source: `185`.

```text
OK, THUMBSUP, THANKS, MUSCLE, FINGERHEART, APPLAUSE, FISTBUMP, JIAYI
DONE, SMILE, BLUSH, LAUGH, SMIRK, LOL, FACEPALM, LOVE
WINK, PROUD, WITTY, SMART, SCOWL, THINKING, SOB, CRY
ERROR, NOSEPICK, HAUGHTY, SLAP, SPITBLOOD, TOASTED, GLANCE, DULL
INNOCENTSMILE, JOYFUL, WOW, TRICK, YEAH, ENOUGH, TEARS, EMBARRASSED
KISS, SMOOCH, DROOL, OBSESSED, MONEY, TEASE, SHOWOFF, COMFORT
CLAP, PRAISE, STRIVE, XBLUSH, SILENT, WAVE, WHAT, FROWN
SHY, DIZZY, LOOKDOWN, CHUCKLE, WAIL, CRAZY, WHIMPER, HUG
BLUBBER, WRONGED, HUSKY, SHHH, SMUG, ANGRY, HAMMER, SHOCKED
TERROR, PETRIFIED, SKULL, SWEAT, SPEECHLESS, SLEEP, DROWSY, YAWN
SICK, PUKE, BETRAYED, HEADSET, EatingFood, MeMeMe, Sigh, Typing
Lemon, Get, LGTM, OnIt, OneSecond, VRHeadset, YouAreTheBest, SALUTE
SHAKE, HIGHFIVE, UPPERLEFT, ThumbsDown, SLIGHT, TONGUE, EYESCLOSED, RoarForYou
CALF, BEAR, BULL, RAINBOWPUKE, ROSE, HEART, PARTY, LIPS
BEER, CAKE, GIFT, CUCUMBER, Drumstick, Pepper, CANDIEDHAWS, BubbleTea
Coffee, Yes, No, OKR, CheckMark, CrossMark, MinusOne, Hundred
AWESOMEN, Pin, Alarm, Loudspeaker, Trophy, Fire, BOMB, Music
XmasTree, Snowman, XmasHat, FIREWORKS, 2022, REDPACKET, FORTUNE, LUCK
FIRECRACKER, StickyRiceBalls, HEARTBROKEN, POOP, StatusFlashOfInspiration, 18X, CLEAVER, Soccer
Basketball, GeneralDoNotDisturb, Status_PrivateMessage, GeneralInMeetingBusy, StatusReading, StatusInFlight, GeneralBusinessTrip, GeneralWorkFromHome
StatusEnjoyLife, GeneralTravellingCar, StatusBus, GeneralSun, GeneralMoonRest, MoonRabbit, Mooncake, JubilantRabbit
TV, Movie, Pumpkin, BeamingFace, Delighted, ColdSweat, FullMoonFace, Partying
GoGoGo, ThanksFace, SaluteFace, Shrug, ClownFace, HappyDragon
```

## References

- [lark-im](../SKILL.md) - all IM commands
- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters
- Official emoji doc: `https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message-reaction/emojis-introduce`
references/lark-im-threads-messages-list.md
# im +threads-messages-list

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules.

Fetch the reply message list inside a thread. When `im +chat-messages-list` returns messages that include a `thread_id` field, use this command to inspect all replies in that thread.

By default each reply also carries a `reactions` block (counts + details from `im.reactions.batch_query`) when the server has reactions for it, and `update_time` for messages that were actually edited. Pass `--no-reactions` to skip the extra round-trip. Pass `--download-resources` to additionally download message resources (image/file/audio/video/media + post-embedded, excluding stickers) into `./lark-im-resources/` and attach a `resources` block — off by default, no extra requests when omitted. See [message enrichment](lark-im-message-enrichment.md) for the full contract.

This skill maps to the shortcut: `lark-cli im +threads-messages-list` (internally calls `GET /open-apis/im/v1/messages` with `container_id_type=thread` to fetch thread messages).

## Commands

```bash
# Get thread replies (ascending by time by default, table output)
lark-cli im +threads-messages-list --thread omt_xxx

# Reverse chronological order (latest first)
lark-cli im +threads-messages-list --thread omt_xxx --order desc

# Control page size
lark-cli im +threads-messages-list --thread omt_xxx --page-size 20

# Pagination
lark-cli im +threads-messages-list --thread omt_xxx --page-token <PAGE_TOKEN>

# Fetch multiple pages automatically, up to 10 pages by default
lark-cli im +threads-messages-list --thread omt_xxx --page-all

# Output format options
lark-cli im +threads-messages-list --thread omt_xxx --format pretty
lark-cli im +threads-messages-list --thread omt_xxx --format table
lark-cli im +threads-messages-list --thread omt_xxx --format csv

# View as a bot
lark-cli im +threads-messages-list --thread omt_xxx --as bot

# Preview the request without executing it
lark-cli im +threads-messages-list --thread omt_xxx --dry-run
```

## Parameters

| Parameter | Required | Description |
|------|------|------|
| `--thread <id>` | Yes | Thread ID (`om_xxx` or `omt_xxx` format) |
| `--no-reactions` | No | Skip auto-fetching the `reactions` block |
| `--download-resources` | No | Download message resources (image/file/audio/video/media + post-embedded, excluding stickers) into `./lark-im-resources/` and attach a `resources` block. Off by default |
| `--order <order>` | No | Sort order: `asc` (default) / `desc` |
| `--page-size <n>` | No | Number of items per page (default 50, range 1-50) |
| `--page-token <token>` | No | Starting cursor, normally returned by a previous response |
| `--page-all` | No | Automatically fetch and merge subsequent pages; capped by `--page-limit` |
| `--page-limit <n>` | No | Maximum pages fetched by `--page-all` (default 10, range 1-1000) |
| `--format <fmt>` | No | Output format: `json` (default) / `pretty` / `table` / `ndjson` / `csv` |
| `--as <identity>` | No | Identity type: `user` (default) / `bot` |
| `--dry-run` | No | Print the request only, do not execute it |

## Core Constraints

### 1. Source of `thread_id`

`thread_id` (`omt_xxx` or `om_xxx`) comes from the `thread_id` field in results returned by `im +chat-messages-list` or `im +messages-search`. Do not guess a thread ID. Fetch messages first and use the returned value.

### 2. No time filtering support

Thread messages do not support `start_time` / `end_time` filtering because of Feishu API limitations. Use pagination and sort order to control the scope.

### 3. Pagination (`has_more` / `page_token`)

Default is one page. With `--page-all`, `--page-token` sets the starting cursor; if `meta.pagination.complete=false`, resume from `meta.pagination.next_token` or raise `--page-limit`.

### 4. Recommended expansion strategy

| Scenario | Recommended Parameters |
|------|---------|
| Quickly inspect recent replies | `--order desc --page-size 10` |
| Read the full thread in chronological order | `--order asc --page-size 50`, then paginate as needed |
| Just confirm whether replies exist | `--order desc --page-size 1` |

## Usage Scenarios

### Scenario 1: Expand a thread discovered in group messages

```bash
# Step 1: Fetch group messages and find one that contains thread_id
lark-cli im +chat-messages-list --chat-id oc_xxx

# Step 2: Extract thread_id from the JSON output and fetch thread replies
lark-cli im +threads-messages-list --thread omt_xxx
```

### Scenario 2: Paginate through a long thread

```bash
# First page
lark-cli im +threads-messages-list --thread omt_xxx

# If has_more=true is returned, continue with page_token
lark-cli im +threads-messages-list --thread omt_xxx --page-token <PAGE_TOKEN>
```

## Resource Rendering

Thread replies are rendered into human-readable text. Image messages appear as placeholders such as `![Image](img_xxx)`; by default resource binaries are **not** downloaded.

Pass `--download-resources` to download every eligible resource (image/file/audio/video/media + post-embedded, excluding stickers) into `./lark-im-resources/` in one pass and attach a `resources` block to each reply (see [message enrichment](lark-im-message-enrichment.md#resource-auto-download---download-resources-opt-in)). Otherwise download individual resources manually through `im +messages-resources-download` (see [lark-im-messages-resources-download](lark-im-messages-resources-download.md)).

## Common Errors and Troubleshooting

| Symptom | Root Cause | Solution |
|---------|---------|---------|
| "Invalid thread ID format" | `thread_id` does not start with `om_` or `omt_` | Use a valid `om_xxx` or `omt_xxx` value |
| Empty thread result | Wrong thread_id or no replies in the thread | Confirm the thread_id came from `im +chat-messages-list` output |
| Permission denied | The user is not authorized or is not a conversation member | Make sure OAuth authorization is complete and the identity is a chat member |

## References

- [lark-im](../SKILL.md) - all message-related commands
- [lark-im-chat-messages-list](lark-im-chat-messages-list.md) - fetch conversation messages (source of `thread_id`)
- [lark-shared](../../lark-shared/SKILL.md) - authentication and global parameters
SKILL.md
---
name: lark-im
version: 1.0.0
description: "飞书即时通讯:收发消息和管理群聊。发送和回复消息、搜索聊天记录、管理群聊成员、上传下载图片和文件、管理表情回复、发送应用内/短信/电话加急、发送和处理交互卡片(Interactive Card)、监听卡片按钮回调(card.action.trigger)。当用户需要发消息、查看或搜索聊天记录、下载聊天中的文件、查看群成员、搜索群、创建群聊或话题群、管理标记数据、管理 Feed 置顶(添加/移除/查询置顶会话)、管理标签数据、处理卡片回调时使用。"
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.
- **Flag**: A bookmark on a message or thread.
- **Feed Shortcut**: A chat pinned to the current user's feed sidebar, identified by `feed_card_id` (an `oc_xxx` open_chat_id for CHAT type).
- **Feed Group**: A tag that groups feed cards in the feed list, identified by `feed_group_id` (ofg_xxx). Members are feed cards, each identified by `feed_id` + `feed_type`. Two types: `normal` (members managed explicitly) and `rule` (members auto-derived from rules).

## Resource Relationships

```
Chat (oc_xxx)
├── Message (om_xxx)
│   ├── Thread (reply thread)
│   ├── Reaction (emoji)
│   └── Resource (image / file / video / audio)
└── Member (user / bot)
```

## Important Notes

### AppLink and Share Links

Prefer CLI-returned links: use `chat_app_link` to open joined conversations, `message_app_link` to open messages, and `share_link` to invite others to groups. If manually building a joined-conversation AppLink, use `https://<applink_host>/client/chat/open?openChatId=<oc_xxx>`, never `chatId=<oc_xxx>` or `lark://...chat_id=<oc_xxx>`.

### 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

When fetching messages (`+chat-messages-list`, `+threads-messages-list`, `+messages-mget`, `+messages-search`), the CLI shows a display name for both user and bot senders:

- **Server-provided name**: the read APIs return `sender_name` (plus the full-i18n `sender_i18n_names` map) on each message `sender`; the CLI surfaces it as the sender's `name` for users and bots alike. No name lookup and no extra permission are needed — **no contact scope** and no `application:bot.basic_info:read`.
- **Fallback to id**: when the server does not provide a name, the sender is shown by its id and the command still exits 0. There is no contact-directory fallback.

The raw `sender_name` is not duplicated in output (its value is in `name`); the full `sender_i18n_names` map (all locales) is preserved for consumers that need a specific language, alongside an optional `open_bot_id` (`ou_`) for bot senders aligned with the message-receive event channel. System messages (`msg_type: system`) have no sender name — that is normal, not an error.

### Default message enrichment (reactions / update_time)

The four message-pulling shortcuts (`+messages-mget`, `+chat-messages-list`, `+messages-search`, `+threads-messages-list`) automatically attach a `reactions` block and (for edited messages) `update_time` to each returned message — no separate `im.reactions.batch_query` call is needed. Pass `--no-reactions` to opt out. For the full contract (output shape, the `im:message.reactions:read` scope requirement, and the "missing field ≠ fetch failure" data rules), read [`references/lark-im-message-enrichment.md`](references/lark-im-message-enrichment.md).

### Opt-in resource auto-download (`--download-resources`)

`+chat-messages-list`, `+messages-mget`, and `+threads-messages-list` accept `--download-resources` to save eligible attachments into `./lark-im-resources/` and add a `resources` array to each message. It is off by default; stickers are not downloadable. A failed attachment is reported on that resource without aborting the message pull. Use [`+messages-resources-download`](references/lark-im-messages-resources-download.md) for one attachment. See [`references/lark-im-message-enrichment.md`](references/lark-im-message-enrichment.md) for the output contract.

### Card Messages (Interactive)

**Before sending, replying with, or updating any `interactive` card (`+messages-send` / `+messages-reply` / `messages.patch`), you MUST read [`references/card/lark-im-card-create.md`](references/card/lark-im-card-create.md) and follow its workflow.** The card JSON passed to `--msg-type interactive --content` (send/reply) or `messages.patch --data` (update) must be the output of that workflow — never hand-write or copy a card payload.

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.

`interactive` cards support callback events (`card.action.trigger`) — see [`references/lark-im-card-action-reply.md`](references/lark-im-card-action-reply.md).

### Audio Messages

`--audio` sends a voice message and supports only Opus audio files, for example `.opus` files or Ogg Opus (`.ogg`) files. For `mp3`, `wav`, or other non-Opus audio, either convert to `.opus` first and keep using `--audio`, or send the original file as an attachment with `--file`.

### Sending Doc Content as a Message

When sending content fetched from a Lark doc as a message, fetch the doc with --doc-format im-markdown, then send it as a message using the --markdown format. The fetched content is already in markdown; in any content-forwarding scenario, keep the fetched original text and send it in the --markdown format. Note: if the doc contains a cite tag with type="user", keep it as-is and do not strip the tag.

### Flag Types

Flags support two layers:

- **Message-layer flag**: `(ItemTypeDefault, FlagTypeMessage)` — regular message bookmark
- **Feed-layer flag**: `(ItemTypeThread/ItemTypeMsgThread, FlagTypeFeed)` — thread as feed-layer bookmark

Item types for feed-layer flags:
- **ItemTypeThread** (4) = thread in a topic-style chat
- **ItemTypeMsgThread** (11) = thread in a regular chat

### Feed Shortcut

Feed shortcuts add chats to the current user's feed sidebar. They are distinct from flags:

- **Flag** = bookmark on a message/thread, scoped to the user's bookmark list.
- **Feed shortcut** = entry in the user's feed sidebar (currently only chats).

Key limits:
- Only **CHAT-type** (`feed_card_id` is `oc_xxx`) is exposed via OpenAPI; doc/app/subscription shortcuts exist internally but are not yet whitelisted.
- All three operations (create/remove/list) are **user-identity only** — they sign with `user_access_token`.
- Batch size is **10 per call** for create/remove; list is a one-page wrapper with opaque `page_token` pagination.

## Shortcuts(推荐优先使用)

Shortcut 是对常用操作的高级封装(`lark-cli im +<verb> [flags]`)。有 Shortcut 的操作优先使用。

| Shortcut | 说明 |
|----------|------|
| [`+chat-create`](references/lark-im-chat-create.md) | Create a group chat or topic chat; user/bot; --chat-mode group|topic; private/public; invites users/bots; optionally sets bot manager |
| [`+chat-list`](references/lark-im-chat-list.md) | List chats the current user/bot is a member of; defaults to groups; pass --types=p2p,group to include p2p single chats (user-only); user/bot; supports sorting, auto-pagination, --exclude-muted (user-only) |
| [`+chat-members-list`](references/lark-im-chat-members-list.md) | List members of a chat; returns separate users[] / bots[] buckets; callable as user or bot; --member-types filters which kinds to return; --page-all pagination; surfaces truncations[] when the server caps a bucket |
| [`+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, --order asc/desc sorting, auto-pagination |
| [`+chat-search`](references/lark-im-chat-search.md) | Search visible group chats by --query keyword and/or --member-ids; user/bot; e.g. look up chat_id by group name; supports type filters, sorting, auto-pagination, and --exclude-muted (user identity only) |
| [`+chat-update`](references/lark-im-chat-update.md) | Update group chat name or description; user/bot; updates a chat's name or description |
| [`+message-read-users`](references/lark-im-message-read-status.md) | List users who read one message; user/bot; identity-specific scopes; supports bounded auto-pagination |
| [`+messages-edit`](references/lark-im-messages-edit.md) | Edit a message's content (text/post, including the attachment zone); bot-only (user identity is rejected by the server); PUT /open-apis/im/v1/messages/:message_id |
| [`+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-read-status`](references/lark-im-message-read-status.md) | Batch query whether the current user read 1–50 messages; user-only; returns readable items and invalid message IDs |
| [`+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 an image or file attached to a message; user/bot |
| [`+messages-search`](references/lark-im-messages-search.md) | Search messages across chats (supports keyword, sender, time range filters) with user or bot identity; 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 --order asc/desc sorting, auto-pagination |
| [`+flag-create`](references/lark-im-flag-create.md) | Create a bookmark on a message; user-only; defaults to message-layer flag; use --flag-type feed for feed-layer flag (item_type auto-detected from chat mode) |
| [`+flag-cancel`](references/lark-im-flag-cancel.md) | Cancel (remove) a bookmark. When no --flag-type is given, best-effort double-cancel: removes message layer and (when chat_type is determinable) feed layer |
| [`+flag-list`](references/lark-im-flag-list.md) | List bookmarks; user-only; auto-enriches feed-type thread entries with message content; `--page-all` is capped by `--page-limit` (default 20, max 1000), and `has_more=true` means the result is incomplete |
| [`+feed-shortcut-create`](references/lark-im-feed-shortcut-create.md) | Add chats to the user's feed shortcuts; user-only; oc_xxx chat IDs only; batch up to 10 per call; `--head`/`--tail` controls insertion order; partial failures return an `ok:false` ledger |
| [`+feed-shortcut-remove`](references/lark-im-feed-shortcut-remove.md) | Remove chats from the user's feed shortcuts; user-only; batch up to 10 per call; removing an absent shortcut is idempotent success; real per-item failures return an `ok:false` ledger |
| [`+feed-shortcut-list`](references/lark-im-feed-shortcut-list.md) | List one page of the user's feed shortcuts; user-only; omit `--page-token` for the first page; default output enriches CHAT entries under `detail`; pass `--no-detail` to skip the extra lookup and `im:chat:read` scope |
| [`+feed-group-list`](references/lark-im-feed-group-list.md) | List the caller's feed groups (tags); user-only; supports `--page-all` auto-pagination |
| [`+feed-group-list-item`](references/lark-im-feed-group-list-item.md) | List feed cards in a feed group (tag); user-only; enriches each item with chat_name resolved from feed_id; supports --page-all auto-pagination |
| [`+feed-group-query-item`](references/lark-im-feed-group-query-item.md) | Look up specific feed cards in a feed group (tag) by ID; user-only; enriches each item with chat_name resolved from feed_id |

## API Resources

```bash
lark-cli schema im.<resource>.<method>   # 调用 API 前必须先查看参数结构
lark-cli im <resource> <method> [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.
  - `update` — 更新群信息。Identity: supports `user` and `bot`.

### chat.members

  - `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.

### chat.user_setting

  - `batch_query` — 批量查询当前用户在群内的个人偏好设置 (e.g. `is_muted` mutes normal messages, `is_mute_at_all` mutes @all messages); up to 10 chats per request. Identity: `user` only (`user_access_token`); the caller must be in each target chat.
  - `batch_update` — 批量更新当前用户在群内的个人偏好设置 (e.g. `is_muted` mutes normal messages, `is_mute_at_all` mutes @all messages); up to 10 chats per request. Identity: `user` only (`user_access_token`); the caller must be in each target chat.

### chat.nickname

  - `get` — 获取自己的群昵称。Get your own nickname in the chat (self-only). Identity: `user` only (`user_access_token`); returns an empty string when no nickname is set.
  - `update` — 设置自己的群昵称。Set or update your own nickname in the chat (self-only). Identity: `user` only (`user_access_token`); `nickname` must be a non-empty string (max 300 bytes). Use DELETE to clear it.
  - `delete` — 清空自己的群昵称。Clear your own nickname in the chat (self-only). Identity: `user` only (`user_access_token`).

### chat.join_requests

  - `list` — 列出群的待审批入群申请(仅群主/管理员,user_access_token)。List pending join requests for a chat. Identity: `user` only (`user_access_token`); the caller must be the chat owner or an admin. Paginated (`page_size` 1-100); stop on `has_more == false` — `page_token` is returned even on the last page, so paging while it is present never terminates.
  - `handle` — 批量审批入群申请(approve/reject,仅群主/管理员,user_access_token)。Approve or reject pending join requests in bulk (1-50 items, processed in order). Identity: `user` only (`user_access_token`); the caller must be the chat owner or an admin. `results[]` mirrors `items[]` in count and order — check each `result` (`success` / `failed` / `already_handled`); exit 0 does not mean every item succeeded.

### chat.managers

  - `add_managers` — 指定群管理员。Identity: supports `user` and `bot`; only the group owner can add managers; max 10 managers per chat (20 for super-large chats), and at most 5 bots per request.
  - `delete_managers` — 删除群管理员。Identity: supports `user` and `bot`; only the group owner can remove managers; max 50 users or 5 bots per request.

### chat.moderation

  - `get` — 获取群成员发言权限。Identity: supports `user` and `bot`; the caller must be in the target chat and belong to the same tenant.
  - `update` — 更新群发言权限。Identity: supports `user` and `bot`; only the group owner (or creator bot with `im:chat:operate_as_owner`) can update; the caller must be in the chat.

### messages

  - `read_status` — 批量查询当前用户对消息的已读状态。Identity: `user` only (`user_access_token`); accepts up to 50 message IDs and returns readable items plus invalid message IDs.[Must-read](references/lark-im-message-read-status.md)
  - `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: supports `user` and `bot`.
  - `merge_forward` — 合并转发消息。Identity: `bot` only (`tenant_access_token`).
  - `read_users` — 查询消息已读信息。Identity: supports `user` and `bot`; the caller must still be in the chat. A user can query messages they sent within the last 7 days, while a bot can query only messages sent by that bot within the last 7 days.[Must-read](references/lark-im-message-read-status.md)
  - `patch` — 更新已发送的消息卡片。Update an interactive message card sent by the app. Identity: supports `user` and `bot`; the message must have been sent within the last 14 days, and `content` must be a JSON-serialized string no larger than 30 KB.[Must-read](references/card/lark-im-card-create.md)
  - `urgent_app` — 发送应用内加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.
  - `urgent_phone` — 发送电话加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.
  - `urgent_sms` — 发送短信加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.

### 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)

### threads

  - `forward` — 转发话题。Identity: supports `user` and `bot`.

### images

  - `create` — 上传图片。Identity: supports `user` and `bot`; user identity requires `im:resource` scope on the UAT.

### files

  - `create` — 上传文件。Identity: supports `user` and `bot`; user identity requires `im:resource` scope on the UAT.

### pins

  - `create` — Pin 消息。Identity: supports `user` and `bot`.
  - `delete` — 移除 Pin 消息。Identity: supports `user` and `bot`.
  - `list` — 获取群内 Pin 消息。Identity: supports `user` and `bot`.

### feed.groups

  - `batch_add_item` — Batch add feed cards to a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
  - `batch_query` — Batch query feed groups. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
  - `batch_remove_item` — Batch remove feed cards from a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
  - `create` — Create a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
  - `delete` — Delete a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
  - `update` — Update a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)

## 权限表

| 方法 | 所需 scope |
|------|-----------|
| `chats.create` | `im:chat:create` |
| `chats.get` | `im:chat:read` |
| `chats.link` | `im:chat:read` |
| `chats.update` | `im:chat:update` |
| `chat.members.create` | `im:chat.members:write_only` |
| `chat.members.delete` | `im:chat.members:write_only` |
| `chat.members.get` | `im:chat.members:read` |
| `+chat-members-list` | `im:chat.members:read` |
| `chat.user_setting.batch_query` | `im:chat.user_setting:read` |
| `chat.user_setting.batch_update` | `im:chat.user_setting:write` |
| `chat.managers.add_managers` | `im:chat.managers:write_only` |
| `chat.managers.delete_managers` | `im:chat.managers:write_only` |
| `chat.moderation.get` | `im:chat.moderation:read` |
| `chat.moderation.update` | `im:chat:moderation:write_only` |
| `chat.join_requests.list` | `im:chat.membership_application:read` |
| `chat.join_requests.handle` | `im:chat.membership_application:write` |
| `+messages-read-status` | user: `im:message:readonly` (recommended), `im:message`, or `im:message:get_as_user` |
| `+message-read-users` | user: `im:message:readonly` (recommended), `im:message`, `im:message:basic`, or `im:message:get_as_user`; bot: `im:message:readonly` |
| `messages.read_status` | `im:message:readonly` (recommended), `im:message`, or `im:message:get_as_user` |
| `messages.delete` | `im:message:recall` |
| `messages.forward` | `im:message` |
| `messages.merge_forward` | `im:message` |
| `messages.read_users` | user: `im:message:readonly` (recommended), `im:message`, `im:message:basic`, or `im:message:get_as_user`; bot: `im:message:readonly` |
| `messages.patch` | `im:message:update` |
| `messages.urgent_app` | `im:message.urgent` |
| `messages.urgent_phone` | `im:message.urgent:phone` |
| `messages.urgent_sms` | `im:message.urgent:sms` |
| `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` |
| `threads.forward` | `im:message` |
| `images.create` | `im:resource` |
| `files.create` | `im:resource` |
| `pins.create` | `im:message.pins:write_only` |
| `pins.delete` | `im:message.pins:write_only` |
| `pins.list` | `im:message.pins:read` |
| `feed.groups.batch_add_item` | `im:feed_group_v1:write` |
| `feed.groups.batch_query` | `im:feed_group_v1:read` |
| `feed.groups.batch_remove_item` | `im:feed_group_v1:write` |
| `feed.groups.create` | `im:feed_group_v1:write` |
| `feed.groups.delete` | `im:feed_group_v1:write` |
| `feed.groups.update` | `im:feed_group_v1:write` |