新增设备身份管理、认证限流、并发通道、Webhook路由、RBAC权限控制、SSE/轮询降级等全套网关通道功能,包含: 1. 设备身份生成与签名验证 2. 设备令牌认证与速率限制 3. 内存+数据库双重设备注册表 4. 并发通道限流管理 5. Webhook安全处理与路由 6. RBAC权限校验系统 7. OpenAI API兼容适配层 8. Tailscale认证支持 9. HTTP轮询降级机制
1445 lines
52 KiB
Python
1445 lines
52 KiB
Python
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())
|