feat: add Feishu (Lark) channel plugin
This commit is contained in:
parent
9025da2296
commit
f8abce1740
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