yifaban-cli 0.1.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.
- yifaban_cli-0.1.0/PKG-INFO +98 -0
- yifaban_cli-0.1.0/README.md +85 -0
- yifaban_cli-0.1.0/pyproject.toml +24 -0
- yifaban_cli-0.1.0/setup.cfg +4 -0
- yifaban_cli-0.1.0/yifaban/__init__.py +29 -0
- yifaban_cli-0.1.0/yifaban/api.py +219 -0
- yifaban_cli-0.1.0/yifaban/cli.py +520 -0
- yifaban_cli-0.1.0/yifaban/config.py +78 -0
- yifaban_cli-0.1.0/yifaban/constants.py +34 -0
- yifaban_cli-0.1.0/yifaban/errors.py +14 -0
- yifaban_cli-0.1.0/yifaban/logger.py +66 -0
- yifaban_cli-0.1.0/yifaban/publishers/base.py +136 -0
- yifaban_cli-0.1.0/yifaban/publishers/honor.py +229 -0
- yifaban_cli-0.1.0/yifaban/publishers/huawei.py +228 -0
- yifaban_cli-0.1.0/yifaban/publishers/oppo.py +255 -0
- yifaban_cli-0.1.0/yifaban/publishers/tencent.py +422 -0
- yifaban_cli-0.1.0/yifaban/publishers/vivo.py +200 -0
- yifaban_cli-0.1.0/yifaban/publishers/xiaomi.py +399 -0
- yifaban_cli-0.1.0/yifaban/utils.py +53 -0
- yifaban_cli-0.1.0/yifaban_cli.egg-info/PKG-INFO +98 -0
- yifaban_cli-0.1.0/yifaban_cli.egg-info/SOURCES.txt +23 -0
- yifaban_cli-0.1.0/yifaban_cli.egg-info/dependency_links.txt +1 -0
- yifaban_cli-0.1.0/yifaban_cli.egg-info/entry_points.txt +2 -0
- yifaban_cli-0.1.0/yifaban_cli.egg-info/requires.txt +6 -0
- yifaban_cli-0.1.0/yifaban_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yifaban-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 基于易发版账号登录,从本地电脑直接把渠道包发布到各大应用市场
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: requests>=2.25.1
|
|
8
|
+
Requires-Dist: urllib3==1.26.15
|
|
9
|
+
Requires-Dist: cryptography>=3.4.8
|
|
10
|
+
Requires-Dist: pycryptodome>=3.15.0
|
|
11
|
+
Requires-Dist: requests-toolbelt==1.0.0
|
|
12
|
+
Requires-Dist: typer>=0.12.0
|
|
13
|
+
|
|
14
|
+
# apk-publisher-cli
|
|
15
|
+
|
|
16
|
+
基于**易发版账号登录(默认 Q助理扫码)**,从本地电脑直接把渠道包发布到各大应用市场的 CLI 工具。
|
|
17
|
+
|
|
18
|
+
- 渠道凭据(client_id/secret、access_key 等)全部来自**易发版后台已配置的参数**,CLI 登录后自动拉取,本地不落盘密钥。
|
|
19
|
+
- 发布时文件在**本机直接上传到各市场官方接口**(华为/荣耀/小米/OPPO/vivo/腾讯应用宝),不经任何中转服务器。
|
|
20
|
+
- 发布器代码复用自 [apk-publisher-python](https://github.com/shootdev/apk-publisher-python)(`app/publishers/`)。
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd apk-publisher-cli
|
|
26
|
+
python3 -m venv .venv
|
|
27
|
+
source .venv/bin/activate
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 快速开始
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 1. 登录易发版(默认 Q助理扫码登录:自动打开默认浏览器显示授权扫码页,用 Q助理 App 扫码即登录)
|
|
35
|
+
yifaban login
|
|
36
|
+
# 或指定服务器:
|
|
37
|
+
yifaban login --server https://yifaban.com
|
|
38
|
+
# 非交互/脚本场景可走账号密码(需同时提供 -u/-p):
|
|
39
|
+
yifaban login -u your_account -p your_password --server https://yifaban.com
|
|
40
|
+
|
|
41
|
+
# 2. 查看账号下的应用
|
|
42
|
+
yifaban apps
|
|
43
|
+
# 输出:ID 包名 应用名称 付费
|
|
44
|
+
|
|
45
|
+
# 3. 查看某应用已配置的渠道(凭据脱敏显示)
|
|
46
|
+
yifaban channels <应用ID或包名>
|
|
47
|
+
|
|
48
|
+
# 4. 验证渠道凭据(本地直连各市场查询应用信息)
|
|
49
|
+
yifaban test-config <应用ID或包名>
|
|
50
|
+
|
|
51
|
+
# 5. 发布
|
|
52
|
+
# 单个 APK 发多个渠道:
|
|
53
|
+
yifaban publish ./app-20300.apk --app <应用ID或包名> \
|
|
54
|
+
--version-code 20300 --version-name 2.3.0 \
|
|
55
|
+
--channels xiaomi,oppo,vivo --update-desc "更新说明"
|
|
56
|
+
# 本地渠道包一对一直传(每个渠道用各自的文件):
|
|
57
|
+
yifaban publish --app <应用ID或包名> --version-code 20300 \
|
|
58
|
+
--channel-file huawei=./app-huawei.apk \
|
|
59
|
+
--channel-file xiaomi=./app-xiaomi.apk \
|
|
60
|
+
--channel-file oppo=./app-oppo.apk \
|
|
61
|
+
--update-desc "更新说明"
|
|
62
|
+
|
|
63
|
+
# 6. 查询各渠道审核状态
|
|
64
|
+
yifaban status <应用ID或包名>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 命令一览
|
|
68
|
+
|
|
69
|
+
| 命令 | 作用 |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `login` / `logout` / `whoami` | 登录(默认 Q助理扫码自动打开浏览器,也可 `-u/-p` 密码)/ 退出 / 查看会话 |
|
|
72
|
+
| `apps` | 列出账号下的应用 |
|
|
73
|
+
| `channels <应用>` | 查看某应用已配置的渠道(脱敏) |
|
|
74
|
+
| `test-config <应用>` | 验证各渠道凭据(本地直连市场查询) |
|
|
75
|
+
| `publish` | 发布(见上方示例) |
|
|
76
|
+
| `submit-review <应用> <渠道>` | 华为补交审核(`--no-review` 后使用) |
|
|
77
|
+
| `status <应用>` | 查询各渠道审核状态 |
|
|
78
|
+
|
|
79
|
+
## 细节
|
|
80
|
+
|
|
81
|
+
- **登录方式**:默认 Q助理扫码登录(自动打开默认浏览器显示授权扫码页,用 Q助理 App 扫码授权后自动登录;
|
|
82
|
+
若 Q助理 账号未绑定易发版账号,会提示输入易发版账号密码完成一次性绑定)。
|
|
83
|
+
同时提供 `--username/--password` 账号密码登录(非交互/脚本场景),两者互斥。
|
|
84
|
+
- **服务器地址**:默认 `https://yifaban.com`,可用 `--server`、环境变量 `APKPUB_SERVER` 覆盖。
|
|
85
|
+
- **凭据来源**:发布时实时从易发版 `/api/config/get_app_channel_configs` 拉取;
|
|
86
|
+
单字段可用环境变量覆盖(调试用):`APKPUB_XIAOMI_PASSWORD=xxx`。
|
|
87
|
+
- **签名协议**:与后端一致(参数排序 → 中文 URL 编码 → 拼接 `&token={token}123D77aHE@m` → MD5)。
|
|
88
|
+
- **`--version-code` 必须高于各市场当前线上版本**。
|
|
89
|
+
- **华为**:上传成功后需等待 2 分钟才能提交审核;默认自动等待并提交,
|
|
90
|
+
可用 `--no-review` 跳过,之后用 `yifaban submit-review <应用> huawei` 补交。
|
|
91
|
+
- **荣耀**:审核状态查询需要市场发布ID(发布成功时输出),查询时加 `--release-id`。
|
|
92
|
+
- **腾讯**:接口未使用 update_desc(仅更新包文件)。
|
|
93
|
+
|
|
94
|
+
## 安全说明
|
|
95
|
+
|
|
96
|
+
- 登录 token 存于 `~/.yifaban/auth.json`(权限 600),渠道密钥不落盘。
|
|
97
|
+
- Q助理扫码登录的授权、票据校验全部在易发版服务端完成,CLI 不持有 Q助理 应用密钥。
|
|
98
|
+
- 如果服务器返回"登录已过期",重新运行 `yifaban login`。
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# apk-publisher-cli
|
|
2
|
+
|
|
3
|
+
基于**易发版账号登录(默认 Q助理扫码)**,从本地电脑直接把渠道包发布到各大应用市场的 CLI 工具。
|
|
4
|
+
|
|
5
|
+
- 渠道凭据(client_id/secret、access_key 等)全部来自**易发版后台已配置的参数**,CLI 登录后自动拉取,本地不落盘密钥。
|
|
6
|
+
- 发布时文件在**本机直接上传到各市场官方接口**(华为/荣耀/小米/OPPO/vivo/腾讯应用宝),不经任何中转服务器。
|
|
7
|
+
- 发布器代码复用自 [apk-publisher-python](https://github.com/shootdev/apk-publisher-python)(`app/publishers/`)。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd apk-publisher-cli
|
|
13
|
+
python3 -m venv .venv
|
|
14
|
+
source .venv/bin/activate
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 快速开始
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. 登录易发版(默认 Q助理扫码登录:自动打开默认浏览器显示授权扫码页,用 Q助理 App 扫码即登录)
|
|
22
|
+
yifaban login
|
|
23
|
+
# 或指定服务器:
|
|
24
|
+
yifaban login --server https://yifaban.com
|
|
25
|
+
# 非交互/脚本场景可走账号密码(需同时提供 -u/-p):
|
|
26
|
+
yifaban login -u your_account -p your_password --server https://yifaban.com
|
|
27
|
+
|
|
28
|
+
# 2. 查看账号下的应用
|
|
29
|
+
yifaban apps
|
|
30
|
+
# 输出:ID 包名 应用名称 付费
|
|
31
|
+
|
|
32
|
+
# 3. 查看某应用已配置的渠道(凭据脱敏显示)
|
|
33
|
+
yifaban channels <应用ID或包名>
|
|
34
|
+
|
|
35
|
+
# 4. 验证渠道凭据(本地直连各市场查询应用信息)
|
|
36
|
+
yifaban test-config <应用ID或包名>
|
|
37
|
+
|
|
38
|
+
# 5. 发布
|
|
39
|
+
# 单个 APK 发多个渠道:
|
|
40
|
+
yifaban publish ./app-20300.apk --app <应用ID或包名> \
|
|
41
|
+
--version-code 20300 --version-name 2.3.0 \
|
|
42
|
+
--channels xiaomi,oppo,vivo --update-desc "更新说明"
|
|
43
|
+
# 本地渠道包一对一直传(每个渠道用各自的文件):
|
|
44
|
+
yifaban publish --app <应用ID或包名> --version-code 20300 \
|
|
45
|
+
--channel-file huawei=./app-huawei.apk \
|
|
46
|
+
--channel-file xiaomi=./app-xiaomi.apk \
|
|
47
|
+
--channel-file oppo=./app-oppo.apk \
|
|
48
|
+
--update-desc "更新说明"
|
|
49
|
+
|
|
50
|
+
# 6. 查询各渠道审核状态
|
|
51
|
+
yifaban status <应用ID或包名>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 命令一览
|
|
55
|
+
|
|
56
|
+
| 命令 | 作用 |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `login` / `logout` / `whoami` | 登录(默认 Q助理扫码自动打开浏览器,也可 `-u/-p` 密码)/ 退出 / 查看会话 |
|
|
59
|
+
| `apps` | 列出账号下的应用 |
|
|
60
|
+
| `channels <应用>` | 查看某应用已配置的渠道(脱敏) |
|
|
61
|
+
| `test-config <应用>` | 验证各渠道凭据(本地直连市场查询) |
|
|
62
|
+
| `publish` | 发布(见上方示例) |
|
|
63
|
+
| `submit-review <应用> <渠道>` | 华为补交审核(`--no-review` 后使用) |
|
|
64
|
+
| `status <应用>` | 查询各渠道审核状态 |
|
|
65
|
+
|
|
66
|
+
## 细节
|
|
67
|
+
|
|
68
|
+
- **登录方式**:默认 Q助理扫码登录(自动打开默认浏览器显示授权扫码页,用 Q助理 App 扫码授权后自动登录;
|
|
69
|
+
若 Q助理 账号未绑定易发版账号,会提示输入易发版账号密码完成一次性绑定)。
|
|
70
|
+
同时提供 `--username/--password` 账号密码登录(非交互/脚本场景),两者互斥。
|
|
71
|
+
- **服务器地址**:默认 `https://yifaban.com`,可用 `--server`、环境变量 `APKPUB_SERVER` 覆盖。
|
|
72
|
+
- **凭据来源**:发布时实时从易发版 `/api/config/get_app_channel_configs` 拉取;
|
|
73
|
+
单字段可用环境变量覆盖(调试用):`APKPUB_XIAOMI_PASSWORD=xxx`。
|
|
74
|
+
- **签名协议**:与后端一致(参数排序 → 中文 URL 编码 → 拼接 `&token={token}123D77aHE@m` → MD5)。
|
|
75
|
+
- **`--version-code` 必须高于各市场当前线上版本**。
|
|
76
|
+
- **华为**:上传成功后需等待 2 分钟才能提交审核;默认自动等待并提交,
|
|
77
|
+
可用 `--no-review` 跳过,之后用 `yifaban submit-review <应用> huawei` 补交。
|
|
78
|
+
- **荣耀**:审核状态查询需要市场发布ID(发布成功时输出),查询时加 `--release-id`。
|
|
79
|
+
- **腾讯**:接口未使用 update_desc(仅更新包文件)。
|
|
80
|
+
|
|
81
|
+
## 安全说明
|
|
82
|
+
|
|
83
|
+
- 登录 token 存于 `~/.yifaban/auth.json`(权限 600),渠道密钥不落盘。
|
|
84
|
+
- Q助理扫码登录的授权、票据校验全部在易发版服务端完成,CLI 不持有 Q助理 应用密钥。
|
|
85
|
+
- 如果服务器返回"登录已过期",重新运行 `yifaban login`。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yifaban-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "基于易发版账号登录,从本地电脑直接把渠道包发布到各大应用市场"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"requests>=2.25.1",
|
|
13
|
+
"urllib3==1.26.15",
|
|
14
|
+
"cryptography>=3.4.8",
|
|
15
|
+
"pycryptodome>=3.15.0",
|
|
16
|
+
"requests-toolbelt==1.0.0",
|
|
17
|
+
"typer>=0.12.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
yifaban = "yifaban.cli:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
include = ["yifaban*"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# -*- coding=utf8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
yifaban - 本地直连应用市场发布 CLI
|
|
5
|
+
|
|
6
|
+
复用 apk-publisher-python 仓库中的渠道发布器,直接从本地电脑
|
|
7
|
+
把 APK(渠道包)上传到华为/荣耀/小米/OPPO/vivo/腾讯应用宝。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .logger import log_t, get_g_log
|
|
11
|
+
from .constants import (
|
|
12
|
+
HUAWEI_CHANNEL_NAME,
|
|
13
|
+
XIAOMI_CHANNEL_NAME,
|
|
14
|
+
OPPO_CHANNEL_NAME,
|
|
15
|
+
TENCENT_CHANNEL_NAME,
|
|
16
|
+
VIVO_CHANNEL_NAME,
|
|
17
|
+
HONOR_CHANNEL_NAME,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"log_t",
|
|
22
|
+
"get_g_log",
|
|
23
|
+
"HUAWEI_CHANNEL_NAME",
|
|
24
|
+
"XIAOMI_CHANNEL_NAME",
|
|
25
|
+
"OPPO_CHANNEL_NAME",
|
|
26
|
+
"TENCENT_CHANNEL_NAME",
|
|
27
|
+
"VIVO_CHANNEL_NAME",
|
|
28
|
+
"HONOR_CHANNEL_NAME",
|
|
29
|
+
]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# -*- coding=utf8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
易发版后端 API 客户端
|
|
5
|
+
|
|
6
|
+
签名协议与 apk-publisher-python 后端 core/utils.py 完全一致:
|
|
7
|
+
1. 参数按 key 排序(排除 key 字段),过滤 None / 空字符串 / 字典值
|
|
8
|
+
2. 值含中文([\u4e00-\u9fff])时做 URL 编码(safe="!~*'()",先归一化换行)
|
|
9
|
+
3. 拼接 key=value&... 后追加 &token={token}123D77aHE@m
|
|
10
|
+
4. 取 MD5 作为 key 参数
|
|
11
|
+
|
|
12
|
+
登录使用空 token 签名;业务请求 header 携带 Authorization/uid,body 携带 key。
|
|
13
|
+
响应格式统一为 {code, message, data},data 键为驼峰命名。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import re
|
|
18
|
+
import time
|
|
19
|
+
import urllib.parse
|
|
20
|
+
|
|
21
|
+
import requests
|
|
22
|
+
|
|
23
|
+
from yifaban.config import apply_env_overrides
|
|
24
|
+
|
|
25
|
+
SIGN_SALT = "123D77aHE@m"
|
|
26
|
+
_CHINESE_RE = re.compile(r"[\u4e00-\u9fff]+")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApiError(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def calculate_api_key(data: dict, token: str) -> str:
|
|
34
|
+
"""计算 API 签名(与后端 core/utils.py 的 calculate_api_key 一致)"""
|
|
35
|
+
valid_items = []
|
|
36
|
+
for key in sorted(data.keys()):
|
|
37
|
+
if key == "key":
|
|
38
|
+
continue
|
|
39
|
+
value = data[key]
|
|
40
|
+
if value is None or isinstance(value, dict) or len(str(value)) == 0:
|
|
41
|
+
continue
|
|
42
|
+
value_str = str(value)
|
|
43
|
+
if _CHINESE_RE.search(value_str):
|
|
44
|
+
value_str = urllib.parse.quote(
|
|
45
|
+
value_str.replace("\r\n", "\n").replace("\r", "\n"),
|
|
46
|
+
safe="!~*'()",
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
)
|
|
49
|
+
valid_items.append(f"{key}={value_str}")
|
|
50
|
+
s = "&".join(valid_items)
|
|
51
|
+
return hashlib.md5(f"{s}&token={token}{SIGN_SALT}".encode("utf-8")).hexdigest()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _post(server, path, data, token="", uid="", timeout=30):
|
|
55
|
+
payload = dict(data or {})
|
|
56
|
+
payload["key"] = calculate_api_key(payload, token)
|
|
57
|
+
headers = {"Content-Type": "application/json"}
|
|
58
|
+
if token:
|
|
59
|
+
headers["Authorization"] = token
|
|
60
|
+
headers["uid"] = uid
|
|
61
|
+
try:
|
|
62
|
+
resp = requests.post(f"{server}{path}", json=payload, headers=headers, timeout=timeout)
|
|
63
|
+
except requests.RequestException as e:
|
|
64
|
+
raise ApiError(f"网络请求失败: {e}") from e
|
|
65
|
+
try:
|
|
66
|
+
body = resp.json()
|
|
67
|
+
except ValueError:
|
|
68
|
+
raise ApiError(f"服务器返回非 JSON(HTTP {resp.status_code}): {resp.text[:200]}")
|
|
69
|
+
if resp.status_code == 401 and body.get("code") == 4001:
|
|
70
|
+
raise ApiError("登录已过期,请重新运行 yifaban login")
|
|
71
|
+
if body.get("code") != 200:
|
|
72
|
+
raise ApiError(f"{body.get('message') or '请求失败'}(code={body.get('code')})")
|
|
73
|
+
return body.get("data", {})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _post_public(server, path, data, timeout=30):
|
|
77
|
+
"""无 token 签名 POST(登录、Q助理 exchange/bind/cli poll 等公开接口)"""
|
|
78
|
+
payload = dict(data or {})
|
|
79
|
+
payload["key"] = calculate_api_key(payload, "")
|
|
80
|
+
try:
|
|
81
|
+
resp = requests.post(f"{server}{path}", json=payload, timeout=timeout)
|
|
82
|
+
except requests.RequestException as e:
|
|
83
|
+
raise ApiError(f"网络请求失败: {e}") from e
|
|
84
|
+
try:
|
|
85
|
+
body = resp.json()
|
|
86
|
+
except ValueError:
|
|
87
|
+
raise ApiError(f"服务器返回非 JSON(HTTP {resp.status_code}): {resp.text[:200]}")
|
|
88
|
+
if body.get("code") != 200:
|
|
89
|
+
raise ApiError(body.get("message") or "请求失败")
|
|
90
|
+
return body.get("data", {})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def save_login_session(server: str, result: dict, fallback_username: str = ""):
|
|
94
|
+
"""保存登录会话到 ~/.yifaban/auth.json(登录/扫码兑换/绑定登录共用)"""
|
|
95
|
+
from yifaban.config import save_auth
|
|
96
|
+
|
|
97
|
+
save_auth({
|
|
98
|
+
"server": server,
|
|
99
|
+
"token": result.get("token", ""),
|
|
100
|
+
"uid": result.get("uid", ""),
|
|
101
|
+
"username": result.get("username", fallback_username),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def login(username: str, password: str, server: str):
|
|
106
|
+
"""账号密码登录易发版,保存会话到 ~/.yifaban/auth.json"""
|
|
107
|
+
data = {
|
|
108
|
+
"username": username,
|
|
109
|
+
"password": password,
|
|
110
|
+
"t": str(int(time.time())),
|
|
111
|
+
}
|
|
112
|
+
result = _post_public(server, "/api/auth/login", data)
|
|
113
|
+
save_login_session(server, result, username)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def qzhuli_auth_url(server: str):
|
|
118
|
+
"""获取 Q助理扫码授权地址与轮询会话 ID(响应 data 为驼峰键)"""
|
|
119
|
+
try:
|
|
120
|
+
resp = requests.get(f"{server}/api/qzhuli/cli/auth-url", timeout=30)
|
|
121
|
+
except requests.RequestException as e:
|
|
122
|
+
raise ApiError(f"网络请求失败: {e}") from e
|
|
123
|
+
try:
|
|
124
|
+
body = resp.json()
|
|
125
|
+
except ValueError:
|
|
126
|
+
raise ApiError(f"服务器返回非 JSON(HTTP {resp.status_code}): {resp.text[:200]}")
|
|
127
|
+
if body.get("code") != 200:
|
|
128
|
+
raise ApiError(body.get("message") or "获取授权地址失败")
|
|
129
|
+
data = body.get("data", {})
|
|
130
|
+
url, request_id = data.get("url", ""), data.get("requestId", "")
|
|
131
|
+
if not url or not request_id:
|
|
132
|
+
raise ApiError("服务器未返回授权地址")
|
|
133
|
+
return {"url": url, "request_id": request_id}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def qzhuli_poll(server: str, request_id: str):
|
|
137
|
+
"""轮询 Q助理扫码结果:{status: pending|ok|expired, ticket?}"""
|
|
138
|
+
return _post_public(server, "/api/qzhuli/cli/poll", {"request_id": request_id})
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def qzhuli_exchange(server: str, ticket: str):
|
|
142
|
+
"""
|
|
143
|
+
用 ticket 兑换登录(响应 data 为驼峰键):
|
|
144
|
+
已绑定 -> {token, uid, username, ...}
|
|
145
|
+
未绑定 -> {needBind: true, bindToken, nickname, qUid}
|
|
146
|
+
"""
|
|
147
|
+
return _post_public(server, "/api/qzhuli/exchange", {"ticket": ticket})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def qzhuli_bind(server: str, bind_token: str, username: str, password: str):
|
|
151
|
+
"""未绑定 Q助理:用易发版账号密码完成绑定并登录"""
|
|
152
|
+
return _post_public(server, "/api/qzhuli/bind", {
|
|
153
|
+
"bind_token": bind_token,
|
|
154
|
+
"username": username,
|
|
155
|
+
"password": password,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def require_auth():
|
|
160
|
+
from yifaban.config import load_auth
|
|
161
|
+
|
|
162
|
+
auth = load_auth()
|
|
163
|
+
if not auth.get("token") or not auth.get("uid"):
|
|
164
|
+
raise ApiError("未登录,请先运行 yifaban login")
|
|
165
|
+
return auth
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_apps():
|
|
169
|
+
"""获取账号下的应用列表"""
|
|
170
|
+
auth = require_auth()
|
|
171
|
+
data = _post(
|
|
172
|
+
auth["server"],
|
|
173
|
+
"/api/app/get_apps_data",
|
|
174
|
+
{"uid": auth["uid"], "with_channel_config": 0, "with_latest_task": 0},
|
|
175
|
+
auth["token"],
|
|
176
|
+
auth["uid"],
|
|
177
|
+
)
|
|
178
|
+
return data.get("apps", [])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_channel_configs(app_id: str):
|
|
182
|
+
"""获取某应用在所有市场的渠道配置"""
|
|
183
|
+
auth = require_auth()
|
|
184
|
+
return _post(
|
|
185
|
+
auth["server"],
|
|
186
|
+
"/api/config/get_app_channel_configs",
|
|
187
|
+
{"uid": auth["uid"], "app_id": app_id},
|
|
188
|
+
auth["token"],
|
|
189
|
+
auth["uid"],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
_CAMEL_TO_SNAKE = {
|
|
194
|
+
"clientId": "client_id",
|
|
195
|
+
"clientSecret": "client_secret",
|
|
196
|
+
"appId": "app_id",
|
|
197
|
+
"accessKey": "access_key",
|
|
198
|
+
"accessSecret": "access_secret",
|
|
199
|
+
"publicKey": "public_key",
|
|
200
|
+
"userId": "user_id",
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def channel_publisher_config(server_cfg: dict, channel: str) -> dict:
|
|
205
|
+
"""
|
|
206
|
+
把服务器返回的渠道配置(驼峰键)转换为发布器需要的配置(下划线键)。
|
|
207
|
+
小米 publicKey 服务器侧已做 base64 解码,这里直接透传。
|
|
208
|
+
"""
|
|
209
|
+
config_data = (server_cfg or {}).get("configData") or {}
|
|
210
|
+
result = {}
|
|
211
|
+
for k, v in config_data.items():
|
|
212
|
+
if v is None:
|
|
213
|
+
continue
|
|
214
|
+
snake = _CAMEL_TO_SNAKE.get(k, k)
|
|
215
|
+
result[snake] = v
|
|
216
|
+
# 渠道行自身的 app_id 兜底(华为/荣耀/腾讯发布器需要)
|
|
217
|
+
if "app_id" not in result and server_cfg.get("appId"):
|
|
218
|
+
result["app_id"] = server_cfg.get("appId")
|
|
219
|
+
return apply_env_overrides(channel, result)
|