@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 +1 -0
- package/dist/VERSION +1 -1
- package/dist/reference_publish.md +1 -1
- package/dist/scripts/extraction_engine.py +20 -2
- package/dist/scripts/field_definitions.py +3 -3
- package/dist/scripts/i18n.py +4 -0
- package/dist/scripts/product_client.py +4 -4
- package/dist/scripts/product_skill.py +9 -0
- package/dist/scripts/publish_module.py +44 -8
- package/package.json +1 -1
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.
|
|
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~
|
|
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
|
-
|
|
185
|
-
|
|
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
|
|
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-
|
|
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 且小于
|
|
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,商品将挂靠在该类目下。",
|
package/dist/scripts/i18n.py
CHANGED
|
@@ -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
|
-
|
|
243
|
+
params: Dict[str, Any] = {}
|
|
244
244
|
if title:
|
|
245
|
-
|
|
245
|
+
params["title"] = title
|
|
246
246
|
if image_urls:
|
|
247
|
-
|
|
248
|
-
result = self.client.request("
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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"
|