feishu-user-plugin 1.3.3 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +21 -0
- package/README.md +19 -5
- package/package.json +2 -2
- package/skills/feishu-user-plugin/SKILL.md +3 -3
- package/skills/feishu-user-plugin/references/CLAUDE.md +99 -3
- package/src/doc-blocks.js +70 -0
- package/src/error-codes.js +78 -0
- package/src/index.js +249 -66
- package/src/oauth.js +6 -1
- package/src/official.js +476 -10
- package/src/resolver.js +151 -0
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const path = require('path');
|
|
|
21
21
|
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
|
22
22
|
const { LarkUserClient } = require('./client');
|
|
23
23
|
const { LarkOfficialClient } = require('./official');
|
|
24
|
+
const { resolveToObj, resolveToken, parseFeishuInput } = require('./resolver');
|
|
24
25
|
|
|
25
26
|
// --- Chat ID Mapper ---
|
|
26
27
|
|
|
@@ -434,12 +435,14 @@ const TOOLS = [
|
|
|
434
435
|
},
|
|
435
436
|
{
|
|
436
437
|
name: 'create_doc',
|
|
437
|
-
description: '[Official API] Create a new Feishu document.',
|
|
438
|
+
description: '[Official API] Create a new Feishu document. Can place directly under a Wiki space by passing wiki_space_id (optionally wiki_parent_node_token for nested placement) — the plugin creates the doc in drive then attaches it as a Wiki node.',
|
|
438
439
|
inputSchema: {
|
|
439
440
|
type: 'object',
|
|
440
441
|
properties: {
|
|
441
442
|
title: { type: 'string', description: 'Document title' },
|
|
442
|
-
folder_id: { type: 'string', description: 'Parent folder token (optional)' },
|
|
443
|
+
folder_id: { type: 'string', description: 'Parent folder token (optional; ignored when wiki_space_id is set)' },
|
|
444
|
+
wiki_space_id: { type: 'string', description: 'Wiki space ID to place the doc under (optional)' },
|
|
445
|
+
wiki_parent_node_token: { type: 'string', description: 'Parent wiki node token within the space (optional; defaults to space root)' },
|
|
443
446
|
},
|
|
444
447
|
required: ['title'],
|
|
445
448
|
},
|
|
@@ -448,12 +451,14 @@ const TOOLS = [
|
|
|
448
451
|
// ========== Bitable — Official API ==========
|
|
449
452
|
{
|
|
450
453
|
name: 'create_bitable',
|
|
451
|
-
description: '[Official API] Create a new Bitable (multi-dimensional table) app.',
|
|
454
|
+
description: '[Official API] Create a new Bitable (multi-dimensional table) app. Can place directly under a Wiki space via wiki_space_id (and optional wiki_parent_node_token).',
|
|
452
455
|
inputSchema: {
|
|
453
456
|
type: 'object',
|
|
454
457
|
properties: {
|
|
455
458
|
name: { type: 'string', description: 'Bitable app name' },
|
|
456
|
-
folder_id: { type: 'string', description: 'Parent folder token (optional, defaults to root)' },
|
|
459
|
+
folder_id: { type: 'string', description: 'Parent folder token (optional, defaults to root; ignored when wiki_space_id is set)' },
|
|
460
|
+
wiki_space_id: { type: 'string', description: 'Wiki space ID to place the bitable under (optional)' },
|
|
461
|
+
wiki_parent_node_token: { type: 'string', description: 'Parent wiki node token within the space (optional)' },
|
|
457
462
|
},
|
|
458
463
|
},
|
|
459
464
|
},
|
|
@@ -830,29 +835,32 @@ const TOOLS = [
|
|
|
830
835
|
// ========== Docs — Block Editing ==========
|
|
831
836
|
{
|
|
832
837
|
name: 'create_doc_block',
|
|
833
|
-
description: '[Official API] Insert content blocks into a document.
|
|
838
|
+
description: '[Official API] Insert content blocks into a document. Three modes:\n (A) Generic — pass `children` array (e.g. [{block_type:2, text:{...}}]) for text/heading/list/etc.\n (B) Image from local file — pass `image_path` (absolute path); the plugin creates an image block, uploads the file to drive, and patches the block with the token. Returns block_id + image_token.\n (C) Image from uploaded token — pass `image_token` (from a previous uploadDocMedia or docx image block) to reuse an already-uploaded image.\n`document_id` accepts native document_id, wiki node token, or Feishu URL.',
|
|
834
839
|
inputSchema: {
|
|
835
840
|
type: 'object',
|
|
836
841
|
properties: {
|
|
837
|
-
document_id: { type: 'string', description: 'Document ID' },
|
|
842
|
+
document_id: { type: 'string', description: 'Document ID, wiki node token, or Feishu URL' },
|
|
838
843
|
parent_block_id: { type: 'string', description: 'Parent block ID (use document_id for root)' },
|
|
839
|
-
children: { type: 'array', description: '
|
|
844
|
+
children: { type: 'array', description: 'Generic block objects — mode A. E.g. [{block_type:2, text:{elements:[{text_run:{content:"Hello"}}]}}]', items: { type: 'object' } },
|
|
845
|
+
image_path: { type: 'string', description: 'Local image path — mode B (mutually exclusive with children / image_token)' },
|
|
846
|
+
image_token: { type: 'string', description: 'Pre-uploaded docx image token — mode C (mutually exclusive with children / image_path)' },
|
|
840
847
|
index: { type: 'number', description: 'Insert position (optional, appends to end if omitted)' },
|
|
841
848
|
},
|
|
842
|
-
required: ['document_id', 'parent_block_id'
|
|
849
|
+
required: ['document_id', 'parent_block_id'],
|
|
843
850
|
},
|
|
844
851
|
},
|
|
845
852
|
{
|
|
846
853
|
name: 'update_doc_block',
|
|
847
|
-
description: '[Official API] Update a specific block in a document
|
|
854
|
+
description: '[Official API] Update a specific block in a document. Generic mode: pass update_body. Image-replace mode: pass image_token to swap the picture in an existing image block. document_id accepts native ID, wiki node token, or Feishu URL.',
|
|
848
855
|
inputSchema: {
|
|
849
856
|
type: 'object',
|
|
850
857
|
properties: {
|
|
851
|
-
document_id: { type: 'string', description: 'Document ID' },
|
|
858
|
+
document_id: { type: 'string', description: 'Document ID, wiki node token, or Feishu URL' },
|
|
852
859
|
block_id: { type: 'string', description: 'Block ID to update' },
|
|
853
|
-
update_body: { type: 'object', description: '
|
|
860
|
+
update_body: { type: 'object', description: 'Generic update payload. E.g. {update_text_elements:{elements:[{text_run:{content:"new text"}}]}}' },
|
|
861
|
+
image_token: { type: 'string', description: 'Pre-uploaded image token — if provided, update_body is ignored and the block is patched with {replace_image:{token}}' },
|
|
854
862
|
},
|
|
855
|
-
required: ['document_id', 'block_id'
|
|
863
|
+
required: ['document_id', 'block_id'],
|
|
856
864
|
},
|
|
857
865
|
},
|
|
858
866
|
{
|
|
@@ -1005,14 +1013,112 @@ const TOOLS = [
|
|
|
1005
1013
|
// ========== Message Resources (Image/File Download) ==========
|
|
1006
1014
|
{
|
|
1007
1015
|
name: 'download_image',
|
|
1008
|
-
description: '[User Identity / Official API] Download an image
|
|
1016
|
+
description: '[User Identity / Official API] Download an image so the model can actually see it. Two modes: (1) message image — pass message_id + image_key from read_messages / read_p2p_messages. (2) docx image — pass doc_token + image_token (the block.image.token from get_doc_blocks). doc_token accepts native document_id, wiki node token, or Feishu URL. Tries user identity first, falls back to app.',
|
|
1009
1017
|
inputSchema: {
|
|
1010
1018
|
type: 'object',
|
|
1011
1019
|
properties: {
|
|
1012
|
-
message_id: { type: 'string', description: '
|
|
1013
|
-
image_key: { type: 'string', description: '
|
|
1020
|
+
message_id: { type: 'string', description: 'Message ID (om_xxx) — for mode 1 only' },
|
|
1021
|
+
image_key: { type: 'string', description: 'Image key (img_xxx) from message content — for mode 1 only' },
|
|
1022
|
+
doc_token: { type: 'string', description: 'Document ID, wiki node token, or Feishu URL — for mode 2 only' },
|
|
1023
|
+
image_token: { type: 'string', description: 'Image token from a docx image block (block.image.token via get_doc_blocks) — for mode 2 only' },
|
|
1014
1024
|
},
|
|
1015
|
-
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
// ========== Wiki Node — Object Resolution (v1.3.4) ==========
|
|
1029
|
+
{
|
|
1030
|
+
name: 'get_wiki_node',
|
|
1031
|
+
description: '[Official API] Resolve a Wiki node token to its underlying object (docx / bitable / sheet / mindnote / file). Returns obj_type + obj_token + space_id so you can read/write the real resource via the usual docx / bitable tools. Accepts bare wiki node token (wikcnXXX) or a full Feishu /wiki/ URL.',
|
|
1032
|
+
inputSchema: {
|
|
1033
|
+
type: 'object',
|
|
1034
|
+
properties: {
|
|
1035
|
+
node_token: { type: 'string', description: 'Wiki node token (wikcnXXX / wikmXXX / wiknXXX) or full Feishu /wiki/<token> URL' },
|
|
1036
|
+
},
|
|
1037
|
+
required: ['node_token'],
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
|
|
1041
|
+
// ========== OKR — Official API (v1.3.4) ==========
|
|
1042
|
+
{
|
|
1043
|
+
name: 'list_user_okrs',
|
|
1044
|
+
description: '[Official API + UAT] List a user\'s OKRs. Requires the user\'s open_id (get yours via get_login_status or search_contacts). Filter by period_ids to narrow to a specific quarter.',
|
|
1045
|
+
inputSchema: {
|
|
1046
|
+
type: 'object',
|
|
1047
|
+
properties: {
|
|
1048
|
+
user_id: { type: 'string', description: 'Target user\'s open_id (or the matching user_id_type)' },
|
|
1049
|
+
user_id_type: { type: 'string', enum: ['user_id', 'union_id', 'open_id', 'people_admin_id'], description: 'Type of user_id (default: open_id)' },
|
|
1050
|
+
period_ids: { type: 'array', items: { type: 'string' }, description: 'Filter by OKR period IDs (optional). Get period IDs via list_okr_periods.' },
|
|
1051
|
+
offset: { type: 'number', description: 'Pagination offset (default 0)' },
|
|
1052
|
+
limit: { type: 'number', description: 'Items per page (default 10, max 10)' },
|
|
1053
|
+
lang: { type: 'string', description: 'Response language (optional, e.g. "zh_cn", "en_us")' },
|
|
1054
|
+
},
|
|
1055
|
+
required: ['user_id'],
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
name: 'get_okrs',
|
|
1060
|
+
description: '[Official API + UAT] Batch-fetch full OKR details (objectives, key results, progress, alignments) by OKR IDs.',
|
|
1061
|
+
inputSchema: {
|
|
1062
|
+
type: 'object',
|
|
1063
|
+
properties: {
|
|
1064
|
+
okr_ids: { type: 'array', items: { type: 'string' }, description: 'OKR IDs (max 10 per call). From list_user_okrs.' },
|
|
1065
|
+
user_id_type: { type: 'string', enum: ['user_id', 'union_id', 'open_id', 'people_admin_id'], description: 'Type of user_ids in response (default: open_id)' },
|
|
1066
|
+
lang: { type: 'string', description: 'Response language (optional)' },
|
|
1067
|
+
},
|
|
1068
|
+
required: ['okr_ids'],
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
name: 'list_okr_periods',
|
|
1073
|
+
description: '[Official API + UAT] List OKR periods (quarters / years) defined in the tenant. Use period_ids from this to filter list_user_okrs.',
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
type: 'object',
|
|
1076
|
+
properties: {
|
|
1077
|
+
page_size: { type: 'number', description: 'Items per page (default 10)' },
|
|
1078
|
+
page_token: { type: 'string', description: 'Pagination token' },
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
|
|
1083
|
+
// ========== Calendar — Official API (v1.3.4) ==========
|
|
1084
|
+
{
|
|
1085
|
+
name: 'list_calendars',
|
|
1086
|
+
description: '[Official API + UAT] List the current user\'s calendars (primary + shared + subscribed). Requires UAT — app identity only sees calendars it was explicitly invited to. Requires `calendar:calendar:readonly` scope on the OAuth.',
|
|
1087
|
+
inputSchema: {
|
|
1088
|
+
type: 'object',
|
|
1089
|
+
properties: {
|
|
1090
|
+
page_size: { type: 'number', description: 'Items per page (min 50, default 50). Feishu\'s calendar endpoint rejects page_size < 50.' },
|
|
1091
|
+
page_token: { type: 'string', description: 'Pagination token' },
|
|
1092
|
+
sync_token: { type: 'string', description: 'Incremental sync token (optional)' },
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
name: 'list_calendar_events',
|
|
1098
|
+
description: '[Official API + UAT] List events in a calendar within an optional time range. Typical usage: first list_calendars to find calendar_id (primary calendar has type="primary"), then list events in e.g. [now, now+7d] (Unix seconds).',
|
|
1099
|
+
inputSchema: {
|
|
1100
|
+
type: 'object',
|
|
1101
|
+
properties: {
|
|
1102
|
+
calendar_id: { type: 'string', description: 'Calendar ID from list_calendars' },
|
|
1103
|
+
start_time: { type: 'string', description: 'Range start (Unix seconds, optional)' },
|
|
1104
|
+
end_time: { type: 'string', description: 'Range end (Unix seconds, optional)' },
|
|
1105
|
+
page_size: { type: 'number', description: 'Items per page (default 50)' },
|
|
1106
|
+
page_token: { type: 'string', description: 'Pagination token' },
|
|
1107
|
+
sync_token: { type: 'string', description: 'Incremental sync token (optional)' },
|
|
1108
|
+
},
|
|
1109
|
+
required: ['calendar_id'],
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
name: 'get_calendar_event',
|
|
1114
|
+
description: '[Official API + UAT] Get full details of a single calendar event (summary, description, start/end, attendees, location, attachments, meeting link).',
|
|
1115
|
+
inputSchema: {
|
|
1116
|
+
type: 'object',
|
|
1117
|
+
properties: {
|
|
1118
|
+
calendar_id: { type: 'string', description: 'Calendar ID' },
|
|
1119
|
+
event_id: { type: 'string', description: 'Event ID from list_calendar_events' },
|
|
1120
|
+
},
|
|
1121
|
+
required: ['calendar_id', 'event_id'],
|
|
1016
1122
|
},
|
|
1017
1123
|
},
|
|
1018
1124
|
|
|
@@ -1040,6 +1146,13 @@ const text = (s) => ({ content: [{ type: 'text', text: s }] });
|
|
|
1040
1146
|
const json = (o) => text(JSON.stringify(o, null, 2));
|
|
1041
1147
|
const sendResult = (r, desc) => text(r.success ? desc : `Send failed (status: ${r.status})`);
|
|
1042
1148
|
|
|
1149
|
+
// Resolver helper: turn document_id / app_token / wiki node / Feishu URL into
|
|
1150
|
+
// a native token. No-op for already-native inputs. See src/resolver.js.
|
|
1151
|
+
async function resolveDocId(input) {
|
|
1152
|
+
if (!input) return input;
|
|
1153
|
+
return resolveToken(input, getOfficialClient());
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1043
1156
|
async function handleTool(name, args) {
|
|
1044
1157
|
|
|
1045
1158
|
switch (name) {
|
|
@@ -1226,32 +1339,21 @@ async function handleTool(name, args) {
|
|
|
1226
1339
|
// Get userClient for name resolution fallback (best-effort)
|
|
1227
1340
|
let uc = null;
|
|
1228
1341
|
try { uc = await getUserClient(); } catch (_) {}
|
|
1229
|
-
const resolvedChatId = await chatIdMapper.resolveToOcId(args.chat_id, official);
|
|
1230
1342
|
|
|
1231
|
-
//
|
|
1343
|
+
// Path A — chat_id that resolves inside bot's / official search scope.
|
|
1344
|
+
const resolvedChatId = await chatIdMapper.resolveToOcId(args.chat_id, official);
|
|
1232
1345
|
if (resolvedChatId) {
|
|
1233
|
-
|
|
1234
|
-
return json(await official.readMessages(resolvedChatId, msgOpts, uc));
|
|
1235
|
-
} catch (botErr) {
|
|
1236
|
-
// Bot API failed (e.g. bot not in group, no permission) — fall through to UAT
|
|
1237
|
-
console.error(`[feishu-user-plugin] read_messages bot API failed for ${resolvedChatId}: ${botErr.message}`);
|
|
1238
|
-
if (official.hasUAT) {
|
|
1239
|
-
try {
|
|
1240
|
-
return json(await official.readMessagesAsUser(resolvedChatId, msgOpts, uc));
|
|
1241
|
-
} catch (uatErr) {
|
|
1242
|
-
console.error(`[feishu-user-plugin] read_messages UAT fallback also failed for ${resolvedChatId}: ${uatErr.message}`);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
throw botErr; // Re-throw original error if UAT also failed
|
|
1246
|
-
}
|
|
1346
|
+
return json(await official.readMessagesWithFallback(resolvedChatId, msgOpts, uc));
|
|
1247
1347
|
}
|
|
1248
1348
|
|
|
1249
|
-
//
|
|
1349
|
+
// Path B — external group discovered only via cookie search_contacts.
|
|
1350
|
+
// When we got here the bot definitely can't see it, so skip bot entirely
|
|
1351
|
+
// and go straight to UAT with a `contacts` via label.
|
|
1250
1352
|
if (official.hasUAT) {
|
|
1251
1353
|
if (!uc) try { uc = await getUserClient(); } catch (_) {}
|
|
1252
1354
|
const contactChatId = await chatIdMapper.resolveViaContacts(args.chat_id, uc);
|
|
1253
1355
|
if (contactChatId) {
|
|
1254
|
-
return json(await official.
|
|
1356
|
+
return json(await official.readMessagesWithFallback(contactChatId, msgOpts, uc, { skipBot: true, via: 'contacts' }));
|
|
1255
1357
|
}
|
|
1256
1358
|
}
|
|
1257
1359
|
|
|
@@ -1267,56 +1369,70 @@ async function handleTool(name, args) {
|
|
|
1267
1369
|
case 'search_docs':
|
|
1268
1370
|
return json(await getOfficialClient().searchDocs(args.query));
|
|
1269
1371
|
case 'read_doc':
|
|
1270
|
-
return json(await getOfficialClient().readDoc(args.document_id));
|
|
1372
|
+
return json(await getOfficialClient().readDoc(await resolveDocId(args.document_id)));
|
|
1271
1373
|
case 'get_doc_blocks':
|
|
1272
|
-
return json(await getOfficialClient().getDocBlocks(args.document_id));
|
|
1374
|
+
return json(await getOfficialClient().getDocBlocks(await resolveDocId(args.document_id)));
|
|
1273
1375
|
case 'create_doc': {
|
|
1274
|
-
const r = await getOfficialClient().createDoc(args.title, args.folder_id
|
|
1376
|
+
const r = await getOfficialClient().createDoc(args.title, args.folder_id, {
|
|
1377
|
+
wikiSpaceId: args.wiki_space_id,
|
|
1378
|
+
wikiParentNodeToken: args.wiki_parent_node_token,
|
|
1379
|
+
});
|
|
1275
1380
|
const ownership = r.viaUser ? ' (as user)' : ' (as app — UAT unavailable or failed; document owned by the app, not you)';
|
|
1276
|
-
|
|
1381
|
+
const wikiNote = r.wikiNodeToken ? ` [wiki node: ${r.wikiNodeToken}]`
|
|
1382
|
+
: r.wikiAttachTaskId ? ` [wiki attach queued — task_id: ${r.wikiAttachTaskId}]`
|
|
1383
|
+
: r.wikiAttachError ? ` [WARNING: wiki attach failed — ${r.wikiAttachError}. Doc exists in drive root/folder.]`
|
|
1384
|
+
: '';
|
|
1385
|
+
return text(`Document created${ownership}: ${r.documentId}${wikiNote}`);
|
|
1277
1386
|
}
|
|
1278
1387
|
|
|
1279
1388
|
// --- Official API: Bitable ---
|
|
1280
1389
|
|
|
1281
1390
|
case 'create_bitable': {
|
|
1282
|
-
const r = await getOfficialClient().createBitable(args.name, args.folder_id
|
|
1391
|
+
const r = await getOfficialClient().createBitable(args.name, args.folder_id, {
|
|
1392
|
+
wikiSpaceId: args.wiki_space_id,
|
|
1393
|
+
wikiParentNodeToken: args.wiki_parent_node_token,
|
|
1394
|
+
});
|
|
1283
1395
|
const ownership = r.viaUser ? ' (as user)' : ' (as app — UAT unavailable or failed; bitable owned by the app, not you)';
|
|
1284
|
-
|
|
1396
|
+
const wikiNote = r.wikiNodeToken ? `\nWiki node: ${r.wikiNodeToken}`
|
|
1397
|
+
: r.wikiAttachTaskId ? `\nWiki attach queued — task_id: ${r.wikiAttachTaskId}`
|
|
1398
|
+
: r.wikiAttachError ? `\nWARNING: wiki attach failed — ${r.wikiAttachError}. Bitable exists in drive root/folder.`
|
|
1399
|
+
: '';
|
|
1400
|
+
return text(`Bitable created${ownership}: ${r.appToken}\nURL: ${r.url || ''}${wikiNote}`);
|
|
1285
1401
|
}
|
|
1286
1402
|
case 'list_bitable_tables':
|
|
1287
|
-
return json(await getOfficialClient().listBitableTables(args.app_token));
|
|
1403
|
+
return json(await getOfficialClient().listBitableTables(await resolveDocId(args.app_token)));
|
|
1288
1404
|
case 'create_bitable_table':
|
|
1289
|
-
return text(`Table created: ${(await getOfficialClient().createBitableTable(args.app_token, args.name, args.fields)).tableId}`);
|
|
1405
|
+
return text(`Table created: ${(await getOfficialClient().createBitableTable(await resolveDocId(args.app_token), args.name, args.fields)).tableId}`);
|
|
1290
1406
|
case 'list_bitable_fields':
|
|
1291
|
-
return json(await getOfficialClient().listBitableFields(args.app_token, args.table_id));
|
|
1407
|
+
return json(await getOfficialClient().listBitableFields(await resolveDocId(args.app_token), args.table_id));
|
|
1292
1408
|
case 'create_bitable_field': {
|
|
1293
1409
|
const config = { field_name: args.field_name, type: args.type };
|
|
1294
1410
|
if (args.property) config.property = args.property;
|
|
1295
|
-
return json(await getOfficialClient().createBitableField(args.app_token, args.table_id, config));
|
|
1411
|
+
return json(await getOfficialClient().createBitableField(await resolveDocId(args.app_token), args.table_id, config));
|
|
1296
1412
|
}
|
|
1297
1413
|
case 'update_bitable_field': {
|
|
1298
1414
|
const config = {};
|
|
1299
1415
|
if (args.field_name) config.field_name = args.field_name;
|
|
1300
1416
|
if (args.type) config.type = args.type;
|
|
1301
1417
|
if (args.property) config.property = args.property;
|
|
1302
|
-
return json(await getOfficialClient().updateBitableField(args.app_token, args.table_id, args.field_id, config));
|
|
1418
|
+
return json(await getOfficialClient().updateBitableField(await resolveDocId(args.app_token), args.table_id, args.field_id, config));
|
|
1303
1419
|
}
|
|
1304
1420
|
case 'delete_bitable_field': {
|
|
1305
|
-
const r = await getOfficialClient().deleteBitableField(args.app_token, args.table_id, args.field_id);
|
|
1421
|
+
const r = await getOfficialClient().deleteBitableField(await resolveDocId(args.app_token), args.table_id, args.field_id);
|
|
1306
1422
|
return text(r.deleted ? `Field ${r.fieldId} deleted` : `Field deletion returned deleted=${r.deleted}`);
|
|
1307
1423
|
}
|
|
1308
1424
|
case 'list_bitable_views':
|
|
1309
|
-
return json(await getOfficialClient().listBitableViews(args.app_token, args.table_id));
|
|
1425
|
+
return json(await getOfficialClient().listBitableViews(await resolveDocId(args.app_token), args.table_id));
|
|
1310
1426
|
case 'search_bitable_records':
|
|
1311
|
-
return json(await getOfficialClient().searchBitableRecords(args.app_token, args.table_id, {
|
|
1427
|
+
return json(await getOfficialClient().searchBitableRecords(await resolveDocId(args.app_token), args.table_id, {
|
|
1312
1428
|
filter: args.filter, sort: args.sort, pageSize: args.page_size,
|
|
1313
1429
|
}));
|
|
1314
1430
|
case 'batch_create_bitable_records':
|
|
1315
|
-
return json(await getOfficialClient().batchCreateBitableRecords(args.app_token, args.table_id, args.records));
|
|
1431
|
+
return json(await getOfficialClient().batchCreateBitableRecords(await resolveDocId(args.app_token), args.table_id, args.records));
|
|
1316
1432
|
case 'batch_update_bitable_records':
|
|
1317
|
-
return json(await getOfficialClient().batchUpdateBitableRecords(args.app_token, args.table_id, args.records));
|
|
1433
|
+
return json(await getOfficialClient().batchUpdateBitableRecords(await resolveDocId(args.app_token), args.table_id, args.records));
|
|
1318
1434
|
case 'batch_delete_bitable_records':
|
|
1319
|
-
return json(await getOfficialClient().batchDeleteBitableRecords(args.app_token, args.table_id, args.record_ids));
|
|
1435
|
+
return json(await getOfficialClient().batchDeleteBitableRecords(await resolveDocId(args.app_token), args.table_id, args.record_ids));
|
|
1320
1436
|
|
|
1321
1437
|
// --- Official API: Wiki ---
|
|
1322
1438
|
|
|
@@ -1394,29 +1510,54 @@ async function handleTool(name, args) {
|
|
|
1394
1510
|
|
|
1395
1511
|
// --- Official API: Doc Block Editing ---
|
|
1396
1512
|
|
|
1397
|
-
case 'create_doc_block':
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1513
|
+
case 'create_doc_block': {
|
|
1514
|
+
const official = getOfficialClient();
|
|
1515
|
+
const docId = await resolveDocId(args.document_id);
|
|
1516
|
+
// Image shortcut: if image_path or image_token is provided, orchestrate the
|
|
1517
|
+
// 3-step docx image creation. Mutually exclusive with children.
|
|
1518
|
+
if (args.image_path || args.image_token) {
|
|
1519
|
+
if (args.children) return text('create_doc_block: pass children OR image_path OR image_token, not both.');
|
|
1520
|
+
const r = await official.createDocBlockWithImage(docId, args.parent_block_id, {
|
|
1521
|
+
imagePath: args.image_path,
|
|
1522
|
+
imageToken: args.image_token,
|
|
1523
|
+
index: args.index,
|
|
1524
|
+
});
|
|
1525
|
+
return json(r);
|
|
1526
|
+
}
|
|
1527
|
+
if (!args.children) return text('create_doc_block: children (generic blocks), image_path, or image_token is required.');
|
|
1528
|
+
return json(await official.createDocBlock(docId, args.parent_block_id, args.children, args.index));
|
|
1529
|
+
}
|
|
1530
|
+
case 'update_doc_block': {
|
|
1531
|
+
const official = getOfficialClient();
|
|
1532
|
+
const docId = await resolveDocId(args.document_id);
|
|
1533
|
+
if (args.image_token && args.update_body) {
|
|
1534
|
+
return text('update_doc_block: pass image_token OR update_body, not both.');
|
|
1535
|
+
}
|
|
1536
|
+
if (args.image_token) {
|
|
1537
|
+
return json(await official.updateDocBlockImage(docId, args.block_id, args.image_token));
|
|
1538
|
+
}
|
|
1539
|
+
if (!args.update_body) return text('update_doc_block: update_body or image_token is required.');
|
|
1540
|
+
return json(await official.updateDocBlock(docId, args.block_id, args.update_body));
|
|
1541
|
+
}
|
|
1401
1542
|
case 'delete_doc_blocks':
|
|
1402
|
-
return text(`Blocks deleted: ${(await getOfficialClient().deleteDocBlocks(args.document_id, args.parent_block_id, args.start_index, args.end_index)).deleted}`);
|
|
1543
|
+
return text(`Blocks deleted: ${(await getOfficialClient().deleteDocBlocks(await resolveDocId(args.document_id), args.parent_block_id, args.start_index, args.end_index)).deleted}`);
|
|
1403
1544
|
|
|
1404
1545
|
// --- Official API: Bitable Additional ---
|
|
1405
1546
|
|
|
1406
1547
|
case 'get_bitable_record':
|
|
1407
|
-
return json(await getOfficialClient().getBitableRecord(args.app_token, args.table_id, args.record_id));
|
|
1548
|
+
return json(await getOfficialClient().getBitableRecord(await resolveDocId(args.app_token), args.table_id, args.record_id));
|
|
1408
1549
|
case 'delete_bitable_table':
|
|
1409
|
-
return text(`Table deleted: ${(await getOfficialClient().deleteBitableTable(args.app_token, args.table_id)).deleted}`);
|
|
1550
|
+
return text(`Table deleted: ${(await getOfficialClient().deleteBitableTable(await resolveDocId(args.app_token), args.table_id)).deleted}`);
|
|
1410
1551
|
case 'get_bitable_meta':
|
|
1411
|
-
return json(await getOfficialClient().getBitableMeta(args.app_token));
|
|
1552
|
+
return json(await getOfficialClient().getBitableMeta(await resolveDocId(args.app_token)));
|
|
1412
1553
|
case 'update_bitable_table':
|
|
1413
|
-
return text(`Table renamed: ${(await getOfficialClient().updateBitableTable(args.app_token, args.table_id, args.name)).name}`);
|
|
1554
|
+
return text(`Table renamed: ${(await getOfficialClient().updateBitableTable(await resolveDocId(args.app_token), args.table_id, args.name)).name}`);
|
|
1414
1555
|
case 'create_bitable_view':
|
|
1415
|
-
return json(await getOfficialClient().createBitableView(args.app_token, args.table_id, args.view_name, args.view_type));
|
|
1556
|
+
return json(await getOfficialClient().createBitableView(await resolveDocId(args.app_token), args.table_id, args.view_name, args.view_type));
|
|
1416
1557
|
case 'delete_bitable_view':
|
|
1417
|
-
return text(`View deleted: ${(await getOfficialClient().deleteBitableView(args.app_token, args.table_id, args.view_id)).deleted}`);
|
|
1558
|
+
return text(`View deleted: ${(await getOfficialClient().deleteBitableView(await resolveDocId(args.app_token), args.table_id, args.view_id)).deleted}`);
|
|
1418
1559
|
case 'copy_bitable':
|
|
1419
|
-
return json(await getOfficialClient().copyBitable(args.app_token, args.name, args.folder_id));
|
|
1560
|
+
return json(await getOfficialClient().copyBitable(await resolveDocId(args.app_token), args.name, args.folder_id));
|
|
1420
1561
|
|
|
1421
1562
|
// --- Official API: Drive File Operations ---
|
|
1422
1563
|
|
|
@@ -1428,17 +1569,59 @@ async function handleTool(name, args) {
|
|
|
1428
1569
|
return text(`File deleted: task=${(await getOfficialClient().deleteFile(args.file_token, args.type)).taskId}`);
|
|
1429
1570
|
|
|
1430
1571
|
case 'download_image': {
|
|
1431
|
-
const
|
|
1572
|
+
const official = getOfficialClient();
|
|
1573
|
+
let r;
|
|
1574
|
+
let source;
|
|
1575
|
+
if (args.image_token) {
|
|
1576
|
+
// Docx image mode — doc_token may be a URL / wiki node; resolve it.
|
|
1577
|
+
const docToken = args.doc_token ? await resolveDocId(args.doc_token) : undefined;
|
|
1578
|
+
r = await official.downloadDocImage(args.image_token, docToken);
|
|
1579
|
+
source = docToken ? `docx ${docToken}` : 'drive media';
|
|
1580
|
+
} else if (args.message_id && args.image_key) {
|
|
1581
|
+
r = await official.downloadMessageResource(args.message_id, args.image_key, 'image');
|
|
1582
|
+
source = `message ${args.message_id}`;
|
|
1583
|
+
} else {
|
|
1584
|
+
return text('download_image requires either (message_id + image_key) for chat images, or (image_token, optionally with doc_token) for docx images.');
|
|
1585
|
+
}
|
|
1432
1586
|
// Return as MCP image content so the model sees the pixels directly.
|
|
1433
|
-
// Also include a tiny text preamble so the via-identity + size are visible in transcript.
|
|
1434
1587
|
return {
|
|
1435
1588
|
content: [
|
|
1436
|
-
{ type: 'text', text: `Image downloaded (${r.viaUser ? 'as user' : 'as app'}, ${r.bytes} bytes, ${r.mimeType}):` },
|
|
1589
|
+
{ type: 'text', text: `Image downloaded from ${source} (${r.viaUser ? 'as user' : 'as app'}, ${r.bytes} bytes, ${r.mimeType}):` },
|
|
1437
1590
|
{ type: 'image', data: r.base64, mimeType: r.mimeType },
|
|
1438
1591
|
],
|
|
1439
1592
|
};
|
|
1440
1593
|
}
|
|
1441
1594
|
|
|
1595
|
+
// --- Wiki Node Resolution (v1.3.4) ---
|
|
1596
|
+
case 'get_wiki_node': {
|
|
1597
|
+
// Accept either a bare wiki node token or a full /wiki/ URL — parse first.
|
|
1598
|
+
const parsed = parseFeishuInput(args.node_token);
|
|
1599
|
+
const token = (parsed.kind === 'wiki' || parsed.kind === 'raw') ? parsed.token : args.node_token;
|
|
1600
|
+
return json(await getOfficialClient().getWikiNode(token));
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// --- OKR (v1.3.4) ---
|
|
1604
|
+
case 'list_user_okrs':
|
|
1605
|
+
return json(await getOfficialClient().listUserOkrs(args.user_id, {
|
|
1606
|
+
periodIds: args.period_ids, offset: args.offset, limit: args.limit, lang: args.lang,
|
|
1607
|
+
userIdType: args.user_id_type,
|
|
1608
|
+
}));
|
|
1609
|
+
case 'get_okrs':
|
|
1610
|
+
return json(await getOfficialClient().getOkrs(args.okr_ids, { lang: args.lang, userIdType: args.user_id_type }));
|
|
1611
|
+
case 'list_okr_periods':
|
|
1612
|
+
return json(await getOfficialClient().listOkrPeriods({ pageSize: args.page_size, pageToken: args.page_token }));
|
|
1613
|
+
|
|
1614
|
+
// --- Calendar (v1.3.4) ---
|
|
1615
|
+
case 'list_calendars':
|
|
1616
|
+
return json(await getOfficialClient().listCalendars({ pageSize: args.page_size, pageToken: args.page_token, syncToken: args.sync_token }));
|
|
1617
|
+
case 'list_calendar_events':
|
|
1618
|
+
return json(await getOfficialClient().listCalendarEvents(args.calendar_id, {
|
|
1619
|
+
startTime: args.start_time, endTime: args.end_time,
|
|
1620
|
+
pageSize: args.page_size, pageToken: args.page_token, syncToken: args.sync_token,
|
|
1621
|
+
}));
|
|
1622
|
+
case 'get_calendar_event':
|
|
1623
|
+
return json(await getOfficialClient().getCalendarEvent(args.calendar_id, args.event_id));
|
|
1624
|
+
|
|
1442
1625
|
default:
|
|
1443
1626
|
return text(`Unknown tool: ${name}`);
|
|
1444
1627
|
}
|
package/src/oauth.js
CHANGED
|
@@ -23,7 +23,12 @@ const PORT = 9997;
|
|
|
23
23
|
const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`;
|
|
24
24
|
// offline_access is required to get refresh_token for auto-renewal
|
|
25
25
|
// Write scopes (docx:document, drive:drive, bitable:app) allow creating resources as the user, not the app
|
|
26
|
-
|
|
26
|
+
// v1.3.4 additions:
|
|
27
|
+
// okr:* for list_user_okrs / get_okrs / list_okr_periods
|
|
28
|
+
// calendar:* for list_calendars / list_calendar_events / get_calendar_event
|
|
29
|
+
// wiki:wiki write access for move_docs_to_wiki (attach docs/bitables to wiki)
|
|
30
|
+
// docs:document.media:(upload|download) for docx image read/write
|
|
31
|
+
const SCOPES = 'offline_access auth:user.id:read im:message im:message:readonly im:chat im:chat:readonly contact:user.base:readonly contact:user.id:readonly docx:document drive:drive bitable:app wiki:wiki:readonly wiki:wiki okr:okr:readonly okr:okr.period:readonly okr:okr.content:readonly calendar:calendar:readonly calendar:calendar.event:read docs:document.media:download docs:document.media:upload';
|
|
27
32
|
|
|
28
33
|
if (!APP_ID || !APP_SECRET) {
|
|
29
34
|
console.error('Missing LARK_APP_ID or LARK_APP_SECRET.');
|