docs(event): slim lark-event references to recipes + gotchas; CLI owns structure

- references (im/vc/minutes/whiteboard): drop key catalogs, field/scope tables
  and other content already emitted by `event list`/`event schema`; keep only
  jq recipes, domain semantic gotchas, required params, and per-key identity.
  ~55% token cut across the four references (4343 -> 1940, cl100k_base).
- vc: document the 3 previously-undocumented recording keys, then replace the
  hardcoded key catalog with a pointer to `event list` to stop doc-vs-code drift.
- SKILL.md Topic index: reword rows to advertise recipes/gotchas and route
  structure lookups to the CLI, matching the slimmed references.

Validated by a command-correctness eval (skill+CLI available): 21/21 across the
four domains, identity correct per key, whiteboard required `-p whiteboard_id`
preserved; the VC recording-transcript key (previously unanswerable from the
skill alone) now resolves.

Change-Id: I666e9706ae6ef3e2bbcd56a3fb70c4f8be94182c
This commit is contained in:
liuxinyang.lxy
2026-06-13 18:10:25 +08:00
committed by liangshuo-1
parent 5a806febc8
commit cbd729757b
5 changed files with 63 additions and 239 deletions

View File

@@ -147,7 +147,7 @@ Lark-defined semantic tags (**not** JSON Schema's standard `format`). Common val
| Topic | Reference | Coverage |
|------------|------------------------------------------------------------------------------|---|
| IM | [`references/lark-event-im.md`](references/lark-event-im.md) | Catalog of 11 IM EventKeys + shape notes (flat vs V2 envelope) + `im.message.receive_v1` field gotchas (`sender_id` is open_id only; `.content` is plain text except for `interactive` cards) + common jq recipes (filter by chat_type / message_type / sender) |
| VC | [`references/lark-event-vc.md`](references/lark-event-vc.md) | Catalog of 2 VC EventKeys (`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) |
| IM | [`references/lark-event-im.md`](references/lark-event-im.md) | jq recipes (filter by chat_type / message_type / sender) + payload gotchas (`.content` is pre-rendered text — don't `fromjson`; `sender_id` is open_id; flat vs `.event` envelope) |
| VC | [`references/lark-event-vc.md`](references/lark-event-vc.md) | jq recipes (meeting-ended / note / transcript) + behavior gotchas (time conversion, note_source meeting-only, recording batches). All VC keys need `--as user` |
| Minutes | [`references/lark-event-minutes.md`](references/lark-event-minutes.md) | jq recipes + enrichment gotcha (`title` may be empty on detail-API failure; `minute_source` meeting-only). `--as user` |
| Whiteboard | [`references/lark-event-whiteboard.md`](references/lark-event-whiteboard.md) | jq recipes + subscription gotcha (**required** `-p whiteboard_id`, needs manage access or 403). `--as user\|bot` |

View File

@@ -2,85 +2,45 @@
> **Prerequisite:** Read [`../SKILL.md`](../SKILL.md) first for the `event consume` essentials (commands, subprocess contract, jq usage).
>
> **Heads-up for AI agents**: this key's `.content` is **NOT** the raw OAPI payload shape your training data may suggest. `lark-cli` runs a Process hook (`convertlib`) that flattens the V2 envelope and **pre-renders** `.content` to human-readable text for `text` / `post` / `image` / `file` / `audio` / etc. Only `interactive` (cards) keeps the raw JSON string. Don't blindly `fromjson`.
> **The catalog lives in the CLI, not here.** `lark-cli event list` lists all 11 IM EventKeys; `lark-cli event schema <key>` gives any key's fields / types / enums. This file only covers what the schema can't: payload-shape gotchas and ready-to-use jq recipes.
## Key catalog (11)
## Shape: flat vs enveloped
| EventKey | Purpose |
|---|---|
| `im.message.receive_v1` | Receive IM messages |
| `im.message.message_read_v1` | User read a bot's **p2p** message (group messages don't fire this) |
| `im.message.reaction.created_v1` | Reaction added to a message |
| `im.message.reaction.deleted_v1` | Reaction removed from a message |
| `im.chat.updated_v1` | Chat settings changed (owner, avatar, name, permissions, etc.) |
| `im.chat.disbanded_v1` | Chat disbanded |
| `im.chat.member.bot.added_v1` | Bot added to a chat |
| `im.chat.member.bot.deleted_v1` | Bot removed from a chat |
| `im.chat.member.user.added_v1` | User joined a chat (including topic chats) |
| `im.chat.member.user.deleted_v1` | User left voluntarily **or** was removed |
| `im.chat.member.user.withdrawn_v1` | Pending chat invite withdrawn (inviter canceled; user never actually joined) |
`im.message.receive_v1` is the only **flat** key (fields at `.xxx`). The other 10 IM keys are **V2-enveloped** — fields live at `.event.xxx` (e.g. `.event.chat_id`). `event schema <key>` confirms it (its Output Schema nests everything under `event`).
> **Shape**: `im.message.receive_v1` is the only flat key (fields at `.xxx`); the other 10 are V2-enveloped (fields at `.event.xxx`).
## `.content` is pre-rendered — do NOT blindly `fromjson` (`im.message.receive_v1`)
## Gotchas (`im.message.receive_v1`)
`lark-cli` runs a Process hook that **pre-renders `.content` to human-readable text** for every `message_type` except `interactive` (`@mentions` resolved to display names). Only `interactive` (cards) keeps the raw JSON string.
**sender_id is open_id only**: the event payload carries no display name. Call the contact API separately if you need the sender's name.
**`.content` shape depends on `message_type`** (this key uses a flat Custom schema; see [`events/im/message_receive.go`](../../../events/im/message_receive.go)):
| message_type | `.content` shape | How to read |
| message_type | `.content` | How to read |
|---|---|---|
| `text` / `post` / `image` / `file` / `audio` / `sticker` / `share_chat` / `share_user` / `media` / `system` | Human-readable text (convertlib-processed; `@mentions` resolved to display names) | Use `.content` directly |
| `interactive` (card) | Raw card JSON string (structured actions can't be losslessly flattened) | `.content \| fromjson` to get the card object |
| everything except `interactive` | plain text | use `.content` directly |
| `interactive` (card) | raw card JSON string | `.content \| fromjson` |
**Do not blindly `fromjson`** — for non-interactive messages it fails with `jq: fromjson cannot be applied to "hello"` because `.content` isn't JSON-encoded.
Applying `fromjson` to a non-interactive message errors per event (`jq: fromjson cannot be applied to "hello"`) and the consumer **silently drops** it — looks alive, emits nothing.
**`sender_id` is `open_id` only** — the payload carries no display name; resolve via the contact API if you need one.
## jq recipes (`im.message.receive_v1`)
> Default = no `--jq` (stream every message). Use these only when asked to narrow the stream.
```bash
# text: .content is plain text — no fromjson needed
lark-cli event consume im.message.receive_v1 --as bot \
--jq 'select(.message_type=="text") | .content'
# interactive: .content is a JSON string — fromjson to parse
lark-cli event consume im.message.receive_v1 --as bot \
--jq 'select(.message_type=="interactive") | .content | fromjson'
```
## On-demand filter recipes
> **Default = no `--jq`.** Run `lark-cli event consume im.message.receive_v1 --as bot` to see every message. The recipes below are only for cases where the user has asked to narrow the stream.
### 1. Filter by chat type (p2p vs group)
`chat_type` is an enum with values `p2p` / `group`.
```bash
# p2p only (direct messages)
lark-cli event consume im.message.receive_v1 --as bot \
--jq 'select(.chat_type=="p2p") | {from: .sender_id, msg: .content}'
# group only
# group chats only (chat_type enum: p2p | group)
lark-cli event consume im.message.receive_v1 --as bot \
--jq 'select(.chat_type=="group") | {chat: .chat_id, from: .sender_id, msg: .content}'
```
### 2. Filter by message type
```bash
# text only — content is plain human-readable text
# text messages only — .content is plain text
lark-cli event consume im.message.receive_v1 --as bot \
--jq 'select(.message_type=="text") | .content'
# interactive (card) only — parse the card body
# interactive cards only — parse the card body
lark-cli event consume im.message.receive_v1 --as bot \
--jq 'select(.message_type=="interactive") | .content | fromjson'
# one sender's messages only
lark-cli event consume im.message.receive_v1 --as bot \
--jq 'select(.sender_id=="ou_xxxx") | {msg_id: .message_id, text: .content}'
```
### 3. Filter by sender (only one user's messages)
```bash
# example: only messages from the given open_id
lark-cli event consume im.message.receive_v1 --as bot\
--jq 'select(.sender_id=="ou_xxxxxxxxxxxxxxxxxxxxxxxxxx") | {msg_id: .message_id, text: .content}'
```
Get your own open_id via `lark-cli contact +get-user --as user`; other users' via `lark-cli contact +search-user`.
Get your own open_id via `lark-cli contact +get-user --as user`; others' via `lark-cli contact +search-user`.

View File

@@ -1,54 +1,25 @@
# Minutes Events
> **Prerequisite:** Read [`../SKILL.md`](../SKILL.md) first for the `event consume` essentials (commands, subprocess contract, jq usage).
>
> Catalog & fields live in the CLI: `event list` (the one key `minutes.minute.generated_v1`) and `event schema minutes.minute.generated_v1`. This file only covers what the schema can't: enrichment behavior and recipes. **Requires `--as user`.** Flat output (fields at `.xxx`).
## Key catalog (1)
## Enrichment & degradation (the gotcha)
| EventKey | Purpose |
|---|---|
| `minutes.minute.generated_v1` | A minute (妙记) has been generated |
The Process hook calls the minutes detail API to enrich `title`. **If that call fails, `title` is left empty** — the base fields (`type`, `event_id`, `timestamp`, `minute_token`, `minute_source`) are always present. So filter on `.title != ""` if you only want enriched events.
This key uses a **Custom schema** (flat output at `.xxx`) and carries a **PreConsume hook** that auto-subscribes / unsubscribes via OAPI on first / last consumer.
`minute_source` comes from the payload directly (survives enrichment failure) and is **only present when the minute originates from a meeting** (`source_type == "meeting"`); for recording / local-upload sources it is absent.
## Scopes & auth
| EventKey | Scope | Auth |
|---|---|---|
| `minutes.minute.generated_v1` | `minutes:minutes.basic:read` | user |
Requires `--as user`.
## `minutes.minute.generated_v1`
### Output fields
| Field | Type | Description |
|---|---|---|
| `type` | string | Event type; always `minutes.minute.generated_v1` |
| `event_id` | string | Globally unique event ID; safe for deduplication |
| `timestamp` | string (timestamp_ms) | Event delivery time (ms timestamp string) |
| `minute_token` | string | Minute token |
| `title` | string | Minute title (enriched via detail API) |
| `minute_source` | object | Minute source metadata; only present when the source is a meeting |
| `minute_source.source_type` | string | Source type; only present when the source is a meeting (value: `meeting`) |
| `minute_source.source_entity_id` | string | Source entity ID (meeting ID); only present when the source is a meeting |
### Enrichment & degradation
The Process hook calls `GET /open-apis/minutes/v1/minutes/{minute_token}` to enrich `title`. If the detail API fails, this field is left empty — the base fields (`type`, `event_id`, `timestamp`, `minute_token`, `minute_source`) are always present.
`minute_source` is populated from the event payload directly (not the detail API), so it survives enrichment failures. Note: `minute_source` is only present when the minute originates from a meeting; for other sources (e.g. recording, local upload) this field is absent.
### Example
## jq recipes
```bash
lark-cli event consume minutes.minute.generated_v1 --as user
# Project title and token only (skip events where enrichment failed)
# title + token, skipping events where enrichment failed
lark-cli event consume minutes.minute.generated_v1 --as user \
--jq 'select(.title != "") | {minute_token, title}'
# Filter by source type
# meeting-sourced minutes only
lark-cli event consume minutes.minute.generated_v1 --as user \
--jq 'select(.minute_source.source_type == "meeting") | {minute_token, title}'
```

View File

@@ -1,94 +1,27 @@
# VC Events
> **Prerequisite:** Read [`../SKILL.md`](../SKILL.md) first for the `event consume` essentials (commands, subprocess contract, jq usage).
>
> Catalog & fields live in the CLI: `event list` shows the VC keys (meeting-ended, note-generated, recording started / ended / transcript-generated); `event schema <key>` shows each one's fields. This file only covers what the schema can't: behavior gotchas and recipes. **All VC keys require `--as user`.** Flat output (fields at `.xxx`).
## Key catalog (2)
## Behavior gotchas
| EventKey | Purpose |
|---|---|
| `vc.meeting.participant_meeting_ended_v1` | A meeting the current user participates in has ended |
| `vc.note.generated_v1` | A note has been generated (meeting, recording, upload, etc.) |
- **`participant_meeting_ended_v1`**: `start_time` / `end_time` are **not** raw unix seconds — the Process hook converts them to local-timezone RFC3339. If the raw value is empty/non-numeric, the field is left empty. No detail API call; all fields come from the payload.
- **`note.generated_v1`**: fires for meetings *and* recordings/uploads. `note_token` / `verbatim_token` may be empty if detail isn't ready yet. `note_source` (and `note_source.source_entity_id` = meeting ID) is **only present when `source_type == "meeting"`**.
- **`recording.*`**: only fire on Feishu-connected software. `recording_started`/`recording_ended` share `unique_key` (pairs a start with its end). `recording_transcript_generated` carries `transcript_items` as an **array, delivered in batches** — expect multiple events per recording.
Both keys use a **Custom schema** (flat output) and carry a **PreConsume hook** that auto-subscribes / unsubscribes via OAPI on first / last consumer. Both require `--as user`.
## Scopes & auth
| EventKey | Scope | Auth |
|---|---|---|
| `vc.meeting.participant_meeting_ended_v1` | `vc:meeting.meetingevent:read` | user |
| `vc.note.generated_v1` | `vc:note:read` | user |
---
## `vc.meeting.participant_meeting_ended_v1`
### Output fields
| Field | Type | Description |
|---|---|---|
| `type` | string | Event type; always `vc.meeting.participant_meeting_ended_v1` |
| `event_id` | string | Globally unique event ID; safe for deduplication |
| `timestamp` | string (timestamp_ms) | Event delivery time (ms timestamp string) |
| `meeting_id` | string | Meeting ID |
| `topic` | string | Meeting topic |
| `meeting_no` | string | Meeting number |
| `start_time` | string | Meeting start time in RFC3339, converted to the local timezone |
| `end_time` | string | Meeting end time in RFC3339, converted to the local timezone |
| `calendar_event_id` | string | Calendar event ID associated with the meeting |
### Gotchas
- `start_time` / `end_time` are **not** the raw unix-seconds from OAPI — the Process hook converts them to local-timezone RFC3339. If the raw value is empty or non-numeric, the field is left empty.
- No detail API call is made; all fields come from the event payload itself.
### Example
## jq recipes
```bash
lark-cli event consume vc.meeting.participant_meeting_ended_v1 --as user
# Project meeting topic and end time only
# meeting ended: topic + end time
lark-cli event consume vc.meeting.participant_meeting_ended_v1 --as user \
--jq '{meeting: .meeting_id, topic: .topic, ended: .end_time}'
```
---
## `vc.note.generated_v1`
Fires when a note is generated — not just from meetings, but also from realtime recordings and local file uploads.
### Output fields
| Field | Type | Description |
|---|---|---|
| `type` | string | Event type; always `vc.note.generated_v1` |
| `event_id` | string | Globally unique event ID; safe for deduplication |
| `timestamp` | string (timestamp_ms) | Event delivery time (ms timestamp string) |
| `note_id` | string | Note ID |
| `note_token` | string | Note document token; may be empty if detail is not yet available |
| `verbatim_token` | string | Verbatim document token; may be empty if detail is not yet available |
| `note_source` | object | Source metadata; only present when source is a meeting |
| `note_source.source_type` | string | Source type; only present when source is a meeting (value: `meeting`) |
| `note_source.source_entity_id` | string | Source entity ID (meeting ID); only present when source is a meeting |
### Source type semantics
| `source_type` | Trigger |
|---|---|
| `meeting` | Note generated from a meeting |
`note_source` (and its sub-fields) are only populated when `source_type` is `meeting`. For other sources the field is absent.
### Example
```bash
lark-cli event consume vc.note.generated_v1 --as user
# Only notes with enriched tokens, skip incomplete ones
# notes: meeting-sourced only, with enriched tokens
lark-cli event consume vc.note.generated_v1 --as user \
--jq 'select(.note_token != "") | {note_id, note_token, verbatim_token}'
--jq 'select(.note_source.source_type == "meeting" and .note_token != "") | {note_id, note_token, meeting_id: .note_source.source_entity_id}'
# Filter to meeting-sourced notes only
lark-cli event consume vc.note.generated_v1 --as user \
--jq 'select(.note_source.source_type == "meeting") | {note_id, meeting_id: .note_source.source_entity_id}'
# recording transcript: stream speaker + text per line
lark-cli event consume vc.recording.recording_transcript_generated_v1 --as user \
--jq '.transcript_items[] | {speaker: .speaker_name, text}'
```

View File

@@ -1,67 +1,27 @@
# Whiteboard Events
> **Prerequisite:** Read [`../SKILL.md`](../SKILL.md) first for the `event consume` essentials (commands, subprocess contract, jq usage).
>
> One key: `board.whiteboard.updated_v1` (run `event schema` for fields). Supports `--as user` **or** `--as bot`. Output is V2-enveloped — fields at `.event.xxx`.
## Key catalog (1)
## Per-whiteboard subscription (the gotcha)
| EventKey | Purpose |
|---|---|
| `board.whiteboard.updated_v1` | A whiteboard has been edited |
Unlike global keys, this one subscribes **per whiteboard**. **Required param: `-p whiteboard_id=<whiteboard_token>`** — omitting it fails param validation up-front (`required param "whiteboard_id" missing ...`) before any subscription.
This key uses a **Native schema** (V2 envelope; output rooted at `.event`) and carries a **PreConsume hook** that auto-subscribes / unsubscribes via OAPI on first / last consumer.
- Get the token via the docs OAPI [list document blocks](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-v1/document-block/list): the block with `block_type=43` is a whiteboard; its `block.token` is the whiteboard token.
- The caller must have **manage** access to that whiteboard, otherwise the subscribe OAPI returns 403 and `event consume` exits with an auth error **before** listening.
- `.event.operator_ids` is an **array** — multiple collaborators editing in one tick collapse into a single event with multiple entries.
## Scopes & auth
| EventKey | Scope | Auth |
|---|---|---|
| `board.whiteboard.updated_v1` | `board:whiteboard:node:read` | user, bot |
Supports `--as user` or `--as bot`. The caller must have **manage** access to the target whiteboard, otherwise the subscribe OAPI returns 403 and `event consume` exits with an auth error before listening.
## `board.whiteboard.updated_v1`
### Per-whiteboard subscription
Unlike global event keys (e.g. minutes / im), this key subscribes **per whiteboard**: `event consume` calls `POST /open-apis/board/v1/whiteboards/{whiteboard_id}/subscribe` on startup with the `whiteboard_id` you pass via `-p`. **Required parameter**: `-p whiteboard_id=<whiteboard_token>`. Missing this param fails param validation up-front with `required param "whiteboard_id" missing for EventKey board.whiteboard.updated_v1` before any subscription happens.
Whiteboard token can be obtained via the docs OAPI [list document blocks](https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-v1/document-block/list): the block whose `block_type=43` is a whiteboard, and `block.token` is the whiteboard token.
### Output fields (V2 envelope; root path `.event`)
| Field | Type | Description |
|---|---|---|
| `.event.whiteboard_id` | string (kind=whiteboard_id) | Whiteboard token |
| `.event.operator_ids[].open_id` | string (kind=open_id) | Editor's open_id (`ou_` prefix) |
| `.event.operator_ids[].union_id` | string (kind=union_id) | Editor's union_id |
| `.event.operator_ids[].user_id` | string (kind=user_id) | Editor's user_id (only present when the caller's app has the user_id-related contact scope granted by the OAPI side) |
`operator_ids` is an array — multi-user collaborative editing within one tick collapses into a single event with multiple entries.
### Subscription lifecycle
| Phase | Behavior |
|---|---|
| Startup | `event consume` calls `subscribe` OAPI; on success stderr emits `[event] consuming as ...`, `[event] running pre-consume setup...`, `[event] listening for events (key=board.whiteboard.updated_v1)...`, then the AI-facing ready marker `[event] ready event_key=board.whiteboard.updated_v1` |
| Running | Edits to the whiteboard stream as NDJSON to stdout |
| Graceful exit (Ctrl+C / SIGTERM / `--max-events` / `--timeout` / stdin EOF) | `event consume` calls `unsubscribe` OAPI |
| `kill -9` | **Skips unsubscribe → server-side subscription leaks**, may cause `subscription already exists` or duplicate delivery on next consume. See SKILL.md "Never `kill -9`". |
### Example
## jq recipes
```bash
# Stream every edit on whiteboard <token> until Ctrl+C
lark-cli event consume board.whiteboard.updated_v1 \
-p whiteboard_id=<whiteboard_token> \
--as user
# stream every edit on the whiteboard until Ctrl+C
lark-cli event consume board.whiteboard.updated_v1 -p whiteboard_id=<whiteboard_token> --as user
# Sample one event for payload inspection
lark-cli event consume board.whiteboard.updated_v1 \
-p whiteboard_id=<whiteboard_token> \
--as user --max-events 1 --timeout 2m
# sample one event to inspect the payload
lark-cli event consume board.whiteboard.updated_v1 -p whiteboard_id=<whiteboard_token> --as user --max-events 1 --timeout 2m
# Project to "edit summary": who edited which whiteboard
lark-cli event consume board.whiteboard.updated_v1 \
-p whiteboard_id=<whiteboard_token> \
--as user \
--jq '{whiteboard: .event.whiteboard_id, editors: (.event.operator_ids | map(.open_id))}'
# edit summary: who edited which whiteboard
lark-cli event consume board.whiteboard.updated_v1 -p whiteboard_id=<whiteboard_token> --as user \
--jq '{whiteboard: .event.whiteboard_id, editors: (.event.operator_ids | map(.open_id))}'
```