feat(channels): add Feishu (Lark) support

This commit is contained in:
maxwell 2026-01-29 09:43:25 +08:00
parent 9688454a30
commit 963c5b5a7a
34 changed files with 1678 additions and 94 deletions

5
.github/labeler.yml vendored
View File

@ -9,6 +9,11 @@
- "src/discord/**"
- "extensions/discord/**"
- "docs/channels/discord.md"
"channel: feishu":
- changed-files:
- any-glob-to-any-file:
- "extensions/feishu/**"
- "docs/channels/feishu.md"
"channel: googlechat":
- changed-files:
- any-glob-to-any-file:

59
docs/channels/feishu.md Normal file
View File

@ -0,0 +1,59 @@
---
summary: "Feishu (Lark) bot support status, capabilities, and configuration"
read_when:
- Working on Feishu channel features
---
# Feishu (Lark)
Status: beta; inbound via WebSocket using `@larksuiteoapi/node-sdk`.
## Quick setup
1) Create a Feishu/Lark app and enable the IM message receive event.
2) Subscribe to `im.message.receive_v1` in your app settings.
3) Configure `channels.feishu.appId` and `channels.feishu.appSecret`.
4) Start the gateway.
Minimal config:
```json5
{
channels: {
feishu: {
enabled: true,
appId: "YOUR_APP_ID",
appSecret: "YOUR_APP_SECRET"
}
}
}
```
Multi-account example:
```json5
{
channels: {
feishu: {
accounts: {
work: { appId: "APP_ID", appSecret: "APP_SECRET" },
personal: { appId: "APP_ID", appSecret: "APP_SECRET" }
}
}
}
}
```
## How it works
- Messages are received over the Feishu WebSocket event stream.
- Replies are sent back to the same `chat_id`.
- DMs use pairing by default (`channels.feishu.dm.policy`).
- Group chats can be restricted with `channels.feishu.groupPolicy` and `channels.feishu.groups`.
## Target formats
- `chat:<chat_id>` for group chats.
- `user:<open_id>` for direct messages.
## Configuration reference (Feishu)
- `channels.feishu.appId`: Feishu App ID.
- `channels.feishu.appSecret`: Feishu App Secret.
- `channels.feishu.dm.policy`: DM policy (`pairing`, `allowlist`, `open`, `disabled`).
- `channels.feishu.dm.allowFrom`: allowlist for DMs when policy is `allowlist` or `open`.
- `channels.feishu.groupPolicy`: `open`, `allowlist`, or `disabled`.
- `channels.feishu.groups`: per-chat overrides keyed by `chat_id` (supports `requireMention`, `tools`, `users`).

View File

@ -14,6 +14,7 @@ Text is supported everywhere; media and reactions vary by channel.
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Feishu](/channels/feishu) — Lark/Feishu app via WebSocket events.
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).

View File

@ -0,0 +1,18 @@
import type { MoltbotPluginApi } from "clawdbot/plugin-sdk";
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
import { feishuPlugin } from "./src/channel.js";
import { setFeishuRuntime } from "./src/runtime.js";
const plugin = {
id: "feishu",
name: "Feishu",
description: "Feishu channel plugin",
configSchema: emptyPluginConfigSchema(),
register(api: MoltbotPluginApi) {
setFeishuRuntime(api.runtime);
api.registerChannel({ plugin: feishuPlugin });
},
};
export default plugin;

View File

@ -0,0 +1,11 @@
{
"id": "feishu",
"name": "Feishu",
"description": "Feishu channel plugin",
"channels": ["feishu"],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@ -0,0 +1,17 @@
{
"name": "@moltbot/feishu",
"version": "2026.1.27-beta.1",
"type": "module",
"description": "Moltbot Feishu channel plugin",
"moltbot": {
"extensions": [
"./index.ts"
]
},
"dependencies": {
"@larksuiteoapi/node-sdk": "1.56.1"
},
"devDependencies": {
"moltbot": "workspace:*"
}
}

View File

@ -0,0 +1,73 @@
import type { FeishuAccountConfig, MoltbotConfig } from "clawdbot/plugin-sdk";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
export type ResolvedFeishuAccount = {
accountId: string;
enabled: boolean;
name?: string;
appId?: string;
appSecret?: string;
config: FeishuAccountConfig;
};
function listConfiguredAccountIds(cfg: MoltbotConfig): string[] {
const accounts = cfg.channels?.feishu?.accounts;
if (!accounts || typeof accounts !== "object") return [];
return Object.keys(accounts).filter(Boolean);
}
export function listFeishuAccountIds(cfg: MoltbotConfig): string[] {
const ids = listConfiguredAccountIds(cfg);
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
return ids.sort((a, b) => a.localeCompare(b));
}
export function resolveDefaultFeishuAccountId(cfg: MoltbotConfig): string {
const ids = listFeishuAccountIds(cfg);
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
return ids[0] ?? DEFAULT_ACCOUNT_ID;
}
function resolveAccountConfig(
cfg: MoltbotConfig,
accountId: string,
): FeishuAccountConfig | undefined {
const accounts = cfg.channels?.feishu?.accounts;
if (!accounts || typeof accounts !== "object") return undefined;
return accounts[accountId] as FeishuAccountConfig | undefined;
}
function mergeFeishuAccountConfig(cfg: MoltbotConfig, accountId: string): FeishuAccountConfig {
const { accounts: _ignored, ...base } = (cfg.channels?.feishu ?? {}) as FeishuAccountConfig & {
accounts?: unknown;
};
const account = resolveAccountConfig(cfg, accountId) ?? {};
return { ...base, ...account };
}
export function resolveFeishuAccount(params: {
cfg: MoltbotConfig;
accountId?: string | null;
}): ResolvedFeishuAccount {
const accountId = normalizeAccountId(params.accountId);
const baseEnabled = params.cfg.channels?.feishu?.enabled !== false;
const merged = mergeFeishuAccountConfig(params.cfg, accountId);
const accountEnabled = merged.enabled !== false;
const enabled = baseEnabled && accountEnabled;
const appId = merged.appId?.trim() || undefined;
const appSecret = merged.appSecret?.trim() || undefined;
return {
accountId,
enabled,
name: merged.name?.trim() || undefined,
appId,
appSecret,
config: merged,
};
}
export function listEnabledFeishuAccounts(cfg: MoltbotConfig): ResolvedFeishuAccount[] {
return listFeishuAccountIds(cfg)
.map((accountId) => resolveFeishuAccount({ cfg, accountId }))
.filter((account) => account.enabled);
}

View File

@ -0,0 +1,318 @@
import type { ChannelPlugin, MoltbotConfig } from "clawdbot/plugin-sdk";
import {
applyAccountNameToChannelSection,
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
formatPairingApproveHint,
getChatChannelMeta,
migrateBaseNameToDefaultAccount,
missingTargetError,
normalizeAccountId,
PAIRING_APPROVED_MESSAGE,
setAccountEnabledInConfigSection,
} from "clawdbot/plugin-sdk";
import { FeishuConfigSchema } from "clawdbot/plugin-sdk";
import {
listFeishuAccountIds,
resolveDefaultFeishuAccountId,
resolveFeishuAccount,
type ResolvedFeishuAccount,
} from "./accounts.js";
import { startFeishuMonitor } from "./monitor.js";
import { resolveFeishuGroupToolPolicy } from "./policy.js";
import { getFeishuRuntime } from "./runtime.js";
import { sendFeishuMessage } from "./send.js";
import {
looksLikeFeishuTargetId,
normalizeFeishuMessagingTarget,
} from "./targets.js";
const meta = getChatChannelMeta("feishu");
function formatAllowFromEntry(entry: string): string {
return entry
.trim()
.replace(/^(feishu|lark|user|open_id|user_id|union_id|email):/i, "")
.toLowerCase();
}
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
id: "feishu",
meta: {
...meta,
},
pairing: {
idLabel: "feishuUserId",
normalizeAllowEntry: (entry) => formatAllowFromEntry(entry),
notifyApproval: async ({ cfg, id }) => {
const account = resolveFeishuAccount({
cfg: cfg as MoltbotConfig,
accountId: DEFAULT_ACCOUNT_ID,
});
if (!account.appId || !account.appSecret) return;
await sendFeishuMessage({
account,
to: `user:${id}`,
text: PAIRING_APPROVED_MESSAGE,
});
},
},
capabilities: {
chatTypes: ["direct", "channel"],
reactions: false,
threads: false,
media: false,
nativeCommands: false,
},
reload: { configPrefixes: ["channels.feishu"] },
configSchema: buildChannelConfigSchema(FeishuConfigSchema),
config: {
listAccountIds: (cfg) => listFeishuAccountIds(cfg as MoltbotConfig),
resolveAccount: (cfg, accountId) =>
resolveFeishuAccount({ cfg: cfg as MoltbotConfig, accountId }),
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg as MoltbotConfig),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg: cfg as MoltbotConfig,
sectionKey: "feishu",
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg: cfg as MoltbotConfig,
sectionKey: "feishu",
accountId,
clearBaseFields: ["appId", "appSecret", "name"],
}),
isConfigured: (account) => Boolean(account.appId?.trim() && account.appSecret?.trim()),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.appId?.trim() && account.appSecret?.trim()),
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(resolveFeishuAccount({ cfg: cfg as MoltbotConfig, accountId }).config.dm?.allowFrom ?? []).map(
(entry) => String(entry),
),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map(formatAllowFromEntry),
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(
(cfg as MoltbotConfig).channels?.feishu?.accounts?.[resolvedAccountId],
);
const allowFromPath = useAccountPath
? `channels.feishu.accounts.${resolvedAccountId}.dm.`
: "channels.feishu.dm.";
return {
policy: account.config.dm?.policy ?? "pairing",
allowFrom: account.config.dm?.allowFrom ?? [],
allowFromPath,
approveHint: formatPairingApproveHint("feishu"),
normalizeEntry: (raw) => formatAllowFromEntry(raw),
};
},
},
groups: {
resolveToolPolicy: resolveFeishuGroupToolPolicy,
},
messaging: {
normalizeTarget: normalizeFeishuMessagingTarget,
targetResolver: {
looksLikeId: looksLikeFeishuTargetId,
hint: "<chatId|user:OPEN_ID|chat:ID>",
},
},
outbound: {
deliveryMode: "direct",
chunker: (text, limit) => {
const runtime = getFeishuRuntime();
return runtime.channel.text.chunkText(text, limit);
},
chunkerMode: "text",
textChunkLimit: 4000,
resolveTarget: ({ to, allowFrom, mode }) => {
const trimmed = to?.trim() ?? "";
const allowListRaw = (allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
const allowList = allowListRaw
.filter((entry) => entry !== "*")
.map((entry) => normalizeFeishuMessagingTarget(entry))
.filter((entry): entry is string => Boolean(entry));
if (trimmed) {
const normalized = normalizeFeishuMessagingTarget(trimmed);
if (!normalized) {
if ((mode === "implicit" || mode === "heartbeat") && allowList.length > 0) {
return { ok: true, to: allowList[0] };
}
return {
ok: false,
error: missingTargetError(
"Feishu",
"<chat:ID|user:OPEN_ID> or channels.feishu.dm.allowFrom[0]",
),
};
}
return { ok: true, to: normalized };
}
if (allowList.length > 0) {
return { ok: true, to: allowList[0] };
}
return {
ok: false,
error: missingTargetError(
"Feishu",
"<chat:ID|user:OPEN_ID> or channels.feishu.dm.allowFrom[0]",
),
};
},
sendText: async ({ cfg, to, text, accountId }) => {
const account = resolveFeishuAccount({ cfg: cfg as MoltbotConfig, accountId });
const result = await sendFeishuMessage({ account, to, text });
return { channel: "feishu", messageId: result.messageId };
},
sendMedia: async ({ mediaUrl }) => {
if (!mediaUrl) {
throw new Error("Feishu mediaUrl is required.");
}
throw new Error("Feishu media is not supported yet.");
},
},
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
buildChannelSummary: ({ snapshot }) => ({
configured: snapshot.configured ?? false,
running: snapshot.running ?? false,
lastStartAt: snapshot.lastStartAt ?? null,
lastStopAt: snapshot.lastStopAt ?? null,
lastError: snapshot.lastError ?? null,
lastInboundAt: snapshot.lastInboundAt ?? null,
lastOutboundAt: snapshot.lastOutboundAt ?? null,
}),
buildAccountSnapshot: ({ account, runtime }) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.appId?.trim() && account.appSecret?.trim()),
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
lastInboundAt: runtime?.lastInboundAt ?? null,
lastOutboundAt: runtime?.lastOutboundAt ?? null,
dmPolicy: account.config.dm?.policy ?? "pairing",
}),
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
if (!account.appId?.trim() || !account.appSecret?.trim()) {
throw new Error(
`Feishu appId/appSecret missing for account "${account.accountId}" (set channels.feishu.appId/appSecret or channels.feishu.accounts.${account.accountId}.appId/appSecret).`,
);
}
ctx.log?.info(`[${account.accountId}] starting Feishu websocket`);
ctx.setStatus({
accountId: account.accountId,
running: true,
lastStartAt: Date.now(),
});
const stop = await startFeishuMonitor({
account,
config: ctx.cfg as MoltbotConfig,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
statusSink: (patch) => ctx.setStatus({ accountId: account.accountId, ...patch }),
});
return () => {
stop();
ctx.setStatus({
accountId: account.accountId,
running: false,
lastStopAt: Date.now(),
});
};
},
},
setup: {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }) =>
applyAccountNameToChannelSection({
cfg: cfg as MoltbotConfig,
channelKey: "feishu",
accountId,
name,
}),
validateInput: ({ input }) => {
if (!input.appId || !input.appSecret) {
return "Feishu requires appId and appSecret.";
}
return null;
},
applyAccountConfig: ({ cfg, accountId, input }) => {
const namedConfig = applyAccountNameToChannelSection({
cfg: cfg as MoltbotConfig,
channelKey: "feishu",
accountId,
name: input.name,
});
const next =
accountId !== DEFAULT_ACCOUNT_ID
? migrateBaseNameToDefaultAccount({
cfg: namedConfig,
channelKey: "feishu",
})
: namedConfig;
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
channels: {
...next.channels,
feishu: {
...next.channels?.feishu,
enabled: true,
appId: input.appId,
appSecret: input.appSecret,
},
},
} as MoltbotConfig;
}
return {
...next,
channels: {
...next.channels,
feishu: {
...next.channels?.feishu,
enabled: true,
accounts: {
...next.channels?.feishu?.accounts,
[accountId]: {
...next.channels?.feishu?.accounts?.[accountId],
enabled: true,
appId: input.appId,
appSecret: input.appSecret,
},
},
},
},
} as MoltbotConfig;
},
},
};

View File

@ -0,0 +1,498 @@
import * as Lark from "@larksuiteoapi/node-sdk";
import type { MoltbotConfig } from "clawdbot/plugin-sdk";
import {
resolveMentionGatingWithBypass,
type ReplyPayload,
} from "clawdbot/plugin-sdk";
import { getFeishuRuntime } from "./runtime.js";
import type { ResolvedFeishuAccount } from "./accounts.js";
import { sendFeishuMessage } from "./send.js";
export type FeishuRuntimeEnv = {
log?: (message: string) => void;
error?: (message: string) => void;
};
export type FeishuMonitorOptions = {
account: ResolvedFeishuAccount;
config: MoltbotConfig;
runtime: FeishuRuntimeEnv;
abortSignal: AbortSignal;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
};
type FeishuSender = {
sender_type?: string;
sender_id?: {
user_id?: string;
open_id?: string;
union_id?: string;
};
};
type FeishuMessage = {
chat_id?: string;
chat_type?: string;
message_id?: string;
message_type?: string;
content?: string;
create_time?: string;
root_id?: string;
parent_id?: string;
};
type FeishuMessageEvent = {
message?: FeishuMessage;
sender?: FeishuSender;
};
type FeishuCoreRuntime = ReturnType<typeof getFeishuRuntime>;
type FeishuWsClient = Lark.WSClient & {
stop?: () => void | Promise<void>;
};
function logVerbose(core: FeishuCoreRuntime, runtime: FeishuRuntimeEnv, message: string): void {
if (core.logging.shouldLogVerbose()) {
runtime.log?.(`[feishu] ${message}`);
}
}
function requireFeishuCredentials(account: ResolvedFeishuAccount): {
appId: string;
appSecret: string;
} {
const appId = account.appId?.trim();
const appSecret = account.appSecret?.trim();
if (!appId || !appSecret) {
throw new Error(
`Feishu credentials missing for account "${account.accountId}" (set channels.feishu.appId/appSecret or channels.feishu.accounts.${account.accountId}.appId/appSecret).`,
);
}
return { appId, appSecret };
}
function resolveSenderInfo(
sender?: FeishuSender,
): { id: string; idType: "open_id" | "user_id" | "union_id" } | null {
if (!sender) return null;
const ids = sender.sender_id;
if (ids?.open_id?.trim()) {
return { id: ids.open_id.trim(), idType: "open_id" };
}
if (ids?.user_id?.trim()) {
return { id: ids.user_id.trim(), idType: "user_id" };
}
if (ids?.union_id?.trim()) {
return { id: ids.union_id.trim(), idType: "union_id" };
}
return null;
}
function parseMessageText(params: {
content: string;
runtime: FeishuRuntimeEnv;
accountId: string;
}): string | null {
const trimmed = params.content.trim();
if (!trimmed) return null;
try {
const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== "object") return null;
const text = (parsed as { text?: unknown }).text;
return typeof text === "string" ? text : null;
} catch (err) {
params.runtime.error?.(
`[${params.accountId}] Feishu message content parse failed: ${String(err)}`,
);
return null;
}
}
function resolveMentionState(text: string): { hasAnyMention: boolean; wasMentioned: boolean } {
const hasAnyMention = /<at\b/i.test(text);
return { hasAnyMention, wasMentioned: hasAnyMention };
}
function normalizeAllowEntry(raw: string): string {
return raw
.trim()
.replace(/^(feishu|lark|user|open_id|user_id|union_id|email):/i, "")
.toLowerCase();
}
function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
if (allowFrom.length === 0) return false;
const normalizedSender = normalizeAllowEntry(senderId);
for (const entry of allowFrom) {
const normalized = normalizeAllowEntry(entry);
if (!normalized) continue;
if (normalized === "*") return true;
if (normalized === normalizedSender) return true;
}
return false;
}
function resolveGroupEntry(account: ResolvedFeishuAccount, chatId: string) {
const groups = account.config.groups ?? {};
const entry = groups[chatId];
const wildcard = groups["*"];
const allowlistConfigured = Object.keys(groups).length > 0;
return { entry, wildcard, allowlistConfigured };
}
function resolveGroupPolicy(cfg: MoltbotConfig, account: ResolvedFeishuAccount): "open" | "allowlist" | "disabled" {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
return account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
}
function resolveChatType(chatType?: string): "direct" | "channel" {
if (!chatType) return "channel";
return chatType === "p2p" ? "direct" : "channel";
}
async function stopFeishuWsClient(wsClient: FeishuWsClient, runtime: FeishuRuntimeEnv): Promise<void> {
const stop = wsClient.stop;
if (typeof stop !== "function") return;
try {
await Promise.resolve(stop.call(wsClient));
} catch (err) {
runtime.error?.(`feishu websocket stop failed: ${String(err)}`);
}
}
async function deliverFeishuReply(params: {
payload: ReplyPayload;
account: ResolvedFeishuAccount;
chatId: string;
runtime: FeishuRuntimeEnv;
core: FeishuCoreRuntime;
config: MoltbotConfig;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
}): Promise<void> {
const { payload, account, chatId, runtime, core, config, statusSink } = params;
const text = payload.text ?? "";
if (!text.trim()) return;
const textLimit = core.channel.text.resolveTextChunkLimit(config, "feishu", account.accountId, {
fallbackLimit: 4000,
});
const chunkMode = core.channel.text.resolveChunkMode(config, "feishu", account.accountId);
const chunks = core.channel.text.chunkTextWithMode(text, textLimit, chunkMode);
for (const chunk of chunks) {
await sendFeishuMessage({ account, to: `chat:${chatId}`, text: chunk });
statusSink?.({ lastOutboundAt: Date.now() });
}
logVerbose(core, runtime, `reply sent to chat=${chatId}`);
}
async function handleFeishuMessage(params: {
event: FeishuMessageEvent;
account: ResolvedFeishuAccount;
config: MoltbotConfig;
runtime: FeishuRuntimeEnv;
core: FeishuCoreRuntime;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
}): Promise<void> {
const { event, account, config, runtime, core, statusSink } = params;
const message = event.message;
if (!message) return;
const chatId = message.chat_id?.trim();
if (!chatId) return;
const senderInfo = resolveSenderInfo(event.sender);
if (!senderInfo) return;
const senderId = senderInfo.id;
const senderType = event.sender?.sender_type;
if (senderType && senderType !== "user" && account.config.allowBots !== true) {
logVerbose(core, runtime, `drop bot message senderType=${senderType}`);
return;
}
const messageType = message.message_type?.trim() || "";
if (messageType && messageType !== "text") {
logVerbose(core, runtime, `drop unsupported message type=${messageType}`);
return;
}
if (typeof message.content !== "string") return;
const rawBody = parseMessageText({
content: message.content,
runtime,
accountId: account.accountId,
});
if (rawBody === null) return;
const chatType = resolveChatType(message.chat_type);
const isGroup = chatType !== "direct";
statusSink?.({ lastInboundAt: Date.now() });
const groupPolicy = resolveGroupPolicy(config, account);
const groupInfo = resolveGroupEntry(account, chatId);
const groupEntry = groupInfo.entry;
if (isGroup) {
if (groupPolicy === "disabled") return;
const groupAllowed = Boolean(groupEntry) || Boolean(groupInfo.wildcard);
if (groupPolicy === "allowlist") {
if (!groupInfo.allowlistConfigured) {
logVerbose(core, runtime, `drop group message (allowlist empty, chat=${chatId})`);
return;
}
if (!groupAllowed) {
logVerbose(core, runtime, `drop group message (not allowlisted, chat=${chatId})`);
return;
}
}
if (groupEntry?.enabled === false || groupEntry?.allow === false) {
logVerbose(core, runtime, `drop group message (disabled, chat=${chatId})`);
return;
}
if (groupEntry?.users && groupEntry.users.length > 0) {
const ok = isSenderAllowed(senderId, groupEntry.users.map((value) => String(value)));
if (!ok) {
logVerbose(core, runtime, `drop group message (sender not allowed, ${senderId})`);
return;
}
}
}
const dmPolicy = account.config.dm?.policy ?? "pairing";
const configAllowFrom = (account.config.dm?.allowFrom ?? []).map((value) => String(value));
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
const storeAllowFrom = !isGroup && (dmPolicy !== "open" || shouldComputeAuth)
? await core.channel.pairing.readAllowFromStore("feishu").catch((err) => {
runtime.error?.(`feishu: failed reading allowFrom store: ${String(err)}`);
return [];
})
: [];
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
const commandAllowFrom = isGroup
? (groupEntry?.users ?? []).map((value) => String(value))
: effectiveAllowFrom;
const useAccessGroups = config.commands?.useAccessGroups !== false;
const senderAllowedForCommands = isSenderAllowed(senderId, commandAllowFrom);
const commandAuthorized = shouldComputeAuth
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers: [
{
configured: commandAllowFrom.length > 0,
allowed: senderAllowedForCommands,
},
],
})
: undefined;
let effectiveWasMentioned: boolean | undefined;
if (isGroup) {
const requireMention = groupEntry?.requireMention ?? account.config.requireMention ?? true;
const mentionState = resolveMentionState(rawBody);
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
cfg: config,
surface: "feishu",
});
const mentionGate = resolveMentionGatingWithBypass({
isGroup: true,
requireMention,
canDetectMention: true,
wasMentioned: mentionState.wasMentioned,
implicitMention: false,
hasAnyMention: mentionState.hasAnyMention,
allowTextCommands,
hasControlCommand: core.channel.text.hasControlCommand(rawBody, config),
commandAuthorized: commandAuthorized === true,
});
effectiveWasMentioned = mentionGate.effectiveWasMentioned;
if (mentionGate.shouldSkip) {
logVerbose(core, runtime, `drop group message (mention required, chat=${chatId})`);
return;
}
}
if (!isGroup) {
if (dmPolicy === "disabled" || account.config.dm?.enabled === false) {
logVerbose(core, runtime, `drop DM (dmPolicy=disabled, sender=${senderId})`);
return;
}
if (dmPolicy !== "open") {
const allowed = senderAllowedForCommands;
if (!allowed) {
if (dmPolicy === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "feishu",
id: senderId,
});
if (created) {
try {
await sendFeishuMessage({
account,
to: `${senderInfo.idType}:${senderId}`,
text: core.channel.pairing.buildPairingReply({
channel: "feishu",
idLine: `Your Feishu user id: ${senderId}`,
code,
}),
});
statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
runtime.error?.(`feishu pairing reply failed: ${String(err)}`);
}
}
} else {
logVerbose(core, runtime, `drop DM (unauthorized, sender=${senderId})`);
}
return;
}
}
}
if (
isGroup &&
core.channel.commands.isControlCommandMessage(rawBody, config) &&
commandAuthorized !== true
) {
logVerbose(core, runtime, `feishu: drop control command from ${senderId}`);
return;
}
const route = core.channel.routing.resolveAgentRoute({
cfg: config,
channel: "feishu",
accountId: account.accountId,
peer: {
kind: isGroup ? "group" : "dm",
id: chatId,
},
});
const fromLabel = isGroup ? `chat:${chatId}` : `user:${senderId}`;
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
agentId: route.agentId,
});
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
storePath,
sessionKey: route.sessionKey,
});
const timestampMs = Number(message.create_time);
const timestamp = Number.isFinite(timestampMs) ? timestampMs : undefined;
const body = core.channel.reply.formatAgentEnvelope({
channel: "Feishu",
from: fromLabel,
timestamp,
previousTimestamp,
envelope: envelopeOptions,
body: rawBody,
});
const groupSystemPrompt = groupEntry?.systemPrompt?.trim() || undefined;
const ctxPayload = core.channel.reply.finalizeInboundContext({
Body: body,
RawBody: rawBody,
CommandBody: rawBody,
From: `feishu:${senderId}`,
To: `feishu:${chatId}`,
SessionKey: route.sessionKey,
AccountId: route.accountId,
ChatType: isGroup ? "channel" : "direct",
ConversationLabel: fromLabel,
SenderId: senderId,
WasMentioned: isGroup ? effectiveWasMentioned : undefined,
CommandAuthorized: commandAuthorized,
Provider: "feishu",
Surface: "feishu",
MessageSid: message.message_id,
MessageSidFull: message.message_id,
ReplyToId: message.parent_id ?? message.root_id,
ReplyToIdFull: message.parent_id ?? message.root_id,
GroupSpace: isGroup ? chatId : undefined,
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
OriginatingChannel: "feishu",
OriginatingTo: `feishu:${chatId}`,
});
void core.channel.session
.recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
})
.catch((err) => {
runtime.error?.(`feishu: failed updating session meta: ${String(err)}`);
});
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg: config,
dispatcherOptions: {
deliver: async (payload) => {
await deliverFeishuReply({
payload,
account,
chatId,
runtime,
core,
config,
statusSink,
});
},
onError: (err, info) => {
runtime.error?.(`[${account.accountId}] Feishu ${info.kind} reply failed: ${String(err)}`);
},
},
});
}
export async function startFeishuMonitor(opts: FeishuMonitorOptions): Promise<() => void> {
const core = getFeishuRuntime();
const { appId, appSecret } = requireFeishuCredentials(opts.account);
const loggerLevel = core.logging.shouldLogVerbose()
? Lark.LoggerLevel.debug
: Lark.LoggerLevel.error;
const wsClient: FeishuWsClient = new Lark.WSClient({
appId,
appSecret,
loggerLevel,
});
const dispatcher = new Lark.EventDispatcher({}).register({
"im.message.receive_v1": async (data: unknown) => {
const event = data as FeishuMessageEvent;
try {
await handleFeishuMessage({
event,
account: opts.account,
config: opts.config,
runtime: opts.runtime,
core,
statusSink: opts.statusSink,
});
} catch (err) {
opts.runtime.error?.(`feishu inbound handler failed: ${String(err)}`);
}
},
});
await Promise.resolve(
wsClient.start({
eventDispatcher: dispatcher,
}),
);
const abortHandler = () => {
void stopFeishuWsClient(wsClient, opts.runtime);
};
if (opts.abortSignal.aborted) {
abortHandler();
} else {
opts.abortSignal.addEventListener("abort", abortHandler, { once: true });
}
logVerbose(core, opts.runtime, `Feishu WS connected for account=${opts.account.accountId}`);
return () => {
opts.abortSignal.removeEventListener("abort", abortHandler);
void stopFeishuWsClient(wsClient, opts.runtime);
logVerbose(core, opts.runtime, `Feishu WS stopped for account=${opts.account.accountId}`);
};
}

View File

@ -0,0 +1,62 @@
import type { GroupToolPolicyConfig, MoltbotConfig } from "clawdbot/plugin-sdk";
import { normalizeAccountId, resolveToolsBySender } from "clawdbot/plugin-sdk";
type FeishuGroupConfig = {
tools?: GroupToolPolicyConfig;
toolsBySender?: Record<string, GroupToolPolicyConfig>;
};
type FeishuGroupsConfig = Record<string, FeishuGroupConfig | undefined> | undefined;
function resolveGroupsConfig(
cfg: MoltbotConfig,
accountId?: string | null,
): FeishuGroupsConfig {
const normalizedAccountId = normalizeAccountId(accountId);
const channelConfig = cfg.channels?.feishu as
| {
accounts?: Record<string, { groups?: FeishuGroupsConfig }>;
groups?: FeishuGroupsConfig;
}
| undefined;
if (!channelConfig) return undefined;
const accountGroups =
channelConfig.accounts?.[normalizedAccountId]?.groups ??
channelConfig.accounts?.[
Object.keys(channelConfig.accounts ?? {}).find(
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
) ?? ""
]?.groups;
return accountGroups ?? channelConfig.groups;
}
export function resolveFeishuGroupToolPolicy(params: {
cfg: MoltbotConfig;
accountId?: string | null;
groupId?: string | null;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
}): GroupToolPolicyConfig | undefined {
const groups = resolveGroupsConfig(params.cfg, params.accountId);
if (!groups) return undefined;
const groupId = params.groupId?.trim();
const groupConfig = groupId ? groups[groupId] : undefined;
const defaultConfig = groups["*"];
const groupSenderPolicy = resolveToolsBySender({
toolsBySender: groupConfig?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
});
if (groupSenderPolicy) return groupSenderPolicy;
if (groupConfig?.tools) return groupConfig.tools;
const defaultSenderPolicy = resolveToolsBySender({
toolsBySender: defaultConfig?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
});
if (defaultSenderPolicy) return defaultSenderPolicy;
return defaultConfig?.tools;
}

View File

@ -0,0 +1,14 @@
import type { PluginRuntime } from "clawdbot/plugin-sdk";
let runtime: PluginRuntime | null = null;
export function setFeishuRuntime(next: PluginRuntime) {
runtime = next;
}
export function getFeishuRuntime(): PluginRuntime {
if (!runtime) {
throw new Error("Feishu runtime not initialized");
}
return runtime;
}

View File

@ -0,0 +1,83 @@
import * as Lark from "@larksuiteoapi/node-sdk";
import type { MoltbotConfig } from "clawdbot/plugin-sdk";
import { resolveFeishuAccount, type ResolvedFeishuAccount } from "./accounts.js";
import { parseFeishuTarget, type FeishuReceiveIdType } from "./targets.js";
export type SendFeishuMessageParams = {
account: ResolvedFeishuAccount;
to: string;
text: string;
};
export type SendFeishuMessageResult = {
messageId: string;
};
function requireFeishuCredentials(account: ResolvedFeishuAccount): {
appId: string;
appSecret: string;
} {
const appId = account.appId?.trim();
const appSecret = account.appSecret?.trim();
if (!appId || !appSecret) {
throw new Error(
`Feishu credentials missing for account "${account.accountId}" (set channels.feishu.appId/appSecret or channels.feishu.accounts.${account.accountId}.appId/appSecret).`,
);
}
return { appId, appSecret };
}
function buildTextContent(text: string): string {
return JSON.stringify({ text });
}
function readMessageId(value: unknown): string {
if (!value || typeof value !== "object") return "";
const data = (value as { data?: unknown }).data;
if (!data || typeof data !== "object") return "";
const record = data as { message_id?: unknown; messageId?: unknown };
if (typeof record.message_id === "string") return record.message_id;
if (typeof record.messageId === "string") return record.messageId;
return "";
}
function resolveTarget(input: string): { receiveIdType: FeishuReceiveIdType; receiveId: string } {
const parsed = parseFeishuTarget(input);
if (!parsed) {
throw new Error(
"Feishu target is required (use chat:<id>, user:<open_id>, or open_id/user_id/union_id/email prefixes).",
);
}
return parsed;
}
export async function sendFeishuMessage(
params: SendFeishuMessageParams,
): Promise<SendFeishuMessageResult> {
const { appId, appSecret } = requireFeishuCredentials(params.account);
const target = resolveTarget(params.to);
const client = new Lark.Client({ appId, appSecret });
const response = await client.im.v1.message.create({
params: {
receive_id_type: target.receiveIdType,
},
data: {
receive_id: target.receiveId,
content: buildTextContent(params.text ?? ""),
msg_type: "text",
},
});
return { messageId: readMessageId(response) };
}
export async function sendFeishuMessageFromConfig(params: {
cfg: MoltbotConfig;
accountId?: string | null;
to: string;
text: string;
}): Promise<SendFeishuMessageResult> {
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
return await sendFeishuMessage({ account, to: params.to, text: params.text });
}

View File

@ -0,0 +1,61 @@
export type FeishuReceiveIdType = "chat_id" | "open_id" | "user_id" | "union_id" | "email";
export type FeishuTarget = {
receiveIdType: FeishuReceiveIdType;
receiveId: string;
};
function normalizeInput(raw: string): string {
return raw.trim().replace(/^feishu:/i, "");
}
export function parseFeishuTarget(raw: string): FeishuTarget | null {
const trimmed = normalizeInput(raw);
if (!trimmed) return null;
const lowered = trimmed.toLowerCase();
if (lowered.startsWith("chat:")) {
const id = trimmed.slice("chat:".length).trim();
return id ? { receiveIdType: "chat_id", receiveId: id } : null;
}
if (lowered.startsWith("user:")) {
const id = trimmed.slice("user:".length).trim();
return id ? { receiveIdType: "open_id", receiveId: id } : null;
}
if (lowered.startsWith("open_id:")) {
const id = trimmed.slice("open_id:".length).trim();
return id ? { receiveIdType: "open_id", receiveId: id } : null;
}
if (lowered.startsWith("user_id:")) {
const id = trimmed.slice("user_id:".length).trim();
return id ? { receiveIdType: "user_id", receiveId: id } : null;
}
if (lowered.startsWith("union_id:")) {
const id = trimmed.slice("union_id:".length).trim();
return id ? { receiveIdType: "union_id", receiveId: id } : null;
}
if (lowered.startsWith("email:")) {
const id = trimmed.slice("email:".length).trim();
return id ? { receiveIdType: "email", receiveId: id } : null;
}
if (/^ou_/i.test(trimmed)) return { receiveIdType: "open_id", receiveId: trimmed };
if (/^oc_/i.test(trimmed)) return { receiveIdType: "chat_id", receiveId: trimmed };
return { receiveIdType: "chat_id", receiveId: trimmed };
}
export function looksLikeFeishuTargetId(raw: string): boolean {
const trimmed = normalizeInput(raw);
if (!trimmed) return false;
if (/^(chat|user|open_id|user_id|union_id|email):/i.test(trimmed)) return true;
return /^oc_/.test(trimmed) || /^ou_/.test(trimmed);
}
export function normalizeFeishuMessagingTarget(raw: string): string | undefined {
const parsed = parseFeishuTarget(raw);
if (!parsed) return undefined;
if (parsed.receiveIdType === "chat_id") return `chat:${parsed.receiveId}`;
if (parsed.receiveIdType === "open_id") return `user:${parsed.receiveId}`;
if (parsed.receiveIdType === "user_id") return `user_id:${parsed.receiveId}`;
if (parsed.receiveIdType === "union_id") return `union_id:${parsed.receiveId}`;
if (parsed.receiveIdType === "email") return `email:${parsed.receiveId}`;
return undefined;
}

224
pnpm-lock.yaml generated
View File

@ -172,6 +172,13 @@ importers:
zod:
specifier: ^4.3.6
version: 4.3.6
optionalDependencies:
'@napi-rs/canvas':
specifier: ^0.1.88
version: 0.1.88
node-llama-cpp:
specifier: 3.15.0
version: 3.15.0(typescript@5.9.3)
devDependencies:
'@grammyjs/types':
specifier: ^3.23.0
@ -254,13 +261,6 @@ importers:
wireit:
specifier: ^0.14.12
version: 0.14.12
optionalDependencies:
'@napi-rs/canvas':
specifier: ^0.1.88
version: 0.1.88
node-llama-cpp:
specifier: 3.15.0
version: 3.15.0(typescript@5.9.3)
extensions/bluebubbles: {}
@ -304,6 +304,16 @@ importers:
extensions/discord: {}
extensions/feishu:
dependencies:
'@larksuiteoapi/node-sdk':
specifier: 1.56.1
version: 1.56.1
devDependencies:
moltbot:
specifier: workspace:*
version: link:../..
extensions/google-antigravity-auth: {}
extensions/google-gemini-cli-auth: {}
@ -383,12 +393,12 @@ importers:
'@microsoft/agents-hosting-extensions-teams':
specifier: ^1.2.2
version: 1.2.2
moltbot:
specifier: workspace:*
version: link:../..
express:
specifier: ^5.2.1
version: 5.2.1
moltbot:
specifier: workspace:*
version: link:../..
proper-lockfile:
specifier: ^4.1.2
version: 4.1.2
@ -1126,89 +1136,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@ -1288,24 +1314,28 @@ packages:
engines: {node: '>= 18'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@lancedb/lancedb-linux-arm64-musl@0.23.0':
resolution: {integrity: sha512-c2UCtGoYjA3oDdw5y3RLK7J2th3rSjYBng+1I03vU9g092y8KATAJO/lV2AtyxSC+esSuyY1dMEaj8ADcXjZAA==}
engines: {node: '>= 18'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@lancedb/lancedb-linux-x64-gnu@0.23.0':
resolution: {integrity: sha512-OPL7tK3JCTx43ZxvbVs+CljfCer0KrojANQbcJ2V4VAp6XBhKx1sBAlIVGuCrd93pA8UOUP3iHsM7aglPo6rCg==}
engines: {node: '>= 18'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@lancedb/lancedb-linux-x64-musl@0.23.0':
resolution: {integrity: sha512-1ZEoQDwOrKvwPyAG+95/r1NYqX8Ca5bRek8Vr62CzWCEmHd/pFeEGWZ5STrkh+Bt3GLdi2JOivFtRbmuBAJypQ==}
engines: {node: '>= 18'}
cpu: [x64]
os: [linux]
libc: [musl]
'@lancedb/lancedb-win32-arm64-msvc@0.23.0':
resolution: {integrity: sha512-OuD1mkrgXvijRlXdbx3LvfuorO04FD5qHegnTOWGXh1sIwwrvvhcJAvXUGBNLY4n/lsWvA+xTjtMwRjUitvPKg==}
@ -1322,11 +1352,13 @@ packages:
'@lancedb/lancedb@0.23.0':
resolution: {integrity: sha512-aYrIoEG24AC+wILCL57Ius/Y4yU+xFHDPKLvmjzzN4byAjzeIGF0TC86S5RBt4Ji+dxS7yIWV5Q/gE5/fybIFQ==}
engines: {node: '>= 18'}
cpu: [x64, arm64]
os: [darwin, linux, win32]
peerDependencies:
apache-arrow: '>=15.0.0 <=18.1.0'
'@larksuiteoapi/node-sdk@1.56.1':
resolution: {integrity: sha512-/ixtyJnWOmcupKgDXz+6G6qTLMi3cNrR+LGOuq2PMwcJ6hhXTUJNyAF+ADY7ah9OoeDniGU/UJwMb2gqKdxwcA==}
'@line/bot-sdk@10.6.0':
resolution: {integrity: sha512-4hSpglL/G/cW2JCcohaYz/BS0uOSJNV9IEYdMm0EiPEvDLayoI2hGq2D86uYPQFD2gvgkyhmzdShpWLG3P5r3w==}
engines: {node: '>=20'}
@ -1398,24 +1430,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-riscv64-gnu@0.3.0':
resolution: {integrity: sha512-4BC08CIaOXSSAGRZLEjqJmQfioED8ohAzwt0k2amZPEbH96YKoBNorq5EdwPf5VT+odS0DeyCwhwtxokRLZIvQ==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-x64-gnu@0.3.0':
resolution: {integrity: sha512-GpNY5Y9nOzr0Vt0Qi5U88qwe6piiIHk44kSMexl8ns90LluN5UTNYmyfi7Xq3/lmPZCpnB2xvBTYbsXCxnopIA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-x64-musl@0.3.0':
resolution: {integrity: sha512-+PnR48/x9GMY5Kh8BLjzHMx6trOegMtxAuqTM9X/bhV3QuW6sLLd7nojDHSGj/ZueK6i0tcQxvOrgNLozVtNDA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@mariozechner/clipboard-win32-arm64-msvc@0.3.0':
resolution: {integrity: sha512-+dy2vZ1Ph4EYj0cotB+bVUVk/uKl2bh9LOp/zlnFqoCCYDN6sm+L0VyIOPPo3hjoEVdGpHe1MUxp3qG/OLwXgg==}
@ -1516,30 +1552,35 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.88':
resolution: {integrity: sha512-kYyNrUsHLkoGHBc77u4Unh067GrfiCUMbGHC2+OTxbeWfZkPt2o32UOQkhnSswKd9Fko/wSqqGkY956bIUzruA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.88':
resolution: {integrity: sha512-HVuH7QgzB0yavYdNZDRyAsn/ejoXB0hn8twwFnOqUbCCdkV+REna7RXjSR7+PdfW0qMQ2YYWsLvVBT5iL/mGpw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.88':
resolution: {integrity: sha512-hvcvKIcPEQrvvJtJnwD35B3qk6umFJ8dFIr8bSymfrSMem0EQsfn1ztys8ETIFndTwdNWJKWluvxztA41ivsEw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.88':
resolution: {integrity: sha512-eSMpGYY2xnZSQ6UxYJ6plDboxq4KeJ4zT5HaVkUnbObNN6DlbJe0Mclh3wifAmquXfrlgTZt6zhHsUgz++AK6g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@0.1.88':
resolution: {integrity: sha512-qcIFfEgHrchyYqRrxsCeTQgpJZ/GqHiqPcU/Fvw/ARVlQeDX1VyFH+X+0gCR2tca6UJrq96vnW+5o7buCq+erA==}
@ -1585,36 +1626,42 @@ packages:
engines: {node: '>=20.0.0'}
cpu: [arm64, x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-armv7l@3.15.0':
resolution: {integrity: sha512-ZuZ3q6mejQnEP4o22la7zBv7jNR+IZfgItDm3KjAl04HUXTKJ43HpNwjnf9GyYYd+dEgtoX0MESvWz4RnGH8Jw==}
engines: {node: '>=20.0.0'}
cpu: [arm, x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-cuda-ext@3.15.0':
resolution: {integrity: sha512-wQwgSl7Qm8vH56oBt7IuWWDNNsDECkVMS000C92wl3PkbzjwZFiWzehwa+kF8Lr2BBMiCJNkI5nEabhYH3RN2Q==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-cuda@3.15.0':
resolution: {integrity: sha512-mDjyVulCTRYilm9Emm3lDMx7dbI1vzGqk28Pj28shartjERTUu8aUNDYOmVKNMLpUKS1akw7vy0lMF8t4qswxQ==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-vulkan@3.15.0':
resolution: {integrity: sha512-htVIthQKq/rr8v5e7NiVtcHsstqTBAAC50kUymmDMbrzAu6d/EHacCJpNbU57b1UUa1nKN5cBqr6Jr+QqEalMA==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64@3.15.0':
resolution: {integrity: sha512-etUuTqSyNefRObqc5+JZviNTkuef2XEtHcQLaamEIWwjI1dj7nTD2YMZPBP7H3M3E55HSIY82vqCQ1bp6ZILiA==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/mac-arm64-metal@3.15.0':
resolution: {integrity: sha512-3Vkq6bpyQZaIzoaLLP7H2Tt8ty5BS0zxUY2pX0ox2S9P4fp8Au0CCJuUJF4V+EKi+/PTn70A6R1QCkRMfMQJig==}
@ -1962,21 +2009,25 @@ packages:
resolution: {integrity: sha512-GubkQeQT5d3B/Jx/IiR7NMkSmXrCZcVI0BPh1i7mpFi8HgD1hQ/LbhiBKAMsMqs5bbugdQOgBEl8bOhe8JhW1g==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/linux-arm64-musl@0.26.0':
resolution: {integrity: sha512-OEypUwK69bFPj+aa3/LYCnlIUPgoOLu//WNcriwpnWNmt47808Ht7RJSg+MNK8a7pSZHpXJ5/E6CRK/OTwFdaQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/linux-x64-gnu@0.26.0':
resolution: {integrity: sha512-xO6iEW2bC6ZHyOTPmPWrg/nM6xgzyRPaS84rATy6F8d79wz69LdRdJ3l/PXlkqhi7XoxhvX4ExysA0Nf10ZZEQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/linux-x64-musl@0.26.0':
resolution: {integrity: sha512-Z3KuZFC+MIuAyFCXBHY71kCsdRq1ulbsbzTe71v+hrEv7zVBn6yzql+/AZcgfIaKzWO9OXNuz5WWLWDmVALwow==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/win32-arm64@0.26.0':
resolution: {integrity: sha512-3zRbqwVWK1mDhRhTknlQFpRFL9GhEB5GfU6U7wawnuEwpvi39q91kJ+SRJvJnhyPCARkjZBd1V8XnweN5IFd1g==}
@ -2032,21 +2083,25 @@ packages:
resolution: {integrity: sha512-Fow7H84Bs8XxuaK1yfSEWBC8HI7rfEQB9eR2A0J61un1WgCas7jNrt1HbT6+p6KmUH2bhR+r/RDu/6JFAvvj4g==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/linux-arm64-musl@1.41.0':
resolution: {integrity: sha512-WoRRDNwgP5W3rjRh42Zdx8ferYnqpKoYCv2QQLenmdrLjRGYwAd52uywfkcS45mKEWHeY1RPwPkYCSROXiGb2w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/linux-x64-gnu@1.41.0':
resolution: {integrity: sha512-75k3CKj3fOc/a/2aSgO81s3HsTZOFROthPJ+UI2Oatic1LhvH6eKjKfx3jDDyVpzeDS2qekPlc/y3N33iZz5Og==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/linux-x64-musl@1.41.0':
resolution: {integrity: sha512-8r82eBwGPoAPn67ZvdxTlX/Z3gVb+ZtN6nbkyFzwwHWAh8yGutX+VBcVkyrePSl6XgBP4QAaddPnHmkvJjqY0g==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/win32-arm64@1.41.0':
resolution: {integrity: sha512-aK+DAcckQsNCOXKruatyYuY/ROjNiRejQB1PeJtkZwM21+8rV9ODYbvKNvt0pW+YCws7svftBSFMCpl3ke2unw==}
@ -2118,24 +2173,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@reflink/reflink-linux-arm64-musl@0.1.19':
resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@reflink/reflink-linux-x64-gnu@0.1.19':
resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@reflink/reflink-linux-x64-musl@0.1.19':
resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@reflink/reflink-win32-arm64-msvc@0.1.19':
resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==}
@ -2188,24 +2247,28 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.1':
resolution: {integrity: sha512-UvApLEGholmxw/HIwmUnLq3CwdydbhaHHllvWiCTNbyGom7wTwOtz5OAQbAKZYyiEOeIXZNPkM7nA4Dtng7CLw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.1':
resolution: {integrity: sha512-uVctNgZHiGnJx5Fij7wHLhgw4uyZBVi6mykeWKOqE7bVy9Hcxn0fM/IuqdMwk6hXlaf9fFShDTFz2+YejP+x0A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.1':
resolution: {integrity: sha512-T6Eg0xWwcxd/MzBcuv4Z37YVbUbJxy5cMNnbIt/Yr99wFwli30O4BPlY8hKeGyn6lWNtU0QioBS46lVzDN38bg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.1':
resolution: {integrity: sha512-PuGZVS2xNJyLADeh2F04b+Cz4NwvpglbtWACgrDOa5YDTEHKwmiTDjoD5eZ9/ptXtcpeFrMqD2H4Zn33KAh1Eg==}
@ -2267,66 +2330,79 @@ packages:
resolution: {integrity: sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.55.3':
resolution: {integrity: sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.55.3':
resolution: {integrity: sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.55.3':
resolution: {integrity: sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.55.3':
resolution: {integrity: sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.55.3':
resolution: {integrity: sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.55.3':
resolution: {integrity: sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.55.3':
resolution: {integrity: sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.55.3':
resolution: {integrity: sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.55.3':
resolution: {integrity: sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.55.3':
resolution: {integrity: sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.55.3':
resolution: {integrity: sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.55.3':
resolution: {integrity: sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.55.3':
resolution: {integrity: sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==}
@ -3071,6 +3147,9 @@ packages:
aws4@1.13.2:
resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==}
axios@0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
@ -3214,11 +3293,6 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
clawdbot@2026.1.24-3:
resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==}
engines: {node: '>=22.12.0'}
hasBin: true
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@ -4097,24 +4171,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@ -4165,6 +4243,9 @@ packages:
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.identity@3.0.0:
resolution: {integrity: sha1-rXvGpOZH15yXLhuA/u968VYmeHY=}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
@ -4183,9 +4264,15 @@ packages:
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash.pickby@4.6.0:
resolution: {integrity: sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=}
lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
@ -6882,6 +6969,20 @@ snapshots:
'@lancedb/lancedb-win32-arm64-msvc': 0.23.0
'@lancedb/lancedb-win32-x64-msvc': 0.23.0
'@larksuiteoapi/node-sdk@1.56.1':
dependencies:
axios: 0.27.2
lodash.identity: 3.0.0
lodash.merge: 4.6.2
lodash.pickby: 4.6.0
protobufjs: 7.5.4
qs: 6.14.1
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
'@line/bot-sdk@10.6.0':
dependencies:
'@types/node': 24.10.9
@ -8929,6 +9030,13 @@ snapshots:
aws4@1.13.2: {}
axios@0.27.2:
dependencies:
follow-redirects: 1.15.11(debug@4.4.3)
form-data: 4.0.5
transitivePeerDependencies:
- debug
axios@1.13.2(debug@4.4.3):
dependencies:
follow-redirects: 1.15.11(debug@4.4.3)
@ -9098,84 +9206,6 @@ snapshots:
dependencies:
clsx: 2.1.1
clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3):
dependencies:
'@agentclientprotocol/sdk': 0.13.1(zod@4.3.6)
'@aws-sdk/client-bedrock': 3.975.0
'@buape/carbon': 0.14.0(hono@4.11.4)
'@clack/prompts': 0.11.0
'@grammyjs/runner': 2.0.3(grammy@1.39.3)
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3)
'@homebridge/ciao': 1.3.4
'@line/bot-sdk': 10.6.0
'@lydell/node-pty': 1.2.0-beta.3
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-tui': 0.49.3
'@mozilla/readability': 0.6.0
'@sinclair/typebox': 0.34.47
'@slack/bolt': 4.6.0(@types/express@5.0.6)
'@slack/web-api': 7.13.0
'@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
ajv: 8.17.1
body-parser: 2.2.2
chalk: 5.6.2
chokidar: 5.0.0
chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482)
cli-highlight: 2.1.11
commander: 14.0.2
croner: 9.1.0
detect-libc: 2.1.2
discord-api-types: 0.38.37
dotenv: 17.2.3
express: 5.2.1
file-type: 21.3.0
grammy: 1.39.3
hono: 4.11.4
jiti: 2.6.1
json5: 2.2.3
jszip: 3.10.1
linkedom: 0.18.12
long: 5.3.2
markdown-it: 14.1.0
node-edge-tts: 1.2.9
osc-progress: 0.3.0
pdfjs-dist: 5.4.530
playwright-core: 1.58.0
proper-lockfile: 4.1.2
qrcode-terminal: 0.12.0
sharp: 0.34.5
sqlite-vec: 0.1.7-alpha.2
tar: 7.5.4
tslog: 4.10.2
undici: 7.19.0
ws: 8.19.0
yaml: 2.8.2
zod: 4.3.6
optionalDependencies:
'@napi-rs/canvas': 0.1.88
node-llama-cpp: 3.15.0(typescript@5.9.3)
transitivePeerDependencies:
- '@discordjs/opus'
- '@modelcontextprotocol/sdk'
- '@types/express'
- audio-decode
- aws-crt
- bufferutil
- canvas
- debug
- devtools-protocol
- encoding
- ffmpeg-static
- jimp
- link-preview-js
- node-opus
- opusscript
- supports-color
- typescript
- utf-8-validate
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@ -10251,6 +10281,8 @@ snapshots:
lodash.debounce@4.0.8:
optional: true
lodash.identity@3.0.0: {}
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
@ -10263,8 +10295,12 @@ snapshots:
lodash.isstring@4.0.1: {}
lodash.merge@4.6.2: {}
lodash.once@4.1.1: {}
lodash.pickby@4.6.0: {}
lodash@4.17.23: {}
log-symbols@6.0.0:

View File

@ -13,6 +13,8 @@ import { requireActivePluginRegistry } from "../plugins/runtime.js";
import {
resolveDiscordGroupRequireMention,
resolveDiscordGroupToolPolicy,
resolveFeishuGroupRequireMention,
resolveFeishuGroupToolPolicy,
resolveGoogleChatGroupRequireMention,
resolveGoogleChatGroupToolPolicy,
resolveIMessageGroupRequireMention,
@ -76,6 +78,12 @@ const formatLower = (allowFrom: Array<string | number>) =>
.filter(Boolean)
.map((entry) => entry.toLowerCase());
const normalizeFeishuAllowEntry = (entry: string) =>
entry
.trim()
.replace(/^(feishu|lark|user|open_id|user_id|union_id|email):/i, "")
.toLowerCase();
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Channel docks: lightweight channel metadata/behavior for shared code paths.
@ -213,6 +221,40 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
}),
},
},
feishu: {
id: "feishu",
capabilities: {
chatTypes: ["direct", "channel"],
},
outbound: { textChunkLimit: 4000 },
config: {
resolveAllowFrom: ({ cfg, accountId }) => {
const channel = cfg.channels?.feishu as
| {
accounts?: Record<string, { dm?: { allowFrom?: Array<string | number> } }>;
dm?: { allowFrom?: Array<string | number> };
}
| undefined;
const normalized = normalizeAccountId(accountId);
const account =
channel?.accounts?.[normalized] ??
channel?.accounts?.[
Object.keys(channel?.accounts ?? {}).find(
(key) => key.toLowerCase() === normalized.toLowerCase(),
) ?? ""
];
return (account?.dm?.allowFrom ?? channel?.dm?.allowFrom ?? []).map((entry) =>
String(entry),
);
},
formatAllowFrom: ({ allowFrom }) =>
allowFrom.map((entry) => normalizeFeishuAllowEntry(String(entry))).filter(Boolean),
},
groups: {
resolveRequireMention: resolveFeishuGroupRequireMention,
resolveToolPolicy: resolveFeishuGroupToolPolicy,
},
},
googlechat: {
id: "googlechat",
capabilities: {

View File

@ -172,6 +172,15 @@ export function resolveGoogleChatGroupRequireMention(params: GroupMentionParams)
});
}
export function resolveFeishuGroupRequireMention(params: GroupMentionParams): boolean {
return resolveChannelGroupRequireMention({
cfg: params.cfg,
channel: "feishu",
groupId: params.groupId,
accountId: params.accountId,
});
}
export function resolveGoogleChatGroupToolPolicy(
params: GroupMentionParams,
): GroupToolPolicyConfig | undefined {
@ -187,6 +196,21 @@ export function resolveGoogleChatGroupToolPolicy(
});
}
export function resolveFeishuGroupToolPolicy(
params: GroupMentionParams,
): GroupToolPolicyConfig | undefined {
return resolveChannelGroupToolsPolicy({
cfg: params.cfg,
channel: "feishu",
groupId: params.groupId,
accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
}
export function resolveSlackGroupRequireMention(params: GroupMentionParams): boolean {
const account = resolveSlackAccount({
cfg: params.cfg,

View File

@ -22,6 +22,8 @@ export type ChannelSetupInput = {
tokenFile?: string;
botToken?: string;
appToken?: string;
appId?: string;
appSecret?: string;
signalNumber?: string;
cliPath?: string;
dbPath?: string;

View File

@ -11,6 +11,7 @@ describe("channel registry", () => {
expect(normalizeChatChannelId("imsg")).toBe("imessage");
expect(normalizeChatChannelId("gchat")).toBe("googlechat");
expect(normalizeChatChannelId("google-chat")).toBe("googlechat");
expect(normalizeChatChannelId("lark")).toBe("feishu");
expect(normalizeChatChannelId("web")).toBeNull();
});

View File

@ -8,6 +8,7 @@ export const CHAT_CHANNEL_ORDER = [
"telegram",
"whatsapp",
"discord",
"feishu",
"googlechat",
"slack",
"signal",
@ -58,6 +59,16 @@ const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
blurb: "very well supported right now.",
systemImage: "bubble.left.and.bubble.right",
},
feishu: {
id: "feishu",
label: "Feishu",
selectionLabel: "Feishu (Lark SDK)",
detailLabel: "Feishu Bot",
docsPath: "/channels/feishu",
docsLabel: "feishu",
blurb: "Lark/Feishu app via WebSocket.",
systemImage: "message.badge",
},
googlechat: {
id: "googlechat",
label: "Google Chat",
@ -104,6 +115,7 @@ export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = {
imsg: "imessage",
"google-chat": "googlechat",
gchat: "googlechat",
lark: "feishu",
};
const normalizeChannelKey = (raw?: string | null): string | undefined => {

View File

@ -25,6 +25,8 @@ const optionNamesAdd = [
"tokenFile",
"botToken",
"appToken",
"appId",
"appSecret",
"signalNumber",
"cliPath",
"dbPath",
@ -162,6 +164,8 @@ export function registerChannelsCli(program: Command) {
.option("--token-file <path>", "Bot token file (Telegram)")
.option("--bot-token <token>", "Slack bot token (xoxb-...)")
.option("--app-token <token>", "Slack app token (xapp-...)")
.option("--app-id <id>", "Feishu app id")
.option("--app-secret <secret>", "Feishu app secret")
.option("--signal-number <e164>", "Signal account number (E.164)")
.option("--cli-path <path>", "CLI path (signal-cli or imsg)")
.option("--db-path <path>", "iMessage database path")

View File

@ -26,6 +26,8 @@ export function applyChannelAccountConfig(params: {
tokenFile?: string;
botToken?: string;
appToken?: string;
appId?: string;
appSecret?: string;
signalNumber?: string;
cliPath?: string;
dbPath?: string;
@ -63,6 +65,8 @@ export function applyChannelAccountConfig(params: {
tokenFile: params.tokenFile,
botToken: params.botToken,
appToken: params.appToken,
appId: params.appId,
appSecret: params.appSecret,
signalNumber: params.signalNumber,
cliPath: params.cliPath,
dbPath: params.dbPath,

View File

@ -23,6 +23,8 @@ export type ChannelsAddOptions = {
tokenFile?: string;
botToken?: string;
appToken?: string;
appId?: string;
appSecret?: string;
signalNumber?: string;
cliPath?: string;
dbPath?: string;
@ -191,6 +193,8 @@ export async function channelsAddCommand(
tokenFile: opts.tokenFile,
botToken: opts.botToken,
appToken: opts.appToken,
appId: opts.appId,
appSecret: opts.appSecret,
signalNumber: opts.signalNumber,
cliPath: opts.cliPath,
dbPath: opts.dbPath,
@ -234,6 +238,8 @@ export async function channelsAddCommand(
tokenFile: opts.tokenFile,
botToken: opts.botToken,
appToken: opts.appToken,
appId: opts.appId,
appSecret: opts.appSecret,
signalNumber: opts.signalNumber,
cliPath: opts.cliPath,
dbPath: opts.dbPath,

View File

@ -0,0 +1,63 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempHome } from "./test-helpers.js";
describe("config feishu", () => {
let previousHome: string | undefined;
beforeEach(() => {
previousHome = process.env.HOME;
});
afterEach(() => {
process.env.HOME = previousHome;
});
it("loads feishu app credentials + dm and group settings", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".clawdbot");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "moltbot.json"),
JSON.stringify(
{
channels: {
feishu: {
enabled: true,
appId: "app-id",
appSecret: "app-secret",
dm: {
enabled: true,
policy: "allowlist",
allowFrom: ["ou_123"],
},
groups: {
oc_456: {
requireMention: false,
users: ["ou_123"],
},
},
},
},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
expect(cfg.channels?.feishu?.enabled).toBe(true);
expect(cfg.channels?.feishu?.appId).toBe("app-id");
expect(cfg.channels?.feishu?.appSecret).toBe("app-secret");
expect(cfg.channels?.feishu?.dm?.policy).toBe("allowlist");
expect(cfg.channels?.feishu?.dm?.allowFrom).toEqual(["ou_123"]);
expect(cfg.channels?.feishu?.groups?.["oc_456"]?.requireMention).toBe(false);
expect(cfg.channels?.feishu?.groups?.["oc_456"]?.users).toEqual(["ou_123"]);
});
});
});

View File

@ -294,6 +294,7 @@ const FIELD_LABELS: Record<string, string> = {
"channels.telegram": "Telegram",
"channels.telegram.customCommands": "Telegram Custom Commands",
"channels.discord": "Discord",
"channels.feishu": "Feishu",
"channels.slack": "Slack",
"channels.mattermost": "Mattermost",
"channels.signal": "Signal",
@ -320,6 +321,7 @@ const FIELD_LABELS: Record<string, string> = {
"channels.imessage.dmPolicy": "iMessage DM Policy",
"channels.bluebubbles.dmPolicy": "BlueBubbles DM Policy",
"channels.discord.dm.policy": "Discord DM Policy",
"channels.feishu.dm.policy": "Feishu DM Policy",
"channels.discord.retry.attempts": "Discord Retry Attempts",
"channels.discord.retry.minDelayMs": "Discord Retry Min Delay (ms)",
"channels.discord.retry.maxDelayMs": "Discord Retry Max Delay (ms)",
@ -330,6 +332,8 @@ const FIELD_LABELS: Record<string, string> = {
"channels.slack.dm.policy": "Slack DM Policy",
"channels.slack.allowBots": "Slack Allow Bot Messages",
"channels.discord.token": "Discord Bot Token",
"channels.feishu.appId": "Feishu App ID",
"channels.feishu.appSecret": "Feishu App Secret",
"channels.slack.botToken": "Slack Bot Token",
"channels.slack.appToken": "Slack App Token",
"channels.slack.userToken": "Slack User Token",
@ -602,6 +606,8 @@ const FIELD_HELP: Record<string, string> = {
"Allow Mattermost to write config in response to channel events/commands (default: true).",
"channels.discord.configWrites":
"Allow Discord to write config in response to channel events/commands (default: true).",
"channels.feishu.configWrites":
"Allow Feishu to write config in response to channel events/commands (default: true).",
"channels.whatsapp.configWrites":
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
"channels.signal.configWrites":
@ -661,6 +667,8 @@ const FIELD_HELP: Record<string, string> = {
'Direct message access control ("pairing" recommended). "open" requires channels.bluebubbles.allowFrom=["*"].',
"channels.discord.dm.policy":
'Direct message access control ("pairing" recommended). "open" requires channels.discord.dm.allowFrom=["*"].',
"channels.feishu.dm.policy":
'Direct message access control ("pairing" recommended). "open" requires channels.feishu.dm.allowFrom=["*"].',
"channels.discord.retry.attempts":
"Max retry attempts for outbound Discord API calls (default: 3).",
"channels.discord.retry.minDelayMs": "Minimum retry delay in ms for Discord outbound calls.",

View File

@ -1,4 +1,5 @@
import type { DiscordConfig } from "./types.discord.js";
import type { FeishuConfig } from "./types.feishu.js";
import type { GoogleChatConfig } from "./types.googlechat.js";
import type { IMessageConfig } from "./types.imessage.js";
import type { MSTeamsConfig } from "./types.msteams.js";
@ -28,6 +29,7 @@ export type ChannelsConfig = {
whatsapp?: WhatsAppConfig;
telegram?: TelegramConfig;
discord?: DiscordConfig;
feishu?: FeishuConfig;
googlechat?: GoogleChatConfig;
slack?: SlackConfig;
signal?: SignalConfig;

View File

@ -0,0 +1,90 @@
import type {
BlockStreamingCoalesceConfig,
DmPolicy,
GroupPolicy,
MarkdownConfig,
ReplyToMode,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js";
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type FeishuDmConfig = {
/** If false, ignore all incoming Feishu DMs. Default: true. */
enabled?: boolean;
/** Direct message access policy (default: pairing). */
policy?: DmPolicy;
/** Allowlist for DM senders (ids). */
allowFrom?: Array<string | number>;
};
export type FeishuGroupConfig = {
/** If false, disable the bot in this chat. (Alias for allow: false.) */
enabled?: boolean;
/** Legacy group allow toggle; prefer enabled. */
allow?: boolean;
/** Require mentioning the bot to trigger replies. */
requireMention?: boolean;
/** Optional tool policy overrides for this chat. */
tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
/** Allowlist of users that can invoke the bot in this chat. */
users?: Array<string | number>;
/** Optional system prompt for this chat. */
systemPrompt?: string;
};
export type FeishuAccountConfig = {
/** Optional display name for this account (used in CLI/UI lists). */
name?: string;
/** Optional provider capability tags used for agent/runtime guidance. */
capabilities?: string[];
/** Markdown formatting overrides (tables). */
markdown?: MarkdownConfig;
/** Allow channel-initiated config writes (default: true). */
configWrites?: boolean;
/** If false, do not start this Feishu account. Default: true. */
enabled?: boolean;
/** Feishu app id. */
appId?: string;
/** Feishu app secret. */
appSecret?: string;
/** Allow bot-authored messages to trigger replies (default: false). */
allowBots?: boolean;
/** Default mention requirement for group chats (default: true). */
requireMention?: boolean;
/**
* Controls how group chats are handled:
* - "open": chats bypass allowlists; mention-gating applies
* - "disabled": block all group chats
* - "allowlist": only allow chats present in channels.feishu.groups
*/
groupPolicy?: GroupPolicy;
/** Max group messages to keep as history context (0 disables). */
historyLimit?: number;
/** Max DM turns to keep as history context. */
dmHistoryLimit?: number;
/** Per-DM config overrides keyed by user ID. */
dms?: Record<string, DmConfig>;
/** Outbound text chunk size (chars). Default: 4000. */
textChunkLimit?: number;
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
chunkMode?: "length" | "newline";
/** Merge streamed block replies before sending. */
blockStreaming?: boolean;
/** Merge streamed block replies before sending. */
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
/** Max media size in MB. */
mediaMaxMb?: number;
/** Control reply threading when reply tags are present (off|first|all). */
replyToMode?: ReplyToMode;
dm?: FeishuDmConfig;
groups?: Record<string, FeishuGroupConfig>;
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
export type FeishuConfig = {
/** Optional per-account Feishu configuration (multi-account). */
accounts?: Record<string, FeishuAccountConfig>;
} & FeishuAccountConfig;

View File

@ -25,6 +25,7 @@ export type HookMappingConfig = {
| "whatsapp"
| "telegram"
| "discord"
| "feishu"
| "googlechat"
| "slack"
| "signal"

View File

@ -12,6 +12,7 @@ export type QueueModeByProvider = {
whatsapp?: QueueMode;
telegram?: QueueMode;
discord?: QueueMode;
feishu?: QueueMode;
googlechat?: QueueMode;
slack?: QueueMode;
signal?: QueueMode;

View File

@ -10,6 +10,7 @@ export * from "./types.channels.js";
export * from "./types.clawdbot.js";
export * from "./types.cron.js";
export * from "./types.discord.js";
export * from "./types.feishu.js";
export * from "./types.googlechat.js";
export * from "./types.gateway.js";
export * from "./types.hooks.js";

View File

@ -306,6 +306,7 @@ export const QueueModeBySurfaceSchema = z
whatsapp: QueueModeSchema.optional(),
telegram: QueueModeSchema.optional(),
discord: QueueModeSchema.optional(),
feishu: QueueModeSchema.optional(),
slack: QueueModeSchema.optional(),
mattermost: QueueModeSchema.optional(),
signal: QueueModeSchema.optional(),

View File

@ -23,6 +23,7 @@ export const HookMappingSchema = z
z.literal("whatsapp"),
z.literal("telegram"),
z.literal("discord"),
z.literal("feishu"),
z.literal("slack"),
z.literal("signal"),
z.literal("imessage"),

View File

@ -282,6 +282,67 @@ export const DiscordConfigSchema = DiscordAccountSchema.extend({
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
});
export const FeishuDmSchema = z
.object({
enabled: z.boolean().optional(),
policy: DmPolicySchema.optional().default("pairing"),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
})
.strict()
.superRefine((value, ctx) => {
requireOpenAllowFrom({
policy: value.policy,
allowFrom: value.allowFrom,
ctx,
path: ["allowFrom"],
message:
'channels.feishu.dm.policy="open" requires channels.feishu.dm.allowFrom to include "*"',
});
});
export const FeishuGroupSchema = z
.object({
enabled: z.boolean().optional(),
allow: z.boolean().optional(),
requireMention: z.boolean().optional(),
tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
users: z.array(z.union([z.string(), z.number()])).optional(),
systemPrompt: z.string().optional(),
})
.strict();
export const FeishuAccountSchema = z
.object({
name: z.string().optional(),
capabilities: z.array(z.string()).optional(),
markdown: MarkdownConfigSchema.optional(),
configWrites: z.boolean().optional(),
enabled: z.boolean().optional(),
appId: z.string().optional(),
appSecret: z.string().optional(),
allowBots: z.boolean().optional(),
requireMention: z.boolean().optional(),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(),
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
textChunkLimit: z.number().int().positive().optional(),
chunkMode: z.enum(["length", "newline"]).optional(),
blockStreaming: z.boolean().optional(),
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
mediaMaxMb: z.number().positive().optional(),
replyToMode: ReplyToModeSchema.optional(),
dm: FeishuDmSchema.optional(),
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict();
export const FeishuConfigSchema = FeishuAccountSchema.extend({
accounts: z.record(z.string(), FeishuAccountSchema.optional()).optional(),
});
export const GoogleChatDmSchema = z
.object({
enabled: z.boolean().optional(),

View File

@ -3,6 +3,7 @@ import { z } from "zod";
import {
BlueBubblesConfigSchema,
DiscordConfigSchema,
FeishuConfigSchema,
GoogleChatConfigSchema,
IMessageConfigSchema,
MSTeamsConfigSchema,
@ -30,6 +31,7 @@ export const ChannelsSchema = z
whatsapp: WhatsAppConfigSchema.optional(),
telegram: TelegramConfigSchema.optional(),
discord: DiscordConfigSchema.optional(),
feishu: FeishuConfigSchema.optional(),
googlechat: GoogleChatConfigSchema.optional(),
slack: SlackConfigSchema.optional(),
signal: SignalConfigSchema.optional(),

View File

@ -84,6 +84,7 @@ export type {
GroupToolPolicyBySenderConfig,
MarkdownConfig,
MarkdownTableMode,
FeishuConfig,
GoogleChatAccountConfig,
GoogleChatConfig,
GoogleChatDmConfig,
@ -96,6 +97,7 @@ export type {
} from "../config/types.js";
export {
DiscordConfigSchema,
FeishuConfigSchema,
GoogleChatConfigSchema,
IMessageConfigSchema,
MSTeamsConfigSchema,