claude-switch 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.
- claude_switch/__init__.py +14 -0
- claude_switch/cli.py +694 -0
- claude_switch/config.py +529 -0
- claude_switch/core.py +189 -0
- claude_switch-1.0.0.dist-info/METADATA +290 -0
- claude_switch-1.0.0.dist-info/RECORD +9 -0
- claude_switch-1.0.0.dist-info/WHEEL +4 -0
- claude_switch-1.0.0.dist-info/entry_points.txt +3 -0
- claude_switch-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Switch - Anthropic Claude API Configuration Management Tool
|
|
3
|
+
|
|
4
|
+
A Python CLI tool for managing multiple Anthropic Claude API configurations
|
|
5
|
+
with seamless environment switching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
__author__ = "Claude"
|
|
10
|
+
__email__ = "noreply@anthropic.com"
|
|
11
|
+
|
|
12
|
+
from .cli import cli
|
|
13
|
+
|
|
14
|
+
__all__ = ["cli"]
|
claude_switch/cli.py
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import click
|
|
3
|
+
import questionary
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
from .config import ConfigManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group(invoke_without_command=True)
|
|
9
|
+
@click.option('--version', is_flag=True, help='显示版本信息')
|
|
10
|
+
@click.pass_context
|
|
11
|
+
def cli(ctx, version):
|
|
12
|
+
"""Claude Switch - Anthropic Claude API 配置管理工具,支持多环境切换"""
|
|
13
|
+
if version:
|
|
14
|
+
click.echo("claude-switch 1.0.0")
|
|
15
|
+
click.echo("Anthropic Claude API 配置管理工具")
|
|
16
|
+
click.echo("GitHub: https://github.com/your-username/claude-switch")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
if ctx.invoked_subcommand is None:
|
|
20
|
+
interactive_select()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cli.command()
|
|
24
|
+
def list():
|
|
25
|
+
"""列出所有配置"""
|
|
26
|
+
cm = ConfigManager()
|
|
27
|
+
configs = cm.get_configs()
|
|
28
|
+
current = cm.get_current_config_name()
|
|
29
|
+
|
|
30
|
+
if not configs:
|
|
31
|
+
click.echo("暂无配置")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
for name, config in configs.items():
|
|
35
|
+
prefix = "→ " if name == current else " "
|
|
36
|
+
display = format_config_display(name, config)
|
|
37
|
+
# 为当前激活的配置添加背景色高亮
|
|
38
|
+
if name == current:
|
|
39
|
+
# 使用 ANSI 颜色码添加绿色背景
|
|
40
|
+
click.echo(f"\033[42m\033[30m{prefix}{display}\033[0m")
|
|
41
|
+
else:
|
|
42
|
+
click.echo(f"{prefix}{display}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@cli.command()
|
|
46
|
+
@click.argument('name')
|
|
47
|
+
def use(name):
|
|
48
|
+
"""切换到指定配置"""
|
|
49
|
+
cm = ConfigManager()
|
|
50
|
+
if cm.set_active_config(name):
|
|
51
|
+
config = cm.get_config(name)
|
|
52
|
+
if config and (config["api_keys"] or config["auth_tokens"]):
|
|
53
|
+
cm.update_env_file()
|
|
54
|
+
display = format_config_display(name, config)
|
|
55
|
+
click.echo(f"已切换到: {display}")
|
|
56
|
+
else:
|
|
57
|
+
click.echo(f"配置 '{name}' 没有可用的凭据")
|
|
58
|
+
else:
|
|
59
|
+
click.echo(f"配置 '{name}' 不存在", err=True)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@cli.command()
|
|
64
|
+
@click.option('--export', is_flag=True, help='输出可直接执行的export命令')
|
|
65
|
+
def current(export):
|
|
66
|
+
"""显示当前配置"""
|
|
67
|
+
cm = ConfigManager()
|
|
68
|
+
current_name = cm.get_current_config_name()
|
|
69
|
+
if current_name:
|
|
70
|
+
config = cm.get_config(current_name)
|
|
71
|
+
if config:
|
|
72
|
+
if export:
|
|
73
|
+
export_cmd = cm.print_current_export()
|
|
74
|
+
click.echo(export_cmd)
|
|
75
|
+
else:
|
|
76
|
+
display = format_config_display(current_name, config)
|
|
77
|
+
click.echo(f"当前配置: {display}")
|
|
78
|
+
else:
|
|
79
|
+
click.echo("暂无激活配置")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@cli.command()
|
|
83
|
+
def add():
|
|
84
|
+
"""添加新配置(交互式)"""
|
|
85
|
+
cm = ConfigManager()
|
|
86
|
+
|
|
87
|
+
name = click.prompt("配置名称")
|
|
88
|
+
if cm.get_config(name):
|
|
89
|
+
click.echo(f"配置 '{name}' 已存在", err=True)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
base_url = click.prompt("Base URL", default="https://api.anthropic.com")
|
|
93
|
+
note = click.prompt("备注说明", default=name, show_default=False)
|
|
94
|
+
|
|
95
|
+
if cm.add_config(name, base_url, note):
|
|
96
|
+
click.echo(f"已添加配置: {name}")
|
|
97
|
+
|
|
98
|
+
# 询问是否添加凭据
|
|
99
|
+
if click.confirm("是否添加凭据?", default=True):
|
|
100
|
+
add_credential_interactive(cm, name)
|
|
101
|
+
else:
|
|
102
|
+
click.echo("添加失败", err=True)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@cli.command()
|
|
106
|
+
@click.argument('name')
|
|
107
|
+
def edit(name):
|
|
108
|
+
"""编辑配置"""
|
|
109
|
+
cm = ConfigManager()
|
|
110
|
+
config = cm.get_config(name)
|
|
111
|
+
if not config:
|
|
112
|
+
click.echo(f"配置 '{name}' 不存在", err=True)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
click.echo(f"正在编辑配置: {name}")
|
|
116
|
+
|
|
117
|
+
# 基础信息
|
|
118
|
+
base_url = click.prompt("Base URL", default=config['base_url'])
|
|
119
|
+
note = click.prompt("备注说明", default=config['note'])
|
|
120
|
+
|
|
121
|
+
if cm.get_config(name):
|
|
122
|
+
# 更新基础信息
|
|
123
|
+
config['base_url'] = base_url
|
|
124
|
+
config['note'] = note
|
|
125
|
+
cm._save_config(cm._load_config())
|
|
126
|
+
click.echo(f"已更新配置: {name}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@cli.command()
|
|
130
|
+
@click.argument('name', required=False)
|
|
131
|
+
def remove(name):
|
|
132
|
+
"""删除配置或凭据(交互式)"""
|
|
133
|
+
cm = ConfigManager()
|
|
134
|
+
|
|
135
|
+
if name:
|
|
136
|
+
# 保持向后兼容:直接删除指定配置
|
|
137
|
+
if click.confirm(f'确定要删除配置 "{name}" 吗?'):
|
|
138
|
+
if cm.remove_config(name):
|
|
139
|
+
click.echo(f"已删除配置: {name}")
|
|
140
|
+
else:
|
|
141
|
+
click.echo(f"删除失败或配置 '{name}' 不存在", err=True)
|
|
142
|
+
else:
|
|
143
|
+
# 无参数时进入交互式删除模式
|
|
144
|
+
interactive_remove()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@cli.command()
|
|
148
|
+
def init():
|
|
149
|
+
"""初始化shell集成(自动生效)"""
|
|
150
|
+
cm = ConfigManager()
|
|
151
|
+
|
|
152
|
+
click.echo("🔧 正在初始化 claude-switch shell 集成...")
|
|
153
|
+
click.echo("")
|
|
154
|
+
|
|
155
|
+
result = cm.init_shell()
|
|
156
|
+
click.echo(result)
|
|
157
|
+
|
|
158
|
+
# 如果初始化成功,检查是否需要添加配置
|
|
159
|
+
configs = cm.get_configs()
|
|
160
|
+
if not configs or len(configs) == 0:
|
|
161
|
+
click.echo("")
|
|
162
|
+
click.echo("🎯 检测到尚无配置,建议立即添加一个配置:")
|
|
163
|
+
if click.confirm("是否现在添加第一个配置?", default=True):
|
|
164
|
+
# 调用添加配置的交互式流程
|
|
165
|
+
add_first_config_interactive(cm)
|
|
166
|
+
else:
|
|
167
|
+
click.echo("")
|
|
168
|
+
click.echo(f"📋 当前已有 {len(configs)} 个配置,可以直接使用 'cs' 命令选择")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def add_first_config_interactive(cm: ConfigManager):
|
|
172
|
+
"""添加第一个配置的交互式流程"""
|
|
173
|
+
click.echo("")
|
|
174
|
+
click.echo("📝 添加第一个配置:")
|
|
175
|
+
|
|
176
|
+
name = click.prompt("配置名称", default="default")
|
|
177
|
+
base_url = click.prompt("Base URL", default="https://api.anthropic.com")
|
|
178
|
+
note = click.prompt("备注说明", default=name, show_default=False)
|
|
179
|
+
|
|
180
|
+
if cm.add_config(name, base_url, note):
|
|
181
|
+
click.echo(f"✅ 已添加配置: {name}")
|
|
182
|
+
|
|
183
|
+
# 询问是否添加凭据
|
|
184
|
+
if click.confirm("是否添加凭据(API Key 或 Auth Token)?", default=True):
|
|
185
|
+
add_credential_interactive(cm, name)
|
|
186
|
+
|
|
187
|
+
# 自动激活刚添加的配置
|
|
188
|
+
if cm.set_active_config(name):
|
|
189
|
+
cm.update_env_file()
|
|
190
|
+
click.echo(f"✅ 已激活配置: {name}")
|
|
191
|
+
|
|
192
|
+
click.echo("")
|
|
193
|
+
click.echo("🎉 初始化完成!现在可以使用以下命令:")
|
|
194
|
+
click.echo(" cs # 交互式选择配置")
|
|
195
|
+
click.echo(" cs list # 查看所有配置")
|
|
196
|
+
click.echo(" cs current # 查看当前配置")
|
|
197
|
+
click.echo(" cs --help # 查看更多命令")
|
|
198
|
+
else:
|
|
199
|
+
click.echo("❌ 添加配置失败", err=True)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@cli.command()
|
|
203
|
+
@click.argument('config_name')
|
|
204
|
+
def add_credential(config_name):
|
|
205
|
+
"""添加凭据到配置"""
|
|
206
|
+
cm = ConfigManager()
|
|
207
|
+
if not cm.get_config(config_name):
|
|
208
|
+
click.echo(f"配置 '{config_name}' 不存在", err=True)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
add_credential_interactive(cm, config_name)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def interactive_select():
|
|
215
|
+
"""交互式选择配置(支持箭头键)"""
|
|
216
|
+
cm = ConfigManager()
|
|
217
|
+
configs = cm.get_configs()
|
|
218
|
+
current = cm.get_current_config_name()
|
|
219
|
+
|
|
220
|
+
if not configs:
|
|
221
|
+
click.echo("暂无配置,请使用 'cs add' 添加")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# 准备选择列表
|
|
225
|
+
choices = []
|
|
226
|
+
for name, config in configs.items():
|
|
227
|
+
display = format_config_display(name, config)
|
|
228
|
+
prefix = "→ " if name == current else " "
|
|
229
|
+
# 为当前激活的配置添加特殊标记,用于自定义样式
|
|
230
|
+
if name == current:
|
|
231
|
+
choice_text = f"✅ {display}" # 使用勾号代替箭头,更清晰
|
|
232
|
+
else:
|
|
233
|
+
choice_text = f" {display}"
|
|
234
|
+
choices.append(questionary.Choice(
|
|
235
|
+
choice_text,
|
|
236
|
+
value=name
|
|
237
|
+
))
|
|
238
|
+
|
|
239
|
+
# 使用箭头键选择
|
|
240
|
+
click.echo("\n使用↑↓箭头键浏览,回车键确认选择,ESC键退出")
|
|
241
|
+
|
|
242
|
+
selected = _safe_questionary_select(
|
|
243
|
+
"选择配置:",
|
|
244
|
+
choices=choices,
|
|
245
|
+
default=current
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not selected:
|
|
249
|
+
# 用户按ESC或取消了选择,直接退出
|
|
250
|
+
click.echo("\n✗ 已取消配置选择")
|
|
251
|
+
sys.exit(2) # 设置退出码为2,表示用户取消,防止shell包装器显示成功提示
|
|
252
|
+
|
|
253
|
+
if selected:
|
|
254
|
+
# 保存原始配置状态,以便在取消时恢复
|
|
255
|
+
original_config = cm.get_current_config_name()
|
|
256
|
+
|
|
257
|
+
config = cm.get_config(selected)
|
|
258
|
+
if config:
|
|
259
|
+
# 先不切换配置,先让用户选择凭据
|
|
260
|
+
if len(config["api_keys"]) + len(config["auth_tokens"]) > 1:
|
|
261
|
+
click.echo(f"\n即将切换到配置: {selected}")
|
|
262
|
+
click.echo("请选择要使用的凭据:")
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
credential_selected = select_credential_interactive(
|
|
266
|
+
cm, selected)
|
|
267
|
+
|
|
268
|
+
if credential_selected is not None:
|
|
269
|
+
# 用户确认了凭据选择,现在执行完整的切换
|
|
270
|
+
if cm.set_active_config(selected):
|
|
271
|
+
credential_type, index = credential_selected
|
|
272
|
+
cm.set_active_credential(
|
|
273
|
+
selected, credential_type, index)
|
|
274
|
+
cm.update_env_file()
|
|
275
|
+
|
|
276
|
+
display = format_config_display(selected, config)
|
|
277
|
+
click.echo(f"\n✓ 已完成切换到: {display}")
|
|
278
|
+
else:
|
|
279
|
+
click.echo("✗ 配置切换失败", err=True)
|
|
280
|
+
else:
|
|
281
|
+
# 用户取消了凭据选择,不执行配置切换
|
|
282
|
+
click.echo(f"\n✗ 已取消切换到配置: {selected}")
|
|
283
|
+
click.echo(f"保持当前配置: {original_config}")
|
|
284
|
+
|
|
285
|
+
except KeyboardInterrupt:
|
|
286
|
+
# Ctrl+C 中断,不执行任何切换
|
|
287
|
+
click.echo(f"\n\n✗ 已取消切换到配置: {selected}")
|
|
288
|
+
click.echo(f"保持当前配置: {original_config}")
|
|
289
|
+
sys.exit(2) # 设置退出码为2,表示用户取消,防止shell包装器显示成功提示
|
|
290
|
+
else:
|
|
291
|
+
# 只有一个凭据,直接切换
|
|
292
|
+
if cm.set_active_config(selected):
|
|
293
|
+
cm.update_env_file()
|
|
294
|
+
display = format_config_display(selected, config)
|
|
295
|
+
click.echo(f"已切换到: {display}")
|
|
296
|
+
else:
|
|
297
|
+
click.echo("配置切换失败", err=True)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _build_credential_choices(config: Dict[str, Any], active_label: str = "默认激活") -> List[questionary.Choice]:
|
|
301
|
+
"""构建凭据选择列表的公共函数"""
|
|
302
|
+
choices = []
|
|
303
|
+
|
|
304
|
+
# 添加auth tokens
|
|
305
|
+
for i, token in enumerate(config["auth_tokens"]):
|
|
306
|
+
is_active = config["active_auth"] == i
|
|
307
|
+
masked, display_name = parse_credential_with_name(token)
|
|
308
|
+
display_text = f"{masked} ({display_name})" if display_name else masked
|
|
309
|
+
|
|
310
|
+
if is_active:
|
|
311
|
+
choice_text = f"✅ Auth Token {i+1}: {display_text} [{active_label}]"
|
|
312
|
+
else:
|
|
313
|
+
choice_text = f" Auth Token {i+1}: {display_text}"
|
|
314
|
+
|
|
315
|
+
choices.append(questionary.Choice(
|
|
316
|
+
choice_text,
|
|
317
|
+
value=("auth_token", i)
|
|
318
|
+
))
|
|
319
|
+
|
|
320
|
+
# 添加api keys
|
|
321
|
+
for i, key in enumerate(config["api_keys"]):
|
|
322
|
+
is_active = config["active_key"] == i
|
|
323
|
+
masked, display_name = parse_credential_with_name(key)
|
|
324
|
+
display_text = f"{masked} ({display_name})" if display_name else masked
|
|
325
|
+
|
|
326
|
+
if is_active:
|
|
327
|
+
choice_text = f"✅ API Key {i+1}: {display_text} [{active_label}]"
|
|
328
|
+
else:
|
|
329
|
+
choice_text = f" API Key {i+1}: {display_text}"
|
|
330
|
+
|
|
331
|
+
choices.append(questionary.Choice(
|
|
332
|
+
choice_text,
|
|
333
|
+
value=("api_key", i)
|
|
334
|
+
))
|
|
335
|
+
|
|
336
|
+
return choices
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _get_questionary_style() -> questionary.Style:
|
|
341
|
+
"""获取统一的 questionary 样式"""
|
|
342
|
+
return questionary.Style([
|
|
343
|
+
('qmark', 'fg:#673ab7 bold'),
|
|
344
|
+
('question', 'bold'),
|
|
345
|
+
('answer', 'fg:#f44336 bold'),
|
|
346
|
+
('pointer', 'fg:#00aa00 bold'),
|
|
347
|
+
('highlighted', 'fg:#ffffff bold bg:#006600'),
|
|
348
|
+
('selected', 'fg:#cc5454'),
|
|
349
|
+
('separator', 'fg:#cc5454'),
|
|
350
|
+
('instruction', ''),
|
|
351
|
+
('text', ''),
|
|
352
|
+
('disabled', 'fg:#858585 italic')
|
|
353
|
+
])
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _safe_questionary_select(message: str, choices: List[questionary.Choice],
|
|
357
|
+
default=None, style=None) -> Optional[str]:
|
|
358
|
+
"""安全的 questionary select,确保ESC键正常工作"""
|
|
359
|
+
try:
|
|
360
|
+
result = questionary.select(
|
|
361
|
+
message,
|
|
362
|
+
choices=choices,
|
|
363
|
+
default=default,
|
|
364
|
+
style=style or _get_questionary_style()
|
|
365
|
+
).ask()
|
|
366
|
+
|
|
367
|
+
# questionary 在ESC时可能返回None或空字符串
|
|
368
|
+
return result if result else None
|
|
369
|
+
|
|
370
|
+
except (KeyboardInterrupt, EOFError, Exception):
|
|
371
|
+
# 处理各种中断情况:
|
|
372
|
+
# - KeyboardInterrupt: Ctrl+C 或某些情况下的ESC键
|
|
373
|
+
# - EOFError: 输入流结束
|
|
374
|
+
# - Exception: 其他可能的异常(如某些终端环境下的ESC键处理)
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def select_credential_interactive(cm: ConfigManager, config_name: str):
|
|
379
|
+
"""交互式选择凭据(预览模式,用于配置切换)"""
|
|
380
|
+
config = cm.get_config(config_name)
|
|
381
|
+
if not config or (len(config["api_keys"]) + len(config["auth_tokens"]) <= 1):
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
choices = _build_credential_choices(config, "默认激活")
|
|
385
|
+
|
|
386
|
+
if choices:
|
|
387
|
+
click.echo("\n使用↑↓箭头键浏览,回车键确认选择,ESC键退出")
|
|
388
|
+
click.echo("⚠️ 注意: 只有按回车键确认选择才会执行配置切换")
|
|
389
|
+
|
|
390
|
+
selected = _safe_questionary_select(
|
|
391
|
+
f"选择 {config_name} 的凭据:",
|
|
392
|
+
choices=choices
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
return selected # 返回选择结果,不直接修改配置
|
|
396
|
+
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def add_credential_interactive(cm: ConfigManager, config_name: str):
|
|
401
|
+
"""交互式添加凭据"""
|
|
402
|
+
config = cm.get_config(config_name)
|
|
403
|
+
if not config:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
default_credential_type = None
|
|
407
|
+
|
|
408
|
+
while True:
|
|
409
|
+
click.echo("\n使用↑↓箭头键浏览,回车键确认选择,ESC键退出")
|
|
410
|
+
|
|
411
|
+
credential_type = _safe_questionary_select(
|
|
412
|
+
"选择凭据类型:",
|
|
413
|
+
choices=[
|
|
414
|
+
questionary.Choice("Auth Token", value="auth_token"),
|
|
415
|
+
questionary.Choice("API Key", value="api_key")
|
|
416
|
+
],
|
|
417
|
+
default=default_credential_type
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if not credential_type:
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
# 记住当前选择的类型,用于下次默认
|
|
424
|
+
default_credential_type = credential_type
|
|
425
|
+
|
|
426
|
+
value = click.prompt(f"输入 {credential_type}")
|
|
427
|
+
if value:
|
|
428
|
+
cm.add_credential(config_name, credential_type, value)
|
|
429
|
+
click.echo(f"已添加 {credential_type}")
|
|
430
|
+
|
|
431
|
+
if not click.confirm("继续添加更多凭据?", default=True):
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def format_config_display(name: str, config: Dict[str, Any]) -> str:
|
|
436
|
+
"""格式化配置显示"""
|
|
437
|
+
base_url = config["base_url"]
|
|
438
|
+
note = config["note"]
|
|
439
|
+
|
|
440
|
+
# 计算凭据数量
|
|
441
|
+
key_count = len(config["api_keys"])
|
|
442
|
+
token_count = len(config["auth_tokens"])
|
|
443
|
+
|
|
444
|
+
# 显示激活的凭据,包括名称支持
|
|
445
|
+
active_display = ""
|
|
446
|
+
if config["active_auth"] >= 0 and config["active_auth"] < token_count:
|
|
447
|
+
token_value = config["auth_tokens"][config["active_auth"]]
|
|
448
|
+
if '|' in token_value:
|
|
449
|
+
_, name_part = token_value.split('|', 1)
|
|
450
|
+
active_display = f"token{config['active_auth']+1}({name_part})"
|
|
451
|
+
else:
|
|
452
|
+
active_display = f"token{config['active_auth']+1}"
|
|
453
|
+
elif config["active_key"] >= 0 and config["active_key"] < key_count:
|
|
454
|
+
key_value = config["api_keys"][config["active_key"]]
|
|
455
|
+
if '|' in key_value:
|
|
456
|
+
_, name_part = key_value.split('|', 1)
|
|
457
|
+
active_display = f"key{config['active_key']+1}({name_part})"
|
|
458
|
+
else:
|
|
459
|
+
active_display = f"key{config['active_key']+1}"
|
|
460
|
+
else:
|
|
461
|
+
active_display = "未设置"
|
|
462
|
+
|
|
463
|
+
credentials_info = f"({token_count}tokens, {key_count}keys) [{active_display}]"
|
|
464
|
+
|
|
465
|
+
return f"{name} - {note} [{base_url}] {credentials_info}"
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def parse_credential_with_name(value: str) -> tuple[str, str]:
|
|
469
|
+
"""解析带名称的凭据,返回(masked_credential, name)"""
|
|
470
|
+
if '|' in value:
|
|
471
|
+
credential, name = value.split('|', 1)
|
|
472
|
+
return mask_credential(credential), name
|
|
473
|
+
else:
|
|
474
|
+
return mask_credential(value), ""
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def mask_credential(value: str) -> str:
|
|
478
|
+
"""掩码显示凭据"""
|
|
479
|
+
if len(value) <= 8:
|
|
480
|
+
return "***"
|
|
481
|
+
return f"{value[:4]}...{value[-4:]}"
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def interactive_remove():
|
|
485
|
+
"""交互式删除主流程"""
|
|
486
|
+
cm = ConfigManager()
|
|
487
|
+
configs = cm.get_configs()
|
|
488
|
+
|
|
489
|
+
if not configs:
|
|
490
|
+
click.echo("暂无配置可删除")
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
if len(configs) <= 1:
|
|
494
|
+
click.echo("至少需要保留一个配置,无法删除")
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
# 第一步:选择配置
|
|
498
|
+
selected_config = select_config_for_removal(cm, configs)
|
|
499
|
+
if not selected_config:
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
# 第二步:选择操作类型
|
|
503
|
+
while True:
|
|
504
|
+
action = select_removal_action(selected_config)
|
|
505
|
+
if not action:
|
|
506
|
+
# ESC返回配置选择
|
|
507
|
+
selected_config = select_config_for_removal(cm, configs)
|
|
508
|
+
if not selected_config:
|
|
509
|
+
return
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
if action == "delete_config":
|
|
513
|
+
# 删除整个配置
|
|
514
|
+
if confirm_and_remove_config(cm, selected_config):
|
|
515
|
+
return
|
|
516
|
+
# 如果取消删除,返回操作选择
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
elif action == "delete_credential":
|
|
520
|
+
# 删除凭据
|
|
521
|
+
config = cm.get_config(selected_config)
|
|
522
|
+
if not config or (len(config["api_keys"]) + len(config["auth_tokens"])) == 0:
|
|
523
|
+
click.echo(f"配置 '{selected_config}' 没有凭据可删除")
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
result = remove_credential_interactive(cm, selected_config)
|
|
527
|
+
if result == "back":
|
|
528
|
+
# 返回操作选择
|
|
529
|
+
continue
|
|
530
|
+
elif result == "deleted":
|
|
531
|
+
# 删除成功,询问是否继续
|
|
532
|
+
if not click.confirm("是否继续删除其他项目?", default=True):
|
|
533
|
+
return
|
|
534
|
+
continue
|
|
535
|
+
else:
|
|
536
|
+
# 取消或其他情况,返回配置选择
|
|
537
|
+
selected_config = select_config_for_removal(cm, configs)
|
|
538
|
+
if not selected_config:
|
|
539
|
+
return
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def select_config_for_removal(cm: ConfigManager, configs: Dict[str, Dict[str, Any]]) -> Optional[str]:
|
|
544
|
+
"""选择要删除的配置"""
|
|
545
|
+
current = cm.get_current_config_name()
|
|
546
|
+
|
|
547
|
+
choices = []
|
|
548
|
+
for name, config in configs.items():
|
|
549
|
+
display = format_config_display(name, config)
|
|
550
|
+
if name == current:
|
|
551
|
+
choice_text = f"✅ {display}"
|
|
552
|
+
else:
|
|
553
|
+
choice_text = f" {display}"
|
|
554
|
+
choices.append(questionary.Choice(choice_text, value=name))
|
|
555
|
+
|
|
556
|
+
click.echo("\n使用↑↓箭头键浏览,回车键确认选择,ESC键退出")
|
|
557
|
+
|
|
558
|
+
selected = _safe_questionary_select(
|
|
559
|
+
"选择要操作的配置:",
|
|
560
|
+
choices=choices
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if not selected:
|
|
564
|
+
click.echo("\n✗ 已退出删除操作")
|
|
565
|
+
|
|
566
|
+
return selected
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def select_removal_action(config_name: str) -> Optional[str]:
|
|
570
|
+
"""选择删除操作类型"""
|
|
571
|
+
choices = [
|
|
572
|
+
questionary.Choice(f"🗑️ 删除整个配置 \"{config_name}\"", value="delete_config"),
|
|
573
|
+
questionary.Choice("🔑 删除配置中的凭据", value="delete_credential")
|
|
574
|
+
]
|
|
575
|
+
|
|
576
|
+
click.echo("\n使用↑↓箭头键浏览,回车键确认选择,ESC键返回配置选择")
|
|
577
|
+
|
|
578
|
+
selected = _safe_questionary_select(
|
|
579
|
+
f"选择对配置 \"{config_name}\" 的操作:",
|
|
580
|
+
choices=choices
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
return selected # 返回None表示ESC,调用方处理返回上一步
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def remove_credential_interactive(cm: ConfigManager, config_name: str) -> str:
|
|
587
|
+
"""交互式删除凭据"""
|
|
588
|
+
config = cm.get_config(config_name)
|
|
589
|
+
if not config:
|
|
590
|
+
return "error"
|
|
591
|
+
|
|
592
|
+
choices = _build_credential_choices(config, "当前激活")
|
|
593
|
+
|
|
594
|
+
if not choices:
|
|
595
|
+
click.echo(f"配置 '{config_name}' 没有凭据可删除")
|
|
596
|
+
return "back"
|
|
597
|
+
|
|
598
|
+
click.echo("\n使用↑↓箭头键浏览,回车键确认选择,ESC键返回操作选择")
|
|
599
|
+
|
|
600
|
+
selected = _safe_questionary_select(
|
|
601
|
+
f"选择要删除的凭据 (配置: {config_name}):",
|
|
602
|
+
choices=choices
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
if selected:
|
|
606
|
+
credential_type, index = selected
|
|
607
|
+
|
|
608
|
+
# 显示要删除的凭据信息
|
|
609
|
+
if credential_type == "auth_token":
|
|
610
|
+
credential_value = config["auth_tokens"][index]
|
|
611
|
+
type_display = "Auth Token"
|
|
612
|
+
else:
|
|
613
|
+
credential_value = config["api_keys"][index]
|
|
614
|
+
type_display = "API Key"
|
|
615
|
+
|
|
616
|
+
masked, name = parse_credential_with_name(credential_value)
|
|
617
|
+
display_text = f"{masked} ({name})" if name else masked
|
|
618
|
+
|
|
619
|
+
# 检查是否是当前激活的凭据
|
|
620
|
+
is_active = False
|
|
621
|
+
if credential_type == "auth_token" and config["active_auth"] == index:
|
|
622
|
+
is_active = True
|
|
623
|
+
elif credential_type == "api_key" and config["active_key"] == index:
|
|
624
|
+
is_active = True
|
|
625
|
+
|
|
626
|
+
active_warning = "\n⚠️ 警告: 这是当前激活的凭据,删除后将自动切换到其他凭据" if is_active else ""
|
|
627
|
+
|
|
628
|
+
click.echo(f"\n即将删除:")
|
|
629
|
+
click.echo(f" 配置: {config_name}")
|
|
630
|
+
click.echo(f" 类型: {type_display} {index + 1}")
|
|
631
|
+
click.echo(f" 内容: {display_text}")
|
|
632
|
+
click.echo(active_warning)
|
|
633
|
+
|
|
634
|
+
if click.confirm(f"\n确定要删除这个{type_display.lower()}吗?", default=False):
|
|
635
|
+
if cm.remove_credential(config_name, credential_type, index):
|
|
636
|
+
# 如果删除的是当前激活配置的凭据,更新环境变量
|
|
637
|
+
if config_name == cm.get_current_config_name():
|
|
638
|
+
cm.update_env_file()
|
|
639
|
+
|
|
640
|
+
click.echo(f"✅ 已删除 {type_display.lower()}: {display_text}")
|
|
641
|
+
return "deleted"
|
|
642
|
+
else:
|
|
643
|
+
click.echo(f"❌ 删除失败")
|
|
644
|
+
return "back"
|
|
645
|
+
else:
|
|
646
|
+
click.echo("✗ 已取消删除")
|
|
647
|
+
return "back"
|
|
648
|
+
else:
|
|
649
|
+
# 用户按ESC返回
|
|
650
|
+
return "back"
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def confirm_and_remove_config(cm: ConfigManager, config_name: str) -> bool:
|
|
654
|
+
"""确认并删除配置"""
|
|
655
|
+
config = cm.get_config(config_name)
|
|
656
|
+
if not config:
|
|
657
|
+
click.echo(f"配置 '{config_name}' 不存在")
|
|
658
|
+
return False
|
|
659
|
+
|
|
660
|
+
current = cm.get_current_config_name()
|
|
661
|
+
is_current = config_name == current
|
|
662
|
+
|
|
663
|
+
# 显示要删除的配置详细信息
|
|
664
|
+
display = format_config_display(config_name, config)
|
|
665
|
+
|
|
666
|
+
click.echo(f"\n即将删除整个配置:")
|
|
667
|
+
click.echo(f" {display}")
|
|
668
|
+
|
|
669
|
+
if is_current:
|
|
670
|
+
click.echo("⚠️ 警告: 这是当前激活的配置,删除后将自动切换到其他配置")
|
|
671
|
+
|
|
672
|
+
# 显示凭据数量
|
|
673
|
+
total_credentials = len(config["api_keys"]) + len(config["auth_tokens"])
|
|
674
|
+
if total_credentials > 0:
|
|
675
|
+
click.echo(f"⚠️ 警告: 将同时删除 {total_credentials} 个凭据")
|
|
676
|
+
|
|
677
|
+
if click.confirm(f"\n确定要删除配置 \"{config_name}\" 吗?这个操作不可撤销!", default=False):
|
|
678
|
+
if cm.remove_config(config_name):
|
|
679
|
+
# 如果删除的是当前激活配置,更新环境变量
|
|
680
|
+
if is_current:
|
|
681
|
+
cm.update_env_file()
|
|
682
|
+
|
|
683
|
+
click.echo(f"✅ 已删除配置: {config_name}")
|
|
684
|
+
return True
|
|
685
|
+
else:
|
|
686
|
+
click.echo("❌ 删除失败")
|
|
687
|
+
return False
|
|
688
|
+
else:
|
|
689
|
+
click.echo("✗ 已取消删除")
|
|
690
|
+
return False
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
if __name__ == '__main__':
|
|
694
|
+
cli()
|