@openclaw-plugins/feishu-plus 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +560 -0
- package/index.ts +63 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +65 -0
- package/skills/feishu-doc/SKILL.md +99 -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-wiki/SKILL.md +96 -0
- package/src/accounts.ts +140 -0
- package/src/bitable.ts +441 -0
- package/src/bot.ts +881 -0
- package/src/channel.ts +334 -0
- package/src/client.ts +114 -0
- package/src/config-schema.ts +199 -0
- package/src/directory.ts +165 -0
- package/src/doc-schema.ts +47 -0
- package/src/docx.ts +480 -0
- package/src/drive-schema.ts +46 -0
- package/src/drive.ts +207 -0
- package/src/dynamic-agent.ts +131 -0
- package/src/media.ts +523 -0
- package/src/mention.ts +121 -0
- package/src/monitor.ts +190 -0
- package/src/onboarding.ts +358 -0
- package/src/outbound.ts +40 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +166 -0
- package/src/policy.ts +92 -0
- package/src/probe.ts +43 -0
- package/src/reactions.ts +160 -0
- package/src/reply-dispatcher.ts +174 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +360 -0
- package/src/targets.ts +58 -0
- package/src/tools-config.ts +21 -0
- package/src/types.ts +77 -0
- package/src/typing.ts +75 -0
- package/src/wiki-schema.ts +55 -0
- package/src/wiki.ts +224 -0
package/src/channel.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, 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
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
35
|
+
id: "feishu",
|
|
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, accountId }) => {
|
|
43
|
+
await sendMessageFeishu({
|
|
44
|
+
cfg,
|
|
45
|
+
to: id,
|
|
46
|
+
text: PAIRING_APPROVED_MESSAGE,
|
|
47
|
+
accountId,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
capabilities: {
|
|
52
|
+
chatTypes: ["direct", "channel"],
|
|
53
|
+
polls: false,
|
|
54
|
+
threads: true,
|
|
55
|
+
media: true,
|
|
56
|
+
reactions: true,
|
|
57
|
+
edit: true,
|
|
58
|
+
reply: true,
|
|
59
|
+
},
|
|
60
|
+
agentPrompt: {
|
|
61
|
+
messageToolHints: () => [
|
|
62
|
+
"- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
|
|
63
|
+
"- Feishu supports interactive cards for rich messages.",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
groups: {
|
|
67
|
+
resolveToolPolicy: resolveFeishuGroupToolPolicy,
|
|
68
|
+
},
|
|
69
|
+
reload: { configPrefixes: ["channels.feishu"] },
|
|
70
|
+
configSchema: {
|
|
71
|
+
schema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
properties: {
|
|
75
|
+
enabled: { type: "boolean" },
|
|
76
|
+
appId: { type: "string" },
|
|
77
|
+
appSecret: { type: "string" },
|
|
78
|
+
encryptKey: { type: "string" },
|
|
79
|
+
verificationToken: { type: "string" },
|
|
80
|
+
domain: {
|
|
81
|
+
oneOf: [
|
|
82
|
+
{ type: "string", enum: ["feishu", "lark"] },
|
|
83
|
+
{ type: "string", format: "uri", pattern: "^https://" },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
87
|
+
webhookPath: { type: "string" },
|
|
88
|
+
webhookPort: { type: "integer", minimum: 1 },
|
|
89
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
90
|
+
allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
91
|
+
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
92
|
+
groupAllowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
93
|
+
requireMention: { type: "boolean" },
|
|
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
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
config: {
|
|
121
|
+
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
|
122
|
+
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
|
123
|
+
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
|
124
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
125
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
126
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
127
|
+
|
|
128
|
+
if (isDefault) {
|
|
129
|
+
// For default account, set top-level enabled
|
|
130
|
+
return {
|
|
131
|
+
...cfg,
|
|
132
|
+
channels: {
|
|
133
|
+
...cfg.channels,
|
|
134
|
+
feishu: {
|
|
135
|
+
...cfg.channels?.feishu,
|
|
136
|
+
enabled,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// For named accounts, set enabled in accounts[accountId]
|
|
143
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
144
|
+
return {
|
|
145
|
+
...cfg,
|
|
146
|
+
channels: {
|
|
147
|
+
...cfg.channels,
|
|
148
|
+
feishu: {
|
|
149
|
+
...feishuCfg,
|
|
150
|
+
accounts: {
|
|
151
|
+
...feishuCfg?.accounts,
|
|
152
|
+
[accountId]: {
|
|
153
|
+
...feishuCfg?.accounts?.[accountId],
|
|
154
|
+
enabled,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
deleteAccount: ({ cfg, accountId }) => {
|
|
162
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
163
|
+
|
|
164
|
+
if (isDefault) {
|
|
165
|
+
// Delete entire feishu config
|
|
166
|
+
const next = { ...cfg } as ClawdbotConfig;
|
|
167
|
+
const nextChannels = { ...cfg.channels };
|
|
168
|
+
delete (nextChannels as Record<string, unknown>).feishu;
|
|
169
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
170
|
+
next.channels = nextChannels;
|
|
171
|
+
} else {
|
|
172
|
+
delete next.channels;
|
|
173
|
+
}
|
|
174
|
+
return next;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Delete specific account from accounts
|
|
178
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
179
|
+
const accounts = { ...feishuCfg?.accounts };
|
|
180
|
+
delete accounts[accountId];
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
...cfg,
|
|
184
|
+
channels: {
|
|
185
|
+
...cfg.channels,
|
|
186
|
+
feishu: {
|
|
187
|
+
...feishuCfg,
|
|
188
|
+
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
isConfigured: (account) => account.configured,
|
|
194
|
+
describeAccount: (account) => ({
|
|
195
|
+
accountId: account.accountId,
|
|
196
|
+
enabled: account.enabled,
|
|
197
|
+
configured: account.configured,
|
|
198
|
+
name: account.name,
|
|
199
|
+
appId: account.appId,
|
|
200
|
+
domain: account.domain,
|
|
201
|
+
}),
|
|
202
|
+
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
203
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
204
|
+
return account.config?.allowFrom ?? [];
|
|
205
|
+
},
|
|
206
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
207
|
+
allowFrom
|
|
208
|
+
.map((entry) => String(entry).trim())
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.map((entry) => entry.toLowerCase()),
|
|
211
|
+
},
|
|
212
|
+
security: {
|
|
213
|
+
collectWarnings: ({ cfg, accountId }) => {
|
|
214
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
215
|
+
const feishuCfg = account.config;
|
|
216
|
+
const defaultGroupPolicy = (cfg.channels as Record<string, { groupPolicy?: string }> | undefined)?.defaults?.groupPolicy;
|
|
217
|
+
const groupPolicy = feishuCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
218
|
+
if (groupPolicy !== "open") return [];
|
|
219
|
+
return [
|
|
220
|
+
`- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
|
|
221
|
+
];
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
setup: {
|
|
225
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
226
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
227
|
+
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
228
|
+
|
|
229
|
+
if (isDefault) {
|
|
230
|
+
return {
|
|
231
|
+
...cfg,
|
|
232
|
+
channels: {
|
|
233
|
+
...cfg.channels,
|
|
234
|
+
feishu: {
|
|
235
|
+
...cfg.channels?.feishu,
|
|
236
|
+
enabled: true,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
243
|
+
return {
|
|
244
|
+
...cfg,
|
|
245
|
+
channels: {
|
|
246
|
+
...cfg.channels,
|
|
247
|
+
feishu: {
|
|
248
|
+
...feishuCfg,
|
|
249
|
+
accounts: {
|
|
250
|
+
...feishuCfg?.accounts,
|
|
251
|
+
[accountId]: {
|
|
252
|
+
...feishuCfg?.accounts?.[accountId],
|
|
253
|
+
enabled: true,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
onboarding: feishuOnboardingAdapter,
|
|
262
|
+
messaging: {
|
|
263
|
+
normalizeTarget: normalizeFeishuTarget,
|
|
264
|
+
targetResolver: {
|
|
265
|
+
looksLikeId: looksLikeFeishuId,
|
|
266
|
+
hint: "<chatId|user:openId|chat:chatId>",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
directory: {
|
|
270
|
+
self: async () => null,
|
|
271
|
+
listPeers: async ({ cfg, query, limit, accountId }) =>
|
|
272
|
+
listFeishuDirectoryPeers({ cfg, query, limit, accountId }),
|
|
273
|
+
listGroups: async ({ cfg, query, limit, accountId }) =>
|
|
274
|
+
listFeishuDirectoryGroups({ cfg, query, limit, accountId }),
|
|
275
|
+
listPeersLive: async ({ cfg, query, limit, accountId }) =>
|
|
276
|
+
listFeishuDirectoryPeersLive({ cfg, query, limit, accountId }),
|
|
277
|
+
listGroupsLive: async ({ cfg, query, limit, accountId }) =>
|
|
278
|
+
listFeishuDirectoryGroupsLive({ cfg, query, limit, accountId }),
|
|
279
|
+
},
|
|
280
|
+
outbound: feishuOutbound,
|
|
281
|
+
status: {
|
|
282
|
+
defaultRuntime: {
|
|
283
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
284
|
+
running: false,
|
|
285
|
+
lastStartAt: null,
|
|
286
|
+
lastStopAt: null,
|
|
287
|
+
lastError: null,
|
|
288
|
+
port: null,
|
|
289
|
+
},
|
|
290
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
291
|
+
configured: snapshot.configured ?? false,
|
|
292
|
+
running: snapshot.running ?? false,
|
|
293
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
294
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
295
|
+
lastError: snapshot.lastError ?? null,
|
|
296
|
+
port: snapshot.port ?? null,
|
|
297
|
+
probe: snapshot.probe,
|
|
298
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
299
|
+
}),
|
|
300
|
+
probeAccount: async ({ cfg, accountId }) => {
|
|
301
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
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,199 @@
|
|
|
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 FeishuDomainSchema = z.union([
|
|
7
|
+
z.enum(["feishu", "lark"]),
|
|
8
|
+
z.string().url().startsWith("https://"),
|
|
9
|
+
]);
|
|
10
|
+
const FeishuConnectionModeSchema = z.enum(["websocket", "webhook"]);
|
|
11
|
+
|
|
12
|
+
const ToolPolicySchema = z
|
|
13
|
+
.object({
|
|
14
|
+
allow: z.array(z.string()).optional(),
|
|
15
|
+
deny: z.array(z.string()).optional(),
|
|
16
|
+
})
|
|
17
|
+
.strict()
|
|
18
|
+
.optional();
|
|
19
|
+
|
|
20
|
+
const DmConfigSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
enabled: z.boolean().optional(),
|
|
23
|
+
systemPrompt: z.string().optional(),
|
|
24
|
+
})
|
|
25
|
+
.strict()
|
|
26
|
+
.optional();
|
|
27
|
+
|
|
28
|
+
const MarkdownConfigSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
mode: z.enum(["native", "escape", "strip"]).optional(),
|
|
31
|
+
tableMode: z.enum(["native", "ascii", "simple"]).optional(),
|
|
32
|
+
})
|
|
33
|
+
.strict()
|
|
34
|
+
.optional();
|
|
35
|
+
|
|
36
|
+
// Message render mode: auto (default) = detect markdown, raw = plain text, card = always card
|
|
37
|
+
const RenderModeSchema = z.enum(["auto", "raw", "card"]).optional();
|
|
38
|
+
|
|
39
|
+
const BlockStreamingCoalesceSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
enabled: z.boolean().optional(),
|
|
42
|
+
minDelayMs: z.number().int().positive().optional(),
|
|
43
|
+
maxDelayMs: z.number().int().positive().optional(),
|
|
44
|
+
})
|
|
45
|
+
.strict()
|
|
46
|
+
.optional();
|
|
47
|
+
|
|
48
|
+
const ChannelHeartbeatVisibilitySchema = z
|
|
49
|
+
.object({
|
|
50
|
+
visibility: z.enum(["visible", "hidden"]).optional(),
|
|
51
|
+
intervalMs: z.number().int().positive().optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict()
|
|
54
|
+
.optional();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Dynamic agent creation configuration.
|
|
58
|
+
* When enabled, a new agent is created for each unique DM user.
|
|
59
|
+
*/
|
|
60
|
+
const DynamicAgentCreationSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
enabled: z.boolean().optional(),
|
|
63
|
+
workspaceTemplate: z.string().optional(),
|
|
64
|
+
agentDirTemplate: z.string().optional(),
|
|
65
|
+
maxAgents: z.number().int().positive().optional(),
|
|
66
|
+
})
|
|
67
|
+
.strict()
|
|
68
|
+
.optional();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Feishu tools configuration.
|
|
72
|
+
* Controls which tool categories are enabled.
|
|
73
|
+
*
|
|
74
|
+
* Dependencies:
|
|
75
|
+
* - wiki requires doc (wiki content is edited via doc tools)
|
|
76
|
+
* - perm can work independently but is typically used with drive
|
|
77
|
+
*/
|
|
78
|
+
const FeishuToolsConfigSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
doc: z.boolean().optional(), // Document operations (default: true)
|
|
81
|
+
wiki: z.boolean().optional(), // Knowledge base operations (default: true, requires doc)
|
|
82
|
+
drive: z.boolean().optional(), // Cloud storage operations (default: true)
|
|
83
|
+
perm: z.boolean().optional(), // Permission management (default: false, sensitive)
|
|
84
|
+
scopes: z.boolean().optional(), // App scopes diagnostic (default: true)
|
|
85
|
+
})
|
|
86
|
+
.strict()
|
|
87
|
+
.optional();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Topic session isolation mode for group chats.
|
|
91
|
+
* - "disabled" (default): All messages in a group share one session
|
|
92
|
+
* - "enabled": Messages in different topics get separate sessions
|
|
93
|
+
*
|
|
94
|
+
* When enabled, the session key becomes `chat:{chatId}:topic:{rootId}`
|
|
95
|
+
* for messages within a topic thread, allowing isolated conversations.
|
|
96
|
+
*/
|
|
97
|
+
const TopicSessionModeSchema = z.enum(["disabled", "enabled"]).optional();
|
|
98
|
+
|
|
99
|
+
export const FeishuGroupSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
requireMention: z.boolean().optional(),
|
|
102
|
+
tools: ToolPolicySchema,
|
|
103
|
+
skills: z.array(z.string()).optional(),
|
|
104
|
+
enabled: z.boolean().optional(),
|
|
105
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
106
|
+
systemPrompt: z.string().optional(),
|
|
107
|
+
topicSessionMode: TopicSessionModeSchema,
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Per-account configuration.
|
|
113
|
+
* All fields are optional - missing fields inherit from top-level config.
|
|
114
|
+
*/
|
|
115
|
+
export const FeishuAccountConfigSchema = z
|
|
116
|
+
.object({
|
|
117
|
+
enabled: z.boolean().optional(),
|
|
118
|
+
name: z.string().optional(), // Display name for this account
|
|
119
|
+
appId: z.string().optional(),
|
|
120
|
+
appSecret: z.string().optional(),
|
|
121
|
+
encryptKey: z.string().optional(),
|
|
122
|
+
verificationToken: z.string().optional(),
|
|
123
|
+
domain: FeishuDomainSchema.optional(),
|
|
124
|
+
connectionMode: FeishuConnectionModeSchema.optional(),
|
|
125
|
+
webhookPath: z.string().optional(),
|
|
126
|
+
webhookPort: z.number().int().positive().optional(),
|
|
127
|
+
capabilities: z.array(z.string()).optional(),
|
|
128
|
+
markdown: MarkdownConfigSchema,
|
|
129
|
+
configWrites: z.boolean().optional(),
|
|
130
|
+
dmPolicy: DmPolicySchema.optional(),
|
|
131
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
132
|
+
groupPolicy: GroupPolicySchema.optional(),
|
|
133
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
134
|
+
requireMention: z.boolean().optional(),
|
|
135
|
+
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
136
|
+
historyLimit: z.number().int().min(0).optional(),
|
|
137
|
+
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
138
|
+
dms: z.record(z.string(), DmConfigSchema).optional(),
|
|
139
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
140
|
+
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
141
|
+
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
|
142
|
+
mediaMaxMb: z.number().positive().optional(),
|
|
143
|
+
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
144
|
+
renderMode: RenderModeSchema,
|
|
145
|
+
tools: FeishuToolsConfigSchema,
|
|
146
|
+
})
|
|
147
|
+
.strict();
|
|
148
|
+
|
|
149
|
+
export const FeishuConfigSchema = z
|
|
150
|
+
.object({
|
|
151
|
+
enabled: z.boolean().optional(),
|
|
152
|
+
// Top-level credentials (backward compatible for single-account mode)
|
|
153
|
+
appId: z.string().optional(),
|
|
154
|
+
appSecret: z.string().optional(),
|
|
155
|
+
encryptKey: z.string().optional(),
|
|
156
|
+
verificationToken: z.string().optional(),
|
|
157
|
+
domain: FeishuDomainSchema.optional().default("feishu"),
|
|
158
|
+
connectionMode: FeishuConnectionModeSchema.optional().default("websocket"),
|
|
159
|
+
webhookPath: z.string().optional().default("/feishu/events"),
|
|
160
|
+
webhookPort: z.number().int().positive().optional(),
|
|
161
|
+
capabilities: z.array(z.string()).optional(),
|
|
162
|
+
markdown: MarkdownConfigSchema,
|
|
163
|
+
configWrites: z.boolean().optional(),
|
|
164
|
+
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
165
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
166
|
+
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
|
167
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
168
|
+
requireMention: z.boolean().optional().default(true),
|
|
169
|
+
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
170
|
+
topicSessionMode: TopicSessionModeSchema,
|
|
171
|
+
historyLimit: z.number().int().min(0).optional(),
|
|
172
|
+
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
173
|
+
dms: z.record(z.string(), DmConfigSchema).optional(),
|
|
174
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
175
|
+
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
176
|
+
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
|
177
|
+
mediaMaxMb: z.number().positive().optional(),
|
|
178
|
+
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
179
|
+
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
|
180
|
+
tools: FeishuToolsConfigSchema,
|
|
181
|
+
// Dynamic agent creation for DM users
|
|
182
|
+
dynamicAgentCreation: DynamicAgentCreationSchema,
|
|
183
|
+
// Multi-account configuration
|
|
184
|
+
accounts: z.record(z.string(), FeishuAccountConfigSchema.optional()).optional(),
|
|
185
|
+
})
|
|
186
|
+
.strict()
|
|
187
|
+
.superRefine((value, ctx) => {
|
|
188
|
+
if (value.dmPolicy === "open") {
|
|
189
|
+
const allowFrom = value.allowFrom ?? [];
|
|
190
|
+
const hasWildcard = allowFrom.some((entry) => String(entry).trim() === "*");
|
|
191
|
+
if (!hasWildcard) {
|
|
192
|
+
ctx.addIssue({
|
|
193
|
+
code: z.ZodIssueCode.custom,
|
|
194
|
+
path: ["allowFrom"],
|
|
195
|
+
message: 'channels.feishu.dmPolicy="open" requires channels.feishu.allowFrom to include "*"',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|