feishu-user-plugin 1.3.4 → 1.3.6
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/README.md +25 -11
- package/package.json +2 -2
- package/scripts/test-uat-race-child.js +24 -0
- package/scripts/test-uat-race.js +68 -0
- package/skills/feishu-user-plugin/SKILL.md +3 -3
- package/skills/feishu-user-plugin/references/CLAUDE.md +39 -6
- package/src/doc-blocks.js +20 -5
- package/src/index.js +332 -30
- package/src/oauth.js +4 -1
- package/src/official.js +471 -53
|
@@ -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.6",
|
|
4
|
+
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself (incl. batch_send and real @-mentions), read chats (auto-expanded merge_forward), manage docs (image + file blocks) / bitable (with attachment upload) / wiki / drive (file upload + wiki attach) / OKR / calendar / multi-profile. 81 tools + 9 skills, 3 auth layers. v1.3.6: upload completeness, sheets:spreadsheet scope, batch_send, multi-profile, send_card_as_user (bot-default).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "EthanQC"
|
|
7
7
|
},
|
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 -- 81 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,9 +337,9 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
337
337
|
}
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
## Tools (
|
|
340
|
+
## Tools (81 total)
|
|
341
341
|
|
|
342
|
-
### User Identity -- Messaging (
|
|
342
|
+
### User Identity -- Messaging (10 tools, cookie auth)
|
|
343
343
|
|
|
344
344
|
| Tool | Description |
|
|
345
345
|
|------|-------------|
|
|
@@ -351,6 +351,8 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
351
351
|
| `send_post_as_user` | Send rich text with title + formatted paragraphs |
|
|
352
352
|
| `send_sticker_as_user` | Send sticker/emoji |
|
|
353
353
|
| `send_audio_as_user` | Send audio message |
|
|
354
|
+
| `batch_send` | Fan-out send to multiple targets in one call (text / image / file / post). v1.3.6 |
|
|
355
|
+
| `send_card_as_user` | Send a Feishu interactive card. v1.3.6 default routes through bot identity; user-identity is reserved for v1.3.7. |
|
|
354
356
|
|
|
355
357
|
### User Identity -- Contacts & Info (5 tools, cookie auth)
|
|
356
358
|
|
|
@@ -390,7 +392,8 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
390
392
|
| `add_members` | Add users to a group |
|
|
391
393
|
| `remove_members` | Remove users from a group |
|
|
392
394
|
| `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. |
|
|
395
|
+
| `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. For merge_forward children, use the child's `parentMessageId`, not the child id. |
|
|
396
|
+
| `download_file` | Download a file attachment (msg_type=file). Returns base64 + mimeType + byte count; optional `save_path` writes to disk. Same parent-id rule for merge_forward children. (v1.3.5) |
|
|
394
397
|
|
|
395
398
|
### Wiki, OKR, and Calendar (v1.3.4)
|
|
396
399
|
|
|
@@ -414,11 +417,11 @@ All docx / bitable tools' `document_id` / `app_token` parameter also accepts a W
|
|
|
414
417
|
| `read_doc` | Read raw text content |
|
|
415
418
|
| `get_doc_blocks` | Get structured block tree |
|
|
416
419
|
| `create_doc` | Create a new document |
|
|
417
|
-
| `create_doc_block` | Insert content blocks
|
|
418
|
-
| `update_doc_block` | Update a specific block |
|
|
420
|
+
| `create_doc_block` | Insert content blocks: generic `children`, `image_path` / `image_token` (image block), `file_path` / `file_token` (file attachment block, v1.3.6) |
|
|
421
|
+
| `update_doc_block` | Update a specific block: generic `update_body`, `image_token`, or `file_token` (v1.3.6) |
|
|
419
422
|
| `delete_doc_blocks` | Delete a range of blocks |
|
|
420
423
|
|
|
421
|
-
### Official API -- Bitable (
|
|
424
|
+
### Official API -- Bitable (18 tools)
|
|
422
425
|
|
|
423
426
|
| Tool | Description |
|
|
424
427
|
|------|-------------|
|
|
@@ -429,6 +432,7 @@ All docx / bitable tools' `document_id` / `app_token` parameter also accepts a W
|
|
|
429
432
|
| `search_bitable_records` / `get_bitable_record` | Query records |
|
|
430
433
|
| `create_bitable_record` / `update_bitable_record` / `delete_bitable_record` | Single record CRUD |
|
|
431
434
|
| `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` | Batch operations (max 500/call) |
|
|
435
|
+
| `upload_bitable_attachment` | Upload a file into a Bitable Attachment-type field. Returns `file_token` to write into the field as `[{file_token}]`. v1.3.6 |
|
|
432
436
|
|
|
433
437
|
### Official API -- Calendar (5 tools)
|
|
434
438
|
|
|
@@ -450,7 +454,7 @@ All docx / bitable tools' `document_id` / `app_token` parameter also accepts a W
|
|
|
450
454
|
| `update_task` | Update a task |
|
|
451
455
|
| `complete_task` | Mark task as complete |
|
|
452
456
|
|
|
453
|
-
### Official API -- Drive (
|
|
457
|
+
### Official API -- Drive (6 tools)
|
|
454
458
|
|
|
455
459
|
| Tool | Description |
|
|
456
460
|
|------|-------------|
|
|
@@ -459,6 +463,7 @@ All docx / bitable tools' `document_id` / `app_token` parameter also accepts a W
|
|
|
459
463
|
| `copy_file` | Copy a file |
|
|
460
464
|
| `move_file` | Move a file |
|
|
461
465
|
| `delete_file` | Delete a file/folder |
|
|
466
|
+
| `upload_drive_file` | Upload a local file into a Drive folder (`drive/v1/files/upload_all`). Optional `wiki_space_id` attaches the upload as a Wiki node atomically. v1.3.6 |
|
|
462
467
|
|
|
463
468
|
### Official API -- Wiki & Contacts (4 tools)
|
|
464
469
|
|
|
@@ -467,6 +472,13 @@ All docx / bitable tools' `document_id` / `app_token` parameter also accepts a W
|
|
|
467
472
|
| `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` | Wiki spaces, search, browse |
|
|
468
473
|
| `find_user` | Find user by email or mobile number |
|
|
469
474
|
|
|
475
|
+
### Plugin -- Profiles (2 tools, v1.3.6)
|
|
476
|
+
|
|
477
|
+
| Tool | Description |
|
|
478
|
+
|------|-------------|
|
|
479
|
+
| `list_profiles` | List available identity profiles (default + extras from `LARK_PROFILES_JSON`) and the active one |
|
|
480
|
+
| `switch_profile` | Hot-swap active profile; cached client instances rebuild against new credentials |
|
|
481
|
+
|
|
470
482
|
## Claude Code Slash Commands (9 skills)
|
|
471
483
|
|
|
472
484
|
This plugin includes 9 built-in skills in `skills/feishu-user-plugin/`:
|
|
@@ -508,10 +520,12 @@ Skills are automatically available when the plugin is installed.
|
|
|
508
520
|
|------------|-------|----------|---------|
|
|
509
521
|
| Cookie | `sl_session` | 12h max-age | Auto-refreshed every 4h via heartbeat |
|
|
510
522
|
| App Token | `tenant_access_token` | 2h | Auto-managed by SDK |
|
|
511
|
-
| User OAuth | `user_access_token` | ~2h | Auto-refreshed via `refresh_token`, saved to
|
|
523
|
+
| User OAuth | `user_access_token` | ~2h | Auto-refreshed via `refresh_token`, saved to MCP config |
|
|
512
524
|
|
|
513
525
|
When the cookie expires (after ~12-24h without heartbeat), re-login at feishu.cn and update `LARK_COOKIE`. Use `get_login_status` to check health proactively.
|
|
514
526
|
|
|
527
|
+
If UAT refresh fails with `invalid_grant`, re-run `npx feishu-user-plugin oauth` and restart Claude Code / Codex. v1.3.5+ also re-reads the persisted MCP config before refreshing, so duplicate MCP processes can adopt a token already rotated by another process instead of retrying a stale refresh token.
|
|
528
|
+
|
|
515
529
|
## Project Structure
|
|
516
530
|
|
|
517
531
|
```
|
|
@@ -523,7 +537,7 @@ feishu-user-plugin/
|
|
|
523
537
|
│ ├── SKILL.md # Main skill definition (trigger, tools, auth)
|
|
524
538
|
│ └── references/ # 8 skill reference docs + CLAUDE.md
|
|
525
539
|
├── src/
|
|
526
|
-
│ ├── index.js # MCP server entry point (
|
|
540
|
+
│ ├── index.js # MCP server entry point (81 tools)
|
|
527
541
|
│ ├── client.js # User identity client (Protobuf gateway)
|
|
528
542
|
│ ├── official.js # Official API client (REST, UAT)
|
|
529
543
|
│ ├── 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 (
|
|
3
|
+
"version": "1.3.6",
|
|
4
|
+
"description": "All-in-one Feishu plugin for Claude Code & Codex — messaging (with merge_forward expansion + batch_send), docs (image + file blocks read/write), bitable (with attachment upload), wiki, drive (file upload + wiki attach), OKR, calendar, multi-profile. 81 tools + 9 skills, 3 auth layers. v1.3.6: upload completeness, sheets:spreadsheet scope, batch_send, multi-profile, send_card_as_user (bot-default).",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"feishu-user-plugin": "src/cli.js"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Child worker for test-uat-race.js. Acquires the UAT refresh lock, holds
|
|
2
|
+
// for a brief window (simulating the refresh + persist), then releases.
|
|
3
|
+
// Writes a single line to stdout: "<id> acquired <ts_ms>; released <ts_ms>"
|
|
4
|
+
|
|
5
|
+
const { LarkOfficialClient } = require('../src/official');
|
|
6
|
+
|
|
7
|
+
const id = process.argv[2] || '?';
|
|
8
|
+
const holdMs = parseInt(process.argv[3] || '250');
|
|
9
|
+
|
|
10
|
+
const client = new LarkOfficialClient('test', 'test');
|
|
11
|
+
const lockPath = client._uatLockPath();
|
|
12
|
+
|
|
13
|
+
(async () => {
|
|
14
|
+
const got = await client._acquireRefreshLock(lockPath, { timeoutMs: 15000 });
|
|
15
|
+
if (!got) {
|
|
16
|
+
console.log(`${id} FAILED_TO_ACQUIRE`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const acquired = Date.now();
|
|
20
|
+
await new Promise(r => setTimeout(r, holdMs));
|
|
21
|
+
const released = Date.now();
|
|
22
|
+
client._releaseRefreshLock(lockPath);
|
|
23
|
+
console.log(`${id} acquired ${acquired}; released ${released}`);
|
|
24
|
+
})().catch(e => { console.log(`${id} ERROR ${e.message}`); process.exit(1); });
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Spawn N child processes that all try to hold the UAT refresh lock
|
|
3
|
+
// concurrently. Verify mutual exclusion: no two hold-windows overlap.
|
|
4
|
+
// Exit 0 on PASS, 1 on FAIL.
|
|
5
|
+
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const N = 4;
|
|
11
|
+
const HOLD_MS = 300;
|
|
12
|
+
|
|
13
|
+
const child = path.join(__dirname, 'test-uat-race-child.js');
|
|
14
|
+
|
|
15
|
+
// Clean up any stale lock from prior runs
|
|
16
|
+
try {
|
|
17
|
+
const os = require('os');
|
|
18
|
+
fs.unlinkSync(path.join(os.homedir(), '.claude', 'feishu-uat-refresh.lock'));
|
|
19
|
+
console.log('(cleaned up stale lock)');
|
|
20
|
+
} catch (_) {}
|
|
21
|
+
|
|
22
|
+
(async () => {
|
|
23
|
+
const workers = Array.from({ length: N }, (_, i) => {
|
|
24
|
+
const p = spawn('node', [child, String(i), String(HOLD_MS)], { stdio: ['ignore', 'pipe', 'inherit'] });
|
|
25
|
+
let out = '';
|
|
26
|
+
p.stdout.on('data', d => out += d);
|
|
27
|
+
return new Promise(resolve => p.on('close', () => resolve(out.trim())));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const lines = await Promise.all(workers);
|
|
31
|
+
console.log('\nraw output:');
|
|
32
|
+
lines.forEach(l => console.log(' ' + l));
|
|
33
|
+
|
|
34
|
+
const events = [];
|
|
35
|
+
for (const l of lines) {
|
|
36
|
+
const m = l.match(/^(\d+) acquired (\d+); released (\d+)$/);
|
|
37
|
+
if (!m) continue;
|
|
38
|
+
events.push({ id: parseInt(m[1]), acquired: parseInt(m[2]), released: parseInt(m[3]) });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (events.length !== N) {
|
|
42
|
+
console.log(`\n❌ expected ${N} successful workers, got ${events.length}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
events.sort((a, b) => a.acquired - b.acquired);
|
|
47
|
+
console.log('\ntimeline (sorted by acquire):');
|
|
48
|
+
events.forEach((e, i) => console.log(` worker ${e.id}: [${e.acquired - events[0].acquired}ms .. ${e.released - events[0].acquired}ms]`));
|
|
49
|
+
|
|
50
|
+
let ok = true;
|
|
51
|
+
for (let i = 1; i < events.length; i++) {
|
|
52
|
+
const prev = events[i - 1];
|
|
53
|
+
const curr = events[i];
|
|
54
|
+
if (curr.acquired < prev.released) {
|
|
55
|
+
ok = false;
|
|
56
|
+
console.log(`\n❌ OVERLAP: worker ${prev.id} held until ${prev.released}, worker ${curr.id} acquired at ${curr.acquired} (overlap ${prev.released - curr.acquired}ms)`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (ok) {
|
|
61
|
+
const totalSpan = events[events.length - 1].released - events[0].acquired;
|
|
62
|
+
const expectedMin = N * HOLD_MS;
|
|
63
|
+
console.log(`\n✅ mutual exclusion PASSED — ${N} workers serialised in ${totalSpan}ms (expected >= ${expectedMin}ms)`);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
} else {
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feishu-user-plugin
|
|
3
|
-
version: "1.3.
|
|
4
|
-
description: "All-in-one Feishu plugin — send messages as yourself, read group/P2P chats, manage docs/tables/wiki (
|
|
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,
|
|
3
|
+
version: "1.3.6"
|
|
4
|
+
description: "All-in-one Feishu plugin — send messages as yourself (incl. batch_send), read group/P2P chats (auto-expands merge_forward), manage docs/tables/wiki/drive (image + file blocks, drive uploads, bitable attachments), OKR, calendar, multi-profile. v1.3.6: upload completeness, sheets:spreadsheet scope, batch_send, multi-profile, send_card_as_user (bot-default)."
|
|
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, batch_send, send_card_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, list_profiles, switch_profile, read_p2p_messages, list_user_chats, list_chats, read_messages, reply_message, forward_message, search_docs, read_doc, create_doc, create_doc_block, update_doc_block, list_bitable_tables, list_bitable_fields, search_bitable_records, batch_create_bitable_records, batch_update_bitable_records, upload_bitable_attachment, list_wiki_spaces, search_wiki, list_wiki_nodes, get_wiki_node, list_files, create_folder, upload_drive_file, find_user, download_image, download_file, 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,11 +6,12 @@ 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 (81 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.
|
|
13
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.
|
|
14
15
|
- `send_as_user` — Send text to any chat by ID, supports reply threading (root_id/parent_id)
|
|
15
16
|
- `send_image_as_user` — Send image (requires image_key from `upload_image`)
|
|
16
17
|
- `send_file_as_user` — Send file (requires file_key from `upload_file`)
|
|
@@ -32,7 +33,7 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
|
|
|
32
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.
|
|
33
34
|
|
|
34
35
|
### Official API Tools (app credentials)
|
|
35
|
-
- `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.
|
|
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`.
|
|
36
37
|
- `send_message_as_bot` — Bot sends message to any chat (text, post, interactive, etc.)
|
|
37
38
|
- `reply_message` / `forward_message` — Message operations (as bot)
|
|
38
39
|
- `delete_message` / `update_message` — Recall or edit bot's own messages
|
|
@@ -52,7 +53,11 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
|
|
|
52
53
|
- `list_files` / `create_folder` — Drive
|
|
53
54
|
- `copy_file` / `move_file` / `delete_file` — Drive file operations (copy, move, delete)
|
|
54
55
|
- `upload_image` / `upload_file` — Upload image/file, returns key for send_image/send_file
|
|
55
|
-
- `
|
|
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.
|
|
56
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.
|
|
57
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.
|
|
58
63
|
- `find_user` — Contact lookup by email/mobile
|
|
@@ -125,10 +130,23 @@ Write — `create_doc_block` now has image shortcuts:
|
|
|
125
130
|
- Create doc with content → `create_doc` → `create_doc_block` (use document_id as parent_block_id for root)
|
|
126
131
|
- Edit existing block → `get_doc_blocks` to find block_id → `update_doc_block`
|
|
127
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`.
|
|
128
136
|
|
|
129
137
|
### Diagnostics
|
|
130
138
|
- Diagnose issues → `get_login_status` first
|
|
131
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:
|
|
146
|
+
```json
|
|
147
|
+
{"alt": {"LARK_COOKIE":"...","LARK_APP_ID":"...","LARK_APP_SECRET":"...","LARK_USER_ACCESS_TOKEN":"...","LARK_USER_REFRESH_TOKEN":"..."}}
|
|
148
|
+
```
|
|
149
|
+
|
|
132
150
|
## Auth & Session
|
|
133
151
|
- **LARK_COOKIE**: Required for user identity tools. Session auto-refreshed every 4h via heartbeat and persisted to config.
|
|
134
152
|
- **LARK_APP_ID + LARK_APP_SECRET**: Required for official API tools.
|
|
@@ -307,7 +325,22 @@ Two known root causes, both fixed in v1.3.3:
|
|
|
307
325
|
### If UAT refresh fails with "invalid_grant"
|
|
308
326
|
- The refresh token has expired or been revoked — auto-refresh cannot recover this
|
|
309
327
|
- **Fix**: Re-run OAuth: `npx feishu-user-plugin oauth`
|
|
310
|
-
- Then restart Claude Code
|
|
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.
|
|
311
344
|
|
|
312
345
|
### If OAuth fails with "Missing LARK_APP_ID"
|
|
313
346
|
- `oauth.js` reads credentials from `~/.claude.json` MCP config (not .env)
|
|
@@ -525,10 +558,10 @@ The v1.3.4 tools require additional scopes on the app + UAT:
|
|
|
525
558
|
|---------|-------------------------------------------|
|
|
526
559
|
| OKR read | `okr:okr:readonly`, `okr:period:read` |
|
|
527
560
|
| Calendar read | `calendar:calendar:readonly`, `calendar:calendar.event:read` |
|
|
528
|
-
| Docx
|
|
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) |
|
|
529
562
|
| Wiki attach (`move_docs_to_wiki`) | `wiki:wiki` (edit scope, the readonly one is insufficient) |
|
|
530
563
|
|
|
531
|
-
If a tool returns `access_denied` or error code `99991672` (scope not granted),
|
|
564
|
+
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`).
|
|
532
565
|
|
|
533
566
|
## Known Limitations
|
|
534
567
|
- CARD message type (type=14) not yet implemented — complex JSON schema
|
package/src/doc-blocks.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// Helpers for constructing docx block payloads.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// create_doc_block / update_doc_block
|
|
5
|
-
// (
|
|
6
|
-
// → wiki-docx sync feature is built; parking the skeleton here now so the
|
|
7
|
-
// later additions don't introduce a new file.
|
|
3
|
+
// v1.3.4 added image block builders. v1.3.6 adds file block builders so the
|
|
4
|
+
// create_doc_block / update_doc_block tools can attach arbitrary file
|
|
5
|
+
// attachments (PDF / zip / etc.) the same way they handle images.
|
|
8
6
|
|
|
9
7
|
// docx v1 block_type enum (relevant subset).
|
|
10
8
|
// Docs: https://open.feishu.cn/document/server-docs/docs/docs/docx-v1/document-block/create
|
|
@@ -63,8 +61,25 @@ function buildReplaceImagePayload(imageToken) {
|
|
|
63
61
|
return { replace_image: { token: imageToken } };
|
|
64
62
|
}
|
|
65
63
|
|
|
64
|
+
// File-block flow mirrors the image-block flow but uses block_type=23 (FILE)
|
|
65
|
+
// and parent_type=docx_file when uploading the binary, and replace_file in
|
|
66
|
+
// the PATCH body. See official.js::createDocBlockWithFile.
|
|
67
|
+
|
|
68
|
+
/** Empty file block placeholder for step 1 of file attachment flow. */
|
|
69
|
+
function buildEmptyFileBlock() {
|
|
70
|
+
return { block_type: BLOCK_TYPE.FILE, file: {} };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Patch body that swaps an empty file block's content with an uploaded file token. */
|
|
74
|
+
function buildReplaceFilePayload(fileToken) {
|
|
75
|
+
if (!fileToken) throw new Error('buildReplaceFilePayload: fileToken is required');
|
|
76
|
+
return { replace_file: { token: fileToken } };
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
module.exports = {
|
|
67
80
|
BLOCK_TYPE,
|
|
68
81
|
buildEmptyImageBlock,
|
|
69
82
|
buildReplaceImagePayload,
|
|
83
|
+
buildEmptyFileBlock,
|
|
84
|
+
buildReplaceFilePayload,
|
|
70
85
|
};
|