上线版本
This commit is contained in:
@@ -4,13 +4,6 @@
|
||||
<div style="position: absolute;left: 20px; top: 20px;">
|
||||
<el-button style="background: linear-gradient(90deg, #60a5fa, #34d399); color: azure;"
|
||||
@click="showMyInfo = true">人设编辑</el-button>
|
||||
|
||||
<!-- <el-button style="background: linear-gradient(90deg, #60a5fa, #34d399); color: azure;" @click="MesNewList.push({
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
})">新增</el-button> -->
|
||||
</div>
|
||||
|
||||
<div class="center-line"> <!-- 左边栏按钮 -->
|
||||
@@ -35,8 +28,7 @@
|
||||
<div class="video-container" v-for="(device, index) in deviceInformation" :key="device.deviceId">
|
||||
<div class="video-canvas" :class="{ active: selectedDevice === index }" :style="getCanvasStyle(index)"
|
||||
@click="selectDevice(index)">
|
||||
<img class="stream" :key="device.deviceId + '-' + imgKeyTick"
|
||||
:src="`http://localhost:${device.screenPort}/?t=${Date.now() || 0}`" :data-id="device.deviceId"
|
||||
<img class="stream" :src="imgSrcMap[device.deviceId] || ''" :data-id="device.deviceId"
|
||||
:ref="el => (imgRefs[device.deviceId] = el)" />
|
||||
|
||||
<canvas v-show="selectedDevice === index" class="overlay"
|
||||
@@ -47,28 +39,9 @@
|
||||
<div class="input-info" v-show="selectedDevice == index">
|
||||
|
||||
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
|
||||
<div class="app-button" @click="stopScript({ udid: 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="mqSend()">mq</div> -->
|
||||
<!-- <div class="app-button"
|
||||
@click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true; istranslate = true">
|
||||
翻译本页对话</div>
|
||||
<div class="app-button" @click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true;">
|
||||
回复消息</div>
|
||||
<div class="app-button" @click="wsActions.test(device.deviceId, index)">打印ui节点树</div>
|
||||
<div class="app-button" @click="wsActions.isOneLive(device.deviceId, index)">判断单人还是双人</div>
|
||||
<div class="app-button" @click="wsActions.slideDown(device.deviceId, index)">下滑</div>
|
||||
<div class="app-button" @click="wsActions.killNow(device.deviceId, index)">关闭当前应用</div>
|
||||
<div class="app-button" @click="chooseFile(device.deviceId, index, 1, wsActions)">安装 APK
|
||||
文件</div>
|
||||
<div class="app-button" @click="chooseFile(device.deviceId, index, 2, wsActions)">
|
||||
传送文件</div> -->
|
||||
<!-- <div style="display: flex;">
|
||||
<input style="border: 1px solid #000;margin:0px 14px;" v-model="textContent[index]" type="text"></input>
|
||||
<div class="app-button" style="margin: 0px;height: 40px;width: 60px;font-size: 14px;"
|
||||
@click="setComText(index)">发送
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,8 +50,8 @@
|
||||
<ChatDialog :visible="openShowChat" :messages="chatList" />
|
||||
<MessageDialogd :visible="openShowChat" :messages="MesNewList" :sound-src="ding" />
|
||||
</div>
|
||||
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
|
||||
@confirm="onDialogConfirm" @cancel="stopAll" />
|
||||
<MultiLineInputDialog v-model:visible="showDialog" :initialText='initialTextStr' :title="dialogTitle"
|
||||
:index="selectedDevice" @confirm="onDialogConfirm" @cancel="stopAll" />
|
||||
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" @invitType="invitTypeFun" />
|
||||
|
||||
</div>
|
||||
@@ -90,12 +63,12 @@
|
||||
contact: borkerConfig.contact
|
||||
}" @save="onSave" />
|
||||
<!-- 定时调度配置弹窗 -->
|
||||
<el-dialog v-model="showScheduleDlg" title="定时调度(每小时)" width="420px">
|
||||
<el-dialog v-model="showScheduleDlg" title="定时调度(每小时)" width="550px" :close-on-click-modal="false" draggable>
|
||||
<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;">
|
||||
<el-option label="一键关注" value="follow" />
|
||||
<el-option label="一键私信" value="follow" />
|
||||
<el-option label="刷视频(养号)" value="like" />
|
||||
<el-option label="刷直播" value="brushLive" />
|
||||
<el-option label="监测消息" value="listen" />
|
||||
@@ -119,8 +92,18 @@
|
||||
<div>总时长</div>
|
||||
<div><b>{{ schedAMin + schedBMin }}</b> 分钟(必须等于 60)</div>
|
||||
|
||||
<!-- <div>启用调度</div>
|
||||
<div><el-switch v-model="scheduleEnabled" /></div> -->
|
||||
<!-- <div>换号</div>
|
||||
<div style="display:flex; gap:8px; align-items:center;">
|
||||
<el-switch v-model="interruptEnabled" active-text="开启换号" />
|
||||
<el-input-number v-model="interruptEveryMin" :min="1" :max="180" />
|
||||
<span>分钟换一次</span>
|
||||
</div> -->
|
||||
<div>联盟号</div>
|
||||
<div style="display:flex; gap:8px; align-items:center;">
|
||||
<el-switch v-model="isAlliance" active-text="联盟号快速私信" />
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -129,8 +112,6 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -140,7 +121,7 @@ import {
|
||||
setphoneXYinfo, getphoneXYinfo, getUser,
|
||||
getHostList, setHostList, getContentpriList,
|
||||
setContentpriList, getContentList, setContentList,
|
||||
setsessionId, getsessionId
|
||||
setsessionId, getsessionId, getContentpriListMultiline
|
||||
} from '@/stores/storage'
|
||||
import { connectSSE } from '@/utils/sseUtils'
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
||||
@@ -163,6 +144,7 @@ import {
|
||||
watchLiveForGrowth,
|
||||
monitorMessages,
|
||||
passAnchorData,
|
||||
followAndGreetUnion,
|
||||
addTempAnchorData,
|
||||
deviceAppList,
|
||||
launchApp,
|
||||
@@ -179,7 +161,7 @@ const router = useRouter();
|
||||
const openShowChat = ref(true)
|
||||
//主播库
|
||||
const showHostDlg = ref(false)
|
||||
//ai人设
|
||||
//ai人设弹框
|
||||
const showMyInfo = ref(false)
|
||||
// 假设这是你已有的数据
|
||||
const borkerConfig = reactive({
|
||||
@@ -189,6 +171,7 @@ const borkerConfig = reactive({
|
||||
contact: ''
|
||||
})
|
||||
|
||||
let initialTextStr = ref('') // 初始文本字符串
|
||||
// 批次缓冲(仅用于当前“波”)
|
||||
let batch = []; // [{ country, text }]
|
||||
let flushTimer = null;
|
||||
@@ -201,15 +184,13 @@ let chatList = ref([])
|
||||
|
||||
let MesNewList = ref([])
|
||||
|
||||
// 引入刷新方法
|
||||
const reload = inject("reload")
|
||||
// 刷新方法
|
||||
const reloadImg = () => {
|
||||
refreshAllImgs()
|
||||
}
|
||||
//start弹窗
|
||||
let isMsgPop = ref(false)
|
||||
//缓存主播联动数组
|
||||
let stroageHost = ref([])
|
||||
|
||||
let runType = ref('')
|
||||
let isMonitorOn = ref(false)
|
||||
const hoverIndex = ref(null) //选中
|
||||
@@ -217,8 +198,6 @@ let showDialog = ref(false);//弹窗是否显示
|
||||
let dialogTitle = ref('');//当前弹窗类型
|
||||
let deviceInformation = ref([])
|
||||
// 你可以用这种方式声明按钮们
|
||||
//联动用作标记
|
||||
let batchMode = ref('init'); // 'init' | 'follow'(仅作标记)
|
||||
//停止中
|
||||
let stopLoading = null
|
||||
|
||||
@@ -229,6 +208,7 @@ const isLocked = (type) => !!runType.value && runType.value !== type
|
||||
const sseEnabled = ref(JSON.parse(localStorage.getItem('SSE_ENABLED') ?? 'true'))
|
||||
watch(sseEnabled, v => {
|
||||
localStorage.setItem('SSE_ENABLED', JSON.stringify(v))
|
||||
if (!v) dropCurrentWave() // 关掉总开关时,立刻丢弃本波缓冲并取消待flush
|
||||
})
|
||||
|
||||
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
|
||||
@@ -319,31 +299,7 @@ const buttons = [
|
||||
},
|
||||
style: () => ctrlStyle('like')
|
||||
},
|
||||
// {
|
||||
// label: '一键关注并打招呼',
|
||||
// onClick: () => {
|
||||
|
||||
// if (runType.value == 'follow') {
|
||||
// deviceInformation.value.forEach((item) => {
|
||||
// stopScript({ udid: item.deviceId })
|
||||
// })
|
||||
// runType.value = ''
|
||||
// return
|
||||
// };
|
||||
|
||||
// if (isLocked('follow')) return
|
||||
// showDialog.value = true;
|
||||
// dialogTitle.value = '主播ID';
|
||||
|
||||
// },
|
||||
// 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: '启动调度任务',
|
||||
onClick: () => openScheduleDialog(),
|
||||
@@ -399,13 +355,7 @@ const buttons = [
|
||||
},
|
||||
{
|
||||
label: '登出',
|
||||
onClick: () => {
|
||||
refreshAllStopImgs() // 停止所有视频流
|
||||
logout({ userId: userdata.id, tenantId: userdata.tenantId })
|
||||
|
||||
router.push('/')
|
||||
|
||||
},
|
||||
onClick: () => doLogout(),
|
||||
show: () => true,
|
||||
img: {
|
||||
normal: new URL('@/assets/video/leftBtn9.png', import.meta.url).href,
|
||||
@@ -413,10 +363,34 @@ const buttons = [
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const isAlliance = ref(false)
|
||||
|
||||
|
||||
// —— 打断器配置 ——
|
||||
// 是否开启
|
||||
const interruptEnabled = ref(false)
|
||||
// 每隔多少分钟打断一次(可做弹窗配置)
|
||||
const interruptEveryMin = ref(15)
|
||||
// 打断器最大重试次数 & 每次超时(按需调)
|
||||
const interruptMaxRetries = 3
|
||||
const interruptCallTimeoutMs = 120_000
|
||||
|
||||
// 运行态
|
||||
let interrupting = false
|
||||
let lastInterruptTs = Number(localStorage.getItem('INT_LAST_TS') || '0')
|
||||
|
||||
// 暂存“被打断时”的片段状态,用于恢复
|
||||
let pauseSnapshot = null
|
||||
// 结构:{ index, elapsedBeforePause }
|
||||
|
||||
|
||||
|
||||
const schedulePlan = [
|
||||
{ key: 'follow', duration: 40 * 60 * 1000 },
|
||||
{ key: 'like', duration: 20 * 60 * 1000 },
|
||||
]
|
||||
|
||||
// 调度状态(持久化一下,避免刷新丢失)
|
||||
let scheduleState = (() => {
|
||||
try {
|
||||
@@ -486,23 +460,101 @@ const selectedDevice = ref(null)
|
||||
|
||||
// 每台设备的 <img> 引用
|
||||
const imgRefs = ref({}) // { [id]: HTMLImageElement }
|
||||
const imgTs = ref({}) // { [id]: number } // cache-bust
|
||||
const imgKeyTick = ref(0) // 强制重建 <img>
|
||||
|
||||
function refreshOneImg(id, port) {
|
||||
const el = imgRefs.value[id]
|
||||
console.log("终止", id)
|
||||
// 新增:每台设备当前展示的 URL
|
||||
const imgSrcMap = reactive({}) // { [deviceId]: string }
|
||||
|
||||
// 新增:每台设备循环状态(timer/abort 等)
|
||||
const loops = new Map(); // deviceId -> { timer, stopped, lastUrl }
|
||||
|
||||
/** 生成一次地址 */
|
||||
const makeUrl = (port) => `http://localhost:${port}/?t=${Date.now()}`;
|
||||
|
||||
/** 真正断开某台设备当前连接并清理 */
|
||||
function hardCloseImg(deviceId) {
|
||||
const el = imgRefs.value[deviceId];
|
||||
if (el) {
|
||||
// 硬中断旧请求
|
||||
el.src = ''
|
||||
el.removeAttribute('src')
|
||||
el.src = '';
|
||||
el.removeAttribute('src');
|
||||
}
|
||||
// 下一拍再重建
|
||||
nextTick(() => {
|
||||
imgTs.value[id] = Date.now()
|
||||
imgKeyTick.value++ // 触发 key 变化 -> 销毁并新建 <img>
|
||||
})
|
||||
// 如果你用的是 blob/objectURL,这里应该 revokeObjectURL;我们当前直接 URL,不需要。
|
||||
imgSrcMap[deviceId] = '';
|
||||
}
|
||||
|
||||
/** 只启动某台设备的 3 秒循环(第 idx 台错峰 idx*200ms) */
|
||||
function startLoop(dev, idx = 0) {
|
||||
stopLoop(dev.deviceId); // 防止重复开
|
||||
const state = { timer: null, stopped: false, lastUrl: '' };
|
||||
loops.set(dev.deviceId, state);
|
||||
|
||||
const tick = () => {
|
||||
if (state.stopped) return;
|
||||
|
||||
// 1) 先把老连接硬断开
|
||||
// hardCloseImg(dev.deviceId);
|
||||
|
||||
// 2) 立刻换新地址
|
||||
const url = makeUrl(dev.screenPort);
|
||||
imgSrcMap[dev.deviceId] = url;
|
||||
|
||||
// 3) 3 秒后再来一轮(串行,不并发)
|
||||
state.timer = window.setTimeout(tick, 3000);
|
||||
};
|
||||
|
||||
// 错峰启动,避免 N 台同时一口气重连
|
||||
state.timer = window.setTimeout(tick, idx * 200);
|
||||
}
|
||||
|
||||
/** 停止某台设备的循环并断开连接 */
|
||||
function stopLoop(deviceId) {
|
||||
const s = loops.get(deviceId);
|
||||
if (!s) {
|
||||
hardCloseImg(deviceId); // 也确保断一次
|
||||
return;
|
||||
}
|
||||
s.stopped = true;
|
||||
if (s.timer) clearTimeout(s.timer);
|
||||
loops.delete(deviceId);
|
||||
hardCloseImg(deviceId);
|
||||
}
|
||||
|
||||
|
||||
// —— 设备列表变化时,同步循环 ——
|
||||
// 注意:你的 deviceInformation 每 3 秒会被重新赋值。
|
||||
// 这里不每次都重启,而是只做“增删对齐”,避免不必要中断。
|
||||
watch(deviceInformation, (list) => {
|
||||
reconcileLoopsByDevices(list || []);
|
||||
}, { deep: true });
|
||||
|
||||
|
||||
/** 批量控制:根据 deviceInformation 启停循环(新增启动,移除停止) */
|
||||
function reconcileLoopsByDevices(list) {
|
||||
const keep = new Set(list.map(d => d.deviceId));
|
||||
|
||||
// 停掉已不存在的设备
|
||||
for (const id of Array.from(loops.keys())) {
|
||||
if (!keep.has(id)) stopLoop(id);
|
||||
}
|
||||
// 为新增设备启动循环(带错峰)
|
||||
list.forEach((d, i) => {
|
||||
if (!loops.has(d.deviceId)) startLoop(d, i);
|
||||
});
|
||||
}// 强制重建 <img>
|
||||
|
||||
// 如果你还有“手动刷新”按钮,改成只刷新当前/所有设备的一轮
|
||||
function refreshOneImg(deviceId) {
|
||||
// 立即强制进入下一轮:先停后启
|
||||
const dev = deviceInformation.value.find(d => d.deviceId === deviceId);
|
||||
if (dev) {
|
||||
stopLoop(deviceId);
|
||||
startLoop(dev, 0);
|
||||
}
|
||||
}
|
||||
function refreshAllImgs() {
|
||||
deviceInformation.value.forEach((d, i) => {
|
||||
stopLoop(d.deviceId);
|
||||
startLoop(d, i);
|
||||
});
|
||||
}
|
||||
|
||||
// —— 关键:登出/离开时“批量硬中断”所有图片流 ——
|
||||
@@ -520,9 +572,7 @@ async function hardStopAllImgStreams(id, port) {
|
||||
|
||||
|
||||
|
||||
function refreshAllImgs() {
|
||||
Object.keys(imgRefs.value).forEach(id => refreshOneImg(id))
|
||||
}
|
||||
|
||||
function refreshAllStopImgs() {
|
||||
Object.keys(imgRefs.value).forEach(id => hardStopAllImgStreams(id))
|
||||
}
|
||||
@@ -754,15 +804,18 @@ async function stopAll() {
|
||||
// 所有操作完成后执行以下代码
|
||||
scheduleEnabled.value = false
|
||||
runType.value = ''
|
||||
batchMode.value = 'init' // 初始化状态 关注状态
|
||||
isMsgPop.value = false; //弹窗状态(不是弹窗)
|
||||
dropCurrentWave() // 丢弃当前波的残留缓冲
|
||||
|
||||
try {
|
||||
const res = await stopAllTask(deviceInformation.value.map((item) => item.deviceId))
|
||||
console.log(`全部停止成功`, printCurrentTime())
|
||||
ElMessage.success(`全部停止成功`)
|
||||
stopLoading.close()
|
||||
stopAllTask(deviceInformation.value.map((item) => item.deviceId)).then((res) => {
|
||||
setTimeout(() => {
|
||||
stopLoading.close()
|
||||
console.log(`全部停止成功`, printCurrentTime())
|
||||
ElMessage.success(`全部停止成功`)
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.log(`停止失败`, printCurrentTime())
|
||||
ElMessage.error(`脚本已停止`)
|
||||
@@ -775,50 +828,94 @@ async function stopAll() {
|
||||
//确认多行文本框内容
|
||||
function onDialogConfirm(result, type, index, isMon) {
|
||||
// console.log(type, result, isMon);
|
||||
console.log(type)
|
||||
if (type == '主播ID') {
|
||||
hostList = (result || []).map(id => ({ id, country: '' }))
|
||||
|
||||
dialogTitle.value = '私信';
|
||||
setTimeout(() => {
|
||||
showDialog.value = true;
|
||||
}, 600)
|
||||
|
||||
initialTextStr.value = getContentpriListMultiline();
|
||||
}, 500)
|
||||
} else if (type == '私信') {
|
||||
runType.value = 'follow'
|
||||
setContentpriList(result)
|
||||
console.log('hostList', hostList)
|
||||
passAnchorData(
|
||||
{
|
||||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||
anchorList: hostList.map(item => ({
|
||||
anchorId: item.id,
|
||||
country: item.country,
|
||||
invitationType: item.invitationType,
|
||||
state: stateByInvType(item.invitationType),
|
||||
})),
|
||||
prologueList: result,
|
||||
needReply: isMon
|
||||
}
|
||||
).then((res) => {
|
||||
ElMessage({ type: 'success', message: '任务开启成功' });
|
||||
// console.log('hostList', hostList)
|
||||
|
||||
if (isAlliance.value) {
|
||||
followAndGreetUnion(
|
||||
{
|
||||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||
anchorList: hostList.map(item => ({
|
||||
anchorId: item.id,
|
||||
country: item.country,
|
||||
invitationType: item.invitationType,
|
||||
state: stateByInvType(item.invitationType),
|
||||
})),
|
||||
prologueList: result,
|
||||
needReply: isMon
|
||||
}
|
||||
).then((res) => {
|
||||
ElMessage({ type: 'success', message: '任务开启成功' });
|
||||
|
||||
hostList = []
|
||||
})
|
||||
} else {
|
||||
passAnchorData(
|
||||
{
|
||||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||
anchorList: hostList.map(item => ({
|
||||
anchorId: item.id,
|
||||
country: item.country,
|
||||
invitationType: item.invitationType,
|
||||
state: stateByInvType(item.invitationType),
|
||||
})),
|
||||
prologueList: result,
|
||||
needReply: isMon
|
||||
}
|
||||
).then((res) => {
|
||||
ElMessage({ type: 'success', message: '任务开启成功' });
|
||||
|
||||
hostList = []
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
hostList = []
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: 'Loading',
|
||||
text: '检测设备中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
});
|
||||
getDeviceListFun()
|
||||
|
||||
|
||||
const res = await window.electronAPI.isiproxy({
|
||||
intervalMs: 2000,
|
||||
exeName: 'iproxy.exe',
|
||||
maxWaitMs: 300000, // 可选:5分钟超时
|
||||
});
|
||||
if (res.running) {
|
||||
// 检测到了,你再决定是否跳转
|
||||
loading.close();
|
||||
console.log('检测到了')
|
||||
} else {
|
||||
// 超时兜底提示
|
||||
loading.close();
|
||||
console.log('未检测到设备')
|
||||
ElMessage.error(`未检测到设备`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
window.electronAPI.startMq(userdata.tenantId, userdata.id)
|
||||
// 初始化时获取设备列表
|
||||
getListtimer = setInterval(() => {
|
||||
loading.close();
|
||||
getDeviceListFun()
|
||||
selectLastFun()
|
||||
|
||||
@@ -827,8 +924,7 @@ onMounted(async () => {
|
||||
if (!await isAiConfig()) {
|
||||
showMyInfo.value = true
|
||||
}
|
||||
|
||||
|
||||
reconcileLoopsByDevices(deviceInformation.value || []);
|
||||
|
||||
function scheduleFlush(handler, delay = 400) {
|
||||
if (flushTimer) clearTimeout(flushTimer);
|
||||
@@ -851,110 +947,33 @@ onMounted(async () => {
|
||||
// console.log('来自服务端:', data);
|
||||
// console.log(1)
|
||||
//总开关
|
||||
// 总开关关闭:不弹窗、丢弃所有消息
|
||||
if (!sseEnabled.value) {
|
||||
if (data === 'start') {
|
||||
// 开始一个新波时顺便清空(保险起见)
|
||||
dropCurrentWave()
|
||||
}
|
||||
return // 其余任何数据也直接丢弃
|
||||
}
|
||||
|
||||
if (!sseEnabled.value) return
|
||||
|
||||
if (data === 'start') {
|
||||
console.log(2)
|
||||
|
||||
// 新一波开始:根据当前状态决定“本波 flush 用谁”
|
||||
if (!isMsgPop.value) {
|
||||
console.log(3)
|
||||
|
||||
// 还没开始过 -> 首次弹框,确认后使用处理本波
|
||||
isMsgPop.value = true;
|
||||
batchMode.value = 'init';
|
||||
ElMessageBox.confirm(
|
||||
'检测到YOLO助手正在爬取主播,是否进行操作?',
|
||||
'消息提醒',
|
||||
{ confirmButtonText: '开始', cancelButtonText: '取消', type: 'success' }
|
||||
)
|
||||
.then(() => {
|
||||
// runType.value = 'follow';
|
||||
batchMode.value = 'follow';
|
||||
|
||||
// 不在这里立刻提交;让后续主播数据先进 batch,再由防抖统一 flush
|
||||
// 不直接发;把这“一波”的主播先塞进 hostList,然后弹出“私信”输入框
|
||||
scheduleFlush((items) => {
|
||||
//1普票 2金票 invitationType
|
||||
hostList = (items || []).map(h => ({ id: h.text, country: h.country || '', invitationType: h.invitationType }))
|
||||
|
||||
})
|
||||
setTimeout(() => {
|
||||
showScheduleDlg.value = true
|
||||
}, 600)
|
||||
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消:清理状态,丢弃批次
|
||||
batch.length = 0;
|
||||
isMsgPop.value = false;
|
||||
});
|
||||
} else {
|
||||
// 已经在运行 follow:本波用 addTempAnchorData 追加
|
||||
batchMode.value = 'follow';
|
||||
// 立刻安排一次“尾随防抖”flush,等本波数据齐了再送
|
||||
scheduleFlush((items) => {
|
||||
// 这里 items 元素是 h
|
||||
const list = items.map(h => ({
|
||||
anchorId: h.text,
|
||||
country: h.country || '',
|
||||
invitationType: h.invitationType,
|
||||
state: stateByInvType(h.invitationType)
|
||||
}))
|
||||
addTempAnchorData(list)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 非 start:本波主播数据进入批次
|
||||
const country = data && data.country != null ? data.country : '';
|
||||
const text = data && (data.hostsId != null ? data.hostsId : data.text);
|
||||
const invitationType = data && (data.invitationType != null ? data.invitationType : '');
|
||||
if (text == null) {
|
||||
// 数据格式不对,丢弃或打印
|
||||
console.warn('[SSE] 非法数据,缺少 hostsId/text:', data);
|
||||
return;
|
||||
}
|
||||
batch.push({ country, text, invitationType });
|
||||
|
||||
// 根据当前模式,刷新防抖(让“最后一条到来后”延迟几百毫秒再统一提交)
|
||||
if (batchMode.value === 'init') {
|
||||
// 首次确认前:等用户点“开始”后由上面的 scheduleFlush 执行
|
||||
scheduleFlush((items) => {
|
||||
// 安全起见:只有在 runType 已经 follow 时才真正提交
|
||||
if (runType.value === 'follow') {
|
||||
passAnchorData({
|
||||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||
anchorList: items.map(h => ({ anchorId: h.text, country: h.country || '', invitationType: h.invitationType, state: stateByInvType(h.invitationType) })),
|
||||
needReply: false
|
||||
});
|
||||
} else {
|
||||
// 还没开始就来了数据:把它们留回批次,等待上面 then 里的 scheduleFlush 再处理
|
||||
batch.push(...items);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 已在关注:走追加逻辑
|
||||
scheduleFlush((items) => {
|
||||
// 这里 items 元素是 h
|
||||
const list = items.map(h => ({
|
||||
anchorId: h.text,
|
||||
country: h.country || '',
|
||||
invitationType: h.invitationType,
|
||||
state: stateByInvType(h.invitationType)
|
||||
}))
|
||||
addTempAnchorData(list)
|
||||
});
|
||||
}
|
||||
// 新一波开始:清空上一波的缓冲,重置防抖
|
||||
dropCurrentWave()
|
||||
return
|
||||
}
|
||||
});
|
||||
|
||||
// 非 start:正常入缓冲 → 防抖批量 addTempAnchorData
|
||||
const country = data && data.country != null ? data.country : ''
|
||||
const text = data && (data.hostsId != null ? data.hostsId : data.text)
|
||||
const invitationType = data && (data.invitationType != null ? data.invitationType : '')
|
||||
if (!text) return
|
||||
|
||||
batch.push({ country, text, invitationType })
|
||||
|
||||
scheduleFlush((items) => {
|
||||
// 批量入库
|
||||
const list = items.map(h => ({
|
||||
anchorId: h.text,
|
||||
country: h.country || '',
|
||||
invitationType: h.invitationType,
|
||||
state: stateByInvType(h.invitationType),
|
||||
}))
|
||||
addTempAnchorData(list)
|
||||
}, 400)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
@@ -1065,8 +1084,7 @@ function runTask(key, deviceId) {
|
||||
|
||||
forceActivate(key, async () => {
|
||||
if (key === 'follow') {
|
||||
|
||||
|
||||
console.log("进入follow", scheduleEnabled.value)
|
||||
if (scheduleEnabled.value) {
|
||||
if (!deviceId) {
|
||||
await stopAll()
|
||||
@@ -1081,6 +1099,7 @@ function runTask(key, deviceId) {
|
||||
).then((res) => {
|
||||
hostList = []
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1103,11 +1122,8 @@ function runTask(key, deviceId) {
|
||||
}
|
||||
|
||||
scheduleEnabled.value = true
|
||||
if (batchMode.value == 'init') {
|
||||
dialogTitle.value = '主播ID';
|
||||
} else {
|
||||
dialogTitle.value = '私信';
|
||||
}
|
||||
initialTextStr.value = '';
|
||||
dialogTitle.value = '主播ID';
|
||||
showDialog.value = true;
|
||||
|
||||
} else if (key === 'like') {
|
||||
@@ -1158,43 +1174,132 @@ function runTask(key, deviceId) {
|
||||
})
|
||||
}
|
||||
|
||||
async function stopCurrentMode() {
|
||||
// 如果你希望“只停当前片段的设备”,也可以用 stopScript 针对设备循环
|
||||
await stopAll()
|
||||
}
|
||||
|
||||
/** 恢复:回到 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))
|
||||
|
||||
// 确保任务是该片段
|
||||
runTask(schedulePlan[scheduleState.index].key)
|
||||
pauseSnapshot = null
|
||||
}
|
||||
|
||||
async function runInterrupterOnce() {
|
||||
// TODO: 换成你的真实调用,比如:替换
|
||||
// const ok = await someApi({ ... })
|
||||
// return !!ok
|
||||
|
||||
// 示例:包装一个带超时的 Promise;把你的方法放到 promiseFn 里
|
||||
const promiseFn = async () => {
|
||||
// 这里放你的真实逻辑
|
||||
changeAccount
|
||||
console.log('[换号] 正在执行...')
|
||||
// 比如 await doDailyCheck(); 然后返回 true/false
|
||||
return true
|
||||
}
|
||||
return await withTimeout(promiseFn(), interruptCallTimeoutMs).catch(() => false)
|
||||
}
|
||||
|
||||
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) })
|
||||
})
|
||||
}
|
||||
|
||||
function startScheduleLoop() {
|
||||
// 先按照当前 index 跑一次,保证“即刻对齐”
|
||||
lastInterruptTs = Date.now()
|
||||
localStorage.setItem('INT_LAST_TS', String(lastInterruptTs))
|
||||
// 先按当前 index 跑一次,保持“即刻对齐”
|
||||
runTask(schedulePlan[scheduleState.index].key)
|
||||
|
||||
// 清理旧轮询,防止重复
|
||||
if (scheduleTimer) clearInterval(scheduleTimer)
|
||||
|
||||
scheduleTimer = setInterval(() => {
|
||||
scheduleTimer = setInterval(async () => {
|
||||
// 关总调度就什么都不做
|
||||
if (!scheduleEnabled.value) return
|
||||
|
||||
// —— 先处理“打断器” ——
|
||||
if (interruptEnabled.value && !interrupting) {
|
||||
const now = Date.now()
|
||||
if (!lastInterruptTs) lastInterruptTs = now // 首次初始化
|
||||
|
||||
const due = now - lastInterruptTs >= interruptEveryMin.value * 60_000
|
||||
|
||||
console.log(
|
||||
'due=', due,
|
||||
'elapsed=', now - lastInterruptTs,
|
||||
'threshold=', interruptEveryMin.value * 60_000
|
||||
)
|
||||
if (due) {
|
||||
interrupting = true
|
||||
try {
|
||||
// 记录暂停前的进度(用于恢复)
|
||||
const cur = schedulePlan[scheduleState.index]
|
||||
const elapsedBeforePause = now - scheduleState.startTime
|
||||
pauseSnapshot = { index: scheduleState.index, elapsedBeforePause }
|
||||
|
||||
// 停掉当前片段
|
||||
await stopCurrentMode()
|
||||
|
||||
// 执行中断任务(带重试)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// —— 正常片段轮换(你原有的逻辑) ——
|
||||
const now = Date.now()
|
||||
const cur = schedulePlan[scheduleState.index]
|
||||
const elapsed = now - scheduleState.startTime
|
||||
|
||||
if (elapsed >= cur.duration) {
|
||||
// 进入下一个时间片
|
||||
scheduleState.index = (scheduleState.index + 1) % schedulePlan.length
|
||||
scheduleState.startTime = now
|
||||
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
||||
|
||||
runTask(schedulePlan[scheduleState.index].key)
|
||||
}
|
||||
}, scheduleTickMs)
|
||||
}
|
||||
|
||||
|
||||
function forceActivate(key, runner) {
|
||||
// 跳过互斥逻辑,直接切换
|
||||
runType.value = key;
|
||||
if (typeof runner === 'function') runner();
|
||||
}
|
||||
|
||||
|
||||
function mqSend() {
|
||||
window.electronAPI.mqSend("start")
|
||||
}
|
||||
|
||||
|
||||
function getTranslation(list) {
|
||||
list.forEach((item, index) => {
|
||||
translationToChinese({ msg: item.text }).then(res => {
|
||||
@@ -1207,21 +1312,7 @@ function getTranslation(list) {
|
||||
function onHostSaved(list) {
|
||||
console.log('保存后的主播id:', list)
|
||||
}
|
||||
// //sse接收爬虫发送的消息
|
||||
// const es = connectSSE(`http://localhost:3311/events`, (data) => {
|
||||
// // connectSSE(`http://192.168.1.155:19665/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
|
||||
// // 处理服务端推送的数据
|
||||
// console.log('来自服务端:', data)
|
||||
|
||||
// //接收到start
|
||||
// if (data === 'start') {
|
||||
|
||||
// } else {
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// })
|
||||
|
||||
//当前时间获取
|
||||
function printCurrentTime() {
|
||||
@@ -1240,7 +1331,8 @@ function onSave(payload) {
|
||||
|
||||
|
||||
async function isAiConfig() {
|
||||
const res = await window.electronAPI.fileExists("resources/iOSAI/data/aiConfig.json");
|
||||
const res = await window.electronAPI.fileExists();
|
||||
|
||||
console.log(res);
|
||||
return res.exists;
|
||||
}
|
||||
@@ -1278,7 +1370,36 @@ function dropCurrentWave() {
|
||||
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null }
|
||||
// 复位本波的弹窗与模式标记
|
||||
isMsgPop.value = false
|
||||
batchMode.value = 'init'
|
||||
}
|
||||
|
||||
|
||||
async function doLogout() {
|
||||
try {
|
||||
dropCurrentWave()
|
||||
// es?.close?.() // 如果 connectSSE 返回 EventSource,调用 close
|
||||
refreshAllStopImgs() // 你已有:把所有 <img> src 清空
|
||||
clearInterval(getListtimer)
|
||||
getListtimer = null
|
||||
await logout({ userId: userdata.id, tenantId: userdata.tenantId })
|
||||
} finally {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user