Initial upload for secondary development

This commit is contained in:
2026-06-08 19:00:03 +08:00
commit b913b8c78c
81 changed files with 27139 additions and 0 deletions

234
scripts/build-desktop.ps1 Normal file
View File

@@ -0,0 +1,234 @@
param(
[string]$PythonLauncher = "py",
[string]$PythonVersion = "-3.12",
[switch]$SkipIcon,
[switch]$SkipBackend,
[switch]$SkipFrontend,
[switch]$SkipInstaller,
[switch]$Sign,
[string]$CertificateFile,
[string]$CertificatePassword,
[string]$PublisherName,
[string]$TimestampServer = "http://timestamp.digicert.com",
[string[]]$VoiceSmokeKeys = @(),
[switch]$ForceSign
)
$ErrorActionPreference = "Stop"
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
$Frontend = Join-Path $Root "chatlab-web\frontend"
$Backend = Join-Path $Root "chatlog_fastAPI"
$Electron = Join-Path $Root "electron-launcher"
$Resources = Join-Path $Electron "build-resources"
$Release = Join-Path $Root "release"
function Invoke-Python312($Arguments) {
$pyCommand = Get-Command $PythonLauncher -ErrorAction SilentlyContinue
if ($pyCommand) {
try {
& $PythonLauncher $PythonVersion -V | Out-Null
if ($LASTEXITCODE -eq 0) {
& $PythonLauncher $PythonVersion @Arguments
return
}
} catch {
Write-Host "Python launcher $PythonLauncher $PythonVersion is not available, falling back to user Python312."
}
}
$fallback = Join-Path $env:LOCALAPPDATA "Programs\Python\Python312\python.exe"
if (-not (Test-Path $fallback)) {
throw "Python 3.12 was not found. Install it first or pass -PythonLauncher/-PythonVersion."
}
& $fallback @Arguments
}
function Reset-Dir($Path) {
if (Test-Path $Path) {
$resolved = Resolve-Path $Path
if (-not $resolved.Path.StartsWith($Root.Path)) {
throw "Refusing to remove path outside project: $($resolved.Path)"
}
Remove-Item -LiteralPath $resolved.Path -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $Path | Out-Null
}
function Copy-Dir($Source, $Dest) {
if (-not (Test-Path $Source)) {
throw "Missing required source: $Source"
}
New-Item -ItemType Directory -Force -Path (Split-Path $Dest) | Out-Null
Copy-Item -LiteralPath $Source -Destination $Dest -Recurse -Force
}
function Set-OptionalEnv($Name, $Value) {
if ([string]::IsNullOrWhiteSpace($Value)) {
Remove-Item -Path "Env:$Name" -ErrorAction SilentlyContinue
} else {
Set-Item -Path "Env:$Name" -Value $Value
}
}
function Test-ForbiddenReleaseFile($File) {
return $File.Name -eq ".env" -or
$File.Name -like "knowledge*.db" -or
$File.FullName -match "\\__pycache__\\" -or
$File.Name -like "*.pfx" -or
$File.Name -like "*.p12" -or
$File.Name -like "*.pvk" -or
$File.Name -like "*.cer" -or
$File.Name -like "*.crt" -or
$File.Name -like "*.key" -or
$File.FullName -match "\\certs\\"
}
Set-Location $Root
$resolvedCertificateFile = $null
if ($CertificateFile) {
if (-not (Test-Path -LiteralPath $CertificateFile)) {
throw "Certificate file was not found: $CertificateFile"
}
$resolvedCertificateFile = (Resolve-Path -LiteralPath $CertificateFile).Path
if ($resolvedCertificateFile.StartsWith($Root.Path)) {
throw "For safety, keep the code signing certificate outside the project folder: $resolvedCertificateFile"
}
}
$envCertificateFile = if ($resolvedCertificateFile) { $resolvedCertificateFile } else { $env:CHATLAB_PFX_FILE }
$envCertificateFile = if ($envCertificateFile) { $envCertificateFile.Trim() } else { "" }
$shouldSign = $Sign -or -not [string]::IsNullOrWhiteSpace($envCertificateFile)
if ($ForceSign -and -not $shouldSign) {
throw "-ForceSign requires -CertificateFile or CHATLAB_PFX_FILE."
}
if ($shouldSign) {
if (-not $envCertificateFile) {
throw "Signing was requested, but no certificate was provided. Use -CertificateFile or CHATLAB_PFX_FILE."
}
if (-not (Test-Path -LiteralPath $envCertificateFile)) {
throw "Certificate file was not found: $envCertificateFile"
}
$envCertificateFile = (Resolve-Path -LiteralPath $envCertificateFile).Path
if ($envCertificateFile.StartsWith($Root.Path)) {
throw "For safety, keep the code signing certificate outside the project folder: $envCertificateFile"
}
Set-OptionalEnv "CHATLAB_PFX_FILE" $envCertificateFile
if ($CertificatePassword) {
Set-OptionalEnv "CHATLAB_PFX_PASSWORD" $CertificatePassword
Set-OptionalEnv "WIN_CSC_KEY_PASSWORD" $CertificatePassword
Set-OptionalEnv "CSC_KEY_PASSWORD" $CertificatePassword
}
if ($PublisherName) { Set-OptionalEnv "CHATLAB_CERT_PUBLISHER_NAME" $PublisherName }
Set-OptionalEnv "CHATLAB_TIMESTAMP_SERVER" $TimestampServer
if ($ForceSign) { Set-OptionalEnv "CHATLAB_FORCE_SIGN" "1" }
Write-Host "Code signing enabled. Certificate: $envCertificateFile"
} else {
Remove-Item -Path "Env:CHATLAB_PFX_FILE" -ErrorAction SilentlyContinue
Remove-Item -Path "Env:CHATLAB_PFX_PASSWORD" -ErrorAction SilentlyContinue
Remove-Item -Path "Env:CHATLAB_CERT_PUBLISHER_NAME" -ErrorAction SilentlyContinue
Remove-Item -Path "Env:CHATLAB_FORCE_SIGN" -ErrorAction SilentlyContinue
Remove-Item -Path "Env:WIN_CSC_KEY_PASSWORD" -ErrorAction SilentlyContinue
Remove-Item -Path "Env:CSC_KEY_PASSWORD" -ErrorAction SilentlyContinue
Write-Host "Code signing disabled. Unsigned installer build is allowed."
}
if (-not $SkipIcon) {
& node (Join-Path $Root "scripts\make-icon.cjs")
}
if (-not $SkipFrontend) {
$dist = Join-Path $Frontend "dist"
if (Test-Path $dist) {
Remove-Item -LiteralPath (Resolve-Path $dist).Path -Recurse -Force
}
Push-Location $Frontend
& npm.cmd run build
Pop-Location
}
if (-not $SkipBackend) {
Push-Location $Backend
Invoke-Python312 @("-m", "PyInstaller", "ChatLabBackend.spec", "--noconfirm", "--clean")
Pop-Location
}
Reset-Dir $Resources
New-Item -ItemType Directory -Force -Path $Release | Out-Null
Copy-Item -LiteralPath (Join-Path $Root "chatlog.exe") -Destination (Join-Path $Resources "chatlog.exe") -Force
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$chatlogVersionOutput = & (Join-Path $Resources "chatlog.exe") version 2>&1
$chatlogVersionExitCode = $LASTEXITCODE
$ErrorActionPreference = $previousErrorActionPreference
if ($chatlogVersionExitCode -ne 0) {
throw "chatlog.exe version check failed:`n$chatlogVersionOutput"
}
Write-Host "chatlog.exe version: $chatlogVersionOutput"
foreach ($voiceKey in $VoiceSmokeKeys) {
if ([string]::IsNullOrWhiteSpace($voiceKey)) { continue }
try {
$response = Invoke-WebRequest -Uri "http://127.0.0.1:5030/voice/$voiceKey" -UseBasicParsing -TimeoutSec 15
if ($response.StatusCode -ge 400 -or $response.RawContentLength -le 0) {
throw "HTTP $($response.StatusCode), length=$($response.RawContentLength)"
}
Write-Host "voice smoke passed: $voiceKey ($($response.RawContentLength) bytes)"
} catch {
throw "voice smoke failed for $voiceKey. Do not ship this installer until chatlog can read WeChat voice media. $($_.Exception.Message)"
}
}
Copy-Dir (Join-Path $Root "lib") (Join-Path $Resources "lib")
Copy-Dir (Join-Path $Frontend "dist") (Join-Path $Resources "frontend")
Copy-Dir (Join-Path $Backend "dist\ChatLabBackend") (Join-Path $Resources "backend")
Copy-Item -LiteralPath (Join-Path $Root "DISCLAIMER.md") -Destination (Join-Path $Resources "DISCLAIMER.md") -Force
Copy-Item -LiteralPath (Join-Path $Root "LICENSE") -Destination (Join-Path $Resources "LICENSE") -Force
$forbidden = Get-ChildItem -LiteralPath $Resources -Recurse -Force |
Where-Object { Test-ForbiddenReleaseFile $_ }
if ($forbidden) {
$names = ($forbidden | Select-Object -ExpandProperty FullName) -join "`n"
throw "Sensitive or cache files found in release resources:`n$names"
}
Get-ChildItem -LiteralPath $Resources -Recurse -File |
Select-Object FullName, Length |
Out-File -Encoding UTF8 (Join-Path $Release "manifest.txt")
if (-not $SkipInstaller) {
Push-Location $Electron
& npm.cmd run build
Pop-Location
Copy-Item -Path (Join-Path $Electron "dist\*.exe") -Destination $Release -Force
Copy-Item -Path (Join-Path $Electron "dist\*.blockmap") -Destination $Release -Force -ErrorAction SilentlyContinue
$releaseForbidden = Get-ChildItem -LiteralPath $Release -Recurse -Force |
Where-Object { Test-ForbiddenReleaseFile $_ }
if ($releaseForbidden) {
$names = ($releaseForbidden | Select-Object -ExpandProperty FullName) -join "`n"
throw "Sensitive certificate, private data, or cache files found in release output:`n$names"
}
$installer = Get-ChildItem -LiteralPath $Release -Filter "ChatLab-Setup-*.exe" -File |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($installer) {
$signature = Get-AuthenticodeSignature -FilePath $installer.FullName
if ($shouldSign -or $ForceSign) {
if ($signature.Status -ne "Valid") {
throw "Installer signing verification failed: $($signature.Status) $($signature.StatusMessage)"
}
Write-Host "Installer signature verified: $($signature.SignerCertificate.Subject)"
} else {
Write-Host "Installer is unsigned by design for this build."
}
}
}
Write-Host "Desktop build completed. Output: $Release"