181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
import {
|
|
DEFAULT_ACCOUNT_ID,
|
|
getChatChannelMeta,
|
|
type ChannelPlugin,
|
|
} from "../../../src/plugin-sdk/index.js";
|
|
|
|
// Import feishu functions directly from source
|
|
import {
|
|
getStartupChatIds,
|
|
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 to all configured startup chat IDs
|
|
const startupChatIds = getStartupChatIds(account.config);
|
|
const timestamp = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
|
|
for (const chatId of startupChatIds) {
|
|
try {
|
|
await client.sendTextMessage(
|
|
chatId,
|
|
`🚀 Clawdbot 飞书网关已启动 (${timestamp})`,
|
|
"chat_id",
|
|
);
|
|
ctx.log?.info(`[${account.accountId}] sent startup message to ${chatId}`);
|
|
} catch (err) {
|
|
ctx.log?.warn(`[${account.accountId}] failed to send startup message to ${chatId}: ${err}`);
|
|
}
|
|
}
|
|
|
|
return monitorPromise;
|
|
},
|
|
},
|
|
};
|