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:
2026-06-05 11:46:40 +08:00
parent 236ac19789
commit 2c5205b06e
120 changed files with 8321 additions and 1865 deletions

View 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,
};

View File

@ -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 }));
}