from __future__ import annotations import os import re import time from typing import Optional, Callable from config_loader import get_config def _now_datestr() -> str: from datetime import datetime return datetime.now().strftime("%Y-%m-%d") def _safe_screenshot(page, save_path: str) -> None: try: page.screenshot(path=save_path, full_page=True) except Exception: pass class Step1FeastCoding: """ Step 1: 访问 feast.yidaima.cn,登录并点击 FeastCoding 按钮 参考: docs/step1.md """ def __init__(self, chrome_path: str, username: str, password: str, log_callback: Optional[Callable[[str], None]] = None): self.chrome_path = chrome_path self.username = username self.password = password self.url = "https://feast.yidaima.cn/login" # self.url = "http://localhost/login" self.project_name = "【A173】基于Springboot + vue3实现的学生交流互助平台" self.feast_button = "FeastCoding" self.browser = None self.page = None self.log_callback = log_callback def _log(self, message: str): """输出日志,支持回调到 GUI""" print(message) if self.log_callback: self.log_callback(message) def run(self) -> bool: """执行 Step 1 流程,成功返回 True""" from playwright.sync_api import sync_playwright save_dir = os.path.join(".", "data", "screenshots", _now_datestr()) os.makedirs(save_dir, exist_ok=True) err_path = os.path.join(save_dir, "step1_error.png") try: with sync_playwright() as p: self.browser = p.chromium.launch(headless=False, executable_path=self.chrome_path) context = self.browser.new_context(viewport={"width": 1280, "height": 800}) self.page = context.new_page() # 1) 登录页 self._log("[Step1] 正在打开登录页...") self.page.goto(self.url, wait_until="networkidle") self.page.wait_for_timeout(2000) # 等待输入框出现 self.page.wait_for_selector("input[type='text'], input[name*='user' i], input[placeholder*='用' i]", timeout=15000) # 查找用户名和密码输入框 user_candidates = [ self.page.locator("input[type='text']").first, self.page.locator("input[name*='user' i]").first, self.page.locator("input[name*='username' i]").first, self.page.locator("input[placeholder*='用' i]").first, self.page.get_by_placeholder(re.compile(r"用户名|账号|登录名", re.I)).first, ] pass_candidates = [ self.page.locator("input[type='password']").first, self.page.locator("input[name*='pass' i]").first, self.page.locator("input[placeholder*='密' i]").first, self.page.get_by_placeholder(re.compile(r"密码", re.I)).first, ] user_loc = next((c for c in user_candidates if c.count() > 0 and c.is_visible()), None) pass_loc = next((c for c in pass_candidates if c.count() > 0 and c.is_visible()), None) if user_loc is None or pass_loc is None: raise RuntimeError("未能定位到用户名/密码输入框。") # 输入用户名密码 self._log("[Step1] 正在输入用户名密码...") user_loc.click(timeout=5000, force=True) user_loc.fill(self.username) pass_loc.click(timeout=5000, force=True) pass_loc.fill(self.password) # 登录按钮 login_btn = self.page.get_by_role("button", name=re.compile(r"(登录|Log in|Sign in|立即登录)", re.I)).first try: if login_btn.count() > 0: login_btn.click(timeout=10000, force=True) else: self.page.locator("button[type='submit']").first.click(timeout=10000, force=True) except Exception: self.page.keyboard.press("Enter") # 等待菜单 self._log("[Step1] 等待登录完成...") self.page.wait_for_selector("text=工作室运营", timeout=30000) time.sleep(1) # 2) 进入源码管理 self._log("[Step1] 进入源码管理...") self.page.get_by_text("工作室运营", exact=False).first.click(timeout=10000, force=True) time.sleep(0.8) self.page.get_by_text("源码管理", exact=False).first.click(timeout=10000, force=True) time.sleep(1) # 3) 搜索项目 self._log("[Step1] 搜索项目...") self.page.wait_for_timeout(1500) search_input_candidates = [ self.page.locator("input[type='search']").first, self.page.locator("input[placeholder*='搜索' i]").first, self.page.get_by_placeholder("搜索").first, self.page.locator(".el-input__inner").filter(has_text=re.compile(r"搜索", re.I)).first, ] search_input = next((c for c in search_input_candidates if c.count() > 0 and c.is_visible()), None) if search_input is None: all_inputs = self.page.locator("input[type='text']").all() for inp in all_inputs: if inp.is_visible(): search_input = inp break if search_input is None: raise RuntimeError("未能定位到搜索输入框。") search_input.click(timeout=5000, force=True) search_input.fill(self.project_name) search_input.press("Enter") # 从 project_name 提取项目编号(前6个字符) project_code = self.project_name[:6] if len(self.project_name) >= 6 else self.project_name # 等待列表渲染 self.page.wait_for_selector(f"text={project_code}", timeout=20000) self.page.wait_for_timeout(1000) # 查找项目行 row = self.page.locator("tr", has_text=self.project_name).first if row.count() == 0: row = self.page.locator("tr", has_text=project_code).first # 点击"查看"按钮 view_btn = row.get_by_role("button", name=re.compile(r"(查看|View)", re.I)).first if view_btn.count() == 0 or not view_btn.is_visible(): raise RuntimeError("未能定位到'查看'按钮。") view_btn.click(timeout=10000) # 4) 滚动到底并点击 FeastCoding self._log("[Step1] 查找 FeastCoding 按钮...") self.page.wait_for_timeout(1500) feast_loc = self.page.get_by_text(self.feast_button, exact=False).first feast_loc.wait_for(state="visible", timeout=15000) self._log("[Step1] 已找到 FeastCoding 按钮") feast_loc.scroll_into_view_if_needed(timeout=10000) self.page.wait_for_timeout(800) self._log("[Step1] 点击 FeastCoding 按钮...") feast_loc.click(timeout=10000) self.page.wait_for_timeout(3000) self._log("[Step1] 已完成,内容已复制到剪贴板") return True except Exception as e: if self.page is not None: _safe_screenshot(self.page, err_path) raise e finally: if self.browser is not None: try: self.browser.close() except Exception: pass def main(): """单独运行 Step1""" config = get_config() chrome_path = config.chrome_path step1_config = config.step1_config username = step1_config.get("username", "wangpeng") password = step1_config.get("password", "Feastcoding@123") step1 = Step1FeastCoding(chrome_path, username, password) try: step1.run() print("[INFO] Step1 执行成功!") except Exception as e: print(f"[ERROR] Step1 执行失败: {e}") if __name__ == "__main__": main()