mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-09 05:19:32 +08:00
Channels: add Feishu/Lark support
This commit is contained in:
33
extensions/feishu/package.json
Normal file
33
extensions/feishu/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.2.2",
|
||||
"description": "OpenClaw Feishu channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"openclaw": "workspace:*"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"channel": {
|
||||
"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
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/feishu",
|
||||
"localPath": "extensions/feishu",
|
||||
"defaultChoice": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
276
extensions/feishu/src/channel.ts
Normal file
276
extensions/feishu/src/channel.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
46
extensions/feishu/src/config-schema.ts
Normal file
46
extensions/feishu/src/config-schema.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
const toolsBySenderSchema = z.record(z.string(), ToolPolicySchema).optional();
|
||||
|
||||
const FeishuGroupSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
allowFrom: z.array(allowFromEntry).optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: toolsBySenderSchema,
|
||||
systemPrompt: z.string().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const FeishuAccountSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
appId: z.string().optional(),
|
||||
appSecret: z.string().optional(),
|
||||
appSecretFile: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
botName: z.string().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
|
||||
groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional(),
|
||||
allowFrom: z.array(allowFromEntry).optional(),
|
||||
groupAllowFrom: z.array(allowFromEntry).optional(),
|
||||
historyLimit: z.number().optional(),
|
||||
dmHistoryLimit: z.number().optional(),
|
||||
textChunkLimit: z.number().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
streaming: z.boolean().optional(),
|
||||
mediaMaxMb: z.number().optional(),
|
||||
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const FeishuConfigSchema = FeishuAccountSchema.extend({
|
||||
accounts: z.object({}).catchall(FeishuAccountSchema).optional(),
|
||||
});
|
||||
278
extensions/feishu/src/onboarding.ts
Normal file
278
extensions/feishu/src/onboarding.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import type {
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
DmPolicy,
|
||||
OpenClawConfig,
|
||||
WizardPrompter,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
normalizeAccountId,
|
||||
promptAccountId,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import {
|
||||
listFeishuAccountIds,
|
||||
resolveDefaultFeishuAccountId,
|
||||
resolveFeishuAccount,
|
||||
} from "openclaw/plugin-sdk";
|
||||
|
||||
const channel = "feishu" as const;
|
||||
|
||||
function setFeishuDmPolicy(cfg: OpenClawConfig, policy: DmPolicy): OpenClawConfig {
|
||||
const allowFrom =
|
||||
policy === "open" ? addWildcardAllowFrom(cfg.channels?.feishu?.allowFrom) : undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...cfg.channels?.feishu,
|
||||
enabled: true,
|
||||
dmPolicy: policy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function noteFeishuSetup(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
"Create a Feishu/Lark app and enable Bot + Event Subscription (WebSocket).",
|
||||
"Copy the App ID and App Secret from the app credentials page.",
|
||||
'Lark (global): use open.larksuite.com and set domain="lark".',
|
||||
`Docs: ${formatDocsLink("/channels/feishu", "channels/feishu")}`,
|
||||
].join("\n"),
|
||||
"Feishu setup",
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAllowEntry(entry: string): string {
|
||||
return entry.replace(/^(feishu|lark):/i, "").trim();
|
||||
}
|
||||
|
||||
function resolveDomainChoice(domain?: string | null): "feishu" | "lark" {
|
||||
const normalized = String(domain ?? "").toLowerCase();
|
||||
if (normalized.includes("lark") || normalized.includes("larksuite")) {
|
||||
return "lark";
|
||||
}
|
||||
return "feishu";
|
||||
}
|
||||
|
||||
async function promptFeishuAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string | null;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const { cfg, prompter } = params;
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const existingAllowFrom = isDefault
|
||||
? (cfg.channels?.feishu?.allowFrom ?? [])
|
||||
: (cfg.channels?.feishu?.accounts?.[accountId]?.allowFrom ?? []);
|
||||
|
||||
const entry = await prompter.text({
|
||||
message: "Feishu allowFrom (open_id or union_id)",
|
||||
placeholder: "ou_xxx",
|
||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const entries = raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((item) => normalizeAllowEntry(item))
|
||||
.filter(Boolean);
|
||||
const invalid = entries.filter((item) => item !== "*" && !/^o[un]_[a-zA-Z0-9]+$/.test(item));
|
||||
if (invalid.length > 0) {
|
||||
return `Invalid Feishu ids: ${invalid.join(", ")}`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const parsed = String(entry)
|
||||
.split(/[\n,;]+/g)
|
||||
.map((item) => normalizeAllowEntry(item))
|
||||
.filter(Boolean);
|
||||
const merged = [
|
||||
...existingAllowFrom.map((item) => normalizeAllowEntry(String(item))),
|
||||
...parsed,
|
||||
].filter(Boolean);
|
||||
const unique = Array.from(new Set(merged));
|
||||
|
||||
if (isDefault) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...cfg.channels?.feishu,
|
||||
enabled: true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...cfg.channels?.feishu,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...cfg.channels?.feishu?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.channels?.feishu?.accounts?.[accountId],
|
||||
enabled: cfg.channels?.feishu?.accounts?.[accountId]?.enabled ?? true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Feishu",
|
||||
channel,
|
||||
policyKey: "channels.feishu.dmPolicy",
|
||||
allowFromKey: "channels.feishu.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.feishu?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptFeishuAllowFrom,
|
||||
};
|
||||
|
||||
function updateFeishuConfig(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
updates: { appId?: string; appSecret?: string; domain?: string; enabled?: boolean },
|
||||
): OpenClawConfig {
|
||||
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const next = { ...cfg } as OpenClawConfig;
|
||||
const feishu = { ...next.channels?.feishu } as Record<string, unknown>;
|
||||
const accounts = feishu.accounts
|
||||
? { ...(feishu.accounts as Record<string, unknown>) }
|
||||
: undefined;
|
||||
|
||||
if (isDefault && !accounts) {
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: {
|
||||
...feishu,
|
||||
...updates,
|
||||
enabled: updates.enabled ?? true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedAccounts = accounts ?? {};
|
||||
const existing = (resolvedAccounts[accountId] as Record<string, unknown>) ?? {};
|
||||
resolvedAccounts[accountId] = {
|
||||
...existing,
|
||||
...updates,
|
||||
enabled: updates.enabled ?? true,
|
||||
};
|
||||
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: {
|
||||
...feishu,
|
||||
accounts: resolvedAccounts,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
dmPolicy,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = listFeishuAccountIds(cfg).some((id) => {
|
||||
const acc = resolveFeishuAccount({ cfg, accountId: id });
|
||||
return acc.tokenSource !== "none";
|
||||
});
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`Feishu: ${configured ? "configured" : "needs app credentials"}`],
|
||||
selectionHint: configured ? "configured" : "requires app credentials",
|
||||
quickstartScore: configured ? 1 : 10,
|
||||
};
|
||||
},
|
||||
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
|
||||
let next = cfg;
|
||||
const override = accountOverrides.feishu?.trim();
|
||||
const defaultId = resolveDefaultFeishuAccountId(next);
|
||||
let accountId = override ? normalizeAccountId(override) : defaultId;
|
||||
|
||||
if (shouldPromptAccountIds && !override) {
|
||||
accountId = await promptAccountId({
|
||||
cfg: next,
|
||||
prompter,
|
||||
label: "Feishu",
|
||||
currentId: accountId,
|
||||
listAccountIds: listFeishuAccountIds,
|
||||
defaultAccountId: defaultId,
|
||||
});
|
||||
}
|
||||
|
||||
await noteFeishuSetup(prompter);
|
||||
|
||||
const resolved = resolveFeishuAccount({ cfg: next, accountId });
|
||||
const domainChoice = await prompter.select({
|
||||
message: "Feishu domain",
|
||||
options: [
|
||||
{ value: "feishu", label: "Feishu (China) — open.feishu.cn" },
|
||||
{ value: "lark", label: "Lark (global) — open.larksuite.com" },
|
||||
],
|
||||
initialValue: resolveDomainChoice(resolved.config.domain),
|
||||
});
|
||||
const domain = domainChoice === "lark" ? "lark" : "feishu";
|
||||
|
||||
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const envAppId = process.env.FEISHU_APP_ID?.trim();
|
||||
const envSecret = process.env.FEISHU_APP_SECRET?.trim();
|
||||
if (isDefault && envAppId && envSecret) {
|
||||
const useEnv = await prompter.confirm({
|
||||
message: "FEISHU_APP_ID/FEISHU_APP_SECRET detected. Use env vars?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (useEnv) {
|
||||
next = updateFeishuConfig(next, accountId, { enabled: true, domain });
|
||||
return { cfg: next, accountId };
|
||||
}
|
||||
}
|
||||
const appId = String(
|
||||
await prompter.text({
|
||||
message: "Feishu App ID (cli_...)",
|
||||
initialValue: resolved.config.appId?.trim() || undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const appSecret = String(
|
||||
await prompter.text({
|
||||
message: "Feishu App Secret",
|
||||
initialValue: resolved.config.appSecret?.trim() || undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
|
||||
next = updateFeishuConfig(next, accountId, { appId, appSecret, domain, enabled: true });
|
||||
|
||||
return { cfg: next, accountId };
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user