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.
This commit is contained in:
1 parent
bfcd828fa8
commit
89fe60bf95
22 files changed
+445
-140
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# 所有模型调用仅使用 OpenAI Responses API;环境变量优先于 .env。
|
# 所有模型调用仅使用 OpenAI Responses API;环境变量优先于 .env。
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
# 接口根地址:通常以 /v1 结尾,不要添加 /responses 或 /chat/completions。
|
# 接口根地址:通常以 /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
|
OPENAI_MODEL=mimo-v2.6-flash
|
||||||
# 单次模型 HTTP 请求超时(秒)
|
# 单次模型 HTTP 请求超时(秒)
|
||||||
OPENAI_TIMEOUT_SECONDS=300
|
OPENAI_TIMEOUT_SECONDS=300
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
## 安装与启动
|
## 安装与启动
|
||||||
|
|
||||||
准备一台开启 USB 调试并完成授权的 Android 手机,或已启动的 Android 模拟器。首次安装需要联网,建议使用 Git 克隆以支持后续更新。
|
准备一台开启 USB 调试并完成授权的 Android 手机,或已启动的 Android 模拟器。首次安装需要联网。可使用 Git 克隆或下载 1.1.1 及更新版本的发布源码包;新版 ZIP 解压后也保留 Git 更新能力。
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ cd Artemis-Eapil
|
|||||||
.\start.bat
|
.\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 秒**;视频分析与录屏默认关闭。模型及思考参数是否可用取决于所配置网关;不支持时会明确报错。
|
预设模型为 `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。
|
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)
|
更多说明:[批量任务与报告](docs/automation-workspace.md) · [AI 接入](docs/ai-automation.md) · [开发协作指南](AGENTS.md)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ _write_lock = threading.Lock()
|
|||||||
|
|
||||||
|
|
||||||
class ModelSettingsUpdate(BaseModel):
|
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)
|
model: str = Field(min_length=1, max_length=200)
|
||||||
api_key: SecretStr | None = None # Omitted/blank keeps the existing key.
|
api_key: SecretStr | None = None # Omitted/blank keeps the existing key.
|
||||||
timeout_seconds: float = Field(default=300, gt=0, le=3600)
|
timeout_seconds: float = Field(default=300, gt=0, le=3600)
|
||||||
@@ -41,7 +41,7 @@ class ModelSettingsUpdate(BaseModel):
|
|||||||
):
|
):
|
||||||
raise ValueError("请输入有效的 HTTP(S) 接口根地址,不要包含密钥、查询参数或片段")
|
raise ValueError("请输入有效的 HTTP(S) 接口根地址,不要包含密钥、查询参数或片段")
|
||||||
if value.endswith(("/responses", "/chat/completions")):
|
if value.endswith(("/responses", "/chat/completions")):
|
||||||
raise ValueError("请输入接口根地址,例如 https://api.openai.com/v1")
|
raise ValueError("请输入接口根地址,例如 https://api-n-cd.playones.com/v1")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator("model")
|
@field_validator("model")
|
||||||
@@ -57,7 +57,7 @@ def get_model_settings() -> dict:
|
|||||||
return {
|
return {
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"api_mode": "responses",
|
"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,
|
"model": settings.OPENAI_MODEL,
|
||||||
"api_key_configured": bool(settings.OPENAI_API_KEY),
|
"api_key_configured": bool(settings.OPENAI_API_KEY),
|
||||||
"timeout_seconds": settings.OPENAI_TIMEOUT_SECONDS,
|
"timeout_seconds": settings.OPENAI_TIMEOUT_SECONDS,
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^22.1.5",
|
"@angular/cdk": "^22.1.5",
|
||||||
"@angular/common": "^22.1.4",
|
"@angular/common": "^22.1.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve --proxy-config proxy.conf.json",
|
"start": "ng serve --proxy-config proxy.conf.json",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface ModelSettings {
|
|||||||
<p>统一使用 OpenAI Responses。保存后新任务立即生效,重启后自动加载。</p>
|
<p>统一使用 OpenAI Responses。保存后新任务立即生效,重启后自动加载。</p>
|
||||||
<label for="model-base-url">接口地址</label>
|
<label for="model-base-url">接口地址</label>
|
||||||
<input id="model-base-url" name="baseUrl" type="url" [(ngModel)]="baseUrl" required
|
<input id="model-base-url" name="baseUrl" type="url" [(ngModel)]="baseUrl" required
|
||||||
placeholder="https://api.openai.com/v1" [disabled]="busy() || loading()" />
|
placeholder="https://api-n-cd.playones.com/v1" [disabled]="busy() || loading()" />
|
||||||
<small>填写接口根地址,通常包含 /v1,无需添加 /responses。</small>
|
<small>填写接口根地址,通常包含 /v1,无需添加 /responses。</small>
|
||||||
<label for="model-name">模型名称</label>
|
<label for="model-name">模型名称</label>
|
||||||
<div class="model-presets" role="group" aria-label="预设模型">
|
<div class="model-presets" role="group" aria-label="预设模型">
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class Settings(BaseSettings):
|
|||||||
API_KEY: SecretStr | None = None
|
API_KEY: SecretStr | None = None
|
||||||
|
|
||||||
# Custom Provider Endpoints
|
# 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_MODEL: str = Field(default="mimo-v2.6-flash", min_length=1)
|
||||||
OPENAI_TIMEOUT_SECONDS: float = Field(default=300.0, gt=0)
|
OPENAI_TIMEOUT_SECONDS: float = Field(default=300.0, gt=0)
|
||||||
OPENAI_MAX_RETRIES: int = Field(default=5, ge=0, le=10)
|
OPENAI_MAX_RETRIES: int = Field(default=5, ge=0, le=10)
|
||||||
|
|||||||
@@ -115,9 +115,9 @@ async def validate_api_key(
|
|||||||
return False, f"Vision OCR API verification failed ({resp.status_code}): {err_msg}"
|
return False, f"Vision OCR API verification failed ({resp.status_code}): {err_msg}"
|
||||||
|
|
||||||
elif clean_provider == "openai":
|
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(
|
resp = await client.post(
|
||||||
f"{base}/responses",
|
f"{base}/responses",
|
||||||
headers={"Authorization": f"Bearer {clean_key}"},
|
headers={"Authorization": f"Bearer {clean_key}"},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
新增工具通过统一后台服务运行,与网页共享配置、队列、历史和报告;不另外启动独立调度器。`ARTEMIS_STANDALONE` 模式不支持这些工作区工具。
|
新增工具通过统一后台服务运行,与网页共享配置、队列、历史和报告;不另外启动独立调度器。`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` 和构建,成功后才记录指纹。国内镜像选项继续生效。检测到正在执行/排队的任务时,自动更新启动会停止,让原任务继续。
|
Python 每次按 `uv.lock` 检查并同步缺失/变化的依赖;前端依据源码、资源及包锁文件指纹决定是否重新执行 `npm ci` 和构建,成功后才记录指纹。国内镜像选项继续生效。检测到正在执行/排队的任务时,自动更新启动会停止,让原任务继续。
|
||||||
|
|
||||||
|
|||||||
@@ -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` 用于校验源码包。
|
||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "artemis"
|
name = "artemis"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
description = "Autonomous AI Assistant for Mobile Automation"
|
description = "Autonomous AI Assistant for Mobile Automation"
|
||||||
|
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
@@ -1,42 +1,48 @@
|
|||||||
# Python bootstrap shared by the Windows launcher and its isolated checks.
|
# Python bootstrap shared by the Windows launcher and its isolated checks.
|
||||||
# Keep this file ASCII for Windows PowerShell 5.1.
|
# 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 {
|
function Initialize-ArtemisPython {
|
||||||
param([Parameter(Mandatory=$true)][string]$ProjectRoot)
|
param([Parameter(Mandatory=$true)][string]$ProjectRoot)
|
||||||
|
|
||||||
$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot -ErrorAction Stop).Path
|
$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot -ErrorAction Stop).Path
|
||||||
if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot 'pyproject.toml'))) {
|
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'))) {
|
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 (-not (Test-CommandExists 'uv')) {
|
||||||
if ($ArtemisDownloads -and $ArtemisDownloads.python_index) {
|
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
|
Install-ArtemisUvFromIndex -IndexUrl $ArtemisDownloads.python_index
|
||||||
} else {
|
} 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')
|
$installer = Join-Path ([IO.Path]::GetTempPath()) ('artemis-uv-' + [guid]::NewGuid().ToString('N') + '.ps1')
|
||||||
try {
|
try {
|
||||||
$installerUrl = if ($ArtemisDownloads) { $ArtemisDownloads.uv_installer_url } else { 'https://astral.sh/uv/install.ps1' }
|
$installerUrl = if ($ArtemisDownloads) { $ArtemisDownloads.uv_installer_url } else { 'https://astral.sh/uv/install.ps1' }
|
||||||
if (-not (Invoke-DownloadFile -Uri $installerUrl -OutFile $installer)) {
|
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
|
& 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 {
|
} finally {
|
||||||
Remove-Item -LiteralPath $installer -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $installer -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Update-EnvironmentPath
|
Update-EnvironmentPath
|
||||||
} else {
|
} 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
|
& 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'
|
$venvDir = Join-Path $ProjectRoot '.venv'
|
||||||
$pythonExe = Join-Path $venvDir 'Scripts\python.exe'
|
$pythonExe = Join-Path $venvDir 'Scripts\python.exe'
|
||||||
@@ -45,18 +51,18 @@ function Initialize-ArtemisPython {
|
|||||||
Push-Location -LiteralPath $ProjectRoot
|
Push-Location -LiteralPath $ProjectRoot
|
||||||
try {
|
try {
|
||||||
if (-not (Test-Path -LiteralPath $venvDir)) {
|
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
|
& 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 {
|
} 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)) {
|
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
|
& $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) {
|
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.
|
# 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' }
|
$lockOption = if ($ArtemisDownloads -and $ArtemisDownloads.python_index) { '--frozen' } else { '--locked' }
|
||||||
& uv sync --check $lockOption --no-dev --inexact --quiet 2>$null | Out-Host
|
& uv sync --check $lockOption --no-dev --inexact --quiet 2>$null | Out-Host
|
||||||
if ($LASTEXITCODE -eq 0) {
|
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 {
|
} 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) {
|
if ($ArtemisDownloads -and $ArtemisDownloads.python_index) {
|
||||||
Install-ArtemisLockedDependencies -PythonExe $pythonExe -IndexUrl $ArtemisDownloads.python_index
|
Install-ArtemisLockedDependencies -PythonExe $pythonExe -IndexUrl $ArtemisDownloads.python_index
|
||||||
} else {
|
} else {
|
||||||
& uv sync --locked --no-dev --inexact | Out-Host
|
& 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
|
& $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.' }
|
if ($LASTEXITCODE -ne 0) { throw (Get-BootstrapText 'Python dependency verification failed. The UI will not be started.' 'Python 依赖验证失败,网页不会启动。') }
|
||||||
Write-Host ' [OK] Python environment is ready.' -ForegroundColor Green
|
Write-Host (Get-BootstrapText ' [OK] Python environment is ready.' ' [完成] Python 环境已就绪。') -ForegroundColor Green
|
||||||
return $pythonExe
|
return $pythonExe
|
||||||
} finally {
|
} finally {
|
||||||
Pop-Location
|
Pop-Location
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Download settings are process-local; never edit global pip/npm/uv settings.
|
# Download settings are process-local; never edit global pip/npm/uv settings.
|
||||||
# Keep this file ASCII for Windows PowerShell 5.1.
|
# Save as UTF-8 with BOM so Windows PowerShell 5.1 reads Chinese messages correctly.
|
||||||
|
|
||||||
function Initialize-ArtemisDownloads {
|
function Initialize-ArtemisDownloads {
|
||||||
param([string]$ProjectRoot, [string]$Mode)
|
param([string]$ProjectRoot, [string]$Mode)
|
||||||
@@ -42,7 +42,11 @@ function Initialize-ArtemisDownloads {
|
|||||||
# Official scrcpy v4.1 win64 release digest. Verify every mirror/override equally.
|
# Official scrcpy v4.1 win64 release digest. Verify every mirror/override equally.
|
||||||
$values.scrcpy_sha256 = '5b12172b3264b2889f4583ee64752ce832e29bc8b1089dca81093459697165db'
|
$values.scrcpy_sha256 = '5b12172b3264b2889f4583ee64752ce832e29bc8b1089dca81093459697165db'
|
||||||
$script:ArtemisDownloads = [pscustomobject]$values
|
$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 {
|
function Install-ArtemisUvFromIndex {
|
||||||
|
|||||||
+111
-11
@@ -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
|
import argparse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from urllib.parse import urlsplit
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
@@ -83,6 +86,9 @@ EXCLUDED_SUFFIXES = {
|
|||||||
".swo",
|
".swo",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GIT_METADATA_FILES = {"HEAD", "config", "index", "shallow", "packed-refs"}
|
||||||
|
GIT_METADATA_DIRS = {"objects", "refs"}
|
||||||
|
|
||||||
|
|
||||||
def excluded(path: Path) -> bool:
|
def excluded(path: Path) -> bool:
|
||||||
parts = tuple(part.lower() for part in path.parts)
|
parts = tuple(part.lower() for part in path.parts)
|
||||||
@@ -116,22 +122,116 @@ def source_files(root: Path):
|
|||||||
yield 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]:
|
def package_source(root: Path, output: Path) -> tuple[int, str]:
|
||||||
root = root.resolve()
|
root = root.resolve()
|
||||||
output = output.resolve()
|
output = output.resolve()
|
||||||
|
branch, remote_url = release_remote(root)
|
||||||
|
revision = git(root, "rev-parse", "HEAD")
|
||||||
output.parent.mkdir(parents=True, exist_ok=True)
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
temporary = output.with_suffix(".zip.tmp")
|
temporary = output.with_suffix(".zip.tmp")
|
||||||
files = [p for p in source_files(root) if p.resolve() not in {output, temporary}]
|
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
with tempfile.TemporaryDirectory(prefix="artemis-release-") as work:
|
||||||
for path in files:
|
checkout = Path(work) / "artemis"
|
||||||
archive.write(path, Path("artemis") / path.relative_to(root))
|
git(
|
||||||
with zipfile.ZipFile(temporary) as archive:
|
root,
|
||||||
bad_file = archive.testzip()
|
"clone",
|
||||||
if bad_file:
|
"--depth",
|
||||||
raise RuntimeError(f"ZIP integrity check failed: {bad_file}")
|
"1",
|
||||||
if any(excluded(Path(name).relative_to("artemis")) for name in archive.namelist()):
|
"--single-branch",
|
||||||
raise RuntimeError("An excluded file was found in the source archive")
|
"--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)
|
temporary.replace(output)
|
||||||
finally:
|
finally:
|
||||||
temporary.unlink(missing_ok=True)
|
temporary.unlink(missing_ok=True)
|
||||||
|
|||||||
+44
-38
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2026 Google LLC
|
# Copyright 2026 Google LLC
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -24,6 +24,12 @@ param(
|
|||||||
$ErrorActionPreference = "Continue"
|
$ErrorActionPreference = "Continue"
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[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)
|
# Ensure modern TLS protocols are enabled for downloads (GitHub releases, nodejs.org, Google repositories)
|
||||||
try {
|
try {
|
||||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
|
[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
|
exit $LASTEXITCODE
|
||||||
}
|
}
|
||||||
if ($updateExit -ne 0) {
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,12 +64,12 @@ if ((-not $SkipUpdate) -and (Test-Path -LiteralPath $existingPython)) {
|
|||||||
try {
|
try {
|
||||||
Initialize-ArtemisDownloads -ProjectRoot $RootDir -Mode $Mirror
|
Initialize-ArtemisDownloads -ProjectRoot $RootDir -Mode $Mirror
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host " [ERROR] Invalid download settings: $_" -ForegroundColor Red
|
Write-Host (Get-LaunchText " [ERROR] Invalid download settings: $_" " [错误] 下载配置无效:$_") -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "======================================================" -ForegroundColor Cyan
|
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 "======================================================" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
@@ -181,7 +187,7 @@ function Install-PortablePlatformTools {
|
|||||||
$env:PATH = "$ptDir;$env:PATH"
|
$env:PATH = "$ptDir;$env:PATH"
|
||||||
return $true
|
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 {
|
try {
|
||||||
$zipPath = "$env:TEMP\platform-tools-windows.zip"
|
$zipPath = "$env:TEMP\platform-tools-windows.zip"
|
||||||
if (Invoke-DownloadFile -Uri $ArtemisDownloads.adb_url -OutFile $zipPath) {
|
if (Invoke-DownloadFile -Uri $ArtemisDownloads.adb_url -OutFile $zipPath) {
|
||||||
@@ -190,12 +196,12 @@ function Install-PortablePlatformTools {
|
|||||||
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
||||||
if (Test-Path "$ptDir\adb.exe") {
|
if (Test-Path "$ptDir\adb.exe") {
|
||||||
$env:PATH = "$ptDir;$env:PATH"
|
$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
|
return $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} 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
|
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/')) {
|
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
|
return $false
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -244,12 +250,12 @@ function Install-PortableScrcpy {
|
|||||||
Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
if (Test-Path "$scrcpyDir\scrcpy.exe") {
|
if (Test-Path "$scrcpyDir\scrcpy.exe") {
|
||||||
$env:PATH = "$scrcpyDir;$env:PATH"
|
$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
|
return $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} 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
|
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 {
|
try {
|
||||||
$nodeVer = "v22.23.2"
|
$nodeVer = "v22.23.2"
|
||||||
$arch = if ([System.Environment]::Is64BitOperatingSystem) {
|
$arch = if ([System.Environment]::Is64BitOperatingSystem) {
|
||||||
@@ -319,12 +325,12 @@ function Install-PortableNode {
|
|||||||
Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
if (Test-Path "$nodeDir\node.exe") {
|
if (Test-Path "$nodeDir\node.exe") {
|
||||||
$env:PATH = "$nodeDir;$env:PATH"
|
$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
|
return $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} 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
|
return $false
|
||||||
}
|
}
|
||||||
@@ -344,16 +350,16 @@ try {
|
|||||||
if (-not (Test-Path -LiteralPath (Join-Path $RootDir '.env'))) {
|
if (-not (Test-Path -LiteralPath (Join-Path $RootDir '.env'))) {
|
||||||
if (Test-Path -LiteralPath (Join-Path $RootDir '.env.example')) {
|
if (Test-Path -LiteralPath (Join-Path $RootDir '.env.example')) {
|
||||||
Copy-Item -LiteralPath (Join-Path $RootDir '.env.example') -Destination (Join-Path $RootDir '.env') -ErrorAction Stop
|
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
|
& $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') mcp
|
||||||
if ($LASTEXITCODE -ne 0) {
|
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) {
|
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
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,8 +377,8 @@ if ($missingCore.Count -gt 0) {
|
|||||||
$useWinGet = $false
|
$useWinGet = $false
|
||||||
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "winget")) {
|
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "winget")) {
|
||||||
if ([Console]::IsInputRedirected -eq $false) {
|
if ([Console]::IsInputRedirected -eq $false) {
|
||||||
Write-Host " [INFO] Missing components detected: $($missingCore -join ', ')." -ForegroundColor Cyan
|
Write-Host (Get-LaunchText " [INFO] Missing components detected: $($missingCore -join ', ')." " [检查] 缺少组件:$($missingCore -join ', ')。") -ForegroundColor Cyan
|
||||||
$ans = Read-Host " Install system packages via WinGet? [Y/n]"
|
$ans = Read-Host (Get-LaunchText " Install system packages via WinGet? [Y/n]" " 通过 WinGet 安装?[Y/n]")
|
||||||
if ($ans -eq "" -or $ans -match "^[Yy]") {
|
if ($ans -eq "" -or $ans -match "^[Yy]") {
|
||||||
$useWinGet = $true
|
$useWinGet = $true
|
||||||
}
|
}
|
||||||
@@ -382,7 +388,7 @@ if ($missingCore.Count -gt 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($useWinGet) {
|
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") {
|
if ($missingCore -contains "adb") {
|
||||||
winget install --id Google.PlatformTools -e --accept-source-agreements --accept-package-agreements --silent 2>$null | Out-Null
|
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 (Test-CommandExists "scrcpy")) {
|
||||||
if (-not (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
|
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"
|
$ShowcaseIndexAlt2 = "$RootDir\apps\showcase_ui\dist\index.html"
|
||||||
& $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') frontend-check
|
& $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') frontend-check
|
||||||
$frontendChanged = $LASTEXITCODE -eq 10
|
$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 ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1)) -and (-not (Test-Path $ShowcaseIndexAlt2)))) {
|
||||||
if (-not (Test-NodeCompatible)) {
|
if (-not (Test-NodeCompatible)) {
|
||||||
if (Test-CommandExists "node") {
|
if (Test-CommandExists "node") {
|
||||||
$curVer = (& node -v 2>$null)
|
$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 {
|
} 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
|
# 1. Try nvm if available on Windows
|
||||||
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "nvm")) {
|
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 install 22.23.2 | Out-Null
|
||||||
& nvm use 22.23.2 | Out-Null
|
& nvm use 22.23.2 | Out-Null
|
||||||
Update-EnvironmentPath
|
Update-EnvironmentPath
|
||||||
@@ -438,8 +444,8 @@ if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Pat
|
|||||||
$useWinGetNode = $false
|
$useWinGetNode = $false
|
||||||
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "winget")) {
|
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "winget")) {
|
||||||
if ([Console]::IsInputRedirected -eq $false) {
|
if ([Console]::IsInputRedirected -eq $false) {
|
||||||
Write-Host " [INFO] Node.js >= v22.22.0 required for Showcase UI." -ForegroundColor Cyan
|
Write-Host (Get-LaunchText " [INFO] Node.js >= v22.22.0 required for Showcase UI." " [检查] 网页需要 Node.js v22.22.0 或更新版本。") -ForegroundColor Cyan
|
||||||
$ans = Read-Host " Install/Upgrade Node.js via WinGet (may require Administrator)? [Y/n]"
|
$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]") {
|
if ($ans -eq "" -or $ans -match "^[Yy]") {
|
||||||
$useWinGetNode = $true
|
$useWinGetNode = $true
|
||||||
}
|
}
|
||||||
@@ -449,7 +455,7 @@ if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Pat
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($useWinGetNode) {
|
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
|
winget install --id OpenJS.NodeJS.LTS -e --accept-source-agreements --accept-package-agreements --silent 2>$null | Out-Null
|
||||||
Update-EnvironmentPath
|
Update-EnvironmentPath
|
||||||
}
|
}
|
||||||
@@ -463,36 +469,36 @@ if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Pat
|
|||||||
|
|
||||||
if (Test-NodeCompatible) {
|
if (Test-NodeCompatible) {
|
||||||
$nodeVer = (& node -v 2>$null)
|
$nodeVer = (& node -v 2>$null)
|
||||||
Write-Host " [OK] Node.js $nodeVer is ready." -ForegroundColor Green
|
Write-Host (Get-LaunchText " [OK] Node.js $nodeVer is ready." " [完成] Node.js $nodeVer 已就绪。") -ForegroundColor Green
|
||||||
Write-Host " [INFO] Showcase UI sources/dependencies changed or build missing. Compiling..." -ForegroundColor Yellow
|
Write-Host (Get-LaunchText " [INFO] Showcase UI sources/dependencies changed or build missing. Compiling..." " [构建] 前端源码/依赖有变化或缺少构建结果,准备重新构建...") -ForegroundColor Yellow
|
||||||
Push-Location "$RootDir\apps\showcase_ui"
|
Push-Location "$RootDir\apps\showcase_ui"
|
||||||
try {
|
try {
|
||||||
$npmExec = if (Test-CommandExists "npm.cmd") { "npm.cmd" } else { "npm" }
|
$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
|
& $npmExec ci --no-audit --no-fund --loglevel=error
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "npm ci failed (exit $LASTEXITCODE). Check the network and retry."
|
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
|
& $npmExec run build
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Frontend build failed (exit $LASTEXITCODE)." }
|
if ($LASTEXITCODE -ne 0) { throw "Frontend build failed (exit $LASTEXITCODE)." }
|
||||||
if (Test-Path $ShowcaseIndex) {
|
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)) {
|
} 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 {
|
} else {
|
||||||
throw 'Frontend build did not produce index.html.'
|
throw 'Frontend build did not produce index.html.'
|
||||||
}
|
}
|
||||||
& $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') frontend-mark
|
& $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') frontend-mark
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'Could not save frontend build fingerprint.' }
|
if ($LASTEXITCODE -ne 0) { throw 'Could not save frontend build fingerprint.' }
|
||||||
} catch {
|
} 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
|
exit 1
|
||||||
} finally {
|
} finally {
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,7 +511,7 @@ Start-LocalAdbServer
|
|||||||
|
|
||||||
# Launch via `python -m artemis` (not the `artemis.exe` console-script shim) so the
|
# 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.
|
# 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) {
|
if ($NoOpen) {
|
||||||
& $ArtemisPython -m artemis ui --port $Port --no-open
|
& $ArtemisPython -m artemis ui --port $Port --no-open
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ from urllib.parse import urlsplit
|
|||||||
ROOT = Path(__file__).resolve().parents[1]
|
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:
|
def git(root: Path, *args: str, timeout: int = 15) -> str:
|
||||||
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GCM_INTERACTIVE": "Never"}
|
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GCM_INTERACTIVE": "Never"}
|
||||||
result = subprocess.run(
|
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"))
|
or (status.get("status") == "running" and status.get("pid"))
|
||||||
):
|
):
|
||||||
print(
|
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
|
return 20
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
pass
|
pass
|
||||||
if not shutil.which("git"):
|
if not shutil.which("git"):
|
||||||
print(
|
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
|
return 0
|
||||||
if not (root / ".git").exists():
|
if not (root / ".git").exists():
|
||||||
print(
|
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
|
return 0
|
||||||
try:
|
try:
|
||||||
@@ -79,32 +92,49 @@ def update_source(root: Path, port: int = 8000) -> int:
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Use a clean Git remote URL and a credential manager, not embedded credentials."
|
"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)
|
git(root, "fetch", "--no-tags", remote, merge_ref, timeout=45)
|
||||||
target = git(root, "rev-parse", "FETCH_HEAD")
|
target = git(root, "rev-parse", "FETCH_HEAD")
|
||||||
current = git(root, "rev-parse", "HEAD")
|
current = git(root, "rev-parse", "HEAD")
|
||||||
if current == target:
|
if current == target:
|
||||||
print("[OK] Source is up to date.")
|
print(launch_text("[OK] Source is up to date.", "[完成] 源码已是最新版。"))
|
||||||
return 0
|
return 0
|
||||||
if git(root, "status", "--porcelain"):
|
if git(root, "status", "--porcelain"):
|
||||||
print(
|
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
|
return 0
|
||||||
ancestor = git(root, "merge-base", "HEAD", target)
|
ancestor = git(root, "merge-base", "HEAD", target)
|
||||||
if ancestor != current:
|
if ancestor != current:
|
||||||
print(
|
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
|
return 0
|
||||||
git(root, "merge", "--ff-only", target, timeout=30)
|
git(root, "merge", "--ff-only", target, timeout=30)
|
||||||
print(
|
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
|
return 10
|
||||||
except (RuntimeError, subprocess.TimeoutExpired) as exc:
|
except (RuntimeError, subprocess.TimeoutExpired) as exc:
|
||||||
print(
|
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
|
return 0
|
||||||
|
|
||||||
@@ -275,14 +305,32 @@ def refresh_mcp(root: Path) -> None:
|
|||||||
else refresh_codex(path, root, sys.executable, ARTEMIS_MCP_TOOLS)
|
else refresh_codex(path, root, sys.executable, ARTEMIS_MCP_TOOLS)
|
||||||
)
|
)
|
||||||
if updated:
|
if updated:
|
||||||
print(f"[UPDATED] Existing MCP configuration: {path}")
|
print(
|
||||||
|
launch_text(
|
||||||
|
f"[UPDATED] Existing MCP configuration: {path}",
|
||||||
|
f"[已更新] 已有 MCP 配置:{path}",
|
||||||
|
)
|
||||||
|
)
|
||||||
changed += 1
|
changed += 1
|
||||||
except (OSError, ValueError, TypeError, AttributeError):
|
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(
|
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 <client>",
|
||||||
|
"[提示] 新客户端可执行:uv run artemis mcp --install <client>",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
print("[TIP] For a new client run: uv run artemis mcp --install <client>")
|
|
||||||
|
|
||||||
|
|
||||||
def frontend_fingerprint(root: Path) -> str:
|
def frontend_fingerprint(root: Path) -> str:
|
||||||
|
|||||||
+29
-3
@@ -1,6 +1,32 @@
|
|||||||
@echo off
|
@echo off
|
||||||
rem Use China mirrors for first setup and subsequent launches.
|
chcp 65001 >nul 2>&1
|
||||||
rem All start.bat arguments, including -SetupOnly, are supported.
|
|
||||||
setlocal DisableDelayedExpansion
|
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 %*
|
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
|
||||||
@@ -15,12 +15,12 @@ rem limitations under the License.
|
|||||||
|
|
||||||
chcp 65001 >nul 2>&1
|
chcp 65001 >nul 2>&1
|
||||||
setlocal DisableDelayedExpansion
|
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
|
pushd "%~dp0" || exit /b 1
|
||||||
|
|
||||||
echo ======================================================
|
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 ======================================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ set "ARTEMIS_EXIT_CODE=%ERRORLEVEL%"
|
|||||||
|
|
||||||
if not "%ARTEMIS_EXIT_CODE%"=="0" (
|
if not "%ARTEMIS_EXIT_CODE%"=="0" (
|
||||||
echo.
|
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
|
if not defined ARTEMIS_NO_PAUSE pause
|
||||||
)
|
)
|
||||||
popd
|
popd
|
||||||
|
|||||||
@@ -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"
|
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(
|
@pytest.mark.parametrize(
|
||||||
"url",
|
"url",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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
|
BOM-less scripts are decoded using the system ANSI code page. UTF-8 symbols
|
||||||
can become quote delimiters under cp1252 and cause parse errors.
|
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)
|
@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()
|
raw = script.read_bytes()
|
||||||
offending = []
|
if any(byte > 0x7F for byte in raw):
|
||||||
line_no = 1
|
assert raw.startswith(b"\xef\xbb\xbf"), (
|
||||||
for index, byte in enumerate(raw):
|
f"{script.name} contains non-ASCII bytes without a UTF-8 BOM; "
|
||||||
if byte == 0x0A:
|
"Windows PowerShell 5.1 would use the system ANSI code page."
|
||||||
line_no += 1
|
)
|
||||||
elif byte > 0x7F:
|
raw.decode("utf-8-sig")
|
||||||
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."
|
|
||||||
)
|
|
||||||
@@ -1,36 +1,134 @@
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import zipfile
|
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"
|
SCRIPT = Path(__file__).resolve().parents[2] / "scripts/package_source.py"
|
||||||
spec = importlib.util.spec_from_file_location("package_source", script)
|
spec = importlib.util.spec_from_file_location("package_source", SCRIPT)
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
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"]
|
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 = [
|
omit = [
|
||||||
".env",
|
".env",
|
||||||
".env.local",
|
".env.local",
|
||||||
".venv/pyvenv.cfg",
|
".venv/pyvenv.cfg",
|
||||||
"nested/node_modules/foo.js",
|
"nested/node_modules/foo.js",
|
||||||
"apps/showcase_ui/dist/main.js",
|
"apps/showcase_ui/dist/main.js",
|
||||||
"artemis/resources/showcase_ui/main.js",
|
|
||||||
"__pycache__/a.pyc",
|
"__pycache__/a.pyc",
|
||||||
"traces/data.db",
|
"traces/data.db",
|
||||||
"helper.apk",
|
|
||||||
"debug.keystore",
|
|
||||||
".git/config",
|
|
||||||
".roo/mcp.json",
|
".roo/mcp.json",
|
||||||
]
|
]
|
||||||
for name in keep + omit:
|
for name in keep + tracked_builds + omit:
|
||||||
target = tmp_path / name
|
target = root / name
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
target.write_text("fixture")
|
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"
|
output = tmp_path / "artifacts/source.zip"
|
||||||
count, digest = module.package_source(tmp_path, output)
|
count, digest = module.package_source(root, output)
|
||||||
assert count == len(keep)
|
assert count > len(keep)
|
||||||
assert len(digest) == 64
|
assert len(digest) == 64
|
||||||
with zipfile.ZipFile(output) as archive:
|
with zipfile.ZipFile(output) as archive:
|
||||||
assert archive.testzip() is None
|
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")
|
||||||
Reference in new issue
Block a user