UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

277 lines (270 loc) 9.3 kB
import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, feishuOutbound, formatPairingApproveHint, listFeishuAccountIds, monitorFeishuProvider, normalizeFeishuTarget, PAIRING_APPROVED_MESSAGE, probeFeishu, resolveDefaultFeishuAccountId, resolveFeishuAccount, resolveFeishuConfig, resolveFeishuGroupRequireMention, setAccountEnabledInConfigSection, type ChannelAccountSnapshot, type ChannelPlugin, type ChannelStatusIssue, type ResolvedFeishuAccount, } from "openclaw/plugin-sdk"; import { FeishuConfigSchema } from "./config-schema.js"; import { feishuOnboardingAdapter } from "./onboarding.js"; const meta = { id: "feishu", label: "Feishu", selectionLabel: "Feishu (Lark Open Platform)", detailLabel: "Feishu Bot", docsPath: "/channels/feishu", docsLabel: "feishu", blurb: "Feishu/Lark bot via WebSocket.", aliases: ["lark"], order: 35, quickstartAllowFrom: true, }; const normalizeAllowEntry = (entry: string) => entry.replace(/^(feishu|lark):/i, "").trim(); export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { id: "feishu", meta, onboarding: feishuOnboardingAdapter, pairing: { idLabel: "feishuOpenId", normalizeAllowEntry: normalizeAllowEntry, notifyApproval: async ({ cfg, id }) => { const account = resolveFeishuAccount({ cfg }); if (!account.config.appId || !account.config.appSecret) { throw new Error("Feishu app credentials not configured"); } await feishuOutbound.sendText({ cfg, to: id, text: PAIRING_APPROVED_MESSAGE }); }, }, capabilities: { chatTypes: ["direct", "group"], media: true, reactions: false, threads: false, polls: false, nativeCommands: false, blockStreaming: true, }, reload: { configPrefixes: ["channels.feishu"] }, outbound: feishuOutbound, messaging: { normalizeTarget: normalizeFeishuTarget, targetResolver: { looksLikeId: (raw, normalized) => { const value = (normalized ?? raw).trim(); if (!value) { return false; } return /^o[cun]_[a-zA-Z0-9]+$/.test(value) || /^(user|group|chat):/i.test(value); }, hint: "<open_id|union_id|chat_id>", }, }, configSchema: buildChannelConfigSchema(FeishuConfigSchema), config: { listAccountIds: (cfg) => listFeishuAccountIds(cfg), resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }), defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg), setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({ cfg, sectionKey: "feishu", accountId, enabled, allowTopLevel: true, }), deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({ cfg, sectionKey: "feishu", accountId, clearBaseFields: ["appId", "appSecret", "appSecretFile", "name", "botName"], }), isConfigured: (account) => account.tokenSource !== "none", describeAccount: (account): ChannelAccountSnapshot => ({ accountId: account.accountId, name: account.name, enabled: account.enabled, configured: account.tokenSource !== "none", tokenSource: account.tokenSource, }), resolveAllowFrom: ({ cfg, accountId }) => resolveFeishuConfig({ cfg, accountId: accountId ?? undefined }).allowFrom.map((entry) => String(entry), ), formatAllowFrom: ({ allowFrom }) => allowFrom .map((entry) => String(entry).trim()) .filter(Boolean) .map((entry) => (entry === "*" ? entry : normalizeAllowEntry(entry))) .map((entry) => (entry === "*" ? entry : entry.toLowerCase())), }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; const useAccountPath = Boolean(cfg.channels?.feishu?.accounts?.[resolvedAccountId]); const basePath = useAccountPath ? `channels.feishu.accounts.${resolvedAccountId}.` : "channels.feishu."; return { policy: account.config.dmPolicy ?? "pairing", allowFrom: account.config.allowFrom ?? [], policyPath: `${basePath}dmPolicy`, allowFromPath: basePath, approveHint: formatPairingApproveHint("feishu"), normalizeEntry: normalizeAllowEntry, }; }, }, groups: { resolveRequireMention: ({ cfg, accountId, groupId }) => { if (!groupId) { return true; } return resolveFeishuGroupRequireMention({ cfg, accountId: accountId ?? undefined, chatId: groupId, }); }, }, directory: { self: async () => null, listPeers: async ({ cfg, accountId, query, limit }) => { const resolved = resolveFeishuConfig({ cfg, accountId: accountId ?? undefined }); const normalizedQuery = query?.trim().toLowerCase() ?? ""; const peers = resolved.allowFrom .map((entry) => String(entry).trim()) .filter((entry) => Boolean(entry) && entry !== "*") .map((entry) => normalizeAllowEntry(entry)) .filter((entry) => (normalizedQuery ? entry.toLowerCase().includes(normalizedQuery) : true)) .slice(0, limit && limit > 0 ? limit : undefined) .map((id) => ({ kind: "user", id }) as const); return peers; }, listGroups: async ({ cfg, accountId, query, limit }) => { const resolved = resolveFeishuConfig({ cfg, accountId: accountId ?? undefined }); const normalizedQuery = query?.trim().toLowerCase() ?? ""; const groups = Object.keys(resolved.groups ?? {}) .filter((id) => (normalizedQuery ? id.toLowerCase().includes(normalizedQuery) : true)) .slice(0, limit && limit > 0 ? limit : undefined) .map((id) => ({ kind: "group", id }) as const); return groups; }, }, status: { defaultRuntime: { accountId: DEFAULT_ACCOUNT_ID, running: false, lastStartAt: null, lastStopAt: null, lastError: null, }, collectStatusIssues: (accounts) => { const issues: ChannelStatusIssue[] = []; for (const account of accounts) { if (!account.configured) { issues.push({ channel: "feishu", accountId: account.accountId ?? DEFAULT_ACCOUNT_ID, kind: "config", message: "Feishu app ID/secret not configured", }); } } return issues; }, buildChannelSummary: async ({ snapshot }) => ({ configured: snapshot.configured ?? false, tokenSource: snapshot.tokenSource ?? "none", running: snapshot.running ?? false, lastStartAt: snapshot.lastStartAt ?? null, lastStopAt: snapshot.lastStopAt ?? null, lastError: snapshot.lastError ?? null, probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null, }), probeAccount: async ({ account, timeoutMs }) => probeFeishu(account.config.appId, account.config.appSecret, timeoutMs, account.config.domain), buildAccountSnapshot: ({ account, runtime, probe }) => { const configured = account.tokenSource !== "none"; return { accountId: account.accountId, name: account.name, enabled: account.enabled, configured, tokenSource: account.tokenSource, running: runtime?.running ?? false, lastStartAt: runtime?.lastStartAt ?? null, lastStopAt: runtime?.lastStopAt ?? null, lastError: runtime?.lastError ?? null, probe, lastInboundAt: runtime?.lastInboundAt ?? null, lastOutboundAt: runtime?.lastOutboundAt ?? null, }; }, logSelfId: ({ account, runtime }) => { const appId = account.config.appId; if (appId) { runtime.log?.(`feishu:${appId}`); } }, }, gateway: { startAccount: async (ctx) => { const { account, log, setStatus, abortSignal, cfg, runtime } = ctx; const { appId, appSecret, domain } = account.config; if (!appId || !appSecret) { throw new Error("Feishu app ID/secret not configured"); } let feishuBotLabel = ""; try { const probe = await probeFeishu(appId, appSecret, 5000, domain); if (probe.ok && probe.bot?.appName) { feishuBotLabel = ` (${probe.bot.appName})`; } if (probe.ok && probe.bot) { setStatus({ accountId: account.accountId, bot: probe.bot }); } } catch (err) { log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`); } log?.info(`[${account.accountId}] starting Feishu provider${feishuBotLabel}`); setStatus({ accountId: account.accountId, running: true, lastStartAt: Date.now(), }); try { await monitorFeishuProvider({ appId, appSecret, accountId: account.accountId, config: cfg, runtime, abortSignal, }); } catch (err) { setStatus({ accountId: account.accountId, running: false, lastError: err instanceof Error ? err.message : String(err), }); throw err; } }, }, };