Files
ds2api/routes/admin/vercel.py

275 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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,
})