""" 艾叶 IM Bridge — 个人网页通道 对接方式: 1. REST API — POST /api/web/chat 2. WebSocket — ws://host:port/ws/web/chat 提供即开即用的网页聊天入口,也可作为第三方系统接入的通用 API。 配置项: allowed_origins: ["*"] # CORS 白名单 """ from __future__ import annotations import json import logging import uuid from typing import Any, Dict, Set from fastapi import Request, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse from core.channel_base import ChannelBase, InboundMessage, MessageType, OutboundMessage logger = logging.getLogger("aiye.channel.web") class WebChannel(ChannelBase): """个人网页通道(REST + WebSocket)""" _ws_connections: Dict[str, WebSocket] = {} @property def platform(self) -> str: return "web" async def start(self) -> None: logger.info("网页通道已就绪,API: /api/web/chat | WS: /ws/web/chat") async def stop(self) -> None: for ws in self._ws_connections.values(): try: await ws.close() except Exception: pass self._ws_connections.clear() logger.info("网页通道已停止") async def send(self, msg: OutboundMessage) -> bool: ws = self._ws_connections.get(msg.chat_id) if ws: try: await ws.send_json({"type": "reply", "content": msg.content}) return True except Exception as e: logger.warning("WebSocket 发送失败: %s", e) return True def register_routes(self, app: Any) -> None: channel = self @app.post("/api/web/chat") async def web_chat_api(request: Request): """REST 聊天接口:{"message": "你好", "session_id": "可选"}""" try: data = await request.json() except Exception: return {"code": 400, "msg": "invalid json"} message = data.get("message", "").strip() session_id = data.get("session_id", "") or str(uuid.uuid4())[:8] if not message: return {"code": 400, "msg": "empty message"} inbound = InboundMessage( channel_id=channel.channel_id, platform=channel.platform, sender_id=session_id, sender_name=data.get("name", "网页用户"), chat_id=session_id, content=message, ) reply = await channel.dispatch(inbound) return { "code": 0, "reply": reply.content if reply else "", "session_id": session_id, } @app.websocket("/ws/web/chat") async def web_chat_ws(websocket: WebSocket): """WebSocket 聊天接口""" await websocket.accept() ws_id = str(uuid.uuid4())[:8] channel._ws_connections[ws_id] = websocket await websocket.send_json({"type": "connected", "session_id": ws_id}) logger.info("WebSocket 连接: %s", ws_id) try: while True: raw = await websocket.receive_text() try: data = json.loads(raw) message = data.get("message", "").strip() except Exception: message = raw.strip() if not message: continue inbound = InboundMessage( channel_id=channel.channel_id, platform=channel.platform, sender_id=ws_id, sender_name="网页用户", chat_id=ws_id, content=message, ) reply = await channel.dispatch(inbound) if reply: await websocket.send_json({ "type": "reply", "content": reply.content, }) except WebSocketDisconnect: logger.info("WebSocket 断开: %s", ws_id) except Exception as e: logger.warning("WebSocket 异常: %s", e) finally: channel._ws_connections.pop(ws_id, None) @app.get("/chat", response_class=HTMLResponse) async def web_chat_page(): """内嵌网页聊天界面""" html_path = channel._config.get("html_path", "") if html_path: try: from pathlib import Path return Path(html_path).read_text(encoding="utf-8") except Exception: pass return _DEFAULT_CHAT_HTML _DEFAULT_CHAT_HTML = """