From 8b3d0bab36f3a470d88bc8845cf9cd5e540a52f8 Mon Sep 17 00:00:00 2001
From: nasakura <92672842+yingzeliangzi@users.noreply.github.com>
Date: Thu, 15 Jan 2026 05:31:10 +0000
Subject: [PATCH] feat: add listen address configuration option
---
cmd/desktop/app.go | 3 +
cmd/desktop/frontend/src/i18n/en.js | 12 +-
cmd/desktop/frontend/src/i18n/zh-CN.js | 12 +-
cmd/desktop/frontend/src/main.js | 2 +
cmd/desktop/frontend/src/modules/config.js | 7 +-
cmd/desktop/frontend/src/modules/modal.js | 23 +-
cmd/desktop/frontend/src/modules/ui.js | 9 +
cmd/desktop/frontend/src/style.css | 25 +
cmd/desktop/frontend/wailsjs/go/main/App.d.ts | 2 +
cmd/desktop/frontend/wailsjs/go/main/App.js | 4 +
cmd/server/main.go | 275 ++++-----
cmd/server/webui/api/config.go | 32 +-
cmd/server/webui/ui/js/api.js | 8 +-
internal/config/config.go | 66 ++-
internal/proxy/proxy.go | 29 +-
internal/service/settings.go | 553 +++++++++---------
16 files changed, 618 insertions(+), 444 deletions(-)
diff --git a/cmd/desktop/app.go b/cmd/desktop/app.go
index c2bf7a75..fb907f54 100644
--- a/cmd/desktop/app.go
+++ b/cmd/desktop/app.go
@@ -397,6 +397,9 @@ func (a *App) UpdateConfig(configJSON string) error {
return a.settings.UpdateConfig(configJSON, a.proxy)
}
func (a *App) UpdatePort(port int) error { return a.settings.UpdatePort(port) }
+func (a *App) UpdateNetwork(port int, listenAddr string) error {
+ return a.settings.UpdateNetwork(port, listenAddr)
+}
func (a *App) GetSystemLanguage() string { return a.settings.GetSystemLanguage() }
func (a *App) GetLanguage() string { return a.settings.GetLanguage() }
func (a *App) SetLanguage(language string) error { return a.settings.SetLanguage(language) }
diff --git a/cmd/desktop/frontend/src/i18n/en.js b/cmd/desktop/frontend/src/i18n/en.js
index db2c9bdc..e4f04018 100644
--- a/cmd/desktop/frontend/src/i18n/en.js
+++ b/cmd/desktop/frontend/src/i18n/en.js
@@ -71,10 +71,16 @@ export default {
changePort: 'Change Port',
port: 'Port',
portLabel: 'Port (1-65535):',
- portNote: 'Note: Changing port requires application restart',
+ listenAddrLabel: 'Listen address:',
+ listenAddrPlaceholder: 'e.g., 127.0.0.1',
+ listenAddrPresetPublic: 'Public 0.0.0.0',
+ listenAddrPresetLAN: 'LAN 192.168.0.0',
+ listenAddrPresetLocal: 'Local 127.0.0.1',
+ listenAddrInvalid: 'Please enter a valid listen address',
+ portNote: 'Note: Changing port or listen address requires application restart',
portInvalid: 'Please enter a valid port number (1-65535)',
- portUpdateSuccess: 'Port updated successfully! Please restart the application for changes to take effect.',
- portUpdateFailed: 'Failed to update port: {error}',
+ portUpdateSuccess: 'Port and listen address updated successfully! Please restart the application for changes to take effect.',
+ portUpdateFailed: 'Failed to update port or listen address: {error}',
requiredFields: 'Please fill in all required fields',
modelRequired: 'Model field is required for {transformer} transformer',
saveFailed: 'Failed to save: {error}',
diff --git a/cmd/desktop/frontend/src/i18n/zh-CN.js b/cmd/desktop/frontend/src/i18n/zh-CN.js
index f3fbca0b..39a4c55c 100644
--- a/cmd/desktop/frontend/src/i18n/zh-CN.js
+++ b/cmd/desktop/frontend/src/i18n/zh-CN.js
@@ -71,10 +71,16 @@ export default {
changePort: '修改端口',
port: '端口',
portLabel: '端口号 (1-65535):',
- portNote: '注意:修改端口号需要重启应用',
+ listenAddrLabel: '监听地址:',
+ listenAddrPlaceholder: '例如:127.0.0.1',
+ listenAddrPresetPublic: '公网 0.0.0.0',
+ listenAddrPresetLAN: '局域网 192.168.0.0',
+ listenAddrPresetLocal: '本地 127.0.0.1',
+ listenAddrInvalid: '请输入有效的监听地址',
+ portNote: '注意:修改端口号或监听地址需要重启应用',
portInvalid: '请输入有效的端口号(1-65535)',
- portUpdateSuccess: '端口修改成功!请重启应用以使更改生效。',
- portUpdateFailed: '端口修改失败:{error}',
+ portUpdateSuccess: '端口和监听地址修改成功!请重启应用以使更改生效。',
+ portUpdateFailed: '端口或监听地址修改失败:{error}',
requiredFields: '请填写所有必填项',
modelRequired: '使用 {transformer} 转换器时,模型字段为必填项',
saveFailed: '保存失败:{error}',
diff --git a/cmd/desktop/frontend/src/main.js b/cmd/desktop/frontend/src/main.js
index 88f83f15..5c0d9e02 100644
--- a/cmd/desktop/frontend/src/main.js
+++ b/cmd/desktop/frontend/src/main.js
@@ -28,6 +28,7 @@ import {
showEditPortModal,
savePort,
closePortModal,
+ setListenAddrPreset,
showWelcomeModal,
closeWelcomeModal,
showWelcomeModalIfFirstTime,
@@ -177,6 +178,7 @@ window.toggleModelDropdown = toggleModelDropdown;
window.showEditPortModal = showEditPortModal;
window.savePort = savePort;
window.closePortModal = closePortModal;
+window.setListenAddrPreset = setListenAddrPreset;
window.showWelcomeModal = showWelcomeModal;
window.closeWelcomeModal = closeWelcomeModal;
window.showChangelogModal = showChangelogModal;
diff --git a/cmd/desktop/frontend/src/modules/config.js b/cmd/desktop/frontend/src/modules/config.js
index 6b57aab4..9ded95e8 100644
--- a/cmd/desktop/frontend/src/modules/config.js
+++ b/cmd/desktop/frontend/src/modules/config.js
@@ -15,7 +15,8 @@ export async function loadConfig() {
const configStr = await window.go.main.App.GetConfig();
const config = JSON.parse(configStr);
- document.getElementById('proxyPort').textContent = config.port;
+ const listenAddr = config.listenAddr || '127.0.0.1';
+ document.getElementById('proxyPort').textContent = `${listenAddr}:${config.port}`;
document.getElementById('totalEndpoints').textContent = config.endpoints.length;
const activeCount = config.endpoints.filter(ep => ep.enabled !== false).length;
@@ -32,6 +33,10 @@ export async function updatePort(port) {
await window.go.main.App.UpdatePort(port);
}
+export async function updateNetwork(port, listenAddr) {
+ await window.go.main.App.UpdateNetwork(port, listenAddr);
+}
+
export async function addEndpoint(name, url, key, transformer, model, remark) {
await window.go.main.App.AddEndpoint(name, url, key, transformer, model, remark || '');
}
diff --git a/cmd/desktop/frontend/src/modules/modal.js b/cmd/desktop/frontend/src/modules/modal.js
index 53d7aa59..d3a584df 100644
--- a/cmd/desktop/frontend/src/modules/modal.js
+++ b/cmd/desktop/frontend/src/modules/modal.js
@@ -1,6 +1,6 @@
import { t } from '../i18n/index.js';
import { escapeHtml } from '../utils/format.js';
-import { addEndpoint, updateEndpoint, removeEndpoint, testEndpoint, testEndpointLight, updatePort } from './config.js';
+import { addEndpoint, updateEndpoint, removeEndpoint, testEndpoint, testEndpointLight, updateNetwork } from './config.js';
import { setTestState, clearTestState, saveEndpointTestStatus } from './endpoints.js';
let currentEditIndex = -1;
@@ -345,19 +345,30 @@ export async function showEditPortModal() {
const config = JSON.parse(configStr);
document.getElementById('portInput').value = config.port;
+ const listenAddrInput = document.getElementById('listenAddrInput');
+ if (listenAddrInput) {
+ listenAddrInput.value = config.listenAddr || '127.0.0.1';
+ }
document.getElementById('portModal').classList.add('active');
}
export async function savePort() {
const port = parseInt(document.getElementById('portInput').value);
+ const listenAddrInput = document.getElementById('listenAddrInput');
+ const listenAddr = (listenAddrInput?.value || '').trim() || '127.0.0.1';
if (!port || port < 1 || port > 65535) {
showNotification(t('modal.portInvalid'), 'error');
return;
}
+ if (!listenAddr || /\s/.test(listenAddr)) {
+ showNotification(t('modal.listenAddrInvalid'), 'error');
+ return;
+ }
+
try {
- await updatePort(port);
+ await updateNetwork(port, listenAddr);
closePortModal();
window.loadConfig();
showNotification(t('modal.portUpdateSuccess'), 'success');
@@ -370,6 +381,14 @@ export function closePortModal() {
document.getElementById('portModal').classList.remove('active');
}
+export function setListenAddrPreset(addr) {
+ const input = document.getElementById('listenAddrInput');
+ if (input) {
+ input.value = addr;
+ input.focus();
+ }
+}
+
// ========== 加群二维码URL配置 ==========
// 上传到图床后填写URL,过期时直接替换图床文件即可自动更新
diff --git a/cmd/desktop/frontend/src/modules/ui.js b/cmd/desktop/frontend/src/modules/ui.js
index 6ae70f9f..995eaff5 100644
--- a/cmd/desktop/frontend/src/modules/ui.js
+++ b/cmd/desktop/frontend/src/modules/ui.js
@@ -442,6 +442,15 @@ export function initUI() {
+
⚠️ ${t('modal.portNote')}
diff --git a/cmd/desktop/frontend/src/style.css b/cmd/desktop/frontend/src/style.css
index 6f35ff4d..ecb0a956 100644
--- a/cmd/desktop/frontend/src/style.css
+++ b/cmd/desktop/frontend/src/style.css
@@ -1399,6 +1399,31 @@ body {
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
}
+/* Listen address presets */
+.listen-addr-presets {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.preset-chip {
+ border: 1px solid rgba(102, 126, 234, 0.35);
+ background: rgba(102, 126, 234, 0.08);
+ color: #4c51bf;
+ padding: 6px 10px;
+ border-radius: 8px;
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.preset-chip:hover {
+ background: rgba(102, 126, 234, 0.15);
+ border-color: rgba(102, 126, 234, 0.6);
+ transform: translateY(-1px);
+}
+
/* Endpoint Toggle Button */
.endpoint-toggle-btn {
padding: 0 10px;
diff --git a/cmd/desktop/frontend/wailsjs/go/main/App.d.ts b/cmd/desktop/frontend/wailsjs/go/main/App.d.ts
index db0bd7ef..b14aa290 100755
--- a/cmd/desktop/frontend/wailsjs/go/main/App.d.ts
+++ b/cmd/desktop/frontend/wailsjs/go/main/App.d.ts
@@ -191,6 +191,8 @@ export function UpdateEndpoint(arg1:number,arg2:string,arg3:string,arg4:string,a
export function UpdateLocalBackupDir(arg1:string):Promise;
+export function UpdateNetwork(arg1:number,arg2:string):Promise;
+
export function UpdatePort(arg1:number):Promise;
export function UpdateS3BackupConfig(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string,arg6:string,arg7:string,arg8:boolean,arg9:boolean):Promise;
diff --git a/cmd/desktop/frontend/wailsjs/go/main/App.js b/cmd/desktop/frontend/wailsjs/go/main/App.js
index f546a9fc..d509a724 100755
--- a/cmd/desktop/frontend/wailsjs/go/main/App.js
+++ b/cmd/desktop/frontend/wailsjs/go/main/App.js
@@ -382,6 +382,10 @@ export function UpdateLocalBackupDir(arg1) {
return window['go']['main']['App']['UpdateLocalBackupDir'](arg1);
}
+export function UpdateNetwork(arg1, arg2) {
+ return window['go']['main']['App']['UpdateNetwork'](arg1, arg2);
+}
+
export function UpdatePort(arg1) {
return window['go']['main']['App']['UpdatePort'](arg1);
}
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 514f8f28..61d88e9f 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -1,153 +1,160 @@
package main
import (
- "errors"
- "net/http"
- "os"
- "os/signal"
- "path/filepath"
- "strconv"
- "syscall"
-
- "github.com/lich0821/ccNexus/internal/config"
- "github.com/lich0821/ccNexus/internal/logger"
- "github.com/lich0821/ccNexus/internal/proxy"
- "github.com/lich0821/ccNexus/internal/storage"
+ "errors"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "strconv"
+ "syscall"
+
+ "github.com/lich0821/ccNexus/internal/config"
+ "github.com/lich0821/ccNexus/internal/logger"
+ "github.com/lich0821/ccNexus/internal/proxy"
+ "github.com/lich0821/ccNexus/internal/storage"
)
func main() {
- dataDir := resolveDataDir()
- if err := os.MkdirAll(dataDir, 0755); err != nil {
- logger.Error("Failed to create data dir %s: %v", dataDir, err)
- os.Exit(1)
- }
-
- dbPath := os.Getenv("CCNEXUS_DB_PATH")
- if dbPath == "" {
- dbPath = filepath.Join(dataDir, "ccnexus.db")
- }
-
- sqliteStorage, err := storage.NewSQLiteStorage(dbPath)
- if err != nil {
- logger.Error("Failed to open SQLite storage: %v", err)
- os.Exit(1)
- }
- defer sqliteStorage.Close()
-
- cfg, err := loadConfig(sqliteStorage)
- if err != nil {
- logger.Error("Unable to load configuration: %v", err)
- os.Exit(1)
- }
-
- applyEnvOverrides(cfg)
- setLogLevels(cfg.GetLogLevel())
-
- if err := cfg.Validate(); err != nil {
- logger.Error("Invalid configuration: %v", err)
- os.Exit(1)
- }
-
- deviceID, err := sqliteStorage.GetOrCreateDeviceID()
- if err != nil {
- logger.Warn("Failed to get device ID: %v, using default", err)
- deviceID = "default"
- }
-
- statsAdapter := storage.NewStatsStorageAdapter(sqliteStorage)
- p := proxy.New(cfg, statsAdapter, deviceID)
-
- // Create HTTP mux
- mux := http.NewServeMux()
-
- // Initialize and register Web UI (optional plugin)
- // If webui package is not available, this will be skipped at compile time
- if err := registerWebUI(mux, cfg, p, sqliteStorage); err != nil {
- logger.Warn("Web UI not available: %v", err)
- } else {
- logger.Info("Web UI available at /ui/")
- }
-
- errCh := make(chan error, 1)
- go func() {
- errCh <- p.StartWithMux(mux)
- }()
-
- logger.Info("ccNexus headless API listening on :%d (data dir: %s, db: %s)", cfg.GetPort(), dataDir, dbPath)
-
- sigCh := make(chan os.Signal, 1)
- signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
-
- select {
- case sig := <-sigCh:
- logger.Info("Received signal %s, shutting down", sig.String())
- if err := p.Stop(); err != nil {
- logger.Warn("Graceful shutdown failed: %v", err)
- }
- case err := <-errCh:
- if err != nil && !errors.Is(err, http.ErrServerClosed) {
- logger.Error("Proxy server stopped with error: %v", err)
- os.Exit(1)
- }
- }
-
- logger.Info("ccNexus stopped")
+ dataDir := resolveDataDir()
+ if err := os.MkdirAll(dataDir, 0755); err != nil {
+ logger.Error("Failed to create data dir %s: %v", dataDir, err)
+ os.Exit(1)
+ }
+
+ dbPath := os.Getenv("CCNEXUS_DB_PATH")
+ if dbPath == "" {
+ dbPath = filepath.Join(dataDir, "ccnexus.db")
+ }
+
+ sqliteStorage, err := storage.NewSQLiteStorage(dbPath)
+ if err != nil {
+ logger.Error("Failed to open SQLite storage: %v", err)
+ os.Exit(1)
+ }
+ defer sqliteStorage.Close()
+
+ cfg, err := loadConfig(sqliteStorage)
+ if err != nil {
+ logger.Error("Unable to load configuration: %v", err)
+ os.Exit(1)
+ }
+
+ applyEnvOverrides(cfg)
+ setLogLevels(cfg.GetLogLevel())
+
+ if err := cfg.Validate(); err != nil {
+ logger.Error("Invalid configuration: %v", err)
+ os.Exit(1)
+ }
+
+ deviceID, err := sqliteStorage.GetOrCreateDeviceID()
+ if err != nil {
+ logger.Warn("Failed to get device ID: %v, using default", err)
+ deviceID = "default"
+ }
+
+ statsAdapter := storage.NewStatsStorageAdapter(sqliteStorage)
+ p := proxy.New(cfg, statsAdapter, deviceID)
+
+ // Create HTTP mux
+ mux := http.NewServeMux()
+
+ // Initialize and register Web UI (optional plugin)
+ // If webui package is not available, this will be skipped at compile time
+ if err := registerWebUI(mux, cfg, p, sqliteStorage); err != nil {
+ logger.Warn("Web UI not available: %v", err)
+ } else {
+ logger.Info("Web UI available at /ui/")
+ }
+
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- p.StartWithMux(mux)
+ }()
+
+ listenAddr := cfg.GetListenAddr()
+ addr := net.JoinHostPort(listenAddr, strconv.Itoa(cfg.GetPort()))
+ logger.Info("ccNexus headless API listening on %s (data dir: %s, db: %s)", addr, dataDir, dbPath)
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ select {
+ case sig := <-sigCh:
+ logger.Info("Received signal %s, shutting down", sig.String())
+ if err := p.Stop(); err != nil {
+ logger.Warn("Graceful shutdown failed: %v", err)
+ }
+ case err := <-errCh:
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
+ logger.Error("Proxy server stopped with error: %v", err)
+ os.Exit(1)
+ }
+ }
+
+ logger.Info("ccNexus stopped")
}
func resolveDataDir() string {
- if dir := os.Getenv("CCNEXUS_DATA_DIR"); dir != "" {
- return dir
- }
- if home, err := os.UserHomeDir(); err == nil {
- return filepath.Join(home, ".ccNexus")
- }
- return "/data"
+ if dir := os.Getenv("CCNEXUS_DATA_DIR"); dir != "" {
+ return dir
+ }
+ if home, err := os.UserHomeDir(); err == nil {
+ return filepath.Join(home, ".ccNexus")
+ }
+ return "/data"
}
func loadConfig(sqliteStorage *storage.SQLiteStorage) (*config.Config, error) {
- adapter := storage.NewConfigStorageAdapter(sqliteStorage)
- cfg, err := config.LoadFromStorage(adapter)
- if err != nil {
- logger.Warn("Failed to load config from storage, using default: %v", err)
- cfg = config.DefaultConfig()
- if saveErr := cfg.SaveToStorage(adapter); saveErr != nil {
- logger.Warn("Failed to persist default config: %v", saveErr)
- }
- }
-
- // Seed a default endpoint when none are configured to avoid boot failure
- if len(cfg.Endpoints) == 0 {
- logger.Warn("No endpoints found; seeding a default endpoint")
- cfg.Endpoints = config.DefaultConfig().Endpoints
- if saveErr := cfg.SaveToStorage(adapter); saveErr != nil {
- logger.Warn("Failed to persist seeded endpoint: %v", saveErr)
- }
- }
- return cfg, nil
+ adapter := storage.NewConfigStorageAdapter(sqliteStorage)
+ cfg, err := config.LoadFromStorage(adapter)
+ if err != nil {
+ logger.Warn("Failed to load config from storage, using default: %v", err)
+ cfg = config.DefaultConfig()
+ if saveErr := cfg.SaveToStorage(adapter); saveErr != nil {
+ logger.Warn("Failed to persist default config: %v", saveErr)
+ }
+ }
+
+ // Seed a default endpoint when none are configured to avoid boot failure
+ if len(cfg.Endpoints) == 0 {
+ logger.Warn("No endpoints found; seeding a default endpoint")
+ cfg.Endpoints = config.DefaultConfig().Endpoints
+ if saveErr := cfg.SaveToStorage(adapter); saveErr != nil {
+ logger.Warn("Failed to persist seeded endpoint: %v", saveErr)
+ }
+ }
+ return cfg, nil
}
func applyEnvOverrides(cfg *config.Config) {
- if portStr := os.Getenv("CCNEXUS_PORT"); portStr != "" {
- if port, err := strconv.Atoi(portStr); err == nil {
- cfg.UpdatePort(port)
- } else {
- logger.Warn("Invalid CCNEXUS_PORT value %q: %v", portStr, err)
- }
- }
-
- if levelStr := os.Getenv("CCNEXUS_LOG_LEVEL"); levelStr != "" {
- if level, err := strconv.Atoi(levelStr); err == nil {
- cfg.UpdateLogLevel(level)
- } else {
- logger.Warn("Invalid CCNEXUS_LOG_LEVEL value %q: %v", levelStr, err)
- }
- }
+ if portStr := os.Getenv("CCNEXUS_PORT"); portStr != "" {
+ if port, err := strconv.Atoi(portStr); err == nil {
+ cfg.UpdatePort(port)
+ } else {
+ logger.Warn("Invalid CCNEXUS_PORT value %q: %v", portStr, err)
+ }
+ }
+
+ if addr := os.Getenv("CCNEXUS_LISTEN_ADDR"); addr != "" {
+ cfg.UpdateListenAddr(addr)
+ }
+
+ if levelStr := os.Getenv("CCNEXUS_LOG_LEVEL"); levelStr != "" {
+ if level, err := strconv.Atoi(levelStr); err == nil {
+ cfg.UpdateLogLevel(level)
+ } else {
+ logger.Warn("Invalid CCNEXUS_LOG_LEVEL value %q: %v", levelStr, err)
+ }
+ }
}
func setLogLevels(level int) {
- if level < 0 {
- return
- }
- logger.GetLogger().SetMinLevel(logger.LogLevel(level))
- logger.GetLogger().SetConsoleLevel(logger.LogLevel(level))
+ if level < 0 {
+ return
+ }
+ logger.GetLogger().SetMinLevel(logger.LogLevel(level))
+ logger.GetLogger().SetConsoleLevel(logger.LogLevel(level))
}
diff --git a/cmd/server/webui/api/config.go b/cmd/server/webui/api/config.go
index d96c83fc..6952bc48 100644
--- a/cmd/server/webui/api/config.go
+++ b/cmd/server/webui/api/config.go
@@ -3,6 +3,7 @@ package api
import (
"encoding/json"
"net/http"
+ "strings"
"github.com/lich0821/ccNexus/internal/logger"
"github.com/lich0821/ccNexus/internal/storage"
@@ -23,16 +24,18 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
// getConfig returns the full configuration
func (h *Handler) getConfig(w http.ResponseWriter, r *http.Request) {
WriteSuccess(w, map[string]interface{}{
- "port": h.config.GetPort(),
- "logLevel": h.config.GetLogLevel(),
+ "port": h.config.GetPort(),
+ "listenAddr": h.config.GetListenAddr(),
+ "logLevel": h.config.GetLogLevel(),
})
}
// updateConfig updates the full configuration
func (h *Handler) updateConfig(w http.ResponseWriter, r *http.Request) {
var req struct {
- Port int `json:"port"`
- LogLevel int `json:"logLevel"`
+ Port int `json:"port"`
+ ListenAddr string `json:"listenAddr"`
+ LogLevel int `json:"logLevel"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -45,6 +48,10 @@ func (h *Handler) updateConfig(w http.ResponseWriter, r *http.Request) {
h.config.UpdatePort(req.Port)
}
+ if strings.TrimSpace(req.ListenAddr) != "" {
+ h.config.UpdateListenAddr(req.ListenAddr)
+ }
+
// Update log level if provided
if req.LogLevel >= 0 {
h.config.UpdateLogLevel(req.LogLevel)
@@ -68,11 +75,13 @@ func (h *Handler) handleConfigPort(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
WriteSuccess(w, map[string]interface{}{
- "port": h.config.GetPort(),
+ "port": h.config.GetPort(),
+ "listenAddr": h.config.GetListenAddr(),
})
case http.MethodPut:
var req struct {
- Port int `json:"port"`
+ Port int `json:"port"`
+ ListenAddr string `json:"listenAddr"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -85,7 +94,13 @@ func (h *Handler) handleConfigPort(w http.ResponseWriter, r *http.Request) {
return
}
+ if strings.TrimSpace(req.ListenAddr) == "" {
+ WriteError(w, http.StatusBadRequest, "Invalid listen address")
+ return
+ }
+
h.config.UpdatePort(req.Port)
+ h.config.UpdateListenAddr(req.ListenAddr)
// Save to storage
adapter := storage.NewConfigStorageAdapter(h.storage)
@@ -96,8 +111,9 @@ func (h *Handler) handleConfigPort(w http.ResponseWriter, r *http.Request) {
}
WriteSuccess(w, map[string]interface{}{
- "port": req.Port,
- "message": "Port updated successfully (restart required)",
+ "port": req.Port,
+ "listenAddr": req.ListenAddr,
+ "message": "Port and listen address updated successfully (restart required)",
})
default:
WriteError(w, http.StatusMethodNotAllowed, "Method not allowed")
diff --git a/cmd/server/webui/ui/js/api.js b/cmd/server/webui/ui/js/api.js
index 73488763..ed9e3b8d 100644
--- a/cmd/server/webui/ui/js/api.js
+++ b/cmd/server/webui/ui/js/api.js
@@ -106,8 +106,12 @@ class APIClient {
return this.request('GET', '/config/port');
}
- async updatePort(port) {
- return this.request('PUT', '/config/port', { port });
+ async updatePort(port, listenAddr) {
+ const payload = { port };
+ if (listenAddr) {
+ payload.listenAddr = listenAddr;
+ }
+ return this.request('PUT', '/config/port', payload);
}
async getLogLevel() {
diff --git a/internal/config/config.go b/internal/config/config.go
index 29c601f1..8e03aa40 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"strconv"
+ "strings"
"sync"
)
@@ -73,31 +74,33 @@ type ProxyConfig struct {
// Config represents the application configuration
type Config struct {
- Port int `json:"port"`
- Endpoints []Endpoint `json:"endpoints"`
- LogLevel int `json:"logLevel"` // 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR
- Language string `json:"language"` // UI language: en, zh-CN
- Theme string `json:"theme"` // UI theme: light, dark
- ThemeAuto bool `json:"themeAuto"` // Auto switch theme based on time
- AutoLightTheme string `json:"autoLightTheme,omitempty"` // Theme to use in daytime when auto mode is on
- AutoDarkTheme string `json:"autoDarkTheme,omitempty"` // Theme to use in nighttime when auto mode is on
- WindowWidth int `json:"windowWidth"` // Window width in pixels
- WindowHeight int `json:"windowHeight"` // Window height in pixels
+ Port int `json:"port"`
+ ListenAddr string `json:"listenAddr"`
+ Endpoints []Endpoint `json:"endpoints"`
+ LogLevel int `json:"logLevel"` // 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR
+ Language string `json:"language"` // UI language: en, zh-CN
+ Theme string `json:"theme"` // UI theme: light, dark
+ ThemeAuto bool `json:"themeAuto"` // Auto switch theme based on time
+ AutoLightTheme string `json:"autoLightTheme,omitempty"` // Theme to use in daytime when auto mode is on
+ AutoDarkTheme string `json:"autoDarkTheme,omitempty"` // Theme to use in nighttime when auto mode is on
+ WindowWidth int `json:"windowWidth"` // Window width in pixels
+ WindowHeight int `json:"windowHeight"` // Window height in pixels
CloseWindowBehavior string `json:"closeWindowBehavior,omitempty"` // "quit", "minimize", "ask"
ClaudeNotificationEnabled bool `json:"claudeNotificationEnabled"` // Enable Claude Code task completion notification
ClaudeNotificationType string `json:"claudeNotificationType"` // Notification type: toast, dialog, disabled
WebDAV *WebDAVConfig `json:"webdav,omitempty"` // WebDAV synchronization config
- Backup *BackupConfig `json:"backup,omitempty"` // Backup/sync configuration
- Update *UpdateConfig `json:"update,omitempty"` // Update configuration
- Terminal *TerminalConfig `json:"terminal,omitempty"` // Terminal launcher config
- Proxy *ProxyConfig `json:"proxy,omitempty"` // HTTP proxy config
- mu sync.RWMutex
+ Backup *BackupConfig `json:"backup,omitempty"` // Backup/sync configuration
+ Update *UpdateConfig `json:"update,omitempty"` // Update configuration
+ Terminal *TerminalConfig `json:"terminal,omitempty"` // Terminal launcher config
+ Proxy *ProxyConfig `json:"proxy,omitempty"` // HTTP proxy config
+ mu sync.RWMutex
}
// DefaultConfig returns a default configuration
func DefaultConfig() *Config {
return &Config{
Port: 3000,
+ ListenAddr: "127.0.0.1",
LogLevel: 1, // Default to INFO level
Language: "zh-CN", // Default to Chinese
WindowWidth: 1024, // Default window width
@@ -123,6 +126,10 @@ func (c *Config) Validate() error {
c.mu.RLock()
defer c.mu.RUnlock()
+ if strings.TrimSpace(c.ListenAddr) == "" {
+ return fmt.Errorf("invalid listen address")
+ }
+
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("invalid port: %d", c.Port)
}
@@ -170,6 +177,16 @@ func (c *Config) GetPort() int {
return c.Port
}
+// GetListenAddr returns the configured listen address (thread-safe)
+func (c *Config) GetListenAddr() string {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ if strings.TrimSpace(c.ListenAddr) == "" {
+ return "127.0.0.1"
+ }
+ return c.ListenAddr
+}
+
// GetLogLevel returns the configured log level (thread-safe)
func (c *Config) GetLogLevel() int {
c.mu.RLock()
@@ -191,6 +208,17 @@ func (c *Config) UpdatePort(port int) {
c.Port = port
}
+// UpdateListenAddr updates the listen address (thread-safe)
+func (c *Config) UpdateListenAddr(addr string) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if strings.TrimSpace(addr) == "" {
+ c.ListenAddr = "127.0.0.1"
+ return
+ }
+ c.ListenAddr = addr
+}
+
// UpdateLogLevel updates the log level (thread-safe)
func (c *Config) UpdateLogLevel(level int) {
c.mu.Lock()
@@ -458,6 +486,13 @@ func LoadFromStorage(storage StorageAdapter) (*Config, error) {
config.Port = 3000
}
+ if listenAddr, err := storage.GetConfig("listenAddr"); err == nil {
+ config.ListenAddr = listenAddr
+ }
+ if strings.TrimSpace(config.ListenAddr) == "" {
+ config.ListenAddr = "127.0.0.1"
+ }
+
if logLevelStr, err := storage.GetConfig("logLevel"); err == nil && logLevelStr != "" {
if logLevel, err := strconv.Atoi(logLevelStr); err == nil {
config.LogLevel = logLevel
@@ -681,6 +716,7 @@ func (c *Config) SaveToStorage(storage StorageAdapter) error {
// Save app config
storage.SetConfig("port", strconv.Itoa(c.Port))
+ storage.SetConfig("listenAddr", c.ListenAddr)
storage.SetConfig("logLevel", strconv.Itoa(c.LogLevel))
storage.SetConfig("language", c.Language)
storage.SetConfig("theme", c.Theme)
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
index c5f17f1c..77c0584c 100644
--- a/internal/proxy/proxy.go
+++ b/internal/proxy/proxy.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "net"
"net/http"
"strings"
"sync"
@@ -33,17 +34,17 @@ type APIResponse struct {
// Proxy represents the proxy server
type Proxy struct {
- config *config.Config
- stats *Stats
- currentIndex int
- mu sync.RWMutex
- server *http.Server
- activeRequests map[string]bool // tracks active requests by endpoint name
- activeRequestsMu sync.RWMutex // protects activeRequests map
- endpointCtx map[string]context.Context // context per endpoint for cancellation
- endpointCancel map[string]context.CancelFunc // cancel functions per endpoint
- ctxMu sync.RWMutex // protects context maps
- onEndpointSuccess func(endpointName string) // callback when endpoint request succeeds
+ config *config.Config
+ stats *Stats
+ currentIndex int
+ mu sync.RWMutex
+ server *http.Server
+ activeRequests map[string]bool // tracks active requests by endpoint name
+ activeRequestsMu sync.RWMutex // protects activeRequests map
+ endpointCtx map[string]context.Context // context per endpoint for cancellation
+ endpointCancel map[string]context.CancelFunc // cancel functions per endpoint
+ ctxMu sync.RWMutex // protects context maps
+ onEndpointSuccess func(endpointName string) // callback when endpoint request succeeds
}
// New creates a new Proxy instance
@@ -73,6 +74,8 @@ func (p *Proxy) Start() error {
// StartWithMux starts the proxy server with an optional custom mux
func (p *Proxy) StartWithMux(customMux *http.ServeMux) error {
port := p.config.GetPort()
+ listenAddr := p.config.GetListenAddr()
+ addr := net.JoinHostPort(listenAddr, fmt.Sprintf("%d", port))
var mux *http.ServeMux
if customMux != nil {
@@ -88,11 +91,11 @@ func (p *Proxy) StartWithMux(customMux *http.ServeMux) error {
mux.HandleFunc("/stats", p.handleStats)
p.server = &http.Server{
- Addr: fmt.Sprintf(":%d", port),
+ Addr: addr,
Handler: mux,
}
- logger.Info("ccNexus starting on port %d", port)
+ logger.Info("ccNexus starting on %s", addr)
logger.Info("Configured %d endpoints", len(p.config.GetEndpoints()))
return p.server.ListenAndServe()
diff --git a/internal/service/settings.go b/internal/service/settings.go
index 8ac9b272..67c3d1d2 100644
--- a/internal/service/settings.go
+++ b/internal/service/settings.go
@@ -1,313 +1,340 @@
package service
import (
- "encoding/json"
- "fmt"
- "os"
- "strings"
-
- "github.com/lich0821/ccNexus/internal/config"
- "github.com/lich0821/ccNexus/internal/logger"
- "github.com/lich0821/ccNexus/internal/storage"
- "github.com/lich0821/ccNexus/internal/tray"
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/lich0821/ccNexus/internal/config"
+ "github.com/lich0821/ccNexus/internal/logger"
+ "github.com/lich0821/ccNexus/internal/storage"
+ "github.com/lich0821/ccNexus/internal/tray"
)
// SettingsService handles settings operations
type SettingsService struct {
- config *config.Config
- storage *storage.SQLiteStorage
+ config *config.Config
+ storage *storage.SQLiteStorage
}
// NewSettingsService creates a new SettingsService
func NewSettingsService(cfg *config.Config, s *storage.SQLiteStorage) *SettingsService {
- return &SettingsService{config: cfg, storage: s}
+ return &SettingsService{config: cfg, storage: s}
}
// GetConfig returns the current configuration as JSON
func (s *SettingsService) GetConfig() string {
- data, _ := json.Marshal(s.config)
- return string(data)
+ data, _ := json.Marshal(s.config)
+ return string(data)
}
// UpdateConfig updates the configuration
func (s *SettingsService) UpdateConfig(configJSON string, proxy interface{ UpdateConfig(*config.Config) error }) error {
- var newConfig config.Config
- if err := json.Unmarshal([]byte(configJSON), &newConfig); err != nil {
- return fmt.Errorf("invalid config format: %w", err)
- }
-
- if err := newConfig.Validate(); err != nil {
- return fmt.Errorf("invalid config: %w", err)
- }
-
- if err := proxy.UpdateConfig(&newConfig); err != nil {
- return err
- }
-
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := newConfig.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save config: %w", err)
- }
- }
-
- *s.config = newConfig
- return nil
+ var newConfig config.Config
+ if err := json.Unmarshal([]byte(configJSON), &newConfig); err != nil {
+ return fmt.Errorf("invalid config format: %w", err)
+ }
+
+ if strings.TrimSpace(newConfig.ListenAddr) == "" {
+ newConfig.ListenAddr = "127.0.0.1"
+ }
+
+ if err := newConfig.Validate(); err != nil {
+ return fmt.Errorf("invalid config: %w", err)
+ }
+
+ if err := proxy.UpdateConfig(&newConfig); err != nil {
+ return err
+ }
+
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := newConfig.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save config: %w", err)
+ }
+ }
+
+ *s.config = newConfig
+ return nil
}
// UpdatePort updates the proxy port
func (s *SettingsService) UpdatePort(port int) error {
- if port < 1 || port > 65535 {
- return fmt.Errorf("invalid port: %d", port)
- }
+ if port < 1 || port > 65535 {
+ return fmt.Errorf("invalid port: %d", port)
+ }
- s.config.UpdatePort(port)
+ s.config.UpdatePort(port)
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save config: %w", err)
- }
- }
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save config: %w", err)
+ }
+ }
- return nil
+ return nil
+}
+
+// UpdateNetwork updates port and listen address together
+func (s *SettingsService) UpdateNetwork(port int, listenAddr string) error {
+ if port < 1 || port > 65535 {
+ return fmt.Errorf("invalid port: %d", port)
+ }
+
+ if strings.TrimSpace(listenAddr) == "" {
+ return fmt.Errorf("invalid listen address")
+ }
+
+ s.config.UpdatePort(port)
+ s.config.UpdateListenAddr(listenAddr)
+
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save config: %w", err)
+ }
+ }
+
+ return nil
}
// GetSystemLanguage detects the system language
func (s *SettingsService) GetSystemLanguage() string {
- locale := os.Getenv("LANG")
- if locale == "" {
- locale = os.Getenv("LC_ALL")
- }
- if locale == "" {
- locale = os.Getenv("LANGUAGE")
- }
- if locale == "" {
- return "en"
- }
-
- if strings.Contains(strings.ToLower(locale), "zh") {
- return "zh-CN"
- }
- return "en"
+ locale := os.Getenv("LANG")
+ if locale == "" {
+ locale = os.Getenv("LC_ALL")
+ }
+ if locale == "" {
+ locale = os.Getenv("LANGUAGE")
+ }
+ if locale == "" {
+ return "en"
+ }
+
+ if strings.Contains(strings.ToLower(locale), "zh") {
+ return "zh-CN"
+ }
+ return "en"
}
// GetLanguage returns the current language setting
func (s *SettingsService) GetLanguage() string {
- lang := s.config.GetLanguage()
- if lang == "" {
- return s.GetSystemLanguage()
- }
- return lang
+ lang := s.config.GetLanguage()
+ if lang == "" {
+ return s.GetSystemLanguage()
+ }
+ return lang
}
// SetLanguage sets the UI language
func (s *SettingsService) SetLanguage(language string) error {
- s.config.UpdateLanguage(language)
-
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save language: %w", err)
- }
- }
-
- tray.UpdateLanguage(language)
- logger.Info("Language changed to: %s", language)
- return nil
+ s.config.UpdateLanguage(language)
+
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save language: %w", err)
+ }
+ }
+
+ tray.UpdateLanguage(language)
+ logger.Info("Language changed to: %s", language)
+ return nil
}
// GetTheme returns the current theme setting
func (s *SettingsService) GetTheme() string {
- theme := s.config.GetTheme()
- if theme == "" {
- return "light"
- }
- return theme
+ theme := s.config.GetTheme()
+ if theme == "" {
+ return "light"
+ }
+ return theme
}
// SetTheme sets the UI theme
func (s *SettingsService) SetTheme(theme string) error {
- s.config.UpdateTheme(theme)
+ s.config.UpdateTheme(theme)
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save theme: %w", err)
- }
- }
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save theme: %w", err)
+ }
+ }
- logger.Info("Theme changed to: %s", theme)
- return nil
+ logger.Info("Theme changed to: %s", theme)
+ return nil
}
// GetThemeAuto returns whether auto theme switching is enabled
func (s *SettingsService) GetThemeAuto() bool {
- return s.config.GetThemeAuto()
+ return s.config.GetThemeAuto()
}
// SetThemeAuto enables or disables auto theme switching
func (s *SettingsService) SetThemeAuto(auto bool) error {
- s.config.UpdateThemeAuto(auto)
+ s.config.UpdateThemeAuto(auto)
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save theme auto setting: %w", err)
- }
- }
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save theme auto setting: %w", err)
+ }
+ }
- logger.Info("Theme auto mode changed to: %v", auto)
- return nil
+ logger.Info("Theme auto mode changed to: %v", auto)
+ return nil
}
// GetAutoLightTheme returns the theme to use in daytime when auto mode is on
func (s *SettingsService) GetAutoLightTheme() string {
- theme := s.config.GetAutoLightTheme()
- if theme == "" {
- return "light"
- }
- return theme
+ theme := s.config.GetAutoLightTheme()
+ if theme == "" {
+ return "light"
+ }
+ return theme
}
// SetAutoLightTheme sets the theme to use in daytime when auto mode is on
func (s *SettingsService) SetAutoLightTheme(theme string) error {
- s.config.UpdateAutoLightTheme(theme)
+ s.config.UpdateAutoLightTheme(theme)
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save auto light theme: %w", err)
- }
- }
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save auto light theme: %w", err)
+ }
+ }
- logger.Info("Auto light theme changed to: %s", theme)
- return nil
+ logger.Info("Auto light theme changed to: %s", theme)
+ return nil
}
// GetAutoDarkTheme returns the theme to use in nighttime when auto mode is on
func (s *SettingsService) GetAutoDarkTheme() string {
- theme := s.config.GetAutoDarkTheme()
- if theme == "" {
- return "dark"
- }
- return theme
+ theme := s.config.GetAutoDarkTheme()
+ if theme == "" {
+ return "dark"
+ }
+ return theme
}
// SetAutoDarkTheme sets the theme to use in nighttime when auto mode is on
func (s *SettingsService) SetAutoDarkTheme(theme string) error {
- s.config.UpdateAutoDarkTheme(theme)
+ s.config.UpdateAutoDarkTheme(theme)
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save auto dark theme: %w", err)
- }
- }
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save auto dark theme: %w", err)
+ }
+ }
- logger.Info("Auto dark theme changed to: %s", theme)
- return nil
+ logger.Info("Auto dark theme changed to: %s", theme)
+ return nil
}
// GetLogs returns all log entries
func (s *SettingsService) GetLogs() string {
- logs := logger.GetLogger().GetLogs()
- data, _ := json.Marshal(logs)
- return string(data)
+ logs := logger.GetLogger().GetLogs()
+ data, _ := json.Marshal(logs)
+ return string(data)
}
// GetLogsByLevel returns logs filtered by level
func (s *SettingsService) GetLogsByLevel(level int) string {
- logs := logger.GetLogger().GetLogsByLevel(logger.LogLevel(level))
- data, _ := json.Marshal(logs)
- return string(data)
+ logs := logger.GetLogger().GetLogsByLevel(logger.LogLevel(level))
+ data, _ := json.Marshal(logs)
+ return string(data)
}
// ClearLogs clears all log entries
func (s *SettingsService) ClearLogs() {
- logger.GetLogger().Clear()
+ logger.GetLogger().Clear()
}
// SetLogLevel sets the minimum log level to record
func (s *SettingsService) SetLogLevel(level int) {
- logger.GetLogger().SetMinLevel(logger.LogLevel(level))
- s.config.UpdateLogLevel(level)
-
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- logger.Warn("Failed to save log level: %v", err)
- } else {
- logger.Debug("Log level saved: %d", level)
- }
- }
+ logger.GetLogger().SetMinLevel(logger.LogLevel(level))
+ s.config.UpdateLogLevel(level)
+
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ logger.Warn("Failed to save log level: %v", err)
+ } else {
+ logger.Debug("Log level saved: %d", level)
+ }
+ }
}
// GetLogLevel returns the current minimum log level
func (s *SettingsService) GetLogLevel() int {
- return s.config.GetLogLevel()
+ return s.config.GetLogLevel()
}
// SetCloseWindowBehavior sets the user's preference for close window behavior
func (s *SettingsService) SetCloseWindowBehavior(behavior string) error {
- if behavior != "quit" && behavior != "minimize" && behavior != "ask" {
- return fmt.Errorf("invalid behavior: %s (must be 'quit', 'minimize', or 'ask')", behavior)
- }
-
- s.config.UpdateCloseWindowBehavior(behavior)
-
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- logger.Warn("Failed to save close window behavior: %v", err)
- return err
- }
- }
-
- logger.Info("Close window behavior set to: %s", behavior)
- return nil
+ if behavior != "quit" && behavior != "minimize" && behavior != "ask" {
+ return fmt.Errorf("invalid behavior: %s (must be 'quit', 'minimize', or 'ask')", behavior)
+ }
+
+ s.config.UpdateCloseWindowBehavior(behavior)
+
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ logger.Warn("Failed to save close window behavior: %v", err)
+ return err
+ }
+ }
+
+ logger.Info("Close window behavior set to: %s", behavior)
+ return nil
}
// SaveWindowSize saves the window size to config
func (s *SettingsService) SaveWindowSize(width, height int) {
- if width > 0 && height > 0 {
- s.config.UpdateWindowSize(width, height)
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- logger.Warn("Failed to save window size: %v", err)
- } else {
- logger.Debug("Window size saved: %dx%d", width, height)
- }
- }
- }
+ if width > 0 && height > 0 {
+ s.config.UpdateWindowSize(width, height)
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ logger.Warn("Failed to save window size: %v", err)
+ } else {
+ logger.Debug("Window size saved: %dx%d", width, height)
+ }
+ }
+ }
}
// GetProxyURL returns the current proxy URL
func (s *SettingsService) GetProxyURL() string {
- if proxy := s.config.GetProxy(); proxy != nil {
- return proxy.URL
- }
- return ""
+ if proxy := s.config.GetProxy(); proxy != nil {
+ return proxy.URL
+ }
+ return ""
}
// SetProxyURL sets the proxy URL
func (s *SettingsService) SetProxyURL(proxyURL string) error {
- var proxyCfg *config.ProxyConfig
- if proxyURL != "" {
- proxyCfg = &config.ProxyConfig{URL: proxyURL}
- }
- s.config.UpdateProxy(proxyCfg)
-
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save proxy config: %w", err)
- }
- }
-
- logger.Info("Proxy URL changed to: %s", proxyURL)
- return nil
+ var proxyCfg *config.ProxyConfig
+ if proxyURL != "" {
+ proxyCfg = &config.ProxyConfig{URL: proxyURL}
+ }
+ s.config.UpdateProxy(proxyCfg)
+
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save proxy config: %w", err)
+ }
+ }
+
+ logger.Info("Proxy URL changed to: %s", proxyURL)
+ return nil
}
// SettingsData represents the settings data for batch save
@@ -324,71 +351,71 @@ type SettingsData struct {
// SaveSettings saves all settings in a single operation to avoid database lock issues
func (s *SettingsService) SaveSettings(settingsJSON string) error {
- var settings SettingsData
- if err := json.Unmarshal([]byte(settingsJSON), &settings); err != nil {
- return fmt.Errorf("invalid settings format: %w", err)
- }
-
- // Validate close window behavior
- if settings.CloseWindowBehavior != "" &&
- settings.CloseWindowBehavior != "quit" &&
- settings.CloseWindowBehavior != "minimize" &&
- settings.CloseWindowBehavior != "ask" {
- return fmt.Errorf("invalid close window behavior: %s", settings.CloseWindowBehavior)
- }
-
- // Update all settings in memory
- if settings.CloseWindowBehavior != "" {
- s.config.UpdateCloseWindowBehavior(settings.CloseWindowBehavior)
- }
-
- if settings.Theme != "" {
- s.config.UpdateTheme(settings.Theme)
- }
-
- s.config.UpdateThemeAuto(settings.ThemeAuto)
-
- // Update auto theme settings
- if settings.AutoLightTheme != "" {
- s.config.UpdateAutoLightTheme(settings.AutoLightTheme)
- }
- if settings.AutoDarkTheme != "" {
- s.config.UpdateAutoDarkTheme(settings.AutoDarkTheme)
- }
-
- // Update proxy config
- var proxyCfg *config.ProxyConfig
- if settings.ProxyURL != "" {
- proxyCfg = &config.ProxyConfig{URL: settings.ProxyURL}
- }
- s.config.UpdateProxy(proxyCfg)
-
- // Update Claude notification config
- // Validate notification type
- if settings.ClaudeNotificationType != "" &&
- settings.ClaudeNotificationType != "toast" &&
- settings.ClaudeNotificationType != "dialog" &&
- settings.ClaudeNotificationType != "disabled" {
- return fmt.Errorf("invalid notification type: %s", settings.ClaudeNotificationType)
- }
- s.config.UpdateClaudeNotification(settings.ClaudeNotificationEnabled, settings.ClaudeNotificationType)
-
- // Save to storage only once
- if s.storage != nil {
- configAdapter := storage.NewConfigStorageAdapter(s.storage)
- if err := s.config.SaveToStorage(configAdapter); err != nil {
- return fmt.Errorf("failed to save settings: %w", err)
- }
- }
-
- // Apply Claude notification hook to ~/.claude/settings.json
- claudeService := NewClaudeConfigService(s.config)
- if err := claudeService.UpdateNotificationHook(); err != nil {
- logger.Warn("Failed to update Claude notification hook: %v", err)
- // Don't fail the whole save operation, just log the warning
- }
-
- logger.Info("Settings saved: closeWindowBehavior=%s, theme=%s, themeAuto=%v, proxyUrl=%s, claudeNotification=%v",
- settings.CloseWindowBehavior, settings.Theme, settings.ThemeAuto, settings.ProxyURL, settings.ClaudeNotificationEnabled)
- return nil
+ var settings SettingsData
+ if err := json.Unmarshal([]byte(settingsJSON), &settings); err != nil {
+ return fmt.Errorf("invalid settings format: %w", err)
+ }
+
+ // Validate close window behavior
+ if settings.CloseWindowBehavior != "" &&
+ settings.CloseWindowBehavior != "quit" &&
+ settings.CloseWindowBehavior != "minimize" &&
+ settings.CloseWindowBehavior != "ask" {
+ return fmt.Errorf("invalid close window behavior: %s", settings.CloseWindowBehavior)
+ }
+
+ // Update all settings in memory
+ if settings.CloseWindowBehavior != "" {
+ s.config.UpdateCloseWindowBehavior(settings.CloseWindowBehavior)
+ }
+
+ if settings.Theme != "" {
+ s.config.UpdateTheme(settings.Theme)
+ }
+
+ s.config.UpdateThemeAuto(settings.ThemeAuto)
+
+ // Update auto theme settings
+ if settings.AutoLightTheme != "" {
+ s.config.UpdateAutoLightTheme(settings.AutoLightTheme)
+ }
+ if settings.AutoDarkTheme != "" {
+ s.config.UpdateAutoDarkTheme(settings.AutoDarkTheme)
+ }
+
+ // Update proxy config
+ var proxyCfg *config.ProxyConfig
+ if settings.ProxyURL != "" {
+ proxyCfg = &config.ProxyConfig{URL: settings.ProxyURL}
+ }
+ s.config.UpdateProxy(proxyCfg)
+
+ // Update Claude notification config
+ // Validate notification type
+ if settings.ClaudeNotificationType != "" &&
+ settings.ClaudeNotificationType != "toast" &&
+ settings.ClaudeNotificationType != "dialog" &&
+ settings.ClaudeNotificationType != "disabled" {
+ return fmt.Errorf("invalid notification type: %s", settings.ClaudeNotificationType)
+ }
+ s.config.UpdateClaudeNotification(settings.ClaudeNotificationEnabled, settings.ClaudeNotificationType)
+
+ // Save to storage only once
+ if s.storage != nil {
+ configAdapter := storage.NewConfigStorageAdapter(s.storage)
+ if err := s.config.SaveToStorage(configAdapter); err != nil {
+ return fmt.Errorf("failed to save settings: %w", err)
+ }
+ }
+
+ // Apply Claude notification hook to ~/.claude/settings.json
+ claudeService := NewClaudeConfigService(s.config)
+ if err := claudeService.UpdateNotificationHook(); err != nil {
+ logger.Warn("Failed to update Claude notification hook: %v", err)
+ // Don't fail the whole save operation, just log the warning
+ }
+
+ logger.Info("Settings saved: closeWindowBehavior=%s, theme=%s, themeAuto=%v, proxyUrl=%s, claudeNotification=%v",
+ settings.CloseWindowBehavior, settings.Theme, settings.ThemeAuto, settings.ProxyURL, settings.ClaudeNotificationEnabled)
+ return nil
}