feishu-user-plugin 1.3.6 → 1.3.8

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 (71) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +71 -0
  3. package/README.md +72 -41
  4. package/package.json +10 -3
  5. package/scripts/capture-feishu-protobuf.js +86 -0
  6. package/scripts/check-changelog.js +31 -0
  7. package/scripts/check-docs-sync.js +41 -0
  8. package/scripts/check-tool-count.js +40 -0
  9. package/scripts/check-version.js +40 -0
  10. package/scripts/decode-feishu-protobuf.js +115 -0
  11. package/scripts/smoke.js +224 -0
  12. package/scripts/sync-claude-md.sh +12 -0
  13. package/scripts/sync-server-json.js +71 -0
  14. package/scripts/sync-team-skills.sh +22 -0
  15. package/scripts/test-all-tools.js +158 -0
  16. package/scripts/test-wiki-attach-fallback.js +71 -0
  17. package/scripts/test-ws-events.js +84 -0
  18. package/skills/feishu-user-plugin/SKILL.md +5 -5
  19. package/skills/feishu-user-plugin/references/CLAUDE.md +248 -318
  20. package/skills/feishu-user-plugin/references/table.md +18 -9
  21. package/src/auth/cookie.js +30 -0
  22. package/src/auth/credentials.js +399 -0
  23. package/src/auth/profile-router.js +248 -0
  24. package/src/auth/uat.js +231 -0
  25. package/src/cli.js +45 -13
  26. package/src/clients/official/base.js +188 -0
  27. package/src/clients/official/bitable.js +269 -0
  28. package/src/clients/official/calendar.js +176 -0
  29. package/src/clients/official/contacts.js +54 -0
  30. package/src/clients/official/docs.js +301 -0
  31. package/src/clients/official/drive.js +77 -0
  32. package/src/clients/official/groups.js +68 -0
  33. package/src/clients/official/im.js +414 -0
  34. package/src/clients/official/index.js +30 -0
  35. package/src/clients/official/okr.js +127 -0
  36. package/src/clients/official/tasks.js +142 -0
  37. package/src/clients/official/uploads.js +260 -0
  38. package/src/clients/official/wiki.js +207 -0
  39. package/src/{client.js → clients/user.js} +25 -33
  40. package/src/config.js +13 -8
  41. package/src/events/event-buffer.js +100 -0
  42. package/src/events/index.js +5 -0
  43. package/src/events/ws-server.js +86 -0
  44. package/src/index.js +4 -1977
  45. package/src/logger.js +20 -0
  46. package/src/oauth.js +5 -1
  47. package/src/official.js +5 -1944
  48. package/src/prompts/_registry.js +69 -0
  49. package/src/prompts/index.js +54 -0
  50. package/src/server.js +305 -0
  51. package/src/setup.js +16 -1
  52. package/src/test-all.js +2 -2
  53. package/src/test-comprehensive.js +3 -3
  54. package/src/test-send.js +1 -1
  55. package/src/tools/_registry.js +31 -0
  56. package/src/tools/bitable.js +246 -0
  57. package/src/tools/calendar.js +207 -0
  58. package/src/tools/contacts.js +66 -0
  59. package/src/tools/diagnostics.js +172 -0
  60. package/src/tools/docs.js +158 -0
  61. package/src/tools/drive.js +111 -0
  62. package/src/tools/events.js +64 -0
  63. package/src/tools/groups.js +81 -0
  64. package/src/tools/im-read.js +259 -0
  65. package/src/tools/messaging-bot.js +151 -0
  66. package/src/tools/messaging-user.js +292 -0
  67. package/src/tools/okr.js +159 -0
  68. package/src/tools/profile.js +74 -0
  69. package/src/tools/tasks.js +168 -0
  70. package/src/tools/uploads.js +63 -0
  71. package/src/tools/wiki.js +191 -0
@@ -2,65 +2,122 @@
2
2
 
3
3
  ## What This Is
4
4
  All-in-one Feishu plugin for Claude Code with three auth layers:
5
- - **User Identity** (cookie auth): Send messages (text, image, file, post, sticker, audio) as yourself
5
+ - **User Identity** (cookie auth): Send messages (text, image, file, post) as yourself
6
6
  - **Official API** (app credentials): Read group messages, docs, tables, wiki, drive, contacts, upload files
7
7
  - **User OAuth UAT** (user_access_token): Read P2P chat history, list all user's chats
8
8
 
9
- ## Tool Categories (81 tools)
10
-
11
- ### User Identity Messaging (reverse-engineered, cookie-based)
12
- - `send_to_user` — Search user + send text (one step, most common). Returns candidates if multiple matches.
13
- - `send_to_group` Search group + send text (one step). Returns candidates if multiple matches.
14
- - `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.
15
- - `send_as_user` Send text to any chat by ID, supports reply threading (root_id/parent_id)
16
- - `send_image_as_user` Send image (requires image_key from `upload_image`)
17
- - `send_file_as_user` Send file (requires file_key from `upload_file`)
18
- - `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).
19
- - `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).
20
- - `send_sticker_as_user` Send sticker/emoji
21
- - `send_audio_as_user` Send audio message
22
-
23
- ### User Identity Contacts & Info
24
- - `search_contacts` — Search users/groups by name
25
- - `create_p2p_chat` Create/get P2P chat
26
- - `get_chat_info` — Group details (name, members, owner). Supports both oc_xxx and numeric chat_id (Official API + protobuf fallback)
27
- - `get_user_info` User display name lookup (official API first, cookie cache fallback)
28
- - `get_login_status` — Check cookie, app, and UAT status
29
-
30
- ### User OAuth UAT Tools (P2P chat reading + user-identity creation)
31
- - `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.
32
- - `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`.
33
- - **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`, `list_bitable_tables`) are also UAT-first so user-owned resources remain readable.
34
-
35
- ### Official API Tools (app credentials)
36
- - `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`.
37
- - `send_message_as_bot` Bot sends message to any chat (text, post, interactive, etc.)
38
- - `reply_message` / `forward_message` — Message operations (as bot)
39
- - `delete_message` / `update_message` Recall or edit bot's own messages
40
- - `add_reaction` / `delete_reaction` Emoji reactions on messages
41
- - `pin_message` — Pin or unpin a message (pinned=true/false)
42
- - `create_group` / `update_group` Create and manage group chats
43
- - `list_members` / `manage_members` — Group membership (manage_members: action=add/remove)
44
- - `search_docs` / `read_doc` / `get_doc_blocks` / `create_doc` — Document operations
45
- - `create_doc_block` / `update_doc_block` / `delete_doc_blocks` — Document content editing (insert/update/delete blocks)
46
- - `create_bitable` / `get_bitable_meta` / `copy_bitable` — Bitable app management (create, get info, copy)
47
- - `list_bitable_tables` / `create_bitable_table` / `update_bitable_table` / `delete_bitable_table` Table management (CRUD + rename)
48
- - `list_bitable_fields` / `create_bitable_field` / `update_bitable_field` / `delete_bitable_field`Field (column) management
49
- - `list_bitable_views` / `create_bitable_view` / `delete_bitable_view` — View management (grid, kanban, gallery, form, gantt, calendar)
50
- - `search_bitable_records` / `get_bitable_record` Query records
51
- - `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` Record CRUD (single or batch, max 500/call)
52
- - `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` / `get_wiki_node` — Wiki (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.)
53
- - `list_files` / `create_folder` Drive
54
- - `copy_file` / `move_file` / `delete_file` Drive file operations (copy, move, delete)
55
- - `upload_image` / `upload_file` Upload image/file, returns key for send_image/send_file
56
- - `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.
57
- - `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 `batch_create/update_bitable_records` as `[{file_token: "..."}]`.
58
- - `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.
59
- - `download_image` Download an image and return it as MCP image content so the model **sees the pixels**. Two modes: (1) **message image** pass `message_id` + `image_key` from read_messages. (2) **docx image** — pass `image_token` (from `get_doc_blocks` image block) and optionally `doc_token` (native id / wiki node / Feishu URL). Tries UAT first, falls back to app token. **merge_forward children**: use the child's `parentMessageId` (NOT the child id) — Feishu returns `File not in msg` with the child id.
60
- - `download_file` Download a file (msg_type=file) attachment. Returns base64 + mimeType + byte count; optional `save_path` writes the file to disk. Same parent-id rule for merge_forward children as download_image.
61
- - `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.
62
- - `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.
63
- - `find_user` Contact lookup by email/mobile
9
+ ## MCP Prompts (v1.3.7)
10
+
11
+ The 9 Claude Code skills are also exposed as MCP prompts (`prompts/list` + `prompts/get`) so Codex, Cursor, OpenClaw, and Windsurf — which cannot load Claude Code skills — get the same guided UX. Prompt bodies are read at server start from `skills/feishu-user-plugin/references/`.
12
+
13
+ | Prompt | Description |
14
+ |--------|-------------|
15
+ | `/send` | Send a message as yourself (non-bot) |
16
+ | `/reply` | Read recent messages and reply |
17
+ | `/digest` | Summarise recent group or P2P messages |
18
+ | `/search` | Search Feishu contacts or groups |
19
+ | `/doc` | Search, read, or create a Feishu document |
20
+ | `/table` | Operate on a Feishu Bitable (multi-dimensional table) |
21
+ | `/wiki` | Search and browse a Feishu Wiki space |
22
+ | `/drive` | List files or create folders in Feishu Drive |
23
+ | `/status` | Check all three auth layers (cookie, app, UAT) |
24
+
25
+ Each prompt accepts a single `arguments` free-form string (mirroring the `$ARGUMENTS` convention used by Claude Code skills). `status` has no arguments.
26
+
27
+ ## Tool Categories (82 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` is **broken via cookie protobuf** (HTTP 400 — wire format incomplete). Workaround: `send_message_as_bot(msg_type="image")`. Wire-format reverse-engineering deferred to v1.3.8. See Known Limitations.
38
+
39
+ ### User IdentityContacts & 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 APIIM (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 (5 tools)
60
+ `search_docs` / `read_doc` / `get_doc_blocks` / `create_doc` / `manage_doc_block` / `download_doc_image`
61
+
62
+ - `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.
63
+ - `download_doc_image` same 2 MiB cap as `download_message_resource`.
64
+ - All `document_id` / `app_token` accept native token / wiki node token / full Feishu URL (resolved via `getWikiNode`, 10 min cache).
65
+
66
+ ### Official API — Bitable (5 tools, v1.3.7 consolidation)
67
+ `manage_bitable_app(action=create|copy|get_meta)` / `manage_bitable_table` / `manage_bitable_field` / `manage_bitable_view` / `manage_bitable_record` / `upload_bitable_attachment`
68
+
69
+ - `manage_bitable_field(action=update)` requires `type` even when only renaming (Feishu API limit).
70
+ - `manage_bitable_record` create/update/delete accept arrays (single or up to 500).
71
+ - `manage_bitable_app(action=create)` accepts optional `wiki_space_id` (+ `wiki_parent_node_token`) for direct Wiki placement.
72
+ - `upload_bitable_attachment` returns `file_token` → write into Attachment field via `manage_bitable_record(action=create|update, records=[{fields:{<field>:[{file_token:"..."}]}}])`.
73
+
74
+ ### Official API — Wiki (9 tools)
75
+ `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`
76
+
77
+ - `list_wiki_spaces` / `list_wiki_nodes` are UAT-first; bot path returns `scopeHint` when empty (typically `wiki:wiki:readonly` missing).
78
+ - `get_wiki_node` accepts both wiki node tokens AND underlying `obj_token`s from `search_wiki` (synthesizes node-shape).
79
+ - `update_wiki_node` only patches `title` (Feishu wiki API doesn't take content edits — those go through docx/bitable/sheet tools).
80
+ - `delete_wiki_node` only removes the Wiki node pointer; underlying drive resource needs separate `manage_drive_file(action=delete)`.
81
+
82
+ ### Official API — Drive (5 tools)
83
+ `list_files` / `create_folder` / `manage_drive_file(action=copy|move|delete)` / `upload_image` / `upload_file` / `upload_drive_file`
84
+
85
+ - `manage_drive_file` requires `type` (`file/folder/docx/sheet/bitable/mindnote/slides`) — Feishu rejects with 1061002 / 1062501 otherwise.
86
+ - `upload_drive_file` with `wiki_space_id` calls `attachToWiki(obj_type=file)` to place the upload as a Wiki node atomically.
87
+
88
+ ### Official API — OKR (6 tools)
89
+ `list_user_okrs` / `get_okrs` / `list_okr_periods` / `create_okr_progress_record` / `list_okr_progress_records` / `delete_okr_progress_record`
90
+
91
+ - Writes need `okr:okr.content:write` scope.
92
+ - `list_okr_progress_records` extracts triples from `get_okrs` (Feishu has no native list endpoint).
93
+ - OKR objective/key-result CRUD doesn't exist in Feishu's open API.
94
+
95
+ ### Official API — Calendar (8 tools)
96
+ `list_calendars` / `list_calendar_events` / `get_calendar_event` / `create_calendar_event` / `update_calendar_event` / `delete_calendar_event` / `respond_calendar_event` / `get_freebusy`
97
+
98
+ - Writes need `calendar:calendar.event:write` scope.
99
+ - UAT-first for read (primary + shared + subscribed); bot only sees calendars it was explicitly invited to.
100
+
101
+ ### Official API — Tasks v2 (7 tools, v1.3.7 new domain)
102
+ `list_tasks` / `get_task` / `create_task` / `update_task` / `complete_task` / `delete_task` / `manage_task_members`
103
+
104
+ - Identifier is `task_guid`, not v1 numeric `task_id`.
105
+ - `update_task` requires explicit `update_fields=["summary","due","completed_at",...]` array — Feishu only patches listed fields.
106
+ - Needs `task:task` scope.
107
+
108
+ ### Plugin — Diagnostics & Profiles (4 tools)
109
+ `get_login_status` / `list_profiles` / `switch_profile` / `manage_profile_hints`
110
+
111
+ - `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.
112
+ - `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.
113
+
114
+ ### Plugin — Realtime Events (1 tool, v1.3.8)
115
+ `get_new_events`
116
+
117
+ - WS connection started at MCP boot when APP_ID + APP_SECRET are configured. Connects to feishu.cn — Lark international not supported.
118
+ - Buffer cap 1000 events; oldest dropped. Drain semantics: consumers see each event once.
119
+ - Currently emits `im.message.receive_v1` only. Future: approval / calendar / docs comments behind config flag.
120
+ - Filter by `event_type` / `event_types` / `chat_id` / `since_seconds`. `peek=true` keeps events in buffer.
64
121
 
65
122
  ## Usage Patterns
66
123
 
@@ -72,14 +129,14 @@ All docx and bitable tools now accept three input forms for their `document_id`
72
129
  The plugin resolves wiki nodes to their underlying `obj_token` via `getWikiNode`, then calls the normal docx / bitable endpoint. Results are cached for 10 min to avoid repeated node lookups.
73
130
 
74
131
  Create content directly into a Wiki space:
75
- - `create_doc` / `create_bitable` accept optional `wiki_space_id` (+ `wiki_parent_node_token`). The plugin creates the resource in drive, then calls `wiki/v2/spaces/{space_id}/nodes/move_docs_to_wiki` to attach it — returns `wikiNodeToken` on immediate success, or `wikiAttachTaskId` if Feishu queues the move.
132
+ - `create_doc` / `manage_bitable_app(action=create)` accept optional `wiki_space_id` (+ `wiki_parent_node_token`). The plugin creates the resource in drive, then calls `wiki/v2/spaces/{space_id}/nodes/move_docs_to_wiki` to attach it — returns `wikiNodeToken` on immediate success, or `wikiAttachTaskId` if Feishu queues the move.
76
133
 
77
134
  ### Document images
78
- Read — `download_image` with `doc_token` + `image_token` returns the image as MCP image content (base64 + mimeType). `doc_token` accepts native id / wiki node / URL.
79
- Write — `create_doc_block` now has image shortcuts:
135
+ Read — `download_doc_image(image_token, doc_token?, save_path?)` returns the image as MCP image content (base64 + mimeType). `doc_token` accepts native id / wiki node / URL. Force `save_path` when image > 2 MiB.
136
+ Write — `manage_doc_block(action=create)` has image shortcuts:
80
137
  - `image_path` (absolute local file path) → plugin creates an image block, uploads the pixels via `drive/v1/medias/upload_all`, and patches the block with the uploaded token.
81
138
  - `image_token` (already uploaded) → plugin creates block and attaches token.
82
- `update_doc_block` accepts `image_token` to swap the picture in an existing image block.
139
+ `manage_doc_block(action=update, image_token=...)` swaps the picture in an existing image block.
83
140
 
84
141
  ### OKR
85
142
  1. `list_okr_periods` — find the period id for current quarter.
@@ -87,74 +144,65 @@ Write — `create_doc_block` now has image shortcuts:
87
144
  3. `get_okrs(okr_ids)` — batch fetch full objective + key result structure with progress + alignments.
88
145
  `user_id` is required — use your own open_id (from `get_login_status` / `search_contacts`) to read your own OKRs, or a colleague's open_id for theirs (subject to permissions).
89
146
 
147
+ Write (v1.3.7, requires `okr:okr.content:write` scope):
148
+ 4. `create_okr_progress_record(target_id, target_type=1|2, content_text, source_title?, source_url?, progress_percent?)` — `target_type` is 1 for objectives, 2 for key results. `content_text` is auto-wrapped into Feishu's required block format; pass `content` directly for richer payloads (lists, mentions, docs links, gallery).
149
+ 5. `list_okr_progress_records(okr_id)` — extracts `{progress_id, target_id, target_type}` triples from `get_okrs` (Feishu has no native list endpoint).
150
+ 6. `delete_okr_progress_record(progress_id)`.
151
+
90
152
  ### Calendar
91
153
  1. `list_calendars` — get your calendars; the one with `type=primary` is your personal calendar.
92
154
  2. `list_calendar_events(calendar_id, start_time=<unix_sec>, end_time=<unix_sec>)` — list events in a time window.
93
155
  3. `get_calendar_event(calendar_id, event_id)` — full details (attendees, location, attachments, meeting link).
94
-
95
- ### External-group message read (hardened in v1.3.4)
96
- `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.
97
-
98
- ### Messaging
99
- - Send text as yourself → `send_to_user` or `send_to_group`
100
- - Send image → `upload_image` → `send_image_as_user`
101
- - Send file `upload_file` `send_file_as_user`
102
- - Send rich content → `send_post_as_user` (formatted text + links + real @-mentions via `{tag:"at",userId,name}`)
103
- - Send text with @-mentions (plain text)`send_as_user` / `send_to_user` / `send_to_group` with `ats:[{userId,name}]` + text containing `@<name>` markers
104
- - Bot-identity @-mention alternative `send_message_as_bot` with `<at user_id="ou_xxx">Name</at>` inline in content text
105
- - Reply as user in thread `send_as_user` with root_id
106
- - Reply as bot `reply_message` (official API)
107
-
108
- ### Reading
109
- - Read any group chat history → `read_messages` with chat name or ID (auto-handles external groups via UAT fallback)
110
- - Read P2P chat history → `search_contacts` → `create_p2p_chat` → `read_p2p_messages`
111
- - Get chat details `get_chat_info` (supports both oc_xxx and numeric ID)
112
-
113
- ### Bitable (Multi-dimensional Tables)
114
- - Create a bitable from scratch `create_bitable` `create_bitable_table` `create_bitable_field`
115
- - Get bitable info → `get_bitable_meta`
116
- - Copy a bitable `copy_bitable` with name and optional folder
117
- - Query data → `list_bitable_tables` → `list_bitable_fields` → `search_bitable_records`
118
- - Rename table → `update_bitable_table` with new name
119
- - Read single record `get_bitable_record`
120
- - Create/update/delete records → `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` (works for single or up to 500)
121
- - Manage fields → `create_bitable_field` / `update_bitable_field` (requires type param) / `delete_bitable_field`
122
- - Manage views → `create_bitable_view` (type: grid/kanban/gallery/form/gantt/calendar) / `delete_bitable_view`
123
-
124
- ### Group Management
125
- - Create a group → `create_group` with name and optional member open_ids
126
- - Add/remove members → `manage_members` with chat_id + member_ids + action (add/remove)
127
- - List members → `list_members`
128
-
129
- ### Document Editing
130
- - Create doc with content → `create_doc` → `create_doc_block` (use document_id as parent_block_id for root)
131
- - Edit existing block → `get_doc_blocks` to find block_id → `update_doc_block`
132
- - Delete blocks → `delete_doc_blocks` with start/end index range
133
- - Insert image → `create_doc_block` with `image_path` (local file) or `image_token` (already uploaded). Three-step flow handled internally.
134
- - Insert file attachment (PDF/zip/xlsx/...) → `create_doc_block` with `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.
135
- - Replace existing image/file in a block → `update_doc_block` with `image_token` / `file_token`.
136
-
137
- ### Diagnostics
138
- - Diagnose issues → `get_login_status` first
139
-
140
- ### Profiles (v1.3.6)
141
- Multi-account / multi-tenant support without restarting the MCP server:
142
- - `list_profiles` — see all profiles + the active one. Default profile uses top-level env vars; extras come from `LARK_PROFILES_JSON`.
143
- - `switch_profile(name)` — hot-swap credentials. Cached client instances are invalidated so the next call rebuilds against the new profile.
144
-
145
- To register more profiles, set `LARK_PROFILES_JSON` in the MCP env:
156
+ 4. `create_calendar_event(calendar_id, summary, start_time, end_time, ...)` — `start_time` / `end_time` are objects: `{timestamp:"<unix-seconds>", timezone?:"Asia/Shanghai"}` or `{date:"YYYY-MM-DD"}` for all-day. v1.3.7+ requires `calendar:calendar.event:write` scope.
157
+ 5. `update_calendar_event(calendar_id, event_id, ...patch)` pass only the fields to change.
158
+ 6. `delete_calendar_event(calendar_id, event_id, need_notification?)` pass `meeting_chat_id` to also dissolve the linked meeting chat if any.
159
+ 7. `respond_calendar_event(calendar_id, event_id, rsvp_status=accept|decline|tentative)` — RSVP as the current UAT identity.
160
+ 8. `get_freebusy(time_min, time_max, user_ids=[...])` — freebusy windows in RFC3339; useful for finding meeting slots.
161
+
162
+ ### Tasks (v2, v1.3.7)
163
+ Whole new domain. Identifier is `task_guid` (not numeric task_id like v1). Requires `task:task` scope.
164
+ 1. `list_tasks(completed?, type?)` current user's tasks, paginated.
165
+ 2. `get_task(task_guid)` full details.
166
+ 3. `create_task(summary, due?, members?, ...)` at minimum `summary`; `due` is `{timestamp:"<unix-millis>", is_all_day?}`.
167
+ 4. `update_task(task_guid, update_fields=["summary","due","completed_at"], task={...})` Feishu only patches the listed fields.
168
+ 5. `complete_task(task_guid, completed=true|false)` convenience for the completed_at toggle.
169
+ 6. `delete_task(task_guid)`.
170
+ 7. `manage_task_members(action=add|remove, task_guid, members=[{id,role:"assignee"|"follower",type?:"user",name?}])`.
171
+
172
+ ### External-group message read
173
+ `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`.
174
+
175
+ ### Multi-profile auto-switch (v1.3.8)
176
+ 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.
177
+
178
+ 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`.
179
+
180
+ ### Multi-profile registration
181
+ For more profiles beyond the default, set `LARK_PROFILES_JSON` in the MCP env (or use `credentials.json` profiles map):
146
182
  ```json
147
183
  {"alt": {"LARK_COOKIE":"...","LARK_APP_ID":"...","LARK_APP_SECRET":"...","LARK_USER_ACCESS_TOKEN":"...","LARK_USER_REFRESH_TOKEN":"..."}}
148
184
  ```
149
185
 
150
186
  ## Auth & Session
151
- - **LARK_COOKIE**: Required for user identity tools. Session auto-refreshed every 4h via heartbeat and persisted to config.
187
+ - **LARK_COOKIE**: Required for user identity tools. Session auto-refreshed every 4h via heartbeat and persisted to credentials store.
152
188
  - **LARK_APP_ID + LARK_APP_SECRET**: Required for official API tools.
153
- - **LARK_USER_ACCESS_TOKEN + LARK_USER_REFRESH_TOKEN**: Required for P2P reading. Auto-refreshed on expiry (error codes 99991668/99991663/99991677). Token auto-persisted to MCP config on refresh.
189
+ - **LARK_USER_ACCESS_TOKEN + LARK_USER_REFRESH_TOKEN**: Required for P2P reading. Auto-refreshed on expiry (error codes 99991668/99991663/99991677). Token auto-persisted to credentials store on refresh.
154
190
  - Cookie expiry: sl_session has 12h max-age, auto-refreshed by heartbeat every 4h.
155
191
  - UAT expiry: 2h, auto-refreshed via refresh_token.
156
192
  - Refresh token expiry: 7 days. Use `keepalive` cron to prevent expiration.
157
193
 
194
+ ### Credentials store (v1.3.7+)
195
+ 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.
196
+
197
+ Opt-in migration:
198
+ ```bash
199
+ npx feishu-user-plugin migrate # dry-run (default) — prints what would be written
200
+ npx feishu-user-plugin migrate --confirm # writes credentials.json
201
+ ```
202
+ After migration the harness env blocks remain as backward-compat fallback. Delete `~/.feishu-user-plugin/credentials.json` to revert to legacy behaviour.
203
+
204
+ Backward compat: v1.3.6 users without credentials.json see zero behaviour change. The credentials file is preferred only when it exists. The MCP server's `Auth:` startup line on stderr now shows the source (`credentials.json profile=default` vs `env vars (legacy)`) so you can tell at a glance which path is active.
205
+
158
206
  ## Required Environment Variables (ALL are required for full functionality)
159
207
 
160
208
  | Variable | Purpose |
@@ -202,158 +250,61 @@ crontab -e
202
250
 
203
251
  ## Automated Cookie Setup via Playwright
204
252
 
205
- ### Prerequisites
206
- Playwright MCP must be available. If not installed:
207
- > Run: `npx @anthropic-ai/claude-code mcp add playwright -- npx @anthropic-ai/mcp-server-playwright` then restart Claude Code.
208
-
209
- ### Automated Flow — FOLLOW EXACTLY, DO NOT IMPROVISE
210
-
211
- **Step 1: Clear existing browser session (MANDATORY)**
212
-
213
- Playwright MCP uses Edge's persistent profile. It may have a cached login from a DIFFERENT Feishu account. You MUST clear cookies first:
214
-
215
- ```
216
- browser_run_code:
217
- await context.clearCookies();
218
- ```
219
-
220
- Then navigate:
221
- ```
222
- browser_navigate: https://www.feishu.cn/messenger/
223
- ```
224
-
225
- **Step 2: Wait for user to scan QR code**
226
-
227
- Take a screenshot to show the QR code:
228
- ```
229
- browser_take_screenshot
230
- ```
231
-
232
- Tell the user: "Please scan the QR code with Feishu mobile app to log in. Make sure you use the correct account."
233
-
234
- Poll with `browser_snapshot` every 5 seconds until the URL changes away from `/accounts/` (indicating login complete).
235
-
236
- **Step 3: Extract cookie — TWO-STEP approach (MANDATORY)**
253
+ Prerequisite: Playwright MCP installed (`npx @anthropic-ai/claude-code mcp add playwright -- npx @anthropic-ai/mcp-server-playwright` then restart).
237
254
 
238
- 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.
255
+ Procedure (three gotchas embedded skip any and you'll fail):
239
256
 
240
- Step 3a Store cookie in page context via `browser_run_code`:
241
- ```js
242
- const cookies = await page.context().cookies('https://www.feishu.cn');
243
- const str = cookies.map(c => c.name + '=' + c.value).join('; ');
244
- await page.evaluate(s => { window.__COOKIE__ = s; }, str);
245
- return 'Stored ' + cookies.length + ' cookies, length=' + str.length;
246
- ```
247
-
248
- Step 3b — Read the clean cookie string via `browser_evaluate`:
249
- ```js
250
- window.__COOKIE__
251
- ```
252
-
253
- This two-step approach ensures the cookie string is clean, with no markdown prefix or page content mixed in.
254
-
255
- **Step 4: Validate BEFORE writing (MANDATORY)**
257
+ 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/`.
258
+ 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/`.
259
+ 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__`.
260
+ 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.
261
+ 5. **Write to config.** Use `persistToConfig` or update `~/.claude.json` → `mcpServers.feishu-user-plugin.env.LARK_COOKIE`.
262
+ 6. **OAuth for UAT.** `npx feishu-user-plugin oauth` (browser consent flow, auto-saves tokens).
263
+ 7. **`browser_close` + tell user to restart.** One restart is enough.
256
264
 
257
- Check the cookie string:
258
- 1. Must be pure ASCII — no Chinese characters, no markdown (`###`), no HTML
259
- 2. Must contain `session=` and `sl_session=`
260
- 3. Length should be 500-5000 characters. If >10000, it is contaminated — DO NOT write it.
261
- 4. Must NOT start with `###` or contain `\n` followed by non-cookie content
265
+ ## Troubleshooting Guide
262
266
 
263
- If validation fails: STOP. Debug the extraction. Do NOT write a bad cookie to config.
267
+ ### Official API returns 401 / "token invalid" every time
268
+ `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.
264
269
 
265
- **Step 5: Write cookie to config**
270
+ ### MCP tools not available
271
+ 1. Config must be in **top-level** `~/.claude.json` `mcpServers`, NOT under `projects[*]`. For Codex: `~/.codex/config.toml` has `[mcp_servers.feishu-user-plugin]`.
272
+ 2. Restart after config changes; first call may briefly say "No such tool" while tools register — retry once.
266
273
 
267
- Use `persistToConfig` or directly update the `LARK_COOKIE` field in `~/.claude.json` → `mcpServers` → `feishu-user-plugin` → `env`.
274
+ ### Cookie authentication fails
275
+ - 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).
276
+ - Playwright logs into the wrong account: ALWAYS `context.clearCookies()` before navigating.
268
277
 
269
- **Step 6: Run OAuth for UAT (if not already configured)**
278
+ ### `read_messages` returns an error
279
+ 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.
270
280
 
271
- ```bash
272
- npx feishu-user-plugin oauth
273
- ```
281
+ ### UAT refresh fails with `invalid_grant`
282
+ 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.
274
283
 
275
- This opens a browser for OAuth consent. After completion, tokens are auto-saved to `~/.claude.json`.
284
+ v1.3.5+ hardening means the "6 MCP processes racing on UAT refresh and burning the token" case is fixed automatically:
285
+ - Cross-process file lock at `~/.claude/feishu-uat-refresh.lock` (`O_CREAT|O_EXCL`, 30 s stale)
286
+ - Lock holder re-reads persisted config inside the critical section, adopts a peer's fresh token if one was rotated
287
+ - `get_login_status` does a real UAT health check (`listChatsAsUser({pageSize:1})`) — no more "configured but actually 401" surprises
276
288
 
277
- **Step 7: Close browser and prompt restart**
289
+ ### Multiple / duplicate MCP server processes
290
+ 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.
278
291
 
279
- ```
280
- browser_close
281
- ```
292
+ ### `create_*` tool warns "UAT failed, created as BOT"
293
+ 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.
282
294
 
283
- Tell user to restart Claude Code. Only ONE restart should be needed.
295
+ ### OAuth CLI fails with "Missing LARK_APP_ID"
296
+ `oauth.js` reads from `~/.claude.json` MCP config (not `.env`). Run `npx feishu-user-plugin setup` first.
284
297
 
285
- ## Troubleshooting Guide
298
+ ### `list_user_chats` doesn't return P2P chats
299
+ Expected — Feishu API only returns groups. P2P flow: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`.
286
300
 
287
- ### If MCP disconnects mid-session
288
- Two known root causes, both fixed in v1.3.3:
289
-
290
- 1. **stdout pollution** (partial fix in v1.3.1, fully closed in v1.3.3):
291
- - `@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.
292
- - 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.
293
-
294
- 2. **unbounded fetch hangs** (fixed in v1.3.3):
295
- - 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".
296
- - Fix: `utils.js::fetchWithTimeout` with `AbortController`, 30s default. All `client.js` + `official.js` fetches go through it.
297
- - 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`.
298
-
299
- ### If Official API tools return 401 / "token invalid" every time
300
- - **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).
301
- - **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.
302
- - **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.
303
-
304
- ### If MCP tools are not available
305
- 1. Check `~/.claude.json` — config must be in **top-level** `mcpServers`, not inside `projects[*]`
306
- 2. For Codex: check `~/.codex/config.toml` has `[mcp_servers.feishu-user-plugin]` section
307
- 3. Restart Claude Code / Codex after config changes
308
- 4. After restart, tools may take a few seconds to register — if first call fails with "No such tool", wait and retry once
309
-
310
- ### If cookie authentication fails
311
- - `document.cookie` in browser console CANNOT access HttpOnly cookies (`session`, `sl_session`)
312
- - **Correct method**: Network tab → first request → Request Headers → Cookie → Copy value
313
- - **Best method**: Playwright two-step extraction (see above)
314
-
315
- ### If Playwright logs into the wrong Feishu account
316
- - Playwright uses Edge's persistent profile with cached sessions
317
- - **ALWAYS clear cookies first** with `context.clearCookies()` before navigating to feishu.cn
318
-
319
- ### If read_messages returns an error
320
- - Error messages include the actual Feishu error code and description
321
- - `read_messages` auto-falls back to UAT when bot API fails (e.g. external groups)
322
- - Chat name resolution: bot's group list → `im.chat.search` → `search_contacts` (cookie)
323
- - If all three strategies fail, provide the oc_xxx or numeric chat ID directly
324
-
325
- ### If UAT refresh fails with "invalid_grant"
326
- - The refresh token has expired or been revoked — auto-refresh cannot recover this
327
- - **Fix**: Re-run OAuth: `npx feishu-user-plugin oauth`
328
- - Then restart Claude Code / Codex so running MCP server processes load the new token
329
- - **v1.3.5+ hardening** (no manual action required, fixes the common case):
330
- - 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.
331
- - 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.
332
- - 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.
333
- - `get_login_status` now does a real UAT health check (calls `listChatsAsUser({pageSize:1})`) — no more "token configured but actually 401" surprises.
334
-
335
- ### If multiple MCP server processes keep spawning
336
- - 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.
337
- - v1.3.5 neutralises the damage (UAT refresh serialised via file lock) but the stale processes still consume memory.
338
- - **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.
339
-
340
- ### If a create_* tool warns "UAT failed, created as BOT"
341
- - v1.3.5 added an explicit `⚠️` warning to MCP responses whenever `_asUserOrApp` silently fell back to bot identity for a write (create_doc / create_bitable / create_folder / create_doc_block / ...). Before v1.3.5 this was silent and led to the "teammate can read my 'private' doc" issue.
342
- - **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.
343
- - **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.
344
-
345
- ### If OAuth fails with "Missing LARK_APP_ID"
346
- - `oauth.js` reads credentials from `~/.claude.json` MCP config (not .env)
347
- - Run `npx feishu-user-plugin setup` first, then re-run OAuth
348
-
349
- ### If two MCP servers are running (duplicate tools)
350
- - This happens when both `~/.claude.json` mcpServers AND a team-skills plugin have feishu-user-plugin
351
- - team-skills plugin should NOT have `.mcp.json` — it only provides skills and CLAUDE.md
352
- - Delete `.mcp.json` from the team-skills plugin directory if it exists
353
-
354
- ### If list_user_chats doesn't return P2P chats
355
- - This is expected — the API only returns group chats
356
- - **Correct P2P flow**: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`
301
+ ### Realtime events (`get_new_events`) returns empty / `Realtime events are not available`
302
+ - **APP_ID/SECRET not configured**: `get_login_status` will show this. Fix: re-run setup.
303
+ - **Feishu WS handshake failed**: check server stderr for `WS start failed` — common reasons:
304
+ - Lark international tenant (lark.com) not supported by Feishu's WSClient. No fix; use polling tools (`read_messages`) instead.
305
+ - Network restriction corporate proxy blocking outbound WSS.
306
+ - **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.
307
+ - **Multiple MCP processes**: Each process has its own WS, so events are duplicated. De-dupe on `event_id` in your consumer.
357
308
 
358
309
  ## Architecture
359
310
 
@@ -394,79 +345,65 @@ NPM_TOKEN is stored as a GitHub repo secret.
394
345
 
395
346
  **IMPORTANT: team-skills 仓库禁止直接推送 main。所有变更必须走 PR。**
396
347
 
397
- team-skills 推送规范:
398
- 1. **创建 feature branch**: `git checkout -b fix/feishu-xxx` `sync/feishu-v1.x.x`
399
- 2. **提交变更并推送 branch**: `git push -u origin <branch-name>`
400
- 3. **创建 PR 并设置 auto-merge**: `gh pr create --title "..." --body "..."` 然后 `gh pr merge <number> --auto --merge`
401
- 4. **CI 通过后自动合并**: validate workflow 检查三方版本一致性,通过即自动 merge,无需手动操作
402
- 5. **如 CI 失败**: 修复后 push 到同一 branch,CI 会重跑,通过后自动合并
348
+ What is automatic now (Phase B3 hooks):
349
+ - **pre-commit (this repo)**: any change to `CLAUDE.md` auto-syncs `AGENTS.md` + `skills/feishu-user-plugin/references/CLAUDE.md` (script: `scripts/sync-claude-md.sh`).
350
+ - **post-merge (this repo, on main)**: copies `skills/` + `.claude-plugin/plugin.json` into `team-skills/plugins/feishu-user-plugin/`, creates `sync/feishu-v<version>` branch, opens a PR with `--auto --merge` (script: `scripts/sync-team-skills.sh`).
351
+
352
+ What still needs a manual touch in team-skills:
353
+ - `README.md` team-skills has its own README (with team-shared APP_ID/SECRET hardcoded). Tool count, changelog, install prompt all need hand edits.
354
+ - `skills/feishu-user-plugin/SKILL.md` — version + `allowed-tools` list.
403
355
 
404
- 三方版本一致性规则:
405
- - `plugins/feishu-user-plugin/.claude-plugin/plugin.json` `version`
406
- - `plugins/feishu-user-plugin/skills/feishu-user-plugin/SKILL.md` frontmatter `version`
407
- - `plugins/feishu-user-plugin/README.md` 更新日志里第一个 `### vX.Y.Z` 标题
408
- - 这三个版本号必须相同,否则 CI 会失败。每次 npm 发包后,team-skills 的版本号也要同步更新。
356
+ team-skills PR 流程:
357
+ 1. 创建 branch: `git checkout -b sync/feishu-v1.x.x` `fix/feishu-xxx`
358
+ 2. push branch + `gh pr create` + `gh pr merge <number> --auto --merge`
359
+ 3. CI (`validate.yml`) checks the three-way version triangle (`plugin.json` / `SKILL.md` / first `### vX.Y.Z` in README) — must match or CI fails.
360
+ 4. If CI fails: fix + push to same branch, CI re-runs, auto-merge proceeds.
409
361
 
410
- 同步内容(每次发版后执行):
362
+ Manual sync fallback (hook failed / dry-run / first-time):
411
363
  ```bash
412
- # 1. 同步 skills + plugin.json
413
- cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md
414
- cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/
364
+ # CLAUDE.md AGENTS.md + skill ref now handled by pre-commit hook
365
+ cp -r skills/. /Users/abble/team-skills/plugins/feishu-user-plugin/skills/
415
366
  cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/
416
- # 2. 手动更新 team-skills 的 README.md(工具数、更新日志)和 SKILL.md(version + allowed-tools)
417
- # 3. 走 PR 流程推送
418
367
  # Do NOT copy .mcp.json — team-skills plugin should not have one
419
368
  ```
420
369
 
421
370
  ## Development Workflow
422
371
 
423
372
  ### Keeping all docs in sync
424
- When making ANY code change (new tools, bug fixes, features), update ALL of these:
425
373
 
426
- **本仓库内:**
374
+ When making ANY code change (new tools, bug fixes, features), update these in this repo:
427
375
  - `CLAUDE.md` — tool count, tool list, usage patterns, known limitations
428
- - `AGENTS.md` — **每次改 CLAUDE.md 必须同步**:正文(第 2 行起)与 CLAUDE.md 完全一致,仅首行标题保留 `# feishu-user-plugin — Codex Instructions`。同步命令:`tail -n +2 CLAUDE.md > /tmp/body.md && { echo "# feishu-user-plugin — Codex Instructions"; cat /tmp/body.md; } > AGENTS.md`
429
- - `README.md` — tool count (badge + heading + tool table), feature highlights, OpenClaw/Claude Code config examples
376
+ - `README.md` — tool count badge + heading + tool table, feature highlights, OpenClaw/Claude Code config examples
430
377
  - `ROADMAP.md` — check off completed items, add new findings
431
- - `package.json` — version, description (tool count)
432
- - `skills/feishu-user-plugin/references/CLAUDE.md` — always copy from root: `cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md`
433
- - `prompts/openclaw-setup.md` — if OpenClaw 相关配置变了要更新
378
+ - `package.json` — version + description (tool count). All three of `package.json`, `.claude-plugin/plugin.json`, and `skills/feishu-user-plugin/SKILL.md` must agree on version (CI enforces).
379
+ - `prompts/openclaw-setup.md` — only if OpenClaw config changed
434
380
 
435
- **team-skills 仓库 (`/Users/abble/team-skills/plugins/feishu-user-plugin/`):**
436
- - `skills/` — 同步技能文件: `cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/`
437
- - `README.md` — team-skills 有自己的 README(含团队 APP_ID/SECRET),需要同步更新:工具数量、功能列表、更新日志、安装 prompt
438
- - 两个 README 都必须包含 Claude Code 安装 prompt 和 OpenClaw 安装 prompt
439
- - team-skills README 的安装 prompt 包含团队共享的 APP_ID/SECRET(hardcoded),本仓库 README 用占位符
381
+ `AGENTS.md` (Codex) and `skills/feishu-user-plugin/references/CLAUDE.md` are auto-derived from `CLAUDE.md` by the pre-commit hook — do **not** edit them by hand.
440
382
 
441
- **同步命令(每次发版后执行):**
442
- ```bash
443
- # 1. 同步 skills + plugin.json
444
- cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md
445
- cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/
446
- cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/
447
- # 2. 手动更新 team-skills README(工具数、功能列表、更新日志)+ SKILL.md(version + allowed-tools)
448
- # 3. 走 PR 流程推送 team-skills(禁止直接推 main)
449
- ```
383
+ 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.
450
384
 
451
385
  ### Keeping ROADMAP.md up to date
452
- - When completing a feature or fixing a bug, check the corresponding item in ROADMAP.md as `[x]` done
453
- - When discovering new bugs, limitations, or feature ideas during development, add them to the appropriate section in ROADMAP.md
454
- - When a version is released (tag pushed), move completed items under the "已完成" section with the version number
455
- - When researching a direction and deciding not to implement, add it to "已调研但暂不实施" with the reasoning
456
-
457
- ### When adding new tools
458
- 1. Add method to `src/official.js`(Official API)or `src/client.js`(Cookie 身份)
459
- 2. Add tool definition to `TOOLS` array in `src/index.js`
460
- 3. Add handler case in `handleTool()` switch in `src/index.js`
461
- 4. Run `node -c src/official.js && node -c src/index.js` to verify syntax
462
- 5. Update this file (CLAUDE.md) — tool count, tool list, usage patterns
463
- 6. Update ROADMAP.md if relevant
386
+ 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.
387
+
388
+ ### When adding new tools (post-v1.3.7 layout)
389
+ 1. Add the underlying API method to the right domain file:
390
+ - Official API → `src/clients/official/<domain>.js` (im, docs, bitable, drive, wiki, calendar, okr, uploads, contacts, groups). Cross-domain helpers stay in `src/clients/official/base.js`.
391
+ - Cookie identity `src/clients/user.js`.
392
+ 2. Add the MCP tool schema + handler to `src/tools/<domain>.js`. Each module exports `{ schemas: [...], handlers: { [name]: async (args, ctx) => MCPResponse } }` — see existing tools for the pattern. Handlers receive `ctx` (factories, profile state, resolveDocId — see `src/tools/_registry.js` docstring).
393
+ 3. If the new file is a brand-new domain (rare), also append it to the `TOOL_MODULES` list in `src/server.js`.
394
+ 4. Run smoke: `npm run smoke:baseline` to update the baseline (only when adding/removing/renaming tools is intentional), then `npm run smoke` to verify no other regression. For pure body changes (no schema delta) just `npm run smoke` should pass against the existing baseline.
395
+ 5. `node -c` lint each touched file.
396
+ 6. Update this file (CLAUDE.md) — tool count, tool list, usage patterns. See `docs/REFACTOR-NOTES.md` for the file-responsibility matrix.
397
+ 7. Update ROADMAP.md if relevant.
464
398
 
465
399
  ### When fixing bugs
466
400
  1. Write a standalone test script (`node -e "..."`) to reproduce the bug before fixing
467
401
  2. After fixing, verify with the same script
468
402
  3. If the bug affects MCP tool behavior, test via MCP tool call after server restart
469
403
 
404
+ ### Testing methodology
405
+ See `docs/TESTING-METHODOLOGY.md` for the full regression playbook (when to use unit / smoke / live MCP / `scripts/test-all-tools.js`). The semi-automated path is `node scripts/test-all-tools.js`; the smoke gate is `npm run smoke` (regenerate baseline with `npm run smoke:baseline` only when a tool schema delta is intentional).
406
+
470
407
  ### Commit conventions
471
408
  - `feat:` new tools or capabilities
472
409
  - `fix:` bug fixes
@@ -526,7 +463,7 @@ feishu-user-plugin vX.Y.Z 发布
526
463
 
527
464
  **写作规范**:
528
465
  - **开篇**:一到两句陈述式总结,不宣传、不夸大。参考 v1.3.2:"本次更新主要补齐了 X 能力,并修复了 Y 问题;同时将 Z 统一调整为 ..."
529
- - **每条 bullet**:先写用户可见现象,再写底层机制。引用具体错误码(如 1770032 / 91403)、接口名(如 create_doc_block)、参数名(如 RichText.atIds)——专业读者信赖的是细节
466
+ - **每条 bullet**:先写用户可见现象,再写底层机制。引用具体错误码(如 1770032 / 91403)、接口名(如 manage_doc_block)、参数名(如 RichText.atIds)——专业读者信赖的是细节
530
467
  - **字符**:bullet 用 `•`(U+2022),不用 `-` 或 `*`;代码/工具名在正文中直接写,不加反引号
531
468
  - **禁用**:emoji、🔴🟡🟢 之类严重度标记、`@` 任何人、营销词("强大"、"全新"、"重磅")、夸张修辞
532
469
  - **语气**:技术 release note 的中性语气,像写给同行的内部更新。参考 v1.3.2 全文
@@ -538,18 +475,6 @@ feishu-user-plugin vX.Y.Z 发布
538
475
 
539
476
  **发送前**:始终先用 `send_to_user` 或类似工具发给用户自己审核,或直接以文本形式贴在对话里等用户批准。用户说"发"才调 `send_post_as_user` 到目标群。
540
477
 
541
- ### Syncing to team-skills (after any CLAUDE.md or skills change)
542
- 1. Copy CLAUDE.md to skill reference: `cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md`
543
- 2. Sync to team-skills repo: `cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/`
544
- 3. Also sync plugin.json: `cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/`
545
- 4. Update SKILL.md version + allowed-tools, README.md changelog + tool count
546
- 5. **走 PR 流程**(创建 branch → push → PR → 等 CI 通过 → merge),禁止直接推 main
547
-
548
- ### Testing a tool
549
- - For Official API tools: can test directly via MCP tool call or standalone script using `readCredentials()` from `src/config.js`
550
- - For Cookie tools: need active session, test via MCP tool call
551
- - Always verify `_safeSDKCall` handles the response format (multipart uploads return data at top level, not nested under `.data`)
552
-
553
478
  ## OAuth Scopes (when re-running `npx feishu-user-plugin oauth`)
554
479
 
555
480
  The v1.3.4 tools require additional scopes on the app + UAT:
@@ -557,8 +482,11 @@ The v1.3.4 tools require additional scopes on the app + UAT:
557
482
  | Feature | Scopes to enable on app + include in OAuth |
558
483
  |---------|-------------------------------------------|
559
484
  | OKR read | `okr:okr:readonly`, `okr:period:read` |
485
+ | OKR progress write (v1.3.7: create/delete_okr_progress_record) | `okr:okr.content:write` |
560
486
  | Calendar read | `calendar:calendar:readonly`, `calendar:calendar.event:read` |
561
- | Docx/Bitable/Drive media upload (`uploadMedia`, `upload_drive_file`, `upload_bitable_attachment`, `create_doc_block` image/file) | `drive:drive`, `drive:file:upload`, `docs:document.media:upload`, `sheets:spreadsheet` (only for sheet uploads) |
487
+ | Calendar write (v1.3.7: create/update/delete/respond_calendar_event) | `calendar:calendar.event:write` |
488
+ | Tasks v2 (v1.3.7: list/get/create/update/complete/delete_task, manage_task_members) | `task:task` |
489
+ | Docx/Bitable/Drive media upload (`uploadMedia`, `upload_drive_file`, `upload_bitable_attachment`, `manage_doc_block(action=create, image_path|file_path|...)`) | `drive:drive`, `drive:file:upload`, `docs:document.media:upload`, `sheets:spreadsheet` (only for sheet uploads) |
562
490
  | Wiki attach (`move_docs_to_wiki`) | `wiki:wiki` (edit scope, the readonly one is insufficient) |
563
491
 
564
492
  If a tool returns `access_denied` or error code `99991672` (scope not granted), the scope is missing on either the app or the UAT. Re-run `npx feishu-user-plugin oauth` so the UAT picks up the latest scope list (defined in `src/oauth.js`).
@@ -568,6 +496,8 @@ If a tool returns `access_denied` or error code `99991672` (scope not granted),
568
496
  - External tenant users may not be resolvable via `get_user_info` (contact API scope limitation)
569
497
  - Cookie auth requires human interaction (QR scan) — cannot be fully automated
570
498
  - Refresh token expires after 7 days without use — set up `keepalive` cron to prevent this
571
- - `update_bitable_field` requires `type` parameter even when only changing field name (Feishu API requirement)
572
- - `list_wiki_spaces` may return empty if bot lacks `wiki:wiki:readonly` permission
499
+ - `manage_bitable_field(action=update)` requires `type` parameter even when only changing field name (Feishu API requirement)
500
+ - `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)
501
+ - `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.
573
502
  - `search_wiki` uses same API as `search_docs` — `docs_types` filter may not work as expected
503
+ - `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.)