12 KiB
12 KiB
批量自动化发车流程分析 v3.9.6
🎯 问题描述
现象:
- ✅ 游戏功能模块单独测试:成功且非常快
- ❌ 批量自动化测试:超时(20秒)
用户反馈:
"单独测试是成功的,而且查询的非常快"
这说明:
- ✅ 账号已加入俱乐部
- ✅ 服务器响应正常且快速
- ❌ 问题出在批量自动化的实现上
📋 批量自动化发车完整流程
阶段1:初始化(startBatchExecution)
代码位置:batchTaskStore.js 第187-232行
// 1. 重置状态
isExecuting.value = true
executionStats.value = { ... }
// 2. 初始化任务队列
const selectedTokenIds = tokenStore.selectedTokens.map(t => t.id)
const tasks = getTaskList() // ['sendCar']
阶段2:并发控制(executeBatchWithConcurrency)
代码位置:batchTaskStore.js 第240-323行
关键逻辑:
// 1. 错峰连接(v3.9.6: 每3秒一个)
const delayMs = connectionIndex * 3000
// 2. 完全串行执行(v3.9.6: maxConcurrency = 1)
while (executing.length < maxConcurrency.value && queue.length > 0) {
const tokenId = queue.shift()
// 延迟后执行
await new Promise(resolve => setTimeout(resolve, delayMs))
await executeTokenTasks(tokenId, tasks)
}
v3.9.6 配置:
- 并发数:1个(完全串行)
- 账号间隔:3秒
- 第1个账号:立即执行
- 第2个账号:3秒后执行
阶段3:建立连接(executeTokenTasks)
代码位置:batchTaskStore.js 第340-348行
// 1. 确保WebSocket连接(带重试机制)
const wsClient = await ensureConnection(tokenId, 5) // 重试5次
if (!wsClient) {
throw new Error('WebSocket连接失败')
}
// 2. 等待连接稳定(2秒)
console.log(`⏳ 等待连接稳定...`)
await new Promise(resolve => setTimeout(resolve, 2000))
ensureConnection 逻辑:
// 检查现有连接
const connection = tokenStore.wsConnections[tokenId]
if (connection && connection.status === 'connected') {
return connection.client // 复用现有连接
}
// 新建连接
const wsClient = await tokenStore.reconnectWebSocket(tokenId)
return wsClient
阶段4:执行发车任务(executeTask - sendCar)
代码位置:batchTaskStore.js 第1115-1359行
第1步:查询车辆
console.log(`🚗 [${tokenId}] 开始查询俱乐部车辆...`)
const queryResponse = await client.sendWithPromise('car_getrolecar', {}, 20000)
if (!queryResponse || !queryResponse.roleCar) {
throw new Error('查询车辆失败:未返回车辆数据')
}
const carDataMap = queryResponse.roleCar.carDataMap || {}
console.log(`✅ [${tokenId}] 查询到 ${carIds.length} 辆车`)
第2步:批量刷新(可选)
const refreshCount = carRefreshCount.value // 配置的刷新次数
if (refreshCount > 0) {
for (let round = 1; round <= refreshCount; round++) {
for (const carId of carIds) {
// 跳过有刷新票的车辆
if (carHasRefreshTicket(carInfo)) {
continue
}
// 刷新车辆
await client.sendWithPromise('car_refresh', { carId }, 5000)
await new Promise(resolve => setTimeout(resolve, 300)) // 间隔300ms
}
}
// 重新查询车辆状态
await new Promise(resolve => setTimeout(resolve, 500))
const reQueryResponse = await client.sendWithPromise('car_getrolecar', {}, 20000)
}
第3步:检查每日发车次数
const dailySendKey = getTodayKey(tokenId) // 'car_daily_send_count_2025-10-08_tokenId'
const dailySendCount = parseInt(localStorage.getItem(dailySendKey) || '0')
if (dailySendCount >= 4) {
return { success: true, message: '今日发车次数已达上限(4/4)' }
}
第4步:批量收获
for (const carId of carIds) {
const state = getCarState(carInfo) // 0=待发车, 1=运输中, 2=已到达
if (state === 2) { // 已到达
await client.sendWithPromise('car_claim', { carId }, 5000)
await new Promise(resolve => setTimeout(resolve, 300))
}
}
第5步:批量发送
// 重新查询车辆状态
await new Promise(resolve => setTimeout(resolve, 500))
const finalQueryResponse = await client.sendWithPromise('car_getrolecar', {}, 20000)
// 找到待发车的车辆
const readyToSendCars = carIds.filter(carId => getCarState(carDataMap[carId]) === 0)
const remainingSendCount = 4 - dailySendCount
const carsToSend = readyToSendCars.slice(0, remainingSendCount)
// 发送车辆
for (const carId of carsToSend) {
await client.sendWithPromise('car_send', { carId, helperId: 0, text: "" }, 5000)
// 更新发车次数
const newCount = dailySendCount + sendSuccessCount
localStorage.setItem(dailySendKey, newCount.toString())
await new Promise(resolve => setTimeout(resolve, 300))
}
🆚 批量自动化 vs 游戏功能模块对比
游戏功能模块(成功,快速)
代码位置:CarManagement.vue 第505行
// 直接调用
const response = await tokenStore.sendMessageAsync(
tokenId,
'car_getrolecar',
{},
10000 // 10秒超时
)
sendMessageAsync 内部:
// tokenStore.js 第1414-1430行
const sendMessageWithPromise = async (tokenId, cmd, params = {}, timeout = 1000) => {
// 1. 获取连接
const connection = wsConnections.value[tokenId]
if (!connection || connection.status !== 'connected') {
return Promise.reject(new Error(`WebSocket未连接 [${tokenId}]`))
}
// 2. 发送命令
const client = connection.client
return await client.sendWithPromise(cmd, params, timeout)
}
特点:
- ✅ 使用
wsConnections.value[tokenId](响应式) - ✅ 检查
connection.status === 'connected' - ✅ 超时10秒
- ✅ 直接发送,无延迟
批量自动化(失败,超时)
代码位置:batchTaskStore.js 第1152行
// 通过 ensureConnection 获取 client
const client = await ensureConnection(tokenId)
// 发送命令
const queryResponse = await client.sendWithPromise('car_getrolecar', {}, 20000)
ensureConnection 内部:
// batchTaskStore.js 第1375-1380行
const connection = tokenStore.wsConnections[tokenId] // ⚠️ 非响应式
if (connection && connection.status === 'connected') {
return connection.client
}
// 新建连接
const wsClient = await tokenStore.reconnectWebSocket(tokenId)
return wsClient
特点:
- ⚠️ 使用
tokenStore.wsConnections[tokenId](非响应式) - ✅ 检查
connection.status === 'connected' - ✅ 超时20秒
- ⚠️ 等待2秒才发送(可能导致连接状态变化)
🔍 关键差异分析
差异1:响应式 vs 非响应式
游戏功能模块:
wsConnections.value[tokenId] // 响应式,实时状态
批量自动化:
tokenStore.wsConnections[tokenId] // 非响应式,可能是旧状态
影响:
- 批量自动化可能获取到过期的连接对象
- 连接状态可能已经改变,但引用还是旧的
差异2:等待时间
游戏功能模块:
// 立即发送
await tokenStore.sendMessageAsync(...)
批量自动化:
// 等待2秒后发送
await ensureConnection(tokenId)
await new Promise(resolve => setTimeout(resolve, 2000)) // ⚠️ 等待
await executeTask(tokenId, taskName)
影响:
- 2秒内连接状态可能发生变化
- WebSocket client 可能被替换或失效
差异3:获取 client 的方式
游戏功能模块:
// 每次发送时重新获取
const connection = wsConnections.value[tokenId]
const client = connection.client
批量自动化:
// 一次性获取,后续复用
const client = await ensureConnection(tokenId)
// ... 2秒后
// ... 多次使用这个 client
await client.sendWithPromise('car_getrolecar', {}, 20000)
await client.sendWithPromise('car_refresh', { carId }, 5000)
await client.sendWithPromise('car_claim', { carId }, 5000)
影响:
- client 对象可能在获取后失效
- 特别是在等待期间,连接可能被重置
💡 问题根源推测
最可能原因:client 对象失效
流程:
ensureConnection获取client对象(时间点 T0)- 等待连接稳定 2秒(T0 → T2)
- 发送
car_getrolecar命令(时间点 T2) - ❌ 但此时 client 对象可能已经失效
为什么 client 会失效?
可能的情况:
-
连接在等待期间被替换
tokenStore.reconnectWebSocket可能创建了新的 client- 旧的 client 引用失效
-
Promise 管理器状态不一致
- WebSocket client 内部有 Promise 管理器
- 如果连接重置,Promise 管理器也会重置
- 旧的 Promise 永远不会被 resolve
-
事件监听器失效
- WebSocket client 依赖
onmessage事件 - 连接重置后,旧的事件监听器失效
- 新消息不会触发旧 client 的 Promise resolve
- WebSocket client 依赖
🛠️ 解决方案
方案1:直接使用 tokenStore.sendMessageAsync ⭐ 推荐
修改:batchTaskStore.js 第1152行及后续所有 client.sendWithPromise 调用
// 从
const client = await ensureConnection(tokenId)
const queryResponse = await client.sendWithPromise('car_getrolecar', {}, 20000)
// 改为
await ensureConnection(tokenId) // 只确保连接,不获取 client
const queryResponse = await tokenStore.sendMessageAsync(
tokenId,
'car_getrolecar',
{},
20000
)
优点:
- ✅ 每次发送时都获取最新的 client
- ✅ 使用响应式连接对象
- ✅ 与游戏功能模块保持一致
- ✅ 简单可靠
缺点:
- 需要修改所有
client.sendWithPromise调用
方案2:每次获取最新的 client
修改:在每次发送命令前重新获取 client
// 获取最新的 client
const getLatestClient = (tokenId) => {
const connection = tokenStore.wsConnections[tokenId]
if (!connection || connection.status !== 'connected') {
throw new Error('WebSocket未连接')
}
return connection.client
}
// 使用
const client = getLatestClient(tokenId)
const queryResponse = await client.sendWithPromise('car_getrolecar', {}, 20000)
优点:
- ✅ 总是使用最新的 client
- ✅ 修改量相对较小
缺点:
- ⚠️ 仍需修改多处
- ⚠️ 不如方案1简洁
方案3:移除等待稳定时间
修改:batchTaskStore.js 第346-348行
// 删除或减少等待时间
// await new Promise(resolve => setTimeout(resolve, 2000))
优点:
- ✅ 减少 client 失效的时间窗口
- ✅ 提升执行速度
缺点:
- ❌ 可能导致连接不稳定
- ❌ 不能根本解决问题
📊 推荐实施方案
⭐ 采用方案1:统一使用 tokenStore.sendMessageAsync
修改列表:
car_getrolecar(第1152行) - 初次查询car_refresh(第1200行) - 刷新车辆car_getrolecar(第1227行) - 刷新后重新查询car_claim(第1268行) - 收获车辆car_getrolecar(第1298行) - 发送前最后查询car_send(第1311行) - 发送车辆
修改模板:
// 从
const client = await ensureConnection(tokenId)
await client.sendWithPromise('COMMAND', params, timeout)
// 改为
await ensureConnection(tokenId)
await tokenStore.sendMessageAsync(tokenId, 'COMMAND', params, timeout)
🎯 预期效果
修改后:
- ✅ 批量自动化与游戏功能模块使用相同的发送机制
- ✅ 每次发送都获取最新的 client 对象
- ✅ 使用响应式连接状态
- ✅ 避免 client 失效问题
- ✅ 批量自动化应该也能快速成功
📝 后续测试步骤
- 应用修改
- 重启开发服务器
- 批量测试2个账号(第2个账号是待发车状态)
- 观察是否快速成功
期望结果:
✅ [token_xxx] 开始查询俱乐部车辆...
✅ [token_xxx] 查询到 4 辆车 ← 应该1-2秒内完成
🔄 总结
问题本质:
- 批量自动化一次性获取 client 对象后长时间复用
- 在等待和执行期间,client 对象可能失效
- 导致后续命令的 Promise 永远不会被 resolve
解决思路:
- 不复用 client 对象
- 每次发送命令时获取最新的 client
- 使用响应式连接状态
关键改进:
- 统一使用
tokenStore.sendMessageAsync - 与游戏功能模块保持一致
- 确保每次都使用最新、有效的 client