新增全渠道网关的 API 路由层和网关端点。 主要变更: - channel_router.py: 渠道管理 API 路由(CRUD、配置、绑定等) - gateway_sse_router.py: SSE 网关端点 - gateway_ws_router.py: WebSocket 网关端点 - webhook_router.py: Webhook 回调路由 - auth_router.py: 认证路由更新 - routers/__init__.py: 路由注册更新 - main.py: 主入口更新 - lifespan.py: 应用生命周期更新,渠道扩展加载与卸载
661 lines
22 KiB
Python
661 lines
22 KiB
Python
from pydantic import BaseModel, Field
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from yuxi.channel.doctor import doctor_runner
|
|
from yuxi.channel.protocols import DirectoryProtocol, DirectorySearchParams
|
|
from yuxi.channel.plugins.registry import ChannelPluginRegistry
|
|
from yuxi.services.channel_service import (
|
|
approve_channel_pairing,
|
|
create_channel_binding,
|
|
create_channel_config,
|
|
create_thread_mapping,
|
|
create_user_mapping,
|
|
delete_channel_binding,
|
|
delete_channel_config,
|
|
delete_channel_pairing_record,
|
|
delete_thread_mapping,
|
|
delete_user_mapping,
|
|
find_thread_mapping_by_channel_chat,
|
|
find_thread_mapping_by_thread_id,
|
|
find_user_mapping_by_channel,
|
|
find_user_mappings_by_internal_user,
|
|
generate_agent_pairing_code,
|
|
get_channel_binding,
|
|
get_channel_config,
|
|
get_channel_pairing_record,
|
|
get_gateway_health,
|
|
get_gateway_healthz,
|
|
get_hook_status,
|
|
get_thread_mapping,
|
|
get_user_mapping,
|
|
install_hook,
|
|
list_channel_bindings,
|
|
list_channel_bindings_by_type,
|
|
list_channel_configs,
|
|
list_channel_pairing_records,
|
|
list_user_mappings,
|
|
reject_channel_pairing,
|
|
reload_hooks,
|
|
resolve_channel_thread,
|
|
resolve_channel_user,
|
|
uninstall_hook,
|
|
update_channel_binding,
|
|
update_channel_config,
|
|
)
|
|
from yuxi.storage.postgres.models_business import User
|
|
|
|
from server.utils.auth_middleware import get_admin_user
|
|
|
|
channel = APIRouter(prefix="/system/channel", tags=["channel"])
|
|
gateway_router = APIRouter(prefix="/system/gateway", tags=["gateway"])
|
|
|
|
|
|
class RepairRequest(BaseModel):
|
|
step_id: str = Field(..., min_length=1)
|
|
account_id: str = "default"
|
|
|
|
|
|
class ChannelConfigCreate(BaseModel):
|
|
channel_type: str
|
|
name: str
|
|
config: dict | None = None
|
|
|
|
|
|
class ChannelConfigUpdate(BaseModel):
|
|
channel_type: str | None = None
|
|
name: str | None = None
|
|
config: dict | None = None
|
|
enabled: bool | None = None
|
|
|
|
|
|
class ChannelBindingCreate(BaseModel):
|
|
channel_type: str
|
|
channel_config_id: str | None = None
|
|
account_id: str | None = None
|
|
peer_kind: str | None = None
|
|
peer_id: str | None = None
|
|
agent_config_id: int
|
|
priority: int = 0
|
|
dm_scope: str = "per-channel-peer"
|
|
guild_id: str | None = None
|
|
team_id: str | None = None
|
|
roles: list[str] | None = None
|
|
session_dm_scope: str | None = None
|
|
|
|
|
|
class ChannelBindingUpdate(BaseModel):
|
|
channel_type: str | None = None
|
|
channel_config_id: str | None = None
|
|
account_id: str | None = None
|
|
peer_kind: str | None = None
|
|
peer_id: str | None = None
|
|
agent_config_id: int | None = None
|
|
priority: int | None = None
|
|
dm_scope: str | None = None
|
|
guild_id: str | None = None
|
|
team_id: str | None = None
|
|
roles: list[str] | None = None
|
|
session_dm_scope: str | None = None
|
|
|
|
|
|
class ChannelUserMappingCreate(BaseModel):
|
|
channel_id: str
|
|
channel_user_id: str
|
|
internal_user_id: str
|
|
|
|
|
|
class ChannelThreadMappingCreate(BaseModel):
|
|
channel_id: str
|
|
channel_chat_id: str
|
|
internal_user_id: str
|
|
thread_id: str
|
|
agent_id: str | None = None
|
|
|
|
|
|
# =============================================================================
|
|
# === 渠道配置 CRUD ===
|
|
# =============================================================================
|
|
|
|
|
|
@channel.get("/channels")
|
|
async def list_channels(
|
|
channel_type: str | None = Query(None, description="按渠道类型过滤"),
|
|
enabled: bool | None = Query(None, description="按启用状态过滤"),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
return await list_channel_configs(channel_type=channel_type, enabled=enabled)
|
|
|
|
|
|
@channel.get("/channels/{config_id}")
|
|
async def get_channel(config_id: str, current_user: User = Depends(get_admin_user)):
|
|
config = await get_channel_config(config_id)
|
|
if not config:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="渠道配置不存在")
|
|
return config
|
|
|
|
|
|
@channel.post("/channels", status_code=status.HTTP_201_CREATED)
|
|
async def create_channel(
|
|
data: ChannelConfigCreate,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
return await create_channel_config(
|
|
{
|
|
"channel_type": data.channel_type,
|
|
"name": data.name,
|
|
"config": data.config or {},
|
|
}
|
|
)
|
|
|
|
|
|
@channel.put("/channels/{config_id}")
|
|
async def update_channel(
|
|
config_id: str,
|
|
data: ChannelConfigUpdate,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
update_data = {k: v for k, v in data.model_dump().items() if v is not None}
|
|
config = await update_channel_config(config_id, update_data)
|
|
if not config:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="渠道配置不存在")
|
|
return config
|
|
|
|
|
|
@channel.delete("/channels/{config_id}", status_code=status.HTTP_200_OK)
|
|
async def delete_channel(config_id: str, current_user: User = Depends(get_admin_user)):
|
|
deleted = await delete_channel_config(config_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="渠道配置不存在")
|
|
return {"success": True, "message": "渠道配置已删除"}
|
|
|
|
|
|
# =============================================================================
|
|
# === 渠道绑定 CRUD ===
|
|
# =============================================================================
|
|
|
|
|
|
@channel.get("/bindings")
|
|
async def list_bindings(
|
|
agent_config_id: int | None = Query(None, description="按 AgentConfig ID 过滤"),
|
|
channel_type: str | None = Query(None, description="按渠道类型过滤"),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
if channel_type:
|
|
return await list_channel_bindings_by_type(channel_type)
|
|
return await list_channel_bindings(agent_config_id=agent_config_id)
|
|
|
|
|
|
@channel.get("/bindings/{binding_id}")
|
|
async def get_binding(binding_id: str, current_user: User = Depends(get_admin_user)):
|
|
binding = await get_channel_binding(binding_id)
|
|
if not binding:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="渠道绑定不存在")
|
|
return binding
|
|
|
|
|
|
@channel.post("/bindings", status_code=status.HTTP_201_CREATED)
|
|
async def create_binding(
|
|
data: ChannelBindingCreate,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
return await create_channel_binding(data.model_dump())
|
|
|
|
|
|
@channel.put("/bindings/{binding_id}")
|
|
async def update_binding(
|
|
binding_id: str,
|
|
data: ChannelBindingUpdate,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
update_data = {k: v for k, v in data.model_dump().items() if v is not None}
|
|
binding = await update_channel_binding(binding_id, update_data)
|
|
if not binding:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="渠道绑定不存在")
|
|
return binding
|
|
|
|
|
|
@channel.delete("/bindings/{binding_id}", status_code=status.HTTP_200_OK)
|
|
async def delete_binding(binding_id: str, current_user: User = Depends(get_admin_user)):
|
|
deleted = await delete_channel_binding(binding_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="渠道绑定不存在")
|
|
return {"success": True, "message": "渠道绑定已删除"}
|
|
|
|
|
|
# =============================================================================
|
|
# === 配对管理 ===
|
|
# =============================================================================
|
|
|
|
|
|
@channel.get("/pairing")
|
|
async def list_pairing(
|
|
status_filter: str | None = Query(None, alias="status"),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
return await list_channel_pairing_records(status=status_filter)
|
|
|
|
|
|
@channel.get("/pairing/agent/{agent_config_id}/code")
|
|
async def get_agent_pairing_code(
|
|
agent_config_id: int,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
try:
|
|
return await generate_agent_pairing_code(agent_config_id)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
|
|
@channel.post("/pairing/{record_id}/approve")
|
|
async def approve_pairing(
|
|
record_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
record = await approve_channel_pairing(record_id)
|
|
if not record:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="配对记录不存在")
|
|
return record
|
|
|
|
|
|
@channel.post("/pairing/{record_id}/reject")
|
|
async def reject_pairing(
|
|
record_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
record = await reject_channel_pairing(record_id)
|
|
if not record:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="配对记录不存在")
|
|
return record
|
|
|
|
|
|
@channel.get("/pairing/{record_id}")
|
|
async def get_pairing(
|
|
record_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
record = await get_channel_pairing_record(record_id)
|
|
if not record:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="配对记录不存在")
|
|
return record
|
|
|
|
|
|
@channel.delete("/pairing/{record_id}", status_code=status.HTTP_200_OK)
|
|
async def delete_pairing(
|
|
record_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
deleted = await delete_channel_pairing_record(record_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="配对记录不存在")
|
|
return {"success": True, "message": "配对记录已删除"}
|
|
|
|
|
|
# =============================================================================
|
|
# === Gateway 健康检查 ===
|
|
# =============================================================================
|
|
|
|
|
|
@gateway_router.get("/health")
|
|
async def gateway_health():
|
|
return await get_gateway_health()
|
|
|
|
|
|
@gateway_router.get("/healthz")
|
|
async def gateway_healthz():
|
|
return await get_gateway_healthz()
|
|
|
|
|
|
@gateway_router.get("/health/ready")
|
|
async def gateway_readiness():
|
|
from yuxi.channel.runtime.manager import gateway
|
|
|
|
trace = gateway.get_startup_trace()
|
|
if trace is None:
|
|
return {
|
|
"ready": False,
|
|
"state": "not_ready",
|
|
"reason": "Gateway not started",
|
|
}
|
|
|
|
success = bool(trace.get("completed_at")) and not trace.get("failed_at")
|
|
ready_state = trace.get("ready_state", "unknown")
|
|
|
|
return {
|
|
"ready": success and ready_state in ("ready", "degraded"),
|
|
"state": ready_state,
|
|
"trace": trace,
|
|
}
|
|
|
|
|
|
@gateway_router.get("/health/boot")
|
|
async def gateway_boot_status():
|
|
from yuxi.channel.runtime.manager import gateway
|
|
|
|
return gateway.get_boot_status()
|
|
|
|
|
|
@gateway_router.get("/health/channels")
|
|
async def gateway_channels_health():
|
|
from yuxi.channel.monitoring.readiness import readiness_checker
|
|
|
|
result = await readiness_checker.check()
|
|
return {
|
|
"status": "healthy" if result.ready else "degraded",
|
|
"timestamp": result.checked_at,
|
|
"channels": [
|
|
{
|
|
"key": c.key,
|
|
"healthy": c.healthy,
|
|
"reason": c.reason,
|
|
"detail": c.detail,
|
|
}
|
|
for c in result.channels
|
|
],
|
|
"failing": result.failing,
|
|
"uptime_ms": result.uptime_ms,
|
|
}
|
|
|
|
|
|
@gateway_router.get("/health/channels/metrics")
|
|
async def gateway_channels_health_metrics():
|
|
from yuxi.channel.monitoring.health_monitor import channel_health_monitor
|
|
|
|
return channel_health_monitor.metrics.to_dict()
|
|
|
|
|
|
@gateway_router.get("/hooks")
|
|
async def gateway_hook_status():
|
|
return await get_hook_status()
|
|
|
|
|
|
class InstallHookRequest(BaseModel):
|
|
hook_id: str
|
|
hook_md_content: str
|
|
target: str = "workspace"
|
|
|
|
|
|
@gateway_router.post("/hooks/install", status_code=status.HTTP_201_CREATED)
|
|
async def gateway_install_hook(req: InstallHookRequest, _user=Depends(get_admin_user)):
|
|
return await install_hook(req.hook_id, req.hook_md_content, req.target)
|
|
|
|
|
|
@gateway_router.delete("/hooks/{source}/{hook_id}")
|
|
async def gateway_uninstall_hook(source: str, hook_id: str, _user=Depends(get_admin_user)):
|
|
result = await uninstall_hook(source, hook_id)
|
|
if not result.get("success"):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=result.get("error"))
|
|
return result
|
|
|
|
|
|
@gateway_router.post("/hooks/reload")
|
|
async def gateway_reload_hooks(_user=Depends(get_admin_user)):
|
|
return await reload_hooks()
|
|
|
|
|
|
# =============================================================================
|
|
# === 渠道诊断 ===
|
|
# =============================================================================
|
|
|
|
|
|
async def _resolve_config(channel_type: str, account_id: str) -> dict:
|
|
plugin = ChannelPluginRegistry.get(channel_type)
|
|
if plugin is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"渠道 {channel_type} 未注册")
|
|
return await plugin.resolve_account(account_id)
|
|
|
|
|
|
@channel.get("/{channel_type}/diagnose")
|
|
async def diagnose_channel(
|
|
channel_type: str,
|
|
account_id: str = Query("default"),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
config = await _resolve_config(channel_type, account_id)
|
|
result = await doctor_runner.diagnose(channel_type, account_id, config)
|
|
return result.to_dict()
|
|
|
|
|
|
@channel.post("/{channel_type}/diagnose/repair")
|
|
async def repair_channel(
|
|
channel_type: str,
|
|
body: RepairRequest,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
config = await _resolve_config(channel_type, body.account_id)
|
|
result = await doctor_runner.repair(channel_type, body.step_id, config)
|
|
return {
|
|
"step_id": result.step_id,
|
|
"success": result.success,
|
|
"message": result.message,
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# === Directory 目录查询 ===
|
|
# =============================================================================
|
|
|
|
|
|
async def _resolve_plugin_direct(config_id: str):
|
|
config = await get_channel_config(config_id)
|
|
if not config:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="渠道配置不存在")
|
|
channel_type = config["channel_type"]
|
|
plugin = ChannelPluginRegistry.get(channel_type)
|
|
if not plugin or not isinstance(plugin, DirectoryProtocol):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"渠道 {channel_type} 不支持目录查询",
|
|
)
|
|
return plugin, config
|
|
|
|
|
|
def _plugin_config(config: dict) -> dict:
|
|
cfg = config.get("config")
|
|
return cfg if isinstance(cfg, dict) else {}
|
|
|
|
|
|
@channel.get("/{config_id}/directory/peers")
|
|
async def list_directory_peers(
|
|
config_id: str,
|
|
query: str = Query(default=""),
|
|
limit: int = Query(default=20, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
plugin, config = await _resolve_plugin_direct(config_id)
|
|
plugin_cfg = _plugin_config(config)
|
|
|
|
if query:
|
|
params = DirectorySearchParams(query=query, kind="user", limit=limit, offset=offset)
|
|
result = await plugin.search_peers(plugin_cfg, params)
|
|
return {"items": [item.__dict__ for item in result.items], "total": result.total, "has_more": result.has_more}
|
|
|
|
peers = await plugin.list_peers(plugin_cfg)
|
|
return {"items": [p.__dict__ for p in peers], "total": len(peers)}
|
|
|
|
|
|
@channel.get("/{config_id}/directory/groups")
|
|
async def list_directory_groups(
|
|
config_id: str,
|
|
query: str = Query(default=""),
|
|
limit: int = Query(default=20, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
plugin, config = await _resolve_plugin_direct(config_id)
|
|
plugin_cfg = _plugin_config(config)
|
|
|
|
if query:
|
|
params = DirectorySearchParams(query=query, kind="group", limit=limit, offset=offset)
|
|
result = await plugin.search_groups(plugin_cfg, params)
|
|
return {"items": [item.__dict__ for item in result.items], "total": result.total, "has_more": result.has_more}
|
|
|
|
groups = await plugin.list_groups(plugin_cfg)
|
|
return {"items": [g.__dict__ for g in groups], "total": len(groups)}
|
|
|
|
|
|
@channel.get("/{config_id}/directory/peers/{peer_id}")
|
|
async def get_directory_peer(
|
|
config_id: str,
|
|
peer_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
plugin, config = await _resolve_plugin_direct(config_id)
|
|
peer = await plugin.get_peer(_plugin_config(config), peer_id)
|
|
if peer is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
|
return peer.__dict__
|
|
|
|
|
|
@channel.get("/{config_id}/directory/groups/{group_id}")
|
|
async def get_directory_group(
|
|
config_id: str,
|
|
group_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
plugin, config = await _resolve_plugin_direct(config_id)
|
|
group = await plugin.get_group(_plugin_config(config), group_id)
|
|
if group is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="群组不存在")
|
|
return group.__dict__
|
|
|
|
|
|
# =============================================================================
|
|
# === 渠道用户映射 CRUD ===
|
|
# =============================================================================
|
|
|
|
|
|
@channel.get("/user-mappings/{mapping_id}")
|
|
async def get_user_mapping_endpoint(mapping_id: int, current_user: User = Depends(get_admin_user)):
|
|
mapping = await get_user_mapping(mapping_id)
|
|
if not mapping:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户映射不存在")
|
|
return mapping
|
|
|
|
|
|
@channel.get("/user-mappings")
|
|
async def find_user_mapping(
|
|
channel_id: str | None = Query(default=None),
|
|
channel_user_id: str | None = Query(default=None),
|
|
internal_user_id: str | None = Query(default=None),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
if channel_id and channel_user_id:
|
|
mapping = await find_user_mapping_by_channel(channel_id, channel_user_id)
|
|
return {"items": [mapping] if mapping else [], "total": 1 if mapping else 0}
|
|
if internal_user_id:
|
|
mappings = await find_user_mappings_by_internal_user(internal_user_id)
|
|
return {"items": mappings, "total": len(mappings)}
|
|
mappings = await list_user_mappings()
|
|
return {"items": mappings, "total": len(mappings)}
|
|
|
|
|
|
@channel.post("/user-mappings", status_code=status.HTTP_201_CREATED)
|
|
async def create_user_mapping_endpoint(
|
|
data: ChannelUserMappingCreate,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
return await create_user_mapping(data.model_dump())
|
|
|
|
|
|
@channel.post("/user-mappings/resolve")
|
|
async def resolve_user_mapping(
|
|
channel_id: str = Query(...),
|
|
channel_user_id: str = Query(...),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
return await resolve_channel_user(channel_id, channel_user_id)
|
|
|
|
|
|
@channel.delete("/user-mappings/{mapping_id}", status_code=status.HTTP_200_OK)
|
|
async def delete_user_mapping_endpoint(mapping_id: int, current_user: User = Depends(get_admin_user)):
|
|
deleted = await delete_user_mapping(mapping_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户映射不存在")
|
|
return {"success": True, "message": "用户映射已删除"}
|
|
|
|
|
|
# =============================================================================
|
|
# === 渠道会话线程映射 CRUD ===
|
|
# =============================================================================
|
|
|
|
|
|
@channel.get("/thread-mappings/{mapping_id}")
|
|
async def get_thread_mapping_endpoint(mapping_id: int, current_user: User = Depends(get_admin_user)):
|
|
mapping = await get_thread_mapping(mapping_id)
|
|
if not mapping:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="线程映射不存在")
|
|
return mapping
|
|
|
|
|
|
@channel.get("/thread-mappings")
|
|
async def find_thread_mapping(
|
|
channel_id: str | None = Query(default=None),
|
|
channel_chat_id: str | None = Query(default=None),
|
|
internal_user_id: str | None = Query(default=None),
|
|
thread_id: str | None = Query(default=None),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
if channel_id and channel_chat_id and internal_user_id:
|
|
mapping = await find_thread_mapping_by_channel_chat(
|
|
channel_id,
|
|
channel_chat_id,
|
|
internal_user_id,
|
|
)
|
|
return {"items": [mapping] if mapping else [], "total": 1 if mapping else 0}
|
|
if thread_id:
|
|
mapping = await find_thread_mapping_by_thread_id(thread_id)
|
|
return {"items": [mapping] if mapping else [], "total": 1 if mapping else 0}
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="请提供 channel_id+channel_chat_id+internal_user_id 或 thread_id",
|
|
)
|
|
|
|
|
|
@channel.post("/thread-mappings", status_code=status.HTTP_201_CREATED)
|
|
async def create_thread_mapping_endpoint(
|
|
data: ChannelThreadMappingCreate,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
return await create_thread_mapping(data.model_dump())
|
|
|
|
|
|
@channel.post("/thread-mappings/resolve")
|
|
async def resolve_thread_mapping(
|
|
channel_id: str = Query(...),
|
|
channel_chat_id: str = Query(...),
|
|
internal_user_id: str = Query(...),
|
|
agent_id: str | None = Query(default=None),
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
return await resolve_channel_thread(channel_id, channel_chat_id, internal_user_id, agent_id)
|
|
|
|
|
|
@channel.delete("/thread-mappings/{mapping_id}", status_code=status.HTTP_200_OK)
|
|
async def delete_thread_mapping_endpoint(mapping_id: int, current_user: User = Depends(get_admin_user)):
|
|
deleted = await delete_thread_mapping(mapping_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="线程映射不存在")
|
|
return {"success": True, "message": "线程映射已删除"}
|
|
|
|
|
|
@channel.get("/ui-schemas")
|
|
async def list_channel_ui_schemas(current_user: User = Depends(get_admin_user)):
|
|
from yuxi.channel.ui.schema import list_all_ui_metadata
|
|
|
|
schemas = [m.to_dict() for m in list_all_ui_metadata()]
|
|
return {"schemas": schemas, "total": len(schemas)}
|
|
|
|
|
|
@channel.get("/ui-schemas/{channel_type}")
|
|
async def get_channel_ui_schema(channel_type: str, current_user: User = Depends(get_admin_user)):
|
|
from yuxi.channel.ui.schema import get_channel_ui_metadata
|
|
|
|
metadata = get_channel_ui_metadata(channel_type)
|
|
if metadata is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"渠道类型 '{channel_type}' 的 UI Schema 不存在",
|
|
)
|
|
return metadata.to_dict()
|