// ==UserScript== // @name 美团外卖自动出餐 // @namespace https://circlecloud.ltd/ // @version 0.0.1 // @description 自动出餐 // @author MiaoWoo // @match https://e.waimai.meituan.com/**region_id=**®ion_version=** // @icon https://www.google.com/s2/favicons?sz=64&domain=meituan.com // @grant none // ==/UserScript== (function () { 'use strict'; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function generateRandomString(length) { var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; var result = ''; var charactersLength = characters.length; for (var i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; } function convertSecondsToMinutesSeconds(seconds) { var minutes = Math.floor(seconds / 60); var remainingSeconds = seconds % 60; return minutes + "分" + remainingSeconds.toFixed(0) + "秒"; } function request(path, params) { return fetch(`https://e.waimai.meituan.com/${path}?region_id=${cookies.region_id}®ion_version=${cookies.region_version}`, { "headers": { "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "content-type": "application/x-www-form-urlencoded", "sec-ch-ua": "\"Microsoft Edge\";v=\"119\", \"Chromium\";v=\"119\", \"Not?A_Brand\";v=\"24\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin" }, "referrer": "https://e.waimai.meituan.com/new_fe/business_gw", "referrerPolicy": "unsafe-url", "body": (new URLSearchParams(params)).toString(), "method": "POST", "mode": "cors", "credentials": "include" }).then(r => r.json()) } async function requestGet(path, params) { return await fetch(`https://e.waimai.meituan.com/gw/phf${path}?${(new URLSearchParams({ region_id: cookies.region_id, region_version: cookies.region_version, ...params })).toString()}`, { "headers": { "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "sec-ch-ua": "\"Microsoft Edge\";v=\"119\", \"Chromium\";v=\"119\", \"Not?A_Brand\";v=\"24\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin" }, "referrer": "https://e.waimai.meituan.com/new_fe/business_gw", "referrerPolicy": "unsafe-url", "body": null, "method": "GET", "mode": "cors", "credentials": "include" }).then(r => r.json()); } function listUnprocessedOrder() { return request('/gw/api/order/mix/unprocessed/list/common', { tag: "process", pageSize: 10, pageNum: 1, pageGray: 1, }) } async function completeMealTime(order, cookTime = order.cookTime, tip = order.deliveOnShop ? '骑手已到店' : '骑手未到店') { let wmOrderViewId = order.wmOrderViewIdStr let daySeq = order.daySeq let result = await request('/v2/common/w/reported/completeMealTime', { wmPoiId: mt.wmPoiId, wmOrderViewId: wmOrderViewId }) debug('订单 #' + daySeq + ' 上报出餐结束.') mt.submitOrders.push({ completeTime: new Date().toLocaleTimeString(), daySn: daySeq, orderId: wmOrderViewId, tip, cookTime }) if (mt.submitOrders.length > mt.maxLogOrderLength) { mt.submitOrders = mt.submitOrders.slice(mt.submitOrders.length - mt.maxLogOrderLength) } localStorage.setItem(SubmitOrdersKey, JSON.stringify(mt.submitOrders)) await sleep(5) await syncOrders() return result } async function checkCooking() { if (!mt.processOrders) return for (const order of mt.processOrders) { if (order.isFoodDone) continue order.cookTime -= 5 order.leftTime -= 5 order.submitLeft -= 5 if (order.submitLeft < 0) { if (order.cookTime < 180) { console.log(order) debug('订单 #' + order.daySeq + ' 出餐时间 ' + order.cookTime + 's 小于 ' + 180 + 's 取消自动上报.') continue } if (order.deliveOnShop) { debug('订单 #' + order.daySeq + ' 骑手已到店.') debug('订单 #' + order.daySeq + ' 剩余出餐时间 ' + order.leftTime + 's 小于 ' + mt.autoSubmitLeftTimeWhenOnShop + 's 模拟提交出餐.') } else { debug('订单 #' + order.daySeq + ' 骑手未到店.') debug('订单 #' + order.daySeq + ' 剩余出餐时间 ' + order.leftTime + 's 小于 ' + mt.autoSubmitLeftTime + 's 模拟提交出餐.') } return await completeMealTime(order, order.cookTime) } if (order.submitLeft < 20) { debug('订单 #' + order.daySeq + ' 还剩 ' + order.leftTime + 's 上报超时 将于 ' + order.submitLeft + 's 后自动上报.') } else { updateInfo() } } } async function syncPhfOrderMeal(wmOrderViewId) { let result = await requestGet('/v2/order/receive/processed/r/orderAsyncInfos/v3', { orderInfos: JSON.stringify([{ "wmOrderViewId": wmOrderViewId, "wmPoiId": cookies.wmPoiId, "cityId": cookies.city_id }]) }) return result.data[wmOrderViewId] } async function syncPhfOrderlLgistic(wmOrderViewId) { let result = await requestGet('/v2/order/receive/processed/r/distribute/list/v2', { orderInfos: JSON.stringify([{ "wmOrderViewId": wmOrderViewId, "wmPoiId": cookies.wmPoiId, "cityId": cookies.city_id }]) }) return result.data[wmOrderViewId] } async function syncOrderInfo(order) { let now = Math.floor(Date.now() / 1000) order.wordLogo = '' order.daySeq = order.commonInfo.wm_poi_order_dayseq order.cookTime = now - order.commonInfo.confirmTime order.leftTime = order.commonInfo.confirmTime + 480 - now if (order.businessType == 1) { // 获取订单类型 普通订单存在预订单 order.wordLogo = order.orderInfo.orderInfo.wordLogo || '普' order.wmOrderViewIdStr = order.orderInfo.orderInfo.wmOrderViewId order.isFoodDone = !order.orderInfo.mealInfo.foodDoneButtonVo.isShow order.cookTime = now - order.orderInfo.mealInfo.countDownTimerVo.timerShowVo.preMealBeginTime order.logisticsStatus = order.orderInfo.logisticsInfo.logisticsProcessVo.processDesc // 部分数据存在长 结束备餐时间可能是0 需要计算开始时间 + 480(配置的出餐时间) - 当前时间 if (order.orderInfo.mealInfo.countDownTimerVo.timerShowVo.preMealEndTime) { order.leftTime = order.orderInfo.mealInfo.countDownTimerVo.timerShowVo.preMealEndTime - now } else { order.leftTime = order.orderInfo.mealInfo.countDownTimerVo.timerShowVo.preMealBeginTime + 480 - now } if (order.wordLogo == '预' && !order.isFoodDone) { // 预订单需要从提醒时间开始计算备餐时间 order.leftTime = order.orderInfo.mealInfo.foodDoneTitleVo.reminderTime + 480 - now } order.submitCanClickLimitTime = order.orderInfo.mealInfo.foodDoneButtonVo.buttonClickVo?.canClickLimitTime || 0 } else if (order.businessType == 2) { order.wordLogo = '拼' order.wmOrderViewIdStr = order.orderInfo.wm_order_id_view_str order.mealInfo = await syncPhfOrderMeal(order.wmOrderViewIdStr) order.logisticsInfo = await syncPhfOrderlLgistic(order.wmOrderViewIdStr) order.isFoodDone = order.mealInfo.prepareStauts == 2 || order.phfOperationContent order.logisticsStatus = order.logisticsInfo.latestLogisticsLogDesc.split(' ')[1] order.submitCanClickLimitTime = order.mealInfo.buttonTimeLimit } order.deliveOnShop = order.logisticsStatus == '骑手已到店' // 剩余时间取最小出餐时间和剩余出餐时间大的值 order.submitLeft = Math.max(order.submitCanClickLimitTime - now, order.leftTime - (order.deliveOnShop ? mt.autoSubmitLeftTimeWhenOnShop : mt.autoSubmitLeftTime)) } async function syncOrders() { let originCount = mt.processOrders.length let orders = await listUnprocessedOrder() orders = orders.data.wmOrderList?.map(o => { o.commonInfo = JSON.parse(o.commonInfo) o.orderInfo = JSON.parse(o.orderInfo) return o }) await Promise.all(orders.map(syncOrderInfo)) mt.processOrders = orders || [] if (originCount != mt.processOrders.length) { debug('更新订单数据 目前进行中订单: ' + mt.processOrders.length + '个') } else { updateInfo() } } function printOrderInfo(order) { let leftTime = order.submitCanClickLimitTime < Date.now() / 1000 ? order.submitLeft : order.submitCanClickLimitTime - Date.now() / 1000 return `订单: #${order.daySeq} ${order.wordLogo} 出餐状态 ${order.isFoodDone ? '已出餐' : `未出餐(${leftTime.toFixed(0)}s)`} 配送状态 ${order.logisticsStatus}` } function debug(msg) { mt.logs.push('[' + (new Date().toLocaleTimeString()) + '] ' + msg) if (mt.logs.length > mt.maxLogLength) { mt.logs = mt.logs.slice(mt.logs.length - mt.maxLogLength) } updateInfo() } function updateInfo() { try { let title = `${mt.title} Version: ${mt.version} By ${mt.author}` let configInfo = `当前配置:
订单 刷新间隔: ${mt.syncInterval}s 检测间隔: ${mt.checkInterval}s
出餐 最长时间: ${mt.autoSubmitMaxCookTime}s 骑手未到店: ${mt.autoSubmitLeftTime}s 骑手已到店: ${mt.autoSubmitLeftTimeWhenOnShop}s
` let orderInfo = `进行中的订单信息 更新时间:${new Date().toLocaleTimeString()}
${mt.processOrders.map(printOrderInfo).join('
')}` let submitInfo = `已自动出餐的订单信息:
${mt.submitOrders.length ? mt.submitOrders .map(o => '[' + o.completeTime + '] 订单: #' + o.daySn + ' 出餐用时 ' + convertSecondsToMinutesSeconds(o.cookTime) + ' ' + o.tip).join('
') : '当前没有自动出餐的订单.'}
` let logs = `运行日志:
${mt.logs.join('
')}
` window.appContainerNoticeBar.innerHTML = `
${title}
${configInfo}
${orderInfo}
${submitInfo}
${logs}
` } catch (error) { console.log(msg) } } async function scheduleCheck() { await checkCooking() await sleep(mt.checkInterval * 1000) scheduleCheck() } async function main() { while (!document.getElementById('hashframe')) { await sleep(300) } console.log(`${mt.title} Version: ${mt.version} By ${mt.author}`) window.appContainerNoticeBar = document.createElement('div') let iframe = document.getElementById('hashframe') iframe.parentNode.insertBefore(window.appContainerNoticeBar, iframe) debug('页面注入成功 开始运行...') syncOrders() scheduleCheck() setInterval(() => syncOrders(), mt.syncInterval * 1000) } let cookies = document.cookie.split('; ').map(cookie => cookie.split('=')).reduce((c, [key, value]) => { c[key] = decodeURIComponent(value); return c; }, {}); var SubmitOrdersKey = 'AutoMealComplete:SubmitOrders' let mt = { title: '美团自动出餐', version: '0.0.5', author: 'MiaoWoo', wmPoiId: localStorage.product_local_first_enter_screen, checkInterval: 5, syncInterval: 15, maxLogLength: 20, maxLogOrderLength: 20, autoSubmitLeftTime: 260, autoSubmitMaxCookTime: 420, autoSubmitLeftTimeWhenOnShop: 280, logs: [], processOrders: [], submitOrders: JSON.parse(localStorage.getItem(SubmitOrdersKey)) || [], debug: debug, syncOrders: syncOrders, } window.mt = mt main() })();