import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy, ClawdbotConfig, DmPolicy, WizardPrompter, } from "openclaw/plugin-sdk"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "openclaw/plugin-sdk"; import type { FeishuConfig } from "./types.js"; import { resolveFeishuCredentials } from "./accounts.js"; import { probeFeishu } from "./probe.js"; const channel = "feishu" as const; function setFeishuDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig { const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.feishu?.allowFrom)?.map((entry) => String(entry)) : undefined; return { ...cfg, channels: { ...cfg.channels, feishu: { ...cfg.channels?.feishu, dmPolicy, ...(allowFrom ? { allowFrom } : {}), }, }, }; } function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig { return { ...cfg, channels: { ...cfg.channels, feishu: { ...cfg.channels?.feishu, allowFrom, }, }, }; } function parseAllowFromInput(raw: string): string[] { return raw .split(/[\n,;]+/g) .map((entry) => entry.trim()) .filter(Boolean); } async function promptFeishuAllowFrom(params: { cfg: ClawdbotConfig; prompter: WizardPrompter; }): Promise { const existing = params.cfg.channels?.feishu?.allowFrom ?? []; await params.prompter.note( [ "Allowlist Feishu DMs by open_id or user_id.", "You can find user open_id in Feishu admin console or via API.", "Examples:", "- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ].join("\n"), "Feishu allowlist", ); while (true) { const entry = await params.prompter.text({ message: "Feishu allowFrom (user open_ids)", placeholder: "ou_xxxxx, ou_yyyyy", initialValue: existing[0] ? String(existing[0]) : undefined, validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), }); const parts = parseAllowFromInput(String(entry)); if (parts.length === 0) { await params.prompter.note("Enter at least one user.", "Feishu allowlist"); continue; } const unique = [ ...new Set([ ...existing.map((v: string | number) => String(v).trim()).filter(Boolean), ...parts, ]), ]; return setFeishuAllowFrom(params.cfg, unique); } } async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise { await prompter.note( [ "1) Go to Feishu Open Platform (open.feishu.cn)", "2) Create a self-built app", "3) Get App ID and App Secret from Credentials page", "4) Enable required permissions: im:message, im:chat, contact:user.base:readonly", "5) Publish the app or add it to a test group", "Tip: you can also set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.", `Docs: ${formatDocsLink("/channels/feishu", "feishu")}`, ].join("\n"), "Feishu credentials", ); } function setFeishuGroupPolicy( cfg: ClawdbotConfig, groupPolicy: "open" | "allowlist" | "disabled", ): ClawdbotConfig { return { ...cfg, channels: { ...cfg.channels, feishu: { ...cfg.channels?.feishu, enabled: true, groupPolicy, }, }, }; } function setFeishuGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig { return { ...cfg, channels: { ...cfg.channels, feishu: { ...cfg.channels?.feishu, groupAllowFrom, }, }, }; } const dmPolicy: ChannelOnboardingDmPolicy = { label: "Feishu", channel, policyKey: "channels.feishu.dmPolicy", allowFromKey: "channels.feishu.allowFrom", getCurrent: (cfg) => (cfg.channels?.feishu as FeishuConfig | undefined)?.dmPolicy ?? "pairing", setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy), promptAllowFrom: promptFeishuAllowFrom, }; export const feishuOnboardingAdapter: ChannelOnboardingAdapter = { channel, getStatus: async ({ cfg }) => { const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined; const configured = Boolean(resolveFeishuCredentials(feishuCfg)); // Try to probe if configured let probeResult = null; if (configured && feishuCfg) { try { probeResult = await probeFeishu(feishuCfg); } catch { // Ignore probe errors } } const statusLines: string[] = []; if (!configured) { statusLines.push("Feishu: needs app credentials"); } else if (probeResult?.ok) { statusLines.push( `Feishu: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`, ); } else { statusLines.push("Feishu: configured (connection not verified)"); } return { channel, configured, statusLines, selectionHint: configured ? "configured" : "needs app creds", quickstartScore: configured ? 2 : 0, }; }, configure: async ({ cfg, prompter }) => { const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined; const resolved = resolveFeishuCredentials(feishuCfg); const hasConfigCreds = Boolean(feishuCfg?.appId?.trim() && feishuCfg?.appSecret?.trim()); const canUseEnv = Boolean( !hasConfigCreds && process.env.FEISHU_APP_ID?.trim() && process.env.FEISHU_APP_SECRET?.trim(), ); let next = cfg; let appId: string | null = null; let appSecret: string | null = null; if (!resolved) { await noteFeishuCredentialHelp(prompter); } if (canUseEnv) { const keepEnv = await prompter.confirm({ message: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?", initialValue: true, }); if (keepEnv) { next = { ...next, channels: { ...next.channels, feishu: { ...next.channels?.feishu, enabled: true }, }, }; } else { appId = String( await prompter.text({ message: "Enter Feishu App ID", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); appSecret = String( await prompter.text({ message: "Enter Feishu App Secret", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); } } else if (hasConfigCreds) { const keep = await prompter.confirm({ message: "Feishu credentials already configured. Keep them?", initialValue: true, }); if (!keep) { appId = String( await prompter.text({ message: "Enter Feishu App ID", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); appSecret = String( await prompter.text({ message: "Enter Feishu App Secret", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); } } else { appId = String( await prompter.text({ message: "Enter Feishu App ID", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); appSecret = String( await prompter.text({ message: "Enter Feishu App Secret", validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); } if (appId && appSecret) { next = { ...next, channels: { ...next.channels, feishu: { ...next.channels?.feishu, enabled: true, appId, appSecret, }, }, }; // Test connection const testCfg = next.channels?.feishu as FeishuConfig; try { const probe = await probeFeishu(testCfg); if (probe.ok) { await prompter.note( `Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`, "Feishu connection test", ); } else { await prompter.note( `Connection failed: ${probe.error ?? "unknown error"}`, "Feishu connection test", ); } } catch (err) { await prompter.note(`Connection test failed: ${String(err)}`, "Feishu connection test"); } } // Domain selection const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu"; const domain = await prompter.select({ message: "Which Feishu domain?", options: [ { value: "feishu", label: "Feishu (feishu.cn) - China" }, { value: "lark", label: "Lark (larksuite.com) - International" }, ], initialValue: currentDomain, }); if (domain) { next = { ...next, channels: { ...next.channels, feishu: { ...next.channels?.feishu, domain: domain as "feishu" | "lark", }, }, }; } // Group policy const groupPolicy = await prompter.select({ message: "Group chat policy", options: [ { value: "allowlist", label: "Allowlist - only respond in specific groups" }, { value: "open", label: "Open - respond in all groups (requires mention)" }, { value: "disabled", label: "Disabled - don't respond in groups" }, ], initialValue: (next.channels?.feishu as FeishuConfig | undefined)?.groupPolicy ?? "allowlist", }); if (groupPolicy) { next = setFeishuGroupPolicy(next, groupPolicy as "open" | "allowlist" | "disabled"); } // Group allowlist if needed if (groupPolicy === "allowlist") { const existing = (next.channels?.feishu as FeishuConfig | undefined)?.groupAllowFrom ?? []; const entry = await prompter.text({ message: "Group chat allowlist (chat_ids)", placeholder: "oc_xxxxx, oc_yyyyy", initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined, }); if (entry) { const parts = parseAllowFromInput(String(entry)); if (parts.length > 0) { next = setFeishuGroupAllowFrom(next, parts); } } } return { cfg: next, accountId: DEFAULT_ACCOUNT_ID }; }, dmPolicy, disable: (cfg) => ({ ...cfg, channels: { ...cfg.channels, feishu: { ...cfg.channels?.feishu, enabled: false }, }, }), };