Files
tkAiPage/src/views/VideoStream.vue
2025-08-22 16:35:32 +08:00

1642 lines
62 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="main">
<el-scrollbar class="left"> <!-- 左边栏 -->
<LeftToolbar :buttons="buttons" :active-key="activeKey" :is-locked="isLocked" @click="handleBtnClick" />
<el-button style="position: absolute;left: 20px; bottom: 20px;" @click="showHostDlg = true">执行主播库</el-button>
</el-scrollbar>
<!-- 中间手机区域 -->
<div class="content" @click.self="selectedDevice = 999">
<div class="video-container" @click.self="selectedDevice = 999" v-for="(device, index) in deviceInformation"
:key="device.udid" @mouseup="(e) => handleCanvasup(device.udid, e, index)">
<div class="video-canvas">
<video :ref="(el) => (videoElement[device.udid] = el)" autoplay muted playsinline
:style="getVideoStyle(index)" @click.stop="selectedDevice = index"></video>
<canvas class="canvas" v-show="selectedDevice == index" :ref="(el) => (canvasRef[device.udid] = el)"
@mousemove.stop="(e) => handleMouseMove(device.udid, e, index)"
@mousedown.stop="(e) => handleCanvasdown(device.udid, e, index)" :style="{
transform: getTransformStyle(index)
}">
</canvas>
</div>
<div class="input-info" v-show="selectedDevice == index"
:style="{ left: phone.width * 1.4 + 4 + '0px', transform: getTransformStyle(index), }">
<div class="app-button" @click="resetApp(device.udid, index)">重置应用</div>
<div class="app-button"
@click="wsActions.clickCopyList(device.udid, index); openShowChat = true; istranslate = true">
翻译本页对话</div>
<div class="app-button" @click="wsActions.clickCopyList(device.udid, index); openShowChat = true;">
回复消息</div>
<!-- <div class="app-button" @click="wsActions.getSize(device.udid, index)">获取屏幕尺寸</div> -->
<div class="app-button" @click="wsActions.test(device.udid, index)">打印ui节点树</div>
<!-- <div class="app-button" @click="wsActions.isOneLive(device.udid, index)">判断单人还是双人</div> -->
<div class="app-button" @click="wsActions.slideDown(device.udid, index)">下滑</div>
<div class="app-button" @click="wsActions.killNow(device.udid, index)">关闭当前应用</div>
<div class="app-button" @click="chooseFile(device.udid, index, 1, wsActions)">安装 APK
文件</div>
<div class="app-button" @click="chooseFile(device.udid, 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 class="app-button" @click="wsActions.isHost(device.udid, index)">一键养号</div> -->
</div>
</div>
</div>
<div class="right" @click.self="selectedDevice = 999">
<div style="margin: 14px;"></div>
<ChatDialog :visible="openShowChat" :messages="chatList" />
</div>
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
@confirm="onDialogConfirm" @cancel="stop" />
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" />
</div>
</template>
<script setup>
import { ref, onMounted, computed, onUnmounted, onBeforeUnmount, watch, inject, nextTick } from "vue";
import VideoConverter from "h264-converter";
import { useRouter } from 'vue-router';
import {
setphoneXYinfo, getphoneXYinfo, getUser,
getHostList, setHostList, addToHostList, getContentpriList,
setContentpriList, getContentList, setContentList,
setsessionId, getsessionId
} from '@/stores/storage'
import { toBufferBtn, stringToUtf8ByteArray, getClipboard, setClipboard, bufferToString, startsWithHeader, trimLongArray, base64ToBinary, toBuffer } from '@/utils/bufferUtils';
import { createWsActions } from '@/utils/wsActions';
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import { chat, translationToChinese, translation } from "@/api/chat";
import { update } from '@/api/account'; //更新主播信息
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue'; // 根据实际路径修改
import ChatDialog from '@/components/ChatDialog.vue'
// import { splitArray } from '@/utils/arrUtil' //分割数组 分配主播 已废弃
import { chooseFile } from '@/utils/fileUtil'
import { connectSSE } from '@/utils/sseUtils'
import { prologue, comment } from '@/api/account';
import { createTaskQueue } from '@/composables/useTaskQueue' //创建任务
import { useCanvasPointer } from '@/composables/useCanvasPointer' //canvas 初始化 点击转换
import { attachTrimmerForIndex } from '@/composables/useVideoStream' //修剪器
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
import { useMonitor } from '@/composables/useMonitor'
import { useTeardown } from '@/composables/useTeardown' //销毁
import LeftToolbar from '@/components/LeftToolbar.vue' //左侧工具栏
import { useStreams } from '@/composables/useStreams'
const router = useRouter();
let wsActions = null;
let userdata = getUser();
// 引入刷新方法
const reloadPage = inject("reload")
let phone = ref({ width: 207, height: 470 });
const openStr = base64ToBinary("ZQBwAAAAAAA8CgLQAtAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA"); //开启视频流的启动命令
const eitwo = base64ToBinary("BAIAAABHVFJD"); //开启设备信息的命令
let isshow = ref(true); //页面是否隐藏
let playTimer = ref([{}, {}, {}, {}, {}, {}, {}, {}]); // 播放定时器
let isdown = ref(false); //是否是按下鼠标状态
const videoElement = ref({}); //视频流元素DOM
let deviceInformation = ref([]); //设备信息列表
//当前弹窗类型
let dialogTitle = ref('');
//评论文本内容
let textContent = ref(['', '', '', '', '', '', '', '']);
let textContentArr = ref([[], [], [], [], [], [], [], [], []]);
//当前主播id内容
let hostIdArr = ref([{}, {}, {}, {}, {}, {}, {}, {}]);
// let hostIdContentArr = ref([[], [], [], [], [], [], [], [], []]);
//私信文本内容
let textContentpri = ref(['', '', '', '', '', '', '', '']);
let textContentpriArr = ref([[], [], [], [], [], [], [], [], []]);
//保存聊天内容
let chatList = ref([]);
//保存爬虫发送的主播信息 待存缓存
let stroageHost = ref([]);
//选中设备
let selectedDevice = ref(999);
//ws列表
let wslist = [];
// 是否停止
let isStop = ref(false);
//sse弹窗是否存在
let isMsgPop = ref(false);
//播放器列表
let instanceList = [{}, {}, {}, {}, {}, {}, {}, {}];
//是否是在关注主播
let runType = ref('');
//屏幕尺寸系数
let isMonitor = ref(false);
let iponeCoefficient = ref([{ width: 1, height: 1 }, { width: 1, height: 1 }, { width: 1, height: 1 }, { width: 1, height: 1 }, { width: 1, height: 1 }, { width: 1, height: 1 }, { width: 1, height: 1 }, { width: 1, height: 1 }]);
//是否是发送的内容
let isSend = ref(false)
//弹窗是否显示
let showDialog = ref(false);
//监听消息定时器
let isShowMes = ref();
//给ws-scrcpy发送的数据格式
const mouseData = {
type: 2,
action: 0,
pointerId: 0,
position: {
point: { x: 0, y: 0 },
screenSize: { width: 320, height: 720 },
},
pressure: 1,
buttons: 1,
};
//打开聊天窗口的状态
let openShowChat = ref(true);
let istranslate = ref(false); //是否是翻译本页
let phoneXYinfo = ref(getphoneXYinfo() == null ? [{}, {}, {}, {}, {}, {}, {}, {}] : getphoneXYinfo());
const isMonitorOn = ref(false) // false 表示关闭true 表示开启
// 这四个互斥模式的 key和你的 runType 对应
const EXCLUSIVE_KEYS = ['brushLive', 'like', 'follow', 'listen'];
// 映射一下中文名,仅用于提示
const KEY_LABEL = {
brushLive: '刷直播',
like: '刷视频',
follow: '一键关注并打招呼',
listen: '监测消息',
};
const showHostDlg = ref(false)
function onHostSaved(list) {
console.log('保存后的 HostList:', list)
}
// 当前激活的互斥 keyrunType 里只要是这四个之一就视为锁定)
const activeKey = computed(() => EXCLUSIVE_KEYS.includes(runType.value) ? runType.value : '');
// 是否处于锁定(有激活模式)
const isLocked = computed(() => !!activeKey.value);
// 尝试激活某个模式(带拦截、可二次点击退出)
function tryActivate(key, runner) {
// 其它模式在运行,阻止切换
if (activeKey.value && activeKey.value !== key) {
ElMessage.warning(`当前正在【${KEY_LABEL[activeKey.value]}】。请先点击“全部停止”或再次点击当前红色按钮停止。`);
return;
}
// 二次点击同一按钮 => 视为停止
if (activeKey.value === key) {
stop(); // 你已有的停止方法,会把 runType 置空并清理状态
return;
}
// 设置 runType 并跑逻辑
runType.value = key;
if (typeof runner === 'function') runner();
}
// 供模板使用:是否为激活按钮
function isActive(btn) {
return !!btn.key && activeKey.value === btn.key;
}
// 供模板使用:是否应禁用
function isDisabled(btn) {
// 只禁用互斥组内的按钮;非互斥按钮(刷新/重置/登出等)不受影响
return !!btn.key && isLocked.value && activeKey.value !== btn.key && EXCLUSIVE_KEYS.includes(btn.key);
}
// 统一处理点击,拦截禁用状态
function handleBtnClick(btn) {
if (isDisabled(btn)) return;
if (typeof btn.onClick === 'function') btn.onClick();
}
// 你可以用这种方式声明按钮们
const buttons = [
{
label: '刷新',
onClick: () => reload({ onlySelected: selectedDevice.value !== 999, hard: true }),
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: '重置tiktok',
onClick: () => resetTk(),
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: '刷直播',
key: 'brushLive',
onClick: () => tryActivate('brushLive', () => {
runType.value = 'brushLive';
brushLive();
}),
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: () => ({ backgroundColor: isActive({ key: 'brushLive' }) ? 'red' : '' })
},
{
label: '刷视频',
key: 'like',
onClick: () => tryActivate('like', () => {
runType.value = 'like';
parentNum();
}),
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: () => ({ backgroundColor: isActive({ key: 'like' }) ? 'red' : '' })
},
{
label: '一键关注并打招呼',
key: 'follow',
onClick: () => tryActivate('follow', () => {
runType.value = 'follow';
showDialog.value = true;
dialogTitle.value = '主播ID';
selectedDevice.value = 999;
}),
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: () => ({ backgroundColor: isActive({ key: 'follow' }) ? 'red' : '' })
},
{
label: '监测消息',
key: 'listen',
onClick: () => tryActivate('listen', () => {
isMonitorOn.value = true;
openMonitor();
}),
show: () => true,
img: {
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
},
style: () => ({ backgroundColor: isActive({ key: 'listen' }) ? 'red' : '' })
},
{
label: '全部停止',
onClick: () => stop(),
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: '登出',
onClick: () => {
td.disposeAll('logout')
router.push('/')
},
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
}
}
]
// 建立 monitor
const {
isPausedByIndex,
pauseMonitorByIndex,
resumeMonitorByIndex,
openMonitor,
closeMonitor,
stopAll,
resumeAndKick,
} = useMonitor({
deviceInformation,
runType,
isStop,
isMonitorOn,
isShowMes,
wsActionsRef: () => wsActions, // wsActions 是你 onopen 里创建的
});
// 放在变量都已声明之后(要能拿到 phone、toBuffer、wslist
const { canvasRef, frameMeta, initCanvas, getCanvasCoordinate, sendPointer } =
useCanvasPointer({
phone, // 你已有的 ref({ width, height })
toBuffer, // 你已有的工具函数
getWs: (i) => wslist[i] // 取对应 index 的 WebSocket
});
// —— 放在变量区deviceInformation 已经是 ref([]))——
const pausedDevices = new Set(); // 用 UDID 做键
const wsCache = new Map();
const td = useTeardown({
deviceInformation,
wslist,
instanceList,
videoElement,
canvasRef,
playTimer,
isShowMes,
wsCache,
createTaskQueue,
stopAll, // useMonitor 里的
removeDocListeners: () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
}
})
const { feedState, pushFrame, resetFeedState, waitForVideoEl, refreshStream } = useStreams({
instanceList,
videoElement,
wslist,
openStr,
VideoConverter, // 传入页面已引入的构造器,避免在 composable 重复引入
})
//````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
// 初始化 手机显示WebSocket 和视频流
const initVideoStream = async (udid, index) => {
//````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
// 1. 检查缓存中是否已有实例
if (wsCache.has(udid)) {
const cached = wsCache.get(udid);
if (cached?.ws?.readyState === WebSocket.OPEN) {
return cached.ws;
}
// 如果连接已关闭,清除缓存并重新创建
wsCache.delete(udid);
}
// 等待 <video> 元素挂载
const el = await waitForVideoEl(udid);
if (!el) {
console.error('[initVideoStream] video element not ready for', udid);
return;
}
// 准备容器
instanceList[index] = { converter: null, timer: null };
//````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
if (!videoElement.value) return;
if (!videoElement.value?.[udid]) {
console.warn('video element missing for', udid);
return; // 不创建 converter避免传 null 进去
}
// 1. 创建 h264-converter 实例
instanceList[index].converter = new VideoConverter(videoElement.value[udid], 60, 1);
// 2. 连接 WebSocket
wslist[index] = new WebSocket(
`ws://127.0.0.1:8000/?action=proxy-adb&remote=tcp%3A8886&udid=${udid}`
);
wslist[index].binaryType = "arraybuffer";
attachTrimmerForIndex(instanceList, videoElement, deviceInformation, index, 10, 2000); // 挂上修剪器
wslist[index].onopen = () => {
console.log("手机显示ws已开启");
wsActions = createWsActions(wslist);
// 发送 开启 视频流数据
setTimeout(() => {
wslist[index].send(openStr);
}, 300);
wsCache.set(udid, { ws: wslist[index], index });
};
const magicSize = stringToUtf8ByteArray('scrcpy_message');
// 3. 处理接收到的二进制数据
wslist[index].onmessage = (event) => {
//判断返回的如果是字符串为自定义返回
if (typeof event.data == 'string') {
if (isStop.value) {
createTaskQueue(index).clear();//清除队伍中的任务
return;
}
const resData = JSON.parse(event.data)
//成功处理
console.log('自定义返回', resData)
if (resData.status === 'success') {
//触发长按该位置
if (resData.type == 'clickCopy') {
console.log('长按', resData.x * iponeCoefficient.value[index].width, resData.y * iponeCoefficient.value[index].height, index)
clickxy(resData.x * iponeCoefficient.value[index].width, resData.y * iponeCoefficient.value[index].height, index, 9) //index为9的时候长按
setTimeout(() => {
wsActions.clickCopyText(resData.device, index)
}, 1500);
} else if (resData.type == 'getmesNum') {
if (resData.message == 0) {
console.log('没有消息')
if (runType.value == 'follow') {
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
}
//如果检测到有新消息会收到两条ws回复一条message==1 一条message==成功
} else if (resData.message == 1) {
console.log('有消息')
pauseMonitorByIndex(index); // 新增:暂停该设备的轮询
} else if (resData.message == '点击成功') {
console.log('双击', resData.x, resData.y, index)
console.log('双击', resData.x * iponeCoefficient.value[index].width, resData.y * iponeCoefficient.value[index].height, index)
setTimeout(() => {
clickxy(resData.x * getphoneXYinfo()[index].width, resData.y * getphoneXYinfo()[index].height, index)
setTimeout(() => {
clickxy(resData.x * getphoneXYinfo()[index].width, resData.y * getphoneXYinfo()[index].height, index) //index为9的时候长按
wsActions.clickSysMesage(deviceInformation.value[index].udid, index) //点击消息进入对话框
}, 100)
}, 2000)
}
} else if (resData.type == 'clickMesage') {
//点击进入新消息页面以后,获取页面信息
wsActions.clickCopyList(deviceInformation.value[index].udid, index)
} else if (resData.type == 'clickSysMesage') {
setTimeout(() => {
Back('', index)
setTimeout(() => {
Back('', index)
if (runType.value == 'follow') {
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
}
// 仅监听模式下恢复
if (runType.value === 'listen' || isMonitorOn.value) {
resumeMonitorByIndex(index);
// 轻微延迟后立刻补一次检测
setTimeout(() => {
const udid = deviceInformation.value[index]?.udid;
if (udid) wsActions.getmesNum(udid, index);
}, 1500);
}
}, 1000)
}, 1000)
} else if (resData.type == 'isVideoAndLive') {
console.log(resData.message)
} else if (resData.type == 'clickCopyList') { //获取到的消息列表
//打印所有信息
console.log('消息列表', resData.message)
chatList.value = resData.message
getTranslation(chatList.value)
const mesBox = resData.message[resData.message.length - 1]
//打印最新一条信息
console.log('最新消息', mesBox)
console.log("翻译", istranslate.value)
if (istranslate.value == false) {
if (mesBox.position == 'right') {
Back('', index)
setTimeout(() => {
Back('', index)
}, 1000)
return
}
openShowChat.value = true
console.log("执行ai")
chat({ msg: mesBox.text }).then(res => {
console.log("ai返回", res)
textContentpri.value[index] = res.result
PrivatetexToPrivatePush(deviceInformation.value[index].udid, index)
})
} else {
console.log("翻译本页")
istranslate.value = false
}
} else if (resData.action == 'getSize') {
// console.log(iponeCoefficient.value, '手机尺寸宽度:', resData.width, '高度:', resData.height);
const RAW_W = Math.max(1, resData.width || 0);
const RAW_H = Math.max(1, resData.height || 1);
// 目标高度固定 720按比例缩放宽
const TARGET_H = 720;
const ratio = TARGET_H / RAW_H;
const scaledWFloat = RAW_W * ratio;
const scaledH = TARGET_H;
// 只向下对齐floor。要 32 对齐就把 ALIGN 改 32
const ALIGN = 16;
const downAlign = (x, a) => Math.max(a, Math.floor(x / a) * a);
const scaledW = downAlign(scaledWFloat, ALIGN);
// 1) 给后续发送 pointer 用的 screenSize服务端就按这个坐标系解释 point
mouseData.position.screenSize.width = scaledW;
mouseData.position.screenSize.height = scaledH;
// 2) 保存“对齐后的尺寸”,用于坐标换算等
frameMeta.value[resData.device] = {
w: scaledW,
h: scaledH,
rotation: 0,
};
// 3) 你代码里很多地方用 resData.x * iponeCoefficient把它同步到 320×720 逻辑坐标
// 这样 UI 节点坐标(原始帧坐标) -> 320×720 逻辑坐标
// iponeCoefficient.value[index].width = 320 / scaledW;
// iponeCoefficient.value[index].height = 720 / scaledH;
iponeCoefficient.value[index].width = scaledW / resData.width
iponeCoefficient.value[index].height = scaledH / resData.height
console.log(index)
phoneXYinfo.value[index].width = scaledW / resData.width
phoneXYinfo.value[index].height = scaledH / resData.height
console.log(
`[getSize] raw=${RAW_W}x${RAW_H} -> scaled=${scaledW}x${scaledH} (align↓${ALIGN}) ${iponeCoefficient.value[index].width} ${iponeCoefficient.value[index].height}`
);
} else {
console.log(resData.type, '坐标返回x:', resData.x, 'y:', resData.y);
}
phoneXYinfo.value[index].id = resData.device
if (resData.type == 'Likes') {//判断是否是 点赞
phoneXYinfo.value[index].Likes = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
if (runType.value == 'follow') {
wsActions.slideDown(deviceInformation.value[index].udid, index)//是否继续下一个视频
}
} else if (resData.type == 'Comment') {//打开评论
phoneXYinfo.value[index].Comment = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
} else if (resData.type == 'CommentText') {//复制评论
phoneXYinfo.value[index].CommentText = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
textContent.value[index] = resData.message
} else if (resData.type == 'Comtext') {//评论输入
phoneXYinfo.value[index].Comtext = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
} else if (resData.type == 'ComPush') {//评论发送
phoneXYinfo.value[index].ComPush = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
setTimeout(() => {
Back('', index)
if (runType.value == 'follow') {
setTimeout(() => {
Back('', index)
}, 1000)
} else {
setTimeout(() => {
wsActions.slideDown(deviceInformation.value[index].udid, index)//是否继续下一个视频
setTimeout(() => {
console.log('观看视频中')
randomSeeVideo(deviceInformation.value[index].udid, index) //50秒后继续循环任务
}, 1000);
}, 1000)
}
}, 800)
} else if (resData.type == 'tomy') {//打开主页
phoneXYinfo.value[index].tomy = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
} else if (resData.type == 'Attention') {//关注、打开私信
phoneXYinfo.value[index].Attention = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
// LikesToCommentToComPush(deviceInformation.value[index].udid, index) //是否继续循环任务
} else if (resData.type == 'return') {//私信评论
phoneXYinfo.value[index].return = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
} else if (resData.type == 'addHost') {//添加关注
phoneXYinfo.value[index].addHost = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
wsActions.slideDown(deviceInformation.value[index].udid, index)//是否继续下一个视频
setTimeout(() => {
wsActions.isHost(deviceInformation.value[index].udid, index)//检测
}, 1000);
} else if (resData.type == 'Privatetex') {//私信评论
phoneXYinfo.value[index].Privatetex = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
} else if (resData.type == 'PrivatePush' || resData.type == 'PrivatePushFollow') {//私信发送
phoneXYinfo.value[index].PrivatePush = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
setTimeout(() => {
Back('', index);
setTimeout(() => {
Back('', index);
setTimeout(() => {
Back('', index);
setTimeout(() => {
if (resData.type == 'PrivatePush') {
Back('', index);
setTimeout(() => {
Back('', index);
setTimeout(() => {
Back('', index);
if (isMonitor.value) {
//正常没有消息,发送完私信以后,返回六次,然后继续下一个任务
wsActions.getmesNum(deviceInformation.value[index].udid, index)
} else {
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
}
}, 1000);
}, 1000);
} else if (resData.type == 'PrivatePushFollow') {
//如果有新消息,回复完私信以后,返回三次,然后继续下一个任务
// wsActions.getmesNum(deviceInformation.value[index].udid, index)
// LikesToLikesToLikes(deviceInformation.value[index].udid, index)
if (runType.value === 'listen' || isMonitorOn.value) {
resumeAndKick(index); // ← 一步到位:恢复并在 1.5s 后补一次 getmesNum
}
}
}, 1000);
}, 1000);
}, 1000);
}, 1000);
} else if (resData.type == 'clickCopy') {//点击复制
phoneXYinfo.value[index].clickCopy = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
} else if (resData.type == 'hostVideo') {//观看主播视频
phoneXYinfo.value[index].hostVideo = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
} else if (resData.type == 'isHost') {//视频关注主播
phoneXYinfo.value[index].isHost = { x: resData.x * iponeCoefficient.value[index].width, y: resData.y * iponeCoefficient.value[index].height }
if (resData.message == 0) {
console.log('无关注', index)
Back(deviceInformation.value[index].udid, index)
setTimeout(() => {
console.log('观看视频中', index)
randomSeeVideo(deviceInformation.value[index].udid, index) //30-50秒后继续循环任务
}, 1000);
} else if (resData.message == 1) {
console.log('有关注', index)
const randomNum = Math.random(); // 生成一个0到1之间的随机数
//是否点赞评论的概率
if (randomNum < 0.07) {
console.log('进行点赞评论', index)
LikesToCommentToComPush(deviceInformation.value[index].udid, index)
} else {
console.log('下一个', index)
wsActions.slideDown(deviceInformation.value[index].udid, index)//是否继续下一个视频
randomSeeVideo(deviceInformation.value[index].udid, index) //30-50秒后继续循环任务
}
}
} else if (resData.type == 'openDY') {//视频关注主播
createTaskQueue(index).clear();//清除队伍中的任务
} else if (resData.type == 'toLive') {//打开直播
wsActions.isHead(deviceInformation.value[index].udid, index)//判断直播
} else if (resData.type == 'isHead') {//判断有没有头像
if (resData.message == 1) {
wsActions.isOneLive(deviceInformation.value[index].udid, index)//判断直播
} else {
return
}
} else if (resData.type == 'isOneLive') {//判断单人还是双人
if (resData.message == 0) {
wsActions.slideDown(deviceInformation.value[index].udid, index)//是否继续下一个视频
wsActions.isHead(deviceInformation.value[index].udid, index)//判断直播
return
}
// 用法:
(async () => {
const result = await randomDelayAndExecute();
if (runType.value !== 'brushLive') {
return
}
//30%概率
if (result) {
let count = 0;
const num = Math.floor(Math.random() * 31) + 5;
const interval = setInterval(() => {
if (count >= num) {
clearInterval(interval);
wsActions.slideDown(deviceInformation.value[index].udid, index)//是否继续下一个视频
wsActions.isHead(deviceInformation.value[index].udid, index)//判断直播
return;
}
clickxy(
resData.x * iponeCoefficient.value[index].width,
resData.y * iponeCoefficient.value[index].height,
index
)
count++;
}, 300);
return;
} else {
wsActions.slideDown(deviceInformation.value[index].udid, index)//是否继续下一个视频
wsActions.isHead(deviceInformation.value[index].udid, index)//判断直播
return
}
})();
}
setphoneXYinfo(phoneXYinfo.value)
if (resData.type != 'isHost') {
if (resData.type == 'hostVideo' || resData.type == 'Likes') {
setTimeout(() => {
createTaskQueue(index).next(); // 继续队列中下一个任务
}, 5000)
} else if (resData.type == 'getmesNum') {
} else {
createTaskQueue(index).next(); // 继续队列中下一个任务
}
}
} else {
// ----------------------------------------------------------------------------------------------------报错处理
//如果该视频无法被评论,或者没有评论,返回刷下一条视频
if (resData.type == 'Comtext' || resData.type == 'CommentText') {
if (runType.value == 'follow') {
Back('', index)
setTimeout(() => {
Back('', index)
createTaskQueue(index).next(); // 继续队列中下一个任务
createTaskQueue(index).next(); // 继续队列中下一个任务
}, 1000)
} else {
resetApp(udid, index)
setTimeout(() => {
wsActions.isHost(deviceInformation.value[index].udid, index)
}, 1000)
}
//如果该视频无法被搜索到,返回刷下一条视频
} else if (resData.type == 'toHost') {
Back('', index)
setTimeout(() => {
Back('', index)
createTaskQueue(index).clear();//清除队伍中的任务
setTimeout(() => {
Back('', index)
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
}, 1000)
}, 1000)
} else if (resData.type == 'PrivatePush' || resData.type == 'Privatetex' || resData.type == 'hostVideo' || resData.type == 'search' || resData.type == 'Attention' || resData.type == 'Comment') {
if (runType.value == 'follow') {
//关注的时候出现无法私信和没有视频的情况 错误重置
resetApp(udid, index)
setTimeout(() => {
if (isMonitor.value) {
//正常没有消息,发送完私信以后,返回六次,然后继续下一个任务
wsActions.getmesNum(deviceInformation.value[index].udid, index)
} else {
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
}
// LikesToLikesToLikes(deviceInformation.value[index].udid, index)
}, 1000)
}
} else if (resData.type == 'getmesNum') {
if (runType.value == 'follow') {
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
}
//getmesNum出现没有消息的情况
} else if (resData.type == 'clickMesage' || resData.type == 'ComPush') {
//ComPush关注主播的时候出现无法评论的视频 会跳过评论发送,不处理评论发送的返回错误
} else if (resData.type == 'clickCopyList') {
//如果无法获取到聊天记录,返回继续检测
console.error('未找到聊天,返回')
Back('', index)
} else if (resData.type == 'clickSysMesage') {
//如果没有系统消息,则点击新消息
wsActions.clickMesage(deviceInformation.value[index].udid, index) //点击消息进入对话框
} else {
console.error(resData.message, resData.type); // 错误处理
ElMessage.error(resData.message);
createTaskQueue(index).clear();//清除队伍中的任务
}
// createTaskQueue(index).next(); // 继续队列中下一个任务
}
} else {
const buf = event.data; // ArrayBuffer
const view = new Uint8Array(buf); // 只创建视图,不复制
// 粘贴板消息头判断FIX: 用 view别用未定义的 data
if (startsWithHeader(magicSize, view)) {
if (!isSend.value) {
const payload = trimLongArray(view, magicSize);
const paste = bufferToString(payload);
console.log('获取粘贴板内容', paste);
}
return; // 这类消息不走视频通道
}
// 视频流
if (instanceList[index].converter && isshow.value) {
pushFrame(index, buf); //处理视频流帧
}
}
};
// 4. 错误处理
wslist[index].onerror = (error) => {
wsCache.delete(udid); // 错误时清理缓存
};
//``````````````````````````````````````````````````````````````````````````````````
wslist[index].onclose = (event) => {
wsCache.delete(udid)// 自动清理缓存
clearInterval(instanceList[index].timer); // 清理定时器// 移除缓存
};
//``````````````````````````````````````````````````````````````````````````````````
};
// 鼠标按下事件处理
const handleCanvasdown = (udid, e, index) => {
const { x, y } = getCanvasCoordinate(e, udid);
isdown.value = true;
sendPointer(udid, index, 0, x, y);
setTimeout(() => { selectedDevice.value = index; }, 300);
};
// 鼠标抬起事件处理
const handleCanvasup = (udid, e, index) => {
const { x, y } = getCanvasCoordinate(e, udid);
isdown.value = false;
sendPointer(udid, index, 1, x, y);
};
// 鼠标移动事件处理
const handleMouseMove = (udid, e, index) => {
if (!isdown.value) return;
const { x, y } = getCanvasCoordinate(e, udid);
sendPointer(udid, index, 2, x, y);
};
// ======= 对齐工具 =======
const ALIGN_BASE = 16; // 改成 32 就是 32 对齐
const ROUND_MODE = 'nearest'; // 'down' | 'up' | 'nearest'
function alignTo(x, a = ALIGN_BASE, mode = ROUND_MODE) {
if (a <= 1) return Math.round(x);
const q = x / a;
if (mode === 'down') return Math.floor(q) * a;
if (mode === 'up') return Math.ceil(q) * a;
/* nearest */ return Math.round(q) * a;
}
//返回上一层
const Back = (udid, index) => {
wslist[index].send(toBufferBtn({ "type": 0, "action": 0, "keycode": 4, "metaState": 0, "repeat": 0 }));
wslist[index].send(toBufferBtn({ "type": 0, "action": 1, "keycode": 4, "metaState": 0, "repeat": 0 }));
}
//返回首页
const Home = (udid, index) => {
wslist[index].send(toBufferBtn({ "type": 0, "action": 0, "keycode": 3, "metaState": 0, "repeat": 0 }));
wslist[index].send(toBufferBtn({ "type": 0, "action": 1, "keycode": 3, "metaState": 0, "repeat": 0 }));
}
//任务视图
const Overview = (udid, index) => {
wslist[index].send(toBufferBtn({ "type": 0, "action": 0, "keycode": 187, "metaState": 0, "repeat": 0 }));
wslist[index].send(toBufferBtn({ "type": 0, "action": 1, "keycode": 187, "metaState": 0, "repeat": 0 }));
}
//回车 index手机下标 state up按下 down抬起 keycode 键
const key = (index, state, keycode) => {
// console.log("退格", index)
if (index === 999) {
return
} else {
if (state == 'up') {
wslist[index].send(toBufferBtn({ "type": 0, "action": 1, "keycode": keycode, "metaState": 0, "repeat": 0 }));
} else if (state == 'down') {
wslist[index].send(toBufferBtn({ "type": 0, "action": 0, "keycode": keycode, "metaState": 0, "repeat": 0 }));
}
}
}
const handleVisibilityChange = () => {
if (document.visibilityState === "hidden") {
console.log("页面被隐藏");
isshow.value = false;
// 页面被隐藏时执行的逻辑
} else if (document.visibilityState === "visible") {
console.log("页面变为可见");
setTimeout(() => {
isshow.value = true;
// ObtainDeviceInformation();
}, 500);
}
};
onMounted(() => {
// document.addEventListener("visibilitychange", handleVisibilityChange);
ObtainDeviceInformation();
// window.addEventListener('keydown', (e) => {
// // console.log('触发按键了 键盘事件有效', e)
// if (e.key == 'Backspace') {
// key(selectedDevice.value, 'down', 67)
// }
// })
// window.addEventListener('keyup', (e) => {
// // console.log('触发按键了 键盘事件有效抬起', e.keyCode, e.key)
// if (e.key == 'Backspace') {
// key(selectedDevice.value, 'up', 67)
// }
// })
const loading = ElLoading.service({
lock: true,
text: '初始化中...',
background: 'rgba(0, 0, 0, 0.7)',
})
// reloadPage()
setTimeout(() => {
loading.close()
}, 2000)
//sse接收爬虫发送的消息
const es = connectSSE(`https://datasave.api.yolozs.com/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
// connectSSE(`http://192.168.1.155:19665/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
// 处理服务端推送的数据
console.log('来自服务端:', data)
//接收到start
if (data === 'start') {
if (!isMsgPop.value) {
isMsgPop.value = true;
ElMessageBox.confirm(
'检测到YOLO助手正在爬取主播是否进行操作',
'消息提醒',
{
confirmButtonText: '开始',
cancelButtonText: '取消',
type: 'success',
}
)
.then(() => {
//开启sse版follow任务
ElMessage({
type: 'success',
message: '任务开启成功',
})
addToHostList(stroageHost.value)
//重启tk
resetTk()
//获取评论
comment().then((resA) => {
console.log('resA:', resA);
setContentList(resA)
//评论
textContentArr.value.forEach((item, indexA) => {
textContent.value[indexA] = resA[getRandomNumber(resA.length - 1)];
})
prologue().then((resB) => {
console.log('resB:', resB);
setContentpriList(resB)
//私信
textContentpriArr.value.forEach((item, indexA) => {
textContentpri.value[indexA] = resB[getRandomNumber(resB.length - 1)];
runType.value = 'follow'
console.log('runType', runType.value)
})
deviceInformation.value.forEach((device, indexB) => {
if (getHostList().length <= 0) return;
// LikesToLikesToLikes(device.udid, indexB)
wsActions.getmesNum(device.udid, indexB)
})
})
})
})
.catch(() => {
stroageHost.value = []
isMsgPop.value = false;
})
}
} else {
stroageHost.value = getHostList()
stroageHost.value.push(({ country: data.country, text: data.hostsId, state: false }))
if (runType.value == 'follow') {
addToHostList([{ country: data.country, text: data.hostsId, state: false }])
}
}
})
td.setSSE(es)
});
//更新状态
// update(
// {
// id: data.id,
// operationStatus: 1,
// }
// ).then(() => {
// })
onBeforeUnmount(() => {
// document.removeEventListener("visibilitychange", handleVisibilityChange);
});
// 组件卸载时清理
onUnmounted(() => {
console.log("卸载组件");
cloesMonitor(); //关闭检测消息
});
//获取设备信息
const ObtainDeviceInformation = () => {
// 2. 连接 WebSocket
const ws = new WebSocket("ws://127.0.0.1:8000/?action=multiplex");
td.setMultiplexWS(ws)
ws.binaryType = "arraybuffer";
ws.onopen = () => {
ws.send(eitwo);
};
// 3. 处理接收到的二进制数据
ws.onmessage = async (event) => {
const data = JSON.parse(new TextDecoder('utf-8').decode(event.data).replace(/[^\x20-\x7F]/g, ''));
try {
console.log('数组', data)
if (data.type == "devicelist") {
deviceInformation.value = [];
const filteredList = data.data.list.filter(item => item.state === 'device');
//检测到设备列表时,渲染所有设备
for (let i = 0; i < filteredList.length; i++) {
const item = filteredList[i];
deviceInformation.value.push(item);
await nextTick(); // 等 v-for 渲染出 <video>
initCanvas(item.udid); // 如果它也依赖 DOM同样要在 nextTick 之后
initVideoStream(item.udid, i); // 直接使用循环变量 i
// getSize 建议放到 wslist[index].onopen 里最稳,
// 若保留延时也可以:
setTimeout(() => wsActions?.getSize(item.udid, i), 2000); // 直接使用循环变量 i
}
} else if (data.type == "device") {
if (data.data.device.state === "offline") {
//监听设备信息,出现离线设备,则删除设备信息
deviceInformation.value.forEach((item, index) => {
if (item.udid === data.data.device.udid) {
deviceInformation.value.splice(index, 1);
if (index < wslist.length - 1) {
deviceInformation.value.forEach((item, index1) => {
//关闭websocket连接
wslist.forEach((item, index) => {
new VideoConvertitem.close();
})
//重新连接websocket
new Promise((resolve, reject) => {
setTimeout(() => {
try {
initVideoStream(item.udid, index1);
initCanvas(item.udid);
} catch (error) {
reject(error);
}
}, 300);
});
})
}
console.log("deviceInformation", deviceInformation.value);
}
})
} else if (data.data.device.state === "device") {
let isrepeat = false;
//监听设备信息,出现新设备,则添加设备信息
deviceInformation.value.forEach((item, index) => {
if (item.udid == data.data.device.udid) {
isrepeat = true;
}
})
if (!isrepeat) {
deviceInformation.value.push(data.data.device);
new Promise((resolve, reject) => {
setTimeout(() => {
try {
initVideoStream(data.data.device.udid, deviceInformation.value.length - 1);
initCanvas(data.data.device.udid);
} catch (error) {
// reject(error);
}
}, 1000);
});
}
};
}
} catch (e) {
console.error(e);
}
};
}
//喜欢-输入评论
async function LikesToCommentToComPush(udid, index) {
await sendWsTask(index, { udid, action: 'click', type: 'Likes', index, resourceId: 'com.zhiliaoapp.musically:id/dy6' });
await sendWsTask(index, { udid, action: 'click', type: 'Comment', index, resourceId: 'com.zhiliaoapp.musically:id/cvd' });
await sendWsTask(index, { udid, action: 'dump', type: 'CommentText', index, resourceId: 'com.zhiliaoapp.musically:id/d5q' });
await sendWsTask(index, { udid, action: 'click', type: 'Comtext', index, resourceId: 'com.zhiliaoapp.musically:id/cs0' });
await sendWsTask(index, { udid, action: 'click', type: 'ComPush', index, resourceId: 'com.zhiliaoapp.musically:id/bqg' });
}
//点赞主页4个作品+评论最后一个作品并返回
async function LikesToLikesToLikes(udid, index) {
isStop.value = false;
runType.value = 'follow';
// await sendWsTask(index, { udid, action: 'click', type: 'getmesNum', index, resourceId: 'com.zhiliaoapp.musically:id/jyv' });
await sendWsTask(index, { udid, action: 'click', type: 'search', index, resourceId: 'com.zhiliaoapp.musically:id/gtz' });
await sendWsTask(index, { udid, action: 'click', type: 'searchHost', index, resourceId: 'com.zhiliaoapp.musically:id/t6f' });
await sendWsTask(index, { udid, action: 'click', type: 'toHost', index, resourceId: 'com.zhiliaoapp.musically:id/iso' });
await sendWsTask(index, { udid, action: 'click', type: 'hostVideo', index, resourceId: 'com.zhiliaoapp.musically:id/u3o', num: 0 });
await sendWsTask(index, { udid, action: 'click', type: 'Likes', index, resourceId: 'com.zhiliaoapp.musically:id/dy6' });
await sendWsTask(index, { udid, action: 'click', type: 'Likes', index, resourceId: 'com.zhiliaoapp.musically:id/dy6' });
await sendWsTask(index, { udid, action: 'click', type: 'Likes', index, resourceId: 'com.zhiliaoapp.musically:id/dy6' });
await sendWsTask(index, { udid, action: 'click', type: 'Likes', index, resourceId: 'com.zhiliaoapp.musically:id/dy6' });
await sendWsTask(index, { udid, action: 'click', type: 'Comment', index, resourceId: 'com.zhiliaoapp.musically:id/cvd' });
await sendWsTask(index, { udid, action: 'click', type: 'Comtext', index, resourceId: 'com.zhiliaoapp.musically:id/cs0' });
await sendWsTask(index, { udid, action: 'click', type: 'ComPush', index, resourceId: 'com.zhiliaoapp.musically:id/bqg' });
await sendWsTask(index, { udid, action: 'click', type: 'Attention', index, resourceId: 'com.zhiliaoapp.musically:id/dhx' });
await sendWsTask(index, { udid, action: 'click', type: 'Attention', index, resourceId: 'com.zhiliaoapp.musically:id/dhx' });
await sendWsTask(index, { udid, action: 'click', type: 'Privatetex', index, resourceId: 'com.zhiliaoapp.musically:id/hob' });
await sendWsTask(index, { udid, action: 'click', type: 'PrivatePush', index, resourceId: 'com.zhiliaoapp.musically:id/hog' });
// await sendWsTask(index, { udid, action: 'click', type: 'fanhui', index, resourceId: 'com.zhiliaoapp.musically:id/awi' });
}
//私信
async function PrivatetexToPrivatePush(udid, index) {
await sendWsTask(index, { udid, action: 'click', type: 'Privatetex', index, resourceId: 'com.zhiliaoapp.musically:id/hob' });
await sendWsTask(index, { udid, action: 'click', type: 'PrivatePushFollow', index, resourceId: 'com.zhiliaoapp.musically:id/hog' });
}
//重置当前应用
async function resetApp(udid, index) {
createTaskQueue(index).clear();//清除队伍中的任务
await sendWsTask(index, { udid, action: 'killNow' });
await sendWsTask(index, { udid, action: 'openDY' });
}
//发送评论字符到手机方法
function setComText(index) {
isSend.value = true;
setTimeout(() => {
isSend.value = false;
}, 300);
console.log('发送评论内容', index, textContent.value[index])
wslist[index].send(setClipboard(textContent.value[index]));
if (runType.value === 'follow') {
textContent.value[index] = getContentList()[index][getRandomNumber(getContentList()[index].length - 1)];
}
}
//发送主播ID字符到手机方法
function setHostId(index) {
isSend.value = true;
setTimeout(() => {
isSend.value = false;
}, 300);
const host = markFirstFalseAsTrue(getHostList())
if (host == null) {
createTaskQueue(index).clear();//清除队伍中的任务
ElMessage.success('主播ID已全部发送完毕');
return;
}
hostIdArr.value[index] = host;
wslist[index].send(setClipboard(host.text)); //发送内容
}
//发送私信字符到手机方法
function setPrivateText(index, datatype) {
isSend.value = true;
setTimeout(() => {
isSend.value = false;
}, 300);
console.log('发送私信内容', index, textContentpri.value[index])
//如果是在关注主播中,发送的开场白进行对应国家翻译
if (runType.value === 'follow' && datatype == 'PrivatePush') {
translation({ msg: getContentpriList()[index][getRandomNumber(getContentpriList()[index].length - 1)], country: hostIdArr.value[index].country }).then(res => {
console.log(res)
textContentpri.value[index] = res;
wslist[index].send(setClipboard(textContentpri.value[index]));
})
} else {
wslist[index].send(setClipboard(textContentpri.value[index]));
if (getContentpriList()[index]) {
textContentpri.value[index] = getContentpriList()[index][getRandomNumber(getContentpriList()[index].length - 1)];
}
}
}
//获取手机粘贴板方法
function getText(index) {
wslist[index].send(getClipboard());
}
function getComArr(index, type) {
if (type == '评论') {
ElMessageBox.alert(getContentList()[index], `当前${type}内容`, {
confirmButtonText: 'OK',
})
} else {
ElMessageBox.alert(getContentpriList()[index], `当前${type}内容`, {
confirmButtonText: 'OK',
})
}
}
// 小工具
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function tapAt(udid, index, x, y, holdMs = 80) {
sendPointer(udid, index, 0, x, y); // down
await sleep(holdMs);
sendPointer(udid, index, 1, x, y); // up
}
async function longPressAt(udid, index, x, y, holdMs = 1500) {
sendPointer(udid, index, 0, x, y); // down
await sleep(holdMs);
sendPointer(udid, index, 1, x, y); // up
}
/** 可用于自定义滑动/拖拽(如果以后要替代 slideDown/slideRight */
async function drag(udid, index, x1, y1, x2, y2, durationMs = 300, steps = 8) {
sendPointer(udid, index, 0, x1, y1); // down
const dx = (x2 - x1) / steps;
const dy = (y2 - y1) / steps;
const dt = Math.max(8, Math.floor(durationMs / steps));
for (let i = 1; i <= steps; i++) {
await sleep(dt);
sendPointer(udid, index, 2, Math.round(x1 + dx * i), Math.round(y1 + dy * i)); // move
}
await sleep(20);
sendPointer(udid, index, 1, x2, y2); // up
}
// 用 pointer down/up/move 改写后的 clickxy
async function clickxy(x, y, index, type) {
const udid = deviceInformation.value[index].udid;
if (!udid) {
console.error('clickxy: no udid');
return;
};
try {
if (type === 3) {
// 关注/私信后的返回和下滑
await tapAt(udid, index, x, y, 80);
await sleep(300);
Back('', index);
await sleep(300);
wsActions.slideDown(phoneXYinfo.value[index].id, index);
// 你原代码里这里还有 start(...),如果没定义会报错;需要的话保留:
// await sleep(300);
// start(phoneXYinfo.value[index].id, index);
return;
}
if (type === 2) {
// 评论后的返回和右滑
await tapAt(udid, index, x, y, 80);
await sleep(300);
Back('', index);
await sleep(300);
wsActions.slideRight(phoneXYinfo.value[index].id, index);
return;
}
if (type === 1) {
// 评论框点击 -> 粘贴发送
await tapAt(udid, index, x, y, 80);
await sleep(300);
setComText(index);
return;
}
if (type === 9) {
// 长按
await longPressAt(udid, index, x, y, 1500);
return;
}
// 默认:普通轻点
await tapAt(udid, index, x, y, 80);
} catch (e) {
console.error('clickxy error:', e);
}
}
const reload = (opts = {}) => {
const { onlySelected = false, hard = true } = opts;
const targets = (onlySelected && selectedDevice.value !== 999)
? [selectedDevice.value]
: deviceInformation.value.map((_, i) => i);
targets.forEach(i => refreshStream(i, hard));
ElMessage.success(`已刷新${onlySelected ? '当前设备' : '全部设备'}`);
};
//发送任务前的处理
function sendWsTask(index, data) {
console.log('任务等待中', data.type);
return new Promise((resolve) => {
try {
const queue = createTaskQueue(index);
// console.log("创建队列", queue.getNum());
const task = () => {
//发送评论的文本粘贴事件
if (data.type == 'Likes') {
clickxy(160, 360, index)
}
if (data.type == 'isHost') {
}
if (data.type == 'Comment') {
if (runType.value == 'follow') {
clickxy(160, 360, index)
}
}
if (data.type == 'ComPush') {
setTimeout(() => {
setComText(index)//粘贴内容
}, 500)
}
if (data.type == 'searchHost') {
setTimeout(() => {
setHostId(index)//粘贴内容
}, 500)
}
//发送私信的文本粘贴事件
if (data.type == 'PrivatePush' || data.type == 'PrivatePushFollow') {
setPrivateText(index, data.type)
}
//关注前的返回和右滑
if (data.type == 'Attention') {
}
//发送关注之前的返回
if (data.type == 'addHost') {
}
//发送任务
if (data.type == 'Attention') {
setTimeout(() => {
wslist[index].send(JSON.stringify(data));
resolve();
}, 1000)
} else {
wslist[index].send(JSON.stringify(data));
resolve();
}
// 表示当前任务“已发出”,但不是“已完成”
// 实际完成由 onmessage 中的 success 决定并继续执行队列
};
queue.enqueue(task);
} catch (e) {
console.error('发送任务错误:', e);
}
});
}
// 生成start秒到end秒的随机延迟
function randomDelay(start, end) {
return Math.floor(Math.random() * end * 1000) + start * 1000;
}
//一个0到n之间的随机整数
function getRandomNumber(n) {
return Math.floor(Math.random() * (n + 1));
}
//延迟30-50秒后检测关注
function randomSeeVideo(udid, index) {
const delay = Math.floor(Math.random() * (50 - 30) + 10) * 1000;
playTimer.value[index] = setTimeout(() => {
console.log('观看结束', index);
wsActions.isHost(udid, index)//检测
}, delay);
}
//延迟1-4分钟后秒后检测关注
async function randomDelayAndExecute() {
const minDelay = 1 * 60 * 1000;
const maxDelay = 4 * 60 * 1000;
const randomDelay = Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay;
console.log(`将延迟 ${Math.round(randomDelay / 1000)} 秒后执行...`);
return new Promise((resolve) => {
setTimeout(() => {
const randomNum = Math.floor(Math.random() * 100);
if (randomNum < 30) {
console.log("30%概率触发");
resolve(true);
} else {
console.log("70%概率触发");
resolve(false);
}
}, randomDelay);
});
}
//选中样式
function getVideoStyle(index) {
const isSelected = selectedDevice.value === index;
const baseWidth = phone.value.width;
const baseHeight = phone.value.height;
return {
width: isSelected ? baseWidth * 1.4 + 'px' : baseWidth + 'px',
height: isSelected ? baseHeight * 1.4 + 'px' : baseHeight + 'px',
// border: isSelected ? '2px solid blue' : '1px solid blue',
position: isSelected ? 'absolute' : 'relative',
top: isSelected ? '0' : 'unset',
left: isSelected ? '0' : 'unset',
zIndex: isSelected ? 1000 : 1,
pointerEvents: isSelected ? 'none' : 'auto',
transform: getTransformStyle(index),
transition: 'all 0.3s ease',
};
}
function stop() {
// actions[index] = [];
stopAll(); // ← 替代 cloesMonitor + pausedDevices.clear
isStop.value = true; //停止所有任务
isMsgPop.value = false;//关闭爬虫sse任务
deviceInformation.value.forEach((item, i) => {
console.log('停止', i);
createTaskQueue(i).clear();//清除队伍中的任务
console.log(createTaskQueue(i).getNum())
//停止当前任务
//重置当前状态
runType.value = ''
//清除观看视频定时器
clearInterval(playTimer.value[i])
playTimer.value[i] = null;//清除定时器
})
console.log('停止', isStop.value);
}
function openTk() {
deviceInformation.value.forEach((device, index) => {
wsActions.open(device.udid, index)
})
}
function resetTk() {
isStop.value = false;
deviceInformation.value.forEach((device, index) => {
resetApp(device.udid, index)
})
}
//关闭监听
function cloesMonitor() {
isMonitorOn.value = false;
deviceInformation.value.forEach(() => { runType.value = '' });
clearInterval(isShowMes.value);
isShowMes.value = '';
pausedDevices.clear(); // 新增
}
//一键养号
function parentNum() {
isStop.value = false;
deviceInformation.value.forEach((device, index) => {
runType.value = 'like'
wsActions.isHost(device.udid, index)
})
}
function brushLive() {
isStop.value = false;
deviceInformation.value.forEach((device, index) => {
runType.value = 'brushLive'
wsActions.toLive(device.udid, index)
})
}
//确认多行文本框内容
function onDialogConfirm(result, type, index, isMon) {
console.log(type, index, isMon);
if (type == '评论') {
if (index == 998) {
textContentArr.value.forEach((item, indexA) => {
textContent.value[indexA] = result[getRandomNumber(result.length - 1)];
})
setContentList(result)
//打开私信弹窗
selectedDevice.value = 999;
dialogTitle.value = '私信';
setTimeout(() => {
showDialog.value = true;
}, 600)
} else {
setContentList(result)
textContent.value[index] = result[getRandomNumber(result.length - 1)];
}
} else if (type == '私信') {
//index ==999 表示全部
if (index == 999) {
isMonitor.value = isMon //是否自动回复
textContentpriArr.value.forEach((item, indexA) => {
textContentpri.value[indexA] = result[getRandomNumber(result.length - 1)];
runType.value = 'follow'
})
// isStop.value = true; //停止所有任务
setContentpriList(result)
deviceInformation.value.forEach((device, indexB) => {
if (getHostList().length <= 0) return;
if (isMon) {
console.log(isMon)
console.error('自动回复');
wsActions.getmesNum(device.udid, indexB)
} else {
console.error('开始关注');
LikesToLikesToLikes(device.udid, indexB)
}
// LikesToLikesToLikes(device.udid, indexB)
})
} else {
textContentpriArr.value[index] = result;
textContentpri.value[index] = result[getRandomNumber(result.length - 1)];
}
} else if (type == '主播ID') {
//index ==999 表示全部
if (index == 999) {
//分割数组 几台设备 分为几个数组
let hostListResult = [];
result.forEach((item, indexA) => {
hostListResult.push({ country: '', text: item, state: false })
})
addToHostList(hostListResult)
//打开评论弹窗
selectedDevice.value = 998;
dialogTitle.value = '评论';
setTimeout(() => {
showDialog.value = true;
}, 600)
}
}
}
function getTranslation(list) {
list.forEach((item, index) => {
translationToChinese({ msg: item.text }).then(res => {
console.log(res);
chatList.value[index].text = res
})
})
}
function markFirstFalseAsTrue(hostList) {
const index = hostList.findIndex(item => item.state === false);
if (index !== -1) {
hostList[index].state = true;
setHostList(hostList)
return hostList[index]; // 可选:返回被修改的对象
}
return null; // 没有找到 false 的项
}
//计算手机canvas是否需要偏移
function getTransformStyle(index) {
return selectedDevice.value === index && index >= 3
? 'translateY(-30%)'
: 'none';
}
function manualGc() {
window.electronAPI.manualGc()
}
</script>
<style scoped lang="less">
@import '../static/css/video.less';
</style>