Initial commit: POE2 automated trade bot

Monitors pathofexile.com/trade2 for new listings, travels to seller
hideouts, buys items from public stash tabs, and stores them.

Includes persistent C# OCR daemon for fast screen capture + Windows
native OCR, web dashboard for managing trade links and settings,
and full game automation via Win32 SendInput.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Boki 2026-02-10 14:03:47 -05:00
commit 41d174195e
28 changed files with 6449 additions and 0 deletions

View file

@ -0,0 +1,227 @@
import express from 'express';
import http from 'http';
import { WebSocketServer, WebSocket } from 'ws';
import path from 'path';
import { fileURLToPath } from 'url';
import { logger } from '../util/logger.js';
import type { BotController } from './BotController.js';
import type { ScreenReader } from '../game/ScreenReader.js';
import type { GameController } from '../game/GameController.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export interface DebugDeps {
screenReader: ScreenReader;
gameController: GameController;
}
export class DashboardServer {
private app = express();
private server: http.Server;
private wss: WebSocketServer;
private clients: Set<WebSocket> = new Set();
private bot: BotController;
private debug: DebugDeps | null = null;
constructor(bot: BotController, private port: number = 3000) {
this.bot = bot;
this.app.use(express.json());
this.app.get('/', (_req, res) => {
res.sendFile(path.join(__dirname, '..', '..', 'src', 'dashboard', 'index.html'));
});
// Status
this.app.get('/api/status', (_req, res) => {
res.json(this.bot.getStatus());
});
// Pause / Resume
this.app.post('/api/pause', (_req, res) => {
this.bot.pause();
this.broadcastStatus();
res.json({ ok: true });
});
this.app.post('/api/resume', (_req, res) => {
this.bot.resume();
this.broadcastStatus();
res.json({ ok: true });
});
// Links CRUD
this.app.post('/api/links', (req, res) => {
const { url, name } = req.body as { url: string; name?: string };
if (!url || !url.includes('pathofexile.com/trade')) {
res.status(400).json({ error: 'Invalid trade URL' });
return;
}
this.bot.addLink(url, name || '');
this.broadcastStatus();
res.json({ ok: true });
});
this.app.delete('/api/links/:id', (req, res) => {
this.bot.removeLink(req.params.id);
this.broadcastStatus();
res.json({ ok: true });
});
// Toggle link active/inactive
this.app.post('/api/links/:id/toggle', (req, res) => {
const { active } = req.body as { active: boolean };
this.bot.toggleLink(req.params.id, active);
this.broadcastStatus();
res.json({ ok: true });
});
// Rename link
this.app.post('/api/links/:id/name', (req, res) => {
const { name } = req.body as { name: string };
this.bot.updateLinkName(req.params.id, name);
this.broadcastStatus();
res.json({ ok: true });
});
// Settings
this.app.post('/api/settings', (req, res) => {
const updates = req.body as Record<string, unknown>;
const store = this.bot.getStore();
store.updateSettings(updates);
this.broadcastStatus();
res.json({ ok: true });
});
// Debug endpoints
this.app.post('/api/debug/screenshot', async (_req, res) => {
if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; }
try {
const files = await this.debug.screenReader.saveDebugScreenshots('debug-screenshots');
this.broadcastLog('info', `Debug screenshots saved: ${files.map(f => f.split(/[\\/]/).pop()).join(', ')}`);
res.json({ ok: true, files });
} catch (err) {
logger.error({ err }, 'Debug screenshot failed');
res.status(500).json({ error: 'Screenshot failed' });
}
});
this.app.post('/api/debug/ocr', async (_req, res) => {
if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; }
try {
const text = await this.debug.screenReader.readFullScreen();
this.broadcastLog('info', `OCR result (${text.length} chars): ${text.substring(0, 200)}`);
res.json({ ok: true, text });
} catch (err) {
logger.error({ err }, 'Debug OCR failed');
res.status(500).json({ error: 'OCR failed' });
}
});
this.app.post('/api/debug/find-text', async (req, res) => {
if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; }
const { text } = req.body as { text: string };
if (!text) { res.status(400).json({ error: 'Missing text parameter' }); return; }
try {
const pos = await this.debug.screenReader.findTextOnScreen(text);
if (pos) {
this.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y})`);
} else {
this.broadcastLog('warn', `"${text}" not found on screen`);
}
res.json({ ok: true, found: !!pos, position: pos });
} catch (err) {
logger.error({ err }, 'Debug find-text failed');
res.status(500).json({ error: 'Find text failed' });
}
});
this.app.post('/api/debug/click', async (req, res) => {
if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; }
const { x, y } = req.body as { x: number; y: number };
if (x == null || y == null) { res.status(400).json({ error: 'Missing x/y' }); return; }
try {
await this.debug.gameController.focusGame();
await this.debug.gameController.leftClickAt(x, y);
this.broadcastLog('info', `Clicked at (${x}, ${y})`);
res.json({ ok: true });
} catch (err) {
logger.error({ err }, 'Debug click failed');
res.status(500).json({ error: 'Click failed' });
}
});
this.app.post('/api/debug/find-and-click', async (req, res) => {
if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; }
const { text } = req.body as { text: string };
if (!text) { res.status(400).json({ error: 'Missing text parameter' }); return; }
try {
const pos = await this.debug.screenReader.findTextOnScreen(text);
if (pos) {
await this.debug.gameController.focusGame();
await this.debug.gameController.leftClickAt(pos.x, pos.y);
this.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y}) and clicked`);
res.json({ ok: true, found: true, position: pos });
} else {
this.broadcastLog('warn', `"${text}" not found on screen`);
res.json({ ok: true, found: false, position: null });
}
} catch (err) {
logger.error({ err }, 'Debug find-and-click failed');
res.status(500).json({ error: 'Find and click failed' });
}
});
this.server = http.createServer(this.app);
this.wss = new WebSocketServer({ server: this.server });
this.wss.on('connection', (ws) => {
this.clients.add(ws);
ws.send(JSON.stringify({ type: 'status', data: this.bot.getStatus() }));
ws.on('close', () => this.clients.delete(ws));
});
}
setDebugDeps(deps: DebugDeps): void {
this.debug = deps;
logger.info('Debug tools available on dashboard');
}
broadcastStatus(): void {
const msg = JSON.stringify({ type: 'status', data: this.bot.getStatus() });
for (const client of this.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
}
}
broadcastLog(level: string, message: string): void {
const msg = JSON.stringify({
type: 'log',
data: { level, message, time: new Date().toISOString() },
});
for (const client of this.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
}
}
async start(): Promise<void> {
return new Promise((resolve) => {
this.server.listen(this.port, () => {
logger.info({ port: this.port }, `Dashboard running at http://localhost:${this.port}`);
resolve();
});
});
}
async stop(): Promise<void> {
for (const client of this.clients) {
client.close();
}
return new Promise((resolve) => {
this.server.close(() => resolve());
});
}
}