mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-16 22:25:15 +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:
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -204,38 +203,191 @@ func (h *Handler) batchImport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
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{"json": jsonStr, "base64": b64})
|
||||
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()
|
||||
syncable := map[string]any{"keys": snap.Keys, "accounts": []map[string]any{}}
|
||||
accounts := make([]map[string]any, 0, len(snap.Accounts))
|
||||
for _, a := range snap.Accounts {
|
||||
m := map[string]any{}
|
||||
if a.Email != "" {
|
||||
m["email"] = a.Email
|
||||
}
|
||||
if a.Mobile != "" {
|
||||
m["mobile"] = a.Mobile
|
||||
}
|
||||
if a.Password != "" {
|
||||
m["password"] = a.Password
|
||||
}
|
||||
accounts = append(accounts, m)
|
||||
}
|
||||
sort.Slice(accounts, func(i, j int) bool {
|
||||
ai := fmt.Sprintf("%v%v", accounts[i]["email"], accounts[i]["mobile"])
|
||||
aj := fmt.Sprintf("%v%v", accounts[j]["email"], accounts[j]["mobile"])
|
||||
return ai < aj
|
||||
})
|
||||
syncable["accounts"] = accounts
|
||||
b, _ := json.Marshal(syncable)
|
||||
snap := h.Store.Snapshot().Clone()
|
||||
snap.VercelSyncHash = ""
|
||||
snap.VercelSyncTime = 0
|
||||
b, _ := json.Marshal(snap)
|
||||
sum := md5.Sum(b)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user