[ISSUE-0055] saveParams 治本¶
ISSUE-0054 #1 用短期拦截兜底了 client mode 数据安全。本轮(v0.10.75)做长期治本:让
apiLocalDataDelete/apiLocalDataExport真正按 UI 筛选范围操作。
病灶(v0.10.74 之前)¶
saveParams 字段:
没有 taskId / hasEmailOnly。这是 List API 早就接受的两个 JS filter 维度(jsstore where 不可靠 → 走全表 + JS filter),但 Delete/Export 没接受。
apiLocalDataDelete 只看 filters+keyword+logic:
let query = exViewFilterToQuery(filters, keyword, logic);
if (selectOption === 'front') { ... }
else if (selectOption === 'current') { query = { id: { in: selectKeys } }; }
await deleteByQuery(query); // 'all' 时 query 是空对象 → 删全表
数据灾难场景:
- 用户在 has-email chip + filterTaskId='X' 看 10 行
- 选 "全部 10 项"(selectOption='all')
- → server-side query = exViewFilterToQuery([], '', 'and') = {} → 删全表 22w
修¶
1. search.ts 抽 resolveTargetRows helper¶
统一三种 selectOption 行为:
async function resolveTargetRows(params: any): Promise<{
rows: any[] | null;
jsstoreQuery: any | null;
}> {
const { selectOption, selectKeys, selectTotal, filters, keyword, logic, sort,
taskId, hasEmailOnly } = params;
// 1. 'current' — 直接 id list(最快,最安全)
if (selectOption === 'current') {
return { rows: null, jsstoreQuery: { id: { in: selectKeys || [] } } };
}
const needsClientFilter = !!taskId || !!hasEmailOnly;
// 2. 无 JS filter → 走原 jsstore where(高效,保持 v0.10.74 之前的行为)
if (!needsClientFilter) {
const baseQuery = exViewFilterToQuery(filters, keyword, logic);
if (selectOption === 'all') return { rows: null, jsstoreQuery: baseQuery };
// 'front': 先 select 前 N → id list
const { list } = await getListByQuery(baseQuery, 1, selectTotal, sort);
return { rows: null, jsstoreQuery: { id: { in: list.map((r) => r.id) } } };
}
// 3. 有 taskId / hasEmailOnly → 全表 50k + JS filter + 截取
let scanned: any[];
if (taskId) {
const big = await getListByTaskId(taskId, 1, HAS_EMAIL_SCAN_LIMIT, sort, keyword);
scanned = big?.list || [];
} else {
const big = await getListByQuery(exViewFilterToQuery(filters, keyword, logic), 1, HAS_EMAIL_SCAN_LIMIT, sort);
scanned = big?.list || [];
}
if (hasEmailOnly) {
scanned = scanned.filter((r) => Array.isArray(r.emails) && r.emails.length > 0);
}
const target = selectOption === 'front' ? scanned.slice(0, selectTotal) : scanned;
return { rows: target, jsstoreQuery: null };
}
2. apiLocalDataDelete / Export 都用它¶
export async function apiLocalDataDelete(params: any) {
const { rows, jsstoreQuery } = await resolveTargetRows(params);
if (jsstoreQuery) await deleteByQuery(jsstoreQuery);
else if (rows?.length > 0) await deleteByQuery({ id: { in: rows.map((r) => r.id) } });
return { success: true };
}
export async function apiLocalDataExport(params: any) {
const { rows, jsstoreQuery } = await resolveTargetRows(params);
let exportRows: any[];
if (jsstoreQuery) {
const { list } = await getListByQuery(jsstoreQuery, 1, params.selectTotal || HAS_EMAIL_SCAN_LIMIT, params.sort);
exportRows = list || [];
} else {
exportRows = rows || [];
}
await exportAsCsvFile(params.fieldsData, exportRows);
return { success: true };
}
3. local-data-view.tsx saveParams 加新字段¶
const saveParams = {
...existing,
taskId: filterTaskId,
hasEmailOnly: quickFilter === 'has-email',
quickFilter,
};
4. 撤销 v0.10.74 短期拦截¶
onSelectChange 不再拦截 client mode 的 'all'/'front',因为 server 现在正确处理 UI 筛选范围。
效果¶
| 场景 | v0.10.74 | v0.10.75 |
|---|---|---|
| client mode + has-email chip + 选全部 10 项 → 删 | 拦截,提示先切完整模式 | 直接删这 10 个 has-email 商家 ✓ |
| client mode + filterTaskId='X' + 选全部 N 项 → 删 | 拦截 | 只删 task X 内的 N 个 ✓ |
| client mode + 无 filter + 选全部 22w → 删 | 拦截 | 走 jsstore native 全表删(同 server mode) |
| 导出场景同 | 拦截 | 正确导出筛选范围内 |
已知遗留(下轮)¶
- keyword 不一致:client 端 keyword 是
(name|domain|category).includes(k)(OR),server 端走 jsstore like — 可能搜到字段不同。selectOption='all'时用 server keyword 解析,可能比 client 看到的多/少几行。优先级低,下轮统一。 - filters 数组:高级筛选已自动切 server mode(ISSUE-0053 设计),filters 在 server 走 jsstore where,无 client/server 不一致问题。
audit_grep¶
- pattern: "apiLocalDataDelete\\(\\s*\\{[\\s\\S]{0,500}filters[\\s\\S]{0,200}\\}\\s*\\)"
description: "新 delete 调用必须经 saveParams 走(含 taskId+hasEmailOnly),不能裸传 filters"
元洞察¶
Agent 元洞察"useDataSource hook 抽成单一信号源"是更彻底的方案,但工程量大。 本轮先做最关键的数据安全治本(删/导按 UI 看到的范围操作),useDataSource hook 重构留下轮(如果再出 ISSUE-0056/0057 才上)。
短期补丁 + 长期治本的分离 — agent 提示了重构方向但不强求一次到位。
相关¶
- [[0054-round10-agent-a2-6-bugs|0054-第10轮agent-A2-6处真bug]] — 上一轮 #1 用拦截兜底
- [[0053-a2-merchant-client-server-hybrid|0053-A2商家列表client-server-hybrid]] — A2 上线引入问题
- 修bug全字典扫描 — saveParams / delete / export 全字典对齐