aa
This commit is contained in:
77
app_web.py
77
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,6 +602,7 @@ async def api_stats_overview(
|
||||
|
||||
@app.get("/")
|
||||
async def index(request: Request):
|
||||
try:
|
||||
env_vals = read_env_dict()
|
||||
fields = [
|
||||
{"key": k, "label": label, "type": field_type, "value": env_vals.get(k, "")}
|
||||
@@ -599,6 +635,27 @@ async def index(request: Request):
|
||||
"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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user