From 51ae0756e00bac8668e5d9ca0dcd41abb2f1331e Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 12 Feb 2026 17:10:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=93=88=E5=93=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/protocol.py | 3 +- requirements.txt | 2 + server/api/accounts.py | 103 ++++++++++++++++++++ server/config.py | 7 ++ server/db.py | 182 ++++++++++++++++++++++++++++++++++++ server/main.py | 39 +++++++- server/models.py | 114 ++++++++++++++++++++-- worker/tasks/check_login.py | 136 +++++++++++++++++++++++++++ worker/tasks/registry.py | 2 + 9 files changed, 578 insertions(+), 10 deletions(-) create mode 100644 server/api/accounts.py create mode 100644 server/db.py create mode 100644 worker/tasks/check_login.py diff --git a/common/protocol.py b/common/protocol.py index f03ea1b..4b78755 100644 --- a/common/protocol.py +++ b/common/protocol.py @@ -44,7 +44,8 @@ class TaskStatus(str, Enum): class TaskType(str, Enum): """可扩展的任务类型。新增任务在此追加即可。""" - BOSS_RECRUIT = "boss_recruit" # BOSS 直聘招聘流程 + BOSS_RECRUIT = "boss_recruit" # BOSS 直聘招聘流程 + CHECK_LOGIN = "check_login" # 检测 BOSS 账号是否已登录 # ────────────────────────── 辅助函数 ────────────────────────── diff --git a/requirements.txt b/requirements.txt index e6d299d..935678f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ fastapi>=0.115.0 uvicorn>=0.34.0 pydantic>=2.0.0 +SQLAlchemy>=2.0.0 +PyMySQL>=1.1.0 # ─── Worker 代理 (worker/) ─── websockets>=14.0 diff --git a/server/api/accounts.py b/server/api/accounts.py new file mode 100644 index 0000000..c763850 --- /dev/null +++ b/server/api/accounts.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +BOSS 账号 API: +- POST /api/accounts -> 前台添加账号时绑定到指定电脑 +- POST /api/accounts/check -> 提交环境名称,自动派发 check_login 任务 +- GET /api/accounts -> 查询所有账号登录状态 +- GET /api/accounts/{worker_id} -> 查询指定 Worker 的账号状态 +""" +from __future__ import annotations + +from typing import Optional + +from fastapi import APIRouter, HTTPException + +from common.protocol import TaskStatus, TaskType +from server.models import AccountBindRequest, CheckLoginRequest, TaskCreate +from server.core.worker_manager import worker_manager +from server.core.task_dispatcher import task_dispatcher +from server import db + +router = APIRouter(prefix="/api/accounts", tags=["accounts"]) + + +@router.post("", status_code=201) +async def bind_account(req: AccountBindRequest): + """前台添加账号:保存账号环境与电脑绑定关系。""" + db.bind_account_to_worker(worker_id=req.worker_id, browser_name=req.browser_name) + return {"message": f"账号绑定已保存: {req.browser_name} -> {req.worker_id}"} + + +# ────────────────────────── 一键检测登录 ────────────────────────── + +@router.post("/check", status_code=201) +async def check_login(req: CheckLoginRequest): + """ + 前端提交: + { + "browser_name": "环境名称" + // 可选 "worker_id": "pc-1" + } + + 系统自动: + 1. 如果请求没传 worker_id,则按已绑定关系查 worker_id + 2. 派发 check_login 任务(Worker 会在比特浏览器中按名称查找该环境并打开) + 3. 返回任务 ID,前端可轮询 GET /api/tasks/{task_id} 获取结果 + """ + worker_id = req.worker_id + if not worker_id: + bind = db.get_account_by_name(req.browser_name) + if not bind: + raise HTTPException( + status_code=400, + detail=f"未找到账号绑定关系,请先调用 POST /api/accounts 绑定: {req.browser_name}", + ) + worker_id = bind.get("worker_id") + + # 检查 Worker 是否在线 + if not worker_manager.is_online(worker_id): + raise HTTPException(status_code=503, detail=f"Worker {worker_id} 不在线") + + # 创建 check_login 任务 + task_req = TaskCreate( + task_type=TaskType.CHECK_LOGIN, + worker_id=worker_id, + account_name=req.browser_name, + params={"account_name": req.browser_name}, + ) + task = task_dispatcher.create_task(task_req) + + # 通过 WebSocket 派发 + ws = worker_manager.get_ws(worker_id) + if not ws: + task.status = TaskStatus.FAILED + task.error = "Worker WebSocket 连接不存在" + raise HTTPException(status_code=503, detail="Worker WebSocket 连接不存在") + + success = await task_dispatcher.dispatch(task, ws.send_json) + if not success: + raise HTTPException(status_code=503, detail=f"任务派发失败: {task.error}") + + worker_manager.set_current_task(worker_id, task.task_id) + + return { + "message": f"检测任务已派发,环境: {req.browser_name},目标: {worker_id}", + "task_id": task.task_id, + "worker_id": worker_id, + } + + +# ────────────────────────── 查询账号状态 ────────────────────────── + +@router.get("") +async def list_accounts(worker_id: Optional[str] = None): + """查询 BOSS 账号登录状态列表。""" + if worker_id: + return db.get_accounts_by_worker(worker_id) + return db.get_all_accounts() + + +@router.get("/{worker_id}") +async def get_worker_accounts(worker_id: str): + """查询指定 Worker 的所有账号状态。""" + return db.get_accounts_by_worker(worker_id) diff --git a/server/config.py b/server/config.py index 47f6d7a..1814ab0 100644 --- a/server/config.py +++ b/server/config.py @@ -17,6 +17,13 @@ HEARTBEAT_TIMEOUT: int = 90 # 超时未收到心跳视为离 # ─── 安全(可选) ─── API_TOKEN: str = os.getenv("API_TOKEN", "") # 非空时校验 Header: Authorization: Bearer +# ─── 数据库(MySQL) ─── +DB_HOST: str = os.getenv("DB_HOST", "8.137.99.82") +DB_PORT: int = int(os.getenv("DB_PORT", "3306")) +DB_USER: str = os.getenv("DB_USER", "boss_dp") +DB_PASSWORD: str = os.getenv("DB_PASSWORD", "H7sEY3t4YbF6Jp2h") +DB_NAME: str = os.getenv("DB_NAME", "boss_dp") + # ─── 隧道(内网穿透) ─── TUNNEL_CONTROL_PORT: int = int(os.getenv("TUNNEL_CONTROL_PORT", "9090")) # 隧道控制端口(客户端连接) TUNNEL_STREAM_PORT: int = int(os.getenv("TUNNEL_STREAM_PORT", "9091")) # 隧道流端口(桥接用) diff --git a/server/db.py b/server/db.py new file mode 100644 index 0000000..d541c36 --- /dev/null +++ b/server/db.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +""" +数据库模块:SQLAlchemy 引擎、会话管理、CRUD 操作。 +""" +from __future__ import annotations + +import logging +from datetime import datetime +from typing import Optional + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker + +from server import config +from server.models import Base, BossAccount, TaskLog + +logger = logging.getLogger("server.db") + +# ────────────────────────── 引擎与会话 ────────────────────────── + +_db_url = ( + f"mysql+pymysql://{config.DB_USER}:{config.DB_PASSWORD}" + f"@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}" + f"?charset=utf8mb4" +) + +engine = create_engine(_db_url, pool_pre_ping=True, pool_recycle=3600, echo=False) +SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False) + + +def get_session() -> Session: + """获取一个新的数据库会话(调用方负责关闭)。""" + return SessionLocal() + + +def init_db() -> None: + """创建所有 ORM 定义的表(如果不存在)。""" + Base.metadata.create_all(bind=engine) + logger.info("数据库表初始化完成 (SQLAlchemy ORM)") + + +# ────────────────────────── BossAccount CRUD ────────────────────────── + +def upsert_account_status( + worker_id: str, + browser_id: str, + browser_name: str, + boss_username: str, + is_logged_in: bool, +) -> BossAccount: + """插入或更新 BOSS 账号登录状态。""" + with get_session() as session: + # 优先使用 worker_id + browser_name 匹配(前台绑定关系) + account = None + if browser_name: + account = ( + session.query(BossAccount) + .filter_by(worker_id=worker_id, browser_name=browser_name) + .first() + ) + # 兜底:使用 worker_id + browser_id 匹配 + if account is None and browser_id: + account = ( + session.query(BossAccount) + .filter_by(worker_id=worker_id, browser_id=browser_id) + .first() + ) + if account: + account.browser_id = browser_id or account.browser_id + account.browser_name = browser_name or account.browser_name + account.boss_username = boss_username + account.is_logged_in = is_logged_in + account.checked_at = datetime.now() + else: + account = BossAccount( + worker_id=worker_id, + browser_id=browser_id or f"name:{browser_name}", + browser_name=browser_name, + boss_username=boss_username, + is_logged_in=is_logged_in, + checked_at=datetime.now(), + ) + session.add(account) + session.commit() + session.refresh(account) + logger.info( + "账号状态更新: worker=%s, browser=%s(%s), username=%s, logged_in=%s", + worker_id, browser_name, browser_id, boss_username, is_logged_in, + ) + return account + + +def bind_account_to_worker(worker_id: str, browser_name: str) -> BossAccount: + """ + 前台添加账号时建立绑定关系:环境名称 -> 电脑(worker)。 + 初始状态设为未登录,等待后续 check_login 刷新。 + """ + with get_session() as session: + account = ( + session.query(BossAccount) + .filter_by(worker_id=worker_id, browser_name=browser_name) + .first() + ) + if account: + return account + account = BossAccount( + worker_id=worker_id, + # 避免 browser_id 为空导致联合唯一冲突,先放占位值 + browser_id=f"name:{browser_name}", + browser_name=browser_name, + boss_username="", + is_logged_in=False, + checked_at=None, + ) + session.add(account) + session.commit() + session.refresh(account) + logger.info("账号绑定已保存: %s -> %s", browser_name, worker_id) + return account + + +def get_all_accounts() -> list[dict]: + """获取所有账号状态。""" + with get_session() as session: + rows = session.query(BossAccount).order_by(BossAccount.updated_at.desc()).all() + return [r.to_dict() for r in rows] + + +def get_accounts_by_worker(worker_id: str) -> list[dict]: + """获取指定 Worker 的所有账号状态。""" + with get_session() as session: + rows = ( + session.query(BossAccount) + .filter_by(worker_id=worker_id) + .order_by(BossAccount.updated_at.desc()) + .all() + ) + return [r.to_dict() for r in rows] + + +def get_account_by_name(browser_name: str, worker_id: Optional[str] = None) -> Optional[dict]: + """按浏览器环境名查找账号记录。""" + with get_session() as session: + q = session.query(BossAccount).filter_by(browser_name=browser_name) + if worker_id: + q = q.filter_by(worker_id=worker_id) + row = q.first() + return row.to_dict() if row else None + + +# ────────────────────────── TaskLog CRUD ────────────────────────── + +def save_task_log( + task_id: str, + task_type: str, + worker_id: str, + status: str, + params: dict = None, + result=None, + error: str = None, +) -> TaskLog: + """保存或更新任务执行记录。""" + with get_session() as session: + log = session.query(TaskLog).filter_by(task_id=task_id).first() + if log: + log.status = status + log.result = result + log.error = error + else: + log = TaskLog( + task_id=task_id, + task_type=task_type, + worker_id=worker_id, + status=status, + params=params, + result=result, + error=error, + ) + session.add(log) + session.commit() + session.refresh(log) + return log diff --git a/server/main.py b/server/main.py index 7c7d6ee..5f3b2af 100644 --- a/server/main.py +++ b/server/main.py @@ -14,12 +14,14 @@ import uvicorn from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware -from common.protocol import MsgType, make_msg +from common.protocol import MsgType, TaskType, make_msg from server import config from server.api.workers import router as workers_router from server.api.tasks import router as tasks_router +from server.api.accounts import router as accounts_router from server.core.worker_manager import worker_manager from server.core.task_dispatcher import task_dispatcher +from server import db from tunnel.server import TunnelServer logging.basicConfig( @@ -36,6 +38,13 @@ logger = logging.getLogger("server.main") async def lifespan(app: FastAPI): """应用生命周期:启动时初始化隧道和心跳巡检,关闭时清理资源。""" # ── startup ── + # 初始化数据库(SQLAlchemy ORM 建表) + try: + db.init_db() + logger.info("数据库初始化完成") + except Exception as e: + logger.error("数据库初始化失败: %s(服务继续运行,但数据库功能不可用)", e) + asyncio.create_task(worker_manager.check_heartbeats_loop()) tunnel_server = TunnelServer( control_port=config.TUNNEL_CONTROL_PORT, @@ -70,6 +79,7 @@ app.add_middleware( app.include_router(workers_router) app.include_router(tasks_router) +app.include_router(accounts_router) # ────────────────────────── 健康检查 ────────────────────────── @@ -141,6 +151,33 @@ async def ws_endpoint(ws: WebSocket): worker_manager.set_current_task(worker_id, None) logger.info("任务 %s 已完成", task_id) + # ── 将结果写入数据库 ── + try: + task_info = task_dispatcher.get_task(task_id) + if task_info: + # 保存任务日志 + db.save_task_log( + task_id=task_id, + task_type=task_info.task_type.value if hasattr(task_info.task_type, 'value') else str(task_info.task_type), + worker_id=worker_id, + status=task_info.status.value if hasattr(task_info.status, 'value') else str(task_info.status), + params=task_info.params, + result=result, + error=error, + ) + # check_login 任务:更新账号状态表 + task_type_val = task_info.task_type.value if hasattr(task_info.task_type, 'value') else str(task_info.task_type) + if task_type_val == TaskType.CHECK_LOGIN.value and result and not error: + db.upsert_account_status( + worker_id=worker_id, + browser_id=result.get("browser_id", ""), + browser_name=result.get("browser_name", ""), + boss_username=result.get("boss_username", ""), + is_logged_in=result.get("is_logged_in", False), + ) + except Exception as db_err: + logger.error("任务 %s 写入数据库失败: %s", task_id, db_err) + else: logger.warning("未知消息类型: %s (from %s)", msg_type, worker_id) diff --git a/server/models.py b/server/models.py index df70180..d2b7340 100644 --- a/server/models.py +++ b/server/models.py @@ -1,19 +1,103 @@ # -*- coding: utf-8 -*- """ -Pydantic 数据模型 —— 用于 REST API 请求 / 响应以及内部状态。 +数据模型:SQLAlchemy ORM 表模型 + Pydantic 请求/响应模型。 """ from __future__ import annotations import time import uuid +from datetime import datetime from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field +from sqlalchemy import ( + Boolean, Column, DateTime, Integer, JSON, String, Text, + UniqueConstraint, func, +) +from sqlalchemy.orm import DeclarativeBase from common.protocol import TaskStatus, TaskType -# ────────────────────────── Worker ────────────────────────── +# ══════════════════════════════════════════════════════════════ +# SQLAlchemy ORM 模型 +# ══════════════════════════════════════════════════════════════ + +class Base(DeclarativeBase): + """SQLAlchemy 声明式基类。""" + pass + + +class BossAccount(Base): + """BOSS 账号登录状态表。""" + __tablename__ = "boss_account" + + id = Column(Integer, primary_key=True, autoincrement=True) + worker_id = Column(String(64), nullable=False, comment="Worker 标识") + browser_id = Column(String(128), nullable=False, default="", comment="比特浏览器窗口 ID") + browser_name = Column(String(128), default="", comment="比特浏览器窗口名称(环境名)") + boss_username = Column(String(128), default="", comment="BOSS 直聘用户名") + is_logged_in = Column(Boolean, default=False, comment="是否已登录") + checked_at = Column(DateTime, nullable=True, comment="最近一次检测时间") + created_at = Column(DateTime, default=func.now(), comment="创建时间") + updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间") + + __table_args__ = ( + UniqueConstraint("worker_id", "browser_id", name="uk_worker_browser"), + {"mysql_charset": "utf8mb4", "comment": "BOSS 账号登录状态"}, + ) + + def to_dict(self) -> dict: + return { + "id": self.id, + "worker_id": self.worker_id, + "browser_id": self.browser_id, + "browser_name": self.browser_name, + "boss_username": self.boss_username, + "is_logged_in": self.is_logged_in, + "checked_at": self.checked_at.isoformat() if self.checked_at else None, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class TaskLog(Base): + """任务执行记录表。""" + __tablename__ = "task_log" + + id = Column(Integer, primary_key=True, autoincrement=True) + task_id = Column(String(32), nullable=False, unique=True, comment="任务 ID") + task_type = Column(String(64), nullable=False, comment="任务类型") + worker_id = Column(String(64), default="", comment="执行的 Worker") + status = Column(String(32), default="", comment="最终状态") + params = Column(JSON, nullable=True, comment="任务参数") + result = Column(JSON, nullable=True, comment="任务结果") + error = Column(Text, nullable=True, comment="错误信息") + created_at = Column(DateTime, default=func.now(), comment="创建时间") + + __table_args__ = ( + {"mysql_charset": "utf8mb4", "comment": "任务执行记录"}, + ) + + def to_dict(self) -> dict: + return { + "id": self.id, + "task_id": self.task_id, + "task_type": self.task_type, + "worker_id": self.worker_id, + "status": self.status, + "params": self.params, + "result": self.result, + "error": self.error, + "created_at": self.created_at.isoformat() if self.created_at else None, + } + + +# ══════════════════════════════════════════════════════════════ +# Pydantic 请求 / 响应模型(API 用) +# ══════════════════════════════════════════════════════════════ + +# ─── Worker ─── class BrowserProfile(BaseModel): """比特浏览器窗口信息(Worker 上报)。""" @@ -42,14 +126,14 @@ class WorkerOut(BaseModel): current_task_id: Optional[str] = None -# ────────────────────────── Task ────────────────────────── +# ─── Task ─── class TaskCreate(BaseModel): """前端提交任务的请求体。""" task_type: TaskType - worker_id: Optional[str] = None # 手动指定机器 - account_name: Optional[str] = None # 按比特浏览器窗口名自动路由 - params: Dict[str, Any] = {} # 任务参数(如 job_title, max_greet 等) + worker_id: Optional[str] = None + account_name: Optional[str] = None + params: Dict[str, Any] = {} class TaskInfo(BaseModel): @@ -60,8 +144,8 @@ class TaskInfo(BaseModel): worker_id: Optional[str] = None account_name: Optional[str] = None params: Dict[str, Any] = {} - progress: Optional[str] = None # 最新进度描述 - result: Any = None # 最终结果 + progress: Optional[str] = None + result: Any = None error: Optional[str] = None created_at: float = Field(default_factory=time.time) updated_at: float = Field(default_factory=time.time) @@ -80,3 +164,17 @@ class TaskOut(BaseModel): error: Optional[str] = None created_at: float updated_at: float + + +# ─── 简化接口:前端添加环境名 ─── + +class CheckLoginRequest(BaseModel): + """前端提交检测登录请求。worker_id 可不传(走绑定关系)。""" + browser_name: str + worker_id: Optional[str] = None + + +class AccountBindRequest(BaseModel): + """前端添加账号时提交绑定:账号环境名 + 归属电脑。""" + browser_name: str + worker_id: str diff --git a/worker/tasks/check_login.py b/worker/tasks/check_login.py new file mode 100644 index 0000000..7425d20 --- /dev/null +++ b/worker/tasks/check_login.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +""" +检测 BOSS 直聘账号登录状态任务。 +流程:在比特浏览器分组中按名称查找环境 → 打开该浏览器 → 访问 BOSS 聊天页 → 获取 .user-name 文本。 +""" +from __future__ import annotations + +import asyncio +from typing import Any, Callable, Coroutine, Dict + +from common.protocol import TaskType +from worker.tasks.base import BaseTaskHandler +from worker.bit_browser import BitBrowserAPI +from worker.browser_control import connect_browser, human_delay + + +CHAT_INDEX_URL = "https://www.zhipin.com/web/chat/index" + + +class CheckLoginHandler(BaseTaskHandler): + """检测 BOSS 直聘账号是否已登录。""" + + task_type = TaskType.CHECK_LOGIN.value + + async def execute( + self, + task_id: str, + params: Dict[str, Any], + progress_cb: Callable[[str, str], Coroutine], + ) -> Any: + """ + params: + - account_name: str 比特浏览器环境名称(前端传入的 browser_name) + - account_id: str 比特浏览器窗口 ID(可选,优先级高于 name) + - bit_api_base: str 比特浏览器 API 地址(可选) + """ + account_name = params.get("account_name", "") + account_id = params.get("account_id", "") + bit_api_base = params.get("bit_api_base", "http://127.0.0.1:54345") + + await progress_cb(task_id, f"正在比特浏览器中查找环境: {account_name}...") + + result = await asyncio.get_event_loop().run_in_executor( + None, + self._run_sync, + account_name, account_id, bit_api_base, + ) + + status_text = "已登录" if result["is_logged_in"] else "未登录" + await progress_cb(task_id, f"检测完成: {result['browser_name']} → {status_text} ({result['boss_username']})") + return result + + def _run_sync( + self, + account_name: str, + account_id: str, + bit_api_base: str, + ) -> dict: + """同步执行(线程池中运行)。""" + bit_api = BitBrowserAPI(bit_api_base) + + # ── 1. 在比特浏览器分组中查找匹配的环境 ── + browser_id = account_id + browser_name = account_name + + if not browser_id and account_name: + self.logger.info("正在比特浏览器中查找环境: %s", account_name) + # 获取所有浏览器窗口,按名称匹配 + all_browsers = bit_api.list_browsers(page_size=200) + matched = None + + # 精确匹配 + for item in all_browsers: + if item.get("name", "").strip() == account_name.strip(): + matched = item + break + + # 未精确匹配到则模糊匹配 + if not matched: + for item in all_browsers: + name = item.get("name", "").strip().lower() + if account_name.strip().lower() in name: + matched = item + break + + if not matched: + raise RuntimeError( + f"在比特浏览器中未找到名为 '{account_name}' 的环境。" + f"当前共 {len(all_browsers)} 个环境," + f"名称列表: {[b.get('name', '') for b in all_browsers[:10]]}..." + ) + + browser_id = matched.get("id", "") + browser_name = matched.get("name", account_name) + self.logger.info( + "找到匹配环境: name=%s, id=%s, remark=%s", + browser_name, browser_id, matched.get("remark", ""), + ) + + if not browser_id: + raise RuntimeError("未指定 account_name 或 account_id,无法确定浏览器环境") + + # ── 2. 打开该浏览器 ── + cdp_addr, port, browser_id = bit_api.open_browser(browser_id=browser_id) + self.logger.info("已打开浏览器 %s (%s), CDP: %s", browser_name, browser_id, cdp_addr) + + # ── 3. 连接浏览器 ── + browser = connect_browser(port=port) + tab = browser.latest_tab + + # ── 4. 访问 BOSS 直聘聊天页 ── + tab.get(CHAT_INDEX_URL) + tab.wait.load_start() + human_delay(3.0, 5.0) + + # ── 5. 查找 .user-name 元素,判断是否已登录 ── + boss_username = "" + is_logged_in = False + + try: + user_name_ele = tab.ele("css:.user-name", timeout=10) + if user_name_ele and user_name_ele.text: + boss_username = user_name_ele.text.strip() + is_logged_in = bool(boss_username) + self.logger.info("环境 %s 已登录: %s", browser_name, boss_username) + else: + self.logger.info("环境 %s 未检测到 .user-name,账号未登录", browser_name) + except Exception as e: + self.logger.warning("环境 %s 查找 .user-name 失败: %s", browser_name, e) + + return { + "browser_id": browser_id, + "browser_name": browser_name, + "boss_username": boss_username, + "is_logged_in": is_logged_in, + } diff --git a/worker/tasks/registry.py b/worker/tasks/registry.py index a2a3273..dfa7ad4 100644 --- a/worker/tasks/registry.py +++ b/worker/tasks/registry.py @@ -38,7 +38,9 @@ def list_handlers() -> list[str]: def register_all_handlers() -> None: """注册所有内置任务处理器。在此函数中 import 并注册。""" from worker.tasks.boss_recruit import BossRecruitHandler + from worker.tasks.check_login import CheckLoginHandler register_handler(BossRecruitHandler) + register_handler(CheckLoginHandler) # 未来扩展:在此处添加新的 handler # from worker.tasks.xxx import XxxHandler # register_handler(XxxHandler)