Files
2026-08-11 17:09:23 +08:00

148 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件方案修正说明
> 对上一轮输出(`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 张要好。这不算"智能等待",只是把结果如实告诉用户。
---
## 修正后的 M1M4
| 里程碑 | 内容 | 关键改动 |
|---|---|---|
| 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 变体的、一个详情图很多的)。没有真实页面,选择器配置只能停在推测。