@openclaw/feishu 2026.2.2
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/index.ts +15 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +33 -0
- package/src/channel.ts +276 -0
- package/src/config-schema.ts +46 -0
- package/src/onboarding.ts +278 -0
package/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { feishuPlugin } from "./src/channel.js";
|
|
4
|
+
|
|
5
|
+
const plugin = {
|
|
6
|
+
id: "feishu",
|
|
7
|
+
name: "Feishu",
|
|
8
|
+
description: "Feishu (Lark) channel plugin",
|
|
9
|
+
configSchema: emptyPluginConfigSchema(),
|
|
10
|
+
register(api: OpenClawPluginApi) {
|
|
11
|
+
api.registerChannel({ plugin: feishuPlugin });
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openclaw/feishu",
|
|
3
|
+
"version": "2026.2.2",
|
|
4
|
+
"description": "OpenClaw Feishu channel plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"openclaw": "workspace:*"
|
|
8
|
+
},
|
|
9
|
+
"openclaw": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./index.ts"
|
|
12
|
+
],
|
|
13
|
+
"channel": {
|
|
14
|
+
"id": "feishu",
|
|
15
|
+
"label": "Feishu",
|
|
16
|
+
"selectionLabel": "Feishu (Lark Open Platform)",
|
|
17
|
+
"detailLabel": "Feishu Bot",
|
|
18
|
+
"docsPath": "/channels/feishu",
|
|
19
|
+
"docsLabel": "feishu",
|
|
20
|
+
"blurb": "Feishu/Lark bot via WebSocket.",
|
|
21
|
+
"aliases": [
|
|
22
|
+
"lark"
|
|
23
|
+
],
|
|
24
|
+
"order": 35,
|
|
25
|
+
"quickstartAllowFrom": true
|
|
26
|
+
},
|
|
27
|
+
"install": {
|
|
28
|
+
"npmSpec": "@openclaw/feishu",
|
|
29
|
+
"localPath": "extensions/feishu",
|
|
30
|
+
"defaultChoice": "npm"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildChannelConfigSchema,
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
deleteAccountFromConfigSection,
|
|
5
|
+
feishuOutbound,
|
|
6
|
+
formatPairingApproveHint,
|
|
7
|
+
listFeishuAccountIds,
|
|
8
|
+
monitorFeishuProvider,
|
|
9
|
+
normalizeFeishuTarget,
|
|
10
|
+
PAIRING_APPROVED_MESSAGE,
|
|
11
|
+
probeFeishu,
|
|
12
|
+
resolveDefaultFeishuAccountId,
|
|
13
|
+
resolveFeishuAccount,
|
|
14
|
+
resolveFeishuConfig,
|
|
15
|
+
resolveFeishuGroupRequireMention,
|
|
16
|
+
setAccountEnabledInConfigSection,
|
|
17
|
+
type ChannelAccountSnapshot,
|
|
18
|
+
type ChannelPlugin,
|
|
19
|
+
type ChannelStatusIssue,
|
|
20
|
+
type ResolvedFeishuAccount,
|
|
21
|
+
} from "openclaw/plugin-sdk";
|
|
22
|
+
import { FeishuConfigSchema } from "./config-schema.js";
|
|
23
|
+
import { feishuOnboardingAdapter } from "./onboarding.js";
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
id: "feishu",
|
|
27
|
+
label: "Feishu",
|
|
28
|
+
selectionLabel: "Feishu (Lark Open Platform)",
|
|
29
|
+
detailLabel: "Feishu Bot",
|
|
30
|
+
docsPath: "/channels/feishu",
|
|
31
|
+
docsLabel: "feishu",
|
|
32
|
+
blurb: "Feishu/Lark bot via WebSocket.",
|
|
33
|
+
aliases: ["lark"],
|
|
34
|
+
order: 35,
|
|
35
|
+
quickstartAllowFrom: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizeAllowEntry = (entry: string) => entry.replace(/^(feishu|lark):/i, "").trim();
|
|
39
|
+
|
|
40
|
+
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
41
|
+
id: "feishu",
|
|
42
|
+
meta,
|
|
43
|
+
onboarding: feishuOnboardingAdapter,
|
|
44
|
+
pairing: {
|
|
45
|
+
idLabel: "feishuOpenId",
|
|
46
|
+
normalizeAllowEntry: normalizeAllowEntry,
|
|
47
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
48
|
+
const account = resolveFeishuAccount({ cfg });
|
|
49
|
+
if (!account.config.appId || !account.config.appSecret) {
|
|
50
|
+
throw new Error("Feishu app credentials not configured");
|
|
51
|
+
}
|
|
52
|
+
await feishuOutbound.sendText({ cfg, to: id, text: PAIRING_APPROVED_MESSAGE });
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
capabilities: {
|
|
56
|
+
chatTypes: ["direct", "group"],
|
|
57
|
+
media: true,
|
|
58
|
+
reactions: false,
|
|
59
|
+
threads: false,
|
|
60
|
+
polls: false,
|
|
61
|
+
nativeCommands: false,
|
|
62
|
+
blockStreaming: true,
|
|
63
|
+
},
|
|
64
|
+
reload: { configPrefixes: ["channels.feishu"] },
|
|
65
|
+
outbound: feishuOutbound,
|
|
66
|
+
messaging: {
|
|
67
|
+
normalizeTarget: normalizeFeishuTarget,
|
|
68
|
+
targetResolver: {
|
|
69
|
+
looksLikeId: (raw, normalized) => {
|
|
70
|
+
const value = (normalized ?? raw).trim();
|
|
71
|
+
if (!value) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return /^o[cun]_[a-zA-Z0-9]+$/.test(value) || /^(user|group|chat):/i.test(value);
|
|
75
|
+
},
|
|
76
|
+
hint: "<open_id|union_id|chat_id>",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
configSchema: buildChannelConfigSchema(FeishuConfigSchema),
|
|
80
|
+
config: {
|
|
81
|
+
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
|
82
|
+
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
|
83
|
+
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
|
84
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
85
|
+
setAccountEnabledInConfigSection({
|
|
86
|
+
cfg,
|
|
87
|
+
sectionKey: "feishu",
|
|
88
|
+
accountId,
|
|
89
|
+
enabled,
|
|
90
|
+
allowTopLevel: true,
|
|
91
|
+
}),
|
|
92
|
+
deleteAccount: ({ cfg, accountId }) =>
|
|
93
|
+
deleteAccountFromConfigSection({
|
|
94
|
+
cfg,
|
|
95
|
+
sectionKey: "feishu",
|
|
96
|
+
accountId,
|
|
97
|
+
clearBaseFields: ["appId", "appSecret", "appSecretFile", "name", "botName"],
|
|
98
|
+
}),
|
|
99
|
+
isConfigured: (account) => account.tokenSource !== "none",
|
|
100
|
+
describeAccount: (account): ChannelAccountSnapshot => ({
|
|
101
|
+
accountId: account.accountId,
|
|
102
|
+
name: account.name,
|
|
103
|
+
enabled: account.enabled,
|
|
104
|
+
configured: account.tokenSource !== "none",
|
|
105
|
+
tokenSource: account.tokenSource,
|
|
106
|
+
}),
|
|
107
|
+
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
108
|
+
resolveFeishuConfig({ cfg, accountId: accountId ?? undefined }).allowFrom.map((entry) =>
|
|
109
|
+
String(entry),
|
|
110
|
+
),
|
|
111
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
112
|
+
allowFrom
|
|
113
|
+
.map((entry) => String(entry).trim())
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map((entry) => (entry === "*" ? entry : normalizeAllowEntry(entry)))
|
|
116
|
+
.map((entry) => (entry === "*" ? entry : entry.toLowerCase())),
|
|
117
|
+
},
|
|
118
|
+
security: {
|
|
119
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
120
|
+
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
121
|
+
const useAccountPath = Boolean(cfg.channels?.feishu?.accounts?.[resolvedAccountId]);
|
|
122
|
+
const basePath = useAccountPath
|
|
123
|
+
? `channels.feishu.accounts.${resolvedAccountId}.`
|
|
124
|
+
: "channels.feishu.";
|
|
125
|
+
return {
|
|
126
|
+
policy: account.config.dmPolicy ?? "pairing",
|
|
127
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
128
|
+
policyPath: `${basePath}dmPolicy`,
|
|
129
|
+
allowFromPath: basePath,
|
|
130
|
+
approveHint: formatPairingApproveHint("feishu"),
|
|
131
|
+
normalizeEntry: normalizeAllowEntry,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
groups: {
|
|
136
|
+
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
|
137
|
+
if (!groupId) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return resolveFeishuGroupRequireMention({
|
|
141
|
+
cfg,
|
|
142
|
+
accountId: accountId ?? undefined,
|
|
143
|
+
chatId: groupId,
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
directory: {
|
|
148
|
+
self: async () => null,
|
|
149
|
+
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
150
|
+
const resolved = resolveFeishuConfig({ cfg, accountId: accountId ?? undefined });
|
|
151
|
+
const normalizedQuery = query?.trim().toLowerCase() ?? "";
|
|
152
|
+
const peers = resolved.allowFrom
|
|
153
|
+
.map((entry) => String(entry).trim())
|
|
154
|
+
.filter((entry) => Boolean(entry) && entry !== "*")
|
|
155
|
+
.map((entry) => normalizeAllowEntry(entry))
|
|
156
|
+
.filter((entry) => (normalizedQuery ? entry.toLowerCase().includes(normalizedQuery) : true))
|
|
157
|
+
.slice(0, limit && limit > 0 ? limit : undefined)
|
|
158
|
+
.map((id) => ({ kind: "user", id }) as const);
|
|
159
|
+
return peers;
|
|
160
|
+
},
|
|
161
|
+
listGroups: async ({ cfg, accountId, query, limit }) => {
|
|
162
|
+
const resolved = resolveFeishuConfig({ cfg, accountId: accountId ?? undefined });
|
|
163
|
+
const normalizedQuery = query?.trim().toLowerCase() ?? "";
|
|
164
|
+
const groups = Object.keys(resolved.groups ?? {})
|
|
165
|
+
.filter((id) => (normalizedQuery ? id.toLowerCase().includes(normalizedQuery) : true))
|
|
166
|
+
.slice(0, limit && limit > 0 ? limit : undefined)
|
|
167
|
+
.map((id) => ({ kind: "group", id }) as const);
|
|
168
|
+
return groups;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
status: {
|
|
172
|
+
defaultRuntime: {
|
|
173
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
174
|
+
running: false,
|
|
175
|
+
lastStartAt: null,
|
|
176
|
+
lastStopAt: null,
|
|
177
|
+
lastError: null,
|
|
178
|
+
},
|
|
179
|
+
collectStatusIssues: (accounts) => {
|
|
180
|
+
const issues: ChannelStatusIssue[] = [];
|
|
181
|
+
for (const account of accounts) {
|
|
182
|
+
if (!account.configured) {
|
|
183
|
+
issues.push({
|
|
184
|
+
channel: "feishu",
|
|
185
|
+
accountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
186
|
+
kind: "config",
|
|
187
|
+
message: "Feishu app ID/secret not configured",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return issues;
|
|
192
|
+
},
|
|
193
|
+
buildChannelSummary: async ({ snapshot }) => ({
|
|
194
|
+
configured: snapshot.configured ?? false,
|
|
195
|
+
tokenSource: snapshot.tokenSource ?? "none",
|
|
196
|
+
running: snapshot.running ?? false,
|
|
197
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
198
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
199
|
+
lastError: snapshot.lastError ?? null,
|
|
200
|
+
probe: snapshot.probe,
|
|
201
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
202
|
+
}),
|
|
203
|
+
probeAccount: async ({ account, timeoutMs }) =>
|
|
204
|
+
probeFeishu(account.config.appId, account.config.appSecret, timeoutMs, account.config.domain),
|
|
205
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
|
206
|
+
const configured = account.tokenSource !== "none";
|
|
207
|
+
return {
|
|
208
|
+
accountId: account.accountId,
|
|
209
|
+
name: account.name,
|
|
210
|
+
enabled: account.enabled,
|
|
211
|
+
configured,
|
|
212
|
+
tokenSource: account.tokenSource,
|
|
213
|
+
running: runtime?.running ?? false,
|
|
214
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
215
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
216
|
+
lastError: runtime?.lastError ?? null,
|
|
217
|
+
probe,
|
|
218
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
219
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
logSelfId: ({ account, runtime }) => {
|
|
223
|
+
const appId = account.config.appId;
|
|
224
|
+
if (appId) {
|
|
225
|
+
runtime.log?.(`feishu:${appId}`);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
gateway: {
|
|
230
|
+
startAccount: async (ctx) => {
|
|
231
|
+
const { account, log, setStatus, abortSignal, cfg, runtime } = ctx;
|
|
232
|
+
const { appId, appSecret, domain } = account.config;
|
|
233
|
+
if (!appId || !appSecret) {
|
|
234
|
+
throw new Error("Feishu app ID/secret not configured");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let feishuBotLabel = "";
|
|
238
|
+
try {
|
|
239
|
+
const probe = await probeFeishu(appId, appSecret, 5000, domain);
|
|
240
|
+
if (probe.ok && probe.bot?.appName) {
|
|
241
|
+
feishuBotLabel = ` (${probe.bot.appName})`;
|
|
242
|
+
}
|
|
243
|
+
if (probe.ok && probe.bot) {
|
|
244
|
+
setStatus({ accountId: account.accountId, bot: probe.bot });
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
log?.info(`[${account.accountId}] starting Feishu provider${feishuBotLabel}`);
|
|
251
|
+
setStatus({
|
|
252
|
+
accountId: account.accountId,
|
|
253
|
+
running: true,
|
|
254
|
+
lastStartAt: Date.now(),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await monitorFeishuProvider({
|
|
259
|
+
appId,
|
|
260
|
+
appSecret,
|
|
261
|
+
accountId: account.accountId,
|
|
262
|
+
config: cfg,
|
|
263
|
+
runtime,
|
|
264
|
+
abortSignal,
|
|
265
|
+
});
|
|
266
|
+
} catch (err) {
|
|
267
|
+
setStatus({
|
|
268
|
+
accountId: account.accountId,
|
|
269
|
+
running: false,
|
|
270
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
271
|
+
});
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const allowFromEntry = z.union([z.string(), z.number()]);
|
|
5
|
+
const toolsBySenderSchema = z.record(z.string(), ToolPolicySchema).optional();
|
|
6
|
+
|
|
7
|
+
const FeishuGroupSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
enabled: z.boolean().optional(),
|
|
10
|
+
requireMention: z.boolean().optional(),
|
|
11
|
+
allowFrom: z.array(allowFromEntry).optional(),
|
|
12
|
+
tools: ToolPolicySchema,
|
|
13
|
+
toolsBySender: toolsBySenderSchema,
|
|
14
|
+
systemPrompt: z.string().optional(),
|
|
15
|
+
skills: z.array(z.string()).optional(),
|
|
16
|
+
})
|
|
17
|
+
.strict();
|
|
18
|
+
|
|
19
|
+
const FeishuAccountSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
name: z.string().optional(),
|
|
22
|
+
enabled: z.boolean().optional(),
|
|
23
|
+
appId: z.string().optional(),
|
|
24
|
+
appSecret: z.string().optional(),
|
|
25
|
+
appSecretFile: z.string().optional(),
|
|
26
|
+
domain: z.string().optional(),
|
|
27
|
+
botName: z.string().optional(),
|
|
28
|
+
markdown: MarkdownConfigSchema,
|
|
29
|
+
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
|
|
30
|
+
groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional(),
|
|
31
|
+
allowFrom: z.array(allowFromEntry).optional(),
|
|
32
|
+
groupAllowFrom: z.array(allowFromEntry).optional(),
|
|
33
|
+
historyLimit: z.number().optional(),
|
|
34
|
+
dmHistoryLimit: z.number().optional(),
|
|
35
|
+
textChunkLimit: z.number().optional(),
|
|
36
|
+
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
37
|
+
blockStreaming: z.boolean().optional(),
|
|
38
|
+
streaming: z.boolean().optional(),
|
|
39
|
+
mediaMaxMb: z.number().optional(),
|
|
40
|
+
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
|
|
44
|
+
export const FeishuConfigSchema = FeishuAccountSchema.extend({
|
|
45
|
+
accounts: z.object({}).catchall(FeishuAccountSchema).optional(),
|
|
46
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOnboardingAdapter,
|
|
3
|
+
ChannelOnboardingDmPolicy,
|
|
4
|
+
DmPolicy,
|
|
5
|
+
OpenClawConfig,
|
|
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
|
+
import {
|
|
16
|
+
listFeishuAccountIds,
|
|
17
|
+
resolveDefaultFeishuAccountId,
|
|
18
|
+
resolveFeishuAccount,
|
|
19
|
+
} from "openclaw/plugin-sdk";
|
|
20
|
+
|
|
21
|
+
const channel = "feishu" as const;
|
|
22
|
+
|
|
23
|
+
function setFeishuDmPolicy(cfg: OpenClawConfig, policy: DmPolicy): OpenClawConfig {
|
|
24
|
+
const allowFrom =
|
|
25
|
+
policy === "open" ? addWildcardAllowFrom(cfg.channels?.feishu?.allowFrom) : undefined;
|
|
26
|
+
return {
|
|
27
|
+
...cfg,
|
|
28
|
+
channels: {
|
|
29
|
+
...cfg.channels,
|
|
30
|
+
feishu: {
|
|
31
|
+
...cfg.channels?.feishu,
|
|
32
|
+
enabled: true,
|
|
33
|
+
dmPolicy: policy,
|
|
34
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function noteFeishuSetup(prompter: WizardPrompter): Promise<void> {
|
|
41
|
+
await prompter.note(
|
|
42
|
+
[
|
|
43
|
+
"Create a Feishu/Lark app and enable Bot + Event Subscription (WebSocket).",
|
|
44
|
+
"Copy the App ID and App Secret from the app credentials page.",
|
|
45
|
+
'Lark (global): use open.larksuite.com and set domain="lark".',
|
|
46
|
+
`Docs: ${formatDocsLink("/channels/feishu", "channels/feishu")}`,
|
|
47
|
+
].join("\n"),
|
|
48
|
+
"Feishu setup",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeAllowEntry(entry: string): string {
|
|
53
|
+
return entry.replace(/^(feishu|lark):/i, "").trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveDomainChoice(domain?: string | null): "feishu" | "lark" {
|
|
57
|
+
const normalized = String(domain ?? "").toLowerCase();
|
|
58
|
+
if (normalized.includes("lark") || normalized.includes("larksuite")) {
|
|
59
|
+
return "lark";
|
|
60
|
+
}
|
|
61
|
+
return "feishu";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function promptFeishuAllowFrom(params: {
|
|
65
|
+
cfg: OpenClawConfig;
|
|
66
|
+
prompter: WizardPrompter;
|
|
67
|
+
accountId?: string | null;
|
|
68
|
+
}): Promise<OpenClawConfig> {
|
|
69
|
+
const { cfg, prompter } = params;
|
|
70
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
71
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
72
|
+
const existingAllowFrom = isDefault
|
|
73
|
+
? (cfg.channels?.feishu?.allowFrom ?? [])
|
|
74
|
+
: (cfg.channels?.feishu?.accounts?.[accountId]?.allowFrom ?? []);
|
|
75
|
+
|
|
76
|
+
const entry = await prompter.text({
|
|
77
|
+
message: "Feishu allowFrom (open_id or union_id)",
|
|
78
|
+
placeholder: "ou_xxx",
|
|
79
|
+
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
80
|
+
validate: (value) => {
|
|
81
|
+
const raw = String(value ?? "").trim();
|
|
82
|
+
if (!raw) {
|
|
83
|
+
return "Required";
|
|
84
|
+
}
|
|
85
|
+
const entries = raw
|
|
86
|
+
.split(/[\n,;]+/g)
|
|
87
|
+
.map((item) => normalizeAllowEntry(item))
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
const invalid = entries.filter((item) => item !== "*" && !/^o[un]_[a-zA-Z0-9]+$/.test(item));
|
|
90
|
+
if (invalid.length > 0) {
|
|
91
|
+
return `Invalid Feishu ids: ${invalid.join(", ")}`;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const parsed = String(entry)
|
|
98
|
+
.split(/[\n,;]+/g)
|
|
99
|
+
.map((item) => normalizeAllowEntry(item))
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
const merged = [
|
|
102
|
+
...existingAllowFrom.map((item) => normalizeAllowEntry(String(item))),
|
|
103
|
+
...parsed,
|
|
104
|
+
].filter(Boolean);
|
|
105
|
+
const unique = Array.from(new Set(merged));
|
|
106
|
+
|
|
107
|
+
if (isDefault) {
|
|
108
|
+
return {
|
|
109
|
+
...cfg,
|
|
110
|
+
channels: {
|
|
111
|
+
...cfg.channels,
|
|
112
|
+
feishu: {
|
|
113
|
+
...cfg.channels?.feishu,
|
|
114
|
+
enabled: true,
|
|
115
|
+
dmPolicy: "allowlist",
|
|
116
|
+
allowFrom: unique,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...cfg,
|
|
124
|
+
channels: {
|
|
125
|
+
...cfg.channels,
|
|
126
|
+
feishu: {
|
|
127
|
+
...cfg.channels?.feishu,
|
|
128
|
+
enabled: true,
|
|
129
|
+
accounts: {
|
|
130
|
+
...cfg.channels?.feishu?.accounts,
|
|
131
|
+
[accountId]: {
|
|
132
|
+
...cfg.channels?.feishu?.accounts?.[accountId],
|
|
133
|
+
enabled: cfg.channels?.feishu?.accounts?.[accountId]?.enabled ?? true,
|
|
134
|
+
dmPolicy: "allowlist",
|
|
135
|
+
allowFrom: unique,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
144
|
+
label: "Feishu",
|
|
145
|
+
channel,
|
|
146
|
+
policyKey: "channels.feishu.dmPolicy",
|
|
147
|
+
allowFromKey: "channels.feishu.allowFrom",
|
|
148
|
+
getCurrent: (cfg) => cfg.channels?.feishu?.dmPolicy ?? "pairing",
|
|
149
|
+
setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy),
|
|
150
|
+
promptAllowFrom: promptFeishuAllowFrom,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function updateFeishuConfig(
|
|
154
|
+
cfg: OpenClawConfig,
|
|
155
|
+
accountId: string,
|
|
156
|
+
updates: { appId?: string; appSecret?: string; domain?: string; enabled?: boolean },
|
|
157
|
+
): OpenClawConfig {
|
|
158
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
159
|
+
const next = { ...cfg } as OpenClawConfig;
|
|
160
|
+
const feishu = { ...next.channels?.feishu } as Record<string, unknown>;
|
|
161
|
+
const accounts = feishu.accounts
|
|
162
|
+
? { ...(feishu.accounts as Record<string, unknown>) }
|
|
163
|
+
: undefined;
|
|
164
|
+
|
|
165
|
+
if (isDefault && !accounts) {
|
|
166
|
+
return {
|
|
167
|
+
...next,
|
|
168
|
+
channels: {
|
|
169
|
+
...next.channels,
|
|
170
|
+
feishu: {
|
|
171
|
+
...feishu,
|
|
172
|
+
...updates,
|
|
173
|
+
enabled: updates.enabled ?? true,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const resolvedAccounts = accounts ?? {};
|
|
180
|
+
const existing = (resolvedAccounts[accountId] as Record<string, unknown>) ?? {};
|
|
181
|
+
resolvedAccounts[accountId] = {
|
|
182
|
+
...existing,
|
|
183
|
+
...updates,
|
|
184
|
+
enabled: updates.enabled ?? true,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
...next,
|
|
189
|
+
channels: {
|
|
190
|
+
...next.channels,
|
|
191
|
+
feishu: {
|
|
192
|
+
...feishu,
|
|
193
|
+
accounts: resolvedAccounts,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
200
|
+
channel,
|
|
201
|
+
dmPolicy,
|
|
202
|
+
getStatus: async ({ cfg }) => {
|
|
203
|
+
const configured = listFeishuAccountIds(cfg).some((id) => {
|
|
204
|
+
const acc = resolveFeishuAccount({ cfg, accountId: id });
|
|
205
|
+
return acc.tokenSource !== "none";
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
channel,
|
|
209
|
+
configured,
|
|
210
|
+
statusLines: [`Feishu: ${configured ? "configured" : "needs app credentials"}`],
|
|
211
|
+
selectionHint: configured ? "configured" : "requires app credentials",
|
|
212
|
+
quickstartScore: configured ? 1 : 10,
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
|
|
216
|
+
let next = cfg;
|
|
217
|
+
const override = accountOverrides.feishu?.trim();
|
|
218
|
+
const defaultId = resolveDefaultFeishuAccountId(next);
|
|
219
|
+
let accountId = override ? normalizeAccountId(override) : defaultId;
|
|
220
|
+
|
|
221
|
+
if (shouldPromptAccountIds && !override) {
|
|
222
|
+
accountId = await promptAccountId({
|
|
223
|
+
cfg: next,
|
|
224
|
+
prompter,
|
|
225
|
+
label: "Feishu",
|
|
226
|
+
currentId: accountId,
|
|
227
|
+
listAccountIds: listFeishuAccountIds,
|
|
228
|
+
defaultAccountId: defaultId,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await noteFeishuSetup(prompter);
|
|
233
|
+
|
|
234
|
+
const resolved = resolveFeishuAccount({ cfg: next, accountId });
|
|
235
|
+
const domainChoice = await prompter.select({
|
|
236
|
+
message: "Feishu domain",
|
|
237
|
+
options: [
|
|
238
|
+
{ value: "feishu", label: "Feishu (China) — open.feishu.cn" },
|
|
239
|
+
{ value: "lark", label: "Lark (global) — open.larksuite.com" },
|
|
240
|
+
],
|
|
241
|
+
initialValue: resolveDomainChoice(resolved.config.domain),
|
|
242
|
+
});
|
|
243
|
+
const domain = domainChoice === "lark" ? "lark" : "feishu";
|
|
244
|
+
|
|
245
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
246
|
+
const envAppId = process.env.FEISHU_APP_ID?.trim();
|
|
247
|
+
const envSecret = process.env.FEISHU_APP_SECRET?.trim();
|
|
248
|
+
if (isDefault && envAppId && envSecret) {
|
|
249
|
+
const useEnv = await prompter.confirm({
|
|
250
|
+
message: "FEISHU_APP_ID/FEISHU_APP_SECRET detected. Use env vars?",
|
|
251
|
+
initialValue: true,
|
|
252
|
+
});
|
|
253
|
+
if (useEnv) {
|
|
254
|
+
next = updateFeishuConfig(next, accountId, { enabled: true, domain });
|
|
255
|
+
return { cfg: next, accountId };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const appId = String(
|
|
259
|
+
await prompter.text({
|
|
260
|
+
message: "Feishu App ID (cli_...)",
|
|
261
|
+
initialValue: resolved.config.appId?.trim() || undefined,
|
|
262
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
263
|
+
}),
|
|
264
|
+
).trim();
|
|
265
|
+
|
|
266
|
+
const appSecret = String(
|
|
267
|
+
await prompter.text({
|
|
268
|
+
message: "Feishu App Secret",
|
|
269
|
+
initialValue: resolved.config.appSecret?.trim() || undefined,
|
|
270
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
271
|
+
}),
|
|
272
|
+
).trim();
|
|
273
|
+
|
|
274
|
+
next = updateFeishuConfig(next, accountId, { appId, appSecret, domain, enabled: true });
|
|
275
|
+
|
|
276
|
+
return { cfg: next, accountId };
|
|
277
|
+
},
|
|
278
|
+
};
|