ForcePilot/backend/package/yuxi/services/channel_service.py
Kris fb754d82f1 feat(channel): 添加全渠道网关核心基础架构
新增渠道扩展框架的核心基础层,为所有渠道扩展提供统一的基础设施。

主要变更:
- channel/extensions/__init__.py: 渠道扩展包初始化与注册
- channel/extensions/base.py: 渠道插件基类,定义统一接口规范
- channel_service.py: 渠道服务核心业务逻辑
- models_channels.py: 渠道相关数据库模型
- 8 个 channel_*_repo.py: 渠道数据仓库层
- storage/postgres/manager.py: 数据库管理器,新增渠道表
- models_business.py: 业务模型更新
- config/app.py: 应用配置更新
- conversation_repository.py: 会话仓库更新
- oidc_service.py: OIDC 服务更新
- logging_config.py: 日志配置更新
2026-05-21 12:08:03 +08:00

245 lines
8.1 KiB
Python

from typing import Any
from yuxi.channel.runtime.manager import gateway
from yuxi.channel.security.pairing import PairingManager
from yuxi.repositories.channel_binding_repo import ChannelBindingRepository
from yuxi.repositories.channel_config_repo import ChannelConfigRepository
from yuxi.repositories.channel_pairing_repo import ChannelPairingRecordRepository
from yuxi.repositories.channel_thread_mapping_repo import ChannelThreadMappingRepository
from yuxi.repositories.channel_user_mapping_repo import ChannelUserMappingRepository
_PAIRING_PEER_ID_PREFIX = "admin:pairing"
_config_repo = ChannelConfigRepository()
_binding_repo = ChannelBindingRepository()
_pairing_repo = ChannelPairingRecordRepository()
_user_mapping_repo = ChannelUserMappingRepository()
_thread_mapping_repo = ChannelThreadMappingRepository()
async def list_channel_configs(
channel_type: str | None = None,
enabled: bool | None = None,
) -> list[dict[str, Any]]:
if channel_type and enabled is not None:
configs = await _config_repo.list_enabled(channel_type)
elif enabled is not None:
configs = await _config_repo.list_enabled()
elif channel_type:
configs = await _config_repo.list_by_type(channel_type)
else:
configs = await _config_repo.list_all()
return [c.to_dict(mask_secrets=True) for c in configs]
async def get_channel_config(config_id: str) -> dict[str, Any] | None:
config = await _config_repo.get_by_id(config_id)
return config.to_dict(mask_secrets=True) if config else None
async def create_channel_config(data: dict[str, Any]) -> dict[str, Any]:
config = await _config_repo.create(data)
return config.to_dict(mask_secrets=True)
async def update_channel_config(config_id: str, data: dict[str, Any]) -> dict[str, Any] | None:
config = await _config_repo.update(config_id, data)
return config.to_dict(mask_secrets=True) if config else None
async def delete_channel_config(config_id: str) -> bool:
return await _config_repo.delete(config_id)
async def list_channel_bindings(agent_config_id: int | None = None) -> list[dict[str, Any]]:
if agent_config_id is not None:
bindings = await _binding_repo.list_by_agent(agent_config_id)
else:
bindings = await _binding_repo.list_all()
return [b.to_dict() for b in bindings]
async def get_channel_binding(binding_id: str) -> dict[str, Any] | None:
binding = await _binding_repo.get_by_id(binding_id)
return binding.to_dict() if binding else None
async def create_channel_binding(data: dict[str, Any]) -> dict[str, Any]:
binding = await _binding_repo.create(data)
return binding.to_dict()
async def update_channel_binding(binding_id: str, data: dict[str, Any]) -> dict[str, Any] | None:
binding = await _binding_repo.update(binding_id, data)
return binding.to_dict() if binding else None
async def delete_channel_binding(binding_id: str) -> bool:
return await _binding_repo.delete(binding_id)
async def list_channel_bindings_by_type(channel_type: str) -> list[dict[str, Any]]:
bindings = await _binding_repo.list_by_channel_type(channel_type)
return [b.to_dict() for b in bindings]
async def list_channel_pairing_records(status: str | None = None) -> list[dict[str, Any]]:
records = await _pairing_repo.list_all(status=status)
return [r.to_dict() for r in records]
async def approve_channel_pairing(record_id: str) -> dict[str, Any] | None:
record = await _pairing_repo.approve(record_id)
return record.to_dict() if record else None
async def reject_channel_pairing(record_id: str) -> dict[str, Any] | None:
record = await _pairing_repo.reject(record_id)
return record.to_dict() if record else None
async def get_channel_pairing_record(record_id: str) -> dict[str, Any] | None:
record = await _pairing_repo.get_by_id(record_id)
return record.to_dict() if record else None
async def delete_channel_pairing_record(record_id: str) -> bool:
return await _pairing_repo.delete(record_id)
async def generate_agent_pairing_code(agent_config_id: int) -> dict[str, Any]:
bindings = await _binding_repo.list_by_agent(agent_config_id)
if not bindings:
raise ValueError(f"Agent config {agent_config_id} has no channel bindings")
binding = bindings[0]
mgr = PairingManager()
peer_id = f"{_PAIRING_PEER_ID_PREFIX}:{agent_config_id}"
req = await mgr.upsert_code(
channel_type=binding.channel_type,
peer_id=peer_id,
account_id=binding.account_id or "default",
)
return {
"code": req.code,
"channel_type": binding.channel_type,
"account_id": binding.account_id or "default",
"agent_config_id": agent_config_id,
"record_id": req._record_id,
}
async def list_user_mappings() -> list[dict[str, Any]]:
mappings = await _user_mapping_repo.list_all()
return [m.to_dict() for m in mappings]
async def get_user_mapping(mapping_id: int) -> dict[str, Any] | None:
mapping = await _user_mapping_repo.get_by_id(mapping_id)
return mapping.to_dict() if mapping else None
async def find_user_mapping_by_channel(channel_id: str, channel_user_id: str) -> dict[str, Any] | None:
mapping = await _user_mapping_repo.find_by_channel(channel_id, channel_user_id)
return mapping.to_dict() if mapping else None
async def resolve_channel_user(channel_id: str, channel_user_id: str) -> dict[str, Any]:
mapping = await _user_mapping_repo.resolve_user(channel_id, channel_user_id)
return mapping.to_dict()
async def find_user_mappings_by_internal_user(internal_user_id: str) -> list[dict[str, Any]]:
mappings = await _user_mapping_repo.find_by_internal_user(internal_user_id)
return [m.to_dict() for m in mappings]
async def create_user_mapping(data: dict[str, Any]) -> dict[str, Any]:
mapping = await _user_mapping_repo.create(data)
return mapping.to_dict()
async def delete_user_mapping(mapping_id: int) -> bool:
return await _user_mapping_repo.delete(mapping_id)
async def resolve_channel_thread(
channel_id: str,
channel_chat_id: str,
internal_user_id: str,
agent_id: str | None = None,
) -> dict[str, Any]:
mapping = await _thread_mapping_repo.resolve_thread(
channel_id,
channel_chat_id,
internal_user_id,
agent_id,
)
return mapping.to_dict()
async def get_thread_mapping(mapping_id: int) -> dict[str, Any] | None:
mapping = await _thread_mapping_repo.get_by_id(mapping_id)
return mapping.to_dict() if mapping else None
async def find_thread_mapping_by_channel_chat(
channel_id: str,
channel_chat_id: str,
internal_user_id: str,
) -> dict[str, Any] | None:
mapping = await _thread_mapping_repo.find_by_channel_chat(
channel_id,
channel_chat_id,
internal_user_id,
)
return mapping.to_dict() if mapping else None
async def find_thread_mapping_by_thread_id(thread_id: str) -> dict[str, Any] | None:
mapping = await _thread_mapping_repo.find_by_thread_id(thread_id)
return mapping.to_dict() if mapping else None
async def create_thread_mapping(data: dict[str, Any]) -> dict[str, Any]:
mapping = await _thread_mapping_repo.create(data)
return mapping.to_dict()
async def delete_thread_mapping(mapping_id: int) -> bool:
return await _thread_mapping_repo.delete(mapping_id)
async def get_gateway_health() -> dict[str, Any]:
report = gateway.get_health()
return {
"status": report.status,
"channels": report.channels,
"summary": report.summary,
"boot": gateway.get_boot_status().to_dict(),
}
async def get_gateway_healthz() -> dict[str, Any]:
return {"status": "ok"}
async def get_hook_status() -> dict[str, Any]:
result = gateway.get_hook_status()
return result.to_dict()
async def reload_hooks() -> dict[str, Any]:
result = gateway.reload_hooks()
return result.to_dict()
async def install_hook(hook_id: str, hook_md_content: str, target: str = "workspace") -> dict[str, Any]:
return gateway.install_hook(hook_id, hook_md_content, target)
async def uninstall_hook(source: str, hook_id: str) -> dict[str, Any]:
return gateway.uninstall_hook(source, hook_id)