fix config anthropic model, add log request provider
This commit is contained in:
parent
e38e2a3a63
commit
ff7095e88e
14 changed files with 493 additions and 350 deletions
|
|
@ -51,9 +51,9 @@ func hiddenValues(key string, value map[string]any, ch config.ChannelsConfig) {
|
||||||
case "wecom":
|
case "wecom":
|
||||||
value["secret"] = ch.WeCom.Secret()
|
value["secret"] = ch.WeCom.Secret()
|
||||||
case "dingtalk":
|
case "dingtalk":
|
||||||
value["secret"] = ch.QQ.AppSecret()
|
|
||||||
case "qq":
|
|
||||||
value["secret"] = ch.DingTalk.ClientSecret()
|
value["secret"] = ch.DingTalk.ClientSecret()
|
||||||
|
case "qq":
|
||||||
|
value["secret"] = ch.QQ.AppSecret()
|
||||||
case "irc":
|
case "irc":
|
||||||
value["password"] = ch.IRC.Password()
|
value["password"] = ch.IRC.Password()
|
||||||
value["serv_password"] = ch.IRC.NickServPassword()
|
value["serv_password"] = ch.IRC.NickServPassword()
|
||||||
|
|
|
||||||
|
|
@ -1739,17 +1739,35 @@ func SaveConfig(path string, cfg *Config) error {
|
||||||
cfg.Version = CurrentVersion
|
cfg.Version = CurrentVersion
|
||||||
}
|
}
|
||||||
names := toNameIndex(cfg.ModelList)
|
names := toNameIndex(cfg.ModelList)
|
||||||
|
newModelList := make(map[string]ModelSecurityEntry)
|
||||||
|
|
||||||
for i, m := range cfg.ModelList {
|
for i, m := range cfg.ModelList {
|
||||||
|
if m.IsVirtual() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newName := names[i]
|
||||||
|
oldName := m.secModelName
|
||||||
|
|
||||||
if m.secDirty {
|
if m.secDirty {
|
||||||
if m.secModelName == "" {
|
newModelList[newName] = ModelSecurityEntry{APIKeys: m.apiKeys}
|
||||||
m.secModelName = names[i]
|
|
||||||
}
|
|
||||||
cfg.security.ModelList[m.secModelName] = ModelSecurityEntry{
|
|
||||||
APIKeys: m.apiKeys,
|
|
||||||
}
|
|
||||||
m.secDirty = false
|
m.secDirty = false
|
||||||
|
} else {
|
||||||
|
if oldName == "" {
|
||||||
|
oldName = newName
|
||||||
|
}
|
||||||
|
if entry, ok := cfg.security.ModelList[oldName]; ok {
|
||||||
|
newModelList[newName] = entry
|
||||||
|
} else {
|
||||||
|
// Fallback to storing plaintext if not found, will trigger encrypt later if enabled
|
||||||
|
newModelList[newName] = ModelSecurityEntry{APIKeys: m.apiKeys}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.secModelName = newName
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.security.ModelList = newModelList
|
||||||
if cfg.Channels.Pico.secDirty {
|
if cfg.Channels.Pico.secDirty {
|
||||||
cfg.security.Channels.Pico = &PicoSecurity{
|
cfg.security.Channels.Pico = &PicoSecurity{
|
||||||
Token: cfg.Channels.Pico.Token(),
|
Token: cfg.Channels.Pico.Token(),
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,16 @@ func saveSecurityConfig(securityPath string, sec *SecurityConfig) error {
|
||||||
return fileutil.WriteFileAtomic(securityPath, buf.Bytes(), 0o600)
|
return fileutil.WriteFileAtomic(securityPath, buf.Bytes(), 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MergeAndApplySecurity merges explicitly provided security tokens from JSON API payloads
|
||||||
|
// into the current configuration's security state, preventing them from being dropped
|
||||||
|
// when json.Unmarshal ignores the unexported Token fields.
|
||||||
|
func (c *Config) MergeAndApplySecurity(newer *SecurityConfig) error {
|
||||||
|
if newer == nil {
|
||||||
|
return c.ApplySecurity()
|
||||||
|
}
|
||||||
|
c.security = mergeSecurityConfig(c.security, newer)
|
||||||
|
return applySecurityConfig(c, c.security)
|
||||||
|
}
|
||||||
// mergeSecurityConfig merges two SecurityConfig instances, preferring non-empty values from 'newer'.
|
// mergeSecurityConfig merges two SecurityConfig instances, preferring non-empty values from 'newer'.
|
||||||
// This is used during config migration to preserve existing security data while adding new entries.
|
// This is used during config migration to preserve existing security data while adding new entries.
|
||||||
func mergeSecurityConfig(existing, newer *SecurityConfig) *SecurityConfig {
|
func mergeSecurityConfig(existing, newer *SecurityConfig) *SecurityConfig {
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,9 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error
|
||||||
return fmt.Errorf("error loading config: %w", err)
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = cfg.ApplySecurity(); err != nil {
|
||||||
|
return fmt.Errorf("error applying security config: %w", err)
|
||||||
|
}
|
||||||
logger.SetLevelFromString(cfg.Gateway.LogLevel)
|
logger.SetLevelFromString(cfg.Gateway.LogLevel)
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
|
|
@ -204,6 +207,11 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error
|
||||||
runningServices.reloading.Store(false)
|
runningServices.reloading.Store(false)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if err = newCfg.ApplySecurity(); err != nil {
|
||||||
|
logger.Errorf("Failed to apply security config: %v", err)
|
||||||
|
runningServices.reloading.Store(false)
|
||||||
|
continue
|
||||||
|
}
|
||||||
err = executeReload(ctx, agentLoop, newCfg, &provider, runningServices, msgBus, allowEmptyStartup)
|
err = executeReload(ctx, agentLoop, newCfg, &provider, runningServices, msgBus, allowEmptyStartup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Manual reload failed: %v", err)
|
logger.Errorf("Manual reload failed: %v", err)
|
||||||
|
|
@ -581,7 +589,11 @@ func setupConfigWatcherPolling(configPath string, debug bool) (chan *config.Conf
|
||||||
logger.Warn(" Using previous valid config")
|
logger.Warn(" Using previous valid config")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if err := newCfg.ApplySecurity(); err != nil {
|
||||||
|
logger.Errorf(" ⚠ Failed to apply security to new config: %v", err)
|
||||||
|
logger.Warn(" Using previous valid config")
|
||||||
|
continue
|
||||||
|
}
|
||||||
logger.Info("✓ Config file validated and loaded")
|
logger.Info("✓ Config file validated and loaded")
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers/common"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -59,9 +60,7 @@ func NewProviderWithTimeout(apiKey, apiBase string, timeoutSeconds int) *Provide
|
||||||
return &Provider{
|
return &Provider{
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
apiBase: baseURL,
|
apiBase: baseURL,
|
||||||
httpClient: &http.Client{
|
httpClient: common.NewHTTPClientWithTimeout("", timeout),
|
||||||
Timeout: timeout,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/auth"
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -36,9 +37,7 @@ type AntigravityProvider struct {
|
||||||
func NewAntigravityProvider() *AntigravityProvider {
|
func NewAntigravityProvider() *AntigravityProvider {
|
||||||
return &AntigravityProvider{
|
return &AntigravityProvider{
|
||||||
tokenSource: createAntigravityTokenSource(),
|
tokenSource: createAntigravityTokenSource(),
|
||||||
httpClient: &http.Client{
|
httpClient: common.NewHTTPClientWithTimeout("", 120*time.Second),
|
||||||
Timeout: 120 * time.Second,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -633,7 +632,7 @@ func FetchAntigravityProjectID(accessToken string) (string, error) {
|
||||||
req.Header.Set("User-Agent", antigravityUserAgent)
|
req.Header.Set("User-Agent", antigravityUserAgent)
|
||||||
req.Header.Set("X-Goog-Api-Client", antigravityXGoogClient)
|
req.Header.Set("X-Goog-Api-Client", antigravityXGoogClient)
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
client := common.NewHTTPClientWithTimeout("", 15*time.Second)
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -677,7 +676,7 @@ func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelIn
|
||||||
req.Header.Set("User-Agent", antigravityUserAgent)
|
req.Header.Set("User-Agent", antigravityUserAgent)
|
||||||
req.Header.Set("X-Goog-Api-Client", antigravityXGoogClient)
|
req.Header.Set("X-Goog-Api-Client", antigravityXGoogClient)
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
client := common.NewHTTPClientWithTimeout("", 15*time.Second)
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/auth"
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -35,6 +36,7 @@ func NewCodexProvider(token, accountID string) *CodexProvider {
|
||||||
option.WithAPIKey(token),
|
option.WithAPIKey(token),
|
||||||
option.WithHeader("originator", "codex_cli_rs"),
|
option.WithHeader("originator", "codex_cli_rs"),
|
||||||
option.WithHeader("OpenAI-Beta", "responses=experimental"),
|
option.WithHeader("OpenAI-Beta", "responses=experimental"),
|
||||||
|
option.WithHTTPClient(common.NewHTTPClientWithTimeout("", 0)),
|
||||||
}
|
}
|
||||||
if accountID != "" {
|
if accountID != "" {
|
||||||
opts = append(opts, option.WithHeader("Chatgpt-Account-Id", accountID))
|
opts = append(opts, option.WithHeader("Chatgpt-Account-Id", accountID))
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,19 @@ type (
|
||||||
|
|
||||||
const DefaultRequestTimeout = 120 * time.Second
|
const DefaultRequestTimeout = 120 * time.Second
|
||||||
|
|
||||||
// NewHTTPClient creates an *http.Client with an optional proxy and the default timeout.
|
// NewHTTPClient creates an *http.Client with an optional proxy, the default timeout, and debug logging installed.
|
||||||
func NewHTTPClient(proxy string) *http.Client {
|
func NewHTTPClient(proxy string) *http.Client {
|
||||||
client := &http.Client{
|
return NewHTTPClientWithTimeout(proxy, DefaultRequestTimeout)
|
||||||
Timeout: DefaultRequestTimeout,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewHTTPClientWithTimeout creates an *http.Client with an optional proxy, a custom timeout, and debug logging installed.
|
||||||
|
func NewHTTPClientWithTimeout(proxy string, timeout time.Duration) *http.Client {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseTransport http.RoundTripper = http.DefaultTransport
|
||||||
|
|
||||||
if proxy != "" {
|
if proxy != "" {
|
||||||
parsed, err := url.Parse(proxy)
|
parsed, err := url.Parse(proxy)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -50,10 +58,10 @@ func NewHTTPClient(proxy string) *http.Client {
|
||||||
if base, ok := http.DefaultTransport.(*http.Transport); ok {
|
if base, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||||
tr := base.Clone()
|
tr := base.Clone()
|
||||||
tr.Proxy = http.ProxyURL(parsed)
|
tr.Proxy = http.ProxyURL(parsed)
|
||||||
client.Transport = tr
|
baseTransport = tr
|
||||||
} else {
|
} else {
|
||||||
// Fallback: minimal transport if DefaultTransport is not *http.Transport.
|
// Fallback: minimal transport if DefaultTransport is not *http.Transport.
|
||||||
client.Transport = &http.Transport{
|
baseTransport = &http.Transport{
|
||||||
Proxy: http.ProxyURL(parsed),
|
Proxy: http.ProxyURL(parsed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +69,9 @@ func NewHTTPClient(proxy string) *http.Client {
|
||||||
log.Printf("common: invalid proxy URL %q: %v", proxy, err)
|
log.Printf("common: invalid proxy URL %q: %v", proxy, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap with debug logger
|
||||||
|
client.Transport = &LoggingRoundTripper{Proxied: baseTransport}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,13 @@ func TestNewHTTPClient_DefaultTimeout(t *testing.T) {
|
||||||
|
|
||||||
func TestNewHTTPClient_WithProxy(t *testing.T) {
|
func TestNewHTTPClient_WithProxy(t *testing.T) {
|
||||||
client := NewHTTPClient("http://127.0.0.1:8080")
|
client := NewHTTPClient("http://127.0.0.1:8080")
|
||||||
transport, ok := client.Transport.(*http.Transport)
|
lrt, ok := client.Transport.(*LoggingRoundTripper)
|
||||||
|
if !ok || lrt == nil {
|
||||||
|
t.Fatalf("expected *LoggingRoundTripper, got %T", client.Transport)
|
||||||
|
}
|
||||||
|
transport, ok := lrt.Proxied.(*http.Transport)
|
||||||
if !ok || transport == nil {
|
if !ok || transport == nil {
|
||||||
t.Fatalf("expected http.Transport with proxy, got %T", client.Transport)
|
t.Fatalf("expected http.Transport with proxy, got %T", lrt.Proxied)
|
||||||
}
|
}
|
||||||
req := &http.Request{URL: &url.URL{Scheme: "https", Host: "api.example.com"}}
|
req := &http.Request{URL: &url.URL{Scheme: "https", Host: "api.example.com"}}
|
||||||
gotProxy, err := transport.Proxy(req)
|
gotProxy, err := transport.Proxy(req)
|
||||||
|
|
@ -38,8 +42,12 @@ func TestNewHTTPClient_WithProxy(t *testing.T) {
|
||||||
|
|
||||||
func TestNewHTTPClient_NoProxy(t *testing.T) {
|
func TestNewHTTPClient_NoProxy(t *testing.T) {
|
||||||
client := NewHTTPClient("")
|
client := NewHTTPClient("")
|
||||||
if client.Transport != nil {
|
lrt, ok := client.Transport.(*LoggingRoundTripper)
|
||||||
t.Errorf("expected nil transport without proxy, got %T", client.Transport)
|
if !ok || lrt == nil {
|
||||||
|
t.Fatalf("expected *LoggingRoundTripper, got %T", client.Transport)
|
||||||
|
}
|
||||||
|
if lrt.Proxied != http.DefaultTransport {
|
||||||
|
t.Errorf("expected Proxied to be http.DefaultTransport without proxy, got %T", lrt.Proxied)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
65
pkg/providers/common/http_logger.go
Normal file
65
pkg/providers/common/http_logger.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoggingRoundTripper wraps an http.RoundTripper and logs HTTP requests and responses
|
||||||
|
// when PicoClaw is configured in DEBUG mode.
|
||||||
|
type LoggingRoundTripper struct {
|
||||||
|
Proxied http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lrt *LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
if logger.GetLevel() <= logger.DEBUG {
|
||||||
|
logger.DebugCF("http_client", "Req: "+req.Method+" "+req.URL.String(), nil)
|
||||||
|
|
||||||
|
// Log Headers (redact sensitive)
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for k, v := range req.Header {
|
||||||
|
lowerK := strings.ToLower(k)
|
||||||
|
if lowerK == "authorization" || lowerK == "api-key" || lowerK == "x-api-key" {
|
||||||
|
headers[k] = "[REDACTED]"
|
||||||
|
} else {
|
||||||
|
headers[k] = strings.Join(v, ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.DebugCF("http_client", "Req Headers", map[string]any{"headers": headers})
|
||||||
|
|
||||||
|
// Log Body
|
||||||
|
if req.Body != nil {
|
||||||
|
bodyBytes, err := io.ReadAll(req.Body)
|
||||||
|
if err == nil {
|
||||||
|
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
logger.DebugCF("http_client", "Req Body", map[string]any{"body": string(bodyBytes)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := lrt.Proxied.RoundTrip(req)
|
||||||
|
|
||||||
|
if logger.GetLevel() <= logger.DEBUG && res != nil {
|
||||||
|
logger.DebugCF("http_client", "Res Status: "+res.Status, nil)
|
||||||
|
|
||||||
|
// Optional: Log Response Body
|
||||||
|
if res.Body != nil {
|
||||||
|
bodyBytes, err := io.ReadAll(res.Body)
|
||||||
|
if err == nil {
|
||||||
|
res.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
// Truncate response body if too long to avoid flooding logs
|
||||||
|
respStr := string(bodyBytes)
|
||||||
|
if len(respStr) > 4000 {
|
||||||
|
respStr = respStr[:4000] + "... [TRUNCATED]"
|
||||||
|
}
|
||||||
|
logger.DebugCF("http_client", "Res Body", map[string]any{"body": respStr})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
@ -240,14 +240,10 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
if cfg.APIKey() == "" {
|
if cfg.APIKey() == "" {
|
||||||
return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model)
|
return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model)
|
||||||
}
|
}
|
||||||
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
return anthropicmessages.NewProviderWithTimeout(
|
||||||
cfg.APIKey(),
|
cfg.APIKey(),
|
||||||
apiBase,
|
apiBase,
|
||||||
cfg.Proxy,
|
|
||||||
cfg.MaxTokensField,
|
|
||||||
cfg.RequestTimeout,
|
cfg.RequestTimeout,
|
||||||
cfg.ExtraBody,
|
|
||||||
cfg.ExtraHeaders,
|
|
||||||
), modelID, nil
|
), modelID, nil
|
||||||
|
|
||||||
case "anthropic-messages":
|
case "anthropic-messages":
|
||||||
|
|
|
||||||
|
|
@ -519,9 +519,13 @@ func TestProvider_ProxyConfigured(t *testing.T) {
|
||||||
proxyURL := "http://127.0.0.1:8080"
|
proxyURL := "http://127.0.0.1:8080"
|
||||||
p := NewProvider("key", "https://example.com", proxyURL)
|
p := NewProvider("key", "https://example.com", proxyURL)
|
||||||
|
|
||||||
transport, ok := p.httpClient.Transport.(*http.Transport)
|
lrt, ok := p.httpClient.Transport.(*common.LoggingRoundTripper)
|
||||||
|
if !ok || lrt == nil {
|
||||||
|
t.Fatalf("expected *common.LoggingRoundTripper, got %T", p.httpClient.Transport)
|
||||||
|
}
|
||||||
|
transport, ok := lrt.Proxied.(*http.Transport)
|
||||||
if !ok || transport == nil {
|
if !ok || transport == nil {
|
||||||
t.Fatalf("expected http transport with proxy, got %T", p.httpClient.Transport)
|
t.Fatalf("expected http transport with proxy, got %T", lrt.Proxied)
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &http.Request{URL: &url.URL{Scheme: "https", Host: "api.example.com"}}
|
req := &http.Request{URL: &url.URL{Scheme: "https", Host: "api.example.com"}}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,14 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
cfg.SecurityCopyFrom(oldCfg)
|
cfg.SecurityCopyFrom(oldCfg)
|
||||||
|
|
||||||
|
// Intercept explicitly provided security tokens from JSON payload that json.Unmarshal drops.
|
||||||
|
var incomingSec config.SecurityConfig
|
||||||
|
if err := json.Unmarshal(body, &incomingSec); err == nil {
|
||||||
|
cfg.MergeAndApplySecurity(&incomingSec)
|
||||||
|
} else {
|
||||||
|
cfg.ApplySecurity()
|
||||||
|
}
|
||||||
|
|
||||||
if errs := validateConfig(&cfg); len(errs) > 0 {
|
if errs := validateConfig(&cfg); len(errs) > 0 {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
@ -154,12 +162,13 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore security fields (tokens/keys) from the loaded config before validation,
|
// Restore security fields from existing config and merge explicitly provided overrides.
|
||||||
// because private fields are lost during JSON round-trip.
|
|
||||||
newCfg.SecurityCopyFrom(cfg)
|
newCfg.SecurityCopyFrom(cfg)
|
||||||
if err := newCfg.ApplySecurity(); err != nil {
|
var incomingSec config.SecurityConfig
|
||||||
http.Error(w, fmt.Sprintf("Failed to apply security config: %v", err), http.StatusInternalServerError)
|
if err := json.Unmarshal(patchBody, &incomingSec); err == nil {
|
||||||
return
|
newCfg.MergeAndApplySecurity(&incomingSec)
|
||||||
|
} else {
|
||||||
|
newCfg.ApplySecurity()
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs := validateConfig(&newCfg); len(errs) > 0 {
|
if errs := validateConfig(&newCfg); len(errs) > 0 {
|
||||||
|
|
|
||||||
632
web/frontend/pnpm-lock.yaml
generated
632
web/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue