From 5237e3b59405c900c7d4178b9aeb303d0ea7a083 Mon Sep 17 00:00:00 2001 From: jianf <2198972886@qq.com> Date: Mon, 17 Aug 2026 12:00:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B3=A8=E5=86=8C=20PUT=20/api/manager/?= =?UTF-8?q?settings=20=E4=BD=BF=E8=AE=BE=E7=BD=AE=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server.go NewServeMux 按方法分发到 handleSettingsPut(原仅注册 GET,PUT 静默返回旧值) 新增 TestSettingsPutRoundTrip 回归测试;gofmt 清理历史格式问题 --- internal/httpapi/handlers.go | 3 --- internal/httpapi/server.go | 36 +++++++++++++++++------------ internal/httpapi/server_test.go | 41 +++++++++++++++++++++++++++++++++ internal/store/store.go | 4 ++-- plan.md | 4 ++-- 5 files changed, 66 insertions(+), 22 deletions(-) diff --git a/internal/httpapi/handlers.go b/internal/httpapi/handlers.go index 91b1897..0e8c3a5 100644 --- a/internal/httpapi/handlers.go +++ b/internal/httpapi/handlers.go @@ -257,8 +257,6 @@ func (h *Handler) handleProfile(w http.ResponseWriter, r *http.Request) { // ---- helpers ---- - - // tailFile returns the last maxBytes bytes of a file. func tailFile(path string, maxBytes int64) (string, error) { info, err := os.Stat(path) @@ -280,4 +278,3 @@ func tailFile(path string, maxBytes int64) (string, error) { } return string(buf), nil } - diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 0af37a3..c2417f9 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -61,7 +61,13 @@ func NewServeMux(h *Handler) (http.Handler, error) { // API routes (basic auth). mux.HandleFunc(apiPrefix+"/status", auth(h.handleStatus)) mux.HandleFunc(apiPrefix+"/canvas", auth(h.handleCanvasSave)) - mux.HandleFunc(apiPrefix+"/settings", auth(h.handleSettingsGet)) + mux.HandleFunc(apiPrefix+"/settings", auth(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut { + h.handleSettingsPut(w, r) + return + } + h.handleSettingsGet(w, r) + })) mux.HandleFunc(apiPrefix+"/binary/status", auth(h.handleBinaryStatus)) mux.HandleFunc(apiPrefix+"/binary/install", auth(h.handleBinaryInstall)) @@ -135,10 +141,10 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { settings, _ := h.Store.Settings() type profileStatus struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Status process.Status `json:"process"` - HasProc bool `json:"hasProcess"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + Status process.Status `json:"process"` + HasProc bool `json:"hasProcess"` Forwards []store.Forward `json:"forwards"` } @@ -155,7 +161,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { }) } - // Local status: each local plus its forwarding targets and whether each + // Local status: each local plus its forwarding targets and whether each // target's worker is healthy (running). type localTargetStatus struct { Remote string `json:"remote"` @@ -163,8 +169,8 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { WorkerState string `json:"workerState"` } type localStatus struct { - Local store.Local `json:"local"` - Targets []localTargetStatus `json:"targets"` + Local store.Local `json:"local"` + Targets []localTargetStatus `json:"targets"` } localStatuses := make([]localStatus, 0, len(locals)) @@ -186,13 +192,13 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { } resp := map[string]any{ - "version": "0.1.0", - "workDir": h.WorkDir, - "settings": settings, - "services": locals, - "remotes": remotes, - "binaryPath": binary, - "profiles": profiles, + "version": "0.1.0", + "workDir": h.WorkDir, + "settings": settings, + "services": locals, + "remotes": remotes, + "binaryPath": binary, + "profiles": profiles, "localStatus": localStatuses, } writeJSON(w, http.StatusOK, resp) diff --git a/internal/httpapi/server_test.go b/internal/httpapi/server_test.go index 0e11d4b..fd9c2d8 100644 --- a/internal/httpapi/server_test.go +++ b/internal/httpapi/server_test.go @@ -97,3 +97,44 @@ func TestCanvasRoundTrip(t *testing.T) { t.Fatalf("remotes after reload = %+v", got2.Remotes) } } + +func TestSettingsPutRoundTrip(t *testing.T) { + _, ts := newTestHandler(t) + client := ts.Client() + + // PUT updated settings + body := `{"autoStartProfiles":false,"restartOnExit":false,"restartIntervalSeconds":12}` + req, _ := http.NewRequest(http.MethodPut, ts.URL+"/api/manager/settings", bytes.NewBufferString(body)) + req.SetBasicAuth("admin", "pw") + resp, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("PUT settings status = %d, want 200", resp.StatusCode) + } + + // Confirm response body reflects saved values + var saved store.Settings + if err := json.NewDecoder(resp.Body).Decode(&saved); err != nil { + t.Fatal(err) + } + if saved.AutoStartProfiles || saved.RestartOnExit || saved.RestartIntervalSeconds != 12 { + t.Fatalf("saved settings = %+v", saved) + } + + // GET again to confirm persistence + req2, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/manager/settings", nil) + req2.SetBasicAuth("admin", "pw") + resp2, err2 := client.Do(req2) + if err2 != nil { + t.Fatal(err2) + } + defer resp2.Body.Close() + var got store.Settings + _ = json.NewDecoder(resp2.Body).Decode(&got) + if got.AutoStartProfiles || got.RestartOnExit || got.RestartIntervalSeconds != 12 { + t.Fatalf("settings after reload = %+v", got) + } +} diff --git a/internal/store/store.go b/internal/store/store.go index 4d82cb8..1305b7d 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -24,7 +24,7 @@ type Local struct { type Remote struct { Name string `json:"name"` IP string `json:"ip"` - Port int `json:"port"` // port to connect to frps + Port int `json:"port"` // port to connect to frps Token string `json:"token,omitempty"` URL string `json:"url,omitempty"` Enabled bool `json:"enabled"` @@ -32,7 +32,7 @@ type Remote struct { // Link connects one local to one remote. type Link struct { - ID int64 `json:"id,omitempty"` + ID int64 `json:"id,omitempty"` Local string `json:"local"` Remote string `json:"remote"` RemotePort int `json:"remotePort"` diff --git a/plan.md b/plan.md index 20f3291..168d318 100644 --- a/plan.md +++ b/plan.md @@ -30,7 +30,7 @@ ## 已知问题(审查发现,待修复) -1. **设置保存失效(P0)**:`internal/httpapi/handlers.go` 定义了 `handleSettingsPut`,但 `server.go` 的 `NewServeMux` 只注册了 GET `/api/manager/settings`,**未注册 PUT**。PUT 请求会落入 `handleSettingsGet` 静默返回旧值,SettingsView 保存不生效。 +1. ~~设置保存失效(P0)~~ **已修复**:`server.go` 的 `NewServeMux` 现在对 `PUT /api/manager/settings` 分发到 `handleSettingsPut`,新增 `TestSettingsPutRoundTrip` 回归测试,前端 SettingsView 保存生效。 2. **dev proxy 端口不一致**:`web/vite.config.ts` 的 proxy 指向 `127.0.0.1:17650`,与后端默认监听 `127.0.0.1:7500` 不符,前端 dev 联调需对齐。 3. **冗余依赖**:`vue-router` 在 package.json 依赖中但未被使用(App.vue 以视图 ref 手动切页,无路由)。 4. **README 笔误**:架构图写 `cmds/webui4frpc`,实际目录为 `cmd/webui4frpc`。 @@ -76,7 +76,7 @@ ## 建议推进顺序 -1. 修复「设置保存失效」——给 `PUT /api/manager/settings` 补注册(约 10 分钟) +1. [x] 修复「设置保存失效」——给 `PUT /api/manager/settings` 补注册(server.go 方法分发 + httpapi 单测) 2. 对齐 dev proxy 端口(vite.config.ts → 7500) 3. M1 高级传输参数(store 加列 → render 输出 → 前端折叠表单 → 单测回归) 4. M5 补一键构建脚本 / CI