tars-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.
- tars_cli-0.1.0/PKG-INFO +18 -0
- tars_cli-0.1.0/pyproject.toml +43 -0
- tars_cli-0.1.0/setup.cfg +4 -0
- tars_cli-0.1.0/tars_cli/__init__.py +1 -0
- tars_cli-0.1.0/tars_cli/__main__.py +4 -0
- tars_cli-0.1.0/tars_cli/app.py +164 -0
- tars_cli-0.1.0/tars_cli/auth.py +58 -0
- tars_cli-0.1.0/tars_cli/client.py +117 -0
- tars_cli-0.1.0/tars_cli/commands/__init__.py +0 -0
- tars_cli-0.1.0/tars_cli/commands/auth_cmd.py +71 -0
- tars_cli-0.1.0/tars_cli/commands/dataset_cmd.py +258 -0
- tars_cli-0.1.0/tars_cli/commands/eval_cmd.py +126 -0
- tars_cli-0.1.0/tars_cli/commands/plugin_cmd.py +103 -0
- tars_cli-0.1.0/tars_cli/commands/template_cmd.py +59 -0
- tars_cli-0.1.0/tars_cli/config.py +33 -0
- tars_cli-0.1.0/tars_cli/hints.py +59 -0
- tars_cli-0.1.0/tars_cli/output.py +64 -0
- tars_cli-0.1.0/tars_cli/utils/__init__.py +0 -0
- tars_cli-0.1.0/tars_cli/utils/polling.py +67 -0
- tars_cli-0.1.0/tars_cli/utils/progress.py +44 -0
- tars_cli-0.1.0/tars_cli.egg-info/PKG-INFO +18 -0
- tars_cli-0.1.0/tars_cli.egg-info/SOURCES.txt +29 -0
- tars_cli-0.1.0/tars_cli.egg-info/dependency_links.txt +1 -0
- tars_cli-0.1.0/tars_cli.egg-info/entry_points.txt +2 -0
- tars_cli-0.1.0/tars_cli.egg-info/requires.txt +14 -0
- tars_cli-0.1.0/tars_cli.egg-info/top_level.txt +1 -0
- tars_cli-0.1.0/tests/test_auth.py +94 -0
- tars_cli-0.1.0/tests/test_client.py +90 -0
- tars_cli-0.1.0/tests/test_config.py +40 -0
- tars_cli-0.1.0/tests/test_hints.py +39 -0
- tars_cli-0.1.0/tests/test_output.py +63 -0
tars_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tars-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: tars 平台命令行工具
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: typer>=0.12
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: rich>=13.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: platformdirs>=4.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
15
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
18
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tars-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "tars 平台命令行工具"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"typer>=0.12",
|
|
12
|
+
"httpx>=0.27",
|
|
13
|
+
"rich>=13.0",
|
|
14
|
+
"pyyaml>=6.0",
|
|
15
|
+
"platformdirs>=4.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.0",
|
|
21
|
+
"pytest-mock>=3.12",
|
|
22
|
+
"pytest-cov>=5.0",
|
|
23
|
+
"respx>=0.21",
|
|
24
|
+
"mypy>=1.8",
|
|
25
|
+
"ruff>=0.3",
|
|
26
|
+
"build>=1.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
tars = "tars_cli.app:app"
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
|
|
35
|
+
[tool.mypy]
|
|
36
|
+
python_version = "3.11"
|
|
37
|
+
strict = false
|
|
38
|
+
warn_return_any = false
|
|
39
|
+
disallow_untyped_defs = true
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
target-version = "py311"
|
|
43
|
+
line-length = 120
|
tars_cli-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from tars_cli import __version__
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="tars",
|
|
14
|
+
help=(
|
|
15
|
+
"tars CLI - 评估平台命令行工具\n\n"
|
|
16
|
+
"🚀 快速上手:\n"
|
|
17
|
+
" 路线 A(智能生成): tars auth login → tars plugin import → tars dataset generate → tars eval run\n"
|
|
18
|
+
" 路线 B(手动上传): tars auth login → tars plugin import → tars template download → tars dataset upload → tars eval run\n\n"
|
|
19
|
+
"运行 tars quickstart 查看完整示例"
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
auth_app = typer.Typer(
|
|
24
|
+
name="auth",
|
|
25
|
+
help="认证管理\n\n典型流程: tars auth login --server <url>",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
)
|
|
28
|
+
plugin_app = typer.Typer(
|
|
29
|
+
name="plugin",
|
|
30
|
+
help="Plugin 管理\n\n典型流程: tars plugin import <git_url> → tars plugin export-report <plugin_id>",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
)
|
|
33
|
+
dataset_app = typer.Typer(
|
|
34
|
+
name="dataset",
|
|
35
|
+
help="数据集管理\n\n典型流程: tars dataset generate <plugin_id> → tars dataset publish <version_id>",
|
|
36
|
+
no_args_is_help=True,
|
|
37
|
+
)
|
|
38
|
+
eval_app = typer.Typer(
|
|
39
|
+
name="eval",
|
|
40
|
+
help="评估管理\n\n典型流程: tars eval run <plugin_id> → tars eval status <task_id>",
|
|
41
|
+
no_args_is_help=True,
|
|
42
|
+
)
|
|
43
|
+
template_app = typer.Typer(
|
|
44
|
+
name="template",
|
|
45
|
+
help="模板管理\n\n典型流程: tars template download plan → tars template download testcase-single",
|
|
46
|
+
no_args_is_help=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
app.add_typer(auth_app, name="auth")
|
|
50
|
+
app.add_typer(plugin_app, name="plugin")
|
|
51
|
+
app.add_typer(dataset_app, name="dataset")
|
|
52
|
+
app.add_typer(eval_app, name="eval")
|
|
53
|
+
app.add_typer(template_app, name="template")
|
|
54
|
+
|
|
55
|
+
# Global options stored in context
|
|
56
|
+
_output_format: str = "table"
|
|
57
|
+
_quiet: bool = False
|
|
58
|
+
_verbose: bool = False
|
|
59
|
+
_server: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback(invoke_without_command=True)
|
|
63
|
+
def main(
|
|
64
|
+
ctx: typer.Context,
|
|
65
|
+
output: str = typer.Option("table", "--output", "-o", help="输出格式: table, json, yaml"),
|
|
66
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="静默模式,仅输出核心结果"),
|
|
67
|
+
verbose: bool = typer.Option(False, "--verbose", help="详细模式,输出 HTTP 请求日志"),
|
|
68
|
+
server: Optional[str] = typer.Option(None, "--server", help="服务端地址"),
|
|
69
|
+
version: bool = typer.Option(False, "--version", "-V", help="显示版本号"),
|
|
70
|
+
) -> None:
|
|
71
|
+
global _output_format, _quiet, _verbose, _server
|
|
72
|
+
if version:
|
|
73
|
+
typer.echo(f"tars-cli {__version__}")
|
|
74
|
+
raise typer.Exit()
|
|
75
|
+
_output_format = output
|
|
76
|
+
_quiet = quiet
|
|
77
|
+
_verbose = verbose
|
|
78
|
+
_server = server
|
|
79
|
+
if ctx.invoked_subcommand is None:
|
|
80
|
+
typer.echo(ctx.get_help())
|
|
81
|
+
raise typer.Exit()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_global_options() -> tuple[str, bool, bool, Optional[str]]:
|
|
85
|
+
return _output_format, _quiet, _verbose, _server
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
QUICKSTART_TEXT = """\
|
|
89
|
+
🚀 tars CLI 快速上手
|
|
90
|
+
|
|
91
|
+
━━━ 路线 A: 智能生成 ━━━
|
|
92
|
+
|
|
93
|
+
# 1. 登录
|
|
94
|
+
tars auth login --server https://tars.example.com
|
|
95
|
+
|
|
96
|
+
# 2. 导入 Plugin
|
|
97
|
+
tars plugin import https://git.example.com/my-plugin.git
|
|
98
|
+
|
|
99
|
+
# 3. 智能生成数据集(Agent / Skill)
|
|
100
|
+
tars dataset generate <plugin_id> --agent-name <name> --publish
|
|
101
|
+
tars dataset generate <plugin_id> --skill-name <name> --publish
|
|
102
|
+
|
|
103
|
+
# 多轮场景
|
|
104
|
+
tars dataset generate <plugin_id> --agent-name <name> -t multi_turn --publish
|
|
105
|
+
|
|
106
|
+
# 4. 运行评估
|
|
107
|
+
tars eval run <plugin_id> --agent-name <name> --dataset-version <version_id>
|
|
108
|
+
tars eval run <plugin_id> --skill-name <name> --dataset-version <version_id>
|
|
109
|
+
|
|
110
|
+
# 5. 导出报告(可选)
|
|
111
|
+
tars plugin export-report <plugin_id>
|
|
112
|
+
|
|
113
|
+
━━━ 路线 B: 手动上传 ━━━
|
|
114
|
+
|
|
115
|
+
# 1. 登录
|
|
116
|
+
tars auth login --server https://tars.example.com
|
|
117
|
+
|
|
118
|
+
# 2. 导入 Plugin
|
|
119
|
+
tars plugin import https://git.example.com/my-plugin.git
|
|
120
|
+
|
|
121
|
+
# 3. 下载模板
|
|
122
|
+
tars template download plan
|
|
123
|
+
tars template download testcase-single
|
|
124
|
+
tars template download testcase-multi
|
|
125
|
+
|
|
126
|
+
# 4. 填写模板后上传(Agent / Skill)
|
|
127
|
+
tars dataset upload <plugin_id> --agent-name <name> --plan plan.yaml --cases cases.yaml
|
|
128
|
+
tars dataset upload <plugin_id> --skill-name <name> --plan plan.yaml --cases cases.yaml
|
|
129
|
+
|
|
130
|
+
# 5. 运行评估
|
|
131
|
+
tars eval run <plugin_id> --agent-name <name> --dataset-version <version_id>
|
|
132
|
+
|
|
133
|
+
━━━ 其他常用命令 ━━━
|
|
134
|
+
|
|
135
|
+
# 查看数据集版本列表
|
|
136
|
+
tars dataset list <plugin_id> --agent-name <name>
|
|
137
|
+
tars dataset list <plugin_id> --skill-name <name> -t multi_turn
|
|
138
|
+
|
|
139
|
+
# 发布草稿数据集
|
|
140
|
+
tars dataset publish <version_id>
|
|
141
|
+
|
|
142
|
+
# 查看评估任务状态
|
|
143
|
+
tars eval status <task_id>
|
|
144
|
+
|
|
145
|
+
━━━ CI/CD 集成 ━━━
|
|
146
|
+
|
|
147
|
+
export TARS_ACCESS_TOKEN=eyJhbGc...
|
|
148
|
+
export TARS_SERVER_URL=https://tars.example.com
|
|
149
|
+
tars eval run <plugin_id> --agent-name <name> --dataset-version <id> -o json --threshold 0.8
|
|
150
|
+
# 退出码: 0=通过, 1=未通过, 2=错误
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command()
|
|
155
|
+
def quickstart() -> None:
|
|
156
|
+
"""输出完整的端到端使用示例"""
|
|
157
|
+
typer.echo(QUICKSTART_TEXT)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
import tars_cli.commands.auth_cmd # noqa: E402, F401
|
|
161
|
+
import tars_cli.commands.plugin_cmd # noqa: E402, F401
|
|
162
|
+
import tars_cli.commands.dataset_cmd # noqa: E402, F401
|
|
163
|
+
import tars_cli.commands.eval_cmd # noqa: E402, F401
|
|
164
|
+
import tars_cli.commands.template_cmd # noqa: E402, F401
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import stat
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from tars_cli.config import get_credentials_path, get_env_token, resolve_server_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Credentials:
|
|
13
|
+
server_url: str
|
|
14
|
+
access_token: str
|
|
15
|
+
refresh_token: str
|
|
16
|
+
expires_at: str = ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_credentials(creds: Credentials) -> None:
|
|
20
|
+
path = get_credentials_path()
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
path.write_text(json.dumps({
|
|
23
|
+
"server_url": creds.server_url,
|
|
24
|
+
"access_token": creds.access_token,
|
|
25
|
+
"refresh_token": creds.refresh_token,
|
|
26
|
+
"expires_at": creds.expires_at,
|
|
27
|
+
}, indent=2))
|
|
28
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_credentials(cli_server: str | None = None) -> Credentials | None:
|
|
32
|
+
env_token = get_env_token()
|
|
33
|
+
if env_token:
|
|
34
|
+
return Credentials(
|
|
35
|
+
server_url=resolve_server_url(cli_server),
|
|
36
|
+
access_token=env_token,
|
|
37
|
+
refresh_token="",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
path = get_credentials_path()
|
|
41
|
+
if not path.exists():
|
|
42
|
+
return None
|
|
43
|
+
try:
|
|
44
|
+
data = json.loads(path.read_text())
|
|
45
|
+
return Credentials(
|
|
46
|
+
server_url=resolve_server_url(cli_server) if cli_server else data.get("server_url", ""),
|
|
47
|
+
access_token=data["access_token"],
|
|
48
|
+
refresh_token=data.get("refresh_token", ""),
|
|
49
|
+
expires_at=data.get("expires_at", ""),
|
|
50
|
+
)
|
|
51
|
+
except (json.JSONDecodeError, KeyError):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def clear_credentials() -> None:
|
|
56
|
+
path = get_credentials_path()
|
|
57
|
+
if path.exists():
|
|
58
|
+
path.unlink()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from httpx import HTTPTransport
|
|
9
|
+
|
|
10
|
+
from tars_cli.auth import Credentials, load_credentials, save_credentials
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthExpiredError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TarsAPIClient:
|
|
18
|
+
def __init__(self, server_url: str, credentials: Credentials, verbose: bool = False) -> None:
|
|
19
|
+
self._server_url = server_url.rstrip("/")
|
|
20
|
+
self._credentials = credentials
|
|
21
|
+
self._verbose = verbose
|
|
22
|
+
self._client = httpx.Client(base_url=f"{self._server_url}/api/v1", timeout=30.0, transport=HTTPTransport(proxy=None))
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_context(cls, cli_server: str | None = None, verbose: bool = False) -> TarsAPIClient:
|
|
26
|
+
creds = load_credentials(cli_server)
|
|
27
|
+
if not creds:
|
|
28
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
29
|
+
raise typer.Exit(code=2)
|
|
30
|
+
server_url = creds.server_url
|
|
31
|
+
return cls(server_url=server_url, credentials=creds, verbose=verbose)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def server_url(self) -> str:
|
|
35
|
+
return self._server_url
|
|
36
|
+
|
|
37
|
+
def _headers(self) -> dict[str, str]:
|
|
38
|
+
return {"Authorization": f"Bearer {self._credentials.access_token}"}
|
|
39
|
+
|
|
40
|
+
def _log_request(self, method: str, url: str, **kwargs: Any) -> None:
|
|
41
|
+
if self._verbose:
|
|
42
|
+
typer.echo(f" → {method} {url}", err=True)
|
|
43
|
+
if "json" in kwargs:
|
|
44
|
+
typer.echo(f" Body: {kwargs['json']}", err=True)
|
|
45
|
+
|
|
46
|
+
def _log_response(self, resp: httpx.Response) -> None:
|
|
47
|
+
if self._verbose:
|
|
48
|
+
typer.echo(f" ← {resp.status_code} ({len(resp.content)} bytes)", err=True)
|
|
49
|
+
|
|
50
|
+
def _refresh_token(self) -> bool:
|
|
51
|
+
if not self._credentials.refresh_token:
|
|
52
|
+
return False
|
|
53
|
+
try:
|
|
54
|
+
resp = self._client.post("/auth/refresh", json={"refresh_token": self._credentials.refresh_token})
|
|
55
|
+
if resp.status_code == 200:
|
|
56
|
+
data = resp.json()
|
|
57
|
+
self._credentials.access_token = data["access_token"]
|
|
58
|
+
self._credentials.refresh_token = data.get("refresh_token", self._credentials.refresh_token)
|
|
59
|
+
self._credentials.expires_at = data.get("expires_at", "")
|
|
60
|
+
save_credentials(self._credentials)
|
|
61
|
+
return True
|
|
62
|
+
except httpx.HTTPError:
|
|
63
|
+
pass
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
|
67
|
+
url = path
|
|
68
|
+
headers = kwargs.pop("headers", None) or {}
|
|
69
|
+
headers.update(self._headers())
|
|
70
|
+
kwargs["headers"] = headers
|
|
71
|
+
self._log_request(method, url, **kwargs)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
resp = self._client.request(method, url, **kwargs)
|
|
75
|
+
except httpx.ConnectError:
|
|
76
|
+
typer.echo(f"无法连接到服务端: {self._server_url}", err=True)
|
|
77
|
+
raise typer.Exit(code=2)
|
|
78
|
+
except httpx.TimeoutException:
|
|
79
|
+
typer.echo(f"请求超时: {method} {url}", err=True)
|
|
80
|
+
raise typer.Exit(code=2)
|
|
81
|
+
|
|
82
|
+
self._log_response(resp)
|
|
83
|
+
|
|
84
|
+
if resp.status_code == 401:
|
|
85
|
+
if self._refresh_token():
|
|
86
|
+
kwargs["headers"].update(self._headers())
|
|
87
|
+
self._log_request(method, url, **kwargs)
|
|
88
|
+
resp = self._client.request(method, url, **kwargs)
|
|
89
|
+
self._log_response(resp)
|
|
90
|
+
else:
|
|
91
|
+
raise AuthExpiredError("认证已过期,请重新执行 tars auth login")
|
|
92
|
+
|
|
93
|
+
return resp
|
|
94
|
+
|
|
95
|
+
def get(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
96
|
+
try:
|
|
97
|
+
return self._request("GET", path, **kwargs)
|
|
98
|
+
except AuthExpiredError as e:
|
|
99
|
+
typer.echo(str(e), err=True)
|
|
100
|
+
raise typer.Exit(code=2)
|
|
101
|
+
|
|
102
|
+
def post(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
103
|
+
try:
|
|
104
|
+
return self._request("POST", path, **kwargs)
|
|
105
|
+
except AuthExpiredError as e:
|
|
106
|
+
typer.echo(str(e), err=True)
|
|
107
|
+
raise typer.Exit(code=2)
|
|
108
|
+
|
|
109
|
+
def put(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
110
|
+
try:
|
|
111
|
+
return self._request("PUT", path, **kwargs)
|
|
112
|
+
except AuthExpiredError as e:
|
|
113
|
+
typer.echo(str(e), err=True)
|
|
114
|
+
raise typer.Exit(code=2)
|
|
115
|
+
|
|
116
|
+
def close(self) -> None:
|
|
117
|
+
self._client.close()
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from httpx import HTTPTransport
|
|
7
|
+
|
|
8
|
+
from tars_cli.app import auth_app, get_global_options
|
|
9
|
+
from tars_cli.auth import Credentials, clear_credentials, load_credentials, save_credentials
|
|
10
|
+
from tars_cli.client import TarsAPIClient
|
|
11
|
+
from tars_cli.config import resolve_server_url
|
|
12
|
+
from tars_cli.hints import print_hints
|
|
13
|
+
from tars_cli.output import render
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@auth_app.command()
|
|
17
|
+
def login(
|
|
18
|
+
server: str = typer.Option("", "--server", "-s", help="服务端地址"),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""登录 tars 平台"""
|
|
21
|
+
output_fmt, quiet, verbose, global_server = get_global_options()
|
|
22
|
+
server_url = resolve_server_url(server or global_server)
|
|
23
|
+
|
|
24
|
+
username = typer.prompt("用户名")
|
|
25
|
+
password = typer.prompt("密码", hide_input=True)
|
|
26
|
+
|
|
27
|
+
client = httpx.Client(base_url=f"{server_url}/api/v1", timeout=30.0, transport=HTTPTransport(proxy=None))
|
|
28
|
+
try:
|
|
29
|
+
resp = client.post("/auth/login", json={"username": username, "password": password})
|
|
30
|
+
if resp.status_code != 200:
|
|
31
|
+
typer.echo(f"登录失败: {resp.json().get('detail', resp.text)}", err=True)
|
|
32
|
+
raise typer.Exit(code=2)
|
|
33
|
+
|
|
34
|
+
data = resp.json()
|
|
35
|
+
creds = Credentials(
|
|
36
|
+
server_url=server_url,
|
|
37
|
+
access_token=data["access_token"],
|
|
38
|
+
refresh_token=data.get("refresh_token", ""),
|
|
39
|
+
expires_at=data.get("expires_at", ""),
|
|
40
|
+
)
|
|
41
|
+
save_credentials(creds)
|
|
42
|
+
|
|
43
|
+
me_resp = client.get("/auth/me", headers={"Authorization": f"Bearer {creds.access_token}"})
|
|
44
|
+
user_info = me_resp.json() if me_resp.status_code == 200 else {}
|
|
45
|
+
|
|
46
|
+
result = {
|
|
47
|
+
"message": "登录成功",
|
|
48
|
+
"username": user_info.get("username", username),
|
|
49
|
+
"server": server_url,
|
|
50
|
+
}
|
|
51
|
+
web_url, next_step = print_hints(server_url, "auth_login", quiet=quiet)
|
|
52
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
53
|
+
finally:
|
|
54
|
+
client.close()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@auth_app.command()
|
|
58
|
+
def logout() -> None:
|
|
59
|
+
"""登出并清除本地凭证"""
|
|
60
|
+
output_fmt, quiet, verbose, global_server = get_global_options()
|
|
61
|
+
creds = load_credentials(global_server)
|
|
62
|
+
if creds and creds.refresh_token:
|
|
63
|
+
try:
|
|
64
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
65
|
+
client.post("/auth/logout", json={"refresh_token": creds.refresh_token})
|
|
66
|
+
client.close()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
clear_credentials()
|
|
71
|
+
typer.echo("已登出")
|