goofish-cli 0.1.0__tar.gz → 0.2.1__tar.gz
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.
- goofish_cli-0.2.1/CHANGELOG.md +73 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/PKG-INFO +15 -8
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/README.md +12 -7
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/docs/mcp-setup.md +2 -2
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/pyproject.toml +11 -1
- goofish_cli-0.2.1/src/goofish_cli/__init__.py +9 -0
- goofish_cli-0.2.1/src/goofish_cli/commands/auth/login.py +116 -0
- goofish_cli-0.2.1/src/goofish_cli/commands/item/view.py +182 -0
- goofish_cli-0.2.1/src/goofish_cli/commands/message/list_chats.py +110 -0
- goofish_cli-0.2.1/src/goofish_cli/commands/search/search.py +169 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/__init__.py +4 -0
- goofish_cli-0.2.1/src/goofish_cli/core/browser.py +147 -0
- goofish_cli-0.2.1/src/goofish_cli/core/browser_cookie.py +262 -0
- goofish_cli-0.2.1/src/goofish_cli/core/errors.py +51 -0
- goofish_cli-0.2.1/src/goofish_cli/core/session.py +151 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/ws.py +107 -0
- goofish_cli-0.2.1/src/goofish_cli/mcp_server.py +75 -0
- goofish_cli-0.2.1/tests/__init__.py +0 -0
- goofish_cli-0.2.1/tests/test_auth_login.py +74 -0
- goofish_cli-0.2.1/tests/test_browser_cookie.py +164 -0
- goofish_cli-0.2.1/tests/test_item_view.py +24 -0
- goofish_cli-0.2.1/tests/test_list_chats.py +106 -0
- goofish_cli-0.2.1/tests/test_search.py +36 -0
- goofish_cli-0.2.1/tests/test_session_bootstrap.py +137 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/uv.lock +219 -1
- goofish_cli-0.1.0/CHANGELOG.md +0 -32
- goofish_cli-0.1.0/src/goofish_cli/__init__.py +0 -1
- goofish_cli-0.1.0/src/goofish_cli/commands/auth/login.py +0 -66
- goofish_cli-0.1.0/src/goofish_cli/core/errors.py +0 -31
- goofish_cli-0.1.0/src/goofish_cli/core/session.py +0 -88
- goofish_cli-0.1.0/src/goofish_cli/mcp_server.py +0 -56
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/.env.example +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/.github/pull_request_template.md +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/.github/workflows/ci.yml +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/.github/workflows/publish.yml +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/.gitignore +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/CONTRIBUTING.md +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/LICENSE +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/NOTICE +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/docs/architecture.md +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/docs/compliance.md +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/examples/README.md +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/examples/publish_from_folder.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/examples/watch_to_autoreply.sh +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/cli.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/__init__.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/auth/__init__.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/auth/reset_guard.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/auth/status.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/category/__init__.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/category/recommend.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/item/__init__.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/item/delete.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/item/get.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/item/publish.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/location/__init__.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/location/default.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/media/__init__.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/media/upload.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/message/__init__.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/message/history.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/message/send.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/commands/message/watch.py +0 -0
- {goofish_cli-0.1.0/tests → goofish_cli-0.2.1/src/goofish_cli/commands/search}/__init__.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/guard.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/limiter.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/mtop.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/output.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/registry.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/sign.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/strategy.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/core/token.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/src/goofish_cli/static/goofish_js_version_2.js +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/tests/test_guard.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/tests/test_limiter.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/tests/test_mtop.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/tests/test_publish_payload.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/tests/test_registry.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/tests/test_sign.py +0 -0
- {goofish_cli-0.1.0 → goofish_cli-0.2.1}/tests/test_ws_codec.py +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.1] - 2026-04-22
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- MCP script entry 新增 `goofish-cli`(与 package 名一致),`uvx goofish-cli` 即可直接拉起
|
|
13
|
+
MCP server。Claude Code 等 AI Agent 的 MCP 配置应使用这个入口。`goofish-mcp` 作为
|
|
14
|
+
次级入口保留,但只在本包已装到某 env 时可用(`pip install goofish-cli` /
|
|
15
|
+
`uv tool install goofish-cli` / `uvx --from goofish-cli goofish-mcp`)——单写
|
|
16
|
+
`uvx goofish-mcp` 仍会因 PyPI 无同名包解析失败。
|
|
17
|
+
- `goofish_cli.__version__` 改为从 `importlib.metadata` 动态读取 package 版本,避免和
|
|
18
|
+
`pyproject.toml` 的 `version` 字段 drift(之前硬编码在 `__init__.py` 里,0.2.0 忘了同步)。
|
|
19
|
+
|
|
20
|
+
## [0.2.0] - 2026-04-22
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **浏览器自动化子系统**(`core/browser.py`):基于 Playwright + 系统 Chrome(`channel="chrome"`)
|
|
24
|
+
每次调用独立 tmp profile(`~/.goofish-cli/profiles/chrome-<tmp>/`),退出清理——
|
|
25
|
+
避开 Chrome `SingletonLock` 并发冲突。自动从 `Session.load()` 注入登录态。
|
|
26
|
+
默认 **headful**(headless 会被闲鱼识别为"非法访问"),CI 可 `GOOFISH_HEADLESS=1` 切回。
|
|
27
|
+
- `goofish search items <query>` [`--limit N`]:浏览器路径搜索商品。对标 OpenCLI
|
|
28
|
+
`xianyu/search.js`,打开搜索页 → 自动滚动触发懒加载 → 从 DOM 提卡片。返回
|
|
29
|
+
`rank / item_id / title / price / condition / brand / location / badge / url`。
|
|
30
|
+
- `goofish item view <item_id>`:浏览器视角看商品详情。在商品页上下文里调
|
|
31
|
+
`window.lib.mtop.request('mtop.taobao.idle.pc.detail')`,从 `itemDO/sellerDO/itemLabelExtList`
|
|
32
|
+
抽 20+ 字段(description / want_count / browse_count / 成色 / 品牌 / seller_score /
|
|
33
|
+
reply_ratio_24h / image_urls 等)。和现有 `item get`(CLI 直签,字段浅)并存。
|
|
34
|
+
- `goofish message list-chats`:拉取会话列表(对应网页左栏),返回 `session_id`、对方昵称、
|
|
35
|
+
未读数、最后一条消息摘要、`sessionType`(1=真人 / 3=系统 / 6=互动 / 23=官方通知)。
|
|
36
|
+
AI Agent 可基于 `sessionType=1 and unread>0` 一键过滤"待回复真人会话"。
|
|
37
|
+
- 接口:`mtop.taobao.idlemessage.pc.session.sync` v3.0,入参 `fetchNum`。
|
|
38
|
+
- `list-chats --watch-secs N`:在 baseline 之上连 WS 收 N 秒 `ackDiff(pts=0)` 的历史推送,
|
|
39
|
+
补 h5 `session.sync` 漏掉的 cid(网页左栏 50 条也靠 ACCS 累积,单次 HTTP 拿不到)。每条
|
|
40
|
+
record 带 `source` 字段(`baseline` / `watch`)。watch 记录只有 cid / session_type /
|
|
41
|
+
item_id / ts,要正文要自己调 `message history <cid>`。
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- MCP handler 用 `asyncio.to_thread` 包装同步命令调用,避免 `asyncio.run()` 在已有事件
|
|
45
|
+
循环里抛 `RuntimeError` —— `search items` / `item view` / `list-chats --watch-secs` 现在
|
|
46
|
+
可以通过 MCP 正常调用。
|
|
47
|
+
|
|
48
|
+
## [0.1.0] - 2026-04-21
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
- 初始发布。12 个命令覆盖闲鱼核心运营链路:
|
|
52
|
+
- `auth`:login / status / reset-guard
|
|
53
|
+
- `item`:get / publish / delete
|
|
54
|
+
- `media upload`、`category recommend`、`location default`
|
|
55
|
+
- `message`:watch / history / send(WebSocket IM)
|
|
56
|
+
- `message watch` 三类事件分类输出(JSONL):
|
|
57
|
+
- `event=message`:收到消息正文
|
|
58
|
+
- `event=read`:对方已读回执(带 msg_ids)
|
|
59
|
+
- `event=new_msg`:轻量新消息通知(无正文,需 history 拉取)
|
|
60
|
+
- WebSocket 自动重连 + 指数退避
|
|
61
|
+
- 全局速率限制(令牌桶 1 写/分钟)+ RGV587 熔断器
|
|
62
|
+
- MCP server:`goofish-mcp` 同一命令集自动暴露给 Claude Code / Cursor / Codex
|
|
63
|
+
- 统一输出契约 `--format json/yaml/table/md/csv`
|
|
64
|
+
- 29 个单测
|
|
65
|
+
|
|
66
|
+
### Notes
|
|
67
|
+
- 本版本需要用户手动从浏览器导入 cookie(含 `unb` / `_m_h5_tk` / `x5sec`)
|
|
68
|
+
- 遇到 `RGV587_ERROR` 风控时,需在浏览器完成滑块验证并**重新导出**带 `x5sec` 的 cookie
|
|
69
|
+
|
|
70
|
+
[Unreleased]: https://github.com/fancyboi999/goofish-cli/compare/v0.2.1...HEAD
|
|
71
|
+
[0.2.1]: https://github.com/fancyboi999/goofish-cli/compare/v0.2.0...v0.2.1
|
|
72
|
+
[0.2.0]: https://github.com/fancyboi999/goofish-cli/compare/v0.1.0...v0.2.0
|
|
73
|
+
[0.1.0]: https://github.com/fancyboi999/goofish-cli/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: goofish-cli
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: 闲鱼 CLI — 支持 MCP,未来支持 Claude Skills。为 AI Agent 提供闲鱼自动化基础能力。
|
|
5
5
|
Project-URL: Homepage, https://github.com/fancyboi999/goofish-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/fancyboi999/goofish-cli
|
|
@@ -18,8 +18,10 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: browser-cookie3>=0.20
|
|
21
22
|
Requires-Dist: loguru>=0.7
|
|
22
23
|
Requires-Dist: mcp>=1.2
|
|
24
|
+
Requires-Dist: playwright>=1.58.0
|
|
23
25
|
Requires-Dist: pydantic>=2.6
|
|
24
26
|
Requires-Dist: pyexecjs>=1.5
|
|
25
27
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -56,7 +58,7 @@ Description-Content-Type: text/markdown
|
|
|
56
58
|
**同一份定义**同时输出给三种消费者:
|
|
57
59
|
|
|
58
60
|
- 👨💻 **人类**:`goofish item get 12345 --format table`
|
|
59
|
-
- 🤖 **AI Agent(Claude Code / Cursor / Codex)**:`uvx goofish-
|
|
61
|
+
- 🤖 **AI Agent(Claude Code / Cursor / Codex)**:`uvx goofish-cli` → 自动注册成 MCP tool
|
|
60
62
|
- 🧩 **Claude Skills**(规划中):`skills/` 目录直接放进 Agent
|
|
61
63
|
|
|
62
64
|
> 架构思想来自 [opencli](https://github.com/jackwener/opencli) 的 single-registry 设计。
|
|
@@ -65,7 +67,7 @@ Description-Content-Type: text/markdown
|
|
|
65
67
|
|
|
66
68
|
## ✨ 核心特性
|
|
67
69
|
|
|
68
|
-
- 🔐 **
|
|
70
|
+
- 🔐 **13 个命令覆盖核心链路**:发布、下架、查询、图片上传、AI 类目识别、默认地址、IM 收发 + 会话列表
|
|
69
71
|
- 📡 **真·实时 IM**:WebSocket 长连 + 自动重连 + **三类事件分类输出**
|
|
70
72
|
- `event=message`(收到消息)· `event=read`(已读回执)· `event=new_msg`(轻量通知)
|
|
71
73
|
- 🛡 **内置风控护栏**:令牌桶限流(1 写/分钟)+ RGV587 自动熔断
|
|
@@ -116,6 +118,9 @@ $ goofish list-commands --format table
|
|
|
116
118
|
| `media upload` | 上传图片到闲鱼 CDN | ✅ |
|
|
117
119
|
| `category recommend` | AI 识别商品类目 | ❌ |
|
|
118
120
|
| `location default` | 获取默认发布地址 | ❌ |
|
|
121
|
+
| `message list-chats` | 拉取会话列表(左栏;`--watch-secs N` 叠加 WS 历史推送补漏) | ❌ |
|
|
122
|
+
| `search items` | 搜索闲鱼商品(浏览器路径 Playwright + 系统 Chrome) | ❌ |
|
|
123
|
+
| `item view` | 浏览器视角看商品详情(字段完整,抗风控;`item get` 的姊妹版) | ❌ |
|
|
119
124
|
| `message history` | 拉取会话历史消息 | ❌ |
|
|
120
125
|
| `message send` | 发送文本/图片 | ✅ |
|
|
121
126
|
| `message watch` | 常驻 IM 长连(JSONL 输出) | ❌ |
|
|
@@ -210,7 +215,7 @@ $ goofish item publish \
|
|
|
210
215
|
"mcpServers": {
|
|
211
216
|
"goofish": {
|
|
212
217
|
"command": "uvx",
|
|
213
|
-
"args": ["goofish-
|
|
218
|
+
"args": ["goofish-cli"]
|
|
214
219
|
}
|
|
215
220
|
}
|
|
216
221
|
}
|
|
@@ -226,19 +231,21 @@ Claude 会自动把全部命令看成 tool:`goofish_item_get` / `goofish_item_
|
|
|
226
231
|
|---|---|
|
|
227
232
|
| 11 个核心 mtop 接口 | 发布/下架/查询/图片/类目/地址/IM 全覆盖 |
|
|
228
233
|
| CLI + `--format` 多格式输出 | `json` / `yaml` / `table` / `md` / `csv`,人机两用 |
|
|
229
|
-
| MCP Server | `uvx goofish-
|
|
234
|
+
| MCP Server | `uvx goofish-cli` 一行接入 Claude Code / Cursor |
|
|
230
235
|
| WebSocket 批量 push 全量解码 | 一帧多条消息全部还原,不丢单 |
|
|
231
236
|
| WebSocket 自动重连 | 断线自退避重连,长跑无感知 |
|
|
232
237
|
| 已读回执 / typing / 新消息通知分类 | `/s/sync` 元事件结构化为三类 JSONL |
|
|
233
238
|
| 全局限流 + 风控熔断 | 令牌桶 1 写/分钟 + RGV587 自动熔断 |
|
|
234
|
-
| 单元测试 |
|
|
235
|
-
| 包分发 | `pip install goofish-cli` / `uvx goofish-
|
|
239
|
+
| 单元测试 | 33 个,ruff 零告警 |
|
|
240
|
+
| 包分发 | `pip install goofish-cli` / `uvx goofish-cli` |
|
|
236
241
|
|
|
237
242
|
---
|
|
238
243
|
|
|
239
244
|
## 🗺 Roadmap
|
|
240
245
|
|
|
241
246
|
- [x] v0.1:12 个命令 + MCP + IM 三类事件
|
|
247
|
+
- [x] v0.2:`goofish message list-chats`(会话列表 + sessionType 分类:1 真人 / 3 系统 / 6 互动 / 23 通知;`--watch-secs` 支持合并 WS `ackDiff(pts=0)` 历史推送补齐 h5 接口漏掉的会话)
|
|
248
|
+
- [x] v0.2:浏览器自动化链路(吸纳 [OpenCLI](https://github.com/jackwener/opencli) 精华)—— Playwright + 系统 Chrome 驱动 `goofish search items` / `goofish item view`,抗风控 & 完整字段
|
|
242
249
|
- [ ] v0.2:`goofish message create-chat`(主动与陌生用户建会话)
|
|
243
250
|
- [ ] v0.2:Claude Skills 包装(`skills/` 目录)
|
|
244
251
|
- [ ] v0.3:`goofish order`(订单状态查询 / 发货)
|
|
@@ -254,7 +261,7 @@ cd goofish-cli
|
|
|
254
261
|
uv venv --python 3.11
|
|
255
262
|
uv pip install -e ".[dev]"
|
|
256
263
|
|
|
257
|
-
uv run pytest #
|
|
264
|
+
uv run pytest # 33 测全绿
|
|
258
265
|
uv run ruff check src tests # 零告警
|
|
259
266
|
```
|
|
260
267
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
**同一份定义**同时输出给三种消费者:
|
|
20
20
|
|
|
21
21
|
- 👨💻 **人类**:`goofish item get 12345 --format table`
|
|
22
|
-
- 🤖 **AI Agent(Claude Code / Cursor / Codex)**:`uvx goofish-
|
|
22
|
+
- 🤖 **AI Agent(Claude Code / Cursor / Codex)**:`uvx goofish-cli` → 自动注册成 MCP tool
|
|
23
23
|
- 🧩 **Claude Skills**(规划中):`skills/` 目录直接放进 Agent
|
|
24
24
|
|
|
25
25
|
> 架构思想来自 [opencli](https://github.com/jackwener/opencli) 的 single-registry 设计。
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
## ✨ 核心特性
|
|
30
30
|
|
|
31
|
-
- 🔐 **
|
|
31
|
+
- 🔐 **13 个命令覆盖核心链路**:发布、下架、查询、图片上传、AI 类目识别、默认地址、IM 收发 + 会话列表
|
|
32
32
|
- 📡 **真·实时 IM**:WebSocket 长连 + 自动重连 + **三类事件分类输出**
|
|
33
33
|
- `event=message`(收到消息)· `event=read`(已读回执)· `event=new_msg`(轻量通知)
|
|
34
34
|
- 🛡 **内置风控护栏**:令牌桶限流(1 写/分钟)+ RGV587 自动熔断
|
|
@@ -79,6 +79,9 @@ $ goofish list-commands --format table
|
|
|
79
79
|
| `media upload` | 上传图片到闲鱼 CDN | ✅ |
|
|
80
80
|
| `category recommend` | AI 识别商品类目 | ❌ |
|
|
81
81
|
| `location default` | 获取默认发布地址 | ❌ |
|
|
82
|
+
| `message list-chats` | 拉取会话列表(左栏;`--watch-secs N` 叠加 WS 历史推送补漏) | ❌ |
|
|
83
|
+
| `search items` | 搜索闲鱼商品(浏览器路径 Playwright + 系统 Chrome) | ❌ |
|
|
84
|
+
| `item view` | 浏览器视角看商品详情(字段完整,抗风控;`item get` 的姊妹版) | ❌ |
|
|
82
85
|
| `message history` | 拉取会话历史消息 | ❌ |
|
|
83
86
|
| `message send` | 发送文本/图片 | ✅ |
|
|
84
87
|
| `message watch` | 常驻 IM 长连(JSONL 输出) | ❌ |
|
|
@@ -173,7 +176,7 @@ $ goofish item publish \
|
|
|
173
176
|
"mcpServers": {
|
|
174
177
|
"goofish": {
|
|
175
178
|
"command": "uvx",
|
|
176
|
-
"args": ["goofish-
|
|
179
|
+
"args": ["goofish-cli"]
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
182
|
}
|
|
@@ -189,19 +192,21 @@ Claude 会自动把全部命令看成 tool:`goofish_item_get` / `goofish_item_
|
|
|
189
192
|
|---|---|
|
|
190
193
|
| 11 个核心 mtop 接口 | 发布/下架/查询/图片/类目/地址/IM 全覆盖 |
|
|
191
194
|
| CLI + `--format` 多格式输出 | `json` / `yaml` / `table` / `md` / `csv`,人机两用 |
|
|
192
|
-
| MCP Server | `uvx goofish-
|
|
195
|
+
| MCP Server | `uvx goofish-cli` 一行接入 Claude Code / Cursor |
|
|
193
196
|
| WebSocket 批量 push 全量解码 | 一帧多条消息全部还原,不丢单 |
|
|
194
197
|
| WebSocket 自动重连 | 断线自退避重连,长跑无感知 |
|
|
195
198
|
| 已读回执 / typing / 新消息通知分类 | `/s/sync` 元事件结构化为三类 JSONL |
|
|
196
199
|
| 全局限流 + 风控熔断 | 令牌桶 1 写/分钟 + RGV587 自动熔断 |
|
|
197
|
-
| 单元测试 |
|
|
198
|
-
| 包分发 | `pip install goofish-cli` / `uvx goofish-
|
|
200
|
+
| 单元测试 | 33 个,ruff 零告警 |
|
|
201
|
+
| 包分发 | `pip install goofish-cli` / `uvx goofish-cli` |
|
|
199
202
|
|
|
200
203
|
---
|
|
201
204
|
|
|
202
205
|
## 🗺 Roadmap
|
|
203
206
|
|
|
204
207
|
- [x] v0.1:12 个命令 + MCP + IM 三类事件
|
|
208
|
+
- [x] v0.2:`goofish message list-chats`(会话列表 + sessionType 分类:1 真人 / 3 系统 / 6 互动 / 23 通知;`--watch-secs` 支持合并 WS `ackDiff(pts=0)` 历史推送补齐 h5 接口漏掉的会话)
|
|
209
|
+
- [x] v0.2:浏览器自动化链路(吸纳 [OpenCLI](https://github.com/jackwener/opencli) 精华)—— Playwright + 系统 Chrome 驱动 `goofish search items` / `goofish item view`,抗风控 & 完整字段
|
|
205
210
|
- [ ] v0.2:`goofish message create-chat`(主动与陌生用户建会话)
|
|
206
211
|
- [ ] v0.2:Claude Skills 包装(`skills/` 目录)
|
|
207
212
|
- [ ] v0.3:`goofish order`(订单状态查询 / 发货)
|
|
@@ -217,7 +222,7 @@ cd goofish-cli
|
|
|
217
222
|
uv venv --python 3.11
|
|
218
223
|
uv pip install -e ".[dev]"
|
|
219
224
|
|
|
220
|
-
uv run pytest #
|
|
225
|
+
uv run pytest # 33 测全绿
|
|
221
226
|
uv run ruff check src tests # 零告警
|
|
222
227
|
```
|
|
223
228
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"mcpServers": {
|
|
10
10
|
"goofish": {
|
|
11
11
|
"command": "uvx",
|
|
12
|
-
"args": ["goofish-
|
|
12
|
+
"args": ["goofish-cli"]
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
{
|
|
22
22
|
"mcpServers": {
|
|
23
23
|
"goofish": {
|
|
24
|
-
"command": "/Users/you/Desktop/goofish-cli/.venv/bin/goofish-
|
|
24
|
+
"command": "/Users/you/Desktop/goofish-cli/.venv/bin/goofish-cli"
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "goofish-cli"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "闲鱼 CLI — 支持 MCP,未来支持 Claude Skills。为 AI Agent 提供闲鱼自动化基础能力。"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -30,6 +30,8 @@ dependencies = [
|
|
|
30
30
|
"tenacity>=8.2",
|
|
31
31
|
"mcp>=1.2",
|
|
32
32
|
"websockets>=13",
|
|
33
|
+
"browser-cookie3>=0.20",
|
|
34
|
+
"playwright>=1.58.0",
|
|
33
35
|
]
|
|
34
36
|
|
|
35
37
|
[project.optional-dependencies]
|
|
@@ -42,6 +44,14 @@ dev = [
|
|
|
42
44
|
|
|
43
45
|
[project.scripts]
|
|
44
46
|
goofish = "goofish_cli.cli:app"
|
|
47
|
+
# MCP server 入口。名字与 package 名一致,`uvx goofish-cli` 可直接拉起(uvx 默认按
|
|
48
|
+
# command 名查包,和 scripts 名对齐就不用 --from 绕弯)。这是 Claude Code 等 AI Agent
|
|
49
|
+
# 配 MCP 时的推荐写法。
|
|
50
|
+
goofish-cli = "goofish_cli.mcp_server:main"
|
|
51
|
+
# 保留 `goofish-mcp` 别名只在 **本包已装** 时可用(`pip install goofish-cli` /
|
|
52
|
+
# `uv tool install goofish-cli` / `uvx --from goofish-cli goofish-mcp`)。单独的
|
|
53
|
+
# `uvx goofish-mcp` 仍会因 PyPI 无同名包解析失败 —— 这不是别名能救的,Claude Code
|
|
54
|
+
# MCP 配置必须用上面的 `uvx goofish-cli`。
|
|
45
55
|
goofish-mcp = "goofish_cli.mcp_server:main"
|
|
46
56
|
|
|
47
57
|
[project.urls]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""auth login — 导入闲鱼登录态到 ~/.goofish-cli/cookies.json。
|
|
2
|
+
|
|
3
|
+
默认行为(零参数):从本机所有已装浏览器中自动探测 → 最低认知负荷。
|
|
4
|
+
支持的浏览器:Chrome / Edge / Brave / Chromium / Opera / OperaGX / Vivaldi
|
|
5
|
+
/ Arc / Firefox / LibreWolf / Safari(由 browser_cookie3 提供)。
|
|
6
|
+
|
|
7
|
+
降级路径:
|
|
8
|
+
|
|
9
|
+
- `auth login` 自动 auto-detect(推荐)
|
|
10
|
+
- `auth login --browser edge` 指定单个浏览器
|
|
11
|
+
- `auth login <path>` 从 JSON 文件导入
|
|
12
|
+
- `auth login <cookie_str> --raw` 粘贴 "k=v; k=v" 字符串
|
|
13
|
+
|
|
14
|
+
触发条件:
|
|
15
|
+
1. 所有浏览器都没拿到 → 报错里列出手动兜底方案
|
|
16
|
+
2. 环境变量 GOOFISH_NO_CHROME_BOOTSTRAP=1 只影响其它命令启动时的
|
|
17
|
+
Session.load 自动 bootstrap,不影响本命令。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from goofish_cli.core import Strategy, command
|
|
25
|
+
from goofish_cli.core.errors import AuthRequiredError
|
|
26
|
+
from goofish_cli.core.session import DEFAULT_COOKIE_PATH, write_cookies_json
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@command(
|
|
30
|
+
namespace="auth",
|
|
31
|
+
name="login",
|
|
32
|
+
description="导入登录态(默认从本机浏览器 auto-detect;支持 Chrome/Edge/Brave/Safari/Firefox 等)",
|
|
33
|
+
strategy=Strategy.PUBLIC,
|
|
34
|
+
columns=["source", "path", "unb", "tracknick", "cookies_count"],
|
|
35
|
+
)
|
|
36
|
+
def login(
|
|
37
|
+
source: str | None = None,
|
|
38
|
+
*,
|
|
39
|
+
raw: bool = False,
|
|
40
|
+
browser: str = "auto",
|
|
41
|
+
) -> dict[str, object]:
|
|
42
|
+
target = DEFAULT_COOKIE_PATH
|
|
43
|
+
|
|
44
|
+
if source is None:
|
|
45
|
+
if raw:
|
|
46
|
+
# --raw 要求紧跟 cookie 字符串;如果没传 source,说明用户漏了参数——
|
|
47
|
+
# 不能静默走浏览器 auto-detect,否则 --raw 被吞,用户会困惑。
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"--raw 需要配合 cookie 字符串使用,如:"
|
|
50
|
+
"goofish auth login 'unb=...; _m_h5_tk=...' --raw"
|
|
51
|
+
)
|
|
52
|
+
cookies, source_label = _pull_from_browser(browser)
|
|
53
|
+
elif raw:
|
|
54
|
+
cookies = _parse_raw(source)
|
|
55
|
+
source_label = "raw"
|
|
56
|
+
else:
|
|
57
|
+
p = Path(source).expanduser()
|
|
58
|
+
cookies = _parse_json(p.read_text())
|
|
59
|
+
source_label = f"file:{p}"
|
|
60
|
+
|
|
61
|
+
if "unb" not in cookies or "_m_h5_tk" not in cookies:
|
|
62
|
+
raise AuthRequiredError(
|
|
63
|
+
"cookie 缺失关键字段 unb / _m_h5_tk。"
|
|
64
|
+
"请先在浏览器里登录 https://www.goofish.com 再试。"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
write_cookies_json(target, cookies)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"source": source_label,
|
|
71
|
+
"path": str(target),
|
|
72
|
+
"unb": cookies.get("unb", ""),
|
|
73
|
+
"tracknick": cookies.get("tracknick", ""),
|
|
74
|
+
"cookies_count": len(cookies),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _pull_from_browser(browser: str) -> tuple[dict[str, str], str]:
|
|
79
|
+
from goofish_cli.core.browser_cookie import (
|
|
80
|
+
BrowserCookieError,
|
|
81
|
+
available_browsers,
|
|
82
|
+
extract_goofish_cookies,
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
used, cookies = extract_goofish_cookies(browser=browser)
|
|
86
|
+
return cookies, f"browser:{used}"
|
|
87
|
+
except BrowserCookieError as e:
|
|
88
|
+
supported = ", ".join(available_browsers())
|
|
89
|
+
raise AuthRequiredError(
|
|
90
|
+
f"浏览器登录态导入失败:{e}\n"
|
|
91
|
+
f"兜底方案:\n"
|
|
92
|
+
f" 1. 确认已在任一浏览器里登录 https://www.goofish.com 后重试\n"
|
|
93
|
+
f" 2. 指定具体浏览器:`goofish auth login --browser edge`(支持:{supported})\n"
|
|
94
|
+
f" 3. 手动导出 JSON:`goofish auth login ~/Downloads/cookies.json`\n"
|
|
95
|
+
f" 4. 粘 cookie 字符串:`goofish auth login 'unb=...; _m_h5_tk=...' --raw`"
|
|
96
|
+
) from e
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_raw(raw: str) -> dict[str, str]:
|
|
100
|
+
out: dict[str, str] = {}
|
|
101
|
+
for part in raw.split(";"):
|
|
102
|
+
part = part.strip()
|
|
103
|
+
if not part or "=" not in part:
|
|
104
|
+
continue
|
|
105
|
+
k, _, v = part.partition("=")
|
|
106
|
+
out[k.strip()] = v.strip()
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _parse_json(text: str) -> dict[str, str]:
|
|
111
|
+
data = json.loads(text)
|
|
112
|
+
if isinstance(data, list):
|
|
113
|
+
return {c["name"]: c["value"] for c in data if "name" in c and "value" in c}
|
|
114
|
+
if isinstance(data, dict):
|
|
115
|
+
return {str(k): str(v) for k, v in data.items()}
|
|
116
|
+
raise AuthRequiredError("cookie JSON 格式不识别(需 list 或 dict)")
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""item view — 浏览器视角的商品详情。对标 OpenCLI `xianyu/item.js`。
|
|
2
|
+
|
|
3
|
+
`item get` 已经在 CLI 里直签调 `mtop.taobao.idle.pc.detail` v1.0,字段浅抽(5 项)。
|
|
4
|
+
`item view` 在真实 Chrome 的商品页上下文里调同一 API(走 `window.lib.mtop.request`),
|
|
5
|
+
好处:
|
|
6
|
+
|
|
7
|
+
1. **字段完整**:直接按 OpenCLI 的提取器从 `data.itemDO / data.sellerDO / itemLabelExtList`
|
|
8
|
+
抽 20+ 字段(description / want_count / browse_count / 成色 / 品牌 / image_urls /
|
|
9
|
+
seller_score / reply_ratio_24h 等)。
|
|
10
|
+
2. **抗风控兜底**:CLI 直签偶尔遇到 x5sec / h5_token 失效,浏览器路径作为备份链路。
|
|
11
|
+
|
|
12
|
+
保留 `item get`(快且不需要 Chrome),两条路并存。用户根据场景选。
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from goofish_cli.core import Strategy, command
|
|
22
|
+
from goofish_cli.core.browser import goofish_page
|
|
23
|
+
from goofish_cli.core.errors import AuthRequiredError, GoofishError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _normalize_item_id(value: Any) -> str:
|
|
27
|
+
s = str(value or "").strip()
|
|
28
|
+
if not re.fullmatch(r"\d+", s):
|
|
29
|
+
raise GoofishError(f"item_id 必须是数字,收到:{value!r}")
|
|
30
|
+
return s
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_item_url(item_id: str) -> str:
|
|
34
|
+
return f"https://www.goofish.com/item?id={item_id}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# 页面上下文里运行的抽取脚本。直接复用 OpenCLI 的提取逻辑(DOM 文案检查 + mtop 调用)。
|
|
38
|
+
_FETCH_JS = r"""
|
|
39
|
+
(itemId) => (async () => {
|
|
40
|
+
const clean = (v) => String(v ?? '').replace(/\s+/g, ' ').trim();
|
|
41
|
+
const extractRetCode = (ret) => {
|
|
42
|
+
const first = Array.isArray(ret) ? ret[0] : '';
|
|
43
|
+
return clean(first).split('::')[0] || '';
|
|
44
|
+
};
|
|
45
|
+
const waitFor = async (predicate, timeoutMs = 5000) => {
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
while (Date.now() - start < timeoutMs) {
|
|
48
|
+
if (predicate()) return true;
|
|
49
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const bodyText = document.body?.innerText || '';
|
|
55
|
+
// 注意:「登录后」在页脚也会出现,不能作为 auth 判定;真正的未登录靠 mtop 返回的
|
|
56
|
+
// FAIL_SYS_SESSION_EXPIRED 来判,这里只拦显式验证码/风控页。
|
|
57
|
+
if (/验证码|安全验证|异常访问/.test(bodyText)) return { error: 'blocked' };
|
|
58
|
+
|
|
59
|
+
await waitFor(() => window.lib?.mtop?.request);
|
|
60
|
+
if (!window.lib || !window.lib.mtop || typeof window.lib.mtop.request !== 'function') {
|
|
61
|
+
return { error: 'mtop-not-ready' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await window.lib.mtop.request({
|
|
67
|
+
api: 'mtop.taobao.idle.pc.detail',
|
|
68
|
+
data: { itemId: String(itemId) },
|
|
69
|
+
type: 'POST',
|
|
70
|
+
v: '1.0',
|
|
71
|
+
dataType: 'json',
|
|
72
|
+
needLogin: false,
|
|
73
|
+
needLoginPC: false,
|
|
74
|
+
sessionOption: 'AutoLoginOnly',
|
|
75
|
+
ecode: 0,
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const ret = error?.ret || [];
|
|
79
|
+
return {
|
|
80
|
+
error: 'mtop-request-failed',
|
|
81
|
+
error_code: extractRetCode(ret),
|
|
82
|
+
error_message: clean(Array.isArray(ret) ? ret.join(' | ') : error?.message || String(error)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const retCode = extractRetCode(response?.ret || []);
|
|
87
|
+
if (retCode && retCode !== 'SUCCESS') {
|
|
88
|
+
return {
|
|
89
|
+
error: 'mtop-response-error',
|
|
90
|
+
error_code: retCode,
|
|
91
|
+
error_message: clean((response?.ret || []).join(' | ')),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const data = response?.data || {};
|
|
96
|
+
const item = data.itemDO || {};
|
|
97
|
+
const seller = data.sellerDO || {};
|
|
98
|
+
const labels = Array.isArray(item.itemLabelExtList) ? item.itemLabelExtList : [];
|
|
99
|
+
const findLabel = (name) => labels.find((l) => clean(l.propertyText) === name)?.text || '';
|
|
100
|
+
const images = Array.isArray(item.imageInfos)
|
|
101
|
+
? item.imageInfos.map((e) => e?.url).filter(Boolean)
|
|
102
|
+
: [];
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
item_id: clean(item.itemId || itemId),
|
|
106
|
+
title: clean(item.title || ''),
|
|
107
|
+
description: clean(item.desc || ''),
|
|
108
|
+
price: clean('¥' + (item.soldPrice || item.defaultPrice || '')).replace(/^¥\s*$/, ''),
|
|
109
|
+
original_price: clean(item.originalPrice || ''),
|
|
110
|
+
want_count: String(item.wantCnt ?? ''),
|
|
111
|
+
collect_count: String(item.collectCnt ?? ''),
|
|
112
|
+
browse_count: String(item.browseCnt ?? ''),
|
|
113
|
+
status: clean(item.itemStatusStr || ''),
|
|
114
|
+
condition: clean(findLabel('成色')),
|
|
115
|
+
brand: clean(findLabel('品牌')),
|
|
116
|
+
category: clean(findLabel('分类')),
|
|
117
|
+
location: clean(seller.publishCity || seller.city || ''),
|
|
118
|
+
seller_name: clean(seller.nick || seller.uniqueName || ''),
|
|
119
|
+
seller_id: String(seller.sellerId || ''),
|
|
120
|
+
seller_score: clean(seller.xianyuSummary || ''),
|
|
121
|
+
reply_ratio_24h: clean(seller.replyRatio24h || ''),
|
|
122
|
+
reply_interval: clean(seller.replyInterval || ''),
|
|
123
|
+
seller_url: seller.sellerId ? 'https://www.goofish.com/personal?userId=' + seller.sellerId : '',
|
|
124
|
+
image_count: String(images.length),
|
|
125
|
+
image_urls: images,
|
|
126
|
+
};
|
|
127
|
+
})()
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def _run(item_id: str) -> dict[str, Any]:
|
|
132
|
+
url = _build_item_url(item_id)
|
|
133
|
+
async with goofish_page() as page:
|
|
134
|
+
await page.goto(url, wait_until="domcontentloaded")
|
|
135
|
+
await page.wait_for_timeout(2000)
|
|
136
|
+
result = await page.evaluate(_FETCH_JS, item_id)
|
|
137
|
+
|
|
138
|
+
if not isinstance(result, dict):
|
|
139
|
+
raise GoofishError("商品详情页返回结构非预期")
|
|
140
|
+
|
|
141
|
+
err = result.get("error")
|
|
142
|
+
if err == "blocked":
|
|
143
|
+
raise GoofishError("商品详情页被验证码/安全验证拦截,触发风控")
|
|
144
|
+
if err == "mtop-not-ready":
|
|
145
|
+
raise GoofishError("页面 window.lib.mtop 未就绪(等待超时),页面加载异常")
|
|
146
|
+
|
|
147
|
+
# auth 判定统一走 mtop 返回的 error_code/error_message 里的 SESSION_EXPIRED,
|
|
148
|
+
# 不再依赖 body 文案正则("登录后" 在页脚也会出现,误报多)。
|
|
149
|
+
err_code = str(result.get("error_code") or "")
|
|
150
|
+
err_msg = str(result.get("error_message") or "")
|
|
151
|
+
if re.search(r"FAIL_SYS_SESSION_EXPIRED|SESSION_EXPIRED", err_code + " " + err_msg):
|
|
152
|
+
raise AuthRequiredError("mtop 返回 session 过期:" + (err_msg or err_code))
|
|
153
|
+
if err:
|
|
154
|
+
raise GoofishError(f"mtop 调用失败 [{err_code or err}]: {err_msg or '无详情'}")
|
|
155
|
+
if not result.get("title"):
|
|
156
|
+
raise GoofishError(f"未拿到商品 {item_id} 的标题,接口返回为空")
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@command(
|
|
162
|
+
namespace="item",
|
|
163
|
+
name="view",
|
|
164
|
+
description="浏览器视角查看商品详情(字段比 item get 更全,抗风控)",
|
|
165
|
+
strategy=Strategy.COOKIE,
|
|
166
|
+
columns=[
|
|
167
|
+
"item_id", "title", "price", "condition", "brand", "location",
|
|
168
|
+
"seller_name", "want_count", "browse_count",
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
def view(item_id: str) -> dict[str, Any]:
|
|
172
|
+
normalized = _normalize_item_id(item_id)
|
|
173
|
+
# MCP / table 渲染依赖 dict;image_urls 是列表(JSON 里保留,table 里压成长度)
|
|
174
|
+
result = asyncio.run(_run(normalized))
|
|
175
|
+
result["_image_urls_json"] = json.dumps(result.get("image_urls", []), ensure_ascii=False)
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
__test__ = {
|
|
180
|
+
"_normalize_item_id": _normalize_item_id,
|
|
181
|
+
"_build_item_url": _build_item_url,
|
|
182
|
+
}
|