fix(docker): multi-theme packaging with PS1 orchestration and full bundle export

Replace all-themes bat logic with PowerShell to survive branch checkout; add api-admin and full-themes service modes; fix bat findstr path check and cmd exit codes; document six-in-one tar and update gitignore for player/full-themes artifacts.
This commit is contained in:
2026-06-24 14:18:31 +08:00
parent 25dd0cf514
commit 9c5e8d6f5c
8 changed files with 425 additions and 298 deletions

4
.gitignore vendored
View File

@@ -8,6 +8,10 @@ docker-build.log
thebet365-images.tar thebet365-images.tar
thebet365-images-*.tar thebet365-images-*.tar
thebet365-images-*.manifest.txt thebet365-images-*.manifest.txt
thebet365-full-themes-*.tar
thebet365-full-themes-*.manifest.txt
thebet365-player-*.tar
thebet365-player-*.manifest.txt
thebet365-admin-latest.tar thebet365-admin-latest.tar
thebet365-admin-latest.manifest.txt thebet365-admin-latest.manifest.txt
.claude/ .claude/

View File

@@ -1,261 +1,13 @@
@echo off @echo off
setlocal EnableDelayedExpansion REM Thin launcher for build-and-export-all-themes.ps1
REM CMD reads batch files line-by-line from disk; git checkout to theme-* removes
REM ================================================================ REM this file from the working tree. PowerShell loads the script into memory first.
REM build-and-export-all-themes.bat
REM REM
REM Four-theme player image batch builder.
REM Automatically switches Git branches to build each theme.
REM Usage: REM Usage:
REM docs\docker\build-and-export-all-themes.bat REM docs\docker\build-and-export-all-themes.bat
REM docs\docker\build-and-export-all-themes.bat --use-cache REM docs\docker\build-and-export-all-themes.bat --use-cache
REM docs\docker\build-and-export-all-themes.bat --export-only REM docs\docker\build-and-export-all-themes.bat --export-only
REM docs\docker\build-and-export-all-themes.bat --skip-api-admin REM docs\docker\build-and-export-all-themes.bat --skip-api-admin
REM
REM Output files (project root, already in .gitignore):
REM thebet365-player-main.tar -> thebet365-player:main
REM thebet365-player-theme-2.tar -> thebet365-player:theme-2
REM thebet365-player-theme-3.tar -> thebet365-player:theme-3
REM thebet365-player-theme-4.tar -> thebet365-player:theme-4
REM thebet365-images-latest.tar -> api:latest + admin:latest
REM ================================================================
cd /d "%~dp0..\.." powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build-and-export-all-themes.ps1" %*
set "ROOT_DIR=%CD%" exit /b %ERRORLEVEL%
set "USE_CACHE=0"
set "EXPORT_ONLY=0"
set "SKIP_API_ADMIN=0"
:parse_args
if "%~1"=="" goto args_done
if /i "%~1"=="--use-cache" set "USE_CACHE=1" & shift & goto parse_args
if /i "%~1"=="--export-only" set "EXPORT_ONLY=1" & shift & goto parse_args
if /i "%~1"=="--skip-api-admin" set "SKIP_API_ADMIN=1" & shift & goto parse_args
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
if /i "%~1"=="/?" goto show_help
echo Unknown argument: %~1
goto show_help
:show_help
echo.
echo Usage: docs\docker\build-and-export-all-themes.bat [options]
echo.
echo --use-cache Use Docker build cache (default: --no-cache)
echo --export-only Skip build, export existing images only
echo --skip-api-admin Only build 4 player themes, skip api/admin
echo -h, --help Show this help
echo.
echo Output (project root):
echo thebet365-player-main.tar
echo thebet365-player-theme-2.tar
echo thebet365-player-theme-3.tar
echo thebet365-player-theme-4.tar
echo thebet365-images-latest.tar (api + admin, unless --skip-api-admin)
echo.
exit /b 0
:args_done
REM -- Validate docker is available
where docker >nul 2>&1
if errorlevel 1 (
echo ERROR: docker not found. Please start Docker Desktop first.
exit /b 1
)
REM -- Record current branch so we can restore it
set "ORIGINAL_BRANCH="
for /f "delims=" %%B in ('git rev-parse --abbrev-ref HEAD 2^>nul') do set "ORIGINAL_BRANCH=%%B"
if not defined ORIGINAL_BRANCH (
echo ERROR: Cannot determine current Git branch. Run this script from the repo root.
exit /b 1
)
echo [INFO] Current branch: %ORIGINAL_BRANCH%
REM -- Stash any local changes so branch switching works cleanly
set "DID_STASH=0"
git diff --quiet 2>nul
set "D1=!ERRORLEVEL!"
git diff --cached --quiet 2>nul
set "D2=!ERRORLEVEL!"
if not "!D1!"=="0" goto do_stash
if not "!D2!"=="0" goto do_stash
goto stash_done
:do_stash
echo [INFO] Stashing local changes before branch switching...
git stash push -m "build-all-themes auto-stash" >nul 2>&1
if errorlevel 1 (
echo [WARN] git stash failed. Branch switching may fail if there are conflicts.
) else (
set "DID_STASH=1"
echo [INFO] Changes stashed.
)
:stash_done
REM -- Build extra arguments for the base script
set "EXTRA_ARGS="
if "%USE_CACHE%"=="1" set "EXTRA_ARGS=!EXTRA_ARGS! --use-cache"
if "%EXPORT_ONLY%"=="1" set "EXTRA_ARGS=!EXTRA_ARGS! --export-only"
set "FAILED_THEMES="
set "SUCCESS_COUNT=0"
REM ================================================================
REM Build each player theme
REM ================================================================
call :build_theme main main
call :build_theme theme-2 theme-2
call :build_theme theme-3 theme-3
call :build_theme theme-4 theme-4
REM -- Restore original branch
echo.
echo [INFO] Restoring original branch: %ORIGINAL_BRANCH%
cd /d "%ROOT_DIR%"
git checkout %ORIGINAL_BRANCH% >nul 2>&1
if errorlevel 1 (
echo [WARN] Could not restore branch %ORIGINAL_BRANCH%. Run: git checkout %ORIGINAL_BRANCH%
) else (
echo [INFO] Restored to %ORIGINAL_BRANCH%
)
REM -- Pop stash if we stashed earlier
if "%DID_STASH%"=="1" (
echo [INFO] Restoring stashed changes...
git stash pop >nul 2>&1
if errorlevel 1 (
echo [WARN] git stash pop failed. Run: git stash pop
) else (
echo [INFO] Stash restored.
)
)
REM -- Build api + admin on main branch
if "%SKIP_API_ADMIN%"=="1" goto skip_api_admin
echo.
echo [INFO] Building api + admin (tag: latest) on branch main...
cd /d "%ROOT_DIR%"
git checkout main >nul 2>&1
cd /d "%ROOT_DIR%"
call docs\docker\build-and-export-images.bat --tag latest!EXTRA_ARGS!
set "API_ERR=!ERRORLEVEL!"
cd /d "%ROOT_DIR%"
if "!API_ERR!"=="0" goto api_ok
echo [ERROR] api/admin build failed
set "FAILED_THEMES=!FAILED_THEMES! api/admin"
goto after_api_admin
:api_ok
set /a SUCCESS_COUNT+=1
:after_api_admin
echo.
echo [INFO] Restoring original branch: %ORIGINAL_BRANCH%
git checkout %ORIGINAL_BRANCH% >nul 2>&1
:skip_api_admin
REM ================================================================
REM Summary
REM ================================================================
echo.
echo ================================================================
echo BUILD SUMMARY
echo ================================================================
echo Succeeded: !SUCCESS_COUNT! task(s)
if not defined FAILED_THEMES goto all_ok
echo Failed: !FAILED_THEMES!
echo.
echo Tip: Retry failed themes manually:
echo git checkout ^<branch^>
echo docs\docker\build-and-export-images.bat --service player --tag ^<tag^>
echo ================================================================
echo.
endlocal
exit /b 1
:all_ok
echo.
echo All done! Output files in project root:
echo thebet365-player-main.tar
echo thebet365-player-theme-2.tar
echo thebet365-player-theme-3.tar
echo thebet365-player-theme-4.tar
if "%SKIP_API_ADMIN%"=="0" (
echo thebet365-images-latest.tar (api + admin)
)
echo.
echo Server import commands:
echo docker load -i thebet365-images-latest.tar
echo docker load -i thebet365-player-main.tar
echo docker load -i thebet365-player-theme-2.tar
echo docker load -i thebet365-player-theme-3.tar
echo docker load -i thebet365-player-theme-4.tar
echo.
echo Start (first multi-theme deploy):
echo docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d
echo ================================================================
echo.
endlocal
exit /b 0
REM ================================================================
REM :build_theme <branch> <tag>
REM Switch to branch, build player, mark success/fail.
REM Uses cd /d ROOT_DIR instead of pushd to avoid stack issues.
REM ================================================================
:build_theme
set "THEME_BRANCH=%~1"
set "THEME_TAG=%~2"
echo.
echo ----------------------------------------------------------------
echo [INFO] Theme: %THEME_BRANCH% (image tag: %THEME_TAG%)
echo ----------------------------------------------------------------
cd /d "%ROOT_DIR%"
git checkout %THEME_BRANCH% >nul 2>&1
if errorlevel 1 (
echo [ERROR] Cannot switch to branch %THEME_BRANCH% -- skipping
set "FAILED_THEMES=!FAILED_THEMES! %THEME_BRANCH%"
exit /b 0
)
echo [INFO] Switched to %THEME_BRANCH%
cd /d "%ROOT_DIR%"
call docs\docker\build-and-export-images.bat --service player --tag %THEME_TAG%!EXTRA_ARGS!
set "BUILD_ERR=!ERRORLEVEL!"
cd /d "%ROOT_DIR%"
if "!BUILD_ERR!"=="0" goto build_ok_%THEME_TAG%
echo [ERROR] Player build failed for %THEME_BRANCH%
set "FAILED_THEMES=!FAILED_THEMES! %THEME_BRANCH%"
exit /b 0
:build_ok_main
echo [OK] thebet365-player:main exported successfully
set /a SUCCESS_COUNT+=1
exit /b 0
:build_ok_theme-2
echo [OK] thebet365-player:theme-2 exported successfully
set /a SUCCESS_COUNT+=1
exit /b 0
:build_ok_theme-3
echo [OK] thebet365-player:theme-3 exported successfully
set /a SUCCESS_COUNT+=1
exit /b 0
:build_ok_theme-4
echo [OK] thebet365-player:theme-4 exported successfully
set /a SUCCESS_COUNT+=1
exit /b 0

View File

@@ -0,0 +1,285 @@
# 四套主题 player 一键打包(自动切换 Git 分支)
# PowerShell 会在启动时将整份脚本载入内存git checkout 后仍可继续执行。
# 用法(项目根目录或本目录均可):
# .\docs\docker\build-and-export-all-themes.ps1
# .\docs\docker\build-and-export-all-themes.ps1 -UseCache
# .\docs\docker\build-and-export-all-themes.ps1 -ExportOnly
# .\docs\docker\build-and-export-all-themes.ps1 -SkipApiAdmin
# 或通过 bat 启动(参数支持 --use-cache 等 CMD 风格):
# docs\docker\build-and-export-all-themes.bat --use-cache
param(
[switch]$UseCache,
[switch]$ExportOnly,
[switch]$SkipApiAdmin,
[switch]$SkipBundle,
[switch]$Help
)
$ErrorActionPreference = "Stop"
foreach ($arg in $args) {
switch ($arg.ToLowerInvariant()) {
"--use-cache" { $UseCache = $true; continue }
"--export-only" { $ExportOnly = $true; continue }
"--skip-api-admin" { $SkipApiAdmin = $true; continue }
"--skip-bundle" { $SkipBundle = $true; continue }
"-h" { $Help = $true; continue }
"--help" { $Help = $true; continue }
"/?" { $Help = $true; continue }
default { throw "Unknown argument: $arg" }
}
}
function Show-Help {
Write-Host @"
Usage: docs\docker\build-and-export-all-themes.ps1 [options]
-UseCache, --use-cache Use Docker build cache (default: --no-cache)
-ExportOnly, --export-only Skip build, export existing images only
-SkipApiAdmin, --skip-api-admin
Only build 4 player themes, skip api/admin
-SkipBundle, --skip-bundle Skip thebet365-full-themes-latest.tar (6-in-1 bundle)
-Help, -h, --help Show this help
Output (project root):
thebet365-player-main.tar
thebet365-player-theme-2.tar
thebet365-player-theme-3.tar
thebet365-player-theme-4.tar
thebet365-images-latest.tar (api + admin, unless -SkipApiAdmin)
thebet365-full-themes-latest.tar (api + admin + 4 player themes, unless -SkipBundle)
"@
}
if ($Help) {
Show-Help
exit 0
}
$Root = (Resolve-Path (Join-Path $PSScriptRoot "../..")).Path
$Themes = @(
@{ Branch = "main"; Tag = "main" },
@{ Branch = "theme-2"; Tag = "theme-2" },
@{ Branch = "theme-3"; Tag = "theme-3" },
@{ Branch = "theme-4"; Tag = "theme-4" }
)
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw "docker not found. Please start Docker Desktop first."
}
if (-not (Test-Path (Join-Path $Root "docs\docker\build-and-export-images.bat"))) {
throw "Missing build script: docs\docker\build-and-export-images.bat"
}
function Invoke-Git {
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$GitArgs)
$prevErrorAction = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
try {
& git @GitArgs 2>$null | Out-Null
return $LASTEXITCODE
} finally {
$ErrorActionPreference = $prevErrorAction
}
}
function Get-GitOutput {
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$GitArgs)
$prevErrorAction = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
try {
return ((& git @GitArgs 2>$null | Out-String).Trim())
} finally {
$ErrorActionPreference = $prevErrorAction
}
}
Set-Location $Root
$OriginalBranch = Get-GitOutput rev-parse --abbrev-ref HEAD
if ([string]::IsNullOrWhiteSpace($OriginalBranch)) {
throw "Cannot determine current Git branch. Run this script from the repo root."
}
Write-Host "[INFO] Current branch: $OriginalBranch"
$DidStash = $false
$diffExit = Invoke-Git diff --quiet
$cachedExit = Invoke-Git diff --cached --quiet
if ($diffExit -ne 0 -or $cachedExit -ne 0) {
Write-Host "[INFO] Stashing local changes before branch switching..."
$stashExit = Invoke-Git stash push -m "build-all-themes auto-stash"
if ($stashExit -eq 0) {
$DidStash = $true
Write-Host "[INFO] Changes stashed."
} else {
Write-Warning "git stash failed. Branch switching may fail if there are conflicts."
}
}
$FailedThemes = [System.Collections.Generic.List[string]]::new()
$SuccessCount = 0
function Invoke-BuildExport {
param(
[string]$Service = "",
[string]$Tag
)
$argList = @()
if (-not [string]::IsNullOrWhiteSpace($Service)) {
$argList += "--service"
$argList += $Service
}
$argList += "--tag"
$argList += $Tag
if ($UseCache) { $argList += "--use-cache" }
if ($ExportOnly) { $argList += "--export-only" }
Set-Location $Root
$batPath = Join-Path $Root "docs\docker\build-and-export-images.bat"
$argText = ($argList | ForEach-Object {
if ($_ -match '\s') { "`"$_`"" } else { $_ }
}) -join ' '
& cmd.exe /c "`"$batPath`" $argText"
return $LASTEXITCODE
}
function Invoke-PlayerThemeBuild {
param(
[string]$Branch,
[string]$Tag
)
Write-Host ""
Write-Host "----------------------------------------------------------------"
Write-Host "[INFO] Theme: $Branch (image tag: $Tag)"
Write-Host "----------------------------------------------------------------"
Set-Location $Root
$checkoutExit = Invoke-Git checkout $Branch
if ($checkoutExit -ne 0) {
Write-Host "[ERROR] Cannot switch to branch $Branch -- skipping"
$script:FailedThemes.Add($Branch) | Out-Null
return
}
Write-Host "[INFO] Switched to $Branch"
Set-Location $Root
if ((Invoke-BuildExport -Service "player" -Tag $Tag) -ne 0) {
Write-Host "[ERROR] Player build failed for $Branch"
$script:FailedThemes.Add($Branch) | Out-Null
return
}
Write-Host "[OK] thebet365-player:$Tag exported successfully"
$script:SuccessCount++
}
foreach ($theme in $Themes) {
Invoke-PlayerThemeBuild -Branch $theme.Branch -Tag $theme.Tag
}
Write-Host ""
Write-Host "[INFO] Restoring original branch: $OriginalBranch"
Set-Location $Root
if ((Invoke-Git checkout $OriginalBranch) -eq 0) {
Write-Host "[INFO] Restored to $OriginalBranch"
} else {
Write-Warning "Could not restore branch $OriginalBranch. Run: git checkout $OriginalBranch"
}
if ($DidStash) {
Write-Host "[INFO] Restoring stashed changes..."
if ((Invoke-Git stash pop) -eq 0) {
Write-Host "[INFO] Stash restored."
} else {
Write-Warning "git stash pop failed. Run: git stash pop"
}
}
if (-not $SkipApiAdmin) {
Write-Host ""
Write-Host "[INFO] Building api + admin (tag: latest) on branch main..."
Set-Location $Root
if ((Invoke-Git checkout main) -ne 0) {
Write-Host "[ERROR] Cannot switch to branch main for api/admin build"
$FailedThemes.Add("api/admin") | Out-Null
} else {
Set-Location $Root
if ((Invoke-BuildExport -Service "api-admin" -Tag "latest") -ne 0) {
Write-Host "[ERROR] api/admin build failed"
$FailedThemes.Add("api/admin") | Out-Null
} else {
$SuccessCount++
}
}
Write-Host ""
Write-Host "[INFO] Restoring original branch: $OriginalBranch"
Set-Location $Root
Invoke-Git checkout $OriginalBranch | Out-Null
}
if (-not $SkipBundle -and -not $SkipApiAdmin -and $FailedThemes.Count -eq 0) {
Write-Host ""
Write-Host "[INFO] Exporting full bundle (api + admin + 4 player themes)..."
Set-Location $Root
if ((Invoke-BuildExport -Service "full-themes" -Tag "latest") -ne 0) {
Write-Host "[ERROR] Full bundle export failed"
$FailedThemes.Add("full-bundle") | Out-Null
} else {
Write-Host "[OK] thebet365-full-themes-latest.tar exported successfully"
$SuccessCount++
}
}
Write-Host ""
Write-Host "================================================================"
Write-Host " BUILD SUMMARY"
Write-Host "================================================================"
Write-Host " Succeeded: $SuccessCount task(s)"
if ($FailedThemes.Count -gt 0) {
Write-Host " Failed: $($FailedThemes -join ' ')"
Write-Host ""
Write-Host " Tip: Retry failed themes manually:"
Write-Host " git checkout <branch>"
Write-Host " docs\docker\build-and-export-images.bat --service player --tag <tag>"
Write-Host "================================================================"
exit 1
}
Write-Host ""
Write-Host " All done! Output files in project root:"
Write-Host " thebet365-player-main.tar"
Write-Host " thebet365-player-theme-2.tar"
Write-Host " thebet365-player-theme-3.tar"
Write-Host " thebet365-player-theme-4.tar"
if (-not $SkipApiAdmin) {
Write-Host " thebet365-images-latest.tar (api + admin)"
}
if (-not $SkipBundle -and -not $SkipApiAdmin) {
Write-Host " thebet365-full-themes-latest.tar (api + admin + 4 themes, upload this only)"
}
Write-Host ""
Write-Host " Server import (single bundle):"
Write-Host " docker load -i thebet365-full-themes-latest.tar"
Write-Host " docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d"
Write-Host ""
Write-Host " Or import separate tars:"
if (-not $SkipApiAdmin) {
Write-Host " docker load -i thebet365-images-latest.tar"
}
Write-Host " docker load -i thebet365-player-main.tar"
Write-Host " docker load -i thebet365-player-theme-2.tar"
Write-Host " docker load -i thebet365-player-theme-3.tar"
Write-Host " docker load -i thebet365-player-theme-4.tar"
Write-Host "================================================================"
exit 0

View File

@@ -69,7 +69,7 @@ goto parse_args
echo. echo.
echo Usage: docs\docker\build-and-export-images.bat [options] echo Usage: docs\docker\build-and-export-images.bat [options]
echo. echo.
echo --service SVC api ^| player ^| admin ^| all (default: all) echo --service SVC api ^| player ^| admin ^| api-admin ^| full-themes ^| all (default: all)
echo --tag TAG Image tag (default: latest) echo --tag TAG Image tag (default: latest)
echo --use-cache Use Docker build cache echo --use-cache Use Docker build cache
echo --export-only Skip build, export existing images only echo --export-only Skip build, export existing images only
@@ -117,8 +117,7 @@ if errorlevel 1 exit /b 1
if not defined OUTPUT call :default_output if not defined OUTPUT call :default_output
set "OUTPUT_PATH=%OUTPUT%" set "OUTPUT_PATH=%OUTPUT%"
echo %OUTPUT_PATH%| findstr /r "^[A-Za-z]:\\" >nul if /i not "%OUTPUT:~1,1%"==":" set "OUTPUT_PATH=%CD%\%OUTPUT%"
if errorlevel 1 set "OUTPUT_PATH=%CD%\%OUTPUT%"
where docker >nul 2>&1 where docker >nul 2>&1
if errorlevel 1 ( if errorlevel 1 (
@@ -126,7 +125,7 @@ if errorlevel 1 (
exit /b 1 exit /b 1
) )
if "%EXPORT_ONLY%"=="0" goto do_build if "%EXPORT_ONLY%"=="0" if /i not "%SERVICE%"=="full-themes" goto do_build
goto do_export goto do_export
:do_build :do_build
@@ -135,6 +134,8 @@ set "IMAGE_TAG=%TAG%"
if "%NO_CACHE%"=="1" goto build_no_cache if "%NO_CACHE%"=="1" goto build_no_cache
if /i "%SERVICE%"=="all" ( if /i "%SERVICE%"=="all" (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build api player admin docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build api player admin
) else if /i "%SERVICE%"=="api-admin" (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build api admin
) else ( ) else (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build %SERVICE% docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build %SERVICE%
) )
@@ -143,6 +144,8 @@ goto build_done
:build_no_cache :build_no_cache
if /i "%SERVICE%"=="all" ( if /i "%SERVICE%"=="all" (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build --no-cache api player admin docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build --no-cache api player admin
) else if /i "%SERVICE%"=="api-admin" (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build --no-cache api admin
) else ( ) else (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build --no-cache %SERVICE% docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build --no-cache %SERVICE%
) )
@@ -157,6 +160,10 @@ if errorlevel 1 (
echo ==^> Exporting %SERVICE% to %OUTPUT_PATH% echo ==^> Exporting %SERVICE% to %OUTPUT_PATH%
if /i "%SERVICE%"=="all" ( if /i "%SERVICE%"=="all" (
docker save thebet365-api:%TAG% thebet365-player:%TAG% thebet365-admin:%TAG% -o "%OUTPUT_PATH%" docker save thebet365-api:%TAG% thebet365-player:%TAG% thebet365-admin:%TAG% -o "%OUTPUT_PATH%"
) else if /i "%SERVICE%"=="api-admin" (
docker save thebet365-api:%TAG% thebet365-admin:%TAG% -o "%OUTPUT_PATH%"
) else if /i "%SERVICE%"=="full-themes" (
docker save thebet365-api:%TAG% thebet365-admin:%TAG% thebet365-player:main thebet365-player:theme-2 thebet365-player:theme-3 thebet365-player:theme-4 -o "%OUTPUT_PATH%"
) else ( ) else (
docker save thebet365-%SERVICE%:%TAG% -o "%OUTPUT_PATH%" docker save thebet365-%SERVICE%:%TAG% -o "%OUTPUT_PATH%"
) )
@@ -187,16 +194,34 @@ for %%F in ("%OUTPUT_PATH%") do set /a SIZE_MB=%%~zF/1048576
echo git_dirty=!GIT_DIRTY! echo git_dirty=!GIT_DIRTY!
echo tar=!TAR_NAME! echo tar=!TAR_NAME!
echo tar_sha256=!TAR_SHA256! echo tar_sha256=!TAR_SHA256!
if /i not "!SERVICE!"=="all" ( if /i "!SERVICE!"=="all" (
echo image=thebet365-!SERVICE!:%TAG%
echo image_id=!SVC_IMAGE_ID!
) else (
echo api_image=thebet365-api:%TAG% echo api_image=thebet365-api:%TAG%
echo api_image_id=!API_IMAGE_ID! echo api_image_id=!API_IMAGE_ID!
echo player_image=thebet365-player:%TAG% echo player_image=thebet365-player:%TAG%
echo player_image_id=!PLAYER_IMAGE_ID! echo player_image_id=!PLAYER_IMAGE_ID!
echo admin_image=thebet365-admin:%TAG% echo admin_image=thebet365-admin:%TAG%
echo admin_image_id=!ADMIN_IMAGE_ID! echo admin_image_id=!ADMIN_IMAGE_ID!
) else if /i "!SERVICE!"=="api-admin" (
echo api_image=thebet365-api:%TAG%
echo api_image_id=!API_IMAGE_ID!
echo admin_image=thebet365-admin:%TAG%
echo admin_image_id=!ADMIN_IMAGE_ID!
) else if /i "!SERVICE!"=="full-themes" (
echo api_image=thebet365-api:%TAG%
echo api_image_id=!API_IMAGE_ID!
echo admin_image=thebet365-admin:%TAG%
echo admin_image_id=!ADMIN_IMAGE_ID!
echo player_main_image=thebet365-player:main
echo player_main_image_id=!PLAYER_MAIN_IMAGE_ID!
echo player_theme_2_image=thebet365-player:theme-2
echo player_theme_2_image_id=!PLAYER_THEME2_IMAGE_ID!
echo player_theme_3_image=thebet365-player:theme-3
echo player_theme_3_image_id=!PLAYER_THEME3_IMAGE_ID!
echo player_theme_4_image=thebet365-player:theme-4
echo player_theme_4_image_id=!PLAYER_THEME4_IMAGE_ID!
) else (
echo image=thebet365-!SERVICE!:%TAG%
echo image_id=!SVC_IMAGE_ID!
) )
) )
@@ -207,6 +232,12 @@ echo Server load and recreate:
if /i "%SERVICE%"=="all" ( if /i "%SERVICE%"=="all" (
echo docker load -i !TAR_NAME! echo docker load -i !TAR_NAME!
echo docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate api player admin echo docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate api player admin
) else if /i "%SERVICE%"=="api-admin" (
echo docker load -i !TAR_NAME!
echo docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate api admin
) else if /i "%SERVICE%"=="full-themes" (
echo docker load -i !TAR_NAME!
echo docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d
) else ( ) else (
echo docker load -i !TAR_NAME! echo docker load -i !TAR_NAME!
echo docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate %SERVICE% echo docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate %SERVICE%
@@ -223,6 +254,10 @@ exit /b 0
:default_output :default_output
if /i "%SERVICE%"=="all" ( if /i "%SERVICE%"=="all" (
set "OUTPUT=thebet365-images-%TAG%.tar" set "OUTPUT=thebet365-images-%TAG%.tar"
) else if /i "%SERVICE%"=="api-admin" (
set "OUTPUT=thebet365-images-%TAG%.tar"
) else if /i "%SERVICE%"=="full-themes" (
set "OUTPUT=thebet365-full-themes-%TAG%.tar"
) else ( ) else (
set "OUTPUT=thebet365-%SERVICE%-%TAG%.tar" set "OUTPUT=thebet365-%SERVICE%-%TAG%.tar"
) )
@@ -230,10 +265,12 @@ exit /b 0
:validate_service :validate_service
if /i "%SERVICE%"=="all" exit /b 0 if /i "%SERVICE%"=="all" exit /b 0
if /i "%SERVICE%"=="api-admin" exit /b 0
if /i "%SERVICE%"=="full-themes" exit /b 0
if /i "%SERVICE%"=="api" exit /b 0 if /i "%SERVICE%"=="api" exit /b 0
if /i "%SERVICE%"=="player" exit /b 0 if /i "%SERVICE%"=="player" exit /b 0
if /i "%SERVICE%"=="admin" exit /b 0 if /i "%SERVICE%"=="admin" exit /b 0
echo ERROR: invalid --service: %SERVICE% (use api, player, admin, or all) echo ERROR: invalid --service: %SERVICE% (use api, player, admin, api-admin, full-themes, or all)
echo TIP: 四套主题一键打包请使用 docs\docker\build-and-export-all-themes.bat echo TIP: 四套主题一键打包请使用 docs\docker\build-and-export-all-themes.bat
exit /b 1 exit /b 1
@@ -273,10 +310,19 @@ set "API_IMAGE_ID="
set "PLAYER_IMAGE_ID=" set "PLAYER_IMAGE_ID="
set "ADMIN_IMAGE_ID=" set "ADMIN_IMAGE_ID="
set "SVC_IMAGE_ID=" set "SVC_IMAGE_ID="
set "PLAYER_MAIN_IMAGE_ID="
set "PLAYER_THEME2_IMAGE_ID="
set "PLAYER_THEME3_IMAGE_ID="
set "PLAYER_THEME4_IMAGE_ID="
for /f "delims=" %%I in ('docker image inspect thebet365-api:%TAG% --format "{{.Id}}" 2^>nul') do set "API_IMAGE_ID=%%I" for /f "delims=" %%I in ('docker image inspect thebet365-api:%TAG% --format "{{.Id}}" 2^>nul') do set "API_IMAGE_ID=%%I"
for /f "delims=" %%I in ('docker image inspect thebet365-player:%TAG% --format "{{.Id}}" 2^>nul') do set "PLAYER_IMAGE_ID=%%I" for /f "delims=" %%I in ('docker image inspect thebet365-player:%TAG% --format "{{.Id}}" 2^>nul') do set "PLAYER_IMAGE_ID=%%I"
for /f "delims=" %%I in ('docker image inspect thebet365-admin:%TAG% --format "{{.Id}}" 2^>nul') do set "ADMIN_IMAGE_ID=%%I" for /f "delims=" %%I in ('docker image inspect thebet365-admin:%TAG% --format "{{.Id}}" 2^>nul') do set "ADMIN_IMAGE_ID=%%I"
if /i not "%SERVICE%"=="all" ( if /i "%SERVICE%"=="full-themes" (
for /f "delims=" %%I in ('docker image inspect thebet365-player:main --format "{{.Id}}" 2^>nul') do set "PLAYER_MAIN_IMAGE_ID=%%I"
for /f "delims=" %%I in ('docker image inspect thebet365-player:theme-2 --format "{{.Id}}" 2^>nul') do set "PLAYER_THEME2_IMAGE_ID=%%I"
for /f "delims=" %%I in ('docker image inspect thebet365-player:theme-3 --format "{{.Id}}" 2^>nul') do set "PLAYER_THEME3_IMAGE_ID=%%I"
for /f "delims=" %%I in ('docker image inspect thebet365-player:theme-4 --format "{{.Id}}" 2^>nul') do set "PLAYER_THEME4_IMAGE_ID=%%I"
) else if /i not "%SERVICE%"=="all" (
for /f "delims=" %%I in ('docker image inspect thebet365-%SERVICE%:%TAG% --format "{{.Id}}" 2^>nul') do set "SVC_IMAGE_ID=%%I" for /f "delims=" %%I in ('docker image inspect thebet365-%SERVICE%:%TAG% --format "{{.Id}}" 2^>nul') do set "SVC_IMAGE_ID=%%I"
) )
exit /b 0 exit /b 0

View File

@@ -1,17 +1,17 @@
# 鏋勫缓 api / player / admin 鐢熶骇闀滃儚骞跺<EFBFBD>鍑轰负 tar # ?? api / player / admin ???????? tar
# 鐢ㄦ硶锛堝湪椤圭洰鏍圭洰褰曟垨鏈<EFBFBD>洰褰曟墽琛屽潎鍙<EFBFBD>: # ??????????????????:
# .\docs\docker\build-and-export-images.ps1 # .\docs\docker\build-and-export-images.ps1
# .\docs\docker\build-and-export-images.ps1 -Service admin # .\docs\docker\build-and-export-images.ps1 -Service admin
# .\docs\docker\build-and-export-images.ps1 -Tag v1.2.3 # .\docs\docker\build-and-export-images.ps1 -Tag v1.2.3
# .\docs\docker\build-and-export-images.ps1 -UseCache # .\docs\docker\build-and-export-images.ps1 -UseCache
# .\docs\docker\build-and-export-images.ps1 -ExportOnly # .\docs\docker\build-and-export-images.ps1 -ExportOnly
# #
# 四套主题玩家端一键打包(自动切分支,推荐): # ????????????????????????????????????
# docs\docker\build-and-export-all-themes.bat # docs\docker\build-and-export-all-themes.bat
param( param(
[ValidateSet("all", "api", "player", "admin")] [ValidateSet("all", "api", "player", "admin", "api-admin")]
[string]$Service = "all", [string]$Service = "all",
[string]$Tag = $env:IMAGE_TAG, [string]$Tag = $env:IMAGE_TAG,
[switch]$UseCache, [switch]$UseCache,
@@ -31,27 +31,27 @@ function Get-DefaultTag { "latest" }
function Get-DefaultOutput { function Get-DefaultOutput {
param([string]$Svc, [string]$ImageTag) param([string]$Svc, [string]$ImageTag)
if ($Svc -eq "all") { return "thebet365-images-$ImageTag.tar" } if ($Svc -eq "all" -or $Svc -eq "api-admin") { return "thebet365-images-$ImageTag.tar" }
return "thebet365-$Svc-$ImageTag.tar" return "thebet365-$Svc-$ImageTag.tar"
} }
function Assert-ImageTag { function Assert-ImageTag {
param([string]$Value) param([string]$Value)
if ($Value -notmatch '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$') { if ($Value -notmatch '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$') {
throw "闀滃儚 tag 涓嶅悎娉? $Value" throw "?? tag ???? $Value"
} }
} }
if (-not (Test-Path $ComposeFile)) { if (-not (Test-Path $ComposeFile)) {
throw "<EFBFBD>壘鍒?$ComposeFile锛堝綋鍓嶇洰褰? $Root? throw "????$ComposeFile?????? $Root??
} }
if (-not (Test-Path $EnvFile)) { if (-not (Test-Path $EnvFile)) {
if (Test-Path ".env.docker.example") { if (Test-Path ".env.docker.example") {
Write-Warning "<EFBFBD>壘鍒?.env.docker锛屼娇鐢?.env.docker.example锛堢敓浜ц<EFBFBD>澶嶅埗骞朵慨鏀瑰瘑閽ワ級" Write-Warning "????.env.docker????.env.docker.example????????????"
$EnvFile = ".env.docker.example" $EnvFile = ".env.docker.example"
} else { } else {
throw "<EFBFBD>壘鍒?.env.docker ?.env.docker.example" throw "????.env.docker ??.env.docker.example"
} }
} }
@@ -60,7 +60,11 @@ if ([string]::IsNullOrWhiteSpace($Tag)) {
} }
Assert-ImageTag $Tag Assert-ImageTag $Tag
$BuildServices = if ($Service -eq "all") { $AllServices } else { @($Service) } $BuildServices = switch ($Service) {
"all" { $AllServices }
"api-admin" { @("api", "admin") }
default { @($Service) }
}
$SaveImages = $BuildServices | ForEach-Object { "thebet365-${_}:${Tag}" } $SaveImages = $BuildServices | ForEach-Object { "thebet365-${_}:${Tag}" }
if ([string]::IsNullOrWhiteSpace($Output)) { if ([string]::IsNullOrWhiteSpace($Output)) {
@@ -68,7 +72,7 @@ if ([string]::IsNullOrWhiteSpace($Output)) {
} }
if (-not $ExportOnly) { if (-not $ExportOnly) {
Write-Host "==> 鏋勫缓闀滃儚: $($BuildServices -join ', ') (tag: $Tag)" Write-Host "==> ????: $($BuildServices -join ', ') (tag: $Tag)"
$buildArgs = @( $buildArgs = @(
"compose", "-f", $ComposeFile, "--env-file", $EnvFile, "build" "compose", "-f", $ComposeFile, "--env-file", $EnvFile, "build"
) )
@@ -80,7 +84,7 @@ if (-not $ExportOnly) {
try { try {
$env:IMAGE_TAG = $Tag $env:IMAGE_TAG = $Tag
& docker @buildArgs & docker @buildArgs
if ($LASTEXITCODE -ne 0) { throw "docker build 澶辫触锛岄鍑虹爜 $LASTEXITCODE" } if ($LASTEXITCODE -ne 0) { throw "docker build ?????? $LASTEXITCODE" }
} finally { } finally {
$env:IMAGE_TAG = $oldImageTag $env:IMAGE_TAG = $oldImageTag
} }
@@ -88,9 +92,9 @@ if (-not $ExportOnly) {
$OutputPath = if ([System.IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $Root $Output } $OutputPath = if ([System.IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $Root $Output }
Write-Host "==> 瀵煎嚭闀滃儚 ($Service) -> $OutputPath" Write-Host "==> ???? ($Service) -> $OutputPath"
& docker save @SaveImages -o $OutputPath & docker save @SaveImages -o $OutputPath
if ($LASTEXITCODE -ne 0) { throw "docker save 澶辫触锛岄鍑虹爜 $LASTEXITCODE" } if ($LASTEXITCODE -ne 0) { throw "docker save ?????? $LASTEXITCODE" }
$manifestPath = if ($OutputPath.EndsWith(".tar")) { $manifestPath = if ($OutputPath.EndsWith(".tar")) {
$OutputPath.Substring(0, $OutputPath.Length - 4) + ".manifest.txt" $OutputPath.Substring(0, $OutputPath.Length - 4) + ".manifest.txt"
@@ -121,7 +125,7 @@ $manifestLines = @(
) )
foreach ($svc in $BuildServices) { foreach ($svc in $BuildServices) {
$imageId = (& docker image inspect "thebet365-${svc}:${Tag}" --format "{{.Id}}").Trim() $imageId = (& docker image inspect "thebet365-${svc}:${Tag}" --format "{{.Id}}").Trim()
if ($Service -eq "all") { if ($Service -eq "all" -or $Service -eq "api-admin") {
$manifestLines += "${svc}_image=thebet365-${svc}:${Tag}" $manifestLines += "${svc}_image=thebet365-${svc}:${Tag}"
$manifestLines += "${svc}_image_id=$imageId" $manifestLines += "${svc}_image_id=$imageId"
} else { } else {
@@ -132,13 +136,17 @@ foreach ($svc in $BuildServices) {
$manifestLines | Set-Content -Encoding UTF8 $manifestPath $manifestLines | Set-Content -Encoding UTF8 $manifestPath
$sizeMb = [math]::Round((Get-Item $OutputPath).Length / 1MB, 2) $sizeMb = [math]::Round((Get-Item $OutputPath).Length / 1MB, 2)
Write-Host "瀹屾垚: $OutputPath (${sizeMb} MB)" Write-Host "??: $OutputPath (${sizeMb} MB)"
Write-Host "Manifest: $manifestPath" Write-Host "Manifest: $manifestPath"
$recreate = if ($Service -eq "all") { "api player admin" } else { $Service } $recreate = switch ($Service) {
"all" { "api player admin" }
"api-admin" { "api admin" }
default { $Service }
}
Write-Host @" Write-Host @"
鏈嶅姟鍣ㄥ<EFBFBD>鍏ュ苟閲嶅缓: ????????:
docker load -i $([System.IO.Path]::GetFileName($OutputPath)) docker load -i $([System.IO.Path]::GetFileName($OutputPath))
docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate $recreate docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate $recreate
"@ "@

View File

@@ -25,7 +25,7 @@ default_tag() { echo "latest"; }
default_output() { default_output() {
local svc="$1" image_tag="$2" local svc="$1" image_tag="$2"
if [[ "$svc" == "all" ]]; then if [[ "$svc" == "all" || "$svc" == "api-admin" ]]; then
echo "thebet365-images-${image_tag}.tar" echo "thebet365-images-${image_tag}.tar"
else else
echo "thebet365-${svc}-${image_tag}.tar" echo "thebet365-${svc}-${image_tag}.tar"
@@ -39,8 +39,8 @@ validate_tag() {
validate_service() { validate_service() {
case "$1" in case "$1" in
all|api|player|admin) ;; all|api|player|admin|api-admin) ;;
*) echo "错误: --service 无效: $1(可选 api / player / admin / all" >&2; exit 1 ;; *) echo "错误: --service 无效: $1(可选 api / player / admin / api-admin / all" >&2; exit 1 ;;
esac esac
} }
@@ -49,7 +49,7 @@ usage() {
用法: docs/docker/build-and-export-images.sh [选项] 用法: docs/docker/build-and-export-images.sh [选项]
选项: 选项:
--service SVC api | player | admin | all默认 all --service SVC api | player | admin | api-admin | all默认 all
--tag TAG 镜像 tag默认 latest --tag TAG 镜像 tag默认 latest
--use-cache 构建时使用 Docker 缓存(默认 --no-cache --use-cache 构建时使用 Docker 缓存(默认 --no-cache
--export-only 跳过构建,仅导出已有指定 tag 镜像 --export-only 跳过构建,仅导出已有指定 tag 镜像
@@ -105,6 +105,8 @@ OUTPUT="${OUTPUT:-$(default_output "$SERVICE" "$TAG")}"
if [[ "$SERVICE" == "all" ]]; then if [[ "$SERVICE" == "all" ]]; then
BUILD_SERVICES=(api player admin) BUILD_SERVICES=(api player admin)
elif [[ "$SERVICE" == "api-admin" ]]; then
BUILD_SERVICES=(api admin)
else else
BUILD_SERVICES=("$SERVICE") BUILD_SERVICES=("$SERVICE")
fi fi
@@ -161,7 +163,7 @@ fi
echo "tar=$(basename "$OUTPUT_PATH")" echo "tar=$(basename "$OUTPUT_PATH")"
echo "tar_sha256=$CHECKSUM" echo "tar_sha256=$CHECKSUM"
for svc in "${BUILD_SERVICES[@]}"; do for svc in "${BUILD_SERVICES[@]}"; do
if [[ "$SERVICE" == "all" ]]; then if [[ "$SERVICE" == "all" || "$SERVICE" == "api-admin" ]]; then
echo "${svc}_image=thebet365-${svc}:$TAG" echo "${svc}_image=thebet365-${svc}:$TAG"
echo "${svc}_image_id=$(docker image inspect "thebet365-${svc}:$TAG" --format '{{.Id}}')" echo "${svc}_image_id=$(docker image inspect "thebet365-${svc}:$TAG" --format '{{.Id}}')"
else else
@@ -182,6 +184,8 @@ echo "Manifest: $MANIFEST_PATH"
RECREATE="$SERVICE" RECREATE="$SERVICE"
if [[ "$SERVICE" == "all" ]]; then if [[ "$SERVICE" == "all" ]]; then
RECREATE="api player admin" RECREATE="api player admin"
elif [[ "$SERVICE" == "api-admin" ]]; then
RECREATE="api admin"
fi fi
cat <<EOF cat <<EOF

View File

@@ -20,7 +20,8 @@
| `docs/docker/build-and-export-player-theme-3.bat` | Windows仅 theme-3 移动主题,自动切分支) | | `docs/docker/build-and-export-player-theme-3.bat` | Windows仅 theme-3 移动主题,自动切分支) |
| `docs/docker/build-and-export-player-theme-4.bat` | Windows仅 theme-4 海军蓝极简,自动切分支) | | `docs/docker/build-and-export-player-theme-4.bat` | Windows仅 theme-4 海军蓝极简,自动切分支) |
| `docs/docker/build-and-export-admin.bat` | Windows仅 admin可双击 | | `docs/docker/build-and-export-admin.bat` | Windows仅 admin可双击 |
| `docs/docker/build-and-export-all-themes.bat` | Windows**四套主题 player 一键打包**,自动切分支) | | `docs/docker/build-and-export-all-themes.ps1` | Windows**四套主题 player 一键打包**,自动切分支,推荐 |
| `docs/docker/build-and-export-all-themes.bat` | Windows同上薄包装启动器双击可用 |
| `docs/docker/build-and-export-images.ps1` | WindowsPowerShell | | `docs/docker/build-and-export-images.ps1` | WindowsPowerShell |
| `docs/docker/build-and-export-images.sh` | Linux / macOS / Git Bash | | `docs/docker/build-and-export-images.sh` | Linux / macOS / Git Bash |
@@ -131,18 +132,27 @@ docs\docker\build-and-export-images.bat --export-only
脚本会自动:切到各主题分支 → 构建 player 镜像 → 导出 tar → 切回原分支 → 构建 api/admin。 脚本会自动:切到各主题分支 → 构建 player 镜像 → 导出 tar → 切回原分支 → 构建 api/admin。
```bat > **为何用 PowerShell** 本脚本仅存在于 `main` 分支;执行过程中会 `git checkout theme-*`CMD 批处理会从磁盘逐行读取切分支后脚本文件消失会导致中断。PowerShell 启动时已将整份脚本载入内存,可安全跨分支执行。`.bat` 仅为薄包装,内部转调 `.ps1`。
```powershell
cd C:\path\to\thebet365 cd C:\path\to\thebet365
.\docs\docker\build-and-export-all-themes.ps1
```
或双击 / CMD
```bat
docs\docker\build-and-export-all-themes.bat docs\docker\build-and-export-all-themes.bat
``` ```
可选参数: 可选参数PowerShell 与 bat 均支持)
| 参数 | 说明 | | 参数 | 说明 |
|------|------| |------|------|
| `--use-cache` | 使用 Docker 层缓存,加快重复构建速度 | | `-UseCache` / `--use-cache` | 使用 Docker 层缓存,加快重复构建速度 |
| `--export-only` | 跳过构建,仅导出本地已有镜像 | | `-ExportOnly` / `--export-only` | 跳过构建,仅导出本地已有镜像 |
| `--skip-api-admin` | 只打四套 player跳过 api/admin | | `-SkipApiAdmin` / `--skip-api-admin` | 只打四套 player跳过 api/admin |
| `-SkipBundle` / `--skip-bundle` | 跳过六合一包,仅保留分散 tar |
### 产物 ### 产物
@@ -153,6 +163,13 @@ docs\docker\build-and-export-all-themes.bat
| `thebet365-player-theme-3.tar` | `thebet365-player:theme-3` | | `thebet365-player-theme-3.tar` | `thebet365-player:theme-3` |
| `thebet365-player-theme-4.tar` | `thebet365-player:theme-4` | | `thebet365-player-theme-4.tar` | `thebet365-player:theme-4` |
| `thebet365-images-latest.tar` | `thebet365-api:latest` + `thebet365-admin:latest` | | `thebet365-images-latest.tar` | `thebet365-api:latest` + `thebet365-admin:latest` |
| **`thebet365-full-themes-latest.tar`** | **上面全部 6 个镜像(上传这一个即可)** |
也可单独导出六合一包(本地镜像已齐时):
```bat
docs\docker\build-and-export-images.bat --service full-themes --export-only --tag latest
```
### 分支要求 ### 分支要求

View File

@@ -276,7 +276,18 @@ pnpm dev:player # http://localhost:5173 目视确认仍是本主题皮
## 六、构建与上传镜像 ## 六、构建与上传镜像
### 6.1 四个 player 镜像Windows CMD ### 6.1 四个 player 镜像
**一键打包(推荐,仅 `main` 分支提供脚本):**
```powershell
cd C:\path\to\thebet365
.\docs\docker\build-and-export-all-themes.ps1 -SkipApiAdmin
```
`docs\docker\build-and-export-all-themes.bat --skip-api-admin`(内部转调 PowerShell避免切分支后 bat 从磁盘消失)。
**手动逐步Windows CMD**
```bat ```bat
cd C:\path\to\thebet365 cd C:\path\to\thebet365