goofish-cli 0.3.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 (91) hide show
  1. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/.claude-plugin/marketplace.json +1 -1
  2. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/CHANGELOG.md +37 -1
  3. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/PKG-INFO +53 -21
  4. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/README.md +51 -20
  5. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/pyproject.toml +2 -1
  6. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-overview/SKILL.md +1 -2
  7. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-overview/references/mcp-tools-index.md +13 -10
  8. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/SKILL.md +11 -7
  9. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/references/category-selection.md +2 -2
  10. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-reply-buyer/SKILL.md +6 -3
  11. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/cli.py +5 -0
  12. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/auth/login.py +33 -23
  13. goofish_cli-0.5.0/src/goofish_cli/commands/category/recommend.py +101 -0
  14. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/delete.py +2 -3
  15. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/get.py +2 -10
  16. goofish_cli-0.5.0/src/goofish_cli/commands/item/publish.py +226 -0
  17. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/view.py +5 -2
  18. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/location/default.py +1 -0
  19. goofish_cli-0.5.0/src/goofish_cli/commands/media/upload.py +77 -0
  20. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/message/history.py +13 -3
  21. goofish_cli-0.5.0/src/goofish_cli/commands/message/list_chats.py +225 -0
  22. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/message/send.py +38 -10
  23. goofish_cli-0.5.0/src/goofish_cli/commands/search/search.py +506 -0
  24. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/browser.py +35 -28
  25. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/browser_cookie.py +30 -13
  26. goofish_cli-0.5.0/src/goofish_cli/core/cookie_types.py +10 -0
  27. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/crypto.py +4 -4
  28. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/errors.py +10 -0
  29. goofish_cli-0.5.0/src/goofish_cli/core/guard.py +80 -0
  30. goofish_cli-0.5.0/src/goofish_cli/core/item_data.py +40 -0
  31. goofish_cli-0.5.0/src/goofish_cli/core/limiter.py +56 -0
  32. goofish_cli-0.5.0/src/goofish_cli/core/local_state.py +55 -0
  33. goofish_cli-0.5.0/src/goofish_cli/core/message_history.py +177 -0
  34. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/output.py +66 -4
  35. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/qr_login.py +22 -15
  36. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/refresh.py +38 -18
  37. goofish_cli-0.5.0/src/goofish_cli/core/search_data.py +68 -0
  38. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/session.py +62 -32
  39. goofish_cli-0.5.0/src/goofish_cli/core/write_operation.py +15 -0
  40. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/ws.py +133 -77
  41. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/mcp_server.py +6 -1
  42. goofish_cli-0.3.0/src/goofish_cli/commands/category/recommend.py +0 -64
  43. goofish_cli-0.3.0/src/goofish_cli/commands/item/publish.py +0 -175
  44. goofish_cli-0.3.0/src/goofish_cli/commands/media/upload.py +0 -53
  45. goofish_cli-0.3.0/src/goofish_cli/commands/message/list_chats.py +0 -110
  46. goofish_cli-0.3.0/src/goofish_cli/commands/search/search.py +0 -169
  47. goofish_cli-0.3.0/src/goofish_cli/core/guard.py +0 -63
  48. goofish_cli-0.3.0/src/goofish_cli/core/limiter.py +0 -60
  49. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/.gitignore +0 -0
  50. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/LICENSE +0 -0
  51. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/NOTICE +0 -0
  52. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/examples/README.md +0 -0
  53. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-overview/references/accounts.md +0 -0
  54. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-overview/references/xianyu-concepts.md +0 -0
  55. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/references/common-pitfalls.md +0 -0
  56. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/references/image-checklist.md +0 -0
  57. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-publish-item/references/title-formula.md +0 -0
  58. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-reply-buyer/references/bargain-ladder.md +0 -0
  59. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-reply-buyer/references/intent-classification.md +0 -0
  60. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-reply-buyer/references/tone-guide.md +0 -0
  61. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/SKILL.md +0 -0
  62. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/references/external-contact-keywords.md +0 -0
  63. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/references/forbidden-words.md +0 -0
  64. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/references/publish-red-lines.md +0 -0
  65. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-risk-guard/references/x5sec-recovery.md +0 -0
  66. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-shop-diagnosis/SKILL.md +0 -0
  67. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-shop-diagnosis/references/diagnosis-workflow.md +0 -0
  68. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-shop-diagnosis/references/rank-factors.md +0 -0
  69. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/skills/goofish-shop-diagnosis/references/traffic-drop-causes.md +0 -0
  70. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/__init__.py +0 -0
  71. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/__init__.py +0 -0
  72. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/auth/__init__.py +0 -0
  73. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/auth/reset_guard.py +0 -0
  74. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/auth/status.py +0 -0
  75. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/category/__init__.py +0 -0
  76. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/__init__.py +0 -0
  77. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/item/list.py +0 -0
  78. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/location/__init__.py +0 -0
  79. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/media/__init__.py +0 -0
  80. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/message/__init__.py +0 -0
  81. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/message/watch.py +0 -0
  82. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/search/__init__.py +0 -0
  83. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/skills/__init__.py +0 -0
  84. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/commands/skills/install.py +0 -0
  85. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/__init__.py +0 -0
  86. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/mtop.py +0 -0
  87. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/registry.py +0 -0
  88. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/sign.py +0 -0
  89. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/strategy.py +0 -0
  90. {goofish_cli-0.3.0 → goofish_cli-0.5.0}/src/goofish_cli/core/token.py +0 -0
  91. {goofish_cli-0.3.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.3.0",
6
+ "version": "0.5.0",
7
7
  "author": {
8
8
  "name": "fancy",
9
9
  "url": "https://github.com/fancyboi999"
@@ -6,6 +6,40 @@ 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
+
29
+ ## [0.4.0] - 2026-09-07
30
+
31
+ ### Added
32
+ - 搜索支持 `goofish search items --pages N`,跨页按商品 ID 去重;默认仍读取一页,
33
+ `--limit` 控制所有页面的结果总数。返回 `pages_fetched`、`pages_total` 和
34
+ `stopped_reason`,说明实际翻页数量与停止原因。翻页过程中发生错误或遇到登录阻拦时,
35
+ 保留已经取得的结果。
36
+
37
+ ### Fixed
38
+ - 浏览器导入、JSON 导入、扫码登录和 Cookie 刷新保留 Cookie 的 domain/path,
39
+ 防止同名 Cookie 覆盖或注入到错误域名、路径;兼容旧版扁平 Cookie 数据。
40
+ - 修复 Cookie 刷新在 `persist=False` 时的异常,并保持内存与持久化记录一致。
41
+ - 恢复搜索成功时的结构化返回;仅在首屏没有结果且明确要求登录时重试一次。
42
+
9
43
  ## [0.3.0] - 2026-08-19
10
44
 
11
45
  ### Added
@@ -182,7 +216,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
182
216
  - 本版本需要用户手动从浏览器导入 cookie(含 `unb` / `_m_h5_tk` / `x5sec`)
183
217
  - 遇到 `RGV587_ERROR` 风控时,需在浏览器完成滑块验证并**重新导出**带 `x5sec` 的 cookie
184
218
 
185
- [Unreleased]: https://github.com/fancyboi999/goofish-cli/compare/v0.3.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
221
+ [0.4.0]: https://github.com/fancyboi999/goofish-cli/compare/v0.3.0...v0.4.0
186
222
  [0.3.0]: https://github.com/fancyboi999/goofish-cli/compare/v0.2.4...v0.3.0
187
223
  [0.2.4]: https://github.com/fancyboi999/goofish-cli/compare/v0.2.3...v0.2.4
188
224
  [0.2.3]: https://github.com/fancyboi999/goofish-cli/compare/v0.2.2...v0.2.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: goofish-cli
3
- Version: 0.3.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
  - ✅ **真实端到端验证**:每个命令都跑过真实账号
@@ -153,13 +172,14 @@ claude /plugin marketplace add fancyboi999/goofish-cli
153
172
 
154
173
  ## OpenClaw / ClawHub
155
174
 
156
- OpenClaw `2026.6.1` 及以上可把本仓库作为 compatible bundle 加载。ClawHub 发布后:
175
+ OpenClaw `2026.6.1` 及以上可把本仓库作为 compatible bundle 加载。已发布到
176
+ [ClawHub](https://clawhub.ai/plugins/openclaw-goofish),安装:
157
177
 
158
178
  ```bash
159
179
  openclaw plugins install clawhub:openclaw-goofish
160
180
 
161
181
  # 登录态由用户在终端初始化,不交给 Agent 覆盖
162
- uvx --from goofish-cli==0.3.0 goofish auth login --qr
182
+ uvx --from goofish-cli==0.5.0 goofish auth login --qr
163
183
 
164
184
  openclaw plugins inspect goofish --json
165
185
  openclaw gateway restart
@@ -201,7 +221,7 @@ $ goofish list-commands --format table
201
221
  | `media upload` | 上传图片到闲鱼 CDN | ✅ |
202
222
  | `category recommend` | AI 识别商品类目 | ❌ |
203
223
  | `location default` | 获取默认发布地址 | ❌ |
204
- | `message list-chats` | 拉取会话列表(左栏;`--watch-secs N` 叠加 WS 历史推送补漏) | ❌ |
224
+ | `message list-chats` | 会话列表(默认 HTTP + 短时 WS,补最近消息并按活动时间倒序) | ❌ |
205
225
  | `search items` | 搜索闲鱼商品(浏览器路径 Playwright + 系统 Chrome) | ❌ |
206
226
  | `item view` | 浏览器视角看商品详情(字段完整,抗风控;`item get` 的姊妹版) | ❌ |
207
227
  | `message history` | 拉取会话历史消息 | ❌ |
@@ -210,6 +230,22 @@ $ goofish list-commands --format table
210
230
 
211
231
  </details>
212
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
+
213
249
  <details>
214
250
  <summary><b><code>goofish auth status</code></b> — 登录态健康检查</summary>
215
251
 
@@ -266,25 +302,21 @@ $ goofish message send <masked-cid> <masked-user-id> \
266
302
  <summary><b><code>goofish item publish</code></b> — 发布商品(含风控护栏)</summary>
267
303
 
268
304
  ```bash
269
- $ goofish item publish \
270
- --title "男士毛呢大衣 驼色长款" \
271
- --desc "全新未拆封 原价 2999 现 999" \
272
- --images ./a.png,./b.png \
273
- --price 999
305
+ goofish item publish "男士毛呢大衣 驼色长款" \
306
+ "全新未拆封 原价 2999 现 999" ./a.png ./b.png 999 --format json
274
307
  ```
275
308
 
276
- 流程:
277
- 1. `media upload` 每张图 → CDN URL + 尺寸
278
- 2. `category recommend` 拿 AI 识别的 catId
279
- 3. `location default` 拿默认地址
280
- 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。
281
313
 
282
- 返回:
283
- ```json
284
- {"ok": true, "itemId": "1046118265141", "status": "published"}
285
- ```
314
+ 返回 `item_id`、`status="accepted"` 和 `requires_readback=true`;使用
315
+ `goofish item get <item_id>` 和自己的商品列表确认保存。失败也保留已上传图片和准备信息;
316
+ `submission_unknown` 必须先核对商品列表,不自动重复发布。
286
317
 
287
- **触发令牌桶限流**(1 写/分钟)。高频调用会被本地拒绝,避免被闲鱼风控。
318
+ 发布与下架共用账号的 `item.write` 预算,默认 1 次/分钟;媒体上传独立计数,默认
319
+ 9 次/分钟。一个多图商品只消耗一次商品预算。
288
320
  </details>
289
321
 
290
322
  ---
@@ -318,7 +350,7 @@ Claude 会自动把全部命令看成 tool:`goofish_item_get` / `goofish_item_
318
350
  | WebSocket 批量 push 全量解码 | 一帧多条消息全部还原,不丢单 |
319
351
  | WebSocket 自动重连 | 断线自退避重连,长跑无感知 |
320
352
  | 已读回执 / typing / 新消息通知分类 | `/s/sync` 元事件结构化为三类 JSONL |
321
- | 全局限流 + 风控熔断 | 令牌桶 1 写/分钟 + RGV587 自动熔断 |
353
+ | 全局限流 + 风控熔断 | 账号业务预算 + RGV587 自动熔断 |
322
354
  | 单元测试 | 33 个,ruff 零告警 |
323
355
  | 包分发 | `pip install goofish-cli` / `uvx goofish-cli` |
324
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
  - ✅ **真实端到端验证**:每个命令都跑过真实账号
@@ -113,13 +131,14 @@ claude /plugin marketplace add fancyboi999/goofish-cli
113
131
 
114
132
  ## OpenClaw / ClawHub
115
133
 
116
- OpenClaw `2026.6.1` 及以上可把本仓库作为 compatible bundle 加载。ClawHub 发布后:
134
+ OpenClaw `2026.6.1` 及以上可把本仓库作为 compatible bundle 加载。已发布到
135
+ [ClawHub](https://clawhub.ai/plugins/openclaw-goofish),安装:
117
136
 
118
137
  ```bash
119
138
  openclaw plugins install clawhub:openclaw-goofish
120
139
 
121
140
  # 登录态由用户在终端初始化,不交给 Agent 覆盖
122
- uvx --from goofish-cli==0.3.0 goofish auth login --qr
141
+ uvx --from goofish-cli==0.5.0 goofish auth login --qr
123
142
 
124
143
  openclaw plugins inspect goofish --json
125
144
  openclaw gateway restart
@@ -161,7 +180,7 @@ $ goofish list-commands --format table
161
180
  | `media upload` | 上传图片到闲鱼 CDN | ✅ |
162
181
  | `category recommend` | AI 识别商品类目 | ❌ |
163
182
  | `location default` | 获取默认发布地址 | ❌ |
164
- | `message list-chats` | 拉取会话列表(左栏;`--watch-secs N` 叠加 WS 历史推送补漏) | ❌ |
183
+ | `message list-chats` | 会话列表(默认 HTTP + 短时 WS,补最近消息并按活动时间倒序) | ❌ |
165
184
  | `search items` | 搜索闲鱼商品(浏览器路径 Playwright + 系统 Chrome) | ❌ |
166
185
  | `item view` | 浏览器视角看商品详情(字段完整,抗风控;`item get` 的姊妹版) | ❌ |
167
186
  | `message history` | 拉取会话历史消息 | ❌ |
@@ -170,6 +189,22 @@ $ goofish list-commands --format table
170
189
 
171
190
  </details>
172
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
+
173
208
  <details>
174
209
  <summary><b><code>goofish auth status</code></b> — 登录态健康检查</summary>
175
210
 
@@ -226,25 +261,21 @@ $ goofish message send <masked-cid> <masked-user-id> \
226
261
  <summary><b><code>goofish item publish</code></b> — 发布商品(含风控护栏)</summary>
227
262
 
228
263
  ```bash
229
- $ goofish item publish \
230
- --title "男士毛呢大衣 驼色长款" \
231
- --desc "全新未拆封 原价 2999 现 999" \
232
- --images ./a.png,./b.png \
233
- --price 999
264
+ goofish item publish "男士毛呢大衣 驼色长款" \
265
+ "全新未拆封 原价 2999 现 999" ./a.png ./b.png 999 --format json
234
266
  ```
235
267
 
236
- 流程:
237
- 1. `media upload` 每张图 → CDN URL + 尺寸
238
- 2. `category recommend` 拿 AI 识别的 catId
239
- 3. `location default` 拿默认地址
240
- 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。
241
272
 
242
- 返回:
243
- ```json
244
- {"ok": true, "itemId": "1046118265141", "status": "published"}
245
- ```
273
+ 返回 `item_id`、`status="accepted"` 和 `requires_readback=true`;使用
274
+ `goofish item get <item_id>` 和自己的商品列表确认保存。失败也保留已上传图片和准备信息;
275
+ `submission_unknown` 必须先核对商品列表,不自动重复发布。
246
276
 
247
- **触发令牌桶限流**(1 写/分钟)。高频调用会被本地拒绝,避免被闲鱼风控。
277
+ 发布与下架共用账号的 `item.write` 预算,默认 1 次/分钟;媒体上传独立计数,默认
278
+ 9 次/分钟。一个多图商品只消耗一次商品预算。
248
279
  </details>
249
280
 
250
281
  ---
@@ -278,7 +309,7 @@ Claude 会自动把全部命令看成 tool:`goofish_item_get` / `goofish_item_
278
309
  | WebSocket 批量 push 全量解码 | 一帧多条消息全部还原,不丢单 |
279
310
  | WebSocket 自动重连 | 断线自退避重连,长跑无感知 |
280
311
  | 已读回执 / typing / 新消息通知分类 | `/s/sync` 元事件结构化为三类 JSONL |
281
- | 全局限流 + 风控熔断 | 令牌桶 1 写/分钟 + RGV587 自动熔断 |
312
+ | 全局限流 + 风控熔断 | 账号业务预算 + RGV587 自动熔断 |
282
313
  | 单元测试 | 33 个,ruff 零告警 |
283
314
  | 包分发 | `pip install goofish-cli` / `uvx goofish-cli` |
284
315
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "goofish-cli"
7
- version = "0.3.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
@@ -23,8 +23,14 @@ import json
23
23
  from pathlib import Path
24
24
 
25
25
  from goofish_cli.core import Strategy, command
26
+ from goofish_cli.core.cookie_types import CookieRecord
26
27
  from goofish_cli.core.errors import AuthRequiredError
27
- from goofish_cli.core.session import DEFAULT_COOKIE_PATH, write_cookies_json
28
+ from goofish_cli.core.session import (
29
+ DEFAULT_COOKIE_PATH,
30
+ _coerce_records,
31
+ _records_to_flat,
32
+ write_cookies_json,
33
+ )
28
34
 
29
35
 
30
36
  @command(
@@ -56,16 +62,8 @@ def login(
56
62
  # qr_timeout=None 让 core.qr_login 走统一的 env → 默认值 兜底逻辑;
57
63
  # 这里若写 int 默认(例如 120)会把 env 覆盖路径挡掉(CLI 总是显式传值)。
58
64
  from goofish_cli.core.qr_login import login_via_qr
59
- cookies = login_via_qr(timeout=qr_timeout, persist=False)
60
- if not cookies:
61
- # 空 dict 可能来自两种失败:超时未扫码,或 Playwright 起不来(Chrome
62
- # 未装、端口占用等)。文案同时覆盖,让用户知道去翻日志。
63
- raise AuthRequiredError(
64
- "QR 扫码登录未完成——可能是超时内未扫码 / 手机未确认,"
65
- "也可能是 Playwright 浏览器启动失败(Chrome 未装、端口占用等,"
66
- "详见前面的 warning 日志)。可重试并延长超时:"
67
- "goofish auth login --qr --qr-timeout 180"
68
- )
65
+
66
+ records = login_via_qr(timeout=qr_timeout, persist=False)
69
67
  source_label = "qr"
70
68
  elif source is None:
71
69
  if raw:
@@ -75,33 +73,35 @@ def login(
75
73
  "--raw 需要配合 cookie 字符串使用,如:"
76
74
  "goofish auth login 'unb=...; _m_h5_tk=...' --raw"
77
75
  )
78
- cookies, source_label = _pull_from_browser(browser)
76
+ records, source_label = _pull_from_browser(browser)
79
77
  elif raw:
80
- cookies = _parse_raw(source)
78
+ records = _coerce_records(_parse_raw(source))
81
79
  source_label = "raw"
82
80
  else:
83
81
  p = Path(source).expanduser()
84
- cookies = _parse_json(p.read_text())
82
+ records = _parse_json(p.read_text())
85
83
  source_label = f"file:{p}"
86
84
 
87
- if "unb" not in cookies or "_m_h5_tk" not in cookies:
85
+ # 校验用 flat 视角
86
+ flat = _records_to_flat(records)
87
+ if "unb" not in flat or "_m_h5_tk" not in flat:
88
88
  raise AuthRequiredError(
89
89
  "cookie 缺失关键字段 unb / _m_h5_tk。"
90
90
  "请先在浏览器里登录 https://www.goofish.com 再试。"
91
91
  )
92
92
 
93
- write_cookies_json(target, cookies)
93
+ write_cookies_json(target, records)
94
94
 
95
95
  return {
96
96
  "source": source_label,
97
97
  "path": str(target),
98
- "unb": cookies.get("unb", ""),
99
- "tracknick": cookies.get("tracknick", ""),
100
- "cookies_count": len(cookies),
98
+ "unb": flat.get("unb", ""),
99
+ "tracknick": flat.get("tracknick", ""),
100
+ "cookies_count": len(flat),
101
101
  }
102
102
 
103
103
 
104
- def _pull_from_browser(browser: str) -> tuple[dict[str, str], str]:
104
+ def _pull_from_browser(browser: str) -> tuple[list[CookieRecord], str]:
105
105
  from goofish_cli.core.browser_cookie import (
106
106
  BrowserCookieError,
107
107
  available_browsers,
@@ -133,10 +133,20 @@ def _parse_raw(raw: str) -> dict[str, str]:
133
133
  return out
134
134
 
135
135
 
136
- def _parse_json(text: str) -> dict[str, str]:
136
+ def _parse_json(text: str) -> list[CookieRecord]:
137
+ """解析导入文件。list-form JSON → records(保留 path/domain);dict → coerce。"""
137
138
  data = json.loads(text)
138
139
  if isinstance(data, list):
139
- return {c["name"]: c["value"] for c in data if "name" in c and "value" in c}
140
+ return [
141
+ {
142
+ "name": c["name"],
143
+ "value": c["value"],
144
+ "domain": c.get("domain", ""),
145
+ "path": c.get("path", ""),
146
+ }
147
+ for c in data
148
+ if "name" in c and "value" in c
149
+ ]
140
150
  if isinstance(data, dict):
141
- return {str(k): str(v) for k, v in data.items()}
151
+ return _coerce_records(data)
142
152
  raise AuthRequiredError("cookie JSON 格式不识别(需 list 或 dict)")