@bytebrand/export-cars 1.26.30 → 1.26.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/services/images.service.d.ts +4 -0
- package/dist/services/images.service.js +9 -3
- package/dist/services/images.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/openai-ads/AUDIT.md +1 -0
- package/docs/openai-ads/RUNBOOK.md +19 -0
- package/package.json +1 -1
- package/src/services/images.service.ts +15 -4
- package/test/clean-images.test.ts +26 -0
package/docs/openai-ads/AUDIT.md
CHANGED
|
@@ -182,6 +182,7 @@ Filters before mapping (all reasons counted in the run report):
|
|
|
182
182
|
| Q11 | OAI-AdsBot / OAI-SearchBot reachability of `www.auto.de` VDP and `images.auto.de` behind Cloudflare (js_challenge rules from ADV-193). Test only, no CF change without approval. | Eng + approval | go-live |
|
|
183
183
|
| Q12 | Image: ADV-336 clean profile is used (no overlay). Approval of plain photos + `ext_lr_main` question open in ADV-336 | ADV-336 | image_url |
|
|
184
184
|
| Q13 | Is a "Stable" vs "Draft" schema selector visible in your docs/account? The public page shows none on 09.10. | Marketing | — |
|
|
185
|
+
| Q14 | Make mapping at import (out of feed scope): DEV car titled "Jeep SWM G01a" — model SONSTIGE, variant names SWM (a different manufacturer), so the feed sends `brand=Jeep` for a non-Jeep car. Fix belongs to the import make/model mapping (sheet / CBD), not to the exporter. | Import owner | brand correctness |
|
|
185
186
|
|
|
186
187
|
---
|
|
187
188
|
|
|
@@ -77,6 +77,25 @@ up for the **live** output only (separate, approved step).
|
|
|
77
77
|
| `count_drop` | > N % fewer cars than the last good run | confirm the drop is real (CBD online count, dealer offboarding), then ONE run with `feedGuardAcceptDrop=true` |
|
|
78
78
|
| `locked` | another run holds `run.lock` (< 6 h) | wait; a lock older than 6 h is taken over automatically |
|
|
79
79
|
|
|
80
|
+
## Known risk: other export-cars jobs report failures as success (accepted, 12.10)
|
|
81
|
+
|
|
82
|
+
Every export-cars run except `openai_ads` ends with `{"complete":1,"code":0}`, even when the exporter threw:
|
|
83
|
+
nest-commander's default `serviceErrorHandler` only prints the error and `main.ts` then reports success. Cronicle
|
|
84
|
+
therefore treats a failed part 1 as successful and starts the chained part 2, which can upload an empty or
|
|
85
|
+
partial file (Google feed incident 24.09: a 169-byte feed was uploaded and fetched by Google). Decision
|
|
86
|
+
(user, 12.10): keep it — no global fix, no `chain_error` handlers, no Cronicle config change; the chains keep
|
|
87
|
+
running after a failure as before.
|
|
88
|
+
|
|
89
|
+
Only `openai_ads` is protected: the feed guard keeps the last good file, and the job exits with code 1
|
|
90
|
+
(`exitError` + exit 1 in `openai-ads.exporter.ts`). It has no chain, so this stops nothing else.
|
|
91
|
+
|
|
92
|
+
## Known source-data issues (not fixed by the feed)
|
|
93
|
+
|
|
94
|
+
- Wrong make from the import mapping: e.g. DEV "Jeep SWM G01a" (model SONSTIGE, the variant names another
|
|
95
|
+
manufacturer). The feed sends the stored make as `brand`; correct it in the import make/model mapping (AUDIT Q14).
|
|
96
|
+
- Handled in the feed since 1.26.30: placeholder model `SONSTIGE` is left out of title/description; UTF-8 read as
|
|
97
|
+
Latin-1/cp1252 upstream (`ü` → `ü`) is repaired.
|
|
98
|
+
|
|
80
99
|
## Rollback
|
|
81
100
|
|
|
82
101
|
- Bad content published: `cp <output>.prev <output>` (the previous good file), then disable the Cronicle event.
|
package/package.json
CHANGED
|
@@ -28,6 +28,11 @@ export const CLEAN_HERO_IMAGE_CODES = ['ext_f_right', 'ext_f_left'];
|
|
|
28
28
|
// Explicit JPEG variant — the extension-less `large` URL negotiates WebP from the Accept header
|
|
29
29
|
// (no Vary), so a crawler could get a different format than the one we validated.
|
|
30
30
|
export const CLEAN_IMAGE_VARIANT = 'large-cached.jpeg';
|
|
31
|
+
// Real exterior views the shared IMAGES_SORT_ORDER doesn't know, accepted by the clean profile only
|
|
32
|
+
// (other feeds stay unchanged). Ranks sit between the last exterior view (18) and conext_* (19).
|
|
33
|
+
export const CLEAN_EXTRA_IMAGE_SORT_ORDER: { [imageCode: string]: number } = {
|
|
34
|
+
ext_lr_main: 18.5,
|
|
35
|
+
};
|
|
31
36
|
|
|
32
37
|
@Injectable()
|
|
33
38
|
export class ImagesService {
|
|
@@ -195,8 +200,9 @@ export class ImagesService {
|
|
|
195
200
|
/**
|
|
196
201
|
* Clean channel profile (ADV-336): the car's own photos only, as explicit JPEG URLs, in a
|
|
197
202
|
* deterministic order — 3/4 exterior hero first, then the remaining exterior views, then the
|
|
198
|
-
* interior views (IMAGES_SORT_ORDER). Feature close-ups (`_feat_`),
|
|
199
|
-
*
|
|
203
|
+
* interior views (IMAGES_SORT_ORDER + CLEAN_EXTRA_IMAGE_SORT_ORDER). Feature close-ups (`_feat_`),
|
|
204
|
+
* `conext_*` and codes neither order knows (simple_*, promo, ...) are dropped; so are repeated
|
|
205
|
+
* imageIdentifiers.
|
|
200
206
|
* Returns [] when the car has no 3/4 exterior shot — callers skip the car, never substitute a
|
|
201
207
|
* placeholder. Does not validate the URLs (see ImageUrlValidator) and never mutates `images`.
|
|
202
208
|
*/
|
|
@@ -214,7 +220,8 @@ export class ImagesService {
|
|
|
214
220
|
seen.add(image.imageIdentifier);
|
|
215
221
|
return true;
|
|
216
222
|
});
|
|
217
|
-
|
|
223
|
+
// stable sort, ties keep Imbo order
|
|
224
|
+
const sorted = eligible.sort((a, b) => this.getCleanSortOrder(this.getImageCode(a)) - this.getCleanSortOrder(this.getImageCode(b)));
|
|
218
225
|
|
|
219
226
|
const heroIndex = CLEAN_HERO_IMAGE_CODES
|
|
220
227
|
.map(code => sorted.findIndex(image => this.getImageCode(image) === code))
|
|
@@ -240,7 +247,11 @@ export class ImagesService {
|
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
private isCleanImageCode(imageCode: string): boolean {
|
|
243
|
-
return /^(ext|int)_/.test(imageCode) && !imageCode.includes('_feat_') && imageCode
|
|
250
|
+
return /^(ext|int)_/.test(imageCode) && !imageCode.includes('_feat_') && this.getCleanSortOrder(imageCode) !== undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private getCleanSortOrder(imageCode: string): number | undefined {
|
|
254
|
+
return this.IMAGES_SORT_ORDER[imageCode] ?? CLEAN_EXTRA_IMAGE_SORT_ORDER[imageCode];
|
|
244
255
|
}
|
|
245
256
|
|
|
246
257
|
private getImageCode(image: ImboImageResponseInterface): string | undefined {
|
|
@@ -38,6 +38,32 @@ const cases: Array<[string, () => void | Promise<void>]> = [
|
|
|
38
38
|
];
|
|
39
39
|
assert.deepEqual(service.getCleanImages('car', images).map(i => i.imageIdentifier), ['left', 'cowl']);
|
|
40
40
|
}],
|
|
41
|
+
['ext_lr_main (clean-only allowlist) comes after the other exterior views, before interior', () => {
|
|
42
|
+
const images = [
|
|
43
|
+
{ imageIdentifier: 'dash', imageCodePredicted: 'int_f_ipanel1' },
|
|
44
|
+
{ imageIdentifier: 'lr', imageCodePredicted: 'ext_lr_main-normalcar' },
|
|
45
|
+
{ imageIdentifier: 'opened', imageCodePredicted: 'ext_f_right_opened' },
|
|
46
|
+
{ imageIdentifier: 'back', imageCodePredicted: 'ext_b_main' },
|
|
47
|
+
{ imageIdentifier: 'right', imageCodePredicted: 'ext_f_right-normalcar' },
|
|
48
|
+
];
|
|
49
|
+
const result = service.getCleanImages('car', images);
|
|
50
|
+
assert.deepEqual(result.map(i => i.imageIdentifier), ['right', 'back', 'opened', 'lr', 'dash']);
|
|
51
|
+
assert.equal(result.find(i => i.imageIdentifier === 'lr').heroCandidate, false);
|
|
52
|
+
}],
|
|
53
|
+
['ext_lr_main is never the hero', () => {
|
|
54
|
+
assert.deepEqual(service.getCleanImages('car', [{ imageIdentifier: 'lr', imageCodePredicted: 'ext_lr_main' }]), []);
|
|
55
|
+
}],
|
|
56
|
+
['the clean-only allowlist does not touch the other profiles (ext_lr_main still ranks 100 there)', async () => {
|
|
57
|
+
const images = () => [
|
|
58
|
+
{ imageIdentifier: 'lr', imageCodePredicted: 'ext_lr_main' },
|
|
59
|
+
{ imageIdentifier: 'dash', imageCodePredicted: 'int_f_ipanel1' },
|
|
60
|
+
{ imageIdentifier: 'right', imageCodePredicted: 'ext_f_right' },
|
|
61
|
+
];
|
|
62
|
+
const urls = await service.getImages({ _id: 'car' } as any, images(), ImageType.default, true);
|
|
63
|
+
assert.deepEqual(urls.map(u => u.split('/')[5]), ['right', 'dash', 'lr']);
|
|
64
|
+
const google = await service.getExtIntDefaultImages({ _id: 'car' } as any, images(), ImageType.default, false);
|
|
65
|
+
assert.deepEqual(google.ext.map(u => u.split('/')[5]), ['lr']);
|
|
66
|
+
}],
|
|
41
67
|
['no 3/4 exterior shot: no images at all (car is skipped, no fallback hero)', () => {
|
|
42
68
|
assert.deepEqual(clean('noHero'), []);
|
|
43
69
|
assert.deepEqual(clean('onlyUnknown'), []);
|