fix(feishu): simplify embedded tool schemas

This commit is contained in:
MumuTW 2026-03-10 14:01:43 +00:00
parent 53fb317e7f
commit 782779f0df
5 changed files with 201 additions and 188 deletions

View File

@ -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<typeof FeishuDocSchema>;

View File

@ -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<string, { enum?: unknown; description?: unknown }>;
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"));
});
});

View File

@ -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<string, { enum?: unknown; description?: unknown }>;
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({

View File

@ -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> | 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),
};
}

View File

@ -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<typeof FeishuWikiSchema>;