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

1091 lines
37 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 express = require('express');
const http = require('http');
const path = require('path');
const os = require('os'); // Added for system stats
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 { Server } = require('socket.io');
const { FarmBot } = require('./src/core/FarmBot');
const { loadProto, types } = require('./src/proto');
const { log, toLong, toNum } = require('./src/utils');
const { getLevelExpProgress, getPlantNameBySeedId, getFruitName, getItemNameById } = require('./src/gameConfig');
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 io = new Server(server, {
cors: {
origin: "*", // 开发阶段允许跨域,生产环境应限制
methods: ["GET", "POST"]
}
});
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.get('/api/auth/oauth/status', (req, res) => {
res.json({ enabled: oauthEnabled });
});
// 存储活跃的 Bot 实例: Map<email, FarmBot>
const activeBots = new Map();
const unluckyBoards = new Map();
userManager.loadUnluckyBoards().then((stored) => {
if (!stored) return;
for (const [email, board] of stored.entries()) {
unluckyBoards.set(email, board);
}
}).catch((err) => {
log('Leaderboard', `加载失败: ${err.message}`);
});
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;
}
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}`;
}
async function fetchTaskInfo(bot) {
const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish();
const { body: replyBody } = await bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body);
const reply = types.TaskInfoReply.decode(replyBody);
return reply.task_info || null;
}
function buildTaskList(taskInfo) {
const tasks = [];
if (!taskInfo) return tasks;
const seen = new Set();
const seenContent = new Set();
const addTasks = (list, category) => {
if (!list) return;
for (const task of list) {
const id = toNum(task.id);
if (seen.has(id)) continue;
const progress = toNum(task.progress);
const totalProgress = toNum(task.total_progress);
const isClaimed = Boolean(task.is_claimed);
const isUnlocked = Boolean(task.is_unlocked);
const shareMultiple = toNum(task.share_multiple);
const rewards = [];
const rewardList = task.rewards || [];
for (const reward of rewardList) {
rewards.push({
id: toNum(reward.id),
count: toNum(reward.count)
});
}
const desc = task.desc || `任务#${id}`;
const contentKey = `${desc.trim()}|${progress}|${totalProgress}`;
if (seenContent.has(contentKey)) continue;
tasks.push({
id,
desc,
progress,
totalProgress,
isClaimed,
isUnlocked,
shareMultiple,
rewards,
category
});
seen.add(id);
seenContent.add(contentKey);
}
};
addTasks(taskInfo.growth_tasks, 'growth');
addTasks(taskInfo.daily_tasks, 'daily');
addTasks(taskInfo.tasks, 'normal');
return tasks;
}
function isTaskClaimable(task) {
if (!task.isUnlocked || task.isClaimed) return false;
if (task.totalProgress <= 0) return false;
return task.progress >= task.totalProgress;
}
async function claimTaskReward(bot, taskId, doShared) {
const body = types.ClaimTaskRewardRequest.encode(types.ClaimTaskRewardRequest.create({
id: toLong(taskId),
do_shared: Boolean(doShared)
})).finish();
const { body: replyBody } = await bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'ClaimTaskReward', body);
return types.ClaimTaskRewardReply.decode(replyBody);
}
function updateUnluckyBoard(email, name, count) {
if (!email || !name || !Number.isFinite(count) || count <= 0) return;
let board = unluckyBoards.get(email);
if (!board) {
board = new Map();
unluckyBoards.set(email, board);
}
const prev = board.get(name) || 0;
board.set(name, prev + count);
userManager.saveUnlucky(email, name, count).catch((err) => {
log('Leaderboard', `保存失败: ${err.message}`);
});
}
// 初始化 Proto
loadProto().then(() => {
log('系统', '协议定义加载完成。');
}).catch(err => {
console.error('Failed to load Proto:', err);
process.exit(1);
});
// ============ API 路由 ============
// QR Code Login API (Mini Program)
app.post('/api/qr/create', async (req, res) => {
try {
log('扫码', '正在获取登录二维码...');
const result = await MiniProgramLoginSession.requestLoginCode();
log('扫码', '已获取登录二维码');
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: '邮箱或密码错误' });
}
// 返回用户信息和 Bot 状态
const botRunning = activeBots.has(email);
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 || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
if (bot.config && bot.config.enableFriendOps === false) {
return res.status(403).json({ error: 'Friend operations disabled' });
}
try {
const reply = await bot.friendManager.getAllFriends();
// 处理好友列表
const friends = (reply.friends || []).map(f => ({
uin: f.uin ? f.uin.toString() : '',
userName: f.userName,
headPic: f.headPic,
yellowLevel: f.yellowLevel,
exp: f.exp,
money: f.money
}));
res.json({ friends });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// 仓库出售 API
app.post('/api/warehouse/sell', async (req, res) => {
const { email, items } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ error: 'No items to sell' });
}
const bot = activeBots.get(email);
try {
const reply = await bot.warehouseManager.sellItems(items);
const gold = bot.warehouseManager.extractGold(reply);
// 更新本地金币缓存
if (bot.user) {
bot.user.gold = (bot.user.gold || 0) + gold;
}
res.json({ success: true, gold });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// 访问好友农场 API
app.post('/api/friends/visit', async (req, res) => {
const { email, friendUid } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
if (bot.config && bot.config.enableFriendOps === false) {
return res.status(403).json({ error: 'Friend operations disabled' });
}
try {
const reply = await bot.friendManager.enterFriendFarm(friendUid);
if (!reply.farm || !reply.farm.lands) {
return res.json({ lands: [] });
}
// 使用 FriendManager 的逻辑解析好友土地数据
const analysis = bot.friendManager.analyzeFriendLands(lands, bot.user.gid);
const formattedLands = lands.map(land => {
const id = Number(land.id);
if (!land.unlocked) return { id, type: 'locked' };
const plant = land.plant;
if (!plant || !plant.phases || plant.phases.length === 0) {
return { id, type: 'empty' };
}
const currentPhase = bot.farmManager.getCurrentPhase(plant.phases, false);
const phaseVal = currentPhase ? currentPhase.phase : 0;
// 6=Mature, 7=Dead
let status = 'growing';
if (phaseVal === 7) status = 'dead';
else if (phaseVal === 6) status = 'mature';
return {
id,
type: 'planted',
status,
plantId: Number(plant.id),
plantName: getPlantNameBySeedId(Number(plant.id)) || `作物${plant.id}`,
phase: phaseVal,
needs: {
water: analysis.needWater.includes(id),
weed: analysis.needWeed.includes(id),
bug: analysis.needBug.includes(id)
},
canSteal: analysis.stealable.includes(id) // 使用 analyzeFriendLands 的 stealable 结果
};
});
res.json({ lands: formattedLands, farmUser: reply.farm.user });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// 好友互动 API (偷菜/浇水等)
app.post('/api/friends/action', async (req, res) => {
const { email, friendUid, actionType, landIds } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
if (bot.config && bot.config.enableFriendOps === false) {
return res.status(403).json({ error: 'Friend operations disabled' });
}
if (actionType === 'steal' && bot.config && bot.config.enableSteal === false) {
return res.status(403).json({ error: 'Steal disabled' });
}
try {
let result;
let message = '';
switch (actionType) {
case 'water':
result = await bot.friendManager.helpWater(friendUid, landIds);
message = '浇水成功';
break;
case 'weed':
result = await bot.friendManager.helpWeed(friendUid, landIds);
message = '除草成功';
break;
case 'insecticide':
result = await bot.friendManager.helpInsecticide(friendUid, landIds);
message = '除虫成功';
break;
case 'steal':
result = await bot.friendManager.stealHarvest(friendUid, landIds);
message = '偷菜成功';
break;
default:
throw new Error('Unknown action type');
}
res.json({ success: true, message, result });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// 启动 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 = code;
if (gameCode) {
userManager.updateCode(email, gameCode);
} else {
gameCode = user.code;
}
if (!gameCode) {
return res.status(400).json({ error: 'Game code is required. Please provide it.' });
}
try {
// 如果已存在 Bot先停止
if (activeBots.has(email)) {
const oldBot = activeBots.get(email);
oldBot.stop();
activeBots.delete(email);
}
const botConfig = { code: gameCode, ...normalizeSettings(settings) };
const bot = new FarmBot(botConfig);
// 监听 Bot 事件并通过 Socket.IO 推送给前端 (使用 email 作为 room/key)
bot.on('started', () => {
io.emit(`bot-status-${email}`, { status: 'running' });
});
bot.on('stopped', () => {
io.emit(`bot-status-${email}`, { status: 'stopped' });
activeBots.delete(email);
});
bot.on('error', (err) => {
io.emit(`bot-error-${email}`, { message: err.message });
userManager.saveBotLog(email, { tag: 'Error', msg: err.message, type: 'error', time: Date.now() });
});
bot.on('log', (logData) => {
io.emit(`bot-log-${email}`, logData);
userManager.saveBotLog(email, logData);
});
bot.on('stealRecord', (data) => {
const name = data && data.name;
const count = Number(data && data.count);
updateUnluckyBoard(email, name, count);
});
// 启动 Bot
await bot.start();
activeBots.set(email, bot);
res.json({ success: true, message: 'Bot started successfully.' });
} catch (error) {
console.error('Bot start failed:', error);
res.status(500).json({ error: error.message });
}
});
// 停止 Bot API
app.post('/api/bot/stop', (req, res) => {
const { email } = req.body;
if (activeBots.has(email)) {
const bot = activeBots.get(email);
bot.stop();
activeBots.delete(email);
res.json({ success: true });
} else {
res.status(404).json({ error: 'Bot is not running' });
}
});
// 获取状态 API
app.get('/api/status', async (req, res) => {
const { email } = req.query;
if (!email || !activeBots.has(email)) {
// 返回 stopped 状态而不是 404以便前端正确显示 UI
return res.json({ status: 'stopped', user: null });
}
const bot = activeBots.get(email);
const user = bot.user || {};
const todayKey = getTodayKey();
const freeMallClaimed = user.freeMallClaimDate === todayKey;
// Calculate next level exp
const levelProgress = getLevelExpProgress(user.level || 0, user.exp || 0);
const tickets = user.tickets || 0;
const fertilizerContainer = Number(user.fertilizerContainer) || 0;
const fertilizerItems = user.fertilizerItems || {};
const fallbackHours = fertilizerContainer >= 3600 ? fertilizerContainer / 3600 : fertilizerContainer;
const organicFertilizerContainer = Number(user.organicFertilizerContainer) || 0;
const organicFertilizerItems = user.organicFertilizerItems || {};
const fallbackOrganicHours = organicFertilizerContainer >= 3600 ? organicFertilizerContainer / 3600 : organicFertilizerContainer;
let fertilizerHours = Number.isFinite(user.fertilizerHours) ? user.fertilizerHours : fallbackHours;
let organicFertilizerHours = Number.isFinite(user.organicFertilizerHours) ? user.organicFertilizerHours : fallbackOrganicHours;
if (bot.warehouseManager) {
try {
const bagReply = await bot.warehouseManager.getBag();
const items = bot.warehouseManager.getBagItems(bagReply);
let normalContainer = 0;
let organicContainer = 0;
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
if (id === 1011) normalContainer = count;
if (id === 1012) organicContainer = count;
}
const normalContainerHours = normalContainer >= 3600 ? normalContainer / 3600 : normalContainer;
const organicContainerHours = organicContainer >= 3600 ? organicContainer / 3600 : organicContainer;
const normalHours = normalContainerHours;
const organicHours = organicContainerHours;
fertilizerHours = normalHours;
user.normalFertilizerHours = normalHours;
user.organicFertilizerHours = organicHours;
organicFertilizerHours = organicHours;
} catch (error) {
log('状态', `获取背包失败: ${error.message}`);
}
}
res.json({
status: 'running',
user: {
nickname: user.name || 'Farmer',
level: user.level || 0,
exp: levelProgress.current || 0,
nextLevelExp: levelProgress.needed || 100,
gold: user.gold || 0,
tickets,
fertilizerHours,
normalFertilizerHours: Number.isFinite(user.normalFertilizerHours) ? user.normalFertilizerHours : fertilizerHours,
organicFertilizerHours,
avatarUrl: user.avatarUrl || '',
freeMallClaimed
}
});
});
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/debug/fertilizer', async (req, res) => {
const { email } = req.query;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
try {
const bagReply = await bot.warehouseManager.getBag();
const items = bot.warehouseManager.getBagItems(bagReply);
let normalContainer = 0;
let normal1 = 0;
let normal4 = 0;
let normal8 = 0;
let normal12 = 0;
let organicContainer = 0;
let organic1 = 0;
let organic4 = 0;
let organic8 = 0;
let organic12 = 0;
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
if (id === 1011) normalContainer = count;
if (id === 80001) normal1 = count;
if (id === 80002) normal4 = count;
if (id === 80003) normal8 = count;
if (id === 80004) normal12 = count;
if (id === 1012) organicContainer = count;
if (id === 80011) organic1 = count;
if (id === 80012) organic4 = count;
if (id === 80013) organic8 = count;
if (id === 80014) organic12 = count;
}
const normalContainerHours = normalContainer >= 3600 ? normalContainer / 3600 : normalContainer;
const organicContainerHours = organicContainer >= 3600 ? organicContainer / 3600 : organicContainer;
const normalHours = normalContainerHours + normal1 + normal4 * 4 + normal8 * 8 + normal12 * 12;
const organicHours = organicContainerHours + organic1 + organic4 * 4 + organic8 * 8 + organic12 * 12;
res.json({
normal: {
container: normalContainer,
hour1: normal1,
hour4: normal4,
hour8: normal8,
hour12: normal12,
hours: normalHours
},
organic: {
container: organicContainer,
hour1: organic1,
hour4: organic4,
hour8: organic8,
hour12: organic12,
hours: organicHours
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/leaderboard/unlucky', (req, res) => {
const { email } = req.query;
if (!email) {
return res.status(400).json({ error: 'Email is required' });
}
if (activeBots.has(email)) {
const bot = activeBots.get(email);
if (bot.config && bot.config.enableFriendOps === false) {
return res.json({ items: [] });
}
if (bot.config && bot.config.enableSteal === false) {
return res.json({ items: [] });
}
}
const board = unluckyBoards.get(email);
if (!board) {
return res.json({ items: [] });
}
const items = Array.from(board.entries())
.map(([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
res.json({ items });
});
app.get('/api/tasks', async (req, res) => {
const { email } = req.query;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
try {
const taskInfo = await fetchTaskInfo(bot);
const tasks = buildTaskList(taskInfo);
res.json({ tasks });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/tasks/claim', async (req, res) => {
const { email } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
try {
const taskInfo = await fetchTaskInfo(bot);
const tasks = buildTaskList(taskInfo);
const claimable = [];
for (const task of tasks) {
if (isTaskClaimable(task)) {
claimable.push(task);
}
}
let claimedCount = 0;
const failed = [];
for (const task of claimable) {
try {
await claimTaskReward(bot, task.id, task.shareMultiple > 1);
claimedCount += 1;
} catch (err) {
failed.push({ id: task.id, message: err.message });
}
}
const latestTaskInfo = await fetchTaskInfo(bot);
const latestTasks = buildTaskList(latestTaskInfo);
res.json({ claimedCount, failed, tasks: latestTasks });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/tasks/claim-one', async (req, res) => {
const { email, taskId, doShared } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const id = Number(taskId);
if (!Number.isFinite(id) || id <= 0) {
return res.status(400).json({ error: 'Invalid taskId' });
}
const bot = activeBots.get(email);
try {
await claimTaskReward(bot, id, Boolean(doShared));
const latestTaskInfo = await fetchTaskInfo(bot);
const latestTasks = buildTaskList(latestTaskInfo);
res.json({ claimedCount: 1, failedCount: 0, tasks: latestTasks });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get lands API
app.get('/api/lands', async (req, res) => {
const { email } = req.query;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
try {
const lands = await bot.farmManager.getFormattedLands();
res.json({ lands });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/lands/remove', async (req, res) => {
const { email, landIds } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
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 bot = activeBots.get(email);
try {
await bot.farmManager.removePlant(ids);
res.json({ success: true, removed: ids.length });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/lands/unlock', async (req, res) => {
const { email, landId, doShared } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const id = Number(landId);
if (!Number.isFinite(id) || id <= 0) {
return res.status(400).json({ error: 'Invalid landId' });
}
const bot = activeBots.get(email);
try {
await bot.farmManager.unlockLand(id, Boolean(doShared));
res.json({ success: true, landId: id });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Shop API
app.get('/api/shop', async (req, res) => {
const { email } = req.query;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
try {
const goods = await bot.shopManager.getSeedShopList();
res.json({ goods });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/shop/buy', async (req, res) => {
const { email, goodsId, count, price } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
try {
await bot.shopManager.buyGoods(goodsId, count, price);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/paymall/buy', async (req, res) => {
const { email, itemId, count } = req.body;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
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 bot = activeBots.get(email);
try {
await bot.shopManager.purchaseMallItem(id, buyCount);
if (id === 1001) {
bot.user.freeMallClaimDate = getTodayKey();
bot.emit('userUpdate', bot.user);
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Warehouse API
app.get('/api/warehouse', async (req, res) => {
const { email } = req.query;
if (!email || !activeBots.has(email)) {
return res.status(404).json({ error: 'Bot not running' });
}
const bot = activeBots.get(email);
try {
const data = await bot.warehouseManager.getFormattedBag();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/warehouse/sell', async (req, res) => {
const { email, items } = req.body;
const bot = activeBots.get(email);
if (!bot) return res.status(404).json({ error: 'Bot not found' });
try {
const reply = await bot.warehouseManager.sellItems(items);
const gold = bot.warehouseManager.extractGold(reply);
res.json({ success: true, gold });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
app.post('/api/warehouse/use', async (req, res) => {
const { email, items } = req.body;
const bot = activeBots.get(email);
if (!bot) return res.status(404).json({ error: 'Bot not found' });
try {
const result = await bot.warehouseManager.useItems(items);
res.json(result);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// 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),
};
res.json({
memory: memoryUsage,
uptime: Math.floor(process.uptime()),
activeBots: activeBots.size
});
});
// 所有其他请求返回前端 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)');
}
});
// ============ Socket.IO ============
io.on('connection', (socket) => {
log('Server', `Client connected: ${socket.id}`);
socket.on('disconnect', () => {
log('Server', `Client disconnected: ${socket.id}`);
});
});
// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
log('Server', `Server running on http://localhost:${PORT}`);
});