From 2ddec53954ee1d3b51024b8535fb73cd014bd4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E9=93=AD=E8=BD=A9?= Date: Tue, 22 Sep 2026 19:16:17 +0800 Subject: [PATCH] feat: add Chinese UI, Responses models, and mirrored Windows setup Localize the web console and persist custom model and capability settings. Use OpenAI Responses exclusively and move runtime limits into environment configuration. Bootstrap uv and the virtual environment on Windows, support optional China mirrors, and package source distributions without environments, build output, or credentials. Update regression tests and repository quality checks for the new behavior. --- .env.example | 49 +- .gitignore | 1 + README.md | 10 +- README_CN.md | 67 +- apps/admin_console/replay_manager.py | 143 +- apps/admin_console/routers/system.py | 108 +- apps/admin_console/services/model_service.py | 8 +- .../services/model_settings_service.py | 161 +++ apps/showcase_ui/src/app/app.config.ts | 6 +- .../agent-stream/agent-stream-startup.spec.ts | 10 +- .../agent-stream/agent-stream.component.html | 216 ++-- .../agent-stream/agent-stream.component.ts | 145 +-- .../chat-interface.component.html | 39 +- .../chat-interface.component.ts | 15 +- .../floating-video-player.component.html | 76 +- .../legacy-agent-stream.component.html | 26 +- .../capability-settings.component.ts | 54 + .../model-settings.component.spec.ts | 73 ++ .../model-settings.component.ts | 108 ++ .../nav-switcher/nav-switcher.component.ts | 22 +- .../src/app/core/data/smart-tasks.data.ts | 208 +-- .../src/app/core/models/pro-tuning.model.ts | 96 +- .../src/app/core/models/stream.model.ts | 3 +- .../src/app/pages/home/home.component.html | 700 +++------- .../src/app/pages/home/home.component.ts | 348 +---- .../pages/workspace/workspace.component.html | 16 +- .../pages/workspace/workspace.component.ts | 8 +- .../src/app/services/agent.service.spec.ts | 6 +- .../src/app/services/agent.service.ts | 40 +- .../src/app/services/system.service.ts | 11 +- .../app/utils/action-formatter.util.spec.ts | 16 +- .../src/app/utils/action-formatter.util.ts | 62 +- .../src/app/utils/image-overlay.util.ts | 32 +- .../src/app/utils/markdown-parser.util.ts | 2 +- .../src/app/utils/run-info.util.spec.ts | 8 +- .../app/utils/stream-aggregator.util.spec.ts | 11 +- .../src/app/utils/stream-aggregator.util.ts | 20 +- .../src/app/utils/tool-formatter.util.spec.ts | 18 +- .../src/app/utils/tool-formatter.util.ts | 176 +-- .../src/app/utils/ui-label.pipe.ts | 17 + apps/showcase_ui/src/index.html | 2 +- artemis/agents/checker/checker.json | 2 +- artemis/agents/diagnoser/diagnoser.json | 2 +- artemis/agents/diagnoser/diagnoser.py | 6 +- artemis/agents/explorer/constants.py | 4 +- artemis/agents/explorer/explorer.json | 2 +- artemis/agents/explorer/explorer.py | 126 +- artemis/agents/explorer/native_runner.py | 878 ------------- artemis/agents/explorer/tool_declarations.py | 216 +--- artemis/agents/explorer/universal_runner.py | 5 +- artemis/agents/flash/flash_runner.md | 8 + artemis/agents/flash/flash_summarizer.md | 12 +- .../agents/flash/flash_summarizer_single.md | 12 +- artemis/agents/flash/runner.py | 10 +- artemis/agents/flash/summarizer.py | 11 +- .../history_analyzer/history_analyzer.py | 4 +- artemis/agents/log_analyzer/log_analyzer.py | 4 +- .../agents/log_analyzer/output_analyzer.py | 4 +- artemis/agents/operator/operator.json | 2 +- artemis/agents/operator/operator.py | 2 +- artemis/agents/outputter/outputter.md | 6 + artemis/agents/outputter/outputter.py | 4 +- artemis/agents/planner/planner.json | 4 +- artemis/agents/validator/tool_declarations.py | 39 - artemis/agents/video_analyzer/__init__.py | 2 +- .../{audio_native.py => audio_runner.py} | 274 +--- .../agents/video_analyzer/chunk_agentic.py | 455 ------- .../video_analyzer/chunk_conversation.py | 491 ------- .../{chunk_native.py => chunk_runner.py} | 155 +-- artemis/agents/video_analyzer/gemini_files.py | 162 --- .../video_analyzer/native_coordinator.py | 541 -------- .../agents/video_analyzer/universal_engine.py | 6 +- .../agents/video_analyzer/universal_tools.py | 154 +-- .../agents/video_analyzer/video_analyzer.py | 150 +-- artemis/config/__init__.py | 14 - artemis/config/agent.py | 52 +- artemis/config/constants.py | 12 +- artemis/config/core/engine.py | 2 +- artemis/config/llm.py | 65 +- artemis/config/settings.py | 92 +- artemis/constants.py | 25 - .../diagnostics/probes/credentials_probe.py | 208 +-- .../core/diagnostics/probes/runtime_probe.py | 8 +- .../diagnostics/probes/toolchain_probe.py | 4 +- artemis/core/diagnostics/readiness.py | 2 +- artemis/core/diagnostics/schema.py | 2 +- artemis/interfaces/cli/commands/init.py | 18 +- artemis/interfaces/cli/commands/run.py | 2 +- artemis/llm/google/__init__.py | 57 - artemis/llm/google/provider.py | 185 --- artemis/llm/router.py | 366 +----- artemis/llm/{google => }/usage.py | 0 artemis/memory/chunking.py | 16 +- artemis/resources/config/artemis.jsonc | 253 +--- artemis/resources/showcase_ui/index.html | 8 +- .../resources/showcase_ui/main-BGAFACUZ.js | 18 - .../resources/showcase_ui/main-WGGFA77O.js | 157 +++ ...tyles-CDJM3XQS.css => styles-SCTCINHC.css} | 2 +- artemis/sdk/agent.py | 62 +- .../examples/video_transcription_example.py | 29 +- artemis/services/llm.py | 218 +--- artemis/services/token_meter.py | 4 +- artemis/tools/base.py | 40 - artemis/tools/video_tool.py | 6 +- artemis/utils/credentials_validator.py | 92 +- config/artemis.jsonc | 253 +--- config/downloads.json | 20 + mcp_server/tools/diagnose.py | 42 +- .../backend_manager/app/auth/jwt_handler.py | 8 +- .../backend_manager/app/auth/otp_service.py | 6 +- .../app/services/bigquery_service.py | 2 +- .../app/services/docker_service.py | 2 +- .../app/services/session_manager.py | 8 +- pyproject.toml | 4 - pyright-core.json | 1 - scripts/bootstrap_python.ps1 | 85 ++ scripts/download_config.ps1 | 103 ++ scripts/package_source.py | 154 +++ scripts/start.ps1 | 108 +- start-cn.bat | 6 + start.bat | 15 +- tests/tools/diagnoser/test_diagnoser_tools.py | 14 +- .../test_video_analyzer_tools.py | 38 +- .../unit/admin_console/test_model_service.py | 4 +- .../unit/admin_console/test_model_settings.py | 108 ++ .../admin_console/test_security_boundary.py | 2 +- .../admin_console/test_step_repository.py | 10 +- tests/unit/agents/test_explorer.py | 1150 ----------------- tests/unit/agents/test_explorer_prepass.py | 28 - tests/unit/agents/test_explorer_tiers.py | 211 +-- tests/unit/agents/test_explorer_universal.py | 4 +- tests/unit/agents/test_operator_transcript.py | 6 +- tests/unit/agents/test_video_agentic.py | 1086 ---------------- tests/unit/agents/test_video_analyzer.py | 616 --------- .../agents/test_video_analyzer_universal.py | 4 +- tests/unit/agents/test_video_audio_native.py | 157 --- tests/unit/agents/test_video_blackboard.py | 6 +- tests/unit/agents/test_video_reliability.py | 136 +- .../config/test_video_processing_config.py | 39 - tests/unit/core/test_diagnostics.py | 22 +- tests/unit/core/test_readiness.py | 2 +- tests/unit/mcp/test_diagnose_tool.py | 77 +- tests/unit/memory/test_history_chunking.py | 10 +- tests/unit/test_cli.py | 12 +- tests/unit/test_config_package.py | 14 +- tests/unit/test_credentials_validator.py | 24 +- tests/unit/test_download_mirrors.py | 160 +++ tests/unit/test_gemini_role.py | 79 -- tests/unit/test_google_provider.py | 146 --- tests/unit/test_google_usage.py | 2 +- tests/unit/test_llm_grounding.py | 164 --- tests/unit/test_llm_pause.py | 83 +- tests/unit/test_openai_responses.py | 215 +++ tests/unit/test_planner.py | 5 +- tests/unit/test_source_archive.py | 35 + tests/unit/test_windows_bootstrap.py | 148 +++ tests/unit/tools/history/test_declarations.py | 6 +- .../unit/tools/test_append_note_pure_tool.py | 6 +- tests/unit/tools/test_append_note_tool.py | 6 +- tests/unit/tools/test_command_tool.py | 18 +- tests/unit/tools/test_committee_tool.py | 4 +- tests/unit/tools/test_explorer_tool.py | 6 +- tests/unit/tools/test_image_processor_tool.py | 6 +- tests/unit/tools/test_launch_app_tool.py | 4 +- tests/unit/tools/test_list_notes_pure_tool.py | 2 +- tests/unit/tools/test_list_notes_tool.py | 2 +- tests/unit/tools/test_log_tool.py | 4 +- .../unit/tools/test_object_detection_tool.py | 10 +- tests/unit/tools/test_ocr_tool.py | 2 +- tests/unit/tools/test_read_hierarchy_tool.py | 2 +- tests/unit/tools/test_read_logs_tool.py | 8 +- tests/unit/tools/test_read_note_pure_tool.py | 8 +- tests/unit/tools/test_read_note_tool.py | 8 +- tests/unit/tools/test_save_note_pure_tool.py | 6 +- tests/unit/tools/test_save_note_tool.py | 6 +- tests/unit/tools/test_search_logs.py | 14 +- .../unit/tools/test_update_note_pure_tool.py | 8 +- tests/unit/tools/test_update_note_tool.py | 8 +- .../tools/test_video_analyzer_pure_tool.py | 6 +- tests/unit/tools/test_video_analyzer_tool.py | 6 +- tests/unit/tools/test_wait_tool.py | 4 +- tests/unit/utils/test_optional_ocr.py | 2 +- uv.lock | 579 --------- 183 files changed, 3427 insertions(+), 11857 deletions(-) create mode 100644 apps/admin_console/services/model_settings_service.py create mode 100644 apps/showcase_ui/src/app/components/model-settings/capability-settings.component.ts create mode 100644 apps/showcase_ui/src/app/components/model-settings/model-settings.component.spec.ts create mode 100644 apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts create mode 100644 apps/showcase_ui/src/app/utils/ui-label.pipe.ts delete mode 100644 artemis/agents/explorer/native_runner.py rename artemis/agents/video_analyzer/{audio_native.py => audio_runner.py} (57%) delete mode 100644 artemis/agents/video_analyzer/chunk_agentic.py delete mode 100644 artemis/agents/video_analyzer/chunk_conversation.py rename artemis/agents/video_analyzer/{chunk_native.py => chunk_runner.py} (74%) delete mode 100644 artemis/agents/video_analyzer/gemini_files.py delete mode 100644 artemis/agents/video_analyzer/native_coordinator.py delete mode 100644 artemis/llm/google/__init__.py delete mode 100644 artemis/llm/google/provider.py rename artemis/llm/{google => }/usage.py (100%) delete mode 100644 artemis/resources/showcase_ui/main-BGAFACUZ.js create mode 100644 artemis/resources/showcase_ui/main-WGGFA77O.js rename artemis/resources/showcase_ui/{styles-CDJM3XQS.css => styles-SCTCINHC.css} (91%) create mode 100644 config/downloads.json create mode 100644 scripts/bootstrap_python.ps1 create mode 100644 scripts/download_config.ps1 create mode 100644 scripts/package_source.py create mode 100644 start-cn.bat create mode 100644 tests/unit/admin_console/test_model_settings.py delete mode 100644 tests/unit/agents/test_video_agentic.py delete mode 100644 tests/unit/agents/test_video_analyzer.py delete mode 100644 tests/unit/agents/test_video_audio_native.py delete mode 100644 tests/unit/config/test_video_processing_config.py create mode 100644 tests/unit/test_download_mirrors.py delete mode 100644 tests/unit/test_gemini_role.py delete mode 100644 tests/unit/test_google_provider.py delete mode 100644 tests/unit/test_llm_grounding.py create mode 100644 tests/unit/test_openai_responses.py create mode 100644 tests/unit/test_source_archive.py create mode 100644 tests/unit/test_windows_bootstrap.py diff --git a/.env.example b/.env.example index 8b23d0a..b6ffe89 100644 --- a/.env.example +++ b/.env.example @@ -1,32 +1,39 @@ -# ============================================================================== -# Artemis Environment Configuration Template -# ============================================================================== -# Copy this file to .env and fill in your API credentials: -# cp .env.example .env - -# LLM Providers (Configure at least one) -GEMINI_API_KEY= -GOOGLE_API_KEY= +# 复制为 .env,或在首页「模型配置」「功能开关」中填写并保存。 +# 所有模型调用仅使用 OpenAI Responses API;环境变量优先于 .env。 OPENAI_API_KEY= -ANTHROPIC_API_KEY= -OPEN_ROUTER_API_KEY= -XAI_API_KEY= +# 接口根地址:通常以 /v1 结尾,不要添加 /responses 或 /chat/completions。 +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-5-mini +# 单次模型 HTTP 请求超时(秒) +OPENAI_TIMEOUT_SECONDS=120 +# SDK 内部重试次数;默认 0,由 Artemis 的分类恢复机制处理重试。 +OPENAI_MAX_RETRIES=0 -# Google Cloud Vision OCR (Optional - for advanced OCR processing) +# 功能开关:保存后对新任务生效 +ARTEMIS_VIDEO_ENABLED=true +ARTEMIS_OUTPUTTER_ENABLED=true +ARTEMIS_STEP_SUMMARIZER_ENABLED=true + +# 视频分段、重试与单次分析超时(秒) +ARTEMIS_VIDEO_CHUNK_SECONDS=60 +ARTEMIS_VIDEO_TIMEOUT_SECONDS=120 +ARTEMIS_VIDEO_MAX_RETRIES=1 +# 检查点、退出汇总与凭据验证超时(秒) +ARTEMIS_CHECKPOINT_TIMEOUT_SECONDS=180 +ARTEMIS_SETTLEMENT_TIMEOUT_SECONDS=120 +ARTEMIS_CREDENTIAL_TIMEOUT_SECONDS=12 +ARTEMIS_MODEL_CACHE_TTL_SECONDS=10 +# 模型重试耗尽后的暂停等待上限(秒);0 表示无限等待 +LLM_PAUSE_TIMEOUT_SECONDS=900 + +# 可选 Google Cloud Vision OCR,独立于模型调用,不配置也可运行。 OCR_API_KEY= -# Android ADB Connectivity (Optional - defaults to 127.0.0.1:5037) +# Android ADB 连接 ADB_HOST=127.0.0.1 ADB_PORT=5037 # ADB_DEVICE_SERIAL= -# Keep the display awake, falling back to a wake lock or safe heartbeat when needed. -# Set to false if Artemis is used with a personal device whose screen should time out. ARTEMIS_KEEP_DEVICE_AWAKE=true -# UI hierarchy backend: auto (Accessibility Helper, UIAutomator2 fallback) | helper | uiautomator ARTEMIS_HIERARCHY_BACKEND=auto -# Let tasks install / upgrade the Accessibility Helper APK on a device they hold. -# Set to false on shared or personal phones: then only `artemis helper install` installs it. ARTEMIS_HELPER_AUTO_INSTALL=true - -# Artemis Execution Defaults (flash or pro) ARTEMIS_DEFAULT_PROFILE=flash diff --git a/.gitignore b/.gitignore index 31cf3e9..5cda9f7 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,7 @@ live_screenshot*.png *.key # OS Artifacts +artifacts/ .DS_Store .DS_Store? ._* diff --git a/README.md b/README.md index d188360..f6216b3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Python 3.12+ License: Apache-2.0 MCP Native - Multi-Model + OpenAI Responses AndroidWorld SOTA

@@ -104,6 +104,14 @@ cd artemis > **Tip**: Opens `http://localhost:8000` in your default browser with a device connection wizard, live screen mirroring, prompt sandbox, and execution replays. You can also run directly from CLI: `uv run artemis run "Open Settings, find Battery and tell me current level" --profile flash`. +### Model and feature settings + +The web console now uses Simplified Chinese. Open **系统设置与运行准备 → 模型配置** to edit and save the Responses base URL, model, API key, request timeout, and retry count. **测试连接** tests the current form without saving; **保存配置** persists it to `.env`. Leave the key blank to retain the existing credential. Existing keys are never returned to the browser. + +All models use **OpenAI Responses**, including custom gateways. Set `OPENAI_API_KEY`, `OPENAI_BASE_URL` (the API root, usually ending in `/v1`), and `OPENAI_MODEL` (for example `mimo-v2.6-flash`). Gemini, Anthropic and Chat Completions transports are no longer supported. Video analysis sends sampled image frames and requires image input support. Structured output uses Responses function calling with local schema validation. + +The **功能开关** section persists video analysis/recording, final reports, and Flash step summaries. Changes apply to new tasks and survive restart. Environment variables take precedence over `.env`; explicit per-node or task overrides take precedence over global defaults. See [`.env.example`](.env.example) for timeout settings and the [Chinese setup guide](README_CN.md#网页模型配置与功能开关) for details. +
diff --git a/README_CN.md b/README_CN.md index 7c425c6..031c709 100644 --- a/README_CN.md +++ b/README_CN.md @@ -20,7 +20,7 @@ Python 3.12+ License: Apache-2.0 MCP Native - Multi-Model + OpenAI Responses AndroidWorld SOTA

@@ -104,8 +104,73 @@ cd artemis > PowerShell 默认不会从当前目录查找可执行脚本,因此必须使用 `.\start.bat`,且命令末尾不要添加 `\`。如果使用传统命令提示符(CMD),则运行 `start.bat`。 +首次启动会检测 uv:已有则复用,缺少则从 Astral 官方安装。随后检查项目 `.venv`,缺少时自动安装所需的 Python 3.12 并创建环境;已有且满足 Python 3.12+ 要求的环境直接复用。依赖与 `uv.lock` 一致时跳过安装,否则只同步缺失或变化的依赖,保留已有开发工具。安装失败会停止启动并显示原因,不会删除已有环境。 + +国内网络可直接双击 **`start-cn.bat`**,或在 PowerShell 中运行: + +```powershell +.\start-cn.bat +# 仅安装 Python 环境,也支持镜像模式 +.\start-cn.bat -SetupOnly +# 与上面的入口等价 +.\start.bat -Mirror China +# 本次使用官方源 +.\start.bat -Mirror Official +``` + +下载配置位于 `config/downloads.json`。将 `mode` 改成 `China` 后,普通 `start.bat` 也默认使用国内源;命令行 `-Mirror` 优先。`timeout_seconds` 控制下载超时(默认 180 秒,范围 10~3600 秒)。国内模式使用: + +| 组件 | 下载来源 | +| --- | --- | +| uv 与 Python 依赖 | 清华 PyPI 镜像;uv 直接从 wheel 提取并校验 SHA256,无需预装 Python | +| Python 运行时 | 南京大学 python-build-standalone 镜像 | +| Node.js 与 npm 包 | npmmirror | +| ADB | 腾讯 Android SDK 镜像 | +| FFmpeg | `imageio-ffmpeg` Python 包附带的程序 | +| scrcpy | 默认仍使用 GitHub 官方发布,可用下述配置替换 | + +镜像模式从 `uv.lock` 导出固定版本和哈希,再通过所选索引安装,避免锁文件中的官方直链绕过镜像;不会改写锁文件或升级已锁定版本。npm 使用本次启动进程的 registry 配置。所有设置仅影响当前启动进程,不修改用户全局 pip、uv、npm 或 nvm 配置。已有 `UV_DEFAULT_INDEX`(或 `UV_INDEX_URL`)、`UV_PYTHON_INSTALL_MIRROR`、`npm_config_registry` 环境变量优先。 + +需要公司内网源或替换不可用镜像时,在 `config/downloads.json` 的 `overrides` 中填写对应 HTTPS 地址;空字符串表示使用所选模式的默认值。`python_index` 是以 `/simple` 结尾的索引,`python_download_mirror` 是 Python 发布目录,`node_mirror` 是包含版本子目录的 Node.js 下载根路径,`adb_url` 和 `scrcpy_url` 是可直接下载的 Windows ZIP 地址。scrcpy 暂无内置国内镜像,GitHub 不可达时请配置可信的 `scrcpy_url` 或预先安装 scrcpy。`uv_installer_url` 只用于没有设置 Python 镜像时的 PowerShell 安装器路径。镜像缺包或下载失败会显示错误;修正地址后重试即可,不会静默切换到官方源。 + +镜像说明参考:[清华 PyPI](https://mirrors.tuna.tsinghua.edu.cn/help/pypi/)、[uv 下载镜像配置](https://docs.astral.sh/uv/reference/environment/#uv_python_install_mirror)。 + +也可先单独准备 Python 环境,不启动页面、不安装设备工具、不配置 MCP: + +```powershell +.\start.bat -SetupOnly +# 环境准备好后正常启动 +.\start.bat +# 可选:指定端口、不自动打开浏览器 +.\start.bat -Port 8001 -NoOpen +``` + +源码压缩包不携带 `.venv`、`node_modules`、前端编译结果或 APK。解压到可写目录后运行 `start.bat`,首次完整启动会安装所需 Node.js/npm 并构建网页,需要联网;已有构建时跳过。`.env` 不存在时才从 `.env.example` 创建,已有配置不会覆盖。Android 辅助 APK 的源码位于 `packages/artemis-accessibility-helper`,需要该功能时按该目录的构建脚本生成。 + +重新打源码包可运行 `uv run --no-sync python scripts/package_source.py`,输出到 `artifacts/`;脚本会排除本地密钥、运行数据、缓存、依赖目录与编译产物,并检查 ZIP 完整性。 + > **提示**:启动后将自动在默认浏览器中打开 Web 控制台(`http://localhost:8000`),提供设备连接向导、实时投屏、任务演练与状态回放面板。你也可以通过命令行直接运行:`uv run artemis run "打开系统设置,找到电池选项并告诉我当前电量" --profile flash`。 +### 网页模型配置与功能开关 + +Web 控制台使用简体中文。在首页打开「系统设置与运行准备」,可以修改接口地址、模型名称、API Key、请求超时和重试次数。点击「测试连接」会使用当前表单发送一次真实的 Responses 请求;点击「保存配置」才会写入工作目录的 `.env`。密钥留空会保留现有值,网页不会读取或显示已有密钥。 + +所有模型调用统一使用 **OpenAI Responses API**,不再提供 Gemini、Anthropic 或 Chat Completions 调用路径。自定义网关必须实现 `/responses`,接口地址填写根地址(通常以 `/v1` 结尾)。例如: + +```dotenv +OPENAI_API_KEY=你的密钥 +OPENAI_BASE_URL=https://你的网关/v1 +OPENAI_MODEL=mimo-v2.6-flash +OPENAI_TIMEOUT_SECONDS=120 +OPENAI_MAX_RETRIES=0 +``` + +「功能开关」提供视频分析与录屏、最终报告生成、Flash 步骤摘要压缩,可分别保存。设置对**新任务**生效,重启后保留。环境变量优先于 `.env`;在网页保存时会同时更新当前服务进程的配置,服务的启动环境若显式定义了同名变量,后续重启仍以启动环境为准。手动配置的节点模型或任务级选项优先于全局默认值。 + +视频分析通过抽取画面帧调用模型,因此需要支持图片输入的模型。`mimo-v2.6-flash` 已验证文本、图片、工具调用、流式输出及结构化函数调用;对于不支持 `json_schema` 的网关,结构化结果使用 Responses 函数调用并在本地校验。音频输入是否可用取决于端点能力,拒绝音频时视频分析会尝试仅分析画面。 + +视频分段时长、分析超时、检查点超时等配置及单位见 [`.env.example`](.env.example)。可选 Cloud Vision OCR 独立于模型调用,未配置 OCR 密钥也可运行。 +
diff --git a/apps/admin_console/replay_manager.py b/apps/admin_console/replay_manager.py index 7174d64..14f5b75 100644 --- a/apps/admin_console/replay_manager.py +++ b/apps/admin_console/replay_manager.py @@ -86,14 +86,14 @@ class ReplayManager: It reconstructs historical execution states (SQLite database records, trace histories, and screenshots) and replays a single agent step in an isolated, deterministic sandbox environment while interacting live with a physical - Android device and Gemini. + Android device and OpenAI Responses. Key Responsibilities: - Reconstructs and preloads historical SQLite states up to a specific step. - Establishes sandboxed `ArtemisContext` and `DataEngine` instances. - Integrates with physical devices via ADB and UIAutomator. - - Hooks and spies on the Gemini API to capture thoughts and raw outputs. + - Records Responses model calls to capture thoughts and raw outputs. - Reconciles live and preloaded traces into unified chronological trees. Attributes: @@ -1552,8 +1552,8 @@ class ReplayManager: # Update values conn_temp.execute( """ - UPDATE video_recordings - SET local_video_path = ?, start_time = ?, end_time = ? + UPDATE video_recordings + SET local_video_path = ?, start_time = ?, end_time = ? WHERE session_id = ? """, ( @@ -1583,8 +1583,8 @@ class ReplayManager: conn_temp.execute( """ - UPDATE video_recordings - SET local_video_path = ?, start_time = ?, end_time = ? + UPDATE video_recordings + SET local_video_path = ?, start_time = ?, end_time = ? WHERE session_id = ? """, ( @@ -1885,121 +1885,34 @@ class ReplayManager: data_engine = ctx.data_engine - print("Executing using Live Gemini Client...") + print("Executing using OpenAI Responses...") - from google.genai.models import AsyncModels + from langchain_openai import ChatOpenAI from unittest.mock import patch - original_generate_content = AsyncModels.generate_content + original_generate = ChatOpenAI._agenerate - async def wrapped_generate_content(self, *, model, contents, config=None, **kwargs): - system_instruction = None - if config and config.system_instruction: - if isinstance(config.system_instruction, str): - system_instruction = config.system_instruction - elif hasattr(config.system_instruction, "parts"): - parts = [] - for p in config.system_instruction.parts: - if hasattr(p, "text") and p.text: - parts.append(p.text) - system_instruction = " ".join(parts) - else: - system_instruction = str(config.system_instruction) - - prompt_data = { - "model": model, - "system_instruction": system_instruction, - "contents": [], + async def wrapped_generate(model, messages, *args, **kwargs): + record = { + "prompt": { + "model": model.model_name, + "contents": [ + {"role": message.type, "parts": message.content} for message in messages + ], + }, + "response": None, + "error": None, } - - contents_list = contents if isinstance(contents, list) else [contents] - for content in contents_list: - parts_desc = [] - if hasattr(content, "parts") and content.parts: - for part in content.parts: - if hasattr(part, "text") and part.text: - parts_desc.append({"type": "text", "text": part.text}) - elif hasattr(part, "inline_data") and part.inline_data: - parts_desc.append( - { - "type": "image", - "mime_type": part.inline_data.mime_type, - "length": len(part.inline_data.data), - } - ) - elif hasattr(part, "function_call") and part.function_call: - parts_desc.append( - { - "type": "function_call", - "name": part.function_call.name, - "args": ( - dict(part.function_call.args) - if part.function_call.args - else {} - ), - } - ) - elif hasattr(part, "function_call") and part.function_response: - parts_desc.append( - { - "type": "function_response", - "name": part.function_response.name, - "response": ( - dict(part.function_response.response) - if part.function_response.response - else {} - ), - } - ) - else: - parts_desc.append({"type": "raw", "value": str(content)}) - - prompt_data["contents"].append( - { - "role": getattr(content, "role", "unknown"), - "parts": parts_desc, - } - ) - - record = {"prompt": prompt_data, "response": None, "error": None} llm_calls_log.append(record) - try: - res = await original_generate_content( - self, - model=model, - contents=contents, - config=config, - **kwargs, - ) - - thinking_parts = [] - if hasattr(res, "candidates") and res.candidates and res.candidates[0].content: - for part in res.candidates[0].content.parts: - if getattr(part, "thought", False): - thinking_parts.append(part.text) - - resp_data = { - "text": res.text if hasattr(res, "text") else None, - "function_calls": [], - "thought": ("\n".join(thinking_parts) if thinking_parts else None), - } - if hasattr(res, "function_calls") and res.function_calls: - for fc in res.function_calls: - resp_data["function_calls"].append( - { - "name": fc.name, - "args": dict(fc.args) if fc.args else {}, - } - ) - record["response"] = resp_data - return res - except Exception as err: - err_str = str(err) - record["error"] = ( - f"{err.__class__.__name__}: {err_str}" if err_str else err.__class__.__name__ - ) - raise err + result = await original_generate(model, messages, *args, **kwargs) + record["response"] = [ + generation.message.model_dump(mode="json") for generation in result.generations + ] + return result + except Exception as error: + record["error"] = error.__class__.__name__ + raise try: try: @@ -2042,7 +1955,7 @@ class ReplayManager: val = resolved_val tool_args[name] = val - with patch.object(AsyncModels, "generate_content", wrapped_generate_content): + with patch.object(ChatOpenAI, "_agenerate", wrapped_generate): outcome = await tool_fn(**tool_args) replay_result["success"] = True diff --git a/apps/admin_console/routers/system.py b/apps/admin_console/routers/system.py index 5127eca..141e272 100644 --- a/apps/admin_console/routers/system.py +++ b/apps/admin_console/routers/system.py @@ -31,6 +31,63 @@ from artemis.core.diagnostics.schema import SystemReadinessReport router = APIRouter(prefix="/api/system", tags=["system"]) +from apps.admin_console.services.model_settings_service import ( + ModelSettingsUpdate, + get_model_settings, + save_model_settings, + test_model_settings, + CapabilitySettings, + get_capability_settings, + save_capability_settings, +) + + +@router.get("/capability-settings") +async def read_capability_settings(): + return get_capability_settings() + + +@router.post("/capability-settings") +async def write_capability_settings(payload: CapabilitySettings): + try: + return save_capability_settings(payload) + except OSError as exc: + raise HTTPException( + status_code=500, detail="无法保存功能开关,请检查 .env 文件权限" + ) from exc + + +@router.get("/model-settings") +async def read_model_settings(): + return get_model_settings() + + +@router.post("/model-settings") +async def write_model_settings(payload: ModelSettingsUpdate): + try: + result = save_model_settings(payload) + except OSError as exc: + raise HTTPException(status_code=500, detail="无法写入 .env,请检查文件权限") from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail="API Key 格式不正确,请检查后重试") from exc + await readiness_engine.run_all(force_refresh=True) + return {**result, "message": "配置已保存,新任务立即生效,重启后自动加载。"} + + +@router.post("/model-settings/test") +async def check_model_settings(payload: ModelSettingsUpdate): + try: + return await test_model_settings(payload) + except Exception as exc: + # SDK exceptions may contain URLs or request data; never echo credentials. + status = getattr(exc, "status_code", None) + detail = ( + f"Responses 连接失败(HTTP {status})" + if status + else "Responses 连接失败,请检查接口地址、模型、密钥和超时设置。" + ) + raise HTTPException(status_code=400, detail=detail) from exc + def _require_local_admin_request(request: Request) -> None: """Keep endpoint probing and mutation on the local administration boundary.""" @@ -264,8 +321,8 @@ class UpdateCredentialsRequest(BaseModel): """Payload to update and configure LLM or Vision OCR API credentials.""" provider: str = Field( - default="google", - description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)", + default="openai", + description="Provider identifier (openai or ocr)", ) api_key: str = Field(description="The secret API key string to configure") persist_to_env: bool = Field( @@ -277,8 +334,8 @@ class ValidateCredentialsRequest(BaseModel): """Payload to test and verify LLM or Vision OCR API credentials without saving.""" provider: str = Field( - default="google", - description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)", + default="openai", + description="Provider identifier (openai or ocr)", ) api_key: str = Field(description="The secret API key string to test") base_url: str | None = Field(default=None, description="Optional custom base URL") @@ -294,9 +351,8 @@ async def get_credentials(): """ from artemis.config import settings - providers = ("google", "openai", "anthropic", "openrouter", "ocr") + providers = ("openai", "ocr") status = {name: bool(settings.get_api_key(name)) for name in providers} - status["gemini"] = status["google"] return { "providers": [ {"name": name, "configured": configured} for name, configured in status.items() @@ -415,13 +471,7 @@ async def get_model_config_and_env(): return "****" return f"****{k[-4:]}" - gemini_real = get_real_env_value("GEMINI_API_KEY", "google") or get_real_env_value( - "GOOGLE_API_KEY", "google" - ) openai_real = get_real_env_value("OPENAI_API_KEY", "openai") - anthropic_real = get_real_env_value("ANTHROPIC_API_KEY", "anthropic") - openrouter_real = get_real_env_value("OPEN_ROUTER_API_KEY", "openrouter") - xai_real = get_real_env_value("XAI_API_KEY", "xai") base_url_val = settings.OPENAI_BASE_URL or os.environ.get("OPENAI_BASE_URL") if base_url_val and is_placeholder_key(base_url_val): base_url_val = None @@ -430,47 +480,19 @@ async def get_model_config_and_env(): ) env_vars = [ - { - "name": "GEMINI_API_KEY", - "provider": "google", - "is_set": bool(gemini_real), - "preview": mask_key(gemini_real), - "description": "Google Gemini multimodal vision API key (free tier available)", - }, { "name": "OPENAI_API_KEY", "provider": "openai", "is_set": bool(openai_real), "preview": mask_key(openai_real), - "description": "OpenAI API key (GPT-4o, GPT-4o-mini)", - }, - { - "name": "ANTHROPIC_API_KEY", - "provider": "anthropic", - "is_set": bool(anthropic_real), - "preview": mask_key(anthropic_real), - "description": "Anthropic Claude API key (Claude 3.5 Sonnet, Claude 3.7)", - }, - { - "name": "OPEN_ROUTER_API_KEY", - "provider": "openrouter", - "is_set": bool(openrouter_real), - "preview": mask_key(openrouter_real), - "description": "OpenRouter unified API gateway key", - }, - { - "name": "XAI_API_KEY", - "provider": "xai", - "is_set": bool(xai_real), - "preview": mask_key(xai_real), - "description": "xAI Grok vision API key", + "description": "OpenAI Responses API 密钥", }, { "name": "OPENAI_BASE_URL", - "provider": "custom", + "provider": "openai", "is_set": bool(base_url_val), "preview": base_url_val, - "description": "Custom API endpoint (for local Ollama, vLLM, DeepSeek, or proxies)", + "description": "OpenAI Responses 接口根地址(包含 /v1,不包含 /responses)", }, { "name": "VISION_API_KEY", diff --git a/apps/admin_console/services/model_service.py b/apps/admin_console/services/model_service.py index 0ccc0bc..b8d4ca0 100644 --- a/apps/admin_console/services/model_service.py +++ b/apps/admin_console/services/model_service.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from artemis.config.settings import settings + import json import logging import time @@ -26,7 +28,7 @@ class ModelService: # parse_llm_config re-reads and re-validates the config file on every call; # /api/sessions calls this once per session row, so cache (provider, model_id) # for a short TTL. Config edits take effect within _LLM_INFO_TTL seconds. - _LLM_INFO_TTL = 10.0 + _LLM_INFO_TTL = settings.ARTEMIS_MODEL_CACHE_TTL_SECONDS _llm_info_cache: tuple[float, str, str] | None = None @classmethod @@ -35,8 +37,8 @@ class ModelService: now = time.monotonic() if cached and now - cached[0] < cls._LLM_INFO_TTL: return cached[1], cached[2] - provider = "google" - model_id = "gemini-3.7-flash" + provider = "openai" + model_id = settings.OPENAI_MODEL try: from artemis.config import parse_llm_config diff --git a/apps/admin_console/services/model_settings_service.py b/apps/admin_console/services/model_settings_service.py new file mode 100644 index 0000000..2564280 --- /dev/null +++ b/apps/admin_console/services/model_settings_service.py @@ -0,0 +1,161 @@ +"""Persist the single Responses endpoint without returning credentials to the browser.""" + +import os +import tempfile +import threading +from pathlib import Path +from urllib.parse import urlsplit + +from pydantic import BaseModel, Field, SecretStr, field_validator + +from artemis.config.paths import get_env_file +from artemis.config.settings import settings + +_write_lock = threading.Lock() + + +class ModelSettingsUpdate(BaseModel): + base_url: str = "https://api.openai.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=120, gt=0, le=3600) + max_retries: int = Field(default=0, ge=0, le=10) + + @field_validator("base_url") + @classmethod + def validate_base_url(cls, value: str) -> str: + value = value.strip().rstrip("/") + parsed = urlsplit(value) + if ( + parsed.scheme not in ("https", "http") + or not parsed.hostname + or parsed.username + or parsed.password + or parsed.query + or parsed.fragment + ): + raise ValueError("请输入有效的 HTTP(S) 接口根地址,不要包含密钥、查询参数或片段") + if value.endswith(("/responses", "/chat/completions")): + raise ValueError("请输入接口根地址,例如 https://api.openai.com/v1") + return value + + @field_validator("model") + @classmethod + def validate_model(cls, value: str) -> str: + value = value.strip() + if not value or any(c in value for c in "\r\n\x00"): + raise ValueError("模型名称不能为空或包含换行") + return value + + +def get_model_settings() -> dict: + return { + "provider": "openai", + "api_mode": "responses", + "base_url": settings.OPENAI_BASE_URL or "https://api.openai.com/v1", + "model": settings.OPENAI_MODEL, + "api_key_configured": bool(settings.OPENAI_API_KEY), + "timeout_seconds": settings.OPENAI_TIMEOUT_SECONDS, + "max_retries": settings.OPENAI_MAX_RETRIES, + "env_path": str(get_env_file()), + } + + +def save_model_settings(update: ModelSettingsUpdate) -> dict: + if update.api_key and any(c in update.api_key.get_secret_value() for c in "\r\n\x00"): + raise ValueError("API Key 不能包含换行") + values = { + "OPENAI_BASE_URL": update.base_url, + "OPENAI_MODEL": update.model, + "OPENAI_TIMEOUT_SECONDS": str(update.timeout_seconds), + "OPENAI_MAX_RETRIES": str(update.max_retries), + } + if update.api_key and update.api_key.get_secret_value().strip(): + values["OPENAI_API_KEY"] = update.api_key.get_secret_value().strip() + persist_environment(values) + settings.OPENAI_BASE_URL = update.base_url + settings.OPENAI_MODEL = update.model + settings.OPENAI_TIMEOUT_SECONDS = update.timeout_seconds + settings.OPENAI_MAX_RETRIES = update.max_retries + if "OPENAI_API_KEY" in values: + settings.OPENAI_API_KEY = SecretStr(values["OPENAI_API_KEY"]) + from artemis.llm.router import ModelFactory + from apps.admin_console.services.model_service import ModelService + + ModelFactory._cache.clear() + ModelService._llm_info_cache = None + return get_model_settings() + + +def persist_environment(values: dict[str, str]) -> None: + with _write_lock: + env_path = get_env_file() + env_path.parent.mkdir(parents=True, exist_ok=True) + original = env_path.read_text(encoding="utf-8") if env_path.exists() else "" + # Preserve comments and unrelated variables; quote values to avoid dotenv interpolation. + kept = [ + line + for line in original.splitlines() + if line.removeprefix("export ").split("=", 1)[0].strip() not in values + ] + for key, value in values.items(): + escaped = value.replace("\\", "\\\\").replace("'", "\\'") + kept.append(f"{key}='{escaped}'") + content = "\n".join(kept) + "\n" + fd, temp_name = tempfile.mkstemp(prefix=".env-", dir=env_path.parent) + try: + with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temp_name, env_path) + finally: + Path(temp_name).unlink(missing_ok=True) + for key, value in values.items(): + os.environ[key] = value + + +class CapabilitySettings(BaseModel): + video_enabled: bool = True + outputter_enabled: bool = True + step_summarizer_enabled: bool = True + + +def get_capability_settings() -> CapabilitySettings: + return CapabilitySettings( + video_enabled=settings.ARTEMIS_VIDEO_ENABLED, + outputter_enabled=settings.ARTEMIS_OUTPUTTER_ENABLED, + step_summarizer_enabled=settings.ARTEMIS_STEP_SUMMARIZER_ENABLED, + ) + + +def save_capability_settings(update: CapabilitySettings) -> CapabilitySettings: + values = { + "ARTEMIS_VIDEO_ENABLED": update.video_enabled, + "ARTEMIS_OUTPUTTER_ENABLED": update.outputter_enabled, + "ARTEMIS_STEP_SUMMARIZER_ENABLED": update.step_summarizer_enabled, + } + persist_environment({key: str(value).lower() for key, value in values.items()}) + for key, value in values.items(): + setattr(settings, key, value) + return get_capability_settings() + + +async def test_model_settings(update: ModelSettingsUpdate) -> dict: + from langchain_core.messages import HumanMessage + from artemis.llm.router import ModelEndpoint, ModelFactory + + key = update.api_key.get_secret_value().strip() if update.api_key else None + endpoint = ModelEndpoint( + model_name=update.model, + api_key=key or None, + api_base=update.base_url, + timeout_seconds=update.timeout_seconds, + max_retries=0, + max_tokens=64, + ) + model = ModelFactory.create_model(endpoint) + result = await model.ainvoke([HumanMessage(content="Reply with OK only.")]) + if not result.content: + raise ValueError("Responses 接口返回空内容,请检查模型配置") + return {"success": True, "message": "Responses 连接测试成功", "model": update.model} diff --git a/apps/showcase_ui/src/app/app.config.ts b/apps/showcase_ui/src/app/app.config.ts index c5a0f87..29d6d98 100644 --- a/apps/showcase_ui/src/app/app.config.ts +++ b/apps/showcase_ui/src/app/app.config.ts @@ -1,3 +1,6 @@ +import { registerLocaleData } from '@angular/common'; +import zh from '@angular/common/locales/zh'; +registerLocaleData(zh); /** * Copyright 2026 Google LLC * @@ -14,7 +17,7 @@ * limitations under the License. */ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { ApplicationConfig, LOCALE_ID, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withXhr } from '@angular/common/http'; @@ -22,6 +25,7 @@ import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ + { provide: LOCALE_ID, useValue: 'zh-CN' }, provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(withXhr()) diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts b/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts index 5c915fd..faa5b8e 100644 --- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts @@ -4,17 +4,17 @@ import { buildStartupWorkItems } from './agent-stream.component'; describe('startup Work block', () => { it('shows only the three device preparation operations', () => { const events: StartupProgressEvent[] = [ - { stage: 'submitting', message: 'Submitting the task', timestamp: 100 }, + { stage: 'submitting', message: '正在提交任务', timestamp: 100 }, { stage: 'queued', message: 'Task received and queued', timestamp: 100.9 }, { stage: 'launching', message: 'Starting the execution process', timestamp: 101 }, { stage: 'configuration', message: 'Loading the run configuration', timestamp: 103.1 }, { stage: 'device_check', message: 'Checking the Android device', timestamp: 103.2 }, - { stage: 'device_ready', message: 'Android device connected', timestamp: 123 }, + { stage: 'device_ready', message: 'Android 设备已连接', timestamp: 123 }, { stage: 'model_warmup', message: 'Warming the model connection', timestamp: 123 }, { stage: 'uiautomator', message: 'Connecting to UI Automator', timestamp: 124 }, { stage: 'uiautomator_ready', message: 'UI Automator is ready', timestamp: 128 }, { stage: 'environment', message: 'Preparing the device environment', timestamp: 128 }, - { stage: 'environment_ready', message: 'Device environment is ready', timestamp: 131 } + { stage: 'environment_ready', message: '设备环境已就绪', timestamp: 131 } ]; const items = buildStartupWorkItems(events, 131, false, true); @@ -90,7 +90,7 @@ describe('startup Work block', () => { const [item] = buildStartupWorkItems(events, 20, false, true); - expect(item.message).toBe('UI hierarchy service is ready'); + expect(item.message).toBe('界面层级服务已就绪'); }); it('uses old first_response events only as a hidden completion boundary', () => { @@ -101,7 +101,7 @@ describe('startup Work block', () => { const [item] = buildStartupWorkItems(events, 24, false, true); - expect(item.message).toBe('Device environment is ready'); + expect(item.message).toBe('设备环境已就绪'); expect(item.elapsed).toBe('3.0s'); expect(item.isActive).toBeFalse(); }); diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html index 93172d3..ca4590c 100644 --- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html @@ -24,11 +24,11 @@ limitations under the License. class="vscode-tab-btn" [class.active]="isTaskDropdownOpen()" (click)="toggleTaskDropdown($event)" - title="Task Queue" - aria-label="Task Queue" + title="任务队列" + aria-label="任务队列" > list_alt - Task Queue + 任务队列 @@ -57,7 +57,7 @@ limitations under the License.
list_alt - Task Queue + 任务队列
@if (activeQueue().length === 0) { } @for (session of activeQueue(); track trackSession($index, session)) { @@ -80,7 +81,7 @@ limitations under the License. @if (getTaskStatus(session) === 'running') { } - {{ getTaskStatus(session) | uppercase }} + {{ getTaskStatus(session) | uiLabel }}
@if (getDeviceSerial(session)) { @@ -98,8 +99,8 @@ limitations under the License. type="button" class="btn-stop-card" (click)="stopTask(session.session_id, $event)" - title="Stop this task" - aria-label="Stop this task" + title="停止此任务" + aria-label="停止此任务" > stop_circle @@ -109,8 +110,8 @@ limitations under the License. type="button" class="btn-delete-card" (click)="deleteTask(session.session_id, $event)" - title="Delete this task" - aria-label="Delete this task" + title="删除此任务" + aria-label="删除此任务" > delete @@ -118,7 +119,7 @@ limitations under the License.
- {{ session.initial_goal || 'Unnamed Task' }} + {{ session.initial_goal || '未命名任务' }}
@if (historyTasks().length === 0) { }
- {{ session.initial_goal || 'Unnamed Task' }} + {{ session.initial_goal || '未命名任务' }}
@@ -847,7 +500,7 @@ 3 } - 3. Android OS Boot + 3. 系统启动
@@ -856,7 +509,7 @@
check_circle
- 4. Ready + 4. 就绪 @@ -877,7 +530,7 @@
info - Android virtualization cold boot usually takes 20-45 seconds. Live progress is actively tracked above—please do not click launch repeatedly. + Android 模拟器冷启动通常需要 20–45 秒。上方会显示实时进度,请勿重复点击启动。
@@ -886,14 +539,14 @@
terminal - Emulator Console Stream (stdout / stderr) + 模拟器控制台输出(stdout / stderr)
-
@if (emulatorLaunchState()?.logs?.length) {@for (log of emulatorLaunchState()?.logs; track log) {
{{ log }}
}} @else {
Waiting for process output...
}
+
@if (emulatorLaunchState()?.logs?.length) {@for (log of emulatorLaunchState()?.logs; track log) {
{{ log }}
}} @else {
正在等待进程输出…
}
} @@ -910,11 +563,11 @@ error
-

Emulator Launch Failed

+

模拟器启动失败

{{ emulatorLaunchState()?.error || 'The Android emulator process failed to start or crashed during initialization.' }}

- @@ -922,7 +575,7 @@ @if (emulatorLaunchState()?.logs?.length) {
- Process Output / Failure Logs: + 进程输出与错误日志:
@@ -968,13 +621,14 @@
- Android Emulator + Android 模拟器
@if (installedAvds().length > 0) { - {{ installedAvds().length }} local virtual device{{ installedAvds().length > 1 ? 's' : '' }} available + {{ installedAvds().length }} 个本地虚拟设备可用 } @else { - Launch a local Android Virtual Device + + 启动本地 Android 虚拟设备 }
@@ -996,9 +650,9 @@
- USB Cable + USB 连接
- Connect physical phone via USB + 通过 USB 连接实体手机
@@ -1018,9 +672,9 @@
- Wireless ADB + 无线 ADB
- Connect over local Wi-Fi + 通过局域网 Wi-Fi 连接
@@ -1040,9 +694,9 @@
- Remote Computer + 远程计算机
- Phone connected to another computer + 连接其他计算机上的手机
@@ -1056,9 +710,9 @@
hub - Artemis is currently using - {{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }}. - Switch to local ADB before using a local emulator, USB cable, or Wireless ADB. + + Artemis 当前使用 + {{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }}。使用本地模拟器、USB 或无线 ADB 前,请先切换至本地 ADB。
@@ -1133,7 +787,7 @@ @@ -1155,7 +809,7 @@ (click)="setEmulatorSetupMode('studio')" > desktop_windows - Android Studio (GUI) + Android Studio(图形界面) @@ -1177,9 +831,9 @@ 1 download -
Install Android Studio
+
安装 Android Studio
- Download for {{ effectiveOs() === 'darwin' ? 'macOS' : (effectiveOs() === 'windows' ? 'Windows' : 'Linux') }} from developer.android.com/studio and install. + Download for {{ effectiveOs() === 'darwin' ? 'macOS' : (effectiveOs() === 'windows' ? 'Windows' : 'Linux') }} from developer.android.com/studio 并安装。
@@ -1188,9 +842,10 @@ 2 developer_mode -
Open Device Manager
+
打开设备管理器
- On Studio welcome screen, open More Actions > Virtual Device Manager (or Tools > Device Manager). + + 在 Android Studio 欢迎页面打开 更多操作 > 虚拟设备管理器 (或 工具 > 设备管理器).
@@ -1199,9 +854,10 @@ 3 add_circle -
Create Virtual Device
+
创建虚拟设备
- Click Create Device, choose Pixel 8, select Android 14 (API 34), and click Finish. + + 点击 创建设备,选择 Pixel 8,选择 Android 14 (API 34),然后点击 完成.
@@ -1210,9 +866,10 @@ 4 play_arrow -
Launch Emulator
+
启动模拟器
- Click ▶ Play in Device Manager. Artemis will detect and connect to it automatically! + + 点击 ▶ 播放 。Artemis 会自动检测并连接设备。
@@ -1232,7 +889,7 @@ @@ -1241,15 +898,15 @@
2
-
Download Emulator & System Image (API 34)
-

Install Android 14 Google APIs system image using SDK Manager:

+
下载模拟器与系统镜像(API 34)
+

通过 SDK 管理器安装 Android 14 Google APIs 系统镜像:

{{ effectiveOs() === 'windows' ? 'PS>' : '$' }} {{ emuSdkInstallCmd() }}
@@ -1258,15 +915,15 @@
3
-
Create Virtual Device (Pixel 8 AVD)
-

Initialize a new Android Virtual Device named Pixel_8_API_34:

+
创建虚拟设备(Pixel 8 AVD)
+

创建 Android 虚拟设备,名称为 Pixel_8_API_34:

{{ effectiveOs() === 'windows' ? 'PS>' : '$' }} {{ emuCreateAvdCmd() }}
@@ -1275,15 +932,15 @@
4
-
Start the Emulator
-

Launch the virtual device (or add -no-window for headless execution):

+
启动模拟器
+

启动虚拟设备(可添加 -no-window 以无界面模式运行):

{{ effectiveOs() === 'windows' ? 'PS>' : '$' }} emulator -avd Pixel_8_API_34 -no-boot-anim
@@ -1301,7 +958,7 @@
cable - Connect your Android phone via USB cable and enable USB Debugging: + 通过 USB 数据线连接 Android 手机,并启用 USB 调试:
@@ -1310,8 +967,8 @@ 1 usb
-
Connect USB Cable
-
Plug your Android phone into this computer with a data cable.
+
连接 USB 数据线
+
使用数据线将 Android 手机连接到此计算机。
@@ -1319,9 +976,8 @@ 2 touch_app
-
Enable Developer Options
-
In phone Settings > About Phone, tap Build Number 7 - times.
+
启用开发者选项
+
在手机上打开 设置 > 关于手机,点击 版本号 7 次。
@@ -1329,8 +985,8 @@ 3 toggle_on
-
Enable USB Debugging
-
In Settings > Developer Options, turn USB Debugging ON. +
启用 USB 调试
+
在 设置 > 开发者选项中,将 USB 调试 开启。
@@ -1339,8 +995,8 @@ 4 verified_user
-
Authorize Computer
-
Check "Always allow" and tap Allow on the phone prompt. +
授权计算机
+
勾选 “始终允许” ,然后点击 允许 完成手机上的授权提示。
@@ -1352,44 +1008,44 @@
wifi - Connect your phone and computer on the same Wi-Fi network: + 将手机和计算机连接到同一 Wi-Fi 网络:
- +
- +
router + placeholder="例如:192.168.1.108" spellcheck="false" />
- +
- +
tag
- +
- + @if (wifiConnectMessage()) {
check_circle @@ -1406,7 +1062,8 @@
help_outline
- Find IP & port in phone Settings > Developer Options > Wireless Debugging. + + 在手机中查看 IP 地址与端口: 设置 > 开发者选项 > 无线调试.
@@ -1417,13 +1074,13 @@
hub - Enter the host and port of an ADB server endpoint that this computer can access, so Artemis can discover and control a phone physically connected to another computer. + 输入本机可访问的 ADB 服务器地址和端口,即可发现并控制连接在其他计算机上的手机。
- +
dns
- +
tag @if (isConnectingRemoteAdb()) { sync - Checking... + 正在检查… } @else { bolt - Test Connection + 测试连接 }
@@ -1481,7 +1138,7 @@ [ngModel]="rememberRemoteAdb()" (ngModelChange)="rememberRemoteAdb.set($event)" /> - Remember this endpoint for future Artemis launches + 保存此地址,供后续启动使用 @if (remoteAdbMessage()) { @@ -1507,7 +1164,7 @@ {{ device.serial }}
- {{ device.state === 'device' ? 'Ready' : device.state }} + {{ device.state === 'device' ? '就绪' : device.state }}
} @@ -1523,17 +1180,18 @@ > @if (isActivatingRemoteAdb()) { sync - Connecting... + 正在连接… } @else { check - {{ remoteAdbHasReadyDevice() ? 'Use Connection' : 'Use Anyway' }} + {{ remoteAdbHasReadyDevice() ? '使用此连接' : '仍然使用' }} } }

- Artemis sends all ADB commands to this server instead of the default local server. Any phone visible to that server becomes available to Artemis. + + Artemis 将向此服务器发送 ADB 命令,并使用该服务器能够发现的手机。

} @@ -1556,14 +1214,14 @@
@if (connectedDevices().length > 1) {
- Select Active Device: + 选择当前设备:
@for (dev of connectedDevices(); track dev.serial) { } @@ -1593,14 +1251,15 @@
screen_lock_portrait
-
USB Debugging Authorization Pending
+
等待 USB 调试授权
- Your Android device was detected, but it requires authorization. Please unlock your phone screen and tap "Allow USB Debugging". + + 已检测到 Android 设备,但尚未授权。请解锁手机并点击 “允许 USB 调试”.
@@ -1612,7 +1271,7 @@
error
- Android Platform Tools (adb) not found. Please install ADB on your operating system: + 未找到 Android 平台工具(adb)。 请在当前操作系统中安装 ADB:
@@ -1621,7 +1280,7 @@
Linux (Ubuntu / Debian) @if (effectiveOs() === 'linux') { - Detected + 已检测到 }
@@ -1630,7 +1289,7 @@
@@ -1639,7 +1298,7 @@
macOS (Homebrew) @if (effectiveOs() === 'darwin') { - Detected + 已检测到 }
@@ -1648,7 +1307,7 @@
@@ -1657,7 +1316,7 @@
Windows (WinGet) @if (effectiveOs() === 'windows') { - Detected + 已检测到 }
@@ -1666,7 +1325,7 @@
@@ -1685,12 +1344,12 @@ check
-

System Ready for Autonomous Tasks

-

All dependencies, AI model credentials, and Android device are verified and ready to run.

+

系统已就绪,可以开始自动化任务

+

依赖、模型凭据和 Android 设备均已就绪。

@@ -1711,8 +1370,8 @@ {{ adbProbe()?.summary === 'Device Locked' ? 'lock' : 'warning' }} {{ adbProbe()?.summary === 'Device Locked' - ? 'Android device is locked. Unlock it and enter the home screen before running a task.' - : 'Prerequisites incomplete: runtime, credentials, or Android device is not ready. Click here to open Setup Guide.' }} + ? 'Android 设备已锁定,请先解锁并进入主屏幕。' + : '运行环境、模型凭据或 Android 设备尚未就绪。点击此处打开系统设置。' }} arrow_forward @@ -1720,18 +1379,19 @@
- + @if (showIntentSuggestion()) {
auto_awesome - Continuous monitoring or complex task detected. Recommended: Pro Mode + + 检测到持续监控或复杂任务,建议使用: Pro 专业模式
@@ -1745,7 +1405,7 @@ rows="3" [(ngModel)]="taskGoal" [disabled]="isSubmitting()" - placeholder="Describe what you want to achieve on your phone (e.g. Open Gmail and search for latest emails)..." + placeholder="描述你希望在手机上完成的任务,例如:打开 Gmail 并查找最新邮件…" (keydown.control.enter)="runTask()" (keydown.meta.enter)="runTask()" > @@ -1761,14 +1421,14 @@

Flash · - Token-efficient, rapid 3–5s/step decisions straight from the live screen. Best for everyday tasks with a clear path, however long they run. - (No plan, no result check, no ADB commands) + 节省 Token,快速响应 每步 3–5 秒 直接根据实时屏幕执行操作,适合路径明确的日常任务。 + (不规划、不检查结果、不执行 ADB 命令)

} @else {

Pro · - Plans first, checks its results and recovers when a tap is blocked. Handles 100+ step workflows and live phone monitoring (15–40s / step). + 先规划、再检查结果,遇到操作受阻时自动恢复。支持 超过 100 步的 工作流和实时手机监控(15–40 秒 / 步)。

}
@@ -1792,7 +1452,7 @@ class="model-toggle-btn btn-flash" [class.active]="selectedProfile() === 'flash'" (click)="setProfile('flash')" - title="Flash: fast single-look loop for everyday app tasks (3–5s/step)" + title="Flash:快速执行日常应用任务(每步 3–5 秒)" > bolt Flash @@ -1802,7 +1462,7 @@ class="model-toggle-btn btn-pro" [class.active]="selectedProfile() === 'pro'" (click)="setProfile('pro')" - title="Pro: plans first, checks results and recovers from blocked steps. For long or complex tasks (15–40s/step)" + title="Pro:先规划、检查结果并恢复受阻步骤,适合长流程或复杂任务(每步 15–40 秒)" > psychology Pro @@ -1821,13 +1481,13 @@ class="btn-run-composer" [class.btn-run-pro]="selectedProfile() === 'pro'" [disabled]="!taskGoal().trim() || isSubmitting() || !isReady()" - [title]="!isReady() ? (adbProbe()?.summary === 'Device Locked' ? 'Unlock the Android device before running' : 'Complete System Setup before running') : 'Run task'" + [title]="!isReady() ? (adbProbe()?.summary === 'Device Locked' ? '请先解锁 Android 设备' : '请先完成系统设置') : '执行任务'" (click)="runTask()" > @if (isSubmitting()) { - Starting... + 正在启动… } @else { - play_arrowRun Task + play_arrow执行任务 } @@ -1838,8 +1498,8 @@ @if (selectedProfile() === 'pro') {
- Expected Output - (Optional) + 预期输出 + (可选)
@@ -1856,7 +1516,7 @@ type="button" class="btn-clear-standalone" (click)="expectedOutput.set('')" - title="Clear expected output" + title="清空预期输出" > close @@ -1867,9 +1527,9 @@
- Pro settings + Pro 设置 @if (!isTuningDefault()) { - + }
@@ -1949,7 +1609,7 @@

{{ slider.level.tagline }}

- What happens + 运行方式
    @for (line of slider.level.runs; track $index) {
  • {{ line }}
  • @@ -1958,7 +1618,7 @@
@if (slider.level.skips?.length) {
- Not included + 不包含的能力
    @for (line of slider.level.skips; track $index) {
  • {{ line }}
  • @@ -1967,7 +1627,7 @@
}
- Use it for + 适用场景 {{ slider.level.bestFor }}
@@ -1986,7 +1646,7 @@
@@ -1997,7 +1657,7 @@ [class.active]="selectedCategory() === 'all'" (click)="setCategory('all')" > - All + 全部
-
@@ -2071,7 +1731,7 @@ + [title]="item.profile === 'pro' ? '使用 ARTEMIS Pro:多智能体协作' : '使用 ARTEMIS Flash:快速执行'"> {{ item.profile === 'pro' ? 'Pro' : 'Flash' }} @@ -2086,7 +1746,7 @@ diff --git a/apps/showcase_ui/src/app/pages/home/home.component.ts b/apps/showcase_ui/src/app/pages/home/home.component.ts index e189b9c..6667d1e 100644 --- a/apps/showcase_ui/src/app/pages/home/home.component.ts +++ b/apps/showcase_ui/src/app/pages/home/home.component.ts @@ -1,3 +1,5 @@ +import { CapabilitySettingsComponent } from '../../components/model-settings/capability-settings.component'; +import { ModelSettingsComponent } from '../../components/model-settings/model-settings.component'; /** * Copyright 2026 Google LLC * @@ -14,9 +16,9 @@ * limitations under the License. */ -import { Component, signal, computed, effect, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { Component, signal, computed, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { Router, RouterLink } from '@angular/router'; +import { Router } from '@angular/router'; import { AgentService } from '../../services/agent.service'; import { SystemService } from '../../services/system.service'; import { @@ -103,7 +105,7 @@ type AdbGuideTab = 'emulator' | 'usb' | 'wifi' | 'remote'; @Component({ selector: 'app-home', standalone: true, - imports: [FormsModule], + imports: [FormsModule, ModelSettingsComponent, CapabilitySettingsComponent], templateUrl: './home.component.html', changeDetection: ChangeDetectionStrategy.Eager, styleUrl: './home.component.scss' @@ -121,32 +123,6 @@ export class HomeComponent implements OnInit, OnDestroy { public activeAdbGuideTab = signal('emulator'); public emulatorSetupMode = signal<'studio' | 'cli'>('studio'); - // Interactive guide tab for LLM / OCR credentials: 'gemini' | 'ocr' - public modelSetupMode = signal<'gemini' | 'custom'>('gemini'); - public showOcrConfig = signal(false); - public showFullConfigFile = signal(false); - - // Model & Environment configuration from backend - public modelConfigEnv = computed(() => this.systemService.modelConfigEnv()); - - // Google Gemini API Key State - public geminiKeyInput = signal(''); - public showGeminiKey = signal(false); - public isSavingGeminiKey = signal(false); - public isTestingGeminiKey = signal(false); - public geminiSaveMessage = signal(null); - public geminiSaveError = signal(null); - public isGeminiKeyEdited = signal(false); - - // Vision OCR API Key State - public ocrKeyInput = signal(''); - public showOcrKey = signal(false); - public isSavingOcrKey = signal(false); - public isTestingOcrKey = signal(false); - public ocrSaveMessage = signal(null); - public ocrSaveError = signal(null); - public isOcrKeyEdited = signal(false); - // Clipboard copy state tracker for interactive feedback public copiedId = signal(null); @@ -198,7 +174,6 @@ export class HomeComponent implements OnInit, OnDestroy { // Pro Mode Outputter & Structured Output Configuration public expectedOutput = signal(''); - public enableOutputter = signal(true); public showOutputterDrawer = signal(false); // Slider indexes map to the API ids in VERIFICATION_LEVELS / EXPLORER_MODES. @@ -238,8 +213,8 @@ export class HomeComponent implements OnInit, OnDestroy { return [ { kind: 'verify', - name: 'Result check', - ends: ['Off', 'Strict'], + name: "结果检查", + ends: ["关闭", "严格检查"], ladder: VERIFICATION_LEVELS, index: vi, level: VERIFICATION_LEVELS[vi], @@ -247,8 +222,8 @@ export class HomeComponent implements OnInit, OnDestroy { }, { kind: 'explore', - name: 'Screen reading', - ends: ['Faster', 'Sharper'], + name: "屏幕识别", + ends: ["更快", "更精细"], ladder: EXPLORER_MODES, index: ei, level: EXPLORER_MODES[ei], @@ -352,13 +327,13 @@ export class HomeComponent implements OnInit, OnDestroy { public emulatorLaunchState = computed(() => this.systemService.emulatorLaunchState()); public isEmulatorLaunching = computed(() => this.systemService.isEmulatorLaunching()); public showLaunchLogs = signal(false); - + // Probes public pythonProbe = computed(() => this.systemService.pythonProbe()); public configProbe = computed(() => this.systemService.configProbe()); public adbProbe = computed(() => this.systemService.adbProbe()); public llmProbe = computed(() => this.systemService.llmProbe()); - public geminiProbe = computed(() => this.systemService.geminiProbe()); + public openaiProbe = computed(() => this.systemService.openaiProbe()); public ocrProbe = computed(() => this.systemService.ocrProbe()); public toolchainProbe = computed(() => this.systemService.toolchainProbe()); @@ -426,16 +401,16 @@ export class HomeComponent implements OnInit, OnDestroy { public emuHypervisorTitle = computed(() => { const os = this.effectiveOs(); - if (os === 'windows') return 'Enable Windows Hypervisor Platform (WHPX)'; - if (os === 'darwin') return 'Verify macOS Hypervisor / Install Tools'; - return 'Enable KVM Hardware Acceleration (Linux)'; + if (os === 'windows') return "启用 Windows 虚拟机监控程序平台(WHPX)"; + if (os === 'darwin') return "检查 macOS 虚拟化并安装工具"; + return "启用 KVM 硬件加速(Linux)"; }); public emuHypervisorDesc = computed(() => { const os = this.effectiveOs(); - if (os === 'windows') return 'Enable Windows Hypervisor Platform in PowerShell (Run as Administrator):'; - if (os === 'darwin') return 'macOS uses native Hypervisor.framework. Install SDK tools via Homebrew (or Studio):'; - return 'Ensure virtualization permissions are granted to your user account:'; + if (os === 'windows') return "以管理员身份打开 PowerShell,启用 Windows 虚拟机监控程序平台:"; + if (os === 'darwin') return "macOS 使用系统 Hypervisor.framework。通过 Homebrew 或 Android Studio 安装 SDK 工具:"; + return "确保当前用户拥有虚拟化权限:"; }); public emuHypervisorCmd = computed(() => { @@ -467,27 +442,6 @@ export class HomeComponent implements OnInit, OnDestroy { return meta?.['configured'] === true; }); - public currentApiKey = computed(() => this.systemService.currentApiKey()); - public apiKeysMap = computed>(() => this.systemService.apiKeysMap()); - - public savedGeminiKey = computed(() => { - const keys = this.apiKeysMap(); - return keys['google'] || this.currentApiKey() || ''; - }); - - public isGeminiModified = computed(() => { - return this.geminiKeyInput().trim() !== this.savedGeminiKey().trim(); - }); - - public savedOcrKey = computed(() => { - const keys = this.apiKeysMap(); - return keys['ocr'] || ''; - }); - - public isOcrModified = computed(() => { - return this.ocrKeyInput().trim() !== this.savedOcrKey().trim(); - }); - // Rich Smart Suggestions Library (Device-Aware, Flash vs Pro Tailored) public readonly allSuggestions = this.taskRecService.allTasks; @@ -520,21 +474,6 @@ export class HomeComponent implements OnInit, OnDestroy { this.systemService.fetchReadiness().subscribe(); }; - constructor() { - effect(() => { - const keys = this.systemService.apiKeysMap(); - const current = this.systemService.currentApiKey(); - const googleKey = keys['google'] || current || ''; - const ocrKey = keys['ocr'] || ''; - if (!this.isGeminiKeyEdited()) { - this.geminiKeyInput.set(googleKey); - } - if (!this.isOcrKeyEdited()) { - this.ocrKeyInput.set(ocrKey); - } - }); - } - ngOnInit(): void { this.loadProTuningDefaults(); @@ -581,234 +520,15 @@ export class HomeComponent implements OnInit, OnDestroy { this.emulatorSetupMode.set(mode); } - public setModelSetupMode(mode: 'gemini' | 'custom'): void { - this.modelSetupMode.set(mode); - if (mode === 'custom') { - this.systemService.setSkipCredentialsCheck(true); - this.systemService.fetchModelConfigEnv().subscribe(); - } else { - this.systemService.setSkipCredentialsCheck(false); - } - } - public toggleFullConfigFile(): void { - this.showFullConfigFile.update(v => !v); - } - public toggleGeminiKeyVisibility(): void { - this.showGeminiKey.update(v => !v); - } - public toggleOcrKeyVisibility(): void { - this.showOcrKey.update(v => !v); - } - public toggleOcrConfig(): void { - this.showOcrConfig.update(v => !v); - } - - public onGeminiKeyChange(val: string): void { - this.geminiKeyInput.set(val); - this.isGeminiKeyEdited.set(true); - this.geminiSaveError.set(null); - this.geminiSaveMessage.set(null); - } - - public onOcrKeyChange(val: string): void { - this.ocrKeyInput.set(val); - this.isOcrKeyEdited.set(true); - this.ocrSaveError.set(null); - this.ocrSaveMessage.set(null); - } - - public saveGeminiKey(): void { - const key = this.geminiKeyInput().trim(); - if (!key) return; - this.isSavingGeminiKey.set(true); - this.geminiSaveError.set(null); - this.geminiSaveMessage.set(null); - - this.systemService.updateApiKey('google', key, true).subscribe({ - next: (res) => { - this.isSavingGeminiKey.set(false); - this.isGeminiKeyEdited.set(false); - this.geminiSaveMessage.set(res?.message || '✓ Gemini API key verified & saved successfully.'); - setTimeout(() => this.geminiSaveMessage.set(null), 5000); - }, - error: (err) => { - this.isSavingGeminiKey.set(false); - this.geminiSaveError.set(err?.error?.detail || err?.message || 'Failed to update Gemini API key.'); - } - }); - } - - public clearGeminiKey(): void { - this.geminiKeyInput.set(''); - this.isGeminiKeyEdited.set(false); - this.geminiSaveError.set(null); - this.geminiSaveMessage.set(null); - - if (this.savedGeminiKey().trim()) { - this.isSavingGeminiKey.set(true); - this.systemService.updateApiKey('google', '', true).subscribe({ - next: (res) => { - this.isSavingGeminiKey.set(false); - this.geminiSaveMessage.set(res?.message || '✓ Gemini API key cleared.'); - setTimeout(() => this.geminiSaveMessage.set(null), 5000); - }, - error: (err) => { - this.isSavingGeminiKey.set(false); - this.geminiSaveError.set(err?.error?.detail || err?.message || 'Failed to clear Gemini API key.'); - } - }); - } else { - this.geminiSaveMessage.set('✓ Gemini API key cleared.'); - setTimeout(() => this.geminiSaveMessage.set(null), 3000); - } - } - - public saveOcrKey(): void { - const key = this.ocrKeyInput().trim(); - if (!key) return; - this.isSavingOcrKey.set(true); - this.ocrSaveError.set(null); - this.ocrSaveMessage.set(null); - - this.systemService.updateApiKey('ocr', key, true).subscribe({ - next: (res) => { - this.isSavingOcrKey.set(false); - this.isOcrKeyEdited.set(false); - this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key verified & saved.'); - setTimeout(() => this.ocrSaveMessage.set(null), 5000); - }, - error: (err) => { - this.isSavingOcrKey.set(false); - this.ocrSaveError.set(err?.error?.detail || err?.message || 'Failed to update Vision OCR key.'); - } - }); - } - - public clearOcrKey(): void { - this.ocrKeyInput.set(''); - this.isOcrKeyEdited.set(false); - this.ocrSaveError.set(null); - this.ocrSaveMessage.set(null); - - if (this.savedOcrKey().trim()) { - this.isSavingOcrKey.set(true); - this.systemService.updateApiKey('ocr', '', true).subscribe({ - next: (res) => { - this.isSavingOcrKey.set(false); - this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key cleared.'); - setTimeout(() => this.ocrSaveMessage.set(null), 5000); - }, - error: (err) => { - this.isSavingOcrKey.set(false); - this.ocrSaveError.set(err?.error?.detail || err?.message || 'Failed to clear Vision OCR key.'); - } - }); - } else { - this.ocrSaveMessage.set('✓ Vision OCR API key cleared.'); - setTimeout(() => this.ocrSaveMessage.set(null), 3000); - } - } - - public testGeminiKey(): void { - const key = this.geminiKeyInput().trim(); - if (!key) return; - this.isTestingGeminiKey.set(true); - this.geminiSaveError.set(null); - this.geminiSaveMessage.set(null); - - this.systemService.testApiKey('google', key).subscribe({ - next: (res) => { - this.isTestingGeminiKey.set(false); - if (res?.valid) { - this.geminiSaveMessage.set(res?.message || '✓ Gemini API key is valid!'); - } else { - this.geminiSaveError.set(res?.message || 'Gemini API key verification failed.'); - } - setTimeout(() => this.geminiSaveMessage.set(null), 5000); - }, - error: (err) => { - this.isTestingGeminiKey.set(false); - this.geminiSaveError.set(err?.error?.detail || err?.message || 'Gemini API key test failed.'); - } - }); - } - - public testOcrKey(): void { - const key = this.ocrKeyInput().trim(); - if (!key) return; - this.isTestingOcrKey.set(true); - this.ocrSaveError.set(null); - this.ocrSaveMessage.set(null); - - this.systemService.testApiKey('ocr', key).subscribe({ - next: (res) => { - this.isTestingOcrKey.set(false); - if (res?.valid) { - this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key is valid!'); - } else { - this.ocrSaveError.set(res?.message || 'Vision OCR API key verification failed.'); - } - setTimeout(() => this.ocrSaveMessage.set(null), 5000); - }, - error: (err) => { - this.isTestingOcrKey.set(false); - this.ocrSaveError.set(err?.error?.detail || err?.message || 'Vision OCR API key test failed.'); - } - }); - } - - public getProviderDisplayName(tab: string): string { - switch (tab) { - case 'gemini': return 'Gemini'; - case 'ocr': return 'Vision OCR'; - default: return tab; - } - } - - public getProviderEnvVar(tab: string): string { - switch (tab) { - case 'gemini': return 'GEMINI_API_KEY'; - case 'ocr': return 'GOOGLE_VISION_API_KEY'; - default: return 'API_KEY'; - } - } - - public getProviderHint(tab: string): string { - switch (tab) { - case 'gemini': - return 'For a quick start, Google Gemini provides a free API key. Artemis also supports other models (OpenAI, Claude, OpenRouter, etc.)—you can configure your own API keys directly in .env or your environment.'; - case 'ocr': - return 'Google Cloud Vision API key for on-screen OCR text detection and UI grounding.'; - default: - return 'Configure your API key or use environment definitions.'; - } - } public skipCredentialsCheck(): void { this.systemService.skipCredentialsCheck(); } - public getApiKeyPlaceholder(tab: string): string { - switch (tab) { - case 'gemini': return 'Enter Gemini API Key (e.g. AIzaSy...)'; - case 'ocr': return 'Enter Vision OCR API Key (e.g. AIzaSy...)'; - default: return 'Enter API Key...'; - } - } - - public isTabProviderActive(tab: string): boolean { - if (tab === 'ocr') { - return this.isOcrConfigured(); - } - const targetProvider = tab === 'gemini' ? 'google' : tab; - const providers = this.configuredLlmProviders(); - return providers.some(p => p.provider === targetProvider); - } - public refreshReadiness(): void { this.isRefreshingDiagnostics.set(true); this.systemService.fetchReadiness(false, true).subscribe({ @@ -829,12 +549,12 @@ export class HomeComponent implements OnInit, OnDestroy { this.systemService.restartAdb().subscribe({ next: (res) => { this.adbRestartFeedback.set( - res?.restart_result?.skipped ? 'Devices Refreshed ✓' : 'ADB Refreshed ✓' + res?.restart_result?.skipped ? "设备已刷新 ✓" : "ADB 已刷新 ✓" ); setTimeout(() => this.adbRestartFeedback.set(null), 2500); }, error: () => { - this.adbRestartFeedback.set('Restart Failed'); + this.adbRestartFeedback.set("重启失败"); setTimeout(() => this.adbRestartFeedback.set(null), 3000); } }); @@ -842,7 +562,7 @@ export class HomeComponent implements OnInit, OnDestroy { public connectWifiDevice(): void { if (this.isRemoteAdbServer()) { - this.wifiConnectError.set('Switch to local ADB before connecting a Wireless ADB device.'); + this.wifiConnectError.set("连接无线设备前,请先切换至本地 ADB。"); return; } const host = this.wifiHost().trim(); @@ -850,7 +570,7 @@ export class HomeComponent implements OnInit, OnDestroy { const port = parseInt(portStr, 10) || 5555; if (!host) { - this.wifiConnectError.set('Please enter a valid IP address.'); + this.wifiConnectError.set("请输入有效的 IP 地址。"); return; } @@ -863,15 +583,15 @@ export class HomeComponent implements OnInit, OnDestroy { this.isConnectingWifi.set(false); const cr = res?.connect_result; if (cr?.success) { - this.wifiConnectMessage.set(`Connected to ${host}:${port}!`); + this.wifiConnectMessage.set(`已连接至 ${host}:${port}!`); setTimeout(() => this.wifiConnectMessage.set(null), 4000); } else { - this.wifiConnectError.set(cr?.message || 'Connection failed. Please check phone IP & Wi-Fi.'); + this.wifiConnectError.set(cr?.message || "连接失败,请检查手机 IP 地址和 Wi-Fi。"); } }, error: (err) => { this.isConnectingWifi.set(false); - this.wifiConnectError.set(err?.error?.detail || 'Failed to connect. Please check adb connection.'); + this.wifiConnectError.set(err?.error?.detail || "连接失败,请检查 ADB 连接。"); } }); } @@ -881,11 +601,11 @@ export class HomeComponent implements OnInit, OnDestroy { const port = Number(this.remoteAdbPort().trim()); if (!host) { - this.remoteAdbError.set('Enter the host name or IP address of the ADB server.'); + this.remoteAdbError.set("请输入 ADB 服务器的主机名或 IP 地址。"); return; } if (!Number.isInteger(port) || port < 1 || port > 65535) { - this.remoteAdbError.set('Enter a port between 1 and 65535.'); + this.remoteAdbError.set("请输入 1–65535 之间的端口。"); return; } @@ -910,7 +630,7 @@ export class HomeComponent implements OnInit, OnDestroy { error: error => { this.isConnectingRemoteAdb.set(false); this.remoteAdbError.set( - error?.error?.detail || 'Unable to test the ADB server endpoint.' + error?.error?.detail || "无法测试 ADB 服务器连接。" ); } }); @@ -919,7 +639,7 @@ export class HomeComponent implements OnInit, OnDestroy { public activateRemoteAdbServer(): void { const tested = this.remoteAdbProbeResult(); if (!tested?.success) { - this.remoteAdbError.set('Test the endpoint before using it.'); + this.remoteAdbError.set("请先测试服务器连接。"); return; } @@ -944,7 +664,7 @@ export class HomeComponent implements OnInit, OnDestroy { error: error => { this.isActivatingRemoteAdb.set(false); this.remoteAdbError.set( - error?.error?.detail || 'Unable to use the ADB server endpoint.' + error?.error?.detail || "无法使用此 ADB 服务器。" ); } }); @@ -979,7 +699,7 @@ export class HomeComponent implements OnInit, OnDestroy { error: error => { this.isSwitchingToLocalAdb.set(false); this.remoteAdbError.set( - error?.error?.detail || 'Unable to switch back to the local ADB server.' + error?.error?.detail || "无法切换回本地 ADB 服务器。" ); } }); @@ -1069,12 +789,12 @@ export class HomeComponent implements OnInit, OnDestroy { public runTask(): void { const goal = this.taskGoal().trim(); if (!goal) { - this.errorMessage.set('Please enter a task goal before running.'); + this.errorMessage.set("请先输入任务目标。"); return; } if (!this.isReady()) { - this.errorMessage.set('System prerequisites are not satisfied. Please review System Setup first.'); + this.errorMessage.set("运行条件尚未满足,请先检查系统设置。"); this.activeTab.set('diagnostics'); return; } @@ -1089,7 +809,7 @@ export class HomeComponent implements OnInit, OnDestroy { this.selectedProfile() === 'pro' && this.expectedOutput().trim() ? this.expectedOutput().trim() : undefined, - this.selectedProfile() === 'pro' ? this.enableOutputter() : undefined, + undefined, this.selectedProfile() === 'pro' ? { verificationLevel: this.verificationLevel().id, explorerMode: this.explorerMode().id } : undefined @@ -1103,7 +823,7 @@ export class HomeComponent implements OnInit, OnDestroy { console.error('Failed to submit task from home page:', err); this.isSubmitting.set(false); this.errorMessage.set( - err?.error?.detail || 'Failed to submit task. Please check server connection.' + err?.error?.detail || "提交任务失败,请检查服务器连接。" ); } }); diff --git a/apps/showcase_ui/src/app/pages/workspace/workspace.component.html b/apps/showcase_ui/src/app/pages/workspace/workspace.component.html index a44cd84..fedc937 100644 --- a/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +++ b/apps/showcase_ui/src/app/pages/workspace/workspace.component.html @@ -27,7 +27,7 @@ limitations under the License.
error {{ errorMessage() }} -
@@ -89,7 +89,7 @@ limitations under the License.
- Start a new task... + 开始新任务…
@@ -104,7 +104,7 @@ limitations under the License. (blur)="onInputBlur()" rows="1" [disabled]="isSubmitting()" - [placeholder]="selectedProfile() === 'pro' + [placeholder]="selectedProfile() === 'pro' ? 'Pro: plans first, checks results and recovers from blocked steps. For long or complex tasks' : 'Flash: fast single-look loop for everyday app tasks'" class="dock-textarea" @@ -115,7 +115,7 @@ limitations under the License. @if (taskInput.trim() && !isSubmitting()) { } @@ -137,7 +137,7 @@ limitations under the License. [disabled]="!taskInput.trim() || isSubmitting()" (click)="submitTask()" [title]="isSubmitting() ? 'Starting task...' : 'Start task (Enter)'" - aria-label="Start task" + aria-label="开始任务" > @if (isSubmitting()) { sync @@ -152,16 +152,16 @@ limitations under the License.