goofish-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- goofish_cli/__init__.py +1 -0
- goofish_cli/cli.py +119 -0
- goofish_cli/commands/__init__.py +0 -0
- goofish_cli/commands/auth/__init__.py +0 -0
- goofish_cli/commands/auth/login.py +66 -0
- goofish_cli/commands/auth/reset_guard.py +16 -0
- goofish_cli/commands/auth/status.py +40 -0
- goofish_cli/commands/category/__init__.py +0 -0
- goofish_cli/commands/category/recommend.py +64 -0
- goofish_cli/commands/item/__init__.py +0 -0
- goofish_cli/commands/item/delete.py +32 -0
- goofish_cli/commands/item/get.py +34 -0
- goofish_cli/commands/item/publish.py +175 -0
- goofish_cli/commands/location/__init__.py +0 -0
- goofish_cli/commands/location/default.py +40 -0
- goofish_cli/commands/media/__init__.py +0 -0
- goofish_cli/commands/media/upload.py +53 -0
- goofish_cli/commands/message/__init__.py +0 -0
- goofish_cli/commands/message/history.py +23 -0
- goofish_cli/commands/message/send.py +102 -0
- goofish_cli/commands/message/watch.py +58 -0
- goofish_cli/core/__init__.py +26 -0
- goofish_cli/core/errors.py +31 -0
- goofish_cli/core/guard.py +63 -0
- goofish_cli/core/limiter.py +60 -0
- goofish_cli/core/mtop.py +131 -0
- goofish_cli/core/output.py +82 -0
- goofish_cli/core/registry.py +78 -0
- goofish_cli/core/session.py +88 -0
- goofish_cli/core/sign.py +37 -0
- goofish_cli/core/strategy.py +10 -0
- goofish_cli/core/token.py +88 -0
- goofish_cli/core/ws.py +493 -0
- goofish_cli/mcp_server.py +56 -0
- goofish_cli/static/goofish_js_version_2.js +568 -0
- goofish_cli-0.1.0.dist-info/METADATA +279 -0
- goofish_cli-0.1.0.dist-info/RECORD +41 -0
- goofish_cli-0.1.0.dist-info/WHEEL +4 -0
- goofish_cli-0.1.0.dist-info/entry_points.txt +3 -0
- goofish_cli-0.1.0.dist-info/licenses/LICENSE +190 -0
- goofish_cli-0.1.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""cookie 加载 + requests.Session 管理 + token 提取。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from goofish_cli.core.errors import AuthRequiredError
|
|
12
|
+
from goofish_cli.core.sign import generate_device_id
|
|
13
|
+
|
|
14
|
+
DEFAULT_COOKIE_PATH = Path.home() / ".goofish-cli" / "cookies.json"
|
|
15
|
+
DEVICE_CACHE_PATH = Path.home() / ".goofish-cli" / "device.json"
|
|
16
|
+
USER_AGENT = (
|
|
17
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
18
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
19
|
+
"Chrome/146.0.0.0 Safari/537.36"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Session:
|
|
25
|
+
http: requests.Session
|
|
26
|
+
unb: str
|
|
27
|
+
tracknick: str
|
|
28
|
+
device_id: str
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def load(cls, cookie_path: Path | str | None = None) -> Session:
|
|
32
|
+
path = Path(os.path.expanduser(
|
|
33
|
+
cookie_path or os.environ.get("GOOFISH_COOKIES_PATH") or DEFAULT_COOKIE_PATH
|
|
34
|
+
))
|
|
35
|
+
if not path.exists():
|
|
36
|
+
raise AuthRequiredError(
|
|
37
|
+
f"cookie 文件不存在:{path}。请先 `goofish auth login --from <path>` 导入。"
|
|
38
|
+
)
|
|
39
|
+
cookies = _load_cookies(path)
|
|
40
|
+
if "unb" not in cookies or "_m_h5_tk" not in cookies:
|
|
41
|
+
raise AuthRequiredError(
|
|
42
|
+
f"cookie 缺失 unb / _m_h5_tk,检查 {path} 是否完整(建议从 goofish.com 登录后再导一次)"
|
|
43
|
+
)
|
|
44
|
+
http = requests.Session()
|
|
45
|
+
http.cookies.update(cookies)
|
|
46
|
+
return cls(
|
|
47
|
+
http=http,
|
|
48
|
+
unb=cookies["unb"],
|
|
49
|
+
tracknick=cookies.get("tracknick", ""),
|
|
50
|
+
device_id=_load_or_mint_device_id(cookies["unb"]),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def h5_token(self) -> str:
|
|
55
|
+
raw = self.http.cookies.get("_m_h5_tk", "")
|
|
56
|
+
return raw.split("_")[0] if raw else ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _load_or_mint_device_id(unb: str) -> str:
|
|
60
|
+
"""device_id 必须在 unb 维度稳定。
|
|
61
|
+
|
|
62
|
+
IM WebSocket 的 accessToken 会绑定 (appKey, deviceId)。若每次 Session.load 调用
|
|
63
|
+
JS 重新随机生成 device_id,token 签发时用 A,/reg 时用 B,会返回 401
|
|
64
|
+
"device id or appkey is not equal"。
|
|
65
|
+
"""
|
|
66
|
+
if DEVICE_CACHE_PATH.exists():
|
|
67
|
+
try:
|
|
68
|
+
raw = json.loads(DEVICE_CACHE_PATH.read_text())
|
|
69
|
+
if raw.get("unb") == unb and raw.get("device_id"):
|
|
70
|
+
return raw["device_id"]
|
|
71
|
+
except (json.JSONDecodeError, OSError):
|
|
72
|
+
pass
|
|
73
|
+
device_id = generate_device_id(unb)
|
|
74
|
+
DEVICE_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
DEVICE_CACHE_PATH.write_text(json.dumps({"unb": unb, "device_id": device_id}))
|
|
76
|
+
DEVICE_CACHE_PATH.chmod(0o600)
|
|
77
|
+
return device_id
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _load_cookies(path: Path) -> dict[str, str]:
|
|
81
|
+
text = path.read_text()
|
|
82
|
+
raw = json.loads(text)
|
|
83
|
+
# 兼容两种格式:Chrome 扩展导出的 list[{name,value,...}] / 纯 dict
|
|
84
|
+
if isinstance(raw, list):
|
|
85
|
+
return {c["name"]: c["value"] for c in raw if "name" in c and "value" in c}
|
|
86
|
+
if isinstance(raw, dict):
|
|
87
|
+
return {str(k): str(v) for k, v in raw.items()}
|
|
88
|
+
raise AuthRequiredError(f"cookies.json 格式不识别:{path}")
|
goofish_cli/core/sign.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""execjs 桥接 goofish_js_version_2.js。提供 sign/device_id/mid/uuid/decrypt。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
from functools import lru_cache, partial
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
|
|
8
|
+
# 静默 Windows 编码问题(跨平台无害)
|
|
9
|
+
subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")
|
|
10
|
+
|
|
11
|
+
import execjs # noqa: E402 必须在 subprocess 补丁之后
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache(maxsize=1)
|
|
15
|
+
def _ctx() -> execjs._abstract_runtime.AbstractRuntimeContext:
|
|
16
|
+
js_path = files("goofish_cli.static").joinpath("goofish_js_version_2.js")
|
|
17
|
+
return execjs.compile(js_path.read_text(encoding="utf-8"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_sign(t: str, token: str, data: str) -> str:
|
|
21
|
+
return _ctx().call("generate_sign", t, token, data)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_device_id(user_id: str) -> str:
|
|
25
|
+
return _ctx().call("generate_device_id", user_id)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def generate_mid() -> str:
|
|
29
|
+
return _ctx().call("generate_mid")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_uuid() -> str:
|
|
33
|
+
return _ctx().call("generate_uuid")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decrypt(data: str) -> str:
|
|
37
|
+
return _ctx().call("decrypt", data)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""IM accessToken 获取/刷新。对应 mtop.taobao.idlemessage.pc.login.token。
|
|
2
|
+
|
|
3
|
+
用于 WebSocket 鉴权(/reg 阶段)。不同于 HTTP 的 _m_h5_tk,这个 token 是给 IM 长连用的。
|
|
4
|
+
|
|
5
|
+
accessToken 会被缓存到 ~/.goofish-cli/im_token.json(TTL 由 GOOFISH_TOKEN_TTL 控制,默认 30 分钟)。
|
|
6
|
+
闲鱼风控对 `mtop.taobao.idlemessage.pc.login.token` 比较敏感,高频调用会 RGV587。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from goofish_cli.core.errors import AuthRequiredError
|
|
16
|
+
from goofish_cli.core.mtop import call
|
|
17
|
+
from goofish_cli.core.session import Session
|
|
18
|
+
|
|
19
|
+
IM_APP_KEY = "444e9908a51d1cb236a27862abc769c9"
|
|
20
|
+
TOKEN_CACHE = Path.home() / ".goofish-cli" / "im_token.json"
|
|
21
|
+
DEFAULT_TTL = 30 * 60 # 30 分钟,远低于真实 token 过期(观察约 2h)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ttl() -> int:
|
|
25
|
+
try:
|
|
26
|
+
return max(60, int(os.environ.get("GOOFISH_TOKEN_TTL", DEFAULT_TTL)))
|
|
27
|
+
except ValueError:
|
|
28
|
+
return DEFAULT_TTL
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load_cache(unb: str) -> str | None:
|
|
32
|
+
if not TOKEN_CACHE.exists():
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
raw = json.loads(TOKEN_CACHE.read_text())
|
|
36
|
+
except (json.JSONDecodeError, OSError):
|
|
37
|
+
return None
|
|
38
|
+
if raw.get("unb") != unb:
|
|
39
|
+
return None
|
|
40
|
+
if time.time() - float(raw.get("t", 0)) > _ttl():
|
|
41
|
+
return None
|
|
42
|
+
return raw.get("token") or None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _save_cache(unb: str, token: str) -> None:
|
|
46
|
+
TOKEN_CACHE.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
TOKEN_CACHE.write_text(json.dumps({"unb": unb, "token": token, "t": time.time()}))
|
|
48
|
+
TOKEN_CACHE.chmod(0o600)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_access_token(session: Session, *, force_refresh: bool = False) -> str:
|
|
52
|
+
"""取 WebSocket 用的 accessToken。
|
|
53
|
+
|
|
54
|
+
优先级:环境变量 GOOFISH_IM_TOKEN > 文件缓存 > 调用 mtop 接口。
|
|
55
|
+
支持注入原因:`mtop.taobao.idlemessage.pc.login.token` 风控非常敏感,
|
|
56
|
+
完成滑块后 web 端自己持有的 token 可以直接拷出来塞给 CLI 用。
|
|
57
|
+
"""
|
|
58
|
+
env_token = os.environ.get("GOOFISH_IM_TOKEN", "").strip()
|
|
59
|
+
if env_token:
|
|
60
|
+
return env_token
|
|
61
|
+
if not force_refresh:
|
|
62
|
+
cached = _load_cache(session.unb)
|
|
63
|
+
if cached:
|
|
64
|
+
return cached
|
|
65
|
+
data = {"appKey": IM_APP_KEY, "deviceId": session.device_id}
|
|
66
|
+
raw = call(
|
|
67
|
+
session,
|
|
68
|
+
api="mtop.taobao.idlemessage.pc.login.token",
|
|
69
|
+
data=data,
|
|
70
|
+
version="1.0",
|
|
71
|
+
spm_cnt="a21ybx.im.0.0",
|
|
72
|
+
)
|
|
73
|
+
token = (raw.get("data") or {}).get("accessToken", "")
|
|
74
|
+
if not token:
|
|
75
|
+
raise AuthRequiredError(f"accessToken 获取失败:{raw.get('ret')}")
|
|
76
|
+
_save_cache(session.unb, token)
|
|
77
|
+
return token
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def refresh_login(session: Session) -> dict[str, Any]:
|
|
81
|
+
"""刷新登录态(轻量 ping),长连常驻时 10 分钟调一次避免掉线。"""
|
|
82
|
+
return call(
|
|
83
|
+
session,
|
|
84
|
+
api="mtop.taobao.idlemessage.pc.loginuser.get",
|
|
85
|
+
data={},
|
|
86
|
+
version="1.0",
|
|
87
|
+
spm_cnt="a21ybx.im.0.0",
|
|
88
|
+
)
|