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.
- sellersprite_cli/__init__.py +8 -0
- sellersprite_cli/cli.py +649 -0
- sellersprite_cli/errors.py +19 -0
- sellersprite_cli/generators.py +295 -0
- sellersprite_cli/markdown_utils.py +67 -0
- sellersprite_cli/mcp_client.py +262 -0
- sellersprite_cli/registry.py +90 -0
- sellersprite_cli/skills/README.md +106 -0
- sellersprite_cli/skills/agent-instructions.md +68 -0
- sellersprite_cli/skills/category-structure/high-margin-lightweight.md +64 -0
- sellersprite_cli/skills/category-structure/high-new-product-ratio.md +60 -0
- sellersprite_cli/skills/category-structure/low-brand-monopoly.md +63 -0
- sellersprite_cli/skills/comprehensive/ad-optimizer.md +169 -0
- sellersprite_cli/skills/comprehensive/competitor-analysis.md +149 -0
- sellersprite_cli/skills/comprehensive/keyword-research.md +132 -0
- sellersprite_cli/skills/comprehensive/listing-optimizer.md +167 -0
- sellersprite_cli/skills/comprehensive/market-analysis.md +144 -0
- sellersprite_cli/skills/comprehensive/opportunity-finder.md +154 -0
- sellersprite_cli/skills/comprehensive/pricing-strategy.md +142 -0
- sellersprite_cli/skills/comprehensive/product-research.md +104 -0
- sellersprite_cli/skills/comprehensive/review-insights.md +145 -0
- sellersprite_cli/skills/comprehensive/traffic-analysis.md +167 -0
- sellersprite_cli/skills/edge-opportunity/fbm-intercept.md +62 -0
- sellersprite_cli/skills/edge-opportunity/high-ticket-long-tail.md +65 -0
- sellersprite_cli/skills/edge-opportunity/local-premium-disruption.md +63 -0
- sellersprite_cli/skills/edge-opportunity/poor-listing-winner.md +64 -0
- sellersprite_cli/skills/edge-opportunity/seasonal-prepositioning.md +97 -0
- sellersprite_cli/skills/keyword-trend/aba-high-growth-trend.md +62 -0
- sellersprite_cli/skills/keyword-trend/low-monopoly-keyword.md +61 -0
- sellersprite_cli/skills/keyword-trend/title-density-gap.md +73 -0
- sellersprite_cli/skills/new-product-burst/hidden-bestseller.md +60 -0
- sellersprite_cli/skills/new-product-burst/new-product-burst.md +64 -0
- sellersprite_cli/skills/product-defect/hot-low-rating.md +63 -0
- sellersprite_cli/skills/product-defect/review-sentiment.md +75 -0
- sellersprite_cli/skills/traffic-audit/natural-traffic-audit.md +81 -0
- sellersprite_cli/skills/traffic-audit/variant-gap-analysis.md +61 -0
- sellersprite_cli-0.1.0.dist-info/METADATA +166 -0
- sellersprite_cli-0.1.0.dist-info/RECORD +42 -0
- sellersprite_cli-0.1.0.dist-info/WHEEL +5 -0
- sellersprite_cli-0.1.0.dist-info/entry_points.txt +2 -0
- sellersprite_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- sellersprite_cli-0.1.0.dist-info/top_level.txt +1 -0
sellersprite_cli/cli.py
ADDED
|
@@ -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}")
|