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
This commit is contained in:
@@ -39,7 +39,7 @@ def create_app(config_name='default'):
|
|||||||
def index():
|
def index():
|
||||||
return {
|
return {
|
||||||
'service': '智队中枢',
|
'service': '智队中枢',
|
||||||
'version': '0.9.2',
|
'version': '0.9.4',
|
||||||
'status': 'running',
|
'status': 'running',
|
||||||
'endpoints': {
|
'endpoints': {
|
||||||
'health': '/health',
|
'health': '/health',
|
||||||
@@ -93,7 +93,8 @@ def _register_blueprints(app):
|
|||||||
from app.routes.gateways import gateways_bp
|
from app.routes.gateways import gateways_bp
|
||||||
from app.routes.messages import messages_bp
|
from app.routes.messages import messages_bp
|
||||||
from app.routes.stats import stats_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.routes import web_bp
|
||||||
from app.web.api import web_api_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(gateways_bp, url_prefix='/api/gateways')
|
||||||
app.register_blueprint(messages_bp, url_prefix='/api/messages')
|
app.register_blueprint(messages_bp, url_prefix='/api/messages')
|
||||||
app.register_blueprint(stats_bp, url_prefix='/api/stats')
|
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_bp, url_prefix='/web')
|
||||||
app.register_blueprint(web_api_bp)
|
app.register_blueprint(web_api_bp)
|
||||||
|
|
||||||
|
|||||||
382
app/routes/chat.py
Normal file
382
app/routes/chat.py
Normal file
@@ -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/<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
|
||||||
Reference in New Issue
Block a user