This commit is contained in:
2026-04-27 01:42:47 +08:00
parent e30292e330
commit 384d7e4838
2 changed files with 281 additions and 35 deletions

View File

@@ -7,6 +7,7 @@ import io
import json
import logging
import os
import secrets
import sqlite3
import time
import traceback
@@ -16,8 +17,9 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
from fastapi import FastAPI, Form, Query, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware
BASE_DIR = Path(__file__).resolve().parent
ENV_FILE = BASE_DIR / ".env"
@@ -30,7 +32,43 @@ os.environ.setdefault("TELEGRAM_WEB_UI", "1")
logger = logging.getLogger("uvicorn.error")
WEB_CONSOLE_AUTH_KEY = "web_console_authenticated"
def _web_console_password() -> str:
return os.getenv("WEB_CONSOLE_PASSWORD", "Aa123456")
def _web_session_secret() -> str:
s = (os.getenv("WEB_CONSOLE_SESSION_SECRET") or "").strip()
if s:
return s
return "telegram-scraper-web-console-dev-secret-change-me"
def is_console_authed(request: Request) -> bool:
return bool(request.session.get(WEB_CONSOLE_AUTH_KEY))
def redirect_if_console_unauthed(request: Request) -> Optional[RedirectResponse]:
if is_console_authed(request):
return None
return RedirectResponse(url="/?needauth=1", status_code=303)
def json_if_console_unauthed(request: Request) -> Optional[JSONResponse]:
if is_console_authed(request):
return None
return JSONResponse({"ok": False, "error": "需要控制台密码验证"}, status_code=401)
app = FastAPI(title="Telegram Scraper Web Console")
app.add_middleware(
SessionMiddleware,
secret_key=_web_session_secret(),
max_age=14 * 24 * 3600,
same_site="lax",
)
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
# Starlette 较新版本TemplateResponse(request, name, context);旧版:(name, context)
@@ -608,6 +646,41 @@ def compute_storage_stats(base: Path, days: int, keyword_list: Tuple[str, ...])
}
@app.get("/auth/console/status")
async def auth_console_status(request: Request):
return {"ok": is_console_authed(request)}
@app.post("/auth/console/login")
async def auth_console_login(request: Request):
ct = (request.headers.get("content-type") or "").lower()
password = ""
if "application/json" in ct:
try:
body = await request.json()
password = str(body.get("password", ""))
except Exception:
password = ""
else:
form = await request.form()
password = str(form.get("password", ""))
expected = _web_console_password()
try:
ok = secrets.compare_digest(password.encode("utf-8"), expected.encode("utf-8"))
except Exception:
ok = False
if ok:
request.session[WEB_CONSOLE_AUTH_KEY] = True
return {"ok": True}
return JSONResponse({"ok": False, "error": "密码错误"}, status_code=401)
@app.post("/auth/console/logout")
async def auth_console_logout(request: Request):
request.session.pop(WEB_CONSOLE_AUTH_KEY, None)
return RedirectResponse(url="/", status_code=303)
@app.get("/api/stats/overview")
async def api_stats_overview(
days: int = Query(30, ge=1, le=366),
@@ -636,10 +709,13 @@ async def index(request: Request):
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)
authed = is_console_authed(request)
if authed:
try:
account_channels = await service.list_account_channels()
except Exception as e:
err = str(e)
logs_text = "\n".join(service.logs) if authed else ""
return template_response(
request,
"index.html",
@@ -649,13 +725,15 @@ async def index(request: Request):
"monitored": monitored,
"monitored_ids": monitored_ids,
"account_channels": account_channels,
"console_authed": authed,
"need_auth_banner": request.query_params.get("needauth") == "1",
"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),
"logs": logs_text,
"error": err,
},
)
@@ -684,6 +762,9 @@ async def index(request: Request):
@app.post("/config")
async def save_config(request: Request):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
form = await request.form()
updates: Dict[str, str] = {}
for k, _label, _field_type in CONFIG_KEYS:
@@ -696,7 +777,10 @@ async def save_config(request: Request):
@app.post("/start")
async def start_scraper():
async def start_scraper(request: Request):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
try:
await service.ensure_ready()
except Exception as e:
@@ -705,13 +789,19 @@ async def start_scraper():
@app.post("/stop")
async def stop_scraper():
async def stop_scraper(request: Request):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
await service.disconnect()
return RedirectResponse(url="/", status_code=303)
@app.post("/channels/add")
async def add_channel(channel_spec: str = Form(...)):
async def add_channel(request: Request, channel_spec: str = Form(...)):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
try:
msg = await service.add_channel(channel_spec)
service._append(msg)
@@ -722,6 +812,9 @@ async def add_channel(channel_spec: str = Form(...)):
@app.post("/channels/add-selected")
async def add_channels_selected(request: Request):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
form = await request.form()
raw_ids = form.getlist("channel_id")
ids = [str(x).strip() for x in raw_ids if str(x).strip()]
@@ -738,7 +831,10 @@ async def add_channels_selected(request: Request):
@app.post("/channels/remove")
async def remove_channel(channel_spec: str = Form(...)):
async def remove_channel(request: Request, channel_spec: str = Form(...)):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
try:
msg = await service.remove_channel(channel_spec)
service._append(msg)
@@ -748,7 +844,10 @@ async def remove_channel(channel_spec: str = Form(...)):
@app.post("/jobs/scrape")
async def start_scrape(selection: str = Form("all")):
async def start_scrape(request: Request, selection: str = Form("all")):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
try:
msg = await service.start_scrape_job(selection)
service._append(msg)
@@ -758,7 +857,10 @@ async def start_scrape(selection: str = Form("all")):
@app.post("/jobs/export")
async def start_export(selection: str = Form("all")):
async def start_export(request: Request, selection: str = Form("all")):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
try:
msg = await service.start_export_job(selection)
service._append(msg)
@@ -768,7 +870,10 @@ async def start_export(selection: str = Form("all")):
@app.post("/jobs/rescrape")
async def start_rescrape(selection: str = Form("all")):
async def start_rescrape(request: Request, selection: str = Form("all")):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
try:
msg = await service.start_rescrape_job(selection)
service._append(msg)
@@ -778,7 +883,10 @@ async def start_rescrape(selection: str = Form("all")):
@app.post("/jobs/continuous/start")
async def start_continuous():
async def start_continuous(request: Request):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
try:
msg = await service.start_continuous()
service._append(msg)
@@ -788,7 +896,10 @@ async def start_continuous():
@app.post("/jobs/continuous/stop")
async def stop_continuous():
async def stop_continuous(request: Request):
redir = redirect_if_console_unauthed(request)
if redir:
return redir
try:
msg = await service.stop_continuous()
service._append(msg)
@@ -798,7 +909,10 @@ async def stop_continuous():
@app.get("/status")
async def status():
async def status(request: Request):
denied = json_if_console_unauthed(request)
if denied:
return denied
return {
"ready": service.scraper is not None,
"connected": service.is_connected(),
@@ -818,7 +932,10 @@ async def api_monitored_channels():
@app.get("/api/channels/account")
async def api_account_channels():
async def api_account_channels(request: Request):
denied = json_if_console_unauthed(request)
if denied:
return denied
try:
items = await service.list_account_channels()
return {"items": items}
@@ -827,62 +944,84 @@ async def api_account_channels():
@app.post("/api/channels/add")
async def api_add_channel(channel_spec: str = Form(...)):
async def api_add_channel(request: Request, channel_spec: str = Form(...)):
denied = json_if_console_unauthed(request)
if denied:
return denied
msg = await service.add_channel(channel_spec)
service._append(msg)
return {"ok": True, "message": msg}
@app.post("/api/channels/remove")
async def api_remove_channel(channel_spec: str = Form(...)):
async def api_remove_channel(request: Request, channel_spec: str = Form(...)):
denied = json_if_console_unauthed(request)
if denied:
return denied
msg = await service.remove_channel(channel_spec)
service._append(msg)
return {"ok": True, "message": msg}
@app.post("/api/jobs/scrape")
async def api_job_scrape(selection: str = Form("all")):
async def api_job_scrape(request: Request, selection: str = Form("all")):
denied = json_if_console_unauthed(request)
if denied:
return denied
msg = await service.start_scrape_job(selection)
service._append(msg)
return {"ok": True, "message": msg}
@app.post("/api/jobs/export")
async def api_job_export(selection: str = Form("all")):
async def api_job_export(request: Request, selection: str = Form("all")):
denied = json_if_console_unauthed(request)
if denied:
return denied
msg = await service.start_export_job(selection)
service._append(msg)
return {"ok": True, "message": msg}
@app.post("/api/jobs/rescrape")
async def api_job_rescrape(selection: str = Form("all")):
async def api_job_rescrape(request: Request, selection: str = Form("all")):
denied = json_if_console_unauthed(request)
if denied:
return denied
msg = await service.start_rescrape_job(selection)
service._append(msg)
return {"ok": True, "message": msg}
@app.get("/api/jobs/status")
async def api_job_status():
return {
async def api_job_status(request: Request):
payload = {
"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": list(service.logs),
"logs": list(service.logs) if is_console_authed(request) else [],
}
return payload
@app.post("/api/jobs/continuous/start")
async def api_start_continuous():
async def api_start_continuous(request: Request):
denied = json_if_console_unauthed(request)
if denied:
return denied
msg = await service.start_continuous()
service._append(msg)
return {"ok": True, "message": msg}
@app.post("/api/jobs/continuous/stop")
async def api_stop_continuous():
async def api_stop_continuous(request: Request):
denied = json_if_console_unauthed(request)
if denied:
return denied
msg = await service.stop_continuous()
service._append(msg)
return {"ok": True, "message": msg}