diff --git a/scripts/start-server.ps1 b/scripts/start-server.ps1 index a4d4bde..d00a27f 100644 --- a/scripts/start-server.ps1 +++ b/scripts/start-server.ps1 @@ -10,6 +10,10 @@ param( ) $ErrorActionPreference = "Stop" + +# Go 进程输出 UTF-8。不显式设定的话,中文日志会按控制台的 ANSI 代码页(简中为 +# GBK)解码,显示成乱码——见 #48 启动日志。 +try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { } $workspaceRoot = Split-Path -Parent $PSScriptRoot $serverDirectory = Join-Path $workspaceRoot "server" $migrationLog = Join-Path $serverDirectory "temp\startup-migration.log" diff --git a/scripts/start-web.ps1 b/scripts/start-web.ps1 index 1ba794f..f5470cc 100644 --- a/scripts/start-web.ps1 +++ b/scripts/start-web.ps1 @@ -5,6 +5,10 @@ param( ) $ErrorActionPreference = "Stop" + +# Go 进程输出 UTF-8。不显式设定的话,中文日志会按控制台的 ANSI 代码页(简中为 +# GBK)解码,显示成乱码——见 #48 启动日志。 +try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { } $workspaceRoot = Split-Path -Parent $PSScriptRoot $webDirectory = Join-Path $workspaceRoot "web" diff --git a/server/config/local.go b/server/config/local.go index 45b33f8..40d4b94 100644 --- a/server/config/local.go +++ b/server/config/local.go @@ -2,11 +2,11 @@ package config import ( "fmt" - "log" "os" "path/filepath" "strconv" "strings" + "time" sdkconfig "github.com/go-admin-team/go-admin-core/sdk/config" "gopkg.in/yaml.v2" @@ -34,6 +34,14 @@ func LocalConfigPath() string { if _, err := os.Stat(LocalConfigName); err == nil { return LocalConfigName } + // One level up. The server is normally started from server/ while + // config.yaml lives at the repository root, so without this the file is + // found only when a launcher happens to export GOAUTO_CONFIG — and a + // developer running `go run .` by hand gets no local configuration at all. + parent := filepath.Join("..", LocalConfigName) + if _, err := os.Stat(parent); err == nil { + return parent + } // Next to the executable, so double-clicking a packaged binary works no // matter what the working directory happens to be. if executable, err := os.Executable(); err == nil { @@ -72,23 +80,23 @@ func ApplyLocalConfig() { // `[必须]` Say so out loud. Silently doing nothing here surfaces much // later as a puzzling "credential not configured" error that never // mentions the file the operator actually edited. - log.Printf("本地配置:未找到 %s(依次查找 GOAUTO_CONFIG、当前目录、可执行文件同级目录),仅使用 settings.yml 和环境变量", LocalConfigName) + logInfo("本地配置:未找到 %s(依次查找 GOAUTO_CONFIG、当前目录、上级目录、可执行文件同级目录),仅使用 settings.yml 和环境变量", LocalConfigName) return } raw, err := os.ReadFile(path) if err != nil { - fmt.Fprintf(os.Stderr, "读取本地配置 %s 失败,已忽略: %v\n", path, err) + logInfo("本地配置:读取 %s 失败,已忽略: %v", path, err) return } var file localFile if err := yaml.Unmarshal(raw, &file); err != nil { - fmt.Fprintf(os.Stderr, "解析本地配置 %s 失败,已忽略: %v\n", path, err) + logInfo("本地配置:解析 %s 失败,已忽略: %v", path, err) return } applyLocalDatabase(file.Database) applyLocalPorts(file.Ports) ApplyLocalSYB(file.SYB) - log.Printf("本地配置:已加载 %s", path) + logInfo("本地配置:已加载 %s", path) } // LogEffectiveConfig prints what the layered configuration actually resolved @@ -105,7 +113,7 @@ func LogEffectiveConfig() { case syb.HasCredentials(): source = LocalConfigName } - log.Printf("顺云宝配置:凭据=%v(来源:%s)base_url=%s 验证码识别=%v", + logInfo("顺云宝配置:凭据=%v(来源:%s)base_url=%s 验证码识别=%v", syb.HasCredentials(), source, syb.BaseURL, syb.OcrURL != "") } @@ -169,6 +177,16 @@ func ApplyLocalSYB(syb map[string]any) { } } +// logInfo writes an informational startup line to stdout. +// +// `[必须]` Not stderr. The development launcher pipes the server through +// `2>&1 | Tee-Object`, which turns every stderr write into a PowerShell +// NativeCommandError — informational lines would show up as a red error block +// and look like a failed startup. +func logInfo(format string, args ...any) { + fmt.Printf(time.Now().Format("2006/01/02 15:04:05")+" "+format+"\n", args...) +} + // scalar renders one YAML value as a string regardless of how YAML typed it. // // `[必须]` Floats are formatted without an exponent and without a trailing diff --git a/server/config/local_test.go b/server/config/local_test.go index a002aeb..e112691 100644 --- a/server/config/local_test.go +++ b/server/config/local_test.go @@ -218,3 +218,55 @@ func TestLocalConfigPathPrefersExplicitEnvOverCwd(t *testing.T) { t.Fatalf("GOAUTO_CONFIG 应优先于其它位置: %q", got) } } + +// 服务端通常从 server/ 启动,而 config.yaml 在仓库根。没有这条上级目录查找, +// 手工跑 `go run .` 就完全读不到本地配置——启动脚本导出 GOAUTO_CONFIG 只是 +// 恰好掩盖了这一点。 +func TestLocalConfigPathFindsFileInParentDirectory(t *testing.T) { + restoreConfigState(t) + t.Setenv("GOAUTO_CONFIG", "") + + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, LocalConfigName), + []byte("syb:\n username: operator\n password: \"654321\"\n"), 0o600); err != nil { + t.Fatalf("写配置失败: %v", err) + } + child := filepath.Join(root, "server") + if err := os.Mkdir(child, 0o755); err != nil { + t.Fatalf("建目录失败: %v", err) + } + previous, err := os.Getwd() + if err != nil { + t.Fatalf("取工作目录失败: %v", err) + } + if err := os.Chdir(child); err != nil { + t.Fatalf("切目录失败: %v", err) + } + t.Cleanup(func() { os.Chdir(previous) }) + + ApplyLocalConfig() + if !ExtConfig.SYB.HasCredentials() { + t.Fatal("从 server/ 启动时应能在上级目录找到 config.yaml") + } +} + +// 当前目录优先于上级目录。 +func TestLocalConfigPathPrefersCwdOverParent(t *testing.T) { + restoreConfigState(t) + t.Setenv("GOAUTO_CONFIG", "") + + root := t.TempDir() + os.WriteFile(filepath.Join(root, LocalConfigName), []byte("syb:\n username: parent\n password: \"1\"\n"), 0o600) + child := filepath.Join(root, "server") + os.Mkdir(child, 0o755) + os.WriteFile(filepath.Join(child, LocalConfigName), []byte("syb:\n username: cwd\n password: \"1\"\n"), 0o600) + + previous, _ := os.Getwd() + os.Chdir(child) + t.Cleanup(func() { os.Chdir(previous) }) + + ApplyLocalConfig() + if ExtConfig.SYB.Username != "cwd" { + t.Fatalf("当前目录应优先: %q", ExtConfig.SYB.Username) + } +}