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:
189
external-connector/tests/node/feishu_event_utils.test.js
Normal file
189
external-connector/tests/node/feishu_event_utils.test.js
Normal file
@ -0,0 +1,189 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {
|
||||
bridgeEventFromFeishu,
|
||||
bridgeEventFromNormalizedMessage,
|
||||
buildChannelOptions,
|
||||
ignoreReason,
|
||||
parsePolicyEnv,
|
||||
} = require("../../external_connector/node/feishu_event_utils");
|
||||
|
||||
test("ignores Feishu app or bot sender events", () => {
|
||||
assert.equal(ignoreReason({ sender: { sender_type: "app" }, message: { content: "{\"text\":\"hello\"}" } }), "sender_type:app");
|
||||
assert.equal(ignoreReason({ sender: { sender_type: "bot" }, message: { content: "{\"text\":\"hello\"}" } }), "sender_type:bot");
|
||||
});
|
||||
|
||||
test("ignores Feishu slash commands intended for the platform integration", () => {
|
||||
assert.equal(ignoreReason({ sender: { sender_type: "user" }, message: { content: "{\"text\":\"/feishu start\"}" } }), "feishu_command");
|
||||
});
|
||||
|
||||
test("keeps user messages and records sender type metadata", () => {
|
||||
const event = bridgeEventFromFeishu(
|
||||
{
|
||||
event_id: "evt_1",
|
||||
sender: { sender_type: "user", sender_id: { open_id: "ou_user" } },
|
||||
message: {
|
||||
message_id: "om_1",
|
||||
chat_id: "oc_1",
|
||||
chat_type: "p2p",
|
||||
message_type: "text",
|
||||
content: "{\"text\":\"hello\"}",
|
||||
},
|
||||
},
|
||||
{ connectionId: "conn_1", channelId: "feishu-main", accountId: "feishu:cli_1" },
|
||||
);
|
||||
|
||||
assert.equal(ignoreReason({ sender: { sender_type: "user" }, message: { content: "{\"text\":\"hello\"}" } }), "");
|
||||
assert.equal(event.content, "hello");
|
||||
assert.equal(event.peerId, "ou_user");
|
||||
assert.equal(event.metadata.senderType, "user");
|
||||
});
|
||||
|
||||
test("uses chat id as peer id for group messages", () => {
|
||||
const event = bridgeEventFromFeishu(
|
||||
{
|
||||
event_id: "evt_1",
|
||||
sender: { sender_type: "user", sender_id: { open_id: "ou_user" } },
|
||||
message: {
|
||||
message_id: "om_1",
|
||||
chat_id: "oc_group",
|
||||
chat_type: "group",
|
||||
message_type: "text",
|
||||
content: "{\"text\":\"@bot hello\"}",
|
||||
},
|
||||
},
|
||||
{ connectionId: "conn_1", channelId: "feishu-main", accountId: "feishu:cli_1" },
|
||||
);
|
||||
|
||||
assert.equal(event.peerType, "group");
|
||||
assert.equal(event.peerId, "oc_group");
|
||||
assert.equal(event.userId, "ou_user");
|
||||
});
|
||||
|
||||
test("builds SDK channel options from explicit Feishu policy environment", () => {
|
||||
const policy = parsePolicyEnv({
|
||||
FEISHU_REQUIRE_MENTION_IN_GROUPS: "false",
|
||||
FEISHU_RESPOND_TO_MENTION_ALL: "true",
|
||||
FEISHU_DM_MODE: "allowlist",
|
||||
FEISHU_ALLOW_FROM: "ou_1, ou_2",
|
||||
FEISHU_GROUP_ALLOW_FROM: "oc_1\noc_2",
|
||||
FEISHU_MAX_MESSAGE_CHARS: "1234",
|
||||
FEISHU_TEXT_BATCH_DELAY_MS: "250",
|
||||
FEISHU_TEXT_BATCH_MAX_MESSAGES: "5",
|
||||
FEISHU_TEXT_BATCH_MAX_CHARS: "2048",
|
||||
});
|
||||
const options = buildChannelOptions({
|
||||
appId: "cli_1",
|
||||
appSecret: "secret",
|
||||
domain: "feishu",
|
||||
policy,
|
||||
});
|
||||
|
||||
assert.equal(options.policy.requireMention, false);
|
||||
assert.equal(options.policy.respondToMentionAll, true);
|
||||
assert.equal(options.policy.dmMode, "allowlist");
|
||||
assert.deepEqual(options.policy.dmAllowlist, ["ou_1", "ou_2"]);
|
||||
assert.deepEqual(options.policy.groupAllowlist, ["oc_1", "oc_2"]);
|
||||
assert.equal(options.outbound.textChunkLimit, 1234);
|
||||
assert.equal(options.safety.batch.text.delayMs, 250);
|
||||
assert.equal(options.safety.batch.text.maxMessages, 5);
|
||||
assert.equal(options.safety.batch.text.maxChars, 2048);
|
||||
assert.equal(options.includeRawEvent, true);
|
||||
});
|
||||
|
||||
test("normalizes SDK message events for Beaver bridge", () => {
|
||||
const event = bridgeEventFromNormalizedMessage(
|
||||
{
|
||||
messageId: "om_1",
|
||||
chatId: "oc_group",
|
||||
chatType: "group",
|
||||
senderId: "ou_user",
|
||||
content: "hello",
|
||||
rawContentType: "text",
|
||||
resources: [{ type: "image", fileKey: "img_1", fileName: "photo.png" }],
|
||||
mentions: [{ openId: "ou_bot", name: "Beaver", isBot: true }],
|
||||
mentionAll: false,
|
||||
mentionedBot: true,
|
||||
createTime: 1710000000000,
|
||||
raw: { event_id: "evt_1" },
|
||||
},
|
||||
{ connectionId: "conn_1", channelId: "feishu-main", accountId: "feishu:cli_1" },
|
||||
{ maxMessageChars: 100 },
|
||||
);
|
||||
|
||||
assert.equal(event.eventId, "evt_1");
|
||||
assert.equal(event.peerType, "group");
|
||||
assert.equal(event.peerId, "oc_group");
|
||||
assert.equal(event.userId, "ou_user");
|
||||
assert.equal(event.threadId, "oc_group");
|
||||
assert.match(event.content, /^hello/);
|
||||
assert.deepEqual(event.metadata.mentions[0].openId, "ou_bot");
|
||||
assert.deepEqual(event.metadata.resources[0].type, "image");
|
||||
});
|
||||
|
||||
test("uses sender id as peer id for SDK direct messages", () => {
|
||||
const event = bridgeEventFromNormalizedMessage(
|
||||
{
|
||||
messageId: "om_dm",
|
||||
chatId: "oc_dm",
|
||||
chatType: "p2p",
|
||||
senderId: "ou_user",
|
||||
content: "hello dm",
|
||||
rawContentType: "text",
|
||||
resources: [],
|
||||
mentions: [],
|
||||
mentionAll: false,
|
||||
mentionedBot: false,
|
||||
createTime: 1710000000000,
|
||||
raw: { header: { event_id: "evt_dm" } },
|
||||
},
|
||||
{ connectionId: "conn_1", channelId: "feishu-main", accountId: "feishu:cli_1" },
|
||||
);
|
||||
|
||||
assert.equal(event.peerType, "dm");
|
||||
assert.equal(event.peerId, "ou_user");
|
||||
assert.equal(event.threadId, "oc_dm");
|
||||
});
|
||||
|
||||
test("drops empty and oversized SDK message events", () => {
|
||||
assert.equal(
|
||||
bridgeEventFromNormalizedMessage(
|
||||
{
|
||||
messageId: "om_empty",
|
||||
chatId: "oc_dm",
|
||||
chatType: "p2p",
|
||||
senderId: "ou_user",
|
||||
content: " ",
|
||||
rawContentType: "text",
|
||||
resources: [],
|
||||
mentions: [],
|
||||
mentionAll: false,
|
||||
mentionedBot: false,
|
||||
createTime: 1710000000000,
|
||||
},
|
||||
{ connectionId: "conn_1", channelId: "feishu-main", accountId: "feishu:cli_1" },
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
bridgeEventFromNormalizedMessage(
|
||||
{
|
||||
messageId: "om_big",
|
||||
chatId: "oc_dm",
|
||||
chatType: "p2p",
|
||||
senderId: "ou_user",
|
||||
content: "x".repeat(11),
|
||||
rawContentType: "text",
|
||||
resources: [],
|
||||
mentions: [],
|
||||
mentionAll: false,
|
||||
mentionedBot: false,
|
||||
createTime: 1710000000000,
|
||||
},
|
||||
{ connectionId: "conn_1", channelId: "feishu-main", accountId: "feishu:cli_1" },
|
||||
{ maxMessageChars: 10 },
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from external_connector.app import create_app
|
||||
@ -12,6 +13,17 @@ from external_connector.state import SidecarStateStore
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_feishu_node_event_utils() -> None:
|
||||
result = subprocess.run(
|
||||
["node", "--test", "tests/node/feishu_event_utils.test.js"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, payload: dict[str, object]) -> None:
|
||||
self.payload = payload
|
||||
@ -169,6 +181,36 @@ def test_feishu_bot_provider_connects_with_app_credentials(tmp_path) -> None:
|
||||
assert receiver_starts == ["conn_1"]
|
||||
|
||||
|
||||
def test_feishu_bot_provider_stores_runtime_policy_options(tmp_path) -> None:
|
||||
receiver_starts: list[str] = []
|
||||
provider = _provider(tmp_path, receiver_starts=receiver_starts)
|
||||
|
||||
session = provider.start_session(
|
||||
{
|
||||
"kind": "feishu",
|
||||
"connectionId": "conn_1",
|
||||
"channelId": "feishu-main",
|
||||
"displayName": "Feishu Main",
|
||||
"callbackBaseUrl": "http://beaver:8080",
|
||||
"options": {
|
||||
"appId": "cli_xxx",
|
||||
"appSecret": "secret",
|
||||
"verificationToken": "verify-token",
|
||||
"requireMentionInGroups": True,
|
||||
"allowFrom": ["ou_1"],
|
||||
"groupAllowFrom": ["oc_1"],
|
||||
"maxMessageChars": 1234,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
stored = provider.store.get_session(session["sessionId"])
|
||||
assert stored.metadata["requireMentionInGroups"] is True
|
||||
assert stored.metadata["allowFrom"] == ["ou_1"]
|
||||
assert stored.metadata["groupAllowFrom"] == ["oc_1"]
|
||||
assert stored.metadata["maxMessageChars"] == 1234
|
||||
|
||||
|
||||
def test_feishu_bot_provider_send_uses_tenant_token_and_dedupes(tmp_path) -> None:
|
||||
provider = _provider(tmp_path)
|
||||
session = provider.start_session(
|
||||
@ -205,13 +247,96 @@ def test_feishu_bot_provider_send_uses_tenant_token_and_dedupes(tmp_path) -> Non
|
||||
assert send_posts[0][1]["msg_type"] == "text"
|
||||
|
||||
|
||||
def test_feishu_event_route_returns_challenge(tmp_path) -> None:
|
||||
def test_feishu_bot_provider_send_uses_chat_id_for_group_targets(tmp_path) -> None:
|
||||
provider = _provider(tmp_path)
|
||||
provider.start_session(
|
||||
{
|
||||
"kind": "feishu",
|
||||
"connectionId": "conn_1",
|
||||
"channelId": "feishu-main",
|
||||
"displayName": "Feishu Main",
|
||||
"callbackBaseUrl": "http://beaver:8080",
|
||||
"options": {"appId": "cli_xxx", "appSecret": "secret"},
|
||||
}
|
||||
)
|
||||
|
||||
result = provider.send(
|
||||
{
|
||||
"requestId": "out_group_1",
|
||||
"connectionId": "conn_1",
|
||||
"channelId": "feishu-main",
|
||||
"kind": "feishu",
|
||||
"target": {"peerId": "oc_group", "peerType": "group", "threadId": "oc_group"},
|
||||
"content": "hello group",
|
||||
"metadata": {},
|
||||
}
|
||||
)
|
||||
|
||||
send_posts = [item for item in provider.http.posts if "/open-apis/im/v1/messages" in item[0]]
|
||||
assert result["ok"] is True
|
||||
assert send_posts[-1][0].endswith("?receive_id_type=chat_id")
|
||||
assert send_posts[-1][1]["receive_id"] == "oc_group"
|
||||
|
||||
|
||||
def test_feishu_bot_provider_send_chunks_oversized_text(tmp_path) -> None:
|
||||
provider = _provider(tmp_path)
|
||||
session = provider.start_session(
|
||||
{
|
||||
"kind": "feishu",
|
||||
"connectionId": "conn_1",
|
||||
"channelId": "feishu-main",
|
||||
"displayName": "Feishu Main",
|
||||
"callbackBaseUrl": "http://beaver:8080",
|
||||
"options": {"appId": "cli_xxx", "appSecret": "secret", "maxMessageChars": 5},
|
||||
}
|
||||
)
|
||||
|
||||
result = provider.send(
|
||||
{
|
||||
"requestId": "out_chunked",
|
||||
"connectionId": "conn_1",
|
||||
"channelId": "feishu-main",
|
||||
"kind": "feishu",
|
||||
"target": {"peerId": "ou_user", "peerType": "dm", "threadId": None},
|
||||
"content": "helloworld!",
|
||||
"metadata": {},
|
||||
}
|
||||
)
|
||||
|
||||
send_posts = [item for item in provider.http.posts if "/open-apis/im/v1/messages" in item[0]]
|
||||
contents = [json.loads(item[1]["content"])["text"] for item in send_posts]
|
||||
assert session["status"] == "connected"
|
||||
assert result["ok"] is True
|
||||
assert contents == ["hello", "world", "!"]
|
||||
|
||||
|
||||
def test_feishu_event_route_requires_known_verification_token_for_challenge(tmp_path) -> None:
|
||||
provider = _provider(tmp_path)
|
||||
app = create_app(provider=provider, api_token="sidecar-token")
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/feishu/events", json={"challenge": "abc"})
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_feishu_event_route_returns_challenge_for_matching_token(tmp_path) -> None:
|
||||
provider = _provider(tmp_path)
|
||||
provider.start_session(
|
||||
{
|
||||
"kind": "feishu",
|
||||
"connectionId": "conn_1",
|
||||
"channelId": "feishu-main",
|
||||
"displayName": "Feishu Main",
|
||||
"callbackBaseUrl": "http://beaver:8080",
|
||||
"options": {"appId": "cli_xxx", "appSecret": "secret", "verificationToken": "verify-token"},
|
||||
}
|
||||
)
|
||||
app = create_app(provider=provider, api_token="sidecar-token")
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/feishu/events", json={"challenge": "abc", "token": "verify-token"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"challenge": "abc"}
|
||||
|
||||
@ -259,6 +384,62 @@ def test_feishu_event_route_forwards_message_to_bridge(tmp_path) -> None:
|
||||
assert bridge_posts[0][1]["peerId"] == "ou_user"
|
||||
|
||||
|
||||
def test_feishu_event_route_ignores_bot_sender_and_platform_commands(tmp_path) -> None:
|
||||
bridge_posts: list[tuple[str, dict[str, object], dict[str, str]]] = []
|
||||
provider = _provider(tmp_path, bridge_posts=bridge_posts)
|
||||
provider.start_session(
|
||||
{
|
||||
"kind": "feishu",
|
||||
"connectionId": "conn_1",
|
||||
"channelId": "feishu-main",
|
||||
"displayName": "Feishu Main",
|
||||
"callbackBaseUrl": "http://beaver:8080",
|
||||
"options": {"appId": "cli_xxx", "appSecret": "secret", "verificationToken": "verify-token"},
|
||||
}
|
||||
)
|
||||
app = create_app(provider=provider, api_token="sidecar-token")
|
||||
|
||||
with TestClient(app) as client:
|
||||
bot = client.post(
|
||||
"/feishu/events",
|
||||
json={
|
||||
"header": {"event_id": "evt_bot", "token": "verify-token", "app_id": "cli_xxx"},
|
||||
"event": {
|
||||
"sender": {"sender_type": "bot", "sender_id": {"open_id": "ou_bot"}},
|
||||
"message": {
|
||||
"message_id": "om_bot",
|
||||
"chat_id": "oc_chat",
|
||||
"chat_type": "p2p",
|
||||
"message_type": "text",
|
||||
"content": "{\"text\":\"hello\"}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
command = client.post(
|
||||
"/feishu/events",
|
||||
json={
|
||||
"header": {"event_id": "evt_command", "token": "verify-token", "app_id": "cli_xxx"},
|
||||
"event": {
|
||||
"sender": {"sender_type": "user", "sender_id": {"open_id": "ou_user"}},
|
||||
"message": {
|
||||
"message_id": "om_command",
|
||||
"chat_id": "oc_chat",
|
||||
"chat_type": "p2p",
|
||||
"message_type": "text",
|
||||
"content": "{\"text\":\"/feishu start\"}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert bot.status_code == 200
|
||||
assert command.status_code == 200
|
||||
assert bot.json() == {"ok": True, "ignored": "sender_type:bot"}
|
||||
assert command.json() == {"ok": True, "ignored": "feishu_command"}
|
||||
assert bridge_posts == []
|
||||
|
||||
|
||||
def test_composite_provider_routes_feishu_and_weixin_descriptors(tmp_path) -> None:
|
||||
store = SidecarStateStore(tmp_path / "state.json")
|
||||
provider = CompositeProvider([FakeProvider(store), _provider(tmp_path)])
|
||||
|
||||
@ -70,6 +70,17 @@ def test_sidecar_http_api_requires_bearer_token(tmp_path) -> None:
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_sidecar_http_api_fails_closed_without_configured_token(tmp_path) -> None:
|
||||
app = create_app(provider=FakeProvider(SidecarStateStore(tmp_path / "state.json")), api_token="")
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/connectors")
|
||||
health = client.get("/health")
|
||||
|
||||
assert response.status_code == 503
|
||||
assert health.status_code == 200
|
||||
|
||||
|
||||
def test_sidecar_http_api_session_and_send(tmp_path) -> None:
|
||||
app = create_app(provider=FakeProvider(SidecarStateStore(tmp_path / "state.json")), api_token="sidecar-token")
|
||||
headers = {"Authorization": "Bearer sidecar-token"}
|
||||
|
||||
@ -206,6 +206,38 @@ def test_weixin_ilink_provider_recovers_token_session_persisted_as_scanned(tmp_p
|
||||
assert recovered["accountId"] == "weixin:bot-1@im.bot"
|
||||
|
||||
|
||||
def test_weixin_ilink_provider_starts_existing_connected_receiver_on_startup(tmp_path) -> None:
|
||||
store = SidecarStateStore(tmp_path / "state.json")
|
||||
session = store.create_session(
|
||||
kind="weixin",
|
||||
connection_id="conn_1",
|
||||
channel_id="weixin-main",
|
||||
display_name="Weixin Main",
|
||||
options={},
|
||||
)
|
||||
store.update_session(
|
||||
session.session_id,
|
||||
status="connected",
|
||||
account_id="weixin:bot-1@im.bot",
|
||||
metadata={
|
||||
"token": "bot-token",
|
||||
"baseUrl": "https://api.weixin.example",
|
||||
"userId": "wx-owner",
|
||||
"getUpdatesBuf": "buf",
|
||||
},
|
||||
)
|
||||
|
||||
provider = WeixinIlinkProvider(
|
||||
store=store,
|
||||
http_client=FakeHttpClient(),
|
||||
bridge_base_url="http://beaver:8080",
|
||||
bridge_token="bridge-token",
|
||||
)
|
||||
|
||||
assert "conn_1" in provider._receiver_stops
|
||||
provider.logout("conn_1")
|
||||
|
||||
|
||||
def test_weixin_ilink_provider_send_uses_saved_token_and_dedupes(tmp_path) -> None:
|
||||
http = FakeHttpClient()
|
||||
provider = WeixinIlinkProvider(
|
||||
|
||||
Reference in New Issue
Block a user