11@ echo off
2- setlocal
3- cd /d " %~dp0 "
4- set " ELAINABOT_WINDOWS_LAUNCHER = %~f0 "
5- set " ELAINABOT_ROOT = %~dp0 "
6- set " ELAINABOT_FIRST_ARGUMENT = %~1 "
7- set " ELAINABOT_SECOND_ARGUMENT = %~2 "
8- chcp 65001 > nul
9- set " PYTHONUTF8 = 1"
10- set " PYTHONIOENCODING = utf-8"
11- powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -OutputFormat Text -EncodedCommand JABsAD0ARwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGUAbgB2ADoARQBMAEEASQBOAEEAQgBPAFQAXwBXAEkATgBEAE8AVwBTAF8ATABBAFUATgBDAEgARQBSACAALQBFAG4AYwBvAGQAaQBuAGcAIABVAFQARgA4ADsAJABtAD0AWwBhAHIAcgBhAHkAXQA6ADoASQBuAGQAZQB4AE8AZgAoACQAbAAsACcAIwAgAIVRTF0gAFAAbwB3AGUAcgBTAGgAZQBsAGwAIADjTgF4JwApADsAaQBmACgAJABtAC0AbAB0ADAAKQB7AHQAaAByAG8AdwAgACcAKmd+YjBShVFMXYR2IABQAG8AdwBlAHIAUwBoAGUAbABsACAA404BeAIwJwB9ADsAJABwAD0AJABsAFsAKAAkAG0AKwAxACkALgAuACgAJABsAC4AQwBvAHUAbgB0AC0AMQApAF0ALQBqAG8AaQBuAFsARQBuAHYAaQByAG8AbgBtAGUAbgB0AF0AOgA6AE4AZQB3AEwAaQBuAGUAOwAmACgAWwBzAGMAcgBpAHAAdABiAGwAbwBjAGsAXQA6ADoAQwByAGUAYQB0AGUAKAAkAHAAKQApAA==
12- set " ELAINABOT_EXIT_CODE = %ERRORLEVEL% "
13- exit /b %ELAINABOT_EXIT_CODE%
2+ @ setlocal
3+ @ cd /d " %~dp0 "
4+ @ set " ELAINABOT_WINDOWS_LAUNCHER = %~f0 "
5+ @ set " ELAINABOT_ROOT = %~dp0 "
6+ @ set " ELAINABOT_FIRST_ARGUMENT = %~1 "
7+ @ set " ELAINABOT_SECOND_ARGUMENT = %~2 "
8+ @ set " PYTHONUTF8 = 1"
9+ @ set " PYTHONIOENCODING = utf-8"
10+ @ powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -OutputFormat Text -EncodedCommand JABsAD0ARwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGUAbgB2ADoARQBMAEEASQBOAEEAQgBPAFQAXwBXAEkATgBEAE8AVwBTAF8ATABBAFUATgBDAEgARQBSACAALQBFAG4AYwBvAGQAaQBuAGcAIABVAFQARgA4ADsAJABtAD0AWwBhAHIAcgBhAHkAXQA6ADoASQBuAGQAZQB4AE8AZgAoACQAbAAsACcAIwAgAIVRTF0gAFAAbwB3AGUAcgBTAGgAZQBsAGwAIADjTgF4JwApADsAaQBmACgAJABtAC0AbAB0ADAAKQB7AHQAaAByAG8AdwAgACcAKmd+YjBShVFMXYR2IABQAG8AdwBlAHIAUwBoAGUAbABsACAA404BeAIwJwB9ADsAJABwAD0AJABsAFsAKAAkAG0AKwAxACkALgAuACgAJABsAC4AQwBvAHUAbgB0AC0AMQApAF0ALQBqAG8AaQBuAFsARQBuAHYAaQByAG8AbgBtAGUAbgB0AF0AOgA6AE4AZQB3AEwAaQBuAGUAOwAmACgAWwBzAGMAcgBpAHAAdABiAGwAbwBjAGsAXQA6ADoAQwByAGUAYQB0AGUAKAAkAHAAKQApAA==
11+ @ set " ELAINABOT_EXIT_CODE = %ERRORLEVEL% "
12+ @ if not " %ELAINABOT_EXIT_CODE% " == " 0" (
13+ @ echo(
14+ @ echo [ElainaBot] Startup failed with exit code %ELAINABOT_EXIT_CODE% .
15+ @ echo [ElainaBot] Review the error above, then press any key to close this window...
16+ @ pause > nul
17+ )
18+ @ exit /b %ELAINABOT_EXIT_CODE%
1419
1520# 内嵌 PowerShell 代码
1621$Utf8Encoding = New-Object Text.UTF8Encoding($false)
17- [Console]::InputEncoding = $Utf8Encoding
18- [Console]::OutputEncoding = $Utf8Encoding
19- $OutputEncoding = $Utf8Encoding
22+ $WindowsVersion = [Environment]::OSVersion.Version
23+ $UseLegacyWindowsPath = $WindowsVersion.Major -lt 10
24+ $UseSystemBrowserPanel = $UseLegacyWindowsPath
25+ $ProgressPreference = 'SilentlyContinue'
2026$env:PYTHONUTF8 = '1'
21- $env:PYTHONIOENCODING = 'utf-8'
27+ $env:PYTHONIOENCODING = [Console]::OutputEncoding.WebName
28+ [Net.ServicePointManager]::SecurityProtocol =
29+ [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
2230
2331function Write-ConsoleLine {
2432 param(
2533 [Parameter(Mandatory = $true)][string]$Message,
2634 [ConsoleColor]$Color = [Console]::ForegroundColor
2735 )
2836
29- if ([Console]::IsOutputRedirected) {
30- [Console]::WriteLine($Message)
31- return
32- }
33-
34- $previousColor = [Console]::ForegroundColor
3537 try {
36- [Console]::ForegroundColor = $Color
37- [Console]::WriteLine($Message)
38- } finally {
39- [Console]::ForegroundColor = $previousColor
38+ [Console]::Out.WriteLine($Message)
39+ } catch {
40+ # Some legacy consoles cannot accept Unicode writes; keep the failure readable.
41+ $asciiMessage = [Text.RegularExpressions.Regex]::Replace($Message, '[^ \x00-\x7F]', '?')
42+ try { [Console]::Out.WriteLine($asciiMessage) } catch { }
4043 }
4144}
4245
@@ -71,7 +74,8 @@ Set-StrictMode -Version Latest
7174$ErrorActionPreference = 'Stop'
7275
7376$BootstrapVersion = '5'
74- $PythonVersion = '3.13'
77+ $MinimumPythonVersion = '3.11'
78+ $ManagedPythonVersion = '3.13'
7579$DefaultPythonInstallMirror = 'https://registry.npmmirror.com/-/binary/python-build-standalone'
7680$PythonInstallMirror = if (-not [string]::IsNullOrWhiteSpace($env:ELAINABOT_PYTHON_MIRROR)) {
7781 $env:ELAINABOT_PYTHON_MIRROR.Trim().TrimEnd('/')
@@ -99,7 +103,7 @@ function Invoke-Checked {
99103 [Parameter(Mandatory = $true)][string[]]$Arguments
100104 )
101105
102- & $FilePath @ Arguments | Out-Host
106+ & $FilePath @ Arguments
103107 if ($LASTEXITCODE -ne 0) {
104108 throw " 命令执行失败,退出代码 ${LASTEXITCODE}:$FilePath $($Arguments -join ' ')"
105109 }
@@ -109,7 +113,7 @@ function Invoke-PipInstall {
109113 param([Parameter(Mandatory = $true)][string[]]$Arguments)
110114
111115 Write-Step '正在优先使用清华 PyPI 镜像安装依赖...'
112- & $VenvPython -m pip install --disable-pip-version-check --index-url $PipMirror @ Arguments | Out-Host
116+ & $VenvPython -m pip install --disable-pip-version-check --index-url $PipMirror @ Arguments
113117 if ($LASTEXITCODE -eq 0) {
114118 return
115119 }
@@ -133,20 +137,16 @@ function Get-CommandPath {
133137function Test-PythonCandidate {
134138 param(
135139 [string]$FilePath,
136- [string[]]$BaseArguments = @ (),
137- [string]$RequiredVersion = ''
140+ [string[]]$BaseArguments = @ ()
138141 )
139142
140143 if (-not $FilePath) {
141144 return $null
142145 }
143146 $previousPreference = $ErrorActionPreference
144147 $ErrorActionPreference = 'Continue'
145- $versionCheck = " import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)"
146- if ($RequiredVersion) {
147- $parts = $RequiredVersion.Split('.')
148- $versionCheck = " import sys; raise SystemExit(0 if sys.version_info[:2] == ($($parts[0]), $($parts[1])) else 1)"
149- }
148+ $minimumParts = $MinimumPythonVersion.Split('.')
149+ $versionCheck = " import sys; raise SystemExit(0 if sys.version_info >= ($($minimumParts[0]), $($minimumParts[1])) else 1)"
150150 try {
151151 & $FilePath @ BaseArguments -c $versionCheck *> $null
152152 $probeExitCode = $LASTEXITCODE
@@ -169,15 +169,15 @@ function Test-PythonCandidate {
169169function Find-PreferredPython {
170170 $pyLauncher = Get-CommandPath 'py'
171171 if ($pyLauncher) {
172- $candidate = Test-PythonCandidate -FilePath $pyLauncher -BaseArguments @ ('-3.13') -RequiredVersion $PythonVersion
172+ $candidate = Test-PythonCandidate -FilePath $pyLauncher -BaseArguments @ ('-3')
173173 if ($candidate) {
174174 return $candidate
175175 }
176176 }
177177
178- foreach ($name in @ ('python3. 13', 'python3', 'python ')) {
178+ foreach ($name in @ ('python', ' python3', 'python3.14', 'python3. 13', 'python3.12 ', 'python3.11 ')) {
179179 $path = Get-CommandPath $name
180- $candidate = Test-PythonCandidate -FilePath $path -RequiredVersion $PythonVersion
180+ $candidate = Test-PythonCandidate -FilePath $path
181181 if ($candidate) {
182182 return $candidate
183183 }
@@ -220,12 +220,26 @@ function Ensure-Uv {
220220 New-Item -ItemType Directory -Path $ToolsDir -Force | Out-Null
221221 $installerPath = Join-Path ([IO.Path]::GetTempPath()) 'elainabot-uv-install.ps1'
222222 try {
223- Invoke-WebRequest -Uri 'https://astral.sh/uv/install.ps1' -OutFile $installerPath
223+ Invoke-WebRequest -UseBasicParsing - Uri 'https://astral.sh/uv/install.ps1' -OutFile $installerPath
224224 $env:UV_INSTALL_DIR = $ToolsDir
225225 $env:UV_NO_MODIFY_PATH = '1'
226- Invoke-Checked 'powershell.exe' @ (
227- '-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installerPath
228- )
226+ # The uv installer redraws progress in place. Capturing its output avoids
227+ # duplicated Chinese status text in both Windows 11 and legacy cmd.exe.
228+ $previousPreference = $ErrorActionPreference
229+ try {
230+ $ErrorActionPreference = 'Continue'
231+ $installerOutput = @ (& powershell.exe @ (
232+ '-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $installerPath
233+ ) 2 >& 1)
234+ $installerExitCode = $LASTEXITCODE
235+ } finally {
236+ $ErrorActionPreference = $previousPreference
237+ }
238+ if ($installerExitCode -ne 0) {
239+ $details = ($installerOutput | Select-Object -Last 5 | ForEach-Object { $_.ToString() }) -join ' '
240+ if ([string]::IsNullOrWhiteSpace($details)) { $details = '未返回详细错误信息' }
241+ throw " 项目专用的 Python 环境引导工具安装失败,退出代码 ${installerExitCode}:${details}"
242+ }
229243 } finally {
230244 Remove-Item -LiteralPath $installerPath -Force -ErrorAction SilentlyContinue
231245 }
@@ -249,17 +263,32 @@ function Install-ManagedPython {
249263 $null
250264 }
251265 try {
252- Remove-Item Env:UV_NO_PROGRESS -ErrorAction SilentlyContinue
253- Write-Step " [1/6] 正在通过镜像下载项目专用的 Python $PythonVersion(将显示实时进度)..."
254- & $UvPath --color always python install --no-bin --no-registry --mirror $PythonInstallMirror $PythonVersion | Out-Host
255- if ($LASTEXITCODE -eq 0) {
266+ $env:UV_NO_PROGRESS = '1'
267+ Write-Step " [1/6] 正在通过镜像下载项目专用的 Python $ManagedPythonVersion..."
268+ $previousPreference = $ErrorActionPreference
269+ try {
270+ $ErrorActionPreference = 'Continue'
271+ $mirrorOutput = @ (& $UvPath --color never python install --no-bin --no-registry --mirror $PythonInstallMirror $ManagedPythonVersion 2 >& 1)
272+ $mirrorExitCode = $LASTEXITCODE
273+ } finally {
274+ $ErrorActionPreference = $previousPreference
275+ }
276+ if ($mirrorExitCode -eq 0) {
256277 return
257278 }
258279
259280 Write-Step 'Python 镜像下载失败,正在切换到官方源...'
260- & $UvPath --color always python install --no-bin --no-registry $PythonVersion | Out-Host
261- if ($LASTEXITCODE -ne 0) {
262- throw " Python $PythonVersion 下载失败,镜像源和官方源均不可用。"
281+ try {
282+ $ErrorActionPreference = 'Continue'
283+ $officialOutput = @ (& $UvPath --color never python install --no-bin --no-registry $ManagedPythonVersion 2 >& 1)
284+ $officialExitCode = $LASTEXITCODE
285+ } finally {
286+ $ErrorActionPreference = $previousPreference
287+ }
288+ if ($officialExitCode -ne 0) {
289+ $details = (($mirrorOutput + $officialOutput) | Select-Object -Last 5 | ForEach-Object { $_.ToString() }) -join ' '
290+ if ([string]::IsNullOrWhiteSpace($details)) { $details = '未返回详细错误信息' }
291+ throw " Python $ManagedPythonVersion 下载失败,镜像源和官方源均不可用:${details}"
263292 }
264293 } finally {
265294 if ($hadNoProgress) {
@@ -280,7 +309,7 @@ function Backup-InvalidVenv {
280309}
281310
282311function Ensure-VirtualEnvironment {
283- Write-Step ' [1/6] 正在检查 Python 3.11 或更高版本...'
312+ Write-Step " [1/6] 正在检查 Python $MinimumPythonVersion 或更高版本..."
284313 if (Test-Path -LiteralPath $VenvPython) {
285314 $existing = Test-PythonCandidate $VenvPython
286315 if ($existing) {
@@ -293,7 +322,7 @@ function Ensure-VirtualEnvironment {
293322 Backup-InvalidVenv
294323 $python = Find-PreferredPython
295324 if ($python) {
296- Write-Step " [1/6] 已找到 Python 3.13 :$($python.Version)"
325+ Write-Step " [1/6] 已找到兼容的 Python:$($python.Version)"
297326 Write-Step '[2/6] 正在创建项目虚拟环境:.venv...'
298327 & $python.FilePath @ ($python.Arguments) -m venv $VenvDir
299328 if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $VenvPython)) {
@@ -311,7 +340,7 @@ function Ensure-VirtualEnvironment {
311340 Install-ManagedPython -UvPath $uvPath
312341 Write-Step '[2/6] 正在创建项目虚拟环境:.venv...'
313342 Invoke-Checked $uvPath @ (
314- 'venv', '--python', $PythonVersion , '--managed-python', '--no-python-downloads', $VenvDir
343+ 'venv', '--python', $ManagedPythonVersion , '--managed-python', '--no-python-downloads', $VenvDir
315344 )
316345 if (-not (Test-Path -LiteralPath $VenvPython)) {
317346 throw '虚拟环境创建结束,但未找到可用的 python.exe。'
@@ -425,6 +454,11 @@ function Test-WebPanelDependency {
425454}
426455
427456function Ensure-WebPanelDependency {
457+ if ($UseSystemBrowserPanel) {
458+ Write-Step '[5/6] 当前 Windows 不支持内嵌桌面窗口,将使用外部浏览器打开管理面板。'
459+ return
460+ }
461+
428462 Write-Step '[5/6] 正在检查 Windows 桌面窗口组件...'
429463 if (Test-WebPanelDependency) {
430464 Write-Step '[5/6] Windows 桌面窗口组件已经安装,无需重复安装。'
@@ -528,7 +562,58 @@ function Test-WebPanelAvailable {
528562function Start-WebPanelWindow {
529563 param([Parameter(Mandatory = $true)][string]$Url)
530564
531- $windowSource = @ '
565+ $windowSource = if ($UseSystemBrowserPanel) {
566+ @ '
567+ import os
568+ import shutil
569+ import subprocess
570+ import sys
571+ import time
572+ import urllib.request
573+
574+ panel_url = sys.argv[1]
575+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
576+ for _ in range(120):
577+ try:
578+ with opener.open(panel_url, timeout=2):
579+ pass
580+ break
581+ except Exception:
582+ time.sleep(1)
583+ else:
584+ raise SystemExit(1)
585+
586+ browser_candidates = (
587+ 'msedge.exe',
588+ 'chrome.exe',
589+ 'firefox.exe',
590+ 'iexplore.exe',
591+ )
592+ browser_paths = []
593+ for name in browser_candidates:
594+ resolved = shutil.which(name)
595+ if resolved:
596+ browser_paths.append(resolved)
597+
598+ for base_name in ('PROGRAMFILES', 'PROGRAMFILES(X86)', 'LOCALAPPDATA'):
599+ base_path = os.environ.get(base_name)
600+ if not base_path:
601+ continue
602+ browser_paths.extend((
603+ os.path.join(base_path, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
604+ os.path.join(base_path, 'Google', 'Chrome', 'Application', 'chrome.exe'),
605+ os.path.join(base_path, 'Mozilla Firefox', 'firefox.exe'),
606+ ))
607+
608+ for browser_path in browser_paths:
609+ if os.path.isfile(browser_path):
610+ subprocess.Popen([browser_path, panel_url])
611+ raise SystemExit(0)
612+
613+ raise SystemExit(2)
614+ '@
615+ } else {
616+ @ '
532617import sys
533618import time
534619import urllib.request
@@ -556,6 +641,7 @@ webview.create_window(
556641)
557642webview.start()
558643'@
644+ }
559645
560646 $encodedSource = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($windowSource))
561647 $launcherSource = " import base64;exec(base64.b64decode('$encodedSource'))"
@@ -587,18 +673,20 @@ try {
587673 if (-not (Test-WebPanelAvailable -Url $panelUrl)) {
588674 throw " 配置端口 $panelPort 已被占用,但未检测到 ElainaBot 管理面板。请检查端口占用情况。"
589675 }
590- Write-Step " [6/6] 检测到框架已经运行,仅重新打开桌面面板窗口 。"
676+ Write-Step " [6/6] 检测到框架已经运行,仅重新打开管理面板 。"
591677 $existingPanelWindow = Start-WebPanelWindow -Url $panelUrl
592678 if ($existingPanelWindow) {
593679 $existingPanelWindow.Dispose()
594680 }
595- Write-Step '桌面面板窗口已打开 ,无需重新启动框架。'
681+ Write-Step '管理面板已打开 ,无需重新启动框架。'
596682 exit 0
597683 }
598684
599685 Write-Step " [6/6] 配置端口 $panelPort 尚未开启,正在启动 ElainaBot 框架..."
600- Write-Step '面板就绪后将自动打开 ElainaBot 桌面窗口。'
601- $panelWindow = Start-WebPanelWindow -Url $panelUrl
686+ Write-Step '面板就绪后将自动打开 ElainaBot 管理面板。'
687+ # Keep the framework in the foreground on every supported Windows version
688+ # so this console remains available for runtime logs and diagnostics.
689+ $panelWindow = Start-WebPanelWindow -Url $panelUrl
602690 try {
603691 & $VenvPython (Join-Path $RootDir 'main.py')
604692 $frameworkExitCode = $LASTEXITCODE
0 commit comments