1
0

feat: 区分 server 与 desktop 配置加载入口,取消自动创建配置文件

- config.go 重构:抽取 loadConfig 共享逻辑,新增 LoadServerConfig/LoadDesktopConfig/LoadDesktopConfigAtPath,LoadConfig 保持向后兼容
- setupConfigFile 移除 SafeWriteConfigAs 自动创建逻辑,文件不存在时仅使用默认值
- cmd/desktop 切换为 LoadDesktopConfig,端口/HTTP/浏览器/托盘统一使用 cfg.Server.Port
- cmd/server 显式使用 LoadServerConfig 明确入口语义
- 提取 desktop 可测 helper:desktopListenAddr/desktopURL/desktopPortMenuTitle/desktopConfigErrorMessage
- 新增测试:desktop 忽略 CLI/env/未知参数、配置快照不变、无效配置文件不静默回退、端口 helper 一致性
- README 区分 server/desktop 配置源,移除首次启动自动创建配置文件描述
- 同步 delta specs 到 openspec/specs/ 主规范
This commit is contained in:
2026-05-06 11:59:19 +08:00
parent 598e2acb7e
commit 5513f0c13d
10 changed files with 589 additions and 147 deletions

View File

@@ -43,10 +43,23 @@ var (
)
func main() {
port := 9826
minimalLogger := pkgLogger.NewMinimal()
cfg, err := config.LoadDesktopConfig()
if err != nil {
minimalLogger.Error("加载配置失败", zap.Error(err))
showError(appName, desktopConfigErrorMessage(getDesktopConfigPath(), err))
os.Exit(1)
}
port := cfg.Server.Port
if err := checkPortAvailable(port); err != nil {
minimalLogger.Error("端口不可用", zap.Error(err))
showError(appName, err.Error())
os.Exit(1)
}
singleLock := NewSingletonLock(filepath.Join(os.TempDir(), "nex-gateway.lock"))
if err := singleLock.Lock(); err != nil {
minimalLogger.Error("已有 Nex 实例运行")
@@ -59,17 +72,6 @@ func main() {
}
}()
if err := checkPortAvailable(port); err != nil {
minimalLogger.Error("端口不可用", zap.Error(err))
showError(appName, err.Error())
return
}
cfg, err := config.LoadConfig()
if err != nil {
minimalLogger.Fatal("加载配置失败", zap.Error(err))
}
zapLogger, err = pkgLogger.Upgrade(minimalLogger, pkgLogger.Config{
Level: cfg.Log.Level,
Path: cfg.Log.Path,
@@ -144,7 +146,7 @@ func main() {
setupStaticFiles(r)
server = &http.Server{
Addr: fmt.Sprintf(":%d", port),
Addr: desktopListenAddr(port),
Handler: r,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
@@ -165,7 +167,7 @@ func main() {
go func() {
time.Sleep(500 * time.Millisecond)
if err := openBrowser(fmt.Sprintf("http://localhost:%d", port)); err != nil {
if err := openBrowser(desktopURL(port)); err != nil {
zapLogger.Warn("无法打开浏览器", zap.Error(err))
}
}()
@@ -309,7 +311,7 @@ func setupSystray(port int) {
systray.AddSeparator()
mStatus := systray.AddMenuItem("状态: 运行中", "")
mStatus.Disable()
mPort := systray.AddMenuItem(fmt.Sprintf("端口: %d", port), "")
mPort := systray.AddMenuItem(desktopPortMenuTitle(port), "")
mPort.Disable()
systray.AddSeparator()
mQuit := systray.AddMenuItem("退出", "停止服务并退出")
@@ -318,7 +320,7 @@ func setupSystray(port int) {
for {
select {
case <-mOpen.ClickedCh:
if err := openBrowser(fmt.Sprintf("http://localhost:%d", port)); err != nil {
if err := openBrowser(desktopURL(port)); err != nil {
zapLogger.Warn("打开浏览器失败", zap.Error(err))
}
case <-mQuit.ClickedCh:
@@ -349,6 +351,30 @@ func doShutdown() {
}
}
func getDesktopConfigPath() string {
configDir, err := config.GetConfigDir()
if err != nil {
return "~/.nex/config.yaml"
}
return filepath.Join(configDir, "config.yaml")
}
func desktopConfigErrorMessage(configPath string, err error) string {
return fmt.Sprintf("加载配置失败\n\n配置文件: %s\n\n%v", configPath, err)
}
func desktopListenAddr(port int) string {
return fmt.Sprintf(":%d", port)
}
func desktopURL(port int) string {
return fmt.Sprintf("http://localhost:%d", port)
}
func desktopPortMenuTitle(port int) string {
return fmt.Sprintf("端口: %d", port)
}
func checkPortAvailable(port int) error {
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"errors"
"net"
"net/http"
"strings"
"testing"
"time"
)
@@ -67,3 +68,62 @@ func TestCheckPortAvailableAfterClose(t *testing.T) {
t.Log("端口关闭后可用测试通过")
}
func TestCheckPortAvailableErrorContainsPort(t *testing.T) {
port := 19829
listener, err := net.Listen("tcp", ":19829") //nolint:gosec
if err != nil {
t.Fatalf("无法启动测试服务器: %v", err)
}
defer listener.Close()
time.Sleep(100 * time.Millisecond)
err = checkPortAvailable(port)
if err == nil {
t.Fatal("端口被占用时应该返回错误")
}
if !strings.Contains(err.Error(), "19829") {
t.Fatalf("错误信息应包含端口号 19829实际: %v", err)
}
t.Log("端口错误信息包含端口号测试通过")
}
func TestGetDesktopConfigPath(t *testing.T) {
path := getDesktopConfigPath()
if path == "" {
t.Fatal("getDesktopConfigPath 应返回非空路径")
}
if !strings.Contains(path, "config.yaml") {
t.Fatalf("路径应包含 config.yaml实际: %s", path)
}
t.Log("getDesktopConfigPath 测试通过")
}
func TestDesktopConfiguredPortHelpers(t *testing.T) {
port := 19830
if got := desktopListenAddr(port); got != ":19830" {
t.Fatalf("HTTP 监听地址应使用配置端口,实际: %s", got)
}
if got := desktopURL(port); got != "http://localhost:19830" {
t.Fatalf("浏览器 URL 应使用配置端口,实际: %s", got)
}
if got := desktopPortMenuTitle(port); got != "端口: 19830" {
t.Fatalf("托盘端口显示应使用配置端口,实际: %s", got)
}
}
func TestDesktopConfigErrorMessageContainsPathAndReason(t *testing.T) {
msg := desktopConfigErrorMessage("/tmp/nex/config.yaml", errors.New("yaml parse failed"))
if !strings.Contains(msg, "/tmp/nex/config.yaml") {
t.Fatalf("配置错误提示应包含配置路径,实际: %s", msg)
}
if !strings.Contains(msg, "yaml parse failed") {
t.Fatalf("配置错误提示应包含失败原因,实际: %s", msg)
}
}