From 1fdc20a24febf0f5e13d97ff47ac37d4c399fe14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=92=E9=9B=B2?= <137844255@qq.com> Date: Tue, 3 Mar 2026 11:44:40 +0800 Subject: [PATCH] refactor(feishu): unify Lark SDK error handling with LarkApiError (#31450) * refactor(feishu): unify Lark SDK error handling with LarkApiError - Add LarkApiError class with code, api, and context fields for better diagnostics - Add ensureLarkSuccess helper to replace 9 duplicate error check patterns - Update tool registration layer to return structured error info (code, api, context) This improves: - Observability: errors now include API name and request context for easier debugging - Maintainability: single point of change for error handling logic - Extensibility: foundation for retry strategies, error classification, etc. Affected APIs: - wiki.space.getNode - bitable.app.get - bitable.app.create - bitable.appTableField.list - bitable.appTableField.create - bitable.appTableRecord.list - bitable.appTableRecord.get - bitable.appTableRecord.create - bitable.appTableRecord.update * Changelog: note Feishu bitable error handling unification --------- Co-authored-by: echoVic Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + extensions/feishu/src/bitable.ts | 66 +++++++++++++++++++------------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 586be0059..a2dedb312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ Docs: https://docs.openclaw.ai - Feishu/Typing notification suppression: skip typing keepalive reaction re-adds when the indicator is already active, preventing duplicate notification pings from repeated identical emoji adds. (#31580) - Feishu/Probe failure backoff: cache API and timeout probe failures for one minute per account key while preserving abort-aware probe timeouts, reducing repeated health-check retries during transient credential/network outages. (#29970) - Feishu/Streaming block fallback: preserve markdown block stream text as final streaming-card content when final payload text is missing, while still suppressing non-card internal block chunk delivery. (#30663) +- Feishu/Bitable API errors: unify Feishu Bitable tool error handling with structured `LarkApiError` responses and consistent API/context attribution across wiki/base metadata, field, and record operations. (#31450) - BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204. - WebChat/markdown tables: ensure GitHub-flavored markdown table parsing is explicitly enabled at render time and add horizontal overflow handling for wide tables, with regression coverage for table-only and mixed text+table content. (#32365) Thanks @BlueBirdBack. - Feishu/default account resolution: always honor explicit `channels.feishu.defaultAccount` during outbound account selection (including top-level-credential setups where the preferred id is not present in `accounts`), instead of silently falling back to another account id. (#32253) Thanks @bmendonca3. diff --git a/extensions/feishu/src/bitable.ts b/extensions/feishu/src/bitable.ts index 5e0575bba..8617282bb 100644 --- a/extensions/feishu/src/bitable.ts +++ b/extensions/feishu/src/bitable.ts @@ -13,6 +13,31 @@ function json(data: unknown) { }; } +type LarkResponse = { code?: number; msg?: string; data?: T }; + +export class LarkApiError extends Error { + readonly code: number; + readonly api: string; + readonly context?: Record; + constructor(code: number, message: string, api: string, context?: Record) { + super(`[${api}] code=${code} message=${message}`); + this.name = "LarkApiError"; + this.code = code; + this.api = api; + this.context = context; + } +} + +function ensureLarkSuccess( + res: LarkResponse, + api: string, + context?: Record, +): asserts res is LarkResponse & { code: 0 } { + if (res.code !== 0) { + throw new LarkApiError(res.code ?? -1, res.msg ?? "unknown error", api, context); + } +} + /** Field type ID to human-readable name */ const FIELD_TYPE_NAMES: Record = { 1: "Text", @@ -69,9 +94,7 @@ async function getAppTokenFromWiki(client: Lark.Client, nodeToken: string): Prom const res = await client.wiki.space.getNode({ params: { token: nodeToken }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "wiki.space.getNode", { nodeToken }); const node = res.data?.node; if (!node) { @@ -102,9 +125,7 @@ async function getBitableMeta(client: Lark.Client, url: string) { const res = await client.bitable.app.get({ path: { app_token: appToken }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "bitable.app.get", { appToken }); // List tables if no table_id specified let tables: { table_id: string; name: string }[] = []; @@ -136,9 +157,7 @@ async function listFields(client: Lark.Client, appToken: string, tableId: string const res = await client.bitable.appTableField.list({ path: { app_token: appToken, table_id: tableId }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "bitable.appTableField.list", { appToken, tableId }); const fields = res.data?.items ?? []; return { @@ -168,9 +187,7 @@ async function listRecords( ...(pageToken && { page_token: pageToken }), }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "bitable.appTableRecord.list", { appToken, tableId, pageSize }); return { records: res.data?.items ?? [], @@ -184,9 +201,7 @@ async function getRecord(client: Lark.Client, appToken: string, tableId: string, const res = await client.bitable.appTableRecord.get({ path: { app_token: appToken, table_id: tableId, record_id: recordId }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "bitable.appTableRecord.get", { appToken, tableId, recordId }); return { record: res.data?.record, @@ -204,9 +219,7 @@ async function createRecord( // oxlint-disable-next-line typescript/no-explicit-any data: { fields: fields as any }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "bitable.appTableRecord.create", { appToken, tableId }); return { record: res.data?.record, @@ -334,9 +347,7 @@ async function createApp( ...(folderToken && { folder_token: folderToken }), }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "bitable.app.create", { name, folderToken }); const appToken = res.data?.app?.app_token; if (!appToken) { @@ -393,9 +404,12 @@ async function createField( ...(property && { property }), }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "bitable.appTableField.create", { + appToken, + tableId, + fieldName, + fieldType, + }); return { field_id: res.data?.field?.field_id, @@ -417,9 +431,7 @@ async function updateRecord( // oxlint-disable-next-line typescript/no-explicit-any data: { fields: fields as any }, }); - if (res.code !== 0) { - throw new Error(res.msg); - } + ensureLarkSuccess(res, "bitable.appTableRecord.update", { appToken, tableId, recordId }); return { record: res.data?.record,