diff --git a/app_web.py b/app_web.py index 3dbf02f..403c266 100644 --- a/app_web.py +++ b/app_web.py @@ -32,6 +32,25 @@ os.environ.setdefault("TELEGRAM_WEB_UI", "1") logger = logging.getLogger("uvicorn.error") + +def web_url_prefix() -> str: + """反代子路径时设置 WEB_URL_PREFIX=/前缀(勿尾斜杠),与 nginx location 一致。""" + return (os.getenv("WEB_URL_PREFIX") or "").strip().rstrip("/") + + +def with_url_prefix(path: str) -> str: + path = path if path.startswith("/") else f"/{path}" + pre = web_url_prefix() + return f"{pre}{path}" if pre else path + + +def app_home_url(*, needauth: bool = False) -> str: + pre = web_url_prefix() + base = f"{pre}/" if pre else "/" + if needauth: + return f"{base}?needauth=1" + return base + WEB_CONSOLE_AUTH_KEY = "web_console_authenticated" @@ -53,7 +72,7 @@ def is_console_authed(request: Request) -> bool: def redirect_if_console_unauthed(request: Request) -> Optional[RedirectResponse]: if is_console_authed(request): return None - return RedirectResponse(url="/?needauth=1", status_code=303) + return RedirectResponse(url=app_home_url(needauth=True), status_code=303) def json_if_console_unauthed(request: Request) -> Optional[JSONResponse]: @@ -179,6 +198,7 @@ class WebScraperService: self.logs: deque[str] = deque(maxlen=1200) self.continuous_task: Optional[asyncio.Task] = None self.continuous_started_at: Optional[float] = None + self.continuous_start_lock = asyncio.Lock() def _append(self, text: str) -> None: ts = time.strftime("%Y-%m-%d %H:%M:%S") @@ -425,25 +445,26 @@ class WebScraperService: return await self.start_job(f"补抓媒体({len(channels)}个频道)", runner()) async def start_continuous(self) -> str: - await self.ensure_ready() - if self.is_continuous_running(): - return "持续抓取已在运行中" - if not self.scraper.state.get("channels"): - raise ValueError("当前没有监控频道,请先添加频道。") + async with self.continuous_start_lock: + await self.ensure_ready() + if self.is_continuous_running(): + return "持续抓取已在运行中" + if not self.scraper.state.get("channels"): + raise ValueError("当前没有监控频道,请先添加频道。") - async def runner(): - try: - await self._run_and_capture(self.scraper.continuous_scraping()) - except asyncio.CancelledError: - self._append("持续抓取任务已取消。") - raise - except Exception as e: - self._append(f"持续抓取异常:{e}") + async def runner(): + try: + await self._run_and_capture(self.scraper.continuous_scraping()) + except asyncio.CancelledError: + self._append("持续抓取任务已取消。") + raise + except Exception as e: + self._append(f"持续抓取异常:{e}") - self.continuous_started_at = time.time() - self.continuous_task = asyncio.create_task(runner()) - self._append("已启动持续抓取(含心跳逻辑)。") - return "持续抓取已启动" + self.continuous_started_at = time.time() + self.continuous_task = asyncio.create_task(runner()) + self._append("已启动持续抓取(含心跳逻辑)。") + return "持续抓取已启动" async def stop_continuous(self) -> str: if not self.is_continuous_running(): @@ -678,7 +699,7 @@ async def auth_console_login(request: Request): @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) + return RedirectResponse(url=app_home_url(), status_code=303) @app.get("/api/stats/overview") @@ -727,6 +748,9 @@ async def index(request: Request): "account_channels": account_channels, "console_authed": authed, "need_auth_banner": request.query_params.get("needauth") == "1", + "app_url": with_url_prefix, + "url_prefix": web_url_prefix(), + "app_home": app_home_url(), "connected": service.is_connected(), "job_running": service.is_job_running(), "job_name": service.job_name, @@ -773,7 +797,7 @@ async def save_config(request: Request): else: updates[k] = str(form.get(k, "")).strip() write_env_updates(updates) - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/start") @@ -785,7 +809,7 @@ async def start_scraper(request: Request): await service.ensure_ready() except Exception as e: service._append(f"初始化失败:{e}") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/stop") @@ -794,7 +818,7 @@ async def stop_scraper(request: Request): if redir: return redir await service.disconnect() - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/channels/add") @@ -807,7 +831,7 @@ async def add_channel(request: Request, channel_spec: str = Form(...)): service._append(msg) except Exception as e: service._append(f"添加频道失败:{e}") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/channels/add-selected") @@ -820,14 +844,14 @@ async def add_channels_selected(request: Request): ids = [str(x).strip() for x in raw_ids if str(x).strip()] if not ids: service._append("未选择任何频道,请在列表中勾选后再提交。") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) for spec in ids: try: msg = await service.add_channel(spec) service._append(msg) except Exception as e: service._append(f"添加失败({spec}):{e}") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/channels/remove") @@ -840,7 +864,7 @@ async def remove_channel(request: Request, channel_spec: str = Form(...)): service._append(msg) except Exception as e: service._append(f"移除频道失败:{e}") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/jobs/scrape") @@ -853,7 +877,7 @@ async def start_scrape(request: Request, selection: str = Form("all")): service._append(msg) except Exception as e: service._append(f"启动抓取失败:{e}") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/jobs/export") @@ -866,7 +890,7 @@ async def start_export(request: Request, selection: str = Form("all")): service._append(msg) except Exception as e: service._append(f"启动导出失败:{e}") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/jobs/rescrape") @@ -879,20 +903,24 @@ async def start_rescrape(request: Request, selection: str = Form("all")): service._append(msg) except Exception as e: service._append(f"启动补抓失败:{e}") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/jobs/continuous/start") -async def start_continuous(request: Request): +async def start_continuous_route(request: Request): redir = redirect_if_console_unauthed(request) if redir: return redir - try: - msg = await service.start_continuous() - service._append(msg) - except Exception as e: - service._append(f"启动持续抓取失败:{e}") - return RedirectResponse(url="/", status_code=303) + + async def _run_start_continuous() -> None: + try: + await service.start_continuous() + except Exception as e: + service._append(f"启动持续抓取失败:{e}") + + asyncio.create_task(_run_start_continuous()) + service._append("已接收「启动持续抓取」:正在后台连接 Telegram 并启动(请勿重复点击;进度见运行日志)。") + return RedirectResponse(url=app_home_url(), status_code=303) @app.post("/jobs/continuous/stop") @@ -905,7 +933,7 @@ async def stop_continuous(request: Request): service._append(msg) except Exception as e: service._append(f"停止持续抓取失败:{e}") - return RedirectResponse(url="/", status_code=303) + return RedirectResponse(url=app_home_url(), status_code=303) @app.get("/status") diff --git a/templates/index.html b/templates/index.html index 276f6aa..bd18f3a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -764,7 +764,7 @@ {% if console_authed %} -
{% endif %} @@ -859,16 +859,16 @@客户端:{{ error }}
{% endif %}@用户名、-100…、https://t.me/xxx。
不支持仅靠群内显示标题。
-
移除:all;1,2;-100…;@用户名 / t.me(须已在监控中)。
范围:all、1,3、-100… 或已在监控的 @ / 链接。
账号可见频道:勾选后加入监控。标题包含配置项「账号列表隐藏」中任一子串的会话不会出现在此列表(默认隐藏含「远程-到岗-技术招聘」的群,避免选到自有招聘群)。
{% if account_channels %} -