feat: initial commit for TheFarmer project

This commit is contained in:
Karriis
2026-02-18 13:52:06 +08:00
commit 8ceb5fa9db
420 changed files with 61918 additions and 0 deletions

945
server/index.js Normal file
View File

@@ -0,0 +1,945 @@
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}`);
});