mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 09:13:23 +08:00
Fix channel draft persistence
This commit is contained in:
@@ -241,6 +241,32 @@ func (s *Server) effectiveWeixinRuntime(persisted cfgpkg.WeixinConfig) (cfgpkg.W
|
||||
return cloneWeixinConfig(persisted), s.weixinChannel, false
|
||||
}
|
||||
|
||||
func (s *Server) ensureWeixinRuntimeForLogin(persisted cfgpkg.WeixinConfig) (cfgpkg.WeixinConfig, *channels.WeixinChannel, bool, error) {
|
||||
s.draftMu.Lock()
|
||||
defer s.draftMu.Unlock()
|
||||
if s.channelDrafts.Weixin != nil {
|
||||
s.syncWeixinDraftLocked()
|
||||
effective := cloneWeixinConfig(*s.channelDrafts.Weixin)
|
||||
return effective, s.channelDrafts.weixinRuntime, true, nil
|
||||
}
|
||||
if s.weixinChannel != nil {
|
||||
return cloneWeixinConfig(persisted), s.weixinChannel, false, nil
|
||||
}
|
||||
|
||||
bootstrap := cloneWeixinConfig(persisted)
|
||||
bootstrap.Enabled = true
|
||||
if strings.TrimSpace(bootstrap.BaseURL) == "" {
|
||||
bootstrap.BaseURL = "https://ilinkai.weixin.qq.com"
|
||||
}
|
||||
s.channelDrafts.Weixin = &bootstrap
|
||||
if err := s.replaceWeixinDraftRuntimeLocked(&bootstrap); err != nil {
|
||||
return cloneWeixinConfig(persisted), nil, true, err
|
||||
}
|
||||
s.syncWeixinDraftLocked()
|
||||
effective := cloneWeixinConfig(*s.channelDrafts.Weixin)
|
||||
return effective, s.channelDrafts.weixinRuntime, true, nil
|
||||
}
|
||||
|
||||
func (s *Server) currentChannelDraftPayload(cfg *cfgpkg.Config, channel string) map[string]interface{} {
|
||||
channel = strings.ToLower(strings.TrimSpace(channel))
|
||||
payload := map[string]interface{}{
|
||||
@@ -765,7 +791,12 @@ func (s *Server) saveWebUIConfig(r *http.Request) error {
|
||||
if err := json.NewDecoder(r.Body).Decode(cfg); err != nil {
|
||||
return fmt.Errorf("decode config: %w", err)
|
||||
}
|
||||
return s.persistWebUIConfig(cfg)
|
||||
s.applyChannelDrafts(cfg)
|
||||
if err := s.persistWebUIConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
s.clearChannelDrafts()
|
||||
return nil
|
||||
case "normalized":
|
||||
cfg, err := cfgpkg.LoadConfig(s.configPath)
|
||||
if err != nil {
|
||||
@@ -776,7 +807,12 @@ func (s *Server) saveWebUIConfig(r *http.Request) error {
|
||||
return fmt.Errorf("decode normalized config: %w", err)
|
||||
}
|
||||
cfg.ApplyNormalizedView(view)
|
||||
return s.persistWebUIConfig(cfg)
|
||||
s.applyChannelDrafts(cfg)
|
||||
if err := s.persistWebUIConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
s.clearChannelDrafts()
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported config mode: %s", mode)
|
||||
}
|
||||
@@ -1597,7 +1633,11 @@ func (s *Server) handleWebUIWeixinLoginStart(w http.ResponseWriter, r *http.Requ
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, ch, _ := s.effectiveWeixinRuntime(cfg.Channels.Weixin)
|
||||
_, ch, _, err := s.ensureWeixinRuntimeForLogin(cfg.Channels.Weixin)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
if ch == nil {
|
||||
http.Error(w, "weixin channel unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
|
||||
@@ -234,6 +234,103 @@ func TestHandleWebUIWeixinStatusReflectsDraftRuntime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWebUIConfigPostPersistsChannelDrafts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
cfgPath := filepath.Join(tmp, "config.json")
|
||||
cfg := cfgpkg.DefaultConfig()
|
||||
cfg.Channels.Weixin.Enabled = false
|
||||
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
||||
t.Fatalf("save config: %v", err)
|
||||
}
|
||||
|
||||
srv := NewServer("127.0.0.1", 0, "")
|
||||
srv.SetConfigPath(cfgPath)
|
||||
srv.SetMessageBus(bus.NewMessageBus())
|
||||
srv.SetConfigAfterHook(func(forceRuntimeReload bool) error { return nil })
|
||||
|
||||
draftReq := httptest.NewRequest(http.MethodPost, "/api/channels/draft", strings.NewReader(`{"channel":"weixin","config":{"enabled":true,"base_url":"https://ilinkai.weixin.qq.com"}}`))
|
||||
draftReq.Header.Set("Content-Type", "application/json")
|
||||
draftRec := httptest.NewRecorder()
|
||||
srv.handleWebUIChannelDraft(draftRec, draftReq)
|
||||
if draftRec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 from weixin draft save, got %d: %s", draftRec.Code, draftRec.Body.String())
|
||||
}
|
||||
|
||||
saveReq := httptest.NewRequest(http.MethodPost, "/api/config", strings.NewReader(`{"gateway":{"host":"127.0.0.1","port":7788,"token":"abc"},"logging":{"enabled":false,"persist":false,"level":"debug","file":"logs/app.log","format":"text"},"models":{"providers":{"openai":{"api_base":"https://api.openai.com/v1","auth":"bearer","api_key":"secret","models":["gpt-5"],"timeout_sec":120}}},"tools":{"shell":{"enabled":true},"mcp":{"enabled":false}},"agents":{"defaults":{"model":{"primary":"openai/gpt-5"},"max_tool_iterations":10,"execution":{"run_state_ttl_seconds":3600,"run_state_max":128,"tool_parallel_safe_names":[],"tool_max_parallel_calls":4}},"router":{"enabled":false,"policy":{"intent_max_input_chars":2000,"max_rounds_without_user":3}},"subagents":{}},"channels":{"telegram":{"enabled":true,"token":"bot-token"}},"cron":{"enabled":false},"sentinel":{"enabled":false}}`))
|
||||
saveReq.Header.Set("Content-Type", "application/json")
|
||||
saveRec := httptest.NewRecorder()
|
||||
srv.handleWebUIConfig(saveRec, saveReq)
|
||||
if saveRec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 from config save, got %d: %s", saveRec.Code, saveRec.Body.String())
|
||||
}
|
||||
|
||||
updated, err := cfgpkg.LoadConfig(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("reload config: %v", err)
|
||||
}
|
||||
if !updated.Channels.Weixin.Enabled {
|
||||
t.Fatalf("expected weixin enabled after config save with drafts")
|
||||
}
|
||||
|
||||
srv.draftMu.Lock()
|
||||
defer srv.draftMu.Unlock()
|
||||
if srv.channelDrafts.Weixin != nil {
|
||||
t.Fatalf("expected weixin draft cleared after config save")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWebUIWeixinLoginStartBootstrapsDraftRuntime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/ilink/bot/get_bot_qrcode" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"qrcode": "wx-qr",
|
||||
"qrcode_img_content": "wx-qr-img",
|
||||
})
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
cfgPath := filepath.Join(tmp, "config.json")
|
||||
cfg := cfgpkg.DefaultConfig()
|
||||
cfg.Channels.Weixin.Enabled = false
|
||||
cfg.Channels.Weixin.BaseURL = upstream.URL
|
||||
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
||||
t.Fatalf("save config: %v", err)
|
||||
}
|
||||
|
||||
srv := NewServer("127.0.0.1", 0, "")
|
||||
srv.SetConfigPath(cfgPath)
|
||||
srv.SetMessageBus(bus.NewMessageBus())
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/weixin/login/start", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.handleWebUIWeixinLoginStart(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 from login start, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if payload["draft_dirty"] != true {
|
||||
t.Fatalf("expected draft_dirty=true, got %#v", payload["draft_dirty"])
|
||||
}
|
||||
if payload["config_enabled"] != false {
|
||||
t.Fatalf("expected config_enabled=false, got %#v", payload["config_enabled"])
|
||||
}
|
||||
if payload["runtime_enabled"] != true {
|
||||
t.Fatalf("expected runtime_enabled=true, got %#v", payload["runtime_enabled"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithCORSEchoesPreflightHeaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user