const { createClient } = require('redis'); const { FarmBot } = require('./src/core/FarmBot'); const { loadProto, types } = require('./src/proto'); const { log, toLong, toNum } = require('./src/utils'); const { getLevelExpProgress, getPlantNameBySeedId } = require('./src/gameConfig'); const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379'; const redisPublisher = createClient({ url: redisUrl }); const redisSubscriber = createClient({ url: redisUrl }); const bots = new Map(); const locks = new Map(); function safeParseJson(input) { try { return JSON.parse(input); } catch { return {}; } } function publish(channel, payload) { if (!redisPublisher.isOpen) return; const body = JSON.stringify(payload || {}); redisPublisher.publish(channel, body).catch(() => {}); } 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 withEmailLock(email, task) { if (!email) return task(); const prev = locks.get(email) || Promise.resolve(); let release; const next = new Promise((resolve) => { release = resolve; }); locks.set(email, prev.then(() => next)); await prev; try { return await task(); } finally { release(); if (locks.get(email) === next) locks.delete(email); } } 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 attachBotEvents(email, bot) { bot.on('started', () => { publish(`bot-status-${email}`, { status: 'running' }); }); bot.on('stopped', () => { publish(`bot-status-${email}`, { status: 'stopped' }); bots.delete(email); }); bot.on('error', (err) => { publish(`bot-error-${email}`, { message: err.message }); }); bot.on('log', (logData) => { publish(`bot-log-${email}`, logData); }); bot.on('stealRecord', (data) => { publish(`bot-steal-${email}`, data || {}); }); } async function buildStatus(bot) { const user = bot.user || {}; const todayKey = getTodayKey(); const freeMallClaimed = user.freeMallClaimDate === todayKey; const levelProgress = getLevelExpProgress(user.level || 0, user.exp || 0); const tickets = user.tickets || 0; const fertilizerContainer = Number(user.fertilizerContainer) || 0; const fallbackHours = fertilizerContainer >= 3600 ? fertilizerContainer / 3600 : fertilizerContainer; const organicFertilizerContainer = Number(user.organicFertilizerContainer) || 0; 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}`); } } return { 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 } }; } function rpcOk(id, data) { return { id, ok: true, data }; } function rpcErr(id, error, status = 500) { return { id, ok: false, error, status }; } async function handleRpcRequest(req) { const id = req && req.id; const action = req && req.action; const email = req && req.email; const payload = (req && req.payload) || {}; if (!id || !action) return null; try { if (action === 'worker.ping') return rpcOk(id, { ok: true }); if (action === 'worker.stats') { const memory = process.memoryUsage(); return rpcOk(id, { activeBots: bots.size, memory: { rss: Math.round(memory.rss / 1024 / 1024), heapTotal: Math.round(memory.heapTotal / 1024 / 1024), heapUsed: Math.round(memory.heapUsed / 1024 / 1024) }, uptime: Math.floor(process.uptime()) }); } if (action === 'worker.stopAll') { let stopped = 0; for (const bot of bots.values()) { try { bot.stop(); stopped += 1; } catch { continue; } } bots.clear(); return rpcOk(id, { success: true, stopped }); } if (action === 'worker.shutdown') { for (const bot of bots.values()) { try { bot.stop(); } catch { continue; } } bots.clear(); setTimeout(() => process.exit(0), 200); return rpcOk(id, { success: true }); } if (action === 'bot.isRunning') { return rpcOk(id, { running: bots.has(email) }); } if (action === 'bot.start') { if (!email) return rpcErr(id, 'Email is required', 400); const rawCode = typeof payload.code === 'string' ? payload.code.trim() : ''; payload.code = rawCode; const code = rawCode; if (!code) return rpcErr(id, 'Game code is required. Please provide it.', 400); const response = await withEmailLock(email, async () => { const existing = bots.get(email); if (existing) { existing.stop(); bots.delete(email); } const bot = new FarmBot(payload); attachBotEvents(email, bot); const started = await bot.start(); if (!started) return rpcErr(id, bot.lastStartError || '启动失败', 503); bots.set(email, bot); return rpcOk(id, { success: true }); }); return response; } if (action === 'bot.stop') { if (!email) return rpcErr(id, 'Email is required', 400); const response = await withEmailLock(email, async () => { const bot = bots.get(email); if (!bot) return rpcErr(id, 'Bot is not running', 404); bot.stop(); bots.delete(email); return rpcOk(id, { success: true }); }); return response; } if (action === 'bot.status') { if (!email) return rpcErr(id, 'Email is required', 400); const bot = bots.get(email); if (!bot) return rpcOk(id, { status: 'stopped', user: null }); const data = await buildStatus(bot); return rpcOk(id, data); } if (action === 'bot.updateConfig') { if (!email) return rpcErr(id, 'Email is required', 400); const response = await withEmailLock(email, async () => { const bot = bots.get(email); if (!bot) return rpcErr(id, 'Bot not running', 404); const settings = payload && typeof payload === 'object' ? (payload.settings && typeof payload.settings === 'object' ? payload.settings : payload) : {}; bot.applyConfig(settings); return rpcOk(id, { success: true }); }); return response; } if (!email) return rpcErr(id, 'Email is required', 400); const bot = bots.get(email); if (!bot) return rpcErr(id, 'Bot not running', 404); if (action === 'friends.list') { if (bot.config && bot.config.enableFriendOps === false) { return rpcErr(id, 'Friend operations disabled', 403); } 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 })); return rpcOk(id, { friends }); } if (action === 'friends.visit') { if (bot.config && bot.config.enableFriendOps === false) { return rpcErr(id, 'Friend operations disabled', 403); } const friendUid = payload.friendUid; const reply = await bot.friendManager.enterFriendFarm(friendUid); const lands = reply && reply.farm && reply.farm.lands ? reply.farm.lands : []; if (lands.length === 0) return rpcOk(id, { lands: [] }); const analysis = bot.friendManager.analyzeFriendLands(lands, bot.user.gid); const formattedLands = lands.map(land => { const landId = Number(land.id); if (!land.unlocked) return { id: landId, type: 'locked' }; const plant = land.plant; if (!plant || !plant.phases || plant.phases.length === 0) { return { id: landId, type: 'empty' }; } const currentPhase = bot.farmManager.getCurrentPhase(plant.phases, false); const phaseVal = currentPhase ? currentPhase.phase : 0; let status = 'growing'; if (phaseVal === 7) status = 'dead'; else if (phaseVal === 6) status = 'mature'; return { id: landId, type: 'planted', status, plantId: Number(plant.id), plantName: getPlantNameBySeedId(Number(plant.id)) || `作物${plant.id}`, phase: phaseVal, needs: { water: analysis.needWater.includes(landId), weed: analysis.needWeed.includes(landId), bug: analysis.needBug.includes(landId) }, canSteal: analysis.stealable.includes(landId) }; }); return rpcOk(id, { lands: formattedLands, farmUser: reply.farm.user }); } if (action === 'friends.action') { if (bot.config && bot.config.enableFriendOps === false) { return rpcErr(id, 'Friend operations disabled', 403); } if (payload.actionType === 'steal' && bot.config && bot.config.enableSteal === false) { return rpcErr(id, 'Steal disabled', 403); } let result; let message = ''; if (payload.actionType === 'water') { result = await bot.friendManager.helpWater(payload.friendUid, payload.landIds); message = '浇水成功'; } else if (payload.actionType === 'weed') { result = await bot.friendManager.helpWeed(payload.friendUid, payload.landIds); message = '除草成功'; } else if (payload.actionType === 'insecticide') { result = await bot.friendManager.helpInsecticide(payload.friendUid, payload.landIds); message = '除虫成功'; } else if (payload.actionType === 'steal') { result = await bot.friendManager.stealHarvest(payload.friendUid, payload.landIds); message = '偷菜成功'; } else { return rpcErr(id, 'Unknown action type', 400); } return rpcOk(id, { success: true, message, result }); } if (action === 'warehouse.list') { const data = await bot.warehouseManager.getFormattedBag(); return rpcOk(id, data); } if (action === 'warehouse.sell') { const reply = await bot.warehouseManager.sellItems(payload.items); const gold = bot.warehouseManager.extractGold(reply); return rpcOk(id, { success: true, gold }); } if (action === 'warehouse.use') { const result = await bot.warehouseManager.useItems(payload.items); return rpcOk(id, result); } if (action === 'tasks.list') { const taskInfo = await fetchTaskInfo(bot); const tasks = buildTaskList(taskInfo); return rpcOk(id, { tasks }); } if (action === 'tasks.claimAll') { 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); return rpcOk(id, { claimedCount, failed, tasks: latestTasks }); } if (action === 'tasks.claimOne') { const idNum = Number(payload.taskId); if (!Number.isFinite(idNum) || idNum <= 0) return rpcErr(id, 'Invalid taskId', 400); await claimTaskReward(bot, idNum, Boolean(payload.doShared)); const latestTaskInfo = await fetchTaskInfo(bot); const latestTasks = buildTaskList(latestTaskInfo); return rpcOk(id, { claimedCount: 1, failedCount: 0, tasks: latestTasks }); } if (action === 'lands.list') { const lands = await bot.farmManager.getFormattedLands(); return rpcOk(id, { lands }); } if (action === 'lands.remove') { const ids = Array.isArray(payload.landIds) ? payload.landIds.map(Number).filter(idNum => Number.isFinite(idNum) && idNum > 0) : []; if (ids.length === 0) return rpcErr(id, 'Invalid landIds', 400); await bot.farmManager.removePlant(ids); return rpcOk(id, { success: true, removed: ids.length }); } if (action === 'lands.unlock') { const landId = Number(payload.landId); if (!Number.isFinite(landId) || landId <= 0) return rpcErr(id, 'Invalid landId', 400); const reply = await bot.farmManager.unlockLand(landId, Boolean(payload.doShared)); let land = reply && reply.land ? reply.land : null; if (!land || !land.unlocked) { const landsReply = await bot.farmManager.getAllLands(); const lands = landsReply && landsReply.lands ? landsReply.lands : []; land = lands.find(item => Number(item.id) === landId) || null; if (!land || !land.unlocked) { return rpcErr(id, 'Land not unlocked', 409); } } return rpcOk(id, { success: true, landId, land }); } if (action === 'shop.list') { const goods = await bot.shopManager.getSeedShopList(); return rpcOk(id, { goods }); } if (action === 'shop.buy') { await bot.shopManager.buyGoods(payload.goodsId, payload.count, payload.price); return rpcOk(id, { success: true }); } if (action === 'shop.paymallBuy') { const itemId = Number(payload.itemId); const buyCount = Number(payload.count ?? 1); if (!Number.isFinite(itemId) || itemId <= 0) return rpcErr(id, 'Invalid itemId', 400); if (!Number.isFinite(buyCount) || buyCount <= 0) return rpcErr(id, 'Invalid count', 400); await bot.shopManager.purchaseMallItem(itemId, buyCount); if (itemId === 1001) { bot.user.freeMallClaimDate = getTodayKey(); bot.emit('userUpdate', bot.user); } return rpcOk(id, { success: true }); } if (action === 'debug.fertilizer') { 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 idNum = toNum(item.id); const countNum = toNum(item.count); if (idNum === 1011) normalContainer = countNum; if (idNum === 80001) normal1 = countNum; if (idNum === 80002) normal4 = countNum; if (idNum === 80003) normal8 = countNum; if (idNum === 80004) normal12 = countNum; if (idNum === 1012) organicContainer = countNum; if (idNum === 80011) organic1 = countNum; if (idNum === 80012) organic4 = countNum; if (idNum === 80013) organic8 = countNum; if (idNum === 80014) organic12 = countNum; } 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; return rpcOk(id, { 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 } }); } return rpcErr(id, 'Unknown action', 400); } catch (err) { const message = err && err.message ? err.message : 'Worker error'; return rpcErr(id, message, 500); } } async function startWorker() { await loadProto(); await redisPublisher.connect(); await redisSubscriber.connect(); await redisSubscriber.subscribe('bot:rpc:req', async (message) => { const req = safeParseJson(message); const response = await handleRpcRequest(req); if (!response) return; const body = JSON.stringify(response); redisPublisher.publish('bot:rpc:res', body).catch(() => {}); }); log('Worker', 'Worker ready'); } startWorker().catch((err) => { log('Worker', `Worker failed: ${err.message}`); process.exit(1); });