Skip to content

fix(139): 子目录列表静默回退根目录与文件路径套娃 - #126

Open
wumingzhinu wants to merge 2 commits into
OpenListTeam:mainfrom
wumingzhinu:fix/139-subdir-listing
Open

wumingzhinu wants to merge 2 commits into
OpenListTeam:mainfrom
wumingzhinu:fix/139-subdir-listing

Conversation

@wumingzhinu

@wumingzhinu wumingzhinu commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary / 摘要

修复 #112 报告的移动云盘(139 个人云)两类异常列表行为。

根因

维护者在评审中指出 #121(break→throw,与本 PR 首个 commit 同实现)并未解决问题——该判断成立:throw 只覆盖「按名称找不到目录」的场景,而 #112 报告者的现象是按名查找成功、不报任何错、却拿到根目录内容。重新对照 Go 版驱动、boxplayer、Cnotech/cloud139 三方实现与报告者抓包后,症状 1 存在两条同样绕过 throw 的静默回退路径:

  1. 路由 host 静默回退(主嫌疑):init() 捕获 queryRoutePolicy 的任何错误后仅告警并继续使用默认域 https://yun.139.com;Go 版驱动在此处是硬失败(PersonalCloudHost is empty → Init error)。新版个人云的 /file/list 只存在于路由下发的 host(如 xxx/hcy,见 OpenList 文档「hcy/file/list」、Cnotech/cloud139 配置 personal_cloud_host = ".../hcy");打到默认域的是 legacy 端点,忽略 parentFileId 恒返回根目录——目录仍按名解析成功,任何 throw 都不触发。driverCache 以 ${id}_${modified} 为键缓存驱动实例,错误 host 在 isolate 生命周期内粘滞、随冷启动抖动,与维护者「似乎和缓存有点关系」的观察吻合。
  2. 目录项缺 id:/file/list 映射只读 catalogID: i.fileId;boxplayer 对同一字段做了四重回退 fileId || catalogId || catalogID || contentID(不同账号/版本的响应形态差异)。目录项缺 fileId 时 catalogID 为 undefined,listFiles(undefined) 落到 parentFileId = undefined || root_folder_id || "/"——同样静默列根目录,且按名查找照样成功、throw 不触发。

佐证:报告者抓包中错误响应的时间戳格式 2026-10-04T19:47:40.056+08:00 正是 Go 版 getPersonalTime(2006-01-02T15:04:05.999-07:00,新版 /file/list 的 updatedAt)解析格式,而旧版 getDisk 是 20060102150405 紧凑格式——证明执行的是 personal_new 分支;新旧 API 的请求体、字段名(fileId/type/updatedAt)与 Go 版逐项一致,差异只剩 host 处理与 id 回退这两处。

套娃症状(/手机软件/阅读.apk/阅读.apk)的成因仍是 break 静默回退到父目录 id(路径末段是文件时 list() 返回父级内容),首版分析在该症状上成立。

修复

  • resolveCatalogId 解析不到即 throw new Error("Path not found: <path>"),不再静默回退(修套娃);目录项仍拿不到 id 时抛 [139] folder "<名>" has no catalog id in listing(堵路径 2)。
  • /file/list 目录/文件 id 并集回退 fileId || catalogId || catalogID(对齐 boxplayer),目录项缺 fileId 时不再退化为 undefined。
  • init() 在 personal_new 拿不到路由下发的 personal host 时硬失败(对齐 Go 版),错误携带原始原因;旧版个人云走 /orchestration 硬编码路径不依赖路由,仅告警(堵路径 1)。
  • 新增 buildRequestUrls:personal_new 拒绝把业务端点发到默认域(防御长生命周期 isolate 的陈旧实例);候选 URL 按「host 直拼(现网 /hcy 形态)→ orchestration 形态(裸 host 补 /orchestration、/hcy 去前缀回退)」排列,仅 404 / route not found 才回退,校验/权限错误原样抛出——对齐 gaozhangmin/boxplayer cloud139PersonalUrl(s) 的实测序列与重试语义。
  • 个人云 /file/list 翻页游标优先读 data.pageInfo.nextPageCursor、保留 data.nextPageCursor 兼容回退,游标不变时终止循环防死循环;此前超过 100 项的目录只返回第一页(顺带修复;cloud139 读 top-level、boxplayer 读 pageInfo,两形态均覆盖)。
  • 目录/文件名比较改为 trim + Unicode NFC 归一化(get()/remove() 同步)。
  • 错误信息附带请求 URL;非 JSON 响应(如被重定向到网页)显式报错——复现者无需函数日志即可看到打到了哪个 host。

变更文件

  • src/backend/drivers/139/driver.ts:resolveCatalogId 显式报错(找不到 / 缺 id)+ nameEquals 归一化比较
  • src/backend/drivers/139/util.ts:路由硬失败、候选 URL 与回退语义、id 并集映射、游标读取与死循环防护、错误带 URL
  • src/backend/drivers/139/types.ts:PersonalFileItem 补 catalogId/catalogID、fileId 改为可选;PersonalListResp.data.pageInfo 类型
  • src/backend/drivers/139/driver.test.ts:新增 6 组回归测试

诚实说明(验证边界)

  • 已验证:139 驱动单测 9/9 通过、tsc --strict 聚焦检查通过、prettier --check 通过。

  • 未验证:无 139 账号凭据,未做端到端真实 API 测试;两条根因路径均以「机制分析 + 单测锁定」方式修复——修复后「静默展示根目录内容」这一 bug class 被消除:要么列出子目录真实内容,要么返回带 请求 URL 的明确错误。

  • 若报告者/维护者部署后仍复现,请携带错误信息(现已包含请求 URL 与状态码)回报,可直接定位是 host 还是上游数据问题。

  • This PR has breaking changes.
    / 此 PR 包含破坏性变更。

  • This PR changes public API, config, storage format, or migration behavior.
    / 此 PR 修改了公开 API、配置、存储格式或迁移行为。
    (行为变化:personal_new 路由查询失败由「静默用错 host 列出根目录」变为初始化显式报错;不存在/缺 id 路径由静默回退变为报错——均属 bug 修复。)

  • This PR requires corresponding changes in related repositories.
    / 此 PR 需要关联仓库同步修改。

Related repository PRs / 关联仓库 PR :

  • OpenList: (Go 版 resolveCatalogId 不存在——由核心按 id 解析路径;其 Init 对空 PersonalCloudHost 本就是硬失败,本 PR 对齐该语义)
  • OpenList-Docs:

Related Issues / 关联 Issue

Relates to #112

Testing / 测试

  • tsc --strict 聚焦类型检查(src/backend/drivers/139/*.ts)通过
  • tsx --test src/backend/drivers/139/driver.test.ts — 9/9 通过:
    - 既有 3 组(签名计算、实例化/列表/链接、token 刷新)无回归;
    - 回归组 1:子目录返回子目录内容(症状 1)、文件路径 list 报 Path not found(套娃)、不存在目录报错;
    - 回归组 2:NFD 形态目录名 + NFC 路径仍能匹配;
    - 回归组 3:pageInfo.nextPageCursor 两页合并、回传游标断言;
    - 回归组 4:目录项仅带 catalogId 时并集回退可解析子目录;完全缺 id 时报 has no catalog id;
    - 回归组 5:personal_new 路由失败/空列表 → init 报 queryRoutePolicy failed;旧版个人云仅告警不失败;路由正常时 host 生效;
    - 回归组 6:候选 URL 序列(/hcy 双候选、裸 host 补 /orchestration、已带路径单候选)、404 回退第二候选、500 不重试且错误带 URL。
  • prettier --check:4 个改动文件全部通过(prettier --write 顺手修正了 util.ts 与测试文件上游各一处既有违规行)

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING。
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with prettier where applicable.
    / 我已按适用情况使用 prettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • Other (please specify) / 其他(请注明): MiMo (OpenCode)

Usage scope / 使用范围:

  • Code generation / 代码生成

  • Tests / 测试

  • Documentation / 文档

  • I have reviewed and validated all AI-assisted content included in this PR.
    / 我已审核并验证此 PR 中的所有 AI 辅助内容。

  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中的所有 AI 辅助内容。(请提交人确认后勾选)

resolveCatalogId 逐段查找目录时,任何一段没找到就 break 并静默返回
最近一个已解析的父目录 id,list() 拿着这个 id 继续列目录:

- 路径末段是文件(/手机软件/阅读.apk)时,list() 返回父目录内容,
  前端把文件路径渲染成目录,出现 /手机软件/阅读.apk/阅读.apk 套娃;
- 首段就解析失败(列表瞬时缺失、缓存不一致等)时回退到根目录,
  表现为访问子目录却看到根目录的全部文件夹(issue OpenListTeam#112 症状 1)。

修复:
- resolveCatalogId 解析不到即抛 Path not found: <path>,不再静默
  回退;平台返回明确错误,前端不再把文件/不存在路径当目录渲染。
- 目录/文件名比较改为 trim + Unicode NFC 归一化,避免同名目录因
  归一化形态不同被误判为找不到。
- 个人云 /file/list 翻页游标优先读 data.pageInfo.nextPageCursor
  (社区实现 gaozhangmin/boxplayer 实测的响应形态),保留
  data.nextPageCursor 兼容回退;游标不再变化时终止循环防死循环。
  此前只读 data.nextPageCursor,超过 100 项的目录只返回第一页。

新增 3 组回归测试:子目录内容/文件路径报错/不存在目录报错、NFC
归一化匹配、pageInfo 翻页合并;139 驱动测试 6/6 通过。

Relates to OpenListTeam#112

Co-Authored-By: MiMo (OpenCode) <noreply@opencode.ai>
@wumingzhinu

Copy link
Copy Markdown
Contributor Author

简要说明:错误原因 + 已修改未测试

错误原因

driver.ts 的 resolveCatalogId 逐段按名称查找目录,任何一段没找到就 break 并静默返回最近一个已解析的父目录 id,list() 拿着这个 id 继续列目录:

  1. 套娃(/手机软件/阅读.apk/阅读.apk):末段是文件而非目录 → 解析停在 手机软件 → list() 返回父目录内容 → 前端把文件路径渲染成目录;
  2. 子目录显示根目录内容:首段就解析失败(列表瞬时缺失/缓存不一致,与 @PIKACHUIM "似乎和缓存有点关系" 的观察吻合)→ 静默回退到根目录 id → 列出根目录全部文件夹。

顺带发现的独立 bug:个人云 /file/list 分页游标实际在 data.pageInfo.nextPageCursor(社区实现 gaozhangmin/boxplayer 实测),代码读的是 data.nextPageCursor——超过 100 项的目录此前一直只返回第一页。

已修改(commit 97adb8b)

  • resolveCatalogId 解析不到即抛 Path not found: <path>,不再静默回退;
  • 名称比较统一为 trim + Unicode NFC 归一化(get()/remove() 同步);
  • 游标优先读 data.pageInfo.nextPageCursor、兼容旧形态、游标不变时终止循环;
  • 新增 3 组回归测试(症状1 子目录内容 / 套娃报错 / 不存在目录报错、NFD 目录名+NFC 路径、两页游标合并)。

未测试(需要有凭据的复现者/维护者验证)

  • ❌ 无 139 账号凭据,未做端到端真实 API 验证
  • ✅ 已测:139 驱动单测 6/6 通过、tsc --strict 通过、prettier 合规
  • 修复后的预期行为:正常情况列出子目录自己的内容;列表瞬时不一致时返回明确的 Path not found 错误,而不是静默展示根目录内容

请报告者 @MoForgt 或有凭据的维护者部署本 PR 后验证:若修复后仍出现「无任何报错、直接显示根目录内容」,说明上游 /file/list 对部分请求返回了不一致数据,请携带请求时间回报 Worker 日志,便于继续排查。

@PIKACHUIM

Copy link
Copy Markdown
Member

break这个应该不是根因,在#121有相同的实现,但没有解决问题

维护者反馈 OpenListTeam#121(break→throw)并未解决症状 1:按名查找目录成功时
任何 throw 都不会触发,问题出在查找成功之后——解析结果或请求目标
host 静默退化为根目录:

- init() 在 personal_new 取不到 queryRoutePolicy 下发的 personal host
  时硬失败(对齐 Go 版 PersonalCloudHost is empty → Init error),
  不再静默回退默认域 yun.139.com——默认域的 /file/list 是 legacy
  端点,忽略 parentFileId 恒返回根目录内容,且因按名解析仍成功而不
  触发任何报错(issue OpenListTeam#112 症状 1、OpenListTeam#121 无效的原因)。
- 新增 buildRequestUrls:personal_new 拒绝把业务端点发到默认域;
  候选 URL 按 host 直拼(现网 /hcy 形态)优先,404/route not found
  时回退 orchestration 形态(裸 host 补 /orchestration),仅路由类
  错误重试——对齐 gaozhangmin/boxplayer 的候选序列与重试语义。
- /file/list 目录项 id 并集回退 fileId||catalogId||catalogID;仍拿
  不到 id 时 resolveCatalogId 抛错,杜绝 listFiles(undefined) 回退
  parentFileId="/" 的静默列根目录路径。
- 错误信息附带请求 URL,非 JSON 响应显式报错,便于复现者直接定位。

新增 3 组回归测试(id 并集与空 id 报错、init 硬失败、URL 候选与
重试语义);139 驱动测试 9/9 通过,tsc --strict、prettier 通过。
未做端到端真实 API 验证(无139 账号凭据)。

Relates to OpenListTeam#112

Co-Authored-By: MiMo (OpenCode) <noreply@opencode.ai>
@wumingzhinu

Copy link
Copy Markdown
Contributor Author

简要说明:认可证伪 + 重新定位的根因 + 已修改未测试

@PIKACHUIM 的质疑成立,感谢指正。#121(break→throw)与本 PR 首个 commit 同实现却未解决问题,说明症状 1 不是「按名找不到目录」——查找是成功的,所以任何 throw 都不会触发。首版分析只完整解释套娃(症状 2);症状 1 需要一条绕过 throw 的静默路径。重新对照 Go 版驱动、gaozhangmin/boxplayer、Cnotech/cloud139 与报告者抓包后,有两条:

路径 A(主嫌疑):路由 host 静默回退

  • Worker 的 init() 把 queryRoutePolicy 的任何错误 catch 后仅告警,继续用默认域 https://yun.139.com;Go 版同处是硬失败(PersonalCloudHost is empty → Init error),所以 Go 版永远不会拿着错 host 请求。
  • 新版个人云的 /file/list 只存在于路由下发的 host(xxx/hcy——OpenList 文档「hcy/file/list」、Cnotech/cloud139 配置 personal_cloud_host = ".../hcy" 均为证);打到默认域的是 legacy 端点,忽略 parentFileId 恒返回根目录——目录仍按名解析成功,throw 不触发,页面无报错,与 [BUG] 移动云盘访问子目录出现根目录套娃情况 #112 完全一致。
  • driverCache 以 ${id}_${modified} 缓存驱动实例,错误 host 在 isolate 内粘滞、随冷启动抖动——与「似乎和缓存有点关系」的观察吻合。
  • boxplayer 的实现侧面印证:裸 host 必须补 /orchestration 才是新版端点、/hcy 变体在 route not found 时回退 orchestration 路径(cloud139PersonalUrl(s)),这些容错 Worker 一个都没有。

路径 B:目录项缺 id

  • /file/list 映射只读 catalogID: i.fileId;boxplayer 对同一字段做四重回退 fileId || catalogId || catalogID || contentID(不同账号响应形态差异的实证)。
  • 目录项缺 fileId → catalogID = undefined → listFiles(undefined) → parentFileId = undefined || root_folder_id || "/" → 静默列根目录;按名查找照样成功,fix(139): throw instead of silently break when folder not found in path #121/本 PR 的 throw 均不触发。

排除项(均已逐项核对):新旧 API 分支——报告者抓包时间戳 2026-10-04T19:47:40.056+08:00 正是 Go 版 getPersonalTime(新 API updatedAt)解析格式,旧 getDisk 是 20060102150405 紧凑格式,证明走的 personal_new 分支;请求体、字段名、headers 与 Go 版逐项一致;resolvePath/平台层无回退。

已修改(commit b48634c)

  • init() 硬失败:personal_new 拿不到路由下发的 personal host 即抛 [139] queryRoutePolicy failed: <原始错误>(对齐 Go 版);旧版个人云走 /orchestration 硬编码路径不依赖路由,仅告警;
  • buildRequestUrls:personal_new 拒绝把业务端点发到默认域(兜底长生命周期 isolate 的陈旧实例);候选序列「host 直拼(现网 /hcy 形态)→ orchestration 形态(裸 host 补前缀、/hcy 去前缀)」,仅 404 / route not found 回退,校验/权限错误原样抛出(对齐 boxplayer 语义);
  • id 并集 fileId || catalogId || catalogID;仍无 id 时 resolveCatalogId 抛 [139] folder "<名>" has no catalog id in listing;
  • 错误信息附带请求 URL、非 JSON 响应显式报错——无需函数日志也能看到打到了哪个 host;
  • 新增 3 组回归测试(id 并集/空 id 报错、init 硬失败、URL 候选与重试语义),累计 9/9 通过。

已测 / 未测(诚实边界)

  • ✅ 139 驱动单测 9/9、tsc --strict、prettier --check 通过
  • ❌ 无 139 账号凭据,未做端到端真实 API 验证;两条路径均为「机制分析 + 单测锁定」
  • 修复后「静默展示根目录内容」这一 bug class 被消除:要么列出子目录真实内容,要么返回带请求 URL 与状态码的明确错误

请报告者 @MoForgt 或有凭据的维护者部署本 PR 验证:若部署后仍出现无报错的根目录内容,请把页面/日志里的错误信息(现已包含请求 URL)贴回本 PR,可直接判定剩余问题在 host 还是上游数据。

@PIKACHUIM

Copy link
Copy Markdown
Member

验证了,并未修复问题,和#121一样

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants