mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
394 lines
10 KiB
Go
394 lines
10 KiB
Go
package admin
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"ds2api/internal/config"
|
|
)
|
|
|
|
func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
|
|
snap := h.Store.Snapshot()
|
|
safe := map[string]any{
|
|
"keys": snap.Keys,
|
|
"accounts": []map[string]any{},
|
|
"claude_mapping": func() map[string]string {
|
|
if len(snap.ClaudeMapping) > 0 {
|
|
return snap.ClaudeMapping
|
|
}
|
|
return snap.ClaudeModelMap
|
|
}(),
|
|
}
|
|
accounts := make([]map[string]any, 0, len(snap.Accounts))
|
|
for _, acc := range snap.Accounts {
|
|
token := strings.TrimSpace(acc.Token)
|
|
preview := ""
|
|
if token != "" {
|
|
if len(token) > 20 {
|
|
preview = token[:20] + "..."
|
|
} else {
|
|
preview = token
|
|
}
|
|
}
|
|
accounts = append(accounts, map[string]any{
|
|
"identifier": acc.Identifier(),
|
|
"email": acc.Email,
|
|
"mobile": acc.Mobile,
|
|
"has_password": strings.TrimSpace(acc.Password) != "",
|
|
"has_token": token != "",
|
|
"token_preview": preview,
|
|
})
|
|
}
|
|
safe["accounts"] = accounts
|
|
writeJSON(w, http.StatusOK, safe)
|
|
}
|
|
|
|
func (h *Handler) updateConfig(w http.ResponseWriter, r *http.Request) {
|
|
var req map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid json"})
|
|
return
|
|
}
|
|
old := h.Store.Snapshot()
|
|
err := h.Store.Update(func(c *config.Config) error {
|
|
if keys, ok := toStringSlice(req["keys"]); ok {
|
|
c.Keys = keys
|
|
}
|
|
if accountsRaw, ok := req["accounts"].([]any); ok {
|
|
existing := map[string]config.Account{}
|
|
for _, a := range old.Accounts {
|
|
existing[a.Identifier()] = a
|
|
}
|
|
accounts := make([]config.Account, 0, len(accountsRaw))
|
|
for _, item := range accountsRaw {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
acc := toAccount(m)
|
|
id := acc.Identifier()
|
|
if prev, ok := existing[id]; ok {
|
|
if strings.TrimSpace(acc.Password) == "" {
|
|
acc.Password = prev.Password
|
|
}
|
|
if strings.TrimSpace(acc.Token) == "" {
|
|
acc.Token = prev.Token
|
|
}
|
|
}
|
|
accounts = append(accounts, acc)
|
|
}
|
|
c.Accounts = accounts
|
|
}
|
|
if m, ok := req["claude_mapping"].(map[string]any); ok {
|
|
newMap := map[string]string{}
|
|
for k, v := range m {
|
|
newMap[k] = fmt.Sprintf("%v", v)
|
|
}
|
|
c.ClaudeMapping = newMap
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
h.Pool.Reset()
|
|
writeJSON(w, http.StatusOK, map[string]any{"success": true, "message": "配置已更新"})
|
|
}
|
|
|
|
func (h *Handler) addKey(w http.ResponseWriter, r *http.Request) {
|
|
var req map[string]any
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
key, _ := req["key"].(string)
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "Key 不能为空"})
|
|
return
|
|
}
|
|
err := h.Store.Update(func(c *config.Config) error {
|
|
for _, k := range c.Keys {
|
|
if k == key {
|
|
return fmt.Errorf("Key 已存在")
|
|
}
|
|
}
|
|
c.Keys = append(c.Keys, key)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_keys": len(h.Store.Snapshot().Keys)})
|
|
}
|
|
|
|
func (h *Handler) deleteKey(w http.ResponseWriter, r *http.Request) {
|
|
key := chi.URLParam(r, "key")
|
|
err := h.Store.Update(func(c *config.Config) error {
|
|
idx := -1
|
|
for i, k := range c.Keys {
|
|
if k == key {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx < 0 {
|
|
return fmt.Errorf("Key 不存在")
|
|
}
|
|
c.Keys = append(c.Keys[:idx], c.Keys[idx+1:]...)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_keys": len(h.Store.Snapshot().Keys)})
|
|
}
|
|
|
|
func (h *Handler) batchImport(w http.ResponseWriter, r *http.Request) {
|
|
var req map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "无效的 JSON 格式"})
|
|
return
|
|
}
|
|
importedKeys, importedAccounts := 0, 0
|
|
err := h.Store.Update(func(c *config.Config) error {
|
|
if keys, ok := req["keys"].([]any); ok {
|
|
existing := map[string]bool{}
|
|
for _, k := range c.Keys {
|
|
existing[k] = true
|
|
}
|
|
for _, k := range keys {
|
|
key := strings.TrimSpace(fmt.Sprintf("%v", k))
|
|
if key == "" || existing[key] {
|
|
continue
|
|
}
|
|
c.Keys = append(c.Keys, key)
|
|
existing[key] = true
|
|
importedKeys++
|
|
}
|
|
}
|
|
if accounts, ok := req["accounts"].([]any); ok {
|
|
existing := map[string]bool{}
|
|
for _, a := range c.Accounts {
|
|
existing[a.Identifier()] = true
|
|
}
|
|
for _, item := range accounts {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
acc := toAccount(m)
|
|
id := acc.Identifier()
|
|
if id == "" || existing[id] {
|
|
continue
|
|
}
|
|
c.Accounts = append(c.Accounts, acc)
|
|
existing[id] = true
|
|
importedAccounts++
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
h.Pool.Reset()
|
|
writeJSON(w, http.StatusOK, map[string]any{"success": true, "imported_keys": importedKeys, "imported_accounts": importedAccounts})
|
|
}
|
|
|
|
func (h *Handler) exportConfig(w http.ResponseWriter, _ *http.Request) {
|
|
h.configExport(w, nil)
|
|
}
|
|
|
|
func (h *Handler) configExport(w http.ResponseWriter, _ *http.Request) {
|
|
snap := h.Store.Snapshot()
|
|
jsonStr, b64, err := h.Store.ExportJSONAndBase64()
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"success": true,
|
|
"config": snap,
|
|
"json": jsonStr,
|
|
"base64": b64,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) configImport(w http.ResponseWriter, r *http.Request) {
|
|
var req map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid json"})
|
|
return
|
|
}
|
|
|
|
mode := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("mode")))
|
|
if mode == "" {
|
|
mode = strings.TrimSpace(strings.ToLower(fieldString(req, "mode")))
|
|
}
|
|
if mode == "" {
|
|
mode = "merge"
|
|
}
|
|
if mode != "merge" && mode != "replace" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "mode must be merge or replace"})
|
|
return
|
|
}
|
|
|
|
payload := req
|
|
if raw, ok := req["config"].(map[string]any); ok && len(raw) > 0 {
|
|
payload = raw
|
|
}
|
|
rawJSON, err := json.Marshal(payload)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid config payload"})
|
|
return
|
|
}
|
|
var incoming config.Config
|
|
if err := json.Unmarshal(rawJSON, &incoming); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
|
|
importedKeys, importedAccounts := 0, 0
|
|
err = h.Store.Update(func(c *config.Config) error {
|
|
next := c.Clone()
|
|
if mode == "replace" {
|
|
next = incoming.Clone()
|
|
next.VercelSyncHash = c.VercelSyncHash
|
|
next.VercelSyncTime = c.VercelSyncTime
|
|
importedKeys = len(next.Keys)
|
|
importedAccounts = len(next.Accounts)
|
|
} else {
|
|
existingKeys := map[string]struct{}{}
|
|
for _, k := range next.Keys {
|
|
existingKeys[k] = struct{}{}
|
|
}
|
|
for _, k := range incoming.Keys {
|
|
key := strings.TrimSpace(k)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
if _, ok := existingKeys[key]; ok {
|
|
continue
|
|
}
|
|
existingKeys[key] = struct{}{}
|
|
next.Keys = append(next.Keys, key)
|
|
importedKeys++
|
|
}
|
|
|
|
existingAccounts := map[string]struct{}{}
|
|
for _, acc := range next.Accounts {
|
|
existingAccounts[acc.Identifier()] = struct{}{}
|
|
}
|
|
for _, acc := range incoming.Accounts {
|
|
id := acc.Identifier()
|
|
if id == "" {
|
|
continue
|
|
}
|
|
if _, ok := existingAccounts[id]; ok {
|
|
continue
|
|
}
|
|
existingAccounts[id] = struct{}{}
|
|
next.Accounts = append(next.Accounts, acc)
|
|
importedAccounts++
|
|
}
|
|
|
|
if len(incoming.ClaudeMapping) > 0 {
|
|
if next.ClaudeMapping == nil {
|
|
next.ClaudeMapping = map[string]string{}
|
|
}
|
|
for k, v := range incoming.ClaudeMapping {
|
|
next.ClaudeMapping[k] = v
|
|
}
|
|
}
|
|
if len(incoming.ClaudeModelMap) > 0 {
|
|
if next.ClaudeModelMap == nil {
|
|
next.ClaudeModelMap = map[string]string{}
|
|
}
|
|
for k, v := range incoming.ClaudeModelMap {
|
|
next.ClaudeModelMap[k] = v
|
|
}
|
|
}
|
|
|
|
if len(incoming.ModelAliases) > 0 {
|
|
if next.ModelAliases == nil {
|
|
next.ModelAliases = map[string]string{}
|
|
}
|
|
for k, v := range incoming.ModelAliases {
|
|
next.ModelAliases[k] = v
|
|
}
|
|
}
|
|
if strings.TrimSpace(incoming.Toolcall.Mode) != "" {
|
|
next.Toolcall.Mode = incoming.Toolcall.Mode
|
|
}
|
|
if strings.TrimSpace(incoming.Toolcall.EarlyEmitConfidence) != "" {
|
|
next.Toolcall.EarlyEmitConfidence = incoming.Toolcall.EarlyEmitConfidence
|
|
}
|
|
if incoming.Responses.StoreTTLSeconds > 0 {
|
|
next.Responses.StoreTTLSeconds = incoming.Responses.StoreTTLSeconds
|
|
}
|
|
if strings.TrimSpace(incoming.Embeddings.Provider) != "" {
|
|
next.Embeddings.Provider = incoming.Embeddings.Provider
|
|
}
|
|
if strings.TrimSpace(incoming.Admin.PasswordHash) != "" {
|
|
next.Admin.PasswordHash = incoming.Admin.PasswordHash
|
|
}
|
|
if incoming.Admin.JWTExpireHours > 0 {
|
|
next.Admin.JWTExpireHours = incoming.Admin.JWTExpireHours
|
|
}
|
|
if incoming.Admin.JWTValidAfterUnix > 0 {
|
|
next.Admin.JWTValidAfterUnix = incoming.Admin.JWTValidAfterUnix
|
|
}
|
|
if incoming.Runtime.AccountMaxInflight > 0 {
|
|
next.Runtime.AccountMaxInflight = incoming.Runtime.AccountMaxInflight
|
|
}
|
|
if incoming.Runtime.AccountMaxQueue > 0 {
|
|
next.Runtime.AccountMaxQueue = incoming.Runtime.AccountMaxQueue
|
|
}
|
|
if incoming.Runtime.GlobalMaxInflight > 0 {
|
|
next.Runtime.GlobalMaxInflight = incoming.Runtime.GlobalMaxInflight
|
|
}
|
|
}
|
|
|
|
normalizeSettingsConfig(&next)
|
|
if err := validateSettingsConfig(next); err != nil {
|
|
return newRequestError(err.Error())
|
|
}
|
|
|
|
*c = next
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if detail, ok := requestErrorDetail(err); ok {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": detail})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
|
|
h.Pool.Reset()
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"success": true,
|
|
"mode": mode,
|
|
"imported_keys": importedKeys,
|
|
"imported_accounts": importedAccounts,
|
|
"message": "config imported",
|
|
})
|
|
}
|
|
|
|
func (h *Handler) computeSyncHash() string {
|
|
snap := h.Store.Snapshot().Clone()
|
|
snap.VercelSyncHash = ""
|
|
snap.VercelSyncTime = 0
|
|
b, _ := json.Marshal(snap)
|
|
sum := md5.Sum(b)
|
|
return fmt.Sprintf("%x", sum)
|
|
}
|