凡科/华可商城(webportal.top)产品编辑自动化
把「本地 AI 图 → 上传素材库 → 绑定产品 → 填卖点/编码/详情 → 保存校验」整条链路脚本化、无头自动化。
适用后台特征:
- 顶层管理框架 i.mall.vip.webportal.top(如 siteId=6、__aacct=aluo01)
- 商城 *.mall.vip.webportal.top,素材/上传走 mall-upload.webportal.top
- 产品编辑页在 productEditV2 子 iframe 内
四大坑(趟平了,照做即可)
-
产品「产品图片」隐藏
input[type=file]直传是死路
直传只把图填进组件的本地picPathList(base64 缩略图),保存时pic:[]为空,不回写 CDN url。
→ 必须走「添加图片 → 素材库(我的文件)选图」弹窗,选中的素材带真实 CDN url 才会写进pic。 -
素材弹窗选中必须用 Playwright 受信点击
.fa-basic-upload-file-view-list-item--content
- 合成el.click()(在page.evaluate里点)不触发 Vue 的@click处理器,计数器已选择(N/50)不变。
- class 必须精确是--content(少--content前缀会点错元素)。
- 受信点击:const h = await page.evaluateHandle(findContentByAbui); await h.asElement().click({force:true})。 -
ueditor 详情:要点「精确文本=详情介绍」的 tab 才会挂载编辑器
[class*=tab]首个匹配「详情介绍」的是整段容器(innerText 是「基本信息 产品规格 详情介绍 其它设置」),
点它不切 tab。要找innerText.trim()==='详情介绍'。挂载后写baidu_editor_0iframe 的 body。
window.UE.getEditor在此环境是 undefined(实例键是ueditorInstant0),不能用,直接写 iframe body 最稳。 -
产品编码/卖点是 ant 受控组件:直接
input.value=x不触发 v-model
必须用原生 setter 触发:Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(el,x)
再 dispatchinput/change。否则保存时该字段为空(产品 11 曾因此编码存空)。
一、FaiUpload advance 分块上传协议(把图传进素材库)
图片进素材库有两步:① 取 accessKey;② 分块上传。
1.1 取 accessKey(GET,不是 POST)
GET https://i.mall.vip.webportal.top/api/manage/fileupload/getAccessKey?siteId=6&_lcid=2052&_TOKEN=<TOKEN>
Cookie: <登录态>
返回 JSON,取 data.accessKey(或顶层 accessKey),是 urlsafe-base64:
ak = base64urlsafe_decode(accessKey) → {token, aid:34133765, app:21, cmd:"mall-fileUpload", visitType:"v", url:"mall-upload.webportal.top"}
1.2 分块上传(手工 multipart,禁用 fetch+FormData)
> 坑:Node 全局 fetch + FormData 拼 multipart 会让分块 POST 返回 404;必须用 https.request 手工拼。
POST https://<url>/v/<app>/advance?cmd=mall-fileUpload&token=<token>
multipart body:
fields: fileMd5, totalSize, index:"0", chunkSize, totalChunks:"1", complete:"true", fileName, accessKey
file field: "ctrl" (binary chunk, Content-Type: application/octet-stream)
单文件(<2MB)可一次传: totalChunks:"1", index:"0", complete:"true"。分块大小 splitSize=2097152(取自 /advance/info 端点)。
成功返回:
{"code":200,"msg":"上传成功","data":{"p":"//34133765.s21i.faimallusr.com/2/6/<ABUI>.jpg","i":"<ABUI id>", ...}}
CDN 根:34133765.s21i.faimallusr.com,真实图地址 https://34133765.s21i.faimallusr.com/2/6/<ABUI>.jpg。
> 多文件/大文件:先 POST /v/<app>/advance/info?<qs>(form-urlencoded fileMd5,fileSize,fileName,accessKey)取分块规划,再按 splitSize 切片逐块传,最后一块 complete:"true"。本 skill 单文件上传器已覆盖常见 <2MB 产品图。
二、端到端脚本(bind_product.js)
一键完成:上传本地图 → 弹窗选图绑定 → 填卖点/编码/详情 → 保存 → 重开校验。
// 配置读 configs.json(见 README),每条:
{ id, nameSub, sell, code, imgs:[本地文件名...], detail }
// imgs 本地文件放 <OUT>/products/ 下
node scripts/bind_product.js
脚本内部关键点(照四大坑实现):
- uploadImage(file):取 accessKey → 手工 multipart 传 → 返回 {abui, p}。
- openEdit(page,nameSub,id):进列表 iframe productManageV2 → 点行内 .operate_wrap .text 编辑 → 等 productEditV2 编辑 iframe。
- clearImages(ef):循环点 .fa-upload-list-item-delete-action 清空旧图。
- selectFromMaterial(page,abuiIds):切「我的文件」tab → 按 img.src 含 ABUI id 找 item--content → 受信点击 → 读 已选择(N/50) 计数器确认。
- fillDetail(ef,txt):精确点「详情介绍」tab → 等 baidu_editor_0 iframe → 写 body.innerHTML。
- 受控组件用原生 setter 写卖点/编码。
- 保存:点 .fk_confirmBar 内文案等于「确认」的 button;抓 setProductV2 请求解码 productInfo 坐实落库。
- 校验:重开编辑框,数 .fa-upload-list-item 与 CDN 图、回读卖点/详情。
二-b、发现其它 SKU(list_products.js)
套到更多产品前,先列出后台全部产品的 id/名称/编码,把目标条目抄进 configs.json 即可批量绑定:
node scripts/list_products.js
# 输出每行: {"id":13,"name":"冰嘎精酿啤酒11°P瓶装456ml*12","code":"BGPJ-11P-456ML-12","imgInList":0}
# 并保存 <WM_OUT>/product_list.json
> 实测:aluo01 账号(siteId=6)当前仅有 2 款产品(id 13 啤酒、id 11 果汁),均已由本 skill 绑定完成。
> 新增产品后重跑本命令即可拿到新 id。
三、凭据与运行环境
- 需要登录态:
state.json(Playwrightcontext.cookies()导出),含_TOKEN等 cookie。从浏览器导出或让 session 工具落盘。 getAccessKey的_TOKEN优先从state.jsoncookies 取_TOKEN,缺失时回退process.env.WM_TOKEN。- Chromium:用
playwright-core+ 本地 Chrome 路径(envWM_CHROME,默认C:/Users/Administrator/.agent-browser/browsers/chrome-152.0.7977.82/chrome.exe)。 - 脚本内必须
delete所有 proxy 环境变量(HTTP_PROXY 等),否则直连 404。 - 路径可配:env
WM_OUT(默认脚本旁out/)。products/放本地待传图,state.json/accesskey.json放WM_OUT。
四、验证清单
- [ ] FaiUpload 上传返回
上传成功+data.p含s21i.faimallusr.com(图已进素材库) - [ ] 弹窗
已选择(N/50)计数器 == 目标图数 - [ ] 关闭弹窗后
.fa-upload-list-item数 == N 且 CDN 图数 == N - [ ] 抓到
setProductV2,解码productInfo.pic.length==N、productCode非空、descInfo有值 - [ ] 重开编辑框回读一致(图片/卖点/详情)
五、易错速查
| 现象 | 根因 | 修法 |
|---|---|---|
| 分块 POST 404 | fetch+FormData 拼 multipart 失败 | 改用 https.request 手工拼 |
getAccessKey rt:-2 |
误用 POST | 改成 GET |
| 弹窗点图计数器不变 | 合成 click / class 错 | 受信点击 item--content |
保存后 pic:[] |
文件 input 直传不回写 CDN | 走素材弹窗选图 |
| 详情写不进 | 点了整段容器 tab | 精确点 innerText==='详情介绍' |
| 产品编码存空 | input.value= 不触发 v-model |
原生 setter + dispatch |
六、安全
state.json/accesskey.json含会话凭据,勿入库、勿外传;本 skill 包默认不随附这些文件。_TOKEN是会话令牌,过期需重新导出state.json。- 操作会真实改写线上产品,先在小批量 SKU 验证再批量。