sellersprite-cli 0.1.0__py3-none-any.whl

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.
Files changed (42) hide show
  1. sellersprite_cli/__init__.py +8 -0
  2. sellersprite_cli/cli.py +649 -0
  3. sellersprite_cli/errors.py +19 -0
  4. sellersprite_cli/generators.py +295 -0
  5. sellersprite_cli/markdown_utils.py +67 -0
  6. sellersprite_cli/mcp_client.py +262 -0
  7. sellersprite_cli/registry.py +90 -0
  8. sellersprite_cli/skills/README.md +106 -0
  9. sellersprite_cli/skills/agent-instructions.md +68 -0
  10. sellersprite_cli/skills/category-structure/high-margin-lightweight.md +64 -0
  11. sellersprite_cli/skills/category-structure/high-new-product-ratio.md +60 -0
  12. sellersprite_cli/skills/category-structure/low-brand-monopoly.md +63 -0
  13. sellersprite_cli/skills/comprehensive/ad-optimizer.md +169 -0
  14. sellersprite_cli/skills/comprehensive/competitor-analysis.md +149 -0
  15. sellersprite_cli/skills/comprehensive/keyword-research.md +132 -0
  16. sellersprite_cli/skills/comprehensive/listing-optimizer.md +167 -0
  17. sellersprite_cli/skills/comprehensive/market-analysis.md +144 -0
  18. sellersprite_cli/skills/comprehensive/opportunity-finder.md +154 -0
  19. sellersprite_cli/skills/comprehensive/pricing-strategy.md +142 -0
  20. sellersprite_cli/skills/comprehensive/product-research.md +104 -0
  21. sellersprite_cli/skills/comprehensive/review-insights.md +145 -0
  22. sellersprite_cli/skills/comprehensive/traffic-analysis.md +167 -0
  23. sellersprite_cli/skills/edge-opportunity/fbm-intercept.md +62 -0
  24. sellersprite_cli/skills/edge-opportunity/high-ticket-long-tail.md +65 -0
  25. sellersprite_cli/skills/edge-opportunity/local-premium-disruption.md +63 -0
  26. sellersprite_cli/skills/edge-opportunity/poor-listing-winner.md +64 -0
  27. sellersprite_cli/skills/edge-opportunity/seasonal-prepositioning.md +97 -0
  28. sellersprite_cli/skills/keyword-trend/aba-high-growth-trend.md +62 -0
  29. sellersprite_cli/skills/keyword-trend/low-monopoly-keyword.md +61 -0
  30. sellersprite_cli/skills/keyword-trend/title-density-gap.md +73 -0
  31. sellersprite_cli/skills/new-product-burst/hidden-bestseller.md +60 -0
  32. sellersprite_cli/skills/new-product-burst/new-product-burst.md +64 -0
  33. sellersprite_cli/skills/product-defect/hot-low-rating.md +63 -0
  34. sellersprite_cli/skills/product-defect/review-sentiment.md +75 -0
  35. sellersprite_cli/skills/traffic-audit/natural-traffic-audit.md +81 -0
  36. sellersprite_cli/skills/traffic-audit/variant-gap-analysis.md +61 -0
  37. sellersprite_cli-0.1.0.dist-info/METADATA +166 -0
  38. sellersprite_cli-0.1.0.dist-info/RECORD +42 -0
  39. sellersprite_cli-0.1.0.dist-info/WHEEL +5 -0
  40. sellersprite_cli-0.1.0.dist-info/entry_points.txt +2 -0
  41. sellersprite_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  42. sellersprite_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ """卖家精灵 CLI — Python MCP 客户端 + 交互式终端 + 27 AI Skills."""
2
+
3
+ from importlib.metadata import version as _version
4
+
5
+ __version__ = _version("sellersprite-cli")
6
+
7
+ from .mcp_client import SellerSprite
8
+ from .errors import ApiError, McpError
@@ -0,0 +1,649 @@
1
+ """CLI entry point — Typer app with domain-grouped subcommands and interactive TUI."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Annotated, Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+
13
+ from .registry import TOOLS, DOMAINS, DOMAIN_TOOLS, to_camel, ALL_LIST_PARAMS
14
+ from .markdown_utils import load_skills, get_skills_by_category, CATEGORY_NAMES, load_skills_index
15
+ from .generators import CLIENT_NAMES, generate
16
+
17
+ app = typer.Typer(
18
+ name="sellersprite",
19
+ help="卖家精灵 CLI — 交互式终端 + MCP 客户端 + 27 AI Skills",
20
+ no_args_is_help=False,
21
+ add_completion=False,
22
+ )
23
+
24
+ console = Console()
25
+
26
+ # ── Shared state ──────────────────────────────────────────────
27
+
28
+ _state = {"key": None, "marketplace": "US"}
29
+
30
+
31
+ def _resolve_key(explicit: str | None = None) -> str:
32
+ key = explicit or _state.get("key") or os.environ.get("SELLERSPRITE_KEY", "")
33
+ if not key:
34
+ # Try .env file
35
+ env_file = Path.cwd() / ".env"
36
+ if env_file.exists():
37
+ for line in env_file.read_text(encoding="utf-8").splitlines():
38
+ line = line.strip()
39
+ if line.startswith("SELLERSPRITE_KEY="):
40
+ key = line.split("=", 1)[1].strip().strip("\"'")
41
+ break
42
+ if not key:
43
+ console.print("[red]缺少 API 密钥。[/red]请设置 SELLERSPRITE_KEY 环境变量或使用 --key")
44
+ raise typer.Exit(1)
45
+ return key
46
+
47
+
48
+ def _get_sdk(key: str | None = None, marketplace: str = "US"):
49
+ from .mcp_client import SellerSprite
50
+ return SellerSprite(secret_key=_resolve_key(key), marketplace=marketplace)
51
+
52
+
53
+ def _call_tool(tool_name: str, key: str | None, marketplace: str, **kwargs):
54
+ """Generic tool caller — routes to the right MCP client method."""
55
+ ss = _get_sdk(key, marketplace)
56
+ method = getattr(ss, tool_name)
57
+ cleaned = {k: v for k, v in kwargs.items() if v is not None}
58
+ return method(**cleaned)
59
+
60
+
61
+ def _coerce_arg(key: str, value: str):
62
+ """Auto-coerce CLI string args to Python types."""
63
+ camel = to_camel(key)
64
+ if value.lower() in ("true", "yes", "y"):
65
+ return True
66
+ if value.lower() in ("false", "no", "n"):
67
+ return False
68
+ if camel in ALL_LIST_PARAMS:
69
+ return [v.strip() for v in value.split(",")]
70
+ try:
71
+ return int(value)
72
+ except ValueError:
73
+ pass
74
+ try:
75
+ return float(value)
76
+ except ValueError:
77
+ pass
78
+ return value
79
+
80
+
81
+ def _print_result(data):
82
+ """Pretty-print tool result."""
83
+ if isinstance(data, (dict, list)):
84
+ console.print_json(json.dumps(data, ensure_ascii=False))
85
+ else:
86
+ console.print(str(data))
87
+
88
+
89
+ # ── Root callback (no command = TUI) ──────────────────────────
90
+
91
+ @app.callback(invoke_without_command=True)
92
+ def main(
93
+ ctx: typer.Context,
94
+ key: Annotated[Optional[str], typer.Option("--key", "-k", help="API 密钥")] = None,
95
+ marketplace: Annotated[str, typer.Option("--marketplace", "-m", help="站点 (默认 US)")] = "US",
96
+ ):
97
+ """卖家精灵 CLI — 不带子命令时进入交互式 TUI。"""
98
+ _state["key"] = key
99
+ _state["marketplace"] = marketplace
100
+ if ctx.invoked_subcommand is None:
101
+ _run_tui()
102
+
103
+
104
+ # ── Interactive TUI ──────────────────────────────────────────
105
+
106
+ def _run_tui():
107
+ """Launch interactive terminal UI."""
108
+ from rich.prompt import Prompt
109
+
110
+ console.print(Panel(
111
+ "[bold cyan]卖家精灵 CLI[/bold cyan] — 交互式模式\n"
112
+ "选择工具分类,然后选择具体工具执行查询",
113
+ title="SellerSprite",
114
+ border_style="cyan",
115
+ ))
116
+
117
+ # Step 1: Select domain
118
+ console.print("\n[bold]选择工具分类:[/bold]")
119
+ domain_keys = list(DOMAINS.keys())
120
+ for i, dk in enumerate(domain_keys, 1):
121
+ tools = DOMAIN_TOOLS.get(dk, [])
122
+ console.print(f" [cyan]{i}[/cyan]. {DOMAINS[dk]} ({len(tools)} 个工具)")
123
+ console.print(f" [cyan]0[/cyan]. 退出")
124
+
125
+ choice = Prompt.ask("\n请选择", choices=[str(i) for i in range(len(domain_keys) + 1)], default="0")
126
+ if choice == "0":
127
+ return
128
+
129
+ domain = domain_keys[int(choice) - 1]
130
+ tool_names = DOMAIN_TOOLS[domain]
131
+
132
+ # Step 2: Select tool
133
+ console.print(f"\n[bold]{DOMAINS[domain]} — 选择工具:[/bold]")
134
+ for i, tn in enumerate(tool_names, 1):
135
+ meta = TOOLS[tn]
136
+ console.print(f" [cyan]{i}[/cyan]. {meta.label} [dim]({tn})[/dim]")
137
+ console.print(f" [cyan]0[/cyan]. 返回")
138
+
139
+ choice = Prompt.ask("\n请选择", choices=[str(i) for i in range(len(tool_names) + 1)], default="0")
140
+ if choice == "0":
141
+ return _run_tui()
142
+
143
+ tool_name = tool_names[int(choice) - 1]
144
+ meta = TOOLS[tool_name]
145
+
146
+ # Step 3: Collect required params
147
+ kwargs = {"marketplace": _state["marketplace"]}
148
+ console.print(f"\n[bold]执行: {meta.label}[/bold] [dim]({tool_name})[/dim]")
149
+
150
+ for param in meta.required:
151
+ val = Prompt.ask(f" [yellow]{param}[/yellow] (必填)")
152
+ kwargs[param] = _coerce_arg(param, val)
153
+
154
+ # Optional params
155
+ console.print(" [dim]输入可选参数 (格式: key=value),直接回车跳过[/dim]")
156
+ while True:
157
+ extra = Prompt.ask(" 可选参数", default="")
158
+ if not extra:
159
+ break
160
+ if "=" in extra:
161
+ k, v = extra.split("=", 1)
162
+ kwargs[k.strip()] = _coerce_arg(k.strip(), v.strip())
163
+
164
+ # Step 4: Execute
165
+ console.print(f"\n[dim]正在查询 {tool_name}...[/dim]")
166
+ try:
167
+ result = _call_tool(tool_name, _state["key"], _state["marketplace"], **kwargs)
168
+ _print_result(result)
169
+ except Exception as e:
170
+ console.print(f"[red]错误: {e}[/red]")
171
+
172
+
173
+ # ── Domain sub-apps ──────────────────────────────────────────
174
+
175
+ asin_app = typer.Typer(help="ASIN 分析 (5 个工具)")
176
+ product_app = typer.Typer(help="商品与竞品 (3 个工具)")
177
+ keyword_app = typer.Typer(help="关键词 (4 个工具)")
178
+ traffic_app = typer.Typer(help="流量 (6 个工具)")
179
+ market_app = typer.Typer(help="市场分析 (14 个工具)")
180
+ trend_app = typer.Typer(help="ABA / 趋势 (4 个工具)")
181
+
182
+ app.add_typer(asin_app, name="asin")
183
+ app.add_typer(product_app, name="product")
184
+ app.add_typer(keyword_app, name="keyword")
185
+ app.add_typer(traffic_app, name="traffic")
186
+ app.add_typer(market_app, name="market")
187
+ app.add_typer(trend_app, name="trend")
188
+
189
+
190
+ # ── Common param annotations ─────────────────────────────────
191
+
192
+ KeyOpt = Annotated[Optional[str], typer.Option("--key", "-k", help="API 密钥")]
193
+ MpOpt = Annotated[str, typer.Option("--marketplace", "-m", help="站点")]
194
+ AsinArg = Annotated[str, typer.Argument(help="ASIN 编号")]
195
+ SizeOpt = Annotated[Optional[int], typer.Option("--size", help="返回条数")]
196
+
197
+
198
+ # ── ASIN commands ─────────────────────────────────────────────
199
+
200
+ @asin_app.command("detail")
201
+ def asin_detail(asin: AsinArg, marketplace: MpOpt = "US", key: KeyOpt = None):
202
+ """查询 ASIN 完整详情"""
203
+ _print_result(_call_tool("asin_detail", key, marketplace, asin=asin))
204
+
205
+
206
+ @asin_app.command("predict")
207
+ def asin_predict(asin: AsinArg, marketplace: MpOpt = "US", key: KeyOpt = None):
208
+ """销量与销售额预测"""
209
+ _print_result(_call_tool("asin_prediction", key, marketplace, asin=asin))
210
+
211
+
212
+ @asin_app.command("coupon")
213
+ def asin_coupon(asin: AsinArg, marketplace: MpOpt = "US", key: KeyOpt = None):
214
+ """优惠价格趋势"""
215
+ _print_result(_call_tool("asin_coupon_trend", key, marketplace, asin=asin))
216
+
217
+
218
+ @asin_app.command("detail-coupon")
219
+ def asin_detail_coupon(asin: AsinArg, marketplace: MpOpt = "US", key: KeyOpt = None):
220
+ """详情 + 优惠趋势"""
221
+ _print_result(_call_tool("asin_detail_with_coupon_trend", key, marketplace, asin=asin))
222
+
223
+
224
+ @asin_app.command("keepa")
225
+ def asin_keepa(asin: AsinArg, marketplace: MpOpt = "US", key: KeyOpt = None):
226
+ """Keepa 历史趋势"""
227
+ _print_result(_call_tool("keepa_info", key, marketplace, asin=asin))
228
+
229
+
230
+ # ── Product commands ──────────────────────────────────────────
231
+
232
+ @product_app.command("search")
233
+ def product_search(
234
+ keyword: Annotated[Optional[str], typer.Option("--keyword", help="关键词")] = None,
235
+ min_price: Annotated[Optional[float], typer.Option("--min-price", help="最低价")] = None,
236
+ max_price: Annotated[Optional[float], typer.Option("--max-price", help="最高价")] = None,
237
+ min_units: Annotated[Optional[int], typer.Option("--min-units", help="最低月销")] = None,
238
+ max_units: Annotated[Optional[int], typer.Option("--max-units", help="最高月销")] = None,
239
+ min_rating: Annotated[Optional[float], typer.Option("--min-rating", help="最低评分")] = None,
240
+ max_rating: Annotated[Optional[float], typer.Option("--max-rating", help="最高评分")] = None,
241
+ page: Annotated[Optional[int], typer.Option("--page", help="页码")] = None,
242
+ size: SizeOpt = None,
243
+ marketplace: MpOpt = "US",
244
+ key: KeyOpt = None,
245
+ ):
246
+ """高级商品筛选"""
247
+ _print_result(_call_tool("product_research", key, marketplace,
248
+ keyword=keyword, priceMin=min_price, priceMax=max_price,
249
+ unitsMin=min_units, unitsMax=max_units,
250
+ ratingMin=min_rating, ratingMax=max_rating,
251
+ page=page, size=size))
252
+
253
+
254
+ @product_app.command("competitor")
255
+ def product_competitor(
256
+ asins: Annotated[Optional[str], typer.Option("--asins", help="ASIN 列表 (逗号分隔)")] = None,
257
+ marketplace: MpOpt = "US",
258
+ key: KeyOpt = None,
259
+ ):
260
+ """竞品查询"""
261
+ kwargs = {}
262
+ if asins:
263
+ kwargs["asins"] = [a.strip() for a in asins.split(",")]
264
+ _print_result(_call_tool("competitor_lookup", key, marketplace, **kwargs))
265
+
266
+
267
+ @product_app.command("node")
268
+ def product_node(
269
+ keyword: Annotated[Optional[str], typer.Option("--keyword", help="关键词")] = None,
270
+ marketplace: MpOpt = "US",
271
+ key: KeyOpt = None,
272
+ ):
273
+ """产品类目查询"""
274
+ _print_result(_call_tool("product_node", key, marketplace, keyword=keyword))
275
+
276
+
277
+ # ── Keyword commands ──────────────────────────────────────────
278
+
279
+ @keyword_app.command("mine")
280
+ def keyword_mine(
281
+ keyword: Annotated[Optional[str], typer.Option("--keyword", help="关键词")] = None,
282
+ keyword_list: Annotated[Optional[str], typer.Option("--keyword-list", help="关键词列表 (逗号分隔)")] = None,
283
+ size: SizeOpt = None,
284
+ marketplace: MpOpt = "US",
285
+ key: KeyOpt = None,
286
+ ):
287
+ """关键词深度挖掘"""
288
+ kwargs = {}
289
+ if keyword:
290
+ kwargs["keyword"] = keyword
291
+ if keyword_list:
292
+ kwargs["keywordList"] = [k.strip() for k in keyword_list.split(",")]
293
+ if size:
294
+ kwargs["size"] = size
295
+ _print_result(_call_tool("keyword_miner", key, marketplace, **kwargs))
296
+
297
+
298
+ @keyword_app.command("research")
299
+ def keyword_research(
300
+ keyword: Annotated[Optional[str], typer.Option("--keyword", help="关键词")] = None,
301
+ keyword_list: Annotated[Optional[str], typer.Option("--keyword-list", help="关键词列表 (逗号分隔)")] = None,
302
+ size: SizeOpt = None,
303
+ marketplace: MpOpt = "US",
304
+ key: KeyOpt = None,
305
+ ):
306
+ """关键词市场选品分析"""
307
+ kwargs = {}
308
+ if keyword:
309
+ kwargs["keyword"] = keyword
310
+ if keyword_list:
311
+ kwargs["keywordList"] = [k.strip() for k in keyword_list.split(",")]
312
+ if size:
313
+ kwargs["size"] = size
314
+ _print_result(_call_tool("keyword_research", key, marketplace, **kwargs))
315
+
316
+
317
+ @keyword_app.command("order")
318
+ def keyword_order(
319
+ asins: Annotated[Optional[str], typer.Option("--asins", help="ASIN 列表 (逗号分隔)")] = None,
320
+ marketplace: MpOpt = "US",
321
+ key: KeyOpt = None,
322
+ ):
323
+ """关键词反查(转化分析)"""
324
+ kwargs = {}
325
+ if asins:
326
+ kwargs["asins"] = [a.strip() for a in asins.split(",")]
327
+ _print_result(_call_tool("keyword_order", key, marketplace, **kwargs))
328
+
329
+
330
+ @keyword_app.command("bsr")
331
+ def keyword_bsr(
332
+ bsr: Annotated[int, typer.Argument(help="BSR 排名")],
333
+ category_id: Annotated[str, typer.Argument(help="类目 ID")],
334
+ marketplace: MpOpt = "US",
335
+ key: KeyOpt = None,
336
+ ):
337
+ """BSR 销量预测"""
338
+ _print_result(_call_tool("bsr_prediction", key, marketplace, bsr=bsr, category_id=category_id))
339
+
340
+
341
+ # ── Traffic commands ──────────────────────────────────────────
342
+
343
+ @traffic_app.command("keyword")
344
+ def traffic_keyword(
345
+ asin_list: Annotated[Optional[str], typer.Option("--asin-list", help="ASIN 列表 (逗号分隔)")] = None,
346
+ marketplace: MpOpt = "US",
347
+ key: KeyOpt = None,
348
+ ):
349
+ """流量关键词明细"""
350
+ kwargs = {}
351
+ if asin_list:
352
+ kwargs["asinList"] = [a.strip() for a in asin_list.split(",")]
353
+ _print_result(_call_tool("traffic_keyword", key, marketplace, **kwargs))
354
+
355
+
356
+ @traffic_app.command("keyword-stat")
357
+ def traffic_keyword_stat(
358
+ asin: AsinArg,
359
+ month: Annotated[Optional[str], typer.Option("--month", help="月份 (YYYY-MM)")] = None,
360
+ marketplace: MpOpt = "US",
361
+ key: KeyOpt = None,
362
+ ):
363
+ """流量关键词统计"""
364
+ _print_result(_call_tool("traffic_keyword_stat", key, marketplace, asin=asin, month=month))
365
+
366
+
367
+ @traffic_app.command("source")
368
+ def traffic_source(
369
+ asin: Annotated[Optional[str], typer.Option("--asin", help="ASIN")] = None,
370
+ marketplace: MpOpt = "US",
371
+ key: KeyOpt = None,
372
+ ):
373
+ """流量来源分析"""
374
+ kwargs = {}
375
+ if asin:
376
+ kwargs["asin"] = asin
377
+ _print_result(_call_tool("traffic_source", key, marketplace, **kwargs))
378
+
379
+
380
+ @traffic_app.command("listing-stat")
381
+ def traffic_listing_stat(
382
+ asin: AsinArg,
383
+ marketplace: MpOpt = "US",
384
+ key: KeyOpt = None,
385
+ ):
386
+ """免费/付费流量结构"""
387
+ _print_result(_call_tool("traffic_listing_stat", key, marketplace, asin=asin))
388
+
389
+
390
+ @traffic_app.command("listing")
391
+ def traffic_listing(
392
+ asin_list: Annotated[str, typer.Option("--asin-list", help="ASIN 列表 (逗号分隔)")],
393
+ relations: Annotated[str, typer.Option("--relations", help="关联类型 (逗号分隔)")],
394
+ marketplace: MpOpt = "US",
395
+ key: KeyOpt = None,
396
+ ):
397
+ """关联商品查询"""
398
+ _print_result(_call_tool("traffic_listing", key, marketplace,
399
+ asin_list=[a.strip() for a in asin_list.split(",")],
400
+ relations=[r.strip() for r in relations.split(",")]))
401
+
402
+
403
+ @traffic_app.command("extend")
404
+ def traffic_extend(
405
+ asin_list: Annotated[str, typer.Option("--asin-list", help="ASIN 列表 (逗号分隔)")],
406
+ marketplace: MpOpt = "US",
407
+ key: KeyOpt = None,
408
+ ):
409
+ """关键词拓展"""
410
+ _print_result(_call_tool("traffic_extend", key, marketplace,
411
+ asin_list=[a.strip() for a in asin_list.split(",")]))
412
+
413
+
414
+ # ── Market commands ───────────────────────────────────────────
415
+
416
+ @market_app.command("research")
417
+ def market_research(
418
+ keyword: Annotated[Optional[str], typer.Option("--keyword", help="关键词")] = None,
419
+ marketplace: MpOpt = "US",
420
+ key: KeyOpt = None,
421
+ ):
422
+ """类目市场分析"""
423
+ _print_result(_call_tool("market_research", key, marketplace, keyword=keyword))
424
+
425
+
426
+ def _make_market_command(tool_name: str, label: str):
427
+ """Factory for market analysis commands that all take node_id_path."""
428
+ def cmd(
429
+ node_id_path: Annotated[str, typer.Option("--node-id-path", help="类目节点路径")],
430
+ marketplace: MpOpt = "US",
431
+ key: KeyOpt = None,
432
+ ):
433
+ _print_result(_call_tool(tool_name, key, marketplace, node_id_path=node_id_path))
434
+ cmd.__doc__ = label
435
+ return cmd
436
+
437
+
438
+ # Dynamically register all node-based market commands
439
+ _NODE_COMMANDS = {
440
+ "market_research_statistics": ("stats", "类目统计深度分析"),
441
+ "market_price_distribution": ("price", "价格区间分布"),
442
+ "market_brand_concentration": ("brand", "品牌集中度"),
443
+ "market_product_concentration": ("product", "商品集中度"),
444
+ "market_seller_concentration": ("seller", "卖家集中度"),
445
+ "market_rating_distribution": ("rating", "评分值分布"),
446
+ "market_ratings_count_distribution": ("ratings-count", "评分数分布"),
447
+ "market_listing_date_distribution": ("listing-date", "上架时间分布"),
448
+ "market_listing_trend_distribution": ("listing-trend", "上架时间趋势"),
449
+ "market_seller_country_distribution": ("seller-country", "卖家所属地分布"),
450
+ "market_seller_type_concentration": ("seller-type", "发货类型分布"),
451
+ "market_ebc_distribution": ("ebc", "A+页面与视频分布"),
452
+ "market_product_demand_trend": ("demand", "需求趋势"),
453
+ }
454
+
455
+ for _tool, (_cmd_name, _label) in _NODE_COMMANDS.items():
456
+ market_app.command(name=_cmd_name)(_make_market_command(_tool, _label))
457
+
458
+
459
+ # ── Trend commands ────────────────────────────────────────────
460
+
461
+ @trend_app.command("aba-weekly")
462
+ def trend_aba_weekly(
463
+ keyword_list: Annotated[Optional[str], typer.Option("--keyword-list", help="关键词列表 (逗号分隔)")] = None,
464
+ marketplace: MpOpt = "US",
465
+ key: KeyOpt = None,
466
+ ):
467
+ """ABA 周度趋势"""
468
+ kwargs = {}
469
+ if keyword_list:
470
+ kwargs["keywordList"] = [k.strip() for k in keyword_list.split(",")]
471
+ _print_result(_call_tool("aba_research_weekly", key, marketplace, **kwargs))
472
+
473
+
474
+ @trend_app.command("aba-monthly")
475
+ def trend_aba_monthly(
476
+ keyword_list: Annotated[Optional[str], typer.Option("--keyword-list", help="关键词列表 (逗号分隔)")] = None,
477
+ marketplace: MpOpt = "US",
478
+ key: KeyOpt = None,
479
+ ):
480
+ """ABA 月度趋势"""
481
+ kwargs = {}
482
+ if keyword_list:
483
+ kwargs["keywordList"] = [k.strip() for k in keyword_list.split(",")]
484
+ _print_result(_call_tool("aba_research_monthly", key, marketplace, **kwargs))
485
+
486
+
487
+ @trend_app.command("google")
488
+ def trend_google(
489
+ keyword: Annotated[Optional[str], typer.Option("--keyword", help="关键词")] = None,
490
+ marketplace: MpOpt = "US",
491
+ key: KeyOpt = None,
492
+ ):
493
+ """Google 搜索趋势"""
494
+ _print_result(_call_tool("google_trend", key, marketplace, keyword=keyword))
495
+
496
+
497
+ @trend_app.command("review")
498
+ def trend_review(
499
+ asin: AsinArg,
500
+ marketplace: MpOpt = "US",
501
+ key: KeyOpt = None,
502
+ ):
503
+ """买家评论查询"""
504
+ _print_result(_call_tool("review", key, marketplace, asin=asin))
505
+
506
+
507
+ # ── List command ──────────────────────────────────────────────
508
+
509
+ # Mapping: tool_name -> CLI command path
510
+ _TOOL_COMMANDS = {
511
+ "asin_detail": "sellersprite asin detail",
512
+ "asin_prediction": "sellersprite asin predict",
513
+ "asin_coupon_trend": "sellersprite asin coupon",
514
+ "asin_detail_with_coupon_trend": "sellersprite asin detail-coupon",
515
+ "keepa_info": "sellersprite asin keepa",
516
+ "product_research": "sellersprite product search",
517
+ "competitor_lookup": "sellersprite product competitor",
518
+ "product_node": "sellersprite product node",
519
+ "keyword_miner": "sellersprite keyword mine",
520
+ "keyword_research": "sellersprite keyword research",
521
+ "keyword_order": "sellersprite keyword order",
522
+ "bsr_prediction": "sellersprite keyword bsr",
523
+ "traffic_keyword": "sellersprite traffic keyword",
524
+ "traffic_keyword_stat": "sellersprite traffic keyword-stat",
525
+ "traffic_source": "sellersprite traffic source",
526
+ "traffic_listing_stat": "sellersprite traffic listing-stat",
527
+ "traffic_listing": "sellersprite traffic listing",
528
+ "traffic_extend": "sellersprite traffic extend",
529
+ "market_research": "sellersprite market research",
530
+ "market_research_statistics": "sellersprite market stats",
531
+ "market_price_distribution": "sellersprite market price",
532
+ "market_brand_concentration": "sellersprite market brand",
533
+ "market_product_concentration": "sellersprite market product",
534
+ "market_seller_concentration": "sellersprite market seller",
535
+ "market_rating_distribution": "sellersprite market rating",
536
+ "market_ratings_count_distribution": "sellersprite market ratings-count",
537
+ "market_listing_date_distribution": "sellersprite market listing-date",
538
+ "market_listing_trend_distribution": "sellersprite market listing-trend",
539
+ "market_seller_country_distribution": "sellersprite market seller-country",
540
+ "market_seller_type_concentration": "sellersprite market seller-type",
541
+ "market_ebc_distribution": "sellersprite market ebc",
542
+ "market_product_demand_trend": "sellersprite market demand",
543
+ "aba_research_weekly": "sellersprite trend aba-weekly",
544
+ "aba_research_monthly": "sellersprite trend aba-monthly",
545
+ "google_trend": "sellersprite trend google",
546
+ "review": "sellersprite trend review",
547
+ }
548
+
549
+
550
+ @app.command("list")
551
+ def list_tools():
552
+ """列出所有 36 个可用工具"""
553
+ for domain, tools in DOMAIN_TOOLS.items():
554
+ table = Table(title=DOMAINS.get(domain, domain), show_header=True,
555
+ header_style="bold cyan")
556
+ table.add_column("命令", style="green")
557
+ table.add_column("说明")
558
+ for tn in tools:
559
+ table.add_row(_TOOL_COMMANDS.get(tn, f"sellersprite {domain} {tn}"), TOOLS[tn].label)
560
+ console.print(table)
561
+ console.print()
562
+ console.print(f"[bold]共 {len(TOOLS)} 个工具[/bold]")
563
+
564
+
565
+ # ── Init command ──────────────────────────────────────────────
566
+
567
+ @app.command("init")
568
+ def init_client(
569
+ client: Annotated[Optional[str], typer.Argument(help="客户端名称")] = None,
570
+ all_clients: Annotated[bool, typer.Option("--all", help="生成所有客户端配置")] = False,
571
+ skills: Annotated[bool, typer.Option("--skills", help="同时复制 Skills 文件")] = False,
572
+ project: Annotated[Optional[str], typer.Option("--project", help="目标项目目录")] = None,
573
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="预览模式")] = False,
574
+ key: KeyOpt = None,
575
+ ):
576
+ """生成 AI 客户端 MCP 配置"""
577
+ if all_clients:
578
+ clients = sorted(CLIENT_NAMES)
579
+ elif client:
580
+ clients = [client]
581
+ else:
582
+ console.print("[red]请指定客户端名称或使用 --all[/red]")
583
+ console.print(f"可用客户端: {', '.join(sorted(CLIENT_NAMES))}")
584
+ raise typer.Exit(1)
585
+
586
+ unknown = [c for c in clients if c not in CLIENT_NAMES]
587
+ if unknown:
588
+ console.print(f"[red]未知客户端: {', '.join(unknown)}[/red]")
589
+ raise typer.Exit(1)
590
+
591
+ project_dir = Path(project).resolve() if project else Path.cwd()
592
+ api_key = _resolve_key(key)
593
+
594
+ for name in clients:
595
+ msgs = generate(name, api_key, project_dir, skills=skills, dry_run=dry_run)
596
+ for msg in msgs:
597
+ if msg.startswith(" wrote:") or msg.startswith(" copied:"):
598
+ console.print(f"[green]{msg}[/green]")
599
+ elif msg.startswith(" ["):
600
+ console.print(f"[yellow]{msg}[/yellow]")
601
+ else:
602
+ console.print(f"[bold]{msg}[/bold]")
603
+ console.print()
604
+
605
+
606
+ # ── Skill commands ────────────────────────────────────────────
607
+
608
+ @app.command("skill")
609
+ def skill_cmd(
610
+ action: Annotated[Optional[str], typer.Argument(help="list | show")] = None,
611
+ name: Annotated[Optional[str], typer.Option("--name", "-n", help="Skill 名称")] = None,
612
+ ):
613
+ """查看 Skills 技能卡片"""
614
+ if action == "list" or action is None:
615
+ _skill_list()
616
+ elif action == "show":
617
+ if not name:
618
+ console.print("[red]请指定 --name[/red]")
619
+ raise typer.Exit(1)
620
+ _skill_show(name)
621
+ else:
622
+ console.print(f"[red]未知操作: {action}[/red]")
623
+ console.print("可用操作: list, show")
624
+ raise typer.Exit(1)
625
+
626
+
627
+ def _skill_list():
628
+ """List all skills in a table."""
629
+ by_category = get_skills_by_category()
630
+ for cat, cards in by_category.items():
631
+ cat_label = CATEGORY_NAMES.get(cat, cat)
632
+ table = Table(title=cat_label, show_header=True, header_style="bold cyan")
633
+ table.add_column("名称", style="green")
634
+ table.add_column("文件")
635
+ for card in cards:
636
+ table.add_row(card.name, str(card.path.name))
637
+ console.print(table)
638
+ console.print()
639
+ console.print(f"[bold]共 {sum(len(c) for c in by_category.values())} 个 Skills[/bold]")
640
+
641
+
642
+ def _skill_show(name: str):
643
+ """Show a specific skill's content."""
644
+ for card in load_skills():
645
+ if card.name == name:
646
+ console.print(Panel(card.content, title=card.name, border_style="cyan"))
647
+ return
648
+ console.print(f"[red]未找到 Skill: {name}[/red]")
649
+ console.print("使用 [bold]sellersprite skill list[/bold] 查看所有可用 Skills")
@@ -0,0 +1,19 @@
1
+ """MCP and API error types."""
2
+
3
+
4
+ class McpError(Exception):
5
+ """JSON-RPC / transport layer error."""
6
+
7
+ def __init__(self, err: dict):
8
+ self.code = err.get("code")
9
+ self.message = err.get("message", "")
10
+ super().__init__(f"MCP Error {self.code}: {self.message}")
11
+
12
+
13
+ class ApiError(Exception):
14
+ """Business logic error from the SellerSprite API."""
15
+
16
+ def __init__(self, code: str, message: str):
17
+ self.code = code
18
+ self.message = message
19
+ super().__init__(f"API Error {code}: {message}")