pymecli 0.6.0__tar.gz → 0.6.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.
- {pymecli-0.6.0 → pymecli-0.6.1}/PKG-INFO +26 -1
- {pymecli-0.6.0 → pymecli-0.6.1}/README.md +25 -0
- pymecli-0.6.1/cli/dnspod.py +259 -0
- pymecli-0.6.1/cli/quark.py +258 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/pyproject.toml +3 -1
- pymecli-0.6.1/utils/notify.py +1092 -0
- pymecli-0.6.1/utils/sendNotify.py +429 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/.gitignore +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/api/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/api/v1/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/api/v1/clash.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/api/v1/redis.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/bitget.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/dst.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/etcd_mysql.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/example.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/fast.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/gate.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/okx.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/redis_csv.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/tele.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/cli/util.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/core/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/core/clash.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/core/config.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/core/redis_client.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/crypto/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/crypto/bitget.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/crypto/gate.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/crypto/okx.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/data/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/data/dou_dict.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/data/dou_list.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/data/main.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/data/template.yaml +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/models/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/models/douzero_model.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/models/ocr_model.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/models/response.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/elapsed.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/etcd.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/helper.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/logger.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/mysql.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/path.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/pd.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/pyredis.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/sleep.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/text.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.1}/utils/toml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pymecli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: My CLI
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/pymecli
|
|
6
6
|
Project-URL: Repository, https://github.com/meme2046/pymecli
|
|
@@ -72,3 +72,28 @@ etcdsync etcd2mysql "" -e d:/.env/pymecli.env -t kvs # 全量
|
|
|
72
72
|
etcdsync mysql2etcd /cron/jobs -e d:/.env/pymecli.env -t kvs
|
|
73
73
|
etcdsync mysql2etcd -e d:/.env/pymecli.env -t kvs # 全量
|
|
74
74
|
```
|
|
75
|
+
# quark
|
|
76
|
+
```shell
|
|
77
|
+
# 执行签到(默认会发通知)
|
|
78
|
+
uv run quark sign
|
|
79
|
+
# 签到但不发通知
|
|
80
|
+
uv run quark sign --no-notify
|
|
81
|
+
# 指定不同的 dotenv 路径
|
|
82
|
+
uv run quark sign -e /path/to/.env
|
|
83
|
+
# 只检查账号状态(不签到),方便调试
|
|
84
|
+
uv run quark check
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
# dnspod
|
|
88
|
+
```shell
|
|
89
|
+
# 列出域名下所有记录(先跑这个查 record_id)
|
|
90
|
+
uv run dnspod list
|
|
91
|
+
# 更新 IPv4 A 记录
|
|
92
|
+
uv run dnspod ddns
|
|
93
|
+
# 强制更新(即使 IP 没变化)
|
|
94
|
+
uv run dnspod ddns --force
|
|
95
|
+
# 更新 IPv6 AAAA 记录(从 Redis 读 local.IPv6)
|
|
96
|
+
uv run dnspod ddns6
|
|
97
|
+
|
|
98
|
+
uv run dnspod ddns6 --force
|
|
99
|
+
```
|
|
@@ -33,3 +33,28 @@ etcdsync etcd2mysql "" -e d:/.env/pymecli.env -t kvs # 全量
|
|
|
33
33
|
etcdsync mysql2etcd /cron/jobs -e d:/.env/pymecli.env -t kvs
|
|
34
34
|
etcdsync mysql2etcd -e d:/.env/pymecli.env -t kvs # 全量
|
|
35
35
|
```
|
|
36
|
+
# quark
|
|
37
|
+
```shell
|
|
38
|
+
# 执行签到(默认会发通知)
|
|
39
|
+
uv run quark sign
|
|
40
|
+
# 签到但不发通知
|
|
41
|
+
uv run quark sign --no-notify
|
|
42
|
+
# 指定不同的 dotenv 路径
|
|
43
|
+
uv run quark sign -e /path/to/.env
|
|
44
|
+
# 只检查账号状态(不签到),方便调试
|
|
45
|
+
uv run quark check
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
# dnspod
|
|
49
|
+
```shell
|
|
50
|
+
# 列出域名下所有记录(先跑这个查 record_id)
|
|
51
|
+
uv run dnspod list
|
|
52
|
+
# 更新 IPv4 A 记录
|
|
53
|
+
uv run dnspod ddns
|
|
54
|
+
# 强制更新(即使 IP 没变化)
|
|
55
|
+
uv run dnspod ddns --force
|
|
56
|
+
# 更新 IPv6 AAAA 记录(从 Redis 读 local.IPv6)
|
|
57
|
+
uv run dnspod ddns6
|
|
58
|
+
|
|
59
|
+
uv run dnspod ddns6 --force
|
|
60
|
+
```
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""DNSPod DDNS 自动更新 CLI。
|
|
2
|
+
|
|
3
|
+
环境变量 (Go 版命名保持一致):
|
|
4
|
+
DP_ID: DNSPod 登录 ID
|
|
5
|
+
DP_TOKEN: DNSPod 登录 Token
|
|
6
|
+
DP_DOMAIN: 域名 (如 cursor.email)
|
|
7
|
+
DP_RECORD_ID: IPv4 A 记录 ID
|
|
8
|
+
DP_RECORD_ID_IPV6: IPv6 AAAA 记录 ID
|
|
9
|
+
DP_SUB_DOMAIN: 子域名前缀 (默认 "api")
|
|
10
|
+
DP_RECORD_LINE: 记录线路 (默认 "默认")
|
|
11
|
+
|
|
12
|
+
Redis 依赖 (用于 IPv6 DDNS):
|
|
13
|
+
REDIS_URL: Redis 地址,key = "local.IPv6"
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
import typer
|
|
20
|
+
from dotenv import load_dotenv
|
|
21
|
+
|
|
22
|
+
from utils.logger import get_logger
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
app = typer.Typer(help="DNSPod DDNS 自动更新")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
API_BASE = "https://dnsapi.cn"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# 底层 API
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def _call(action: str, data: dict) -> dict:
|
|
36
|
+
"""通用 DNSPod API 调用(form-urlencoded POST)"""
|
|
37
|
+
resp = requests.post(f"{API_BASE}/{action}", data=data, timeout=15)
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
body = resp.json()
|
|
40
|
+
|
|
41
|
+
status = body.get("status", {})
|
|
42
|
+
code = str(status.get("code", ""))
|
|
43
|
+
if code != "1":
|
|
44
|
+
msg = status.get("message", "未知错误")
|
|
45
|
+
raise RuntimeError(f"DNSPod API 错误 [{code}]: {msg}")
|
|
46
|
+
|
|
47
|
+
return body
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def record_modify(
|
|
51
|
+
login_token: str,
|
|
52
|
+
domain: str,
|
|
53
|
+
record_id: str,
|
|
54
|
+
sub_domain: str,
|
|
55
|
+
record_type: str,
|
|
56
|
+
value: str,
|
|
57
|
+
record_line: str = "默认",
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""调用 Record.Modify 修改 DNS 记录"""
|
|
60
|
+
data = {
|
|
61
|
+
"login_token": login_token,
|
|
62
|
+
"format": "json",
|
|
63
|
+
"domain": domain,
|
|
64
|
+
"record_id": record_id,
|
|
65
|
+
"sub_domain": sub_domain,
|
|
66
|
+
"record_type": record_type,
|
|
67
|
+
"record_line": record_line,
|
|
68
|
+
"value": value,
|
|
69
|
+
}
|
|
70
|
+
return _call("Record.Modify", data)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def record_list(login_token: str, domain: str) -> list[dict]:
|
|
74
|
+
"""调用 Record.List 列出域名下所有记录"""
|
|
75
|
+
data = {
|
|
76
|
+
"login_token": login_token,
|
|
77
|
+
"format": "json",
|
|
78
|
+
"domain": domain,
|
|
79
|
+
}
|
|
80
|
+
body = _call("Record.List", data)
|
|
81
|
+
return body.get("records", [])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_public_ipv4() -> str:
|
|
85
|
+
"""从 ip.3322.net 获取公网 IPv4(纯文本)"""
|
|
86
|
+
resp = requests.get("http://ip.3322.net", timeout=10)
|
|
87
|
+
resp.raise_for_status()
|
|
88
|
+
ip = resp.text.strip()
|
|
89
|
+
# 简单校验
|
|
90
|
+
if not re.match(r"^\d{1,3}(?:\.\d{1,3}){3}$", ip):
|
|
91
|
+
raise RuntimeError(f"获取到的不是有效 IPv4: {ip!r}")
|
|
92
|
+
return ip
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_public_ipv6() -> str:
|
|
96
|
+
"""从 Redis 读取公网 IPv6(key=local.IPv6)"""
|
|
97
|
+
from utils.pyredis import get_redis_client_sync
|
|
98
|
+
|
|
99
|
+
r = get_redis_client_sync()
|
|
100
|
+
try:
|
|
101
|
+
val = r.get("local.IPv6")
|
|
102
|
+
if not val:
|
|
103
|
+
raise RuntimeError("Redis 中 local.IPv6 为空")
|
|
104
|
+
return val.strip()
|
|
105
|
+
finally:
|
|
106
|
+
r.close()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# CLI 子命令
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _require_env(env_path: str) -> dict:
|
|
114
|
+
"""加载 dotenv 并返回公共配置"""
|
|
115
|
+
load_dotenv(env_path)
|
|
116
|
+
dp_id = os.getenv("DP_ID")
|
|
117
|
+
dp_token = os.getenv("DP_TOKEN")
|
|
118
|
+
dp_domain = os.getenv("DP_DOMAIN")
|
|
119
|
+
|
|
120
|
+
missing = []
|
|
121
|
+
if not dp_id:
|
|
122
|
+
missing.append("DP_ID")
|
|
123
|
+
if not dp_token:
|
|
124
|
+
missing.append("DP_TOKEN")
|
|
125
|
+
if not dp_domain:
|
|
126
|
+
missing.append("DP_DOMAIN")
|
|
127
|
+
if missing:
|
|
128
|
+
logger.error("缺少环境变量: %s", ", ".join(missing))
|
|
129
|
+
raise typer.Exit(1)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"login_token": f"{dp_id},{dp_token}",
|
|
133
|
+
"domain": dp_domain,
|
|
134
|
+
"sub_domain": os.getenv("DP_SUB_DOMAIN", "api"),
|
|
135
|
+
"record_line": os.getenv("DP_RECORD_LINE", "默认"),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.command()
|
|
140
|
+
def list(
|
|
141
|
+
dotenv_path: str = typer.Option(".env", "--env", "-e", help="dotenv 文件路径"),
|
|
142
|
+
):
|
|
143
|
+
"""列出域名下所有 DNS 记录(方便查 record_id)"""
|
|
144
|
+
cfg = _require_env(dotenv_path)
|
|
145
|
+
|
|
146
|
+
records = record_list(cfg["login_token"], cfg["domain"])
|
|
147
|
+
if not records:
|
|
148
|
+
typer.secho("没有记录", fg=typer.colors.YELLOW)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
typer.secho(f"\n域名 {cfg['domain']} 共 {len(records)} 条记录:\n", fg=typer.colors.CYAN)
|
|
152
|
+
typer.secho(f"{'ID':<10} {'类型':<6} {'子域':<16} {'值':<30} {'状态'}", fg=typer.colors.GREEN)
|
|
153
|
+
typer.secho("-" * 80, fg=typer.colors.CYAN)
|
|
154
|
+
for r in records:
|
|
155
|
+
typer.echo(
|
|
156
|
+
f"{r['id']:<10} {r['type']:<6} {r['name']:<16} {r['value']:<30} {r['status']}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.command()
|
|
161
|
+
def ddns(
|
|
162
|
+
dotenv_path: str = typer.Option(".env", "--env", "-e", help="dotenv 文件路径"),
|
|
163
|
+
force: bool = typer.Option(False, "--force", help="IP 未变化也强制更新"),
|
|
164
|
+
):
|
|
165
|
+
"""更新 A 记录(IPv4 DDNS)"""
|
|
166
|
+
cfg = _require_env(dotenv_path)
|
|
167
|
+
|
|
168
|
+
record_id = os.getenv("DP_RECORD_ID")
|
|
169
|
+
if not record_id:
|
|
170
|
+
logger.error("缺少环境变量: DP_RECORD_ID")
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
cur_ip = get_public_ipv4()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.exception("获取公网 IPv4 失败: %s", e)
|
|
177
|
+
raise typer.Exit(1)
|
|
178
|
+
|
|
179
|
+
typer.secho(f"当前公网 IPv4: {cur_ip}", fg=typer.colors.CYAN)
|
|
180
|
+
|
|
181
|
+
# 获取现有记录对比
|
|
182
|
+
records = record_list(cfg["login_token"], cfg["domain"])
|
|
183
|
+
existing = next((r for r in records if r["id"] == record_id), None)
|
|
184
|
+
if existing and existing["value"] == cur_ip and not force:
|
|
185
|
+
typer.secho(f"IP 未变化 ({cur_ip}),跳过更新", fg=typer.colors.YELLOW)
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
resp = record_modify(
|
|
190
|
+
login_token=cfg["login_token"],
|
|
191
|
+
domain=cfg["domain"],
|
|
192
|
+
record_id=record_id,
|
|
193
|
+
sub_domain=cfg["sub_domain"],
|
|
194
|
+
record_type="A",
|
|
195
|
+
value=cur_ip,
|
|
196
|
+
record_line=cfg["record_line"],
|
|
197
|
+
)
|
|
198
|
+
typer.secho(
|
|
199
|
+
f"✅ A 记录更新成功 | "
|
|
200
|
+
f"新地址: {resp['record']['value']} | "
|
|
201
|
+
f"接口返回: {resp['status']['message']}",
|
|
202
|
+
fg=typer.colors.GREEN,
|
|
203
|
+
)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.exception("A 记录更新失败: %s", e)
|
|
206
|
+
raise typer.Exit(1)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command()
|
|
210
|
+
def ddns6(
|
|
211
|
+
dotenv_path: str = typer.Option(".env", "--env", "-e", help="dotenv 文件路径"),
|
|
212
|
+
force: bool = typer.Option(False, "--force", help="IP 未变化也强制更新"),
|
|
213
|
+
):
|
|
214
|
+
"""更新 AAAA 记录(IPv6 DDNS,从 Redis 读取 IPv6)"""
|
|
215
|
+
cfg = _require_env(dotenv_path)
|
|
216
|
+
|
|
217
|
+
record_id = os.getenv("DP_RECORD_ID_IPV6")
|
|
218
|
+
if not record_id:
|
|
219
|
+
logger.error("缺少环境变量: DP_RECORD_ID_IPV6")
|
|
220
|
+
raise typer.Exit(1)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
cur_ip = get_public_ipv6()
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.exception("获取公网 IPv6 失败: %s", e)
|
|
226
|
+
raise typer.Exit(1)
|
|
227
|
+
|
|
228
|
+
typer.secho(f"当前公网 IPv6: {cur_ip}", fg=typer.colors.CYAN)
|
|
229
|
+
|
|
230
|
+
# 获取现有记录对比
|
|
231
|
+
records = record_list(cfg["login_token"], cfg["domain"])
|
|
232
|
+
existing = next((r for r in records if r["id"] == record_id), None)
|
|
233
|
+
if existing and existing["value"] == cur_ip and not force:
|
|
234
|
+
typer.secho(f"IPv6 未变化 ({cur_ip}),跳过更新", fg=typer.colors.YELLOW)
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
resp = record_modify(
|
|
239
|
+
login_token=cfg["login_token"],
|
|
240
|
+
domain=cfg["domain"],
|
|
241
|
+
record_id=record_id,
|
|
242
|
+
sub_domain=cfg["sub_domain"],
|
|
243
|
+
record_type="AAAA",
|
|
244
|
+
value=cur_ip,
|
|
245
|
+
record_line=cfg["record_line"],
|
|
246
|
+
)
|
|
247
|
+
typer.secho(
|
|
248
|
+
f"✅ AAAA 记录更新成功 | "
|
|
249
|
+
f"新地址: {resp['record']['value']} | "
|
|
250
|
+
f"接口返回: {resp['status']['message']}",
|
|
251
|
+
fg=typer.colors.GREEN,
|
|
252
|
+
)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.exception("AAAA 记录更新失败: %s", e)
|
|
255
|
+
raise typer.Exit(1)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
app()
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""夸克网盘自动签到 CLI。
|
|
2
|
+
|
|
3
|
+
环境变量:
|
|
4
|
+
COOKIE_QUARK: 多账户用 回车 或 && 分开。
|
|
5
|
+
新格式: user=张三; url=https://drive-m.quark.cn/1/clouddrive/act/growth/reward?...&kps=xxx&sign=xxx&vcode=xxx;
|
|
6
|
+
旧格式: user=张三; kps=xxx; sign=xxx; vcode=xxx;
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
import typer
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
|
|
16
|
+
from utils.logger import get_logger
|
|
17
|
+
from utils.notify import send
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
app = typer.Typer(help="夸克网盘自动签到")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# 核心逻辑(改编自 checkIn_Quark.py)
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _convert_bytes(b: float) -> str:
|
|
29
|
+
"""将字节转换为人类可读格式 (B/KB/MB/GB/TB/...)"""
|
|
30
|
+
units = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
|
31
|
+
i = 0
|
|
32
|
+
while b >= 1024 and i < len(units) - 1:
|
|
33
|
+
b /= 1024
|
|
34
|
+
i += 1
|
|
35
|
+
return f"{b:.2f} {units[i]}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _extract_params(url: str) -> dict:
|
|
39
|
+
"""从完整 URL 中提取 kps / sign / vcode 参数"""
|
|
40
|
+
query_start = url.find("?")
|
|
41
|
+
query_string = url[query_start + 1 :] if query_start != -1 else ""
|
|
42
|
+
|
|
43
|
+
params = {}
|
|
44
|
+
for param in query_string.split("&"):
|
|
45
|
+
if "=" in param:
|
|
46
|
+
key, value = param.split("=", 1)
|
|
47
|
+
params[key] = value
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"kps": params.get("kps", ""),
|
|
51
|
+
"sign": params.get("sign", ""),
|
|
52
|
+
"vcode": params.get("vcode", ""),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _Quark:
|
|
57
|
+
"""夸克签到 API 封装(内部类,不对外暴露)"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, user_data: dict):
|
|
60
|
+
self.param = user_data
|
|
61
|
+
|
|
62
|
+
# ---- API ----
|
|
63
|
+
|
|
64
|
+
def get_growth_info(self):
|
|
65
|
+
url = "https://drive-m.quark.cn/1/clouddrive/capacity/growth/info"
|
|
66
|
+
resp = requests.get(url, params=self._qs(pr="ucpro", fr="android")).json()
|
|
67
|
+
return resp.get("data") or False
|
|
68
|
+
|
|
69
|
+
def get_growth_sign(self):
|
|
70
|
+
url = "https://drive-m.quark.cn/1/clouddrive/capacity/growth/sign"
|
|
71
|
+
resp = requests.post(
|
|
72
|
+
url, json={"sign_cyclic": True}, params=self._qs(pr="ucpro", fr="android")
|
|
73
|
+
).json()
|
|
74
|
+
if resp.get("data"):
|
|
75
|
+
return True, resp["data"]["sign_daily_reward"]
|
|
76
|
+
return False, resp.get("message", "未知错误")
|
|
77
|
+
|
|
78
|
+
def query_balance(self):
|
|
79
|
+
url = "https://coral2.quark.cn/currency/v1/queryBalance"
|
|
80
|
+
resp = requests.get(
|
|
81
|
+
url,
|
|
82
|
+
params={
|
|
83
|
+
"moduleCode": "1f3563d38896438db994f118d4ff53cb",
|
|
84
|
+
"kps": self.param.get("kps"),
|
|
85
|
+
},
|
|
86
|
+
).json()
|
|
87
|
+
if resp.get("data"):
|
|
88
|
+
return resp["data"]["balance"]
|
|
89
|
+
return resp.get("msg", "未知错误")
|
|
90
|
+
|
|
91
|
+
# ---- 业务入口 ----
|
|
92
|
+
|
|
93
|
+
def do_sign(self) -> str:
|
|
94
|
+
log = ""
|
|
95
|
+
growth_info = self.get_growth_info()
|
|
96
|
+
if growth_info:
|
|
97
|
+
log += (
|
|
98
|
+
f" {'88VIP' if growth_info['88VIP'] else '普通用户'} {self.param.get('user')}\n"
|
|
99
|
+
f"💾 网盘总容量:{_convert_bytes(growth_info['total_capacity'])},"
|
|
100
|
+
f"签到累计容量:"
|
|
101
|
+
)
|
|
102
|
+
comp = growth_info.get("cap_composition", {})
|
|
103
|
+
if "sign_reward" in comp:
|
|
104
|
+
log += f"{_convert_bytes(comp['sign_reward'])}\n"
|
|
105
|
+
else:
|
|
106
|
+
log += "0 MB\n"
|
|
107
|
+
|
|
108
|
+
cap_sign = growth_info["cap_sign"]
|
|
109
|
+
if cap_sign["sign_daily"]:
|
|
110
|
+
log += (
|
|
111
|
+
f"✅ 签到日志: 今日已签到+{_convert_bytes(cap_sign['sign_daily_reward'])},"
|
|
112
|
+
f"连签进度({cap_sign['sign_progress']}/{cap_sign['sign_target']})\n"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
ok, ret = self.get_growth_sign()
|
|
116
|
+
if ok:
|
|
117
|
+
log += (
|
|
118
|
+
f"✅ 执行签到: 今日签到+{_convert_bytes(ret)},"
|
|
119
|
+
f"连签进度({cap_sign['sign_progress'] + 1}/{cap_sign['sign_target']})\n"
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
log += f"❌ 签到异常: {ret}\n"
|
|
123
|
+
else:
|
|
124
|
+
log += "❌ 签到异常: 获取成长信息失败\n"
|
|
125
|
+
|
|
126
|
+
return log
|
|
127
|
+
|
|
128
|
+
# ---- 内部工具 ----
|
|
129
|
+
|
|
130
|
+
def _qs(self, **extra) -> dict:
|
|
131
|
+
"""构造请求公共 query string 参数"""
|
|
132
|
+
qs = {
|
|
133
|
+
"kps": self.param.get("kps"),
|
|
134
|
+
"sign": self.param.get("sign"),
|
|
135
|
+
"vcode": self.param.get("vcode"),
|
|
136
|
+
}
|
|
137
|
+
qs.update(extra)
|
|
138
|
+
return qs
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# CLI 子命令
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command()
|
|
147
|
+
def sign(
|
|
148
|
+
dotenv_path: str = typer.Option(".env", "--env", "-e", help="dotenv 文件路径"),
|
|
149
|
+
notify_flag: bool = typer.Option(True, "--notify/--no-notify", help="是否发送通知"),
|
|
150
|
+
):
|
|
151
|
+
"""执行夸克网盘每日签到。
|
|
152
|
+
|
|
153
|
+
需要 .env 中配置 COOKIE_QUARK。多账户用 回车 或 && 分隔。
|
|
154
|
+
"""
|
|
155
|
+
load_dotenv(dotenv_path)
|
|
156
|
+
|
|
157
|
+
raw = os.getenv("COOKIE_QUARK")
|
|
158
|
+
if not raw:
|
|
159
|
+
logger.error("未添加 COOKIE_QUARK 环境变量")
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
# 解析多账户
|
|
163
|
+
cookie_list = re.split(r"\n|&&", raw)
|
|
164
|
+
cookie_list = [c.strip() for c in cookie_list if c.strip()]
|
|
165
|
+
|
|
166
|
+
typer.secho(f"✅ 检测到共 {len(cookie_list)} 个夸克账号\n", fg=typer.colors.GREEN)
|
|
167
|
+
|
|
168
|
+
msg = ""
|
|
169
|
+
for i, raw_account in enumerate(cookie_list):
|
|
170
|
+
# 解析 user_data
|
|
171
|
+
user_data: dict = {}
|
|
172
|
+
for part in raw_account.replace(" ", "").split(";"):
|
|
173
|
+
if not part or "=" not in part:
|
|
174
|
+
continue
|
|
175
|
+
k, v = part.split("=", 1)
|
|
176
|
+
user_data[k] = v
|
|
177
|
+
|
|
178
|
+
# 从 url 中提取 kps / sign / vcode(兼容新格式)
|
|
179
|
+
if "url" in user_data:
|
|
180
|
+
user_data.update(_extract_params(user_data["url"]))
|
|
181
|
+
|
|
182
|
+
# 必须的参数检查
|
|
183
|
+
missing = [k for k in ("kps", "sign", "vcode") if not user_data.get(k)]
|
|
184
|
+
if missing:
|
|
185
|
+
msg += f"🙍🏻♂️ 第{i + 1}个账号 ❌ 缺少参数: {', '.join(missing)}\n"
|
|
186
|
+
logger.warning("账号 %s 缺少参数: %s", i + 1, missing)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
msg += f"🙍🏻♂️ 第{i + 1}个账号\n"
|
|
190
|
+
result = _Quark(user_data).do_sign()
|
|
191
|
+
msg += result + "\n"
|
|
192
|
+
|
|
193
|
+
# 实时打印
|
|
194
|
+
typer.echo(f"🙍🏻♂️ 第{i + 1}个账号 {user_data.get('user', '(未命名)')}")
|
|
195
|
+
typer.echo(result)
|
|
196
|
+
|
|
197
|
+
# 发送通知
|
|
198
|
+
if notify_flag and msg:
|
|
199
|
+
try:
|
|
200
|
+
send("夸克自动签到", msg)
|
|
201
|
+
except Exception:
|
|
202
|
+
logger.exception("发送通知失败")
|
|
203
|
+
|
|
204
|
+
# 返回给调用者(方便编程调用)
|
|
205
|
+
typer.secho("\n----------夸克网盘签到完毕----------", fg=typer.colors.CYAN)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command()
|
|
209
|
+
def check(
|
|
210
|
+
dotenv_path: str = typer.Option(".env", "--env", "-e", help="dotenv 文件路径"),
|
|
211
|
+
):
|
|
212
|
+
"""检查 COOKIE_QUARK 是否配置,以及每个账号的成长信息。"""
|
|
213
|
+
load_dotenv(dotenv_path)
|
|
214
|
+
|
|
215
|
+
raw = os.getenv("COOKIE_QUARK")
|
|
216
|
+
if not raw:
|
|
217
|
+
logger.error("未添加 COOKIE_QUARK 环境变量")
|
|
218
|
+
raise typer.Exit(1)
|
|
219
|
+
|
|
220
|
+
cookie_list = re.split(r"\n|&&", raw)
|
|
221
|
+
cookie_list = [c.strip() for c in cookie_list if c.strip()]
|
|
222
|
+
|
|
223
|
+
typer.secho(f"✅ 检测到共 {len(cookie_list)} 个夸克账号\n", fg=typer.colors.GREEN)
|
|
224
|
+
|
|
225
|
+
for i, raw_account in enumerate(cookie_list):
|
|
226
|
+
user_data: dict = {}
|
|
227
|
+
for part in raw_account.replace(" ", "").split(";"):
|
|
228
|
+
if not part or "=" not in part:
|
|
229
|
+
continue
|
|
230
|
+
k, v = part.split("=", 1)
|
|
231
|
+
user_data[k] = v
|
|
232
|
+
if "url" in user_data:
|
|
233
|
+
user_data.update(_extract_params(user_data["url"]))
|
|
234
|
+
|
|
235
|
+
typer.secho(
|
|
236
|
+
f"🙍🏻♂️ 第{i + 1}个账号 {user_data.get('user', '(未命名)')}",
|
|
237
|
+
fg=typer.colors.CYAN,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
missing = [k for k in ("kps", "sign", "vcode") if not user_data.get(k)]
|
|
241
|
+
if missing:
|
|
242
|
+
typer.secho(f" ❌ 缺少参数: {', '.join(missing)}", fg=typer.colors.RED)
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
growth = _Quark(user_data).get_growth_info()
|
|
246
|
+
if growth:
|
|
247
|
+
typer.secho(
|
|
248
|
+
f" 👤 {'88VIP' if growth['88VIP'] else '普通用户'} | "
|
|
249
|
+
f"💾 {_convert_bytes(growth['total_capacity'])} | "
|
|
250
|
+
f"✅今日已签到={growth['cap_sign']['sign_daily']}",
|
|
251
|
+
fg=typer.colors.GREEN,
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
typer.secho(" ❌ 获取成长信息失败(参数可能已过期)", fg=typer.colors.RED)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__":
|
|
258
|
+
app()
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
name = "pymecli"
|
|
39
39
|
readme = "README.md"
|
|
40
40
|
requires-python = ">=3.10"
|
|
41
|
-
version = "0.6.
|
|
41
|
+
version = "0.6.1"
|
|
42
42
|
|
|
43
43
|
[project.urls]
|
|
44
44
|
Homepage = "https://pypi.org/project/pymecli"
|
|
@@ -47,12 +47,14 @@
|
|
|
47
47
|
[project.scripts]
|
|
48
48
|
# baidu = "cli.baidu:app"
|
|
49
49
|
bitget = "cli.bitget:app"
|
|
50
|
+
dnspod = "cli.dnspod:app"
|
|
50
51
|
dst = "cli.dst:app"
|
|
51
52
|
etcdsync = "cli.etcd_mysql:app"
|
|
52
53
|
example = "cli.example:app"
|
|
53
54
|
fast = "cli.fast:typer_app"
|
|
54
55
|
gate = "cli.gate:app"
|
|
55
56
|
okx = "cli.okx:app"
|
|
57
|
+
quark = "cli.quark:app"
|
|
56
58
|
util = "cli.util:app"
|
|
57
59
|
# 临时命令
|
|
58
60
|
rd = "cli.redis_csv:app"
|