android-midscene-automation 0.2.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -1
- package/README.md +63 -1
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +10 -8
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +13 -1
- package/src/App.vue +72 -23
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +369 -186
- package/src/types.ts +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Video Sub-Agent - Chunk Analyzer
|
|
2
|
+
|
|
3
|
+
You are a specialized video chunk analyzer. Your task is to analyze a specific segment of a video and answer the query provided by the Main Agent.
|
|
4
|
+
|
|
5
|
+
You must follow these principles strictly:
|
|
6
|
+
1. **Causality and Chronology**: First establish a chronology of states or events on screen over the entire video segment to help differentiate between user inputs (e.g., physical taps, touch indicators) and system state changes.
|
|
7
|
+
2. **Objective Observations**: Describe only what you see in the video. Pay extreme attention to mobile UI specifics: identify which icons are clicked (visual touch indicators), screen transitions, loading spinners, popup dialogs, keyboard appearing/disappearing, and any changes in UI text or status bars. Use fuzzy matching when evaluating UI text and notifications. Do not assume or speculate about things not visible in this chunk.
|
|
8
|
+
3. **Context Isolation**: You only see a part of the full video. Do not assume what happened before or after this segment.
|
|
9
|
+
4. **Focus on Intent**: Answer the specific query or prompt provided by the Main Agent. Do not give a generic summary if a specific question is asked.
|
|
10
|
+
5. **Structured Output**: Provide your findings clearly, using the exact global timestamps calculated according to the IMPORTANT CONTEXT prepended to your prompt. You must rely on the timestamp burned directly into the video frames to accurately report event timings. Never use relative 00:xx formats; always output the final global test time in seconds.
|
|
11
|
+
6. **Visual Proof**: Identify the exact best frame that provides visual proof of your findings. Supply this timestamp as `verification_timestamp_secs` when calling `submit_answer`.
|
|
12
|
+
7. **Slow-Motion Awareness**: If a slowdown warning is present in prompt context, animations, transitions, and timers appear in slow-motion. Do not misclassify slow-motion as lag, stutter, or frozen UI.
|
|
13
|
+
|
|
14
|
+
## Output Format
|
|
15
|
+
You MUST invoke the `submit_answer` tool to return your findings.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Audio Sub-Agent - Chunk Analyzer
|
|
2
|
+
|
|
3
|
+
You are a specialized audio chunk analyzer. Your task is to analyze a specific audio segment extracted from a video and answer the query provided by the Main Agent.
|
|
4
|
+
|
|
5
|
+
You must follow these principles strictly:
|
|
6
|
+
1. **Objective Observations**: Describe only what you hear in the audio (speech, conversations, background sounds, tone, language). Pay attention not only to the spoken words but also to non-verbal cues (tone, hesitation, emotion) and critical background sounds (notification chimes, click sounds, error beeps) if they are relevant to the query. Do not assume or speculate about things not audible.
|
|
7
|
+
2. **Context Isolation**: You only hear a part of the full audio. Do not assume what happened before or after this segment.
|
|
8
|
+
3. **Focus on Intent**: Answer the specific query or prompt provided by the Main Agent. Do not give a generic summary if a specific question is asked.
|
|
9
|
+
4. **Structured Output**: Provide your findings clearly, using the exact global timestamps calculated according to the IMPORTANT CONTEXT prepended to your prompt. Never use relative 00:xx formats; always output the final global test time in seconds.
|
|
10
|
+
|
|
11
|
+
## Output Format
|
|
12
|
+
You MUST invoke the `submit_answer` tool to return your findings.
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""HTTP client for the Artemis Accessibility Helper running on a device.
|
|
16
|
+
|
|
17
|
+
A lightweight, conflict-free alternative to UIAutomator2: it never takes the
|
|
18
|
+
singleton ``UiAutomationConnection``, so it runs alongside Mobly, Appium and
|
|
19
|
+
Espresso. This class only speaks the helper's HTTP API. Installing, enabling,
|
|
20
|
+
port forwarding, the session token and hot-plug recovery live in
|
|
21
|
+
:mod:`artemis.runtime.helper_manager`; this client asks the manager for a
|
|
22
|
+
session lazily and hands transport failures back to it once.
|
|
23
|
+
|
|
24
|
+
The screen-data contract is byte-for-byte the one :class:`UIAutomatorClient`
|
|
25
|
+
exposes (``UIAutomatorScreenData`` with the element dictionaries produced by
|
|
26
|
+
``parse_hierarchy_xml_to_elements``), so every consumer downstream
|
|
27
|
+
(``filter_ui_hierarchy``, the Explorer index, the MCP actuators that compare
|
|
28
|
+
``clickable == "true"``) sees identical shapes whichever backend is active.
|
|
29
|
+
|
|
30
|
+
One observation is one request: ``/snapshot?fields=xml`` returns the XML and,
|
|
31
|
+
on Android 11+, the screenshot taken at the same instant with the bitmap's own
|
|
32
|
+
dimensions. On older Android the same answer carries the XML and the client
|
|
33
|
+
only adds an adb screencap; the hierarchy is never dumped twice.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from io import BytesIO
|
|
39
|
+
import json
|
|
40
|
+
import subprocess
|
|
41
|
+
from typing import Any
|
|
42
|
+
import urllib.error
|
|
43
|
+
import urllib.request
|
|
44
|
+
|
|
45
|
+
from PIL import Image
|
|
46
|
+
|
|
47
|
+
from artemis.runtime.adb_endpoint import adb_command
|
|
48
|
+
from artemis.runtime.awake_service import ensure_device_awake
|
|
49
|
+
from artemis.runtime.helper_manager import (
|
|
50
|
+
DEVICE_PORT,
|
|
51
|
+
PACKAGE_NAME,
|
|
52
|
+
SERVICE_NAME,
|
|
53
|
+
AccessibilityHelperManager,
|
|
54
|
+
HelperSession,
|
|
55
|
+
HelperUnavailable,
|
|
56
|
+
ProvisionEvent,
|
|
57
|
+
helper_manager,
|
|
58
|
+
)
|
|
59
|
+
from third_party.mobile_use.clients.ui_automator_client import (
|
|
60
|
+
UIAUTOMATOR_BOOL_ATTRIBUTES,
|
|
61
|
+
UIAutomatorScreenData,
|
|
62
|
+
parse_hierarchy_xml_to_elements,
|
|
63
|
+
pil_to_base64,
|
|
64
|
+
)
|
|
65
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
66
|
+
|
|
67
|
+
logger = get_logger(__name__)
|
|
68
|
+
|
|
69
|
+
# Kept for callers that imported these from here.
|
|
70
|
+
DEFAULT_PORT = DEVICE_PORT
|
|
71
|
+
TOKEN_HEADER = "X-Artemis-Token"
|
|
72
|
+
__all__ = [
|
|
73
|
+
"AccessibilityClient",
|
|
74
|
+
"DEFAULT_PORT",
|
|
75
|
+
"HelperEmptyHierarchy",
|
|
76
|
+
"HelperRequestError",
|
|
77
|
+
"HelperUnavailable",
|
|
78
|
+
"PACKAGE_NAME",
|
|
79
|
+
"SERVICE_NAME",
|
|
80
|
+
"TOKEN_HEADER",
|
|
81
|
+
"normalize_helper_elements",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
_BOOL_ATTRIBUTES = (*UIAUTOMATOR_BOOL_ATTRIBUTES, "visible-to-user")
|
|
85
|
+
_GLOBAL_KEYS = ("back", "home", "recents", "notifications", "quick_settings")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class HelperEmptyHierarchy(RuntimeError):
|
|
89
|
+
"""The helper answered but found no window to dump.
|
|
90
|
+
|
|
91
|
+
Raised instead of returning a screen with zero elements, so ``auto`` mode
|
|
92
|
+
can try UIAutomator2 and ``helper`` mode reports the real cause instead of
|
|
93
|
+
showing the agent an empty screen.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HelperRequestError(RuntimeError):
|
|
98
|
+
"""The helper answered with an HTTP error that a new tunnel cannot fix."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, status: int, path: str, body: str):
|
|
101
|
+
self.status = status
|
|
102
|
+
self.path = path
|
|
103
|
+
self.body = body
|
|
104
|
+
super().__init__(f"accessibility helper {path} answered HTTP {status}: {body[:200]}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def normalize_helper_elements(elements: Any) -> list[dict[str, Any]]:
|
|
108
|
+
"""Coerce the helper's JSON element list into the UIAutomator element shape.
|
|
109
|
+
|
|
110
|
+
Used only when a dump carries elements but no XML. The JSON encodes flags
|
|
111
|
+
as booleans and omits ``accessibilityText``; the XML parser emits string
|
|
112
|
+
flags and mirrors ``content-desc`` into ``accessibilityText``, and several
|
|
113
|
+
consumers compare against the string ``"true"``.
|
|
114
|
+
"""
|
|
115
|
+
if isinstance(elements, dict):
|
|
116
|
+
elements = [elements]
|
|
117
|
+
if not isinstance(elements, list):
|
|
118
|
+
return []
|
|
119
|
+
normalized: list[dict[str, Any]] = []
|
|
120
|
+
for raw in elements:
|
|
121
|
+
if not isinstance(raw, dict):
|
|
122
|
+
continue
|
|
123
|
+
element = dict(raw)
|
|
124
|
+
for key in _BOOL_ATTRIBUTES:
|
|
125
|
+
if key in element and isinstance(element[key], bool):
|
|
126
|
+
element[key] = "true" if element[key] else "false"
|
|
127
|
+
desc = element.get("content-desc")
|
|
128
|
+
if desc is not None and "accessibilityText" not in element:
|
|
129
|
+
element["accessibilityText"] = desc
|
|
130
|
+
bounds = element.get("parsed_bounds")
|
|
131
|
+
if isinstance(bounds, dict):
|
|
132
|
+
element["parsed_bounds"] = {
|
|
133
|
+
side: int(bounds.get(side, 0)) for side in ("left", "top", "right", "bottom")
|
|
134
|
+
}
|
|
135
|
+
element.pop("children", None)
|
|
136
|
+
normalized.append(element)
|
|
137
|
+
return normalized
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class AccessibilityClient:
|
|
141
|
+
"""Screen-data client over the helper's loopback HTTP API."""
|
|
142
|
+
|
|
143
|
+
backend_name = "helper"
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
device_id: str,
|
|
148
|
+
manager: AccessibilityHelperManager | None = None,
|
|
149
|
+
*,
|
|
150
|
+
provision_on_connect: bool = True,
|
|
151
|
+
request_timeout: float = 6.0,
|
|
152
|
+
) -> None:
|
|
153
|
+
self._device_id = device_id
|
|
154
|
+
self._manager = manager or helper_manager
|
|
155
|
+
self._provision_on_connect = provision_on_connect
|
|
156
|
+
self._request_timeout = request_timeout
|
|
157
|
+
self._session: HelperSession | None = None
|
|
158
|
+
self._awake_strategy: str | None = None
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------ #
|
|
161
|
+
# Session lifecycle
|
|
162
|
+
# ------------------------------------------------------------------ #
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def device_id(self) -> str:
|
|
166
|
+
return self._device_id
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def session(self) -> HelperSession | None:
|
|
170
|
+
return self._session
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def active_backend(self) -> str | None:
|
|
174
|
+
return self.backend_name if self._session is not None else None
|
|
175
|
+
|
|
176
|
+
def connect(self, on_event: ProvisionEvent | None = None) -> None:
|
|
177
|
+
"""Task-path entry: provision (install / upgrade / enable) and attach.
|
|
178
|
+
|
|
179
|
+
``on_event`` receives ``installing`` / ``upgrading`` before the slow step.
|
|
180
|
+
"""
|
|
181
|
+
self._session = self._manager.attach(
|
|
182
|
+
self._device_id, provision=self._provision_on_connect, on_event=on_event
|
|
183
|
+
)
|
|
184
|
+
if self._awake_strategy is None:
|
|
185
|
+
self._awake_strategy = ensure_device_awake(self._device_id)
|
|
186
|
+
|
|
187
|
+
def _ensure_session(self) -> HelperSession:
|
|
188
|
+
"""Observer-path entry: attach to a helper that is already running."""
|
|
189
|
+
if self._session is None:
|
|
190
|
+
self._session = self._manager.attach(self._device_id, provision=False)
|
|
191
|
+
if self._awake_strategy is None:
|
|
192
|
+
self._awake_strategy = ensure_device_awake(self._device_id)
|
|
193
|
+
return self._session
|
|
194
|
+
|
|
195
|
+
def disconnect(self) -> None:
|
|
196
|
+
self._manager.detach(self._device_id)
|
|
197
|
+
self._session = None
|
|
198
|
+
self._awake_strategy = None
|
|
199
|
+
|
|
200
|
+
def ping(self) -> bool:
|
|
201
|
+
try:
|
|
202
|
+
session = self._ensure_session()
|
|
203
|
+
except HelperUnavailable:
|
|
204
|
+
return False
|
|
205
|
+
return self._manager.ping(session.local_port) is not None
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------ #
|
|
208
|
+
# Transport
|
|
209
|
+
# ------------------------------------------------------------------ #
|
|
210
|
+
|
|
211
|
+
def _http(
|
|
212
|
+
self, path: str, payload: dict[str, Any] | None = None, timeout: float | None = None
|
|
213
|
+
) -> bytes:
|
|
214
|
+
"""One request with two single-shot repairs.
|
|
215
|
+
|
|
216
|
+
* HTTP 401: the helper lost its token (re-bound service, reboot): push it
|
|
217
|
+
again and retry once.
|
|
218
|
+
* Transport failure on a read: rebuild the tunnel once and retry.
|
|
219
|
+
Actions are not replayed because they may already have executed.
|
|
220
|
+
|
|
221
|
+
Any other HTTP status is a :class:`HelperRequestError`; a new tunnel
|
|
222
|
+
would not change the answer.
|
|
223
|
+
"""
|
|
224
|
+
session = self._ensure_session()
|
|
225
|
+
try:
|
|
226
|
+
return self._http_once(session, path, payload, timeout)
|
|
227
|
+
except urllib.error.HTTPError as exc:
|
|
228
|
+
body = _read_error_body(exc)
|
|
229
|
+
if exc.code != 401:
|
|
230
|
+
raise HelperRequestError(exc.code, path, body) from exc
|
|
231
|
+
logger.warning(
|
|
232
|
+
f"Accessibility helper on {self._device_id} rejected the session token "
|
|
233
|
+
f"({body[:120]}); pushing it again."
|
|
234
|
+
)
|
|
235
|
+
self._manager.push_token(self._device_id)
|
|
236
|
+
try:
|
|
237
|
+
return self._http_once(session, path, payload, timeout)
|
|
238
|
+
except urllib.error.HTTPError as again:
|
|
239
|
+
raise HelperRequestError(again.code, path, _read_error_body(again)) from again
|
|
240
|
+
except (urllib.error.URLError, OSError, TimeoutError) as exc:
|
|
241
|
+
if payload is not None:
|
|
242
|
+
raise
|
|
243
|
+
logger.warning(
|
|
244
|
+
f"Accessibility helper request {path} on {self._device_id} failed ({exc}); "
|
|
245
|
+
"rebuilding the tunnel once."
|
|
246
|
+
)
|
|
247
|
+
self._session = self._manager.reattach(self._device_id)
|
|
248
|
+
try:
|
|
249
|
+
return self._http_once(self._session, path, payload, timeout)
|
|
250
|
+
except urllib.error.HTTPError as again:
|
|
251
|
+
raise HelperRequestError(again.code, path, _read_error_body(again)) from again
|
|
252
|
+
|
|
253
|
+
def _http_once(
|
|
254
|
+
self,
|
|
255
|
+
session: HelperSession,
|
|
256
|
+
path: str,
|
|
257
|
+
payload: dict[str, Any] | None,
|
|
258
|
+
timeout: float | None,
|
|
259
|
+
) -> bytes:
|
|
260
|
+
data = (
|
|
261
|
+
json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
|
|
262
|
+
)
|
|
263
|
+
headers: dict[str, str] = {}
|
|
264
|
+
token = session.token or self._manager.host_token()
|
|
265
|
+
if token:
|
|
266
|
+
headers[TOKEN_HEADER] = token
|
|
267
|
+
if data is not None:
|
|
268
|
+
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
269
|
+
request = urllib.request.Request(f"{session.base_url}{path}", data=data, headers=headers)
|
|
270
|
+
with urllib.request.urlopen(request, timeout=timeout or self._request_timeout) as resp:
|
|
271
|
+
return resp.read()
|
|
272
|
+
|
|
273
|
+
def _rpc(self, cmd: str, params: dict[str, Any] | None = None) -> bool:
|
|
274
|
+
payload = {"cmd": cmd, **(params or {})}
|
|
275
|
+
try:
|
|
276
|
+
data = json.loads(self._http("/action", payload).decode("utf-8"))
|
|
277
|
+
except (
|
|
278
|
+
HelperUnavailable,
|
|
279
|
+
HelperRequestError,
|
|
280
|
+
urllib.error.URLError,
|
|
281
|
+
OSError,
|
|
282
|
+
ValueError,
|
|
283
|
+
TimeoutError,
|
|
284
|
+
) as exc:
|
|
285
|
+
logger.warning(f"Accessibility helper command '{cmd}' failed: {exc}")
|
|
286
|
+
return False
|
|
287
|
+
return bool(data.get("success", False))
|
|
288
|
+
|
|
289
|
+
# ------------------------------------------------------------------ #
|
|
290
|
+
# Screen data
|
|
291
|
+
# ------------------------------------------------------------------ #
|
|
292
|
+
|
|
293
|
+
def get_hierarchy(self) -> str:
|
|
294
|
+
"""Raw UIAutomator-format XML, the same contract as ``UIAutomatorClient``."""
|
|
295
|
+
return self._http("/dump_xml").decode("utf-8")
|
|
296
|
+
|
|
297
|
+
def get_hierarchy_json(self, *, include_invisible: bool = False) -> dict[str, Any]:
|
|
298
|
+
"""The helper's JSON dump (XML plus its own element list); diagnostics only."""
|
|
299
|
+
query = "fields=xml,elements" + ("&include_invisible=1" if include_invisible else "")
|
|
300
|
+
return json.loads(self._http(f"/dump?{query}").decode("utf-8"))
|
|
301
|
+
|
|
302
|
+
def _snapshot(self) -> dict[str, Any]:
|
|
303
|
+
"""``/snapshot?fields=xml``: hierarchy plus, on Android 11+, the screenshot."""
|
|
304
|
+
data = json.loads(self._http("/snapshot?fields=xml", timeout=8.0).decode("utf-8"))
|
|
305
|
+
if not isinstance(data, dict):
|
|
306
|
+
raise HelperRequestError(200, "/snapshot", "non-object answer")
|
|
307
|
+
if not data.get("success"):
|
|
308
|
+
raise HelperEmptyHierarchy(
|
|
309
|
+
f"accessibility helper on {self._device_id} returned no hierarchy: "
|
|
310
|
+
f"{data.get('error') or 'unknown reason'}"
|
|
311
|
+
)
|
|
312
|
+
return data
|
|
313
|
+
|
|
314
|
+
def get_atomic_snapshot(self) -> dict[str, Any] | None:
|
|
315
|
+
"""Screenshot + hierarchy captured in one call on the device (Android 11+), else None."""
|
|
316
|
+
data = self._snapshot()
|
|
317
|
+
if data.get("has_screenshot") and data.get("screenshot_base64"):
|
|
318
|
+
return data
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
def get_screenshot(self) -> Image.Image | None:
|
|
322
|
+
try:
|
|
323
|
+
result = subprocess.run(
|
|
324
|
+
adb_command(["-s", self._device_id, "exec-out", "screencap", "-p"]),
|
|
325
|
+
stdin=subprocess.DEVNULL,
|
|
326
|
+
capture_output=True,
|
|
327
|
+
timeout=10,
|
|
328
|
+
check=True,
|
|
329
|
+
)
|
|
330
|
+
return Image.open(BytesIO(result.stdout))
|
|
331
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
332
|
+
logger.error(f"Failed to capture screenshot via adb: {exc}")
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
def get_screenshot_base64(self) -> str | None:
|
|
336
|
+
screenshot = self.get_screenshot()
|
|
337
|
+
return None if screenshot is None else pil_to_base64(screenshot, format="JPEG")
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def _elements_from_dump(dump: dict[str, Any]) -> tuple[str, list[dict[str, Any]]]:
|
|
341
|
+
xml = dump.get("xml") or ""
|
|
342
|
+
if isinstance(xml, str) and xml.strip():
|
|
343
|
+
return xml, parse_hierarchy_xml_to_elements(xml)
|
|
344
|
+
return "", normalize_helper_elements(dump.get("elements"))
|
|
345
|
+
|
|
346
|
+
def get_screen_data(self) -> UIAutomatorScreenData:
|
|
347
|
+
snapshot = self._snapshot()
|
|
348
|
+
xml, elements = self._elements_from_dump(snapshot)
|
|
349
|
+
if snapshot.get("has_screenshot") and snapshot.get("screenshot_base64"):
|
|
350
|
+
width, height = _dimensions(snapshot)
|
|
351
|
+
return UIAutomatorScreenData(
|
|
352
|
+
base64=str(snapshot["screenshot_base64"]),
|
|
353
|
+
hierarchy_xml=xml,
|
|
354
|
+
elements=elements,
|
|
355
|
+
width=width,
|
|
356
|
+
height=height,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Android 10 and below (or a rate-limited capture that did not recover):
|
|
360
|
+
# keep the hierarchy we already have and only add an adb screencap.
|
|
361
|
+
reason = snapshot.get("screenshot_error")
|
|
362
|
+
if reason:
|
|
363
|
+
logger.debug(f"Helper snapshot without screenshot ({reason}); using adb screencap")
|
|
364
|
+
screenshot = self.get_screenshot()
|
|
365
|
+
if screenshot is None:
|
|
366
|
+
raise RuntimeError("Failed to capture screenshot")
|
|
367
|
+
return UIAutomatorScreenData(
|
|
368
|
+
base64=pil_to_base64(screenshot, format="JPEG"),
|
|
369
|
+
hierarchy_xml=xml,
|
|
370
|
+
elements=elements,
|
|
371
|
+
width=screenshot.width,
|
|
372
|
+
height=screenshot.height,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# ------------------------------------------------------------------ #
|
|
376
|
+
# Input
|
|
377
|
+
# ------------------------------------------------------------------ #
|
|
378
|
+
|
|
379
|
+
def set_clipboard(self, text: str) -> bool:
|
|
380
|
+
"""Put ``text`` on the device clipboard so the driver can paste it."""
|
|
381
|
+
return self._rpc("clipboard", {"text": text})
|
|
382
|
+
|
|
383
|
+
def send_text(self, text: str) -> bool:
|
|
384
|
+
"""Append ``text`` to the focused field, the way typing through an IME does."""
|
|
385
|
+
return self._rpc("type", {"text": text, "append": True})
|
|
386
|
+
|
|
387
|
+
def clear_text(self) -> bool:
|
|
388
|
+
return self._rpc("clear", {})
|
|
389
|
+
|
|
390
|
+
def press_key(self, key: str) -> bool:
|
|
391
|
+
key_lower = str(key).lower()
|
|
392
|
+
if key_lower in _GLOBAL_KEYS:
|
|
393
|
+
return self._rpc("global", {"action": key_lower})
|
|
394
|
+
result = subprocess.run(
|
|
395
|
+
adb_command(
|
|
396
|
+
[
|
|
397
|
+
"-s",
|
|
398
|
+
self._device_id,
|
|
399
|
+
"shell",
|
|
400
|
+
"input",
|
|
401
|
+
"keyevent",
|
|
402
|
+
f"KEYCODE_{key_lower.upper()}",
|
|
403
|
+
]
|
|
404
|
+
),
|
|
405
|
+
stdin=subprocess.DEVNULL,
|
|
406
|
+
capture_output=True,
|
|
407
|
+
text=True,
|
|
408
|
+
timeout=15,
|
|
409
|
+
check=False,
|
|
410
|
+
)
|
|
411
|
+
return result.returncode == 0
|
|
412
|
+
|
|
413
|
+
def tap(self, x: float, y: float) -> bool:
|
|
414
|
+
return self._rpc("tap", {"x": x, "y": y})
|
|
415
|
+
|
|
416
|
+
def swipe(self, x1: float, y1: float, x2: float, y2: float, duration_ms: int = 300) -> bool:
|
|
417
|
+
return self._rpc("swipe", {"x1": x1, "y1": y1, "x2": x2, "y2": y2, "duration": duration_ms})
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _read_error_body(exc: urllib.error.HTTPError) -> str:
|
|
421
|
+
try:
|
|
422
|
+
return exc.read().decode("utf-8", "replace")
|
|
423
|
+
except OSError:
|
|
424
|
+
return str(exc)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _dimensions(snapshot: dict[str, Any]) -> tuple[int, int]:
|
|
428
|
+
"""The screenshot's own size; a snapshot without it is a broken answer, not a 1080x2400 one."""
|
|
429
|
+
try:
|
|
430
|
+
width = int(snapshot.get("width") or 0)
|
|
431
|
+
height = int(snapshot.get("height") or 0)
|
|
432
|
+
except (TypeError, ValueError):
|
|
433
|
+
width = height = 0
|
|
434
|
+
if width <= 0 or height <= 0:
|
|
435
|
+
raise HelperRequestError(
|
|
436
|
+
200, "/snapshot", f"screenshot without dimensions (width={width}, height={height})"
|
|
437
|
+
)
|
|
438
|
+
return width, height
|