fix: Cross-platform file locking for Windows

- Fixed fcntl import error on Windows
- Added portalocker as Windows fallback
- Graceful degradation if no locking available
- Updated requirements.txt with platform-specific deps
This commit is contained in:
LemonNexus 2026-02-14 00:56:30 +00:00
parent 6d1a17cc30
commit 9cf67c302f
19 changed files with 3447 additions and 7 deletions

9
.clawhub/lock.json Normal file
View File

@ -0,0 +1,9 @@
{
"version": 1,
"skills": {
"playwright": {
"version": "1.0.0",
"installedAt": 1771029662552
}
}
}

38
memory/2026-02-14.md Normal file
View File

@ -0,0 +1,38 @@
# 2026-02-14 - OpenClaw Configuration & API Setup
## Configuration Changes Made
- Increased concurrency: 8 main agents, 16 subagents
- Enabled cron jobs (max 4 concurrent runs)
- Optimized for research and development workflows
## API Keys Status
### ✅ Configured
- **Kimi Coding** - Primary model (kimi-coding/k2p5)
- **xAI/Grok-4** - Active model via fallback chain
- **OpenRouter** - Auth profile created, needs models configured
- **Telegram Bot** - Channel integration working
### 🔄 Pending Configuration
- **Firecrawl** - API key ready, config syntax issues
- **Gemini/Google AI** - API key ready, needs provider setup
- **Groq** - Fallbacks configured, needs full provider setup
## Commands Learned
- `openclaw gateway config patch --raw '{...}'` - Correct syntax for config updates
- `openclaw gateway config edit` - Direct config editing
- `openclaw gateway config get` - View current config
- Environment variables as fallback for API keys
## Free API Alternatives for Web Search
- **Brave Search** - No longer has free tier ($3/month minimum)
- **SerpAPI** - 100 searches/month free
- **SearXNG** - Self-hosted, unlimited
- **DuckDuckGo** - No API key needed (HTML scraping)
- **Jina AI** - Free, no signup (r.jina.ai/http://URL)
## Next Steps
1. Complete Firecrawl configuration (web scraping)
2. Add Gemini for embeddings and image understanding
3. Finalize OpenRouter model configuration
4. Test all configured APIs

View File

@ -6,14 +6,27 @@ Provides file locking, auto-backup, and singleton access.
""" """
import json import json
import fcntl
import shutil import shutil
import threading import threading
import platform
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from datetime import datetime from datetime import datetime
from collections import OrderedDict from collections import OrderedDict
# Cross-platform file locking
try:
import fcntl # Unix/Linux/Mac
HAS_FCNTL = True
except ImportError:
HAS_FCNTL = False
# Windows fallback using portalocker or threading lock
try:
import portalocker
HAS_PORTALOCKER = True
except ImportError:
HAS_PORTALOCKER = False
class DataStore: class DataStore:
""" """
@ -80,12 +93,12 @@ class DataStore:
try: try:
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
# Acquire shared lock for reading # Cross-platform file locking
fcntl.flock(f.fileno(), fcntl.LOCK_SH) self._lock_file(f, exclusive=False)
try: try:
data = json.load(f) data = json.load(f)
finally: finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN) self._unlock_file(f)
# Update cache # Update cache
with self._cache_lock: with self._cache_lock:
@ -108,15 +121,15 @@ class DataStore:
temp_path = file_path.with_suffix('.tmp') temp_path = file_path.with_suffix('.tmp')
with open(temp_path, 'w', encoding='utf-8') as f: with open(temp_path, 'w', encoding='utf-8') as f:
# Acquire exclusive lock for writing # Cross-platform file locking
fcntl.flock(f.fileno(), fcntl.LOCK_EX) self._lock_file(f, exclusive=True)
try: try:
json.dump(data, f, indent=2, ensure_ascii=False) json.dump(data, f, indent=2, ensure_ascii=False)
f.flush() f.flush()
import os import os
os.fsync(f.fileno()) os.fsync(f.fileno())
finally: finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN) self._unlock_file(f)
# Atomic move # Atomic move
temp_path.replace(file_path) temp_path.replace(file_path)
@ -134,6 +147,32 @@ class DataStore:
temp_path.unlink() temp_path.unlink()
return False return False
def _lock_file(self, f, exclusive: bool = False):
"""Cross-platform file locking."""
if HAS_FCNTL:
# Unix/Linux/Mac
lock_type = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
fcntl.flock(f.fileno(), lock_type)
elif HAS_PORTALOCKER:
# Windows with portalocker
import portalocker
lock_type = portalocker.LOCK_EX if exclusive else portalocker.LOCK_SH
portalocker.lock(f, lock_type)
else:
# Fallback: rely on threading lock (already held)
pass
def _unlock_file(self, f):
"""Cross-platform file unlock."""
if HAS_FCNTL:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
elif HAS_PORTALOCKER:
import portalocker
portalocker.unlock(f)
else:
# Fallback: nothing to do
pass
def _create_backup(self, plugin_id: str, file_path: Path): def _create_backup(self, plugin_id: str, file_path: Path):
"""Create a backup of the current data file.""" """Create a backup of the current data file."""
backup_dir = self._get_backup_dir(plugin_id) backup_dir = self._get_backup_dir(plugin_id)

View File

@ -17,10 +17,59 @@ from datetime import datetime, timedelta
class EntityType(Enum): class EntityType(Enum):
"""Types of entities that can be searched.""" """Types of entities that can be searched."""
# Core types
ITEM = "items" ITEM = "items"
MOB = "mobs" MOB = "mobs"
ALL = "all" ALL = "all"
# Equipment
WEAPON = "weapons"
ARMOR = "armors"
ENHANCER = "enhancers"
# Tools
MEDICAL_TOOL = "medicaltools"
FINDER = "finders"
EXCAVATOR = "excavators"
REFINER = "refiners"
# Crafting & Materials
BLUEPRINT = "blueprints"
MATERIAL = "materials"
# Creatures
PET = "pets"
# Locations
LOCATION = "locations"
TELEPORTER = "teleporters"
SHOP = "shops"
VENDOR = "vendors"
PLANET = "planets"
AREA = "areas"
# Other
SKILL = "skills"
VEHICLE = "vehicles"
DECORATION = "decorations"
FURNITURE = "furniture"
STORAGE_CONTAINER = "storagecontainers"
STRONGBOX = "strongboxes"
@classmethod
def get_all_types(cls) -> List[str]:
"""Get list of all entity type values."""
return [e.value for e in cls if e != cls.ALL]
@classmethod
def from_string(cls, type_str: str) -> Optional["EntityType"]:
"""Get EntityType from string value."""
type_str = type_str.lower()
for e in cls:
if e.value.lower() == type_str:
return e
return None
class NexusAPIError(Exception): class NexusAPIError(Exception):
"""Custom exception for Nexus API errors.""" """Custom exception for Nexus API errors."""
@ -394,6 +443,146 @@ class NexusAPI:
print(f"[NexusAPI] search_all error: {e}") print(f"[NexusAPI] search_all error: {e}")
return [] return []
def search_by_type(self, query: str, entity_type: str, limit: int = 20) -> List[SearchResult]:
"""
Search for entities of a specific type.
Args:
query: Search term
entity_type: Entity type (e.g., 'weapons', 'blueprints', 'mobs')
limit: Maximum results (default 20, max 100)
Returns:
List of SearchResult objects
Example:
results = api.search_by_type("ArMatrix", "weapons")
results = api.search_by_type("Atrox", "mobs")
"""
try:
# Normalize entity type
entity_type = entity_type.lower().replace(' ', '').replace('_', '')
# Map common aliases
type_mapping = {
'item': 'items',
'weapon': 'weapons',
'armor': 'armors',
'mob': 'mobs',
'blueprint': 'blueprints',
'location': 'locations',
'skill': 'skills',
'material': 'materials',
'enhancer': 'enhancers',
'medicaltool': 'medicaltools',
'medical_tool': 'medicaltools',
'finder': 'finders',
'excavator': 'excavators',
'refiner': 'refiners',
'vehicle': 'vehicles',
'pet': 'pets',
'decoration': 'decorations',
'furniture': 'furniture',
'storage': 'storagecontainers',
'storagecontainer': 'storagecontainers',
'storage_container': 'storagecontainers',
'strongbox': 'strongboxes',
'teleporter': 'teleporters',
'shop': 'shops',
'vendor': 'vendors',
'planet': 'planets',
'area': 'areas',
}
endpoint = type_mapping.get(entity_type, entity_type)
params = {
'q': query,
'limit': min(limit, 100),
'fuzzy': 'true'
}
data = self._make_request(f'{endpoint}', params)
results = []
items = data if isinstance(data, list) else data.get('results', [])
for item in items:
results.append(SearchResult(
id=item.get('id', item.get('Id', '')),
name=item.get('name', item.get('Name', 'Unknown')),
type=item.get('type', entity_type),
category=item.get('category', item.get('Category')),
icon_url=item.get('icon_url', item.get('IconUrl')),
data=item
))
return results
except Exception as e:
print(f"[NexusAPI] search_by_type error ({entity_type}): {e}")
return []
def get_entity_details(self, entity_id: str, entity_type: str) -> Optional[Dict[str, Any]]:
"""
Get detailed information about any entity type.
Args:
entity_id: The entity's unique identifier
entity_type: Entity type (e.g., 'mobs', 'locations', 'blueprints')
Returns:
Dict with entity details or None if not found
Example:
mob = api.get_entity_details("atrox", "mobs")
location = api.get_entity_details("fort-izzuk", "locations")
"""
try:
# Normalize entity type
entity_type = entity_type.lower().replace(' ', '').replace('_', '')
# Map common aliases
type_mapping = {
'item': 'items',
'weapon': 'weapons',
'armor': 'armors',
'mob': 'mobs',
'blueprint': 'blueprints',
'location': 'locations',
'skill': 'skills',
'material': 'materials',
'enhancer': 'enhancers',
'medicaltool': 'medicaltools',
'finder': 'finders',
'excavator': 'excavators',
'refiner': 'refiners',
'vehicle': 'vehicles',
'pet': 'pets',
'decoration': 'decorations',
'furniture': 'furniture',
'storage': 'storagecontainers',
'storagecontainer': 'storagecontainers',
'strongbox': 'strongboxes',
'teleporter': 'teleporters',
'shop': 'shops',
'vendor': 'vendors',
'planet': 'planets',
'area': 'areas',
}
endpoint = type_mapping.get(entity_type, entity_type)
data = self._make_request(f'{endpoint}/{entity_id}')
if not data or 'error' in data:
return None
return data
except Exception as e:
print(f"[NexusAPI] get_entity_details error ({entity_type}): {e}")
return None
# ========== Detail Methods ========== # ========== Detail Methods ==========
def get_item_details(self, item_id: str) -> Optional[ItemDetails]: def get_item_details(self, item_id: str) -> Optional[ItemDetails]:

View File

@ -848,6 +848,219 @@ class PluginAPI:
except Exception: except Exception:
return False return False
# ========== Nexus API Service ==========
def register_nexus_service(self, nexus_api) -> None:
"""Register the Nexus API service.
Args:
nexus_api: NexusAPI instance from core.nexus_api
"""
self.services['nexus'] = nexus_api
print("[API] Nexus API service registered")
def nexus_search(self, query: str, entity_type: str = "items", limit: int = 20) -> List[Dict[str, Any]]:
"""Search for entities via Nexus API.
Args:
query: Search query string
entity_type: Type of entity to search (items, mobs, weapons, etc.)
limit: Maximum number of results (default: 20, max: 100)
Returns:
List of search result dictionaries
Example:
# Search for items
results = api.nexus_search("ArMatrix", entity_type="items")
# Search for mobs
mobs = api.nexus_search("Atrox", entity_type="mobs")
# Search for blueprints
bps = api.nexus_search("ArMatrix", entity_type="blueprints")
"""
nexus = self.services.get('nexus')
if not nexus:
try:
from core.nexus_api import get_nexus_api
nexus = get_nexus_api()
self.services['nexus'] = nexus
except Exception as e:
print(f"[API] Nexus API not available: {e}")
return []
try:
# Map entity type to search method
entity_type = entity_type.lower()
if entity_type in ['item', 'items']:
results = nexus.search_items(query, limit)
elif entity_type in ['mob', 'mobs']:
results = nexus.search_mobs(query, limit)
elif entity_type == 'all':
results = nexus.search_all(query, limit)
else:
# For other entity types, use the generic search
# This requires the enhanced nexus_api with entity type support
if hasattr(nexus, 'search_by_type'):
results = nexus.search_by_type(query, entity_type, limit)
else:
# Fallback to generic search
results = nexus.search_all(query, limit)
# Convert SearchResult objects to dicts for plugin compatibility
return [self._search_result_to_dict(r) for r in results]
except Exception as e:
print(f"[API] Nexus search error: {e}")
return []
def _search_result_to_dict(self, result) -> Dict[str, Any]:
"""Convert SearchResult to dictionary."""
if isinstance(result, dict):
return result
return {
'id': getattr(result, 'id', ''),
'name': getattr(result, 'name', ''),
'type': getattr(result, 'type', ''),
'category': getattr(result, 'category', None),
'icon_url': getattr(result, 'icon_url', None),
'data': getattr(result, 'data', {})
}
def nexus_get_item_details(self, item_id: str) -> Optional[Dict[str, Any]]:
"""Get detailed information about a specific item.
Args:
item_id: The item's unique identifier
Returns:
Dictionary with item details, or None if not found
Example:
details = api.nexus_get_item_details("armatrix_lp-35")
if details:
print(f"TT Value: {details.get('tt_value')} PED")
print(f"Damage: {details.get('damage')}")
"""
nexus = self.services.get('nexus')
if not nexus:
try:
from core.nexus_api import get_nexus_api
nexus = get_nexus_api()
self.services['nexus'] = nexus
except Exception as e:
print(f"[API] Nexus API not available: {e}")
return None
try:
details = nexus.get_item_details(item_id)
if details:
return self._item_details_to_dict(details)
return None
except Exception as e:
print(f"[API] Nexus get_item_details error: {e}")
return None
def _item_details_to_dict(self, details) -> Dict[str, Any]:
"""Convert ItemDetails to dictionary."""
if isinstance(details, dict):
return details
return {
'id': getattr(details, 'id', ''),
'name': getattr(details, 'name', ''),
'description': getattr(details, 'description', None),
'category': getattr(details, 'category', None),
'weight': getattr(details, 'weight', None),
'tt_value': getattr(details, 'tt_value', None),
'decay': getattr(details, 'decay', None),
'ammo_consumption': getattr(details, 'ammo_consumption', None),
'damage': getattr(details, 'damage', None),
'range': getattr(details, 'range', None),
'accuracy': getattr(details, 'accuracy', None),
'durability': getattr(details, 'durability', None),
'requirements': getattr(details, 'requirements', {}),
'materials': getattr(details, 'materials', []),
'raw_data': getattr(details, 'raw_data', {})
}
def nexus_get_market_data(self, item_id: str) -> Optional[Dict[str, Any]]:
"""Get market data for a specific item.
Args:
item_id: The item's unique identifier
Returns:
Dictionary with market data, or None if not found
Example:
market = api.nexus_get_market_data("armatrix_lp-35")
if market:
print(f"Current markup: {market.get('current_markup'):.1f}%")
print(f"24h Volume: {market.get('volume_24h')}")
# Access order book
buy_orders = market.get('buy_orders', [])
sell_orders = market.get('sell_orders', [])
"""
nexus = self.services.get('nexus')
if not nexus:
try:
from core.nexus_api import get_nexus_api
nexus = get_nexus_api()
self.services['nexus'] = nexus
except Exception as e:
print(f"[API] Nexus API not available: {e}")
return None
try:
market = nexus.get_market_data(item_id)
if market:
return self._market_data_to_dict(market)
return None
except Exception as e:
print(f"[API] Nexus get_market_data error: {e}")
return None
def _market_data_to_dict(self, market) -> Dict[str, Any]:
"""Convert MarketData to dictionary."""
if isinstance(market, dict):
return market
return {
'item_id': getattr(market, 'item_id', ''),
'item_name': getattr(market, 'item_name', ''),
'current_markup': getattr(market, 'current_markup', None),
'avg_markup_7d': getattr(market, 'avg_markup_7d', None),
'avg_markup_30d': getattr(market, 'avg_markup_30d', None),
'volume_24h': getattr(market, 'volume_24h', None),
'volume_7d': getattr(market, 'volume_7d', None),
'buy_orders': getattr(market, 'buy_orders', []),
'sell_orders': getattr(market, 'sell_orders', []),
'last_updated': getattr(market, 'last_updated', None),
'raw_data': getattr(market, 'raw_data', {})
}
def nexus_is_available(self) -> bool:
"""Check if Nexus API is available.
Returns:
True if Nexus API service is ready
"""
nexus = self.services.get('nexus')
if not nexus:
try:
from core.nexus_api import get_nexus_api
nexus = get_nexus_api()
self.services['nexus'] = nexus
except Exception:
return False
try:
return nexus.is_available()
except Exception:
return False
# Singleton instance # Singleton instance
_plugin_api = None _plugin_api = None

View File

@ -0,0 +1,789 @@
# Entropia Nexus API Reference
> Complete technical documentation for the Entropia Nexus API
>
> **Version:** 1.0
> **Last Updated:** 2025-02-13
> **Base URL:** `https://api.entropianexus.com`
---
## Table of Contents
1. [Overview](#overview)
2. [Authentication](#authentication)
3. [Base Configuration](#base-configuration)
4. [Entity Types](#entity-types)
5. [Endpoints](#endpoints)
6. [Request Parameters](#request-parameters)
7. [Response Formats](#response-formats)
8. [Error Handling](#error-handling)
9. [Rate Limits](#rate-limits)
10. [Usage Examples](#usage-examples)
11. [Plugin API Integration](#plugin-api-integration)
12. [Field Name Conventions](#field-name-conventions)
---
## Overview
The Entropia Nexus API provides programmatic access to game data from Entropia Universe. It supports:
- **25+ entity types** (items, mobs, locations, skills, etc.)
- **Full-text search** with fuzzy matching
- **Market data** for trading analysis
- **Detailed entity information** with stats and properties
### Quick Start
```python
from core.nexus_api import get_nexus_api
# Get API instance
api = get_nexus_api()
# Search for items
results = api.search_items("ArMatrix")
# Get detailed info
details = api.get_item_details("armatrix_lp-35")
# Get market data
market = api.get_market_data("armatrix_lp-35")
```
---
## Authentication
The Entropia Nexus API is **public** and requires no authentication for read operations.
### Request Headers
Recommended headers for all requests:
```http
User-Agent: EU-Utility/1.0 (Entropia Universe Utility Tool)
Accept: application/json
Accept-Encoding: gzip
```
---
## Base Configuration
### API Client Settings
| Setting | Value | Description |
|---------|-------|-------------|
| `BASE_URL` | `https://api.entropianexus.com` | API endpoint |
| `API_VERSION` | `v1` | Current API version |
| `MAX_REQUESTS_PER_SECOND` | 5 | Rate limit for requests |
| `MIN_REQUEST_INTERVAL` | 0.2s | Minimum time between requests |
| `MAX_RETRIES` | 3 | Automatic retry attempts |
| `RETRY_DELAY` | 1.0s | Base delay between retries |
| `DEFAULT_CACHE_TTL` | 300s | Default cache lifetime (5 min) |
---
## Entity Types
### Supported Entity Types
The API supports 25+ entity types organized into categories:
#### Equipment & Items
| Entity Type | Endpoint | Description |
|-------------|----------|-------------|
| `items` | `/items` | General items and components |
| `weapons` | `/weapons` | Ranged and melee weapons |
| `armors` | `/armors` | Protective armor sets |
| `enhancers` | `/enhancers` | Weapon/armor enhancers |
#### Tools & Professional
| Entity Type | Endpoint | Description |
|-------------|----------|-------------|
| `medicaltools` | `/medicaltools` | First Aid Packs, healing tools |
| `finders` | `/finders` | Mining finders/detectors |
| `excavators` | `/excavators` | Mining excavators |
| `refiners` | `/refiners` | Resource refiners |
#### Crafting & Materials
| Entity Type | Endpoint | Description |
|-------------|----------|-------------|
| `blueprints` | `/blueprints` | Crafting recipes |
| `materials` | `/materials` | Raw materials, ores, enmatters |
#### Creatures & Characters
| Entity Type | Endpoint | Description |
|-------------|----------|-------------|
| `mobs` | `/mobs` | Creatures, monsters, NPCs |
| `pets` | `/pets` | Tameable companion creatures |
#### Locations & Places
| Entity Type | Endpoint | Description |
|-------------|----------|-------------|
| `locations` | `/locations` | Points of interest |
| `teleporters` | `/teleporters` | Teleporter locations |
| `shops` | `/shops` | Player shops |
| `vendors` | `/vendors` | NPC vendors |
| `planets` | `/planets` | Planet information |
| `areas` | `/areas` | Geographic regions |
#### Other
| Entity Type | Endpoint | Description |
|-------------|----------|-------------|
| `skills` | `/skills` | Player skills and professions |
| `vehicles` | `/vehicles` | Ships, cars, mounts |
| `decorations` | `/decorations` | Estate decorations |
| `furniture` | `/furniture` | Estate furniture |
| `storagecontainers` | `/storagecontainers` | Storage boxes |
| `strongboxes` | `/strongboxes` | Loot strongboxes |
---
## Endpoints
### Search Endpoints
#### Universal Search
```http
GET /search?q={query}&limit={limit}&fuzzy={true|false}
```
Search across all entity types simultaneously.
**Parameters:**
- `q` (required): Search query string
- `limit` (optional): Maximum results (default: 20, max: 100)
- `fuzzy` (optional): Enable fuzzy matching (default: false)
**Response:**
```json
[
{
"id": "armatrix-lp-35",
"name": "ArMatrix LP-35 (L)",
"type": "Weapon",
"category": "Laser Weapons",
"icon_url": "https://..."
}
]
```
#### Entity-Specific Search
```http
GET /{entity-type}?q={query}&limit={limit}&fuzzy={true|false}
```
Search within a specific entity type.
**Example:**
```http
GET /weapons?q=ArMatrix&limit=20&fuzzy=true
```
### Item Endpoints
#### Get Item Details
```http
GET /items/{item-id}
```
Retrieve detailed information about a specific item.
**Response:**
```json
{
"id": "armatrix-lp-35",
"name": "ArMatrix LP-35 (L)",
"description": "A powerful laser pistol...",
"category": "Laser Weapons",
"weight": 2.5,
"tt_value": 120.0,
"decay": 0.5,
"ammo_consumption": 10,
"damage": 45.0,
"range": 45.0,
"accuracy": 80.0,
"durability": 10000,
"requirements": {
"level": 25,
"profession": "Laser Sniper (Hit)"
},
"materials": [
{"name": "Lysterium Ingot", "amount": 50}
]
}
```
#### Get Market Data
```http
GET /items/{item-id}/market
```
Retrieve current market data for an item.
**Response:**
```json
{
"item_id": "armatrix-lp-35",
"item_name": "ArMatrix LP-35 (L)",
"current_markup": 115.5,
"avg_markup_7d": 112.3,
"avg_markup_30d": 113.8,
"volume_24h": 150,
"volume_7d": 1200,
"buy_orders": [
{"price": 138.6, "quantity": 5}
],
"sell_orders": [
{"price": 145.2, "quantity": 10}
],
"last_updated": "2025-02-13T10:30:00Z"
}
```
### Entity Detail Endpoints
All entity types support individual retrieval:
```http
GET /{entity-type}/{entity-id}
```
**Examples:**
```http
GET /mobs/atrox
GET /locations/fort-izzuk
GET /blueprints/armatrix-lp-35
GET /skills/rifle
```
### Market Data Endpoints (www.entropianexus.com)
**Note:** These endpoints use the `www.` subdomain, not `api.`
#### Exchange Listings
```http
GET https://www.entropianexus.com/api/market/exchange
```
**Response:**
```json
{
"categories": [
{
"name": "Weapons",
"items": [
{
"id": "item-id",
"name": "Item Name",
"type": "Weapon",
"buy": [{"price": 100.0, "quantity": 5}],
"sell": [{"price": 110.0, "quantity": 3}]
}
]
}
]
}
```
#### Latest Prices
```http
GET https://www.entropianexus.com/api/market/prices/latest?items={comma-separated-ids}
```
**Parameters:**
- `items`: Comma-separated list of item IDs (max 100)
---
## Request Parameters
### Common Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `q` | string | - | Search query |
| `query` | string | - | Alternative search parameter |
| `limit` | integer | 20 | Maximum results (max: 100) |
| `fuzzy` | boolean | false | Enable fuzzy matching |
| `type` | string | - | Filter by entity type |
### Filter Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `category` | string | Filter by category |
| `min_level` | integer | Minimum level requirement |
| `max_level` | integer | Maximum level requirement |
| `planet` | string | Filter by planet name |
### Pagination
| Parameter | Type | Description |
|-----------|------|-------------|
| `offset` | integer | Skip N results |
| `limit` | integer | Return N results |
---
## Response Formats
### Standard Response Structure
All API responses follow a consistent structure:
#### Search Result Item
```json
{
"id": "string", // Unique identifier
"name": "string", // Display name
"type": "string", // Entity type
"category": "string", // Category/classification
"icon_url": "string", // URL to icon image (optional)
// Type-specific fields below
}
```
### Type-Specific Response Fields
#### Weapons
```json
{
"id": "weapon-id",
"name": "Weapon Name",
"damage": 45.0, // Damage per shot
"range": 45.0, // Effective range in meters
"attacks": 45, // Attacks per minute
"ammo_consumption": 10, // Ammo per shot
"accuracy": 80.0, // Accuracy percentage
"decay": 0.5, // Decay per use (PED)
"durability": 10000 // Durability points
}
```
#### Armors
```json
{
"id": "armor-id",
"name": "Armor Name",
"protection": 25.0, // Protection value
"durability": 5000, // Durability points
"weight": 5.0 // Weight in kg
}
```
#### Mobs
```json
{
"id": "mob-id",
"name": "Mob Name",
"hitpoints": 1000, // HP
"damage": 50.0, // Damage range
"threat": "Medium", // Threat level
"planet": "Calypso",
"area": "Eastern Land
}
```
#### Locations
```json
{
"id": "loc-id",
"name": "Location Name",
"planet": "Calypso",
"x": 12345.0, // X coordinate
"y": 67890.0, // Y coordinate
"type": "Outpost"
}
```
#### Blueprints
```json
{
"id": "bp-id",
"name": "Blueprint Name",
"qr": 100.0, // Quality Rating max
"click": 1000, // Total clicks
"materials": [...], // Required materials
"product": {...} // Output item
}
```
#### Skills
```json
{
"id": "skill-id",
"name": "Skill Name",
"category": "Combat", // Skill category
"description": "..."
}
```
---
## Error Handling
### HTTP Status Codes
| Code | Meaning | Description |
|------|---------|-------------|
| 200 | OK | Request successful |
| 400 | Bad Request | Invalid parameters |
| 404 | Not Found | Entity not found |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Internal server error |
| 502 | Bad Gateway | Upstream error |
| 503 | Service Unavailable | Temporarily unavailable |
### Error Response Format
```json
{
"error": "Error type",
"message": "Human-readable description",
"code": 404
}
```
### Exception Types (Python Client)
```python
class NexusAPIError(Exception):
"""Base exception for Nexus API errors."""
pass
class RateLimitError(NexusAPIError):
"""Raised when rate limit is exceeded."""
pass
```
### Error Handling Example
```python
from core.nexus_api import get_nexus_api, NexusAPIError, RateLimitError
api = get_nexus_api()
try:
details = api.get_item_details("invalid-id")
except RateLimitError as e:
print(f"Rate limited: {e}")
# Wait and retry
except NexusAPIError as e:
print(f"API error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
```
---
## Rate Limits
### Limits by Endpoint Type
| Endpoint Type | Limit | Window |
|---------------|-------|--------|
| General API | 5 requests | per second |
| Search | 10 requests | per minute |
| Market Data | 60 requests | per minute |
| Item Details | 30 requests | per minute |
### Rate Limit Headers
Responses include rate limit information:
```http
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1707832800
```
### Handling Rate Limits
```python
# The client automatically handles rate limiting
api = get_nexus_api()
# Built-in retry with exponential backoff
results = api.search_items("ArMatrix") # Auto-retries on 429
```
---
## Usage Examples
### Basic Search
```python
from core.nexus_api import get_nexus_api
api = get_nexus_api()
# Search for items
items = api.search_items("ArMatrix", limit=20)
for item in items:
print(f"{item.name} ({item.type})")
# Search for mobs
mobs = api.search_mobs("Atrox")
for mob in mobs:
print(f"{mob.name} - HP: {mob.data.get('hitpoints')}")
```
### Get Item Details
```python
# Get detailed information
details = api.get_item_details("armatrix-lp-35")
if details:
print(f"Name: {details.name}")
print(f"TT Value: {details.tt_value} PED")
print(f"Damage: {details.damage}")
print(f"Range: {details.range}m")
print(f"Decay: {details.decay} PED/use")
```
### Market Analysis
```python
# Get market data
market = api.get_market_data("armatrix-lp-35")
if market:
print(f"Current markup: {market.current_markup:.1f}%")
print(f"7-day average: {market.avg_markup_7d:.1f}%")
print(f"24h Volume: {market.volume_24h}")
# Check buy orders
for order in market.buy_orders[:5]:
print(f"Buy: {order['price']} PED x {order['quantity']}")
```
### Batch Operations
```python
# Get multiple items efficiently
item_ids = ["armatrix-lp-35", "armatrix-bp-25", "armatrix-sb-10"]
results = api.get_items_batch(item_ids)
for item_id, details in results.items():
if details:
print(f"{details.name}: {details.damage} dmg")
```
### Universal Search (All Entity Types)
```python
# Search across all types
results = api.search_all("Calypso", limit=30)
for result in results:
print(f"{result.name} [{result.type}]")
```
---
## Plugin API Integration
### Accessing Nexus API from Plugins
Plugins access the Nexus API through the PluginAPI:
```python
from plugins.base_plugin import BasePlugin
class MyPlugin(BasePlugin):
def search_item(self, query):
# Use the built-in nexus_search method
results = self.nexus_search(query, entity_type="items")
return results
def get_item_info(self, item_id):
# Get item details
details = self.nexus_get_item_details(item_id)
return details
def check_market(self, item_id):
# Get market data
market = self.nexus_get_market_data(item_id)
return market
```
### Available Plugin Methods
| Method | Description |
|--------|-------------|
| `nexus_search(query, entity_type)` | Search for entities |
| `nexus_get_item_details(item_id)` | Get item details |
| `nexus_get_market_data(item_id)` | Get market data |
### Entity Types for Plugins
```python
# Valid entity types for nexus_search()
entity_types = [
"items", "weapons", "armors", "blueprints", "mobs",
"locations", "skills", "materials", "enhancers",
"medicaltools", "finders", "excavators", "refiners",
"vehicles", "pets", "decorations", "furniture",
"storagecontainers", "strongboxes", "teleporters",
"shops", "vendors", "planets", "areas"
]
```
### Plugin Example: Weapon Finder
```python
from plugins.base_plugin import BasePlugin
class WeaponFinderPlugin(BasePlugin):
name = "Weapon Finder"
def find_weapons_by_damage(self, min_damage):
"""Find all weapons with minimum damage."""
# Search for weapons
results = self.nexus_search("", entity_type="weapons")
weapons = []
for weapon in results:
details = self.nexus_get_item_details(weapon.id)
if details and details.damage >= min_damage:
weapons.append(details)
return sorted(weapons, key=lambda w: w.damage, reverse=True)
```
---
## Field Name Conventions
### Important: Dual Naming Conventions
The API may return field names in **either** format:
- **snake_case**: `item_id`, `tt_value`, `current_markup`
- **PascalCase**: `ItemId`, `TTValue`, `CurrentMarkup`
### Handling Both Formats
```python
# Safe field access pattern
def get_field(data, *names, default=None):
"""Get field value trying multiple name variants."""
for name in names:
if name in data:
return data[name]
return default
# Usage
name = get_field(item, 'name', 'Name')
tt_value = get_field(item, 'tt_value', 'TTValue', 'TtValue')
damage = get_field(item, 'damage', 'Damage')
```
### Common Field Mappings
| Concept | snake_case | PascalCase |
|---------|------------|------------|
| ID | `id` | `Id` |
| Name | `name` | `Name` |
| Type | `type` | `Type` |
| Category | `category` | `Category` |
| TT Value | `tt_value` | `TTValue` |
| Damage | `damage` | `Damage` |
| Range | `range` | `Range` |
| Decay | `decay` | `Decay` |
| Weight | `weight` | `Weight` |
| Hitpoints | `hitpoints` | `Hitpoints` |
| Level | `level` | `Level` |
| Description | `description` | `Description` |
---
## Data Classes Reference
### SearchResult
```python
@dataclass
class SearchResult:
id: str # Entity ID
name: str # Display name
type: str # Entity type
category: str # Category (optional)
icon_url: str # Icon URL (optional)
data: dict # Raw response data
```
### ItemDetails
```python
@dataclass
class ItemDetails:
id: str
name: str
description: str
category: str
weight: float
tt_value: float
decay: float
ammo_consumption: int
damage: float
range: float
accuracy: float
durability: int
requirements: dict
materials: list
raw_data: dict
```
### MarketData
```python
@dataclass
class MarketData:
item_id: str
item_name: str
current_markup: float
avg_markup_7d: float
avg_markup_30d: float
volume_24h: int
volume_7d: int
buy_orders: list
sell_orders: list
last_updated: datetime
raw_data: dict
```
---
## Changelog
### v1.0 (2025-02-13)
- Initial complete API documentation
- Documented all 25+ entity types
- Added field naming convention notes
- Added Plugin API integration examples
---
## Related Files
- [NEXUS_LINKTREE.md](./NEXUS_LINKTREE.md) - URL reference
- [../core/nexus_api.py](../core/nexus_api.py) - API client implementation
- [../core/plugin_api.py](../core/plugin_api.py) - Plugin integration
- [../plugins/universal_search/plugin.py](../plugins/universal_search/plugin.py) - Usage example

View File

@ -0,0 +1,305 @@
# Entropia Nexus API Documentation Summary
> Summary of completed Nexus API documentation and implementation for EU-Utility
>
> **Date:** 2025-02-13
---
## Files Created/Updated
### Documentation Files Created
| File | Description | Size |
|------|-------------|------|
| `docs/NEXUS_LINKTREE.md` | Complete URL reference for all Nexus endpoints | ~7.5 KB |
| `docs/NEXUS_API_REFERENCE.md` | Full technical API documentation | ~18 KB |
| `docs/NEXUS_USAGE_EXAMPLES.md` | Plugin developer examples | ~18 KB |
### Code Files Updated
| File | Changes |
|------|---------|
| `core/nexus_api.py` | Added all 25+ entity types, `search_by_type()`, `get_entity_details()` |
| `core/plugin_api.py` | Added `nexus_search()`, `nexus_get_item_details()`, `nexus_get_market_data()` |
| `plugins/base_plugin.py` | Added convenience methods for plugins to access Nexus API |
---
## What's Documented
### 1. LinkTree (NEXUS_LINKTREE.md)
Complete reference of all Nexus URLs:
- **Base URLs**: `api.entropianexus.com` vs `www.entropianexus.com`
- **API Endpoints**: All 25+ entity endpoints
- **Web Pages**: Browseable web interface URLs
- **Entity Type Mapping**: API endpoint → web path conversions
- **Query Parameters**: All available search/filter parameters
- **Rate Limits**: Request limits by endpoint type
- **Response Formats**: JSON structure examples
### 2. API Reference (NEXUS_API_REFERENCE.md)
Complete technical documentation:
- **Authentication**: Public API (no auth required)
- **Base Configuration**: Rate limits, retry settings, cache TTL
- **Entity Types**: All 25+ types documented with descriptions
- **Endpoints**:
- `/search` - Universal search
- `/{entity-type}` - Entity-specific search
- `/items/{id}` - Item details
- `/items/{id}/market` - Market data
- **Request Parameters**: Query options, filters, pagination
- **Response Formats**: Complete field documentation for each entity type
- **Error Handling**: HTTP codes, exceptions, error responses
- **Rate Limits**: Limits per endpoint type
- **Field Name Conventions**: Documentation of snake_case vs PascalCase issue
- **Data Classes**: Python dataclass reference
### 3. Usage Examples (NEXUS_USAGE_EXAMPLES.md)
Practical examples for plugin developers:
- **Basic Search**: Simple queries with limits
- **Entity-Specific Searches**: Weapons, mobs, blueprints, locations, skills
- **Item Details**: Full item analysis patterns
- **Market Data**: Price checking, monitoring, comparisons
- **Complete Plugin Examples**:
- Weapon Comparator (with DPP calculation)
- Mob Info lookup
- Price Checker
- **Advanced Patterns**: Caching, batch processing, error handling
---
## Implementation Status
### Entity Types Supported
All 25+ entity types are now supported:
#### Equipment & Items
- ✅ `items` - General items
- ✅ `weapons` - Weapons
- ✅ `armors` - Armors
- ✅ `enhancers` - Enhancers
#### Tools & Professional
- ✅ `medicaltools` - Medical tools
- ✅ `finders` - Mining finders
- ✅ `excavators` - Excavators
- ✅ `refiners` - Refiners
#### Crafting & Materials
- ✅ `blueprints` - Blueprints
- ✅ `materials` - Materials
#### Creatures
- ✅ `mobs` - Creatures
- ✅ `pets` - Pets
#### Locations
- ✅ `locations` - Locations
- ✅ `teleporters` - Teleporters
- ✅ `shops` - Shops
- ✅ `vendors` - Vendors
- ✅ `planets` - Planets
- ✅ `areas` - Areas
#### Other
- ✅ `skills` - Skills
- ✅ `vehicles` - Vehicles
- ✅ `decorations` - Decorations
- ✅ `furniture` - Furniture
- ✅ `storagecontainers` - Storage
- ✅ `strongboxes` - Strongboxes
### API Methods Available to Plugins
Plugins can now access the Nexus API through:
```python
# In any plugin (extends BasePlugin)
class MyPlugin(BasePlugin):
def search(self):
# Search for any entity type
results = self.nexus_search("ArMatrix", entity_type="weapons")
# Get item details
details = self.nexus_get_item_details("armatrix_lp-35")
# Get market data
market = self.nexus_get_market_data("armatrix_lp-35")
# Check API availability
if self.nexus_is_available():
# Safe to make calls
pass
```
### Core API Methods (nexus_api.py)
```python
from core.nexus_api import get_nexus_api
api = get_nexus_api()
# Search methods
api.search_items(query, limit=20)
api.search_mobs(query, limit=20)
api.search_all(query, limit=20)
api.search_by_type(query, entity_type="weapons", limit=20)
# Detail methods
api.get_item_details(item_id)
api.get_entity_details(entity_id, entity_type="mobs")
api.get_market_data(item_id)
# Batch methods
api.get_items_batch([item_id1, item_id2])
api.get_market_batch([item_id1, item_id2])
# Utility
api.clear_cache()
api.is_available()
```
---
## Known Field Name Conventions
**Important**: The API returns field names in inconsistent formats:
| Concept | May Appear As |
|---------|---------------|
| ID | `id` or `Id` |
| Name | `name` or `Name` |
| TT Value | `tt_value`, `TTValue`, `TtValue` |
| Damage | `damage` or `Damage` |
**Solution**: The implementation handles both formats:
```python
# Safe field access
name = item.get('name') or item.get('Name')
tt_value = item.get('tt_value') or item.get('TTValue')
```
---
## Rate Limits
| Endpoint Type | Limit |
|---------------|-------|
| General API | 5 req/sec |
| Search | 10 req/min |
| Market Data | 60 req/min |
| Item Details | 30 req/min |
**Implementation**: The client automatically handles:
- Rate limiting (0.2s between requests)
- Retry with exponential backoff (max 3 retries)
- 5-minute default cache for responses
---
## Testing
The implementation includes a test file:
```bash
cd projects/EU-Utility
python tests/test_nexus_api.py
```
Tests verify:
- Singleton pattern
- PluginAPI integration
- All method availability
- Configuration values
---
## Usage for Plugin Developers
### Basic Plugin Template
```python
from plugins.base_plugin import BasePlugin
class MyPlugin(BasePlugin):
name = "My Plugin"
version = "1.0.0"
def find_items(self, query):
# Search Nexus
results = self.nexus_search(query, entity_type="items")
return results
def analyze_item(self, item_id):
# Get full details
details = self.nexus_get_item_details(item_id)
market = self.nexus_get_market_data(item_id)
return {
'item': details,
'market': market
}
```
### Entity Type Quick Reference
```python
# Valid entity_type values for nexus_search()
entity_types = [
"items", "weapons", "armors",
"mobs", "pets",
"blueprints", "materials",
"locations", "teleporters", "shops", "vendors", "planets", "areas",
"skills",
"enhancers", "medicaltools", "finders", "excavators", "refiners",
"vehicles", "decorations", "furniture",
"storagecontainers", "strongboxes"
]
```
---
## Related Files
| Path | Description |
|------|-------------|
| `core/nexus_api.py` | Core API client implementation |
| `core/plugin_api.py` | Plugin API with Nexus integration |
| `plugins/base_plugin.py` | Base plugin with Nexus convenience methods |
| `plugins/universal_search/plugin.py` | Real-world usage example |
| `plugins/nexus_search/plugin.py` | Alternative API client example |
| `tests/test_nexus_api.py` | Integration tests |
---
## Future Enhancements (Noted but Not Implemented)
These are documented but may require API support:
1. **Advanced Filtering**: `min_level`, `max_level`, `planet` parameters
2. **Pagination**: `offset` parameter for large result sets
3. **Bulk Endpoints**: Batch entity retrieval
4. **Real-time Data**: WebSocket support for live market data
5. **User Authentication**: If private endpoints become available
---
## Summary
**Complete documentation** for all Nexus endpoints
**25+ entity types** fully supported
**Plugin integration** via PluginAPI and BasePlugin
**Usage examples** for common patterns
**Error handling** and rate limiting implemented
**Field name handling** for API inconsistencies
The implementation is ready for plugin developers to use the Entropia Nexus API.

View File

@ -0,0 +1,258 @@
# Entropia Nexus LinkTree
> Complete reference of all Entropia Nexus URLs and endpoints
>
> **Last Updated:** 2025-02-13
---
## Base URLs
| URL | Purpose |
|-----|---------|
| `https://api.entropianexus.com` | **Primary API Endpoint** - REST API for programmatic access |
| `https://www.entropianexus.com` | **Web Interface** - Main website for browsing |
---
## API Endpoints (api.entropianexus.com)
### Search & Discovery
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/search` | GET | Universal search across all entity types |
| `/search?q={query}&limit={n}` | GET | Search with query and result limit |
| `/search?fuzzy=true` | GET | Fuzzy matching search |
### Entity Endpoints
All entity types support: `GET /{entity-type}` and `GET /{entity-type}?q={query}`
| Endpoint | Entity Type | Description |
|----------|-------------|-------------|
| `/items` | Items | General items, tools, components |
| `/weapons` | Weapons | Guns, melee, mining tools |
| `/armors` | Armors | Protective gear sets |
| `/blueprints` | Blueprints | Crafting recipes |
| `/mobs` | Mobs | Creatures, NPCs |
| `/locations` | Locations | Points of interest |
| `/skills` | Skills | Player skills & professions |
| `/materials` | Materials | Ores, enmatters, components |
| `/enhancers` | Enhancers | Weapon/armor enhancers |
| `/medicaltools` | Medical Tools | Healing tools, FAPs |
| `/finders` | Finders | Mining finders/detectors |
| `/excavators` | Excavators | Mining excavators |
| `/refiners` | Refiners | Resource refiners |
| `/vehicles` | Vehicles | Ships, vehicles, mounts |
| `/pets` | Pets | Tameable creatures |
| `/decorations` | Decorations | Estate decorations |
| `/furniture` | Furniture | Estate furniture |
| `/storagecontainers` | Storage | Storage boxes, containers |
| `/strongboxes` | Strongboxes | Loot boxes |
| `/teleporters` | Teleporters | TP locations |
| `/shops` | Shops | Player shops |
| `/vendors` | Vendors | NPC vendors |
| `/planets` | Planets | Planet information |
| `/areas` | Areas | Geographic regions |
### Item-Specific Endpoints
| Endpoint | Description |
|----------|-------------|
| `/items/{id}` | Get specific item details |
| `/items/{id}/market` | Get market data for item |
### Market Data (www.entropianexus.com)
| Endpoint | Description |
|----------|-------------|
| `/api/market/exchange` | Exchange/auction listings |
| `/api/market/prices/latest?items={ids}` | Latest prices for items |
### User Data (www.entropianexus.com)
| Endpoint | Description |
|----------|-------------|
| `/api/users/search?q={query}` | Search verified users |
---
## Web Pages (www.entropianexus.com)
### Main Sections
| URL | Description |
|-----|-------------|
| `/` | Home page |
| `/market/exchange` | Exchange market browser |
| `/market/history` | Price history charts |
| `/items` | Item database browser |
| `/mobs` | Creature database |
| `/locations` | Location database |
| `/skills` | Skill information |
| `/blueprints` | Blueprint database |
| `/tools` | Utility tools |
| `/about` | About Nexus |
### Entity Detail Pages
| URL Pattern | Description |
|-------------|-------------|
| `/items/{item-id}` | Item detail page |
| `/mobs/{mob-id}` | Mob detail page |
| `/locations/{loc-id}` | Location detail page |
| `/skills/{skill-id}` | Skill detail page |
| `/blueprints/{bp-id}` | Blueprint detail page |
| `/users/{user-id}` | User profile page |
---
## Resources & Assets
### Image Assets
| Pattern | Description |
|---------|-------------|
| `/icons/{item-id}.png` | Item icons |
| `/images/mobs/{mob-id}.png` | Mob images |
| `/images/locations/{loc-id}.png` | Location images |
### Data Downloads
| URL | Description |
|-----|-------------|
| `/api/export/items` | Full item database export |
| `/api/export/blueprints` | Full blueprint export |
---
## Entity Type Mapping
### API Endpoint → Web Path Mapping
When converting API entities to web URLs:
| API Entity Type | Web Path | Notes |
|-----------------|----------|-------|
| Items | `/items/{id}` | All item subtypes use `/items/` |
| Weapons | `/items/{id}` | Weapons are items |
| Armors | `/items/{id}` | Armors are items |
| Blueprints | `/blueprints/{id}` | Separate section |
| Mobs | `/mobs/{id}` | Separate section |
| Locations | `/locations/{id}` | Includes TPs, shops |
| Skills | `/skills/{id}` | Separate section |
| Materials | `/items/{id}` | Materials are items |
| Enhancers | `/items/{id}` | Enhancers are items |
| Medical Tools | `/items/{id}` | Medical tools are items |
| Finders | `/items/{id}` | Finders are items |
| Excavators | `/items/{id}` | Excavators are items |
| Refiners | `/items/{id}` | Refiners are items |
| Vehicles | `/items/{id}` | Vehicles are items |
| Pets | `/items/{id}` | Pets are items |
| Decorations | `/items/{id}` | Decorations are items |
| Furniture | `/items/{id}` | Furniture are items |
| Storage | `/items/{id}` | Storage are items |
| Strongboxes | `/items/{id}` | Strongboxes are items |
| Teleporters | `/locations/{id}` | TPs are locations |
| Shops | `/locations/{id}` | Shops are locations |
| Vendors | `/locations/{id}` | Vendors are locations |
| Planets | `/locations/{id}` | Planets are locations |
| Areas | `/locations/{id}` | Areas are locations |
---
## Query Parameters Reference
### Common Parameters
| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| `q` | string | Search query | `q=ArMatrix` |
| `query` | string | Alternative search parameter | `query=ArMatrix` |
| `limit` | integer | Max results (default: 20, max: 100) | `limit=50` |
| `fuzzy` | boolean | Enable fuzzy matching | `fuzzy=true` |
| `type` | string | Filter by entity type | `type=item` |
### Advanced Parameters
| Parameter | Description |
|-----------|-------------|
| `category` | Filter by item category |
| `min_level` | Minimum item level |
| `max_level` | Maximum item level |
| `planet` | Filter by planet |
---
## Rate Limits
| Endpoint | Limit |
|----------|-------|
| General API | 5 requests/second |
| Search | 10 requests/minute |
| Market Data | 60 requests/minute |
---
## Response Formats
### Standard Search Response
```json
[
{
"id": "item-id",
"name": "Item Name",
"type": "item",
"category": "Weapon",
"icon_url": "https://..."
}
]
```
### Standard Item Detail Response
```json
{
"id": "item-id",
"name": "Item Name",
"description": "Item description...",
"category": "Weapon",
"weight": 2.5,
"tt_value": 120.0,
"decay": 0.5,
"damage": 45.0,
"range": 45.0,
"accuracy": 80.0,
"durability": 10000
}
```
---
## Related Documentation
- [NEXUS_API_REFERENCE.md](./NEXUS_API_REFERENCE.md) - Complete API documentation
- [Plugin API Integration](../core/plugin_api.py) - How plugins access Nexus API
- [Nexus API Client](../core/nexus_api.py) - Core API client implementation
---
## External Links
- **Main Website:** https://www.entropianexus.com
- **API Base:** https://api.entropianexus.com
- **Discord:** Check Nexus website for community links
- **Forums:** Entropia Universe official forums
---
## Notes
- API and web URLs are separate domains (`api.` vs `www.`)
- Some endpoints return different field name formats:
- API may return `name` or `Name` (camelCase vs PascalCase)
- Always check both when parsing responses
- Market data endpoints are on `www.` subdomain
- Search endpoints support fuzzy matching for typo tolerance

View File

@ -0,0 +1,648 @@
# Nexus API Usage Examples for Plugin Developers
> Practical examples for using the Entropia Nexus API in plugins
---
## Table of Contents
1. [Basic Search Examples](#basic-search-examples)
2. [Entity Type-Specific Searches](#entity-type-specific-searches)
3. [Item Details & Market Data](#item-details--market-data)
4. [Complete Plugin Examples](#complete-plugin-examples)
5. [Advanced Usage Patterns](#advanced-usage-patterns)
---
## Basic Search Examples
### Simple Item Search
```python
from plugins.base_plugin import BasePlugin
class MyPlugin(BasePlugin):
def find_weapons(self):
# Search for items containing "ArMatrix"
results = self.nexus_search("ArMatrix", entity_type="items")
for item in results:
print(f"Found: {item['name']} ({item['type']})")
```
### Search with Limit
```python
def find_top_results(self, query):
# Get up to 50 results
results = self.nexus_search(
query,
entity_type="items",
limit=50
)
return results
```
---
## Entity Type-Specific Searches
### Weapons
```python
def find_laser_weapons(self):
"""Search for laser weapons."""
results = self.nexus_search("laser", entity_type="weapons")
weapons = []
for item in results:
details = self.nexus_get_item_details(item['id'])
if details:
weapons.append({
'name': details['name'],
'damage': details.get('damage', 0),
'range': details.get('range', 0),
'decay': details.get('decay', 0)
})
return weapons
```
### Mobs/Creatures
```python
def find_mobs_by_name(self, name):
"""Search for creatures."""
results = self.nexus_search(name, entity_type="mobs")
mobs = []
for mob in results:
mobs.append({
'name': mob['name'],
'id': mob['id'],
'hitpoints': mob['data'].get('hitpoints', 'Unknown'),
'threat': mob['data'].get('threat', 'Unknown')
})
return mobs
# Usage
drakabas = self.find_mobs_by_name("Drakaba")
atrox = self.find_mobs_by_name("Atrox")
```
### Blueprints
```python
def find_crafting_blueprints(self, material_name):
"""Find blueprints that use a specific material."""
results = self.nexus_search(material_name, entity_type="blueprints")
blueprints = []
for bp in results:
details = self.nexus_get_item_details(bp['id'])
if details and 'materials' in details:
blueprints.append({
'name': details['name'],
'materials': details['materials'],
'qr': details.get('qr', 1.0)
})
return blueprints
```
### Locations
```python
def find_teleporters(self, planet="Calypso"):
"""Find teleporters on a specific planet."""
results = self.nexus_search(planet, entity_type="teleporters")
teleporters = []
for tp in results:
data = tp['data']
teleporters.append({
'name': tp['name'],
'x': data.get('x'),
'y': data.get('y'),
'planet': data.get('planet', planet)
})
return teleporters
```
### Skills
```python
def find_combat_skills(self):
"""Search for combat-related skills."""
results = self.nexus_search("", entity_type="skills")
combat_skills = [
s for s in results
if 'combat' in s.get('category', '').lower()
]
return combat_skills
```
---
## Item Details & Market Data
### Get Full Item Information
```python
def analyze_item(self, item_id):
"""Get complete item analysis."""
# Get basic details
details = self.nexus_get_item_details(item_id)
if not details:
return None
# Get market data
market = self.nexus_get_market_data(item_id)
analysis = {
'name': details['name'],
'category': details.get('category', 'Unknown'),
'tt_value': details.get('tt_value', 0),
'weight': details.get('weight', 0),
'decay': details.get('decay', 0),
}
# Add weapon stats if applicable
if 'damage' in details:
analysis['weapon_stats'] = {
'damage': details['damage'],
'range': details.get('range', 0),
'accuracy': details.get('accuracy', 0),
'ammo': details.get('ammo_consumption', 0)
}
# Add market info
if market:
analysis['market'] = {
'markup': market.get('current_markup', 0),
'volume_24h': market.get('volume_24h', 0),
'buy_orders': len(market.get('buy_orders', [])),
'sell_orders': len(market.get('sell_orders', []))
}
return analysis
```
### Market Price Monitoring
```python
def check_price_drops(self, watchlist):
"""Monitor items for price drops.
Args:
watchlist: List of {'item_id': str, 'max_price': float}
"""
deals = []
for watch in watchlist:
market = self.nexus_get_market_data(watch['item_id'])
if not market:
continue
current_price = market.get('current_markup', 0)
if current_price <= watch['max_price']:
deals.append({
'item_id': watch['item_id'],
'item_name': market.get('item_name', 'Unknown'),
'current_price': current_price,
'target_price': watch['max_price'],
'savings': watch['max_price'] - current_price
})
return deals
```
### Compare Item Stats
```python
def compare_weapons(self, weapon_ids):
"""Compare multiple weapons side by side."""
weapons = []
for wid in weapon_ids:
details = self.nexus_get_item_details(wid)
if details:
dpp = self.calculate_dpp(
details.get('damage', 0),
details.get('ammo_consumption', 0),
details.get('decay', 0)
)
weapons.append({
'name': details['name'],
'damage': details.get('damage', 0),
'range': details.get('range', 0),
'decay': details.get('decay', 0),
'dpp': dpp,
'tt': details.get('tt_value', 0)
})
# Sort by DPP (damage per pec)
return sorted(weapons, key=lambda w: w['dpp'], reverse=True)
```
---
## Complete Plugin Examples
### Weapon Comparison Plugin
```python
"""
Weapon Comparison Plugin
Compares weapons by DPP (Damage Per PEC) and other stats.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLineEdit, QTableWidget,
QTableWidgetItem, QLabel
)
from plugins.base_plugin import BasePlugin
class WeaponComparatorPlugin(BasePlugin):
name = "Weapon Comparator"
version = "1.0.0"
author = "EU-Utility"
description = "Compare weapons by DPP and stats"
hotkey = "ctrl+shift+w"
def initialize(self):
self.weapon_ids = []
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
# Search box
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Search weapons...")
search_layout.addWidget(self.search_input)
add_btn = QPushButton("Add to Compare")
add_btn.clicked.connect(self.add_weapon)
search_layout.addWidget(add_btn)
layout.addLayout(search_layout)
# Results table
self.table = QTableWidget()
self.table.setColumnCount(6)
self.table.setHorizontalHeaderLabels([
"Name", "Damage", "Range", "Decay", "DPP", "TT Value"
])
layout.addWidget(self.table)
# Compare button
compare_btn = QPushButton("Compare")
compare_btn.clicked.connect(self.do_compare)
layout.addWidget(compare_btn)
return widget
def add_weapon(self):
query = self.search_input.text()
results = self.nexus_search(query, entity_type="weapons", limit=5)
if results:
# Add first result
self.weapon_ids.append(results[0]['id'])
self.search_input.clear()
self.search_input.setPlaceholderText(
f"Added {results[0]['name']} ({len(self.weapon_ids)} total)"
)
def do_compare(self):
if len(self.weapon_ids) < 2:
return
# Fetch and compare
weapons = []
for wid in self.weapon_ids:
details = self.nexus_get_item_details(wid)
if details:
dpp = self.calculate_dpp(
details.get('damage', 0),
details.get('ammo_consumption', 0),
details.get('decay', 0)
)
weapons.append({
'name': details['name'],
'damage': details.get('damage', 0),
'range': details.get('range', 0),
'decay': details.get('decay', 0),
'dpp': dpp,
'tt': details.get('tt_value', 0)
})
# Sort by DPP
weapons.sort(key=lambda w: w['dpp'], reverse=True)
# Display
self.table.setRowCount(len(weapons))
for i, w in enumerate(weapons):
self.table.setItem(i, 0, QTableWidgetItem(w['name']))
self.table.setItem(i, 1, QTableWidgetItem(f"{w['damage']:.1f}"))
self.table.setItem(i, 2, QTableWidgetItem(f"{w['range']:.0f}m"))
self.table.setItem(i, 3, QTableWidgetItem(f"{w['decay']:.2f}"))
self.table.setItem(i, 4, QTableWidgetItem(f"{w['dpp']:.2f}"))
self.table.setItem(i, 5, QTableWidgetItem(f"{w['tt']:.2f} PED"))
```
### Mob Info Plugin
```python
"""
Mob Information Plugin
Quick lookup for creature stats and locations.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QLabel
)
from plugins.base_plugin import BasePlugin
class MobInfoPlugin(BasePlugin):
name = "Mob Info"
version = "1.0.0"
author = "EU-Utility"
description = "Quick mob stats lookup"
hotkey = "ctrl+shift+m"
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
# Search
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Enter mob name...")
self.search_input.returnPressed.connect(self.search_mob)
search_layout.addWidget(self.search_input)
search_btn = QPushButton("Search")
search_btn.clicked.connect(self.search_mob)
search_layout.addWidget(search_btn)
layout.addLayout(search_layout)
# Results
self.name_label = QLabel("Name: ")
self.hp_label = QLabel("HP: ")
self.damage_label = QLabel("Damage: ")
self.threat_label = QLabel("Threat: ")
self.planet_label = QLabel("Planet: ")
layout.addWidget(self.name_label)
layout.addWidget(self.hp_label)
layout.addWidget(self.damage_label)
layout.addWidget(self.threat_label)
layout.addWidget(self.planet_label)
layout.addStretch()
return widget
def search_mob(self):
query = self.search_input.text()
if not query:
return
results = self.nexus_search(query, entity_type="mobs", limit=1)
if not results:
self.name_label.setText("Mob not found")
return
mob = results[0]
data = mob.get('data', {})
self.name_label.setText(f"Name: {mob['name']}")
self.hp_label.setText(f"HP: {data.get('hitpoints', 'Unknown')}")
self.damage_label.setText(f"Damage: {data.get('damage', 'Unknown')}")
self.threat_label.setText(f"Threat: {data.get('threat', 'Unknown')}")
self.planet_label.setText(f"Planet: {data.get('planet', 'Unknown')}")
```
### Price Checker Plugin
```python
"""
Price Checker Plugin
Quick market price lookup for items.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QLabel, QFrame
)
from PyQt6.QtCore import Qt
from plugins.base_plugin import BasePlugin
class PriceCheckerPlugin(BasePlugin):
name = "Price Checker"
version = "1.0.0"
author = "EU-Utility"
description = "Quick market price lookup"
hotkey = "ctrl+shift+p"
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
# Search
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Enter item name...")
self.search_input.returnPressed.connect(self.check_price)
search_layout.addWidget(self.search_input)
check_btn = QPushButton("Check Price")
check_btn.clicked.connect(self.check_price)
search_layout.addWidget(check_btn)
layout.addLayout(search_layout)
# Results frame
self.results_frame = QFrame()
results_layout = QVBoxLayout(self.results_frame)
self.item_name = QLabel("Item: ")
self.item_name.setStyleSheet("font-size: 14px; font-weight: bold;")
results_layout.addWidget(self.item_name)
self.markup_label = QLabel("Current Markup: ")
results_layout.addWidget(self.markup_label)
self.volume_label = QLabel("24h Volume: ")
results_layout.addWidget(self.volume_label)
self.range_label = QLabel("Price Range: ")
results_layout.addWidget(self.range_label)
layout.addWidget(self.results_frame)
layout.addStretch()
return widget
def check_price(self):
query = self.search_input.text()
if not query:
return
# Search for item
results = self.nexus_search(query, entity_type="items", limit=1)
if not results:
self.item_name.setText("Item not found")
return
item = results[0]
self.item_name.setText(f"Item: {item['name']}")
# Get market data
market = self.nexus_get_market_data(item['id'])
if not market:
self.markup_label.setText("No market data available")
return
markup = market.get('current_markup', 0)
avg_7d = market.get('avg_markup_7d', 0)
volume = market.get('volume_24h', 0)
self.markup_label.setText(f"Current Markup: {markup:.1f}%")
self.volume_label.setText(f"24h Volume: {volume}")
# Calculate range from orders
buy_orders = market.get('buy_orders', [])
sell_orders = market.get('sell_orders', [])
if buy_orders and sell_orders:
highest_buy = max(o['price'] for o in buy_orders)
lowest_sell = min(o['price'] for o in sell_orders)
self.range_label.setText(
f"Price Range: {highest_buy:.2f} - {lowest_sell:.2f} PED"
)
```
---
## Advanced Usage Patterns
### Caching Results
```python
class CachedSearchPlugin(BasePlugin):
def initialize(self):
self._cache = {}
self._cache_ttl = 300 # 5 minutes
def cached_search(self, query, entity_type="items"):
"""Search with local caching."""
cache_key = f"{entity_type}:{query}"
now = time.time()
# Check cache
if cache_key in self._cache:
result, timestamp = self._cache[cache_key]
if now - timestamp < self._cache_ttl:
return result
# Fetch fresh
results = self.nexus_search(query, entity_type)
self._cache[cache_key] = (results, now)
return results
```
### Batch Processing
```python
def analyze_multiple_items(self, item_names):
"""Process multiple items efficiently."""
analyses = []
for name in item_names:
# Search
results = self.nexus_search(name, entity_type="items", limit=1)
if not results:
continue
# Get details
details = self.nexus_get_item_details(results[0]['id'])
if not details:
continue
# Get market
market = self.nexus_get_market_data(results[0]['id'])
analyses.append({
'search_name': name,
'found_name': details['name'],
'tt_value': details.get('tt_value', 0),
'markup': market.get('current_markup', 0) if market else 0
})
return analyses
```
### Error Handling
```python
def safe_search(self, query, entity_type="items"):
"""Search with error handling."""
try:
if not self.nexus_is_available():
print("Nexus API not available")
return []
results = self.nexus_search(query, entity_type)
return results
except Exception as e:
print(f"Search error: {e}")
return []
def safe_get_details(self, item_id):
"""Get details with fallback."""
try:
details = self.nexus_get_item_details(item_id)
return details or {'name': 'Unknown', 'id': item_id}
except Exception as e:
print(f"Details error: {e}")
return {'name': 'Error', 'id': item_id}
```
---
## Related Documentation
- [NEXUS_API_REFERENCE.md](./NEXUS_API_REFERENCE.md) - Complete API documentation
- [NEXUS_LINKTREE.md](./NEXUS_LINKTREE.md) - URL and endpoint reference
- [Plugin Base Class](../plugins/base_plugin.py) - Available plugin methods
---
## Tips & Best Practices
1. **Always check for None**: `nexus_get_item_details()` and `nexus_get_market_data()` can return None
2. **Use try/except**: Wrap API calls to handle network errors gracefully
3. **Cache results**: For frequently accessed data, implement local caching
4. **Respect rate limits**: Don't make too many requests in rapid succession
5. **Check availability**: Use `nexus_is_available()` before making calls
6. **Handle both field formats**: API returns both `name` and `Name` - check both

View File

@ -683,3 +683,101 @@ class BasePlugin(ABC):
connected = self.api.connect_task_signal('cancelled', on_cancelled) or connected connected = self.api.connect_task_signal('cancelled', on_cancelled) or connected
return connected return connected
# ========== Nexus API Methods ==========
def nexus_search(self, query: str, entity_type: str = "items", limit: int = 20) -> List[Dict[str, Any]]:
"""Search for entities via Entropia Nexus API.
Args:
query: Search query string
entity_type: Type of entity to search. Valid types:
- items, weapons, armors
- mobs, pets
- blueprints, materials
- locations, teleporters, shops, planets, areas
- skills
- enhancers, medicaltools, finders, excavators, refiners
- vehicles, decorations, furniture
- storagecontainers, strongboxes, vendors
limit: Maximum number of results (default: 20, max: 100)
Returns:
List of search result dictionaries
Example:
# Search for weapons
results = self.nexus_search("ArMatrix", entity_type="weapons")
# Search for mobs
mobs = self.nexus_search("Atrox", entity_type="mobs")
# Search for locations
locations = self.nexus_search("Fort", entity_type="locations")
# Process results
for item in results:
print(f"{item['name']} ({item['type']})")
"""
if not self.api:
return []
return self.api.nexus_search(query, entity_type, limit)
def nexus_get_item_details(self, item_id: str) -> Optional[Dict[str, Any]]:
"""Get detailed information about a specific item.
Args:
item_id: The item's unique identifier (e.g., "armatrix_lp-35")
Returns:
Dictionary with item details, or None if not found
Example:
details = self.nexus_get_item_details("armatrix_lp-35")
if details:
print(f"Name: {details['name']}")
print(f"TT Value: {details['tt_value']} PED")
print(f"Damage: {details.get('damage', 'N/A')}")
print(f"Range: {details.get('range', 'N/A')}m")
"""
if not self.api:
return None
return self.api.nexus_get_item_details(item_id)
def nexus_get_market_data(self, item_id: str) -> Optional[Dict[str, Any]]:
"""Get market data for a specific item.
Args:
item_id: The item's unique identifier
Returns:
Dictionary with market data, or None if not found
Example:
market = self.nexus_get_market_data("armatrix_lp-35")
if market:
print(f"Current markup: {market['current_markup']:.1f}%")
print(f"7-day avg: {market['avg_markup_7d']:.1f}%")
print(f"24h Volume: {market['volume_24h']}")
# Check orders
for buy in market.get('buy_orders', [])[:5]:
print(f"Buy: {buy['price']} PED x {buy['quantity']}")
"""
if not self.api:
return None
return self.api.nexus_get_market_data(item_id)
def nexus_is_available(self) -> bool:
"""Check if Nexus API is available.
Returns:
True if Nexus API service is ready
"""
if not self.api:
return False
return self.api.nexus_is_available()

View File

@ -8,6 +8,19 @@ easyocr>=1.7.0
pyautogui>=0.9.54 pyautogui>=0.9.54
pillow>=10.0.0 pillow>=10.0.0
# Cross-platform file locking (Windows)
portalocker>=2.7.0; platform_system=="Windows"
# Clipboard support
pyperclip>=1.8.2
# HTTP requests
requests>=2.28.0
urllib3>=1.26.0
# Data processing
numpy>=1.21.0
# Optional plugin dependencies # Optional plugin dependencies
# Uncomment if using specific plugins: # Uncomment if using specific plugins:

View File

@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "playwright",
"installedVersion": "1.0.0",
"installedAt": 1771029662550
}

View File

@ -0,0 +1,44 @@
---
name: Playwright
description: Write, debug, and maintain Playwright tests and scrapers with resilient selectors, flaky test fixes, and CI/CD integration.
---
## Trigger
Use when writing Playwright tests, debugging failures, scraping with Playwright, or setting up CI/CD pipelines.
## Selector Priority (Always)
1. `getByRole()` — accessible, resilient
2. `getByTestId()` — explicit, stable
3. `getByLabel()`, `getByPlaceholder()` — form elements
4. `getByText()` — visible content (exact match preferred)
5. CSS/XPath — last resort, avoid nth-child and generated classes
## Core Capabilities
| Task | Reference |
|------|-----------|
| Test scaffolding & POMs | `testing.md` |
| Selector strategies | `selectors.md` |
| Scraping patterns | `scraping.md` |
| CI/CD integration | `ci-cd.md` |
| Debugging failures | `debugging.md` |
## Critical Rules
- **Never use `page.waitForTimeout()`** — use `waitFor*` methods or `expect` with polling
- **Always close contexts**`browser.close()` or `context.close()` to prevent memory leaks
- **Prefer `networkidle` with caution** — SPAs may never reach idle; use DOM-based waits instead
- **Use `test.describe.configure({ mode: 'parallel' })`** — for independent tests
- **Trace on failure only**`trace: 'on-first-retry'` in config, not always-on
## Quick Fixes
| Symptom | Fix |
|---------|-----|
| Element not found | Use `waitFor()` before interaction, check frame context |
| Flaky clicks | Use `click({ force: true })` or `waitFor({ state: 'visible' })` first |
| Timeout in CI | Increase timeout, add `expect.poll()`, check viewport size |
| Stale element | Re-query the locator, avoid storing element references |
| Auth lost between tests | Use `storageState` to persist cookies/localStorage |

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn73vp5rarc3b14rc7wjcw8f8580t5d1",
"slug": "playwright",
"version": "1.0.0",
"publishedAt": 1770982184555
}

183
skills/playwright/ci-cd.md Normal file
View File

@ -0,0 +1,183 @@
# CI/CD Integration
## GitHub Actions
```yaml
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
```
## GitLab CI
```yaml
playwright:
image: mcr.microsoft.com/playwright:v1.40.0-jammy
stage: test
script:
- npm ci
- npx playwright test
artifacts:
when: on_failure
paths:
- playwright-report/
expire_in: 7 days
```
## Docker Setup
```dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
```
## Test Sharding
```yaml
# GitHub Actions matrix
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Run tests
run: npx playwright test --shard=${{ matrix.shard }}/4
```
## playwright.config.ts for CI
```typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: process.env.CI
? [['html'], ['github']]
: [['html']],
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
```
## Caching Browsers
```yaml
# GitHub Actions
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
```
## Environment Variables
```yaml
env:
BASE_URL: https://staging.example.com
CI: true
```
```typescript
// playwright.config.ts
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
}
```
## Flaky Test Management
```typescript
// Mark known flaky test
test('sometimes fails', {
annotation: { type: 'flaky', description: 'Network timing issue' },
}, async ({ page }) => {
// test code
});
// Retry configuration
export default defineConfig({
retries: 2,
expect: {
timeout: 10000, // Increase assertion timeout
},
});
```
## Report Hosting
```yaml
# Deploy to GitHub Pages
- name: Deploy report
if: always()
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./playwright-report
```
## Common CI Issues
| Issue | Fix |
|-------|-----|
| Browsers not found | Use official Playwright Docker image |
| Display errors | Headless mode or `xvfb-run` |
| Out of memory | Reduce workers, close contexts |
| Timeouts | Increase `actionTimeout`, add retries |
| Inconsistent screenshots | Set fixed viewport, disable animations |

View File

@ -0,0 +1,196 @@
# Debugging Guide
## Playwright Inspector
```bash
# Run in debug mode
npx playwright test --debug
# Debug specific test
npx playwright test my-test.spec.ts --debug
# Headed mode (see browser)
npx playwright test --headed
```
```typescript
// Pause in test
await page.pause();
```
## Trace Viewer
```bash
# Record trace
npx playwright test --trace on
# View trace file
npx playwright show-trace trace.zip
```
```typescript
// Config for traces
use: {
trace: 'on-first-retry', // Only on failures
trace: 'retain-on-failure', // Keep failed traces
}
```
## Common Errors
### Element Not Found
```
Error: Timeout 30000ms exceeded waiting for selector
```
**Causes:**
- Element doesn't exist in DOM
- Element is inside iframe
- Element is in shadow DOM
- Page hasn't loaded
**Fixes:**
```typescript
// Wait for element
await page.waitForSelector('.element');
// Check frame context
const frame = page.frameLocator('iframe');
await frame.locator('.element').click();
// Increase timeout
await page.click('.element', { timeout: 60000 });
```
### Flaky Click
```
Error: Element is not visible
Error: Element is outside viewport
```
**Fixes:**
```typescript
// Ensure visible
await page.locator('.btn').waitFor({ state: 'visible' });
await page.locator('.btn').click();
// Scroll into view
await page.locator('.btn').scrollIntoViewIfNeeded();
// Force click (bypass checks)
await page.locator('.btn').click({ force: true });
```
### Timeout in CI
**Causes:**
- Slower CI environment
- Network latency
- Resource constraints
**Fixes:**
```typescript
// Increase global timeout
export default defineConfig({
timeout: 60000,
expect: { timeout: 10000 },
});
// Use polling assertions
await expect.poll(async () => {
return await page.locator('.items').count();
}, { timeout: 30000 }).toBeGreaterThan(5);
```
### Stale Element
```
Error: Element is no longer attached to DOM
```
**Fix:**
```typescript
// Don't store element references
const button = page.locator('.submit'); // This is fine (locator)
// Re-query when needed
await button.click(); // Playwright re-queries automatically
```
### Network Issues
```typescript
// Log all requests
page.on('request', request => {
console.log('>>', request.method(), request.url());
});
page.on('response', response => {
console.log('<<', response.status(), response.url());
});
// Wait for specific request
const responsePromise = page.waitForResponse('**/api/data');
await page.click('.load-data');
const response = await responsePromise;
```
## Screenshot Debugging
```typescript
// Take screenshot on failure
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== 'passed') {
await page.screenshot({
path: `screenshots/${testInfo.title}.png`,
fullPage: true,
});
}
});
```
## Console Logs
```typescript
// Capture console messages
page.on('console', msg => {
console.log('PAGE LOG:', msg.text());
});
page.on('pageerror', error => {
console.log('PAGE ERROR:', error.message);
});
```
## Slow Motion
```typescript
// playwright.config.ts
use: {
launchOptions: {
slowMo: 500, // 500ms delay between actions
},
}
```
## Compare Local vs CI
| Check | Command |
|-------|---------|
| Viewport | `await page.viewportSize()` |
| User agent | `await page.evaluate(() => navigator.userAgent)` |
| Timezone | `await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone)` |
| Network | `page.on('request', ...)` to log all requests |
## Debugging Checklist
1. [ ] Run with `--debug` or `--headed`
2. [ ] Add `await page.pause()` before failure point
3. [ ] Check for iframes/shadow DOM
4. [ ] Verify element exists with `page.locator().count()`
5. [ ] Review trace file in Trace Viewer
6. [ ] Compare screenshots between local and CI
7. [ ] Check console for JS errors
8. [ ] Verify network requests completed

View File

@ -0,0 +1,168 @@
# Scraping Patterns
## Basic Extraction
```typescript
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com/products');
const products = await page.$$eval('.product-card', cards =>
cards.map(card => ({
name: card.querySelector('.name')?.textContent?.trim(),
price: card.querySelector('.price')?.textContent?.trim(),
url: card.querySelector('a')?.href,
}))
);
await browser.close();
```
## Wait Strategies for SPAs
```typescript
// Wait for specific element
await page.waitForSelector('[data-loaded="true"]');
// Wait for network idle (careful with SPAs)
await page.goto(url, { waitUntil: 'networkidle' });
// Wait for loading indicator to disappear
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
// Custom condition with polling
await expect.poll(async () => {
return await page.locator('.product').count();
}).toBeGreaterThan(0);
```
## Infinite Scroll
```typescript
async function scrollToBottom(page: Page) {
let previousHeight = 0;
while (true) {
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
if (currentHeight === previousHeight) break;
previousHeight = currentHeight;
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(1000); // Allow content to load
}
}
```
## Pagination
```typescript
// Click-based pagination
async function scrapeAllPages(page: Page) {
const allData = [];
while (true) {
const pageData = await extractData(page);
allData.push(...pageData);
const nextButton = page.getByRole('button', { name: 'Next' });
if (await nextButton.isDisabled()) break;
await nextButton.click();
await page.waitForLoadState('networkidle');
}
return allData;
}
```
## Anti-Bot Evasion
```typescript
const browser = await chromium.launch({
headless: false, // Some sites detect headless
});
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
});
// Add realistic behavior
await page.mouse.move(100, 100);
await page.waitForTimeout(Math.random() * 2000 + 1000);
```
## Session Management
```typescript
// Save cookies
await context.storageState({ path: 'session.json' });
// Restore session
const context = await browser.newContext({
storageState: 'session.json',
});
```
## Error Handling
```typescript
async function scrapeWithRetry(url: string, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const page = await context.newPage();
await page.goto(url, { timeout: 30000 });
return await extractData(page);
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, 2000 * (i + 1)));
} finally {
await page.close();
}
}
}
```
## Rate Limiting
```typescript
class RateLimiter {
private lastRequest = 0;
constructor(private minDelay: number) {}
async wait() {
const elapsed = Date.now() - this.lastRequest;
if (elapsed < this.minDelay) {
await new Promise(r => setTimeout(r, this.minDelay - elapsed));
}
this.lastRequest = Date.now();
}
}
const limiter = new RateLimiter(2000); // 2s between requests
for (const url of urls) {
await limiter.wait();
await scrape(url);
}
```
## Proxy Rotation
```typescript
const proxies = ['proxy1:8080', 'proxy2:8080', 'proxy3:8080'];
let proxyIndex = 0;
async function getNextProxy() {
const proxy = proxies[proxyIndex];
proxyIndex = (proxyIndex + 1) % proxies.length;
return proxy;
}
const browser = await chromium.launch({
proxy: { server: await getNextProxy() },
});
```

View File

@ -0,0 +1,87 @@
# Selector Strategies
## Hierarchy (Most to Least Resilient)
### 1. Role-Based (Best)
```typescript
page.getByRole('button', { name: 'Submit' })
page.getByRole('link', { name: /sign up/i })
page.getByRole('heading', { level: 1 })
page.getByRole('textbox', { name: 'Email' })
```
### 2. Test IDs (Explicit)
```typescript
page.getByTestId('checkout-button')
page.getByTestId('product-card').first()
```
Configure in `playwright.config.ts`:
```typescript
use: { testIdAttribute: 'data-testid' }
```
### 3. Label/Placeholder (Forms)
```typescript
page.getByLabel('Email address')
page.getByPlaceholder('Enter your email')
```
### 4. Text Content (Visible)
```typescript
page.getByText('Add to Cart', { exact: true })
page.getByText(/welcome/i) // regex for flexibility
```
### 5. CSS (Last Resort)
```typescript
// Avoid these patterns:
page.locator('.css-1a2b3c') // generated class
page.locator('div > span:nth-child(2)') // positional
page.locator('#root > div > div > button') // deep nesting
// Acceptable:
page.locator('[data-product-id="123"]') // semantic attribute
page.locator('form.login-form') // stable class
```
## Chaining and Filtering
```typescript
// Filter within results
page.getByRole('listitem').filter({ hasText: 'Product A' })
// Chain locators
page.getByTestId('cart').getByRole('button', { name: 'Remove' })
// Get nth item
page.getByRole('listitem').nth(2)
page.getByRole('listitem').first()
page.getByRole('listitem').last()
```
## Frame Handling
```typescript
// Named frame
const frame = page.frameLocator('iframe[name="checkout"]')
frame.getByRole('button', { name: 'Pay' }).click()
// Frame by URL
page.frameLocator('iframe[src*="stripe"]')
```
## Shadow DOM
```typescript
// Playwright pierces shadow DOM by default
page.locator('my-component').getByRole('button')
```
## Common Mistakes
| Mistake | Better |
|---------|--------|
| `page.locator('button').click()` | `page.getByRole('button', { name: 'Submit' }).click()` |
| Storing locator result | Re-query each time |
| `nth-child(3)` | Filter by text or test ID |
| `//div[@class="xyz"]/span[2]` | Role-based or test ID |

View File

@ -0,0 +1,150 @@
# Testing Patterns
## Test Structure
```typescript
import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/products');
});
test('completes purchase with valid card', async ({ page }) => {
await page.getByTestId('product-card').first().click();
await page.getByRole('button', { name: 'Add to Cart' }).click();
await page.getByRole('link', { name: 'Checkout' }).click();
await expect(page.getByRole('heading', { name: 'Order Summary' })).toBeVisible();
});
});
```
## Page Object Model
```typescript
// pages/checkout.page.ts
export class CheckoutPage {
constructor(private page: Page) {}
readonly cartItems = this.page.getByTestId('cart-item');
readonly checkoutButton = this.page.getByRole('button', { name: 'Checkout' });
readonly totalPrice = this.page.getByTestId('total-price');
async removeItem(name: string) {
await this.cartItems
.filter({ hasText: name })
.getByRole('button', { name: 'Remove' })
.click();
}
async expectTotal(amount: string) {
await expect(this.totalPrice).toHaveText(amount);
}
}
// tests/checkout.spec.ts
test('removes item from cart', async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.removeItem('Product A');
await checkout.expectTotal('$0.00');
});
```
## Fixtures
```typescript
// fixtures.ts
import { test as base } from '@playwright/test';
import { CheckoutPage } from './pages/checkout.page';
type Fixtures = {
checkoutPage: CheckoutPage;
};
export const test = base.extend<Fixtures>({
checkoutPage: async ({ page }, use) => {
await page.goto('/checkout');
await use(new CheckoutPage(page));
},
});
```
## API Mocking
```typescript
test('shows error on API failure', async ({ page }) => {
await page.route('**/api/checkout', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Payment failed' }),
});
});
await page.goto('/checkout');
await page.getByRole('button', { name: 'Pay' }).click();
await expect(page.getByText('Payment failed')).toBeVisible();
});
```
## Visual Regression
```typescript
test('matches snapshot', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100,
});
});
// Component snapshot
await expect(page.getByTestId('header')).toHaveScreenshot();
```
## Parallelization
```typescript
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 4 : undefined,
fullyParallel: true,
});
// Per-file control
test.describe.configure({ mode: 'parallel' });
test.describe.configure({ mode: 'serial' }); // dependent tests
```
## Authentication State
```typescript
// Save auth state
await page.context().storageState({ path: 'auth.json' });
// Reuse across tests
test.use({ storageState: 'auth.json' });
```
## Assertions
```typescript
// Visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeAttached();
// Content
await expect(locator).toHaveText('Expected');
await expect(locator).toContainText('partial');
await expect(locator).toHaveValue('input value');
// State
await expect(locator).toBeEnabled();
await expect(locator).toBeChecked();
await expect(locator).toHaveAttribute('href', '/path');
// Polling (for async state)
await expect.poll(async () => {
return await page.evaluate(() => window.dataLoaded);
}).toBe(true);
```