feishu-user-plugin 1.1.3 → 1.2.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/package.json +2 -2
- package/src/cli.js +85 -12
- package/src/client.js +6 -2
- package/src/config.js +188 -0
- package/src/index.js +267 -23
- package/src/oauth-auto.js +4 -25
- package/src/oauth.js +27 -53
- package/src/official.js +145 -65
- package/src/setup.js +96 -102
package/src/index.js
CHANGED
|
@@ -6,6 +6,8 @@ const {
|
|
|
6
6
|
ListToolsRequestSchema,
|
|
7
7
|
} = require('@modelcontextprotocol/sdk/types.js');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
// Local dev fallback: MCP clients inject env vars from config's env block at spawn time.
|
|
10
|
+
// This dotenv line only matters when running locally with a .env file (e.g. during development).
|
|
9
11
|
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
|
10
12
|
const { LarkUserClient } = require('./client');
|
|
11
13
|
const { LarkOfficialClient } = require('./official');
|
|
@@ -270,10 +272,10 @@ const TOOLS = [
|
|
|
270
272
|
},
|
|
271
273
|
{
|
|
272
274
|
name: 'get_chat_info',
|
|
273
|
-
description: '[User Identity] Get chat details: name, description, member count, owner.',
|
|
275
|
+
description: '[Official API + User Identity fallback] Get chat details: name, description, member count, owner. Supports both oc_xxx and numeric chat_id.',
|
|
274
276
|
inputSchema: {
|
|
275
277
|
type: 'object',
|
|
276
|
-
properties: { chat_id: { type: 'string', description: 'Chat ID' } },
|
|
278
|
+
properties: { chat_id: { type: 'string', description: 'Chat ID (oc_xxx or numeric)' } },
|
|
277
279
|
required: ['chat_id'],
|
|
278
280
|
},
|
|
279
281
|
},
|
|
@@ -394,6 +396,17 @@ const TOOLS = [
|
|
|
394
396
|
required: ['document_id'],
|
|
395
397
|
},
|
|
396
398
|
},
|
|
399
|
+
{
|
|
400
|
+
name: 'get_doc_blocks',
|
|
401
|
+
description: '[Official API] Get structured block tree of a document. Returns block types, content, and hierarchy for precise document analysis.',
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {
|
|
405
|
+
document_id: { type: 'string', description: 'Document ID (from search_docs or create_doc)' },
|
|
406
|
+
},
|
|
407
|
+
required: ['document_id'],
|
|
408
|
+
},
|
|
409
|
+
},
|
|
397
410
|
{
|
|
398
411
|
name: 'create_doc',
|
|
399
412
|
description: '[Official API] Create a new Feishu document.',
|
|
@@ -408,6 +421,17 @@ const TOOLS = [
|
|
|
408
421
|
},
|
|
409
422
|
|
|
410
423
|
// ========== Bitable — Official API ==========
|
|
424
|
+
{
|
|
425
|
+
name: 'create_bitable',
|
|
426
|
+
description: '[Official API] Create a new Bitable (multi-dimensional table) app.',
|
|
427
|
+
inputSchema: {
|
|
428
|
+
type: 'object',
|
|
429
|
+
properties: {
|
|
430
|
+
name: { type: 'string', description: 'Bitable app name' },
|
|
431
|
+
folder_id: { type: 'string', description: 'Parent folder token (optional, defaults to root)' },
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
411
435
|
{
|
|
412
436
|
name: 'list_bitable_tables',
|
|
413
437
|
description: '[Official API] List all tables in a Bitable app.',
|
|
@@ -417,6 +441,23 @@ const TOOLS = [
|
|
|
417
441
|
required: ['app_token'],
|
|
418
442
|
},
|
|
419
443
|
},
|
|
444
|
+
{
|
|
445
|
+
name: 'create_bitable_table',
|
|
446
|
+
description: '[Official API] Create a new data table in a Bitable app. Optionally define initial fields.',
|
|
447
|
+
inputSchema: {
|
|
448
|
+
type: 'object',
|
|
449
|
+
properties: {
|
|
450
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
451
|
+
name: { type: 'string', description: 'Table name' },
|
|
452
|
+
fields: {
|
|
453
|
+
type: 'array',
|
|
454
|
+
description: 'Initial field definitions (optional). Each item: {field_name, type} where type is 1=Text, 2=Number, 3=SingleSelect, 4=MultiSelect, 5=DateTime, 7=Checkbox, 11=User, 13=Phone, 15=URL, 17=Attachment, 18=Link, 20=Formula, 21=DuplexLink, 22=Location, 23=GroupChat, 1001=CreateTime, 1002=ModifiedTime, 1003=Creator, 1004=Modifier',
|
|
455
|
+
items: { type: 'object' },
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
required: ['app_token', 'name'],
|
|
459
|
+
},
|
|
460
|
+
},
|
|
420
461
|
{
|
|
421
462
|
name: 'list_bitable_fields',
|
|
422
463
|
description: '[Official API] List all fields (columns) in a Bitable table.',
|
|
@@ -429,6 +470,62 @@ const TOOLS = [
|
|
|
429
470
|
required: ['app_token', 'table_id'],
|
|
430
471
|
},
|
|
431
472
|
},
|
|
473
|
+
{
|
|
474
|
+
name: 'create_bitable_field',
|
|
475
|
+
description: '[Official API] Create a new field (column) in a Bitable table.',
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: 'object',
|
|
478
|
+
properties: {
|
|
479
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
480
|
+
table_id: { type: 'string', description: 'Table ID' },
|
|
481
|
+
field_name: { type: 'string', description: 'Field display name' },
|
|
482
|
+
type: { type: 'number', description: 'Field type: 1=Text, 2=Number, 3=SingleSelect, 4=MultiSelect, 5=DateTime, 7=Checkbox, 11=User, 13=Phone, 15=URL, 17=Attachment, 18=Link, 20=Formula, 21=DuplexLink, 22=Location, 23=GroupChat, 1001=CreateTime, 1002=ModifiedTime, 1003=Creator, 1004=Modifier' },
|
|
483
|
+
property: { type: 'object', description: 'Field-type-specific properties (optional). E.g. for SingleSelect: {options: [{name:"A"},{name:"B"}]}' },
|
|
484
|
+
},
|
|
485
|
+
required: ['app_token', 'table_id', 'field_name', 'type'],
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: 'update_bitable_field',
|
|
490
|
+
description: '[Official API] Update an existing field (column) in a Bitable table.',
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: 'object',
|
|
493
|
+
properties: {
|
|
494
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
495
|
+
table_id: { type: 'string', description: 'Table ID' },
|
|
496
|
+
field_id: { type: 'string', description: 'Field ID to update' },
|
|
497
|
+
field_name: { type: 'string', description: 'New field name (optional)' },
|
|
498
|
+
type: { type: 'number', description: 'Field type (REQUIRED by Feishu API, see create_bitable_field for values)' },
|
|
499
|
+
property: { type: 'object', description: 'Field-type-specific properties (optional)' },
|
|
500
|
+
},
|
|
501
|
+
required: ['app_token', 'table_id', 'field_id', 'type'],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'delete_bitable_field',
|
|
506
|
+
description: '[Official API] Delete a field (column) from a Bitable table.',
|
|
507
|
+
inputSchema: {
|
|
508
|
+
type: 'object',
|
|
509
|
+
properties: {
|
|
510
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
511
|
+
table_id: { type: 'string', description: 'Table ID' },
|
|
512
|
+
field_id: { type: 'string', description: 'Field ID to delete' },
|
|
513
|
+
},
|
|
514
|
+
required: ['app_token', 'table_id', 'field_id'],
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: 'list_bitable_views',
|
|
519
|
+
description: '[Official API] List all views in a Bitable table.',
|
|
520
|
+
inputSchema: {
|
|
521
|
+
type: 'object',
|
|
522
|
+
properties: {
|
|
523
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
524
|
+
table_id: { type: 'string', description: 'Table ID' },
|
|
525
|
+
},
|
|
526
|
+
required: ['app_token', 'table_id'],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
432
529
|
{
|
|
433
530
|
name: 'search_bitable_records',
|
|
434
531
|
description: '[Official API] Search/query records in a Bitable table.',
|
|
@@ -471,6 +568,58 @@ const TOOLS = [
|
|
|
471
568
|
required: ['app_token', 'table_id', 'record_id', 'fields'],
|
|
472
569
|
},
|
|
473
570
|
},
|
|
571
|
+
{
|
|
572
|
+
name: 'delete_bitable_record',
|
|
573
|
+
description: '[Official API] Delete a record (row) from a Bitable table.',
|
|
574
|
+
inputSchema: {
|
|
575
|
+
type: 'object',
|
|
576
|
+
properties: {
|
|
577
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
578
|
+
table_id: { type: 'string', description: 'Table ID' },
|
|
579
|
+
record_id: { type: 'string', description: 'Record ID to delete' },
|
|
580
|
+
},
|
|
581
|
+
required: ['app_token', 'table_id', 'record_id'],
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: 'batch_create_bitable_records',
|
|
586
|
+
description: '[Official API] Batch create multiple records (rows) in a Bitable table. Max 500 per call.',
|
|
587
|
+
inputSchema: {
|
|
588
|
+
type: 'object',
|
|
589
|
+
properties: {
|
|
590
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
591
|
+
table_id: { type: 'string', description: 'Table ID' },
|
|
592
|
+
records: { type: 'array', description: 'Array of {fields: {field_name: value}} objects', items: { type: 'object' } },
|
|
593
|
+
},
|
|
594
|
+
required: ['app_token', 'table_id', 'records'],
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: 'batch_update_bitable_records',
|
|
599
|
+
description: '[Official API] Batch update multiple records in a Bitable table. Max 500 per call.',
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: 'object',
|
|
602
|
+
properties: {
|
|
603
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
604
|
+
table_id: { type: 'string', description: 'Table ID' },
|
|
605
|
+
records: { type: 'array', description: 'Array of {record_id, fields: {field_name: value}} objects', items: { type: 'object' } },
|
|
606
|
+
},
|
|
607
|
+
required: ['app_token', 'table_id', 'records'],
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: 'batch_delete_bitable_records',
|
|
612
|
+
description: '[Official API] Batch delete multiple records from a Bitable table. Max 500 per call.',
|
|
613
|
+
inputSchema: {
|
|
614
|
+
type: 'object',
|
|
615
|
+
properties: {
|
|
616
|
+
app_token: { type: 'string', description: 'Bitable app token' },
|
|
617
|
+
table_id: { type: 'string', description: 'Table ID' },
|
|
618
|
+
record_ids: { type: 'array', description: 'Array of record IDs to delete', items: { type: 'string' } },
|
|
619
|
+
},
|
|
620
|
+
required: ['app_token', 'table_id', 'record_ids'],
|
|
621
|
+
},
|
|
622
|
+
},
|
|
474
623
|
|
|
475
624
|
// ========== Wiki — Official API ==========
|
|
476
625
|
{
|
|
@@ -522,6 +671,33 @@ const TOOLS = [
|
|
|
522
671
|
},
|
|
523
672
|
},
|
|
524
673
|
|
|
674
|
+
// ========== Upload — Official API ==========
|
|
675
|
+
{
|
|
676
|
+
name: 'upload_image',
|
|
677
|
+
description: '[Official API] Upload an image file to Feishu. Returns image_key for use with send_image_as_user.',
|
|
678
|
+
inputSchema: {
|
|
679
|
+
type: 'object',
|
|
680
|
+
properties: {
|
|
681
|
+
image_path: { type: 'string', description: 'Absolute path to the image file on disk' },
|
|
682
|
+
image_type: { type: 'string', enum: ['message', 'avatar'], description: 'Image usage type (default: message)' },
|
|
683
|
+
},
|
|
684
|
+
required: ['image_path'],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: 'upload_file',
|
|
689
|
+
description: '[Official API] Upload a file to Feishu. Returns file_key for use with send_file_as_user.',
|
|
690
|
+
inputSchema: {
|
|
691
|
+
type: 'object',
|
|
692
|
+
properties: {
|
|
693
|
+
file_path: { type: 'string', description: 'Absolute path to the file on disk' },
|
|
694
|
+
file_type: { type: 'string', enum: ['opus', 'mp4', 'pdf', 'doc', 'xls', 'ppt', 'stream'], description: 'File type (default: stream for generic files)' },
|
|
695
|
+
file_name: { type: 'string', description: 'Display file name (optional, defaults to basename)' },
|
|
696
|
+
},
|
|
697
|
+
required: ['file_path'],
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
|
|
525
701
|
// ========== Contact — Official API ==========
|
|
526
702
|
{
|
|
527
703
|
name: 'find_user',
|
|
@@ -539,7 +715,7 @@ const TOOLS = [
|
|
|
539
715
|
// --- Server ---
|
|
540
716
|
|
|
541
717
|
const server = new Server(
|
|
542
|
-
{ name: 'feishu-user-plugin', version: '
|
|
718
|
+
{ name: 'feishu-user-plugin', version: require('../package.json').version },
|
|
543
719
|
{ capabilities: { tools: {} } }
|
|
544
720
|
);
|
|
545
721
|
|
|
@@ -571,8 +747,13 @@ async function handleTool(name, args) {
|
|
|
571
747
|
case 'send_to_user': {
|
|
572
748
|
const c = await getUserClient();
|
|
573
749
|
const results = await c.search(args.user_name);
|
|
574
|
-
const
|
|
575
|
-
if (
|
|
750
|
+
const users = results.filter(r => r.type === 'user');
|
|
751
|
+
if (users.length === 0) return text(`User "${args.user_name}" not found. Results: ${JSON.stringify(results)}`);
|
|
752
|
+
if (users.length > 1) {
|
|
753
|
+
const candidates = users.slice(0, 5).map(u => ` - ${u.title} (ID: ${u.id})`).join('\n');
|
|
754
|
+
return text(`Multiple users match "${args.user_name}":\n${candidates}\nUse search_contacts to find the exact user, then create_p2p_chat + send_as_user.`);
|
|
755
|
+
}
|
|
756
|
+
const user = users[0];
|
|
576
757
|
const chatId = await c.createChat(user.id);
|
|
577
758
|
if (!chatId) return text(`Failed to create chat with ${user.title}`);
|
|
578
759
|
const r = await c.sendMessage(chatId, args.text);
|
|
@@ -581,8 +762,13 @@ async function handleTool(name, args) {
|
|
|
581
762
|
case 'send_to_group': {
|
|
582
763
|
const c = await getUserClient();
|
|
583
764
|
const results = await c.search(args.group_name);
|
|
584
|
-
const
|
|
585
|
-
if (
|
|
765
|
+
const groups = results.filter(r => r.type === 'group');
|
|
766
|
+
if (groups.length === 0) return text(`Group "${args.group_name}" not found. Results: ${JSON.stringify(results)}`);
|
|
767
|
+
if (groups.length > 1) {
|
|
768
|
+
const candidates = groups.slice(0, 5).map(g => ` - ${g.title} (ID: ${g.id})`).join('\n');
|
|
769
|
+
return text(`Multiple groups match "${args.group_name}":\n${candidates}\nUse search_contacts to find the exact group, then send_as_user with the ID.`);
|
|
770
|
+
}
|
|
771
|
+
const group = groups[0];
|
|
586
772
|
const r = await c.sendMessage(group.id, args.text);
|
|
587
773
|
return sendResult(r, `Text sent to group "${group.title}" (${group.id})`);
|
|
588
774
|
}
|
|
@@ -627,26 +813,37 @@ async function handleTool(name, args) {
|
|
|
627
813
|
return text(chatId ? `P2P chat: ${chatId}` : 'Failed to create P2P chat');
|
|
628
814
|
}
|
|
629
815
|
case 'get_chat_info': {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
816
|
+
// Strategy 1: Official API im.chat.get (supports oc_xxx format)
|
|
817
|
+
if (args.chat_id.startsWith('oc_')) {
|
|
818
|
+
try {
|
|
819
|
+
const info = await getOfficialClient().getChatInfo(args.chat_id);
|
|
820
|
+
return info ? json(info) : text(`No info for chat ${args.chat_id}`);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
console.error(`[feishu-user-plugin] Official getChatInfo failed: ${e.message}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// Strategy 2: Protobuf gateway (supports numeric chat_id)
|
|
826
|
+
try {
|
|
827
|
+
const c = await getUserClient();
|
|
828
|
+
const info = await c.getGroupInfo(args.chat_id);
|
|
829
|
+
if (info) return json(info);
|
|
830
|
+
} catch (e) {
|
|
831
|
+
console.error(`[feishu-user-plugin] Protobuf getChatInfo failed: ${e.message}`);
|
|
832
|
+
}
|
|
833
|
+
return text(`No info for chat ${args.chat_id}`);
|
|
633
834
|
}
|
|
634
835
|
case 'get_user_info': {
|
|
635
836
|
let n = null;
|
|
636
|
-
// Strategy 1:
|
|
837
|
+
// Strategy 1: Official API contact lookup (works for same-tenant users by open_id)
|
|
637
838
|
try {
|
|
638
|
-
const
|
|
639
|
-
n = await
|
|
640
|
-
if (!n && args.user_id) {
|
|
641
|
-
await c.search(args.user_id);
|
|
642
|
-
n = await c.getUserName(args.user_id);
|
|
643
|
-
}
|
|
839
|
+
const official = getOfficialClient();
|
|
840
|
+
n = await official.getUserById(args.user_id, 'open_id');
|
|
644
841
|
} catch {}
|
|
645
|
-
// Strategy 2:
|
|
842
|
+
// Strategy 2: User identity client cache (populated by previous search/init calls)
|
|
646
843
|
if (!n) {
|
|
647
844
|
try {
|
|
648
|
-
const
|
|
649
|
-
n = await
|
|
845
|
+
const c = await getUserClient();
|
|
846
|
+
n = await c.getUserName(args.user_id);
|
|
650
847
|
} catch {}
|
|
651
848
|
}
|
|
652
849
|
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.`);
|
|
@@ -672,7 +869,8 @@ async function handleTool(name, args) {
|
|
|
672
869
|
const official = getOfficialClient();
|
|
673
870
|
let chatId = args.chat_id;
|
|
674
871
|
let uc = null;
|
|
675
|
-
|
|
872
|
+
let ucError = null;
|
|
873
|
+
try { uc = await getUserClient(); } catch (e) { ucError = e; }
|
|
676
874
|
// If chat_id is not numeric or oc_, try to resolve as user name → P2P chat
|
|
677
875
|
if (!/^\d+$/.test(chatId) && !chatId.startsWith('oc_')) {
|
|
678
876
|
if (uc) {
|
|
@@ -689,7 +887,8 @@ async function handleTool(name, args) {
|
|
|
689
887
|
else return text(`Cannot resolve "${args.chat_id}" to a chat. Use search_contacts to find the ID first.`);
|
|
690
888
|
}
|
|
691
889
|
} else {
|
|
692
|
-
|
|
890
|
+
const hint = ucError ? `Cookie auth failed: ${ucError.message}. Fix LARK_COOKIE first, or p` : 'P';
|
|
891
|
+
return text(`"${args.chat_id}" is not a valid chat ID. ${hint}rovide a numeric ID or oc_xxx format. Use search_contacts + create_p2p_chat to get the ID.`);
|
|
693
892
|
}
|
|
694
893
|
}
|
|
695
894
|
return json(await official.readMessagesAsUser(chatId, {
|
|
@@ -752,15 +951,41 @@ async function handleTool(name, args) {
|
|
|
752
951
|
return json(await getOfficialClient().searchDocs(args.query));
|
|
753
952
|
case 'read_doc':
|
|
754
953
|
return json(await getOfficialClient().readDoc(args.document_id));
|
|
954
|
+
case 'get_doc_blocks':
|
|
955
|
+
return json(await getOfficialClient().getDocBlocks(args.document_id));
|
|
755
956
|
case 'create_doc':
|
|
756
957
|
return text(`Document created: ${(await getOfficialClient().createDoc(args.title, args.folder_id)).documentId}`);
|
|
757
958
|
|
|
758
959
|
// --- Official API: Bitable ---
|
|
759
960
|
|
|
961
|
+
case 'create_bitable': {
|
|
962
|
+
const r = await getOfficialClient().createBitable(args.name, args.folder_id);
|
|
963
|
+
return text(`Bitable created: ${r.appToken}${r.url ? `\nURL: ${r.url}` : ''}`);
|
|
964
|
+
}
|
|
760
965
|
case 'list_bitable_tables':
|
|
761
966
|
return json(await getOfficialClient().listBitableTables(args.app_token));
|
|
967
|
+
case 'create_bitable_table':
|
|
968
|
+
return text(`Table created: ${(await getOfficialClient().createBitableTable(args.app_token, args.name, args.fields)).tableId}`);
|
|
762
969
|
case 'list_bitable_fields':
|
|
763
970
|
return json(await getOfficialClient().listBitableFields(args.app_token, args.table_id));
|
|
971
|
+
case 'create_bitable_field': {
|
|
972
|
+
const config = { field_name: args.field_name, type: args.type };
|
|
973
|
+
if (args.property) config.property = args.property;
|
|
974
|
+
return json(await getOfficialClient().createBitableField(args.app_token, args.table_id, config));
|
|
975
|
+
}
|
|
976
|
+
case 'update_bitable_field': {
|
|
977
|
+
const config = {};
|
|
978
|
+
if (args.field_name) config.field_name = args.field_name;
|
|
979
|
+
if (args.type) config.type = args.type;
|
|
980
|
+
if (args.property) config.property = args.property;
|
|
981
|
+
return json(await getOfficialClient().updateBitableField(args.app_token, args.table_id, args.field_id, config));
|
|
982
|
+
}
|
|
983
|
+
case 'delete_bitable_field': {
|
|
984
|
+
const r = await getOfficialClient().deleteBitableField(args.app_token, args.table_id, args.field_id);
|
|
985
|
+
return text(r.deleted ? `Field ${r.fieldId} deleted` : `Field deletion returned deleted=${r.deleted}`);
|
|
986
|
+
}
|
|
987
|
+
case 'list_bitable_views':
|
|
988
|
+
return json(await getOfficialClient().listBitableViews(args.app_token, args.table_id));
|
|
764
989
|
case 'search_bitable_records':
|
|
765
990
|
return json(await getOfficialClient().searchBitableRecords(args.app_token, args.table_id, {
|
|
766
991
|
filter: args.filter, sort: args.sort, pageSize: args.page_size,
|
|
@@ -769,6 +994,14 @@ async function handleTool(name, args) {
|
|
|
769
994
|
return text(`Record created: ${(await getOfficialClient().createBitableRecord(args.app_token, args.table_id, args.fields)).recordId}`);
|
|
770
995
|
case 'update_bitable_record':
|
|
771
996
|
return text(`Record updated: ${(await getOfficialClient().updateBitableRecord(args.app_token, args.table_id, args.record_id, args.fields)).recordId}`);
|
|
997
|
+
case 'delete_bitable_record':
|
|
998
|
+
return text(`Record deleted: ${(await getOfficialClient().deleteBitableRecord(args.app_token, args.table_id, args.record_id)).deleted}`);
|
|
999
|
+
case 'batch_create_bitable_records':
|
|
1000
|
+
return json(await getOfficialClient().batchCreateBitableRecords(args.app_token, args.table_id, args.records));
|
|
1001
|
+
case 'batch_update_bitable_records':
|
|
1002
|
+
return json(await getOfficialClient().batchUpdateBitableRecords(args.app_token, args.table_id, args.records));
|
|
1003
|
+
case 'batch_delete_bitable_records':
|
|
1004
|
+
return json(await getOfficialClient().batchDeleteBitableRecords(args.app_token, args.table_id, args.record_ids));
|
|
772
1005
|
|
|
773
1006
|
// --- Official API: Wiki ---
|
|
774
1007
|
|
|
@@ -791,6 +1024,17 @@ async function handleTool(name, args) {
|
|
|
791
1024
|
case 'find_user':
|
|
792
1025
|
return json(await getOfficialClient().findUserByIdentity({ emails: args.email, mobiles: args.mobile }));
|
|
793
1026
|
|
|
1027
|
+
// --- Upload ---
|
|
1028
|
+
|
|
1029
|
+
case 'upload_image': {
|
|
1030
|
+
const r = await getOfficialClient().uploadImage(args.image_path, args.image_type);
|
|
1031
|
+
return text(`Image uploaded: ${r.imageKey}\nUse this image_key with send_image_as_user to send it.`);
|
|
1032
|
+
}
|
|
1033
|
+
case 'upload_file': {
|
|
1034
|
+
const r = await getOfficialClient().uploadFile(args.file_path, args.file_type, args.file_name);
|
|
1035
|
+
return text(`File uploaded: ${r.fileKey}\nUse this file_key with send_file_as_user to send it.`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
794
1038
|
default:
|
|
795
1039
|
return text(`Unknown tool: ${name}`);
|
|
796
1040
|
}
|
|
@@ -804,7 +1048,7 @@ async function main() {
|
|
|
804
1048
|
const hasCookie = !!process.env.LARK_COOKIE;
|
|
805
1049
|
const hasApp = !!(process.env.LARK_APP_ID && process.env.LARK_APP_SECRET);
|
|
806
1050
|
const hasUAT = !!process.env.LARK_USER_ACCESS_TOKEN;
|
|
807
|
-
console.error(`[feishu-user-plugin] MCP Server
|
|
1051
|
+
console.error(`[feishu-user-plugin] MCP Server v${require('../package.json').version} — ${TOOLS.length} tools`);
|
|
808
1052
|
console.error(`[feishu-user-plugin] Auth: Cookie=${hasCookie ? 'YES' : 'NO'} App=${hasApp ? 'YES' : 'NO'} UAT=${hasUAT ? 'YES' : 'NO'}`);
|
|
809
1053
|
if (!hasCookie) console.error('[feishu-user-plugin] WARNING: LARK_COOKIE not set — user identity tools (send_to_user, etc.) will fail');
|
|
810
1054
|
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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Automated OAuth
|
|
2
|
+
// DEV ONLY: Automated OAuth using local Playwright (not used in production).
|
|
3
|
+
// Uses .env directly; not migrated to config module.
|
|
4
|
+
// Requires: npm install playwright (not in package.json dependencies)
|
|
3
5
|
const http = require('http');
|
|
4
6
|
const { chromium } = require('playwright');
|
|
5
7
|
const fs = require('fs');
|
|
@@ -30,7 +32,7 @@ function saveToken(tokenData) {
|
|
|
30
32
|
LARK_USER_ACCESS_TOKEN: tokenData.access_token,
|
|
31
33
|
LARK_USER_REFRESH_TOKEN: tokenData.refresh_token || '',
|
|
32
34
|
LARK_UAT_SCOPE: tokenData.scope || '',
|
|
33
|
-
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + tokenData.expires_in)),
|
|
35
|
+
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + (typeof tokenData.expires_in === 'number' && tokenData.expires_in > 0 ? tokenData.expires_in : 7200))),
|
|
34
36
|
};
|
|
35
37
|
for (const [key, val] of Object.entries(updates)) {
|
|
36
38
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
@@ -161,29 +163,6 @@ async function run() {
|
|
|
161
163
|
console.log('scope:', tokenData.scope);
|
|
162
164
|
console.log('expires_in:', tokenData.expires_in, 's');
|
|
163
165
|
|
|
164
|
-
// Test P2P message reading
|
|
165
|
-
console.log('\n[test] Testing P2P message reading...');
|
|
166
|
-
const testRes = await fetch('https://open.feishu.cn/open-apis/im/v1/messages?container_id_type=chat&container_id=oc_97a52756ee2c4351a2a86e6aa33e8ca4&page_size=2&sort_type=ByCreateTimeDesc', {
|
|
167
|
-
headers: { 'Authorization': `Bearer ${tokenData.access_token}` },
|
|
168
|
-
});
|
|
169
|
-
const testData = await testRes.json();
|
|
170
|
-
if (testData.code === 0) {
|
|
171
|
-
console.log('[test] P2P: SUCCESS!', testData.data?.items?.length, 'messages');
|
|
172
|
-
} else {
|
|
173
|
-
console.log('[test] P2P: Error', testData.code, testData.msg);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Test group messages
|
|
177
|
-
const grpRes = await fetch('https://open.feishu.cn/open-apis/im/v1/messages?container_id_type=chat&container_id=oc_6ae081b457d07e9651d615493b7f1096&page_size=2&sort_type=ByCreateTimeDesc', {
|
|
178
|
-
headers: { 'Authorization': `Bearer ${tokenData.access_token}` },
|
|
179
|
-
});
|
|
180
|
-
const grpData = await grpRes.json();
|
|
181
|
-
if (grpData.code === 0) {
|
|
182
|
-
console.log('[test] Group: SUCCESS!', grpData.data?.items?.length, 'messages');
|
|
183
|
-
} else {
|
|
184
|
-
console.log('[test] Group: Error', grpData.code, grpData.msg);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
166
|
} catch (e) {
|
|
188
167
|
console.error('\nError:', e.message);
|
|
189
168
|
await page.screenshot({ path: '/tmp/feishu-oauth-error.png' }).catch(() => {});
|
package/src/oauth.js
CHANGED
|
@@ -2,33 +2,31 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* OAuth 授权脚本 — 获取带 IM 权限的 user_access_token
|
|
4
4
|
*
|
|
5
|
-
* 用法:
|
|
5
|
+
* 用法: npx feishu-user-plugin oauth
|
|
6
6
|
*
|
|
7
7
|
* 流程 (新版 End User Consent):
|
|
8
8
|
* 1. 查询应用信息,提示用户选择正确的飞书账号
|
|
9
9
|
* 2. 启动本地 HTTP 服务器 (端口 9997)
|
|
10
10
|
* 3. 打开 accounts.feishu.cn 授权页面 (新版 OAuth 2.0)
|
|
11
11
|
* 4. 用户点击"授权"后,用 /authen/v2/oauth/token 交换 token
|
|
12
|
-
* 5. 保存
|
|
12
|
+
* 5. 保存 token 到 MCP 配置文件
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const http = require('http');
|
|
16
16
|
const { execSync } = require('child_process');
|
|
17
|
-
const
|
|
18
|
-
const path = require('path');
|
|
19
|
-
const dotenv = require('dotenv');
|
|
17
|
+
const { readCredentials, persistToConfig } = require('./config');
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const APP_SECRET = process.env.LARK_APP_SECRET;
|
|
19
|
+
const creds = readCredentials();
|
|
20
|
+
const APP_ID = creds.LARK_APP_ID;
|
|
21
|
+
const APP_SECRET = creds.LARK_APP_SECRET;
|
|
25
22
|
const PORT = 9997;
|
|
26
23
|
const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`;
|
|
27
24
|
// offline_access is required to get refresh_token for auto-renewal
|
|
28
25
|
const SCOPES = 'offline_access im:message im:message:readonly im:chat:readonly contact:user.base:readonly';
|
|
29
26
|
|
|
30
27
|
if (!APP_ID || !APP_SECRET) {
|
|
31
|
-
console.error('Missing LARK_APP_ID or LARK_APP_SECRET
|
|
28
|
+
console.error('Missing LARK_APP_ID or LARK_APP_SECRET.');
|
|
29
|
+
console.error('Run "npx feishu-user-plugin setup" first to configure app credentials.');
|
|
32
30
|
process.exit(1);
|
|
33
31
|
}
|
|
34
32
|
|
|
@@ -100,10 +98,6 @@ async function exchangeCode(code) {
|
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
function saveToken(tokenData) {
|
|
103
|
-
const envPath = path.join(__dirname, '..', '.env');
|
|
104
|
-
let envContent = '';
|
|
105
|
-
try { envContent = fs.readFileSync(envPath, 'utf8'); } catch {}
|
|
106
|
-
|
|
107
101
|
const updates = {
|
|
108
102
|
LARK_USER_ACCESS_TOKEN: tokenData.access_token,
|
|
109
103
|
LARK_USER_REFRESH_TOKEN: tokenData.refresh_token || '',
|
|
@@ -111,44 +105,13 @@ function saveToken(tokenData) {
|
|
|
111
105
|
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + (typeof tokenData.expires_in === 'number' && tokenData.expires_in > 0 ? tokenData.expires_in : 7200))),
|
|
112
106
|
};
|
|
113
107
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
envContent += `\n${key}=${val}`;
|
|
108
|
+
const ok = persistToConfig(updates);
|
|
109
|
+
if (!ok) {
|
|
110
|
+
console.error('WARNING: Tokens could not be saved to config. Copy them manually:');
|
|
111
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
112
|
+
console.error(` ${k}=${v}`);
|
|
120
113
|
}
|
|
121
114
|
}
|
|
122
|
-
|
|
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 (e) {
|
|
148
|
-
console.error(`[feishu-user-plugin] Failed to persist tokens to ${cjPath}: ${e.message}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
console.error('[feishu-user-plugin] WARNING: Could not persist tokens to ~/.claude.json. Tokens saved to .env only — copy them to your MCP config manually.');
|
|
152
115
|
}
|
|
153
116
|
|
|
154
117
|
const server = http.createServer(async (req, res) => {
|
|
@@ -173,7 +136,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
173
136
|
<p>scope: ${tokenData.scope}</p>
|
|
174
137
|
<p>expires_in: ${tokenData.expires_in}s</p>
|
|
175
138
|
<p>refresh_token: ${hasRefresh ? '✅ 已获取(30天有效,支持自动续期)' : '❌ 未返回(token 将在 2 小时后过期,需重新授权)'}</p>
|
|
176
|
-
<p>已保存到
|
|
139
|
+
<p>已保存到 MCP 配置文件,可以关闭此页面。</p>`);
|
|
177
140
|
|
|
178
141
|
console.log('\n=== OAuth 授权成功 ===');
|
|
179
142
|
console.log('scope:', tokenData.scope);
|
|
@@ -185,7 +148,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
185
148
|
console.log(' - 授权时 scope 中未包含 offline_access');
|
|
186
149
|
console.log(' Token 将在 2 小时后过期,届时需要重新运行此脚本。');
|
|
187
150
|
}
|
|
188
|
-
console.log('token 已保存到
|
|
151
|
+
console.log('token 已保存到 MCP 配置文件');
|
|
189
152
|
|
|
190
153
|
setTimeout(() => { server.close(); process.exit(0); }, 1000);
|
|
191
154
|
} catch (e) {
|
|
@@ -200,6 +163,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
200
163
|
res.end('Not found');
|
|
201
164
|
});
|
|
202
165
|
|
|
166
|
+
server.on('error', (e) => {
|
|
167
|
+
if (e.code === 'EADDRINUSE') {
|
|
168
|
+
console.error(`\nPort ${PORT} is already in use. Another OAuth process may be running.`);
|
|
169
|
+
console.error('Wait a minute and try again, or kill the process using the port.');
|
|
170
|
+
} else {
|
|
171
|
+
console.error('Server error:', e.message);
|
|
172
|
+
}
|
|
173
|
+
process.exit(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
203
176
|
server.listen(PORT, '127.0.0.1', async () => {
|
|
204
177
|
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${APP_ID}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&response_type=code&scope=${encodeURIComponent(SCOPES)}`;
|
|
205
178
|
|
|
@@ -229,7 +202,8 @@ server.listen(PORT, '127.0.0.1', async () => {
|
|
|
229
202
|
console.log('授权 URL:', authUrl);
|
|
230
203
|
|
|
231
204
|
try {
|
|
232
|
-
|
|
205
|
+
const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
206
|
+
execSync(`${openCmd} "${authUrl}"`);
|
|
233
207
|
} catch {
|
|
234
208
|
console.log('\n请手动在浏览器中打开上面的 URL');
|
|
235
209
|
}
|