mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-20 23:14:59 +00:00
Feishu: harden docx create/upload failure handling
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.",
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user