fix(feishu): insert document blocks sequentially to preserve order (#26022) (openclaw#26172) thanks @echoVic
Verified: - pnpm build - pnpm check - pnpm vitest run --config vitest.extensions.config.ts extensions/feishu/src/docx.test.ts Co-authored-by: echoVic <16428813+echoVic@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -105,6 +105,64 @@ describe("feishu_doc image fetch hardening", () => {
|
||||
scopeListMock.mockResolvedValue({ code: 0, data: { scopes: [] } });
|
||||
});
|
||||
|
||||
it("inserts blocks sequentially to preserve document order", async () => {
|
||||
const blocks = [
|
||||
{ block_type: 3, block_id: "h1" },
|
||||
{ block_type: 2, block_id: "t1" },
|
||||
{ block_type: 3, block_id: "h2" },
|
||||
];
|
||||
convertMock.mockResolvedValue({
|
||||
code: 0,
|
||||
data: {
|
||||
blocks,
|
||||
first_level_block_ids: ["h1", "t1", "h2"],
|
||||
},
|
||||
});
|
||||
|
||||
blockListMock.mockResolvedValue({ code: 0, data: { items: [] } });
|
||||
|
||||
// Each call returns the single block that was passed in
|
||||
blockChildrenCreateMock
|
||||
.mockResolvedValueOnce({ code: 0, data: { children: [{ block_type: 3, block_id: "h1" }] } })
|
||||
.mockResolvedValueOnce({ code: 0, data: { children: [{ block_type: 2, block_id: "t1" }] } })
|
||||
.mockResolvedValueOnce({ code: 0, data: { children: [{ block_type: 3, block_id: "h2" }] } });
|
||||
|
||||
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: "append",
|
||||
doc_token: "doc_1",
|
||||
content: "## H1\ntext\n## H2",
|
||||
});
|
||||
|
||||
// Verify sequential insertion: one call per block
|
||||
expect(blockChildrenCreateMock).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Verify each call received exactly one block in the correct order
|
||||
const calls = blockChildrenCreateMock.mock.calls;
|
||||
expect(calls[0][0].data.children).toHaveLength(1);
|
||||
expect(calls[0][0].data.children[0].block_id).toBe("h1");
|
||||
expect(calls[1][0].data.children[0].block_id).toBe("t1");
|
||||
expect(calls[2][0].data.children[0].block_id).toBe("h2");
|
||||
|
||||
expect(result.details.blocks_added).toBe(3);
|
||||
});
|
||||
|
||||
it("skips image upload when markdown image URL is blocked", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
fetchRemoteMediaMock.mockRejectedValueOnce(
|
||||
|
||||
@@ -122,17 +122,25 @@ async function insertBlocks(
|
||||
return { children: [], skipped };
|
||||
}
|
||||
|
||||
const res = await client.docx.documentBlockChildren.create({
|
||||
path: { document_id: docToken, block_id: blockId },
|
||||
data: {
|
||||
children: cleaned,
|
||||
...(index !== undefined && { index }),
|
||||
},
|
||||
});
|
||||
if (res.code !== 0) {
|
||||
throw new Error(res.msg);
|
||||
// Insert blocks one at a time to preserve document order.
|
||||
// The batch API (sending all children at once) does not guarantee ordering
|
||||
// because Feishu processes the batch asynchronously. Sequential single-block
|
||||
// inserts (each appended to the end) produce deterministic results.
|
||||
const allInserted: any[] = [];
|
||||
for (const [offset, block] of cleaned.entries()) {
|
||||
const res = await client.docx.documentBlockChildren.create({
|
||||
path: { document_id: docToken, block_id: blockId },
|
||||
data: {
|
||||
children: [block],
|
||||
...(index !== undefined ? { index: index + offset } : {}),
|
||||
},
|
||||
});
|
||||
if (res.code !== 0) {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
allInserted.push(...(res.data?.children ?? []));
|
||||
}
|
||||
return { children: res.data?.children ?? [], skipped };
|
||||
return { children: allInserted, skipped };
|
||||
}
|
||||
|
||||
async function clearDocumentContent(client: Lark.Client, docToken: string) {
|
||||
|
||||
Reference in New Issue
Block a user