mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-16 06:05:07 +08:00
feat: Implement admin settings UI, enhance admin authentication with password hashing, and add new streaming runtime logic for Claude and OpenAI adapters with extensive compatibility tests.
This commit is contained in:
@@ -3,8 +3,8 @@ package admin
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -19,6 +19,62 @@ func (h *Handler) syncVercel(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid json"})
|
||||
return
|
||||
}
|
||||
opts, err := parseVercelSyncOptions(req)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
validated, failed := h.validateAccountsForVercelSync(r.Context(), opts.AutoValidate)
|
||||
_, cfgB64, err := h.Store.ExportJSONAndBase64()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
params := buildVercelParams(opts.TeamID)
|
||||
headers := map[string]string{"Authorization": "Bearer " + opts.VercelToken}
|
||||
|
||||
envResp, status, err := vercelRequest(r.Context(), client, http.MethodGet, "https://api.vercel.com/v9/projects/"+opts.ProjectID+"/env", params, headers, nil)
|
||||
if err != nil || status != http.StatusOK {
|
||||
writeJSON(w, statusOr(status, http.StatusInternalServerError), map[string]any{"detail": "获取环境变量失败"})
|
||||
return
|
||||
}
|
||||
envs, _ := envResp["envs"].([]any)
|
||||
status, err = upsertVercelEnv(r.Context(), client, opts.ProjectID, params, headers, envs, "DS2API_CONFIG_JSON", cfgB64)
|
||||
if err != nil || (status != http.StatusOK && status != http.StatusCreated) {
|
||||
writeJSON(w, statusOr(status, http.StatusInternalServerError), map[string]any{"detail": "更新环境变量失败"})
|
||||
return
|
||||
}
|
||||
savedCreds := h.saveVercelProjectCredentials(r.Context(), client, opts, params, headers, envs)
|
||||
manual, deployURL := triggerVercelDeployment(r.Context(), client, opts.ProjectID, params, headers)
|
||||
_ = h.Store.SetVercelSync(h.computeSyncHash(), time.Now().Unix())
|
||||
result := map[string]any{"success": true, "validated_accounts": validated}
|
||||
if manual {
|
||||
result["message"] = "配置已同步到 Vercel,请手动触发重新部署"
|
||||
result["manual_deploy_required"] = true
|
||||
} else {
|
||||
result["message"] = "配置已同步,正在重新部署..."
|
||||
result["deployment_url"] = deployURL
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
result["failed_accounts"] = failed
|
||||
}
|
||||
if len(savedCreds) > 0 {
|
||||
result["saved_credentials"] = savedCreds
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
type vercelSyncOptions struct {
|
||||
VercelToken string
|
||||
ProjectID string
|
||||
TeamID string
|
||||
AutoValidate bool
|
||||
SaveCreds bool
|
||||
UsePreconfig bool
|
||||
}
|
||||
|
||||
func parseVercelSyncOptions(req map[string]any) (vercelSyncOptions, error) {
|
||||
vercelToken, _ := req["vercel_token"].(string)
|
||||
projectID, _ := req["project_id"].(string)
|
||||
teamID, _ := req["team_id"].(string)
|
||||
@@ -40,108 +96,117 @@ func (h *Handler) syncVercel(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.TrimSpace(teamID) == "" {
|
||||
teamID = strings.TrimSpace(os.Getenv("VERCEL_TEAM_ID"))
|
||||
}
|
||||
vercelToken = strings.TrimSpace(vercelToken)
|
||||
projectID = strings.TrimSpace(projectID)
|
||||
teamID = strings.TrimSpace(teamID)
|
||||
if vercelToken == "" || projectID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "需要 Vercel Token 和 Project ID"})
|
||||
return
|
||||
return vercelSyncOptions{}, fmt.Errorf("需要 Vercel Token 和 Project ID")
|
||||
}
|
||||
return vercelSyncOptions{
|
||||
VercelToken: vercelToken,
|
||||
ProjectID: projectID,
|
||||
TeamID: teamID,
|
||||
AutoValidate: autoValidate,
|
||||
SaveCreds: saveCreds,
|
||||
UsePreconfig: usePreconfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildVercelParams(teamID string) url.Values {
|
||||
params := url.Values{}
|
||||
if strings.TrimSpace(teamID) != "" {
|
||||
params.Set("teamId", strings.TrimSpace(teamID))
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func (h *Handler) validateAccountsForVercelSync(ctx context.Context, enabled bool) (int, []string) {
|
||||
if !enabled {
|
||||
return 0, nil
|
||||
}
|
||||
validated, failed := 0, []string{}
|
||||
if autoValidate {
|
||||
for _, acc := range h.Store.Snapshot().Accounts {
|
||||
if strings.TrimSpace(acc.Token) != "" {
|
||||
continue
|
||||
}
|
||||
token, err := h.DS.Login(r.Context(), acc)
|
||||
if err != nil {
|
||||
failed = append(failed, acc.Identifier())
|
||||
} else {
|
||||
validated++
|
||||
_ = h.Store.UpdateAccountToken(acc.Identifier(), token)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
for _, acc := range h.Store.Snapshot().Accounts {
|
||||
if strings.TrimSpace(acc.Token) != "" {
|
||||
continue
|
||||
}
|
||||
token, err := h.DS.Login(ctx, acc)
|
||||
if err != nil {
|
||||
failed = append(failed, acc.Identifier())
|
||||
} else {
|
||||
validated++
|
||||
_ = h.Store.UpdateAccountToken(acc.Identifier(), token)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return validated, failed
|
||||
}
|
||||
|
||||
cfgJSON, _, err := h.Store.ExportJSONAndBase64()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
func upsertVercelEnv(ctx context.Context, client *http.Client, projectID string, params url.Values, headers map[string]string, envs []any, key, value string) (int, error) {
|
||||
existingID := findEnvID(envs, key)
|
||||
if existingID != "" {
|
||||
_, status, err := vercelRequest(ctx, client, http.MethodPatch, "https://api.vercel.com/v9/projects/"+projectID+"/env/"+existingID, params, headers, map[string]any{"value": value})
|
||||
return status, err
|
||||
}
|
||||
cfgB64 := base64.StdEncoding.EncodeToString([]byte(cfgJSON))
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
params := url.Values{}
|
||||
if teamID != "" {
|
||||
params.Set("teamId", teamID)
|
||||
_, status, err := vercelRequest(ctx, client, http.MethodPost, "https://api.vercel.com/v10/projects/"+projectID+"/env", params, headers, map[string]any{
|
||||
"key": key,
|
||||
"value": value,
|
||||
"type": "encrypted",
|
||||
"target": []string{"production", "preview"},
|
||||
})
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (h *Handler) saveVercelProjectCredentials(ctx context.Context, client *http.Client, opts vercelSyncOptions, params url.Values, headers map[string]string, envs []any) []string {
|
||||
if !opts.SaveCreds || opts.UsePreconfig {
|
||||
return nil
|
||||
}
|
||||
headers := map[string]string{"Authorization": "Bearer " + vercelToken}
|
||||
envResp, status, err := vercelRequest(r.Context(), client, http.MethodGet, "https://api.vercel.com/v9/projects/"+projectID+"/env", params, headers, nil)
|
||||
if err != nil || status != http.StatusOK {
|
||||
writeJSON(w, statusOr(status, http.StatusInternalServerError), map[string]any{"detail": "获取环境变量失败"})
|
||||
return
|
||||
saved := []string{}
|
||||
creds := [][2]string{{"VERCEL_TOKEN", opts.VercelToken}, {"VERCEL_PROJECT_ID", opts.ProjectID}}
|
||||
if opts.TeamID != "" {
|
||||
creds = append(creds, [2]string{"VERCEL_TEAM_ID", opts.TeamID})
|
||||
}
|
||||
envs, _ := envResp["envs"].([]any)
|
||||
existingEnvID := findEnvID(envs, "DS2API_CONFIG_JSON")
|
||||
if existingEnvID != "" {
|
||||
_, status, err = vercelRequest(r.Context(), client, http.MethodPatch, "https://api.vercel.com/v9/projects/"+projectID+"/env/"+existingEnvID, params, headers, map[string]any{"value": cfgB64})
|
||||
} else {
|
||||
_, status, err = vercelRequest(r.Context(), client, http.MethodPost, "https://api.vercel.com/v10/projects/"+projectID+"/env", params, headers, map[string]any{"key": "DS2API_CONFIG_JSON", "value": cfgB64, "type": "encrypted", "target": []string{"production", "preview"}})
|
||||
}
|
||||
if err != nil || (status != http.StatusOK && status != http.StatusCreated) {
|
||||
writeJSON(w, statusOr(status, http.StatusInternalServerError), map[string]any{"detail": "更新环境变量失败"})
|
||||
return
|
||||
}
|
||||
savedCreds := []string{}
|
||||
if saveCreds && !usePreconfig {
|
||||
creds := [][2]string{{"VERCEL_TOKEN", vercelToken}, {"VERCEL_PROJECT_ID", projectID}}
|
||||
if teamID != "" {
|
||||
creds = append(creds, [2]string{"VERCEL_TEAM_ID", teamID})
|
||||
}
|
||||
for _, kv := range creds {
|
||||
id := findEnvID(envs, kv[0])
|
||||
if id != "" {
|
||||
_, status, _ = vercelRequest(r.Context(), client, http.MethodPatch, "https://api.vercel.com/v9/projects/"+projectID+"/env/"+id, params, headers, map[string]any{"value": kv[1]})
|
||||
} else {
|
||||
_, status, _ = vercelRequest(r.Context(), client, http.MethodPost, "https://api.vercel.com/v10/projects/"+projectID+"/env", params, headers, map[string]any{"key": kv[0], "value": kv[1], "type": "encrypted", "target": []string{"production", "preview"}})
|
||||
}
|
||||
if status == http.StatusOK || status == http.StatusCreated {
|
||||
savedCreds = append(savedCreds, kv[0])
|
||||
}
|
||||
for _, kv := range creds {
|
||||
status, _ := upsertVercelEnv(ctx, client, opts.ProjectID, params, headers, envs, kv[0], kv[1])
|
||||
if status == http.StatusOK || status == http.StatusCreated {
|
||||
saved = append(saved, kv[0])
|
||||
}
|
||||
}
|
||||
projectResp, status, _ := vercelRequest(r.Context(), client, http.MethodGet, "https://api.vercel.com/v9/projects/"+projectID, params, headers, nil)
|
||||
manual := true
|
||||
deployURL := ""
|
||||
if status == http.StatusOK {
|
||||
if link, ok := projectResp["link"].(map[string]any); ok {
|
||||
if linkType, _ := link["type"].(string); linkType == "github" {
|
||||
repoID := intFrom(link["repoId"])
|
||||
ref, _ := link["productionBranch"].(string)
|
||||
if ref == "" {
|
||||
ref = "main"
|
||||
}
|
||||
depResp, depStatus, _ := vercelRequest(r.Context(), client, http.MethodPost, "https://api.vercel.com/v13/deployments", params, headers, map[string]any{"name": projectID, "project": projectID, "target": "production", "gitSource": map[string]any{"type": "github", "repoId": repoID, "ref": ref}})
|
||||
if depStatus == http.StatusOK || depStatus == http.StatusCreated {
|
||||
deployURL, _ = depResp["url"].(string)
|
||||
manual = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return saved
|
||||
}
|
||||
|
||||
func triggerVercelDeployment(ctx context.Context, client *http.Client, projectID string, params url.Values, headers map[string]string) (bool, string) {
|
||||
projectResp, status, _ := vercelRequest(ctx, client, http.MethodGet, "https://api.vercel.com/v9/projects/"+projectID, params, headers, nil)
|
||||
if status != http.StatusOK {
|
||||
return true, ""
|
||||
}
|
||||
_ = h.Store.SetVercelSync(h.computeSyncHash(), time.Now().Unix())
|
||||
result := map[string]any{"success": true, "validated_accounts": validated}
|
||||
if manual {
|
||||
result["message"] = "配置已同步到 Vercel,请手动触发重新部署"
|
||||
result["manual_deploy_required"] = true
|
||||
} else {
|
||||
result["message"] = "配置已同步,正在重新部署..."
|
||||
result["deployment_url"] = deployURL
|
||||
link, ok := projectResp["link"].(map[string]any)
|
||||
if !ok {
|
||||
return true, ""
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
result["failed_accounts"] = failed
|
||||
linkType, _ := link["type"].(string)
|
||||
if linkType != "github" {
|
||||
return true, ""
|
||||
}
|
||||
if len(savedCreds) > 0 {
|
||||
result["saved_credentials"] = savedCreds
|
||||
repoID := intFrom(link["repoId"])
|
||||
ref, _ := link["productionBranch"].(string)
|
||||
if ref == "" {
|
||||
ref = "main"
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
depResp, depStatus, _ := vercelRequest(ctx, client, http.MethodPost, "https://api.vercel.com/v13/deployments", params, headers, map[string]any{
|
||||
"name": projectID,
|
||||
"project": projectID,
|
||||
"target": "production",
|
||||
"gitSource": map[string]any{
|
||||
"type": "github",
|
||||
"repoId": repoID,
|
||||
"ref": ref,
|
||||
},
|
||||
})
|
||||
if depStatus != http.StatusOK && depStatus != http.StatusCreated {
|
||||
return true, ""
|
||||
}
|
||||
deployURL, _ := depResp["url"].(string)
|
||||
return false, deployURL
|
||||
}
|
||||
|
||||
func (h *Handler) vercelStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user