@openclaw/feishu 2026.3.1 → 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.
- package/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +268 -11
- package/src/accounts.ts +101 -14
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +9 -1
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +945 -77
- package/src/bot.ts +492 -165
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +72 -68
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +221 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +33 -6
- package/src/config-schema.ts +18 -10
- package/src/dedup.ts +47 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/doc-schema.ts +16 -22
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +10 -16
- package/src/docx.test.ts +41 -189
- 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 +164 -14
- package/src/media.ts +44 -10
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +284 -25
- package/src/monitor.reaction.test.ts +395 -46
- package/src/monitor.startup.test.ts +25 -8
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +88 -9
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +13 -11
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +213 -106
- 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 +54 -36
- package/src/probe.ts +57 -37
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +216 -0
- package/src/reply-dispatcher.ts +89 -22
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +7 -3
- 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.test.ts +29 -0
- package/src/targets.ts +25 -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 +11 -4
- 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
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
resolveDefaultFeishuAccountId,
|
|
4
|
+
resolveDefaultFeishuAccountSelection,
|
|
5
|
+
resolveFeishuAccount,
|
|
6
|
+
resolveFeishuCredentials,
|
|
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
|
+
}
|
|
3
40
|
|
|
4
41
|
describe("resolveDefaultFeishuAccountId", () => {
|
|
5
42
|
it("prefers channels.feishu.defaultAccount when configured", () => {
|
|
@@ -8,8 +45,8 @@ describe("resolveDefaultFeishuAccountId", () => {
|
|
|
8
45
|
feishu: {
|
|
9
46
|
defaultAccount: "router-d",
|
|
10
47
|
accounts: {
|
|
11
|
-
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
12
|
-
"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
|
|
13
50
|
},
|
|
14
51
|
},
|
|
15
52
|
},
|
|
@@ -24,7 +61,7 @@ describe("resolveDefaultFeishuAccountId", () => {
|
|
|
24
61
|
feishu: {
|
|
25
62
|
defaultAccount: "Router D",
|
|
26
63
|
accounts: {
|
|
27
|
-
"router-d": { appId: "cli_router", appSecret: "secret_router" },
|
|
64
|
+
"router-d": { appId: "cli_router", appSecret: "secret_router" }, // pragma: allowlist secret
|
|
28
65
|
},
|
|
29
66
|
},
|
|
30
67
|
},
|
|
@@ -33,14 +70,29 @@ describe("resolveDefaultFeishuAccountId", () => {
|
|
|
33
70
|
expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
|
|
34
71
|
});
|
|
35
72
|
|
|
36
|
-
it("
|
|
73
|
+
it("keeps configured defaultAccount even when not present in accounts map", () => {
|
|
37
74
|
const cfg = {
|
|
38
75
|
channels: {
|
|
39
76
|
feishu: {
|
|
40
|
-
defaultAccount: "
|
|
77
|
+
defaultAccount: "router-d",
|
|
41
78
|
accounts: {
|
|
42
|
-
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
43
|
-
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
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("falls back to literal default account id when present", () => {
|
|
90
|
+
const cfg = {
|
|
91
|
+
channels: {
|
|
92
|
+
feishu: {
|
|
93
|
+
accounts: {
|
|
94
|
+
default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
|
|
95
|
+
zeta: { appId: "cli_zeta", appSecret: "secret_zeta" }, // pragma: allowlist secret
|
|
44
96
|
},
|
|
45
97
|
},
|
|
46
98
|
},
|
|
@@ -48,9 +100,171 @@ describe("resolveDefaultFeishuAccountId", () => {
|
|
|
48
100
|
|
|
49
101
|
expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("default");
|
|
50
102
|
});
|
|
103
|
+
|
|
104
|
+
it("reports selection source for configured defaults and mapped defaults", () => {
|
|
105
|
+
const explicitDefaultCfg = {
|
|
106
|
+
channels: {
|
|
107
|
+
feishu: {
|
|
108
|
+
defaultAccount: "router-d",
|
|
109
|
+
accounts: {},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
expect(resolveDefaultFeishuAccountSelection(explicitDefaultCfg as never)).toEqual({
|
|
114
|
+
accountId: "router-d",
|
|
115
|
+
source: "explicit-default",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const mappedDefaultCfg = {
|
|
119
|
+
channels: {
|
|
120
|
+
feishu: {
|
|
121
|
+
accounts: {
|
|
122
|
+
default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
expect(resolveDefaultFeishuAccountSelection(mappedDefaultCfg as never)).toEqual({
|
|
128
|
+
accountId: "default",
|
|
129
|
+
source: "mapped-default",
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
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
|
+
});
|
|
51
244
|
});
|
|
52
245
|
|
|
53
246
|
describe("resolveFeishuAccount", () => {
|
|
247
|
+
it("uses top-level credentials with configured default account id even without account map entry", () => {
|
|
248
|
+
const cfg = {
|
|
249
|
+
channels: {
|
|
250
|
+
feishu: {
|
|
251
|
+
defaultAccount: "router-d",
|
|
252
|
+
appId: "top_level_app",
|
|
253
|
+
appSecret: "top_level_secret", // pragma: allowlist secret
|
|
254
|
+
accounts: {
|
|
255
|
+
default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
|
|
262
|
+
expect(account.accountId).toBe("router-d");
|
|
263
|
+
expect(account.selectionSource).toBe("explicit-default");
|
|
264
|
+
expect(account.configured).toBe(true);
|
|
265
|
+
expect(account.appId).toBe("top_level_app");
|
|
266
|
+
});
|
|
267
|
+
|
|
54
268
|
it("uses configured default account when accountId is omitted", () => {
|
|
55
269
|
const cfg = {
|
|
56
270
|
channels: {
|
|
@@ -58,7 +272,7 @@ describe("resolveFeishuAccount", () => {
|
|
|
58
272
|
defaultAccount: "router-d",
|
|
59
273
|
accounts: {
|
|
60
274
|
default: { enabled: true },
|
|
61
|
-
"router-d": { appId: "cli_router", appSecret: "secret_router", enabled: true },
|
|
275
|
+
"router-d": { appId: "cli_router", appSecret: "secret_router", enabled: true }, // pragma: allowlist secret
|
|
62
276
|
},
|
|
63
277
|
},
|
|
64
278
|
},
|
|
@@ -66,6 +280,7 @@ describe("resolveFeishuAccount", () => {
|
|
|
66
280
|
|
|
67
281
|
const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
|
|
68
282
|
expect(account.accountId).toBe("router-d");
|
|
283
|
+
expect(account.selectionSource).toBe("explicit-default");
|
|
69
284
|
expect(account.configured).toBe(true);
|
|
70
285
|
expect(account.appId).toBe("cli_router");
|
|
71
286
|
});
|
|
@@ -76,8 +291,8 @@ describe("resolveFeishuAccount", () => {
|
|
|
76
291
|
feishu: {
|
|
77
292
|
defaultAccount: "router-d",
|
|
78
293
|
accounts: {
|
|
79
|
-
default: { appId: "cli_default", appSecret: "secret_default" },
|
|
80
|
-
"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
|
|
81
296
|
},
|
|
82
297
|
},
|
|
83
298
|
},
|
|
@@ -85,6 +300,48 @@ describe("resolveFeishuAccount", () => {
|
|
|
85
300
|
|
|
86
301
|
const account = resolveFeishuAccount({ cfg: cfg as never, accountId: "default" });
|
|
87
302
|
expect(account.accountId).toBe("default");
|
|
303
|
+
expect(account.selectionSource).toBe("explicit");
|
|
88
304
|
expect(account.appId).toBe("cli_default");
|
|
89
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
|
+
});
|
|
90
347
|
});
|
package/src/accounts.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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
|
+
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
|
|
3
4
|
import type {
|
|
4
5
|
FeishuConfig,
|
|
5
6
|
FeishuAccountConfig,
|
|
7
|
+
FeishuDefaultAccountSelectionSource,
|
|
6
8
|
FeishuDomain,
|
|
7
9
|
ResolvedFeishuAccount,
|
|
8
10
|
} from "./types.js";
|
|
@@ -32,19 +34,38 @@ export function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
|
-
* Resolve the default account
|
|
37
|
+
* Resolve the default account selection and its source.
|
|
36
38
|
*/
|
|
37
|
-
export function
|
|
39
|
+
export function resolveDefaultFeishuAccountSelection(cfg: ClawdbotConfig): {
|
|
40
|
+
accountId: string;
|
|
41
|
+
source: FeishuDefaultAccountSelectionSource;
|
|
42
|
+
} {
|
|
38
43
|
const preferredRaw = (cfg.channels?.feishu as FeishuConfig | undefined)?.defaultAccount?.trim();
|
|
39
44
|
const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : undefined;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
if (preferred) {
|
|
46
|
+
return {
|
|
47
|
+
accountId: preferred,
|
|
48
|
+
source: "explicit-default",
|
|
49
|
+
};
|
|
43
50
|
}
|
|
51
|
+
const ids = listFeishuAccountIds(cfg);
|
|
44
52
|
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
45
|
-
return
|
|
53
|
+
return {
|
|
54
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
55
|
+
source: "mapped-default",
|
|
56
|
+
};
|
|
46
57
|
}
|
|
47
|
-
return
|
|
58
|
+
return {
|
|
59
|
+
accountId: ids[0] ?? DEFAULT_ACCOUNT_ID,
|
|
60
|
+
source: "fallback",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the default account ID.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
|
|
68
|
+
return resolveDefaultFeishuAccountSelection(cfg).accountId;
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
/**
|
|
@@ -87,17 +108,75 @@ export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
|
|
87
108
|
encryptKey?: string;
|
|
88
109
|
verificationToken?: string;
|
|
89
110
|
domain: FeishuDomain;
|
|
111
|
+
} | null;
|
|
112
|
+
export function resolveFeishuCredentials(
|
|
113
|
+
cfg: FeishuConfig | undefined,
|
|
114
|
+
options: { allowUnresolvedSecretRef?: boolean },
|
|
115
|
+
): {
|
|
116
|
+
appId: string;
|
|
117
|
+
appSecret: string;
|
|
118
|
+
encryptKey?: string;
|
|
119
|
+
verificationToken?: string;
|
|
120
|
+
domain: FeishuDomain;
|
|
121
|
+
} | null;
|
|
122
|
+
export function resolveFeishuCredentials(
|
|
123
|
+
cfg?: FeishuConfig,
|
|
124
|
+
options?: { allowUnresolvedSecretRef?: boolean },
|
|
125
|
+
): {
|
|
126
|
+
appId: string;
|
|
127
|
+
appSecret: string;
|
|
128
|
+
encryptKey?: string;
|
|
129
|
+
verificationToken?: string;
|
|
130
|
+
domain: FeishuDomain;
|
|
90
131
|
} | null {
|
|
91
|
-
const
|
|
92
|
-
|
|
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
|
+
|
|
93
169
|
if (!appId || !appSecret) {
|
|
94
170
|
return null;
|
|
95
171
|
}
|
|
96
172
|
return {
|
|
97
173
|
appId,
|
|
98
174
|
appSecret,
|
|
99
|
-
encryptKey: cfg?.encryptKey
|
|
100
|
-
verificationToken:
|
|
175
|
+
encryptKey: normalizeString(cfg?.encryptKey),
|
|
176
|
+
verificationToken: resolveSecretLike(
|
|
177
|
+
cfg?.verificationToken,
|
|
178
|
+
"channels.feishu.verificationToken",
|
|
179
|
+
),
|
|
101
180
|
domain: cfg?.domain ?? "feishu",
|
|
102
181
|
};
|
|
103
182
|
}
|
|
@@ -111,9 +190,15 @@ export function resolveFeishuAccount(params: {
|
|
|
111
190
|
}): ResolvedFeishuAccount {
|
|
112
191
|
const hasExplicitAccountId =
|
|
113
192
|
typeof params.accountId === "string" && params.accountId.trim() !== "";
|
|
193
|
+
const defaultSelection = hasExplicitAccountId
|
|
194
|
+
? null
|
|
195
|
+
: resolveDefaultFeishuAccountSelection(params.cfg);
|
|
114
196
|
const accountId = hasExplicitAccountId
|
|
115
197
|
? normalizeAccountId(params.accountId)
|
|
116
|
-
:
|
|
198
|
+
: (defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID);
|
|
199
|
+
const selectionSource = hasExplicitAccountId
|
|
200
|
+
? "explicit"
|
|
201
|
+
: (defaultSelection?.source ?? "fallback");
|
|
117
202
|
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
|
118
203
|
|
|
119
204
|
// Base enabled state (top-level)
|
|
@@ -128,12 +213,14 @@ export function resolveFeishuAccount(params: {
|
|
|
128
213
|
|
|
129
214
|
// Resolve credentials from merged config
|
|
130
215
|
const creds = resolveFeishuCredentials(merged);
|
|
216
|
+
const accountName = (merged as FeishuAccountConfig).name;
|
|
131
217
|
|
|
132
218
|
return {
|
|
133
219
|
accountId,
|
|
220
|
+
selectionSource,
|
|
134
221
|
enabled,
|
|
135
222
|
configured: Boolean(creds),
|
|
136
|
-
name:
|
|
223
|
+
name: typeof accountName === "string" ? accountName.trim() || undefined : undefined,
|
|
137
224
|
appId: creds?.appId,
|
|
138
225
|
appSecret: creds?.appSecret,
|
|
139
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,
|
|
@@ -3,7 +3,7 @@ import { parseFeishuMessageEvent } from "./bot.js";
|
|
|
3
3
|
|
|
4
4
|
// Helper to build a minimal FeishuMessageEvent for testing
|
|
5
5
|
function makeEvent(
|
|
6
|
-
chatType: "p2p" | "group",
|
|
6
|
+
chatType: "p2p" | "group" | "private",
|
|
7
7
|
mentions?: Array<{ key: string; name: string; id: { open_id?: string } }>,
|
|
8
8
|
text = "hello",
|
|
9
9
|
) {
|
|
@@ -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" } },
|