- 移除前端localStorage依赖,改用后端SQLite作为唯一数据源 - 新增getWanchuanConfig和saveWanchuanConfig函数用于配置读写 - 添加getBoundKnowledgeBase函数统一获取绑定知识库信息 - 支持桌面应用端口变化时正确读取配置 refactor(settings): 重构万川平台配置管理逻辑 - 移除localStorage配置存储,改为后端API调用 - 实现配置自动恢复和防抖保存机制 - 添加token过期自动重登功能 - 优化知识库选择和连接状态管理 fix(knowledge): 修复知识库上传异步问题 - 将getBoundKnowledgeBase调用改为await异步处理 - 统一各页面的知识库信息获取方式 - 修正上传接口datasetId使用逻辑 feat(electron): 添加chatlog.exe存在性检查 - 新增ensureChatlogExe函数验证执行文件存在 - 防止杀毒软件误删导致的ENONENT错误 - 提供用户友好的错误提示和解决方案
252 lines
9.7 KiB
PowerShell
252 lines
9.7 KiB
PowerShell
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,
|
||
[switch]$CleanBackend
|
||
)
|
||
|
||
$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) {
|
||
$inBackendInternal = $File.FullName -match '[\\/]backend[\\/]_internal[\\/]'
|
||
return $File.Name -eq ".env" -or
|
||
$File.Name -like "knowledge*.db" -or
|
||
($File.FullName -match "\\__pycache__\\" -and -not $inBackendInternal) -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")
|
||
}
|
||
|
||
$logoDest = Join-Path $Electron "build\company-logo.jpg"
|
||
$logoSrc = Join-Path $Frontend "public\company-logo.jpg"
|
||
if (Test-Path $logoSrc) {
|
||
Copy-Item -LiteralPath $logoSrc -Destination $logoDest -Force
|
||
Write-Host "company-logo.jpg synced to electron-launcher\build\"
|
||
}
|
||
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
|
||
# 默认复用 PyInstaller 的分析缓存(build\ 目录),代码未大改时增量打包可从 ~9 分钟降到 1-2 分钟。
|
||
# 仅在升级依赖或缓存异常时用 -CleanBackend 做一次全量重建。
|
||
$pyArgs = @("-m", "PyInstaller", "ChatLabBackend.spec", "--noconfirm")
|
||
if ($CleanBackend) {
|
||
$pyArgs += "--clean"
|
||
Write-Host "Backend: full clean rebuild (--clean)."
|
||
} else {
|
||
Write-Host "Backend: incremental build (reusing cache). Use -CleanBackend for a full rebuild."
|
||
}
|
||
Invoke-Python312 $pyArgs
|
||
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"
|