diff --git a/config.example.yaml b/config.example.yaml index 852916a..763b136 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -4,3 +4,7 @@ database: user: root password: "" name: goauto + +ports: + server: 8000 + web: 9527 diff --git a/docs/00-project-profile.md b/docs/00-project-profile.md index f7e8411..b0d0bff 100644 --- a/docs/00-project-profile.md +++ b/docs/00-project-profile.md @@ -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 架构允许未来增加独立采购规则的创建订单能力,但付款能力禁止进入项目。 diff --git a/docs/04-local-development-and-verification.md b/docs/04-local-development-and-verification.md index 850b633..69e473d 100644 --- a/docs/04-local-development-and-verification.md +++ b/docs/04-local-development-and-verification.md @@ -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:` 和 `http://127.0.0.1:`,默认仍是 9527 和 8000。前端开发 API 地址会自动跟随 `ports.server`。修改服务端端口后,还需同步修改 Android Agent 保存的服务地址。API 根路径显示 go-admin 默认欢迎页属于正常现象。 只验证最小闭环迁移和数据库约束: diff --git a/docs/09-delivery-issues.md b/docs/09-delivery-issues.md index bfeb4d9..2899874 100644 --- a/docs/09-delivery-issues.md +++ b/docs/09-delivery-issues.md @@ -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 | ## 延期 diff --git a/docs/10-real-device-acceptance.md b/docs/10-real-device-acceptance.md index eac41e8..e926fc0 100644 --- a/docs/10-real-device-acceptance.md +++ b/docs/10-real-device-acceptance.md @@ -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 链接打开、规则快照执行和结构化结果提交。 diff --git a/scripts/start-server.ps1 b/scripts/start-server.ps1 index fd73d22..4301fec 100644 --- a/scripts/start-server.ps1 +++ b/scripts/start-server.ps1 @@ -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 } diff --git a/scripts/start-web.ps1 b/scripts/start-web.ps1 new file mode 100644 index 0000000..1ba794f --- /dev/null +++ b/scripts/start-web.ps1 @@ -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 +} diff --git a/server/cmd/api/server.go b/server/cmd/api/server.go index fe691e2..01b3399 100644 --- a/server/cmd/api/server.go +++ b/server/cmd/api/server.go @@ -63,7 +63,7 @@ func setup() { //1. 读取配置 config.Setup( file.NewSource(file.WithPath(configYml)), - ext.ApplyDatabaseEnvironment, + ext.ApplyEnvironment, database.Setup, storage.Setup, ) diff --git a/server/cmd/migrate/server.go b/server/cmd/migrate/server.go index ff7602a..1b57da9 100644 --- a/server/cmd/migrate/server.go +++ b/server/cmd/migrate/server.go @@ -51,7 +51,7 @@ func run() { //1. 读取配置 config.Setup( file.NewSource(file.WithPath(configYml)), - ext.ApplyDatabaseEnvironment, + ext.ApplyEnvironment, initDB, ) } else { diff --git a/server/config/extend.go b/server/config/extend.go index cc2f10e..c560cb0 100644 --- a/server/config/extend.go +++ b/server/config/extend.go @@ -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 diff --git a/server/config/extend_test.go b/server/config/extend_test.go index 7e12dbd..3746db6 100644 --- a/server/config/extend_test.go +++ b/server/config/extend_test.go @@ -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") + } } diff --git a/start-web.bat b/start-web.bat index 84552f9..7b1593d 100644 --- a/start-web.bat +++ b/start-web.bat @@ -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% diff --git a/web/vite.config.mjs b/web/vite.config.mjs index 37162db..c9cc32f 100644 --- a/web/vite.config.mjs +++ b/web/vite.config.mjs @@ -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') },