const crypto = require('crypto'); const { spawn } = require('child_process'); const axios = require('axios'); const express = require('express'); const http = require('http'); const path = require('path'); require('dotenv').config({ path: path.join(__dirname, '.env') }); require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const cors = require('cors'); const bodyParser = require('body-parser'); const updateRouter = require('./src/routes/update'); // 新增更新路由 const { createClient } = require('redis'); const { log } = require('./src/utils'); const userManager = require('./src/userManager'); const { MiniProgramLoginSession } = require('./src/qrlib/session'); // OAuth Dependencies const session = require('express-session'); const passport = require('passport'); const OAuth2Strategy = require('passport-oauth2'); // 初始化 Express App const app = express(); const server = http.createServer(app); const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379'; const redisPublisher = createClient({ url: redisUrl }); const redisSubscriber = createClient({ url: redisUrl }); const rpcPending = new Map(); let workerStarting = null; let workerProcess = null; let updateRunning = null; const workerMode = (process.env.FARMBOT_WORKER_MODE || 'detached').toLowerCase(); const adminWhitelistRaw = process.env.ADMIN_WHITELIST || ''; const adminWhitelist = new Set( adminWhitelistRaw.split(/[,;\s]+/).map(item => item.trim().toLowerCase()).filter(Boolean) ); redisPublisher.on('error', (err) => { log('Redis', `连接异常: ${err.message}`); }); redisPublisher.connect().then(() => { log('Redis', 'Publisher 已连接'); }).catch((err) => { log('Redis', `连接失败: ${err.message}`); }); redisSubscriber.on('error', (err) => { log('Redis', `订阅异常: ${err.message}`); }); redisSubscriber.connect().then(async () => { log('Redis', 'Subscriber 已连接'); await redisSubscriber.subscribe('bot:rpc:res', (message) => { const data = safeParseJson(message); const id = data && data.id; if (!id) return; const resolver = rpcPending.get(id); if (!resolver) return; rpcPending.delete(id); resolver(data); }); await redisSubscriber.pSubscribe('bot-log-*', (message, channel) => { const email = typeof channel === 'string' ? channel.slice('bot-log-'.length) : ''; if (!email) return; const data = safeParseJson(message); userManager.saveBotLog(email, data); }); await redisSubscriber.pSubscribe('bot-steal-*', (message, channel) => { const email = typeof channel === 'string' ? channel.slice('bot-steal-'.length) : ''; if (!email) return; const data = safeParseJson(message); const name = data && data.name; const count = Number(data && data.count); userManager.saveUnlucky(email, name, count); }); }).catch((err) => { log('Redis', `订阅连接失败: ${err.message}`); }); function safeParseJson(input) { try { return JSON.parse(input); } catch { return {}; } } function isAdminEmail(email) { if (!email) return false; return adminWhitelist.has(String(email).toLowerCase()); } function getRequestEmail(req) { const headerEmail = req.headers['x-admin-email']; if (headerEmail) return String(headerEmail); if (req.query && req.query.email) return String(req.query.email); if (req.body && req.body.email) return String(req.body.email); return ''; } function requireAdmin(req, res, next) { const email = getRequestEmail(req); if (!isAdminEmail(email)) { return res.status(403).json({ error: 'Admin only' }); } req.adminEmail = email; return next(); } function startWorkerProcess() { if (workerMode === 'external') { log('Worker', '外部模式,跳过拉起'); return; } if (workerProcess && !workerProcess.killed) return; const detached = workerMode === 'detached'; workerProcess = spawn(process.execPath, ['bot-worker.js'], { cwd: __dirname, stdio: detached ? 'ignore' : 'inherit', detached, windowsHide: true }); if (detached) { workerProcess.unref(); } else { workerProcess.on('exit', (code, signal) => { log('Worker', `Worker exited (code=${code}, signal=${signal || 'none'})`); }); } } async function sendRpc(action, email, payload, timeoutMs = 6000) { if (!redisPublisher.isOpen || !redisSubscriber.isOpen) { return { ok: false, error: 'Redis 未就绪', status: 503 }; } const id = crypto.randomUUID(); const body = JSON.stringify({ id, action, email, payload, ts: Date.now() }); return new Promise((resolve) => { const timer = setTimeout(() => { rpcPending.delete(id); resolve({ ok: false, error: 'Worker 响应超时', status: 504 }); }, timeoutMs); rpcPending.set(id, (response) => { clearTimeout(timer); resolve(response); }); redisPublisher.publish('bot:rpc:req', body).catch((err) => { clearTimeout(timer); rpcPending.delete(id); resolve({ ok: false, error: err.message || 'RPC 发送失败', status: 503 }); }); }); } async function ensureWorkerReady() { if (workerStarting) return workerStarting; workerStarting = (async () => { const ping = await sendRpc('worker.ping', null, null, 800); if (ping.ok) return true; startWorkerProcess(); await new Promise((resolve) => setTimeout(resolve, 500)); const retry = await sendRpc('worker.ping', null, null, 1500); if (!retry.ok) { return { ok: false, error: retry.error || 'Worker 未就绪' }; } return true; })(); try { return await workerStarting; } finally { workerStarting = null; } } async function callWorker(action, email, payload, timeoutMs) { try { const ready = await ensureWorkerReady(); if (ready !== true) { return { ok: false, error: ready && ready.error ? ready.error : 'Worker 未就绪', status: 503 }; } return sendRpc(action, email, payload, timeoutMs); } catch (err) { return { ok: false, error: err && err.message ? err.message : 'Worker 未就绪', status: 503 }; } } function runGit(args, cwd) { return new Promise((resolve) => { const child = spawn('git', args, { cwd, windowsHide: true }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); child.on('error', (err) => { resolve({ code: 1, stdout: '', stderr: err && err.message ? err.message : 'spawn failed' }); }); child.on('close', (code) => { resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() }); }); }); } async function fetchRemoteUpdateSummary() { const repo = 'Tony/Tieldalaes'; const base = 'https://gitea.infras.host/api/v1'; const commitsUrl = `${base}/repos/${repo}/commits?limit=5`; const releasesUrl = `${base}/repos/${repo}/releases?limit=5`; const [commitsRes, releasesRes] = await Promise.allSettled([ axios.get(commitsUrl, { timeout: 6000 }), axios.get(releasesUrl, { timeout: 6000 }) ]); const commits = commitsRes.status === 'fulfilled' && Array.isArray(commitsRes.value.data) ? commitsRes.value.data.map((item) => ({ sha: String(item?.sha || '').slice(0, 7), message: String(item?.commit?.message || '').split('\n')[0], author: String(item?.commit?.author?.name || ''), date: String(item?.commit?.author?.date || '') })) : []; const releases = releasesRes.status === 'fulfilled' && Array.isArray(releasesRes.value.data) ? releasesRes.value.data.map((item) => ({ tag: String(item?.tag_name || ''), name: String(item?.name || ''), date: String(item?.published_at || item?.created_at || ''), body: String(item?.body || '') })) : []; return { commits, releases }; } const oauthEnabled = Boolean(process.env.OAUTH_AUTHENTIK_ISSUER) && Boolean(process.env.OAUTH_AUTHENTIK_CLIENT_ID) && Boolean(process.env.OAUTH_AUTHENTIK_CLIENT_SECRET) && Boolean(process.env.OAUTH_AUTHENTIK_REDIRECT_URI) && Boolean(process.env.OAUTH_AUTHENTIK_SCOPE); // OAuth Setup if (oauthEnabled) { app.use(session({ secret: 'farm-session-secret', resave: false, saveUninitialized: false })); app.use(passport.initialize()); app.use(passport.session()); passport.serializeUser((user, done) => done(null, user.email)); passport.deserializeUser((email, done) => { const user = userManager.getUser(email); done(null, user); }); passport.use('authentik', new OAuth2Strategy({ authorizationURL: `${process.env.OAUTH_AUTHENTIK_ISSUER}authorize/`, tokenURL: `${process.env.OAUTH_AUTHENTIK_ISSUER}token/`, clientID: process.env.OAUTH_AUTHENTIK_CLIENT_ID, clientSecret: process.env.OAUTH_AUTHENTIK_CLIENT_SECRET, callbackURL: process.env.OAUTH_AUTHENTIK_REDIRECT_URI, scope: process.env.OAUTH_AUTHENTIK_SCOPE.split(' ') }, async (accessToken, refreshToken, profile, cb) => { try { // Fetch user info using accessToken const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); const userInfoRes = await fetch(`${process.env.OAUTH_AUTHENTIK_ISSUER}userinfo/`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); const userInfo = await userInfoRes.json(); const email = userInfo.email || userInfo.preferred_username; // Adjust based on Authentik response // Register or Login const user = await userManager.register(email, null, 'authentik', userInfo.sub); return cb(null, user); } catch (err) { return cb(err); } })); // OAuth Routes app.get('/api/auth/oauth/authentik', passport.authenticate('authentik', { state: true })); app.get('/api/auth/oauth/authentik/callback', (req, res, next) => { passport.authenticate('authentik', { state: true }, (err, user, info) => { const frontendUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:5173' : ''; if (err) { console.error('OAuth Error:', err); return res.redirect(`${frontendUrl}/login?error=oauth_failed`); } if (!user) { console.error('OAuth Failed: No user returned'); return res.redirect(`${frontendUrl}/login?error=oauth_failed`); } req.logIn(user, (loginErr) => { if (loginErr) { console.error('Login Error:', loginErr); return res.redirect(`${frontendUrl}/login?error=login_err`); } res.redirect(`${frontendUrl}/login?email=${encodeURIComponent(user.email)}&oauth=success&_t=${Date.now()}`); }); })(req, res, next); }); } // 中间件 app.use(cors()); app.use(bodyParser.json()); if (process.env.NODE_ENV === 'production') { app.use(express.static(path.join(__dirname, '../web/dist'))); } app.use('/api/update', updateRouter); // 注册更新路由 app.get('/api/auth/oauth/status', (req, res) => { res.json({ enabled: oauthEnabled }); }); function normalizeSettings(settings) { if (!settings || typeof settings !== 'object') return {}; const result = {}; const farmIntervalSec = Number(settings.farmIntervalSec); if (Number.isFinite(farmIntervalSec) && farmIntervalSec >= 1) { result.farmCheckInterval = Math.floor(farmIntervalSec * 1000); } const friendIntervalSec = Number(settings.friendIntervalSec); if (Number.isFinite(friendIntervalSec) && friendIntervalSec >= 1) { result.friendCheckInterval = Math.floor(friendIntervalSec * 1000); } if (settings.seedStrategy === 'forceLowest') { result.forceLowestLevelCrop = true; } else if (settings.seedStrategy === 'default') { result.forceLowestLevelCrop = false; } if (typeof settings.enableFriendOps === 'boolean') { result.enableFriendOps = settings.enableFriendOps; } if (typeof settings.enableSteal === 'boolean') { result.enableSteal = settings.enableSteal; } if (typeof settings.enableNormalFertilize === 'boolean') { result.enableNormalFertilize = settings.enableNormalFertilize; } if (typeof settings.enableOrganicFertilize === 'boolean') { result.enableOrganicFertilize = settings.enableOrganicFertilize; } if (typeof settings.allowTicketFertilizerPurchase === 'boolean') { result.allowTicketFertilizerPurchase = settings.allowTicketFertilizerPurchase; } if (typeof settings.enableAutoSell === 'boolean') { result.enableAutoSell = settings.enableAutoSell; } if (typeof settings.allowBuySeeds === 'boolean') { result.allowBuySeeds = settings.allowBuySeeds; } if (typeof settings.allowRemove === 'boolean') { result.allowRemove = settings.allowRemove; } if (settings.idleStrategy === 'task') { result.idleStrategy = 'task'; } else if (settings.idleStrategy === 'exp') { result.idleStrategy = 'exp'; } return result; } async function handleRpcResponse(res, response, fallback = {}) { if (!response || response.ok !== true) { const status = response && response.status ? response.status : 500; const error = response && response.error ? response.error : 'Worker 错误'; res.status(status).json({ error }); return false; } res.json(response.data ?? fallback); return true; } function getTodayKey() { const now = new Date(); const y = now.getFullYear(); const m = `${now.getMonth() + 1}`.padStart(2, '0'); const d = `${now.getDate()}`.padStart(2, '0'); return `${y}-${m}-${d}`; } const qrLogState = new Map(); function logQrOnce(message, minIntervalMs = 20000) { const now = Date.now(); const lastAt = qrLogState.get(message) || 0; if (now - lastAt < minIntervalMs) return; qrLogState.set(message, now); log('扫码', message); } // ============ API 路由 ============ // QR Code Login API (Mini Program) app.post('/api/qr/create', async (req, res) => { try { logQrOnce('正在获取登录二维码...'); const result = await MiniProgramLoginSession.requestLoginCode(); logQrOnce('已获取登录二维码'); res.json({ success: true, qrsig: result.code, qrcode: `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(result.url)}`, url: result.url }); } catch (error) { console.error('QR Create Error:', error); res.status(500).json({ success: false, message: error.message }); } }); app.post('/api/qr/check', async (req, res) => { const { qrsig } = req.body; if (!qrsig) { return res.status(400).json({ success: false, message: 'Missing qrsig/code' }); } try { const result = await MiniProgramLoginSession.queryStatus(qrsig); if (result.status === 'Wait') { res.json({ success: true, status: 'Wait', msg: '等待扫码...' }); } else if (result.status === 'Used') { res.json({ success: true, status: 'Used', msg: '二维码已失效' }); } else if (result.status === 'OK') { const ticket = result.ticket; // Determine AppID (Farm AppID) const appid = MiniProgramLoginSession.Presets.farm.appid; // QQ Classic Farm const code = await MiniProgramLoginSession.getAuthCode(ticket, appid); res.json({ success: true, status: 'OK', msg: '登录成功', code: code // This is the Game Code we need }); } else { res.json({ success: true, status: 'Error', msg: '状态查询错误' }); } } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); // 注册 API app.post('/api/auth/register', async (req, res) => { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: '请输入邮箱和密码' }); } try { if (userManager.getUser(email)) { return res.status(400).json({ error: '该邮箱已被注册' }); } const user = userManager.register(email, password); log('Auth', `New user registered: ${email}`); res.json({ success: true, user: { email: user.email, hasCode: !!user.code } }); } catch (e) { res.status(500).json({ error: e.message }); } }); // 登录 API (仅登录) app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: '请输入邮箱和密码' }); } try { const user = userManager.login(email, password); if (!user) { return res.status(401).json({ error: '邮箱或密码错误' }); } const rpcRes = await callWorker('bot.isRunning', email, null, 2000); const botRunning = rpcRes && rpcRes.ok && rpcRes.data && rpcRes.data.running === true; res.json({ success: true, user: { email: user.email, code: user.code, hasCode: !!user.code }, botRunning }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/auth/profile', async (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const user = userManager.getUser(email); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json({ user: { email: user.email, hasCode: !!user.code, auth_provider: user.auth_provider || 'local' } }); }); // 好友 API app.get('/api/friends', async (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('friends.list', email, null); await handleRpcResponse(res, response, { friends: [] }); }); // 访问好友农场 API app.post('/api/friends/visit', async (req, res) => { const { email, friendUid } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('friends.visit', email, { friendUid }); await handleRpcResponse(res, response, { lands: [] }); }); // 好友互动 API (偷菜/浇水等) app.post('/api/friends/action', async (req, res) => { const { email, friendUid, actionType, landIds } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('friends.action', email, { friendUid, actionType, landIds }); await handleRpcResponse(res, response, { success: true }); }); // 启动 Bot API app.post('/api/bot/start', async (req, res) => { const { email, code, settings } = req.body; // 如果提供了 code,更新并启动 if (!email) { return res.status(400).json({ error: 'Email is required' }); } const user = userManager.getUser(email); if (!user) { return res.status(404).json({ error: 'User not found' }); } // 如果提供了新 code,更新用户数据 let gameCode = typeof code === 'string' ? code.trim() : code; if (gameCode) { userManager.updateCode(email, gameCode); } else { gameCode = typeof user.code === 'string' ? user.code.trim() : user.code; } if (!gameCode) { return res.status(400).json({ error: 'Game code is required. Please provide it.' }); } try { const botConfig = { code: gameCode, ...normalizeSettings(settings) }; const response = await callWorker('bot.start', email, botConfig, 12000); await handleRpcResponse(res, response, { success: true, message: 'Bot started successfully.' }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 停止 Bot API app.post('/api/bot/stop', (req, res) => { const { email } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } callWorker('bot.stop', email, null).then((response) => { handleRpcResponse(res, response, { success: true }); }).catch((err) => { res.status(500).json({ error: err.message }); }); }); app.post('/api/bot/settings', async (req, res) => { const { email, settings } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const user = userManager.getUser(email); if (!user) { return res.status(404).json({ error: 'User not found' }); } try { const botConfig = normalizeSettings(settings); const response = await callWorker('bot.updateConfig', email, { settings: botConfig }, 8000); await handleRpcResponse(res, response, { success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 获取状态 API app.get('/api/status', async (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('bot.status', email, null, 5000); if (!response || response.ok !== true) { return res.json({ status: 'stopped', user: null }); } res.json(response.data); }); app.get('/api/announcement', (req, res) => { const message = typeof process.env.LOGIN_ANNOUNCEMENT === 'string' ? process.env.LOGIN_ANNOUNCEMENT : ''; res.json({ message }); }); app.get('/api/logs', async (req, res) => { const { email, limit } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const count = Number(limit); try { const logs = await userManager.getBotLogs(email, count); res.json({ logs }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/admin/me', (req, res) => { const { email } = req.query; res.json({ isAdmin: isAdminEmail(email) }); }); app.get('/api/admin/logs', requireAdmin, async (req, res) => { const { email, limit } = req.query; const count = Number(limit); try { const logs = await userManager.getAllBotLogs(count, email); res.json({ logs }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/admin/worker/stop-all', requireAdmin, async (req, res) => { const response = await callWorker('worker.stopAll', null, null, 5000); await handleRpcResponse(res, response, { success: true }); }); app.post('/api/admin/worker/shutdown', requireAdmin, async (req, res) => { const response = await callWorker('worker.shutdown', null, null, 5000); await handleRpcResponse(res, response, { success: true }); }); app.post('/api/admin/worker/start', requireAdmin, async (req, res) => { const ready = await ensureWorkerReady(); if (ready === true) { return res.json({ success: true }); } res.status(503).json({ error: ready && ready.error ? ready.error : 'Worker not ready' }); }); app.get('/api/admin/update/summary', requireAdmin, async (req, res) => { const repoRoot = path.join(__dirname, '..'); try { const [localHeadRes, remoteHeadRes, branchRes, remoteSummary] = await Promise.all([ runGit(['rev-parse', 'HEAD'], repoRoot), runGit(['ls-remote', 'origin', '-h', 'refs/heads/main'], repoRoot), runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot), fetchRemoteUpdateSummary() ]); const localHead = localHeadRes.stdout || ''; const remoteLine = remoteHeadRes.stdout || ''; const remoteHead = remoteLine.split('\t')[0] || ''; const branch = branchRes.stdout || ''; res.json({ local: { head: localHead, branch }, remote: { head: remoteHead }, updateNeeded: Boolean(localHead && remoteHead && localHead !== remoteHead), commits: remoteSummary.commits, releases: remoteSummary.releases, repo: 'https://gitea.infras.host/Tony/Tieldalaes' }); } catch (error) { res.status(500).json({ error: error && error.message ? error.message : '更新信息获取失败' }); } }); app.post('/api/admin/update/run', requireAdmin, async (req, res) => { if (updateRunning) { return res.status(409).json({ error: '更新进行中' }); } const repoRoot = path.join(__dirname, '..'); updateRunning = (async () => { const before = await runGit(['rev-parse', 'HEAD'], repoRoot); const pull = await runGit(['pull', '--rebase', 'origin', 'main'], repoRoot); const after = await runGit(['rev-parse', 'HEAD'], repoRoot); return { ok: pull.code === 0, before: before.stdout || '', after: after.stdout || '', output: pull.stdout || '', error: pull.stderr || '' }; })(); try { const result = await updateRunning; if (!result.ok) { return res.status(500).json({ error: result.error || '更新失败', output: result.output, before: result.before, after: result.after }); } res.json({ success: true, output: result.output, before: result.before, after: result.after }); } finally { updateRunning = null; } }); app.get('/api/debug/fertilizer', async (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('debug.fertilizer', email, null, 12000); await handleRpcResponse(res, response, {}); }); app.get('/api/leaderboard/unlucky', (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } userManager.getUnluckyTop(email, 10).then((items) => { res.json({ items }); }).catch((err) => { res.status(500).json({ error: err.message }); }); }); app.get('/api/tasks', async (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('tasks.list', email, null); await handleRpcResponse(res, response, { tasks: [] }); }); app.post('/api/tasks/claim', async (req, res) => { const { email } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('tasks.claimAll', email, null, 12000); await handleRpcResponse(res, response, { claimedCount: 0, failed: [], tasks: [] }); }); app.post('/api/tasks/claim-one', async (req, res) => { const { email, taskId, doShared } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const id = Number(taskId); if (!Number.isFinite(id) || id <= 0) { return res.status(400).json({ error: 'Invalid taskId' }); } const response = await callWorker('tasks.claimOne', email, { taskId: id, doShared: Boolean(doShared) }, 12000); await handleRpcResponse(res, response, { claimedCount: 0, failedCount: 0, tasks: [] }); }); // Get lands API app.get('/api/lands', async (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('lands.list', email, null); await handleRpcResponse(res, response, { lands: [] }); }); app.post('/api/lands/remove', async (req, res) => { const { email, landIds } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } if (!Array.isArray(landIds)) { return res.status(400).json({ error: 'Invalid landIds' }); } const ids = landIds.map(Number).filter(id => Number.isFinite(id) && id > 0); if (ids.length === 0) { return res.status(400).json({ error: 'Invalid landIds' }); } const response = await callWorker('lands.remove', email, { landIds: ids }); await handleRpcResponse(res, response, { success: true, removed: ids.length }); }); app.post('/api/lands/unlock', async (req, res) => { const { email, landId, doShared } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const id = Number(landId); if (!Number.isFinite(id) || id <= 0) { return res.status(400).json({ error: 'Invalid landId' }); } const response = await callWorker('lands.unlock', email, { landId: id, doShared: Boolean(doShared) }, 12000); await handleRpcResponse(res, response, { success: true, landId: id }); }); // Shop API app.get('/api/shop', async (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('shop.list', email, null); await handleRpcResponse(res, response, { goods: [] }); }); app.post('/api/shop/buy', async (req, res) => { const { email, goodsId, count, price } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('shop.buy', email, { goodsId, count, price }, 12000); await handleRpcResponse(res, response, { success: true }); }); app.post('/api/paymall/buy', async (req, res) => { const { email, itemId, count } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const id = Number(itemId); if (!Number.isFinite(id) || id <= 0) { return res.status(400).json({ error: 'Invalid itemId' }); } const buyCount = Number(count ?? 1); if (!Number.isFinite(buyCount) || buyCount <= 0) { return res.status(400).json({ error: 'Invalid count' }); } const response = await callWorker('shop.paymallBuy', email, { itemId: id, count: buyCount }, 12000); await handleRpcResponse(res, response, { success: true }); }); // Warehouse API app.get('/api/warehouse', async (req, res) => { const { email } = req.query; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('warehouse.list', email, null); await handleRpcResponse(res, response, {}); }); app.post('/api/warehouse/sell', async (req, res) => { const { email, items } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('warehouse.sell', email, { items }); await handleRpcResponse(res, response, { success: true, gold: 0 }); }); app.post('/api/warehouse/use', async (req, res) => { const { email, items } = req.body; if (!email) { return res.status(400).json({ error: 'Email is required' }); } const response = await callWorker('warehouse.use', email, { items }, 12000); await handleRpcResponse(res, response, {}); }); // System stats API app.get('/api/system/stats', (req, res) => { const memory = process.memoryUsage(); // Convert to MB const memoryUsage = { rss: Math.round(memory.rss / 1024 / 1024), heapTotal: Math.round(memory.heapTotal / 1024 / 1024), heapUsed: Math.round(memory.heapUsed / 1024 / 1024), }; callWorker('worker.stats', null, null, 2000).then((response) => { const workerData = response && response.ok && response.data ? response.data : null; const activeBots = workerData && Number.isFinite(workerData.activeBots) ? workerData.activeBots : 0; res.json({ memory: memoryUsage, uptime: Math.floor(process.uptime()), activeBots, worker: workerData ? { memory: workerData.memory || null, uptime: workerData.uptime || 0, activeBots } : null }); }).catch(() => { res.json({ memory: memoryUsage, uptime: Math.floor(process.uptime()), activeBots: 0, worker: null }); }); }); // 所有其他请求返回前端 index.html (SPA 支持) app.get('*', (req, res) => { if (process.env.NODE_ENV === 'production') { res.sendFile(path.join(__dirname, '../web/dist/index.html')); } else { res.status(404).send('Not Found (Development Mode: Use Frontend Dev Server at port 5173)'); } }); // 启动服务器 const PORT = process.env.PORT || 3000; server.listen(PORT, () => { log('Server', `Server running on http://localhost:${PORT}`); });