fix(ops): 记录图搜规格匹配的逐条结果,保留 Agent 原始失败原因 (#294)
排查时两次卡在缺记录上,只能靠手工重放接口和按失败耗时反推。 服务端:BatchSpecMatch 的返回值此前用 `_` 丢弃,23 条明细因 AI 服务 503 全部 失败时日志一个字都没有。改为记录四个计数并附前几条阻塞原因。 Agent:IMAGE_SEARCH_ENTRY_NOT_FOUND 有「找不到入口」和「归位失败」两个来源, 统一文案把两者抹平。改为在用户文案后括注内部原因,写法与候选点击失败一致。 两处都只记录错误码、计数与规格层面的原因,不含凭据、账号、订单和个人数据。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
This commit is contained in:
+16
-1
@@ -417,6 +417,17 @@ class AgentForegroundService : Service() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 把采购员看得懂的文案和内部原因拼在一起:文案在前,原始说法括注在后。
|
||||
*
|
||||
* 内部原因只描述页面形态与归位结果,不含凭据、账号、订单和个人数据(#294)。
|
||||
*/
|
||||
private fun imageSearchFailureMessage(code: String, internal: String): String {
|
||||
if (!code.startsWith("IMAGE_SEARCH")) return internal
|
||||
val user = imageSearchUserMessage(code)
|
||||
return if (internal.isBlank() || internal == user) user else "$user[$internal]"
|
||||
}
|
||||
|
||||
/** #277:图搜任务失败码到人话文案的映射,采购员看得懂即可,不暴露内部状态机细节。 */
|
||||
private fun imageSearchUserMessage(code: String): String = when (code) {
|
||||
"IMAGE_SEARCH_PERMISSION_REQUIRED" -> "未授予相册权限,无法准备图搜参考图,请到“状态”页开启相册权限后重试。"
|
||||
@@ -763,7 +774,11 @@ class AgentForegroundService : Service() {
|
||||
// `[必须]` 归位与选图是同一个调用:中间多一次无障碍树抓取就可能错过
|
||||
// 拼多多摄像头自动弹出重试弹窗前的约 4 秒窗口,详见 enterAndSelectImage。
|
||||
val entry = imageSearch.enterAndSelectImage(::launchPddToForeground, ::resetPddToHome)
|
||||
if (!entry.successful) throw TaskFailure(entry.code, imageSearchUserMessage(entry.code).takeIf { entry.code.startsWith("IMAGE_SEARCH") } ?: entry.message)
|
||||
// `[必须]` 保留归位状态机的原始说法。IMAGE_SEARCH_ENTRY_NOT_FOUND 有两个
|
||||
// 来源——「找不到唯一可点的拍照搜索入口」和「归位失败(预算耗尽)」——
|
||||
// 统一文案会把两者抹平,排查任务 156 时只能靠失败耗时反推(#294)。
|
||||
// 括注写法与下面候选点击失败一致。
|
||||
if (!entry.successful) throw TaskFailure(entry.code, imageSearchFailureMessage(entry.code, entry.message))
|
||||
if (!imageSearch.waitForResultsPage()) {
|
||||
throw TaskFailure("IMAGE_SEARCH_RESULTS_NOT_READY", imageSearchUserMessage("IMAGE_SEARCH_RESULTS_NOT_READY"))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package task
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"go-admin/app/goauto/purchase"
|
||||
|
||||
@@ -112,9 +114,48 @@ func (service *Service) matchSpecsAfterImageSearch(ctx context.Context, linked a
|
||||
// 先把该虾皮商品的完整颜色尺码补齐,再匹配:只匹配本次明细会留下空洞,
|
||||
// 新订单带来新组合时又得重来(#290)。
|
||||
service.syncShopeeSpecs(ctx, linked.ShopeeProductID, linked.PDDProductID)
|
||||
if _, err := purchase.NewService(service.DB).BatchSpecMatch(ctx, purchase.BatchSpecMatchRequest{
|
||||
purchaseService := purchase.NewService(service.DB)
|
||||
response, err := purchaseService.BatchSpecMatch(ctx, purchase.BatchSpecMatchRequest{
|
||||
SYBProductIDs: linked.SYBProductIDs,
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("image search spec match failed for syb products %v: %v", linked.SYBProductIDs, err)
|
||||
} else {
|
||||
// `[必须]` 逐条结果必须落日志。此前这里是 `if _, err := ...`,23 条明细因 AI
|
||||
// 服务 503 全部失败时日志一个字都没有,真实原因只能靠手工重放接口拿到(#294)。
|
||||
// 只记录计数与规格层面的原因,不记录凭据、账号、订单和个人数据。
|
||||
log.Printf("image search spec match for shopee product %d: confirmed=%d pending=%d failed=%d skipped=%d; %s",
|
||||
linked.ShopeeProductID, response.AutoConfirmedCount, response.PendingCount,
|
||||
response.FailedCount, response.SkippedCount, batchSpecMatchDigest(response))
|
||||
}
|
||||
// 同步补进档案的规格值没有对应明细,上面那轮碰不到它们;等订单真来了还得人工
|
||||
// 点一次匹配,正是 #290 想消除的动作。这里对档案里所有未映射的值再跑一轮(#293)。
|
||||
archive, err := purchaseService.MatchArchiveSpecs(ctx, linked.ShopeeProductID)
|
||||
if err != nil {
|
||||
log.Printf("archive spec match failed for shopee product %d: %v", linked.ShopeeProductID, err)
|
||||
return
|
||||
}
|
||||
log.Printf("archive spec match for shopee product %d: confirmed=%d skipped=%d failed=%d",
|
||||
linked.ShopeeProductID, archive.Confirmed, archive.Skipped, archive.Failed)
|
||||
}
|
||||
|
||||
// batchSpecMatchDigest renders the first few non-successful reasons so a total
|
||||
// failure says why, without dumping every row (#294).
|
||||
func batchSpecMatchDigest(response purchase.BatchSpecMatchResponse) string {
|
||||
reasons := make([]string, 0, specMatchDigestLimit)
|
||||
for _, item := range response.Items {
|
||||
if item.Status == purchase.BatchSpecMatchAutoConfirmed || strings.TrimSpace(item.Reason) == "" {
|
||||
continue
|
||||
}
|
||||
reasons = append(reasons, fmt.Sprintf("syb %d %s: %s", item.SYBProductID, item.Status, item.Reason))
|
||||
if len(reasons) == specMatchDigestLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(reasons) == 0 {
|
||||
return "no blocking reasons"
|
||||
}
|
||||
return strings.Join(reasons, " | ")
|
||||
}
|
||||
|
||||
const specMatchDigestLimit = 3
|
||||
|
||||
Reference in New Issue
Block a user