feishu-user-plugin 1.0.2 → 1.1.1
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 +37 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/skills/feishu-user-plugin/SKILL.md +1 -1
- package/skills/feishu-user-plugin/references/CLAUDE.md +3 -3
- package/src/cli.js +104 -0
- package/src/index.js +95 -19
- package/src/oauth-auto.js +1 -1
- package/src/official.js +206 -93
- package/src/setup.js +178 -0
- package/src/test-comprehensive.js +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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,43 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.1.1] - 2026-03-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **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`.
|
|
11
|
+
- **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`.
|
|
12
|
+
- **Numeric chat IDs not accepted by read_messages**: `resolveToOcId` now passes through numeric IDs directly.
|
|
13
|
+
|
|
14
|
+
## [1.1.0] - 2026-03-11
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **read_messages 400 error hidden**: Now shows actual Feishu error code and description instead of just "Request failed with status code 400"
|
|
18
|
+
- **Messages returned oldest first**: Default sort is now `ByCreateTimeDesc` (newest messages first) for both `read_messages` and `read_p2p_messages`
|
|
19
|
+
- **Chat name resolution**: Added `im.v1.chat.search` API as fallback when bot's group list doesn't contain the target chat
|
|
20
|
+
- **get_user_info fails for external users**: Added official contact API fallback (`contact.user.get`) for cross-tenant user lookup
|
|
21
|
+
- **Messages lack sender names**: `read_messages` and `read_p2p_messages` now auto-resolve sender IDs to display names
|
|
22
|
+
- **UAT persistence writes to npx temp dir**: Now persists refreshed tokens to `~/.claude.json` MCP config instead
|
|
23
|
+
- **oauth-auto.js missing offline_access scope**: Added `offline_access` to SCOPES (was missing, causing no refresh_token)
|
|
24
|
+
- **README "8 slash commands"**: Corrected to "9 slash commands" (was missing /drive)
|
|
25
|
+
- **CLAUDE.md false "type: stdio" warning**: Removed — `"type": "stdio"` is standard and harmless in Claude Code
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `sort_type` parameter for `read_messages` and `read_p2p_messages` (`ByCreateTimeDesc` / `ByCreateTimeAsc`)
|
|
29
|
+
- `senderName` field in message results (auto-resolved from sender ID)
|
|
30
|
+
- CLI subcommands: `npx feishu-user-plugin setup` (wizard), `oauth`, `status`
|
|
31
|
+
- `src/cli.js` — CLI dispatcher for subcommands
|
|
32
|
+
- `src/setup.js` — Interactive setup wizard (writes MCP config, validates credentials)
|
|
33
|
+
- `chatSearch()` method in official client (uses `im.v1.chat.search`)
|
|
34
|
+
- `getUserById()` method with caching for user name resolution
|
|
35
|
+
- `_safeSDKCall()` wrapper that extracts real Feishu errors from Lark SDK AxiosErrors
|
|
36
|
+
- `_populateSenderNames()` for batch sender name resolution in message lists
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- `package.json` bin entry points to `src/cli.js` (supports subcommands, default still starts MCP server)
|
|
40
|
+
- team-skills README rewritten for pure npm flow (no clone needed)
|
|
41
|
+
- CLAUDE.md OAuth instructions updated to use `npx feishu-user-plugin oauth`
|
|
42
|
+
- Error messages across all 33 tools now include actual Feishu error codes
|
|
43
|
+
|
|
7
44
|
## [1.0.2] - 2026-03-10
|
|
8
45
|
|
|
9
46
|
### Fixed
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The only MCP server that lets you send messages as your **personal identity** (n
|
|
|
16
16
|
- **Read everything** -- Group chats via bot API, P2P (direct messages) via OAuth UAT.
|
|
17
17
|
- **Full Feishu suite** -- Docs, Bitable (spreadsheets), Wiki, Drive, Contacts -- all in one plugin.
|
|
18
18
|
- **3 auth layers** -- Cookie-based user identity, app credentials (Official API), and OAuth UAT (P2P reading). All three are needed for full functionality.
|
|
19
|
-
- **
|
|
19
|
+
- **9 slash commands** for Claude Code -- `/send`, `/reply`, `/search`, `/digest`, `/doc`, `/table`, `/wiki`, `/drive`, `/status`
|
|
20
20
|
- **Auto session management** -- Cookie heartbeat every 4h, UAT auto-refresh with token rotation.
|
|
21
21
|
- **Chat name resolution** -- Pass a group name instead of `oc_xxx` ID; it resolves automatically.
|
|
22
22
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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": {
|
|
7
|
-
"feishu-user-plugin": "src/
|
|
7
|
+
"feishu-user-plugin": "src/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feishu-user-plugin
|
|
3
|
-
version: "1.
|
|
3
|
+
version: "1.1.1"
|
|
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)
|
|
@@ -93,7 +93,7 @@ window.__COOKIE__
|
|
|
93
93
|
|
|
94
94
|
**Step 4: Validate** — Must be pure ASCII, contain `session=` and `sl_session=`, length 500-5000. If >10000, it's contaminated.
|
|
95
95
|
|
|
96
|
-
**Step 5: Write config** using exact format
|
|
96
|
+
**Step 5: Write config** using exact format:
|
|
97
97
|
```json
|
|
98
98
|
{
|
|
99
99
|
"feishu-user-plugin": {
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for feishu-user-plugin
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx feishu-user-plugin → Start MCP server (default, used by Claude Code)
|
|
7
|
+
* npx feishu-user-plugin setup → Interactive setup wizard
|
|
8
|
+
* npx feishu-user-plugin oauth → Run OAuth flow for UAT
|
|
9
|
+
* npx feishu-user-plugin status → Check auth status
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const cmd = process.argv[2];
|
|
13
|
+
|
|
14
|
+
switch (cmd) {
|
|
15
|
+
case 'setup':
|
|
16
|
+
require('./setup');
|
|
17
|
+
break;
|
|
18
|
+
case 'oauth':
|
|
19
|
+
require('./oauth');
|
|
20
|
+
break;
|
|
21
|
+
case 'status':
|
|
22
|
+
checkStatus();
|
|
23
|
+
break;
|
|
24
|
+
case 'help':
|
|
25
|
+
case '--help':
|
|
26
|
+
case '-h':
|
|
27
|
+
printHelp();
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
// Default: start MCP server (used by Claude Code / MCP clients)
|
|
31
|
+
require('./index');
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printHelp() {
|
|
36
|
+
console.log(`
|
|
37
|
+
feishu-user-plugin — All-in-one Feishu MCP Server
|
|
38
|
+
|
|
39
|
+
Commands:
|
|
40
|
+
(default) Start MCP server (used by Claude Code)
|
|
41
|
+
setup Interactive setup wizard — writes MCP config
|
|
42
|
+
oauth Run OAuth flow to obtain user_access_token
|
|
43
|
+
status Check authentication status
|
|
44
|
+
help Show this help
|
|
45
|
+
|
|
46
|
+
Quick Start (team members):
|
|
47
|
+
1. npx feishu-user-plugin setup
|
|
48
|
+
2. Follow the prompts to configure credentials
|
|
49
|
+
3. Restart Claude Code
|
|
50
|
+
|
|
51
|
+
Quick Start (external users):
|
|
52
|
+
1. Create a Feishu app at https://open.feishu.cn/app
|
|
53
|
+
2. npx feishu-user-plugin setup
|
|
54
|
+
3. npx feishu-user-plugin oauth
|
|
55
|
+
4. Restart Claude Code
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function checkStatus() {
|
|
60
|
+
const { LarkUserClient } = require('./client');
|
|
61
|
+
const { LarkOfficialClient } = require('./official');
|
|
62
|
+
const path = require('path');
|
|
63
|
+
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
|
64
|
+
|
|
65
|
+
console.log('=== feishu-user-plugin Auth Status ===\n');
|
|
66
|
+
|
|
67
|
+
// Cookie
|
|
68
|
+
const cookie = process.env.LARK_COOKIE;
|
|
69
|
+
if (cookie) {
|
|
70
|
+
try {
|
|
71
|
+
const client = new LarkUserClient(cookie);
|
|
72
|
+
await client.init();
|
|
73
|
+
console.log(`Cookie: OK (user: ${client.userName || client.userId})`);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.log(`Cookie: FAILED — ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.log('Cookie: NOT SET');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// App credentials
|
|
82
|
+
const appId = process.env.LARK_APP_ID;
|
|
83
|
+
const appSecret = process.env.LARK_APP_SECRET;
|
|
84
|
+
console.log(`App credentials: ${appId && appSecret ? 'OK' : 'NOT SET'}`);
|
|
85
|
+
|
|
86
|
+
// UAT
|
|
87
|
+
const uat = process.env.LARK_USER_ACCESS_TOKEN;
|
|
88
|
+
const rt = process.env.LARK_USER_REFRESH_TOKEN;
|
|
89
|
+
if (uat) {
|
|
90
|
+
console.log(`UAT: SET (refresh_token: ${rt ? 'YES' : 'NO'})`);
|
|
91
|
+
if (appId && appSecret) {
|
|
92
|
+
const official = new LarkOfficialClient(appId, appSecret);
|
|
93
|
+
official.loadUAT();
|
|
94
|
+
try {
|
|
95
|
+
const chats = await official.listChatsAsUser({ pageSize: 1 });
|
|
96
|
+
console.log(` UAT test: OK (can list chats)`);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.log(` UAT test: FAILED — ${e.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
console.log('UAT: NOT SET (run: npx feishu-user-plugin oauth)');
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/index.js
CHANGED
|
@@ -48,8 +48,47 @@ class ChatIdMapper {
|
|
|
48
48
|
|
|
49
49
|
async resolveToOcId(chatIdOrName, official) {
|
|
50
50
|
if (chatIdOrName.startsWith('oc_')) return chatIdOrName;
|
|
51
|
-
//
|
|
52
|
-
|
|
51
|
+
// Also accept raw numeric IDs (from search_contacts)
|
|
52
|
+
if (/^\d+$/.test(chatIdOrName)) return chatIdOrName;
|
|
53
|
+
// Strategy 1: Search in bot's group list cache
|
|
54
|
+
const cached = await this.findByName(chatIdOrName, official);
|
|
55
|
+
if (cached) return cached;
|
|
56
|
+
// Strategy 2: Use im.v1.chat.search API (finds groups even if not in cache)
|
|
57
|
+
try {
|
|
58
|
+
const results = await official.chatSearch(chatIdOrName);
|
|
59
|
+
for (const chat of results) {
|
|
60
|
+
this.nameCache.set(chat.chat_id, chat.name || '');
|
|
61
|
+
if (chat.name === chatIdOrName) return chat.chat_id;
|
|
62
|
+
}
|
|
63
|
+
// Partial match on search results
|
|
64
|
+
for (const chat of results) {
|
|
65
|
+
if (chat.name && chat.name.includes(chatIdOrName)) return chat.chat_id;
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.error('[feishu-user-plugin] chatSearch fallback failed:', e.message);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
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;
|
|
53
92
|
}
|
|
54
93
|
}
|
|
55
94
|
|
|
@@ -251,7 +290,7 @@ const TOOLS = [
|
|
|
251
290
|
// ========== IM — Official API (User Identity via UAT) ==========
|
|
252
291
|
{
|
|
253
292
|
name: 'read_p2p_messages',
|
|
254
|
-
description: '[User UAT] Read P2P (direct message) chat history using user_access_token. Works for chats the bot cannot access.
|
|
293
|
+
description: '[User UAT] Read P2P (direct message) chat history using user_access_token. Works for chats the bot cannot access. Returns newest messages first by default. Requires OAuth setup.',
|
|
255
294
|
inputSchema: {
|
|
256
295
|
type: 'object',
|
|
257
296
|
properties: {
|
|
@@ -259,6 +298,7 @@ const TOOLS = [
|
|
|
259
298
|
page_size: { type: 'number', description: 'Messages to fetch (default 20, max 50)' },
|
|
260
299
|
start_time: { type: 'string', description: 'Start timestamp in seconds (optional)' },
|
|
261
300
|
end_time: { type: 'string', description: 'End timestamp in seconds (optional)' },
|
|
301
|
+
sort_type: { type: 'string', enum: ['ByCreateTimeDesc', 'ByCreateTimeAsc'], description: 'Sort order (default: ByCreateTimeDesc = newest first)' },
|
|
262
302
|
},
|
|
263
303
|
required: ['chat_id'],
|
|
264
304
|
},
|
|
@@ -289,14 +329,15 @@ const TOOLS = [
|
|
|
289
329
|
},
|
|
290
330
|
{
|
|
291
331
|
name: 'read_messages',
|
|
292
|
-
description: '[Official API] Read message history. Accepts oc_xxx ID or chat name (auto-
|
|
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.',
|
|
293
333
|
inputSchema: {
|
|
294
334
|
type: 'object',
|
|
295
335
|
properties: {
|
|
296
|
-
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)' },
|
|
297
337
|
page_size: { type: 'number', description: 'Messages to fetch (default 20, max 50)' },
|
|
298
338
|
start_time: { type: 'string', description: 'Start timestamp in seconds (optional)' },
|
|
299
339
|
end_time: { type: 'string', description: 'End timestamp in seconds (optional)' },
|
|
340
|
+
sort_type: { type: 'string', enum: ['ByCreateTimeDesc', 'ByCreateTimeAsc'], description: 'Sort order (default: ByCreateTimeDesc = newest first)' },
|
|
300
341
|
},
|
|
301
342
|
required: ['chat_id'],
|
|
302
343
|
},
|
|
@@ -490,7 +531,7 @@ const TOOLS = [
|
|
|
490
531
|
// --- Server ---
|
|
491
532
|
|
|
492
533
|
const server = new Server(
|
|
493
|
-
{ name: 'feishu-user-plugin', version: '1.
|
|
534
|
+
{ name: 'feishu-user-plugin', version: '1.1.1' },
|
|
494
535
|
{ capabilities: { tools: {} } }
|
|
495
536
|
);
|
|
496
537
|
|
|
@@ -583,15 +624,24 @@ async function handleTool(name, args) {
|
|
|
583
624
|
return info ? json(info) : text(`No info for chat ${args.chat_id}`);
|
|
584
625
|
}
|
|
585
626
|
case 'get_user_info': {
|
|
586
|
-
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
// Try searching to populate the cache
|
|
591
|
-
await c.search(args.user_id);
|
|
627
|
+
let n = null;
|
|
628
|
+
// Strategy 1: User identity client cache
|
|
629
|
+
try {
|
|
630
|
+
const c = await getUserClient();
|
|
592
631
|
n = await c.getUserName(args.user_id);
|
|
632
|
+
if (!n && args.user_id) {
|
|
633
|
+
await c.search(args.user_id);
|
|
634
|
+
n = await c.getUserName(args.user_id);
|
|
635
|
+
}
|
|
636
|
+
} catch {}
|
|
637
|
+
// Strategy 2: Official API contact lookup (works for same-tenant users)
|
|
638
|
+
if (!n) {
|
|
639
|
+
try {
|
|
640
|
+
const official = getOfficialClient();
|
|
641
|
+
n = await official.getUserById(args.user_id, 'open_id');
|
|
642
|
+
} catch {}
|
|
593
643
|
}
|
|
594
|
-
return text(n ? `User ${args.user_id}: ${n}` : `Could not resolve user ${args.user_id}. Try search_contacts with the user's name instead.`);
|
|
644
|
+
return text(n ? `User ${args.user_id}: ${n}` : `Could not resolve user ${args.user_id}. This user may be from an external tenant. Try search_contacts with the user's display name instead.`);
|
|
595
645
|
}
|
|
596
646
|
case 'get_login_status': {
|
|
597
647
|
const parts = [];
|
|
@@ -614,6 +664,7 @@ async function handleTool(name, args) {
|
|
|
614
664
|
const official = getOfficialClient();
|
|
615
665
|
return json(await official.readMessagesAsUser(args.chat_id, {
|
|
616
666
|
pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time,
|
|
667
|
+
sortType: args.sort_type,
|
|
617
668
|
}));
|
|
618
669
|
}
|
|
619
670
|
case 'list_user_chats':
|
|
@@ -625,13 +676,38 @@ async function handleTool(name, args) {
|
|
|
625
676
|
return json(await getOfficialClient().listChats({ pageSize: args.page_size, pageToken: args.page_token }));
|
|
626
677
|
case 'read_messages': {
|
|
627
678
|
const official = getOfficialClient();
|
|
679
|
+
const msgOpts = { pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time, sortType: args.sort_type };
|
|
628
680
|
const resolvedChatId = await chatIdMapper.resolveToOcId(args.chat_id, official);
|
|
629
|
-
|
|
630
|
-
|
|
681
|
+
|
|
682
|
+
// Try bot API first if we resolved an oc_ ID
|
|
683
|
+
if (resolvedChatId) {
|
|
684
|
+
try {
|
|
685
|
+
return json(await official.readMessages(resolvedChatId, msgOpts));
|
|
686
|
+
} catch (botErr) {
|
|
687
|
+
// Bot API failed (e.g. bot not in group, no permission) — fall through to UAT
|
|
688
|
+
console.error(`[feishu-user-plugin] read_messages bot API failed for ${resolvedChatId}: ${botErr.message}`);
|
|
689
|
+
if (official.hasUAT) {
|
|
690
|
+
try {
|
|
691
|
+
return json(await official.readMessagesAsUser(resolvedChatId, msgOpts));
|
|
692
|
+
} catch (uatErr) {
|
|
693
|
+
console.error(`[feishu-user-plugin] read_messages UAT fallback also failed for ${resolvedChatId}: ${uatErr.message}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
throw botErr; // Re-throw original error if UAT also failed
|
|
697
|
+
}
|
|
631
698
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
699
|
+
|
|
700
|
+
// Bot couldn't resolve the chat name — try search_contacts + UAT for external groups
|
|
701
|
+
if (official.hasUAT) {
|
|
702
|
+
let contactClient = null;
|
|
703
|
+
try { contactClient = await getUserClient(); } catch (_) {}
|
|
704
|
+
const contactChatId = await chatIdMapper.resolveViaContacts(args.chat_id, contactClient);
|
|
705
|
+
if (contactChatId) {
|
|
706
|
+
return json(await official.readMessagesAsUser(contactChatId, msgOpts));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
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.`);
|
|
635
711
|
}
|
|
636
712
|
case 'reply_message':
|
|
637
713
|
return text(`Reply sent: ${(await getOfficialClient().replyMessage(args.message_id, args.text)).messageId}`);
|
|
@@ -696,7 +772,7 @@ async function main() {
|
|
|
696
772
|
const hasCookie = !!process.env.LARK_COOKIE;
|
|
697
773
|
const hasApp = !!(process.env.LARK_APP_ID && process.env.LARK_APP_SECRET);
|
|
698
774
|
const hasUAT = !!process.env.LARK_USER_ACCESS_TOKEN;
|
|
699
|
-
console.error(`[feishu-user-plugin] MCP Server v1.
|
|
775
|
+
console.error(`[feishu-user-plugin] MCP Server v1.1.1 — ${TOOLS.length} tools`);
|
|
700
776
|
console.error(`[feishu-user-plugin] Auth: Cookie=${hasCookie ? 'YES' : 'NO'} App=${hasApp ? 'YES' : 'NO'} UAT=${hasUAT ? 'YES' : 'NO'}`);
|
|
701
777
|
if (!hasCookie) console.error('[feishu-user-plugin] WARNING: LARK_COOKIE not set — user identity tools (send_to_user, etc.) will fail');
|
|
702
778
|
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-auto.js
CHANGED
|
@@ -13,7 +13,7 @@ const APP_SECRET = process.env.LARK_APP_SECRET;
|
|
|
13
13
|
const COOKIE_STR = process.env.LARK_COOKIE;
|
|
14
14
|
const PORT = 9997;
|
|
15
15
|
const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`;
|
|
16
|
-
const SCOPES = 'im:message im:message:readonly im:chat:readonly contact:user.base:readonly';
|
|
16
|
+
const SCOPES = 'offline_access im:message im:message:readonly im:chat:readonly contact:user.base:readonly';
|
|
17
17
|
|
|
18
18
|
function parseCookies(cookieStr) {
|
|
19
19
|
return cookieStr.split(';').map(c => {
|
package/src/official.js
CHANGED
|
@@ -8,6 +8,7 @@ class LarkOfficialClient {
|
|
|
8
8
|
this._uat = null;
|
|
9
9
|
this._uatRefresh = null;
|
|
10
10
|
this._uatExpires = 0;
|
|
11
|
+
this._userNameCache = new Map(); // open_id → display name
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// --- UAT (User Access Token) Management ---
|
|
@@ -66,14 +67,60 @@ class LarkOfficialClient {
|
|
|
66
67
|
_persistUAT() {
|
|
67
68
|
const fs = require('fs');
|
|
68
69
|
const path = require('path');
|
|
70
|
+
const updates = {
|
|
71
|
+
LARK_USER_ACCESS_TOKEN: this._uat,
|
|
72
|
+
LARK_USER_REFRESH_TOKEN: this._uatRefresh,
|
|
73
|
+
LARK_UAT_EXPIRES: String(this._uatExpires),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Strategy 1: Update ~/.claude.json MCP config (works for npx users)
|
|
77
|
+
const claudeJsonPaths = [
|
|
78
|
+
path.join(process.env.HOME || '', '.claude.json'),
|
|
79
|
+
path.join(process.env.HOME || '', '.claude', '.claude.json'),
|
|
80
|
+
];
|
|
81
|
+
for (const cjPath of claudeJsonPaths) {
|
|
82
|
+
try {
|
|
83
|
+
const raw = fs.readFileSync(cjPath, 'utf8');
|
|
84
|
+
const config = JSON.parse(raw);
|
|
85
|
+
const servers = config.mcpServers || {};
|
|
86
|
+
// Find our server entry by name
|
|
87
|
+
for (const name of ['feishu-user-plugin', 'feishu']) {
|
|
88
|
+
if (servers[name]?.env) {
|
|
89
|
+
Object.assign(servers[name].env, updates);
|
|
90
|
+
fs.writeFileSync(cjPath, JSON.stringify(config, null, 2) + '\n');
|
|
91
|
+
console.error('[feishu-user-plugin] UAT persisted to', cjPath);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Strategy 2: Update project .mcp.json
|
|
99
|
+
const mcpJsonPaths = [
|
|
100
|
+
path.join(process.cwd(), '.mcp.json'),
|
|
101
|
+
];
|
|
102
|
+
for (const mjPath of mcpJsonPaths) {
|
|
103
|
+
try {
|
|
104
|
+
const raw = fs.readFileSync(mjPath, 'utf8');
|
|
105
|
+
const config = JSON.parse(raw);
|
|
106
|
+
const servers = config.mcpServers || config;
|
|
107
|
+
for (const name of ['feishu-user-plugin', 'feishu']) {
|
|
108
|
+
if (servers[name]?.env) {
|
|
109
|
+
Object.assign(servers[name].env, updates);
|
|
110
|
+
fs.writeFileSync(mjPath, JSON.stringify(config, null, 2) + '\n');
|
|
111
|
+
console.error('[feishu-user-plugin] UAT persisted to', mjPath);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Strategy 3: Fallback to .env in project root (for local dev)
|
|
69
119
|
const envPath = path.join(__dirname, '..', '.env');
|
|
70
120
|
try {
|
|
71
|
-
let env =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
LARK_USER_REFRESH_TOKEN: this._uatRefresh,
|
|
75
|
-
LARK_UAT_EXPIRES: String(this._uatExpires),
|
|
76
|
-
})) {
|
|
121
|
+
let env = '';
|
|
122
|
+
try { env = fs.readFileSync(envPath, 'utf8'); } catch {}
|
|
123
|
+
for (const [key, val] of Object.entries(updates)) {
|
|
77
124
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
78
125
|
if (regex.test(env)) env = env.replace(regex, `${key}=${val}`);
|
|
79
126
|
else env += `\n${key}=${val}`;
|
|
@@ -109,9 +156,10 @@ class LarkOfficialClient {
|
|
|
109
156
|
return { items: data.data.items || [], pageToken: data.data.page_token, hasMore: data.data.has_more };
|
|
110
157
|
}
|
|
111
158
|
|
|
112
|
-
async readMessagesAsUser(chatId, { pageSize = 20, startTime, endTime, pageToken } = {}) {
|
|
159
|
+
async readMessagesAsUser(chatId, { pageSize = 20, startTime, endTime, pageToken, sortType = 'ByCreateTimeDesc' } = {}) {
|
|
113
160
|
const params = new URLSearchParams({
|
|
114
161
|
container_id_type: 'chat', container_id: chatId, page_size: String(pageSize),
|
|
162
|
+
sort_type: sortType,
|
|
115
163
|
});
|
|
116
164
|
if (startTime) params.set('start_time', startTime);
|
|
117
165
|
if (endTime) params.set('end_time', endTime);
|
|
@@ -123,98 +171,107 @@ class LarkOfficialClient {
|
|
|
123
171
|
return res.json();
|
|
124
172
|
});
|
|
125
173
|
if (data.code !== 0) throw new Error(`readMessagesAsUser failed (${data.code}): ${data.msg}`);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
pageToken: data.data.page_token,
|
|
130
|
-
};
|
|
174
|
+
const items = (data.data.items || []).map(m => this._formatMessage(m));
|
|
175
|
+
await this._populateSenderNames(items);
|
|
176
|
+
return { items, hasMore: data.data.has_more, pageToken: data.data.page_token };
|
|
131
177
|
}
|
|
132
178
|
|
|
133
179
|
// --- IM ---
|
|
134
180
|
|
|
135
181
|
async listChats({ pageSize = 20, pageToken } = {}) {
|
|
136
|
-
const res = await this.
|
|
137
|
-
|
|
182
|
+
const res = await this._safeSDKCall(
|
|
183
|
+
() => this.client.im.chat.list({ params: { page_size: pageSize, page_token: pageToken } }),
|
|
184
|
+
'listChats'
|
|
185
|
+
);
|
|
138
186
|
return { items: res.data.items || [], pageToken: res.data.page_token, hasMore: res.data.has_more };
|
|
139
187
|
}
|
|
140
188
|
|
|
141
|
-
async readMessages(chatId, { pageSize = 20, startTime, endTime, pageToken } = {}) {
|
|
142
|
-
const params = { container_id_type: 'chat', container_id: chatId, page_size: pageSize };
|
|
189
|
+
async readMessages(chatId, { pageSize = 20, startTime, endTime, pageToken, sortType = 'ByCreateTimeDesc' } = {}) {
|
|
190
|
+
const params = { container_id_type: 'chat', container_id: chatId, page_size: pageSize, sort_type: sortType };
|
|
143
191
|
if (startTime) params.start_time = startTime;
|
|
144
192
|
if (endTime) params.end_time = endTime;
|
|
145
193
|
if (pageToken) params.page_token = pageToken;
|
|
146
|
-
const res = await this.client.im.message.list({ params });
|
|
147
|
-
|
|
148
|
-
|
|
194
|
+
const res = await this._safeSDKCall(() => this.client.im.message.list({ params }), 'readMessages');
|
|
195
|
+
const items = (res.data.items || []).map(m => this._formatMessage(m));
|
|
196
|
+
await this._populateSenderNames(items);
|
|
197
|
+
return { items, hasMore: res.data.has_more, pageToken: res.data.page_token };
|
|
149
198
|
}
|
|
150
199
|
|
|
151
200
|
async getMessage(messageId) {
|
|
152
|
-
const res = await this.
|
|
153
|
-
|
|
201
|
+
const res = await this._safeSDKCall(
|
|
202
|
+
() => this.client.im.message.get({ path: { message_id: messageId } }),
|
|
203
|
+
'getMessage'
|
|
204
|
+
);
|
|
154
205
|
return this._formatMessage(res.data);
|
|
155
206
|
}
|
|
156
207
|
|
|
157
208
|
async replyMessage(messageId, text, msgType = 'text') {
|
|
158
209
|
const content = msgType === 'text' ? JSON.stringify({ text }) : text;
|
|
159
|
-
const res = await this.
|
|
160
|
-
path: { message_id: messageId },
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (res.code !== 0) throw new Error(`replyMessage failed (${res.code}): ${res.msg}`);
|
|
210
|
+
const res = await this._safeSDKCall(
|
|
211
|
+
() => this.client.im.message.reply({ path: { message_id: messageId }, data: { content, msg_type: msgType } }),
|
|
212
|
+
'replyMessage'
|
|
213
|
+
);
|
|
164
214
|
return { messageId: res.data.message_id };
|
|
165
215
|
}
|
|
166
216
|
|
|
167
217
|
async forwardMessage(messageId, receiverId, receiveIdType = 'chat_id') {
|
|
168
|
-
const res = await this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
218
|
+
const res = await this._safeSDKCall(
|
|
219
|
+
() => this.client.im.message.forward({
|
|
220
|
+
path: { message_id: messageId },
|
|
221
|
+
data: { receive_id: receiverId },
|
|
222
|
+
params: { receive_id_type: receiveIdType },
|
|
223
|
+
}),
|
|
224
|
+
'forwardMessage'
|
|
225
|
+
);
|
|
174
226
|
return { messageId: res.data.message_id };
|
|
175
227
|
}
|
|
176
228
|
|
|
177
229
|
// --- Docs ---
|
|
178
230
|
|
|
179
231
|
async searchDocs(query, { pageSize = 10, pageToken } = {}) {
|
|
180
|
-
const res = await this.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
232
|
+
const res = await this._safeSDKCall(
|
|
233
|
+
() => this.client.request({
|
|
234
|
+
method: 'POST', url: '/open-apis/suite/docs-api/search/object',
|
|
235
|
+
data: { search_key: query, count: pageSize, offset: pageToken ? parseInt(pageToken) : 0, owner_ids: [], chat_ids: [], docs_types: [] },
|
|
236
|
+
}),
|
|
237
|
+
'searchDocs'
|
|
238
|
+
);
|
|
186
239
|
return { items: res.data.docs_entities || [], hasMore: res.data.has_more };
|
|
187
240
|
}
|
|
188
241
|
|
|
189
242
|
async readDoc(documentId) {
|
|
190
|
-
const res = await this.
|
|
191
|
-
|
|
243
|
+
const res = await this._safeSDKCall(
|
|
244
|
+
() => this.client.docx.document.rawContent({ path: { document_id: documentId }, params: { lang: 0 } }),
|
|
245
|
+
'readDoc'
|
|
246
|
+
);
|
|
192
247
|
return { content: res.data.content };
|
|
193
248
|
}
|
|
194
249
|
|
|
195
250
|
async createDoc(title, folderId) {
|
|
196
|
-
const res = await this.
|
|
197
|
-
|
|
251
|
+
const res = await this._safeSDKCall(
|
|
252
|
+
() => this.client.docx.document.create({ data: { title, folder_token: folderId || '' } }),
|
|
253
|
+
'createDoc'
|
|
254
|
+
);
|
|
198
255
|
return { documentId: res.data.document?.document_id };
|
|
199
256
|
}
|
|
200
257
|
|
|
201
258
|
async getDocBlocks(documentId) {
|
|
202
|
-
const res = await this.
|
|
203
|
-
|
|
259
|
+
const res = await this._safeSDKCall(
|
|
260
|
+
() => this.client.docx.documentBlock.list({ path: { document_id: documentId }, params: { page_size: 500 } }),
|
|
261
|
+
'getDocBlocks'
|
|
262
|
+
);
|
|
204
263
|
return { items: res.data.items || [] };
|
|
205
264
|
}
|
|
206
265
|
|
|
207
266
|
// --- Bitable ---
|
|
208
267
|
|
|
209
268
|
async listBitableTables(appToken) {
|
|
210
|
-
const res = await this.client.bitable.appTable.list({ path: { app_token: appToken } });
|
|
211
|
-
if (res.code !== 0) throw new Error(`listTables failed (${res.code}): ${res.msg}`);
|
|
269
|
+
const res = await this._safeSDKCall(() => this.client.bitable.appTable.list({ path: { app_token: appToken } }), 'listTables');
|
|
212
270
|
return { items: res.data.items || [] };
|
|
213
271
|
}
|
|
214
272
|
|
|
215
273
|
async listBitableFields(appToken, tableId) {
|
|
216
|
-
const res = await this.client.bitable.appTableField.list({ path: { app_token: appToken, table_id: tableId } });
|
|
217
|
-
if (res.code !== 0) throw new Error(`listFields failed (${res.code}): ${res.msg}`);
|
|
274
|
+
const res = await this._safeSDKCall(() => this.client.bitable.appTableField.list({ path: { app_token: appToken, table_id: tableId } }), 'listFields');
|
|
218
275
|
return { items: res.data.items || [] };
|
|
219
276
|
}
|
|
220
277
|
|
|
@@ -224,55 +281,46 @@ class LarkOfficialClient {
|
|
|
224
281
|
if (sort) data.sort = sort;
|
|
225
282
|
if (pageSize) data.page_size = pageSize;
|
|
226
283
|
if (pageToken) data.page_token = pageToken;
|
|
227
|
-
const res = await this.
|
|
228
|
-
path: { app_token: appToken, table_id: tableId },
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (res.code !== 0) throw new Error(`searchRecords failed (${res.code}): ${res.msg}`);
|
|
284
|
+
const res = await this._safeSDKCall(
|
|
285
|
+
() => this.client.bitable.appTableRecord.search({ path: { app_token: appToken, table_id: tableId }, data }),
|
|
286
|
+
'searchRecords'
|
|
287
|
+
);
|
|
232
288
|
return { items: res.data.items || [], total: res.data.total, hasMore: res.data.has_more };
|
|
233
289
|
}
|
|
234
290
|
|
|
235
291
|
async createBitableRecord(appToken, tableId, fields) {
|
|
236
|
-
const res = await this.
|
|
237
|
-
path: { app_token: appToken, table_id: tableId },
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (res.code !== 0) throw new Error(`createRecord failed (${res.code}): ${res.msg}`);
|
|
292
|
+
const res = await this._safeSDKCall(
|
|
293
|
+
() => this.client.bitable.appTableRecord.create({ path: { app_token: appToken, table_id: tableId }, data: { fields } }),
|
|
294
|
+
'createRecord'
|
|
295
|
+
);
|
|
241
296
|
return { recordId: res.data.record?.record_id };
|
|
242
297
|
}
|
|
243
298
|
|
|
244
299
|
async updateBitableRecord(appToken, tableId, recordId, fields) {
|
|
245
|
-
const res = await this.
|
|
246
|
-
path: { app_token: appToken, table_id: tableId, record_id: recordId },
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (res.code !== 0) throw new Error(`updateRecord failed (${res.code}): ${res.msg}`);
|
|
300
|
+
const res = await this._safeSDKCall(
|
|
301
|
+
() => this.client.bitable.appTableRecord.update({ path: { app_token: appToken, table_id: tableId, record_id: recordId }, data: { fields } }),
|
|
302
|
+
'updateRecord'
|
|
303
|
+
);
|
|
250
304
|
return { recordId: res.data.record?.record_id };
|
|
251
305
|
}
|
|
252
306
|
|
|
253
307
|
// --- Wiki ---
|
|
254
308
|
|
|
255
309
|
async listWikiSpaces() {
|
|
256
|
-
const res = await this.client.wiki.space.list({ params: { page_size: 50 } });
|
|
257
|
-
if (res.code !== 0) throw new Error(`listSpaces failed (${res.code}): ${res.msg}`);
|
|
310
|
+
const res = await this._safeSDKCall(() => this.client.wiki.space.list({ params: { page_size: 50 } }), 'listSpaces');
|
|
258
311
|
return { items: res.data.items || [] };
|
|
259
312
|
}
|
|
260
313
|
|
|
261
314
|
async searchWiki(query) {
|
|
262
|
-
const res = await this.
|
|
263
|
-
method: 'POST',
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
267
|
-
if (res.code !== 0) throw new Error(`searchWiki failed (${res.code}): ${res.msg}`);
|
|
315
|
+
const res = await this._safeSDKCall(
|
|
316
|
+
() => this.client.request({ method: 'POST', url: '/open-apis/suite/docs-api/search/object', data: { search_key: query, count: 20, offset: 0, owner_ids: [], chat_ids: [], docs_types: ['wiki'] } }),
|
|
317
|
+
'searchWiki'
|
|
318
|
+
);
|
|
268
319
|
return { items: res.data.docs_entities || [] };
|
|
269
320
|
}
|
|
270
321
|
|
|
271
322
|
async getWikiNode(spaceId, nodeToken) {
|
|
272
|
-
const res = await this.client.wiki.space.getNode({
|
|
273
|
-
params: { token: nodeToken },
|
|
274
|
-
});
|
|
275
|
-
if (res.code !== 0) throw new Error(`getNode failed (${res.code}): ${res.msg}`);
|
|
323
|
+
const res = await this._safeSDKCall(() => this.client.wiki.space.getNode({ params: { token: nodeToken } }), 'getNode');
|
|
276
324
|
return res.data.node;
|
|
277
325
|
}
|
|
278
326
|
|
|
@@ -280,11 +328,10 @@ class LarkOfficialClient {
|
|
|
280
328
|
const params = { page_size: 50 };
|
|
281
329
|
if (parentNodeToken) params.parent_node_token = parentNodeToken;
|
|
282
330
|
if (pageToken) params.page_token = pageToken;
|
|
283
|
-
const res = await this.
|
|
284
|
-
path: { space_id: spaceId },
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (res.code !== 0) throw new Error(`listNodes failed (${res.code}): ${res.msg}`);
|
|
331
|
+
const res = await this._safeSDKCall(
|
|
332
|
+
() => this.client.wiki.spaceNode.list({ path: { space_id: spaceId }, params }),
|
|
333
|
+
'listNodes'
|
|
334
|
+
);
|
|
288
335
|
return { items: res.data.items || [], hasMore: res.data.has_more };
|
|
289
336
|
}
|
|
290
337
|
|
|
@@ -293,16 +340,15 @@ class LarkOfficialClient {
|
|
|
293
340
|
async listFiles(folderToken, { pageSize = 50, pageToken } = {}) {
|
|
294
341
|
const params = { page_size: pageSize, folder_token: folderToken || '' };
|
|
295
342
|
if (pageToken) params.page_token = pageToken;
|
|
296
|
-
const res = await this.client.drive.file.list({ params });
|
|
297
|
-
if (res.code !== 0) throw new Error(`listFiles failed (${res.code}): ${res.msg}`);
|
|
343
|
+
const res = await this._safeSDKCall(() => this.client.drive.file.list({ params }), 'listFiles');
|
|
298
344
|
return { items: res.data.files || [], hasMore: res.data.has_more };
|
|
299
345
|
}
|
|
300
346
|
|
|
301
347
|
async createFolder(name, parentToken) {
|
|
302
|
-
const res = await this.
|
|
303
|
-
data: { name, folder_token: parentToken || '' },
|
|
304
|
-
|
|
305
|
-
|
|
348
|
+
const res = await this._safeSDKCall(
|
|
349
|
+
() => this.client.drive.file.createFolder({ data: { name, folder_token: parentToken || '' } }),
|
|
350
|
+
'createFolder'
|
|
351
|
+
);
|
|
306
352
|
return { token: res.data.token };
|
|
307
353
|
}
|
|
308
354
|
|
|
@@ -312,11 +358,10 @@ class LarkOfficialClient {
|
|
|
312
358
|
const data = {};
|
|
313
359
|
if (emails) data.emails = Array.isArray(emails) ? emails : [emails];
|
|
314
360
|
if (mobiles) data.mobiles = Array.isArray(mobiles) ? mobiles : [mobiles];
|
|
315
|
-
const res = await this.
|
|
316
|
-
data,
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (res.code !== 0) throw new Error(`findUser failed (${res.code}): ${res.msg}`);
|
|
361
|
+
const res = await this._safeSDKCall(
|
|
362
|
+
() => this.client.contact.user.batchGetId({ data, params: { user_id_type: 'open_id' } }),
|
|
363
|
+
'findUser'
|
|
364
|
+
);
|
|
320
365
|
return { userList: res.data.user_list || [] };
|
|
321
366
|
}
|
|
322
367
|
|
|
@@ -327,8 +372,10 @@ class LarkOfficialClient {
|
|
|
327
372
|
let pageToken;
|
|
328
373
|
let hasMore = true;
|
|
329
374
|
while (hasMore) {
|
|
330
|
-
const res = await this.
|
|
331
|
-
|
|
375
|
+
const res = await this._safeSDKCall(
|
|
376
|
+
() => this.client.im.chat.list({ params: { page_size: 100, page_token: pageToken } }),
|
|
377
|
+
'listAllChats'
|
|
378
|
+
);
|
|
332
379
|
allChats.push(...(res.data.items || []));
|
|
333
380
|
pageToken = res.data.page_token;
|
|
334
381
|
hasMore = res.data.has_more && !!pageToken;
|
|
@@ -336,6 +383,72 @@ class LarkOfficialClient {
|
|
|
336
383
|
return allChats;
|
|
337
384
|
}
|
|
338
385
|
|
|
386
|
+
// --- Safe SDK Call (extracts real Feishu error from AxiosError) ---
|
|
387
|
+
|
|
388
|
+
async _safeSDKCall(fn, label = 'API') {
|
|
389
|
+
try {
|
|
390
|
+
const res = await fn();
|
|
391
|
+
if (res.code !== 0) throw new Error(`${label} failed (${res.code}): ${res.msg}`);
|
|
392
|
+
return res;
|
|
393
|
+
} catch (err) {
|
|
394
|
+
// Lark SDK uses axios; extract actual Feishu error from response body
|
|
395
|
+
if (err.response?.data) {
|
|
396
|
+
const d = err.response.data;
|
|
397
|
+
const code = d.code ?? d.error ?? 'unknown';
|
|
398
|
+
const msg = d.msg ?? d.error_description ?? d.message ?? JSON.stringify(d);
|
|
399
|
+
throw new Error(`${label} failed (HTTP ${err.response.status}, code=${code}): ${msg}`);
|
|
400
|
+
}
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- Chat Search (keyword-based, works even if bot isn't in the group's list) ---
|
|
406
|
+
|
|
407
|
+
async chatSearch(query) {
|
|
408
|
+
const res = await this._safeSDKCall(
|
|
409
|
+
() => this.client.im.chat.search({ params: { query, page_size: 20 } }),
|
|
410
|
+
'chatSearch'
|
|
411
|
+
);
|
|
412
|
+
return res.data.items || [];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// --- User Name Resolution ---
|
|
416
|
+
|
|
417
|
+
async getUserById(userId, userIdType = 'open_id') {
|
|
418
|
+
if (this._userNameCache.has(userId)) return this._userNameCache.get(userId);
|
|
419
|
+
try {
|
|
420
|
+
const res = await this.client.contact.user.get({
|
|
421
|
+
path: { user_id: userId },
|
|
422
|
+
params: { user_id_type: userIdType },
|
|
423
|
+
});
|
|
424
|
+
if (res.code === 0 && res.data?.user?.name) {
|
|
425
|
+
this._userNameCache.set(userId, res.data.user.name);
|
|
426
|
+
return res.data.user.name;
|
|
427
|
+
}
|
|
428
|
+
} catch {}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async _populateSenderNames(items) {
|
|
433
|
+
// Collect unique sender IDs that aren't cached
|
|
434
|
+
const unknownIds = new Set();
|
|
435
|
+
for (const item of items) {
|
|
436
|
+
if (item.senderId && !this._userNameCache.has(item.senderId)) {
|
|
437
|
+
unknownIds.add(item.senderId);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Batch resolve (sequential, with caching to avoid duplicate calls)
|
|
441
|
+
for (const id of unknownIds) {
|
|
442
|
+
await this.getUserById(id);
|
|
443
|
+
}
|
|
444
|
+
// Populate senderName field
|
|
445
|
+
for (const item of items) {
|
|
446
|
+
if (item.senderId) {
|
|
447
|
+
item.senderName = this._userNameCache.get(item.senderId) || null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
339
452
|
// --- Helpers ---
|
|
340
453
|
|
|
341
454
|
_formatMessage(m) {
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Interactive setup wizard for feishu-user-plugin
|
|
4
|
+
*
|
|
5
|
+
* Writes MCP config to ~/.claude.json (or .mcp.json) with credentials.
|
|
6
|
+
* Does NOT require cloning the repo.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const readline = require('readline');
|
|
12
|
+
|
|
13
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
15
|
+
|
|
16
|
+
const CLAUDE_JSON_PATH = path.join(process.env.HOME || '', '.claude.json');
|
|
17
|
+
const SERVER_NAME = 'feishu-user-plugin';
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
console.log('='.repeat(60));
|
|
21
|
+
console.log(' feishu-user-plugin Setup Wizard');
|
|
22
|
+
console.log('='.repeat(60));
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
// Check existing config
|
|
26
|
+
let config = {};
|
|
27
|
+
let existingEnv = {};
|
|
28
|
+
try {
|
|
29
|
+
config = JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, 'utf8'));
|
|
30
|
+
const existing = config.mcpServers?.[SERVER_NAME]?.env || config.mcpServers?.feishu?.env;
|
|
31
|
+
if (existing) {
|
|
32
|
+
existingEnv = existing;
|
|
33
|
+
console.log('Found existing feishu-user-plugin config in ~/.claude.json');
|
|
34
|
+
const update = await ask('Update existing config? (Y/n): ');
|
|
35
|
+
if (update.toLowerCase() === 'n') {
|
|
36
|
+
console.log('Cancelled.');
|
|
37
|
+
rl.close();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
42
|
+
|
|
43
|
+
// Collect credentials
|
|
44
|
+
console.log('\n--- App Credentials ---');
|
|
45
|
+
console.log('Team members: press Enter to use the shared defaults.');
|
|
46
|
+
console.log('External users: get these from https://open.feishu.cn/app\n');
|
|
47
|
+
|
|
48
|
+
const defaultAppId = existingEnv.LARK_APP_ID || '';
|
|
49
|
+
const defaultAppSecret = existingEnv.LARK_APP_SECRET || '';
|
|
50
|
+
|
|
51
|
+
let appId = await ask(`LARK_APP_ID [${defaultAppId || 'required'}]: `);
|
|
52
|
+
appId = appId.trim() || defaultAppId;
|
|
53
|
+
if (!appId) {
|
|
54
|
+
console.error('Error: LARK_APP_ID is required.');
|
|
55
|
+
rl.close();
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let appSecret = await ask(`LARK_APP_SECRET [${defaultAppSecret ? '***' : 'required'}]: `);
|
|
60
|
+
appSecret = appSecret.trim() || defaultAppSecret;
|
|
61
|
+
if (!appSecret) {
|
|
62
|
+
console.error('Error: LARK_APP_SECRET is required.');
|
|
63
|
+
rl.close();
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate app credentials
|
|
68
|
+
console.log('\nValidating app credentials...');
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'content-type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
74
|
+
});
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
if (data.app_access_token) {
|
|
77
|
+
console.log('App credentials: VALID');
|
|
78
|
+
} else {
|
|
79
|
+
console.error(`App credentials: INVALID — ${data.msg || JSON.stringify(data)}`);
|
|
80
|
+
console.error('Please check your LARK_APP_ID and LARK_APP_SECRET.');
|
|
81
|
+
rl.close();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.warn(`Could not validate: ${e.message}. Continuing anyway.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Cookie
|
|
89
|
+
console.log('\n--- Cookie ---');
|
|
90
|
+
console.log('Get your cookie from feishu.cn (Network tab → first request → Cookie header).');
|
|
91
|
+
console.log('Or let Claude Code + Playwright extract it automatically after setup.\n');
|
|
92
|
+
|
|
93
|
+
const existingCookie = existingEnv.LARK_COOKIE;
|
|
94
|
+
const hasCookie = existingCookie && existingCookie !== 'PLACEHOLDER' && existingCookie.includes('session=');
|
|
95
|
+
if (hasCookie) {
|
|
96
|
+
console.log('Existing cookie found (has session token).');
|
|
97
|
+
const keepCookie = await ask('Keep existing cookie? (Y/n): ');
|
|
98
|
+
if (keepCookie.toLowerCase() === 'n') {
|
|
99
|
+
console.log('You can update it later or use Playwright extraction.');
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
console.log('No valid cookie found. You can add it later via:');
|
|
103
|
+
console.log(' 1. Tell Claude Code: "帮我设置飞书 Cookie" (with Playwright MCP)');
|
|
104
|
+
console.log(' 2. Manual: DevTools → Network → Cookie header → paste into config');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cookie = hasCookie ? existingCookie : 'SETUP_NEEDED';
|
|
108
|
+
|
|
109
|
+
// UAT
|
|
110
|
+
const existingUAT = existingEnv.LARK_USER_ACCESS_TOKEN;
|
|
111
|
+
const existingRT = existingEnv.LARK_USER_REFRESH_TOKEN;
|
|
112
|
+
const hasUAT = existingUAT && existingUAT !== 'PLACEHOLDER' && existingUAT.length > 20;
|
|
113
|
+
|
|
114
|
+
if (!hasUAT) {
|
|
115
|
+
console.log('\n--- OAuth UAT ---');
|
|
116
|
+
console.log('UAT not configured. After setup, run:');
|
|
117
|
+
console.log(' npx feishu-user-plugin oauth');
|
|
118
|
+
console.log('This will open a browser for OAuth consent.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Write config
|
|
122
|
+
console.log('\n--- Writing Config ---');
|
|
123
|
+
|
|
124
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
125
|
+
config.mcpServers[SERVER_NAME] = {
|
|
126
|
+
command: 'npx',
|
|
127
|
+
args: ['-y', 'feishu-user-plugin'],
|
|
128
|
+
env: {
|
|
129
|
+
LARK_COOKIE: cookie,
|
|
130
|
+
LARK_APP_ID: appId,
|
|
131
|
+
LARK_APP_SECRET: appSecret,
|
|
132
|
+
LARK_USER_ACCESS_TOKEN: hasUAT ? existingUAT : 'SETUP_NEEDED',
|
|
133
|
+
LARK_USER_REFRESH_TOKEN: hasUAT ? (existingRT || '') : '',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Remove old 'feishu' entry if exists (consolidate)
|
|
138
|
+
if (config.mcpServers.feishu && config.mcpServers[SERVER_NAME]) {
|
|
139
|
+
delete config.mcpServers.feishu;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
143
|
+
console.log(`Written to ${CLAUDE_JSON_PATH}`);
|
|
144
|
+
|
|
145
|
+
// Also write .env for oauth.js to use
|
|
146
|
+
const envPath = path.join(__dirname, '..', '.env');
|
|
147
|
+
const envContent = [
|
|
148
|
+
`LARK_APP_ID=${appId}`,
|
|
149
|
+
`LARK_APP_SECRET=${appSecret}`,
|
|
150
|
+
cookie !== 'SETUP_NEEDED' ? `LARK_COOKIE=${cookie}` : '',
|
|
151
|
+
hasUAT ? `LARK_USER_ACCESS_TOKEN=${existingUAT}` : '',
|
|
152
|
+
hasUAT && existingRT ? `LARK_USER_REFRESH_TOKEN=${existingRT}` : '',
|
|
153
|
+
].filter(Boolean).join('\n') + '\n';
|
|
154
|
+
fs.writeFileSync(envPath, envContent);
|
|
155
|
+
|
|
156
|
+
// Summary
|
|
157
|
+
console.log('\n' + '='.repeat(60));
|
|
158
|
+
console.log(' Setup Complete!');
|
|
159
|
+
console.log('='.repeat(60));
|
|
160
|
+
console.log('');
|
|
161
|
+
|
|
162
|
+
const todo = [];
|
|
163
|
+
if (cookie === 'SETUP_NEEDED') todo.push('Get Cookie: tell Claude Code "帮我设置飞书 Cookie"');
|
|
164
|
+
if (!hasUAT) todo.push('Get UAT: run "npx feishu-user-plugin oauth"');
|
|
165
|
+
todo.push('Restart Claude Code');
|
|
166
|
+
|
|
167
|
+
console.log('Next steps:');
|
|
168
|
+
todo.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
169
|
+
console.log('');
|
|
170
|
+
|
|
171
|
+
rl.close();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch(e => {
|
|
175
|
+
console.error('Setup failed:', e.message);
|
|
176
|
+
rl.close();
|
|
177
|
+
process.exit(1);
|
|
178
|
+
});
|
|
@@ -274,7 +274,7 @@ async function testUAT() {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
async function main() {
|
|
277
|
-
console.log('=== feishu-user-plugin v1.
|
|
277
|
+
console.log('=== feishu-user-plugin v1.1.1 — Comprehensive Test ===\n');
|
|
278
278
|
|
|
279
279
|
await testUserIdentity();
|
|
280
280
|
console.log('');
|