feat: 1688 图搜优先筛选铺货素材包 (#40)

This commit is contained in:
ila
2026-10-07 17:05:08 +08:00
parent d7578a3383
commit dc55f5646a
3 changed files with 123 additions and 3 deletions
+3 -3
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Architecture-and-Code-Map
wiki_url: https://git.ilapage.cn/chengma/cmsp/wiki/Architecture-and-Code-Map.-
wiki_revision: 928afb2d53fd93391b384fe3af843180a0bfc728
synchronized_at: 2026-10-07T08:26:24Z
wiki_revision: 3097e923b60ef404eef7b58cbff42f4ed1bc787a
synchronized_at: 2026-10-07T09:03:51Z
<!-- gitea-wiki-mirror:end -->
# 架构与代码地图
@@ -183,4 +183,4 @@ cmsp/
## 1688 视频下载来源(2026-10-07,#37)
商品列表“下载视频”旁选择淘宝或 1688;`download.source` 写入本机 `config.yaml`,批次启动时固定来源。1688 与淘宝共用现有专属 Chrome Profile、进程和运行状态文件;默认目录由 `internal/config.DefaultChromeUserDataDir` 定位到 `config.yaml` 旁的 `运行数据/浏览器/淘宝1688`,两个站点均由操作者手动登录。`app_1688.go` 调用 `internal/alibaba1688/search.go`:上传商品主图到网页以图搜,按结果顺序进入候选详情,读取页面视频地址。`internal/downloader` 将候选 MP4 下载到临时文件;`internal/alibaba1688/quality.go` 用 ffprobe 校验、ffmpeg 抽帧检测黑底文字广告和静止拼图。只有判为 `usable` 的视频才晋升为商品文件,`internal/store.CompleteVideoDownload` 在同一 SQLite 事务中写视频记录与商品已下载状态。拒绝、无法判定、候选下载失败时保留原商品状态;上传仍沿用原有 erpgo 流程。
商品列表“下载视频”旁选择淘宝或 1688;`download.source` 写入本机 `config.yaml`,批次启动时固定来源。1688 与淘宝共用现有专属 Chrome Profile、进程和运行状态文件;默认目录由 `internal/config.DefaultChromeUserDataDir` 定位到 `config.yaml` 旁的 `运行数据/浏览器/淘宝1688`,两个站点均由操作者手动登录。`app_1688.go` 调用 `internal/alibaba1688/search.go`:上传商品主图到网页以图搜,先在结果页选择“铺货素材包”并确认筛选状态和 `offerTags=4358146` 生效,再按结果顺序读取候选 ID、进入详情并读取视频地址。筛选控件缺失或未生效时停止该商品,不使用默认候选列表。结果卡片目前没有可直接读取的详情 href,详情仍按 ID 打开。`internal/downloader` 将候选 MP4 下载到临时文件;`internal/alibaba1688/quality.go` 用 ffprobe 校验、ffmpeg 抽帧检测黑底文字广告和静止拼图。只有判为 `usable` 的视频才晋升为商品文件,`internal/store.CompleteVideoDownload` 在同一 SQLite 事务中写视频记录与商品已下载状态。拒绝、无法判定、候选下载失败时保留原商品状态;上传仍沿用原有 erpgo 流程。
+79
View File
@@ -196,6 +196,12 @@ func SearchByImage(ctx context.Context, page *taobao.CDP, port int, imageURL str
if err := checkPage(page, false); err != nil {
return nil, err
}
if err := selectDistributionMaterial(ctx, page); err != nil {
return nil, err
}
if err := checkPage(page, false); err != nil {
return nil, err
}
if limit < 10 {
limit = 10
}
@@ -225,6 +231,79 @@ func SearchByImage(ctx context.Context, page *taobao.CDP, port int, imageURL str
return valid, nil
}
type materialFilterPage interface {
Evaluate(string) (string, error)
Send(string, any) (json.RawMessage, error)
}
// selectDistributionMaterial 在读取候选之前通过页面控件选择“铺货素材包”。
// 未确认筛选生效时不使用默认结果,避免将低质量候选作为已筛选结果。
func selectDistributionMaterial(ctx context.Context, page materialFilterPage) error {
const stateExpr = `(()=>{const e=[...document.querySelectorAll('span')].find(x=>x.textContent.trim()==='铺货素材包');if(!e)return JSON.stringify({found:false});const r=e.getBoundingClientRect();return JSON.stringify({found:true,selected:e.className.includes('selected'),x:r.x+r.width/2,y:r.y+r.height/2,tags:new URL(location.href).searchParams.get('offerTags')})})()`
type filterState struct {
Found bool `json:"found"`
Selected bool `json:"selected"`
X float64 `json:"x"`
Y float64 `json:"y"`
Tags string `json:"tags"`
}
readState := func() (filterState, error) {
value, err := page.Evaluate(stateExpr)
if err != nil {
return filterState{}, err
}
var state filterState
if err := json.Unmarshal([]byte(value), &state); err != nil {
return filterState{}, fmt.Errorf("解析 1688 图搜筛选状态失败:%w", err)
}
return state, nil
}
var state filterState
for i := 0; i < 10; i++ {
var err error
state, err = readState()
if err != nil {
return err
}
if state.Found {
break
}
if err := wait(ctx, time.Second); err != nil {
return err
}
}
if !state.Found {
return fmt.Errorf("1688 图搜未找到“铺货素材包”筛选项")
}
if state.Selected && state.Tags == "4358146" {
return nil
}
if !state.Selected {
if _, err := page.Send("Page.bringToFront", nil); err != nil {
return err
}
for _, typ := range []string{"mouseMoved", "mousePressed", "mouseReleased"} {
if _, err := page.Send("Input.dispatchMouseEvent", map[string]any{"type": typ, "x": state.X, "y": state.Y, "button": "left", "clickCount": 1}); err != nil {
return err
}
}
}
for i := 0; i < 10; i++ {
if err := wait(ctx, time.Second); err != nil {
return err
}
var err error
state, err = readState()
if err != nil {
return err
}
if state.Selected && state.Tags == "4358146" {
return nil
}
}
return fmt.Errorf("1688 图搜“铺货素材包”筛选未生效,未读取默认候选")
}
func OfferVideo(page *taobao.CDP, id string, delay time.Duration) (string, error) {
if !offerIDPattern.MatchString(id) {
return "", fmt.Errorf("1688 商品 ID 无效")
+41
View File
@@ -0,0 +1,41 @@
package alibaba1688
import (
"context"
"encoding/json"
"testing"
)
type materialPageStub struct {
selected bool
tags string
clicks int
}
func (p *materialPageStub) Evaluate(string) (string, error) {
value, _ := json.Marshal(map[string]any{"found": true, "selected": p.selected, "tags": p.tags, "x": 100, "y": 100})
return string(value), nil
}
func (p *materialPageStub) Send(method string, params any) (json.RawMessage, error) {
if method == "Input.dispatchMouseEvent" && params.(map[string]any)["type"] == "mouseReleased" {
p.clicks++
p.selected = true
p.tags = "4358146"
}
return nil, nil
}
func Test铺货素材包已选中时不重复点击(t *testing.T) {
page := &materialPageStub{selected: true, tags: "4358146"}
if err := selectDistributionMaterial(context.Background(), page); err != nil || page.clicks != 0 {
t.Fatalf("已筛选页面不应重复点击:err=%v clicks=%d", err, page.clicks)
}
}
func Test铺货素材包未选中时点击并确认(t *testing.T) {
page := &materialPageStub{}
if err := selectDistributionMaterial(context.Background(), page); err != nil || page.clicks != 1 {
t.Fatalf("应点击并确认筛选:err=%v clicks=%d", err, page.clicks)
}
}