feishu-user-plugin 1.1.0 → 1.1.2
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 +1 -1
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/skills/feishu-user-plugin/SKILL.md +1 -1
- package/skills/feishu-user-plugin/references/CLAUDE.md +2 -2
- package/src/cli.js +0 -0
- package/src/index.js +85 -12
- package/src/oauth.js +25 -0
- package/src/official.js +21 -6
- package/src/test-comprehensive.js +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 33 tools + 9 skills, 3 auth layers.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "EthanQC"
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ 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.1.2] - 2026-03-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Double OAuth on first install**: `oauth.js` now writes tokens to both `.env` and `~/.claude.json` MCP config directly, so MCP restart picks them up immediately without needing a second OAuth run.
|
|
11
|
+
- **readMessagesAsUser fails with start_time but no end_time**: Auto-sets `end_time` to current timestamp when `start_time` is provided but `end_time` is not, preventing "end_time earlier than start_time" error.
|
|
12
|
+
- **read_p2p_messages rejects chat names**: Now resolves user/group names automatically via search_contacts.
|
|
13
|
+
- **External group messages show sender IDs instead of names**: `_populateSenderNames` now falls back to cookie-based user identity lookup for external tenant users.
|
|
14
|
+
|
|
15
|
+
## [1.1.1] - 2026-03-11
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **read_messages can't read external groups**: `read_messages` now auto-falls back to UAT when bot API fails (e.g. bot not in group, external groups). No need to manually switch to `read_p2p_messages`.
|
|
19
|
+
- **Chat name resolution for external groups**: Added Strategy 3 using `search_contacts` (cookie-based) to find groups not visible to bot or `im.chat.search`.
|
|
20
|
+
- **Numeric chat IDs not accepted by read_messages**: `resolveToOcId` now passes through numeric IDs directly.
|
|
21
|
+
|
|
7
22
|
## [1.1.0] - 2026-03-11
|
|
8
23
|
|
|
9
24
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 33 tools + 9 skills, 3 auth layers.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feishu-user-plugin
|
|
3
|
-
version: "1.1.
|
|
3
|
+
version: "1.1.2"
|
|
4
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
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
|
|
6
6
|
user_invocable: true
|
|
@@ -30,7 +30,7 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
|
|
|
30
30
|
- `list_user_chats` — List group chats the user is in. Note: API only returns groups, not P2P. For P2P, use: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`.
|
|
31
31
|
|
|
32
32
|
### Official API Tools (app credentials)
|
|
33
|
-
- `list_chats` / `read_messages` — Chat history (
|
|
33
|
+
- `list_chats` / `read_messages` — Chat history (accepts chat name, oc_ ID, or numeric ID; auto-falls back to UAT for external groups)
|
|
34
34
|
- `reply_message` / `forward_message` — Message operations (as bot). reply_message only works for text messages.
|
|
35
35
|
- `search_docs` / `read_doc` / `create_doc` — Document operations
|
|
36
36
|
- `list_bitable_tables` / `list_bitable_fields` / `search_bitable_records` — Table queries
|
|
@@ -42,7 +42,7 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
|
|
|
42
42
|
## Usage Patterns
|
|
43
43
|
- Send text as yourself → `send_to_user` or `send_to_group`
|
|
44
44
|
- Send rich content → `send_post_as_user` (formatted text), `send_image_as_user` (images)
|
|
45
|
-
- Read group chat history → `read_messages` with chat name or
|
|
45
|
+
- Read any group chat history → `read_messages` with chat name or ID (auto-handles external groups via UAT fallback)
|
|
46
46
|
- Read P2P chat history → `search_contacts` → `create_p2p_chat` → `read_p2p_messages`
|
|
47
47
|
- Reply as user in thread → `send_as_user` with root_id
|
|
48
48
|
- Reply as bot → `reply_message` (official API, text messages only)
|
package/src/cli.js
CHANGED
|
File without changes
|
package/src/index.js
CHANGED
|
@@ -48,6 +48,8 @@ class ChatIdMapper {
|
|
|
48
48
|
|
|
49
49
|
async resolveToOcId(chatIdOrName, official) {
|
|
50
50
|
if (chatIdOrName.startsWith('oc_')) return chatIdOrName;
|
|
51
|
+
// Also accept raw numeric IDs (from search_contacts)
|
|
52
|
+
if (/^\d+$/.test(chatIdOrName)) return chatIdOrName;
|
|
51
53
|
// Strategy 1: Search in bot's group list cache
|
|
52
54
|
const cached = await this.findByName(chatIdOrName, official);
|
|
53
55
|
if (cached) return cached;
|
|
@@ -67,6 +69,27 @@ class ChatIdMapper {
|
|
|
67
69
|
}
|
|
68
70
|
return null;
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
// Strategy 3: Use search_contacts (cookie-based) to find external groups by name
|
|
74
|
+
// Returns numeric chat_id that works with UAT readMessagesAsUser
|
|
75
|
+
async resolveViaContacts(chatName, userClient) {
|
|
76
|
+
if (!userClient) return null;
|
|
77
|
+
try {
|
|
78
|
+
const results = await userClient.search(chatName);
|
|
79
|
+
const groups = results.filter(r => r.type === 'group');
|
|
80
|
+
// Exact match first
|
|
81
|
+
for (const g of groups) {
|
|
82
|
+
if (g.title === chatName) return String(g.id);
|
|
83
|
+
}
|
|
84
|
+
// Partial match
|
|
85
|
+
for (const g of groups) {
|
|
86
|
+
if (g.title && g.title.includes(chatName)) return String(g.id);
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error('[feishu-user-plugin] search_contacts fallback failed:', e.message);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
// --- Client Singletons ---
|
|
@@ -306,11 +329,11 @@ const TOOLS = [
|
|
|
306
329
|
},
|
|
307
330
|
{
|
|
308
331
|
name: 'read_messages',
|
|
309
|
-
description: '[Official API] Read message history. Accepts oc_xxx ID or chat name (auto-searched
|
|
332
|
+
description: '[Official API + UAT fallback] Read message history from any group. Accepts oc_xxx ID, numeric ID, or chat name (auto-searched). Auto-falls back to UAT for external groups the bot cannot access. Returns newest messages first by default, with sender names resolved.',
|
|
310
333
|
inputSchema: {
|
|
311
334
|
type: 'object',
|
|
312
335
|
properties: {
|
|
313
|
-
chat_id: { type: 'string', description: 'Chat ID (oc_xxx) or chat name (auto-searched)' },
|
|
336
|
+
chat_id: { type: 'string', description: 'Chat ID (oc_xxx), numeric ID, or chat name (auto-searched via bot groups, im.chat.search, and user contacts)' },
|
|
314
337
|
page_size: { type: 'number', description: 'Messages to fetch (default 20, max 50)' },
|
|
315
338
|
start_time: { type: 'string', description: 'Start timestamp in seconds (optional)' },
|
|
316
339
|
end_time: { type: 'string', description: 'End timestamp in seconds (optional)' },
|
|
@@ -508,7 +531,7 @@ const TOOLS = [
|
|
|
508
531
|
// --- Server ---
|
|
509
532
|
|
|
510
533
|
const server = new Server(
|
|
511
|
-
{ name: 'feishu-user-plugin', version: '1.1.
|
|
534
|
+
{ name: 'feishu-user-plugin', version: '1.1.2' },
|
|
512
535
|
{ capabilities: { tools: {} } }
|
|
513
536
|
);
|
|
514
537
|
|
|
@@ -639,10 +662,34 @@ async function handleTool(name, args) {
|
|
|
639
662
|
|
|
640
663
|
case 'read_p2p_messages': {
|
|
641
664
|
const official = getOfficialClient();
|
|
642
|
-
|
|
665
|
+
let chatId = args.chat_id;
|
|
666
|
+
// If chat_id is not numeric or oc_, try to resolve as user name → P2P chat
|
|
667
|
+
if (!/^\d+$/.test(chatId) && !chatId.startsWith('oc_')) {
|
|
668
|
+
let uc = null;
|
|
669
|
+
try { uc = await getUserClient(); } catch (_) {}
|
|
670
|
+
if (uc) {
|
|
671
|
+
const results = await uc.search(chatId);
|
|
672
|
+
const user = results.find(r => r.type === 'user');
|
|
673
|
+
if (user) {
|
|
674
|
+
const pChatId = await uc.createChat(String(user.id));
|
|
675
|
+
if (pChatId) chatId = String(pChatId);
|
|
676
|
+
else return text(`Found user "${user.title}" but failed to create P2P chat.`);
|
|
677
|
+
} else {
|
|
678
|
+
// Maybe it's a group name
|
|
679
|
+
const group = results.find(r => r.type === 'group');
|
|
680
|
+
if (group) chatId = String(group.id);
|
|
681
|
+
else return text(`Cannot resolve "${args.chat_id}" to a chat. Use search_contacts to find the ID first.`);
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
return text(`"${args.chat_id}" is not a valid chat ID. Provide a numeric ID or oc_xxx format. Use search_contacts + create_p2p_chat to get the ID.`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
let uc = null;
|
|
688
|
+
try { uc = await getUserClient(); } catch (_) {}
|
|
689
|
+
return json(await official.readMessagesAsUser(chatId, {
|
|
643
690
|
pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time,
|
|
644
691
|
sortType: args.sort_type,
|
|
645
|
-
}));
|
|
692
|
+
}, uc));
|
|
646
693
|
}
|
|
647
694
|
case 'list_user_chats':
|
|
648
695
|
return json(await getOfficialClient().listChatsAsUser({ pageSize: args.page_size, pageToken: args.page_token }));
|
|
@@ -653,14 +700,40 @@ async function handleTool(name, args) {
|
|
|
653
700
|
return json(await getOfficialClient().listChats({ pageSize: args.page_size, pageToken: args.page_token }));
|
|
654
701
|
case 'read_messages': {
|
|
655
702
|
const official = getOfficialClient();
|
|
703
|
+
const msgOpts = { pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time, sortType: args.sort_type };
|
|
704
|
+
// Get userClient for name resolution fallback (best-effort)
|
|
705
|
+
let uc = null;
|
|
706
|
+
try { uc = await getUserClient(); } catch (_) {}
|
|
656
707
|
const resolvedChatId = await chatIdMapper.resolveToOcId(args.chat_id, official);
|
|
657
|
-
|
|
658
|
-
|
|
708
|
+
|
|
709
|
+
// Try bot API first if we resolved an oc_ ID
|
|
710
|
+
if (resolvedChatId) {
|
|
711
|
+
try {
|
|
712
|
+
return json(await official.readMessages(resolvedChatId, msgOpts, uc));
|
|
713
|
+
} catch (botErr) {
|
|
714
|
+
// Bot API failed (e.g. bot not in group, no permission) — fall through to UAT
|
|
715
|
+
console.error(`[feishu-user-plugin] read_messages bot API failed for ${resolvedChatId}: ${botErr.message}`);
|
|
716
|
+
if (official.hasUAT) {
|
|
717
|
+
try {
|
|
718
|
+
return json(await official.readMessagesAsUser(resolvedChatId, msgOpts, uc));
|
|
719
|
+
} catch (uatErr) {
|
|
720
|
+
console.error(`[feishu-user-plugin] read_messages UAT fallback also failed for ${resolvedChatId}: ${uatErr.message}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
throw botErr; // Re-throw original error if UAT also failed
|
|
724
|
+
}
|
|
659
725
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
726
|
+
|
|
727
|
+
// Bot couldn't resolve the chat name — try search_contacts + UAT for external groups
|
|
728
|
+
if (official.hasUAT) {
|
|
729
|
+
if (!uc) try { uc = await getUserClient(); } catch (_) {}
|
|
730
|
+
const contactChatId = await chatIdMapper.resolveViaContacts(args.chat_id, uc);
|
|
731
|
+
if (contactChatId) {
|
|
732
|
+
return json(await official.readMessagesAsUser(contactChatId, msgOpts, uc));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return text(`Cannot resolve "${args.chat_id}" to a chat ID.\nSearched: bot's group list, im.chat.search API, and user contacts (search_contacts).\nTry: provide the oc_xxx or numeric chat ID directly.`);
|
|
664
737
|
}
|
|
665
738
|
case 'reply_message':
|
|
666
739
|
return text(`Reply sent: ${(await getOfficialClient().replyMessage(args.message_id, args.text)).messageId}`);
|
|
@@ -725,7 +798,7 @@ async function main() {
|
|
|
725
798
|
const hasCookie = !!process.env.LARK_COOKIE;
|
|
726
799
|
const hasApp = !!(process.env.LARK_APP_ID && process.env.LARK_APP_SECRET);
|
|
727
800
|
const hasUAT = !!process.env.LARK_USER_ACCESS_TOKEN;
|
|
728
|
-
console.error(`[feishu-user-plugin] MCP Server v1.1.
|
|
801
|
+
console.error(`[feishu-user-plugin] MCP Server v1.1.2 — ${TOOLS.length} tools`);
|
|
729
802
|
console.error(`[feishu-user-plugin] Auth: Cookie=${hasCookie ? 'YES' : 'NO'} App=${hasApp ? 'YES' : 'NO'} UAT=${hasUAT ? 'YES' : 'NO'}`);
|
|
730
803
|
if (!hasCookie) console.error('[feishu-user-plugin] WARNING: LARK_COOKIE not set — user identity tools (send_to_user, etc.) will fail');
|
|
731
804
|
if (!hasApp) console.error('[feishu-user-plugin] WARNING: LARK_APP_ID/SECRET not set — official API tools (read_messages, docs, etc.) will fail');
|
package/src/oauth.js
CHANGED
|
@@ -121,6 +121,31 @@ function saveToken(tokenData) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
fs.writeFileSync(envPath, envContent.trim() + '\n');
|
|
124
|
+
|
|
125
|
+
// Also persist to ~/.claude.json MCP config so MCP restart picks up tokens immediately
|
|
126
|
+
_persistToClaudeJson(updates);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _persistToClaudeJson(updates) {
|
|
130
|
+
const claudeJsonPaths = [
|
|
131
|
+
path.join(process.env.HOME || '', '.claude.json'),
|
|
132
|
+
path.join(process.env.HOME || '', '.claude', '.claude.json'),
|
|
133
|
+
];
|
|
134
|
+
for (const cjPath of claudeJsonPaths) {
|
|
135
|
+
try {
|
|
136
|
+
const raw = fs.readFileSync(cjPath, 'utf8');
|
|
137
|
+
const config = JSON.parse(raw);
|
|
138
|
+
const servers = config.mcpServers || {};
|
|
139
|
+
for (const name of ['feishu-user-plugin', 'feishu']) {
|
|
140
|
+
if (servers[name]?.env) {
|
|
141
|
+
Object.assign(servers[name].env, updates);
|
|
142
|
+
fs.writeFileSync(cjPath, JSON.stringify(config, null, 2) + '\n');
|
|
143
|
+
console.log(`[feishu-user-plugin] OAuth tokens persisted to ${cjPath}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
124
149
|
}
|
|
125
150
|
|
|
126
151
|
const server = http.createServer(async (req, res) => {
|
package/src/official.js
CHANGED
|
@@ -156,7 +156,11 @@ class LarkOfficialClient {
|
|
|
156
156
|
return { items: data.data.items || [], pageToken: data.data.page_token, hasMore: data.data.has_more };
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
async readMessagesAsUser(chatId, { pageSize = 20, startTime, endTime, pageToken, sortType = 'ByCreateTimeDesc' } = {}) {
|
|
159
|
+
async readMessagesAsUser(chatId, { pageSize = 20, startTime, endTime, pageToken, sortType = 'ByCreateTimeDesc' } = {}, userClient) {
|
|
160
|
+
// Feishu API requires end_time >= start_time; auto-set end_time to now if missing
|
|
161
|
+
if (startTime && !endTime) {
|
|
162
|
+
endTime = String(Math.floor(Date.now() / 1000));
|
|
163
|
+
}
|
|
160
164
|
const params = new URLSearchParams({
|
|
161
165
|
container_id_type: 'chat', container_id: chatId, page_size: String(pageSize),
|
|
162
166
|
sort_type: sortType,
|
|
@@ -172,7 +176,7 @@ class LarkOfficialClient {
|
|
|
172
176
|
});
|
|
173
177
|
if (data.code !== 0) throw new Error(`readMessagesAsUser failed (${data.code}): ${data.msg}`);
|
|
174
178
|
const items = (data.data.items || []).map(m => this._formatMessage(m));
|
|
175
|
-
await this._populateSenderNames(items);
|
|
179
|
+
await this._populateSenderNames(items, userClient);
|
|
176
180
|
return { items, hasMore: data.data.has_more, pageToken: data.data.page_token };
|
|
177
181
|
}
|
|
178
182
|
|
|
@@ -186,14 +190,14 @@ class LarkOfficialClient {
|
|
|
186
190
|
return { items: res.data.items || [], pageToken: res.data.page_token, hasMore: res.data.has_more };
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
async readMessages(chatId, { pageSize = 20, startTime, endTime, pageToken, sortType = 'ByCreateTimeDesc' } = {}) {
|
|
193
|
+
async readMessages(chatId, { pageSize = 20, startTime, endTime, pageToken, sortType = 'ByCreateTimeDesc' } = {}, userClient) {
|
|
190
194
|
const params = { container_id_type: 'chat', container_id: chatId, page_size: pageSize, sort_type: sortType };
|
|
191
195
|
if (startTime) params.start_time = startTime;
|
|
192
196
|
if (endTime) params.end_time = endTime;
|
|
193
197
|
if (pageToken) params.page_token = pageToken;
|
|
194
198
|
const res = await this._safeSDKCall(() => this.client.im.message.list({ params }), 'readMessages');
|
|
195
199
|
const items = (res.data.items || []).map(m => this._formatMessage(m));
|
|
196
|
-
await this._populateSenderNames(items);
|
|
200
|
+
await this._populateSenderNames(items, userClient);
|
|
197
201
|
return { items, hasMore: res.data.has_more, pageToken: res.data.page_token };
|
|
198
202
|
}
|
|
199
203
|
|
|
@@ -429,7 +433,7 @@ class LarkOfficialClient {
|
|
|
429
433
|
return null;
|
|
430
434
|
}
|
|
431
435
|
|
|
432
|
-
async _populateSenderNames(items) {
|
|
436
|
+
async _populateSenderNames(items, userClient) {
|
|
433
437
|
// Collect unique sender IDs that aren't cached
|
|
434
438
|
const unknownIds = new Set();
|
|
435
439
|
for (const item of items) {
|
|
@@ -437,10 +441,21 @@ class LarkOfficialClient {
|
|
|
437
441
|
unknownIds.add(item.senderId);
|
|
438
442
|
}
|
|
439
443
|
}
|
|
440
|
-
// Batch resolve
|
|
444
|
+
// Batch resolve via official contact API
|
|
441
445
|
for (const id of unknownIds) {
|
|
442
446
|
await this.getUserById(id);
|
|
443
447
|
}
|
|
448
|
+
// Fallback: resolve remaining unknowns via cookie-based user identity client
|
|
449
|
+
if (userClient) {
|
|
450
|
+
for (const id of unknownIds) {
|
|
451
|
+
if (!this._userNameCache.has(id) || !this._userNameCache.get(id)) {
|
|
452
|
+
try {
|
|
453
|
+
const name = await userClient.getUserName(id);
|
|
454
|
+
if (name) this._userNameCache.set(id, name);
|
|
455
|
+
} catch {}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
444
459
|
// Populate senderName field
|
|
445
460
|
for (const item of items) {
|
|
446
461
|
if (item.senderId) {
|
|
@@ -274,7 +274,7 @@ async function testUAT() {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
async function main() {
|
|
277
|
-
console.log('=== feishu-user-plugin v1.1.
|
|
277
|
+
console.log('=== feishu-user-plugin v1.1.2 — Comprehensive Test ===\n');
|
|
278
278
|
|
|
279
279
|
await testUserIdentity();
|
|
280
280
|
console.log('');
|