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.
- cli_anything/cliproxyapi/__init__.py +3 -0
- cli_anything/cliproxyapi/cliproxyapi_cli.py +1238 -0
- cli_anything/cliproxyapi/core/__init__.py +1 -0
- cli_anything/cliproxyapi/core/api_keys.py +144 -0
- cli_anything/cliproxyapi/core/auth.py +176 -0
- cli_anything/cliproxyapi/core/client.py +101 -0
- cli_anything/cliproxyapi/core/config.py +194 -0
- cli_anything/cliproxyapi/core/logs.py +47 -0
- cli_anything/cliproxyapi/core/models.py +65 -0
- cli_anything/cliproxyapi/core/oauth.py +57 -0
- cli_anything/cliproxyapi/core/proxy.py +145 -0
- cli_anything/cliproxyapi/core/usage.py +25 -0
- cli_anything/cliproxyapi/tests/test_core.py +621 -0
- cli_anything/cliproxyapi/tests/test_full_e2e.py +309 -0
- cli_anything/cliproxyapi/utils/__init__.py +1 -0
- cli_anything/cliproxyapi/utils/output.py +77 -0
- cli_anything_cliproxyapi-1.0.0.dist-info/METADATA +10 -0
- cli_anything_cliproxyapi-1.0.0.dist-info/RECORD +21 -0
- cli_anything_cliproxyapi-1.0.0.dist-info/WHEEL +5 -0
- cli_anything_cliproxyapi-1.0.0.dist-info/entry_points.txt +2 -0
- cli_anything_cliproxyapi-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1238 @@
|
|
|
1
|
+
"""CLIProxyAPI CLI 入口 - 基于 Click 的命令行工具。
|
|
2
|
+
|
|
3
|
+
支持 REPL 模式和一次性命令,所有命令支持 --json 输出。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from .core.client import ConnectionConfig, ManagementClient
|
|
13
|
+
from .core.config import ConfigManager
|
|
14
|
+
from .core.auth import AuthManager
|
|
15
|
+
from .core.oauth import OAuthManager
|
|
16
|
+
from .core.models import ModelManager
|
|
17
|
+
from .core.usage import UsageManager
|
|
18
|
+
from .core.logs import LogManager
|
|
19
|
+
from .core.api_keys import APIKeyManager
|
|
20
|
+
from .core.proxy import ProxyManager
|
|
21
|
+
from .utils.output import output_result, output_json, output_error, output_table, console
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---- 全局上下文 ----
|
|
25
|
+
|
|
26
|
+
class Context:
|
|
27
|
+
"""CLI 运行时上下文。"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.json_mode = False
|
|
31
|
+
self._client: Optional[ManagementClient] = None
|
|
32
|
+
self._conn: Optional[ConnectionConfig] = None
|
|
33
|
+
|
|
34
|
+
def setup(self, url: Optional[str], key: Optional[str]):
|
|
35
|
+
self._conn = ConnectionConfig(url=url, key=key)
|
|
36
|
+
self._client = ManagementClient(self._conn)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def client(self) -> ManagementClient:
|
|
40
|
+
if self._client is None:
|
|
41
|
+
self.setup(None, None)
|
|
42
|
+
return self._client
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def conn(self) -> ConnectionConfig:
|
|
46
|
+
if self._conn is None:
|
|
47
|
+
self.setup(None, None)
|
|
48
|
+
return self._conn
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def config_mgr(self) -> ConfigManager:
|
|
52
|
+
return ConfigManager(self.client)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def auth_mgr(self) -> AuthManager:
|
|
56
|
+
return AuthManager(self.client)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def oauth_mgr(self) -> OAuthManager:
|
|
60
|
+
return OAuthManager(self.client)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def model_mgr(self) -> ModelManager:
|
|
64
|
+
return ModelManager(self.client)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def usage_mgr(self) -> UsageManager:
|
|
68
|
+
return UsageManager(self.client)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def log_mgr(self) -> LogManager:
|
|
72
|
+
return LogManager(self.client)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def key_mgr(self) -> APIKeyManager:
|
|
76
|
+
return APIKeyManager(self.client)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def proxy_mgr(self) -> ProxyManager:
|
|
80
|
+
return ProxyManager(self.client)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
pass_ctx = click.make_pass_decorator(Context, ensure=True)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def handle_error(ctx: Context, exc: Exception):
|
|
87
|
+
if ctx.json_mode:
|
|
88
|
+
output_json({"error": str(exc)})
|
|
89
|
+
else:
|
|
90
|
+
output_error(str(exc))
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---- 根命令组 ----
|
|
95
|
+
|
|
96
|
+
@click.group(invoke_without_command=True)
|
|
97
|
+
@click.option("--url", "-u", envvar="CPA_URL", help="CLIProxyAPI 服务器地址")
|
|
98
|
+
@click.option("--key", "-k", envvar="CPA_KEY", help="管理 API 密钥")
|
|
99
|
+
@click.option("--json", "json_mode", is_flag=True, help="JSON 格式输出")
|
|
100
|
+
@click.option("--repl", "repl_mode", is_flag=True, help="进入交互式 REPL")
|
|
101
|
+
@click.version_option(version="1.0.0", prog_name="cli-anything-cliproxyapi")
|
|
102
|
+
@click.pass_context
|
|
103
|
+
def main(click_ctx, url, key, json_mode, repl_mode):
|
|
104
|
+
"""CLIProxyAPI 命令行管理工具。
|
|
105
|
+
|
|
106
|
+
通过 Management API 管理 CLIProxyAPI 代理服务器的配置、认证、模型等。
|
|
107
|
+
|
|
108
|
+
连接参数优先级: --url/--key > 环境变量 CPA_URL/CPA_KEY > ~/.cliproxyapi-cli.yaml
|
|
109
|
+
"""
|
|
110
|
+
ctx: Context = click_ctx.ensure_object(Context)
|
|
111
|
+
ctx.json_mode = json_mode
|
|
112
|
+
ctx.setup(url, key)
|
|
113
|
+
|
|
114
|
+
if repl_mode:
|
|
115
|
+
_enter_repl(click_ctx, ctx)
|
|
116
|
+
elif click_ctx.invoked_subcommand is None:
|
|
117
|
+
click.echo(click_ctx.get_help())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _enter_repl(click_ctx, ctx: Context):
|
|
121
|
+
"""交互式 REPL 模式。"""
|
|
122
|
+
import prompt_toolkit
|
|
123
|
+
from prompt_toolkit.history import FileHistory
|
|
124
|
+
|
|
125
|
+
history = FileHistory(str(ConnectionConfig._load_from_config.__func__.__self__ or ""))
|
|
126
|
+
session = prompt_toolkit.PromptSession(
|
|
127
|
+
history=FileHistory(str(ConnectionConfig.DEFAULT_CONFIG_PATH.parent / ".cliproxyapi_history")),
|
|
128
|
+
message="cliproxyapi> ",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
console.print("[bold]CLIProxyAPI REPL[/bold] - 输入 help 查看命令, exit 退出")
|
|
132
|
+
while True:
|
|
133
|
+
try:
|
|
134
|
+
line = session.prompt()
|
|
135
|
+
except (EOFError, KeyboardInterrupt):
|
|
136
|
+
break
|
|
137
|
+
line = line.strip()
|
|
138
|
+
if not line or line in ("exit", "quit"):
|
|
139
|
+
break
|
|
140
|
+
if line == "help":
|
|
141
|
+
click.echo(click_ctx.get_help())
|
|
142
|
+
continue
|
|
143
|
+
try:
|
|
144
|
+
args = line.split()
|
|
145
|
+
main(args, standalone_mode=False, parent=click_ctx.parent)
|
|
146
|
+
except SystemExit:
|
|
147
|
+
pass
|
|
148
|
+
except Exception as e:
|
|
149
|
+
output_error(str(e))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ============================================================
|
|
153
|
+
# server 命令组
|
|
154
|
+
# ============================================================
|
|
155
|
+
|
|
156
|
+
@main.group()
|
|
157
|
+
@pass_ctx
|
|
158
|
+
def server(ctx: Context):
|
|
159
|
+
"""服务器状态管理。"""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@server.command("status")
|
|
163
|
+
@pass_ctx
|
|
164
|
+
def server_status(ctx: Context):
|
|
165
|
+
"""检查服务器健康状态。"""
|
|
166
|
+
try:
|
|
167
|
+
result = ctx.proxy_mgr.health_check()
|
|
168
|
+
output_result(result, ctx.json_mode)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
handle_error(ctx, e)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@server.command("version")
|
|
174
|
+
@pass_ctx
|
|
175
|
+
def server_version(ctx: Context):
|
|
176
|
+
"""获取最新版本信息。"""
|
|
177
|
+
try:
|
|
178
|
+
result = ctx.config_mgr.get_latest_version()
|
|
179
|
+
output_result(result, ctx.json_mode)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
handle_error(ctx, e)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ============================================================
|
|
185
|
+
# config 命令组
|
|
186
|
+
# ============================================================
|
|
187
|
+
|
|
188
|
+
@main.group()
|
|
189
|
+
@pass_ctx
|
|
190
|
+
def config(ctx: Context):
|
|
191
|
+
"""配置管理。"""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@config.command("get")
|
|
195
|
+
@pass_ctx
|
|
196
|
+
def config_get(ctx: Context):
|
|
197
|
+
"""获取当前配置。"""
|
|
198
|
+
try:
|
|
199
|
+
result = ctx.config_mgr.get_config()
|
|
200
|
+
output_result(result, ctx.json_mode)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
handle_error(ctx, e)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@config.command("get-yaml")
|
|
206
|
+
@pass_ctx
|
|
207
|
+
def config_get_yaml(ctx: Context):
|
|
208
|
+
"""获取 YAML 配置。"""
|
|
209
|
+
try:
|
|
210
|
+
result = ctx.config_mgr.get_config_yaml()
|
|
211
|
+
output_result(result, ctx.json_mode)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
handle_error(ctx, e)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@config.command("set-yaml")
|
|
217
|
+
@click.argument("yaml_content", required=False)
|
|
218
|
+
@click.option("--file", "-f", type=click.Path(exists=True), help="从文件读取 YAML")
|
|
219
|
+
@pass_ctx
|
|
220
|
+
def config_set_yaml(ctx: Context, yaml_content, file):
|
|
221
|
+
"""更新 YAML 配置。"""
|
|
222
|
+
try:
|
|
223
|
+
if file:
|
|
224
|
+
with open(file) as f:
|
|
225
|
+
yaml_content = f.read()
|
|
226
|
+
if not yaml_content:
|
|
227
|
+
click.echo("请提供 YAML 内容或使用 --file 指定文件")
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
result = ctx.config_mgr.put_config_yaml(yaml_content)
|
|
230
|
+
output_result(result, ctx.json_mode)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
handle_error(ctx, e)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@config.command("debug")
|
|
236
|
+
@click.argument("value", type=bool, required=False)
|
|
237
|
+
@pass_ctx
|
|
238
|
+
def config_debug(ctx: Context, value):
|
|
239
|
+
"""获取或设置调试模式。"""
|
|
240
|
+
try:
|
|
241
|
+
if value is None:
|
|
242
|
+
result = ctx.config_mgr.get_debug()
|
|
243
|
+
else:
|
|
244
|
+
result = ctx.config_mgr.set_debug(value)
|
|
245
|
+
output_result(result, ctx.json_mode)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
handle_error(ctx, e)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@config.command("proxy-url")
|
|
251
|
+
@click.argument("value", required=False)
|
|
252
|
+
@click.option("--delete", is_flag=True, help="删除代理 URL")
|
|
253
|
+
@pass_ctx
|
|
254
|
+
def config_proxy_url(ctx: Context, value, delete):
|
|
255
|
+
"""获取、设置或删除代理 URL。"""
|
|
256
|
+
try:
|
|
257
|
+
if delete:
|
|
258
|
+
result = ctx.proxy_mgr.delete_proxy_url()
|
|
259
|
+
elif value:
|
|
260
|
+
result = ctx.config_mgr.set_proxy_url(value)
|
|
261
|
+
else:
|
|
262
|
+
result = ctx.config_mgr.get_proxy_url()
|
|
263
|
+
output_result(result, ctx.json_mode)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
handle_error(ctx, e)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@config.command("routing")
|
|
269
|
+
@click.argument("value", required=False)
|
|
270
|
+
@pass_ctx
|
|
271
|
+
def config_routing(ctx: Context, value):
|
|
272
|
+
"""获取或设置路由策略 (round-robin / fill-first)。"""
|
|
273
|
+
try:
|
|
274
|
+
if value:
|
|
275
|
+
result = ctx.config_mgr.set_routing_strategy(value)
|
|
276
|
+
else:
|
|
277
|
+
result = ctx.config_mgr.get_routing_strategy()
|
|
278
|
+
output_result(result, ctx.json_mode)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
handle_error(ctx, e)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@config.command("retry")
|
|
284
|
+
@click.argument("value", type=int, required=False)
|
|
285
|
+
@pass_ctx
|
|
286
|
+
def config_retry(ctx: Context, value):
|
|
287
|
+
"""获取或设置请求重试次数。"""
|
|
288
|
+
try:
|
|
289
|
+
if value is not None:
|
|
290
|
+
result = ctx.config_mgr.set_request_retry(value)
|
|
291
|
+
else:
|
|
292
|
+
result = ctx.config_mgr.get_request_retry()
|
|
293
|
+
output_result(result, ctx.json_mode)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
handle_error(ctx, e)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@config.command("max-retry-interval")
|
|
299
|
+
@click.argument("value", type=int, required=False)
|
|
300
|
+
@pass_ctx
|
|
301
|
+
def config_max_retry_interval(ctx: Context, value):
|
|
302
|
+
"""获取或设置最大重试等待时间(秒)。"""
|
|
303
|
+
try:
|
|
304
|
+
if value is not None:
|
|
305
|
+
result = ctx.config_mgr.set_max_retry_interval(value)
|
|
306
|
+
else:
|
|
307
|
+
result = ctx.config_mgr.get_max_retry_interval()
|
|
308
|
+
output_result(result, ctx.json_mode)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
handle_error(ctx, e)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@config.command("force-model-prefix")
|
|
314
|
+
@click.argument("value", type=bool, required=False)
|
|
315
|
+
@pass_ctx
|
|
316
|
+
def config_force_model_prefix(ctx: Context, value):
|
|
317
|
+
"""获取或设置强制模型前缀。"""
|
|
318
|
+
try:
|
|
319
|
+
if value is None:
|
|
320
|
+
result = ctx.config_mgr.get_force_model_prefix()
|
|
321
|
+
else:
|
|
322
|
+
result = ctx.config_mgr.set_force_model_prefix(value)
|
|
323
|
+
output_result(result, ctx.json_mode)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
handle_error(ctx, e)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@config.command("ws-auth")
|
|
329
|
+
@click.argument("value", type=bool, required=False)
|
|
330
|
+
@pass_ctx
|
|
331
|
+
def config_ws_auth(ctx: Context, value):
|
|
332
|
+
"""获取或设置 WebSocket 认证。"""
|
|
333
|
+
try:
|
|
334
|
+
if value is None:
|
|
335
|
+
result = ctx.config_mgr.get_ws_auth()
|
|
336
|
+
else:
|
|
337
|
+
result = ctx.config_mgr.set_ws_auth(value)
|
|
338
|
+
output_result(result, ctx.json_mode)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
handle_error(ctx, e)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@config.command("quota-switch-project")
|
|
344
|
+
@click.argument("value", type=bool, required=False)
|
|
345
|
+
@pass_ctx
|
|
346
|
+
def config_quota_switch_project(ctx: Context, value):
|
|
347
|
+
"""获取或设置配额超限时自动切换项目。"""
|
|
348
|
+
try:
|
|
349
|
+
if value is None:
|
|
350
|
+
result = ctx.config_mgr.get_switch_project()
|
|
351
|
+
else:
|
|
352
|
+
result = ctx.config_mgr.set_switch_project(value)
|
|
353
|
+
output_result(result, ctx.json_mode)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
handle_error(ctx, e)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@config.command("quota-switch-preview")
|
|
359
|
+
@click.argument("value", type=bool, required=False)
|
|
360
|
+
@pass_ctx
|
|
361
|
+
def config_quota_switch_preview(ctx: Context, value):
|
|
362
|
+
"""获取或设置配额超限时自动切换预览模型。"""
|
|
363
|
+
try:
|
|
364
|
+
if value is None:
|
|
365
|
+
result = ctx.config_mgr.get_switch_preview_model()
|
|
366
|
+
else:
|
|
367
|
+
result = ctx.config_mgr.set_switch_preview_model(value)
|
|
368
|
+
output_result(result, ctx.json_mode)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
handle_error(ctx, e)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@config.command("save-connection")
|
|
374
|
+
@click.option("--url", "-u", required=True, help="服务器地址")
|
|
375
|
+
@click.option("--key", "-k", required=True, help="管理密钥")
|
|
376
|
+
@pass_ctx
|
|
377
|
+
def config_save_connection(ctx: Context, url, key):
|
|
378
|
+
"""保存连接参数到配置文件。"""
|
|
379
|
+
try:
|
|
380
|
+
ctx.conn.save(url, key)
|
|
381
|
+
output_result({"status": "ok", "saved_to": str(ConnectionConfig.DEFAULT_CONFIG_PATH)}, ctx.json_mode)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
handle_error(ctx, e)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ============================================================
|
|
387
|
+
# auth 命令组
|
|
388
|
+
# ============================================================
|
|
389
|
+
|
|
390
|
+
@main.group()
|
|
391
|
+
@pass_ctx
|
|
392
|
+
def auth(ctx: Context):
|
|
393
|
+
"""认证文件管理。"""
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@auth.command("list")
|
|
397
|
+
@click.option("--enabled", is_flag=True, help="仅显示启用的认证文件")
|
|
398
|
+
@click.option("--disabled", "disabled_only", is_flag=True, help="仅显示禁用的认证文件")
|
|
399
|
+
@pass_ctx
|
|
400
|
+
def auth_list(ctx: Context, enabled, disabled_only):
|
|
401
|
+
"""列出认证文件。"""
|
|
402
|
+
try:
|
|
403
|
+
if enabled and disabled_only:
|
|
404
|
+
raise click.UsageError("--enabled 和 --disabled 不能同时使用")
|
|
405
|
+
disabled = None
|
|
406
|
+
if enabled:
|
|
407
|
+
disabled = False
|
|
408
|
+
elif disabled_only:
|
|
409
|
+
disabled = True
|
|
410
|
+
result = ctx.auth_mgr.list_auth_files(disabled=disabled)
|
|
411
|
+
output_result(result, ctx.json_mode)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
handle_error(ctx, e)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@auth.command("codex-quota")
|
|
417
|
+
@pass_ctx
|
|
418
|
+
def auth_codex_quota(ctx: Context):
|
|
419
|
+
"""获取已启用 Codex 凭证额度。"""
|
|
420
|
+
try:
|
|
421
|
+
result = ctx.auth_mgr.get_codex_quotas()
|
|
422
|
+
if ctx.json_mode:
|
|
423
|
+
output_result(result, True)
|
|
424
|
+
return
|
|
425
|
+
quotas = result.get("quotas", [])
|
|
426
|
+
rows = []
|
|
427
|
+
for item in quotas:
|
|
428
|
+
if item.get("error"):
|
|
429
|
+
rows.append([
|
|
430
|
+
item.get("name", ""),
|
|
431
|
+
item.get("plan_type", "-"),
|
|
432
|
+
"失败",
|
|
433
|
+
item.get("error", ""),
|
|
434
|
+
"失败",
|
|
435
|
+
"",
|
|
436
|
+
])
|
|
437
|
+
continue
|
|
438
|
+
primary = item.get("primary_window") or {}
|
|
439
|
+
secondary = item.get("secondary_window") or {}
|
|
440
|
+
rows.append([
|
|
441
|
+
item.get("name", ""),
|
|
442
|
+
item.get("plan_type", "-"),
|
|
443
|
+
f"{primary.get('remaining_percent', 0)}%",
|
|
444
|
+
primary.get("reset_at_local", ""),
|
|
445
|
+
f"{secondary.get('remaining_percent', 0)}%",
|
|
446
|
+
secondary.get("reset_at_local", ""),
|
|
447
|
+
])
|
|
448
|
+
output_table(["文件", "套餐", "5小时额度", "5小时重置", "周额度", "周重置"], rows, title="Codex 额度")
|
|
449
|
+
except Exception as e:
|
|
450
|
+
handle_error(ctx, e)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@auth.command("models")
|
|
454
|
+
@pass_ctx
|
|
455
|
+
def auth_models(ctx: Context):
|
|
456
|
+
"""查看认证文件关联的模型。"""
|
|
457
|
+
try:
|
|
458
|
+
result = ctx.auth_mgr.get_auth_file_models()
|
|
459
|
+
output_result(result, ctx.json_mode)
|
|
460
|
+
except Exception as e:
|
|
461
|
+
handle_error(ctx, e)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@auth.command("upload")
|
|
465
|
+
@click.argument("filename")
|
|
466
|
+
@click.option("--content", "-c", help="认证文件内容")
|
|
467
|
+
@click.option("--file", "-f", type=click.Path(exists=True), help="从文件上传")
|
|
468
|
+
@pass_ctx
|
|
469
|
+
def auth_upload(ctx: Context, filename, content, file):
|
|
470
|
+
"""上传认证文件。"""
|
|
471
|
+
try:
|
|
472
|
+
if file:
|
|
473
|
+
with open(file) as f:
|
|
474
|
+
content = f.read()
|
|
475
|
+
if not content:
|
|
476
|
+
click.echo("请提供 --content 或 --file")
|
|
477
|
+
sys.exit(1)
|
|
478
|
+
result = ctx.auth_mgr.upload_auth_file(filename, content)
|
|
479
|
+
output_result(result, ctx.json_mode)
|
|
480
|
+
except Exception as e:
|
|
481
|
+
handle_error(ctx, e)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@auth.command("download")
|
|
485
|
+
@click.argument("filename")
|
|
486
|
+
@pass_ctx
|
|
487
|
+
def auth_download(ctx: Context, filename):
|
|
488
|
+
"""下载认证文件。"""
|
|
489
|
+
try:
|
|
490
|
+
result = ctx.auth_mgr.download_auth_file(filename)
|
|
491
|
+
output_result(result, ctx.json_mode)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
handle_error(ctx, e)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@auth.command("delete")
|
|
497
|
+
@click.argument("filename")
|
|
498
|
+
@pass_ctx
|
|
499
|
+
def auth_delete(ctx: Context, filename):
|
|
500
|
+
"""删除认证文件。"""
|
|
501
|
+
try:
|
|
502
|
+
result = ctx.auth_mgr.delete_auth_file(filename)
|
|
503
|
+
output_result(result, ctx.json_mode)
|
|
504
|
+
except Exception as e:
|
|
505
|
+
handle_error(ctx, e)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@auth.command("status")
|
|
509
|
+
@click.argument("filename")
|
|
510
|
+
@click.argument("disabled", type=bool)
|
|
511
|
+
@pass_ctx
|
|
512
|
+
def auth_status(ctx: Context, filename, disabled):
|
|
513
|
+
"""设置认证文件启用/禁用状态。"""
|
|
514
|
+
try:
|
|
515
|
+
result = ctx.auth_mgr.patch_auth_file_status(filename, disabled)
|
|
516
|
+
output_result(result, ctx.json_mode)
|
|
517
|
+
except Exception as e:
|
|
518
|
+
handle_error(ctx, e)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@auth.command("fields")
|
|
522
|
+
@click.argument("filename")
|
|
523
|
+
@click.option("--set", "field_sets", multiple=True, nargs=2, help="设置字段: --set key value")
|
|
524
|
+
@pass_ctx
|
|
525
|
+
def auth_fields(ctx: Context, filename, field_sets):
|
|
526
|
+
"""更新认证文件字段。"""
|
|
527
|
+
try:
|
|
528
|
+
fields = {k: v for k, v in field_sets}
|
|
529
|
+
result = ctx.auth_mgr.patch_auth_file_fields(filename, fields)
|
|
530
|
+
output_result(result, ctx.json_mode)
|
|
531
|
+
except Exception as e:
|
|
532
|
+
handle_error(ctx, e)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@auth.command("vertex-import")
|
|
536
|
+
@click.argument("key_json")
|
|
537
|
+
@click.option("--prefix", default="", help="Vertex 模型命名前缀")
|
|
538
|
+
@pass_ctx
|
|
539
|
+
def auth_vertex_import(ctx: Context, key_json, prefix):
|
|
540
|
+
"""导入 Vertex 服务账号密钥。"""
|
|
541
|
+
try:
|
|
542
|
+
result = ctx.auth_mgr.import_vertex(key_json, prefix)
|
|
543
|
+
output_result(result, ctx.json_mode)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
handle_error(ctx, e)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@auth.command("definitions")
|
|
549
|
+
@click.argument("channel")
|
|
550
|
+
@pass_ctx
|
|
551
|
+
def auth_definitions(ctx: Context, channel):
|
|
552
|
+
"""查看指定渠道的模型定义。"""
|
|
553
|
+
try:
|
|
554
|
+
result = ctx.auth_mgr.get_model_definitions(channel)
|
|
555
|
+
output_result(result, ctx.json_mode)
|
|
556
|
+
except Exception as e:
|
|
557
|
+
handle_error(ctx, e)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ============================================================
|
|
561
|
+
# oauth 命令组
|
|
562
|
+
# ============================================================
|
|
563
|
+
|
|
564
|
+
@main.group()
|
|
565
|
+
@pass_ctx
|
|
566
|
+
def oauth(ctx: Context):
|
|
567
|
+
"""OAuth 登录管理。"""
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@oauth.command("login")
|
|
571
|
+
@click.argument("provider", type=click.Choice(list(OAuthManager.PROVIDERS.keys())))
|
|
572
|
+
@click.option("--no-browser", is_flag=True, help="不自动打开浏览器")
|
|
573
|
+
@pass_ctx
|
|
574
|
+
def oauth_login(ctx: Context, provider, no_browser):
|
|
575
|
+
"""发起 OAuth 登录。支持: anthropic, codex, gemini, antigravity, qwen, kimi, iflow"""
|
|
576
|
+
try:
|
|
577
|
+
result = ctx.oauth_mgr.request_auth_url(provider, no_browser=no_browser)
|
|
578
|
+
output_result(result, ctx.json_mode)
|
|
579
|
+
except Exception as e:
|
|
580
|
+
handle_error(ctx, e)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@oauth.command("iflow-cookie")
|
|
584
|
+
@click.argument("cookie")
|
|
585
|
+
@pass_ctx
|
|
586
|
+
def oauth_iflow_cookie(ctx: Context, cookie):
|
|
587
|
+
"""使用 Cookie 登录 iFlow。"""
|
|
588
|
+
try:
|
|
589
|
+
result = ctx.oauth_mgr.request_iflow_cookie(cookie)
|
|
590
|
+
output_result(result, ctx.json_mode)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
handle_error(ctx, e)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
@oauth.command("callback")
|
|
596
|
+
@click.option("--provider", required=True, help="提供商名称")
|
|
597
|
+
@click.option("--code", required=True, help="授权码")
|
|
598
|
+
@click.option("--state", required=True, help="状态参数")
|
|
599
|
+
@pass_ctx
|
|
600
|
+
def oauth_callback(ctx: Context, provider, code, state):
|
|
601
|
+
"""处理 OAuth 回调。"""
|
|
602
|
+
try:
|
|
603
|
+
result = ctx.oauth_mgr.post_oauth_callback(provider, code, state)
|
|
604
|
+
output_result(result, ctx.json_mode)
|
|
605
|
+
except Exception as e:
|
|
606
|
+
handle_error(ctx, e)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@oauth.command("status")
|
|
610
|
+
@click.argument("session_id")
|
|
611
|
+
@pass_ctx
|
|
612
|
+
def oauth_status(ctx: Context, session_id):
|
|
613
|
+
"""查看认证会话状态。"""
|
|
614
|
+
try:
|
|
615
|
+
result = ctx.oauth_mgr.get_auth_status(session_id)
|
|
616
|
+
output_result(result, ctx.json_mode)
|
|
617
|
+
except Exception as e:
|
|
618
|
+
handle_error(ctx, e)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# ============================================================
|
|
622
|
+
# keys 命令组
|
|
623
|
+
# ============================================================
|
|
624
|
+
|
|
625
|
+
@main.group()
|
|
626
|
+
@pass_ctx
|
|
627
|
+
def keys(ctx: Context):
|
|
628
|
+
"""API 密钥管理。"""
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# ---- 全局 API 密钥 ----
|
|
632
|
+
|
|
633
|
+
@keys.command("list")
|
|
634
|
+
@pass_ctx
|
|
635
|
+
def keys_list(ctx: Context):
|
|
636
|
+
"""列出 API 密钥。"""
|
|
637
|
+
try:
|
|
638
|
+
result = ctx.key_mgr.list_api_keys()
|
|
639
|
+
output_result(result, ctx.json_mode)
|
|
640
|
+
except Exception as e:
|
|
641
|
+
handle_error(ctx, e)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
@keys.command("set")
|
|
645
|
+
@click.argument("keys_list", nargs=-1, required=True)
|
|
646
|
+
@pass_ctx
|
|
647
|
+
def keys_set(ctx: Context, keys_list):
|
|
648
|
+
"""设置 API 密钥(覆盖)。"""
|
|
649
|
+
try:
|
|
650
|
+
result = ctx.key_mgr.set_api_keys(list(keys_list))
|
|
651
|
+
output_result(result, ctx.json_mode)
|
|
652
|
+
except Exception as e:
|
|
653
|
+
handle_error(ctx, e)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@keys.command("add")
|
|
657
|
+
@click.argument("key")
|
|
658
|
+
@pass_ctx
|
|
659
|
+
def keys_add(ctx: Context, key):
|
|
660
|
+
"""添加一个 API 密钥。"""
|
|
661
|
+
try:
|
|
662
|
+
result = ctx.key_mgr.add_api_key(key)
|
|
663
|
+
output_result(result, ctx.json_mode)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
handle_error(ctx, e)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
@keys.command("delete")
|
|
669
|
+
@click.argument("key")
|
|
670
|
+
@pass_ctx
|
|
671
|
+
def keys_delete(ctx: Context, key):
|
|
672
|
+
"""删除一个 API 密钥。"""
|
|
673
|
+
try:
|
|
674
|
+
result = ctx.key_mgr.delete_api_key(key)
|
|
675
|
+
output_result(result, ctx.json_mode)
|
|
676
|
+
except Exception as e:
|
|
677
|
+
handle_error(ctx, e)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
# ---- Gemini 密钥子命令 ----
|
|
681
|
+
|
|
682
|
+
@keys.group("gemini")
|
|
683
|
+
@pass_ctx
|
|
684
|
+
def keys_gemini(ctx: Context):
|
|
685
|
+
"""Gemini API 密钥管理。"""
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
@keys_gemini.command("list")
|
|
689
|
+
@pass_ctx
|
|
690
|
+
def keys_gemini_list(ctx: Context):
|
|
691
|
+
try:
|
|
692
|
+
result = ctx.key_mgr.list_gemini_keys()
|
|
693
|
+
output_result(result, ctx.json_mode)
|
|
694
|
+
except Exception as e:
|
|
695
|
+
handle_error(ctx, e)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@keys_gemini.command("add")
|
|
699
|
+
@click.option("--api-key", required=True, help="API 密钥")
|
|
700
|
+
@click.option("--prefix", default="", help="前缀")
|
|
701
|
+
@click.option("--base-url", default="", help="基础 URL")
|
|
702
|
+
@click.option("--proxy-url", default="", help="代理 URL")
|
|
703
|
+
@pass_ctx
|
|
704
|
+
def keys_gemini_add(ctx: Context, api_key, prefix, base_url, proxy_url):
|
|
705
|
+
try:
|
|
706
|
+
data = {"api_key": api_key}
|
|
707
|
+
if prefix:
|
|
708
|
+
data["prefix"] = prefix
|
|
709
|
+
if base_url:
|
|
710
|
+
data["base_url"] = base_url
|
|
711
|
+
if proxy_url:
|
|
712
|
+
data["proxy_url"] = proxy_url
|
|
713
|
+
result = ctx.key_mgr.add_gemini_key(data)
|
|
714
|
+
output_result(result, ctx.json_mode)
|
|
715
|
+
except Exception as e:
|
|
716
|
+
handle_error(ctx, e)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
@keys_gemini.command("delete")
|
|
720
|
+
@click.argument("api_key")
|
|
721
|
+
@pass_ctx
|
|
722
|
+
def keys_gemini_delete(ctx: Context, api_key):
|
|
723
|
+
try:
|
|
724
|
+
result = ctx.key_mgr.delete_gemini_key(api_key)
|
|
725
|
+
output_result(result, ctx.json_mode)
|
|
726
|
+
except Exception as e:
|
|
727
|
+
handle_error(ctx, e)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
# ---- Claude 密钥子命令 ----
|
|
731
|
+
|
|
732
|
+
@keys.group("claude")
|
|
733
|
+
@pass_ctx
|
|
734
|
+
def keys_claude(ctx: Context):
|
|
735
|
+
"""Claude API 密钥管理。"""
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@keys_claude.command("list")
|
|
739
|
+
@pass_ctx
|
|
740
|
+
def keys_claude_list(ctx: Context):
|
|
741
|
+
try:
|
|
742
|
+
result = ctx.key_mgr.list_claude_keys()
|
|
743
|
+
output_result(result, ctx.json_mode)
|
|
744
|
+
except Exception as e:
|
|
745
|
+
handle_error(ctx, e)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@keys_claude.command("add")
|
|
749
|
+
@click.option("--api-key", required=True, help="API 密钥")
|
|
750
|
+
@click.option("--prefix", default="", help="前缀")
|
|
751
|
+
@click.option("--base-url", default="", help="基础 URL")
|
|
752
|
+
@click.option("--proxy-url", default="", help="代理 URL")
|
|
753
|
+
@pass_ctx
|
|
754
|
+
def keys_claude_add(ctx: Context, api_key, prefix, base_url, proxy_url):
|
|
755
|
+
try:
|
|
756
|
+
data = {"api_key": api_key}
|
|
757
|
+
if prefix:
|
|
758
|
+
data["prefix"] = prefix
|
|
759
|
+
if base_url:
|
|
760
|
+
data["base_url"] = base_url
|
|
761
|
+
if proxy_url:
|
|
762
|
+
data["proxy_url"] = proxy_url
|
|
763
|
+
result = ctx.key_mgr.add_claude_key(data)
|
|
764
|
+
output_result(result, ctx.json_mode)
|
|
765
|
+
except Exception as e:
|
|
766
|
+
handle_error(ctx, e)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
@keys_claude.command("delete")
|
|
770
|
+
@click.argument("api_key")
|
|
771
|
+
@pass_ctx
|
|
772
|
+
def keys_claude_delete(ctx: Context, api_key):
|
|
773
|
+
try:
|
|
774
|
+
result = ctx.key_mgr.delete_claude_key(api_key)
|
|
775
|
+
output_result(result, ctx.json_mode)
|
|
776
|
+
except Exception as e:
|
|
777
|
+
handle_error(ctx, e)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
# ---- Codex 密钥子命令 ----
|
|
781
|
+
|
|
782
|
+
@keys.group("codex")
|
|
783
|
+
@pass_ctx
|
|
784
|
+
def keys_codex(ctx: Context):
|
|
785
|
+
"""Codex API 密钥管理。"""
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@keys_codex.command("list")
|
|
789
|
+
@pass_ctx
|
|
790
|
+
def keys_codex_list(ctx: Context):
|
|
791
|
+
try:
|
|
792
|
+
result = ctx.key_mgr.list_codex_keys()
|
|
793
|
+
output_result(result, ctx.json_mode)
|
|
794
|
+
except Exception as e:
|
|
795
|
+
handle_error(ctx, e)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
@keys_codex.command("add")
|
|
799
|
+
@click.option("--api-key", required=True, help="API 密钥")
|
|
800
|
+
@click.option("--prefix", default="", help="前缀")
|
|
801
|
+
@click.option("--base-url", default="", help="基础 URL")
|
|
802
|
+
@click.option("--proxy-url", default="", help="代理 URL")
|
|
803
|
+
@pass_ctx
|
|
804
|
+
def keys_codex_add(ctx: Context, api_key, prefix, base_url, proxy_url):
|
|
805
|
+
try:
|
|
806
|
+
data = {"api_key": api_key}
|
|
807
|
+
if prefix:
|
|
808
|
+
data["prefix"] = prefix
|
|
809
|
+
if base_url:
|
|
810
|
+
data["base_url"] = base_url
|
|
811
|
+
if proxy_url:
|
|
812
|
+
data["proxy_url"] = proxy_url
|
|
813
|
+
result = ctx.key_mgr.add_codex_key(data)
|
|
814
|
+
output_result(result, ctx.json_mode)
|
|
815
|
+
except Exception as e:
|
|
816
|
+
handle_error(ctx, e)
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
@keys_codex.command("delete")
|
|
820
|
+
@click.argument("api_key")
|
|
821
|
+
@pass_ctx
|
|
822
|
+
def keys_codex_delete(ctx: Context, api_key):
|
|
823
|
+
try:
|
|
824
|
+
result = ctx.key_mgr.delete_codex_key(api_key)
|
|
825
|
+
output_result(result, ctx.json_mode)
|
|
826
|
+
except Exception as e:
|
|
827
|
+
handle_error(ctx, e)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
# ---- OpenAI 兼容提供商子命令 ----
|
|
831
|
+
|
|
832
|
+
@keys.group("openai-compat")
|
|
833
|
+
@pass_ctx
|
|
834
|
+
def keys_openai_compat(ctx: Context):
|
|
835
|
+
"""OpenAI 兼容提供商管理。"""
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
@keys_openai_compat.command("list")
|
|
839
|
+
@pass_ctx
|
|
840
|
+
def keys_openai_compat_list(ctx: Context):
|
|
841
|
+
try:
|
|
842
|
+
result = ctx.key_mgr.list_openai_compat()
|
|
843
|
+
output_result(result, ctx.json_mode)
|
|
844
|
+
except Exception as e:
|
|
845
|
+
handle_error(ctx, e)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
@keys_openai_compat.command("add")
|
|
849
|
+
@click.option("--name", required=True, help="提供商名称")
|
|
850
|
+
@click.option("--base-url", required=True, help="基础 URL")
|
|
851
|
+
@click.option("--api-key", required=True, help="API 密钥")
|
|
852
|
+
@click.option("--prefix", default="", help="前缀")
|
|
853
|
+
@pass_ctx
|
|
854
|
+
def keys_openai_compat_add(ctx: Context, name, base_url, api_key, prefix):
|
|
855
|
+
try:
|
|
856
|
+
provider = {"name": name, "base_url": base_url, "api_key_entries": [{"api_key": api_key}]}
|
|
857
|
+
if prefix:
|
|
858
|
+
provider["prefix"] = prefix
|
|
859
|
+
result = ctx.key_mgr.add_openai_compat(provider)
|
|
860
|
+
output_result(result, ctx.json_mode)
|
|
861
|
+
except Exception as e:
|
|
862
|
+
handle_error(ctx, e)
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
@keys_openai_compat.command("delete")
|
|
866
|
+
@click.argument("name")
|
|
867
|
+
@pass_ctx
|
|
868
|
+
def keys_openai_compat_delete(ctx: Context, name):
|
|
869
|
+
try:
|
|
870
|
+
result = ctx.key_mgr.delete_openai_compat(name)
|
|
871
|
+
output_result(result, ctx.json_mode)
|
|
872
|
+
except Exception as e:
|
|
873
|
+
handle_error(ctx, e)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
# ---- Vertex 密钥子命令 ----
|
|
877
|
+
|
|
878
|
+
@keys.group("vertex")
|
|
879
|
+
@pass_ctx
|
|
880
|
+
def keys_vertex(ctx: Context):
|
|
881
|
+
"""Vertex API 密钥管理。"""
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
@keys_vertex.command("list")
|
|
885
|
+
@pass_ctx
|
|
886
|
+
def keys_vertex_list(ctx: Context):
|
|
887
|
+
try:
|
|
888
|
+
result = ctx.key_mgr.list_vertex_keys()
|
|
889
|
+
output_result(result, ctx.json_mode)
|
|
890
|
+
except Exception as e:
|
|
891
|
+
handle_error(ctx, e)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
@keys_vertex.command("add")
|
|
895
|
+
@click.option("--api-key", required=True, help="API 密钥")
|
|
896
|
+
@click.option("--base-url", default="", help="基础 URL")
|
|
897
|
+
@click.option("--prefix", default="", help="前缀")
|
|
898
|
+
@pass_ctx
|
|
899
|
+
def keys_vertex_add(ctx: Context, api_key, base_url, prefix):
|
|
900
|
+
try:
|
|
901
|
+
data = {"api_key": api_key}
|
|
902
|
+
if base_url:
|
|
903
|
+
data["base_url"] = base_url
|
|
904
|
+
if prefix:
|
|
905
|
+
data["prefix"] = prefix
|
|
906
|
+
result = ctx.key_mgr.add_vertex_key(data)
|
|
907
|
+
output_result(result, ctx.json_mode)
|
|
908
|
+
except Exception as e:
|
|
909
|
+
handle_error(ctx, e)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
@keys_vertex.command("delete")
|
|
913
|
+
@click.argument("api_key")
|
|
914
|
+
@pass_ctx
|
|
915
|
+
def keys_vertex_delete(ctx: Context, api_key):
|
|
916
|
+
try:
|
|
917
|
+
result = ctx.key_mgr.delete_vertex_key(api_key)
|
|
918
|
+
output_result(result, ctx.json_mode)
|
|
919
|
+
except Exception as e:
|
|
920
|
+
handle_error(ctx, e)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# ============================================================
|
|
924
|
+
# models 命令组
|
|
925
|
+
# ============================================================
|
|
926
|
+
|
|
927
|
+
@main.group()
|
|
928
|
+
@pass_ctx
|
|
929
|
+
def models(ctx: Context):
|
|
930
|
+
"""模型管理。"""
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
@models.command("list")
|
|
934
|
+
@click.option("--api-key", envvar="CPA_API_KEY", help="代理 API 密钥(非管理密钥)")
|
|
935
|
+
@pass_ctx
|
|
936
|
+
def models_list(ctx: Context, api_key):
|
|
937
|
+
"""列出可用模型。"""
|
|
938
|
+
try:
|
|
939
|
+
if not api_key:
|
|
940
|
+
click.echo("需要 --api-key 或设置 CPA_API_KEY 环境变量")
|
|
941
|
+
sys.exit(1)
|
|
942
|
+
result = ctx.model_mgr.list_models(api_key)
|
|
943
|
+
output_result(result, ctx.json_mode)
|
|
944
|
+
except Exception as e:
|
|
945
|
+
handle_error(ctx, e)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
@models.command("aliases")
|
|
949
|
+
@click.option("--set", "alias_data", default=None, help="设置别名的 JSON 数据")
|
|
950
|
+
@click.option("--delete", "delete_alias", nargs=2, type=str, help="删除别名: --delete channel name")
|
|
951
|
+
@pass_ctx
|
|
952
|
+
def models_aliases(ctx: Context, alias_data, delete_alias):
|
|
953
|
+
"""管理 OAuth 模型别名。"""
|
|
954
|
+
try:
|
|
955
|
+
if delete_alias:
|
|
956
|
+
channel, name = delete_alias
|
|
957
|
+
result = ctx.model_mgr.delete_oauth_model_alias(channel, name)
|
|
958
|
+
elif alias_data:
|
|
959
|
+
import json as _json
|
|
960
|
+
data = _json.loads(alias_data)
|
|
961
|
+
result = ctx.model_mgr.put_oauth_model_alias(data)
|
|
962
|
+
else:
|
|
963
|
+
result = ctx.model_mgr.get_oauth_model_alias()
|
|
964
|
+
output_result(result, ctx.json_mode)
|
|
965
|
+
except Exception as e:
|
|
966
|
+
handle_error(ctx, e)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@models.command("excluded")
|
|
970
|
+
@click.option("--set", "excl_data", default=None, help="设置排除模型的 JSON 数据")
|
|
971
|
+
@click.option("--delete", "delete_excl", nargs=2, type=str, help="删除排除: --delete channel model")
|
|
972
|
+
@pass_ctx
|
|
973
|
+
def models_excluded(ctx: Context, excl_data, delete_excl):
|
|
974
|
+
"""管理 OAuth 排除模型。"""
|
|
975
|
+
try:
|
|
976
|
+
if delete_excl:
|
|
977
|
+
channel, model = delete_excl
|
|
978
|
+
result = ctx.model_mgr.delete_oauth_excluded_models(channel, model)
|
|
979
|
+
elif excl_data:
|
|
980
|
+
import json as _json
|
|
981
|
+
data = _json.loads(excl_data)
|
|
982
|
+
result = ctx.model_mgr.put_oauth_excluded_models(data)
|
|
983
|
+
else:
|
|
984
|
+
result = ctx.model_mgr.get_oauth_excluded_models()
|
|
985
|
+
output_result(result, ctx.json_mode)
|
|
986
|
+
except Exception as e:
|
|
987
|
+
handle_error(ctx, e)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
# ============================================================
|
|
991
|
+
# usage 命令组
|
|
992
|
+
# ============================================================
|
|
993
|
+
|
|
994
|
+
@main.group()
|
|
995
|
+
@pass_ctx
|
|
996
|
+
def usage(ctx: Context):
|
|
997
|
+
"""使用统计。"""
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
@usage.command("stats")
|
|
1001
|
+
@pass_ctx
|
|
1002
|
+
def usage_stats(ctx: Context):
|
|
1003
|
+
"""获取使用统计。"""
|
|
1004
|
+
try:
|
|
1005
|
+
result = ctx.usage_mgr.get_stats()
|
|
1006
|
+
output_result(result, ctx.json_mode)
|
|
1007
|
+
except Exception as e:
|
|
1008
|
+
handle_error(ctx, e)
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
@usage.command("export")
|
|
1012
|
+
@pass_ctx
|
|
1013
|
+
def usage_export(ctx: Context):
|
|
1014
|
+
"""导出统计数据。"""
|
|
1015
|
+
try:
|
|
1016
|
+
result = ctx.usage_mgr.export_stats()
|
|
1017
|
+
output_result(result, ctx.json_mode)
|
|
1018
|
+
except Exception as e:
|
|
1019
|
+
handle_error(ctx, e)
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
@usage.command("import")
|
|
1023
|
+
@click.argument("data")
|
|
1024
|
+
@pass_ctx
|
|
1025
|
+
def usage_import(ctx: Context, data):
|
|
1026
|
+
"""导入统计数据。"""
|
|
1027
|
+
try:
|
|
1028
|
+
result = ctx.usage_mgr.import_stats(data)
|
|
1029
|
+
output_result(result, ctx.json_mode)
|
|
1030
|
+
except Exception as e:
|
|
1031
|
+
handle_error(ctx, e)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
# ============================================================
|
|
1035
|
+
# logs 命令组
|
|
1036
|
+
# ============================================================
|
|
1037
|
+
|
|
1038
|
+
@main.group()
|
|
1039
|
+
@pass_ctx
|
|
1040
|
+
def logs(ctx: Context):
|
|
1041
|
+
"""日志管理。"""
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
@logs.command("list")
|
|
1045
|
+
@click.option("--lines", "-n", default=100, help="显示行数")
|
|
1046
|
+
@pass_ctx
|
|
1047
|
+
def logs_list(ctx: Context, lines):
|
|
1048
|
+
"""查看日志。"""
|
|
1049
|
+
try:
|
|
1050
|
+
result = ctx.log_mgr.get_logs(lines)
|
|
1051
|
+
output_result(result, ctx.json_mode)
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
handle_error(ctx, e)
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
@logs.command("clear")
|
|
1057
|
+
@pass_ctx
|
|
1058
|
+
def logs_clear(ctx: Context):
|
|
1059
|
+
"""清除日志。"""
|
|
1060
|
+
try:
|
|
1061
|
+
result = ctx.log_mgr.delete_logs()
|
|
1062
|
+
output_result(result, ctx.json_mode)
|
|
1063
|
+
except Exception as e:
|
|
1064
|
+
handle_error(ctx, e)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
@logs.command("request")
|
|
1068
|
+
@click.argument("value", type=bool, required=False)
|
|
1069
|
+
@pass_ctx
|
|
1070
|
+
def logs_request(ctx: Context, value):
|
|
1071
|
+
"""获取或设置请求日志。"""
|
|
1072
|
+
try:
|
|
1073
|
+
if value is None:
|
|
1074
|
+
result = ctx.log_mgr.get_request_log()
|
|
1075
|
+
else:
|
|
1076
|
+
result = ctx.log_mgr.set_request_log(value)
|
|
1077
|
+
output_result(result, ctx.json_mode)
|
|
1078
|
+
except Exception as e:
|
|
1079
|
+
handle_error(ctx, e)
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
@logs.command("errors")
|
|
1083
|
+
@click.option("--download", "-d", "name", default=None, help="下载指定错误日志文件")
|
|
1084
|
+
@pass_ctx
|
|
1085
|
+
def logs_errors(ctx: Context, name):
|
|
1086
|
+
"""查看或下载错误日志。"""
|
|
1087
|
+
try:
|
|
1088
|
+
if name:
|
|
1089
|
+
result = ctx.log_mgr.download_request_error_log(name)
|
|
1090
|
+
else:
|
|
1091
|
+
result = ctx.log_mgr.get_request_error_logs()
|
|
1092
|
+
output_result(result, ctx.json_mode)
|
|
1093
|
+
except Exception as e:
|
|
1094
|
+
handle_error(ctx, e)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
@logs.command("by-id")
|
|
1098
|
+
@click.argument("log_id")
|
|
1099
|
+
@pass_ctx
|
|
1100
|
+
def logs_by_id(ctx: Context, log_id):
|
|
1101
|
+
"""按 ID 查看请求日志。"""
|
|
1102
|
+
try:
|
|
1103
|
+
result = ctx.log_mgr.get_request_log_by_id(log_id)
|
|
1104
|
+
output_result(result, ctx.json_mode)
|
|
1105
|
+
except Exception as e:
|
|
1106
|
+
handle_error(ctx, e)
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
# ============================================================
|
|
1110
|
+
# amp 命令组
|
|
1111
|
+
# ============================================================
|
|
1112
|
+
|
|
1113
|
+
@main.group()
|
|
1114
|
+
@pass_ctx
|
|
1115
|
+
def amp(ctx: Context):
|
|
1116
|
+
"""Amp CLI 集成管理。"""
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
@amp.command("config")
|
|
1120
|
+
@pass_ctx
|
|
1121
|
+
def amp_config(ctx: Context):
|
|
1122
|
+
"""查看 Amp 配置。"""
|
|
1123
|
+
try:
|
|
1124
|
+
result = ctx.proxy_mgr.get_amp_config()
|
|
1125
|
+
output_result(result, ctx.json_mode)
|
|
1126
|
+
except Exception as e:
|
|
1127
|
+
handle_error(ctx, e)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
@amp.command("upstream-url")
|
|
1131
|
+
@click.argument("value", required=False)
|
|
1132
|
+
@click.option("--delete", is_flag=True, help="删除上游 URL")
|
|
1133
|
+
@pass_ctx
|
|
1134
|
+
def amp_upstream_url(ctx: Context, value, delete):
|
|
1135
|
+
"""获取、设置或删除 Amp 上游 URL。"""
|
|
1136
|
+
try:
|
|
1137
|
+
if delete:
|
|
1138
|
+
result = ctx.proxy_mgr.delete_amp_upstream_url()
|
|
1139
|
+
elif value:
|
|
1140
|
+
result = ctx.proxy_mgr.set_amp_upstream_url(value)
|
|
1141
|
+
else:
|
|
1142
|
+
result = ctx.proxy_mgr.get_amp_upstream_url()
|
|
1143
|
+
output_result(result, ctx.json_mode)
|
|
1144
|
+
except Exception as e:
|
|
1145
|
+
handle_error(ctx, e)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
@amp.command("upstream-api-key")
|
|
1149
|
+
@click.argument("value", required=False)
|
|
1150
|
+
@click.option("--delete", is_flag=True, help="删除上游 API 密钥")
|
|
1151
|
+
@pass_ctx
|
|
1152
|
+
def amp_upstream_api_key(ctx: Context, value, delete):
|
|
1153
|
+
"""获取、设置或删除 Amp 上游 API 密钥。"""
|
|
1154
|
+
try:
|
|
1155
|
+
if delete:
|
|
1156
|
+
result = ctx.proxy_mgr.delete_amp_upstream_api_key()
|
|
1157
|
+
elif value:
|
|
1158
|
+
result = ctx.proxy_mgr.set_amp_upstream_api_key(value)
|
|
1159
|
+
else:
|
|
1160
|
+
result = ctx.proxy_mgr.get_amp_upstream_api_key()
|
|
1161
|
+
output_result(result, ctx.json_mode)
|
|
1162
|
+
except Exception as e:
|
|
1163
|
+
handle_error(ctx, e)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
@amp.command("model-mappings")
|
|
1167
|
+
@click.option("--set", "mappings_json", default=None, help="设置映射的 JSON")
|
|
1168
|
+
@click.option("--add", "add_json", default=None, help="添加一条映射的 JSON")
|
|
1169
|
+
@click.option("--delete", "delete_from", default=None, help="删除指定 from 模型的映射")
|
|
1170
|
+
@pass_ctx
|
|
1171
|
+
def amp_model_mappings(ctx: Context, mappings_json, add_json, delete_from):
|
|
1172
|
+
"""管理 Amp 模型映射。"""
|
|
1173
|
+
try:
|
|
1174
|
+
if delete_from:
|
|
1175
|
+
result = ctx.proxy_mgr.delete_amp_model_mappings(delete_from)
|
|
1176
|
+
elif mappings_json:
|
|
1177
|
+
import json as _json
|
|
1178
|
+
data = _json.loads(mappings_json)
|
|
1179
|
+
result = ctx.proxy_mgr.set_amp_model_mappings(data)
|
|
1180
|
+
elif add_json:
|
|
1181
|
+
import json as _json
|
|
1182
|
+
data = _json.loads(add_json)
|
|
1183
|
+
result = ctx.proxy_mgr.patch_amp_model_mappings(data)
|
|
1184
|
+
else:
|
|
1185
|
+
result = ctx.proxy_mgr.get_amp_model_mappings()
|
|
1186
|
+
output_result(result, ctx.json_mode)
|
|
1187
|
+
except Exception as e:
|
|
1188
|
+
handle_error(ctx, e)
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
@amp.command("force-model-mappings")
|
|
1192
|
+
@click.argument("value", type=bool, required=False)
|
|
1193
|
+
@pass_ctx
|
|
1194
|
+
def amp_force_model_mappings(ctx: Context, value):
|
|
1195
|
+
"""获取或设置强制模型映射。"""
|
|
1196
|
+
try:
|
|
1197
|
+
if value is None:
|
|
1198
|
+
result = ctx.proxy_mgr.get_amp_force_model_mappings()
|
|
1199
|
+
else:
|
|
1200
|
+
result = ctx.proxy_mgr.set_amp_force_model_mappings(value)
|
|
1201
|
+
output_result(result, ctx.json_mode)
|
|
1202
|
+
except Exception as e:
|
|
1203
|
+
handle_error(ctx, e)
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
# ============================================================
|
|
1207
|
+
# api-call 命令
|
|
1208
|
+
# ============================================================
|
|
1209
|
+
|
|
1210
|
+
@main.command("api-call")
|
|
1211
|
+
@click.option("--method", "-X", default="GET", help="HTTP 方法")
|
|
1212
|
+
@click.option("--url", required=True, help="请求 URL")
|
|
1213
|
+
@click.option("--header", "-H", multiple=True, help="请求头 (key:value)")
|
|
1214
|
+
@click.option("--data", "-d", default=None, help="请求体")
|
|
1215
|
+
@click.option("--auth-index", default=None, help="认证索引")
|
|
1216
|
+
@pass_ctx
|
|
1217
|
+
def api_call(ctx: Context, method, url, header, data, auth_index):
|
|
1218
|
+
"""通过代理发起 HTTP 请求。"""
|
|
1219
|
+
try:
|
|
1220
|
+
headers = {}
|
|
1221
|
+
for h in header:
|
|
1222
|
+
if ":" in h:
|
|
1223
|
+
k, v = h.split(":", 1)
|
|
1224
|
+
headers[k.strip()] = v.strip()
|
|
1225
|
+
result = ctx.proxy_mgr.api_call(
|
|
1226
|
+
method=method,
|
|
1227
|
+
url=url,
|
|
1228
|
+
headers=headers or None,
|
|
1229
|
+
data=data,
|
|
1230
|
+
auth_index=auth_index,
|
|
1231
|
+
)
|
|
1232
|
+
output_result(result, ctx.json_mode)
|
|
1233
|
+
except Exception as e:
|
|
1234
|
+
handle_error(ctx, e)
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
if __name__ == "__main__":
|
|
1238
|
+
main()
|