diff --git a/extensions/feishu/src/doc-schema.ts b/extensions/feishu/src/doc-schema.ts index ab657065a69..7fb38b6de61 100644 --- a/extensions/feishu/src/doc-schema.ts +++ b/extensions/feishu/src/doc-schema.ts @@ -1,12 +1,35 @@ import { Type, type Static } from "@sinclair/typebox"; +const FEISHU_DOC_ACTION_VALUES = [ + "read", + "write", + "append", + "insert", + "create", + "list_blocks", + "get_block", + "update_block", + "delete_block", + "create_table", + "write_table_cells", + "create_table_with_values", + "insert_table_row", + "insert_table_column", + "delete_table_rows", + "delete_table_columns", + "merge_table_cells", + "upload_image", + "upload_file", + "color_text", +] as const; + const tableCreationProperties = { - doc_token: Type.String({ description: "Document token" }), + doc_token: Type.Optional(Type.String({ description: "Document token" })), parent_block_id: Type.Optional( Type.String({ description: "Parent block ID (default: document root)" }), ), - row_size: Type.Integer({ description: "Table row count", minimum: 1 }), - column_size: Type.Integer({ description: "Table column count", minimum: 1 }), + row_size: Type.Optional(Type.Integer({ description: "Table row count", minimum: 1 })), + column_size: Type.Optional(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)", @@ -14,169 +37,103 @@ const tableCreationProperties = { ), }; -export const FeishuDocSchema = Type.Union([ - Type.Object({ - action: Type.Literal("read"), - doc_token: Type.String({ description: "Document token (extract from URL /docx/XXX)" }), - }), - Type.Object({ - action: Type.Literal("write"), - doc_token: Type.String({ description: "Document token" }), - content: Type.String({ - description: "Markdown content to write (replaces entire document content)", +export const FeishuDocSchema = Type.Object( + { + action: Type.Unsafe<(typeof FEISHU_DOC_ACTION_VALUES)[number]>({ + type: "string", + enum: [...FEISHU_DOC_ACTION_VALUES], + description: + "Document action to run: read, write, append, insert, create, list_blocks, get_block, update_block, delete_block, create_table, write_table_cells, create_table_with_values, insert_table_row, insert_table_column, delete_table_rows, delete_table_columns, merge_table_cells, upload_image, upload_file, color_text", }), - }), - Type.Object({ - action: Type.Literal("append"), - doc_token: Type.String({ description: "Document token" }), - content: Type.String({ description: "Markdown content to append to end of document" }), - }), - Type.Object({ - action: Type.Literal("insert"), - doc_token: Type.String({ description: "Document token" }), - content: Type.String({ description: "Markdown content to insert" }), - after_block_id: Type.String({ - description: "Insert content after this block ID. Use list_blocks to find block IDs.", - }), - }), - Type.Object({ - action: Type.Literal("create"), - title: Type.String({ description: "Document title" }), - folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })), + doc_token: Type.Optional( + Type.String({ + description: + "Document token. Required for all actions except create. Extract from URL /docx/XXX.", + }), + ), + content: Type.Optional( + Type.String({ + description: + "Markdown or text payload. Required for write, append, insert, update_block, and color_text.", + }), + ), + after_block_id: Type.Optional( + Type.String({ + description: "Required for insert. Insert content after this block ID.", + }), + ), + title: Type.Optional( + Type.String({ + description: "Required for create and rename-style content creation flows.", + }), + ), + folder_token: Type.Optional(Type.String({ description: "Optional target folder for create." })), grant_to_requester: Type.Optional( Type.Boolean({ description: - "Grant edit permission to the trusted requesting Feishu user from runtime context (default: true).", + "For create, grant edit permission to the trusted requesting Feishu user from runtime context.", + }), + ), + block_id: Type.Optional( + Type.String({ + description: + "Block ID. Required for get_block, update_block, delete_block, table row/column operations, merge_table_cells, and color_text.", }), ), - }), - Type.Object({ - action: Type.Literal("list_blocks"), - doc_token: Type.String({ description: "Document token" }), - }), - Type.Object({ - action: Type.Literal("get_block"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Block ID (from list_blocks)" }), - }), - Type.Object({ - action: Type.Literal("update_block"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Block ID (from list_blocks)" }), - content: Type.String({ description: "New text content" }), - }), - Type.Object({ - action: Type.Literal("delete_block"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Block ID" }), - }), - // Table creation (explicit structure) - Type.Object({ - action: Type.Literal("create_table"), ...tableCreationProperties, - }), - Type.Object({ - action: Type.Literal("write_table_cells"), - doc_token: Type.String({ description: "Document token" }), - table_block_id: Type.String({ description: "Table block ID" }), - values: Type.Array(Type.Array(Type.String()), { - description: "2D matrix values[row][col] to write into table cells", - minItems: 1, - }), - }), - Type.Object({ - action: Type.Literal("create_table_with_values"), - ...tableCreationProperties, - values: Type.Array(Type.Array(Type.String()), { - description: "2D matrix values[row][col] to write into table cells", - minItems: 1, - }), - }), - // Table row/column manipulation - Type.Object({ - action: Type.Literal("insert_table_row"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Table block ID" }), + table_block_id: Type.Optional( + Type.String({ description: "Table block ID. Required for write_table_cells." }), + ), + values: Type.Optional( + Type.Array(Type.Array(Type.String()), { + description: + "2D matrix values[row][col]. Required for write_table_cells and create_table_with_values.", + minItems: 1, + }), + ), row_index: Type.Optional( - Type.Number({ description: "Row index to insert at (-1 for end, default: -1)" }), + Type.Number({ description: "Optional row index for insert_table_row (-1 for end)." }), ), - }), - Type.Object({ - action: Type.Literal("insert_table_column"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Table block ID" }), column_index: Type.Optional( - Type.Number({ description: "Column index to insert at (-1 for end, default: -1)" }), + Type.Number({ + description: "Optional column index for insert_table_column (-1 for end).", + }), ), - }), - Type.Object({ - action: Type.Literal("delete_table_rows"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Table block ID" }), - row_start: Type.Number({ description: "Start row index (0-based)" }), - row_count: Type.Optional(Type.Number({ description: "Number of rows to delete (default: 1)" })), - }), - Type.Object({ - action: Type.Literal("delete_table_columns"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Table block ID" }), - column_start: Type.Number({ description: "Start column index (0-based)" }), - column_count: Type.Optional( - Type.Number({ description: "Number of columns to delete (default: 1)" }), + row_start: Type.Optional( + Type.Number({ description: "Start row index. Required for delete/merge row actions." }), + ), + row_count: Type.Optional(Type.Number({ description: "Rows to delete (default: 1)." })), + row_end: Type.Optional( + Type.Number({ description: "End row index (exclusive). Required for merge_table_cells." }), + ), + column_start: Type.Optional( + Type.Number({ description: "Start column index. Required for delete/merge column actions." }), + ), + column_count: Type.Optional(Type.Number({ description: "Columns to delete (default: 1)." })), + column_end: Type.Optional( + Type.Number({ + description: "End column index (exclusive). Required for merge_table_cells.", + }), + ), + url: Type.Optional(Type.String({ description: "Remote file/image URL for upload actions." })), + file_path: Type.Optional( + Type.String({ description: "Local file path for upload_image or upload_file." }), ), - }), - Type.Object({ - action: Type.Literal("merge_table_cells"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Table block ID" }), - row_start: Type.Number({ description: "Start row index" }), - row_end: Type.Number({ description: "End row index (exclusive)" }), - column_start: Type.Number({ description: "Start column index" }), - column_end: Type.Number({ description: "End column index (exclusive)" }), - }), - // Image / file upload - Type.Object({ - action: Type.Literal("upload_image"), - doc_token: Type.String({ description: "Document token" }), - url: Type.Optional(Type.String({ description: "Remote image URL (http/https)" })), - file_path: Type.Optional(Type.String({ description: "Local image file path" })), image: Type.Optional( Type.String({ description: - "Image as data URI (data:image/png;base64,...) or plain base64 string. Use instead of url/file_path for DALL-E outputs, canvas screenshots, etc.", + "Image as data URI or base64 string for upload_image when no URL/file_path is used.", }), ), - parent_block_id: Type.Optional( - Type.String({ description: "Parent block ID (default: document root)" }), - ), - filename: Type.Optional(Type.String({ description: "Optional filename override" })), + filename: Type.Optional(Type.String({ description: "Optional upload filename override." })), index: Type.Optional( Type.Integer({ minimum: 0, - description: "Insert position (0-based index among siblings). Omit to append.", + description: "Optional insert position among siblings for upload_image.", }), ), - }), - Type.Object({ - action: Type.Literal("upload_file"), - doc_token: Type.String({ description: "Document token" }), - url: Type.Optional(Type.String({ description: "Remote file URL (http/https)" })), - file_path: Type.Optional(Type.String({ description: "Local file path" })), - parent_block_id: Type.Optional( - Type.String({ description: "Parent block ID (default: document root)" }), - ), - filename: Type.Optional(Type.String({ description: "Optional filename override" })), - }), - // Text color / style - Type.Object({ - action: Type.Literal("color_text"), - doc_token: Type.String({ description: "Document token" }), - block_id: Type.String({ description: "Text block ID to update" }), - content: Type.String({ - description: - 'Text with color markup. Tags: [red], [green], [blue], [orange], [yellow], [purple], [grey], [bold], [bg:yellow]. Example: "Revenue [green]+15%[/green] YoY"', - }), - }), -]); + }, + { additionalProperties: false }, +); export type FeishuDocParams = Static; diff --git a/extensions/feishu/src/docx.account-selection.test.ts b/extensions/feishu/src/docx.account-selection.test.ts index 1f11e290815..11a28e69def 100644 --- a/extensions/feishu/src/docx.account-selection.test.ts +++ b/extensions/feishu/src/docx.account-selection.test.ts @@ -67,4 +67,26 @@ describe("feishu_doc account selection", () => { expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-a"); }); + + test("registers provider-friendly feishu_doc parameters for embedded sessions", () => { + const cfg = createDocEnabledConfig(); + const { api, resolveTool } = createToolFactoryHarness(cfg); + registerFeishuDocTools(api); + + const docTool = resolveTool("feishu_doc", { agentAccountId: "a" }); + const schema = docTool.parameters as { + type?: unknown; + anyOf?: unknown; + properties?: Record; + required?: unknown; + }; + + expect(schema.type).toBe("object"); + expect(schema.anyOf).toBeUndefined(); + expect(schema.required).toEqual(["action"]); + expect(schema.properties?.action?.enum).toEqual( + expect.arrayContaining(["read", "create", "list_blocks", "upload_image"]), + ); + expect(schema.properties?.action?.description).toEqual(expect.stringContaining("create_table")); + }); }); diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index b5697676493..059118868e2 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -69,6 +69,31 @@ describe("feishu tool account routing", () => { expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-b"); }); + test("wiki tool exposes object parameters with explicit action enum", () => { + const { api, resolveTool } = createToolFactoryHarness( + createConfig({ + toolsA: { wiki: true }, + }), + ); + registerFeishuWikiTools(api); + + const tool = resolveTool("feishu_wiki", { agentAccountId: "a" }); + const schema = tool.parameters as { + type?: unknown; + anyOf?: unknown; + properties?: Record; + required?: unknown; + }; + + expect(schema.type).toBe("object"); + expect(schema.anyOf).toBeUndefined(); + expect(schema.required).toEqual(["action"]); + expect(schema.properties?.action?.enum).toEqual( + expect.arrayContaining(["spaces", "nodes", "get", "create", "rename"]), + ); + expect(schema.properties?.action?.description).toEqual(expect.stringContaining("search")); + }); + test("wiki tool prefers configured defaultAccount over inherited default account context", async () => { const { api, resolveTool } = createToolFactoryHarness( createConfig({ diff --git a/extensions/feishu/src/tool-factory-test-harness.ts b/extensions/feishu/src/tool-factory-test-harness.ts index f5bd19672dd..4ae478cdacb 100644 --- a/extensions/feishu/src/tool-factory-test-harness.ts +++ b/extensions/feishu/src/tool-factory-test-harness.ts @@ -8,6 +8,8 @@ type ToolFactoryLike = (ctx: ToolContextLike) => AnyAgentTool | AnyAgentTool[] | export type ToolLike = { name: string; + description?: string; + parameters?: unknown; execute: (toolCallId: string, params: unknown) => Promise | unknown; }; @@ -30,6 +32,8 @@ function asToolLike(tool: AnyAgentTool, fallbackName?: string): ToolLike { } return { name, + description: candidate.description, + parameters: candidate.parameters, execute: (toolCallId, params) => execute(toolCallId, params), }; } diff --git a/extensions/feishu/src/wiki-schema.ts b/extensions/feishu/src/wiki-schema.ts index 006cc2da39d..db249e8cf20 100644 --- a/extensions/feishu/src/wiki-schema.ts +++ b/extensions/feishu/src/wiki-schema.ts @@ -1,55 +1,60 @@ import { Type, type Static } from "@sinclair/typebox"; -export const FeishuWikiSchema = Type.Union([ - Type.Object({ - action: Type.Literal("spaces"), - }), - Type.Object({ - action: Type.Literal("nodes"), - space_id: Type.String({ description: "Knowledge space ID" }), - parent_node_token: Type.Optional( - Type.String({ description: "Parent node token (optional, omit for root)" }), - ), - }), - Type.Object({ - action: Type.Literal("get"), - token: Type.String({ description: "Wiki node token (from URL /wiki/XXX)" }), - }), - Type.Object({ - action: Type.Literal("search"), - query: Type.String({ description: "Search query" }), - space_id: Type.Optional(Type.String({ description: "Limit search to this space (optional)" })), - }), - Type.Object({ - action: Type.Literal("create"), - space_id: Type.String({ description: "Knowledge space ID" }), - title: Type.String({ description: "Node title" }), - obj_type: Type.Optional( - Type.Union([Type.Literal("docx"), Type.Literal("sheet"), Type.Literal("bitable")], { - description: "Object type (default: docx)", +const FEISHU_WIKI_ACTION_VALUES = [ + "spaces", + "nodes", + "get", + "search", + "create", + "move", + "rename", +] as const; +const FEISHU_WIKI_OBJECT_TYPE_VALUES = ["docx", "sheet", "bitable"] as const; + +export const FeishuWikiSchema = Type.Object( + { + action: Type.Unsafe<(typeof FEISHU_WIKI_ACTION_VALUES)[number]>({ + type: "string", + enum: [...FEISHU_WIKI_ACTION_VALUES], + description: "Wiki action to run: spaces, nodes, get, search, create, move, rename", + }), + space_id: Type.Optional( + Type.String({ + description: "Knowledge space ID. Required for nodes, create, move, and rename.", }), ), parent_node_token: Type.Optional( - Type.String({ description: "Parent node token (optional, omit for root)" }), + Type.String({ + description: "Optional parent node token for nodes/create. Omit for the root level.", + }), + ), + token: Type.Optional( + Type.String({ description: "Wiki node token. Required for get." }), + ), + query: Type.Optional( + Type.String({ description: "Search query. Required for search." }), + ), + title: Type.Optional( + Type.String({ description: "Node title. Required for create and rename." }), + ), + obj_type: Type.Optional( + Type.Unsafe<(typeof FEISHU_WIKI_OBJECT_TYPE_VALUES)[number]>({ + type: "string", + enum: [...FEISHU_WIKI_OBJECT_TYPE_VALUES], + description: "Object type for create (default: docx).", + }), + ), + node_token: Type.Optional( + Type.String({ description: "Node token. Required for move and rename." }), ), - }), - Type.Object({ - action: Type.Literal("move"), - space_id: Type.String({ description: "Source knowledge space ID" }), - node_token: Type.String({ description: "Node token to move" }), target_space_id: Type.Optional( - Type.String({ description: "Target space ID (optional, same space if omitted)" }), + Type.String({ description: "Optional target space for move." }), ), target_parent_token: Type.Optional( - Type.String({ description: "Target parent node token (optional, root if omitted)" }), + Type.String({ description: "Optional target parent node token for move." }), ), - }), - Type.Object({ - action: Type.Literal("rename"), - space_id: Type.String({ description: "Knowledge space ID" }), - node_token: Type.String({ description: "Node token to rename" }), - title: Type.String({ description: "New title" }), - }), -]); + }, + { additionalProperties: false }, +); export type FeishuWikiParams = Static;