凡科商城(webportal.top)产品管理自动化
把「登录 → 选品 → 传图 → 填字段 → 写详情 → 保存 → 校验」整条链路脚本化。
适用范围
i.mall.vip.webportal.top(顶层管理框架)+ {aacct}-{siteId}.mall.vip.webportal.top(商城子域)+ mall-upload.webportal.top(上传)。
已实测站点:古玩商城(aluo01 / siteId=4 / 95 件商品,古玩为主含少量特产酒类)。
快速开始
cd <SKILL_DIR>
# 0) 一次性:写 credentials.json(主账号/成员账号/密码/siteId/aacct)
# 1) 登录(会弹浏览器窗口,人工拖一下滑块即可)
node scripts/login.js
# 2) 看有哪些产品
node scripts/list_products.js
node scripts/list_products.js --search 古玉
# 3) 传图到素材库(返回 ABUI id)
node scripts/upload_image.js ~/pics/main.jpg ~/pics/detail1.jpg
# 4) 把图绑成主图 + 填字段 + 写详情 + 保存 + 回读校验
node scripts/bind_images.js --id 113 \
--abui ABUIxxxx,ABUIyyyy \
--sell "和田玉材质,镂空雕,寓意以小博大" \
--price 38888 --market 300000 --stock 1 \
--detail "<p>详情HTML</p>"
目录结构
webportal-product-admin/
├── SKILL.md
├── README.md
├── credentials.example.json
├── scripts/
│ ├── login.js# 登录 → out/state.json(有头,人工过滑块)
│ ├── list_all.js # ★ 翻页拉全量产品(务必先跑,见坑 8)
│ ├── list_products.js # 列出/搜索首屏产品
│ ├── upload_image.js# 上传素材库 → out/uploaded.json(--append 支持分批)
│ ├── bind_images.js # 单件:绑图+填字段+写详情+保存+回读校验
│ ├── build_detail.js # 拼单件详情页 HTML(示例:产品113)
│ ├── build_detail_106.js # 食品/酒类专用详情结构(参数表照抄标签)
│ ├── fix_106_images.js # 定向删除指定图片(如误加的他商品测试图)
│ ├── verify_frontend.js # 前台验收(商城停业时不可用)
│ │
│ │── 以下为批量流水线(每件 1 主图 + 3 详情图 + 四段式详情页)
│ ├── collect_batch.js # 采集商品信息 + 实拍图(--from/--to/--skip)
│ ├── fetch_all_imgs.js # 下载全部实拍图到本地
│ ├── derive_v3.py # 派生:统一方形裁切(近方形器物更紧凑)
│ ├── derive_v4.py # ★ 派生:按宽高比分流,细高器物不裁切
│ ├── build_batch_details.js # 批量生成详情 HTML(品类模板引擎,10 品类)
│ ├── batch_apply.js# 批量落库:绑图+写详情+保存+校验
│ ├── final_check.js # 独立复核(不依赖执行日志,重新读库)
│ ├── run_batch.js # 分组顺序执行器(--dir 切换批次)
│ └── check_details.js # 本地校验详情 HTML(段数/图数/CDN)
├── references/
│ ├── protocol.md # 逆向出的完整接口协议
│ ├── fields.md # 产品编辑页四个 tab 的字段字典
│ ├── pitfalls.md # ★ 踩坑清单 19 条(先看这个,能省 80% 调试时间)
│ ├── batch_image_pipeline.md # 零积分批量配图方案
│ └── batch_notes.md # 派生方案 v1→v4 迭代记录与失败教训
└── out/ # 运行时产物(state.json / 各类 json)
一、十个必须知道的坑(全部实测踩过)
> 做任何"全站/批量"任务前,先跑 node scripts/list_all.js确认真实产品总数——
> 列表接口单页只返 50 条,见坑 9。
1. 登录有滑块风控 → 无头模式必被拦
点登录后出现「请完成下方验证 向右滑动填充拼图」。必须 headless:false 弹真实窗口人工过一次。
login.js 已默认有头,会自动填好三个框,用户只需拖滑块 + 点登录。
2. getAccessKey 必须在产品编辑页 frame 内发
在顶层管理框架(i.mall.vip.webportal.top)发同一个 URL,会返回 {"rt":-20,"msg":"当前权限不足"}。
在编辑页 frame({sub}.mall.vip.webportal.top/cn/manage_v2/productEditV2.jsp)内发则 200 正常。
同源不等于同上下文 —— 这是最容易误判的一点。
3. 上传的 accessKey 必须传原始 base64 字符串
fd.append('accessKey', akStr); // ✅ 原始 base64 串
fd.append('accessKey', JSON.stringify(AK)); // ❌ 返回 code:300「系统错误」
akStr 是接口返回的 urlsafe-base64 原串;AK 是它解码后的对象。两者不能混用。
4. 上传的文件字段名固定是 ctrl
用 file 会返回「上传的文件为空」。端点是:
POST https://mall-upload.webportal.top/v/21/advance?cmd=mall-fileUpload&token=<AK.token>
必须用浏览器原生 FormData。Node 侧手工拼 multipart 反而是这个环境唯一可行方式(见 references/protocol.md)。
5. _TOKEN 在 <meta id="_TOKEN">,不在 cookie
const tok = document.getElementById('_TOKEN').getAttribute('value');
从 state.json 的 cookies 里找不到 _TOKEN(那是旧版本文档的过时说法)。
jQuery ajaxSetup 会自动给同源 XHR 附加它,所以页面自身请求不用管;手动 fetch 才要自己拼 &_TOKEN=。
6. 受控组件必须用原生 setter 写值
const d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value');
d.set.call(el, v);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
直接 el.value = v 不会触发 v-model,保存时字段为空。
> 另外:page.$eval(sel, fn, arg) 里 el 是第一个参数,不是 this。
> 写成 new Function('v','...this...') 会报 Illegal invocation。
7. 素材库弹窗选中必须「受信点击」
选图项 class 是 .fa-basic-upload-file-view-list-item--content。
- elementHandle.click({force:true}) ✅ 会触发 Vue @click,计数器「已选择(N/48)」会变
- page.evaluate(() => el.click()) ❌ 合成事件不触发
8. 详情介绍写完必须多派发几个事件,否则静默丢失
只 dispatchEvent(new Event('input')) 会导致保存按钮仍 disabled,详情根本没落库。
for (const t of ['input','change','keyup','blur','paste']) d.body.dispatchEvent(new w.Event(t, {bubbles:true}));
写完务必回读校验:textLen 和 imgCount 都要 > 0。
9.列表接口单页只返 50 条,翻页参数是全小写 pageno
最容易造成「以为做完了其实漏一半」的坑。
- 只解析第一页 → 95 个商品只做了 50 个
- 翻页写成 pageNo / currentPage → 被静默忽略,每页都返前 50 条
- 终止条件:某页返回数< pageSize 才停
u.searchParams.set('pageSize', '50');
u.searchParams.set('pageno', String(pn)); // 全小写!
已封装 list_all.js。另外 collect_batch.js 已改为直接按 ID 遍历编辑页,
不再依赖列表数据筛 ID(ID 较小的商品全在第 2 页之外)。
10. 素材库图多了以后,按 ABUI 找不到图 → 用文件名搜索定位
素材库网格带分页,新传的图不在当前渲染页。用它自带的「搜索当前文件夹」框
(上传时按 <id>_main.jpg 命名,天然唯一):
const box = await page.$('input[placeholder*="搜索当前文件夹"]');
await box.click({ force: true });
await box.fill('');
await box.type(`${id}_main`, { delay: 90 }); // 必须真实键入
await box.press('Enter');
⚠️ 搜索框用原生 setter 写值不会触发筛选(可见项仍是全量),
必须用 Playwright 的 type()。
二、关键接口速查
| 用途 | 方法 | 端点 |
|---|---|---|
| 登录页 | GET | https://adm.webportal.top/index.jsp?url=<目标URL> |
| 取上传凭证 | GET | /api/manage/fileupload/getAccessKey?siteId={SITE}&_lcid=2052&_TOKEN={T} |
| 分块规划(大文件) | POST | https://{AK.url}/v/{AK.app}/advance/info?cmd=..&token=.. |
| 上传文件 | POST | https://{AK.url}/v/{AK.app}/advance?cmd=mall-fileUpload&token={AK.token} |
| 产品列表 | GET | /cn/api/manage/product/getFilterProduct?isAjax=true&lid=1&pageno=1&pageSize=50 |
| 保存产品 | POST | /cn/api/manage/product/setProductV2?cmd=setWafCk_set&id={ID}&src=null |
| 查重名 | POST | /cn/api/manage/product/isProductExisted (id, name) |
完整请求/响应结构见 references/protocol.md。
三、产品编辑页结构(四个 tab)
| Tab | 内容 |
|---|---|
| 基本信息 | 产品名称、卖点描述、产品图片(多图槽)、主图视频、产品分类、产品单位、产品参数(编号/类型/规格/材质/颜色/品牌/型号)、库存量、市场价、价格 |
| 产品规格 | SKU 规格项与规格值(多规格模式才需要) |
| 详情介绍 | 富文本编辑器(ueditor iframe baidu_editor_N)、添加标签 |
| 其它设置 | 配送设置(需要配送/快递/到店自提/同城配送)、上架设置(立即/暂不上架/定时)、交易设置(限购)、百度优化(SEO) |
产品参数是下拉选择,不是自由输入 —— 选项来自「管理参数」里预定义的参数值。写这些字段要走下拉交互,不能直接设 input.value。
单规格商品(当前古玩商城全部是单规格)只需填基本信息,价格/库存直接是普通 input。
四、批量处理建议
list_products.js --raw导出全量 → 改字段 → 回灌- 大批量传图:先
upload_image.js一次传多张拿 ABUI,再bind_images.js --abui a,b,c批量绑 - 改价/改库存批量:直接调
setProductV2(POST form-urlencoded)比走 UI 快,但要自己保证productInfoJSON 字段完整 —— 建议优先用 UI 脚本,UI 路径已验证且会自动带齐所有隐藏字段
四之二、批量配图(零积分方案)
批量给一批商品配「1 主图 + 3 详情图」+ 四段式详情页,不消耗 AI 生图积分:
node scripts/collect_batch.js 10 --skip 106 # 1. 采集商品信息
node scripts/fetch_all_imgs.js 10 --skip 106 # 2. 下载全部实拍图
python scripts/derive_v3.py products/batch products/batch/derived # 3. 派生图
node scripts/build_batch_details.js # 4. 生成四段式详情 HTML
node scripts/batch_apply.js # 5. 批量绑图+写详情+保存+回读校验
node scripts/batch_apply.js --only 113 # 只做某件
node scripts/batch_apply.js --dry # 预览不保存
思路:不造新图,而是把已有实拍图用对位置 —— 细节特写展示工艺、整体图展示器型、
主图白底圆角卡用于搜索展示。
已知局限:纯图像处理做不了语义级背景分离,这批实拍图是店铺实景(背景有货架、
其他商品、展台边),只能压暗四角、无法生成真白底。
若需干净白底主图,只能对主图单独用图生图净化(9 张约 45-90 积分),详情图仍用实拍派生。
详见 references/batch_image_pipeline.md。
五、环境要求
| 项 | 要求 |
|---|---|
| Node | ≥ 18(实测 22.22) |
| 依赖 | playwright-core(NODE_PATH 指向其 node_modules) |
| 浏览器 | 本地 Chrome/Chromium,WM_CHROME 指定;否则自动探测 ~/.agent-browser/browsers/chrome-* |
| 凭据 | credentials.json 或 env WM_USER / WM_PASS / WM_MEMBER |
所有脚本启动时都会清掉 proxy 环境变量 —— 直连上传端点必须如此,否则返回 404。
六、验证纪律
任何写操作后必须回读校验(脚本已内置):
1. 抓 setProductV2 请求体,确认 productInfo 里 pic 数组含真实 CDN 路径(pic:[] 为空 = 没绑上)
2. 重新打开编辑页,数 .fa-upload-list-item 数量、回读卖点/价格字段
3. 报告时说"已落库"必须有第 2 步证据,不能只凭保存接口返回 200
安全说明
本技能文档/脚本中出现以下系统级调用模式,均为自动化场景的必要用途,边界如下:
child_process用于调用本机命令行工具(如 CLI 自动化),命令与参数来源于技能文档约定,不拼接外部不可信输入eval()仅用于说明/示例或受控的表达式求值场景,不处理外部不可信输入
本技能不读取、不上传任何用户敏感数据;网络请求仅发往文档中明确列出的域名。