""" 爬虫任务 Admin """ import json import logging from pathlib import Path from typing import Any, ClassVar from django.contrib import admin from django.http import HttpRequest from django.utils.html import format_html, format_html_join from django.utils.safestring import SafeData from django.utils.translation import gettext_lazy as _ from apps.automation.models import ScraperTask logger = logging.getLogger(__name__) @admin.register(ScraperTask) class ScraperTaskAdmin(admin.ModelAdmin[ScraperTask]): """爬虫任务管理""" def get_model_perms(self, request: HttpRequest) -> dict[str, bool]: """隐藏后台入口(保留代码与直达地址能力)""" return {} list_display = ( "id", "task_type", "priority", "status_colored", "retry_info", "url_short", "case", "created_at", "duration", ) list_filter = ("task_type", "status", "priority", "created_at") search_fields = ("url", "error_message") readonly_fields = ( "created_at", "updated_at", "started_at", "finished_at", "result_display", "error_message", "retry_count", ) fieldsets = ( (_("基本信息"), {"fields": ("task_type", "status", "priority", "url", "case")}), (_("重试配置"), {"fields": ("retry_count", "max_retries", "scheduled_at")}), (_("配置"), {"fields": ("config",), "classes": ("collapse",)}), (_("执行结果"), {"fields": ("result_display", "error_message")}), (_("时间信息"), {"fields": ("created_at", "started_at", "finished_at", "updated_at")}), ) @admin.display(description="状态") def status_colored(self, obj: Any) -> SafeData: """带颜色的状态显示""" colors = { "pending": "#ffa500", "running": "#007bff", "success": "#28a745", "failed": "#dc3545", } color = colors.get(obj.status, "#666") return format_html( '{}', color, obj.get_status_display(), ) @admin.display(description="URL") def url_short(self, obj: Any) -> str: """缩短的 URL 显示""" url: str = str(obj.url) if len(url) > 50: return url[:50] + "..." return url @admin.display(description="重试") def retry_info(self, obj: Any) -> SafeData | str: """显示重试信息""" if obj.retry_count > 0: return format_html( '{}/{}', obj.retry_count, obj.max_retries, ) return f"0/{obj.max_retries}" @admin.display(description="耗时") def duration(self, obj: Any) -> str: """计算任务耗时""" if obj.started_at and obj.finished_at: delta = obj.finished_at - obj.started_at seconds = delta.total_seconds() if seconds < 60: return f"{seconds:.1f}秒" else: minutes = seconds / 60 return f"{minutes:.1f}分钟" return "-" def _file_icon(self, filename: str) -> str: """根据文件扩展名返回图标""" if filename.endswith(".pdf"): return "📄" if filename.endswith(".zip"): return "📦" if filename.endswith((".doc", ".docx")): return "📝" return "📎" def _render_files_html(self, files: list[str]) -> SafeData: """渲染文件列表 HTML,返回安全的 format_html 结果""" from django.conf import settings def _file_item(f: str) -> SafeData: filename = f.split("/")[-1] if "/" in f else f try: file_path = Path(f) media_root = Path(settings.MEDIA_ROOT) relative_path = file_path.relative_to(media_root) file_url = settings.MEDIA_URL + str(relative_path) icon = self._file_icon(filename) return format_html( '
{}',
result_json,
)
files_block = self._render_files_html(files) if files else format_html("{}", "")
if screenshot:
from django.conf import settings
if screenshot.startswith(str(settings.MEDIA_ROOT)):
screenshot_url = screenshot.replace(str(settings.MEDIA_ROOT), settings.MEDIA_URL)
single_screenshot_block = format_html(
'