@openclaw/feishu 2026.3.2 → 2026.3.8-beta.1
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.
- package/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +199 -13
- package/src/accounts.ts +45 -17
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +8 -0
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +516 -9
- package/src/bot.ts +366 -109
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +52 -64
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +207 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +14 -6
- package/src/config-schema.ts +5 -1
- package/src/dedup.ts +1 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +3 -3
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +60 -13
- package/src/media.ts +23 -9
- package/src/monitor.account.ts +19 -8
- package/src/monitor.reaction.test.ts +111 -105
- package/src/monitor.startup.test.ts +11 -10
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.ts +4 -1
- package/src/monitor.test-mocks.ts +42 -9
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +8 -23
- package/src/onboarding.status.test.ts +1 -1
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +86 -71
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +18 -18
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +175 -0
- package/src/reply-dispatcher.ts +69 -21
- package/src/runtime.ts +5 -13
- package/src/secret-input.ts +8 -14
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +1 -1
- package/src/send-target.ts +1 -1
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.ts +5 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +2 -3
- package/src/typing.ts +1 -1
- 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
package/src/accounts.test.ts
CHANGED
|
@@ -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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
146
|
-
verificationToken:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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" } },
|