@miravia_test/miravia-publish-skill 0.1.2 → 0.1.4

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.
@@ -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 modules.publish_module import PublishModule
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
- for line in result.get("summary", []):
363
- print(f" {line}")
364
- key_missing = result.get("key_missing", [])
365
- if key_missing:
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 key_missing:
369
- print(f" • {field}")
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",