zt-fop-cli 0.0.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.
- zt_fop_cli-0.0.1/PKG-INFO +102 -0
- zt_fop_cli-0.0.1/README.md +91 -0
- zt_fop_cli-0.0.1/pyproject.toml +25 -0
- zt_fop_cli-0.0.1/setup.cfg +4 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli/__init__.py +0 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli/api.py +93 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli/auth.py +100 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli/cli.py +179 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli/config.py +33 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli.egg-info/PKG-INFO +102 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli.egg-info/SOURCES.txt +13 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli.egg-info/dependency_links.txt +1 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli.egg-info/entry_points.txt +2 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli.egg-info/requires.txt +4 -0
- zt_fop_cli-0.0.1/src/zt_fop_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zt-fop-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: ZT 接口管理平台 CLI
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: playwright>=1.40.0
|
|
8
|
+
Requires-Dist: requests>=2.31.0
|
|
9
|
+
Requires-Dist: click>=8.1.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
|
|
12
|
+
# zt-fop-cli
|
|
13
|
+
|
|
14
|
+
FOP/ARK 接口查询命令行工具,用于登录并搜索接口、查看接口详情与响应说明。
|
|
15
|
+
|
|
16
|
+
## 环境要求
|
|
17
|
+
|
|
18
|
+
- Python `>=3.10`
|
|
19
|
+
- 可用浏览器(首次登录会自动打开)
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
### 开发安装(源码)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 打包发布
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m pip install --upgrade build twine
|
|
33
|
+
python -m build
|
|
34
|
+
twine upload dist/*
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 快速开始
|
|
38
|
+
|
|
39
|
+
### 1) 登录并缓存 Token
|
|
40
|
+
|
|
41
|
+
首次运行会打开浏览器,请完成登录。成功后会从 `localStorage` 读取 `micro-gosAdmin-Token` 并缓存到本地。
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
zt-fop-cli login
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2) 搜索接口
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
zt-fop-cli interface search -k 库存
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3) 查看接口详情(含响应说明)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
zt-fop-cli interface detail 123456
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 全局参数
|
|
60
|
+
- `-o, --output`:输出格式,支持:
|
|
61
|
+
- `json`(默认)
|
|
62
|
+
- `human`(表格)
|
|
63
|
+
|
|
64
|
+
示例:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
zt-fop-cli -o human interface search -k 订单
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> 说明:`interface detail` 输出为一个合并对象,包含:
|
|
71
|
+
> - `interface_detail`:接口详情
|
|
72
|
+
> - `interface_resp`:接口响应说明
|
|
73
|
+
|
|
74
|
+
## 配置说明
|
|
75
|
+
|
|
76
|
+
默认配置目录:`~/.zt-fop-cli`
|
|
77
|
+
- Token 缓存:`~/.zt-fop-cli/fop_bearer_token.txt`
|
|
78
|
+
- 配置文件:`~/.zt-fop-cli/config.yaml`
|
|
79
|
+
|
|
80
|
+
可选配置项示例:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
browser_channel: chromium
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 环境变量(可选)
|
|
87
|
+
|
|
88
|
+
支持通过环境变量自动填充登录页账号密码(如 SSO 页面字段可用):
|
|
89
|
+
|
|
90
|
+
- `ZT_SSO_USERNAME`
|
|
91
|
+
- `ZT_SSO_PASSWORD`
|
|
92
|
+
|
|
93
|
+
未设置时可在浏览器中手动完成登录。
|
|
94
|
+
|
|
95
|
+
## 命令一览
|
|
96
|
+
|
|
97
|
+
- `zt-fop-cli login`
|
|
98
|
+
- 浏览器登录并缓存 Token
|
|
99
|
+
- `zt-fop-cli interface search -k <keyword>`
|
|
100
|
+
- 模糊搜索接口
|
|
101
|
+
- `zt-fop-cli interface detail <interface_id> [--ability 1] [--customer-app-id PLM]`
|
|
102
|
+
- 查询接口详情并同时返回响应说明
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# zt-fop-cli
|
|
2
|
+
|
|
3
|
+
FOP/ARK 接口查询命令行工具,用于登录并搜索接口、查看接口详情与响应说明。
|
|
4
|
+
|
|
5
|
+
## 环境要求
|
|
6
|
+
|
|
7
|
+
- Python `>=3.10`
|
|
8
|
+
- 可用浏览器(首次登录会自动打开)
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
### 开发安装(源码)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 打包发布
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
python -m pip install --upgrade build twine
|
|
22
|
+
python -m build
|
|
23
|
+
twine upload dist/*
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
### 1) 登录并缓存 Token
|
|
29
|
+
|
|
30
|
+
首次运行会打开浏览器,请完成登录。成功后会从 `localStorage` 读取 `micro-gosAdmin-Token` 并缓存到本地。
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
zt-fop-cli login
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2) 搜索接口
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
zt-fop-cli interface search -k 库存
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3) 查看接口详情(含响应说明)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
zt-fop-cli interface detail 123456
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 全局参数
|
|
49
|
+
- `-o, --output`:输出格式,支持:
|
|
50
|
+
- `json`(默认)
|
|
51
|
+
- `human`(表格)
|
|
52
|
+
|
|
53
|
+
示例:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
zt-fop-cli -o human interface search -k 订单
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> 说明:`interface detail` 输出为一个合并对象,包含:
|
|
60
|
+
> - `interface_detail`:接口详情
|
|
61
|
+
> - `interface_resp`:接口响应说明
|
|
62
|
+
|
|
63
|
+
## 配置说明
|
|
64
|
+
|
|
65
|
+
默认配置目录:`~/.zt-fop-cli`
|
|
66
|
+
- Token 缓存:`~/.zt-fop-cli/fop_bearer_token.txt`
|
|
67
|
+
- 配置文件:`~/.zt-fop-cli/config.yaml`
|
|
68
|
+
|
|
69
|
+
可选配置项示例:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
browser_channel: chromium
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 环境变量(可选)
|
|
76
|
+
|
|
77
|
+
支持通过环境变量自动填充登录页账号密码(如 SSO 页面字段可用):
|
|
78
|
+
|
|
79
|
+
- `ZT_SSO_USERNAME`
|
|
80
|
+
- `ZT_SSO_PASSWORD`
|
|
81
|
+
|
|
82
|
+
未设置时可在浏览器中手动完成登录。
|
|
83
|
+
|
|
84
|
+
## 命令一览
|
|
85
|
+
|
|
86
|
+
- `zt-fop-cli login`
|
|
87
|
+
- 浏览器登录并缓存 Token
|
|
88
|
+
- `zt-fop-cli interface search -k <keyword>`
|
|
89
|
+
- 模糊搜索接口
|
|
90
|
+
- `zt-fop-cli interface detail <interface_id> [--ability 1] [--customer-app-id PLM]`
|
|
91
|
+
- 查询接口详情并同时返回响应说明
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zt-fop-cli"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "ZT 接口管理平台 CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"playwright>=1.40.0",
|
|
13
|
+
"requests>=2.31.0",
|
|
14
|
+
"click>=8.1.0",
|
|
15
|
+
"pyyaml>=6.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
zt-fop-cli = "zt_fop_cli.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""API 请求封装"""
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from .auth import load_fop_bearer_token
|
|
7
|
+
|
|
8
|
+
# ARK Gateway(FOP 接口文档/搜索),与 docs/curl.md 一致
|
|
9
|
+
ARK_GATEWAY_BASE_URL = "https://ark-gateway.goodcang.com"
|
|
10
|
+
ARK_GOS_API_PREFIX = "/gos/api/v1"
|
|
11
|
+
|
|
12
|
+
class FopArkAPI:
|
|
13
|
+
"""ARK Gateway GOS 接口(Bearer Token),对应 curl.md 中的 FOP 接口搜索/详情/响应"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
base_url: Optional[str] = None,
|
|
18
|
+
):
|
|
19
|
+
self.base_url = (base_url or os.environ.get("ZT_FOP_ARK_BASE_URL") or ARK_GATEWAY_BASE_URL).rstrip("/")
|
|
20
|
+
self.bearer_token = load_fop_bearer_token()
|
|
21
|
+
|
|
22
|
+
def _headers(self, *, json_body: bool = False) -> dict[str, str]:
|
|
23
|
+
h: dict[str, str] = {
|
|
24
|
+
"Accept": "application/json, text/plain, */*",
|
|
25
|
+
"Accept-Language": "zh-CN",
|
|
26
|
+
"Origin": "https://fop.goodcang.com",
|
|
27
|
+
"Referer": "https://fop.goodcang.com/",
|
|
28
|
+
"User-Agent": (
|
|
29
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
30
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
if self.bearer_token:
|
|
34
|
+
h["Authorization"] = f"Bearer {self.bearer_token}"
|
|
35
|
+
if json_body:
|
|
36
|
+
h["Content-Type"] = "application/json"
|
|
37
|
+
return h
|
|
38
|
+
|
|
39
|
+
def _request(
|
|
40
|
+
self,
|
|
41
|
+
method: str,
|
|
42
|
+
path: str,
|
|
43
|
+
*,
|
|
44
|
+
params: Optional[dict[str, Any]] = None,
|
|
45
|
+
json: Any = None,
|
|
46
|
+
) -> dict:
|
|
47
|
+
if not self.bearer_token:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"缺少 ARK Bearer Token:请先执行 zt-fop-cli login,或设置 ZT_FOP_BEARER_TOKEN / 使用 --token"
|
|
50
|
+
)
|
|
51
|
+
url = f"{self.base_url}{path}" if path.startswith("/") else f"{self.base_url}/{path}"
|
|
52
|
+
use_json = json is not None
|
|
53
|
+
response = requests.request(
|
|
54
|
+
method=method,
|
|
55
|
+
url=url,
|
|
56
|
+
headers=self._headers(json_body=use_json),
|
|
57
|
+
params=params,
|
|
58
|
+
json=json,
|
|
59
|
+
timeout=60,
|
|
60
|
+
)
|
|
61
|
+
if response.status_code >= 400:
|
|
62
|
+
raise Exception(f"API 错误: {response.status_code} {response.text}")
|
|
63
|
+
return response.json()
|
|
64
|
+
|
|
65
|
+
def search_interface(self, vague_key: str) -> dict:
|
|
66
|
+
"""POST /gos/api/v1/interface_base/search,body: {\"vague_key\": ...}"""
|
|
67
|
+
return self._request(
|
|
68
|
+
"POST",
|
|
69
|
+
f"{ARK_GOS_API_PREFIX}/interface_base/search",
|
|
70
|
+
json={"vague_key": vague_key},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def get_interface_detail(
|
|
74
|
+
self,
|
|
75
|
+
interface_id: str,
|
|
76
|
+
*,
|
|
77
|
+
ability: int = 1,
|
|
78
|
+
customer_app_id: str = "PLM",
|
|
79
|
+
) -> dict:
|
|
80
|
+
"""GET .../interface_base/get_interface_detail/{id}?ability=&customer_app_id="""
|
|
81
|
+
return self._request(
|
|
82
|
+
"GET",
|
|
83
|
+
f"{ARK_GOS_API_PREFIX}/interface_base/get_interface_detail/{interface_id}",
|
|
84
|
+
params={"ability": ability, "customer_app_id": customer_app_id},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def get_detail_resp(self, interface_id: str, *, ability: int = 1) -> dict:
|
|
88
|
+
"""GET .../interface_reqresp/get_detail_resp/{id}?ability="""
|
|
89
|
+
return self._request(
|
|
90
|
+
"GET",
|
|
91
|
+
f"{ARK_GOS_API_PREFIX}/interface_reqresp/get_detail_resp/{interface_id}",
|
|
92
|
+
params={"ability": ability},
|
|
93
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""认证模块 - 通过 Playwright 登录 FOP 并从 localStorage 读取 GOS Admin Token"""
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from playwright.sync_api import sync_playwright
|
|
6
|
+
|
|
7
|
+
from .config import config
|
|
8
|
+
|
|
9
|
+
# FOP SSO(登录成功后页面会写入 localStorage)
|
|
10
|
+
LOGIN_URL = "https://sso.ztn.cn/cas/login?service=https://fop.goodcang.com&renew=true&locale=zh_CN"
|
|
11
|
+
|
|
12
|
+
# 与前端一致:ARK 请求使用的 JWT 存在该 key
|
|
13
|
+
FOP_LOCALSTORAGE_TOKEN_KEY = "micro-gosAdmin-Token"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_fop_bearer_token() -> str:
|
|
17
|
+
"""从本地缓存文件读取 Bearer(不含 Bearer 前缀)"""
|
|
18
|
+
if not config.FOP_BEARER_FILE.exists():
|
|
19
|
+
return ""
|
|
20
|
+
return config.FOP_BEARER_FILE.read_text(encoding="utf-8").strip()
|
|
21
|
+
|
|
22
|
+
def save_fop_bearer_token(token: str) -> None:
|
|
23
|
+
config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
raw = token.strip()
|
|
25
|
+
if raw.lower().startswith("bearer "):
|
|
26
|
+
raw = raw[7:].strip()
|
|
27
|
+
config.FOP_BEARER_FILE.write_text(raw, encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
def open_login_page(page) -> bool:
|
|
30
|
+
"""自动登录 - 尝试读取环境变量自动填写账号密码"""
|
|
31
|
+
username = os.environ.get("ZT_SSO_USERNAME")
|
|
32
|
+
password = os.environ.get("ZT_SSO_PASSWORD")
|
|
33
|
+
|
|
34
|
+
if not username or not password:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
print("检测到环境变量,自动登录中...")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
page.wait_for_selector("#username", timeout=60_000)
|
|
41
|
+
except Exception:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
page.fill("#username", username)
|
|
46
|
+
print("已填写用户名")
|
|
47
|
+
page.fill("#password2", password)
|
|
48
|
+
print("已填写密码")
|
|
49
|
+
page.click(".login_in")
|
|
50
|
+
print("已点击登录按钮")
|
|
51
|
+
return True
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(f"自动登录失败: {e}")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def get_fop_token_from_browser() -> str:
|
|
57
|
+
"""打开浏览器完成 FOP 登录,等待 localStorage 中的 micro-gosAdmin-Token 出现并读取"""
|
|
58
|
+
print("正在打开浏览器,请登录 FOP...")
|
|
59
|
+
|
|
60
|
+
with sync_playwright() as p:
|
|
61
|
+
browser = p.chromium.launch(headless=False)
|
|
62
|
+
context = browser.new_context()
|
|
63
|
+
page = context.new_page()
|
|
64
|
+
page.goto(LOGIN_URL)
|
|
65
|
+
|
|
66
|
+
if not open_login_page(page):
|
|
67
|
+
print("未配置自动登录时,请在浏览器中手动完成登录...")
|
|
68
|
+
|
|
69
|
+
page.wait_for_function(
|
|
70
|
+
f"""() => {{
|
|
71
|
+
const t = localStorage.getItem('{FOP_LOCALSTORAGE_TOKEN_KEY}');
|
|
72
|
+
return t != null && String(t).trim().length > 0;
|
|
73
|
+
}}""",
|
|
74
|
+
timeout=300_000,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
token = page.evaluate(
|
|
78
|
+
f"""() => localStorage.getItem('{FOP_LOCALSTORAGE_TOKEN_KEY}') || ''"""
|
|
79
|
+
)
|
|
80
|
+
browser.close()
|
|
81
|
+
|
|
82
|
+
token = (token or "").strip()
|
|
83
|
+
if token.lower().startswith("bearer "):
|
|
84
|
+
token = token[7:].strip()
|
|
85
|
+
if not token:
|
|
86
|
+
raise RuntimeError(f"已超时或未能读取 localStorage['{FOP_LOCALSTORAGE_TOKEN_KEY}']")
|
|
87
|
+
|
|
88
|
+
print("已检测到 GOS Admin Token")
|
|
89
|
+
return token
|
|
90
|
+
|
|
91
|
+
def login() -> str:
|
|
92
|
+
"""打开浏览器登录 FOP,将 Token 写入缓存文件"""
|
|
93
|
+
token = get_fop_token_from_browser()
|
|
94
|
+
save_fop_bearer_token(token)
|
|
95
|
+
print(f"登录成功,Token 已保存: {config.FOP_BEARER_FILE}")
|
|
96
|
+
return token
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
print(login())
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""CLI 入口"""
|
|
2
|
+
import click
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from .api import FopArkAPI
|
|
6
|
+
from .auth import login
|
|
7
|
+
from .config import config
|
|
8
|
+
|
|
9
|
+
def _normalize_cell(value):
|
|
10
|
+
if value is None:
|
|
11
|
+
return "-"
|
|
12
|
+
return str(value)
|
|
13
|
+
|
|
14
|
+
def _truncate_text(text, width):
|
|
15
|
+
if width <= 0:
|
|
16
|
+
return ""
|
|
17
|
+
if len(text) <= width:
|
|
18
|
+
return text
|
|
19
|
+
if width <= 1:
|
|
20
|
+
return text[:width]
|
|
21
|
+
return text[: width - 1] + "…"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def render_table(headers, rows, max_width=120, no_truncate_headers=None):
|
|
25
|
+
if not rows:
|
|
26
|
+
click.echo("暂无数据")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
terminal_width = shutil.get_terminal_size((max_width, 20)).columns
|
|
30
|
+
table_max_width = min(max_width, terminal_width)
|
|
31
|
+
|
|
32
|
+
no_truncate_headers = set(no_truncate_headers or [])
|
|
33
|
+
|
|
34
|
+
normalized_rows = []
|
|
35
|
+
for row in rows:
|
|
36
|
+
normalized = [_normalize_cell(cell) for cell in row[: len(headers)]]
|
|
37
|
+
if len(normalized) < len(headers):
|
|
38
|
+
normalized.extend(["-"] * (len(headers) - len(normalized)))
|
|
39
|
+
normalized_rows.append(normalized)
|
|
40
|
+
|
|
41
|
+
widths = [len(h) for h in headers]
|
|
42
|
+
for row in normalized_rows:
|
|
43
|
+
for idx, cell in enumerate(row):
|
|
44
|
+
widths[idx] = max(widths[idx], len(cell))
|
|
45
|
+
|
|
46
|
+
# 边框和分隔占用: 左右边框 + 每列两侧空格 + 列分隔符
|
|
47
|
+
overhead = 3 * len(headers) + 1
|
|
48
|
+
available = max(20, table_max_width - overhead)
|
|
49
|
+
min_col_width = 10
|
|
50
|
+
widths = [max(min_col_width, w) for w in widths]
|
|
51
|
+
|
|
52
|
+
total = sum(widths)
|
|
53
|
+
if total > available:
|
|
54
|
+
fixed_idx = {i for i, h in enumerate(headers) if h in no_truncate_headers}
|
|
55
|
+
shrinkable_idx = [i for i in range(len(headers)) if i not in fixed_idx]
|
|
56
|
+
|
|
57
|
+
if shrinkable_idx:
|
|
58
|
+
while sum(widths) > available:
|
|
59
|
+
idx = max(shrinkable_idx, key=lambda i: widths[i])
|
|
60
|
+
if widths[idx] > min_col_width:
|
|
61
|
+
widths[idx] -= 1
|
|
62
|
+
else:
|
|
63
|
+
if all(widths[i] <= min_col_width for i in shrinkable_idx):
|
|
64
|
+
break
|
|
65
|
+
# 若仍超出可用宽度,保留不截断列完整内容,允许终端自动换行/横向滚动查看
|
|
66
|
+
|
|
67
|
+
def format_row(cells):
|
|
68
|
+
parts = []
|
|
69
|
+
for i, cell in enumerate(cells):
|
|
70
|
+
text = cell if headers[i] in no_truncate_headers else _truncate_text(cell, widths[i])
|
|
71
|
+
parts.append(f" {text.ljust(widths[i])} ")
|
|
72
|
+
return "|" + "|".join(parts) + "|"
|
|
73
|
+
|
|
74
|
+
border = "+" + "+".join("-" * (w + 2) for w in widths) + "+"
|
|
75
|
+
click.echo(border)
|
|
76
|
+
click.echo(format_row(headers))
|
|
77
|
+
click.echo(border)
|
|
78
|
+
for row in normalized_rows:
|
|
79
|
+
click.echo(format_row(row))
|
|
80
|
+
click.echo(border)
|
|
81
|
+
|
|
82
|
+
def _emit_json_or_table(ctx, payload: dict, *, table_list_key: str | None = "data"):
|
|
83
|
+
"""json 模式输出完整 JSON;human 模式下若存在列表字段则尝试表格,否则缩进 JSON。"""
|
|
84
|
+
if ctx.obj.get("output_format") == "json":
|
|
85
|
+
click.echo(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
|
|
86
|
+
return
|
|
87
|
+
if table_list_key is None:
|
|
88
|
+
click.echo(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
|
|
89
|
+
return
|
|
90
|
+
rows_raw = payload.get(table_list_key)
|
|
91
|
+
if isinstance(rows_raw, list) and rows_raw and isinstance(rows_raw[0], dict):
|
|
92
|
+
first = rows_raw[0]
|
|
93
|
+
keys = list(first.keys())
|
|
94
|
+
if len(keys) > 10:
|
|
95
|
+
keys = keys[:10]
|
|
96
|
+
render_table(
|
|
97
|
+
headers=[str(k) for k in keys],
|
|
98
|
+
rows=[[r.get(k) for k in keys] for r in rows_raw],
|
|
99
|
+
no_truncate_headers={str(k) for k in keys if "id" in str(k).lower()},
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
click.echo(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@click.group()
|
|
106
|
+
@click.option(
|
|
107
|
+
"--output",
|
|
108
|
+
"-o",
|
|
109
|
+
"output_format",
|
|
110
|
+
type=click.Choice(["human", "json"], case_sensitive=False),
|
|
111
|
+
default="json",
|
|
112
|
+
show_default=True,
|
|
113
|
+
help="输出格式: human(表格) / json",
|
|
114
|
+
)
|
|
115
|
+
@click.pass_context
|
|
116
|
+
def cli(ctx, output_format):
|
|
117
|
+
"""DevOps 平台迭代管理工具"""
|
|
118
|
+
ctx.ensure_object(dict)
|
|
119
|
+
ctx.obj["output_format"] = output_format.lower()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@cli.command("login")
|
|
123
|
+
def cmd_login():
|
|
124
|
+
"""登录 FOP - 浏览器完成 SSO 后从 localStorage 缓存 micro-gosAdmin-Token"""
|
|
125
|
+
login()
|
|
126
|
+
|
|
127
|
+
@cli.group("interface")
|
|
128
|
+
def iface():
|
|
129
|
+
"""FOP/ARK 接口:搜索、详情(含响应说明)(先 login 缓存 Token)"""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@iface.command("search")
|
|
133
|
+
@click.option(
|
|
134
|
+
"--keyword",
|
|
135
|
+
"-k",
|
|
136
|
+
"vague_key",
|
|
137
|
+
required=True,
|
|
138
|
+
help="模糊搜索关键字(对应请求体 vague_key)",
|
|
139
|
+
)
|
|
140
|
+
@click.pass_context
|
|
141
|
+
def interface_search(ctx, vague_key):
|
|
142
|
+
"""接口搜索:POST /gos/api/v1/interface_base/search"""
|
|
143
|
+
api = FopArkAPI()
|
|
144
|
+
try:
|
|
145
|
+
result = api.search_interface(vague_key)
|
|
146
|
+
_emit_json_or_table(ctx, result)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
click.echo(f"错误: {e}", err=True)
|
|
149
|
+
raise click.Abort()
|
|
150
|
+
|
|
151
|
+
@iface.command("detail")
|
|
152
|
+
@click.argument("interface_id")
|
|
153
|
+
@click.option("--ability", type=int, default=1, show_default=True)
|
|
154
|
+
@click.option("--customer-app-id", default="PLM", show_default=True)
|
|
155
|
+
@click.pass_context
|
|
156
|
+
def interface__detail(ctx, interface_id, ability, customer_app_id):
|
|
157
|
+
"""接口详情+响应说明:GET detail/{id} + resp/{id}"""
|
|
158
|
+
api = FopArkAPI()
|
|
159
|
+
try:
|
|
160
|
+
detail_result = api.get_interface_detail(
|
|
161
|
+
interface_id,
|
|
162
|
+
ability=ability,
|
|
163
|
+
customer_app_id=customer_app_id,
|
|
164
|
+
)
|
|
165
|
+
resp_result = api.get_detail_resp(interface_id, ability=ability)
|
|
166
|
+
result = {
|
|
167
|
+
"interface_detail": detail_result,
|
|
168
|
+
"interface_resp": resp_result,
|
|
169
|
+
}
|
|
170
|
+
_emit_json_or_table(ctx, result, table_list_key=None)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
click.echo(f"错误: {e}", err=True)
|
|
173
|
+
raise click.Abort()
|
|
174
|
+
|
|
175
|
+
def main():
|
|
176
|
+
cli()
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""配置管理模块"""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Config:
|
|
10
|
+
"""配置类"""
|
|
11
|
+
|
|
12
|
+
CONFIG_DIR = Path.home() / ".zt-fop-cli"
|
|
13
|
+
COOKIE_FILE = CONFIG_DIR / "cookies.json"
|
|
14
|
+
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
15
|
+
# FOP 登录后从 localStorage 写入的 JWT,供 ARK API Bearer 使用
|
|
16
|
+
FOP_BEARER_FILE = CONFIG_DIR / "fop_bearer_token.txt"
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._config = self._load_config()
|
|
20
|
+
|
|
21
|
+
def _load_config(self) -> dict:
|
|
22
|
+
"""加载配置文件"""
|
|
23
|
+
if self.CONFIG_FILE.exists():
|
|
24
|
+
with open(self.CONFIG_FILE) as f:
|
|
25
|
+
return yaml.safe_load(f) or {}
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def browser_channel(self) -> str:
|
|
30
|
+
return self._config.get("browser_channel", "chromium")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
config = Config()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zt-fop-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: ZT 接口管理平台 CLI
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: playwright>=1.40.0
|
|
8
|
+
Requires-Dist: requests>=2.31.0
|
|
9
|
+
Requires-Dist: click>=8.1.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
|
|
12
|
+
# zt-fop-cli
|
|
13
|
+
|
|
14
|
+
FOP/ARK 接口查询命令行工具,用于登录并搜索接口、查看接口详情与响应说明。
|
|
15
|
+
|
|
16
|
+
## 环境要求
|
|
17
|
+
|
|
18
|
+
- Python `>=3.10`
|
|
19
|
+
- 可用浏览器(首次登录会自动打开)
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
### 开发安装(源码)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 打包发布
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m pip install --upgrade build twine
|
|
33
|
+
python -m build
|
|
34
|
+
twine upload dist/*
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 快速开始
|
|
38
|
+
|
|
39
|
+
### 1) 登录并缓存 Token
|
|
40
|
+
|
|
41
|
+
首次运行会打开浏览器,请完成登录。成功后会从 `localStorage` 读取 `micro-gosAdmin-Token` 并缓存到本地。
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
zt-fop-cli login
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2) 搜索接口
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
zt-fop-cli interface search -k 库存
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3) 查看接口详情(含响应说明)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
zt-fop-cli interface detail 123456
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 全局参数
|
|
60
|
+
- `-o, --output`:输出格式,支持:
|
|
61
|
+
- `json`(默认)
|
|
62
|
+
- `human`(表格)
|
|
63
|
+
|
|
64
|
+
示例:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
zt-fop-cli -o human interface search -k 订单
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> 说明:`interface detail` 输出为一个合并对象,包含:
|
|
71
|
+
> - `interface_detail`:接口详情
|
|
72
|
+
> - `interface_resp`:接口响应说明
|
|
73
|
+
|
|
74
|
+
## 配置说明
|
|
75
|
+
|
|
76
|
+
默认配置目录:`~/.zt-fop-cli`
|
|
77
|
+
- Token 缓存:`~/.zt-fop-cli/fop_bearer_token.txt`
|
|
78
|
+
- 配置文件:`~/.zt-fop-cli/config.yaml`
|
|
79
|
+
|
|
80
|
+
可选配置项示例:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
browser_channel: chromium
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 环境变量(可选)
|
|
87
|
+
|
|
88
|
+
支持通过环境变量自动填充登录页账号密码(如 SSO 页面字段可用):
|
|
89
|
+
|
|
90
|
+
- `ZT_SSO_USERNAME`
|
|
91
|
+
- `ZT_SSO_PASSWORD`
|
|
92
|
+
|
|
93
|
+
未设置时可在浏览器中手动完成登录。
|
|
94
|
+
|
|
95
|
+
## 命令一览
|
|
96
|
+
|
|
97
|
+
- `zt-fop-cli login`
|
|
98
|
+
- 浏览器登录并缓存 Token
|
|
99
|
+
- `zt-fop-cli interface search -k <keyword>`
|
|
100
|
+
- 模糊搜索接口
|
|
101
|
+
- `zt-fop-cli interface detail <interface_id> [--ability 1] [--customer-app-id PLM]`
|
|
102
|
+
- 查询接口详情并同时返回响应说明
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/zt_fop_cli/__init__.py
|
|
4
|
+
src/zt_fop_cli/api.py
|
|
5
|
+
src/zt_fop_cli/auth.py
|
|
6
|
+
src/zt_fop_cli/cli.py
|
|
7
|
+
src/zt_fop_cli/config.py
|
|
8
|
+
src/zt_fop_cli.egg-info/PKG-INFO
|
|
9
|
+
src/zt_fop_cli.egg-info/SOURCES.txt
|
|
10
|
+
src/zt_fop_cli.egg-info/dependency_links.txt
|
|
11
|
+
src/zt_fop_cli.egg-info/entry_points.txt
|
|
12
|
+
src/zt_fop_cli.egg-info/requires.txt
|
|
13
|
+
src/zt_fop_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zt_fop_cli
|