feat(ai-image): AI生图开发

This commit is contained in:
2026-09-27 22:40:52 +08:00
parent e670d06ca6
commit a29dd92676
105 changed files with 9943 additions and 361 deletions
+51 -17
View File
@@ -4,7 +4,7 @@ import {
isOzonProductPage,
type OzonPageJsonFetcher,
} from './ozon-parser.js';
import { collectPlatformDomAssets } from './product-media.js';
import { collectPlatformDomAssets, normaliseImageUrl } from './product-media.js';
import { formatCollectedDescription } from './description-format.js';
type SourcePlatform = '1688' | 'taobao' | 'tmall' | 'ozon';
@@ -49,7 +49,10 @@ export async function collectCurrentProduct(
function collect1688(root: Document, pageUrl: string, bridgedState?: unknown): CollectedProductPayload {
const state = [bridgedState, ...pageState(root)];
const skuMap = findFirst(state, 'skuMapOriginal');
const skuInfoMap = findFirst(state, 'skuInfoMap');
const skuMap = isRecord(skuInfoMap) && Object.keys(skuInfoMap).length
? skuInfoMap
: findFirst(state, 'skuMapOriginal');
const skuProps = findFirst(state, 'skuProps');
const axes = parse1688Axes(skuProps);
const evidence: CollectedProductPayload['evidence'] = [
@@ -70,32 +73,29 @@ function collect1688(root: Document, pageUrl: string, bridgedState?: unknown): C
evidence.push({
ref: '1688:sku-map',
kind: 'state',
path: 'skuMapOriginal',
path: isRecord(skuInfoMap) ? 'skuModel.skuInfoMap' : 'skuMapOriginal',
raw_value: `组合记录 ${skuRows.length} 条`,
});
for (const [combination, row] of skuRows) {
if (!isRecord(row)) continue;
const sourceSku = stringAt(row, ['skuId', 'sku_id', 'skuCode']);
// A skuMapOriginal entry itself is a source-provided sellable-combination
// record. Never expand it with the Cartesian product of skuProps.
const specAttrs = stringAt(row, ['specAttrs', 'propPath']) ?? combination;
const selections = selectionsFor1688Combination(specAttrs, axes);
// 来源记录本身证明可售组合,不能用规格轴的笛卡尔积扩充 SKU。
const sourceVariantKey = sourceSku ?? `1688:${combination}`;
if (!combination || combination === 'undefined') continue;
variants.push({
sourceVariantKey,
...(sourceSku ? { sourceSku } : {}),
displayName:
selectionsForCombination(combination, axes)
selections
.map((item) => item.valueName)
.join(' / ') || `1688 SKU ${sourceSku ?? combination}`,
selections: selectionsForCombination(combination, axes),
...(moneyAt(row, ['discountPrice', 'price', 'salePrice'])
.join(' / ') || specAttrs,
selections,
...(moneyAt(row, ['price'])
? {
sourcePrice: {
amount: moneyAt(row, [
'discountPrice',
'price',
'salePrice',
])!,
amount: moneyAt(row, ['price'])!,
currencyCode: 'CNY',
},
}
@@ -105,7 +105,7 @@ function collect1688(root: Document, pageUrl: string, bridgedState?: unknown): C
});
}
}
return basePayload(
const payload = basePayload(
'1688',
offerId(pageUrl),
root,
@@ -113,9 +113,29 @@ function collect1688(root: Document, pageUrl: string, bridgedState?: unknown): C
axes,
uniqueVariants(variants),
evidence,
'1688-structured-2.0.1',
'1688-structured-2.1.0',
state,
);
// SKU 图片由规格值和真实可售组合关联,独立于页面当时选中的 DOM 状态。
for (const variant of payload.variants) {
for (const selection of variant.selections) {
const rawUrl = axes.find((axis) => axis.key === selection.axisKey)
?.values.find((value) => value.key === selection.valueKey)?.image_url;
const url = rawUrl && normaliseImageUrl(rawUrl, pageUrl, '1688');
if (!url || payload.assets.length >= 500) continue;
if (payload.assets.some((asset) => asset.variantSourceKey === variant.sourceVariantKey && asset.url === url)) continue;
payload.assets.push({
url,
mediaType: 'image',
role: 'source_sku',
relation: 'axis_value',
variantSourceKey: variant.sourceVariantKey,
sortOrder: payload.assets.length,
evidenceRef: '1688:sku-map',
});
}
}
return payload;
}
function collectTaobao(
@@ -411,13 +431,14 @@ function pageState(root: Document) {
}
}
for (const marker of [
'skuInfoMap',
'skuMapOriginal',
'skuBase',
'skuCore',
'skuProps',
]) {
const object = objectAfterMarker(text, marker);
if (object) all.push(object);
if (object) all.push({ [marker]: object });
}
}
return all;
@@ -589,6 +610,19 @@ function selectionsForCombination(combination: string, axes: Axis[]) {
: [];
});
}
function selectionsFor1688Combination(combination: string, axes: Axis[]) {
const selected = selectionsForCombination(combination, axes);
if (selected.length === axes.length) return selected;
const parts = combination.split(/[;,&|]/).map((part) => part.trim()).filter(Boolean);
return axes.flatMap((axis) => {
const existing = selected.find((item) => item.axisKey === axis.key);
if (existing) return [existing];
const value = axis.values.find((item) =>
parts.some((part) => part === item.name || part === `${axis.name}:${item.name}`)
);
return value ? [{ axisKey: axis.key, valueKey: value.key, valueName: value.name }] : [];
});
}
function stringAt(value: Json, keys: string[]) {
for (const key of keys)
if (typeof value[key] === 'string' || typeof value[key] === 'number')
+62
View File
@@ -0,0 +1,62 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { collectCurrentProduct } from '../lib/source-parsers.ts';
function fixtureSkuModel(html: string) {
const marker = '"skuModel": ';
const start = html.indexOf(marker);
assert.ok(start >= 0);
const source = html.slice(start + marker.length);
let depth = 0;
let quoted = false;
let escaped = false;
for (let index = 0; index < source.length; index += 1) {
const char = source[index];
if (quoted) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === '"') quoted = false;
} else if (char === '"') quoted = true;
else if (char === '{') depth += 1;
else if (char === '}' && --depth === 0) return JSON.parse(source.slice(0, index + 1));
}
throw new Error('1688 样本缺少完整 skuModel');
}
test('1688 真实样本按 skuInfoMap 的 price 和规格图片生成逐 SKU 数据', async () => {
const html = await readFile(new URL('../../../packages/reference/1688.html', import.meta.url), 'utf8');
const skuModel = fixtureSkuModel(html);
const root = {
scripts: [],
querySelector: () => null,
querySelectorAll: () => [],
} as unknown as Document;
const payload = await collectCurrentProduct(
root,
'https://detail.1688.com/offer/123456789.html',
undefined,
async () => ({ skuModel }),
);
assert.equal(payload.variants.length, Object.keys(skuModel.skuInfoMap).length);
for (const [name, row] of Object.entries<any>(skuModel.skuInfoMap)) {
const variant = payload.variants.find((item) => item.sourceSku === String(row.skuId));
assert.ok(variant, `缺少 ${name}`);
assert.equal(variant.displayName, name);
assert.equal(variant.sourcePrice?.amount, row.price);
const image = skuModel.skuProps[0].value.find((item: any) => item.name === name)?.imageUrl;
assert.ok(payload.assets.some((asset) =>
asset.variantSourceKey === variant.sourceVariantKey && asset.url === image
), `${name} 未关联规格图片`);
}
const scriptRoot = {
...root,
scripts: [{ type: 'text/javascript', textContent: html }],
} as unknown as Document;
const withoutBridge = await collectCurrentProduct(
scriptRoot,
'https://detail.1688.com/offer/123456789.html',
);
assert.equal(withoutBridge.variants.length, payload.variants.length);
assert.equal(withoutBridge.assets.filter((asset) => asset.variantSourceKey).length, payload.variants.length);
});