Spaces:
Paused
Paused
asemxin commited on
Commit ·
731d915
1
Parent(s): ce37b8f
feat: add API key modification, account deletion, password protection
Browse files- account_manager.py +47 -0
- app.py +121 -12
- models.py +2 -0
account_manager.py
CHANGED
|
@@ -11,6 +11,7 @@ from models import Account, OAuthToken, AccountStats
|
|
| 11 |
# 数据文件路径 (HF Spaces 持久化目录)
|
| 12 |
DATA_DIR = os.environ.get("DATA_DIR", "./data")
|
| 13 |
ACCOUNTS_FILE = os.path.join(DATA_DIR, "accounts.json")
|
|
|
|
| 14 |
|
| 15 |
# Google OAuth 配置 (Antigravity 使用的 Client ID)
|
| 16 |
OAUTH_CLIENT_ID = "595848968694-r5ng3t6qb9elhe1u1h1hqgq4j2r3hgvk.apps.googleusercontent.com"
|
|
@@ -189,5 +190,51 @@ class AccountManager:
|
|
| 189 |
)
|
| 190 |
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
# 全局单例
|
| 193 |
account_manager = AccountManager()
|
|
|
|
|
|
|
|
|
| 11 |
# 数据文件路径 (HF Spaces 持久化目录)
|
| 12 |
DATA_DIR = os.environ.get("DATA_DIR", "./data")
|
| 13 |
ACCOUNTS_FILE = os.path.join(DATA_DIR, "accounts.json")
|
| 14 |
+
CONFIG_FILE = os.path.join(DATA_DIR, "config.json")
|
| 15 |
|
| 16 |
# Google OAuth 配置 (Antigravity 使用的 Client ID)
|
| 17 |
OAUTH_CLIENT_ID = "595848968694-r5ng3t6qb9elhe1u1h1hqgq4j2r3hgvk.apps.googleusercontent.com"
|
|
|
|
| 190 |
)
|
| 191 |
|
| 192 |
|
| 193 |
+
class ConfigManager:
|
| 194 |
+
"""配置管理器 - 管理 API Key 等可变配置"""
|
| 195 |
+
|
| 196 |
+
def __init__(self):
|
| 197 |
+
self._config = {
|
| 198 |
+
"api_key": "sk-antigravity"
|
| 199 |
+
}
|
| 200 |
+
self._ensure_data_dir()
|
| 201 |
+
self._load_config()
|
| 202 |
+
|
| 203 |
+
def _ensure_data_dir(self):
|
| 204 |
+
"""确保数据目录存在"""
|
| 205 |
+
os.makedirs(DATA_DIR, exist_ok=True)
|
| 206 |
+
|
| 207 |
+
def _load_config(self):
|
| 208 |
+
"""从文件加载配置"""
|
| 209 |
+
if os.path.exists(CONFIG_FILE):
|
| 210 |
+
try:
|
| 211 |
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
| 212 |
+
self._config.update(json.load(f))
|
| 213 |
+
except Exception as e:
|
| 214 |
+
print(f"加载配置失败: {e}")
|
| 215 |
+
|
| 216 |
+
def _save_config(self):
|
| 217 |
+
"""保存配置到文件"""
|
| 218 |
+
try:
|
| 219 |
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
| 220 |
+
json.dump(self._config, f, ensure_ascii=False, indent=2)
|
| 221 |
+
except Exception as e:
|
| 222 |
+
print(f"保存配置失败: {e}")
|
| 223 |
+
|
| 224 |
+
def get_api_key(self) -> str:
|
| 225 |
+
"""获取 API Key"""
|
| 226 |
+
return self._config.get("api_key", "sk-antigravity")
|
| 227 |
+
|
| 228 |
+
def set_api_key(self, api_key: str) -> bool:
|
| 229 |
+
"""设置 API Key"""
|
| 230 |
+
if not api_key or len(api_key.strip()) == 0:
|
| 231 |
+
return False
|
| 232 |
+
self._config["api_key"] = api_key.strip()
|
| 233 |
+
self._save_config()
|
| 234 |
+
return True
|
| 235 |
+
|
| 236 |
+
|
| 237 |
# 全局单例
|
| 238 |
account_manager = AccountManager()
|
| 239 |
+
config_manager = ConfigManager()
|
| 240 |
+
|
app.py
CHANGED
|
@@ -11,13 +11,17 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 11 |
from contextlib import asynccontextmanager
|
| 12 |
|
| 13 |
from models import OpenAIChatRequest, OpenAIMessage, SUPPORTED_MODELS, ServiceConfig
|
| 14 |
-
from account_manager import account_manager
|
| 15 |
from load_balancer import load_balancer
|
| 16 |
from proxy_service import stream_chat_completion, chat_completion
|
| 17 |
|
| 18 |
# ============ 服务配置 ============
|
| 19 |
config = ServiceConfig()
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# ============ FastAPI 应用 ============
|
| 23 |
@asynccontextmanager
|
|
@@ -74,6 +78,12 @@ async def chat_completions(request: Request):
|
|
| 74 |
if not auth_header.startswith("Bearer "):
|
| 75 |
raise HTTPException(status_code=401, detail="Missing API key")
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
# 获取请求体
|
| 78 |
body = await request.json()
|
| 79 |
|
|
@@ -128,10 +138,11 @@ def get_accounts_table():
|
|
| 128 |
"""获取账号列表表格数据"""
|
| 129 |
accounts = account_manager.get_all_accounts()
|
| 130 |
if not accounts:
|
| 131 |
-
return [["暂无账号", "-", "-", "-", "-"]]
|
| 132 |
|
| 133 |
return [
|
| 134 |
[
|
|
|
|
| 135 |
acc.email,
|
| 136 |
"✅ 正常" if acc.is_available() else "❌ 冷却中",
|
| 137 |
str(acc.total_requests),
|
|
@@ -159,9 +170,54 @@ def add_account(email: str, access_token: str, refresh_token: str, project_id: s
|
|
| 159 |
return f"❌ 添加失败: {e}", get_accounts_table()
|
| 160 |
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
def get_service_info():
|
| 163 |
"""获取服务信息"""
|
| 164 |
stats = account_manager.get_stats()
|
|
|
|
| 165 |
|
| 166 |
# 获取当前 Space URL
|
| 167 |
space_url = os.environ.get("SPACE_HOST", "localhost:7860")
|
|
@@ -173,7 +229,7 @@ def get_service_info():
|
|
| 173 |
|
| 174 |
**Base URL:** `{space_url}/v1`
|
| 175 |
|
| 176 |
-
**API Key:** `{
|
| 177 |
|
| 178 |
**支持的模型:**
|
| 179 |
- `gemini-2.5-pro` / `gemini-2.5-flash` / `gemini-2.5-flash-lite`
|
|
@@ -199,7 +255,7 @@ def get_service_info():
|
|
| 199 |
```bash
|
| 200 |
curl {space_url}/v1/chat/completions \\
|
| 201 |
-H "Content-Type: application/json" \\
|
| 202 |
-
-H "Authorization: Bearer
|
| 203 |
-d '{{
|
| 204 |
"model": "gemini-2.5-flash",
|
| 205 |
"messages": [{{"role": "user", "content": "Hello"}}],
|
|
@@ -210,7 +266,7 @@ curl {space_url}/v1/chat/completions \\
|
|
| 210 |
### NextChat / Cherry Studio 配置
|
| 211 |
1. API 类型: OpenAI
|
| 212 |
2. Base URL: `{space_url}/v1`
|
| 213 |
-
3. API Key: `
|
| 214 |
"""
|
| 215 |
|
| 216 |
|
|
@@ -224,6 +280,7 @@ with gr.Blocks(
|
|
| 224 |
css="""
|
| 225 |
.container { max-width: 1200px; margin: auto; }
|
| 226 |
.logo { font-size: 2em; font-weight: bold; }
|
|
|
|
| 227 |
"""
|
| 228 |
) as demo:
|
| 229 |
gr.Markdown(
|
|
@@ -252,7 +309,7 @@ with gr.Blocks(
|
|
| 252 |
|
| 253 |
access_token_input = gr.Textbox(
|
| 254 |
label="Access Token",
|
| 255 |
-
placeholder="ya29.xxx...",
|
| 256 |
lines=2
|
| 257 |
)
|
| 258 |
refresh_token_input = gr.Textbox(
|
|
@@ -264,34 +321,86 @@ with gr.Blocks(
|
|
| 264 |
add_btn = gr.Button("➕ 添加账号", variant="primary")
|
| 265 |
result_text = gr.Textbox(label="结果", interactive=False)
|
| 266 |
|
|
|
|
| 267 |
gr.Markdown("### 账号列表")
|
| 268 |
accounts_table = gr.Dataframe(
|
| 269 |
-
headers=["邮箱", "状态", "请求数", "成功率", "最后使用"],
|
| 270 |
value=get_accounts_table,
|
| 271 |
interactive=False
|
| 272 |
)
|
| 273 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
add_btn.click(
|
| 275 |
fn=add_account,
|
| 276 |
inputs=[email_input, access_token_input, refresh_token_input, project_id_input],
|
| 277 |
outputs=[result_text, accounts_table]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
)
|
| 279 |
|
| 280 |
# 设置
|
| 281 |
with gr.Tab("⚙️ 设置"):
|
| 282 |
-
gr.Markdown("###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
gr.Markdown("""
|
| 284 |
-
> 以下配置在 HF Spaces 环境下通过
|
| 285 |
|
| 286 |
| 配置项 | 环境变量 | 默认值 |
|
| 287 |
|--------|----------|--------|
|
| 288 |
| 数据目录 | `DATA_DIR` | `./data` |
|
| 289 |
-
|
|
|
|
|
| 290 |
""")
|
| 291 |
|
| 292 |
|
| 293 |
-
# 挂载 Gradio 到 FastAPI
|
| 294 |
-
app = gr.mount_gradio_app(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
|
| 297 |
if __name__ == "__main__":
|
|
|
|
| 11 |
from contextlib import asynccontextmanager
|
| 12 |
|
| 13 |
from models import OpenAIChatRequest, OpenAIMessage, SUPPORTED_MODELS, ServiceConfig
|
| 14 |
+
from account_manager import account_manager, config_manager
|
| 15 |
from load_balancer import load_balancer
|
| 16 |
from proxy_service import stream_chat_completion, chat_completion
|
| 17 |
|
| 18 |
# ============ 服务配置 ============
|
| 19 |
config = ServiceConfig()
|
| 20 |
|
| 21 |
+
# 从环境变量获取管理员凭据
|
| 22 |
+
ADMIN_USERNAME = os.environ.get("ADMIN_USERNAME", "admin")
|
| 23 |
+
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "antigravity")
|
| 24 |
+
|
| 25 |
|
| 26 |
# ============ FastAPI 应用 ============
|
| 27 |
@asynccontextmanager
|
|
|
|
| 78 |
if not auth_header.startswith("Bearer "):
|
| 79 |
raise HTTPException(status_code=401, detail="Missing API key")
|
| 80 |
|
| 81 |
+
# 验证 API Key 是否匹配
|
| 82 |
+
provided_key = auth_header[7:] # 去掉 "Bearer " 前缀
|
| 83 |
+
expected_key = config_manager.get_api_key()
|
| 84 |
+
if provided_key != expected_key:
|
| 85 |
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
| 86 |
+
|
| 87 |
# 获取请求体
|
| 88 |
body = await request.json()
|
| 89 |
|
|
|
|
| 138 |
"""获取账号列表表格数据"""
|
| 139 |
accounts = account_manager.get_all_accounts()
|
| 140 |
if not accounts:
|
| 141 |
+
return [["暂无账号", "-", "-", "-", "-", "-"]]
|
| 142 |
|
| 143 |
return [
|
| 144 |
[
|
| 145 |
+
acc.id,
|
| 146 |
acc.email,
|
| 147 |
"✅ 正常" if acc.is_available() else "❌ 冷却中",
|
| 148 |
str(acc.total_requests),
|
|
|
|
| 170 |
return f"❌ 添加失败: {e}", get_accounts_table()
|
| 171 |
|
| 172 |
|
| 173 |
+
def delete_account(email_to_delete: str):
|
| 174 |
+
"""删除账号"""
|
| 175 |
+
if not email_to_delete or email_to_delete == "暂无账号":
|
| 176 |
+
return "❌ 请选择要删除的账号", get_accounts_table()
|
| 177 |
+
|
| 178 |
+
# 查找账号 ID
|
| 179 |
+
accounts = account_manager.get_all_accounts()
|
| 180 |
+
target_account = None
|
| 181 |
+
for acc in accounts:
|
| 182 |
+
if acc.email == email_to_delete:
|
| 183 |
+
target_account = acc
|
| 184 |
+
break
|
| 185 |
+
|
| 186 |
+
if not target_account:
|
| 187 |
+
return f"❌ 账号 {email_to_delete} 不存在", get_accounts_table()
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
if account_manager.remove_account(target_account.id):
|
| 191 |
+
return f"✅ 账号 {email_to_delete} 已删除", get_accounts_table()
|
| 192 |
+
else:
|
| 193 |
+
return f"❌ 删除账号 {email_to_delete} 失败", get_accounts_table()
|
| 194 |
+
except Exception as e:
|
| 195 |
+
return f"❌ 删除失败: {e}", get_accounts_table()
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def get_account_emails():
|
| 199 |
+
"""获取所有账号邮箱列表"""
|
| 200 |
+
accounts = account_manager.get_all_accounts()
|
| 201 |
+
if not accounts:
|
| 202 |
+
return ["暂无账号"]
|
| 203 |
+
return [acc.email for acc in accounts]
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def update_api_key(new_api_key: str):
|
| 207 |
+
"""更新 API Key"""
|
| 208 |
+
if not new_api_key or len(new_api_key.strip()) < 3:
|
| 209 |
+
return "❌ API Key 不能为空且至少需要 3 个字符", config_manager.get_api_key()
|
| 210 |
+
|
| 211 |
+
if config_manager.set_api_key(new_api_key.strip()):
|
| 212 |
+
return f"✅ API Key 已更新为: {new_api_key.strip()}", new_api_key.strip()
|
| 213 |
+
else:
|
| 214 |
+
return "❌ 更新 API Key 失败", config_manager.get_api_key()
|
| 215 |
+
|
| 216 |
+
|
| 217 |
def get_service_info():
|
| 218 |
"""获取服务信息"""
|
| 219 |
stats = account_manager.get_stats()
|
| 220 |
+
current_api_key = config_manager.get_api_key()
|
| 221 |
|
| 222 |
# 获取当前 Space URL
|
| 223 |
space_url = os.environ.get("SPACE_HOST", "localhost:7860")
|
|
|
|
| 229 |
|
| 230 |
**Base URL:** `{space_url}/v1`
|
| 231 |
|
| 232 |
+
**API Key:** `{current_api_key}`
|
| 233 |
|
| 234 |
**支持的模型:**
|
| 235 |
- `gemini-2.5-pro` / `gemini-2.5-flash` / `gemini-2.5-flash-lite`
|
|
|
|
| 255 |
```bash
|
| 256 |
curl {space_url}/v1/chat/completions \\
|
| 257 |
-H "Content-Type: application/json" \\
|
| 258 |
+
-H "Authorization: Bearer {current_api_key}" \\
|
| 259 |
-d '{{
|
| 260 |
"model": "gemini-2.5-flash",
|
| 261 |
"messages": [{{"role": "user", "content": "Hello"}}],
|
|
|
|
| 266 |
### NextChat / Cherry Studio 配置
|
| 267 |
1. API 类型: OpenAI
|
| 268 |
2. Base URL: `{space_url}/v1`
|
| 269 |
+
3. API Key: `{current_api_key}`
|
| 270 |
"""
|
| 271 |
|
| 272 |
|
|
|
|
| 280 |
css="""
|
| 281 |
.container { max-width: 1200px; margin: auto; }
|
| 282 |
.logo { font-size: 2em; font-weight: bold; }
|
| 283 |
+
.danger-btn { background-color: #dc3545 !important; }
|
| 284 |
"""
|
| 285 |
) as demo:
|
| 286 |
gr.Markdown(
|
|
|
|
| 309 |
|
| 310 |
access_token_input = gr.Textbox(
|
| 311 |
label="Access Token",
|
| 312 |
+
placeholder="ya29.xxx... 或填 pending",
|
| 313 |
lines=2
|
| 314 |
)
|
| 315 |
refresh_token_input = gr.Textbox(
|
|
|
|
| 321 |
add_btn = gr.Button("➕ 添加账号", variant="primary")
|
| 322 |
result_text = gr.Textbox(label="结果", interactive=False)
|
| 323 |
|
| 324 |
+
gr.Markdown("---")
|
| 325 |
gr.Markdown("### 账号列表")
|
| 326 |
accounts_table = gr.Dataframe(
|
| 327 |
+
headers=["ID", "邮箱", "状态", "请求数", "成功率", "最后使用"],
|
| 328 |
value=get_accounts_table,
|
| 329 |
interactive=False
|
| 330 |
)
|
| 331 |
|
| 332 |
+
gr.Markdown("### 删除账号")
|
| 333 |
+
with gr.Row():
|
| 334 |
+
delete_email_dropdown = gr.Dropdown(
|
| 335 |
+
label="选择要删除的账号",
|
| 336 |
+
choices=get_account_emails,
|
| 337 |
+
interactive=True
|
| 338 |
+
)
|
| 339 |
+
delete_btn = gr.Button("🗑️ 删除账号", variant="stop")
|
| 340 |
+
delete_result = gr.Textbox(label="删除结果", interactive=False)
|
| 341 |
+
|
| 342 |
+
# 绑定事件
|
| 343 |
add_btn.click(
|
| 344 |
fn=add_account,
|
| 345 |
inputs=[email_input, access_token_input, refresh_token_input, project_id_input],
|
| 346 |
outputs=[result_text, accounts_table]
|
| 347 |
+
).then(
|
| 348 |
+
fn=get_account_emails,
|
| 349 |
+
outputs=delete_email_dropdown
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
delete_btn.click(
|
| 353 |
+
fn=delete_account,
|
| 354 |
+
inputs=[delete_email_dropdown],
|
| 355 |
+
outputs=[delete_result, accounts_table]
|
| 356 |
+
).then(
|
| 357 |
+
fn=get_account_emails,
|
| 358 |
+
outputs=delete_email_dropdown
|
| 359 |
)
|
| 360 |
|
| 361 |
# 设置
|
| 362 |
with gr.Tab("⚙️ 设置"):
|
| 363 |
+
gr.Markdown("### API Key 配置")
|
| 364 |
+
gr.Markdown("> 修改后,所有 API 请求需要使用新的 API Key")
|
| 365 |
+
|
| 366 |
+
with gr.Row():
|
| 367 |
+
api_key_input = gr.Textbox(
|
| 368 |
+
label="当前 API Key",
|
| 369 |
+
value=config_manager.get_api_key,
|
| 370 |
+
placeholder="输入新的 API Key",
|
| 371 |
+
interactive=True
|
| 372 |
+
)
|
| 373 |
+
save_api_key_btn = gr.Button("💾 保存 API Key", variant="primary")
|
| 374 |
+
|
| 375 |
+
api_key_result = gr.Textbox(label="结果", interactive=False)
|
| 376 |
+
|
| 377 |
+
save_api_key_btn.click(
|
| 378 |
+
fn=update_api_key,
|
| 379 |
+
inputs=[api_key_input],
|
| 380 |
+
outputs=[api_key_result, api_key_input]
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
gr.Markdown("---")
|
| 384 |
+
gr.Markdown("### 环境变量配置")
|
| 385 |
gr.Markdown("""
|
| 386 |
+
> 以下配置在 HF Spaces 环境下通过 Secrets 设置
|
| 387 |
|
| 388 |
| 配置项 | 环境变量 | 默认值 |
|
| 389 |
|--------|----------|--------|
|
| 390 |
| 数据目录 | `DATA_DIR` | `./data` |
|
| 391 |
+
| 管理员用户名 | `ADMIN_USERNAME` | `admin` |
|
| 392 |
+
| 管理员密码 | `ADMIN_PASSWORD` | `antigravity` |
|
| 393 |
""")
|
| 394 |
|
| 395 |
|
| 396 |
+
# 挂载 Gradio 到 FastAPI (带密码保护)
|
| 397 |
+
app = gr.mount_gradio_app(
|
| 398 |
+
app,
|
| 399 |
+
demo,
|
| 400 |
+
path="/",
|
| 401 |
+
auth=(ADMIN_USERNAME, ADMIN_PASSWORD),
|
| 402 |
+
auth_message="🔐 请输入管理员凭据登录 Antigravity API Proxy"
|
| 403 |
+
)
|
| 404 |
|
| 405 |
|
| 406 |
if __name__ == "__main__":
|
models.py
CHANGED
|
@@ -64,6 +64,8 @@ class AccountStats(BaseModel):
|
|
| 64 |
class ServiceConfig(BaseModel):
|
| 65 |
"""服务配置"""
|
| 66 |
api_key: str = "sk-antigravity"
|
|
|
|
|
|
|
| 67 |
port: int = 7860
|
| 68 |
enable_cors: bool = True
|
| 69 |
max_retries: int = 3
|
|
|
|
| 64 |
class ServiceConfig(BaseModel):
|
| 65 |
"""服务配置"""
|
| 66 |
api_key: str = "sk-antigravity"
|
| 67 |
+
admin_username: str = "admin"
|
| 68 |
+
admin_password: str = "antigravity"
|
| 69 |
port: int = 7860
|
| 70 |
enable_cors: bool = True
|
| 71 |
max_retries: int = 3
|