aa
This commit is contained in:
137
app_web.py
137
app_web.py
@@ -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,38 +602,60 @@ async def api_stats_overview(
|
|||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def index(request: Request):
|
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:
|
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:
|
except Exception as e:
|
||||||
err = str(e)
|
logger.exception("GET / 渲染失败")
|
||||||
return templates.TemplateResponse(
|
tb = traceback.format_exc()
|
||||||
"index.html",
|
try:
|
||||||
{
|
service._append(f"首页异常:{e}")
|
||||||
"request": request,
|
for line in tb.splitlines()[:80]:
|
||||||
"fields": fields,
|
if line.strip():
|
||||||
"binary_env_keys": list(BINARY_ENV_KEYS),
|
service._append(line.strip())
|
||||||
"monitored": monitored,
|
except Exception:
|
||||||
"monitored_ids": monitored_ids,
|
pass
|
||||||
"account_channels": account_channels,
|
detail = tb[:12000]
|
||||||
"connected": service.is_connected(),
|
return HTMLResponse(
|
||||||
"job_running": service.is_job_running(),
|
"<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\"/><title>错误</title></head>"
|
||||||
"job_name": service.job_name,
|
"<body style=\"font-family:system-ui;padding:16px;background:#0f172a;color:#e2e8f0;\">"
|
||||||
"job_uptime": service.current_job_uptime(),
|
"<h1>首页 Internal Server Error</h1>"
|
||||||
"continuous_running": service.is_continuous_running(),
|
"<p>常见原因:Docker 挂载目录里缺少 <code>templates/</code> 或 <code>telegram-scraper.py</code>;或 <code>state.json</code> 格式异常。</p>"
|
||||||
"continuous_uptime": service.continuous_uptime(),
|
"<pre style=\"white-space:pre-wrap;word-break:break-all;font-size:12px;\">"
|
||||||
"logs": "\n".join(service.logs),
|
+ html.escape(detail)
|
||||||
"error": err,
|
+ "</pre></body></html>",
|
||||||
},
|
status_code=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/config")
|
@app.post("/config")
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user