feishu-user-plugin 1.3.1 → 1.3.3

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/src/official.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const lark = require('@larksuiteoapi/node-sdk');
2
+ const { fetchWithTimeout } = require('./utils');
2
3
 
3
4
  // Redirect all Lark SDK logs to stderr.
4
5
  // The SDK's defaultLogger.error uses console.log (stdout), which corrupts
@@ -39,6 +40,50 @@ class LarkOfficialClient {
39
40
  return !!this._uat;
40
41
  }
41
42
 
43
+ // Fetches (and caches) an app_access_token directly via the internal endpoint.
44
+ // Avoids relying on SDK-internal token-manager APIs that may change across versions.
45
+ async _getAppToken() {
46
+ const now = Math.floor(Date.now() / 1000);
47
+ if (this._appToken && this._appTokenExpires > now + 60) return this._appToken;
48
+ const res = await fetchWithTimeout('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal', {
49
+ method: 'POST',
50
+ headers: { 'content-type': 'application/json' },
51
+ body: JSON.stringify({ app_id: this.appId, app_secret: this.appSecret }),
52
+ timeoutMs: 10000,
53
+ });
54
+ const data = await res.json();
55
+ if (data.code !== 0 || !data.app_access_token) {
56
+ throw new Error(`app_access_token failed: ${data.code}: ${data.msg || 'unknown'}`);
57
+ }
58
+ this._appToken = data.app_access_token;
59
+ this._appTokenExpires = now + (typeof data.expire === 'number' ? data.expire : 7200);
60
+ return this._appToken;
61
+ }
62
+
63
+ // Probe APP_ID/SECRET validity by requesting a tenant access token.
64
+ // Catches the common "user's Claude filled in a wrong/stale APP_ID" failure mode
65
+ // (observed in production: 周宇's machine ran with an APP_ID nobody recognized,
66
+ // causing all Official API calls to 401 with cryptic messages that looked like
67
+ // MCP "掉线" to the user). Returns { valid, appId, appName?, error? }.
68
+ async verifyApp() {
69
+ try {
70
+ const token = await this._getAppToken();
71
+ // Try to fetch app display name (best-effort; requires application scope)
72
+ let appName = null;
73
+ try {
74
+ const infoRes = await fetchWithTimeout(`https://open.feishu.cn/open-apis/application/v6/applications/${this.appId}?lang=zh_cn`, {
75
+ headers: { 'Authorization': `Bearer ${token}` },
76
+ timeoutMs: 10000,
77
+ });
78
+ const info = await infoRes.json();
79
+ if (info.code === 0) appName = info.data?.app?.app_name || null;
80
+ } catch (_) { /* name is best-effort; valid creds still matter most */ }
81
+ return { valid: true, appId: this.appId, appName };
82
+ } catch (e) {
83
+ return { valid: false, appId: this.appId, error: e.message };
84
+ }
85
+ }
86
+
42
87
  async _getValidUAT() {
43
88
  if (!this._uat) throw new Error('No user_access_token. Run: npx feishu-user-plugin oauth');
44
89
 
@@ -53,7 +98,7 @@ class LarkOfficialClient {
53
98
  async _refreshUAT() {
54
99
  if (!this._uatRefresh) throw new Error('UAT expired and no refresh token. Run: npx feishu-user-plugin oauth');
55
100
 
56
- const res = await fetch('https://open.feishu.cn/open-apis/authen/v2/oauth/token', {
101
+ const res = await fetchWithTimeout('https://open.feishu.cn/open-apis/authen/v2/oauth/token', {
57
102
  method: 'POST',
58
103
  headers: { 'content-type': 'application/json' },
59
104
  body: JSON.stringify({
@@ -101,11 +146,49 @@ class LarkOfficialClient {
101
146
  return data;
102
147
  }
103
148
 
149
+ // Generic UAT REST helper. Returns parsed JSON ({code, msg, data}).
150
+ async _uatREST(method, path, { body, query } = {}) {
151
+ const qs = query ? '?' + new URLSearchParams(query).toString() : '';
152
+ const url = 'https://open.feishu.cn' + path + qs;
153
+ return this._withUAT(async (uat) => {
154
+ const headers = { 'Authorization': `Bearer ${uat}` };
155
+ const init = { method, headers };
156
+ if (body !== undefined) {
157
+ headers['content-type'] = 'application/json';
158
+ init.body = JSON.stringify(body);
159
+ }
160
+ const res = await fetchWithTimeout(url, init);
161
+ return res.json();
162
+ });
163
+ }
164
+
165
+ // Try UAT first (for resources likely owned by the user), fall back to app SDK on failure.
166
+ // Returns SDK-shaped {code, msg, data, _viaUser}. _viaUser is true iff the UAT call succeeded;
167
+ // callers can surface this to distinguish "created by user" vs "created by app" for resources
168
+ // whose ownership matters (docs, bitables, folders).
169
+ async _asUserOrApp({ uatPath, method = 'GET', body, query, sdkFn, label }) {
170
+ if (this.hasUAT) {
171
+ try {
172
+ const data = await this._uatREST(method, uatPath, { body, query });
173
+ if (data.code === 0) {
174
+ data._viaUser = true;
175
+ return data;
176
+ }
177
+ console.error(`[feishu-user-plugin] ${label} as user failed (${data.code}: ${data.msg}), retrying as app`);
178
+ } catch (err) {
179
+ console.error(`[feishu-user-plugin] ${label} as user threw (${err.message}), retrying as app`);
180
+ }
181
+ }
182
+ const appData = await this._safeSDKCall(sdkFn, label);
183
+ if (appData && typeof appData === 'object') appData._viaUser = false;
184
+ return appData;
185
+ }
186
+
104
187
  async listChatsAsUser({ pageSize = 20, pageToken } = {}) {
105
188
  const params = new URLSearchParams({ page_size: String(pageSize) });
106
189
  if (pageToken) params.set('page_token', pageToken);
107
190
  const data = await this._withUAT(async (uat) => {
108
- const res = await fetch(`https://open.feishu.cn/open-apis/im/v1/chats?${params}`, {
191
+ const res = await fetchWithTimeout(`https://open.feishu.cn/open-apis/im/v1/chats?${params}`, {
109
192
  headers: { 'Authorization': `Bearer ${uat}` },
110
193
  });
111
194
  return res.json();
@@ -127,7 +210,7 @@ class LarkOfficialClient {
127
210
  if (endTime) params.set('end_time', endTime);
128
211
  if (pageToken) params.set('page_token', pageToken);
129
212
  const data = await this._withUAT(async (uat) => {
130
- const res = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?${params}`, {
213
+ const res = await fetchWithTimeout(`https://open.feishu.cn/open-apis/im/v1/messages?${params}`, {
131
214
  headers: { 'Authorization': `Bearer ${uat}` },
132
215
  });
133
216
  return res.json();
@@ -167,6 +250,57 @@ class LarkOfficialClient {
167
250
  return this._formatMessage(res.data);
168
251
  }
169
252
 
253
+ // Download a resource (image/file) attached to a message.
254
+ // Tries UAT first (works for any chat the user is in), falls back to app token
255
+ // (requires the bot to be in the same chat — Feishu restriction).
256
+ // resourceType: 'image' | 'file'. Returns { base64, mimeType, viaUser }.
257
+ async downloadMessageResource(messageId, fileKey, resourceType = 'image') {
258
+ const path = `/open-apis/im/v1/messages/${encodeURIComponent(messageId)}/resources/${encodeURIComponent(fileKey)}?type=${encodeURIComponent(resourceType)}`;
259
+ const url = 'https://open.feishu.cn' + path;
260
+
261
+ // Attempt 1: user identity
262
+ if (this.hasUAT) {
263
+ try {
264
+ const uat = await this._getValidUAT();
265
+ const res = await fetchWithTimeout(url, {
266
+ headers: { 'Authorization': `Bearer ${uat}` },
267
+ timeoutMs: 60000,
268
+ });
269
+ if (res.ok && !res.headers.get('content-type')?.includes('application/json')) {
270
+ const buf = Buffer.from(await res.arrayBuffer());
271
+ return {
272
+ base64: buf.toString('base64'),
273
+ mimeType: res.headers.get('content-type') || 'application/octet-stream',
274
+ bytes: buf.length,
275
+ viaUser: true,
276
+ };
277
+ }
278
+ const errJson = await res.json().catch(() => null);
279
+ console.error(`[feishu-user-plugin] downloadMessageResource as user failed: ${errJson?.code}: ${errJson?.msg || res.statusText}, retrying as app`);
280
+ } catch (e) {
281
+ console.error(`[feishu-user-plugin] downloadMessageResource as user threw (${e.message}), retrying as app`);
282
+ }
283
+ }
284
+
285
+ // Attempt 2: app identity
286
+ const token = await this._getAppToken();
287
+ const res = await fetchWithTimeout(url, {
288
+ headers: { 'Authorization': `Bearer ${token}` },
289
+ timeoutMs: 60000,
290
+ });
291
+ if (!res.ok || res.headers.get('content-type')?.includes('application/json')) {
292
+ const errJson = await res.json().catch(() => null);
293
+ throw new Error(`downloadMessageResource failed: ${errJson?.code}: ${errJson?.msg || res.statusText}. Note: app identity requires the bot to be in the same chat.`);
294
+ }
295
+ const buf = Buffer.from(await res.arrayBuffer());
296
+ return {
297
+ base64: buf.toString('base64'),
298
+ mimeType: res.headers.get('content-type') || 'application/octet-stream',
299
+ bytes: buf.length,
300
+ viaUser: false,
301
+ };
302
+ }
303
+
170
304
  async replyMessage(messageId, text, msgType = 'text') {
171
305
  const content = msgType === 'text' ? JSON.stringify({ text }) : text;
172
306
  const res = await this._safeSDKCall(
@@ -371,61 +505,77 @@ class LarkOfficialClient {
371
505
  }
372
506
 
373
507
  async readDoc(documentId) {
374
- const res = await this._safeSDKCall(
375
- () => this.client.docx.document.rawContent({ path: { document_id: documentId }, params: { lang: 0 } }),
376
- 'readDoc'
377
- );
508
+ const res = await this._asUserOrApp({
509
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/raw_content`,
510
+ query: { lang: '0' },
511
+ sdkFn: () => this.client.docx.document.rawContent({ path: { document_id: documentId }, params: { lang: 0 } }),
512
+ label: 'readDoc',
513
+ });
378
514
  return { content: res.data.content };
379
515
  }
380
516
 
381
517
  async createDoc(title, folderId) {
382
- const res = await this._safeSDKCall(
383
- () => this.client.docx.document.create({ data: { title, folder_token: folderId || '' } }),
384
- 'createDoc'
385
- );
386
- return { documentId: res.data.document?.document_id };
518
+ const res = await this._asUserOrApp({
519
+ uatPath: `/open-apis/docx/v1/documents`,
520
+ method: 'POST',
521
+ body: { title, folder_token: folderId || '' },
522
+ sdkFn: () => this.client.docx.document.create({ data: { title, folder_token: folderId || '' } }),
523
+ label: 'createDoc',
524
+ });
525
+ return { documentId: res.data.document?.document_id, viaUser: !!res._viaUser };
387
526
  }
388
527
 
389
528
  async getDocBlocks(documentId) {
390
- const res = await this._safeSDKCall(
391
- () => this.client.docx.documentBlock.list({ path: { document_id: documentId }, params: { page_size: 500 } }),
392
- 'getDocBlocks'
393
- );
529
+ const res = await this._asUserOrApp({
530
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks`,
531
+ query: { page_size: '500' },
532
+ sdkFn: () => this.client.docx.documentBlock.list({ path: { document_id: documentId }, params: { page_size: 500 } }),
533
+ label: 'getDocBlocks',
534
+ });
394
535
  return { items: res.data.items || [] };
395
536
  }
396
537
 
397
538
  async createDocBlock(documentId, parentBlockId, children, index) {
398
539
  const data = { children };
399
540
  if (index !== undefined) data.index = index;
400
- const res = await this._safeSDKCall(
401
- () => this.client.docx.documentBlockChildren.create({
541
+ const res = await this._asUserOrApp({
542
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${parentBlockId}/children`,
543
+ method: 'POST',
544
+ body: data,
545
+ sdkFn: () => this.client.docx.documentBlockChildren.create({
402
546
  path: { document_id: documentId, block_id: parentBlockId },
403
547
  data,
404
548
  }),
405
- 'createDocBlock'
406
- );
549
+ label: 'createDocBlock',
550
+ });
407
551
  return { blocks: res.data.children || [] };
408
552
  }
409
553
 
410
554
  async updateDocBlock(documentId, blockId, updateBody) {
411
- const res = await this._safeSDKCall(
412
- () => this.client.docx.documentBlock.patch({
555
+ const res = await this._asUserOrApp({
556
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${blockId}`,
557
+ method: 'PATCH',
558
+ body: updateBody,
559
+ sdkFn: () => this.client.docx.documentBlock.patch({
413
560
  path: { document_id: documentId, block_id: blockId },
414
561
  data: updateBody,
415
562
  }),
416
- 'updateDocBlock'
417
- );
563
+ label: 'updateDocBlock',
564
+ });
418
565
  return { block: res.data.block };
419
566
  }
420
567
 
421
568
  async deleteDocBlocks(documentId, parentBlockId, startIndex, endIndex) {
422
- const res = await this._safeSDKCall(
423
- () => this.client.docx.documentBlockChildren.batchDelete({
569
+ await this._asUserOrApp({
570
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${parentBlockId}/children/batch_delete`,
571
+ method: 'DELETE',
572
+ body: { start_index: startIndex, end_index: endIndex },
573
+ sdkFn: () => this.client.docx.documentBlockChildren.batchDelete({
424
574
  path: { document_id: documentId, block_id: parentBlockId },
425
575
  data: { start_index: startIndex, end_index: endIndex },
426
576
  }),
427
- 'deleteDocBlocks'
428
- );
577
+ label: 'deleteDocBlocks',
578
+ });
429
579
  return { deleted: true };
430
580
  }
431
581
 
@@ -445,15 +595,22 @@ class LarkOfficialClient {
445
595
  const data = {};
446
596
  if (name) data.name = name;
447
597
  if (folderId) data.folder_token = folderId;
448
- const res = await this._safeSDKCall(
449
- () => this.client.bitable.app.create({ data }),
450
- 'createBitable'
451
- );
452
- return { appToken: res.data.app?.app_token, name: res.data.app?.name, url: res.data.app?.url };
598
+ const res = await this._asUserOrApp({
599
+ uatPath: `/open-apis/bitable/v1/apps`,
600
+ method: 'POST',
601
+ body: data,
602
+ sdkFn: () => this.client.bitable.app.create({ data }),
603
+ label: 'createBitable',
604
+ });
605
+ return { appToken: res.data.app?.app_token, name: res.data.app?.name, url: res.data.app?.url, viaUser: !!res._viaUser };
453
606
  }
454
607
 
455
608
  async listBitableTables(appToken) {
456
- const res = await this._safeSDKCall(() => this.client.bitable.appTable.list({ path: { app_token: appToken } }), 'listTables');
609
+ const res = await this._asUserOrApp({
610
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables`,
611
+ sdkFn: () => this.client.bitable.appTable.list({ path: { app_token: appToken } }),
612
+ label: 'listTables',
613
+ });
457
614
  return { items: res.data.items || [] };
458
615
  }
459
616
 
@@ -461,39 +618,54 @@ class LarkOfficialClient {
461
618
  const data = { table: { name } };
462
619
  if (fields && fields.length > 0) data.table.default_view_name = name;
463
620
  if (fields && fields.length > 0) data.table.fields = fields;
464
- const res = await this._safeSDKCall(
465
- () => this.client.bitable.appTable.create({ path: { app_token: appToken }, data }),
466
- 'createTable'
467
- );
621
+ const res = await this._asUserOrApp({
622
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables`,
623
+ method: 'POST',
624
+ body: data,
625
+ sdkFn: () => this.client.bitable.appTable.create({ path: { app_token: appToken }, data }),
626
+ label: 'createTable',
627
+ });
468
628
  return { tableId: res.data.table_id };
469
629
  }
470
630
 
471
631
  async listBitableFields(appToken, tableId) {
472
- const res = await this._safeSDKCall(() => this.client.bitable.appTableField.list({ path: { app_token: appToken, table_id: tableId } }), 'listFields');
632
+ const res = await this._asUserOrApp({
633
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/fields`,
634
+ sdkFn: () => this.client.bitable.appTableField.list({ path: { app_token: appToken, table_id: tableId } }),
635
+ label: 'listFields',
636
+ });
473
637
  return { items: res.data.items || [] };
474
638
  }
475
639
 
476
640
  async createBitableField(appToken, tableId, fieldConfig) {
477
- const res = await this._safeSDKCall(
478
- () => this.client.bitable.appTableField.create({ path: { app_token: appToken, table_id: tableId }, data: fieldConfig }),
479
- 'createField'
480
- );
641
+ const res = await this._asUserOrApp({
642
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/fields`,
643
+ method: 'POST',
644
+ body: fieldConfig,
645
+ sdkFn: () => this.client.bitable.appTableField.create({ path: { app_token: appToken, table_id: tableId }, data: fieldConfig }),
646
+ label: 'createField',
647
+ });
481
648
  return { field: res.data.field };
482
649
  }
483
650
 
484
651
  async updateBitableField(appToken, tableId, fieldId, fieldConfig) {
485
- const res = await this._safeSDKCall(
486
- () => this.client.bitable.appTableField.update({ path: { app_token: appToken, table_id: tableId, field_id: fieldId }, data: fieldConfig }),
487
- 'updateField'
488
- );
652
+ const res = await this._asUserOrApp({
653
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/fields/${fieldId}`,
654
+ method: 'PUT',
655
+ body: fieldConfig,
656
+ sdkFn: () => this.client.bitable.appTableField.update({ path: { app_token: appToken, table_id: tableId, field_id: fieldId }, data: fieldConfig }),
657
+ label: 'updateField',
658
+ });
489
659
  return { field: res.data.field };
490
660
  }
491
661
 
492
662
  async deleteBitableField(appToken, tableId, fieldId) {
493
- const res = await this._safeSDKCall(
494
- () => this.client.bitable.appTableField.delete({ path: { app_token: appToken, table_id: tableId, field_id: fieldId } }),
495
- 'deleteField'
496
- );
663
+ const res = await this._asUserOrApp({
664
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/fields/${fieldId}`,
665
+ method: 'DELETE',
666
+ sdkFn: () => this.client.bitable.appTableField.delete({ path: { app_token: appToken, table_id: tableId, field_id: fieldId } }),
667
+ label: 'deleteField',
668
+ });
497
669
  return { fieldId: res.data.field_id, deleted: res.data.deleted };
498
670
  }
499
671
 
@@ -501,126 +673,169 @@ class LarkOfficialClient {
501
673
  const data = {};
502
674
  if (filter) data.filter = filter;
503
675
  if (sort) data.sort = sort;
504
- if (pageSize) data.page_size = pageSize;
505
- if (pageToken) data.page_token = pageToken;
506
- const res = await this._safeSDKCall(
507
- () => this.client.bitable.appTableRecord.search({ path: { app_token: appToken, table_id: tableId }, data }),
508
- 'searchRecords'
509
- );
676
+ const query = {};
677
+ if (pageSize) query.page_size = String(pageSize);
678
+ if (pageToken) query.page_token = pageToken;
679
+ const res = await this._asUserOrApp({
680
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/search`,
681
+ method: 'POST',
682
+ body: data,
683
+ query,
684
+ sdkFn: () => this.client.bitable.appTableRecord.search({
685
+ path: { app_token: appToken, table_id: tableId },
686
+ params: { page_size: pageSize, ...(pageToken ? { page_token: pageToken } : {}) },
687
+ data,
688
+ }),
689
+ label: 'searchRecords',
690
+ });
510
691
  return { items: res.data.items || [], total: res.data.total, hasMore: res.data.has_more };
511
692
  }
512
693
 
513
694
  async createBitableRecord(appToken, tableId, fields) {
514
- const res = await this._safeSDKCall(
515
- () => this.client.bitable.appTableRecord.create({ path: { app_token: appToken, table_id: tableId }, data: { fields } }),
516
- 'createRecord'
517
- );
695
+ const res = await this._asUserOrApp({
696
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`,
697
+ method: 'POST',
698
+ body: { fields },
699
+ sdkFn: () => this.client.bitable.appTableRecord.create({ path: { app_token: appToken, table_id: tableId }, data: { fields } }),
700
+ label: 'createRecord',
701
+ });
518
702
  return { recordId: res.data.record?.record_id };
519
703
  }
520
704
 
521
705
  async updateBitableRecord(appToken, tableId, recordId, fields) {
522
- const res = await this._safeSDKCall(
523
- () => this.client.bitable.appTableRecord.update({ path: { app_token: appToken, table_id: tableId, record_id: recordId }, data: { fields } }),
524
- 'updateRecord'
525
- );
706
+ const res = await this._asUserOrApp({
707
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/${recordId}`,
708
+ method: 'PUT',
709
+ body: { fields },
710
+ sdkFn: () => this.client.bitable.appTableRecord.update({ path: { app_token: appToken, table_id: tableId, record_id: recordId }, data: { fields } }),
711
+ label: 'updateRecord',
712
+ });
526
713
  return { recordId: res.data.record?.record_id };
527
714
  }
528
715
 
529
716
  async deleteBitableRecord(appToken, tableId, recordId) {
530
- const res = await this._safeSDKCall(
531
- () => this.client.bitable.appTableRecord.delete({ path: { app_token: appToken, table_id: tableId, record_id: recordId } }),
532
- 'deleteRecord'
533
- );
717
+ const res = await this._asUserOrApp({
718
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/${recordId}`,
719
+ method: 'DELETE',
720
+ sdkFn: () => this.client.bitable.appTableRecord.delete({ path: { app_token: appToken, table_id: tableId, record_id: recordId } }),
721
+ label: 'deleteRecord',
722
+ });
534
723
  return { deleted: res.data.deleted };
535
724
  }
536
725
 
537
726
  async batchCreateBitableRecords(appToken, tableId, records) {
538
- const res = await this._safeSDKCall(
539
- () => this.client.bitable.appTableRecord.batchCreate({ path: { app_token: appToken, table_id: tableId }, data: { records } }),
540
- 'batchCreateRecords'
541
- );
727
+ const res = await this._asUserOrApp({
728
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/batch_create`,
729
+ method: 'POST',
730
+ body: { records },
731
+ sdkFn: () => this.client.bitable.appTableRecord.batchCreate({ path: { app_token: appToken, table_id: tableId }, data: { records } }),
732
+ label: 'batchCreateRecords',
733
+ });
542
734
  return { records: res.data.records || [] };
543
735
  }
544
736
 
545
737
  async batchUpdateBitableRecords(appToken, tableId, records) {
546
- const res = await this._safeSDKCall(
547
- () => this.client.bitable.appTableRecord.batchUpdate({ path: { app_token: appToken, table_id: tableId }, data: { records } }),
548
- 'batchUpdateRecords'
549
- );
738
+ const res = await this._asUserOrApp({
739
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/batch_update`,
740
+ method: 'POST',
741
+ body: { records },
742
+ sdkFn: () => this.client.bitable.appTableRecord.batchUpdate({ path: { app_token: appToken, table_id: tableId }, data: { records } }),
743
+ label: 'batchUpdateRecords',
744
+ });
550
745
  return { records: res.data.records || [] };
551
746
  }
552
747
 
553
748
  async batchDeleteBitableRecords(appToken, tableId, recordIds) {
554
- const res = await this._safeSDKCall(
555
- () => this.client.bitable.appTableRecord.batchDelete({ path: { app_token: appToken, table_id: tableId }, data: { records: recordIds } }),
556
- 'batchDeleteRecords'
557
- );
749
+ const res = await this._asUserOrApp({
750
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/batch_delete`,
751
+ method: 'POST',
752
+ body: { records: recordIds },
753
+ sdkFn: () => this.client.bitable.appTableRecord.batchDelete({ path: { app_token: appToken, table_id: tableId }, data: { records: recordIds } }),
754
+ label: 'batchDeleteRecords',
755
+ });
558
756
  return { records: res.data.records || [] };
559
757
  }
560
758
 
561
759
  async listBitableViews(appToken, tableId) {
562
- const res = await this._safeSDKCall(
563
- () => this.client.bitable.appTableView.list({ path: { app_token: appToken, table_id: tableId }, params: { page_size: 50 } }),
564
- 'listViews'
565
- );
760
+ const res = await this._asUserOrApp({
761
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/views`,
762
+ query: { page_size: '50' },
763
+ sdkFn: () => this.client.bitable.appTableView.list({ path: { app_token: appToken, table_id: tableId }, params: { page_size: 50 } }),
764
+ label: 'listViews',
765
+ });
566
766
  return { items: res.data.items || [] };
567
767
  }
568
768
 
569
769
  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
- );
770
+ const res = await this._asUserOrApp({
771
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/${recordId}`,
772
+ sdkFn: () => this.client.bitable.appTableRecord.get({ path: { app_token: appToken, table_id: tableId, record_id: recordId } }),
773
+ label: 'getRecord',
774
+ });
574
775
  return { record: res.data.record };
575
776
  }
576
777
 
577
778
  async deleteBitableTable(appToken, tableId) {
578
- await this._safeSDKCall(
579
- () => this.client.bitable.appTable.delete({ path: { app_token: appToken, table_id: tableId } }),
580
- 'deleteTable'
581
- );
779
+ await this._asUserOrApp({
780
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}`,
781
+ method: 'DELETE',
782
+ sdkFn: () => this.client.bitable.appTable.delete({ path: { app_token: appToken, table_id: tableId } }),
783
+ label: 'deleteTable',
784
+ });
582
785
  return { deleted: true };
583
786
  }
584
787
 
585
788
  async getBitableMeta(appToken) {
586
- const res = await this._safeSDKCall(
587
- () => this.client.bitable.app.get({ path: { app_token: appToken } }),
588
- 'getBitableMeta'
589
- );
789
+ const res = await this._asUserOrApp({
790
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}`,
791
+ sdkFn: () => this.client.bitable.app.get({ path: { app_token: appToken } }),
792
+ label: 'getBitableMeta',
793
+ });
590
794
  return { app: res.data.app };
591
795
  }
592
796
 
593
797
  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
- );
798
+ const res = await this._asUserOrApp({
799
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}`,
800
+ method: 'PATCH',
801
+ body: { name },
802
+ sdkFn: () => this.client.bitable.appTable.patch({ path: { app_token: appToken, table_id: tableId }, data: { name } }),
803
+ label: 'updateTable',
804
+ });
598
805
  return { name: res.data.name };
599
806
  }
600
807
 
601
808
  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
- );
809
+ const res = await this._asUserOrApp({
810
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/views`,
811
+ method: 'POST',
812
+ body: { view_name: viewName, view_type: viewType },
813
+ sdkFn: () => this.client.bitable.appTableView.create({ path: { app_token: appToken, table_id: tableId }, data: { view_name: viewName, view_type: viewType } }),
814
+ label: 'createView',
815
+ });
606
816
  return { view: res.data.view };
607
817
  }
608
818
 
609
819
  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
- );
820
+ await this._asUserOrApp({
821
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/views/${viewId}`,
822
+ method: 'DELETE',
823
+ sdkFn: () => this.client.bitable.appTableView.delete({ path: { app_token: appToken, table_id: tableId, view_id: viewId } }),
824
+ label: 'deleteView',
825
+ });
614
826
  return { deleted: true };
615
827
  }
616
828
 
617
829
  async copyBitable(appToken, name, folderId) {
618
830
  const data = { name };
619
831
  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
- );
832
+ const res = await this._asUserOrApp({
833
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/copy`,
834
+ method: 'POST',
835
+ body: data,
836
+ sdkFn: () => this.client.bitable.app.copy({ path: { app_token: appToken }, data }),
837
+ label: 'copyBitable',
838
+ });
624
839
  return { app: res.data.app };
625
840
  }
626
841
 
@@ -665,11 +880,15 @@ class LarkOfficialClient {
665
880
  }
666
881
 
667
882
  async createFolder(name, parentToken) {
668
- const res = await this._safeSDKCall(
669
- () => this.client.drive.file.createFolder({ data: { name, folder_token: parentToken || '' } }),
670
- 'createFolder'
671
- );
672
- return { token: res.data.token };
883
+ const body = { name, folder_token: parentToken || '' };
884
+ const res = await this._asUserOrApp({
885
+ uatPath: `/open-apis/drive/v1/files/create_folder`,
886
+ method: 'POST',
887
+ body,
888
+ sdkFn: () => this.client.drive.file.createFolder({ data: body }),
889
+ label: 'createFolder',
890
+ });
891
+ return { token: res.data.token, viaUser: !!res._viaUser };
673
892
  }
674
893
 
675
894
  // --- Drive: File Operations ---
@@ -731,53 +950,6 @@ class LarkOfficialClient {
731
950
  return allChats;
732
951
  }
733
952
 
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
-
781
953
  // --- Safe SDK Call (extracts real Feishu error from AxiosError) ---
782
954
 
783
955
  async _safeSDKCall(fn, label = 'API') {
@@ -863,7 +1035,7 @@ class LarkOfficialClient {
863
1035
  if (!m) return null;
864
1036
  let body = m.body?.content || '';
865
1037
  try { body = JSON.parse(body); } catch {}
866
- return {
1038
+ const out = {
867
1039
  messageId: m.message_id,
868
1040
  chatId: m.chat_id,
869
1041
  senderId: m.sender?.id,
@@ -873,6 +1045,8 @@ class LarkOfficialClient {
873
1045
  createTime: this._normalizeTimestamp(m.create_time),
874
1046
  updateTime: this._normalizeTimestamp(m.update_time),
875
1047
  };
1048
+ if (Array.isArray(m.mentions) && m.mentions.length > 0) out.mentions = m.mentions;
1049
+ return out;
876
1050
  }
877
1051
 
878
1052
  _normalizeTimestamp(ts) {