211 lines
8.4 KiB
Python
211 lines
8.4 KiB
Python
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()
|