Merge 94517cfa7096a1fc1e156d2ae631d36a5a0fe9dd into 9fb78453e088cd7b553d7779faa0de5c83708e70
This commit is contained in:
commit
12dc8ef2c1
@ -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>;
|
||||||
|
|||||||
@ -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"),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user