1. 新增 content.js 页面级保活引擎,自动注入拼多多商家后台 (mms.pinduoduo.com); 2. 主动防休眠与静默续期:每 2 分钟向 MMS 接口发送轻量只读心跳并派发微动作,主动刷新服务端 PASS_ID 有效期,从根本上阻止空闲弹窗生成; 3. 超时弹窗秒级自动拦截:使用 MutationObserver 实时监听 DOM,毫秒级捕获「当前页面停留时间过长」弹窗并自动点击「刷新」,彻底解决页面挂死导致 Cookie 会话过期被踢出账号的问题; 4. 后台长效平滑刷新:后台标签页静置超 25 分钟自动执行无感安全重载,保障 CSRF Token 与会话 24 小时鲜活; 5. 日志链路打通:将页面防休眠、弹窗拦截与周期刷新事件接入扩展日志控制台。
217 lines
7.2 KiB
JavaScript
217 lines
7.2 KiB
JavaScript
/**
|
||
* Pinduoduo Merchant Page Active Keepalive & Anti-Inactivity Content Script
|
||
* 拼多多商家后台页面主动保活、防休眠心跳与超时弹窗秒级拦截注入脚本
|
||
*/
|
||
|
||
;(function () {
|
||
'use strict'
|
||
|
||
const TAG = '[PDD-KEEPALIVE]'
|
||
const HEARTBEAT_INTERVAL_MS = 2 * 60 * 1000 // 每 2 分钟执行一次主动心跳与会话续期
|
||
const AUTO_RELOAD_INTERVAL_MS = 25 * 60 * 1000 // 每 25 分钟自动平滑刷新一次页面以保证全状态新鲜
|
||
let lastUserActivityTime = Date.now()
|
||
let isReloading = false
|
||
|
||
console.log(`${TAG} 拼多多页面主动保活与防休眠助手已加载:`, window.location.href)
|
||
|
||
// 辅助:向 background 记录运行日志
|
||
function reportLog(level, action, message, details = null) {
|
||
try {
|
||
chrome.runtime.sendMessage({
|
||
action: 'content_log',
|
||
level,
|
||
module: '页面保活',
|
||
actionName: action,
|
||
message,
|
||
details,
|
||
})
|
||
} catch {
|
||
// 忽略通信异常
|
||
}
|
||
}
|
||
|
||
// 1. 监听真实用户操作,记录活跃时间戳
|
||
const userEvents = ['mousemove', 'mousedown', 'keydown', 'scroll', 'touchstart']
|
||
userEvents.forEach((evt) => {
|
||
window.addEventListener(
|
||
evt,
|
||
() => {
|
||
lastUserActivityTime = Date.now()
|
||
},
|
||
{ passive: true },
|
||
)
|
||
})
|
||
|
||
// 2. 模拟轻量用户活动(重置拼多多前端页面的空闲计时器)
|
||
function dispatchFakeUserActivity() {
|
||
try {
|
||
const evt = new MouseEvent('mousemove', {
|
||
view: window,
|
||
bubbles: true,
|
||
cancelable: true,
|
||
clientX: Math.floor(Math.random() * 200) + 100,
|
||
clientY: Math.floor(Math.random() * 200) + 100,
|
||
})
|
||
document.dispatchEvent(evt)
|
||
window.dispatchEvent(new Event('focus'))
|
||
} catch (e) {
|
||
console.warn(`${TAG} 模拟微动作异常:`, e)
|
||
}
|
||
}
|
||
|
||
// 3. 发送轻量级静默 API 心跳(主动向拼多多服务端续期 PASS_ID 与会话 TTL)
|
||
async function sendSilentSessionHeartbeat() {
|
||
try {
|
||
dispatchFakeUserActivity()
|
||
|
||
// 选择拼多多商家后台最轻量的只读接口进行探测与续期
|
||
const endpoints = [
|
||
'/mangkhut/mms/recentOrderList?page=1&pageSize=1',
|
||
'/home/',
|
||
'/orders/list',
|
||
]
|
||
const targetEndpoint = endpoints[Math.floor(Math.random() * endpoints.length)]
|
||
|
||
const resp = await fetch(targetEndpoint, {
|
||
method: 'GET',
|
||
credentials: 'include',
|
||
headers: {
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
Accept: 'application/json, text/plain, */*',
|
||
},
|
||
})
|
||
|
||
if (resp.ok) {
|
||
console.log(`${TAG} 服务端会话静默续期成功 (${targetEndpoint})`)
|
||
} else if (resp.status === 401 || resp.status === 403) {
|
||
reportLog('warn', '会话预警', `页面心跳返回 HTTP ${resp.status},可能需要重新登录`)
|
||
}
|
||
} catch (err) {
|
||
console.warn(`${TAG} 发送静默续期心跳异常:`, err)
|
||
}
|
||
}
|
||
|
||
// 4. 实时拦截超时提示遮罩弹窗(秒级自动点击刷新/恢复)
|
||
function scanAndBustInactivityModal() {
|
||
if (isReloading) return
|
||
|
||
// 拼多多超时弹窗典型文案特征
|
||
const triggerKeywords = [
|
||
'当前页面停留时间过长',
|
||
'为了避免数据延迟',
|
||
'建议刷新一次页面',
|
||
'停留时间过长',
|
||
]
|
||
|
||
// 查找所有可能的弹窗/Modal 容器
|
||
const modalContainers = document.querySelectorAll(
|
||
'.ant-modal, .pdd-modal, [class*="modal"], [class*="dialog"], [role="dialog"]',
|
||
)
|
||
|
||
for (const container of modalContainers) {
|
||
const text = container.textContent || ''
|
||
const hasKeyword = triggerKeywords.some((kw) => text.includes(kw))
|
||
if (!hasKeyword) continue
|
||
|
||
console.warn(`${TAG} 🚨 捕获到拼多多超时休眠弹窗!立即执行秒级自动恢复...`)
|
||
reportLog('warn', '弹窗拦截', '捕获到「页面停留时间过长」弹窗,立即自动点击刷新续期会话')
|
||
|
||
// 寻找弹窗内的「刷新」或确定按钮
|
||
const buttons = container.querySelectorAll('button, a, [role="button"]')
|
||
let clicked = false
|
||
|
||
for (const btn of buttons) {
|
||
const btnText = (btn.textContent || '').trim()
|
||
if (btnText.includes('刷新') || btnText.includes('确定') || btnText.includes('知道了')) {
|
||
btn.click()
|
||
clicked = true
|
||
console.log(`${TAG} ✅ 已自动触发弹窗内【${btnText}】按钮`)
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!clicked) {
|
||
// 若未找到合适按钮,直接调用页面无感平滑重载
|
||
isReloading = true
|
||
console.log(`${TAG} 未找到确定按钮,执行 location.reload()`)
|
||
setTimeout(() => {
|
||
window.location.reload()
|
||
}, 500)
|
||
}
|
||
return
|
||
}
|
||
|
||
// 全局文本扫描(兜底非标准 modal 结构)
|
||
const allDivs = document.querySelectorAll('div')
|
||
for (const div of allDivs) {
|
||
if (div.children.length === 0 && div.textContent) {
|
||
if (div.textContent.includes('当前页面停留时间过长')) {
|
||
const parent = div.closest('div[style*="z-index"]') || div.parentElement
|
||
const btn = parent ? parent.querySelector('button') : null
|
||
if (btn) {
|
||
console.warn(`${TAG} 兜底捕获超时弹窗,触发点击`)
|
||
btn.click()
|
||
} else {
|
||
isReloading = true
|
||
window.location.reload()
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. DOM 变化观察器(毫秒级感知弹窗挂载)
|
||
const observer = new MutationObserver(() => {
|
||
scanAndBustInactivityModal()
|
||
})
|
||
|
||
if (document.body) {
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true,
|
||
})
|
||
} else {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true,
|
||
})
|
||
})
|
||
}
|
||
|
||
// 6. 定时执行静默会话心跳
|
||
setInterval(() => {
|
||
sendSilentSessionHeartbeat()
|
||
scanAndBustInactivityModal()
|
||
}, HEARTBEAT_INTERVAL_MS)
|
||
|
||
// 7. 定时平滑刷新兜底(每 25 分钟:若用户 10 分钟未操作,自动安全刷新当前页面刷新凭证)
|
||
setInterval(() => {
|
||
const idleTime = Date.now() - lastUserActivityTime
|
||
// 如果页面处于静置状态(超过 5 分钟未打字或点击),执行一次平滑刷新保持全局新鲜
|
||
if (idleTime > 5 * 60 * 1000 && !isReloading) {
|
||
console.log(`${TAG} 页面静置已超周期,执行安全平滑重载以更新全量会话凭证`)
|
||
reportLog('info', '周期刷新', '后台页面静置超 25 分钟,自动平滑重载以保持 Token 绝对新鲜')
|
||
isReloading = true
|
||
window.location.reload()
|
||
}
|
||
}, AUTO_RELOAD_INTERVAL_MS)
|
||
|
||
// 8. 响应来自 Background 的保活 Ping
|
||
chrome.runtime.onMessage.addListener((req, _sender, sendResponse) => {
|
||
if (req.action === 'ping_keepalive') {
|
||
sendSilentSessionHeartbeat()
|
||
scanAndBustInactivityModal()
|
||
sendResponse({ status: 'alive', timestamp: Date.now() })
|
||
}
|
||
})
|
||
|
||
// 初始化首次扫描与心跳
|
||
setTimeout(() => {
|
||
sendSilentSessionHeartbeat()
|
||
scanAndBustInactivityModal()
|
||
reportLog('info', '页面接入', '拼多多商家后台已成功注入主动保活与防退出引擎')
|
||
}, 3000)
|
||
})()
|