# Rule: Tương tác với Shopee / TikTok (lấy dữ liệu & lắng nghe dữ liệu)

Tài liệu **quy ước bắt buộc** khi viết code lấy dữ liệu (fetch) hoặc lắng nghe dữ liệu
(listen) từ Shopee và TikTok trong extension. Mọi feature mới đụng tới network/API của 2
sàn này PHẢI tuân theo các rule dưới đây — không tự fetch tùy tiện.

> Liên quan: `docs/shopee/fetch-product-flow.md` (flow chi tiết `get_pc`),
> `docs/dev/extension/content-scripts.md`, `docs/dev/extension/bridge.md`.

---

## 0. Mô hình 3 tầng (3 worlds) — phải nắm trước khi code

Mọi tương tác đều chạy trên 1 trong 3 context. Chọn sai context = fetch fail (CORS / thiếu
cookie / bị anti-bot chặn).

| Tầng | Context | Truy cập được | KHÔNG có |
|------|---------|---------------|----------|
| **Page Script** | `world: 'MAIN'` — cùng context JS với trang Shopee/TikTok | `fetch` có cookie + session, không bị CORS, set được header anti-bot | DOM API của extension, `chrome.*`, webext-bridge |
| **Content Script** | isolated world | DOM của trang, `chrome.runtime`, webext-bridge | biến JS của trang, `fetch` **không** kèm cookie sàn |
| **Background** | service worker (MV3) | `webRequest`, `cookies`, in-memory store, mọi tab | `window`, DOM |

**Kênh giao tiếp giữa các tầng (BẮT BUỘC đi đúng kênh):**

```
 Popup/Options ──webext-bridge(EVENT_*)──► Background ──chrome.tabs(tabId)──► Content Script
                                                                                   │
 Content Script ──webext-bridge(EVENT_*)──► Background                              │ window.postMessage
                                                                                   ▼
                                                                              Page Script (MAIN)
```

- **Content/Popup ↔ Background**: `webext-bridge` — `sendMessage(EVENT_*, data)` / `onMessage`.
  Event name khai báo TẬP TRUNG ở `src/app/bridge/config/event.js`, wrapper ở
  `src/app/bridge/content/background.js`. **Không** hardcode chuỗi event rải rác.
- **Content ↔ Page Script**: `window.postMessage` qua class `PostMessageApi`
  (`src/app/bridge/content/postMessage.js`). **Không** gọi `window.postMessage` thô — luôn
  bọc qua `PostMessageApi` để có UUID match request/response + timeout.
- **Background → Content theo tab**: `src/app/bridge/content/tab.js` (chrome runtime + tabId).

---

## 1. RULE lấy dữ liệu (FETCH)

### R1.1 — Shopee Client API (PDP, search, shop, category, market, affiliate): fetch trong MAIN world, KHÔNG fetch từ content

Áp dụng cho TẤT CẢ endpoint client `shopee.vn/api/*` — không riêng product:
`api/shopee/client/{product,search,shop,category,market}`, `api/shopee/affiliate.js`.
Endpoint client yêu cầu cookie + header anti-bot và bị CORS chặn từ isolated world.
**Luôn** đẩy request xuống Page Script qua `PostMessageFetchUrl`:

```
api/shopee/.../get.js  →  readyFetchUrlShopeeClient(url)   [bridge/shopee/api.js]
                       →  EVENT_TAB_FETCH_URL (webext-bridge, theo tabId)
                       →  PostMessageFetchUrl.sendMessage   [content/service/event.js]
                       →  fetchUrlService(payload)          [content/shopee/script/fetch.js, MAIN]
```

- Page Script (`src/content/shopee/script/fetch.js`) là nơi DUY NHẤT được gọi `fetch()` tới
  API Shopee client. Header chuẩn: `accept: application/json`, `af-ac-enc-dat: null`,
  `content-type: application/json`.
- Đăng ký page script qua entrypoint `world: 'MAIN'`
  (`src/entrypoints/shopee-fetch.content.js`). Page script **không** import được module
  extension nặng — giữ tối giản, chỉ fetch + postMessage.

### R1.2 — Ưu tiên header thật bắt từ network (chống anti-bot)

Trước khi tự fetch, thử dùng header **thật** mà trình duyệt đã gửi khi user duyệt trang:

- Background bắt header qua `webRequest.onBeforeSendHeaders`
  (`src/background/bridge/listener/index.js`), lưu vào in-memory store
  (`RequestProductPayload`, `RequestSearchPayload`, `RequestShopeeClient`).
- `getShopeeProductId()` thử theo thứ tự: **cache → `request` (header thật) → `content` →
  `popup`**. Dừng ở bước đầu tiên thành công. Không đảo thứ tự này.
- Sau mỗi response phải chạy `checkCrawlerDetected()` — nếu bị nhận diện bot thì bỏ header
  đó, thử nguồn tiếp theo.

### R1.3 — Shopee Seller API: axios + cookie `SPC_CDS`

Khác client, seller API (`banhang.shopee.vn`) dùng axios instance có sẵn cookie phiên
đăng nhập — **GET thường gọi thẳng**, không cần page-script:

- Dùng `apiShopeeSeller` (`src/app/api/shopee/seller/init.js`) — baseURL
  `SHOPEE_DOMAIN_SELLER`, header từ `getBaseHeader()`, payload kèm `SPC_CDS` đọc từ cookie
  (`getKeyCookie('SPC_CDS', ...)`). **Không** tự tạo axios instance mới cho seller.
  Toàn bộ `api/shopee/seller/{product,order,ads,marketing,flashsale,rating,...}` đi qua
  instance này.
- **POST cần context trang seller** (request có chữ ký/anti-bot) thì đẩy qua page-script:
  `readyPostUrlShopeeSeller(url, payload)` → `makeTabShopeeSellerProduct()` tìm/tạo tab
  `banhang.shopee.vn` → fetch trong tab đó (`src/app/bridge/shopee/api.js`). Cùng cơ chế
  với client nhưng khác domain/tab.
- Order list dùng pagination kiểu `sentinel` + batch (`p-map`, `PromiseMap`) — xem
  `api/shopee/seller/order/list.js`, không gọi tuần tự từng đơn.

### R1.4 — TikTok API: axios + `axios-fetch-adapter`

- Client: `apiTiktoklient` (`api/tiktok/client/init.js`); Seller: `apiTiktokSeller`
  (`api/tiktok/seller/init.js`). Cả hai dùng `@vespaiach/axios-fetch-adapter` +
  interceptor trả thẳng `response.data`. Domain lấy từ `@/config/api`, không hardcode.
- Lấy **sản phẩm TikTok**: `getTiktokProductId()` (`api/tiktok/client/product.js`) gọi
  `/pdp/:id?__loader=...` qua `apiTiktoklient` + cache `CacheTiktokProductId`. Seller info:
  `getTiktokSellerInfo()` (`api/tiktok/seller/get.js`). Đây là cơ chế lấy data text/JSON
  của TikTok — tách biệt với tải media blob (R1.5).

### R1.5 — TikTok tải media (video blob): fetch MAIN world với `credentials: 'include'`

CDN TikTok khóa theo origin → phải fetch từ Page Script
(`public/js/content/tiktok/script/fetch.js`), không tải từ content/background:

- Content gửi `{channelRequest: ATSP_TT_FETCH_BLOB_REQUEST, messageId, payload:{url}}` qua
  `window.postMessage`; Page Script fetch `credentials:'include'`, `referrer:
  'https://www.tiktok.com/'`, trả `blob` về qua `ATSP_TT_FETCH_BLOB_REPLY`.
- Blob lớn truyền qua bridge dưới dạng base64 (`bridgeVideoBlobStore` → `blobId`), không
  nhồi blob thô vào message.

### R1.6 — Không inject script bằng string interpolation (đang nợ kỹ thuật)

`excuteFetchUrl()` (`api/shopee/fetch.js`) nối URL trực tiếp vào chuỗi JS → rủi ro
break/XSS nếu URL có ký tự đặc biệt, lại dùng `delay()` cố định. **Code mới KHÔNG dùng lại
pattern này** — luôn đi qua `PostMessageFetchUrl`.

---

## 2. RULE lắng nghe dữ liệu (LISTEN)

Có 4 cơ chế lắng nghe, mỗi cơ chế cho 1 mục đích — chọn đúng, không lạm dụng.

### R2.1 — Bắt response qua monkey-patch `window.fetch` (MAIN world)

Để thu dữ liệu Shopee tự trả khi user thao tác (vd stock theo variant):

- Page Script ghi đè `window.fetch`, clone response các URL chứa `/api/`, parse JSON rồi
  forward về content qua `PostMessageFetchListener`
  (`src/content/shopee/script/fetch.js: listenerFetchData`).
- Content forward tiếp về background qua `bridgeShopeeListenFetch` (`EVENT_SHOPEE_LISTEN_FETCH`).
- Background (`onBridgeShopeeListenFetch`, `background/bridge/global.js`) lọc URL
  (`filterFetchUrlPush`) → gom vào `FetchListenerBucketShopee`; nếu URL chứa
  `select_variation_pc`/`get_pc_vsku` thì `handleShopeeSelectVariation()` trích stock và lưu
  3 tầng (Map → Cache → KV).

**Rule:** patch `fetch` phải luôn gọi `originalFetch(...args)` và trả response gốc TRƯỚC, chỉ
quan sát trên bản `clone()`. Không bao giờ làm chậm/chặn request thật của trang.

### R2.2 — Bắt request header qua `webRequest.onBeforeSendHeaders` (Background)

Dùng để lấy header thật (anti-bot) chứ không sửa request:

- Đăng ký tập trung ở `src/background/bridge/listener/index.js`, URL pattern lấy từ
  `constant/{shopee,tiktok,lazada}.js` (vd `SHOPEE_URLS.PRODUCT_GET_PC`). Listener không
  dùng `'blocking'` (chỉ quan sát), trừ trường hợp đặc biệt đã được duyệt.
- Mỗi domain/URL cần lắng nghe phải khai báo trong `host_permissions` của `wxt.config.ts`.

### R2.3 — Theo dõi media đang phát: `PerformanceObserver` (TikTok)

`TiktokFeedLoader.startMediaObserver()` quan sát `resource` entries, match
`MEDIA_URL_REGEX` để lấy URL video mp4 đang phát. Dùng cơ chế này thay vì cố đọc `src` của
`<video>` (TikTok dùng blob URL).

### R2.4 — Theo dõi DOM: `MutationObserver` + bám selector ổn định

Inject nút/widget vào trang động (feed TikTok, bảng sản phẩm Shopee) dùng
`MutationObserver` (`startButtonObserver`). Class của Shopee/TikTok bị obfuscate →
**bám `data-e2e`, `class*="suffix"`, hoặc `href` pattern**, không bám class hash đầy đủ
(xem `SELECTORS` trong `content/tiktok/constant.js`). Luôn check tồn tại trước khi chèn để
tránh nhân đôi.

### R2.5 — Realtime qua WebSocket native (Shopee seller chat push)

Tin nhắn chat seller được nhận realtime qua WebSocket của Shopee, KHÔNG poll:

- `socketConnectChat()` (`src/app/api/shopee/seller/chat/socket.js`) mở
  `wss://seller-push-ws.shopee.vn/socket.io/...` (giao thức Socket.IO/Engine.IO **thô** —
  tự ghép frame `40/42/420`, không qua thư viện `socket.io-client`).
- **Rule khi đụng socket này:** giữ nguyên trình tự handshake — chờ frame `40` → gửi auth
  (`420[...login...]` với `token` + `account_id` từ `readySendChat()`) → ping định kỳ
  (`2` + `42[heartbeat]`) mỗi 5s. Bỏ ping = bị server ngắt. Parse message bằng
  `parseChatMessage` (cắt prefix số rồi `JSON.parse`), chỉ xử lý `event === 'message'` có
  `message_content`.
- Phân biệt với **Socket.IO của AutoShopee backend** (`src/background/socket/*`) — đó là
  kênh riêng tới server nội bộ, không phải sàn. Đừng nhầm 2 socket.

### R2.6 — Polling định kỳ (metric / task nền)

Dữ liệu tổng hợp không có realtime (vd thống kê đơn) thu bằng cách gọi API seller theo chu kỳ:

- `src/background/fetch/metric/{shopee,tiktok,lazada}.js` — vd `fetchMetricOrderShopee()`
  gọi `getOrderStats()` rồi chuẩn hóa về `{unpaid, unprocessed, ...}`.
- Task nền lập lịch ở `src/background/shopee/task.js` + `chrome.alarms` (permission `alarms`).
- **Rule:** không tự `setInterval` rải rác trong content/popup để poll API sàn — đăng ký
  task ở background để chỉ chạy 1 nơi, tránh nhân bản request và rate-limit.

---

## 3. RULE cache & lưu trữ

- Stock/sản phẩm Shopee đọc-ghi theo 3 tầng, đúng thứ tự: **`*ProductStock` Map (RAM) →
  `CacheShopee*` (in-memory cache) → `KVShopee*` (IndexedDB, bền qua reload)**.
- Cache chỉ in-memory (`CacheShopeeProduct`, `CacheShopeeStock`) sẽ mất khi reload extension —
  dữ liệu cần bền phải ghi KV.
- In-memory store header (`RequestProductPayload`) hiện không tự clear → khi thêm store
  kiểu này phải giới hạn size / clear sau khi dùng, tránh memory leak.

---

## 4. Checklist trước khi merge code đụng Shopee/TikTok data

- [ ] Fetch API sàn chạy trong **MAIN world** (Shopee client / TikTok media) hoặc qua axios
      instance chuẩn (seller) — không `fetch()` thẳng từ content/background.
- [ ] Đi đúng kênh: webext-bridge giữa content/popup ↔ background; `PostMessageApi` giữa
      content ↔ page; không `window.postMessage` thô.
- [ ] Event name khai báo trong `bridge/config/event.js`, URL pattern trong `constant/*`,
      domain trong `config/api` + `host_permissions` (`wxt.config.ts`).
- [ ] Với fetch sản phẩm: thử **header thật trước** (`request` method) và chạy
      `checkCrawlerDetected()`.
- [ ] Monkey-patch `fetch` chỉ quan sát trên `clone()`, trả response gốc trước.
- [ ] Realtime sàn dùng WebSocket native (giữ đúng handshake + heartbeat), không poll thay thế.
- [ ] Polling/metric đăng ký ở background (1 nơi), không `setInterval` rải rác ở content/popup.
- [ ] Selector DOM bám `data-e2e`/suffix/href ổn định, có `MutationObserver` + chống chèn trùng.
- [ ] Dữ liệu cần bền ghi xuống KV; không lạm dụng in-memory store không có cơ chế clear.
- [ ] KHÔNG dùng lại `excuteFetchUrl` (string-interpolation inject).

---

## 5. File tham chiếu nhanh

| Vai trò | File |
|---------|------|
| Page script Shopee (MAIN): fetch + listen | `src/content/shopee/script/fetch.js` |
| Page script TikTok (MAIN): fetch blob | `public/js/content/tiktok/script/fetch.js` |
| Bọc postMessage content ↔ page | `src/app/bridge/content/postMessage.js` |
| Wrapper webext-bridge (content → bg) | `src/app/bridge/content/background.js` |
| Khai báo event name | `src/app/bridge/config/event.js` |
| Tìm/tạo tab Shopee client + seller, gửi fetch | `src/app/bridge/shopee/{api,tab}.js` |
| Đăng ký webRequest listener | `src/background/bridge/listener/index.js` |
| Xử lý fetch-listen + stock ở bg | `src/background/bridge/global.js` |
| Lấy sản phẩm Shopee client (3 method) | `src/app/api/shopee/client/product/get.js` |
| Lấy sản phẩm Shopee seller (axios) | `src/app/api/shopee/seller/product/get.js` |
| Axios seller Shopee (cookie SPC_CDS) | `src/app/api/shopee/seller/init.js` |
| WebSocket realtime chat seller | `src/app/api/shopee/seller/chat/socket.js` |
| Polling metric / task nền | `src/background/fetch/metric/*`, `src/background/shopee/task.js` |
| Socket.IO backend AutoShopee (không phải sàn) | `src/background/socket/*` |
| Lấy sản phẩm TikTok (axios) | `src/app/api/tiktok/client/product.js` |
| Axios TikTok client/seller | `src/app/api/tiktok/{client,seller}/init.js` |
| Loader feed TikTok (observer + inject) | `src/content/tiktok/loader.js` |
| Constant URL/selector | `src/app/constant/shopee.js`, `src/content/tiktok/constant.js` |
| Endpoint reference (đầy đủ URL) | `docs/dev/api/shopee.md`, `docs/dev/api/third-party.md` |
