#!/usr/bin/env python3 """ direv 管理后台 - 自动审核(完全自动版) 登录检测改用页面元素判断,不需要人工按键确认 用法: python3 auto_audit.py # 审核所有待审核项 python3 auto_audit.py --dry-run # 仅列出,不操作 """ import asyncio import json import os import sys from datetime import datetime from playwright.async_api import async_playwright ADMIN_URL = "https://www.direv.com.cn/vue/#/" ADMIN_LOGIN = "https://www.direv.com.cn/vue/#/login" AUTH_FILE = "/tmp/direv-auth.json" def log(msg: str): print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True) async def try_auth_from_file(page): """尝试从 auth 文件恢复登录态(注入 localStorage),返回 True 如果成功""" if not os.path.exists(AUTH_FILE): return False try: with open(AUTH_FILE) as f: auth_data = json.load(f) # 先访问站点,然后注入 localStorage await page.goto(ADMIN_URL, wait_until="domcontentloaded") await page.wait_for_timeout(2000) for origin_entry in auth_data.get("origins", []): if origin_entry.get("origin") == "https://www.direv.com.cn": for item in origin_entry.get("localStorage", []): key = item["name"] val = item["value"] await page.evaluate(f"localStorage.setItem('{key}', '{val}')") log("✅ 已注入 localStorage(token + userInfo)") break # 重新导航以应用 localStorage await page.goto(ADMIN_URL, wait_until="domcontentloaded") await page.wait_for_timeout(4000) # 检查是否在管理后台 has_admin = await page.query_selector( ".el-menu, [class*=sidebar], [class*=nav-bar], " "span:has-text('数据统计'), span:has-text('询价管理')" ) if has_admin: log("✅ auth 文件登录成功!") return True log("⚠️ auth 文件注入后未能进入管理后台") return False except Exception as e: log(f"⚠️ auth 文件恢复失败: {e}") return False async def wait_for_login(page, timeout=120): """ 打开登录页,等用户登录。通过检测页面元素判断登录成功。 超时 120 秒。 """ await page.goto(ADMIN_LOGIN, wait_until="domcontentloaded") await page.wait_for_timeout(2000) log("🔑 已打开 direv 管理后台登录页,浏览器窗口可见,请登录...") # 轮询等待:URL 不再包含 login,或侧边栏出现 start = asyncio.get_event_loop().time() while True: await page.wait_for_timeout(3000) url = page.url has_login_url = "login" in url.lower() # 检查是否有登录表单 has_login_form = await page.query_selector( "input[placeholder*=手机], input[placeholder*=验证码], " "button:has-text('登录'), button:has-text('登 录'), " ".login-form, [class*=login-form]" ) # 检查是否出现了管理后台特有的元素 has_admin_elements = await page.query_selector( ".el-menu, [class*=sidebar], [class*=nav-bar], " "span:has-text('数据统计'), span:has-text('询价管理'), " "span:has-text('基础设置')" ) elapsed = asyncio.get_event_loop().time() - start if has_admin_elements: log("✅ 检测到管理后台界面 - 登录成功!") return True if not has_login_form and not has_login_url: log("✅ URL 已跳转离开登录页 - 登录成功!") await page.wait_for_timeout(3000) return True if elapsed > timeout: log("❌ 登录超时") return False # 进度提示 if int(elapsed) % 30 == 0 and elapsed > 0: log(f" ⏳ 等待登录中... ({int(elapsed)}秒)") async def navigate_to_inquiry_list(page): """导航到询价列表页""" # 确保在管理后台首页 await page.goto(ADMIN_URL, wait_until="domcontentloaded") await page.wait_for_timeout(3000) # 找侧边栏中的「询价管理」点击展开 clicked = False for el in await page.query_selector_all(".el-submenu__title, li.el-submenu > div, [class*=submenu] > div"): try: t = ((await el.text_content()) or "").strip() if "询价管理" in t: await el.click() clicked = True await page.wait_for_timeout(2500) log(" → 展开询价管理") break except: pass if not clicked: log("❌ 未找到「询价管理」菜单") return False # 点击「询价列表」— 通常在展开后的内联菜单中 await page.wait_for_timeout(1500) # Element UI 展开的子菜单项 selectors = [ ".el-menu--inline .el-menu-item", "li.is-active.el-menu-item", ".el-menu-item", "li[role=menuitem]", ] for sel in selectors: for el in await page.query_selector_all(sel): try: t = ((await el.text_content()) or "").strip() if "询价列表" in t: await el.click() await page.wait_for_timeout(4000) log(" → 进入询价列表") return True except: pass # 兜底:在所有元素中查找 for el in await page.query_selector_all("*"): try: t = ((await el.text_content()) or "").strip() if t == "询价列表": await el.click() await page.wait_for_timeout(4000) log(" → 进入询价列表") return True except: pass log("❌ 未找到「询价列表」") return False async def set_page_size(page, size=100): """调整分页器每页显示条数 页面结构(底部 pagination bar): 左侧:Total 169(共169条) 右侧:小下拉框 10/page → 可选 100/page 对应 Element UI 结构:.el-pagination > .el-pagination__sizes > .el-select """ await page.wait_for_timeout(2000) size_texts = [f"{size}条/页", f"{size} / 页", f"{size}/page", str(size)] try: size_selector = None # 策略1:Element UI 标准选择器 size_selector = await page.query_selector( ".el-pagination__sizes .el-select .el-select__wrapper" ) or await page.query_selector( ".el-pagination__sizes .el-select" ) or await page.query_selector( ".el-pagination .el-select" ) # 策略2:按文本内容找包含 "条/页" 或 "/page" 的 select/input if not size_selector: for el in await page.query_selector_all(".el-pagination *"): try: t = ((await el.text_content()) or "").strip() if any(kw in t for kw in ("条/页", "/page", "条/页")): # 往上找可点击的 select 容器 parent = await el.evaluate("""(el) => { let p = el; while (p) { if (p.classList.contains('el-select') || p.classList.contains('el-select__wrapper') || p.tagName === 'INPUT') return p; p = p.parentElement; } return el; }""") size_selector = el break except: pass if not size_selector: log(" ⚠️ 未找到每页条数选择器") return False # 点击打开下拉 await size_selector.click() await page.wait_for_timeout(1000) # 策略3:在下拉列表中找目标条数 target_found = False for el in await page.query_selector_all( ".el-select-dropdown__item, .el-select-dropdown li, " ".el-popper li, [class*=dropdown] li, .el-scrollbar__view li, " ".el-select-dropdown__wrap li, .el-select-dropdown__list li" ): t = ((await el.text_content()) or "").strip() for target in size_texts: if t == target or t.startswith(target): await el.click() target_found = True await page.wait_for_timeout(2000) log(f" → 已调整每页显示 {size} 条") return True if not target_found: log(f" ⚠️ 下拉中未找到 {size} 选项,保持默认") await page.keyboard.press("Escape") return target_found except Exception as e: log(f" ⚠️ 调整每页条数失败: {e}") return False async def get_table_data(page): """获取询价列表所有行数据""" await page.wait_for_timeout(3000) rows = await page.query_selector_all( ".el-table__body-wrapper tbody tr, .el-table__row, tbody tr" ) data = [] for i, row in enumerate(rows): cells = await row.query_selector_all("td, th") texts = [] for cell in cells: t = ((await cell.text_content()) or "").strip() texts.append(t) if texts: data.append({"index": i, "row": row, "cells": texts}) return data def find_targets(table_data): """筛选:发布人=用户6727740, 审核状态=待审核, 询价状态=进行中,按询价单号去重""" seen_orders = set() targets = [] for item in table_data: s = " ".join(item["cells"]) if "用户6727740" in s and "待审核" in s and "进行中" in s: # 提取询价单号去重 (X开头的编号) order_no = None for c in item["cells"]: if c.startswith("X") and len(c) > 10: order_no = c break if order_no and order_no in seen_orders: continue if order_no: seen_orders.add(order_no) targets.append(item) return targets async def audit_row(page, item, idx, total): """审核单条记录""" row = item["row"] cells = item["cells"] title = cells[4] if len(cells) > 4 else "?" log(f"\n[{idx+1}/{total}] {title[:50]}") # 找到操作列中「审核」红色文字链接 # 操作列的审核是红色文字链接,非标准 button audit_btn = None for el in await row.query_selector_all("td:last-child *"): try: if ((await el.text_content()) or "").strip() == "审核": audit_btn = el break except: pass # 兜底:在整行中找 if not audit_btn: for el in await row.query_selector_all("span, a, button, td *"): try: if ((await el.text_content()) or "").strip() == "审核": audit_btn = el break except: pass if not audit_btn: log(" ⚠️ 未找到审核链接") return False # 用 evaluate 强制点击(红色文字链接可能不响应普通 click) try: await audit_btn.evaluate("el => el.click()") except: await audit_btn.click(force=True) await page.wait_for_timeout(2500) # 弹窗标题「更换管理员」,选「通过」radio,点「提交」 dialog = await page.query_selector(".el-dialog__wrapper:not([style*='display: none']), .el-dialog:not([style*='display: none'])") if not dialog: # 找最后出现的对话框 dialogs = await page.query_selector_all(".el-dialog, .el-dialog__wrapper, [role=dialog]") dialog = dialogs[-1] if dialogs else None if not dialog: log(" ⚠️ 未找到审核弹窗") return False # 在弹窗中选「通过」(Element UI radio,通常 label 包裹 radio input) for el in await dialog.query_selector_all(".el-radio, .el-radio__label, label"): try: t = ((await el.text_content()) or "").strip() if t == "通过": await el.click() await page.wait_for_timeout(500) break except: pass # 点提交按钮 for btn in await dialog.query_selector_all("button, .el-button"): try: t = ((await btn.text_content()) or "").strip() if t in ("提交", "提 交"): await btn.click() log(" ✅ 审核通过") await page.wait_for_timeout(3500) return True except: pass log(" ⚠️ 未找到提交按钮") await page.screenshot(path="/tmp/audit_dialog_err.png") return False async def main(): dry_run = "--dry-run" in sys.argv async with async_playwright() as p: browser = await p.chromium.launch(headless=False) ctx = await browser.new_context(viewport={"width": 1440, "height": 900}) page = await ctx.new_page() page.set_default_timeout(15000) # 1. 登录:先尝试 auth 文件,失败再走手动登录 ok = await try_auth_from_file(page) if not ok: ok = await wait_for_login(page, timeout=180) if not ok: log("❌ 登录失败或超时") await browser.close() return # 2. 导航到询价列表 ok = await navigate_to_inquiry_list(page) if not ok: await browser.close() return # 2.5. 调整每页显示 100 条 await set_page_size(page, 100) # 3. 获取表格数据 table_data = await get_table_data(page) log(f"\n📊 询价列表共 {len(table_data)} 行\n") for item in table_data: short = " | ".join(c[:16] for c in item["cells"][:10]) log(f" [{item['index']}] {short[:130]}") # 4. 筛选 targets = find_targets(table_data) if not targets: log("\n✅ 无待审核项!") await browser.close() return log(f"\n🎯 找到 {len(targets)} 条待审核(发布人=用户6727740 + 待审核 + 进行中)") if dry_run: for t in targets: title = t["cells"][4] if len(t["cells"]) > 4 else "?" log(f" → {title[:50]}") log("\n⬆️ --dry-run 模式") await browser.close() return # 5. 逐条审核 success = 0 for i, item in enumerate(targets): try: ok = await audit_row(page, item, i, len(targets)) if ok: success += 1 except Exception as e: log(f" ❌ {e}") try: await page.screenshot(path=f"/tmp/audit_err_{i}.png") except: pass log(f"\n{'='*30}") log(f"✅ 审核通过: {success} | ❌ 失败: {len(targets) - success}") await browser.close() if __name__ == "__main__": asyncio.run(main())