Files
beaver_project/app-instance/backend/beaver/interfaces/channels/connections/external.py
steven_li 2c5205b06e 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功能
2026-06-05 11:46:40 +08:00

201 lines
7.4 KiB
Python

"""Sidecar-backed channel connectors."""
from __future__ import annotations
from typing import Any
from .models import ChannelRuntimeSpec, ValidationResult
from .sidecar_client import ConnectorSidecarClient
from .store import ChannelConnectionStore, CredentialStore
POLICY_CONFIG_KEYS = {
"allowFrom",
"groupAllowFrom",
"requireMentionInGroups",
"respondToMentionAll",
"dmMode",
"maxMessageChars",
"textBatchDelayMs",
"textBatchMaxMessages",
"textBatchMaxChars",
}
class ExternalConnectorBase:
kind = ""
capabilities: list[str] = []
def __init__(
self,
*,
connection_store: ChannelConnectionStore,
credential_store: CredentialStore,
sidecar_client: ConnectorSidecarClient | Any,
sidecar_base_url: str,
) -> None:
self.connection_store = connection_store
self.credential_store = credential_store
self.sidecar_client = sidecar_client
self.sidecar_base_url = sidecar_base_url
async def start_session(
self,
*,
display_name: str,
owner_user_id: str | None,
options: dict[str, Any],
) -> dict[str, Any]:
runtime_config = {"sidecarBaseUrl": self.sidecar_base_url}
runtime_config.update(_policy_runtime_config(options))
connection = self.connection_store.create(
kind=self.kind,
mode="sidecar",
display_name=display_name or self.kind,
account_id="",
owner_user_id=owner_user_id,
auth_type="connector_session",
runtime_config=runtime_config,
capabilities=list(self.capabilities),
)
connection = self.connection_store.update_status(connection.connection_id, status="pairing", last_error=None)
payload = {
"kind": self.kind,
"connectionId": connection.connection_id,
"channelId": connection.channel_id,
"displayName": connection.display_name,
"callbackBaseUrl": "",
"options": dict(options),
}
view = dict(await self.sidecar_client.start_session(payload))
connection.pairing_session_id = str(view.get("sessionId") or "")
connection = self.connection_store.update(connection)
connection = self._apply_session_view(connection, view)
view["connectionId"] = connection.connection_id
view["channelId"] = connection.channel_id
return view
async def poll_session(self, session_id: str) -> dict[str, Any]:
view = dict(await self.sidecar_client.get_session(session_id))
connection = self._connection_for_session(session_id)
connection = self._apply_session_view(connection, view)
view["connectionId"] = connection.connection_id
view["channelId"] = connection.channel_id
return view
def _apply_session_view(self, connection: Any, view: dict[str, Any]) -> Any:
status = str(view.get("status") or "")
if status == "connected":
connection.account_id = str(view.get("accountId") or connection.account_id)
connection.display_name = str(view.get("displayName") or connection.display_name)
metadata = view.get("metadata") if isinstance(view.get("metadata"), dict) else {}
state_ref = metadata.get("stateRef")
if state_ref:
connection.credentials_ref = self.credential_store.put(kind=self.kind, values={"stateRef": state_ref})
self.connection_store.update(connection)
self.connection_store.update_status(connection.connection_id, status="connected", last_error=None)
elif status in {"expired", "error", "cancelled"}:
self.connection_store.update_status(
connection.connection_id,
status="error",
last_error=str(view.get("error") or status),
)
return self.connection_store.get(connection.connection_id)
async def validate(self, connection_id: str) -> ValidationResult:
connection = self.connection_store.get(connection_id)
if connection.status in {"connected", "running"}:
return ValidationResult(
ok=True,
status="connected",
account_id=connection.account_id,
display_name=connection.display_name,
)
return ValidationResult(ok=False, status=connection.status, error=connection.last_error)
async def materialize_runtime(self, connection_id: str) -> ChannelRuntimeSpec:
connection = self.connection_store.get(connection_id)
if connection.status not in {"connected", "running"}:
raise ValueError(f"Connection is not connected: {connection.connection_id}")
return ChannelRuntimeSpec(
channel_id=connection.channel_id,
kind="external_connector",
mode="http",
account_id=connection.account_id,
display_name=connection.display_name,
config={
"platformKind": self.kind,
"connectionId": connection.connection_id,
**dict(connection.runtime_config),
"sidecarBaseUrl": connection.runtime_config.get("sidecarBaseUrl") or self.sidecar_base_url,
},
secrets_ref=None,
)
async def revoke(self, connection_id: str) -> None:
await self.sidecar_client.logout(connection_id)
def _connection_for_session(self, session_id: str):
for connection in self.connection_store.list():
if connection.pairing_session_id == session_id:
return connection
raise KeyError(session_id)
class WeixinConnector(ExternalConnectorBase):
kind = "weixin"
capabilities = ["receive_text", "send_text", "receive_media", "direct_messages"]
class FeishuConnector(ExternalConnectorBase):
kind = "feishu"
capabilities = ["receive_text", "send_text", "receive_media", "groups"]
def _policy_runtime_config(options: dict[str, Any]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key in POLICY_CONFIG_KEYS:
if key not in options:
continue
value = options[key]
if key in {"allowFrom", "groupAllowFrom"}:
items = _string_list(value)
if items:
result[key] = items
continue
if key in {"maxMessageChars", "textBatchDelayMs", "textBatchMaxMessages", "textBatchMaxChars"}:
number = _positive_int(value)
if number is not None:
result[key] = number
continue
if key in {"requireMentionInGroups", "respondToMentionAll"}:
result[key] = _bool(value)
continue
text = str(value or "").strip()
if text:
result[key] = text
return result
def _string_list(value: Any) -> list[str]:
if isinstance(value, str):
raw_items = value.replace("\n", ",").split(",")
elif isinstance(value, list):
raw_items = value
else:
raw_items = []
return [str(item).strip() for item in raw_items if str(item).strip()]
def _positive_int(value: Any) -> int | None:
try:
number = int(value)
except (TypeError, ValueError):
return None
return number if number > 0 else None
def _bool(value: Any) -> bool:
if isinstance(value, bool):
return value
return str(value).strip().lower() in {"1", "true", "yes", "on"}