Files
pit-router/app/routes/chat.py
feifei.xu 04132c298a feat: Step 5 - 聊天 API (v0.9.4)
- 创建 chat.py 路由文件
- 实现 7 个 REST API 端点:
  - GET /api/chat/sessions - 获取聊天会话列表
  - POST /api/chat/sessions - 创建聊天会话
  - GET /api/chat/sessions/:id - 获取会话详情
  - GET /api/chat/sessions/:id/messages - 获取消息历史
  - POST /api/chat/sessions/:id/messages - 发送消息
  - PUT /api/chat/sessions/:id/read - 标记已读
  - DELETE /api/chat/sessions/:id - 关闭会话
- 支持分页、过滤、Bot 信息关联
- 注册蓝图到应用
- 更新版本号到 0.9.4
2026-03-15 10:43:10 +08:00

383 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
聊天路由 - 聊天会话管理 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/<session_id>', 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/<session_id>/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/<session_id>/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/<session_id>/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/<session_id>', 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