@bobotu/feishu-fork 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 +922 -0
- package/index.ts +65 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +72 -0
- package/skills/feishu-doc/SKILL.md +161 -0
- package/skills/feishu-doc/references/block-types.md +102 -0
- package/skills/feishu-drive/SKILL.md +96 -0
- package/skills/feishu-perm/SKILL.md +90 -0
- package/skills/feishu-task/SKILL.md +210 -0
- package/skills/feishu-wiki/SKILL.md +96 -0
- package/src/accounts.ts +140 -0
- package/src/bitable-tools/actions.ts +199 -0
- package/src/bitable-tools/common.ts +90 -0
- package/src/bitable-tools/index.ts +1 -0
- package/src/bitable-tools/meta.ts +80 -0
- package/src/bitable-tools/register.ts +195 -0
- package/src/bitable-tools/schemas.ts +221 -0
- package/src/bot.ts +1125 -0
- package/src/channel.ts +334 -0
- package/src/client.ts +114 -0
- package/src/config-schema.ts +237 -0
- package/src/dedup.ts +54 -0
- package/src/directory.ts +165 -0
- package/src/doc-tools/actions.ts +341 -0
- package/src/doc-tools/common.ts +33 -0
- package/src/doc-tools/index.ts +2 -0
- package/src/doc-tools/register.ts +90 -0
- package/src/doc-tools/schemas.ts +85 -0
- package/src/doc-write-service.ts +711 -0
- package/src/drive-tools/actions.ts +182 -0
- package/src/drive-tools/common.ts +18 -0
- package/src/drive-tools/index.ts +2 -0
- package/src/drive-tools/register.ts +71 -0
- package/src/drive-tools/schemas.ts +67 -0
- package/src/dynamic-agent.ts +135 -0
- package/src/external-keys.ts +19 -0
- package/src/media.ts +510 -0
- package/src/mention.ts +121 -0
- package/src/monitor.ts +323 -0
- package/src/onboarding.ts +449 -0
- package/src/outbound.ts +40 -0
- package/src/perm-tools/actions.ts +111 -0
- package/src/perm-tools/common.ts +18 -0
- package/src/perm-tools/index.ts +2 -0
- package/src/perm-tools/register.ts +65 -0
- package/src/perm-tools/schemas.ts +52 -0
- package/src/policy.ts +117 -0
- package/src/probe.ts +147 -0
- package/src/reactions.ts +160 -0
- package/src/reply-dispatcher.ts +240 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +391 -0
- package/src/streaming-card.ts +211 -0
- package/src/targets.ts +58 -0
- package/src/task-tools/actions.ts +590 -0
- package/src/task-tools/common.ts +18 -0
- package/src/task-tools/constants.ts +13 -0
- package/src/task-tools/index.ts +1 -0
- package/src/task-tools/register.ts +263 -0
- package/src/task-tools/schemas.ts +567 -0
- package/src/text/markdown-links.ts +104 -0
- package/src/tools-common/feishu-api.ts +184 -0
- package/src/tools-common/tool-context.ts +23 -0
- package/src/tools-common/tool-exec.ts +73 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +79 -0
- package/src/typing.ts +75 -0
- package/src/wiki-tools/actions.ts +166 -0
- package/src/wiki-tools/common.ts +18 -0
- package/src/wiki-tools/index.ts +2 -0
- package/src/wiki-tools/register.ts +66 -0
- package/src/wiki-tools/schemas.ts +55 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOnboardingAdapter,
|
|
3
|
+
ChannelOnboardingDmPolicy,
|
|
4
|
+
ClawdbotConfig,
|
|
5
|
+
DmPolicy,
|
|
6
|
+
WizardPrompter,
|
|
7
|
+
} from "openclaw/plugin-sdk";
|
|
8
|
+
import {
|
|
9
|
+
addWildcardAllowFrom,
|
|
10
|
+
DEFAULT_ACCOUNT_ID,
|
|
11
|
+
formatDocsLink,
|
|
12
|
+
normalizeAccountId,
|
|
13
|
+
promptAccountId,
|
|
14
|
+
} from "openclaw/plugin-sdk";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
listFeishuAccountIds,
|
|
18
|
+
resolveDefaultFeishuAccountId,
|
|
19
|
+
resolveFeishuAccount,
|
|
20
|
+
resolveFeishuCredentials,
|
|
21
|
+
} from "./accounts.js";
|
|
22
|
+
import { probeFeishu } from "./probe.js";
|
|
23
|
+
import type { FeishuConfig } from "./types.js";
|
|
24
|
+
|
|
25
|
+
const channel = "feishu" as const;
|
|
26
|
+
let onboardingDmPolicyAccountId: string | undefined;
|
|
27
|
+
|
|
28
|
+
function resolveOnboardingAccountId(cfg: ClawdbotConfig, accountId?: string | null): string {
|
|
29
|
+
const raw = accountId?.trim();
|
|
30
|
+
if (raw) {
|
|
31
|
+
return normalizeAccountId(raw);
|
|
32
|
+
}
|
|
33
|
+
return resolveDefaultFeishuAccountId(cfg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveDmPolicyAccountId(cfg: ClawdbotConfig, accountId?: string | null): string {
|
|
37
|
+
return resolveOnboardingAccountId(cfg, accountId ?? onboardingDmPolicyAccountId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function upsertFeishuAccountConfig(
|
|
41
|
+
cfg: ClawdbotConfig,
|
|
42
|
+
accountId: string,
|
|
43
|
+
patch: Partial<FeishuConfig>,
|
|
44
|
+
): ClawdbotConfig {
|
|
45
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
46
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
47
|
+
|
|
48
|
+
if (normalizedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
49
|
+
return {
|
|
50
|
+
...cfg,
|
|
51
|
+
channels: {
|
|
52
|
+
...cfg.channels,
|
|
53
|
+
feishu: {
|
|
54
|
+
...feishuCfg,
|
|
55
|
+
...patch,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const existingAccount = feishuCfg?.accounts?.[normalizedAccountId];
|
|
62
|
+
return {
|
|
63
|
+
...cfg,
|
|
64
|
+
channels: {
|
|
65
|
+
...cfg.channels,
|
|
66
|
+
feishu: {
|
|
67
|
+
...feishuCfg,
|
|
68
|
+
enabled: true,
|
|
69
|
+
accounts: {
|
|
70
|
+
...feishuCfg?.accounts,
|
|
71
|
+
[normalizedAccountId]: {
|
|
72
|
+
...existingAccount,
|
|
73
|
+
enabled: existingAccount?.enabled ?? true,
|
|
74
|
+
...patch,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setFeishuDmPolicy(
|
|
83
|
+
cfg: ClawdbotConfig,
|
|
84
|
+
dmPolicy: DmPolicy,
|
|
85
|
+
accountId?: string,
|
|
86
|
+
): ClawdbotConfig {
|
|
87
|
+
const resolvedAccountId = resolveDmPolicyAccountId(cfg, accountId);
|
|
88
|
+
const account = resolveFeishuAccount({ cfg, accountId: resolvedAccountId });
|
|
89
|
+
// Feishu channel config does not support "disabled" as a dmPolicy value.
|
|
90
|
+
const effectiveDmPolicy = dmPolicy === "disabled" ? "pairing" : dmPolicy;
|
|
91
|
+
const allowFrom =
|
|
92
|
+
effectiveDmPolicy === "open"
|
|
93
|
+
? addWildcardAllowFrom(account.config.allowFrom)?.map((entry) => String(entry))
|
|
94
|
+
: undefined;
|
|
95
|
+
return upsertFeishuAccountConfig(cfg, resolvedAccountId, {
|
|
96
|
+
dmPolicy: effectiveDmPolicy,
|
|
97
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[], accountId?: string): ClawdbotConfig {
|
|
102
|
+
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
103
|
+
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { allowFrom });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseAllowFromInput(raw: string): string[] {
|
|
107
|
+
return raw
|
|
108
|
+
.split(/[\n,;]+/g)
|
|
109
|
+
.map((entry) => entry.trim())
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function promptFeishuAllowFrom(params: {
|
|
114
|
+
cfg: ClawdbotConfig;
|
|
115
|
+
prompter: WizardPrompter;
|
|
116
|
+
accountId?: string;
|
|
117
|
+
}): Promise<ClawdbotConfig> {
|
|
118
|
+
const accountId = resolveDmPolicyAccountId(params.cfg, params.accountId);
|
|
119
|
+
const existing = resolveFeishuAccount({ cfg: params.cfg, accountId }).config.allowFrom ?? [];
|
|
120
|
+
const accountLabel = accountId === DEFAULT_ACCOUNT_ID ? "default" : accountId;
|
|
121
|
+
await params.prompter.note(
|
|
122
|
+
[
|
|
123
|
+
`Account: ${accountLabel}`,
|
|
124
|
+
"Allowlist Feishu DMs by open_id or user_id.",
|
|
125
|
+
"You can find user open_id in Feishu admin console or via API.",
|
|
126
|
+
"Examples:",
|
|
127
|
+
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
128
|
+
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
"Feishu allowlist",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
while (true) {
|
|
134
|
+
const entry = await params.prompter.text({
|
|
135
|
+
message: "Feishu allowFrom (user open_ids)",
|
|
136
|
+
placeholder: "ou_xxxxx, ou_yyyyy",
|
|
137
|
+
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
138
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
139
|
+
});
|
|
140
|
+
const parts = parseAllowFromInput(String(entry));
|
|
141
|
+
if (parts.length === 0) {
|
|
142
|
+
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const unique = [
|
|
147
|
+
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts]),
|
|
148
|
+
];
|
|
149
|
+
return setFeishuAllowFrom(params.cfg, unique, accountId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
|
154
|
+
await prompter.note(
|
|
155
|
+
[
|
|
156
|
+
"1) Go to Feishu Open Platform (open.feishu.cn)",
|
|
157
|
+
"2) Create a self-built app",
|
|
158
|
+
"3) Get App ID and App Secret from Credentials page",
|
|
159
|
+
"4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
|
|
160
|
+
"5) Publish the app or add it to a test group",
|
|
161
|
+
"Tip: you can also set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.",
|
|
162
|
+
`Docs: ${formatDocsLink("/channels/feishu", "feishu")}`,
|
|
163
|
+
].join("\n"),
|
|
164
|
+
"Feishu credentials",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function setFeishuGroupPolicy(
|
|
169
|
+
cfg: ClawdbotConfig,
|
|
170
|
+
groupPolicy: "open" | "allowlist" | "disabled",
|
|
171
|
+
accountId?: string,
|
|
172
|
+
): ClawdbotConfig {
|
|
173
|
+
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
174
|
+
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { enabled: true, groupPolicy });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setFeishuGroupAllowFrom(
|
|
178
|
+
cfg: ClawdbotConfig,
|
|
179
|
+
groupAllowFrom: string[],
|
|
180
|
+
accountId?: string,
|
|
181
|
+
): ClawdbotConfig {
|
|
182
|
+
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
183
|
+
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { groupAllowFrom });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function setFeishuDomain(cfg: ClawdbotConfig, domain: "feishu" | "lark", accountId?: string): ClawdbotConfig {
|
|
187
|
+
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
188
|
+
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { domain });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
192
|
+
label: "Feishu",
|
|
193
|
+
channel,
|
|
194
|
+
policyKey: "channels.feishu.dmPolicy",
|
|
195
|
+
allowFromKey: "channels.feishu.allowFrom",
|
|
196
|
+
getCurrent: (cfg) => {
|
|
197
|
+
const accountId = resolveDmPolicyAccountId(cfg);
|
|
198
|
+
return resolveFeishuAccount({ cfg, accountId }).config.dmPolicy ?? "pairing";
|
|
199
|
+
},
|
|
200
|
+
setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy, onboardingDmPolicyAccountId),
|
|
201
|
+
promptAllowFrom: promptFeishuAllowFrom,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
205
|
+
channel,
|
|
206
|
+
getStatus: async ({ cfg, accountOverrides }) => {
|
|
207
|
+
const override = accountOverrides?.feishu?.trim();
|
|
208
|
+
const accountIds = override
|
|
209
|
+
? [resolveOnboardingAccountId(cfg, override)]
|
|
210
|
+
: listFeishuAccountIds(cfg);
|
|
211
|
+
const accounts = accountIds.map((accountId) => resolveFeishuAccount({ cfg, accountId }));
|
|
212
|
+
const configuredAccounts = accounts.filter((account) => account.configured);
|
|
213
|
+
const configured = configuredAccounts.length > 0;
|
|
214
|
+
|
|
215
|
+
// Try to probe if configured
|
|
216
|
+
let probeResult = null;
|
|
217
|
+
if (configuredAccounts[0]) {
|
|
218
|
+
try {
|
|
219
|
+
probeResult = await probeFeishu(configuredAccounts[0]);
|
|
220
|
+
} catch {
|
|
221
|
+
// Ignore probe errors
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const statusLines: string[] = [];
|
|
226
|
+
if (!configured) {
|
|
227
|
+
statusLines.push("Feishu: needs app credentials");
|
|
228
|
+
} else if (probeResult?.ok) {
|
|
229
|
+
const probeAccountId = configuredAccounts[0]?.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
230
|
+
const accountNote = probeAccountId === DEFAULT_ACCOUNT_ID ? "" : ` [${probeAccountId}]`;
|
|
231
|
+
statusLines.push(
|
|
232
|
+
`Feishu${accountNote}: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`,
|
|
233
|
+
);
|
|
234
|
+
if (!override && configuredAccounts.length > 1) {
|
|
235
|
+
statusLines.push(`Feishu: ${configuredAccounts.length} account(s) configured`);
|
|
236
|
+
}
|
|
237
|
+
} else if (!override && configuredAccounts.length > 1) {
|
|
238
|
+
statusLines.push(`Feishu: configured (${configuredAccounts.length} account(s), connection not verified)`);
|
|
239
|
+
} else {
|
|
240
|
+
statusLines.push("Feishu: configured (connection not verified)");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
channel,
|
|
245
|
+
configured,
|
|
246
|
+
statusLines,
|
|
247
|
+
selectionHint: configured ? "configured" : "needs app creds",
|
|
248
|
+
quickstartScore: configured ? 2 : 0,
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
|
|
253
|
+
const feishuOverride = accountOverrides?.feishu?.trim();
|
|
254
|
+
const defaultFeishuAccountId = resolveDefaultFeishuAccountId(cfg);
|
|
255
|
+
let feishuAccountId = feishuOverride
|
|
256
|
+
? normalizeAccountId(feishuOverride)
|
|
257
|
+
: defaultFeishuAccountId;
|
|
258
|
+
if (shouldPromptAccountIds && !feishuOverride) {
|
|
259
|
+
feishuAccountId = await promptAccountId({
|
|
260
|
+
cfg,
|
|
261
|
+
prompter,
|
|
262
|
+
label: "Feishu",
|
|
263
|
+
currentId: feishuAccountId,
|
|
264
|
+
listAccountIds: listFeishuAccountIds,
|
|
265
|
+
defaultAccountId: defaultFeishuAccountId,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
onboardingDmPolicyAccountId = feishuAccountId;
|
|
269
|
+
const accountLabel = feishuAccountId === DEFAULT_ACCOUNT_ID ? "default" : feishuAccountId;
|
|
270
|
+
const currentAccount = resolveFeishuAccount({ cfg, accountId: feishuAccountId });
|
|
271
|
+
const resolved = resolveFeishuCredentials(currentAccount.config);
|
|
272
|
+
const hasConfigCreds = Boolean(
|
|
273
|
+
currentAccount.config.appId?.trim() && currentAccount.config.appSecret?.trim(),
|
|
274
|
+
);
|
|
275
|
+
const canUseEnv = Boolean(
|
|
276
|
+
feishuAccountId === DEFAULT_ACCOUNT_ID &&
|
|
277
|
+
!hasConfigCreds &&
|
|
278
|
+
process.env.FEISHU_APP_ID?.trim() &&
|
|
279
|
+
process.env.FEISHU_APP_SECRET?.trim(),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
let next = cfg;
|
|
283
|
+
let appId: string | null = null;
|
|
284
|
+
let appSecret: string | null = null;
|
|
285
|
+
const appIdPrompt =
|
|
286
|
+
feishuAccountId === DEFAULT_ACCOUNT_ID
|
|
287
|
+
? "Enter Feishu App ID"
|
|
288
|
+
: `Enter Feishu App ID for account "${accountLabel}"`;
|
|
289
|
+
const appSecretPrompt =
|
|
290
|
+
feishuAccountId === DEFAULT_ACCOUNT_ID
|
|
291
|
+
? "Enter Feishu App Secret"
|
|
292
|
+
: `Enter Feishu App Secret for account "${accountLabel}"`;
|
|
293
|
+
|
|
294
|
+
if (!resolved) {
|
|
295
|
+
await noteFeishuCredentialHelp(prompter);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (canUseEnv) {
|
|
299
|
+
const keepEnv = await prompter.confirm({
|
|
300
|
+
message: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
301
|
+
initialValue: true,
|
|
302
|
+
});
|
|
303
|
+
if (keepEnv) {
|
|
304
|
+
next = upsertFeishuAccountConfig(next, feishuAccountId, { enabled: true });
|
|
305
|
+
} else {
|
|
306
|
+
appId = String(
|
|
307
|
+
await prompter.text({
|
|
308
|
+
message: appIdPrompt,
|
|
309
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
310
|
+
}),
|
|
311
|
+
).trim();
|
|
312
|
+
appSecret = String(
|
|
313
|
+
await prompter.text({
|
|
314
|
+
message: appSecretPrompt,
|
|
315
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
316
|
+
}),
|
|
317
|
+
).trim();
|
|
318
|
+
}
|
|
319
|
+
} else if (hasConfigCreds) {
|
|
320
|
+
const keep = await prompter.confirm({
|
|
321
|
+
message: `Feishu credentials already configured for account "${accountLabel}". Keep them?`,
|
|
322
|
+
initialValue: true,
|
|
323
|
+
});
|
|
324
|
+
if (!keep) {
|
|
325
|
+
appId = String(
|
|
326
|
+
await prompter.text({
|
|
327
|
+
message: appIdPrompt,
|
|
328
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
329
|
+
}),
|
|
330
|
+
).trim();
|
|
331
|
+
appSecret = String(
|
|
332
|
+
await prompter.text({
|
|
333
|
+
message: appSecretPrompt,
|
|
334
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
335
|
+
}),
|
|
336
|
+
).trim();
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
appId = String(
|
|
340
|
+
await prompter.text({
|
|
341
|
+
message: appIdPrompt,
|
|
342
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
343
|
+
}),
|
|
344
|
+
).trim();
|
|
345
|
+
appSecret = String(
|
|
346
|
+
await prompter.text({
|
|
347
|
+
message: appSecretPrompt,
|
|
348
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
349
|
+
}),
|
|
350
|
+
).trim();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (appId && appSecret) {
|
|
354
|
+
next = upsertFeishuAccountConfig(next, feishuAccountId, {
|
|
355
|
+
enabled: true,
|
|
356
|
+
appId,
|
|
357
|
+
appSecret,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Test connection
|
|
361
|
+
const testAccount = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId });
|
|
362
|
+
try {
|
|
363
|
+
const probe = await probeFeishu(testAccount);
|
|
364
|
+
if (probe.ok) {
|
|
365
|
+
await prompter.note(
|
|
366
|
+
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
367
|
+
`Feishu connection test (${accountLabel})`,
|
|
368
|
+
);
|
|
369
|
+
} else {
|
|
370
|
+
await prompter.note(
|
|
371
|
+
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
372
|
+
`Feishu connection test (${accountLabel})`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
} catch (err) {
|
|
376
|
+
await prompter.note(
|
|
377
|
+
`Connection test failed: ${String(err)}`,
|
|
378
|
+
`Feishu connection test (${accountLabel})`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Domain selection
|
|
384
|
+
const currentDomain = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId }).config.domain ?? "feishu";
|
|
385
|
+
const domain = await prompter.select({
|
|
386
|
+
message: "Which Feishu domain?",
|
|
387
|
+
options: [
|
|
388
|
+
{ value: "feishu", label: "Feishu (feishu.cn) - China" },
|
|
389
|
+
{ value: "lark", label: "Lark (larksuite.com) - International" },
|
|
390
|
+
],
|
|
391
|
+
initialValue: currentDomain,
|
|
392
|
+
});
|
|
393
|
+
if (domain) {
|
|
394
|
+
next = setFeishuDomain(next, domain as "feishu" | "lark", feishuAccountId);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Group policy
|
|
398
|
+
const groupPolicyAccount = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId });
|
|
399
|
+
const groupPolicy = await prompter.select({
|
|
400
|
+
message: "Group chat policy",
|
|
401
|
+
options: [
|
|
402
|
+
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
403
|
+
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
404
|
+
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
405
|
+
],
|
|
406
|
+
initialValue: groupPolicyAccount.config.groupPolicy ?? "allowlist",
|
|
407
|
+
});
|
|
408
|
+
if (groupPolicy) {
|
|
409
|
+
next = setFeishuGroupPolicy(
|
|
410
|
+
next,
|
|
411
|
+
groupPolicy as "open" | "allowlist" | "disabled",
|
|
412
|
+
feishuAccountId,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Group allowlist if needed
|
|
417
|
+
if (groupPolicy === "allowlist") {
|
|
418
|
+
const existing =
|
|
419
|
+
resolveFeishuAccount({ cfg: next, accountId: feishuAccountId }).config.groupAllowFrom ?? [];
|
|
420
|
+
const entry = await prompter.text({
|
|
421
|
+
message: "Group chat allowlist (chat_ids)",
|
|
422
|
+
placeholder: "oc_xxxxx, oc_yyyyy",
|
|
423
|
+
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
424
|
+
});
|
|
425
|
+
if (entry) {
|
|
426
|
+
const parts = parseAllowFromInput(String(entry));
|
|
427
|
+
if (parts.length > 0) {
|
|
428
|
+
next = setFeishuGroupAllowFrom(next, parts, feishuAccountId);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return { cfg: next, accountId: feishuAccountId };
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
dmPolicy,
|
|
437
|
+
|
|
438
|
+
onAccountRecorded: (accountId) => {
|
|
439
|
+
onboardingDmPolicyAccountId = normalizeAccountId(accountId);
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
disable: (cfg) => ({
|
|
443
|
+
...cfg,
|
|
444
|
+
channels: {
|
|
445
|
+
...cfg.channels,
|
|
446
|
+
feishu: { ...cfg.channels?.feishu, enabled: false },
|
|
447
|
+
},
|
|
448
|
+
}),
|
|
449
|
+
};
|
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChannelOutboundAdapter } from "openclaw/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, accountId }) => {
|
|
12
|
+
const result = await sendMessageFeishu({ cfg, to, text, accountId });
|
|
13
|
+
return { channel: "feishu", ...result };
|
|
14
|
+
},
|
|
15
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
16
|
+
// Send text first if provided
|
|
17
|
+
if (text?.trim()) {
|
|
18
|
+
await sendMessageFeishu({ cfg, to, text, accountId });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Upload and send media if URL provided
|
|
22
|
+
if (mediaUrl) {
|
|
23
|
+
try {
|
|
24
|
+
const result = await sendMediaFeishu({ cfg, to, mediaUrl, accountId });
|
|
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, accountId });
|
|
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 ?? "", accountId });
|
|
38
|
+
return { channel: "feishu", ...result };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { runPermApiCall, type PermClient } from "./common.js";
|
|
2
|
+
import type { FeishuPermParams } from "./schemas.js";
|
|
3
|
+
|
|
4
|
+
type ListTokenType =
|
|
5
|
+
| "doc"
|
|
6
|
+
| "sheet"
|
|
7
|
+
| "file"
|
|
8
|
+
| "wiki"
|
|
9
|
+
| "bitable"
|
|
10
|
+
| "docx"
|
|
11
|
+
| "mindnote"
|
|
12
|
+
| "minutes"
|
|
13
|
+
| "slides";
|
|
14
|
+
type CreateTokenType =
|
|
15
|
+
| "doc"
|
|
16
|
+
| "sheet"
|
|
17
|
+
| "file"
|
|
18
|
+
| "wiki"
|
|
19
|
+
| "bitable"
|
|
20
|
+
| "docx"
|
|
21
|
+
| "folder"
|
|
22
|
+
| "mindnote"
|
|
23
|
+
| "minutes"
|
|
24
|
+
| "slides";
|
|
25
|
+
type MemberType =
|
|
26
|
+
| "email"
|
|
27
|
+
| "openid"
|
|
28
|
+
| "unionid"
|
|
29
|
+
| "openchat"
|
|
30
|
+
| "opendepartmentid"
|
|
31
|
+
| "userid"
|
|
32
|
+
| "groupid"
|
|
33
|
+
| "wikispaceid";
|
|
34
|
+
type PermType = "view" | "edit" | "full_access";
|
|
35
|
+
|
|
36
|
+
async function listMembers(client: PermClient, token: string, type: string) {
|
|
37
|
+
const res = await runPermApiCall("drive.permissionMember.list", () =>
|
|
38
|
+
client.drive.permissionMember.list({
|
|
39
|
+
path: { token },
|
|
40
|
+
params: { type: type as ListTokenType },
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
members:
|
|
46
|
+
res.data?.items?.map((m) => ({
|
|
47
|
+
member_type: m.member_type,
|
|
48
|
+
member_id: m.member_id,
|
|
49
|
+
perm: m.perm,
|
|
50
|
+
name: m.name,
|
|
51
|
+
})) ?? [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function addMember(
|
|
56
|
+
client: PermClient,
|
|
57
|
+
token: string,
|
|
58
|
+
type: string,
|
|
59
|
+
memberType: string,
|
|
60
|
+
memberId: string,
|
|
61
|
+
perm: string,
|
|
62
|
+
) {
|
|
63
|
+
const res = await runPermApiCall("drive.permissionMember.create", () =>
|
|
64
|
+
client.drive.permissionMember.create({
|
|
65
|
+
path: { token },
|
|
66
|
+
params: { type: type as CreateTokenType, need_notification: false },
|
|
67
|
+
data: {
|
|
68
|
+
member_type: memberType as MemberType,
|
|
69
|
+
member_id: memberId,
|
|
70
|
+
perm: perm as PermType,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
member: res.data?.member,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function removeMember(
|
|
82
|
+
client: PermClient,
|
|
83
|
+
token: string,
|
|
84
|
+
type: string,
|
|
85
|
+
memberType: string,
|
|
86
|
+
memberId: string,
|
|
87
|
+
) {
|
|
88
|
+
await runPermApiCall("drive.permissionMember.delete", () =>
|
|
89
|
+
client.drive.permissionMember.delete({
|
|
90
|
+
path: { token, member_id: memberId },
|
|
91
|
+
params: { type: type as CreateTokenType, member_type: memberType as MemberType },
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function runPermAction(client: PermClient, params: FeishuPermParams) {
|
|
101
|
+
switch (params.action) {
|
|
102
|
+
case "list":
|
|
103
|
+
return listMembers(client, params.token, params.type);
|
|
104
|
+
case "add":
|
|
105
|
+
return addMember(client, params.token, params.type, params.member_type, params.member_id, params.perm);
|
|
106
|
+
case "remove":
|
|
107
|
+
return removeMember(client, params.token, params.type, params.member_type, params.member_id);
|
|
108
|
+
default:
|
|
109
|
+
return { error: `Unknown action: ${(params as any).action}` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFeishuClient } from "../client.js";
|
|
2
|
+
import {
|
|
3
|
+
errorResult,
|
|
4
|
+
json,
|
|
5
|
+
runFeishuApiCall,
|
|
6
|
+
type FeishuApiResponse,
|
|
7
|
+
} from "../tools-common/feishu-api.js";
|
|
8
|
+
|
|
9
|
+
export type PermClient = ReturnType<typeof createFeishuClient>;
|
|
10
|
+
|
|
11
|
+
export { json, errorResult };
|
|
12
|
+
|
|
13
|
+
export async function runPermApiCall<T extends FeishuApiResponse>(
|
|
14
|
+
context: string,
|
|
15
|
+
fn: () => Promise<T>,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
return runFeishuApiCall(context, fn);
|
|
18
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
|
|
4
|
+
import { runPermAction } from "./actions.js";
|
|
5
|
+
import { errorResult, json, type PermClient } from "./common.js";
|
|
6
|
+
import { FeishuPermSchema, type FeishuPermParams } from "./schemas.js";
|
|
7
|
+
|
|
8
|
+
type PermToolSpec<P> = {
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters: TSchema;
|
|
13
|
+
run: (client: PermClient, params: P) => Promise<unknown>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function registerPermTool<P>(api: OpenClawPluginApi, spec: PermToolSpec<P>) {
|
|
17
|
+
api.registerTool(
|
|
18
|
+
{
|
|
19
|
+
name: spec.name,
|
|
20
|
+
label: spec.label,
|
|
21
|
+
description: spec.description,
|
|
22
|
+
parameters: spec.parameters,
|
|
23
|
+
async execute(_toolCallId, params) {
|
|
24
|
+
try {
|
|
25
|
+
return await withFeishuToolClient({
|
|
26
|
+
api,
|
|
27
|
+
toolName: spec.name,
|
|
28
|
+
requiredTool: "perm",
|
|
29
|
+
run: async ({ client }) => json(await spec.run(client as PermClient, params as P)),
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return errorResult(err);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{ name: spec.name },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
|
41
|
+
if (!api.config) {
|
|
42
|
+
api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
47
|
+
api.logger.debug?.("feishu_perm: No Feishu accounts configured, skipping perm tools");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "perm")) {
|
|
52
|
+
api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
registerPermTool<FeishuPermParams>(api, {
|
|
57
|
+
name: "feishu_perm",
|
|
58
|
+
label: "Feishu Perm",
|
|
59
|
+
description: "Feishu permission management. Actions: list, add, remove",
|
|
60
|
+
parameters: FeishuPermSchema,
|
|
61
|
+
run: (client, params) => runPermAction(client, params),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
api.logger.debug?.("feishu_perm: Registered feishu_perm tool");
|
|
65
|
+
}
|