fix(server): 订单核单失败保留 Agent 诊断证据(PAYMENT_REPEATED 根因分析前提) #271

Open
opened 2026-09-12 10:55:43 +08:00 by ila · 1 comment
Owner

原始需求

来源:用户,2026-09-12 会话。排查「线上采购 PAYMENT_REPEATED 占比回升」时发现根因分析无法进行——Agent 上报的诊断证据没有落库。

用户确认先做服务端这一条,拿到真实证据后再决定 Agent 侧参数怎么改。

当前事实(代码事实 + 线上数据,2026-09-12 核验)

问题:同一个 switch 的两个分支对 Agent 证据处理相反

server/app/goauto/purchase/lifecycle.go 约 386-401 行:

case "order_result_unknown":
    failureCode, failureMessage, failureErr := normalizeOrderUnknownFailure(req.ErrorCode, req.ErrorMessage)
    ...
    a.ErrorCode, a.ErrorMessage = &failureCode, &failureMessage   // Agent 证据被替换
    t.ErrorCode, t.ErrorMessage = &failureCode, &failureMessage

case "failed":
    t.ErrorCode = &req.ErrorCode
    t.ErrorMessage = &req.ErrorMessage                            // Agent 证据原样保留

normalizeOrderUnknownFailure(约 450 行)无条件返回固定文案,Agent 的 message 只用来判断非空:

canonicalMessage, ok := allowedOrderUnknownFailures[code]
if !ok || message == "" {
    return "", "", fail(CodeInvalidRequest, "订单核单失败阶段无效")
}
return code, canonicalMessage, nil

Agent 侧确认在发送

PurchaseRehearsalExecutor.kt:154 把 live.lastOrderReadFailure 的 code 和 message 原样上报。PurchaseLiveAutomation.kt:410-417 构造的消息带诊断后缀:

"支付页安全返回后持续无订单证据,已停止自动核单[paymentBackAttempts=N;consecutivePaymentSamplesAfterBack=M]"

purchase_task_attempt.error_message 与 purchase_task.error_message 均为 size:1000,空间充足。

线上数据佐证

  • 2026-09-07 ~ 09-11,PURCHASE_ORDER_PAYMENT_REPEATED 共 61 次,error_message 全部是固定文案 支付页重复出现,已停止自动核单,零条带诊断后缀。
  • 同期占比:09-07 100%(15/15)、09-08 44%(12/27)、09-09 20%(6/30)、09-10 61%(22/36)、09-11 43%(6/14)。
  • 这 61 次中 irreversible_at 置位 61/61(订单极可能已创建),pdd_order_no 恢复 0/61。
  • 对照:同一晚任务 170/171 的规格失败走 "failed" 分支,证据完整落库:[type=UNKNOWN;scrollables=1;headings=2;options=8;summary=false;quantity=true;orderAction=false;pageEvidence=true]。

由此导致的分析盲区

#210 加入的 paymentBackAttempts / consecutivePaymentSamplesAfterBack 从未进入数据库,因此无法区分以下三种情形,而三者的修法完全不同:

  1. Back 未生效(按了但没离开支付 Activity)
  2. 页面渲染慢于容忍窗口(当前约 500 + 3×200 = 1.1 秒,而整体预算 60×200 = 12 秒)
  3. 页面已是订单页但关键词未匹配(ORDER_CONTEXT_MARKERS / UNPAID_MARKERS 未命中)

方案

保留固定文案作为面向人的稳定摘要,在其后附加 Agent 证据。

normalizeOrderUnknownFailure 改为返回 canonicalMessage 与 Agent 证据的拼接:

  • 固定文案仍由 allowedOrderUnknownFailures[code] 决定,不采用 Agent 文本作为主文案(避免把不可信自由文本当作权威描述,也保持界面文案稳定)。
  • Agent 的 message 作为证据附加在固定文案之后。
  • [必须] 附加前做边界处理:去除换行与控制字符、压缩连续空白、按 error_message 的 size:1000 截断(固定文案优先保留,证据部分被截断时以省略号结尾)。
  • [必须] Agent 文本为空时行为不变(仍按现有规则拒绝,见 normalizeOrderUnknownFailure 现有校验)。

同时修正第二处不一致:"failed" 分支只写 t.ErrorCode / t.ErrorMessage,未写 a.ErrorCode / a.ErrorMessage,导致该路径产生的 attempt 行 error_code 为 NULL(线上任务 170/171 的 spec_probe attempt 即是如此)。本单一并补齐,使两条路径的 attempt 都带错误码与消息。

非目标

  • 不改 Agent。本单不调整 ORDER_RESULT_PAYMENT_POST_BACK_MAX_SAMPLES、ORDER_RESULT_SAMPLE_INTERVAL_MS,不改 Back 次数限制。参数如何调整取决于本单上线后拿到的真实证据,届时另建工单。
  • 不改动 allowedOrderUnknownFailures 的固定文案内容与错误码集合。
  • 不处理已有 61 单的订单号恢复(属 #242 范围)。
  • 不新增数据库列。

前置依赖

无。可与 #242 并行(不同交付单元)。

子项目影响

  • server/:app/goauto/purchase/lifecycle.go
  • web/:无(error_message 已在采购管理页展示)
  • android/:无

验收

  1. order_result_unknown 上报后,purchase_task.error_message 与 purchase_task_attempt.error_message 均包含固定文案和 Agent 证据
  2. 固定文案内容不变,仍由错误码决定
  3. Agent 证据含换行或控制字符时被清理,不破坏展示
  4. 超长证据被截断且固定文案完整保留
  5. Agent message 为空时行为与现在一致
  6. "failed" 分支产生的 attempt 行带 error_code 与 error_message
  7. 采购管理页能看到完整消息,无需改前端

验证

  • lifecycle.go 相关单元测试:拼接、清理、截断、空消息、两条分支的 attempt 写入
  • go build ./...、go test -p 1 ./...
  • 上线后验证(本单验收的关键一步):下一次出现 PURCHASE_ORDER_PAYMENT_REPEATED 时,error_message 中应能读到 paymentBackAttempts 与 consecutivePaymentSamplesAfterBack 的实际值

风险

  • Agent 文本不可信:error_message 会出现 Agent 提供的自由文本。已通过「固定文案仍为主、证据仅追加、清理控制字符、限长截断」控制。该路径与现有 "failed" 分支的做法一致,不引入新的信任假设。
  • 不得写入个人数据:Agent 现有诊断为有界的 key=value 结构,不含账号、地址、订单个人信息。实施时须确认所有 allowedOrderUnknownFailures 对应的 Agent 消息都不携带此类内容。
  • 本单只恢复可观测性,不降低 PAYMENT_REPEATED 发生率。占比问题需等证据到手后另建工单处理。

文档影响

docs/08-agent-api-contract.md(Wiki Android-Agent-API-Contract):订单核单失败的 errorMessage 语义由「服务端固定文案」变为「固定文案 + Agent 证据」,属于共享契约变化,须先改线上 Wiki 再同步镜像。

## 原始需求 来源:用户,2026-09-12 会话。排查「线上采购 PAYMENT_REPEATED 占比回升」时发现根因分析无法进行——Agent 上报的诊断证据没有落库。 用户确认先做服务端这一条,拿到真实证据后再决定 Agent 侧参数怎么改。 ## 当前事实(代码事实 + 线上数据,2026-09-12 核验) ### 问题:同一个 switch 的两个分支对 Agent 证据处理相反 `server/app/goauto/purchase/lifecycle.go` 约 386-401 行: ```go case "order_result_unknown": failureCode, failureMessage, failureErr := normalizeOrderUnknownFailure(req.ErrorCode, req.ErrorMessage) ... a.ErrorCode, a.ErrorMessage = &failureCode, &failureMessage // Agent 证据被替换 t.ErrorCode, t.ErrorMessage = &failureCode, &failureMessage case "failed": t.ErrorCode = &req.ErrorCode t.ErrorMessage = &req.ErrorMessage // Agent 证据原样保留 ``` `normalizeOrderUnknownFailure`(约 450 行)无条件返回固定文案,Agent 的 `message` 只用来判断非空: ```go canonicalMessage, ok := allowedOrderUnknownFailures[code] if !ok || message == "" { return "", "", fail(CodeInvalidRequest, "订单核单失败阶段无效") } return code, canonicalMessage, nil ``` ### Agent 侧确认在发送 `PurchaseRehearsalExecutor.kt:154` 把 `live.lastOrderReadFailure` 的 `code` 和 `message` 原样上报。`PurchaseLiveAutomation.kt:410-417` 构造的消息带诊断后缀: ``` "支付页安全返回后持续无订单证据,已停止自动核单[paymentBackAttempts=N;consecutivePaymentSamplesAfterBack=M]" ``` `purchase_task_attempt.error_message` 与 `purchase_task.error_message` 均为 `size:1000`,空间充足。 ### 线上数据佐证 - 2026-09-07 ~ 09-11,`PURCHASE_ORDER_PAYMENT_REPEATED` 共 **61** 次,`error_message` **全部**是固定文案 `支付页重复出现,已停止自动核单`,**零条**带诊断后缀。 - 同期占比:09-07 100%(15/15)、09-08 44%(12/27)、09-09 20%(6/30)、09-10 61%(22/36)、09-11 43%(6/14)。 - 这 61 次中 `irreversible_at` 置位 **61/61**(订单极可能已创建),`pdd_order_no` 恢复 **0/61**。 - 对照:同一晚任务 170/171 的规格失败走 `"failed"` 分支,证据完整落库:`[type=UNKNOWN;scrollables=1;headings=2;options=8;summary=false;quantity=true;orderAction=false;pageEvidence=true]`。 ### 由此导致的分析盲区 `#210` 加入的 `paymentBackAttempts` / `consecutivePaymentSamplesAfterBack` 从未进入数据库,因此**无法区分**以下三种情形,而三者的修法完全不同: 1. Back 未生效(按了但没离开支付 Activity) 2. 页面渲染慢于容忍窗口(当前约 `500 + 3×200 = 1.1` 秒,而整体预算 `60×200 = 12` 秒) 3. 页面已是订单页但关键词未匹配(`ORDER_CONTEXT_MARKERS` / `UNPAID_MARKERS` 未命中) ## 方案 保留固定文案作为面向人的稳定摘要,在其后附加 Agent 证据。 `normalizeOrderUnknownFailure` 改为返回 `canonicalMessage` 与 Agent 证据的拼接: - 固定文案仍由 `allowedOrderUnknownFailures[code]` 决定,**不采用 Agent 文本作为主文案**(避免把不可信自由文本当作权威描述,也保持界面文案稳定)。 - Agent 的 `message` 作为证据附加在固定文案之后。 - `[必须]` 附加前做边界处理:去除换行与控制字符、压缩连续空白、按 `error_message` 的 `size:1000` 截断(固定文案优先保留,证据部分被截断时以省略号结尾)。 - `[必须]` Agent 文本为空时行为不变(仍按现有规则拒绝,见 `normalizeOrderUnknownFailure` 现有校验)。 同时修正第二处不一致:`"failed"` 分支只写 `t.ErrorCode` / `t.ErrorMessage`,**未写 `a.ErrorCode` / `a.ErrorMessage`**,导致该路径产生的 attempt 行 `error_code` 为 NULL(线上任务 170/171 的 spec_probe attempt 即是如此)。本单一并补齐,使两条路径的 attempt 都带错误码与消息。 ## 非目标 - **不改 Agent**。本单不调整 `ORDER_RESULT_PAYMENT_POST_BACK_MAX_SAMPLES`、`ORDER_RESULT_SAMPLE_INTERVAL_MS`,不改 Back 次数限制。参数如何调整取决于本单上线后拿到的真实证据,届时另建工单。 - 不改动 `allowedOrderUnknownFailures` 的固定文案内容与错误码集合。 - 不处理已有 61 单的订单号恢复(属 #242 范围)。 - 不新增数据库列。 ## 前置依赖 无。可与 #242 并行(不同交付单元)。 ## 子项目影响 - `server/`:`app/goauto/purchase/lifecycle.go` - `web/`:无(`error_message` 已在采购管理页展示) - `android/`:无 ## 验收 1. `order_result_unknown` 上报后,`purchase_task.error_message` 与 `purchase_task_attempt.error_message` 均包含固定文案**和** Agent 证据 2. 固定文案内容不变,仍由错误码决定 3. Agent 证据含换行或控制字符时被清理,不破坏展示 4. 超长证据被截断且固定文案完整保留 5. Agent `message` 为空时行为与现在一致 6. `"failed"` 分支产生的 attempt 行带 `error_code` 与 `error_message` 7. 采购管理页能看到完整消息,无需改前端 ## 验证 - `lifecycle.go` 相关单元测试:拼接、清理、截断、空消息、两条分支的 attempt 写入 - `go build ./...`、`go test -p 1 ./...` - **上线后验证(本单验收的关键一步)**:下一次出现 `PURCHASE_ORDER_PAYMENT_REPEATED` 时,`error_message` 中应能读到 `paymentBackAttempts` 与 `consecutivePaymentSamplesAfterBack` 的实际值 ## 风险 - **Agent 文本不可信**:`error_message` 会出现 Agent 提供的自由文本。已通过「固定文案仍为主、证据仅追加、清理控制字符、限长截断」控制。该路径与现有 `"failed"` 分支的做法一致,不引入新的信任假设。 - **不得写入个人数据**:Agent 现有诊断为有界的 `key=value` 结构,不含账号、地址、订单个人信息。实施时须确认所有 `allowedOrderUnknownFailures` 对应的 Agent 消息都不携带此类内容。 - 本单只恢复可观测性,**不降低 PAYMENT_REPEATED 发生率**。占比问题需等证据到手后另建工单处理。 ## 文档影响 `docs/08-agent-api-contract.md`(Wiki `Android-Agent-API-Contract`):订单核单失败的 `errorMessage` 语义由「服务端固定文案」变为「固定文案 + Agent 证据」,属于共享契约变化,须先改线上 Wiki 再同步镜像。
Author
Owner

实施完成并已部署线上(2026-09-12,待验收)

提交 c14f5f5,已合并 main 并推送:8293b27..c14f5f5。

实现

1. 保留 Agent 证据

normalizeOrderUnknownFailure 返回「固定文案 + Agent 证据」。固定文案仍由错误码唯一决定并排在最前,Agent 文本仅作为追加证据,不作为权威描述。

新增 appendAgentEvidence / flattenAgentEvidence:

  • 压平为单行:去除控制字符与 U+FEFF,连续空白合并为一个空格
  • 按 orderFailureMessageLimit = 1000(对应 error_message 列宽)截断,固定文案优先完整保留,被截断的证据以 … 结尾
  • 证据与固定文案相同时不重复拼接
  • Agent 文本为空或错误码不在白名单时行为不变,仍返回 INVALID_REQUEST

2. 补齐 "failed" 分支的 attempt 字段

原来只写 t.ErrorCode / t.ErrorMessage,attempt 上为 NULL,导致按 attempt 统计失败时该路径的记录隐形(线上规格失败即是如此)。现同时写入 a.ErrorCode / a.ErrorMessage。

验证

  • go build ./... 通过
  • go test -p 1 ./... 33 个包全绿(合并到 main 后重跑确认),既有 order_result_unknown 相关用例未被破坏
  • python dev_scripts/harness.py sync --check 一致
  • 新增 order_failure_evidence_test.go,覆盖:诊断值必须出现在消息中、换行/制表符/BOM 压平、4000 字证据截断且固定文案完整、空消息仍拒绝、未批准错误码仍拒绝、证据等于固定文案时不重复拼接、端到端两张表均写入、"failed" 分支 attempt 带错误码与消息

线上部署(2026-09-12 11:20)

本单无数据库迁移、无前端改动,仅替换后端二进制。

  • 交叉编译:GOOS=linux GOARCH=amd64 CGO_ENABLED=0,静态链接,与线上原二进制构建方式一致
  • 上传后校验 SHA256 前 16 位 9fdc9145c760bfe5,本地与服务器一致
  • 按既有 release 目录约定发布:/home/goauto/releases/20260912-c14f5f5,以上一 release 20260912-8293b27 为基础复制后替换 goauto-server
  • 切换 current 软链并 systemctl restart goauto.service
  • 部署后:服务 active,路由注册与 JobCore 启动正常,无 error/panic;http://185.216.248.75:9527/ 返回 200;/api/admin/v1/syb-product-filters 返回 401(鉴权正常)

回滚方式:ln -sfn /home/goauto/releases/20260912-8293b27 /home/goauto/current 后重启服务。上一 release 目录完整保留。

顺带核实:线上 #269 的两个迁移(1789112900000、1789113000000)已应用,syb_product_filter 8 行就位,SYB 同步过滤正常。

未验证(本单验收的关键一步仍待完成)

[必须] 线上尚未出现新的 PURCHASE_ORDER_PAYMENT_REPEATED,因此还没有拿到真实的 paymentBackAttempts 与 consecutivePaymentSamplesAfterBack。

下一次该错误发生后,须核对 purchase_task.error_message 与 purchase_task_attempt.error_message 是否包含这两个值。拿到后才能区分:

  1. Back 未生效(按了但没离开支付 Activity)
  2. 页面渲染慢于容忍窗口(当前约 500 + 3×200 = 1.1 秒,整体预算 60×200 = 12 秒)
  3. 页面已是订单页但 ORDER_CONTEXT_MARKERS / UNPAID_MARKERS 未命中

三者修法不同,Agent 侧参数调整须待此证据到手后另建工单。

本单不改变的事

本单只恢复可观测性,不降低 PAYMENT_REPEATED 发生率(2026-09-07 至 09-11 共 61 次,占比 20%~100% 波动)。这 61 单 irreversible_at 全部置位、订单号全部未恢复,单号补回属 #242 范围。

文档影响(已完成)

先改线上 Wiki 并回读 revision,再同步镜像:

  • Android-Agent-API-Contract → revision ba259178(镜像 docs/08-agent-api-contract.md)

契约原文「服务端只接受白名单并按错误码写入固定提示,不信任或保存页面原文」已改为「固定提示 + Agent 诊断证据」,并写明压平/截断规则、failed 分支 attempt 写入要求及改动原因。

## 实施完成并已部署线上(2026-09-12,待验收) 提交 `c14f5f5`,已合并 main 并推送:`8293b27..c14f5f5`。 ### 实现 **1. 保留 Agent 证据** `normalizeOrderUnknownFailure` 返回「固定文案 + Agent 证据」。固定文案仍由错误码唯一决定并排在最前,Agent 文本仅作为追加证据,不作为权威描述。 新增 `appendAgentEvidence` / `flattenAgentEvidence`: - 压平为单行:去除控制字符与 U+FEFF,连续空白合并为一个空格 - 按 `orderFailureMessageLimit = 1000`(对应 `error_message` 列宽)截断,**固定文案优先完整保留**,被截断的证据以 `…` 结尾 - 证据与固定文案相同时不重复拼接 - Agent 文本为空或错误码不在白名单时行为不变,仍返回 `INVALID_REQUEST` **2. 补齐 `"failed"` 分支的 attempt 字段** 原来只写 `t.ErrorCode` / `t.ErrorMessage`,attempt 上为 NULL,导致按 attempt 统计失败时该路径的记录隐形(线上规格失败即是如此)。现同时写入 `a.ErrorCode` / `a.ErrorMessage`。 ### 验证 - `go build ./...` 通过 - `go test -p 1 ./...` **33 个包全绿**(合并到 main 后重跑确认),既有 `order_result_unknown` 相关用例未被破坏 - `python dev_scripts/harness.py sync --check` 一致 - 新增 `order_failure_evidence_test.go`,覆盖:诊断值必须出现在消息中、换行/制表符/BOM 压平、4000 字证据截断且固定文案完整、空消息仍拒绝、未批准错误码仍拒绝、证据等于固定文案时不重复拼接、端到端两张表均写入、`"failed"` 分支 attempt 带错误码与消息 ### 线上部署(2026-09-12 11:20) 本单无数据库迁移、无前端改动,仅替换后端二进制。 - 交叉编译:`GOOS=linux GOARCH=amd64 CGO_ENABLED=0`,静态链接,与线上原二进制构建方式一致 - 上传后校验 SHA256 前 16 位 `9fdc9145c760bfe5`,本地与服务器一致 - 按既有 release 目录约定发布:`/home/goauto/releases/20260912-c14f5f5`,以上一 release `20260912-8293b27` 为基础复制后替换 `goauto-server` - 切换 `current` 软链并 `systemctl restart goauto.service` - 部署后:服务 `active`,路由注册与 JobCore 启动正常,无 error/panic;`http://185.216.248.75:9527/` 返回 200;`/api/admin/v1/syb-product-filters` 返回 401(鉴权正常) **回滚方式**:`ln -sfn /home/goauto/releases/20260912-8293b27 /home/goauto/current` 后重启服务。上一 release 目录完整保留。 顺带核实:线上 #269 的两个迁移(`1789112900000`、`1789113000000`)已应用,`syb_product_filter` 8 行就位,SYB 同步过滤正常。 ### 未验证(本单验收的关键一步仍待完成) `[必须]` **线上尚未出现新的 `PURCHASE_ORDER_PAYMENT_REPEATED`**,因此还没有拿到真实的 `paymentBackAttempts` 与 `consecutivePaymentSamplesAfterBack`。 下一次该错误发生后,须核对 `purchase_task.error_message` 与 `purchase_task_attempt.error_message` 是否包含这两个值。拿到后才能区分: 1. Back 未生效(按了但没离开支付 Activity) 2. 页面渲染慢于容忍窗口(当前约 `500 + 3×200 = 1.1` 秒,整体预算 `60×200 = 12` 秒) 3. 页面已是订单页但 `ORDER_CONTEXT_MARKERS` / `UNPAID_MARKERS` 未命中 三者修法不同,Agent 侧参数调整须待此证据到手后另建工单。 ### 本单不改变的事 本单只恢复可观测性,**不降低 PAYMENT_REPEATED 发生率**(2026-09-07 至 09-11 共 61 次,占比 20%~100% 波动)。这 61 单 `irreversible_at` 全部置位、订单号全部未恢复,单号补回属 #242 范围。 ### 文档影响(已完成) 先改线上 Wiki 并回读 revision,再同步镜像: - `Android-Agent-API-Contract` → revision `ba259178`(镜像 `docs/08-agent-api-contract.md`) 契约原文「服务端只接受白名单并按错误码写入固定提示,不信任或保存页面原文」已改为「固定提示 + Agent 诊断证据」,并写明压平/截断规则、`failed` 分支 attempt 写入要求及改动原因。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#271