From dc55f5646a469ceb47bdc02112a156d79b411690 Mon Sep 17 00:00:00 2001 From: QiuSW Date: Wed, 7 Oct 2026 17:05:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=201688=20=E5=9B=BE=E6=90=9C=E4=BC=98?= =?UTF-8?q?=E5=85=88=E7=AD=9B=E9=80=89=E9=93=BA=E8=B4=A7=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E5=8C=85=20(#40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/02-architecture-and-code-map.md | 6 +-- internal/alibaba1688/search.go | 79 ++++++++++++++++++++++++++++ internal/alibaba1688/search_test.go | 41 +++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 internal/alibaba1688/search_test.go diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index 432ca1e..55d3d40 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -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 # 架构与代码地图 @@ -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 流程。 diff --git a/internal/alibaba1688/search.go b/internal/alibaba1688/search.go index b3e8bf7..91e20a8 100644 --- a/internal/alibaba1688/search.go +++ b/internal/alibaba1688/search.go @@ -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 无效") diff --git a/internal/alibaba1688/search_test.go b/internal/alibaba1688/search_test.go new file mode 100644 index 0000000..3786bdc --- /dev/null +++ b/internal/alibaba1688/search_test.go @@ -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) + } +}