aukeys-opscli 0.0.6__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.
- aukeys_opscli-0.0.6.dist-info/METADATA +15 -0
- aukeys_opscli-0.0.6.dist-info/RECORD +92 -0
- aukeys_opscli-0.0.6.dist-info/WHEEL +4 -0
- aukeys_opscli-0.0.6.dist-info/entry_points.txt +2 -0
- opscli/__init__.py +9 -0
- opscli/amazon/__init__.py +13 -0
- opscli/amazon/cli.py +5 -0
- opscli/amazon/client.py +5 -0
- opscli/amazon/commands/__init__.py +1 -0
- opscli/amazon/commands/cli.py +200 -0
- opscli/amazon/domain/__init__.py +25 -0
- opscli/amazon/domain/exceptions.py +73 -0
- opscli/amazon/domain/models.py +74 -0
- opscli/amazon/exceptions.py +21 -0
- opscli/amazon/manager.py +5 -0
- opscli/amazon/models.py +5 -0
- opscli/amazon/parser.py +5 -0
- opscli/amazon/scraper.py +5 -0
- opscli/amazon/scraping/__init__.py +12 -0
- opscli/amazon/scraping/parser.py +66 -0
- opscli/amazon/scraping/scraper.py +315 -0
- opscli/amazon/services/__init__.py +5 -0
- opscli/amazon/services/manager.py +105 -0
- opscli/amazon/transport/__init__.py +5 -0
- opscli/amazon/transport/client.py +77 -0
- opscli/auth/__init__.py +79 -0
- opscli/auth/cli.py +255 -0
- opscli/auth/commands/__init__.py +1 -0
- opscli/auth/commands/cli.py +5 -0
- opscli/auth/config.py +55 -0
- opscli/auth/core/__init__.py +1 -0
- opscli/auth/core/device_flow.py +80 -0
- opscli/auth/core/system_registry.py +89 -0
- opscli/auth/core/token_manager.py +167 -0
- opscli/auth/domain/__init__.py +23 -0
- opscli/auth/domain/exceptions.py +33 -0
- opscli/auth/exceptions.py +23 -0
- opscli/auth/storage/__init__.py +1 -0
- opscli/auth/storage/credential_store.py +163 -0
- opscli/auth/storage/crypto.py +67 -0
- opscli/cli.py +36 -0
- opscli/config.py +3 -0
- opscli/query/__init__.py +6 -0
- opscli/query/cli.py +5 -0
- opscli/query/client.py +5 -0
- opscli/query/commands/__init__.py +1 -0
- opscli/query/commands/cli.py +140 -0
- opscli/query/domain/__init__.py +23 -0
- opscli/query/domain/exceptions.py +73 -0
- opscli/query/domain/models.py +21 -0
- opscli/query/exceptions.py +21 -0
- opscli/query/manager.py +5 -0
- opscli/query/models.py +5 -0
- opscli/query/services/__init__.py +5 -0
- opscli/query/services/manager.py +373 -0
- opscli/query/transport/__init__.py +5 -0
- opscli/query/transport/client.py +57 -0
- opscli/skills/__init__.py +37 -0
- opscli/skills/cli.py +5 -0
- opscli/skills/commands/__init__.py +1 -0
- opscli/skills/commands/cli.py +389 -0
- opscli/skills/detector.py +5 -0
- opscli/skills/discovery/__init__.py +5 -0
- opscli/skills/discovery/detector.py +225 -0
- opscli/skills/domain/__init__.py +23 -0
- opscli/skills/domain/exceptions.py +51 -0
- opscli/skills/domain/models.py +144 -0
- opscli/skills/exceptions.py +5 -0
- opscli/skills/manager.py +5 -0
- opscli/skills/models.py +19 -0
- opscli/skills/services/__init__.py +5 -0
- opscli/skills/services/manager.py +276 -0
- opscli/skills/sync/__init__.py +5 -0
- opscli/skills/sync/updater.py +274 -0
- opscli/skills/templates/ops-amazon/SKILL.md +181 -0
- opscli/skills/templates/ops-amazon/data/VERSION.json +4 -0
- opscli/skills/templates/ops-auth/SKILL.md +466 -0
- opscli/skills/templates/ops-auth/data/VERSION.json +4 -0
- opscli/skills/templates/ops-dataset-query/SKILL.md +691 -0
- opscli/skills/templates/ops-dataset-query/data/VERSION.json +4 -0
- opscli/skills/templates/ops-dataset-query/data/dataset_fields.csv +1 -0
- opscli/skills/templates/ops-dataset-query/data/datasets.csv +1 -0
- opscli/skills/templates/ops-dataset-query/data/query_metadata.json +4 -0
- opscli/skills/templates/ops-dataset-query/references//346/225/260/346/215/256/346/237/245/350/257/242/346/234/215/345/212/241/345/274/200/345/217/221/350/257/264/346/230/216/346/226/207/346/241/243.md +1126 -0
- opscli/skills/templates/ops-dataset-query/scripts/core.py +140 -0
- opscli/skills/templates/ops-dataset-query/scripts/query.py +145 -0
- opscli/skills/templates/ops-dataset-query/scripts/search.py +36 -0
- opscli/skills/templates/ops-dataset-query/scripts/updater.py +106 -0
- opscli/skills/templates/ops-skills/SKILL.md +494 -0
- opscli/skills/templates/ops-skills/data/VERSION.json +4 -0
- opscli/skills/updater.py +5 -0
- opscli/version.py +19 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Skill 远端数据更新器。
|
|
2
|
+
|
|
3
|
+
负责从运营系统后端拉取最新的 ops-dataset-query 数据,
|
|
4
|
+
通过"先写临时目录、再原子替换"的策略安全地更新本地 Skill 数据文件。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from tempfile import TemporaryDirectory
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from opscli.auth import AuthClient, OPS_URL
|
|
16
|
+
from opscli.auth.exceptions import AuthError
|
|
17
|
+
from opscli.skills.domain.exceptions import SkillRemoteError
|
|
18
|
+
from opscli.skills.domain.models import SkillRecord, SkillUpgradeResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SkillsUpdater:
|
|
22
|
+
"""Skill 数据更新器,从远端拉取最新数据并更新本地安装。
|
|
23
|
+
|
|
24
|
+
当前仅支持 ops-dataset-query 这一个 Skill 的远端更新。
|
|
25
|
+
更新流程采用临时目录 + 原子替换策略,确保更新失败不会损坏已有数据。
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# 运营系统后端 API 端点
|
|
29
|
+
MANIFEST_ENDPOINT = "/v1/data-metrics/datasets/skill/manifest" # 版本清单
|
|
30
|
+
FIELDS_ENDPOINT = "/v1/data-metrics/datasets/skill/export" # 字段数据导出
|
|
31
|
+
DATASETS_ENDPOINT = "/v1/data-metrics/datasets/skill/export-datasets" # 数据集导出
|
|
32
|
+
QUERY_METADATA_ENDPOINT = "/v1/data-metrics/datasets/query-metadata" # 查询元数据
|
|
33
|
+
|
|
34
|
+
def build_remote_summary(self, skill_name: str) -> dict:
|
|
35
|
+
"""构建远端状态摘要,供 `skills status` 与调试输出复用。"""
|
|
36
|
+
manifest = self.fetch_manifest(skill_name)
|
|
37
|
+
return {
|
|
38
|
+
"skill_name": skill_name,
|
|
39
|
+
"ops_url": OPS_URL,
|
|
40
|
+
"manifest_endpoint": f"{OPS_URL}{self.MANIFEST_ENDPOINT}",
|
|
41
|
+
"manifest": manifest,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def fetch_manifest(self, skill_name: str) -> dict | None:
|
|
45
|
+
"""获取远端 Skill 的版本清单(manifest)。
|
|
46
|
+
|
|
47
|
+
当前仅支持 ops-dataset-query,其他 Skill 返回 None。
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
manifest 数据字典,包含 version 等字段;不存在时返回 None
|
|
51
|
+
"""
|
|
52
|
+
if skill_name != "ops-dataset-query":
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
response = self._get(self.MANIFEST_ENDPOINT)
|
|
56
|
+
payload = self._parse_json_response(response, endpoint=self.MANIFEST_ENDPOINT)
|
|
57
|
+
return payload.get("data")
|
|
58
|
+
|
|
59
|
+
def compare_versions(self, current: str, remote: str) -> int:
|
|
60
|
+
"""比较两个语义化版本号。
|
|
61
|
+
|
|
62
|
+
支持带 "v" 前缀的版本号(如 v1.2.3),最多解析三段(major.minor.patch)。
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
-1 表示 current < remote(需要更新)
|
|
66
|
+
0 表示相等
|
|
67
|
+
1 表示 current > remote
|
|
68
|
+
"""
|
|
69
|
+
def parse(value: str) -> tuple[int, int, int]:
|
|
70
|
+
"""将版本字符串解析为 (major, minor, patch) 三元组。"""
|
|
71
|
+
normalized = value.strip()
|
|
72
|
+
if normalized.startswith("v"):
|
|
73
|
+
normalized = normalized[1:]
|
|
74
|
+
parts = normalized.split(".")
|
|
75
|
+
ints = [int(part) if part.isdigit() else 0 for part in parts[:3]]
|
|
76
|
+
# 不足三段的补零,如 "1.2" → (1, 2, 0)
|
|
77
|
+
while len(ints) < 3:
|
|
78
|
+
ints.append(0)
|
|
79
|
+
return ints[0], ints[1], ints[2]
|
|
80
|
+
|
|
81
|
+
current_parts = parse(current or "v0.0.0")
|
|
82
|
+
remote_parts = parse(remote or "v0.0.0")
|
|
83
|
+
if current_parts < remote_parts:
|
|
84
|
+
return -1
|
|
85
|
+
if current_parts > remote_parts:
|
|
86
|
+
return 1
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
def upgrade_ops_dataset_query(self, record: SkillRecord, force: bool = False) -> SkillUpgradeResult:
|
|
90
|
+
"""执行 ops-dataset-query Skill 的数据升级。
|
|
91
|
+
|
|
92
|
+
流程:
|
|
93
|
+
1. 拉取远端 manifest 获取最新版本号
|
|
94
|
+
2. 对比本地版本,判断是否需要更新
|
|
95
|
+
3. 如需更新,拉取所有数据文件(CSV + JSON)
|
|
96
|
+
4. 先写入临时目录,再原子替换到目标目录,避免中途失败导致数据损坏
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
record: 本地已安装的 Skill 记录
|
|
100
|
+
force: 为 True 时即使版本号相同也强制更新
|
|
101
|
+
"""
|
|
102
|
+
manifest = self.fetch_manifest(record.name)
|
|
103
|
+
if not manifest:
|
|
104
|
+
raise ValueError("远端 manifest 不存在")
|
|
105
|
+
|
|
106
|
+
remote_version = str(manifest.get("version", "v0.0.0"))
|
|
107
|
+
current_version = record.version
|
|
108
|
+
needs_update = force or self.compare_versions(current_version, remote_version) < 0
|
|
109
|
+
|
|
110
|
+
# 版本一致且未强制更新,直接返回
|
|
111
|
+
if not needs_update:
|
|
112
|
+
return SkillUpgradeResult(
|
|
113
|
+
name=record.name,
|
|
114
|
+
from_version=current_version,
|
|
115
|
+
to_version=current_version,
|
|
116
|
+
runtime=record.runtime,
|
|
117
|
+
target_dir=record.root,
|
|
118
|
+
updated=False,
|
|
119
|
+
field_count=self._extract_field_count(manifest=manifest),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# 拉取所有远端数据文件
|
|
123
|
+
fields_csv = self._get(self.FIELDS_ENDPOINT).text
|
|
124
|
+
datasets_csv = self._get(self.DATASETS_ENDPOINT).text
|
|
125
|
+
query_metadata = self._parse_json_response(
|
|
126
|
+
self._get(self.QUERY_METADATA_ENDPOINT),
|
|
127
|
+
endpoint=self.QUERY_METADATA_ENDPOINT,
|
|
128
|
+
)
|
|
129
|
+
field_count = self._extract_field_count(query_metadata=query_metadata, manifest=manifest)
|
|
130
|
+
|
|
131
|
+
data_dir = record.root / "data"
|
|
132
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
|
|
134
|
+
# 先写入临时目录,再原子替换,确保更新过程安全
|
|
135
|
+
with TemporaryDirectory() as tmp_dir:
|
|
136
|
+
tmp_path = Path(tmp_dir)
|
|
137
|
+
(tmp_path / "dataset_fields.csv").write_text(fields_csv, encoding="utf-8")
|
|
138
|
+
(tmp_path / "datasets.csv").write_text(datasets_csv, encoding="utf-8")
|
|
139
|
+
(tmp_path / "query_metadata.json").write_text(
|
|
140
|
+
json.dumps(query_metadata.get("data"), ensure_ascii=False, indent=2),
|
|
141
|
+
encoding="utf-8",
|
|
142
|
+
)
|
|
143
|
+
(tmp_path / "VERSION.json").write_text(
|
|
144
|
+
json.dumps(
|
|
145
|
+
{
|
|
146
|
+
"name": record.name,
|
|
147
|
+
"version": remote_version,
|
|
148
|
+
},
|
|
149
|
+
ensure_ascii=False,
|
|
150
|
+
indent=2,
|
|
151
|
+
),
|
|
152
|
+
encoding="utf-8",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# 原子替换:Path.replace() 在同文件系统上是原子操作
|
|
156
|
+
for filename in ["dataset_fields.csv", "datasets.csv", "query_metadata.json", "VERSION.json"]:
|
|
157
|
+
(tmp_path / filename).replace(data_dir / filename)
|
|
158
|
+
|
|
159
|
+
return SkillUpgradeResult(
|
|
160
|
+
name=record.name,
|
|
161
|
+
from_version=current_version,
|
|
162
|
+
to_version=remote_version,
|
|
163
|
+
runtime=record.runtime,
|
|
164
|
+
target_dir=record.root,
|
|
165
|
+
updated=True,
|
|
166
|
+
field_count=field_count,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def _extract_field_count(
|
|
170
|
+
self,
|
|
171
|
+
*,
|
|
172
|
+
query_metadata: dict | None = None,
|
|
173
|
+
manifest: dict | None = None,
|
|
174
|
+
) -> int:
|
|
175
|
+
"""优先从 query-metadata 中提取字段数,失败时回退到 manifest。"""
|
|
176
|
+
if query_metadata:
|
|
177
|
+
data = query_metadata.get("data")
|
|
178
|
+
if isinstance(data, dict):
|
|
179
|
+
fields = data.get("fields")
|
|
180
|
+
if isinstance(fields, list):
|
|
181
|
+
return len(fields)
|
|
182
|
+
|
|
183
|
+
if manifest and isinstance(manifest.get("field_count"), int):
|
|
184
|
+
return int(manifest["field_count"])
|
|
185
|
+
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
def _get(self, endpoint: str) -> httpx.Response:
|
|
189
|
+
"""发送带认证的 GET 请求到运营系统后端。
|
|
190
|
+
|
|
191
|
+
自动带上 ops 系统的统一认证参数用于鉴权。
|
|
192
|
+
"""
|
|
193
|
+
auth_client = AuthClient()
|
|
194
|
+
try:
|
|
195
|
+
headers, cookies = auth_client.build_request_auth("ops")
|
|
196
|
+
except AuthError as exc:
|
|
197
|
+
raise SkillRemoteError(
|
|
198
|
+
"未登录 ops,请先执行 `opscli auth login`",
|
|
199
|
+
endpoint=endpoint,
|
|
200
|
+
) from exc
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
response = httpx.get(
|
|
204
|
+
f"{OPS_URL}{endpoint}",
|
|
205
|
+
headers=headers,
|
|
206
|
+
cookies=cookies,
|
|
207
|
+
timeout=20,
|
|
208
|
+
follow_redirects=True,
|
|
209
|
+
)
|
|
210
|
+
response.raise_for_status()
|
|
211
|
+
return response
|
|
212
|
+
except httpx.HTTPStatusError as exc:
|
|
213
|
+
raise SkillRemoteError(
|
|
214
|
+
self._format_http_error(endpoint, exc.response.status_code),
|
|
215
|
+
endpoint=endpoint,
|
|
216
|
+
status_code=exc.response.status_code,
|
|
217
|
+
) from exc
|
|
218
|
+
except httpx.HTTPError as exc:
|
|
219
|
+
raise SkillRemoteError(
|
|
220
|
+
f"请求远端 Skill 服务失败: {exc}",
|
|
221
|
+
endpoint=endpoint,
|
|
222
|
+
) from exc
|
|
223
|
+
|
|
224
|
+
def _parse_json_response(self, response: httpx.Response, *, endpoint: str) -> dict:
|
|
225
|
+
"""解析 JSON 响应,并在格式错误时给出清晰提示。"""
|
|
226
|
+
try:
|
|
227
|
+
payload = response.json()
|
|
228
|
+
except ValueError as exc:
|
|
229
|
+
raise SkillRemoteError(
|
|
230
|
+
f"远端接口返回了无法解析的 JSON: {endpoint}",
|
|
231
|
+
endpoint=endpoint,
|
|
232
|
+
status_code=response.status_code,
|
|
233
|
+
) from exc
|
|
234
|
+
|
|
235
|
+
if not isinstance(payload, dict):
|
|
236
|
+
raise SkillRemoteError(
|
|
237
|
+
f"远端接口返回格式异常: {endpoint}",
|
|
238
|
+
endpoint=endpoint,
|
|
239
|
+
status_code=response.status_code,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
business_code = payload.get("code")
|
|
243
|
+
if business_code not in (None, 200):
|
|
244
|
+
raise SkillRemoteError(
|
|
245
|
+
self._format_business_error(endpoint, business_code, payload.get("msg")),
|
|
246
|
+
endpoint=endpoint,
|
|
247
|
+
status_code=int(business_code) if isinstance(business_code, int) else None,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return payload
|
|
251
|
+
|
|
252
|
+
def _format_http_error(self, endpoint: str, status_code: int) -> str:
|
|
253
|
+
"""将常见 HTTP 状态码转换为更友好的错误信息。"""
|
|
254
|
+
if status_code == 401:
|
|
255
|
+
return f"远端 Skill 接口鉴权失败,请重新登录后重试: {endpoint}"
|
|
256
|
+
if status_code == 403:
|
|
257
|
+
return f"当前账号无权访问远端 Skill 接口: {endpoint}"
|
|
258
|
+
if status_code == 404:
|
|
259
|
+
return f"远端环境未部署该 Skill 接口: {OPS_URL}{endpoint}"
|
|
260
|
+
if status_code >= 500:
|
|
261
|
+
return f"远端 Skill 服务异常(HTTP {status_code}): {OPS_URL}{endpoint}"
|
|
262
|
+
return f"远端 Skill 接口请求失败(HTTP {status_code}): {OPS_URL}{endpoint}"
|
|
263
|
+
|
|
264
|
+
def _format_business_error(self, endpoint: str, code: int | str, message: str | None) -> str:
|
|
265
|
+
"""将业务层 code/msg 包装为更清晰的远端错误。"""
|
|
266
|
+
if code == 404:
|
|
267
|
+
return f"远端 Skill 数据尚未发布: {message or endpoint}"
|
|
268
|
+
if code == 401:
|
|
269
|
+
return f"远端 Skill 接口鉴权失败,请重新登录后重试: {message or endpoint}"
|
|
270
|
+
if code == 403:
|
|
271
|
+
return f"当前账号无权访问远端 Skill 接口: {message or endpoint}"
|
|
272
|
+
if code == 422:
|
|
273
|
+
return f"远端 Skill 接口参数校验失败: {message or endpoint}"
|
|
274
|
+
return f"远端 Skill 接口返回业务错误(code={code}): {message or endpoint}"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ops-amazon
|
|
3
|
+
description: 使用 opscli amazon 命令抓取 Amazon 商品页和搜索结果,并输出用于 ops API 与数据表设计的标准样本
|
|
4
|
+
version: v0.1.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ops-amazon
|
|
8
|
+
|
|
9
|
+
使用 `opscli amazon` 子命令抓取 Amazon 商品页快照、搜索结果页样本和预留给 ops API 的标准 payload。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 使用原则
|
|
14
|
+
|
|
15
|
+
- 本 Skill 只负责指导 AI 使用 `opscli amazon` 正式命令完成抓取、取样和字段确认
|
|
16
|
+
- 抓取动作统一通过 `opscli amazon` 执行,禁止在 Skill 内直接调用 Amazon HTTP 接口
|
|
17
|
+
- 当前阶段以“先抓数据、API 预留”为主,不要求直接提交到 ops API
|
|
18
|
+
- 若目标是后端设计,优先使用 `scrape`、`payload`、`search`、`schema` 四个命令取样
|
|
19
|
+
- 搜索结果页的 `review_count_value` 应视为近似值;商品页 `scrape` 的评论数更适合作为精确快照值
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 前置要求
|
|
24
|
+
|
|
25
|
+
首次使用前请确认本地已安装 Amazon 抓取依赖:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install "opscli[amazon]"
|
|
29
|
+
playwright install chromium
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
如果是源码开发环境:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e ".[amazon]"
|
|
36
|
+
playwright install chromium
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 何时使用
|
|
42
|
+
|
|
43
|
+
以下场景建议使用 `ops-amazon`:
|
|
44
|
+
|
|
45
|
+
- 需要抓取某个 Amazon 商品的价格、评分、评论数、配送位置
|
|
46
|
+
- 需要抓取关键词搜索结果做竞品样本分析
|
|
47
|
+
- 需要输出未来提交给 ops API 的标准 payload
|
|
48
|
+
- 需要拿真实样本给后端做字段设计、表结构设计、接口设计
|
|
49
|
+
- 需要查看某个商品的本地历史抓取记录
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 核心命令
|
|
54
|
+
|
|
55
|
+
### `opscli amazon scrape`
|
|
56
|
+
|
|
57
|
+
抓取单个商品页。
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
opscli amazon scrape --asin B09LCJPZ1P
|
|
61
|
+
opscli amazon scrape --asin B09LCJPZ1P --zip-code 10001 --include-raw --pretty
|
|
62
|
+
opscli amazon scrape --asin B09LCJPZ1P --no-save-history
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
适用场景:
|
|
66
|
+
|
|
67
|
+
- 获取商品页精确快照
|
|
68
|
+
- 对比价格、评分、评论数变化
|
|
69
|
+
- 给后端确认商品快照字段结构
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### `opscli amazon payload`
|
|
74
|
+
|
|
75
|
+
抓取商品页,并输出预留给 ops API 的标准请求体。
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
opscli amazon payload --asin B09LCJPZ1P
|
|
79
|
+
opscli amazon payload --asin B09LCJPZ1P --zip-code 10001 --pretty
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
适用场景:
|
|
83
|
+
|
|
84
|
+
- 设计商品快照提交接口
|
|
85
|
+
- 给后端确认最终 payload 契约
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### `opscli amazon search`
|
|
90
|
+
|
|
91
|
+
抓取关键词搜索结果页。
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
opscli amazon search --keyword "usb c cable"
|
|
95
|
+
opscli amazon search --keyword "usb c cable" --zip-code 10001 --limit 10 --pretty
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
适用场景:
|
|
99
|
+
|
|
100
|
+
- 采集竞品结果集
|
|
101
|
+
- 设计搜索批次表和搜索结果表
|
|
102
|
+
- 验证关键词结果样本
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### `opscli amazon schema`
|
|
107
|
+
|
|
108
|
+
输出当前字段契约。
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
opscli amazon schema --pretty
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
适用场景:
|
|
115
|
+
|
|
116
|
+
- 对照字段类型
|
|
117
|
+
- 做 API 请求体验收
|
|
118
|
+
- 做数据库字段映射
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### `opscli amazon history`
|
|
123
|
+
|
|
124
|
+
读取某个商品的本地历史抓取记录。
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
opscli amazon history --asin B09LCJPZ1P --pretty
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
默认历史路径:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
~/.config/opscli/amazon/history/<ASIN>.jsonl
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 推荐工作流
|
|
139
|
+
|
|
140
|
+
### 场景一:做商品快照字段设计
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
opscli amazon scrape --asin B09LCJPZ1P --include-raw --pretty
|
|
144
|
+
opscli amazon payload --asin B09LCJPZ1P --pretty
|
|
145
|
+
opscli amazon schema --pretty
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 场景二:做搜索结果表设计
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
opscli amazon search --keyword "usb c cable" --limit 10 --pretty
|
|
152
|
+
opscli amazon schema --pretty
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 场景三:查看历史变化
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
opscli amazon scrape --asin B09LCJPZ1P --pretty
|
|
159
|
+
opscli amazon history --asin B09LCJPZ1P --pretty
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 字段口径提醒
|
|
165
|
+
|
|
166
|
+
- 商品页 `review_count_value`:优先视为精确值
|
|
167
|
+
- 搜索页 `review_count_value`:通常是页面展示缩写解析值,适合做近似分析
|
|
168
|
+
- `location`:当前已做零宽字符清洗
|
|
169
|
+
- `raw`:建议在字段调试和后端设计阶段保留输出
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 当前边界
|
|
174
|
+
|
|
175
|
+
本 Skill 一期不负责:
|
|
176
|
+
|
|
177
|
+
- 直接提交到 ops API
|
|
178
|
+
- 本地定时调度
|
|
179
|
+
- 多商品批量任务编排
|
|
180
|
+
|
|
181
|
+
这些能力后续应继续收口到 `opscli amazon` 模块本身,而不是在 Skill 侧分叉实现。
|