platform-assistant/scripts/direv-publish/auto_audit.py
Sisyphus afac18e843 feat(F002): W26 + W29.x + W30 + W31 平台助手全部改动首次 commit
- W26: NestJS API + Next.js Web + Prisma + SQLite scaffold
- W26: Lead 模型 + Crawler + 询价发布助手 (线索池 + 一键发布)
- W29.x: MCP 集成 (Integration 模型 + connectMcp + publish_rfq 5 工具端到端跑通)
- W30: 3 字段补充 (notes / quotationType / quotationDemand)
- W31 P1: 自动填 enterpriseId / userId (Prisma companyUserId 字段 + get_app_identity tool + lead.service.ts publishArgs)
- W31: detail 详情页 + admin UI 集成管理 (OpenAppListView + AuditLogListView)

关联: ~/Documents/02-平台开发/产品开发/F002-W31-交接文档.md
GIT-AUDIT-REPORT-2026-06-24.md
2026-06-24 11:07:11 +08:00

447 lines
15 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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())