feishu-user-plugin 1.3.6 → 1.3.8

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.
Files changed (71) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +71 -0
  3. package/README.md +72 -41
  4. package/package.json +10 -3
  5. package/scripts/capture-feishu-protobuf.js +86 -0
  6. package/scripts/check-changelog.js +31 -0
  7. package/scripts/check-docs-sync.js +41 -0
  8. package/scripts/check-tool-count.js +40 -0
  9. package/scripts/check-version.js +40 -0
  10. package/scripts/decode-feishu-protobuf.js +115 -0
  11. package/scripts/smoke.js +224 -0
  12. package/scripts/sync-claude-md.sh +12 -0
  13. package/scripts/sync-server-json.js +71 -0
  14. package/scripts/sync-team-skills.sh +22 -0
  15. package/scripts/test-all-tools.js +158 -0
  16. package/scripts/test-wiki-attach-fallback.js +71 -0
  17. package/scripts/test-ws-events.js +84 -0
  18. package/skills/feishu-user-plugin/SKILL.md +5 -5
  19. package/skills/feishu-user-plugin/references/CLAUDE.md +248 -318
  20. package/skills/feishu-user-plugin/references/table.md +18 -9
  21. package/src/auth/cookie.js +30 -0
  22. package/src/auth/credentials.js +399 -0
  23. package/src/auth/profile-router.js +248 -0
  24. package/src/auth/uat.js +231 -0
  25. package/src/cli.js +45 -13
  26. package/src/clients/official/base.js +188 -0
  27. package/src/clients/official/bitable.js +269 -0
  28. package/src/clients/official/calendar.js +176 -0
  29. package/src/clients/official/contacts.js +54 -0
  30. package/src/clients/official/docs.js +301 -0
  31. package/src/clients/official/drive.js +77 -0
  32. package/src/clients/official/groups.js +68 -0
  33. package/src/clients/official/im.js +414 -0
  34. package/src/clients/official/index.js +30 -0
  35. package/src/clients/official/okr.js +127 -0
  36. package/src/clients/official/tasks.js +142 -0
  37. package/src/clients/official/uploads.js +260 -0
  38. package/src/clients/official/wiki.js +207 -0
  39. package/src/{client.js → clients/user.js} +25 -33
  40. package/src/config.js +13 -8
  41. package/src/events/event-buffer.js +100 -0
  42. package/src/events/index.js +5 -0
  43. package/src/events/ws-server.js +86 -0
  44. package/src/index.js +4 -1977
  45. package/src/logger.js +20 -0
  46. package/src/oauth.js +5 -1
  47. package/src/official.js +5 -1944
  48. package/src/prompts/_registry.js +69 -0
  49. package/src/prompts/index.js +54 -0
  50. package/src/server.js +305 -0
  51. package/src/setup.js +16 -1
  52. package/src/test-all.js +2 -2
  53. package/src/test-comprehensive.js +3 -3
  54. package/src/test-send.js +1 -1
  55. package/src/tools/_registry.js +31 -0
  56. package/src/tools/bitable.js +246 -0
  57. package/src/tools/calendar.js +207 -0
  58. package/src/tools/contacts.js +66 -0
  59. package/src/tools/diagnostics.js +172 -0
  60. package/src/tools/docs.js +158 -0
  61. package/src/tools/drive.js +111 -0
  62. package/src/tools/events.js +64 -0
  63. package/src/tools/groups.js +81 -0
  64. package/src/tools/im-read.js +259 -0
  65. package/src/tools/messaging-bot.js +151 -0
  66. package/src/tools/messaging-user.js +292 -0
  67. package/src/tools/okr.js +159 -0
  68. package/src/tools/profile.js +74 -0
  69. package/src/tools/tasks.js +168 -0
  70. package/src/tools/uploads.js +63 -0
  71. package/src/tools/wiki.js +191 -0
@@ -0,0 +1,301 @@
1
+ // src/clients/official/docs.js
2
+ // Mixed into LarkOfficialClient.prototype by ./index.js (or temporarily by
3
+ // ./base.js during phase A.4–A.11). Methods receive `this` bound to the
4
+ // LarkOfficialClient instance, so they can use this.client, this._safeSDKCall,
5
+ // this._asUserOrApp, this._uatREST, this.uploadMedia, etc. — all defined in
6
+ // base.js or mixed in via other domain modules.
7
+
8
+ const { buildEmptyImageBlock, buildReplaceImagePayload, buildEmptyFileBlock, buildReplaceFilePayload } = require('../../doc-blocks');
9
+
10
+ module.exports = {
11
+ // --- Docs ---
12
+
13
+ async searchDocs(query, { pageSize = 10, pageToken } = {}) {
14
+ const res = await this._safeSDKCall(
15
+ () => this.client.request({
16
+ method: 'POST', url: '/open-apis/suite/docs-api/search/object',
17
+ data: { search_key: query, count: pageSize, offset: pageToken ? parseInt(pageToken) : 0, owner_ids: [], chat_ids: [], docs_types: [] },
18
+ }),
19
+ 'searchDocs'
20
+ );
21
+ return { items: res.data.docs_entities || [], hasMore: res.data.has_more };
22
+ },
23
+
24
+ async readDoc(documentId) {
25
+ const res = await this._asUserOrApp({
26
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/raw_content`,
27
+ query: { lang: '0' },
28
+ sdkFn: () => this.client.docx.document.rawContent({ path: { document_id: documentId }, params: { lang: 0 } }),
29
+ label: 'readDoc',
30
+ });
31
+ return { content: res.data.content };
32
+ },
33
+
34
+ async createDoc(title, folderId, { wikiSpaceId, wikiParentNodeToken } = {}) {
35
+ const res = await this._asUserOrApp({
36
+ uatPath: `/open-apis/docx/v1/documents`,
37
+ method: 'POST',
38
+ body: { title, folder_token: folderId || '' },
39
+ sdkFn: () => this.client.docx.document.create({ data: { title, folder_token: folderId || '' } }),
40
+ label: 'createDoc',
41
+ });
42
+ const documentId = res.data.document?.document_id;
43
+ const out = { documentId, viaUser: !!res._viaUser, fallbackWarning: res._fallbackWarning || null };
44
+ if (documentId && wikiSpaceId) {
45
+ try {
46
+ const node = await this.attachToWiki(wikiSpaceId, 'docx', documentId, wikiParentNodeToken);
47
+ if (node?.node_token) out.wikiNodeToken = node.node_token;
48
+ else if (node?.task_id) out.wikiAttachTaskId = node.task_id;
49
+ } catch (e) {
50
+ out.wikiAttachError = e.message;
51
+ }
52
+ }
53
+ return out;
54
+ },
55
+
56
+ async getDocBlocks(documentId) {
57
+ const res = await this._asUserOrApp({
58
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks`,
59
+ query: { page_size: '500' },
60
+ sdkFn: () => this.client.docx.documentBlock.list({ path: { document_id: documentId }, params: { page_size: 500 } }),
61
+ label: 'getDocBlocks',
62
+ });
63
+ return { items: res.data.items || [] };
64
+ },
65
+
66
+ async createDocBlock(documentId, parentBlockId, children, index) {
67
+ const data = { children };
68
+ if (index !== undefined) data.index = index;
69
+ const res = await this._asUserOrApp({
70
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${parentBlockId}/children`,
71
+ method: 'POST',
72
+ body: data,
73
+ sdkFn: () => this.client.docx.documentBlockChildren.create({
74
+ path: { document_id: documentId, block_id: parentBlockId },
75
+ data,
76
+ }),
77
+ label: 'createDocBlock',
78
+ });
79
+ return { blocks: res.data.children || [], fallbackWarning: res._fallbackWarning || null };
80
+ },
81
+
82
+ async updateDocBlock(documentId, blockId, updateBody) {
83
+ const res = await this._asUserOrApp({
84
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${blockId}`,
85
+ method: 'PATCH',
86
+ body: updateBody,
87
+ sdkFn: () => this.client.docx.documentBlock.patch({
88
+ path: { document_id: documentId, block_id: blockId },
89
+ data: updateBody,
90
+ }),
91
+ label: 'updateDocBlock',
92
+ });
93
+ return { block: res.data.block };
94
+ },
95
+
96
+ async deleteDocBlocks(documentId, parentBlockId, startIndex, endIndex) {
97
+ await this._asUserOrApp({
98
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${parentBlockId}/children/batch_delete`,
99
+ method: 'DELETE',
100
+ body: { start_index: startIndex, end_index: endIndex },
101
+ sdkFn: () => this.client.docx.documentBlockChildren.batchDelete({
102
+ path: { document_id: documentId, block_id: parentBlockId },
103
+ data: { start_index: startIndex, end_index: endIndex },
104
+ }),
105
+ label: 'deleteDocBlocks',
106
+ });
107
+ return { deleted: true };
108
+ },
109
+
110
+ // Create a new image block and populate it from either a local file path or
111
+ // an already-uploaded media token. Orchestrates the three-step Feishu flow:
112
+ // 1) create empty image placeholder block
113
+ // 2) upload pixels (skipped if caller passes a ready-made imageToken)
114
+ // 3) patch the placeholder with the uploaded token
115
+ // Returns { blockId, imageToken, viaUser }.
116
+ async createDocBlockWithImage(documentId, parentBlockId, { imagePath, imageToken, index } = {}) {
117
+ if (!imagePath && !imageToken) {
118
+ throw new Error('createDocBlockWithImage: either imagePath or imageToken is required');
119
+ }
120
+
121
+ // Step 1 — empty placeholder.
122
+ const placeholder = buildEmptyImageBlock();
123
+ const createBody = { children: [placeholder] };
124
+ if (index !== undefined) createBody.index = index;
125
+ const created = await this._asUserOrApp({
126
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${parentBlockId}/children`,
127
+ method: 'POST',
128
+ body: createBody,
129
+ sdkFn: () => this.client.docx.documentBlockChildren.create({
130
+ path: { document_id: documentId, block_id: parentBlockId },
131
+ data: createBody,
132
+ }),
133
+ label: 'createDocBlockWithImage.placeholder',
134
+ });
135
+ const newBlock = (created.data.children || [])[0];
136
+ const blockId = newBlock?.block_id;
137
+ if (!blockId) throw new Error(`createDocBlockWithImage: placeholder creation returned no block_id: ${JSON.stringify(created.data).slice(0, 400)}`);
138
+
139
+ // Step 2 — upload (if needed).
140
+ let finalToken = imageToken;
141
+ let viaUser = !!created._viaUser;
142
+ let fallbackWarning = created._fallbackWarning || null;
143
+ if (!finalToken) {
144
+ const uploaded = await this.uploadMedia(imagePath, blockId, 'docx_image');
145
+ finalToken = uploaded.fileToken;
146
+ viaUser = viaUser && uploaded.viaUser; // true iff both steps went via user
147
+ }
148
+
149
+ // Step 3 — attach token to the placeholder via PATCH replace_image.
150
+ const patch = buildReplaceImagePayload(finalToken);
151
+ await this._asUserOrApp({
152
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${blockId}`,
153
+ method: 'PATCH',
154
+ body: patch,
155
+ sdkFn: () => this.client.docx.documentBlock.patch({
156
+ path: { document_id: documentId, block_id: blockId },
157
+ data: patch,
158
+ }),
159
+ label: 'createDocBlockWithImage.replaceImage',
160
+ });
161
+
162
+ return { blockId, imageToken: finalToken, viaUser, fallbackWarning };
163
+ },
164
+
165
+ // Replace an existing image block's media token (e.g. swap the picture in an
166
+ // already-created image block). Expects an uploaded media token — use
167
+ // uploadMedia or create_doc_block's image_path shortcut to obtain one.
168
+ async updateDocBlockImage(documentId, blockId, imageToken) {
169
+ const patch = buildReplaceImagePayload(imageToken);
170
+ await this._asUserOrApp({
171
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${blockId}`,
172
+ method: 'PATCH',
173
+ body: patch,
174
+ sdkFn: () => this.client.docx.documentBlock.patch({
175
+ path: { document_id: documentId, block_id: blockId },
176
+ data: patch,
177
+ }),
178
+ label: 'updateDocBlockImage',
179
+ });
180
+ return { blockId, imageToken };
181
+ },
182
+
183
+ // Create a file-attachment block in a docx, mirroring createDocBlockWithImage:
184
+ // 1) create empty file placeholder block
185
+ // 2) upload the binary via uploadMedia(parent_type=docx_file)
186
+ // 3) PATCH with replace_file.token to attach
187
+ // Returns { blockId, fileToken, viaUser, fallbackWarning }.
188
+ async createDocBlockWithFile(documentId, parentBlockId, { filePath, fileToken, index } = {}) {
189
+ if (!filePath && !fileToken) {
190
+ throw new Error('createDocBlockWithFile: either filePath or fileToken is required');
191
+ }
192
+ const placeholder = buildEmptyFileBlock();
193
+ const createBody = { children: [placeholder] };
194
+ if (index !== undefined) createBody.index = index;
195
+ const created = await this._asUserOrApp({
196
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${parentBlockId}/children`,
197
+ method: 'POST',
198
+ body: createBody,
199
+ sdkFn: () => this.client.docx.documentBlockChildren.create({
200
+ path: { document_id: documentId, block_id: parentBlockId },
201
+ data: createBody,
202
+ }),
203
+ label: 'createDocBlockWithFile.placeholder',
204
+ });
205
+ // Feishu auto-wraps a FILE block (block_type=23) in a VIEW block
206
+ // (block_type=33) — the create response returns the OUTER view block.
207
+ // We need the inner file block's id for both the media upload (parent_node)
208
+ // and the replace_file PATCH. Walk children to find it; fall back to a
209
+ // get_doc_blocks lookup if the response didn't materialize the descendant.
210
+ const newBlock = (created.data.children || [])[0];
211
+ const outerBlockId = newBlock?.block_id;
212
+ if (!outerBlockId) throw new Error(`createDocBlockWithFile: placeholder creation returned no block_id: ${JSON.stringify(created.data).slice(0, 400)}`);
213
+ // Feishu auto-wraps a FILE block (23) in a VIEW block (33). The create
214
+ // response's outer block is the view; we need to find the inner file
215
+ // block for both the media upload (parent_node) and the replace_file PATCH.
216
+ let blockId = outerBlockId;
217
+ if (newBlock.block_type !== 23) {
218
+ const inner = await this._findFileChildOf(documentId, outerBlockId, newBlock.children);
219
+ if (!inner) throw new Error(`createDocBlockWithFile: could not locate inner FILE block under view ${outerBlockId}`);
220
+ blockId = inner;
221
+ }
222
+
223
+ let finalToken = fileToken;
224
+ let viaUser = !!created._viaUser;
225
+ let fallbackWarning = created._fallbackWarning || null;
226
+ if (!finalToken) {
227
+ const uploaded = await this.uploadMedia(filePath, blockId, 'docx_file');
228
+ finalToken = uploaded.fileToken;
229
+ viaUser = viaUser && uploaded.viaUser;
230
+ }
231
+
232
+ const patch = buildReplaceFilePayload(finalToken);
233
+ await this._asUserOrApp({
234
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${blockId}`,
235
+ method: 'PATCH',
236
+ body: patch,
237
+ sdkFn: () => this.client.docx.documentBlock.patch({
238
+ path: { document_id: documentId, block_id: blockId },
239
+ data: patch,
240
+ }),
241
+ label: 'createDocBlockWithFile.replaceFile',
242
+ });
243
+
244
+ return { blockId, viewBlockId: outerBlockId !== blockId ? outerBlockId : undefined, fileToken: finalToken, viaUser, fallbackWarning };
245
+ },
246
+
247
+ // Helper for createDocBlockWithFile — given a view block id and the children
248
+ // array surfaced by the create response (just IDs in docx v1), find the
249
+ // FILE child (block_type=23). If no children list was returned, fall back
250
+ // to listing the doc and walking by parent_id.
251
+ async _findFileChildOf(documentId, viewBlockId, childIds) {
252
+ if (Array.isArray(childIds) && childIds.length > 0) {
253
+ // childIds[0] is most likely the file block — verify with a get
254
+ for (const childId of childIds) {
255
+ try {
256
+ const res = await this._asUserOrApp({
257
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${childId}`,
258
+ method: 'GET',
259
+ sdkFn: () => this.client.docx.documentBlock.get({ path: { document_id: documentId, block_id: childId } }),
260
+ label: '_findFileChildOf.get',
261
+ });
262
+ if (res?.data?.block?.block_type === 23) return childId;
263
+ } catch (_) { /* fall through */ }
264
+ }
265
+ // None matched directly; return the first as best-effort
266
+ return childIds[0];
267
+ }
268
+ // Fallback: list all blocks and find a 23 whose parent_id is the view block
269
+ try {
270
+ const res = await this._asUserOrApp({
271
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks`,
272
+ method: 'GET',
273
+ sdkFn: () => this.client.docx.documentBlock.list({ path: { document_id: documentId } }),
274
+ label: '_findFileChildOf.list',
275
+ });
276
+ const items = res?.data?.items || [];
277
+ const match = items.find(b => b.block_type === 23 && b.parent_id === viewBlockId);
278
+ return match?.block_id || null;
279
+ } catch (_) {
280
+ return null;
281
+ }
282
+ },
283
+
284
+ // Replace an existing file block's media token. Expects an already-uploaded
285
+ // file token (use uploadMedia with parent_type=docx_file, or
286
+ // create_doc_block's file_path shortcut).
287
+ async updateDocBlockFile(documentId, blockId, fileToken) {
288
+ const patch = buildReplaceFilePayload(fileToken);
289
+ await this._asUserOrApp({
290
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${blockId}`,
291
+ method: 'PATCH',
292
+ body: patch,
293
+ sdkFn: () => this.client.docx.documentBlock.patch({
294
+ path: { document_id: documentId, block_id: blockId },
295
+ data: patch,
296
+ }),
297
+ label: 'updateDocBlockFile',
298
+ });
299
+ return { blockId, fileToken };
300
+ },
301
+ };
@@ -0,0 +1,77 @@
1
+ // src/clients/official/drive.js
2
+ // Mixed into LarkOfficialClient.prototype by ./index.js (or temporarily by
3
+ // ./base.js during phase A.4–A.11). Methods receive `this` bound to the
4
+ // LarkOfficialClient instance, so they can use this.client, this._safeSDKCall,
5
+ // this._asUserOrApp, this._uatREST, etc. — all defined in base.js.
6
+
7
+ module.exports = {
8
+ // --- Drive ---
9
+
10
+ async listFiles(folderToken, { pageSize = 50, pageToken } = {}) {
11
+ const params = { page_size: pageSize, folder_token: folderToken || '' };
12
+ if (pageToken) params.page_token = pageToken;
13
+ const res = await this._safeSDKCall(() => this.client.drive.file.list({ params }), 'listFiles');
14
+ return { items: res.data.files || [], hasMore: res.data.has_more };
15
+ },
16
+
17
+ async createFolder(name, parentToken) {
18
+ const body = { name, folder_token: parentToken || '' };
19
+ const res = await this._asUserOrApp({
20
+ uatPath: `/open-apis/drive/v1/files/create_folder`,
21
+ method: 'POST',
22
+ body,
23
+ sdkFn: () => this.client.drive.file.createFolder({ data: body }),
24
+ label: 'createFolder',
25
+ });
26
+ return { token: res.data.token, viaUser: !!res._viaUser, fallbackWarning: res._fallbackWarning || null };
27
+ },
28
+
29
+ // --- Drive: File Operations ---
30
+
31
+ async copyFile(fileToken, name, folderToken, type) {
32
+ const data = { name, folder_token: folderToken || '' };
33
+ if (type) data.type = type;
34
+ // _asUserOrApp so UAT-owned files (created by the user) can be copied
35
+ // without the bot needing edit permission. Bot-only path returned 1062501.
36
+ const res = await this._asUserOrApp({
37
+ uatPath: `/open-apis/drive/v1/files/${fileToken}/copy`,
38
+ method: 'POST',
39
+ body: data,
40
+ sdkFn: () => this.client.drive.file.copy({ path: { file_token: fileToken }, data }),
41
+ label: 'copyFile',
42
+ });
43
+ return { file: res.data.file, viaUser: !!res._viaUser, fallbackWarning: res._fallbackWarning || null };
44
+ },
45
+
46
+ async moveFile(fileToken, folderToken, type) {
47
+ // Feishu drive move requires `type` in the request body — without it Feishu
48
+ // returns 1061002 ("invalid params"). type values: file, folder, doc,
49
+ // sheet, bitable, docx, mindnote, slides. _asUserOrApp so user-owned
50
+ // resources can be moved without bot edit permission.
51
+ const data = { folder_token: folderToken || '' };
52
+ if (type) data.type = type;
53
+ const res = await this._asUserOrApp({
54
+ uatPath: `/open-apis/drive/v1/files/${fileToken}/move`,
55
+ method: 'POST',
56
+ body: data,
57
+ sdkFn: () => this.client.drive.file.move({ path: { file_token: fileToken }, data }),
58
+ label: 'moveFile',
59
+ });
60
+ return { taskId: res.data.task_id, viaUser: !!res._viaUser, fallbackWarning: res._fallbackWarning || null };
61
+ },
62
+
63
+ async deleteFile(fileToken, type) {
64
+ // _asUserOrApp so UAT-owned files can be deleted by the user. Bot-only
65
+ // path returned 1062501 because the bot lacks edit permission on
66
+ // user-created resources. Feishu also requires `type` as a query param.
67
+ const params = { type: type || 'file' };
68
+ const res = await this._asUserOrApp({
69
+ uatPath: `/open-apis/drive/v1/files/${fileToken}`,
70
+ method: 'DELETE',
71
+ query: params,
72
+ sdkFn: () => this.client.drive.file.delete({ path: { file_token: fileToken }, params }),
73
+ label: 'deleteFile',
74
+ });
75
+ return { taskId: res.data.task_id, viaUser: !!res._viaUser, fallbackWarning: res._fallbackWarning || null };
76
+ },
77
+ };
@@ -0,0 +1,68 @@
1
+ // src/clients/official/groups.js
2
+ // Mixed into LarkOfficialClient.prototype by ./index.js (or temporarily by
3
+ // ./base.js during phase A.4–A.11). Methods receive `this` bound to the
4
+ // LarkOfficialClient instance, so they can use this.client, this._safeSDKCall,
5
+ // this._asUserOrApp, this._uatREST, etc. — all defined in base.js.
6
+
7
+ module.exports = {
8
+ // --- IM: Chat Management ---
9
+
10
+ async createChat({ name, description, userIds, botIds } = {}) {
11
+ const data = {};
12
+ if (name) data.name = name;
13
+ if (description) data.description = description;
14
+ if (userIds) data.user_id_list = userIds;
15
+ if (botIds) data.bot_id_list = botIds;
16
+ const res = await this._safeSDKCall(
17
+ () => this.client.im.chat.create({ params: { user_id_type: 'open_id' }, data }),
18
+ 'createChat'
19
+ );
20
+ return { chatId: res.data.chat_id };
21
+ },
22
+
23
+ async updateChat(chatId, { name, description } = {}) {
24
+ const data = {};
25
+ if (name) data.name = name;
26
+ if (description) data.description = description;
27
+ const res = await this._safeSDKCall(
28
+ () => this.client.im.chat.update({ path: { chat_id: chatId }, data }),
29
+ 'updateChat'
30
+ );
31
+ return { updated: true };
32
+ },
33
+
34
+ async listChatMembers(chatId, { pageSize = 50, pageToken } = {}) {
35
+ const res = await this._safeSDKCall(
36
+ () => this.client.im.chatMembers.get({
37
+ path: { chat_id: chatId },
38
+ params: { member_id_type: 'open_id', page_size: pageSize, page_token: pageToken },
39
+ }),
40
+ 'listChatMembers'
41
+ );
42
+ return { items: res.data.items || [], hasMore: res.data.has_more, pageToken: res.data.page_token };
43
+ },
44
+
45
+ async addChatMembers(chatId, userIds, memberIdType = 'open_id') {
46
+ const res = await this._safeSDKCall(
47
+ () => this.client.im.chatMembers.create({
48
+ path: { chat_id: chatId },
49
+ params: { member_id_type: memberIdType },
50
+ data: { id_list: userIds },
51
+ }),
52
+ 'addChatMembers'
53
+ );
54
+ return { invalidIds: res.data.invalid_id_list || [] };
55
+ },
56
+
57
+ async removeChatMembers(chatId, userIds, memberIdType = 'open_id') {
58
+ const res = await this._safeSDKCall(
59
+ () => this.client.im.chatMembers.delete({
60
+ path: { chat_id: chatId },
61
+ params: { member_id_type: memberIdType },
62
+ data: { id_list: userIds },
63
+ }),
64
+ 'removeChatMembers'
65
+ );
66
+ return { invalidIds: res.data.invalid_id_list || [] };
67
+ },
68
+ };