aa
This commit is contained in:
137
app_web.py
137
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(
|
||||
"<!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")
|
||||
|
||||
Reference in New Issue
Block a user