@noah-claw/feishu 0.1.0
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/LICENSE +21 -0
- package/README.md +317 -0
- package/index.ts +41 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +60 -0
- package/src/accounts.ts +53 -0
- package/src/bot.ts +654 -0
- package/src/channel.ts +224 -0
- package/src/client.ts +66 -0
- package/src/config-schema.ts +107 -0
- package/src/directory.ts +159 -0
- package/src/media.ts +515 -0
- package/src/monitor.ts +151 -0
- package/src/onboarding.ts +358 -0
- package/src/outbound.ts +40 -0
- package/src/policy.ts +92 -0
- package/src/probe.ts +46 -0
- package/src/reactions.ts +157 -0
- package/src/reply-dispatcher.ts +156 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +308 -0
- package/src/targets.ts +58 -0
- package/src/types.ts +50 -0
- package/src/typing.ts +73 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOnboardingAdapter,
|
|
3
|
+
ChannelOnboardingDmPolicy,
|
|
4
|
+
ClawdbotConfig,
|
|
5
|
+
DmPolicy,
|
|
6
|
+
WizardPrompter,
|
|
7
|
+
} from "clawdbot/plugin-sdk";
|
|
8
|
+
import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "clawdbot/plugin-sdk";
|
|
9
|
+
|
|
10
|
+
import { resolveFeishuCredentials } from "./accounts.js";
|
|
11
|
+
import { probeFeishu } from "./probe.js";
|
|
12
|
+
import type { FeishuConfig } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const channel = "feishu" as const;
|
|
15
|
+
|
|
16
|
+
function setFeishuDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
|
|
17
|
+
const allowFrom =
|
|
18
|
+
dmPolicy === "open"
|
|
19
|
+
? addWildcardAllowFrom(cfg.channels?.feishu?.allowFrom)?.map((entry) => String(entry))
|
|
20
|
+
: undefined;
|
|
21
|
+
return {
|
|
22
|
+
...cfg,
|
|
23
|
+
channels: {
|
|
24
|
+
...cfg.channels,
|
|
25
|
+
feishu: {
|
|
26
|
+
...cfg.channels?.feishu,
|
|
27
|
+
dmPolicy,
|
|
28
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
|
35
|
+
return {
|
|
36
|
+
...cfg,
|
|
37
|
+
channels: {
|
|
38
|
+
...cfg.channels,
|
|
39
|
+
feishu: {
|
|
40
|
+
...cfg.channels?.feishu,
|
|
41
|
+
allowFrom,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseAllowFromInput(raw: string): string[] {
|
|
48
|
+
return raw
|
|
49
|
+
.split(/[\n,;]+/g)
|
|
50
|
+
.map((entry) => entry.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function promptFeishuAllowFrom(params: {
|
|
55
|
+
cfg: ClawdbotConfig;
|
|
56
|
+
prompter: WizardPrompter;
|
|
57
|
+
}): Promise<ClawdbotConfig> {
|
|
58
|
+
const existing = params.cfg.channels?.feishu?.allowFrom ?? [];
|
|
59
|
+
await params.prompter.note(
|
|
60
|
+
[
|
|
61
|
+
"Allowlist Feishu DMs by open_id or user_id.",
|
|
62
|
+
"You can find user open_id in Feishu admin console or via API.",
|
|
63
|
+
"Examples:",
|
|
64
|
+
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
65
|
+
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
66
|
+
].join("\n"),
|
|
67
|
+
"Feishu allowlist",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
while (true) {
|
|
71
|
+
const entry = await params.prompter.text({
|
|
72
|
+
message: "Feishu allowFrom (user open_ids)",
|
|
73
|
+
placeholder: "ou_xxxxx, ou_yyyyy",
|
|
74
|
+
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
75
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
76
|
+
});
|
|
77
|
+
const parts = parseAllowFromInput(String(entry));
|
|
78
|
+
if (parts.length === 0) {
|
|
79
|
+
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const unique = [
|
|
84
|
+
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts]),
|
|
85
|
+
];
|
|
86
|
+
return setFeishuAllowFrom(params.cfg, unique);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
|
91
|
+
await prompter.note(
|
|
92
|
+
[
|
|
93
|
+
"1) Go to Feishu Open Platform (open.feishu.cn)",
|
|
94
|
+
"2) Create a self-built app",
|
|
95
|
+
"3) Get App ID and App Secret from Credentials page",
|
|
96
|
+
"4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
|
|
97
|
+
"5) Publish the app or add it to a test group",
|
|
98
|
+
"Tip: you can also set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.",
|
|
99
|
+
`Docs: ${formatDocsLink("/channels/feishu", "feishu")}`,
|
|
100
|
+
].join("\n"),
|
|
101
|
+
"Feishu credentials",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setFeishuGroupPolicy(
|
|
106
|
+
cfg: ClawdbotConfig,
|
|
107
|
+
groupPolicy: "open" | "allowlist" | "disabled",
|
|
108
|
+
): ClawdbotConfig {
|
|
109
|
+
return {
|
|
110
|
+
...cfg,
|
|
111
|
+
channels: {
|
|
112
|
+
...cfg.channels,
|
|
113
|
+
feishu: {
|
|
114
|
+
...cfg.channels?.feishu,
|
|
115
|
+
enabled: true,
|
|
116
|
+
groupPolicy,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setFeishuGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig {
|
|
123
|
+
return {
|
|
124
|
+
...cfg,
|
|
125
|
+
channels: {
|
|
126
|
+
...cfg.channels,
|
|
127
|
+
feishu: {
|
|
128
|
+
...cfg.channels?.feishu,
|
|
129
|
+
groupAllowFrom,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
136
|
+
label: "Feishu",
|
|
137
|
+
channel,
|
|
138
|
+
policyKey: "channels.feishu.dmPolicy",
|
|
139
|
+
allowFromKey: "channels.feishu.allowFrom",
|
|
140
|
+
getCurrent: (cfg) => (cfg.channels?.feishu as FeishuConfig | undefined)?.dmPolicy ?? "pairing",
|
|
141
|
+
setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy),
|
|
142
|
+
promptAllowFrom: promptFeishuAllowFrom,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
146
|
+
channel,
|
|
147
|
+
getStatus: async ({ cfg }) => {
|
|
148
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
149
|
+
const configured = Boolean(resolveFeishuCredentials(feishuCfg));
|
|
150
|
+
|
|
151
|
+
// Try to probe if configured
|
|
152
|
+
let probeResult = null;
|
|
153
|
+
if (configured && feishuCfg) {
|
|
154
|
+
try {
|
|
155
|
+
probeResult = await probeFeishu(feishuCfg);
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore probe errors
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const statusLines: string[] = [];
|
|
162
|
+
if (!configured) {
|
|
163
|
+
statusLines.push("Feishu: needs app credentials");
|
|
164
|
+
} else if (probeResult?.ok) {
|
|
165
|
+
statusLines.push(`Feishu: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`);
|
|
166
|
+
} else {
|
|
167
|
+
statusLines.push("Feishu: configured (connection not verified)");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
channel,
|
|
172
|
+
configured,
|
|
173
|
+
statusLines,
|
|
174
|
+
selectionHint: configured ? "configured" : "needs app creds",
|
|
175
|
+
quickstartScore: configured ? 2 : 0,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
configure: async ({ cfg, prompter }) => {
|
|
180
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
181
|
+
const resolved = resolveFeishuCredentials(feishuCfg);
|
|
182
|
+
const hasConfigCreds = Boolean(feishuCfg?.appId?.trim() && feishuCfg?.appSecret?.trim());
|
|
183
|
+
const canUseEnv = Boolean(
|
|
184
|
+
!hasConfigCreds &&
|
|
185
|
+
process.env.FEISHU_APP_ID?.trim() &&
|
|
186
|
+
process.env.FEISHU_APP_SECRET?.trim(),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
let next = cfg;
|
|
190
|
+
let appId: string | null = null;
|
|
191
|
+
let appSecret: string | null = null;
|
|
192
|
+
|
|
193
|
+
if (!resolved) {
|
|
194
|
+
await noteFeishuCredentialHelp(prompter);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (canUseEnv) {
|
|
198
|
+
const keepEnv = await prompter.confirm({
|
|
199
|
+
message: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
200
|
+
initialValue: true,
|
|
201
|
+
});
|
|
202
|
+
if (keepEnv) {
|
|
203
|
+
next = {
|
|
204
|
+
...next,
|
|
205
|
+
channels: {
|
|
206
|
+
...next.channels,
|
|
207
|
+
feishu: { ...next.channels?.feishu, enabled: true },
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
} else {
|
|
211
|
+
appId = String(
|
|
212
|
+
await prompter.text({
|
|
213
|
+
message: "Enter Feishu App ID",
|
|
214
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
215
|
+
}),
|
|
216
|
+
).trim();
|
|
217
|
+
appSecret = String(
|
|
218
|
+
await prompter.text({
|
|
219
|
+
message: "Enter Feishu App Secret",
|
|
220
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
221
|
+
}),
|
|
222
|
+
).trim();
|
|
223
|
+
}
|
|
224
|
+
} else if (hasConfigCreds) {
|
|
225
|
+
const keep = await prompter.confirm({
|
|
226
|
+
message: "Feishu credentials already configured. Keep them?",
|
|
227
|
+
initialValue: true,
|
|
228
|
+
});
|
|
229
|
+
if (!keep) {
|
|
230
|
+
appId = String(
|
|
231
|
+
await prompter.text({
|
|
232
|
+
message: "Enter Feishu App ID",
|
|
233
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
234
|
+
}),
|
|
235
|
+
).trim();
|
|
236
|
+
appSecret = String(
|
|
237
|
+
await prompter.text({
|
|
238
|
+
message: "Enter Feishu App Secret",
|
|
239
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
240
|
+
}),
|
|
241
|
+
).trim();
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
appId = String(
|
|
245
|
+
await prompter.text({
|
|
246
|
+
message: "Enter Feishu App ID",
|
|
247
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
248
|
+
}),
|
|
249
|
+
).trim();
|
|
250
|
+
appSecret = String(
|
|
251
|
+
await prompter.text({
|
|
252
|
+
message: "Enter Feishu App Secret",
|
|
253
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
254
|
+
}),
|
|
255
|
+
).trim();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (appId && appSecret) {
|
|
259
|
+
next = {
|
|
260
|
+
...next,
|
|
261
|
+
channels: {
|
|
262
|
+
...next.channels,
|
|
263
|
+
feishu: {
|
|
264
|
+
...next.channels?.feishu,
|
|
265
|
+
enabled: true,
|
|
266
|
+
appId,
|
|
267
|
+
appSecret,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Test connection
|
|
273
|
+
const testCfg = next.channels?.feishu as FeishuConfig;
|
|
274
|
+
try {
|
|
275
|
+
const probe = await probeFeishu(testCfg);
|
|
276
|
+
if (probe.ok) {
|
|
277
|
+
await prompter.note(
|
|
278
|
+
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
279
|
+
"Feishu connection test",
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
await prompter.note(
|
|
283
|
+
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
284
|
+
"Feishu connection test",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
await prompter.note(`Connection test failed: ${String(err)}`, "Feishu connection test");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Domain selection
|
|
293
|
+
const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu";
|
|
294
|
+
const domain = await prompter.select({
|
|
295
|
+
message: "Which Feishu domain?",
|
|
296
|
+
options: [
|
|
297
|
+
{ value: "feishu", label: "Feishu (feishu.cn) - China" },
|
|
298
|
+
{ value: "lark", label: "Lark (larksuite.com) - International" },
|
|
299
|
+
],
|
|
300
|
+
initialValue: currentDomain,
|
|
301
|
+
});
|
|
302
|
+
if (domain) {
|
|
303
|
+
next = {
|
|
304
|
+
...next,
|
|
305
|
+
channels: {
|
|
306
|
+
...next.channels,
|
|
307
|
+
feishu: {
|
|
308
|
+
...next.channels?.feishu,
|
|
309
|
+
domain: domain as "feishu" | "lark",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Group policy
|
|
316
|
+
const groupPolicy = await prompter.select({
|
|
317
|
+
message: "Group chat policy",
|
|
318
|
+
options: [
|
|
319
|
+
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
320
|
+
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
321
|
+
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
322
|
+
],
|
|
323
|
+
initialValue:
|
|
324
|
+
(next.channels?.feishu as FeishuConfig | undefined)?.groupPolicy ?? "allowlist",
|
|
325
|
+
});
|
|
326
|
+
if (groupPolicy) {
|
|
327
|
+
next = setFeishuGroupPolicy(next, groupPolicy as "open" | "allowlist" | "disabled");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Group allowlist if needed
|
|
331
|
+
if (groupPolicy === "allowlist") {
|
|
332
|
+
const existing = (next.channels?.feishu as FeishuConfig | undefined)?.groupAllowFrom ?? [];
|
|
333
|
+
const entry = await prompter.text({
|
|
334
|
+
message: "Group chat allowlist (chat_ids)",
|
|
335
|
+
placeholder: "oc_xxxxx, oc_yyyyy",
|
|
336
|
+
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
337
|
+
});
|
|
338
|
+
if (entry) {
|
|
339
|
+
const parts = parseAllowFromInput(String(entry));
|
|
340
|
+
if (parts.length > 0) {
|
|
341
|
+
next = setFeishuGroupAllowFrom(next, parts);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
dmPolicy,
|
|
350
|
+
|
|
351
|
+
disable: (cfg) => ({
|
|
352
|
+
...cfg,
|
|
353
|
+
channels: {
|
|
354
|
+
...cfg.channels,
|
|
355
|
+
feishu: { ...cfg.channels?.feishu, enabled: false },
|
|
356
|
+
},
|
|
357
|
+
}),
|
|
358
|
+
};
|
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChannelOutboundAdapter } from "clawdbot/plugin-sdk";
|
|
2
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
3
|
+
import { sendMessageFeishu } from "./send.js";
|
|
4
|
+
import { sendMediaFeishu } from "./media.js";
|
|
5
|
+
|
|
6
|
+
export const feishuOutbound: ChannelOutboundAdapter = {
|
|
7
|
+
deliveryMode: "direct",
|
|
8
|
+
chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
9
|
+
chunkerMode: "markdown",
|
|
10
|
+
textChunkLimit: 4000,
|
|
11
|
+
sendText: async ({ cfg, to, text }) => {
|
|
12
|
+
const result = await sendMessageFeishu({ cfg, to, text });
|
|
13
|
+
return { channel: "feishu", ...result };
|
|
14
|
+
},
|
|
15
|
+
sendMedia: async ({ cfg, to, text, mediaUrl }) => {
|
|
16
|
+
// Send text first if provided
|
|
17
|
+
if (text?.trim()) {
|
|
18
|
+
await sendMessageFeishu({ cfg, to, text });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Upload and send media if URL provided
|
|
22
|
+
if (mediaUrl) {
|
|
23
|
+
try {
|
|
24
|
+
const result = await sendMediaFeishu({ cfg, to, mediaUrl });
|
|
25
|
+
return { channel: "feishu", ...result };
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// Log the error for debugging
|
|
28
|
+
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
|
29
|
+
// Fallback to URL link if upload fails
|
|
30
|
+
const fallbackText = `📎 ${mediaUrl}`;
|
|
31
|
+
const result = await sendMessageFeishu({ cfg, to, text: fallbackText });
|
|
32
|
+
return { channel: "feishu", ...result };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// No media URL, just return text result
|
|
37
|
+
const result = await sendMessageFeishu({ cfg, to, text: text ?? "" });
|
|
38
|
+
return { channel: "feishu", ...result };
|
|
39
|
+
},
|
|
40
|
+
};
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ChannelGroupContext, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
|
2
|
+
import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export type FeishuAllowlistMatch = {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
matchKey?: string;
|
|
7
|
+
matchSource?: "wildcard" | "id" | "name";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function resolveFeishuAllowlistMatch(params: {
|
|
11
|
+
allowFrom: Array<string | number>;
|
|
12
|
+
senderId: string;
|
|
13
|
+
senderName?: string | null;
|
|
14
|
+
}): FeishuAllowlistMatch {
|
|
15
|
+
const allowFrom = params.allowFrom
|
|
16
|
+
.map((entry) => String(entry).trim().toLowerCase())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
|
|
19
|
+
if (allowFrom.length === 0) return { allowed: false };
|
|
20
|
+
if (allowFrom.includes("*")) {
|
|
21
|
+
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const senderId = params.senderId.toLowerCase();
|
|
25
|
+
if (allowFrom.includes(senderId)) {
|
|
26
|
+
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const senderName = params.senderName?.toLowerCase();
|
|
30
|
+
if (senderName && allowFrom.includes(senderName)) {
|
|
31
|
+
return { allowed: true, matchKey: senderName, matchSource: "name" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { allowed: false };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveFeishuGroupConfig(params: {
|
|
38
|
+
cfg?: FeishuConfig;
|
|
39
|
+
groupId?: string | null;
|
|
40
|
+
}): FeishuGroupConfig | undefined {
|
|
41
|
+
const groups = params.cfg?.groups ?? {};
|
|
42
|
+
const groupId = params.groupId?.trim();
|
|
43
|
+
if (!groupId) return undefined;
|
|
44
|
+
|
|
45
|
+
const direct = groups[groupId] as FeishuGroupConfig | undefined;
|
|
46
|
+
if (direct) return direct;
|
|
47
|
+
|
|
48
|
+
const lowered = groupId.toLowerCase();
|
|
49
|
+
const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
|
|
50
|
+
return matchKey ? (groups[matchKey] as FeishuGroupConfig | undefined) : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveFeishuGroupToolPolicy(
|
|
54
|
+
params: ChannelGroupContext,
|
|
55
|
+
): GroupToolPolicyConfig | undefined {
|
|
56
|
+
const cfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
|
57
|
+
if (!cfg) return undefined;
|
|
58
|
+
|
|
59
|
+
const groupConfig = resolveFeishuGroupConfig({
|
|
60
|
+
cfg,
|
|
61
|
+
groupId: params.groupId,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return groupConfig?.tools;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isFeishuGroupAllowed(params: {
|
|
68
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
69
|
+
allowFrom: Array<string | number>;
|
|
70
|
+
senderId: string;
|
|
71
|
+
senderName?: string | null;
|
|
72
|
+
}): boolean {
|
|
73
|
+
const { groupPolicy } = params;
|
|
74
|
+
if (groupPolicy === "disabled") return false;
|
|
75
|
+
if (groupPolicy === "open") return true;
|
|
76
|
+
return resolveFeishuAllowlistMatch(params).allowed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveFeishuReplyPolicy(params: {
|
|
80
|
+
isDirectMessage: boolean;
|
|
81
|
+
globalConfig?: FeishuConfig;
|
|
82
|
+
groupConfig?: FeishuGroupConfig;
|
|
83
|
+
}): { requireMention: boolean } {
|
|
84
|
+
if (params.isDirectMessage) {
|
|
85
|
+
return { requireMention: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const requireMention =
|
|
89
|
+
params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? true;
|
|
90
|
+
|
|
91
|
+
return { requireMention };
|
|
92
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FeishuConfig, FeishuProbeResult } from "./types.js";
|
|
2
|
+
import { createFeishuClient } from "./client.js";
|
|
3
|
+
import { resolveFeishuCredentials } from "./accounts.js";
|
|
4
|
+
|
|
5
|
+
export async function probeFeishu(cfg?: FeishuConfig): Promise<FeishuProbeResult> {
|
|
6
|
+
const creds = resolveFeishuCredentials(cfg);
|
|
7
|
+
if (!creds) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
error: "missing credentials (appId, appSecret)",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const client = createFeishuClient(cfg!);
|
|
16
|
+
// Use im.chat.list as a simple connectivity test
|
|
17
|
+
// The bot info API path varies by SDK version
|
|
18
|
+
const response = await (client as any).request({
|
|
19
|
+
method: "GET",
|
|
20
|
+
url: "/open-apis/bot/v3/info",
|
|
21
|
+
data: {},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (response.code !== 0) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
appId: creds.appId,
|
|
28
|
+
error: `API error: ${response.msg || `code ${response.code}`}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const bot = response.bot || response.data?.bot;
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
appId: creds.appId,
|
|
36
|
+
botName: bot?.bot_name,
|
|
37
|
+
botOpenId: bot?.open_id,
|
|
38
|
+
};
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
appId: creds.appId,
|
|
43
|
+
error: err instanceof Error ? err.message : String(err),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/reactions.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
|
2
|
+
import type { FeishuConfig } from "./types.js";
|
|
3
|
+
import { createFeishuClient } from "./client.js";
|
|
4
|
+
|
|
5
|
+
export type FeishuReaction = {
|
|
6
|
+
reactionId: string;
|
|
7
|
+
emojiType: string;
|
|
8
|
+
operatorType: "app" | "user";
|
|
9
|
+
operatorId: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Add a reaction (emoji) to a message.
|
|
14
|
+
* @param emojiType - Feishu emoji type, e.g., "SMILE", "THUMBSUP", "HEART"
|
|
15
|
+
* @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
|
|
16
|
+
*/
|
|
17
|
+
export async function addReactionFeishu(params: {
|
|
18
|
+
cfg: ClawdbotConfig;
|
|
19
|
+
messageId: string;
|
|
20
|
+
emojiType: string;
|
|
21
|
+
}): Promise<{ reactionId: string }> {
|
|
22
|
+
const { cfg, messageId, emojiType } = params;
|
|
23
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
24
|
+
if (!feishuCfg) {
|
|
25
|
+
throw new Error("Feishu channel not configured");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const client = createFeishuClient(feishuCfg);
|
|
29
|
+
|
|
30
|
+
const response = (await client.im.messageReaction.create({
|
|
31
|
+
path: { message_id: messageId },
|
|
32
|
+
data: {
|
|
33
|
+
reaction_type: {
|
|
34
|
+
emoji_type: emojiType,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
})) as {
|
|
38
|
+
code?: number;
|
|
39
|
+
msg?: string;
|
|
40
|
+
data?: { reaction_id?: string };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (response.code !== 0) {
|
|
44
|
+
throw new Error(`Feishu add reaction failed: ${response.msg || `code ${response.code}`}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const reactionId = response.data?.reaction_id;
|
|
48
|
+
if (!reactionId) {
|
|
49
|
+
throw new Error("Feishu add reaction failed: no reaction_id returned");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { reactionId };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Remove a reaction from a message.
|
|
57
|
+
*/
|
|
58
|
+
export async function removeReactionFeishu(params: {
|
|
59
|
+
cfg: ClawdbotConfig;
|
|
60
|
+
messageId: string;
|
|
61
|
+
reactionId: string;
|
|
62
|
+
}): Promise<void> {
|
|
63
|
+
const { cfg, messageId, reactionId } = params;
|
|
64
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
65
|
+
if (!feishuCfg) {
|
|
66
|
+
throw new Error("Feishu channel not configured");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const client = createFeishuClient(feishuCfg);
|
|
70
|
+
|
|
71
|
+
const response = (await client.im.messageReaction.delete({
|
|
72
|
+
path: {
|
|
73
|
+
message_id: messageId,
|
|
74
|
+
reaction_id: reactionId,
|
|
75
|
+
},
|
|
76
|
+
})) as { code?: number; msg?: string };
|
|
77
|
+
|
|
78
|
+
if (response.code !== 0) {
|
|
79
|
+
throw new Error(`Feishu remove reaction failed: ${response.msg || `code ${response.code}`}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List all reactions for a message.
|
|
85
|
+
*/
|
|
86
|
+
export async function listReactionsFeishu(params: {
|
|
87
|
+
cfg: ClawdbotConfig;
|
|
88
|
+
messageId: string;
|
|
89
|
+
emojiType?: string;
|
|
90
|
+
}): Promise<FeishuReaction[]> {
|
|
91
|
+
const { cfg, messageId, emojiType } = params;
|
|
92
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
93
|
+
if (!feishuCfg) {
|
|
94
|
+
throw new Error("Feishu channel not configured");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const client = createFeishuClient(feishuCfg);
|
|
98
|
+
|
|
99
|
+
const response = (await client.im.messageReaction.list({
|
|
100
|
+
path: { message_id: messageId },
|
|
101
|
+
params: emojiType ? { reaction_type: emojiType } : undefined,
|
|
102
|
+
})) as {
|
|
103
|
+
code?: number;
|
|
104
|
+
msg?: string;
|
|
105
|
+
data?: {
|
|
106
|
+
items?: Array<{
|
|
107
|
+
reaction_id?: string;
|
|
108
|
+
reaction_type?: { emoji_type?: string };
|
|
109
|
+
operator_type?: string;
|
|
110
|
+
operator_id?: { open_id?: string; user_id?: string; union_id?: string };
|
|
111
|
+
}>;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (response.code !== 0) {
|
|
116
|
+
throw new Error(`Feishu list reactions failed: ${response.msg || `code ${response.code}`}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const items = response.data?.items ?? [];
|
|
120
|
+
return items.map((item) => ({
|
|
121
|
+
reactionId: item.reaction_id ?? "",
|
|
122
|
+
emojiType: item.reaction_type?.emoji_type ?? "",
|
|
123
|
+
operatorType: item.operator_type === "app" ? "app" : "user",
|
|
124
|
+
operatorId:
|
|
125
|
+
item.operator_id?.open_id ?? item.operator_id?.user_id ?? item.operator_id?.union_id ?? "",
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Common Feishu emoji types for convenience.
|
|
131
|
+
* @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
|
|
132
|
+
*/
|
|
133
|
+
export const FeishuEmoji = {
|
|
134
|
+
// Common reactions
|
|
135
|
+
THUMBSUP: "THUMBSUP",
|
|
136
|
+
THUMBSDOWN: "THUMBSDOWN",
|
|
137
|
+
HEART: "HEART",
|
|
138
|
+
SMILE: "SMILE",
|
|
139
|
+
GRINNING: "GRINNING",
|
|
140
|
+
LAUGHING: "LAUGHING",
|
|
141
|
+
CRY: "CRY",
|
|
142
|
+
ANGRY: "ANGRY",
|
|
143
|
+
SURPRISED: "SURPRISED",
|
|
144
|
+
THINKING: "THINKING",
|
|
145
|
+
CLAP: "CLAP",
|
|
146
|
+
OK: "OK",
|
|
147
|
+
FIST: "FIST",
|
|
148
|
+
PRAY: "PRAY",
|
|
149
|
+
FIRE: "FIRE",
|
|
150
|
+
PARTY: "PARTY",
|
|
151
|
+
CHECK: "CHECK",
|
|
152
|
+
CROSS: "CROSS",
|
|
153
|
+
QUESTION: "QUESTION",
|
|
154
|
+
EXCLAMATION: "EXCLAMATION",
|
|
155
|
+
} as const;
|
|
156
|
+
|
|
157
|
+
export type FeishuEmojiType = (typeof FeishuEmoji)[keyof typeof FeishuEmoji];
|