This commit is contained in:
2026-07-12 17:26:59 -04:00
parent 60663b8d4a
commit ba1bef8ff9
25 changed files with 158880 additions and 1529 deletions
+185
View File
@@ -163,6 +163,31 @@ class Database {
UNIQUE(site_id, player_name)
)
`);
// Application settings table (runtime-tunable, editable via web UI and LLM)
await this.db.exec(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'string' CHECK(type IN ('string', 'number', 'boolean', 'json', 'secret')),
category TEXT NOT NULL DEFAULT 'general',
label TEXT,
description TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Per-bot settings table (autoConnect, autoReConnect, onDemand, plugins, etc.)
await this.db.exec(`
CREATE TABLE IF NOT EXISTS bot_settings (
bot_name TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'string' CHECK(type IN ('string', 'number', 'boolean', 'json', 'secret')),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(bot_name, key)
)
`);
}
async insertDefaultPermissions() {
@@ -181,6 +206,134 @@ class Database {
}
}
// ========================================
// Settings
// ========================================
async seedDefaultSettings(defaults) {
for (const entry of defaults) {
try {
await this.db.run(
'INSERT OR IGNORE INTO settings (key, value, type, category, label, description) VALUES (?, ?, ?, ?, ?, ?)',
[entry.key, entry.value, entry.type, entry.category, entry.label, entry.description || null]
);
} catch (error) {
console.error('Error seeding setting:', entry.key, error);
}
}
}
async getAllSettings() {
return await this.db.all('SELECT * FROM settings ORDER BY category, key');
}
async getSettingsByCategory(category) {
return await this.db.all('SELECT * FROM settings WHERE category = ? ORDER BY key', [category]);
}
async getSetting(key) {
return await this.db.get('SELECT * FROM settings WHERE key = ?', [key]);
}
async setSetting(key, value) {
const row = await this.db.get('SELECT type FROM settings WHERE key = ?', [key]);
if (!row) throw new Error(`Unknown setting: ${key}`);
const coerced = this._coerceValue(value, row.type);
await this.db.run(
'UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?',
[coerced, key]
);
return coerced;
}
_coerceValue(value, type) {
switch (type) {
case 'number': {
const n = Number(value);
if (isNaN(n)) throw new Error(`Expected number, got: ${value}`);
return String(n);
}
case 'boolean': return value === true || value === 'true' || value === '1' ? 'true' : 'false';
case 'json': return JSON.stringify(typeof value === 'string' ? JSON.parse(value) : value);
default: return String(value);
}
}
// ========================================
// Bot Settings (per-bot configuration)
// ========================================
async getAllBotSettings(botName) {
return await this.db.all('SELECT * FROM bot_settings WHERE bot_name = ? ORDER BY key', [botName]);
}
async getBotSetting(botName, key) {
return await this.db.get('SELECT * FROM bot_settings WHERE bot_name = ? AND key = ?', [botName, key]);
}
async setBotSetting(botName, key, value, type) {
const coerced = this._coerceValue(value, type || 'string');
await this.db.run(`
INSERT INTO bot_settings (bot_name, key, value, type)
VALUES (?, ?, ?, ?)
ON CONFLICT(bot_name, key) DO UPDATE SET
value = excluded.value,
type = excluded.type,
updated_at = CURRENT_TIMESTAMP
`, [botName, key, coerced, type || 'string']);
return coerced;
}
async setBotSettings(botName, settings) {
await this.db.run('SAVEPOINT setBotSettings');
try {
for (const [key, entry] of Object.entries(settings)) {
const type = entry.type || 'string';
const coerced = this._coerceValue(entry.value, type);
await this.db.run(`
INSERT INTO bot_settings (bot_name, key, value, type)
VALUES (?, ?, ?, ?)
ON CONFLICT(bot_name, key) DO UPDATE SET
value = excluded.value,
type = excluded.type,
updated_at = CURRENT_TIMESTAMP
`, [botName, key, coerced, type]);
}
await this.db.run('RELEASE setBotSettings');
} catch (error) {
await this.db.run('ROLLBACK TO setBotSettings');
throw error;
}
}
async seedDefaultBotSettings() {
const conf = require('../../conf');
const bots = conf.mc?.bots || {};
for (const [botName, botConfig] of Object.entries(bots)) {
const defaults = {
username: { value: String(botConfig.username || ''), type: 'string' },
password: { value: String(botConfig.password || ''), type: 'secret' },
auth: { value: String(botConfig.auth || 'microsoft'), type: 'string' },
autoConnect: { value: String(botConfig.autoConnect ?? true), type: 'boolean' },
autoReConnect: { value: String(botConfig.autoReConnect ?? true), type: 'boolean' },
onDemand: { value: String(botConfig.onDemand || false), type: 'boolean' },
idleTimeout: { value: String(botConfig.idleTimeout || 30000), type: 'number' },
commands: { value: JSON.stringify(botConfig.commands || []), type: 'json' },
plugins: { value: JSON.stringify(botConfig.plugins || {}), type: 'json' },
hasAi: { value: String(botConfig.hasAi || false), type: 'boolean' },
};
for (const [key, entry] of Object.entries(defaults)) {
try {
await this.db.run(
'INSERT OR IGNORE INTO bot_settings (bot_name, key, value, type) VALUES (?, ?, ?, ?)',
[botName, key, entry.value, entry.type]
);
} catch (e) { /* ignore duplicates */ }
}
}
console.log('Bot settings seeded from config defaults');
}
// ========================================
// Permissions
// ========================================
@@ -238,6 +391,11 @@ class Database {
return await this.db.get('SELECT * FROM chests WHERE id = ?', [id]);
}
async markChestLost(x, y, z) {
await this.db.run('DELETE FROM chests WHERE pos_x = ? AND pos_y = ? AND pos_z = ?', [x, y, z]);
console.log(`Scanner: Removed lost chest at ${x},${y},${z}`);
}
async getChestByPosition(x, y, z) {
return await this.db.get(
'SELECT * FROM chests WHERE pos_x = ? AND pos_y = ? AND pos_z = ?',
@@ -258,6 +416,29 @@ class Database {
`, values);
}
async batchUpsertChests(chests) {
if (!chests || chests.length === 0) return;
await this.db.run('BEGIN TRANSACTION');
try {
for (const c of chests) {
await this.db.run(`
INSERT INTO chests (pos_x, pos_y, pos_z, chest_type, row, column, category)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(pos_x, pos_y, pos_z) DO UPDATE SET
chest_type = excluded.chest_type,
row = excluded.row,
column = excluded.column,
category = excluded.category,
last_scan = CURRENT_TIMESTAMP
`, [c.x, c.y, c.z, c.type, c.row, c.column, c.category]);
}
await this.db.run('COMMIT');
} catch (err) {
await this.db.run('ROLLBACK');
throw err;
}
}
// ========================================
// Shulkers
// ========================================
@@ -325,6 +506,10 @@ class Database {
return await this.db.run('DELETE FROM shulkers WHERE id = ?', [id]);
}
async deleteShulkersByChest(chestId) {
return await this.db.run('DELETE FROM shulkers WHERE chest_id = ?', [chestId]);
}
// Find a shulker that already stores this item type and has space (<27 slots used, not in-transit)
async findShulkerWithSpace(itemName, excludeId = null) {
return await this.db.get(`