feat: introduce MCP server, cross-platform runtime, and Showcase UI reorganization

- Showcase UI Reorganization:
  * Move web UI dashboard from root directory into apps/showcase_ui/ for cleaner project structure
  * Add 'artemis ui' CLI command (artemis/interfaces/cli/commands/ui.py) and launcher scripts for the interactive showcase console
- Standalone MCP Server & Notifiers (mcp_server/):
  * Implement dedicated MCP server package with mobile automation tools (device_state, inspect_trace, task_manager, task_runner)
  * Add multi-channel notifiers (AgentAPI, Desktop notifications, Webhooks, File output, and Composite routing)
  * Introduce background task runner and trace store for autonomous mobile execution monitoring
- Cross-Platform Runtime & Toolchain Support:
  * Add artemis/platform/ with OS-specific implementations for Linux, macOS (Darwin), and Windows
  * Add artemis/runtime/supervisor.py for resilient process and agent runtime supervision
  * Add artemis/toolchain/ with dependency resolution and SDK descriptors
  * Add cross-platform launcher scripts (start.sh, start.bat) and installer scripts (scripts/install_deps.*)
- Core Enhancements & Diagnostics:
  * Enhance coordinate normalization, OCR API fallback, and perception pipeline
  * Add credential validation (credentials_validator.py) and system diagnostics
  * Update bilingual documentation (README.md, README_CN.md) and architecture diagrams
- Test Suite Expansion:
  * Add integration tests for cloud gateway, remote ADB, and Artemis login
  * Add unit tests covering core diagnostics, platform detection, toolchain, credentials validator, optional OCR, and MCP tools
This commit is contained in:
somew1nd committed 2026-08-18 13:59:27 -07:00
1 parent 78e478f502
commit 9a5aaf99b7
249 files changed
+37226 -9339

No files matched your search

+123 -21
View File
@@ -20,6 +20,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Artemis Debug UI</title>
<link rel="icon" type="image/png" sizes="192x192" href="/logo.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="/logo.png?v=2">
<link rel="icon" type="image/svg+xml" href="/logo.svg?v=2">
<link rel="shortcut icon" href="/favicon.ico?v=2">
<link rel="apple-touch-icon" href="/logo.png?v=2">
<style>
pre.light-code {
background: transparent !important;
@@ -385,21 +390,33 @@
marker.style.top = `${pctY}%`;
marker.style.transform = 'translate(-50%, -50%)';
marker.style.zIndex = '10';
marker.style.pointerEvents = 'none';
const dotSize = sequenceIndex !== undefined ? 20 : 14;
const dot = document.createElement('div');
dot.style.width = '14px';
dot.style.height = '14px';
dot.style.width = `${dotSize}px`;
dot.style.height = `${dotSize}px`;
dot.style.borderRadius = '50%';
dot.style.backgroundColor = '#ef4444';
dot.style.border = '2px solid white';
dot.style.boxShadow = '0 0 6px rgba(0,0,0,0.4)';
if (sequenceIndex !== undefined) {
dot.style.display = 'flex';
dot.style.alignItems = 'center';
dot.style.justifyContent = 'center';
dot.style.fontSize = '11px';
dot.style.fontWeight = 'bold';
dot.style.color = 'white';
dot.innerText = String(sequenceIndex);
}
const pulseSize = dotSize * 2;
const pulse = document.createElement('div');
pulse.style.position = 'absolute';
pulse.style.top = '-9px';
pulse.style.left = '-9px';
pulse.style.width = '28px';
pulse.style.height = '28px';
pulse.style.top = `-${dotSize / 2 + 2}px`;
pulse.style.left = `-${dotSize / 2 + 2}px`;
pulse.style.width = `${pulseSize}px`;
pulse.style.height = `${pulseSize}px`;
pulse.style.borderRadius = '50%';
pulse.style.border = '2px solid #ef4444';
pulse.style.animation = 'pulse-animation 1.5s infinite ease-out';
@@ -409,14 +426,18 @@
marker.appendChild(dot);
const label = document.createElement('div');
label.innerText = actionType.toUpperCase();
if (sequenceIndex !== undefined) {
label.innerText = totalSequence && totalSequence > 1 ? `#${sequenceIndex} TAP` : actionType.toUpperCase();
} else {
label.innerText = actionType.toUpperCase();
}
label.style.position = 'absolute';
label.style.top = '16px';
label.style.top = `${dotSize + 2}px`;
label.style.left = '50%';
label.style.transform = 'translateX(-50%)';
label.style.backgroundColor = 'rgba(15, 23, 42, 0.8)';
label.style.backgroundColor = 'rgba(15, 23, 42, 0.9)';
label.style.color = 'white';
label.style.padding = '1px 5px';
label.style.padding = '1.5px 5px';
label.style.borderRadius = '3px';
label.style.fontSize = '8px';
label.style.fontWeight = 'bold';
@@ -427,6 +448,66 @@
overlay.appendChild(marker);
}
function createSequenceConnector(x1, y1, x2, y2, overlay, isNormalized) {
let pctX1 = x1 / 10;
let pctY1 = y1 / 10;
let pctX2 = x2 / 10;
let pctY2 = y2 / 10;
const container = overlay.parentElement;
const img = container ? container.querySelector('img') : null;
if (!isNormalized && img && img.naturalWidth && img.naturalHeight) {
if (x1 > 1000 || y1 > 1000 || x2 > 1000 || y2 > 1000 || (x1 > 100 && y1 > 100 && img.naturalWidth > 500)) {
pctX1 = (x1 / img.naturalWidth) * 100;
pctY1 = (y1 / img.naturalHeight) * 100;
pctX2 = (x2 / img.naturalWidth) * 100;
pctY2 = (y2 / img.naturalHeight) * 100;
}
}
let svg = overlay.querySelector('svg');
if (!svg) {
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.style.position = 'absolute';
svg.style.top = '0';
svg.style.left = '0';
svg.style.pointerEvents = 'none';
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
marker.setAttribute('id', 'seq-arrow');
marker.setAttribute('viewBox', '0 0 10 10');
marker.setAttribute('refX', '5');
marker.setAttribute('refY', '5');
marker.setAttribute('markerWidth', '6');
marker.setAttribute('markerHeight', '6');
marker.setAttribute('orient', 'auto');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
path.setAttribute('fill', '#ef4444');
marker.appendChild(path);
defs.appendChild(marker);
svg.appendChild(defs);
overlay.appendChild(svg);
}
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', `${pctX1}%`);
line.setAttribute('y1', `${pctY1}%`);
line.setAttribute('x2', `${pctX2}%`);
line.setAttribute('y2', `${pctY2}%`);
line.setAttribute('stroke', '#ef4444');
line.setAttribute('stroke-width', '2.5');
line.setAttribute('marker-end', 'url(#seq-arrow)');
line.setAttribute('stroke-dasharray', '4,4');
svg.appendChild(line);
}
function createLineMarker(x1, y1, x2, y2, overlay, isNormalized) {
let pctX1 = x1 / 10;
let pctY1 = y1 / 10;
@@ -518,9 +599,37 @@
return;
}
// 1. Multi-point click sequence
const isSequenceAction = actionName.includes('sequence') || Boolean(act.sequence || args.sequence || act.normalized_sequence || args.normalized_sequence);
let rawSeq = act.normalized_sequence || args.normalized_sequence || act.sequence || args.sequence || act.targets || args.targets;
if (typeof rawSeq === 'string') {
try { rawSeq = JSON.parse(rawSeq); } catch (e) {}
}
if (Array.isArray(rawSeq) && rawSeq.length > 0) {
const validPoints = [];
rawSeq.forEach(pt => {
if (typeof pt === 'string') {
try { pt = JSON.parse(pt); } catch (e) {}
}
if (Array.isArray(pt) && pt.length >= 2 && typeof pt[0] === 'number' && typeof pt[1] === 'number') {
validPoints.push([pt[0], pt[1]]);
}
});
if (validPoints.length > 0 && (isSequenceAction || validPoints.length > 1)) {
const isNormSeq = Boolean(act.normalized_sequence || args.normalized_sequence) || validPoints.every(p => p[0] <= 1000 && p[1] <= 1000);
for (let i = 0; i < validPoints.length - 1; i++) {
createSequenceConnector(validPoints[i][0], validPoints[i][1], validPoints[i+1][0], validPoints[i+1][1], overlayElement, isNormSeq);
}
validPoints.forEach((pt, idx) => {
createPointMarker(pt[0], pt[1], actionName, overlayElement, isNormSeq, idx + 1, validPoints.length);
});
return;
}
}
const isSwipeLike = actionName.includes('swipe') || actionName.includes('scroll') || actionName.includes('drag') || actionName.includes('slide');
// 1. Separate start and end coordinates
// 2. Separate start and end coordinates
const normStart = act.normalized_start_coordinates || args.normalized_start_coordinates;
const normEnd = act.normalized_end_coordinates || args.normalized_end_coordinates;
const startCoords = normStart || act.start_coordinates || act.start_point || act.start || act.from ||
@@ -538,11 +647,11 @@
return;
}
// 2. 4-coordinate array
// 3. 4-coordinate array
const normCoords = act.normalized_coordinates || args.normalized_coordinates;
const isNorm = Boolean(normCoords);
const rawCoords = normCoords || act.coordinates || act.coords || act.target ||
args.target || args.coordinates || args.coords || args.sequence ||
args.target || args.coordinates || args.coords ||
(isSwipeLike && Array.isArray(args.action) ? args.action : null) ||
(isSwipeLike && Array.isArray(act.action) ? act.action : null) ||
(isSwipeLike && Array.isArray(args.gesture) ? args.gesture : null) ||
@@ -557,17 +666,10 @@
createPointMarker(rawCoords[0], rawCoords[1], actionName, overlayElement, isNorm);
return;
}
} else if (Array.isArray(rawCoords[0]) && rawCoords[0].length === 2) {
rawCoords.forEach(pt => {
if (Array.isArray(pt) && pt.length === 2 && typeof pt[0] === 'number') {
createPointMarker(pt[0], pt[1], actionName, overlayElement, isNorm);
}
});
return;
}
}
// 3. Directional swipe / scroll ONLY for swipe-like actions
// 4. Directional swipe / scroll ONLY for swipe-like actions
if (isSwipeLike) {
const rawDir = (typeof args.action === 'string' ? args.action : null) ||
(typeof act.action === 'string' && act.action !== actionName ? act.action : null) ||