moltbot-channel-feishu 0.0.8
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 +68 -0
- package/clawdbot.plugin.json +33 -0
- package/moltbot.plugin.json +33 -0
- package/package.json +86 -0
- package/src/api/client.ts +140 -0
- package/src/api/directory.ts +186 -0
- package/src/api/media.ts +335 -0
- package/src/api/messages.ts +290 -0
- package/src/api/reactions.ts +155 -0
- package/src/config/schema.ts +183 -0
- package/src/core/dispatcher.ts +227 -0
- package/src/core/gateway.ts +202 -0
- package/src/core/handler.ts +231 -0
- package/src/core/parser.ts +112 -0
- package/src/core/policy.ts +199 -0
- package/src/core/reply-dispatcher.ts +151 -0
- package/src/core/runtime.ts +27 -0
- package/src/index.ts +108 -0
- package/src/plugin/channel.ts +367 -0
- package/src/plugin/index.ts +28 -0
- package/src/plugin/onboarding.ts +378 -0
- package/src/types/clawdbot.d.ts +377 -0
- package/src/types/events.ts +72 -0
- package/src/types/index.ts +6 -0
- package/src/types/messages.ts +172 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding wizard for Feishu channel configuration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ChannelOnboardingAdapter,
|
|
7
|
+
ChannelOnboardingDmPolicy,
|
|
8
|
+
ClawdbotConfig,
|
|
9
|
+
DmPolicy,
|
|
10
|
+
WizardPrompter,
|
|
11
|
+
} from "clawdbot/plugin-sdk";
|
|
12
|
+
import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "clawdbot/plugin-sdk";
|
|
13
|
+
|
|
14
|
+
import type { Config } from "../config/schema.js";
|
|
15
|
+
import { resolveCredentials } from "../config/schema.js";
|
|
16
|
+
import { probeConnection } from "../api/client.js";
|
|
17
|
+
|
|
18
|
+
const channel = "feishu" as const;
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Config Helpers
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
function setDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
|
|
25
|
+
const existingAllowFrom = cfg.channels?.feishu?.allowFrom as (string | number)[] | undefined;
|
|
26
|
+
const allowFrom =
|
|
27
|
+
dmPolicy === "open"
|
|
28
|
+
? addWildcardAllowFrom(existingAllowFrom)?.map((entry) => String(entry))
|
|
29
|
+
: undefined;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...cfg,
|
|
33
|
+
channels: {
|
|
34
|
+
...cfg.channels,
|
|
35
|
+
feishu: {
|
|
36
|
+
...cfg.channels?.feishu,
|
|
37
|
+
dmPolicy,
|
|
38
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
|
45
|
+
return {
|
|
46
|
+
...cfg,
|
|
47
|
+
channels: {
|
|
48
|
+
...cfg.channels,
|
|
49
|
+
feishu: { ...cfg.channels?.feishu, allowFrom },
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setGroupPolicy(
|
|
55
|
+
cfg: ClawdbotConfig,
|
|
56
|
+
groupPolicy: "open" | "allowlist" | "disabled"
|
|
57
|
+
): ClawdbotConfig {
|
|
58
|
+
return {
|
|
59
|
+
...cfg,
|
|
60
|
+
channels: {
|
|
61
|
+
...cfg.channels,
|
|
62
|
+
feishu: { ...cfg.channels?.feishu, enabled: true, groupPolicy },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function setGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig {
|
|
68
|
+
return {
|
|
69
|
+
...cfg,
|
|
70
|
+
channels: {
|
|
71
|
+
...cfg.channels,
|
|
72
|
+
feishu: { ...cfg.channels?.feishu, groupAllowFrom },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseAllowFromInput(raw: string): string[] {
|
|
78
|
+
return raw
|
|
79
|
+
.split(/[\n,;]+/g)
|
|
80
|
+
.map((entry) => entry.trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Prompts
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
async function promptAllowFrom(params: {
|
|
89
|
+
cfg: ClawdbotConfig;
|
|
90
|
+
prompter: WizardPrompter;
|
|
91
|
+
}): Promise<ClawdbotConfig> {
|
|
92
|
+
const existing = (params.cfg.channels?.feishu?.allowFrom ?? []) as (string | number)[];
|
|
93
|
+
|
|
94
|
+
await params.prompter.note(
|
|
95
|
+
[
|
|
96
|
+
"Allowlist Feishu DMs by open_id or user_id.",
|
|
97
|
+
"Find user open_id in Feishu admin console or via API.",
|
|
98
|
+
"Examples:",
|
|
99
|
+
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
100
|
+
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
101
|
+
].join("\n"),
|
|
102
|
+
"Feishu allowlist"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
while (true) {
|
|
106
|
+
const entry = await params.prompter.text({
|
|
107
|
+
message: "Feishu allowFrom (user open_ids)",
|
|
108
|
+
placeholder: "ou_xxxxx, ou_yyyyy",
|
|
109
|
+
initialValue: existing.length > 0 ? String(existing[0]) : undefined,
|
|
110
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const parts = parseAllowFromInput(String(entry));
|
|
114
|
+
if (parts.length === 0) {
|
|
115
|
+
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const unique = [
|
|
120
|
+
...new Set([
|
|
121
|
+
...existing.map((v: string | number) => String(v).trim()).filter(Boolean),
|
|
122
|
+
...parts,
|
|
123
|
+
]),
|
|
124
|
+
];
|
|
125
|
+
return setAllowFrom(params.cfg, unique);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function showCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
|
130
|
+
await prompter.note(
|
|
131
|
+
[
|
|
132
|
+
"1) Go to Feishu Open Platform (open.feishu.cn)",
|
|
133
|
+
"2) Create a self-built app",
|
|
134
|
+
"3) Get App ID and App Secret from Credentials page",
|
|
135
|
+
"4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
|
|
136
|
+
"5) Publish the app or add it to a test group",
|
|
137
|
+
"Tip: set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.",
|
|
138
|
+
`Docs: ${formatDocsLink("/channels/feishu", "feishu")}`,
|
|
139
|
+
].join("\n"),
|
|
140
|
+
"Feishu credentials"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// DM Policy Adapter
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
149
|
+
label: "Feishu",
|
|
150
|
+
channel,
|
|
151
|
+
policyKey: "channels.feishu.dmPolicy",
|
|
152
|
+
allowFromKey: "channels.feishu.allowFrom",
|
|
153
|
+
getCurrent: (cfg) => (cfg.channels?.feishu as Config | undefined)?.dmPolicy ?? "pairing",
|
|
154
|
+
setPolicy: (cfg, policy) => setDmPolicy(cfg, policy),
|
|
155
|
+
promptAllowFrom,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Onboarding Adapter
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
export const feishuOnboarding: ChannelOnboardingAdapter = {
|
|
163
|
+
channel,
|
|
164
|
+
|
|
165
|
+
getStatus: async ({ cfg }) => {
|
|
166
|
+
const feishuCfg = cfg.channels?.feishu as Config | undefined;
|
|
167
|
+
const configured = Boolean(resolveCredentials(feishuCfg));
|
|
168
|
+
|
|
169
|
+
let probeResult = null;
|
|
170
|
+
if (configured && feishuCfg) {
|
|
171
|
+
try {
|
|
172
|
+
probeResult = await probeConnection(feishuCfg);
|
|
173
|
+
} catch {
|
|
174
|
+
// Ignore probe errors
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const statusLines: string[] = [];
|
|
179
|
+
if (!configured) {
|
|
180
|
+
statusLines.push("Feishu: needs app credentials");
|
|
181
|
+
} else if (probeResult?.ok) {
|
|
182
|
+
statusLines.push(
|
|
183
|
+
`Feishu: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
statusLines.push("Feishu: configured (connection not verified)");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
channel,
|
|
191
|
+
configured,
|
|
192
|
+
statusLines,
|
|
193
|
+
selectionHint: configured ? "configured" : "needs app creds",
|
|
194
|
+
quickstartScore: configured ? 2 : 0,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
configure: async ({ cfg, prompter }) => {
|
|
199
|
+
const feishuCfg = cfg.channels?.feishu as Config | undefined;
|
|
200
|
+
const resolved = resolveCredentials(feishuCfg);
|
|
201
|
+
const hasConfigCreds = Boolean(feishuCfg?.appId?.trim() && feishuCfg?.appSecret?.trim());
|
|
202
|
+
const canUseEnv = Boolean(
|
|
203
|
+
!hasConfigCreds &&
|
|
204
|
+
process.env["FEISHU_APP_ID"]?.trim() &&
|
|
205
|
+
process.env["FEISHU_APP_SECRET"]?.trim()
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
let next = cfg;
|
|
209
|
+
let appId: string | null = null;
|
|
210
|
+
let appSecret: string | null = null;
|
|
211
|
+
|
|
212
|
+
if (!resolved) {
|
|
213
|
+
await showCredentialHelp(prompter);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for env vars
|
|
217
|
+
if (canUseEnv) {
|
|
218
|
+
const keepEnv = await prompter.confirm({
|
|
219
|
+
message: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
220
|
+
initialValue: true,
|
|
221
|
+
});
|
|
222
|
+
if (keepEnv) {
|
|
223
|
+
next = {
|
|
224
|
+
...next,
|
|
225
|
+
channels: {
|
|
226
|
+
...next.channels,
|
|
227
|
+
feishu: { ...next.channels?.feishu, enabled: true },
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
} else {
|
|
231
|
+
appId = String(
|
|
232
|
+
await prompter.text({
|
|
233
|
+
message: "Enter Feishu App ID",
|
|
234
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
235
|
+
})
|
|
236
|
+
).trim();
|
|
237
|
+
appSecret = String(
|
|
238
|
+
await prompter.text({
|
|
239
|
+
message: "Enter Feishu App Secret",
|
|
240
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
241
|
+
})
|
|
242
|
+
).trim();
|
|
243
|
+
}
|
|
244
|
+
} else if (hasConfigCreds) {
|
|
245
|
+
const keep = await prompter.confirm({
|
|
246
|
+
message: "Feishu credentials already configured. Keep them?",
|
|
247
|
+
initialValue: true,
|
|
248
|
+
});
|
|
249
|
+
if (!keep) {
|
|
250
|
+
appId = String(
|
|
251
|
+
await prompter.text({
|
|
252
|
+
message: "Enter Feishu App ID",
|
|
253
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
254
|
+
})
|
|
255
|
+
).trim();
|
|
256
|
+
appSecret = String(
|
|
257
|
+
await prompter.text({
|
|
258
|
+
message: "Enter Feishu App Secret",
|
|
259
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
260
|
+
})
|
|
261
|
+
).trim();
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
appId = String(
|
|
265
|
+
await prompter.text({
|
|
266
|
+
message: "Enter Feishu App ID",
|
|
267
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
268
|
+
})
|
|
269
|
+
).trim();
|
|
270
|
+
appSecret = String(
|
|
271
|
+
await prompter.text({
|
|
272
|
+
message: "Enter Feishu App Secret",
|
|
273
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
274
|
+
})
|
|
275
|
+
).trim();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Apply credentials
|
|
279
|
+
if (appId && appSecret) {
|
|
280
|
+
next = {
|
|
281
|
+
...next,
|
|
282
|
+
channels: {
|
|
283
|
+
...next.channels,
|
|
284
|
+
feishu: {
|
|
285
|
+
...next.channels?.feishu,
|
|
286
|
+
enabled: true,
|
|
287
|
+
appId,
|
|
288
|
+
appSecret,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Test connection
|
|
294
|
+
const testCfg = next.channels?.feishu as Config;
|
|
295
|
+
try {
|
|
296
|
+
const probe = await probeConnection(testCfg);
|
|
297
|
+
if (probe.ok) {
|
|
298
|
+
await prompter.note(
|
|
299
|
+
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
300
|
+
"Feishu connection test"
|
|
301
|
+
);
|
|
302
|
+
} else {
|
|
303
|
+
await prompter.note(
|
|
304
|
+
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
305
|
+
"Feishu connection test"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
await prompter.note(`Connection test failed: ${String(err)}`, "Feishu connection test");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Domain selection
|
|
314
|
+
const currentDomain = (next.channels?.feishu as Config | undefined)?.domain ?? "feishu";
|
|
315
|
+
const domain = await prompter.select({
|
|
316
|
+
message: "Which Feishu domain?",
|
|
317
|
+
options: [
|
|
318
|
+
{ value: "feishu", label: "Feishu (feishu.cn) - China" },
|
|
319
|
+
{ value: "lark", label: "Lark (larksuite.com) - International" },
|
|
320
|
+
],
|
|
321
|
+
initialValue: currentDomain,
|
|
322
|
+
});
|
|
323
|
+
if (domain) {
|
|
324
|
+
next = {
|
|
325
|
+
...next,
|
|
326
|
+
channels: {
|
|
327
|
+
...next.channels,
|
|
328
|
+
feishu: {
|
|
329
|
+
...next.channels?.feishu,
|
|
330
|
+
domain: domain as "feishu" | "lark",
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Group policy
|
|
337
|
+
const groupPolicy = await prompter.select({
|
|
338
|
+
message: "Group chat policy",
|
|
339
|
+
options: [
|
|
340
|
+
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
341
|
+
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
342
|
+
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
343
|
+
],
|
|
344
|
+
initialValue: (next.channels?.feishu as Config | undefined)?.groupPolicy ?? "allowlist",
|
|
345
|
+
});
|
|
346
|
+
if (groupPolicy) {
|
|
347
|
+
next = setGroupPolicy(next, groupPolicy as "open" | "allowlist" | "disabled");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Group allowlist
|
|
351
|
+
if (groupPolicy === "allowlist") {
|
|
352
|
+
const existing = (next.channels?.feishu as Config | undefined)?.groupAllowFrom ?? [];
|
|
353
|
+
const entry = await prompter.text({
|
|
354
|
+
message: "Group chat allowlist (chat_ids)",
|
|
355
|
+
placeholder: "oc_xxxxx, oc_yyyyy",
|
|
356
|
+
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
357
|
+
});
|
|
358
|
+
if (entry) {
|
|
359
|
+
const parts = parseAllowFromInput(String(entry));
|
|
360
|
+
if (parts.length > 0) {
|
|
361
|
+
next = setGroupAllowFrom(next, parts);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
dmPolicy,
|
|
370
|
+
|
|
371
|
+
disable: (cfg) => ({
|
|
372
|
+
...cfg,
|
|
373
|
+
channels: {
|
|
374
|
+
...cfg.channels,
|
|
375
|
+
feishu: { ...cfg.channels?.feishu, enabled: false },
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
};
|