@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 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;
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "feishu",
3
+ "channels": ["feishu"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {}
8
+ }
9
+ }
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
+ };