goofish-cli 0.4.0__tar.gz → 0.5.0__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.
Files changed (90) hide show
  1. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/.claude-plugin/marketplace.json +1 -1
  2. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/CHANGELOG.md +22 -1
  3. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/PKG-INFO +51 -20
  4. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/README.md +49 -19
  5. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/pyproject.toml +2 -1
  6. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-overview/SKILL.md +1 -2
  7. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-overview/references/mcp-tools-index.md +13 -10
  8. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/SKILL.md +11 -7
  9. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/references/category-selection.md +2 -2
  10. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-reply-buyer/SKILL.md +6 -3
  11. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/cli.py +5 -0
  12. goofish_cli-0.5.0/src/goofish_cli/commands/category/recommend.py +101 -0
  13. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/delete.py +2 -3
  14. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/get.py +2 -10
  15. goofish_cli-0.5.0/src/goofish_cli/commands/item/publish.py +226 -0
  16. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/view.py +5 -2
  17. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/location/default.py +1 -0
  18. goofish_cli-0.5.0/src/goofish_cli/commands/media/upload.py +77 -0
  19. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/message/history.py +13 -3
  20. goofish_cli-0.5.0/src/goofish_cli/commands/message/list_chats.py +225 -0
  21. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/message/send.py +38 -10
  22. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/search/search.py +150 -20
  23. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/errors.py +10 -0
  24. goofish_cli-0.5.0/src/goofish_cli/core/guard.py +80 -0
  25. goofish_cli-0.5.0/src/goofish_cli/core/item_data.py +40 -0
  26. goofish_cli-0.5.0/src/goofish_cli/core/limiter.py +56 -0
  27. goofish_cli-0.5.0/src/goofish_cli/core/local_state.py +55 -0
  28. goofish_cli-0.5.0/src/goofish_cli/core/message_history.py +177 -0
  29. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/output.py +66 -4
  30. goofish_cli-0.5.0/src/goofish_cli/core/search_data.py +68 -0
  31. goofish_cli-0.5.0/src/goofish_cli/core/write_operation.py +15 -0
  32. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/ws.py +133 -77
  33. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/mcp_server.py +6 -1
  34. goofish_cli-0.4.0/src/goofish_cli/commands/category/recommend.py +0 -64
  35. goofish_cli-0.4.0/src/goofish_cli/commands/item/publish.py +0 -175
  36. goofish_cli-0.4.0/src/goofish_cli/commands/media/upload.py +0 -53
  37. goofish_cli-0.4.0/src/goofish_cli/commands/message/list_chats.py +0 -110
  38. goofish_cli-0.4.0/src/goofish_cli/core/guard.py +0 -63
  39. goofish_cli-0.4.0/src/goofish_cli/core/limiter.py +0 -60
  40. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/.gitignore +0 -0
  41. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/LICENSE +0 -0
  42. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/NOTICE +0 -0
  43. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/examples/README.md +0 -0
  44. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-overview/references/accounts.md +0 -0
  45. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-overview/references/xianyu-concepts.md +0 -0
  46. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/references/common-pitfalls.md +0 -0
  47. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/references/image-checklist.md +0 -0
  48. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/references/title-formula.md +0 -0
  49. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-reply-buyer/references/bargain-ladder.md +0 -0
  50. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-reply-buyer/references/intent-classification.md +0 -0
  51. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-reply-buyer/references/tone-guide.md +0 -0
  52. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/SKILL.md +0 -0
  53. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/references/external-contact-keywords.md +0 -0
  54. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/references/forbidden-words.md +0 -0
  55. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/references/publish-red-lines.md +0 -0
  56. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/references/x5sec-recovery.md +0 -0
  57. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-shop-diagnosis/SKILL.md +0 -0
  58. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-shop-diagnosis/references/diagnosis-workflow.md +0 -0
  59. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-shop-diagnosis/references/rank-factors.md +0 -0
  60. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/skills/goofish-shop-diagnosis/references/traffic-drop-causes.md +0 -0
  61. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/__init__.py +0 -0
  62. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/__init__.py +0 -0
  63. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/auth/__init__.py +0 -0
  64. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/auth/login.py +0 -0
  65. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/auth/reset_guard.py +0 -0
  66. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/auth/status.py +0 -0
  67. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/category/__init__.py +0 -0
  68. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/__init__.py +0 -0
  69. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/list.py +0 -0
  70. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/location/__init__.py +0 -0
  71. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/media/__init__.py +0 -0
  72. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/message/__init__.py +0 -0
  73. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/message/watch.py +0 -0
  74. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/search/__init__.py +0 -0
  75. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/skills/__init__.py +0 -0
  76. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/skills/install.py +0 -0
  77. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/__init__.py +0 -0
  78. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/browser.py +0 -0
  79. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/browser_cookie.py +0 -0
  80. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/cookie_types.py +0 -0
  81. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/crypto.py +0 -0
  82. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/mtop.py +0 -0
  83. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/qr_login.py +0 -0
  84. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/refresh.py +0 -0
  85. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/registry.py +0 -0
  86. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/session.py +0 -0
  87. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/sign.py +0 -0
  88. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/strategy.py +0 -0
  89. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/core/token.py +0 -0
  90. {goofish_cli-0.4.0 → goofish_cli-0.5.0}/src/goofish_cli/static/goofish_js_version_2.js +0 -0
@@ -3,7 +3,7 @@
3
3
  "name": "goofish-cli-skills",
4
4
  "displayName": "goofish-cli Skills",
5
5
  "description": "闲鱼(goofish.com)自动化运营的 Claude Skills 套件。配合 goofish-cli MCP server 使用,让 Agent 在发布商品 / 回复买家 / 风控预检 / 店铺诊断这些场景里上手即会。",
6
- "version": "0.4.0",
6
+ "version": "0.5.0",
7
7
  "author": {
8
8
  "name": "fancy",
9
9
  "url": "https://github.com/fancyboi999"
@@ -6,6 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-10-10
10
+
11
+ ### Changed
12
+ - 会话列表默认启用 5 秒 WS 发现,补齐最近消息并按活动时间倒序;
13
+ 缺失时间和未知未读数返回 null,列表完整性与最近消息上下文状态分别说明。
14
+ - 历史消息保留 CID、消息 ID 和创建时间;支持 limit 限定最近条数及 timeout,
15
+ 按服务端消息 ID 去重,缺少 ID 的记录不按正文或时间推断为重复。
16
+
17
+ ### Fixed
18
+ - 多图发布仅最终提交计一次商品预算,媒体上传独立计数;已上传收据、已确认类目和地址可复用,失败保留准备信息与提交状态。
19
+ - 本机限流和熔断按账号持久化,跨进程事务加锁、原子保存,损坏状态拒绝写入;风控保存失败保留原始错误。
20
+ - 搜索从真实页面响应读取语义字段,保留未知标签,区分昵称、地域与原价;无命中时排除推荐卡片,分页响应绑定查询与页码,部分失败在 CLI/MCP 明确报告。
21
+ - 商品详情从 itemDO/sellerDO 提取,保留价格和状态 0;类目分数对应选中分类,发布地址使用 selectedPoi。
22
+ - 单聊结合 userInfo 与 ownerInfo 及当前账号确认对端,避免买卖家角色对换时
23
+ 选到自己;WS 会话的角色信息与消息参加者共同用于身份确认。
24
+ - MCP 输出 schema 与统一的 ok/data 或错误对象一致,避免历史工具返回 list
25
+ 的声明与适配器实际对象冲突。
26
+ - 消息读取在超时、连接失败或分页游标不推进时明确报告失败;列表保留已有
27
+ 结果并标注未完成的摘要,取消时清理连接和心跳。
28
+
9
29
  ## [0.4.0] - 2026-09-07
10
30
 
11
31
  ### Added
@@ -196,7 +216,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
196
216
  - 本版本需要用户手动从浏览器导入 cookie(含 `unb` / `_m_h5_tk` / `x5sec`)
197
217
  - 遇到 `RGV587_ERROR` 风控时,需在浏览器完成滑块验证并**重新导出**带 `x5sec` 的 cookie
198
218
 
199
- [Unreleased]: https://github.com/fancyboi999/goofish-cli/compare/v0.4.0...HEAD
219
+ [Unreleased]: https://github.com/fancyboi999/goofish-cli/compare/v0.5.0...HEAD
220
+ [0.5.0]: https://github.com/fancyboi999/goofish-cli/compare/v0.4.0...v0.5.0
200
221
  [0.4.0]: https://github.com/fancyboi999/goofish-cli/compare/v0.3.0...v0.4.0
201
222
  [0.3.0]: https://github.com/fancyboi999/goofish-cli/compare/v0.2.4...v0.3.0
202
223
  [0.2.4]: https://github.com/fancyboi999/goofish-cli/compare/v0.2.3...v0.2.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: goofish-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
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
@@ -20,6 +20,7 @@ Classifier: Topic :: Internet :: WWW/HTTP
20
20
  Requires-Python: >=3.11
21
21
  Requires-Dist: browser-cookie3>=0.20
22
22
  Requires-Dist: cryptography>=41.0
23
+ Requires-Dist: filelock<4,>=3.16
23
24
  Requires-Dist: loguru>=0.7
24
25
  Requires-Dist: mcp<2,>=1.2
25
26
  Requires-Dist: playwright>=1.58.0
@@ -83,12 +84,30 @@ Description-Content-Type: text/markdown
83
84
 
84
85
  ---
85
86
 
87
+ ## 特别鸣谢
88
+
89
+ <p align="center">
90
+ <a href="https://www.ipwo.net/?ref=githubgoofishcli">
91
+ <img src="media/ipwo-banner.png" alt="IPWO 住宅代理:稳定连接、全球节点、多协议支持" width="95%">
92
+ </a>
93
+ </p>
94
+
95
+ <p align="center">
96
+ <a href="https://www.ipwo.net/?ref=githubgoofishcli"><ins>IPWO</ins></a> 拥有 9000 万+ 活跃住宅 IP 资源,覆盖 195+ 个国家和地区。<br>
97
+ 提供动态住宅代理、静态住宅代理及不限量住宅代理。<br>
98
+ 适用于网页抓取、数据采集、浏览器自动化、跨境业务等场景。<br>
99
+ 我们订阅使用 Claude / Claude Code 时采用住宅 IP,长期使用稳定。<br>
100
+ 支持<a href="https://www.ipwo.net/?ref=githubgoofishcli"><ins>免费测试</ins></a>,<strong>9 折优惠码:</strong><code>0203</code>
101
+ </p>
102
+
103
+ ---
104
+
86
105
  ## ✨ 核心特性
87
106
 
88
107
  - 🔐 **17 个命令覆盖核心链路**:发布、下架、查询、图片上传、AI 类目识别、默认地址、IM 收发 + 会话列表、skills 安装
89
108
  - 📡 **真·实时 IM**:WebSocket 长连 + 自动重连 + **三类事件分类输出**
90
109
  - `event=message`(收到消息)· `event=read`(已读回执)· `event=new_msg`(轻量通知)
91
- - 🛡 **内置风控护栏**:令牌桶限流(1 写/分钟)+ RGV587 自动熔断
110
+ - 🛡 **内置风控护栏**:账号与业务桶限流(经营 1 次/分钟、媒体 9 次/分钟)+ RGV587 自动熔断
92
111
  - 🧠 **AI-first I/O**:`--format json/yaml/table/md/csv`,给 LLM 喂 JSON、给人看表格
93
112
  - ⚡ **一次定义,三种入口**:CLI / MCP / Skill 共享同一 registry
94
113
  - ✅ **真实端到端验证**:每个命令都跑过真实账号
@@ -160,7 +179,7 @@ OpenClaw `2026.6.1` 及以上可把本仓库作为 compatible bundle 加载。
160
179
  openclaw plugins install clawhub:openclaw-goofish
161
180
 
162
181
  # 登录态由用户在终端初始化,不交给 Agent 覆盖
163
- uvx --from goofish-cli==0.4.0 goofish auth login --qr
182
+ uvx --from goofish-cli==0.5.0 goofish auth login --qr
164
183
 
165
184
  openclaw plugins inspect goofish --json
166
185
  openclaw gateway restart
@@ -202,7 +221,7 @@ $ goofish list-commands --format table
202
221
  | `media upload` | 上传图片到闲鱼 CDN | ✅ |
203
222
  | `category recommend` | AI 识别商品类目 | ❌ |
204
223
  | `location default` | 获取默认发布地址 | ❌ |
205
- | `message list-chats` | 拉取会话列表(左栏;`--watch-secs N` 叠加 WS 历史推送补漏) | ❌ |
224
+ | `message list-chats` | 会话列表(默认 HTTP + 短时 WS,补最近消息并按活动时间倒序) | ❌ |
206
225
  | `search items` | 搜索闲鱼商品(浏览器路径 Playwright + 系统 Chrome) | ❌ |
207
226
  | `item view` | 浏览器视角看商品详情(字段完整,抗风控;`item get` 的姊妹版) | ❌ |
208
227
  | `message history` | 拉取会话历史消息 | ❌ |
@@ -211,6 +230,22 @@ $ goofish list-commands --format table
211
230
 
212
231
  </details>
213
232
 
233
+ 会话读取使用真实消息时间,而非会话创建时间:
234
+
235
+ ```bash
236
+ goofish message list-chats --format json
237
+ goofish message history <cid> --limit 20 --timeout 30 --format json
238
+ ```
239
+
240
+ `list-chats` 默认 `--watch-secs 5`,在一个已就绪的连接中补齐真人会话的最近消息页;
241
+ `--watch-secs 0` 关闭 WS 会话发现,结果可能遗漏 HTTP 未返回的会话。
242
+ `--timeout` 限制摘要读取阶段。历史输出保留 `cid`、`message_id` 和毫秒时间 `created_at`,
243
+ `--limit 0` 保留完整翻页行为,其他正数只读最近 N 条。
244
+
245
+ `metadata_status` 标明摘要是当前、为空、不完整或读取失败;无法确定的时间为 `null`。
246
+ `has_more` 只代表 HTTP 分页,`enumeration_complete=false` 表明短时同步不能证明全部历史会话已覆盖。
247
+ 接口契约与异常边界见 [会话同步](docs/conversation-sync.md)。
248
+
214
249
  <details>
215
250
  <summary><b><code>goofish auth status</code></b> — 登录态健康检查</summary>
216
251
 
@@ -267,25 +302,21 @@ $ goofish message send <masked-cid> <masked-user-id> \
267
302
  <summary><b><code>goofish item publish</code></b> — 发布商品(含风控护栏)</summary>
268
303
 
269
304
  ```bash
270
- $ goofish item publish \
271
- --title "男士毛呢大衣 驼色长款" \
272
- --desc "全新未拆封 原价 2999 现 999" \
273
- --images ./a.png,./b.png \
274
- --price 999
305
+ goofish item publish "男士毛呢大衣 驼色长款" \
306
+ "全新未拆封 原价 2999 现 999" ./a.png ./b.png 999 --format json
275
307
  ```
276
308
 
277
- 流程:
278
- 1. `media upload` 每张图 → CDN URL + 尺寸
279
- 2. `category recommend` 拿 AI 识别的 catId
280
- 3. `location default` 拿默认地址
281
- 4. `mtop.idle.pc.idleitem.publish` 落库
309
+ 流程:上传图片 → 推荐类目 → 默认地址 → 提交。已有上传收据可通过
310
+ `--images-json '[{"url":"https://example.alicdn.com/image.png","width":1024,"height":1024}]'`
311
+ 复用,省略本地图片参数;已确认类目和地址分别通过 `--category-json`、`--location-json`
312
+ 传入对应工具返回的完整 DTO。
282
313
 
283
- 返回:
284
- ```json
285
- {"ok": true, "itemId": "1046118265141", "status": "published"}
286
- ```
314
+ 返回 `item_id`、`status="accepted"` 和 `requires_readback=true`;使用
315
+ `goofish item get <item_id>` 和自己的商品列表确认保存。失败也保留已上传图片和准备信息;
316
+ `submission_unknown` 必须先核对商品列表,不自动重复发布。
287
317
 
288
- **触发令牌桶限流**(1 写/分钟)。高频调用会被本地拒绝,避免被闲鱼风控。
318
+ 发布与下架共用账号的 `item.write` 预算,默认 1 次/分钟;媒体上传独立计数,默认
319
+ 9 次/分钟。一个多图商品只消耗一次商品预算。
289
320
  </details>
290
321
 
291
322
  ---
@@ -319,7 +350,7 @@ Claude 会自动把全部命令看成 tool:`goofish_item_get` / `goofish_item_
319
350
  | WebSocket 批量 push 全量解码 | 一帧多条消息全部还原,不丢单 |
320
351
  | WebSocket 自动重连 | 断线自退避重连,长跑无感知 |
321
352
  | 已读回执 / typing / 新消息通知分类 | `/s/sync` 元事件结构化为三类 JSONL |
322
- | 全局限流 + 风控熔断 | 令牌桶 1 写/分钟 + RGV587 自动熔断 |
353
+ | 全局限流 + 风控熔断 | 账号业务预算 + RGV587 自动熔断 |
323
354
  | 单元测试 | 33 个,ruff 零告警 |
324
355
  | 包分发 | `pip install goofish-cli` / `uvx goofish-cli` |
325
356
 
@@ -43,12 +43,30 @@
43
43
 
44
44
  ---
45
45
 
46
+ ## 特别鸣谢
47
+
48
+ <p align="center">
49
+ <a href="https://www.ipwo.net/?ref=githubgoofishcli">
50
+ <img src="media/ipwo-banner.png" alt="IPWO 住宅代理:稳定连接、全球节点、多协议支持" width="95%">
51
+ </a>
52
+ </p>
53
+
54
+ <p align="center">
55
+ <a href="https://www.ipwo.net/?ref=githubgoofishcli"><ins>IPWO</ins></a> 拥有 9000 万+ 活跃住宅 IP 资源,覆盖 195+ 个国家和地区。<br>
56
+ 提供动态住宅代理、静态住宅代理及不限量住宅代理。<br>
57
+ 适用于网页抓取、数据采集、浏览器自动化、跨境业务等场景。<br>
58
+ 我们订阅使用 Claude / Claude Code 时采用住宅 IP,长期使用稳定。<br>
59
+ 支持<a href="https://www.ipwo.net/?ref=githubgoofishcli"><ins>免费测试</ins></a>,<strong>9 折优惠码:</strong><code>0203</code>
60
+ </p>
61
+
62
+ ---
63
+
46
64
  ## ✨ 核心特性
47
65
 
48
66
  - 🔐 **17 个命令覆盖核心链路**:发布、下架、查询、图片上传、AI 类目识别、默认地址、IM 收发 + 会话列表、skills 安装
49
67
  - 📡 **真·实时 IM**:WebSocket 长连 + 自动重连 + **三类事件分类输出**
50
68
  - `event=message`(收到消息)· `event=read`(已读回执)· `event=new_msg`(轻量通知)
51
- - 🛡 **内置风控护栏**:令牌桶限流(1 写/分钟)+ RGV587 自动熔断
69
+ - 🛡 **内置风控护栏**:账号与业务桶限流(经营 1 次/分钟、媒体 9 次/分钟)+ RGV587 自动熔断
52
70
  - 🧠 **AI-first I/O**:`--format json/yaml/table/md/csv`,给 LLM 喂 JSON、给人看表格
53
71
  - ⚡ **一次定义,三种入口**:CLI / MCP / Skill 共享同一 registry
54
72
  - ✅ **真实端到端验证**:每个命令都跑过真实账号
@@ -120,7 +138,7 @@ OpenClaw `2026.6.1` 及以上可把本仓库作为 compatible bundle 加载。
120
138
  openclaw plugins install clawhub:openclaw-goofish
121
139
 
122
140
  # 登录态由用户在终端初始化,不交给 Agent 覆盖
123
- uvx --from goofish-cli==0.4.0 goofish auth login --qr
141
+ uvx --from goofish-cli==0.5.0 goofish auth login --qr
124
142
 
125
143
  openclaw plugins inspect goofish --json
126
144
  openclaw gateway restart
@@ -162,7 +180,7 @@ $ goofish list-commands --format table
162
180
  | `media upload` | 上传图片到闲鱼 CDN | ✅ |
163
181
  | `category recommend` | AI 识别商品类目 | ❌ |
164
182
  | `location default` | 获取默认发布地址 | ❌ |
165
- | `message list-chats` | 拉取会话列表(左栏;`--watch-secs N` 叠加 WS 历史推送补漏) | ❌ |
183
+ | `message list-chats` | 会话列表(默认 HTTP + 短时 WS,补最近消息并按活动时间倒序) | ❌ |
166
184
  | `search items` | 搜索闲鱼商品(浏览器路径 Playwright + 系统 Chrome) | ❌ |
167
185
  | `item view` | 浏览器视角看商品详情(字段完整,抗风控;`item get` 的姊妹版) | ❌ |
168
186
  | `message history` | 拉取会话历史消息 | ❌ |
@@ -171,6 +189,22 @@ $ goofish list-commands --format table
171
189
 
172
190
  </details>
173
191
 
192
+ 会话读取使用真实消息时间,而非会话创建时间:
193
+
194
+ ```bash
195
+ goofish message list-chats --format json
196
+ goofish message history <cid> --limit 20 --timeout 30 --format json
197
+ ```
198
+
199
+ `list-chats` 默认 `--watch-secs 5`,在一个已就绪的连接中补齐真人会话的最近消息页;
200
+ `--watch-secs 0` 关闭 WS 会话发现,结果可能遗漏 HTTP 未返回的会话。
201
+ `--timeout` 限制摘要读取阶段。历史输出保留 `cid`、`message_id` 和毫秒时间 `created_at`,
202
+ `--limit 0` 保留完整翻页行为,其他正数只读最近 N 条。
203
+
204
+ `metadata_status` 标明摘要是当前、为空、不完整或读取失败;无法确定的时间为 `null`。
205
+ `has_more` 只代表 HTTP 分页,`enumeration_complete=false` 表明短时同步不能证明全部历史会话已覆盖。
206
+ 接口契约与异常边界见 [会话同步](docs/conversation-sync.md)。
207
+
174
208
  <details>
175
209
  <summary><b><code>goofish auth status</code></b> — 登录态健康检查</summary>
176
210
 
@@ -227,25 +261,21 @@ $ goofish message send <masked-cid> <masked-user-id> \
227
261
  <summary><b><code>goofish item publish</code></b> — 发布商品(含风控护栏)</summary>
228
262
 
229
263
  ```bash
230
- $ goofish item publish \
231
- --title "男士毛呢大衣 驼色长款" \
232
- --desc "全新未拆封 原价 2999 现 999" \
233
- --images ./a.png,./b.png \
234
- --price 999
264
+ goofish item publish "男士毛呢大衣 驼色长款" \
265
+ "全新未拆封 原价 2999 现 999" ./a.png ./b.png 999 --format json
235
266
  ```
236
267
 
237
- 流程:
238
- 1. `media upload` 每张图 → CDN URL + 尺寸
239
- 2. `category recommend` 拿 AI 识别的 catId
240
- 3. `location default` 拿默认地址
241
- 4. `mtop.idle.pc.idleitem.publish` 落库
268
+ 流程:上传图片 → 推荐类目 → 默认地址 → 提交。已有上传收据可通过
269
+ `--images-json '[{"url":"https://example.alicdn.com/image.png","width":1024,"height":1024}]'`
270
+ 复用,省略本地图片参数;已确认类目和地址分别通过 `--category-json`、`--location-json`
271
+ 传入对应工具返回的完整 DTO。
242
272
 
243
- 返回:
244
- ```json
245
- {"ok": true, "itemId": "1046118265141", "status": "published"}
246
- ```
273
+ 返回 `item_id`、`status="accepted"` 和 `requires_readback=true`;使用
274
+ `goofish item get <item_id>` 和自己的商品列表确认保存。失败也保留已上传图片和准备信息;
275
+ `submission_unknown` 必须先核对商品列表,不自动重复发布。
247
276
 
248
- **触发令牌桶限流**(1 写/分钟)。高频调用会被本地拒绝,避免被闲鱼风控。
277
+ 发布与下架共用账号的 `item.write` 预算,默认 1 次/分钟;媒体上传独立计数,默认
278
+ 9 次/分钟。一个多图商品只消耗一次商品预算。
249
279
  </details>
250
280
 
251
281
  ---
@@ -279,7 +309,7 @@ Claude 会自动把全部命令看成 tool:`goofish_item_get` / `goofish_item_
279
309
  | WebSocket 批量 push 全量解码 | 一帧多条消息全部还原,不丢单 |
280
310
  | WebSocket 自动重连 | 断线自退避重连,长跑无感知 |
281
311
  | 已读回执 / typing / 新消息通知分类 | `/s/sync` 元事件结构化为三类 JSONL |
282
- | 全局限流 + 风控熔断 | 令牌桶 1 写/分钟 + RGV587 自动熔断 |
312
+ | 全局限流 + 风控熔断 | 账号业务预算 + RGV587 自动熔断 |
283
313
  | 单元测试 | 33 个,ruff 零告警 |
284
314
  | 包分发 | `pip install goofish-cli` / `uvx goofish-cli` |
285
315
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "goofish-cli"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "闲鱼 CLI — 支持 MCP,未来支持 Claude Skills。为 AI Agent 提供闲鱼自动化基础能力。"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -35,6 +35,7 @@ dependencies = [
35
35
  "browser-cookie3>=0.20",
36
36
  "playwright>=1.58.0",
37
37
  "cryptography>=41.0",
38
+ "filelock>=3.16,<4",
38
39
  ]
39
40
 
40
41
  [project.optional-dependencies]
@@ -90,8 +90,7 @@ metadata:
90
90
  Agent 在 goofish 任务里请遵守:
91
91
 
92
92
  1. **任何写操作前先读 `auth_status`**,避免 token 已过期却继续写 → 白忙一场。
93
- 2. **`item_publish / media_upload / message_send` 都有速率限制**(令牌桶
94
- 1 写/分钟),不要短时间连发。RGV587 触发后需用户手动 `goofish auth reset-guard`。
93
+ 2. **`item_publish / media_upload / message_send` 都有速率限制**(账号业务预算),不要短时间连发。RGV587 触发后需用户手动 `goofish auth reset-guard`。
95
94
  3. **所有对外发送(发商品、发消息)务必让用户先确认文案**,不要自动提交。
96
95
  4. **多步任务中途报状态**:上一步完成了什么、下一步准备做什么。闲鱼任务
97
96
  通常是 3-6 步,用户期望有进度感。
@@ -20,17 +20,17 @@
20
20
  | `item_get` | HTTP 视角拉详情(只读) | `item_id` | 无 |
21
21
  | `item_list` | 查看当前账号的在售商品 | `limit?` | 无 |
22
22
  | `item_view` | 浏览器视角拉详情(字段更全、抗风控) | `item_id` | 触发 Playwright 启动 |
23
- | `item_publish` | 发布商品(自动类目+默认地址) | `title, desc, price, image_urls, cat_id?, addr?` | **写操作**,令牌桶 1 写/分钟 |
23
+ | `item_publish` | 发布商品(自动类目+默认地址) | `title, desc, price, images?, images_json?, category_json?, location_json?` | **写操作**,item.write 默认 1 次/分钟 |
24
24
  | `item_delete` | 下架/删除商品 | `item_id` | **写操作** + 风控护栏 |
25
25
 
26
- **发布前强依赖**:`category_recommend` 拿 catId、`media_upload` 拿 image_urls、`location_default` 兜底 addr。
26
+ **发布前强依赖**:`category_recommend` 拿 catId、`media_upload` 拿图片收据、`location_default` 兜底 location_json。
27
27
 
28
28
  ## Message(4 个)
29
29
 
30
30
  | 工具 | 用途 | 典型入参 | 备注 |
31
31
  |---|---|---|---|
32
- | `message_list_chats` | 会话列表(左栏) | `limit?, watch_secs?` | session.sync v3.0 是阉割版,默认会启 WebSocket 增量补齐 cid |
33
- | `message_history` | 某 cid 的历史消息(翻页到底) | `cid` | 拉上下文做意图分类用 |
32
+ | `message_list_chats` | 按最近活动排序的会话摘要 | `fetch_num?, watch_secs?, timeout?` | 默认 5 秒 WS 发现并读取最近消息;检查 metadata_status,不能把 has_more 当全部会话完整性 |
33
+ | `message_history` | 某 cid 的历史消息 | `cid, limit_per_page?, limit?, timeout?` | limit=0 翻页到底;正数只读最近 N 条,保留 message_id/created_at |
34
34
  | `message_watch` | 常驻 IM 长连接,事件以 JSONL 输出 | `secs?` | 阻塞式;Skill 里慎用,短连接更合适 |
35
35
  | `message_send` | 发消息(text/image) | `cid, text` 或 `cid, image_url, w, h` | **写操作** + 外联词风控 |
36
36
 
@@ -41,7 +41,7 @@
41
41
  | `category_recommend` | AI 识别类目(输入标题+图片返回 catId/catName) | 发布前必跑,类目错放会降权 |
42
42
  | `media_upload` | 上传图片到闲鱼 CDN | 返回 `{url, width, height}`,给 `item_publish` 直接用 |
43
43
  | `search_items` | 搜闲鱼商品(浏览器路径,抗风控) | 诊断限流时用"卖家视角 vs 买家视角"对比 |
44
- | `location_default` | 账号默认发布地址 | `item_publish` 不传 addr 时的兜底 |
44
+ | `location_default` | 账号默认发布地址 | `item_publish` 不传 location_json 时的兜底 |
45
45
 
46
46
  ## Skills(1 个,辅助类)
47
47
 
@@ -66,12 +66,15 @@ auth_status → message_list_chats → message_history (按 cid) → [LLM 意图
66
66
  auth_status → search_items (用自家核心词,买家视角) → item_view (拉自家详情) → item_get (历史元数据对比)
67
67
  ```
68
68
 
69
- ## 写操作全局节流
69
+ ## 写操作预算与错误恢复
70
70
 
71
- `item_publish / item_delete / media_upload / message_send` 共用令牌桶:
72
- - 容量 1、每分钟补 1
73
- - 短时连发会返回 `RATE_LIMITED`
74
- - RGV587(服务端风控)触发后需用户从浏览器重新导 cookie(带 x5sec/mtop_partitioned_detect),`auth_reset_guard` 只解本地熔断
71
+ - 同账号发布和下架使用 `item.write`,消息使用 `message.write`,默认各 1 次/分钟。
72
+ - 上传使用独立 `media.write`,默认 9 次/分钟;上传收据通过 `images_json` 复用。
73
+ - CLI 进程与 MCP 线程共享加锁、原子保存的状态;损坏文件拒绝写入。
74
+ - 本机预算不代表平台额度;平台风控触发账号熔断,`auth_reset_guard` 只改变本机状态。
75
+ - 发布失败保留图片、类目、地址及提交状态;`submission_unknown` 先回读,不能自动重发。
76
+ - 搜索 `complete=false` 是未完成,CLI 非零退出,MCP `ok=false` 仍带部分 `data`。
77
+ - 搜索的品牌、成色未知时是 null;用 labels 查看原始标签,不能按顺序猜属性。空查询命中不含网页推荐。
75
78
 
76
79
  ## 错误类型映射
77
80
 
@@ -67,8 +67,8 @@ Claude Code / Cursor 调 `mcp__goofish__<逻辑名>`。frontmatter 的 `allowed-
67
67
  ### Step 3 · 类目识别
68
68
 
69
69
  ```
70
- 调 category_recommend(title=候选标题, images=[首图])
71
- → 拿到 catId / catName
70
+ 调 category_recommend(title=候选标题, images_json="[]")
71
+ → 拿到完整类目 DTO;已有上传收据时用 images_json=JSON.stringify(收据数组)
72
72
  ```
73
73
 
74
74
  **结果校验**:
@@ -96,8 +96,8 @@ Claude Code / Cursor 调 `mcp__goofish__<逻辑名>`。frontmatter 的 `allowed-
96
96
  ### Step 6 · 图片处理
97
97
 
98
98
  ```
99
- for image in images:
100
- 调 media_upload(image)
99
+ for image in 本地图片路径:
100
+ 调 media_upload(path=image)
101
101
  收集返回的 {url, width, height}
102
102
 
103
103
  检查:
@@ -130,12 +130,16 @@ for image in images:
130
130
  title=...,
131
131
  desc=...,
132
132
  price=...,
133
- image_urls=[...],
134
- cat_id=...,
135
- addr=...(没有就省略,让服务端用 location_default)
133
+ images_json=JSON.stringify([{url, width, height}, ...]),
134
+ category_json=JSON.stringify(category_recommend 的完整 DTO),
135
+ location_json=JSON.stringify(location_default 的完整 DTO)
136
136
  )
137
137
  ```
138
138
 
139
+ 发布也可用 `images=[本地路径, ...]` 自动上传,不能把 CDN URL 当本地路径。已提前上传时复用 `images_json`,避免重复上传。类目 DTO 必须包含 cat_id/cat_name/channel_cat_id/tb_cat_id,地址采用 selected 字段。缺少身份的类目不能猜补。模型分数不是校准概率,未知值为 null。
140
+
141
+ 成功返回 accepted 与 item_id 后,用 item_get 回读确认。失败返回的 images/category/location 可复用;submission_unknown 必须先核对自己的商品列表,禁止盲目重试。发布和下架共用 item.write 默认 1 次/分钟,上传独立 media.write 默认 9 次/分钟。
142
+
139
143
  ## 反模式(不要做)
140
144
 
141
145
  ❌ **跳过 category_recommend**,自己猜 catId
@@ -13,7 +13,7 @@ category_recommend(
13
13
  )
14
14
  ```
15
15
 
16
- 返回 `{catId, catName, confidence?}`。
16
+ 返回 `{cat_id, cat_name, channel_cat_id, tb_cat_id, confidence, confidence_source}`。confidence 为模型分数,不是校准概率;缺失为 null。
17
17
 
18
18
  ## 结果处理
19
19
 
@@ -75,4 +75,4 @@ category_recommend(
75
75
 
76
76
  ## 本模块不调其它工具
77
77
 
78
- 只调 `category_recommend`。不需要 `item_get` 也不需要反查——API 给的 catId 直接传给 `item_publish` 就够。
78
+ 只调 `category_recommend`。不需要 `item_get` 也不需要反查——将完整类目 DTO 作为 category_json 传给 item_publish;缺少 cat_id/channel_cat_id/tb_cat_id 时不得猜补。
@@ -60,10 +60,13 @@ auth_status → valid=false 直接停,提示用户 auth login
60
60
  ### Step 2 · 拉未读会话
61
61
 
62
62
  ```
63
- message_list_chats(limit=20)
63
+ message_list_chats(fetch_num=20)
64
64
  ```
65
65
 
66
- 返回会话列表,每项含 cid / 对方昵称 / 最后一条消息预览 / 未读数。
66
+ 返回按活动时间倒序的会话列表,每项含 session_id / 对方昵称 / 最后一条消息预览 / 未读数。
67
+ 默认短时 WS 会话发现;先检查 metadata_status 和 metadata_errors。身份缺失或摘要过期的会话先补信息,不猜对方身份。
68
+ HTTP 的 has_more 不代表整份会话列表完整,短时同步也不能保证发现全部历史会话。
69
+ WS 条目的 unread=null 表示未知,不能按零忽略,也不能直接与数字比较。
67
70
 
68
71
  **筛选策略**:
69
72
  - 优先未读数 > 0 的
@@ -74,7 +77,7 @@ message_list_chats(limit=20)
74
77
 
75
78
  ```
76
79
  对每个选中的 cid:
77
- message_history(cid=..., limit=20)
80
+ message_history(cid=session_id, limit=20)
78
81
  ```
79
82
 
80
83
  20 条基本够分类意图了。太长的会话(50+ 条)拉 30 条。
@@ -9,6 +9,7 @@ import typer
9
9
  from loguru import logger
10
10
 
11
11
  from goofish_cli.core import GoofishError, iter_commands
12
+ from goofish_cli.core.errors import PartialResultError
12
13
  from goofish_cli.core.output import Format, render
13
14
  from goofish_cli.core.registry import Command, discover
14
15
 
@@ -51,6 +52,10 @@ def _wrap(cmd: Command):
51
52
  fmt = Format(kwargs.pop("format"))
52
53
  try:
53
54
  result = cmd.func(**kwargs)
55
+ except PartialResultError as e:
56
+ render(e.data, fmt=fmt, columns=cmd.columns or None)
57
+ typer.secho(f"[{e.error.get('error_type', 'PartialResultError')}] {e}", fg=typer.colors.RED, err=True)
58
+ raise typer.Exit(code=e.exit_code) from e
54
59
  except GoofishError as e:
55
60
  typer.secho(f"[{type(e).__name__}] {e}", fg=typer.colors.RED, err=True)
56
61
  raise typer.Exit(code=e.exit_code) from e
@@ -0,0 +1,101 @@
1
+ """category recommend — AI 识别商品类目(发布前置)。
2
+
3
+ 接口:mtop.taobao.idle.kgraph.property.recommend v2.0
4
+ """
5
+
6
+ import json
7
+ import math
8
+ import time
9
+ from typing import Any
10
+
11
+ from goofish_cli.commands.media.upload import validate_image_receipt
12
+ from goofish_cli.core import Session, Strategy, command
13
+ from goofish_cli.core.errors import GoofishError
14
+ from goofish_cli.core.mtop import call
15
+
16
+
17
+ def _selected_score(data: dict, predict: dict) -> tuple[float | None, str | None]:
18
+ explicit = predict.get("confidence")
19
+ if isinstance(explicit, (float, int)) and not isinstance(explicit, bool) and math.isfinite(explicit):
20
+ return float(explicit), "categoryPredictResult.confidence"
21
+ values = []
22
+ for card in data.get("cardList") or []:
23
+ body = card.get("cardData") or {}
24
+ if str(body.get("propertyId")) != "-10000":
25
+ continue
26
+ for candidate in body.get("valuesList") or []:
27
+ keys = ("channelCatId", "tbCatId", "catId")
28
+ if all(str(candidate.get(key)) == str(predict[key]) for key in keys if predict.get(key) is not None):
29
+ score = candidate.get("score")
30
+ if isinstance(score, (float, int)) and not isinstance(score, bool) and math.isfinite(score):
31
+ values.append(float(score))
32
+ return (values[0], "classification_card.score") if len(values) == 1 else (None, None)
33
+
34
+
35
+ @command(
36
+ namespace="category",
37
+ name="recommend",
38
+ description="AI 识别商品类目,输入标题+图片返回 catId/catName",
39
+ strategy=Strategy.COOKIE,
40
+ columns=["cat_id", "cat_name", "channel_cat_id", "tb_cat_id", "confidence"],
41
+ )
42
+ def recommend(
43
+ title: str,
44
+ images_json: str = "[]",
45
+ ) -> dict[str, Any]:
46
+ """images_json 是 JSON 字符串:[{"url":"...","width":1024,"height":1024}, ...]"""
47
+ if not title.strip():
48
+ raise GoofishError("类目推荐标题不能为空")
49
+ try:
50
+ images = json.loads(images_json) if images_json else []
51
+ except ValueError:
52
+ raise GoofishError("images-json 必须是有效 JSON 图片收据数组") from None
53
+ if not isinstance(images, list) or len(images) > 9:
54
+ raise GoofishError("images-json 必须是至多 9 张图片收据的数组")
55
+ images = [validate_image_receipt(value) for value in images]
56
+ session = Session.load()
57
+ unique_code = str(time.time_ns() // 1000)
58
+ image_infos: list[dict[str, Any]] = []
59
+ for img in images:
60
+ image_infos.append({
61
+ "extraInfo": {"isH": "false", "isT": "false", "raw": "false"},
62
+ "isQrCode": False,
63
+ "url": img["url"],
64
+ "heightSize": img["height"],
65
+ "widthSize": img["width"],
66
+ "major": True,
67
+ "type": 0,
68
+ "status": "done",
69
+ })
70
+
71
+ raw = call(
72
+ session,
73
+ api="mtop.taobao.idle.kgraph.property.recommend",
74
+ data={
75
+ "title": title,
76
+ "lockCpv": False,
77
+ "multiSKU": False,
78
+ "publishScene": "mainPublish",
79
+ "scene": "newPublishChoice",
80
+ "description": title,
81
+ "imageInfos": image_infos,
82
+ "uniqueCode": unique_code,
83
+ },
84
+ version="2.0",
85
+ spm_cnt="a21ybx.publish.0.0",
86
+ )
87
+ predict = (raw.get("data", {}) or {}).get("categoryPredictResult", {}) or {}
88
+ if not predict.get("catId") or not predict.get("channelCatId") or not predict.get("tbCatId"):
89
+ raise GoofishError("类目推荐响应缺少选中分类身份,不能继续自动发布", raw=raw)
90
+ score, source = _selected_score(raw.get("data") or {}, predict)
91
+ return {
92
+ "cat_id": str(predict.get("catId", "")),
93
+ "cat_name": predict.get("catName", ""),
94
+ "channel_cat_id": str(predict.get("channelCatId", "")),
95
+ "tb_cat_id": str(predict.get("tbCatId", "")),
96
+ "confidence": score,
97
+ "confidence_source": source,
98
+ "confidence_kind": "model_score_not_calibrated_probability",
99
+ "unique_code": unique_code,
100
+ "raw": raw,
101
+ }
@@ -1,9 +1,8 @@
1
1
  """item delete — 下架商品。接口 com.taobao.idle.item.delete v1.1(写操作)"""
2
2
 
3
3
  from goofish_cli.core import Session, Strategy, command
4
- from goofish_cli.core.guard import watch
5
- from goofish_cli.core.limiter import acquire
6
4
  from goofish_cli.core.mtop import call
5
+ from goofish_cli.core.write_operation import write_operation
7
6
 
8
7
 
9
8
  @command(
@@ -16,7 +15,7 @@ from goofish_cli.core.mtop import call
16
15
  )
17
16
  def delete(item_id: str) -> dict[str, object]:
18
17
  session = Session.load()
19
- with acquire("item.write"), watch():
18
+ with write_operation(session, "item.write"):
20
19
  raw = call(
21
20
  session,
22
21
  api="com.taobao.idle.item.delete",
@@ -3,6 +3,7 @@
3
3
  from typing import Any
4
4
 
5
5
  from goofish_cli.core import Session, Strategy, command
6
+ from goofish_cli.core.item_data import normalize_item
6
7
  from goofish_cli.core.mtop import call
7
8
 
8
9
 
@@ -22,13 +23,4 @@ def get(item_id: str) -> dict[str, Any]:
22
23
  version="1.0",
23
24
  spm_cnt="a21ybx.item.0.0",
24
25
  )
25
- data = raw.get("data", {}) or {}
26
- track = data.get("trackParams", {}) or {}
27
- return {
28
- "item_id": track.get("id", item_id),
29
- "title": track.get("title", ""),
30
- "price": track.get("soldPrice") or track.get("price", ""),
31
- "seller_nick": track.get("seller_nick", ""),
32
- "status": track.get("itemStatus", ""),
33
- "raw": raw,
34
- }
26
+ return normalize_item(raw, str(item_id))