feat: 添加MinIO文件系统支持并优化外部连接器功能
- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等) - 更新外部连接器配置结构,包括BASE_URL和认证令牌设置 - 改进connector provider支持更多类型(official, feishu_bot等) - 实现Mistral模型推理模式支持reasoning_effort参数 - 增强外部连接器策略配置和运行时配置管理 - 添加connector bridge事件验证和安全保护机制 - 优化任务路由逻辑,区分simple_chat和new_task场景 - 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
216
external-connector/external_connector/node/feishu_event_utils.js
Normal file
216
external-connector/external_connector/node/feishu_event_utils.js
Normal file
@ -0,0 +1,216 @@
|
||||
function bridgeEventFromFeishu(data, env) {
|
||||
const message = objectValue(data.message);
|
||||
const sender = objectValue(data.sender);
|
||||
const senderId = objectValue(sender.sender_id);
|
||||
const isGroup = message.chat_type === "group";
|
||||
const peerId = isGroup
|
||||
? stringValue(message.chat_id || "")
|
||||
: stringValue(senderId.open_id || senderId.user_id || "");
|
||||
const userId = stringValue(senderId.open_id || senderId.user_id || "");
|
||||
const messageId = stringValue(message.message_id || randomId());
|
||||
const eventId = stringValue(data.event_id || data.eventId || `${env.channelId}:${messageId}`);
|
||||
return {
|
||||
eventId,
|
||||
timestamp: new Date().toISOString(),
|
||||
deliveryAttempt: 1,
|
||||
connectionId: env.connectionId,
|
||||
channelId: env.channelId,
|
||||
kind: "feishu",
|
||||
accountId: env.accountId,
|
||||
peerId,
|
||||
peerType: isGroup ? "group" : "dm",
|
||||
userId,
|
||||
threadId: stringValue(message.chat_id || "") || null,
|
||||
messageId,
|
||||
messageType: stringValue(message.message_type || "text"),
|
||||
content: extractText(message),
|
||||
metadata: {
|
||||
chatId: message.chat_id || null,
|
||||
rawMessageType: message.message_type || null,
|
||||
senderType: sender.sender_type || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function bridgeEventFromNormalizedMessage(message, env, options = {}) {
|
||||
const maxMessageChars = positiveInt(options.maxMessageChars, 20000);
|
||||
const content = normalizedContent(message).trim();
|
||||
if (!content || content.length > maxMessageChars) {
|
||||
return null;
|
||||
}
|
||||
const isGroup = message.chatType === "group";
|
||||
const messageId = stringValue(message.messageId || randomId());
|
||||
const chatId = stringValue(message.chatId || "");
|
||||
const senderId = stringValue(message.senderId || "");
|
||||
const eventId = stringValue(rawEventId(message.raw) || `${env.channelId}:${messageId}`);
|
||||
return {
|
||||
eventId,
|
||||
timestamp: new Date().toISOString(),
|
||||
deliveryAttempt: 1,
|
||||
connectionId: env.connectionId,
|
||||
channelId: env.channelId,
|
||||
kind: "feishu",
|
||||
accountId: env.accountId,
|
||||
peerId: isGroup ? chatId : senderId,
|
||||
peerType: isGroup ? "group" : "dm",
|
||||
userId: senderId,
|
||||
threadId: chatId || null,
|
||||
messageId,
|
||||
messageType: stringValue(message.rawContentType || "text"),
|
||||
content,
|
||||
metadata: {
|
||||
chatId: chatId || null,
|
||||
rawMessageType: message.rawContentType || null,
|
||||
senderName: message.senderName || null,
|
||||
mentions: Array.isArray(message.mentions) ? message.mentions : [],
|
||||
mentionAll: Boolean(message.mentionAll),
|
||||
mentionedBot: Boolean(message.mentionedBot),
|
||||
resources: Array.isArray(message.resources) ? message.resources : [],
|
||||
createTime: message.createTime || null,
|
||||
rootId: message.rootId || null,
|
||||
threadId: message.threadId || null,
|
||||
replyToMessageId: message.replyToMessageId || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildChannelOptions({ appId, appSecret, domain, policy }) {
|
||||
const resolvedPolicy = policy || parsePolicyEnv(process.env);
|
||||
return {
|
||||
appId,
|
||||
appSecret,
|
||||
domain,
|
||||
includeRawEvent: true,
|
||||
source: "beaver",
|
||||
handshakeTimeoutMs: 15000,
|
||||
wsConfig: { pingTimeout: 10 },
|
||||
policy: {
|
||||
requireMention: resolvedPolicy.requireMentionInGroups,
|
||||
respondToMentionAll: resolvedPolicy.respondToMentionAll,
|
||||
dmMode: resolvedPolicy.dmMode,
|
||||
dmAllowlist: resolvedPolicy.allowFrom,
|
||||
groupAllowlist: resolvedPolicy.groupAllowFrom,
|
||||
},
|
||||
safety: {
|
||||
chatQueue: { enabled: true },
|
||||
staleMessageWindowMs: positiveInt(resolvedPolicy.staleMessageWindowMs, 10 * 60 * 1000),
|
||||
batch: {
|
||||
text: {
|
||||
delayMs: positiveInt(resolvedPolicy.textBatchDelayMs, 0),
|
||||
maxMessages: positiveInt(resolvedPolicy.textBatchMaxMessages, 10),
|
||||
maxChars: positiveInt(resolvedPolicy.textBatchMaxChars, resolvedPolicy.maxMessageChars),
|
||||
},
|
||||
},
|
||||
},
|
||||
outbound: {
|
||||
textChunkLimit: resolvedPolicy.maxMessageChars,
|
||||
retry: { maxAttempts: 3, baseDelayMs: 500 },
|
||||
ssrfGuard: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parsePolicyEnv(env) {
|
||||
return {
|
||||
requireMentionInGroups: envBool(env.FEISHU_REQUIRE_MENTION_IN_GROUPS, true),
|
||||
respondToMentionAll: envBool(env.FEISHU_RESPOND_TO_MENTION_ALL, false),
|
||||
dmMode: stringValue(env.FEISHU_DM_MODE || "open") || "open",
|
||||
allowFrom: envList(env.FEISHU_ALLOW_FROM || env.FEISHU_DM_ALLOW_FROM || ""),
|
||||
groupAllowFrom: envList(env.FEISHU_GROUP_ALLOW_FROM || ""),
|
||||
maxMessageChars: positiveInt(env.FEISHU_MAX_MESSAGE_CHARS, 20000),
|
||||
textBatchDelayMs: positiveInt(env.FEISHU_TEXT_BATCH_DELAY_MS, 0),
|
||||
textBatchMaxMessages: positiveInt(env.FEISHU_TEXT_BATCH_MAX_MESSAGES, 10),
|
||||
textBatchMaxChars: positiveInt(env.FEISHU_TEXT_BATCH_MAX_CHARS, 20000),
|
||||
staleMessageWindowMs: positiveInt(env.FEISHU_STALE_MESSAGE_WINDOW_MS, 10 * 60 * 1000),
|
||||
};
|
||||
}
|
||||
|
||||
function ignoreReason(data) {
|
||||
const sender = objectValue(data.sender);
|
||||
const senderType = stringValue(sender.sender_type || "").toLowerCase();
|
||||
if (senderType && senderType !== "user") {
|
||||
return `sender_type:${senderType}`;
|
||||
}
|
||||
const content = extractText(objectValue(data.message)).trim();
|
||||
if (content.startsWith("/feishu")) {
|
||||
return "feishu_command";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizedContent(message) {
|
||||
const parts = [stringValue(message.content || "")];
|
||||
const resources = Array.isArray(message.resources) ? message.resources : [];
|
||||
for (const resource of resources) {
|
||||
if (!resource || typeof resource !== "object") {
|
||||
continue;
|
||||
}
|
||||
const type = stringValue(resource.type || "file");
|
||||
const fileName = stringValue(resource.fileName || "");
|
||||
parts.push(`[${type}${fileName ? `: ${fileName}` : ""}]`);
|
||||
}
|
||||
return parts.filter((part) => part.trim()).join("\n");
|
||||
}
|
||||
|
||||
function rawEventId(raw) {
|
||||
const rawObject = objectValue(raw);
|
||||
const header = objectValue(rawObject.header);
|
||||
return rawObject.event_id || rawObject.eventId || header.event_id || header.eventId || "";
|
||||
}
|
||||
|
||||
function extractText(message) {
|
||||
const content = message.content;
|
||||
if (typeof content !== "string") {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
if (parsed && parsed.text != null) {
|
||||
return String(parsed.text);
|
||||
}
|
||||
} catch (_error) {
|
||||
return content;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function envList(value) {
|
||||
return stringValue(value)
|
||||
.replace(/\n/g, ",")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function envBool(value, defaultValue) {
|
||||
if (value == null || value === "") {
|
||||
return defaultValue;
|
||||
}
|
||||
return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
|
||||
}
|
||||
|
||||
function positiveInt(value, defaultValue) {
|
||||
const number = Number.parseInt(String(value ?? ""), 10);
|
||||
return Number.isFinite(number) && number > 0 ? number : defaultValue;
|
||||
}
|
||||
|
||||
function objectValue(value) {
|
||||
return value && typeof value === "object" ? value : {};
|
||||
}
|
||||
|
||||
function stringValue(value) {
|
||||
return value == null ? "" : String(value);
|
||||
}
|
||||
|
||||
function randomId() {
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bridgeEventFromFeishu,
|
||||
bridgeEventFromNormalizedMessage,
|
||||
buildChannelOptions,
|
||||
ignoreReason,
|
||||
extractText,
|
||||
parsePolicyEnv,
|
||||
};
|
||||
@ -1,4 +1,9 @@
|
||||
const Lark = require("@larksuiteoapi/node-sdk");
|
||||
const {
|
||||
bridgeEventFromNormalizedMessage,
|
||||
buildChannelOptions,
|
||||
parsePolicyEnv,
|
||||
} = require("./feishu_event_utils");
|
||||
|
||||
const appId = requireEnv("FEISHU_APP_ID");
|
||||
const appSecret = requireEnv("FEISHU_APP_SECRET");
|
||||
@ -10,99 +15,78 @@ const bridgeToken = requireEnv("BEAVER_BRIDGE_TOKEN");
|
||||
const domain = (process.env.FEISHU_DOMAIN || "feishu").toLowerCase() === "lark"
|
||||
? Lark.Domain.Lark
|
||||
: Lark.Domain.Feishu;
|
||||
const policy = parsePolicyEnv(process.env);
|
||||
const env = { connectionId, channelId, accountId };
|
||||
|
||||
const wsClient = new Lark.WSClient({
|
||||
appId,
|
||||
appSecret,
|
||||
domain,
|
||||
loggerLevel: Lark.LoggerLevel.info,
|
||||
onReady: () => log("feishu_ws_ready", {}),
|
||||
onError: (error) => log("feishu_ws_error", { error: redact(String(error && error.message ? error.message : error)) }),
|
||||
onReconnecting: () => log("feishu_ws_reconnecting", {}),
|
||||
onReconnected: () => log("feishu_ws_reconnected", {}),
|
||||
handshakeTimeoutMs: 15000,
|
||||
wsConfig: { pingTimeout: 10 },
|
||||
});
|
||||
|
||||
const dispatcher = new Lark.EventDispatcher({}).register({
|
||||
"im.message.receive_v1": async (data) => {
|
||||
const event = bridgeEventFromFeishu(data);
|
||||
const channel = Lark.createLarkChannel(buildChannelOptions({ appId, appSecret, domain, policy }));
|
||||
channel.on({
|
||||
message: async (message) => {
|
||||
const event = bridgeEventFromNormalizedMessage(message, env, { maxMessageChars: policy.maxMessageChars });
|
||||
if (!event) {
|
||||
log("feishu_inbound_ignored", {
|
||||
connectionId,
|
||||
messageId: message.messageId,
|
||||
reason: "empty_or_oversized",
|
||||
});
|
||||
return;
|
||||
}
|
||||
log("feishu_inbound_message", {
|
||||
connectionId,
|
||||
eventId: event.eventId,
|
||||
messageId: event.messageId,
|
||||
peerId: event.peerId,
|
||||
peerType: event.peerType,
|
||||
textLength: event.content.length,
|
||||
});
|
||||
await postJson(`${bridgeBaseUrl}/api/channel-connector-bridge/events`, event);
|
||||
},
|
||||
reject: (event) => {
|
||||
log("feishu_inbound_ignored", {
|
||||
connectionId,
|
||||
messageId: event.messageId,
|
||||
peerId: event.chatId,
|
||||
userId: event.senderId,
|
||||
reason: event.reason,
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
log("feishu_ws_error", {
|
||||
connectionId,
|
||||
code: error.code || "unknown",
|
||||
error: redact(String(error && error.message ? error.message : error)),
|
||||
});
|
||||
},
|
||||
reconnecting: () => log("feishu_ws_reconnecting", { connectionId }),
|
||||
reconnected: () => log("feishu_ws_reconnected", { connectionId }),
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
wsClient.close({ force: true });
|
||||
process.exit(0);
|
||||
channel.disconnect().finally(() => process.exit(0));
|
||||
});
|
||||
process.on("SIGINT", () => {
|
||||
wsClient.close({ force: true });
|
||||
process.exit(0);
|
||||
channel.disconnect().finally(() => process.exit(0));
|
||||
});
|
||||
|
||||
wsClient.start({ eventDispatcher: dispatcher }).catch((error) => {
|
||||
channel.connect().then(() => {
|
||||
log("feishu_ws_ready", {
|
||||
connectionId,
|
||||
requireMentionInGroups: policy.requireMentionInGroups,
|
||||
dmMode: policy.dmMode,
|
||||
groupAllowlistSize: policy.groupAllowFrom.length,
|
||||
dmAllowlistSize: policy.allowFrom.length,
|
||||
});
|
||||
}).catch((error) => {
|
||||
log("feishu_ws_start_failed", { error: redact(String(error && error.message ? error.message : error)) });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
if (typeof wsClient.getConnectionStatus === "function") {
|
||||
log("feishu_ws_status", wsClient.getConnectionStatus());
|
||||
const status = channel.getConnectionStatus();
|
||||
if (status) {
|
||||
log("feishu_ws_status", { connectionId, ...status });
|
||||
}
|
||||
}, 60000).unref();
|
||||
|
||||
function bridgeEventFromFeishu(data) {
|
||||
const message = objectValue(data.message);
|
||||
const sender = objectValue(data.sender);
|
||||
const senderId = objectValue(sender.sender_id);
|
||||
const peerId = stringValue(senderId.open_id || senderId.user_id || "");
|
||||
const messageId = stringValue(message.message_id || randomId());
|
||||
const eventId = stringValue(data.event_id || data.eventId || `${channelId}:${messageId}`);
|
||||
return {
|
||||
eventId,
|
||||
timestamp: new Date().toISOString(),
|
||||
deliveryAttempt: 1,
|
||||
connectionId,
|
||||
channelId,
|
||||
kind: "feishu",
|
||||
accountId,
|
||||
peerId,
|
||||
peerType: message.chat_type === "group" ? "group" : "dm",
|
||||
userId: peerId,
|
||||
threadId: stringValue(message.chat_id || "") || null,
|
||||
messageId,
|
||||
messageType: stringValue(message.message_type || "text"),
|
||||
content: extractText(message),
|
||||
metadata: {
|
||||
chatId: message.chat_id || null,
|
||||
rawMessageType: message.message_type || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function extractText(message) {
|
||||
const content = message.content;
|
||||
if (typeof content !== "string") {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
if (parsed && parsed.text != null) {
|
||||
return String(parsed.text);
|
||||
}
|
||||
} catch (_error) {
|
||||
return content;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
async function postJson(url, payload) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
@ -126,18 +110,6 @@ function requireEnv(name) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function objectValue(value) {
|
||||
return value && typeof value === "object" ? value : {};
|
||||
}
|
||||
|
||||
function stringValue(value) {
|
||||
return value == null ? "" : String(value);
|
||||
}
|
||||
|
||||
function randomId() {
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
function log(event, fields) {
|
||||
console.log(JSON.stringify({ event, ...fields }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user