feishu-user-plugin 1.3.7 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +13 -3
  2. package/CHANGELOG.md +87 -0
  3. package/README.md +20 -4
  4. package/package.json +10 -6
  5. package/proto/lark.proto +10 -0
  6. package/scripts/capture-feishu-protobuf.js +86 -0
  7. package/scripts/check-changelog.js +31 -0
  8. package/scripts/check-docs-sync.js +41 -0
  9. package/scripts/check-tool-count.js +32 -7
  10. package/scripts/decode-feishu-protobuf.js +115 -0
  11. package/scripts/explore-card-protobuf.js +144 -0
  12. package/scripts/explore-image-minimize.js +163 -0
  13. package/scripts/generate-release-artifacts.js +318 -0
  14. package/scripts/probe-feishu-docx.js +203 -0
  15. package/scripts/sync-server-json.js +71 -0
  16. package/scripts/sync-team-skills.sh +109 -7
  17. package/scripts/test-wiki-attach-fallback.js +71 -0
  18. package/scripts/test-ws-events.js +84 -0
  19. package/skills/feishu-user-plugin/SKILL.md +77 -5
  20. package/skills/feishu-user-plugin/references/CLAUDE.md +208 -297
  21. package/src/auth/cookie.js +30 -0
  22. package/src/auth/credentials.js +85 -0
  23. package/src/auth/profile-router.js +248 -0
  24. package/src/auth/uat.js +231 -0
  25. package/src/cli.js +86 -42
  26. package/src/clients/official/base.js +12 -248
  27. package/src/clients/user.js +19 -31
  28. package/src/config.js +13 -8
  29. package/src/events/cursor.js +103 -0
  30. package/src/events/event-buffer.js +103 -0
  31. package/src/events/event-log.js +151 -0
  32. package/src/events/index.js +12 -0
  33. package/src/events/lockfile.js +126 -0
  34. package/src/events/owner.js +73 -0
  35. package/src/events/ws-server.js +156 -0
  36. package/src/oauth.js +48 -7
  37. package/src/resolver.js +10 -0
  38. package/src/server.js +285 -3
  39. package/src/setup.js +100 -11
  40. package/src/test-all.js +12 -9
  41. package/src/test-events-cursor.js +56 -0
  42. package/src/test-events-lockfile.js +36 -0
  43. package/src/test-events-log.js +67 -0
  44. package/src/test-events-owner.js +64 -0
  45. package/src/test-fixtures/doc-blocks/sample-1.json +1256 -0
  46. package/src/test-read-doc-markdown.js +61 -0
  47. package/src/test-switch-profile.js +171 -0
  48. package/src/tools/_registry.js +1 -0
  49. package/src/tools/diagnostics.js +10 -3
  50. package/src/tools/docs.js +93 -3
  51. package/src/tools/events.js +174 -0
  52. package/src/tools/messaging-bot.js +2 -3
  53. package/src/tools/messaging-user.js +23 -14
  54. package/src/tools/profile.js +43 -7
@@ -24,62 +24,102 @@ The 9 Claude Code skills are also exposed as MCP prompts (`prompts/list` + `prom
24
24
 
25
25
  Each prompt accepts a single `arguments` free-form string (mirroring the `$ARGUMENTS` convention used by Claude Code skills). `status` has no arguments.
26
26
 
27
- ## Tool Categories (80 tools)
28
-
29
- ### User Identity Messaging (reverse-engineered, cookie-based)
30
- - `send_to_user` — Search user + send text (one step, most common). Returns candidates if multiple matches.
31
- - `send_to_group`Search group + send text (one step). Returns candidates if multiple matches.
32
- - `batch_send` Fan-out send to multiple targets in one call (text/image/file/post). Each target {type, id, content, via?} dispatches sequentially with throttling, returns per-target ok/error.
33
- - `send_as_user` — Send text to any chat by ID, supports reply threading (root_id/parent_id)
34
- - `send_image_as_user` Send image (requires image_key from `upload_image`)
35
- - `send_file_as_user` — Send file (requires file_key from `upload_file`)
36
- - `send_post_as_user` — Send rich text with title + formatted paragraphs. Elements: `{tag:"text"}`, `{tag:"a",href,text}`, `{tag:"at",userId,name}`. **@-mentions trigger real notifications** (fixed by registering AT element IDs in RichText.atIds field 6 — reverse-engineered from Feishu Web bundle's AtProperty + RichText schemas).
37
- - `send_as_user` / `send_to_user` / `send_to_group` plain text sends now accept optional `ats: [{userId, name}]`; the text must contain the `@<name>` marker for each entry. The marker is spliced into a real AT element so the mentioned user is notified. Identity is the cookie user (not bot).
38
- - **Cookie sends accept oc_xxx chat IDs (v1.3.7 C1.4)**: `send_as_user`, `send_image_as_user`, `send_file_as_user`, `send_post_as_user`, `send_card_as_user`, and `batch_send` previously required numeric chat IDs from `create_p2p_chat` / cookie search. They now auto-resolve `oc_xxx` via `getChatInfo(name) → cookie search(name) → numeric id` and cache the mapping. Numeric IDs still work and skip the resolver. If resolution fails (chat not in your search index, no group with matching name), the tool throws a clear error with remediation guidance.
39
-
40
- ### User Identity Contacts & Info
41
- - `search_contacts` — Search users/groups by name
42
- - `create_p2p_chat` Create/get P2P chat
43
- - `get_chat_info` — Group details (name, members, owner). Supports both oc_xxx and numeric chat_id (Official API + protobuf fallback)
44
- - `get_user_info` User display name lookup (official API first, cookie cache fallback)
45
- - `get_login_status` Check cookie, app, and UAT status
46
-
47
- ### User OAuth UAT Tools (P2P chat reading + user-identity creation)
48
- - `read_p2p_messages` Read P2P (direct message) chat history. chat_id accepts both numeric IDs (from create_p2p_chat) and oc_xxx format. Returns newest messages first by default.
49
- - `list_user_chats` — List group chats the user is in. Note: API only returns groups, not P2P. For P2P, use: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`.
50
- - **All docx + bitable + drive create/read/write tools are UAT-first**: when UAT is configured, every operation (create/edit/delete doc blocks, bitable tables/fields/views/records, drive folders) tries the user's token first and falls back to app token on failure. This keeps resources consistently owned by the user and avoids 403 errors when the app can't access user-created resources. Read-only tools (e.g. `read_doc`, `get_doc_blocks`, `manage_bitable_table(action=list)`) are also UAT-first so user-owned resources remain readable.
51
-
52
- ### Official API Tools (app credentials)
53
- - `list_chats` / `read_messages` Chat history (read_messages accepts chat name, oc_ ID, or numeric ID; auto-resolves via bot's group list → im.chat.search → search_contacts). **Auto-falls back to UAT for external groups the bot cannot access.** Returns newest messages first by default. Messages include sender names. **v1.3.5**: `merge_forward` messages now auto-expand into their child messages (2 images + 4 texts, with original sender / time / origin chat preserved); text messages get `urls[]` + `feishuDocs[]` extracted so agents can feed them straight into `read_doc` / WebFetch. Disable expansion with `expand_merge_forward=false`.
54
- - `send_message_as_bot` Bot sends message to any chat (text, post, interactive, etc.)
55
- - `reply_message` / `forward_message` Message operations (as bot). `forward_message` accepts `receive_id_type` (chat_id/open_id/union_id/user_id/email; auto-detects when omitted by inspecting the receive_id prefix).
56
- - `delete_message` / `update_message` Recall or edit bot's own messages. `update_message` only supports `msg_type=text` or `interactive` (Feishu API limit; other types are rejected with a clear error before hitting the API).
57
- - `add_reaction` / `delete_reaction` Emoji reactions on messages
58
- - `pin_message` — Pin or unpin a message (pinned=true/false)
59
- - `create_group` / `update_group` Create and manage group chats
60
- - `list_members` / `manage_members` Group membership (manage_members: action=add/remove, member_id_type=open_id|union_id|user_id default open_id; pass union_id/user_id explicitly when your member_ids use those formats, otherwise Feishu rejects with code 9499)
61
- - `search_docs` / `read_doc` / `get_doc_blocks` / `create_doc` — Document operations
62
- - `manage_doc_block(action=create|update|delete)` Document content editing (v1.3.7 consolidates v1.3.6 create_doc_block / update_doc_block / delete_doc_blocks). Image + file shortcuts (`image_path`/`image_token`/`file_path`/`file_token`) flow through unchanged.
63
- - `manage_bitable_app(action=create|copy|get_meta)` Bitable app management. v1.3.7 consolidates create_bitable / copy_bitable / get_bitable_meta.
64
- - `manage_bitable_table(action=list|create|update|delete)` Table CRUD + rename.
65
- - `manage_bitable_field(action=list|create|update|delete)` Field (column) management. Feishu requires `type` for both create AND update (rename).
66
- - `manage_bitable_view(action=list|create|delete)` — Views (grid / kanban / gallery / form / gantt / calendar).
67
- - `manage_bitable_record(action=search|get|create|update|delete)`Record CRUD. create/update/delete accept arrays (single or up to 500 per call).
68
- - `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` / `get_wiki_node` Wiki read (v1.3.4 adds `get_wiki_node` which resolves a wiki node token to its underlying `obj_type` + `obj_token`, so you can feed the node straight into `read_doc`, bitable tools, etc. v1.3.7 hardens this: `get_wiki_node` now also accepts underlying `obj_token`s from `search_wiki` (synthesizes a node-shape so callers don't have to know which ID space they hold), and `list_wiki_spaces` is UAT-first with a `scopeHint` field surfaced when the bot returns an empty list — typically because `wiki:wiki:readonly` is missing or the bot was never invited.)
69
- - `create_wiki_node` / `update_wiki_node` / `move_wiki_node` / `copy_wiki_node` / `delete_wiki_node` — Wiki write (v1.3.7). UAT-first so resources are owned by the user. `create_wiki_node` builds a fresh `doc/sheet/bitable/mindnote/file/docx/slides` inside a wiki space (or a `node_type=shortcut` pointer to an existing node). `update_wiki_node` renames (only `title` is updatable via wiki API; content edits go through docx/bitable/sheet tools). `move_wiki_node` and `copy_wiki_node` accept `target_parent_token` + optional `target_space_id` to re-parent within the same space or migrate to another. `delete_wiki_node` calls `DELETE /open-apis/wiki/v2/spaces/{space_id}/nodes/{token}` via raw REST (the SDK doesn't type it); **only the wiki node pointer is removed — the underlying drive resource is NOT deleted**, follow up with `manage_drive_file(action=delete, type=...)` if you also want the resource gone.
70
- - `list_files` / `create_folder` Drive
71
- - `manage_drive_file(action=copy|move|delete)` — Drive file operations (v1.3.7 consolidates v1.3.6 copy_file / move_file / delete_file). UAT-first. `type` is always required (`file/folder/docx/sheet/bitable/mindnote/slides`) — Feishu rejects with 1061002 / 1062501 otherwise.
72
- - `upload_image` / `upload_file` Upload image/file, returns key for send_image/send_file
73
- - `upload_drive_file` Upload a local file into a Drive folder (`drive/v1/files/upload_all`, `parent_type=explorer`). Returns `file_token` + `url`. If `wiki_space_id` is provided, the upload is followed by `attachToWiki(obj_type=file)` so the file lands as a Wiki node atomically. UAT-first with bot fallback.
74
- - `upload_bitable_attachment` — Upload a local file as a Bitable attachment (`drive/v1/medias/upload_all` with `parent_type=bitable_image` or `bitable_file`). Returns `file_token` to write into an Attachment-type field via `manage_bitable_record(action=create|update, records=[{fields:{<attachment_field>:[{file_token:"..."}]}}])`.
75
- - `send_card_as_user`Send a Feishu interactive card. **v1.3.6 default routes through bot identity** (the `as_user` suffix is reserved for the v1.3.7 reverse-engineered cookie path; default flips when that lands). Pass `card` JSON; `via="user"` returns an explicit deferred error in v1.3.6.
76
- - `download_message_resource` Download a message-attached image OR file. v1.3.7 (C2.4) consolidates v1.3.6 download_image (message-mode) + download_file. Args: `message_id`, `key` (image_key or file_key), `kind=image|file`, optional `save_path`. **Payloads > 2 MiB MUST pass save_path** — the Anthropic API rejects responses > 5 MB; we cap at 2 MiB so the inline image / base64 has multipart headroom. Tries UAT first, falls back to app. **merge_forward children**: use the child's `parentMessageId` (NOT the child id) — Feishu returns `File not in msg` with the child id.
77
- - `download_doc_image` — Download an image embedded in a docx document so the model sees pixels. Args: `image_token` (from `get_doc_blocks` image block), optional `doc_token` (native id / wiki node / Feishu URL — recommended for permission scoping), optional `save_path`. Same 2 MiB inline cap as `download_message_resource`. UAT-first.
78
- - `list_user_okrs` / `get_okrs` / `list_okr_periods` — OKR read. UAT-first (works for the authenticated user's OKRs) with app fallback when OKR scope is granted.
79
- - `create_okr_progress_record` / `list_okr_progress_records` / `delete_okr_progress_record` OKR progress writes (v1.3.7). UAT-first. Requires `okr:okr.content:write` scope. `create_okr_progress_record` accepts a simplified `content_text` (auto-wrapped into the Feishu block schema) plus optional `source_title` / `source_url` / `progress_percent`. `list_okr_progress_records` extracts progress_record IDs from `get_okrs` since Feishu has no native list endpoint.
80
- - `list_calendars` / `list_calendar_events` / `get_calendar_event` — Calendar read. UAT-first (primary + shared + subscribed); app identity only sees calendars the bot was explicitly invited to.
81
- - `create_calendar_event` / `update_calendar_event` / `delete_calendar_event` / `respond_calendar_event` / `get_freebusy` Calendar write (v1.3.7). UAT-first. Requires `calendar:calendar.event:write` scope (re-run `npx feishu-user-plugin oauth` after enabling on the app console). `get_freebusy` is a query, not a write, but groups here for the calendar domain.
82
- - `list_tasks` / `get_task` / `create_task` / `update_task` / `complete_task` / `delete_task` / `manage_task_members` — Task v2 (new domain in v1.3.7). UAT-first. Requires `task:task` scope. v2 uses `task_guid` as the identifier (not numeric task_id like v1). `update_task` requires an explicit `update_fields` array (Feishu only patches the listed fields). `complete_task(completed=true|false)` is a convenience wrapper around `update_task` setting `completed_at`.
27
+ ## Tool Categories (84 tools)
28
+
29
+ Per-tool descriptions live in each tool's MCP `inputSchema.description`. This section lists names + cross-domain caveats only.
30
+
31
+ ### User Identity Messaging (cookie protobuf, 8 tools)
32
+ `send_to_user` / `send_to_group` / `send_as_user` / `send_image_as_user` / `send_file_as_user` / `send_post_as_user` / `send_card_as_user` / `batch_send`
33
+
34
+ - All cookie sends auto-resolve `oc_xxx` chat IDs to numeric since v1.3.7 (C1.4: `getChatInfo search → numeric`, cached).
35
+ - Plain-text sends accept `ats:[{userId,name}]` — the marker `@<name>` must appear in `text`; spliced into a real AT element that triggers notifications.
36
+ - `send_post_as_user` paragraphs accept `{tag:"text"}` / `{tag:"a",href,text}` / `{tag:"at",userId,name}` elements; `at` element triggers a real notification.
37
+ - `send_image_as_user` works as-of v1.3.9 (cookie protobuf reverse-engineered via brute-force probe see `scripts/explore-image-minimize.js`). Required Content fields: `imageKey` + `thumbnailKey`. Plugin defaults thumbnailKey = imageKey when caller omits it. Optional metadata: width / height / mime / size all auto-derivable on Feishu's side, no pre-compute needed.
38
+
39
+ ### User Identity — Contacts & Info (5 tools)
40
+ `search_contacts` / `create_p2p_chat` / `get_chat_info` / `get_user_info` / `get_login_status`
41
+
42
+ - `get_chat_info` accepts both `oc_xxx` and numeric chat_id (Official API + protobuf fallback).
43
+
44
+ ### User OAuth UAT P2P Chat (2 tools)
45
+ `read_p2p_messages` / `list_user_chats`
46
+
47
+ - `list_user_chats` returns **groups only** (Feishu API limit). For P2P chat list, use `search_contacts` → `create_p2p_chat`.
48
+ - All docx / bitable / drive / wiki / OKR / calendar / tasks create+edit are UAT-first by default UAT first, bot fallback, with ⚠ warning in response when forced to bot. Resources consistently owned by the caller.
49
+
50
+ ### Official API IM (15 tools)
51
+ `list_chats` / `read_messages` / `send_message_as_bot` / `reply_message` / `forward_message` / `delete_message` / `update_message` / `add_reaction` / `delete_reaction` / `pin_message` / `create_group` / `update_group` / `list_members` / `manage_members` / `download_message_resource`
52
+
53
+ - `read_messages` resolves chat name bot list → `im.chat.search`cookie `search_contacts`. Auto-falls back to UAT for external groups. `merge_forward` auto-expands; text messages get `urls[]` + `feishuDocs[]` extracted (disable with `expand_merge_forward=false`).
54
+ - `update_message` only supports `msg_type=text|interactive` (Feishu limit; rejected before API call).
55
+ - `forward_message` auto-detects `receive_id_type` from prefix (`ou_`/`on_`/`email`/...).
56
+ - `manage_members` requires `member_id_type` to match the IDs you pass (`open_id` default; pass `union_id`/`user_id` explicitly to avoid 9499).
57
+ - `download_message_resource(kind=image|file)` MUST pass `save_path` when payload > 2 MiB (Anthropic 5 MB inline cap). For `merge_forward` children use `parentMessageId`, not child id.
58
+
59
+ ### Official APIDocs (7 tools)
60
+ `search_docs` / `read_doc` / `read_doc_markdown` / `get_doc_blocks` / `create_doc` / `manage_doc_block` / `download_doc_image`
61
+
62
+ - `read_doc_markdown` returns a markdown string instead of structured JSON — saves ~60% tokens for RAG / digest / summarisation. Embedded images / files surface as `feishu://image_token/<TOKEN>` / `feishu://file_token/<TOKEN>` placeholders; pair with `download_doc_image` for binaries. `document_id` accepts native token / wiki node / Feishu URL same as the others.
63
+ - `manage_doc_block(action=create)` has image (`image_path`/`image_token`) and file (`file_path`/`file_token`) shortcuts; FILE blocks (block_type=23) are auto-wrapped in VIEW container (block_type=33), plugin walks into the inner file block before `replace_file` PATCH.
64
+ - `download_doc_image` same 2 MiB cap as `download_message_resource`.
65
+ - All `document_id` / `app_token` accept native token / wiki node token / full Feishu URL (resolved via `getWikiNode`, 10 min cache).
66
+
67
+ ### Official API Bitable (5 tools, v1.3.7 consolidation)
68
+ `manage_bitable_app(action=create|copy|get_meta)` / `manage_bitable_table` / `manage_bitable_field` / `manage_bitable_view` / `manage_bitable_record` / `upload_bitable_attachment`
69
+
70
+ - `manage_bitable_field(action=update)` requires `type` even when only renaming (Feishu API limit).
71
+ - `manage_bitable_record` create/update/delete accept arrays (single or up to 500).
72
+ - `manage_bitable_app(action=create)` accepts optional `wiki_space_id` (+ `wiki_parent_node_token`) for direct Wiki placement.
73
+ - `upload_bitable_attachment` returns `file_token` write into Attachment field via `manage_bitable_record(action=create|update, records=[{fields:{<field>:[{file_token:"..."}]}}])`.
74
+
75
+ ### Official API Wiki (9 tools)
76
+ `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` / `get_wiki_node` / `create_wiki_node` / `update_wiki_node` / `move_wiki_node` / `copy_wiki_node` / `delete_wiki_node`
77
+
78
+ - `list_wiki_spaces` / `list_wiki_nodes` are UAT-first; bot path returns `scopeHint` when empty (typically `wiki:wiki:readonly` missing).
79
+ - `get_wiki_node` accepts both wiki node tokens AND underlying `obj_token`s from `search_wiki` (synthesizes node-shape).
80
+ - `update_wiki_node` only patches `title` (Feishu wiki API doesn't take content edits those go through docx/bitable/sheet tools).
81
+ - `delete_wiki_node` only removes the Wiki node pointer; underlying drive resource needs separate `manage_drive_file(action=delete)`.
82
+
83
+ ### Official API — Drive (5 tools)
84
+ `list_files` / `create_folder` / `manage_drive_file(action=copy|move|delete)` / `upload_image` / `upload_file` / `upload_drive_file`
85
+
86
+ - `manage_drive_file` requires `type` (`file/folder/docx/sheet/bitable/mindnote/slides`) — Feishu rejects with 1061002 / 1062501 otherwise.
87
+ - `upload_drive_file` with `wiki_space_id` calls `attachToWiki(obj_type=file)` to place the upload as a Wiki node atomically.
88
+
89
+ ### Official API — OKR (6 tools)
90
+ `list_user_okrs` / `get_okrs` / `list_okr_periods` / `create_okr_progress_record` / `list_okr_progress_records` / `delete_okr_progress_record`
91
+
92
+ - Writes need `okr:okr.content:write` scope.
93
+ - `list_okr_progress_records` extracts triples from `get_okrs` (Feishu has no native list endpoint).
94
+ - OKR objective/key-result CRUD doesn't exist in Feishu's open API.
95
+
96
+ ### Official API — Calendar (8 tools)
97
+ `list_calendars` / `list_calendar_events` / `get_calendar_event` / `create_calendar_event` / `update_calendar_event` / `delete_calendar_event` / `respond_calendar_event` / `get_freebusy`
98
+
99
+ - Writes need `calendar:calendar.event:write` scope.
100
+ - UAT-first for read (primary + shared + subscribed); bot only sees calendars it was explicitly invited to.
101
+
102
+ ### Official API — Tasks v2 (7 tools, v1.3.7 new domain)
103
+ `list_tasks` / `get_task` / `create_task` / `update_task` / `complete_task` / `delete_task` / `manage_task_members`
104
+
105
+ - Identifier is `task_guid`, not v1 numeric `task_id`.
106
+ - `update_task` requires explicit `update_fields=["summary","due","completed_at",...]` array — Feishu only patches listed fields.
107
+ - Needs `task:task` scope.
108
+
109
+ ### Plugin — Diagnostics & Profiles (4 tools)
110
+ `get_login_status` / `list_profiles` / `switch_profile` / `manage_profile_hints`
111
+
112
+ - `switch_profile` invalidates cached client instances; next call rebuilds against the new profile. Multi-profile registered via `LARK_PROFILES_JSON` env or `credentials.json` profiles map.
113
+ - `manage_profile_hints(action=list|set|clear, resource_key?, profile?)` (v1.3.8) inspects / edits the resourceKey → profile cache the auto-switch middleware uses. No-op when credentials.json doesn't exist.
114
+
115
+ ### Plugin — Realtime Events (2 tools, v1.3.9)
116
+ `get_new_events` / `manage_ws_status`
117
+
118
+ - **v1.3.9 machine-level**: a single MCP process per machine owns the WS via `~/.feishu-user-plugin/ws-owner.lock`. Events written to `~/.feishu-user-plugin/events.jsonl` (append-only, 10 MB soft / 20 MB hard cap). All harnesses read through a shared `events.cursor.json` — **every event delivered exactly once across all MCP processes**, no more duplicates.
119
+ - WS connection started at MCP boot when APP_ID + APP_SECRET configured. Connects to feishu.cn — Lark international not supported.
120
+ - Default subscriptions = `["im.message.receive_v1"]`. Edit `credentials.json::profiles[<active>].events` to add others (`approval.instance.created_v4`, `calendar.calendar.event.changed_v4`, etc.); call `manage_ws_status(action=reconfig)` to apply without restart.
121
+ - `get_new_events` filter by `event_type` / `event_types` / `chat_id` / `since_seconds` / `profile`. `peek=true` keeps cursor unchanged. **Default `profile` filter = current active**.
122
+ - `manage_ws_status(action=info|reconnect|claim|rotate|reconfig)` — diagnose / control the WS owner. `claim --force` steals an active lock; `rotate` forces events.jsonl rotation.
83
123
 
84
124
  ## Usage Patterns
85
125
 
@@ -131,58 +171,18 @@ Whole new domain. Identifier is `task_guid` (not numeric task_id like v1). Requi
131
171
  6. `delete_task(task_guid)`.
132
172
  7. `manage_task_members(action=add|remove, task_guid, members=[{id,role:"assignee"|"follower",type?:"user",name?}])`.
133
173
 
134
- ### External-group message read (hardened in v1.3.4)
135
- `read_messages` and `read_p2p_messages` now expose a `via` field in the response (`"bot"`, `"user"`, or `"contacts"`) so callers can tell which identity actually read the data. When bot fails with a known code (external tenant / no permission / not in chat) the plugin hops straight to UAT; transient errors (rate limit / 5xx / ECONNRESET / fetch timeout) retry once with a 2 s delay before falling back. When UAT isn't configured, the error message now tells the user to run `npx feishu-user-plugin oauth` instead of leaking the raw Feishu payload.
136
-
137
- ### Messaging
138
- - Send text as yourself `send_to_user` or `send_to_group`
139
- - Send image → `upload_image` → `send_image_as_user`
140
- - Send file `upload_file` `send_file_as_user`
141
- - Send rich content → `send_post_as_user` (formatted text + links + real @-mentions via `{tag:"at",userId,name}`)
142
- - Send text with @-mentions (plain text) → `send_as_user` / `send_to_user` / `send_to_group` with `ats:[{userId,name}]` + text containing `@<name>` markers
143
- - Bot-identity @-mention alternative → `send_message_as_bot` with `<at user_id="ou_xxx">Name</at>` inline in content text
144
- - Reply as user in thread → `send_as_user` with root_id
145
- - Reply as bot `reply_message` (official API)
146
-
147
- ### Reading
148
- - Read any group chat history → `read_messages` with chat name or ID (auto-handles external groups via UAT fallback)
149
- - Read P2P chat history → `search_contacts` → `create_p2p_chat` → `read_p2p_messages`
150
- - Get chat details → `get_chat_info` (supports both oc_xxx and numeric ID)
151
-
152
- ### Bitable (Multi-dimensional Tables)
153
- All bitable ops collapse into 5 `manage_bitable_*` tools (v1.3.7) — pick the action.
154
- - Create from scratch → `manage_bitable_app(action=create)` → `manage_bitable_table(action=create)` → `manage_bitable_field(action=create)`
155
- - Get info → `manage_bitable_app(action=get_meta)`
156
- - Duplicate → `manage_bitable_app(action=copy, name=..., folder_id?)`
157
- - Query → `manage_bitable_table(action=list)` → `manage_bitable_field(action=list)` → `manage_bitable_record(action=search, filter?, sort?, page_size?)`
158
- - Read single record → `manage_bitable_record(action=get, record_id=...)`
159
- - Records CRUD → `manage_bitable_record(action=create|update|delete, records|record_ids=[...])` (single or up to 500/call)
160
- - Fields → `manage_bitable_field(action=create|update|delete, ...)` — `type` required for both create AND update (rename)
161
- - Views → `manage_bitable_view(action=list|create|delete, view_type=grid|kanban|gallery|form|gantt|calendar)`
162
-
163
- ### Group Management
164
- - Create a group → `create_group` with name and optional member open_ids
165
- - Add/remove members → `manage_members` with chat_id + member_ids + action (add/remove)
166
- - List members → `list_members`
167
-
168
- ### Document Editing
169
- All block ops go through one tool: `manage_doc_block(action=create|update|delete, ...)`.
170
- - Create doc with content → `create_doc` → `manage_doc_block(action=create, parent_block_id=document_id, children=[...])`
171
- - Edit existing block → `get_doc_blocks` to find block_id → `manage_doc_block(action=update, block_id=..., update_body={...})`
172
- - Delete blocks → `manage_doc_block(action=delete, parent_block_id=..., start_index=..., end_index=...)`
173
- - Insert image → `manage_doc_block(action=create, parent_block_id=..., image_path=...)` (local file) or `image_token=...` (already uploaded). Three-step flow handled internally.
174
- - Insert file attachment (PDF/zip/xlsx/...) → `manage_doc_block(action=create, file_path=...)` or `file_token=...`. Feishu auto-wraps the FILE block (block_type=23) inside a VIEW container (block_type=33); the plugin walks into the inner file block automatically before the `replace_file` PATCH so the upload + attach succeed.
175
- - Replace existing image/file → `manage_doc_block(action=update, block_id=..., image_token=... | file_token=...)`.
176
-
177
- ### Diagnostics
178
- - Diagnose issues → `get_login_status` first
179
-
180
- ### Profiles (v1.3.6)
181
- Multi-account / multi-tenant support without restarting the MCP server:
182
- - `list_profiles` — see all profiles + the active one. Default profile uses top-level env vars; extras come from `LARK_PROFILES_JSON`.
183
- - `switch_profile(name)` — hot-swap credentials. Cached client instances are invalidated so the next call rebuilds against the new profile.
184
-
185
- To register more profiles, set `LARK_PROFILES_JSON` in the MCP env:
174
+ ### External-group message read
175
+ `read_messages` / `read_p2p_messages` expose a `via` field (`"bot"`/`"user"`/`"contacts"`). On known bot failures (external tenant / no permission / not in chat) the plugin hops straight to UAT; transient errors (rate limit / 5xx / ECONNRESET / timeout) retry once with 2 s delay before falling back. Without UAT, the error points to `npx feishu-user-plugin oauth`.
176
+
177
+ ### Multi-profile auto-switch (v1.3.8)
178
+ For users with ≥2 profiles in `~/.feishu-user-plugin/credentials.json`. Read-only tools (`read_*` / `list_*` / `get_*` / `search_*` / `download_*`) auto-retry across profiles on `91403 / 1254301 / 1254000 / 99991672 / HTTP 403`. Writes never auto-switch.
179
+
180
+ Override per call with `via_profile: "<name>"` to pin, or `via_profile: "auto"` to allow auto-switch on a write. Hints persist in `credentials.json::profileHints` and are inspectable via `manage_profile_hints`.
181
+
182
+ v1.3.9: `FEISHU_PLUGIN_PROFILE` env is bootstrap-only `credentials.json::active` is authoritative once the file exists. Cross-process sync via dispatcher mtime check (~10μs/call).
183
+
184
+ ### Multi-profile registration
185
+ For more profiles beyond the default, set `LARK_PROFILES_JSON` in the MCP env (or use `credentials.json` profiles map):
186
186
  ```json
187
187
  {"alt": {"LARK_COOKIE":"...","LARK_APP_ID":"...","LARK_APP_SECRET":"...","LARK_USER_ACCESS_TOKEN":"...","LARK_USER_REFRESH_TOKEN":"..."}}
188
188
  ```
@@ -194,6 +194,9 @@ To register more profiles, set `LARK_PROFILES_JSON` in the MCP env:
194
194
  - Cookie expiry: sl_session has 12h max-age, auto-refreshed by heartbeat every 4h.
195
195
  - UAT expiry: 2h, auto-refreshed via refresh_token.
196
196
  - Refresh token expiry: 7 days. Use `keepalive` cron to prevent expiration.
197
+ - `~/.feishu-user-plugin/ws-owner.lock`: lock file owned by the one MCP process driving the WS connection (O_CREAT|O_EXCL, 30 s stale).
198
+ - `~/.feishu-user-plugin/events.jsonl`: append-only event log written by the WS owner; 10 MB soft / 20 MB hard cap then rotated to `events.jsonl.old`.
199
+ - `~/.feishu-user-plugin/events.cursor.json`: global drain cursor shared across all MCP processes — advancing it marks events as consumed for all harnesses on the machine.
197
200
 
198
201
  ### Credentials store (v1.3.7+)
199
202
  Single source of truth at `~/.feishu-user-plugin/credentials.json` (mode 0600). Schema documented at `docs/CREDENTIALS-FORMAT.md`. The MCP server reads from this file when present; cookie heartbeat and UAT refresh persist back to it atomically. Multiple harnesses (Claude Code, Codex) sharing the same file see token rotations consistently — no more "Codex still has the old UAT" drift after a refresh in Claude Code.
@@ -254,158 +257,63 @@ crontab -e
254
257
 
255
258
  ## Automated Cookie Setup via Playwright
256
259
 
257
- ### Prerequisites
258
- Playwright MCP must be available. If not installed:
259
- > Run: `npx @anthropic-ai/claude-code mcp add playwright -- npx @anthropic-ai/mcp-server-playwright` then restart Claude Code.
260
+ Prerequisite: Playwright MCP installed (`npx @anthropic-ai/claude-code mcp add playwright -- npx @anthropic-ai/mcp-server-playwright` then restart).
260
261
 
261
- ### Automated FlowFOLLOW EXACTLY, DO NOT IMPROVISE
262
+ Procedure (three gotchas embedded skip any and you'll fail):
262
263
 
263
- **Step 1: Clear existing browser session (MANDATORY)**
264
+ 1. **Clear cookies first.** Playwright MCP uses Edge's persistent profile and may have a cached login from a different account. Run `browser_run_code: await context.clearCookies();` then `browser_navigate: https://www.feishu.cn/messenger/`.
265
+ 2. **Wait for QR scan.** `browser_take_screenshot` to show the code; tell user to scan with Feishu mobile (and verify which account). Poll `browser_snapshot` until URL leaves `/accounts/`.
266
+ 3. **Two-step cookie extraction.** `browser_run_code` output contains markdown prefix + console logs that contaminate the cookie string. Stash via `page.evaluate(s => { window.__COOKIE__ = s; }, str)` then read clean via `browser_evaluate: window.__COOKIE__`.
267
+ 4. **Validate before writing.** Cookie must be pure ASCII (no Chinese, no `###`), contain `session=` AND `sl_session=`, length 500–5000 chars. If > 10000 it's contaminated — STOP, do not write.
268
+ 5. **Write to config.** Use `persistToConfig` or update `~/.claude.json` → `mcpServers.feishu-user-plugin.env.LARK_COOKIE`.
269
+ 6. **OAuth for UAT.** `npx feishu-user-plugin oauth` (browser consent flow, auto-saves tokens).
270
+ 7. **`browser_close` + tell user to restart.** One restart is enough.
264
271
 
265
- Playwright MCP uses Edge's persistent profile. It may have a cached login from a DIFFERENT Feishu account. You MUST clear cookies first:
266
-
267
- ```
268
- browser_run_code:
269
- await context.clearCookies();
270
- ```
271
-
272
- Then navigate:
273
- ```
274
- browser_navigate: https://www.feishu.cn/messenger/
275
- ```
276
-
277
- **Step 2: Wait for user to scan QR code**
278
-
279
- Take a screenshot to show the QR code:
280
- ```
281
- browser_take_screenshot
282
- ```
283
-
284
- Tell the user: "Please scan the QR code with Feishu mobile app to log in. Make sure you use the correct account."
285
-
286
- Poll with `browser_snapshot` every 5 seconds until the URL changes away from `/accounts/` (indicating login complete).
287
-
288
- **Step 3: Extract cookie — TWO-STEP approach (MANDATORY)**
289
-
290
- NEVER use `browser_run_code` output directly as the cookie string. Its output includes `### Result\n` markdown prefix, page snapshots, and console logs that contaminate the cookie.
291
-
292
- Step 3a — Store cookie in page context via `browser_run_code`:
293
- ```js
294
- const cookies = await page.context().cookies('https://www.feishu.cn');
295
- const str = cookies.map(c => c.name + '=' + c.value).join('; ');
296
- await page.evaluate(s => { window.__COOKIE__ = s; }, str);
297
- return 'Stored ' + cookies.length + ' cookies, length=' + str.length;
298
- ```
299
-
300
- Step 3b — Read the clean cookie string via `browser_evaluate`:
301
- ```js
302
- window.__COOKIE__
303
- ```
272
+ ## Troubleshooting Guide
304
273
 
305
- This two-step approach ensures the cookie string is clean, with no markdown prefix or page content mixed in.
274
+ ### Official API returns 401 / "token invalid" every time
275
+ `LARK_APP_ID` is wrong or stale (most common: agent guessed/copied an unrelated APP_ID at install time). `get_login_status` reports `App credentials: INVALID — app_id=<x> rejected by Feishu`; MCP stderr logs `LARK_APP_ID=<x> was REJECTED`. **Fix**: re-run the canonical install prompt from `team-skills/plugins/feishu-user-plugin/README.md` (correct APP_ID + SECRET), restart.
306
276
 
307
- **Step 4: Validate BEFORE writing (MANDATORY)**
277
+ ### MCP tools not available
278
+ 1. Config must be in **top-level** `~/.claude.json` `mcpServers`, NOT under `projects[*]`. For Codex: `~/.codex/config.toml` has `[mcp_servers.feishu-user-plugin]`.
279
+ 2. Restart after config changes; first call may briefly say "No such tool" while tools register — retry once.
308
280
 
309
- Check the cookie string:
310
- 1. Must be pure ASCII no Chinese characters, no markdown (`###`), no HTML
311
- 2. Must contain `session=` and `sl_session=`
312
- 3. Length should be 500-5000 characters. If >10000, it is contaminated — DO NOT write it.
313
- 4. Must NOT start with `###` or contain `\n` followed by non-cookie content
281
+ ### Cookie authentication fails
282
+ - Browser-console `document.cookie` cannot access HttpOnly cookies (`session`, `sl_session`). Use DevTools Network tab → first request → Request Headers → Cookie. Or use Playwright two-step extraction (see above).
283
+ - Playwright logs into the wrong account: ALWAYS `context.clearCookies()` before navigating.
314
284
 
315
- If validation fails: STOP. Debug the extraction. Do NOT write a bad cookie to config.
285
+ ### `read_messages` returns an error
286
+ Error includes Feishu's actual code + description. Auto-falls back to UAT for external groups. Chat name resolution: bot's group list → `im.chat.search` → cookie `search_contacts`. If all three fail, pass `oc_xxx` or numeric ID directly.
316
287
 
317
- **Step 5: Write cookie to config**
288
+ ### UAT refresh fails with `invalid_grant`
289
+ Refresh token expired or revoked — auto-refresh cannot recover. **Fix**: `npx feishu-user-plugin oauth`, then restart Claude Code / Codex so running MCP processes load the new token.
318
290
 
319
- Use `persistToConfig` or directly update the `LARK_COOKIE` field in `~/.claude.json` `mcpServers` `feishu-user-plugin` `env`.
291
+ v1.3.5+ hardening means the "6 MCP processes racing on UAT refresh and burning the token" case is fixed automatically:
292
+ - Cross-process file lock at `~/.claude/feishu-uat-refresh.lock` (`O_CREAT|O_EXCL`, 30 s stale)
293
+ - Lock holder re-reads persisted config inside the critical section, adopts a peer's fresh token if one was rotated
294
+ - `get_login_status` does a real UAT health check (`listChatsAsUser({pageSize:1})`) — no more "configured but actually 401" surprises
320
295
 
321
- **Step 6: Run OAuth for UAT (if not already configured)**
296
+ ### Multiple / duplicate MCP server processes
297
+ Codex + Claude Code both can respawn the server per tool session without cleanup; 6 concurrent processes isn't unusual. v1.3.5 neutralises the damage (file lock above) but stale processes still hold memory. **Manual cleanup when you notice**: `pkill -f 'feishu-user-plugin/src/index.js'`. Also: a team-skills plugin must NOT ship `.mcp.json` — if both `~/.claude.json` and team-skills register the same MCP, you get duplicates; delete `.mcp.json` from the team-skills plugin dir.
322
298
 
323
- ```bash
324
- npx feishu-user-plugin oauth
325
- ```
299
+ v1.3.9: events are now machine-level; each event delivered exactly once across all MCP processes. Old per-process duplicates issue resolved.
326
300
 
327
- This opens a browser for OAuth consent. After completion, tokens are auto-saved to `~/.claude.json`.
301
+ ### `create_*` tool warns "UAT failed, created as BOT"
302
+ UAT is failing (expired / scope missing / race), so the plugin fell back to bot. Resource is now owned by the shared bot, tenant-readable. **Fix**: `npx feishu-user-plugin oauth`, restart, delete the bot-owned copy and recreate.
328
303
 
329
- **Step 7: Close browser and prompt restart**
304
+ ### OAuth CLI fails with "Missing LARK_APP_ID"
305
+ `oauth.js` reads from `~/.claude.json` MCP config (not `.env`). Run `npx feishu-user-plugin setup` first.
330
306
 
331
- ```
332
- browser_close
333
- ```
307
+ ### `list_user_chats` doesn't return P2P chats
308
+ Expected — Feishu API only returns groups. P2P flow: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`.
334
309
 
335
- Tell user to restart Claude Code. Only ONE restart should be needed.
336
-
337
- ## Troubleshooting Guide
338
-
339
- ### If MCP disconnects mid-session
340
- Two known root causes, both fixed in v1.3.3:
341
-
342
- 1. **stdout pollution** (partial fix in v1.3.1, fully closed in v1.3.3):
343
- - `@larksuiteoapi/node-sdk`'s `defaultLogger.error` uses `console.log` (stdout). MCP uses stdout for JSON-RPC, so any stray write corrupts the transport and disconnects the client.
344
- - v1.3.1 replaced the SDK's logger. v1.3.3 also globally redirects `console.log` / `console.info` → `console.error` at the top of `src/index.js` as defense-in-depth against ANY future dependency leaking to stdout.
345
-
346
- 2. **unbounded fetch hangs** (fixed in v1.3.3):
347
- - All raw `fetch` calls to `feishu.cn` / `internal-api-lark-api.feishu.cn` used to have no timeout. A stalled connection (ECONNRESET, slow DNS, upstream hang) would block a tool handler indefinitely; the MCP client times out the request, which some clients handle by tearing down the stdio transport — observed as "mid-session disconnect".
348
- - Fix: `utils.js::fetchWithTimeout` with `AbortController`, 30s default. All `client.js` + `official.js` fetches go through it.
349
- - If still happening: check for any `console.log` calls in server code (only `console.error` is safe), and grep for raw `await fetch(` — every one must go through `fetchWithTimeout`.
350
-
351
- ### If Official API tools return 401 / "token invalid" every time
352
- - **Likely cause**: `LARK_APP_ID` is wrong or stale. Observed in production: Claude Code auto-installed the plugin and guessed/copied a wrong APP_ID that doesn't match the team's real app (e.g. from an unrelated app, from someone else's machine, or hallucinated).
353
- - **Diagnosis**: `get_login_status` now reports `App credentials: INVALID — app_id=<x> rejected by Feishu (<code>: <msg>)`. MCP startup logs `[feishu-user-plugin] ERROR: LARK_APP_ID=<x> was REJECTED by Feishu` on stderr when this happens.
354
- - **Fix**: Re-run the canonical install prompt from `team-skills/plugins/feishu-user-plugin/README.md` which contains the correct APP_ID/SECRET, and restart Claude Code.
355
-
356
- ### If MCP tools are not available
357
- 1. Check `~/.claude.json` — config must be in **top-level** `mcpServers`, not inside `projects[*]`
358
- 2. For Codex: check `~/.codex/config.toml` has `[mcp_servers.feishu-user-plugin]` section
359
- 3. Restart Claude Code / Codex after config changes
360
- 4. After restart, tools may take a few seconds to register — if first call fails with "No such tool", wait and retry once
361
-
362
- ### If cookie authentication fails
363
- - `document.cookie` in browser console CANNOT access HttpOnly cookies (`session`, `sl_session`)
364
- - **Correct method**: Network tab → first request → Request Headers → Cookie → Copy value
365
- - **Best method**: Playwright two-step extraction (see above)
366
-
367
- ### If Playwright logs into the wrong Feishu account
368
- - Playwright uses Edge's persistent profile with cached sessions
369
- - **ALWAYS clear cookies first** with `context.clearCookies()` before navigating to feishu.cn
370
-
371
- ### If read_messages returns an error
372
- - Error messages include the actual Feishu error code and description
373
- - `read_messages` auto-falls back to UAT when bot API fails (e.g. external groups)
374
- - Chat name resolution: bot's group list → `im.chat.search` → `search_contacts` (cookie)
375
- - If all three strategies fail, provide the oc_xxx or numeric chat ID directly
376
-
377
- ### If UAT refresh fails with "invalid_grant"
378
- - The refresh token has expired or been revoked — auto-refresh cannot recover this
379
- - **Fix**: Re-run OAuth: `npx feishu-user-plugin oauth`
380
- - Then restart Claude Code / Codex so running MCP server processes load the new token
381
- - **v1.3.5+ hardening** (no manual action required, fixes the common case):
382
- - Cross-process file lock at `~/.claude/feishu-uat-refresh.lock` (`O_CREAT|O_EXCL`, 30 s stale detection) — at most one MCP process refreshes at a time.
383
- - Inside the critical section the lock holder re-reads `~/.claude.json` to see if a peer already rotated the token; if so, it adopts the fresh token instead of consuming an already-invalidated refresh token.
384
- - This closes the "Codex spawned 6 MCP servers, all shared the same refresh_token, all raced to refresh" failure mode observed on 2026-04-23.
385
- - `get_login_status` now does a real UAT health check (calls `listChatsAsUser({pageSize:1})`) — no more "token configured but actually 401" surprises.
386
-
387
- ### If multiple MCP server processes keep spawning
388
- - Observed on Codex + Claude Code when the client respawns the server for each tool session without cleaning up the previous one. 6 concurrent `src/index.js` processes is not unusual under heavy use.
389
- - v1.3.5 neutralises the damage (UAT refresh serialised via file lock) but the stale processes still consume memory.
390
- - **Manual cleanup when you notice**: `pkill -f 'feishu-user-plugin/src/index.js'` — the client will respawn one fresh process on the next tool call.
391
-
392
- ### If a create_* tool warns "UAT failed, created as BOT"
393
- - v1.3.5 added an explicit `⚠️` warning to MCP responses whenever `_asUserOrApp` silently fell back to bot identity for a write (create_doc / manage_bitable_app(action=create) / create_folder / manage_doc_block(action=create) / ...). Before v1.3.5 this was silent and led to the "teammate can read my 'private' doc" issue.
394
- - **Cause**: your UAT is failing (expired / scope missing / race) so the plugin reached for bot credentials. The resulting resource is owned by the shared bot, tenant-readable by default, NOT by you.
395
- - **Fix**: run `npx feishu-user-plugin oauth` and restart Claude Code / Codex. If the resource needs to be yours, delete the bot-owned copy and recreate after UAT is valid.
396
-
397
- ### If OAuth fails with "Missing LARK_APP_ID"
398
- - `oauth.js` reads credentials from `~/.claude.json` MCP config (not .env)
399
- - Run `npx feishu-user-plugin setup` first, then re-run OAuth
400
-
401
- ### If two MCP servers are running (duplicate tools)
402
- - This happens when both `~/.claude.json` mcpServers AND a team-skills plugin have feishu-user-plugin
403
- - team-skills plugin should NOT have `.mcp.json` — it only provides skills and CLAUDE.md
404
- - Delete `.mcp.json` from the team-skills plugin directory if it exists
405
-
406
- ### If list_user_chats doesn't return P2P chats
407
- - This is expected — the API only returns group chats
408
- - **Correct P2P flow**: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`
310
+ ### Realtime events (`get_new_events`) returns empty / `Realtime events are not available`
311
+ - **APP_ID/SECRET not configured**: `get_login_status` will show this. Fix: re-run setup.
312
+ - **Feishu WS handshake failed**: check server stderr for `WS start failed` — common reasons:
313
+ - Lark international tenant (lark.com) — not supported by Feishu's WSClient. No fix; use polling tools (`read_messages`) instead.
314
+ - Network restriction corporate proxy blocking outbound WSS.
315
+ - **Bot not in the chat where the message was sent**: `im.message.receive_v1` only fires for chats the bot is a member of. Add the bot to the chat to receive events.
316
+ - **Multiple MCP processes**: v1.3.9: events are now machine-level; each event delivered exactly once across all MCP processes. Old per-process duplicates issue resolved.
409
317
 
410
318
  ## Architecture
411
319
 
@@ -484,10 +392,7 @@ When making ANY code change (new tools, bug fixes, features), update these in th
484
392
  For team-skills repo: see [Syncing to team-skills](#syncing-to-team-skills) above. Bottom line: `skills/` + `plugin.json` auto-sync via post-merge hook; team-skills README + SKILL.md still need manual edits per release.
485
393
 
486
394
  ### Keeping ROADMAP.md up to date
487
- - When completing a feature or fixing a bug, check the corresponding item in ROADMAP.md as `[x]` done
488
- - When discovering new bugs, limitations, or feature ideas during development, add them to the appropriate section in ROADMAP.md
489
- - When a version is released (tag pushed), move completed items under the "已完成" section with the version number
490
- - When researching a direction and deciding not to implement, add it to "已调研但暂不实施" with the reasoning
395
+ ROADMAP.md is **forward-only** (open `[ ]` tasks for v1.3.8 / v1.4 candidates only). CHANGELOG.md owns the history of completed work. When you finish a task, **delete the line** — don't move it or check it off. When you discover new bugs / feature ideas, add to the matching section (A–I or v1.4). When you research a direction and rule it out, add to "已调研但暂不实施" with the reasoning.
491
396
 
492
397
  ### When adding new tools (post-v1.3.7 layout)
493
398
  1. Add the underlying API method to the right domain file:
@@ -515,74 +420,80 @@ See `docs/TESTING-METHODOLOGY.md` for the full regression playbook (when to use
515
420
  - `chore:` dependencies, CI, config changes
516
421
 
517
422
  ### Publishing
518
- **IMPORTANT: Version number must ALWAYS be confirmed with the user before publishing.**
519
- Any operation involving `npm version`, modifying `package.json` version, `git tag v*`, or `git push --tags` requires explicit user confirmation of the target version number. Do not auto-decide version numbers.
423
+ **IMPORTANT: User confirmation is required exactly TWICE per release** — once on target version (before any publish operation), once on the announcement card before sending. Don't ask between steps; run end-to-end.
520
424
 
521
425
  Three-layer version safety:
522
- 1. **Claude rule** (this section): Ask user to confirm version before any publish-related operation
426
+ 1. **Claude rule** (this section): Confirm version once with user. Then run all publish steps without asking. Stop only on (a) failure, or (b) the announcement-preview gate.
523
427
  2. **Local gate** (`prepublishOnly`): Interactive confirmation when running `npm publish` locally (skipped in CI)
524
428
  3. **CI gate** (`.github/workflows/publish.yml`): Tag must match `package.json` version or publish fails
525
429
 
526
430
  Steps:
527
- 1. Confirm target version with user
528
- 2. Update `version` in `package.json`
529
- 3. `git add <files> && git commit -m "v1.x.x: description"`
530
- 4. `git tag v1.x.x && git push && git push --tags`
531
- 5. GitHub Actions verifies tag matches package.json, then auto-publishes to npm
431
+ 1. Confirm target version with user (once)
432
+ 2. Bump `version` in `package.json` + `.claude-plugin/plugin.json` + `skills/feishu-user-plugin/SKILL.md` (single commit; `scripts/check-version.js` enforces triangle equality)
433
+ 3. Open release PR, wait for CI green (auto-merge enabled on this repo, so `gh pr merge --auto --squash`)
434
+ 4. After merge, `git tag vX.Y.Z && git push origin vX.Y.Z` triggers GitHub Actions `Publish to npm` workflow
435
+ 5. Verify: `npm view feishu-user-plugin version` returns the new version
436
+ 6. post-merge hook runs `scripts/sync-team-skills.sh` which auto-syncs team-skills (skills + plugin.json + child README changelog + root README catalog row + catalog.yaml regen + `gh pr merge --admin --squash` on the sync PR). No manual touches in team-skills.
437
+ 7. Run `node scripts/generate-release-artifacts.js` to produce `/tmp/feishu-release/v$VERSION/feishu-card.json`
438
+ 8. Present the card preview to user. Wait for "发"
439
+ 9. `send_card_as_user(chat_id="oc_0fab8e155f500f28bd437e8686921870", card=<JSON>)` — only after user explicitly approves
532
440
  6. **After npm confirms the new version is live, draft a release announcement in Chinese for the "AI技术解决(内部)" Feishu group and show it to the user for approval BEFORE sending.** Do not send until the user explicitly approves.
533
441
 
534
442
  ### Release announcement rules (every release)
535
- After a successful publish, draft a group announcement to "AI技术解决(内部)" (chat_id `7599552782038813643`) and ALWAYS show it to the user for review first. Only send after explicit approval.
536
443
 
537
- **Transport**: `send_post_as_user` (rich-text post). No @-mentions announcements are impersonal broadcasts. No emojis. No marketing language.
444
+ After successful publish, send announcement to "AI技术解决(内部)" group (chat_id `oc_0fab8e155f500f28bd437e8686921870`). **Never send without explicit user approval** show preview first, wait for "发".
538
445
 
539
- **Structure** (in this order; omit a section if it doesn't apply this release):
446
+ **Transport (v1.3.9+)**: `send_card_as_user` (interactive Feishu card). No @-mentions, no emojis, no marketing.
540
447
 
448
+ **Source of truth**: `CHANGELOG.md` v$VERSION section. **Never hand-write announcements** — the generator script extracts the text deterministically:
449
+
450
+ ```bash
451
+ node scripts/generate-release-artifacts.js [version]
452
+ # Outputs to /tmp/feishu-release/v<version>/:
453
+ # feishu-card.json ← full Feishu card payload, ready for send_card_as_user
454
+ # team-skills-changelog.md ← markdown block injected into team-skills child README by post-merge hook
455
+ # team-skills-readme-row.md ← root README catalog row replacement
541
456
  ```
542
- feishu-user-plugin vX.Y.Z 发布
543
457
 
544
- <一到两句开篇总结本次发布的主题,陈述语气,不推销>
458
+ **CHANGELOG conventions** (the generator parses these — keep the convention or output diverges):
545
459
 
546
- 修复
547
- <缺陷描述>:<根因与修复机制,引用具体错误码/接口名/参数>
548
- • ...
460
+ ```markdown
461
+ ## [X.Y.Z] - YYYY-MM-DD
549
462
 
550
- 新增
551
- • 新增 <tool 名> 工具:<一句话功能描述>。<关键约束或调用条件>
552
- • ...
463
+ <一到两句陈述式开篇,可空,generator 用作 card 第一段;不宣传不夸大>
553
464
 
554
- 调整
555
- <行为变化的描述>
556
- ...
465
+ ### Added (翻译为"新增")
466
+ - **简短标题 (代号)**:用户可见现象。底层机制 / 错误码 / 接口名 / 文件路径。
467
+ - ...
557
468
 
558
- 下版本计划
559
- <条目>
560
- ...
469
+ ### Changed ("调整")
470
+ ### Fixed ("修复")
471
+ ### Removed | Deprecated | Security ("移除" / "废弃" / "安全")
472
+ ### Deferred to vN.M.P ("下版本计划 (vN.M.P)",从上版本拷过来 - 本版完成的条目)
561
473
 
562
- 升级方式
563
- 重启 Claude Code / Codex 即可自动拉取 X.Y.Z
564
- <若有相关新日志/错误提示,说明怎么应对>
565
- • 建议复测 N 个场景:<场景 1>、<场景 2>、<场景 3>
474
+ ### Test scenarios (可选;用作"升级方式"段的"建议复测"行)
475
+ - 调用 X 时观察 Y 出现 Z
476
+ - ...
566
477
  ```
567
478
 
568
- **写作规范**:
569
- - **开篇**:一到两句陈述式总结,不宣传、不夸大。参考 v1.3.2:"本次更新主要补齐了 X 能力,并修复了 Y 问题;同时将 Z 统一调整为 ..."
570
- - **每条 bullet**:先写用户可见现象,再写底层机制。引用具体错误码(如 1770032 / 91403)、接口名(如 manage_doc_block)、参数名(如 RichText.atIds)——专业读者信赖的是细节
571
- - **字符**:bullet `•`(U+2022),不用 `-` `*`;代码/工具名在正文中直接写,不加反引号
572
- - **禁用**:emoji、🔴🟡🟢 之类严重度标记、`@` 任何人、营销词("强大"、"全新"、"重磅")、夸张修辞
573
- - **语气**:技术 release note 的中性语气,像写给同行的内部更新。参考 v1.3.2 全文
574
- - **长度**:单屏为宜,一般 400–700 汉字。每条 bullet 一到三行
575
- - **下版本计划**:复制自上一版公告仍未完成的条目 + 本次发布中暴露的新方向。本版已完成的条目必须删除
576
- - **升级方式**:至少包含重启指令;若本次修了某类错误(如 APP_ID 校验),列出对应诊断日志字样;以"建议复测 N 个场景"收尾,场景要具体可操作
577
-
578
- **结尾**:不加 CHANGELOG 链接(v1.3.2 风格未含链接,群内读者不需要)。
579
-
580
- **发送前**:始终先用 `send_to_user` 或类似工具发给用户自己审核,或直接以文本形式贴在对话里等用户批准。用户说"发"才调 `send_post_as_user` 到目标群。
581
-
582
- ### Testing a tool
583
- - For Official API tools: can test directly via MCP tool call or standalone script using `readCredentials()` from `src/config.js`
584
- - For Cookie tools: need active session, test via MCP tool call
585
- - Always verify `_safeSDKCall` handles the response format (multipart uploads return data at top level, not nested under `.data`)
479
+ **写作规范** (writes flow into the card directly):
480
+ - 每条 bullet:先用户可见现象,再底层机制。引用具体错误码 (91403 / 1254301)、接口名 (`manage_bitable_record`)、参数名 (`via_profile`)、文件路径 (`src/auth/profile-router.js`)
481
+ - 代号语:`(B)` `(D.1)` 等可保留,对应 ROADMAP / plan 编号
482
+ - 禁用:emoji / `@` 任何人 / "强大"等营销词 / 夸张修辞
483
+ - 长度:单屏,整段 400-800 汉字。每条 bullet 1-3 行
484
+
485
+ **升级方式** is generated automatically by the script:
486
+ - "重启 Claude Code / Codex 自动拉取 X.Y.Z" — always
487
+ - "推荐运行 npx feishu-user-plugin migrate --confirm ..." added when bullets mention migrate / credentials.json / FEISHU_PLUGIN_PROFILE
488
+ - "启动看 stderr 带 WS connected ..." — added when bullets mention WS / WebSocket / get_new_events
489
+ - "建议复测 N 个场景:..." — uses `### Test scenarios` bullets if present, otherwise top-3 Added bullet titles
490
+
491
+ **Step-by-step at release time**:
492
+ 1. Bump version → tag → push → wait for `Publish to npm` workflow success → confirm `npm view feishu-user-plugin version`
493
+ 2. post-merge hook on this repo runs `scripts/sync-team-skills.sh`, which calls `scripts/generate-release-artifacts.js` and auto-injects v$VERSION block into team-skills child README + updates root README catalog row + opens & `--admin --squash`-merges the team-skills sync PR. Zero manual steps in team-skills.
494
+ 3. `node scripts/generate-release-artifacts.js` (idempotent same input gives same output) on this repo to (re)produce `feishu-card.json`
495
+ 4. **Show the rendered card preview to user** — paste a summary or re-render via `cat /tmp/feishu-release/v$VERSION/feishu-card.json | jq` and let user inspect. Do not send.
496
+ 5. User says "发" `send_card_as_user(chat_id=oc_0fab8e155f500f28bd437e8686921870, card=<JSON content>)`
586
497
 
587
498
  ## OAuth Scopes (when re-running `npx feishu-user-plugin oauth`)
588
499
 
@@ -609,4 +520,4 @@ If a tool returns `access_denied` or error code `99991672` (scope not granted),
609
520
  - `list_wiki_spaces` may return empty if bot lacks `wiki:wiki:readonly` permission (v1.3.7+: `scopeHint` field is appended to the response when this happens)
610
521
  - `delete_wiki_node` calls an undocumented-in-SDK endpoint (`DELETE /wiki/v2/spaces/{id}/nodes/{token}`); v1.3.7 ships it because Feishu's API console exposes it, but if Feishu retires the endpoint the tool will fail with a clear 404 — fall back to `manage_drive_file(action=delete)` on the underlying obj_token in that case.
611
522
  - `search_wiki` uses same API as `search_docs` — `docs_types` filter may not work as expected
612
- - `send_image_as_user` is currently broken: Feishu's cookie protobuf gateway rejects the simple `{imageKey}` content payload (HTTP 400) because the Feishu Web client encodes images with extra metadata (image dimensions, mime type, etc.) that we don't have in `proto/lark.proto`. Reverse-engineering needs Chrome DevTools traffic capture and is deferred to v1.3.8. v1.3.7 surfaces a clear error pointing to `send_message_as_bot(msg_type="image", ...)` as the workaround. (`send_file_as_user` and `send_post_as_user` work fine only IMAGE is affected.)
523
+ - `send_card_as_user` only routes through bot. User-identity (cookie protobuf) card sending was confirmed server-side disabled in v1.3.9 (exhaustive brute-force, `scripts/explore-card-protobuf.js`). The "as_user" suffix is historical naming kept for backward compat the `via` parameter and the user-path codepath were removed; the tool is bot-only.