7.6 KiB
插件方案修正说明
对上一轮输出(
profiles-ozon.ts/download-implementation.ts/IMPLEMENTATION_PLAN.md)的复核 最后更新:2026-08-11 上游:总体架构 · 契约
按你的三点反馈复核后,方案主体成立,但有 5 处需要改。R1 和 R2 是实质性问题,其余是准确性修正。
R1 · 保存目录:downloads API 做不到「选目录」🔴
上一轮说「完全采用 1688 的 downloads 方案」,这个结论对 1688 插件成立,对我们不成立。
chrome.downloads.download() 的 filename 只能是下载目录下的相对路径,不接受绝对路径,也不接受 ..。1688 插件够用是因为它只需要「按商品名建子目录」;而你的需求里有一条它没有:
用户可以选择采集数据保存的目录,这样同一商品不同平台采集的数据放在同一文件夹内
downloads API 下这意味着每次都落在 ~/Downloads/<商品名>/,用户无法指定别的位置,也无法可靠地"追加到上次那个文件夹"(只能靠商品名字符串撞对)。
改用 File System Access API
// 首次:用户选一次根目录(如 ~/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 页面上实测,把哈希类名全部替换掉。
替代思路,按优先级:
data-widget属性:Ozon 的区块标记,改版时相对稳定- JSON-LD /
__NUXT__之类的内嵌数据:见 R3 - 结构关系:
h1在页面第一个data-widget里、图片在<picture>中等 - 哈希类名:只在实测确认当前有效时临时用,并标注"随时会失效"
R3 · 内嵌 JSON 可能比 DOM 选择器更靠得住 🟡
2026-08-11 更新:这条对淘宝/天猫已证伪。 两站实测
script[type="application/ld+json"]都是空数组,只能走 DOM(详见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)。这是同步可读、无需拦截网络的:
// 路径 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):
插件 → 写 _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 变体的、一个详情图很多的)。没有真实页面,选择器配置只能停在推测。