@miravia_test/miravia-publish-skill 0.1.3 → 0.1.5

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
@@ -119,6 +119,7 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
119
119
  | `need_images` | 部分图片迁移失败 | 替换图片后重跑 |
120
120
  | `unsupported_format` | 上传了不支持的文件格式(zip/txt/psd 等) | 提示仅支持 PDF/Word/Excel/Image/URL,允许重新上传 |
121
121
  | `file_empty_or_corrupted` | 文件内容为空或损坏(0KB PDF / 乱码 Word) | 提示文件内容为空或损坏,允许重新上传 |
122
+ | `llm_not_configured` | 未配置 LLM API key(OPENAI_API_KEY / DASHSCOPE_API_KEY),AI 抽取不可用 | 提示配置 key 后带 --source 重跑;不要绕过确认流程改走手动模式 |
122
123
  | `invalid_url` | 输入的 URL 无法抓取 | 提示检查 URL 后重试 |
123
124
  | `title_invalid` | 商品标题不符合规范(过短/过长/词数不足) | 提示具体错误 + 标题格式建议(Brand+Category+Model+Features+Scenario),阻断后续流程,禁止转 Draft |
124
125
  | `need_category_permission` | 卖家无该类目发布权限 | 询问是否以 Draft 模式发布(待审批通过后自动激活),提供类目权限申请链接 https://sellercenter.miravia.es/apps/setting/category;或更换类目后重跑 |
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.3
1
+ 0.1.5
@@ -249,7 +249,7 @@ PRD 中标记为新增 API,但当前 Miravia Open Platform 文档站点未上
249
249
 
250
250
  | Schema 字段(snake_case) | API 字段 / 位置 | 必填 | 说明 |
251
251
  |---|---|---|---|
252
- | `product_title` | `Attributes.name` | M | 商品标题,20~255 字符,≥5 个单词 |
252
+ | `product_title` | `Attributes.name` | M | 商品标题,20~205 字符,≥5 个单词 |
253
253
  | `product_images` | `Images.Image[]` | M | 主图列表,第 1 张为主图 |
254
254
  | `product_long_description` | `Attributes.description` | O | 长描述,可含简单 HTML |
255
255
  | `category_id` | `PrimaryCategory` | M | 叶子类目 ID |
@@ -102,6 +102,20 @@ class MiraviaListingSchema:
102
102
  # ---------------------------------------------------------------------------
103
103
 
104
104
 
105
+ class LLMNotConfiguredError(RuntimeError):
106
+ """Raised when AI extraction is requested but no LLM API key is configured.
107
+
108
+ Callers must surface this as a structured "llm_not_configured" status so
109
+ agents prompt for the key instead of silently bypassing the confirm-first
110
+ extraction flow.
111
+ """
112
+
113
+
114
+ LLM_NOT_CONFIGURED_HINT = (
115
+ "No LLM API key configured. Set OPENAI_API_KEY or DASHSCOPE_API_KEY."
116
+ )
117
+
118
+
105
119
  class LLMClient:
106
120
  """Minimal OpenAI-compatible LLM client."""
107
121
 
@@ -181,8 +195,12 @@ class ExtractionEngine:
181
195
 
182
196
  def extract(self, source_text: str) -> MiraviaListingSchema:
183
197
  if not self.client.is_configured():
184
- logger.warning("LLM not configured; running heuristic extraction")
185
- return self._heuristic_extract(source_text)
198
+ # Never silently degrade to heuristic extraction: without LLM the
199
+ # confirm-first flow (ctb → field summary → publish) breaks because
200
+ # agents judge the regex output unreliable and bypass the skill.
201
+ # Fail loudly so the caller returns a structured status instead.
202
+ logger.error("LLM not configured; refusing heuristic fallback")
203
+ raise LLMNotConfiguredError(LLM_NOT_CONFIGURED_HINT)
186
204
 
187
205
  schema_fields = "\n".join(f"- {f.name}" for f in fields(MiraviaListingSchema))
188
206
  messages = [
@@ -16,7 +16,7 @@ FIELD_DEFINITIONS: Dict[str, Dict[str, str]] = {
16
16
  "mo": "Mandatory",
17
17
  "definition": "The title / name of the product.",
18
18
  "requirement": (
19
- "Length must be greater than 20 characters and less than 255 characters. "
19
+ "Length must be greater than 20 characters and less than 205 characters. "
20
20
  "Suggested format: Brand + category + collection/model + key features + using scenarios."
21
21
  ),
22
22
  },
@@ -375,7 +375,7 @@ FIELD_DEFINITIONS: Dict[str, Dict[str, str]] = {
375
375
 
376
376
  FIELD_GUIDES: Dict[str, Dict[str, str]] = {
377
377
  "en": {
378
- "product_title": "Product title (Mandatory): The title / name of the product. Must be 20-255 characters. Suggested format: Brand + category + collection/model + key features + using scenarios.",
378
+ "product_title": "Product title (Mandatory): The title / name of the product. Must be 20-205 characters. Suggested format: Brand + category + collection/model + key features + using scenarios.",
379
379
  "product_images": "Product images (Mandatory): Image URLs. The first image is the main image shown in search/home. First image must have white background, 1:1 aspect ratio, 330-5000px, <5MB, jpg/png/jpeg/webp. Max 8 images.",
380
380
  "product_long_description": "Product long description (Optional): May include simple HTML. 20-3000 characters; embedded HTML is not supported.",
381
381
  "category_id": "Category ID (Mandatory): The internal Miravia leaf category ID where the product will be listed.",
@@ -414,7 +414,7 @@ FIELD_GUIDES: Dict[str, Dict[str, str]] = {
414
414
  "selling_marketplace_aliexpress": "Selling marketplace - AliExpress (Optional): Selling status per country on the AliExpress platform.",
415
415
  },
416
416
  "zh": {
417
- "product_title": "商品标题(必填):商品的标题/名称。长度需大于 20 且小于 255 个字符。建议格式:品牌 + 类目 + 系列/型号 + 核心卖点 + 使用场景。",
417
+ "product_title": "商品标题(必填):商品的标题/名称。长度需大于 20 且小于 205 个字符。建议格式:品牌 + 类目 + 系列/型号 + 核心卖点 + 使用场景。",
418
418
  "product_images": "商品图片(必填):商品图片 URL。第一张图是在搜索/首页展示的主图,必须使用纯白背景、1:1 比例、330-5000px、<5MB,仅支持 jpg/png/jpeg/webp。最多 8 张。",
419
419
  "product_long_description": "商品长描述(选填):可包含简单 HTML 的长描述。长度 20-3000 字符;不支持嵌套复杂 HTML,建议使用列表、加粗、字体等简单标签。",
420
420
  "category_id": "类目 ID(必填):Miravia 叶子类目内部 ID,商品将挂靠在该类目下。",
@@ -172,6 +172,8 @@ 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": "AI extraction is unavailable: no LLM API key is configured (OPENAI_API_KEY or DASHSCOPE_API_KEY).",
176
+ "pub_llm_not_configured_next": "Configure the LLM API key in the environment, then rerun with --source. Do not bypass the confirm-first flow with manual mode.",
175
177
  "pub_create_requires_category": "create_product requires --category-id (leaf category)",
176
178
  "pub_create_requires_name": "create_product requires --product-name",
177
179
  "pub_create_preview_message": "Please review the product creation details below and confirm before I create the product.",
@@ -807,6 +809,8 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
807
809
  "pub_unsupported_format": "不支持的文件格式:{source}。仅支持 PDF、Word、Excel、图片和 URL。",
808
810
  "pub_invalid_url": "无法获取 URL:{source}。请检查 URL 后重试。",
809
811
  "pub_url_fetch_failed": "无法从 {source} 获取内容:{error}。请检查 URL 后重试。",
812
+ "pub_llm_not_configured": "AI 抽取不可用:未配置 LLM API key(OPENAI_API_KEY 或 DASHSCOPE_API_KEY)。",
813
+ "pub_llm_not_configured_next": "请在环境中配置 LLM API key 后,带 --source 重新运行;不要绕过确认流程改走手动模式。",
810
814
  "pub_create_requires_category": "创建商品必须提供 --category-id(叶子类目 ID)",
811
815
  "pub_create_requires_name": "创建商品必须提供 --product-name(商品标题)",
812
816
  "pub_create_preview_message": "请检查以下商品信息,确认无误后我将创建商品。",
@@ -240,12 +240,12 @@ class ProductClient:
240
240
  Returns:
241
241
  List of category candidates, each with category_id, name, confidence, etc.
242
242
  """
243
- body: Dict[str, Any] = {}
243
+ params: Dict[str, Any] = {}
244
244
  if title:
245
- body["title"] = title
245
+ params["title"] = title
246
246
  if image_urls:
247
- body["imageUrls"] = image_urls
248
- result = self.client.request("POST", "/product/category/recommend/get", body)
247
+ params["imageUrls"] = image_urls
248
+ result = self.client.request("GET", "/product/category/recommend/get", params)
249
249
  data = result.get("data", []) or []
250
250
  if isinstance(data, list):
251
251
  return data
@@ -327,6 +327,15 @@ def _print_conversational_result(result: Dict[str, Any]) -> None:
327
327
  print(f"• {action}")
328
328
  return
329
329
 
330
+ # LLM not configured — fail loudly so agents configure the key instead of
331
+ # bypassing the confirm-first flow with manual mode
332
+ if status == "llm_not_configured":
333
+ print(result.get("message", ""))
334
+ print()
335
+ for action in result.get("next_actions", []):
336
+ print(f"• {action}")
337
+ return
338
+
330
339
  # Invalid URL
331
340
  if status == "invalid_url":
332
341
  print(result.get("message", ""))
@@ -33,6 +33,7 @@ from extraction_engine import (
33
33
  CRITICAL_FIELDS,
34
34
  ExtractionEngine,
35
35
  LLMClient,
36
+ LLMNotConfiguredError,
36
37
  MiraviaListingSchema,
37
38
  check_missing,
38
39
  validate_title,
@@ -156,10 +157,25 @@ class PublishModule:
156
157
  category_id = self.args.category_id
157
158
  product_name = self.args.product_name
158
159
  if not category_id:
159
- raise ValueError(i18n._("pub_create_requires_category"))
160
+ return {
161
+ "status": "need_category",
162
+ "message": i18n._("pub_create_requires_category"),
163
+ "next_actions": [i18n._("pub_ai_post_provide_category")],
164
+ }
160
165
  if not product_name:
161
- raise ValueError(i18n._("pub_create_requires_name"))
162
- _validate_title(product_name)
166
+ return {
167
+ "status": "title_invalid",
168
+ "message": i18n._("pub_create_requires_name"),
169
+ "next_actions": [i18n._("pub_ai_post_requires_source")],
170
+ }
171
+ title_err = validate_title(product_name)
172
+ if title_err:
173
+ return {
174
+ "status": "title_invalid",
175
+ "message": title_err,
176
+ "format_suggestion": i18n._("pub_title_format_suggestion"),
177
+ "next_actions": [i18n._("pub_ai_post_requires_source")],
178
+ }
163
179
 
164
180
  brand_resolution = self._resolve_brand_interactive()
165
181
  if brand_resolution.get("status") == "need_brand":
@@ -1095,7 +1111,11 @@ class PublishModule:
1095
1111
  "next_actions": [i18n._("pub_ai_post_requires_source")],
1096
1112
  }
1097
1113
  if not content_items:
1098
- raise ValueError(i18n._("pub_ai_post_no_content"))
1114
+ return {
1115
+ "status": "file_empty_or_corrupted",
1116
+ "message": i18n._("pub_file_empty_or_corrupted", source=sources[0] if sources else ""),
1117
+ "next_actions": [i18n._("pub_ai_post_requires_source")],
1118
+ }
1099
1119
 
1100
1120
  # Check for multiple products in Excel
1101
1121
  row_count = get_excel_row_count(content_items)
@@ -1115,7 +1135,18 @@ class PublishModule:
1115
1135
 
1116
1136
  source_text = extract_text(content_items)
1117
1137
  engine = ExtractionEngine(LLMClient())
1118
- schema = engine.extract(source_text)
1138
+ try:
1139
+ schema = engine.extract(source_text)
1140
+ except LLMNotConfiguredError:
1141
+ # Fail loudly instead of degrading to regex: without LLM the
1142
+ # confirm-first order (title/brand/category before missing-field
1143
+ # summary) cannot be guaranteed, so agents must configure the key
1144
+ # rather than bypass the skill.
1145
+ return {
1146
+ "status": "llm_not_configured",
1147
+ "message": i18n._("pub_llm_not_configured"),
1148
+ "next_actions": [i18n._("pub_llm_not_configured_next")],
1149
+ }
1119
1150
 
1120
1151
  if getattr(self.args, "output_schema", False):
1121
1152
  return {
@@ -1738,7 +1769,7 @@ class PublishModule:
1738
1769
 
1739
1770
  package_dims = ("package_weight", "package_length", "package_width", "package_height")
1740
1771
  for idx, sku in enumerate(skus, start=1):
1741
- if not sku.get("SellerSku"):
1772
+ if not (sku.get("SellerSku") or sku.get("seller_sku")):
1742
1773
  missing.append(f"Seller SKU (SKU #{idx})")
1743
1774
  for dim in package_dims:
1744
1775
  val = sku.get(dim)
@@ -1809,7 +1840,7 @@ class PublishModule:
1809
1840
  if self._is_category_attribute_mandatory(category_attributes, "compliance_documents") and not schema.compliance_documents:
1810
1841
  missing.append("compliance_documents")
1811
1842
 
1812
- if not any(sku.get("SellerSku") for sku in skus):
1843
+ if not any(sku.get("SellerSku") or sku.get("seller_sku") for sku in skus):
1813
1844
  missing.append("seller_sku")
1814
1845
 
1815
1846
  return missing
@@ -2727,6 +2758,11 @@ class PublishModule:
2727
2758
  skus: List[Dict[str, Any]] = []
2728
2759
  for idx, sku in enumerate(data):
2729
2760
  processed = dict(sku)
2761
+ # Normalize seller_sku → SellerSku (accept both snake_case and PascalCase)
2762
+ if "seller_sku" in processed and "SellerSku" not in processed:
2763
+ processed["SellerSku"] = processed.pop("seller_sku")
2764
+ elif "seller_sku" in processed:
2765
+ processed.pop("seller_sku") # SellerSku already set
2730
2766
  if not processed.get("SellerSku"):
2731
2767
  raise ValueError(
2732
2768
  i18n._("pub_seller_sku_required", idx=idx + 1)
@@ -2904,7 +2940,7 @@ def _validate_create_payload(payload: Dict[str, Any]) -> None:
2904
2940
  raise ValueError(i18n._("pub_ean_duplicate_in_product"))
2905
2941
 
2906
2942
  # Check Seller SKU duplicate within same product
2907
- seller_skus = [sku.get("SellerSku") for sku in skus if sku.get("SellerSku")]
2943
+ seller_skus = [sku.get("SellerSku") or sku.get("seller_sku") for sku in skus if sku.get("SellerSku") or sku.get("seller_sku")]
2908
2944
  if len(seller_skus) != len(set(seller_skus)):
2909
2945
  raise ValueError(i18n._("pub_seller_sku_duplicate_in_product"))
2910
2946
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miravia_test/miravia-publish-skill",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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"