Files
rag/api/task_routes.py
lacerate551 183a57e7f1 refactor(api): 统一响应格式迁移 + 异步任务系统 + 状态码体系完善
- 将全部路由文件(12个)的 jsonify 响应迁移至 success_response/error_response 统一格式
- 修复 sync_routes.py error_response 参数错误(P0)
- 新增异步任务系统:task_registry + task_routes
- 新增状态码:TASK_NOT_FOUND(4014)、TASK_CONFLICT(4015)、REINDEX_ERROR(5040)
- 修正 task_routes/exam_pkg 中语义不匹配的状态码
- 更新 curl 测试手册、后端对接规范文档
- 添加缓存性能报告和 Redis 迁移计划
2026-06-05 22:56:00 +08:00

192 lines
5.3 KiB
Python
Raw Permalink 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
提供任务状态的 JSON 轮询和 SSE 流式两种查询方式。
路由列表:
GET /tasks : 任务列表
GET /tasks/<id> : 任务状态JSON
GET /tasks/<id>/progress: 任务进度SSE 流式)
GET /tasks/stats : 任务统计
后端组调用方式:
1. POST /sync → 返回 {"task_id": "xxx", ...}
2. GET /tasks/xxx → 轮询状态,直到 status 为 completed 或 failed
dev-ui 调用方式:
1. POST /sync → 返回 task_id
2. GET /tasks/xxx/progress → SSE 流式接收进度事件
"""
import json
import time
import logging
from typing import Tuple, Any
from flask import Blueprint, request, Response, stream_with_context
from auth.gateway import require_gateway_auth
from api.response_utils import success_response, error_response
from core.status_codes import SUCCESS, NOT_FOUND
logger = logging.getLogger(__name__)
task_bp = Blueprint('tasks', __name__)
def _get_registry():
"""获取任务注册表"""
from core.task_registry import get_registry
return get_registry()
# ==================== 任务查询 API ====================
@task_bp.route('/tasks', methods=['GET'])
@require_gateway_auth
def list_tasks() -> Tuple[Any, int]:
"""
获取任务列表
查询参数:
status: 过滤状态running / completed / failed / pending
type: 过滤类型sync / reindex / upload / batch_upload / exam_generate / exam_grade
limit: 返回数量限制(默认 50
Returns:
{"success": true, "data": {"tasks": [...], "total": N}}
"""
registry = _get_registry()
registry.maybe_cleanup()
status = request.args.get('status')
task_type = request.args.get('type')
limit = request.args.get('limit', 50, type=int)
tasks = registry.list_tasks(status=status, task_type=task_type, limit=limit)
return success_response(
data={
'tasks': [t.to_dict() for t in tasks],
'total': len(tasks)
},
status_code=SUCCESS,
message="查询成功"
)
@task_bp.route('/tasks/stats', methods=['GET'])
@require_gateway_auth
def task_stats() -> Tuple[Any, int]:
"""
获取任务统计信息
Returns:
{"success": true, "data": {"total": N, "by_status": {...}, "by_type": {...}}}
"""
registry = _get_registry()
return success_response(
data=registry.get_stats(),
status_code=SUCCESS,
message="查询成功"
)
@task_bp.route('/tasks/<task_id>', methods=['GET'])
@require_gateway_auth
def get_task(task_id: str) -> Tuple[Any, int]:
"""
获取单个任务状态JSON 轮询接口)
后端组推荐使用此接口轮询任务进度。
轮询建议:
- 间隔 1-2 秒
- 当 status 为 completed 或 failed 时停止轮询
Returns:
成功: {"success": true, "data": {task详情}}
未找到: {"error": "任务不存在"}
"""
registry = _get_registry()
task = registry.get_task(task_id)
if not task:
return error_response(
"TASK_NOT_FOUND", NOT_FOUND,
f"任务不存在: {task_id}",
http_status=404
)
return success_response(
data=task.to_dict(),
status_code=SUCCESS,
message="查询成功"
)
@task_bp.route('/tasks/<task_id>/progress', methods=['GET'])
@require_gateway_auth
def task_progress_stream(task_id: str):
"""
SSE 流式任务进度推送
dev-ui 前端推荐使用此接口,实时接收任务进度事件。
SSE 事件类型:
- start: 任务开始
- progress: 进度更新(含 progress/current/total/stage/message
- complete: 任务完成(含完整结果)
- error: 任务失败(含错误信息)
- heartbeat: 每 15 秒发送一次保活
Returns:
text/event-stream
"""
registry = _get_registry()
task = registry.get_task(task_id)
if not task:
return error_response(
"TASK_NOT_FOUND", NOT_FOUND,
f"任务不存在: {task_id}",
http_status=404
)
def generate_sse():
"""SSE 生成器"""
sent_complete = False
while True:
# 取出缓冲区事件
events = task.drain_events()
for event in events:
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
if event.get('type') in ('complete', 'error'):
sent_complete = True
# 如果任务已结束且事件已发送完毕,退出
if sent_complete:
break
# 如果任务已完成但没有事件,发送最终状态后退出
if task.status in ('completed', 'failed') and not events:
final_event = {
'type': 'complete' if task.status == 'completed' else 'error',
'data': task.to_dict()
}
yield f"data: {json.dumps(final_event, ensure_ascii=False)}\n\n"
break
# 心跳保活
yield f": heartbeat\n\n"
time.sleep(1)
return Response(
stream_with_context(generate_sse()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no',
'Connection': 'keep-alive'
}
)