1091 lines
37 KiB
JavaScript
1091 lines
37 KiB
JavaScript
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}`);
|
||
});
|