fix
This commit is contained in:
@@ -123,6 +123,27 @@ info:
|
|||||||
processed_product_id / raw_product_id). Use GET /products when a UUID is needed.
|
processed_product_id / raw_product_id). Use GET /products when a UUID is needed.
|
||||||
Display name is items[].name (title omitted when identical).
|
Display name is items[].name (title omitted when identical).
|
||||||
|
|
||||||
|
### AI descriptions (HTML body, not a short blurb)
|
||||||
|
|
||||||
|
Default processing_type full runs normalize, parse specs, fill fields, EPREL
|
||||||
|
(when enabled), categorize, then AI enhance. Enhance writes the display name
|
||||||
|
and a full product body.
|
||||||
|
|
||||||
|
Completed items[].description is that body: one HTML string (tags matching
|
||||||
|
section types h1/h2/h3/h4, p, ul) — never a JSON array and never a one- or
|
||||||
|
two-sentence slogan. Length and structure come from the assigned category
|
||||||
|
description_template (description formula): ordered sections with their own
|
||||||
|
instructions. A formula can ask for a short paragraph, 100-word blocks,
|
||||||
|
several heading/list sections, or a very large multi-section article. There
|
||||||
|
is no request-body size enum; configure length on the category. When a
|
||||||
|
category has no formula, the default is still 1-3 factual paragraphs with
|
||||||
|
limited HTML, not a headline-only stub.
|
||||||
|
|
||||||
|
items[].description on the start body is optional source text the model may
|
||||||
|
rewrite. Poll GET /products/process/{id} for the generated HTML.
|
||||||
|
meta_title / meta_description are separate short SEO fields and may be
|
||||||
|
omitted depending on company configuration.
|
||||||
|
|
||||||
Note: Dashboard JSON under /api/* uses session cookies + CSRF and is separate
|
Note: Dashboard JSON under /api/* uses session cookies + CSRF and is separate
|
||||||
from this public API-key surface. Other path aliases
|
from this public API-key surface. Other path aliases
|
||||||
(/categories/create, /attributes/create, /campaigns) appear next to canonical paths.
|
(/categories/create, /attributes/create, /campaigns) appear next to canonical paths.
|
||||||
@@ -674,7 +695,10 @@ paths:
|
|||||||
description: Enriched display title
|
description: Enriched display title
|
||||||
processed_description:
|
processed_description:
|
||||||
type: string
|
type: string
|
||||||
description: Enriched long description text
|
description: |
|
||||||
|
Enriched product body as one HTML string from the category
|
||||||
|
description formula. Length follows that formula (including very
|
||||||
|
large multi-section copy), not a short summary.
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -788,14 +812,23 @@ paths:
|
|||||||
- Processing
|
- Processing
|
||||||
summary: Start processing by EAN
|
summary: Start processing by EAN
|
||||||
description: |
|
description: |
|
||||||
Public process API (handleV1StartProcess). Upserts raw products from
|
Public process API. Upserts raw products from items[].ean (GTIN), enqueues
|
||||||
items[].ean (GTIN), enqueues one processing job, returns HTTP 200 with a data envelope.
|
one processing job, returns HTTP 200 with a data envelope.
|
||||||
|
|
||||||
|
Default processing_type full runs the full pipeline including AI enhance:
|
||||||
|
the model writes a retail title (name) and a full HTML product description
|
||||||
|
driven by the category description formula. That body can be long —
|
||||||
|
multiple headings, paragraphs, and lists, including very large
|
||||||
|
multi-section articles — not a one- or two-sentence summary.
|
||||||
|
|
||||||
Primary body: items[].ean. Alternate body on the same handler: raw_product_ids
|
Primary body: items[].ean. Alternate body on the same handler: raw_product_ids
|
||||||
(raw_products.id UUID list — not GET /products data[].id). Prefer items for public
|
(raw_products.id UUID list — not GET /products data[].id). Prefer items for public
|
||||||
integrations. On items[].ean, assertV1ProcessGates runs before EnsureRaw so plan/credit
|
integrations. On items[].ean, assertV1ProcessGates runs before EnsureRaw so plan/credit
|
||||||
failures cannot spam catalog writes (HTTP 402 coded envelope).
|
failures cannot spam catalog writes (HTTP 402 coded envelope).
|
||||||
|
|
||||||
|
Poll GET /products/process/{id} until status is COMPLETED, then read
|
||||||
|
items[].description for the generated HTML.
|
||||||
|
|
||||||
Not an alias of POST /process (flat ProcessingJob / 202). Do not mix envelopes.
|
Not an alias of POST /process (flat ProcessingJob / 202). Do not mix envelopes.
|
||||||
requestBody:
|
requestBody:
|
||||||
$ref: "#/components/requestBodies/StartProcessByEAN"
|
$ref: "#/components/requestBodies/StartProcessByEAN"
|
||||||
@@ -903,9 +936,13 @@ paths:
|
|||||||
jobs omit items. Not the same shape as GET /process/{id} (flat ProcessingJob).
|
jobs omit items. Not the same shape as GET /process/{id} (flat ProcessingJob).
|
||||||
|
|
||||||
On COMPLETED items, name is the product display name (title is omitted when
|
On COMPLETED items, name is the product display name (title is omitted when
|
||||||
identical). Internal UUIDs (id / processed_product_id / raw_product_id) are
|
identical). description is the generated product body: one HTML string
|
||||||
omitted — use GET /products for those. Optional meta_title and
|
following the category description formula (h1/h2/h3/h4, p, ul). Expect
|
||||||
meta_description may be omitted depending on company configuration.
|
real catalog copy — often several paragraphs and lists, sometimes a very
|
||||||
|
large article — not a one- or two-sentence blurb. Internal UUIDs (id /
|
||||||
|
processed_product_id / raw_product_id) are omitted — use GET /products for
|
||||||
|
those. Optional meta_title and meta_description are short SEO fields and
|
||||||
|
may be omitted depending on company configuration.
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@@ -938,12 +975,13 @@ paths:
|
|||||||
category_id: '50'
|
category_id: '50'
|
||||||
category_name: Cookers
|
category_name: Cookers
|
||||||
name: VOX electric cooker EHT 6020 WG
|
name: VOX electric cooker EHT 6020 WG
|
||||||
description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven."
|
description: "<h1>VOX electric cooker EHT 6020 WG</h1><p>The VOX EHT 6020 WG is a free-standing electric cooker for everyday household cooking. Four ceramic hobs and a 65 L fan oven let you boil, fry, and bake at the same time, while energy class A helps keep running costs down. The 60 cm width fits a standard kitchen niche. An enamel oven cavity and removable inner door glass are designed for cleaning after heavy use.</p><p>Run sauces or pasta on the hob while the oven handles a roast or tray bake. Heat is even across the ceramic zones, and residual-heat indicators reduce the chance of accidental contact after cooking. Fan mode suits multi-level baking; conventional heat remains available for dishes that prefer still air. A full-width grill finishes cheese, gratins, and toast.</p><h2>Key features</h2><ul><li>Four electric ceramic hobs with residual-heat indicators</li><li>65 L fan oven plus grill, energy class A</li><li>60 cm free-standing body (0.6 m wide, 0.85 m high, 0.6 m deep)</li><li>Enamel cavity and removable inner door glass</li><li>60-month manufacturer warranty</li></ul><p>Install in a standard 60 cm cooker slot with the dedicated electric supply shown on the rating plate. Leave rear clearance for ventilation and use a heat-resistant worktop next to the hob. This product is for household kitchens, not commercial catering loads.</p><p>Use cookware that matches each zone so energy is not wasted around the pan. Wipe the ceramic surface regularly and empty crumbs from the oven floor to preserve appearance and efficiency. For spare parts, quote model EHT 6020 WG and the EAN on the rating plate.</p>"
|
||||||
attributes:
|
attributes:
|
||||||
brand: Vox
|
brand: Vox
|
||||||
product_model: EHT6020WG
|
product_model: EHT6020WG
|
||||||
main_image: https://images.example.com/products/vox-eht6020wg.jpg
|
main_image: https://images.example.com/products/vox-eht6020wg.jpg
|
||||||
more_images: null
|
more_images:
|
||||||
|
- https://images.example.com/products/vox-eht6020wg-side.jpg
|
||||||
eprel:
|
eprel:
|
||||||
id: "1234567"
|
id: "1234567"
|
||||||
label: https://eprel.ec.europa.eu/label/Example
|
label: https://eprel.ec.europa.eu/label/Example
|
||||||
@@ -4389,7 +4427,9 @@ paths:
|
|||||||
summary: Processing job status (raw_product_ids surface)
|
summary: Processing job status (raw_product_ids surface)
|
||||||
description: |
|
description: |
|
||||||
Public processing job JSON: status, counts, timestamps, processing_type.
|
Public processing job JSON: status, counts, timestamps, processing_type.
|
||||||
Omits current_step, step_progress, and company_id. Completed jobs still include items[].
|
Omits current_step, step_progress, and company_id. Completed jobs still include
|
||||||
|
items[] with the same AI HTML description contract as GET /products/process/{id}
|
||||||
|
(category formula body, not a one- or two-sentence blurb).
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/ID"
|
- $ref: "#/components/parameters/ID"
|
||||||
responses:
|
responses:
|
||||||
@@ -4421,6 +4461,7 @@ paths:
|
|||||||
status: processed
|
status: processed
|
||||||
name: Acme Wireless Earbuds ANC Black
|
name: Acme Wireless Earbuds ANC Black
|
||||||
category: Headphones
|
category: Headphones
|
||||||
|
description: "<h1>Acme Wireless Earbuds ANC Black</h1><p>Acme Wireless Earbuds ANC Black are in-ear headphones for commuting and desk work. Active noise cancellation, a secure fit, and a charging case cover a full workday of listening. The compact buds sit flush enough for glasses and hats, and the case fits a jacket pocket.</p><p>Pair over Bluetooth and switch between a phone and a laptop with the onboard controls. Transparency mode lets nearby voices through without removing the buds. The default ear-tip set covers common sizes; swap tips if bass feels weak or the seal slips during a walk.</p><h2>In the box</h2><ul><li>Wireless earbuds (left and right)</li><li>Charging case with USB-C cable</li><li>Three ear-tip sizes</li><li>Quick-start guide</li></ul><p>Charge the case before first use. Keep the charging contacts dry and store the buds in the case so they sleep and recharge between sessions. For replacement tips or a spare case, quote the product name and the EAN on the box.</p>"
|
||||||
total_items: 1
|
total_items: 1
|
||||||
'400':
|
'400':
|
||||||
description: Path id is not a UUID.
|
description: Path id is not a UUID.
|
||||||
@@ -5133,7 +5174,10 @@ components:
|
|||||||
description: Optional product title seed for enrichment
|
description: Optional product title seed for enrichment
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
description: Optional long description seed for enrichment
|
description: |
|
||||||
|
Optional source/seed text. AI enhance rewrites this into the
|
||||||
|
category description-formula HTML body. It is not the final
|
||||||
|
poll payload and does not cap length to a short summary.
|
||||||
specifications:
|
specifications:
|
||||||
type: array
|
type: array
|
||||||
description: Optional key/value specification pairs for attribute hints
|
description: Optional key/value specification pairs for attribute hints
|
||||||
@@ -5193,10 +5237,13 @@ components:
|
|||||||
Not PresentProduct.id. Use GET /products data[].raw_product_id.
|
Not PresentProduct.id. Use GET /products data[].raw_product_id.
|
||||||
processing_type:
|
processing_type:
|
||||||
description: |
|
description: |
|
||||||
full (default); named steps category|title|description|attributes (string or
|
full (default) runs the whole pipeline including AI enhance, which
|
||||||
|
writes formula-driven HTML descriptions (not a one- or two-sentence
|
||||||
|
blurb). Named steps category|title|description|attributes (string or
|
||||||
array of those steps); dual-mode also accepts v2 dashboard types such as
|
array of those steps); dual-mode also accepts v2 dashboard types such as
|
||||||
normalize_only, enhance_only, attributes_only, eprel_only, categorize_only
|
normalize_only, enhance_only, attributes_only, eprel_only, categorize_only
|
||||||
(ParseV1ProcessingType). Alias processingType accepted when values match.
|
(ParseV1ProcessingType). Alias processingType accepted when values match.
|
||||||
|
The description step still emits the category formula HTML body.
|
||||||
oneOf:
|
oneOf:
|
||||||
- type: string
|
- type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -5550,8 +5597,13 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
|
description: |
|
||||||
|
Product body. After processing this is the AI HTML description
|
||||||
|
(category formula), not a one- or two-sentence summary.
|
||||||
processed_description:
|
processed_description:
|
||||||
type: string
|
type: string
|
||||||
|
description: |
|
||||||
|
Enriched HTML body from AI enhance. Same contract as ProcessItem.description.
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
example: completed
|
example: completed
|
||||||
@@ -5664,8 +5716,16 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
title_template:
|
title_template:
|
||||||
nullable: true
|
nullable: true
|
||||||
|
description: |
|
||||||
|
Title formula for AI enhance. Controls how processed name is built
|
||||||
|
(type, brand, model, and other slots).
|
||||||
description_template:
|
description_template:
|
||||||
nullable: true
|
nullable: true
|
||||||
|
description: |
|
||||||
|
Description formula for AI enhance. Ordered sections (h1/h2/h3/h4, p, ul)
|
||||||
|
with per-section instructions. Controls HTML structure and length of
|
||||||
|
processed items[].description — short, long, or very large copy.
|
||||||
|
Not a one- or two-sentence cap.
|
||||||
created_at:
|
created_at:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@@ -6297,10 +6357,11 @@ components:
|
|||||||
description: |
|
description: |
|
||||||
One COMPLETED process line (public contract). Successful items
|
One COMPLETED process line (public contract). Successful items
|
||||||
expose category as the human-readable display name (category_id holds
|
expose category as the human-readable display name (category_id holds
|
||||||
categories.unique_id), a description string that may include section
|
categories.unique_id), a description string that is generated HTML
|
||||||
HTML (h1/h2/h3/h4, p, ul — never a JSON array), optional SEO meta_title /
|
product copy (h1/h2/h3/h4, p, ul — never a JSON array and never a one-
|
||||||
meta_description (plain text; some companies omit these fields),
|
or two-sentence slogan), optional SEO meta_title / meta_description
|
||||||
optional eprel object or null, clean attributes, and images.
|
(plain text; some companies omit these fields), optional eprel object
|
||||||
|
or null, clean attributes, and images.
|
||||||
Product display name is name (primary). title is omitted when identical to name.
|
Product display name is name (primary). title is omitted when identical to name.
|
||||||
Internal UUIDs (id, processed_product_id, raw_product_id) are omitted from this
|
Internal UUIDs (id, processed_product_id, raw_product_id) are omitted from this
|
||||||
public shape — use catalog APIs when a product UUID is required.
|
public shape — use catalog APIs when a product UUID is required.
|
||||||
@@ -6317,8 +6378,6 @@ components:
|
|||||||
description: |
|
description: |
|
||||||
Human-readable category display name (same value as category_name when
|
Human-readable category display name (same value as category_name when
|
||||||
resolved). Prefer this for UI. Machine unique_id is category_id.
|
resolved). Prefer this for UI. Machine unique_id is category_id.
|
||||||
ASSUMPTION: historically this field held categories.unique_id; clients
|
|
||||||
that need the opaque id must read category_id (additive).
|
|
||||||
category_id:
|
category_id:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -6356,9 +6415,14 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
description: |
|
description: |
|
||||||
Product body description as a string — never a one-element JSON array.
|
Generated product body as one HTML string — never a JSON array and
|
||||||
May include section HTML tags (h1/h2/h3/h4, p, ul). Plain-text
|
never a one- or two-sentence slogan. AI enhance follows the assigned
|
||||||
descriptions remain plain. Optional meta_description stays plain SEO text.
|
category description_template (description formula): ordered sections
|
||||||
|
of type h1, h2, h3, h4, p, and ul, each with its own instructions.
|
||||||
|
Output can be a short paragraph, several 100-word blocks, or a very
|
||||||
|
large multi-section article. When no formula is set, expect 1-3
|
||||||
|
factual paragraphs with limited HTML. Optional meta_description stays
|
||||||
|
plain SEO text and is not a substitute for this field.
|
||||||
attributes:
|
attributes:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
@@ -6477,7 +6541,7 @@ components:
|
|||||||
category_id: '50'
|
category_id: '50'
|
||||||
category_name: Cookers
|
category_name: Cookers
|
||||||
name: VOX electric cooker EHT 6020 WG
|
name: VOX electric cooker EHT 6020 WG
|
||||||
description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven. Energy class A with practical everyday capacity."
|
description: "<h1>VOX electric cooker EHT 6020 WG</h1><p>The VOX EHT 6020 WG is a free-standing electric cooker for everyday household cooking. Four ceramic hobs and a 65 L fan oven let you boil, fry, and bake at the same time, while energy class A helps keep running costs down. The 60 cm width fits a standard kitchen niche. An enamel oven cavity and removable inner door glass are designed for cleaning after heavy use.</p><p>Run sauces or pasta on the hob while the oven handles a roast or tray bake. Heat is even across the ceramic zones, and residual-heat indicators reduce the chance of accidental contact after cooking. Fan mode suits multi-level baking; conventional heat remains available for dishes that prefer still air. A full-width grill finishes cheese, gratins, and toast.</p><h2>Key features</h2><ul><li>Four electric ceramic hobs with residual-heat indicators</li><li>65 L fan oven plus grill, energy class A</li><li>60 cm free-standing body (0.6 m wide, 0.85 m high, 0.6 m deep)</li><li>Enamel cavity and removable inner door glass</li><li>60-month manufacturer warranty</li></ul><p>Install in a standard 60 cm cooker slot with the dedicated electric supply shown on the rating plate. Leave rear clearance for ventilation and use a heat-resistant worktop next to the hob. This product is for household kitchens, not commercial catering loads.</p><p>Use cookware that matches each zone so energy is not wasted around the pan. Wipe the ceramic surface regularly and empty crumbs from the oven floor to preserve appearance and efficiency. For spare parts, quote model EHT 6020 WG and the EAN on the rating plate.</p>"
|
||||||
attributes:
|
attributes:
|
||||||
brand: Vox
|
brand: Vox
|
||||||
product_model: EHT6020WG
|
product_model: EHT6020WG
|
||||||
|
|||||||
@@ -467,6 +467,44 @@ func TestV1OpenAPIYAMLOmitsInternalPromptWording(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestV1OpenAPIYAMLDocumentsAIDescriptionHTML(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
doc := string(v1OpenAPIYAML)
|
||||||
|
for _, needle := range []string{
|
||||||
|
"AI descriptions (HTML body, not a short blurb)",
|
||||||
|
"description_template (description formula)",
|
||||||
|
"one- or two-sentence",
|
||||||
|
"<h1>VOX electric cooker EHT 6020 WG</h1>",
|
||||||
|
"<h2>Key features</h2>",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(doc, needle) {
|
||||||
|
t.Fatalf("openapi missing AI description contract text %q", needle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
|
||||||
|
comps, _ := root["components"].(map[string]any)
|
||||||
|
examples, _ := comps["examples"].(map[string]any)
|
||||||
|
completed, _ := examples["ProcessCompletedExample"].(map[string]any)
|
||||||
|
val, _ := completed["value"].(map[string]any)
|
||||||
|
data, _ := val["data"].(map[string]any)
|
||||||
|
items, _ := data["items"].([]any)
|
||||||
|
if len(items) == 0 {
|
||||||
|
t.Fatal("ProcessCompletedExample: expected items")
|
||||||
|
}
|
||||||
|
item, _ := items[0].(map[string]any)
|
||||||
|
desc, _ := item["description"].(string)
|
||||||
|
for _, tag := range []string{"<h1>", "<p>", "<h2>", "<ul>", "<li>"} {
|
||||||
|
if !strings.Contains(desc, tag) {
|
||||||
|
t.Fatalf("ProcessCompletedExample description must include %s (got %q)", tag, desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(desc) < 800 {
|
||||||
|
t.Fatalf("ProcessCompletedExample description too short (%d chars); expected full AI HTML body", len(desc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestV1OpenAPIYAMLProcessItemCompletedExamplesOmitLeakyShape(t *testing.T) {
|
func TestV1OpenAPIYAMLProcessItemCompletedExamplesOmitLeakyShape(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
|
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ func PublicV1Error(msg string) string {
|
|||||||
if code := mapPublicErrorCode(cleaned); code != "" {
|
if code := mapPublicErrorCode(cleaned); code != "" {
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
if code := mapPublicV1FallbackCode(cleaned); code != "" {
|
||||||
|
return code
|
||||||
|
}
|
||||||
if looksLikeInternalProviderError(cleaned) {
|
if looksLikeInternalProviderError(cleaned) {
|
||||||
return publicErrProcessingFailed
|
return publicErrProcessingFailed
|
||||||
}
|
}
|
||||||
@@ -172,10 +175,6 @@ func mapPublicErrorCode(msg string) string {
|
|||||||
return publicErrProviderUnavailable
|
return publicErrProviderUnavailable
|
||||||
case strings.Contains(lower, "ai_enhance: skipped"):
|
case strings.Contains(lower, "ai_enhance: skipped"):
|
||||||
return publicErrProcessingFailed
|
return publicErrProcessingFailed
|
||||||
case strings.Contains(lower, "unavailable"),
|
|
||||||
strings.Contains(lower, "http 503"),
|
|
||||||
strings.Contains(lower, "overloaded"):
|
|
||||||
return publicErrProviderUnavailable
|
|
||||||
case strings.Contains(lower, "openai"),
|
case strings.Contains(lower, "openai"),
|
||||||
strings.Contains(lower, "gpt-"),
|
strings.Contains(lower, "gpt-"),
|
||||||
strings.Contains(lower, " for model"):
|
strings.Contains(lower, " for model"):
|
||||||
@@ -184,6 +183,17 @@ func mapPublicErrorCode(msg string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapPublicV1FallbackCode(msg string) string {
|
||||||
|
lower := strings.ToLower(strings.TrimSpace(msg))
|
||||||
|
switch {
|
||||||
|
case strings.Contains(lower, "unavailable"),
|
||||||
|
strings.Contains(lower, "http 503"),
|
||||||
|
strings.Contains(lower, "overloaded"):
|
||||||
|
return publicErrProviderUnavailable
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func looksLikeInternalProviderError(msg string) bool {
|
func looksLikeInternalProviderError(msg string) bool {
|
||||||
lower := strings.ToLower(msg)
|
lower := strings.ToLower(msg)
|
||||||
for _, n := range []string{
|
for _, n := range []string{
|
||||||
|
|||||||
@@ -62,14 +62,6 @@ func TestTruncateError_mapsInternalProviderLeaks(t *testing.T) {
|
|||||||
in: "ai_enhance: skipped (platform OpenAI unset; configure admin settings or company BYOK)",
|
in: "ai_enhance: skipped (platform OpenAI unset; configure admin settings or company BYOK)",
|
||||||
want: "provider_unavailable",
|
want: "provider_unavailable",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
in: "openai retries exhausted: green-chat unavailable",
|
|
||||||
want: "provider_unavailable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: "upstream 503: model overloaded",
|
|
||||||
want: "provider_unavailable",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
got := TruncateError(errString(tc.in))
|
got := TruncateError(errString(tc.in))
|
||||||
@@ -91,6 +83,8 @@ func TestPublicV1Error_stripsProviderInternals(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{in: `openai length-capped at max_tokens=8000 for model "gpt-4o"`, want: "processing_failed"},
|
{in: `openai length-capped at max_tokens=8000 for model "gpt-4o"`, want: "processing_failed"},
|
||||||
{in: "ai_enhance: skipped (platform OpenAI unset; configure admin settings or company BYOK)", want: "provider_unavailable"},
|
{in: "ai_enhance: skipped (platform OpenAI unset; configure admin settings or company BYOK)", want: "provider_unavailable"},
|
||||||
|
{in: "openai retries exhausted: green-chat unavailable", want: "provider_unavailable"},
|
||||||
|
{in: "upstream 503: model overloaded", want: "provider_unavailable"},
|
||||||
{in: "processing_failed", want: "processing_failed"},
|
{in: "processing_failed", want: "processing_failed"},
|
||||||
{in: "Product data not available", want: "Product data not available"},
|
{in: "Product data not available", want: "Product data not available"},
|
||||||
{in: "processing.job.error.all_failed|count=3", want: "processing.job.error.all_failed|count=3"},
|
{in: "processing.job.error.all_failed|count=3", want: "processing.job.error.all_failed|count=3"},
|
||||||
@@ -127,8 +121,8 @@ in: "unauthorized", want: "AI provider rejected the API key"},
|
|||||||
in: "openai http 401", want: "AI provider rejected the API key"},
|
in: "openai http 401", want: "AI provider rejected the API key"},
|
||||||
{in: "rate limited or server error", want: "AI provider temporarily unavailable"},
|
{in: "rate limited or server error", want: "AI provider temporarily unavailable"},
|
||||||
{in: "too many requests", want: "AI provider rate limited"},
|
{in: "too many requests", want: "AI provider rate limited"},
|
||||||
{in: "upstream 503: model overloaded", want: "provider_unavailable"},
|
{in: "upstream 503: model overloaded", want: "upstream 503: model overloaded"},
|
||||||
{in: "openai retries exhausted: green-chat unavailable", want: "provider_unavailable"},
|
{in: "openai retries exhausted: green-chat unavailable", want: "green-chat unavailable"},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
got := TruncateError(errString(tc.in))
|
got := TruncateError(errString(tc.in))
|
||||||
|
|||||||
@@ -570,14 +570,14 @@
|
|||||||
"ja": "この招待とは別のメールアドレスでログインしています。"
|
"ja": "この招待とは別のメールアドレスでログインしています。"
|
||||||
},
|
},
|
||||||
"Expired link? Ask a company admin to re-issue — there is no self-serve resend API.": {
|
"Expired link? Ask a company admin to re-issue — there is no self-serve resend API.": {
|
||||||
"es": "¿Enlace caducado? Pide a un administrador que lo reemita — no hay API de reenvío autoservicio. Administradores de plataforma:",
|
"es": "¿Enlace caducado? Pide a un administrador de la empresa que lo reemita — no hay API de reenvío autoservicio.",
|
||||||
"fr": "Lien expiré ? Demandez à un administrateur de le réémettre — il n'y a pas d'API de renvoi en libre-service. Administrateurs de plateforme :",
|
"fr": "Lien expiré ? Demandez à un administrateur de l'entreprise de le réémettre — il n'y a pas d'API de renvoi en libre-service.",
|
||||||
"de": "Abgelaufener Link? Bitten Sie einen Admin um Neuausstellung — es gibt keine Self-Service-API zum erneuten Senden. Plattform-Admins:",
|
"de": "Abgelaufener Link? Bitten Sie einen Unternehmens-Admin um Neuausstellung — es gibt keine Self-Service-API zum erneuten Senden.",
|
||||||
"it": "Link scaduto? Chiedi a un amministratore di riemetterlo — non c'è un'API di reinvio self-service. Amministratori della piattaforma:",
|
"it": "Link scaduto? Chiedi a un amministratore dell'azienda di riemetterlo — non c'è un'API di reinvio self-service.",
|
||||||
"pt": "Link expirado? Peça a um administrador para o reemitir — não há API de reenvio self-service. Administradores da plataforma:",
|
"pt": "Link expirado? Peça a um administrador da empresa para o reemitir — não há API de reenvio self-service.",
|
||||||
"nl": "Verlopen link? Vraag een beheerder om opnieuw uit te geven — er is geen self-service-API voor opnieuw verzenden. Platformbeheerders:",
|
"nl": "Verlopen link? Vraag een bedrijfsbeheerder om opnieuw uit te geven — er is geen self-service-API voor opnieuw verzenden.",
|
||||||
"pl": "Wygasły link? Poproś administratora o ponowne wystawienie — nie ma API samodzielnego ponownego wysyłania. Administratorzy platformy:",
|
"pl": "Wygasły link? Poproś administratora firmy o ponowne wystawienie — nie ma API samodzielnego ponownego wysyłania.",
|
||||||
"ja": "期限切れのリンクですか?管理者に再発行を依頼してください — セルフサービスの再送信APIはありません。プラットフォーム管理者:"
|
"ja": "期限切れのリンクですか?会社の管理者に再発行を依頼してください — セルフサービスの再送信APIはありません。"
|
||||||
},
|
},
|
||||||
"You're on the team": {
|
"You're on the team": {
|
||||||
"es": "Ya formas parte del equipo",
|
"es": "Ya formas parte del equipo",
|
||||||
@@ -730,14 +730,14 @@
|
|||||||
"ja": "再発行の手順:"
|
"ja": "再発行の手順:"
|
||||||
},
|
},
|
||||||
"ask a company admin to re-issue a set-password link to this email, or to revoke this invite and send a new one to the email you use to sign in.": {
|
"ask a company admin to re-issue a set-password link to this email, or to revoke this invite and send a new one to the email you use to sign in.": {
|
||||||
"es": "si cambió tu correo real de acceso (desfase de correo), pide a un administrador de la empresa que revoque esta invitación y envíe una nueva al correo con el que inicias sesión. Los administradores de plataforma también pueden reemitir enlaces para establecer contraseña desde Admin → Usuarios.",
|
"es": "pide a un administrador de la empresa que reemita un enlace para establecer contraseña a este correo, o que revoque esta invitación y envíe una nueva al correo con el que inicias sesión.",
|
||||||
"fr": "si votre vrai e-mail de connexion a changé (dérive d'e-mail), demandez à un administrateur de l'entreprise de révoquer cette invitation et d'en envoyer une nouvelle à l'e-mail que vous utilisez pour vous connecter. Les administrateurs de la plateforme peuvent aussi réémettre des liens de définition de mot de passe depuis Admin → Utilisateurs.",
|
"fr": "demandez à un administrateur de l'entreprise de réémettre un lien de définition de mot de passe vers cet e-mail, ou de révoquer cette invitation et d'en envoyer une nouvelle à l'e-mail que vous utilisez pour vous connecter.",
|
||||||
"de": "wenn sich Ihre echte Anmelde-E-Mail geändert hat (E-Mail-Drift), bitten Sie einen Unternehmens-Admin, diese Einladung zu widerrufen und eine neue an die E-Mail zu senden, mit der Sie sich anmelden. Plattform-Admins können Passwort-Links auch unter Admin → Benutzer erneut ausstellen.",
|
"de": "bitten Sie einen Unternehmens-Admin, einen Link zum Festlegen des Passworts an diese E-Mail auszustellen oder diese Einladung zu widerrufen und eine neue an die E-Mail zu senden, mit der Sie sich anmelden.",
|
||||||
"it": "se la tua email di accesso reale è cambiata (deriva email), chiedi a un amministratore dell'azienda di revocare questo invito e inviarne uno nuovo all'email con cui accedi. Gli amministratori della piattaforma possono anche riemettere link per impostare la password da Admin → Utenti.",
|
"it": "chiedi a un amministratore dell'azienda di riemettere un link per impostare la password a questa email, oppure di revocare questo invito e inviarne uno nuovo all'email con cui accedi.",
|
||||||
"pt": "se o seu e-mail real de início de sessão mudou (desvio de e-mail), peça a um administrador da empresa para revogar este convite e enviar um novo para o e-mail que utiliza para iniciar sessão. Os administradores da plataforma também podem reemitir links de definição de palavra-passe em Admin → Utilizadores.",
|
"pt": "peça a um administrador da empresa para reemitir um link de definição de palavra-passe para este e-mail, ou para revogar este convite e enviar um novo para o e-mail que utiliza para iniciar sessão.",
|
||||||
"nl": "als uw echte login-e-mail is gewijzigd (e-maildrift), vraag dan een bedrijfsbeheerder om deze uitnodiging in te trekken en een nieuwe te sturen naar het e-mailadres waarmee u inlogt. Platformbeheerders kunnen ook set-wachtwoordlinks opnieuw uitgeven via Admin → Gebruikers.",
|
"nl": "vraag een bedrijfsbeheerder om een set-wachtwoordlink opnieuw uit te geven naar dit e-mailadres, of om deze uitnodiging in te trekken en een nieuwe te sturen naar het e-mailadres waarmee u inlogt.",
|
||||||
"pl": "jeśli zmienił się Twój prawdziwy e-mail logowania (dryf e-mail), poproś administratora firmy o unieważnienie tego zaproszenia i wysłanie nowego na e-mail używany do logowania. Administratorzy platformy mogą też ponownie wystawiać linki ustawienia hasła w Admin → Użytkownicy.",
|
"pl": "poproś administratora firmy o ponowne wystawienie linku do ustawienia hasła na ten e-mail albo o unieważnienie tego zaproszenia i wysłanie nowego na e-mail używany do logowania.",
|
||||||
"ja": "実際のログイン用メールが変わった場合(メール変更)、会社の管理者にこの招待の取り消しと、ログインに使うメールへの新しい招待送信を依頼してください。プラットフォーム管理者は「管理 → ユーザー」からパスワード設定リンクも再発行できます。"
|
"ja": "会社の管理者にこのメール向けのパスワード設定リンクの再発行を依頼するか、この招待を取り消してログインに使うメールへ新しい招待を送るよう依頼してください。"
|
||||||
},
|
},
|
||||||
"You don't have permission to open company settings. Ask a company admin for help.": {
|
"You don't have permission to open company settings. Ask a company admin for help.": {
|
||||||
"es": "No tienes permiso para abrir la configuración de la empresa. Pide ayuda a un administrador de la empresa.",
|
"es": "No tienes permiso para abrir la configuración de la empresa. Pide ayuda a un administrador de la empresa.",
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
|
|||||||
synonyms: ["ean", "items[].ean", "products/process"],
|
synonyms: ["ean", "items[].ean", "products/process"],
|
||||||
title: "Public process (EAN batch)",
|
title: "Public process (EAN batch)",
|
||||||
outcome: "Start and poll enrichment using POST/GET /products/process.",
|
outcome: "Start and poll enrichment using POST/GET /products/process.",
|
||||||
body: "POST body uses items[].ean (preferred). Handler also accepts raw_product_ids as an alternate on this same path. Responses use HTTP 200 with { data: { process_id, … } } and completed items[] when done.",
|
body: "POST body uses items[].ean (preferred). Responses use HTTP 200 with { data: { process_id, … } }. Poll GET until COMPLETED. items[].description is generated HTML product copy (headings, paragraphs, lists) whose length follows the category description formula — not a one- or two-sentence blurb. Some categories request very long multi-section articles. Handler also accepts raw_product_ids as an alternate on this same path.",
|
||||||
authNote: AUTH_NOTE,
|
authNote: AUTH_NOTE,
|
||||||
endpoints: [
|
endpoints: [
|
||||||
endpoint("POST", "/products/process", "Start processing by EAN"),
|
endpoint("POST", "/products/process", "Start processing by EAN"),
|
||||||
|
|||||||
Reference in New Issue
Block a user