Files
Farmer/server/index.js
2026-02-18 13:52:06 +08:00

946 lines
33 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`);
});