This commit is contained in:
2026-04-27 01:28:50 +08:00
parent b00a0c40d8
commit 4c48525b3a
2 changed files with 98 additions and 41 deletions

View File

@@ -1,27 +1,42 @@
import asyncio import asyncio
import contextlib import contextlib
import html
import importlib.util import importlib.util
import io import io
import json import json
import logging
import os import os
import sqlite3 import sqlite3
import time import time
import traceback
from collections import deque from collections import deque
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path 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 import FastAPI, Form, Query, Request
from fastapi.responses import RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
ENV_FILE = BASE_DIR / ".env" ENV_FILE = BASE_DIR / ".env"
SCRIPT_FILE = BASE_DIR / "telegram-scraper.py" SCRIPT_FILE = BASE_DIR / "telegram-scraper.py"
STATE_FILE = BASE_DIR / "state.json" STATE_FILE = BASE_DIR / "state.json"
TEMPLATES_DIR = BASE_DIR / "templates"
logger = logging.getLogger("uvicorn.error")
app = FastAPI(title="Telegram Scraper Web Console") 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]]: 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")) raw = json.loads(STATE_FILE.read_text(encoding="utf-8"))
titles = raw.get("channel_titles") or {} titles = raw.get("channel_titles") or {}
names = raw.get("channel_names") 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()} return {str(k): str(v) for k, v in titles.items()}, {str(k): str(v) for k, v in names.items()}
except Exception: except Exception:
return {}, {} 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: def channel_display_name(cid: str, titles: Dict[str, str], names: Dict[str, str]) -> str:
cid = str(cid) 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()): if t and t != cid and not (len(t) > 6 and t.startswith("-") and t[1:].replace("-", "").isdigit()):
return t 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": if u and u != "no_username":
return "@" + u.lstrip("@") return "@" + u.lstrip("@")
return "未命名频道" return "未命名频道"
@@ -172,9 +197,15 @@ class WebScraperService:
return [] return []
try: try:
raw = json.loads(STATE_FILE.read_text(encoding="utf-8")) raw = json.loads(STATE_FILE.read_text(encoding="utf-8"))
channels = raw.get("channels", {}) channels = raw.get("channels") or {}
names = raw.get("channel_names", {}) if not isinstance(channels, dict):
titles = raw.get("channel_titles", {}) 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 [ return [
{ {
"channel_id": str(cid), "channel_id": str(cid),
@@ -186,8 +217,12 @@ class WebScraperService:
] ]
except Exception: except Exception:
return [] return []
titles = self.scraper.state.get("channel_titles", {}) titles = self.scraper.state.get("channel_titles") or {}
names = self.scraper.state.get("channel_names", {}) 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]] = [] rows: List[Dict[str, str]] = []
for cid, last_id in self.scraper.state.get("channels", {}).items(): for cid, last_id in self.scraper.state.get("channels", {}).items():
rows.append( rows.append(
@@ -567,6 +602,7 @@ async def api_stats_overview(
@app.get("/") @app.get("/")
async def index(request: Request): async def index(request: Request):
try:
env_vals = read_env_dict() env_vals = read_env_dict()
fields = [ fields = [
{"key": k, "label": label, "type": field_type, "value": env_vals.get(k, "")} {"key": k, "label": label, "type": field_type, "value": env_vals.get(k, "")}
@@ -599,6 +635,27 @@ async def index(request: Request):
"error": err, "error": err,
}, },
) )
except Exception as e:
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(
"<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\"/><title>错误</title></head>"
"<body style=\"font-family:system-ui;padding:16px;background:#0f172a;color:#e2e8f0;\">"
"<h1>首页 Internal Server Error</h1>"
"<p>常见原因Docker 挂载目录里缺少 <code>templates/</code> 或 <code>telegram-scraper.py</code>;或 <code>state.json</code> 格式异常。</p>"
"<pre style=\"white-space:pre-wrap;word-break:break-all;font-size:12px;\">"
+ html.escape(detail)
+ "</pre></body></html>",
status_code=500,
)
@app.post("/config") @app.post("/config")

View File

@@ -908,7 +908,7 @@
<div class="list" id="monitored-list" role="list"> <div class="list" id="monitored-list" role="list">
{% if monitored %} {% if monitored %}
{% for ch in monitored %} {% for ch in monitored %}
<div class="line" role="listitem">[{{ loop.index }}] {{ ch.display_name }} · 进度消息 ID {{ ch.last_message_id }}</div> <div class="line" role="listitem">[{{ loop.index }}] {{ ch.display_name | default('未命名频道') }} · 进度消息 ID {{ ch.last_message_id }}</div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<div class="line" role="listitem">暂无</div> <div class="line" role="listitem">暂无</div>