feat: 试用过程中的细节优化

This commit is contained in:
2026-09-13 22:19:16 +08:00
parent dbb2bb80bd
commit 897e5585c0
40 changed files with 1156 additions and 257 deletions
@@ -47,11 +47,11 @@ export default defineBackground(() => {
// 通用文本代理:采集引擎拉跨域资源(1688 详情 CDN / mtop API)
if (msg?.action === 'fetchText') {
const headers: Record<string, string> = {};
if (msg.referer) headers['Referer'] = msg.referer;
fetch(msg.url, {
headers,
credentials: msg.credentials ? 'include' : 'omit',
// Referer 是受保护请求头,不能通过 headers 设置;使用 Fetch 的标准选项。
referrer: typeof msg.referer === 'string' ? msg.referer : undefined,
referrerPolicy: 'strict-origin-when-cross-origin',
})
.then(async (res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
@@ -300,6 +300,11 @@ const App: React.FC = () => {
}
setReporting(true);
try {
// 上报遵循采集图片区的勾选状态:勾选了就只传勾选项;未勾选则全量传。
// 避免详情图过多时超过服务端的单次素材上限,同时保留“全不选 = 全量”的便捷行为。
const imagesToReport = selectedKeys.size > 0
? allImages.filter(img => selectedKeys.has(img.key))
: allImages;
const data = await send<{ product_id: string }>('reportProduct', {
reportBaseUrl: settings.reportBaseUrl,
payload: {
@@ -310,8 +315,7 @@ const App: React.FC = () => {
collectedAt: result.scannedAt,
},
texts: editedTexts(),
// 全量上报采集图(main/sku/detail/video),试算页需要完整素材
images: result.images.map(img => ({
images: imagesToReport.map(img => ({
groupKey: img.groupKey,
groupName: img.groupName,
variantName: img.variantName ?? null,
@@ -30,6 +30,7 @@ export interface State1688 {
videos: Array<{ url: string; cover?: string }>;
skus: Sku1688[];
params: Array<{ key: string; value: string }>;
detailUrl?: string;
}
/** 深度查找指定键(BFS + 访问标记 + 节点数上限,防大对象拖死) */
@@ -150,6 +151,13 @@ export function extract1688State(context: unknown): State1688 | null {
temp.topCategoryId ? String(temp.topCategoryId) : '',
].filter(Boolean);
// 图文详情位于独立 CDN,SSR 直接给出地址;无需点击标签或滚动页面。
const description = deepFind(context, 'description');
const candidateDetailUrl = description?.fields?.detailUrl ?? description?.detailUrl;
const detailUrl = typeof candidateDetailUrl === 'string' && /^https:\/\//.test(candidateDetailUrl)
? candidateDetailUrl
: undefined;
if (!title && mainImages.length === 0 && skus.length === 0) return null;
return {
@@ -164,5 +172,6 @@ export function extract1688State(context: unknown): State1688 | null {
videos,
skus,
params,
detailUrl,
};
}
@@ -0,0 +1,97 @@
/**
* 图文详情资源提取。接口可返回 HTML,也可返回 `var desc = '...'` 形式的脚本;
* 本模块只解析字符串和 HTML,绝不执行远端脚本。
*/
import { dedupeKey, pickBestFromSrcset, toOriginalUrl } from './url';
import type { ImageMaterial } from './image';
import type { SiteProfile } from '../profiles/types';
const IMAGE_ATTRS = ['src', 'data-src', 'data-lazyload-src', 'data-original', 'data-lazy', 'data-ks-lazyload'];
function normalizeEscapedText(raw: string): string {
return raw
.replace(/\\u([0-9a-f]{4})/gi, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
.replace(/\\x([0-9a-f]{2})/gi, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
.replace(/\\\//g, '/');
}
function normalizeUrl(raw: string, baseUrl: string): string | null {
const value = raw.trim().replace(/^['"]|['"]$/g, '');
if (!value || /^data:|^javascript:|^blob:/i.test(value)) return null;
try {
const url = new URL(value, baseUrl);
return /^https?:$/i.test(url.protocol) ? url.href : null;
} catch {
return null;
}
}
function addUrl(out: string[], seen: Set<string>, raw: string | null, baseUrl: string): void {
if (!raw) return;
const url = normalizeUrl(raw, baseUrl);
if (!url || seen.has(url)) return;
seen.add(url);
out.push(url);
}
/** 从详情 HTML 或字符串化的详情脚本中提取图片地址。 */
export function extractDetailImageUrls(raw: string, baseUrl: string): string[] {
const urls: string[] = [];
const seen = new Set<string>();
const decoded = normalizeEscapedText(raw);
const doc = new DOMParser().parseFromString(decoded, 'text/html');
for (const el of Array.from(doc.querySelectorAll('img, source'))) {
for (const attr of IMAGE_ATTRS) addUrl(urls, seen, el.getAttribute(attr), baseUrl);
const srcset = el.getAttribute('srcset');
if (srcset) addUrl(urls, seen, pickBestFromSrcset(srcset), baseUrl);
}
for (const el of Array.from(doc.querySelectorAll<HTMLElement>('[style]'))) {
addUrl(urls, seen, el.style.backgroundImage.match(/url\(([^)]+)\)/i)?.[1] ?? null, baseUrl);
}
// 某些 1688 响应会把 HTML 包在 JS 字符串里;补扫图片 CDN 的 URL 字面量。
const candidates = decoded.match(/(?:https?:)?\/\/[^\s'"<>\\]+/g) ?? [];
for (const candidate of candidates) {
const url = normalizeUrl(candidate.replace(/[),;]+$/, ''), baseUrl);
if (!url) continue;
const hostIsImageCdn = /(?:alicdn|taobaocdn|tmallcdn)\.com$/i.test(new URL(url).hostname);
const hasImageExt = /\.(?:avif|bmp|gif|jpe?g|png|webp)(?:[?#]|$)/i.test(url);
if (hostIsImageCdn || hasImageExt) addUrl(urls, seen, url, baseUrl);
}
return urls;
}
/** 由 background service worker 跨域读取详情接口。 */
export function fetchDetailText(url: string, referer: string): Promise<string> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
{ action: 'fetchText', url, referer, credentials: true },
(response: { ok?: boolean; text?: string; error?: string } | undefined) => {
const runtimeError = chrome.runtime.lastError;
if (runtimeError) return reject(new Error(runtimeError.message));
if (!response?.ok || typeof response.text !== 'string') {
return reject(new Error(response?.error || '详情接口未返回内容'));
}
resolve(response.text);
},
);
});
}
export function detailUrlsToMaterials(urls: string[], profile: SiteProfile): ImageMaterial[] {
const seen = new Set<string>();
const result: ImageMaterial[] = [];
for (const rawUrl of urls) {
const url = toOriginalUrl(rawUrl, profile.originalUrlRules);
const key = dedupeKey(url, profile.originalUrlRules);
if (seen.has(key)) continue;
seen.add(key);
result.push({
key: `detail-${String(result.length + 1).padStart(3, '0')}`,
groupKey: 'detail', groupName: '详情图', url, thumbUrl: rawUrl,
index: result.length, type: 'img',
});
}
return result;
}
+19
View File
@@ -53,6 +53,25 @@ export function queryAllDeep(selectors: string[]): Element[] {
return out;
}
/**
* 淘宝等页面会把图文详情放在延迟挂载的标签页中;仅滚动页面不会触发该标签的内容加载。
* 只匹配明确的详情标签,避免误点商品页中的普通链接或推荐内容。
*/
export async function activateDetailTab(): Promise<void> {
const labels = new Set(['图文详情', '商品详情', '详情']);
const candidates = Array.from(document.querySelectorAll<HTMLElement>('[role="tab"], button, [class*="tab"]'));
const tab = candidates.find((el) => {
const label = (el.textContent || '').replace(/\s+/g, ' ').trim();
const rect = el.getBoundingClientRect();
return labels.has(label) && rect.width > 0 && rect.height > 0 && el.getAttribute('aria-selected') !== 'true';
});
if (!tab) return;
tab.click();
// 给懒加载容器一次渲染机会,再由后续滚动加载实际图片。
await new Promise((resolve) => setTimeout(resolve, 600));
}
/**
* 自动滚动到页面底部,触发懒加载(详情图在页面尾部,不滚不加载)。
* 有界滚动:小步分段 + 随机延迟(模拟人工浏览节奏,避免"一滚到底"的机器人特征),
+86
View File
@@ -0,0 +1,86 @@
/** 小型 MD5 实现,仅用于淘宝公开 H5 接口的请求签名。 */
function add32(a: number, b: number): number { return (a + b) & 0xffffffff; }
function rol(num: number, cnt: number): number { return (num << cnt) | (num >>> (32 - cnt)); }
function cmn(q: number, a: number, b: number, x: number, s: number, t: number): number {
return add32(rol(add32(add32(a, q), add32(x, t)), s), b);
}
function ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
return cmn((b & c) | (~b & d), a, b, x, s, t);
}
function gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
return cmn((b & d) | (c & ~d), a, b, x, s, t);
}
function hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
return cmn(b ^ c ^ d, a, b, x, s, t);
}
function ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
return cmn(c ^ (b | ~d), a, b, x, s, t);
}
function cycle(state: number[], block: number[]): void {
let [a, b, c, d] = state;
a = ff(a, b, c, d, block[0], 7, -680876936); d = ff(d, a, b, c, block[1], 12, -389564586);
c = ff(c, d, a, b, block[2], 17, 606105819); b = ff(b, c, d, a, block[3], 22, -1044525330);
a = ff(a, b, c, d, block[4], 7, -176418897); d = ff(d, a, b, c, block[5], 12, 1200080426);
c = ff(c, d, a, b, block[6], 17, -1473231341); b = ff(b, c, d, a, block[7], 22, -45705983);
a = ff(a, b, c, d, block[8], 7, 1770035416); d = ff(d, a, b, c, block[9], 12, -1958414417);
c = ff(c, d, a, b, block[10], 17, -42063); b = ff(b, c, d, a, block[11], 22, -1990404162);
a = ff(a, b, c, d, block[12], 7, 1804603682); d = ff(d, a, b, c, block[13], 12, -40341101);
c = ff(c, d, a, b, block[14], 17, -1502002290); b = ff(b, c, d, a, block[15], 22, 1236535329);
a = gg(a, b, c, d, block[1], 5, -165796510); d = gg(d, a, b, c, block[6], 9, -1069501632);
c = gg(c, d, a, b, block[11], 14, 643717713); b = gg(b, c, d, a, block[0], 20, -373897302);
a = gg(a, b, c, d, block[5], 5, -701558691); d = gg(d, a, b, c, block[10], 9, 38016083);
c = gg(c, d, a, b, block[15], 14, -660478335); b = gg(b, c, d, a, block[4], 20, -405537848);
a = gg(a, b, c, d, block[9], 5, 568446438); d = gg(d, a, b, c, block[14], 9, -1019803690);
c = gg(c, d, a, b, block[3], 14, -187363961); b = gg(b, c, d, a, block[8], 20, 1163531501);
a = gg(a, b, c, d, block[13], 5, -1444681467); d = gg(d, a, b, c, block[2], 9, -51403784);
c = gg(c, d, a, b, block[7], 14, 1735328473); b = gg(b, c, d, a, block[12], 20, -1926607734);
a = hh(a, b, c, d, block[5], 4, -378558); d = hh(d, a, b, c, block[8], 11, -2022574463);
c = hh(c, d, a, b, block[11], 16, 1839030562); b = hh(b, c, d, a, block[14], 23, -35309556);
a = hh(a, b, c, d, block[1], 4, -1530992060); d = hh(d, a, b, c, block[4], 11, 1272893353);
c = hh(c, d, a, b, block[7], 16, -155497632); b = hh(b, c, d, a, block[10], 23, -1094730640);
a = hh(a, b, c, d, block[13], 4, 681279174); d = hh(d, a, b, c, block[0], 11, -358537222);
c = hh(c, d, a, b, block[3], 16, -722521979); b = hh(b, c, d, a, block[6], 23, 76029189);
a = hh(a, b, c, d, block[9], 4, -640364487); d = hh(d, a, b, c, block[12], 11, -421815835);
c = hh(c, d, a, b, block[15], 16, 530742520); b = hh(b, c, d, a, block[2], 23, -995338651);
a = ii(a, b, c, d, block[0], 6, -198630844); d = ii(d, a, b, c, block[7], 10, 1126891415);
c = ii(c, d, a, b, block[14], 15, -1416354905); b = ii(b, c, d, a, block[5], 21, -57434055);
a = ii(a, b, c, d, block[12], 6, 1700485571); d = ii(d, a, b, c, block[3], 10, -1894986606);
c = ii(c, d, a, b, block[10], 15, -1051523); b = ii(b, c, d, a, block[1], 21, -2054922799);
a = ii(a, b, c, d, block[8], 6, 1873313359); d = ii(d, a, b, c, block[15], 10, -30611744);
c = ii(c, d, a, b, block[6], 15, -1560198380); b = ii(b, c, d, a, block[13], 21, 1309151649);
a = ii(a, b, c, d, block[4], 6, -145523070); d = ii(d, a, b, c, block[11], 10, -1120210379);
c = ii(c, d, a, b, block[2], 15, 718787259); b = ii(b, c, d, a, block[9], 21, -343485551);
state[0] = add32(a, state[0]); state[1] = add32(b, state[1]);
state[2] = add32(c, state[2]); state[3] = add32(d, state[3]);
}
function block(input: string): number[] {
const out: number[] = [];
for (let i = 0; i < 64; i += 4) {
out[i >> 2] = input.charCodeAt(i) + (input.charCodeAt(i + 1) << 8)
+ (input.charCodeAt(i + 2) << 16) + (input.charCodeAt(i + 3) << 24);
}
return out;
}
/** 返回 UTF-8 字符串的 32 位小写 MD5。 */
export function md5(input: string): string {
const text = unescape(encodeURIComponent(input));
const state = [1732584193, -271733879, -1732584194, 271733878];
let i = 64;
for (; i <= text.length; i += 64) cycle(state, block(text.slice(i - 64, i)));
const tail = new Array<number>(16).fill(0);
const rest = text.slice(i - 64);
for (let j = 0; j < rest.length; j++) tail[j >> 2] |= rest.charCodeAt(j) << ((j % 4) << 3);
tail[rest.length >> 2] |= 0x80 << ((rest.length % 4) << 3);
if (rest.length > 55) cycle(state, tail);
tail.fill(0, rest.length > 55 ? 0 : 15);
tail[14] = text.length * 8;
cycle(state, tail);
return state.map((word) => {
let out = '';
for (let j = 0; j < 4; j++) out += ((word >> (j * 8)) & 0xff).toString(16).padStart(2, '0');
return out;
}).join('');
}
+1 -1
View File
@@ -83,7 +83,7 @@ export function finalize(
const warnings: string[] = [];
if (!texts.some((t) => t.kind === 'title')) warnings.push('未采集到标题');
if (images.length === 0) warnings.push('未扫描到任何图片/视频');
if ((stats.detail ?? 0) === 0) warnings.push('详情图为 0 张,请滚动到页面底部后重新采集');
if ((stats.detail ?? 0) === 0) warnings.push('详情图为 0 张:页面未提供可读取的图文详情数据');
return {
platform: profile.id,
@@ -2,16 +2,16 @@
* 1688 采集编排(平台文件):
* ① MAIN world 桥读 window.context(模块化 SSR 状态)★主路径
* —— 主图 / SKU 全规格图 / 价格区间 / SKU 级价格库存 / 每 SKU 长宽高重量 / 销量 / 店铺
* ② DOM 兜底 + 补充:#productAttributes 参数表(antd Descriptions)、#detail 详情图
* 说明:不再直调 description.detailUrl 数据端点(与淘宝 mtop 同样的风控考虑),
* 详情图改为滚动加载后由 DOM 采集补齐。
* ② description.detailUrl:直接读取图文详情,不操作用户页面
* ③ DOM 兜底 + 补充当前已挂载内容
*/
import { autoScrollToBottom, waitForAny } from '../dom';
import { waitForAny } from '../dom';
import { collectImages, type ImageMaterial } from '../image';
import { collectTexts, mergeTexts, type TextMaterial } from '../text';
import { toOriginalUrl } from '../url';
import { readWindowKeys } from '../../bridge/read-window';
import { extract1688State, type State1688 } from '../1688-state';
import { detailUrlsToMaterials, extractDetailImageUrls, fetchDetailText } from '../detail-images';
import { finalize, mergeImages, type ScanResult } from '../merge';
import type { SiteProfile } from '../../profiles/types';
@@ -20,6 +20,7 @@ export async function scan1688(profile: SiteProfile, itemId: string | null): Pro
let primaryImages: ImageMaterial[] = [];
let source: ScanResult['source'] = 'dom';
let st: State1688 | null = null;
const detailWarnings: string[] = [];
// ① 桥读 window.context
const keys = await readWindowKeys(['context']);
@@ -75,11 +76,22 @@ export async function scan1688(profile: SiteProfile, itemId: string | null): Pro
});
});
source = 'state';
if (st.detailUrl) {
try {
const rawDetail = await fetchDetailText(st.detailUrl, location.href);
const detailImages = detailUrlsToMaterials(extractDetailImageUrls(rawDetail, st.detailUrl), profile);
primaryImages.push(...detailImages);
if (detailImages.length === 0) detailWarnings.push('详情接口未解析到图片');
} catch (error) {
detailWarnings.push(`详情接口读取失败: ${error instanceof Error ? error.message : String(error)}`);
}
} else {
detailWarnings.push('页面未提供详情接口地址');
}
}
// ② DOM 兜底 + 补充(参数表 #productAttributes、详情图 #detail 在这里进结果)
// 慢速分段滚动触发懒加载(详情图不滚不加载),有界滚动防无限推荐流
await autoScrollToBottom();
// ③ DOM 兜底:只读取当前已经挂载的内容,不点击标签、不滚动页面。
const anchor = await waitForAny(profile.readySelectors, profile.readyTimeoutMs ?? 10_000);
if (!anchor) console.warn('[SuiteCollector] 等待页面就绪超时(继续尝试 DOM 采集)');
const { materials: domTexts, missingRequired } = collectTexts(profile);
@@ -90,6 +102,7 @@ export async function scan1688(profile: SiteProfile, itemId: string | null): Pro
const texts = mergeTexts(primaryTexts, domTexts);
const images = mergeImages(primaryImages, domImages, profile);
const result = finalize(profile, itemId, texts, images, [], source);
if ((result.stats.detail ?? 0) === 0) result.warnings.push(...detailWarnings);
if (missingRequired.length > 0) result.warnings.push(`缺少必需字段: ${missingRequired.join(', ')}`);
return result;
}
@@ -5,7 +5,7 @@
* ③ 站内页 JSON API(entrypoint-api.bx)
* ④ DOM data-widget 选择器兜底 + 详情图补充
*/
import { autoScrollToBottom, waitForAny } from '../dom';
import { activateDetailTab, autoScrollToBottom, waitForAny } from '../dom';
import { collectImages, type ImageMaterial } from '../image';
import { collectTexts, mergeTexts, type TextMaterial } from '../text';
import { toOriginalUrl, toThumbUrl } from '../url';
@@ -148,6 +148,7 @@ export async function scanOzon(profile: SiteProfile, itemId: string | null): Pro
// ── 路径④:DOM 采集(兜底 + 详情图补充)──
// 先滚到底触发懒加载(详情图不滚不加载),有界滚动防无限推荐流
await activateDetailTab();
await autoScrollToBottom();
const anchor = await waitForAny(profile.readySelectors, profile.readyTimeoutMs ?? 8_000);
if (!anchor) console.warn('[SuiteCollector] 等待页面就绪超时(继续尝试 DOM 采集)');
@@ -3,15 +3,15 @@
* ① MAIN world 桥读页面全局(__ICE_APP_CONTEXT__ 等)★主路径
* (isolated world 读不到 window 变量,v1 直读是无效的)
* ② DOM 兜底 + 补充
* 说明:不再直调 mtop 签名接口(h5api.m.taobao.com / h5api.m.tmall.com),
* 仅读取页面已加载数据(SSR 全局 + DOM),避免触发平台风控。
* 说明:图文详情通过页面已有登录态请求 mtop 描述接口;不点击标签、不滚动页面。
*/
import { autoScrollToBottom, waitForAny } from '../dom';
import { waitForAny } from '../dom';
import { collectImages, type ImageMaterial } from '../image';
import { collectTexts, mergeTexts, type TextMaterial } from '../text';
import { readWindowKeys } from '../../bridge/read-window';
import { buildFromSSR } from '../ssr-builder';
import { taobaoStateFromBridge } from '../taobao-state';
import { collectTaobaoDetailImages } from '../taobao-detail';
import { finalize, mergeImages, type ScanResult } from '../merge';
import type { SiteProfile } from '../../profiles/types';
@@ -19,6 +19,7 @@ export async function scanTaobao(profile: SiteProfile, _itemId: string | null):
let primaryTexts: TextMaterial[] = [];
let primaryImages: ImageMaterial[] = [];
let source: ScanResult['source'] = 'dom';
const detailWarnings: string[] = [];
// ① 桥读页面全局
const keys = await readWindowKeys(['__ICE_APP_CONTEXT__', '__general_skupanel_cache_data', '__ICE_DATA_LOADER__']);
@@ -31,9 +32,20 @@ export async function scanTaobao(profile: SiteProfile, _itemId: string | null):
source = 'ssr';
}
// ② DOM 兜底 + 补充
// 慢速分段滚动触发懒加载(详情图不滚不加载),有界滚动防无限推荐流
await autoScrollToBottom();
// ② 图文详情 API:使用页面已有登录态签名请求,不点击标签、不滚动页面。
if (_itemId) {
try {
const detailImages = await collectTaobaoDetailImages(_itemId, profile);
primaryImages.push(...detailImages);
if (detailImages.length === 0) detailWarnings.push('详情接口未解析到图片');
} catch (error) {
detailWarnings.push('详情接口读取失败: ' + (error instanceof Error ? error.message : String(error)));
}
} else {
detailWarnings.push('未识别到商品 ID');
}
// ③ DOM 兜底:只读取首屏已挂载内容,不点击详情标签、不滚动页面。
const anchor = await waitForAny(profile.readySelectors, profile.readyTimeoutMs ?? 10_000);
if (!anchor) console.warn('[SuiteCollector] 等待页面就绪超时(继续尝试 DOM 采集)');
const { materials: domTexts, missingRequired } = collectTexts(profile);
@@ -44,6 +56,7 @@ export async function scanTaobao(profile: SiteProfile, _itemId: string | null):
const texts = mergeTexts(primaryTexts, domTexts);
const images = mergeImages(primaryImages, domImages, profile);
const result = finalize(profile, _itemId, texts, images, [], source);
if ((result.stats.detail ?? 0) === 0) result.warnings.push(...detailWarnings);
if (missingRequired.length > 0) result.warnings.push(`缺少必需字段: ${missingRequired.join(', ')}`);
return result;
}
@@ -139,9 +139,9 @@ export function buildFromSSR(data: SSRData, profile: SiteProfile): ScanResult {
if (images.length === 0) {
warnings.push('未扫描到任何图片/视频');
}
// SSR 数据里没有详情图,需要 DOM 补充
// SSR 数据里没有详情图;由平台接口或当前已挂载 DOM 补充。
if (stats.detail === undefined) {
warnings.push('详情图需 DOM 补充:请滚动到页面底部后重新采集');
warnings.push('详情图需补充:页面未提供可读取的图文详情数据');
}
return {
@@ -0,0 +1,56 @@
import { detailUrlsToMaterials, extractDetailImageUrls, fetchDetailText } from './detail-images';
import { md5 } from './md5';
import type { ImageMaterial } from './image';
import type { SiteProfile } from '../profiles/types';
function getMtopToken(): string | null {
const match = document.cookie.match(/(?:^|;\s*)_m_h5_tk=([^;]+)/);
if (!match) return null;
try {
return decodeURIComponent(match[1]).split('_')[0] || null;
} catch {
return match[1].split('_')[0] || null;
}
}
function mtopUrl(itemId: string, token: string): string {
const now = Date.now().toString();
const current = new URL(location.href);
const data = JSON.stringify({
abbucket: current.searchParams.get('abbucket') || '',
id: itemId,
ns: current.searchParams.get('ns') || '',
spm: current.searchParams.get('spm') || '',
detail_v: '3.3.0',
preferWireless: 'true',
});
const host = current.hostname === 'detail.tmall.com' ? 'h5api.m.tmall.com' : 'h5api.m.taobao.com';
const url = new URL('https://' + host + '/h5/mtop.taobao.detail.getdesc/7.0/');
url.search = new URLSearchParams({
jsv: '2.7.0', appKey: '12574478', t: now, sign: md5(token + '&' + now + '&12574478&' + data),
api: 'mtop.taobao.detail.getdesc', v: '7.0', isSec: '0', ecode: '0',
AntiFlood: 'true', AntiCreep: 'true', H5Request: 'true', ttid: '2022@tmall_litepc_9.17.0',
type: 'json', dataType: 'json', data,
}).toString();
return url.href;
}
/** 无滚动读取淘宝/天猫图文详情。仅使用浏览器现有登录态,不会改动页面。 */
export async function collectTaobaoDetailImages(itemId: string, profile: SiteProfile): Promise<ImageMaterial[]> {
const token = getMtopToken();
if (!token) throw new Error('未找到淘宝登录态;请登录后重新采集');
const raw = await fetchDetailText(mtopUrl(itemId, token), location.href);
let response: any;
try {
response = JSON.parse(raw);
} catch {
throw new Error('详情接口返回了无效数据');
}
if (!response?.data?.components?.componentData) {
throw new Error(response?.ret?.[0] || '详情接口未返回图文详情');
}
const components = response.data.components.componentData;
const richText = components.desc_richtext_pc?.model?.text;
const source = typeof richText === 'string' ? richText : JSON.stringify(components);
return detailUrlsToMaterials(extractDetailImageUrls(source, location.href), profile);
}
+2
View File
@@ -21,6 +21,8 @@ export default defineConfig({
'https://detail.1688.com/*',
'https://item.taobao.com/*',
'https://detail.tmall.com/*',
'https://h5api.m.taobao.com/*',
'https://h5api.m.tmall.com/*',
'https://*.alicdn.com/*',
// 1688 详情数据 CDN(description.detailUrl)
'https://itemcdn.tmall.com/*',