Commit Graph
209 Commits
Author SHA1 Message Date
QiuSW 368f2c2357 feat: filter purchase tasks by successful SYB order writeback (#327) 2026-09-19 15:38:42 +08:00
QiuSW 3a2472dd20 feat: complete purchase order information and simplify SYB writeback (#326) 2026-09-19 15:16:39 +08:00
QiuSW b76fb73e12 fix: wait for unpaid order evidence and report payable total (#325) 2026-09-19 11:52:53 +08:00
QiuSW 10be37498d feat: add Chrome order backfill workflow (#316) 2026-09-18 16:03:27 +08:00
QiuSW e89de1a085 feat: queue and reconcile SYB purchase order numbers (#305) 2026-09-18 10:13:20 +08:00
QiuSW 8c01329f97 feat: capture optional PDD order amount in manual backfill (#306) 2026-09-18 09:22:12 +08:00
QiuSWandClaude Opus 5 8c1cec4430 merge: #241 服务端按地址后缀批量回填订单号与下单时间
合并 feat/241-order-backfill-endpoint(290a17e、72b8b5d)。

代码自动合并无冲突;与今天 #302(PaymentPageObservedAt)、#303(原地重试)
同文件的改动经 purchase / purchasecontract / task 测试验证无语义冲突。

docs 三个镜像冲突取主线版本:主线镜像于 2026-09-11 同步 Wiki,已包含
#241/#242 说明及其后 #254、#271 内容;分支侧为 2026-09-08 旧快照。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 15:49:30 +08:00
QiuSWandClaude Opus 5 60c75261d3 fix: 采购管理批量重试改为原地重试选中任务,不再新建 (#303)
采购员勾选失败任务点重试,要的是这条任务本身再跑一次。原先 BatchRetry 一律
调用 Create 新建(CG-224 → CG-234),任务号变化,同一 SYB 明细的多次尝试分散在
多个任务上。

- BatchRetry 改为逐条调用既有 Reset:任务号不变,状态回到 pending,本次执行记入
  purchase_task_attempt。Reset 的全部保护原样沿用——碰过下单边界、同一明细已有
  更新任务、规格快照不完整均拒绝并返回原因,拒绝后不退回新建。
- 先识别重放再做资格预检:首次重试后任务已是 pending,先预检会把同一 requestId
  的重复提交判为「只有失败任务可以重试」,破坏幂等。测试抓到后修正。
- 资格预检关闭设备占用检查,同一设备上勾选的多条可排队;真正的占用判断在 Reset
  事务内,ensureDeviceFree 不把租约为空的 pending 计为占用。
- AgentRetry(替代商品已匹配、继续采购)改走 batchRetryCreate,保持新建:商品已
  替换,原任务的商品与规格快照不能复用。
- 前端文案改为「已重试 / 第 N 次执行」。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 14:50:05 +08:00
QiuSWandClaude Sonnet 5 f634996dad fix: order_result_unknown 补充"是否见过支付/待付款页"诊断标记 (#302)
order_result_unknown 目前是全有或全无:parseOrderEvidence 要求订单号、下单
时间、待付款/支付文案同时命中才算 order_created,任何一项缺失就落进同一个
order_result_unknown,无法区分"确实到过支付页只是没读全证据"和"根本没到
那一步"——前者大概率已在 PDD 建了真实订单。

Agent:readOrderResult 采样循环中,只要命中过支付页 Activity 或
unpaidContextVisible(待付款/待支付/去支付文案),记 paymentPageObserved,
与订单号是否解析成功无关,随 order_result_unknown 一起上报。

服务端:PurchaseTask 新增 PaymentPageObservedAt,仅在请求带
paymentPageObserved=true 时写入服务端当前时间;不回填既有 107 笔历史记录,
无法从历史数据反推当时是否见过支付页。

不改判定结果本身,order_created 的四项条件、批量重试逻辑均未动。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 14:25:47 +08:00
QiuSWandClaude Opus 5 57f6cec803 fix(server): 档案合并改为无条件,既有数据可自愈 (#301)
只在键发生变化时才补档案是不够的:既有数据的键早已被前一次重解析改对了,档案
却还是空的,那样永远补不上——线上 215 条全部返回 unchanged,档案一条都没补进去。

mergeParsedSpec 对已存在的值幂等,无条件合并让这条路径能自愈。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 09:44:48 +08:00
QiuSWandClaude Opus 5 0e41000f62 fix(server): 消歧后的新键补进档案 (#301)
resyncShopeeProductKeys 改了兄弟明细的 target_color,却没把新键写进档案;只有被
直接重解析的那一条走了 mergeParsedSpec。

线上 2026-09-17 的后果:明细已是 `黑色【長袖】`,档案里还是旧的 `黑色`,采购查
映射查不到,明细永远停在「规格待匹配」;而采购员点「一键匹配」匹到的是档案里
剩下的旧键,看起来成功了却没有任何明细在用它。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 09:42:15 +08:00
QiuSWandClaude Opus 5 c1bd39496d fix(server): 规格键消歧,剥离 【...】 不再让不同商品塌缩成同一键 (#301)
`黑色【短袖】` 与 `黑色【長袖】` 剥离后都成了 `黑色`,档案里只有一个条目,两个
不同商品共用一份映射,必然有一半买错。#289 因此拦截,但人工匹配救不了——坏的是
键本身。线上 59 个塌缩键、45 个商品、344 条明细被卡住。

新增 sybspec.ResolveKeys:只在会产生歧义时保留括号内容。不塌缩的键与今天逐字
一致,线上一万四千多条明细中的绝大多数不受影响。

- 消歧需要同组全部原始规格,单条明细判断不了自己是否安全,因此在拿到
  shopeeProduct 之后、写档案之前做,并回写键发生变化的兄弟明细:新明细的到来
  可能让原本安全的键变成歧义,不同步会造成同组一半旧键一半新键。
- 不碰人工或 AI 已确认的明细,与 Reparse 不带 force 时的规则一致。
- 重解析同样走消歧,否则它会把键写回塌缩形式、悄悄撤销导入时的拆分。
- 键比较与产出统一去空白:SYB 对括号前的空格写法不一致,否则同一规格的两种写法
  会被误判为歧义。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 09:19:35 +08:00
QiuSWandClaude Opus 5 87a532f3bb fix(server): 图搜后的规格匹配脱离 Agent 请求 ctx (#300)
匹配挂在 Agent 提交采集结果那个 HTTP 请求的 context 上。AI 匹配要几十秒到几分钟,
Agent 先超时断开,ctx 被取消,匹配当场中断,采购员还得手动点一次「一键匹配」。

线上 2026-09-16 实测六个商品里四个是这样死的(#294 的日志第一次派上用场):
  shopee 28111/8544/5250/5259: archive spec match failed: context canceled
  shopee 9214/26680: 成功——只是 Agent 尚未超时

- 用 context.WithoutCancel 派生,保留请求携带的值(trace 不断链),只切断取消
  信号,再加 10 分钟上限兜底。
- 新增 specMatchAborted,与 unavailable 分开。ctx 取消是本端调度问题不是 Provider
  故障,重试用的是同一个已死的 ctx,毫无意义。此前被归成「AI 匹配服务暂时不可用」,
  线上出现过 unavailable=7 而 AI 服务完全正常,会一直误导排查。
- 全档案匹配遇到 aborted 立即早停,不再刷出一串同样的失败。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 17:20:11 +08:00
QiuSWandClaude Opus 5 8f337d5248 fix(server): 取消状态的 CHECK 约束改由 version-local 迁移应用 (#297)
约束修复原本放在 goautomigrations.Migrate 里,而 Migrate 只被 version-local 下的
迁移文件调用——那些文件在既有库上都已应用、会被跳过,于是修复永远不执行。

线上 2026-09-16 发布后实测:服务起来了、新代码在跑,ck_collection_task_status
却仍只认旧五个状态,一点取消就会被数据库拒绝。verify.go 里早有同样的警告:
「只把模型加进 migrations.Migrate 对已有数据库无效」。

单测能过是因为测试库是新建的,GORM 按模型标签直接建出含 cancelled 的约束;
既有库拿不到。

新增 1789700000000_collection_task_cancelled.go,并把约束函数导出供其调用。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 16:48:41 +08:00
QiuSWandClaude Opus 5 03647dd7dd feat(web): 采集任务页增加来源筛选与「取消未开始的任务」按钮 (#298)
页面混显三种来源却只有 goodsId 和状态两个筛选,批量取消时无法限定范围。

- 服务端 AdminList 增加 source 过滤,非法值报参数错误而非静默忽略。
- 前端增加来源筛选与来源列;状态补 cancelled(info 色,取消不是错误,不与
  failed 共用红色)。
- 「取消未开始的任务」按钮带实时数量,确认框列出将被取消的任务,并写明范围是
  整个筛选条件而非当前页。hasMore 时提示还有未处理的任务。

`[必须]` 取消范围包含 goodsId。少了这一维,按 goods_id 筛出两条、按钮却取消
三十几条——那正是当初放弃「两个固定按钮」、改用「筛选 + 一个按钮」想避免的事。
BatchCancel 的 goodsId 与 AdminList 用同一种匹配方式,否则两边范围会悄悄错开。

实施:sonnet 子代理;goodsId 范围一致性由复核补入。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 16:39:07 +08:00
QiuSWandClaude Opus 5 7c43dc6029 feat(server): 放开未开始采集任务的取消 (#297)
批量建单后无法中途叫停:删除只放行 failed,pending 一条都撤不掉,只能等设备
逐个跑完。图搜成功率并不高,批量越大越需要叫停。

新增 cancelled 终态,与 PurchaseTaskStatusCancelled 的既有约定对齐。

- pending → cancelled;running 拒绝取消。running 正在设备上操作 PDD,中途打断
  后页面停在哪一步不可控,会影响下一个任务归位(#292 已为此付过代价)。语义是
  「停止后续,当前这个跑完」。
- 取消与 Claim 的互斥点是同一条件更新。关键:Claim 领取时并不改 status,只写
  device_id 和租约,因此条件里必须带 lease_expires_at,否则会把刚被领走的任务
  误取消。
- 批量逐条更新、不包在一个事务里:一条因并发领取而跳过,不应回滚已成功取消的
  其它任务。超出单批上限时以 HasMore 如实上报,不静默截断。
- status 的 CHECK 约束只认旧五值,GORM 在 MySQL 上不改写既有 CHECK,按同文件
  ensureMySQLDirectSelectConstraint 的手法补幂等 DROP/ADD。cancelled 并入
  syncGuardSlots 终态分支以满足 active_slot / device_run_slot 两个约束。

实施:sonnet 子代理,改动经独立复核与重跑验证。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 16:28:07 +08:00
QiuSWandClaude Opus 5 9cdc9f325c fix(server): 「一键匹配颜色和尺码」未拿到结论的规格值重问一次 (#299)
规格匹配有三条路径,#295 只覆盖了采购侧两条。虾皮商品详情页「一键匹配」走的是
shopeeproduct.suggestMappings,仍然一次失败即放弃。实测虾皮 1528 的「黑色」在
PDD 8514 里唯一对应(黑色-冰块猫,9 个在售组合),却被判为「AI 未给出可靠建议」。

这条路径是批量调用,不能照搬 #295 的逐值重试:

- 只把没拿到可靠结论的 source 组成第二次 SuggestBatch,已有结论的不重问。
- 第二次沿用同一份 candidates。
- SuggestBatch 整体报错时也重试一次;两条重试路径互斥,总调用严格不超过 2 次。

判据一律未动:候选集校验、candidateValues 的 defensive 检查、
AutoConfirmMinConfidence 门槛、preserved 与确定性匹配分支全部保持原样。

实施:grok-4.6(派单试点),改动经独立复核与重跑验证。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 15:46:08 +08:00
QiuSWandClaude Opus 5 08d77bd982 fix(server): 规格匹配的 AI 调用重试一次,并区分无匹配与服务不可用 (#295)
AI 对同一输入会给出不同答案(deepseek-v4-flash 在 Temperature=0 下仍如此:
紫色/S 第一次答「未找到可靠的 PDD 规格」,第二次答「唯一匹配」),而匹配一个值
只调一次、失败即放弃,于是本该匹上的值因一次抽风永久留空。

- resolveSpecMatch 重试一次,上限 2 次调用。真失败(PDD 没有该颜色、白色有歧义)
  每次都会失败,重试更多只是浪费调用——实测连续三轮失败数稳定在 12。
- 区分 AI 明确「无匹配」与服务不可用,两者处置不同。此前都记成 failed,排查时
  分不开(#294 的遗留问题)。
- BatchSpecMatch 与 MatchArchiveSpecs 两条路径都接上。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 14:59:16 +08:00
QiuSWandClaude Opus 5 b2b8c0d1e0 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
2026-09-16 11:53:43 +08:00
QiuSWandClaude Opus 5 1274161a1d fix(server): 规格同步后匹配档案里所有未映射的规格值 (#293)
BatchSpecMatch 遍历的是 SYB 明细,只匹配明细需要的值。#290 把虾皮完整颜色尺码
同步进档案后,还没有订单的值一个都不会被尝试——等订单真来了仍要人工点一次匹配,
正是 #290 想消除的动作。

新增 MatchArchiveSpecs:对档案里每个未确认映射的值逐个匹配,确定性优先、AI 兜底。

- 只写映射,不创建采购。人工检查点不变。
- 逐值匹配没有另一半规格,无法校验完整可售组合,因此至少要求该值出现在某个在售
  SKU 里;完整组合仍由采购预检把关。
- 没有 SKU 证据时放行,与 aiMatchQualificationForDataset 的既有口径一致。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 11:53:43 +08:00
QiuSWandClaude Opus 5 5b3c8df3bc feat(server): 同步虾皮完整颜色尺码并一次性匹配 (#290)
图搜采集成功并自动关联后,拉取该虾皮商品的完整颜色尺码清单合并进档案,
再触发一次匹配。此后同一虾皮商品的其它颜色 SYB 订单无需再采集、再点匹配。

- 新增 shopeespec 叶子包,凭据只从 GOAUTO_ERPGO_BASE_URL/GOAUTO_ERPGO_APIKEY
  读取;错误文本不携带 URL,避免 apikey 流进日志。
- 写入档案前经 sybspec.StripAnnotations 剥离 【...】,与 SYB 明细同源,
  否则 confirmedMappings 查不到、映射全部落空。
- 标记记录同步时对着哪个 PDD 商品(spec_sync_pdd_product_id),不是布尔值,
  重新关联时自动失效。
- 已完整同步的商品再次图搜时返回 IMAGE_SEARCH_SPEC_SYNCED。
- 迁移只加列不回填:既有档案仍是从 SYB 明细增量累积的,不能假装已同步。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 10:33:53 +08:00
QiuSWandClaude Opus 5 a27db5f960 fix(server): 检出 SYB 规格塌缩并明确失败,不再静默共用同一映射 (#289)
sybimport.Parse 剥离 【...】,而括号里经常是真正的规格标识而非备注:
款号 白色【207A】、色号 黑色【M0059C1】、颜色组合 【深灰+淺灰】。剥离后
不同的虾皮规格塌缩成同一个 target_color,而它正是采购查映射的键,同键
即同映射,Agent 会为不同规格点击同一个 PDD 值。

线上实测(2026-09-16):颜色塌缩键 78 个、涉及 61 个商品、190 个原始
规格;尺码塌缩键 24 个;受影响明细 407 条。落在塌缩键上的采购任务 3 笔,
order_created 0 笔——至今没出事是因为每个塌缩键目前只有一条规格真正
下过单,不是设计上有保证。最高危的 6 个键全部尚未采购。

本次不改解析契约、不迁移数据,只让它明确失败:采购预览命中塌缩键时返回
SPEC_KEY_AMBIGUOUS 并提前返回,不再落到就绪判定;process_stage 一并拦截,
因为塌缩的键不能走 AI 匹配那条路——再匹配一次也只会为同一个键写一份映射,
而问题恰恰是多个规格共用这个键。

`[必须]` 解析原语下沉到新的叶子包 sybspec。sybimport 已依赖 purchase,
采购侧直接引用会成环;而两边各写一份镜像实现必然漂移,届时塌缩检测会拿错
半边去比,结论反而不可信。sybimport 保留别名转发,调用方无需改动。

`[必须]` 检测按虾皮商品加载全部明细,不是本次请求的那几条:塌缩是商品级
属性,只看请求内的明细会漏判。

TestBatchPreviewBulkLoadsAndNeverCallsAIMatcher 的查询数由 7 改为 8,新增
的是一次有界批量查询。该断言守的是不得出现 N+1,不是具体数字。

测试样本全部取自线上真实数据。app/goauto/sybimport 的 12 个失败先于本次
存在(#285),未新增。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 10:20:09 +08:00
QiuSWandClaude Opus 5 41996f33b2 feat(server): 图搜采集成功后自动触发规格匹配 (#287)
图搜是异步的:Agent 在手机上跑几分钟,采购员必须离开页面、稍后回来、
重新勾选、再点 AI 匹配。旧流程虽然全手工但一气呵成,图搜把它切成了
两段,中间靠人记着回来。本次在关联写入之后接一个钩子消除这个断层。

`[必须]` 只能接在这里。图搜批量结果对话框在任务创建后立刻弹出,那时
Agent 一个都还没执行,无从匹配。

autoLinkImageSearch 改为返回是否确实写入关联及受影响的 SYB 明细。
RowsAffected 为 0 表示乐观并发谓词拒写——任务创建后有人改过这条关联,
目标商品已不是我们找到的那个,此时匹配上去只会把错误结果写进档案,
因此不触发。

匹配失败只记录不冒泡:采集结果已经提交完成,不能因为这个可选增强让
SubmitResult 失败、让 Agent 以为采集没成功。

不自动创建采购。匹配只把明细推到“采购就绪”,创建采购仍由人点击——那是
整条链上唯一的人工检查点,因为图搜找商品、自动关联、AI 匹配规格三步
都没有人看过,而 #200 已取消 SYB 批量入口的置信度门槛,不能以“AI 没
把握会停下”来兜底。

标记文案相应扩展为“商品由图搜找到、规格由 AI 自动匹配,均未经人工
确认”,覆盖 SYB 商品页、采购任务列表与详情、虾皮商品详情抽屉。

app/goauto/sybimport 的 12 个失败先于本次存在(#285),未新增。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 17:51:42 +08:00
QiuSWandClaude Opus 5 174d92a1c9 fix(server): SYB 结构过滤合并为一条,要求同时包含 - 和 # (#286)
结构过滤原为两条 char 规则(- 和 #),RuleSet.Match 逐条 Contains、
命中即返回,是或关系。线上最近一次完整同步(syb_sync_run 541)里
- 命中 384、# 命中 148,合计 532 条被过滤;因 - 先判且命中即返回,
那 148 条是含 # 但不含 - 的。

改为一条,keyword 存必需字符集合 -#,匹配要求集合中每个字符都出现:

- 同时含 - 和 # → 过滤
- 只含其一 → 放行
- 都不含 → 放行

已向用户说明每次同步至少多放行 148 条,用户确认后实施。

`[必须]` migrations/migrate.go 的种子必须同步改成一条。该函数每次启动
都会 FirstOrCreate,若继续播种旧的两条,下次重启就会把迁移
1789500000000 合并掉的行重新建回来,过滤静默退回或关系。已加
TestFreshDatabaseSeedsOneStructuralRule 锁住。

关键词过滤不变,仍是子串匹配,并加测试防止字符集合语义误用到关键词上。

连带效果:真实样本 300斤牛奶絲圓領#A057 只含 #,不再被结构过滤命中后
落到关键词规则上仍被过滤——#269 把关键词称为 forward safety net,这是
它第一次真的接住东西。realworld 用例的统计由 char=5/keyword=0/kept=6
变为 char=3/keyword=1/kept=7。

app/goauto/sybimport 的 12 个失败先于本次存在(#285),未新增。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 17:15:40 +08:00
QiuSWandClaude Opus 5 95b5f05ae8 fix(#277): 图搜候选点击改瞄有文案子节点,关联不再被跨币种判据挡死
真机连续排查出三处缺陷,均已实测确认:

1) 候选卡片点不动。快照里 SnapshotNode.label 只取节点自身的 text/
   contentDescription,而 clickFreshDetailed 重新定位时用
   preferredOrDescendantLabel()(会往子孙里挖)。候选卡片是自身无文案的
   FrameLayout,两者算出的 label 一个是空串一个是标题文字,永远对不上,
   重找直接 TARGET_NOT_FOUND。改为瞄准卡片内最靠上的有文案子节点,由
   clickFreshDetailed 既有的向上走到可点祖先逻辑落到卡片容器上。
   卡片内文案的归属同时从 path 前缀改为几何包含——path 父子语义从未核对,
   几何包含已用真机 dump 直接验证。

2) 错误码拆分。IMAGE_SEARCH_NO_CANDIDATES 此前同时表示“结果页没出现”和
   “卡片点不动”,定位问题时完全分不清。拆为 RESULTS_NOT_READY /
   NO_CANDIDATES / CANDIDATE_CLICK_FAILED,并把 clickFreshDetailed 的
   result 与 reason 带进失败消息——这两个值本来就有,此前被丢弃。

3) 自动关联从上线起一次都没执行过。snapshot.PriceGuardSkipped 与
   shopee_product.currency != "CNY" 两处各自 return nil,而 priceSkipped
   的唯一来源就是 referenceCurrency != "CNY",两者是同一个条件;线上 26902
   个虾皮商品全部是 TWD。表现为图搜任务 completed、PDD 商品采集完整,但
   关联停在旧值,采购因此无法创建(订单 26091536MJHXG8 即如此)。
   币种不同只说明价格没法比,不说明关联不该建立,两件事已解耦。

   补偿控制是 image_search_linked 标记,已显示在商品页与采购视图。
   ImageSearchPriceAllowed 零调用方,加 [未接线] 标注,避免再被当成生效的
   防线;待引入可配汇率后再接。

测试夹具默认币种由 CNY 改为 TWD(线上真实币种),并新增跨币种必须关联、
同币种行为不变两个用例。此前夹具用 CNY,所以整套用例全绿而线上从未成功。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 16:27:15 +08:00
QiuSWandClaude Opus 5 750b8763dc feat(#280): 图搜确认后覆盖已有关联,去掉覆盖复选框
用户 2026-09-15 选定行为:去掉「覆盖已有关联」复选框,点击创建时若
所选商品中存在已关联的,弹窗说明将被覆盖,确认后覆盖。

原复选框是误导的:它只放行任务创建,落库时的 CAS 谓词仍要求
pdd_product_id IS NULL,所以勾上之后设备白跑 20-40 秒、关联一点不变,
界面还显示任务已完成。

服务端把关联写入从「必须为空」改为乐观并发:比对快照里的
OriginalPDDProductID,只要任务创建后没人动过这条关联就写入。直接去掉
谓词会让 Agent 执行的 20-40 秒变成静默吞掉人工改动的窗口;改为乐观并发
既满足覆盖需求,又让并发的人工改动继续胜出。该快照字段与
sameImageSearchLink 早已存在,此前未被关联路径使用。

前端确认框只在确实存在已关联商品时弹出并给出条数,一条都没有时不弹,
避免变成每次都要点掉的噪声;取消则整批不创建。

测试重写为四个用例:未变更时覆盖、任务创建后被改则放弃、创建时无关联
但执行中被抢先关联则放弃、常规无关联路径正常写入。原
TestAutoLinkImageSearchDoesNotOverwriteManualAssociation 断言的是被本次
取代的旧契约,已移除。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 15:23:34 +08:00
QiuSWandClaude Opus 5 93a175d04f fix(syb): 空格分隔的颜色尺码按逗号同规则拆分 (#284)
#274 删除 ambiguousPattern 以接受描述性颜色(黑色+白色 簡約親膚),
意图正确,但副作用是「黑色 XL」也被整条当成颜色、尺码丢失且判为
success。success 的行不进 AI 解析队列(ai_parse_batch.go 只选
uncertain/failed),因此没有任何机制会纠正它们。2026-09-15 线上有
152 条 parse_status='success' 且 target_size='' 的明细。

不恢复被删的旧闸——那会退回 #274 之前重新拒绝描述性颜色。改为把
空格当作与逗号同等的分隔符,复用逗号分支已有的规则:恰好一个空格
分隔 token 带明确尺码特征时才拆分,不构成猜测。

- 多于一个 token 命中尺码特征时不拆,对齐逗号分支「两侧均像尺码则
  无法安全识别颜色」。
- 没有尺码 token 时原样保留为颜色,#274 的意图不受影响。
- 一并修正 parse.go 中停留在旧 uncertain 描述的文档注释。

新增 parse_whitespace_split_test.go 锁住完整对照表,含 #274 描述性
颜色不被退回的回归。

`[必须]` app/goauto/sybimport 存在 12 个先于本工单的失败用例,来自
#272/#274 改变契约后未更新的旧断言,已记录为 #285。本次提交前后失败
集合逐条比对完全一致,未新增。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 14:23:02 +08:00
QiuSWandClaude Opus 5 c30b8c4624 fix(#277): 调度垫底改由服务端排序实现,并记录真机判据核对结果
客户端 TaskDispatchPolicy 无法实现「采集 > 图搜」:图搜任务就是
source='image_search' 的普通采集任务,由同一个 /tasks/next 端点下发,
Agent 调用时服务端已经选好了行。原先新增的 hasImageSearchWaiting 参数
和 CHECK_IMAGE_SEARCH 分支从未被传入 true,是不可达的死代码,且注释
声称的优先级与代码顺序相反。

改为在 task.Service.Next 的两处 ORDER BY 加入来源降级:

- 设备专属队列:来源降级作为主键。
- 共享候选池:来源降级排在 device_id 之后——指派设备是人的显式选择,
  必须继续优先于来源降级,否则未指派的采集任务会抢在运营明确路由到
  本机的图搜任务前面。

另外:

- #280 的批次上限去掉 env 覆盖。确认对话框要在提交前禁用按钮,前端
  必然持有同一个数字,env 覆盖只会让两侧静默不一致。
- #277 判据常量按 2026-09-15 真机 dump 核对:首页入口、图搜页四段文案、
  最近项目网格几何、结果页判据全部命中;已记录「唯一可点」过滤为必需
  (个人中心根节点带同一 content-desc 且覆盖全屏)、取景提示文案与移植
  来源不符、相册网格含视频、以及重试弹窗未验证。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 10:51:10 +08:00
QiuSW 2ddfd52f5d Merge branch 'fix/b280-imagesearch' into integrate/imagesearch 2026-09-15 10:36:34 +08:00
QiuSWandClaude Opus 5 c2c0bae056 feat(purchase): surface image-search-linked marker in purchase views (#279)
Item 5 of #279 required bringing the image-search auto-link annotation
into the purchase-facing views, display-only, without gating anything.

- BatchPreviewItem and AdminTaskItem now carry imageSearchLinked, sourced
  read-only from shopee_product.image_search_linked (already loaded in
  the batch preview dataset; bulk-loaded for the task list/detail views).
- Batch purchase preview dialog and purchase task list/detail now show a
  "图搜未核" tag plus a restrained summary hint when applicable.
- Does not touch Eligible/ReasonCode/Reason/NextAction or the #283
  mappingTargetsValid gate; regression test confirms an ineligible row's
  outcome is unchanged when imageSearchLinked is true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 10:35:22 +08:00
QiuSWandClaude Opus 5 b7babfba7e feat(#280): cap image-search batch size and warn about device time
Server: BatchCreateImageSearch now rejects a batch whose deduplicated
task count (one task per shopee_product, after merging SYB detail
rows) exceeds imageSearchMaxBatchTasks (50), returning
IMAGE_SEARCH_BATCH_TOO_LARGE with a readable Chinese message instead
of silently truncating. The limit is overridable via
GOAUTO_IMAGE_SEARCH_MAX_BATCH_TASKS. #277's scheduling floor (采购 >
采集 > 图搜) isn't implemented yet, so this cap is the only guard
against a large image-search batch starving manually started
collection/purchase tasks.

Web: the batch confirm dialog now shows the deduplicated task count
and an estimated device-occupation time range (20-40s/task), and
disables the submit button with an explanation when the batch would
exceed the same limit, instead of letting the request fail after
submit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-15 10:35:21 +08:00
QiuSW a3cf98940a fix(#279): only auto-link active pdd products 2026-09-15 09:51:39 +08:00
QiuSW cda1978bc6 test: prevent image snapshot payload leakage (#278) 2026-09-15 09:37:01 +08:00
QiuSW 90c1668fe3 test: cover image search auto-link guards (#279) 2026-09-15 09:01:10 +08:00
QiuSW 36d1f82ce2 feat: filter image-search linked shopee products (#279) 2026-09-15 08:53:37 +08:00
QiuSW 1c998911be feat: auto link completed image search results (#279) 2026-09-15 08:50:19 +08:00
QiuSW 45437a9214 feat: record cross-currency image search price guard (#278) 2026-09-15 08:44:39 +08:00
QiuSW 42b5312198 feat: support image search identity handoff and safe payload (#278) 2026-09-15 08:42:11 +08:00
QiuSW 7b540f3163 feat: create and dispatch image search tasks (#278) 2026-09-15 08:41:42 +08:00
QiuSW ffa5fc0710 fix: add idempotent image snapshot column migration (#278) 2026-09-14 18:02:22 +08:00
QiuSW 66ab36c88c feat: expose image search snapshot in task payload (#278) 2026-09-14 16:49:35 +08:00
QiuSW 35b69fc25a fix: narrow image search migration (#278) 2026-09-14 16:31:07 +08:00
QiuSW b4dc7fc9ae feat: track image search links and preserve manual overrides (#279) 2026-09-14 16:08:55 +08:00
QiuSW baa9408a36 feat: allow image search collection task source (#278) 2026-09-14 16:05:01 +08:00
QiuSW c137f5072f fix: keep purchase readiness consistent with confirmed mappings (#283) 2026-09-14 14:42:11 +08:00
QiuSW b83f20c6b7 fix(syb): accept descriptive color specs (#274) 2026-09-14 10:52:02 +08:00
QiuSW e0ed05d15a fix(syb): accept reliable single spec dimension (#274) 2026-09-12 12:01:15 +08:00
QiuSW 8163dd10eb fix(ai): match size by standard token (#273) 2026-09-12 11:46:46 +08:00
QiuSW aed295c8c1 fix(syb): reduce parse status to success or failed (#272) 2026-09-12 11:36:43 +08:00
QiuSWandClaude Opus 5 c14f5f528b fix(server): keep agent diagnostics on order-check failures #271
normalizeOrderUnknownFailure replaced the agent's message with a canonical
sentence, so the diagnostic #210 added never reached the database. Across the
61 PURCHASE_ORDER_PAYMENT_REPEATED failures on production between 2026-09-07
and 09-11, not one carried paymentBackAttempts or
consecutivePaymentSamplesAfterBack, which left three very different causes
indistinguishable: the Back never left the payment activity, the page rendered
slower than the ~1.1s window the agent allows, or the page was already the
order page and no marker matched. Those need different fixes, and there was no
way to tell which one to make.

The canonical sentence stays authoritative and leads the message — agent text
is free-form and the wording per error code has to stay stable — but the agent
evidence is now appended behind it, flattened to one line (control characters
and U+FEFF removed, whitespace collapsed) and truncated to the column's 1000
runes with the canonical part kept whole. The sibling "failed" branch already
stored agent text verbatim, so this adds no new trust assumption.

That branch also only wrote the failure onto the task, never the attempt, so
every attempt it produced carried a NULL error_code and was invisible to
attempt-level statistics — the spec-panel failures on production are exactly
that. It now writes both.

This restores observability only; it does not reduce how often
PAYMENT_REPEATED happens. Tuning the agent's sampling window is deliberately
left out until the next production failure shows real values.

Wiki updated first: Android-Agent-API-Contract@ba259178.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-12 11:13:02 +08:00