Feishu: harden docx create/upload failure handling

This commit is contained in:
Tak Hoffman
2026-02-27 17:59:17 -06:00
parent 16e37054ea
commit 158ad850e9
4 changed files with 102 additions and 6 deletions

View File

@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
- Android/Nodes: add `camera.list`, `device.permissions`, `device.health`, and `notifications.actions` (`open`/`dismiss`/`reply`) on Android nodes, plus first-class node-tool actions for the new device/notification commands. (#28260) Thanks @obviyus.
- Android/Gateway capability refresh: add live Android capability integration coverage and node canvas capability refresh wiring, plus runtime hardening for A2UI readiness retries, scoped canvas URL normalization, debug diagnostics JSON, and JavaScript MIME delivery. (#28388) Thanks @obviyus.
- Feishu/Doc permissions: support optional owner permission grant fields on `feishu_doc` create and report permission metadata only when the grant call succeeds, with regression coverage for success/failure/omitted-owner paths. (#28295) Thanks @zhoulongchao77.
- Feishu/Docx tables + uploads: add `feishu_doc` actions for Docx table creation/cell writing (`create_table`, `write_table_cells`, `create_table_with_values`) and image/file uploads (`upload_image`, `upload_file`) with stricter create/upload error handling for missing `document_id` and placeholder cleanup failures. (#20304) Thanks @xuhao1.
### Fixes

View File

@@ -56,8 +56,8 @@ export const FeishuDocSchema = Type.Union([
parent_block_id: Type.Optional(
Type.String({ description: "Parent block ID (default: document root)" }),
),
row_size: Type.Number({ description: "Table row count", minimum: 1 }),
column_size: Type.Number({ description: "Table column count", minimum: 1 }),
row_size: Type.Integer({ description: "Table row count", minimum: 1 }),
column_size: Type.Integer({ description: "Table column count", minimum: 1 }),
column_width: Type.Optional(
Type.Array(Type.Number({ minimum: 1 }), {
description: "Column widths in px (length should match column_size)",
@@ -79,8 +79,8 @@ export const FeishuDocSchema = Type.Union([
parent_block_id: Type.Optional(
Type.String({ description: "Parent block ID (default: document root)" }),
),
row_size: Type.Number({ description: "Table row count", minimum: 1 }),
column_size: Type.Number({ description: "Table column count", minimum: 1 }),
row_size: Type.Integer({ description: "Table row count", minimum: 1 }),
column_size: Type.Integer({ description: "Table column count", minimum: 1 }),
column_width: Type.Optional(
Type.Array(Type.Number({ minimum: 1 }), {
description: "Column widths in px (length should match column_size)",
@@ -101,7 +101,8 @@ export const FeishuDocSchema = Type.Union([
),
filename: Type.Optional(Type.String({ description: "Optional filename override" })),
index: Type.Optional(
Type.Number({
Type.Integer({
minimum: 0,
description: "Insert position (0-based index among siblings). Omit to append.",
}),
),

View File

@@ -248,6 +248,40 @@ describe("feishu_doc image fetch hardening", () => {
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,
@@ -302,4 +336,55 @@ describe("feishu_doc image fetch hardening", () => {
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);
}
});
});

View File

@@ -349,16 +349,22 @@ async function uploadFileBlock(
const childrenRes = await client.docx.documentBlockChildren.get({
path: { document_id: docToken, block_id: parentId },
});
if (childrenRes.code !== 0) {
throw new Error(childrenRes.msg);
}
const items = childrenRes.data?.items ?? [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
const placeholderIdx = items.findIndex(
(item: any) => item.block_id === placeholderBlock.block_id,
);
if (placeholderIdx >= 0) {
await client.docx.documentBlockChildren.batchDelete({
const deleteRes = await client.docx.documentBlockChildren.batchDelete({
path: { document_id: docToken, block_id: parentId },
data: { start_index: placeholderIdx, end_index: placeholderIdx + 1 },
});
if (deleteRes.code !== 0) {
throw new Error(deleteRes.msg);
}
}
// Upload file to Feishu drive
@@ -446,6 +452,9 @@ async function createDoc(
}
const doc = res.data?.document;
const docToken = doc?.document_id;
if (!docToken) {
throw new Error("Document creation succeeded but no document_id was returned");
}
let ownerPermissionAdded = false;
// Auto add owner permission if ownerOpenId is provided