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
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
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
Unrelated to #269. These three pages were changed online by other work and
the local mirror had drifted (02 and 03 from 2026-09-10, 06 from 2026-09-04).
The sync that mirrored the #269 pages regenerates every mapping, so it brought
these along; they are committed separately to keep the ticket's own commit
limited to its files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
Wiki updated online first, then mirrored:
- SYB-ERP-Interface-Contract@f1792b7f — filter order after shop admission,
OR-not-AND with the 8498/6392/6386/8504 figures from the 2026-09-11
production check, empty variationSku kept, per-rule hit counts, and the
no-add/no-delete rule for structural criteria.
- Android-Agent-API-Contract@38869285 — the syb-product-filters endpoints,
the server-side refusal of char add/delete, and the live lastHitCount
requirement for the disable confirmation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
Three defects that only a real MySQL run could show.
The table had no version-local migration. Adding the model to
migrations.Migrate covers databases built from scratch, but every existing
one already has the older versions recorded in sys_migration, so that list
never runs again there. cmd/migrate's post-migration table check caught it:
"迁移后仍缺少表:syb_product_filter". The new version file creates the table
and seeds the same eight rules with FirstOrCreate on the normalized key, so
running both paths against one database cannot duplicate them.
Keyword and NormalizedKeyword were typed text, which MySQL refuses to put in
a key specification without a prefix length (Error 1170), so the unique index
could not be built at all. They are sized 200 now, matching how SYBShop sizes
its own matching key. SQLite accepts text in an index, which is why the whole
test suite passed while the real migration failed.
The 只看启用 filter is removed rather than kept: hiding a disabled structural
rule would conceal the one state on this page that matters most, since a
disabled "#" means roughly 65% of 档口 rows start entering the system again.
Keyword search stays.
Verified against the local MySQL database: migration applies, eight rules
present with traditional Chinese preserved (印花大學T normalizes to 印花大學t),
menu row created under 采采管理.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
Registering a module in access.GoAutoModules() does not make its page
reachable on an existing database. sys_menu rows are written by the
versioned migration that first created them, and that version is already
recorded in sys_migration everywhere — it never runs again. Startup only
reconciles the casbin API matrix, not menus.
So on a fresh database the page appeared and on every real one it did not:
the table, the API and the route were all present with no way to navigate
there. This adds a migration that upserts the entry under 采采管理 beside
「SYB 店铺」 and binds it to both 管理员 and 采购员.
Verified by deleting the row from an already-migrated database and
re-running migrate: 新执行 1 个,跳过 42 个, row restored with both roles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
The samples are the ones the production database actually holds: 档口 codes
like "新寮-雅伊阁大码女装#301" and "DD#004" must be filtered, while own
codes like "PDD0582", "my238", "1" and "引流款" must not, and an empty
variationSku is a product without a supplier code rather than a 档口 item.
The test also asserts the two structural rules record different hit counts.
That is the property the previous aggregate-per-kind write could not hold,
and the disable-confirmation dialog depends on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
UpdateHits wrote one aggregate per kind, so every structural rule stored
the same number. The disable-confirmation dialog quotes that number
("停用「#」后…约 N 条明细会恢复入库"), and on real data "#" matches 8498
rows while "-" matches 6392 — the dialog would have warned with a figure
2100 rows off for one of them.
Match now returns the rule that matched instead of a char/keyword pair,
so the caller can attribute each hit to one rule, and UpdateHits writes
every rule's own count. Rules that matched nothing are written as 0 so a
previous run's number is never left behind.
The management page is reformatted to the repo's usual Vue layout and
gains the search / 只看启用 / 重置 controls the approved prototype shows.
Cancelling the disable confirmation now just reverts the switch instead
of throwing an unhandled rejection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F