@openclaw/feishu 2026.3.13 → 2026.5.1-beta.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/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1653 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +95 -7
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +778 -775
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +63 -1
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +32 -94
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +375 -26
- package/src/media.ts +434 -88
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +218 -312
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +108 -48
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +11 -9
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +660 -29
- package/src/reply-dispatcher.ts +407 -154
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +77 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +399 -86
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +479 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/docx.test.ts
CHANGED
|
@@ -1,40 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { join } from "node:path";
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
4
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createToolFactoryHarness, type ToolLike } from "./tool-factory-test-harness.js";
|
|
5
5
|
|
|
6
6
|
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
const resolveFeishuToolAccountMock = vi.hoisted(() => vi.fn());
|
|
7
8
|
const fetchRemoteMediaMock = vi.hoisted(() => vi.fn());
|
|
8
|
-
|
|
9
|
-
vi.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
vi.
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const loadWebMediaMock = vi.hoisted(() => vi.fn());
|
|
10
|
+
const convertMock = vi.hoisted(() => vi.fn());
|
|
11
|
+
const documentCreateMock = vi.hoisted(() => vi.fn());
|
|
12
|
+
const blockListMock = vi.hoisted(() => vi.fn());
|
|
13
|
+
const blockChildrenCreateMock = vi.hoisted(() => vi.fn());
|
|
14
|
+
const blockChildrenGetMock = vi.hoisted(() => vi.fn());
|
|
15
|
+
const blockChildrenBatchDeleteMock = vi.hoisted(() => vi.fn());
|
|
16
|
+
const blockDescendantCreateMock = vi.hoisted(() => vi.fn());
|
|
17
|
+
const driveUploadAllMock = vi.hoisted(() => vi.fn());
|
|
18
|
+
const permissionMemberCreateMock = vi.hoisted(() => vi.fn());
|
|
19
|
+
const blockPatchMock = vi.hoisted(() => vi.fn());
|
|
20
|
+
const scopeListMock = vi.hoisted(() => vi.fn());
|
|
21
|
+
const toolAccountModule = await import("./tool-account.js");
|
|
22
|
+
const runtimeModule = await import("./runtime.js");
|
|
23
|
+
|
|
24
|
+
vi.spyOn(toolAccountModule, "createFeishuToolClient").mockImplementation(() =>
|
|
25
|
+
createFeishuClientMock(),
|
|
26
|
+
);
|
|
27
|
+
vi.spyOn(toolAccountModule, "resolveAnyEnabledFeishuToolsConfig").mockReturnValue({
|
|
28
|
+
doc: true,
|
|
29
|
+
chat: false,
|
|
30
|
+
wiki: false,
|
|
31
|
+
drive: false,
|
|
32
|
+
perm: false,
|
|
33
|
+
scopes: false,
|
|
34
|
+
});
|
|
35
|
+
vi.spyOn(toolAccountModule, "resolveFeishuToolAccount").mockImplementation((...args) =>
|
|
36
|
+
resolveFeishuToolAccountMock(...args),
|
|
37
|
+
);
|
|
38
|
+
vi.spyOn(runtimeModule, "getFeishuRuntime").mockImplementation(
|
|
39
|
+
() =>
|
|
40
|
+
({
|
|
41
|
+
channel: {
|
|
42
|
+
media: {
|
|
43
|
+
fetchRemoteMedia: fetchRemoteMediaMock,
|
|
44
|
+
saveMediaBuffer: vi.fn(),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
16
47
|
media: {
|
|
17
|
-
|
|
48
|
+
loadWebMedia: loadWebMediaMock,
|
|
49
|
+
detectMime: vi.fn(async () => "application/octet-stream"),
|
|
50
|
+
mediaKindFromMime: vi.fn(() => "image"),
|
|
51
|
+
isVoiceCompatibleAudio: vi.fn(() => false),
|
|
52
|
+
getImageMetadata: vi.fn(async () => null),
|
|
53
|
+
resizeToJpeg: vi.fn(async () => Buffer.alloc(0)),
|
|
18
54
|
},
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
}));
|
|
55
|
+
}) as unknown as ReturnType<typeof runtimeModule.getFeishuRuntime>,
|
|
56
|
+
);
|
|
22
57
|
|
|
23
|
-
|
|
58
|
+
const { registerFeishuDocTools } = await import("./docx.js");
|
|
24
59
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const blockListMock = vi.hoisted(() => vi.fn());
|
|
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());
|
|
33
|
-
const driveUploadAllMock = vi.hoisted(() => vi.fn());
|
|
34
|
-
const permissionMemberCreateMock = vi.hoisted(() => vi.fn());
|
|
35
|
-
const blockPatchMock = vi.hoisted(() => vi.fn());
|
|
36
|
-
const scopeListMock = vi.hoisted(() => vi.fn());
|
|
60
|
+
type ToolResultWithDetails = {
|
|
61
|
+
details: Record<string, unknown>;
|
|
62
|
+
};
|
|
37
63
|
|
|
64
|
+
describe("feishu_doc image fetch hardening", () => {
|
|
38
65
|
beforeEach(() => {
|
|
39
66
|
vi.clearAllMocks();
|
|
40
67
|
|
|
@@ -71,6 +98,9 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
71
98
|
},
|
|
72
99
|
},
|
|
73
100
|
});
|
|
101
|
+
resolveFeishuToolAccountMock.mockReturnValue({
|
|
102
|
+
config: { mediaMaxMb: 30 },
|
|
103
|
+
});
|
|
74
104
|
|
|
75
105
|
convertMock.mockResolvedValue({
|
|
76
106
|
code: 0,
|
|
@@ -115,26 +145,26 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
115
145
|
});
|
|
116
146
|
|
|
117
147
|
function resolveFeishuDocTool(context: Record<string, unknown> = {}) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
appSecret: "app_secret",
|
|
125
|
-
},
|
|
148
|
+
const harness = createToolFactoryHarness({
|
|
149
|
+
channels: {
|
|
150
|
+
feishu: {
|
|
151
|
+
enabled: true,
|
|
152
|
+
appId: "app_id",
|
|
153
|
+
appSecret: "app_secret",
|
|
126
154
|
},
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const tool = registerTool.mock.calls
|
|
133
|
-
.map((call) => call[0])
|
|
134
|
-
.map((candidate) => (typeof candidate === "function" ? candidate(context) : candidate))
|
|
135
|
-
.find((candidate) => candidate.name === "feishu_doc");
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
registerFeishuDocTools(harness.api);
|
|
158
|
+
const tool = harness.resolveTool("feishu_doc", context);
|
|
136
159
|
expect(tool).toBeDefined();
|
|
137
|
-
return tool
|
|
160
|
+
return tool;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function executeFeishuDocTool(
|
|
164
|
+
tool: ToolLike,
|
|
165
|
+
params: Record<string, unknown>,
|
|
166
|
+
): Promise<ToolResultWithDetails> {
|
|
167
|
+
return (await tool.execute("tool-call", params)) as ToolResultWithDetails;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
it("inserts blocks sequentially to preserve document order", async () => {
|
|
@@ -160,7 +190,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
160
190
|
|
|
161
191
|
const feishuDocTool = resolveFeishuDocTool();
|
|
162
192
|
|
|
163
|
-
const result = await feishuDocTool
|
|
193
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
164
194
|
action: "append",
|
|
165
195
|
doc_token: "doc_1",
|
|
166
196
|
content: "plain text body",
|
|
@@ -175,6 +205,50 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
175
205
|
expect(result.details.blocks_added).toBe(3);
|
|
176
206
|
});
|
|
177
207
|
|
|
208
|
+
it("reorders convert output by document tree instead of raw block array order", async () => {
|
|
209
|
+
const blocks = [
|
|
210
|
+
{ block_type: 13, block_id: "li2", parent_id: "list1" },
|
|
211
|
+
{ block_type: 4, block_id: "h2" },
|
|
212
|
+
{ block_type: 13, block_id: "li1", parent_id: "list1" },
|
|
213
|
+
{ block_type: 3, block_id: "h1" },
|
|
214
|
+
{ block_type: 12, block_id: "list1", children: ["li1", "li2"] },
|
|
215
|
+
{ block_type: 2, block_id: "p1" },
|
|
216
|
+
];
|
|
217
|
+
convertMock.mockResolvedValue({
|
|
218
|
+
code: 0,
|
|
219
|
+
data: {
|
|
220
|
+
blocks,
|
|
221
|
+
first_level_block_ids: ["h1", "p1", "h2", "list1"],
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
blockDescendantCreateMock.mockImplementationOnce(async ({ data }) => ({
|
|
226
|
+
code: 0,
|
|
227
|
+
data: {
|
|
228
|
+
children: (data.children_id as string[]).map((id) => ({ block_id: id })),
|
|
229
|
+
},
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
233
|
+
|
|
234
|
+
await feishuDocTool.execute("tool-call", {
|
|
235
|
+
action: "append",
|
|
236
|
+
doc_token: "doc_1",
|
|
237
|
+
content: "tree reorder",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const call = blockDescendantCreateMock.mock.calls[0]?.[0];
|
|
241
|
+
expect(call?.data.children_id).toEqual(["h1", "p1", "h2", "list1"]);
|
|
242
|
+
expect((call?.data.descendants as Array<{ block_id: string }>).map((b) => b.block_id)).toEqual([
|
|
243
|
+
"h1",
|
|
244
|
+
"p1",
|
|
245
|
+
"h2",
|
|
246
|
+
"list1",
|
|
247
|
+
"li1",
|
|
248
|
+
"li2",
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
178
252
|
it("falls back to size-based convert chunking for long no-heading markdown", async () => {
|
|
179
253
|
let successChunkCount = 0;
|
|
180
254
|
convertMock.mockImplementation(async ({ data }) => {
|
|
@@ -209,7 +283,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
209
283
|
(_, i) => `line ${i} with enough content to trigger fallback chunking`,
|
|
210
284
|
).join("\n");
|
|
211
285
|
|
|
212
|
-
const result = await feishuDocTool
|
|
286
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
213
287
|
action: "append",
|
|
214
288
|
doc_token: "doc_1",
|
|
215
289
|
content: longMarkdown,
|
|
@@ -263,7 +337,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
263
337
|
"Tail paragraph three with enough text to exceed API limits when combined. ".repeat(8),
|
|
264
338
|
].join("\n");
|
|
265
339
|
|
|
266
|
-
const result = await feishuDocTool
|
|
340
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
267
341
|
action: "append",
|
|
268
342
|
doc_token: "doc_1",
|
|
269
343
|
content: fencedMarkdown,
|
|
@@ -286,7 +360,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
286
360
|
|
|
287
361
|
const feishuDocTool = resolveFeishuDocTool();
|
|
288
362
|
|
|
289
|
-
const result = await feishuDocTool
|
|
363
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
290
364
|
action: "write",
|
|
291
365
|
doc_token: "doc_1",
|
|
292
366
|
content: "",
|
|
@@ -306,7 +380,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
306
380
|
requesterSenderId: "ou_123",
|
|
307
381
|
});
|
|
308
382
|
|
|
309
|
-
const result = await feishuDocTool
|
|
383
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
310
384
|
action: "create",
|
|
311
385
|
title: "Demo",
|
|
312
386
|
});
|
|
@@ -331,7 +405,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
331
405
|
messageChannel: "feishu",
|
|
332
406
|
});
|
|
333
407
|
|
|
334
|
-
const result = await feishuDocTool
|
|
408
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
335
409
|
action: "create",
|
|
336
410
|
title: "Demo",
|
|
337
411
|
});
|
|
@@ -347,7 +421,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
347
421
|
requesterSenderId: "ou_123",
|
|
348
422
|
});
|
|
349
423
|
|
|
350
|
-
const result = await feishuDocTool
|
|
424
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
351
425
|
action: "create",
|
|
352
426
|
title: "Demo",
|
|
353
427
|
grant_to_requester: false,
|
|
@@ -365,7 +439,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
365
439
|
|
|
366
440
|
const feishuDocTool = resolveFeishuDocTool();
|
|
367
441
|
|
|
368
|
-
const result = await feishuDocTool
|
|
442
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
369
443
|
action: "create",
|
|
370
444
|
title: "Demo",
|
|
371
445
|
});
|
|
@@ -381,15 +455,17 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
381
455
|
},
|
|
382
456
|
});
|
|
383
457
|
|
|
384
|
-
|
|
385
|
-
|
|
458
|
+
loadWebMediaMock.mockResolvedValueOnce({
|
|
459
|
+
buffer: Buffer.from("hello from local file", "utf8"),
|
|
460
|
+
fileName: "test-local.txt",
|
|
461
|
+
});
|
|
386
462
|
|
|
387
463
|
const feishuDocTool = resolveFeishuDocTool();
|
|
388
464
|
|
|
389
|
-
const result = await feishuDocTool
|
|
465
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
390
466
|
action: "upload_file",
|
|
391
467
|
doc_token: "doc_1",
|
|
392
|
-
file_path:
|
|
468
|
+
file_path: "/tmp/allowed/test-local.txt",
|
|
393
469
|
filename: "test-local.txt",
|
|
394
470
|
});
|
|
395
471
|
|
|
@@ -397,6 +473,13 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
397
473
|
expect(result.details.file_token).toBe("token_1");
|
|
398
474
|
expect(result.details.file_name).toBe("test-local.txt");
|
|
399
475
|
|
|
476
|
+
// Without workspace-only policy, localRoots stays undefined so loadWebMedia
|
|
477
|
+
// applies its default managed-root access behavior.
|
|
478
|
+
expect(loadWebMediaMock).toHaveBeenCalledWith(
|
|
479
|
+
expect.stringContaining("test-local.txt"),
|
|
480
|
+
expect.objectContaining({ optimizeImages: false, localRoots: undefined }),
|
|
481
|
+
);
|
|
482
|
+
|
|
400
483
|
expect(driveUploadAllMock).toHaveBeenCalledWith(
|
|
401
484
|
expect.objectContaining({
|
|
402
485
|
data: expect.objectContaining({
|
|
@@ -406,8 +489,124 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
406
489
|
}),
|
|
407
490
|
}),
|
|
408
491
|
);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("passes workspace localRoots for upload_file when workspace-only policy is active", async () => {
|
|
495
|
+
blockChildrenCreateMock.mockResolvedValueOnce({
|
|
496
|
+
code: 0,
|
|
497
|
+
data: {
|
|
498
|
+
children: [{ block_type: 23, block_id: "file_block_1" }],
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
loadWebMediaMock.mockResolvedValueOnce({
|
|
503
|
+
buffer: Buffer.from("hello from local file", "utf8"),
|
|
504
|
+
fileName: "test-local.txt",
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const feishuDocTool = resolveFeishuDocTool({
|
|
508
|
+
workspaceDir: "/workspace",
|
|
509
|
+
fsPolicy: { workspaceOnly: true },
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
await executeFeishuDocTool(feishuDocTool, {
|
|
513
|
+
action: "upload_file",
|
|
514
|
+
doc_token: "doc_1",
|
|
515
|
+
file_path: "/tmp/openclaw-1000/test-local.txt",
|
|
516
|
+
filename: "test-local.txt",
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
expect(loadWebMediaMock).toHaveBeenCalledWith(
|
|
520
|
+
expect.stringContaining("test-local.txt"),
|
|
521
|
+
expect.objectContaining({ optimizeImages: false, localRoots: ["/workspace"] }),
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("passes empty localRoots when workspace-only policy is active without workspaceDir", async () => {
|
|
526
|
+
blockChildrenCreateMock.mockResolvedValueOnce({
|
|
527
|
+
code: 0,
|
|
528
|
+
data: {
|
|
529
|
+
children: [{ block_type: 23, block_id: "file_block_1" }],
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
loadWebMediaMock.mockResolvedValueOnce({
|
|
534
|
+
buffer: Buffer.from("hello from local file", "utf8"),
|
|
535
|
+
fileName: "test-local.txt",
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const feishuDocTool = resolveFeishuDocTool({
|
|
539
|
+
fsPolicy: { workspaceOnly: true },
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await executeFeishuDocTool(feishuDocTool, {
|
|
543
|
+
action: "upload_file",
|
|
544
|
+
doc_token: "doc_1",
|
|
545
|
+
file_path: "/tmp/openclaw-1000/test-local.txt",
|
|
546
|
+
filename: "test-local.txt",
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
expect(loadWebMediaMock).toHaveBeenCalledWith(
|
|
550
|
+
expect.stringContaining("test-local.txt"),
|
|
551
|
+
expect.objectContaining({ optimizeImages: false, localRoots: [] }),
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("passes workspace localRoots for upload_image local paths when workspace-only policy is active", async () => {
|
|
556
|
+
loadWebMediaMock.mockResolvedValueOnce({
|
|
557
|
+
buffer: Buffer.from("hello from local file", "utf8"),
|
|
558
|
+
fileName: "test-local.png",
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const feishuDocTool = resolveFeishuDocTool({
|
|
562
|
+
workspaceDir: "/workspace",
|
|
563
|
+
fsPolicy: { workspaceOnly: true },
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
await executeFeishuDocTool(feishuDocTool, {
|
|
567
|
+
action: "upload_image",
|
|
568
|
+
doc_token: "doc_1",
|
|
569
|
+
image: "./test-local.png",
|
|
570
|
+
filename: "test-local.png",
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
expect(loadWebMediaMock).toHaveBeenCalledWith(
|
|
574
|
+
expect.stringContaining("test-local.png"),
|
|
575
|
+
expect.objectContaining({ optimizeImages: false, localRoots: ["/workspace"] }),
|
|
576
|
+
);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("passes workspace localRoots for upload_image absolute local paths when workspace-only policy is active", async () => {
|
|
580
|
+
const fixtureDir = path.join(process.cwd(), ".tmp-docx-upload-image-absolute");
|
|
581
|
+
const absoluteImagePath = path.join(fixtureDir, "absolute-image.png");
|
|
582
|
+
mkdirSync(fixtureDir, { recursive: true });
|
|
583
|
+
writeFileSync(absoluteImagePath, "not-real-image");
|
|
584
|
+
|
|
585
|
+
loadWebMediaMock.mockResolvedValueOnce({
|
|
586
|
+
buffer: Buffer.from("hello from local file", "utf8"),
|
|
587
|
+
fileName: "absolute-image.png",
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const feishuDocTool = resolveFeishuDocTool({
|
|
591
|
+
workspaceDir: "/workspace",
|
|
592
|
+
fsPolicy: { workspaceOnly: true },
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
await executeFeishuDocTool(feishuDocTool, {
|
|
597
|
+
action: "upload_image",
|
|
598
|
+
doc_token: "doc_1",
|
|
599
|
+
image: absoluteImagePath,
|
|
600
|
+
filename: "absolute-image.png",
|
|
601
|
+
});
|
|
409
602
|
|
|
410
|
-
|
|
603
|
+
expect(loadWebMediaMock).toHaveBeenCalledWith(
|
|
604
|
+
expect.stringContaining("absolute-image.png"),
|
|
605
|
+
expect.objectContaining({ optimizeImages: false, localRoots: ["/workspace"] }),
|
|
606
|
+
);
|
|
607
|
+
} finally {
|
|
608
|
+
rmSync(fixtureDir, { recursive: true, force: true });
|
|
609
|
+
}
|
|
411
610
|
});
|
|
412
611
|
|
|
413
612
|
it("returns an error when upload_file cannot list placeholder siblings", async () => {
|
|
@@ -423,23 +622,64 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
423
622
|
data: { items: [] },
|
|
424
623
|
});
|
|
425
624
|
|
|
426
|
-
|
|
427
|
-
|
|
625
|
+
loadWebMediaMock.mockResolvedValueOnce({
|
|
626
|
+
buffer: Buffer.from("hello from local file", "utf8"),
|
|
627
|
+
fileName: "test-local.txt",
|
|
628
|
+
});
|
|
428
629
|
|
|
429
|
-
|
|
430
|
-
const feishuDocTool = resolveFeishuDocTool();
|
|
630
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
431
631
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
632
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
633
|
+
action: "upload_file",
|
|
634
|
+
doc_token: "doc_1",
|
|
635
|
+
file_path: "/tmp/allowed/test-local.txt",
|
|
636
|
+
filename: "test-local.txt",
|
|
637
|
+
});
|
|
438
638
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
639
|
+
expect(result.details.error).toBe("list failed");
|
|
640
|
+
expect(driveUploadAllMock).not.toHaveBeenCalled();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("rejects traversal paths in upload_file via loadWebMedia sandbox", async () => {
|
|
644
|
+
loadWebMediaMock.mockRejectedValueOnce(
|
|
645
|
+
new Error("Local media path is not under an allowed directory: /etc/passwd"),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
649
|
+
|
|
650
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
651
|
+
action: "upload_file",
|
|
652
|
+
doc_token: "doc_1",
|
|
653
|
+
file_path: "/etc/passwd",
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
expect(result.details.error).toContain("not under an allowed directory");
|
|
657
|
+
expect(driveUploadAllMock).not.toHaveBeenCalled();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("rejects traversal paths in upload_image via loadWebMedia sandbox", async () => {
|
|
661
|
+
blockChildrenCreateMock.mockResolvedValueOnce({
|
|
662
|
+
code: 0,
|
|
663
|
+
data: {
|
|
664
|
+
children: [{ block_type: 27, block_id: "img_block_1" }],
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
loadWebMediaMock.mockRejectedValueOnce(
|
|
669
|
+
new Error(
|
|
670
|
+
"Local media path is not under an allowed directory: /home/admin/.openclaw/openclaw.json",
|
|
671
|
+
),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
675
|
+
|
|
676
|
+
const result = await executeFeishuDocTool(feishuDocTool, {
|
|
677
|
+
action: "upload_image",
|
|
678
|
+
doc_token: "doc_1",
|
|
679
|
+
file_path: "/home/admin/.openclaw/openclaw.json",
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
expect(result.details.error).toContain("not under an allowed directory");
|
|
683
|
+
expect(driveUploadAllMock).not.toHaveBeenCalled();
|
|
444
684
|
});
|
|
445
685
|
});
|