cli-anything-cliproxyapi 1.0.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.
@@ -0,0 +1 @@
1
+ """Core modules for CLIProxyAPI CLI harness."""
@@ -0,0 +1,144 @@
1
+ """API 密钥管理模块。"""
2
+
3
+ from typing import Any, Optional
4
+
5
+ from .client import ManagementClient
6
+
7
+
8
+ class APIKeyManager:
9
+ """管理 CLIProxyAPI 的各类 API 密钥。"""
10
+
11
+ def __init__(self, client: ManagementClient):
12
+ self.client = client
13
+
14
+ # ---- 全局 API 密钥 ----
15
+
16
+ def list_api_keys(self) -> list:
17
+ resp = self.client.get("/api-keys")
18
+ resp.raise_for_status()
19
+ return resp.json().get("value", [])
20
+
21
+ def set_api_keys(self, keys: list) -> dict:
22
+ resp = self.client.put("/api-keys", json_data={"value": keys})
23
+ resp.raise_for_status()
24
+ return resp.json()
25
+
26
+ def add_api_key(self, key: str) -> dict:
27
+ resp = self.client.patch("/api-keys", json_data={"value": key})
28
+ resp.raise_for_status()
29
+ return resp.json()
30
+
31
+ def delete_api_key(self, key: str) -> dict:
32
+ resp = self.client.delete("/api-keys", json_data={"value": key})
33
+ resp.raise_for_status()
34
+ return resp.json()
35
+
36
+ # ---- Gemini API 密钥 ----
37
+
38
+ def list_gemini_keys(self) -> list:
39
+ resp = self.client.get("/gemini-api-key")
40
+ resp.raise_for_status()
41
+ return resp.json().get("value", [])
42
+
43
+ def set_gemini_keys(self, keys: list) -> dict:
44
+ resp = self.client.put("/gemini-api-key", json_data={"value": keys})
45
+ resp.raise_for_status()
46
+ return resp.json()
47
+
48
+ def add_gemini_key(self, key_data: dict) -> dict:
49
+ resp = self.client.patch("/gemini-api-key", json_data=key_data)
50
+ resp.raise_for_status()
51
+ return resp.json()
52
+
53
+ def delete_gemini_key(self, api_key: str) -> dict:
54
+ resp = self.client.delete("/gemini-api-key", json_data={"api_key": api_key})
55
+ resp.raise_for_status()
56
+ return resp.json()
57
+
58
+ # ---- Claude API 密钥 ----
59
+
60
+ def list_claude_keys(self) -> list:
61
+ resp = self.client.get("/claude-api-key")
62
+ resp.raise_for_status()
63
+ return resp.json().get("value", [])
64
+
65
+ def set_claude_keys(self, keys: list) -> dict:
66
+ resp = self.client.put("/claude-api-key", json_data={"value": keys})
67
+ resp.raise_for_status()
68
+ return resp.json()
69
+
70
+ def add_claude_key(self, key_data: dict) -> dict:
71
+ resp = self.client.patch("/claude-api-key", json_data=key_data)
72
+ resp.raise_for_status()
73
+ return resp.json()
74
+
75
+ def delete_claude_key(self, api_key: str) -> dict:
76
+ resp = self.client.delete("/claude-api-key", json_data={"api_key": api_key})
77
+ resp.raise_for_status()
78
+ return resp.json()
79
+
80
+ # ---- Codex API 密钥 ----
81
+
82
+ def list_codex_keys(self) -> list:
83
+ resp = self.client.get("/codex-api-key")
84
+ resp.raise_for_status()
85
+ return resp.json().get("value", [])
86
+
87
+ def set_codex_keys(self, keys: list) -> dict:
88
+ resp = self.client.put("/codex-api-key", json_data={"value": keys})
89
+ resp.raise_for_status()
90
+ return resp.json()
91
+
92
+ def add_codex_key(self, key_data: dict) -> dict:
93
+ resp = self.client.patch("/codex-api-key", json_data=key_data)
94
+ resp.raise_for_status()
95
+ return resp.json()
96
+
97
+ def delete_codex_key(self, api_key: str) -> dict:
98
+ resp = self.client.delete("/codex-api-key", json_data={"api_key": api_key})
99
+ resp.raise_for_status()
100
+ return resp.json()
101
+
102
+ # ---- OpenAI 兼容提供商 ----
103
+
104
+ def list_openai_compat(self) -> list:
105
+ resp = self.client.get("/openai-compatibility")
106
+ resp.raise_for_status()
107
+ return resp.json().get("value", [])
108
+
109
+ def set_openai_compat(self, providers: list) -> dict:
110
+ resp = self.client.put("/openai-compatibility", json_data={"value": providers})
111
+ resp.raise_for_status()
112
+ return resp.json()
113
+
114
+ def add_openai_compat(self, provider: dict) -> dict:
115
+ resp = self.client.patch("/openai-compatibility", json_data=provider)
116
+ resp.raise_for_status()
117
+ return resp.json()
118
+
119
+ def delete_openai_compat(self, name: str) -> dict:
120
+ resp = self.client.delete("/openai-compatibility", json_data={"name": name})
121
+ resp.raise_for_status()
122
+ return resp.json()
123
+
124
+ # ---- Vertex API 密钥 ----
125
+
126
+ def list_vertex_keys(self) -> list:
127
+ resp = self.client.get("/vertex-api-key")
128
+ resp.raise_for_status()
129
+ return resp.json().get("value", [])
130
+
131
+ def set_vertex_keys(self, keys: list) -> dict:
132
+ resp = self.client.put("/vertex-api-key", json_data={"value": keys})
133
+ resp.raise_for_status()
134
+ return resp.json()
135
+
136
+ def add_vertex_key(self, key_data: dict) -> dict:
137
+ resp = self.client.patch("/vertex-api-key", json_data=key_data)
138
+ resp.raise_for_status()
139
+ return resp.json()
140
+
141
+ def delete_vertex_key(self, api_key: str) -> dict:
142
+ resp = self.client.delete("/vertex-api-key", json_data={"api_key": api_key})
143
+ resp.raise_for_status()
144
+ return resp.json()
@@ -0,0 +1,176 @@
1
+ """认证文件管理模块。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from typing import Any, Optional
8
+
9
+ from .client import ManagementClient
10
+
11
+ CODEX_QUOTA_URL = "https://chatgpt.com/backend-api/wham/usage"
12
+ CODEX_QUOTA_USER_AGENT = "codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal"
13
+
14
+
15
+ class AuthManager:
16
+ """管理 CLIProxyAPI 的认证文件。"""
17
+
18
+ def __init__(self, client: ManagementClient):
19
+ self.client = client
20
+
21
+ def list_auth_files(self, disabled: Optional[bool] = None) -> dict:
22
+ resp = self.client.get("/auth-files", params=None)
23
+ resp.raise_for_status()
24
+ data = resp.json()
25
+ files = data.get("files", [])
26
+ if disabled is None:
27
+ return data
28
+ filtered = [item for item in files if bool(item.get("disabled", False)) is disabled]
29
+ return {**data, "files": filtered}
30
+
31
+ def get_codex_quotas(self) -> dict:
32
+ payload = self.list_auth_files(disabled=False)
33
+ files = payload.get("files", [])
34
+ codex_files = [item for item in files if str(item.get("provider") or item.get("type") or "").strip().lower() == "codex"]
35
+ quotas = []
36
+ success = 0
37
+ failed = 0
38
+
39
+ for item in codex_files:
40
+ auth_index = str(item.get("auth_index") or "").strip()
41
+ name = str(item.get("name") or item.get("id") or auth_index or "codex")
42
+ email = str(item.get("email") or "").strip()
43
+ id_token = item.get("id_token") if isinstance(item.get("id_token"), dict) else {}
44
+ account_id = str(id_token.get("chatgpt_account_id") or "").strip()
45
+ plan_type = str(id_token.get("plan_type") or "").strip()
46
+ quota = {
47
+ "auth_index": auth_index,
48
+ "name": name,
49
+ "email": email,
50
+ "plan_type": plan_type,
51
+ }
52
+
53
+ if not auth_index:
54
+ quota["error"] = "missing auth_index"
55
+ quotas.append(quota)
56
+ failed += 1
57
+ continue
58
+ if not account_id:
59
+ quota["error"] = "missing chatgpt_account_id"
60
+ quotas.append(quota)
61
+ failed += 1
62
+ continue
63
+
64
+ request_body = {
65
+ "auth_index": auth_index,
66
+ "method": "GET",
67
+ "url": CODEX_QUOTA_URL,
68
+ "header": {
69
+ "Authorization": "Bearer $TOKEN$",
70
+ "Content-Type": "application/json",
71
+ "User-Agent": CODEX_QUOTA_USER_AGENT,
72
+ "Chatgpt-Account-Id": account_id,
73
+ },
74
+ }
75
+
76
+ try:
77
+ resp = self.client.post("/api-call", json_data=request_body)
78
+ resp.raise_for_status()
79
+ api_result = resp.json()
80
+ body = api_result.get("body") or "{}"
81
+ payload_json = json.loads(body)
82
+ rate_limit = payload_json.get("rate_limit") or {}
83
+ primary = self._build_window(rate_limit.get("primary_window") or {})
84
+ secondary = self._build_window(rate_limit.get("secondary_window") or {})
85
+ quota.update({
86
+ "status_code": api_result.get("status_code"),
87
+ "email": str(payload_json.get("email") or email).strip(),
88
+ "plan_type": str(payload_json.get("plan_type") or plan_type).strip(),
89
+ "primary_window": primary,
90
+ "secondary_window": secondary,
91
+ })
92
+ quotas.append(quota)
93
+ success += 1
94
+ except Exception as exc:
95
+ quota["error"] = str(exc)
96
+ quotas.append(quota)
97
+ failed += 1
98
+
99
+ return {
100
+ "total": len(quotas),
101
+ "success": success,
102
+ "failed": failed,
103
+ "quotas": quotas,
104
+ }
105
+
106
+ @staticmethod
107
+ def _build_window(window: dict) -> dict:
108
+ used_percent = int(window.get("used_percent") or 0)
109
+ reset_at = int(window.get("reset_at") or 0)
110
+ return {
111
+ "used_percent": used_percent,
112
+ "remaining_percent": max(0, 100 - used_percent),
113
+ "limit_window_seconds": int(window.get("limit_window_seconds") or 0),
114
+ "reset_after_seconds": int(window.get("reset_after_seconds") or 0),
115
+ "reset_at": reset_at,
116
+ "reset_at_local": datetime.fromtimestamp(reset_at).strftime("%m/%d %H:%M") if reset_at else "",
117
+ }
118
+
119
+ def get_auth_file_models(self) -> dict:
120
+ resp = self.client.get("/auth-files/models")
121
+ resp.raise_for_status()
122
+ return resp.json()
123
+
124
+ def upload_auth_file(self, filename: str, content: str) -> dict:
125
+ import requests
126
+
127
+ url = f"{self.client.conn.base_url}/v0/management/auth-files"
128
+ files = {"file": (filename, content, "application/json")}
129
+ headers = {"Authorization": f"Bearer {self.client.conn.management_key}"}
130
+ resp = requests.post(url, files=files, headers=headers, timeout=30)
131
+ resp.raise_for_status()
132
+ return resp.json()
133
+
134
+ def download_auth_file(self, filename: str) -> str:
135
+ resp = self.client.get("/auth-files/download", params={"filename": filename})
136
+ resp.raise_for_status()
137
+ return resp.text
138
+
139
+ def delete_auth_file(self, filename: str) -> dict:
140
+ resp = self.client.delete("/auth-files", json_data={"filename": filename})
141
+ resp.raise_for_status()
142
+ return resp.json()
143
+
144
+ def patch_auth_file_status(self, filename: str, disabled: bool) -> dict:
145
+ resp = self.client.patch("/auth-files/status", json_data={
146
+ "filename": filename,
147
+ "disabled": disabled,
148
+ })
149
+ resp.raise_for_status()
150
+ return resp.json()
151
+
152
+ def patch_auth_file_fields(self, filename: str, fields: dict) -> dict:
153
+ resp = self.client.patch("/auth-files/fields", json_data={
154
+ "filename": filename,
155
+ **fields,
156
+ })
157
+ resp.raise_for_status()
158
+ return resp.json()
159
+
160
+ def get_model_definitions(self, channel: str) -> dict:
161
+ resp = self.client.get(f"/model-definitions/{channel}")
162
+ resp.raise_for_status()
163
+ return resp.json()
164
+
165
+ def get_auth_status(self, session_id: str) -> dict:
166
+ resp = self.client.get("/get-auth-status", params={"session_id": session_id})
167
+ resp.raise_for_status()
168
+ return resp.json()
169
+
170
+ def import_vertex(self, key_json: str, prefix: str = "") -> dict:
171
+ data: dict[str, Any] = {"key": key_json}
172
+ if prefix:
173
+ data["prefix"] = prefix
174
+ resp = self.client.post("/vertex/import", json_data=data)
175
+ resp.raise_for_status()
176
+ return resp.json()
@@ -0,0 +1,101 @@
1
+ """HTTP 客户端,封装与 CLIProxyAPI 管理 API 的所有通信。"""
2
+
3
+ import json
4
+ import os
5
+ from typing import Any, Optional
6
+ from pathlib import Path
7
+
8
+ import requests
9
+
10
+
11
+ # 默认配置文件路径
12
+ DEFAULT_CONFIG_PATH = Path.home() / ".cliproxyapi-cli.yaml"
13
+
14
+
15
+ class ConnectionConfig:
16
+ """从 CLI 参数 / 环境变量 / 配置文件解析连接参数。"""
17
+
18
+ def __init__(
19
+ self,
20
+ url: Optional[str] = None,
21
+ key: Optional[str] = None,
22
+ ):
23
+ self.base_url = (url or os.getenv("CPA_URL") or self._load_from_config("url") or "").rstrip("/")
24
+ self.management_key = key or os.getenv("CPA_KEY") or self._load_from_config("key") or ""
25
+
26
+ @staticmethod
27
+ def _load_from_config(field: str) -> Optional[str]:
28
+ if not DEFAULT_CONFIG_PATH.exists():
29
+ return None
30
+ try:
31
+ import yaml
32
+
33
+ data = yaml.safe_load(DEFAULT_CONFIG_PATH.read_text())
34
+ if isinstance(data, dict):
35
+ return data.get(field)
36
+ except Exception:
37
+ pass
38
+ return None
39
+
40
+ def save(self, url: str, key: str) -> None:
41
+ import yaml
42
+
43
+ data = {}
44
+ if DEFAULT_CONFIG_PATH.exists():
45
+ try:
46
+ data = yaml.safe_load(DEFAULT_CONFIG_PATH.read_text()) or {}
47
+ except Exception:
48
+ data = {}
49
+ data["url"] = url.rstrip("/")
50
+ data["key"] = key
51
+ DEFAULT_CONFIG_PATH.write_text(yaml.dump(data, default_flow_style=False))
52
+
53
+ @property
54
+ def is_configured(self) -> bool:
55
+ return bool(self.base_url and self.management_key)
56
+
57
+
58
+ class ManagementClient:
59
+ """CLIProxyAPI 管理 API 的 HTTP 客户端。"""
60
+
61
+ def __init__(self, conn: ConnectionConfig):
62
+ self.conn = conn
63
+ self._session = requests.Session()
64
+
65
+ @property
66
+ def _headers(self) -> dict:
67
+ return {
68
+ "Authorization": f"Bearer {self.conn.management_key}",
69
+ "Content-Type": "application/json",
70
+ }
71
+
72
+ def _url(self, path: str) -> str:
73
+ return f"{self.conn.base_url}/v0/management{path}"
74
+
75
+ def get(self, path: str, params: Optional[dict] = None) -> requests.Response:
76
+ return self._session.get(self._url(path), headers=self._headers, params=params, timeout=30)
77
+
78
+ def post(self, path: str, json_data: Any = None, **kwargs) -> requests.Response:
79
+ return self._session.post(self._url(path), headers=self._headers, json=json_data, timeout=30, **kwargs)
80
+
81
+ def put(self, path: str, json_data: Any = None) -> requests.Response:
82
+ return self._session.put(self._url(path), headers=self._headers, json=json_data, timeout=30)
83
+
84
+ def patch(self, path: str, json_data: Any = None) -> requests.Response:
85
+ return self._session.patch(self._url(path), headers=self._headers, json=json_data, timeout=30)
86
+
87
+ def delete(self, path: str, json_data: Any = None) -> requests.Response:
88
+ return self._session.delete(self._url(path), headers=self._headers, json=json_data, timeout=30)
89
+
90
+ # ---- 代理 API(非管理端点) ----
91
+
92
+ def health_check(self) -> requests.Response:
93
+ return self._session.get(f"{self.conn.base_url}/healthz", timeout=10)
94
+
95
+ def proxy_get(self, path: str, api_key: str, params: Optional[dict] = None) -> requests.Response:
96
+ headers = {"Authorization": f"Bearer {api_key}"}
97
+ return self._session.get(f"{self.conn.base_url}{path}", headers=headers, params=params, timeout=30)
98
+
99
+ def proxy_post(self, path: str, api_key: str, json_data: Any = None) -> requests.Response:
100
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
101
+ return self._session.post(f"{self.conn.base_url}{path}", headers=headers, json=json_data, timeout=60)
@@ -0,0 +1,194 @@
1
+ """配置管理模块。"""
2
+
3
+ from typing import Any, Optional
4
+
5
+ from .client import ManagementClient
6
+
7
+
8
+ class ConfigManager:
9
+ """管理 CLIProxyAPI 的配置项。"""
10
+
11
+ def __init__(self, client: ManagementClient):
12
+ self.client = client
13
+
14
+ def get_config(self) -> dict:
15
+ resp = self.client.get("/config")
16
+ resp.raise_for_status()
17
+ return resp.json()
18
+
19
+ def get_config_yaml(self) -> str:
20
+ resp = self.client.get("/config.yaml")
21
+ resp.raise_for_status()
22
+ return resp.text
23
+
24
+ def put_config_yaml(self, yaml_content: str) -> dict:
25
+ resp = self.client.put(
26
+ "/config.yaml",
27
+ json_data=yaml_content,
28
+ )
29
+ resp.raise_for_status()
30
+ return resp.json()
31
+
32
+ # ---- 调试模式 ----
33
+
34
+ def get_debug(self) -> bool:
35
+ resp = self.client.get("/debug")
36
+ resp.raise_for_status()
37
+ return resp.json().get("value", False)
38
+
39
+ def set_debug(self, enabled: bool) -> dict:
40
+ resp = self.client.put("/debug", json_data={"value": enabled})
41
+ resp.raise_for_status()
42
+ return resp.json()
43
+
44
+ # ---- 日志写文件 ----
45
+
46
+ def get_logging_to_file(self) -> bool:
47
+ resp = self.client.get("/logging-to-file")
48
+ resp.raise_for_status()
49
+ return resp.json().get("value", False)
50
+
51
+ def set_logging_to_file(self, enabled: bool) -> dict:
52
+ resp = self.client.put("/logging-to-file", json_data={"value": enabled})
53
+ resp.raise_for_status()
54
+ return resp.json()
55
+
56
+ # ---- 日志大小限制 ----
57
+
58
+ def get_logs_max_total_size_mb(self) -> int:
59
+ resp = self.client.get("/logs-max-total-size-mb")
60
+ resp.raise_for_status()
61
+ return resp.json().get("value", 0)
62
+
63
+ def set_logs_max_total_size_mb(self, value: int) -> dict:
64
+ resp = self.client.put("/logs-max-total-size-mb", json_data={"value": value})
65
+ resp.raise_for_status()
66
+ return resp.json()
67
+
68
+ # ---- 错误日志文件数限制 ----
69
+
70
+ def get_error_logs_max_files(self) -> int:
71
+ resp = self.client.get("/error-logs-max-files")
72
+ resp.raise_for_status()
73
+ return resp.json().get("value", 10)
74
+
75
+ def set_error_logs_max_files(self, value: int) -> dict:
76
+ resp = self.client.put("/error-logs-max-files", json_data={"value": value})
77
+ resp.raise_for_status()
78
+ return resp.json()
79
+
80
+ # ---- 使用统计 ----
81
+
82
+ def get_usage_statistics_enabled(self) -> bool:
83
+ resp = self.client.get("/usage-statistics-enabled")
84
+ resp.raise_for_status()
85
+ return resp.json().get("value", False)
86
+
87
+ def set_usage_statistics_enabled(self, enabled: bool) -> dict:
88
+ resp = self.client.put("/usage-statistics-enabled", json_data={"value": enabled})
89
+ resp.raise_for_status()
90
+ return resp.json()
91
+
92
+ # ---- 代理 URL ----
93
+
94
+ def get_proxy_url(self) -> Optional[str]:
95
+ resp = self.client.get("/proxy-url")
96
+ resp.raise_for_status()
97
+ return resp.json().get("value")
98
+
99
+ def set_proxy_url(self, url: str) -> dict:
100
+ resp = self.client.put("/proxy-url", json_data={"value": url})
101
+ resp.raise_for_status()
102
+ return resp.json()
103
+
104
+ def delete_proxy_url(self) -> dict:
105
+ resp = self.client.delete("/proxy-url")
106
+ resp.raise_for_status()
107
+ return resp.json()
108
+
109
+ # ---- 请求重试 ----
110
+
111
+ def get_request_retry(self) -> int:
112
+ resp = self.client.get("/request-retry")
113
+ resp.raise_for_status()
114
+ return resp.json().get("value", 3)
115
+
116
+ def set_request_retry(self, value: int) -> dict:
117
+ resp = self.client.put("/request-retry", json_data={"value": value})
118
+ resp.raise_for_status()
119
+ return resp.json()
120
+
121
+ def get_max_retry_interval(self) -> int:
122
+ resp = self.client.get("/max-retry-interval")
123
+ resp.raise_for_status()
124
+ return resp.json().get("value", 30)
125
+
126
+ def set_max_retry_interval(self, value: int) -> dict:
127
+ resp = self.client.put("/max-retry-interval", json_data={"value": value})
128
+ resp.raise_for_status()
129
+ return resp.json()
130
+
131
+ # ---- 模型前缀 ----
132
+
133
+ def get_force_model_prefix(self) -> bool:
134
+ resp = self.client.get("/force-model-prefix")
135
+ resp.raise_for_status()
136
+ return resp.json().get("value", False)
137
+
138
+ def set_force_model_prefix(self, enabled: bool) -> dict:
139
+ resp = self.client.put("/force-model-prefix", json_data={"value": enabled})
140
+ resp.raise_for_status()
141
+ return resp.json()
142
+
143
+ # ---- 路由策略 ----
144
+
145
+ def get_routing_strategy(self) -> str:
146
+ resp = self.client.get("/routing/strategy")
147
+ resp.raise_for_status()
148
+ return resp.json().get("value", "round-robin")
149
+
150
+ def set_routing_strategy(self, strategy: str) -> dict:
151
+ resp = self.client.put("/routing/strategy", json_data={"value": strategy})
152
+ resp.raise_for_status()
153
+ return resp.json()
154
+
155
+ # ---- 配额超限行为 ----
156
+
157
+ def get_switch_project(self) -> bool:
158
+ resp = self.client.get("/quota-exceeded/switch-project")
159
+ resp.raise_for_status()
160
+ return resp.json().get("value", True)
161
+
162
+ def set_switch_project(self, enabled: bool) -> dict:
163
+ resp = self.client.put("/quota-exceeded/switch-project", json_data={"value": enabled})
164
+ resp.raise_for_status()
165
+ return resp.json()
166
+
167
+ def get_switch_preview_model(self) -> bool:
168
+ resp = self.client.get("/quota-exceeded/switch-preview-model")
169
+ resp.raise_for_status()
170
+ return resp.json().get("value", True)
171
+
172
+ def set_switch_preview_model(self, enabled: bool) -> dict:
173
+ resp = self.client.put("/quota-exceeded/switch-preview-model", json_data={"value": enabled})
174
+ resp.raise_for_status()
175
+ return resp.json()
176
+
177
+ # ---- WebSocket 认证 ----
178
+
179
+ def get_ws_auth(self) -> bool:
180
+ resp = self.client.get("/ws-auth")
181
+ resp.raise_for_status()
182
+ return resp.json().get("value", False)
183
+
184
+ def set_ws_auth(self, enabled: bool) -> dict:
185
+ resp = self.client.put("/ws-auth", json_data={"value": enabled})
186
+ resp.raise_for_status()
187
+ return resp.json()
188
+
189
+ # ---- 最新版本 ----
190
+
191
+ def get_latest_version(self) -> dict:
192
+ resp = self.client.get("/latest-version")
193
+ resp.raise_for_status()
194
+ return resp.json()
@@ -0,0 +1,47 @@
1
+ """日志管理模块。"""
2
+
3
+ from typing import Optional
4
+
5
+ from .client import ManagementClient
6
+
7
+
8
+ class LogManager:
9
+ """管理 CLIProxyAPI 的日志。"""
10
+
11
+ def __init__(self, client: ManagementClient):
12
+ self.client = client
13
+
14
+ def get_logs(self, lines: int = 100) -> str:
15
+ resp = self.client.get("/logs", params={"lines": lines})
16
+ resp.raise_for_status()
17
+ return resp.text
18
+
19
+ def delete_logs(self) -> dict:
20
+ resp = self.client.delete("/logs")
21
+ resp.raise_for_status()
22
+ return resp.json()
23
+
24
+ def get_request_log(self) -> dict:
25
+ resp = self.client.get("/request-log")
26
+ resp.raise_for_status()
27
+ return resp.json()
28
+
29
+ def set_request_log(self, enabled: bool) -> dict:
30
+ resp = self.client.put("/request-log", json_data={"value": enabled})
31
+ resp.raise_for_status()
32
+ return resp.json()
33
+
34
+ def get_request_error_logs(self) -> dict:
35
+ resp = self.client.get("/request-error-logs")
36
+ resp.raise_for_status()
37
+ return resp.json()
38
+
39
+ def download_request_error_log(self, name: str) -> str:
40
+ resp = self.client.get(f"/request-error-logs/{name}")
41
+ resp.raise_for_status()
42
+ return resp.text
43
+
44
+ def get_request_log_by_id(self, log_id: str) -> dict:
45
+ resp = self.client.get(f"/request-log-by-id/{log_id}")
46
+ resp.raise_for_status()
47
+ return resp.json()