2025-09-08 20:53:24 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="main">
|
|
|
|
|
|
<el-scrollbar class="left"> <!-- 左边栏 -->
|
2025-09-11 16:46:12 +08:00
|
|
|
|
<div style="position: absolute;left: 20px; top: 20px;">
|
2025-10-28 19:41:05 +08:00
|
|
|
|
<el-button style="background: linear-gradient(90deg, #60a5fa, #34d399); color: azure; "
|
2025-09-11 16:46:12 +08:00
|
|
|
|
@click="showMyInfo = true">人设编辑</el-button>
|
|
|
|
|
|
</div>
|
2025-09-20 13:31:06 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
<div class="center-line"> <!-- 左边栏按钮 -->
|
|
|
|
|
|
<div v-for="(btn, index) in buttons" :key="index" style="width: 100%;">
|
2025-11-10 14:36:15 +08:00
|
|
|
|
<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">
|
2025-09-08 20:53:24 +08:00
|
|
|
|
<img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt="">
|
|
|
|
|
|
{{ btn.label }}
|
|
|
|
|
|
</div>
|
2025-11-10 14:36:15 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style="position: absolute;left: 20px; bottom: 20px;">
|
|
|
|
|
|
<el-button @click="showHostDlg = true">执行主播库</el-button>
|
|
|
|
|
|
<el-button type="info" @click="uploadLogFile">上传日志</el-button>
|
2025-11-18 18:05:29 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<!-- 监听 q.tenant.*(爬虫) -->
|
|
|
|
|
|
<el-switch v-model="sseCrawlerEnabled" inline-prompt active-text="监听爬虫" inactive-text="监听爬虫"
|
|
|
|
|
|
style="margin-left: 8px;" @change="onToggleCrawler" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 监听 b.tenant.*(大哥) -->
|
|
|
|
|
|
<el-switch v-model="sseBossEnabled" inline-prompt active-text="监听大哥" inactive-text="监听大哥"
|
|
|
|
|
|
style="margin-left: 8px;" @change="onToggleBoss" />
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
2025-09-08 20:53:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-scrollbar>
|
|
|
|
|
|
<!-- 中间手机区域 -->
|
|
|
|
|
|
<div class="content" @click.self="selectedDevice = 999">
|
2025-09-20 13:31:06 +08:00
|
|
|
|
<div class="video-container" v-for="(device, index) in deviceInformation" :key="device.deviceId">
|
2025-10-31 19:39:57 +08:00
|
|
|
|
<div class="video-canvas" :class="{
|
|
|
|
|
|
active: selectedDevice === index,
|
|
|
|
|
|
'net-bad': netStatus[device.deviceId] === false
|
|
|
|
|
|
}" :style="getCanvasStyle(index)" @click="selectDevice(index)">
|
2025-10-10 15:53:19 +08:00
|
|
|
|
<img class="stream" :src="imgSrcMap[device.deviceId] || ''" :data-id="device.deviceId"
|
2025-09-20 13:31:06 +08:00
|
|
|
|
:ref="el => (imgRefs[device.deviceId] = el)" />
|
|
|
|
|
|
|
2025-09-08 23:12:00 +08:00
|
|
|
|
<canvas v-show="selectedDevice === index" class="overlay"
|
2025-09-08 20:53:24 +08:00
|
|
|
|
@mousedown.stop="(e) => onCanvasDown(device.deviceId, e, index)"
|
|
|
|
|
|
@mouseup.stop="(e) => onCanvasUp(device.deviceId, e, index)"
|
2025-09-08 23:12:00 +08:00
|
|
|
|
@mousemove.stop="(e) => onCanvasMove(device.deviceId, e, index)" />
|
2025-09-08 20:53:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="input-info" v-show="selectedDevice == index">
|
2025-10-28 19:41:05 +08:00
|
|
|
|
<div class="app-button" @click="restartTikTok({ udid: device.deviceId })">重置tiktok</div>
|
2025-09-08 20:53:24 +08:00
|
|
|
|
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
|
2025-10-10 15:53:19 +08:00
|
|
|
|
<div class="app-button" @click="stopOne(device.deviceId)">停止任务</div>
|
2025-11-10 14:36:15 +08:00
|
|
|
|
<div class="app-button" @click="scheduleEnabled = true; runTask(runType, device.deviceId);">
|
|
|
|
|
|
开启
|
|
|
|
|
|
</div>
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-11 16:46:12 +08:00
|
|
|
|
<div class="right center-line" @click.self="selectedDevice = 999">
|
2025-09-08 20:53:24 +08:00
|
|
|
|
<!-- <div style="margin: 30px;"></div> -->
|
2025-10-28 19:41:05 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
<ChatDialog :visible="openShowChat" :messages="chatList" />
|
2025-09-20 13:31:06 +08:00
|
|
|
|
<MessageDialogd :visible="openShowChat" :messages="MesNewList" :sound-src="ding" />
|
2025-09-08 20:53:24 +08:00
|
|
|
|
</div>
|
2025-10-28 19:41:05 +08:00
|
|
|
|
<img v-if="isWifi" style="position: absolute; right: 20px; top: 10px; height: 30px;" src="@/assets/wifi.png"></img>
|
2025-10-10 15:53:19 +08:00
|
|
|
|
<MultiLineInputDialog v-model:visible="showDialog" :initialText='initialTextStr' :title="dialogTitle"
|
2025-11-11 15:13:03 +08:00
|
|
|
|
:index="selectedDevice" @confirm="onDialogConfirm" @cancel="stopAll(2000, 'click')" />
|
2025-09-20 13:31:06 +08:00
|
|
|
|
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" @invitType="invitTypeFun" />
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
<TranslationDialog v-model="showtransDlg" :type="transDlgType" :translateFn="doTranslate"
|
2025-11-11 15:13:03 +08:00
|
|
|
|
storage-key-prefix="demo-translation" @confirm="onConfirm" @cancel="stopAll(2000, 'click')" />
|
2025-09-11 16:46:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<!-- <AgentGuildDialog v-model="showMyInfo" :model="formInit" @save="handleSave" /> -->
|
|
|
|
|
|
<AgentGuildDialog v-model="showMyInfo" :model="{
|
|
|
|
|
|
agentName: borkerConfig.agentName,
|
|
|
|
|
|
guildName: borkerConfig.guildName,
|
|
|
|
|
|
contactTool: borkerConfig.contactTool,
|
|
|
|
|
|
contact: borkerConfig.contact
|
|
|
|
|
|
}" @save="onSave" />
|
2025-09-08 20:53:24 +08:00
|
|
|
|
<!-- 定时调度配置弹窗 -->
|
2025-10-10 15:53:19 +08:00
|
|
|
|
<el-dialog v-model="showScheduleDlg" title="定时调度(每小时)" width="550px" :close-on-click-modal="false" draggable>
|
2025-09-08 20:53:24 +08:00
|
|
|
|
<div style="display:grid;grid-template-columns: 100px 1fr; gap:12px; align-items:center;">
|
|
|
|
|
|
<div>片段 A</div>
|
|
|
|
|
|
<div style="display:flex; gap:8px; align-items:center;">
|
|
|
|
|
|
<el-select v-model="schedAKey" style="width:140px;">
|
2025-10-10 15:53:19 +08:00
|
|
|
|
<el-option label="一键私信" value="follow" />
|
2025-09-08 20:53:24 +08:00
|
|
|
|
<el-option label="刷视频(养号)" value="like" />
|
|
|
|
|
|
<el-option label="刷直播" value="brushLive" />
|
|
|
|
|
|
<el-option label="监测消息" value="listen" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-input-number v-model="schedAMin" :min="1" :max="59" />
|
|
|
|
|
|
<span>分钟</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>片段 B</div>
|
|
|
|
|
|
<div style="display:flex; gap:8px; align-items:center;">
|
|
|
|
|
|
<el-select v-model="schedBKey" style="width:140px;">
|
|
|
|
|
|
<!-- <el-option label="一键关注" value="follow" /> -->
|
|
|
|
|
|
<el-option label="刷视频(养号)" value="like" />
|
|
|
|
|
|
<el-option label="刷直播" value="brushLive" />
|
|
|
|
|
|
<el-option label="监测消息" value="listen" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-input-number v-model="schedBMin" :min="1" :max="59" />
|
|
|
|
|
|
<span>分钟</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>总时长</div>
|
|
|
|
|
|
<div><b>{{ schedAMin + schedBMin }}</b> 分钟(必须等于 60)</div>
|
|
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
<div>换号</div>
|
2025-10-10 15:53:19 +08:00
|
|
|
|
<div style="display:flex; gap:8px; align-items:center;">
|
|
|
|
|
|
<el-switch v-model="interruptEnabled" active-text="开启换号" />
|
2025-10-28 19:41:05 +08:00
|
|
|
|
<el-input-number v-model="interruptEveryMin" :min="1" :max="24" />
|
|
|
|
|
|
<span>小时换一次</span>
|
|
|
|
|
|
</div>
|
2025-10-10 15:53:19 +08:00
|
|
|
|
<div>联盟号</div>
|
|
|
|
|
|
<div style="display:flex; gap:8px; align-items:center;">
|
|
|
|
|
|
<el-switch v-model="isAlliance" active-text="联盟号快速私信" />
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showScheduleDlg = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="saveSchedule">开启</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-11-18 18:05:29 +08:00
|
|
|
|
import { ref, reactive, onMounted, onUnmounted, watch, computed } from "vue";
|
2025-09-08 20:53:24 +08:00
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
|
import {
|
|
|
|
|
|
setphoneXYinfo, getphoneXYinfo, getUser,
|
|
|
|
|
|
getHostList, setHostList, getContentpriList,
|
|
|
|
|
|
setContentpriList, getContentList, setContentList,
|
2025-11-18 18:05:29 +08:00
|
|
|
|
getContentListMultiline, getContentpriListMultiline
|
2025-09-08 20:53:24 +08:00
|
|
|
|
} from '@/stores/storage'
|
|
|
|
|
|
import { connectSSE } from '@/utils/sseUtils'
|
|
|
|
|
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
2025-10-28 19:41:05 +08:00
|
|
|
|
import { chat, translationToChinese, translation, customTranslation } from "@/api/chat";
|
2025-09-08 20:53:24 +08:00
|
|
|
|
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
|
2025-09-11 16:46:12 +08:00
|
|
|
|
import AgentGuildDialog from '@/components/AgentGuildDialog.vue'
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue';
|
|
|
|
|
|
import TranslationDialog from '@/components/translationDialog.vue';
|
2025-10-28 19:41:05 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
import ChatDialog from '@/components/ChatDialog.vue'
|
2025-09-11 16:46:12 +08:00
|
|
|
|
import MessageDialogd from '@/components/MessageDialogd.vue'
|
2025-11-10 14:36:15 +08:00
|
|
|
|
import { logout, updates, health } from '@/api/account';
|
2025-09-08 20:53:24 +08:00
|
|
|
|
import {
|
|
|
|
|
|
toHome,
|
|
|
|
|
|
growAccount,
|
|
|
|
|
|
stopScript,
|
|
|
|
|
|
watchLiveForGrowth,
|
|
|
|
|
|
monitorMessages,
|
|
|
|
|
|
passAnchorData,
|
2025-10-10 15:53:19 +08:00
|
|
|
|
followAndGreetUnion,
|
2025-09-08 20:53:24 +08:00
|
|
|
|
addTempAnchorData,
|
|
|
|
|
|
getChatTextInfo,
|
|
|
|
|
|
setLoginInfo,
|
2025-09-11 16:46:12 +08:00
|
|
|
|
aiConfig,
|
2025-09-20 13:31:06 +08:00
|
|
|
|
selectLast,
|
2025-10-28 19:41:05 +08:00
|
|
|
|
changeAccount,
|
|
|
|
|
|
stopAllTask,
|
|
|
|
|
|
anchorList,
|
2025-10-31 19:39:57 +08:00
|
|
|
|
restartTikTok,
|
|
|
|
|
|
getDeviceNetStatus
|
2025-09-08 20:53:24 +08:00
|
|
|
|
} from '@/api/ios';
|
2025-09-20 13:31:06 +08:00
|
|
|
|
import ding from '@/assets/mes.wav'
|
2025-11-18 18:05:29 +08:00
|
|
|
|
//引入两个分包方法
|
|
|
|
|
|
import { useDevices } from '@/composables/useDevices'
|
|
|
|
|
|
import { useScreenStreams } from '@/composables/useScreenStreams'
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
const router = useRouter();
|
2025-09-20 13:31:06 +08:00
|
|
|
|
const openShowChat = ref(true)
|
2025-09-08 20:53:24 +08:00
|
|
|
|
//主播库
|
|
|
|
|
|
const showHostDlg = ref(false)
|
2025-10-10 15:53:19 +08:00
|
|
|
|
//ai人设弹框
|
2025-09-11 16:46:12 +08:00
|
|
|
|
const showMyInfo = ref(false)
|
2025-10-28 19:41:05 +08:00
|
|
|
|
//翻译弹框
|
|
|
|
|
|
const showtransDlg = ref(false)
|
|
|
|
|
|
let transDlgType = ref('')
|
|
|
|
|
|
|
2025-09-11 16:46:12 +08:00
|
|
|
|
// 假设这是你已有的数据
|
|
|
|
|
|
const borkerConfig = reactive({
|
|
|
|
|
|
agentName: '',
|
|
|
|
|
|
guildName: '',
|
|
|
|
|
|
contactTool: '',
|
|
|
|
|
|
contact: ''
|
|
|
|
|
|
})
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
//评论 自动化
|
|
|
|
|
|
let common = ref(true);
|
2025-10-29 19:45:51 +08:00
|
|
|
|
// 自动化
|
|
|
|
|
|
let auto = ref(true);
|
2025-11-10 14:36:15 +08:00
|
|
|
|
let isTranslate = ref(false)
|
2025-10-10 15:53:19 +08:00
|
|
|
|
let initialTextStr = ref('') // 初始文本字符串
|
2025-09-20 13:31:06 +08:00
|
|
|
|
// 批次缓冲(仅用于当前“波”)
|
|
|
|
|
|
let batch = []; // [{ country, text }]
|
|
|
|
|
|
let flushTimer = null;
|
|
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
let hostList = [] // 主播列表
|
|
|
|
|
|
let comonList = [] //评论列表
|
2025-11-10 14:36:15 +08:00
|
|
|
|
//查询 列表 新消息轮询
|
|
|
|
|
|
let getNetworkListtimer = null;
|
2025-09-08 20:53:24 +08:00
|
|
|
|
let userdata = getUser();
|
|
|
|
|
|
let chatList = ref([])
|
2025-09-11 16:46:12 +08:00
|
|
|
|
|
2025-09-20 13:31:06 +08:00
|
|
|
|
let MesNewList = ref([])
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
//start弹窗
|
|
|
|
|
|
let isMsgPop = ref(false)
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
let runType = ref('')
|
|
|
|
|
|
let isMonitorOn = ref(false)
|
|
|
|
|
|
const hoverIndex = ref(null) //选中
|
|
|
|
|
|
let showDialog = ref(false);//弹窗是否显示
|
|
|
|
|
|
let dialogTitle = ref('');//当前弹窗类型
|
|
|
|
|
|
// 你可以用这种方式声明按钮们
|
2025-09-11 16:46:12 +08:00
|
|
|
|
//停止中
|
|
|
|
|
|
let stopLoading = null
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 是否未开通 AI 自动回复(= 监测消息不可用)
|
|
|
|
|
|
const isListenLockedByPlan = computed(() => String(userdata?.aiReplay ?? '0') === '0')
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-11-18 18:05:29 +08:00
|
|
|
|
// === 1. 设备相关 ===
|
|
|
|
|
|
const {
|
|
|
|
|
|
deviceInformation,
|
|
|
|
|
|
getDeviceListFun,
|
|
|
|
|
|
getListTimer,
|
|
|
|
|
|
openTk,
|
|
|
|
|
|
stopOne,
|
|
|
|
|
|
} = useDevices()
|
|
|
|
|
|
|
|
|
|
|
|
// === 2. 屏幕流相关 ===
|
|
|
|
|
|
const {
|
|
|
|
|
|
selectedDevice,
|
|
|
|
|
|
imgRefs,
|
|
|
|
|
|
imgSrcMap,
|
|
|
|
|
|
refreshAllImgs,
|
|
|
|
|
|
refreshOneImg,
|
|
|
|
|
|
refreshAllStopImgs,
|
|
|
|
|
|
getCanvasStyle,
|
|
|
|
|
|
imgWH,
|
|
|
|
|
|
selectDevice,
|
|
|
|
|
|
onCanvasDown,
|
|
|
|
|
|
onCanvasMove,
|
|
|
|
|
|
onCanvasUp,
|
|
|
|
|
|
} = useScreenStreams(deviceInformation)
|
|
|
|
|
|
|
2025-10-31 19:39:57 +08:00
|
|
|
|
// 每台设备的网络状态:true=正常,false=异常
|
|
|
|
|
|
const netStatus = reactive({}) // { [deviceId]: boolean }
|
|
|
|
|
|
|
|
|
|
|
|
// —— 网络波动联动(每设备暂停/恢复)——
|
|
|
|
|
|
const offlineFlags = reactive({}) // { [deviceId]: true/false } 是否因断网而暂停过
|
|
|
|
|
|
const lastNet = reactive({}) // { [deviceId]: true/false } 上一次看到的网络状态
|
|
|
|
|
|
const resumeTimers = new Map() // deviceId -> setTimeout id
|
|
|
|
|
|
const NET_RESUME_STABLE_MS = 6000 // 断网恢复后,等待网络稳定的毫秒数
|
|
|
|
|
|
|
|
|
|
|
|
function clearResumeTimer(id) {
|
|
|
|
|
|
const t = resumeTimers.get(id)
|
|
|
|
|
|
if (t) {
|
|
|
|
|
|
clearTimeout(t)
|
|
|
|
|
|
resumeTimers.delete(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// —— 在线判定/过滤 ——
|
|
|
|
|
|
// 红框=netStatus[id] === false 视为离线;其它(true/undefined)都当作可用
|
|
|
|
|
|
const isOnline = (id) => netStatus[id] !== false
|
|
|
|
|
|
const onlineOnly = (ids) => ids.filter(isOnline)
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
// 当前是否被其它模式占用(四个互斥按钮专用)
|
|
|
|
|
|
const isLocked = (type) => !!runType.value && runType.value !== type
|
|
|
|
|
|
|
2025-11-18 18:05:29 +08:00
|
|
|
|
// —— SSE 开关(分别控制爬虫队列 & 大哥队列)——
|
|
|
|
|
|
const sseCrawlerEnabled = ref(JSON.parse(localStorage.getItem('SSE_CRAWLER_ENABLED') ?? 'true')) // q.tenant.*
|
|
|
|
|
|
const sseBossEnabled = ref(JSON.parse(localStorage.getItem('SSE_BOSS_ENABLED') ?? 'false')) // b.tenant.*
|
|
|
|
|
|
|
|
|
|
|
|
watch(sseCrawlerEnabled, v => {
|
|
|
|
|
|
localStorage.setItem('SSE_CRAWLER_ENABLED', JSON.stringify(v))
|
2025-09-20 13:31:06 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-18 18:05:29 +08:00
|
|
|
|
watch(sseBossEnabled, v => {
|
|
|
|
|
|
localStorage.setItem('SSE_BOSS_ENABLED', JSON.stringify(v))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 至少有一个开着才算启用 SSE
|
|
|
|
|
|
const sseAnyEnabled = computed(() => sseCrawlerEnabled.value || sseBossEnabled.value)
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
|
2025-11-10 14:36:15 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
const buttons = [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '刷新',
|
2025-11-18 18:05:29 +08:00
|
|
|
|
onClick: () => refreshAllImgs(),
|
2025-09-08 20:53:24 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '打开tiktok',
|
|
|
|
|
|
onClick: () => openTk(),
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn2.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn2-2.png', import.meta.url).href
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '返回主页',
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
toHome({ udid: item.deviceId })
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn3.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn3-3.png', import.meta.url).href
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '刷直播',
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
if (runType.value == 'brushLive') {
|
|
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
stopScript({ udid: item.deviceId })
|
|
|
|
|
|
})
|
|
|
|
|
|
runType.value = ''
|
|
|
|
|
|
return
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 若被其它模式占用:直接返回(已在样式层禁点,这里双保险)
|
|
|
|
|
|
if (isLocked('brushLive')) return
|
|
|
|
|
|
|
|
|
|
|
|
runType.value = 'brushLive'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
|
|
|
|
|
},
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn4.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn4-4.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
style: () => ctrlStyle('brushLive')
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '刷视频',
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
if (runType.value == 'like') {
|
|
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
stopScript({ udid: item.deviceId })
|
|
|
|
|
|
})
|
|
|
|
|
|
runType.value = ''
|
|
|
|
|
|
return
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (isLocked('like')) return
|
2025-10-28 19:41:05 +08:00
|
|
|
|
// runType.value = 'like'
|
|
|
|
|
|
// deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
|
|
|
|
|
dialogTitle.value = '视频评论';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
showDialog.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
initialTextStr.value = getContentListMultiline();
|
|
|
|
|
|
}, 500)
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn5.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn5-5.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
style: () => ctrlStyle('like')
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '启动调度任务',
|
|
|
|
|
|
onClick: () => openScheduleDialog(),
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn6.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn6-6.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
style: () => ctrlStyle('follow')
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '监测消息',
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 点击前先判断是否未开通,未开通只提示不执行
|
2025-09-08 20:53:24 +08:00
|
|
|
|
onClick: () => {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
if (isListenLockedByPlan.value) {
|
|
|
|
|
|
ElMessage.warning('未开通 AI 自动回复功能,请联系管理员开通');
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-10-28 19:41:05 +08:00
|
|
|
|
if (runType.value == 'listen') {
|
2025-09-08 20:53:24 +08:00
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
stopScript({ udid: item.deviceId })
|
|
|
|
|
|
})
|
|
|
|
|
|
runType.value = ''
|
|
|
|
|
|
return
|
2025-11-10 14:36:15 +08:00
|
|
|
|
}
|
2025-10-28 19:41:05 +08:00
|
|
|
|
if (isLocked('listen')) return
|
|
|
|
|
|
runType.value = 'listen'
|
2025-09-08 20:53:24 +08:00
|
|
|
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
|
|
|
|
|
},
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 悬停提示文案
|
|
|
|
|
|
tooltip: () => (isListenLockedByPlan.value ? '未开通:AI 自动回复' : ''),
|
2025-09-08 20:53:24 +08:00
|
|
|
|
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
|
|
|
|
|
|
},
|
2025-10-28 19:41:05 +08:00
|
|
|
|
style: () => ctrlStyle('listen')
|
2025-09-08 20:53:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
// {
|
|
|
|
|
|
// label: '定时调度',
|
|
|
|
|
|
// onClick: () => openScheduleDialog(),
|
|
|
|
|
|
// 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
|
|
|
|
|
|
// }
|
|
|
|
|
|
// },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '全部停止',
|
2025-11-11 15:13:03 +08:00
|
|
|
|
onClick: () => stopAll(2000, 'click'),
|
2025-09-08 20:53:24 +08:00
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn8.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn8-8.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '登出',
|
2025-10-10 15:53:19 +08:00
|
|
|
|
onClick: () => doLogout(),
|
2025-09-08 20:53:24 +08:00
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn9.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn9-9.png', import.meta.url).href
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
|
|
|
|
|
const isAlliance = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// —— 打断器配置 ——
|
|
|
|
|
|
// 是否开启
|
|
|
|
|
|
const interruptEnabled = ref(false)
|
|
|
|
|
|
// 每隔多少分钟打断一次(可做弹窗配置)
|
2025-10-28 19:41:05 +08:00
|
|
|
|
const interruptEveryMin = ref(2)
|
2025-10-10 15:53:19 +08:00
|
|
|
|
// 打断器最大重试次数 & 每次超时(按需调)
|
2025-10-28 19:41:05 +08:00
|
|
|
|
const interruptMaxRetries = 1
|
2025-10-10 15:53:19 +08:00
|
|
|
|
const interruptCallTimeoutMs = 120_000
|
|
|
|
|
|
|
|
|
|
|
|
// 运行态
|
|
|
|
|
|
let interrupting = false
|
|
|
|
|
|
let lastInterruptTs = Number(localStorage.getItem('INT_LAST_TS') || '0')
|
|
|
|
|
|
|
|
|
|
|
|
// 暂存“被打断时”的片段状态,用于恢复
|
|
|
|
|
|
let pauseSnapshot = null
|
|
|
|
|
|
// 结构:{ index, elapsedBeforePause }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
const schedulePlan = [
|
|
|
|
|
|
{ key: 'follow', duration: 40 * 60 * 1000 },
|
|
|
|
|
|
{ key: 'like', duration: 20 * 60 * 1000 },
|
|
|
|
|
|
]
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
// 调度状态(持久化一下,避免刷新丢失)
|
|
|
|
|
|
let scheduleState = (() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const saved = JSON.parse(localStorage.getItem('SCHEDULE_STATE') || '{}')
|
|
|
|
|
|
if (saved && typeof saved.index === 'number' && typeof saved.startTime === 'number') {
|
|
|
|
|
|
return saved
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch { }
|
|
|
|
|
|
return { index: 0, startTime: Date.now() }
|
|
|
|
|
|
})()
|
|
|
|
|
|
|
|
|
|
|
|
let scheduleTimer = null // 轮询定时器句柄
|
|
|
|
|
|
const scheduleTickMs = 30_000 // 每 30s 检查一次是否到切换点
|
2025-09-08 23:12:00 +08:00
|
|
|
|
let scheduleEnabled = ref(false) // 需要时可手动关闭调度(例如“全部停止”)
|
2025-09-08 20:53:24 +08:00
|
|
|
|
// 弹窗
|
|
|
|
|
|
const showScheduleDlg = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 两个时间片(默认 A=follow 40min,B=like 20min)
|
|
|
|
|
|
const schedAKey = ref('follow')
|
|
|
|
|
|
const schedAMin = ref(40)
|
|
|
|
|
|
const schedBKey = ref('like')
|
|
|
|
|
|
const schedBMin = ref(20)
|
|
|
|
|
|
// 打开弹窗:把当前 schedulePlan 映射到 UI
|
|
|
|
|
|
function openScheduleDialog() {
|
|
|
|
|
|
// 把当前计划读出来(只支持两个片段的简易版)
|
|
|
|
|
|
if (Array.isArray(schedulePlan) && schedulePlan.length >= 2) {
|
|
|
|
|
|
const a = schedulePlan[0], b = schedulePlan[1]
|
|
|
|
|
|
schedAKey.value = a?.key || 'follow'
|
|
|
|
|
|
schedAMin.value = Math.max(1, Math.round((a?.duration || 40 * 60_000) / 60_000))
|
|
|
|
|
|
schedBKey.value = b?.key || 'like'
|
|
|
|
|
|
schedBMin.value = Math.max(1, Math.round((b?.duration || 20 * 60_000) / 60_000))
|
|
|
|
|
|
}
|
|
|
|
|
|
showScheduleDlg.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存:校验=60 分钟 → 更新 schedulePlan → 持久化 → 重启轮询
|
|
|
|
|
|
function saveSchedule() {
|
2025-09-08 23:12:00 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
const total = schedAMin.value + schedBMin.value
|
|
|
|
|
|
if (total !== 60) {
|
|
|
|
|
|
ElMessage.error('两个片段相加必须等于 60 分钟')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
schedulePlan.splice(0, schedulePlan.length,
|
|
|
|
|
|
{ key: schedAKey.value, duration: schedAMin.value * 60_000 },
|
|
|
|
|
|
{ key: schedBKey.value, duration: schedBMin.value * 60_000 },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 存 localStorage
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_PLAN', JSON.stringify(schedulePlan))
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_ENABLED', JSON.stringify(!!scheduleEnabled.value))
|
|
|
|
|
|
|
|
|
|
|
|
// 重置时间片起点并立即生效
|
|
|
|
|
|
scheduleState.index = 0
|
|
|
|
|
|
scheduleState.startTime = Date.now()
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
|
|
|
|
|
|
|
|
|
|
|
// 若启用则重启轮询
|
2025-09-08 23:12:00 +08:00
|
|
|
|
startScheduleLoop()
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
showScheduleDlg.value = false
|
|
|
|
|
|
ElMessage.success('已保存定时调度')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getMesList(deviceId) {
|
|
|
|
|
|
getChatTextInfo({ udid: deviceId }).then((res) => {
|
|
|
|
|
|
if (res) {
|
|
|
|
|
|
chatList.value = res
|
|
|
|
|
|
console.log(chatList.value)
|
|
|
|
|
|
getTranslation(chatList.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 15:13:03 +08:00
|
|
|
|
async function stopAll(time, type) {
|
2025-09-11 16:46:12 +08:00
|
|
|
|
stopLoading = ElLoading.service({
|
|
|
|
|
|
lock: true,
|
|
|
|
|
|
text: '停止中',
|
|
|
|
|
|
background: 'rgba(0, 0, 0, 0.7)',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 🔒 先强制关闭所有提示弹窗
|
|
|
|
|
|
ElMessageBox.close();
|
2025-11-11 15:13:03 +08:00
|
|
|
|
if (type == 'click') {
|
|
|
|
|
|
// 关闭调度定时器
|
|
|
|
|
|
clearInterval(scheduleTimer)
|
|
|
|
|
|
scheduleTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
|
2025-10-29 19:45:51 +08:00
|
|
|
|
scheduleEnabled.value = false;
|
|
|
|
|
|
runType.value = '';
|
|
|
|
|
|
isMsgPop.value = false;
|
|
|
|
|
|
dropCurrentWave();
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// ========== ⏰ 新增超时逻辑 ==========
|
|
|
|
|
|
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)
|
|
|
|
|
|
),
|
|
|
|
|
|
]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-29 19:45:51 +08:00
|
|
|
|
try {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 1️⃣ 带超时保护地调用接口
|
|
|
|
|
|
const res = await withTimeout(
|
|
|
|
|
|
stopAllTask(deviceInformation.value.map(item => item.deviceId)),
|
|
|
|
|
|
STOP_TASK_TIMEOUT_MS
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (res === 'timeout') {
|
|
|
|
|
|
console.warn('stopAllTask 请求超时,自动继续后续逻辑');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2️⃣ 等待指定时间(原逻辑保留)
|
2025-10-29 19:45:51 +08:00
|
|
|
|
await new Promise(r => setTimeout(r, time));
|
|
|
|
|
|
|
|
|
|
|
|
stopLoading.close();
|
|
|
|
|
|
console.log('全部停止成功', printCurrentTime());
|
|
|
|
|
|
ElMessage.success('全部停止成功');
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 3️⃣ 无论接口成功或超时都返回 true
|
2025-10-29 19:45:51 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
} catch (e) {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
console.error('停止失败', printCurrentTime(), e);
|
2025-10-29 19:45:51 +08:00
|
|
|
|
ElMessage.error('脚本已停止');
|
|
|
|
|
|
stopLoading.close();
|
|
|
|
|
|
return false;
|
2025-09-11 16:46:12 +08:00
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
//确认多行文本框内容
|
2025-10-28 19:41:05 +08:00
|
|
|
|
function onDialogConfirm(result, type, index, data) {
|
|
|
|
|
|
console.log(type, result, data);
|
2025-09-08 20:53:24 +08:00
|
|
|
|
if (type == '主播ID') {
|
|
|
|
|
|
hostList = (result || []).map(id => ({ id, country: '' }))
|
|
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
//无需评论,注释,如果需要注释下面代码,放开后面代码
|
|
|
|
|
|
// dialogTitle.value = '私信';
|
|
|
|
|
|
// setTimeout(() => {
|
|
|
|
|
|
// showDialog.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
// initialTextStr.value = getContentpriListMultiline();
|
|
|
|
|
|
// }, 500)
|
|
|
|
|
|
dialogTitle.value = '评论';
|
2025-09-08 20:53:24 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
showDialog.value = true;
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
initialTextStr.value = getContentListMultiline();
|
2025-10-10 15:53:19 +08:00
|
|
|
|
}, 500)
|
2025-10-28 19:41:05 +08:00
|
|
|
|
} else if (type == '评论') {
|
|
|
|
|
|
comonList = result
|
|
|
|
|
|
setContentList(result)
|
|
|
|
|
|
common.value = data.common
|
|
|
|
|
|
|
|
|
|
|
|
transDlgType.value = '私信'
|
|
|
|
|
|
showtransDlg.value = true
|
|
|
|
|
|
} else if (type == '视频评论') {
|
|
|
|
|
|
runType.value = 'like'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId, comment: result, isComment: data.common }))
|
|
|
|
|
|
} else if (type == '评论(无消息将刷视频)') {
|
|
|
|
|
|
runType.value = 'listen'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId, comment: result }))
|
2025-10-10 15:53:19 +08:00
|
|
|
|
}
|
2025-10-28 19:41:05 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-20 13:31:06 +08:00
|
|
|
|
onMounted(async () => {
|
2025-09-08 20:53:24 +08:00
|
|
|
|
const loading = ElLoading.service({
|
|
|
|
|
|
lock: true,
|
2025-10-10 15:53:19 +08:00
|
|
|
|
text: '检测设备中...',
|
2025-09-08 20:53:24 +08:00
|
|
|
|
background: 'rgba(0, 0, 0, 0.7)',
|
|
|
|
|
|
});
|
|
|
|
|
|
getDeviceListFun()
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const res = await window.electronAPI.isiproxy({
|
|
|
|
|
|
intervalMs: 2000,
|
|
|
|
|
|
exeName: 'iproxy.exe',
|
2025-10-28 19:41:05 +08:00
|
|
|
|
// maxWaitMs: 3000, // 可选:5分钟超时
|
2025-10-10 15:53:19 +08:00
|
|
|
|
maxWaitMs: 300000, // 可选:5分钟超时
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res.running) {
|
|
|
|
|
|
// 检测到了,你再决定是否跳转
|
|
|
|
|
|
loading.close();
|
|
|
|
|
|
console.log('检测到了')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 超时兜底提示
|
|
|
|
|
|
loading.close();
|
|
|
|
|
|
console.log('未检测到设备')
|
|
|
|
|
|
ElMessage.error(`未检测到设备`)
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2025-11-10 14:36:15 +08:00
|
|
|
|
//MQ链接
|
|
|
|
|
|
window.electronAPI.startMq(userdata.tenantId, userdata.id)
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
// 初始化时获取设备列表
|
2025-10-28 19:41:05 +08:00
|
|
|
|
//每3秒获取一次设备列表 消息列表 和 查询主播列表是否还有主播
|
2025-11-18 18:05:29 +08:00
|
|
|
|
getListTimer.value = setInterval(async () => {
|
2025-10-31 19:39:57 +08:00
|
|
|
|
getDeviceListFun() //获取设备列表
|
2025-11-10 14:36:15 +08:00
|
|
|
|
selectLastFun() //获取新消息
|
|
|
|
|
|
const hostsList = await getStoredHostList() //获取主播列表
|
2025-10-28 19:41:05 +08:00
|
|
|
|
// console.log(hostsList.length)
|
|
|
|
|
|
//当私信主播时,主播列表没有数据了,提示列表空了 并且关闭私信
|
|
|
|
|
|
if (runType.value == 'follow') {
|
|
|
|
|
|
if (hostsList.length <= 0) {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
await stopAll(2000)
|
2025-10-28 19:41:05 +08:00
|
|
|
|
runType.value = 'like'
|
2025-11-10 14:36:15 +08:00
|
|
|
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId, comment: getContentList(), isComment: common.value }))
|
|
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
ElMessageBox.alert('私信全部完成!(刷视频中)', '提示', {
|
|
|
|
|
|
confirmButtonText: 'OK',
|
|
|
|
|
|
callback: (action) => {
|
2025-09-20 13:31:06 +08:00
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
},
|
|
|
|
|
|
})
|
2025-11-10 14:36:15 +08:00
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
2025-09-20 13:31:06 +08:00
|
|
|
|
}, 3000)
|
2025-10-28 19:41:05 +08:00
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
getNetworkListtimer = setInterval(async () => {
|
2025-10-28 19:41:05 +08:00
|
|
|
|
await checkVPN()
|
2025-10-31 19:39:57 +08:00
|
|
|
|
await refreshNetStatus()
|
2025-11-10 14:36:15 +08:00
|
|
|
|
health().then((res) => {
|
|
|
|
|
|
|
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
|
if (err.code === 40400) {
|
|
|
|
|
|
//关闭获取设备列表的方法
|
2025-11-18 18:05:29 +08:00
|
|
|
|
clearInterval(getListTimer.value)
|
|
|
|
|
|
getListTimer.value = null
|
2025-11-10 14:36:15 +08:00
|
|
|
|
//关闭获取网络状态的方法
|
|
|
|
|
|
clearInterval(getNetworkListtimer)
|
|
|
|
|
|
getNetworkListtimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
})
|
2025-11-18 18:05:29 +08:00
|
|
|
|
}, 1000 * 90)
|
2025-10-28 19:41:05 +08:00
|
|
|
|
|
2025-10-31 19:39:57 +08:00
|
|
|
|
|
2025-09-20 13:31:06 +08:00
|
|
|
|
if (!await isAiConfig()) {
|
|
|
|
|
|
showMyInfo.value = true
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
function scheduleFlush(handler, delay = 400) {
|
|
|
|
|
|
if (flushTimer) clearTimeout(flushTimer);
|
|
|
|
|
|
flushTimer = setTimeout(() => {
|
|
|
|
|
|
if (batch.length) {
|
|
|
|
|
|
const items = batch.slice(); // 拷贝一份
|
|
|
|
|
|
batch.length = 0; // 清空批次
|
|
|
|
|
|
try {
|
|
|
|
|
|
handler(items);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('[SSE flush error]', e);
|
|
|
|
|
|
// 出错不回灌,避免重复提交;必要时可根据需要 batch.push(...items)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, delay);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// —— SSE 接收 ——
|
|
|
|
|
|
const es = connectSSE('http://localhost:3312/events', (data) => {
|
2025-11-18 18:05:29 +08:00
|
|
|
|
// 所有 SSE 关掉的话,直接不处理
|
|
|
|
|
|
if (!sseAnyEnabled.value) return
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
if (data === 'start') {
|
2025-10-10 15:53:19 +08:00
|
|
|
|
// 新一波开始:清空上一波的缓冲,重置防抖
|
|
|
|
|
|
dropCurrentWave()
|
|
|
|
|
|
return
|
2025-09-08 20:53:24 +08:00
|
|
|
|
}
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
2025-11-18 18:05:29 +08:00
|
|
|
|
// 1️⃣ 判断来源:_mqMeta = 1(q.tenant.*) / 2(b.tenant.*)
|
|
|
|
|
|
const metaCode = data && data._mqMeta
|
|
|
|
|
|
const fromCrawler = metaCode === 1 || metaCode === '1' // 爬虫队列 q.tenant.*
|
|
|
|
|
|
const fromBoss = metaCode === 2 || metaCode === '2' // 大哥队列 b.tenant.*
|
|
|
|
|
|
|
|
|
|
|
|
// 没有标记的老数据,一律按爬虫处理(可选)
|
|
|
|
|
|
const isUnknown = !fromCrawler && !fromBoss
|
|
|
|
|
|
|
|
|
|
|
|
// 2️⃣ 按开关过滤
|
|
|
|
|
|
if (fromCrawler && !sseCrawlerEnabled.value) return
|
|
|
|
|
|
if (fromBoss && !sseBossEnabled.value) return
|
|
|
|
|
|
if (isUnknown && !sseCrawlerEnabled.value) return // 老数据当爬虫看
|
|
|
|
|
|
|
|
|
|
|
|
// 3️⃣ 按来源取不同字段
|
|
|
|
|
|
let country = ''
|
|
|
|
|
|
let text = ''
|
|
|
|
|
|
let invitationType = ''
|
|
|
|
|
|
let id = ''
|
|
|
|
|
|
|
|
|
|
|
|
if (fromBoss) {
|
|
|
|
|
|
// 大哥队列 b.tenant.* 进来的那条 JSON 结构:
|
|
|
|
|
|
// {"id":5681,"displayId":"80",...,"region":"西南",...,"_mqMeta":2}
|
|
|
|
|
|
country = data.region || ''
|
|
|
|
|
|
// 主播ID 优先 displayId → hostDisplayId → userIdStr → userId
|
|
|
|
|
|
text = data.displayId
|
|
|
|
|
|
|| data.hostDisplayId
|
|
|
|
|
|
|| data.userIdStr
|
|
|
|
|
|
|| (data.userId != null ? String(data.userId) : '')
|
|
|
|
|
|
invitationType = data.invitationType != null ? data.invitationType : 2 // 金票默认 2
|
|
|
|
|
|
id = data.id != null ? data.id : ''
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 爬虫队列 q.tenant.*(以前的老逻辑)
|
|
|
|
|
|
country = data && data.country != null ? data.country : ''
|
|
|
|
|
|
text = data && (data.hostsId != null ? data.hostsId : data.text)
|
|
|
|
|
|
invitationType = data && (data.invitationType != null ? data.invitationType : '')
|
|
|
|
|
|
id = data && data.id != null ? data.id : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-10 15:53:19 +08:00
|
|
|
|
if (!text) return
|
2025-11-18 18:05:29 +08:00
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
batch.push({ country, text, invitationType, id })
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
2025-11-18 18:05:29 +08:00
|
|
|
|
//400ms内如果没有数据了 进行入库,如果400ms内进入了数据 重置计时器 一波一波入库
|
2025-10-10 15:53:19 +08:00
|
|
|
|
scheduleFlush((items) => {
|
|
|
|
|
|
// 批量入库
|
|
|
|
|
|
const list = items.map(h => ({
|
|
|
|
|
|
anchorId: h.text,
|
|
|
|
|
|
country: h.country || '',
|
|
|
|
|
|
invitationType: h.invitationType,
|
|
|
|
|
|
state: stateByInvType(h.invitationType),
|
|
|
|
|
|
}))
|
2025-10-28 19:41:05 +08:00
|
|
|
|
updates(items.map(h => ({
|
|
|
|
|
|
id: h.id,
|
|
|
|
|
|
aiOperation: 1,
|
|
|
|
|
|
})))
|
2025-10-10 15:53:19 +08:00
|
|
|
|
addTempAnchorData(list)
|
|
|
|
|
|
}, 400)
|
|
|
|
|
|
})
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-11-18 18:05:29 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
//关闭获取设备列表的方法
|
2025-11-18 18:05:29 +08:00
|
|
|
|
clearInterval(getListTimer.value)
|
|
|
|
|
|
getListTimer.value = null
|
2025-11-10 14:36:15 +08:00
|
|
|
|
//关闭获取网络状态的方法
|
|
|
|
|
|
clearInterval(getNetworkListtimer)
|
|
|
|
|
|
getNetworkListtimer = null
|
|
|
|
|
|
|
2025-11-18 18:05:29 +08:00
|
|
|
|
//关闭调度定时器
|
|
|
|
|
|
if (scheduleTimer) {
|
|
|
|
|
|
clearInterval(scheduleTimer)
|
|
|
|
|
|
scheduleTimer = null
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
})
|
2025-09-11 16:46:12 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
2025-09-20 13:31:06 +08:00
|
|
|
|
//获取新消息
|
|
|
|
|
|
const selectLastFun = () => {
|
|
|
|
|
|
selectLast().then((res) => {
|
|
|
|
|
|
let mesInfoData = res
|
|
|
|
|
|
mesInfoData.forEach(element => {
|
|
|
|
|
|
deviceInformation.value.forEach((item, index) => {
|
|
|
|
|
|
if (item.deviceId == element.device) {
|
|
|
|
|
|
element.device = index + 1 + '号设备'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
MesNewList.value = [...mesInfoData];
|
2025-09-20 13:31:06 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
async function uploadLogFile() {
|
|
|
|
|
|
let loading = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先弹出确认框
|
|
|
|
|
|
await ElMessageBox.confirm(
|
|
|
|
|
|
'确定要上传日志文件吗?',
|
|
|
|
|
|
'提示',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
// 如果点了确定,就会走到这里
|
|
|
|
|
|
loading = ElLoading.service({
|
|
|
|
|
|
lock: true,
|
|
|
|
|
|
text: '上传中...',
|
|
|
|
|
|
background: 'rgba(0, 0, 0, 0.7)',
|
|
|
|
|
|
})
|
|
|
|
|
|
const res = await setLoginInfo({
|
|
|
|
|
|
"tenantId": userdata.tenantId,
|
|
|
|
|
|
"userId": userdata.id,
|
|
|
|
|
|
"token": userdata.tokenValue
|
|
|
|
|
|
})
|
|
|
|
|
|
loading.close()
|
|
|
|
|
|
console.log("上传文件返回", res)
|
|
|
|
|
|
if (res) {
|
|
|
|
|
|
console.log("✅ 上传成功:", res)
|
|
|
|
|
|
ElMessage.success('✅ 上传成功')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("❌ 上传失败:", res.msg)
|
|
|
|
|
|
ElMessage.error('❌ 上传失败: ' + (res.msg || '未知错误'))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
loading.close()
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果用户点了取消,会进入这里
|
|
|
|
|
|
if (err === 'cancel' || err === 'close') {
|
|
|
|
|
|
ElMessage.info('已取消上传')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("❌ 上传异常:", err)
|
|
|
|
|
|
ElMessage.error('❌ 上传异常: ' + err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-28 19:41:05 +08:00
|
|
|
|
function runTask(key, deviceId, type) {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
|
|
|
|
|
|
// —— 若指定单设备,且该设备当前离线,则直接跳过 ——
|
|
|
|
|
|
if (deviceId && !isOnline(deviceId)) {
|
|
|
|
|
|
markPendingForOffline([deviceId]); // ⭐ 新增:让它后续来网能自动跟上
|
|
|
|
|
|
ElMessage.warning('该设备当前网络不可用,已跳过');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
console.log('[schedule] 切换到任务:', key, printCurrentTime())
|
|
|
|
|
|
|
2025-09-11 16:46:12 +08:00
|
|
|
|
forceActivate(key, async () => {
|
2025-09-08 20:53:24 +08:00
|
|
|
|
if (key === 'follow') {
|
2025-10-10 15:53:19 +08:00
|
|
|
|
console.log("进入follow", scheduleEnabled.value)
|
2025-09-08 23:12:00 +08:00
|
|
|
|
if (scheduleEnabled.value) {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
//如果有id 就只执行一个 并且return出去
|
|
|
|
|
|
if (deviceId) {
|
2025-10-31 19:39:57 +08:00
|
|
|
|
if (isAlliance.value) {
|
|
|
|
|
|
followAndGreetUnion(
|
|
|
|
|
|
{
|
|
|
|
|
|
deviceList: [deviceId],
|
|
|
|
|
|
anchorList: [],
|
|
|
|
|
|
prologueList: getContentpriList(),
|
|
|
|
|
|
needReply: auto.value,
|
2025-11-10 14:36:15 +08:00
|
|
|
|
needTranslate: isTranslate.value,
|
2025-10-31 19:39:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
).then((res) => {
|
|
|
|
|
|
hostList = []
|
2025-11-10 14:36:15 +08:00
|
|
|
|
return
|
2025-10-31 19:39:57 +08:00
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
passAnchorData(
|
|
|
|
|
|
{
|
|
|
|
|
|
deviceList: [deviceId],
|
|
|
|
|
|
anchorList: [],
|
|
|
|
|
|
prologueList: getContentpriList(),
|
|
|
|
|
|
comment: comonList,
|
|
|
|
|
|
needReply: auto.value,
|
2025-11-10 14:36:15 +08:00
|
|
|
|
needTranslate: isTranslate.value,
|
2025-10-31 19:39:57 +08:00
|
|
|
|
isComment: common.value //是否评论
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
).then((res) => {
|
|
|
|
|
|
hostList = []
|
2025-11-10 14:36:15 +08:00
|
|
|
|
return
|
2025-10-31 19:39:57 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 16:46:12 +08:00
|
|
|
|
return
|
2025-10-31 19:39:57 +08:00
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
//没有id 直接停止所有设备
|
|
|
|
|
|
await stopAll(2000)
|
|
|
|
|
|
|
|
|
|
|
|
//第一个小时结束后,第二轮开始的时候,直接进入follow
|
|
|
|
|
|
setTimeout(() => {
|
2025-11-18 18:05:29 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-11-10 14:36:15 +08:00
|
|
|
|
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
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-08 23:12:00 +08:00
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
}, 1000)
|
2025-09-08 23:12:00 +08:00
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果是暂停状态,则打开弹窗进行开启第一轮任务
|
|
|
|
|
|
scheduleEnabled.value = true
|
|
|
|
|
|
initialTextStr.value = '';
|
|
|
|
|
|
dialogTitle.value = '主播ID';
|
|
|
|
|
|
showDialog.value = true;
|
2025-09-08 23:12:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
} else if (key === 'like') {
|
|
|
|
|
|
|
2025-09-11 16:46:12 +08:00
|
|
|
|
if (!deviceId) {
|
2025-11-18 18:05:29 +08:00
|
|
|
|
if (scheduleEnabled.value) {
|
|
|
|
|
|
await stopAll(2000)
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
scheduleEnabled.value = true
|
|
|
|
|
|
runType.value = 'like'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId, comment: getContentList(), isComment: common.value }))
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dialogTitle.value = '视频评论';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
showDialog.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
initialTextStr.value = getContentListMultiline();
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 16:46:12 +08:00
|
|
|
|
} else {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
growAccount({ udid: deviceId, comment: getContentList(), isComment: common.value })
|
2025-09-11 16:46:12 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
2025-11-18 18:05:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
} else if (key === 'brushLive') {
|
2025-09-11 16:46:12 +08:00
|
|
|
|
if (!deviceId) {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
await stopAll(2000)
|
2025-09-11 16:46:12 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
watchLiveForGrowth({ udid: deviceId })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
2025-09-08 23:12:00 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-09-08 20:53:24 +08:00
|
|
|
|
runType.value = 'brushLive'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
runType.value = 'brushLive'
|
|
|
|
|
|
} else if (key === 'listen') {
|
2025-09-11 16:46:12 +08:00
|
|
|
|
if (!deviceId) {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
await stopAll(2000)
|
2025-09-11 16:46:12 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
monitorMessages({ udid: deviceId })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
runType.value = 'listen'
|
2025-09-08 23:12:00 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-09-08 20:53:24 +08:00
|
|
|
|
isMonitorOn.value = true
|
|
|
|
|
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
|
|
|
|
|
/** 恢复:回到 scheduleState.index 对应片段,并让 startTime 回到“暂停前进度” */
|
|
|
|
|
|
function resumeAfterInterrupt() {
|
|
|
|
|
|
if (!pauseSnapshot) return
|
|
|
|
|
|
const { index, elapsedBeforePause } = pauseSnapshot
|
|
|
|
|
|
scheduleState.index = index
|
|
|
|
|
|
scheduleEnabled.value = true
|
|
|
|
|
|
// 让当前片段已用时 = 暂停前的 elapsedBeforePause
|
|
|
|
|
|
scheduleState.startTime = Date.now() - elapsedBeforePause
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
|
|
|
|
|
|
|
|
|
|
|
// 确保任务是该片段
|
2025-11-10 14:36:15 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-10-10 15:53:19 +08:00
|
|
|
|
runTask(schedulePlan[scheduleState.index].key)
|
|
|
|
|
|
pauseSnapshot = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 执行一次中断器函数
|
|
|
|
|
|
* 该函数会尝试使用设备列表中的每个设备更改账户,并检查是否所有操作都成功
|
|
|
|
|
|
* @returns {Promise<boolean>} 返回一个Promise,解析为布尔值,表示操作是否全部成功
|
|
|
|
|
|
*/
|
2025-10-10 15:53:19 +08:00
|
|
|
|
async function runInterrupterOnce() {
|
2025-10-28 19:41:05 +08:00
|
|
|
|
// 定义一个异步函数,用于执行设备账户更改操作
|
2025-10-10 15:53:19 +08:00
|
|
|
|
const promiseFn = async () => {
|
2025-10-28 19:41:05 +08:00
|
|
|
|
// 从deviceInformation中提取设备ID,并过滤掉无效值
|
|
|
|
|
|
const devices = (deviceInformation.value || [])
|
|
|
|
|
|
.map(d => d?.deviceId)
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有有效设备,直接返回false
|
|
|
|
|
|
if (!devices.length) return false;
|
|
|
|
|
|
console.log(devices.length + '台设备换账号')
|
|
|
|
|
|
// 为每个设备创建一个检查函数,用于尝试更改账户
|
|
|
|
|
|
const checks = devices.map(async (udid) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 尝试更改账户并检查返回结果
|
|
|
|
|
|
const res = await changeAccount({ udid: udid });
|
|
|
|
|
|
// 检查返回值是否表示成功
|
|
|
|
|
|
return (
|
|
|
|
|
|
res.code === 200
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 如果出错,返回false
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 等待所有检查完成,并获取结果
|
|
|
|
|
|
const settled = await Promise.allSettled(checks);
|
|
|
|
|
|
// 检查是否所有操作都成功完成
|
|
|
|
|
|
const allOk = settled.every(s => s.status === 'fulfilled' && s.value === true);
|
|
|
|
|
|
return allOk;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 使用超时机制执行promiseFn,捕获超时或其他错误并返回false
|
|
|
|
|
|
return await withTimeout(promiseFn(), interruptCallTimeoutMs).catch(() => false);
|
2025-10-10 15:53:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function withTimeout(p, ms) {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const t = setTimeout(() => reject(new Error('timeout')), ms)
|
|
|
|
|
|
p.then(v => { clearTimeout(t); resolve(v) })
|
|
|
|
|
|
.catch(e => { clearTimeout(t); reject(e) })
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
function startScheduleLoop() {
|
2025-11-11 15:13:03 +08:00
|
|
|
|
|
2025-10-10 15:53:19 +08:00
|
|
|
|
lastInterruptTs = Date.now()
|
|
|
|
|
|
localStorage.setItem('INT_LAST_TS', String(lastInterruptTs))
|
|
|
|
|
|
// 先按当前 index 跑一次,保持“即刻对齐”
|
2025-10-28 19:41:05 +08:00
|
|
|
|
runTask(schedulePlan[scheduleState.index].key, null, 1)
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
if (scheduleTimer) clearInterval(scheduleTimer)
|
|
|
|
|
|
|
2025-10-10 15:53:19 +08:00
|
|
|
|
scheduleTimer = setInterval(async () => {
|
|
|
|
|
|
// 关总调度就什么都不做
|
2025-09-08 20:53:24 +08:00
|
|
|
|
if (!scheduleEnabled.value) return
|
|
|
|
|
|
|
2025-10-10 15:53:19 +08:00
|
|
|
|
// —— 先处理“打断器” ——
|
|
|
|
|
|
if (interruptEnabled.value && !interrupting) {
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
if (!lastInterruptTs) lastInterruptTs = now // 首次初始化
|
|
|
|
|
|
|
2025-10-29 19:45:51 +08:00
|
|
|
|
const due = now - lastInterruptTs >= interruptEveryMin.value * 60_000 * 60
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
'due=', due,
|
|
|
|
|
|
'elapsed=', now - lastInterruptTs,
|
2025-10-29 19:45:51 +08:00
|
|
|
|
'threshold=', interruptEveryMin.value * 60_000 * 60
|
2025-10-10 15:53:19 +08:00
|
|
|
|
)
|
|
|
|
|
|
if (due) {
|
|
|
|
|
|
interrupting = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 记录暂停前的进度(用于恢复)
|
|
|
|
|
|
const cur = schedulePlan[scheduleState.index]
|
|
|
|
|
|
const elapsedBeforePause = now - scheduleState.startTime
|
|
|
|
|
|
pauseSnapshot = { index: scheduleState.index, elapsedBeforePause }
|
|
|
|
|
|
|
|
|
|
|
|
// 停掉当前片段
|
2025-11-10 14:36:15 +08:00
|
|
|
|
await stopAll(2000)
|
2025-10-28 19:41:05 +08:00
|
|
|
|
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 执行中断任务(带重试)
|
|
|
|
|
|
let ok = false
|
|
|
|
|
|
for (let i = 0; i < interruptMaxRetries; i++) {
|
|
|
|
|
|
ok = await runInterrupterOnce()
|
|
|
|
|
|
if (ok) break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 无论成功与否都更新节拍,避免立刻再次触发
|
|
|
|
|
|
lastInterruptTs = Date.now()
|
|
|
|
|
|
localStorage.setItem('INT_LAST_TS', String(lastInterruptTs))
|
|
|
|
|
|
|
|
|
|
|
|
// 成功:恢复暂停前的片段进度;失败:也恢复(或改成重启当前片段都可)
|
|
|
|
|
|
resumeAfterInterrupt()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('[Interrupter] 失败:', e)
|
|
|
|
|
|
// 失败兜底:恢复任务
|
|
|
|
|
|
resumeAfterInterrupt()
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
interrupting = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ⚠️ 本轮只做打断,不做片段切换判断
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// —— 正常片段轮换(你原有的逻辑) ——
|
2025-09-08 20:53:24 +08:00
|
|
|
|
const now = Date.now()
|
2025-11-11 15:13:03 +08:00
|
|
|
|
const cur = JSON.parse(localStorage.getItem('SCHEDULE_PLAN'))[scheduleState.index]
|
2025-09-08 20:53:24 +08:00
|
|
|
|
const elapsed = now - scheduleState.startTime
|
2025-11-11 15:13:03 +08:00
|
|
|
|
console.log('已调度', elapsed / 1000, '秒', cur.duration / 1000 + '秒后调度', printCurrentTime())
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
if (elapsed >= cur.duration) {
|
|
|
|
|
|
scheduleState.index = (scheduleState.index + 1) % schedulePlan.length
|
|
|
|
|
|
scheduleState.startTime = now
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
2025-11-10 14:36:15 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-09-08 20:53:24 +08:00
|
|
|
|
runTask(schedulePlan[scheduleState.index].key)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, scheduleTickMs)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-10 15:53:19 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
function forceActivate(key, runner) {
|
|
|
|
|
|
// 跳过互斥逻辑,直接切换
|
2025-10-28 19:41:05 +08:00
|
|
|
|
// runType.value = key;
|
2025-09-08 20:53:24 +08:00
|
|
|
|
if (typeof runner === 'function') runner();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTranslation(list) {
|
|
|
|
|
|
list.forEach((item, index) => {
|
|
|
|
|
|
translationToChinese({ msg: item.text }).then(res => {
|
|
|
|
|
|
console.log(res);
|
|
|
|
|
|
chatList.value[index].text = res
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onHostSaved(list) {
|
|
|
|
|
|
console.log('保存后的主播id:', list)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//当前时间获取
|
|
|
|
|
|
function printCurrentTime() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
return now.toLocaleString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 16:46:12 +08:00
|
|
|
|
|
|
|
|
|
|
function onSave(payload) {
|
|
|
|
|
|
console.log(payload)
|
|
|
|
|
|
aiConfig(payload).then((res) => {
|
|
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2025-09-20 13:31:06 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function isAiConfig() {
|
2025-10-10 15:53:19 +08:00
|
|
|
|
const res = await window.electronAPI.fileExists();
|
|
|
|
|
|
|
2025-09-20 13:31:06 +08:00
|
|
|
|
console.log(res);
|
|
|
|
|
|
return res.exists;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//金票
|
|
|
|
|
|
const gold = ref(true) // ON=执行
|
|
|
|
|
|
//普票
|
|
|
|
|
|
const ordinary = ref(true) // ON=执行
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function invitTypeFun(invitType, nextEnabled) {
|
|
|
|
|
|
// 子组件已传来“切换后的布尔值”
|
|
|
|
|
|
if (invitType === 'gold') {
|
|
|
|
|
|
gold.value = nextEnabled
|
|
|
|
|
|
} else if (invitType === 'ordinary') {
|
|
|
|
|
|
ordinary.value = nextEnabled
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 由邀请类型 + 当前全局开关,得到 state(0/1) */
|
|
|
|
|
|
function stateByInvType(invitationType) {
|
|
|
|
|
|
// 2=金票,其他=普票(按你的写法)
|
|
|
|
|
|
const enabled = invitationType == 2 ? gold.value : ordinary.value
|
|
|
|
|
|
return enabled ? 1 : 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//清空当前批次的小工具
|
|
|
|
|
|
function dropCurrentWave() {
|
|
|
|
|
|
// 丢弃当前这“波”已缓冲的数据 & 取消待 flush
|
|
|
|
|
|
batch.length = 0
|
|
|
|
|
|
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null }
|
|
|
|
|
|
// 复位本波的弹窗与模式标记
|
|
|
|
|
|
isMsgPop.value = false
|
2025-10-10 15:53:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function doLogout() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
dropCurrentWave()
|
|
|
|
|
|
// es?.close?.() // 如果 connectSSE 返回 EventSource,调用 close
|
|
|
|
|
|
refreshAllStopImgs() // 你已有:把所有 <img> src 清空
|
2025-11-18 18:05:29 +08:00
|
|
|
|
clearInterval(getListTimer.value)
|
|
|
|
|
|
getListTimer.value = null
|
2025-10-10 15:53:19 +08:00
|
|
|
|
await logout({ userId: userdata.id, tenantId: userdata.tenantId })
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
router.push('/')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-20 13:31:06 +08:00
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
//查看主播库主播信息
|
|
|
|
|
|
async function getStoredHostList() {
|
|
|
|
|
|
const v = await anchorList()
|
|
|
|
|
|
return v ? v : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let isWifi = ref(false);
|
|
|
|
|
|
const checkVPN = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 设置超时 5 秒钟
|
|
|
|
|
|
const timeout = new Promise((_, reject) =>
|
|
|
|
|
|
setTimeout(() => reject(new Error('请求超时')), 10000) // 10秒超时
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 Promise.race 来进行超时控制
|
|
|
|
|
|
const response = await Promise.race([
|
|
|
|
|
|
fetch('https://www.google.com', { method: 'HEAD', mode: 'no-cors' }),
|
|
|
|
|
|
timeout
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 判断 fetch 请求是否成功
|
|
|
|
|
|
if (response && response.type === 'opaque') {
|
|
|
|
|
|
// ElMessage.success('VPN连接正常!');
|
|
|
|
|
|
isWifi.value = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error('VPN连接失败,无法访问网络。');
|
|
|
|
|
|
isWifi.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 捕获超时错误或其他错误
|
|
|
|
|
|
ElMessage.error('VPN连接失败,无法访问网络。');
|
|
|
|
|
|
isWifi.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟翻译函数:你可以接入自己后端或第三方接口 sentences文本 targetLang语言
|
|
|
|
|
|
async function doTranslate(sentences, targetLang) {
|
|
|
|
|
|
const str = arrayToString(sentences)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await customTranslation({
|
|
|
|
|
|
"msg": str,
|
|
|
|
|
|
"language": targetLang
|
|
|
|
|
|
})
|
|
|
|
|
|
console.log(response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 1️⃣ 去掉首尾的大括号
|
|
|
|
|
|
const raw = response.replace(/^{|}$/g, '')
|
|
|
|
|
|
|
|
|
|
|
|
// 2️⃣ 按换行符切割为数组
|
|
|
|
|
|
const arr = raw.split('\n').map(s => s.trim()).filter(Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
console.log(arr)
|
|
|
|
|
|
// 简单演示逻辑(真实情况应调用 API)
|
|
|
|
|
|
return arr
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Translation error:', error)
|
|
|
|
|
|
// 发生错误时返回空数组
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 接收“确定”事件返回结果
|
2025-11-10 14:36:15 +08:00
|
|
|
|
function onConfirm({ type, strings, autoBlo, needTranslate }) {
|
|
|
|
|
|
console.log('✅ 确认返回:', type, strings, autoBlo, needTranslate)
|
2025-10-29 19:45:51 +08:00
|
|
|
|
auto.value = autoBlo
|
2025-11-10 14:36:15 +08:00
|
|
|
|
isTranslate.value = needTranslate
|
2025-10-28 19:41:05 +08:00
|
|
|
|
showtransDlg.value = false
|
|
|
|
|
|
runType.value = 'follow'
|
|
|
|
|
|
setContentpriList(strings)
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
//过滤无网络设备
|
|
|
|
|
|
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)))
|
|
|
|
|
|
|
2025-10-28 19:41:05 +08:00
|
|
|
|
if (isAlliance.value) {
|
|
|
|
|
|
followAndGreetUnion(
|
|
|
|
|
|
{
|
2025-11-10 14:36:15 +08:00
|
|
|
|
deviceList: deviceIds,
|
2025-10-28 19:41:05 +08:00
|
|
|
|
anchorList: hostList.map(item => ({
|
|
|
|
|
|
anchorId: item.id,
|
|
|
|
|
|
country: item.country,
|
|
|
|
|
|
invitationType: item.invitationType,
|
|
|
|
|
|
state: stateByInvType(item.invitationType),
|
|
|
|
|
|
})),
|
|
|
|
|
|
prologueList: strings,
|
2025-10-29 19:45:51 +08:00
|
|
|
|
needReply: autoBlo,
|
2025-11-10 14:36:15 +08:00
|
|
|
|
needTranslate: needTranslate,
|
2025-10-28 19:41:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
).then((res) => {
|
|
|
|
|
|
ElMessage({ type: 'success', message: '任务开启成功' });
|
|
|
|
|
|
|
|
|
|
|
|
hostList = []
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
passAnchorData(
|
|
|
|
|
|
{
|
2025-11-10 14:36:15 +08:00
|
|
|
|
deviceList: deviceIds,
|
2025-10-28 19:41:05 +08:00
|
|
|
|
anchorList: hostList.map(item => ({
|
|
|
|
|
|
anchorId: item.id,
|
|
|
|
|
|
country: item.country,
|
|
|
|
|
|
invitationType: item.invitationType,
|
|
|
|
|
|
state: stateByInvType(item.invitationType),
|
|
|
|
|
|
})),
|
|
|
|
|
|
prologueList: strings, //私信对象
|
|
|
|
|
|
comment: comonList, //评论列表
|
2025-10-29 19:45:51 +08:00
|
|
|
|
needReply: autoBlo, //自动回复
|
2025-11-10 14:36:15 +08:00
|
|
|
|
needTranslate: needTranslate, //翻译
|
2025-10-28 19:41:05 +08:00
|
|
|
|
isComment: common.value //是否评论
|
|
|
|
|
|
}
|
|
|
|
|
|
).then((res) => {
|
|
|
|
|
|
ElMessage({ type: 'success', message: '任务开启成功' });
|
|
|
|
|
|
console.log("启动成功")
|
|
|
|
|
|
hostList = []
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function arrayToString(arr) {
|
|
|
|
|
|
// 过滤空项并用 \n 连接
|
|
|
|
|
|
return arr.filter(Boolean).join(' \n')
|
|
|
|
|
|
}
|
2025-10-31 19:39:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 每台设备请求间隔(毫秒)
|
|
|
|
|
|
const REQ_GAP_MS = 5000
|
|
|
|
|
|
|
2025-10-31 19:39:57 +08:00
|
|
|
|
async function refreshNetStatus() {
|
|
|
|
|
|
const list = [...(deviceInformation.value || [])]
|
2025-11-10 14:36:15 +08:00
|
|
|
|
if (!list.length) return
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-11-18 18:05:29 +08:00
|
|
|
|
firstPass[d.deviceId] = true
|
2025-11-10 14:36:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 不是最后一台才等待 5s
|
|
|
|
|
|
if (i < list.length - 1) {
|
|
|
|
|
|
await new Promise(r => setTimeout(r, REQ_GAP_MS))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-31 19:39:57 +08:00
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 二次复核
|
|
|
|
|
|
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 }
|
|
|
|
|
|
})
|
2025-10-31 19:39:57 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
const effective = { ...firstPass }
|
|
|
|
|
|
confirmResults.forEach(({ id, stableOffline }) => {
|
|
|
|
|
|
effective[id] = !stableOffline
|
2025-10-31 19:39:57 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 写回 UI
|
|
|
|
|
|
Object.keys(effective).forEach(id => { netStatus[id] = effective[id] })
|
|
|
|
|
|
console.log('设备在线状态(有效值)', effective)
|
|
|
|
|
|
|
|
|
|
|
|
// 清理已下线设备
|
2025-10-31 19:39:57 +08:00
|
|
|
|
const aliveIds = new Set(list.map(d => d.deviceId))
|
|
|
|
|
|
Object.keys(netStatus).forEach(id => {
|
|
|
|
|
|
if (!aliveIds.has(id)) {
|
|
|
|
|
|
delete netStatus[id]
|
|
|
|
|
|
delete lastNet[id]
|
|
|
|
|
|
delete offlineFlags[id]
|
|
|
|
|
|
clearResumeTimer(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
// 任务联动(保持原逻辑)
|
2025-10-31 19:39:57 +08:00
|
|
|
|
if (runType.value) {
|
|
|
|
|
|
list.forEach(({ deviceId: id }) => {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
const prev = lastNet[id]
|
|
|
|
|
|
const curr = netStatus[id]
|
2025-10-31 19:39:57 +08:00
|
|
|
|
if (typeof curr === 'undefined') return
|
|
|
|
|
|
|
|
|
|
|
|
if (prev !== curr) {
|
|
|
|
|
|
if (curr === false) {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
clearResumeTimer(id)
|
2025-10-31 19:39:57 +08:00
|
|
|
|
if (!offlineFlags[id]) {
|
2025-11-10 14:36:15 +08:00
|
|
|
|
console.log(`[NET] ${id} 稳定离线,停止该设备任务`)
|
2025-10-31 19:39:57 +08:00
|
|
|
|
offlineFlags[id] = true
|
|
|
|
|
|
stopOne(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (curr === true && offlineFlags[id]) {
|
|
|
|
|
|
clearResumeTimer(id)
|
2025-11-10 14:36:15 +08:00
|
|
|
|
const t = setTimeout(() => {
|
2025-10-31 19:39:57 +08:00
|
|
|
|
if (runType.value && netStatus[id] === true) {
|
|
|
|
|
|
console.log(`[NET] ${id} 恢复在线,重新启动当前模式: ${runType.value}`)
|
2025-11-10 14:36:15 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-10-31 19:39:57 +08:00
|
|
|
|
runTask(runType.value, id)
|
|
|
|
|
|
offlineFlags[id] = false
|
|
|
|
|
|
}
|
|
|
|
|
|
resumeTimers.delete(id)
|
|
|
|
|
|
}, NET_RESUME_STABLE_MS)
|
2025-11-10 14:36:15 +08:00
|
|
|
|
resumeTimers.set(id, t)
|
2025-10-31 19:39:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastNet[id] = curr
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Object.keys(offlineFlags).forEach(id => { offlineFlags[id] = false })
|
|
|
|
|
|
Array.from(resumeTimers.keys()).forEach(id => clearResumeTimer(id))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 14:36:15 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 新增:二次复核断网所需的小工具 =====
|
|
|
|
|
|
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 {
|
2025-11-18 18:05:29 +08:00
|
|
|
|
return true
|
2025-11-10 14:36:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 返回 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-11-18 18:05:29 +08:00
|
|
|
|
|
|
|
|
|
|
const onToggleCrawler = (val) => {
|
|
|
|
|
|
if (val) sseBossEnabled.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
const onToggleBoss = (val) => {
|
|
|
|
|
|
if (val) sseCrawlerEnabled.value = false
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
|
@import '../static/css/video.less';
|
|
|
|
|
|
</style>
|