cc-switch-ui 0.1.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.
- cc_switch_ui/__init__.py +3 -0
- cc_switch_ui/app.py +41 -0
- cc_switch_ui/config.py +202 -0
- cc_switch_ui/index.html +756 -0
- cc_switch_ui/process.py +221 -0
- cc_switch_ui/server.py +294 -0
- cc_switch_ui-0.1.0.dist-info/METADATA +190 -0
- cc_switch_ui-0.1.0.dist-info/RECORD +10 -0
- cc_switch_ui-0.1.0.dist-info/WHEEL +4 -0
- cc_switch_ui-0.1.0.dist-info/entry_points.txt +2 -0
cc_switch_ui/__init__.py
ADDED
cc_switch_ui/app.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CC Switch Web UI —— CLI 入口点。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import atexit
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
|
|
10
|
+
from .config import CONFIG_PATH
|
|
11
|
+
from .server import create_app
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description="CC Switch Web UI —— Claude Code 多供应商 / 多账号管理面板",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument("--host", default="127.0.0.1", help="监听地址 (默认 127.0.0.1)")
|
|
19
|
+
parser.add_argument("--port", type=int, default=8765, help="监听端口 (默认 8765)")
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
app = create_app()
|
|
23
|
+
claude_proc = app._claude_proc
|
|
24
|
+
|
|
25
|
+
def _cleanup_children(*_):
|
|
26
|
+
"""app 退出时连带停掉 claude 子进程,避免留下「失联孤儿」。"""
|
|
27
|
+
claude_proc.keepalive = False
|
|
28
|
+
claude_proc.stop()
|
|
29
|
+
|
|
30
|
+
# 进程退出 / 被 systemd 或守护脚本 SIGTERM 时,清理 claude 子进程
|
|
31
|
+
atexit.register(_cleanup_children)
|
|
32
|
+
signal.signal(signal.SIGTERM, lambda *a: (_cleanup_children(), os._exit(0)))
|
|
33
|
+
|
|
34
|
+
print(f"配置文件: {CONFIG_PATH}")
|
|
35
|
+
print(f"CC Switch Web UI 已启动 → http://{args.host}:{args.port}")
|
|
36
|
+
# threaded=True 保证 SSE 长连接不阻塞其它请求
|
|
37
|
+
app.run(host=args.host, port=args.port, threaded=True, debug=False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
main()
|
cc_switch_ui/config.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置管理 —— 读写 ~/.ccm_config,管理供应商与账号。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import threading
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# --------------------------------------------------------------------------- #
|
|
12
|
+
# 常量
|
|
13
|
+
# --------------------------------------------------------------------------- #
|
|
14
|
+
|
|
15
|
+
CONFIG_PATH = Path(os.path.expanduser("~/.ccm_config"))
|
|
16
|
+
|
|
17
|
+
# 预置供应商。所有都走 Anthropic 兼容协议,claude 通过环境变量切换后端。
|
|
18
|
+
DEFAULT_PROVIDERS = {
|
|
19
|
+
"claude": {
|
|
20
|
+
"label": "Claude 官方",
|
|
21
|
+
"base_url": "https://api.anthropic.com",
|
|
22
|
+
"auth_var": "ANTHROPIC_API_KEY",
|
|
23
|
+
"model": "",
|
|
24
|
+
"accounts": [],
|
|
25
|
+
"active_account": None,
|
|
26
|
+
},
|
|
27
|
+
"deepseek": {
|
|
28
|
+
"label": "DeepSeek",
|
|
29
|
+
"base_url": "https://api.deepseek.com/anthropic",
|
|
30
|
+
"auth_var": "ANTHROPIC_AUTH_TOKEN",
|
|
31
|
+
"model": "deepseek-chat",
|
|
32
|
+
"accounts": [],
|
|
33
|
+
"active_account": None,
|
|
34
|
+
},
|
|
35
|
+
"kimi": {
|
|
36
|
+
"label": "Kimi (Moonshot)",
|
|
37
|
+
"base_url": "https://api.moonshot.cn/anthropic",
|
|
38
|
+
"auth_var": "ANTHROPIC_AUTH_TOKEN",
|
|
39
|
+
"model": "kimi-k2-0905-preview",
|
|
40
|
+
"accounts": [],
|
|
41
|
+
"active_account": None,
|
|
42
|
+
},
|
|
43
|
+
"glm": {
|
|
44
|
+
"label": "GLM (智谱)",
|
|
45
|
+
"base_url": "https://open.bigmodel.cn/api/anthropic",
|
|
46
|
+
"auth_var": "ANTHROPIC_AUTH_TOKEN",
|
|
47
|
+
"model": "glm-4.6",
|
|
48
|
+
"accounts": [],
|
|
49
|
+
"active_account": None,
|
|
50
|
+
},
|
|
51
|
+
"qwen": {
|
|
52
|
+
"label": "Qwen (通义千问)",
|
|
53
|
+
"base_url": "https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
|
|
54
|
+
"auth_var": "ANTHROPIC_AUTH_TOKEN",
|
|
55
|
+
"model": "qwen3-coder-plus",
|
|
56
|
+
"accounts": [],
|
|
57
|
+
"active_account": None,
|
|
58
|
+
},
|
|
59
|
+
"openrouter": {
|
|
60
|
+
"label": "OpenRouter",
|
|
61
|
+
"base_url": "https://openrouter.ai/api/v1",
|
|
62
|
+
"auth_var": "ANTHROPIC_AUTH_TOKEN",
|
|
63
|
+
"model": "anthropic/claude-3.5-sonnet",
|
|
64
|
+
"accounts": [],
|
|
65
|
+
"active_account": None,
|
|
66
|
+
},
|
|
67
|
+
"custom": {
|
|
68
|
+
"label": "自定义",
|
|
69
|
+
"base_url": "",
|
|
70
|
+
"auth_var": "ANTHROPIC_AUTH_TOKEN",
|
|
71
|
+
"model": "",
|
|
72
|
+
"accounts": [],
|
|
73
|
+
"active_account": None,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
AUTH_VARS = ("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN")
|
|
78
|
+
|
|
79
|
+
_config_lock = threading.Lock()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --------------------------------------------------------------------------- #
|
|
83
|
+
# 配置读写
|
|
84
|
+
# --------------------------------------------------------------------------- #
|
|
85
|
+
|
|
86
|
+
def _default_config():
|
|
87
|
+
"""深拷贝预置项,避免运行期被修改污染默认值。"""
|
|
88
|
+
return {
|
|
89
|
+
"current_provider": "claude",
|
|
90
|
+
"providers": json.loads(json.dumps(DEFAULT_PROVIDERS)),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_config():
|
|
95
|
+
if not CONFIG_PATH.exists():
|
|
96
|
+
cfg = _default_config()
|
|
97
|
+
save_config(cfg)
|
|
98
|
+
return cfg
|
|
99
|
+
try:
|
|
100
|
+
cfg = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
101
|
+
except (json.JSONDecodeError, OSError):
|
|
102
|
+
cfg = _default_config()
|
|
103
|
+
save_config(cfg)
|
|
104
|
+
return cfg
|
|
105
|
+
|
|
106
|
+
# 合并:确保所有预置供应商存在,且字段完整(向前兼容)
|
|
107
|
+
cfg.setdefault("current_provider", "claude")
|
|
108
|
+
providers = cfg.setdefault("providers", {})
|
|
109
|
+
for key, preset in DEFAULT_PROVIDERS.items():
|
|
110
|
+
p = providers.setdefault(key, json.loads(json.dumps(preset)))
|
|
111
|
+
for field, val in preset.items():
|
|
112
|
+
if field not in ("accounts", "active_account"):
|
|
113
|
+
p.setdefault(field, val)
|
|
114
|
+
p.setdefault("accounts", [])
|
|
115
|
+
p.setdefault("active_account", None)
|
|
116
|
+
return cfg
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def save_config(cfg):
|
|
120
|
+
CONFIG_PATH.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
121
|
+
try:
|
|
122
|
+
os.chmod(CONFIG_PATH, 0o600) # 含密钥,限制权限
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --------------------------------------------------------------------------- #
|
|
128
|
+
# 工具函数
|
|
129
|
+
# --------------------------------------------------------------------------- #
|
|
130
|
+
|
|
131
|
+
def mask_key(key: str) -> str:
|
|
132
|
+
if not key:
|
|
133
|
+
return ""
|
|
134
|
+
if len(key) <= 8:
|
|
135
|
+
return "•" * len(key)
|
|
136
|
+
return f"{key[:4]}{'•' * 6}{key[-4:]}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def active_account_of(provider: dict):
|
|
140
|
+
aid = provider.get("active_account")
|
|
141
|
+
for acc in provider.get("accounts", []):
|
|
142
|
+
if acc["id"] == aid:
|
|
143
|
+
return acc
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def public_state():
|
|
148
|
+
"""返回给前端的状态(密钥脱敏)。"""
|
|
149
|
+
cfg = load_config()
|
|
150
|
+
out_providers = {}
|
|
151
|
+
for key, p in cfg["providers"].items():
|
|
152
|
+
accounts = [
|
|
153
|
+
{
|
|
154
|
+
"id": a["id"],
|
|
155
|
+
"name": a.get("name", ""),
|
|
156
|
+
"key_masked": mask_key(a.get("api_key", "")),
|
|
157
|
+
"has_key": bool(a.get("api_key")),
|
|
158
|
+
}
|
|
159
|
+
for a in p.get("accounts", [])
|
|
160
|
+
]
|
|
161
|
+
out_providers[key] = {
|
|
162
|
+
"id": key,
|
|
163
|
+
"label": p.get("label", key),
|
|
164
|
+
"base_url": p.get("base_url", ""),
|
|
165
|
+
"model": p.get("model", ""),
|
|
166
|
+
"auth_var": p.get("auth_var", "ANTHROPIC_API_KEY"),
|
|
167
|
+
"accounts": accounts,
|
|
168
|
+
"active_account": p.get("active_account"),
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
"current_provider": cfg["current_provider"],
|
|
172
|
+
"providers": out_providers,
|
|
173
|
+
"ccm_available": shutil.which("ccm") is not None,
|
|
174
|
+
"claude_available": shutil.which("claude") is not None,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def build_env_for_active():
|
|
179
|
+
"""根据当前供应商 + 激活账号构造注入环境变量。"""
|
|
180
|
+
cfg = load_config()
|
|
181
|
+
pid = cfg["current_provider"]
|
|
182
|
+
provider = cfg["providers"].get(pid, {})
|
|
183
|
+
acc = active_account_of(provider)
|
|
184
|
+
label = provider.get("label", pid)
|
|
185
|
+
|
|
186
|
+
env = {}
|
|
187
|
+
base_url = provider.get("base_url", "")
|
|
188
|
+
model = provider.get("model", "")
|
|
189
|
+
auth_var = provider.get("auth_var", "ANTHROPIC_API_KEY")
|
|
190
|
+
api_key = acc.get("api_key", "") if acc else ""
|
|
191
|
+
|
|
192
|
+
if pid != "claude" and base_url:
|
|
193
|
+
env["ANTHROPIC_BASE_URL"] = base_url
|
|
194
|
+
if api_key:
|
|
195
|
+
env[auth_var] = api_key
|
|
196
|
+
if model:
|
|
197
|
+
env["ANTHROPIC_MODEL"] = model
|
|
198
|
+
return env, label, bool(api_key)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_lock():
|
|
202
|
+
return _config_lock
|