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')