craft fix
This commit is contained in:
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": {},
|
||||
},
|
||||
"ai":{
|
||||
// AI provider: 'gemini' (default) or 'ollama'
|
||||
"provider": "gemini",
|
||||
// Gemini API key (required if using gemini provider)
|
||||
"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,
|
||||
"prompts":{
|
||||
"custom": (name, interval, currentPlayers, custom)=>`
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
const conf = require('../conf');
|
||||
const {sleep} = require('../utils');
|
||||
|
||||
|
||||
const {
|
||||
GoogleGenerativeAI,
|
||||
HarmCategory,
|
||||
HarmBlockThreshold,
|
||||
} = require("@google/generative-ai");
|
||||
const { ProviderFactory } = require('./ai/providers');
|
||||
|
||||
|
||||
class Ai{
|
||||
@@ -21,6 +13,18 @@ class Ai{
|
||||
this.intervalLength = args.intervalLength || 30;
|
||||
this.intervalStop;
|
||||
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(){
|
||||
@@ -38,11 +42,10 @@ class Ai{
|
||||
console.log(`Message ${type}: ${message.toString()}`)
|
||||
messages.push('>', message.toString());
|
||||
});
|
||||
|
||||
|
||||
this.intervalStop = setInterval(async ()=>{
|
||||
let result;
|
||||
// if(messages.length ===0) return;
|
||||
|
||||
|
||||
try{
|
||||
result = await this.chat(JSON.stringify({
|
||||
messages, currentTime:Date.now()+1}
|
||||
@@ -55,9 +58,9 @@ class Ai{
|
||||
|
||||
try{
|
||||
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);
|
||||
if(message.text === '___') return;
|
||||
setTimeout(async (message)=>{
|
||||
@@ -66,12 +69,16 @@ class Ai{
|
||||
}
|
||||
}catch(error){
|
||||
console.log('Error in AI message loop', error, result);
|
||||
if(result || result.response || result.response.text()){
|
||||
console.log(result.response.text())
|
||||
try {
|
||||
if(result && this.provider.getResponse(result)){
|
||||
console.log(this.provider.getResponse(result))
|
||||
}
|
||||
} catch(e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}, this.intervalLength*1000);
|
||||
|
||||
|
||||
}catch(error){
|
||||
console.log('error in onReady', error);
|
||||
}
|
||||
@@ -83,107 +90,56 @@ class Ai{
|
||||
clearInterval(this.intervalStop);
|
||||
this.intervalStop = undefined;
|
||||
}
|
||||
this.messageListener();
|
||||
|
||||
if(this.messageListener){
|
||||
this.messageListener();
|
||||
}
|
||||
if(this.provider){
|
||||
await this.provider.close();
|
||||
}
|
||||
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){
|
||||
const genAI = new GoogleGenerativeAI(conf.ai.key);
|
||||
const config = this.__getConfig();
|
||||
let bulbaItems = {};
|
||||
|
||||
const model = genAI.getGenerativeModel({
|
||||
model: "gemini-1.5-flash",
|
||||
console.log(`${this.bot.name} AI config:`, {
|
||||
provider: config.provider,
|
||||
model: config.model,
|
||||
promptName: this.promptName,
|
||||
baseUrl: config.baseUrl,
|
||||
});
|
||||
|
||||
let bulbaItems = await axios.get('https://webstore.bulbastore.uk/api/listings');
|
||||
bulbaItems = bulbaItems.data.listings.map(i=>i.listing_name);
|
||||
|
||||
console.log('AI for prompts', conf.ai.prompts)
|
||||
|
||||
this.prompt = conf.ai.prompts[this.promptName](
|
||||
const prompt = conf.ai.prompts[this.promptName](
|
||||
this.bot.bot.entity.username,
|
||||
conf.ai.interval,
|
||||
config.interval,
|
||||
Object.values(this.bot.getPlayers()).map(player=>`<[${player.lvl}] ${player.username}>`).join('\n'),
|
||||
bulbaItems,
|
||||
this.prompCustom,
|
||||
);
|
||||
|
||||
this.session = model.startChat({
|
||||
...this.__settings(history),
|
||||
// systemInstruction: this.prompt,
|
||||
// Create the provider instance with merged config and prompt
|
||||
this.provider = ProviderFactory.create({
|
||||
...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){
|
||||
console.log('chat', retryCount)
|
||||
console.log(`chat ${this.bot.name}`, retryCount)
|
||||
try{
|
||||
let result = await this.session.sendMessage(message);
|
||||
|
||||
let result = await this.provider.chat(message);
|
||||
return result
|
||||
}catch(error){
|
||||
console.log('AI chat error', 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++)
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = Ai;
|
||||
// run();
|
||||
module.exports = Ai;
|
||||
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
|
||||
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
|
||||
// Keep track of used inventory slots to avoid reusing the same slot
|
||||
let usedInventorySlots = new Set();
|
||||
let slotCount = 1;
|
||||
for(let shapeRow of recipe.inShape){
|
||||
for(let shape of shapeRow){
|
||||
this.bot.bot.moveSlotItem(
|
||||
inventory().find((element)=> element && element.type === shape.id).slot,
|
||||
slotCount
|
||||
let inventorySlot = inventory.findIndex((element, index) =>
|
||||
element && element.type === shape.id && !usedInventorySlots.has(index)
|
||||
);
|
||||
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}`);
|
||||
slotCount++;
|
||||
}
|
||||
|
||||
@@ -152,16 +152,24 @@ class CraftChests{
|
||||
|
||||
// Move these into openCrating function
|
||||
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
|
||||
// Keep track of used inventory slots to avoid reusing the same slot
|
||||
let usedInventorySlots = new Set();
|
||||
let slotCount = 1;
|
||||
for(let shapeRow of recipe.inShape){
|
||||
for(let shape of shapeRow){
|
||||
this.bot.bot.moveSlotItem(
|
||||
inventory().find((element)=> element && element.type === shape.id).slot,
|
||||
slotCount
|
||||
let inventorySlot = inventory.findIndex((element, index) =>
|
||||
element && element.type === shape.id && !usedInventorySlots.has(index)
|
||||
);
|
||||
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}`);
|
||||
slotCount++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user