@openclaw/feishu 2026.2.24 → 2026.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +2 -0
- package/package.json +2 -1
- package/skills/feishu-doc/SKILL.md +109 -3
- package/src/accounts.test.ts +90 -0
- package/src/accounts.ts +11 -2
- package/src/async.ts +62 -0
- package/src/bitable.ts +189 -215
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +55 -0
- package/src/bot.test.ts +863 -9
- package/src/bot.ts +414 -200
- package/src/card-action.ts +79 -0
- package/src/channel.ts +6 -0
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +107 -0
- package/src/client.ts +13 -0
- package/src/config-schema.test.ts +82 -1
- package/src/config-schema.ts +54 -3
- package/src/doc-schema.ts +141 -0
- package/src/docx-batch-insert.ts +190 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +76 -0
- package/src/docx.test.ts +470 -0
- package/src/docx.ts +996 -72
- package/src/drive.ts +38 -33
- package/src/media.test.ts +123 -6
- package/src/media.ts +31 -10
- package/src/monitor.account.ts +286 -0
- package/src/monitor.reaction.test.ts +235 -0
- package/src/monitor.startup.test.ts +187 -0
- package/src/monitor.startup.ts +51 -0
- package/src/monitor.state.ts +76 -0
- package/src/monitor.transport.ts +163 -0
- package/src/monitor.ts +44 -346
- package/src/monitor.webhook-security.test.ts +27 -1
- package/src/outbound.test.ts +181 -0
- package/src/outbound.ts +94 -7
- package/src/perm.ts +37 -30
- package/src/policy.test.ts +56 -1
- package/src/policy.ts +5 -1
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +253 -0
- package/src/probe.ts +99 -7
- package/src/reply-dispatcher.test.ts +259 -0
- package/src/reply-dispatcher.ts +139 -45
- package/src/send.reply-fallback.test.ts +105 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +143 -18
- package/src/streaming-card.ts +131 -43
- package/src/targets.test.ts +26 -1
- package/src/targets.ts +11 -6
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +140 -10
- package/src/wiki.ts +55 -50
package/src/docx.test.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
5
|
|
|
3
6
|
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
|
@@ -21,9 +24,14 @@ import { registerFeishuDocTools } from "./docx.js";
|
|
|
21
24
|
|
|
22
25
|
describe("feishu_doc image fetch hardening", () => {
|
|
23
26
|
const convertMock = vi.hoisted(() => vi.fn());
|
|
27
|
+
const documentCreateMock = vi.hoisted(() => vi.fn());
|
|
24
28
|
const blockListMock = vi.hoisted(() => vi.fn());
|
|
25
29
|
const blockChildrenCreateMock = vi.hoisted(() => vi.fn());
|
|
30
|
+
const blockChildrenGetMock = vi.hoisted(() => vi.fn());
|
|
31
|
+
const blockChildrenBatchDeleteMock = vi.hoisted(() => vi.fn());
|
|
32
|
+
const blockDescendantCreateMock = vi.hoisted(() => vi.fn());
|
|
26
33
|
const driveUploadAllMock = vi.hoisted(() => vi.fn());
|
|
34
|
+
const permissionMemberCreateMock = vi.hoisted(() => vi.fn());
|
|
27
35
|
const blockPatchMock = vi.hoisted(() => vi.fn());
|
|
28
36
|
const scopeListMock = vi.hoisted(() => vi.fn());
|
|
29
37
|
|
|
@@ -34,6 +42,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
34
42
|
docx: {
|
|
35
43
|
document: {
|
|
36
44
|
convert: convertMock,
|
|
45
|
+
create: documentCreateMock,
|
|
37
46
|
},
|
|
38
47
|
documentBlock: {
|
|
39
48
|
list: blockListMock,
|
|
@@ -41,12 +50,20 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
41
50
|
},
|
|
42
51
|
documentBlockChildren: {
|
|
43
52
|
create: blockChildrenCreateMock,
|
|
53
|
+
get: blockChildrenGetMock,
|
|
54
|
+
batchDelete: blockChildrenBatchDeleteMock,
|
|
55
|
+
},
|
|
56
|
+
documentBlockDescendant: {
|
|
57
|
+
create: blockDescendantCreateMock,
|
|
44
58
|
},
|
|
45
59
|
},
|
|
46
60
|
drive: {
|
|
47
61
|
media: {
|
|
48
62
|
uploadAll: driveUploadAllMock,
|
|
49
63
|
},
|
|
64
|
+
permissionMember: {
|
|
65
|
+
create: permissionMemberCreateMock,
|
|
66
|
+
},
|
|
50
67
|
},
|
|
51
68
|
application: {
|
|
52
69
|
scope: {
|
|
@@ -77,11 +94,212 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
77
94
|
},
|
|
78
95
|
});
|
|
79
96
|
|
|
97
|
+
blockChildrenGetMock.mockResolvedValue({
|
|
98
|
+
code: 0,
|
|
99
|
+
data: { items: [{ block_id: "placeholder_block_1" }] },
|
|
100
|
+
});
|
|
101
|
+
blockChildrenBatchDeleteMock.mockResolvedValue({ code: 0 });
|
|
102
|
+
// write/append use Descendant API; return image block so processImages runs
|
|
103
|
+
blockDescendantCreateMock.mockResolvedValue({
|
|
104
|
+
code: 0,
|
|
105
|
+
data: { children: [{ block_type: 27, block_id: "img_block_1" }] },
|
|
106
|
+
});
|
|
80
107
|
driveUploadAllMock.mockResolvedValue({ file_token: "token_1" });
|
|
108
|
+
documentCreateMock.mockResolvedValue({
|
|
109
|
+
code: 0,
|
|
110
|
+
data: { document: { document_id: "doc_created", title: "Created Doc" } },
|
|
111
|
+
});
|
|
112
|
+
permissionMemberCreateMock.mockResolvedValue({ code: 0 });
|
|
81
113
|
blockPatchMock.mockResolvedValue({ code: 0 });
|
|
82
114
|
scopeListMock.mockResolvedValue({ code: 0, data: { scopes: [] } });
|
|
83
115
|
});
|
|
84
116
|
|
|
117
|
+
it("inserts blocks sequentially to preserve document order", async () => {
|
|
118
|
+
const blocks = [
|
|
119
|
+
{ block_type: 3, block_id: "h1" },
|
|
120
|
+
{ block_type: 2, block_id: "t1" },
|
|
121
|
+
{ block_type: 3, block_id: "h2" },
|
|
122
|
+
];
|
|
123
|
+
convertMock.mockResolvedValue({
|
|
124
|
+
code: 0,
|
|
125
|
+
data: {
|
|
126
|
+
blocks,
|
|
127
|
+
first_level_block_ids: ["h1", "t1", "h2"],
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
blockListMock.mockResolvedValue({ code: 0, data: { items: [] } });
|
|
132
|
+
|
|
133
|
+
blockDescendantCreateMock.mockResolvedValueOnce({
|
|
134
|
+
code: 0,
|
|
135
|
+
data: { children: [{ block_type: 3, block_id: "h1" }] },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const registerTool = vi.fn();
|
|
139
|
+
registerFeishuDocTools({
|
|
140
|
+
config: {
|
|
141
|
+
channels: {
|
|
142
|
+
feishu: { appId: "app_id", appSecret: "app_secret" },
|
|
143
|
+
},
|
|
144
|
+
} as any,
|
|
145
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
146
|
+
registerTool,
|
|
147
|
+
} as any);
|
|
148
|
+
|
|
149
|
+
const feishuDocTool = registerTool.mock.calls
|
|
150
|
+
.map((call) => call[0])
|
|
151
|
+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
152
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
153
|
+
expect(feishuDocTool).toBeDefined();
|
|
154
|
+
|
|
155
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
156
|
+
action: "append",
|
|
157
|
+
doc_token: "doc_1",
|
|
158
|
+
content: "plain text body",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(blockDescendantCreateMock).toHaveBeenCalledTimes(1);
|
|
162
|
+
const call = blockDescendantCreateMock.mock.calls[0]?.[0];
|
|
163
|
+
expect(call?.data.children_id).toEqual(["h1", "t1", "h2"]);
|
|
164
|
+
expect(call?.data.descendants).toBeDefined();
|
|
165
|
+
expect(call?.data.descendants.length).toBeGreaterThanOrEqual(3);
|
|
166
|
+
|
|
167
|
+
expect(result.details.blocks_added).toBe(3);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("falls back to size-based convert chunking for long no-heading markdown", async () => {
|
|
171
|
+
let successChunkCount = 0;
|
|
172
|
+
convertMock.mockImplementation(async ({ data }) => {
|
|
173
|
+
const content = data.content as string;
|
|
174
|
+
if (content.length > 280) {
|
|
175
|
+
return { code: 999, msg: "content too large" };
|
|
176
|
+
}
|
|
177
|
+
successChunkCount++;
|
|
178
|
+
const blockId = `b_${successChunkCount}`;
|
|
179
|
+
return {
|
|
180
|
+
code: 0,
|
|
181
|
+
data: {
|
|
182
|
+
blocks: [{ block_type: 2, block_id: blockId }],
|
|
183
|
+
first_level_block_ids: [blockId],
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
blockDescendantCreateMock.mockImplementation(async ({ data }) => ({
|
|
189
|
+
code: 0,
|
|
190
|
+
data: {
|
|
191
|
+
children: (data.children_id as string[]).map((id) => ({
|
|
192
|
+
block_id: id,
|
|
193
|
+
})),
|
|
194
|
+
},
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
const registerTool = vi.fn();
|
|
198
|
+
registerFeishuDocTools({
|
|
199
|
+
config: {
|
|
200
|
+
channels: {
|
|
201
|
+
feishu: { appId: "app_id", appSecret: "app_secret" },
|
|
202
|
+
},
|
|
203
|
+
} as any,
|
|
204
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
205
|
+
registerTool,
|
|
206
|
+
} as any);
|
|
207
|
+
|
|
208
|
+
const feishuDocTool = registerTool.mock.calls
|
|
209
|
+
.map((call) => call[0])
|
|
210
|
+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
211
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
212
|
+
expect(feishuDocTool).toBeDefined();
|
|
213
|
+
|
|
214
|
+
const longMarkdown = Array.from(
|
|
215
|
+
{ length: 120 },
|
|
216
|
+
(_, i) => `line ${i} with enough content to trigger fallback chunking`,
|
|
217
|
+
).join("\n");
|
|
218
|
+
|
|
219
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
220
|
+
action: "append",
|
|
221
|
+
doc_token: "doc_1",
|
|
222
|
+
content: longMarkdown,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(convertMock.mock.calls.length).toBeGreaterThan(1);
|
|
226
|
+
expect(successChunkCount).toBeGreaterThan(1);
|
|
227
|
+
expect(result.details.blocks_added).toBe(successChunkCount);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("keeps fenced code blocks balanced when size fallback split is needed", async () => {
|
|
231
|
+
const convertedChunks: string[] = [];
|
|
232
|
+
let successChunkCount = 0;
|
|
233
|
+
let failFirstConvert = true;
|
|
234
|
+
convertMock.mockImplementation(async ({ data }) => {
|
|
235
|
+
const content = data.content as string;
|
|
236
|
+
convertedChunks.push(content);
|
|
237
|
+
if (failFirstConvert) {
|
|
238
|
+
failFirstConvert = false;
|
|
239
|
+
return { code: 999, msg: "content too large" };
|
|
240
|
+
}
|
|
241
|
+
successChunkCount++;
|
|
242
|
+
const blockId = `c_${successChunkCount}`;
|
|
243
|
+
return {
|
|
244
|
+
code: 0,
|
|
245
|
+
data: {
|
|
246
|
+
blocks: [{ block_type: 2, block_id: blockId }],
|
|
247
|
+
first_level_block_ids: [blockId],
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
blockChildrenCreateMock.mockImplementation(async ({ data }) => ({
|
|
253
|
+
code: 0,
|
|
254
|
+
data: { children: data.children },
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
const registerTool = vi.fn();
|
|
258
|
+
registerFeishuDocTools({
|
|
259
|
+
config: {
|
|
260
|
+
channels: {
|
|
261
|
+
feishu: { appId: "app_id", appSecret: "app_secret" },
|
|
262
|
+
},
|
|
263
|
+
} as any,
|
|
264
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
265
|
+
registerTool,
|
|
266
|
+
} as any);
|
|
267
|
+
|
|
268
|
+
const feishuDocTool = registerTool.mock.calls
|
|
269
|
+
.map((call) => call[0])
|
|
270
|
+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
271
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
272
|
+
expect(feishuDocTool).toBeDefined();
|
|
273
|
+
|
|
274
|
+
const fencedMarkdown = [
|
|
275
|
+
"## Section",
|
|
276
|
+
"```ts",
|
|
277
|
+
"const alpha = 1;",
|
|
278
|
+
"const beta = 2;",
|
|
279
|
+
"const gamma = alpha + beta;",
|
|
280
|
+
"console.log(gamma);",
|
|
281
|
+
"```",
|
|
282
|
+
"",
|
|
283
|
+
"Tail paragraph one with enough text to exceed API limits when combined. ".repeat(8),
|
|
284
|
+
"Tail paragraph two with enough text to exceed API limits when combined. ".repeat(8),
|
|
285
|
+
"Tail paragraph three with enough text to exceed API limits when combined. ".repeat(8),
|
|
286
|
+
].join("\n");
|
|
287
|
+
|
|
288
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
289
|
+
action: "append",
|
|
290
|
+
doc_token: "doc_1",
|
|
291
|
+
content: fencedMarkdown,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(convertMock.mock.calls.length).toBeGreaterThan(1);
|
|
295
|
+
expect(successChunkCount).toBeGreaterThan(1);
|
|
296
|
+
for (const chunk of convertedChunks) {
|
|
297
|
+
const fenceCount = chunk.match(/```/g)?.length ?? 0;
|
|
298
|
+
expect(fenceCount % 2).toBe(0);
|
|
299
|
+
}
|
|
300
|
+
expect(result.details.blocks_added).toBe(successChunkCount);
|
|
301
|
+
});
|
|
302
|
+
|
|
85
303
|
it("skips image upload when markdown image URL is blocked", async () => {
|
|
86
304
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
87
305
|
fetchRemoteMediaMock.mockRejectedValueOnce(
|
|
@@ -104,6 +322,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
104
322
|
|
|
105
323
|
const feishuDocTool = registerTool.mock.calls
|
|
106
324
|
.map((call) => call[0])
|
|
325
|
+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
107
326
|
.find((tool) => tool.name === "feishu_doc");
|
|
108
327
|
expect(feishuDocTool).toBeDefined();
|
|
109
328
|
|
|
@@ -120,4 +339,255 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
120
339
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
121
340
|
consoleErrorSpy.mockRestore();
|
|
122
341
|
});
|
|
342
|
+
|
|
343
|
+
it("create grants permission only to trusted Feishu requester", async () => {
|
|
344
|
+
const registerTool = vi.fn();
|
|
345
|
+
registerFeishuDocTools({
|
|
346
|
+
config: {
|
|
347
|
+
channels: {
|
|
348
|
+
feishu: {
|
|
349
|
+
appId: "app_id",
|
|
350
|
+
appSecret: "app_secret",
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
} as any,
|
|
354
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
355
|
+
registerTool,
|
|
356
|
+
} as any);
|
|
357
|
+
|
|
358
|
+
const feishuDocTool = registerTool.mock.calls
|
|
359
|
+
.map((call) => call[0])
|
|
360
|
+
.map((tool) =>
|
|
361
|
+
typeof tool === "function"
|
|
362
|
+
? tool({ messageChannel: "feishu", requesterSenderId: "ou_123" })
|
|
363
|
+
: tool,
|
|
364
|
+
)
|
|
365
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
366
|
+
expect(feishuDocTool).toBeDefined();
|
|
367
|
+
|
|
368
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
369
|
+
action: "create",
|
|
370
|
+
title: "Demo",
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(result.details.document_id).toBe("doc_created");
|
|
374
|
+
expect(result.details.requester_permission_added).toBe(true);
|
|
375
|
+
expect(result.details.requester_open_id).toBe("ou_123");
|
|
376
|
+
expect(result.details.requester_perm_type).toBe("edit");
|
|
377
|
+
expect(permissionMemberCreateMock).toHaveBeenCalledWith(
|
|
378
|
+
expect.objectContaining({
|
|
379
|
+
data: expect.objectContaining({
|
|
380
|
+
member_type: "openid",
|
|
381
|
+
member_id: "ou_123",
|
|
382
|
+
perm: "edit",
|
|
383
|
+
}),
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("create skips requester grant when trusted requester identity is unavailable", async () => {
|
|
389
|
+
const registerTool = vi.fn();
|
|
390
|
+
registerFeishuDocTools({
|
|
391
|
+
config: {
|
|
392
|
+
channels: {
|
|
393
|
+
feishu: {
|
|
394
|
+
appId: "app_id",
|
|
395
|
+
appSecret: "app_secret",
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
} as any,
|
|
399
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
400
|
+
registerTool,
|
|
401
|
+
} as any);
|
|
402
|
+
|
|
403
|
+
const feishuDocTool = registerTool.mock.calls
|
|
404
|
+
.map((call) => call[0])
|
|
405
|
+
.map((tool) => (typeof tool === "function" ? tool({ messageChannel: "feishu" }) : tool))
|
|
406
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
407
|
+
expect(feishuDocTool).toBeDefined();
|
|
408
|
+
|
|
409
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
410
|
+
action: "create",
|
|
411
|
+
title: "Demo",
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(permissionMemberCreateMock).not.toHaveBeenCalled();
|
|
415
|
+
expect(result.details.requester_permission_added).toBe(false);
|
|
416
|
+
expect(result.details.requester_permission_skipped_reason).toContain("trusted requester");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("create never grants permissions when grant_to_requester is false", async () => {
|
|
420
|
+
const registerTool = vi.fn();
|
|
421
|
+
registerFeishuDocTools({
|
|
422
|
+
config: {
|
|
423
|
+
channels: {
|
|
424
|
+
feishu: {
|
|
425
|
+
appId: "app_id",
|
|
426
|
+
appSecret: "app_secret",
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
} as any,
|
|
430
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
431
|
+
registerTool,
|
|
432
|
+
} as any);
|
|
433
|
+
|
|
434
|
+
const feishuDocTool = registerTool.mock.calls
|
|
435
|
+
.map((call) => call[0])
|
|
436
|
+
.map((tool) =>
|
|
437
|
+
typeof tool === "function"
|
|
438
|
+
? tool({ messageChannel: "feishu", requesterSenderId: "ou_123" })
|
|
439
|
+
: tool,
|
|
440
|
+
)
|
|
441
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
442
|
+
expect(feishuDocTool).toBeDefined();
|
|
443
|
+
|
|
444
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
445
|
+
action: "create",
|
|
446
|
+
title: "Demo",
|
|
447
|
+
grant_to_requester: false,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(permissionMemberCreateMock).not.toHaveBeenCalled();
|
|
451
|
+
expect(result.details.requester_permission_added).toBeUndefined();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("returns an error when create response omits document_id", async () => {
|
|
455
|
+
documentCreateMock.mockResolvedValueOnce({
|
|
456
|
+
code: 0,
|
|
457
|
+
data: { document: { title: "Created Doc" } },
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const registerTool = vi.fn();
|
|
461
|
+
registerFeishuDocTools({
|
|
462
|
+
config: {
|
|
463
|
+
channels: {
|
|
464
|
+
feishu: {
|
|
465
|
+
appId: "app_id",
|
|
466
|
+
appSecret: "app_secret",
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
} as any,
|
|
470
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
471
|
+
registerTool,
|
|
472
|
+
} as any);
|
|
473
|
+
|
|
474
|
+
const feishuDocTool = registerTool.mock.calls
|
|
475
|
+
.map((call) => call[0])
|
|
476
|
+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
477
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
478
|
+
expect(feishuDocTool).toBeDefined();
|
|
479
|
+
|
|
480
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
481
|
+
action: "create",
|
|
482
|
+
title: "Demo",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(result.details.error).toContain("no document_id");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("uploads local file to doc via upload_file action", async () => {
|
|
489
|
+
blockChildrenCreateMock.mockResolvedValueOnce({
|
|
490
|
+
code: 0,
|
|
491
|
+
data: {
|
|
492
|
+
children: [{ block_type: 23, block_id: "file_block_1" }],
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const localPath = join(tmpdir(), `feishu-docx-upload-${Date.now()}.txt`);
|
|
497
|
+
await fs.writeFile(localPath, "hello from local file", "utf8");
|
|
498
|
+
|
|
499
|
+
const registerTool = vi.fn();
|
|
500
|
+
registerFeishuDocTools({
|
|
501
|
+
config: {
|
|
502
|
+
channels: {
|
|
503
|
+
feishu: {
|
|
504
|
+
appId: "app_id",
|
|
505
|
+
appSecret: "app_secret",
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
} as any,
|
|
509
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
510
|
+
registerTool,
|
|
511
|
+
} as any);
|
|
512
|
+
|
|
513
|
+
const feishuDocTool = registerTool.mock.calls
|
|
514
|
+
.map((call) => call[0])
|
|
515
|
+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
516
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
517
|
+
expect(feishuDocTool).toBeDefined();
|
|
518
|
+
|
|
519
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
520
|
+
action: "upload_file",
|
|
521
|
+
doc_token: "doc_1",
|
|
522
|
+
file_path: localPath,
|
|
523
|
+
filename: "test-local.txt",
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
expect(result.details.success).toBe(true);
|
|
527
|
+
expect(result.details.file_token).toBe("token_1");
|
|
528
|
+
expect(result.details.file_name).toBe("test-local.txt");
|
|
529
|
+
|
|
530
|
+
expect(driveUploadAllMock).toHaveBeenCalledWith(
|
|
531
|
+
expect.objectContaining({
|
|
532
|
+
data: expect.objectContaining({
|
|
533
|
+
parent_type: "docx_file",
|
|
534
|
+
parent_node: "doc_1",
|
|
535
|
+
file_name: "test-local.txt",
|
|
536
|
+
}),
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
await fs.unlink(localPath);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("returns an error when upload_file cannot list placeholder siblings", async () => {
|
|
544
|
+
blockChildrenCreateMock.mockResolvedValueOnce({
|
|
545
|
+
code: 0,
|
|
546
|
+
data: {
|
|
547
|
+
children: [{ block_type: 23, block_id: "file_block_1" }],
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
blockChildrenGetMock.mockResolvedValueOnce({
|
|
551
|
+
code: 999,
|
|
552
|
+
msg: "list failed",
|
|
553
|
+
data: { items: [] },
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const localPath = join(tmpdir(), `feishu-docx-upload-fail-${Date.now()}.txt`);
|
|
557
|
+
await fs.writeFile(localPath, "hello from local file", "utf8");
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const registerTool = vi.fn();
|
|
561
|
+
registerFeishuDocTools({
|
|
562
|
+
config: {
|
|
563
|
+
channels: {
|
|
564
|
+
feishu: {
|
|
565
|
+
appId: "app_id",
|
|
566
|
+
appSecret: "app_secret",
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
} as any,
|
|
570
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
571
|
+
registerTool,
|
|
572
|
+
} as any);
|
|
573
|
+
|
|
574
|
+
const feishuDocTool = registerTool.mock.calls
|
|
575
|
+
.map((call) => call[0])
|
|
576
|
+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
577
|
+
.find((tool) => tool.name === "feishu_doc");
|
|
578
|
+
expect(feishuDocTool).toBeDefined();
|
|
579
|
+
|
|
580
|
+
const result = await feishuDocTool.execute("tool-call", {
|
|
581
|
+
action: "upload_file",
|
|
582
|
+
doc_token: "doc_1",
|
|
583
|
+
file_path: localPath,
|
|
584
|
+
filename: "test-local.txt",
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
expect(result.details.error).toBe("list failed");
|
|
588
|
+
expect(driveUploadAllMock).not.toHaveBeenCalled();
|
|
589
|
+
} finally {
|
|
590
|
+
await fs.unlink(localPath);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
123
593
|
});
|