import type * as Lark from "@larksuiteoapi/node-sdk"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { Type, type TSchema } from "typebox"; import type { OpenClawPluginApi } from "../runtime-api.js"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { createFeishuToolClient } from "./tool-account.js";
/** Parse bitable URL and extract tokens */ function parseBitableUrl(url: string): { token: string; tableId?: string; isWiki: boolean} | null { try { const u = new URL(url); const tableId = u.searchParams.get("table") ?? undefined;
// Wiki format: /wiki/XXXXX?table=YYY const wikiMatch = u.pathname.match(/\/wiki\/([A-Za-z0-9]+)/); if (wikiMatch) { return { token: wikiMatch[1], tableId, isWiki: true };
}
// Base format: /base/XXXXX?table=YYY const baseMatch = u.pathname.match(/\/base\/([A-Za-z0-9]+)/); if (baseMatch) { return { token: baseMatch[1], tableId, isWiki: false };
}
returnnull;
} catch { returnnull;
}
}
/** Get app_token from wiki node_token */
async function getAppTokenFromWiki(client: Lark.Client, nodeToken: string): Promise<string> { const res = await client.wiki.space.getNode({
params: { token: nodeToken },
});
ensureLarkSuccess(res, "wiki.space.getNode", { nodeToken });
const node = res.data?.node; if (!node) { thrownew Error("Node not found");
} if (node.obj_type !== "bitable") { thrownew Error(`Node is not a bitable (type: ${node.obj_type})`);
}
return node.obj_token!;
}
/** Get bitable metadata from URL (handles both /base/ and /wiki/ URLs) */
async function getBitableMeta(client: Lark.Client, url: string) { const parsed = parseBitableUrl(url); if (!parsed) { thrownew Error("Invalid URL format. Expected /base/XXX or /wiki/XXX URL");
}
let appToken: string; if (parsed.isWiki) {
appToken = await getAppTokenFromWiki(client, parsed.token);
} else {
appToken = parsed.token;
}
// Get bitable app info const res = await client.bitable.app.get({
path: { app_token: appToken },
});
ensureLarkSuccess(res, "bitable.app.get", { appToken });
// List tables if no table_id specified
let tables: { table_id: string; name: string }[] = []; if (!parsed.tableId) { const tablesRes = await client.bitable.appTable.list({
path: { app_token: appToken },
}); if (tablesRes.code === 0) {
tables = (tablesRes.data?.items ?? []).map((t) => ({
table_id: t.table_id!,
name: t.name!,
}));
}
}
return {
app_token: appToken,
table_id: parsed.tableId,
name: res.data?.app?.name,
url_type: parsed.isWiki ? "wiki" : "base",
...(tables.length > 0 && { tables }),
hint: parsed.tableId
? `Use app_token="${appToken}" and table_id="${parsed.tableId}"for other bitable tools`
: `Use app_token="${appToken}"for other bitable tools. Select a table_id from the tables list.`,
};
}
/** Default field types created for new Bitable tables (to be cleaned up) */ const DEFAULT_CLEANUP_FIELD_TYPES = new Set([3, 5, 17]); // SingleSelect, DateTime, Attachment
/** Clean up default placeholder rows and fields in a newly created Bitable table */
async function cleanupNewBitable(
client: Lark.Client,
appToken: string,
tableId: string,
tableName: string,
logger: CleanupLogger,
): Promise<{ cleanedRows: number; cleanedFields: number }> {
let cleanedRows = 0;
let cleanedFields = 0;
return {
app_token: appToken,
table_id: tableId,
name: res.data?.app?.name,
url: res.data?.app?.url,
cleaned_placeholder_rows: cleanedRows,
cleaned_default_fields: cleanedFields,
hint: tableId
? `Table created. Use app_token="${appToken}" and table_id="${tableId}"for other bitable tools.`
: "Table created. Use feishu_bitable_get_meta to get table_id and field details.",
};
}
const GetMetaSchema = Type.Object({
url: Type.String({
description: "Bitable URL. Supports both formats: /base/XXX?table=YYY or /wiki/XXX?table=YYY",
}),
});
const ListFieldsSchema = Type.Object({
app_token: Type.String({
description: "Bitable app token (use feishu_bitable_get_meta to get from URL)",
}),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
});
const ListRecordsSchema = Type.Object({
app_token: Type.String({
description: "Bitable app token (use feishu_bitable_get_meta to get from URL)",
}),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
page_size: Type.Optional(
Type.Number({
description: "Number of records per page (1-500, default 100)",
minimum: 1,
maximum: 500,
}),
),
page_token: Type.Optional(
Type.String({ description: "Pagination token from previous response" }),
),
});
const GetRecordSchema = Type.Object({
app_token: Type.String({
description: "Bitable app token (use feishu_bitable_get_meta to get from URL)",
}),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
record_id: Type.String({ description: "Record ID to retrieve" }),
});
const CreateRecordSchema = Type.Object({
app_token: Type.String({
description: "Bitable app token (use feishu_bitable_get_meta to get from URL)",
}),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
fields: Type.Record(Type.String(), Type.Any(), {
description: "Field values keyed by field name. Format by type: Text='string', Number=123, SingleSelect='Option', MultiSelect=['A','B'], DateTime=timestamp_ms, User=[{id:'ou_xxx'}], URL={text:'Display',link:'https://...'}",
}),
});
const CreateAppSchema = Type.Object({
name: Type.String({
description: "Name for the new Bitable application",
}),
folder_token: Type.Optional(
Type.String({
description: "Optional folder token to place the Bitable in a specific folder",
}),
),
});
const CreateFieldSchema = Type.Object({
app_token: Type.String({
description: "Bitable app token (use feishu_bitable_get_meta to get from URL, or feishu_bitable_create_app to create new)",
}),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
field_name: Type.String({ description: "Name for the new field" }),
field_type: Type.Number({
description: "Field type ID: 1=Text, 2=Number, 3=SingleSelect, 4=MultiSelect, 5=DateTime, 7=Checkbox, 11=User, 13=Phone, 15=URL, 17=Attachment, 18=SingleLink, 19=Lookup, 20=Formula, 21=DuplexLink, 22=Location, 23=GroupChat, 1001=CreatedTime, 1002=ModifiedTime, 1003=CreatedUser, 1004=ModifiedUser, 1005=AutoNumber",
minimum: 1,
}),
property: Type.Optional(
Type.Record(Type.String(), Type.Any(), {
description: "Field-specific properties (e.g., options for SingleSelect, format for Number)",
}),
),
});
const UpdateRecordSchema = Type.Object({
app_token: Type.String({
description: "Bitable app token (use feishu_bitable_get_meta to get from URL)",
}),
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
record_id: Type.String({ description: "Record ID to update" }),
fields: Type.Record(Type.String(), Type.Any(), {
description: "Field values to update (same format as create_record)",
}),
});
// ============ Tool Registration ============
export function registerFeishuBitableTools(api: OpenClawPluginApi) { if (!api.config) {
api.logger.debug?.("feishu_bitable: No config available, skipping bitable tools"); return;
}
const accounts = listEnabledFeishuAccounts(api.config); if (accounts.length === 0) {
api.logger.debug?.("feishu_bitable: No Feishu accounts configured, skipping bitable tools"); return;
}
registerBitableTool<{ url: string; accountId?: string }>({
name: "feishu_bitable_get_meta",
label: "Feishu Bitable Get Meta",
description: "Parse a Bitable URL and get app_token, table_id, and table list. Use this first when given a /wiki/ or /base/ URL.",
parameters: GetMetaSchema,
async execute({ params, defaultAccountId }) { return getBitableMeta(getClient(params, defaultAccountId), params.url);
},
});
registerBitableTool<{ app_token: string; table_id: string; accountId?: string }>({
name: "feishu_bitable_list_fields",
label: "Feishu Bitable List Fields",
description: "List all fields (columns) in a Bitable table with their types and properties",
parameters: ListFieldsSchema,
async execute({ params, defaultAccountId }) { return listFields(getClient(params, defaultAccountId), params.app_token, params.table_id);
},
});
registerBitableTool<{
app_token: string;
table_id: string;
page_size?: number;
page_token?: string;
accountId?: string;
}>({
name: "feishu_bitable_list_records",
label: "Feishu Bitable List Records",
description: "List records (rows) from a Bitable table with pagination support",
parameters: ListRecordsSchema,
async execute({ params, defaultAccountId }) { return listRecords(
getClient(params, defaultAccountId),
params.app_token,
params.table_id,
params.page_size,
params.page_token,
);
},
});
registerBitableTool<{
app_token: string;
table_id: string;
record_id: string;
accountId?: string;
}>({
name: "feishu_bitable_get_record",
label: "Feishu Bitable Get Record",
description: "Get a single record by ID from a Bitable table",
parameters: GetRecordSchema,
async execute({ params, defaultAccountId }) { return getRecord(
getClient(params, defaultAccountId),
params.app_token,
params.table_id,
params.record_id,
);
},
});
registerBitableTool<{
app_token: string;
table_id: string;
fields: BitableRecordFields;
accountId?: string;
}>({
name: "feishu_bitable_create_record",
label: "Feishu Bitable Create Record",
description: "Create a new record (row) in a Bitable table",
parameters: CreateRecordSchema,
async execute({ params, defaultAccountId }) { return createRecord(
getClient(params, defaultAccountId),
params.app_token,
params.table_id,
params.fields,
);
},
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.