ha'ha
This commit is contained in:
14
API文档.md
14
API文档.md
@@ -483,30 +483,30 @@ Set-Cookie: auth_token=a1b2c3d4e5f6...; HttpOnly; Max-Age=31536000; SameSite=Lax
|
||||
|
||||
---
|
||||
|
||||
### GET /api/tasks/{task_id}
|
||||
### GET /api/tasks/{account_id}
|
||||
|
||||
**说明**: 查询指定任务的状态和结果。
|
||||
**说明**: 按账号 ID 查询任务列表(不支持按 task_id 查询)。
|
||||
|
||||
**状态码**:
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 成功,返回单个任务对象 |
|
||||
| 200 | 成功,返回任务数组 |
|
||||
| 401 | 未登录或 token 失效 |
|
||||
| 404 | 任务不存在 |
|
||||
| 404 | 账号不存在 |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| task_id | string | 是 | 任务 ID |
|
||||
| account_id | int | 是 | 账号 ID(即 `/api/accounts` 返回的 `id`) |
|
||||
|
||||
**成功响应** (200): 同上单个任务对象
|
||||
**成功响应** (200): 同上任务对象数组
|
||||
|
||||
**失败响应** (404):
|
||||
```json
|
||||
{
|
||||
"detail": "任务 xxx 不存在"
|
||||
"detail": "账号 xxx 不存在"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from rest_framework import status as http_status
|
||||
@@ -13,8 +14,8 @@ from rest_framework.decorators import api_view
|
||||
|
||||
from common.protocol import TaskStatus, TaskType
|
||||
from server.core.response import api_success, api_error
|
||||
from server.models import BossAccount, TaskCreate
|
||||
from server.serializers import TaskCreateSerializer, TaskOutSerializer
|
||||
from server.models import BossAccount, TaskCreate, TaskLog
|
||||
from server.serializers import TaskCreateSerializer
|
||||
from server.core.worker_manager import worker_manager
|
||||
from server.core.task_dispatcher import task_dispatcher
|
||||
|
||||
@@ -44,6 +45,115 @@ def _task_to_dict(t) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _parse_limit(raw_value, default: int = 50, max_value: int = 200) -> int:
|
||||
"""解析分页上限,避免非法参数导致 500。"""
|
||||
try:
|
||||
value = int(raw_value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
if value <= 0:
|
||||
return default
|
||||
return min(value, max_value)
|
||||
|
||||
|
||||
def _parse_task_status(raw_status: Optional[str]) -> Optional[TaskStatus]:
|
||||
"""解析任务状态。为空时返回 None。"""
|
||||
if not raw_status:
|
||||
return None
|
||||
try:
|
||||
return TaskStatus(raw_status)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _task_log_account_name(task_log: TaskLog) -> Optional[str]:
|
||||
"""从任务日志里提取可识别的环境名。"""
|
||||
result = task_log.result if isinstance(task_log.result, dict) else {}
|
||||
params = task_log.params if isinstance(task_log.params, dict) else {}
|
||||
account_name = (
|
||||
result.get("browser_name")
|
||||
or result.get("account_name")
|
||||
or params.get("account_name")
|
||||
or params.get("browser_name")
|
||||
)
|
||||
if not account_name:
|
||||
return None
|
||||
return str(account_name).strip() or None
|
||||
|
||||
|
||||
def _task_log_to_dict(task_log: TaskLog, account_name: Optional[str] = None) -> dict:
|
||||
"""将 TaskLog 转为统一响应结构。"""
|
||||
return {
|
||||
"task_id": task_log.task_id,
|
||||
"task_type": task_log.task_type,
|
||||
"status": task_log.status,
|
||||
"worker_id": task_log.worker_id,
|
||||
"account_name": account_name or _task_log_account_name(task_log),
|
||||
"params": task_log.params if isinstance(task_log.params, dict) else {},
|
||||
"progress": None,
|
||||
"result": task_log.result,
|
||||
"error": task_log.error,
|
||||
"created_at": task_log.created_at.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"updated_at": task_log.created_at.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
}
|
||||
|
||||
|
||||
def _is_task_log_for_account(task_log: TaskLog, account: BossAccount) -> bool:
|
||||
"""判断任务日志是否属于某个账号(用于账号任务列表兼容)。"""
|
||||
if task_log.worker_id and task_log.worker_id != account.worker_id:
|
||||
return False
|
||||
|
||||
if account.current_task_id and task_log.task_id == account.current_task_id:
|
||||
return True
|
||||
|
||||
matched_name = _task_log_account_name(task_log)
|
||||
if matched_name and matched_name == account.browser_name:
|
||||
return True
|
||||
|
||||
params = task_log.params if isinstance(task_log.params, dict) else {}
|
||||
account_pk = str(account.pk)
|
||||
for key in ("id", "account_id", "boss_id"):
|
||||
value = params.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
if str(value).strip() == account_pk:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _list_tasks_by_account(account: BossAccount, task_status: Optional[TaskStatus], limit: int) -> list:
|
||||
"""
|
||||
聚合某账号的任务列表:
|
||||
1) 内存任务(实时)
|
||||
2) TaskLog 历史任务(重启后可查)
|
||||
"""
|
||||
items_by_task_id = {}
|
||||
|
||||
memory_limit = max(limit * 3, 100)
|
||||
memory_tasks = task_dispatcher.list_tasks(worker_id=account.worker_id, status=task_status, limit=memory_limit)
|
||||
for t in memory_tasks:
|
||||
if t.account_name != account.browser_name:
|
||||
continue
|
||||
items_by_task_id[t.task_id] = _task_to_dict(t)
|
||||
|
||||
db_qs = TaskLog.objects.filter(worker_id=account.worker_id).order_by("-created_at")
|
||||
if task_status:
|
||||
db_qs = db_qs.filter(status=task_status.value)
|
||||
|
||||
# 多取一些做过滤,避免因为条件匹配损耗导致结果太少
|
||||
for task_log in db_qs[: max(limit * 8, 200)]:
|
||||
if not _is_task_log_for_account(task_log, account):
|
||||
continue
|
||||
if task_log.task_id in items_by_task_id:
|
||||
continue
|
||||
items_by_task_id[task_log.task_id] = _task_log_to_dict(task_log, account_name=account.browser_name)
|
||||
|
||||
merged = list(items_by_task_id.values())
|
||||
merged.sort(key=lambda item: item.get("created_at") or "", reverse=True)
|
||||
return merged[:limit]
|
||||
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
def task_list(request):
|
||||
"""
|
||||
@@ -58,30 +168,24 @@ def task_list(request):
|
||||
wid = request.query_params.get("worker_id")
|
||||
account_id = request.query_params.get("account_id")
|
||||
st = request.query_params.get("status")
|
||||
limit = int(request.query_params.get("limit", 50))
|
||||
task_status = TaskStatus(st) if st else None
|
||||
|
||||
# 如果指定了 account_id,先查询账号信息,转换为 account_name 过滤
|
||||
account_name = None
|
||||
limit = _parse_limit(request.query_params.get("limit", 50))
|
||||
task_status = _parse_task_status(st)
|
||||
if st and task_status is None:
|
||||
return api_error(http_status.HTTP_400_BAD_REQUEST, f"不支持的任务状态: {st}")
|
||||
|
||||
# 如果指定了 account_id,直接返回该账号的任务列表(兼容前端“查看任务”)
|
||||
if account_id:
|
||||
try:
|
||||
account_id = int(account_id)
|
||||
account = BossAccount.objects.get(pk=account_id)
|
||||
account_name = account.browser_name
|
||||
if not wid:
|
||||
wid = account.worker_id
|
||||
except (ValueError, BossAccount.DoesNotExist):
|
||||
return api_error(
|
||||
http_status.HTTP_404_NOT_FOUND,
|
||||
f"未找到 id={account_id} 的账号",
|
||||
)
|
||||
|
||||
return api_success(_list_tasks_by_account(account, task_status=task_status, limit=limit))
|
||||
|
||||
tasks = task_dispatcher.list_tasks(worker_id=wid, status=task_status, limit=limit)
|
||||
|
||||
# 按 account_name 进一步过滤(如果指定了 account_id)
|
||||
if account_name:
|
||||
tasks = [t for t in tasks if t.account_name == account_name]
|
||||
|
||||
return api_success([_task_to_dict(t) for t in tasks])
|
||||
|
||||
# POST: 提交新任务
|
||||
@@ -179,31 +283,16 @@ def task_list(request):
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def task_detail(request, task_id):
|
||||
"""查询指定任务的状态和结果。"""
|
||||
# 先查内存
|
||||
task = task_dispatcher.get_task(task_id)
|
||||
if task:
|
||||
return api_success(_task_to_dict(task))
|
||||
|
||||
# 再查数据库(任务已完成的情况)
|
||||
try:
|
||||
from server.models import TaskLog
|
||||
task_log = TaskLog.objects.get(task_id=task_id)
|
||||
# 将数据库记录转换为相同格式
|
||||
task_dict = {
|
||||
"task_id": task_log.task_id,
|
||||
"task_type": task_log.task_type,
|
||||
"status": task_log.status,
|
||||
"worker_id": task_log.worker_id,
|
||||
"account_name": None,
|
||||
"params": task_log.params,
|
||||
"progress": None,
|
||||
"result": task_log.result,
|
||||
"error": task_log.error,
|
||||
"created_at": task_log.created_at.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"updated_at": task_log.created_at.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
}
|
||||
return api_success(task_dict)
|
||||
except Exception:
|
||||
return api_error(http_status.HTTP_404_NOT_FOUND, f"任务 {task_id} 不存在")
|
||||
def task_list_by_account(request, account_id: int):
|
||||
"""按账号 ID 查询任务列表(不支持按 task_id 查询)。"""
|
||||
account = BossAccount.objects.filter(pk=account_id).first()
|
||||
if not account:
|
||||
return api_error(http_status.HTTP_404_NOT_FOUND, f"账号 {account_id} 不存在")
|
||||
|
||||
st = request.query_params.get("status")
|
||||
task_status = _parse_task_status(st)
|
||||
if st and task_status is None:
|
||||
return api_error(http_status.HTTP_400_BAD_REQUEST, f"不支持的任务状态: {st}")
|
||||
|
||||
limit = _parse_limit(request.query_params.get("limit", 50))
|
||||
return api_success(_list_tasks_by_account(account, task_status=task_status, limit=limit))
|
||||
|
||||
@@ -22,7 +22,7 @@ urlpatterns = [
|
||||
|
||||
# ─── 任务 ───
|
||||
path("api/tasks", tasks.task_list),
|
||||
path("api/tasks/<str:task_id>", tasks.task_detail),
|
||||
path("api/tasks/<int:account_id>", tasks.task_list_by_account),
|
||||
|
||||
# ─── 账号 ───
|
||||
path("api/accounts", accounts.account_list),
|
||||
|
||||
Reference in New Issue
Block a user