#!/usr/bin/env python3 """ 巨变平台 → direv.com.cn 询价自动发布 用法: python3 publish.py # headless 模式(需要已有登录态) python3 publish.py --headed # 首次使用或登录态过期时使用 依赖: pip3 install playwright && playwright install chromium """ import asyncio import os import sys from playwright.async_api import async_playwright # ============ 固定配置 ============ CONTACT_NAME = "郑钊" CONTACT_PHONE = "15532027557" AUTH_FILE = "/tmp/direv-auth.json" # ============ 待发布线索(每次修改这里)============ LEAD = { "title": "赵王新河治理工程(雄县段)二期施工二标段块石采购", "deadline": "2026-05-09", "delivery": "2026-05-16", "region": "河北省保定市雄县 赵王新河治理工程(雄县段)二期施工二标段项目施工现场", "category": "块石", "unit": "吨", "quantity": "500", } async def fill_form(page, lead): """自动填充 create_square 表单全部 15 个字段""" FILL_INPUT = """(d) => { var titles = document.querySelectorAll(".c_title"); for (var i=0; i 0: # 用 Playwright locator 点击 +添加物料(比 evaluate DOM 更可靠) await page.locator("text=添加物料").first.click() await page.wait_for_timeout(2000) await page.evaluate( """(d) => { var rows = document.querySelectorAll( ".el-table__body-wrapper tbody tr" ); var row = rows[d.i]; if (!row) return; var cells = row.querySelectorAll("td"); // cell[2]=商品名称, cell[4]=规格型号, cell[5]=单位, cell[6]=数量, cell[7]=询价备注 var map = {2: d.cat, 4: d.spec, 5: d.unit, 6: d.qty, 7: d.remark}; for (var ci in map) { var el = cells[ci].querySelector(".el-input__inner"); if (el) { // 确保值是字符串(Element UI 有时对数字类型敏感) var val = map[ci]; if (val !== null && val !== undefined) val = String(val); var nativeSetter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, "value" ).set; nativeSetter.call(el, val); // 触发多个事件确保 Vue/Element UI 响应 el.dispatchEvent(new Event("input", {bubbles:true})); el.dispatchEvent(new Event("change", {bubbles:true})); // 对数量字段额外触发 compositionend(Vue v-model 的 composition 管道) if (ci === "6") { el.dispatchEvent(new CompositionEvent("compositionstart", {bubbles:true})); el.dispatchEvent(new CompositionEvent("compositionend", {bubbles:true})); el.focus(); el.blur(); } } } }""", {"i": row_i, "cat": row_data["category"], "spec": row_data.get("spec", ""), "unit": row_data["unit"], "qty": str(row_data.get("quantity", "")).strip(), "remark": row_data.get("remark", "")}, ) # 采购类型 → 定量采购 await page.evaluate('document.querySelectorAll(".el-select__wrapper")[0].click()') await page.wait_for_timeout(500) await page.evaluate( '(function(){' 'var o=document.querySelectorAll(".el-select-dropdown__item");' 'for(var i=0;i { var btn = document.querySelector(".btm_btn"); if (!btn) return; for (var i=0; i { var errs = document.querySelectorAll('.el-message--error, .el-form-item__error'); if (errs.length > 0) { return Array.from(errs).map(e => e.textContent.trim()).join(' | '); } var invalid = document.querySelectorAll('.is-error .el-input__inner'); if (invalid.length > 0) { return '表单验证失败'; } return null; }""" ) if error_found: return False return True async def publish(lead, headless=True): """主流程""" async with async_playwright() as p: browser = await p.chromium.launch(headless=headless) ctx_options = {"viewport": {"width": 1440, "height": 900}} if os.path.exists(AUTH_FILE): ctx_options["storage_state"] = AUTH_FILE ctx = await browser.new_context(**ctx_options) page = await ctx.new_page() page.set_default_timeout(15000) # 导航到 direv 首页 await page.goto( "https://www.direv.com.cn/#/index", wait_until="domcontentloaded", ) await page.wait_for_timeout(5000) # 点击「发布询价」(优先 button 精确匹配 → 兜底模糊匹配) for el in await page.query_selector_all("button"): try: if ((await el.text_content()) or "").strip() == "发布询价": await el.click() break except Exception: pass else: for el in await page.query_selector_all("span, div, button"): try: if ((await el.text_content()) or "").strip() == "发布询价信息": await el.click() break except Exception: pass await page.wait_for_timeout(8000) # 等待表单加载 try: await page.wait_for_selector(".c_title", timeout=15000) except Exception: print(" ⚠️ 表单未加载,跳过...") await page.screenshot(path=f"/tmp/direv_err_{lead['title'][:10]}.png", full_page=True) await ctx.close() return False # 填表 print(f" 填表: {lead['title'][:30]}...") await fill_form(page, lead) # 提交并检查错误 print(" 提交中...") ok = await submit_form(page) if not ok: print(" ❌ 表单验证错误") await page.screenshot(path=f"/tmp/direv_err_{lead['title'][:10]}.png", full_page=True) await ctx.close() return False # 截图 path = f"/tmp/direv_{lead['title'][:10]}.png" await page.screenshot(path=path, full_page=True) # headed 模式:手动登录后保存登录态 if not headless and not os.path.exists(AUTH_FILE): await ctx.storage_state(path=AUTH_FILE) print(f" ✓ 登录态已保存 → {AUTH_FILE}") print(f" ✓ 发布完成 → {path}") await browser.close() return True # ============ CLI ============ if __name__ == "__main__": headless = "--headed" not in sys.argv if headless: if not os.path.exists(AUTH_FILE): print("❌ 登录态不存在,请先运行: python3 publish.py --headed") sys.exit(1) else: if not os.path.exists(AUTH_FILE): print("⚠️ 首次运行,请手动登录后关闭浏览器...") print(f"模式: {'headless' if headless else 'headed (可见浏览器)'}") ok = asyncio.run(publish(LEAD, headless=headless)) sys.exit(0 if ok else 1)