feat(channels): add Feishu (Lark) support
This commit is contained in:
parent
9688454a30
commit
963c5b5a7a
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@ -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
59
docs/channels/feishu.md
Normal 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`).
|
||||
@ -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).
|
||||
|
||||
18
extensions/feishu/index.ts
Normal file
18
extensions/feishu/index.ts
Normal 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;
|
||||
11
extensions/feishu/moltbot.plugin.json
Normal file
11
extensions/feishu/moltbot.plugin.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "feishu",
|
||||
"name": "Feishu",
|
||||
"description": "Feishu channel plugin",
|
||||
"channels": ["feishu"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
17
extensions/feishu/package.json
Normal file
17
extensions/feishu/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
||||
73
extensions/feishu/src/accounts.ts
Normal file
73
extensions/feishu/src/accounts.ts
Normal 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);
|
||||
}
|
||||
318
extensions/feishu/src/channel.ts
Normal file
318
extensions/feishu/src/channel.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
498
extensions/feishu/src/monitor.ts
Normal file
498
extensions/feishu/src/monitor.ts
Normal 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}`);
|
||||
};
|
||||
}
|
||||
62
extensions/feishu/src/policy.ts
Normal file
62
extensions/feishu/src/policy.ts
Normal 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;
|
||||
}
|
||||
14
extensions/feishu/src/runtime.ts
Normal file
14
extensions/feishu/src/runtime.ts
Normal 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;
|
||||
}
|
||||
83
extensions/feishu/src/send.ts
Normal file
83
extensions/feishu/src/send.ts
Normal 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 });
|
||||
}
|
||||
61
extensions/feishu/src/targets.ts
Normal file
61
extensions/feishu/src/targets.ts
Normal 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
224
pnpm-lock.yaml
generated
@ -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:
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -22,6 +22,8 @@ export type ChannelSetupInput = {
|
||||
tokenFile?: string;
|
||||
botToken?: string;
|
||||
appToken?: string;
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
signalNumber?: string;
|
||||
cliPath?: string;
|
||||
dbPath?: string;
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
63
src/config/config.feishu.test.ts
Normal file
63
src/config/config.feishu.test.ts
Normal 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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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.",
|
||||
|
||||
@ -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;
|
||||
|
||||
90
src/config/types.feishu.ts
Normal file
90
src/config/types.feishu.ts
Normal 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;
|
||||
@ -25,6 +25,7 @@ export type HookMappingConfig = {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "feishu"
|
||||
| "googlechat"
|
||||
| "slack"
|
||||
| "signal"
|
||||
|
||||
@ -12,6 +12,7 @@ export type QueueModeByProvider = {
|
||||
whatsapp?: QueueMode;
|
||||
telegram?: QueueMode;
|
||||
discord?: QueueMode;
|
||||
feishu?: QueueMode;
|
||||
googlechat?: QueueMode;
|
||||
slack?: QueueMode;
|
||||
signal?: QueueMode;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user