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 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(
"<!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")

View File

@@ -908,7 +908,7 @@
<div class="list" id="monitored-list" role="list">
{% if 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 %}
{% else %}
<div class="line" role="listitem">暂无</div>