feishu-user-plugin 1.2.1 → 1.3.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/README.md +103 -39
- package/package.json +5 -3
- package/scripts/confirm-version.js +28 -0
- package/scripts/mcp_stdio_bridge.js +97 -0
- package/skills/feishu-user-plugin/references/CLAUDE.md +338 -61
- package/src/cli.js +12 -7
- package/src/config.js +202 -27
- package/src/index.js +429 -55
- package/src/oauth.js +2 -1
- package/src/official.js +313 -1
- package/src/setup.js +19 -3
package/src/official.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
const lark = require('@larksuiteoapi/node-sdk');
|
|
2
2
|
|
|
3
|
+
// Redirect all Lark SDK logs to stderr.
|
|
4
|
+
// The SDK's defaultLogger.error uses console.log (stdout), which corrupts
|
|
5
|
+
// MCP's JSON-RPC stdio transport and causes session disconnects.
|
|
6
|
+
const stderrLogger = {
|
|
7
|
+
error: (...msg) => console.error('[lark-sdk][error]:', ...msg),
|
|
8
|
+
warn: (...msg) => console.error('[lark-sdk][warn]:', ...msg),
|
|
9
|
+
info: () => {},
|
|
10
|
+
debug: () => {},
|
|
11
|
+
trace: () => {},
|
|
12
|
+
};
|
|
13
|
+
|
|
3
14
|
class LarkOfficialClient {
|
|
4
15
|
constructor(appId, appSecret) {
|
|
5
16
|
this.appId = appId;
|
|
6
17
|
this.appSecret = appSecret;
|
|
7
|
-
this.client = new lark.Client({ appId, appSecret, disableTokenCache: false });
|
|
18
|
+
this.client = new lark.Client({ appId, appSecret, disableTokenCache: false, logger: stderrLogger, loggerLevel: lark.LoggerLevel.warn });
|
|
8
19
|
this._uat = null;
|
|
9
20
|
this._uatRefresh = null;
|
|
10
21
|
this._uatExpires = 0;
|
|
@@ -177,6 +188,139 @@ class LarkOfficialClient {
|
|
|
177
188
|
return { messageId: res.data.message_id };
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
// --- IM: Send (Bot Identity) ---
|
|
192
|
+
|
|
193
|
+
async sendMessageAsBot(chatId, msgType, content, receiveIdType = 'chat_id') {
|
|
194
|
+
const res = await this._safeSDKCall(
|
|
195
|
+
() => this.client.im.message.create({
|
|
196
|
+
params: { receive_id_type: receiveIdType },
|
|
197
|
+
data: { receive_id: chatId, msg_type: msgType, content: typeof content === 'string' ? content : JSON.stringify(content) },
|
|
198
|
+
}),
|
|
199
|
+
'sendMessage'
|
|
200
|
+
);
|
|
201
|
+
return { messageId: res.data.message_id };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async deleteMessage(messageId) {
|
|
205
|
+
await this._safeSDKCall(
|
|
206
|
+
() => this.client.im.message.delete({ path: { message_id: messageId } }),
|
|
207
|
+
'deleteMessage'
|
|
208
|
+
);
|
|
209
|
+
return { deleted: true };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async updateMessage(messageId, msgType, content) {
|
|
213
|
+
const res = await this._safeSDKCall(
|
|
214
|
+
() => this.client.im.message.patch({
|
|
215
|
+
path: { message_id: messageId },
|
|
216
|
+
data: { msg_type: msgType, content: typeof content === 'string' ? content : JSON.stringify(content) },
|
|
217
|
+
}),
|
|
218
|
+
'updateMessage'
|
|
219
|
+
);
|
|
220
|
+
return { messageId: res.data?.message_id || messageId };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- IM: Reactions ---
|
|
224
|
+
|
|
225
|
+
async addReaction(messageId, emojiType) {
|
|
226
|
+
const res = await this._safeSDKCall(
|
|
227
|
+
() => this.client.im.messageReaction.create({
|
|
228
|
+
path: { message_id: messageId },
|
|
229
|
+
data: { reaction_type: { emoji_type: emojiType } },
|
|
230
|
+
}),
|
|
231
|
+
'addReaction'
|
|
232
|
+
);
|
|
233
|
+
return { reactionId: res.data.reaction_id };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async deleteReaction(messageId, reactionId) {
|
|
237
|
+
await this._safeSDKCall(
|
|
238
|
+
() => this.client.im.messageReaction.delete({
|
|
239
|
+
path: { message_id: messageId, reaction_id: reactionId },
|
|
240
|
+
}),
|
|
241
|
+
'deleteReaction'
|
|
242
|
+
);
|
|
243
|
+
return { deleted: true };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- IM: Pins ---
|
|
247
|
+
|
|
248
|
+
async pinMessage(messageId, pinned = true) {
|
|
249
|
+
if (pinned) {
|
|
250
|
+
const res = await this._safeSDKCall(
|
|
251
|
+
() => this.client.im.pin.create({ data: { message_id: messageId } }),
|
|
252
|
+
'pinMessage'
|
|
253
|
+
);
|
|
254
|
+
return { pin: res.data.pin };
|
|
255
|
+
}
|
|
256
|
+
await this._safeSDKCall(
|
|
257
|
+
() => this.client.im.pin.delete({ data: { message_id: messageId } }),
|
|
258
|
+
'unpinMessage'
|
|
259
|
+
);
|
|
260
|
+
return { unpinned: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- IM: Chat Management ---
|
|
264
|
+
|
|
265
|
+
async createChat({ name, description, userIds, botIds } = {}) {
|
|
266
|
+
const data = {};
|
|
267
|
+
if (name) data.name = name;
|
|
268
|
+
if (description) data.description = description;
|
|
269
|
+
if (userIds) data.user_id_list = userIds;
|
|
270
|
+
if (botIds) data.bot_id_list = botIds;
|
|
271
|
+
const res = await this._safeSDKCall(
|
|
272
|
+
() => this.client.im.chat.create({ params: { user_id_type: 'open_id' }, data }),
|
|
273
|
+
'createChat'
|
|
274
|
+
);
|
|
275
|
+
return { chatId: res.data.chat_id };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async updateChat(chatId, { name, description } = {}) {
|
|
279
|
+
const data = {};
|
|
280
|
+
if (name) data.name = name;
|
|
281
|
+
if (description) data.description = description;
|
|
282
|
+
const res = await this._safeSDKCall(
|
|
283
|
+
() => this.client.im.chat.update({ path: { chat_id: chatId }, data }),
|
|
284
|
+
'updateChat'
|
|
285
|
+
);
|
|
286
|
+
return { updated: true };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async listChatMembers(chatId, { pageSize = 50, pageToken } = {}) {
|
|
290
|
+
const res = await this._safeSDKCall(
|
|
291
|
+
() => this.client.im.chatMembers.get({
|
|
292
|
+
path: { chat_id: chatId },
|
|
293
|
+
params: { member_id_type: 'open_id', page_size: pageSize, page_token: pageToken },
|
|
294
|
+
}),
|
|
295
|
+
'listChatMembers'
|
|
296
|
+
);
|
|
297
|
+
return { items: res.data.items || [], hasMore: res.data.has_more, pageToken: res.data.page_token };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async addChatMembers(chatId, userIds) {
|
|
301
|
+
const res = await this._safeSDKCall(
|
|
302
|
+
() => this.client.im.chatMembers.create({
|
|
303
|
+
path: { chat_id: chatId },
|
|
304
|
+
params: { member_id_type: 'open_id' },
|
|
305
|
+
data: { id_list: userIds },
|
|
306
|
+
}),
|
|
307
|
+
'addChatMembers'
|
|
308
|
+
);
|
|
309
|
+
return { invalidIds: res.data.invalid_id_list || [] };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async removeChatMembers(chatId, userIds) {
|
|
313
|
+
const res = await this._safeSDKCall(
|
|
314
|
+
() => this.client.im.chatMembers.delete({
|
|
315
|
+
path: { chat_id: chatId },
|
|
316
|
+
params: { member_id_type: 'open_id' },
|
|
317
|
+
data: { id_list: userIds },
|
|
318
|
+
}),
|
|
319
|
+
'removeChatMembers'
|
|
320
|
+
);
|
|
321
|
+
return { invalidIds: res.data.invalid_id_list || [] };
|
|
322
|
+
}
|
|
323
|
+
|
|
180
324
|
// --- Upload ---
|
|
181
325
|
|
|
182
326
|
async uploadImage(imagePath, imageType = 'message') {
|
|
@@ -250,6 +394,41 @@ class LarkOfficialClient {
|
|
|
250
394
|
return { items: res.data.items || [] };
|
|
251
395
|
}
|
|
252
396
|
|
|
397
|
+
async createDocBlock(documentId, parentBlockId, children, index) {
|
|
398
|
+
const data = { children };
|
|
399
|
+
if (index !== undefined) data.index = index;
|
|
400
|
+
const res = await this._safeSDKCall(
|
|
401
|
+
() => this.client.docx.documentBlockChildren.create({
|
|
402
|
+
path: { document_id: documentId, block_id: parentBlockId },
|
|
403
|
+
data,
|
|
404
|
+
}),
|
|
405
|
+
'createDocBlock'
|
|
406
|
+
);
|
|
407
|
+
return { blocks: res.data.children || [] };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async updateDocBlock(documentId, blockId, updateBody) {
|
|
411
|
+
const res = await this._safeSDKCall(
|
|
412
|
+
() => this.client.docx.documentBlock.patch({
|
|
413
|
+
path: { document_id: documentId, block_id: blockId },
|
|
414
|
+
data: updateBody,
|
|
415
|
+
}),
|
|
416
|
+
'updateDocBlock'
|
|
417
|
+
);
|
|
418
|
+
return { block: res.data.block };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async deleteDocBlocks(documentId, parentBlockId, startIndex, endIndex) {
|
|
422
|
+
const res = await this._safeSDKCall(
|
|
423
|
+
() => this.client.docx.documentBlockChildren.batchDelete({
|
|
424
|
+
path: { document_id: documentId, block_id: parentBlockId },
|
|
425
|
+
data: { start_index: startIndex, end_index: endIndex },
|
|
426
|
+
}),
|
|
427
|
+
'deleteDocBlocks'
|
|
428
|
+
);
|
|
429
|
+
return { deleted: true };
|
|
430
|
+
}
|
|
431
|
+
|
|
253
432
|
// --- Chat Info (Official API) ---
|
|
254
433
|
|
|
255
434
|
async getChatInfo(chatId) {
|
|
@@ -387,6 +566,64 @@ class LarkOfficialClient {
|
|
|
387
566
|
return { items: res.data.items || [] };
|
|
388
567
|
}
|
|
389
568
|
|
|
569
|
+
async getBitableRecord(appToken, tableId, recordId) {
|
|
570
|
+
const res = await this._safeSDKCall(
|
|
571
|
+
() => this.client.bitable.appTableRecord.get({ path: { app_token: appToken, table_id: tableId, record_id: recordId } }),
|
|
572
|
+
'getRecord'
|
|
573
|
+
);
|
|
574
|
+
return { record: res.data.record };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async deleteBitableTable(appToken, tableId) {
|
|
578
|
+
await this._safeSDKCall(
|
|
579
|
+
() => this.client.bitable.appTable.delete({ path: { app_token: appToken, table_id: tableId } }),
|
|
580
|
+
'deleteTable'
|
|
581
|
+
);
|
|
582
|
+
return { deleted: true };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async getBitableMeta(appToken) {
|
|
586
|
+
const res = await this._safeSDKCall(
|
|
587
|
+
() => this.client.bitable.app.get({ path: { app_token: appToken } }),
|
|
588
|
+
'getBitableMeta'
|
|
589
|
+
);
|
|
590
|
+
return { app: res.data.app };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async updateBitableTable(appToken, tableId, name) {
|
|
594
|
+
const res = await this._safeSDKCall(
|
|
595
|
+
() => this.client.bitable.appTable.patch({ path: { app_token: appToken, table_id: tableId }, data: { name } }),
|
|
596
|
+
'updateTable'
|
|
597
|
+
);
|
|
598
|
+
return { name: res.data.name };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async createBitableView(appToken, tableId, viewName, viewType = 'grid') {
|
|
602
|
+
const res = await this._safeSDKCall(
|
|
603
|
+
() => this.client.bitable.appTableView.create({ path: { app_token: appToken, table_id: tableId }, data: { view_name: viewName, view_type: viewType } }),
|
|
604
|
+
'createView'
|
|
605
|
+
);
|
|
606
|
+
return { view: res.data.view };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async deleteBitableView(appToken, tableId, viewId) {
|
|
610
|
+
await this._safeSDKCall(
|
|
611
|
+
() => this.client.bitable.appTableView.delete({ path: { app_token: appToken, table_id: tableId, view_id: viewId } }),
|
|
612
|
+
'deleteView'
|
|
613
|
+
);
|
|
614
|
+
return { deleted: true };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async copyBitable(appToken, name, folderId) {
|
|
618
|
+
const data = { name };
|
|
619
|
+
if (folderId) data.folder_token = folderId;
|
|
620
|
+
const res = await this._safeSDKCall(
|
|
621
|
+
() => this.client.bitable.app.copy({ path: { app_token: appToken }, data }),
|
|
622
|
+
'copyBitable'
|
|
623
|
+
);
|
|
624
|
+
return { app: res.data.app };
|
|
625
|
+
}
|
|
626
|
+
|
|
390
627
|
// --- Wiki ---
|
|
391
628
|
|
|
392
629
|
async listWikiSpaces() {
|
|
@@ -435,6 +672,34 @@ class LarkOfficialClient {
|
|
|
435
672
|
return { token: res.data.token };
|
|
436
673
|
}
|
|
437
674
|
|
|
675
|
+
// --- Drive: File Operations ---
|
|
676
|
+
|
|
677
|
+
async copyFile(fileToken, name, folderToken, type) {
|
|
678
|
+
const data = { name, folder_token: folderToken || '' };
|
|
679
|
+
if (type) data.type = type;
|
|
680
|
+
const res = await this._safeSDKCall(
|
|
681
|
+
() => this.client.drive.file.copy({ path: { file_token: fileToken }, data }),
|
|
682
|
+
'copyFile'
|
|
683
|
+
);
|
|
684
|
+
return { file: res.data.file };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async moveFile(fileToken, folderToken) {
|
|
688
|
+
const res = await this._safeSDKCall(
|
|
689
|
+
() => this.client.drive.file.move({ path: { file_token: fileToken }, data: { folder_token: folderToken || '' } }),
|
|
690
|
+
'moveFile'
|
|
691
|
+
);
|
|
692
|
+
return { taskId: res.data.task_id };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async deleteFile(fileToken, type) {
|
|
696
|
+
const res = await this._safeSDKCall(
|
|
697
|
+
() => this.client.drive.file.delete({ path: { file_token: fileToken }, params: { type: type || 'file' } }),
|
|
698
|
+
'deleteFile'
|
|
699
|
+
);
|
|
700
|
+
return { taskId: res.data.task_id };
|
|
701
|
+
}
|
|
702
|
+
|
|
438
703
|
// --- Contact ---
|
|
439
704
|
|
|
440
705
|
async findUserByIdentity({ emails, mobiles } = {}) {
|
|
@@ -466,6 +731,53 @@ class LarkOfficialClient {
|
|
|
466
731
|
return allChats;
|
|
467
732
|
}
|
|
468
733
|
|
|
734
|
+
// --- UAT-based creation (resources owned by user, not app) ---
|
|
735
|
+
|
|
736
|
+
async createDocAsUser(title, folderId) {
|
|
737
|
+
const data = { title };
|
|
738
|
+
if (folderId) data.folder_token = folderId;
|
|
739
|
+
const result = await this._withUAT(async (uat) => {
|
|
740
|
+
const res = await fetch('https://open.feishu.cn/open-apis/docx/v1/documents', {
|
|
741
|
+
method: 'POST',
|
|
742
|
+
headers: { 'Authorization': `Bearer ${uat}`, 'content-type': 'application/json' },
|
|
743
|
+
body: JSON.stringify(data),
|
|
744
|
+
});
|
|
745
|
+
return res.json();
|
|
746
|
+
});
|
|
747
|
+
if (result.code !== 0) throw new Error(`createDocAsUser failed (${result.code}): ${result.msg}`);
|
|
748
|
+
return { documentId: result.data.document?.document_id };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async createBitableAsUser(name, folderId) {
|
|
752
|
+
const data = {};
|
|
753
|
+
if (name) data.name = name;
|
|
754
|
+
if (folderId) data.folder_token = folderId;
|
|
755
|
+
const result = await this._withUAT(async (uat) => {
|
|
756
|
+
const res = await fetch('https://open.feishu.cn/open-apis/bitable/v1/apps', {
|
|
757
|
+
method: 'POST',
|
|
758
|
+
headers: { 'Authorization': `Bearer ${uat}`, 'content-type': 'application/json' },
|
|
759
|
+
body: JSON.stringify(data),
|
|
760
|
+
});
|
|
761
|
+
return res.json();
|
|
762
|
+
});
|
|
763
|
+
if (result.code !== 0) throw new Error(`createBitableAsUser failed (${result.code}): ${result.msg}`);
|
|
764
|
+
return { appToken: result.data.app?.app_token, name: result.data.app?.name, url: result.data.app?.url };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async createFolderAsUser(name, parentToken) {
|
|
768
|
+
const data = { name, folder_token: parentToken || '' };
|
|
769
|
+
const result = await this._withUAT(async (uat) => {
|
|
770
|
+
const res = await fetch('https://open.feishu.cn/open-apis/drive/v1/files/create_folder', {
|
|
771
|
+
method: 'POST',
|
|
772
|
+
headers: { 'Authorization': `Bearer ${uat}`, 'content-type': 'application/json' },
|
|
773
|
+
body: JSON.stringify(data),
|
|
774
|
+
});
|
|
775
|
+
return res.json();
|
|
776
|
+
});
|
|
777
|
+
if (result.code !== 0) throw new Error(`createFolderAsUser failed (${result.code}): ${result.msg}`);
|
|
778
|
+
return { token: result.data.token };
|
|
779
|
+
}
|
|
780
|
+
|
|
469
781
|
// --- Safe SDK Call (extracts real Feishu error from AxiosError) ---
|
|
470
782
|
|
|
471
783
|
async _safeSDKCall(fn, label = 'API') {
|
package/src/setup.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const readline = require('readline');
|
|
13
13
|
const { findMcpConfig, writeNewConfig } = require('./config');
|
|
14
14
|
|
|
15
|
-
// Parse CLI args: --app-id, --app-secret, --cookie
|
|
15
|
+
// Parse CLI args: --app-id, --app-secret, --cookie, --client
|
|
16
16
|
function parseArgs() {
|
|
17
17
|
const args = {};
|
|
18
18
|
const argv = process.argv.slice(2);
|
|
@@ -20,6 +20,7 @@ function parseArgs() {
|
|
|
20
20
|
if (argv[i] === '--app-id' && argv[i + 1]) args.appId = argv[++i];
|
|
21
21
|
else if (argv[i] === '--app-secret' && argv[i + 1]) args.appSecret = argv[++i];
|
|
22
22
|
else if (argv[i] === '--cookie' && argv[i + 1]) args.cookie = argv[++i];
|
|
23
|
+
else if (argv[i] === '--client' && argv[i + 1]) args.client = argv[++i];
|
|
23
24
|
}
|
|
24
25
|
return args;
|
|
25
26
|
}
|
|
@@ -135,6 +136,20 @@ async function main() {
|
|
|
135
136
|
const existingRT = existingEnv.LARK_USER_REFRESH_TOKEN;
|
|
136
137
|
const hasUAT = existingUAT && existingUAT !== 'SETUP_NEEDED' && existingUAT.length > 20;
|
|
137
138
|
|
|
139
|
+
// Resolve target client
|
|
140
|
+
let client = cliArgs.client || null; // 'claude' | 'codex' | 'both' | null (interactive)
|
|
141
|
+
if (!client && !nonInteractive) {
|
|
142
|
+
console.log('\n--- Target Client ---');
|
|
143
|
+
console.log(' 1. Claude Code (default)');
|
|
144
|
+
console.log(' 2. Codex');
|
|
145
|
+
console.log(' 3. Both');
|
|
146
|
+
const choice = (await ask('Choose target [1]: ')).trim();
|
|
147
|
+
if (choice === '2') client = 'codex';
|
|
148
|
+
else if (choice === '3') client = 'both';
|
|
149
|
+
else client = 'claude';
|
|
150
|
+
}
|
|
151
|
+
if (!client) client = 'claude';
|
|
152
|
+
|
|
138
153
|
// Write config
|
|
139
154
|
console.log('\n--- Writing Config ---');
|
|
140
155
|
|
|
@@ -146,8 +161,9 @@ async function main() {
|
|
|
146
161
|
LARK_USER_REFRESH_TOKEN: hasUAT ? (existingRT || '') : '',
|
|
147
162
|
};
|
|
148
163
|
|
|
149
|
-
const result = writeNewConfig(env);
|
|
150
|
-
console.log(`Written to ${result.configPath} (
|
|
164
|
+
const result = writeNewConfig(env, undefined, undefined, client);
|
|
165
|
+
if (result.configPath) console.log(`Written to ${result.configPath} (Claude Code)`);
|
|
166
|
+
if (result.codexConfigPath) console.log(`Written to ${result.codexConfigPath} (Codex)`);
|
|
151
167
|
|
|
152
168
|
// Summary
|
|
153
169
|
console.log('\n' + '='.repeat(60));
|