feishu-user-plugin 1.3.2 → 1.3.4
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.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +37 -0
- package/README.md +19 -4
- package/package.json +2 -2
- package/skills/feishu-user-plugin/SKILL.md +3 -3
- package/skills/feishu-user-plugin/references/CLAUDE.md +114 -5
- package/src/client.js +4 -4
- package/src/doc-blocks.js +70 -0
- package/src/error-codes.js +78 -0
- package/src/index.js +318 -68
- package/src/oauth.js +6 -1
- package/src/official.js +584 -15
- package/src/resolver.js +151 -0
- package/src/utils.js +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself (incl. real @-mentions), read chats, manage docs/
|
|
3
|
+
"version": "1.3.4",
|
|
4
|
+
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself (incl. real @-mentions), read chats, manage docs (with image read/write) / bitable / wiki (native + move_docs_to_wiki) / drive / OKR / calendar. 74 tools + 9 skills, 3 auth layers.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "EthanQC"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.3.4] - 2026-04-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Wiki-hosted content is now first-class**: every docx and bitable tool accepts the `document_id` / `app_token` parameter in three forms — native token (unchanged), wiki node token (`wikcnXXX` / `wikmXXX` / `wiknXXX`), or a full Feishu URL (`https://xxx.feishu.cn/docx/XXX`, `.../wiki/XXX`, `.../base/XXX`). A new `src/resolver.js` parses the input, calls `wiki/v2/spaces/get_node` when needed to resolve to `obj_token` + `obj_type`, and caches the mapping for 10 min. Zero-lookup path for direct URLs.
|
|
11
|
+
- **`get_wiki_node` tool**: explicitly resolves a Wiki node to its backing object (`obj_type` + `obj_token` + `space_id`). Useful when you need to branch behaviour on whether a node points at a docx, bitable, sheet, mindnote, file, or slides.
|
|
12
|
+
- **Create docx / bitable directly under Wiki**: `create_doc` / `create_bitable` accept optional `wiki_space_id` (and `wiki_parent_node_token` for nested placement). Plugin creates the resource in drive, then calls `wiki/v2/spaces/{space_id}/nodes/move_docs_to_wiki` to attach it. Returns `wikiNodeToken` on success, `wikiAttachTaskId` when Feishu queues the move, or a warning if attach fails (resource still in drive).
|
|
13
|
+
- **Docx image read**: `download_image` now has a docx mode — pass `image_token` (from `get_doc_blocks` image block) and optional `doc_token` (native / wiki node / URL). Routes through `drive/v1/medias/{token}/download`, returns base64 as MCP image content so the model sees the pixels.
|
|
14
|
+
- **Docx image write**: `create_doc_block` gains two shortcut parameters — `image_path` (local file) automatically runs the three-step Feishu flow (create empty image block → upload via `drive/v1/medias/upload_all` with `parent_type=docx_image` and the new block_id → patch with `replace_image`); `image_token` reuses an already-uploaded media token. `update_doc_block` accepts `image_token` to swap the picture in an existing image block.
|
|
15
|
+
- **`list_user_okrs` / `get_okrs` / `list_okr_periods` tools**: read a user's OKRs, batch fetch full objective + key result details (progress, alignments, mentions), and enumerate periods. UAT-first with app fallback when the OKR scope is granted.
|
|
16
|
+
- **`list_calendars` / `list_calendar_events` / `get_calendar_event` tools**: list the user's calendars (primary / shared / subscribed), list events in a time window, and fetch full event details (attendees, location, meeting links, attachments).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **External-group `read_messages` hardening**: new `src/error-codes.js` classifies bot failures. Known-needs-UAT codes (`240001` external tenant, `70009` no permission, `70003` / `99991668` bot not in chat, `19001` chat not found) hop straight to UAT. Transient codes (`42101` rate limit, `5xx`, `ECONNRESET`, fetch timeouts) retry once after a 2 s delay before falling back. Response now includes `via: "bot" | "user" | "contacts"` and, when fallback fires, `via_reason` (e.g. `bot_external_tenant`). When the `chat_id` was discovered via `search_contacts` (i.e. definitely external) the bot path is skipped entirely.
|
|
20
|
+
- **Raw Feishu payload no longer leaks when UAT is missing**: bot failures with no UAT configured now produce `Cannot read chat <id> as bot (<reason>). To read external/private groups, configure UAT via: npx feishu-user-plugin oauth` — previously the caller got the unwrapped Feishu error JSON.
|
|
21
|
+
- **`_uatREST` array query params**: OKR / calendar endpoints that take repeated query keys (e.g. `period_ids=p1&period_ids=p2`) now serialize correctly. Previously `URLSearchParams(query)` would call `toString` on arrays and produce CSV, which Feishu rejects.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Tool count 67 → **74** (+7: `get_wiki_node`, `list_user_okrs`, `get_okrs`, `list_okr_periods`, `list_calendars`, `list_calendar_events`, `get_calendar_event`).
|
|
25
|
+
- `getWikiNode(nodeToken, _spaceId)` — `spaceId` parameter position swapped; retained only for backward-compatibility of any external caller. The endpoint itself ignores `space_id`.
|
|
26
|
+
- `create_doc_block` no longer requires `children` — callers who use the new `image_path` or `image_token` shortcut omit it. One of `children` / `image_path` / `image_token` must be provided.
|
|
27
|
+
|
|
28
|
+
## [1.3.3] - 2026-04-20
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **MCP mid-session disconnect (root fix)**: All raw `fetch` calls to Feishu now go through `fetchWithTimeout` (AbortController, 30s default). A stalled connection used to hang a tool handler indefinitely; the MCP client would time out and some clients tore down the stdio transport — observed as "MCP 中途掉线" on v1.3.2. This was the real cause, not just the v1.3.1 stdout pollution.
|
|
32
|
+
- **stdout pollution (defense-in-depth)**: `src/index.js` now globally redirects `console.log` / `console.info` to stderr at startup, before any other `require`. Any current or future dependency that accidentally writes to stdout can no longer corrupt the JSON-RPC channel. (v1.3.1's Lark-SDK-specific logger override stays as-is.)
|
|
33
|
+
- **`(as user)` label lied for docs/bitable/folder creation**: `create_doc` / `create_bitable` / `create_folder` previously labeled every successful call `(as user)` whenever `LARK_USER_ACCESS_TOKEN` was set, even when the UAT call actually failed and silently fell back to app identity. `_asUserOrApp` now threads a real `_viaUser` flag through; failures show `(as app — UAT unavailable or failed; <resource> owned by the app, not you)`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- **APP_ID startup validation**: MCP server probes `/auth/v3/app_access_token/internal` at boot. Invalid `LARK_APP_ID` / `LARK_APP_SECRET` (wrong-tenant, stale, or hallucinated by an autoinstall) now produce a clear stderr error pointing at the team-skills install prompt. Non-blocking — users running cookie-only workflows are unaffected.
|
|
37
|
+
- **`get_login_status` shows app identity**: Now returns the actual `app_id` plus fetched app name, so users can immediately spot "this isn't my team's app" scenarios.
|
|
38
|
+
- **`download_image` tool**: Download an image embedded in a message by `message_id` + `image_key`, returned as MCP image content so the model can see the pixels (not just the key string). Tries UAT first (works for any chat the user is in); falls back to app token (requires the bot to be in the chat).
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Tool count 66 → **67** (added `download_image`).
|
|
42
|
+
- README tool badge corrected from 76 → 67 (previous 76 was stale and never matched the actual export).
|
|
43
|
+
|
|
7
44
|
## [1.1.3] - 2026-03-11
|
|
8
45
|
|
|
9
46
|
### Fixed
|
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://nodejs.org)
|
|
5
5
|
[](https://modelcontextprotocol.io)
|
|
6
|
-
[](#tools)
|
|
7
7
|
[](CONTRIBUTING.md)
|
|
8
8
|
|
|
9
|
-
**All-in-one Feishu/Lark MCP Server --
|
|
9
|
+
**All-in-one Feishu/Lark MCP Server -- 74 tools, 9 skills, 3 auth layers for messaging, docs, bitable, calendar, tasks, drive, OKR, and more.**
|
|
10
10
|
|
|
11
11
|
The only MCP server that lets you send messages as your **personal identity** (not a bot), while also integrating the full official Feishu API. Works with Claude Code, Cursor, Windsurf, OpenClaw, and any MCP-compatible client.
|
|
12
12
|
|
|
@@ -337,7 +337,7 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
337
337
|
}
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
## Tools (
|
|
340
|
+
## Tools (74 total)
|
|
341
341
|
|
|
342
342
|
### User Identity -- Messaging (8 tools, cookie auth)
|
|
343
343
|
|
|
@@ -390,6 +390,21 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
390
390
|
| `add_members` | Add users to a group |
|
|
391
391
|
| `remove_members` | Remove users from a group |
|
|
392
392
|
| `upload_image` / `upload_file` | Upload image/file, returns key for sending |
|
|
393
|
+
| `download_image` | Download a chat-message image (message_id + image_key) OR a docx image (image_token + optional doc_token). Returned as MCP image content. |
|
|
394
|
+
|
|
395
|
+
### Wiki, OKR, and Calendar (v1.3.4)
|
|
396
|
+
|
|
397
|
+
| Tool | Description |
|
|
398
|
+
|------|-------------|
|
|
399
|
+
| `get_wiki_node` | Resolve a Wiki node token to its underlying obj_type + obj_token + space_id |
|
|
400
|
+
| `list_user_okrs` | List a user's OKRs (requires open_id; filter by period_ids) |
|
|
401
|
+
| `get_okrs` | Batch-fetch full OKR details (objectives, key results, progress, alignments) |
|
|
402
|
+
| `list_okr_periods` | List OKR periods (quarters / years) |
|
|
403
|
+
| `list_calendars` | List the current user's calendars (primary + shared + subscribed) |
|
|
404
|
+
| `list_calendar_events` | List events in a calendar within a time range |
|
|
405
|
+
| `get_calendar_event` | Full event details (attendees, location, meeting link, attachments) |
|
|
406
|
+
|
|
407
|
+
All docx / bitable tools' `document_id` / `app_token` parameter also accepts a Wiki node token or a full Feishu URL — the plugin resolves it transparently.
|
|
393
408
|
|
|
394
409
|
### Official API -- Documents (7 tools)
|
|
395
410
|
|
|
@@ -508,7 +523,7 @@ feishu-user-plugin/
|
|
|
508
523
|
│ ├── SKILL.md # Main skill definition (trigger, tools, auth)
|
|
509
524
|
│ └── references/ # 8 skill reference docs + CLAUDE.md
|
|
510
525
|
├── src/
|
|
511
|
-
│ ├── index.js # MCP server entry point (
|
|
526
|
+
│ ├── index.js # MCP server entry point (74 tools)
|
|
512
527
|
│ ├── client.js # User identity client (Protobuf gateway)
|
|
513
528
|
│ ├── official.js # Official API client (REST, UAT)
|
|
514
529
|
│ ├── utils.js # ID generators, cookie parser
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "All-in-one Feishu plugin for Claude Code & Codex — messaging, docs, bitable, wiki, drive.
|
|
3
|
+
"version": "1.3.4",
|
|
4
|
+
"description": "All-in-one Feishu plugin for Claude Code & Codex — messaging, docs (with image read/write), bitable, wiki (native + move_docs_to_wiki), drive, OKR, calendar. 74 tools + 9 skills, 3 auth layers.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"feishu-user-plugin": "src/cli.js"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feishu-user-plugin
|
|
3
|
-
version: "1.
|
|
4
|
-
description: "All-in-one Feishu plugin — send messages as yourself, read group/P2P chats, manage docs/tables/wiki. Replaces and extends the official Feishu MCP."
|
|
5
|
-
allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_as_user, send_sticker_as_user, send_audio_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, read_p2p_messages, list_user_chats, list_chats, read_messages, reply_message, forward_message, search_docs, read_doc, create_doc, list_bitable_tables, list_bitable_fields, search_bitable_records, create_bitable_record, update_bitable_record, list_wiki_spaces, search_wiki, list_wiki_nodes, list_files, create_folder, find_user
|
|
3
|
+
version: "1.3.4"
|
|
4
|
+
description: "All-in-one Feishu plugin — send messages as yourself, read group/P2P chats, manage docs/tables/wiki (with wiki-node + URL transparent resolution), OKR, calendar, docx image read/write. Replaces and extends the official Feishu MCP."
|
|
5
|
+
allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_as_user, send_sticker_as_user, send_audio_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, read_p2p_messages, list_user_chats, list_chats, read_messages, reply_message, forward_message, search_docs, read_doc, create_doc, list_bitable_tables, list_bitable_fields, search_bitable_records, create_bitable_record, update_bitable_record, list_wiki_spaces, search_wiki, list_wiki_nodes, get_wiki_node, list_files, create_folder, find_user, download_image, list_user_okrs, get_okrs, list_okr_periods, list_calendars, list_calendar_events, get_calendar_event
|
|
6
6
|
user_invocable: true
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -6,7 +6,7 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
|
|
|
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 (
|
|
9
|
+
## Tool Categories (74 tools)
|
|
10
10
|
|
|
11
11
|
### User Identity — Messaging (reverse-engineered, cookie-based)
|
|
12
12
|
- `send_to_user` — Search user + send text (one step, most common). Returns candidates if multiple matches.
|
|
@@ -48,14 +48,48 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
|
|
|
48
48
|
- `list_bitable_views` / `create_bitable_view` / `delete_bitable_view` — View management (grid, kanban, gallery, form, gantt, calendar)
|
|
49
49
|
- `search_bitable_records` / `get_bitable_record` — Query records
|
|
50
50
|
- `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` — Record CRUD (single or batch, max 500/call)
|
|
51
|
-
- `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` — Wiki
|
|
51
|
+
- `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.)
|
|
52
52
|
- `list_files` / `create_folder` — Drive
|
|
53
53
|
- `copy_file` / `move_file` / `delete_file` — Drive file operations (copy, move, delete)
|
|
54
54
|
- `upload_image` / `upload_file` — Upload image/file, returns key for send_image/send_file
|
|
55
|
+
- `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.
|
|
56
|
+
- `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.
|
|
57
|
+
- `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.
|
|
55
58
|
- `find_user` — Contact lookup by email/mobile
|
|
56
59
|
|
|
57
60
|
## Usage Patterns
|
|
58
61
|
|
|
62
|
+
### Wiki-hosted content (docx / bitable / sheet)
|
|
63
|
+
All docx and bitable tools now accept three input forms for their `document_id` / `app_token` parameter:
|
|
64
|
+
- Native token (unchanged): `doccnXXX`, `docxXXX`, `bascnXXX`, ...
|
|
65
|
+
- Wiki node token: `wikcnXXX`, `wikmXXX`, `wiknXXX`
|
|
66
|
+
- Full Feishu URL: `https://xxx.feishu.cn/docx/XXX`, `.../wiki/XXX`, `.../base/XXX`
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
Create content directly into a Wiki space:
|
|
70
|
+
- `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.
|
|
71
|
+
|
|
72
|
+
### Document images
|
|
73
|
+
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.
|
|
74
|
+
Write — `create_doc_block` now has image shortcuts:
|
|
75
|
+
- `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.
|
|
76
|
+
- `image_token` (already uploaded) → plugin creates block and attaches token.
|
|
77
|
+
`update_doc_block` accepts `image_token` to swap the picture in an existing image block.
|
|
78
|
+
|
|
79
|
+
### OKR
|
|
80
|
+
1. `list_okr_periods` — find the period id for current quarter.
|
|
81
|
+
2. `list_user_okrs(user_id=<open_id>, period_ids=[...])` — list the target user's OKRs.
|
|
82
|
+
3. `get_okrs(okr_ids)` — batch fetch full objective + key result structure with progress + alignments.
|
|
83
|
+
`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).
|
|
84
|
+
|
|
85
|
+
### Calendar
|
|
86
|
+
1. `list_calendars` — get your calendars; the one with `type=primary` is your personal calendar.
|
|
87
|
+
2. `list_calendar_events(calendar_id, start_time=<unix_sec>, end_time=<unix_sec>)` — list events in a time window.
|
|
88
|
+
3. `get_calendar_event(calendar_id, event_id)` — full details (attendees, location, attachments, meeting link).
|
|
89
|
+
|
|
90
|
+
### External-group message read (hardened in v1.3.4)
|
|
91
|
+
`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.
|
|
92
|
+
|
|
59
93
|
### Messaging
|
|
60
94
|
- Send text as yourself → `send_to_user` or `send_to_group`
|
|
61
95
|
- Send image → `upload_image` → `send_image_as_user`
|
|
@@ -233,9 +267,21 @@ Tell user to restart Claude Code. Only ONE restart should be needed.
|
|
|
233
267
|
## Troubleshooting Guide
|
|
234
268
|
|
|
235
269
|
### If MCP disconnects mid-session
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
270
|
+
Two known root causes, both fixed in v1.3.3:
|
|
271
|
+
|
|
272
|
+
1. **stdout pollution** (partial fix in v1.3.1, fully closed in v1.3.3):
|
|
273
|
+
- `@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.
|
|
274
|
+
- 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.
|
|
275
|
+
|
|
276
|
+
2. **unbounded fetch hangs** (fixed in v1.3.3):
|
|
277
|
+
- 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".
|
|
278
|
+
- Fix: `utils.js::fetchWithTimeout` with `AbortController`, 30s default. All `client.js` + `official.js` fetches go through it.
|
|
279
|
+
- 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`.
|
|
280
|
+
|
|
281
|
+
### If Official API tools return 401 / "token invalid" every time
|
|
282
|
+
- **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).
|
|
283
|
+
- **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.
|
|
284
|
+
- **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.
|
|
239
285
|
|
|
240
286
|
### If MCP tools are not available
|
|
241
287
|
1. Check `~/.claude.json` — config must be in **top-level** `mcpServers`, not inside `projects[*]`
|
|
@@ -346,6 +392,7 @@ When making ANY code change (new tools, bug fixes, features), update ALL of thes
|
|
|
346
392
|
|
|
347
393
|
**本仓库内:**
|
|
348
394
|
- `CLAUDE.md` — tool count, tool list, usage patterns, known limitations
|
|
395
|
+
- `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`
|
|
349
396
|
- `README.md` — tool count (badge + heading + tool table), feature highlights, OpenClaw/Claude Code config examples
|
|
350
397
|
- `ROADMAP.md` — check off completed items, add new findings
|
|
351
398
|
- `package.json` — version, description (tool count)
|
|
@@ -408,6 +455,55 @@ Steps:
|
|
|
408
455
|
3. `git add <files> && git commit -m "v1.x.x: description"`
|
|
409
456
|
4. `git tag v1.x.x && git push && git push --tags`
|
|
410
457
|
5. GitHub Actions verifies tag matches package.json, then auto-publishes to npm
|
|
458
|
+
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.
|
|
459
|
+
|
|
460
|
+
### Release announcement rules (every release)
|
|
461
|
+
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.
|
|
462
|
+
|
|
463
|
+
**Transport**: `send_post_as_user` (rich-text post). No @-mentions — announcements are impersonal broadcasts. No emojis. No marketing language.
|
|
464
|
+
|
|
465
|
+
**Structure** (in this order; omit a section if it doesn't apply this release):
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
feishu-user-plugin vX.Y.Z 发布
|
|
469
|
+
|
|
470
|
+
<一到两句开篇总结本次发布的主题,陈述语气,不推销>
|
|
471
|
+
|
|
472
|
+
修复
|
|
473
|
+
• <缺陷描述>:<根因与修复机制,引用具体错误码/接口名/参数>
|
|
474
|
+
• ...
|
|
475
|
+
|
|
476
|
+
新增
|
|
477
|
+
• 新增 <tool 名> 工具:<一句话功能描述>。<关键约束或调用条件>
|
|
478
|
+
• ...
|
|
479
|
+
|
|
480
|
+
调整
|
|
481
|
+
• <行为变化的描述>
|
|
482
|
+
• ...
|
|
483
|
+
|
|
484
|
+
下版本计划
|
|
485
|
+
• <条目>
|
|
486
|
+
• ...
|
|
487
|
+
|
|
488
|
+
升级方式
|
|
489
|
+
• 重启 Claude Code / Codex 即可自动拉取 X.Y.Z
|
|
490
|
+
• <若有相关新日志/错误提示,说明怎么应对>
|
|
491
|
+
• 建议复测 N 个场景:<场景 1>、<场景 2>、<场景 3>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**写作规范**:
|
|
495
|
+
- **开篇**:一到两句陈述式总结,不宣传、不夸大。参考 v1.3.2:"本次更新主要补齐了 X 能力,并修复了 Y 问题;同时将 Z 统一调整为 ..."
|
|
496
|
+
- **每条 bullet**:先写用户可见现象,再写底层机制。引用具体错误码(如 1770032 / 91403)、接口名(如 create_doc_block)、参数名(如 RichText.atIds)——专业读者信赖的是细节
|
|
497
|
+
- **字符**:bullet 用 `•`(U+2022),不用 `-` 或 `*`;代码/工具名在正文中直接写,不加反引号
|
|
498
|
+
- **禁用**:emoji、🔴🟡🟢 之类严重度标记、`@` 任何人、营销词("强大"、"全新"、"重磅")、夸张修辞
|
|
499
|
+
- **语气**:技术 release note 的中性语气,像写给同行的内部更新。参考 v1.3.2 全文
|
|
500
|
+
- **长度**:单屏为宜,一般 400–700 汉字。每条 bullet 一到三行
|
|
501
|
+
- **下版本计划**:复制自上一版公告仍未完成的条目 + 本次发布中暴露的新方向。本版已完成的条目必须删除
|
|
502
|
+
- **升级方式**:至少包含重启指令;若本次修了某类错误(如 APP_ID 校验),列出对应诊断日志字样;以"建议复测 N 个场景"收尾,场景要具体可操作
|
|
503
|
+
|
|
504
|
+
**结尾**:不加 CHANGELOG 链接(v1.3.2 风格未含链接,群内读者不需要)。
|
|
505
|
+
|
|
506
|
+
**发送前**:始终先用 `send_to_user` 或类似工具发给用户自己审核,或直接以文本形式贴在对话里等用户批准。用户说"发"才调 `send_post_as_user` 到目标群。
|
|
411
507
|
|
|
412
508
|
### Syncing to team-skills (after any CLAUDE.md or skills change)
|
|
413
509
|
1. Copy CLAUDE.md to skill reference: `cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md`
|
|
@@ -421,6 +517,19 @@ Steps:
|
|
|
421
517
|
- For Cookie tools: need active session, test via MCP tool call
|
|
422
518
|
- Always verify `_safeSDKCall` handles the response format (multipart uploads return data at top level, not nested under `.data`)
|
|
423
519
|
|
|
520
|
+
## OAuth Scopes (when re-running `npx feishu-user-plugin oauth`)
|
|
521
|
+
|
|
522
|
+
The v1.3.4 tools require additional scopes on the app + UAT:
|
|
523
|
+
|
|
524
|
+
| Feature | Scopes to enable on app + include in OAuth |
|
|
525
|
+
|---------|-------------------------------------------|
|
|
526
|
+
| OKR read | `okr:okr:readonly`, `okr:period:read` |
|
|
527
|
+
| Calendar read | `calendar:calendar:readonly`, `calendar:calendar.event:read` |
|
|
528
|
+
| Docx image write (`uploadDocMedia`) | `drive:drive`, `docx:document`, `docx:document:readonly` (image upload piggy-backs on existing docx editing scopes) |
|
|
529
|
+
| Wiki attach (`move_docs_to_wiki`) | `wiki:wiki` (edit scope, the readonly one is insufficient) |
|
|
530
|
+
|
|
531
|
+
If a tool returns `access_denied` or error code `99991672` (scope not granted), enable the missing scope at https://open.feishu.cn/app → Permissions, then re-run `npx feishu-user-plugin oauth` so the UAT picks up the new scopes.
|
|
532
|
+
|
|
424
533
|
## Known Limitations
|
|
425
534
|
- CARD message type (type=14) not yet implemented — complex JSON schema
|
|
426
535
|
- External tenant users may not be resolvable via `get_user_info` (contact API scope limitation)
|
package/src/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const protobuf = require('protobufjs');
|
|
3
|
-
const { generateRequestId, generateCid, parseCookie, formatCookie } = require('./utils');
|
|
3
|
+
const { generateRequestId, generateCid, parseCookie, formatCookie, fetchWithTimeout } = require('./utils');
|
|
4
4
|
|
|
5
5
|
const GATEWAY_URL = 'https://internal-api-lark-api.feishu.cn/im/gateway/';
|
|
6
6
|
const CSRF_URL = 'https://internal-api-lark-api.feishu.cn/accounts/csrf';
|
|
@@ -47,7 +47,7 @@ class LarkUserClient {
|
|
|
47
47
|
// --- Auth ---
|
|
48
48
|
|
|
49
49
|
async _getCsrfToken() {
|
|
50
|
-
const res = await
|
|
50
|
+
const res = await fetchWithTimeout(`${CSRF_URL}?_t=${Date.now()}`, {
|
|
51
51
|
method: 'POST',
|
|
52
52
|
headers: {
|
|
53
53
|
...this._jsonHeaders(),
|
|
@@ -68,7 +68,7 @@ class LarkUserClient {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async _getUserInfo() {
|
|
71
|
-
const res = await
|
|
71
|
+
const res = await fetchWithTimeout(`${USER_INFO_URL}?app_id=12&_t=${Date.now()}`, {
|
|
72
72
|
headers: {
|
|
73
73
|
...this._jsonHeaders(),
|
|
74
74
|
'x-csrf-token': this.csrfToken || '',
|
|
@@ -179,7 +179,7 @@ class LarkUserClient {
|
|
|
179
179
|
cid: generateRequestId(),
|
|
180
180
|
payload: reqBuf,
|
|
181
181
|
});
|
|
182
|
-
const res = await
|
|
182
|
+
const res = await fetchWithTimeout(GATEWAY_URL, {
|
|
183
183
|
method: 'POST',
|
|
184
184
|
headers: this._protoHeaders(cmd, cmdVersion),
|
|
185
185
|
body: packetBuf,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Helpers for constructing docx block payloads.
|
|
2
|
+
//
|
|
3
|
+
// Right now (v1.3.4) only the image path is implemented — enough to cover the
|
|
4
|
+
// create_doc_block / update_doc_block image shortcuts. More block builders
|
|
5
|
+
// (heading / list / code / table) will land in v1.3.5 when the local-markdown
|
|
6
|
+
// → wiki-docx sync feature is built; parking the skeleton here now so the
|
|
7
|
+
// later additions don't introduce a new file.
|
|
8
|
+
|
|
9
|
+
// docx v1 block_type enum (relevant subset).
|
|
10
|
+
// Docs: https://open.feishu.cn/document/server-docs/docs/docs/docx-v1/document-block/create
|
|
11
|
+
const BLOCK_TYPE = {
|
|
12
|
+
PAGE: 1,
|
|
13
|
+
TEXT: 2,
|
|
14
|
+
HEADING1: 3,
|
|
15
|
+
HEADING2: 4,
|
|
16
|
+
HEADING3: 5,
|
|
17
|
+
HEADING4: 6,
|
|
18
|
+
HEADING5: 7,
|
|
19
|
+
HEADING6: 8,
|
|
20
|
+
HEADING7: 9,
|
|
21
|
+
HEADING8: 10,
|
|
22
|
+
HEADING9: 11,
|
|
23
|
+
BULLET: 12,
|
|
24
|
+
ORDERED: 13,
|
|
25
|
+
CODE: 14,
|
|
26
|
+
QUOTE: 15,
|
|
27
|
+
EQUATION: 16,
|
|
28
|
+
TODO: 17,
|
|
29
|
+
BITABLE: 18,
|
|
30
|
+
CALLOUT: 19,
|
|
31
|
+
CHAT_CARD: 20,
|
|
32
|
+
DIAGRAM: 21,
|
|
33
|
+
DIVIDER: 22,
|
|
34
|
+
FILE: 23,
|
|
35
|
+
GRID: 24,
|
|
36
|
+
GRID_COL: 25,
|
|
37
|
+
IFRAME: 26,
|
|
38
|
+
IMAGE: 27,
|
|
39
|
+
TABLE: 31,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The image-block creation flow on Feishu docx v1 is three steps:
|
|
44
|
+
* 1. POST .../blocks/<parent>/children with an empty image placeholder:
|
|
45
|
+
* { block_type: 27, image: {} }
|
|
46
|
+
* This returns a real block_id for the image slot.
|
|
47
|
+
* 2. POST /open-apis/drive/v1/medias/upload_all with parent_type=docx_image
|
|
48
|
+
* and parent_node=<that block_id> to upload the pixels. Returns file_token.
|
|
49
|
+
* 3. PATCH .../blocks/<block_id> with { replace_image: { token: file_token } }
|
|
50
|
+
* to populate the placeholder with the uploaded image.
|
|
51
|
+
*
|
|
52
|
+
* See official.js::createDocBlockWithImage which orchestrates all three steps.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** Empty image block used as the placeholder in step 1. */
|
|
56
|
+
function buildEmptyImageBlock() {
|
|
57
|
+
return { block_type: BLOCK_TYPE.IMAGE, image: {} };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Patch body for step 3 — attaches an uploaded image_token to a placeholder block. */
|
|
61
|
+
function buildReplaceImagePayload(imageToken) {
|
|
62
|
+
if (!imageToken) throw new Error('buildReplaceImagePayload: imageToken is required');
|
|
63
|
+
return { replace_image: { token: imageToken } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
BLOCK_TYPE,
|
|
68
|
+
buildEmptyImageBlock,
|
|
69
|
+
buildReplaceImagePayload,
|
|
70
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Feishu error-code classification for read_messages fallback routing.
|
|
2
|
+
//
|
|
3
|
+
// The v1.3.2 read_messages handler catches any bot failure and unconditionally
|
|
4
|
+
// retries with UAT. That's cheap when the bot fails fast, but it has two flaws
|
|
5
|
+
// in v1.3.3:
|
|
6
|
+
// • Transient errors (rate-limit, network stalls) are treated the same as
|
|
7
|
+
// permanent permission errors — the UAT path runs when a 2-second retry
|
|
8
|
+
// would have worked.
|
|
9
|
+
// • When UAT is absent, the raw Feishu payload leaks to the user verbatim,
|
|
10
|
+
// with no hint that OAuth is the fix.
|
|
11
|
+
//
|
|
12
|
+
// This table classifies known codes into three buckets:
|
|
13
|
+
// 'uat' — permanent bot failure; hop straight to UAT.
|
|
14
|
+
// 'retry' — likely transient; caller should retry once (after short delay)
|
|
15
|
+
// and fall through to UAT if still failing.
|
|
16
|
+
// 'unknown' — not seen before; preserve v1.3.3 behaviour (try UAT silently).
|
|
17
|
+
|
|
18
|
+
const FAILURE_MAP = {
|
|
19
|
+
// External tenant — bot lives in a different tenant, will never be granted.
|
|
20
|
+
240001: { action: 'uat', reason: 'bot_external_tenant' },
|
|
21
|
+
// No permission for the resource (scope missing, or chat restricts bot reads).
|
|
22
|
+
70009: { action: 'uat', reason: 'bot_no_permission' },
|
|
23
|
+
// Bot is not a member of the chat.
|
|
24
|
+
70003: { action: 'uat', reason: 'bot_not_in_chat' },
|
|
25
|
+
99991668: { action: 'uat', reason: 'bot_not_in_chat' },
|
|
26
|
+
// Chat does not exist (from the bot's POV — may still be accessible to user).
|
|
27
|
+
19001: { action: 'uat', reason: 'bot_chat_not_found' },
|
|
28
|
+
|
|
29
|
+
// Rate limited — Feishu throttles, try once more after a brief pause.
|
|
30
|
+
42101: { action: 'retry', reason: 'bot_rate_limited' },
|
|
31
|
+
// Frequency control variants occasionally observed.
|
|
32
|
+
99991400: { action: 'retry', reason: 'bot_rate_limited' },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// HTTP-status / network-error patterns that warrant one retry.
|
|
36
|
+
// Axios-wrapped messages from @larksuiteoapi/node-sdk embed the http status
|
|
37
|
+
// into _safeSDKCall's rethrown message. We match those substrings.
|
|
38
|
+
const TRANSIENT_PATTERNS = [
|
|
39
|
+
/HTTP 5\d\d/i, // Any 5xx from upstream
|
|
40
|
+
/ECONNRESET/i,
|
|
41
|
+
/ETIMEDOUT/i,
|
|
42
|
+
/fetch timeout after/i, // from utils.fetchWithTimeout
|
|
43
|
+
/socket hang up/i,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Classify an error thrown by a bot-API path.
|
|
48
|
+
* Input is either the Feishu code number (preferred) or the Error object —
|
|
49
|
+
* the code is extracted from the message if present.
|
|
50
|
+
*
|
|
51
|
+
* Output: { action: 'uat' | 'retry' | 'unknown', reason: string, code: number|null }
|
|
52
|
+
*/
|
|
53
|
+
function classifyError(errOrCode) {
|
|
54
|
+
let code = null;
|
|
55
|
+
let msg = '';
|
|
56
|
+
if (typeof errOrCode === 'number') {
|
|
57
|
+
code = errOrCode;
|
|
58
|
+
} else if (errOrCode && typeof errOrCode === 'object') {
|
|
59
|
+
msg = errOrCode.message || String(errOrCode);
|
|
60
|
+
// _safeSDKCall formats as "label failed (HTTP N, code=XXX): ..." or "label failed (CODE): ..."
|
|
61
|
+
const m = msg.match(/code[=(]\s*(\d+)/i) || msg.match(/failed\s*\((\d+)\)/i);
|
|
62
|
+
if (m) code = parseInt(m[1], 10);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (code != null && FAILURE_MAP[code]) {
|
|
66
|
+
return { ...FAILURE_MAP[code], code };
|
|
67
|
+
}
|
|
68
|
+
for (const re of TRANSIENT_PATTERNS) {
|
|
69
|
+
if (re.test(msg)) return { action: 'retry', reason: 'bot_network_error', code };
|
|
70
|
+
}
|
|
71
|
+
return { action: 'unknown', reason: 'bot_unknown_error', code };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
classifyError,
|
|
76
|
+
FAILURE_MAP,
|
|
77
|
+
TRANSIENT_PATTERNS,
|
|
78
|
+
};
|