防重入(本工单最高风险项,审核阶段指出 TaskExecutionMutex.tryAcquire 对同一 ID 可重入): OrderBackfillGuard 用 AtomicBoolean.compareAndSet(false, true) 做原子闸门,再取共享 TaskExecutionMutex,取锁失败时回滚 active,finally 释放。测试 mutex occupied rejects and simultaneous double tap has one winner and finally releases 使用 CountDownLatch + Executors 构造真并发,不是顺序调用。问题已正确解决。
安全点击边界:BackfillPagePolicy.safe() 三重判据——控件自身可点可见有尺寸、子树不含 11 个高危词、且不与屏幕上任何含高危词的可见控件几何重叠。第三条比工单要求更严,直接以坐标重叠排除,覆盖「确认收货与查看物流同行仅隔 240px」的实测风险。另有 validate() 在遇登录/验证码/风控词时立即停止。测试 dangerous nodes ancestors and overlapping actions never become click targets 覆盖。
时序自检:OrderBackfillWindow.observe() 要求连续观察至少 5 单(ORDERING_SAMPLE)、期间时间严格递减且无缺失,才允许按时间提前停止;一旦时间回升或出现缺失即禁用提前停止,改为扫至内部上限并输出「列表非严格倒序,已改为有界扫描,可能未覆盖全部」。源码注释「a sorted prefix cannot prove the unvisited tail is sorted」说明理解了该判据的边界。测试 time reversal and missing times disable early stop、unordered list scans to internal cap and reports incomplete 覆盖。
跨单残留:每单只在「唯一后缀 + 唯一订单号 + 至多一个时间」时才形成条目,BackfillDetailReader.finish() 清空全部累加器。测试 ambiguous and noncanonical suffixes are skipped 覆盖。
下单时间取值:正则原样复用,ORDER_TIME 只认「下单时间/创建时间」前缀,不会误取「拼单时间」。测试 expanded detail reads order time not group time across frames 覆盖跨帧场景。
隐私:后缀正则 _cg[1-9][0-9]*(?![0-9A-Za-z_0-9]) 排除前导零与全角数字;测试 payload contains only suffix order and optional RFC3339 time 断言上传载荷不含地址全文、收件人或手机号;untagged order never becomes retained or uploaded candidate 断言无后缀订单不留存不上传。
错误分类:测试 permanent business errors never retry regardless of server retryable flag —— 永久性业务错误码不重试,且不信任服务端返回的 retryable 标志,比工单要求更保守;瞬时错误最多 3 次并使用稳定 transport request id。
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
2026-09-10 范围变更:拆阶段,本工单收敛为「只写本地」
用户决定把回填拆成两个阶段,本工单只做第一阶段,并且在现有代码基础上更新,不重做。原话:「或者我们拆开来,agent增加回填,把从pdd读取的订单号等数据先写入agent的sqlite数据库。这样改动比较小。」「修改#242,要在现有代码基础上来更新」
变更内容
edd1cb1基础上修改,不推倒重做。 该提交已实现扫描、解析、互斥与防重入、安全点击边界、时序自检,全部保留。经核实OrderBackfillScanner.kt不引用任何网络层(无 import network / Upload / http),上传逻辑独立在OrderBackfillUpload.kt(92 行),因此本次改动主要是把该上传步骤替换为本地写入。本阶段新增的硬性约束
addressSuffix、pddOrderNo、orderSubmittedAt。目的是让第二阶段成为「读表 → POST」,不产生数据结构返工。字段命名与语义不得自行发挥。_cgNN,但落库只允许存后缀;收件人姓名、手机号、地址全文一律不得写入本地库、不得进日志。fix/249-local-purchase-diagnostics(659bbc6)重建分支。 理由:现有 #242 分支基于 main(versionCode 72 / 0.9.59),而 Android 修复链已到 80 / 0.9.67,设备上装的就是 0.9.67,在旧基线出包会丢失 #238、#243–#249 全部修复;且两条链在GoAutoAccessibilityService.kt与AgentForegroundService.kt两个文件上重叠,必须解决冲突。本阶段的验收重点
本阶段交付的是验证价值而非业务价值:Admin 仍会显示「尚未取得订单号」,积压的无单号真单不会因此减少。真正要确认的是页面识别判据在真机上是否成立——
list()、cards()、detail()、expansion()四个判据至今一次真机都没跑过,其中list()要求存在 label 为「全部」且 selected 为 true 的节点,该属性是否被 PDD 置位从未验证。这是整条链路能否成立的前提,也是拆阶段的主要理由。2026-09-08 评审更新:内部系统精简方案
用户要求“内部系统,简单易用,不要复杂门禁”,并授权更新本工单正文。本版收敛为人工点击回填、输入天数、自动扫描、小批提交、显示结果;不增加产品内审批、二次确认、后台任务系统或复杂配置。保留设备归属、冲突不覆盖、设备互斥和禁止支付/确认收货等必要边界。
本次只更新方案,不代表批准实施、原型确认、迁移或真机操作。代码核验基线 main
fdd26af;Android 已发布分支7ac1355的互斥锁与历史缓存相关文件与此基线一致。原工单真机现象与积压数量是创建时记录,本轮未重新验证,不作为实时统计。Gitea MCP 当前不可用,回退项目安全配置提供凭据的 Gitea API;未输出或写入凭据。
来源与目标
2026-09-08 用户提出 Agent 端回填入口的具体形态,原话:
以及互斥与时间来源的确认:
目标:在 Agent 采购记录页提供人工触发的回填入口,扫描 PDD「我的订单-全部」,读取收货地址含
_cgNN的订单的订单号与下单时间,更新本地缓存并提交服务端。前置依赖
依赖 #241(服务端批量回填端点)先行落地,本工单调用该端点。#241 未完成前本工单保持待实施。
当前事实(真机实测,设备 192.168.0.173:36805,PDD 8.23.0)
页面形态与字段位置:
_cgNN)与订单编号,二者均为明文可读。clickable=true,位于「商品快照」行右侧)。展开后出现:https://mobile.yangkeduo.com/orders.html可直接进入 PDD App(落NewPageActivity),无需经过首页或个人中心。原实测type=2命中「打包中」、type=4命中「评价」,这是分类/Tab 线索,不是列表分页证据;不可用该参数递增实现翻页。进入后必须识别「全部」列表。现有正则可原样复用,实测均命中且语义正确:
ORDER_TIME只认「下单时间/创建时间」前缀,不会误取「拼单时间」。实施不得改为「抓页面第一个日期」之类的宽松写法。互斥有现成机制可直接复用:
AgentForegroundService已有TaskExecutionMutex,且采集 tab 的「采集当前拼多多商品」按钮就是同一模式的人工触发动作:回填复用同一把锁,但 tryAcquire 对同一 ID 可再次成功。固定预留 ID 本身不能防止重复启动,必须另有原子防重入/运行标记,并在 finally 释放;不改既有任务锁的全局语义。
UI 位置有对称先例:
TaskHistoryFragment采集模式下,搜索按钮右侧已有「采集」按钮(contentDescription = "采集当前拼多多商品")。采购模式下同一位置新增「回填」按钮与之对称。本地缓存已有对应字段:
persistence/TaskHistoryCache.kt使用 SharedPreferences,已缓存pddOrderNo与orderSubmittedAt,不是 SQLite。SQLite 的 PurchaseTaskStore 承载执行与 outbox,不能与历史缓存混为一谈。按本次精简方案只复用历史缓存,不新建 SQLite 回填表,不篡改原执行/outbox。积压规模: 服务端当前 21 个
order_result_unknown任务缺订单号。范围
uiautomator dump取不到 idle 状态,实测四次全部失败;Agent 走无障碍服务不受此限制。)非目标
安全边界(硬性,不可放宽)
扫描过程会经过订单列表与订单详情页,其中存在高危控件。以下必须永久排除为点击目标:确认收货、申请退款、催发货、去支付、立即支付、提交订单。
实测「确认收货」与「查看物流」在同一行、间距仅 240px,误点后果不可逆。允许点击的目标仅限:订单卡片、「展开」、返回。
不执行支付、不创建订单、不修改任何订单状态。整个流程为只读。
方案与设计证据
复用采购/采集现有按钮、对话框和反馈样式,提供可审核的轻量设计,覆盖输入、运行进度/停止、空结果、部分成功、冲突和失败;不新增独立后台页面。按本次仓库规则,独立功能的轻量原型须记录可访问链接与版本,经用户确认后才能写生产代码;本次“更新工单”不视为原型确认,不在本次制作原型。
原始用户提到 SQLite,代码事实为 SharedPreferences 历史缓存;本版按用户要求的简单易用方案明确复用缓存,不追加 SQLite 迁移。
验收
文档影响
实施时更新线上 Business-Rules-and-Glossary 与 Architecture-and-Code-Map:人工订单扫描、设备互斥、防重入、安全点击和缓存处理是长期新增行为。与 #241 共用 API 契约,若客户端实施发现契约需调整,先回到 #241 对齐,不暗自扩展。在线回读后同步/检查镜像;本次只更新工单,不修改 Wiki。保留已有无关工作区改动。
待确认
天数口径已确认为滚动 N×24 小时,不再是待确认项。内部扫描条数/耗时限值作为实现参数记录,不要求用户配置技术参数;触及内部上限时结果必须明确显示「未完整扫描」,不得让用户误以为已扫完。原“7 天硬上限”建议已撤回为非既定要求(用户明确要求内部系统不加复杂门禁)。原型及开始实施仍待用户确认,当前不实施。
实施分工
本工单已建单,用户明确要求暂不实施(原话「建工单,先别做」)。实施启动前需用户再次确认,且需 #241 先行完成。
依赖说明:本工单依赖 #241(服务端批量回填端点)先行落地。#241 完成前保持待实施。
两个工单均已按用户要求建单但不实施,启动实施需用户再次确认。
2026-09-08 用户确认后的调整(Claude 复核 Codex 评审版)
Codex 评审版对本工单的两处代码事实纠错已在基线 main
fdd26af上独立验证,均成立:TaskHistoryCache使用getSharedPreferences,不是 SQLite。用户原话提到 SQLite,代码事实为 SharedPreferences;SQLite 的PurchaseTaskStore承载执行与 outbox,不可混用。TaskExecutionMutex.tryAcquire实现为activeTask.compareAndSet(NO_TASK, taskId) || activeTask.get() == taskId,对同一 ID 可重入。若按原工单用固定预留 ID,用户快速连点两次「回填」将两次返回 true,导致两个扫描并发驱动 PDD。必须另加原子防重入标记,并在 finally 释放。本次调整
该验证无法用 adb 完成:「全部」页存在待付款倒计时持续动画,
uiautomator dump取不到 idle 状态,本轮实测四次全部失败;Agent 走无障碍服务不受此限制,故列为实施内的第一步而非独立工单。已撤回的意见
原提「7 天用户可见上限」已撤回,属于用户明确不要的门禁,且评审版内部上限已提供兜底。
本次仅调整工单方案,不实施。实施仍需 #241 先行完成,并经用户确认原型与开工。
2026-09-08 前置依赖 #241 已完成,本工单开工
#241 已实施并通过审核,待验收。分支
feat/241-order-backfill-endpoint,提交290a17e→72b8b5d→ca7f768→482ba34,基于fdd26af。尚未合并到 main,因此本工单基于 #241 分支顶端开发。#241 实际交付的契约(本工单据此对接)
端点:
POST /api/agent/v1/purchase-tasks/order-backfillAuthorization: Bearer <Device Token>,沿用RequireAgentHTTPS。无需 Admin JWT、claim、start 或 attempt。{requestId, items:[{addressSuffix, pddOrderNo, orderSubmittedAt?}]},每批 1~50 条。addressSuffix必须是规范形式_cg<任务号>,服务端用ParseAddressSuffix回比校验,_cg07、_cg+7、全角数字等非规范形式一律拒绝。Agent 必须自行从地址中提取该后缀,只上传后缀,不上传地址全文。orderSubmittedAt为带时区 RFC3339/RFC3339Nano;缺省时服务端回落该任务的irreversible_at。index、taskId、result、code、status、statusVersion、pddOrderNo、orderSubmittedAt、timeSource、retryable。timeSource取值page/irreversible_at/existing_unknown,客户端展示必须区分页面读到的真实值与回落估算值。业务错误码:
PURCHASE_BACKFILL_SUFFIX_INVALID、PURCHASE_TASK_NOT_FOUND、PURCHASE_BACKFILL_DEVICE_MISMATCH、PURCHASE_STATE_CONFLICT、PURCHASE_INVALID_REQUEST、PURCHASE_ORDER_TIME_INVALID、PURCHASE_ORDER_TIME_MISSING、PURCHASE_BACKFILL_ORDER_CONFLICT、PURCHASE_BACKFILL_BATCH_CONFLICT、PURCHASE_BACKFILL_ORDER_ALREADY_USED、INTERNAL_ERROR。逐条独立事务、逐条独立结果,合法条目不因其他条目失败而回滚;同任务同订单号重复提交返回
already_backfilled。追加范围
INTERNAL_ERROR等瞬时错误才允许有界重试。记录:相邻问题,本工单不修
android/.../persistence/PurchaseOutboxUploader.kt的flush()对永久性业务错误同样会无限重试(submit抛出即停在该条,markUploaded不执行,下次 flush 继续同一条)。该 outbox 服务于采购结果上传,与本工单的回填提交是不同路径,因此不在本工单范围内修改,仅记录。若后续要修,另建工单。本条记录的目的是:实施本工单时不要复用
PurchaseOutboxUploader来提交回填结果,避免继承同一缺陷。已完成并推送,待验收。
实现
分支
feat/242-agent-order-backfill(独立 worktree),基于 #241 分支顶端482ba34(#241 尚未合并到 main)。edd1cb1 feat(android): add read-only order backfill with bounded scanning (#242)—— 11 个 Android 文件。ba0e48b—— Wiki 镜像同步。审核验证(Claude 独立执行)
./gradlew :app:testDebugUnitTest --rerun-tasks:337 通过,0 失败,0 错误,0 跳过,新增 16 个用例。./gradlew :app:assembleDebug:BUILD SUCCESSFUL。git diff --checkclean;git diff 482ba34 HEAD --name-only无server/或web/命中。PurchaseOutboxUploader(全 diff 无该符号),未继承其无限重试缺陷。逐项核对结果
防重入(本工单最高风险项,审核阶段指出
TaskExecutionMutex.tryAcquire对同一 ID 可重入):OrderBackfillGuard用AtomicBoolean.compareAndSet(false, true)做原子闸门,再取共享TaskExecutionMutex,取锁失败时回滚active,finally释放。测试mutex occupied rejects and simultaneous double tap has one winner and finally releases使用CountDownLatch+Executors构造真并发,不是顺序调用。问题已正确解决。安全点击边界:
BackfillPagePolicy.safe()三重判据——控件自身可点可见有尺寸、子树不含 11 个高危词、且不与屏幕上任何含高危词的可见控件几何重叠。第三条比工单要求更严,直接以坐标重叠排除,覆盖「确认收货与查看物流同行仅隔 240px」的实测风险。另有validate()在遇登录/验证码/风控词时立即停止。测试dangerous nodes ancestors and overlapping actions never become click targets覆盖。时序自检:
OrderBackfillWindow.observe()要求连续观察至少 5 单(ORDERING_SAMPLE)、期间时间严格递减且无缺失,才允许按时间提前停止;一旦时间回升或出现缺失即禁用提前停止,改为扫至内部上限并输出「列表非严格倒序,已改为有界扫描,可能未覆盖全部」。源码注释「a sorted prefix cannot prove the unvisited tail is sorted」说明理解了该判据的边界。测试time reversal and missing times disable early stop、unordered list scans to internal cap and reports incomplete覆盖。跨单残留:每单只在「唯一后缀 + 唯一订单号 + 至多一个时间」时才形成条目,
BackfillDetailReader.finish()清空全部累加器。测试ambiguous and noncanonical suffixes are skipped覆盖。下单时间取值:正则原样复用,
ORDER_TIME只认「下单时间/创建时间」前缀,不会误取「拼单时间」。测试expanded detail reads order time not group time across frames覆盖跨帧场景。隐私:后缀正则
_cg[1-9][0-9]*(?![0-9A-Za-z_0-9])排除前导零与全角数字;测试payload contains only suffix order and optional RFC3339 time断言上传载荷不含地址全文、收件人或手机号;untagged order never becomes retained or uploaded candidate断言无后缀订单不留存不上传。错误分类:测试
permanent business errors never retry regardless of server retryable flag—— 永久性业务错误码不重试,且不信任服务端返回的 retryable 标志,比工单要求更保守;瞬时错误最多 3 次并使用稳定 transport request id。内部上限:200 单 / 10 分钟 / 每详情最多 6 轮滚动 / 连续无进展最多 3 次。
未验证(关键,验收前必须知道)
所有页面识别判据均未经真机验证。 实施方无法连接手机(本工单禁止 adb),
list()、cards()、detail()、expansion()四个判据都是基于工单记录的实测证据推导的,未在设备上跑通。具体风险:list()要求存在label == "全部"且selected == true的节点。审核阶段的真机 dump 只确认了「打包中」被选中时该 tabselected=true,未验证「全部」tab 被选中时是否同样置位。若 PDD 不置该属性,扫描会直接不启动。cards()依赖商品区可点击容器与按钮行不发生几何重叠。审核阶段单次 dump 显示商品区 y=710-974、按钮行 y=1114-1207,确实不重叠,但这是单一样本、单一订单状态。expansion()要求同时存在「订单编号」节点与唯一的「商品快照」节点,且「展开」位于两者之间。审核阶段 dump 与此一致。上述判据全部 fail-closed(识别不到即停止并报告未完整扫描),不会误操作,但意味着功能可能在真机上直接跑不起来,真机联调是可用性的前置条件。
一个实际使用上的取舍
ORDERING_SAMPLE = 5意味着必须连续读到 5 单带时间且严格递减,才允许按天数提前停止。若指定天数内订单不足 5 单,样本凑不齐,扫描会一直进行到内部上限(200 单或 10 分钟)。即「填 2 天」不等于「很快结束」。这是保守设计的代价,不是缺陷,但使用前应知晓。文档
线上
Business-Rules-and-Glossary已更新并回读,revision1eb380d07183157a8430c7ced868479e72fbdfe3,新增人工触发只读扫描、三者互斥与防重入、时序自检与未完整标记、只读安全边界、跳过无后缀订单与隐私边界等长期规则。harness.py sync与sync --check通过。未执行
origin/feat/242-agent-order-backfill。依赖提醒
本分支基于 #241 分支,两者需一并合并;单独合并 #242 会因缺少服务端端点而不可用。
Agent:采购记录页回填入口与 PDD 订单扫描to Agent:采购记录页回填入口与 PDD 订单扫描(阶段一:只写本地)2026-09-10 阶段拆分决策记录
用户确认把回填拆为两阶段,本工单收敛为阶段一「只写本地」,并要求在现有代码基础上更新。
为什么这么拆是合理的
风险最大的部分恰好不依赖服务端。 目前唯一未验证的是页面识别判据(
list()/cards()/detail()/expansion()),这四个判据与服务端无关,本地就能验。而 #241 引入的
PurchaseTask.BeforeSave全局钩子会改变存量正式采购流程(影响lifecycle.go的order_created回传、manual.go的 ResolveUnknown/Cancel、reset.go共 6 处既有保存点),且只在 SQLite 上验证过,MySQL 行锁行为未验证。拆开正好把这个风险挡在本阶段之外,服务端零改动。改动量评估(已核实)
OrderBackfillScanner.kt不引用任何网络层,扫描与上传本就解耦。OrderBackfillUpload.kt,共 92 行。本阶段明确不解决的事
数据只落在单台设备本地,Admin 依然显示「尚未取得订单号」,已积压的无单号真单不会减少。按 AGENTS.md,服务端才是事实来源,本地库只能算缓存。本阶段交付的是验证价值,不是业务价值。
这一点必须在验收时如实记录,不得把阶段一的完成表述为回填功能已可用。
阶段二(另议,不在本工单)
待 #241 具备安全部署条件后,新增「读本地表 → 调用 #241 端点 → 按逐条结果更新本地状态」,并处理永久性/瞬时业务错误的重试区分。届时若本阶段的本地记录结构与 #241 请求体对齐,该阶段应只是薄薄一层,不需要改动扫描与解析。
??????:??????????????? TaskHistoryCache/SharedPreferences,??? SQLite;??????????????;??????? addressSuffix?pddOrderNo?orderSubmittedAt,???????????;?????????????????????????,????????/??/?????????????;???????????????????????????;???????????????? outbox;???????????????????????????????,???????????,???????????????????#241 ???????????