feat: configure startup ports from local config (#30)

This commit is contained in:
QiuSW
2026-08-16 19:18:46 +08:00
parent 924c6e0504
commit cde2a84211
13 changed files with 203 additions and 40 deletions
+4
View File
@@ -4,3 +4,7 @@ database:
user: root
password: ""
name: goauto
ports:
server: 8000
web: 9527
+1 -1
View File
@@ -32,4 +32,4 @@
## 当前阶段
当前 MVP 的 T01~T07、T09~T22 均已完成实现、验证并由用户验收。T08(只读实时屏幕)已延期且未实施,不属于当前 MVP。T17 已在一加/ColorOS 真机完成指定设备领取、空闲领取、PDD 商品详情页到达和部分结果提交验证,不包含华为兼容。T23 已完成 v2 一加真机实施验证,正在等待用户验收。T24 已完成规格面板锚定滚动、逐行蛇形颜色遍历和尺码续页实现,并以商品 `236231603269` 验证 14 色、8 尺码和 112 个 SKU,正在等待用户验收。T25 已实现商品页假售罄的一次性下拉恢复,T26 已实现未进入商品详情页时的一次性浏览器重开恢复。T27 已修复“相似商品”推荐区域污染假售罄判定和 Agent 未声明手势能力的问题,并以商品 `731370706977` 的任务 32 在一加真机验证两次下拉恢复及完整任务完成,正在等待用户验收。当前实施范围仍是采集闭环;Agent 架构允许未来增加独立采购规则的创建订单能力,但付款能力禁止进入项目。
当前 MVP 的 T01~T07、T09~T22 均已完成实现、验证并由用户验收。T08(只读实时屏幕)已延期且未实施,不属于当前 MVP。T17 已在一加/ColorOS 真机完成指定设备领取、空闲领取、PDD 商品详情页到达和部分结果提交验证,不包含华为兼容。T23 已完成 v2 一加真机实施验证,正在等待用户验收。T24 已完成规格面板锚定滚动、逐行蛇形颜色遍历和尺码续页实现,并以商品 `236231603269` 验证 14 色、8 尺码和 112 个 SKU,正在等待用户验收。T25 已实现商品页假售罄的一次性下拉恢复,T26 已实现未进入商品详情页时的一次性浏览器重开恢复。T27 已修复“相似商品”推荐区域污染假售罄判定和 Agent 未声明手势能力的问题,并以商品 `731370706977` 的任务 32 在一加真机验证两次下拉恢复及完整任务完成,正在等待用户验收。T28 已实现由本地 `config.yaml` 统一配置 API 与管理端启动端口,并完成非默认端口联合验证,正在等待用户验收。当前实施范围仍是采集闭环;Agent 架构允许未来增加独立采购规则的创建订单能力,但付款能力禁止进入项目。
@@ -25,7 +25,7 @@ go build ./...
服务端固定为 go-admin v2.3.0,模块要求 Go 1.26.5;本机可由 `GOTOOLCHAIN=auto` 获取匹配工具链。对象存储用例带有 `integration` 标签,默认验证不会访问云端凭据。
默认运行目标是 MySQL。开发者在仓库根目录的 `config.yaml` 中维护本机连接参数;该文件已被 Git 忽略,不得提交。可从不含凭据的 `config.example.yaml` 复制。启动脚本读取配置后,仅通过当前进程的 `GOAUTO_DB_DSN` 和 `GOAUTO_DB_DRIVER` 注入服务,不把账号密码写回 `server/config/settings.yml`、日志、工单或文档。
默认运行目标是 MySQL。开发者在仓库根目录的 `config.yaml` 中维护本机连接参数和启动端口;该文件已被 Git 忽略,不得提交。可从不含凭据的 `config.example.yaml` 复制。启动脚本读取配置后,仅通过当前进程环境注入数据库连接、服务端端口和前端 API 地址,不把账号密码写回 `server/config/settings.yml`、日志、工单或文档。
已经执行过旧版 GoAuto 表结构的数据库,会由 `1786700300000_collection_execution.go` 增量补齐任务幂等、结果、软删除和颜色价格字段;不要通过修改已执行迁移的版本号强制重跑。
@@ -35,9 +35,9 @@ Windows 本地 MySQL 8.4 可以从仓库根目录双击或执行:
.\start-server.bat
```
首次使用先编辑根目录 `config.yaml` 的 `database` 节点,配置 `host`、`port`、`user`、`password` 和 `name`。随后脚本直接读取该文件,创建数据库、执行迁移并启动服务,不再交互询问密码。迁移已经执行过时,可使用 `.\start-server.bat -SkipMigration`;只验证配置可使用 `.\start-server.bat -ValidateConfigOnly`。
首次使用先编辑根目录 `config.yaml`:`database` 节点配置 `host`、`port`、`user`、`password` 和 `name`,`ports.server` 与 `ports.web` 配置 API 和管理端开发服务器端口。两个启动端口必须位于 1~65535 且不能相同。随后脚本直接读取该文件,创建数据库、执行迁移并启动服务,不再交互询问密码。迁移已经执行过时,可使用 `.\start-server.bat -SkipMigration`;只验证服务端配置可使用 `.\start-server.bat -ValidateConfigOnly`,只验证前端端口配置可使用 `.\start-web.bat -ValidateConfigOnly`。
管理端前端可单独双击 `start-web.bat`,访问 `http://127.0.0.1:9527`;也可双击 `start-all.bat` 同时打开服务端和前端两个窗口。`http://127.0.0.1:8000` 是 API 服务,根路径显示 go-admin 默认欢迎页属于正常现象。
管理端前端可单独双击 `start-web.bat`;也可双击 `start-all.bat` 同时打开服务端和前端两个窗口。访问地址分别为 `http://127.0.0.1:<ports.web>` 和 `http://127.0.0.1:<ports.server>`,默认仍是 9527 和 8000。前端开发 API 地址会自动跟随 `ports.server`。修改服务端端口后,还需同步修改 Android Agent 保存的服务地址。API 根路径显示 go-admin 默认欢迎页属于正常现象。
只验证最小闭环迁移和数据库约束:
+1
View File
@@ -35,6 +35,7 @@
| T25 | [#27](https://git.ilapage.cn/OPC/goauto/issues/27) | PDD 假售罄页面一次性下拉恢复 | T20~T24 |
| T26 | [#28](https://git.ilapage.cn/OPC/goauto/issues/28) | PDD 商品详情进入失败后一次性重开浏览器恢复 | T20~T25 |
| T27 | [#29](https://git.ilapage.cn/OPC/goauto/issues/29) | 修复相似商品区域导致假售罄恢复不触发 | T25、T26 |
| T28 | [#30](https://git.ilapage.cn/OPC/goauto/issues/30) | 启动端口统一由 config.yaml 配置 | T18 |
## 延期
+1 -1
View File
@@ -13,7 +13,7 @@ T17 只验证 PDD 商品采集最小闭环:Agent 注册与心跳、任务串
- ADB 设备在线,PDD App 已安装。
- Debug Agent 安装成功并保持前台服务。
- Agent 使用局域网地址访问服务端,设备自动注册成功,之后每 15 秒持续心跳。
- 手机到服务端 8000 端口连通。
- 手机到 `config.yaml` 的 `ports.server` 端口连通(默认 8000)。
- Android 单元测试与 Debug APK 构建通过。
- 人工启用“GoAuto 采集服务”后,系统显示服务已启用且已绑定;应用升级或被强制停止后,一加系统可能撤销该授权,必须重新人工启用。
- 指定设备任务成功经历 `pending`、`running`、`completed_partial`,完成任务领取、PDD 链接打开、规则快照执行和结构化结果提交。
+44 -3
View File
@@ -81,6 +81,43 @@ function Read-DatabaseConfig {
}
}
function Read-PortConfig {
param([string]$Path)
$values = @{}
$insidePorts = $false
foreach ($line in Get-Content -LiteralPath $Path) {
if ($line -match '^\s*(#.*)?$') {
continue
}
if ($line -match '^ports\s*:\s*$') {
$insidePorts = $true
continue
}
if ($insidePorts -and $line -match '^\S') {
break
}
if ($insidePorts -and $line -match '^\s+(server|web)\s*:\s*(.*?)\s*$') {
$values[$Matches[1]] = ConvertFrom-YamlScalar $Matches[2]
}
}
foreach ($key in 'server', 'web') {
$parsedPort = 0
if (-not $values.ContainsKey($key) -or
-not [int]::TryParse([string]$values[$key], [ref]$parsedPort) -or
$parsedPort -lt 1 -or $parsedPort -gt 65535) {
throw "ports.$key must be an integer between 1 and 65535 in: $Path"
}
$values[$key] = $parsedPort
}
if ($values.server -eq $values.web) {
throw "ports.server and ports.web must be different in: $Path"
}
return @{ Server = [int]$values.server; Web = [int]$values.web }
}
function Resolve-MySqlClient {
$services = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '^MySQL' -and $_.State -eq 'Running' }
@@ -115,6 +152,7 @@ try {
}
$ConfigPath = [IO.Path]::GetFullPath($ConfigPath)
$databaseConfig = Read-DatabaseConfig $ConfigPath
$portConfig = Read-PortConfig $ConfigPath
if (-not $PSBoundParameters.ContainsKey('DatabaseHost')) {
$DatabaseHost = $databaseConfig.Host
@@ -133,8 +171,9 @@ try {
Write-Host "GoAuto local server" -ForegroundColor Cyan
Write-Host "Config: $ConfigPath"
Write-Host "MySQL: ${DatabaseUser}@${DatabaseHost}:${DatabasePort}/${DatabaseName}"
Write-Host "Ports: server=$($portConfig.Server), web=$($portConfig.Web)"
if ($ValidateConfigOnly) {
Write-Host "Local database configuration is valid." -ForegroundColor Green
Write-Host "Local database and port configuration is valid." -ForegroundColor Green
return
}
@@ -159,6 +198,7 @@ try {
$env:GOAUTO_DB_DRIVER = "mysql"
$env:GOAUTO_DB_DSN = "${DatabaseUser}:${plainPassword}@tcp(${DatabaseHost}:${DatabasePort})/${DatabaseName}?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s"
$env:GOAUTO_SERVER_PORT = [string]$portConfig.Server
Push-Location $serverDirectory
try {
@@ -185,8 +225,8 @@ try {
}
Write-Host "[3/3] Starting server..." -ForegroundColor Green
Write-Host "Local URL: http://127.0.0.1:8000"
Write-Host "Android URL: use this computer's LAN IP on port 8000"
Write-Host "Local URL: http://127.0.0.1:$($portConfig.Server)"
Write-Host "Android URL: use this computer's LAN IP on port $($portConfig.Server)"
Write-Host "Press Ctrl+C to stop." -ForegroundColor DarkYellow
& go run . server -c config/settings.yml
if ($LASTEXITCODE -ne 0) {
@@ -201,5 +241,6 @@ finally {
Remove-Item Env:MYSQL_PWD -ErrorAction SilentlyContinue
Remove-Item Env:GOAUTO_DB_DSN -ErrorAction SilentlyContinue
Remove-Item Env:GOAUTO_DB_DRIVER -ErrorAction SilentlyContinue
Remove-Item Env:GOAUTO_SERVER_PORT -ErrorAction SilentlyContinue
$plainPassword = $null
}
+106
View File
@@ -0,0 +1,106 @@
[CmdletBinding()]
param(
[string]$ConfigPath,
[switch]$ValidateConfigOnly
)
$ErrorActionPreference = "Stop"
$workspaceRoot = Split-Path -Parent $PSScriptRoot
$webDirectory = Join-Path $workspaceRoot "web"
function ConvertFrom-YamlScalar {
param([string]$Value)
$value = $Value.Trim()
if ($value.Length -ge 2) {
if ($value.StartsWith('"') -and $value.EndsWith('"')) {
return $value.Substring(1, $value.Length - 2).Replace('\"', '"').Replace('\\', '\')
}
if ($value.StartsWith("'") -and $value.EndsWith("'")) {
return $value.Substring(1, $value.Length - 2).Replace("''", "'")
}
}
return $value
}
function Read-PortConfig {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
throw "Local configuration file was not found: $Path. Copy config.example.yaml to config.yaml."
}
$values = @{}
$insidePorts = $false
foreach ($line in Get-Content -LiteralPath $Path) {
if ($line -match '^\s*(#.*)?$') {
continue
}
if ($line -match '^ports\s*:\s*$') {
$insidePorts = $true
continue
}
if ($insidePorts -and $line -match '^\S') {
break
}
if ($insidePorts -and $line -match '^\s+(server|web)\s*:\s*(.*?)\s*$') {
$values[$Matches[1]] = ConvertFrom-YamlScalar $Matches[2]
}
}
foreach ($key in 'server', 'web') {
$parsedPort = 0
if (-not $values.ContainsKey($key) -or
-not [int]::TryParse([string]$values[$key], [ref]$parsedPort) -or
$parsedPort -lt 1 -or $parsedPort -gt 65535) {
throw "ports.$key must be an integer between 1 and 65535 in: $Path"
}
$values[$key] = $parsedPort
}
if ($values.server -eq $values.web) {
throw "ports.server and ports.web must be different in: $Path"
}
return @{ Server = [int]$values.server; Web = [int]$values.web }
}
if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
$ConfigPath = Join-Path $workspaceRoot "config.yaml"
}
elseif (-not [IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path $workspaceRoot $ConfigPath
}
$ConfigPath = [IO.Path]::GetFullPath($ConfigPath)
$ports = Read-PortConfig $ConfigPath
Write-Host "GoAuto web UI" -ForegroundColor Cyan
Write-Host "Config: $ConfigPath"
Write-Host "Ports: server=$($ports.Server), web=$($ports.Web)"
if ($ValidateConfigOnly) {
Write-Host "Local port configuration is valid." -ForegroundColor Green
return
}
$pnpm = Get-Command pnpm.cmd -ErrorAction SilentlyContinue
if (-not $pnpm) {
throw "pnpm was not found. Install pnpm and try again."
}
Push-Location $webDirectory
try {
if (-not (Test-Path -LiteralPath "node_modules" -PathType Container)) {
Write-Host "Installing frontend dependencies..." -ForegroundColor Cyan
& $pnpm.Source install --frozen-lockfile
if ($LASTEXITCODE -ne 0) {
throw "Frontend dependency installation failed."
}
}
$env:VUE_APP_BASE_API = "http://127.0.0.1:$($ports.Server)"
Write-Host "Starting GoAuto web UI at http://127.0.0.1:$($ports.Web)" -ForegroundColor Green
& $pnpm.Source exec vite --host 127.0.0.1 --port $ports.Web --strictPort
if ($LASTEXITCODE -ne 0) {
throw "Frontend exited with code $LASTEXITCODE."
}
}
finally {
Remove-Item Env:VUE_APP_BASE_API -ErrorAction SilentlyContinue
Pop-Location
}
+1 -1
View File
@@ -63,7 +63,7 @@ func setup() {
//1. 读取配置
config.Setup(
file.NewSource(file.WithPath(configYml)),
ext.ApplyDatabaseEnvironment,
ext.ApplyEnvironment,
database.Setup,
storage.Setup,
)
+1 -1
View File
@@ -51,7 +51,7 @@ func run() {
//1. 读取配置
config.Setup(
file.NewSource(file.WithPath(configYml)),
ext.ApplyDatabaseEnvironment,
ext.ApplyEnvironment,
initDB,
)
} else {
+13 -7
View File
@@ -2,6 +2,7 @@ package config
import (
"os"
"strconv"
"strings"
sdkconfig "github.com/go-admin-team/go-admin-core/sdk/config"
@@ -10,9 +11,11 @@ import (
var ExtConfig Extend
// Extend 扩展配置
// extend:
// demo:
// name: demo-name
//
// extend:
// demo:
// name: demo-name
//
// 使用方法: config.ExtConfig......即可!!
type Extend struct {
AMap AMap // 这里配置对应配置文件的结构即可
@@ -22,10 +25,13 @@ type AMap struct {
Key string
}
// ApplyDatabaseEnvironment replaces the placeholder database connection with
// process-local values. Credentials stay outside tracked configuration files.
// GOAUTO_DB_DRIVER defaults to mysql when GOAUTO_DB_DSN is present.
func ApplyDatabaseEnvironment() {
// ApplyEnvironment replaces tracked defaults with process-local runtime values.
// Credentials stay outside tracked configuration files. GOAUTO_DB_DRIVER
// defaults to mysql when GOAUTO_DB_DSN is present.
func ApplyEnvironment() {
if port, err := strconv.Atoi(strings.TrimSpace(os.Getenv("GOAUTO_SERVER_PORT"))); err == nil && port >= 1 && port <= 65535 {
sdkconfig.ApplicationConfig.Port = int64(port)
}
dsn := strings.TrimSpace(os.Getenv("GOAUTO_DB_DSN"))
if dsn == "" {
return
+20 -5
View File
@@ -6,18 +6,21 @@ import (
sdkconfig "github.com/go-admin-team/go-admin-core/sdk/config"
)
func TestApplyDatabaseEnvironment(t *testing.T) {
func TestApplyEnvironment(t *testing.T) {
originalDefault := *sdkconfig.DatabaseConfig
originalDatabases := sdkconfig.DatabasesConfig
originalApplication := *sdkconfig.ApplicationConfig
t.Cleanup(func() {
*sdkconfig.DatabaseConfig = originalDefault
sdkconfig.DatabasesConfig = originalDatabases
*sdkconfig.ApplicationConfig = originalApplication
})
sdkconfig.DatabasesConfig = map[string]*sdkconfig.Database{"*": {Driver: "sqlite3", Source: "old.db"}}
t.Setenv("GOAUTO_DB_DSN", "user:secret@tcp(127.0.0.1:3306)/goauto")
t.Setenv("GOAUTO_DB_DRIVER", "mysql")
t.Setenv("GOAUTO_SERVER_PORT", "18000")
ApplyDatabaseEnvironment()
ApplyEnvironment()
if sdkconfig.DatabaseConfig.Driver != "mysql" || sdkconfig.DatabaseConfig.Source == "" {
t.Fatal("default database was not overridden")
@@ -25,18 +28,30 @@ func TestApplyDatabaseEnvironment(t *testing.T) {
if database := sdkconfig.DatabasesConfig["*"]; database.Driver != "mysql" || database.Source != sdkconfig.DatabaseConfig.Source {
t.Fatal("host database was not overridden")
}
if sdkconfig.ApplicationConfig.Port != 18000 {
t.Fatalf("application port = %d, want 18000", sdkconfig.ApplicationConfig.Port)
}
}
func TestApplyDatabaseEnvironmentDoesNothingWithoutDSN(t *testing.T) {
func TestApplyEnvironmentIgnoresEmptyDatabaseAndInvalidPort(t *testing.T) {
original := *sdkconfig.DatabaseConfig
t.Cleanup(func() { *sdkconfig.DatabaseConfig = original })
originalApplication := *sdkconfig.ApplicationConfig
t.Cleanup(func() {
*sdkconfig.DatabaseConfig = original
*sdkconfig.ApplicationConfig = originalApplication
})
sdkconfig.DatabaseConfig.Driver = "sqlite3"
sdkconfig.DatabaseConfig.Source = "local.db"
sdkconfig.ApplicationConfig.Port = 8000
t.Setenv("GOAUTO_DB_DSN", "")
t.Setenv("GOAUTO_SERVER_PORT", "70000")
ApplyDatabaseEnvironment()
ApplyEnvironment()
if sdkconfig.DatabaseConfig.Driver != "sqlite3" || sdkconfig.DatabaseConfig.Source != "local.db" {
t.Fatal("empty environment unexpectedly changed database config")
}
if sdkconfig.ApplicationConfig.Port != 8000 {
t.Fatal("invalid environment unexpectedly changed application port")
}
}
+7 -17
View File
@@ -1,23 +1,13 @@
@echo off
setlocal
cd /d "%~dp0web"
where pnpm >nul 2>nul
if errorlevel 1 (
echo pnpm was not found. Install pnpm and try again.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start-web.ps1" %*
set "GOAUTO_EXIT_CODE=%ERRORLEVEL%"
if not "%GOAUTO_EXIT_CODE%"=="0" (
echo.
echo Startup failed. See the error above.
pause
exit /b 1
)
if not exist "node_modules" (
echo Installing frontend dependencies...
call pnpm install --frozen-lockfile
if errorlevel 1 (
echo Frontend dependency installation failed.
pause
exit /b 1
)
)
echo Starting GoAuto web UI at http://127.0.0.1:9527
call pnpm dev
exit /b %GOAUTO_EXIT_CODE%
+1 -1
View File
@@ -43,7 +43,7 @@ export default defineConfig(({ mode }) => {
// 业务代码沿用 process.env.VUE_APP_* 写法,此处做等价注入
define: {
'process.env.VUE_APP_BASE_API': JSON.stringify(env.VUE_APP_BASE_API || ''),
'process.env.VUE_APP_BASE_API': JSON.stringify(process.env.VUE_APP_BASE_API || env.VUE_APP_BASE_API || ''),
'process.env.NODE_ENV': JSON.stringify(mode === 'development' ? 'development' : 'production')
},