mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 02:25:02 +00:00
feat(feishu): support Docx table create/write + image/file upload actions in feishu_doc (#20304)
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
||||
@@ -24,6 +27,8 @@ describe("feishu_doc image fetch hardening", () => {
|
||||
const documentCreateMock = vi.hoisted(() => vi.fn());
|
||||
const blockListMock = vi.hoisted(() => vi.fn());
|
||||
const blockChildrenCreateMock = vi.hoisted(() => vi.fn());
|
||||
const blockChildrenGetMock = vi.hoisted(() => vi.fn());
|
||||
const blockChildrenBatchDeleteMock = vi.hoisted(() => vi.fn());
|
||||
const driveUploadAllMock = vi.hoisted(() => vi.fn());
|
||||
const permissionMemberCreateMock = vi.hoisted(() => vi.fn());
|
||||
const blockPatchMock = vi.hoisted(() => vi.fn());
|
||||
@@ -44,6 +49,8 @@ describe("feishu_doc image fetch hardening", () => {
|
||||
},
|
||||
documentBlockChildren: {
|
||||
create: blockChildrenCreateMock,
|
||||
get: blockChildrenGetMock,
|
||||
batchDelete: blockChildrenBatchDeleteMock,
|
||||
},
|
||||
},
|
||||
drive: {
|
||||
@@ -83,6 +90,11 @@ describe("feishu_doc image fetch hardening", () => {
|
||||
},
|
||||
});
|
||||
|
||||
blockChildrenGetMock.mockResolvedValue({
|
||||
code: 0,
|
||||
data: { items: [{ block_id: "placeholder_block_1" }] },
|
||||
});
|
||||
blockChildrenBatchDeleteMock.mockResolvedValue({ code: 0 });
|
||||
driveUploadAllMock.mockResolvedValue({ file_token: "token_1" });
|
||||
documentCreateMock.mockResolvedValue({
|
||||
code: 0,
|
||||
@@ -235,4 +247,144 @@ describe("feishu_doc image fetch hardening", () => {
|
||||
expect(permissionMemberCreateMock).not.toHaveBeenCalled();
|
||||
expect(result.details.owner_permission_added).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns an error when create response omits document_id", async () => {
|
||||
documentCreateMock.mockResolvedValueOnce({
|
||||
code: 0,
|
||||
data: { document: { title: "Created Doc" } },
|
||||
});
|
||||
|
||||
const registerTool = vi.fn();
|
||||
registerFeishuDocTools({
|
||||
config: {
|
||||
channels: {
|
||||
feishu: {
|
||||
appId: "app_id",
|
||||
appSecret: "app_secret",
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
||||
registerTool,
|
||||
} as any);
|
||||
|
||||
const feishuDocTool = registerTool.mock.calls
|
||||
.map((call) => call[0])
|
||||
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
||||
.find((tool) => tool.name === "feishu_doc");
|
||||
expect(feishuDocTool).toBeDefined();
|
||||
|
||||
const result = await feishuDocTool.execute("tool-call", {
|
||||
action: "create",
|
||||
title: "Demo",
|
||||
});
|
||||
|
||||
expect(result.details.error).toContain("no document_id");
|
||||
});
|
||||
|
||||
it("uploads local file to doc via upload_file action", async () => {
|
||||
blockChildrenCreateMock.mockResolvedValueOnce({
|
||||
code: 0,
|
||||
data: {
|
||||
children: [{ block_type: 23, block_id: "file_block_1" }],
|
||||
},
|
||||
});
|
||||
|
||||
const localPath = join(tmpdir(), `feishu-docx-upload-${Date.now()}.txt`);
|
||||
await fs.writeFile(localPath, "hello from local file", "utf8");
|
||||
|
||||
const registerTool = vi.fn();
|
||||
registerFeishuDocTools({
|
||||
config: {
|
||||
channels: {
|
||||
feishu: {
|
||||
appId: "app_id",
|
||||
appSecret: "app_secret",
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
||||
registerTool,
|
||||
} as any);
|
||||
|
||||
const feishuDocTool = registerTool.mock.calls
|
||||
.map((call) => call[0])
|
||||
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
||||
.find((tool) => tool.name === "feishu_doc");
|
||||
expect(feishuDocTool).toBeDefined();
|
||||
|
||||
const result = await feishuDocTool.execute("tool-call", {
|
||||
action: "upload_file",
|
||||
doc_token: "doc_1",
|
||||
file_path: localPath,
|
||||
filename: "test-local.txt",
|
||||
});
|
||||
|
||||
expect(result.details.success).toBe(true);
|
||||
expect(result.details.file_token).toBe("token_1");
|
||||
expect(result.details.file_name).toBe("test-local.txt");
|
||||
|
||||
expect(driveUploadAllMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
parent_type: "docx_file",
|
||||
parent_node: "doc_1",
|
||||
file_name: "test-local.txt",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await fs.unlink(localPath);
|
||||
});
|
||||
|
||||
it("returns an error when upload_file cannot list placeholder siblings", async () => {
|
||||
blockChildrenCreateMock.mockResolvedValueOnce({
|
||||
code: 0,
|
||||
data: {
|
||||
children: [{ block_type: 23, block_id: "file_block_1" }],
|
||||
},
|
||||
});
|
||||
blockChildrenGetMock.mockResolvedValueOnce({
|
||||
code: 999,
|
||||
msg: "list failed",
|
||||
data: { items: [] },
|
||||
});
|
||||
|
||||
const localPath = join(tmpdir(), `feishu-docx-upload-fail-${Date.now()}.txt`);
|
||||
await fs.writeFile(localPath, "hello from local file", "utf8");
|
||||
|
||||
try {
|
||||
const registerTool = vi.fn();
|
||||
registerFeishuDocTools({
|
||||
config: {
|
||||
channels: {
|
||||
feishu: {
|
||||
appId: "app_id",
|
||||
appSecret: "app_secret",
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
||||
registerTool,
|
||||
} as any);
|
||||
|
||||
const feishuDocTool = registerTool.mock.calls
|
||||
.map((call) => call[0])
|
||||
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
||||
.find((tool) => tool.name === "feishu_doc");
|
||||
expect(feishuDocTool).toBeDefined();
|
||||
|
||||
const result = await feishuDocTool.execute("tool-call", {
|
||||
action: "upload_file",
|
||||
doc_token: "doc_1",
|
||||
file_path: localPath,
|
||||
filename: "test-local.txt",
|
||||
});
|
||||
|
||||
expect(result.details.error).toBe("list failed");
|
||||
expect(driveUploadAllMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await fs.unlink(localPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user