diff --git a/CHANGELOG.md b/CHANGELOG.md index 4679c37ee95..4a41290a189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/extensions/feishu/src/doc-schema.ts b/extensions/feishu/src/doc-schema.ts index a7e2577c252..d92173c0ff5 100644 --- a/extensions/feishu/src/doc-schema.ts +++ b/extensions/feishu/src/doc-schema.ts @@ -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.", }), ), diff --git a/extensions/feishu/src/docx.test.ts b/extensions/feishu/src/docx.test.ts index 37a67bf1a44..14e36e09c0a 100644 --- a/extensions/feishu/src/docx.test.ts +++ b/extensions/feishu/src/docx.test.ts @@ -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); + } + }); }); diff --git a/extensions/feishu/src/docx.ts b/extensions/feishu/src/docx.ts index e75f9faab20..a934f1d3aa8 100644 --- a/extensions/feishu/src/docx.ts +++ b/extensions/feishu/src/docx.ts @@ -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