diff --git a/app/__init__.py b/app/__init__.py index 329ad0d..58ae026 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -39,7 +39,7 @@ def create_app(config_name='default'): def index(): return { 'service': '智队中枢', - 'version': '0.9.2', + 'version': '0.9.4', 'status': 'running', 'endpoints': { 'health': '/health', @@ -93,7 +93,8 @@ def _register_blueprints(app): from app.routes.gateways import gateways_bp from app.routes.messages import messages_bp from app.routes.stats import stats_bp - from app.routes.bots import bots_bp # Step 3: Bot API + from app.routes.bots import bots_bp + from app.routes.chat import chat_bp # Step 5: Chat API from app.web.routes import web_bp from app.web.api import web_api_bp @@ -103,7 +104,8 @@ def _register_blueprints(app): app.register_blueprint(gateways_bp, url_prefix='/api/gateways') app.register_blueprint(messages_bp, url_prefix='/api/messages') app.register_blueprint(stats_bp, url_prefix='/api/stats') - app.register_blueprint(bots_bp, url_prefix='/api/bots') # Step 3: Bot API + app.register_blueprint(bots_bp, url_prefix='/api/bots') + app.register_blueprint(chat_bp, url_prefix='/api/chat') # Step 5: Chat API app.register_blueprint(web_bp, url_prefix='/web') app.register_blueprint(web_api_bp) diff --git a/app/routes/chat.py b/app/routes/chat.py new file mode 100644 index 0000000..1c162a0 --- /dev/null +++ b/app/routes/chat.py @@ -0,0 +1,382 @@ +""" +聊天路由 - 聊天会话管理 API +提供 REST API 用于管理聊天会话(补充 WebSocket 功能) +""" +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from datetime import datetime + +from app.models import User, Session, Message, Bot, Agent +from app.extensions import db +from app.services.bot_service import BotService + +chat_bp = Blueprint('chat', __name__) + + +@chat_bp.route('/sessions', methods=['GET']) +@jwt_required() +def get_chat_sessions(): + """ + 获取聊天会话列表 + + Query params: + - status: str - 会话状态过滤(active/paused/closed) + - bot_id: str - 按 Bot 过滤 + - limit: int - 返回数量限制(默认 20) + - offset: int - 分页偏移(默认 0) + """ + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + # 获取查询参数 + status = request.args.get('status') + bot_id = request.args.get('bot_id') + limit = min(int(request.args.get('limit', 20)), 100) # 最大 100 + offset = int(request.args.get('offset', 0)) + + # 构建查询 + query = Session.query.filter_by(user_id=user_id) + + # 只查询聊天会话(有 bot_id 的) + query = query.filter(Session.bot_id.isnot(None)) + + if status: + query = query.filter_by(status=status) + + if bot_id: + query = query.filter_by(bot_id=bot_id) + + # 按最后活跃时间倒序 + query = query.order_by(Session.last_active_at.desc()) + + # 分页 + total = query.count() + sessions = query.offset(offset).limit(limit).all() + + # 构建返回数据 + result = [] + for session in sessions: + session_data = session.to_dict() + + # 添加 Bot 信息 + if session.bot_id: + bot = BotService.get_bot_by_id(session.bot_id) + if bot: + session_data['bot'] = { + 'id': bot.id, + 'name': bot.name, + 'display_name': bot.display_name, + 'avatar': bot.avatar, + 'status': bot.status + } + + # 添加最后一条消息预览 + last_message = Message.query.filter_by(session_id=session.id)\ + .order_by(Message.created_at.desc())\ + .first() + if last_message: + session_data['last_message'] = { + 'id': last_message.id, + 'content': last_message.content[:100] if last_message.content else '', + 'sender_type': last_message.sender_type, + 'created_at': last_message.created_at.isoformat() + } + + result.append(session_data) + + return jsonify({ + 'sessions': result, + 'total': total, + 'limit': limit, + 'offset': offset + }), 200 + + +@chat_bp.route('/sessions', methods=['POST']) +@jwt_required() +def create_chat_session(): + """ + 创建聊天会话(HTTP 方式,WebSocket 的替代方案) + + Request body: + - bot_id: str - 机器人 ID(必填) + - title: str - 会话标题(可选) + """ + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + data = request.get_json() + + if not data or 'bot_id' not in data: + return jsonify({'error': 'bot_id is required'}), 400 + + bot_id = data['bot_id'] + bot = BotService.get_bot_by_id(bot_id) + + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'use'): + return jsonify({'error': 'Permission denied'}), 403 + + # 检查 Agent + if not bot.agent_id: + return jsonify({'error': 'Bot has no agent bound'}), 400 + + agent = Agent.query.get(bot.agent_id) + if not agent: + return jsonify({'error': 'Agent not found'}), 404 + + # 创建会话 + title = data.get('title', f'Chat with {bot.display_name or bot.name}') + session = Session( + user_id=user_id, + bot_id=bot.id, + primary_agent_id=agent.id, + title=title, + channel_type='pit-bot', + status='active', + created_at=datetime.utcnow(), + last_active_at=datetime.utcnow() + ) + db.session.add(session) + db.session.commit() + + return jsonify({ + 'session': { + **session.to_dict(), + 'bot': bot.to_dict() + }, + 'message': 'Chat session created' + }), 201 + + +@chat_bp.route('/sessions/', methods=['GET']) +@jwt_required() +def get_chat_session(session_id): + """获取聊天会话详情""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + session = Session.query.get(session_id) + if not session: + return jsonify({'error': 'Session not found'}), 404 + + # 权限检查 + if session.user_id != user_id: + return jsonify({'error': 'Permission denied'}), 403 + + # 验证是聊天会话 + if not session.bot_id: + return jsonify({'error': 'Not a chat session'}), 400 + + session_data = session.to_dict() + + # 添加 Bot 信息 + bot = BotService.get_bot_by_id(session.bot_id) + if bot: + session_data['bot'] = bot.to_dict() + + return jsonify({'session': session_data}), 200 + + +@chat_bp.route('/sessions//messages', methods=['GET']) +@jwt_required() +def get_chat_messages(session_id): + """ + 获取聊天消息历史 + + Query params: + - limit: int - 返回数量限制(默认 50,最大 100) + - before: str - 获取此 ID 之前的消息(分页) + """ + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + session = Session.query.get(session_id) + if not session: + return jsonify({'error': 'Session not found'}), 404 + + # 权限检查 + if session.user_id != user_id: + return jsonify({'error': 'Permission denied'}), 403 + + # 获取查询参数 + limit = min(int(request.args.get('limit', 50)), 100) + before_id = request.args.get('before') + + # 构建查询 + query = Message.query.filter_by(session_id=session_id) + + if before_id: + before_msg = Message.query.get(before_id) + if before_msg: + query = query.filter(Message.created_at < before_msg.created_at) + + # 按时间倒序,取最近的消息 + messages = query.order_by(Message.created_at.desc()).limit(limit).all() + + # 翻转顺序(从旧到新) + messages = list(reversed(messages)) + + return jsonify({ + 'messages': [m.to_dict() for m in messages], + 'count': len(messages), + 'has_more': len(messages) == limit + }), 200 + + +@chat_bp.route('/sessions//messages', methods=['POST']) +@jwt_required() +def send_chat_message(session_id): + """ + 发送消息(HTTP 方式,WebSocket 的替代方案) + + Request body: + - content: str - 消息内容(必填) + - reply_to: str - 回复的消息 ID(可选) + - content_type: str - 内容类型(默认 markdown) + """ + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + session = Session.query.get(session_id) + if not session: + return jsonify({'error': 'Session not found'}), 404 + + # 权限检查 + if session.user_id != user_id: + return jsonify({'error': 'Permission denied'}), 403 + + # 检查会话状态 + if session.status == 'closed': + return jsonify({'error': 'Session is closed'}), 400 + + data = request.get_json() + + if not data or 'content' not in data: + return jsonify({'error': 'content is required'}), 400 + + content = data['content'].strip() + if not content: + return jsonify({'error': 'content cannot be empty'}), 400 + + # 获取 Bot 信息用于 sender_name + bot = None + sender_name = user.nickname or user.username + if session.bot_id: + bot = BotService.get_bot_by_id(session.bot_id) + # 这里不需要修改 sender_name,因为这是用户发送的消息 + + # 创建消息 + message = Message( + session_id=session_id, + sender_type='user', + sender_id=user_id, + sender_name=sender_name, + bot_id=session.bot_id, + message_type='text', + content=content, + content_type=data.get('content_type', 'markdown'), + reply_to=data.get('reply_to'), + status='sent', + ack_status='pending', + created_at=datetime.utcnow() + ) + db.session.add(message) + + # 更新会话 + session.message_count += 1 + session.last_active_at = datetime.utcnow() + session.updated_at = datetime.utcnow() + + db.session.commit() + + return jsonify({ + 'message': message.to_dict(), + 'session_id': session_id + }), 201 + + +@chat_bp.route('/sessions//read', methods=['PUT']) +@jwt_required() +def mark_session_read(session_id): + """标记会话消息已读""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + session = Session.query.get(session_id) + if not session: + return jsonify({'error': 'Session not found'}), 404 + + # 权限检查 + if session.user_id != user_id: + return jsonify({'error': 'Permission denied'}), 403 + + # 将所有未读消息标记为已读 + messages = Message.query.filter_by( + session_id=session_id, + status='delivered' + ).all() + + for msg in messages: + msg.status = 'read' + + # 重置会话未读数 + session.unread_count = 0 + + db.session.commit() + + return jsonify({ + 'session_id': session_id, + 'marked_read': len(messages) + }), 200 + + +@chat_bp.route('/sessions/', methods=['DELETE']) +@jwt_required() +def close_chat_session(session_id): + """关闭聊天会话""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + session = Session.query.get(session_id) + if not session: + return jsonify({'error': 'Session not found'}), 404 + + # 权限检查 + if session.user_id != user_id: + return jsonify({'error': 'Permission denied'}), 403 + + # 关闭会话 + session.status = 'closed' + session.updated_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'session_id': session_id, + 'status': 'closed', + 'message': 'Chat session closed' + }), 200