feat: 添加商品绑定功能
- 在图文发布和视频发布流程中集成商品绑定功能 - 新增 Products 字段到发布请求结构体 - 实现 go-rod 原生商品绑定函数(bindProducts) - 商品绑定失败将阻断发布流程并返回具体错误信息 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,7 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in
|
||||
content, _ := args["content"].(string)
|
||||
imagePathsInterface, _ := args["images"].([]interface{})
|
||||
tagsInterface, _ := args["tags"].([]interface{})
|
||||
productsInterface, _ := args["products"].([]interface{})
|
||||
|
||||
var imagePaths []string
|
||||
for _, path := range imagePathsInterface {
|
||||
@@ -144,6 +145,13 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in
|
||||
}
|
||||
}
|
||||
|
||||
var products []string
|
||||
for _, p := range productsInterface {
|
||||
if pStr, ok := p.(string); ok {
|
||||
products = append(products, pStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析定时发布参数
|
||||
scheduleAt, _ := args["schedule_at"].(string)
|
||||
visibility := parseVisibility(args)
|
||||
@@ -151,7 +159,7 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in
|
||||
// 解析原创参数
|
||||
isOriginal, _ := args["is_original"].(bool)
|
||||
|
||||
logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d, 定时: %s, 原创: %v, visibility: %s", title, len(imagePaths), len(tags), scheduleAt, isOriginal, visibility)
|
||||
logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d, 定时: %s, 原创: %v, visibility: %s, 商品: %v", title, len(imagePaths), len(tags), scheduleAt, isOriginal, visibility, products)
|
||||
|
||||
// 构建发布请求
|
||||
req := &PublishRequest{
|
||||
@@ -162,6 +170,7 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in
|
||||
ScheduleAt: scheduleAt,
|
||||
IsOriginal: isOriginal,
|
||||
Visibility: visibility,
|
||||
Products: products,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
@@ -193,6 +202,7 @@ func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]inte
|
||||
content, _ := args["content"].(string)
|
||||
videoPath, _ := args["video"].(string)
|
||||
tagsInterface, _ := args["tags"].([]interface{})
|
||||
productsInterface, _ := args["products"].([]interface{})
|
||||
|
||||
var tags []string
|
||||
for _, tag := range tagsInterface {
|
||||
@@ -201,6 +211,13 @@ func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]inte
|
||||
}
|
||||
}
|
||||
|
||||
var products []string
|
||||
for _, p := range productsInterface {
|
||||
if pStr, ok := p.(string); ok {
|
||||
products = append(products, pStr)
|
||||
}
|
||||
}
|
||||
|
||||
if videoPath == "" {
|
||||
return &MCPToolResult{
|
||||
Content: []MCPContent{{
|
||||
@@ -215,7 +232,7 @@ func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]inte
|
||||
scheduleAt, _ := args["schedule_at"].(string)
|
||||
visibility := parseVisibility(args)
|
||||
|
||||
logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d, 定时: %s, visibility: %s", title, len(tags), scheduleAt, visibility)
|
||||
logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d, 定时: %s, visibility: %s, 商品: %v", title, len(tags), scheduleAt, visibility, products)
|
||||
|
||||
// 构建发布请求
|
||||
req := &PublishVideoRequest{
|
||||
@@ -225,6 +242,7 @@ func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]inte
|
||||
Tags: tags,
|
||||
ScheduleAt: scheduleAt,
|
||||
Visibility: visibility,
|
||||
Products: products,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
|
||||
@@ -24,6 +24,7 @@ type PublishContentArgs struct {
|
||||
ScheduleAt string `json:"schedule_at,omitempty" jsonschema:"定时发布时间(可选),ISO8601格式如 2024-01-20T10:30:00+08:00,支持1小时至14天内。不填则立即发布"`
|
||||
IsOriginal bool `json:"is_original,omitempty" jsonschema:"是否声明原创(可选),true为声明原创,false或不填则不声明"`
|
||||
Visibility string `json:"visibility,omitempty" jsonschema:"可见范围(可选),支持: 公开可见(默认)、仅自己可见、仅互关好友可见。不填则默认公开可见"`
|
||||
Products []string `json:"products,omitempty" jsonschema:"商品关键词列表(可选),用于绑定带货商品。填写商品名称或商品ID,系统会自动搜索并选择第一个匹配结果。需账号已开通商品功能。示例: [面膜, 防晒霜SPF50]"`
|
||||
}
|
||||
|
||||
// PublishVideoArgs 发布视频的参数(仅支持本地单个视频文件)
|
||||
@@ -34,6 +35,7 @@ type PublishVideoArgs struct {
|
||||
Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"`
|
||||
ScheduleAt string `json:"schedule_at,omitempty" jsonschema:"定时发布时间(可选),ISO8601格式如 2024-01-20T10:30:00+08:00,支持1小时至14天内。不填则立即发布"`
|
||||
Visibility string `json:"visibility,omitempty" jsonschema:"可见范围(可选),支持: 公开可见(默认)、仅自己可见、仅互关好友可见。不填则默认公开可见"`
|
||||
Products []string `json:"products,omitempty" jsonschema:"商品关键词列表(可选),用于绑定带货商品。填写商品名称或商品ID,系统会自动搜索并选择第一个匹配结果。需账号已开通商品功能。示例: [面膜, 防晒霜SPF50]"`
|
||||
}
|
||||
|
||||
// SearchFeedsArgs 搜索内容的参数
|
||||
@@ -219,6 +221,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
|
||||
"schedule_at": args.ScheduleAt,
|
||||
"is_original": args.IsOriginal,
|
||||
"visibility": args.Visibility,
|
||||
"products": convertStringsToInterfaces(args.Products),
|
||||
}
|
||||
result := appServer.handlePublishContent(ctx, argsMap)
|
||||
return convertToMCPResult(result), nil, nil
|
||||
@@ -391,6 +394,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
|
||||
"tags": convertStringsToInterfaces(args.Tags),
|
||||
"schedule_at": args.ScheduleAt,
|
||||
"visibility": args.Visibility,
|
||||
"products": convertStringsToInterfaces(args.Products),
|
||||
}
|
||||
result := appServer.handlePublishVideo(ctx, argsMap)
|
||||
return convertToMCPResult(result), nil, nil
|
||||
|
||||
@@ -35,6 +35,7 @@ type PublishRequest struct {
|
||||
ScheduleAt string `json:"schedule_at,omitempty"` // 定时发布时间,ISO8601格式,为空则立即发布
|
||||
IsOriginal bool `json:"is_original,omitempty"` // 是否声明原创
|
||||
Visibility string `json:"visibility,omitempty"` // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见"
|
||||
Products []string `json:"products,omitempty"` // 商品关键词列表,用于绑定带货商品
|
||||
}
|
||||
|
||||
// LoginStatusResponse 登录状态响应
|
||||
@@ -67,6 +68,7 @@ type PublishVideoRequest struct {
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
ScheduleAt string `json:"schedule_at,omitempty"` // 定时发布时间,ISO8601格式,为空则立即发布
|
||||
Visibility string `json:"visibility,omitempty"` // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见"
|
||||
Products []string `json:"products,omitempty"` // 商品关键词列表,用于绑定带货商品
|
||||
}
|
||||
|
||||
// PublishVideoResponse 发布视频响应
|
||||
@@ -217,6 +219,7 @@ func (s *XiaohongshuService) PublishContent(ctx context.Context, req *PublishReq
|
||||
ScheduleTime: scheduleTime,
|
||||
IsOriginal: req.IsOriginal,
|
||||
Visibility: req.Visibility,
|
||||
Products: req.Products,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
@@ -307,6 +310,7 @@ func (s *XiaohongshuService) PublishVideo(ctx context.Context, req *PublishVideo
|
||||
VideoPath: req.Video,
|
||||
ScheduleTime: scheduleTime,
|
||||
Visibility: req.Visibility,
|
||||
Products: req.Products,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
|
||||
@@ -24,6 +24,7 @@ type PublishImageContent struct {
|
||||
ScheduleTime *time.Time // 定时发布时间,nil 表示立即发布
|
||||
IsOriginal bool // 是否声明原创
|
||||
Visibility string // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见"
|
||||
Products []string // 商品关键词列表,用于绑定带货商品
|
||||
}
|
||||
|
||||
type PublishAction struct {
|
||||
@@ -84,9 +85,9 @@ func (p *PublishAction) Publish(ctx context.Context, content PublishImageContent
|
||||
tags = tags[:10]
|
||||
}
|
||||
|
||||
logrus.Infof("发布内容: title=%s, images=%v, tags=%v, schedule=%v, original=%v, visibility=%s", content.Title, len(content.ImagePaths), tags, content.ScheduleTime, content.IsOriginal, content.Visibility)
|
||||
logrus.Infof("发布内容: title=%s, images=%v, tags=%v, schedule=%v, original=%v, visibility=%s, products=%v", content.Title, len(content.ImagePaths), tags, content.ScheduleTime, content.IsOriginal, content.Visibility, content.Products)
|
||||
|
||||
if err := submitPublish(page, content.Title, content.Content, tags, content.ScheduleTime, content.IsOriginal, content.Visibility); err != nil {
|
||||
if err := submitPublish(page, content.Title, content.Content, tags, content.ScheduleTime, content.IsOriginal, content.Visibility, content.Products); err != nil {
|
||||
return errors.Wrap(err, "小红书发布失败")
|
||||
}
|
||||
|
||||
@@ -270,7 +271,7 @@ func waitForUploadComplete(page *rod.Page, expectedCount int) error {
|
||||
return errors.Errorf("第%d张图片上传超时(60s),请检查网络连接和图片大小", expectedCount)
|
||||
}
|
||||
|
||||
func submitPublish(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, isOriginal bool, visibility string) error {
|
||||
func submitPublish(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, isOriginal bool, visibility string, products []string) error {
|
||||
titleElem, err := page.Element("div.d-input input")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "查找标题输入框失败")
|
||||
@@ -332,6 +333,11 @@ func submitPublish(page *rod.Page, title, content string, tags []string, schedul
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定商品
|
||||
if err := bindProducts(page, products); err != nil {
|
||||
return errors.Wrap(err, "绑定商品失败")
|
||||
}
|
||||
|
||||
submitButton, err := page.Element(".publish-page-publish-btn button.bg-red")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "查找发布按钮失败")
|
||||
@@ -835,3 +841,264 @@ func confirmOriginalDeclaration(page *rod.Page) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clickConfirmButton 点击确定按钮
|
||||
func clickConfirmButton(page *rod.Page) error {
|
||||
// 查找日期选择器弹窗中的确定按钮
|
||||
buttons, err := page.Elements("button.el-picker-panel__link-btn")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "查找确定按钮失败")
|
||||
}
|
||||
|
||||
for _, btn := range buttons {
|
||||
text, err := btn.Text()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(text) == "确定" {
|
||||
if err := btn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
return errors.Wrap(err, "点击确定按钮失败")
|
||||
}
|
||||
slog.Info("已点击确定按钮")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("未找到确定按钮")
|
||||
}
|
||||
|
||||
// bindProducts 绑定商品到发布内容
|
||||
func bindProducts(page *rod.Page, products []string) error {
|
||||
if len(products) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
slog.Info("开始绑定商品", "products", products)
|
||||
|
||||
// 点击"添加商品"按钮
|
||||
if err := clickAddProductButton(page); err != nil {
|
||||
return errors.Wrap(err, "点击添加商品按钮失败")
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 等待商品选择弹窗出现
|
||||
modal, err := waitForProductModal(page)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "等待商品弹窗失败")
|
||||
}
|
||||
slog.Info("商品选择弹窗已打开")
|
||||
|
||||
// 遍历搜索并选择商品
|
||||
var failedProducts []string
|
||||
for _, keyword := range products {
|
||||
if err := searchAndSelectProduct(page, modal, keyword); err != nil {
|
||||
slog.Warn("搜索选择商品失败", "keyword", keyword, "error", err)
|
||||
failedProducts = append(failedProducts, keyword)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 点击保存按钮
|
||||
if err := clickModalSaveButton(page, modal); err != nil {
|
||||
return errors.Wrap(err, "点击保存按钮失败")
|
||||
}
|
||||
|
||||
// 等待弹窗关闭
|
||||
if err := waitForModalClose(page); err != nil {
|
||||
slog.Warn("等待弹窗关闭超时", "error", err)
|
||||
}
|
||||
|
||||
if len(failedProducts) > 0 {
|
||||
return errors.Errorf("部分商品未找到: %v", failedProducts)
|
||||
}
|
||||
|
||||
slog.Info("商品绑定完成", "total", len(products))
|
||||
return nil
|
||||
}
|
||||
|
||||
// clickAddProductButton 点击"添加商品"按钮
|
||||
func clickAddProductButton(page *rod.Page) error {
|
||||
// 查找包含"添加商品"文本的元素
|
||||
spans, err := page.Elements("span.d-text")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "查找商品按钮文本失败")
|
||||
}
|
||||
|
||||
for _, span := range spans {
|
||||
text, err := span.Text()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(text) == "添加商品" {
|
||||
// 向上查找可点击的父元素
|
||||
parent := span
|
||||
for i := 0; i < 5; i++ {
|
||||
p, err := parent.Parent()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
parent = p
|
||||
|
||||
tagName, err := parent.Eval(`() => this.tagName.toLowerCase()`)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tag := tagName.Value.Str()
|
||||
|
||||
// 检查是否为 button 或含 d-button class
|
||||
if tag == "button" {
|
||||
if err := parent.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
return errors.Wrap(err, "点击添加商品按钮失败")
|
||||
}
|
||||
slog.Info("已点击添加商品按钮")
|
||||
return nil
|
||||
}
|
||||
|
||||
cls, _ := parent.Attribute("class")
|
||||
if cls != nil && strings.Contains(*cls, "d-button") {
|
||||
if err := parent.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
return errors.Wrap(err, "点击添加商品按钮失败")
|
||||
}
|
||||
slog.Info("已点击添加商品按钮")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("未找到添加商品按钮,账号可能未开通商品功能")
|
||||
}
|
||||
|
||||
// waitForProductModal 等待商品选择弹窗出现
|
||||
func waitForProductModal(page *rod.Page) (*rod.Element, error) {
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
modal, err := page.Element(".multi-goods-selector-modal")
|
||||
if err == nil && modal != nil {
|
||||
visible, _ := modal.Visible()
|
||||
if visible {
|
||||
return modal, nil
|
||||
}
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil, errors.New("等待商品选择弹窗超时")
|
||||
}
|
||||
|
||||
// searchAndSelectProduct 搜索并选择商品
|
||||
func searchAndSelectProduct(page *rod.Page, modal *rod.Element, keyword string) error {
|
||||
slog.Info("搜索商品", "keyword", keyword)
|
||||
|
||||
// 获取搜索框
|
||||
searchInput, err := modal.Element(`input[placeholder="搜索商品ID 或 商品名称"]`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "未找到商品搜索框")
|
||||
}
|
||||
|
||||
// 清空并输入关键词
|
||||
if err := searchInput.SelectAllText(); err != nil {
|
||||
slog.Warn("选择搜索框文本失败", "error", err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if err := searchInput.Input(keyword); err != nil {
|
||||
return errors.Wrap(err, "输入搜索关键词失败")
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// 模拟回车触发搜索
|
||||
if err := searchInput.MustKeyActions().Press(input.Enter).Do(); err != nil {
|
||||
return errors.Wrap(err, "触发搜索失败")
|
||||
}
|
||||
|
||||
// 等待搜索结果
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 等待 loading 消失
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
loading, err := modal.Element(".d-loading")
|
||||
if err != nil || loading == nil {
|
||||
break
|
||||
}
|
||||
visible, _ := loading.Visible()
|
||||
if !visible {
|
||||
break
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// 点击第一个商品的 checkbox
|
||||
checkbox, err := modal.Element(".goods-item .d-checkbox")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "未找到商品选择框")
|
||||
}
|
||||
|
||||
if err := checkbox.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
return errors.Wrap(err, "点击商品选择框失败")
|
||||
}
|
||||
|
||||
slog.Info("已选择商品", "keyword", keyword)
|
||||
return nil
|
||||
}
|
||||
|
||||
// clickModalSaveButton 点击保存按钮
|
||||
func clickModalSaveButton(page *rod.Page, modal *rod.Element) error {
|
||||
// 查找保存按钮
|
||||
buttons, err := modal.Elements(".goods-selected-footer button")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "查找保存按钮失败")
|
||||
}
|
||||
|
||||
for _, btn := range buttons {
|
||||
text, err := btn.Text()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 保存按钮通常包含"保存"或"确定"文字
|
||||
if strings.Contains(text, "保存") || strings.Contains(text, "确定") {
|
||||
if err := btn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
return errors.Wrap(err, "点击保存按钮失败")
|
||||
}
|
||||
slog.Info("已点击保存按钮")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试点击主按钮
|
||||
primaryBtn, err := modal.Element(".goods-selected-footer .d-button--primary")
|
||||
if err == nil && primaryBtn != nil {
|
||||
if err := primaryBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
return errors.Wrap(err, "点击主按钮失败")
|
||||
}
|
||||
slog.Info("已点击主按钮")
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("未找到保存按钮")
|
||||
}
|
||||
|
||||
// waitForModalClose 等待弹窗关闭
|
||||
func waitForModalClose(page *rod.Page) error {
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
modal, err := page.Element(".multi-goods-selector-modal")
|
||||
if err != nil {
|
||||
return nil // 弹窗已关闭
|
||||
}
|
||||
if modal == nil {
|
||||
return nil
|
||||
}
|
||||
visible, _ := modal.Visible()
|
||||
if !visible {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
return errors.New("等待弹窗关闭超时")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type PublishVideoContent struct {
|
||||
VideoPath string
|
||||
ScheduleTime *time.Time // 定时发布时间,nil 表示立即发布
|
||||
Visibility string // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见"
|
||||
Products []string // 商品关键词列表,用于绑定带货商品
|
||||
}
|
||||
|
||||
// NewPublishVideoAction 进入发布页并切换到"上传视频"
|
||||
@@ -63,7 +64,7 @@ func (p *PublishAction) PublishVideo(ctx context.Context, content PublishVideoCo
|
||||
return errors.Wrap(err, "小红书上传视频失败")
|
||||
}
|
||||
|
||||
if err := submitPublishVideo(page, content.Title, content.Content, content.Tags, content.ScheduleTime, content.Visibility); err != nil {
|
||||
if err := submitPublishVideo(page, content.Title, content.Content, content.Tags, content.ScheduleTime, content.Visibility, content.Products); err != nil {
|
||||
return errors.Wrap(err, "小红书发布失败")
|
||||
}
|
||||
return nil
|
||||
@@ -131,7 +132,7 @@ func waitForPublishButtonClickable(page *rod.Page) (*rod.Element, error) {
|
||||
}
|
||||
|
||||
// submitPublishVideo 填写标题、正文、标签并点击发布(等待按钮可点击后再提交)
|
||||
func submitPublishVideo(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, visibility string) error {
|
||||
func submitPublishVideo(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, visibility string, products []string) error {
|
||||
// 标题
|
||||
titleElem, err := page.Element("div.d-input input")
|
||||
if err != nil {
|
||||
@@ -172,6 +173,11 @@ func submitPublishVideo(page *rod.Page, title, content string, tags []string, sc
|
||||
return errors.Wrap(err, "设置可见范围失败")
|
||||
}
|
||||
|
||||
// 绑定商品
|
||||
if err := bindProducts(page, products); err != nil {
|
||||
return errors.Wrap(err, "绑定商品失败")
|
||||
}
|
||||
|
||||
// 等待发布按钮可点击
|
||||
btn, err := waitForPublishButtonClickable(page)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user