feat: implement channel runtime connectors

This commit is contained in:
2026-06-03 16:22:44 +08:00
parent ee972441f5
commit c3d84b904a
105 changed files with 15621 additions and 322 deletions

View File

@ -13,6 +13,7 @@ from .schema import (
AuthzConfig,
BackendIdentityConfig,
BeaverConfig,
ChannelConfig,
EmbeddingConfig,
MCPServerConfig,
ProviderConfig,
@ -73,6 +74,7 @@ def load_config(
embedding=_parse_embedding(data),
tools=_parse_tools(data.get("tools")),
authz=_parse_authz(data.get("authz")),
channels=_parse_channels(data.get("channels")),
backend_identity=_parse_backend_identity(data.get("backend_identity") or data.get("backendIdentity")),
config_path=path,
)
@ -196,6 +198,48 @@ def _parse_authz(raw: Any) -> AuthzConfig:
)
def _parse_channels(raw: Any) -> dict[str, ChannelConfig]:
channels: dict[str, ChannelConfig] = {}
for channel_id, payload in _as_dict(raw).items():
cleaned_id = str(channel_id).strip()
if not cleaned_id:
continue
channels[cleaned_id] = _parse_channel_config(payload)
return channels
def _parse_channel_config(payload: Any) -> ChannelConfig:
data = _as_dict(payload)
return ChannelConfig(
enabled=_bool(data.get("enabled"), default=False),
kind=_string(data.get("kind")) or "",
mode=_string(data.get("mode")) or "webhook",
account_id=_string(data.get("accountId") or data.get("account_id")) or "",
display_name=_string(data.get("displayName") or data.get("display_name")) or "",
config=_normalize_config_map(data.get("config")),
secrets=_string_dict(data.get("secrets")),
)
def _normalize_config_map(value: Any) -> dict[str, Any]:
if not isinstance(value, dict):
return {}
return {
_camel_to_snake_key(str(key)): item
for key, item in value.items()
if str(key).strip()
}
def _camel_to_snake_key(value: str) -> str:
result: list[str] = []
for char in value:
if char.isupper() and result:
result.append("_")
result.append(char.lower())
return "".join(result)
def _parse_backend_identity(raw: Any) -> BackendIdentityConfig:
data = _as_dict(raw)
return BackendIdentityConfig(

View File

@ -91,6 +91,19 @@ class AuthzConfig:
outlook_mcp_url: str = ""
@dataclass(slots=True)
class ChannelConfig:
"""One configured channel adapter instance."""
enabled: bool = False
kind: str = ""
mode: str = "webhook"
account_id: str = ""
display_name: str = ""
config: dict[str, Any] = field(default_factory=dict)
secrets: dict[str, str] = field(default_factory=dict)
@dataclass(slots=True)
class BackendIdentityConfig:
"""This backend's AuthZ client identity."""
@ -111,6 +124,7 @@ class BeaverConfig:
embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig)
tools: ToolsConfig = field(default_factory=ToolsConfig)
authz: AuthzConfig = field(default_factory=AuthzConfig)
channels: dict[str, ChannelConfig] = field(default_factory=dict)
backend_identity: BackendIdentityConfig = field(default_factory=BackendIdentityConfig)
config_path: Path | None = None

View File

@ -1,5 +1,5 @@
"""Event contracts and dispatch helpers."""
from .message_bus import InboundMessage, MessageBus, OutboundMessage
from .message_bus import ChannelIdentity, InboundMessage, MessageBus, OutboundMessage
__all__ = ["InboundMessage", "MessageBus", "OutboundMessage"]
__all__ = ["ChannelIdentity", "InboundMessage", "MessageBus", "OutboundMessage"]

View File

@ -9,12 +9,58 @@ from typing import Any
from uuid import uuid4
@dataclass(slots=True)
class ChannelIdentity:
"""Normalized channel routing identity.
`channel_id` is the Beaver adapter instance id, not the platform kind.
"""
channel_id: str
kind: str
account_id: str
peer_id: str
thread_id: str | None = None
peer_type: str = "unknown"
user_id: str | None = None
message_id: str | None = None
def validation_error(self) -> str | None:
if not self.channel_id.strip():
return "channel_id is required"
if not self.account_id.strip():
return "account_id is required"
if not self.peer_id.strip():
return "peer_id is required"
return None
def session_id(self) -> str:
parts = [self.channel_id, self.account_id, self.peer_id]
if self.thread_id:
parts.append(self.thread_id)
return ":".join(_clean_session_part(part) for part in parts)
def dedupe_key(self) -> str | None:
if not self.message_id:
return None
return f"{self.session_id()}:{_clean_session_part(self.message_id)}"
def _clean_session_part(value: str) -> str:
cleaned = str(value).strip()
if not cleaned:
return "unknown"
return cleaned.replace(":", "_")
@dataclass(slots=True)
class InboundMessage:
"""A minimal inbound message accepted by the gateway bridge."""
channel: str
content: str
content_type: str = "text"
channel_identity: ChannelIdentity | None = None
session_id: str | None = None
user_id: str | None = None
title: str | None = None
@ -35,6 +81,8 @@ class OutboundMessage:
content: str
session_id: str | None
finish_reason: str
content_type: str = "text"
channel_identity: ChannelIdentity | None = None
message_id: str = field(default_factory=lambda: str(uuid4()))
run_id: str | None = None
provider_name: str | None = None