feishu-user-plugin 1.3.7 → 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.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +49 -0
- package/README.md +19 -3
- package/package.json +3 -3
- package/scripts/capture-feishu-protobuf.js +86 -0
- package/scripts/check-changelog.js +31 -0
- package/scripts/check-docs-sync.js +41 -0
- package/scripts/check-tool-count.js +32 -7
- package/scripts/decode-feishu-protobuf.js +115 -0
- package/scripts/sync-server-json.js +71 -0
- package/scripts/test-wiki-attach-fallback.js +71 -0
- package/scripts/test-ws-events.js +84 -0
- package/skills/feishu-user-plugin/SKILL.md +3 -3
- package/skills/feishu-user-plugin/references/CLAUDE.md +146 -255
- package/src/auth/cookie.js +30 -0
- package/src/auth/credentials.js +49 -0
- package/src/auth/profile-router.js +248 -0
- package/src/auth/uat.js +231 -0
- package/src/cli.js +3 -0
- package/src/clients/official/base.js +12 -248
- package/src/clients/user.js +10 -24
- package/src/config.js +13 -8
- package/src/events/event-buffer.js +100 -0
- package/src/events/index.js +5 -0
- package/src/events/ws-server.js +86 -0
- package/src/server.js +65 -2
- package/src/setup.js +16 -1
- package/src/tools/_registry.js +1 -0
- package/src/tools/events.js +64 -0
- package/src/tools/messaging-user.js +1 -1
- package/src/tools/profile.js +31 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Manual e2e: spawn MCP server, wait for WS connect, send a message to a test
|
|
4
|
+
// chat (configurable via TEST_CHAT_ID env), then call get_new_events and verify
|
|
5
|
+
// the message appears.
|
|
6
|
+
//
|
|
7
|
+
// Skipped on CI (POSIX 77) when no LARK_APP_ID/SECRET/UAT or no TEST_CHAT_ID.
|
|
8
|
+
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { readCredentials } = require('../src/auth/credentials');
|
|
12
|
+
|
|
13
|
+
const creds = readCredentials();
|
|
14
|
+
const TEST_CHAT_ID = process.env.TEST_CHAT_ID;
|
|
15
|
+
if (!creds.LARK_APP_ID || !creds.LARK_APP_SECRET || !TEST_CHAT_ID) {
|
|
16
|
+
console.error('Skipped: needs LARK_APP_ID/SECRET (real, not mock) and TEST_CHAT_ID env.');
|
|
17
|
+
process.exit(77);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
(async () => {
|
|
21
|
+
console.log('Spawning MCP server with WS...');
|
|
22
|
+
const child = spawn('node', [path.join(__dirname, '..', 'src', 'index.js')], {
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
+
env: process.env,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let buf = ''; const responses = new Map();
|
|
28
|
+
child.stdout.on('data', (d) => {
|
|
29
|
+
buf += d.toString();
|
|
30
|
+
const lines = buf.split('\n'); buf = lines.pop();
|
|
31
|
+
for (const line of lines) try { const m = JSON.parse(line); if (m.id != null) responses.set(m.id, m); } catch {}
|
|
32
|
+
});
|
|
33
|
+
let wsConnected = false;
|
|
34
|
+
child.stderr.on('data', (d) => {
|
|
35
|
+
const s = d.toString();
|
|
36
|
+
process.stderr.write(' child: ' + s);
|
|
37
|
+
if (/WS connected/i.test(s)) wsConnected = true;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const send = (id, method, params) => child.stdin.write(JSON.stringify({jsonrpc:'2.0', id, method, params})+'\n');
|
|
41
|
+
const wait = (id, ms = 10000) => new Promise((res, rej) => {
|
|
42
|
+
const t = setInterval(() => { if (responses.has(id)) { clearInterval(t); res(responses.get(id)); } }, 50);
|
|
43
|
+
setTimeout(() => { clearInterval(t); rej(new Error('timeout id=' + id)); }, ms);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
send(1, 'initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'ws-test', version: '0' }});
|
|
47
|
+
await wait(1);
|
|
48
|
+
|
|
49
|
+
// Wait for WS to connect (up to 15s).
|
|
50
|
+
const wsStart = Date.now();
|
|
51
|
+
while (!wsConnected && Date.now() - wsStart < 15000) {
|
|
52
|
+
await new Promise(r => setTimeout(r, 200));
|
|
53
|
+
}
|
|
54
|
+
if (!wsConnected) {
|
|
55
|
+
console.error('FAIL: WS did not connect within 15s.');
|
|
56
|
+
child.kill();
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
console.log(' WS connected after', Date.now() - wsStart, 'ms');
|
|
60
|
+
|
|
61
|
+
// Send a test message via send_message_as_bot (requires bot to be in TEST_CHAT_ID).
|
|
62
|
+
const stamp = `ws-test-${Date.now()}`;
|
|
63
|
+
send(2, 'tools/call', { name: 'send_message_as_bot', arguments: { chat_id: TEST_CHAT_ID, msg_type: 'text', payload: { text: stamp } } });
|
|
64
|
+
await wait(2, 15000);
|
|
65
|
+
console.log(' sent test message:', stamp);
|
|
66
|
+
|
|
67
|
+
// Wait a few seconds for the WS round-trip.
|
|
68
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
69
|
+
|
|
70
|
+
send(3, 'tools/call', { name: 'get_new_events', arguments: { since_seconds: 30, max_events: 50 } });
|
|
71
|
+
const r3 = await wait(3, 5000);
|
|
72
|
+
const txt = r3.result?.content?.[0]?.text || '';
|
|
73
|
+
const found = txt.includes(stamp);
|
|
74
|
+
console.log(' get_new_events response includes stamp?', found);
|
|
75
|
+
|
|
76
|
+
child.kill();
|
|
77
|
+
|
|
78
|
+
if (!found) {
|
|
79
|
+
console.error('FAIL: WS did not deliver the test message via get_new_events.');
|
|
80
|
+
console.error('Response:', txt.slice(0, 500));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
console.log('PASS: WS delivered the test message.');
|
|
84
|
+
})().catch((e) => { console.error('Error:', e.message); process.exit(1); });
|
|
@@ -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 (incl. batch_send), read group/P2P chats (auto-expands merge_forward), manage docs/tables/wiki (full CRUD)/drive, OKR (with progress writes), calendar (read+write), Tasks v2, multi-profile. v1.3.
|
|
5
|
-
allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_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, 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, search_docs, read_doc, get_doc_blocks, create_doc, manage_doc_block, manage_bitable_app, manage_bitable_table, manage_bitable_field, manage_bitable_view, manage_bitable_record, upload_bitable_attachment, 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, list_files, create_folder, upload_drive_file, manage_drive_file, upload_image, upload_file, download_message_resource, download_doc_image, list_user_okrs, get_okrs, list_okr_periods, create_okr_progress_record, list_okr_progress_records, delete_okr_progress_record, list_calendars, list_calendar_events, get_calendar_event, create_calendar_event, update_calendar_event, delete_calendar_event, respond_calendar_event, get_freebusy, list_tasks, get_task, create_task, update_task, complete_task, delete_task, manage_task_members
|
|
3
|
+
version: "1.3.8"
|
|
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 (full CRUD)/drive, OKR (with progress writes), calendar (read+write), Tasks v2, multi-profile auto-switch, real-time WS events. v1.3.8: multi-profile auto-switch on read errors (B), WebSocket realtime im.message events via get_new_events (C), credential pointer-only mode (E), CI gates (F), auth/uat.js + auth/cookie.js extracts (D)."
|
|
5
|
+
allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_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, manage_profile_hints, read_p2p_messages, list_user_chats, 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, search_docs, read_doc, get_doc_blocks, create_doc, manage_doc_block, manage_bitable_app, manage_bitable_table, manage_bitable_field, manage_bitable_view, manage_bitable_record, upload_bitable_attachment, 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, list_files, create_folder, upload_drive_file, manage_drive_file, upload_image, upload_file, download_message_resource, download_doc_image, list_user_okrs, get_okrs, list_okr_periods, create_okr_progress_record, list_okr_progress_records, delete_okr_progress_record, list_calendars, list_calendar_events, get_calendar_event, create_calendar_event, update_calendar_event, delete_calendar_event, respond_calendar_event, get_freebusy, list_tasks, get_task, create_task, update_task, complete_task, delete_task, manage_task_members, get_new_events
|
|
6
6
|
user_invocable: true
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -24,62 +24,100 @@ The 9 Claude Code skills are also exposed as MCP prompts (`prompts/list` + `prom
|
|
|
24
24
|
|
|
25
25
|
Each prompt accepts a single `arguments` free-form string (mirroring the `$ARGUMENTS` convention used by Claude Code skills). `status` has no arguments.
|
|
26
26
|
|
|
27
|
-
## Tool Categories (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
36
|
-
- `send_post_as_user`
|
|
37
|
-
- `
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- `
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- `
|
|
54
|
-
- `
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- `manage_doc_block(action=create
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- `
|
|
70
|
-
- `
|
|
71
|
-
- `
|
|
72
|
-
- `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- `
|
|
78
|
-
- `
|
|
79
|
-
- `
|
|
80
|
-
- `
|
|
81
|
-
|
|
82
|
-
|
|
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 Identity — Contacts & Info (5 tools)
|
|
40
|
+
`search_contacts` / `create_p2p_chat` / `get_chat_info` / `get_user_info` / `get_login_status`
|
|
41
|
+
|
|
42
|
+
- `get_chat_info` accepts both `oc_xxx` and numeric chat_id (Official API + protobuf fallback).
|
|
43
|
+
|
|
44
|
+
### User OAuth UAT — P2P Chat (2 tools)
|
|
45
|
+
`read_p2p_messages` / `list_user_chats`
|
|
46
|
+
|
|
47
|
+
- `list_user_chats` returns **groups only** (Feishu API limit). For P2P chat list, use `search_contacts` → `create_p2p_chat`.
|
|
48
|
+
- All docx / bitable / drive / wiki / OKR / calendar / tasks create+edit are UAT-first by default — UAT first, bot fallback, with ⚠ warning in response when forced to bot. Resources consistently owned by the caller.
|
|
49
|
+
|
|
50
|
+
### Official API — IM (15 tools)
|
|
51
|
+
`list_chats` / `read_messages` / `send_message_as_bot` / `reply_message` / `forward_message` / `delete_message` / `update_message` / `add_reaction` / `delete_reaction` / `pin_message` / `create_group` / `update_group` / `list_members` / `manage_members` / `download_message_resource`
|
|
52
|
+
|
|
53
|
+
- `read_messages` resolves chat name → bot list → `im.chat.search` → cookie `search_contacts`. Auto-falls back to UAT for external groups. `merge_forward` auto-expands; text messages get `urls[]` + `feishuDocs[]` extracted (disable with `expand_merge_forward=false`).
|
|
54
|
+
- `update_message` only supports `msg_type=text|interactive` (Feishu limit; rejected before API call).
|
|
55
|
+
- `forward_message` auto-detects `receive_id_type` from prefix (`ou_`/`on_`/`email`/...).
|
|
56
|
+
- `manage_members` requires `member_id_type` to match the IDs you pass (`open_id` default; pass `union_id`/`user_id` explicitly to avoid 9499).
|
|
57
|
+
- `download_message_resource(kind=image|file)` MUST pass `save_path` when payload > 2 MiB (Anthropic 5 MB inline cap). For `merge_forward` children use `parentMessageId`, not child id.
|
|
58
|
+
|
|
59
|
+
### Official API — Docs (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.
|
|
83
121
|
|
|
84
122
|
## Usage Patterns
|
|
85
123
|
|
|
@@ -131,58 +169,16 @@ Whole new domain. Identifier is `task_guid` (not numeric task_id like v1). Requi
|
|
|
131
169
|
6. `delete_task(task_guid)`.
|
|
132
170
|
7. `manage_task_members(action=add|remove, task_guid, members=[{id,role:"assignee"|"follower",type?:"user",name?}])`.
|
|
133
171
|
|
|
134
|
-
### External-group message read
|
|
135
|
-
`read_messages`
|
|
136
|
-
|
|
137
|
-
###
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
-
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
- Reply as user in thread → `send_as_user` with root_id
|
|
145
|
-
- Reply as bot → `reply_message` (official API)
|
|
146
|
-
|
|
147
|
-
### Reading
|
|
148
|
-
- Read any group chat history → `read_messages` with chat name or ID (auto-handles external groups via UAT fallback)
|
|
149
|
-
- Read P2P chat history → `search_contacts` → `create_p2p_chat` → `read_p2p_messages`
|
|
150
|
-
- Get chat details → `get_chat_info` (supports both oc_xxx and numeric ID)
|
|
151
|
-
|
|
152
|
-
### Bitable (Multi-dimensional Tables)
|
|
153
|
-
All bitable ops collapse into 5 `manage_bitable_*` tools (v1.3.7) — pick the action.
|
|
154
|
-
- Create from scratch → `manage_bitable_app(action=create)` → `manage_bitable_table(action=create)` → `manage_bitable_field(action=create)`
|
|
155
|
-
- Get info → `manage_bitable_app(action=get_meta)`
|
|
156
|
-
- Duplicate → `manage_bitable_app(action=copy, name=..., folder_id?)`
|
|
157
|
-
- Query → `manage_bitable_table(action=list)` → `manage_bitable_field(action=list)` → `manage_bitable_record(action=search, filter?, sort?, page_size?)`
|
|
158
|
-
- Read single record → `manage_bitable_record(action=get, record_id=...)`
|
|
159
|
-
- Records CRUD → `manage_bitable_record(action=create|update|delete, records|record_ids=[...])` (single or up to 500/call)
|
|
160
|
-
- Fields → `manage_bitable_field(action=create|update|delete, ...)` — `type` required for both create AND update (rename)
|
|
161
|
-
- Views → `manage_bitable_view(action=list|create|delete, view_type=grid|kanban|gallery|form|gantt|calendar)`
|
|
162
|
-
|
|
163
|
-
### Group Management
|
|
164
|
-
- Create a group → `create_group` with name and optional member open_ids
|
|
165
|
-
- Add/remove members → `manage_members` with chat_id + member_ids + action (add/remove)
|
|
166
|
-
- List members → `list_members`
|
|
167
|
-
|
|
168
|
-
### Document Editing
|
|
169
|
-
All block ops go through one tool: `manage_doc_block(action=create|update|delete, ...)`.
|
|
170
|
-
- Create doc with content → `create_doc` → `manage_doc_block(action=create, parent_block_id=document_id, children=[...])`
|
|
171
|
-
- Edit existing block → `get_doc_blocks` to find block_id → `manage_doc_block(action=update, block_id=..., update_body={...})`
|
|
172
|
-
- Delete blocks → `manage_doc_block(action=delete, parent_block_id=..., start_index=..., end_index=...)`
|
|
173
|
-
- Insert image → `manage_doc_block(action=create, parent_block_id=..., image_path=...)` (local file) or `image_token=...` (already uploaded). Three-step flow handled internally.
|
|
174
|
-
- Insert file attachment (PDF/zip/xlsx/...) → `manage_doc_block(action=create, file_path=...)` or `file_token=...`. Feishu auto-wraps the FILE block (block_type=23) inside a VIEW container (block_type=33); the plugin walks into the inner file block automatically before the `replace_file` PATCH so the upload + attach succeed.
|
|
175
|
-
- Replace existing image/file → `manage_doc_block(action=update, block_id=..., image_token=... | file_token=...)`.
|
|
176
|
-
|
|
177
|
-
### Diagnostics
|
|
178
|
-
- Diagnose issues → `get_login_status` first
|
|
179
|
-
|
|
180
|
-
### Profiles (v1.3.6)
|
|
181
|
-
Multi-account / multi-tenant support without restarting the MCP server:
|
|
182
|
-
- `list_profiles` — see all profiles + the active one. Default profile uses top-level env vars; extras come from `LARK_PROFILES_JSON`.
|
|
183
|
-
- `switch_profile(name)` — hot-swap credentials. Cached client instances are invalidated so the next call rebuilds against the new profile.
|
|
184
|
-
|
|
185
|
-
To register more profiles, set `LARK_PROFILES_JSON` in the MCP env:
|
|
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):
|
|
186
182
|
```json
|
|
187
183
|
{"alt": {"LARK_COOKIE":"...","LARK_APP_ID":"...","LARK_APP_SECRET":"...","LARK_USER_ACCESS_TOKEN":"...","LARK_USER_REFRESH_TOKEN":"..."}}
|
|
188
184
|
```
|
|
@@ -254,158 +250,61 @@ crontab -e
|
|
|
254
250
|
|
|
255
251
|
## Automated Cookie Setup via Playwright
|
|
256
252
|
|
|
257
|
-
|
|
258
|
-
Playwright MCP must be available. If not installed:
|
|
259
|
-
> Run: `npx @anthropic-ai/claude-code mcp add playwright -- npx @anthropic-ai/mcp-server-playwright` then restart Claude Code.
|
|
260
|
-
|
|
261
|
-
### Automated Flow — FOLLOW EXACTLY, DO NOT IMPROVISE
|
|
262
|
-
|
|
263
|
-
**Step 1: Clear existing browser session (MANDATORY)**
|
|
264
|
-
|
|
265
|
-
Playwright MCP uses Edge's persistent profile. It may have a cached login from a DIFFERENT Feishu account. You MUST clear cookies first:
|
|
266
|
-
|
|
267
|
-
```
|
|
268
|
-
browser_run_code:
|
|
269
|
-
await context.clearCookies();
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
Then navigate:
|
|
273
|
-
```
|
|
274
|
-
browser_navigate: https://www.feishu.cn/messenger/
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
**Step 2: Wait for user to scan QR code**
|
|
278
|
-
|
|
279
|
-
Take a screenshot to show the QR code:
|
|
280
|
-
```
|
|
281
|
-
browser_take_screenshot
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
Tell the user: "Please scan the QR code with Feishu mobile app to log in. Make sure you use the correct account."
|
|
253
|
+
Prerequisite: Playwright MCP installed (`npx @anthropic-ai/claude-code mcp add playwright -- npx @anthropic-ai/mcp-server-playwright` then restart).
|
|
285
254
|
|
|
286
|
-
|
|
255
|
+
Procedure (three gotchas embedded — skip any and you'll fail):
|
|
287
256
|
|
|
288
|
-
**
|
|
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.
|
|
289
264
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
Step 3a — Store cookie in page context via `browser_run_code`:
|
|
293
|
-
```js
|
|
294
|
-
const cookies = await page.context().cookies('https://www.feishu.cn');
|
|
295
|
-
const str = cookies.map(c => c.name + '=' + c.value).join('; ');
|
|
296
|
-
await page.evaluate(s => { window.__COOKIE__ = s; }, str);
|
|
297
|
-
return 'Stored ' + cookies.length + ' cookies, length=' + str.length;
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
Step 3b — Read the clean cookie string via `browser_evaluate`:
|
|
301
|
-
```js
|
|
302
|
-
window.__COOKIE__
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
This two-step approach ensures the cookie string is clean, with no markdown prefix or page content mixed in.
|
|
306
|
-
|
|
307
|
-
**Step 4: Validate BEFORE writing (MANDATORY)**
|
|
308
|
-
|
|
309
|
-
Check the cookie string:
|
|
310
|
-
1. Must be pure ASCII — no Chinese characters, no markdown (`###`), no HTML
|
|
311
|
-
2. Must contain `session=` and `sl_session=`
|
|
312
|
-
3. Length should be 500-5000 characters. If >10000, it is contaminated — DO NOT write it.
|
|
313
|
-
4. Must NOT start with `###` or contain `\n` followed by non-cookie content
|
|
265
|
+
## Troubleshooting Guide
|
|
314
266
|
|
|
315
|
-
|
|
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.
|
|
316
269
|
|
|
317
|
-
|
|
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.
|
|
318
273
|
|
|
319
|
-
|
|
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.
|
|
320
277
|
|
|
321
|
-
|
|
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.
|
|
322
280
|
|
|
323
|
-
|
|
324
|
-
npx feishu-user-plugin oauth
|
|
325
|
-
```
|
|
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.
|
|
326
283
|
|
|
327
|
-
|
|
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
|
|
328
288
|
|
|
329
|
-
|
|
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.
|
|
330
291
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
```
|
|
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.
|
|
334
294
|
|
|
335
|
-
|
|
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.
|
|
336
297
|
|
|
337
|
-
|
|
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`.
|
|
338
300
|
|
|
339
|
-
###
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
2. **unbounded fetch hangs** (fixed in v1.3.3):
|
|
347
|
-
- All raw `fetch` calls to `feishu.cn` / `internal-api-lark-api.feishu.cn` used to have no timeout. A stalled connection (ECONNRESET, slow DNS, upstream hang) would block a tool handler indefinitely; the MCP client times out the request, which some clients handle by tearing down the stdio transport — observed as "mid-session disconnect".
|
|
348
|
-
- Fix: `utils.js::fetchWithTimeout` with `AbortController`, 30s default. All `client.js` + `official.js` fetches go through it.
|
|
349
|
-
- If still happening: check for any `console.log` calls in server code (only `console.error` is safe), and grep for raw `await fetch(` — every one must go through `fetchWithTimeout`.
|
|
350
|
-
|
|
351
|
-
### If Official API tools return 401 / "token invalid" every time
|
|
352
|
-
- **Likely cause**: `LARK_APP_ID` is wrong or stale. Observed in production: Claude Code auto-installed the plugin and guessed/copied a wrong APP_ID that doesn't match the team's real app (e.g. from an unrelated app, from someone else's machine, or hallucinated).
|
|
353
|
-
- **Diagnosis**: `get_login_status` now reports `App credentials: INVALID — app_id=<x> rejected by Feishu (<code>: <msg>)`. MCP startup logs `[feishu-user-plugin] ERROR: LARK_APP_ID=<x> was REJECTED by Feishu` on stderr when this happens.
|
|
354
|
-
- **Fix**: Re-run the canonical install prompt from `team-skills/plugins/feishu-user-plugin/README.md` which contains the correct APP_ID/SECRET, and restart Claude Code.
|
|
355
|
-
|
|
356
|
-
### If MCP tools are not available
|
|
357
|
-
1. Check `~/.claude.json` — config must be in **top-level** `mcpServers`, not inside `projects[*]`
|
|
358
|
-
2. For Codex: check `~/.codex/config.toml` has `[mcp_servers.feishu-user-plugin]` section
|
|
359
|
-
3. Restart Claude Code / Codex after config changes
|
|
360
|
-
4. After restart, tools may take a few seconds to register — if first call fails with "No such tool", wait and retry once
|
|
361
|
-
|
|
362
|
-
### If cookie authentication fails
|
|
363
|
-
- `document.cookie` in browser console CANNOT access HttpOnly cookies (`session`, `sl_session`)
|
|
364
|
-
- **Correct method**: Network tab → first request → Request Headers → Cookie → Copy value
|
|
365
|
-
- **Best method**: Playwright two-step extraction (see above)
|
|
366
|
-
|
|
367
|
-
### If Playwright logs into the wrong Feishu account
|
|
368
|
-
- Playwright uses Edge's persistent profile with cached sessions
|
|
369
|
-
- **ALWAYS clear cookies first** with `context.clearCookies()` before navigating to feishu.cn
|
|
370
|
-
|
|
371
|
-
### If read_messages returns an error
|
|
372
|
-
- Error messages include the actual Feishu error code and description
|
|
373
|
-
- `read_messages` auto-falls back to UAT when bot API fails (e.g. external groups)
|
|
374
|
-
- Chat name resolution: bot's group list → `im.chat.search` → `search_contacts` (cookie)
|
|
375
|
-
- If all three strategies fail, provide the oc_xxx or numeric chat ID directly
|
|
376
|
-
|
|
377
|
-
### If UAT refresh fails with "invalid_grant"
|
|
378
|
-
- The refresh token has expired or been revoked — auto-refresh cannot recover this
|
|
379
|
-
- **Fix**: Re-run OAuth: `npx feishu-user-plugin oauth`
|
|
380
|
-
- Then restart Claude Code / Codex so running MCP server processes load the new token
|
|
381
|
-
- **v1.3.5+ hardening** (no manual action required, fixes the common case):
|
|
382
|
-
- Cross-process file lock at `~/.claude/feishu-uat-refresh.lock` (`O_CREAT|O_EXCL`, 30 s stale detection) — at most one MCP process refreshes at a time.
|
|
383
|
-
- Inside the critical section the lock holder re-reads `~/.claude.json` to see if a peer already rotated the token; if so, it adopts the fresh token instead of consuming an already-invalidated refresh token.
|
|
384
|
-
- This closes the "Codex spawned 6 MCP servers, all shared the same refresh_token, all raced to refresh" failure mode observed on 2026-04-23.
|
|
385
|
-
- `get_login_status` now does a real UAT health check (calls `listChatsAsUser({pageSize:1})`) — no more "token configured but actually 401" surprises.
|
|
386
|
-
|
|
387
|
-
### If multiple MCP server processes keep spawning
|
|
388
|
-
- Observed on Codex + Claude Code when the client respawns the server for each tool session without cleaning up the previous one. 6 concurrent `src/index.js` processes is not unusual under heavy use.
|
|
389
|
-
- v1.3.5 neutralises the damage (UAT refresh serialised via file lock) but the stale processes still consume memory.
|
|
390
|
-
- **Manual cleanup when you notice**: `pkill -f 'feishu-user-plugin/src/index.js'` — the client will respawn one fresh process on the next tool call.
|
|
391
|
-
|
|
392
|
-
### If a create_* tool warns "UAT failed, created as BOT"
|
|
393
|
-
- v1.3.5 added an explicit `⚠️` warning to MCP responses whenever `_asUserOrApp` silently fell back to bot identity for a write (create_doc / manage_bitable_app(action=create) / create_folder / manage_doc_block(action=create) / ...). Before v1.3.5 this was silent and led to the "teammate can read my 'private' doc" issue.
|
|
394
|
-
- **Cause**: your UAT is failing (expired / scope missing / race) so the plugin reached for bot credentials. The resulting resource is owned by the shared bot, tenant-readable by default, NOT by you.
|
|
395
|
-
- **Fix**: run `npx feishu-user-plugin oauth` and restart Claude Code / Codex. If the resource needs to be yours, delete the bot-owned copy and recreate after UAT is valid.
|
|
396
|
-
|
|
397
|
-
### If OAuth fails with "Missing LARK_APP_ID"
|
|
398
|
-
- `oauth.js` reads credentials from `~/.claude.json` MCP config (not .env)
|
|
399
|
-
- Run `npx feishu-user-plugin setup` first, then re-run OAuth
|
|
400
|
-
|
|
401
|
-
### If two MCP servers are running (duplicate tools)
|
|
402
|
-
- This happens when both `~/.claude.json` mcpServers AND a team-skills plugin have feishu-user-plugin
|
|
403
|
-
- team-skills plugin should NOT have `.mcp.json` — it only provides skills and CLAUDE.md
|
|
404
|
-
- Delete `.mcp.json` from the team-skills plugin directory if it exists
|
|
405
|
-
|
|
406
|
-
### If list_user_chats doesn't return P2P chats
|
|
407
|
-
- This is expected — the API only returns group chats
|
|
408
|
-
- **Correct P2P flow**: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`
|
|
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.
|
|
409
308
|
|
|
410
309
|
## Architecture
|
|
411
310
|
|
|
@@ -484,10 +383,7 @@ When making ANY code change (new tools, bug fixes, features), update these in th
|
|
|
484
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.
|
|
485
384
|
|
|
486
385
|
### Keeping ROADMAP.md up to date
|
|
487
|
-
- When
|
|
488
|
-
- When discovering new bugs, limitations, or feature ideas during development, add them to the appropriate section in ROADMAP.md
|
|
489
|
-
- When a version is released (tag pushed), move completed items under the "已完成" section with the version number
|
|
490
|
-
- When researching a direction and deciding not to implement, add it to "已调研但暂不实施" with the reasoning
|
|
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.
|
|
491
387
|
|
|
492
388
|
### When adding new tools (post-v1.3.7 layout)
|
|
493
389
|
1. Add the underlying API method to the right domain file:
|
|
@@ -579,11 +475,6 @@ feishu-user-plugin vX.Y.Z 发布
|
|
|
579
475
|
|
|
580
476
|
**发送前**:始终先用 `send_to_user` 或类似工具发给用户自己审核,或直接以文本形式贴在对话里等用户批准。用户说"发"才调 `send_post_as_user` 到目标群。
|
|
581
477
|
|
|
582
|
-
### Testing a tool
|
|
583
|
-
- For Official API tools: can test directly via MCP tool call or standalone script using `readCredentials()` from `src/config.js`
|
|
584
|
-
- For Cookie tools: need active session, test via MCP tool call
|
|
585
|
-
- Always verify `_safeSDKCall` handles the response format (multipart uploads return data at top level, not nested under `.data`)
|
|
586
|
-
|
|
587
478
|
## OAuth Scopes (when re-running `npx feishu-user-plugin oauth`)
|
|
588
479
|
|
|
589
480
|
The v1.3.4 tools require additional scopes on the app + UAT:
|