Merge 4c2889c6c6 into da71eaebd2
This commit is contained in:
commit
d7cfd283ab
11
extensions/feishu/clawdbot.plugin.json
Normal file
11
extensions/feishu/clawdbot.plugin.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "feishu",
|
||||
"name": "Feishu Channel",
|
||||
"version": "1.0.0",
|
||||
"channels": ["feishu"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
29
extensions/feishu/index.ts
Normal file
29
extensions/feishu/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Clawdbot 飞书通道插件入口
|
||||
*/
|
||||
|
||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||
import { feishuPlugin } from "./src/channel.js";
|
||||
import { setFeishuRuntime } from "./src/runtime.js";
|
||||
|
||||
export const plugin = {
|
||||
id: "feishu",
|
||||
name: "Feishu",
|
||||
description: "Feishu (Lark) messaging channel for Clawdbot",
|
||||
|
||||
register(api: ClawdbotPluginApi) {
|
||||
// 保存运行时引用
|
||||
setFeishuRuntime(api.runtime);
|
||||
|
||||
// 注册飞书通道
|
||||
api.registerChannel({ plugin: feishuPlugin });
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
|
||||
// 同时导出便于直接测试
|
||||
export { feishuPlugin } from "./src/channel.js";
|
||||
export * from "./src/types.js";
|
||||
export * from "./src/client.js";
|
||||
export * from "./src/gateway.js";
|
||||
35
extensions/feishu/package.json
Normal file
35
extensions/feishu/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "feishu",
|
||||
"version": "1.0.1",
|
||||
"description": "Feishu (Lark) channel plugin for Clawdbot",
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {},
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"],
|
||||
"channel": {
|
||||
"id": "feishu",
|
||||
"label": "Feishu",
|
||||
"selectionLabel": "Feishu (Lark)",
|
||||
"docsPath": "/channels/feishu",
|
||||
"docsLabel": "feishu",
|
||||
"blurb": "Connect to Feishu via official Feishu API",
|
||||
"order": 60,
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@moltbot/feishu",
|
||||
"localPath": "extensions/feishu",
|
||||
"defaultChoice": "npm"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "^1.46.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"clawdbot": "*"
|
||||
}
|
||||
}
|
||||
259
extensions/feishu/src/channel.ts
Normal file
259
extensions/feishu/src/channel.ts
Normal file
@ -0,0 +1,259 @@
|
||||
/**
|
||||
* 飞书通道插件定义
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChannelPlugin,
|
||||
ClawdbotConfig,
|
||||
ChannelOnboardingAdapter,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import type { ResolvedFeishuAccount, FeishuChannelConfig } from "./types.js";
|
||||
import { sendTextMessage } from "./client.js";
|
||||
import { startGateway } from "./gateway.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import type { MsgContext } from "./msg-context.js";
|
||||
|
||||
const DEFAULT_ACCOUNT_ID = "default";
|
||||
const CHANNEL_ID = "feishu" as const;
|
||||
|
||||
/**
|
||||
* 获取飞书通道配置
|
||||
*/
|
||||
function getFeishuConfig(cfg: ClawdbotConfig): FeishuChannelConfig | undefined {
|
||||
return (cfg as any).channels?.feishu as FeishuChannelConfig | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从配置中获取飞书账号列表(单账号模式)
|
||||
*/
|
||||
function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
const feishuCfg = getFeishuConfig(cfg);
|
||||
if (!feishuCfg || feishuCfg.enabled === false) return [];
|
||||
if (!feishuCfg.appId || !feishuCfg.appSecret) return [];
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析飞书账号配置(单账号模式)
|
||||
*/
|
||||
function resolveFeishuAccount(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string
|
||||
): ResolvedFeishuAccount | undefined {
|
||||
if (accountId !== DEFAULT_ACCOUNT_ID) return undefined;
|
||||
|
||||
const feishuCfg = getFeishuConfig(cfg);
|
||||
if (!feishuCfg) return undefined;
|
||||
|
||||
return {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
appId: feishuCfg.appId,
|
||||
appSecret: feishuCfg.appSecret,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书 Onboarding Adapter
|
||||
* 用于 clawdbot onboard 交互式配置向导
|
||||
*/
|
||||
const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel: CHANNEL_ID,
|
||||
|
||||
getStatus: async ({ cfg }) => {
|
||||
const feishuCfg = getFeishuConfig(cfg);
|
||||
const configured = !!(feishuCfg?.appId && feishuCfg?.appSecret);
|
||||
return {
|
||||
channel: CHANNEL_ID,
|
||||
configured,
|
||||
statusLines: [`Feishu: ${configured ? "configured" : "needs App ID & Secret"}`],
|
||||
selectionHint: configured ? "configured" : "needs credentials",
|
||||
};
|
||||
},
|
||||
|
||||
configure: async (ctx) => {
|
||||
const { cfg, prompter } = ctx;
|
||||
let next = cfg;
|
||||
const currentCfg = getFeishuConfig(cfg);
|
||||
const hasAppId = !!currentCfg?.appId;
|
||||
const hasAppSecret = !!currentCfg?.appSecret;
|
||||
|
||||
// 显示帮助信息
|
||||
await prompter.note(
|
||||
[
|
||||
"1) 登录飞书开放平台 → 创建企业自建应用",
|
||||
"2) 获取 App ID 和 App Secret",
|
||||
"3) 启用机器人能力,配置消息接收方式为「使用长连接接收消息」",
|
||||
"4) 发布应用并授权",
|
||||
"Docs: https://open.feishu.cn/document/home/develop-a-bot-in-5-minutes",
|
||||
].join("\n"),
|
||||
"飞书机器人配置"
|
||||
);
|
||||
|
||||
let appId: string | null = null;
|
||||
let appSecret: string | null = null;
|
||||
|
||||
// App ID
|
||||
if (hasAppId) {
|
||||
const keep = await prompter.confirm({
|
||||
message: `App ID 已配置 (${currentCfg!.appId.slice(0, 8)}...),是否保留?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keep) {
|
||||
appId = String(
|
||||
await prompter.text({
|
||||
message: "请输入飞书 App ID",
|
||||
validate: (value) => (value?.trim() ? undefined : "必填"),
|
||||
})
|
||||
).trim();
|
||||
}
|
||||
} else {
|
||||
appId = String(
|
||||
await prompter.text({
|
||||
message: "请输入飞书 App ID",
|
||||
validate: (value) => (value?.trim() ? undefined : "必填"),
|
||||
})
|
||||
).trim();
|
||||
}
|
||||
|
||||
// App Secret
|
||||
if (hasAppSecret) {
|
||||
const keep = await prompter.confirm({
|
||||
message: "App Secret 已配置,是否保留?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keep) {
|
||||
appSecret = String(
|
||||
await prompter.text({
|
||||
message: "请输入飞书 App Secret",
|
||||
validate: (value) => (value?.trim() ? undefined : "必填"),
|
||||
})
|
||||
).trim();
|
||||
}
|
||||
} else {
|
||||
appSecret = String(
|
||||
await prompter.text({
|
||||
message: "请输入飞书 App Secret",
|
||||
validate: (value) => (value?.trim() ? undefined : "必填"),
|
||||
})
|
||||
).trim();
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...(next as any).channels,
|
||||
feishu: {
|
||||
...(next as any).channels?.feishu,
|
||||
enabled: true,
|
||||
...(appId ? { appId } : {}),
|
||||
...(appSecret ? { appSecret } : {}),
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
||||
},
|
||||
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...(cfg as any).channels,
|
||||
feishu: { ...(cfg as any).channels?.feishu, enabled: false },
|
||||
},
|
||||
} as ClawdbotConfig),
|
||||
};
|
||||
|
||||
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
id: "feishu",
|
||||
|
||||
meta: {
|
||||
id: "feishu",
|
||||
label: "Feishu",
|
||||
selectionLabel: "Feishu (Lark)",
|
||||
docsPath: "https://open.feishu.cn/document",
|
||||
blurb: "Connect to Feishu via official Feishu API",
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
reactions: false,
|
||||
reply: true,
|
||||
media: true,
|
||||
},
|
||||
|
||||
// Onboarding 配置向导
|
||||
onboarding: feishuOnboardingAdapter,
|
||||
|
||||
config: {
|
||||
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveFeishuAccount(cfg, accountId),
|
||||
isConfigured: async (account) => !!(account.appId && account.appSecret),
|
||||
},
|
||||
|
||||
outbound: {
|
||||
deliveryMode: "gateway",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async (ctx) => {
|
||||
const account = ctx.account as ResolvedFeishuAccount;
|
||||
const result = await sendTextMessage(account, ctx.to, ctx.text);
|
||||
return {
|
||||
ok: result.ok,
|
||||
error: result.error ? new Error(result.error) : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
gateway: {
|
||||
startAccount: async (ctx) => {
|
||||
const runtime = getFeishuRuntime();
|
||||
const account = ctx.account;
|
||||
const cfg = ctx.cfg;
|
||||
|
||||
startGateway({
|
||||
account,
|
||||
abortSignal: ctx.abortSignal,
|
||||
onMessage: async (message) => {
|
||||
// 只处理文本消息
|
||||
if (message.messageType !== "text" || !message.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 打印收到的消息内容
|
||||
console.log(`[feishu:${account.accountId}] 收到消息: ${message.text}`);
|
||||
|
||||
// 构建消息上下文
|
||||
const msgCtx: MsgContext = {
|
||||
From: message.senderId,
|
||||
Body: message.text,
|
||||
AccountId: account.accountId,
|
||||
Provider: "feishu",
|
||||
Surface: "feishu",
|
||||
SessionKey: `feishu:${account.accountId}:${message.chatId}`,
|
||||
To: message.chatId,
|
||||
ChatType: message.chatType === "p2p" ? "direct" : "group",
|
||||
};
|
||||
|
||||
// 使用 dispatchReplyWithBufferedBlockDispatcher
|
||||
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: msgCtx,
|
||||
cfg,
|
||||
dispatcherOptions: {
|
||||
deliver: async (payload) => {
|
||||
// 发送 AI 回复(普通发送,非回复)
|
||||
const text = payload.text ?? "";
|
||||
if (text) {
|
||||
await sendTextMessage(account, message.chatId, text);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
logger: {
|
||||
info: (msg) => console.log(`[feishu:${account.accountId}] ${msg}`),
|
||||
error: (msg) => console.error(`[feishu:${account.accountId}] ${msg}`),
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
92
extensions/feishu/src/client.ts
Normal file
92
extensions/feishu/src/client.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 飞书消息客户端
|
||||
* 负责发送消息
|
||||
*/
|
||||
|
||||
import * as lark from "@larksuiteoapi/node-sdk";
|
||||
import type { ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
// 客户端缓存
|
||||
const clientCache = new Map<string, lark.Client>();
|
||||
|
||||
/**
|
||||
* 获取或创建飞书客户端
|
||||
*/
|
||||
export function getFeishuClient(account: ResolvedFeishuAccount): lark.Client {
|
||||
// 只使用 appId 作为缓存 key,避免敏感信息泄露
|
||||
const cacheKey = account.appId;
|
||||
|
||||
let client = clientCache.get(cacheKey);
|
||||
if (!client) {
|
||||
client = new lark.Client({
|
||||
appId: account.appId,
|
||||
appSecret: account.appSecret,
|
||||
});
|
||||
clientCache.set(cacheKey, client);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文本消息到会话
|
||||
*/
|
||||
export async function sendTextMessage(
|
||||
account: ResolvedFeishuAccount,
|
||||
chatId: string,
|
||||
text: string
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
const client = getFeishuClient(account);
|
||||
|
||||
try {
|
||||
const result = await client.im.v1.message.create({
|
||||
params: {
|
||||
receive_id_type: "chat_id",
|
||||
},
|
||||
data: {
|
||||
receive_id: chatId,
|
||||
msg_type: "text",
|
||||
content: JSON.stringify({ text }),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.code === 0) {
|
||||
return { ok: true };
|
||||
} else {
|
||||
return { ok: false, error: result.msg };
|
||||
}
|
||||
} catch (error) {
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复指定消息
|
||||
*/
|
||||
export async function replyTextMessage(
|
||||
account: ResolvedFeishuAccount,
|
||||
messageId: string,
|
||||
text: string
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
const client = getFeishuClient(account);
|
||||
|
||||
try {
|
||||
const result = await client.im.v1.message.reply({
|
||||
path: {
|
||||
message_id: messageId,
|
||||
},
|
||||
data: {
|
||||
msg_type: "text",
|
||||
content: JSON.stringify({ text }),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.code === 0) {
|
||||
return { ok: true };
|
||||
} else {
|
||||
return { ok: false, error: result.msg };
|
||||
}
|
||||
} catch (error) {
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
185
extensions/feishu/src/gateway.ts
Normal file
185
extensions/feishu/src/gateway.ts
Normal file
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 飞书长连接网关
|
||||
* 负责接收消息
|
||||
*/
|
||||
|
||||
import * as lark from "@larksuiteoapi/node-sdk";
|
||||
import type { ResolvedFeishuAccount, FeishuMessage } from "./types.js";
|
||||
import { sendTextMessage } from "./client.js";
|
||||
|
||||
// WebSocket 客户端缓存
|
||||
const wsClientCache = new Map<string, lark.WSClient>();
|
||||
|
||||
// 消息去重缓存 (messageId -> timestamp)
|
||||
const processedMessages = new Map<string, number>();
|
||||
const MESSAGE_DEDUPE_TTL_MS = 60 * 1000; // 60秒过期
|
||||
|
||||
// 消息过期时间(30分钟)
|
||||
// 如果消息发送时间距离当前时间超过此值,则不处理
|
||||
// 用于避免服务重启后处理一堆过时消息(飞书对未确认消息会重试4次)
|
||||
const MESSAGE_EXPIRE_TTL_MS = 30 * 60 * 1000; // 30分钟
|
||||
|
||||
/**
|
||||
* 清理过期的去重缓存
|
||||
*/
|
||||
function cleanupDedupeCache(): void {
|
||||
const now = Date.now();
|
||||
for (const [messageId, timestamp] of processedMessages) {
|
||||
if (now - timestamp > MESSAGE_DEDUPE_TTL_MS) {
|
||||
processedMessages.delete(messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否已处理过(前置去重)
|
||||
*/
|
||||
function isDuplicateMessage(messageId: string): boolean {
|
||||
if (processedMessages.has(messageId)) {
|
||||
return true;
|
||||
}
|
||||
processedMessages.set(messageId, Date.now());
|
||||
// 定期清理
|
||||
if (processedMessages.size > 100) {
|
||||
cleanupDedupeCache();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否已过期
|
||||
* @param createTimeMs 消息创建时间(毫秒时间戳字符串)
|
||||
* @returns true 表示消息已过期,应该丢弃
|
||||
*/
|
||||
function isMessageExpired(createTimeMs: string | undefined): boolean {
|
||||
if (!createTimeMs) {
|
||||
// 如果没有创建时间,默认不过期
|
||||
return false;
|
||||
}
|
||||
const createTime = parseInt(createTimeMs, 10);
|
||||
if (isNaN(createTime)) {
|
||||
return false;
|
||||
}
|
||||
const now = Date.now();
|
||||
return now - createTime > MESSAGE_EXPIRE_TTL_MS;
|
||||
}
|
||||
|
||||
export interface GatewayOptions {
|
||||
account: ResolvedFeishuAccount;
|
||||
onMessage: (message: FeishuMessage) => Promise<void>;
|
||||
abortSignal?: AbortSignal;
|
||||
logger?: {
|
||||
info: (msg: string) => void;
|
||||
error: (msg: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动飞书长连接网关
|
||||
*/
|
||||
export function startGateway(options: GatewayOptions): lark.WSClient {
|
||||
const { account, onMessage, abortSignal, logger } = options;
|
||||
const cacheKey = account.accountId;
|
||||
|
||||
// 如果已存在,先停止
|
||||
const existing = wsClientCache.get(cacheKey);
|
||||
if (existing) {
|
||||
stopGateway(cacheKey);
|
||||
}
|
||||
|
||||
const wsClient = new lark.WSClient({
|
||||
appId: account.appId,
|
||||
appSecret: account.appSecret,
|
||||
loggerLevel: lark.LoggerLevel.error,
|
||||
});
|
||||
|
||||
// 监听 abortSignal,支持框架优雅停止
|
||||
if (abortSignal) {
|
||||
abortSignal.addEventListener("abort", () => {
|
||||
logger?.info("received abort signal, stopping gateway");
|
||||
stopGateway(cacheKey);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
wsClient.start({
|
||||
eventDispatcher: new lark.EventDispatcher({}).register({
|
||||
"im.message.receive_v1": async (data) => {
|
||||
const message = data.message;
|
||||
if (!message) return {};
|
||||
|
||||
const messageId = message.message_id || "";
|
||||
const createTime = message.create_time;
|
||||
|
||||
// 前置去重检查
|
||||
if (isDuplicateMessage(messageId)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 检查消息是否过期(超过30分钟的消息不处理)
|
||||
// 用于避免服务重启后处理一堆过时消息
|
||||
if (isMessageExpired(createTime)) {
|
||||
logger?.info(`Skipping expired message ${message.content}, create_time: ${createTime}`)
|
||||
return {};
|
||||
}
|
||||
|
||||
const feishuMessage: FeishuMessage = {
|
||||
messageId,
|
||||
chatId: message.chat_id || "",
|
||||
chatType: message.chat_type === "p2p" ? "p2p" : "group",
|
||||
senderId: data.sender?.sender_id?.open_id || "",
|
||||
messageType: message.message_type || "",
|
||||
content: message.content || "",
|
||||
};
|
||||
|
||||
// 解析文本内容
|
||||
if (feishuMessage.messageType === "text") {
|
||||
try {
|
||||
const parsed = JSON.parse(feishuMessage.content);
|
||||
feishuMessage.text = parsed.text;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 异步处理,不阻塞返回
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await onMessage(feishuMessage);
|
||||
} catch (error) {
|
||||
logger?.error(`Error handling message: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 立即返回,避免飞书超时重推
|
||||
return {};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// 登录成功日志
|
||||
logger?.info(`logged in to feishu as ${account.appId}`);
|
||||
|
||||
wsClientCache.set(cacheKey, wsClient);
|
||||
return wsClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止网关
|
||||
*/
|
||||
export function stopGateway(accountId: string): void {
|
||||
const wsClient = wsClientCache.get(accountId);
|
||||
if (wsClient) {
|
||||
try {
|
||||
// 调用 SDK 提供的关闭方法(如果有的话)
|
||||
const client = wsClient as unknown as Record<string, unknown>;
|
||||
if (typeof client.close === "function") {
|
||||
(client.close as () => void)();
|
||||
} else if (typeof client.stop === "function") {
|
||||
(client.stop as () => void)();
|
||||
}
|
||||
} catch {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
wsClientCache.delete(accountId);
|
||||
}
|
||||
}
|
||||
14
extensions/feishu/src/msg-context.ts
Normal file
14
extensions/feishu/src/msg-context.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 消息上下文类型定义
|
||||
*/
|
||||
|
||||
export type MsgContext = {
|
||||
From: string;
|
||||
Body: string;
|
||||
AccountId: string;
|
||||
Provider: string;
|
||||
Surface: string;
|
||||
SessionKey: string;
|
||||
To: string;
|
||||
ChatType: "direct" | "group";
|
||||
};
|
||||
18
extensions/feishu/src/runtime.ts
Normal file
18
extensions/feishu/src/runtime.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 运行时上下文管理
|
||||
*/
|
||||
|
||||
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
|
||||
let feishuRuntime: PluginRuntime | null = null;
|
||||
|
||||
export function setFeishuRuntime(runtime: PluginRuntime) {
|
||||
feishuRuntime = runtime;
|
||||
}
|
||||
|
||||
export function getFeishuRuntime(): PluginRuntime {
|
||||
if (!feishuRuntime) {
|
||||
throw new Error("Feishu runtime not initialized");
|
||||
}
|
||||
return feishuRuntime;
|
||||
}
|
||||
28
extensions/feishu/src/types.ts
Normal file
28
extensions/feishu/src/types.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 飞书通道配置类型(单账号模式)
|
||||
*/
|
||||
|
||||
export interface FeishuChannelConfig {
|
||||
/** 是否启用 */
|
||||
enabled?: boolean;
|
||||
/** 飞书应用 App ID */
|
||||
appId: string;
|
||||
/** 飞书应用 App Secret */
|
||||
appSecret: string;
|
||||
}
|
||||
|
||||
export interface ResolvedFeishuAccount {
|
||||
accountId: string;
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
}
|
||||
|
||||
export interface FeishuMessage {
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
chatType: "p2p" | "group";
|
||||
senderId: string;
|
||||
messageType: string;
|
||||
content: string;
|
||||
text?: string;
|
||||
}
|
||||
14
extensions/feishu/tsconfig.json
Normal file
14
extensions/feishu/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["*.ts", "src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user