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/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +16 -0
- package/README.md +5 -4
- package/package.json +2 -2
- package/proto/lark.proto +27 -0
- package/skills/feishu-user-plugin/SKILL.md +3 -3
- package/skills/feishu-user-plugin/references/CLAUDE.md +50 -15
- package/src/client.js +85 -14
- package/src/index.js +104 -42
- package/src/official.js +340 -166
- package/src/utils.js +13 -0
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
|
|
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
|
|
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
|
|
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.
|
|
375
|
-
|
|
376
|
-
'
|
|
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.
|
|
383
|
-
|
|
384
|
-
'
|
|
385
|
-
|
|
386
|
-
|
|
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.
|
|
391
|
-
|
|
392
|
-
'
|
|
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.
|
|
401
|
-
|
|
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.
|
|
412
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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.
|
|
449
|
-
|
|
450
|
-
'
|
|
451
|
-
|
|
452
|
-
|
|
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.
|
|
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.
|
|
465
|
-
|
|
466
|
-
'
|
|
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.
|
|
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.
|
|
478
|
-
|
|
479
|
-
'
|
|
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.
|
|
486
|
-
|
|
487
|
-
'
|
|
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.
|
|
494
|
-
|
|
495
|
-
'
|
|
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
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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.
|
|
515
|
-
|
|
516
|
-
'
|
|
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.
|
|
523
|
-
|
|
524
|
-
'
|
|
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.
|
|
531
|
-
|
|
532
|
-
'
|
|
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.
|
|
539
|
-
|
|
540
|
-
'
|
|
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.
|
|
547
|
-
|
|
548
|
-
'
|
|
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.
|
|
555
|
-
|
|
556
|
-
'
|
|
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.
|
|
563
|
-
|
|
564
|
-
'
|
|
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.
|
|
571
|
-
|
|
572
|
-
|
|
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.
|
|
579
|
-
|
|
580
|
-
'
|
|
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.
|
|
587
|
-
|
|
588
|
-
|
|
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.
|
|
595
|
-
|
|
596
|
-
'
|
|
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.
|
|
603
|
-
|
|
604
|
-
'
|
|
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.
|
|
611
|
-
|
|
612
|
-
'
|
|
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.
|
|
621
|
-
|
|
622
|
-
'
|
|
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
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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) {
|