@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
package/src/channel.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveFeishuAccount,
|
|
6
|
+
resolveFeishuCredentials,
|
|
7
|
+
listFeishuAccountIds,
|
|
8
|
+
resolveDefaultFeishuAccountId,
|
|
9
|
+
} from "./accounts.js";
|
|
10
|
+
import { feishuOutbound } from "./outbound.js";
|
|
11
|
+
import { probeFeishu } from "./probe.js";
|
|
12
|
+
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
|
13
|
+
import { normalizeFeishuTarget, looksLikeFeishuId, formatFeishuTarget } from "./targets.js";
|
|
14
|
+
import { sendMessageFeishu } from "./send.js";
|
|
15
|
+
import {
|
|
16
|
+
listFeishuDirectoryPeers,
|
|
17
|
+
listFeishuDirectoryGroups,
|
|
18
|
+
listFeishuDirectoryPeersLive,
|
|
19
|
+
listFeishuDirectoryGroupsLive,
|
|
20
|
+
} from "./directory.js";
|
|
21
|
+
import { feishuOnboardingAdapter } from "./onboarding.js";
|
|
22
|
+
|
|
23
|
+
const meta = {
|
|
24
|
+
id: "feishu",
|
|
25
|
+
label: "Feishu",
|
|
26
|
+
selectionLabel: "Feishu/Lark (飞书)",
|
|
27
|
+
docsPath: "/channels/feishu",
|
|
28
|
+
docsLabel: "feishu",
|
|
29
|
+
blurb: "飞书/Lark enterprise messaging.",
|
|
30
|
+
aliases: ["lark"],
|
|
31
|
+
order: 70,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
35
|
+
id: "feishu-fork", // plugin ID: avoids collision with built-in feishu plugin
|
|
36
|
+
meta: {
|
|
37
|
+
...meta,
|
|
38
|
+
},
|
|
39
|
+
pairing: {
|
|
40
|
+
idLabel: "feishuUserId",
|
|
41
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
|
|
42
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
43
|
+
await sendMessageFeishu({
|
|
44
|
+
cfg,
|
|
45
|
+
to: id,
|
|
46
|
+
text: PAIRING_APPROVED_MESSAGE,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
capabilities: {
|
|
51
|
+
chatTypes: ["direct", "channel"],
|
|
52
|
+
polls: false,
|
|
53
|
+
threads: true,
|
|
54
|
+
media: true,
|
|
55
|
+
reactions: true,
|
|
56
|
+
edit: true,
|
|
57
|
+
reply: true,
|
|
58
|
+
},
|
|
59
|
+
agentPrompt: {
|
|
60
|
+
messageToolHints: () => [
|
|
61
|
+
"- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
|
|
62
|
+
"- Feishu supports interactive cards for rich messages.",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
groups: {
|
|
66
|
+
resolveToolPolicy: resolveFeishuGroupToolPolicy,
|
|
67
|
+
},
|
|
68
|
+
reload: { configPrefixes: ["channels.feishu"] },
|
|
69
|
+
configSchema: {
|
|
70
|
+
schema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
enabled: { type: "boolean" },
|
|
75
|
+
appId: { type: "string" },
|
|
76
|
+
appSecret: { type: "string" },
|
|
77
|
+
encryptKey: { type: "string" },
|
|
78
|
+
verificationToken: { type: "string" },
|
|
79
|
+
domain: {
|
|
80
|
+
oneOf: [
|
|
81
|
+
{ type: "string", enum: ["feishu", "lark"] },
|
|
82
|
+
{ type: "string", format: "uri", pattern: "^https://" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
86
|
+
webhookPath: { type: "string" },
|
|
87
|
+
webhookPort: { type: "integer", minimum: 1 },
|
|
88
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
89
|
+
allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
90
|
+
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
91
|
+
groupAllowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
92
|
+
requireMention: { type: "boolean" },
|
|
93
|
+
groupCommandMentionBypass: { type: "string", enum: ["never", "single_bot", "always"] },
|
|
94
|
+
topicSessionMode: { type: "string", enum: ["disabled", "enabled"] },
|
|
95
|
+
historyLimit: { type: "integer", minimum: 0 },
|
|
96
|
+
dmHistoryLimit: { type: "integer", minimum: 0 },
|
|
97
|
+
textChunkLimit: { type: "integer", minimum: 1 },
|
|
98
|
+
chunkMode: { type: "string", enum: ["length", "newline"] },
|
|
99
|
+
mediaMaxMb: { type: "number", minimum: 0 },
|
|
100
|
+
renderMode: { type: "string", enum: ["auto", "raw", "card"] },
|
|
101
|
+
accounts: {
|
|
102
|
+
type: "object",
|
|
103
|
+
additionalProperties: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
enabled: { type: "boolean" },
|
|
107
|
+
name: { type: "string" },
|
|
108
|
+
appId: { type: "string" },
|
|
109
|
+
appSecret: { type: "string" },
|
|
110
|
+
encryptKey: { type: "string" },
|
|
111
|
+
verificationToken: { type: "string" },
|
|
112
|
+
domain: { type: "string", enum: ["feishu", "lark"] },
|
|
113
|
+
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
114
|
+
groupCommandMentionBypass: { type: "string", enum: ["never", "single_bot", "always"] },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
config: {
|
|
122
|
+
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
|
123
|
+
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
|
124
|
+
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
|
125
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
126
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
127
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
128
|
+
|
|
129
|
+
if (isDefault) {
|
|
130
|
+
// For default account, set top-level enabled
|
|
131
|
+
return {
|
|
132
|
+
...cfg,
|
|
133
|
+
channels: {
|
|
134
|
+
...cfg.channels,
|
|
135
|
+
feishu: {
|
|
136
|
+
...cfg.channels?.feishu,
|
|
137
|
+
enabled,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// For named accounts, set enabled in accounts[accountId]
|
|
144
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
145
|
+
return {
|
|
146
|
+
...cfg,
|
|
147
|
+
channels: {
|
|
148
|
+
...cfg.channels,
|
|
149
|
+
feishu: {
|
|
150
|
+
...feishuCfg,
|
|
151
|
+
accounts: {
|
|
152
|
+
...feishuCfg?.accounts,
|
|
153
|
+
[accountId]: {
|
|
154
|
+
...feishuCfg?.accounts?.[accountId],
|
|
155
|
+
enabled,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
deleteAccount: ({ cfg, accountId }) => {
|
|
163
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
164
|
+
|
|
165
|
+
if (isDefault) {
|
|
166
|
+
// Delete entire feishu config
|
|
167
|
+
const next = { ...cfg } as ClawdbotConfig;
|
|
168
|
+
const nextChannels = { ...cfg.channels };
|
|
169
|
+
delete (nextChannels as Record<string, unknown>).feishu;
|
|
170
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
171
|
+
next.channels = nextChannels;
|
|
172
|
+
} else {
|
|
173
|
+
delete next.channels;
|
|
174
|
+
}
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Delete specific account from accounts
|
|
179
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
180
|
+
const accounts = { ...feishuCfg?.accounts };
|
|
181
|
+
delete accounts[accountId];
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...cfg,
|
|
185
|
+
channels: {
|
|
186
|
+
...cfg.channels,
|
|
187
|
+
feishu: {
|
|
188
|
+
...feishuCfg,
|
|
189
|
+
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
isConfigured: (account) => account.configured,
|
|
195
|
+
describeAccount: (account) => ({
|
|
196
|
+
accountId: account.accountId,
|
|
197
|
+
enabled: account.enabled,
|
|
198
|
+
configured: account.configured,
|
|
199
|
+
name: account.name,
|
|
200
|
+
appId: account.appId,
|
|
201
|
+
domain: account.domain,
|
|
202
|
+
}),
|
|
203
|
+
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
204
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
205
|
+
return (account.config?.allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
206
|
+
},
|
|
207
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
208
|
+
allowFrom
|
|
209
|
+
.map((entry) => String(entry).trim())
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.map((entry) => entry.toLowerCase()),
|
|
212
|
+
},
|
|
213
|
+
security: {
|
|
214
|
+
collectWarnings: ({ cfg, accountId }) => {
|
|
215
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
216
|
+
const feishuCfg = account.config;
|
|
217
|
+
const defaultGroupPolicy = (cfg.channels as Record<string, { groupPolicy?: string }> | undefined)?.defaults?.groupPolicy;
|
|
218
|
+
const groupPolicy = feishuCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
219
|
+
if (groupPolicy !== "open") return [];
|
|
220
|
+
return [
|
|
221
|
+
`- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
|
|
222
|
+
];
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
setup: {
|
|
226
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
227
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
228
|
+
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
229
|
+
|
|
230
|
+
if (isDefault) {
|
|
231
|
+
return {
|
|
232
|
+
...cfg,
|
|
233
|
+
channels: {
|
|
234
|
+
...cfg.channels,
|
|
235
|
+
feishu: {
|
|
236
|
+
...cfg.channels?.feishu,
|
|
237
|
+
enabled: true,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
244
|
+
return {
|
|
245
|
+
...cfg,
|
|
246
|
+
channels: {
|
|
247
|
+
...cfg.channels,
|
|
248
|
+
feishu: {
|
|
249
|
+
...feishuCfg,
|
|
250
|
+
accounts: {
|
|
251
|
+
...feishuCfg?.accounts,
|
|
252
|
+
[accountId]: {
|
|
253
|
+
...feishuCfg?.accounts?.[accountId],
|
|
254
|
+
enabled: true,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
onboarding: feishuOnboardingAdapter,
|
|
263
|
+
messaging: {
|
|
264
|
+
normalizeTarget: normalizeFeishuTarget,
|
|
265
|
+
targetResolver: {
|
|
266
|
+
looksLikeId: looksLikeFeishuId,
|
|
267
|
+
hint: "<chatId|user:openId|chat:chatId>",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
directory: {
|
|
271
|
+
self: async () => null,
|
|
272
|
+
listPeers: async ({ cfg, query, limit, accountId }) =>
|
|
273
|
+
listFeishuDirectoryPeers({ cfg, query, limit, accountId }),
|
|
274
|
+
listGroups: async ({ cfg, query, limit, accountId }) =>
|
|
275
|
+
listFeishuDirectoryGroups({ cfg, query, limit, accountId }),
|
|
276
|
+
listPeersLive: async ({ cfg, query, limit, accountId }) =>
|
|
277
|
+
listFeishuDirectoryPeersLive({ cfg, query, limit, accountId }),
|
|
278
|
+
listGroupsLive: async ({ cfg, query, limit, accountId }) =>
|
|
279
|
+
listFeishuDirectoryGroupsLive({ cfg, query, limit, accountId }),
|
|
280
|
+
},
|
|
281
|
+
outbound: feishuOutbound,
|
|
282
|
+
status: {
|
|
283
|
+
defaultRuntime: {
|
|
284
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
285
|
+
running: false,
|
|
286
|
+
lastStartAt: null,
|
|
287
|
+
lastStopAt: null,
|
|
288
|
+
lastError: null,
|
|
289
|
+
port: null,
|
|
290
|
+
},
|
|
291
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
292
|
+
configured: snapshot.configured ?? false,
|
|
293
|
+
running: snapshot.running ?? false,
|
|
294
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
295
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
296
|
+
lastError: snapshot.lastError ?? null,
|
|
297
|
+
port: snapshot.port ?? null,
|
|
298
|
+
probe: snapshot.probe,
|
|
299
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
300
|
+
}),
|
|
301
|
+
probeAccount: async ({ account }) => {
|
|
302
|
+
return await probeFeishu(account);
|
|
303
|
+
},
|
|
304
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
305
|
+
accountId: account.accountId,
|
|
306
|
+
enabled: account.enabled,
|
|
307
|
+
configured: account.configured,
|
|
308
|
+
name: account.name,
|
|
309
|
+
appId: account.appId,
|
|
310
|
+
domain: account.domain,
|
|
311
|
+
running: runtime?.running ?? false,
|
|
312
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
313
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
314
|
+
lastError: runtime?.lastError ?? null,
|
|
315
|
+
port: runtime?.port ?? null,
|
|
316
|
+
probe,
|
|
317
|
+
}),
|
|
318
|
+
},
|
|
319
|
+
gateway: {
|
|
320
|
+
startAccount: async (ctx) => {
|
|
321
|
+
const { monitorFeishuProvider } = await import("./monitor.js");
|
|
322
|
+
const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
323
|
+
const port = account.config?.webhookPort ?? null;
|
|
324
|
+
ctx.setStatus({ accountId: ctx.accountId, port });
|
|
325
|
+
ctx.log?.info(`starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`);
|
|
326
|
+
return monitorFeishuProvider({
|
|
327
|
+
config: ctx.cfg,
|
|
328
|
+
runtime: ctx.runtime,
|
|
329
|
+
abortSignal: ctx.abortSignal,
|
|
330
|
+
accountId: ctx.accountId,
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
};
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Multi-account client cache
|
|
5
|
+
const clientCache = new Map<
|
|
6
|
+
string,
|
|
7
|
+
{
|
|
8
|
+
client: Lark.Client;
|
|
9
|
+
config: { appId: string; appSecret: string; domain?: FeishuDomain };
|
|
10
|
+
}
|
|
11
|
+
>();
|
|
12
|
+
|
|
13
|
+
function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
|
14
|
+
if (domain === "lark") return Lark.Domain.Lark;
|
|
15
|
+
if (domain === "feishu" || !domain) return Lark.Domain.Feishu;
|
|
16
|
+
return domain.replace(/\/+$/, ""); // Custom URL for private deployment
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Credentials needed to create a Feishu client.
|
|
21
|
+
* Both FeishuConfig and ResolvedFeishuAccount satisfy this interface.
|
|
22
|
+
*/
|
|
23
|
+
export type FeishuClientCredentials = {
|
|
24
|
+
accountId?: string;
|
|
25
|
+
appId?: string;
|
|
26
|
+
appSecret?: string;
|
|
27
|
+
domain?: FeishuDomain;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create or get a cached Feishu client for an account.
|
|
32
|
+
* Accepts any object with appId, appSecret, and optional domain/accountId.
|
|
33
|
+
*/
|
|
34
|
+
export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client {
|
|
35
|
+
const { accountId = "default", appId, appSecret, domain } = creds;
|
|
36
|
+
|
|
37
|
+
if (!appId || !appSecret) {
|
|
38
|
+
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check cache
|
|
42
|
+
const cached = clientCache.get(accountId);
|
|
43
|
+
if (
|
|
44
|
+
cached &&
|
|
45
|
+
cached.config.appId === appId &&
|
|
46
|
+
cached.config.appSecret === appSecret &&
|
|
47
|
+
cached.config.domain === domain
|
|
48
|
+
) {
|
|
49
|
+
return cached.client;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create new client
|
|
53
|
+
const client = new Lark.Client({
|
|
54
|
+
appId,
|
|
55
|
+
appSecret,
|
|
56
|
+
appType: Lark.AppType.SelfBuild,
|
|
57
|
+
domain: resolveDomain(domain),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Cache it
|
|
61
|
+
clientCache.set(accountId, {
|
|
62
|
+
client,
|
|
63
|
+
config: { appId, appSecret, domain },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return client;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a Feishu WebSocket client for an account.
|
|
71
|
+
* Note: WSClient is not cached since each call creates a new connection.
|
|
72
|
+
*/
|
|
73
|
+
export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSClient {
|
|
74
|
+
const { accountId, appId, appSecret, domain } = account;
|
|
75
|
+
|
|
76
|
+
if (!appId || !appSecret) {
|
|
77
|
+
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new Lark.WSClient({
|
|
81
|
+
appId,
|
|
82
|
+
appSecret,
|
|
83
|
+
domain: resolveDomain(domain),
|
|
84
|
+
loggerLevel: Lark.LoggerLevel.info,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create an event dispatcher for an account.
|
|
90
|
+
*/
|
|
91
|
+
export function createEventDispatcher(account: ResolvedFeishuAccount): Lark.EventDispatcher {
|
|
92
|
+
return new Lark.EventDispatcher({
|
|
93
|
+
encryptKey: account.encryptKey,
|
|
94
|
+
verificationToken: account.verificationToken,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get a cached client for an account (if exists).
|
|
100
|
+
*/
|
|
101
|
+
export function getFeishuClient(accountId: string): Lark.Client | null {
|
|
102
|
+
return clientCache.get(accountId)?.client ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear client cache for a specific account or all accounts.
|
|
107
|
+
*/
|
|
108
|
+
export function clearClientCache(accountId?: string): void {
|
|
109
|
+
if (accountId) {
|
|
110
|
+
clientCache.delete(accountId);
|
|
111
|
+
} else {
|
|
112
|
+
clientCache.clear();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export { z };
|
|
3
|
+
|
|
4
|
+
const DmPolicySchema = z.enum(["open", "pairing", "allowlist"]);
|
|
5
|
+
const GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
6
|
+
const GroupCommandMentionBypassSchema = z.enum(["never", "single_bot", "always"]).optional();
|
|
7
|
+
const FeishuDomainSchema = z.union([
|
|
8
|
+
z.enum(["feishu", "lark"]),
|
|
9
|
+
z.string().url().startsWith("https://"),
|
|
10
|
+
]);
|
|
11
|
+
const FeishuConnectionModeSchema = z.enum(["websocket", "webhook"]);
|
|
12
|
+
|
|
13
|
+
const ToolPolicySchema = z
|
|
14
|
+
.object({
|
|
15
|
+
allow: z.array(z.string()).optional(),
|
|
16
|
+
deny: z.array(z.string()).optional(),
|
|
17
|
+
})
|
|
18
|
+
.strict()
|
|
19
|
+
.optional();
|
|
20
|
+
|
|
21
|
+
const DmConfigSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
enabled: z.boolean().optional(),
|
|
24
|
+
systemPrompt: z.string().optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict()
|
|
27
|
+
.optional();
|
|
28
|
+
|
|
29
|
+
const MarkdownConfigSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
mode: z.enum(["native", "escape", "strip"]).optional(),
|
|
32
|
+
tableMode: z.enum(["native", "ascii", "simple"]).optional(),
|
|
33
|
+
})
|
|
34
|
+
.strict()
|
|
35
|
+
.optional();
|
|
36
|
+
|
|
37
|
+
// Message render mode: auto (default) = detect markdown, raw = plain text, card = always card
|
|
38
|
+
const RenderModeSchema = z.enum(["auto", "raw", "card"]).optional();
|
|
39
|
+
|
|
40
|
+
// Streaming card mode: default false. When enabled, card replies use Feishu Card Kit streaming API.
|
|
41
|
+
const StreamingModeSchema = z.boolean().optional();
|
|
42
|
+
|
|
43
|
+
const BlockStreamingCoalesceSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
enabled: z.boolean().optional(),
|
|
46
|
+
minDelayMs: z.number().int().positive().optional(),
|
|
47
|
+
maxDelayMs: z.number().int().positive().optional(),
|
|
48
|
+
})
|
|
49
|
+
.strict()
|
|
50
|
+
.optional();
|
|
51
|
+
|
|
52
|
+
const ChannelHeartbeatVisibilitySchema = z
|
|
53
|
+
.object({
|
|
54
|
+
visibility: z.enum(["visible", "hidden"]).optional(),
|
|
55
|
+
intervalMs: z.number().int().positive().optional(),
|
|
56
|
+
})
|
|
57
|
+
.strict()
|
|
58
|
+
.optional();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Dynamic agent creation configuration.
|
|
62
|
+
* When enabled, a new agent is created for each unique DM user.
|
|
63
|
+
*/
|
|
64
|
+
const DynamicAgentCreationSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
enabled: z.boolean().optional(),
|
|
67
|
+
workspaceTemplate: z.string().optional(),
|
|
68
|
+
agentDirTemplate: z.string().optional(),
|
|
69
|
+
maxAgents: z.number().int().positive().optional(),
|
|
70
|
+
})
|
|
71
|
+
.strict()
|
|
72
|
+
.optional();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Feishu tools configuration.
|
|
76
|
+
* Controls which tool categories are enabled.
|
|
77
|
+
*
|
|
78
|
+
* Dependencies:
|
|
79
|
+
* - wiki requires doc (wiki content is edited via doc tools)
|
|
80
|
+
* - perm can work independently but is typically used with drive
|
|
81
|
+
* - task can work independently
|
|
82
|
+
*/
|
|
83
|
+
const FeishuToolsConfigSchema = z
|
|
84
|
+
.object({
|
|
85
|
+
doc: z.boolean().optional(), // Document operations (default: true)
|
|
86
|
+
wiki: z.boolean().optional(), // Knowledge base operations (default: true, requires doc)
|
|
87
|
+
drive: z.boolean().optional(), // Cloud storage operations (default: true)
|
|
88
|
+
perm: z.boolean().optional(), // Permission management (default: false, sensitive)
|
|
89
|
+
scopes: z.boolean().optional(), // App scopes diagnostic (default: true)
|
|
90
|
+
task: z.boolean().optional(), // Task operations (default: true)
|
|
91
|
+
})
|
|
92
|
+
.strict()
|
|
93
|
+
.optional();
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Topic session isolation mode for group chats.
|
|
97
|
+
* - "disabled" (default): All messages in a group share one session
|
|
98
|
+
* - "enabled": Messages in different topics get separate sessions
|
|
99
|
+
*
|
|
100
|
+
* When enabled, the session key becomes `chat:{chatId}:topic:{rootId}`
|
|
101
|
+
* for messages within a topic thread, allowing isolated conversations.
|
|
102
|
+
*/
|
|
103
|
+
const TopicSessionModeSchema = z.enum(["disabled", "enabled"]).optional();
|
|
104
|
+
|
|
105
|
+
export const FeishuGroupSchema = z
|
|
106
|
+
.object({
|
|
107
|
+
requireMention: z.boolean().optional(),
|
|
108
|
+
groupCommandMentionBypass: GroupCommandMentionBypassSchema,
|
|
109
|
+
tools: ToolPolicySchema,
|
|
110
|
+
skills: z.array(z.string()).optional(),
|
|
111
|
+
enabled: z.boolean().optional(),
|
|
112
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
113
|
+
systemPrompt: z.string().optional(),
|
|
114
|
+
topicSessionMode: TopicSessionModeSchema,
|
|
115
|
+
})
|
|
116
|
+
.strict();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Per-account configuration.
|
|
120
|
+
* All fields are optional - missing fields inherit from top-level config.
|
|
121
|
+
*/
|
|
122
|
+
export const FeishuAccountConfigSchema = z
|
|
123
|
+
.object({
|
|
124
|
+
enabled: z.boolean().optional(),
|
|
125
|
+
name: z.string().optional(), // Display name for this account
|
|
126
|
+
appId: z.string().optional(),
|
|
127
|
+
appSecret: z.string().optional(),
|
|
128
|
+
encryptKey: z.string().optional(),
|
|
129
|
+
verificationToken: z.string().optional(),
|
|
130
|
+
domain: FeishuDomainSchema.optional(),
|
|
131
|
+
connectionMode: FeishuConnectionModeSchema.optional(),
|
|
132
|
+
webhookPath: z.string().optional(),
|
|
133
|
+
webhookPort: z.number().int().positive().optional(),
|
|
134
|
+
capabilities: z.array(z.string()).optional(),
|
|
135
|
+
markdown: MarkdownConfigSchema,
|
|
136
|
+
configWrites: z.boolean().optional(),
|
|
137
|
+
dmPolicy: DmPolicySchema.optional(),
|
|
138
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
139
|
+
groupPolicy: GroupPolicySchema.optional(),
|
|
140
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
141
|
+
requireMention: z.boolean().optional(),
|
|
142
|
+
groupCommandMentionBypass: GroupCommandMentionBypassSchema,
|
|
143
|
+
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
144
|
+
historyLimit: z.number().int().min(0).optional(),
|
|
145
|
+
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
146
|
+
dms: z.record(z.string(), DmConfigSchema).optional(),
|
|
147
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
148
|
+
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
149
|
+
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
|
150
|
+
mediaMaxMb: z.number().positive().optional(),
|
|
151
|
+
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
152
|
+
renderMode: RenderModeSchema,
|
|
153
|
+
streaming: StreamingModeSchema,
|
|
154
|
+
tools: FeishuToolsConfigSchema,
|
|
155
|
+
})
|
|
156
|
+
.strict();
|
|
157
|
+
|
|
158
|
+
export const FeishuConfigSchema = z
|
|
159
|
+
.object({
|
|
160
|
+
enabled: z.boolean().optional(),
|
|
161
|
+
// Top-level credentials (backward compatible for single-account mode)
|
|
162
|
+
appId: z.string().optional(),
|
|
163
|
+
appSecret: z.string().optional(),
|
|
164
|
+
encryptKey: z.string().optional(),
|
|
165
|
+
verificationToken: z.string().optional(),
|
|
166
|
+
domain: FeishuDomainSchema.optional().default("feishu"),
|
|
167
|
+
connectionMode: FeishuConnectionModeSchema.optional().default("websocket"),
|
|
168
|
+
webhookPath: z.string().optional().default("/feishu/events"),
|
|
169
|
+
webhookPort: z.number().int().positive().optional(),
|
|
170
|
+
capabilities: z.array(z.string()).optional(),
|
|
171
|
+
markdown: MarkdownConfigSchema,
|
|
172
|
+
configWrites: z.boolean().optional(),
|
|
173
|
+
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
174
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
175
|
+
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
|
176
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
177
|
+
requireMention: z.boolean().optional().default(true),
|
|
178
|
+
groupCommandMentionBypass: GroupCommandMentionBypassSchema.default("single_bot"),
|
|
179
|
+
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
180
|
+
topicSessionMode: TopicSessionModeSchema,
|
|
181
|
+
historyLimit: z.number().int().min(0).optional(),
|
|
182
|
+
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
183
|
+
dms: z.record(z.string(), DmConfigSchema).optional(),
|
|
184
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
185
|
+
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
186
|
+
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
|
187
|
+
mediaMaxMb: z.number().positive().optional(),
|
|
188
|
+
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
189
|
+
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
|
190
|
+
streaming: StreamingModeSchema,
|
|
191
|
+
tools: FeishuToolsConfigSchema,
|
|
192
|
+
// Dynamic agent creation for DM users
|
|
193
|
+
dynamicAgentCreation: DynamicAgentCreationSchema,
|
|
194
|
+
// Multi-account configuration
|
|
195
|
+
accounts: z.record(z.string(), FeishuAccountConfigSchema.optional()).optional(),
|
|
196
|
+
})
|
|
197
|
+
.strict()
|
|
198
|
+
.superRefine((value, ctx) => {
|
|
199
|
+
const hasVerificationToken = (token: string | undefined): boolean => {
|
|
200
|
+
return typeof token === "string" && token.trim().length > 0;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (value.dmPolicy === "open") {
|
|
204
|
+
const allowFrom = value.allowFrom ?? [];
|
|
205
|
+
const hasWildcard = allowFrom.some((entry) => String(entry).trim() === "*");
|
|
206
|
+
if (!hasWildcard) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: z.ZodIssueCode.custom,
|
|
209
|
+
path: ["allowFrom"],
|
|
210
|
+
message: 'channels.feishu.dmPolicy="open" requires channels.feishu.allowFrom to include "*"',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const topLevelConnectionMode = value.connectionMode ?? "websocket";
|
|
216
|
+
if (topLevelConnectionMode === "webhook" && !hasVerificationToken(value.verificationToken)) {
|
|
217
|
+
ctx.addIssue({
|
|
218
|
+
code: z.ZodIssueCode.custom,
|
|
219
|
+
path: ["verificationToken"],
|
|
220
|
+
message: 'channels.feishu.connectionMode="webhook" requires channels.feishu.verificationToken',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const accounts = value.accounts ?? {};
|
|
225
|
+
for (const [accountId, accountCfg] of Object.entries(accounts)) {
|
|
226
|
+
if (!accountCfg) continue;
|
|
227
|
+
const accountConnectionMode = accountCfg.connectionMode ?? topLevelConnectionMode;
|
|
228
|
+
const effectiveVerificationToken = accountCfg.verificationToken ?? value.verificationToken;
|
|
229
|
+
if (accountConnectionMode === "webhook" && !hasVerificationToken(effectiveVerificationToken)) {
|
|
230
|
+
ctx.addIssue({
|
|
231
|
+
code: z.ZodIssueCode.custom,
|
|
232
|
+
path: ["accounts", accountId, "verificationToken"],
|
|
233
|
+
message: `channels.feishu.accounts.${accountId}.connectionMode="webhook" requires verificationToken (account-level or top-level)`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|