148 lines
7.6 KiB
Markdown
148 lines
7.6 KiB
Markdown
# 插件方案修正说明
|
||
|
||
> 对上一轮输出(`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` 里、图片在 `<picture>` 中等
|
||
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 变体的、一个详情图很多的)。没有真实页面,选择器配置只能停在推测。
|