@miravia_test/miravia-publish-skill 0.1.8 → 0.1.9
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 +1 -0
- package/dist/VERSION +1 -1
- package/dist/scripts/config.py +25 -0
- package/dist/scripts/i18n.py +10 -0
- package/dist/scripts/product_client.py +14 -14
- package/dist/scripts/product_skill.py +2 -1
- package/dist/scripts/publish_module.py +185 -21
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -117,6 +117,7 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
|
|
|
117
117
|
| `need_compliance` | 制造商/EU 负责人匹配失败 | 按返回候选选择或补充信息后重跑 |
|
|
118
118
|
| `compliance_missing_suggest_draft` | 合规字段缺失(制造商/欧代/Safety Warning内容/合规文档) | 补充合规字段或加 `--draft` 发布为草稿 |
|
|
119
119
|
| `gpsr_missing_suggest_draft` | GPSR 字段缺失(制造商和/或欧盟负责人为空) | 补充 GPSR 字段或加 `--draft` 发布为草稿 |
|
|
120
|
+
| `need_safety_warning_confirm` | AI/Excel 源检测到安全相关内容,但卖家未显式设置「该产品是否有安全警告」;系统**不会**静默默认为 No | 展示 `detected_safety_content` 证据与当前值,让卖家确认:确有则加 `--safety-warning yes`(必要时 `--safety-warning-content`)重跑;确无则加 `--safety-warning no` 确认;或在 Excel 补齐该列后重跑 |
|
|
120
121
|
| `post_to_inactive_missing_suggest_draft` | 类目必填字段缺失(EAN) | 补充字段或加 `--draft` 发布为草稿 |
|
|
121
122
|
| `special_category_block` | 特殊类目规则阻断(PPU 值无效 / 图书类目 ISBN 缺失) | 修正错误后重跑,不可转 Draft |
|
|
122
123
|
| `special_category_suggest_draft` | 特殊类目字段缺失(PPU 缺失 / 食品属性缺失) | 补充字段或加 `--draft` 发布为草稿 |
|
package/dist/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.9
|
package/dist/scripts/config.py
CHANGED
|
@@ -220,6 +220,31 @@ def require_business_credentials() -> None:
|
|
|
220
220
|
pass
|
|
221
221
|
|
|
222
222
|
|
|
223
|
+
def has_valid_business_credentials() -> bool:
|
|
224
|
+
"""Return True when credentials are present and not expired.
|
|
225
|
+
|
|
226
|
+
Non-raising companion to ``require_business_credentials``. The publish
|
|
227
|
+
flow uses this to decide whether the seller is already authorized: when
|
|
228
|
+
valid credentials exist (they can only exist after a successful
|
|
229
|
+
``auth_cli.py login``, which grants the read-profile / access-products /
|
|
230
|
+
update-products scopes), the skill must not re-prompt the three-item
|
|
231
|
+
authorization consent on every run. Returns False for missing or expired
|
|
232
|
+
credentials so the caller falls back to asking the seller.
|
|
233
|
+
"""
|
|
234
|
+
if not (APP_KEY and APP_SECRET and ACCESS_TOKEN):
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
if TOKEN_EXPIRES_AT:
|
|
238
|
+
try:
|
|
239
|
+
expires_at_ms = int(TOKEN_EXPIRES_AT)
|
|
240
|
+
if expires_at_ms > 0 and int(time.time() * 1000) > expires_at_ms - 60_000:
|
|
241
|
+
return False
|
|
242
|
+
except ValueError:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
|
|
223
248
|
def mask_token(token: str) -> str:
|
|
224
249
|
if not token:
|
|
225
250
|
return "<empty>"
|
package/dist/scripts/i18n.py
CHANGED
|
@@ -218,6 +218,11 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
218
218
|
"pub_compliance_manufacturer_eurp_both_empty": "Manufacturer and EU Responsible Person are both empty. Non-EU manufacturers require an EU Responsible Person.",
|
|
219
219
|
"pub_compliance_eurp_missing_with_manufacturer": "Manufacturer is filled but EU Responsible Person is empty. Non-EU manufacturers require an EU Responsible Person.",
|
|
220
220
|
"pub_safety_warning_content_missing": "Safety Warning is set to Yes but the warning content is empty. Please provide the safety warning content.",
|
|
221
|
+
"pub_safety_confirm_title": "Safety warning needs your confirmation",
|
|
222
|
+
"pub_safety_confirm_message": "We detected safety-related content in your source ({evidence}), but the 'Does this product have a safety warning' flag was not set explicitly. We will NOT silently default it to 'No'. Please confirm the correct value, or complete the field in your Excel and rerun.",
|
|
223
|
+
"pub_safety_confirm_next_yes": "If the product does have a safety warning, rerun with --safety-warning yes (add --safety-warning-content if needed).",
|
|
224
|
+
"pub_safety_confirm_next_no": "If it genuinely has no safety warning, rerun with --safety-warning no to confirm.",
|
|
225
|
+
"pub_safety_confirm_next_excel": "Your Excel fields may be incomplete; you can fill the safety column there and rerun.",
|
|
221
226
|
"pub_compliance_docs_missing": "Compliance documents are required for this category but not provided.",
|
|
222
227
|
"pub_compliance_missing_message": "Some compliance fields are missing. You can fill them or publish to Draft.",
|
|
223
228
|
"pub_compliance_fill_or_draft": "Fill the missing compliance fields, or publish as Draft to complete them later.",
|
|
@@ -887,6 +892,11 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
|
|
|
887
892
|
"pub_compliance_manufacturer_eurp_both_empty": "制造商和欧盟负责人均为空。非欧盟制造商必须填写欧盟负责人。",
|
|
888
893
|
"pub_compliance_eurp_missing_with_manufacturer": "已填写制造商但欧盟负责人为空。非欧盟制造商必须填写欧盟负责人。",
|
|
889
894
|
"pub_safety_warning_content_missing": "安全警告已设为 Yes 但未填写警告内容。请补充安全警告内容。",
|
|
895
|
+
"pub_safety_confirm_title": "安全警告需要你确认",
|
|
896
|
+
"pub_safety_confirm_message": "我们从你的源文件中检测到安全相关内容({evidence}),但“该产品是否有安全警告”这一项没有被显式设置。我们不会把它静默默认为 No。请确认应取的值,或先在 Excel 中补全该项后重新运行。",
|
|
897
|
+
"pub_safety_confirm_next_yes": "如果该商品确实有安全警告,请加 --safety-warning yes 重新运行(必要时再加 --safety-warning-content)。",
|
|
898
|
+
"pub_safety_confirm_next_no": "如果确实没有安全警告,请加 --safety-warning no 确认。",
|
|
899
|
+
"pub_safety_confirm_next_excel": "Excel 字段可能不完整,你可以在其中补上安全警告列后重新运行。",
|
|
890
900
|
"pub_compliance_docs_missing": "该类目要求上传合规文档但未提供。",
|
|
891
901
|
"pub_compliance_missing_message": "部分合规字段缺失。您可以补充完整或发布为草稿。",
|
|
892
902
|
"pub_compliance_fill_or_draft": "补充缺失的合规字段,或发布为草稿(Draft)稍后在 Seller Center 中完善。",
|
|
@@ -360,15 +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.
|
|
364
|
-
#
|
|
363
|
+
# Delivery method -> Miravia create field ``Attributes.delivery_option_sof``,
|
|
364
|
+
# which is a YES/NO flag: "Yes" = DBS (seller-fulfilled), "No" = DBM
|
|
365
|
+
# (platform-fulfilled). Only written when the seller explicitly chose one;
|
|
365
366
|
# when absent, publish_module._check_dbm_limits applies the default
|
|
366
|
-
# (DBM generally, DBS if the seller has DBS permission).
|
|
367
|
+
# (No/DBM generally, Yes/DBS if the seller has DBS permission).
|
|
367
368
|
_dm = str(data.get("delivery_method") or "").strip().upper()
|
|
368
|
-
if "DBS"
|
|
369
|
-
attributes["
|
|
370
|
-
elif "DBM" in _dm:
|
|
371
|
-
attributes["deliverySof"] = "DBM"
|
|
369
|
+
if _dm in ("DBM", "DBS"):
|
|
370
|
+
attributes["delivery_option_sof"] = "Yes" if _dm == "DBS" else "No"
|
|
372
371
|
|
|
373
372
|
product_payload: Dict[str, Any] = {
|
|
374
373
|
"PrimaryCategory": category_id,
|
|
@@ -407,13 +406,14 @@ class ProductClient:
|
|
|
407
406
|
if draft:
|
|
408
407
|
product_payload["Status"] = "inactive"
|
|
409
408
|
|
|
410
|
-
# Country-specific prices
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
409
|
+
# Country-specific prices are NOT injected here at the product level.
|
|
410
|
+
# Verified against the authoritative /product/create contract: each
|
|
411
|
+
# country override lives INSIDE the SKU as
|
|
412
|
+
# ``CountryPrice: [{"CountryCode":"ES","Price":"1.9","SalePrice":"1.8"}]``
|
|
413
|
+
# (capitalised keys). A product-level ``CountryPrice`` list with the
|
|
414
|
+
# old ``{"country","price"}`` shape is silently ignored by the API.
|
|
415
|
+
# This is built per SKU in
|
|
416
|
+
# ``PublishModule._build_sku_list_from_schema``.
|
|
417
417
|
|
|
418
418
|
# Standard unit type / count
|
|
419
419
|
if data.get("standard_unit_type"):
|
|
@@ -815,7 +815,8 @@ def main() -> None:
|
|
|
815
815
|
print(json.dumps(auth_payload, indent=2, ensure_ascii=False))
|
|
816
816
|
return
|
|
817
817
|
|
|
818
|
-
|
|
818
|
+
already_authorized = getattr(args, "authorized", False) or config.has_valid_business_credentials()
|
|
819
|
+
if not skip_credential_check and args.scenario and not already_authorized:
|
|
819
820
|
auth_payload = {
|
|
820
821
|
"status": "need_authorization",
|
|
821
822
|
"title": i18n._("pub_authorization_title"),
|
|
@@ -801,32 +801,55 @@ class PublishModule:
|
|
|
801
801
|
*,
|
|
802
802
|
has_dbs_permission: bool = False,
|
|
803
803
|
) -> Optional[Dict[str, Any]]:
|
|
804
|
-
"""Resolve ``Attributes.
|
|
805
|
-
|
|
806
|
-
Miravia ``/product/create`` expects ``Attributes.
|
|
807
|
-
``"
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
804
|
+
"""Resolve ``Attributes.delivery_option_sof`` and enforce DBM package limits.
|
|
805
|
+
|
|
806
|
+
Miravia ``/product/create`` expects ``Attributes.delivery_option_sof`` to be
|
|
807
|
+
a YES/NO flag: ``"Yes"`` = DBS (seller-fulfilled), ``"No"`` = DBM
|
|
808
|
+
(platform-fulfilled). The legacy ``deliverySof="DBM"/"DBS"`` literal is
|
|
809
|
+
silently ignored by the platform. Internally we normalise to ``"DBM"``/
|
|
810
|
+
``"DBS"`` for the package-limit logic and always write the flag back as
|
|
811
|
+
Yes/No. When the seller did not choose one it is defaulted here: generally
|
|
812
|
+
DBM (No), but DBS (Yes) when the seller has DBS permission. If the
|
|
813
|
+
effective method is DBM but the package exceeds the DBM limits (weight
|
|
814
|
+
<= 40KG, L+W+H <= 300cm, volumetric weight L*W*H/6000 <= 40), it is
|
|
815
|
+
switched to DBS when permitted, otherwise the caller blocks. Returns a
|
|
816
|
+
result dict, or None when nothing to do.
|
|
813
817
|
"""
|
|
814
818
|
attrs = product_payload.setdefault("Attributes", {})
|
|
815
|
-
|
|
819
|
+
|
|
820
|
+
def _write_flag(method: str) -> None:
|
|
821
|
+
# Canonical wire flag the platform actually honours; replaces the
|
|
822
|
+
# legacy deliverySof value so no stale key lingers.
|
|
823
|
+
attrs["delivery_option_sof"] = "Yes" if method == "DBS" else "No"
|
|
824
|
+
attrs.pop("deliverySof", None)
|
|
825
|
+
|
|
826
|
+
_raw = str(
|
|
827
|
+
attrs.get("delivery_option_sof") or attrs.get("deliverySof") or ""
|
|
828
|
+
).strip().upper()
|
|
829
|
+
# Accept the wire flag (Yes/No) as well as legacy DBM/DBS codes.
|
|
830
|
+
if _raw in ("YES", "DBS"):
|
|
831
|
+
delivery_method = "DBS"
|
|
832
|
+
elif _raw in ("NO", "DBM"):
|
|
833
|
+
delivery_method = "DBM"
|
|
834
|
+
else:
|
|
835
|
+
delivery_method = ""
|
|
816
836
|
downgraded_from_dbs = False
|
|
817
837
|
|
|
818
838
|
if delivery_method not in ("DBM", "DBS"):
|
|
819
839
|
# No explicit entry: default to DBS when the seller is permitted,
|
|
820
840
|
# otherwise DBM.
|
|
821
841
|
delivery_method = "DBS" if has_dbs_permission else "DBM"
|
|
822
|
-
|
|
842
|
+
_write_flag(delivery_method)
|
|
823
843
|
elif delivery_method == "DBS" and not has_dbs_permission:
|
|
824
844
|
# Seller explicitly chose DBS but the account has no DBS permission:
|
|
825
845
|
# never grant DBS without permission — fall back to DBM and still
|
|
826
846
|
# enforce the DBM package limits below.
|
|
827
847
|
delivery_method = "DBM"
|
|
828
|
-
|
|
848
|
+
_write_flag(delivery_method)
|
|
829
849
|
downgraded_from_dbs = True
|
|
850
|
+
else:
|
|
851
|
+
# Normalise an explicit/legacy value to the canonical Yes/No flag.
|
|
852
|
+
_write_flag(delivery_method)
|
|
830
853
|
|
|
831
854
|
# Only DBM carries package limits.
|
|
832
855
|
if delivery_method != "DBM":
|
|
@@ -874,7 +897,7 @@ class PublishModule:
|
|
|
874
897
|
# Exceeded DBM limits — check DBS permission
|
|
875
898
|
if has_dbs_permission:
|
|
876
899
|
# Auto-switch to DBS
|
|
877
|
-
|
|
900
|
+
_write_flag("DBS")
|
|
878
901
|
return {
|
|
879
902
|
"switched_to_dbs": True,
|
|
880
903
|
"reasons": exceeded,
|
|
@@ -915,7 +938,9 @@ class PublishModule:
|
|
|
915
938
|
seller_sku_required = self._field_is_required(
|
|
916
939
|
"SellerSku", category_attributes or []
|
|
917
940
|
)
|
|
918
|
-
_validate_create_payload(
|
|
941
|
+
_validate_create_payload(
|
|
942
|
+
product_payload, seller_sku_required=seller_sku_required, draft=draft
|
|
943
|
+
)
|
|
919
944
|
|
|
920
945
|
# Check DBM limits — auto-switch to DBS if exceeded (requires DBS permission)
|
|
921
946
|
has_dbs = self.product.get_shipping_permission()
|
|
@@ -1377,6 +1402,40 @@ class PublishModule:
|
|
|
1377
1402
|
if mapping_check:
|
|
1378
1403
|
return mapping_check
|
|
1379
1404
|
|
|
1405
|
+
# Safety warning: honor an explicit --safety-warning in the source flow.
|
|
1406
|
+
# build_create_product_payload defaults the flag to "No" and manual mode
|
|
1407
|
+
# applies the CLI flags at build time, but the Excel/source flow never did
|
|
1408
|
+
# — so --safety-warning was silently ignored here. Apply it now so the
|
|
1409
|
+
# seller's explicit choice is never overwritten by the default.
|
|
1410
|
+
self._apply_safety_cli_override(product_payload)
|
|
1411
|
+
|
|
1412
|
+
# Safety guidance gate: never silently ship an inferred "No". If the seller
|
|
1413
|
+
# has not chosen a value but the source carries real safety content, pause
|
|
1414
|
+
# and ask them to confirm yes/no (or complete the Excel). The flag can only
|
|
1415
|
+
# be auto-populated once the seller answers, per the "surface assumed
|
|
1416
|
+
# values, do not silently replace" rule.
|
|
1417
|
+
if getattr(self.args, "safety_warning", None) is None:
|
|
1418
|
+
safety_evidence = self._detect_safety_content(product_payload, schema)
|
|
1419
|
+
if safety_evidence:
|
|
1420
|
+
current_flag = (product_payload.get("Attributes") or {}).get(
|
|
1421
|
+
"Does_this_product_have_a_safety_warning"
|
|
1422
|
+
)
|
|
1423
|
+
return {
|
|
1424
|
+
"status": "need_safety_warning_confirm",
|
|
1425
|
+
"title": i18n._("pub_safety_confirm_title"),
|
|
1426
|
+
"message": i18n._(
|
|
1427
|
+
"pub_safety_confirm_message",
|
|
1428
|
+
evidence=", ".join(safety_evidence),
|
|
1429
|
+
),
|
|
1430
|
+
"detected_safety_content": safety_evidence,
|
|
1431
|
+
"current_value": current_flag,
|
|
1432
|
+
"next_actions": [
|
|
1433
|
+
i18n._("pub_safety_confirm_next_yes"),
|
|
1434
|
+
i18n._("pub_safety_confirm_next_no"),
|
|
1435
|
+
i18n._("pub_safety_confirm_next_excel"),
|
|
1436
|
+
],
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1380
1439
|
# Stage 3b: unified compliance check (GPSR + Safety Warning + compliance docs)
|
|
1381
1440
|
draft_issues: list = []
|
|
1382
1441
|
compliance_check = self._check_compliance_fields(
|
|
@@ -1537,8 +1596,8 @@ class PublishModule:
|
|
|
1537
1596
|
|
|
1538
1597
|
# Structural fields Miravia always requires for any product (not category
|
|
1539
1598
|
# attributes, so the API never governs them) — always blocking. NOTE:
|
|
1540
|
-
# delivery method (Attributes.
|
|
1541
|
-
# auto-defaulted (DBM, or DBS when the seller has DBS permission) rather
|
|
1599
|
+
# delivery method (Attributes.delivery_option_sof) is intentionally NOT here — it is
|
|
1600
|
+
# auto-defaulted (DBM=No, or DBS=Yes when the seller has DBS permission) rather
|
|
1542
1601
|
# than hard-collected; see _check_dbm_limits.
|
|
1543
1602
|
_CORE_MANDATORY_KEYS = frozenset(
|
|
1544
1603
|
{
|
|
@@ -2048,8 +2107,8 @@ class PublishModule:
|
|
|
2048
2107
|
# jeans, so an empty EAN must NOT be flagged as missing.
|
|
2049
2108
|
api_optional: List[str] = []
|
|
2050
2109
|
|
|
2051
|
-
# Delivery method (Attributes.
|
|
2052
|
-
# Miravia defaults it when absent (DBM, or DBS if the seller has DBS
|
|
2110
|
+
# Delivery method (Attributes.delivery_option_sof) is NOT a hard-collect field:
|
|
2111
|
+
# Miravia defaults it when absent (DBM=No, or DBS=Yes if the seller has DBS
|
|
2053
2112
|
# permission — see _check_dbm_limits), so an empty value shows as
|
|
2054
2113
|
# "Optional" here and never blocks.
|
|
2055
2114
|
if not (getattr(schema, "delivery_method", None) or "").strip():
|
|
@@ -2844,7 +2903,8 @@ class PublishModule:
|
|
|
2844
2903
|
)
|
|
2845
2904
|
# Do NOT hard-require SellerSku here. Its mandatory-ness comes from
|
|
2846
2905
|
# the category attribute API and is enforced in _validate_create_payload
|
|
2847
|
-
# (
|
|
2906
|
+
# (draft-aware: a live publish blocks an interface-mandatory empty value;
|
|
2907
|
+
# a draft defers it). Omit empty values so create can auto-generate.
|
|
2848
2908
|
if seller_sku:
|
|
2849
2909
|
sku["SellerSku"] = seller_sku
|
|
2850
2910
|
|
|
@@ -2892,6 +2952,25 @@ class PublishModule:
|
|
|
2892
2952
|
if sku_images:
|
|
2893
2953
|
sku["Images"] = {"Image": sku_images}
|
|
2894
2954
|
|
|
2955
|
+
# Country-specific prices live INSIDE each SKU (per the
|
|
2956
|
+
# authoritative /product/create contract), with capitalised keys
|
|
2957
|
+
# ``CountryCode`` / ``Price`` / ``SalePrice``. ``country_general_prices``
|
|
2958
|
+
# supplies the per-country Price and ``country_discounted_prices``
|
|
2959
|
+
# the per-country SalePrice. A product-level list or the old
|
|
2960
|
+
# lowercase ``{"country","price"}`` shape is silently ignored.
|
|
2961
|
+
_cgp = schema.country_general_prices or {}
|
|
2962
|
+
_cdp = schema.country_discounted_prices or {}
|
|
2963
|
+
_country_price = []
|
|
2964
|
+
for _cc in sorted(set(_cgp) | set(_cdp)):
|
|
2965
|
+
_entry: Dict[str, Any] = {"CountryCode": _cc}
|
|
2966
|
+
if _cgp.get(_cc) not in (None, ""):
|
|
2967
|
+
_entry["Price"] = str(_cgp[_cc])
|
|
2968
|
+
if _cdp.get(_cc) not in (None, ""):
|
|
2969
|
+
_entry["SalePrice"] = str(_cdp[_cc])
|
|
2970
|
+
_country_price.append(_entry)
|
|
2971
|
+
if _country_price:
|
|
2972
|
+
sku["CountryPrice"] = _country_price
|
|
2973
|
+
|
|
2895
2974
|
skus.append(sku)
|
|
2896
2975
|
|
|
2897
2976
|
if len(skus) > _MAX_SKUS_PER_PRODUCT:
|
|
@@ -2900,6 +2979,84 @@ class PublishModule:
|
|
|
2900
2979
|
)
|
|
2901
2980
|
return skus
|
|
2902
2981
|
|
|
2982
|
+
# Boilerplate default warning text — must NOT be treated as real safety content.
|
|
2983
|
+
_SAFETY_CONTENT_DEFAULT = "no specific safety warnings apply to this product"
|
|
2984
|
+
|
|
2985
|
+
def _apply_safety_cli_override(self, product_payload: Dict[str, Any]) -> None:
|
|
2986
|
+
"""Honor ``--safety-warning`` / ``--safety-warning-content`` in the source flow.
|
|
2987
|
+
|
|
2988
|
+
``build_create_product_payload`` defaults ``Does_this_product_have_a_safety_warning``
|
|
2989
|
+
to "No" when ``schema.safety_warning`` is falsy, and manual mode applies the CLI
|
|
2990
|
+
flags at build time (see the manual path). The Excel/source flow never applied
|
|
2991
|
+
them, so ``--safety-warning`` was silently ignored there. Write the seller's
|
|
2992
|
+
explicit choice here using the same "Yes"/"No" casing the client already emits,
|
|
2993
|
+
so the platform contract is unchanged and only the previously-ignored flag is
|
|
2994
|
+
now respected.
|
|
2995
|
+
"""
|
|
2996
|
+
safety_arg = getattr(self.args, "safety_warning", None)
|
|
2997
|
+
if safety_arg is None:
|
|
2998
|
+
return
|
|
2999
|
+
is_yes = str(safety_arg).strip().lower() in ("yes", "1", "true", "y")
|
|
3000
|
+
attributes = product_payload.setdefault("Attributes", {})
|
|
3001
|
+
value = "Yes" if is_yes else "No"
|
|
3002
|
+
attributes["Does this product have a safety warning"] = value
|
|
3003
|
+
attributes["Does_this_product_have_a_safety_warning"] = value
|
|
3004
|
+
content = getattr(self.args, "safety_warning_content", None)
|
|
3005
|
+
if is_yes and content:
|
|
3006
|
+
attributes["Safety_warning_content"] = content
|
|
3007
|
+
|
|
3008
|
+
def _detect_safety_content(
|
|
3009
|
+
self, product_payload: Dict[str, Any], schema: MiraviaListingSchema
|
|
3010
|
+
) -> List[str]:
|
|
3011
|
+
"""Return human-readable evidence that the product has safety info.
|
|
3012
|
+
|
|
3013
|
+
Used to decide whether the auto-defaulted safety flag should be surfaced
|
|
3014
|
+
to the seller instead of shipped silently. Ignores the boilerplate default
|
|
3015
|
+
warning text and a plain "No" flag so we do not raise false prompts. Counts
|
|
3016
|
+
real safety artifacts that came from the source: safety documents
|
|
3017
|
+
(Safety_Date_Sheet / CPSR / CPNP URLs), an affirmative Hazmat flag, a
|
|
3018
|
+
non-default ``Safety_warning_content`` already present, or an explicit
|
|
3019
|
+
``Does_this_product_have_a_safety_warning=Yes``.
|
|
3020
|
+
"""
|
|
3021
|
+
attributes = product_payload.get("Attributes") or {}
|
|
3022
|
+
evidence: List[str] = []
|
|
3023
|
+
for key, val in attributes.items():
|
|
3024
|
+
if not isinstance(val, str) or not val.strip():
|
|
3025
|
+
continue
|
|
3026
|
+
low = key.lower()
|
|
3027
|
+
# Skip the yes/no flag itself (handled explicitly below).
|
|
3028
|
+
if low in (
|
|
3029
|
+
"does this product have a safety warning",
|
|
3030
|
+
"does_this_product_have_a_safety_warning",
|
|
3031
|
+
):
|
|
3032
|
+
if val.strip().lower() == "yes":
|
|
3033
|
+
evidence.append(f"{key}=Yes")
|
|
3034
|
+
continue
|
|
3035
|
+
if not any(tok in low for tok in ("safety", "cpsr", "cpnp", "hazmat", "warning")):
|
|
3036
|
+
continue
|
|
3037
|
+
# Boilerplate default content is not evidence.
|
|
3038
|
+
if self._SAFETY_CONTENT_DEFAULT in val.strip().lower():
|
|
3039
|
+
continue
|
|
3040
|
+
# A negative/empty Hazmat flag is not evidence.
|
|
3041
|
+
if "hazmat" in low and val.strip().lower() in ("no", "false", "0", "n/a", "-"):
|
|
3042
|
+
continue
|
|
3043
|
+
# Null-like placeholder strings from empty cells are not evidence.
|
|
3044
|
+
if val.strip().lower() in ("none", "null", "n/a", "na", "-"):
|
|
3045
|
+
continue
|
|
3046
|
+
display = val if len(val) <= 60 else val[:57] + "..."
|
|
3047
|
+
evidence.append(f"{key}={display}")
|
|
3048
|
+
sc = getattr(schema, "safety_warning_content", None)
|
|
3049
|
+
if sc and self._SAFETY_CONTENT_DEFAULT not in str(sc).strip().lower():
|
|
3050
|
+
evidence.append("safety_warning_content provided")
|
|
3051
|
+
# Deduplicate while preserving order.
|
|
3052
|
+
seen: set = set()
|
|
3053
|
+
uniq: List[str] = []
|
|
3054
|
+
for item in evidence:
|
|
3055
|
+
if item not in seen:
|
|
3056
|
+
seen.add(item)
|
|
3057
|
+
uniq.append(item)
|
|
3058
|
+
return uniq
|
|
3059
|
+
|
|
2903
3060
|
def _run_prevalidate(
|
|
2904
3061
|
self, product_payload: Dict[str, Any]
|
|
2905
3062
|
) -> Optional[Dict[str, Any]]:
|
|
@@ -3505,6 +3662,7 @@ def _validate_title(title: str) -> None:
|
|
|
3505
3662
|
def _validate_create_payload(
|
|
3506
3663
|
payload: Dict[str, Any],
|
|
3507
3664
|
seller_sku_required: bool = False,
|
|
3665
|
+
draft: bool = False,
|
|
3508
3666
|
) -> None:
|
|
3509
3667
|
"""Validate a Miravia ``/product/create`` Product payload.
|
|
3510
3668
|
|
|
@@ -3514,7 +3672,9 @@ def _validate_create_payload(
|
|
|
3514
3672
|
|
|
3515
3673
|
``seller_sku_required`` is decided by the caller from the category attribute
|
|
3516
3674
|
API (``_field_is_required``): when False, an empty SellerSku is allowed and
|
|
3517
|
-
the create API auto-generates a unique value.
|
|
3675
|
+
the create API auto-generates a unique value. When True but ``draft`` is set,
|
|
3676
|
+
the empty value is deferred to the draft (surfaced as a draft_issue) rather
|
|
3677
|
+
than hard-blocked, consistent with P7 — only a live publish enforces it.
|
|
3518
3678
|
"""
|
|
3519
3679
|
attributes = payload.get("Attributes") or {}
|
|
3520
3680
|
title = attributes.get("name")
|
|
@@ -3558,7 +3718,11 @@ def _validate_create_payload(
|
|
|
3558
3718
|
raise ValueError(i18n._("pub_seller_sku_duplicate_in_product"))
|
|
3559
3719
|
|
|
3560
3720
|
for idx, sku in enumerate(skus, start=1):
|
|
3561
|
-
if
|
|
3721
|
+
if (
|
|
3722
|
+
seller_sku_required
|
|
3723
|
+
and not draft
|
|
3724
|
+
and not (sku.get("SellerSku") or sku.get("seller_sku"))
|
|
3725
|
+
):
|
|
3562
3726
|
raise ValueError(i18n._("pub_seller_sku_required", idx=idx))
|
|
3563
3727
|
|
|
3564
3728
|
price = sku.get("price")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miravia_test/miravia-publish-skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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"
|