@miravia_test/miravia-publish-skill 0.1.7 → 0.1.8

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 CHANGED
@@ -112,6 +112,7 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
112
112
  | `need_brand_create` | 品牌未找到,需去 Seller Center 处理 | 先引导查看已有品牌及审核状态(https://sellercenter.miravia.es/apps/setting/seller/brandlist,可能已在审核中),确无再创建(https://sellercenter.miravia.es/apps/product/publish/brand);创建后重新运行命令 |
113
113
  | `need_mandatory_fields` | 缺少必填字段;**图片缺失为硬阻断(不可 Draft)**,价格/重量/尺寸缺失可 `--draft` | 补充字段后重跑 |
114
114
  | `need_category_attributes` | 类目必填属性缺失(AI抽取模式) | 通过 `--attribute-data` 补充或使用 `--draft` |
115
+ | `need_attribute_mapping` | Excel/CSV 列未能自动匹配到类目属性(不静默丢弃),返回该列及其候选属性 | 向用户展示候选属性,确认后用 `--attribute-data '{"<属性名>":"<值>"}'` 重跑;非商品属性则删除该列 |
115
116
  | `need_field_summary` | 需要确认字段摘要或选择草稿模式 | 回传 `field_summary_token`,可加 `--draft` |
116
117
  | `need_compliance` | 制造商/EU 负责人匹配失败 | 按返回候选选择或补充信息后重跑 |
117
118
  | `compliance_missing_suggest_draft` | 合规字段缺失(制造商/欧代/Safety Warning内容/合规文档) | 补充合规字段或加 `--draft` 发布为草稿 |
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.7
1
+ 0.1.8
@@ -371,6 +371,7 @@ _TABLE_FIELD_SYNONYMS: Dict[str, tuple] = {
371
371
  "length_cm": ("长度", "长cm", "长", "length", "largura"),
372
372
  "width_cm": ("宽度", "宽cm", "宽", "width", "ancho"),
373
373
  "height_cm": ("高度", "高cm", "高", "height", "altura"),
374
+ "delivery_method": ("deliverymethod", "delivery", "shippingmethod", "shipping", "物流方式", "物流", "发货方式", "配送方式", "metododeenvío", "metododeenvio", "envío", "envio", "spedizione", "metododiconsegna"),
374
375
  "manufacturer_name": ("制造商", "厂家", "manufacturer", "fabricante"),
375
376
  "eu_responsible_person_name": ("欧盟负责人", "欧代", "euresponsibleperson", "eurp"),
376
377
  "variation_1": ("颜色", "color", "colour", "variation1", "variation", "款式"),
@@ -389,6 +390,7 @@ _PRODUCT_LEVEL_FIELDS = {
389
390
  "length_cm",
390
391
  "width_cm",
391
392
  "height_cm",
393
+ "delivery_method",
392
394
  "manufacturer_name",
393
395
  "eu_responsible_person_name",
394
396
  }
@@ -249,6 +249,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
249
249
  "pub_dbm_dim_sum_exceeded": "Dimension sum {dim_sum} exceeds DBM limit {limit}cm.",
250
250
  "pub_dbm_volume_exceeded": "Volumetric weight {volume} exceeds DBM limit {limit}.",
251
251
  "pub_dbm_auto_switch_to_dbs": "Package exceeds DBM limits. Delivery method auto-switched to DBS.",
252
+ "pub_dbs_no_permission_downgrade": "Seller requested DBS but this account has no DBS permission; the delivery method was set to DBM.",
252
253
  "pub_dbm_no_dbs_permission": "Package exceeds DBM limits but seller has no DBS permission. Cannot auto-switch to DBS.",
253
254
  "pub_dbm_fix_or_apply_dbs": "Please reduce package size/weight to meet DBM limits, or apply for DBS permission in Seller Center.",
254
255
  # SKU already exists (concurrency conflict)
@@ -338,6 +339,9 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
338
339
  "pub_need_category_attributes_description": "The category {category_id} requires the following attributes that were not found in the extracted data. Please provide them via --attribute-data or rerun with --draft to publish as an inactive draft.",
339
340
  "pub_need_category_attributes_item": "{idx}. {label} ({en_name}) - {input_type}",
340
341
  "pub_need_category_attributes_next": "Provide the missing attributes and rerun with --attribute-data '<json>', or use --draft to publish as inactive draft and complete them later.",
342
+ "pub_need_attribute_mapping_title": "Some columns could not be matched automatically",
343
+ "pub_need_attribute_mapping_description": "These Excel/CSV columns did not clearly match a category attribute, so they were not bound. Please confirm which attribute each column should map to, then rerun with --attribute-data using the canonical attribute names shown below (or drop the column if it is not a product attribute).",
344
+ "pub_need_attribute_mapping_next": "Rerun with --attribute-data '{\"<attribute_name>\": \"<value>\"}' using one of the suggested attribute names, or remove the column if it is not needed.",
341
345
  "pub_html_cleaned_notice": "The product description has been cleaned: unsafe HTML tags (script, iframe, style attributes, etc.) were removed. Only safe formatting tags (p, br, b, ul, li, etc.) are kept.",
342
346
  "pub_create_pending_review": "Product submitted successfully. It is currently under Miravia review and temporarily unavailable for sale; approval may take up to 48 hours, especially if package photos or documents are required. Once approved, it will be activated automatically. If rejected, please check the Suspended tab.",
343
347
  "pub_pdp_link": "Product detail page: {url}",
@@ -914,6 +918,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
914
918
  "pub_dbm_dim_sum_exceeded": "三边之和 {dim_sum} 超过 DBM 限制 {limit}cm。",
915
919
  "pub_dbm_volume_exceeded": "体积重 {volume} 超过 DBM 限制 {limit}。",
916
920
  "pub_dbm_auto_switch_to_dbs": "包裹超出 DBM 限制,物流方式自动转为 DBS。",
921
+ "pub_dbs_no_permission_downgrade": "商家请求使用 DBS,但当前账号没有 DBS 权限,物流方式已设为 DBM。",
917
922
  "pub_dbm_no_dbs_permission": "包裹超出 DBM 限制,但卖家无 DBS 权限,无法自动转为 DBS。",
918
923
  "pub_dbm_fix_or_apply_dbs": "请减小包裹尺寸/重量以满足 DBM 限制,或在 Seller Center 申请 DBS 权限。",
919
924
  # SKU already exists (concurrency conflict)
@@ -968,6 +973,9 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
968
973
  "pub_need_category_attributes_description": "类目 {category_id} 要求以下属性,但 AI 抽取的数据中未包含。请通过 --attribute-data 补充,或使用 --draft 发布为未激活草稿。",
969
974
  "pub_need_category_attributes_item": "{idx}. {label}({en_name})- {input_type}",
970
975
  "pub_need_category_attributes_next": "补充缺失属性后使用 --attribute-data '<json>' 重新运行,或使用 --draft 发布为未激活草稿稍后补充。",
976
+ "pub_need_attribute_mapping_title": "部分列未能自动匹配",
977
+ "pub_need_attribute_mapping_description": "以下 Excel/CSV 列未能明确匹配到类目属性,因此未绑定。请确认每列应对应哪个属性,然后用下方展示的规范属性名通过 --attribute-data 重新运行(若该列不是商品属性则可删除)。",
978
+ "pub_need_attribute_mapping_next": "使用下方建议的属性名之一,通过 --attribute-data '{\"<属性名>\": \"<值>\"}' 重新运行;若不需要该列则删除。",
971
979
  "pub_html_cleaned_notice": "商品描述已清理:不安全的 HTML 标签(script、iframe、style 属性等)已被移除。仅保留安全格式标签(p、br、b、ul、li 等)。",
972
980
  "pub_create_pending_review": "商品提交成功,目前正在接受 Miravia 审核,暂不可售;审核最长可能需要 48 小时,特别是需要包装照片或文件时。审核通过后会自动激活,如被拒绝请在“已暂停(Suspended)”选项卡中查看。",
973
981
  "pub_pdp_link": "商品详情页:{url}",
@@ -365,8 +365,10 @@ class ProductClient:
365
365
  # when absent, publish_module._check_dbm_limits applies the default
366
366
  # (DBM generally, DBS if the seller has DBS permission).
367
367
  _dm = str(data.get("delivery_method") or "").strip().upper()
368
- if _dm in ("DBM", "DBS"):
369
- attributes["deliverySof"] = _dm
368
+ if "DBS" in _dm:
369
+ attributes["deliverySof"] = "DBS"
370
+ elif "DBM" in _dm:
371
+ attributes["deliverySof"] = "DBM"
370
372
 
371
373
  product_payload: Dict[str, Any] = {
372
374
  "PrimaryCategory": category_id,
@@ -373,6 +373,25 @@ def _print_conversational_result(result: Dict[str, Any]) -> None:
373
373
  print(f"• {action}")
374
374
  return
375
375
 
376
+ # Unmapped Excel/CSV columns needing a confirmed attribute mapping
377
+ if status == "need_attribute_mapping":
378
+ print(result.get("title", ""))
379
+ print(result.get("message", ""))
380
+ print()
381
+ for item in result.get("unmapped_columns", []):
382
+ print(f" 列 \"{item.get('header')}\" = {item.get('value')} 可能对应:")
383
+ for c in item.get("candidates", []):
384
+ mand = "(必填)" if str(c.get("is_mandatory")) == "1" else ""
385
+ print(
386
+ f" - {c.get('label')} "
387
+ f"[name={c.get('name')}, en_name={c.get('en_name')}, "
388
+ f"type={c.get('attribute_type')}]{mand}"
389
+ )
390
+ print()
391
+ for action in result.get("next_actions", []):
392
+ print(f"• {action}")
393
+ return
394
+
376
395
  if status == "need_images":
377
396
  print(result.get("message", ""))
378
397
  if result.get("failed_images"):
@@ -813,12 +813,20 @@ class PublishModule:
813
813
  """
814
814
  attrs = product_payload.setdefault("Attributes", {})
815
815
  delivery_method = str(attrs.get("deliverySof") or "").strip().upper()
816
+ downgraded_from_dbs = False
816
817
 
817
- # Default when there is no explicit entry: DBS if the seller is
818
- # permitted, otherwise DBM.
819
818
  if delivery_method not in ("DBM", "DBS"):
819
+ # No explicit entry: default to DBS when the seller is permitted,
820
+ # otherwise DBM.
820
821
  delivery_method = "DBS" if has_dbs_permission else "DBM"
821
822
  attrs["deliverySof"] = delivery_method
823
+ elif delivery_method == "DBS" and not has_dbs_permission:
824
+ # Seller explicitly chose DBS but the account has no DBS permission:
825
+ # never grant DBS without permission — fall back to DBM and still
826
+ # enforce the DBM package limits below.
827
+ delivery_method = "DBM"
828
+ attrs["deliverySof"] = "DBM"
829
+ downgraded_from_dbs = True
822
830
 
823
831
  # Only DBM carries package limits.
824
832
  if delivery_method != "DBM":
@@ -856,6 +864,11 @@ class PublishModule:
856
864
  )
857
865
 
858
866
  if not exceeded:
867
+ if downgraded_from_dbs:
868
+ return {
869
+ "downgraded_to_dbm": True,
870
+ "message": i18n._("pub_dbs_no_permission_downgrade"),
871
+ }
859
872
  return None
860
873
 
861
874
  # Exceeded DBM limits — check DBS permission
@@ -907,6 +920,7 @@ class PublishModule:
907
920
  # Check DBM limits — auto-switch to DBS if exceeded (requires DBS permission)
908
921
  has_dbs = self.product.get_shipping_permission()
909
922
  dbm_result = self._check_dbm_limits(product_payload, has_dbs_permission=has_dbs)
923
+ dbm_notes: List[str] = []
910
924
  if dbm_result:
911
925
  if dbm_result.get("blocked"):
912
926
  # No DBS permission and exceeds DBM limits — block
@@ -919,10 +933,12 @@ class PublishModule:
919
933
  ],
920
934
  }
921
935
  if dbm_result.get("switched_to_dbs"):
922
- if draft_issues is not None:
923
- draft_issues.append(dbm_result["message"])
924
- for reason in dbm_result.get("reasons", []):
925
- draft_issues.append(f" - {reason}")
936
+ dbm_notes.append(dbm_result["message"])
937
+ dbm_notes.extend(f" - {r}" for r in dbm_result.get("reasons", []))
938
+ if dbm_result.get("downgraded_to_dbm"):
939
+ # Seller asked for DBS but has no DBS permission — explain the
940
+ # fallback so the delivery-method change is not silent.
941
+ dbm_notes.append(dbm_result["message"])
926
942
 
927
943
  # Run pre-validate early so errors are visible in the preview
928
944
  prevalidate_result = self._run_prevalidate(product_payload)
@@ -935,6 +951,7 @@ class PublishModule:
935
951
 
936
952
  # Merge all issues for preview
937
953
  all_issues = list(draft_issues or [])
954
+ all_issues.extend(dbm_notes)
938
955
  if prevalidate_errors:
939
956
  all_issues.extend(prevalidate_errors)
940
957
 
@@ -1340,11 +1357,25 @@ class PublishModule:
1340
1357
  draft=draft_requested,
1341
1358
  )
1342
1359
 
1360
+ # Explicit --attribute-data must reach the AI payload too (it used to be
1361
+ # honored only in the manual flow, so AI-mode values were silently
1362
+ # dropped). Applied before unmatched-column binding so explicit values win.
1363
+ self._apply_attribute_data_override(product_payload)
1364
+
1343
1365
  # Bind extra Excel/CSV columns that the whitelist did not recognize,
1344
1366
  # using the category attribute definitions fetched above.
1345
- self._unmapped_columns = self._apply_unmatched_category_attributes(
1367
+ unmapped_cols = self._apply_unmatched_category_attributes(
1346
1368
  product_payload, schema, category_attributes
1347
1369
  )
1370
+ self._unmapped_columns = unmapped_cols
1371
+
1372
+ # If a provided column could not be confidently matched but plausibly
1373
+ # maps to one or more category attributes, ask the seller to confirm the
1374
+ # mapping instead of silently dropping it (which would otherwise surface
1375
+ # later as a cryptic batchValidate "missing attribute" error).
1376
+ mapping_check = self._check_unmapped_columns(unmapped_cols, product_payload)
1377
+ if mapping_check:
1378
+ return mapping_check
1348
1379
 
1349
1380
  # Stage 3b: unified compliance check (GPSR + Safety Warning + compliance docs)
1350
1381
  draft_issues: list = []
@@ -1672,15 +1703,19 @@ class PublishModule:
1672
1703
  """
1673
1704
  confirmed_brand_id = getattr(self.args, "confirmed_brand_id", None)
1674
1705
  if confirmed_brand_id:
1706
+ # Honor an explicitly passed --brand-name too (aligns with the
1707
+ # manual flow's _resolve_brand_interactive). Otherwise a seller who
1708
+ # confirms by ID but whose source lacks the brand name would have
1709
+ # the brand silently dropped from the payload.
1675
1710
  return {
1676
1711
  "brand_id": int(confirmed_brand_id),
1677
- "brand_name": schema.brand_name or "",
1712
+ "brand_name": getattr(self.args, "brand_name", None) or schema.brand_name or "",
1678
1713
  }
1679
1714
 
1680
1715
  if schema.brand_id:
1681
1716
  return {
1682
1717
  "brand_id": int(schema.brand_id),
1683
- "brand_name": schema.brand_name or "",
1718
+ "brand_name": getattr(self.args, "brand_name", None) or schema.brand_name or "",
1684
1719
  }
1685
1720
 
1686
1721
  brand_name = self.args.brand_name or schema.brand_name
@@ -2179,8 +2214,9 @@ class PublishModule:
2179
2214
  value under the attribute's canonical key — into each SKU for
2180
2215
  ``attribute_type == 'sku'``, otherwise into the product-level
2181
2216
  ``Attributes``. Existing keys are never overwritten (whitelist/LLM
2182
- values win). Returns the headers that matched no category attribute
2183
- so the caller can surface them instead of silently dropping them.
2217
+ values win). Returns a list of ``{header, value, candidates}`` for the
2218
+ headers that matched no category attribute confidently, so the caller can
2219
+ ask the seller to confirm the mapping instead of silently dropping them.
2184
2220
 
2185
2221
  We intentionally bind ONLY the non-whitelisted columns here. Columns
2186
2222
  the whitelist already consumed (Title/Price/Image/Color/Stock/...) are
@@ -2196,11 +2232,31 @@ class PublishModule:
2196
2232
  return []
2197
2233
  attributes = product_payload.setdefault("Attributes", {})
2198
2234
  skus = (product_payload.get("Skus") or {}).get("Sku") or []
2199
- unmapped: List[str] = []
2235
+ unmapped: List[Dict[str, Any]] = []
2200
2236
  for header, value in unmatched.items():
2201
2237
  attr = self._find_category_attribute(category_attributes, (header,))
2202
2238
  if not attr:
2203
- unmapped.append(header)
2239
+ # No confident match: collect the header with fuzzy candidates so
2240
+ # the caller can ask the seller to confirm the intended attribute
2241
+ # instead of silently dropping the column.
2242
+ unmapped.append(
2243
+ {
2244
+ "header": header,
2245
+ "value": value,
2246
+ "candidates": [
2247
+ {
2248
+ "name": c.get("name"),
2249
+ "en_name": c.get("en_name"),
2250
+ "label": c.get("label") or c.get("en_name") or c.get("name"),
2251
+ "attribute_type": c.get("attribute_type") or "normal",
2252
+ "is_mandatory": str(c.get("is_mandatory", "0")),
2253
+ }
2254
+ for c in self._suggest_attribute_candidates(
2255
+ header, category_attributes
2256
+ )
2257
+ ],
2258
+ }
2259
+ )
2204
2260
  continue
2205
2261
  key = attr.get("name") or attr.get("en_name") or header
2206
2262
  if (attr.get("attribute_type") or "normal") == "sku":
@@ -2212,11 +2268,122 @@ class PublishModule:
2212
2268
  attributes[key] = value
2213
2269
  if unmapped:
2214
2270
  logger.info(
2215
- "Excel/CSV columns not matching any category attribute: %s",
2216
- ", ".join(unmapped),
2271
+ "Excel/CSV columns not confidently matched: %s",
2272
+ ", ".join(u["header"] for u in unmapped),
2217
2273
  )
2218
2274
  return unmapped
2219
2275
 
2276
+ def _suggest_attribute_candidates(
2277
+ self,
2278
+ header: str,
2279
+ category_attributes: List[Dict[str, Any]],
2280
+ max_candidates: int = 8,
2281
+ ) -> List[Dict[str, Any]]:
2282
+ """Fuzzy-suggest category attributes a free-text column header might map
2283
+ to — for the confirmation prompt only, never auto-bound.
2284
+
2285
+ Uses a shared-prefix (stem) heuristic so e.g. header ``packaging``
2286
+ suggests ``Package_Label_Photo_EU``. Deliberately looser than
2287
+ ``_find_category_attribute`` because a human confirms the result.
2288
+ """
2289
+ def _words(text: Any) -> List[str]:
2290
+ return [w for w in re.split(r"[^a-z0-9]+", str(text).lower()) if len(w) >= 4]
2291
+
2292
+ header_words = set(_words(header))
2293
+ h_sq = self._squash_attr_name(header)
2294
+ if h_sq:
2295
+ header_words.add(h_sq)
2296
+ out: List[Dict[str, Any]] = []
2297
+ for attr in category_attributes or []:
2298
+ attr_words: List[str] = []
2299
+ for key in ("name", "en_name", "label", "attribute_name"):
2300
+ if attr.get(key):
2301
+ attr_words.extend(_words(attr.get(key)))
2302
+ attr_words.append(self._squash_attr_name(attr.get(key)))
2303
+ hit = False
2304
+ for hw in header_words:
2305
+ for aw in attr_words:
2306
+ if (
2307
+ len(hw) >= 5
2308
+ and len(aw) >= 5
2309
+ and hw[:5] == aw[:5]
2310
+ ) or hw in aw or aw in hw:
2311
+ hit = True
2312
+ break
2313
+ if hit:
2314
+ break
2315
+ if hit:
2316
+ out.append(attr)
2317
+ if len(out) >= max_candidates:
2318
+ break
2319
+ return out
2320
+
2321
+ def _apply_attribute_data_override(self, product_payload: Dict[str, Any]) -> None:
2322
+ """Merge ``--attribute-data`` into the AI-built payload.
2323
+
2324
+ The AI flow builds Attributes/Skus from the extracted schema and never
2325
+ consulted ``--attribute-data``, so values passed there were silently
2326
+ dropped. ``--attribute-data`` is the seller's own explicit input, so it
2327
+ overrides extracted values; SKU-level keys are merged into every SKU.
2328
+ """
2329
+ raw = getattr(self.args, "attribute_data", None)
2330
+ if not raw:
2331
+ return
2332
+ try:
2333
+ data = json.loads(raw)
2334
+ except json.JSONDecodeError as exc:
2335
+ raise ValueError(f"--attribute-data is not valid JSON: {exc}") from exc
2336
+ if not isinstance(data, dict):
2337
+ raise ValueError("--attribute-data must be a JSON object")
2338
+ attributes = product_payload.setdefault("Attributes", {})
2339
+ skus = (product_payload.get("Skus") or {}).get("Sku") or []
2340
+ for key, value in data.items():
2341
+ if key in _SKU_LEVEL_ATTRIBUTE_NAMES:
2342
+ for sku in skus:
2343
+ sku[key] = value
2344
+ else:
2345
+ attributes[key] = value
2346
+
2347
+ def _check_unmapped_columns(
2348
+ self,
2349
+ unmapped_cols: List[Dict[str, Any]],
2350
+ product_payload: Dict[str, Any],
2351
+ ) -> Optional[Dict[str, Any]]:
2352
+ """Return a ``need_attribute_mapping`` prompt when an unmapped column
2353
+ looks like a category attribute the seller intended, so they confirm the
2354
+ mapping instead of us dropping it silently (which would later surface as
2355
+ a cryptic batchValidate "missing attribute" error). Columns with no
2356
+ plausible candidate — or whose candidates are already filled via
2357
+ ``--attribute-data`` — are ignored.
2358
+ """
2359
+ attributes = product_payload.get("Attributes") or {}
2360
+ skus = (product_payload.get("Skus") or {}).get("Sku") or []
2361
+
2362
+ def _filled(cand: Dict[str, Any]) -> bool:
2363
+ names = [cand.get("name"), cand.get("en_name")]
2364
+
2365
+ def _present(container: Dict[str, Any]) -> bool:
2366
+ return any(n and str(container.get(n) or "").strip() for n in names)
2367
+
2368
+ if (cand.get("attribute_type") or "normal") == "sku":
2369
+ return bool(skus) and all(_present(s) for s in skus)
2370
+ return _present(attributes)
2371
+
2372
+ need: List[Dict[str, Any]] = []
2373
+ for item in unmapped_cols or []:
2374
+ open_cands = [c for c in (item.get("candidates") or []) if not _filled(c)]
2375
+ if open_cands:
2376
+ need.append({**item, "candidates": open_cands})
2377
+ if not need:
2378
+ return None
2379
+ return {
2380
+ "status": "need_attribute_mapping",
2381
+ "title": i18n._("pub_need_attribute_mapping_title"),
2382
+ "message": i18n._("pub_need_attribute_mapping_description"),
2383
+ "unmapped_columns": need,
2384
+ "next_actions": [i18n._("pub_need_attribute_mapping_next")],
2385
+ }
2386
+
2220
2387
  def _field_is_required(
2221
2388
  self,
2222
2389
  field_key: str,
@@ -2503,7 +2670,7 @@ class PublishModule:
2503
2670
  confirmed_brand_id = getattr(self.args, "confirmed_brand_id", None)
2504
2671
  if confirmed_brand_id:
2505
2672
  return {
2506
- "brand_name": schema.brand_name or "",
2673
+ "brand_name": getattr(self.args, "brand_name", None) or schema.brand_name or "",
2507
2674
  "brand_id": int(confirmed_brand_id),
2508
2675
  }
2509
2676
 
@@ -2642,7 +2809,11 @@ class PublishModule:
2642
2809
 
2643
2810
  Per the Miravia ``/product/create`` documentation:
2644
2811
 
2645
- - ``SellerSku`` is mandatory for every SKU.
2812
+ - ``SellerSku`` is NOT hard-coded mandatory: whether it must be filled is
2813
+ governed by the category attribute API and enforced once in
2814
+ :meth:`_validate_create_payload` (via ``seller_sku_required``), which
2815
+ applies to both Draft and online publishes. Here we simply omit an
2816
+ empty SellerSku so ``/product/create`` can auto-generate a value.
2646
2817
  - Package dimensions/weight are per-SKU fields
2647
2818
  (``package_length/width/height/weight``).
2648
2819
  - Sales properties are flat SKU fields, not a nested ``Variation``
@@ -2671,11 +2842,11 @@ class PublishModule:
2671
2842
  seller_sku = raw.get("seller_sku") or (
2672
2843
  schema.seller_skus[idx] if idx < len(schema.seller_skus) else None
2673
2844
  )
2674
- if not seller_sku:
2675
- raise ValueError(
2676
- i18n._("pub_seller_sku_required", idx=idx + 1)
2677
- )
2678
- sku["SellerSku"] = seller_sku
2845
+ # Do NOT hard-require SellerSku here. Its mandatory-ness comes from
2846
+ # the category attribute API and is enforced in _validate_create_payload
2847
+ # (not relaxed by draft). Omit empty values so create can auto-generate.
2848
+ if seller_sku:
2849
+ sku["SellerSku"] = seller_sku
2679
2850
 
2680
2851
  ean_code = raw.get("ean_code") or (
2681
2852
  schema.ean_codes[idx] if idx < len(schema.ean_codes) else None
@@ -3186,10 +3357,11 @@ class PublishModule:
3186
3357
  processed["SellerSku"] = processed.pop("seller_sku")
3187
3358
  elif "seller_sku" in processed:
3188
3359
  processed.pop("seller_sku") # SellerSku already set
3360
+ # SellerSku is optional (governed by the category attribute API and
3361
+ # enforced in _validate_create_payload, not relaxed by draft). When
3362
+ # empty, drop the key so /product/create can auto-generate it.
3189
3363
  if not processed.get("SellerSku"):
3190
- raise ValueError(
3191
- i18n._("pub_seller_sku_required", idx=idx + 1)
3192
- )
3364
+ processed.pop("SellerSku", None)
3193
3365
  # Migrate SKU-level images
3194
3366
  sku_images = processed.get("Images", {}).get("Image", [])
3195
3367
  if sku_images:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miravia_test/miravia-publish-skill",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Miravia Product Publish Skill installer — one command to install the Miravia AI product listing skill into any AI Agent (Qoder / Cursor / Claude Code). Upload materials and let the AI extract data and publish products automatically.",
5
5
  "bin": {
6
6
  "miravia-publish-skill": "bin/cli.js"