openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
2,316 lines • 86 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js";
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { t as formatDocsLink } from "./links-CsLBrRff.js";
import { s as hasConfiguredSecretInput } from "./types.secrets-_0JOMGE5.js";
import { n as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js";
import { u as normalizeAgentId } from "./session-key-B_NoIfpX.js";
import { At as boolean, Et as array, Nn as record, Rn as string, Tn as object, Xn as union, Zn as unknown, dn as literal, wn as number, yt as _enum } from "./schemas-6cH6bZ7o.js";
import { n as ZodIssueCode } from "./compat-B-THjfQ8.js";
import { r as buildChannelConfigSchema } from "./config-schema-CO2ikh7C.js";
import { i as resolveSessionFilePath, u as resolveStorePath } from "./paths-NEwU8m3X.js";
import { d as updateSessionStore } from "./store-Qsgtu-0y.js";
import { n as getSessionBindingService } from "./session-binding-service-cfUL2BWM.js";
import { l as normalizeMessagePresentation } from "./payload-CZlThETZ.js";
import { i as createLazyRuntimeNamedExport } from "./lazy-runtime-D-7_JraP.js";
import { i as createActionGate } from "./common-C4yy9V-D.js";
import { i as createHybridChannelConfigAdapter, t as adaptScopedAccountAccessor } from "./channel-config-helpers-lj5quus3.js";
import "./number-runtime-DBLVDypr.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { t as chunkTextForOutbound } from "./text-chunking-D45TeeEs.js";
import { n as describeAccountSnapshot } from "./account-helpers-pcH3lGFY.js";
import { i as createChatChannelPlugin, t as buildChannelOutboundSessionRoute, u as stripChannelTargetPrefix } from "./core-DSxVv-v1.js";
import "./channel-core-IukkNCd-.js";
import "./routing-CQ63ICPX.js";
import { r as buildSecretInputSchema } from "./secret-input-DVCzFGxN.js";
import { t as formatAllowFromLowercase } from "./allow-from-DCaV3hzI.js";
import { E as projectConfigAccountIdWarningCollector, g as createAllowlistProviderGroupPolicyWarningCollector } from "./channel-policy-CIW58SA5.js";
import { t as createChannelDirectoryAdapter } from "./directory-runtime-om8jJAZt.js";
import { n as createRuntimeOutboundDelegates, t as createRuntimeDirectoryLiveAdapter } from "./runtime-forwarders-Dxil5z45.js";
import { t as createSetupTranslator } from "./i18n-7g_f4oaD.js";
import { D as patchTopLevelChannelConfigSection, P as promptSingleChannelSecretInput, Q as splitSetupEntries, v as mergeAllowFromEntries } from "./setup-wizard-helpers-Dv-t1nb8.js";
import "./setup-DHfdBmUU.js";
import { t as resolveApprovalApprovers } from "./approval-approvers-CM-4zRvR.js";
import { t as createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers-DU3OxkhL.js";
import { T as defineChannelMessageAdapter } from "./channel-outbound-B3_Zy-kG.js";
import "./conversation-runtime-VjM1-D7a.js";
import "./account-resolution-DBB0e3KP.js";
import { t as PAIRING_APPROVED_MESSAGE } from "./pairing-message-DNhqI-OE.js";
import { d as createDefaultChannelRuntimeState, i as buildProbeChannelStatusSummary, u as createComputedAccountStatusAdapter } from "./status-helpers-BevxX6Pu.js";
import "./channel-status-DxlXr0Xh.js";
import "./channel-config-primitives-Bk3iiMir.js";
import "./channel-actions-BOfvC-Gl.js";
import { i as createPairingPrefixStripper } from "./channel-pairing-Bk6_JhTm.js";
import { t as loadSessionStore } from "./session-store-runtime-rvUx2Fil.js";
import "./state-paths-DzVdErem.js";
import { a as resolveDefaultFeishuAccountId, i as listFeishuAccountIds, l as resolveFeishuRuntimeAccount, n as inspectFeishuCredentials, p as isRecord$1, r as listEnabledFeishuAccounts, s as resolveFeishuAccount } from "./accounts-BTmGOBDX.js";
import { n as looksLikeFeishuId, r as normalizeFeishuTarget } from "./targets-DGmM3dk9.js";
import { n as createFeishuSendReceipt } from "./send-result-DLMTzWt7.js";
import { a as parseFeishuTargetId, i as parseFeishuDirectConversationId, n as buildFeishuModelOverrideParentCandidates, r as parseFeishuConversationId, t as buildFeishuConversationId } from "./conversation-id-CgQX90Pg.js";
import { i as listFeishuDirectoryPeers, r as listFeishuDirectoryGroups, t as buildFeishuPresentationCard } from "./presentation-card-BX_vye1k.js";
import { t as messageActionTargetAliases } from "./security-audit-BPf2zHl8.js";
import { s as resolveFeishuGroupToolPolicy } from "./policy-CssV62WE.js";
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-CnUQ317L.js";
import { t as collectFeishuSecurityAuditFindings } from "./security-audit-shared-ChAGGcdN.js";
import { t as resolveFeishuSessionConversation } from "./session-conversation-Cxl6gO78.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
//#region extensions/feishu/src/approval-auth.ts
function normalizeFeishuApproverId(value) {
const trimmed = normalizeOptionalLowercaseString(normalizeFeishuTarget(String(value)));
return trimmed?.startsWith("ou_") ? trimmed : void 0;
}
const feishuApprovalAuth = createResolvedApproverActionAuthAdapter({
channelLabel: "Feishu",
resolveApprovers: ({ cfg, accountId }) => {
const account = resolveFeishuAccount({
cfg,
accountId
}).config;
return resolveApprovalApprovers({
allowFrom: account.allowFrom,
normalizeApprover: normalizeFeishuApproverId
});
},
normalizeSenderId: (value) => normalizeFeishuApproverId(value)
});
//#endregion
//#region extensions/feishu/src/config-schema.ts
const ChannelActionsSchema = object({ reactions: boolean().optional() }).strict().optional();
const DmPolicySchema = _enum([
"open",
"pairing",
"allowlist"
]);
const GroupPolicySchema = union([_enum([
"open",
"allowlist",
"disabled"
]), literal("allowall").transform(() => "open")]);
const FeishuDomainSchema = union([_enum(["feishu", "lark"]), string().url().startsWith("https://")]);
const FeishuConnectionModeSchema = _enum(["websocket", "webhook"]);
const TtsOverrideSchema = object({
auto: _enum([
"off",
"always",
"inbound",
"tagged"
]).optional(),
enabled: boolean().optional(),
mode: _enum(["final", "all"]).optional(),
provider: string().optional(),
persona: string().optional(),
personas: record(string(), record(string(), unknown())).optional(),
summaryModel: string().optional(),
modelOverrides: record(string(), unknown()).optional(),
providers: record(string(), record(string(), unknown())).optional(),
prefsPath: string().optional(),
maxTextLength: number().int().min(1).optional(),
timeoutMs: number().int().min(1e3).max(12e4).optional()
}).strict().optional();
const ToolPolicySchema = object({
allow: array(string()).optional(),
deny: array(string()).optional()
}).strict().optional();
const DmConfigSchema = object({
enabled: boolean().optional(),
systemPrompt: string().optional()
}).strict().optional();
const MarkdownConfigSchema = object({
mode: _enum([
"native",
"escape",
"strip"
]).optional(),
tableMode: _enum([
"native",
"ascii",
"simple"
]).optional()
}).strict().optional();
const RenderModeSchema = _enum([
"auto",
"raw",
"card"
]).optional();
const StreamingModeSchema = boolean().optional();
const BlockStreamingSchema = boolean().optional();
const BlockStreamingCoalesceSchema = object({
enabled: boolean().optional(),
minDelayMs: number().int().positive().optional(),
maxDelayMs: number().int().positive().optional()
}).strict().optional();
const ChannelHeartbeatVisibilitySchema = object({
visibility: _enum(["visible", "hidden"]).optional(),
intervalMs: number().int().positive().optional()
}).strict().optional();
/**
* Dynamic agent creation configuration.
* When enabled, a new agent is created for each unique DM user.
*/
const DynamicAgentCreationSchema = object({
enabled: boolean().optional(),
workspaceTemplate: string().optional(),
agentDirTemplate: string().optional(),
maxAgents: number().int().positive().optional()
}).strict().optional();
/**
* Feishu tools configuration.
* Controls which tool categories are enabled.
*
* Dependencies:
* - wiki requires doc (wiki content is edited via doc tools)
* - perm can work independently but is typically used with drive
*/
const FeishuToolsConfigSchema = object({
doc: boolean().optional(),
chat: boolean().optional(),
wiki: boolean().optional(),
drive: boolean().optional(),
perm: boolean().optional(),
scopes: boolean().optional(),
bitable: boolean().optional(),
base: boolean().optional()
}).strict().optional();
/**
* Group session scope for routing Feishu group messages.
* - "group" (default): one session per group chat
* - "group_sender": one session per (group + sender)
* - "group_topic": one session per group topic thread (falls back to group if no topic)
* - "group_topic_sender": one session per (group + topic thread + sender),
* falls back to (group + sender) if no topic
*/
const GroupSessionScopeSchema = _enum([
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]).optional();
/**
* @deprecated Use groupSessionScope instead.
*
* Topic session isolation mode for group chats.
* - "disabled" (default): All messages in a group share one session
* - "enabled": Messages in different topics get separate sessions
*
* Topic routing uses Feishu topic-group `thread_id` when the event identifies a
* native topic group, and keeps `root_id` precedence for normal groups so
* reply-created threads stay on the initiating message session.
*/
const TopicSessionModeSchema = _enum(["disabled", "enabled"]).optional();
const ReactionNotificationModeSchema = _enum([
"off",
"own",
"all"
]).optional();
/**
* Reply-in-thread mode for group chats.
* - "disabled" (default): Bot replies are normal inline replies
* - "enabled": Bot replies create or continue a Feishu topic thread
*
* When enabled, the Feishu reply API is called with `reply_in_thread: true`,
* causing the reply to appear as a topic (话题) under the original message.
*/
const ReplyInThreadSchema = _enum(["disabled", "enabled"]).optional();
const FeishuGroupSchema = object({
requireMention: boolean().optional(),
tools: ToolPolicySchema,
skills: array(string()).optional(),
enabled: boolean().optional(),
allowFrom: array(union([string(), number()])).optional(),
systemPrompt: string().optional(),
groupSessionScope: GroupSessionScopeSchema,
topicSessionMode: TopicSessionModeSchema,
replyInThread: ReplyInThreadSchema
}).strict();
const FeishuSharedConfigShape = {
webhookHost: string().optional(),
webhookPort: number().int().positive().optional(),
capabilities: array(string()).optional(),
markdown: MarkdownConfigSchema,
configWrites: boolean().optional(),
dmPolicy: DmPolicySchema.optional(),
allowFrom: array(union([string(), number()])).optional(),
groupPolicy: GroupPolicySchema.optional(),
groupAllowFrom: array(union([string(), number()])).optional(),
groupSenderAllowFrom: array(union([string(), number()])).optional(),
requireMention: boolean().optional(),
groups: record(string(), FeishuGroupSchema.optional()).optional(),
historyLimit: number().int().min(0).optional(),
dmHistoryLimit: number().int().min(0).optional(),
dms: record(string(), DmConfigSchema).optional(),
textChunkLimit: number().int().positive().optional(),
chunkMode: _enum(["length", "newline"]).optional(),
blockStreaming: BlockStreamingSchema,
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
mediaMaxMb: number().positive().optional(),
httpTimeoutMs: number().int().positive().max(3e5).optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
renderMode: RenderModeSchema,
streaming: StreamingModeSchema,
tools: FeishuToolsConfigSchema,
actions: ChannelActionsSchema,
replyInThread: ReplyInThreadSchema,
reactionNotifications: ReactionNotificationModeSchema,
typingIndicator: boolean().optional(),
resolveSenderNames: boolean().optional(),
tts: TtsOverrideSchema
};
/**
* Per-account configuration.
* All fields are optional - missing fields inherit from top-level config.
*/
const FeishuAccountConfigSchema = object({
enabled: boolean().optional(),
name: string().optional(),
appId: string().optional(),
appSecret: buildSecretInputSchema().optional(),
encryptKey: buildSecretInputSchema().optional(),
verificationToken: buildSecretInputSchema().optional(),
domain: FeishuDomainSchema.optional(),
connectionMode: FeishuConnectionModeSchema.optional(),
webhookPath: string().optional(),
...FeishuSharedConfigShape,
groupSessionScope: GroupSessionScopeSchema,
topicSessionMode: TopicSessionModeSchema
}).strict();
const FeishuConfigSchema = object({
enabled: boolean().optional(),
defaultAccount: string().optional(),
appId: string().optional(),
appSecret: buildSecretInputSchema().optional(),
encryptKey: buildSecretInputSchema().optional(),
verificationToken: buildSecretInputSchema().optional(),
domain: FeishuDomainSchema.optional().default("feishu"),
connectionMode: FeishuConnectionModeSchema.optional().default("websocket"),
webhookPath: string().optional().default("/feishu/events"),
...FeishuSharedConfigShape,
dmPolicy: DmPolicySchema.optional().default("pairing"),
reactionNotifications: ReactionNotificationModeSchema.optional().default("own"),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
requireMention: boolean().optional(),
groupSessionScope: GroupSessionScopeSchema,
topicSessionMode: TopicSessionModeSchema,
dynamicAgentCreation: DynamicAgentCreationSchema,
typingIndicator: boolean().optional().default(true),
resolveSenderNames: boolean().optional().default(true),
accounts: record(string(), FeishuAccountConfigSchema.optional()).optional()
}).strict().superRefine((value, ctx) => {
const defaultAccount = value.defaultAccount?.trim();
if (defaultAccount && value.accounts && Object.keys(value.accounts).length > 0) {
const normalizedDefaultAccount = normalizeAccountId(defaultAccount);
if (!Object.hasOwn(value.accounts, normalizedDefaultAccount)) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["defaultAccount"],
message: `channels.feishu.defaultAccount="${defaultAccount}" does not match a configured account key`
});
}
const defaultConnectionMode = value.connectionMode ?? "websocket";
const defaultVerificationTokenConfigured = hasConfiguredSecretInput(value.verificationToken);
const defaultEncryptKeyConfigured = hasConfiguredSecretInput(value.encryptKey);
if (defaultConnectionMode === "webhook") {
if (!defaultVerificationTokenConfigured) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["verificationToken"],
message: "channels.feishu.connectionMode=\"webhook\" requires channels.feishu.verificationToken"
});
if (!defaultEncryptKeyConfigured) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["encryptKey"],
message: "channels.feishu.connectionMode=\"webhook\" requires channels.feishu.encryptKey"
});
}
for (const [accountId, account] of Object.entries(value.accounts ?? {})) {
if (!account) continue;
if ((account.connectionMode ?? defaultConnectionMode) !== "webhook") continue;
const accountVerificationTokenConfigured = hasConfiguredSecretInput(account.verificationToken) || defaultVerificationTokenConfigured;
const accountEncryptKeyConfigured = hasConfiguredSecretInput(account.encryptKey) || defaultEncryptKeyConfigured;
if (!accountVerificationTokenConfigured) ctx.addIssue({
code: ZodIssueCode.custom,
path: [
"accounts",
accountId,
"verificationToken"
],
message: `channels.feishu.accounts.${accountId}.connectionMode="webhook" requires a verificationToken (account-level or top-level)`
});
if (!accountEncryptKeyConfigured) ctx.addIssue({
code: ZodIssueCode.custom,
path: [
"accounts",
accountId,
"encryptKey"
],
message: `channels.feishu.accounts.${accountId}.connectionMode="webhook" requires an encryptKey (account-level or top-level)`
});
}
if (value.dmPolicy === "open") {
if (!(value.allowFrom ?? []).some((entry) => String(entry).trim() === "*")) ctx.addIssue({
code: ZodIssueCode.custom,
path: ["allowFrom"],
message: "channels.feishu.dmPolicy=\"open\" requires channels.feishu.allowFrom to include \"*\""
});
}
});
//#endregion
//#region extensions/feishu/src/doctor.ts
const FEISHU_STATE_DIR = "feishu";
const BACKUP_PREFIX = "feishu-state-repair";
const BLANK_USER_MESSAGE_REPAIR_THRESHOLD = 3;
const SESSION_FILE_INSPECTION_MAX_BYTES = 16 * 1024 * 1024;
function timestampForPath(now = /* @__PURE__ */ new Date()) {
return now.toISOString().replaceAll(":", "-");
}
function isRecord(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function toFeishuSessionEntry(value) {
if (!isRecord(value)) return {};
return {
sessionId: value.sessionId,
sessionFile: value.sessionFile
};
}
function countLabel(count, singular, plural = `${singular}s`) {
return `${count} ${count === 1 ? singular : plural}`;
}
function existsDir(dir) {
try {
return fs.statSync(dir).isDirectory();
} catch {
return false;
}
}
function existsFile(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch {
return false;
}
}
function safeReadDir(dir) {
try {
return fs.readdirSync(dir, { withFileTypes: true });
} catch {
return [];
}
}
function isPathWithinRoot(targetPath, rootPath) {
const resolvedTarget = path.resolve(targetPath);
const resolvedRoot = path.resolve(rootPath);
const relative = path.relative(resolvedRoot, resolvedTarget);
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
}
function formatDisplayPath(filePath) {
const home = os.homedir();
const resolved = path.resolve(filePath);
return resolved === home || resolved.startsWith(`${home}${path.sep}`) ? `~${resolved.slice(home.length)}` : resolved;
}
function formatFinding(finding) {
switch (finding.kind) {
case "corrupt-state-json": return `- Feishu local JSON state is corrupt: ${formatDisplayPath(finding.path)}`;
case "missing-session-transcript": return `- Feishu session ${finding.sessionKey} points to a missing transcript in ${formatDisplayPath(finding.storePath)}`;
case "invalid-session-transcript": return `- Feishu session ${finding.sessionKey} has an invalid transcript (${finding.reason}): ${formatDisplayPath(finding.path)}`;
case "blank-user-message-run": return `- Feishu session ${finding.sessionKey} contains ${finding.count} blank user messages: ${formatDisplayPath(finding.path)}`;
}
return finding;
}
function isFeishuSessionStoreKey(key) {
const normalized = key.trim().toLowerCase();
return /^agent:[^:]+:feishu(?::|$)/.test(normalized) || /^feishu(?::|$)/.test(normalized);
}
function isFeishuAcpBindingSessionKey(key) {
return /^agent:[^:]+:acp:binding:feishu(?::|$)/.test(key.trim().toLowerCase());
}
function normalizeMetadataString(value) {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function isFeishuSessionEntry(key, value) {
if (isFeishuAcpBindingSessionKey(key)) return false;
if (isFeishuSessionStoreKey(key)) return true;
if (!isRecord(value)) return false;
if (normalizeMetadataString(value.channel) === "feishu" || normalizeMetadataString(value.lastChannel) === "feishu") return true;
if (normalizeMetadataString((isRecord(value.route) ? value.route : null)?.channel) === "feishu") return true;
if (normalizeMetadataString((isRecord(value.deliveryContext) ? value.deliveryContext : null)?.channel) === "feishu") return true;
if (normalizeMetadataString((isRecord(value.pendingFinalDeliveryContext) ? value.pendingFinalDeliveryContext : null)?.channel) === "feishu") return true;
const origin = isRecord(value.origin) ? value.origin : null;
const originProvider = normalizeMetadataString(origin?.provider);
const originSurface = normalizeMetadataString(origin?.surface);
const originFrom = normalizeMetadataString(origin?.from);
return originProvider === "feishu" || originSurface.startsWith("feishu") || originFrom.startsWith("feishu:");
}
function collectConfiguredAgentIds(cfg) {
const ids = /* @__PURE__ */ new Set();
ids.add(resolveConfiguredDefaultAgentId(cfg));
for (const agent of cfg.agents?.list ?? []) if (typeof agent.id === "string" && agent.id.trim()) ids.add(normalizeAgentId(agent.id));
return [...ids].toSorted();
}
function resolveConfiguredDefaultAgentId(cfg) {
const agents = cfg.agents?.list ?? [];
const chosen = agents.find((agent) => agent?.default) ?? agents[0];
return normalizeAgentId(typeof chosen?.id === "string" && chosen.id.trim() ? chosen.id : "main");
}
function collectFeishuSessionTargets(params) {
const byStorePath = /* @__PURE__ */ new Map();
const addTarget = (target) => {
byStorePath.set(path.resolve(target.storePath), {
...target,
storePath: path.resolve(target.storePath)
});
};
for (const agentId of collectConfiguredAgentIds(params.cfg)) addTarget({
agentId,
storePath: resolveStorePath(params.cfg.session?.store, {
agentId,
env: params.env
})
});
const agentsDir = path.join(params.stateDir, "agents");
for (const agentDir of safeReadDir(agentsDir)) {
if (!agentDir.isDirectory()) continue;
const agentId = normalizeAgentId(agentDir.name);
const storePath = path.join(agentsDir, agentDir.name, "sessions", "sessions.json");
if (existsFile(storePath)) addTarget({
agentId,
storePath
});
}
return [...byStorePath.values()].toSorted((left, right) => left.storePath.localeCompare(right.storePath));
}
function collectJsonFiles(rootDir, limit = 200) {
const files = [];
const visit = (dir) => {
if (files.length >= limit) return;
for (const entry of safeReadDir(dir).toSorted((left, right) => left.name.localeCompare(right.name))) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
visit(fullPath);
continue;
}
if (entry.isFile() && entry.name.endsWith(".json")) files.push(fullPath);
if (files.length >= limit) return;
}
};
if (existsDir(rootDir)) visit(rootDir);
return files;
}
function collectCorruptFeishuStateJsonFindings(feishuStateDir) {
const findings = [];
for (const filePath of collectJsonFiles(feishuStateDir)) try {
JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch {
findings.push({
kind: "corrupt-state-json",
path: filePath
});
}
return findings;
}
function resolveSessionTranscriptCandidates(params) {
const candidates = /* @__PURE__ */ new Set();
const sessionsDir = path.dirname(params.storePath);
const addSafeCandidate = (candidate) => {
const resolved = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(sessionsDir, candidate);
if (resolved === sessionsDir || !isPathWithinRoot(resolved, sessionsDir)) return;
candidates.add(resolved);
};
if (typeof params.entry.sessionId === "string" && /^[a-z0-9][a-z0-9._-]{0,127}$/i.test(params.entry.sessionId)) {
candidates.add(resolveSessionFilePath(params.entry.sessionId, typeof params.entry.sessionFile === "string" ? { sessionFile: params.entry.sessionFile } : void 0, {
agentId: params.agentId,
sessionsDir
}));
return [...candidates].toSorted();
}
if (typeof params.entry.sessionFile === "string" && params.entry.sessionFile.trim()) addSafeCandidate(params.entry.sessionFile.trim());
return [...candidates].toSorted();
}
function isSessionHeader(value) {
return isRecord(value) && value.type === "session" && typeof value.id === "string";
}
function isBlankUserMessage(value) {
if (!isRecord(value) || value.type !== "message" || !isRecord(value.message)) return false;
if (value.message.role !== "user") return false;
const content = value.message.content;
if (typeof content === "string") return content.trim().length === 0;
return Array.isArray(content) && content.length === 0;
}
function isUserMessage(value) {
return isRecord(value) && value.type === "message" && isRecord(value.message) && value.message.role === "user";
}
function inspectSessionTranscript(params) {
let stat;
try {
stat = fs.statSync(params.transcriptPath);
} catch {
return null;
}
if (!stat.isFile()) return {
kind: "invalid-session-transcript",
sessionKey: params.sessionKey,
storePath: params.storePath,
path: params.transcriptPath,
reason: "not a file"
};
if (stat.size > SESSION_FILE_INSPECTION_MAX_BYTES) return null;
let raw;
try {
raw = fs.readFileSync(params.transcriptPath, "utf-8");
} catch {
return {
kind: "invalid-session-transcript",
sessionKey: params.sessionKey,
storePath: params.storePath,
path: params.transcriptPath,
reason: "unreadable"
};
}
const entries = [];
let malformedLines = 0;
let blankUserMessageRun = 0;
let maxBlankUserMessageRun = 0;
for (const line of raw.split(/\r?\n/)) {
if (!line.trim()) continue;
try {
const entry = JSON.parse(line);
entries.push(entry);
if (isBlankUserMessage(entry)) {
blankUserMessageRun += 1;
maxBlankUserMessageRun = Math.max(maxBlankUserMessageRun, blankUserMessageRun);
} else if (isUserMessage(entry)) blankUserMessageRun = 0;
} catch {
malformedLines += 1;
}
}
if (entries.length === 0) return {
kind: "invalid-session-transcript",
sessionKey: params.sessionKey,
storePath: params.storePath,
path: params.transcriptPath,
reason: "empty transcript"
};
if (!isSessionHeader(entries[0])) return {
kind: "invalid-session-transcript",
sessionKey: params.sessionKey,
storePath: params.storePath,
path: params.transcriptPath,
reason: "invalid session header"
};
if (malformedLines > 0) return {
kind: "invalid-session-transcript",
sessionKey: params.sessionKey,
storePath: params.storePath,
path: params.transcriptPath,
reason: `${malformedLines} malformed JSONL line(s)`
};
if (maxBlankUserMessageRun >= BLANK_USER_MESSAGE_REPAIR_THRESHOLD) return {
kind: "blank-user-message-run",
sessionKey: params.sessionKey,
storePath: params.storePath,
path: params.transcriptPath,
count: maxBlankUserMessageRun
};
return null;
}
function collectFeishuSessionFindings(params) {
const transcriptCandidates = resolveSessionTranscriptCandidates(params);
const existing = transcriptCandidates.filter(existsFile);
if (transcriptCandidates.length > 0 && existing.length === 0) return [{
kind: "missing-session-transcript",
sessionKey: params.sessionKey,
storePath: params.storePath
}];
const findings = [];
for (const transcriptPath of existing) {
const finding = inspectSessionTranscript({
sessionKey: params.sessionKey,
storePath: params.storePath,
transcriptPath
});
if (finding) findings.push(finding);
}
return findings;
}
function hasCorruptFeishuStateJsonFinding(inspection) {
return inspection.findings.some((finding) => finding.kind === "corrupt-state-json");
}
function sessionEntryId(storePath, key) {
return `${path.resolve(storePath)}\0${key}`;
}
function collectRepairSessionEntries(inspection) {
const entriesById = /* @__PURE__ */ new Map();
for (const entry of inspection.sessionEntries) entriesById.set(sessionEntryId(entry.storePath, entry.key), entry);
const repairEntries = [];
const seen = /* @__PURE__ */ new Set();
for (const finding of inspection.findings) {
if (finding.kind === "corrupt-state-json") continue;
const id = sessionEntryId(finding.storePath, finding.sessionKey);
if (seen.has(id)) continue;
const entry = entriesById.get(id);
if (entry) {
repairEntries.push(entry);
seen.add(id);
}
}
return repairEntries.toSorted((left, right) => left.storePath.localeCompare(right.storePath) || left.key.localeCompare(right.key));
}
function inspectFeishuDoctorState(params) {
const env = params.env ?? process.env;
const stateDir = resolveStateDir(env, os.homedir);
const feishuStateDir = path.join(stateDir, FEISHU_STATE_DIR);
const findings = collectCorruptFeishuStateJsonFindings(feishuStateDir);
const sessionEntries = [];
for (const target of collectFeishuSessionTargets({
cfg: params.cfg,
env,
stateDir
})) {
const store = loadSessionStore(target.storePath, { skipCache: true });
for (const [key, entry] of Object.entries(store).toSorted(([left], [right]) => left.localeCompare(right))) {
if (!isFeishuSessionEntry(key, entry)) continue;
const sessionEntry = toFeishuSessionEntry(entry);
sessionEntries.push({
key,
storePath: target.storePath,
agentId: target.agentId,
entry: sessionEntry
});
findings.push(...collectFeishuSessionFindings({
sessionKey: key,
storePath: target.storePath,
agentId: target.agentId,
entry: sessionEntry
}));
}
}
return {
stateDir,
feishuStateDir,
findings,
sessionEntries
};
}
function ensureBackupDir(stateDir, now) {
const backupDir = path.join(stateDir, "backups", `${BACKUP_PREFIX}-${timestampForPath(now)}`);
fs.mkdirSync(backupDir, {
recursive: true,
mode: 448
});
return backupDir;
}
function resolveUniquePath(candidate) {
if (!fs.existsSync(candidate)) return candidate;
for (let index = 1; index < 1e3; index += 1) {
const next = `${candidate}.${index}`;
if (!fs.existsSync(next)) return next;
}
throw new Error(`Unable to resolve unique path for ${candidate}`);
}
function movePathToBackup(params) {
if (!fs.existsSync(params.sourcePath)) return false;
const targetPath = resolveUniquePath(path.join(params.backupDir, params.relativeTarget));
fs.mkdirSync(path.dirname(targetPath), {
recursive: true,
mode: 448
});
fs.renameSync(params.sourcePath, targetPath);
return true;
}
function copyStoreBackup(params) {
if (!existsFile(params.storePath)) return;
const targetPath = path.join(params.backupDir, "session-stores", params.agentId, path.basename(params.storePath));
fs.mkdirSync(path.dirname(targetPath), {
recursive: true,
mode: 448
});
fs.copyFileSync(params.storePath, resolveUniquePath(targetPath));
}
function collectSessionArtifactPaths(params) {
const artifacts = /* @__PURE__ */ new Set();
for (const transcriptPath of resolveSessionTranscriptCandidates(params)) {
artifacts.add(transcriptPath);
if (transcriptPath.endsWith(".jsonl")) {
const base = transcriptPath.slice(0, -6);
artifacts.add(`${base}.trajectory.jsonl`);
artifacts.add(`${base}.trajectory-path.json`);
}
}
return [...artifacts].toSorted();
}
function archiveSessionArtifacts(params) {
const seen = /* @__PURE__ */ new Set();
let archived = 0;
for (const entry of params.entries) for (const artifactPath of collectSessionArtifactPaths({
storePath: params.storePath,
agentId: entry.agentId,
entry: entry.entry
})) {
if (seen.has(artifactPath) || !existsFile(artifactPath)) continue;
seen.add(artifactPath);
const archivedPath = resolveUniquePath(`${artifactPath}.deleted.${params.archiveTimestamp}`);
fs.renameSync(artifactPath, archivedPath);
archived += 1;
}
return archived;
}
async function repairFeishuDoctorState(params) {
const env = params.env ?? process.env;
const now = params.now ?? /* @__PURE__ */ new Date();
const inspection = params.inspection ?? inspectFeishuDoctorState({
cfg: params.cfg,
env
});
const backupDir = ensureBackupDir(inspection.stateDir, now);
const archiveTimestamp = timestampForPath(now);
const warnings = [];
const stateDirRepairAttempted = hasCorruptFeishuStateJsonFinding(inspection);
let rebuiltStateDir = false;
if (stateDirRepairAttempted) try {
rebuiltStateDir = movePathToBackup({
sourcePath: inspection.feishuStateDir,
backupDir,
relativeTarget: FEISHU_STATE_DIR
});
fs.mkdirSync(inspection.feishuStateDir, {
recursive: true,
mode: 448
});
} catch (error) {
warnings.push(`- Failed to rebuild Feishu local state: ${String(error)}`);
}
const entriesByStore = /* @__PURE__ */ new Map();
for (const entry of collectRepairSessionEntries(inspection)) {
const existing = entriesByStore.get(entry.storePath);
if (existing) existing.entries.push({
key: entry.key,
entry: entry.entry
});
else entriesByStore.set(entry.storePath, {
agentId: entry.agentId,
entries: [{
key: entry.key,
entry: entry.entry
}]
});
}
let removedSessionEntries = 0;
let touchedSessionStores = 0;
let archivedSessionArtifacts = 0;
for (const [storePath, group] of [...entriesByStore.entries()].toSorted(([left], [right]) => left.localeCompare(right))) try {
copyStoreBackup({
storePath,
backupDir,
agentId: group.agentId
});
const keys = new Set(group.entries.map((entry) => entry.key));
const removedEntries = await updateSessionStore(storePath, (store) => {
const removed = [];
for (const key of keys) if (Object.hasOwn(store, key)) {
delete store[key];
const entry = group.entries.find((candidate) => candidate.key === key);
if (entry) removed.push(entry);
}
return removed;
}, { skipMaintenance: true });
const removed = removedEntries.length;
removedSessionEntries += removed;
if (removed > 0) {
touchedSessionStores += 1;
archivedSessionArtifacts += archiveSessionArtifacts({
storePath,
entries: removedEntries.map((entry) => ({
agentId: group.agentId,
entry: entry.entry
})),
archiveTimestamp
});
}
} catch (error) {
warnings.push(`- Failed to archive Feishu sessions in ${formatDisplayPath(storePath)}: ${String(error)}`);
}
return {
backupDir,
stateDirRepairAttempted,
rebuiltStateDir,
removedSessionEntries,
touchedSessionStores,
archivedSessionArtifacts,
warnings
};
}
function formatPreviewWarning(inspection) {
const previewFindings = inspection.findings.slice(0, 5).map(formatFinding);
const remaining = inspection.findings.length - previewFindings.length;
const repairActions = [];
if (hasCorruptFeishuStateJsonFinding(inspection)) repairActions.push(`archive ${formatDisplayPath(inspection.feishuStateDir)}`);
const repairSessionEntries = collectRepairSessionEntries(inspection);
if (repairSessionEntries.length > 0) repairActions.push(`archive artifacts and remove ${countLabel(repairSessionEntries.length, "flagged Feishu-scoped session entry", "flagged Feishu-scoped session entries")}`);
const repairSummary = repairActions.length > 0 ? repairActions.join(" and ") : "apply targeted Feishu state cleanup";
return [
"- Feishu local channel state may need repair.",
...previewFindings,
...remaining > 0 ? [`- ...and ${remaining} more Feishu state finding(s).`] : [],
`- Repair will ${repairSummary}, while preserving Feishu App ID/secret config and healthy session entries.`,
"- Run \"openclaw doctor --fix\" to rebuild Feishu local state."
].join("\n");
}
function formatRepairChange(report) {
const stateRepairStatus = report.stateDirRepairAttempted ? report.rebuiltStateDir ? "yes" : "no existing state" : "not needed";
return [
"Feishu local state repaired.",
`- Backup dir: ${formatDisplayPath(report.backupDir)}`,
`- Rebuilt Feishu runtime state: ${stateRepairStatus}`,
`- Removed ${countLabel(report.removedSessionEntries, "Feishu-scoped session entry", "Feishu-scoped session entries")} from ${countLabel(report.touchedSessionStores, "session store")}.`,
`- Archived ${countLabel(report.archivedSessionArtifacts, "session artifact file")}.`,
"- Preserved Feishu App ID/secret config."
].join("\n");
}
function hasConfiguredFeishuChannel(cfg) {
return Boolean(cfg.channels?.feishu);
}
async function runFeishuDoctorSequence(params) {
if (!hasConfiguredFeishuChannel(params.cfg)) return {
changeNotes: [],
warningNotes: []
};
const inspection = inspectFeishuDoctorState({
cfg: params.cfg,
env: params.env
});
if (inspection.findings.length === 0) return {
changeNotes: [],
warningNotes: []
};
if (!params.shouldRepair) return {
changeNotes: [],
warningNotes: [formatPreviewWarning(inspection)]
};
const report = await repairFeishuDoctorState({
cfg: params.cfg,
env: params.env,
inspection
});
return {
changeNotes: [formatRepairChange(report)],
warningNotes: report.warnings
};
}
const feishuDoctor = { runConfigSequence: async ({ cfg, env, shouldRepair }) => await runFeishuDoctorSequence({
cfg,
env,
shouldRepair
}) };
//#endregion
//#region extensions/feishu/src/session-route.ts
function resolveFeishuOutboundSessionRoute(params) {
let trimmed = stripChannelTargetPrefix(params.target, "feishu", "lark");
if (!trimmed) return null;
const lower = normalizeLowercaseStringOrEmpty(trimmed);
let isGroup = false;
let typeExplicit = false;
if (lower.startsWith("group:") || lower.startsWith("chat:") || lower.startsWith("channel:")) {
trimmed = trimmed.replace(/^(group|chat|channel):/i, "").trim();
isGroup = true;
typeExplicit = true;
} else if (lower.startsWith("user:") || lower.startsWith("dm:")) {
trimmed = trimmed.replace(/^(user|dm):/i, "").trim();
isGroup = false;
typeExplicit = true;
}
if (!typeExplicit) {
const idLower = normalizeLowercaseStringOrEmpty(trimmed);
if (idLower.startsWith("ou_") || idLower.startsWith("on_")) isGroup = false;
}
return buildChannelOutboundSessionRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "feishu",
accountId: params.accountId,
peer: {
kind: isGroup ? "group" : "direct",
id: trimmed
},
chatType: isGroup ? "group" : "direct",
from: isGroup ? `feishu:group:${trimmed}` : `feishu:${trimmed}`,
to: trimmed
});
}
//#endregion
//#region extensions/feishu/src/setup-core.ts
function setFeishuNamedAccountEnabled$1(cfg, accountId, enabled) {
const feishuCfg = cfg.channels?.feishu;
return {
...cfg,
channels: {
...cfg.channels,
feishu: {
...feishuCfg,
accounts: {
...feishuCfg?.accounts,
[accountId]: {
...feishuCfg?.accounts?.[accountId],
enabled
}
}
}
}
};
}
const feishuSetupAdapter = {
resolveAccountId: ({ cfg, accountId }) => accountId?.trim() || resolveDefaultFeishuAccountId(cfg),
applyAccountConfig: ({ cfg, accountId }) => {
if (!accountId || accountId === "default") return {
...cfg,
channels: {
...cfg.channels,
feishu: {
...cfg.channels?.feishu,
enabled: true
}
}
};
return setFeishuNamedAccountEnabled$1(cfg, accountId, true);
}
};
//#endregion
//#region extensions/feishu/src/setup-surface.ts
const t = createSetupTranslator();
const channel = "feishu";
const SCAN_TO_CREATE_TP = "ob_cli_app";
const FEISHU_SETUP_FLOW_KEY = "_flow";
function isFeishuConfigured(cfg) {
const feishuCfg = cfg.channels?.feishu;
const isAppIdConfigured = (value) => {
if (normalizeOptionalString(value)) return true;
if (!value || typeof value !== "object") return false;
const rec = value;
const source = normalizeOptionalString(rec.source)?.toLowerCase();
const id = normalizeOptionalString(rec.id);
if (source === "env" && id) return Boolean(normalizeOptionalString(process.env[id]));
return hasConfiguredSecretInput(value);
};
const topLevelConfigured = isAppIdConfigured(feishuCfg?.appId) && hasConfiguredSecretInput(feishuCfg?.appSecret);
const accountConfigured = Object.values(feishuCfg?.accounts ?? {}).some((account) => {
if (!account || typeof account !== "object") return false;
const hasOwnAppId = Object.hasOwn(account, "appId");
const hasOwnAppSecret = Object.hasOwn(account, "appSecret");
const accountAppIdConfigured = hasOwnAppId ? isAppIdConfigured(account.appId) : isAppIdConfigured(feishuCfg?.appId);
const accountSecretConfigured = hasOwnAppSecret ? hasConfiguredSecretInput(account.appSecret) : hasConfiguredSecretInput(feishuCfg?.appSecret);
return accountAppIdConfigured && accountSecretConfigured;
});
return topLevelConfigured || accountConfigured;
}
function formatFeishuStatusLine(status) {
if (status === "needs-credentials") return `Feishu: ${t("wizard.channels.statusNeedsAppCredentials")}`;
return `Feishu: ${t("wizard.channels.statusConfiguredConnectionNotVerified")}`;
}
/**
* Patch feishu config at the correct location based on accountId.
* - DEFAULT_ACCOUNT_ID → writes to top-level channels.feishu
* - named account → writes to channels.feishu.accounts[accountId]
*/
function patchFeishuConfig(cfg, accountId, patch) {
const feishuCfg = cfg.channels?.feishu;
if (accountId === "default") return patchTopLevelChannelConfigSection({
cfg,
channel,
enabled: true,
patch
});
const nextAccountPatch = {
...feishuCfg?.accounts?.[accountId],
enabled: true,
...patch
};
return patchTopLevelChannelConfigSection({
cfg,
channel,
enabled: true,
patch: { accounts: {
...feishuCfg?.accounts,
[accountId]: nextAccountPatch
} }
});
}
async function promptFeishuAllowFrom(params) {
const feishuCfg = params.cfg.channels?.feishu;
const resolvedAccountId = params.accountId ?? resolveDefaultFeishuAccountId(params.cfg);
const existingAllowFrom = (resolvedAccountId !== "default" ? feishuCfg?.accounts?.[resolvedAccountId] : void 0)?.allowFrom ?? feishuCfg?.allowFrom ?? [];
await params.prompter.note([
t("wizard.feishu.allowlistIntro"),
t("wizard.feishu.allowlistFindUser"),
t("wizard.feishu.examples"),
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
].join("\n"), t("wizard.feishu.allowlistTitle"));
const mergedAllowFrom = mergeAllowFromEntries(existingAllowFrom, splitSetupEntries(await params.prompter.text({
message: t("wizard.feishu.allowFromPrompt"),
placeholder: "ou_xxxxx, ou_yyyyy",
initialValue: existingAllowFrom.length > 0 ? existingAllowFrom.map(String).join(", ") : void 0
})));
return patchFeishuConfig(params.cfg, resolvedAccountId, { allowFrom: mergedAllowFrom });
}
async function noteFeishuCredentialHelp(prompter) {
await prompter.note([
t("wizard.feishu.credentialsStepOpenPlatform"),
t("wizard.feishu.credentialsStepCreateApp"),
t("wizard.feishu.credentialsStepGetCredentials"),
t("wizard.feishu.credentialsStepPermissions"),
t("wizard.feishu.credentialsStepPublish"),
t("wizard.feishu.credentialsEnvTip"),
t("wizard.channels.docs", { link: formatDocsLink("/channels/feishu", "feishu") })
].join("\n"), t("wizard.feishu.credentialsTitle"));
}
async function promptFeishuAppId(params) {
return (await params.prompter.text({
message: t("wizard.feishu.appIdPrompt"),
initialValue: params.initialValue,
validate: (value) => value?.trim() ? void 0 : t("common.required")
})).trim();
}
const feishuDmPolicy = {
label: "Feishu",
channel,
policyKey: "channels.feishu.dmPolicy",
allowFromKey: "channels.feishu.allowFrom",
resolveConfigKeys: (_cfg, accountId) => {
const resolvedAccountId = accountId ?? resolveDefaultFeishuAccountId(_cfg);
return resolvedAccountId !== "default" ? {
policyKey: `channels.feishu.accounts.${resolvedAccountId}.dmPolicy`,
allowFromKey: `channels.feishu.accounts.${resolvedAccountId}.allowFrom`
} : {
policyKey: "channels.feishu.dmPolicy",
allowFromKey: "channels.feishu.allowFrom"
};
},
getCurrent: (cfg, accountId) => {
const feishuCfg = cfg.channels?.feishu;
const resolvedAccountId = accountId ?? resolveDefaultFeishuAccountId(cfg);
if (resolvedAccountId !== "default") {
const account = feishuCfg?.accounts?.[resolvedAccountId];
if (account?.dmPolicy) return account.dmPolicy;
}
return feishuCfg?.dmPolicy ?? "pairing";
},
setPolicy: (cfg, policy, accountId) => {
return patchFeishuConfig(cfg, accountId ?? resolveDefaultFeishuAccountId(cfg), {
dmPolicy: policy,
...policy === "open" ? { allowFrom: mergeAllowFromEntries([], ["*"]) } : {}
});
},
promptAllowFrom: promptFeishuAllowFrom
};
function applyNewAppSecurityPolicy(cfg, accountId, openId, groupPolicy) {
let next = cfg;
if (openId) next = patchFeishuConfig(next, accountId, {
dmPolicy: "allowlist",
allowFrom: [openId]
});
const groupPatch = { groupPolicy };
if (groupPolicy === "open") groupPatch.requireMention = true;
next = patchFeishuConfig(next, accountId, groupPatch);
return next;
}
let appRegistrationModulePromise = null;
const loadAppRegistrationModule = async () => {
appRegistrationModulePromise ??= import("./app-registration-Dt5Z-8qr.js");
return await appRegistrationModulePromise;
};
async function promptFeishuDomain(params) {
return await params.prompter.select({
message: t("wizard.feishu.domainPrompt"),
options: [{
value: "feishu",
label: t("wizard.feishu.domainFeishu")
}, {
value: "lark",
label: t("wizard.feishu.domainLark")
}],
initialValue: params.initialValue ?? "feishu"
});
}
async function promptFeishuSetupMethod(prompter) {
return await prompter.select({
message: t("wizard.feishu.setupMethodPrompt"),
options: [{
value: "manual",
label: t("wizard.feishu.setupMethodManual")
}, {
value: "scan",
label: t("wizard.feishu.setupMethodScan")
}],
initialValue: "manual"
});
}
async function runScanToCreate(prompter, domain) {
const { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode } = await loadAppRegistrationModule();
try {
await initAppRegistration(domain);
} catch {
await prompter.note(t("wizard.feishu.scanUnavailable"), t("wizard.feishu.setupTitle"));
return null;
}
const begin = await beginAppRegistration(domain);
await prompter.note(t("wizard.feishu.scanQr"), t("wizard.feishu.scanTitle"));
await printQrCode(begin.qrUrl);
const progress = prompter.progress(t("wizard.feishu.fetchingConfig"));
const outcome = await pollAppRegistration({
deviceCode: begin.deviceCode,
interval: begin.interval,
expireIn: begin.expireIn,
initialDomain: domain,
tp: SCAN_TO_CREATE_TP
});
switch (outcome.status) {
case "success":
progress.stop(t("wizard.feishu.scanCompleted"));
return outcome.result;
case "access_denied":
progress.stop(t("wizard.feishu.scanDenied"));
return null;
case "expired":
progress.stop(t("wizard.feishu.scanExpired"));
return null;
case "timeout":
progress.stop(t("wizard.feishu.scanTimedOut"));
return null;
case "error":
progress.stop(t("wizard.feishu.scanError", { error: outcome.message }));
return null;
}
return null;
}
async function runNewAppFlow(params) {
const { prompter, options } = params;
let next = params.cfg;
const targetAccountId = resolveDefaultFeishuAccountId(next);
let appId;
let appSecret = null;
let appSecretProbeValue = null;
let scanDomain;
let scanOpenId;
const currentDomain = (next.channels?.feishu)?.domain ?? "feishu";
const setupMethod = await promptFeishuSetupMethod(prompter);
const selectedDomain = await promptFeishuDomain({
prompter,
initialValue: currentDomain
});
scanDomain = selectedDomain;
const scanResult = setupMethod === "scan" ? await runScanToCreate(prompter, selectedDomain) : null;
if (scanResult) {
appId = scanResult.appId;
appSecret = scanResult.appSecret;
scanDomain = scanResult.domain;
scanOpenId = scanResult.openId;
} else {
await noteFeishuCredentialHelp(prompter);
appId = await promptFeishuAppId({
prompter,
initialValue: normalizeOptionalString(process.env.FEISHU_APP_ID)
});
const appSecretResult = await promptSingleChannelSecretInput({
cfg: next,
prompter,
providerHint: "feishu",
credentialLabel: "App Secret",
secretInputMode: options?.secretInputMode,
accountConfigured: false,
canUseEnv: false,
hasConfigToken: false,
envPrompt: "",
keepPrompt: t("wizard.feishu.appSecretKeep"),
inputPrompt: t("wizard.feishu.appSecretPrompt"),
preferredEnvVar: "FEISHU_APP_SECRET"
});
if (appSecretResult.action === "set") {
appSecret = appSecretResult.value;
appSecretProbeValue = appSecretResult.resolvedValue;
}
if (appId && appSecretProbeValue) {
const { getAppOwnerOpenId } = await loadAppRegistrationModule();
scanOpenId = await getAppOwnerOpenId({
appId,
appSecret: appSecretProbeValue,
domain: selectedDomain
});
}
}
const groupPolicy = await prompter.select({
message: t("wizard.feishu.groupPolicyPrompt"),
options: [
{
value: "allowlist",
label: t("wizard.feishu.groupPolicyAllowlist")
},
{
value: "open",
label: t("wizard.feishu.groupPolicyOpen")
},
{
value: "disabled",
label: t("wizard.feishu.groupPolicyDisabled")
}
],
initialValue: "allowlist"
});
const configProgress = prompter.progress(t("wizard.feishu.configuring"));
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
if (appId && appSecret) next = patchFeishuConfig(next, targetAccountId, {
appId,
appSecret,
connectionMode: "websocket",
...scanDomain ? { domain: scanDomain } : {}
});
else if (scanDomain) next = patchFeishuConfig(next, targetAccountId, { domain: scanDomain });
next = applyNewAppSecurityPolicy(next, targetAccountId, scanOpenId, groupPolicy);
configProgress.stop(t("wizard.feishu.botConfigured"));
return { cfg: next };
}
async function runEditFlow(params) {
const { prompter, options } = params;
const next = params.cfg;
const feishuCfg = next.channels?.feishu;
const resolveAppIdLabel = (value) => {
const asString = normalizeOptionalString(value);
if (asString) return asString;
if (value && typeof value === "object") {
const rec = value;
if (normalizeOptionalString(rec.source) && normalizeOptionalString(rec.id)) return normalizeOptionalString(process.env[rec.id]) ?? `env:${String(rec.id)}`;
if (hasConfiguredSecretInput(value)) return "(configured)";
}
};
const existingAppId = resolveAppIdLabel(feishuCfg?.appId) ?? Object.values(feishuCfg?.accounts ?? {}).reduce((found, account) => {
if (found) return found;
if (account && typeof account === "object") return resolveAppIdLabel(account.appId);
}, void 0);
if (existingAppId) {
if (!await prompter.confirm({
message: t("wizard.feishu.existingBotPrompt", { appId: existingAppId }),
initialValue: true
})) return runNewAppFlow({
cfg: next,
prompter,
options
});
} else return runNewAppFlow({
cfg: next,
prompter,
options
});
await prompter.note(t("wizard.feishu.botConfigured"), "");
return { cfg: next };
}
async function runFeishuLogin(params) {
const { cfg, prompter } = params;
const options = {};
if (isFeishuConfigured(cfg)) {
const result = await runEditFlow({
cfg,
prompter,
options
});
if (result === null) return cfg;
return result.cfg;
}
return (await runNewAppFlow({
cfg,
prompter,
options
})).cfg;
}
const feishuSetupWizard = {
channel,
resolveAccountIdForConfigure: ({ accountOverride, defaultAccountId, cfg }) => (typeof accountOverride === "string" && accountOverride.trim() ? accountOverride.trim() : void 0) ?? resolveDefaultFeishuAccountId(cfg) ?? defaultAccountId,
resolveShouldPromptAccountIds: () => false,
status: {
configuredLabel: t("wizard.channels.statusConfigured"),
unconfiguredLabel: t("wizard.channels.statusNeedsAppCredentials"),
configuredHint: t("wizard.channels.statusConfigured"),
unconfiguredHint: t("wizard.channels.statusNeedsAppCreds"),
configuredScore: 2,
unconfiguredScore: 0,
resolveConfigured: ({ cfg }) => isFeishuConfigured(cfg),
resolveStatusLines: async ({ cfg, accountId, configured }) => {
const account = resolveFeishuAccount({
cfg,
accountId
});
let probeResult = null;
if (configured && account.configured) try {
const { probeFeishu } = await import("./probe-DCD4ydvU.js");
probeResult = await probeFeishu(account);
} catch {}
if (!configured) return [formatFeishuStatusLine("needs-credentials")];
if (probeResult?.ok) return [`Feishu: ${t("wizard.channels.statusConnectedAs", { name: probeResult.botName ?? probeResult.botOpenId ?? "bot" })}`];
return [formatFeishuStatusLine("configured-unverified")];
}
},
prepare: async ({ cfg, credentialValues }) => {
if (isFeishuConfigured(cfg)) return { credentialValues: {
...credentialValues,
[FEISHU_SETUP_FLOW_KEY]: "edit"
} };
return { credentialValues: {
...credentialValues,
[FEISHU_SETUP_FLOW_KEY]: "new"
} };
},
credentials: [],
finalize: async ({ cfg, prompter, options, credentialValues }) => {
if ((credentialValues[FEISHU_SETUP_FLOW_KEY] ?? "new") === "edit") {
const result = await runEditFlow({
cfg,
prompter,
options
});
if (result === null) return { cfg };
return result;
}
return runNewAppFlow({
cfg,
prompter,
options
});
},
dmPolicy: feishuDmPolicy,
disable: (cfg) => patchTopLevelChannelConfigSection({
cfg,
channel,
patch: { enabled: false }
})
};
//#endregion
//#region extensions/feishu/src/channel.ts
function readFeishuMediaParam(params) {
const media = params.media;
if (typeof media !== "string") return;
return media.trim() ? media : void 0;
}
function readBooleanParam(params, keys) {
for (const key of keys) {
const value = params[key];
if (typeof value === "boolean") return value;
}
}
function hasLegacyFeishuCardCommandValue(actionValue) {
return isRecord$1(actionValue) && actionValue.oc !== "ocf1" && (Boolean(typeof actionValue.command === "string" && actionValue.command.trim()) || Boolean(typeof actionValue.text === "string" && actionValue.text.trim()));
}
function containsLegacyFeishuCardCommandValue(node) {
if (Array.isArray(node)) return node.some((item) => containsLegacyFeishuCardCommandValue(item));
if (!isRecord$1(node)) return false;
if (node.tag === "button" && hasLegacyFeishuCardCommandValue(node.value)) return true;
if (node.tag === "button" && Array.isArray(node.behaviors) && node.behaviors.some((behavior) => isRecord$1(behavior) && hasLegacyFeishuCardCommandValue(behavior.value))) return true;
return Object.values(node).some((value) => containsLegacyFeishuCardCommandValue(value));
}
const meta = {
id: "feishu",
label: "Feishu",
selectionLabel: "Feishu/Lark (飞书)",
docsPath: "/channels/feishu",
docsLabel: "feishu",
blurb: "飞书/Lark enterprise messaging.",
aliases: ["lark"],
order: 70,
preferSessionLookupForAnnounceTarget: true
};
const loadFeishuChannelRuntime = createLazyRuntimeNamedExport(() => import("./channel.runtime-B-2rjXi3.js"), "feishuChannelRuntime");
function toFeishuMessageSendResult(result, kind) {
const receipt = result.receipt ?? createFeishuSendReceipt({
messageId: result.messageId,
chatId: result.chatId ?? "",
kind
});
return {
messageId: result.messageId || receipt.primaryPlatformMessageId,
receipt
};
}
const feishuMessageAdapter = defineChannelMessageAdapter({
id: "feishu",
durableFinal: { capabilities: {
text: true,
media: true
} },
send: {
text: async (ctx) => {
const sendText = (await loadFeishuChannelRuntime()).feishuOutbound.sendText;
if (!sendText) throw new Error("Feishu text sending is not available.");
return toFeishuMessageSendResult(await sendText(ctx), "text");
},
media: async (ctx) => {
const sendMedia = (await loadFeishuChannelRuntime()).feishuOutbound.sendMedia;
if (!sendMedia) throw new Error("Feishu media sending is not available.");
return toFeishuMessageSendResult(await sendMedia(ctx), "media");
}
}
});
async function createFeishuActionClient(account) {
const { createFeishuClient } = await import("./client-Ix9SzwN8.js");
return createFeishuClient(account);
}
const collectFeishuSecurityWarnings = createAllowlistProviderGroupPolicyWarningCollector({
providerConfigPresent: (cfg) => cfg.channels?.feishu !== void 0,
resolveGroupPolicy: ({ cfg, accountId }) => resolveFeishuAccount({
cfg,
accountId
}).config?.groupPolicy,
collect: ({ cfg, accountId, groupPolicy }) => {
if (groupPolicy !== "open") return [];
return [`- Feishu[${resolveFeishuAccount({
cfg,
accountId
}).accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`];
}
});
function describeFeishuMessageTool({ cfg, accountId }) {
const enabledAccounts = accountId ? [resolveFeishuAccount({
cfg,
accountId
})].filter((account) => account.enabled && account.configured) : listEnabledFeishuAccounts(cfg);
const enabled = enabledAccounts.length > 0 || !accountId && cfg.channels?.feishu?.enabled !== false && Boolean(inspectFeishuCredentials(cfg.channels?.feishu));
if (enabledAccounts.length === 0) return {
actions: [],
capabilities: enabled ? ["presentation"] : []
};
const actions = new Set([
"send",
"read",
"edit",
"thread-reply",
"pin",
"list-pins",
"unpin",
"member-info",
"channel-info",
"channel-list"
]);
if (accountId ? enabledAccounts.some((account) => isFeishuReactionsActionEnabled({
cfg,
account
})) : areAnyFeishuReactionActionsEnabled(cfg)) {
actions.add("react");
actions.add("reactions");
}
return {
actions: Array.from(actions),
capabilities: enabled ? ["presentation"] : []
};
}
function setFeishuNamedAccountEnabled(cfg, accountId, enabled) {
const feishuCfg = cfg.channels?.feishu;
return {
...cfg,
channels: {
...cfg.channels,
feishu: {
...feishuCfg,
accounts: {
...feishuCfg?.accounts,
[accountId]: {
...feishuCfg?.accounts?.[accountId],
enabled
}
}
}
}
};
}
const feishuConfigAdapter = createHybridChannelConfigAdapter({
sectionKey: "feishu",
listAccountIds: listFeishuAccountIds,
resolveAccount: adaptScopedAccountAccessor(resolveFeishuAccount),
defaultAccountId: resolveDefaultFeishuAccountId,
clearBaseFields: [],
resolveAllowFrom: (account) => account.config.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom })
});
function isFeishuReactionsActionEnabled(params) {
if (!params.account.enabled || !params.account.configured) return false;
return createActionGate(params.account.config.actions ?? (params.cfg.channels?.feishu)?.actions)("reactions");
}
function areAnyFeishuReactionActionsEnabled(cfg) {
for (const account of listEnabledFeishuAccounts(cfg)) if (isFeishuReactionsActionEnabled({
cfg,
account
})) return true;
return false;
}
function isFeishuGroupTopicSessionKey(sessionKey) {
if (typeof sessionKey !== "string" || !sessionKey) return false;
const parsed = parseFeishuConversationId({ conversationId: sessionKey });
return parsed?.scope === "group_topic" || parsed?.scope === "group_topic_sender";
}
function resolveFeishuTopicAutoThreadAnchor(ctx) {
if (ctx.action !== "send") return;
if (!isFeishuGroupTopicSessionKey(ctx.sessionKey)) return;
const inbound = ctx.toolContext?.currentMessageId;
return typeof inbound === "string" && inbound.length > 0 ? inbound : void 0;
}
function buildFeishuSendReplyAnchor(ctx) {
if (ctx.action === "thread-reply") return {
replyToMessageId: resolveFeishuMessageId(ctx.params),
replyInThread: true
};
const autoThreadId = resolveFeishuTopicAutoThreadAnchor(ctx);
return {
replyToMessageId: autoThreadId,
replyInThread: autoThreadId !== void 0
};
}
function isSupportedFeishuDirectConversationId(conversationId) {
const trimmed = conversationId.trim();
if (!trimmed || trimmed.includes(":")) return false;
if (trimmed.startsWith("oc_") || trimmed.startsWith("on_")) return false;
return true;
}
function normalizeFeishuAcpConversationId(conversationId) {
const parsed = parseFeishuConversationId({ conversationId });
if (!parsed || parsed.scope !== "group_topic" && parsed.scope !== "group_topic_sender" && !isSupportedFeishuDirectConversationId(parsed.canonicalConversationId)) return null;
return {
conversationId: parsed.canonicalConversationId,
parentConversationId: parsed.scope === "group_topic" || parsed.scope === "group_topic_sender" ? parsed.chatId : void 0
};
}
function matchFeishuAcpConversation(params) {
const binding = normalizeFeishuAcpConversationId(params.bindingConversationId);
if (!binding) return null;
const incoming = parseFeishuConversationId({
conversationId: params.conversationId,
parentConversationId: params.parentConversationId
});
if (!incoming || incoming.scope !== "group_topic" && incoming.scope !== "group_topic_sender" && !isSupportedFeishuDirectConversationId(incoming.canonicalConversationId)) return null;
const matchesCanonicalConversation = binding.conversationId === incoming.canonicalConversationId;
const matchesParentTopicForSenderScopedConversation = incoming.scope === "group_topic_sender" && binding.parentConversationId === incoming.chatId && binding.conversationId === `${incoming.chatId}:topic:${incoming.topicId}`;
if (!matchesCanonicalConversation && !matchesParentTopicForSenderScopedConversation) return null;
return {
conversationId: matchesParentTopicForSenderScopedConversation ? binding.conversationId : incoming.canonicalConversationId,
parentConversationId: incoming.scope === "group_topic" || incoming.scope === "group_topic_sender" ? incoming.chatId : void 0,
matchPriority: matchesCanonicalConversation ? 2 : 1
};
}
function resolveFeishuSenderScopedCommandConversation(params) {
const parentConversationId = params.parentConversationId?.trim();
const threadId = params.threadId?.trim();
const senderId = params.senderId?.trim();
if (!parentConversationId || !threadId || !senderId) return;
const expectedScopePrefix = `feishu:group:${normalizeLowercaseStringOrEmpty(parentConversationId)}:topic:${normalizeLowercaseStringOrEmpty(threadId)}:sender:`;
const isSenderScopedSession = [params.sessionKey, params.parentSessionKey].some((candidate) => {
const normalized = normalizeLowercaseStringOrEmpty(candidate ?? "");
if (!normalized) return false;
return normalized.replace(/^agent:[^:]+:/, "").startsWith(expectedScopePrefix);
});
const senderScopedConversationId = buildFeishuConversationId({
chatId: parentConversationId,
scope: "group_topic_sender",
topicId: threadId,
senderOpenId: senderId
});
if (isSenderScopedSession) return senderScopedConversationId;
if (!params.sessionKey?.trim()) return;
return getSessionBindingService().listBySession(params.sessionKey).find((binding) => {
if (binding.conversation.channel !== "feishu" || binding.conversation.accountId !== params.accountId) return false;
return binding.conversation.conversationId === senderScopedConversationId;
})?.conversation.conversationId;
}
function resolveFeishuCommandConversation(params) {
if (params.threadId) {
const parentConversationId = parseFeishuTargetId(params.originatingTo) ?? parseFeishuTargetId(params.commandTo) ?? parseFeishuTargetId(params.fallbackTo);
if (!parentConversationId) return null;
return {
conversationId: resolveFeishuSenderScopedCommandConversation({
accountId: params.accountId,
parentConversationId,
threadId: params.threadId,
senderId: params.senderId,
sessionKey: params.sessionKey,
parentSessionKey: params.parentSessionKey
}) ?? buildFeishuConversationId({
chatId: parentConversationId,
scope: "group_topic",
topicId: params.threadId
}),
parentConversationId
};
}
const conversationId = parseFeishuDirectConversationId(params.originatingTo) ?? parseFeishuDirectConversationId(params.commandTo) ?? parseFeishuDirectConversationId(params.fallbackTo);
return conversationId ? { conversationId } : null;
}
function jsonActionResult(details) {
return {
content: [{
type: "text",
text: JSON.stringify(details)
}],
details
};
}
function readFirstString(params, keys, fallback) {
for (const key of keys) {
const value = params[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
if (typeof fallback === "string" && fallback.trim()) return fallback.trim();
}
function readOptionalPositiveInteger(params, keys) {
for (const key of keys) {
const parsed = parseStrictPositiveInteger(params[key]);
if (parsed !== void 0) return parsed;
}
}
function resolveFeishuActionTarget(ctx) {
return readFirstString(ctx.params, ["to", "target"], ctx.toolContext?.currentChannelId);
}
function resolveFeishuChatId(ctx) {
const raw = readFirstString(ctx.params, [
"chatId",
"chat_id",
"channelId",
"channel_id",
"to",
"target"
], ctx.toolContext?.currentChannelId);
if (!raw) return;
if (/^(user|dm|open_id):/i.test(raw)) return;
if (/^(chat|group|channel):/i.test(raw)) return normalizeFeishuTarget(raw) ?? void 0;
return raw;
}
function resolveFeishuMessageId(params) {
return readFirstString(params, [
"messageId",
"message_id",
"replyTo",
"reply_to"
]);
}
function resolveFeishuMemberId(params) {
return readFirstString(params, [
"memberId",
"member_id",
"userId",
"user_id",
"openId",
"open_id",
"unionId",
"union_id"
]);
}
function resolveFeishuMemberIdType(params) {
const raw = readFirstString(params, [
"memberIdType",
"member_id_type",
"userIdType",
"user_id_type"
]);
if (raw === "open_id" || raw === "user_id" || raw === "union_id") return raw;
if (readFirstString(params, ["userId", "user_id"]) && !readFirstString(params, [
"openId",
"open_id",
"unionId",
"union_id"
])) return "user_id";
if (readFirstString(params, ["unionId", "union_id"]) && !readFirstString(params, ["openId", "open_id"])) return "union_id";
return "open_id";
}
const feishuPlugin = createChatChannelPlugin({
base: {
id: "feishu",
meta: { ...meta },
capabilities: {
chatTypes: ["direct", "channel"],
polls: false,
threads: true,
media: true,
tts: { voice: {
synthesisTarget: "voice-note",
transcodesAudio: true
} },
reactions: true,
edit: true,
reply: true
},
agentPrompt: { messageToolHints: () => [
"- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
"- Feishu supports interactive cards plus native image, file, audio, and video/media delivery.",
"- Feishu supports `send`, `read`, `edit`, `thread-reply`, pins, and channel/member lookup, plus reactions when enabled."
] },
groups: { resolveToolPolicy: resolveFeishuGroupToolPolicy },
conversationBindings: {
defaultTopLevelPlacement: "current",
buildModelOverrideParentCandidates: ({ parentConversationId }) => buildFeishuModelOverrideParentCandidates(parentConversationId)
},
mentions: { stripPatterns: () => ["<at user_id=\"[^\"]*\">[^<]*</at>"] },
reload: { configPrefixes: ["channels.feishu"] },
doctor: feishuDoctor,
configSchema: buildChannelConfigSchema(FeishuConfigSchema),
config: {
...feishuConfigAdapter,
setAccountEnabled: ({ cfg, accountId, enabled }) => {
if (accountId === "default") return {
...cfg,
channels: {
...cfg.channels,
feishu: {
...cfg.channels?.feishu,
enabled
}
}
};
return setFeishuNamedAccountEnabled(cfg, accountId, enabled);
},
deleteAccount: ({ cfg, accountId }) => {
if (accountId === "default") {
const next = { ...cfg };
const nextChannels = { ...cfg.channels };
delete nextChannels.feishu;
if (Object.keys(nextChannels).length > 0) next.channels = nextChannels;
else delete next.channels;
return next;
}
const feishuCfg = cfg.channels?.feishu;
const accounts = { ...feishuCfg?.accounts };
delete accounts[accountId];
return {
...cfg,
channels: {
...cfg.channels,
feishu: {
...feishuCfg,
accounts: Object.keys(accounts).length > 0 ? accounts : void 0
}
}
};
},
isConfigured: (account) => account.configured,
describeAccount: (account) => describeAccountSnapshot({
account,
configured: account.configured,
extra: {
appId: account.appId,
domain: account.domain
}
})
},
approvalCapability: feishuApprovalAuth,
secrets: {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments
},
actions: {
messageActionTargetAliases,
describeMessageTool: describeFeishuMessageTool,
handleAction: async (ctx) => {
const account = resolveFeishuAccount({
cfg: ctx.cfg,
accountId: ctx.accountId ?? void 0
});
if ((ctx.action === "react" || ctx.action === "reactions") && !isFeishuReactionsActionEnabled({
cfg: ctx.cfg,
account
})) throw new Error("Feishu reactions are disabled via actions.reactions.");
if (ctx.action === "send" || ctx.action === "thread-reply") {
const to = resolveFeishuActionTarget(ctx);
if (!to) throw new Error(`Feishu ${ctx.action} requires a target (to).`);
const { replyToMessageId, replyInThread } = buildFeishuSendReplyAnchor(ctx);
if (ctx.action === "thread-reply" && !replyToMessageId) throw new Error("Feishu thread-reply requires messageId.");
const presentation = normalizeMessagePresentation(ctx.params.presentation);
const text = readFirstString(ctx.params, ["text", "message"]);
const mediaUrl = readFeishuMediaParam(ctx.params);
const audioAsVoice = readBooleanParam(ctx.params, ["asVoice", "audioAsVoice"]);
const card = presentation ? buildFeishuPresentationCard({
presentation,
fallbackText: text
}) : void 0;
if (card && mediaUrl) throw new Error(`Feishu ${ctx.action} does not support card with media.`);
if (!card && !text && !mediaUrl) throw new Error(`Feishu ${ctx.action} requires text/message, media, or card.`);
const runtime = await loadFeishuChannelRuntime();
const maybeSendMedia = runtime.feishuOutbound.sendMedia;
if (mediaUrl && !maybeSendMedia) throw new Error("Feishu media sending is not available.");
const sendMedia = maybeSendMedia;
let result;
if (card) {
if (containsLegacyFeishuCardCommandValue(card)) throw new Error("Feishu card buttons that trigger text or commands must use structured interaction envelopes.");
result = await runtime.sendCardFeishu({
cfg: ctx.cfg,
to,
card,
accountId: ctx.accountId ?? void 0,
replyToMessageId,
replyInThread
});
} else if (mediaUrl) result = await sendMedia({
cfg: ctx.cfg,
to,
text: text ?? "",
mediaUrl,
accountId: ctx.accountId ?? void 0,
mediaLocalRoots: ctx.mediaLocalRoots,
...replyInThread ? { threadId: replyToMessageId } : { replyToId: replyToMessageId },
...audioAsVoice === true ? { audioAsVoice: true } : {}
});
else result = await runtime.sendMessageFeishu({
cfg: ctx.cfg,
to,
text,
accountId: ctx.accountId ?? void 0,
replyToMessageId,
replyInThread
});
return jsonActionResult({
ok: true,
channel: "feishu",
action: ctx.action,
...result
});
}
if (ctx.action === "read") {
const messageId = resolveFeishuMessageId(ctx.params);
if (!messageId) throw new Error("Feishu read requires messageId.");
const { getMessageFeishu } = await loadFeishuChannelRuntime();
const message = await getMessageFeishu({
cfg: ctx.cfg,
messageId,
accountId: ctx.accountId ?? void 0
});
if (!message) return {
isError: true,
content: [{
type: "text",
text: JSON.stringify({ error: `Feishu read failed or message not found: ${messageId}` })
}],
details: { error: `Feishu read failed or message not found: ${messageId}` }
};
return jsonActionResult({
ok: true,
channel: "feishu",
action: "read",
message
});
}
if (ctx.action === "edit") {
const messageId = resolveFeishuMessageId(ctx.params);
if (!messageId) throw new Error("Feishu edit requires messageId.");
const text = readFirstString(ctx.params, ["text", "message"]);
const card = ctx.params.card && typeof ctx.params.card === "object" ? ctx.params.card : void 0;
const { editMessageFeishu } = await loadFeishuChannelRuntime();
return jsonActionResult({
ok: true,
channel: "feishu",
action: "edit",
...await editMessageFeishu({
cfg: ctx.cfg,
messageId,
text,
card,
accountId: ctx.accountId ?? void 0
})
});
}
if (ctx.action === "pin") {
const messageId = resolveFeishuMessageId(ctx.params);
if (!messageId) throw new Error("Feishu pin requires messageId.");
const { createPinFeishu } = await loadFeishuChannelRuntime();
return jsonActionResult({
ok: true,
channel: "feishu",
action: "pin",
pin: await createPinFeishu({
cfg: ctx.cfg,
messageId,
accountId: ctx.accountId ?? void 0
})
});
}
if (ctx.action === "unpin") {
const messageId = resolveFeishuMessageId(ctx.params);
if (!messageId) throw new Error("Feishu unpin requires messageId.");
const { removePinFeishu } = await loadFeishuChannelRuntime();
await removePinFeishu({
cfg: ctx.cfg,
messageId,
accountId: ctx.accountId ?? void 0
});
return jsonActionResult({
ok: true,
channel: "feishu",
action: "unpin",
messageId
});
}
if (ctx.action === "list-pins") {
const chatId = resolveFeishuChatId(ctx);
if (!chatId) throw new Error("Feishu list-pins requires chatId or channelId.");
const { listPinsFeishu } = await loadFeishuChannelRuntime();
return jsonActionResult({
ok: true,
channel: "feishu",
action: "list-pins",
...await listPinsFeishu({
cfg: ctx.cfg,
chatId,
startTime: readFirstString(ctx.params, ["startTime", "start_time"]),
endTime: readFirstString(ctx.params, ["endTime", "end_time"]),
pageSize: readOptionalPositiveInteger(ctx.params, ["pageSize", "page_size"]),
pageToken: readFirstString(ctx.params, ["pageToken", "page_token"]),
accountId: ctx.accountId ?? void 0
})
});
}
if (ctx.action === "channel-info") {
const chatId = resolveFeishuChatId(ctx);
if (!chatId) throw new Error("Feishu channel-info requires chatId or channelId.");
const runtime = await loadFeishuChannelRuntime();
const client = await createFeishuActionClient(account);
const channel = await runtime.getChatInfo(client, chatId);
if (!(ctx.params.includeMembers === true || ctx.params.members === true)) return jsonActionResult({
ok: true,
provider: "feishu",
action: "channel-info",
channel
});
return jsonActionResult({
ok: true,
provider: "feishu",
action: "channel-info",
channel,
members: await runtime.getChatMembers(client, chatId, readOptionalPositiveInteger(ctx.params, ["pageSize", "page_size"]), readFirstString(ctx.params, ["pageToken", "page_token"]), resolveFeishuMemberIdType(ctx.params))
});
}
if (ctx.action === "member-info") {
const runtime = await loadFeishuChannelRuntime();
const client = await createFeishuActionClient(account);
const memberId = resolveFeishuMemberId(ctx.params);
if (memberId) return jsonActionResult({
ok: true,
channel: "feishu",
action: "member-info",
member: await runtime.getFeishuMemberInfo(client, memberId, resolveFeishuMemberIdType(ctx.params))
});
const chatId = resolveFeishuChatId(ctx);
if (!chatId) throw new Error("Feishu member-info requires memberId or chatId/channelId.");
return jsonActionResult({
ok: true,
channel: "feishu",
action: "member-info",
...await runtime.getChatMembers(client, chatId, readOptionalPositiveInteger(ctx.params, ["pageSize", "page_size"]), readFirstString(ctx.params, ["pageToken", "page_token"]), resolveFeishuMemberIdType(ctx.params))
});
}
if (ctx.action === "channel-list") {
const runtime = await loadFeishuChannelRuntime();
const query = readFirstString(ctx.params, ["query"]);
const limit = readOptionalPositiveInteger(ctx.params, ["limit"]);
const scope = readFirstString(ctx.params, ["scope", "kind"]) ?? "all";
if (scope === "groups" || scope === "group" || scope === "channels" || scope === "channel") return jsonActionResult({
ok: true,
channel: "feishu",
action: "channel-list",
groups: await runtime.listFeishuDirectoryGroupsLive({
cfg: ctx.cfg,
query,
limit,
fallbackToStatic: false,
accountId: ctx.accountId ?? void 0
})
});
if (scope === "peers" || scope === "peer" || scope === "members" || scope === "member" || scope === "users" || scope === "user") return jsonActionResult({
ok: true,
channel: "feishu",
action: "channel-list",
peers: await runtime.listFeishuDirectoryPeersLive({
cfg: ctx.cfg,
query,
limit,
fallbackToStatic: false,
accountId: ctx.accountId ?? void 0
})
});
const [groups, peers] = await Promise.all([runtime.listFeishuDirectoryGroupsLive({
cfg: ctx.cfg,
query,
limit,
fallbackToStatic: false,
accountId: ctx.accountId ?? void 0
}), runtime.listFeishuDirectoryPeersLive({
cfg: ctx.cfg,
query,
limit,
fallbackToStatic: false,
accountId: ctx.accountId ?? void 0
})]);
return jsonActionResult({
ok: true,
channel: "feishu",
action: "channel-list",
groups,
peers
});
}
if (ctx.action === "react") {
const messageId = resolveFeishuMessageId(ctx.params);
if (!messageId) throw new Error("Feishu reaction requires messageId.");
const emoji = typeof ctx.params.emoji === "string" ? ctx.params.emoji.trim() : "";
const remove = ctx.params.remove === true;
const clearAll = ctx.params.clearAll === true;
if (remove) {
if (!emoji) throw new Error("Emoji is required to remove a Feishu reaction.");
const { listReactionsFeishu, removeReactionFeishu } = await loadFeishuChannelRuntime();
const ownReaction = (await listReactionsFeishu({
cfg: ctx.cfg,
messageId,
emojiType: emoji,
accountId: ctx.accountId ?? void 0
})).find((entry) => entry.operatorType === "app");
if (!ownReaction) return jsonActionResult({
ok: true,
removed: null
});
await removeReactionFeishu({
cfg: ctx.cfg,
messageId,
reactionId: ownReaction.reactionId,
accountId: ctx.accountId ?? void 0
});
return jsonActionResult({
ok: true,
removed: emoji
});
}
if (!emoji) {
if (!clearAll) throw new Error("Emoji is required to add a Feishu reaction. Set clearAll=true to remove all bot reactions.");
const { listReactionsFeishu, removeReactionFeishu } = await loadFeishuChannelRuntime();
const reactions = await listReactionsFeishu({
cfg: ctx.cfg,
messageId,
accountId: ctx.accountId ?? void 0
});
let removed = 0;
for (const reaction of reactions.filter((entry) => entry.operatorType === "app")) {
await removeReactionFeishu({
cfg: ctx.cfg,
messageId,
reactionId: reaction.reactionId,
accountId: ctx.accountId ?? void 0
});
removed += 1;
}
return jsonActionResult({
ok: true,
removed
});
}
const { addReactionFeishu } = await loadFeishuChannelRuntime();
await addReactionFeishu({
cfg: ctx.cfg,
messageId,
emojiType: emoji,
accountId: ctx.accountId ?? void 0
});
return jsonActionResult({
ok: true,
added: emoji
});
}
if (ctx.action === "reactions") {
const messageId = resolveFeishuMessageId(ctx.params);
if (!messageId) throw new Error("Feishu reactions lookup requires messageId.");
const { listReactionsFeishu } = await loadFeishuChannelRuntime();
return jsonActionResult({
ok: true,
reactions: await listReactionsFeishu({
cfg: ctx.cfg,
messageId,
accountId: ctx.accountId ?? void 0
})
});
}
throw new Error(`Unsupported Feishu action: "${ctx.action}"`);
}
},
bindings: {
compileConfiguredBinding: ({ conversationId }) => normalizeFeishuAcpConversationId(conversationId),
matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => matchFeishuAcpConversation({
bindingConversationId: compiledBinding.conversationId,
conversationId,
parentConversationId
}),
resolveCommandConversation: ({ accountId, threadId, senderId, sessionKey, parentSessionKey, originatingTo, commandTo, fallbackTo }) => resolveFeishuCommandConversation({
accountId,
threadId,
senderId,
sessionKey,
parentSessionKey,
originatingTo,
commandTo,
fallbackTo
})
},
auth: { login: async ({ cfg }) => {
const { createClackPrompter } = await import("./plugin-sdk/setup-runtime.js");
const { replaceConfigFile } = await import("./plugin-sdk/config-mutation.js");
const nextCfg = await runFeishuLogin({
cfg,
prompter: createClackPrompter()
});
if (nextCfg !== cfg) await replaceConfigFile({
nextConfig: nextCfg,
afterWrite: { mode: "auto" }
});
} },
setup: feishuSetupAdapter,
setupWizard: feishuSetupWizard,
messaging: {
targetPrefixes: ["feishu", "lark"],
normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? void 0,
resolveDeliveryTarget: ({ conversationId, parentConversationId }) => {
const directId = parseFeishuDirectConversationId(conversationId);
if (directId) return { to: `user:${directId}` };
const parsed = parseFeishuConversationId({
conversationId,
parentConversationId
});
if (parsed?.topicId) return {
to: `chat:${parentConversationId?.trim() || parsed.chatId}`,
threadId: parsed.topicId
};
return { to: `chat:${parsed?.chatId ?? conversationId.trim()}` };
},
resolveSessionConversation: ({ kind, rawId }) => resolveFeishuSessionConversation({
kind,
rawId
}),
resolveOutboundSessionRoute: (params) => resolveFeishuOutboundSessionRoute(params),
targetResolver: {
looksLikeId: looksLikeFeishuId,
hint: "<chatId|user:openId|chat:chatId>"
}
},
directory: createChannelDirectoryAdapter({
listPeers: async ({ cfg, query, limit, accountId }) => listFeishuDirectoryPeers({
cfg,
query: query ?? void 0,
limit: limit ?? void 0,
accountId: accountId ?? void 0
}),
listGroups: async ({ cfg, query, limit, accountId }) => listFeishuDirectoryGroups({
cfg,
query: query ?? void 0,
limit: limit ?? void 0,
accountId: accountId ?? void 0
}),
...createRuntimeDirectoryLiveAdapter({
getRuntime: loadFeishuChannelRuntime,
listPeersLive: (runtime) => async ({ cfg, query, limit, accountId }) => await runtime.listFeishuDirectoryPeersLive({
cfg,
query: query ?? void 0,
limit: limit ?? void 0,
accountId: accountId ?? void 0
}),
listGroupsLive: (runtime) => async ({ cfg, query, limit, accountId }) => await runtime.listFeishuDirectoryGroupsLive({
cfg,
query: query ?? void 0,
limit: limit ?? void 0,
accountId: accountId ?? void 0
})
})
}),
status: createComputedAccountStatusAdapter({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
buildChannelSummary: ({ snapshot }) => buildProbeChannelStatusSummary(snapshot, { port: snapshot.port ?? null }),
probeAccount: async ({ account }) => await (await loadFeishuChannelRuntime()).probeFeishu(account),
resolveAccountSnapshot: ({ account, runtime }) => ({
accountId: account.accountId,
enabled: account.enabled,
configured: account.configured,
name: account.name,
extra: {
appId: account.appId,
domain: account.domain,
port: runtime?.port ?? null
}
})
}),
gateway: { startAccount: async (ctx) => {
const { monitorFeishuProvider } = await import("./monitor-BFQGKXpb.js");
const account = resolveFeishuRuntimeAccount({
cfg: ctx.cfg,
accountId: ctx.accountId
}, { requireEventSecrets: true });
const port = account.config?.webhookPort ?? null;
ctx.setStatus({
accountId: ctx.accountId,
port
});
ctx.log?.info(`starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`);
return monitorFeishuProvider({
config: ctx.cfg,
runtime: ctx.runtime,
channelRuntime: ctx.channelRuntime,
abortSignal: ctx.abortSignal,
accountId: ctx.accountId
});
} },
message: feishuMessageAdapter
},
security: {
collectWarnings: projectConfigAccountIdWarningCollector(collectFeishuSecurityWarnings),
collectAuditFindings: ({ cfg }) => collectFeishuSecurityAuditFindings({ cfg })
},
pairing: { text: {
idLabel: "feishuUserId",
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: createPairingPrefixStripper(/^(feishu|user|open_id):/i),
notify: async ({ cfg, id, message, accountId }) => {
const { sendMessageFeishu } = await loadFeishuChannelRuntime();
await sendMessageFeishu({
cfg,
to: id,
text: message,
accountId
});
}
} },
outbound: {
deliveryMode: "direct",
chunker: chunkTextForOutbound,
chunkerMode: "markdown",
textChunkLimit: 4e3,
presentationCapabilities: {
supported: true,
buttons: true,
selects: false,
context: true,
divider: true,
limits: {
actions: {
maxActions: 20,
maxActionsPerRow: 5,
maxLabelLength: 40,
maxValueBytes: 1024
},
text: {
maxLength: 4e3,
encoding: "characters",
markdownDialect: "markdown"
}
}
},
renderPresentation: async (ctx) => {
const renderPresentation = (await loadFeishuChannelRuntime()).feishuOutbound.renderPresentation;
return renderPresentation ? await renderPresentation(ctx) : null;
},
sendPayload: async (ctx) => {
const sendPayload = (await loadFeishuChannelRuntime()).feishuOutbound.sendPayload;
if (!sendPayload) throw new Error("Feishu payload sending is not available.");
return await sendPayload(ctx);
},
...createRuntimeOutboundDelegates({
getRuntime: loadFeishuChannelRuntime,
sendText: { resolve: (runtime) => runtime.feishuOutbound.sendText },
sendMedia: { resolve: (runtime) => runtime.feishuOutbound.sendMedia }
})
}
});
//#endregion
export { setFeishuNamedAccountEnabled$1 as a, feishuSetupAdapter as i, feishuSetupWizard as n, runFeishuLogin as r, feishuPlugin as t };