浏览器自动化
确定性优先的浏览器自动化:规则写进代码,LLM 只做兜底
让 Agent 自己点页面听起来很酷,直到它在「批量上架」的按钮上多点了一次。上架自动化和爬虫不一样:爬虫跑错了最多拿到脏数据,上架跑错了是直接把商品挂到店铺上。starmerx_listing 这个项目从头到尾只贯彻一条原则——能被代码表达的规则,绝不交给模型;LLM 只在规则层失败之后才被允许出场。
「失控」是怎么发生的#
先看这个项目要解决的问题:把「产品池 → 加入上架计划 → 图片/价格校验 → 批量上架」这条流程自动化,MVP 范围收窄到圣诞花环单品类。它的反面教材非常典型:
- 把「判断这张图是不是白底主图」交给视觉模型,模型今天说是、明天说不是;
- 把「这个价格算出来是赚是亏」交给模型心算,它给你一个看起来很合理的数字;
- 把「点哪一个按钮」交给模型理解页面,页面改版后它开始点错地方。
这三种失败有一个共同特征:它们都不会报错。程序照常跑完,日志里一片绿,只有店铺后台知道出了事。所以这个项目的对抗手段不是「让模型更聪明」,而是把流程切成两类:规则层(纯代码、可复现、会抛错)和兜底层(模型,只在规则层失败时介入)。
技术栈与四阶段编排#
技术栈就三样:Python 3.13、Playwright 1.62、以及作为兜底的阿里云通义千问 Qwen。Playwright 负责确定性 DOM 操作,Qwen 是预留位——只有在规则层失败时才被调用。
入口是 run.sh,src/main.py 是四阶段编排:
starmerx_listing/
├── run.sh # 一键启动脚本
├── config.json # 全部配置(固定表单值/类目/校验规则/安全开关)
├── src/
│ ├── main.py # 四阶段编排入口
│ ├── login.py # 钉钉扫码 + storageState 持久化
│ ├── phase1.py # 产品池 → 加入上架计划
│ ├── phase2.py # 图片/价格校验 + 批量上架
│ ├── price_calc.py # 利润试算引擎(有界二分,防死循环)
│ ├── dom.py # DOM 操作辅助
│ └── config.py # 配置/日志
├── scripts/
│ └── probe_product_pool.py # 独立勘察工具
└── runtime/ # storage_state.json、勘察截图、日志
把配置单独放进 config.json 是个不太起眼但很重要的决定:固定表单值、类目、校验规则、安全开关全在里面,于是「改规则」不需要改代码,也就不会在改规则的时候顺手改坏流程。
| 配置项 | 值 |
|---|---|
| 平台 / 站点 / 店铺 / 品牌 | Tik Tok / tiktok.com / Garvee Mate / 无品牌 |
| 系统类目(产品池) | 家居厨具 → 节日饰品 → 圣诞花环花带 → 圣诞花带装饰 |
| 平台类目(上架表单) | Home Supplies → Festive → Wreaths, Garlands & Swags |
| SPU 图片 | 8 张(含白底主图),SKU 色标一一对应 |
| 活动营销费占比 | 50% |
| 个人利润目标 | 4%–6%(通过调「公司利润率」实现) |
| 公司利润率边界 | 0.30–0.60(待确认) |
| GTIN / 生成方式 | 豁免 / 按 SPU 维度生成多上架计划 |
一个版本差异值得说明:早期的一份项目宣传页把这条流程写成「NestJS + Playwright 的五步流水线」,而仓库代码与 README 的现状是 Python 3.13 + Playwright 的四阶段编排(
src/main.py)。本文以仓库为准——这也是本文所有参数与文件名的来源。
登录态:钉钉扫码 + storageState 的两天寿命#
登录用的是钉钉扫码,登录结果通过 Playwright 的 storageState 持久化到 runtime/storage_state.json。命令行里第一次要显式跑一次:
./run.sh login # 弹出浏览器,钉钉扫码(首次/过期)
这里有一个必须写进设计里的现实:这份登录态大约 2 天就会过期。它不是「配好就一劳永逸」的那类凭据。过期之后的处理方式决定了这套系统是「能长期跑」还是「跑三天就要人管」:
- 任务开始前检查
storage_state.json是否还可用,不可用就立刻停下并提示重扫,而不是带着失效的登录态把后面每一步都跑出奇怪的错误; - 把「过期提醒」当成常驻功能——README 里也写了
cron每日定时 + storageState 过期自动提醒重扫; - 不要试图绕过扫码去硬编码凭据,这会把一个「两天一次的人工动作」变成「一个迟早会炸的隐式依赖」。
勘察优先:先确认选择器,再谈执行#
项目里有一条硬性流程:任何新页面,先跑 probe 确认选择器,确认后才进入执行模式。
./run.sh probe-phase1 # 勘察产品池页
./run.sh probe-phase2 # 勘察上架计划页
scripts/probe_product_pool.py 是一个独立的勘察工具,只观察、不修改数据。这个「先勘察」的动作解决的是一个很典型的悖论:写自动化最难的部分不是点击,而是在你还没看清页面结构之前,就得先写选择器。没有勘察步骤,你就只能一边跑真流程一边调选择器——而每一次失败的尝试,都可能已经在页面上留下痕迹。
勘察的产物留在 runtime/ 下(截图与日志),它同时充当了「这个选择器当时长什么样」的证据。页面改版导致自动化失效时,翻一眼旧截图就能确认是页面变了,还是代码写错了。
利润试算:用有界二分防死循环#
整条流程里最需要「确定性」的地方是利润试算。业务约束是这样的:个人利润目标定在 4%–6%,而达成方式是反向调整「公司利润率」——因为个人利润是公司利润率的函数。可调边界暂定在 0.30–0.60。
这就是一个典型的单调函数求根问题:公司利润率越高,个人利润越低(或反之),中间存在一个让个人利润落进 4%–6% 区间的取值。用二分法求解最自然,但二分法有一个必须提前掐死的风险:如果目标区间在当前边界内根本不可达,朴素实现会永远迭代下去。
src/price_calc.py 的约束是:最多 20 次迭代,未命中即报「需人工介入」,绝不无限循环。这条规则的意义超出这一个函数——它代表了一种态度:
| 失败情形 | 坏的做法 | 这个项目里的做法 |
|---|---|---|
| 目标区间不可达 | 一直试,直到超时 | 20 次迭代后停止,明确报「需人工介入」 |
| 公式是占位模型 | 假装结果可信 | 在 README 里写明 K=0.24 是占位模型,需替换 |
| 边界值不确定 | 按猜测值上线 | 列为 P1 待确认事项,上线前必须核对 |
第三条尤其值得学。README 里明明白白写着:如果利润率边界错误,利润试算可能永远无法命中区间而死循环——所以需要人在真实的「利润测算」弹窗里确认可调范围。把「我不知道」写进文档,比让程序假装知道要负责任得多。
默认干运行#
所有阶段默认是干运行:只检查、只输出清单,不真正上架。要真执行,必须显式加参数:
# 跑通单品类(默认干运行:只检查不上架)
./run.sh phase1 --max-sku 1 # 选 1 个 SKU 加入上架计划
./run.sh phase2 # 图片/价格校验(干运行)
# 正式执行(确认无误后)
./run.sh phase1 --no-dry-run --max-sku 5
./run.sh phase2 --no-dry-run
注意 --max-sku 这个参数的存在本身也是一种安全设计:它让「先小批量验证」成为一个不需要额外开发就能做到的动作。默认干运行 + 限量执行,两者叠加之后的实际效果是——从「写完代码」到「真实上架」之间,隔着两次显式的确认。这两次确认谁都不能省,包括自己。
每一步都用 DOM 状态验收#
自动化脚本最常见的坏习惯是「点完就走」:click() 之后不检查任何东西,假定它成功了。这个项目的要求是:每次点击后都用 DOM 状态确认,失败即中止——具体确认的对象是输入框的 value、复选框的 checked 这类状态量,而不是「页面看起来变了」。
这条规则把「静默失败」转成了「显式报错」。在批量操作里,静默失败是最贵的:第 3 个 SKU 没勾上,程序继续往下跑,最后你得到的是 100 个商品里混着几个配置错误的——而这些错误要花比跑一遍自动化多得多的时间去清理。
LLM 被放在哪个位置#
把前面的规则串起来,LLM 的位置就非常清楚了:
| 环节 | 执行者 | 理由 |
|---|---|---|
| 选择器定位与点击 | Playwright(确定性 DOM) | 可复现,失败会抛错 |
| 利润试算 | 纯代码 + 有界二分 | 数字必须可复现,不能「感觉差不多」 |
| 图片/价格校验 | 规则优先,Qwen 兜底 | 规则能判的就别问模型 |
| 登录 | 钉钉扫码 + storageState | 凭据交给平台,不自己发明 |
| 视觉/语义兜底 | 通义千问 Qwen(预留) | 只在规则层失败时介入 |
这张表可以概括成一句话:把模型当作「规则失败之后的下一个人」,而不是「第一步就上场的那个人」。它的输出不需要稳定,因为它本来就不是主路径;主路径必须稳定,所以主路径里不能有它。
没有做完的部分,写在 README 里#
待确认事项
这个仓库有一节叫「待确认事项(上线前必须解决)」,里面列了两条:一是公司利润率的可调边界(暂定 0.30–0.60);二是 src/price_calc.py 里的个人利润公式是占位模型(用 K=0.24 做缩放),需要勘察到真实「利润测算」弹窗的逻辑之后替换成精确公式。
把占位模型写在最显眼的位置,是一种工程上的诚实。任何接手这个仓库的人(包括三个月后的自己)都会知道:利润数字目前只能当作流程通路的证据,不能当作决策依据。相比之下,「假装公式是对的,等出问题再说」才是真正的技术债。
部署与服务化#
README 给的部署建议是阿里云轻量 2C2G 的 Ubuntu(香港或新加坡节点,需要先测试 op.starmerx.com 的可达性),用 cron 做每日定时,并配合 storageState 的过期提醒。这个规模的选择很务实:流程本身不重,重的是浏览器——而 Playwright 在 2G 内存上跑单实例是够的,前提是别并发开一堆页面。
需要说明的是,完整的部署文档(docs/DEPLOYMENT.md)在仓库里标注为「待编写」。所以这一节只到「推荐什么机器、怎么定时」为止,再往下写就是编造了。
小结#
这个项目的所有设计选择,都可以用同一把尺子量:这一步出错的时候,我会不会知道?干运行让你知道、DOM 验收让你知道、有界二分让你知道、勘察截图让你知道、把占位模型写进 README 也让你知道。
反过来说,让 LLM 自由发挥之所以在上架场景里危险,不是因为模型不够聪明,而是因为它的失败方式是不报错的失败。确定性的代码不是「低级方案」,恰恰相反:在一个会真金白银出错的场景里,可复现、会抛错、有边界,才是最值钱的三个属性。