import json import logging import uuid from collections.abc import AsyncGenerator from sqlalchemy import select from yuxi.channel.gateway.protocol import ( GatewayErrorCode, GatewayRpcMethod, RpcEvent, RpcRequest, RpcResponse, ) from yuxi.channel.gateway.rpc_dispatcher import rpc_dispatcher from yuxi.channel.gateway.validation import ( ProbeParams, SendMessageParams, StartAccountParams, StopAccountParams, validate_params, ) from yuxi.channel.runtime.manager import gateway from yuxi.storage.postgres.manager import pg_manager logger = logging.getLogger(__name__) def _is_placeholder_user(user_id: str | None) -> bool: if not user_id: return True return user_id in ("shared", "bootstrap") or user_id.startswith(("tailscale:", "device:", "proxy:")) async def _resolve_caller_user(request: RpcRequest, db): from yuxi.storage.postgres.models_business import User user_id = request.caller_user_id if user_id and not _is_placeholder_user(user_id): try: int_id = int(user_id) result = await db.execute(select(User).where(User.id == int_id)) user = result.scalar_one_or_none() if user: return user except (ValueError, TypeError): pass try: from sqlalchemy import func as sa_func result = await db.execute(select(User).where(sa_func.cast(User.id, str) == user_id)) user = result.scalar_one_or_none() if user: return user except Exception: pass result = await db.execute(select(User).limit(1)) user = result.scalar_one_or_none() if user: logger.warning( "RPC handler using fallback user %s for caller_user_id=%s method=%s", user.id, user_id, request.method, ) return user async def _handle_health(request: RpcRequest) -> RpcResponse: report = gateway.get_health() event_loop = None try: from yuxi.channel.monitoring.event_loop_monitor import event_loop_monitor loop_health = event_loop_monitor.health() loop_stats = event_loop_monitor.stats() event_loop = { "degraded": loop_health.degraded, "reasons": loop_health.reasons, "interval_ms": loop_health.interval_ms, "delay": { "p50_ms": loop_stats.p50_ms, "p90_ms": loop_stats.p90_ms, "p99_ms": loop_stats.p99_ms, "p999_ms": loop_stats.p999_ms, "max_ms": loop_stats.max_ms, "min_ms": loop_stats.min_ms, "avg_ms": loop_stats.avg_ms, }, "utilization": loop_stats.utilization, "cpu_core_ratio": loop_health.cpu_core_ratio, "samples": loop_stats.samples, "warnings": loop_stats.warnings, } except Exception: logger.exception("Failed to collect event loop health") return RpcResponse( id=request.id, ok=True, result={ "status": report.status, "summary": { "total": report.summary.total, "running": report.summary.running, "stopped": report.summary.stopped, "degraded": report.summary.degraded, "unhealthy": report.summary.unhealthy, "unknown": report.summary.unknown, "connected": report.summary.connected, "total_active_runs": report.summary.total_active_runs, }, "channels": report.channels, "event_loop": event_loop, }, ) async def _handle_channels_list(request: RpcRequest) -> RpcResponse: snapshots = gateway.get_all_snapshots() return RpcResponse( id=request.id, ok=True, result={"channels": [s.to_dict() for s in snapshots.values()]}, ) async def _handle_channels_status(request: RpcRequest) -> RpcResponse: channel_type = request.params.get("channel_type") if request.params else None account_id = request.params.get("account_id", "default") if request.params else "default" if channel_type: snapshot = gateway.get_snapshot(channel_type, account_id) if snapshot is None: return RpcResponse( id=request.id, ok=False, error_code=None, error_message=f"渠道 {channel_type}:{account_id} 未找到", ) return RpcResponse(id=request.id, ok=True, result=snapshot.to_dict()) snapshots = gateway.get_all_snapshots() return RpcResponse( id=request.id, ok=True, result={"channels": [s.to_dict() for s in snapshots.values()]}, ) @validate_params(ProbeParams) async def _handle_channels_probe(request: RpcRequest) -> RpcResponse: validated = request._validated_params snapshot = gateway.get_snapshot(validated.channel_type, validated.account_id) if snapshot is None: return RpcResponse( id=request.id, ok=False, error_code=None, error_message=f"渠道 {validated.channel_type}:{validated.account_id} 未找到", ) return RpcResponse( id=request.id, ok=True, result={ "channel_type": snapshot.channel_type, "account_id": snapshot.account_id, "state": snapshot.state.value, "connected": snapshot.connected, "configured": snapshot.configured, "enabled": snapshot.enabled, "healthy": snapshot.health_state.value if snapshot.health_state else "unknown", "last_connected_at": snapshot.last_connected_at, "last_disconnect": snapshot.last_disconnect.to_dict() if snapshot.last_disconnect else None, "last_error": snapshot.last_error, }, ) @validate_params(StartAccountParams) async def _handle_channels_start(request: RpcRequest) -> RpcResponse: validated = request._validated_params channel_type = validated.channel_type account_id = validated.account_id if channel_type: snapshot = await gateway.start_channel(channel_type, account_id) return RpcResponse(id=request.id, ok=True, result=snapshot.to_dict()) await gateway.start_all() snapshots = gateway.get_all_snapshots() return RpcResponse( id=request.id, ok=True, result={"channels": [s.to_dict() for s in snapshots.values()]}, ) @validate_params(StopAccountParams) async def _handle_channels_stop(request: RpcRequest) -> RpcResponse: validated = request._validated_params channel_type = validated.channel_type account_id = validated.account_id if channel_type: snapshot = await gateway.stop_channel(channel_type, account_id) return RpcResponse(id=request.id, ok=True, result=snapshot.to_dict()) await gateway.stop_all() return RpcResponse(id=request.id, ok=True, result={"stopped": True}) async def _handle_channels_restart(request: RpcRequest) -> RpcResponse: channel_type = request.params.get("channel_type") if request.params else None account_id = request.params.get("account_id", "default") if request.params else "default" if not channel_type: return RpcResponse( id=request.id, ok=False, error_code=None, error_message="restart 需要指定 channel_type", ) await gateway.stop_channel(channel_type, account_id) snapshot = await gateway.start_channel(channel_type, account_id) return RpcResponse(id=request.id, ok=True, result=snapshot.to_dict()) async def _handle_channels_configure(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") account_id = params.get("account_id", "default") if not channel_type: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 参数") try: snapshot = gateway.get_snapshot(channel_type, account_id) if snapshot is None: return RpcResponse( id=request.id, ok=False, error_message=f"渠道 {channel_type}:{account_id} 未找到", ) config_update = params.get("config", {}) if config_update: from yuxi.channel.config.reloader import reload_channel_config await reload_channel_config(channel_type, account_id, config_update) return RpcResponse( id=request.id, ok=True, result={"channel_type": channel_type, "account_id": account_id, "configured": True}, ) except Exception: logger.exception("channels.configure failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="channels.configure 执行异常", ) async def _handle_chat_send(request: RpcRequest) -> RpcResponse: params = request.params or {} query = params.get("query", "") agent_config_id = params.get("agent_config_id") thread_id = params.get("thread_id") request_id = str(uuid.uuid4()) if not query: return RpcResponse(id=request.id, ok=False, error_message="缺少 query 参数") if not agent_config_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 agent_config_id 参数") try: from yuxi.services.chat_service import agent_chat async with pg_manager.get_async_session_context() as db: current_user = await _resolve_caller_user(request, db) meta = { "source": "gateway_rpc", "channel_type": params.get("channel_type", "rpc"), "account_id": params.get("account_id", "default"), "request_id": request_id, } reply = await agent_chat( query=query, agent_config_id=int(agent_config_id), thread_id=thread_id, meta=meta, image_content=params.get("image_content"), current_user=current_user, db=db, ) return RpcResponse( id=request.id, ok=True, result={ "response": reply.get("response", ""), "thread_id": reply.get("thread_id", thread_id), "status": reply.get("status", "finished"), "request_id": request_id, }, ) except Exception: logger.exception("chat.send failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="chat.send 执行异常", ) async def _handle_chat_stream(request: RpcRequest) -> AsyncGenerator[RpcEvent, None]: params = request.params or {} query = params.get("query", "") agent_config_id = params.get("agent_config_id") thread_id = params.get("thread_id") request_id = str(uuid.uuid4()) if not query or not agent_config_id: yield RpcResponse(id=request.id, ok=False, error_message="缺少 query 或 agent_config_id 参数") return try: from yuxi.services.chat_service import stream_agent_chat async with pg_manager.get_async_session_context() as db: current_user = await _resolve_caller_user(request, db) meta = { "source": "gateway_rpc", "channel_type": params.get("channel_type", "rpc"), "account_id": params.get("account_id", "default"), "peer_id": params.get("peer_id", ""), "request_id": request_id, } async for chunk in stream_agent_chat( query=query, agent_config_id=int(agent_config_id), thread_id=thread_id, meta=meta, image_content=params.get("image_content"), current_user=current_user, db=db, ): try: data = json.loads(chunk.decode("utf-8")) except (json.JSONDecodeError, UnicodeDecodeError): continue yield RpcEvent( event="chat.chunk", data={ "id": request.id, "status": data.get("status", ""), "response": data.get("response", ""), "thread_id": data.get("thread_id", thread_id), "request_id": request_id, }, ) yield RpcResponse(id=request.id, ok=True, result={"status": "finished", "request_id": request_id}) except Exception: logger.exception("chat.stream failed") yield RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="chat.stream 执行异常", ) async def _handle_chat_history(request: RpcRequest) -> RpcResponse: params = request.params or {} thread_id = params.get("thread_id") limit = params.get("limit", 50) offset = params.get("offset", 0) if not thread_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 thread_id 参数") try: from yuxi.repositories.conversation_repository import ConversationRepository async with pg_manager.get_async_session_context() as db: conv_repo = ConversationRepository(db) messages = await conv_repo.get_messages_by_thread_id(thread_id, limit=limit, offset=offset) return RpcResponse( id=request.id, ok=True, result={ "thread_id": thread_id, "messages": [ { "id": m.id, "role": m.role, "content": m.content, "message_type": m.message_type, "created_at": m.created_at.isoformat() if m.created_at else None, } for m in messages ], }, ) except Exception: logger.exception("chat.history failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="chat.history 执行异常", ) async def _handle_chat_cancel(request: RpcRequest) -> RpcResponse: params = request.params or {} run_id = params.get("run_id") thread_id = params.get("thread_id") if not run_id and not thread_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 run_id 或 thread_id 参数") try: async with pg_manager.get_async_session_context() as db: current_user = await _resolve_caller_user(request, db) current_user_id = str(current_user.id) if current_user else None if run_id and current_user_id: from yuxi.services.agent_run_service import cancel_agent_run_view async with pg_manager.get_async_session_context() as db: await cancel_agent_run_view( run_id=run_id, current_user_id=current_user_id, db=db, ) elif thread_id and current_user_id: from yuxi.services.agent_run_service import get_active_run_by_thread async with pg_manager.get_async_session_context() as db: active = await get_active_run_by_thread( thread_id=thread_id, current_user_id=current_user_id, db=db, ) if active and active.get("run_id"): from yuxi.services.agent_run_service import cancel_agent_run_view await cancel_agent_run_view( run_id=active["run_id"], current_user_id=current_user_id, db=db, ) return RpcResponse(id=request.id, ok=True, result={"cancelled": True}) except Exception: logger.exception("chat.cancel failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="chat.cancel 执行异常", ) async def _handle_sessions_list(request: RpcRequest) -> RpcResponse: params = request.params or {} limit = params.get("limit", 50) offset = params.get("offset", 0) try: from yuxi.repositories.conversation_repository import ConversationRepository async with pg_manager.get_async_session_context() as db: conv_repo = ConversationRepository(db) conversations = await conv_repo.list_conversations(limit=limit, offset=offset) return RpcResponse( id=request.id, ok=True, result={ "sessions": [ { "id": conv.thread_id, "title": conv.title, "created_at": conv.created_at.isoformat() if conv.created_at else None, "updated_at": conv.updated_at.isoformat() if conv.updated_at else None, } for conv in conversations ], }, ) except Exception: logger.exception("sessions.list failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="sessions.list 执行异常", ) async def _handle_sessions_history(request: RpcRequest) -> RpcResponse: params = request.params or {} thread_id = params.get("thread_id") limit = params.get("limit", 100) offset = params.get("offset", 0) if not thread_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 thread_id 参数") try: from yuxi.repositories.conversation_repository import ConversationRepository async with pg_manager.get_async_session_context() as db: conv_repo = ConversationRepository(db) messages = await conv_repo.get_messages_by_thread_id(thread_id, limit=limit, offset=offset) return RpcResponse( id=request.id, ok=True, result={ "thread_id": thread_id, "messages": [ { "id": m.id, "role": m.role, "content": m.content, "message_type": m.message_type, "created_at": m.created_at.isoformat() if m.created_at else None, } for m in messages ], }, ) except Exception: logger.exception("sessions.history failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="sessions.history 执行异常", ) async def _handle_plugins_list(request: RpcRequest) -> RpcResponse: from yuxi.channel.plugins.registry import ChannelPluginRegistry plugins = ChannelPluginRegistry.list_all() return RpcResponse( id=request.id, ok=True, result={ "plugins": [ { "id": p.id, "name": p.name, "order": p.order, } for p in plugins ], }, ) async def _handle_plugins_install(request: RpcRequest) -> RpcResponse: return RpcResponse( id=request.id, ok=True, result={"message": "plugins.install: 插件动态安装需要通过 Plugin SDK 完成,当前仅支持内置渠道"}, ) async def _handle_system_config(request: RpcRequest) -> RpcResponse: snapshots = gateway.get_all_snapshots() channel_configs = { channel_type: { "account_count": len(snapshot.accounts) if hasattr(snapshot, "accounts") else 1, "status": snapshot.status, } for channel_type, snapshot in snapshots.items() } return RpcResponse(id=request.id, ok=True, result={"channels": channel_configs}) async def _handle_system_version(request: RpcRequest) -> RpcResponse: try: from yuxi import __version__ as yuxi_version except ImportError: yuxi_version = "unknown" return RpcResponse( id=request.id, ok=True, result={"version": yuxi_version, "platform": "yuxi"}, ) async def _handle_system_log_tail(request: RpcRequest) -> RpcResponse: params = request.params or {} lines = params.get("lines", 50) return RpcResponse( id=request.id, ok=True, result={ "message": f"system.log.tail: 请求尾部 {lines} 行日志,请通过 HTTP API /api/logs 获取", "hint": "use GET /api/logs?tail={lines} instead", }, ) async def _handle_pairing_list(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") account_id = params.get("account_id", "default") if not channel_type: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 参数") try: from yuxi.channel.security.pairing import PairingManager manager = PairingManager() pending = await manager.list_pending(channel_type, account_id) return RpcResponse( id=request.id, ok=True, result={ "pending": [ { "channel_type": r.channel_type, "peer_id": r.peer_id, "account_id": r.account_id, "code": r.code, "created_at": r.created_at, } for r in pending ], }, ) except Exception: logger.exception("pairing.list failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="pairing.list 执行异常", ) async def _handle_pairing_approve(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") peer_id = params.get("peer_id") account_id = params.get("account_id", "default") if not channel_type or not peer_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 或 peer_id 参数") try: from yuxi.channel.security.allowlist import get_allowlist_checker from yuxi.channel.security.pairing import PairingManager allowlist = await get_allowlist_checker() manager = PairingManager(allowlist=allowlist) await manager.approve(channel_type, peer_id, account_id) return RpcResponse( id=request.id, ok=True, result={"approved": True, "peer_id": peer_id, "channel_type": channel_type}, ) except Exception: logger.exception("pairing.approve failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="pairing.approve 执行异常", ) async def _handle_pairing_reject(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") peer_id = params.get("peer_id") account_id = params.get("account_id", "default") if not channel_type or not peer_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 或 peer_id 参数") try: from yuxi.channel.security.allowlist import get_allowlist_checker from yuxi.channel.security.pairing import PairingManager allowlist = await get_allowlist_checker() manager = PairingManager(allowlist=allowlist) await manager.reject(channel_type, peer_id, account_id) return RpcResponse( id=request.id, ok=True, result={"rejected": True, "peer_id": peer_id, "channel_type": channel_type}, ) except Exception: logger.exception("pairing.reject failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="pairing.reject 执行异常", ) async def _handle_allowlist_get(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") if not channel_type: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 参数") try: from yuxi.channel.security.allowlist import get_allowlist_checker checker = await get_allowlist_checker() await checker.ensure_loaded(channel_type) result = { "channel_type": channel_type, "dm_policy": checker.dm_policy.value, "group_policy": checker.group_policy.value, "dm_entries": checker.list_entries(channel_type, "dm"), "group_entries": checker.list_entries(channel_type, "group"), "allow_from_entries": checker.list_entries(channel_type, "allow_from"), } return RpcResponse(id=request.id, ok=True, result=result) except Exception: logger.exception("allowlist.get failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="allowlist.get 执行异常", ) async def _handle_allowlist_add(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") list_type = params.get("list_type", "dm") entry = params.get("entry") if not channel_type or not entry: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 或 entry 参数") try: from yuxi.channel.security.allowlist import get_allowlist_checker checker = await get_allowlist_checker() await checker.ensure_loaded(channel_type) await checker.add_entry(channel_type, list_type, entry) return RpcResponse( id=request.id, ok=True, result={"message": f"将 {entry} 添加到 {channel_type} 的 {list_type} 白名单", "added": True}, ) except Exception: logger.exception("allowlist.add failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="allowlist.add 执行异常", ) async def _handle_allowlist_remove(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") list_type = params.get("list_type", "dm") entry = params.get("entry") if not channel_type or not entry: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 或 entry 参数") try: from yuxi.channel.security.allowlist import get_allowlist_checker checker = await get_allowlist_checker() await checker.ensure_loaded(channel_type) await checker.remove_entry(channel_type, list_type, entry) return RpcResponse( id=request.id, ok=True, result={"message": f"从 {channel_type} 的 {list_type} 白名单移除 {entry}", "removed": True}, ) except Exception: logger.exception("allowlist.remove failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="allowlist.remove 执行异常", ) async def _handle_cron_list(request: RpcRequest) -> RpcResponse: from yuxi.channel.cron.engine import get_cron_engine engine = get_cron_engine() jobs = engine.list_jobs() return RpcResponse( id=request.id, ok=True, result={"jobs": [j.to_dict() for j in jobs]}, ) async def _handle_cron_create(request: RpcRequest) -> RpcResponse: params = request.params or {} job_id = params.get("id") name = params.get("name", "") cron_expr = params.get("cron_expr", "*/5 * * * *") handler_ref = params.get("handler", "") if not job_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 id 参数") from yuxi.channel.cron.engine import CronJob, get_cron_engine from yuxi.channel.cron.handler_registry import register_handler from yuxi.channel.cron.types import CronDeliveryTarget, CronFailureAlert, ScheduleKind engine = get_cron_engine() schedule_kind_raw = params.get("schedule_kind", "cron") try: schedule_kind = ScheduleKind(schedule_kind_raw) except ValueError: schedule_kind = ScheduleKind.CRON schedule_value = params.get("schedule_value", cron_expr) schedule_tz = params.get("schedule_tz") stagger_ms = params.get("stagger_ms", 0) every_ms = params.get("every_ms") anchor_ms = params.get("anchor_ms") max_runs = params.get("max_runs", 0) delete_after_run = params.get("delete_after_run", False) delivery_raw = params.get("delivery", []) delivery = [CronDeliveryTarget.from_dict(t) for t in delivery_raw if isinstance(t, dict)] failure_alert = CronFailureAlert.from_dict(params.get("failure_alert")) handler_name = params.get("handler_name", handler_ref or job_id) handler = _build_cron_handler(handler_ref, params.get("handler_args", {})) try: job = CronJob( id=job_id, name=name, handler_name=handler_name, handler=handler, handler_args=params.get("handler_args", {}), schedule_kind=schedule_kind, schedule_value=schedule_value, schedule_tz=schedule_tz, stagger_ms=stagger_ms, every_ms=every_ms, anchor_ms=anchor_ms, max_runs=max_runs, delete_after_run=delete_after_run, delivery=delivery, failure_alert=failure_alert, ) engine.add_job(job) if handler_ref: register_handler(handler_name, handler) return RpcResponse( id=request.id, ok=True, result={"job": job.to_dict()}, ) except Exception as e: return RpcResponse(id=request.id, ok=False, error_message=str(e)) async def _handle_cron_delete(request: RpcRequest) -> RpcResponse: params = request.params or {} job_id = params.get("id") if not job_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 id 参数") from yuxi.channel.cron.engine import get_cron_engine engine = get_cron_engine() engine.remove_job(job_id) return RpcResponse(id=request.id, ok=True, result={"deleted": True}) async def _handle_cron_update(request: RpcRequest) -> RpcResponse: params = dict(request.params or {}) job_id = params.pop("id", None) if not job_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 id 参数") from yuxi.channel.cron.engine import get_cron_engine engine = get_cron_engine() job = engine.update_job(job_id, **params) if job is None: return RpcResponse(id=request.id, ok=False, error_message=f"任务 {job_id} 不存在") return RpcResponse(id=request.id, ok=True, result={"job": job.to_dict()}) async def _handle_cron_force_run(request: RpcRequest) -> RpcResponse: params = request.params or {} job_id = params.get("id") if not job_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 id 参数") from yuxi.channel.cron.engine import get_cron_engine engine = get_cron_engine() result = await engine.force_run(job_id) return RpcResponse(id=request.id, ok=True, result=result) async def _handle_cron_pause(request: RpcRequest) -> RpcResponse: params = request.params or {} job_id = params.get("id") if not job_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 id 参数") from yuxi.channel.cron.engine import get_cron_engine engine = get_cron_engine() engine.pause_job(job_id) return RpcResponse(id=request.id, ok=True, result={"paused": True}) async def _handle_cron_resume(request: RpcRequest) -> RpcResponse: params = request.params or {} job_id = params.get("id") if not job_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 id 参数") from yuxi.channel.cron.engine import get_cron_engine engine = get_cron_engine() engine.resume_job(job_id) return RpcResponse(id=request.id, ok=True, result={"resumed": True}) async def _handle_cron_diagnostics(request: RpcRequest) -> RpcResponse: params = request.params or {} job_id = params.get("id") limit = int(params.get("limit", 50)) offset = int(params.get("offset", 0)) from yuxi.channel.cron.engine import get_cron_engine engine = get_cron_engine() result = engine.read_diagnostics(job_id=job_id, limit=limit, offset=offset) return RpcResponse(id=request.id, ok=True, result=result) async def _handle_cron_run_log(request: RpcRequest) -> RpcResponse: params = request.params or {} job_id = params.get("id") if not job_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 id 参数") limit = int(params.get("limit", 50)) offset = int(params.get("offset", 0)) from yuxi.channel.cron.engine import get_cron_engine engine = get_cron_engine() result = engine.read_run_log(job_id=job_id, limit=limit, offset=offset) return RpcResponse(id=request.id, ok=True, result=result) def _build_cron_handler(handler_ref: str, args: dict): if handler_ref == "health_check": async def _health_check(**kwargs): from yuxi.channel.runtime.manager import gateway report = gateway.get_health() return { "status": report.status, "summary": { "total": report.summary.total, "running": report.summary.running, }, } return _health_check if handler_ref == "channel_heartbeat": async def _heartbeat(**kwargs): channel_type = kwargs.get("channel_type", "") from yuxi.channel.protocols import HeartbeatProtocol from yuxi.channel.plugins.registry import ChannelPluginRegistry plugin = ChannelPluginRegistry.get(channel_type) if plugin is not None and isinstance(plugin, HeartbeatProtocol): ok = await plugin.on_ping({}) return {"ok": ok, "channel_type": channel_type} return {"ok": False, "error": f"Channel {channel_type} not found"} return _heartbeat async def _noop(**kwargs): return {"message": f"noop handler: {handler_ref}"} return _noop async def _handle_agent_tools_list(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") if not channel_type: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 参数") from yuxi.channel.message.bridge import AgentBridge bridge = AgentBridge(lambda **kw: _empty_stream(), None) tools = bridge.collect_channel_tools(channel_type) return RpcResponse(id=request.id, ok=True, result={"tools": tools}) async def _handle_agent_tools_execute(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") tool_name = params.get("tool_name") tool_params = params.get("params", {}) context = params.get("context", {}) if not channel_type or not tool_name: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 或 tool_name 参数") from yuxi.channel.message.bridge import AgentBridge bridge = AgentBridge(lambda **kw: _empty_stream(), None) result = await bridge.execute_channel_tool(channel_type, tool_name, tool_params, context) return RpcResponse(id=request.id, ok=True, result=result) async def _empty_stream(): if False: yield b"" async def _handle_sessions_create(request: RpcRequest) -> RpcResponse: params = request.params or {} agent_id = params.get("agent_id", "default") title = params.get("title", "新对话") thread_id = params.get("thread_id") user_id = params.get("user_id") try: from yuxi.repositories.conversation_repository import ConversationRepository async with pg_manager.get_async_session_context() as db: conv_repo = ConversationRepository(db) if not user_id: current_user = await _resolve_caller_user(request, db) user_id = str(current_user.id) if current_user else "system" conversation = await conv_repo.create_conversation( user_id=user_id, agent_id=agent_id, title=title, thread_id=thread_id, metadata=params.get("metadata"), ) return RpcResponse( id=request.id, ok=True, result={ "key": conversation.thread_id, "session_id": conversation.thread_id, "title": conversation.title, "agent_id": agent_id, "created_at": conversation.created_at.isoformat() if conversation.created_at else None, }, ) except Exception: logger.exception("sessions.create failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="sessions.create 执行异常", ) async def _handle_sessions_delete(request: RpcRequest) -> RpcResponse: params = request.params or {} thread_id = params.get("thread_id") if not thread_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 thread_id 参数") try: from yuxi.repositories.conversation_repository import ConversationRepository async with pg_manager.get_async_session_context() as db: conv_repo = ConversationRepository(db) success = await conv_repo.delete_conversation(thread_id, soft_delete=True) return RpcResponse( id=request.id, ok=True, result={"key": thread_id, "deleted": success}, ) except Exception: logger.exception("sessions.delete failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="sessions.delete 执行异常", ) async def _handle_config_get(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") try: from yuxi.repositories.channel_config_repo import ChannelConfigRepository repo = ChannelConfigRepository() if channel_type: configs = await repo.list_by_type(channel_type) else: configs = await repo.list_all() return RpcResponse( id=request.id, ok=True, result={ "configs": [ c.to_dict() if hasattr(c, "to_dict") else {"id": str(c.id), "channel_type": c.channel_type, "enabled": c.enabled} for c in configs ], }, ) except Exception: logger.exception("config.get failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="config.get 执行异常", ) async def _handle_config_set(request: RpcRequest) -> RpcResponse: params = request.params or {} config_id = params.get("config_id") channel_type = params.get("channel_type") config_data = params.get("config", {}) if not config_id and not channel_type: return RpcResponse(id=request.id, ok=False, error_message="缺少 config_id 或 channel_type 参数") try: from yuxi.repositories.channel_config_repo import ChannelConfigRepository repo = ChannelConfigRepository() if config_id: updated = await repo.update(config_id, config_data) affected_channel_type = updated.channel_type if updated else channel_type affected_account_id = config_id else: configs = await repo.list_by_type(channel_type) if not configs: return RpcResponse( id=request.id, ok=False, error_message=f"渠道 {channel_type} 未找到配置", ) updated = await repo.update(str(configs[0].id), config_data) affected_channel_type = channel_type affected_account_id = str(configs[0].id) if configs else None result = RpcResponse( id=request.id, ok=True, result={ "updated": True, "config": updated.to_dict() if updated and hasattr(updated, "to_dict") else None, }, ) if updated and affected_channel_type and affected_account_id: try: from yuxi.channel.config.reloader import reload_channel_config await reload_channel_config(affected_channel_type, affected_account_id, config_data) except Exception: logger.exception( "config.set: reload_channel_config failed for %s:%s", affected_channel_type, affected_account_id, ) return result except Exception: logger.exception("config.set failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="config.set 执行异常", ) @validate_params(SendMessageParams) async def _handle_message_send(request: RpcRequest) -> RpcResponse: validated = request._validated_params channel_type = validated.channel_type target_id = validated.target_id content = validated.text account_id = validated.account_id reply_to_id = request.params.get("reply_to_id") if request.params else None thread_id = request.params.get("thread_id") if request.params else None try: from yuxi.channel.protocols import OutboundProtocol from yuxi.channel.plugins.registry import ChannelPluginRegistry plugin = ChannelPluginRegistry.get(channel_type) if plugin is None: return RpcResponse( id=request.id, ok=False, error_message=f"渠道 {channel_type} 未注册", ) if not isinstance(plugin, OutboundProtocol): return RpcResponse( id=request.id, ok=False, error_message=f"渠道 {channel_type} 不支持出站消息发送", ) await plugin.send_text( target_id=target_id, content=content, reply_to_id=reply_to_id, thread_id=thread_id, account_id=account_id, ) return RpcResponse( id=request.id, ok=True, result={ "channel": channel_type, "to": target_id, "sent": True, }, ) except Exception: logger.exception("message.send failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="message.send 执行异常", ) async def _handle_message_action(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") action = params.get("action", "") action_params = params.get("params", {}) context = params.get("context", {}) if not channel_type: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 参数") if not action: return RpcResponse(id=request.id, ok=False, error_message="缺少 action 参数") try: from yuxi.channel.sdk.actions.dispatch import ( DispatchContext, dispatch_message_action, ) ctx = DispatchContext( channel_type=channel_type, action=action, params=action_params, account_id=params.get("account_id", "default"), requester_sender_id=context.get("sender_id"), session_key=context.get("session_key"), agent_id=context.get("agent_id"), ) result = await dispatch_message_action(ctx) return RpcResponse( id=request.id, ok=result["success"], result={ "channel": channel_type, "action": action, "handled": result["success"], "message": result.get("message", ""), "data": result.get("data", {}), } if result["success"] else None, error_message=result.get("message") if not result["success"] else None, ) except Exception: logger.exception("message.action failed") return RpcResponse( id=request.id, ok=False, error_code=GatewayErrorCode.INTERNAL_ERROR, error_message="message.action 执行异常", ) async def _handle_identity_links_list(request: RpcRequest) -> RpcResponse: from yuxi.channel.security.identity_link import IdentityLinkResolver resolver = IdentityLinkResolver() await resolver.ensure_loaded() return RpcResponse(id=request.id, ok=True, result={ "links": resolver.list_links(), }) async def _handle_identity_links_add(request: RpcRequest) -> RpcResponse: params = request.params or {} identity = params.get("identity") channel_type = params.get("channel_type") peer_id = params.get("peer_id") if not all([identity, channel_type, peer_id]): return RpcResponse(id=request.id, ok=False, error_message="缺少 identity、channel_type 或 peer_id 参数") from yuxi.channel.security.identity_link import IdentityLinkResolver resolver = IdentityLinkResolver() await resolver.ensure_loaded() await resolver.add_link(identity, channel_type, peer_id) return RpcResponse(id=request.id, ok=True, result={"added": True}) async def _handle_identity_links_remove(request: RpcRequest) -> RpcResponse: params = request.params or {} channel_type = params.get("channel_type") peer_id = params.get("peer_id") if not channel_type or not peer_id: return RpcResponse(id=request.id, ok=False, error_message="缺少 channel_type 或 peer_id 参数") from yuxi.channel.security.identity_link import IdentityLinkResolver resolver = IdentityLinkResolver() await resolver.ensure_loaded() await resolver.remove_link(channel_type, peer_id) return RpcResponse(id=request.id, ok=True, result={"removed": True}) def register_all_handlers() -> None: rpc_dispatcher.register(GatewayRpcMethod.SYSTEM_HEALTH, _handle_health) rpc_dispatcher.register(GatewayRpcMethod.SYSTEM_CONFIG, _handle_system_config) rpc_dispatcher.register(GatewayRpcMethod.SYSTEM_VERSION, _handle_system_version) rpc_dispatcher.register(GatewayRpcMethod.SYSTEM_LOG_TAIL, _handle_system_log_tail) rpc_dispatcher.register(GatewayRpcMethod.CHANNELS_LIST, _handle_channels_list) rpc_dispatcher.register(GatewayRpcMethod.CHANNELS_STATUS, _handle_channels_status) rpc_dispatcher.register(GatewayRpcMethod.CHANNELS_PROBE, _handle_channels_probe) rpc_dispatcher.register(GatewayRpcMethod.CHANNELS_START, _handle_channels_start) rpc_dispatcher.register(GatewayRpcMethod.CHANNELS_STOP, _handle_channels_stop) rpc_dispatcher.register(GatewayRpcMethod.CHANNELS_RESTART, _handle_channels_restart) rpc_dispatcher.register(GatewayRpcMethod.CHANNELS_CONFIGURE, _handle_channels_configure) rpc_dispatcher.register(GatewayRpcMethod.CHAT_SEND, _handle_chat_send) rpc_dispatcher.register_stream(GatewayRpcMethod.CHAT_STREAM, _handle_chat_stream) rpc_dispatcher.register(GatewayRpcMethod.CHAT_HISTORY, _handle_chat_history) rpc_dispatcher.register(GatewayRpcMethod.CHAT_CANCEL, _handle_chat_cancel) rpc_dispatcher.register(GatewayRpcMethod.SESSIONS_LIST, _handle_sessions_list) rpc_dispatcher.register(GatewayRpcMethod.SESSIONS_HISTORY, _handle_sessions_history) rpc_dispatcher.register(GatewayRpcMethod.PLUGINS_LIST, _handle_plugins_list) rpc_dispatcher.register(GatewayRpcMethod.PLUGINS_INSTALL, _handle_plugins_install) rpc_dispatcher.register(GatewayRpcMethod.PAIRING_LIST, _handle_pairing_list) rpc_dispatcher.register(GatewayRpcMethod.PAIRING_APPROVE, _handle_pairing_approve) rpc_dispatcher.register(GatewayRpcMethod.PAIRING_REJECT, _handle_pairing_reject) rpc_dispatcher.register(GatewayRpcMethod.ALLOWLIST_GET, _handle_allowlist_get) rpc_dispatcher.register(GatewayRpcMethod.ALLOWLIST_ADD, _handle_allowlist_add) rpc_dispatcher.register(GatewayRpcMethod.ALLOWLIST_REMOVE, _handle_allowlist_remove) rpc_dispatcher.register(GatewayRpcMethod.CRON_LIST, _handle_cron_list) rpc_dispatcher.register(GatewayRpcMethod.CRON_CREATE, _handle_cron_create) rpc_dispatcher.register(GatewayRpcMethod.CRON_UPDATE, _handle_cron_update) rpc_dispatcher.register(GatewayRpcMethod.CRON_DELETE, _handle_cron_delete) rpc_dispatcher.register(GatewayRpcMethod.CRON_FORCE_RUN, _handle_cron_force_run) rpc_dispatcher.register(GatewayRpcMethod.CRON_PAUSE, _handle_cron_pause) rpc_dispatcher.register(GatewayRpcMethod.CRON_RESUME, _handle_cron_resume) rpc_dispatcher.register(GatewayRpcMethod.CRON_DIAGNOSTICS, _handle_cron_diagnostics) rpc_dispatcher.register(GatewayRpcMethod.CRON_RUN_LOG, _handle_cron_run_log) rpc_dispatcher.register(GatewayRpcMethod.AGENT_TOOLS_LIST, _handle_agent_tools_list) rpc_dispatcher.register(GatewayRpcMethod.AGENT_TOOLS_EXECUTE, _handle_agent_tools_execute) rpc_dispatcher.register(GatewayRpcMethod.SESSIONS_CREATE, _handle_sessions_create) rpc_dispatcher.register(GatewayRpcMethod.SESSIONS_DELETE, _handle_sessions_delete) rpc_dispatcher.register(GatewayRpcMethod.CONFIG_GET, _handle_config_get) rpc_dispatcher.register(GatewayRpcMethod.CONFIG_SET, _handle_config_set) rpc_dispatcher.register(GatewayRpcMethod.MESSAGE_SEND, _handle_message_send) rpc_dispatcher.register(GatewayRpcMethod.MESSAGE_ACTION, _handle_message_action) rpc_dispatcher.register(GatewayRpcMethod.IDENTITY_LINKS_LIST, _handle_identity_links_list) rpc_dispatcher.register(GatewayRpcMethod.IDENTITY_LINKS_ADD, _handle_identity_links_add) rpc_dispatcher.register(GatewayRpcMethod.IDENTITY_LINKS_REMOVE, _handle_identity_links_remove) rpc_dispatcher.add_alias("health", GatewayRpcMethod.SYSTEM_HEALTH) rpc_dispatcher.add_alias("config", GatewayRpcMethod.SYSTEM_CONFIG) rpc_dispatcher.add_alias("version", GatewayRpcMethod.SYSTEM_VERSION) rpc_dispatcher.add_alias("chat.message", GatewayRpcMethod.CHAT_SEND) rpc_dispatcher.add_alias("channels", GatewayRpcMethod.CHANNELS_LIST) rpc_dispatcher.add_alias("channels.start", GatewayRpcMethod.CHANNELS_START) rpc_dispatcher.add_alias("channels.stop", GatewayRpcMethod.CHANNELS_STOP) rpc_dispatcher.add_alias("channels.restart", GatewayRpcMethod.CHANNELS_RESTART) rpc_dispatcher.add_alias("sessions", GatewayRpcMethod.SESSIONS_LIST) logger.info("Gateway RPC handlers registered: %s", rpc_dispatcher.list_methods())