当前页面采集:按「白名单域名且无 goods_id」触发短链展开(两端同修) #109

Closed
opened 2026-08-27 15:18:45 +08:00 by ila · 3 comments
Owner

所属与来源

  • 关联工单:#101 当前页面采集、#106 分享点击与诊断、#107 前台恢复、#108 短链在 Agent 侧展开。
  • 来源:用户于 2026-08-27 反馈 #108 上线后(Agent 0.9.5、提交 20775cc)当前页面采集仍然无法解析链接;用户提供了 PDD 实际复制到的链接 https://mobile.yangkeduo.com/goods2.html?ps=14mchKPjYR,据此定位到两端的域名判断都把这类链接挡在展开逻辑之外。确认沿用 #108 的分工(展开在 Agent、裁决在服务端),只修两端的判断条件。
  • 类型:Android Agent + Server / 当前页面采集 / 短链展开触发条件缺陷。
  • 设计证据:不新增页面、组件、导航与用户流程;不修改接口字段与数据库结构,属于恢复 #108 预期行为的缺陷修复,不另做原型。

当前事实与根因(提交 20775cc 复核)

PDD App「复制链接」实际给出的是 https://mobile.yangkeduo.com/goods2.html?ps=14mchKPjYR:域名在白名单内,但 query 中没有 goods_id,商品身份藏在 ps= 短码里。两端都恰好落进空档:

Agent 端(android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddShareLinkResolution.kt)

  • PddShareLinkExtractor.select() 中 goodsId(uri) 取不到 goods_id,该链接不算长链;
  • 随后筛短链时只保留 host == "p.pinduoduo.com"(SHORT_HOST),mobile.yangkeduo.com 的链接被排除;
  • 于是 shortLinks 为空,返回 NOT_FOUND,直接失败「无法识别商品链接」,一次展开请求都不会发出;
  • 即使进入展开,PddShareLinkExpander.expand() 循环里还有 if (!parsed.host.equals(SHORT_HOST, ignoreCase = true)) return null,同样会立即放弃。

Server 端(server/app/goauto/task/current_page.go:279)

  • 兜底解析中 if !strings.EqualFold(parsed.Hostname(), "p.pinduoduo.com") { return 无法识别商品链接 },同一个硬编码门槛,同样在发起请求前就拒绝。

结论:#108 实现的展开逻辑本身没有被执行到,它只对 p.pinduoduo.com 生效,而 PDD 实际复制出来的是 yangkeduo 的 ps= 短码链接。

展开可行性已实证(2026-08-27,移动端 UA,curl 单次请求):

GET https://mobile.yangkeduo.com/goods2.html?ps=14mchKPjYR
→ HTTP/2 307
   location: https://mobile.yangkeduo.com/goods2.html?goods_id=971612375909&page_from=35&...

一跳 307 即得到 goods_id=971612375909,无 JS 跳转、无验证页;现有的 4 跳预算、白名单校验与超时设置均足够。注意实际状态码是 307 而非 302,实现必须按 3xx 通配处理。

顺带发现的隐患:Agent 端正文正则 bodyGoodsIDPattern 中 goods_id 前的分隔符组是可选的,refer_goods_id=123456 这类会被误匹配;再撞上 goodsIds.singleOrNull() 的唯一性要求,会把本可成功的页面判为失败。服务端 pddBodyGoodsIDPatterns 第一条已强制要求前置分隔符,两端不一致。

目标

  1. 让「白名单域名 + query 无 goods_id」的链接进入展开流程,不再按具体域名判断。
  2. Agent 与服务端同步修改,保持服务端兜底路径真实可用,不退化为摆设。
  3. 两端正文提取正则对齐,消除 refer_goods_id 误匹配。

非目标

  • 不改变 #108 确立的分工:展开在 Agent,goods_id 的校验、归一化、建档与并发互斥仍由服务端裁决。
  • 不修改接口字段、数据库结构、采集规则格式与 Admin/Web。
  • 不修改分享点击、剪贴板读取、前台恢复逻辑(#105 / #106 / #107 范围)。
  • 不扩大域名白名单(仍为 p.pinduoduo.com 与 mobile.yangkeduo.com),本工单只改「是否值得展开」的判断。
  • 不涉及采购、地址、创建订单与支付。

实施方案

Agent 端

  1. PddShareLinkExtractor.select():短链候选不再按 SHORT_HOST 过滤,改为「通过白名单校验且 query 中没有 goods_id」即视为待展开候选。多个候选且指向不同目标时仍返回 AMBIGUOUS,零候选仍返回 NOT_FOUND。
  2. PddShareLinkExpander.expand():移除循环内 host == SHORT_HOST 的限制,改为只要 parseAllowed 通过(白名单域名、https、无 userinfo、端口空或 443)就继续跟随跳转;每一跳仍重新校验,跳转上限 4 跳、超时 5 秒不变。
  3. 明确按 3xx 通配处理跳转(现有 statusCode in 300..399 已覆盖 307/308,实施时不得收窄为仅 302)。
  4. 正文正则改为强制要求 goods_id 前是 [?&]、&、%26、& 或字符串开头,或形如 "goods_id": 的 JSON 键,消除 refer_goods_id 误匹配;保留「正文中不同 goods_id 必须唯一」的要求。
  5. 诊断沿用 #108 的 LINK_RESOLUTION 阶段与既有原因枚举,不新增字段,不记录链接原文与 goods_id。

Server 端

  1. 删除 current_page.go:279 的 p.pinduoduo.com 判断,改为「已通过 validatePDDShareURL 白名单校验且 query 无 goods_id」即进入 HTTP 兜底解析;CheckRedirect 中的白名单校验与 4 跳上限保持不变。
  2. extractPDDGoodsIDFromBody 的两条正则与 Agent 端对齐(前置分隔符强制、JSON 键形式保留、唯一性要求保留)。
  3. 其余校验、归一化、建档与并发互斥逻辑一律不变,继续对 Agent 上报的一切内容重新校验,不因来自 Agent 而放宽。

安全边界

  • 域名白名单不扩大;每一跳都重新校验 scheme、域名、userinfo 与端口。
  • 展开请求不携带 Cookie、Token、账号与任何个人数据。
  • 链接原文、页面正文、剪贴板正文不落盘、不入日志、不写诊断。
  • Agent 不认定 goods_id;服务端保持唯一裁决权。
  • 不使用 OCR/VLM,不猜测商品,不采购、不改地址、不创建订单、不支付。

验收标准

  • Agent 对 https://mobile.yangkeduo.com/goods2.html?ps=14mchKPjYR 这类链接发起展开,一跳 307 后得到 goods_id=971612375909 并成功识别(真机验证)。
  • 含 goods_id 的长链仍然直接上报,不发起任何额外网络请求。
  • p.pinduoduo.com 短链路径行为不回退,仍可展开。
  • 服务端兜底在只收到未展开短链时同样可解析成功(含 yangkeduo ps= 形态),以 httptest 覆盖 307/302 两种跳转码。
  • 正文中同时存在 goods_id= 与 refer_goods_id= 时,两端都只取 goods_id 且解析成功。
  • 跳转出白名单、超过 4 跳、超时三种情况仍明确失败,不崩溃、不放宽白名单。
  • 服务端的白名单、格式、归一化与并发互斥校验未被放宽(以现有服务端测试全部通过为准)。
  • 诊断不含链接原文、goods_id、剪贴板正文与页面正文。

验证方式

  • ./gradlew :app:testDebugUnitTest(含 select() 新判断与展开逻辑的纯函数测试)。
  • go test ./app/goauto/task/...。
  • 真机验证:一加 PKG110 或等价设备,用真实商品复制链接完成一次当前页面采集;记录设备型号、Agent 版本、规则快照与任务号。
  • 真机、多设备与云环境未覆盖的部分必须在工单如实记录。

风险与回退

  • PDD 可能对同一商品在不同版本给出不同短码形态(ps=、p.pinduoduo.com 短链等);本次改为按「无 goods_id」触发展开,对形态变化不敏感,但域名若变化仍需改白名单,属已知边界。
  • 展开会多一次网络请求,最坏情况增加约 5 秒;长链路径不受影响。
  • 回退方式:还原本工单提交即可回到 #108 的行为,不涉及数据结构与接口变更。

文档影响

  • 待实施时确认:#108 已在 docs/02-architecture-and-code-map.md、docs/03-business-rules-and-glossary.md、docs/08-agent-api-contract.md 中描述了链接解析分工。本次只改判断条件、不改分工,若这些页面写到了「仅 p.pinduoduo.com 短链」之类的具体表述则需同步更新;否则在工单说明「无长期文档影响」并跳过 Wiki 同步。

状态

待实施(由 Codex 执行)。

## 所属与来源 - 关联工单:#101 当前页面采集、#106 分享点击与诊断、#107 前台恢复、#108 短链在 Agent 侧展开。 - 来源:用户于 2026-08-27 反馈 #108 上线后(Agent 0.9.5、提交 `20775cc`)当前页面采集仍然无法解析链接;用户提供了 PDD 实际复制到的链接 `https://mobile.yangkeduo.com/goods2.html?ps=14mchKPjYR`,据此定位到两端的域名判断都把这类链接挡在展开逻辑之外。确认沿用 #108 的分工(展开在 Agent、裁决在服务端),只修两端的判断条件。 - 类型:Android Agent + Server / 当前页面采集 / 短链展开触发条件缺陷。 - 设计证据:不新增页面、组件、导航与用户流程;不修改接口字段与数据库结构,属于恢复 #108 预期行为的缺陷修复,不另做原型。 ## 当前事实与根因(提交 20775cc 复核) PDD App「复制链接」实际给出的是 `https://mobile.yangkeduo.com/goods2.html?ps=14mchKPjYR`:域名在白名单内,但 query 中没有 `goods_id`,商品身份藏在 `ps=` 短码里。两端都恰好落进空档: **Agent 端**(`android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddShareLinkResolution.kt`) - `PddShareLinkExtractor.select()` 中 `goodsId(uri)` 取不到 `goods_id`,该链接不算长链; - 随后筛短链时只保留 `host == "p.pinduoduo.com"`(`SHORT_HOST`),`mobile.yangkeduo.com` 的链接被排除; - 于是 `shortLinks` 为空,返回 `NOT_FOUND`,直接失败「无法识别商品链接」,**一次展开请求都不会发出**; - 即使进入展开,`PddShareLinkExpander.expand()` 循环里还有 `if (!parsed.host.equals(SHORT_HOST, ignoreCase = true)) return null`,同样会立即放弃。 **Server 端**(`server/app/goauto/task/current_page.go:279`) - 兜底解析中 `if !strings.EqualFold(parsed.Hostname(), "p.pinduoduo.com") { return 无法识别商品链接 }`,同一个硬编码门槛,同样在发起请求前就拒绝。 结论:#108 实现的展开逻辑本身没有被执行到,它只对 `p.pinduoduo.com` 生效,而 PDD 实际复制出来的是 yangkeduo 的 `ps=` 短码链接。 **展开可行性已实证**(2026-08-27,移动端 UA,`curl` 单次请求): ``` GET https://mobile.yangkeduo.com/goods2.html?ps=14mchKPjYR → HTTP/2 307 location: https://mobile.yangkeduo.com/goods2.html?goods_id=971612375909&page_from=35&... ``` 一跳 307 即得到 `goods_id=971612375909`,无 JS 跳转、无验证页;现有的 4 跳预算、白名单校验与超时设置均足够。注意实际状态码是 307 而非 302,实现必须按 3xx 通配处理。 **顺带发现的隐患**:Agent 端正文正则 `bodyGoodsIDPattern` 中 `goods_id` 前的分隔符组是可选的,`refer_goods_id=123456` 这类会被误匹配;再撞上 `goodsIds.singleOrNull()` 的唯一性要求,会把本可成功的页面判为失败。服务端 `pddBodyGoodsIDPatterns` 第一条已强制要求前置分隔符,两端不一致。 ## 目标 1. 让「白名单域名 + query 无 goods_id」的链接进入展开流程,不再按具体域名判断。 2. Agent 与服务端同步修改,保持服务端兜底路径真实可用,不退化为摆设。 3. 两端正文提取正则对齐,消除 `refer_goods_id` 误匹配。 ## 非目标 - 不改变 #108 确立的分工:展开在 Agent,goods_id 的校验、归一化、建档与并发互斥仍由服务端裁决。 - 不修改接口字段、数据库结构、采集规则格式与 Admin/Web。 - 不修改分享点击、剪贴板读取、前台恢复逻辑(#105 / #106 / #107 范围)。 - 不扩大域名白名单(仍为 `p.pinduoduo.com` 与 `mobile.yangkeduo.com`),本工单只改「是否值得展开」的判断。 - 不涉及采购、地址、创建订单与支付。 ## 实施方案 ### Agent 端 1. `PddShareLinkExtractor.select()`:短链候选不再按 `SHORT_HOST` 过滤,改为「通过白名单校验且 query 中没有 `goods_id`」即视为待展开候选。多个候选且指向不同目标时仍返回 `AMBIGUOUS`,零候选仍返回 `NOT_FOUND`。 2. `PddShareLinkExpander.expand()`:移除循环内 `host == SHORT_HOST` 的限制,改为只要 `parseAllowed` 通过(白名单域名、https、无 userinfo、端口空或 443)就继续跟随跳转;每一跳仍重新校验,跳转上限 4 跳、超时 5 秒不变。 3. 明确按 3xx 通配处理跳转(现有 `statusCode in 300..399` 已覆盖 307/308,实施时不得收窄为仅 302)。 4. 正文正则改为强制要求 `goods_id` 前是 `[?&]`、`&`、`%26`、`&` 或字符串开头,或形如 `"goods_id":` 的 JSON 键,消除 `refer_goods_id` 误匹配;保留「正文中不同 goods_id 必须唯一」的要求。 5. 诊断沿用 #108 的 `LINK_RESOLUTION` 阶段与既有原因枚举,不新增字段,不记录链接原文与 goods_id。 ### Server 端 6. 删除 `current_page.go:279` 的 `p.pinduoduo.com` 判断,改为「已通过 `validatePDDShareURL` 白名单校验且 query 无 `goods_id`」即进入 HTTP 兜底解析;`CheckRedirect` 中的白名单校验与 4 跳上限保持不变。 7. `extractPDDGoodsIDFromBody` 的两条正则与 Agent 端对齐(前置分隔符强制、JSON 键形式保留、唯一性要求保留)。 8. 其余校验、归一化、建档与并发互斥逻辑一律不变,继续对 Agent 上报的一切内容重新校验,不因来自 Agent 而放宽。 ## 安全边界 - 域名白名单不扩大;每一跳都重新校验 scheme、域名、userinfo 与端口。 - 展开请求不携带 Cookie、Token、账号与任何个人数据。 - 链接原文、页面正文、剪贴板正文不落盘、不入日志、不写诊断。 - Agent 不认定 goods_id;服务端保持唯一裁决权。 - 不使用 OCR/VLM,不猜测商品,不采购、不改地址、不创建订单、不支付。 ## 验收标准 - [ ] Agent 对 `https://mobile.yangkeduo.com/goods2.html?ps=14mchKPjYR` 这类链接发起展开,一跳 307 后得到 `goods_id=971612375909` 并成功识别(真机验证)。 - [ ] 含 `goods_id` 的长链仍然直接上报,不发起任何额外网络请求。 - [ ] `p.pinduoduo.com` 短链路径行为不回退,仍可展开。 - [ ] 服务端兜底在只收到未展开短链时同样可解析成功(含 yangkeduo `ps=` 形态),以 httptest 覆盖 307/302 两种跳转码。 - [ ] 正文中同时存在 `goods_id=` 与 `refer_goods_id=` 时,两端都只取 `goods_id` 且解析成功。 - [ ] 跳转出白名单、超过 4 跳、超时三种情况仍明确失败,不崩溃、不放宽白名单。 - [ ] 服务端的白名单、格式、归一化与并发互斥校验未被放宽(以现有服务端测试全部通过为准)。 - [ ] 诊断不含链接原文、goods_id、剪贴板正文与页面正文。 ## 验证方式 - `./gradlew :app:testDebugUnitTest`(含 `select()` 新判断与展开逻辑的纯函数测试)。 - `go test ./app/goauto/task/...`。 - 真机验证:一加 PKG110 或等价设备,用真实商品复制链接完成一次当前页面采集;记录设备型号、Agent 版本、规则快照与任务号。 - 真机、多设备与云环境未覆盖的部分必须在工单如实记录。 ## 风险与回退 - PDD 可能对同一商品在不同版本给出不同短码形态(`ps=`、`p.pinduoduo.com` 短链等);本次改为按「无 goods_id」触发展开,对形态变化不敏感,但域名若变化仍需改白名单,属已知边界。 - 展开会多一次网络请求,最坏情况增加约 5 秒;长链路径不受影响。 - 回退方式:还原本工单提交即可回到 #108 的行为,不涉及数据结构与接口变更。 ## 文档影响 - 待实施时确认:#108 已在 `docs/02-architecture-and-code-map.md`、`docs/03-business-rules-and-glossary.md`、`docs/08-agent-api-contract.md` 中描述了链接解析分工。本次只改判断条件、不改分工,若这些页面写到了「仅 `p.pinduoduo.com` 短链」之类的具体表述则需同步更新;否则在工单说明「无长期文档影响」并跳过 Wiki 同步。 ## 状态 待实施(由 Codex 执行)。
Author
Owner

开始实施。按工单范围做两端同修:Agent 与 Server 均将“白名单域名且 URL 本身无 goods_id”作为展开触发条件,白名单保持不变;正文提取排除 refer_goods_id 等相近字段。不会改变先取得 goods_id、再请求创建采集任务、设备领取任务后执行的既有时序,也不涉及数据库、共享接口、采购、订单或支付。

首轮针对性测试已通过:Android :app:testDebugUnitTest;Server go test ./app/goauto/task/...。后续继续完整回归、Wiki-first 文档闭环及 PKG110 安装核验。

开始实施。按工单范围做两端同修:Agent 与 Server 均将“白名单域名且 URL 本身无 `goods_id`”作为展开触发条件,白名单保持不变;正文提取排除 `refer_goods_id` 等相近字段。不会改变先取得 `goods_id`、再请求创建采集任务、设备领取任务后执行的既有时序,也不涉及数据库、共享接口、采购、订单或支付。 首轮针对性测试已通过:Android `:app:testDebugUnitTest`;Server `go test ./app/goauto/task/...`。后续继续完整回归、Wiki-first 文档闭环及 PKG110 安装核验。
Author
Owner

#109 已完成实现并提交,现保持工单开放、等待人工验收。

实现:

  • Agent:唯一白名单链接只要 URL 本身未取得有效 goods_id 就进入手机侧展开,不再把展开入口限定为 p.pinduoduo.com;因此实际形式 mobile.yangkeduo.com/goods2.html?ps=... 会发起展开。
  • Server:兜底解析同样允许所有既有白名单域名在无 goods_id 时请求展开,白名单未扩大。
  • Agent 正文提取改为边界明确的 query/text 与 JSON 两类模式,refer_goods_id 不会误命中;服务端对应行为增加回归证据。
  • 既有时序未改变:Agent 先从分享链接取得 goods_id,再调用识别接口由服务端创建/复用商品并绑定临时采集任务,之后才进入既有采集执行;本工单没有改数据库、共享接口、任务状态机、采购、订单或支付。
  • Agent 版本:0.9.6(versionCode 19)。

验证:

  • cd android && .\gradlew.bat :app:testDebugUnitTest :app:assembleDebug:通过。
  • cd server && go test ./...:通过。
  • python dev_scripts/harness.py check --strict:通过。
  • git diff --check / staged diff check:通过。
  • Wiki-first:线上更新并回读 Android-Agent-API-Contract@b722977b0c8b7db2ce0d3c8c2a1351f42941cb24、Local-Development-and-Verification@6362cbe25f4a0a1826b903ee4342c1a69ee87a42;执行一次 sync 和一次 sync --check,通过。
  • PKG110:安装前确认 Agent 0.9.5 在线空闲;已覆盖安装 0.9.6/code 19,安装成功,无障碍服务仍启用,安装后在线空闲。
  • 自动化覆盖:实际 goods2.html?ps=14mchKPjYR 形态、p.pinduoduo.com 302、mobile.yangkeduo.com 307、正文 goods_id、仅 refer_goods_id、白名单外跳转、跳转上限与缺失身份。

提交:

  • daf7406ec97262d12a501d85b1245395847a8584 — fix: expand all whitelisted PDD share links (#109)

未自动执行:未代替用户在 PDD 真实商品详情页点击“采集”创建正式采集任务,因此真机端到端“分享→展开→识别→采集结果”仍需按验收步骤人工触发;没有创建采购任务、订单或支付。

#109 已完成实现并提交,现保持工单开放、等待人工验收。 实现: - Agent:唯一白名单链接只要 URL 本身未取得有效 `goods_id` 就进入手机侧展开,不再把展开入口限定为 `p.pinduoduo.com`;因此实际形式 `mobile.yangkeduo.com/goods2.html?ps=...` 会发起展开。 - Server:兜底解析同样允许所有既有白名单域名在无 `goods_id` 时请求展开,白名单未扩大。 - Agent 正文提取改为边界明确的 query/text 与 JSON 两类模式,`refer_goods_id` 不会误命中;服务端对应行为增加回归证据。 - 既有时序未改变:Agent 先从分享链接取得 `goods_id`,再调用识别接口由服务端创建/复用商品并绑定临时采集任务,之后才进入既有采集执行;本工单没有改数据库、共享接口、任务状态机、采购、订单或支付。 - Agent 版本:0.9.6(versionCode 19)。 验证: - `cd android && .\gradlew.bat :app:testDebugUnitTest :app:assembleDebug`:通过。 - `cd server && go test ./...`:通过。 - `python dev_scripts/harness.py check --strict`:通过。 - `git diff --check` / staged diff check:通过。 - Wiki-first:线上更新并回读 `Android-Agent-API-Contract@b722977b0c8b7db2ce0d3c8c2a1351f42941cb24`、`Local-Development-and-Verification@6362cbe25f4a0a1826b903ee4342c1a69ee87a42`;执行一次 `sync` 和一次 `sync --check`,通过。 - PKG110:安装前确认 Agent 0.9.5 在线空闲;已覆盖安装 0.9.6/code 19,安装成功,无障碍服务仍启用,安装后在线空闲。 - 自动化覆盖:实际 `goods2.html?ps=14mchKPjYR` 形态、p.pinduoduo.com 302、mobile.yangkeduo.com 307、正文 goods_id、仅 refer_goods_id、白名单外跳转、跳转上限与缺失身份。 提交: - `daf7406ec97262d12a501d85b1245395847a8584` — `fix: expand all whitelisted PDD share links (#109)` 未自动执行:未代替用户在 PDD 真实商品详情页点击“采集”创建正式采集任务,因此真机端到端“分享→展开→识别→采集结果”仍需按验收步骤人工触发;没有创建采购任务、订单或支付。
Author
Owner

用户于 2026-08-28 明确确认本工单验收通过。按项目流程记录验收结论并关闭工单;本次仅更新工单状态,无新增长期文档事实,不重复同步 Wiki。

用户于 2026-08-28 明确确认本工单验收通过。按项目流程记录验收结论并关闭工单;本次仅更新工单状态,无新增长期文档事实,不重复同步 Wiki。
ila closed this issue 2026-08-28 15:07:11 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#109