Merge 94517cfa7096a1fc1e156d2ae631d36a5a0fe9dd into 9fb78453e088cd7b553d7779faa0de5c83708e70

This commit is contained in:
MumuTW 2026-03-21 05:08:46 +00:00 committed by GitHub
commit 12dc8ef2c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 311 additions and 220 deletions

View File

@ -1,12 +1,34 @@
import { Type, type Static } from "@sinclair/typebox"; 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 = { const tableCreationProperties = {
doc_token: Type.String({ description: "Document token" }),
parent_block_id: Type.Optional( parent_block_id: Type.Optional(
Type.String({ description: "Parent block ID (default: document root)" }), Type.String({ description: "Parent block ID (default: document root)" }),
), ),
row_size: Type.Integer({ description: "Table row count", minimum: 1 }), row_size: Type.Optional(Type.Integer({ description: "Table row count", minimum: 1 })),
column_size: Type.Integer({ description: "Table column count", minimum: 1 }), column_size: Type.Optional(Type.Integer({ description: "Table column count", minimum: 1 })),
column_width: Type.Optional( column_width: Type.Optional(
Type.Array(Type.Number({ minimum: 1 }), { Type.Array(Type.Number({ minimum: 1 }), {
description: "Column widths in px (length should match column_size)", description: "Column widths in px (length should match column_size)",
@ -14,169 +36,103 @@ const tableCreationProperties = {
), ),
}; };
export const FeishuDocSchema = Type.Union([ export const FeishuDocSchema = Type.Object(
Type.Object({ {
action: Type.Literal("read"), action: Type.Unsafe<(typeof FEISHU_DOC_ACTION_VALUES)[number]>({
doc_token: Type.String({ description: "Document token (extract from URL /docx/XXX)" }), type: "string",
}), enum: [...FEISHU_DOC_ACTION_VALUES],
Type.Object({ description:
action: Type.Literal("write"), "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",
doc_token: Type.String({ description: "Document token" }),
content: Type.String({
description: "Markdown content to write (replaces entire document content)",
}), }),
}), doc_token: Type.Optional(
Type.Object({ Type.String({
action: Type.Literal("append"), description:
doc_token: Type.String({ description: "Document token" }), "Document token. Required for all actions except create. Extract from URL /docx/XXX.",
content: Type.String({ description: "Markdown content to append to end of document" }), }),
}), ),
Type.Object({ content: Type.Optional(
action: Type.Literal("insert"), Type.String({
doc_token: Type.String({ description: "Document token" }), description:
content: Type.String({ description: "Markdown content to insert" }), "Markdown or text payload. Required for write, append, insert, update_block, and color_text.",
after_block_id: Type.String({ }),
description: "Insert content after this block ID. Use list_blocks to find block IDs.", ),
}), after_block_id: Type.Optional(
}), Type.String({
Type.Object({ description: "Required for insert. Insert content after this block ID.",
action: Type.Literal("create"), }),
title: Type.String({ description: "Document title" }), ),
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })), 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( grant_to_requester: Type.Optional(
Type.Boolean({ Type.Boolean({
description: 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, ...tableCreationProperties,
}), table_block_id: Type.Optional(
Type.Object({ Type.String({ description: "Table block ID. Required for write_table_cells." }),
action: Type.Literal("write_table_cells"), ),
doc_token: Type.String({ description: "Document token" }), values: Type.Optional(
table_block_id: Type.String({ description: "Table block ID" }), Type.Array(Type.Array(Type.String()), {
values: Type.Array(Type.Array(Type.String()), { description:
description: "2D matrix values[row][col] to write into table cells", "2D matrix values[row][col]. Required for write_table_cells and create_table_with_values.",
minItems: 1, 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" }),
row_index: Type.Optional( 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( 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).",
}),
), ),
}), row_start: Type.Optional(
Type.Object({ Type.Number({ description: "Start row index. Required for delete/merge row actions." }),
action: Type.Literal("delete_table_rows"), ),
doc_token: Type.String({ description: "Document token" }), row_count: Type.Optional(Type.Number({ description: "Rows to delete (default: 1)." })),
block_id: Type.String({ description: "Table block ID" }), row_end: Type.Optional(
row_start: Type.Number({ description: "Start row index (0-based)" }), Type.Number({ description: "End row index (exclusive). Required for merge_table_cells." }),
row_count: Type.Optional(Type.Number({ description: "Number of rows to delete (default: 1)" })), ),
}), column_start: Type.Optional(
Type.Object({ Type.Number({ description: "Start column index. Required for delete/merge column actions." }),
action: Type.Literal("delete_table_columns"), ),
doc_token: Type.String({ description: "Document token" }), column_count: Type.Optional(Type.Number({ description: "Columns to delete (default: 1)." })),
block_id: Type.String({ description: "Table block ID" }), column_end: Type.Optional(
column_start: Type.Number({ description: "Start column index (0-based)" }), Type.Number({
column_count: Type.Optional( description: "End column index (exclusive). Required for merge_table_cells.",
Type.Number({ description: "Number of columns to delete (default: 1)" }), }),
),
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( image: Type.Optional(
Type.String({ Type.String({
description: 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( filename: Type.Optional(Type.String({ description: "Optional upload filename override." })),
Type.String({ description: "Parent block ID (default: document root)" }),
),
filename: Type.Optional(Type.String({ description: "Optional filename override" })),
index: Type.Optional( index: Type.Optional(
Type.Integer({ Type.Integer({
minimum: 0, minimum: 0,
description: "Insert position (0-based index among siblings). Omit to append.", description: "Optional insert position among siblings for upload_image.",
}), }),
), ),
}), },
Type.Object({ { additionalProperties: false },
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"',
}),
}),
]);
export type FeishuDocParams = Static<typeof FeishuDocSchema>; export type FeishuDocParams = Static<typeof FeishuDocSchema>;

View File

@ -67,4 +67,29 @@ describe("feishu_doc account selection", () => {
expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-a"); 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"));
expect(schema.properties?.doc_token?.description).toEqual(
expect.stringContaining("Required for all actions except create"),
);
});
}); });

View File

@ -33,6 +33,13 @@ function json(data: unknown) {
}; };
} }
function requireParam(value: string | undefined, label: string): string {
if (value) {
return value;
}
throw new Error(`${label} is required for this action.`);
}
/** Extract image URLs from markdown content */ /** Extract image URLs from markdown content */
function extractImageUrls(markdown: string): string[] { function extractImageUrls(markdown: string): string[] {
const regex = /!\[[^\]]*\]\(([^)]+)\)/g; const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
@ -1276,13 +1283,13 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
const client = getClient(p, defaultAccountId); const client = getClient(p, defaultAccountId);
switch (p.action) { switch (p.action) {
case "read": case "read":
return json(await readDoc(client, p.doc_token)); return json(await readDoc(client, requireParam(p.doc_token, "doc_token")));
case "write": case "write":
return json( return json(
await writeDoc( await writeDoc(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
p.content, requireParam(p.content, "content"),
getMediaMaxBytes(p, defaultAccountId), getMediaMaxBytes(p, defaultAccountId),
api.logger, api.logger,
), ),
@ -1291,8 +1298,8 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
return json( return json(
await appendDoc( await appendDoc(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
p.content, requireParam(p.content, "content"),
getMediaMaxBytes(p, defaultAccountId), getMediaMaxBytes(p, defaultAccountId),
api.logger, api.logger,
), ),
@ -1301,9 +1308,9 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
return json( return json(
await insertDoc( await insertDoc(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
p.content, requireParam(p.content, "content"),
p.after_block_id, requireParam(p.after_block_id, "after_block_id"),
getMediaMaxBytes(p, defaultAccountId), getMediaMaxBytes(p, defaultAccountId),
api.logger, api.logger,
), ),
@ -1316,18 +1323,37 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
}), }),
); );
case "list_blocks": case "list_blocks":
return json(await listBlocks(client, p.doc_token)); return json(await listBlocks(client, requireParam(p.doc_token, "doc_token")));
case "get_block": case "get_block":
return json(await getBlock(client, p.doc_token, p.block_id)); return json(
await getBlock(
client,
requireParam(p.doc_token, "doc_token"),
requireParam(p.block_id, "block_id"),
),
);
case "update_block": case "update_block":
return json(await updateBlock(client, p.doc_token, p.block_id, p.content)); return json(
await updateBlock(
client,
requireParam(p.doc_token, "doc_token"),
requireParam(p.block_id, "block_id"),
requireParam(p.content, "content"),
),
);
case "delete_block": case "delete_block":
return json(await deleteBlock(client, p.doc_token, p.block_id)); return json(
await deleteBlock(
client,
requireParam(p.doc_token, "doc_token"),
requireParam(p.block_id, "block_id"),
),
);
case "create_table": case "create_table":
return json( return json(
await createTable( await createTable(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
p.row_size, p.row_size,
p.column_size, p.column_size,
p.parent_block_id, p.parent_block_id,
@ -1336,13 +1362,18 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
); );
case "write_table_cells": case "write_table_cells":
return json( return json(
await writeTableCells(client, p.doc_token, p.table_block_id, p.values), await writeTableCells(
client,
requireParam(p.doc_token, "doc_token"),
requireParam(p.table_block_id, "table_block_id"),
p.values,
),
); );
case "create_table_with_values": case "create_table_with_values":
return json( return json(
await createTableWithValues( await createTableWithValues(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
p.row_size, p.row_size,
p.column_size, p.column_size,
p.values, p.values,
@ -1354,7 +1385,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
return json( return json(
await uploadImageBlock( await uploadImageBlock(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
getMediaMaxBytes(p, defaultAccountId), getMediaMaxBytes(p, defaultAccountId),
p.url, p.url,
p.file_path, p.file_path,
@ -1368,7 +1399,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
return json( return json(
await uploadFileBlock( await uploadFileBlock(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
getMediaMaxBytes(p, defaultAccountId), getMediaMaxBytes(p, defaultAccountId),
p.url, p.url,
p.file_path, p.file_path,
@ -1377,19 +1408,38 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
), ),
); );
case "color_text": case "color_text":
return json(await updateColorText(client, p.doc_token, p.block_id, p.content)); return json(
await updateColorText(
client,
requireParam(p.doc_token, "doc_token"),
requireParam(p.block_id, "block_id"),
requireParam(p.content, "content"),
),
);
case "insert_table_row": case "insert_table_row":
return json(await insertTableRow(client, p.doc_token, p.block_id, p.row_index)); return json(
await insertTableRow(
client,
requireParam(p.doc_token, "doc_token"),
requireParam(p.block_id, "block_id"),
p.row_index,
),
);
case "insert_table_column": case "insert_table_column":
return json( return json(
await insertTableColumn(client, p.doc_token, p.block_id, p.column_index), await insertTableColumn(
client,
requireParam(p.doc_token, "doc_token"),
requireParam(p.block_id, "block_id"),
p.column_index,
),
); );
case "delete_table_rows": case "delete_table_rows":
return json( return json(
await deleteTableRows( await deleteTableRows(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
p.block_id, requireParam(p.block_id, "block_id"),
p.row_start, p.row_start,
p.row_count, p.row_count,
), ),
@ -1398,8 +1448,8 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
return json( return json(
await deleteTableColumns( await deleteTableColumns(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
p.block_id, requireParam(p.block_id, "block_id"),
p.column_start, p.column_start,
p.column_count, p.column_count,
), ),
@ -1408,8 +1458,8 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
return json( return json(
await mergeTableCells( await mergeTableCells(
client, client,
p.doc_token, requireParam(p.doc_token, "doc_token"),
p.block_id, requireParam(p.block_id, "block_id"),
p.row_start, p.row_start,
p.row_end, p.row_end,
p.column_start, p.column_start,

View File

@ -69,6 +69,31 @@ describe("feishu tool account routing", () => {
expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-b"); 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 () => { test("wiki tool prefers configured defaultAccount over inherited default account context", async () => {
const { api, resolveTool } = createToolFactoryHarness( const { api, resolveTool } = createToolFactoryHarness(
createConfig({ createConfig({

View File

@ -8,6 +8,8 @@ type ToolFactoryLike = (ctx: ToolContextLike) => AnyAgentTool | AnyAgentTool[] |
export type ToolLike = { export type ToolLike = {
name: string; name: string;
description?: string;
parameters?: unknown;
execute: (toolCallId: string, params: unknown) => Promise<unknown> | unknown; execute: (toolCallId: string, params: unknown) => Promise<unknown> | unknown;
}; };
@ -30,6 +32,8 @@ function asToolLike(tool: AnyAgentTool, fallbackName?: string): ToolLike {
} }
return { return {
name, name,
description: candidate.description,
parameters: candidate.parameters,
execute: (toolCallId, params) => execute(toolCallId, params), execute: (toolCallId, params) => execute(toolCallId, params),
}; };
} }

View File

@ -1,55 +1,60 @@
import { Type, type Static } from "@sinclair/typebox"; import { Type, type Static } from "@sinclair/typebox";
export const FeishuWikiSchema = Type.Union([ const FEISHU_WIKI_ACTION_VALUES = [
Type.Object({ "spaces",
action: Type.Literal("spaces"), "nodes",
}), "get",
Type.Object({ "search",
action: Type.Literal("nodes"), "create",
space_id: Type.String({ description: "Knowledge space ID" }), "move",
parent_node_token: Type.Optional( "rename",
Type.String({ description: "Parent node token (optional, omit for root)" }), ] as const;
), const FEISHU_WIKI_OBJECT_TYPE_VALUES = ["docx", "sheet", "bitable"] as const;
}),
Type.Object({ export const FeishuWikiSchema = Type.Object(
action: Type.Literal("get"), {
token: Type.String({ description: "Wiki node token (from URL /wiki/XXX)" }), action: Type.Unsafe<(typeof FEISHU_WIKI_ACTION_VALUES)[number]>({
}), type: "string",
Type.Object({ enum: [...FEISHU_WIKI_ACTION_VALUES],
action: Type.Literal("search"), description: "Wiki action to run: spaces, nodes, get, search, create, move, rename",
query: Type.String({ description: "Search query" }), }),
space_id: Type.Optional(Type.String({ description: "Limit search to this space (optional)" })), space_id: Type.Optional(
}), Type.String({
Type.Object({ description: "Knowledge space ID. Required for nodes, create, move, and rename.",
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)",
}), }),
), ),
parent_node_token: Type.Optional( 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( 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( 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({ { additionalProperties: false },
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" }),
}),
]);
export type FeishuWikiParams = Static<typeof FeishuWikiSchema>; export type FeishuWikiParams = Static<typeof FeishuWikiSchema>;

View File

@ -17,6 +17,13 @@ const WIKI_ACCESS_HINT =
"To grant wiki access: Open wiki space → Settings → Members → Add the bot. " + "To grant wiki access: Open wiki space → Settings → Members → Add the bot. " +
"See: https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa#a40ad4ca"; "See: https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa#a40ad4ca";
function requireParam(value: string | undefined, label: string): string {
if (value) {
return value;
}
throw new Error(`${label} is required for this action.`);
}
async function listSpaces(client: Lark.Client) { async function listSpaces(client: Lark.Client) {
const res = await client.wiki.space.list({}); const res = await client.wiki.space.list({});
if (res.code !== 0) { if (res.code !== 0) {
@ -192,9 +199,15 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
case "spaces": case "spaces":
return jsonToolResult(await listSpaces(client)); return jsonToolResult(await listSpaces(client));
case "nodes": case "nodes":
return jsonToolResult(await listNodes(client, p.space_id, p.parent_node_token)); return jsonToolResult(
await listNodes(
client,
requireParam(p.space_id, "space_id"),
p.parent_node_token,
),
);
case "get": case "get":
return jsonToolResult(await getNode(client, p.token)); return jsonToolResult(await getNode(client, requireParam(p.token, "token")));
case "search": case "search":
return jsonToolResult({ return jsonToolResult({
error: error:
@ -202,20 +215,33 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
}); });
case "create": case "create":
return jsonToolResult( return jsonToolResult(
await createNode(client, p.space_id, p.title, p.obj_type, p.parent_node_token), await createNode(
client,
requireParam(p.space_id, "space_id"),
requireParam(p.title, "title"),
p.obj_type,
p.parent_node_token,
),
); );
case "move": case "move":
return jsonToolResult( return jsonToolResult(
await moveNode( await moveNode(
client, client,
p.space_id, requireParam(p.space_id, "space_id"),
p.node_token, requireParam(p.node_token, "node_token"),
p.target_space_id, p.target_space_id,
p.target_parent_token, p.target_parent_token,
), ),
); );
case "rename": case "rename":
return jsonToolResult(await renameNode(client, p.space_id, p.node_token, p.title)); return jsonToolResult(
await renameNode(
client,
requireParam(p.space_id, "space_id"),
requireParam(p.node_token, "node_token"),
requireParam(p.title, "title"),
),
);
default: default:
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback // eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
return unknownToolActionResult((p as { action?: unknown }).action); return unknownToolActionResult((p as { action?: unknown }).action);