Merge f8abce1740 into da71eaebd2
This commit is contained in:
commit
9fa66c4131
72
extensions/feishu/clawdbot.plugin.json
Normal file
72
extensions/feishu/clawdbot.plugin.json
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"id": "feishu",
|
||||||
|
"name": "Feishu (Lark)",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Feishu/Lark messaging integration for Moltbot",
|
||||||
|
"kind": "channel",
|
||||||
|
"channels": ["feishu"],
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"appId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feishu app ID"
|
||||||
|
},
|
||||||
|
"appSecret": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feishu app secret"
|
||||||
|
},
|
||||||
|
"encryptKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feishu encrypt key"
|
||||||
|
},
|
||||||
|
"verificationToken": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feishu verification token"
|
||||||
|
},
|
||||||
|
"webhookPath": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Webhook path for Feishu events"
|
||||||
|
},
|
||||||
|
"dmPolicy": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Direct message policy"
|
||||||
|
},
|
||||||
|
"allowFrom": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Allowed senders"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uiHints": {
|
||||||
|
"appId": {
|
||||||
|
"label": "App ID",
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"appSecret": {
|
||||||
|
"label": "App Secret",
|
||||||
|
"sensitive": true
|
||||||
|
},
|
||||||
|
"encryptKey": {
|
||||||
|
"label": "Encrypt Key",
|
||||||
|
"sensitive": true
|
||||||
|
},
|
||||||
|
"verificationToken": {
|
||||||
|
"label": "Verification Token",
|
||||||
|
"sensitive": true
|
||||||
|
},
|
||||||
|
"webhookPath": {
|
||||||
|
"label": "Webhook Path",
|
||||||
|
"placeholder": "/webhook/feishu"
|
||||||
|
},
|
||||||
|
"dmPolicy": {
|
||||||
|
"label": "DM Policy",
|
||||||
|
"placeholder": "pairing"
|
||||||
|
},
|
||||||
|
"allowFrom": {
|
||||||
|
"label": "Allow From",
|
||||||
|
"placeholder": "[\"*\"]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
extensions/feishu/index.ts
Normal file
34
extensions/feishu/index.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { MoltbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
import { setGlobalDispatcher, ProxyAgent } from "undici";
|
||||||
|
|
||||||
|
// --- FORCE PROXY FOR GEMINI (If Env Var Set) ---
|
||||||
|
try {
|
||||||
|
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
||||||
|
if (proxyUrl) {
|
||||||
|
console.log(`[Feishu Plugin] Detected proxy env var, setting global proxy to: ${proxyUrl}`);
|
||||||
|
const dispatcher = new ProxyAgent(proxyUrl);
|
||||||
|
setGlobalDispatcher(dispatcher);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Feishu Plugin] Failed to set proxy: ${err}`);
|
||||||
|
}
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
|
import { feishuPlugin } from "./src/channel.js";
|
||||||
|
import { setFeishuRuntime } from "./src/runtime.js";
|
||||||
|
import { registerFeishuWebhook } from "./src/monitor.js";
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
id: "feishu",
|
||||||
|
name: "Feishu (Lark)",
|
||||||
|
description: "Feishu/Lark messaging integration",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
|
register(api: MoltbotPluginApi) {
|
||||||
|
setFeishuRuntime(api.runtime);
|
||||||
|
api.registerChannel({ plugin: feishuPlugin });
|
||||||
|
registerFeishuWebhook(api);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
||||||
179
extensions/feishu/src/channel.ts
Normal file
179
extensions/feishu/src/channel.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_ACCOUNT_ID,
|
||||||
|
deleteAccountFromConfigSection,
|
||||||
|
getChatChannelMeta,
|
||||||
|
type ChannelPlugin,
|
||||||
|
type ResolvedAccount,
|
||||||
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
import { getFeishuRuntime } from "./runtime.js";
|
||||||
|
import { monitorFeishuProvider, sendMessageFeishu } from "./monitor.js";
|
||||||
|
|
||||||
|
const meta = getChatChannelMeta("feishu");
|
||||||
|
|
||||||
|
export const feishuPlugin: ChannelPlugin<ResolvedAccount> = {
|
||||||
|
id: "feishu",
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
quickstartAllowFrom: true,
|
||||||
|
},
|
||||||
|
pairing: {
|
||||||
|
idLabel: "feishuUserId",
|
||||||
|
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|fs):/i, ""),
|
||||||
|
notifyApproval: async ({ id }) => {
|
||||||
|
console.log(`[Feishu Plugin] Notifying user ${id} of approval`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
chatTypes: ["direct", "group"],
|
||||||
|
reactions: false,
|
||||||
|
threads: false,
|
||||||
|
media: false,
|
||||||
|
nativeCommands: false,
|
||||||
|
blockStreaming: false,
|
||||||
|
},
|
||||||
|
reload: { configPrefixes: ["channels.feishu"] },
|
||||||
|
configSchema: {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
enabled: { type: "boolean" },
|
||||||
|
appId: { type: "string" },
|
||||||
|
appSecret: { type: "string" },
|
||||||
|
encryptKey: { type: "string" },
|
||||||
|
verificationToken: { type: "string" },
|
||||||
|
webhookPath: { type: "string" },
|
||||||
|
dmPolicy: { type: "string", enum: ["pairing", "open", "allowlist"] },
|
||||||
|
allowFrom: { type: "array", items: { type: "string" } },
|
||||||
|
groupPolicy: { type: "string", enum: ["open", "allowlist"] },
|
||||||
|
groupAllowFrom: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
listAccountIds: (cfg) => {
|
||||||
|
const accounts = [];
|
||||||
|
if (cfg.channels?.feishu) {
|
||||||
|
accounts.push("default");
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
},
|
||||||
|
resolveAccount: (cfg, accountId) => {
|
||||||
|
return cfg.channels?.feishu || {};
|
||||||
|
},
|
||||||
|
defaultAccountId: (cfg) => {
|
||||||
|
return cfg.channels?.feishu ? "default" : undefined;
|
||||||
|
},
|
||||||
|
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||||
|
const feishuConfig = cfg.channels?.feishu || {};
|
||||||
|
if (accountId === "default") {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
feishu: {
|
||||||
|
...feishuConfig,
|
||||||
|
enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return cfg;
|
||||||
|
},
|
||||||
|
deleteAccount: ({ cfg, accountId }) =>
|
||||||
|
deleteAccountFromConfigSection({
|
||||||
|
cfg,
|
||||||
|
sectionKey: "feishu",
|
||||||
|
accountId,
|
||||||
|
clearBaseFields: ["appId", "appSecret", "encryptKey", "verificationToken", "webhookPath"],
|
||||||
|
}),
|
||||||
|
isConfigured: (account) => Boolean(account.appId?.trim() && account.appSecret?.trim()),
|
||||||
|
describeAccount: (account) => ({
|
||||||
|
accountId: account.accountId,
|
||||||
|
enabled: account.enabled,
|
||||||
|
configured: Boolean(account.appId?.trim() && account.appSecret?.trim()),
|
||||||
|
}),
|
||||||
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
||||||
|
const account = cfg.channels?.feishu || {};
|
||||||
|
return account.allowFrom ?? [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||||
|
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||||
|
const feishuConfig = cfg.channels?.feishu || {};
|
||||||
|
return {
|
||||||
|
policy: feishuConfig.dmPolicy ?? "pairing",
|
||||||
|
allowFrom: feishuConfig.allowFrom ?? [],
|
||||||
|
allowFromPath: "channels.feishu.allowFrom",
|
||||||
|
normalizeEntry: (raw) => raw.replace(/^(feishu|fs):/i, ""),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
resolveRequireMention: ({ cfg, accountId, account }) => {
|
||||||
|
const feishuConfig = cfg.channels?.feishu || {};
|
||||||
|
return feishuConfig.groupPolicy === "allowlist";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
defaultRuntime: {
|
||||||
|
accountId: DEFAULT_ACCOUNT_ID,
|
||||||
|
running: false,
|
||||||
|
lastStartAt: null,
|
||||||
|
lastStopAt: null,
|
||||||
|
lastError: null,
|
||||||
|
},
|
||||||
|
buildAccountSnapshot: ({ account, runtime }) => {
|
||||||
|
const configured = Boolean(account.appId?.trim() && account.appSecret?.trim());
|
||||||
|
return {
|
||||||
|
accountId: account.accountId,
|
||||||
|
enabled: account.enabled,
|
||||||
|
configured,
|
||||||
|
running: runtime?.running ?? false,
|
||||||
|
lastStartAt: runtime?.lastStartAt ?? null,
|
||||||
|
lastStopAt: runtime?.lastStopAt ?? null,
|
||||||
|
lastError: runtime?.lastError ?? null,
|
||||||
|
lastInboundAt: runtime?.lastInboundAt ?? null,
|
||||||
|
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
deliveryMode: "direct",
|
||||||
|
chunker: null,
|
||||||
|
textChunkLimit: 2000,
|
||||||
|
sendText: async ({ to, text, accountId, cfg }) => {
|
||||||
|
const account = cfg.channels?.feishu || {};
|
||||||
|
const result = await sendMessageFeishu(
|
||||||
|
to,
|
||||||
|
text,
|
||||||
|
account.appId,
|
||||||
|
account.appSecret,
|
||||||
|
);
|
||||||
|
return { channel: "feishu", ...result };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
startAccount: async (ctx) => {
|
||||||
|
const account = ctx.account;
|
||||||
|
const cfg = ctx.cfg;
|
||||||
|
const feishuConfig = cfg.channels?.feishu || {};
|
||||||
|
|
||||||
|
ctx.log?.info(`[${account.accountId}] starting Feishu provider`);
|
||||||
|
|
||||||
|
return monitorFeishuProvider({
|
||||||
|
appId: feishuConfig.appId,
|
||||||
|
appSecret: feishuConfig.appSecret,
|
||||||
|
encryptKey: feishuConfig.encryptKey,
|
||||||
|
webhookPath: feishuConfig.webhookPath || "/feishu/events",
|
||||||
|
accountId: account.accountId,
|
||||||
|
config: cfg,
|
||||||
|
runtime: ctx.runtime,
|
||||||
|
abortSignal: ctx.abortSignal,
|
||||||
|
statusSink: (patch) => ctx.setStatus({ accountId: account.accountId, ...patch }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
280
extensions/feishu/src/monitor.ts
Normal file
280
extensions/feishu/src/monitor.ts
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import type { MoltbotConfig } from "clawdbot/plugin-sdk";
|
||||||
|
import { dispatchReplyWithBufferedBlockDispatcher } from "../../../src/auto-reply/reply/provider-dispatcher.js";
|
||||||
|
import { resolveEffectiveMessagesConfig } from "../../../src/agents/identity.js";
|
||||||
|
|
||||||
|
class FeishuCipher {
|
||||||
|
encryptKey;
|
||||||
|
constructor(encryptKey) {
|
||||||
|
this.encryptKey = encryptKey;
|
||||||
|
}
|
||||||
|
decrypt(encrypted) {
|
||||||
|
const key = crypto.createHash("sha256").update(this.encryptKey).digest();
|
||||||
|
const encryptedBuffer = Buffer.from(encrypted, "base64");
|
||||||
|
const iv = encryptedBuffer.subarray(0, 16);
|
||||||
|
const content = encryptedBuffer.subarray(16);
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
||||||
|
decipher.setAutoPadding(false);
|
||||||
|
let decrypted = decipher.update(content);
|
||||||
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||||
|
const pad = decrypted[decrypted.length - 1];
|
||||||
|
if (pad < 1 || pad > 32) {
|
||||||
|
return decrypted.toString("utf8");
|
||||||
|
}
|
||||||
|
return decrypted.subarray(0, decrypted.length - pad).toString("utf8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonBody(req: IncomingMessage, maxBytes: number) {
|
||||||
|
const chunks = [];
|
||||||
|
let total = 0;
|
||||||
|
return await new Promise<{ ok: boolean; value?: unknown; error?: string }>((resolve) => {
|
||||||
|
req.on("data", (chunk: Buffer) => {
|
||||||
|
total += chunk.length;
|
||||||
|
if (total > maxBytes) {
|
||||||
|
resolve({ ok: false, error: "payload too large" });
|
||||||
|
req.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chunks.push(chunk);
|
||||||
|
});
|
||||||
|
req.on("end", () => {
|
||||||
|
try {
|
||||||
|
const raw = Buffer.concat(chunks).toString("utf8");
|
||||||
|
if (!raw.trim()) {
|
||||||
|
resolve({ ok: false, error: "empty payload" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({ ok: true, value: JSON.parse(raw) as unknown });
|
||||||
|
} catch (err) {
|
||||||
|
resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.on("error", (err) => {
|
||||||
|
resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFeishuAccessToken(appId: string, appSecret: string) {
|
||||||
|
const response = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
app_id: appId,
|
||||||
|
app_secret: appSecret,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(`Failed to get Feishu access token: ${response.status} ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.tenant_access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMessageFeishu(chatId: string, text: string, appId: string, appSecret: string) {
|
||||||
|
const accessToken = await getFeishuAccessToken(appId, appSecret);
|
||||||
|
|
||||||
|
const response = await fetch("https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
receive_id: chatId,
|
||||||
|
content: JSON.stringify({ text }),
|
||||||
|
msg_type: "text",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(`Failed to send Feishu message: ${response.status} ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function monitorFeishuProvider(opts: {
|
||||||
|
appId: string;
|
||||||
|
appSecret: string;
|
||||||
|
encryptKey?: string;
|
||||||
|
webhookPath: string;
|
||||||
|
accountId: string;
|
||||||
|
config: MoltbotConfig;
|
||||||
|
runtime: any;
|
||||||
|
abortSignal: AbortSignal;
|
||||||
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||||
|
}) {
|
||||||
|
const { appId, appSecret, encryptKey, webhookPath, accountId, config, runtime, abortSignal, statusSink } = opts;
|
||||||
|
|
||||||
|
console.log(`[${accountId}] Starting Feishu provider`);
|
||||||
|
|
||||||
|
statusSink?.({ running: true, lastStartAt: Date.now() });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
abortSignal.addEventListener("abort", () => {
|
||||||
|
console.log(`[${accountId}] Stopping Feishu provider`);
|
||||||
|
statusSink?.({ running: false, lastStopAt: Date.now() });
|
||||||
|
resolve();
|
||||||
|
}, { once: true });
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[${accountId}] Feishu provider error: ${String(err)}`);
|
||||||
|
statusSink?.({ running: false, lastError: String(err) });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerFeishuWebhook(api: any) {
|
||||||
|
api.registerHttpRoute({
|
||||||
|
path: "/feishu/events",
|
||||||
|
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
const config = api.runtime.config.loadConfig();
|
||||||
|
const feishuConfig = config.channels?.feishu;
|
||||||
|
|
||||||
|
if (!feishuConfig) {
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Feishu not configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
res.writeHead(405, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Method Not Allowed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyResult = await readJsonBody(req, 1024 * 1024);
|
||||||
|
if (!bodyResult.ok) {
|
||||||
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
||||||
|
res.end(`Bad Request: ${bodyResult.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = bodyResult.value as any;
|
||||||
|
console.log(`[Feishu Plugin] Received webhook event: ${JSON.stringify(body)}`);
|
||||||
|
|
||||||
|
if (body.challenge) {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ challenge: body.challenge }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.encrypt) {
|
||||||
|
const cipher = new FeishuCipher(feishuConfig.encryptKey);
|
||||||
|
const decrypted = cipher.decrypt(body.encrypt);
|
||||||
|
body = JSON.parse(decrypted);
|
||||||
|
console.log(`[Feishu Plugin] Decrypted webhook event: ${JSON.stringify(body)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((body.header && body.header.event_type === "im.message.receive_v1") || (body.event && body.event.type === "im.message.receive_v1")) {
|
||||||
|
const message = body.event.message;
|
||||||
|
const sender = body.event.sender;
|
||||||
|
|
||||||
|
if (!message || !sender) {
|
||||||
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Bad Request: missing message or sender");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderId = sender.sender_id.user_id || sender.sender_id.open_id || sender.sender_id.union_id;
|
||||||
|
const messageContent = message.content;
|
||||||
|
const text = JSON.parse(messageContent).text || "";
|
||||||
|
const messageId = message.message_id;
|
||||||
|
const chatId = message.chat_id;
|
||||||
|
|
||||||
|
console.log(`[Feishu Plugin] Sender ID: ${senderId}`);
|
||||||
|
console.log(`[Feishu Plugin] Text: ${text}`);
|
||||||
|
console.log(`[Feishu Plugin] Message ID: ${messageId}, Chat ID: ${chatId}`);
|
||||||
|
console.log(`[Feishu Plugin] Dispatching message to agent...`);
|
||||||
|
|
||||||
|
const allowFrom = feishuConfig.allowFrom || ["*"];
|
||||||
|
const dmPolicy = feishuConfig.dmPolicy || "pairing";
|
||||||
|
|
||||||
|
if (dmPolicy !== "open" && !allowFrom.includes("*") && !allowFrom.includes(senderId)) {
|
||||||
|
console.log(`[Feishu Plugin] Sender ${senderId} not allowed`);
|
||||||
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctxPayload = {
|
||||||
|
From: senderId,
|
||||||
|
To: chatId,
|
||||||
|
Body: text,
|
||||||
|
BodyForAgent: text,
|
||||||
|
RawBody: text,
|
||||||
|
CommandBody: text,
|
||||||
|
BodyForCommands: text,
|
||||||
|
SessionKey: `feishu:${chatId}`,
|
||||||
|
MessageSid: messageId,
|
||||||
|
MessageSidFull: messageId,
|
||||||
|
ChatType: message.chat_type === "p2p" ? "direct" : "group",
|
||||||
|
Provider: "feishu",
|
||||||
|
Surface: "feishu",
|
||||||
|
OriginatingChannel: "feishu",
|
||||||
|
OriginatingTo: chatId,
|
||||||
|
AccountId: "default",
|
||||||
|
SenderId: senderId,
|
||||||
|
BodyStripped: text,
|
||||||
|
IsCommand: false,
|
||||||
|
CommandSource: "native",
|
||||||
|
CommandTargetSessionKey: `feishu:${chatId}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[Feishu Plugin] Calling dispatchReplyWithBufferedBlockDispatcher...`);
|
||||||
|
await dispatchReplyWithBufferedBlockDispatcher({
|
||||||
|
ctx: ctxPayload,
|
||||||
|
cfg: config,
|
||||||
|
dispatcherOptions: {
|
||||||
|
responsePrefix: resolveEffectiveMessagesConfig(config, "default").responsePrefix,
|
||||||
|
deliver: async (payload, _info) => {
|
||||||
|
console.log(`[Feishu Plugin] Received reply payload: ${JSON.stringify(payload)}`);
|
||||||
|
const replyContent = payload.text || "";
|
||||||
|
if (replyContent && feishuConfig.appId && feishuConfig.appSecret) {
|
||||||
|
try {
|
||||||
|
console.log(`[Feishu Plugin] Sending reply to ${chatId}: ${replyContent}`);
|
||||||
|
const result = await sendMessageFeishu(chatId, replyContent, feishuConfig.appId, feishuConfig.appSecret);
|
||||||
|
console.log(`[Feishu Plugin] Reply sent successfully: ${JSON.stringify(result)}`);
|
||||||
|
} catch (replyError) {
|
||||||
|
console.error(`[Feishu Plugin] Error sending AI reply: ${replyError.message}`);
|
||||||
|
}
|
||||||
|
} else if (!replyContent) {
|
||||||
|
console.log(`[Feishu Plugin] No reply content to send`);
|
||||||
|
} else if (!feishuConfig.appId || !feishuConfig.appSecret) {
|
||||||
|
console.log(`[Feishu Plugin] Missing Feishu appId or appSecret`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err, info) => {
|
||||||
|
console.error(`[Feishu Plugin] Auto-reply error callback: ${String(err)} (${info.kind})`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`[Feishu Plugin] dispatchReplyWithBufferedBlockDispatcher finished.`);
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ success: true }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Feishu Plugin] Critical error in dispatcher: ${error.message}\n${error.stack}`);
|
||||||
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||||
|
res.end(`Internal Server Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ success: true }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user