feat: add Chinese UI, Responses models, and mirrored Windows setup
Localize the web console and persist custom model and capability settings. Use OpenAI Responses exclusively and move runtime limits into environment configuration. Bootstrap uv and the virtual environment on Windows, support optional China mirrors, and package source distributions without environments, build output, or credentials. Update regression tests and repository quality checks for the new behavior.
This commit is contained in:
1 parent
371aa6df56
commit
2ddec53954
183 files changed
+3427
-11857
No files matched your search
@@ -0,0 +1,85 @@
|
||||
# Python bootstrap shared by the Windows launcher and its isolated checks.
|
||||
# Keep this file ASCII for Windows PowerShell 5.1.
|
||||
|
||||
function Initialize-ArtemisPython {
|
||||
param([Parameter(Mandatory=$true)][string]$ProjectRoot)
|
||||
|
||||
$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot -ErrorAction Stop).Path
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot 'pyproject.toml'))) {
|
||||
throw 'pyproject.toml is missing. Extract the complete source archive first.'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot 'uv.lock'))) {
|
||||
throw 'uv.lock is missing. Extract the complete source archive first.'
|
||||
}
|
||||
|
||||
if (-not (Test-CommandExists 'uv')) {
|
||||
if ($ArtemisDownloads -and $ArtemisDownloads.python_index) {
|
||||
Write-Host ' [INSTALL] Installing uv from the selected Python mirror...' -ForegroundColor Cyan
|
||||
Install-ArtemisUvFromIndex -IndexUrl $ArtemisDownloads.python_index
|
||||
} else {
|
||||
Write-Host ' [INSTALL] uv was not found. Installing from the configured installer...' -ForegroundColor Cyan
|
||||
$installer = Join-Path ([IO.Path]::GetTempPath()) ('artemis-uv-' + [guid]::NewGuid().ToString('N') + '.ps1')
|
||||
try {
|
||||
$installerUrl = if ($ArtemisDownloads) { $ArtemisDownloads.uv_installer_url } else { 'https://astral.sh/uv/install.ps1' }
|
||||
if (-not (Invoke-DownloadFile -Uri $installerUrl -OutFile $installer)) {
|
||||
throw 'Could not download uv. Check your network/proxy and retry.'
|
||||
}
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw "uv installation failed (exit $LASTEXITCODE)." }
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $installer -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
Update-EnvironmentPath
|
||||
} else {
|
||||
Write-Host ' [SKIP] uv is already installed.' -ForegroundColor Green
|
||||
}
|
||||
if (-not (Test-CommandExists 'uv')) { throw 'uv is still unavailable. Install it from https://astral.sh/uv/ and retry.' }
|
||||
& uv --version | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw 'The installed uv could not run. Repair its installation and retry.' }
|
||||
|
||||
$venvDir = Join-Path $ProjectRoot '.venv'
|
||||
$pythonExe = Join-Path $venvDir 'Scripts\python.exe'
|
||||
$previousEnvironment = $env:UV_PROJECT_ENVIRONMENT
|
||||
$env:UV_PROJECT_ENVIRONMENT = $venvDir
|
||||
Push-Location -LiteralPath $ProjectRoot
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $venvDir)) {
|
||||
Write-Host ' [INSTALL] Creating .venv with Python 3.12 (downloaded automatically if needed)...' -ForegroundColor Cyan
|
||||
& uv venv --python 3.12 $venvDir | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not create .venv. Check the download errors above and retry.' }
|
||||
} else {
|
||||
Write-Host ' [SKIP] Reusing the existing project .venv.' -ForegroundColor Green
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $pythonExe)) {
|
||||
throw 'The existing .venv is incomplete. Rename it as a backup, then rerun start.bat.'
|
||||
}
|
||||
& $pythonExe -c 'import sys; sys.exit(0 if sys.version_info >= (3, 12) and sys.prefix != sys.base_prefix else 1)' | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'The existing .venv is unusable or older than Python 3.12. Rename it as a backup, then rerun start.bat.'
|
||||
}
|
||||
|
||||
# A cheap check skips installation when the lock and installed packages agree.
|
||||
# --inexact preserves any optional development tools already installed.
|
||||
$lockOption = if ($ArtemisDownloads -and $ArtemisDownloads.python_index) { '--frozen' } else { '--locked' }
|
||||
& uv sync --check $lockOption --no-dev --inexact --quiet 2>$null | Out-Host
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host ' [SKIP] Python dependencies are already ready.' -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host ' [INSTALL] Synchronizing dependencies from uv.lock...' -ForegroundColor Cyan
|
||||
if ($ArtemisDownloads -and $ArtemisDownloads.python_index) {
|
||||
Install-ArtemisLockedDependencies -PythonExe $pythonExe -IndexUrl $ArtemisDownloads.python_index
|
||||
} else {
|
||||
& uv sync --locked --no-dev --inexact | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed. Fix the error above and rerun start.bat.' }
|
||||
}
|
||||
}
|
||||
& $pythonExe -c 'import artemis, uvicorn, fastapi, langchain_openai' | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Python dependency verification failed. The UI will not be started.' }
|
||||
Write-Host ' [OK] Python environment is ready.' -ForegroundColor Green
|
||||
return $pythonExe
|
||||
} finally {
|
||||
Pop-Location
|
||||
$env:UV_PROJECT_ENVIRONMENT = $previousEnvironment
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
# Download settings are process-local; never edit global pip/npm/uv settings.
|
||||
# Keep this file ASCII for Windows PowerShell 5.1.
|
||||
|
||||
function Initialize-ArtemisDownloads {
|
||||
param([string]$ProjectRoot, [string]$Mode)
|
||||
$path = Join-Path $ProjectRoot 'config\downloads.json'
|
||||
$config = Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
||||
if (-not $Mode) { $Mode = $config.mode }
|
||||
if ($Mode -notin @('Official', 'China')) { throw 'Download mode must be Official or China.' }
|
||||
$timeout = 0
|
||||
if (-not [int]::TryParse([string]$config.timeout_seconds, [ref]$timeout) -or $timeout -lt 10 -or $timeout -gt 3600) {
|
||||
throw 'config/downloads.json: timeout_seconds must be between 10 and 3600.'
|
||||
}
|
||||
$values = @{
|
||||
python_index = ''; python_download_mirror = ''; npm_registry = ''
|
||||
node_mirror = 'https://nodejs.org/dist'
|
||||
adb_url = 'https://dl.google.com/android/repository/platform-tools-latest-windows.zip'
|
||||
scrcpy_url = 'https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-win64-v4.1.zip'
|
||||
uv_installer_url = 'https://astral.sh/uv/install.ps1'
|
||||
}
|
||||
foreach ($name in @($values.Keys)) {
|
||||
if ($Mode -eq 'China' -and $config.china.$name) { $values[$name] = [string]$config.china.$name }
|
||||
if ($config.overrides.$name) { $values[$name] = [string]$config.overrides.$name }
|
||||
if ($values[$name]) {
|
||||
$uri = $null
|
||||
if (-not [Uri]::TryCreate($values[$name], [UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -ne 'https') {
|
||||
throw "config/downloads.json: $name must be an HTTPS URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
# Explicit caller environment settings take priority over the selected preset.
|
||||
if ($env:UV_DEFAULT_INDEX) { $values.python_index = $env:UV_DEFAULT_INDEX }
|
||||
elseif ($env:UV_INDEX_URL) { $values.python_index = $env:UV_INDEX_URL }
|
||||
if ($env:UV_PYTHON_INSTALL_MIRROR) { $values.python_download_mirror = $env:UV_PYTHON_INSTALL_MIRROR }
|
||||
if ($env:npm_config_registry) { $values.npm_registry = $env:npm_config_registry }
|
||||
if ($values.python_download_mirror) { $env:UV_PYTHON_INSTALL_MIRROR = $values.python_download_mirror }
|
||||
if ($values.npm_registry) { $env:npm_config_registry = $values.npm_registry }
|
||||
$env:UV_HTTP_TIMEOUT = [string]$timeout
|
||||
$env:npm_config_fetch_timeout = [string]($timeout * 1000)
|
||||
$values.mode = $Mode
|
||||
$values.timeout_seconds = $timeout
|
||||
$script:ArtemisDownloads = [pscustomobject]$values
|
||||
Write-Host " [INFO] Download mode: $Mode (config/downloads.json)." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Install-ArtemisUvFromIndex {
|
||||
param([string]$IndexUrl, [string]$InstallDir = (Join-Path $env:USERPROFILE '.local\bin'))
|
||||
# A uv wheel contains the standalone executable; no preinstalled Python is needed.
|
||||
$work = Join-Path ([IO.Path]::GetTempPath()) ('artemis-uv-' + [guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $work -ErrorAction Stop | Out-Null
|
||||
try {
|
||||
$index = $IndexUrl.TrimEnd('/') + '/uv/'
|
||||
$page = Join-Path $work 'index.html'
|
||||
if (-not (Invoke-DownloadFile -Uri $index -OutFile $page)) { throw 'Could not download the uv package index.' }
|
||||
$platform = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { 'win_arm64' } else { 'win_amd64' }
|
||||
$pattern = 'href=["''](?<link>[^"'']*uv-(?<version>\d+\.\d+\.\d+)-py3-none-' + $platform + '\.whl#sha256=(?<hash>[a-fA-F0-9]{64}))["'']'
|
||||
$candidates = [regex]::Matches((Get-Content -LiteralPath $page -Raw), $pattern)
|
||||
$candidate = $candidates | Sort-Object { [version]$_.Groups['version'].Value } -Descending | Select-Object -First 1
|
||||
if (-not $candidate) { throw "No stable uv wheel for $platform was found on the selected index." }
|
||||
$url = [Uri]::new([Uri]$index, [System.Net.WebUtility]::HtmlDecode($candidate.Groups['link'].Value))
|
||||
if ($url.Scheme -ne 'https') { throw 'The uv download must use HTTPS.' }
|
||||
$zip = Join-Path $work 'uv.zip'
|
||||
if (-not (Invoke-DownloadFile -Uri $url.AbsoluteUri.Split('#')[0] -OutFile $zip)) { throw 'Could not download uv from the selected index.' }
|
||||
$hasher = [Security.Cryptography.SHA256]::Create()
|
||||
$stream = [IO.File]::OpenRead($zip)
|
||||
try { $digest = [BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-', '') }
|
||||
finally { $stream.Dispose(); $hasher.Dispose() }
|
||||
if ($digest -ne $candidate.Groups['hash'].Value) {
|
||||
throw 'The downloaded uv package failed SHA256 verification.'
|
||||
}
|
||||
$extract = Join-Path $work 'wheel'
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[IO.Compression.ZipFile]::ExtractToDirectory($zip, $extract)
|
||||
$executable = Get-ChildItem -LiteralPath $extract -Filter uv.exe -Recurse -File | Select-Object -First 1
|
||||
if (-not $executable) { throw 'The uv package did not contain uv.exe.' }
|
||||
New-Item -ItemType Directory -Path $InstallDir -Force -ErrorAction Stop | Out-Null
|
||||
Copy-Item -LiteralPath $executable.FullName -Destination (Join-Path $InstallDir 'uv.exe') -Force -ErrorAction Stop
|
||||
$uvx = Join-Path $executable.DirectoryName 'uvx.exe'
|
||||
if (Test-Path -LiteralPath $uvx) { Copy-Item -LiteralPath $uvx -Destination $InstallDir -Force -ErrorAction Stop }
|
||||
$env:PATH = "$InstallDir;$env:PATH"
|
||||
} finally {
|
||||
$resolved = (Resolve-Path -LiteralPath $work).Path
|
||||
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\'
|
||||
if ($resolved.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -and (Split-Path $resolved -Leaf) -like 'artemis-uv-*') {
|
||||
Remove-Item -LiteralPath $resolved -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Install-ArtemisLockedDependencies {
|
||||
param([string]$PythonExe, [string]$IndexUrl)
|
||||
# uv sync uses the original artifact URLs in uv.lock. Export exact versions and
|
||||
# hashes, then let uv pip fetch those same artifacts through the chosen index.
|
||||
$requirements = Join-Path ([IO.Path]::GetTempPath()) ('artemis-requirements-' + [guid]::NewGuid().ToString('N') + '.txt')
|
||||
try {
|
||||
& uv export --frozen --no-dev --format requirements-txt --output-file $requirements 1>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not export the locked dependencies.' }
|
||||
& uv pip install --python $PythonExe --index-url $IndexUrl --requirements $requirements | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed. Check mirror availability or select another source in config/downloads.json.' }
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $requirements -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Create a portable source ZIP without environments, builds, or local credentials."""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
|
||||
EXCLUDED_DIRS = {
|
||||
".git",
|
||||
".venv",
|
||||
"venv",
|
||||
"env",
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
"__pypackages__",
|
||||
".angular",
|
||||
".gradle",
|
||||
".pytest_cache",
|
||||
".ruff_cache",
|
||||
".mypy_cache",
|
||||
".cache",
|
||||
".tox",
|
||||
".nox",
|
||||
".hypothesis",
|
||||
".eggs",
|
||||
".idea",
|
||||
".vscode",
|
||||
".vs",
|
||||
".codex",
|
||||
".claude",
|
||||
".gemini",
|
||||
".cursor",
|
||||
".ipynb_checkpoints",
|
||||
"build",
|
||||
"dist",
|
||||
"out",
|
||||
"coverage",
|
||||
"htmlcov",
|
||||
"artifacts",
|
||||
"traces",
|
||||
"scratch",
|
||||
"outputs",
|
||||
"trace_output",
|
||||
}
|
||||
EXCLUDED_SUFFIXES = {
|
||||
".pyc",
|
||||
".pyo",
|
||||
".pyd",
|
||||
".so",
|
||||
".dll",
|
||||
".exe",
|
||||
".o",
|
||||
".obj",
|
||||
".a",
|
||||
".lib",
|
||||
".class",
|
||||
".jar",
|
||||
".apk",
|
||||
".aab",
|
||||
".wasm",
|
||||
".map",
|
||||
".tsbuildinfo",
|
||||
".egg",
|
||||
".whl",
|
||||
".zip",
|
||||
".7z",
|
||||
".tar",
|
||||
".gz",
|
||||
".log",
|
||||
".db",
|
||||
".sqlite",
|
||||
".sqlite3",
|
||||
".pem",
|
||||
".key",
|
||||
".keystore",
|
||||
".p12",
|
||||
".pfx",
|
||||
".swp",
|
||||
".swo",
|
||||
}
|
||||
|
||||
|
||||
def excluded(path: Path) -> bool:
|
||||
parts = tuple(part.lower() for part in path.parts)
|
||||
name = parts[-1]
|
||||
if any(part in EXCLUDED_DIRS or part.endswith(".egg-info") for part in parts):
|
||||
return True
|
||||
# This tracked directory also contains generated Angular bundles.
|
||||
if parts[:3] == ("artemis", "resources", "showcase_ui"):
|
||||
return True
|
||||
if (name == ".env" or name.startswith(".env.")) and name != ".env.example":
|
||||
return True
|
||||
if name.startswith((".artemis_", ".jetski_", ".coverage")):
|
||||
return True
|
||||
if name in {".ds_store", "thumbs.db", ".ipc_port", ".ls_address", ".npmrc", ".pypirc"}:
|
||||
return True
|
||||
return path.suffix.lower() in EXCLUDED_SUFFIXES or name.endswith((".db-wal", ".db-shm", "~"))
|
||||
|
||||
|
||||
def source_files(root: Path):
|
||||
for directory, dirs, files in os.walk(root, followlinks=False):
|
||||
current = Path(directory)
|
||||
dirs[:] = sorted(
|
||||
name
|
||||
for name in dirs
|
||||
if not (current / name).is_symlink()
|
||||
and not excluded((current / name).relative_to(root))
|
||||
)
|
||||
for name in sorted(files):
|
||||
path = current / name
|
||||
if not path.is_symlink() and not excluded(path.relative_to(root)):
|
||||
yield path
|
||||
|
||||
|
||||
def package_source(root: Path, output: Path) -> tuple[int, str]:
|
||||
root = root.resolve()
|
||||
output = output.resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = output.with_suffix(".zip.tmp")
|
||||
files = [p for p in source_files(root) if p.resolve() not in {output, temporary}]
|
||||
try:
|
||||
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
for path in files:
|
||||
archive.write(path, Path("artemis") / path.relative_to(root))
|
||||
with zipfile.ZipFile(temporary) as archive:
|
||||
bad_file = archive.testzip()
|
||||
if bad_file:
|
||||
raise RuntimeError(f"ZIP integrity check failed: {bad_file}")
|
||||
if any(excluded(Path(name).relative_to("artemis")) for name in archive.namelist()):
|
||||
raise RuntimeError("An excluded file was found in the source archive")
|
||||
temporary.replace(output)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
with output.open("rb") as stream:
|
||||
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
return len(files), digest
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
output = (
|
||||
args.output
|
||||
or args.root / "artifacts" / f"artemis-source-{datetime.now():%Y%m%d-%H%M%S}.zip"
|
||||
)
|
||||
count, digest = package_source(args.root, output)
|
||||
print(f"Archive: {output.resolve()}")
|
||||
print(f"Files: {count}; size: {output.stat().st_size / (1024 * 1024):.2f} MiB")
|
||||
print(f"SHA256: {digest}")
|
||||
+57
-51
@@ -15,7 +15,9 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$Port = 8000,
|
||||
[switch]$NoOpen = $false
|
||||
[switch]$NoOpen = $false,
|
||||
[switch]$SetupOnly = $false,
|
||||
[ValidateSet('Official', 'China')][string]$Mirror
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
@@ -30,6 +32,14 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$RootDir = Split-Path -Parent $ScriptDir
|
||||
Set-Location $RootDir
|
||||
|
||||
. (Join-Path $ScriptDir 'download_config.ps1')
|
||||
try {
|
||||
Initialize-ArtemisDownloads -ProjectRoot $RootDir -Mode $Mirror
|
||||
} catch {
|
||||
Write-Host " [ERROR] Invalid download settings: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "======================================================" -ForegroundColor Cyan
|
||||
Write-Host " Artemis Autonomous Mobile Agent UI " -ForegroundColor Cyan
|
||||
Write-Host "======================================================" -ForegroundColor Cyan
|
||||
@@ -105,7 +115,7 @@ function Invoke-DownloadFile {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Uri,
|
||||
[Parameter(Mandatory=$true)][string]$OutFile,
|
||||
[int]$TimeoutSec = 45
|
||||
[int]$TimeoutSec = $ArtemisDownloads.timeout_seconds
|
||||
)
|
||||
if (Test-Path $OutFile) {
|
||||
Remove-Item -Path $OutFile -Force -ErrorAction SilentlyContinue
|
||||
@@ -152,7 +162,7 @@ function Install-PortablePlatformTools {
|
||||
Write-Host " [INFO] Installing Android platform-tools (adb) in user space..." -ForegroundColor Cyan
|
||||
try {
|
||||
$zipPath = "$env:TEMP\platform-tools-windows.zip"
|
||||
if (Invoke-DownloadFile -Uri "https://dl.google.com/android/repository/platform-tools-latest-windows.zip" -OutFile $zipPath -TimeoutSec 60) {
|
||||
if (Invoke-DownloadFile -Uri $ArtemisDownloads.adb_url -OutFile $zipPath) {
|
||||
New-Item -ItemType Directory -Path "$env:LOCALAPPDATA\Programs" -Force | Out-Null
|
||||
Expand-Archive -Path $zipPath -DestinationPath "$env:LOCALAPPDATA\Programs" -Force
|
||||
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
||||
@@ -186,9 +196,12 @@ function Install-PortableScrcpy {
|
||||
}
|
||||
|
||||
Write-Host " [INFO] Installing portable scrcpy in user space..." -ForegroundColor Cyan
|
||||
if ($ArtemisDownloads.mode -eq 'China' -and $ArtemisDownloads.scrcpy_url.StartsWith('https://github.com/')) {
|
||||
Write-Host ' [INFO] scrcpy uses GitHub. If unreachable, set overrides.scrcpy_url in config/downloads.json.' -ForegroundColor Yellow
|
||||
}
|
||||
try {
|
||||
$zipPath = "$env:TEMP\scrcpy-win64.zip"
|
||||
if (Invoke-DownloadFile -Uri "https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-win64-v4.1.zip" -OutFile $zipPath -TimeoutSec 60) {
|
||||
if (Invoke-DownloadFile -Uri $ArtemisDownloads.scrcpy_url -OutFile $zipPath) {
|
||||
$extractDir = "$env:TEMP\scrcpy_extract"
|
||||
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
|
||||
@@ -262,7 +275,8 @@ function Install-PortableNode {
|
||||
if ($env:PROCESSOR_ARCHITECTURE -match "ARM64") { "arm64" } else { "x64" }
|
||||
} else { "x64" }
|
||||
$zipPath = "$env:TEMP\node-$nodeVer-win-$arch.zip"
|
||||
if (Invoke-DownloadFile -Uri "https://nodejs.org/dist/$nodeVer/node-$nodeVer-win-$arch.zip" -OutFile $zipPath -TimeoutSec 60) {
|
||||
$nodeBase = $ArtemisDownloads.node_mirror.TrimEnd('/')
|
||||
if (Invoke-DownloadFile -Uri "$nodeBase/$nodeVer/node-$nodeVer-win-$arch.zip" -OutFile $zipPath) {
|
||||
$extractDir = "$env:TEMP\node_extract"
|
||||
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
|
||||
@@ -291,33 +305,39 @@ function Install-PortableNode {
|
||||
# 1. Refresh PATH with standard toolchain paths
|
||||
Update-EnvironmentPath
|
||||
|
||||
# 2. Check or install uv
|
||||
if (-not (Test-CommandExists "uv")) {
|
||||
Write-Host " [INFO] uv not found. Installing Astral uv..." -ForegroundColor Yellow
|
||||
try {
|
||||
Invoke-Expression (Invoke-RestMethod -Uri "https://astral.sh/uv/install.ps1")
|
||||
} catch {
|
||||
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
}
|
||||
Update-EnvironmentPath
|
||||
# 2. Prepare the project Python environment before optional device/UI setup.
|
||||
. (Join-Path $ScriptDir 'bootstrap_python.ps1')
|
||||
try {
|
||||
$ArtemisPython = Initialize-ArtemisPython -ProjectRoot $RootDir
|
||||
} catch {
|
||||
Write-Host " [ERROR] $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-CommandExists "uv")) {
|
||||
Write-Host " [ERROR] Astral 'uv' package manager could not be installed or found in PATH." -ForegroundColor Red
|
||||
Write-Host " Please install uv manually from https://astral.sh/ or run:" -ForegroundColor Yellow
|
||||
Write-Host " powershell -c ""irm https://astral.sh/uv/install.ps1 | iex""" -ForegroundColor Cyan
|
||||
exit 1
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $RootDir '.env'))) {
|
||||
if (Test-Path -LiteralPath (Join-Path $RootDir '.env.example')) {
|
||||
Copy-Item -LiteralPath (Join-Path $RootDir '.env.example') -Destination (Join-Path $RootDir '.env') -ErrorAction Stop
|
||||
Write-Host ' [OK] Created .env from .env.example. Configure your model in the web UI.' -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
if ($SetupOnly) {
|
||||
Write-Host ' [OK] Setup complete. Run start.bat to launch Artemis.' -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 3. Check and try installing missing system toolchains (ADB, FFmpeg, scrcpy)
|
||||
$missingCore = @()
|
||||
if (-not (Test-CommandExists "adb")) { $missingCore += "adb" }
|
||||
if (-not (Test-CommandExists "ffmpeg")) { $missingCore += "ffmpeg" }
|
||||
# imageio-ffmpeg ships a working executable in its wheel, including mirror installs.
|
||||
if (-not (Test-CommandExists "ffmpeg")) {
|
||||
& $ArtemisPython -c 'import imageio_ffmpeg; imageio_ffmpeg.get_ffmpeg_exe()' 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { $missingCore += "ffmpeg" }
|
||||
}
|
||||
if (-not (Test-CommandExists "scrcpy")) { $missingCore += "scrcpy" }
|
||||
|
||||
if ($missingCore.Count -gt 0) {
|
||||
$useWinGet = $false
|
||||
if (Test-CommandExists "winget") {
|
||||
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "winget")) {
|
||||
if ([Console]::IsInputRedirected -eq $false) {
|
||||
Write-Host " [INFO] Missing components detected: $($missingCore -join ', ')." -ForegroundColor Cyan
|
||||
$ans = Read-Host " Install system packages via WinGet? [Y/n]"
|
||||
@@ -348,27 +368,15 @@ if ($missingCore.Count -gt 0) {
|
||||
Install-PortablePlatformTools
|
||||
}
|
||||
if (-not (Test-CommandExists "scrcpy")) {
|
||||
Install-PortableScrcpy
|
||||
if (-not (Install-PortableScrcpy)) {
|
||||
Write-Host ' [WARN] scrcpy download failed. Set overrides.scrcpy_url in config/downloads.json to a reachable release ZIP, then retry.' -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure local ADB daemon is warm & listening so probes do not hit 'Connection refused'
|
||||
Start-LocalAdbServer
|
||||
|
||||
# 4. Check or initialize .env
|
||||
if (-not (Test-Path ".env")) {
|
||||
if (Test-Path ".env.example") {
|
||||
Copy-Item ".env.example" ".env"
|
||||
Write-Host " [OK] Initialized .env configuration file." -ForegroundColor Green
|
||||
} else {
|
||||
New-Item -ItemType File -Path ".env" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# 5. Synchronize dependencies
|
||||
Write-Host " [INFO] Synchronizing Python runtime and project dependencies..." -ForegroundColor Cyan
|
||||
uv sync --quiet
|
||||
|
||||
# 6. Check and build Showcase UI if not already compiled
|
||||
$ShowcaseIndex = "$RootDir\apps\showcase_ui\dist\frontend\browser\index.html"
|
||||
$ShowcaseIndexAlt1 = "$RootDir\apps\showcase_ui\dist\browser\index.html"
|
||||
@@ -383,7 +391,7 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
|
||||
}
|
||||
|
||||
# 1. Try nvm if available on Windows
|
||||
if (Test-CommandExists "nvm") {
|
||||
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "nvm")) {
|
||||
Write-Host " [INFO] Installing Node.js 22 LTS via nvm..." -ForegroundColor Cyan
|
||||
& nvm install 22.23.2 | Out-Null
|
||||
& nvm use 22.23.2 | Out-Null
|
||||
@@ -393,7 +401,7 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
|
||||
# 2. Try WinGet
|
||||
if (-not (Test-NodeCompatible)) {
|
||||
$useWinGetNode = $false
|
||||
if (Test-CommandExists "winget") {
|
||||
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "winget")) {
|
||||
if ([Console]::IsInputRedirected -eq $false) {
|
||||
Write-Host " [INFO] Node.js >= v22.22.0 required for Showcase UI." -ForegroundColor Cyan
|
||||
$ans = Read-Host " Install/Upgrade Node.js via WinGet (may require Administrator)? [Y/n]"
|
||||
@@ -426,31 +434,29 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
|
||||
try {
|
||||
$npmExec = if (Test-CommandExists "npm.cmd") { "npm.cmd" } else { "npm" }
|
||||
Write-Host " [INFO] Installing frontend npm dependencies..." -ForegroundColor Cyan
|
||||
& $npmExec install --no-audit --no-fund --loglevel=error
|
||||
& $npmExec ci --no-audit --no-fund --loglevel=error
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " [WARN] npm install returned exit code $LASTEXITCODE. Trying silent install..." -ForegroundColor DarkYellow
|
||||
& $npmExec install --silent
|
||||
}
|
||||
$cliNodeVersion = "$RootDir\apps\showcase_ui\node_modules\@angular\cli\src\utilities\node-version.js"
|
||||
if (Test-Path $cliNodeVersion) {
|
||||
(Get-Content $cliNodeVersion) -replace '22\.22\.3', '22.22.0' | Set-Content $cliNodeVersion
|
||||
throw "npm ci failed (exit $LASTEXITCODE). Check the network and retry."
|
||||
}
|
||||
Write-Host " [INFO] Building Angular frontend application..." -ForegroundColor Cyan
|
||||
& $npmExec run build
|
||||
if ($LASTEXITCODE -ne 0) { throw "Frontend build failed (exit $LASTEXITCODE)." }
|
||||
if (Test-Path $ShowcaseIndex) {
|
||||
Write-Host " [OK] Showcase UI compiled successfully." -ForegroundColor Green
|
||||
} elseif (Test-Path $ShowcaseIndexAlt1) {
|
||||
Write-Host " [OK] Showcase UI compiled successfully." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [WARN] Showcase UI build finished but index.html was not found in expected dist directory." -ForegroundColor DarkYellow
|
||||
throw 'Frontend build did not produce index.html.'
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [WARN] Failed to build Showcase UI: $_" -ForegroundColor DarkYellow
|
||||
Write-Host " [ERROR] Failed to build Showcase UI: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
} else {
|
||||
Write-Host " [WARN] Node.js >= v22.22.0 not found. Showcase UI will show fallback notice on launch." -ForegroundColor DarkYellow
|
||||
Write-Host " [ERROR] A compatible Node.js/npm could not be installed. Install Node.js LTS and retry." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,7 +476,7 @@ if ([Console]::IsInputRedirected -eq $false) {
|
||||
|
||||
if ($installMcp -match "^[Yy]") {
|
||||
Write-Host " Installing MCP server configuration & testing rules..." -ForegroundColor Cyan
|
||||
uv run python -m artemis mcp --install all
|
||||
& $ArtemisPython -m artemis mcp --install all
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host " [OK] MCP configuration and rules installed successfully." -ForegroundColor Green
|
||||
Write-Host " [TIP] You can update or re-install anytime with: uv run artemis mcp --install all" -ForegroundColor Cyan
|
||||
@@ -490,9 +496,9 @@ Start-LocalAdbServer
|
||||
# long-running server never locks .venv\Scripts\artemis.exe against `uv sync` reinstalls.
|
||||
Write-Host " [INFO] Launching Artemis Showcase UI & Admin Console..." -ForegroundColor Green
|
||||
if ($NoOpen) {
|
||||
uv run python -m artemis ui --port $Port --no-open
|
||||
& $ArtemisPython -m artemis ui --port $Port --no-open
|
||||
} else {
|
||||
uv run python -m artemis ui --port $Port --open
|
||||
& $ArtemisPython -m artemis ui --port $Port --open
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
Reference in new issue
Block a user