openclaw/extensions/feishu/src/channel.ts
2026-01-27 21:49:13 +08:00

180 lines
5.8 KiB
TypeScript

import {
DEFAULT_ACCOUNT_ID,
getChatChannelMeta,
type ChannelPlugin,
} from "../../../src/plugin-sdk/index.js";
// Import feishu functions directly from source
import {
listFeishuAccountIds,
resolveFeishuAccount,
resolveDefaultFeishuAccountId,
type ResolvedFeishuAccount,
} from "../../../src/feishu/accounts.js";
import { sendMessageFeishu, type SendFeishuMessageParams } from "../../../src/feishu/send.js";
import { monitorFeishuProvider, type FeishuMessageContext } from "../../../src/feishu/monitor.js";
import { createFeishuClient } from "../../../src/feishu/client.js";
import { dispatchFeishuMessage } from "../../../src/feishu/message-dispatch.js";
const meta = getChatChannelMeta("feishu");
/**
* Detect the appropriate receiveIdType from Feishu target ID prefix
* - ou_xxx → open_id (user)
* - oc_xxx → chat_id (group chat)
* - on_xxx → union_id (cross-app user id)
*/
function detectFeishuReceiveIdType(
target: string,
): SendFeishuMessageParams["receiveIdType"] {
const trimmed = target.trim().toLowerCase();
if (trimmed.startsWith("ou_")) return "open_id";
if (trimmed.startsWith("on_")) return "union_id";
// Default to chat_id for oc_ prefix or unknown formats
return "chat_id";
}
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
id: "feishu",
meta: {
...meta,
aliases: ["lark"],
},
capabilities: {
chatTypes: ["direct", "channel"],
reactions: true,
media: true,
},
config: {
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
isConfigured: (account) => account.credentials.source !== "none",
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentials.source !== "none",
}),
},
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,
}),
buildAccountSnapshot: ({ account, runtime }) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentials.source !== "none",
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
}),
},
messaging: {
targetResolver: {
hint: "Use ou_xxx (open_id), oc_xxx (chat_id), or on_xxx (union_id)",
// Recognize Feishu ID patterns: ou_ (open_id), oc_ (chat_id), on_ (union_id)
looksLikeId: (raw: string) => {
const trimmed = raw.trim();
return /^(ou_|oc_|on_)[a-z0-9]+$/i.test(trimmed);
},
},
},
outbound: {
deliveryMode: "direct",
chunker: null,
textChunkLimit: 4000,
sendText: async ({ to, text, accountId, cfg }) => {
// Detect receiveIdType from target prefix
const receiveIdType = detectFeishuReceiveIdType(to);
const result = await sendMessageFeishu({
to,
text,
accountId: accountId ?? undefined,
config: cfg,
receiveIdType,
});
if (!result.success || !result.messageId) {
throw new Error(result.error ?? "Failed to send Feishu message");
}
return { channel: "feishu" as const, messageId: result.messageId };
},
sendMedia: async ({ to, text, accountId, cfg }) => {
// Feishu media sending requires separate API - for now just send text
const receiveIdType = detectFeishuReceiveIdType(to);
const result = await sendMessageFeishu({
to,
text,
accountId: accountId ?? undefined,
config: cfg,
receiveIdType,
});
if (!result.success || !result.messageId) {
throw new Error(result.error ?? "Failed to send Feishu message");
}
return { channel: "feishu" as const, messageId: result.messageId };
},
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
ctx.log?.info(`[${account.accountId}] starting Feishu provider`);
// Create client for startup message
const client = createFeishuClient(account.credentials, {
timeoutMs: (account.config.timeoutSeconds ?? 30) * 1000,
});
// Message handler that dispatches to the agent system
const onMessage = async (msgCtx: FeishuMessageContext) => {
await dispatchFeishuMessage({
ctx: msgCtx,
cfg: ctx.cfg,
runtime: ctx.runtime,
account,
});
};
// Start the monitor with message handler
const monitorPromise = monitorFeishuProvider({
accountId: account.accountId,
config: ctx.cfg,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
onMessage,
});
// Send startup message if configured
const startupChatId = account.config.startupChatId;
if (startupChatId) {
try {
const timestamp = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
await client.sendTextMessage(
startupChatId,
`🚀 Clawdbot 飞书网关已启动 (${timestamp})`,
"chat_id",
);
ctx.log?.info(`[${account.accountId}] sent startup message to ${startupChatId}`);
} catch (err) {
ctx.log?.warn(`[${account.accountId}] failed to send startup message: ${err}`);
}
}
return monitorPromise;
},
},
};