Skip to content

Commit 1fdc20a

Browse files
echoVicechoVicTakhoffman
authored
refactor(feishu): unify Lark SDK error handling with LarkApiError (openclaw#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 <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent 925da0f commit 1fdc20a

File tree

2 files changed

+40
-27
lines changed

2 files changed

+40
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ Docs: https://docs.openclaw.ai
157157
- 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)
158158
- 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)
159159
- 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)
160+
- 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)
160161
- 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.
161162
- 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.
162163
- 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.

extensions/feishu/src/bitable.ts

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,31 @@ function json(data: unknown) {
1313
};
1414
}
1515

16+
type LarkResponse<T = unknown> = { code?: number; msg?: string; data?: T };
17+
18+
export class LarkApiError extends Error {
19+
readonly code: number;
20+
readonly api: string;
21+
readonly context?: Record<string, unknown>;
22+
constructor(code: number, message: string, api: string, context?: Record<string, unknown>) {
23+
super(`[${api}] code=${code} message=${message}`);
24+
this.name = "LarkApiError";
25+
this.code = code;
26+
this.api = api;
27+
this.context = context;
28+
}
29+
}
30+
31+
function ensureLarkSuccess<T>(
32+
res: LarkResponse<T>,
33+
api: string,
34+
context?: Record<string, unknown>,
35+
): asserts res is LarkResponse<T> & { code: 0 } {
36+
if (res.code !== 0) {
37+
throw new LarkApiError(res.code ?? -1, res.msg ?? "unknown error", api, context);
38+
}
39+
}
40+
1641
/** Field type ID to human-readable name */
1742
const FIELD_TYPE_NAMES: Record<number, string> = {
1843
1: "Text",
@@ -69,9 +94,7 @@ async function getAppTokenFromWiki(client: Lark.Client, nodeToken: string): Prom
6994
const res = await client.wiki.space.getNode({
7095
params: { token: nodeToken },
7196
});
72-
if (res.code !== 0) {
73-
throw new Error(res.msg);
74-
}
97+
ensureLarkSuccess(res, "wiki.space.getNode", { nodeToken });
7598

7699
const node = res.data?.node;
77100
if (!node) {
@@ -102,9 +125,7 @@ async function getBitableMeta(client: Lark.Client, url: string) {
102125
const res = await client.bitable.app.get({
103126
path: { app_token: appToken },
104127
});
105-
if (res.code !== 0) {
106-
throw new Error(res.msg);
107-
}
128+
ensureLarkSuccess(res, "bitable.app.get", { appToken });
108129

109130
// List tables if no table_id specified
110131
let tables: { table_id: string; name: string }[] = [];
@@ -136,9 +157,7 @@ async function listFields(client: Lark.Client, appToken: string, tableId: string
136157
const res = await client.bitable.appTableField.list({
137158
path: { app_token: appToken, table_id: tableId },
138159
});
139-
if (res.code !== 0) {
140-
throw new Error(res.msg);
141-
}
160+
ensureLarkSuccess(res, "bitable.appTableField.list", { appToken, tableId });
142161

143162
const fields = res.data?.items ?? [];
144163
return {
@@ -168,9 +187,7 @@ async function listRecords(
168187
...(pageToken && { page_token: pageToken }),
169188
},
170189
});
171-
if (res.code !== 0) {
172-
throw new Error(res.msg);
173-
}
190+
ensureLarkSuccess(res, "bitable.appTableRecord.list", { appToken, tableId, pageSize });
174191

175192
return {
176193
records: res.data?.items ?? [],
@@ -184,9 +201,7 @@ async function getRecord(client: Lark.Client, appToken: string, tableId: string,
184201
const res = await client.bitable.appTableRecord.get({
185202
path: { app_token: appToken, table_id: tableId, record_id: recordId },
186203
});
187-
if (res.code !== 0) {
188-
throw new Error(res.msg);
189-
}
204+
ensureLarkSuccess(res, "bitable.appTableRecord.get", { appToken, tableId, recordId });
190205

191206
return {
192207
record: res.data?.record,
@@ -204,9 +219,7 @@ async function createRecord(
204219
// oxlint-disable-next-line typescript/no-explicit-any
205220
data: { fields: fields as any },
206221
});
207-
if (res.code !== 0) {
208-
throw new Error(res.msg);
209-
}
222+
ensureLarkSuccess(res, "bitable.appTableRecord.create", { appToken, tableId });
210223

211224
return {
212225
record: res.data?.record,
@@ -334,9 +347,7 @@ async function createApp(
334347
...(folderToken && { folder_token: folderToken }),
335348
},
336349
});
337-
if (res.code !== 0) {
338-
throw new Error(res.msg);
339-
}
350+
ensureLarkSuccess(res, "bitable.app.create", { name, folderToken });
340351

341352
const appToken = res.data?.app?.app_token;
342353
if (!appToken) {
@@ -393,9 +404,12 @@ async function createField(
393404
...(property && { property }),
394405
},
395406
});
396-
if (res.code !== 0) {
397-
throw new Error(res.msg);
398-
}
407+
ensureLarkSuccess(res, "bitable.appTableField.create", {
408+
appToken,
409+
tableId,
410+
fieldName,
411+
fieldType,
412+
});
399413

400414
return {
401415
field_id: res.data?.field?.field_id,
@@ -417,9 +431,7 @@ async function updateRecord(
417431
// oxlint-disable-next-line typescript/no-explicit-any
418432
data: { fields: fields as any },
419433
});
420-
if (res.code !== 0) {
421-
throw new Error(res.msg);
422-
}
434+
ensureLarkSuccess(res, "bitable.appTableRecord.update", { appToken, tableId, recordId });
423435

424436
return {
425437
record: res.data?.record,

0 commit comments

Comments
 (0)