mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 08:55:28 +08:00
275 lines
11 KiB
Python
275 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Admin Vercel 模块 - Vercel 同步和部署"""
|
||
import asyncio
|
||
import base64
|
||
import json
|
||
import os
|
||
|
||
import httpx
|
||
from fastapi import APIRouter, HTTPException, Request, Depends
|
||
from fastapi.responses import JSONResponse
|
||
|
||
from core.config import CONFIG, save_config, logger
|
||
from core.auth import get_account_identifier, init_account_queue
|
||
from core.deepseek import login_deepseek_via_account
|
||
|
||
from .auth import verify_admin
|
||
|
||
router = APIRouter()
|
||
|
||
# Vercel 预配置
|
||
VERCEL_TOKEN = os.getenv("VERCEL_TOKEN", "")
|
||
VERCEL_PROJECT_ID = os.getenv("VERCEL_PROJECT_ID", "")
|
||
VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID", "")
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
# API 测试(通过本地 API)
|
||
# ----------------------------------------------------------------------
|
||
@router.post("/test")
|
||
async def test_api(request: Request, _: bool = Depends(verify_admin)):
|
||
"""测试 API 调用"""
|
||
try:
|
||
data = await request.json()
|
||
model = data.get("model", "deepseek-chat")
|
||
message = data.get("message", "你好")
|
||
api_key = data.get("api_key", "")
|
||
|
||
if not api_key:
|
||
keys = CONFIG.get("keys", [])
|
||
if not keys:
|
||
raise HTTPException(status_code=400, detail="没有可用的 API Key")
|
||
api_key = keys[0]
|
||
|
||
host = request.headers.get("host", "localhost:5001")
|
||
scheme = "https" if "vercel" in host.lower() else "http"
|
||
base_url = f"{scheme}://{host}"
|
||
|
||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||
response = await client.post(
|
||
f"{base_url}/v1/chat/completions",
|
||
headers={"Authorization": f"Bearer {api_key}"},
|
||
json={
|
||
"model": model,
|
||
"messages": [{"role": "user", "content": message}],
|
||
"stream": False,
|
||
},
|
||
)
|
||
|
||
return JSONResponse(content={
|
||
"success": response.status_code == 200,
|
||
"status_code": response.status_code,
|
||
"response": response.json() if response.status_code == 200 else response.text,
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"[test_api] 错误: {e}")
|
||
return JSONResponse(content={"success": False, "error": str(e)})
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
# Vercel 同步
|
||
# ----------------------------------------------------------------------
|
||
@router.post("/vercel/sync")
|
||
async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)):
|
||
"""同步配置到 Vercel 并触发重新部署"""
|
||
try:
|
||
data = await request.json()
|
||
vercel_token = data.get("vercel_token", "")
|
||
project_id = data.get("project_id", "")
|
||
team_id = data.get("team_id", "")
|
||
auto_validate = data.get("auto_validate", True)
|
||
save_vercel_credentials = data.get("save_credentials", True)
|
||
|
||
use_preconfig = vercel_token == "__USE_PRECONFIG__" or not vercel_token
|
||
if use_preconfig:
|
||
vercel_token = VERCEL_TOKEN
|
||
if not project_id:
|
||
project_id = VERCEL_PROJECT_ID
|
||
if not team_id:
|
||
team_id = VERCEL_TEAM_ID
|
||
|
||
if not vercel_token or not project_id:
|
||
raise HTTPException(status_code=400, detail="需要 Vercel Token 和 Project ID")
|
||
|
||
# 自动验证账号
|
||
validated_count = 0
|
||
failed_accounts = []
|
||
if auto_validate:
|
||
accounts = CONFIG.get("accounts", [])
|
||
for acc in accounts:
|
||
acc_id = get_account_identifier(acc)
|
||
if not acc.get("token", "").strip():
|
||
try:
|
||
logger.info(f"[sync_to_vercel] 自动验证账号: {acc_id}")
|
||
login_deepseek_via_account(acc)
|
||
validated_count += 1
|
||
except Exception as e:
|
||
logger.warning(f"[sync_to_vercel] 账号 {acc_id} 验证失败: {e}")
|
||
failed_accounts.append(acc_id)
|
||
await asyncio.sleep(0.5)
|
||
|
||
config_json = json.dumps(CONFIG, ensure_ascii=False, separators=(",", ":"))
|
||
config_b64 = base64.b64encode(config_json.encode("utf-8")).decode("utf-8")
|
||
|
||
headers = {"Authorization": f"Bearer {vercel_token}"}
|
||
base_url = "https://api.vercel.com"
|
||
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
params = {"teamId": team_id} if team_id else {}
|
||
env_resp = await client.get(
|
||
f"{base_url}/v9/projects/{project_id}/env",
|
||
headers=headers,
|
||
params=params,
|
||
)
|
||
|
||
if env_resp.status_code != 200:
|
||
raise HTTPException(status_code=env_resp.status_code, detail=f"获取环境变量失败: {env_resp.text}")
|
||
|
||
env_vars = env_resp.json().get("envs", [])
|
||
existing_env = None
|
||
for env in env_vars:
|
||
if env.get("key") == "DS2API_CONFIG_JSON":
|
||
existing_env = env
|
||
break
|
||
|
||
if existing_env:
|
||
env_id = existing_env["id"]
|
||
update_resp = await client.patch(
|
||
f"{base_url}/v9/projects/{project_id}/env/{env_id}",
|
||
headers=headers,
|
||
params=params,
|
||
json={"value": config_b64},
|
||
)
|
||
if update_resp.status_code not in [200, 201]:
|
||
raise HTTPException(status_code=update_resp.status_code, detail=f"更新环境变量失败: {update_resp.text}")
|
||
else:
|
||
create_resp = await client.post(
|
||
f"{base_url}/v10/projects/{project_id}/env",
|
||
headers=headers,
|
||
params=params,
|
||
json={
|
||
"key": "DS2API_CONFIG_JSON",
|
||
"value": config_b64,
|
||
"type": "encrypted",
|
||
"target": ["production", "preview"],
|
||
},
|
||
)
|
||
if create_resp.status_code not in [200, 201]:
|
||
raise HTTPException(status_code=create_resp.status_code, detail=f"创建环境变量失败: {create_resp.text}")
|
||
|
||
# 保存 Vercel 凭证
|
||
saved_credentials = []
|
||
if save_vercel_credentials and not use_preconfig:
|
||
creds_to_save = [
|
||
("VERCEL_TOKEN", vercel_token),
|
||
("VERCEL_PROJECT_ID", project_id),
|
||
]
|
||
if team_id:
|
||
creds_to_save.append(("VERCEL_TEAM_ID", team_id))
|
||
|
||
for key, value in creds_to_save:
|
||
existing = None
|
||
for env in env_vars:
|
||
if env.get("key") == key:
|
||
existing = env
|
||
break
|
||
|
||
if existing:
|
||
upd_resp = await client.patch(
|
||
f"{base_url}/v9/projects/{project_id}/env/{existing['id']}",
|
||
headers=headers,
|
||
params=params,
|
||
json={"value": value},
|
||
)
|
||
if upd_resp.status_code in [200, 201]:
|
||
saved_credentials.append(key)
|
||
else:
|
||
crt_resp = await client.post(
|
||
f"{base_url}/v10/projects/{project_id}/env",
|
||
headers=headers,
|
||
params=params,
|
||
json={
|
||
"key": key,
|
||
"value": value,
|
||
"type": "encrypted",
|
||
"target": ["production", "preview"],
|
||
},
|
||
)
|
||
if crt_resp.status_code in [200, 201]:
|
||
saved_credentials.append(key)
|
||
|
||
# 触发重新部署
|
||
project_resp = await client.get(
|
||
f"{base_url}/v9/projects/{project_id}",
|
||
headers=headers,
|
||
params=params,
|
||
)
|
||
|
||
if project_resp.status_code == 200:
|
||
project_data = project_resp.json()
|
||
repo = project_data.get("link", {})
|
||
|
||
if repo.get("type") == "github":
|
||
deploy_resp = await client.post(
|
||
f"{base_url}/v13/deployments",
|
||
headers=headers,
|
||
params=params,
|
||
json={
|
||
"name": project_id,
|
||
"project": project_id,
|
||
"target": "production",
|
||
"gitSource": {
|
||
"type": "github",
|
||
"repoId": repo.get("repoId"),
|
||
"ref": repo.get("productionBranch", "main"),
|
||
},
|
||
},
|
||
)
|
||
|
||
if deploy_resp.status_code in [200, 201]:
|
||
deploy_data = deploy_resp.json()
|
||
result = {
|
||
"success": True,
|
||
"message": "配置已同步,正在重新部署...",
|
||
"deployment_url": deploy_data.get("url"),
|
||
"validated_accounts": validated_count,
|
||
}
|
||
if failed_accounts:
|
||
result["failed_accounts"] = failed_accounts
|
||
if saved_credentials:
|
||
result["saved_credentials"] = saved_credentials
|
||
return JSONResponse(content=result)
|
||
|
||
result = {
|
||
"success": True,
|
||
"message": "配置已同步到 Vercel,请手动触发重新部署",
|
||
"manual_deploy_required": True,
|
||
"validated_accounts": validated_count,
|
||
}
|
||
if failed_accounts:
|
||
result["failed_accounts"] = failed_accounts
|
||
if saved_credentials:
|
||
result["saved_credentials"] = saved_credentials
|
||
return JSONResponse(content=result)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"[sync_to_vercel] 错误: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
# 导出配置
|
||
# ----------------------------------------------------------------------
|
||
@router.get("/export")
|
||
async def export_config(_: bool = Depends(verify_admin)):
|
||
"""导出完整配置(JSON 和 Base64)"""
|
||
config_json = json.dumps(CONFIG, ensure_ascii=False, separators=(",", ":"))
|
||
config_b64 = base64.b64encode(config_json.encode("utf-8")).decode("utf-8")
|
||
|
||
return JSONResponse(content={
|
||
"json": config_json,
|
||
"base64": config_b64,
|
||
})
|