diff --git a/app_web.py b/app_web.py index 50bd13d..5730476 100644 --- a/app_web.py +++ b/app_web.py @@ -1,27 +1,42 @@ import asyncio import contextlib +import html import importlib.util import io import json +import logging import os import sqlite3 import time +import traceback from collections import deque from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union from fastapi import FastAPI, Form, Query, Request -from fastapi.responses import RedirectResponse +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates BASE_DIR = Path(__file__).resolve().parent ENV_FILE = BASE_DIR / ".env" SCRIPT_FILE = BASE_DIR / "telegram-scraper.py" STATE_FILE = BASE_DIR / "state.json" +TEMPLATES_DIR = BASE_DIR / "templates" + +logger = logging.getLogger("uvicorn.error") app = FastAPI(title="Telegram Scraper Web Console") -templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) +templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) + + +@app.on_event("startup") +async def _verify_runtime_files() -> None: + idx = TEMPLATES_DIR / "index.html" + if not idx.is_file(): + raise RuntimeError(f"缺少模板文件(请确认挂载目录含 templates/):{idx.resolve()}") + if not SCRIPT_FILE.is_file(): + raise RuntimeError(f"缺少 telegram-scraper.py:{SCRIPT_FILE.resolve()}") def read_state_channel_maps() -> Tuple[Dict[str, str], Dict[str, str]]: @@ -31,17 +46,27 @@ def read_state_channel_maps() -> Tuple[Dict[str, str], Dict[str, str]]: raw = json.loads(STATE_FILE.read_text(encoding="utf-8")) titles = raw.get("channel_titles") or {} names = raw.get("channel_names") or {} + if not isinstance(titles, dict): + titles = {} + if not isinstance(names, dict): + names = {} return {str(k): str(v) for k, v in titles.items()}, {str(k): str(v) for k, v in names.items()} except Exception: return {}, {} +def _as_str(v: Union[str, int, float, None]) -> str: + if v is None: + return "" + return str(v).strip() + + def channel_display_name(cid: str, titles: Dict[str, str], names: Dict[str, str]) -> str: cid = str(cid) - t = (titles.get(cid) or "").strip() + t = _as_str(titles.get(cid)) if t and t != cid and not (len(t) > 6 and t.startswith("-") and t[1:].replace("-", "").isdigit()): return t - u = (names.get(cid) or "no_username").strip() + u = _as_str(names.get(cid)) or "no_username" if u and u != "no_username": return "@" + u.lstrip("@") return "未命名频道" @@ -172,9 +197,15 @@ class WebScraperService: return [] try: raw = json.loads(STATE_FILE.read_text(encoding="utf-8")) - channels = raw.get("channels", {}) - names = raw.get("channel_names", {}) - titles = raw.get("channel_titles", {}) + channels = raw.get("channels") or {} + if not isinstance(channels, dict): + channels = {} + names = raw.get("channel_names") or {} + titles = raw.get("channel_titles") or {} + if not isinstance(names, dict): + names = {} + if not isinstance(titles, dict): + titles = {} return [ { "channel_id": str(cid), @@ -186,8 +217,12 @@ class WebScraperService: ] except Exception: return [] - titles = self.scraper.state.get("channel_titles", {}) - names = self.scraper.state.get("channel_names", {}) + titles = self.scraper.state.get("channel_titles") or {} + names = self.scraper.state.get("channel_names") or {} + if not isinstance(titles, dict): + titles = {} + if not isinstance(names, dict): + names = {} rows: List[Dict[str, str]] = [] for cid, last_id in self.scraper.state.get("channels", {}).items(): rows.append( @@ -567,38 +602,60 @@ async def api_stats_overview( @app.get("/") async def index(request: Request): - env_vals = read_env_dict() - fields = [ - {"key": k, "label": label, "type": field_type, "value": env_vals.get(k, "")} - for k, label, field_type in CONFIG_KEYS - ] - monitored = await service.list_monitored_channels() - monitored_ids = {str(row["channel_id"]) for row in monitored} - account_channels: List[Dict] = [] - err = "" try: - account_channels = await service.list_account_channels() + env_vals = read_env_dict() + fields = [ + {"key": k, "label": label, "type": field_type, "value": env_vals.get(k, "")} + for k, label, field_type in CONFIG_KEYS + ] + monitored = await service.list_monitored_channels() + monitored_ids = {str(row["channel_id"]) for row in monitored} + account_channels: List[Dict] = [] + err = "" + try: + account_channels = await service.list_account_channels() + except Exception as e: + err = str(e) + return templates.TemplateResponse( + "index.html", + { + "request": request, + "fields": fields, + "binary_env_keys": list(BINARY_ENV_KEYS), + "monitored": monitored, + "monitored_ids": monitored_ids, + "account_channels": account_channels, + "connected": service.is_connected(), + "job_running": service.is_job_running(), + "job_name": service.job_name, + "job_uptime": service.current_job_uptime(), + "continuous_running": service.is_continuous_running(), + "continuous_uptime": service.continuous_uptime(), + "logs": "\n".join(service.logs), + "error": err, + }, + ) except Exception as e: - err = str(e) - return templates.TemplateResponse( - "index.html", - { - "request": request, - "fields": fields, - "binary_env_keys": list(BINARY_ENV_KEYS), - "monitored": monitored, - "monitored_ids": monitored_ids, - "account_channels": account_channels, - "connected": service.is_connected(), - "job_running": service.is_job_running(), - "job_name": service.job_name, - "job_uptime": service.current_job_uptime(), - "continuous_running": service.is_continuous_running(), - "continuous_uptime": service.continuous_uptime(), - "logs": "\n".join(service.logs), - "error": err, - }, - ) + logger.exception("GET / 渲染失败") + tb = traceback.format_exc() + try: + service._append(f"首页异常:{e}") + for line in tb.splitlines()[:80]: + if line.strip(): + service._append(line.strip()) + except Exception: + pass + detail = tb[:12000] + return HTMLResponse( + "
常见原因:Docker 挂载目录里缺少 templates/ 或 telegram-scraper.py;或 state.json 格式异常。
" + + html.escape(detail) + + "", + status_code=500, + ) @app.post("/config") diff --git a/templates/index.html b/templates/index.html index 7257624..6a691f1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -908,7 +908,7 @@