@miravia_test/miravia-publish-skill 0.1.2 → 0.1.3
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 +23 -9
- package/dist/TECHNICAL.md +1 -1
- package/dist/VERSION +1 -1
- package/dist/reference_publish.md +1 -1
- package/dist/scripts/cli_common.py +2 -2
- package/dist/scripts/{modules/extraction_engine.py → extraction_engine.py} +15 -6
- package/dist/scripts/{modules/file_reader.py → file_reader.py} +84 -16
- package/dist/scripts/i18n.py +158 -5
- package/dist/scripts/miravia_client.py +1 -0
- package/dist/scripts/product_client.py +25 -0
- package/dist/scripts/product_skill.py +258 -8
- package/dist/scripts/{modules/publish_module.py → publish_module.py} +1121 -126
- package/package.json +1 -1
- package/dist/scripts/modules/__init__.py +0 -1
|
@@ -31,7 +31,52 @@ import i18n
|
|
|
31
31
|
import skill_logger
|
|
32
32
|
import telemetry
|
|
33
33
|
from miravia_client import MiraviaClient, MiraviaError
|
|
34
|
-
from
|
|
34
|
+
from publish_module import PublishModule
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print_category_tree(
|
|
38
|
+
tree: Any,
|
|
39
|
+
indent: int = 0,
|
|
40
|
+
permissions: Optional[Dict[int, bool]] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Recursively print a category tree with indentation.
|
|
43
|
+
|
|
44
|
+
Handles both dict and list structures, showing category name, ID,
|
|
45
|
+
and permission status at each level. Limits depth to 2 levels.
|
|
46
|
+
"""
|
|
47
|
+
max_depth = 2
|
|
48
|
+
if indent >= max_depth:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
if permissions is None:
|
|
52
|
+
permissions = {}
|
|
53
|
+
|
|
54
|
+
if isinstance(tree, dict):
|
|
55
|
+
# Try common tree structures
|
|
56
|
+
categories = tree.get("categories") or tree.get("data") or tree.get("children") or []
|
|
57
|
+
if isinstance(categories, list):
|
|
58
|
+
for cat in categories:
|
|
59
|
+
_print_category_tree(cat, indent, permissions)
|
|
60
|
+
else:
|
|
61
|
+
# Single category node
|
|
62
|
+
name = tree.get("name") or tree.get("category_name") or ""
|
|
63
|
+
cid = tree.get("category_id") or tree.get("id") or ""
|
|
64
|
+
prefix = " " * indent
|
|
65
|
+
if name:
|
|
66
|
+
# Show permission status
|
|
67
|
+
if cid and int(cid) in permissions:
|
|
68
|
+
has_perm = permissions[int(cid)]
|
|
69
|
+
perm_marker = "[OK]" if has_perm else "[NO PERMISSION]"
|
|
70
|
+
print(f"{prefix}• {name} (ID: {cid}) {perm_marker}")
|
|
71
|
+
else:
|
|
72
|
+
print(f"{prefix}• {name} (ID: {cid})" if cid else f"{prefix}• {name}")
|
|
73
|
+
children = tree.get("children") or tree.get("sub_categories") or []
|
|
74
|
+
if isinstance(children, list):
|
|
75
|
+
for child in children:
|
|
76
|
+
_print_category_tree(child, indent + 1, permissions)
|
|
77
|
+
elif isinstance(tree, list):
|
|
78
|
+
for item in tree:
|
|
79
|
+
_print_category_tree(item, indent, permissions)
|
|
35
80
|
|
|
36
81
|
|
|
37
82
|
def _parse_args() -> argparse.Namespace:
|
|
@@ -118,6 +163,11 @@ def _parse_args() -> argparse.Namespace:
|
|
|
118
163
|
action="store_true",
|
|
119
164
|
help="Seller has confirmed the authorization request; skip the authorization step",
|
|
120
165
|
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--decline-auth",
|
|
168
|
+
action="store_true",
|
|
169
|
+
help="Seller explicitly declines authorization; stop the flow and prompt for re-upload",
|
|
170
|
+
)
|
|
121
171
|
parser.add_argument(
|
|
122
172
|
"--output-schema",
|
|
123
173
|
action="store_true",
|
|
@@ -201,6 +251,24 @@ def _print_conversational_result(result: Dict[str, Any]) -> None:
|
|
|
201
251
|
print(result.get("hint", ""))
|
|
202
252
|
return
|
|
203
253
|
|
|
254
|
+
# Multiple products detected in Excel
|
|
255
|
+
if status == "multiple_products_detected":
|
|
256
|
+
print(result.get("message", ""))
|
|
257
|
+
print()
|
|
258
|
+
for product in result.get("product_summaries", []):
|
|
259
|
+
print(
|
|
260
|
+
i18n._("pub_multiple_products_product_line").format(
|
|
261
|
+
index=product.get("index", ""),
|
|
262
|
+
title=product.get("title", "") or "(no title)",
|
|
263
|
+
price=product.get("price", "") or "(no price)",
|
|
264
|
+
sku=product.get("sku", "") or "(no SKU)",
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
print()
|
|
268
|
+
for action in result.get("next_actions", []):
|
|
269
|
+
print(f"• {action}")
|
|
270
|
+
return
|
|
271
|
+
|
|
204
272
|
# Interactive preview (brand / attributes)
|
|
205
273
|
if status == "need_brand":
|
|
206
274
|
print(result.get("message", ""))
|
|
@@ -233,6 +301,58 @@ def _print_conversational_result(result: Dict[str, Any]) -> None:
|
|
|
233
301
|
print(f"• {action}")
|
|
234
302
|
return
|
|
235
303
|
|
|
304
|
+
# Authorization cancelled
|
|
305
|
+
if status == "authorization_cancelled":
|
|
306
|
+
print(result.get("title", ""))
|
|
307
|
+
print()
|
|
308
|
+
print(result.get("message", ""))
|
|
309
|
+
print()
|
|
310
|
+
for action in result.get("next_actions", []):
|
|
311
|
+
print(f"• {action}")
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
# Unsupported file format
|
|
315
|
+
if status == "unsupported_format":
|
|
316
|
+
print(result.get("message", ""))
|
|
317
|
+
print()
|
|
318
|
+
for action in result.get("next_actions", []):
|
|
319
|
+
print(f"• {action}")
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
# File empty or corrupted
|
|
323
|
+
if status == "file_empty_or_corrupted":
|
|
324
|
+
print(result.get("message", ""))
|
|
325
|
+
print()
|
|
326
|
+
for action in result.get("next_actions", []):
|
|
327
|
+
print(f"• {action}")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
# Invalid URL
|
|
331
|
+
if status == "invalid_url":
|
|
332
|
+
print(result.get("message", ""))
|
|
333
|
+
print()
|
|
334
|
+
for action in result.get("next_actions", []):
|
|
335
|
+
print(f"• {action}")
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Generic file read error
|
|
339
|
+
if status == "file_read_error":
|
|
340
|
+
print(result.get("message", ""))
|
|
341
|
+
print()
|
|
342
|
+
for action in result.get("next_actions", []):
|
|
343
|
+
print(f"• {action}")
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# Title invalid with format suggestion
|
|
347
|
+
if status == "title_invalid":
|
|
348
|
+
print(result.get("message", ""))
|
|
349
|
+
print()
|
|
350
|
+
print(result.get("format_suggestion", ""))
|
|
351
|
+
print()
|
|
352
|
+
for action in result.get("next_actions", []):
|
|
353
|
+
print(f"• {action}")
|
|
354
|
+
return
|
|
355
|
+
|
|
236
356
|
# Category mandatory attributes missing (AI extraction flow)
|
|
237
357
|
if status == "need_category_attributes":
|
|
238
358
|
print(result.get("title", ""))
|
|
@@ -257,6 +377,16 @@ def _print_conversational_result(result: Dict[str, Any]) -> None:
|
|
|
257
377
|
|
|
258
378
|
if status == "need_category":
|
|
259
379
|
print(result.get("message", ""))
|
|
380
|
+
# Display category tree if available
|
|
381
|
+
category_tree = result.get("category_tree")
|
|
382
|
+
category_permissions = result.get("category_permissions", {})
|
|
383
|
+
if category_tree:
|
|
384
|
+
print()
|
|
385
|
+
print("--- Available Categories ---")
|
|
386
|
+
_print_category_tree(category_tree, permissions=category_permissions)
|
|
387
|
+
tree_link = result.get("category_tree_link")
|
|
388
|
+
if tree_link:
|
|
389
|
+
print(f"\n{tree_link}")
|
|
260
390
|
print()
|
|
261
391
|
for choice in result.get("choices", []):
|
|
262
392
|
name = choice.get("name") or choice.get("category_name")
|
|
@@ -267,6 +397,19 @@ def _print_conversational_result(result: Dict[str, Any]) -> None:
|
|
|
267
397
|
print(f"• {action}")
|
|
268
398
|
return
|
|
269
399
|
|
|
400
|
+
# Category permission required
|
|
401
|
+
if status == "need_category_permission":
|
|
402
|
+
print(result.get("title", ""))
|
|
403
|
+
print()
|
|
404
|
+
print(result.get("message", ""))
|
|
405
|
+
apply_link = result.get("apply_link")
|
|
406
|
+
if apply_link:
|
|
407
|
+
print(f"\n{apply_link}")
|
|
408
|
+
print()
|
|
409
|
+
for action in result.get("next_actions", []):
|
|
410
|
+
print(f"• {action}")
|
|
411
|
+
return
|
|
412
|
+
|
|
270
413
|
# Preview awaiting confirmation
|
|
271
414
|
if status == "preview":
|
|
272
415
|
print(result.get("message", "请确认以下发布操作:"))
|
|
@@ -354,19 +497,113 @@ def _print_conversational_result(result: Dict[str, Any]) -> None:
|
|
|
354
497
|
print(f"• {action}")
|
|
355
498
|
return
|
|
356
499
|
|
|
500
|
+
# Compliance fields missing — suggest Draft
|
|
501
|
+
if status == "compliance_missing_suggest_draft":
|
|
502
|
+
print(result.get("message", ""))
|
|
503
|
+
print()
|
|
504
|
+
for issue in result.get("compliance_issues", []):
|
|
505
|
+
print(f" • {issue}")
|
|
506
|
+
print()
|
|
507
|
+
for action in result.get("next_actions", []):
|
|
508
|
+
print(f"• {action}")
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
# GPSR fields missing — suggest Draft
|
|
512
|
+
if status == "gpsr_missing_suggest_draft":
|
|
513
|
+
print(result.get("message", ""))
|
|
514
|
+
print()
|
|
515
|
+
for issue in result.get("gpsr_issues", []):
|
|
516
|
+
print(f" • {issue}")
|
|
517
|
+
print()
|
|
518
|
+
for action in result.get("next_actions", []):
|
|
519
|
+
print(f"• {action}")
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
# Post-to-inactive fields missing — suggest Draft
|
|
523
|
+
if status == "post_to_inactive_missing_suggest_draft":
|
|
524
|
+
print(result.get("message", ""))
|
|
525
|
+
print()
|
|
526
|
+
for field in result.get("missing_fields", []):
|
|
527
|
+
print(f" • {field}")
|
|
528
|
+
print()
|
|
529
|
+
for action in result.get("next_actions", []):
|
|
530
|
+
print(f"• {action}")
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
# Special category rules — hard block (invalid PPU, ISBN missing for books)
|
|
534
|
+
if status == "special_category_block":
|
|
535
|
+
print(result.get("message", ""))
|
|
536
|
+
print()
|
|
537
|
+
for issue in result.get("block_issues", []):
|
|
538
|
+
print(f" • {issue}")
|
|
539
|
+
print()
|
|
540
|
+
for action in result.get("next_actions", []):
|
|
541
|
+
print(f"• {action}")
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
# DBM limit exceeded with no DBS permission — hard block
|
|
545
|
+
if status == "dbm_limit_exceeded_no_dbs":
|
|
546
|
+
print(result.get("message", ""))
|
|
547
|
+
print()
|
|
548
|
+
for reason in result.get("reasons", []):
|
|
549
|
+
print(f" • {reason}")
|
|
550
|
+
print()
|
|
551
|
+
for action in result.get("next_actions", []):
|
|
552
|
+
print(f"• {action}")
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
# SKU already exists — concurrency conflict
|
|
556
|
+
if status == "sku_already_exists":
|
|
557
|
+
print(result.get("message", ""))
|
|
558
|
+
code = result.get("code", "")
|
|
559
|
+
if code:
|
|
560
|
+
print(f"\n错误码:{code}")
|
|
561
|
+
hint = result.get("hint")
|
|
562
|
+
if hint:
|
|
563
|
+
print(f"\n{hint}")
|
|
564
|
+
print()
|
|
565
|
+
for action in result.get("next_actions", []):
|
|
566
|
+
print(f"• {action}")
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
# Special category rules — suggest Draft (PPU missing, food attrs missing)
|
|
570
|
+
if status == "special_category_suggest_draft":
|
|
571
|
+
print(result.get("message", ""))
|
|
572
|
+
print()
|
|
573
|
+
for field in result.get("missing_fields", []):
|
|
574
|
+
print(f" • {field}")
|
|
575
|
+
print()
|
|
576
|
+
for action in result.get("next_actions", []):
|
|
577
|
+
print(f"• {action}")
|
|
578
|
+
return
|
|
579
|
+
|
|
357
580
|
# Field extraction summary / draft question
|
|
358
581
|
if status == "need_field_summary":
|
|
359
582
|
print(result.get("title", ""))
|
|
360
583
|
print(result.get("message", ""))
|
|
361
584
|
print()
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
585
|
+
|
|
586
|
+
# Display summary in table-like format with Mandatory/Conditional sections
|
|
587
|
+
summary = result.get("summary", [])
|
|
588
|
+
for line in summary:
|
|
589
|
+
print(line)
|
|
590
|
+
|
|
591
|
+
# Show missing fields by category
|
|
592
|
+
mandatory_missing = result.get("mandatory_missing", [])
|
|
593
|
+
conditional_missing = result.get("conditional_missing", [])
|
|
594
|
+
|
|
595
|
+
if mandatory_missing:
|
|
366
596
|
print()
|
|
367
|
-
print("
|
|
368
|
-
for field in
|
|
369
|
-
print(f"
|
|
597
|
+
print("⚠ 缺失必填字段(阻断发布):")
|
|
598
|
+
for field in mandatory_missing:
|
|
599
|
+
print(f" {field}")
|
|
600
|
+
|
|
601
|
+
if conditional_missing:
|
|
602
|
+
print()
|
|
603
|
+
print("⚡ 缺失条件必填字段(可转 Draft):")
|
|
604
|
+
for field in conditional_missing:
|
|
605
|
+
print(f" ○ {field}")
|
|
606
|
+
|
|
370
607
|
country_note = result.get("country_price_note")
|
|
371
608
|
if country_note:
|
|
372
609
|
print()
|
|
@@ -499,6 +736,19 @@ def main() -> None:
|
|
|
499
736
|
# Authorization check (flow step ii): before any API calls, the seller
|
|
500
737
|
# must confirm that they authorize the skill to read profile, access
|
|
501
738
|
# product data, and update product data.
|
|
739
|
+
if getattr(args, "decline_auth", False):
|
|
740
|
+
auth_payload = {
|
|
741
|
+
"status": "authorization_cancelled",
|
|
742
|
+
"title": i18n._("pub_authorization_title"),
|
|
743
|
+
"message": i18n._("pub_authorization_cancelled"),
|
|
744
|
+
"next_actions": [i18n._("pub_authorization_confirm_next")],
|
|
745
|
+
}
|
|
746
|
+
if args.format == "conversational":
|
|
747
|
+
_print_conversational_result(auth_payload)
|
|
748
|
+
else:
|
|
749
|
+
print(json.dumps(auth_payload, indent=2, ensure_ascii=False))
|
|
750
|
+
return
|
|
751
|
+
|
|
502
752
|
if not skip_credential_check and args.scenario and not getattr(args, "authorized", False):
|
|
503
753
|
auth_payload = {
|
|
504
754
|
"status": "need_authorization",
|