跳转至

[ISSUE-0055] saveParams 治本

ISSUE-0054 #1 用短期拦截兜底了 client mode 数据安全。本轮(v0.10.75)做长期治本:让 apiLocalDataDelete / apiLocalDataExport 真正按 UI 筛选范围操作。

病灶(v0.10.74 之前)

saveParams 字段:

{ viewId, selectKeys, sort, selectTotal, selectOption, filters, filter: {}, keyword, logic }

没有 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 全字典对齐