# 插件方案修正说明 > 对上一轮输出(`profiles-ozon.ts` / `download-implementation.ts` / `IMPLEMENTATION_PLAN.md`)的复核 > 最后更新:2026-08-11 > 上游:[总体架构](../architecture.md) · [契约](../contracts/product-json.md) 按你的三点反馈复核后,方案主体成立,但有 5 处需要改。**R1 和 R2 是实质性问题**,其余是准确性修正。 --- ## R1 · 保存目录:downloads API 做不到「选目录」🔴 上一轮说「完全采用 1688 的 downloads 方案」,这个结论对 1688 插件成立,对我们**不成立**。 `chrome.downloads.download()` 的 `filename` 只能是**下载目录下的相对路径**,不接受绝对路径,也不接受 `..`。1688 插件够用是因为它只需要「按商品名建子目录」;而你的需求里有一条它没有: > 用户可以选择采集数据保存的目录,这样同一商品不同平台采集的数据放在同一文件夹内 downloads API 下这意味着每次都落在 `~/Downloads/<商品名>/`,用户无法指定别的位置,也无法可靠地"追加到上次那个文件夹"(只能靠商品名字符串撞对)。 ### 改用 File System Access API ```ts // 首次:用户选一次根目录(如 ~/Ozon商品库) const rootHandle = await window.showDirectoryPicker({ mode: 'readwrite' }); await idbSet('SH_ROOT_DIR', rootHandle); // IndexedDB 可持久化存 handle // 之后:无需再授权,直接建/进商品子目录 const root = await idbGet('SH_ROOT_DIR'); if (await root.queryPermission({ mode: 'readwrite' }) !== 'granted') { await root.requestPermission({ mode: 'readwrite' }); // 极少数情况需重新确认 } const productDir = await root.getDirectoryHandle('儿童保温杯_316', { create: true }); const imagesDir = await productDir.getDirectoryHandle('images', { create: true }); const mainDir = await imagesDir.getDirectoryHandle('main', { create: true }); const fh = await mainDir.getFileHandle('main-001.jpg', { create: true }); const w = await fh.createWritable(); await w.write(blob); await w.close(); ``` 关键点: - **handle 能存进 IndexedDB 并跨会话复用**,不用每次弹框。这正好支撑"Ozon 采完切 1688 追加到同一文件夹"。 - 只能在**扩展页面上下文**调用(side panel 可以,content script 不行)。采集在 content script,写盘在 side panel,正好符合现有分工。 - 能**读回** `sources.json` 做去重(downloads API 只能写不能读,这是它第二个致命短板)。 - 图片字节仍需 background 代理 fetch(绕 CORS / 防盗链),拿到 blob 再交给 side panel 写盘。 `chrome.downloads` 保留为降级路径:用户拒绝授权目录时,退回 `~/Downloads/<商品名>/`。 --- ## R2 · Ozon 选择器全部未经验证 🔴 上一轮 `profiles-ozon.ts` 里的选择器**是我根据 Ozon 的通用 DOM 惯例推测的,没有在真实页面上跑过**。其中: | 选择器 | 可信度 | 说明 | |---|---|---| | `[data-widget="webProductHeading"]` | 🟡 中 | Ozon 确实用 `data-widget` 标记区块,但具体名称需实测 | | `[data-widget="webGallery"]` | 🟡 中 | 同上 | | `.tsHeadline500Medium` | 🟡 中 | Ozon 设计系统的 typography class,相对稳定 | | `.k1p_27` `.e5k_27` `.c2h9_27` `.h9o_27` `.RA-a1` | 🔴 低 | **哈希类名,每次发版就变,等于无效** | 哈希类名写进配置是负资产——它给人"有兜底"的错觉,实际上一周后就失效。**M2 第一步必须是在真实 Ozon 页面上实测,把哈希类名全部替换掉。** 替代思路,按优先级: 1. **`data-widget` 属性**:Ozon 的区块标记,改版时相对稳定 2. **JSON-LD / `__NUXT__` 之类的内嵌数据**:见 R3 3. **结构关系**:`h1` 在页面第一个 `data-widget` 里、图片在 `` 中等 4. **哈希类名**:只在实测确认当前有效时临时用,并标注"随时会失效" --- ## R3 · 内嵌 JSON 可能比 DOM 选择器更靠得住 🟡 > **2026-08-11 更新:这条对淘宝/天猫已证伪。** 两站实测 `script[type="application/ld+json"]` > 都是空数组,只能走 DOM(详见 [`selectors-taobao.md`](./selectors-taobao.md) §4.2)。 > 下面的推理对 Ozon 仍待验证——Ozon 是 SSR 电商站,带 JSON-LD 的概率仍然不低。 > > 另外淘宝 `window` 上有 `__general_skupanel_cache_data` 等键可能含结构化数据, > 但 MV3 content script 默认在 isolated world,读不到页面 `window`,需 `world: 'MAIN'`。二期评估。 上一轮把接口抓取评估为"MV3 下 webRequest 读不到响应体,建议以 DOM 为主"——这个结论对**网络层拦截**是对的,但漏了第三条路。 Ozon 是 SSR + 水合,页面 HTML 里通常带完整的商品数据(`application/ld+json`、或挂在 `window` 上的 state)。这是**同步可读、无需拦截网络**的: ```ts // 路径 A:JSON-LD(标准化,最稳) document.querySelectorAll('script[type="application/ld+json"]') // → { "@type": "Product", name, sku, brand, offers: { price, priceCurrency }, image[] } // 路径 B:内嵌 state(字段全,但结构随版本变) // 实测时在 Console 里翻 window 上的候选键 ``` 若实测发现 Ozon 的 JSON-LD 里就有标题、价格、品牌、图片列表,那**主路径应该是解析 JSON-LD,DOM 选择器降级为兜底**——JSON-LD 有 schema.org 标准约束,比哈希类名稳定一个数量级。 M2 的实测任务因此扩为两条:DOM 选择器 + 内嵌 JSON,看哪条覆盖率高。 --- ## R4 · product.json 生成逻辑要移出插件 🟡 上一轮 `download-implementation.ts` 里的 `buildOzonProductJson()` 试图填 `attributes[].id`,还标了 `// 需要查询Ozon类目属性字典`。这块**插件做不了也不该做**:属性 id 依赖类目,类目在工作台才定。 按契约([product-json.md §4](../contracts/product-json.md)): ``` 插件 → 写 _raw.params(原始 kv),attributes 留空数组 工作台 → 定类目 → 拉字典 → 映射 attributes ``` 另外两处要改: - `offer_id` 上一轮注释成"需要用户填写",应明确**采集阶段恒为空字符串**。跟卖场景下沿用竞品货号是错的。 - `images` 上一轮直接填了采集到的源站 URL。应填**本地相对路径**到 `_images`,`images` 字段留空——Ozon 要的是我们自己图床的公网 URL,源站 URL 提交上去等于盗链且随时失效。 --- ## R5 · 就绪检测保留人工控制,但补一条提示 🟢 你的判断对,人工触发能绕开绝大部分动态渲染问题,一期不做 MutationObserver。 只补一点:Ozon 详情图是**滚动懒加载**的,用户不滚到底部时详情图根本不在 DOM 里。所以侧边栏在检测到 `detail` 组为 0 张时,要提示: ``` 主图 6 · SKU 4 · 详情 0 ⚠️ 详情图为 0,请滚动到页面底部让图片加载后重新采集 ``` 比静默采到 0 张要好。这不算"智能等待",只是把结果如实告诉用户。 --- ## 修正后的 M1–M4 | 里程碑 | 内容 | 关键改动 | |---|---|---| | M1 | product.json 契约 + TS 类型 | 新增,先定契约 | | **M2** | **Ozon 实测:选择器 + 内嵌 JSON 双路径调研** | R2/R3,**这一步必须在真实页面上做,是整个插件的地基** | | M3 | 采集引擎 + 侧边栏表单 + 图片分组勾选 | | | M4 | File System Access 写商品文件夹 | R1,替换 downloads 方案 | | M5 | 1688 profile + 读 sources.json 去重追加 | | M2 需要你提供 3–5 个不同类目的 Ozon 商品页链接(最好含一个有 SKU 变体的、一个详情图很多的)。没有真实页面,选择器配置只能停在推测。