diff --git a/skills/lark-event/SKILL.md b/skills/lark-event/SKILL.md index 62a5dfbf..a9ef3719 100644 --- a/skills/lark-event/SKILL.md +++ b/skills/lark-event/SKILL.md @@ -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=`) + 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` | diff --git a/skills/lark-event/references/lark-event-im.md b/skills/lark-event/references/lark-event-im.md index 113a8778..8ab0c81b 100644 --- a/skills/lark-event/references/lark-event-im.md +++ b/skills/lark-event/references/lark-event-im.md @@ -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 ` 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 ` 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`. \ No newline at end of file +Get your own open_id via `lark-cli contact +get-user --as user`; others' via `lark-cli contact +search-user`. diff --git a/skills/lark-event/references/lark-event-minutes.md b/skills/lark-event/references/lark-event-minutes.md index 537a25a8..bf0d1e20 100644 --- a/skills/lark-event/references/lark-event-minutes.md +++ b/skills/lark-event/references/lark-event-minutes.md @@ -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}' ``` diff --git a/skills/lark-event/references/lark-event-vc.md b/skills/lark-event/references/lark-event-vc.md index 37e54cd5..2d2a8596 100644 --- a/skills/lark-event/references/lark-event-vc.md +++ b/skills/lark-event/references/lark-event-vc.md @@ -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 ` 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}' ``` diff --git a/skills/lark-event/references/lark-event-whiteboard.md b/skills/lark-event/references/lark-event-whiteboard.md index f9e5b6f5..bb85b66a 100644 --- a/skills/lark-event/references/lark-event-whiteboard.md +++ b/skills/lark-event/references/lark-event-whiteboard.md @@ -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=`** — 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=`. 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 until Ctrl+C -lark-cli event consume board.whiteboard.updated_v1 \ - -p whiteboard_id= \ - --as user +# stream every edit on the whiteboard until Ctrl+C +lark-cli event consume board.whiteboard.updated_v1 -p whiteboard_id= --as user -# Sample one event for payload inspection -lark-cli event consume board.whiteboard.updated_v1 \ - -p whiteboard_id= \ - --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= --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= \ - --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= --as user \ + --jq '{whiteboard: .event.whiteboard_id, editors: (.event.operator_ids | map(.open_id))}' ```