Skill: geelarkbot/awesome-geelark-skill (version 1.0.1)

--- name: awesome-geelark-skill description: Interact with GeeLark Cloud Phone API for managing cloud phones, automation tasks, and social media operations. Use when asked to create cloud phones, manage phones, run automation tasks on TikTok/Instagram/Facebook/YouTube/Reddit, or interact with GeeLark services. --- # awesome-geeLark-skill > ⚠️ **EXPERIMENTAL PROJECT**: This skill is designed for AI agents. It contains safety mechanisms but should NOT be used directly in production without testing. **Configuration**: `assets/config.json` (auto-loads token and base URL) --- > 🔒 **Security Notice** > > **Required Dependencies (not auto-installed):** > - `adb` (Android Debug Bridge) — must be available in system PATH > - `uiautomator2` — Python package for device automation > > **Credential Handling:** > - API credentials (token, appId, apiKey) are stored in `assets/config.json` > - This file is protected by `.gitignore` — **never commit it** > - Set restrictive permissions: `chmod 600 assets/config.json` > - **⚠️RPA tasks require third-party account login. We recommend completing login in GeeLark first. Never send account credentials to the agent.** > - Logs are written to `logs/cloudphone/` — review and mask sensitive data before sharing > > **Autonomous Execution:** > - This skill runs local subprocess commands (`adb`, `glogin`) and accesses API credentials > - **Do not enable unattended autonomous invocation** until you've validated behavior in a sandbox > - Always use human confirmation for destructive operations (deletion, RPA with account credentials) > - **⚠️ RPA tasks require third-party account login. We recommend completing login in GeeLark first. Never send account credentials to the agent.** --- ## First Time Setup ### 1. Install System Dependencies Ensure `adb` (Android Debug Bridge) is available in your system PATH: ```bash # macOS brew install android-platform-tools # Ubuntu/Debian sudo apt update && sudo apt install adb # Windows # Option 1: Using winget winget install Google.AndroidSDK # Option 2: Download Platform Tools from https://developer.android.com/studio/releases/platform-tools # Extract and add to system PATH manually ``` Verify installation: ```bash adb version ``` > 💡 **Troubleshooting:** If `adb` is not found after installation, restart your terminal or add it to PATH manually. ### 2. Set Up Python Environment (Recommended) Use a virtual environment to avoid conflicts with system packages: ```bash # Create virtual environment python3 -m venv .venv # Activate (macOS/Linux) source .venv/bin/activate # Activate (Windows) .venv\Scripts\activate # Install dependencies pip install requests uiautomator2 ``` > ⚠️ **Alternative (not recommended):** If you must install system-wide: > ```bash > pip install requests uiautomator2 --break-system-packages > ``` ### 3. Run Doctor Diagnostic (Recommended) Before starting cloud phone operations, run the doctor script to verify all dependencies: ```bash # Basic check (environment, ADB, network, API) python scripts/doctor.py # Include phone-specific checks python scripts/doctor.py --phone-id <phone_id> # Include uiautomator2 connectivity test python scripts/doctor.py --phone-id <phone_id> --serial 192.168.1.100:5555 # Output as JSON (for programmatic use) python scripts/doctor.py --json ``` The doctor checks: - ✅ Python dependencies (`requests`, `uiautomator2`) - ✅ ADB installation and device connectivity - ✅ DNS resolution and GeeLark API reachability - ✅ API authentication and wallet balance - ✅ Phone status (if `--phone-id` provided) - ✅ uiautomator2 connectivity and UI dump (if `--serial` provided) ### 4. Initialize Configuration ```bash python3 scripts/init_config.py ``` This creates `assets/config.json` with your credentials (protected by `.gitignore`). All scripts automatically load credentials from this file. --- ## Quick Start ### Basic Usage #### List Cloud Phones ```python from scripts.geelark_boot_helper import list_cloud_phones # Token auto-loads from config phones = list_cloud_phones() for phone in phones: print(f"{phone['serialName']} - {phone['id']}") ``` #### Boot Cloud Phone and Connect ```python from scripts.geelark_boot_helper import boot_and_connect # Token auto-loads from config adb_info = boot_and_connect("phone_id") if adb_info: print(f"ADB: {adb_info['ip']}:{adb_info['port']}, [REDACTED_SECRET]'pwd']}") ``` --- ## Core Workflow ### Step 1: Pre-check (Balance & Cloud Phone Confirmation) ⭐⭐⭐ **Before operating cloud phones:** 1. **Check account balance** - Confirm sufficient balance 2. **Query available cloud phones** - Confirm which phone to operate ```python from scripts.geelark_client import GeeLarkClient from scripts.geelark_boot_helper import list_cloud_phones # Initialize client (auto-loads config) client = GeeLarkClient(task_name="my_task", phone_id="batch") # Check balance wallet = client.wallet() balance = wallet['data']['balance'] gift_money = wallet['data'].get('giftMoney', 0) if balance <= 0 and gift_money <= 0: print("❌ Insufficient balance! Please recharge.") exit(1) # List phones phones = list_cloud_phones() for phone in phones: print(f" {phone['serialName']} (ID: {phone['id']})") ``` ### Step 2: Create Cloud Phone ```python # Create single phone response = client.call("/open/v1/phone/addNew", { "mobileType": "Android 13", "data": [{"profileName": "MyPhone"}] }) # Create multiple phones (requires Pro plan) response = client.call("/open/v1/phone/addNew", { "mobileType": "Android 13", "data": [ {"profileName": "Phone1"}, {"profileName": "Phone2"} ] }) ``` ### Step 3: Boot and Enable ADB ```python from scripts.geelark_boot_helper import boot_and_connect # This function handles: # - Balance check # - Cloud phone status check # - Start cloud phone (if stopped) # - Enable ADB (if disabled) # - Return ADB connection info # Token auto-loads from config adb_info = boot_and_connect("phone_id") ``` ### Step 4: Install Application ⭐⭐⭐ **Critical**: Must use `appVersionId`, NOT `appName` #### Option 1: Using install_app.py Script (Recommended) ```bash # Search and install TikTok (auto-select best match) python scripts/install_app.py --phone-id <phone_id> --name TikTok # Exclude TikTok Lite python scripts/install_app.py --phone-id <phone_id> --name TikTok --exclude Lite # Install specific version directly python scripts/install_app.py --phone-id <phone_id> --name Instagram --version-id <appVersionId> ``` #### Option 2: Using API Directly ```python # Step 1: Find app and get appVersionId response = client.call("/open/v1/app/installable/list", { "envId": "phone_id", "name": "TikTok", "page": 1, "pageSize": 20 }) app_version_id = response['data']['items'][0]['appVersionInfoList'][0]['id'] # Step 2: Install using appVersionId response = client.call("/open/v1/app/install", { "envId": "phone_id", "appVersionId": app_version_id }) ``` ### Step 5: Manage Session with PhoneManager **Recommended**: Use `PhoneManager` for session tracking and auto-close. ```python from scripts.phone_manager import PhoneManager # Method 1: Context manager (recommended) with PhoneManager(timeout_minutes=5) as manager: manager.start_monitor() # Connect using phone_id (required) d = manager.connect_device("phone_id_123", ip, port, pwd, name="Android15") # Perform operations d(text="Submit").click() manager.record_activity("phone_id_123") # Automatically stops all phones and saves logs on exit # Method 2: Manual control manager = PhoneManager(timeout_minutes=5) manager.start_monitor() # ... operations ... manager.stop_all() manager.client.save_log() ``` --- ## Safety Mechanisms | Mechanism | Description | |-----------|-------------| | **Balance Check** | Auto-checks balance before starting cloud phones | | **Energy Saving** | Auto enables `energySavingMode=1` to reduce costs | | **Deletion Safeguards** | Anti-automation mechanism: requires interactive terminal, explicit ID + 'YES' confirmation, and enforced delays to prevent scripted or accidental deletion | | **Auto-Close** | `PhoneManager` auto-closes idle devices | | **Endpoint Whitelist** | Only documented endpoints can be called | | **Logging** | All operations logged to `logs/cloudphone/` | > ⚠️ **Deletion Safety**: AI agents must follow the [Deletion Workflow](#-deletion-workflow-mandatory). The `delete_helper.py` script includes multiple anti-automation safeguards: > - Interactive terminal verification > - Input timing validation > - Mandatory delay before final confirmation > > If the script blocks execution, instruct the user to run manually in their terminal. --- ## Cloudphone Operations ### Prerequisites - `adb` must be installed and available in PATH (see [First Time Setup](#first-time-setup)) - `uiautomator2` Python package installed (see [Python Environment](#2-set-up-python-environment-recommended)) ### uiautomator2 Smoke Test If uiautomator2 operations seem slow or hang, run the smoke test to identify the bottleneck: ```bash # Basic connectivity and UI dump test python scripts/ui_smoke_test.py 192.168.1.100:5555 # Test with app start python scripts/ui_smoke_test.py 192.168.1.100:5555 --package com.zhiliaoapp.musically # Custom timeout per step (default: 30s) python scripts/ui_smoke_test.py 192.168.1.100:5555 --timeout 60 # Output as JSON python scripts/ui_smoke_test.py 192.168.1.100:5555 --json ``` The smoke test checks each step with individual timeouts: 1. **Connection** - `u2.connect(serial)` 2. **Device Info** - `d.info` (current activity) 3. **UI Hierarchy** - `d.dump_hierarchy()` 4. **App Start** - `d.app_start(package)` (if package provided) ### Agent Workflow ⭐⭐⭐ Since this skill is designed for AI agents that need to recognize the current screen state, the recommended workflow is: 1. **Identify UI Structure**: Use `uiautomator2` to dump the current page hierarchy (`dump_hierarchy()`), enabling the agent to understand the screen layout. - Parse the XML hierarchy to locate target elements by `text`, `resource-id`, `content-desc`, or `class` - Extract element coordinates (`bounds`) for verification or fallback operations - If `dump_hierarchy()` fails or returns empty, proceed to Step 3 immediately 2. **Prioritize uiautomator2 Operations**: If elements can be identified and interacted with via the UI hierarchy, use `uiautomator2` for clicks, inputs, and swipes. - **Click**: `d(text="Button").click()` or `d(resourceId="com.example:id/btn").click()` - **Input**: `d(text="Username").set_text("hello")` - **Swipe**: `d.swipe(x1, y1, x2, y2)` or `d.swipe_ext("up")` - **Wait**: `d(text="Expected Text").wait(timeout=10)` before interaction - Always verify the element is visible and enabled before interaction 3. **Verify & Retry**: After each operation, execute `dump_hierarchy()` again to confirm the previous action succeeded. - Check if the expected UI state changed (e.g., new screen appeared, text updated, dialog dismissed) - If verification fails, retry the operation up to 3 times with brief delays (2-3 seconds) - If all retries fail, log the error and proceed to Step 4 4. **Fallback to ADB Screenshots**: If elements cannot be recognized or operated via `uiautomator2`, use ADB screenshots to visually confirm the state and perform coordinate-based operations. - Capture screenshot: `d.screenshot("/tmp/screen.png")` or via ADB `screencap` - Use computer vision or LLM to analyze the screenshot and identify target coordinates - Click coordinates: `d.click(x, y)` or `subprocess.run(['adb', '-s', 'device', 'shell', 'input', 'tap', x, y])` - Swipe coordinates: `subprocess.run(['adb', '-s', 'device', 'shell', 'input', 'swipe', x1, y1, x2, y2])` - **Note**: Coordinate-based operations are less reliable; always prefer uiautomator2 when possible ### Basic Operations ```python import uiautomator2 as u2 import subprocess # Connect d = u2.connect(f"{adb_info['ip']}:{adb_info['port']}") # Authenticate (MUST DO IMMEDIATELY) subprocess.run(['adb', '-s', f"{adb_info['ip']}:{adb_info['port']}", 'shell', 'glogin', adb_info['pwd']], capture_output=True) # Get UI hierarchy (Step 1: Identify) xml = d.dump_hierarchy() # Find and click element (Step 2: Prioritize uiautomator2) d(text="Submit").click() # Input text d(text="Username").set_text("hello world") # Screenshot (Step 3: Fallback) d.screenshot("/tmp/screen.png") # Or using ADB subprocess.run(['adb', '-s', f"{adb_info['ip']}:{adb_info['port']}", 'shell', 'screencap', '-p', '/sdcard/screen.png']) subprocess.run(['adb', '-s', f"{adb_info['ip']}:{adb_info['port']}", 'pull', '/sdcard/screen.png', '/tmp/screen.png']) ``` **See**: `references/cloudphone_operations.md` for complete guide. ### Handling Permission Dialogs After app installation or first launch, Android shows permission/system dialogs. Use the reusable handler to auto-dismiss them: ```python from scripts.handle_android_permissions import handle_all_dialogs # Handle all dialogs: permissions (allow/deny) + system dialogs result = handle_all_dialogs(d, permission_action="allow") print(f"Handled {result['total']} dialogs") # Or handle only permission dialogs (deny all) from scripts.handle_android_permissions import handle_permission_dialogs handle_permission_dialogs(d, action="deny", max_retries=5) # Or handle only system dialogs (Not Now, Close, Done, etc.) from scripts.handle_android_permissions import handle_system_dialogs handle_system_dialogs(d, max_retries=3) ``` **As a standalone script:** ```bash # Allow all permissions python scripts/handle_android_permissions.py 192.168.1.100:5555 # Deny all permissions python scripts/handle_android_permissions.py 192.168.1.100:5555 --action deny # Handle up to 10 dialogs python scripts/handle_android_permissions.py 192.168.1.100:5555 --max-retries 10 ``` The handler supports: - ✅ English: `Allow`, `Allow`, `Don't Allow`, `Don't Allow` (curly quotes) - ✅ Chinese: `允许`, `同意`, `拒绝`, `取消`, `关闭` - ✅ System dialogs: `Close`, `Done`, `Not Now`, `稍后` --- ## RPA Tasks GeeLark provides built-in RPA tasks for 9 platforms: - TikTok (login, follow, like, comment, edit, delete) - Instagram (login, publish reels, warmup) - Facebook (login, publish, auto-comment, active account) - YouTube (publish short, publish video, active account) - Reddit (publish image/video, warmup) - Threads (publish image/video) - Pinterest (publish image/video) - X/Twitter (publish) - Google (login, app download) **See**: `references/api_reference.md` for complete endpoint list and parameters --- ## Reference Documentation | Document | Description | When to Read | |----------|-------------|--------------| | `references/api_reference.md` | Complete API endpoint list | Need to look up endpoints/parameters | | `references/error_codes.md` | All error codes and solutions | API call fails | | `references/best_practices.md` | Safety and performance tips | Planning operations | | `references/cloudphone_operations.md` | ADB/uiautomator2 guide | Working with device UI | | `references/auto_close.md` | Auto-close mechanism | Managing idle devices | --- ## Common Error Codes | Code | Description | Action | |------|-------------|--------| | `0` | Success | ✅ | | `41001` | Balance not enough | Recharge account | | `42001` | Cloud phone does not exist | Check phone ID | | `42002` | Cloud phone is not running | Start phone first | **See**: `references/api_reference.md` for complete list. --- ## Structured Error Codes (Local Scripts) The following error codes are used by local diagnostic scripts (doctor.py, boot_and_connect(), etc.): | Error Code | Severity | Description | Quick Fix | |------------|----------|-------------|-----------| | `ENV_DEPENDENCY_MISSING` | 🔴 Critical | Python package or system tool not installed | Run doctor: `python scripts/doctor.py` | | `NETWORK_DNS_FAILED` | 🔴 Critical | Cannot reach GeeLark API | Check DNS, firewall, baseUrl in config | | `GEELARK_API_FAILED` | 🔴 Critical | API authentication or call failed | Verify token: `python scripts/init_config.py` | | `PHONE_START_TIMEOUT` | 🟡 Warning | Phone did not start within timeout | Check balance, retry after few minutes | | `ADB_NOT_FOUND` | 🔴 Critical | ADB not in PATH | Install: `brew install android-platform-tools` | | `ADB_CONNECT_FAILED` | 🔴 Critical | Cannot connect to ADB device | Start phone first with `boot_and_connect()` | | `GLOGIN_REQUIRED` | 🔴 Critical | ADB authentication needed | Run: `adb -s <ip:port> shell glogin <pwd>` | | `UIAUTOMATOR_TIMEOUT` | 🟡 Warning | uiautomator2 operation timed out | Run smoke test: `python scripts/ui_smoke_test.py <serial>` | | `UIAUTOMATOR_CONNECT_FAILED` | 🔴 Critical | Cannot connect to uiautomator2 | Check ADB connection first | | `APP_INSTALL_CANDIDATE_AMBIGUOUS` | 🟡 Warning | Multiple app matches found | Use `--exclude` flag or `--version-id` | | `INSUFFICIENT_BALANCE` | 🔴 Critical | Account balance too low | Recharge via GeeLark dashboard | | `CONFIG_LOAD_FAILED` | 🔴 Critical | Cannot load config file | Run: `python scripts/init_config.py` | **See**: `scripts/error_codes.py` for complete list and recommendations. --- ## 🤖 AI Agent Execution Rules **Critical Directives:** 1. **Pre-flight Check**: Always verify account balance (`wallet()`) before initiating any cloud phone operations. 2. **Identifiers**: Use `phone_id` (UUID) for all API calls, not `serialName`. 3. **RPA Parameter Quirks**: - **YouTube**: `video` parameter is a **string**. - **Others (IG/FB/Reddit/etc.)**: `video`/`images` parameters are **arrays**. - **Instagram**: Field is `Image` (capital I), not `images`. - **Facebook**: Login field is `Email`, not `account`. 4. **Lifecycle Management**: You must **stop** a running phone before deleting it (otherwise error 43009). 5. **Auto-Configuration**: Credentials load automatically from `assets/config.json`. Do not pass `token` manually. 6. **Logging**: Always save operation logs to `logs/cloudphone/` upon task completion. 7. **Credential Safety**: Never print or log full API tokens, passwords, or proxy credentials. Use masked versions (e.g., `tok_abc...xyz`) in outputs. 8. **Autonomous Restriction**: Do not invoke RPA tasks with third-party account credentials without explicit human confirmation. Always require approval before executing social media automation that involves login credentials. 9. **⚠️ RPA tasks require third-party account login. You should strongly recommend users complete login in GeeLark first. Never request account credentials from users under any circumstances.** --- ## 🗑️ Deletion Workflow (MANDATORY) **⚠️ AI agents MUST follow this exact sequence for deletion operations. Never skip steps or auto-execute.** ### Step 1: List Available Phones ```python from scripts.geelark_boot_helper import list_cloud_phones phones = list_cloud_phones() ``` ### Step 2: Display IDs & Status to User Output a clear table like: ``` 📱 Current Cloud Phones: | ID (UUID) | Name | Status | |----------------------------------------|-----------|----------| | a1b2c3d4-e5f6-7890-abcd-ef1234567890 | Android13 | Running | | b2c3d4e5-f6a7-8901-bcde-f12345678901 | Android15 | Stopped | ``` ### Step 3: Explicitly Ask for Confirmation Prompt the user with: ``` ⚠️ Deletion is permanent and cannot be undone. Please confirm which phone IDs to delete by replying with the IDs and typing 'YES'. Example: "Delete a1b2c3d4... YES" ``` ### Step 4: 🛑 STOP AND WAIT **You MUST pause execution and wait for the user's explicit response.** - Do NOT proceed automatically. - Do NOT fill in confirmation prompts via heredoc or automation. - If the user does not reply with explicit confirmation, cancel the operation. ### Step 5: Execute Only After Confirmation Once the user confirms, run: ```bash python3 scripts/delete_helper.py ``` Or call the deletion API with explicit `DELETE_CONFIRMED` code. > 🔒 **Security Rule**: AI agents are **FORBIDDEN** from bypassing confirmation. If you cannot wait for user input, do not attempt deletion. Instruct the user to run `python3 scripts/delete_helper.py` manually. --- ## 🔐 Security Best Practices ### For Human Operators - **Sandbox First**: Test this skill in an isolated environment (VM/container) before production use - **Credential Rotation**: Implement regular credential rotation on a scheduled basis. - **File Permissions**: Restrict access to `assets/config.json`: ```bash chmod 600 assets/config.json ``` - **Log Review**: Before sharing logs, review `logs/cloudphone/` for sensitive data and redact as needed - **Dependency Verification**: Verify `adb` and `uiautomator2` are from official sources ### For AI Agents - **Never** store or transmit credentials outside the `assets/config.json` file - **Always** confirm with the user before executing operations that involve: - Third-party social media account credentials - Cloud phone deletion (follow the mandatory Deletion Workflow) - Bulk operations affecting multiple devices - **Never** bypass the double-confirmation mechanism for destructive operations - **⚠️ RPA tasks require third-party account login. You should strongly recommend users complete login in GeeLark first. Never request account credentials from users under any circumstances.** - **Deletion Operations**: Always follow the 5-step Deletion Workflow. List IDs → Ask for confirmation → STOP AND WAIT → Execute only after explicit user reply. Never auto-fill or chain deletion commands. --- **Skill Repository**: https://github.com/GeeLark/awesome-geeLark-skill **API Documentation**: https://github.com/GeeLark/geelark-openapi

path: references/auto_close.md
content: # Auto-Close Mechanism

Automatically close idle cloud phones to save costs and resources.

---

## Overview

GeeLark cloud phones are billed by usage time. Forgotten devices can rack up unexpected charges. The auto-close mechanism automatically stops devices that have been idle for a specified timeout.

---

## Why Auto-Close?

- 💰 **Save Costs** - Billing continues while phones are running
- 🧹 **Release Resources** - Free up proxies and system resources
- 🔒 **Security Management** - Prevent accidental costs from forgetting to close

---

## Basic Usage

### 1. Create PhoneManager

```python
from scripts.phone_manager import PhoneManager

# Create manager with 5-minute timeout
manager = PhoneManager(timeout_minutes=5)
```

### 2. Start Background Monitoring

```python
# Start background monitoring thread
manager.start_monitor()

print("🔍 Background monitoring started (timeout: 5 minutes)")
```

### 3. Connect and Operate

```python
# Connect to device (phone_id is required, name is optional for display)
d = manager.connect_device("phone_id_123", ip, port, pwd, name="Android15")

# Perform operations
d(text="Settings").click()

# Activity is automatically recorded
```

### 4. Auto-Close

After 5 minutes of no activity, the device will automatically stop:

```
⚠️  Android15: Idle for 5 minutes
🛑 Stopped Android15 (phone_id)
```

---

## Complete Example

```python
import uiautomator2 as u2
from scripts.phone_manager import PhoneManager

# Create manager with 5-minute timeout
manager = PhoneManager(timeout_minutes=5)

# Start background monitoring
manager.start_monitor()

# Connect to device
adb_info = {"ip": "1.2.3.4", "port": "5555", "pwd": "password"}
d = manager.connect_device("phone_id_123",
                           adb_info['ip'],
                           adb_info['port'],
                           adb_info['pwd'],
                           name="Android15")

# Perform operations
d(text="Settings").click()

# Continue working...

# Device will auto-close after 5 minutes of no activity
```

---

## Manual Activity Recording

### Record Activity Manually

```python
# Record activity to prevent auto-close (use phone_id)
manager.record_activity("phone_id_123")
```

### Use During Long-Running Tasks

```python
def long_running_task():
    for i in range(10):
        # Do work
        process_data()

        # Record activity every 2 minutes (use phone_id)
        manager.record_activity("phone_id_123")
        time.sleep(120)

long_running_task()
```

---

## Whitelist Devices

### Add Device to Whitelist

Whitelisted devices are never auto-closed. All operations use `phone_id` for precise matching.

```python
# Add device to whitelist (use phone_id)
manager.add_to_whitelist("phone_id_123")

# Output: 📋 Android15 (phone_id_123): Added to whitelist
```

### Use for Long-Running Tasks

```python
# Add monitoring device to whitelist
manager.add_to_whitelist("monitoring_phone_id")

# This device will never be auto-closed
# Even if idle for hours
```

### Remove from Whitelist

```python
# Remove device from whitelist (use phone_id)
manager.remove_from_whitelist("phone_id_123")

print("📋 phone_id_123: Removed from whitelist")
```

---

## Configuration Options

### Timeout Configuration

```python
# 2-minute timeout (short tasks)
manager = PhoneManager(timeout_minutes=2)

# 5-minute timeout (default)
manager = PhoneManager(timeout_minutes=5)

# 10-minute timeout (long tasks)
manager = PhoneManager(timeout_minutes=10)

# 15-minute timeout (very long tasks)
manager = PhoneManager(timeout_minutes=15)
```

### Check Interval Configuration

```python
# Check every 30 seconds (default: 60)
manager.start_monitor(check_interval=30)

# Check every 60 seconds (default)
manager.start_monitor(check_interval=60)

# Check every 120 seconds (less frequent)
manager.start_monitor(check_interval=120)
```

---

## Stop All Devices

### Manual Stop All

```python
# Stop all active devices
manager.stop_all()

print("🛑 All active devices stopped")
```

### Auto-Close on Exit

Using context manager:

```python
# Automatically stop all devices on exit
with PhoneManager(timeout_minutes=5) as manager:
    manager.start_monitor()

    # Connect and operate
    d = manager.connect_device("Android15", ip, port, pwd, phone_id)
    # ...

# Automatically stops all devices when exiting context
```

---

## Best Practices

### 1. Set Appropriate Timeout

```python
# Short tasks (1-2 minutes): 2-minute timeout
manager = PhoneManager(timeout_minutes=2)

# Regular tasks: 5-minute timeout (default)
manager = PhoneManager(timeout_minutes=5)

# Long tasks: 10-15 minute timeout
manager = PhoneManager(timeout_minutes=10)
```

### 2. Record Activity During Long Tasks

```python
def long_task():
    for i in range(100):
        # Do work
        process_item(i)

        # Record activity every 2 minutes (use phone_id)
        if i % 20 == 0:  # Every 20 iterations
            manager.record_activity("phone_id_123")

long_task()
```

### 3. Use Whitelist for Critical Devices

```python
# Add monitoring device to whitelist (use phone_id)
manager.add_to_whitelist("monitoring_phone_id")

# This device will never be auto-closed
```

### 4. Use Context Manager for Cleanup

```python
# Guaranteed cleanup on exit
with PhoneManager(timeout_minutes=5) as manager:
    manager.start_monitor()

    # Connect using phone_id
    d = manager.connect_device("phone_id_123", ip, port, pwd, name="Android15")
    # ...

# Automatically stops all devices
```

### 5. Monitor Background Thread

```python
# Check if monitor thread is running
if manager.monitor_thread and manager.monitor_thread.is_alive():
    print("✅ Monitor thread is running")
else:
    print("⚠️  Monitor thread is not running")
```

---

## Advanced Usage

### Custom Idle Detection

```python
def custom_idle_check():
    """Custom idle detection logic"""
    now = datetime.now()

    for phone_id, last_time in manager.last_activity.items():
        if phone_id in manager.whitelist:
            continue  # Skip whitelisted

        idle_time = now - last_time

        # Custom logic: different timeouts for different devices
        if "Android15" in manager._display_names.get(phone_id, ""):
            timeout = timedelta(minutes=10)  # 10 minutes for Android15
        else:
            timeout = manager.timeout

        if idle_time > timeout:
            display_name = manager._display_names.get(phone_id, phone_id)
            print(f"⚠️  {display_name}: Idle for {idle_time.seconds // 60} minutes")
            manager.stop_phone(phone_id)
```

### Monitor Specific Devices

```python
# Only monitor specific devices (use phone_id)
target_devices = ["phone_id_123", "phone_id_456"]

for phone_id in list(manager.last_activity.keys()):
    if phone_id not in target_devices:
        # Skip non-target devices
        continue

    idle_time = now - manager.last_activity[phone_id]
    if idle_time > manager.timeout:
        manager.stop_phone(phone_id)
```

---

## Common Scenarios

### Scenario 1: Batch Processing

```python
manager = PhoneManager(timeout_minutes=5)
manager.start_monitor()

# Process 10 devices in batch
for i in range(10):
    phone_id = f"phone_id_{i}"
    device_name = f"Android{i}"

    # Connect and process (use phone_id)
    d = manager.connect_device(phone_id, ip, port, pwd, name=device_name)
    process_data(d)

    # Move to next device
    # Previous devices will auto-close after 5 minutes
```

### Scenario 2: Long-Running Monitoring

```python
manager = PhoneManager(timeout_minutes=10)
manager.start_monitor()

# Add monitoring device to whitelist (use phone_id)
manager.add_to_whitelist("monitoring_phone_id")

# Monitor continuously
while True:
    check_all_devices()
    manager.record_activity("monitoring_phone_id")
    time.sleep(60)
```

### Scenario 3: Interactive Testing

```python
manager = PhoneManager(timeout_minutes=2)
manager.start_monitor()

# Interactive testing loop
while True:
    command = input("Enter command (or 'quit'): ")

    if command == "quit":
        break

    # Execute command
    execute_command(command)

    # Record activity to prevent auto-close (use phone_id)
    manager.record_activity("phone_id_123")
```

---

## Troubleshooting

### Device Not Auto-Closing

**Problem**: Device should auto-close but doesn't.

**Solutions**:
1. Check if device is whitelisted
2. Check if activity is being recorded
3. Check monitor thread is running

```python
# Check whitelist
print(f"Whitelist: {manager.whitelist}")

# Check activity
print(f"Last activity: {manager.last_activity}")

# Check monitor thread
if manager.monitor_thread and manager.monitor_thread.is_alive():
    print("✅ Monitor thread is running")
else:
    print("❌ Monitor thread is not running")
```

### Device Closing Too Early

**Problem**: Device closes before expected.

**Solutions**:
1. Increase timeout
2. Record activity more frequently

```python
# Increase timeout
manager = PhoneManager(timeout_minutes=10)

# Record activity more frequently (use phone_id)
for i in range(100):
    process_item(i)
    if i % 5 == 0:  # Every 5 iterations
        manager.record_activity("phone_id_123")
```

### Monitor Thread Not Starting

**Problem**: Background monitor doesn't start.

**Solution**:
```python
# Check if start_monitor() was called
manager.start_monitor()

# Verify thread is running
if manager.monitor_thread and manager.monitor_thread.is_alive():
    print("✅ Monitor thread is running")
else:
    print("❌ Monitor thread failed to start")
```

---

## API Reference

### PhoneManager Class

```python
class PhoneManager:
    def __init__(self, timeout_minutes: int = 5, token: str = None, base_url: str = None, task_name: str = "phone_manager"):
        """
        Initialize PhoneManager.

        Args:
            timeout_minutes: Timeout in minutes before auto-closing
            [REDACTED_SECRET] API token (optional, reads from config if not provided)
            base_url: API base URL (optional)
            task_name: Name for logging context
        """

    def connect_device(self, phone_id: str, ip: str, port: str, pwd: str, name: str = None) -> u2.Device:
        """
        Connect to cloud phone and record activity.

        Args:
            phone_id: Cloud phone ID (required, used for all operations)
            ip: ADB IP address
            port: ADB port
            pwd: ADB password
            name: Display name (optional, for logging only)

        Returns:
            uiautomator2.Device instance
        """

    def record_activity(self, phone_id: str):
        """
        Record cloud phone activity time.

        Args:
            phone_id: Cloud phone ID
        """

    def stop_phone(self, phone_id: str) -> bool:
        """
        Stop cloud phone.

        Args:
            phone_id: Cloud phone ID

        Returns:
            True if stopped successfully, False otherwise
        """

    def add_to_whitelist(self, phone_id: str):
        """
        Add device to whitelist (not auto-closed).

        Args:
            phone_id: Cloud phone ID
        """

    def remove_from_whitelist(self, phone_id: str):
        """
        Remove device from whitelist.

        Args:
            phone_id: Cloud phone ID
        """

    def start_monitor(self, check_interval: int = 60):
        """
        Start background monitoring thread.

        Args:
            check_interval: Check interval in seconds (default: 60)
        """

    def stop_monitor(self):
        """
        Stop background monitoring thread.
        """

    def stop_all(self):
        """
        Stop all active cloud phones and save logs.
        """
```

---

## Last Updated

2026-04-25

**Related Documents**:
- [Best Practices](best_practices.md) - Safety and performance tips
- [Phone Manager Script](../scripts/phone_manager.py) - Complete implementation

path: references/api_reference.md
content: # GeeLark API Reference

Complete list of all verified GeeLark API endpoints.

---

## Base URL

**Base URL**: `https://openapi.geelark.com`

**Example**: `https://openapi.geelark.com/open/v1/phone/list`

---

## Authentication

All API requests require authentication using one of two methods:

### Method 1: Token Authentication (Recommended)

**Headers**:
```
Content-Type: application/json
traceId: <UUID v4>
Authorization: Bearer <your_token>
```

**Python Example**:
```python
import requests
import uuid

headers = {
    "Content-Type": "application/json",
    "traceId": str(uuid.uuid4()),
    "Authorization": "Bearer your_token_here"
}

response = requests.post(
    "https://openapi.geelark.com/open/v1/phone/list",
    headers=headers,
    json={"page": 1, "pageSize": 10}
)
```

### Method 2: Key Authentication

**Headers**:
```
Content-Type: application/json
appId: <your_appId>
traceId: <UUID v4>
ts: <millisecond timestamp>
nonce: <first 6 chars of traceId>
sign: <SHA256(appId + traceId + ts + nonce + apiKey) uppercase hex>
```

**Python Example**:
```python
import requests
import uuid
import time
import hashlib

APP_ID = "your_app_id"
[REDACTED_SECRET]"

trace_id = str(uuid.uuid4())
ts = str(int(time.time() * 1000))
nonce = trace_id[:6]

sign_raw = f"{APP_ID}{trace_id}{ts}{nonce}{API_KEY}"
sign = hashlib.sha256(sign_raw.encode()).hexdigest().upper()

headers = {
    "Content-Type": "application/json",
    "appId": APP_ID,
    "traceId": trace_id,
    "ts": ts,
    "nonce": nonce,
    "sign": sign
}

response = requests.post(
    "https://openapi.geelark.com/open/v1/phone/list",
    headers=headers,
    json={"page": 1, "pageSize": 10}
)
```

---

## Request Format

- **Method**: All endpoints use `POST`
- **Content-Type**: `application/json`
- **Rate Limit**: 
  - 200 requests per minute
  - 24,000 requests per hour
  - ⚠️ Exceeding limit locks the endpoint for 2 hours

---

## Response Format

All API responses follow this structure:

```json
{
  "traceId": "uuid-string",
  "code": 0,
  "msg": "success",
  "data": {
    // Response data (null on error)
  }
}
```

**Fields**:
- `traceId` - Unique request ID for debugging
- `code` - Status code (`0` = success, non-zero = error)
- `msg` - Status message
- `data` - Response data (contains result on success, error details on failure)

---

## Error Codes

### Common Error Codes

| Code | Description | Category |
|------|-------------|----------|
| `0` | Success | General |
| `40000` | Unknown error | General |
| `40001` | Request body read failed | General |
| `40002` | traceId is empty | General |
| `40003` | Signature verification failed | General |
| `40004` | Parameter validation failed | General |
| `40005` | Resource not found | General |
| `40006` | Batch operation partially succeeded | General |
| `40007` | Request rate limited (1 min) | Rate Limit |
| `40008` | pageSize exceeds limit | General |
| `40009` | Batch operation all failed | General |
| `40010` | Resource occupied, cannot delete | General |
| `40011` | Paid users only | General |
| `40012` | API deprecated | General |
| `40013` | User not found | General |
| `40014` | Hourly limit exceeded (2 hour lock) | Rate Limit |
| `40015` | Insufficient permissions | General |
| `40016` | IP not in whitelist | General |
| `40017` | Too frequent, retry later | Rate Limit |
| `50000` | Server error | General |

### Cloud Phone Error Codes

| Code | Description |
|------|-------------|
| `42001` | Cloud phone does not exist |
| `42002` | Cloud phone not in running state |
| `42003` | Installing corresponding app |
| `42004` | Cannot install lower version |
| `42005` | App not installed |
| `42006` | App does not exist |
| `42007` | Upload task not found |
| `42008` | Wrong app version, only specific version allowed |
| `43001` | Has associated environments, cannot delete |
| `43002` | User not on pro plan |
| `43004` | Cloud phone expired |
| `43005` | Cloud phone executing task, cannot delete/reset |
| `43006` | Cloud phone under remote control, cannot delete/reset |
| `43008` | Daily open limit reached (UTC timezone) |
| `43009` | Cloud phone already running, cannot delete/reset |
| `43010` | Cloud phone starting, cannot delete/reset |
| `43011` | Cloud phone executing reset, cannot operate |
| `43012` | Cloud phone GPS info error |
| `43013` | Plugin installation task does not exist |
| `43014` | GPS not set |
| `43015` | Cloud phone does not support reset |
| `43016` | Cloud phone does not support root |
| `43017` | No monthly subscription device |
| `43018` | Cloud phone not bound to monthly device |
| `43019` | Only monthly subscription supported |
| `43020` | Cloud phone backup data error / Tag name empty |
| `43021` | Cloud phone in use, cannot delete / Tag already exists |
| `43022` | Cannot transfer environment to self / Tag does not exist |
| `43023` | Environment name empty / Tag color does not exist |
| `43024` | Brand/model mismatch |
| `43025` | Language not supported |
| `43026` | System not supported |
| `43027` | Browser does not support transfer |
| `43028` | Sub-user environment group limit |
| `43029` | Device model under maintenance |
| `43030` | Group name empty |
| `43031` | Group already exists |
| `43032` | Group does not exist |
| `43033` | Group name not found |
| `43034` | Group remark not found |
| `43035` | Cannot edit ungrouped |
| `43036` | Cloud phone running, cannot execute operation |
| `43037` | Accessibility hiding not supported |
| `43038` | Device model deleted |
| `43039` | Authorization under maintenance |
| `49001` | ADB not enabled |
| `49002` | Device model does not support ADB |
| `50001` | Device model does not support shell |
| `52001` | Device model does not support SMS |
| `53001` | Device model does not support patch installation |

### Task Error Codes

| Code | Description |
|------|-------------|
| `41000` | Insufficient task credits |
| `41001` | Insufficient balance |
| `48000` | Too many task retries |
| `48001` | Task already succeeded/failed, cannot operate |
| `48002` | Task does not exist |
| `48003` | Task material expired |
| `48004` | TikTok task app version mismatch |

### Material & Webhook Error Codes

| Code | Description |
|------|-------------|
| `60001` | Material library capacity exceeded |
| `60002` | Duplicate tag name |
| `60003` | Invalid link |
| `60004` | File format not supported |
| `60005` | Material does not exist |
| `51001` | User callback does not exist |

### Proxy Error Codes

| Code | Description |
|------|-------------|
| `45001` | Proxy does not exist |
| `45002` | Proxy unavailable |
| `45003` | Proxy not supported (China mainland IP) |
| `45004` | Proxy verification failed |
| `45005` | Region not supported |
| `45006` | Proxy info error |
| `45007` | Duplicate proxy |
| `45008` | Proxy type not supported |

### Plan Error Codes

| Code | Description |
|------|-------------|
| `44001` | Pro plan limit |
| `44002` | Plan environment limit reached |
| `44003` | User environment limit reached |
| `44004` | Daily environment creation limit reached |
| `44005` | Group not filled |
| `44006` | Cloud phone inventory insufficient |
| `46001` | Plan expired |
| `46002` | Plan team environment limit exceeded |
| `46003` | Environment count exceeds plan limit |

### Device Error Codes

| Code | Description |
|------|-------------|
| `47001` | Device concurrency limit |
| `47002` | Devices exhausted |
| `47003` | Associated device expired |
| `47004` | Associated device does not exist |

---

## Cloud Phone Management

| Endpoint | Method | Parameters | Description | Status |
|----------|--------|------------|-------------|--------|
| `/open/v1/phone/list` | POST | `{"page":1,"pageSize":10}` | List all cloud phones | ✅ |
| `/open/v1/phone/status` | POST | `{"ids":["phoneId"]}` | Get cloud phone status | ✅ |
| `/open/v1/phone/start` | POST | `{"ids":["phoneId"]}` | Start cloud phone(s) | ✅ |
| `/open/v1/phone/stop` | POST | `{"ids":["phoneId"]}` | Stop cloud phone(s) | ✅ |
| `/open/v1/phone/delete` | POST | `{"ids":["phoneId"]}` | Delete cloud phone(s) | ✅ ⚠️ |
| `/open/v1/phone/gps/set` | POST | `{"list":[{"id":"xxx","latitude":1.2,"longitude":1.2}]}` | Set GPS location | ✅ |
| `/open/v1/phone/addNew` | POST | `{"mobileType":"Android 11","data":[{"profileName":"myPhone"}]}` | Create new cloud phone | ✅ |
| `/open/v1/phone/screenShot` | POST | `{"id":"phoneId"}` | Take screenshot | ✅ |

**Pagination Parameters**:
- `page` and `pageSize` are optional for list endpoints
- Default values vary by endpoint (typically page=1, pageSize=10)
- `pageSize` has maximum limit (check official docs, typically 100)
- Error 40008 indicates pageSize exceeds allowed range

**Important Notes**:
- `/open/v1/phone/delete` requires cloud phones to be stopped first (error 43009 if running)
- **Batch operations**: 
  - `start`, `stop`, `delete` support multiple IDs in array: `{"ids":["id1","id2","id3"]}`
  - Single device limit: Check official docs for maximum batch size
  - Batch operations are NOT atomic - individual devices may succeed/fail independently
  - Check response `data` field for per-device results on batch operations

---

## Balance & Billing

| Endpoint | Method | Parameters | Description | Status |
|----------|--------|------------|-------------|--------|
| `/open/v1/pay/wallet` | POST | `{}` | Get account balance | ✅ |
| [REDACTED_SECRET] | POST | `{}` | Get billing transactions | ✅ |

---

## Group / Tag / Proxy

### Groups

| Endpoint | Method | Parameters | Description | Status |
|----------|--------|------------|-------------|--------|
| `/open/v1/group/list` | POST | `{"page":1,"pageSize":10}` | List groups | ✅ |
| `/open/v1/group/delete` | POST | `{"ids":["groupId"]}` | Delete group(s) | ✅ |

### Tags

| Endpoint | Method | Parameters | Description | Status |
|----------|--------|------------|-------------|--------|
| `/open/v1/tag/delete` | POST | `{"ids":["tagId"]}` | Delete tag(s) | ✅ |

### Proxies

| Endpoint | Method | Parameters | Description | Status |
|----------|--------|------------|-------------|--------|
| `/open/v1/proxy/add` | POST | `{"list":[{"scheme":"socks5","server":"1.2.3.4","port":8000}]}` | Add proxy | ✅ |
| `/open/v1/proxy/delete` | POST | `{"ids":["proxyId"]}` | Delete proxy | ✅ |

---

## Application Management

| Endpoint | Method | Parameters | Description | Status |
|----------|--------|------------|-------------|--------|
| `/open/v1/app/installable/list` | POST | `{"envId":"xxx","name":"","page":1,"pageSize":100}` | List installable apps | ✅ |
| `/open/v1/app/install` | POST | `{"envId":"xxx","appVersionId":"xxx"}` | Install app | ✅ |
| `/open/v1/app/list` | POST | `{"envId":"xxx","page":1,"pageSize":10}` | List installed apps | ✅ |
| `/open/v1/app/start` | POST | `{"envId":"xxx","packageName":"com.xxx"}` | Start app | ✅ |
| `/open/v1/app/stop` | POST | `{"envId":"xxx","packageName":"com.xxx"}` | Stop app | ✅ |
| `/open/v1/app/uninstall` | POST | `{"envId":"xxx","packageName":"com.xxx"}` | Uninstall app | ✅ |

**Critical Notes**:
- `/open/v1/app/install` requires `appVersionId`, NOT `appName`. Get `appVersionId` from `/open/v1/app/installable/list` response.
- `envId` is the cloud phone ID (same as `id` from `/open/v1/phone/list` response)
- **Batch operations**: Some endpoints support batch operations with arrays (check individual endpoint docs)

---

## ADB

| Endpoint | Method | Parameters | Description | Status |
|----------|--------|------------|-------------|--------|
| `/open/v1/adb/getData` | POST | `{"ids":["phoneId"]}` | Get ADB connection info | ✅ |
| `/open/v1/adb/setStatus` | POST | `{"ids":["phoneId"],"open":true}` | Enable/disable ADB | ✅ |

---

## Task Management

| Endpoint | Method | Parameters | Description | Status |
|----------|--------|------------|-------------|--------|
| `/open/v1/task/query` | POST | `{"ids":["taskId"]}` | Query task status | ✅ |
| `/open/v1/task/detail` | POST | `{"ID":"taskId"}` | Get task details | ⚠️ |
| `/open/v1/task/cancel` | POST | `{"ids":["taskId"]}` | Cancel task | ⚠️ |
| `/open/v1/task/restart` | POST | `{"IDs":["taskId"]}` | Restart task | ⚠️ |
| `/open/v1/task/historyRecords` | POST | `{"page":1,"pageSize":10}` | Get task history | ✅ |
| `/open/v1/task/flow/list` | POST | `{"page":1,"pageSize":10}` | List task flows | ✅ |
| `/open/v1/task/flow/export` | POST | `{"ID":"flowId"}` | Export task flow | ⚠️ |
| `/open/v1/task/flow/import` | POST | `{"Gal":{}}` | Import task flow | ⚠️ |
| `/open/v1/task/rpa/add` | POST | `{"flowId":"xxx","paramMap":{}}` | Add RPA task | ⚠️ |

**⚠️ Important Notes**:
- Parameters marked with ⚠️ require valid task/flow IDs
- **Field name inconsistency**: Some endpoints use `ids` (lowercase), others use `ID` or `IDs` (uppercase)
  - `query` and `cancel` use `ids`
  - `detail`, `restart`, `flow/export` use `ID` or `IDs`
  - Always check the exact field name for each endpoint

---

## RPA Tasks - TikTok

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| `/open/v1/rpa/task/tiktokLogin` | `account`, `password` | `name`, `remark`, `scheduleAt`, `id` | Login to TikTok |
| [REDACTED_SECRET] | `followProbability` (0-100) | `name`, `remark`, `scheduleAt`, `id` | Randomly follow users |
| [REDACTED_SECRET] | `followProbability` | `name`, `remark`, `scheduleAt`, `id` | Randomly follow (Asia) |
| [REDACTED_SECRET] | - | `name`, `remark`, `scheduleAt`, `id` | Randomly like videos |
| [REDACTED_SECRET] | - | `name`, `remark`, `scheduleAt`, `id` | Randomly like (Asia) |
| [REDACTED_SECRET] | `useAi` (1=AI, 2=manual), `comment` | `name`, `remark`, `scheduleAt`, `id` | Randomly comment |
| [REDACTED_SECRET] | `useAi` (1=AI, 2=manual), `comment` | `name`, `remark`, `scheduleAt`, `id` | Randomly comment (Asia) |
| `/open/v1/rpa/task/tiktokEdit` | - | `name`, `remark`, `scheduleAt`, `id` | Edit video |
| `/open/v1/rpa/task/tiktokDel` | - | `name`, `remark`, `scheduleAt`, `id` | Delete video |
| `/open/v1/rpa/task/tiktokDelAsia` | - | `name`, `remark`, `scheduleAt`, `id` | Delete video (Asia) |
| `/open/v1/rpa/task/tiktokHide` | - | `name`, `remark`, `scheduleAt`, `id` | Hide video |
| [REDACTED_SECRET] | - | `name`, `remark`, `scheduleAt`, `id` | Hide video (Asia) |

**Common Parameters**:
```json
{
  "name": "task_name",
  "remark": "task_remark",
  "scheduleAt": 1741846843,  // Unix timestamp
  "id": "phoneId"
}
```

**Complete Request Example**:
```python
import requests

# TikTok Login Task Example
headers = {
    "Content-Type": "application/json",
    "traceId": "your-uuid-here",
    "Authorization": "Bearer your_token"
}

payload = {
    "account": "[REDACTED_SECRET]",
    "password": "your_password",
    "name": "tiktok_login_task",
    "remark": "Auto login task",
    "id": "phone_id_here"
}

response = requests.post(
    "https://openapi.geelark.com/open/v1/rpa/task/tiktokLogin",
    headers=headers,
    json=payload
)

result = response.json()
if result["code"] == 0:
    print(f"✅ Task created: {result['data']}")
else:
    print(f"❌ Failed: {result['msg']}")
```

---

## RPA Tasks - Instagram

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| [REDACTED_SECRET] | `account`, `password` | `name`, `remark`, `scheduleAt`, `id` | Login to Instagram |
| [REDACTED_SECRET] | `description`, `video[]` | `name`, `remark`, `scheduleAt`, `id` | Publish reels |
| [REDACTED_SECRET] | `Image[]` | `name`, `remark`, `scheduleAt`, `id` | Publish reels with images |
| `/open/v1/rpa/task/instagramEdit` | - | `name`, `remark`, `scheduleAt`, `id` | Edit post |
| [REDACTED_SECRET] | - | `name`, `remark`, `scheduleAt`, `id` | Account warmup |

**Important Notes**:
- Field name is `Image` (capital I, not `images`) for `instagramPubReelsImages`
- `Image[]` is an array type
- `video[]` in `instagramPubReels` is also an array

---

## RPA Tasks - Facebook

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| `/open/v1/rpa/task/faceBookLogin` | `Email`, `Password` | `name`, `remark`, `scheduleAt`, `id` | Login to Facebook |
| [REDACTED_SECRET] | `title`, `video[]` | `name`, `remark`, `scheduleAt`, `id` | Publish video |
| [REDACTED_SECRET] | `title`, `video` (string) | `name`, `remark`, `scheduleAt`, `id` | Publish reels |
| [REDACTED_SECRET] | `PostAddress`, `Comment`, `Keyword` | `name`, `remark`, `scheduleAt`, `id` | Auto comment |
| [REDACTED_SECRET] | `BrowsePostsNum`, `Keyword` | `name`, `remark`, `scheduleAt`, `id` | Active account |

**Important Notes**:
- Field name is `Email` (not `account`) for `faceBookLogin`
- **Video field type inconsistency**:
  - `faceBookPublish` uses `video[]` (array)
  - `faceBookPubReels` uses `video` (string)
  - Always check the exact field type for each endpoint

---

## RPA Tasks - YouTube

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| [REDACTED_SECRET] | `title`, `video` (string) | `sameStyleVoice`, `originalVoice`, `name`, `remark`, `scheduleAt`, `id` | Publish short |
| [REDACTED_SECRET] | `title`, `video` | `description`, `name`, `remark`, `scheduleAt`, `id` | Publish video |
| [REDACTED_SECRET] | `BrowseVideoNum`, `Keyword` | `name`, `remark`, `scheduleAt`, `id` | Active account |

**Important Notes**:
- YouTube uses `video` as a **string** (not array) in `youtubePubShort` and `youtubePubVideo`
- This differs from other platforms that use `video[]` (array)
- Always verify the expected type before sending requests

---

## RPA Tasks - Reddit

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| `/open/v1/rpa/task/redditImage` | `title`, `images[]`, `community` | `name`, `remark`, `scheduleAt`, `id` | Publish image |
| `/open/v1/rpa/task/redditVideo` | `title`, `video[]`, `community` | `name`, `remark`, `scheduleAt`, `id` | Publish video |
| `/open/v1/rpa/task/redditWarmup` | - | `name`, `remark`, `scheduleAt`, `id` | Account warmup |

---

## RPA Tasks - Threads

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| `/open/v1/rpa/task/threadsImage` | `title`, `images[]` | `name`, `remark`, `scheduleAt`, `id` | Publish image |
| `/open/v1/rpa/task/threadsVideo` | `title`, `video[]` | `name`, `remark`, `scheduleAt`, `id` | Publish video |

---

## RPA Tasks - Pinterest

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| [REDACTED_SECRET] | `title`, `description`, `images[]` | `name`, `remark`, `scheduleAt`, `id` | Publish image |
| [REDACTED_SECRET] | `title`, `description`, `video[]` | `name`, `remark`, `scheduleAt`, `id` | Publish video |

---

## RPA Tasks - X (Twitter)

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| `/open/v1/rpa/task/xPublish` | `description`, `video[]` | `name`, `remark`, `scheduleAt`, `id` | Publish post |

---

## RPA Tasks - Google

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| `/open/v1/rpa/task/googleLogin` | `Email`, `Password` | `name`, `remark`, `scheduleAt`, `id` | Login to Google |
| [REDACTED_SECRET] | `appName` | `name`, `remark`, `scheduleAt`, `id` | Download app |
| [REDACTED_SECRET] | `appName` | `name`, `remark`, `scheduleAt`, `id` | Browse app |

---

## RPA Tasks - Shein

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| `/open/v1/rpa/task/sheinLogin` | `Email`, `Password` | `name`, `remark`, `scheduleAt`, `id` | Login to Shein |

---

## File Upload / Other Tasks

| Endpoint | Required Parameters | Optional Parameters | Description |
|----------|-------------------|---------------------|-------------|
| `/open/v1/rpa/task/fileUpload` | `Files[]` | `name`, `remark`, `scheduleAt`, `id` | Upload files |
| `/open/v1/rpa/task/keyboxUpload` | `Files[]` | `name`, `remark`, `scheduleAt`, `id` | Upload keybox |
| [REDACTED_SECRET] | `Contacts[]` | `name`, `remark`, `scheduleAt`, `id` | Import contacts |
| [REDACTED_SECRET] | `Title`, `Video[]` | `name`, `remark`, `scheduleAt`, `id` | Multi-platform distribution |

**Important Notes**:
- `Files[]` and `Contacts[]` are array types
- **File upload limits**: Check official docs for size limits and supported formats
- **Supported formats**: Typically MP4, PNG, JPG for media; CSV for contacts
- **File upload workflow**: 
  1. Upload file to GeeLark storage (separate endpoint)
  2. Get file URL/ID from upload response
  3. Use URL/ID in RPA task parameters

---

## Last Updated

2026-04-25

**Official Documentation**: https://github.com/GeeLark/geelark-openapi

path: references/error_codes.md
content: # GeeLark API Error Codes

Complete list of GeeLark API error codes and their solutions.

---

## Error Code Reference

| Code | Description | Severity | Action |
|------|-------------|----------|--------|
| `0` | Success | ✅ | - |
| `40004` | Parameter validation failed | 🔴 | Check request parameters |
| `40005` | Resource not found | 🔴 | Verify resource exists |
| `41001` | Balance not enough | 🔴 | Recharge account |
| `42001` | Cloud phone does not exist | 🔴 | Verify phone ID |
| `42002` | Cloud phone is not running | 🟡 | Start cloud phone first |
| `42006` | App not installed | 🟡 | Install app first |
| `43002` | Pro plan required | 🟡 | Upgrade to Pro plan |
| `43009` | Cloud phone is started | 🔴 | Stop before deleting |
| `43020` | Cloud phone currently unavailable | 🔴 | Try again later |
| `43029` | Device model under maintenance | 🟡 | Try again later |
| `44001` | Cannot batch create | 🔴 | Upgrade to Pro plan |
| `44002` | Reached max creation limit | 🔴 | Upgrade plan or wait |
| `44004` | Reached daily max creation limit | 🟡 | Wait until tomorrow |
| `45006` | Wrong argument | 🔴 | Check parameter format |
| `47002` | Cloud phone resource shortage | 🔴 | Try again later |

---

## Critical Error Codes ⭐⭐⭐

### 41001 - Balance Not Enough

**Description**: Account balance is insufficient to perform the operation.

**Cause**:
- Account balance ≤ 0
- Gift money ≤ 0
- Available time add-on ≤ 0

**Action**:
1. Recharge account at https://open.geelark.com/billing
2. Use `/open/v1/pay/wallet` to check balance
3. Wait for recharge to process

**Example**:
```python
# Check balance
wallet = client.wallet()
balance = wallet['data']['balance']
gift_money = wallet['data'].get('giftMoney', 0)

if balance <= 0 and gift_money <= 0:
    print("❌ Insufficient balance! Please recharge.")
    exit(1)
```

**Historical Error** (2026-04-12):
- Agent deleted cloud phones without authorization
- Root cause: Didn't check `failDetails` in API response
- API returned code 41001 but agent assumed "cloud phone is broken"

---

### 43009 - Cloud Phone Is Started

**Description**: Cannot delete a running cloud phone.

**Cause**:
- Trying to delete a cloud phone that is still running
- API returns `code: 0` but `failAmount > 0` with error 43009

**Action**:
1. Stop the cloud phone first
2. Wait 30-60 seconds for shutdown
3. Then delete

**Example**:
```python
# Step 1: Stop running phones
client.phone_stop([phone_id])

# Step 2: Wait for shutdown
time.sleep(60)

# Step 3: Then delete
client.call("/open/v1/phone/delete", {"ids": [phone_id]})
```

**Correct Order**: Stop → Wait → Delete

---

### 45006 - Wrong Argument

**Description**: Parameter format or value is incorrect.

**Common Causes**:
- `proxyNumber` is string, should be int
- Proxy format is invalid
- Missing required parameters

**Action**:
1. Check parameter types
2. Verify proxy format: `socks5://user:pass@host:port`
3. Ensure all required parameters are present

**Example**:
```python
# ❌ Wrong (proxyNumber as string)
data = {"mobileType": "Android 13", "data": [{"profileName": "phone", "proxyNumber": "6"}]}

# ✅ Correct (proxyNumber as int)
data = {"mobileType": "Android 13", "data": [{"profileName": "phone", "proxyNumber": 6}]}
```

---

## Common Error Patterns

### Fail Details Not Checked ⭐⭐⭐

**Problem**: API returns `code: 0` but operation actually failed.

**Solution**: Always check `failAmount` and `failDetails` in response:

```python
response = client.call("/open/v1/phone/start", {"ids": [phone_id]})

# Check for failures
fail_amount = response.get('data', {}).get('failAmount', 0)
if fail_amount > 0:
    for fail in response['data']['failDetails']:
        error_code = fail.get('code')
        error_msg = fail.get('msg')
        print(f"❌ Failed: code={error_code}, msg={error_msg}")

        # Handle specific errors
        if error_code == 41001:
            print("   → Balance not enough, recharge required")
        elif error_code == 43009:
            print("   → Cloud phone is running, stop first")
```

---

### Cloud Phone Not Ready

**Problem**: Trying to operate on a stopped or unavailable cloud phone.

**Solution**: Use `ensure_cloud_phone_ready()` function:

```python
from scripts.geelark_boot_helper import boot_and_connect

# This function handles:
# - Balance check
# - Cloud phone status check
# - Start cloud phone (if stopped)
# - Enable ADB (if disabled)
# - Return ADB connection info

adb_info = boot_and_connect(phone_id, token)

if not adb_info:
    print("❌ Failed to boot cloud phone")
    exit(1)
```

---

### App Not Installed

**Problem**: Trying to start an app that is not installed.

**Solution**:
1. List installed apps: `/open/v1/app/list`
2. Install app if needed: `/open/v1/app/install`
3. Then start: `/open/v1/app/start`

```python
# Check if app is installed
apps = client.call("/open/v1/app/list", {
    "envId": phone_id,
    "page": 1,
    "pageSize": 100
})

app_found = any(app['packageName'] == target_package for app in apps['data']['items'])

if not app_found:
    print(f"⚠️  App not installed, installing...")
    # Install app
    client.call("/open/v1/app/install", {
        "envId": phone_id,
        "appVersionId": app_version_id
    })
```

---

## Error Handling Best Practices

### 1. Always Check Response Code

```python
response = client.call(endpoint, data)

if response['code'] != 0:
    error_msg = response.get('msg', 'Unknown error')
    print(f"❌ API Error: {error_msg}")
    exit(1)
```

### 2. Check Fail Details

```python
if response.get('data', {}).get('failAmount', 0) > 0:
    for fail in response['data']['failDetails']:
        error_code = fail.get('code')
        # Handle specific error codes
```

### 3. Implement Retry Logic

```python
import time

max_retries = 3
for attempt in range(max_retries):
    response = client.call(endpoint, data)

    if response['code'] == 0:
        break

    if response['code'] in [42006, 47002]:  # Transient errors
        print(f"⚠️  Retrying ({attempt + 1}/{max_retries})...")
        time.sleep(10)
    else:
        break
```

### 4. Use Context Managers

```python
from scripts.geelark_client import GeeLarkClient

# Auto-closes started phones
with GeeLarkClient() as client:
    client.phone_start([phone_id])
    # Your operations here

# Automatically stops all started phones
```

---

## Debugging Tips

### Enable Debug Mode

```python
import logging
logging.basicConfig(level=logging.DEBUG)
```

### Log API Calls

```python
from scripts.cloudphone_logger import CloudPhoneLogger

with CloudPhoneLogger("task_name", phone_id) as log:
    log.api_call(endpoint, data, response_code, response_data)
    # All operations logged automatically
```

### Check Balance Before Operations

```python
wallet = client.wallet()
balance = wallet['data']['balance']

if balance <= 0:
    print("❌ Insufficient balance")
    # Handle low balance
```

---

## Last Updated

2026-04-10

**Related Documents**:
- [API Reference](api_reference.md) - Complete endpoint list
- [Best Practices](best_practices.md) - Safety and performance tips

path: references/rpa_tasks.md
content: # RPA Tasks Guide

Complete guide to GeeLark's built-in RPA (Robotic Process Automation) tasks for 9 major platforms.

---

## Overview

GeeLark provides pre-built RPA tasks for automating operations on:

- **TikTok** - Login, follow, like, comment, edit, delete
- **Instagram** - Login, publish reels, warmup
- **Facebook** - Login, publish, auto-comment, active account
- **YouTube** - Publish short, publish video, active account
- **Reddit** - Publish image/video, warmup
- **Threads** - Publish image/video
- **Pinterest** - Publish image/video
- **X (Twitter)** - Publish post
- **Google** - Login, app download, app browse

---

## Common Parameters

All RPA tasks use these common parameters:

```json
{
  "name": "task_name",
  "remark": "task_remark",
  "scheduleAt": 1741846843,  // Unix timestamp (required)
  "id": "phoneId"  // Cloud phone ID (required)
}
```

**Note**: `scheduleAt` is required for all RPA tasks. Use `Math.floor(Date.now() / 1000) + delay` to schedule future tasks.

---

## TikTok

### Login

```python
client.rpa_task("tiktokLogin", {
    "id": phone_id,
    "account": "[REDACTED_SECRET]",
    "password": "123456",
    "scheduleAt": int(time.time()) + 60,
    "name": "TikTok Login"
})
```

### Random Follow

```python
client.rpa_task("tiktokRandomFollow", {
    "id": phone_id,
    "followProbability": 30,  // 0-100, probability of following
    "scheduleAt": int(time.time()) + 60,
    "name": "Random Follow"
})
```

### Random Like

```python
client.rpa_task("tiktokRandomStar", {
    "id": phone_id,
    "scheduleAt": int(time.time()) + 60,
    "name": "Random Like"
})
```

### Random Comment

```python
client.rpa_task("tiktokRandomComment", {
    "id": phone_id,
    "useAi": 1,  // 1 = AI comment, 2 = manual comment
    "comment": "Nice video!",  // Required if useAi=2
    "scheduleAt": int(time.time()) + 60,
    "name": "Random Comment"
})
```

### Edit Video

```python
client.rpa_task("tiktokEdit", {
    "id": phone_id,
    "scheduleAt": int(time.time()) + 60,
    "name": "Edit Video"
})
```

### Delete Video

```python
client.rpa_task("tiktokDel", {
    "id": phone_id,
    "scheduleAt": int(time.time()) + 60,
    "name": "Delete Video"
})
```

### Hide Video

```python
client.rpa_task("tiktokHide", {
    "id": phone_id,
    "scheduleAt": int(time.time()) + 60,
    "name": "Hide Video"
})
```

---

## Instagram

### Login

```python
client.rpa_task("instagramLogin", {
    "id": phone_id,
    "account": "[REDACTED_SECRET]",
    "password": "123456",
    "scheduleAt": int(time.time()) + 60,
    "name": "Instagram Login"
})
```

### Publish Reels (Video)

```python
client.rpa_task("instagramPubReels", {
    "id": phone_id,
    "description": "Amazing reels!",
    "video": ["/path/to/video.mp4"],  // Array of file paths
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Reels"
})
```

### Publish Reels (Images)

```python
client.rpa_task("instagramPubReelsImages", {
    "id": phone_id,
    "Image": ["/path/to/image1.jpg", "/path/to/image2.jpg"],  // Note: Capital 'I'
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Reels Images"
})
```

**Note**: Field name is `Image` (not `images`) for `instagramPubReelsImages`

### Edit Post

```python
client.rpa_task("instagramEdit", {
    "id": phone_id,
    "scheduleAt": int(time.time()) + 60,
    "name": "Edit Post"
})
```

### Account Warmup

```python
client.rpa_task("instagramWarmup", {
    "id": phone_id,
    "scheduleAt": int(time.time()) + 60,
    "name": "Account Warmup"
})
```

---

## Facebook

### Login

```python
client.rpa_task("faceBookLogin", {
    "id": phone_id,
    "Email": "[REDACTED_SECRET]",  // Note: 'Email' not 'account'
    "Password": "123456",
    "scheduleAt": int(time.time()) + 60,
    "name": "Facebook Login"
})
```

**Note**: Field name is `Email` (not `account`) for `faceBookLogin`

### Publish Video

```python
client.rpa_task("faceBookPublish", {
    "id": phone_id,
    "title": "My Video",
    "video": ["/path/to/video.mp4"],  // Array of file paths
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Video"
})
```

### Publish Reels

```python
client.rpa_task("faceBookPubReels", {
    "id": phone_id,
    "title": "My Reels",
    "video": "/path/to/video.mp4",  // String, not array
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Reels"
})
```

**Note**: Facebook Reels uses `video` as a string, not an array

### Auto Comment

```python
client.rpa_task("faceBookAutoComment", {
    "id": phone_id,
    "PostAddress": "https://facebook.com/post/123",
    "Comment": "Great post!",
    "Keyword": "amazing",
    "scheduleAt": int(time.time()) + 60,
    "name": "Auto Comment"
})
```

### Active Account

```python
client.rpa_task("faceBookActiveAccount", {
    "id": phone_id,
    "BrowsePostsNum": 10,
    "Keyword": "technology",
    "scheduleAt": int(time.time()) + 60,
    "name": "Active Account"
})
```

---

## YouTube

### Publish Short

```python
client.rpa_task("youtubePubShort", {
    "id": phone_id,
    "title": "My Short",
    "video": "/path/to/video.mp4",  // String, not array
    "sameStyleVoice": True,
    "originalVoice": False,
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Short"
})
```

**Note**: YouTube uses `video` as a string, not an array

### Publish Video

```python
client.rpa_task("youtubePubVideo", {
    "id": phone_id,
    "title": "My Video",
    "video": "/path/to/video.mp4",
    "description": "Video description here",
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Video"
})
```

### Active Account

```python
client.rpa_task("youTubeActiveAccount", {
    "id": phone_id,
    "BrowseVideoNum": 10,
    "Keyword": "music",
    "scheduleAt": int(time.time()) + 60,
    "name": "Active Account"
})
```

---

## Reddit

### Publish Image

```python
client.rpa_task("redditImage", {
    "id": phone_id,
    "title": "My Image Post",
    "images": ["/path/to/image1.jpg", "/path/to/image2.jpg"],  // Array
    "community": "r/pics",
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Image"
})
```

### Publish Video

```python
client.rpa_task("redditVideo", {
    "id": phone_id,
    "title": "My Video Post",
    "video": ["/path/to/video.mp4"],  // Array
    "community": "r/videos",
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Video"
})
```

### Account Warmup

```python
client.rpa_task("redditWarmup", {
    "id": phone_id,
    "scheduleAt": int(time.time()) + 60,
    "name": "Account Warmup"
})
```

---

## Threads

### Publish Image

```python
client.rpa_task("threadsImage", {
    "id": phone_id,
    "title": "My Thread",
    "images": ["/path/to/image1.jpg", "/path/to/image2.jpg"],  // Array
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Image"
})
```

### Publish Video

```python
client.rpa_task("threadsVideo", {
    "id": phone_id,
    "title": "My Thread",
    "video": ["/path/to/video.mp4"],  // Array
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Video"
})
```

---

## Pinterest

### Publish Image

```python
client.rpa_task("pinterestImage", {
    "id": phone_id,
    "title": "My Pin",
    "description": "Pin description",
    "images": ["/path/to/image1.jpg", "/path/to/image2.jpg"],  // Array
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Image"
})
```

### Publish Video

```python
client.rpa_task("pinterestVideo", {
    "id": phone_id,
    "title": "My Pin",
    "description": "Pin description",
    "video": ["/path/to/video.mp4"],  // Array
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Video"
})
```

---

## X (Twitter)

### Publish Post

```python
client.rpa_task("xPublish", {
    "id": phone_id,
    "description": "My tweet",
    "video": ["/path/to/video.mp4"],  // Array
    "scheduleAt": int(time.time()) + 60,
    "name": "Publish Tweet"
})
```

---

## Google

### Login

```python
client.rpa_task("googleLogin", {
    "id": phone_id,
    "Email": "[REDACTED_SECRET]",
    "Password": "123456",
    "scheduleAt": int(time.time()) + 60,
    "name": "Google Login"
})
```

### App Download

```python
client.rpa_task("googleAppDownload", {
    "id": phone_id,
    "appName": "WhatsApp",
    "scheduleAt": int(time.time()) + 60,
    "name": "Download App"
})
```

### App Browse

```python
client.rpa_task("googleAppBrowser", {
    "id": phone_id,
    "appName": "Instagram",
    "scheduleAt": int(time.time()) + 60,
    "name": "Browse App"
})
```

---

## Shein

### Login

```python
client.rpa_task("sheinLogin", {
    "id": phone_id,
    "Email": "[REDACTED_SECRET]",
    "Password": "123456",
    "scheduleAt": int(time.time()) + 60,
    "name": "Shein Login"
})
```

---

## Task Management

### Query Task Status

```python
response = client.call("/open/v1/task/query", {
    "ids": [task_id]
})

status = response['data']['items'][0]['status']
print(f"Task status: {status}")
```

### Get Task Details

```python
response = client.call("/open/v1/task/detail", {
    "ID": task_id  # Note: uppercase 'ID'
})

details = response['data']
print(f"Task details: {details}")
```

### Cancel Task

```python
response = client.call("/open/v1/task/cancel", {
    "ids": [task_id]
})

if response['code'] == 0:
    print("✅ Task cancelled")
```

### Get Task History

```python
response = client.call("/open/v1/task/historyRecords", {
    "page": 1,
    "pageSize": 50
})

tasks = response['data']['items']
for task in tasks:
    print(f"Task: {task['name']}, Status: {task['status']}")
```

---

## Best Practices

### 1. Schedule Tasks in the Future

```python
# Schedule task 60 seconds from now
schedule_at = int(time.time()) + 60

client.rpa_task("tiktokLogin", {
    "id": phone_id,
    "account": "[REDACTED_SECRET]",
    "password": "123456",
    "scheduleAt": schedule_at
})
```

### 2. Use Meaningful Task Names

```python
client.rpa_task("tiktokRandomFollow", {
    "id": phone_id,
    "followProbability": 30,
    "scheduleAt": schedule_at,
    "name": "Daily Follow Routine - Account 1",  # Meaningful name
    "remark": "Follow 30% probability, niche: tech"  # Detailed remark
})
```

### 3. Check Task Status After Scheduling

```python
response = client.rpa_task("tiktokLogin", {...})

if response['code'] == 0:
    task_id = response['data']['taskId']
    print(f"✅ Task created: {task_id}")

    # Poll task status
    import time
    while True:
        status = client.call("/open/v1/task/query", {"ids": [task_id]})
        task_status = status['data']['items'][0]['status']

        if task_status == 'success':
            print("✅ Task completed successfully")
            break
        elif task_status == 'failed':
            print("❌ Task failed")
            break

        time.sleep(10)  # Check every 10 seconds
```

### 4. Handle Errors Gracefully

```python
response = client.rpa_task("tiktokLogin", {...})

if response['code'] != 0:
    error_msg = response.get('msg', 'Unknown error')
    print(f"❌ Task creation failed: {error_msg}")

    # Check fail details
    if response.get('data', {}).get('failAmount', 0) > 0:
        for fail in response['data']['failDetails']:
            print(f"   Error: {fail.get('msg')}")
```

---

## Video Parameter Format Summary

| Platform | Video Format | Example |
|----------|-------------|---------|
| **TikTok** | Array | `"video": ["/path/to/video.mp4"]` |
| **Instagram** | Array | `"video": ["/path/to/video.mp4"]` |
| **Instagram Reels Images** | Array (Capital 'I') | `"Image": ["/path/to/image.jpg"]` |
| **Facebook** | Array | `"video": ["/path/to/video.mp4"]` |
| **Facebook Reels** | String | `"video": "/path/to/video.mp4"` |
| **YouTube** | String | `"video": "/path/to/video.mp4"` |
| **Reddit** | Array | `"video": ["/path/to/video.mp4"]` |
| **Threads** | Array | `"video": ["/path/to/video.mp4"]` |
| **Pinterest** | Array | `"video": ["/path/to/video.mp4"]` |
| **X** | Array | `"video": ["/path/to/video.mp4"]` |

**Key Differences**:
- YouTube: `video` is a **string**
- Facebook Reels: `video` is a **string**
- Instagram Images: Field name is `Image` (capital 'I')
- All others: `video` or `images` is an **array**

---

## Field Name Summary

| Platform | Login Account Field | Notes |
|----------|-------------------|-------|
| **TikTok** | `account` | - |
| **Instagram** | `account` | - |
| **Facebook** | `Email` | Capital 'E' |
| **Google** | `Email` | Capital 'E' |
| **Shein** | `Email` | Capital 'E' |

---

## Last Updated

2026-04-10

**Related Documents**:
- [API Reference](api_reference.md) - Complete endpoint list
- [Best Practices](best_practices.md) - Safety and performance tips

path: references/cloudphone_operations.md
content: # ADB Operations Guide

Complete guide to operating cloud phones using ADB and uiautomator2.

---

## Overview

After booting a cloud phone and enabling ADB, you can operate the device using ADB commands. Since this skill is designed for AI agents that need to recognize the current screen state, the recommended workflow is:

1. **Identify UI Structure**: Use `uiautomator2` to dump the current page hierarchy (`dump_hierarchy()`), enabling the agent to understand the screen layout.
2. **Prioritize uiautomator2 Operations**: If elements can be identified and interacted with via the UI hierarchy, use `uiautomator2` for clicks, inputs, and swipes.
3. **Fallback to ADB Screenshots**: If elements cannot be recognized or operated via `uiautomator2`, use ADB screenshots to visually confirm the state and perform coordinate-based operations.

Tools available:
- **ADB commands** - Screenshots, key events, coordinate-based tap/swipe
- **uiautomator2** - UI hierarchy extraction, element-based interactions

---

## Prerequisites

### 1. Install Dependencies

```bash
pip install uiautomator2 --break-system-packages
```

### 2. Boot Cloud Phone and Enable ADB

```python
from scripts.geelark_boot_helper import boot_and_connect

# Boot and get ADB connection info
adb_info = boot_and_connect("phone_id", "your_token")

if adb_info:
    print(f"ADB: {adb_info['ip']}:{adb_info['port']}, [REDACTED_SECRET]'pwd']}")
else:
    print("❌ Failed to boot cloud phone")
    exit(1)
```

---

## ADB Connection

### Connect to Device

```python
import uiautomator2 as u2

# Connect using ADB info
d = u2.connect(f"{adb_info['ip']}:{adb_info['port']}")

print(f"✅ Connected to device")
```

### ⚠️ Authenticate with glogin (MUST DO IMMEDIATELY)

**Critical**: GeeLark requires `glogin` authentication **immediately after connection** before any ADB operations.

```python
import subprocess

# Authenticate using ADB password (must be done right after connecting)
r = subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'glogin', adb_info['pwd']
], capture_output=True, text=True)

if r.returncode == 0:
    print("✅ glogin successful - you can now perform operations")
else:
    print(f"❌ glogin failed: {r.stderr}")
    print(f"   stdout: {r.stdout}")
    exit(1)
```

**⚠️ Important**:
- Must be executed **immediately** after `u2.connect()`
- Without glogin, most ADB operations will fail or hang
- If connection drops, you need to re-authenticate

### Disconnect

```python
import subprocess

subprocess.run([
    'adb', 'disconnect', f"{adb_info['ip']}:{adb_info['port']}"
])
```

---

## Basic ADB Commands

### Tap (Click)

```python
import subprocess

subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'input', 'tap', '500', '1000'
])
```

### Input Text

```python
import subprocess

# ⚠️ Note: ADB input text does NOT support spaces or special characters
# For simple text without spaces:
subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'input', 'text', 'helloworld'
])

# For text with spaces/special characters, use uiautomator2 instead:
# d(text="Username").set_text("hello world")
# See uiautomator2 Operations → Input Text section below
```

### Swipe

```python
import subprocess

subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'input', 'swipe', '500', '1000', '500', '500'
])
```

### Key Events

```python
import subprocess

# Press Back button
subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'input', 'keyevent', '4'
])

# Press Home button
subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'input', 'keyevent', '3'
])

# Press Recent Apps button
subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'input', 'keyevent', '187'
])
```

**Common Key Codes**:
- `3` - Home
- `4` - Back
- `26` - Power
- `66` - Enter
- `67` - Delete (Backspace)
- `187` - Recent Apps
- `224` - Volume Up
- `225` - Volume Down

---

## uiautomator2 Operations

### Get UI Hierarchy

```python
# Get entire UI hierarchy as XML
xml = d.dump_hierarchy()

print(f"UI nodes: {xml.count('<node')}")

# Save to file
with open('/tmp/ui_hierarchy.xml', 'w') as f:
    f.write(xml)
```

### Find Element by Text

```python
# Find element with specific text
element = d(text="Submit")

if element.exists:
    print("✅ Found element")
else:
    print("❌ Element not found")
```

### Find Element by ID

```python
# Find element by resource ID
element = d(resourceId="com.android.settings:id/search")

if element.exists:
    print("✅ Found element")
else:
    print("❌ Element not found")
```

### Find Element by Content Description

```python
# Find element by content-desc
element = d(contentDesc="Navigate up")

if element.exists:
    print("✅ Found element")
else:
    print("❌ Element not found")
```

---

## UI Actions

### Click Element

```python
# Click element by text
d(text="Submit").click()

# Click element by ID
d(resourceId="com.android.settings:id/search").click()

# Click at specific coordinates
d.click(500, 1000)
```

### Long Press

```python
# Long press element
d(text="Item").long_click(duration=2)
```

### Input Text

```python
# Input text into field (recommended)
d(text="Username").set_text("my_username")

# Input text with special characters/spaces
d(resourceId="com.app:id/edit_text").set_text("hello world @123")

# Clear and input
element = d(text="Password")
element.clear_text()
element.set_text("my_secure_password")
```

**Note**: ADB `input text` command does NOT support spaces or special characters. Use uiautomator2 `set_text()` for complex text.
```

### Clear Text

```python
# Clear text field
d(text="Username").clear_text()
```

### Swipe

```python
# Swipe from bottom to top
d.swipe(500, 1000, 500, 500)

# Swipe from top to bottom
d.swipe(500, 500, 500, 1000)

# Swipe with steps (more precise)
d.swipe(500, 1000, 500, 500, steps=10)
```

### Scroll

```python
# Scroll down
d(scrollable=True).scroll.toEnd()

# Scroll up
d(scrollable=True).scroll.toBeginning()

# Scroll to specific text
d(scrollable=True).scroll.to(text="Target Text")
```

### Wait for Element

```python
# Wait up to 10 seconds for element
element = d(text="Submit").wait(timeout=10)

if element:
    print("✅ Element appeared")
else:
    print("❌ Element not found")
```

---

## Screenshot

### Take Screenshot with uiautomator2

```python
# Take screenshot
screenshot = d.screenshot()

# Save to file
screenshot.save("/tmp/screenshot.png")

# Or save directly
d.screenshot("/tmp/screenshot.png")
```

### Take Screenshot with ADB

```python
import subprocess

# Take screenshot on device
subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'screencap', '-p', '/sdcard/screenshot.png'
], capture_output=True)

# Pull to local
subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'pull', '/sdcard/screenshot.png', '/tmp/screenshot.png'
], capture_output=True)
```

---

## State-Aware Operations ⭐⭐⭐

**Core Principle**: Every operation should be: Get state → Act → Verify state.

### Get Page State Before Operation

```python
def get_page_structure(device):
    """Get current page structure"""
    return device.dump_hierarchy()

def find_element_coords(device, target_text):
    """Find element and get its coordinates"""
    xml = device.dump_hierarchy()
    element = device(text=target_text)

    if element.exists:
        bounds = element.info['bounds']
        center_x = (bounds['left'] + bounds['right']) // 2
        center_y = (bounds['top'] + bounds['bottom']) // 2
        return center_x, center_y
    return None
```

### Execute Operation

```python
def click_element(device, target_text):
    """Click element by text"""
    # 1. Get element coordinates
    coords = find_element_coords(device, target_text)

    if coords:
        # 2. Click at coordinates
        x, y = coords
        device.click(x, y)
        print(f"✅ Clicked: {target_text} ({x}, {y})")
        return True
    else:
        print(f"❌ Element not found: {target_text}")
        return False
```

### Verify Result

```python
def verify_element_appeared(device, target_text):
    """Verify element appeared after operation"""
    xml = device.dump_hierarchy()
    return target_text in xml

def verify_element_disappeared(device, target_text):
    """Verify element disappeared after operation"""
    xml = device.dump_hierarchy()
    return target_text not in xml
```

### Complete Example

```python
def click_and_verify(device, button_text, verify_text=None):
    """Click button and verify result"""
    # 1. 获取操作前状态（用于后续对比验证）
    # Note: dump_hierarchy() may take 1-3 seconds on complex UIs
    xml_before = device.dump_hierarchy()

    # 2. Check if button exists
    if button_text not in xml_before:
        print(f"❌ Button not found: {button_text}")
        return False

    # 3. Click button
    if not click_element(device, button_text):
        return False

    # 4. 获取操作后状态（验证是否达到预期）
    xml_after = device.dump_hierarchy()

    # 5. Verify result
    if verify_text:
        if verify_text in xml_after:
            print(f"✅ Verified: {verify_text}")
            return True
        else:
            print(f"❌ Verification failed: {verify_text}")
            return False
    else:
        print("✅ Operation completed (no verification)")
        return True

# Usage
success = click_and_verify(d, "Submit", "Success")
```

**Performance Note**: `dump_hierarchy()` can take 1-3 seconds on complex UIs. For frequent operations, consider:
- Caching XML when multiple checks are needed
- Using element selectors instead of string matching when possible
- Reducing verification steps for non-critical operations

---

## Common Patterns

### Fill Form and Submit

```python
def fill_and_submit_form(device, form_data):
    """Fill form and submit"""
    # 1. Get initial state
    xml_start = device.dump_hierarchy()

    # 2. Fill each field
    for field_label, input_value in form_data.items():
        # Check if field exists
        if field_label not in xml_start:
            print(f"❌ Field not found: {field_label}")
            return False

        # Find element and input text with error handling
        try:
            element = device(text=field_label)
            if not element.exists:
                print(f"❌ Element not found: {field_label}")
                return False
            
            element.set_text(input_value)
        except Exception as e:
            print(f"❌ Failed to input {field_label}: {e}")
            return False

        # Verify input (refresh XML)
        xml_after = device.dump_hierarchy()
        if input_value not in xml_after:
            print(f"❌ Input failed: {input_value}")
            return False

    # 3. Submit form
    success = click_and_verify(device, "Submit", "Success")

    return success

# Usage
form_data = {
    "Username": "my_username",
    "Password": "my_password",
    "Email": "[REDACTED_SECRET]"
}

fill_and_submit_form(d, form_data)
```

### Navigate to Page

```python
def navigate_to_page(device, target_page_text, nav_strategy=None):
    """Navigate to specific page
    
    Args:
        device: uiautomator2 device instance
        target_page_text: Text that appears on target page
        nav_strategy: List of button texts to try in order (customizable)
                     Default: ["Menu", "Back", "Home"]
    """
    if nav_strategy is None:
        nav_strategy = ["Menu", "Back", "Home"]
    
    max_attempts = 3

    for attempt in range(max_attempts):
        # 1. Get current state
        xml = device.dump_hierarchy()

        # 2. Check if already on target page
        if target_page_text in xml:
            print(f"✅ Already on page: {target_page_text}")
            return True

        # 3. Find navigation element from strategy
        nav_element = None
        for btn in nav_strategy:
            if btn in xml:
                nav_element = btn
                break

        if not nav_element:
            print(f"❌ No navigation element found (attempt {attempt + 1}/{max_attempts})")
            return False

        # 4. Click navigation
        if not click_element(device, nav_element):
            print(f"❌ Click failed: {nav_element} (attempt {attempt + 1}/{max_attempts})")
            return False

        # 5. Verify navigation
        xml_after = device.dump_hierarchy()
        if target_page_text in xml_after:
            print(f"✅ Navigated to: {target_page_text} (attempt {attempt + 1}/{max_attempts})")
            return True

    print(f"❌ Navigation failed: {target_page_text}")
    return False

# Usage - with default strategy
navigate_to_page(d, "Settings")

# Usage - with custom strategy for specific app
navigate_to_page(d, "Profile", nav_strategy=["Navigation Drawer", "Back", "Home"])
```

**Note**: Navigation strategy should be customized based on the target application's UI structure.

### Handle Dialogs/Popups

```python
def handle_dialog(device, cancel_button_text="Cancel", timeout=3):
    """Handle unexpected dialogs or popups"""
    try:
        # Wait briefly for dialog to appear
        cancel_btn = device(text=cancel_button_text)
        if cancel_btn.wait(timeout=timeout):
            cancel_btn.click()
            print(f"✅ Closed dialog: {cancel_button_text}")
            return True
    except Exception as e:
        print(f"ℹ️ No dialog found or already closed: {e}")
    return False

def handle_permission_dialog(device):
    """Handle Android permission request dialogs"""
    # Common permission buttons
    allow_buttons = ["Allow", "ALLOW", "允许", "While using the app"]
    
    for btn_text in allow_buttons:
        element = device(text=btn_text)
        if element.exists:
            element.click()
            print(f"✅ Granted permission: {btn_text}")
            return True
    
    return False

# Usage - call before critical operations
handle_dialog(d, "Cancel")
handle_permission_dialog(d)
```

---

## Debug Mode

### Enable Debug Mode

```python
import logging

# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("uiautomator2").setLevel(logging.DEBUG)

# Enable device debug
d = u2.connect(f"{adb_info['ip']}:{adb_info['port']}")
d.debug = True  # See HTTP request/response details
```

### Extend Timeout

```python
import uiautomator2 as u2

# Extend timeout for unstable connections
u2.HTTP_TIMEOUT = 120  # 120 seconds
```

---

## Common Issues

### ADB Connection Timeout

**Problem**: Connection times out or unstable.

**Solution**:
```python
import uiautomator2 as u2

# Extend timeout
u2.HTTP_TIMEOUT = 120

# Enable debug mode
d = u2.connect(f"{adb_info['ip']}:{adb_info['port']}")
d.debug = True
```

### Element Not Found

**Problem**: Element exists but `d(text="xxx")` doesn't find it.

**Solution**:
```python
# Try different selectors
d(text="Submit").click()  # By text
d(resourceId="com.app:id/button").click()  # By ID
d(contentDesc="Submit button").click()  # By content-desc

# Or use XPath (requires uiautomator2 XPath extension)
# Install: pip install uiautomator2[xpath] --break-system-packages
d.xpath('//android.widget.TextView[@text="Submit"]').click()
```

### glogin Failed

**Problem**: glogin authentication fails.

**Solution**:
```python
# Ensure you're using the correct password
r = subprocess.run([
    'adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
    'shell', 'glogin', adb_info['pwd']
], capture_output=True, text=True)

if r.returncode != 0:
    print(f"❌ glogin failed: {r.stderr}")
    print(f" stdout: {r.stdout}")
```

---

## Best Practices

### 1. Always Authenticate with glogin

```python
# Authenticate before any operation
r = subprocess.run(['adb', '-s', f"{adb_info['ip']}:{adb_info['port']}",
                   'shell', 'glogin', adb_info['pwd']], capture_output=True, text=True)

if r.returncode != 0:
    print("❌ glogin failed")
    exit(1)
```

### 2. Use State-Aware Operations

```python
# Get state → Act → Verify state
xml_before = device.dump_hierarchy()

# Check if element exists
if "Submit" not in xml_before:
    return False

# Perform action
device(text="Submit").click()

# Verify result
xml_after = device.dump_hierarchy()
if "Success" in xml_after:
    print("✅ Success")
```

### 3. Log All Operations

```python
from scripts.cloudphone_logger import CloudPhoneLogger

with CloudPhoneLogger("task_name", phone_id) as log:
    log.step("UI", "click", "text='Submit'")
    device(text="Submit").click()
    log.screenshot("/tmp/screen.png", 12345)
```

### 4. Handle Errors Gracefully

```python
try:
    device(text="Submit").click()
except Exception as e:
    print(f"❌ Click failed: {e}")
    # Handle error
```

---

## Last Updated

2026-04-25

**Related Documents**:
- [Best Practices](best_practices.md) - Safety and performance tips
- [Error Codes](error_codes.md) - Error handling guide

path: references/best_practices.md
content: # GeeLark API Best Practices

Safety, security, and performance tips for using GeeLark API.

---

## Safety Practices ⭐⭐⭐

### 1. Always Check Balance Before Operations

**Why**: Prevent unexpected costs and operation failures.

**How**:
```python
from scripts.geelark_client import GeeLarkClient

client = GeeLarkClient()

# Check balance
wallet = client.wallet()
balance = wallet['data']['balance']
gift_money = wallet['data'].get('giftMoney', 0)

if balance <= 0 and gift_money <= 0:
    print("❌ Insufficient balance! Please recharge.")
    exit(1)

print(f"✅ Balance: ${balance:.2f}, Gift: ${gift_money:.2f}")
```

**Rule**: Never start a cloud phone without checking balance first.

---

### 2. Double Confirmation for Delete Operations ⭐⭐⭐

**Why**: Prevent accidental deletion of cloud phones.

**How**: Use the built-in delete helper:

```python
from scripts.delete_helper import delete_cloud_phones_with_confirmation

delete_cloud_phones_with_confirmation(reason="Testing completed")
```

**Process**:
1. List all cloud phones
2. User manually types phone IDs
3. User types 'YES' (case-sensitive)
4. Only then execute deletion

**Rule**: Never call `/open/v1/phone/delete` directly without double confirmation.

---

### 3. Check Fail Details in API Responses ⭐⭐⭐

**Why**: API may return `code: 0` but actual operation failed.

**How**:
```python
response = client.call("/open/v1/phone/start", {"ids": [phone_id]})

# Check for failures
fail_amount = response.get('data', {}).get('failAmount', 0)
if fail_amount > 0:
    print(f"❌ {fail_amount} operation(s) failed:")
    for fail in response['data']['failDetails']:
        error_code = fail.get('code')
        error_msg = fail.get('msg')
        print(f"   code={error_code}, msg={error_msg}")
```

**Historical Error** (2026-04-12):
- Agent deleted cloud phones without authorization
- Root cause: Didn't check `failDetails`
- API returned code 41001 (balance not enough) but agent assumed "cloud phone is broken"

---

### 4. Stop Running Cloud Phones Before Deletion

**Why**: Error 43009 blocks deletion of running phones.

**How**: Use the built-in delete helper (automatically handles stopping):

```python
from scripts.delete_helper import delete_cloud_phones_with_confirmation

# Helper automatically checks status, stops running phones, and requires double confirmation
delete_cloud_phones_with_confirmation(reason="Cleanup completed")
```

**Rule**: Always use `delete_helper` for deletion. It enforces: Stop → Confirm IDs → Confirm 'YES' → Delete.

---

## Performance Optimization



### 2. Enable Energy Saving Mode

**Why**: Reduces cloud phone costs by ~50%.

**How**:
```python
# GeeLarkClient automatically adds energySavingMode=1 when calling /open/v1/phone/start
client.call("/open/v1/phone/start", {"ids": [phone_id]})
# No need to manually set it!
```

**Rule**: `energySavingMode` is auto-enabled by `GeeLarkClient.call()`.

---

### 3. Auto-Close Idle Devices

**Why**: Prevents unnecessary billing.

**How**:
```python
from scripts.phone_manager import PhoneManager

manager = PhoneManager(timeout_minutes=5)
manager.start_monitor()

# Connect using phone_id (name is optional for display)
d = manager.connect_device("phone_id_123", ip, port, pwd, name="Android15")

# Record activity using phone_id
manager.record_activity("phone_id_123")
```

**Benefits**:
- Save costs
- Release resources
- Prevent forgotten phones from racking up charges



## Security Best Practices

### 1. Use Environment Variables for Credentials

**Why**: Prevents credentials from being committed to version control.

**How**:
```python
import os

[REDACTED_SECRET]"GEELARK_TOKEN")
if not token:
    raise ValueError("GEELARK_TOKEN environment variable not set")

client = GeeLarkClient(token)
```

---

### 2. Validate All User Inputs

**Why**: Prevents injection attacks and invalid operations.

**How**:
```python
def validate_phone_id(phone_id: str) -> bool:
    """Validate phone ID format"""
    if not phone_id or not phone_id.isdigit():
        return False
    if len(phone_id) != 18:  # GeeLark phone IDs are 18 digits
        return False
    return True

if not validate_phone_id(user_input):
    print("❌ Invalid phone ID format")
    return
```

---

### 3. Use Endpoint Whitelisting

**Why**: Prevents accidental or malicious API calls to undocumented endpoints.

**How**:
```python
# GeeLarkClient has built-in endpoint whitelisting
ALLOWED_ENDPOINTS = {
    "/open/v1/phone/list",
    "/open/v1/phone/start",
    # ... other endpoints
}

client.call(endpoint, data)  # Raises error if endpoint not in whitelist
```

---

## Operational Best Practices

### 1. Use Context Managers

**Why**: Ensures resources are properly cleaned up.

**How**:
```python
from scripts.phone_manager import PhoneManager

# PhoneManager supports context manager and auto-closes idle devices
with PhoneManager(timeout_minutes=5) as manager:
    # Connect using phone_id
    d = manager.connect_device("phone_id_123", ip, port, pwd, name="Android15")
    # Your operations here
    manager.record_activity("phone_id_123")

# Automatically stops all phones and saves logs on exit
```

---

### 2. Log All Operations

**Why**: Enables debugging and auditing.

**How**:
```python
from scripts.cloudphone_logger import CloudPhoneLog

# Method 1: Direct usage
log = CloudPhoneLog("task_name", phone_id)
log.api_call("/open/v1/phone/start", {...})
log.adb_cmd("adb connect", rc=0)
log.step("UI", "click", "text='Submit'")
log.screenshot("/tmp/screen.png", 12345)
log.save()

# Method 2: Context manager (auto-saves)
with CloudPhoneLog("task_name", phone_id) as log:
    log.api_call("/open/v1/phone/start", {...})
    log.step("UI", "click", "text='Submit'")
# Log auto-saved to: logs/cloudphone/YYYYMMDD_HHMMSS_mmm_task_name_phoneid.log
```

---

### 3. Use State-Aware Operations

**Why**: Prevents blind operations and ensures reliability.

**How**:
```python
# 1. Get page structure before operation
xml_before = device.dump_hierarchy()

# 2. Check if element exists
if "Submit" not in xml_before:
    print("❌ Submit button not found")
    return

# 3. Execute operation
device(text="Submit").click()

# 4. Verify result
xml_after = device.dump_hierarchy()
if "Success" in xml_after:
    print("✅ Operation successful")
else:
    print("❌ Operation failed")
```

**Rule**: Every operation should be: Get state → Act → Verify state.

---

### 4. Implement Retry Logic

**Why**: Handles transient errors and improves reliability.

**How**:
```python
import time

def call_with_retry(client, endpoint, data, max_retries=3):
    """Call API with retry logic for transient errors"""
    for attempt in range(max_retries):
        response = client.call(endpoint, data)

        if response['code'] == 0:
            return response

        # Retry on transient errors
        if response['code'] in [42006, 47002]:  # Transient errors
            print(f"⚠️  Retrying ({attempt + 1}/{max_retries})...")
            time.sleep(10)
        else:
            break

    return response  # Return last response (failed)
```

---

## Cost Optimization

### 1. Monitor Cloud Phone Usage

**Why**: Identify and eliminate unnecessary costs.

**How**:
```python
# Check billing history
billing = client.billing_transactions()

# Analyze usage
total_cost = sum(tx['amount'] for tx in billing['data']['items'])
print(f"Total cost: ${total_cost:.2f}")
```

---


### 3. Stop Idle Devices Promptly

**Why**: Billing continues while phones are running.

**How**:
```python
from scripts.phone_manager import PhoneManager

manager = PhoneManager(timeout_minutes=5)
manager.start_monitor()

# Auto-closes after 5 minutes of no activity
```

---

## Debugging Tips

### 1. Enable Debug Mode

```python
import logging
logging.basicConfig(level=logging.DEBUG)
```

### 2. Use Verbose Logging

```python
import uiautomator2 as u2

d = u2.connect(f"{ip}:{port}")
d.debug = True  # Enable debug mode to see HTTP request/response details
```

### 3. Test with Single Device First

**Why**: Easier to debug than parallel operations.

**How**:
```python
from scripts.geelark_boot_helper import boot_and_connect

# Test with one device first (token auto-loads from config)
adb_info = boot_and_connect(phone_id)
if not adb_info:
    print("❌ Boot failed on single device")
    exit(1)

# Only then scale to multiple devices
```

---

## Common Pitfalls to Avoid

### ❌ Don't

1. Skip balance checks before starting cloud phones
2. Delete cloud phones without double confirmation
3. Ignore `failDetails` in API responses
4. Delete running cloud phones (error 43009)
5. Forget to stop idle cloud phones
6. Use serial operations for multiple devices
7. Skip energy saving mode
8. Hardcode credentials
9. Blindly operate without checking page state
10. Ignore error codes and assume success

---

### ✅ Do

1. Always check balance first
2. Use double confirmation for deletions
3. Check `failAmount` and `failDetails`
4. Stop running phones before deleting
5. Auto-close idle devices
6. Use parallel operations
7. Enable energy saving mode
8. Use environment variables for credentials
9. Verify state before and after operations
10. Handle all error codes properly

---

## Last Updated

2026-04-25

**Related Documents**:
- [Error Codes](error_codes.md) - Complete error handling guide
- [API Reference](api_reference.md) - Complete endpoint list

path: README.md
content: # Awesome GeeLark Skill

> 🚀 GeeLark Cloud Phone API automation for AI agents

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![Status](https://img.shields.io/badge/Status-Experimental-orange.svg)]()

---

> ⚠️ **WARNING: EXPERIMENTAL PROJECT**
>
> Use this skill with AI agents (OpenClaw, Claude, GPT, etc.) to automate GeeLark Cloud Phone operations. **Do not deploy it in production environments** without thorough testing and validation, or without fully understanding the AI's instruction-following reliability.
>
> - Expect API responses to change without notice.
> - Prepare for manual intervention in some automation flows.
> - Review all operations before execution.
> - Use only after fully understanding the risks.

---

## 📋 Overview

GeeLark Skill provides automation for GeeLark Cloud Phone service, enabling AI agents to manage cloud phones, automate social media apps, and execute RPA tasks across 9 major social platforms.


---

## ✨ Features

- 📱 **Cloud Phone Management** - Create, start, stop, delete cloud phones
- 🔧 **App Automation** - Install, start, stop apps automatically
- 🤖 **RPA Tasks** - Built-in automation for TikTok, Instagram, Facebook, YouTube, Reddit, Threads, Pinterest, X

---

## ⚙️ Requirements

```bash
# Install system dependencies
# macOS:   brew install android-platform-tools
# Ubuntu:  sudo apt install adb

# Install Python dependencies (recommended: use virtual environment)
python3 -m venv .venv
source .venv/bin/activate
pip install requests uiautomator2
```

> 💡 **First time setup?** Run the doctor diagnostic to verify all dependencies:
> ```bash
> python scripts/doctor.py
> ```

---

## 🚀 Quick Start


### Configure Credentials

#### Initialization configuration file

```bash
python3 scripts/init_config.py
```

This creates `assets/config.json` with your credentials (protected by `.gitignore`).

#### Get GeeLark API credentials

1. Visit [GeeLark](https://www.geelark.com/) and register an account
2. Download and log in to GeeLark, switch to the **API** tab, and generate your **API Key** and **Bearer Token**
3. Fill in your **APP ID**, **API Key**, and **Bearer Token** into `config.json`
4. Enter your proxy settings in the **Proxy** field
5. Start managing cloud phones through the agent

### Agent Usage Examples

Once configured, simply tell your AI agent what you want to do:

#### Create a Cloud Phone
```
"Create a new cloud phone with Android 15"
"Create a phone with proxy serial number 10"
```

#### View Your Cloud Phones
```
"Show me all my cloud phones and their status"
```

#### Start and Connect to a Phone
```
"Start phone Android13 and connect to it"
```

#### Install an App
```
"Install TikTok on phone Android13"
```

#### Run RPA Tasks
```
"Run the TikTok warmup task on phone Android13"
"Publish a video to Instagram from phone Android15"
```

#### Manage Multiple Phones
```
"Start 3 phones and install Facebook on all of them"
"Stop all running phones and save logs"
```

The agent handles all the complexity:
- ✅ Balance checking before operations
- ✅ Phone startup and ADB connection
- ✅ App installation with correct parameters
- ✅ Session tracking and auto-close
- ✅ Operation logging and error handling

---

## 📦 Contents

### Scripts

| Script | Purpose |
|--------|---------|
| `doctor.py` | **Diagnostic tool** - Check dependencies, network, ADB, API, and uiautomator2 |
| `ui_smoke_test.py` | **uiautomator2 smoke test** - Test device connectivity with per-step timeouts |
| `install_app.py` | **App installer** - Search and install apps with versionCode sorting and strict candidate matching |
| `handle_android_permissions.py` | **Permission dialog handler** - Auto-dismiss Android permission/system dialogs (curly quotes supported) |
| `error_codes.py` | **Error classification** - Structured error codes with recommendations |
| `geelark_client.py` | Core API client with safety mechanisms |
| `geelark_boot_helper.py` | Boot and ADB connection helper with structured stage output and failDetails reporting |
| `phone_manager.py` | Multi-device management with auto-close |
| `cloudphone_logger.py` | Operation logging with signed URL sanitization |
| `delete_helper.py` | Double-confirmation delete helper |
| `init_config.py` | Configuration initialization |
| `create_phone_profile.py` | Create cloud phones with proxy configuration |
| `utils.py` | Utility functions |

### Reference Documentation

| Document | Description |
|----------|-------------|
| [API Reference](references/api_reference.md) | Complete endpoint list and parameters |
| [Error Codes](references/error_codes.md) | All error codes and solutions |
| [Best Practices](references/best_practices.md) | Safety and performance tips |
| [RPA Tasks](references/rpa_tasks.md) | RPA tasks for 9 platforms |
| [Cloud Phone Operations](references/cloudphone_operations.md) | ADB and uiautomator2 guide |
| [Auto-Close](references/auto_close.md) | Auto-close mechanism |

---

## 🛡️ Safety Mechanisms

| Mechanism | Description |
|-----------|-------------|
| **Balance Check** | Auto-checks balance before starting cloud phones |
| **Energy Saving** | Auto enables `energySavingMode=1` to reduce costs |
| **Deletion Safeguards** | Anti-automation mechanism: requires interactive terminal, explicit ID + 'YES' confirmation, and enforced delays to prevent scripted or accidental deletion |
| **Auto-Close** | `PhoneManager` auto-closes idle devices |
| **Endpoint Whitelist** | Only documented endpoints can be called |
| **Logging** | All operations logged to `logs/cloudphone/` with sensitive data masked |

---

## 🔧 Diagnostic & Testing Tools

### Doctor Diagnostic

Check all dependencies and services before running cloud phone operations:

```bash
# Basic check (environment, ADB, network, API)
python scripts/doctor.py

# Include phone-specific checks
python scripts/doctor.py --phone-id <phone_id>

# Include uiautomator2 connectivity test
python scripts/doctor.py --phone-id <phone_id> --serial 192.168.1.100:5555

# Output as JSON (for programmatic use)
python scripts/doctor.py --json
```

### uiautomator2 Smoke Test

If uiautomator2 operations seem slow or hang, run the smoke test:

```bash
# Test connectivity and UI dump (30s timeout per step)
python scripts/ui_smoke_test.py 192.168.1.100:5555

# Test with app start
python scripts/ui_smoke_test.py 192.168.1.100:5555 --package com.zhiliaoapp.musically

# Custom timeout
python scripts/ui_smoke_test.py 192.168.1.100:5555 --timeout 60
```

### App Installer

Install apps with versionCode sorting and strict candidate matching:

```bash
# Search and install TikTok
python scripts/install_app.py --phone-id <phone_id> --name TikTok

# Exclude TikTok Lite
python scripts/install_app.py --phone-id <phone_id> --name TikTok --exclude Lite

# Install specific version
python scripts/install_app.py --phone-id <phone_id> --name Instagram --version-id <appVersionId>
```

### Permission Dialog Handler

Auto-dismiss Android permission/system dialogs after app install:

```bash
# Allow all permissions
python scripts/handle_android_permissions.py 192.168.1.100:5555

# Deny all permissions
python scripts/handle_android_permissions.py 192.168.1.100:5555 --action deny

# Handle up to 10 dialogs
python scripts/handle_android_permissions.py 192.168.1.100:5555 --max-retries 10
```

---

## ⚠️ Common Error Codes

| Code | Description | Action |
|------|-------------|--------|
| `0` | Success | ✅ |
| `41001` | Balance not enough | Recharge account |
| `42001` | Cloud phone does not exist | Check phone ID |
| `42002` | Cloud phone is not running | Start phone first |

**See**: [Error Codes](references/error_codes.md) for complete list.

---

## 📝 Important Notes

1. **Auto-Configuration**: Scripts automatically load credentials from `assets/config.json`. Please do not fill in sensitive credentials via AI.
2. **Deletion Safety**: Direct delete calls are blocked by default. The system uses `scripts/delete_helper.py` for double confirmation. Ensure your AI agent strictly follows instructions and executes deletions with caution.
3. **Logging**: All operation logs are saved in the `logs/cloudphone/` directory.
4. **Phone ID Storage**: `create_phone_profile.py` now saves phone IDs to `~/.geelark/last_created_phone_id.txt` by default. Use `--output` to customize the path.

---

## 📁 File Structure

```
geelark-api-skill/
├── README.md                # This file
├── SKILL.md                 # Agent usage guide
├── LICENSE                  # Apache License 2.0
├── assets/                  # Configuration files
├── scripts/                 # Python utilities (12 files)
├── references/              # Documentation (6 files)
└── examples/                # Usage examples
```

---

## 🔗 Links

- [GeeLark Official API Docs](https://github.com/GeeLark/geelark-openapi)


## 📄 License

Apache License 2.0


---

**Made with ❤️ and ☕️ by GeeLark**

path: examples/basic_usage.py
content: #!/usr/bin/env python3
"""
Basic Usage Example - GeeLark API Skill

This example demonstrates basic operations:
1. List cloud phones
2. Check balance
3. Create cloud phone
4. Boot cloud phone and connect
5. Install app 
6. Stop cloud phone
"""

from scripts.geelark_boot_helper import list_cloud_phones, boot_and_connect
from scripts.geelark_client import GeeLarkClient


def main():
    # Initialize client
    print("🔧 Initializing GeeLark Client...")
    client = GeeLarkClient()

    # 1. Check balance
    print("\n[1] Checking account balance...")
    wallet = client.wallet()
    balance = wallet['data']['balance']
    gift_money = wallet['data'].get('giftMoney', 0)
    print(f"   Balance: ${balance:.2f}, Gift: ${gift_money:.2f}")

    if balance <= 0 and gift_money <= 0:
        print("   ❌ Insufficient balance!")
        return

    # 2. List cloud phones
    print("\n[2] Listing cloud phones...")
    phones = list_cloud_phones(client.token)
    print(f"   Found {len(phones)} cloud phone(s):")
    for phone in phones:
        status = 'Running' if phone['status'] == 0 else 'Stopped'
        print(f"   - {phone['serialName']} ({phone['id']}) - {phone['equipmentInfo']['osVersion']} - {status}")

    # 3. Create new cloud phone (commented out - requires Pro plan for batch)
    # print("\n[3] Creating new cloud phone...")
    # response = client.call("/open/v1/phone/addNew", {
    #     "mobileType": "Android 13",
    #     "data": [{"profileName": "TestPhone"}]
    # })
    # if response['code'] == 0:
    #     phone_id = response['data']['details'][0]['id']
    #     print(f"   ✅ Created: {phone_id}")
    # else:
    #     print(f"   ❌ Failed: {response}")
    #     return

    # Use existing phone for this example
    if phones:
        phone_id = phones[0]['id']
    else:
        print("   ❌ No cloud phones available")
        return

    # 4. Boot cloud phone and get ADB connection
    print(f"\n[4] Booting cloud phone {phone_id}...")
    [REDACTED_SECRET]
    adb_info = boot_and_connect(phone_id, token)

    if not adb_info:
        print("   ❌ Boot failed")
        return

    print(f"   ✅ Connected: {adb_info['ip']}:{adb_info['port']}")

    # 5. Install app (commented out - requires app installation flow)
    # print("\n[5] Installing TikTok...")
    # response = client.call("/open/v1/app/installable/list", {
    #     "envId": phone_id,
    #     "name": "TikTok",
    #     "page": 1,
    #     "pageSize": 20
    # })
    # if response['code'] == 0 and response['data']['items']:
    #     app_version_id = response['data']['items'][0]['appVersionInfoList'][0]['id']
    #     response = client.call("/open/v1/app/install", {
    #         "envId": phone_id,
    #         "appVersionId": app_version_id
    #     })
    #     if response['code'] == 0:
    #         print("   ✅ TikTok installation started")
    #     else:
    #         print(f"   ❌ Installation failed: {response}")
    # else:
    #     print("   ❌ TikTok not found")

    # 6. Stop cloud phone
    print(f"\n[6] Stopping cloud phone {phone_id}...")
    response = client.phone_stop([phone_id])
    if response['code'] == 0:
        print("   ✅ Cloud phone stopped")
    else:
        print(f"   ❌ Stop failed: {response}")

    print("\n✅ Example completed!")


if __name__ == "__main__":
    main()

path: scripts/init_config.py
content: #!/usr/bin/env python3
"""
Initialize GeeLark API Configuration

This script helps users create their config.json from config_template.json
on first use.
"""

import os
import json
import sys


def init_config():
    """Initialize config.json from config_template.json"""

    # Get project root directory (parent of scripts directory)
    script_dir = os.path.dirname(os.path.abspath(__file__))
    project_root = os.path.dirname(script_dir)
    assets_dir = os.path.join(project_root, 'assets')

    # Paths
    template_path = os.path.join(assets_dir, 'config_template.json')
    config_path = os.path.join(assets_dir, 'config.json')

    # Check if config.json already exists
    if os.path.exists(config_path):
        print("✅ config.json already exists")
        print(f"   Location: {config_path}")

        # Ask if user wants to reconfigure
        choice = input("\nDo you want to reconfigure? (y/n): ").strip().lower()
        if choice != 'y':
            print("\n✅ Keeping existing configuration")
            return config_path

    # Load template
    if not os.path.exists(template_path):
        print(f"❌ Template not found: {template_path}")
        return None

    with open(template_path, 'r') as f:
        template = json.load(f)

    print("=" * 60)
    print("🔧 GeeLark API Configuration Setup")
    print("=" * 60)
    print()

    # Get user input
    print("Please enter your GeeLark API credentials:")
    print()

    [REDACTED_SECRET]"Token: ").strip()
    if not [REDACTED_SECRET]"❌ Token is required")
        return None

    app_id = input("App ID (optional, press Enter to skip): ").strip()
    [REDACTED_SECRET]"API Key (optional, press Enter to skip): ").strip()

    # Create config
    config = {
        "auth": {
            "token": token,
        },
        "baseUrl": "https://openapi.geelark.com",
        "rateLimit": {
            "perMinute": 200,
            "perHour": 24000
        }
    }

    if app_id:
        config["auth"]["appId"] = app_id

    if [REDACTED_SECRET]"auth"]["apiKey"] = api_key

    # Save config
    with open(config_path, 'w', encoding='utf-8') as f:
        json.dump(config, f, indent=2, ensure_ascii=False)

    print()
    print("=" * 60)
    print("✅ Configuration saved successfully!")
    print("=" * 60)
    print(f"   Location: {config_path}")
    print()
    print("⚠️  IMPORTANT:")
    print("   - config.json contains your sensitive credentials")
    print("   - Do NOT commit config.json to version control")
    print("   - config.json is already in .gitignore")
    print()
    print("✅ You can now use the GeeLark API skill!")
    print()

    return config_path


if __name__ == "__main__":
    init_config()

path: scripts/ui_smoke_test.py
content: #!/usr/bin/env python3
"""
GeeLark Cloud Phone - uiautomator2 Smoke Test

Tests uiautomator2 connectivity and responsiveness with individual timeouts.
Helps identify if the issue is with connection, agent initialization, or app startup.

Usage:
    python scripts/ui_smoke_test.py <serial>
    python scripts/ui_smoke_test.py 192.168.1.100:5555
"""

import sys
import os
import time
import json
import signal
from typing import Optional

# Add project root to path
_script_dir = os.path.dirname(os.path.abspath(__file__))
_project_root = os.path.dirname(_script_dir)
if _project_root not in sys.path:
    sys.path.insert(0, _project_root)


# ============================================
# Timeout Helper
# ============================================
class TimeoutError(Exception):
    pass


def _timeout_handler(signum, frame):
    raise TimeoutError("Operation timed out")


def run_with_timeout(func, timeout: int, *args, **kwargs):
    """Run function with signal-based timeout (Unix only)"""
    if sys.platform == "win32":
        # Windows doesn't support signal.alarm, run without timeout
        return func(*args, **kwargs)
    
    old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
    signal.alarm(timeout)
    try:
        result = func(*args, **kwargs)
        signal.alarm(0)  # Cancel alarm
        return result
    finally:
        signal.signal(signal.SIGALRM, old_handler)


# ============================================
# Smoke Test Configuration
# ============================================
DEFAULT_TIMEOUT = 30  # seconds per step


# ============================================
# Smoke Test Functions
# ============================================
def step_connect(serial: str, timeout: int = DEFAULT_TIMEOUT) -> dict:
    """Test uiautomator2.connect()"""
    result = {
        "step": "connect",
        "status": "pending",
        "elapsed": 0,
        "error": None
    }

    try:
        import uiautomator2 as u2

        print(f"  ⏳ Connecting to {serial} (timeout={timeout}s)...", flush=True)
        start_time = time.time()

        def _do_connect():
            return u2.connect(serial)

        d = run_with_timeout(_do_connect, timeout)
        
        elapsed = time.time() - start_time
        result["status"] = "ok"
        result["elapsed"] = round(elapsed, 2)
        print(f"  ✅ Connected ({elapsed:.2f}s)", flush=True)
        
        return result, d
    
    except ImportError:
        result["status"] = "failed"
        result["error"] = "uiautomator2 not installed"
        print(f"  ❌ {result['error']}", flush=True)
        return result, None
    
    except Exception as e:
        elapsed = time.time() - start_time
        result["status"] = "failed"
        result["elapsed"] = round(elapsed, 2)
        result["error"] = str(e)
        print(f"  ❌ Connection failed after {elapsed:.2f}s: {e}", flush=True)
        return result, None


def step_get_info(d, timeout: int = DEFAULT_TIMEOUT) -> dict:
    """Test d.info (current activity and device info)"""
    result = {
        "step": "get_info",
        "status": "pending",
        "elapsed": 0,
        "error": None,
        "data": {}
    }
    
    try:
        print(f"  ⏳ Getting device info (timeout={timeout}s)...", flush=True)
        start_time = time.time()

        info = run_with_timeout(lambda: d.info, timeout)
        
        elapsed = time.time() - start_time
        
        if info:
            result["status"] = "ok"
            result["elapsed"] = round(elapsed, 2)
            result["data"] = {
                "current_package": info.get("currentPackageName", "N/A"),
                "display_height": info.get("displayHeight", "N/A"),
                "display_width": info.get("displayWidth", "N/A"),
                "platform": info.get("platform", "N/A")
            }
            print(f"  ✅ Device info retrieved ({elapsed:.2f}s)", flush=True)
            print(f"     Current package: {result['data']['current_package']}", flush=True)
        else:
            result["status"] = "warning"
            result["elapsed"] = round(elapsed, 2)
            result["error"] = "d.info returned empty/None"
            print(f"  ⚠️  d.info returned empty ({elapsed:.2f}s)", flush=True)
        
        return result
    
    except Exception as e:
        elapsed = time.time() - start_time
        result["status"] = "failed"
        result["elapsed"] = round(elapsed, 2)
        result["error"] = str(e)
        print(f"  ❌ Failed to get info after {elapsed:.2f}s: {e}", flush=True)
        return result


def step_dump_hierarchy(d, timeout: int = DEFAULT_TIMEOUT) -> dict:
    """Test d.dump_hierarchy()"""
    result = {
        "step": "dump_hierarchy",
        "status": "pending",
        "elapsed": 0,
        "error": None,
        "data": {}
    }
    
    try:
        print(f"  ⏳ Dumping UI hierarchy (timeout={timeout}s)...", flush=True)
        start_time = time.time()

        hierarchy = run_with_timeout(lambda: d.dump_hierarchy(), timeout)
        
        elapsed = time.time() - start_time
        
        if hierarchy and len(hierarchy) > 100:
            result["status"] = "ok"
            result["elapsed"] = round(elapsed, 2)
            result["data"] = {
                "size_bytes": len(hierarchy),
                "has_root": "<hierarchy" in hierarchy if isinstance(hierarchy, str) else False
            }
            print(f"  ✅ UI hierarchy dumped ({elapsed:.2f}s, {len(hierarchy)} bytes)", flush=True)
        else:
            result["status"] = "warning"
            result["elapsed"] = round(elapsed, 2)
            result["data"] = {
                "size_bytes": len(hierarchy) if hierarchy else 0
            }
            result["error"] = "Hierarchy seems too short or empty"
            print(f"  ⚠️  Hierarchy too short ({elapsed:.2f}s, {len(hierarchy) if hierarchy else 0} bytes)", flush=True)
        
        return result
    
    except Exception as e:
        elapsed = time.time() - start_time
        result["status"] = "failed"
        result["elapsed"] = round(elapsed, 2)
        result["error"] = str(e)
        print(f"  ❌ dump_hierarchy failed after {elapsed:.2f}s: {e}", flush=True)
        return result


def test_app_start(d, package: str, timeout: int = DEFAULT_TIMEOUT) -> dict:
    """Test d.app_start()"""
    result = {
        "step": "app_start",
        "status": "pending",
        "elapsed": 0,
        "error": None,
        "data": {"package": package}
    }
    
    try:
        print(f"  ⏳ Starting app {package} (timeout={timeout}s)...", flush=True)
        start_time = time.time()

        run_with_timeout(lambda: d.app_start(package), timeout)

        elapsed = time.time() - start_time
        result["status"] = "ok"
        result["elapsed"] = round(elapsed, 2)
        print(f"  ✅ App started ({elapsed:.2f}s)", flush=True)

        # Verify current package
        time.sleep(2)
        info = run_with_timeout(lambda: d.info, timeout)
        current = info.get("currentPackageName", "unknown") if info else "unknown"
        result["data"]["current_package"] = current
        result["data"]["expected_package"] = package
        result["data"]["match"] = (current == package)
        
        if current != package:
            print(f"  ⚠️  Expected {package}, got {current}", flush=True)
        
        return result
    
    except Exception as e:
        elapsed = time.time() - start_time
        result["status"] = "failed"
        result["elapsed"] = round(elapsed, 2)
        result["error"] = str(e)
        print(f"  ❌ App start failed after {elapsed:.2f}s: {e}", flush=True)
        return result


# ============================================
# Main Smoke Test Runner
# ============================================
def run_smoke_test(serial: str, package: Optional[str] = None, timeout: int = DEFAULT_TIMEOUT) -> dict:
    """
    Run complete uiautomator2 smoke test.
    
    Args:
        serial: ADB serial (e.g., "192.168.1.100:5555")
        package: Optional app package to test starting
        timeout: Timeout in seconds per step
    
    Returns:
        dict with test results
    """
    all_results = []
    
    print("=" * 70)
    print("🤖 GeeLark Cloud Phone - uiautomator2 Smoke Test")
    print("=" * 70)
    print(f"  Serial:  {serial}")
    print(f"  Timeout: {timeout}s per step")
    print(f"  Package: {package or 'N/A (skip app_start test)'}")
    print("=" * 70)
    
    # Step 1: Connect
    print("\n[1/4] Connection Test")
    print("-" * 70)
    conn_result, d = step_connect(serial, timeout)
    all_results.append(conn_result)
    
    if conn_result["status"] != "ok":
        print("\n❌ Connection failed. Cannot continue tests.")
        return {
            "results": all_results,
            "summary": "failed",
            "failed_at": "connect"
        }
    
    # Step 2: Get device info
    print("\n[2/4] Device Info Test")
    print("-" * 70)
    info_result = step_get_info(d, timeout)
    all_results.append(info_result)
    
    # Step 3: Dump hierarchy
    print("\n[3/4] UI Hierarchy Dump Test")
    print("-" * 70)
    dump_result = step_dump_hierarchy(d, timeout)
    all_results.append(dump_result)
    
    # Step 4: App start (optional)
    if package:
        print("\n[4/4] App Start Test")
        print("-" * 70)
        app_result = test_app_start(d, package, timeout)
        all_results.append(app_result)
    
    # Summary
    print("\n" + "=" * 70)
    print("📊 Smoke Test Summary")
    print("=" * 70)
    
    ok_count = sum(1 for r in all_results if r["status"] == "ok")
    warn_count = sum(1 for r in all_results if r["status"] == "warning")
    fail_count = sum(1 for r in all_results if r["status"] == "failed")
    total_elapsed = sum(r.get("elapsed", 0) for r in all_results)
    
    print(f"  ✅ OK:        {ok_count}")
    print(f"  ⚠️  WARNING:   {warn_count}")
    print(f"  ❌ FAILED:    {fail_count}")
    print(f"  ⏱️  Total:     {total_elapsed:.2f}s")
    
    # Show elapsed times
    print("\n⏱️  Step Timings:")
    for r in all_results:
        status_icon = {"ok": "✅", "warning": "⚠️", "failed": "❌"}.get(r["status"], "❓")
        print(f"  {status_icon} {r['step']:20s} {r.get('elapsed', 0):6.2f}s")
    
    # Determine overall status
    if fail_count > 0:
        summary = "failed"
    elif warn_count > 0:
        summary = "warning"
    else:
        summary = "passed"
    
    print(f"\n🎯 Overall: {summary.upper()}")
    print("=" * 70)
    
    return {
        "results": all_results,
        "summary": summary,
        "total_elapsed": round(total_elapsed, 2)
    }


if __name__ == "__main__":
    import argparse
    
    parser = argparse.ArgumentParser(description="GeeLark uiautomator2 Smoke Test")
    parser.add_argument("serial", help="ADB serial (e.g., 192.168.1.100:5555)")
    parser.add_argument("--package", "-p", help="App package to test (optional)")
    parser.add_argument("--timeout", "-t", type=int, default=DEFAULT_TIMEOUT, help=f"Timeout per step in seconds (default: {DEFAULT_TIMEOUT})")
    parser.add_argument("--json", action="store_true", help="Output results as JSON")
    
    args = parser.parse_args()
    
    result = run_smoke_test(
        serial=args.serial,
        package=args.package,
        timeout=args.timeout
    )
    
    if args.json:
        print("\n" + json.dumps(result, indent=2))
    
    # Exit with error code if tests failed
    sys.exit(1 if result["summary"] == "failed" else 0)


path: scripts/cloudphone_logger.py
content: #!/usr/bin/env python3
"""
Cloud Phone Logger - Automatic logging for all cloud phone operations

Every cloud phone operation MUST generate a log file using this module.

Usage:
    # Method 1: Direct instantiation (for programmatic use)
    from scripts.cloudphone_logger import CloudPhoneLog

    log = CloudPhoneLog("task_name", phone_id)
    log.api_call("/open/v1/phone/start", {...})
    log.save()

    # Method 2: Context manager (auto-saves on exit)
    with CloudPhoneLog("task_name", phone_id) as log:
        log.api_call("/open/v1/phone/start", {...})
        log.step("UI", "click", "text='Submit'")
    # Log auto-saved to: logs/cloudphone/YYYYMMDD_HHMMSS_mmm_task_name_phoneid.log
"""

import json
import re
from datetime import datetime
from pathlib import Path
from contextlib import contextmanager
from typing import Any


# Sensitive field patterns for automatic log desensitization
_SENSITIVE_KEYS = {
    'token', 'pwd', 'password', 'passwd', 'secret', 'apikey', 'api_key',
    'authorization', 'proxy_password', 'proxy_pwd', 'access_key', 'secret_key',
    'credential', 'auth', 'email', 'account'
}

# URL query parameters to redact in signed URLs (OSS, CDN, etc.)
_SENSITIVE_URL_PARAMS = {
    'x-oss-security-token', 'security-token', 'signature', 'x-oss-signature',
    'ossaccesskeyid', 'accesskeyid', 'access_key_id', 'x-oss-access-key-id',
    'expires', 'x-oss-expires', 'policy', 'x-oss-policy',
    'x-oss-date', 'date', 'x-amz-credential', 'x-amz-signature',
    'x-amz-security-token', 'x-amz-date', 'authorization'
}


def _sanitize_url(url: str) -> str:
    """
    Redact sensitive query parameters from signed URLs.
    Keeps the base URL and non-sensitive params, replaces sensitive ones with '***'.
    
    Example:
        "https://bucket.oss.com/app.png?Expires=123&OSSAccessKeyId=ABC&Signature=XYZ"
        → "https://bucket.oss.com/app.png?Expires=***&OSSAccessKeyId=***&Signature=***"
    """
    if not isinstance(url, str) or '://' not in url:
        return url
    
    # Split URL into base and query string
    if '?' not in url:
        return url
    
    base, query = url.split('?', 1)
    
    def _redact_param(match):
        param_name = match.group(1).lower()
        if param_name in _SENSITIVE_URL_PARAMS:
            return f"{match.group(1)}=***"
        return match.group(0)
    
    # Match query params: name=value (handle & separator)
    sanitized_query = re.sub(
        r'([A-Za-z0-9_-]+)=([^&]*)',
        _redact_param,
        query
    )
    
    return f"{base}?{sanitized_query}"


def _mask_sensitive_data(data: Any) -> Any:
    """
    Recursively mask sensitive fields in API request/response data.
    Prevents credentials, tokens, PII, and signed URL parameters from being written to log files.
    """
    if isinstance(data, dict):
        masked = {}
        for k, v in data.items():
            key_str = str(k).lower()
            if any(pattern in key_str for pattern in _SENSITIVE_KEYS):
                masked[k] = '***'
            else:
                masked[k] = _mask_sensitive_data(v)
        return masked
    elif isinstance(data, list):
        return [_mask_sensitive_data(item) for item in data]
    elif isinstance(data, str):
        # Sanitize URLs with signed query parameters
        if '://' in data:
            return _sanitize_url(data)
        return data
    return data


class CloudPhoneLog:
    """Logger for cloud phone operations"""

    def __init__(self, task_name: str, phone_id: str, verbose: bool = True):
        self.task_name = task_name
        self.phone_id = phone_id
        self.verbose = verbose
        self.entries = []
        self.start_time = datetime.now()
        self.log_dir = self._get_log_dir()
        self.log_file = self._get_log_file()
        self._saved = False  # Prevent duplicate saves

    def __enter__(self):
        """Support context manager protocol"""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Auto-save on context exit"""
        self.save()
        return False  # Don't suppress exceptions

    def _get_log_dir(self) -> Path:
        """Get log directory path using pathlib"""
        skill_dir = Path(__file__).resolve().parent
        log_dir = skill_dir.parent / 'logs' / 'cloudphone'
        log_dir.mkdir(parents=True, exist_ok=True)
        return log_dir

    def _get_log_file(self) -> Path:
        """Get log file path with millisecond precision to avoid conflicts"""
        timestamp = self.start_time.strftime("%Y%m%d_%H%M%S_%f")[:-3]  # YYYYMMDD_HHMMSS_mmm
        filename = f"{timestamp}_{self.task_name}_{self.phone_id}.log"
        return self.log_dir / filename

    def _add_entry(self, entry_type: str, **kwargs):
        """Add a log entry"""
        entry = {
            "timestamp": datetime.now().isoformat(),
            "type": entry_type,
            "task": self.task_name,
            "phone_id": self.phone_id,
            **kwargs
        }
        self.entries.append(entry)
        
        # Console output (optional)
        if self.verbose:
            # Filter out redundant fields for cleaner output
            details = {k: v for k, v in kwargs.items() if k not in ('task', 'phone_id')}
            print(f"  📝 LOG [{entry_type}]: {details}")

    def api_call(self, endpoint: str, data: dict, response_code: int = None, response_data: dict = None):
        """Log an API call with sensitive data automatically masked"""
        self._add_entry(
            "API_CALL",
            endpoint=endpoint,
            request_data=_mask_sensitive_data(data),
            response_code=response_code,
            response_data=_mask_sensitive_data(response_data)
        )

    def adb_cmd(self, command: str, rc: int = None, output: str = None):
        """Log an ADB command"""
        self._add_entry(
            "ADB_CMD",
            command=command,
            return_code=rc,
            output=output
        )

    def step(self, category: str, action: str, details: str = None):
        """Log a step"""
        self._add_entry(
            "STEP",
            category=category,
            action=action,
            details=details
        )

    def screenshot(self, path: str, size: int):
        """Log a screenshot"""
        self._add_entry(
            "SCREENSHOT",
            file_path=path,
            size_bytes=size
        )

    def file_op(self, action: str, path: str):
        """Log a file operation"""
        self._add_entry(
            "FILE_OP",
            action=action,
            file_path=path
        )

    def error(self, error_type: str, message: str, details: dict = None):
        """Log an error"""
        self._add_entry(
            "ERROR",
            error_type=error_type,
            message=message,
            details=details
        )

    def info(self, message: str):
        """Log an informational message"""
        self._add_entry(
            "INFO",
            message=message
        )

    def save(self) -> str:
        """Save log entries to file. Returns file path or empty string on failure."""
        if self._saved:
            return str(self.log_file)
        
        try:
            log_data = {
                "task": self.task_name,
                "phone_id": self.phone_id,
                "start_time": self.start_time.isoformat(),
                "end_time": datetime.now().isoformat(),
                "entries": self.entries
            }

            with open(self.log_file, 'w', encoding='utf-8') as f:
                json.dump(log_data, f, indent=2, ensure_ascii=False)

            self._saved = True
            print(f"\n  📄 Log saved to: {self.log_file}")
            return str(self.log_file)
        except Exception as e:
            print(f"  ❌ Failed to save log: {e}")
            return ""


# Backward compatibility alias
@contextmanager
def CloudPhoneLogger(task_name: str, phone_id: str, verbose: bool = True):
    """
    Context manager for logging cloud phone operations.
    Deprecated: Use CloudPhoneLog directly with 'with' statement instead.
    """
    log = CloudPhoneLog(task_name, phone_id, verbose=verbose)
    try:
        yield log
    finally:
        log.save()


if __name__ == "__main__":
    # Test 1: Context manager usage
    print("Test 1: Context manager usage")
    with CloudPhoneLog("test_task", "test_phone_123") as log:
        log.api_call("/open/v1/phone/start", {"ids": ["test_phone_123"]}, response_code=0)
        log.adb_cmd("adb connect 1.2.3.4:5555", rc=0)
        log.step("UI", "click", "text='Submit'")
        log.screenshot("/tmp/test.png", 12345)
        log.info("Test completed")

    # Test 2: Duplicate save prevention
    print("\nTest 2: Duplicate save prevention")
    with CloudPhoneLog("test_task_2", "test_phone_456") as log:
        log.info("Testing duplicate save")
        log.save()  # First save
        log.save()  # Second save (should be ignored)

    # Test 3: Sensitive data masking
    print("\nTest 3: Log desensitization")
    sensitive_payload = {
        "token": "sk_live_abc123",
        "pwd": "my_secret_password",
        "ids": ["phone_001"],
        "proxy": {"password": "proxy_pass", "host": "1.2.3.4"},
        "nested": {"api_key": "key_xyz", "safe_field": "visible_data"},
        "list_data": [{"email": "[REDACTED_SECRET]"}, {"normal": "value"}]
    }
    masked = _mask_sensitive_data(sensitive_payload)
    assert masked["token"] == "***", "Token should be masked"
    assert masked["pwd"] == "***", "Password should be masked"
    assert masked["proxy"]["password"] == "***", "Nested password should be masked"
    assert masked["nested"]["api_key"] == "***", "API key should be masked"
    assert masked["nested"]["safe_field"] == "visible_data", "Non-sensitive fields should remain"
    assert masked["list_data"][0]["email"] == "***", "Email in list should be masked"
    assert masked["list_data"][1]["normal"] == "value", "Normal list items should remain"
    print("✅ All sensitive fields correctly masked")

    # Test 4: URL sanitization
    print("\nTest 4: Signed URL sanitization")
    test_url = "https://cmp1-prod.zxpcloud.com/apps/icon.png?Expires=1778156549&OSSAccessKeyId=STS.NXa75T88zyaZ8Ci2wgGX4HE6d&Signature=IyCSebd75cz4%2BXfK8HzaJDvG3wE%3D&security-[REDACTED_SECRET]"
    sanitized = _sanitize_url(test_url)
    assert "STS.NXa75T88" not in sanitized, "OSSAccessKeyId should be redacted"
    assert "IyCSebd75cz4" not in sanitized, "Signature should be redacted"
    assert "CAISzAJ1q6Ft5B2yfSjIr5vUfI" not in sanitized, "security-token should be redacted"
    assert "Expires=***" in sanitized, "Expires should be redacted"
    assert "cmp1-prod.zxpcloud.com/apps/icon.png" in sanitized, "Base URL should be preserved"
    print(f"  Original: ...{test_url[-60:]}")
    print(f"  Sanitized: ...{sanitized[-60:]}")
    print("✅ Signed URL parameters correctly redacted")

    # Test 5: URL sanitization through _mask_sensitive_data
    print("\nTest 5: URL sanitization through _mask_sensitive_data")
    url_payload = {
        "appIcon": test_url,
        "normalField": "safe_value",
        "nested": {"iconUrl": test_url}
    }
    masked_url = _mask_sensitive_data(url_payload)
    assert "STS.NXa75T88" not in masked_url["appIcon"], "URL in dict value should be sanitized"
    assert "STS.NXa75T88" not in masked_url["nested"]["iconUrl"], "URL in nested dict should be sanitized"
    assert masked_url["normalField"] == "safe_value", "Non-URL fields should remain"
    print("✅ URL sanitization works through _mask_sensitive_data")

    print("\n✅ All tests passed!")


path: scripts/delete_helper.py
content: #!/usr/bin/env python3
"""
GeeLark Cloud Phone Delete Helper - Double Confirmation Mechanism

⚠️ CRITICAL: Deleting cloud phones requires double confirmation:
1. List all phones
2. User manually types phone IDs
3. User types 'YES' (case-sensitive)

Usage:
    from scripts.delete_helper import delete_cloud_phones_with_confirmation

    delete_cloud_phones_with_confirmation(reason="Testing completed")
"""

import sys
import time
import requests
from scripts.geelark_client import GeeLarkClient


def check_interactive_terminal() -> bool:
    """
    Check if running in an interactive terminal.
    
    Returns:
        True if interactive terminal, False otherwise
    """
    if not sys.stdin.isatty():
        print("❌ ERROR: This operation requires an interactive terminal.")
        print("   AI agents cannot perform deletion operations.")
        print("   Please run this script manually in a terminal.")
        return False
    return True


def list_cloud_phones(client: GeeLarkClient) -> list:
    """
    List all available cloud phones.

    Args:
        client: GeeLarkClient instance

    Returns:
        List of cloud phone dictionaries
    """
    try:
        response = client.phone_list()
        if response.get('code') == 0:
            return response.get('data', {}).get('items', [])
        return []
    except requests.RequestException as e:
        print(f"❌ Network error listing phones: {e}")
        return []
    except Exception as e:
        print(f"❌ Unexpected error listing phones: {e}")
        return []


def display_phones(phones: list):
    """
    Display cloud phones for user confirmation.

    Args:
        phones: List of cloud phone dictionaries
    """
    print("=" * 60)
    print("⚠️  WARNING: Delete Operation")
    print("=" * 60)
    print(f"\nFound {len(phones)} cloud phone(s) to delete:\n")

    for i, phone in enumerate(phones, 1):
        status = 'Running' if phone.get('status') == 0 else 'Stopped'
        serial_name = phone.get('serialName', 'N/A')
        os_version = phone.get('equipmentInfo', {}).get('osVersion', 'N/A')
        phone_id = phone.get('id', 'N/A')

        print(f"   {i}. {serial_name} ({phone_id})")
        print(f"      OS: {os_version}, Status: {status}")
    print()


def confirm_phone_ids(phones: list) -> bool:
    """
    First confirmation: User must manually type phone IDs.
    
    Includes timing check to prevent automated rapid input.

    Args:
        phones: List of cloud phone dictionaries

    Returns:
        True if IDs match, False otherwise
    """
    phone_ids = [str(p['id']) for p in phones]

    print("=" * 60)
    print("⚠️  DELETION IS PERMANENT AND CANNOT BE UNDONE!")
    print("=" * 60)
    print()

    print(f"Please type the phone IDs to delete (copy-paste):")
    print(f"   {', '.join(phone_ids)}")
    print()

    # Timing check: prevent automated rapid input
    start_time = time.time()
    
    try:
        user_input = input("Enter phone IDs (comma-separated) to confirm: ").strip()
        elapsed = time.time() - start_time
        
        # If input is too fast (< 0.5 seconds), likely automated
        if elapsed < 0.5:
            print(f"❌ Input too fast ({elapsed:.2f}s). This appears to be automated.")
            print("   Please type the IDs manually.")
            return False
            
    except (EOFError, KeyboardInterrupt):
        print("\n❌ Operation cancelled")
        return False

    input_ids = [id.strip() for id in user_input.split(',')]
    
    # Verify input matches
    if set(input_ids) != set(phone_ids):
        print("❌ Phone IDs do not match. Deletion cancelled.")
        return False

    print()
    return True


def confirm_yes() -> bool:
    """
    Second confirmation: User must type 'YES'.
    
    Includes timing check and mandatory delay to prevent automated input.

    Returns:
        True if user typed 'YES', False otherwise
    """
    # Mandatory 3-second delay before second confirmation
    print("⏳ Waiting 3 seconds before final confirmation...")
    time.sleep(3)
    
    start_time = time.time()
    
    try:
        confirm = input("Type 'YES' (all caps) to confirm deletion: ").strip()
        elapsed = time.time() - start_time
        
        # If input is too fast (< 0.5 seconds), likely automated
        if elapsed < 0.5:
            print(f"❌ Input too fast ({elapsed:.2f}s). This appears to be automated.")
            print("   Please type 'YES' manually.")
            return False
            
    except (EOFError, KeyboardInterrupt):
        print("\n❌ Operation cancelled")
        return False

    if confirm != 'YES':
        print("❌ Confirmation failed. Deletion cancelled.")
        return False

    return True


def check_running_phones(phones: list, client: GeeLarkClient) -> bool:
    """
    Check if any phones are running and must be stopped first.
    
    Updates phone status in the list after stopping to maintain state consistency.

    Args:
        phones: List of cloud phone dictionaries
        client: GeeLarkClient instance

    Returns:
        True if all phones are stopped, False if any are running
    """
    running_phones = [p for p in phones if p.get('status') == 0]

    if running_phones:
        print("⚠️  The following phones are running and must be stopped first:")
        for phone in running_phones:
            print(f"   - {phone.get('serialName', 'N/A')} ({phone.get('id', 'N/A')})")
        print()
        try:
            response = input("Stop these phones now? (y/n): ").strip().lower()
            if response != 'y':
                print("❌ Operation cancelled")
                return False
        except (EOFError, KeyboardInterrupt):
            print("\n❌ Operation cancelled")
            return False

        # Stop running phones
        phone_ids = [p['id'] for p in running_phones]
        print(f"\nStopping {len(phone_ids)} phone(s)...")
        
        try:
            response = client.phone_stop(phone_ids)
            if response.get('code') == 0:
                print("✅ Phones stopped")
                # Update local status to maintain state consistency
                for phone in phones:
                    if phone['id'] in phone_ids:
                        phone['status'] = 1  # Mark as stopped
            else:
                print(f"❌ Failed to stop phones: {response}")
                return False
        except requests.RequestException as e:
            print(f"❌ Network error stopping phones: {e}")
            return False
        except Exception as e:
            print(f"❌ Unexpected error stopping phones: {e}")
            return False

        print()
    return True


def delete_cloud_phones_with_confirmation(client: GeeLarkClient = None, reason: str = ""):
    """
    Delete cloud phones with mandatory double confirmation.
    
    ⚠️  CRITICAL: This function requires an interactive terminal.
        AI agents cannot execute deletion operations.

    Args:
        client: GeeLarkClient instance (optional, will create if not provided)
        reason: Reason for deletion (optional)
    """
    # Verify interactive terminal before proceeding
    if not check_interactive_terminal():
        return
    
    # Create client if not provided
    if client is None:
        try:
            # Initialize with logging enabled
            client = GeeLarkClient(task_name="phone_deletion", phone_id="batch")
        except Exception as e:
            print(f"❌ Failed to create GeeLarkClient: {e}")
            sys.exit(1)

    # Step 1: List phones
    phones = list_cloud_phones(client)
    if not phones:
        print("❌ No available cloud phones")
        client.save_log()
        return

    # Step 2: Display phones
    display_phones(phones)

    # Step 3: Check and stop running phones
    if not check_running_phones(phones, client):
        client.save_log()
        return

    # Step 4: First confirmation - phone IDs
    if not confirm_phone_ids(phones):
        client.save_log()
        return

    # Step 5: Display reason if provided
    if reason:
        print(f"Reason: {reason}")
        print()

    # Step 6: Second confirmation - YES
    if not confirm_yes():
        client.save_log()
        return

    # Step 7: Execute deletion
    phone_ids = [p['id'] for p in phones]
    print(f"\n🗑️  Deleting {len(phone_ids)} cloud phone(s)...")

    try:
        response = client.execute_phone_deletion(phone_ids, confirmation_code='DELETE_CONFIRMED')

        if response.get('code') == 0:
            print("✅ Deletion successful")
        else:
            print(f"❌ Deletion failed: {response.get('msg', 'Unknown error')}")
    except requests.RequestException as e:
        print(f"❌ Network error during deletion: {e}")
    except Exception as e:
        print(f"❌ Unexpected error during deletion: {e}")
    finally:
        # Always save log
        client.save_log()


if __name__ == "__main__":
    # Test
    delete_cloud_phones_with_confirmation(reason="Test deletion")

path: scripts/phone_manager.py
content: #!/usr/bin/env python3
"""
GeeLark Cloud Phone Manager - Auto-Close Idle Devices

Automatically close cloud phones that have been idle for a specified timeout.

Usage:
    from scripts.phone_manager import PhoneManager

    # Method 1: Context manager (recommended)
    with PhoneManager(timeout_minutes=5) as manager:
        d = manager.connect_device(phone_id, ip, port, pwd, name="Android15")
        d(text="Dark theme").click()
        manager.record_activity(phone_id)

    # Method 2: Manual control
    manager = PhoneManager(timeout_minutes=5)
    manager.start_monitor()
    # ... operations ...
    manager.stop_all()
"""

import time
import threading
import subprocess
from datetime import datetime, timedelta
from typing import Dict, Optional

import uiautomator2 as u2

from scripts.geelark_client import GeeLarkClient
from scripts.cloudphone_logger import CloudPhoneLog


class PhoneManager:
    """
    Manages cloud phone connections and auto-closes idle devices.
    All operations use phone_id for precise matching.
    """

    def __init__(self, timeout_minutes: int = 5, token: str = None, base_url: str = None, task_name: str = "phone_manager"):
        """
        Initialize PhoneManager.

        Args:
            timeout_minutes: Timeout in minutes before auto-closing
            [REDACTED_SECRET] API token (optional, reads from config if not provided)
            base_url: API base URL (optional)
            task_name: Name for logging context
        """
        self.timeout = timedelta(minutes=timeout_minutes)
        self.last_activity: Dict[str, datetime] = {}  # key: phone_id
        self.phone_status: Dict[str, str] = {}        # key: phone_id
        self.devices: Dict[str, u2.Device] = {}       # key: phone_id
        self._display_names: Dict[str, str] = {}      # key: phone_id, value: display name
        self.client = GeeLarkClient(token=token, base_url=base_url, task_name=task_name, phone_id="batch")
        self.monitor_thread: Optional[threading.Thread] = None
        self.whitelist: list = []
        self._monitoring = False
        self.task_name = task_name

    def connect_device(self, phone_id: str, ip: str, port: str, pwd: str, name: str = None) -> u2.Device:
        """
        Connect to cloud phone and record activity.

        Args:
            phone_id: Cloud phone ID (required, used for all operations)
            ip: ADB IP address
            port: ADB port
            pwd: ADB password
            name: Display name (optional, for logging only)

        Returns:
            uiautomator2.Device instance
        """
        display_name = name or phone_id
        
        # Connect to device
        d = u2.connect(f"{ip}:{port}")

        # glogin authentication
        try:
            r = subprocess.run(
                ['adb', '-s', f"{ip}:{port}", 'shell', 'glogin', pwd],
                capture_output=True,
                text=True,
                timeout=30
            )
            if r.returncode != 0:
                print(f"❌ glogin failed: {r.stderr}")
                self.client._log.error("GLLOGIN_FAILED", r.stderr) if self.client._log else None
        except subprocess.TimeoutExpired:
            print(f"❌ glogin timeout for {ip}:{port}")
        except Exception as e:
            print(f"❌ glogin error: {e}")

        self.devices[phone_id] = d
        self._display_names[phone_id] = display_name
        self.record_activity(phone_id)

        print(f"✅ Connected to {display_name} ({ip}:{port})")
        return d

    def record_activity(self, phone_id: str):
        """
        Record cloud phone activity time.

        Args:
            phone_id: Cloud phone ID
        """
        self.last_activity[phone_id] = datetime.now()
        self.phone_status[phone_id] = "active"

    def stop_phone(self, phone_id: str) -> bool:
        """
        Stop cloud phone.

        Args:
            phone_id: Cloud phone ID

        Returns:
            True if stopped successfully, False otherwise
        """
        display_name = self._display_names.get(phone_id, phone_id)

        # Call GeeLark API to stop
        try:
            response = self.client.phone_stop([phone_id])
            if response.get('code') == 0:
                print(f"🛑 Stopped {display_name} ({phone_id})")
                self.client._log.info(f"Stopped {display_name}") if self.client._log else None
            else:
                print(f"⚠️  Failed to stop {display_name}: {response.get('msg', 'Unknown error')}")
                return False
        except Exception as e:
            print(f"❌ Error stopping {display_name}: {e}")
            return False

        self.phone_status[phone_id] = "stopped"
        if phone_id in self.devices:
            del self.devices[phone_id]
        if phone_id in self.last_activity:
            del self.last_activity[phone_id]
        if phone_id in self._display_names:
            del self._display_names[phone_id]
        return True

    def check_idle_phones(self):
        """
        Check and close idle cloud phones.
        """
        now = datetime.now()

        for phone_id, last_time in list(self.last_activity.items()):
            # Skip whitelisted devices
            if phone_id in self.whitelist:
                continue

            idle_time = now - last_time
            if idle_time > self.timeout:
                idle_minutes = int(idle_time.total_seconds() / 60)
                display_name = self._display_names.get(phone_id, phone_id)
                print(f"⚠️  {display_name}: Idle for {idle_minutes} minutes, stopping...")
                self.stop_phone(phone_id)

    def start_monitor(self, check_interval: int = 60):
        """
        Start background monitoring thread.

        Args:
            check_interval: Check interval in seconds (default: 60)
        """
        if self._monitoring:
            print("⚠️  Monitor already running")
            return

        self._monitoring = True

        def monitor_loop():
            while self._monitoring:
                try:
                    self.check_idle_phones()
                except Exception as e:
                    print(f"❌ Monitor error: {e}")
                time.sleep(check_interval)

        self.monitor_thread = threading.Thread(target=monitor_loop, daemon=True)
        self.monitor_thread.start()
        print(f"🔍 Background monitoring started (timeout: {self.timeout.seconds // 60} minutes, check every {check_interval}s)")

    def stop_monitor(self):
        """Stop background monitoring."""
        self._monitoring = False
        if self.monitor_thread and self.monitor_thread.is_alive():
            self.monitor_thread.join(timeout=5)
        print("🔍 Background monitoring stopped")

    def add_to_whitelist(self, phone_id: str):
        """
        Add device to whitelist (not auto-closed).

        Args:
            phone_id: Cloud phone ID
        """
        if phone_id not in self.whitelist:
            self.whitelist.append(phone_id)
            display_name = self._display_names.get(phone_id, phone_id)
            print(f"📋 {display_name} ({phone_id}): Added to whitelist")

    def remove_from_whitelist(self, phone_id: str):
        """
        Remove device from whitelist.

        Args:
            phone_id: Cloud phone ID
        """
        if phone_id in self.whitelist:
            self.whitelist.remove(phone_id)
            display_name = self._display_names.get(phone_id, phone_id)
            print(f"📋 {display_name} ({phone_id}): Removed from whitelist")

    def stop_all(self):
        """
        Stop all active cloud phones.
        """
        if not self.devices:
            return

        print(f"\n🛑 Stopping all active cloud phones ({len(self.devices)} devices)...")
        for phone_id in list(self.devices.keys()):
            self.stop_phone(phone_id)

        # Save logs
        self.client.save_log()

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit - stop all phones."""
        self.stop_monitor()
        self.stop_all()
        return False


# Example usage
if __name__ == "__main__":
    print("PhoneManager Example Usage\n")

    # Create manager with 5-minute timeout
    manager = PhoneManager(timeout_minutes=5)

    # Start background monitoring
    manager.start_monitor()

    # Connect to device (this is just example, you'd use real IP/port/pwd)
    # d = manager.connect_device("phone_id_123", "1.2.3.4", "5555", "password", name="Android15")

    # Record activity periodically to prevent auto-close
    def long_running_task():
        for i in range(10):
            print(f"Task step {i+1}/10")
            manager.record_activity("phone_id_123")
            time.sleep(30)  # Record activity every 30 seconds

    # Run long task
    # long_running_task()

    # Phone will auto-close after 5 minutes of no activity

    print("✅ Example completed")


path: scripts/utils.py
content: #!/usr/bin/env python3
"""
Shared utilities for GeeLark API Skill

This module provides common utility functions used across multiple scripts.
"""

import os
import uuid
import json
from typing import Dict, Optional
from pathlib import Path


def generate_traceid() -> str:
    """
    Generate a trace ID for API requests.

    Returns:
        str: Standard UUID4 format (with hyphens), in uppercase
    """
    return str(uuid.uuid4()).upper()


def load_config(config_path: Optional[str] = None) -> Dict:
    """
    Load GeeLark API configuration from config file.

    Args:
        config_path: Path to config file (optional, defaults to assets/config.json)

    Returns:
        dict: Configuration dictionary

    Raises:
        FileNotFoundError: If config file does not exist
        ValueError: If config file is invalid or missing required fields
    """
    if config_path is None:
        # Use pathlib for robust path resolution
        script_dir = Path(__file__).resolve().parent
        config_path = script_dir.parent / 'assets' / 'config.json'

    config_path = str(config_path)

    if not os.path.exists(config_path):
        raise FileNotFoundError(f"Config file not found: {config_path}")

    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            config = json.load(f)
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON in config file: {e}")

    # Validate required fields
    if 'auth' not in config:
        raise ValueError("Invalid config: missing 'auth' section")

    if 'token' not in config['auth']:
        raise ValueError("Invalid config: missing 'token' in 'auth' section")

    if 'baseUrl' not in config:
        raise ValueError("Invalid config: missing 'baseUrl' section")

    return config


def get_token(config_path: Optional[str] = None) -> str:
    """
    Get API token from config file.

    Args:
        config_path: Path to config file (optional)

    Returns:
        str: API token
    """
    config = load_config(config_path)
    return config['auth']['token']


def get_base_url(config_path: Optional[str] = None) -> str:
    """
    Get base URL from config file.

    Args:
        config_path: Path to config file (optional)

    Returns:
        str: Base URL from config
    """
    config = load_config(config_path)
    return config['baseUrl']


if __name__ == "__main__":
    # Test utility functions
    print("Testing utils.py...")

    # Test generate_traceid
    trace_id = generate_traceid()
    print(f"✅ generate_traceid(): {trace_id}")

    # Test load_config
    try:
        config = load_config()
        print(f"✅ load_config(): Loaded config with {len(config)} keys")
        print(f"   baseUrl: {config.get('baseUrl')}")
        print(f"   [REDACTED_SECRET]'auth']['token'][:20]}...")
    except Exception as e:
        print(f"❌ load_config(): {e}")

    print("\n✅ All tests completed!")

path: assets/config_template.json
content: {
  "auth": {
    "token": "your_bearer_token_here",
    "appId": "your_app_id_here",
    "apiKey": "your_api_key_here"
  },
  "baseUrl": "https://openapi.geelark.com",
  "rateLimit": {
    "perMinute": 200,
    "perHour": 24000
  }
}

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)