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 <echovic@163.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
青雲
2026-03-03 11:44:40 +08:00
committed by GitHub
parent 925da0fe99
commit 1fdc20a24f
2 changed files with 40 additions and 27 deletions

View File

@@ -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.

View File

@@ -13,6 +13,31 @@ function json(data: unknown) {
};
}
type LarkResponse<T = unknown> = { code?: number; msg?: string; data?: T };
export class LarkApiError extends Error {
readonly code: number;
readonly api: string;
readonly context?: Record<string, unknown>;
constructor(code: number, message: string, api: string, context?: Record<string, unknown>) {
super(`[${api}] code=${code} message=${message}`);
this.name = "LarkApiError";
this.code = code;
this.api = api;
this.context = context;
}
}
function ensureLarkSuccess<T>(
res: LarkResponse<T>,
api: string,
context?: Record<string, unknown>,
): asserts res is LarkResponse<T> & { 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<number, string> = {
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,