2.9.1最终稳定版

This commit is contained in:
2025-11-10 14:36:15 +08:00
parent fad6cdf227
commit 7b13a6784e
6 changed files with 486 additions and 151 deletions

View File

@@ -8,11 +8,13 @@
<div class="center-line"> <!-- 左边栏按钮 -->
<div v-for="(btn, index) in buttons" :key="index" style="width: 100%;">
<div v-if="btn.show?.()" class="left-button" :style="btn.style ? btn.style() : {}" @click="btn.onClick"
@mouseenter="hoverIndex = index" @mouseleave="hoverIndex = null">
<div v-if="btn.show?.()" class="left-button" :style="btn.style ? btn.style() : {}"
:title="btn.tooltip ? btn.tooltip() : ''" @click="btn.onClick" @mouseenter="hoverIndex = index"
@mouseleave="hoverIndex = null">
<img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt="">
{{ btn.label }}
</div>
</div>
<div style="position: absolute;left: 20px; bottom: 20px;">
<el-button @click="showHostDlg = true">执行主播库</el-button>
@@ -42,7 +44,9 @@
<div class="app-button" @click="restartTikTok({ udid: device.deviceId })">重置tiktok</div>
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
<div class="app-button" @click="stopOne(device.deviceId)">停止任务</div>
<div class="app-button" @click="runTask(runType, device.deviceId)">开启</div>
<div class="app-button" @click="scheduleEnabled = true; runTask(runType, device.deviceId);">
开启
</div>
</div>
</div>
@@ -55,11 +59,11 @@
</div>
<img v-if="isWifi" style="position: absolute; right: 20px; top: 10px; height: 30px;" src="@/assets/wifi.png"></img>
<MultiLineInputDialog v-model:visible="showDialog" :initialText='initialTextStr' :title="dialogTitle"
:index="selectedDevice" @confirm="onDialogConfirm" @cancel="stopAll(100)" />
:index="selectedDevice" @confirm="onDialogConfirm" @cancel="stopAll(2000)" />
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" @invitType="invitTypeFun" />
<TranslationDialog v-model="showtransDlg" :type="transDlgType" :translateFn="doTranslate"
storage-key-prefix="demo-translation" @confirm="onConfirm" @cancel="stopAll(100)" />
storage-key-prefix="demo-translation" @confirm="onConfirm" @cancel="stopAll(2000)" />
</div>
<!-- <AgentGuildDialog v-model="showMyInfo" :model="formInit" @save="handleSave" /> -->
<AgentGuildDialog v-model="showMyInfo" :model="{
@@ -135,13 +139,13 @@ import { chat, translationToChinese, translation, customTranslation } from "@/ap
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
import AgentGuildDialog from '@/components/AgentGuildDialog.vue'
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue'; // 根据实际路径修改
import TranslationDialog from '@/components/translationDialog.vue'; // 根据实际路径修改
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue';
import TranslationDialog from '@/components/translationDialog.vue';
import ChatDialog from '@/components/ChatDialog.vue'
import MessageDialogd from '@/components/MessageDialogd.vue'
import { pickTikTokBundleId } from '@/utils/arrUtils'
import { logout, updates } from '@/api/account';
import { logout, updates, health } from '@/api/account';
import {
getDeviceList,
toHome,
@@ -191,7 +195,7 @@ const borkerConfig = reactive({
let common = ref(true);
// 自动化
let auto = ref(true);
let isTranslate = ref(false)
let initialTextStr = ref('') // 初始文本字符串
// 批次缓冲(仅用于当前“波”)
let batch = []; // [{ country, text }]
@@ -199,8 +203,9 @@ let flushTimer = null;
let hostList = [] // 主播列表
let comonList = [] //评论列表
//查询列表轮询
//查询 列表 新消息轮询
let getListtimer = null;
let getNetworkListtimer = null;
let userdata = getUser();
let chatList = ref([])
@@ -223,6 +228,10 @@ let deviceInformation = ref([])
//停止中
let stopLoading = null
// 是否未开通 AI 自动回复(= 监测消息不可用)
const isListenLockedByPlan = computed(() => String(userdata?.aiReplay ?? '0') === '0')
// 每台设备的网络状态true=正常false=异常
const netStatus = reactive({}) // { [deviceId]: boolean }
@@ -240,6 +249,11 @@ function clearResumeTimer(id) {
}
}
// —— 在线判定/过滤 ——
// 红框=netStatus[id] === false 视为离线其它true/undefined都当作可用
const isOnline = (id) => netStatus[id] !== false
const onlineOnly = (ids) => ids.filter(isOnline)
// 当前是否被其它模式占用(四个互斥按钮专用)
const isLocked = (type) => !!runType.value && runType.value !== type
@@ -251,12 +265,21 @@ watch(sseEnabled, v => {
})
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
const ctrlStyle = (type) => ({
backgroundColor: runType.value === type ? 'red' : '',
opacity: isLocked(type) ? 0.5 : 1,
pointerEvents: isLocked(type) ? 'none' : 'auto',
cursor: isLocked(type) ? 'not-allowed' : 'pointer',
})
const ctrlStyle = (type) => {
const lockedByMode = isLocked(type)
const disableListen = (type === 'listen') && isListenLockedByPlan.value
const style = {
backgroundColor: runType.value === type ? 'red' : '',
opacity: (lockedByMode || disableListen) ? 0.5 : 1,
// 允许事件,以便显示浏览器原生 title 提示 & 点击弹出 ElMessage
pointerEvents: lockedByMode ? 'none' : 'auto',
cursor: (lockedByMode || disableListen) ? 'not-allowed' : 'pointer',
filter: disableListen ? 'grayscale(1)' : ''
}
return style
}
const buttons = [
{
@@ -358,33 +381,31 @@ const buttons = [
},
{
label: '监测消息',
// 点击前先判断是否未开通,未开通只提示不执行
onClick: () => {
if (isListenLockedByPlan.value) {
ElMessage.warning('未开通 AI 自动回复功能,请联系管理员开通');
return
}
if (runType.value == 'listen') {
deviceInformation.value.forEach((item) => {
stopScript({ udid: item.deviceId })
})
runType.value = ''
return
};
}
if (isLocked('listen')) return
//如果传评论就注释一下两行代码,解开后面代码
runType.value = 'listen'
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
// dialogTitle.value = '评论(无消息将刷视频)';
// setTimeout(() => {
// showDialog.value = true;
// initialTextStr.value = getContentListMultiline();
// }, 500)
},
// 悬停提示文案
tooltip: () => (isListenLockedByPlan.value ? '未开通AI 自动回复' : ''),
show: () => true,
img: {
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
},
style: () => ctrlStyle('listen')
},
// {
// label: '定时调度',
@@ -397,7 +418,7 @@ const buttons = [
// },
{
label: '全部停止',
onClick: () => stopAll(100),
onClick: () => stopAll(2000),
show: () => true,
img: {
normal: new URL('@/assets/video/leftBtn8.png', import.meta.url).href,
@@ -521,6 +542,7 @@ const loops = new Map(); // deviceId -> { timer, stopped, lastUrl }
/** 生成一次地址 */
const makeUrl = (port) => `http://localhost:${port}/?t=${Date.now()}`;
// const makeUrl = (port) => `http://192.168.1.209:${port}/?t=${Date.now()}`;
/** 真正断开某台设备当前连接并清理 */
function hardCloseImg(deviceId) {
@@ -880,31 +902,59 @@ async function stopAll(time) {
background: 'rgba(0, 0, 0, 0.7)',
});
// 🔒 先强制关闭所有提示弹窗
ElMessageBox.close();
scheduleEnabled.value = false;
runType.value = '';
isMsgPop.value = false;
dropCurrentWave();
// ========== ⏰ 新增超时逻辑 ==========
const STOP_TASK_TIMEOUT_MS = 120000; // 120 秒超时,可调整
// 封装一个带超时保护的 Promise
const withTimeout = (p, ms) => {
return Promise.race([
p,
new Promise((resolve) =>
setTimeout(() => {
console.warn(`[stopAllTask] 超时 ${ms}ms`);
resolve('timeout');
}, ms)
),
]);
};
try {
// 1) 等待接口完成
await stopAllTask(deviceInformation.value.map(item => item.deviceId));
// 2) 等待 2 秒(和你原逻辑一致)
// 1️⃣ 带超时保护地调用接口
const res = await withTimeout(
stopAllTask(deviceInformation.value.map(item => item.deviceId)),
STOP_TASK_TIMEOUT_MS
);
if (res === 'timeout') {
console.warn('stopAllTask 请求超时,自动继续后续逻辑');
}
// 2⃣ 等待指定时间(原逻辑保留)
await new Promise(r => setTimeout(r, time));
stopLoading.close();
console.log('全部停止成功', printCurrentTime());
ElMessage.success('全部停止成功');
// 3) 明确返回(可选)
// 3️⃣ 无论接口成功或超时都返回 true
return true;
} catch (e) {
console.log('停止失败', printCurrentTime(), e);
console.error('停止失败', printCurrentTime(), e);
ElMessage.error('脚本已停止');
stopLoading.close();
return false;
}
}
//确认多行文本框内容
function onDialogConfirm(result, type, index, data) {
console.log(type, result, data);
@@ -967,37 +1017,54 @@ onMounted(async () => {
ElMessage.error(`未检测到设备`)
}
//MQ链接
window.electronAPI.startMq(userdata.tenantId, userdata.id)
// 初始化时获取设备列表
//每3秒获取一次设备列表 消息列表 和 查询主播列表是否还有主播
getListtimer = setInterval(async () => {
getDeviceListFun() //获取设备列表
selectLastFun() //获取手机网络状态
const hostsList = await getStoredHostList()
selectLastFun() //获取新消息
const hostsList = await getStoredHostList() //获取主播列表
// console.log(hostsList.length)
//当私信主播时,主播列表没有数据了,提示列表空了 并且关闭私信
if (runType.value == 'follow') {
if (hostsList.length <= 0) {
await stopAll(5000)
await stopAll(2000)
runType.value = 'like'
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId, comment: getContentList(), isComment: common.value }))
ElMessageBox.alert('私信全部完成!(刷视频中)', '提示', {
confirmButtonText: 'OK',
callback: (action) => {
},
})
}
}
}, 3000)
setInterval(async () => {
getNetworkListtimer = setInterval(async () => {
await checkVPN()
await refreshNetStatus()
}, 1000 * 20)
health().then((res) => {
}).catch((err) => {
if (err.code === 40400) {
//关闭获取设备列表的方法
clearInterval(getListtimer)
getListtimer = null
//关闭获取网络状态的方法
clearInterval(getNetworkListtimer)
getNetworkListtimer = null
}
})
}, 1000 * 60 * 3)
if (!await isAiConfig()) {
@@ -1063,8 +1130,13 @@ onMounted(async () => {
})
onUnmounted(() => {
//关闭获取设备列表的方法
clearInterval(getListtimer)
getListtimer = null
//关闭获取网络状态的方法
clearInterval(getNetworkListtimer)
getNetworkListtimer = null
})
let isStartLac = false
@@ -1081,7 +1153,7 @@ const getDeviceListFun = () => {
}).catch((err) => {
if (isStartLac) {
ElMessage.error(`IOSAI服务错误`)
isStartLac = true
// isStartLac = true
} else {
}
@@ -1153,15 +1225,21 @@ async function uploadLogFile() {
}
}
function runTask(key, deviceId, type) {
// —— 若指定单设备,且该设备当前离线,则直接跳过 ——
if (deviceId && !isOnline(deviceId)) {
markPendingForOffline([deviceId]); // ⭐ 新增:让它后续来网能自动跟上
ElMessage.warning('该设备当前网络不可用,已跳过');
return;
}
console.log('[schedule] 切换到任务:', key, printCurrentTime())
forceActivate(key, async () => {
if (key === 'follow') {
console.log("进入follow", scheduleEnabled.value)
if (scheduleEnabled.value) {
if (!deviceId) {
await stopAll(5000)
} else {
//如果有id 就只执行一个 并且return出去
if (deviceId) {
if (isAlliance.value) {
followAndGreetUnion(
{
@@ -1169,9 +1247,11 @@ function runTask(key, deviceId, type) {
anchorList: [],
prologueList: getContentpriList(),
needReply: auto.value,
needTranslate: isTranslate.value,
}
).then((res) => {
hostList = []
return
})
} else {
passAnchorData(
@@ -1181,75 +1261,103 @@ function runTask(key, deviceId, type) {
prologueList: getContentpriList(),
comment: comonList,
needReply: auto.value,
needTranslate: isTranslate.value,
isComment: common.value //是否评论
}
).then((res) => {
hostList = []
return
})
}
return
} else {
//没有id 直接停止所有设备
await stopAll(2000)
//第一个小时结束后,第二轮开始的时候,直接进入follow
setTimeout(() => {
runType.value = 'follow'
//过滤无网络设备
const allIds = deviceInformation.value.map(item => item.deviceId)
// 在线要启动的
const deviceIds = onlineOnly(allIds)
if (!deviceIds.length) {
ElMessage.warning('没有在线设备可执行任务');
// 也别忘了把离线的标成待恢复
markPendingForOffline(allIds.filter(id => netStatus[id] === false))
return
}
// ⭐ 给这次被跳过的离线设备打标记(后续来网自动补上)
markPendingForOffline(allIds.filter(id => !deviceIds.includes(id)))
if (isAlliance.value) {
followAndGreetUnion(
{
deviceList: deviceIds,
anchorList: [],
prologueList: getContentpriList(),
needReply: auto.value,
needTranslate: isTranslate.value,
}
).then((res) => {
hostList = []
return
})
} else {
passAnchorData(
{
deviceList: deviceIds,
anchorList: [],
prologueList: getContentpriList(),
comment: comonList,
needReply: auto.value,
needTranslate: isTranslate.value,
isComment: common.value //是否评论
}
).then((res) => {
hostList = []
return
})
}
}, 1000)
return
}
//第一个小时结束后,第二轮开始的时候,直接进入follow
setTimeout(() => {
runType.value = 'follow'
if (isAlliance.value) {
followAndGreetUnion(
{
deviceList: deviceInformation.value.map(item => item.deviceId),
anchorList: [],
prologueList: getContentpriList(),
needReply: auto.value
}
).then((res) => {
hostList = []
})
} else {
passAnchorData(
{
deviceList: deviceInformation.value.map(item => item.deviceId),
anchorList: [],
prologueList: getContentpriList(),
comment: comonList,
needReply: auto.value,
isComment: common.value //是否评论
}
).then((res) => {
hostList = []
})
}
}, 1000)
return
} else {
// 如果是暂停状态,则打开弹窗进行开启第一轮任务
scheduleEnabled.value = true
initialTextStr.value = '';
dialogTitle.value = '主播ID';
showDialog.value = true;
}
scheduleEnabled.value = true
initialTextStr.value = '';
dialogTitle.value = '主播ID';
showDialog.value = true;
} else if (key === 'like') {
if (!deviceId) {
await stopAll(5000)
await stopAll(2000)
} else {
growAccount({ udid: deviceId })
growAccount({ udid: deviceId, comment: getContentList(), isComment: common.value })
return
}
setTimeout(() => {
scheduleEnabled.value = true
runType.value = 'like'
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId, comment: getContentList(), isComment: common.value }))
}, 1000)
} else if (key === 'brushLive') {
if (!deviceId) {
await stopAll(5000)
await stopAll(2000)
} else {
watchLiveForGrowth({ udid: deviceId })
return
@@ -1263,7 +1371,7 @@ function runTask(key, deviceId, type) {
runType.value = 'brushLive'
} else if (key === 'listen') {
if (!deviceId) {
await stopAll(5000)
await stopAll(2000)
} else {
monitorMessages({ udid: deviceId })
return
@@ -1282,7 +1390,7 @@ function runTask(key, deviceId, type) {
async function stopCurrentMode() {
// 如果你希望“只停当前片段的设备”,也可以用 stopScript 针对设备循环
await stopAll(100)
await stopAll(2000)
}
/** 恢复:回到 scheduleState.index 对应片段,并让 startTime 回到“暂停前进度” */
@@ -1296,6 +1404,7 @@ function resumeAfterInterrupt() {
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
// 确保任务是该片段
scheduleEnabled.value = true
runTask(schedulePlan[scheduleState.index].key)
pauseSnapshot = null
}
@@ -1383,7 +1492,7 @@ function startScheduleLoop() {
pauseSnapshot = { index: scheduleState.index, elapsedBeforePause }
// 停掉当前片段
await stopAll(5000)
await stopAll(2000)
// 执行中断任务(带重试)
@@ -1421,6 +1530,7 @@ function startScheduleLoop() {
scheduleState.index = (scheduleState.index + 1) % schedulePlan.length
scheduleState.startTime = now
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
scheduleEnabled.value = true
runTask(schedulePlan[scheduleState.index].key)
}
}, scheduleTickMs)
@@ -1519,18 +1629,9 @@ async function doLogout() {
}
}
function stopOne(deviceId) {
stopLoading = ElLoading.service({
lock: true,
text: '停止中',
background: 'rgba(0, 0, 0, 0.7)',
});
stopScript({ udid: deviceId }).then((res) => {
stopLoading.close()
}).catch((err) => {
stopLoading.close()
}).finally((err) => {
stopLoading.close()
stopScript({ udid: deviceId }).then((res) => {
// ElMessage.success('停止成功');
})
}
@@ -1599,17 +1700,31 @@ async function doTranslate(sentences, targetLang) {
}
// 接收“确定”事件返回结果
function onConfirm({ type, strings, autoBlo }) {
console.log('✅ 确认返回:', type, strings, autoBlo)
function onConfirm({ type, strings, autoBlo, needTranslate }) {
console.log('✅ 确认返回:', type, strings, autoBlo, needTranslate)
auto.value = autoBlo
isTranslate.value = needTranslate
showtransDlg.value = false
runType.value = 'follow'
setContentpriList(strings)
//过滤无网络设备
const allIds = deviceInformation.value.map(item => item.deviceId)
// 在线要启动的
const deviceIds = onlineOnly(allIds)
if (!deviceIds.length) {
ElMessage.warning('没有在线设备可执行任务');
// 也别忘了把离线的标成待恢复
markPendingForOffline(allIds.filter(id => netStatus[id] === false))
return
}
// ⭐ 给这次被跳过的离线设备打标记(后续来网自动补上)
markPendingForOffline(allIds.filter(id => !deviceIds.includes(id)))
if (isAlliance.value) {
followAndGreetUnion(
{
deviceList: deviceInformation.value.map(item => item.deviceId),
deviceList: deviceIds,
anchorList: hostList.map(item => ({
anchorId: item.id,
country: item.country,
@@ -1618,7 +1733,7 @@ function onConfirm({ type, strings, autoBlo }) {
})),
prologueList: strings,
needReply: autoBlo,
// needTranslate: data.needTranslate,
needTranslate: needTranslate,
}
).then((res) => {
ElMessage({ type: 'success', message: '任务开启成功' });
@@ -1628,7 +1743,7 @@ function onConfirm({ type, strings, autoBlo }) {
} else {
passAnchorData(
{
deviceList: deviceInformation.value.map(item => item.deviceId),
deviceList: deviceIds,
anchorList: hostList.map(item => ({
anchorId: item.id,
country: item.country,
@@ -1638,7 +1753,7 @@ function onConfirm({ type, strings, autoBlo }) {
prologueList: strings, //私信对象
comment: comonList, //评论列表
needReply: autoBlo, //自动回复
// needTranslate: data.needTranslate,
needTranslate: needTranslate, //翻译
isComment: common.value //是否评论
}
).then((res) => {
@@ -1656,23 +1771,50 @@ function arrayToString(arr) {
}
// —— 每 20s 刷新网络状态 ——
// 写一个小函数专门刷新网络状态,方便复用
// 每台设备请求间隔(毫秒)
const REQ_GAP_MS = 5000
async function refreshNetStatus() {
const list = [...(deviceInformation.value || [])]
if (!list.length) return
// 并发请求每台设备网络状态(安全处理错误)
const settled = await Promise.allSettled(
list.map(d => getDeviceNetStatus({ udid: d.deviceId }))
const firstPass = {}
// ✅ 串行请求:每台设备间隔 5 秒
for (let i = 0; i < list.length; i++) {
const d = list[i]
try {
const res = await getDeviceNetStatus({ udid: d.deviceId })
firstPass[d.deviceId] = (res === true)
} catch (err) {
firstPass[d.deviceId] = false
}
// 不是最后一台才等待 5s
if (i < list.length - 1) {
await new Promise(r => setTimeout(r, REQ_GAP_MS))
}
}
// 二次复核
const falseIds = Object.keys(firstPass).filter(id => firstPass[id] === false)
const confirmResults = await Promise.all(
falseIds.map(async id => {
const stableOffline = await confirmStableOffline(id)
return { id, stableOffline }
})
)
settled.forEach((s, i) => {
const id = list[i].deviceId
netStatus[id] = (s.status === 'fulfilled' && s.value === true)
const effective = { ...firstPass }
confirmResults.forEach(({ id, stableOffline }) => {
effective[id] = !stableOffline
})
console.log("设备在线状态", settled)
// 清理已下线设备的状态,避免残留
// 写回 UI
Object.keys(effective).forEach(id => { netStatus[id] = effective[id] })
console.log('设备在线状态(有效值)', effective)
// 清理已下线设备
const aliveIds = new Set(list.map(d => d.deviceId))
Object.keys(netStatus).forEach(id => {
if (!aliveIds.has(id)) {
@@ -1683,54 +1825,91 @@ async function refreshNetStatus() {
}
})
// —— 在这里做“网络→任务联动”(只在有运行模式时触发)——
// 任务联动(保持原逻辑)
if (runType.value) {
list.forEach(({ deviceId: id }) => {
const prev = lastNet[id] // 之前的网络状态
const curr = netStatus[id] // 当前的网络状态true/false/undefined)
const prev = lastNet[id]
const curr = netStatus[id]
if (typeof curr === 'undefined') return
// 边沿触发true -> false 断网false -> true 恢复
if (prev !== curr) {
// 断网:立刻停止该设备任务(只停这一台)
if (curr === false) {
clearResumeTimer(id) // 避免有遗留恢复定时器
clearResumeTimer(id)
if (!offlineFlags[id]) {
console.log(`[NET] ${id} 离线,停止该设备任务`)
console.log(`[NET] ${id} 稳定离线,停止该设备任务`)
offlineFlags[id] = true
// 这里调用你已有的“只停一台”的方法
stopOne(id)
}
}
// 恢复:等待网络稳定后再恢复该设备任务
if (curr === true && offlineFlags[id]) {
clearResumeTimer(id)
const timer = setTimeout(() => {
// 二次确认:还在运行模式里、该设备仍在线
const t = setTimeout(() => {
if (runType.value && netStatus[id] === true) {
console.log(`[NET] ${id} 恢复在线,重新启动当前模式: ${runType.value}`)
// 仅启动这一台
scheduleEnabled.value = true
runTask(runType.value, id)
// 标记已恢复
offlineFlags[id] = false
}
resumeTimers.delete(id)
}, NET_RESUME_STABLE_MS)
resumeTimers.set(id, timer)
resumeTimers.set(id, t)
}
}
// 更新 last
lastNet[id] = curr
})
} else {
// 不在运行模式时,清理“暂停标记”和恢复定时器,保持干净
Object.keys(offlineFlags).forEach(id => { offlineFlags[id] = false })
Array.from(resumeTimers.keys()).forEach(id => clearResumeTimer(id))
}
}
// ===== 新增:二次复核断网所需的小工具 =====
const verifyingOffline = {} // { [deviceId]: Promise<boolean> } 去重并发
const RETRY_COUNT = 2 // 额外再查几次
const RETRY_GAP_MS = 1000 // 每次间隔(可调)
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
async function safeGetStatus(udid) {
try {
return await getDeviceNetStatus({ udid }) === true
} catch {
return false
}
}
/** 返回 true=稳定离线false=误报/抖动(有一次为 true 就当没离线) */
async function confirmStableOffline(udid) {
if (verifyingOffline[udid]) return verifyingOffline[udid]
verifyingOffline[udid] = (async () => {
for (let i = 0; i < RETRY_COUNT; i++) {
await sleep(RETRY_GAP_MS)
const ok = await safeGetStatus(udid) // true=在线
if (ok) return false // 有一次在线 => 抖动
}
return true // 全部都是 false => 稳定离线
})().finally(() => { delete verifyingOffline[udid] })
return verifyingOffline[udid]
}
//小工具:标记离线待恢复
const markPendingForOffline = (ids) => {
ids.forEach(id => {
if (netStatus[id] === false) {
offlineFlags[id] = true; // 标记成“暂停/待恢复”
// 可选:初始化上次状态,便于日志判断
if (typeof lastNet[id] === 'undefined') lastNet[id] = false;
}
});
};
</script>
<style scoped lang="less">