feishu-user-plugin 1.1.1 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.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,14 @@ 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
+
7
15
  ## [1.1.1] - 2026-03-11
8
16
 
9
17
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.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.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
package/src/index.js CHANGED
@@ -531,7 +531,7 @@ const TOOLS = [
531
531
  // --- Server ---
532
532
 
533
533
  const server = new Server(
534
- { name: 'feishu-user-plugin', version: '1.1.1' },
534
+ { name: 'feishu-user-plugin', version: '1.1.2' },
535
535
  { capabilities: { tools: {} } }
536
536
  );
537
537
 
@@ -662,10 +662,34 @@ async function handleTool(name, args) {
662
662
 
663
663
  case 'read_p2p_messages': {
664
664
  const official = getOfficialClient();
665
- return json(await official.readMessagesAsUser(args.chat_id, {
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, {
666
690
  pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time,
667
691
  sortType: args.sort_type,
668
- }));
692
+ }, uc));
669
693
  }
670
694
  case 'list_user_chats':
671
695
  return json(await getOfficialClient().listChatsAsUser({ pageSize: args.page_size, pageToken: args.page_token }));
@@ -677,18 +701,21 @@ async function handleTool(name, args) {
677
701
  case 'read_messages': {
678
702
  const official = getOfficialClient();
679
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 (_) {}
680
707
  const resolvedChatId = await chatIdMapper.resolveToOcId(args.chat_id, official);
681
708
 
682
709
  // Try bot API first if we resolved an oc_ ID
683
710
  if (resolvedChatId) {
684
711
  try {
685
- return json(await official.readMessages(resolvedChatId, msgOpts));
712
+ return json(await official.readMessages(resolvedChatId, msgOpts, uc));
686
713
  } catch (botErr) {
687
714
  // Bot API failed (e.g. bot not in group, no permission) — fall through to UAT
688
715
  console.error(`[feishu-user-plugin] read_messages bot API failed for ${resolvedChatId}: ${botErr.message}`);
689
716
  if (official.hasUAT) {
690
717
  try {
691
- return json(await official.readMessagesAsUser(resolvedChatId, msgOpts));
718
+ return json(await official.readMessagesAsUser(resolvedChatId, msgOpts, uc));
692
719
  } catch (uatErr) {
693
720
  console.error(`[feishu-user-plugin] read_messages UAT fallback also failed for ${resolvedChatId}: ${uatErr.message}`);
694
721
  }
@@ -699,11 +726,10 @@ async function handleTool(name, args) {
699
726
 
700
727
  // Bot couldn't resolve the chat name — try search_contacts + UAT for external groups
701
728
  if (official.hasUAT) {
702
- let contactClient = null;
703
- try { contactClient = await getUserClient(); } catch (_) {}
704
- const contactChatId = await chatIdMapper.resolveViaContacts(args.chat_id, contactClient);
729
+ if (!uc) try { uc = await getUserClient(); } catch (_) {}
730
+ const contactChatId = await chatIdMapper.resolveViaContacts(args.chat_id, uc);
705
731
  if (contactChatId) {
706
- return json(await official.readMessagesAsUser(contactChatId, msgOpts));
732
+ return json(await official.readMessagesAsUser(contactChatId, msgOpts, uc));
707
733
  }
708
734
  }
709
735
 
@@ -772,7 +798,7 @@ async function main() {
772
798
  const hasCookie = !!process.env.LARK_COOKIE;
773
799
  const hasApp = !!(process.env.LARK_APP_ID && process.env.LARK_APP_SECRET);
774
800
  const hasUAT = !!process.env.LARK_USER_ACCESS_TOKEN;
775
- console.error(`[feishu-user-plugin] MCP Server v1.1.1 — ${TOOLS.length} tools`);
801
+ console.error(`[feishu-user-plugin] MCP Server v1.1.2 — ${TOOLS.length} tools`);
776
802
  console.error(`[feishu-user-plugin] Auth: Cookie=${hasCookie ? 'YES' : 'NO'} App=${hasApp ? 'YES' : 'NO'} UAT=${hasUAT ? 'YES' : 'NO'}`);
777
803
  if (!hasCookie) console.error('[feishu-user-plugin] WARNING: LARK_COOKIE not set — user identity tools (send_to_user, etc.) will fail');
778
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 (sequential, with caching to avoid duplicate calls)
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.1 — Comprehensive Test ===\n');
277
+ console.log('=== feishu-user-plugin v1.1.2 — Comprehensive Test ===\n');
278
278
 
279
279
  await testUserIdentity();
280
280
  console.log('');