@openclaw/feishu 2026.3.2 → 2026.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/index.ts +2 -2
  2. package/package.json +1 -1
  3. package/src/accounts.test.ts +199 -13
  4. package/src/accounts.ts +45 -17
  5. package/src/bitable.ts +40 -28
  6. package/src/bot.checkBotMentioned.test.ts +8 -0
  7. package/src/bot.stripBotMention.test.ts +118 -22
  8. package/src/bot.test.ts +516 -9
  9. package/src/bot.ts +366 -109
  10. package/src/card-action.ts +1 -1
  11. package/src/channel.test.ts +1 -1
  12. package/src/channel.ts +52 -64
  13. package/src/chat.test.ts +2 -2
  14. package/src/chat.ts +1 -1
  15. package/src/client.test.ts +207 -4
  16. package/src/client.ts +70 -5
  17. package/src/config-schema.test.ts +14 -6
  18. package/src/config-schema.ts +5 -1
  19. package/src/dedup.ts +1 -1
  20. package/src/directory.test.ts +40 -0
  21. package/src/directory.ts +29 -50
  22. package/src/docx-batch-insert.test.ts +90 -0
  23. package/src/docx-batch-insert.ts +8 -11
  24. package/src/docx.account-selection.test.ts +3 -3
  25. package/src/docx.ts +1 -1
  26. package/src/drive.ts +13 -17
  27. package/src/dynamic-agent.ts +1 -1
  28. package/src/feishu-command-handler.ts +59 -0
  29. package/src/media.test.ts +60 -13
  30. package/src/media.ts +23 -9
  31. package/src/monitor.account.ts +19 -8
  32. package/src/monitor.reaction.test.ts +111 -105
  33. package/src/monitor.startup.test.ts +11 -10
  34. package/src/monitor.startup.ts +20 -7
  35. package/src/monitor.state.ts +4 -1
  36. package/src/monitor.test-mocks.ts +42 -9
  37. package/src/monitor.transport.ts +4 -1
  38. package/src/monitor.ts +4 -4
  39. package/src/monitor.webhook-security.test.ts +8 -23
  40. package/src/onboarding.status.test.ts +1 -1
  41. package/src/onboarding.test.ts +143 -0
  42. package/src/onboarding.ts +86 -71
  43. package/src/outbound.test.ts +178 -0
  44. package/src/outbound.ts +39 -6
  45. package/src/perm.ts +11 -15
  46. package/src/policy.test.ts +40 -0
  47. package/src/policy.ts +9 -10
  48. package/src/probe.test.ts +18 -18
  49. package/src/reactions.ts +1 -1
  50. package/src/reply-dispatcher.test.ts +175 -0
  51. package/src/reply-dispatcher.ts +69 -21
  52. package/src/runtime.ts +1 -1
  53. package/src/secret-input.ts +8 -14
  54. package/src/send-message.ts +71 -0
  55. package/src/send-target.test.ts +1 -1
  56. package/src/send-target.ts +1 -1
  57. package/src/send.reply-fallback.test.ts +74 -0
  58. package/src/send.test.ts +1 -1
  59. package/src/send.ts +88 -49
  60. package/src/streaming-card.test.ts +54 -0
  61. package/src/streaming-card.ts +96 -28
  62. package/src/targets.ts +5 -1
  63. package/src/tool-account-routing.test.ts +3 -3
  64. package/src/tool-account.ts +1 -1
  65. package/src/tool-factory-test-harness.ts +1 -1
  66. package/src/tool-result.test.ts +32 -0
  67. package/src/tool-result.ts +14 -0
  68. package/src/types.ts +2 -3
  69. package/src/typing.ts +1 -1
  70. package/src/wiki.ts +15 -19
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/feishu";
3
3
  import { registerFeishuBitableTools } from "./src/bitable.js";
4
4
  import { feishuPlugin } from "./src/channel.js";
5
5
  import { registerFeishuChatTools } from "./src/chat.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/feishu",
3
- "version": "2026.3.2",
3
+ "version": "2026.3.7",
4
4
  "description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -3,7 +3,40 @@ import {
3
3
  resolveDefaultFeishuAccountId,
4
4
  resolveDefaultFeishuAccountSelection,
5
5
  resolveFeishuAccount,
6
+ resolveFeishuCredentials,
6
7
  } from "./accounts.js";
8
+ import type { FeishuConfig } from "./types.js";
9
+
10
+ const asConfig = (value: Partial<FeishuConfig>) => value as FeishuConfig;
11
+
12
+ function withEnvVar(key: string, value: string | undefined, run: () => void) {
13
+ const prev = process.env[key];
14
+ if (value === undefined) {
15
+ delete process.env[key];
16
+ } else {
17
+ process.env[key] = value;
18
+ }
19
+ try {
20
+ run();
21
+ } finally {
22
+ if (prev === undefined) {
23
+ delete process.env[key];
24
+ } else {
25
+ process.env[key] = prev;
26
+ }
27
+ }
28
+ }
29
+
30
+ function expectUnresolvedEnvSecretRefError(key: string) {
31
+ expect(() =>
32
+ resolveFeishuCredentials(
33
+ asConfig({
34
+ appId: "cli_123",
35
+ appSecret: { source: "env", provider: "default", id: key } as never,
36
+ }),
37
+ ),
38
+ ).toThrow(/unresolved SecretRef/i);
39
+ }
7
40
 
8
41
  describe("resolveDefaultFeishuAccountId", () => {
9
42
  it("prefers channels.feishu.defaultAccount when configured", () => {
@@ -12,8 +45,8 @@ describe("resolveDefaultFeishuAccountId", () => {
12
45
  feishu: {
13
46
  defaultAccount: "router-d",
14
47
  accounts: {
15
- default: { appId: "cli_default", appSecret: "secret_default" },
16
- "router-d": { appId: "cli_router", appSecret: "secret_router" },
48
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
49
+ "router-d": { appId: "cli_router", appSecret: "secret_router" }, // pragma: allowlist secret
17
50
  },
18
51
  },
19
52
  },
@@ -28,7 +61,7 @@ describe("resolveDefaultFeishuAccountId", () => {
28
61
  feishu: {
29
62
  defaultAccount: "Router D",
30
63
  accounts: {
31
- "router-d": { appId: "cli_router", appSecret: "secret_router" },
64
+ "router-d": { appId: "cli_router", appSecret: "secret_router" }, // pragma: allowlist secret
32
65
  },
33
66
  },
34
67
  },
@@ -43,8 +76,8 @@ describe("resolveDefaultFeishuAccountId", () => {
43
76
  feishu: {
44
77
  defaultAccount: "router-d",
45
78
  accounts: {
46
- default: { appId: "cli_default", appSecret: "secret_default" },
47
- zeta: { appId: "cli_zeta", appSecret: "secret_zeta" },
79
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
80
+ zeta: { appId: "cli_zeta", appSecret: "secret_zeta" }, // pragma: allowlist secret
48
81
  },
49
82
  },
50
83
  },
@@ -58,8 +91,8 @@ describe("resolveDefaultFeishuAccountId", () => {
58
91
  channels: {
59
92
  feishu: {
60
93
  accounts: {
61
- default: { appId: "cli_default", appSecret: "secret_default" },
62
- zeta: { appId: "cli_zeta", appSecret: "secret_zeta" },
94
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
95
+ zeta: { appId: "cli_zeta", appSecret: "secret_zeta" }, // pragma: allowlist secret
63
96
  },
64
97
  },
65
98
  },
@@ -86,7 +119,7 @@ describe("resolveDefaultFeishuAccountId", () => {
86
119
  channels: {
87
120
  feishu: {
88
121
  accounts: {
89
- default: { appId: "cli_default", appSecret: "secret_default" },
122
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
90
123
  },
91
124
  },
92
125
  },
@@ -98,6 +131,118 @@ describe("resolveDefaultFeishuAccountId", () => {
98
131
  });
99
132
  });
100
133
 
134
+ describe("resolveFeishuCredentials", () => {
135
+ it("throws unresolved SecretRef errors by default for unsupported secret sources", () => {
136
+ expect(() =>
137
+ resolveFeishuCredentials(
138
+ asConfig({
139
+ appId: "cli_123",
140
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" } as never,
141
+ }),
142
+ ),
143
+ ).toThrow(/unresolved SecretRef/i);
144
+ });
145
+
146
+ it("returns null (without throwing) when unresolved SecretRef is allowed", () => {
147
+ const creds = resolveFeishuCredentials(
148
+ asConfig({
149
+ appId: "cli_123",
150
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" } as never,
151
+ }),
152
+ { allowUnresolvedSecretRef: true },
153
+ );
154
+
155
+ expect(creds).toBeNull();
156
+ });
157
+
158
+ it("throws unresolved SecretRef error when env SecretRef points to missing env var", () => {
159
+ const key = "FEISHU_APP_SECRET_MISSING_TEST";
160
+ withEnvVar(key, undefined, () => {
161
+ expectUnresolvedEnvSecretRefError(key);
162
+ });
163
+ });
164
+
165
+ it("resolves env SecretRef objects when unresolved refs are allowed", () => {
166
+ const key = "FEISHU_APP_SECRET_TEST";
167
+ const prev = process.env[key];
168
+ process.env[key] = " secret_from_env ";
169
+
170
+ try {
171
+ const creds = resolveFeishuCredentials(
172
+ asConfig({
173
+ appId: "cli_123",
174
+ appSecret: { source: "env", provider: "default", id: key } as never,
175
+ }),
176
+ { allowUnresolvedSecretRef: true },
177
+ );
178
+
179
+ expect(creds).toEqual({
180
+ appId: "cli_123",
181
+ appSecret: "secret_from_env", // pragma: allowlist secret
182
+ encryptKey: undefined,
183
+ verificationToken: undefined,
184
+ domain: "feishu",
185
+ });
186
+ } finally {
187
+ if (prev === undefined) {
188
+ delete process.env[key];
189
+ } else {
190
+ process.env[key] = prev;
191
+ }
192
+ }
193
+ });
194
+
195
+ it("resolves env SecretRef with custom provider alias when unresolved refs are allowed", () => {
196
+ const key = "FEISHU_APP_SECRET_CUSTOM_PROVIDER_TEST";
197
+ const prev = process.env[key];
198
+ process.env[key] = " secret_from_env_alias ";
199
+
200
+ try {
201
+ const creds = resolveFeishuCredentials(
202
+ asConfig({
203
+ appId: "cli_123",
204
+ appSecret: { source: "env", provider: "corp-env", id: key } as never,
205
+ }),
206
+ { allowUnresolvedSecretRef: true },
207
+ );
208
+
209
+ expect(creds?.appSecret).toBe("secret_from_env_alias");
210
+ } finally {
211
+ if (prev === undefined) {
212
+ delete process.env[key];
213
+ } else {
214
+ process.env[key] = prev;
215
+ }
216
+ }
217
+ });
218
+
219
+ it("preserves unresolved SecretRef diagnostics for env refs in default mode", () => {
220
+ const key = "FEISHU_APP_SECRET_POLICY_TEST";
221
+ withEnvVar(key, "secret_from_env", () => {
222
+ expectUnresolvedEnvSecretRefError(key);
223
+ });
224
+ });
225
+
226
+ it("trims and returns credentials when values are valid strings", () => {
227
+ const creds = resolveFeishuCredentials(
228
+ asConfig({
229
+ appId: " cli_123 ",
230
+ appSecret: " secret_456 ",
231
+ encryptKey: " enc ",
232
+ verificationToken: " vt ",
233
+ }),
234
+ );
235
+
236
+ expect(creds).toEqual({
237
+ appId: "cli_123",
238
+ appSecret: "secret_456", // pragma: allowlist secret
239
+ encryptKey: "enc",
240
+ verificationToken: "vt",
241
+ domain: "feishu",
242
+ });
243
+ });
244
+ });
245
+
101
246
  describe("resolveFeishuAccount", () => {
102
247
  it("uses top-level credentials with configured default account id even without account map entry", () => {
103
248
  const cfg = {
@@ -105,9 +250,9 @@ describe("resolveFeishuAccount", () => {
105
250
  feishu: {
106
251
  defaultAccount: "router-d",
107
252
  appId: "top_level_app",
108
- appSecret: "top_level_secret",
253
+ appSecret: "top_level_secret", // pragma: allowlist secret
109
254
  accounts: {
110
- default: { appId: "cli_default", appSecret: "secret_default" },
255
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
111
256
  },
112
257
  },
113
258
  },
@@ -127,7 +272,7 @@ describe("resolveFeishuAccount", () => {
127
272
  defaultAccount: "router-d",
128
273
  accounts: {
129
274
  default: { enabled: true },
130
- "router-d": { appId: "cli_router", appSecret: "secret_router", enabled: true },
275
+ "router-d": { appId: "cli_router", appSecret: "secret_router", enabled: true }, // pragma: allowlist secret
131
276
  },
132
277
  },
133
278
  },
@@ -146,8 +291,8 @@ describe("resolveFeishuAccount", () => {
146
291
  feishu: {
147
292
  defaultAccount: "router-d",
148
293
  accounts: {
149
- default: { appId: "cli_default", appSecret: "secret_default" },
150
- "router-d": { appId: "cli_router", appSecret: "secret_router" },
294
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
295
+ "router-d": { appId: "cli_router", appSecret: "secret_router" }, // pragma: allowlist secret
151
296
  },
152
297
  },
153
298
  },
@@ -158,4 +303,45 @@ describe("resolveFeishuAccount", () => {
158
303
  expect(account.selectionSource).toBe("explicit");
159
304
  expect(account.appId).toBe("cli_default");
160
305
  });
306
+
307
+ it("surfaces unresolved SecretRef errors in account resolution", () => {
308
+ expect(() =>
309
+ resolveFeishuAccount({
310
+ cfg: {
311
+ channels: {
312
+ feishu: {
313
+ accounts: {
314
+ main: {
315
+ appId: "cli_123",
316
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" },
317
+ } as never,
318
+ },
319
+ },
320
+ },
321
+ } as never,
322
+ accountId: "main",
323
+ }),
324
+ ).toThrow(/unresolved SecretRef/i);
325
+ });
326
+
327
+ it("does not throw when account name is non-string", () => {
328
+ expect(() =>
329
+ resolveFeishuAccount({
330
+ cfg: {
331
+ channels: {
332
+ feishu: {
333
+ accounts: {
334
+ main: {
335
+ name: { bad: true },
336
+ appId: "cli_123",
337
+ appSecret: "secret_456", // pragma: allowlist secret
338
+ } as never,
339
+ },
340
+ },
341
+ },
342
+ } as never,
343
+ accountId: "main",
344
+ }),
345
+ ).not.toThrow();
346
+ });
161
347
  });
package/src/accounts.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
1
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
3
3
  import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
4
4
  import type {
5
5
  FeishuConfig,
@@ -129,27 +129,54 @@ export function resolveFeishuCredentials(
129
129
  verificationToken?: string;
130
130
  domain: FeishuDomain;
131
131
  } | null {
132
- const appId = cfg?.appId?.trim();
133
- const appSecret = options?.allowUnresolvedSecretRef
134
- ? normalizeSecretInputString(cfg?.appSecret)
135
- : normalizeResolvedSecretInputString({
136
- value: cfg?.appSecret,
137
- path: "channels.feishu.appSecret",
138
- });
132
+ const normalizeString = (value: unknown): string | undefined => {
133
+ if (typeof value !== "string") {
134
+ return undefined;
135
+ }
136
+ const trimmed = value.trim();
137
+ return trimmed ? trimmed : undefined;
138
+ };
139
+
140
+ const resolveSecretLike = (value: unknown, path: string): string | undefined => {
141
+ const asString = normalizeString(value);
142
+ if (asString) {
143
+ return asString;
144
+ }
145
+
146
+ // In relaxed/onboarding paths only: allow direct env SecretRef reads for UX.
147
+ // Default resolution path must preserve unresolved-ref diagnostics/policy semantics.
148
+ if (options?.allowUnresolvedSecretRef && typeof value === "object" && value !== null) {
149
+ const rec = value as Record<string, unknown>;
150
+ const source = normalizeString(rec.source)?.toLowerCase();
151
+ const id = normalizeString(rec.id);
152
+ if (source === "env" && id) {
153
+ const envValue = normalizeString(process.env[id]);
154
+ if (envValue) {
155
+ return envValue;
156
+ }
157
+ }
158
+ }
159
+
160
+ if (options?.allowUnresolvedSecretRef) {
161
+ return normalizeSecretInputString(value);
162
+ }
163
+ return normalizeResolvedSecretInputString({ value, path });
164
+ };
165
+
166
+ const appId = resolveSecretLike(cfg?.appId, "channels.feishu.appId");
167
+ const appSecret = resolveSecretLike(cfg?.appSecret, "channels.feishu.appSecret");
168
+
139
169
  if (!appId || !appSecret) {
140
170
  return null;
141
171
  }
142
172
  return {
143
173
  appId,
144
174
  appSecret,
145
- encryptKey: cfg?.encryptKey?.trim() || undefined,
146
- verificationToken:
147
- (options?.allowUnresolvedSecretRef
148
- ? normalizeSecretInputString(cfg?.verificationToken)
149
- : normalizeResolvedSecretInputString({
150
- value: cfg?.verificationToken,
151
- path: "channels.feishu.verificationToken",
152
- })) || undefined,
175
+ encryptKey: normalizeString(cfg?.encryptKey),
176
+ verificationToken: resolveSecretLike(
177
+ cfg?.verificationToken,
178
+ "channels.feishu.verificationToken",
179
+ ),
153
180
  domain: cfg?.domain ?? "feishu",
154
181
  };
155
182
  }
@@ -186,13 +213,14 @@ export function resolveFeishuAccount(params: {
186
213
 
187
214
  // Resolve credentials from merged config
188
215
  const creds = resolveFeishuCredentials(merged);
216
+ const accountName = (merged as FeishuAccountConfig).name;
189
217
 
190
218
  return {
191
219
  accountId,
192
220
  selectionSource,
193
221
  enabled,
194
222
  configured: Boolean(creds),
195
- name: (merged as FeishuAccountConfig).name?.trim() || undefined,
223
+ name: typeof accountName === "string" ? accountName.trim() || undefined : undefined,
196
224
  appId: creds?.appId,
197
225
  appSecret: creds?.appSecret,
198
226
  encryptKey: creds?.encryptKey,
package/src/bitable.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type * as Lark from "@larksuiteoapi/node-sdk";
2
2
  import { Type } from "@sinclair/typebox";
3
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
4
4
  import { listEnabledFeishuAccounts } from "./accounts.js";
5
5
  import { createFeishuToolClient } from "./tool-account.js";
6
6
 
@@ -13,6 +13,31 @@ function json(data: unknown) {
13
13
  };
14
14
  }
15
15
 
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
+
16
41
  /** Field type ID to human-readable name */
17
42
  const FIELD_TYPE_NAMES: Record<number, string> = {
18
43
  1: "Text",
@@ -69,9 +94,7 @@ async function getAppTokenFromWiki(client: Lark.Client, nodeToken: string): Prom
69
94
  const res = await client.wiki.space.getNode({
70
95
  params: { token: nodeToken },
71
96
  });
72
- if (res.code !== 0) {
73
- throw new Error(res.msg);
74
- }
97
+ ensureLarkSuccess(res, "wiki.space.getNode", { nodeToken });
75
98
 
76
99
  const node = res.data?.node;
77
100
  if (!node) {
@@ -102,9 +125,7 @@ async function getBitableMeta(client: Lark.Client, url: string) {
102
125
  const res = await client.bitable.app.get({
103
126
  path: { app_token: appToken },
104
127
  });
105
- if (res.code !== 0) {
106
- throw new Error(res.msg);
107
- }
128
+ ensureLarkSuccess(res, "bitable.app.get", { appToken });
108
129
 
109
130
  // List tables if no table_id specified
110
131
  let tables: { table_id: string; name: string }[] = [];
@@ -136,9 +157,7 @@ async function listFields(client: Lark.Client, appToken: string, tableId: string
136
157
  const res = await client.bitable.appTableField.list({
137
158
  path: { app_token: appToken, table_id: tableId },
138
159
  });
139
- if (res.code !== 0) {
140
- throw new Error(res.msg);
141
- }
160
+ ensureLarkSuccess(res, "bitable.appTableField.list", { appToken, tableId });
142
161
 
143
162
  const fields = res.data?.items ?? [];
144
163
  return {
@@ -168,9 +187,7 @@ async function listRecords(
168
187
  ...(pageToken && { page_token: pageToken }),
169
188
  },
170
189
  });
171
- if (res.code !== 0) {
172
- throw new Error(res.msg);
173
- }
190
+ ensureLarkSuccess(res, "bitable.appTableRecord.list", { appToken, tableId, pageSize });
174
191
 
175
192
  return {
176
193
  records: res.data?.items ?? [],
@@ -184,9 +201,7 @@ async function getRecord(client: Lark.Client, appToken: string, tableId: string,
184
201
  const res = await client.bitable.appTableRecord.get({
185
202
  path: { app_token: appToken, table_id: tableId, record_id: recordId },
186
203
  });
187
- if (res.code !== 0) {
188
- throw new Error(res.msg);
189
- }
204
+ ensureLarkSuccess(res, "bitable.appTableRecord.get", { appToken, tableId, recordId });
190
205
 
191
206
  return {
192
207
  record: res.data?.record,
@@ -204,9 +219,7 @@ async function createRecord(
204
219
  // oxlint-disable-next-line typescript/no-explicit-any
205
220
  data: { fields: fields as any },
206
221
  });
207
- if (res.code !== 0) {
208
- throw new Error(res.msg);
209
- }
222
+ ensureLarkSuccess(res, "bitable.appTableRecord.create", { appToken, tableId });
210
223
 
211
224
  return {
212
225
  record: res.data?.record,
@@ -334,9 +347,7 @@ async function createApp(
334
347
  ...(folderToken && { folder_token: folderToken }),
335
348
  },
336
349
  });
337
- if (res.code !== 0) {
338
- throw new Error(res.msg);
339
- }
350
+ ensureLarkSuccess(res, "bitable.app.create", { name, folderToken });
340
351
 
341
352
  const appToken = res.data?.app?.app_token;
342
353
  if (!appToken) {
@@ -393,9 +404,12 @@ async function createField(
393
404
  ...(property && { property }),
394
405
  },
395
406
  });
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
+ });
399
413
 
400
414
  return {
401
415
  field_id: res.data?.field?.field_id,
@@ -417,9 +431,7 @@ async function updateRecord(
417
431
  // oxlint-disable-next-line typescript/no-explicit-any
418
432
  data: { fields: fields as any },
419
433
  });
420
- if (res.code !== 0) {
421
- throw new Error(res.msg);
422
- }
434
+ ensureLarkSuccess(res, "bitable.appTableRecord.update", { appToken, tableId, recordId });
423
435
 
424
436
  return {
425
437
  record: res.data?.record,
@@ -76,6 +76,14 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
76
76
  expect(ctx.mentionedBot).toBe(true);
77
77
  });
78
78
 
79
+ it("returns mentionedBot=true when bot mention name differs from configured botName", () => {
80
+ const event = makeEvent("group", [
81
+ { key: "@_user_1", name: "OpenClaw Bot (Alias)", id: { open_id: BOT_OPEN_ID } },
82
+ ]);
83
+ const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID, "OpenClaw Bot");
84
+ expect(ctx.mentionedBot).toBe(true);
85
+ });
86
+
79
87
  it("returns mentionedBot=false when only other users are mentioned", () => {
80
88
  const event = makeEvent("group", [
81
89
  { key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },