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.tsxsrc/utils/country-stats.tssrc/components/locations-select/simple-locations-select.tsxsrc/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 全字典扫的工作流