跳转至

title: "[ISSUE-0043] 用户截图:选「1 国」整国采集,提交报「请选具体州/城市」— API shape 防御不够 + 错误消息误导" description: 4 个 caller 全用 Array.isArray(r) ? r : r?.items 二选一,遇到 { data: [...] } 等深包装 shape 全部退化 → 误显示"请选州/城市" tags: [issue, api-shape, error-message-misleading, defensive-parsing, pattern-replication] created: 2026-05-27 updated: 2026-05-27 type: issue status: fixed severity: high fixed_version: v0.10.63 related: - "[[0037-state-load-fail-mistaken-as-empty|0037-州省加载失败被误显示为无数据]]" - "[[0040-round6-agent-6-bugs-incomplete-patch|0040-第6轮agent发现6个真bug-补丁不彻底家族]]" - "[[0041-round7-agent-4-misses-patch-sequel|0041-第7轮agent发现4个漏修-补丁不彻底续集]]" audit_grep: - pattern: "Array\.isArray\(r\)\s\?\sr\s:\sr\?\.items" description: "旧式两 shape 防御 — 必须用 normalizeLocations 覆盖 5 种"


[ISSUE-0043] 用户截图:选「1 国」整国采集,提交报「请选具体州/城市」

用户操作(v0.10.61):创建任务 dialog → doctor 关键词 → 国家=United States(mode='all') → 城市级精度 → 绿色 banner "1 国 · 1 区域 = 1 个任务" → 点创建 → ❌ 红色 toast "未能创建任务(请检查是否选了具体州/城市)"

但用户明明就是选了"整国"!

病灶

src/sections/task/task-create-dialog.tsx:114-116 (v0.10.61):

const r: any = await apiCountryLocations({ code: iso2 });
const items = (Array.isArray(r) ? r : (r?.items || [])) as { v: string; c?: ... }[];
//                                   ^^^^^^^^^^^^^^^^
// 只兜 2 种 shape:直接 array 或 { items: [...] }

API https://locations.api-google-maps.laifayun.com/${code}.json 实际返回的 shape 不在这两种之列(可能 { code, data: [...] }{ data: { items: [...] } } 等深包装)→ items=[]locationEntries=[] → 触发 line 141 "请选具体州/城市"。

误导链

实际 用户认知
API 响应格式不在防御列表 我选了具体国家啊
items=[] 静默退化 哪里没选对?
错误消息说"请选具体州/城市" 还要点进去选州?反复试

这是 ISSUE-0037 同精神:"加载失败" 被误显示为"无数据" — 但本次是"shape 不匹配" 被误显示为"用户操作错"。

1. 抽统一 normalizeLocations 函数(src/api/others.ts

export function normalizeLocations(r: any): LocationNode[] {
  if (!r) return [];
  if (Array.isArray(r)) return r as LocationNode[];           // 1. 直接 array
  if (Array.isArray(r.items)) return r.items;                  // 2. { items }
  if (Array.isArray(r.data)) return r.data;                    // 3. { data: [...] }
  if (Array.isArray(r?.data?.items)) return r.data.items;      // 4. { data: { items } }
  if (Array.isArray(r.list)) return r.list;                    // 5. { list }
  return [];
}

2. 4 个 caller 全切换

  • src/api/others.ts(新加 normalizeLocations 导出)
  • src/sections/task/task-create-dialog.tsx
  • src/utils/country-stats.ts
  • src/components/locations-select/simple-locations-select.tsx
  • src/components/locations-select/location-picker-dialog.tsx

3. task-create-dialog 区分错误消息

if (items.length === 0) {
  console.warn('[task-create] normalizeLocations got empty:', { iso2, raw: r });
  notice.error(
    `无法解析 ${csel.name} 地区数据(API 响应格式异常,请尝试更新版本或反馈给开发者)`
  );
  return;
}

mode='all' 下展开后才知道有没有数据 — 现在区分了"shape 异常" vs "用户没选具体路径"。

历史教训

这是第 5 次同模块出错 — 错误传播家族 / 补丁不彻底家族在 API 防御上的延伸:

ISSUE 修复点 漏的
0037 catch → setX([]) → "无数据" 同款其他 catch
0040 A3 task-create-dialog catch 提示具体原因 防御只 2 shape
0041 Bug 3 country-stats + simple-locations-select Array.isArray 兜底 还是只 2 shape
0042 scan 工具结构性盲区(try/catch + finally + 裸调) API shape 范畴外
0043 抽 normalize helper 覆盖 5 shape + 区分误导消息 如果未来 API 加第 6 种 shape,需扩 helper

audit_grep 闸

audit_grep:
  - pattern: "Array\\.isArray\\(r\\)\\s*\\?\\s*r\\s*:\\s*r\\?\\.items"
    description: "旧式两 shape 防御  必须改用 normalizeLocations"

任何新增 apiCountryLocations caller 用旧防御 → pre-commit 自动拦截。

元洞察

防御代码本身就是约定 — 抽不出 helper 时,每个 caller 各自定义"我接受的 shape", 一旦 API 变 → 全员同时漏 → "补丁不彻底"百分百复现。

修法不是"再加一种 shape",而是抽统一函数集中维护。下次 API 变只改 1 处。

相关

  • [[0037-state-load-fail-mistaken-as-empty|0037-州省加载失败被误显示为无数据]] — "API 失败 → 误显示为无数据" 同精神
  • [[0040-round6-agent-6-bugs-incomplete-patch|0040-第6轮agent发现6个真bug-补丁不彻底家族]] — A3 是本 bug 上一轮防御
  • [[0041-round7-agent-4-misses-patch-sequel|0041-第7轮agent发现4个漏修-补丁不彻底续集]] — Bug 3 同款
  • 修bug全字典扫描 — 4 处 caller 全字典扫的工作流