# Lark Event

> 飞书/Lark 实时事件监听/订阅/消费：通过 `lark-cli event consume EventKey` 以 NDJSON 流式输出事件（覆盖 IM 消息/表情/群聊变更、任务更新、视频会议开始/入会/结束、妙记生成、画板更新等）。适用于飞书机器人、实时消息处理、长时订阅者、流式 webhook/推送处理。支持 `--max-events` / `--timeout` 有界运行，以及 stderr ready-marker 约定——面向以子进程方式运行的 AI agent。

- Skill: `wecode-ai/lark-event` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add wecode-ai/lark-event`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wecode-ai/lark-event/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: wecode-ai (https://skillmd.com/u/wecode-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/wecode-ai/lark-event

---


> 事件订阅需要本地常驻总线，请在来源设备使用应用身份 `--as bot` 执行。云端仅支持 `event list` 和 `event schema`，不要在云端启动 consume、stop、status 或后台总线。

## Wegent 本地与云端运行

- 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前，macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`；Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。
- 下文的 `lark-cli ...` 是逻辑命令。实际执行时，macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`；Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。
- 应用配置和首次用户 OAuth 在本机原连接入口完成；Wegent 在后台托管用户 OAuth（`lark`）及应用凭据（`lark-app`）。云端准备脚本只检查托管认证，不安装 CLI 或发起登录。实际命令必须经过上述包装器，不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`，应用调用显式 `--as bot`，两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入，不使用 stdin。托管状态失效或需要增量授权时，在本机原连接入口重新连接；不要在云端执行 auth/config/profile 命令。

# Lark Events

> **Prerequisite:** Read [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md) first for authentication, `--as user/bot` switching, `Permission denied` handling, and safety rules.

## Core commands

| Command | Purpose |
|------|------|
| `lark-cli event list [--json]` | List all subscribable EventKeys |
| `lark-cli event schema <EventKey> [--json]` | Show an EventKey's params and output schema |
| `lark-cli event consume <EventKey> [flags]` | Blocking consume; events → stdout NDJSON |
| `lark-cli event status [--json] [--fail-on-orphan]` | Inspect the local bus daemon status |
| `lark-cli event stop [--all] [--force]` | Stop the bus daemon |


## Common flags

| Flag | Description |
|---|---|
| `--param key=value` / `-p` | Business params (repeatable; comma-separated for multi-value). Unknown keys fail with valid names listed inline |
| `--jq <expr>` | jq expression to filter / transform each event; empty output skips the event |
| `--max-events N` | Exit after N events. Default 0 = unlimited |
| `--timeout D` | Exit after duration D (e.g. `30s`, `2m`). Default 0 = no timeout. Whichever of `--max-events` / `--timeout` fires first wins |
| `--output-dir <dir>` | Write each event as a file (relative paths only; prevents traversal) |
| `--quiet` | Suppress stderr diagnostics. **AI should not use this** — it silences the ready marker |
| `--as user\|bot\|auto` | Identity for the session (see lark-shared) |


## Examples

```bash
# Default: stream every event for the key (no filter, no projection)
lark-cli event consume im.message.receive_v1 --as bot

# Grab one sample event to inspect payload shape
lark-cli event consume im.message.receive_v1 --max-events 1 --timeout 30s --as bot

# Run for 10 minutes then auto-exit
lark-cli event consume im.message.receive_v1 --timeout 10m --as bot

# Consume multiple EventKeys concurrently using the host executor's background-process support.
# Start each logical command as its own process and capture each stdout stream separately.
lark-cli event consume im.message.receive_v1 --as bot
lark-cli event consume im.message.reaction.created_v1 --as bot

```

## Call flow

1. `lark-cli event list --json` → pick a legal key
2. `lark-cli event schema <key> --json` → read `resolved_output_schema` + `jq_root_path` to determine field paths
3. `lark-cli event consume <key> [--jq '<expr>']` → consume

## Subprocess contract

### Ready marker

`event consume`'s stderr emits a fixed line `[event] ready event_key=<key>`. **Parent processes should block on stderr until this line appears, then start reading stdout.** Do not fall back to `sleep`.

### stdin EOF = graceful exit

`event consume` treats stdin close as a shutdown signal (wired for AI subprocess callers). **Bounded runs are exempt: when `--max-events` or `--timeout` is set (> 0), stdin EOF is ignored and the run exits only via its own bound, timeout, or SIGTERM.** For unbounded runs, `< /dev/null` / `nohup` / systemd's default `StandardInput=null` will cause an immediate graceful exit (stderr `reason: signal`). To keep an unbounded run alive:

- Feed stdin a source that never EOFs: `< <(tail -f /dev/null)`
- Or run bounded: `--max-events N` / `--timeout D`

### Exit codes & reason

On exit, the last stderr line is `[event] exited — received N event(s) in Xs (reason: ...)`.

| exit code | reason | Trigger |
|---|---|---|
| 0 | `reason: limit` | `--max-events` reached |
| 0 | `reason: timeout` | `--timeout` reached |
| 0 | `reason: signal` | Ctrl+C / SIGTERM / stdin EOF (stdin EOF applies to unbounded runs only) |
| 1 | JSON error envelope on stderr | Lark API business failure during pre-consume setup (for example subscription create/delete) |
| 2 | JSON error envelope on stderr (no `exited` line) | Validation failure (unknown EventKey, bad `--param` / `--jq`, another bus already connected) |
| 3 | JSON error envelope on stderr | Auth failure (missing token, missing scopes) |
| 4 / 5 | JSON error envelope on stderr | Network / internal failure (bus startup, handshake, file I/O) |

Startup and runtime failures emit a structured JSON envelope on stderr: `{"ok":false,"error":{"type","subtype","param","message","hint",...}}` (the envelope may also carry top-level `identity` / `_notice` siblings). Parse `error.type` / `error.subtype` to branch (e.g. `missing_scope` carries a `missing_scopes` list), `error.param` to find the offending flag, and `error.hint` for the recovery action — do not regex-match message text.

Orchestrators should treat `reason: limit/timeout/signal` (all exit 0) as "business completion" and non-zero as "failure".

### Never `kill -9`

**Avoid `kill -9` on consume processes**: for EventKeys with a **PreConsume hook** (those that register server-side subscriptions via OAPI), `kill -9` skips the OAPI unsubscribe and leaks server-side subscriptions (symptoms: "subscription already exists" on restart, duplicate event delivery). Prefer SIGTERM or closing stdin.

### One consume, one EventKey (multi-key = multi-shell)

The command takes exactly one positional argument; `k1,k2` and wildcards are unsupported. Listening to N keys means N subprocesses — this is **intentional**:

- One shape per process stdout; no dispatcher logic required in the AI
- Fault isolation (one key failing doesn't affect others)
- Independent `--as` / `--jq` / `--max-events` / `--timeout` per key

All N consumers share a single bus daemon (UDS local IPC), so the overhead is small

## Writing jq via schema

`event schema <key> --json` is the source of truth for writing `--jq`. Four things to look at:

**(1) Where fields start** — see `jq_root_path`

- Value `"."` → fields are at the top level, write `.chat_id`
- Value `".event"` → fields are inside a V2 envelope, write `.event.chat_id`

**(2) Field list and types** — see `resolved_output_schema.properties.<name>`

Each field carries `type` / `description`, and some also have `format`. Snippet (from `event schema im.message.receive_v1 --json`):

```json
{
  "chat_id":     {"type":"string", "format":"chat_id",      "description":"Chat ID, prefixed with oc_"},
  "sender_id":   {"type":"string", "format":"open_id",      "description":"Sender open_id, prefixed with ou_"},
  "create_time": {"type":"string", "format":"timestamp_ms", "description":"Send time as ms-epoch string"}
}
```

**(3) Field semantics** — see the `format` tag

Lark-defined semantic tags (**not** JSON Schema's standard `format`). Common values: `open_id` / `chat_id` / `message_id` / `timestamp_ms` / `email`. Purpose: distinguish "same string type, different meanings" fields so you can reverse-lookup via API or convert formats.

**(4) Decoded state** — read the field's `description`

`event consume` runs Process hooks that may pre-decode some payload fields (flattening V2 envelopes, rendering `.content` to plain text, etc.) — behavior differs from raw OAPI. **Always read the field's `description` before writing jq**, especially for generic field names like `content` / `data` / `body` / `payload`.

**Why it matters**: blindly applying `fromjson` to an already-decoded text field makes jq error on every event and silently drop it — the consumer looks alive but emits nothing, with only a single `WARN` line buried on stderr. (This is the general behavior: any jq runtime error skips the event with a one-line WARN; the loop does not abort.)

**Don't shortcut the schema**: when projecting `event schema --json` with jq, do not strip `.description` from `properties` — that's the field that tells you whether a field is already decoded. Dump the full property objects, not just keys.

---

**Aside**: `--param`'s valid parameters also live in the schema — the `params` section lists `name` / `type` / `required` / `enum` / `default` / `description`; **section missing = this key accepts no `--param`**.

## Topic index

| Topic      | Reference                                                                    | Coverage |
|------------|------------------------------------------------------------------------------|---|
| IM         | [`references/lark-event-im.md`](references/lark-event-im.md)                 | Catalog of 12 IM EventKeys + shape notes (flat vs V2 envelope) + `im.message.receive_v1` field gotchas (`sender_id` is open_id only; `.content` is plain text except for `interactive` cards) + common jq recipes (filter by chat_type / message_type / sender); for `card.action.trigger` see also [`../lark-im/references/lark-im-card-action-reply.md`](../lark-im/references/lark-im-card-action-reply.md) |
| Task       | [`references/lark-event-task.md`](references/lark-event-task.md)             | Catalog of 1 Task EventKey (`task.task.update_user_access_v2`) + Native V2 envelope shape + task commit types + user/bot subscription notes |
| VC         | [`references/lark-event-vc.md`](references/lark-event-vc.md)                 | Catalog of 4 VC EventKeys (`vc.meeting.participant_meeting_started_v1`, `vc.meeting.participant_meeting_joined_v1`, `vc.meeting.participant_meeting_ended_v1`, `vc.note.generated_v1`) + field reference + source type semantics (meeting only) |
| Minutes    | [`references/lark-event-minutes.md`](references/lark-event-minutes.md)       | Catalog of 1 Minutes EventKey (`minutes.minute.generated_v1`) + field reference + source type semantics (meeting only) |
| Whiteboard | [`references/lark-event-whiteboard.md`](references/lark-event-whiteboard.md) | Catalog of 1 Board EventKey (`board.whiteboard.updated_v1`) + per-whiteboard subscription model (requires `-p whiteboard_id=<token>`) + payload field reference (whiteboard_id / operator_ids triple-id) |

