From 8522fa3ca3e341034325bf4844624d0893217e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E9=93=AD=E8=BD=A9?= Date: Thu, 24 Sep 2026 10:49:57 +0800 Subject: [PATCH] fix: ship updatable 1.1.1 source archives Package a clean shallow main checkout with credential-free Gitea remote, localize the China launcher, and set the new-install Responses endpoint. --- .env.example | 2 +- README.md | 8 +- .../services/model_settings_service.py | 6 +- apps/showcase_ui/package-lock.json | 4 +- apps/showcase_ui/package.json | 2 +- .../model-settings.component.ts | 2 +- artemis/config/settings.py | 2 +- artemis/utils/credentials_validator.py | 6 +- docs/ai-automation.md | 2 +- docs/releases/1.1.1.md | 13 ++ pyproject.toml | 2 +- scripts/bootstrap_python.ps1 | 48 ++++--- scripts/download_config.ps1 | 10 +- scripts/package_source.py | 122 +++++++++++++++-- scripts/start.ps1 | 82 ++++++------ scripts/update_installation.py | 74 ++++++++-- start-cn.bat | 32 ++++- start.bat | 6 +- .../unit/admin_console/test_model_settings.py | 11 ++ tests/unit/test_bootstrap_scripts_encoding.py | 23 ++-- tests/unit/test_source_archive.py | 126 ++++++++++++++++-- uv.lock | 2 +- 22 files changed, 445 insertions(+), 140 deletions(-) create mode 100644 docs/releases/1.1.1.md diff --git a/.env.example b/.env.example index 0ecd44f..6674400 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ # 所有模型调用仅使用 OpenAI Responses API;环境变量优先于 .env。 OPENAI_API_KEY= # 接口根地址:通常以 /v1 结尾,不要添加 /responses 或 /chat/completions。 -OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_BASE_URL=https://api-n-cd.playones.com/v1 OPENAI_MODEL=mimo-v2.6-flash # 单次模型 HTTP 请求超时(秒) OPENAI_TIMEOUT_SECONDS=300 diff --git a/README.md b/README.md index 355894b..70c2bab 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ## 安装与启动 -准备一台开启 USB 调试并完成授权的 Android 手机,或已启动的 Android 模拟器。首次安装需要联网,建议使用 Git 克隆以支持后续更新。 +准备一台开启 USB 调试并完成授权的 Android 手机,或已启动的 Android 模拟器。首次安装需要联网。可使用 Git 克隆或下载 1.1.1 及更新版本的发布源码包;新版 ZIP 解压后也保留 Git 更新能力。 ### Windows @@ -24,7 +24,7 @@ cd Artemis-Eapil .\start.bat ``` -国内网络使用 `.\start-cn.bat`。启动脚本会检测并安装缺少的 uv、Python 和所需工具,创建或复用 `.venv`,同步依赖并构建网页。启动后访问 **http://127.0.0.1:8000**。 +国内网络使用 `.\start-cn.bat`,支持 `-Help` 查看中文选项。启动脚本会检测并安装缺少的 uv、Python 和所需工具,创建或复用 `.venv`,同步依赖并构建网页。启动后访问 **http://127.0.0.1:8000**。 常用选项: @@ -49,7 +49,7 @@ cd Artemis-Eapil 预设模型为 `gpt-5.6-luna`、`mimo-v2.6-flash`,也可填写自定义名称。默认模型是 `mimo-v2.6-flash`,思考强度默认 `high`(最高可选 `max`),单次请求超时 **300 秒**,失败后最多重试 **5 次**,间隔 **10 秒**;视频分析与录屏默认关闭。模型及思考参数是否可用取决于所配置网关;不支持时会明确报错。 -也可在项目 `.env` 中设置 `OPENAI_API_KEY`、`OPENAI_BASE_URL` 和 `OPENAI_MODEL`,其他参数见 [.env.example](.env.example)。直接编辑环境文件后需重启相应服务。不要提交自己的密钥。 +也可在项目 `.env` 中设置 `OPENAI_API_KEY`、`OPENAI_BASE_URL` 和 `OPENAI_MODEL`,其他参数见 [.env.example](.env.example)。新安装默认接口地址为 `https://api-n-cd.playones.com/v1`;已保存的地址不变。直接编辑环境文件后需重启相应服务。不要提交自己的密钥。 报告会区分“执行完成”和“验证通过”;没有验证证据的结果标为“未验证”。任务步骤与截图可查看,设备动作回放不提供。 @@ -71,7 +71,7 @@ uv run artemis mcp --install codex Windows 用户再次运行一键脚本时,会先检查 Git 上游;即使 `start.bat` 或 `start-cn.bat` 本身被更新,也会重新启动新版脚本,再检查项目依赖、前端构建和已有 MCP 配置。源码只做快进更新。本地修改、分支分叉或网络认证失败时保留本地版本;有任务执行或排队时停止更新启动。更新后需在 AI 客户端重新加载 MCP。 -ZIP 安装可继续运行;需要自动拉取源码时,请改用 Git 克隆安装。遇到启动或设备问题,在项目目录运行 `uv run artemis doctor`。 +1.1.1 及更新版本的发布 ZIP 含浅层 Git 工作树,解压后再次运行一键脚本即可检查更新。1.1.0 及更早 ZIP 不含 Git 信息,无法自行拉取;需要下载新版源码包重新解压一次,之后即可自动更新。遇到启动或设备问题,在项目目录运行 `uv run artemis doctor`。 更多说明:[批量任务与报告](docs/automation-workspace.md) · [AI 接入](docs/ai-automation.md) · [开发协作指南](AGENTS.md) diff --git a/apps/admin_console/services/model_settings_service.py b/apps/admin_console/services/model_settings_service.py index f81bbc6..c348a1f 100644 --- a/apps/admin_console/services/model_settings_service.py +++ b/apps/admin_console/services/model_settings_service.py @@ -16,7 +16,7 @@ _write_lock = threading.Lock() class ModelSettingsUpdate(BaseModel): - base_url: str = "https://api.openai.com/v1" + base_url: str = "https://api-n-cd.playones.com/v1" model: str = Field(min_length=1, max_length=200) api_key: SecretStr | None = None # Omitted/blank keeps the existing key. timeout_seconds: float = Field(default=300, gt=0, le=3600) @@ -41,7 +41,7 @@ class ModelSettingsUpdate(BaseModel): ): raise ValueError("请输入有效的 HTTP(S) 接口根地址,不要包含密钥、查询参数或片段") if value.endswith(("/responses", "/chat/completions")): - raise ValueError("请输入接口根地址,例如 https://api.openai.com/v1") + raise ValueError("请输入接口根地址,例如 https://api-n-cd.playones.com/v1") return value @field_validator("model") @@ -57,7 +57,7 @@ def get_model_settings() -> dict: return { "provider": "openai", "api_mode": "responses", - "base_url": settings.OPENAI_BASE_URL or "https://api.openai.com/v1", + "base_url": settings.OPENAI_BASE_URL or "https://api-n-cd.playones.com/v1", "model": settings.OPENAI_MODEL, "api_key_configured": bool(settings.OPENAI_API_KEY), "timeout_seconds": settings.OPENAI_TIMEOUT_SECONDS, diff --git a/apps/showcase_ui/package-lock.json b/apps/showcase_ui/package-lock.json index 4a541d2..68db408 100644 --- a/apps/showcase_ui/package-lock.json +++ b/apps/showcase_ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "artemis-showcase-ui", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis-showcase-ui", - "version": "1.1.0", + "version": "1.1.1", "dependencies": { "@angular/cdk": "^22.1.5", "@angular/common": "^22.1.4", diff --git a/apps/showcase_ui/package.json b/apps/showcase_ui/package.json index df7385a..f215c2b 100644 --- a/apps/showcase_ui/package.json +++ b/apps/showcase_ui/package.json @@ -1,6 +1,6 @@ { "name": "artemis-showcase-ui", - "version": "1.1.0", + "version": "1.1.1", "scripts": { "ng": "ng", "start": "ng serve --proxy-config proxy.conf.json", diff --git a/apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts b/apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts index a34b6de..750e179 100644 --- a/apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts +++ b/apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts @@ -24,7 +24,7 @@ interface ModelSettings {

统一使用 OpenAI Responses。保存后新任务立即生效,重启后自动加载。

+ placeholder="https://api-n-cd.playones.com/v1" [disabled]="busy() || loading()" /> 填写接口根地址,通常包含 /v1,无需添加 /responses。
diff --git a/artemis/config/settings.py b/artemis/config/settings.py index ddfd6f4..5f33424 100644 --- a/artemis/config/settings.py +++ b/artemis/config/settings.py @@ -95,7 +95,7 @@ class Settings(BaseSettings): API_KEY: SecretStr | None = None # Custom Provider Endpoints - OPENAI_BASE_URL: str | None = None + OPENAI_BASE_URL: str | None = "https://api-n-cd.playones.com/v1" OPENAI_MODEL: str = Field(default="mimo-v2.6-flash", min_length=1) OPENAI_TIMEOUT_SECONDS: float = Field(default=300.0, gt=0) OPENAI_MAX_RETRIES: int = Field(default=5, ge=0, le=10) diff --git a/artemis/utils/credentials_validator.py b/artemis/utils/credentials_validator.py index 4495d80..8f6fa5a 100644 --- a/artemis/utils/credentials_validator.py +++ b/artemis/utils/credentials_validator.py @@ -115,9 +115,9 @@ async def validate_api_key( return False, f"Vision OCR API verification failed ({resp.status_code}): {err_msg}" elif clean_provider == "openai": - base = (base_url or settings.OPENAI_BASE_URL or "https://api.openai.com/v1").rstrip( - "/" - ) + base = ( + base_url or settings.OPENAI_BASE_URL or "https://api-n-cd.playones.com/v1" + ).rstrip("/") resp = await client.post( f"{base}/responses", headers={"Authorization": f"Bearer {clean_key}"}, diff --git a/docs/ai-automation.md b/docs/ai-automation.md index 6336022..0d8a51c 100644 --- a/docs/ai-automation.md +++ b/docs/ai-automation.md @@ -32,7 +32,7 @@ 新增工具通过统一后台服务运行,与网页共享配置、队列、历史和报告;不另外启动独立调度器。`ARTEMIS_STANDALONE` 模式不支持这些工作区工具。 -Windows 已安装用户再次运行 `start.bat` / `start-cn.bat` 时会检查当前分支的 Git 上游,仅做快进更新;拉取后重新加载新版启动脚本。没有 Git、ZIP 解压安装、网络/认证失败、有本地修改或分支分叉时保留现有源码并给出提示,不重置或覆盖。ZIP 用户可继续运行本地版本;需要自动源码升级时请改用 Git 克隆安装。`-SkipUpdate` 可跳过源码更新检查。 +Windows 已安装用户再次运行 `start.bat` / `start-cn.bat` 时会检查当前分支的 Git 上游,仅做快进更新;拉取后重新加载新版启动脚本。1.1.1 及更新版本的发布 ZIP 是精简的 Git 工作树,解压后也能在后续运行时检查更新。1.1.0 及更早 ZIP 缺少 `.git`,需手动下载新版源码包一次。没有 Git、网络/认证失败、有本地修改或分支分叉时保留现有源码并给出提示,不重置或覆盖。`-SkipUpdate` 可跳过源码更新检查。 Python 每次按 `uv.lock` 检查并同步缺失/变化的依赖;前端依据源码、资源及包锁文件指纹决定是否重新执行 `npm ci` 和构建,成功后才记录指纹。国内镜像选项继续生效。检测到正在执行/排队的任务时,自动更新启动会停止,让原任务继续。 diff --git a/docs/releases/1.1.1.md b/docs/releases/1.1.1.md new file mode 100644 index 0000000..cc48d7f --- /dev/null +++ b/docs/releases/1.1.1.md @@ -0,0 +1,13 @@ +# ARTEMIS-Eapil 1.1.1 + +## 修复与改进 + +- 发布源码包现在包含干净的浅层 Git 工作树,跟踪 Gitea 的 `main`。解压后再次运行 Windows 启动脚本可快进拉取更新;构建输出、虚拟环境、个人配置和本机 Git 凭据不随包发布。 +- `start-cn.bat` 提供中文帮助、启动说明和安装/更新关键提示,继续使用国内镜像。可运行 `start-cn.bat -Help` 查看参数。 +- 新安装默认 Responses 接口地址改为 `https://api-n-cd.playones.com/v1`。已经保存到 `.env` 的地址和密钥保持原样。 + +## 安装与升级 + +下载本版源码 ZIP 并解压,运行 `start.bat` 或 `start-cn.bat`。1.1.0 及更早的 ZIP 本身没有 `.git`,无法自动升级;请下载本版源码包一次,之后可用启动脚本检查更新。首次运行会创建 `.venv`、安装依赖并构建前端,重新加载 AI 客户端的 MCP 后使用新版实现。 + +更新采用 Git 快进;本地修改、分支分叉或任务运行中均不会覆盖。附件提供 `SHA256SUMS.txt` 用于校验源码包。 diff --git a/pyproject.toml b/pyproject.toml index 0938eec..f49b65f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [project] name = "artemis" -version = "1.1.0" +version = "1.1.1" description = "Autonomous AI Assistant for Mobile Automation" authors = [ diff --git a/scripts/bootstrap_python.ps1 b/scripts/bootstrap_python.ps1 index 1969e6d..fe28f3b 100644 --- a/scripts/bootstrap_python.ps1 +++ b/scripts/bootstrap_python.ps1 @@ -1,42 +1,48 @@ -# Python bootstrap shared by the Windows launcher and its isolated checks. -# Keep this file ASCII for Windows PowerShell 5.1. +# Python bootstrap shared by the Windows launcher and its isolated checks. +# Save as UTF-8 with BOM so Windows PowerShell 5.1 reads Chinese messages correctly. + +function Get-BootstrapText { + param([string]$English, [string]$Chinese) + if ($env:ARTEMIS_LAUNCH_LANG -eq 'zh-CN') { return $Chinese } + return $English +} 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.' + throw (Get-BootstrapText 'pyproject.toml is missing. Extract the complete source archive first.' '缺少 pyproject.toml;请重新解压完整源码包。') } if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot 'uv.lock'))) { - throw 'uv.lock is missing. Extract the complete source archive first.' + throw (Get-BootstrapText 'uv.lock is missing. Extract the complete source archive first.' '缺少 uv.lock;请重新解压完整源码包。') } if (-not (Test-CommandExists 'uv')) { if ($ArtemisDownloads -and $ArtemisDownloads.python_index) { - Write-Host ' [INSTALL] Installing uv from the selected Python mirror...' -ForegroundColor Cyan + Write-Host (Get-BootstrapText ' [INSTALL] Installing uv from the selected Python mirror...' ' [安装] 正在从选定的 Python 镜像安装 uv...') -ForegroundColor Cyan Install-ArtemisUvFromIndex -IndexUrl $ArtemisDownloads.python_index } else { - Write-Host ' [INSTALL] uv was not found. Installing from the configured installer...' -ForegroundColor Cyan + Write-Host (Get-BootstrapText ' [INSTALL] uv was not found. Installing from the configured installer...' ' [安装] 未找到 uv,正在下载安装...') -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.' + throw (Get-BootstrapText 'Could not download uv. Check your network/proxy and retry.' 'uv 下载失败;请检查网络、代理或镜像后重试。') } & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer | Out-Host - if ($LASTEXITCODE -ne 0) { throw "uv installation failed (exit $LASTEXITCODE)." } + if ($LASTEXITCODE -ne 0) { throw (Get-BootstrapText "uv installation failed (exit $LASTEXITCODE)." "uv 安装失败(退出码 $LASTEXITCODE)。") } } finally { Remove-Item -LiteralPath $installer -Force -ErrorAction SilentlyContinue } } Update-EnvironmentPath } else { - Write-Host ' [SKIP] uv is already installed.' -ForegroundColor Green + Write-Host (Get-BootstrapText ' [SKIP] uv is already installed.' ' [复用] uv 已安装,跳过下载。') -ForegroundColor Green } - if (-not (Test-CommandExists 'uv')) { throw 'uv is still unavailable. Install it from https://astral.sh/uv/ and retry.' } + if (-not (Test-CommandExists 'uv')) { throw (Get-BootstrapText 'uv is still unavailable. Install it from https://astral.sh/uv/ and retry.' '仍无法运行 uv;请检查安装并重试。') } & uv --version | Out-Host - if ($LASTEXITCODE -ne 0) { throw 'The installed uv could not run. Repair its installation and retry.' } + if ($LASTEXITCODE -ne 0) { throw (Get-BootstrapText 'The installed uv could not run. Repair its installation and retry.' '已安装的 uv 无法运行;请修复安装后重试。') } $venvDir = Join-Path $ProjectRoot '.venv' $pythonExe = Join-Path $venvDir 'Scripts\python.exe' @@ -45,18 +51,18 @@ function Initialize-ArtemisPython { 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 + Write-Host (Get-BootstrapText ' [INSTALL] Creating .venv with Python 3.12 (downloaded automatically if needed)...' ' [安装] 正在创建 Python 3.12 虚拟环境 .venv;缺少 Python 时自动下载...') -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.' } + if ($LASTEXITCODE -ne 0) { throw (Get-BootstrapText 'Could not create .venv. Check the download errors above and retry.' '.venv 创建失败;请查看上方下载错误并重试。') } } else { - Write-Host ' [SKIP] Reusing the existing project .venv.' -ForegroundColor Green + Write-Host (Get-BootstrapText ' [SKIP] Reusing the existing project .venv.' ' [复用] 已有项目 .venv,跳过创建。') -ForegroundColor Green } if (-not (Test-Path -LiteralPath $pythonExe)) { - throw 'The existing .venv is incomplete. Rename it as a backup, then rerun start.bat.' + throw (Get-BootstrapText 'The existing .venv is incomplete. Rename it as a backup, then rerun start.bat.' '已有 .venv 不完整。先重命名备份,再运行 start-cn.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.' + throw (Get-BootstrapText 'The existing .venv is unusable or older than Python 3.12. Rename it as a backup, then rerun start.bat.' '已有 .venv 无法使用或 Python 低于 3.12。先重命名备份,再运行 start-cn.bat。') } # A cheap check skips installation when the lock and installed packages agree. @@ -64,19 +70,19 @@ function Initialize-ArtemisPython { $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 + Write-Host (Get-BootstrapText ' [SKIP] Python dependencies are already ready.' ' [复用] Python 依赖已就绪。') -ForegroundColor Green } else { - Write-Host ' [INSTALL] Synchronizing dependencies from uv.lock...' -ForegroundColor Cyan + Write-Host (Get-BootstrapText ' [INSTALL] Synchronizing dependencies from uv.lock...' ' [安装] 正在按 uv.lock 同步 Python 依赖...') -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.' } + if ($LASTEXITCODE -ne 0) { throw (Get-BootstrapText 'Dependency installation failed. Fix the error above and rerun start.bat.' '依赖安装失败;请处理上方错误,再运行 start-cn.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 + if ($LASTEXITCODE -ne 0) { throw (Get-BootstrapText 'Python dependency verification failed. The UI will not be started.' 'Python 依赖验证失败,网页不会启动。') } + Write-Host (Get-BootstrapText ' [OK] Python environment is ready.' ' [完成] Python 环境已就绪。') -ForegroundColor Green return $pythonExe } finally { Pop-Location diff --git a/scripts/download_config.ps1 b/scripts/download_config.ps1 index 2cc94a7..cad7c9c 100644 --- a/scripts/download_config.ps1 +++ b/scripts/download_config.ps1 @@ -1,5 +1,5 @@ -# Download settings are process-local; never edit global pip/npm/uv settings. -# Keep this file ASCII for Windows PowerShell 5.1. +# Download settings are process-local; never edit global pip/npm/uv settings. +# Save as UTF-8 with BOM so Windows PowerShell 5.1 reads Chinese messages correctly. function Initialize-ArtemisDownloads { param([string]$ProjectRoot, [string]$Mode) @@ -42,7 +42,11 @@ function Initialize-ArtemisDownloads { # Official scrcpy v4.1 win64 release digest. Verify every mirror/override equally. $values.scrcpy_sha256 = '5b12172b3264b2889f4583ee64752ce832e29bc8b1089dca81093459697165db' $script:ArtemisDownloads = [pscustomobject]$values - Write-Host " [INFO] Download mode: $Mode (config/downloads.json)." -ForegroundColor Cyan + if ($env:ARTEMIS_LAUNCH_LANG -eq 'zh-CN') { + Write-Host " [镜像] 使用国内下载源(config/downloads.json)。" -ForegroundColor Cyan + } else { + Write-Host " [INFO] Download mode: $Mode (config/downloads.json)." -ForegroundColor Cyan + } } function Install-ArtemisUvFromIndex { diff --git a/scripts/package_source.py b/scripts/package_source.py index b3c9a37..ac5781d 100644 --- a/scripts/package_source.py +++ b/scripts/package_source.py @@ -1,10 +1,13 @@ -"""Create a portable source ZIP without environments, builds, or local credentials.""" +"""Create a clean, shallow Git checkout ZIP that can fast-forward on launch.""" import argparse from datetime import datetime import hashlib import os from pathlib import Path +import subprocess +import tempfile +from urllib.parse import urlsplit import zipfile @@ -83,6 +86,9 @@ EXCLUDED_SUFFIXES = { ".swo", } +GIT_METADATA_FILES = {"HEAD", "config", "index", "shallow", "packed-refs"} +GIT_METADATA_DIRS = {"objects", "refs"} + def excluded(path: Path) -> bool: parts = tuple(part.lower() for part in path.parts) @@ -116,22 +122,116 @@ def source_files(root: Path): yield path +def git(root: Path, *args: str) -> str: + result = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=90, + ) + if result.returncode: + raise RuntimeError(f"git {args[0]} failed (exit {result.returncode})") + return result.stdout.strip() + + +def git_metadata_files(root: Path): + metadata = root / ".git" + for name in sorted(GIT_METADATA_FILES): + path = metadata / name + if path.is_file(): + yield path + for name in sorted(GIT_METADATA_DIRS): + directory = metadata / name + if directory.exists(): + yield from source_files_without_filter(directory) + for name in ("exclude", "sparse-checkout"): + path = metadata / "info" / name + if path.is_file(): + yield path + + +def source_files_without_filter(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()) + for name in sorted(files): + path = current / name + if not path.is_symlink(): + yield path + + +def release_remote(root: Path) -> tuple[str, str]: + if not (root / ".git").exists(): + raise ValueError("Source packaging requires a Git checkout") + if git(root, "status", "--porcelain", "--untracked-files=no"): + raise ValueError("Commit tracked changes before packaging a release") + branch = git(root, "symbolic-ref", "--quiet", "--short", "HEAD") + remote = git(root, "config", "--get", f"branch.{branch}.remote") + merge_ref = git(root, "config", "--get", f"branch.{branch}.merge") + if remote == "." or merge_ref != f"refs/heads/{branch}": + raise ValueError("The current branch must track the same branch on a remote") + url = git(root, "remote", "get-url", remote) + parsed = urlsplit(url) + if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password: + raise ValueError("The release upstream must be a credential-free HTTPS URL") + if parsed.query or parsed.fragment: + raise ValueError("The release upstream must not contain a query or fragment") + return branch, url + + def package_source(root: Path, output: Path) -> tuple[int, str]: root = root.resolve() output = output.resolve() + branch, remote_url = release_remote(root) + revision = git(root, "rev-parse", "HEAD") 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") + with tempfile.TemporaryDirectory(prefix="artemis-release-") as work: + checkout = Path(work) / "artemis" + git( + root, + "clone", + "--depth", + "1", + "--single-branch", + "--branch", + branch, + root.as_uri(), + str(checkout), + ) + git(checkout, "remote", "set-url", "origin", remote_url) + # Generated assets are tracked upstream but must not appear in the + # unpacked source. Sparse checkout keeps the Git index clean. + omitted = [ + name for name in git(checkout, "ls-files").splitlines() if excluded(Path(name)) + ] + patterns = ["/*", *(f"!/{name}" for name in omitted)] + git(checkout, "sparse-checkout", "set", "--no-cone", *patterns) + # Rebuilt assets in sparse paths must not look like user edits. + info_exclude = checkout / ".git" / "info" / "exclude" + with info_exclude.open("a", encoding="utf-8") as stream: + stream.write("\n".join(["", *(f"/{name}" for name in omitted), ""])) + if git(checkout, "rev-parse", "HEAD") != revision or git( + checkout, "status", "--porcelain" + ): + raise RuntimeError("The packaged checkout is not clean at the requested revision") + files = [*source_files(checkout), *git_metadata_files(checkout)] + names = { + str(Path("artemis") / path.relative_to(checkout)).replace("\\", "/") + for path in files + } + with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive: + for path in files: + archive.write(path, Path("artemis") / path.relative_to(checkout)) + with zipfile.ZipFile(temporary) as archive: + bad_file = archive.testzip() + if bad_file or set(archive.namelist()) != names: + raise RuntimeError( + f"ZIP integrity check failed: {bad_file or 'file list mismatch'}" + ) temporary.replace(output) finally: temporary.unlink(missing_ok=True) diff --git a/scripts/start.ps1 b/scripts/start.ps1 index da40776..758f376 100644 --- a/scripts/start.ps1 +++ b/scripts/start.ps1 @@ -1,4 +1,4 @@ -# Copyright 2026 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,6 +24,12 @@ param( $ErrorActionPreference = "Continue" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +function Get-LaunchText { + param([string]$English, [string]$Chinese) + if ($env:ARTEMIS_LAUNCH_LANG -eq 'zh-CN') { return $Chinese } + return $English +} + # Ensure modern TLS protocols are enabled for downloads (GitHub releases, nodejs.org, Google repositories) try { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 @@ -49,7 +55,7 @@ if ((-not $SkipUpdate) -and (Test-Path -LiteralPath $existingPython)) { exit $LASTEXITCODE } if ($updateExit -ne 0) { - Write-Host ' [ERROR] Source update check failed. Resolve the error above, or use -SkipUpdate for local startup.' -ForegroundColor Red + Write-Host (Get-LaunchText ' [ERROR] Source update check failed. Resolve the error above, or use -SkipUpdate for local startup.' ' [错误] 源码更新检查失败。请处理上方错误;要运行当前源码可传入 -SkipUpdate。') -ForegroundColor Red exit 1 } } @@ -58,12 +64,12 @@ if ((-not $SkipUpdate) -and (Test-Path -LiteralPath $existingPython)) { try { Initialize-ArtemisDownloads -ProjectRoot $RootDir -Mode $Mirror } catch { - Write-Host " [ERROR] Invalid download settings: $_" -ForegroundColor Red + Write-Host (Get-LaunchText " [ERROR] Invalid download settings: $_" " [错误] 下载配置无效:$_") -ForegroundColor Red exit 1 } Write-Host "======================================================" -ForegroundColor Cyan -Write-Host " Artemis Autonomous Mobile Agent UI " -ForegroundColor Cyan +Write-Host (Get-LaunchText " Artemis Autonomous Mobile Agent UI " " ARTEMIS 手机自动化测试平台") -ForegroundColor Cyan Write-Host "======================================================" -ForegroundColor Cyan Write-Host "" @@ -181,7 +187,7 @@ function Install-PortablePlatformTools { $env:PATH = "$ptDir;$env:PATH" return $true } - Write-Host " [INFO] Installing Android platform-tools (adb) in user space..." -ForegroundColor Cyan + Write-Host (Get-LaunchText " [INFO] Installing Android platform-tools (adb) in user space..." " [安装] 正在安装 Android 调试工具 adb(当前用户)...") -ForegroundColor Cyan try { $zipPath = "$env:TEMP\platform-tools-windows.zip" if (Invoke-DownloadFile -Uri $ArtemisDownloads.adb_url -OutFile $zipPath) { @@ -190,12 +196,12 @@ function Install-PortablePlatformTools { Remove-Item $zipPath -Force -ErrorAction SilentlyContinue if (Test-Path "$ptDir\adb.exe") { $env:PATH = "$ptDir;$env:PATH" - Write-Host " [OK] adb installed in user space." -ForegroundColor Green + Write-Host (Get-LaunchText " [OK] adb installed in user space." " [完成] adb 已安装。") -ForegroundColor Green return $true } } } catch { - Write-Host " [WARN] Failed to install portable platform-tools: $_" -ForegroundColor DarkYellow + Write-Host (Get-LaunchText " [WARN] Failed to install portable platform-tools: $_" " [提示] adb 安装失败:$_") -ForegroundColor DarkYellow } return $false } @@ -217,9 +223,9 @@ function Install-PortableScrcpy { } } - Write-Host " [INFO] Installing portable scrcpy in user space..." -ForegroundColor Cyan + Write-Host (Get-LaunchText " [INFO] Installing portable scrcpy in user space..." " [安装] 正在安装 scrcpy(当前用户)...") -ForegroundColor Cyan if ($ArtemisDownloads.mode -eq 'China' -and $ArtemisDownloads.scrcpy_url.StartsWith('https://github.com/')) { - Write-Host ' [WARN] China mode has no non-GitHub scrcpy source. Configure overrides.scrcpy_url or install scrcpy manually.' -ForegroundColor Yellow + Write-Host (Get-LaunchText ' [WARN] China mode has no non-GitHub scrcpy source. Configure overrides.scrcpy_url or install scrcpy manually.' ' [提示] 国内镜像中没有可用的 scrcpy 下载地址;请设置 config/downloads.json 的 overrides.scrcpy_url,或自行安装。') -ForegroundColor Yellow return $false } try { @@ -244,12 +250,12 @@ function Install-PortableScrcpy { Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue if (Test-Path "$scrcpyDir\scrcpy.exe") { $env:PATH = "$scrcpyDir;$env:PATH" - Write-Host " [OK] Portable scrcpy installed in user space." -ForegroundColor Green + Write-Host (Get-LaunchText " [OK] Portable scrcpy installed in user space." " [完成] scrcpy 已安装。") -ForegroundColor Green return $true } } } catch { - Write-Host " [WARN] Failed to install portable scrcpy: $_" -ForegroundColor DarkYellow + Write-Host (Get-LaunchText " [WARN] Failed to install portable scrcpy: $_" " [提示] scrcpy 安装失败:$_") -ForegroundColor DarkYellow } return $false } @@ -295,7 +301,7 @@ function Install-PortableNode { } } - Write-Host " [INFO] Installing portable Node.js LTS (v22.23.2) in user space..." -ForegroundColor Cyan + Write-Host (Get-LaunchText " [INFO] Installing portable Node.js LTS (v22.23.2) in user space..." " [安装] 正在安装 Node.js LTS v22.23.2(当前用户)...") -ForegroundColor Cyan try { $nodeVer = "v22.23.2" $arch = if ([System.Environment]::Is64BitOperatingSystem) { @@ -319,12 +325,12 @@ function Install-PortableNode { Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue if (Test-Path "$nodeDir\node.exe") { $env:PATH = "$nodeDir;$env:PATH" - Write-Host " [OK] Portable Node.js $nodeVer installed in user space." -ForegroundColor Green + Write-Host (Get-LaunchText " [OK] Portable Node.js $nodeVer installed in user space." " [完成] Node.js $nodeVer 已安装。") -ForegroundColor Green return $true } } } catch { - Write-Host " [WARN] Failed to install portable Node.js: $_" -ForegroundColor DarkYellow + Write-Host (Get-LaunchText " [WARN] Failed to install portable Node.js: $_" " [提示] Node.js 安装失败:$_") -ForegroundColor DarkYellow } return $false } @@ -344,16 +350,16 @@ try { 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 + Write-Host (Get-LaunchText ' [OK] Created .env from .env.example. Configure your model in the web UI.' ' [完成] 已按模板创建 .env;请在网页中配置模型。') -ForegroundColor Green } } -Write-Host ' [CHECK] Refreshing already-configured MCP clients...' -ForegroundColor Cyan +Write-Host (Get-LaunchText ' [CHECK] Refreshing already-configured MCP clients...' ' [检查] 正在检查并更新已有的 MCP 客户端配置...') -ForegroundColor Cyan & $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') mcp if ($LASTEXITCODE -ne 0) { - Write-Host ' [WARN] MCP refresh failed. Existing client settings were not replaced; check the errors above.' -ForegroundColor Yellow + Write-Host (Get-LaunchText ' [WARN] MCP refresh failed. Existing client settings were not replaced; check the errors above.' ' [提示] MCP 配置更新失败,原配置仍保留;请查看上方错误。') -ForegroundColor Yellow } if ($SetupOnly) { - Write-Host ' [OK] Setup complete. Run start.bat to launch Artemis.' -ForegroundColor Green + Write-Host (Get-LaunchText ' [OK] Setup complete. Run start.bat to launch Artemis.' ' [完成] 环境已准备好。运行 start-cn.bat 启动 ARTEMIS。') -ForegroundColor Green exit 0 } @@ -371,8 +377,8 @@ if ($missingCore.Count -gt 0) { $useWinGet = $false 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]" + Write-Host (Get-LaunchText " [INFO] Missing components detected: $($missingCore -join ', ')." " [检查] 缺少组件:$($missingCore -join ', ')。") -ForegroundColor Cyan + $ans = Read-Host (Get-LaunchText " Install system packages via WinGet? [Y/n]" " 通过 WinGet 安装?[Y/n]") if ($ans -eq "" -or $ans -match "^[Yy]") { $useWinGet = $true } @@ -382,7 +388,7 @@ if ($missingCore.Count -gt 0) { } if ($useWinGet) { - Write-Host " [INFO] Auto-installing missing components ($($missingCore -join ', '))..." -ForegroundColor DarkYellow + Write-Host (Get-LaunchText " [INFO] Auto-installing missing components ($($missingCore -join ', '))..." " [安装] 正在安装缺少的组件:$($missingCore -join ', ')...") -ForegroundColor DarkYellow if ($missingCore -contains "adb") { winget install --id Google.PlatformTools -e --accept-source-agreements --accept-package-agreements --silent 2>$null | Out-Null } @@ -401,7 +407,7 @@ if ($missingCore.Count -gt 0) { } if (-not (Test-CommandExists "scrcpy")) { 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 + Write-Host (Get-LaunchText ' [WARN] scrcpy download failed. Set overrides.scrcpy_url in config/downloads.json to a reachable release ZIP, then retry.' ' [提示] scrcpy 下载失败。请在 config/downloads.json 中设置可访问的 overrides.scrcpy_url,然后重试。') -ForegroundColor Yellow } } } @@ -415,19 +421,19 @@ $ShowcaseIndexAlt1 = "$RootDir\apps\showcase_ui\dist\browser\index.html" $ShowcaseIndexAlt2 = "$RootDir\apps\showcase_ui\dist\index.html" & $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') frontend-check $frontendChanged = $LASTEXITCODE -eq 10 -if ($LASTEXITCODE -notin @(0, 10)) { Write-Host ' [ERROR] Could not check frontend sources.' -ForegroundColor Red; exit 1 } +if ($LASTEXITCODE -notin @(0, 10)) { Write-Host (Get-LaunchText ' [ERROR] Could not check frontend sources.' ' [错误] 无法检查前端源码。') -ForegroundColor Red; exit 1 } if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1)) -and (-not (Test-Path $ShowcaseIndexAlt2)))) { if (-not (Test-NodeCompatible)) { if (Test-CommandExists "node") { $curVer = (& node -v 2>$null) - Write-Host " [INFO] Detected Node.js $curVer, but Angular CLI requires Node.js >= v22.22.0." -ForegroundColor Yellow + Write-Host (Get-LaunchText " [INFO] Detected Node.js $curVer, but Angular CLI requires Node.js >= v22.22.0." " [检查] 检测到 Node.js $curVer;前端需要 v22.22.0 或更新版本。") -ForegroundColor Yellow } else { - Write-Host " [INFO] Node.js/npm not found (required for Showcase UI)." -ForegroundColor Cyan + Write-Host (Get-LaunchText " [INFO] Node.js/npm not found (required for Showcase UI)." " [检查] 未找到 Node.js/npm,网页构建需要安装。") -ForegroundColor Cyan } # 1. Try nvm if available on Windows if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "nvm")) { - Write-Host " [INFO] Installing Node.js 22 LTS via nvm..." -ForegroundColor Cyan + Write-Host (Get-LaunchText " [INFO] Installing Node.js 22 LTS via nvm..." " [安装] 正在通过 nvm 安装 Node.js 22 LTS...") -ForegroundColor Cyan & nvm install 22.23.2 | Out-Null & nvm use 22.23.2 | Out-Null Update-EnvironmentPath @@ -438,8 +444,8 @@ if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Pat $useWinGetNode = $false 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]" + Write-Host (Get-LaunchText " [INFO] Node.js >= v22.22.0 required for Showcase UI." " [检查] 网页需要 Node.js v22.22.0 或更新版本。") -ForegroundColor Cyan + $ans = Read-Host (Get-LaunchText " Install/Upgrade Node.js via WinGet (may require Administrator)? [Y/n]" " 通过 WinGet 安装/升级 Node.js(可能需要管理员权限)?[Y/n]") if ($ans -eq "" -or $ans -match "^[Yy]") { $useWinGetNode = $true } @@ -449,7 +455,7 @@ if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Pat } if ($useWinGetNode) { - Write-Host " [INFO] Installing/Upgrading Node.js LTS via WinGet..." -ForegroundColor Yellow + Write-Host (Get-LaunchText " [INFO] Installing/Upgrading Node.js LTS via WinGet..." " [安装] 正在通过 WinGet 安装/升级 Node.js LTS...") -ForegroundColor Yellow winget install --id OpenJS.NodeJS.LTS -e --accept-source-agreements --accept-package-agreements --silent 2>$null | Out-Null Update-EnvironmentPath } @@ -463,36 +469,36 @@ if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Pat if (Test-NodeCompatible) { $nodeVer = (& node -v 2>$null) - Write-Host " [OK] Node.js $nodeVer is ready." -ForegroundColor Green - Write-Host " [INFO] Showcase UI sources/dependencies changed or build missing. Compiling..." -ForegroundColor Yellow + Write-Host (Get-LaunchText " [OK] Node.js $nodeVer is ready." " [完成] Node.js $nodeVer 已就绪。") -ForegroundColor Green + Write-Host (Get-LaunchText " [INFO] Showcase UI sources/dependencies changed or build missing. Compiling..." " [构建] 前端源码/依赖有变化或缺少构建结果,准备重新构建...") -ForegroundColor Yellow Push-Location "$RootDir\apps\showcase_ui" try { $npmExec = if (Test-CommandExists "npm.cmd") { "npm.cmd" } else { "npm" } - Write-Host " [INFO] Installing frontend npm dependencies..." -ForegroundColor Cyan + Write-Host (Get-LaunchText " [INFO] Installing frontend npm dependencies..." " [安装] 正在安装前端 npm 依赖...") -ForegroundColor Cyan & $npmExec ci --no-audit --no-fund --loglevel=error if ($LASTEXITCODE -ne 0) { throw "npm ci failed (exit $LASTEXITCODE). Check the network and retry." } - Write-Host " [INFO] Building Angular frontend application..." -ForegroundColor Cyan + Write-Host (Get-LaunchText " [INFO] Building Angular frontend application..." " [构建] 正在构建 Angular 网页...") -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 + Write-Host (Get-LaunchText " [OK] Showcase UI compiled successfully." " [完成] 网页构建成功。") -ForegroundColor Green } elseif ((Test-Path $ShowcaseIndexAlt1) -or (Test-Path $ShowcaseIndexAlt2)) { - Write-Host " [OK] Showcase UI compiled successfully." -ForegroundColor Green + Write-Host (Get-LaunchText " [OK] Showcase UI compiled successfully." " [完成] 网页构建成功。") -ForegroundColor Green } else { throw 'Frontend build did not produce index.html.' } & $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') frontend-mark if ($LASTEXITCODE -ne 0) { throw 'Could not save frontend build fingerprint.' } } catch { - Write-Host " [ERROR] Failed to build Showcase UI: $_" -ForegroundColor Red + Write-Host (Get-LaunchText " [ERROR] Failed to build Showcase UI: $_" " [错误] 网页构建失败:$_") -ForegroundColor Red exit 1 } finally { Pop-Location } } else { - Write-Host " [ERROR] A compatible Node.js/npm could not be installed. Install Node.js LTS and retry." -ForegroundColor Red + Write-Host (Get-LaunchText " [ERROR] A compatible Node.js/npm could not be installed. Install Node.js LTS and retry." " [错误] 无法安装兼容的 Node.js/npm。请安装 Node.js LTS 后重试。") -ForegroundColor Red exit 1 } } @@ -505,7 +511,7 @@ Start-LocalAdbServer # Launch via `python -m artemis` (not the `artemis.exe` console-script shim) so the # long-running server never locks .venv\Scripts\artemis.exe against `uv sync` reinstalls. -Write-Host " [INFO] Launching Artemis Showcase UI & Admin Console..." -ForegroundColor Green +Write-Host (Get-LaunchText " [INFO] Launching Artemis Showcase UI & Admin Console..." " [启动] 正在启动 ARTEMIS 网页与管理服务...") -ForegroundColor Green if ($NoOpen) { & $ArtemisPython -m artemis ui --port $Port --no-open } else { diff --git a/scripts/update_installation.py b/scripts/update_installation.py index 392f43b..c755536 100644 --- a/scripts/update_installation.py +++ b/scripts/update_installation.py @@ -19,6 +19,10 @@ from urllib.parse import urlsplit ROOT = Path(__file__).resolve().parents[1] +def launch_text(english: str, chinese: str) -> str: + return chinese if os.environ.get("ARTEMIS_LAUNCH_LANG") == "zh-CN" else english + + def git(root: Path, *args: str, timeout: int = 15) -> str: env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GCM_INTERACTIVE": "Never"} result = subprocess.run( @@ -50,19 +54,28 @@ def update_source(root: Path, port: int = 8000) -> int: or (status.get("status") == "running" and status.get("pid")) ): print( - "[WAIT] Tasks are active. Finish/stop them before updating dependencies or restarting." + launch_text( + "[WAIT] Tasks are active. Finish/stop them before updating dependencies or restarting.", + "[等待] 有任务正在执行或排队。请先处理任务,再更新依赖或重启。", + ) ) return 20 except (OSError, ValueError): pass if not shutil.which("git"): print( - "[SKIP] Git is not installed. Continuing with local source; install Git for automatic source updates." + launch_text( + "[SKIP] Git is not installed. Continuing with local source; install Git for automatic source updates.", + "[跳过] 未安装 Git,继续使用当前源码;安装 Git 后可自动检查更新。", + ) ) return 0 if not (root / ".git").exists(): print( - "[SKIP] This source ZIP has no Git metadata. Use a Git clone for automatic source updates; local files are preserved." + launch_text( + "[SKIP] This older source ZIP has no Git metadata. Use a new release ZIP or Git clone for automatic updates; local files are preserved.", + "[跳过] 旧版源码包不含 Git 信息。请使用新版源码包或 Git 克隆;当前文件保持不变。", + ) ) return 0 try: @@ -79,32 +92,49 @@ def update_source(root: Path, port: int = 8000) -> int: raise RuntimeError( "Use a clean Git remote URL and a credential manager, not embedded credentials." ) - print("[CHECK] Checking the current branch's configured upstream...") + print( + launch_text( + "[CHECK] Checking the current branch's configured upstream...", + "[检查] 正在检查当前分支的远程更新...", + ) + ) git(root, "fetch", "--no-tags", remote, merge_ref, timeout=45) target = git(root, "rev-parse", "FETCH_HEAD") current = git(root, "rev-parse", "HEAD") if current == target: - print("[OK] Source is up to date.") + print(launch_text("[OK] Source is up to date.", "[完成] 源码已是最新版。")) return 0 if git(root, "status", "--porcelain"): print( - "[SKIP] Upstream differs, but local changes exist. Commit/back up your work before updating; continuing locally." + launch_text( + "[SKIP] Upstream differs, but local changes exist. Commit/back up your work before updating; continuing locally.", + "[跳过] 发现本地修改;请先提交或备份。此次继续使用本地源码。", + ) ) return 0 ancestor = git(root, "merge-base", "HEAD", target) if ancestor != current: print( - "[SKIP] Local branch is ahead or diverged. No automatic merge/rebase; continuing locally." + launch_text( + "[SKIP] Local branch is ahead or diverged. No automatic merge/rebase; continuing locally.", + "[跳过] 本地分支领先或分叉,不自动合并;继续使用本地源码。", + ) ) return 0 git(root, "merge", "--ff-only", target, timeout=30) print( - f"[UPDATED] Source {current[:12]} -> {target[:12]}. Reloading launcher and checking dependencies." + launch_text( + f"[UPDATED] Source {current[:12]} -> {target[:12]}. Reloading launcher and checking dependencies.", + f"[已更新] 源码 {current[:12]} -> {target[:12]},重新载入启动脚本并检查依赖。", + ) ) return 10 except (RuntimeError, subprocess.TimeoutExpired) as exc: print( - f"[WARN] {exc if isinstance(exc, RuntimeError) else 'Git update timed out.'} Continuing with local source." + launch_text( + f"[WARN] {exc if isinstance(exc, RuntimeError) else 'Git update timed out.'} Continuing with local source.", + "[提示] Git 更新失败或超时;请检查网络、凭据和仓库状态。继续使用当前源码。", + ) ) return 0 @@ -275,14 +305,32 @@ def refresh_mcp(root: Path) -> None: else refresh_codex(path, root, sys.executable, ARTEMIS_MCP_TOOLS) ) if updated: - print(f"[UPDATED] Existing MCP configuration: {path}") + print( + launch_text( + f"[UPDATED] Existing MCP configuration: {path}", + f"[已更新] 已有 MCP 配置:{path}", + ) + ) changed += 1 except (OSError, ValueError, TypeError, AttributeError): - print(f"[WARN] Could not safely refresh {path}; original configuration preserved.") + print( + launch_text( + f"[WARN] Could not safely refresh {path}; original configuration preserved.", + f"[提示] 无法安全更新 {path};原配置已保留。", + ) + ) print( - f"[OK] MCP source is current; refreshed {changed} existing client configuration(s). Reload the MCP connection in your AI client after a source update." + launch_text( + f"[OK] MCP source is current; refreshed {changed} existing client configuration(s). Reload the MCP connection in your AI client after a source update.", + f"[完成] MCP 源码已就绪;更新了 {changed} 个已有客户端配置。源码升级后请在 AI 客户端重新加载 MCP。", + ) + ) + print( + launch_text( + "[TIP] For a new client run: uv run artemis mcp --install ", + "[提示] 新客户端可执行:uv run artemis mcp --install ", + ) ) - print("[TIP] For a new client run: uv run artemis mcp --install ") def frontend_fingerprint(root: Path) -> str: diff --git a/start-cn.bat b/start-cn.bat index 0313c72..21f5511 100644 --- a/start-cn.bat +++ b/start-cn.bat @@ -1,6 +1,32 @@ @echo off -rem Use China mirrors for first setup and subsequent launches. -rem All start.bat arguments, including -SetupOnly, are supported. +chcp 65001 >nul 2>&1 setlocal DisableDelayedExpansion +set "ARTEMIS_LAUNCH_LANG=zh-CN" +title ARTEMIS 国内镜像启动 + +if /i "%~1"=="-Help" goto help +if /i "%~1"=="--help" goto help + +echo ====================================================== +echo ARTEMIS 中文启动 - 使用国内镜像 +echo ====================================================== +echo 正在检查已有环境、项目更新及依赖;首次启动可能需要下载组件。 +echo 镜像地址和下载超时可在 config\downloads.json 中调整。 +echo. call "%~dp0start.bat" -Mirror China %* -exit /b %ERRORLEVEL% +set "ARTEMIS_CN_EXIT_CODE=%ERRORLEVEL%" +if not "%ARTEMIS_CN_EXIT_CODE%"=="0" echo 启动未完成;请查看上方错误信息,并检查网络或镜像设置。 +endlocal & exit /b %ARTEMIS_CN_EXIT_CODE% + +:help +echo 用法:start-cn.bat [选项] +echo. +echo -SetupOnly 安装或检查环境,不启动网页 +echo -Port 8001 使用指定端口,默认 8000 +echo -NoOpen 启动服务后不自动打开浏览器 +echo -SkipUpdate 跳过 Git 源码更新检查 +echo -Help 显示本说明 +echo. +echo 国内镜像设置:config\downloads.json +echo 首次运行需联网;后续运行会检查源码、依赖和已有 MCP 配置。 +endlocal & exit /b 0 diff --git a/start.bat b/start.bat index 82e8c5b..bf33a11 100644 --- a/start.bat +++ b/start.bat @@ -15,12 +15,12 @@ rem limitations under the License. chcp 65001 >nul 2>&1 setlocal DisableDelayedExpansion -title Artemis Mobile Agent UI +if /i "%ARTEMIS_LAUNCH_LANG%"=="zh-CN" (title ARTEMIS 手机自动化测试) else (title Artemis Mobile Agent UI) pushd "%~dp0" || exit /b 1 echo ====================================================== -echo Artemis Autonomous Mobile Agent UI +if /i "%ARTEMIS_LAUNCH_LANG%"=="zh-CN" (echo ARTEMIS 手机自动化测试) else (echo Artemis Autonomous Mobile Agent UI) echo ====================================================== echo. @@ -33,7 +33,7 @@ set "ARTEMIS_EXIT_CODE=%ERRORLEVEL%" if not "%ARTEMIS_EXIT_CODE%"=="0" ( echo. - echo An error occurred while starting Artemis. + if /i "%ARTEMIS_LAUNCH_LANG%"=="zh-CN" (echo ARTEMIS 启动失败,请查看上方详细信息。) else (echo An error occurred while starting Artemis.) if not defined ARTEMIS_NO_PAUSE pause ) popd diff --git a/tests/unit/admin_console/test_model_settings.py b/tests/unit/admin_console/test_model_settings.py index 4940dd9..eb2f7c1 100644 --- a/tests/unit/admin_console/test_model_settings.py +++ b/tests/unit/admin_console/test_model_settings.py @@ -59,6 +59,17 @@ def test_save_reload_and_blank_key_preserves_secret(isolated_settings): assert dotenv_values(isolated_settings)["OPENAI_API_KEY"] == "new-test-secret" +def test_new_install_uses_playones_endpoint_without_overriding_saved_url(monkeypatch): + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + expected = "https://api-n-cd.playones.com/v1" + assert Settings(_env_file=None).OPENAI_BASE_URL == expected + assert service.ModelSettingsUpdate(model="test").base_url == expected + monkeypatch.setattr(settings, "OPENAI_BASE_URL", None) + assert service.get_model_settings()["base_url"] == expected + monkeypatch.setattr(settings, "OPENAI_BASE_URL", "https://saved.example/v1") + assert service.get_model_settings()["base_url"] == "https://saved.example/v1" + + @pytest.mark.parametrize( "url", [ diff --git a/tests/unit/test_bootstrap_scripts_encoding.py b/tests/unit/test_bootstrap_scripts_encoding.py index d0b90c1..55095fd 100644 --- a/tests/unit/test_bootstrap_scripts_encoding.py +++ b/tests/unit/test_bootstrap_scripts_encoding.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Keep bootstrap scripts ASCII for Windows PowerShell 5.1 compatibility. +"""Require ASCII or UTF-8 with BOM for Windows PowerShell 5.1 scripts. BOM-less scripts are decoded using the system ANSI code page. UTF-8 symbols can become quote delimiters under cp1252 and cause parse errors. @@ -31,18 +31,11 @@ def test_powershell_scripts_are_discovered(): @pytest.mark.parametrize("script", POWERSHELL_SCRIPTS, ids=lambda p: p.name) -def test_powershell_script_is_pure_ascii(script): +def test_powershell_script_has_windows_compatible_encoding(script): raw = script.read_bytes() - offending = [] - line_no = 1 - for index, byte in enumerate(raw): - if byte == 0x0A: - line_no += 1 - elif byte > 0x7F: - offending.append((line_no, index, hex(byte))) - - assert not offending, ( - f"{script.name} contains non-ASCII bytes at (line, offset, byte) " - f"{offending[:10]}; Windows PowerShell 5.1 decodes BOM-less .ps1 files " - "with the system ANSI code page. Use ASCII status labels." - ) + if any(byte > 0x7F for byte in raw): + assert raw.startswith(b"\xef\xbb\xbf"), ( + f"{script.name} contains non-ASCII bytes without a UTF-8 BOM; " + "Windows PowerShell 5.1 would use the system ANSI code page." + ) + raw.decode("utf-8-sig") diff --git a/tests/unit/test_source_archive.py b/tests/unit/test_source_archive.py index 6240873..eb3b7a3 100644 --- a/tests/unit/test_source_archive.py +++ b/tests/unit/test_source_archive.py @@ -1,36 +1,134 @@ import importlib.util from pathlib import Path +import shutil +import subprocess import zipfile +import pytest -def test_archive_keeps_source_and_excludes_local_builds_and_secrets(tmp_path): - script = Path(__file__).resolve().parents[2] / "scripts/package_source.py" - spec = importlib.util.spec_from_file_location("package_source", script) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) + +SCRIPT = Path(__file__).resolve().parents[2] / "scripts/package_source.py" +spec = importlib.util.spec_from_file_location("package_source", SCRIPT) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +def git(root, *args): + result = subprocess.run( + ["git", "-C", str(root), *args], capture_output=True, text=True, timeout=30 + ) + assert result.returncode == 0, result.stderr + return result.stdout.strip() + + +@pytest.fixture +def repository(tmp_path): + if not shutil.which("git"): + pytest.skip("Git required") + root = tmp_path / "source" + root.mkdir() + git(root, "init", "-b", "main") + git(root, "config", "user.name", "Fixture") + git(root, "config", "user.email", "fixture@example.test") keep = ["start.bat", ".env.example", "uv.lock", "apps/showcase_ui/src/main.ts"] + tracked_builds = [ + "artemis/resources/showcase_ui/main.js", + "packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk", + "packages/artemis-accessibility-helper/debug.keystore", + ] omit = [ ".env", ".env.local", ".venv/pyvenv.cfg", "nested/node_modules/foo.js", "apps/showcase_ui/dist/main.js", - "artemis/resources/showcase_ui/main.js", "__pycache__/a.pyc", "traces/data.db", - "helper.apk", - "debug.keystore", - ".git/config", ".roo/mcp.json", ] - for name in keep + omit: - target = tmp_path / name + for name in keep + tracked_builds + omit: + target = root / name target.parent.mkdir(parents=True, exist_ok=True) target.write_text("fixture") + (root / ".gitignore").write_text( + ".env*\n!.env.example\n.venv/\nnode_modules/\ndist/\ntraces/\n.roo/\n__pycache__/\n" + ) + git(root, "add", "--", ".gitignore", *keep, *tracked_builds) + git(root, "commit", "-m", "initial") + git(root, "remote", "add", "eapil", "https://git.example.invalid/team/project.git") + git(root, "config", "branch.main.remote", "eapil") + git(root, "config", "branch.main.merge", "refs/heads/main") + return root, keep, tracked_builds, omit + + +def test_archive_is_updatable_clean_checkout_without_local_data(repository, tmp_path): + root, keep, tracked_builds, omit = repository output = tmp_path / "artifacts/source.zip" - count, digest = module.package_source(tmp_path, output) - assert count == len(keep) + count, digest = module.package_source(root, output) + assert count > len(keep) assert len(digest) == 64 with zipfile.ZipFile(output) as archive: assert archive.testzip() is None - assert set(archive.namelist()) == {f"artemis/{name}" for name in keep} + names = set(archive.namelist()) + assert {f"artemis/{name}" for name in keep}.issubset(names) + assert not {f"artemis/{name}" for name in tracked_builds + omit} & names + assert "artemis/.git/config" in names + assert "artemis/.git/index" in names + assert not any(name.startswith("artemis/.git/logs/") for name in names) + assert not any(name.startswith("artemis/.git/hooks/") for name in names) + archive.extractall(tmp_path / "unpacked") + extracted = tmp_path / "unpacked/artemis" + assert git(extracted, "symbolic-ref", "--short", "HEAD") == "main" + assert git(extracted, "rev-parse", "HEAD") == git(root, "rev-parse", "HEAD") + assert git(extracted, "status", "--porcelain") == "" + assert git(extracted, "remote", "get-url", "origin") == ( + "https://git.example.invalid/team/project.git" + ) + assert git(extracted, "config", "--get", "branch.main.remote") == "origin" + (extracted / tracked_builds[0]).parent.mkdir(parents=True) + (extracted / tracked_builds[0]).write_text("new bundle") + assert git(extracted, "status", "--porcelain") == "" + + +def test_unpacked_archive_fast_forwards(repository, tmp_path): + root, _, tracked_builds, _ = repository + bare = tmp_path / "upstream.git" + bare.mkdir() + git(bare, "init", "--bare") + git(root, "remote", "add", "test-upstream", str(bare)) + git(root, "push", "test-upstream", "main") + archive = tmp_path / "source.zip" + module.package_source(root, archive) + with zipfile.ZipFile(archive) as contents: + contents.extractall(tmp_path / "unpacked") + extracted = tmp_path / "unpacked/artemis" + git(extracted, "remote", "set-url", "origin", str(bare)) + built_asset = extracted / tracked_builds[0] + built_asset.parent.mkdir(parents=True) + built_asset.write_text("generated after setup") + (root / "start.bat").write_text("updated") + git(root, "add", "--", "start.bat") + git(root, "commit", "-m", "update launcher") + git(root, "push", "test-upstream", "main") + from scripts.update_installation import update_source + + assert update_source(extracted, port=1) == 10 + assert (extracted / "start.bat").read_text() == "updated" + assert git(extracted, "status", "--porcelain") == "" + + +def test_packaging_rejects_uncommitted_changes_and_credentials(repository, tmp_path): + root, _, _, _ = repository + (root / "start.bat").write_text("local change") + with pytest.raises(ValueError, match="Commit tracked changes"): + module.package_source(root, tmp_path / "source.zip") + (root / "start.bat").write_text("fixture") + git( + root, + "remote", + "set-url", + "eapil", + "https://user:secret@git.example.invalid/team/project.git", + ) + with pytest.raises(ValueError, match="credential-free HTTPS"): + module.package_source(root, tmp_path / "source.zip") diff --git a/uv.lock b/uv.lock index 965562a..c3ce3d2 100644 --- a/uv.lock +++ b/uv.lock @@ -206,7 +206,7 @@ wheels = [ [[package]] name = "artemis" -version = "1.1.0" +version = "1.1.1" source = { editable = "." } dependencies = [ { name = "adbutils" },