@miravia_test/miravia-publish-skill 0.1.5 → 0.1.7
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/dist/SKILL.md +18 -9
- package/dist/VERSION +1 -1
- package/dist/scripts/extraction_engine.py +283 -0
- package/dist/scripts/i18n.py +44 -4
- package/dist/scripts/miravia_client.py +6 -0
- package/dist/scripts/product_client.py +21 -4
- package/dist/scripts/product_skill.py +38 -0
- package/dist/scripts/publish_module.py +579 -112
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -25,7 +25,7 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
|
|
|
25
25
|
| 文件 | 职责 |
|
|
26
26
|
|------|------|
|
|
27
27
|
| `scripts/publish_module.py` | 发布流程主控(类目/品牌/属性/图片/SKU/合规/确认/创建) |
|
|
28
|
-
| `scripts/extraction_engine.py` | 从源文本抽取 `MiraviaListingSchema
|
|
28
|
+
| `scripts/extraction_engine.py` | 从源文本抽取 `MiraviaListingSchema`;Excel/CSV 按表头规则抽取(无需 LLM key) |
|
|
29
29
|
| `scripts/file_reader.py` | 读取本地文件、文件夹或网页 URL |
|
|
30
30
|
| `scripts/field_definitions.py` | 商品字段定义与术语说明,供 LLM 抽取与提示使用 |
|
|
31
31
|
| `scripts/product_client.py` | Miravia 商品/图片/类目/品牌/合规 API 封装 |
|
|
@@ -71,11 +71,16 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
|
|
|
71
71
|
|
|
72
72
|
### 4.2 AI 抽取模式
|
|
73
73
|
|
|
74
|
+
> **⚠️ Agent 行为准则(必须遵守,禁止绕过)**
|
|
75
|
+
> 1. 卖家上传文件/文件夹/URL 后,Agent **必须**带 `--source` 调用本 Skill 完成读取与抽取,**禁止**自己读取/解析文件内容后改走手动模式(手动模式仅用于卖家直接以 CLI 参数逐项提供的场景)。
|
|
76
|
+
> 2. **Excel/CSV 源按表头规则抽取,完全不需要 LLM key**;只有 PDF/Word/图片/URL 等非表格源才需要 LLM。Agent **不得**以「未配置 LLM key、正则抽取靠不住」为由绕过 Skill——该前提对 Excel/CSV 不成立。
|
|
77
|
+
> 3. 上传文件后的**第一步固定为 CTB 确认**:先向卖家展示抽取到的**标题、品牌、类目预测**并等待确认(`need_ctb_confirm` / 必要时 `need_category` / `need_brand`)。**在 CTB 确认通过之前,禁止进入后续字段抽取与缺字段(EAN / GPSR / 合规等)校验提示**。CTB 确认之后才依次做关键字段与字段摘要检查。
|
|
78
|
+
|
|
74
79
|
0. **Stage 0 - 授权确认**:
|
|
75
80
|
- 首次操作时,Skill 返回 `status: need_authorization`,列出三项授权请求(读取卖家资料、访问商品数据、更新商品数据)。
|
|
76
81
|
- 卖家确认后,加 `--authorized` 重新运行命令,进入后续流程。
|
|
77
82
|
1. 读取 `--source` 指定的文件/文件夹/URL。
|
|
78
|
-
2.
|
|
83
|
+
2. 抽取 `MiraviaListingSchema`;可通过 `--output-schema` 仅查看抽取结果。**Excel/CSV 源按表头规则直接抽取,无需 LLM key**;PDF/Word/图片/URL 源使用 LLM 抽取(需配置 OPENAI_API_KEY 或 DASHSCOPE_API_KEY)。
|
|
79
84
|
3. **Stage 1 - 类目/标题/品牌确认(CTB)**:
|
|
80
85
|
- 解析 category / brand,返回候选供确认。
|
|
81
86
|
- 生成 `ctb_confirm_token`,返回 `status: need_ctb_confirm`。
|
|
@@ -84,7 +89,8 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
|
|
|
84
89
|
- `/seller/shipping_permission/get` 决定默认物流方式(DBS/DBM)。
|
|
85
90
|
- `/manufacturer/get`、`/eu/responsible/person/get` 匹配制造商/EU 负责人。
|
|
86
91
|
5. **Stage 3 - 关键字段检查**:
|
|
87
|
-
-
|
|
92
|
+
- **主图/SKU 图全空 → 硬阻断且不可转 Draft**(`status: need_mandatory_fields`,`draft_mode_available: false`):Miravia `/product/create` 要求至少一张图片,缺失会以 `4224 MAIN_IMAGE_OR_SKU_IMAGE_EMPTY` 被平台硬拒,草稿也绕不过。
|
|
93
|
+
- 缺价格/重量/长宽高等(接口判定必填时)→ 阻断,但允许 `--draft` 降级发布。
|
|
88
94
|
6. **Stage 4 - 字段摘要/草稿确认**:
|
|
89
95
|
- 汇总已抽取字段与缺失字段。
|
|
90
96
|
- 关键字段缺失时询问是否以 `--draft` 模式发布为 `inactive`。
|
|
@@ -103,8 +109,8 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
|
|
|
103
109
|
| `need_category` | 类目未找到、类目 ID 无效、推荐多候选或预测失败 | 让用户提供正确的类目 ID(`--category-id`)或从候选中选择一个(`--confirmed-category-id`),**禁止重复搜索同一关键词**;所有 ID 均校验类目树;关键词搜索失败后调 `/product/category/recommend/get` 推荐,Top1 自动选中,TopN 展示候选,空结果展示全量类目树 |
|
|
104
110
|
| `need_brand` | 品牌未找到或存在多个候选 | 让用户提供正确的品牌名(`--brand-name`)或从候选中选择一个(`--confirmed-brand-id`),**禁止重复搜索同一关键词** |
|
|
105
111
|
| `need_ctb_confirm` | 需要确认类目/标题/品牌 | 回传 `ctb_confirm_token` |
|
|
106
|
-
| `need_brand_create` | 品牌未找到,需去 Seller Center
|
|
107
|
-
| `need_mandatory_fields` |
|
|
112
|
+
| `need_brand_create` | 品牌未找到,需去 Seller Center 处理 | 先引导查看已有品牌及审核状态(https://sellercenter.miravia.es/apps/setting/seller/brandlist,可能已在审核中),确无再创建(https://sellercenter.miravia.es/apps/product/publish/brand);创建后重新运行命令 |
|
|
113
|
+
| `need_mandatory_fields` | 缺少必填字段;**图片缺失为硬阻断(不可 Draft)**,价格/重量/尺寸缺失可 `--draft` | 补充字段后重跑 |
|
|
108
114
|
| `need_category_attributes` | 类目必填属性缺失(AI抽取模式) | 通过 `--attribute-data` 补充或使用 `--draft` |
|
|
109
115
|
| `need_field_summary` | 需要确认字段摘要或选择草稿模式 | 回传 `field_summary_token`,可加 `--draft` |
|
|
110
116
|
| `need_compliance` | 制造商/EU 负责人匹配失败 | 按返回候选选择或补充信息后重跑 |
|
|
@@ -119,12 +125,14 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
|
|
|
119
125
|
| `need_images` | 部分图片迁移失败 | 替换图片后重跑 |
|
|
120
126
|
| `unsupported_format` | 上传了不支持的文件格式(zip/txt/psd 等) | 提示仅支持 PDF/Word/Excel/Image/URL,允许重新上传 |
|
|
121
127
|
| `file_empty_or_corrupted` | 文件内容为空或损坏(0KB PDF / 乱码 Word) | 提示文件内容为空或损坏,允许重新上传 |
|
|
122
|
-
| `llm_not_configured` |
|
|
128
|
+
| `llm_not_configured` | 非表格源(PDF/Word/图片/URL)走 LLM 抽取时未配置 LLM API key(OPENAI_API_KEY / DASHSCOPE_API_KEY);Excel/CSV 源不会触发此状态 | 提示配置 key 后带 --source 重跑,或改用 Excel/CSV 上传;不要绕过确认流程改走手动模式 |
|
|
123
129
|
| `invalid_url` | 输入的 URL 无法抓取 | 提示检查 URL 后重试 |
|
|
124
130
|
| `title_invalid` | 商品标题不符合规范(过短/过长/词数不足) | 提示具体错误 + 标题格式建议(Brand+Category+Model+Features+Scenario),阻断后续流程,禁止转 Draft |
|
|
125
131
|
| `need_category_permission` | 卖家无该类目发布权限 | 询问是否以 Draft 模式发布(待审批通过后自动激活),提供类目权限申请链接 https://sellercenter.miravia.es/apps/setting/category;或更换类目后重跑 |
|
|
126
132
|
| `preview` | 最终发布预览 | 回传 `publish_preview_token` |
|
|
127
|
-
| `success` |
|
|
133
|
+
| `success` | 创建成功(必须返回 `item_id`,无 `item_id` 一律视为失败) | 返回 `item_id`、`pdp_url`、审核提示 |
|
|
134
|
+
| `draft_created_with_issues` | 草稿创建成功(接口返回了 `item_id`)但必填字段/权限待补全 | 在 Seller Center 草稿中补全字段后激活,禁止当作完全成功 |
|
|
135
|
+
| `create_failed` | `/product/create` 失败或未返回 `item_id`(含 4234 无 `item_id` 的场景,草稿并未创建) | 展示错误码/message/trace_id 定位根因,修复后重跑;重跑前先在 Seller Center 确认是否已有重复草稿 |
|
|
128
136
|
| `brand_permission_failed` | 品牌权限不足/待审批 (BRAND_PENDING / BRAND_PERMISSION_DENIED) | 加 `--draft` 发布为草稿,或在 Seller Center 申请品牌权限后重跑 |
|
|
129
137
|
|
|
130
138
|
---
|
|
@@ -134,11 +142,11 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
|
|
|
134
142
|
| ID | 规则 | 说明 |
|
|
135
143
|
|----|------|------|
|
|
136
144
|
| P1 | **sellerId 自动注入** | 从开放平台鉴权上下文读取,禁止用户传入。 |
|
|
137
|
-
| P2 |
|
|
145
|
+
| P2 | **必填判定统一入口** | 所有必填/条件必填检查都走同一规则(`_field_is_required`):**我们的规则优先命中→否则以 `/category/attributes/get` 的 `is_mandatory` 为准**;接口未返回该属性等同选填。创建前必须先确定叶子类目并拉取类目属性。 |
|
|
138
146
|
| P3 | **图片先迁移** | 外部图片 URL 必须经 `/image/migrate` 迁移到 Miravia CDN。 |
|
|
139
147
|
| P4 | **三阶段确认** | CTB 确认 → 字段摘要确认 → 最终发布预览确认。 |
|
|
140
148
|
| P5 | **pre-check 集中** | 品牌/类目权限等校验统一在 `/product/batchValidate` 提交前完成。 |
|
|
141
|
-
| P6 | **必填项阻断** |
|
|
149
|
+
| P6 | **必填项阻断** | 仅结构核心字段(图片、常规价格、库存、标题、类目、品牌、物流)恒定阻断;重量/长宽高为 SKU 级类目属性,仅当接口标 `is_mandatory=1` 时阻断;Seller SKU 非必填(空时接口自动生成)。batchValidate 为最终权威兜底。 |
|
|
142
150
|
| P7 | **草稿模式** | 关键字段缺失时,商家可选择 `--draft` 发布为 `inactive`。 |
|
|
143
151
|
| P8 | **品牌创建后重跑** | 品牌未找到时不自动轮询,给出 Seller Center 创建链接,创建完成后商家重跑命令。 |
|
|
144
152
|
| P9 | **默认物流** | 通过 `/seller/shipping_permission/get` 的 `DBS_permission` 决定默认 `DBS` 或 `DBM`。 |
|
|
@@ -146,6 +154,7 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
|
|
|
146
154
|
| P11 | **API 重试** | 每个 API 调用最多重试 3 次。 |
|
|
147
155
|
| P12 | **埋点与审计** | 记录 Skill 调用与商品操作审计日志。 |
|
|
148
156
|
| P13 | **创建前必须确认** | AI Agent **禁止**在收集完信息后直接调用 `/product/create`。必须先向卖家展示完整字段摘要(标题、类目、品牌、价格、SKU、包装、图片、GPSR、Safety warning 等所有已填字段),明确标注缺失字段,等卖家回复「确认」后才能执行创建。无论手动模式还是 AI 抽取模式均适用。 |
|
|
157
|
+
| P14 | **文件上传必走 Skill + CTB 优先** | 卖家上传文件/URL 后,Agent 必须带 `--source` 调用 Skill,禁止自己读文件转手动模式;Excel/CSV 无需 LLM key。抽取后第一步固定为 CTB(标题/品牌/类目)确认,CTB 确认前不得提示 EAN/GPSR 等缺字段。 |
|
|
149
158
|
|
|
150
159
|
---
|
|
151
160
|
|
package/dist/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.7
|
|
@@ -87,6 +87,12 @@ class MiraviaListingSchema:
|
|
|
87
87
|
|
|
88
88
|
raw_skus: List[Dict[str, Any]] = field(default_factory=list)
|
|
89
89
|
|
|
90
|
+
# Excel/CSV columns whose header did not match any known field in
|
|
91
|
+
# _TABLE_FIELD_SYNONYMS. Kept verbatim (header -> value) so the publish
|
|
92
|
+
# flow can bind them against the category attribute definitions returned
|
|
93
|
+
# by /category/attributes/get instead of silently dropping them.
|
|
94
|
+
unmatched_columns: Dict[str, str] = field(default_factory=dict)
|
|
95
|
+
|
|
90
96
|
def to_dict(self) -> Dict[str, Any]:
|
|
91
97
|
return asdict(self)
|
|
92
98
|
|
|
@@ -336,3 +342,280 @@ def validate_title(title: Optional[str]) -> Optional[str]:
|
|
|
336
342
|
if len(title.split()) < 5:
|
|
337
343
|
return "Title should contain at least 5 words"
|
|
338
344
|
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
# Rule-based table extraction (Excel/CSV) — no LLM key required
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# Header synonym map: schema field -> candidate header spellings (zh/en/es).
|
|
353
|
+
# Headers are normalized (lowercase, spaces/punctuation stripped) before
|
|
354
|
+
# matching; each field may bind to multiple columns (e.g. several image
|
|
355
|
+
# columns all feed product_images).
|
|
356
|
+
_TABLE_FIELD_SYNONYMS: Dict[str, tuple] = {
|
|
357
|
+
# Keep more specific fields BEFORE generic ones (matching runs
|
|
358
|
+
# exact -> prefix -> substring, but dict order breaks exact ties).
|
|
359
|
+
"discounted_price": ("折扣价", "活动价", "促销价", "折后价", "specialprice", "discountprice", "saleprice", "preciodescuento", "preciorebajado"),
|
|
360
|
+
"general_price": ("价格", "售价", "原价", "价格€", "price", "generalprice", "precio"),
|
|
361
|
+
"product_title": ("标题", "商品标题", "产品名称", "品名", "title", "producttitle", "productname", "titulo", "nombre"),
|
|
362
|
+
"brand_name": ("品牌", "brand", "brandname", "marca"),
|
|
363
|
+
"category_name": ("类目", "类目名", "叶子类目", "category", "categoryname", "categoria"),
|
|
364
|
+
"category_id": ("类目id", "类目id", "categoryid", "categoryid"),
|
|
365
|
+
"product_long_description": ("描述", "详情", "商品描述", "description", "descripcion"),
|
|
366
|
+
"product_images": ("图片", "主图", "细节图", "image", "images", "imageurl", "picture", "photo", "imagen", "imagenes", "foto"),
|
|
367
|
+
"seller_sku": ("sellersku", "sku", "商家编码", "商品编码", "skucode", "reference", "referencia"),
|
|
368
|
+
"ean_code": ("ean", "eancode", "条码", "条形码", "barcode", "codigodebarras"),
|
|
369
|
+
"stock": ("库存", "数量", "stock", "quantity", "qty", "inventario", "cantidad"),
|
|
370
|
+
"weight_kg": ("重量", "包裹重量", "weight", "peso"),
|
|
371
|
+
"length_cm": ("长度", "长cm", "长", "length", "largura"),
|
|
372
|
+
"width_cm": ("宽度", "宽cm", "宽", "width", "ancho"),
|
|
373
|
+
"height_cm": ("高度", "高cm", "高", "height", "altura"),
|
|
374
|
+
"manufacturer_name": ("制造商", "厂家", "manufacturer", "fabricante"),
|
|
375
|
+
"eu_responsible_person_name": ("欧盟负责人", "欧代", "euresponsibleperson", "eurp"),
|
|
376
|
+
"variation_1": ("颜色", "color", "colour", "variation1", "variation", "款式"),
|
|
377
|
+
"variation_2": ("尺寸", "尺码", "size", "talla", "variation2"),
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
# Fields that describe the product as a whole (first non-empty row wins);
|
|
381
|
+
# everything else is SKU-level and collected per row into raw_skus.
|
|
382
|
+
_PRODUCT_LEVEL_FIELDS = {
|
|
383
|
+
"product_title",
|
|
384
|
+
"brand_name",
|
|
385
|
+
"category_name",
|
|
386
|
+
"category_id",
|
|
387
|
+
"product_long_description",
|
|
388
|
+
"weight_kg",
|
|
389
|
+
"length_cm",
|
|
390
|
+
"width_cm",
|
|
391
|
+
"height_cm",
|
|
392
|
+
"manufacturer_name",
|
|
393
|
+
"eu_responsible_person_name",
|
|
394
|
+
}
|
|
395
|
+
# Row-level columns whose header name doubles as the variation dimension name.
|
|
396
|
+
_VARIATION_SOURCE_FIELDS = {"variation_1": "variation_name_1", "variation_2": "variation_name_2"}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _norm_header(header: Any) -> str:
|
|
400
|
+
"""Normalize a table header for synonym matching."""
|
|
401
|
+
return re.sub(r"[\s_\-—·()()【】\[\].,,、::]+", "", str(header)).lower()
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# Overly generic single-word synonyms. These are allowed to match a header by
|
|
405
|
+
# equality or as a prefix (e.g. "Image", "Product name") but must NOT match by
|
|
406
|
+
# substring, otherwise a compound header such as "Package/Label Photo(EU)"
|
|
407
|
+
# (normalized "packagelabelphotoeu") is greedily absorbed by "photo" and
|
|
408
|
+
# reported as a main product image instead of binding to its real category
|
|
409
|
+
# attribute Package_Label_Photo_EU.
|
|
410
|
+
_GENERIC_SUBSTRING_SYNONYMS = frozenset({
|
|
411
|
+
"image", "images", "photo", "photos", "picture", "pictures",
|
|
412
|
+
"name", "names", "size", "color", "colour", "talla", "nombre",
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _match_table_field(header_norm: str) -> Optional[str]:
|
|
417
|
+
"""Resolve a normalized header to a schema field via 3-pass matching
|
|
418
|
+
(exact -> prefix -> substring) to keep specific fields from being
|
|
419
|
+
shadowed by generic synonyms (e.g. "special price" vs "price")."""
|
|
420
|
+
for pass_kind in ("exact", "prefix", "contains"):
|
|
421
|
+
for field_name, synonyms in _TABLE_FIELD_SYNONYMS.items():
|
|
422
|
+
for syn in synonyms:
|
|
423
|
+
if pass_kind == "exact" and header_norm == syn:
|
|
424
|
+
return field_name
|
|
425
|
+
if pass_kind == "prefix" and header_norm.startswith(syn):
|
|
426
|
+
return field_name
|
|
427
|
+
if (
|
|
428
|
+
pass_kind == "contains"
|
|
429
|
+
and syn not in _GENERIC_SUBSTRING_SYNONYMS
|
|
430
|
+
and syn in header_norm
|
|
431
|
+
):
|
|
432
|
+
return field_name
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _clean_numeric(value: Any) -> Optional[str]:
|
|
437
|
+
"""Extract a plain number string from a cell (handles €10,99 / 10.99 / 10)."""
|
|
438
|
+
if value is None:
|
|
439
|
+
return None
|
|
440
|
+
s = str(value).strip()
|
|
441
|
+
if not s:
|
|
442
|
+
return None
|
|
443
|
+
m = re.search(r"(\d+(?:[.,]\d+)?)", s)
|
|
444
|
+
if not m:
|
|
445
|
+
return None
|
|
446
|
+
return m.group(1).replace(",", ".")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _clean_cell(value: Any) -> Optional[str]:
|
|
450
|
+
"""Non-empty string view of a cell."""
|
|
451
|
+
if value is None:
|
|
452
|
+
return None
|
|
453
|
+
s = str(value).strip()
|
|
454
|
+
return s or None
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _extract_image_urls(value: Any) -> List[str]:
|
|
458
|
+
"""Collect http(s) image URLs from a cell (may hold several)."""
|
|
459
|
+
if value is None:
|
|
460
|
+
return []
|
|
461
|
+
return re.findall(r"https?://[^\s,;]+", str(value), re.IGNORECASE)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def extract_schema_from_tables(content_items: List[Dict[str, Any]]) -> Optional[MiraviaListingSchema]:
|
|
465
|
+
"""Extract a listing schema from structured Excel/CSV tables by rule.
|
|
466
|
+
|
|
467
|
+
Uses header synonyms instead of an LLM, so sellers never need an LLM
|
|
468
|
+
API key for the mainstream Excel/CSV flow. Returns None when no table
|
|
469
|
+
data is present or nothing useful was matched, letting the caller fall
|
|
470
|
+
back to LLM extraction for non-structured sources.
|
|
471
|
+
"""
|
|
472
|
+
tables: List[Dict[str, Any]] = []
|
|
473
|
+
for item in content_items or []:
|
|
474
|
+
if item.get("type") != "table":
|
|
475
|
+
continue
|
|
476
|
+
content = item.get("content")
|
|
477
|
+
if not content:
|
|
478
|
+
continue
|
|
479
|
+
for table in (content if isinstance(content, list) else [content]):
|
|
480
|
+
if isinstance(table, dict) and table.get("rows"):
|
|
481
|
+
tables.append(table)
|
|
482
|
+
|
|
483
|
+
if not tables:
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
schema = MiraviaListingSchema()
|
|
487
|
+
# column plans: field -> (table_index, header) pairs, preserving order
|
|
488
|
+
column_plan: Dict[str, List[tuple]] = {}
|
|
489
|
+
# Headers that matched no known field, kept in order for later binding.
|
|
490
|
+
unmatched_headers: List[str] = []
|
|
491
|
+
seen_unmatched: set = set()
|
|
492
|
+
|
|
493
|
+
for t_idx, table in enumerate(tables):
|
|
494
|
+
for header in table.get("headers", []):
|
|
495
|
+
field_name = _match_table_field(_norm_header(header))
|
|
496
|
+
if field_name:
|
|
497
|
+
column_plan.setdefault(field_name, []).append((t_idx, header))
|
|
498
|
+
else:
|
|
499
|
+
hkey = _clean_cell(header)
|
|
500
|
+
if hkey and hkey not in seen_unmatched:
|
|
501
|
+
seen_unmatched.add(hkey)
|
|
502
|
+
unmatched_headers.append(hkey)
|
|
503
|
+
|
|
504
|
+
if not column_plan:
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
all_rows: List[tuple] = [] # (t_idx, row_dict)
|
|
508
|
+
for t_idx, table in enumerate(tables):
|
|
509
|
+
for row in table.get("rows", []):
|
|
510
|
+
if isinstance(row, dict):
|
|
511
|
+
all_rows.append((t_idx, row))
|
|
512
|
+
if not all_rows:
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
def first_non_empty(field_name: str) -> Optional[str]:
|
|
516
|
+
for t_idx, header in column_plan.get(field_name, []):
|
|
517
|
+
for r_idx, row in all_rows:
|
|
518
|
+
if r_idx == t_idx:
|
|
519
|
+
value = _clean_cell(row.get(header))
|
|
520
|
+
if value:
|
|
521
|
+
return value
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
# Product-level fields: first non-empty cell wins.
|
|
525
|
+
for field_name in _PRODUCT_LEVEL_FIELDS:
|
|
526
|
+
value = first_non_empty(field_name)
|
|
527
|
+
if value:
|
|
528
|
+
setattr(schema, field_name, value)
|
|
529
|
+
|
|
530
|
+
# Price/stock need plain numbers. Weight/dimensions keep their original
|
|
531
|
+
# string (e.g. "250g", "15cm"): the caller's _convert_weight/_convert_length
|
|
532
|
+
# normalize units, and stripping the unit here would misread grams as kg.
|
|
533
|
+
for field_name in ("general_price", "discounted_price", "stock"):
|
|
534
|
+
value = _clean_numeric(first_non_empty(field_name))
|
|
535
|
+
if value:
|
|
536
|
+
setattr(schema, field_name, value)
|
|
537
|
+
|
|
538
|
+
# Category/brand IDs keep raw values when present.
|
|
539
|
+
cat_id = _clean_cell(first_non_empty("category_id"))
|
|
540
|
+
if cat_id and str(cat_id).isdigit():
|
|
541
|
+
schema.category_id = str(cat_id)
|
|
542
|
+
|
|
543
|
+
# Images: product-level union of all image columns across rows.
|
|
544
|
+
seen_urls: set = set()
|
|
545
|
+
for t_idx, header in column_plan.get("product_images", []):
|
|
546
|
+
for r_idx, row in all_rows:
|
|
547
|
+
if r_idx == t_idx:
|
|
548
|
+
for url in _extract_image_urls(row.get(header)):
|
|
549
|
+
if url not in seen_urls:
|
|
550
|
+
seen_urls.add(url)
|
|
551
|
+
schema.product_images.append(url)
|
|
552
|
+
|
|
553
|
+
# Variations: dimension name from header, options from distinct row values.
|
|
554
|
+
for src_field, name_field in _VARIATION_SOURCE_FIELDS.items():
|
|
555
|
+
if src_field not in column_plan:
|
|
556
|
+
continue
|
|
557
|
+
first_t_idx, first_header = column_plan[src_field][0]
|
|
558
|
+
dim_name = _clean_cell(first_header)
|
|
559
|
+
options: List[str] = []
|
|
560
|
+
for t_idx, header in column_plan[src_field]:
|
|
561
|
+
for r_idx, row in all_rows:
|
|
562
|
+
if r_idx == t_idx:
|
|
563
|
+
value = _clean_cell(row.get(header))
|
|
564
|
+
if value and value not in options:
|
|
565
|
+
options.append(value)
|
|
566
|
+
if options:
|
|
567
|
+
setattr(schema, name_field, dim_name)
|
|
568
|
+
if name_field == "variation_name_1":
|
|
569
|
+
schema.variation_options_1 = options
|
|
570
|
+
else:
|
|
571
|
+
schema.variation_options_2 = options
|
|
572
|
+
|
|
573
|
+
# SKU rows: one raw_skus entry per data row.
|
|
574
|
+
for t_idx, row in all_rows:
|
|
575
|
+
sku: Dict[str, Any] = {}
|
|
576
|
+
for field_name, sku_key in (
|
|
577
|
+
("seller_sku", "seller_sku"),
|
|
578
|
+
("ean_code", "ean_code"),
|
|
579
|
+
("variation_1", "variation_1"),
|
|
580
|
+
("variation_2", "variation_2"),
|
|
581
|
+
):
|
|
582
|
+
for c_t_idx, header in column_plan.get(field_name, []):
|
|
583
|
+
if c_t_idx == t_idx:
|
|
584
|
+
value = _clean_cell(row.get(header))
|
|
585
|
+
if value:
|
|
586
|
+
sku[sku_key] = value
|
|
587
|
+
break
|
|
588
|
+
for field_name, sku_key in (("general_price", "price"), ("discounted_price", "special_price"), ("stock", "quantity")):
|
|
589
|
+
for c_t_idx, header in column_plan.get(field_name, []):
|
|
590
|
+
if c_t_idx == t_idx:
|
|
591
|
+
value = _clean_numeric(row.get(header))
|
|
592
|
+
if value:
|
|
593
|
+
sku[sku_key] = value
|
|
594
|
+
break
|
|
595
|
+
sku_images: List[str] = []
|
|
596
|
+
for c_t_idx, header in column_plan.get("product_images", []):
|
|
597
|
+
if c_t_idx == t_idx:
|
|
598
|
+
sku_images.extend(_extract_image_urls(row.get(header)))
|
|
599
|
+
if sku_images:
|
|
600
|
+
sku["sku_images"] = sku_images
|
|
601
|
+
if sku:
|
|
602
|
+
schema.raw_skus.append(sku)
|
|
603
|
+
|
|
604
|
+
# Keep every non-whitelisted column (first non-empty value) so the publish
|
|
605
|
+
# flow can bind it against the category attribute definitions instead of
|
|
606
|
+
# dropping it. Keyed by the original header text.
|
|
607
|
+
for header in unmatched_headers:
|
|
608
|
+
for t_idx, row in all_rows:
|
|
609
|
+
value = _clean_cell(row.get(header))
|
|
610
|
+
if value:
|
|
611
|
+
schema.unmatched_columns[header] = value
|
|
612
|
+
break
|
|
613
|
+
|
|
614
|
+
# Only claim success when something meaningful was extracted; otherwise
|
|
615
|
+
# the caller falls back to LLM extraction.
|
|
616
|
+
if schema.product_title or schema.general_price or schema.raw_skus:
|
|
617
|
+
# Mirror single-SKU price/stock to top-level convenience fields.
|
|
618
|
+
if not schema.general_price and schema.raw_skus:
|
|
619
|
+
schema.general_price = schema.raw_skus[0].get("price")
|
|
620
|
+
return schema
|
|
621
|
+
return None
|
package/dist/scripts/i18n.py
CHANGED
|
@@ -172,14 +172,21 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
172
172
|
"pub_unsupported_format": "Unsupported file format: {source}. Only PDF, Word, Excel, Image, and URL are supported.",
|
|
173
173
|
"pub_invalid_url": "Failed to fetch URL: {source}. Please check the URL and try again.",
|
|
174
174
|
"pub_url_fetch_failed": "Failed to fetch content from {source}: {error}. Please check the URL and try again.",
|
|
175
|
-
"pub_llm_not_configured": "
|
|
176
|
-
"pub_llm_not_configured_next": "
|
|
175
|
+
"pub_llm_not_configured": "LLM extraction is unavailable: no LLM API key is configured (OPENAI_API_KEY or DASHSCOPE_API_KEY). This only applies to non-table sources (PDF/Word/image/URL); Excel/CSV sources are extracted by header rules and need no LLM key.",
|
|
176
|
+
"pub_llm_not_configured_next": "If the source is Excel/CSV, just rerun with --source (no key needed). For PDF/Word/image/URL, configure the LLM API key and rerun with --source. Do NOT read the file yourself or switch to manual mode; the first step must be CTB (title/brand/category) confirmation.",
|
|
177
177
|
"pub_create_requires_category": "create_product requires --category-id (leaf category)",
|
|
178
178
|
"pub_create_requires_name": "create_product requires --product-name",
|
|
179
179
|
"pub_create_preview_message": "Please review the product creation details below and confirm before I create the product.",
|
|
180
180
|
"pub_create_preview_next": "If the details are correct, rerun with --publish-preview-token {token} to execute.",
|
|
181
181
|
"pub_create_success": "Product created successfully.",
|
|
182
182
|
"pub_create_success_next": "Ask the seller if they need to view the created product or make further edits.",
|
|
183
|
+
"pub_draft_created_with_issues": "Draft created, but some required fields or authorizations are missing. Complete them in Seller Center to activate the product.",
|
|
184
|
+
"pub_draft_update_hint": "Fill in the missing fields in Seller Center draft list, then activate the product.",
|
|
185
|
+
"pub_create_failed_title": "Product creation failed",
|
|
186
|
+
"pub_create_failed_message": "Product was NOT created (including as draft): error code {code}, reason: {reason}.",
|
|
187
|
+
"pub_create_failed_no_item_id": "The create API returned success but no item_id, so the product was not actually created.",
|
|
188
|
+
"pub_create_failed_check_and_rerun": "Check whether a duplicate draft already exists in Seller Center, fix the reported issue, then rerun.",
|
|
189
|
+
"pub_create_failed_trace_hint": "When contacting platform support, provide trace_id {trace_id} / request_id {request_id} to locate the root cause.",
|
|
183
190
|
"pub_title_too_short": "Product title is too short (minimum {min} characters, got {actual}).",
|
|
184
191
|
"pub_title_too_long": "Product title is too long (maximum {max} characters, got {actual}).",
|
|
185
192
|
"pub_title_too_few_words": "Product title must have at least {min} words (got {actual}).",
|
|
@@ -234,6 +241,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
234
241
|
"pub_summary_conditional_section": "[Conditional Fields]",
|
|
235
242
|
"pub_field_status_filled": "Filled",
|
|
236
243
|
"pub_field_status_missing": "MISSING",
|
|
244
|
+
"pub_field_status_optional": "Optional",
|
|
237
245
|
"pub_mandatory_missing_block": "Cannot publish: missing mandatory fields [{fields}]. Please fill them and rerun.",
|
|
238
246
|
"pub_conditional_missing_draft": "Missing conditional fields [{fields}]. The product will be saved as Inactive (Draft) and auto-activated once completed. Confirm to proceed?",
|
|
239
247
|
# DBM/DBS auto-switch
|
|
@@ -302,6 +310,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
302
310
|
"pub_ctb_confirm_next": "If correct, rerun with --ctb-confirm-token {token} to continue.",
|
|
303
311
|
"pub_brand_not_found_title": "Brand not found",
|
|
304
312
|
"pub_brand_not_found_message": "No brand matching \"{brand_name}\" was found in Miravia. Would you like to create a new brand?",
|
|
313
|
+
"pub_brand_list_link": "Check your existing brands and their approval status here: https://sellercenter.miravia.es/apps/setting/seller/brandlist",
|
|
305
314
|
"pub_brand_create_link": "Create a new brand here: https://sellercenter.miravia.es/apps/product/publish/brand",
|
|
306
315
|
"pub_brand_create_next": "After creating the brand in Seller Center, rerun this command with --brand-name <brand_name> or --confirmed-brand-id <brand_id>.",
|
|
307
316
|
"pub_brand_reconfirm_or_choose": "If you do not want to create a brand, please re-enter the brand name or choose one from the matched list below.",
|
|
@@ -310,7 +319,10 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
310
319
|
"pub_field_summary_description": "Review which fields have been filled and which are still missing.",
|
|
311
320
|
"pub_field_status_filled": "filled",
|
|
312
321
|
"pub_field_status_missing": "missing",
|
|
322
|
+
"pub_field_status_optional": "optional",
|
|
313
323
|
"pub_mandatory_fields_missing": "The following required fields are missing and must be provided before continuing: {fields}",
|
|
324
|
+
"pub_image_required_no_draft": "At least one image (main image or a SKU image) is required. This is a Miravia platform requirement that cannot be bypassed even by publishing as Draft; missing: {fields}.",
|
|
325
|
+
"pub_add_images_and_rerun": "Add at least one product image (URL or local file) and rerun the command.",
|
|
314
326
|
"pub_key_fields_missing_draft_question": "Some key fields are missing. You can publish as a draft and complete them later. To publish as draft, rerun with --draft.",
|
|
315
327
|
"pub_manufacturer_eurp_mismatch": "The manufacturer / EU responsible person does not match an approved record. Please choose a valid one or publish as draft.",
|
|
316
328
|
"pub_seller_sku_recommended": "Seller SKU is strongly recommended as the internal identifier for this SKU. Please provide one if available.",
|
|
@@ -473,6 +485,13 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
473
485
|
"pub_create_preview_next": "Si los detalles son correctos, vuelve a ejecutar con --publish-preview-token {token}.",
|
|
474
486
|
"pub_create_success": "Producto creado correctamente.",
|
|
475
487
|
"pub_create_success_next": "Pregunta al vendedor si necesita ver el producto creado o hacer más ediciones.",
|
|
488
|
+
"pub_draft_created_with_issues": "Borrador creado, pero faltan campos obligatorios o autorizaciones. Complételos en Seller Center para activar el producto.",
|
|
489
|
+
"pub_draft_update_hint": "Complete los campos faltantes en la lista de borradores de Seller Center y active el producto.",
|
|
490
|
+
"pub_create_failed_title": "Error al crear el producto",
|
|
491
|
+
"pub_create_failed_message": "El producto NO se creó (tampoco como borrador): código {code}, motivo: {reason}.",
|
|
492
|
+
"pub_create_failed_no_item_id": "La API de creación devolvió éxito pero no incluyó item_id, por lo que el producto no se creó.",
|
|
493
|
+
"pub_create_failed_check_and_rerun": "Compruebe en Seller Center si ya existe un borrador duplicado, corrija el problema y vuelva a ejecutar.",
|
|
494
|
+
"pub_create_failed_trace_hint": "Al contactar con el soporte, indique trace_id {trace_id} / request_id {request_id}.",
|
|
476
495
|
"pub_title_too_short": "El título del producto es demasiado corto (mínimo {min} caracteres, tiene {actual}).",
|
|
477
496
|
"pub_title_too_long": "El título del producto es demasiado largo (máximo {max} caracteres, tiene {actual}).",
|
|
478
497
|
"pub_title_too_few_words": "El título del producto debe tener al menos {min} palabras (tiene {actual}).",
|
|
@@ -496,6 +515,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
496
515
|
"pub_need_attributes_field_item": "{idx}. {label} ({en_name}) - {input_type}, {mandatory}",
|
|
497
516
|
"pub_attribute_data_next": "Vuelve a ejecutar con --attribute-data '<json>' para proporcionar los valores de atributos.",
|
|
498
517
|
"pub_image_migration_partial": "Algunas imágenes no se migraron: {failed}. Reemplázalas o vuelve a ejecutar sin esas URLs.",
|
|
518
|
+
"pub_field_status_optional": "Opcional",
|
|
499
519
|
},
|
|
500
520
|
"it": {
|
|
501
521
|
"greeting": "Cosa vorresti fare con la skill di inventario? Rispondi con la lettera dell'opzione o descrivi la tua richiesta:",
|
|
@@ -640,6 +660,13 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
640
660
|
"pub_create_preview_next": "Se i dettagli sono corretti, riesegui con --publish-preview-token {token}.",
|
|
641
661
|
"pub_create_success": "Prodotto creato con successo.",
|
|
642
662
|
"pub_create_success_next": "Chiedi al venditore se ha bisogno di visualizzare il prodotto creato o apportare modifiche.",
|
|
663
|
+
"pub_draft_created_with_issues": "Bozza creata, ma mancano campi obbligatori o autorizzazioni. Completali in Seller Center per attivare il prodotto.",
|
|
664
|
+
"pub_draft_update_hint": "Compila i campi mancanti nell'elenco bozze di Seller Center e attiva il prodotto.",
|
|
665
|
+
"pub_create_failed_title": "Creazione del prodotto non riuscita",
|
|
666
|
+
"pub_create_failed_message": "Il prodotto NON è stato creato (nemmeno come bozza): codice {code}, motivo: {reason}.",
|
|
667
|
+
"pub_create_failed_no_item_id": "L'API di creazione ha restituito successo ma senza item_id; il prodotto non è stato creato.",
|
|
668
|
+
"pub_create_failed_check_and_rerun": "Verifica in Seller Center se esiste già una bozza duplicata, correggi il problema e riesegui.",
|
|
669
|
+
"pub_create_failed_trace_hint": "Contattando il supporto, indica trace_id {trace_id} / request_id {request_id}.",
|
|
643
670
|
"pub_title_too_short": "Il titolo del prodotto è troppo corto (minimo {min} caratteri, ha {actual}).",
|
|
644
671
|
"pub_title_too_long": "Il titolo del prodotto è troppo lungo (massimo {max} caratteri, ha {actual}).",
|
|
645
672
|
"pub_title_too_few_words": "Il titolo del prodotto deve avere almeno {min} parole (ha {actual}).",
|
|
@@ -663,6 +690,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
663
690
|
"pub_need_attributes_field_item": "{idx}. {label} ({en_name}) - {input_type}, {mandatory}",
|
|
664
691
|
"pub_attribute_data_next": "Riesegui con --attribute-data '<json>' per fornire i valori degli attributi.",
|
|
665
692
|
"pub_image_migration_partial": "Alcune immagini non sono state migrate: {failed}. Sostituiscile o riesegui senza quegli URL.",
|
|
693
|
+
"pub_field_status_optional": "Opzionale",
|
|
666
694
|
},
|
|
667
695
|
"zh": {
|
|
668
696
|
"greeting": "您想使用库存分析 Skill 做什么?请直接回复选项字母,或输入你的需求:",
|
|
@@ -809,14 +837,21 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
809
837
|
"pub_unsupported_format": "不支持的文件格式:{source}。仅支持 PDF、Word、Excel、图片和 URL。",
|
|
810
838
|
"pub_invalid_url": "无法获取 URL:{source}。请检查 URL 后重试。",
|
|
811
839
|
"pub_url_fetch_failed": "无法从 {source} 获取内容:{error}。请检查 URL 后重试。",
|
|
812
|
-
"pub_llm_not_configured": "
|
|
813
|
-
"pub_llm_not_configured_next": "
|
|
840
|
+
"pub_llm_not_configured": "LLM 抽取不可用:未配置 LLM API key(OPENAI_API_KEY 或 DASHSCOPE_API_KEY)。此状态仅针对非表格源(PDF/Word/图片/URL);Excel/CSV 源按表头规则抽取,无需 LLM key。",
|
|
841
|
+
"pub_llm_not_configured_next": "若源为 Excel/CSV,直接带 --source 重跑即可(无需 key);若为 PDF/Word/图片/URL,请配置 LLM API key 后带 --source 重跑。不要自己读文件改走手动模式;第一步必须是 CTB(标题/品牌/类目)确认。",
|
|
814
842
|
"pub_create_requires_category": "创建商品必须提供 --category-id(叶子类目 ID)",
|
|
815
843
|
"pub_create_requires_name": "创建商品必须提供 --product-name(商品标题)",
|
|
816
844
|
"pub_create_preview_message": "请检查以下商品信息,确认无误后我将创建商品。",
|
|
817
845
|
"pub_create_preview_next": "如果信息正确,请加上 --publish-preview-token {token} 重新执行。",
|
|
818
846
|
"pub_create_success": "商品创建成功。",
|
|
819
847
|
"pub_create_success_next": "询问商家是否需要查看已创建的商品或进一步编辑。",
|
|
848
|
+
"pub_draft_created_with_issues": "草稿已创建,但部分必填字段或权限缺失。请在 Seller Center 补全后激活商品。",
|
|
849
|
+
"pub_draft_update_hint": "在 Seller Center 草稿列表中补全缺失字段后激活商品。",
|
|
850
|
+
"pub_create_failed_title": "商品创建失败",
|
|
851
|
+
"pub_create_failed_message": "商品未创建成功(含草稿):错误码 {code},原因:{reason}。",
|
|
852
|
+
"pub_create_failed_no_item_id": "创建接口返回成功但未返回 item_id,商品实际未创建。",
|
|
853
|
+
"pub_create_failed_check_and_rerun": "请先在 Seller Center 确认是否已存在重复草稿,修复报错问题后重新运行。",
|
|
854
|
+
"pub_create_failed_trace_hint": "如需平台排查,请提供 trace_id {trace_id} / request_id {request_id} 定位根因。",
|
|
820
855
|
"pub_title_too_short": "商品标题过短(最少 {min} 个字符,当前 {actual} 个)。",
|
|
821
856
|
"pub_title_too_long": "商品标题过长(最多 {max} 个字符,当前 {actual} 个)。",
|
|
822
857
|
"pub_title_too_few_words": "商品标题至少需要 {min} 个单词(当前 {actual} 个)。",
|
|
@@ -871,6 +906,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
871
906
|
"pub_summary_conditional_section": "【条件必填字段】",
|
|
872
907
|
"pub_field_status_filled": "已填写",
|
|
873
908
|
"pub_field_status_missing": "缺失",
|
|
909
|
+
"pub_field_status_optional": "选填",
|
|
874
910
|
"pub_mandatory_missing_block": "无法发布:缺少必填字段 [{fields}]。请补充后重新运行。",
|
|
875
911
|
"pub_conditional_missing_draft": "缺少条件必填字段 [{fields}]。商品将以未激活(Draft)状态保存,待补全后自动激活。是否确认?",
|
|
876
912
|
# DBM/DBS auto-switch
|
|
@@ -904,6 +940,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
904
940
|
"pub_ctb_confirm_next": "如果无误,请使用 --ctb-confirm-token {token} 继续。",
|
|
905
941
|
"pub_brand_not_found_title": "未找到品牌",
|
|
906
942
|
"pub_brand_not_found_message": "在 Miravia 中未找到与 \"{brand_name}\" 匹配的品牌。您是否想创建新品牌?",
|
|
943
|
+
"pub_brand_list_link": "点此查看已有品牌及审核状态:https://sellercenter.miravia.es/apps/setting/seller/brandlist",
|
|
907
944
|
"pub_brand_create_link": "点此创建新品牌:https://sellercenter.miravia.es/apps/product/publish/brand",
|
|
908
945
|
"pub_brand_create_next": "在 Seller Center 创建品牌后,请使用 --brand-name <brand_name> 或 --confirmed-brand-id <brand_id> 重新运行命令。",
|
|
909
946
|
"pub_brand_reconfirm_or_choose": "如果您不想创建品牌,请重新输入品牌名称,或从下方匹配列表中选择一个。",
|
|
@@ -912,7 +949,10 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
912
949
|
"pub_field_summary_description": "查看哪些字段已填写,哪些字段仍缺失。",
|
|
913
950
|
"pub_field_status_filled": "已填写",
|
|
914
951
|
"pub_field_status_missing": "未填写",
|
|
952
|
+
"pub_field_status_optional": "选填",
|
|
915
953
|
"pub_mandatory_fields_missing": "以下必填字段缺失,必须补充后才能继续:{fields}",
|
|
954
|
+
"pub_image_required_no_draft": "至少需要一张图片(主图或 SKU 图)。这是 Miravia 平台硬性要求,即使发草稿也无法绕过;缺失:{fields}。",
|
|
955
|
+
"pub_add_images_and_rerun": "请至少补充一张商品图片(URL 或本地文件)后重新运行命令。",
|
|
916
956
|
"pub_key_fields_missing_draft_question": "部分关键字段缺失。您可以选择以草稿模式发布,稍后再补充。如需发布为草稿,请加上 --draft 重新运行。",
|
|
917
957
|
"pub_manufacturer_eurp_mismatch": "制造商 / 欧盟负责人与已审核记录不匹配。请选择有效的记录,或以草稿模式发布。",
|
|
918
958
|
"pub_seller_sku_recommended": "强烈建议填写商家 SKU,作为该 SKU 的内部标识符,方便后续查找。如有请补充。",
|
|
@@ -80,6 +80,12 @@ _BUSINESS_ERROR_HINTS = {
|
|
|
80
80
|
"BRAND_PERMISSION_DENIED": "Your seller account does not have permission for this brand.",
|
|
81
81
|
"BRAND_PENDING": "Brand is pending approval. Use --draft to publish as inactive draft until the brand permission is granted.",
|
|
82
82
|
"PARAM_ILLEGAL": "Request parameter validation failed. Please check the input fields.",
|
|
83
|
+
"4234": (
|
|
84
|
+
"Draft-mode creation reported missing required fields or authorizations. "
|
|
85
|
+
"If no product ID is returned, the draft was NOT created: complete the "
|
|
86
|
+
"missing fields/permissions (e.g. brand or category authorization) in "
|
|
87
|
+
"Seller Center and resubmit."
|
|
88
|
+
),
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
_RETRYABLE_ERROR_CODES = {
|
|
@@ -360,6 +360,14 @@ class ProductClient:
|
|
|
360
360
|
|
|
361
361
|
# GPSR compliance fields are injected at SKU level (see below).
|
|
362
362
|
|
|
363
|
+
# Delivery method -> Miravia create field ``Attributes.deliverySof``
|
|
364
|
+
# ("DBM" or "DBS"). Only written when the seller explicitly chose one;
|
|
365
|
+
# when absent, publish_module._check_dbm_limits applies the default
|
|
366
|
+
# (DBM generally, DBS if the seller has DBS permission).
|
|
367
|
+
_dm = str(data.get("delivery_method") or "").strip().upper()
|
|
368
|
+
if _dm in ("DBM", "DBS"):
|
|
369
|
+
attributes["deliverySof"] = _dm
|
|
370
|
+
|
|
363
371
|
product_payload: Dict[str, Any] = {
|
|
364
372
|
"PrimaryCategory": category_id,
|
|
365
373
|
"Attributes": attributes,
|
|
@@ -379,10 +387,19 @@ class ProductClient:
|
|
|
379
387
|
sku["EU_Responsible"] = eurp_name
|
|
380
388
|
product_payload["Skus"] = {"Sku": skus}
|
|
381
389
|
|
|
382
|
-
# Sales-property variation definition
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
390
|
+
# Sales-property variation definition — NOT sent.
|
|
391
|
+
# Verified against ``/product/create``: including a product-level
|
|
392
|
+
# ``variation`` node ({"variation1": {...}}) makes Miravia reject the
|
|
393
|
+
# whole request with E005 Invalid Request Format, while the identical
|
|
394
|
+
# payload WITHOUT it creates successfully (draft item_id
|
|
395
|
+
# 1374866769737415). Miravia derives sale properties from the flat
|
|
396
|
+
# per-SKU fields (e.g. ``color: "Blue"``), so the separate node is both
|
|
397
|
+
# redundant and unsupported by this endpoint. ``_build_variation_definition``
|
|
398
|
+
# is retained in case a correctly-shaped multi-variation contract is
|
|
399
|
+
# confirmed later, but we must not inject it here.
|
|
400
|
+
# variation = self._build_variation_definition(data)
|
|
401
|
+
# if variation:
|
|
402
|
+
# product_payload["variation"] = variation
|
|
386
403
|
|
|
387
404
|
# Draft mode
|
|
388
405
|
if draft:
|