AI #2
305
nodejs/OLLAMA_SETUP.md
Normal file
305
nodejs/OLLAMA_SETUP.md
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
# Ollama Integration Guide
|
||||||
|
|
||||||
|
This project now supports Ollama as an AI backend alongside Google Gemini, with **per-bot configuration** allowing you to mix providers and personalities across multiple bots.
|
||||||
|
|
||||||
|
## Configuration Hierarchy
|
||||||
|
|
||||||
|
AI settings are merged in this order:
|
||||||
|
1. **Global defaults** in `conf/base.js` → `ai` object
|
||||||
|
2. **Bot-specific overrides** in `conf/secrets.js` → `mc.bots.{botName}.plugins.Ai`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Global Defaults (Optional)
|
||||||
|
|
||||||
|
Edit `conf/base.js` to set defaults for all bots:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
"ai":{
|
||||||
|
// Default provider (can be overridden per-bot)
|
||||||
|
"provider": "gemini", // or "ollama"
|
||||||
|
|
||||||
|
// Gemini API key (used by Gemini provider)
|
||||||
|
"key": "<configure in conf/secrets.js>",
|
||||||
|
|
||||||
|
// Ollama settings (used by Ollama provider)
|
||||||
|
"baseUrl": "http://localhost:11434",
|
||||||
|
"model": "llama3.2",
|
||||||
|
"timeout": 30000,
|
||||||
|
|
||||||
|
// Generation settings (applies to both providers)
|
||||||
|
"temperature": 1,
|
||||||
|
"topP": 0.95,
|
||||||
|
"topK": 64,
|
||||||
|
"maxOutputTokens": 8192,
|
||||||
|
"interval": 20,
|
||||||
|
// ... prompts
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-Bot Configuration
|
||||||
|
|
||||||
|
Edit `conf/secrets.js` to configure each bot individually:
|
||||||
|
|
||||||
|
#### Example 1: Bot using default global settings
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
"art": {
|
||||||
|
"username": "art@vm42.us",
|
||||||
|
"commands": ['fun', 'invite', 'default'],
|
||||||
|
"auth": "microsoft",
|
||||||
|
"plugins": {
|
||||||
|
"Ai":{
|
||||||
|
"promptName": "helpful",
|
||||||
|
// Uses global provider settings from base.js
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example 2: Bot using specific Ollama instance
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
"ayay": {
|
||||||
|
"username": "limtisengyes@gmail.com",
|
||||||
|
"commands": ['fun', 'invite', 'default'],
|
||||||
|
"auth": "microsoft",
|
||||||
|
"plugins": {
|
||||||
|
"Ai":{
|
||||||
|
"promptName": "asshole",
|
||||||
|
"provider": "ollama",
|
||||||
|
"baseUrl": "http://192.168.1.50:11434", // Remote Ollama
|
||||||
|
"model": "llama3.2:7b",
|
||||||
|
"interval": 25,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example 3: Bot using Gemini with custom settings
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
"nova": {
|
||||||
|
"username": "your@email.com",
|
||||||
|
"auth": "microsoft",
|
||||||
|
"commands": ['default', 'fun'],
|
||||||
|
"plugins": {
|
||||||
|
"Ai":{
|
||||||
|
"promptName": "helpful",
|
||||||
|
"provider": "gemini",
|
||||||
|
"model": "gemini-2.0-flash-exp",
|
||||||
|
"temperature": 0.7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Bots with Different Providers
|
||||||
|
|
||||||
|
You can run multiple bots with different providers simultaneously:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// conf/secrets.js
|
||||||
|
"bots": {
|
||||||
|
"bot1": {
|
||||||
|
"plugins": {
|
||||||
|
"Ai": {
|
||||||
|
"promptName": "helpful",
|
||||||
|
"provider": "gemini", // Uses Google Gemini
|
||||||
|
"model": "gemini-2.0-flash-exp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bot2": {
|
||||||
|
"plugins": {
|
||||||
|
"Ai": {
|
||||||
|
"promptName": "asshole",
|
||||||
|
"provider": "ollama", // Uses local Ollama
|
||||||
|
"baseUrl": "http://localhost:11434",
|
||||||
|
"model": "llama3.2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bot3": {
|
||||||
|
"plugins": {
|
||||||
|
"Ai": {
|
||||||
|
"promptName": "Ashley",
|
||||||
|
"provider": "ollama", // Uses remote Ollama
|
||||||
|
"baseUrl": "http://192.168.1.50:11434",
|
||||||
|
"model": "mistral",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixing Personalities and Models
|
||||||
|
|
||||||
|
Each bot can have:
|
||||||
|
- **Different provider** (Gemini or different Ollama instances)
|
||||||
|
- **Different model** (llama3.2, mistral, qwen2.5, etc.)
|
||||||
|
- **Different personality** (helpful, asshole, Ashley, custom)
|
||||||
|
- **Different settings** (temperature, interval, etc.)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
"helpfulBot": {
|
||||||
|
"plugins": {
|
||||||
|
"Ai": {
|
||||||
|
"promptName": "helpful",
|
||||||
|
"provider": "ollama",
|
||||||
|
"baseUrl": "http://server1:11434",
|
||||||
|
"model": "llama3.2:3b",
|
||||||
|
"temperature": 0.5,
|
||||||
|
"interval": 15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toxicBot": {
|
||||||
|
"plugins": {
|
||||||
|
"Ai": {
|
||||||
|
"promptName": "asshole",
|
||||||
|
"provider": "ollama",
|
||||||
|
"baseUrl": "http://server2:11434",
|
||||||
|
"model": "llama3.2:70b",
|
||||||
|
"temperature": 1.2,
|
||||||
|
"interval": 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ollama Setup
|
||||||
|
|
||||||
|
### Install Ollama
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
curl -fsSL https://ollama.com/install.sh | sh
|
||||||
|
|
||||||
|
# Or download from https://ollama.com/download
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Models
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Recommended for chat bots:
|
||||||
|
ollama pull llama3.2
|
||||||
|
ollama pull mistral
|
||||||
|
ollama pull qwen2.5
|
||||||
|
|
||||||
|
# Specific sizes for performance tuning:
|
||||||
|
ollama pull llama3.2:3b # Fast, lightweight
|
||||||
|
ollama pull llama3.2:7b # Good balance
|
||||||
|
ollama pull llama3.2:70b # Smarter, slower
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start Ollama Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local (only)
|
||||||
|
ollama serve
|
||||||
|
|
||||||
|
# Allow remote connections (for multiple servers)
|
||||||
|
OLLAMA_HOST=0.0.0.0:11434 ollama serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure Remote Ollama
|
||||||
|
|
||||||
|
To use Ollama on another machine:
|
||||||
|
|
||||||
|
1. On the Ollama server:
|
||||||
|
```bash
|
||||||
|
OLLAMA_HOST=0.0.0.0:11434 ollama serve
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In bot config:
|
||||||
|
```javascript
|
||||||
|
"Ai": {
|
||||||
|
"provider": "ollama",
|
||||||
|
"baseUrl": "http://ollama-server-ip:11434",
|
||||||
|
"model": "llama3.2",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ollama Model Recommendations
|
||||||
|
|
||||||
|
| Model | Size | Speed | Quality | Best For |
|
||||||
|
|-------|------|-------|---------|----------|
|
||||||
|
| `llama3.2:3b` | 3B | Very Fast | Good | Bots needing fast responses |
|
||||||
|
| `llama3.2:7b` | 7B | Fast | Very Good | General purpose |
|
||||||
|
| `llama3.2:70b` | 70B | Moderate | Excellent | Smart bots, complex prompts |
|
||||||
|
| `mistral` | 7B | Fast | Good | Balanced solution |
|
||||||
|
| `qwen2.5:7b` | 7B | Fast | Very Good | Good instruction following |
|
||||||
|
| `gemma2:9b` | 9B | Fast | Good | Lightweight alternative |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Refused
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if Ollama is running
|
||||||
|
curl http://localhost:11434/api/tags
|
||||||
|
|
||||||
|
# Check specific server
|
||||||
|
curl http://192.168.1.50:11434/api/tags
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model Not Found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check available models
|
||||||
|
ollama list
|
||||||
|
|
||||||
|
# Pull missing model
|
||||||
|
ollama pull llama3.2
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Parsing Errors
|
||||||
|
|
||||||
|
Most models support JSON mode. If issues occur:
|
||||||
|
1. Switch to `llama3.1`, `qwen2.5`, or `mistral`
|
||||||
|
2. Lower `temperature:` (e.g., 0.7)
|
||||||
|
3. Increase `maxOutputTokens:` for longer responses
|
||||||
|
|
||||||
|
### Slow Responses
|
||||||
|
|
||||||
|
- Use smaller models (`llama3.2:3b` vs `70b`)
|
||||||
|
- Increase `interval:` in config
|
||||||
|
- Reduce `maxOutputTokens:`
|
||||||
|
- Check network latency for remote Ollama instances
|
||||||
|
|
||||||
|
### Multiple Bots Overloading Ollama
|
||||||
|
|
||||||
|
If running many bots on one Ollama server:
|
||||||
|
1. Use lighter models for less important bots
|
||||||
|
2. Increase `interval:` to space requests
|
||||||
|
3. Distribute bots across multiple Ollama instances
|
||||||
|
|
||||||
|
## Available Personality Prompts
|
||||||
|
|
||||||
|
| Personality | Description | Best Model |
|
||||||
|
|-------------|-------------|------------|
|
||||||
|
| `helpful` | Shy, helpful Jimmy | llama3.2, mistral |
|
||||||
|
| `asshole` | Sarcastic, unfiltered | llama3.2:70b, gemini |
|
||||||
|
| `Ashley` | Adult content | llama3.2, gemini |
|
||||||
|
| `custom` | Template for custom prompts | Any |
|
||||||
|
|
||||||
|
## Comparing Providers
|
||||||
|
|
||||||
|
| Feature | Gemini | Ollama |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| Cost | API cost | Free (local) |
|
||||||
|
| Latency | 200-500ms | 50-500ms (local) |
|
||||||
|
| Privacy | Cloud | 100% local |
|
||||||
|
| Multiple Servers | No | Yes |
|
||||||
|
| Model Choice | Limited | Any |
|
||||||
|
| Hardware | None Required | GPU Recommended |
|
||||||
|
| Offline | No | Yes |
|
||||||
|
|
||||||
|
## Command Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/msg botname ai <personality> # Change personality
|
||||||
|
/msg botname ai <personality> custom message # Use custom prompt
|
||||||
|
/msg wmantly load botname Ai <personality> # Reload AI with new config
|
||||||
|
```
|
||||||
923
nodejs/STORAGE_SYSTEM.md
Normal file
923
nodejs/STORAGE_SYSTEM.md
Normal file
@@ -0,0 +1,923 @@
|
|||||||
|
# Storage/Trade Bot System Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A plugin-based storage and trading system for Minecraft bots. Any bot (`ez` or others) equipped with the StoragePlugin can:
|
||||||
|
- Automatically discover and track chests in a storage area
|
||||||
|
- Sort incoming items into shulker boxes
|
||||||
|
- Track full inventory metadata (NBT, enchantments, durability)
|
||||||
|
- Provide web API for inventory viewing (24/7)
|
||||||
|
- Handle deposit/withdraw via the `/trade` command
|
||||||
|
|
||||||
|
**Key Design Principle**: The system is NOT hardcoded to any specific bot name. Any bot can be configured with the StoragePlugin via the plugin system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
#### 1. Chest Discovery
|
||||||
|
- Scan all chests within configurable render distance
|
||||||
|
- Automatically detect single vs double chests
|
||||||
|
- Assign row/column positions for organization
|
||||||
|
- No signs required - chests are positional
|
||||||
|
|
||||||
|
#### 2. Storage Organization
|
||||||
|
- **Only shulker boxes stored in chests** - no loose items
|
||||||
|
- **One item type per shulker** - no mixing items in a shulker
|
||||||
|
- Automatic categorization of items (minerals, food, tools, etc.)
|
||||||
|
- Unlimited empty shulkers available from reserve
|
||||||
|
|
||||||
|
#### 3. Trade Integration
|
||||||
|
- Use existing `/trade` command for all item transfers
|
||||||
|
- Max 12 slots per trade (server limitation)
|
||||||
|
- Deposit flow: player trades → bot sorts → items stored
|
||||||
|
- Withdraw flow: player requests → bot gathers → trade window
|
||||||
|
|
||||||
|
#### 4. Database Persistence
|
||||||
|
- SQLite database for inventory tracking
|
||||||
|
- Database independent of bot being online
|
||||||
|
- Web API reads directly from database (24/7 availability)
|
||||||
|
|
||||||
|
#### 5. Permission System
|
||||||
|
- Database-driven permissions (no file editing)
|
||||||
|
- Roles: `owner`, `team`, `readonly`
|
||||||
|
- Commands and web access limited by role
|
||||||
|
|
||||||
|
#### 6. Web Interface
|
||||||
|
- Simple but fully functional UI
|
||||||
|
- Search inventory
|
||||||
|
- View item counts and locations
|
||||||
|
- Request withdrawals via web
|
||||||
|
- No login required (whisper challenge for auth)
|
||||||
|
|
||||||
|
### Technical Requirements
|
||||||
|
|
||||||
|
#### Stack
|
||||||
|
- Node.js + mineflayer (existing infrastructure)
|
||||||
|
- SQLite for database (via `sqlite3` npm package)
|
||||||
|
- Express for web API
|
||||||
|
- Minecraft server: CoreJourney (existing)
|
||||||
|
|
||||||
|
#### Constraints
|
||||||
|
- Plain shulker boxes only (no dyes, no NBT names)
|
||||||
|
- Trade window max 12 slots
|
||||||
|
- Bot location is secret (no player access to chests)
|
||||||
|
- Web server must run 24/7 (separate from bot process)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### System Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ GAME LAYER │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ StorageBot │ (Optional) │ Other Bots │ │
|
||||||
|
│ │ (e.g., ez) │ │ │ │
|
||||||
|
│ │ - Scan │ │ - Proxy │ │
|
||||||
|
│ │ - Store │ │ - Messages │ │
|
||||||
|
│ │ - Trade │ │ │ │
|
||||||
|
│ └──────┬──────┘ └─────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ │ Commands, Trade Events │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌────────────────────────┐ │
|
||||||
|
│ │ StoragePlugin │ ← ANY bot can use this │
|
||||||
|
│ │ (Business Logic) │ via plugin system │
|
||||||
|
│ └────────────┬───────────┘ │
|
||||||
|
└────────────────┼──────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────┼──────────────────────────────────────────────┐
|
||||||
|
│ DATABASE LAYER │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ permissions │ │ chests │ │ shulkers │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ shulker_items│ │ trades │ │ item_index │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||||
|
└────────────────┼──────────────────────────────────────────────┘
|
||||||
|
│ (SQLite: ./storage/storage.db)
|
||||||
|
┌────────────────┼──────────────────────────────────────────────┐
|
||||||
|
│ WEB LAYER (24/7) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ Express │────────▶│ Web UI │ │
|
||||||
|
│ │ Server │ API │ (HTML/JS) │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ │
|
||||||
|
│ ▲ │
|
||||||
|
│ │ REST API │
|
||||||
|
│ │ │
|
||||||
|
│ /api/inventory, /api/chests, /api/withdraw, ... │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Structure (Bot-Agnostic)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Configuration in conf/secrets.js
|
||||||
|
"mc": {
|
||||||
|
"bots": {
|
||||||
|
"ez": {
|
||||||
|
"plugins": {
|
||||||
|
"Storage": {
|
||||||
|
// Bot can be swapped anytime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Another bot can use Storage plugin:
|
||||||
|
"art": {
|
||||||
|
"plugins": {
|
||||||
|
"Storage": {
|
||||||
|
// Different location, same functionality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
|
||||||
|
#### `permissions`
|
||||||
|
Manage access to the storage system.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE permissions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_name TEXT UNIQUE NOT NULL,
|
||||||
|
role TEXT DEFAULT 'team' NOT NULL CHECK(role IN ('owner', 'team', 'readonly')),
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `id` | INTEGER | Primary key |
|
||||||
|
| `player_name` | TEXT | Minecraft username (unique) |
|
||||||
|
| `role` | TEXT | 'owner', 'team', or 'readonly' |
|
||||||
|
| `joined_at` | TIMESTAMP | When player was added |
|
||||||
|
|
||||||
|
#### `chests`
|
||||||
|
Tracked chest blocks in storage area.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE chests (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
pos_x INTEGER NOT NULL,
|
||||||
|
pos_y INTEGER NOT NULL,
|
||||||
|
pos_z INTEGER NOT NULL,
|
||||||
|
chest_type TEXT NOT NULL CHECK(chest_type IN ('single', 'double')),
|
||||||
|
row INTEGER NOT NULL, -- 1-4 (vertical)
|
||||||
|
column INTEGER NOT NULL, -- horizontal grouping
|
||||||
|
category TEXT, -- 'minerals', 'food', etc.
|
||||||
|
last_scan TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(pos_x, pos_y, pos_z)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `shulkers`
|
||||||
|
Shulker boxes stored in chests.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE shulkers (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
chest_id INTEGER NOT NULL,
|
||||||
|
slot INTEGER NOT NULL, -- 0-53 (single) or 0-107 (double)
|
||||||
|
shulker_type TEXT DEFAULT 'shulker_box',
|
||||||
|
category TEXT, -- 'minerals', 'tools', etc.
|
||||||
|
item_focus TEXT, -- Item type stored (e.g., 'diamond')
|
||||||
|
slot_count INTEGER DEFAULT 27, -- Used slots (1-27)
|
||||||
|
total_items INTEGER DEFAULT 0, -- Total item count
|
||||||
|
last_scan TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (chest_id) REFERENCES chests(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `shulker_items`
|
||||||
|
Items inside shulker boxes. Enforces one item type per shulker.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE shulker_items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
shulker_id INTEGER NOT NULL,
|
||||||
|
item_name TEXT NOT NULL,
|
||||||
|
item_id INTEGER NOT NULL,
|
||||||
|
slot INTEGER NOT NULL, -- 0-26 (shulker slots)
|
||||||
|
count INTEGER NOT NULL,
|
||||||
|
nbt_data TEXT, -- JSON: {enchantments: [...], damage: 5}
|
||||||
|
FOREIGN KEY (shulker_id) REFERENCES shulkers(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(shulker_id, item_id),
|
||||||
|
CHECK(slot >= 0 AND slot <= 26),
|
||||||
|
CHECK(count > 0 AND count <= 64)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `trades`
|
||||||
|
Trade history logs.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE trades (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL CHECK(action IN ('deposit', 'withdraw')),
|
||||||
|
items TEXT NOT NULL, -- JSON: [{name, count, nbt}, ...]
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `pending_withdrawals`
|
||||||
|
Withdrawal requests from players (sync between web and in-game).
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE pending_withdrawals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
item_id INTEGER NOT NULL,
|
||||||
|
item_name TEXT NOT NULL,
|
||||||
|
requested_count INTEGER NOT NULL,
|
||||||
|
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'ready', 'completed', 'cancelled')),
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `item_index`
|
||||||
|
Cached aggregated item counts for fast searches.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE item_index (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
item_id INTEGER UNIQUE NOT NULL,
|
||||||
|
item_name TEXT NOT NULL,
|
||||||
|
total_count INTEGER DEFAULT 0,
|
||||||
|
shulker_ids TEXT, -- JSON: [{id, count}, ...]
|
||||||
|
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
nodejs/
|
||||||
|
├── controller/
|
||||||
|
│ ├── storage/
|
||||||
|
│ │ ├── index.js # StoragePlugin main class
|
||||||
|
│ │ ├── database.js # SQLite setup and all DB operations
|
||||||
|
│ │ ├── scanner.js # Chest discovery and scanning
|
||||||
|
│ │ ├── organizer.js # Item sorting and categorization
|
||||||
|
│ │ └── web.js # Express server (24/7 API)
|
||||||
|
│ ├── storage.js # Export for mc-bot.js plugin loading
|
||||||
|
│ └── commands/
|
||||||
|
│ ├── default.js # Add: summon, dismiss commands
|
||||||
|
│ └── trade.js # Add StoragePlugin special handling
|
||||||
|
├── storage/
|
||||||
|
│ ├── storage.db # SQLite database (automatically created)
|
||||||
|
│ └── public/
|
||||||
|
│ ├── index.html # Web UI (single page)
|
||||||
|
│ ├── app.js # Frontend logic
|
||||||
|
│ └── style.css # Styling
|
||||||
|
├── conf/
|
||||||
|
│ ├── base.js # Add storage config
|
||||||
|
│ └── secrets.js # DB path, permissions init
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component Specifications
|
||||||
|
|
||||||
|
### 1. StoragePlugin (`controller/storage/index.js`)
|
||||||
|
|
||||||
|
**Purpose**: Main plugin class that ties together database, scanner, organizer, and trade handling.
|
||||||
|
|
||||||
|
**Constructor Arguments**:
|
||||||
|
- `bot`: The CJbot instance
|
||||||
|
- `dbFile`: Path to SQLite database
|
||||||
|
- `homePos`: Starting position (optional, auto-detect on first scan)
|
||||||
|
|
||||||
|
**Key Methods**:
|
||||||
|
```javascript
|
||||||
|
class StoragePlugin {
|
||||||
|
constructor(args) { ... }
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// Initialize database
|
||||||
|
// Register commands
|
||||||
|
// Start on bot 'onReady'
|
||||||
|
}
|
||||||
|
|
||||||
|
async unload() {
|
||||||
|
// Clean up
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanArea(force = false) {
|
||||||
|
// Discover chests within render distance
|
||||||
|
// Update database
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleTrade(playerName, itemsReceived) {
|
||||||
|
// Process incoming trade items
|
||||||
|
// Sort into shulkers
|
||||||
|
// Update database
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleWithdrawRequest(playerName, itemId, count) {
|
||||||
|
// Gather items to OUTBOX shulker
|
||||||
|
// Mark as ready for pickup
|
||||||
|
}
|
||||||
|
|
||||||
|
async organize() {
|
||||||
|
// Full re-sort (manual command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Database Module (`controller/storage/database.js`)
|
||||||
|
|
||||||
|
**Purpose**: All SQLite operations.
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```javascript
|
||||||
|
// Initialize
|
||||||
|
async initialize(dbFile)
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
async addPlayer(name, role = 'team')
|
||||||
|
async removePlayer(name)
|
||||||
|
async getPlayerRole(name)
|
||||||
|
async getAllPlayers()
|
||||||
|
async checkPermission(name, requiredRole)
|
||||||
|
|
||||||
|
// Chests
|
||||||
|
async upsertChest(position, chestType)
|
||||||
|
async getChests()
|
||||||
|
async deleteOrphanChests()
|
||||||
|
|
||||||
|
// Shulkers
|
||||||
|
async upsertShulker(chestId, slot, category, itemFocus)
|
||||||
|
async getShulkersByChest(chestId)
|
||||||
|
async findShulkerForItem(itemId) // Find shulker with same item and space
|
||||||
|
async createEmptyShulker(chestId, slot)
|
||||||
|
async updateShulkerCounts(shulkerId, slotCount, totalItems)
|
||||||
|
|
||||||
|
// Shulker Items
|
||||||
|
async upsertShulkerItem(shulkerId, item)
|
||||||
|
async getShulkerItems(shulkerId)
|
||||||
|
async deleteShulkerItem(shulkerId, itemId)
|
||||||
|
|
||||||
|
// Trades
|
||||||
|
async logTrade(playerName, action, items)
|
||||||
|
async getRecentTrades(limit = 50)
|
||||||
|
|
||||||
|
// Pending Withdrawals
|
||||||
|
async queueWithdrawal(playerName, itemId, itemName, count)
|
||||||
|
async getPendingWithdrawals(playerName)
|
||||||
|
async updateWithdrawStatus(id, status)
|
||||||
|
async markCompletedWithdrawals(playerName)
|
||||||
|
|
||||||
|
// Item Index
|
||||||
|
async updateItemCount(itemId, shulkerId, count)
|
||||||
|
async searchItems(query = null)
|
||||||
|
async getItemDetails(itemId)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Scanner Module (`controller/storage/scanner.js`)
|
||||||
|
|
||||||
|
**Purpose**: Discover and scan chests/shulkers.
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```javascript
|
||||||
|
async discoverChests(bot, radius) {
|
||||||
|
// Find all chest blocks within radius
|
||||||
|
// Detect single vs double
|
||||||
|
// Assign row/column based on position
|
||||||
|
// Return array of chest positions
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanChest(bot, chestPosition) {
|
||||||
|
// Open chest
|
||||||
|
// Read all slots
|
||||||
|
// Scan any shulkers found
|
||||||
|
// Update database
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanShulker(bot, chestSlot, chestPosition) {
|
||||||
|
// Click shulker to open
|
||||||
|
// Read all 27 slots
|
||||||
|
// Parse NBT data
|
||||||
|
// Return item array
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectChestType(position) {
|
||||||
|
// Check adjacent blocks to detect double chest
|
||||||
|
// Return 'single' or 'double'
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignRowColumn(position, minPos) {
|
||||||
|
// Calculate row from Y (1-4)
|
||||||
|
// Calculate column from X/Z
|
||||||
|
// Return {row, column}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Organizer Module (`controller/storage/organizer.js`)
|
||||||
|
|
||||||
|
**Purpose**: Sort items into shulkers, categorize items.
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
```javascript
|
||||||
|
function categorizeItem(itemName) {
|
||||||
|
// Returns: 'minerals', 'food', 'tools', 'armor', 'blocks', 'redstone', 'misc'
|
||||||
|
}
|
||||||
|
|
||||||
|
async sortItems(itemsDb, itemsToSort) {
|
||||||
|
// For each item:
|
||||||
|
// - Find shulker with same item AND space
|
||||||
|
// - If found: move to that shulker, consolidate stacks
|
||||||
|
// - If not found: create new shulker at category column
|
||||||
|
// Return: moves to execute
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCategoryColumn(category, row) {
|
||||||
|
// Map (category, row) to chest column
|
||||||
|
// Return column number
|
||||||
|
}
|
||||||
|
|
||||||
|
async consolidateStacks(shulkerId) {
|
||||||
|
// Merge partial stacks
|
||||||
|
// Update database
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Web Server (`controller/storage/web.js`)
|
||||||
|
|
||||||
|
**Purpose**: Express API for 24/7 inventory access.
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/inventory
|
||||||
|
GET /api/inventory/:itemId
|
||||||
|
GET /api/chests
|
||||||
|
GET /api/chests/:id
|
||||||
|
GET /api/shulkers
|
||||||
|
GET /api/shulkers/:id
|
||||||
|
GET /api/stats
|
||||||
|
GET /api/trades?limit=50
|
||||||
|
POST /api/withdraw
|
||||||
|
GET /api/pending/:playerName
|
||||||
|
POST /api/auth
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detailed API Spec**:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/inventory
|
||||||
|
Response: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
item_id: 1,
|
||||||
|
item_name: "diamond",
|
||||||
|
total_count: 2304,
|
||||||
|
locations: [
|
||||||
|
{shulker_id: 1, count: 1728},
|
||||||
|
{shulker_id: 5, count: 576}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
GET /api/inventory/:itemId
|
||||||
|
Response: {
|
||||||
|
item_id: 1,
|
||||||
|
item_name: "diamond",
|
||||||
|
total_count: 2304,
|
||||||
|
locations: [
|
||||||
|
{
|
||||||
|
shulker_id: 1,
|
||||||
|
chest_id: 3,
|
||||||
|
chest_pos: {x: 100, y: 64, z: 200},
|
||||||
|
count: 1728
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
GET /api/chests
|
||||||
|
Response: {
|
||||||
|
chests: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
pos_x: 100, pos_y: 64, pos_z: 200,
|
||||||
|
chest_type: "double",
|
||||||
|
row: 1,
|
||||||
|
column: 1,
|
||||||
|
category: "minerals",
|
||||||
|
shulker_count: 26
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
GET /api/stats
|
||||||
|
Response: {
|
||||||
|
totalItems: 15432,
|
||||||
|
totalShulkers: 156,
|
||||||
|
totalChests: 24,
|
||||||
|
emptyShulkers: 12,
|
||||||
|
categories: {
|
||||||
|
minerals: 42,
|
||||||
|
food: 18,
|
||||||
|
tools: 24,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
recentTrades: [
|
||||||
|
{player: "wmantly", action: "deposit", item_count: 45, time: "..."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
POST /api/withdraw
|
||||||
|
Body: {player_name: "wmantly", item_id: 1, count: 64}
|
||||||
|
Response: {success: true, withdraw_id: 123}
|
||||||
|
|
||||||
|
GET /api/pending/:playerName
|
||||||
|
Response: {
|
||||||
|
pending: [
|
||||||
|
{
|
||||||
|
id: 123,
|
||||||
|
item_name: "diamond",
|
||||||
|
requested_count: 64,
|
||||||
|
status: "ready"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Web UI (`storage/public/`)
|
||||||
|
|
||||||
|
**index.html**: Single page application
|
||||||
|
- Search bar
|
||||||
|
- Filter by category
|
||||||
|
- Item list with counts
|
||||||
|
- Click to see details (shulker locations)
|
||||||
|
- "Request Withdraw" button (opens modal)
|
||||||
|
- Stats sidebar
|
||||||
|
|
||||||
|
**app.js**: Frontend logic
|
||||||
|
- Fetch API calls
|
||||||
|
- Search/filter logic
|
||||||
|
- Withdraw request modal
|
||||||
|
- Auto-refresh pending withdrawals
|
||||||
|
|
||||||
|
**style.css**: Simple, clean styling
|
||||||
|
|
||||||
|
### 7. Modified Commands
|
||||||
|
|
||||||
|
**default.js** - Add new commands:
|
||||||
|
```javascript
|
||||||
|
'summon': {
|
||||||
|
desc: 'Summon a bot online indefinitely',
|
||||||
|
allowed: ['owner'],
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from, botName) { ... }
|
||||||
|
},
|
||||||
|
'dismiss': {
|
||||||
|
desc: 'Send a bot offline',
|
||||||
|
allowed: ['owner'],
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from, botName) { ... }
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**trade.js** - Add StoragePlugin handling:
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
'.trade': {
|
||||||
|
desc: 'Bot will take trade requests',
|
||||||
|
async function(from) {
|
||||||
|
// Check if bot has StoragePlugin
|
||||||
|
if (this.plunginsLoaded['Storage']) {
|
||||||
|
await this.plunginsLoaded['Storage'].handleTrade(from, ...);
|
||||||
|
} else {
|
||||||
|
// Original sign-based flow
|
||||||
|
let chestBlock = findChestBySign(this, from);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### conf/base.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
"storage": {
|
||||||
|
// Database location
|
||||||
|
"dbPath": "./storage/storage.db",
|
||||||
|
|
||||||
|
// Chest discovery
|
||||||
|
"scanRadius": 30, // Render distance
|
||||||
|
"homePos": null, // Auto-detect on first scan
|
||||||
|
|
||||||
|
// Category mappings
|
||||||
|
"categories": {
|
||||||
|
"minerals": ["diamond", "netherite_ingot", "gold_ingot", "iron_ingot",
|
||||||
|
"copper_ingot", "emerald", "redstone", "lapis_lazuli"],
|
||||||
|
"food": ["bread", "cooked_porkchop", "steak", "golden_apple", "cooked_beef",
|
||||||
|
"cooked_chicken", "cooked_mutton", "carrot", "potato", "baked_potato"],
|
||||||
|
"tools": ["wooden_sword", "stone_sword", "iron_sword", "diamond_sword",
|
||||||
|
"netherite_sword", "wooden_pickaxe", "stone_pickaxe", "iron_pickaxe",
|
||||||
|
"diamond_pickaxe", "netherite_pickaxe", "wooden_axe", "stone_axe",
|
||||||
|
"iron_axe", "diamond_axe", "netherite_axe"],
|
||||||
|
"armor": ["leather_helmet", "iron_helmet", "diamond_helmet", "netherite_helmet",
|
||||||
|
"leather_chestplate", "iron_chestplate", "diamond_chestplate", "netherite_chestplate",
|
||||||
|
"leather_leggings", "iron_leggings", "diamond_leggings", "netherite_leggings",
|
||||||
|
"leather_boots", "iron_boots", "diamond_boots", "netherite_boots"],
|
||||||
|
"blocks": ["stone", "dirt", "cobblestone", "oak_planks", "spruce_planks",
|
||||||
|
"birch_planks", "oak_log", "spruce_log", "cobblestone_stairs"],
|
||||||
|
"redstone": ["redstone", "repeater", "comparator", "piston", "sticky_piston",
|
||||||
|
"redstone_torch", "lever", "tripwire_hook"],
|
||||||
|
"misc": [] // Everything else falls here
|
||||||
|
},
|
||||||
|
|
||||||
|
// Special shulkers (for bookkeeping)
|
||||||
|
"inboxShulkerName": "INBOX",
|
||||||
|
"outboxShulkerName": "OUTBOX",
|
||||||
|
"newShulkersName": "EMPTY",
|
||||||
|
|
||||||
|
// Web server
|
||||||
|
"webPort": 3000,
|
||||||
|
"webHost": "0.0.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### conf/secrets.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
"storage": {
|
||||||
|
// Database can override location
|
||||||
|
// "dbPath": "./storage/storage.db",
|
||||||
|
|
||||||
|
// Default permissions (inserted on DB init)
|
||||||
|
"defaultPlayers": [
|
||||||
|
{name: "wmantly", role: "owner"},
|
||||||
|
{name: "useless666", role: "owner"},
|
||||||
|
{name: "tux4242", role: "owner"},
|
||||||
|
{name: "pi_chef", role: "team"},
|
||||||
|
{name: "Ethan", role: "team"},
|
||||||
|
{name: "Vince_NL", role: "team"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bot Configuration (Plugin-Based, Bot-Agnostic)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// conf/secrets.js - Example: ez bot
|
||||||
|
"mc": {
|
||||||
|
"bots": {
|
||||||
|
"ez": {
|
||||||
|
"username": "mc3@vm42.us",
|
||||||
|
"auth": "microsoft",
|
||||||
|
"commands": ['default'],
|
||||||
|
"autoConnect": false,
|
||||||
|
"plugins": {
|
||||||
|
"Storage": {
|
||||||
|
// Bot uses Storage plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Any bot can be moved to storage location:
|
||||||
|
"art": {
|
||||||
|
"username": "art@vm42.us",
|
||||||
|
"auth": "microsoft",
|
||||||
|
"plugins": {
|
||||||
|
"Storage": {
|
||||||
|
// Same plugin, different bot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Workflows
|
||||||
|
|
||||||
|
### Deposit Flow (Player Perspective)
|
||||||
|
|
||||||
|
1. **Player collects items** from farms/raids (max 12 slots due to trade window)
|
||||||
|
2. **Player types**: `/msg ez trade` or uses `/trade` command with ez
|
||||||
|
3. **Trade window opens**
|
||||||
|
4. **Player puts items** in their side of trade window
|
||||||
|
5. **Confirm trade**
|
||||||
|
6. **ez automatically**:
|
||||||
|
- Moves all items to INBOX shulker
|
||||||
|
- Categorizes each item
|
||||||
|
- Finds appropriate shulker (creates new if needed)
|
||||||
|
- Moves items to organized shulkers
|
||||||
|
- Updates database
|
||||||
|
7. **ez whispers**: `Received X items. Stored successfully.`
|
||||||
|
8. **Player can verify** on web: `http://server:3000`
|
||||||
|
|
||||||
|
### Withdraw Flow (Player Perspective)
|
||||||
|
|
||||||
|
**Option A: In-Game Only**
|
||||||
|
1. **Player types**: `/msg ez withdraw diamond 64`
|
||||||
|
2. **ez searches database** for diamond locations
|
||||||
|
3. **ez gathers items** to OUTBOX shulker
|
||||||
|
4. **ez whispers**: `Items ready for pickup. /trade with me.`
|
||||||
|
5. **Player trades** with ez
|
||||||
|
6. **ez moves items** from OUTBOX to trade window
|
||||||
|
7. **Confirm trade**
|
||||||
|
8. **ez updates database**
|
||||||
|
|
||||||
|
**Option B: Web Request**
|
||||||
|
1. **Player visits** web: `http://server:3000`
|
||||||
|
2. **Finds item** and enters count
|
||||||
|
3. **Click "Withdraw"**
|
||||||
|
4. **Queues request** to database
|
||||||
|
5. **When ez is online**, processes pending requests
|
||||||
|
6. **ez whispers player**: `Your items are ready. /trade with me.`
|
||||||
|
|
||||||
|
### Admin Workflow (Owner)
|
||||||
|
|
||||||
|
```
|
||||||
|
/msg ez summon # Bring bot online
|
||||||
|
/msg ez dismiss # Send bot offline
|
||||||
|
/msg ez scan # Force chest scan
|
||||||
|
/msg ez chests # List tracked chests
|
||||||
|
/msg ez organize # Force full re-sort
|
||||||
|
/msg ez status # Show storage stats
|
||||||
|
/msg ez addplayer <name> # Add authorized player
|
||||||
|
/msg ez removeplayer <name> # Remove player
|
||||||
|
/msg ez players # List authorized players
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Database Security
|
||||||
|
- SQLite file permissions: Read/write by bot process only
|
||||||
|
- No direct SQL injection (parameterized queries throughout)
|
||||||
|
- Web API uses read-only connections for GET requests
|
||||||
|
|
||||||
|
### Game Security
|
||||||
|
- Bot location kept secret (no `/tp` to chests allowed to team)
|
||||||
|
- Only trade window access for players
|
||||||
|
- No `/msg` command execution to other bots
|
||||||
|
|
||||||
|
### Web Security
|
||||||
|
- No login required (simpler)
|
||||||
|
- Auth via whisper challenge code (6-digit code generated, whispered to player for verification)
|
||||||
|
- Rate limiting on API endpoints
|
||||||
|
- CORS restricted to same origin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Core Database and Plugin Structure
|
||||||
|
- Create `database.js` - SQLite setup and all queries
|
||||||
|
- Create `index.js` - StoragePlugin main class skeleton
|
||||||
|
- Initialize plugin in `mc-bot.js` via plugin system
|
||||||
|
- Test: Database creation, basic connectivity
|
||||||
|
|
||||||
|
### Phase 2: Scanner
|
||||||
|
- Create `scanner.js` - Chest discovery and scanning
|
||||||
|
- Scan loop: Find chests → Detect single/double → Assign row/column
|
||||||
|
- Scan individual chests → Detect shulkers → Read NBT → Update DB
|
||||||
|
- Test: Scan a test chest area, verify DB entries
|
||||||
|
|
||||||
|
### Phase 3: Organizer
|
||||||
|
- Create `organizer.js` - Categorization and sorting
|
||||||
|
- Implement `categorizeItem()` function
|
||||||
|
- Implement sort logic (find shulker, move items, create new if needed)
|
||||||
|
- Test: Sort items from INBOX to organized shulkers
|
||||||
|
|
||||||
|
### Phase 4: Trade Integration
|
||||||
|
- Modify `trade.js` - Add StoragePlugin handling
|
||||||
|
- Implement `handleTrade()` in StoragePlugin
|
||||||
|
- Implement `handleWithdrawRequest()` in StoragePlugin
|
||||||
|
- Test: Deposit and withdraw via trade window
|
||||||
|
|
||||||
|
### Phase 5: Commands
|
||||||
|
- Add `summon`, `dismiss` to `default.js`
|
||||||
|
- Add storage commands (`scan`, `status`, `chests`, `organize`)
|
||||||
|
- Add player management commands (`addplayer`, `removeplayer`, `players`)
|
||||||
|
- Test: All commands work with proper permissions
|
||||||
|
|
||||||
|
### Phase 6: Web Server
|
||||||
|
- Create `web.js` - Express server
|
||||||
|
- Implement API endpoints
|
||||||
|
- Test: API returns correct data from database
|
||||||
|
|
||||||
|
### Phase 7: Web UI
|
||||||
|
- Create `index.html` - Single page UI
|
||||||
|
- Create `app.js` - Frontend logic
|
||||||
|
- Create `style.css` - Styling
|
||||||
|
- Test: View inventory, search, request withdrawal
|
||||||
|
|
||||||
|
### Phase 8: Integration Testing
|
||||||
|
- Full deposit flow end-to-end
|
||||||
|
- Full withdraw flow end-to-end
|
||||||
|
- Web + in-game sync
|
||||||
|
- Multiple trades in sequence
|
||||||
|
- Database persistence after bot restart
|
||||||
|
|
||||||
|
### Phase 9: Documentation and Polish
|
||||||
|
- Update this doc with any changes
|
||||||
|
- Add inline code comments
|
||||||
|
- Error handling improvements
|
||||||
|
- Performance optimization if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### New npm packages required:
|
||||||
|
```
|
||||||
|
sqlite3 # SQLite database
|
||||||
|
express # Web server
|
||||||
|
cors # CORS handling (optional, for external API access)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"cors": "^2.8.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting Guide
|
||||||
|
|
||||||
|
### Database Issues
|
||||||
|
- **Database locked**: Ensure only one process writes at a time
|
||||||
|
- **Corruption**: Use `sqlite3 storage.db "PRAGMA integrity_check;"` to verify
|
||||||
|
|
||||||
|
### Scan Issues
|
||||||
|
- **No chests found**: Check `homePos` is correct, or bot is in render distance
|
||||||
|
- **Double chest detection fails**: Ensure chests are properly adjacent
|
||||||
|
|
||||||
|
### Trade Issues
|
||||||
|
- **Items not sorted**: Check INBOX shulker exists and has space
|
||||||
|
- **Withdraw fails**: Verify item exists in database with sufficient count
|
||||||
|
|
||||||
|
### Web Issues
|
||||||
|
- **Can't access API**: Check `webHost` (use `0.0.0.0` for external access)
|
||||||
|
- **Database not found**: Ensure `dbPath` directory exists and has write permissions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements (Out of Scope for MVP)
|
||||||
|
|
||||||
|
- [ ] Shulker color coding for categories (needs dye access, blocked by server rules)
|
||||||
|
- [ ] Custom shulker names via NBT (blocked by server rules)
|
||||||
|
- [ ] Partial shulker consolidation (current: one item type per shulker, no combining)
|
||||||
|
- [ ] Auto-trading system (bot initiates trades)
|
||||||
|
- [ ] Multi-location support (multiple storage areas)
|
||||||
|
- [ ] Real-time web updates via WebSocket
|
||||||
|
- [ ] Export inventory to CSV/JSON
|
||||||
|
- [ ] Integration with trading APIs (like BulbaStore)
|
||||||
|
- [ ] Recipe calculator (what can be crafted with current storage)
|
||||||
|
- [ ] Value estimation (diamond equivalent of all items)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
| Term | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| **StoragePlugin** | The main plugin class that provides storage functionality to any bot |
|
||||||
|
| **INBOX shulker** | Temporary holding shulker for incoming trade items |
|
||||||
|
| **OUTBOX shulker** | Temporary holding shulker for items ready for withdrawal |
|
||||||
|
| **EMPTY shulkers** | Reserve stock of new shulker boxes |
|
||||||
|
| **One item type per shulker** | Each shulker stores only one item type (e.g., only diamonds) |
|
||||||
|
| **Category** | Item group (minerals, food, tools, armor, blocks, redstone, misc) |
|
||||||
|
| **Row/Column** | Chest positioning: Row (1-4, vertical), Column (horizontal grouping) |
|
||||||
|
| **Render distance** | Distance within which bot can see/click chests |
|
||||||
@@ -14,7 +14,19 @@ module.exports = {
|
|||||||
"swing": {},
|
"swing": {},
|
||||||
},
|
},
|
||||||
"ai":{
|
"ai":{
|
||||||
|
// AI provider: 'gemini' (default) or 'ollama'
|
||||||
|
"provider": "gemini",
|
||||||
|
// Gemini API key (required if using gemini provider)
|
||||||
"key": "<configure in secrets>",
|
"key": "<configure in secrets>",
|
||||||
|
// Ollama settings (only used if provider is 'ollama')
|
||||||
|
"baseUrl": "http://localhost:11434",
|
||||||
|
"model": "llama3.2", // Default for Ollama; for Gemini use gemini-2.0-flash-exp, etc.
|
||||||
|
"timeout": 30000,
|
||||||
|
// Generation settings (applies to both providers)
|
||||||
|
"temperature": 1,
|
||||||
|
"topP": 0.95,
|
||||||
|
"topK": 64,
|
||||||
|
"maxOutputTokens": 8192,
|
||||||
"interval": 20,
|
"interval": 20,
|
||||||
"prompts":{
|
"prompts":{
|
||||||
"custom": (name, interval, currentPlayers, custom)=>`
|
"custom": (name, interval, currentPlayers, custom)=>`
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
const conf = require('../conf');
|
const conf = require('../conf');
|
||||||
const {sleep} = require('../utils');
|
const {sleep} = require('../utils');
|
||||||
|
const { ProviderFactory } = require('./ai/providers');
|
||||||
|
|
||||||
const {
|
|
||||||
GoogleGenerativeAI,
|
|
||||||
HarmCategory,
|
|
||||||
HarmBlockThreshold,
|
|
||||||
} = require("@google/generative-ai");
|
|
||||||
|
|
||||||
|
|
||||||
class Ai{
|
class Ai{
|
||||||
@@ -21,6 +13,18 @@ class Ai{
|
|||||||
this.intervalLength = args.intervalLength || 30;
|
this.intervalLength = args.intervalLength || 30;
|
||||||
this.intervalStop;
|
this.intervalStop;
|
||||||
this.messageListener;
|
this.messageListener;
|
||||||
|
this.provider = null;
|
||||||
|
|
||||||
|
// Bot-specific AI config (overrides global config)
|
||||||
|
this.botConfig = args.botConfig || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get merged config: bot-specific settings override global settings
|
||||||
|
__getConfig() {
|
||||||
|
return {
|
||||||
|
...conf.ai, // Global defaults
|
||||||
|
...this.botConfig, // Bot-specific overrides
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(){
|
async init(){
|
||||||
@@ -41,7 +45,6 @@ class Ai{
|
|||||||
|
|
||||||
this.intervalStop = setInterval(async ()=>{
|
this.intervalStop = setInterval(async ()=>{
|
||||||
let result;
|
let result;
|
||||||
// if(messages.length ===0) return;
|
|
||||||
|
|
||||||
try{
|
try{
|
||||||
result = await this.chat(JSON.stringify({
|
result = await this.chat(JSON.stringify({
|
||||||
@@ -55,9 +58,9 @@ class Ai{
|
|||||||
|
|
||||||
try{
|
try{
|
||||||
messages = [''];
|
messages = [''];
|
||||||
if(!result.response.text()) return;
|
if(!this.provider.getResponse(result)) return;
|
||||||
|
|
||||||
for(let message of JSON.parse(result.response.text())){
|
for(let message of JSON.parse(this.provider.getResponse(result))){
|
||||||
console.log('toSay', message.delay, message.text);
|
console.log('toSay', message.delay, message.text);
|
||||||
if(message.text === '___') return;
|
if(message.text === '___') return;
|
||||||
setTimeout(async (message)=>{
|
setTimeout(async (message)=>{
|
||||||
@@ -66,8 +69,12 @@ class Ai{
|
|||||||
}
|
}
|
||||||
}catch(error){
|
}catch(error){
|
||||||
console.log('Error in AI message loop', error, result);
|
console.log('Error in AI message loop', error, result);
|
||||||
if(result || result.response || result.response.text()){
|
try {
|
||||||
console.log(result.response.text())
|
if(result && this.provider.getResponse(result)){
|
||||||
|
console.log(this.provider.getResponse(result))
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, this.intervalLength*1000);
|
}, this.intervalLength*1000);
|
||||||
@@ -83,107 +90,56 @@ class Ai{
|
|||||||
clearInterval(this.intervalStop);
|
clearInterval(this.intervalStop);
|
||||||
this.intervalStop = undefined;
|
this.intervalStop = undefined;
|
||||||
}
|
}
|
||||||
|
if(this.messageListener){
|
||||||
this.messageListener();
|
this.messageListener();
|
||||||
|
}
|
||||||
|
if(this.provider){
|
||||||
|
await this.provider.close();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
__settings(history){
|
|
||||||
return {
|
|
||||||
generationConfig: {
|
|
||||||
temperature: 1,
|
|
||||||
topP: 0.95,
|
|
||||||
topK: 64,
|
|
||||||
maxOutputTokens: 8192,
|
|
||||||
responseMimeType: "application/json",
|
|
||||||
},
|
|
||||||
safetySettings:[
|
|
||||||
// See https://ai.google.dev/gemini-api/docs/safety-settings
|
|
||||||
{
|
|
||||||
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
||||||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
||||||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
||||||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
||||||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
history: history || [
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
parts: [
|
|
||||||
{text: this.prompt},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "model",
|
|
||||||
parts: [
|
|
||||||
{text: "Chat stuff"},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async start(history){
|
async start(history){
|
||||||
const genAI = new GoogleGenerativeAI(conf.ai.key);
|
const config = this.__getConfig();
|
||||||
|
let bulbaItems = {};
|
||||||
|
|
||||||
const model = genAI.getGenerativeModel({
|
console.log(`${this.bot.name} AI config:`, {
|
||||||
model: "gemini-1.5-flash",
|
provider: config.provider,
|
||||||
|
model: config.model,
|
||||||
|
promptName: this.promptName,
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
let bulbaItems = await axios.get('https://webstore.bulbastore.uk/api/listings');
|
const prompt = conf.ai.prompts[this.promptName](
|
||||||
bulbaItems = bulbaItems.data.listings.map(i=>i.listing_name);
|
|
||||||
|
|
||||||
console.log('AI for prompts', conf.ai.prompts)
|
|
||||||
|
|
||||||
this.prompt = conf.ai.prompts[this.promptName](
|
|
||||||
this.bot.bot.entity.username,
|
this.bot.bot.entity.username,
|
||||||
conf.ai.interval,
|
config.interval,
|
||||||
Object.values(this.bot.getPlayers()).map(player=>`<[${player.lvl}] ${player.username}>`).join('\n'),
|
Object.values(this.bot.getPlayers()).map(player=>`<[${player.lvl}] ${player.username}>`).join('\n'),
|
||||||
bulbaItems,
|
bulbaItems,
|
||||||
this.prompCustom,
|
this.prompCustom,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.session = model.startChat({
|
// Create the provider instance with merged config and prompt
|
||||||
...this.__settings(history),
|
this.provider = ProviderFactory.create({
|
||||||
// systemInstruction: this.prompt,
|
...config,
|
||||||
|
prompt: prompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.provider.start(history);
|
||||||
|
console.log(`${this.bot.name} AI ${config.provider} provider started (model: ${config.model})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async chat(message, retryCount=0){
|
async chat(message, retryCount=0){
|
||||||
console.log('chat', retryCount)
|
console.log(`chat ${this.bot.name}`, retryCount)
|
||||||
try{
|
try{
|
||||||
let result = await this.session.sendMessage(message);
|
let result = await this.provider.chat(message);
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}catch(error){
|
}catch(error){
|
||||||
console.log('AI chat error', error)
|
console.log('AI chat error', error)
|
||||||
|
throw error;
|
||||||
if(retryCount > 3){
|
|
||||||
console.log('hit retry count');
|
|
||||||
return ;
|
|
||||||
};
|
|
||||||
await sleep(500);
|
|
||||||
this.session.params.history.pop();
|
|
||||||
this.start(this.session.params.history);
|
|
||||||
return await this.chat(message, retryCount++)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = Ai;
|
module.exports = Ai;
|
||||||
// run();
|
|
||||||
88
nodejs/controller/ai/providers/gemini.js
Normal file
88
nodejs/controller/ai/providers/gemini.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } = require("@google/generative-ai");
|
||||||
|
|
||||||
|
class GeminiProvider {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.session = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(history) {
|
||||||
|
const genAI = new GoogleGenerativeAI(this.config.key);
|
||||||
|
const model = genAI.getGenerativeModel({
|
||||||
|
model: this.config.model || "gemini-2.0-flash-exp",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.session = model.startChat(this.__settings(history));
|
||||||
|
}
|
||||||
|
|
||||||
|
__settings(history) {
|
||||||
|
return {
|
||||||
|
generationConfig: {
|
||||||
|
temperature: this.config.temperature || 1,
|
||||||
|
topP: this.config.topP || 0.95,
|
||||||
|
topK: this.config.topK || 64,
|
||||||
|
maxOutputTokens: this.config.maxOutputTokens || 8192,
|
||||||
|
responseMimeType: "application/json",
|
||||||
|
},
|
||||||
|
safetySettings: [
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||||
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
history: history || [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
parts: [{ text: this.config.prompt }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "model",
|
||||||
|
parts: [{ text: "Chat stuff" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(message, retryCount = 0) {
|
||||||
|
try {
|
||||||
|
let result = await this.session.sendMessage(message);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (retryCount > 3) {
|
||||||
|
throw new Error(`Gemini API error after ${retryCount} retries: ${error.message}`);
|
||||||
|
}
|
||||||
|
// Recover by removing last history entry and restarting
|
||||||
|
this.session.params.history.pop();
|
||||||
|
await this.start(this.session.params.history);
|
||||||
|
return await this.chat(message, retryCount + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPrompt(prompt) {
|
||||||
|
this.config.prompt = prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponse(result) {
|
||||||
|
return result.response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
this.session = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GeminiProvider;
|
||||||
25
nodejs/controller/ai/providers/index.js
Normal file
25
nodejs/controller/ai/providers/index.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const GeminiProvider = require('./gemini');
|
||||||
|
const OllamaProvider = require('./ollama');
|
||||||
|
|
||||||
|
class ProviderFactory {
|
||||||
|
static create(config) {
|
||||||
|
const provider = config.provider || 'gemini';
|
||||||
|
|
||||||
|
switch (provider.toLowerCase()) {
|
||||||
|
case 'gemini':
|
||||||
|
return new GeminiProvider(config);
|
||||||
|
case 'ollama':
|
||||||
|
return new OllamaProvider(config);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown AI provider: ${provider}. Supported: 'gemini', 'ollama'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ProviderFactory,
|
||||||
|
GeminiProvider,
|
||||||
|
OllamaProvider
|
||||||
|
};
|
||||||
108
nodejs/controller/ai/providers/ollama.js
Normal file
108
nodejs/controller/ai/providers/ollama.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
class OllamaProvider {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.baseUrl = config.baseUrl || 'http://localhost:11434';
|
||||||
|
this.model = config.model || 'llama3.2';
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(history) {
|
||||||
|
// Convert Gemini-style history to Ollama format if needed
|
||||||
|
this.messages = history || [];
|
||||||
|
|
||||||
|
if (this.config.prompt) {
|
||||||
|
console.log('Ollama provider initialized with model:', this.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__settings() {
|
||||||
|
return {
|
||||||
|
temperature: this.config.temperature || 1,
|
||||||
|
top_p: this.config.topP || 0.95,
|
||||||
|
top_k: this.config.topK || 64,
|
||||||
|
num_predict: this.config.maxOutputTokens || 8192,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(message, retryCount = 0) {
|
||||||
|
try {
|
||||||
|
// Build conversation from prompt + history
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: this.config.prompt || 'You are a helpful assistant.'
|
||||||
|
},
|
||||||
|
...this.messages.map(msg => ({
|
||||||
|
role: msg.role === 'model' ? 'assistant' : 'user',
|
||||||
|
content: msg.parts ? msg.parts.map(p => p.text).join('') : (msg.content || '')
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: message
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.baseUrl}/api/chat`,
|
||||||
|
{
|
||||||
|
model: this.model,
|
||||||
|
messages: messages,
|
||||||
|
stream: false,
|
||||||
|
format: 'json', // Request JSON response
|
||||||
|
options: this.__settings()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: this.config.timeout || 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update history
|
||||||
|
this.messages.push({
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ text: message }],
|
||||||
|
content: message
|
||||||
|
});
|
||||||
|
|
||||||
|
this.messages.push({
|
||||||
|
role: 'model',
|
||||||
|
parts: [{ text: response.data.message.content }],
|
||||||
|
content: response.data.message.content
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return in a format compatible with the Ai class
|
||||||
|
return {
|
||||||
|
response: {
|
||||||
|
text: () => response.data.message.content
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (retryCount > 3) {
|
||||||
|
throw new Error(`Ollama API error after ${retryCount} retries: ${error.message}`);
|
||||||
|
}
|
||||||
|
// Retry after delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500 * (retryCount + 1)));
|
||||||
|
return await this.chat(message, retryCount + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPrompt(prompt) {
|
||||||
|
this.config.prompt = prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponse(result) {
|
||||||
|
return result.response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = OllamaProvider;
|
||||||
@@ -138,16 +138,24 @@ class Craft{
|
|||||||
|
|
||||||
// Move these into openCrating function
|
// Move these into openCrating function
|
||||||
let windowOnce = (event)=> new Promise((resolve, reject)=> window.once(event, resolve));
|
let windowOnce = (event)=> new Promise((resolve, reject)=> window.once(event, resolve));
|
||||||
let inventory = ()=> window.slots.slice(window.inventoryStart, window.inventoryEnd)
|
let inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd);
|
||||||
|
|
||||||
// Move the items into the crafting grid
|
// Move the items into the crafting grid
|
||||||
|
// Keep track of used inventory slots to avoid reusing the same slot
|
||||||
|
let usedInventorySlots = new Set();
|
||||||
let slotCount = 1;
|
let slotCount = 1;
|
||||||
for(let shapeRow of recipe.inShape){
|
for(let shapeRow of recipe.inShape){
|
||||||
for(let shape of shapeRow){
|
for(let shape of shapeRow){
|
||||||
this.bot.bot.moveSlotItem(
|
let inventorySlot = inventory.findIndex((element, index) =>
|
||||||
inventory().find((element)=> element && element.type === shape.id).slot,
|
element && element.type === shape.id && !usedInventorySlots.has(index)
|
||||||
slotCount
|
|
||||||
);
|
);
|
||||||
|
if (inventorySlot === -1) {
|
||||||
|
throw new Error(`Not enough items of type ${shape.id} in inventory`);
|
||||||
|
}
|
||||||
|
let actualSlot = window.inventoryStart + inventorySlot;
|
||||||
|
usedInventorySlots.add(inventorySlot);
|
||||||
|
|
||||||
|
this.bot.bot.moveSlotItem(actualSlot, slotCount);
|
||||||
await windowOnce(`updateSlot:${slotCount}`);
|
await windowOnce(`updateSlot:${slotCount}`);
|
||||||
slotCount++;
|
slotCount++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,16 +152,24 @@ class CraftChests{
|
|||||||
|
|
||||||
// Move these into openCrating function
|
// Move these into openCrating function
|
||||||
let windowOnce = (event)=> new Promise((resolve, reject)=> window.once(event, resolve));
|
let windowOnce = (event)=> new Promise((resolve, reject)=> window.once(event, resolve));
|
||||||
let inventory = ()=> window.slots.slice(window.inventoryStart, window.inventoryEnd)
|
let inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd);
|
||||||
|
|
||||||
// Move the items into the crafting grid
|
// Move the items into the crafting grid
|
||||||
|
// Keep track of used inventory slots to avoid reusing the same slot
|
||||||
|
let usedInventorySlots = new Set();
|
||||||
let slotCount = 1;
|
let slotCount = 1;
|
||||||
for(let shapeRow of recipe.inShape){
|
for(let shapeRow of recipe.inShape){
|
||||||
for(let shape of shapeRow){
|
for(let shape of shapeRow){
|
||||||
this.bot.bot.moveSlotItem(
|
let inventorySlot = inventory.findIndex((element, index) =>
|
||||||
inventory().find((element)=> element && element.type === shape.id).slot,
|
element && element.type === shape.id && !usedInventorySlots.has(index)
|
||||||
slotCount
|
|
||||||
);
|
);
|
||||||
|
if (inventorySlot === -1) {
|
||||||
|
throw new Error(`Not enough items of type ${shape.id} in inventory`);
|
||||||
|
}
|
||||||
|
let actualSlot = window.inventoryStart + inventorySlot;
|
||||||
|
usedInventorySlots.add(inventorySlot);
|
||||||
|
|
||||||
|
this.bot.bot.moveSlotItem(actualSlot, slotCount);
|
||||||
await windowOnce(`updateSlot:${slotCount}`);
|
await windowOnce(`updateSlot:${slotCount}`);
|
||||||
slotCount++;
|
slotCount++;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user