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.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +71 -0
- package/README.md +72 -41
- package/package.json +10 -3
- package/scripts/capture-feishu-protobuf.js +86 -0
- package/scripts/check-changelog.js +31 -0
- package/scripts/check-docs-sync.js +41 -0
- package/scripts/check-tool-count.js +40 -0
- package/scripts/check-version.js +40 -0
- package/scripts/decode-feishu-protobuf.js +115 -0
- package/scripts/smoke.js +224 -0
- package/scripts/sync-claude-md.sh +12 -0
- package/scripts/sync-server-json.js +71 -0
- package/scripts/sync-team-skills.sh +22 -0
- package/scripts/test-all-tools.js +158 -0
- package/scripts/test-wiki-attach-fallback.js +71 -0
- package/scripts/test-ws-events.js +84 -0
- package/skills/feishu-user-plugin/SKILL.md +5 -5
- package/skills/feishu-user-plugin/references/CLAUDE.md +248 -318
- package/skills/feishu-user-plugin/references/table.md +18 -9
- package/src/auth/cookie.js +30 -0
- package/src/auth/credentials.js +399 -0
- package/src/auth/profile-router.js +248 -0
- package/src/auth/uat.js +231 -0
- package/src/cli.js +45 -13
- package/src/clients/official/base.js +188 -0
- package/src/clients/official/bitable.js +269 -0
- package/src/clients/official/calendar.js +176 -0
- package/src/clients/official/contacts.js +54 -0
- package/src/clients/official/docs.js +301 -0
- package/src/clients/official/drive.js +77 -0
- package/src/clients/official/groups.js +68 -0
- package/src/clients/official/im.js +414 -0
- package/src/clients/official/index.js +30 -0
- package/src/clients/official/okr.js +127 -0
- package/src/clients/official/tasks.js +142 -0
- package/src/clients/official/uploads.js +260 -0
- package/src/clients/official/wiki.js +207 -0
- package/src/{client.js → clients/user.js} +25 -33
- package/src/config.js +13 -8
- package/src/events/event-buffer.js +100 -0
- package/src/events/index.js +5 -0
- package/src/events/ws-server.js +86 -0
- package/src/index.js +4 -1977
- package/src/logger.js +20 -0
- package/src/oauth.js +5 -1
- package/src/official.js +5 -1944
- package/src/prompts/_registry.js +69 -0
- package/src/prompts/index.js +54 -0
- package/src/server.js +305 -0
- package/src/setup.js +16 -1
- package/src/test-all.js +2 -2
- package/src/test-comprehensive.js +3 -3
- package/src/test-send.js +1 -1
- package/src/tools/_registry.js +31 -0
- package/src/tools/bitable.js +246 -0
- package/src/tools/calendar.js +207 -0
- package/src/tools/contacts.js +66 -0
- package/src/tools/diagnostics.js +172 -0
- package/src/tools/docs.js +158 -0
- package/src/tools/drive.js +111 -0
- package/src/tools/events.js +64 -0
- package/src/tools/groups.js +81 -0
- package/src/tools/im-read.js +259 -0
- package/src/tools/messaging-bot.js +151 -0
- package/src/tools/messaging-user.js +292 -0
- package/src/tools/okr.js +159 -0
- package/src/tools/profile.js +74 -0
- package/src/tools/tasks.js +168 -0
- package/src/tools/uploads.js +63 -0
- 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
|
+
};
|