Expand seller store functionality

This commit is contained in:
R524809
2026-09-07 17:51:11 +08:00
parent 4c0f3a4a79
commit ceb644a3f9
35 changed files with 32883 additions and 364 deletions
+5 -4
View File
@@ -32,8 +32,9 @@
12. [`architecture/database-security.md`](./architecture/database-security.md):迁移/应用数据库角色、RLS 强制策略与部署职责。
13. [`architecture/transactional-outbox-and-idempotency.md`](./architecture/transactional-outbox-and-idempotency.md):关键写请求去重、事务 Outbox、BullMQ 投递与验收规则。
14. [`guides/object-storage-object-key.md`](./guides/object-storage-object-key.md):对象存储、`object_key`、来源 URL 与预签名 URL 的使用边界。
15. [`guides/minio-local-deployment.md`](./guides/minio-local-deployment.md):MinIO 简介、本地 Docker 部署与应用侧使用方法。
16. [`guides/postgresql-local-development.md`](./guides/postgresql-local-development.md):原生 PostgreSQL / Docker Compose 本地数据库、角色、迁移与建表。
17. [`api/README.md`](./api/README.md):HTTP API 统一约定及第一批账户、门店、水印接口。
18. [`reviews/implementation-readiness-open-items.md`](./reviews/implementation-readiness-open-items.md):实现前必须闭环的数据库/API 事项与已确认约束。
15. [`guides/browser-extension-background-service-worker.md`](./guides/browser-extension-background-service-worker.md):浏览器扩展后台 Service Worker 的运行模型、消息调用链与调试方法。
16. [`guides/minio-local-deployment.md`](./guides/minio-local-deployment.md):MinIO 简介、本地 Docker 部署与应用侧使用方法。
17. [`guides/postgresql-local-development.md`](./guides/postgresql-local-development.md):原生 PostgreSQL / Docker Compose 本地数据库、角色、迁移与建表。
18. [`api/README.md`](./api/README.md):HTTP API 统一约定及第一批账户、门店、水印接口。
19. [`reviews/implementation-readiness-open-items.md`](./reviews/implementation-readiness-open-items.md):实现前必须闭环的数据库/API 事项与已确认约束。
19. 后续按模块补全定价、采集、商品编辑、发布和在线商品接口。
+6 -29
View File
@@ -4,42 +4,19 @@
## 1. 浏览器扩展授权
扩展不读取 ERP 网页 localStorage,也不用 Web 端 refresh cookie。每个发布扩展在服务端登记 `client_id` 和允许的 `chrome-extension://{extension-id}/…` 回调地址,扩展使用 PKCE 授权码流程。
首期按毛子 ERP 风格复用 Atlas Web 登录态。用户从插件点击登录后,扩展打开固定 Atlas Web 页面;用户在该页完成登录或会话恢复后,Web 将当前短期 access token 镜像到本站 `localStorage["atlas.extension.web-access"]`。扩展每 2 秒仅从该 Atlas 标签读取这个值,保存到 Chrome extension storage,并以 Bearer token 调用下述接口。
1. 扩展打开:`GET /api/extension/authorize?client_id={id}&redirect_uri={uri}&state={random}&code_challenge={S256}&code_challenge_method=S256`。
2. 用户在 Atlas Web 完成登录;首期不额外展示工作空间选择/授权确认页。
3. 服务端只向已登记的 `redirect_uri` 回跳 `?code={one-time}&state={original}`。授权码 60 秒有效、只能交换一次,且绑定 client、回调地址和 PKCE challenge。
4. 扩展调用 `POST /api/extension/auth/exchange`:
```json
{
"client_id": "atlas-collector",
"code": "one-time-code",
"redirect_uri": "chrome-extension://extension-id/callback",
"code_verifier": "original-pkce-verifier"
}
```
成功返回扩展专用短期 access token 与可轮换 refresh token;令牌只存 Chrome 扩展专属存储:
```json
{ "data": {
"access_token": "eyJ…", "expires_in": 900, "refresh_token": "opaque", "refresh_token_expires_in": 2592000,
"token_type": "Bearer"
} }
```
`POST /api/extension/auth/refresh` 接收 `{ "refresh_token": "opaque" }`,轮换并返回同形状令牌;`POST /api/extension/auth/revoke` 撤销当前扩展 refresh token,返回 `204`。令牌续期、撤销和错误响应不得泄露 Web 会话或其他工作空间信息。
这是维护者批准的自托管插件例外:扩展不读取 refresh cookie、Cookie、来源站 localStorage 或来源站授权信息;Web token 不得进入日志、数据库、Outbox 或采集载荷。Token 失效返回 `401` 后,插件清除自己的缓存并要求用户再次从 Atlas Web 登录同步。完整取舍见 [`../project/extension-login-alternatives.md`](../project/extension-login-alternatives.md)。
`GET /api/extension/session`
使用扩展 access token,返回最小化状态:
使用从 Atlas Web 同步的 Bearer token,返回最小化状态:
```json
{ "data": { "authenticated": true, "workspace": { "id": "uuid", "available": true }, "can_collect": true } }
```
不返回平台、默认店铺、Cookie、Ozon 凭据或 Web refresh token。令牌失效返回 `401`,插件只展示“登录 Atlas ERP”。
不返回平台、默认店铺、Cookie、Ozon 凭据或 Web refresh token。令牌失效返回 `401`,插件清除缓存并只展示“登录 Atlas ERP”。
## 2. 插件上报采集结果
@@ -49,14 +26,14 @@ Headers:扩展 Bearer token 与 `Idempotency-Key` 必填。请求体为已定
请求体不得包含或传入 `workspace_id`、`target_platform_id`、`platform_id`、`store_id`、`api_key`、Cookie、授权头或来源站会话字段。服务端从扩展令牌解析工作空间,并将来源 `platform` 映射为系统已启用的来源平台。
服务端在一个事务中:检查工作空间采集箱上限 → 按 `workspace + source_platform + source_item_key` 查找活动记录 → 创建或复用 `collection_item`、`product`、来源确认的 `product_variants` 和持久化 `collection_asset_transfers` → 再经 outbox 投递素材 Worker。重复上报不会增加采集箱配额,也不覆盖用户已编辑草稿。
服务端在一个事务中:检查工作空间采集箱上限 → 按 `workspace + source_platform + source_item_key` 查找活动记录 → 创建记录,或以本次页面快照覆盖已有记录的来源 URL、标题、价格、原始证据、规范化载荷、SKU 草稿与候选素材 → 再经 outbox 投递素材 Worker。重复上报不会增加采集箱配额,且会保留原有 `collection_item` 与 `product` ID;旧素材任务被取消,新素材任务使用新的来源快照版本,旧 Worker 不得回写新快照。
成功返回 `201`(新建)或 `200`(复用):
```json
{ "data": {
"collection_item": { "id": "uuid", "status": "materializing", "source_title": "商品标题", "variant_count": 3 },
"product_id": "uuid", "reused": false,
"product_id": "uuid", "reused": false, "refreshed": false,
"editor_entry_path": "/products/uuid",
"materialization": { "total": 12, "queued": 12, "succeeded": 0, "failed": 0 }
} }
@@ -0,0 +1,73 @@
# 浏览器扩展的 Background Service Worker
> 本文以 Atlas 采集插件为例,说明 Chrome Manifest V3 扩展的后台运行环境、消息调用链与调试方法。
## 1. 它是什么
`Background Service Worker` 是 Chrome Manifest V3 扩展的后台运行环境。它不属于任何网页,也没有固定 UI;Chrome 在收到消息、标签页事件等需要处理的事件时唤醒它,空闲后停止它以节省资源。
因此,后台代码不能假设进程始终存活,也不能只用模块变量保存登录态或业务状态。需要跨唤醒保留的数据应写入 `chrome.storage`;本项目使用 `chrome.storage.local` 保存插件同步得到的短期登录令牌。
## 2. Atlas 采集插件中的调用链
```text
商品网页(Content Script)
点击“编辑上架”
│ browser.runtime.sendMessage(...)
▼
扩展 Background Service Worker
读取 chrome.storage 中的登录令牌
fetch Atlas API,并附加 Authorization
│
▼
Atlas API
│
▼
Background 将结果回传给 Content Script
打开采集测试页 / 展示错误
```
项目中三个环境彼此隔离:
| 环境 | 运行位置 | 负责内容 |
|---|---|---|
| 商品网页 | Ozon、1688、淘宝、天猫等页面 | 原网站 DOM 与原网站 Network 请求 |
| Content Script | 注入商品网页的扩展脚本 | 悬浮窗、商品解析、向后台发送消息 |
| Background Service Worker | `chrome-extension://…` 扩展上下文 | 登录令牌、跨站 API 请求、打开标签页 |
采集请求由 Background 发出,所以它不会出现在商品网页 DevTools 的 Network 面板。商品页 DevTools 只能看到来源网站自身的网络请求。
## 3. 为什么采用后台请求
- 登录令牌保留在扩展存储中,不必暴露给来源商品页的 JavaScript。
- Content Script 仅负责页面交互和净化后的商品载荷;API 鉴权、请求和跳转集中在后台处理。
- 后台拥有扩展声明的 `host_permissions`,可访问 Atlas API 与受支持来源站点;页面本身的跨域限制不会决定插件后台请求的权限。
后台权限仍应遵循最小化原则:本项目只允许访问 Atlas API、Atlas Web 与明确支持的来源站点,绝不读取来源站 Cookie 或 localStorage。
## 4. 前端如何调试
### 4.1 调试后台 API 请求
1. 打开 `chrome://extensions`,开启“开发者模式”。
2. 找到 Atlas 采集插件,点击卡片中的 **Service Worker** 或“检查视图”。
3. 在打开的 DevTools 中查看:
- **Console**:后台异常、消息处理失败;
- **Network**:`/api/extension/session`、`/api/extension/materials` 等 Atlas API 请求、状态码与响应体;
- **Application → Storage → Extension storage**:插件保存的登录态数据。
这里才是排查 `Authorization` 请求头、API 4xx/5xx,以及采集写入失败的正确位置。
### 4.2 调试悬浮窗和商品解析
在商品详情页按 F12,使用该页面的 DevTools:
- **Console**:查看 Content Script 的页面解析、悬浮窗和 `sendMessage` 调用错误;
- **Sources**:寻找 `chrome-extension://…/content-scripts/collector.js`,可对商品解析或点击处理函数下断点;
- **Network**:只用于分析 Ozon、1688、淘宝、天猫页面自身的请求与页面数据来源,不应期待看到 Atlas API 请求。
### 4.3 Service Worker 生命周期注意事项
Service Worker 空闲后会被 Chrome 停止;再次刷新商品页、点击悬浮窗或收到扩展事件时才会重新唤醒。调试时建议在 Service Worker DevTools 勾选“保留日志”,避免后台停止后丢失 Console 输出。
不要依赖 Service Worker 模块变量存储登录状态或待处理任务。需要持久化的数据应使用 `chrome.storage`、后端数据库或其他可恢复的存储;异步操作应能安全地在下一次后台唤醒后继续或重试。
+32 -6
View File
@@ -1,12 +1,12 @@
# Atlas Seller Kit 交接说明
> 更新:2026-09-06。此文是新 AI 对话、架构讨论与开发工作的首读入口。开始开发前,必须先阅读并遵守 [`development-collaboration-contract.md`](./development-collaboration-contract.md)。
> 更新:2026-09-07。此文是新 AI 对话、架构讨论与开发工作的首读入口。开始开发前,必须先阅读并遵守 [`development-collaboration-contract.md`](./development-collaboration-contract.md)。
## 1. 项目定位
这是一个全新的云端产品,不是 `ozon-seller-kit` 的本地版重构。目标用户可拥有多个门店,每个门店只能绑定一个平台;平台自身可支持多个市场、国家、语言和币种。因此语言、币种、类目和发布规则不能写死为 Ozon 或中文/俄文。
当前已完成 pnpm monorepo 初始化:包含 Web、NestJS API、BullMQ Worker、WXT 浏览器扩展、共享 API 契约、数据库包、本地基础设施 Compose 与依赖锁文件。API 已实现健康/readiness、集中配置与安全基础、认证、个人工作空间上下文、平台目录/上下文、门店、水印/素材、采集箱与扩展采集入库;Worker 已具备 Ozon 凭据验证、采集记录清理和来源素材转存消费链路,仓库同步尚未接入。Web 已具备完整前端框架(布局、路由、主题)、登录/注册页、页头平台选择器、店铺管理、水印管理、真实采集箱页面和采集数据测试页;浏览器扩展已提供 PKCE 授权、悬浮窗及 Ozon/1688/淘宝/天猫首版页面状态解析。不要从旧项目复制 SQLite 模型、无鉴权 API、旧 `/trial/:id` 或商品编辑页组件。可选择性复用已经验证过的纯计价公式、采集扩展思路和 AI 任务经验,但必须先适配多租户和云端边界。
当前已完成 pnpm monorepo 初始化:包含 Web、NestJS API、BullMQ Worker、WXT 浏览器扩展、共享 API 契约、数据库包、本地基础设施 Compose 与依赖锁文件。API 已实现健康/readiness、集中配置与安全基础、认证、个人工作空间上下文、平台目录/上下文、门店、水印/素材、采集箱与扩展采集入库;Worker 已具备 Ozon 凭据验证、采集记录清理和来源素材转存消费链路,仓库同步尚未接入。Web 已具备完整前端框架(布局、路由、主题)、登录/注册页、页头平台选择器、店铺管理、水印管理、真实采集箱页面和采集数据测试页;浏览器扩展已提供毛子 ERP 风格的网页登录态同步、悬浮窗及 Ozon/1688/淘宝/天猫首版页面状态解析。不要从旧项目复制 SQLite 模型、无鉴权 API、旧 `/trial/:id` 或商品编辑页组件。可选择性复用已经验证过的纯计价公式、采集扩展思路和 AI 任务经验,但必须先适配多租户和云端边界。
当前运行时基线:Node 24.20.0、pnpm 11.25.0、PostgreSQL 18.6、Redis 7.4.11、MinIO `RELEASE.2025-09-07T16-13-09Z`;Web 默认端口为 `8100`,API 默认端口为 `8200`,扩展 dev server 默认端口为 `8110`(仅本地开发用),Worker 无 HTTP 端口。完整目录、依赖边界与版本表见 [`architecture/project-structure.md`](../architecture/project-structure.md) 和 [`architecture/technology-and-deployment.md`](../architecture/technology-and-deployment.md)。
@@ -89,7 +89,7 @@
### 1.9 开发进度(2026-09-05):M2.2 / M2.3 扩展采集首个闭环
- 已实现扩展 PKCE 授权码、扩展专用 access/refresh token 轮换、现有 Atlas Web 会话的静默授权分支,以及扩展会话检查/撤销。扩展不读取 Web token 或 refresh Cookie;回调 URI 必须按已安装扩展 ID 精确登记。
- 已实现毛子 ERP 风格的 Atlas Web 登录态同步:扩展打开 Atlas Web 标签,轮询读取本站 localStorage 中的短期 Web access token,保存到 extension storage 并用 Bearer token 检查会话/采集。扩展不读取 refresh Cookie、来源站 Cookie 或来源站 localStorage。
- 已实现 `POST /api/extension/materials`:从扩展身份推导工作空间,创建或复用未绑定目标平台/门店的采集记录、SPU、SKU 草稿和来源素材任务。采集载荷严格限制为来源证据、规范化字段和素材 URL,不能携带店铺、Cookie、API Key 或目标平台。
- Worker 已消费素材转存 outbox 事件,执行 HTTPS 公网地址校验、大小/图片格式限制、哈希去重与 MinIO/Kodo 转存;单个素材失败不回滚采集箱商品。
- 浏览器扩展已实现 Ozon 公开商品页的小型 Shadow DOM 悬浮窗及未登录/已登录状态。Ozon 首版解析器以当前公开 SKU 和公开变体状态为证据,不生成笛卡尔积假 SKU。Ozon Seller 四接口本地观察、Ozon 变体详情逐页补全及 1688/淘宝/天猫解析仍未开始。
@@ -100,7 +100,32 @@
- 扩展浮窗现覆盖 1688 商品详情、淘宝商品详情和天猫商品详情。1688 仅遍历 `skuMapOriginal` 中实际存在的组合;淘宝/天猫仅在同时取得 `skuBase.skus[].skuId + propPath` 时创建多 SKU,并以 `skuCore.sku2info` 补充参考价格。规格轴本身从不展开为笛卡尔积。
- 缺少真实组合证据时,扩展只保存规格轴,服务端创建一个默认 SKU 并标记为待确认;颜色等轴值图片仍作为候选素材,绝不伪造来源 SKU 图片绑定。
- “编辑上架”暂时不进入未完成的商品编辑模块,改为打开 `/collection-test/{collectionItemId}`。该只读测试页展示实际保存的规范化载荷、原始证据、SKU 草稿和素材转存任务,用于验证每个平台的采集结果。`GET /api/collection-items/{id}` 已提供该工作空间隔离的检查读模型。
- 已通过 API/Worker TypeScript 检查、Web 与扩展生产构建;`apps/api/test/extension.e2e.test.ts` 已覆盖 PKCE 授权、扩展会话、采集幂等复用和测试页所用检查读模型。尚未在真实来源商品页执行样本回归,接手者应按第 6 节先完成该验证。
- 已通过 API/Worker TypeScript 检查、Web 与扩展生产构建;`apps/api/test/extension.e2e.test.ts` 已覆盖扩展会话、采集幂等复用和测试页所用检查读模型。尚未在真实来源商品页执行样本回归,接手者应按第 6 节先完成该验证。
### 1.11 开发进度(2026-09-07):扩展网页登录态同步与稳定 ID
- 扩展登录采用毛子 ERP 模式:点击后打开并保留 Atlas Web 标签;Web 登录或恢复会话后写入本站 localStorage,扩展每 2 秒读取并验证该短期 Web Bearer token。没有有效网页会话时,用户在同一标签登录后即可完成插件登录。
- Atlas Web access token 写入 localStorage 是维护者批准的窄范围例外;扩展不读取 refresh Cookie、来源站 Cookie 或来源站 localStorage。Token 失效后插件清除缓存并重新要求网页登录。
- 不再需要 `EXTENSION_COLLECTOR_REDIRECT_URI` 或 `identity` 权限;方案 A 的网页确认 + PKCE 配对设计单独记录在 [`extension-login-alternatives.md`](./extension-login-alternatives.md),当前不作为插件默认路径。
- `apps/extension/wxt.config.ts` 的 manifest 带有仅公开的 RSA `key`,Chrome 为解压加载和自托管包保持稳定扩展 ID;私钥未写入仓库。`wxt build` 已确认最终 `manifest.json` 含该 key。
- 已通过 contracts、API(含测试 TS)、Web、扩展的类型/生产构建,以及 `apps/api/test/extension.e2e.test.ts`:Web Bearer 会话检查 → 工作空间解析 → 采集幂等上报。
- 同一工作空间内的同一来源商品再次采集时,系统保留 `collection_item` / `product` ID,但用最新页面快照覆盖来源标题、价格、证据、SKU 与候选素材。迁移 `013_collection_source_snapshot_refresh.sql` 为记录和素材任务增加快照版本;旧转存任务取消,Worker 仅处理当前版本,不能将旧页面内容回写到新快照。
- Ozon 公开商品页解析器现从 JSON-LD `BreadcrumbList` 提取来源类目(可见面包屑为回退),保存当前页面语言的类目路径及对应面包屑证据;解析器版本为 `ozon-public-1.1.0`。
### 1.12 开发进度(2026-09-07):Ozon 公开多 SKU 采集与归并
- 解析器改为优先读取 Ozon 已渲染的 `state-webAspects-*.data-state.aspects[].variants[]`,不再用脚本文本正则猜测 SKU。保存的三 SKU 页面快照 `packages/reference/ozon1.html` 已确认能直接得到 `5387004839`、`5387005639`、`5387005221`。
- 增加只限 Ozon MAIN world 的页面 JSON bridge:只接受数字 SKU 和固定的 `modal/aspectsNew` / `product/<sku>` 路径,白名单返回 `webAspects`、`webProductHeading`、`webPrice`、`webGallery` 状态;不暴露 Cookie、请求头或任意请求能力。扩展以并发 3、最多 100 SKU 队列补全实际变体详情;个别失败保留已发现变体和证据。
- Ozon 公开页面没有稳定 SPU/组 ID;当前 URL SKU 仅保存为入口证据。API 对 Ozon 用本次 `source_sku` 集合与已有 `product_variants.source_sku` 的交集复用并覆盖同一采集快照,允许用户从同组任一 SKU 页面重复采集。多个既有记录命中时返回 `SOURCE_VARIANT_GROUP_AMBIGUOUS`,不静默合并。
### 1.13 开发进度(2026-09-07):Ozon 商品特征、描述与素材预览
- `/collection-test/{collectionItemId}` 的“素材转存任务”现在直接展示转存成功素材的缩略图,并显示关联来源 SKU。页面为每个 `succeeded + asset_id` 的任务按 4 路并发申请素材短时预览 URL;URL 只保存在浏览器内存,不能写回采集记录、产品或对象存储路径。没有 SKU 直接关联的规格轴图片会明确显示为“商品级”。已在现有三 SKU 采集记录 `7172fa38-0e4d-485e-9d53-1bd92aced79a` 实测确认 37 张素材可显示。
- Ozon 解析器升级为 `ozon-public-2.2.0`。本地页面优先读取 `webShortCharacteristics`、`webDetailedCharacteristics`、`webCharacteristics` 的 `characteristics[]`;当前入口 SKU 的 `/product/<sku>/` 补全响应会再读取完整 `webCharacteristics`,去重后写入 `common.attributes`。这沿用旧插件已验证的数据形状,不通过页面文本或全页 DOM 猜测参数。
- 固定的 Ozon 详情页段仍只读取 `webDescription` / `webRichContent`。其中 `webDescription.richAnnotationJson` 被转换为纯文本写入 `common.description`,原 JSON 同时保留在 `raw_value` 供检查;富文本中的图片只走既有详情素材白名单,不会混入描述文本。
- MAIN world bridge 的返回白名单已经增加上述三个 Characteristics widget,仍然只接受固定 Ozon 同源路径和数字 SKU,不暴露 Cookie、请求头或任意请求能力。
- **兼容性提醒:** 已有采集记录不会自动重新解析历史 `normalized_payload`。例如上面的现有记录仍标记为 `ozon-public-2.1.0`,只能验证新素材预览;需重新在来源商品页执行“编辑上架”,系统会覆盖同一采集快照,届时在规范化载荷查看新版属性与描述。
- 已通过扩展 `wxt prepare + tsc + build`、Web `vite build`、`tsx --test test/*.test.mts`(5 项,包括 Ozon 页面快照中“类型/原产国/材料”等 5 个特征和富文本描述样本)以及浏览器测试页实测。
## 2. 已确认产品决策
@@ -162,10 +187,11 @@ User → Workspace → Membership / Role
下一步建议按以下顺序推进:
1. 用允许保存的单 SKU、单规格轴和颜色×尺码页面样本,在 Ozon、1688、淘宝、天猫逐项验证采集结果;页面状态未暴露组合数据时补 MAIN world bridge 或站点适配器,但绝不以规格轴猜测 SKU。
2. 完成 Ozon 公开变体队列的逐 SKU hydrate;再独立实施 Seller 网页四接口本地观察实验,默认关闭、仅保存净化结果且不影响标准采集。
1. 在真实 Ozon 多 SKU 页面加载已构建扩展,确认 MAIN world bridge 未被页面策略阻断、三 SKU 都写入采集测试页、SKU 级画廊关系正确,并在新版规范化载荷确认完整特征与描述;再以任一兄弟 SKU 页面重采集,确认服务端覆盖同一记录。
2. 用允许保存的单 SKU、单规格轴和颜色×尺码页面样本,在 1688、淘宝、天猫逐项验证采集结果;页面状态未暴露组合数据时补站点适配器,但绝不以规格轴猜测 SKU。之后独立实施 Seller 网页四接口本地观察实验,默认关闭、仅保存净化结果且不影响标准采集。
3. 将当前 `/collection-test/{collectionItemId}` 只读检查面替换为正式商品编辑入口:按页头平台创建发布配置、预选同平台默认门店,并接入 SKU/媒体/类目编辑。
4. 进入商品编辑/在线同步前,关闭评审中的 P1:SKU 与平台发布配置同商品归属、未映射属性持久化、统一乐观锁 Repository 规则;平台能力 Schema 已在 M2.1 按固定键落地。在线商品归档策略和 Client ID 编辑边界仍待确认。
5. **账户认证待开发:忘记密码。** 登录页提供入口;请求接口始终返回同一结果以防邮箱枚举,使用短时有效的一次性不透明邮件令牌(数据库仅存哈希),确认重置后撤销该用户全部 session。实施前先补充认证 API 契约、邮件投递配置和迁移;交付 API、Web、邮件链接与限流/令牌失效回归测试。详情见 [`requirements/account-store-watermark.md`](../requirements/account-store-watermark.md) §2.3。
账户资料的已确认补充:`users.avatar_asset_id` 只保存素材引用,绝不保存公开 URL;`users.revision` 用于 `PATCH /api/auth/me` 的 `If-Match`。资料更新已实现幂等与版本冲突检测;修改密码已实现旧密码校验、按 IP/用户限流、撤销旧会话并轮换新会话。头像上传必须复用素材模块的预签名直传与校验流程,不引入 base64 直传;素材模块尚未实现时头像只能为 `null`。
@@ -42,7 +42,7 @@
1. PostgreSQL 是业务事实来源。所有业务数据必须有 `workspace_id`,或能经强制外键安全追溯到工作空间;查询必须在事务中设置 `SET LOCAL app.workspace_id`。
2. 数据库约束、RLS 与迁移是安全边界;不得只依赖 API 服务层校验。迁移必须可重复执行、可在全新本地环境验证。
3. 关键可重试写入必须实现幂等;触发异步工作的事务必须同时写入 Transactional Outbox。Redis/BullMQ 不得成为业务事实的唯一来源,Worker 必须幂等。
4. access token 不得写入浏览器持久化存储;Cookie、API Key、预签名 URL 等秘密不得写进日志、Outbox 载荷、fixture 或提交进仓库。
4. access token 不得写入浏览器持久化存储;Cookie、API Key、预签名 URL 等秘密不得写进日志、Outbox 载荷、fixture 或提交进仓库。**维护者已于 2026-09-07 批准唯一例外:**为自托管采集插件的网页登录态同步,Atlas Web 可在本站 `localStorage["atlas.extension.web-access"]` 写入当前短期 access token,扩展仅可从 Atlas Web 标签读取并保存到 extension storage;不得读取/写入 refresh Cookie,生产 host permission 必须限制为 Atlas Web 域。完整取舍见 [`extension-login-alternatives.md`](./extension-login-alternatives.md)。
5. 不得从旧 `ozon-seller-kit` 复制 SQLite、多店铺无隔离、无鉴权 API 或旧商品编辑实现。复用逻辑前必须适配多租户与云端边界。
## 5. 工作方式与集成规则
@@ -0,0 +1,31 @@
# 浏览器采集插件登录方案备忘
> 状态:2026-09-07 记录。本文保留备选方案,不代表当前默认实现。
## 方案 A:网页确认 + PKCE 配对(备选,暂不启用)
1. 插件创建 3 分钟有效的连接请求,生成本地 PKCE verifier/challenge。
2. 插件打开固定的 Atlas `/extension-connect` 网页。
3. 网页复用 HttpOnly Web 会话确认连接;未登录时登录后继续。
4. 插件轮询请求状态,凭 verifier 换取独立 access/refresh token。
优点:Web refresh cookie 和 Web access token 永不交给扩展;扩展令牌可独立撤销。缺点:需要配对表、轮询端点、确认页、状态机和失败回收,当前阶段对“登录后采集”的简单需求而言复杂度过高。
## 方案 B:毛子 ERP 风格的网页登录态同步(当前采用)
1. 用户在插件点击登录。
2. 插件打开固定 Atlas ERP 网页;未登录时用户在该网页完成登录。
3. Web 登录或刷新会话后,把当前 access token 写到本站 `localStorage["atlas.extension.web-access"]`。
4. 插件每 2 秒从该 Atlas 网页标签读取该值;读取到后写入自己的 `chrome.storage.local`。
5. 插件用该 Bearer token 调用扩展会话校验接口;校验成功即显示采集操作。
6. 后续扩展 API 请求经后台统一附加 `Authorization: Bearer <token>`。
该流程不依赖 `chrome.identity`、`chromiumapp.org` 回调 URL 或用户配置的扩展 ID;扩展清单的公开 `key` 仅用于稳定扩展 ID。
## 安全取舍与边界
- 这是对项目“Web access token 不落持久化存储”规则的**维护者批准的、窄范围例外**,仅为自托管采集插件互操作。
- Token 仅写入 Atlas Web 自己的 origin,扩展只通过 `scripting.executeScript` 从其被打开的 Atlas 标签读取;不会读取 Cookie、refresh token、来源站点账号或来源站点 localStorage。
- 插件仍只取得短期 Web access token;收到 401 时清除插件缓存,用户需再次从 Atlas 网页同步新 Token。
- 当前阶段不应把该 Token 写入日志、数据库、Outbox、采集载荷或页面 DOM;生产部署必须将扩展 host permission 限定为正式 Atlas Web 域。
- 方案 A 保留为未来需要独立可撤销插件会话时的升级路径。
+18 -12
View File
@@ -4,24 +4,29 @@
## 已交付
1. 扩展使用 PKCE 授权码登录,而不是读取 Atlas Web 的 localStorage 或 refresh Cookie。`GET /api/extension/authorize` 会优先复用用户浏览器已有的 Atlas Web 会话:会话有效则无感签发一次性 code;没有会话才跳转登录页,成功登录后回到原授权请求。
2. 服务端保存 code 与扩展 refresh token 的哈希,code 60 秒且一次性使用,扩展 access token 15 分钟、refresh token 30 天并轮换。令牌含扩展专用 audience,不能被普通 Web/API 身份守卫接受。
3. 扩展上报仅携带来源、规范化 SKU/规格轴、候选素材 URL 和证据;`POST /api/extension/materials` 从扩展令牌解析工作空间,不接受目标平台、门店、Cookie 或 API Key。相同工作空间、来源平台和来源 item key 复用同一采集记录。
4. 上报事务创建来源采集记录、SPU、来源确认 SKU 和素材转存任务;素材异步事件经 Transactional Outbox 进入 Worker。Worker 仅下载 HTTPS 公网地址,限制重定向、私网地址、20MB 和 PNG/JPEG/WebP,按 SHA-256 去重后写入对象存储。单张失败不会使商品草稿丢失。
5. Ozon (`ru/kz/by`) 商品详情页显示 Shadow DOM 小浮窗。未登录只显示“登录 Atlas ERP”;已登录显示“编辑上架 / 采集商品 / 进入 ERP”。编辑上架成功后打开现有 `/products/{productId}` ERP 编辑入口;采集商品只提示进入采集箱。
6. Ozon 解析器保留当前公开 SKU,并只从带 `webProducts`、`webAspects` 或 `aspects` 标识的页面状态发现关联 SKU;不会扫描整页 SKU 或把规格轴做笛卡尔积。来源未给可确认组合时,仍保留当前 SKU。主图作为商品候选素材上报,不伪造 SKU 图片关系。
1. 扩展按毛子 ERP 风格复用 Atlas Web 登录态:点击登录会打开 Atlas Web;Web 登录/恢复后把当前短期 access token 写入本站 localStorage,扩展每 2 秒从该 Atlas 标签读取并保存到 extension storage。未登录时用户在该页完成登录,标签不会被扩展自动关闭。
2. 扩展以同步到的 Web Bearer token 调用会话检查和采集接口;服务端同时验证 JWT 与未撤销的 Web session,并从当前用户成员关系解析工作空间。Token 失效时插件清除缓存并重新要求从 Atlas Web 登录同步。
3. 扩展上报仅携带来源、规范化 SKU/规格轴、候选素材 URL 和证据;`POST /api/extension/materials` 从 Web Bearer 身份解析工作空间,不接受目标平台、门店、Cookie 或 API Key。非 Ozon 来源按 item key 复用;Ozon 额外按已确认 `source_sku` 集合相交复用同一采集记录。
4. 上报事务创建来源采集记录、SPU、来源确认 SKU 和素材转存任务;同一来源商品再次采集时保留记录与商品 ID,并用本次来源快照替换来源字段、SKU 与候选素材。素材异步事件经 Transactional Outbox 进入 Worker,任务带快照版本;Worker 仅处理当前版本,旧任务不得回写。Worker 仅下载 HTTPS 公网地址,限制重定向、私网地址、20MB 和 PNG/JPEG/WebP,按 SHA-256 去重后写入对象存储。单张失败不会使商品草稿丢失。
5. Ozon (`ru/kz/by`) 商品详情页显示 Shadow DOM 小浮窗。未登录只显示“登录 Atlas ERP”;已登录显示“编辑上架 / 采集商品 / 进入 ERP”。商品编辑模块尚未完成时,“编辑上架”成功后打开 `/collection-test/{collectionItemId}` 供审查采集结果;采集商品只提示进入采集箱。
6. Ozon 解析器保留当前公开 SKU 作为入口证据,优先解析 `state-webAspects-*.data-state.aspects[].variants[]` 的真实变体;不会扫描整页 SKU 或把规格轴做笛卡尔积。来源未给可确认组合时,仍保留当前 SKU。
7. Ozon 多 SKU 补全通过受限 MAIN world bridge 调用同源公开页面 JSON:先读取 `modal/aspectsNew` 的 `webAspectsModal`,再以并发 3、上限 100 的队列补全 `/product/<sku>/` 中的标题、价格、画廊和关联 SKU。每个已补全 SKU 的画廊图片使用 `variant_direct` 关联;单个请求失败保留已发现 SKU 与失败证据。
8. Ozon 公开响应没有可依赖 SPU/组 ID。服务端在同一来源平台下按新旧 `source_sku` 集合相交覆盖快照;若命中多条历史记录返回 `SOURCE_VARIANT_GROUP_AMBIGUOUS`,不静默合并。
9. 图片采集沿用旧采集插件已验证的分组 profile:1688、淘宝、天猫只从主图画廊、规格选择器和商品详情容器读取图片,绝不扫描全页 `document.images`。淘宝/天猫详情图排除评论和推荐卡片;URL 会在入库前还原 CDN 原图并按归一化 URL 去重。
10. Ozon 只从 `webGallery` 读取商品画廊,`coverImage` 与 `images[].src` 归为该真实 SKU 的 `source_main + variant_direct`,不会将第二张及以后的画廊图误标为 SKU 图;`webAspects.data.coverImage` 作为 `source_sku + axis_value`。受限 bridge 额外读取固定的富文本页段来获取 `webDescription` / `webRichContent` / `webFeatures` 内的详情图,仍不接触评价或推荐组件。
11. 采集数据测试页会为转存成功且已生成 `asset_id` 的素材按 4 路并发申请短时预览 URL,直接显示缩略图,并展示其关联来源 SKU;预览 URL 不保存到业务数据。Ozon 解析器 `ozon-public-2.2.0` 现额外读取短/完整 `web*Characteristics.characteristics[]` 为公共属性,并将 `webDescription.richAnnotationJson` 转为公共描述文本、原 JSON 放入 `raw_value`。该桥接仍只允许固定 Ozon 页面段和 widget 白名单;已有旧版载荷必须重采集才会拥有这些字段。
## 配置与安全前提
- `EXTENSION_COLLECTOR_REDIRECT_URI` 必须精确等于已安装扩展的 `chrome.identity.getRedirectURL('callback')`;不可用通配符代替。
- 本地默认 API 为 `http://localhost:8200`。生产构建应为 Atlas API 域配置扩展 host permission 与 `ATLAS_API_ORIGIN`,并登记对应扩展 ID 的回调 URI。
- 扩展 refresh token 仅存 Chrome extension storage;Web refresh Cookie 从不返回给扩展。Worker 不接触浏览器 Cookie。
- 扩展清单带有仅公开的 RSA `key`,Chrome 因此为自托管和解压加载生成稳定 ID;该 ID 不参与登录授权,严禁提交私钥。
- 本地默认 API 为 `http://localhost:8200`。生产构建应为 Atlas API 域配置扩展 host permission 与 `ATLAS_API_ORIGIN`;不需要按用户登记回调 URI。
- Web access token 镜像到 Atlas Web 自己的 localStorage 是维护者批准的自托管插件例外;扩展不读取 refresh Cookie、来源站 Cookie 或来源站 localStorage。Worker 不接触浏览器 Cookie。安全取舍见 [`extension-login-alternatives.md`](./extension-login-alternatives.md)。
## 尚未实现
- Ozon 变体队列逐页 hydrate、SKU 级公开详情补全;当前只消费已加载公开页面状态。
- 1688、淘宝、天猫解析器,以及 Ozon Seller 网页四接口本地观察实验。
- 采集箱详情页的素材关联可视化、重试 UI 和商品编辑/发布闭环。
- Ozon 多 SKU 的真实来源页人工回归(`packages/reference/ozon1.html` 已作为三 SKU 快照);Ozon Seller 网页四接口观察实验仍独立于此标准公开采集链路。
- Ozon Seller 网页四接口本地观察实验;1688、淘宝、天猫基础页面状态解析已在 M2.4 完成,但仍需要真实来源页面回归。
- 正式采集箱详情页的素材关联可视化、重试 UI 和商品编辑/发布闭环(当前仅 `/collection-test/{id}` 提供只读素材预览与关联 SKU)。
## 验收命令
@@ -30,5 +35,6 @@
./apps/api/node_modules/.bin/tsc -p apps/api/tsconfig.json --noEmit
./apps/worker/node_modules/.bin/tsc -p apps/worker/tsconfig.json --noEmit
(cd apps/extension && ./node_modules/.bin/wxt prepare && ./node_modules/.bin/tsc -p tsconfig.json --noEmit && ./node_modules/.bin/wxt build)
(cd apps/extension && ./node_modules/.bin/tsx --test test/*.test.mts)
(cd apps/web && ./node_modules/.bin/vite build)
```
+11 -2
View File
@@ -25,9 +25,18 @@
### 2.2 登录与会话
用户用邮箱和密码登录。登录成功进入默认工作空间与首页;若后续一个账户拥有多个工作空间,再增加切换入口。首期需要提供退出登录;忘记密码、短信登录、第三方登录、设备管理和多因素认证均不在本轮范围。
用户用邮箱和密码登录。登录成功进入默认工作空间与首页;若后续一个账户拥有多个工作空间,再增加切换入口。首期需要提供退出登录;短信登录、第三方登录、设备管理和多因素认证均不在本轮范围。
### 2.3 全局平台与当前店铺上下文
### 2.3 忘记密码(待开发任务)
该功能已列入下一轮账户认证开发,尚未实现。登录页增加“忘记密码”入口,用户可提交邮箱并通过邮件中的一次性链接设置新密码。
- 请求重置时,无论邮箱是否已注册,页面均显示相同的成功提示,避免枚举账户;服务端须按 IP 与邮箱摘要限流,且不得记录明文密码、重置令牌或完整重置链接。
- 重置链接使用随机、不透明、一次性令牌;数据库只保存其哈希与过期时间。令牌须短时有效,使用、过期或再次申请后不得重复使用。
- 用户在重置页输入新密码与确认密码。成功后更新密码哈希、撤销该账户所有已有 session,并引导用户用新密码登录;旧 access token 与插件缓存随之失效。
- 开发前须补充 `docs/api/auth.md`、数据库迁移、邮件投递配置与失败/限流错误契约;交付须包含 API、Web 页面与邮件链接的集成测试,且测试不能输出令牌或密码。
### 2.4 全局平台与当前店铺上下文
平台和当前店铺是全站工作上下文,不在每个业务页面重复选择。页头在用户信息左侧放置“平台选择器”;用户切换平台后,菜单、类目、物流商、佣金规则和页面数据均切换到该平台范围。
@@ -1,6 +1,6 @@
# 浏览器采集插件:需求与技术方案
> 状态:已确认核心方向,待实施。本文是 Atlas Seller Kit 从来源商品详情页进入采集箱、商品编辑与后续 Ozon 上线的采集入口规范。
> 状态:核心闭环已实施;Ozon 公开多 SKU 解析与归并方案于 2026-09-07 落地,仍需用真实页面回归验证。本文是 Atlas Seller Kit 从来源商品详情页进入采集箱、商品编辑与后续 Ozon 上线的采集入口规范。
>
> 本文取代旧 `ozon-seller-kit` 采集插件的产品定位,但只复用经验证的页面采集思路,不复用旧项目的本地数据库、无鉴权接口、`/trial/:id` 页面、AI 套图或生图流程。
@@ -43,7 +43,7 @@
首期不在悬浮窗中选择平台或店铺。服务端从插件授权身份只解析工作空间,创建未绑定目标平台/门店的采集记录和商品基础草稿。用户进入 Atlas ERP 编辑页时,页面使用页头当前选中的平台;首期页头默认选择 Ozon,服务端取得该平台发布配置并预选默认店铺。若当前平台没有可用门店,允许保留采集箱记录,但不能进入该平台的商品编辑/发布流程,应引导用户在店铺管理中添加门店。
推荐采用扩展专用的短期授权码流程:插件打开 Atlas 登录/授权页,登录页完成后向 `chrome-extension://{extension-id}/...` 回传一次性 code,插件交换短期 access token 与可轮换 refresh token。令牌只放 Chrome 扩展专属存储,后台请求统一携带 `Authorization: Bearer`。不得像参考插件一样直接读取 ERP 网页 localStorage 中的长期 Token。
首期按毛子 ERP 风格复用 Atlas Web 登录态:插件打开 Atlas Web 登录页,登录/会话恢复后从 Atlas Web 标签读取本站 localStorage 中的短期 access token,保存到 Chrome extension storage,后台请求统一携带 `Authorization: Bearer`。不得读取 refresh Cookie、来源站 Cookie 或来源站 localStorage;该受限例外及后续独立令牌升级方案见 [`../project/extension-login-alternatives.md`](../project/extension-login-alternatives.md)。
### 2.3 三个操作
@@ -84,7 +84,7 @@
| `failed` | 采集请求或关键记录创建失败;保留可诊断错误。 |
| `archived` | 用户不再处理的采集记录。 |
同一工作空间内,以 `source_platform + source_item_key` 进行幂等。重复“采集商品”应返回已有采集记录;重复“编辑上架”打开同一未绑定目标的商品草稿或其已绑定的编辑入口。只有用户在 ERP 中明确选择“重新采集并合并/创建副本”时,才允许覆盖或复制。
同一工作空间内,以 `source_platform + source_item_key` 识别同一来源商品。重复“采集商品”或“编辑上架”保留已有采集记录和商品草稿 ID,但以本次页面重新采集的来源信息覆盖上一份来源快照:标题、价格、原始证据、规范化载荷、来源 SKU 与候选素材均以最新采集为准。旧素材任务取消,新素材任务必须带新的快照版本,旧 Worker 不得回写最新内容。用户希望保留旧版本或创建副本的需求,留待正式商品编辑模块提供明确操作后再设计。
### 3.1 采集箱与目标店铺的绑定时点
@@ -161,7 +161,7 @@ interface CollectedVariant {
1. 创建多个 SKU 的前提是来源提供稳定组合记录、SKU ID、变体 SKU、`propPath` 等可验证证据;单独的“红色”“M 码”选项不够。
2. `source_variant_key` 优先使用来源 SKU ID。不存在来源 ID 时,使用来源提供的属性 ID/值 ID 的有序组合,并记录构造规则与风险;不得只拼本地化展示名称。
3. 同一 `source_variant_key` 重采集应更新其采集记录和候选素材,不应再创建一个 SKU。
3. 同一 `source_variant_key` 重采集应更新其采集记录和候选素材,不应再创建一个 SKU。Ozon 没有可依赖的公开 SPU/变体组 ID:同一来源站点下,本次真实 `source_sku` 集合与既有采集记录任一 SKU 相交时,应复用该记录并覆盖来源快照;若相交到多条历史记录,必须报重复冲突,不得静默合并或删除。
4. 只识别出规格轴但没有组合时,创建一个默认 SKU,状态为 `needs_review`;规格轴仍保留给编辑页辅助人工拆分。
5. 不使用笛卡尔积构造 SKU。来源若只返回“颜色选项”和“尺码选项”而没有组合可售表,无法证明每种组合都存在。
6. 来源价格、库存、重量、尺寸只作采集参考。发布前必须由用户确认有效包装规格、定价快照、目标店铺类目和目标语言内容。
@@ -176,11 +176,20 @@ interface CollectedVariant {
#### 采集步骤
1. 从当前商品页的白名单状态组件读取标题、价格、画廊、视频、参数、类目和当前来源 SKU。
2. 从包含产品变体的页面状态(参考实现中的 `webProductsModal` / `aspects[].variants` 一类组件)提取每一条 `sku`、标题、价格、划线价、封面、规格文本和关联链接。旧采集插件只保留了 `webAspects` 的名称与图片,丢弃了 `sku`,新实现不得如此。
3. 将当前 SKU 和发现到的关联 SKU 放入有界队列;按 `sku` 去重。对每个 SKU 加载该 SKU 的公开详情页面/页面 JSON,补齐图片、视频、描述与可能继续出现的关联 SKU。
1. 优先从当前商品页的白名单状态组件读取标题、价格、画廊、参数、类目和当前来源 SKU。`[id^="state-webAspects-"][data-state]` 的 `aspects[].variants[]` 是首选的已渲染真实变体表;`webShortCharacteristics` / `webDetailedCharacteristics` / `webCharacteristics` 的 `characteristics[]` 转为公共商品属性。不得扫描脚本文本或整页 SKU 字符串。画廊只读取 `webGallery.coverImage` 和 `webGallery.images[].src`,先将 Ozon `/wcN/` / `/cN/` 缩略 URL 还原为原图,再去重。
2. 若当前页只渲染了部分变体,使用只限 Ozon 域名、只接受数字 SKU 的 MAIN world bridge 请求 `/api/entrypoint-api.bx/page/json/v2?url=/modal/aspectsNew?product_id=<sku>`,从 `webAspectsModal-*.aspects[].variants[]` 提取每一条 `sku`、标题、价格、封面和规格文本。桥接只返回这些白名单 widget 状态,绝不暴露 Cookie、请求头或任意 URL 请求能力。详情图只允许从固定的第二页段 `webDescription`、`webRichContent`、`webFeatures` 中取图片字段;`webDescription.richAnnotationJson` 仅转换为描述纯文本,不采集图片 URL 以外的页面组件内容。
3. 将当前 SKU 和发现到的关联 SKU 放入有界队列;按真实 `sku` 去重。对每个 SKU 通过同一 bridge 请求 `/api/entrypoint-api.bx/page/json/v2?url=/product/<sku>/`,从 `webProductHeading`、`webPrice`、`webGallery`、`webAspects` 与当前 SKU 的完整 `webCharacteristics` 补齐详情,并将新发现的真实 SKU 继续入队。
4. 队列达到固定上限(建议 100)或无新 SKU 时停止。记录 `discovered_count`、`hydrated_count`、失败项与页面证据。
5. 每个成功补全的来源 SKU 创建一个 `CollectedVariant`;没有专属图片时设置 `inherited_product_default`,不丢弃变体。
5. 每个发现的来源 SKU 创建一个 `CollectedVariant`;补全成功时记录为 `verified` 并把 `webGallery` 的每一张图片以 `source_main + variant_direct` 绑定到该 SKU。规格 `data.coverImage` 以 `source_sku + axis_value` 记录,不能因同一画廊的第二张图片而虚构 SKU 图片关系。没有专属图片时设置 `inherited_product_default`,不丢弃变体。不得用 `document.images`,以免把 Ozon Logo、支付图和推荐商品误采为商品素材。
#### 组身份与重复采集
Ozon 当前公开页面没有可依赖的 `group_id`、`model_id` 或 SPU ID。`/modal/aspectsNew` 的 `product_id` 参数以及链接的 `from_sku` 都是当前页面 SKU,页面 widget ID 也只是易变的前端实例 ID。因此:
1. `source.itemKey` 保存用户实际打开的 SKU,作为采集入口证据;它不是 SPU 主键。
2. 服务端以同一工作空间、同一来源平台的 `product_variants.source_sku` 与本次完整 SKU 集合求交集。唯一命中时保留既有 `collection_item` / `product` ID,用最新页面资料、SKU 与候选素材覆盖旧快照。
3. 排序 SKU 集合的哈希仅可作为诊断辅助,不能作为永久身份键;Ozon 随时可能增加、隐藏或拆分变体。
4. 多个既有采集记录同时命中时返回 `SOURCE_VARIANT_GROUP_AMBIGUOUS`,等待显式的人工合并能力,避免错误地合并不同商品。
#### 完整性边界