1688 lines
51 KiB
Vue
1688 lines
51 KiB
Vue
<template>
|
||
<div class="main">
|
||
<el-scrollbar class="left"> <!-- 左边栏 -->
|
||
<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
|
||
time: '2023-10-01 12:00:00', // 可选;有些是 'time' 会被过滤或 content
|
||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||
})">新增</el-button> -->
|
||
<!-- <el-button @click="updates([{ id: 10022, aiOperation: 1 }])">更新请求</el-button> -->
|
||
</div>
|
||
|
||
<div class="center-line"> <!-- 左边栏按钮 -->
|
||
<div v-for="(btn, index) in buttons" :key="index" style="width: 100%;">
|
||
<div v-if="btn.show?.()" class="left-button" :style="btn.style ? btn.style() : {}" @click="btn.onClick"
|
||
@mouseenter="hoverIndex = index" @mouseleave="hoverIndex = null">
|
||
<img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt="">
|
||
{{ btn.label }}
|
||
</div>
|
||
</div>
|
||
<div style="position: absolute;left: 20px; bottom: 20px;">
|
||
<el-button @click="showHostDlg = true">执行主播库</el-button>
|
||
<el-button type="info" @click="uploadLogFile">上传日志</el-button>
|
||
<!-- 新增:SSE 弹窗总开关 -->
|
||
<el-switch v-model="sseEnabled" inline-prompt active-text="监听爬虫" inactive-text="监听爬虫"
|
||
style="margin-left: 8px;" />
|
||
</div>
|
||
</div>
|
||
</el-scrollbar>
|
||
<!-- 中间手机区域 -->
|
||
<div class="content" @click.self="selectedDevice = 999">
|
||
<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" :src="imgSrcMap[device.deviceId] || ''" :data-id="device.deviceId"
|
||
:ref="el => (imgRefs[device.deviceId] = el)" />
|
||
|
||
<canvas v-show="selectedDevice === index" class="overlay"
|
||
@mousedown.stop="(e) => onCanvasDown(device.deviceId, e, index)"
|
||
@mouseup.stop="(e) => onCanvasUp(device.deviceId, e, index)"
|
||
@mousemove.stop="(e) => onCanvasMove(device.deviceId, e, index)" />
|
||
</div>
|
||
<div class="input-info" v-show="selectedDevice == index">
|
||
<div class="app-button" @click="restartTikTok({ udid: device.deviceId })">重置tiktok</div>
|
||
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
|
||
<div class="app-button" @click="stopOne(device.deviceId)">停止任务</div>
|
||
<div class="app-button" @click="runTask(runType, device.deviceId)">开启</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="right center-line" @click.self="selectedDevice = 999">
|
||
<!-- <div style="margin: 30px;"></div> -->
|
||
|
||
<ChatDialog :visible="openShowChat" :messages="chatList" />
|
||
<MessageDialogd :visible="openShowChat" :messages="MesNewList" :sound-src="ding" />
|
||
</div>
|
||
<img v-if="isWifi" style="position: absolute; right: 20px; top: 10px; height: 30px;" src="@/assets/wifi.png"></img>
|
||
<MultiLineInputDialog v-model:visible="showDialog" :initialText='initialTextStr' :title="dialogTitle"
|
||
:index="selectedDevice" @confirm="onDialogConfirm" @cancel="stopAll(100)" />
|
||
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" @invitType="invitTypeFun" />
|
||
|
||
<TranslationDialog v-model="showtransDlg" :type="transDlgType" :translateFn="doTranslate"
|
||
storage-key-prefix="demo-translation" @confirm="onConfirm" @cancel="stopAll(100)" />
|
||
</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" />
|
||
<!-- 定时调度配置弹窗 -->
|
||
<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="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>
|
||
|
||
<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="24" />
|
||
<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>
|
||
<el-button @click="showScheduleDlg = false">取消</el-button>
|
||
<el-button type="primary" @click="saveSchedule">开启</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, onUnmounted, onBeforeUnmount, watch, inject, computed, nextTick } from "vue";
|
||
import { useRouter } from 'vue-router';
|
||
import {
|
||
setphoneXYinfo, getphoneXYinfo, getUser,
|
||
getHostList, setHostList, getContentpriList,
|
||
setContentpriList, getContentList, setContentList,
|
||
setsessionId, getsessionId, getContentListMultiline, getContentpriListMultiline
|
||
} from '@/stores/storage'
|
||
import { connectSSE } from '@/utils/sseUtils'
|
||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
||
import { chat, translationToChinese, translation, customTranslation } from "@/api/chat";
|
||
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
|
||
import AgentGuildDialog from '@/components/AgentGuildDialog.vue'
|
||
|
||
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue'; // 根据实际路径修改
|
||
import TranslationDialog from '@/components/translationDialog.vue'; // 根据实际路径修改
|
||
|
||
import ChatDialog from '@/components/ChatDialog.vue'
|
||
import MessageDialogd from '@/components/MessageDialogd.vue'
|
||
import { pickTikTokBundleId } from '@/utils/arrUtils'
|
||
import { logout, updates } from '@/api/account';
|
||
import {
|
||
getDeviceList,
|
||
toHome,
|
||
swipeAction,
|
||
tapAction,
|
||
growAccount,
|
||
stopScript,
|
||
watchLiveForGrowth,
|
||
monitorMessages,
|
||
passAnchorData,
|
||
followAndGreetUnion,
|
||
addTempAnchorData,
|
||
deviceAppList,
|
||
launchApp,
|
||
getChatTextInfo,
|
||
setLoginInfo,
|
||
aiConfig,
|
||
selectLast,
|
||
updatelast,
|
||
changeAccount,
|
||
stopAllTask,
|
||
anchorList,
|
||
restartTikTok
|
||
} from '@/api/ios';
|
||
import ding from '@/assets/mes.wav'
|
||
import { set } from "lodash";
|
||
const router = useRouter();
|
||
const openShowChat = ref(true)
|
||
//主播库
|
||
const showHostDlg = ref(false)
|
||
//ai人设弹框
|
||
const showMyInfo = ref(false)
|
||
//翻译弹框
|
||
const showtransDlg = ref(false)
|
||
let transDlgType = ref('')
|
||
|
||
// 假设这是你已有的数据
|
||
const borkerConfig = reactive({
|
||
agentName: '',
|
||
guildName: '',
|
||
contactTool: '',
|
||
contact: ''
|
||
})
|
||
|
||
//评论 自动化
|
||
let common = ref(true);
|
||
// 自动化
|
||
let auto = ref(true);
|
||
|
||
let initialTextStr = ref('') // 初始文本字符串
|
||
// 批次缓冲(仅用于当前“波”)
|
||
let batch = []; // [{ country, text }]
|
||
let flushTimer = null;
|
||
|
||
let hostList = [] // 主播列表
|
||
let comonList = [] //评论列表
|
||
//查询列表轮询
|
||
let getListtimer = null;
|
||
let userdata = getUser();
|
||
let chatList = ref([])
|
||
|
||
let MesNewList = ref([])
|
||
|
||
// 刷新方法
|
||
const reloadImg = () => {
|
||
refreshAllImgs()
|
||
}
|
||
//start弹窗
|
||
let isMsgPop = ref(false)
|
||
|
||
let runType = ref('')
|
||
let isMonitorOn = ref(false)
|
||
const hoverIndex = ref(null) //选中
|
||
let showDialog = ref(false);//弹窗是否显示
|
||
let dialogTitle = ref('');//当前弹窗类型
|
||
let deviceInformation = ref([])
|
||
// 你可以用这种方式声明按钮们
|
||
//停止中
|
||
let stopLoading = null
|
||
|
||
// 当前是否被其它模式占用(四个互斥按钮专用)
|
||
const isLocked = (type) => !!runType.value && runType.value !== type
|
||
|
||
// —— SSE 弹窗/接收总开关(持久化)——
|
||
const sseEnabled = ref(JSON.parse(localStorage.getItem('SSE_ENABLED') ?? 'true'))
|
||
watch(sseEnabled, v => {
|
||
localStorage.setItem('SSE_ENABLED', JSON.stringify(v))
|
||
if (!v) dropCurrentWave() // 关掉总开关时,立刻丢弃本波缓冲并取消待flush
|
||
})
|
||
|
||
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
|
||
const ctrlStyle = (type) => ({
|
||
backgroundColor: runType.value === type ? 'red' : '',
|
||
opacity: isLocked(type) ? 0.5 : 1,
|
||
pointerEvents: isLocked(type) ? 'none' : 'auto',
|
||
cursor: isLocked(type) ? 'not-allowed' : 'pointer',
|
||
})
|
||
|
||
const buttons = [
|
||
{
|
||
label: '刷新',
|
||
onClick: () => reloadImg(),
|
||
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
|
||
// runType.value = 'like'
|
||
// deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||
dialogTitle.value = '视频评论';
|
||
setTimeout(() => {
|
||
showDialog.value = true;
|
||
|
||
initialTextStr.value = getContentListMultiline();
|
||
}, 500)
|
||
|
||
},
|
||
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: '监测消息',
|
||
onClick: () => {
|
||
if (runType.value == 'listen') {
|
||
deviceInformation.value.forEach((item) => {
|
||
stopScript({ udid: item.deviceId })
|
||
})
|
||
runType.value = ''
|
||
return
|
||
};
|
||
if (isLocked('listen')) return
|
||
//如果传评论就注释一下两行代码,解开后面代码
|
||
runType.value = 'listen'
|
||
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
||
// dialogTitle.value = '评论(无消息将刷视频)';
|
||
// setTimeout(() => {
|
||
// showDialog.value = true;
|
||
|
||
// initialTextStr.value = getContentListMultiline();
|
||
// }, 500)
|
||
|
||
},
|
||
show: () => true,
|
||
img: {
|
||
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
||
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
||
},
|
||
style: () => ctrlStyle('listen')
|
||
|
||
},
|
||
// {
|
||
// label: '定时调度',
|
||
// 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: '全部停止',
|
||
onClick: () => stopAll(100),
|
||
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: () => doLogout(),
|
||
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
|
||
}
|
||
}
|
||
]
|
||
|
||
const isAlliance = ref(false)
|
||
|
||
|
||
// —— 打断器配置 ——
|
||
// 是否开启
|
||
const interruptEnabled = ref(false)
|
||
// 每隔多少分钟打断一次(可做弹窗配置)
|
||
const interruptEveryMin = ref(2)
|
||
// 打断器最大重试次数 & 每次超时(按需调)
|
||
const interruptMaxRetries = 1
|
||
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 {
|
||
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 检查一次是否到切换点
|
||
let scheduleEnabled = ref(false) // 需要时可手动关闭调度(例如“全部停止”)
|
||
// 弹窗
|
||
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() {
|
||
|
||
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))
|
||
|
||
// 若启用则重启轮询
|
||
startScheduleLoop()
|
||
|
||
showScheduleDlg.value = false
|
||
ElMessage.success('已保存定时调度')
|
||
}
|
||
|
||
const selectedDevice = ref(null)
|
||
|
||
|
||
// 每台设备的 <img> 引用
|
||
const imgRefs = ref({}) // { [id]: HTMLImageElement }
|
||
|
||
// 新增:每台设备当前展示的 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');
|
||
}
|
||
// 如果你用的是 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);
|
||
});
|
||
}
|
||
|
||
// —— 关键:登出/离开时“批量硬中断”所有图片流 ——
|
||
async function hardStopAllImgStreams(id, port) {
|
||
const el = imgRefs.value[id]
|
||
console.log("终止", id)
|
||
|
||
if (el) {
|
||
// 硬中断旧请求
|
||
el.src = ''
|
||
el.removeAttribute('src')
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
function refreshAllStopImgs() {
|
||
Object.keys(imgRefs.value).forEach(id => hardStopAllImgStreams(id))
|
||
}
|
||
|
||
|
||
// —— 显示尺寸固定为 320x720;未选中缩略为 THUMB_SCALE 倍 ——
|
||
// 尺寸与排布
|
||
const BASE_W = 320
|
||
const BASE_H = 720
|
||
const THUMB_SCALE = 0.6
|
||
const PER_ROW = 3
|
||
|
||
// 底行上移的位移量:720*(1-0.6)=288
|
||
const BOTTOM_SHIFT = Math.round(BASE_H * (1 - THUMB_SCALE)) // 288
|
||
|
||
// 是否至少有两行
|
||
const hasTwoRows = computed(() => deviceInformation.value.length > PER_ROW)
|
||
|
||
// 真正的“底行”判定:必须有两行以上才成立
|
||
const isBottomRow = (index) => {
|
||
if (!hasTwoRows.value) return false
|
||
const lastRow = Math.floor((deviceInformation.value.length - 1) / PER_ROW)
|
||
return Math.floor(index / PER_ROW) === lastRow
|
||
}
|
||
|
||
// 统一给 .video-canvas 返回 transform(缩略/放大/底行上移)
|
||
function getCanvasStyle(index) {
|
||
const isSelected = selectedDevice.value === index
|
||
if (!isSelected) {
|
||
return { transform: `scale(${THUMB_SCALE})` }
|
||
}
|
||
// 选中:默认正常放大;若在底行且至少两行 -> 先上移再放大
|
||
return isBottomRow(index)
|
||
? { transform: `translateY(-${BOTTOM_SHIFT}px) scale(1)` }
|
||
: { transform: 'scale(1)' }
|
||
}
|
||
// 当前选中的卡片:选中=1倍,未选中=缩略比例
|
||
const imgWH = (index) => {
|
||
const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE
|
||
return {
|
||
width: `${BASE_W * scale}px`,
|
||
height: `${BASE_H * scale}px`,
|
||
transition: 'all 0.3s ease',
|
||
}
|
||
}
|
||
|
||
// 计算某索引当前展示宽高(用于坐标换算)
|
||
const displaySize = (index) => {
|
||
const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE
|
||
return { w: BASE_W * scale, h: BASE_H * scale }
|
||
}
|
||
|
||
// 从 Canvas offset 坐标 → 真实手机分辨率坐标
|
||
const mapToDeviceXY = (index, offsetX, offsetY) => {
|
||
const dev = deviceInformation.value[index] || {}
|
||
const realW = Number(dev.width) || BASE_W // 后端返回的真实分辨率
|
||
const realH = Number(dev.height) || BASE_H
|
||
const rotation = Number(dev.rotation || 0) // 若后端有提供旋转角,可用 0/90/180/270
|
||
|
||
const { w: dispW, h: dispH } = displaySize(index)
|
||
|
||
// 归一化到 0~1
|
||
let nx = Math.min(Math.max(offsetX / dispW, 0), 1)
|
||
let ny = Math.min(Math.max(offsetY / dispH, 0), 1)
|
||
|
||
// 处理旋转(如果你的服务器坐标基于设备原生朝向)
|
||
// 0: 直接映射;90: 顺时针;180、270 同理
|
||
let x, y
|
||
switch (rotation % 360) {
|
||
case 90:
|
||
case -270:
|
||
x = Math.round(ny * realW)
|
||
y = Math.round((1 - nx) * realH)
|
||
break
|
||
case 180:
|
||
case -180:
|
||
x = Math.round((1 - nx) * realW)
|
||
y = Math.round((1 - ny) * realH)
|
||
break
|
||
case 270:
|
||
case -90:
|
||
x = Math.round((1 - ny) * realW)
|
||
y = Math.round(nx * realH)
|
||
break
|
||
default: // 0°
|
||
x = Math.round(nx * realW)
|
||
y = Math.round(ny * realH)
|
||
}
|
||
return { x, y }
|
||
}
|
||
|
||
// 选中:恢复到 320x720 并显示盖层
|
||
const selectDevice = (index) => {
|
||
selectedDevice.value = index
|
||
}
|
||
|
||
// ——— 鼠标交互:按下/移动/抬起 ———
|
||
const dragState = ref({}) // 以 index 作为 key 保存 {ox, oy, t}
|
||
|
||
const onCanvasDown = (udid, e, index) => {
|
||
// 记录起点(Canvas 内 offset)和时间
|
||
const startDev = mapToDeviceXY(index, e.offsetX, e.offsetY) // 也记录“设备坐标”起点,便于直接打印
|
||
dragState.value[index] = {
|
||
ox: e.offsetX,
|
||
oy: e.offsetY,
|
||
t: Date.now(),
|
||
udid,
|
||
startDevXY: startDev,
|
||
startOffsetXY: { x: e.offsetX, y: e.offsetY }
|
||
}
|
||
}
|
||
|
||
const onCanvasMove = (udid, e, index) => {
|
||
// 若要实时观察滑动轨迹(可选)
|
||
const st = dragState.value[index]
|
||
if (!st) return
|
||
const curDev = mapToDeviceXY(index, e.offsetX, e.offsetY)
|
||
// 建议:调试阶段打开,稳定后可注释或做节流
|
||
// console.log('[MOVE]', {
|
||
// udid,
|
||
// offsetXY: { x: e.offsetX, y: e.offsetY },
|
||
// deviceXY: curDev,
|
||
// elapsedMs: Date.now() - st.t
|
||
// })
|
||
}
|
||
|
||
const onCanvasUp = async (udid, e, index) => {
|
||
const st = dragState.value[index]
|
||
if (!st) return
|
||
|
||
const { ox, oy, t, startDevXY, startOffsetXY } = st
|
||
const dx = e.offsetX - ox
|
||
const dy = e.offsetY - oy
|
||
const elapsed = Date.now() - t
|
||
delete dragState.value[index]
|
||
|
||
// 终点(设备坐标 & 画布 offset)
|
||
const endDevXY = mapToDeviceXY(index, e.offsetX, e.offsetY)
|
||
const endOffsetXY = { x: e.offsetX, y: e.offsetY }
|
||
|
||
// ✅ 这里打印:起点/终点(两套坐标)+ 耗时
|
||
console.log('[鼠标滑动,起点/终点)+ 耗时]', {
|
||
udid,
|
||
start: {
|
||
offsetXY: startOffsetXY, // 画布内起点
|
||
deviceXY: startDevXY // 设备坐标起点
|
||
},
|
||
end: {
|
||
offsetXY: endOffsetXY, // 画布内终点
|
||
deviceXY: endDevXY // 设备坐标终点
|
||
},
|
||
deltaOffset: { dx, dy },
|
||
durationMs: elapsed,
|
||
})
|
||
|
||
// === 你原有逻辑:tap or swipe ===
|
||
const MOVE_THR = 5
|
||
const isTap = Math.hypot(dx, dy) < MOVE_THR && elapsed < 500
|
||
|
||
try {
|
||
if (isTap) {
|
||
await tapAction({ udid, x: endDevXY.x, y: endDevXY.y })
|
||
} else {
|
||
// //通过方向判断code 1234
|
||
// const rotation = Number((deviceInformation.value[index] || {}).rotation || 0)
|
||
// const code = getSwipeCodeWithRotation(dx, dy, rotation)
|
||
// await swipeAction({ udid, direction: code })
|
||
|
||
//通过自定义滑动坐标和时间传参
|
||
await swipeAction({ udid, sx: startDevXY.x, sy: startDevXY.y, ex: endDevXY.x, ey: endDevXY.y, duration: elapsed / 1000 })
|
||
}
|
||
} catch (err) {
|
||
console.error(err)
|
||
}
|
||
}
|
||
|
||
/** 方向码:1=上, 2=左, 3=下, 4=右(不返回0,始终给出一个方向) */
|
||
function getSwipeCode(dx, dy) {
|
||
// 哪个轴位移更大就取哪个轴;边界≈45°
|
||
if (Math.abs(dx) >= Math.abs(dy)) {
|
||
return dx < 0 ? 2 : 4 // 左/右
|
||
} else {
|
||
return dy < 0 ? 1 : 3 // 上/下(DOM坐标里向上是负)
|
||
}
|
||
}
|
||
|
||
/** 带设备旋转(0/90/180/270):先把画布向量(dx,dy)旋回设备坐标系再判方向 */
|
||
function getSwipeCodeWithRotation(dx, dy, rotation = 0) {
|
||
let dxD = dx, dyD = dy
|
||
switch ((rotation % 360 + 360) % 360) {
|
||
case 90: dxD = dy; dyD = -dx; break
|
||
case 180: dxD = -dx; dyD = -dy; break
|
||
case 270: dxD = -dy; dyD = dx; break
|
||
default: break
|
||
}
|
||
return getSwipeCode(dxD, dyD)
|
||
}
|
||
|
||
|
||
|
||
async function openTk() {
|
||
if (!deviceInformation.value?.length) {
|
||
ElMessage.warning('暂无在线设备')
|
||
return
|
||
}
|
||
|
||
const loading = ElLoading.service({ text: '正在打开 TikTok …', background: 'rgba(0,0,0,.35)' })
|
||
const results = []
|
||
|
||
try {
|
||
// 为了稳妥,逐台串行(如果你希望更快,可改 Promise.all 并注意并发数)
|
||
for (const dev of deviceInformation.value) {
|
||
const udid = dev.deviceId
|
||
try {
|
||
const apps = await deviceAppList({ udid }) // 期望返回示例中的数组
|
||
const bundleId = pickTikTokBundleId(apps)
|
||
|
||
if (!bundleId) {
|
||
results.push({ udid, ok: false, msg: '未找到 TikTok' })
|
||
continue
|
||
}
|
||
|
||
await launchApp({ udid, bundleId })
|
||
results.push({ udid, ok: true, msg: `已启动 TikTok (${bundleId})` })
|
||
} catch (e) {
|
||
console.error('openTk error', udid, e)
|
||
results.push({ udid, ok: false, msg: '请求失败' })
|
||
}
|
||
}
|
||
} finally {
|
||
loading.close()
|
||
}
|
||
|
||
// 汇总提示(成功/失败各一条)
|
||
const okCount = results.filter(r => r.ok).length
|
||
const fail = results.filter(r => !r.ok)
|
||
if (okCount) ElMessage.success(`已在 ${okCount} 台设备启动 TikTok`)
|
||
if (fail.length) {
|
||
const udids = fail.map(f => f.udid).join(', ')
|
||
ElMessage.error(`以下设备未能启动:${udids}`)
|
||
}
|
||
}
|
||
|
||
function getMesList(deviceId) {
|
||
getChatTextInfo({ udid: deviceId }).then((res) => {
|
||
if (res) {
|
||
chatList.value = res
|
||
console.log(chatList.value)
|
||
getTranslation(chatList.value)
|
||
}
|
||
})
|
||
}
|
||
|
||
async function stopAll(time) {
|
||
stopLoading = ElLoading.service({
|
||
lock: true,
|
||
text: '停止中',
|
||
background: 'rgba(0, 0, 0, 0.7)',
|
||
});
|
||
|
||
scheduleEnabled.value = false;
|
||
runType.value = '';
|
||
isMsgPop.value = false;
|
||
dropCurrentWave();
|
||
|
||
try {
|
||
// 1) 等待接口完成
|
||
await stopAllTask(deviceInformation.value.map(item => item.deviceId));
|
||
// 2) 等待 2 秒(和你原逻辑一致)
|
||
await new Promise(r => setTimeout(r, time));
|
||
|
||
stopLoading.close();
|
||
console.log('全部停止成功', printCurrentTime());
|
||
ElMessage.success('全部停止成功');
|
||
|
||
// 3) 明确返回(可选)
|
||
return true;
|
||
} catch (e) {
|
||
console.log('停止失败', printCurrentTime(), e);
|
||
ElMessage.error('脚本已停止');
|
||
stopLoading.close();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
//确认多行文本框内容
|
||
function onDialogConfirm(result, type, index, data) {
|
||
console.log(type, result, data);
|
||
if (type == '主播ID') {
|
||
hostList = (result || []).map(id => ({ id, country: '' }))
|
||
|
||
//无需评论,注释,如果需要注释下面代码,放开后面代码
|
||
// dialogTitle.value = '私信';
|
||
// setTimeout(() => {
|
||
// showDialog.value = true;
|
||
|
||
// initialTextStr.value = getContentpriListMultiline();
|
||
// }, 500)
|
||
dialogTitle.value = '评论';
|
||
setTimeout(() => {
|
||
showDialog.value = true;
|
||
|
||
initialTextStr.value = getContentListMultiline();
|
||
}, 500)
|
||
} else if (type == '评论') {
|
||
comonList = result
|
||
setContentList(result)
|
||
common.value = data.common
|
||
// dialogTitle.value = '私信';
|
||
// setTimeout(() => {
|
||
// showDialog.value = true;
|
||
|
||
// initialTextStr.value = getContentpriListMultiline();
|
||
// }, 500)
|
||
transDlgType.value = '私信'
|
||
showtransDlg.value = true
|
||
} else if (type == '私信') {
|
||
// runType.value = 'follow'
|
||
// setContentpriList(result)
|
||
|
||
// 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: data.auto,
|
||
// needTranslate: data.needTranslate,
|
||
// }
|
||
// ).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,
|
||
// comment: comonList,
|
||
// needReply: data.auto,
|
||
// needTranslate: data.needTranslate,
|
||
// isComment: data.common
|
||
// }
|
||
// ).then((res) => {
|
||
// ElMessage({ type: 'success', message: '任务开启成功' });
|
||
|
||
// hostList = []
|
||
// })
|
||
|
||
// }
|
||
} 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 }))
|
||
}
|
||
|
||
}
|
||
|
||
onMounted(async () => {
|
||
const loading = ElLoading.service({
|
||
lock: true,
|
||
text: '检测设备中...',
|
||
background: 'rgba(0, 0, 0, 0.7)',
|
||
});
|
||
getDeviceListFun()
|
||
|
||
|
||
const res = await window.electronAPI.isiproxy({
|
||
intervalMs: 2000,
|
||
exeName: 'iproxy.exe',
|
||
// maxWaitMs: 3000, // 可选:5分钟超时
|
||
maxWaitMs: 300000, // 可选:5分钟超时
|
||
});
|
||
if (res.running) {
|
||
// 检测到了,你再决定是否跳转
|
||
loading.close();
|
||
console.log('检测到了')
|
||
} else {
|
||
// 超时兜底提示
|
||
loading.close();
|
||
console.log('未检测到设备')
|
||
ElMessage.error(`未检测到设备`)
|
||
|
||
}
|
||
|
||
|
||
window.electronAPI.startMq(userdata.tenantId, userdata.id)
|
||
// 初始化时获取设备列表
|
||
|
||
//每3秒获取一次设备列表 消息列表 和 查询主播列表是否还有主播
|
||
getListtimer = setInterval(async () => {
|
||
getDeviceListFun()
|
||
selectLastFun()
|
||
const hostsList = await getStoredHostList()
|
||
// console.log(hostsList.length)
|
||
//当私信主播时,主播列表没有数据了,提示列表空了 并且关闭私信
|
||
if (runType.value == 'follow') {
|
||
if (hostsList.length <= 0) {
|
||
await stopAll(5000)
|
||
runType.value = 'like'
|
||
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||
ElMessageBox.alert('私信全部完成!(刷视频中)', '提示', {
|
||
confirmButtonText: 'OK',
|
||
callback: (action) => {
|
||
|
||
},
|
||
})
|
||
}
|
||
}
|
||
|
||
}, 3000)
|
||
|
||
setInterval(async () => {
|
||
await checkVPN()
|
||
}, 1000 * 20)
|
||
|
||
if (!await isAiConfig()) {
|
||
showMyInfo.value = true
|
||
}
|
||
reconcileLoopsByDevices(deviceInformation.value || []);
|
||
|
||
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) => {
|
||
// console.log('来自服务端:', data);
|
||
// console.log(1)
|
||
//总开关
|
||
if (!sseEnabled.value) return
|
||
|
||
if (data === 'start') {
|
||
// 新一波开始:清空上一波的缓冲,重置防抖
|
||
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 : '')
|
||
const id = data && data.id != null ? data.id : ''
|
||
if (!text) return
|
||
|
||
batch.push({ country, text, invitationType, id })
|
||
|
||
scheduleFlush((items) => {
|
||
// 批量入库
|
||
const list = items.map(h => ({
|
||
anchorId: h.text,
|
||
country: h.country || '',
|
||
invitationType: h.invitationType,
|
||
state: stateByInvType(h.invitationType),
|
||
}))
|
||
updates(items.map(h => ({
|
||
id: h.id,
|
||
aiOperation: 1,
|
||
})))
|
||
addTempAnchorData(list)
|
||
}, 400)
|
||
})
|
||
|
||
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
clearInterval(getListtimer)
|
||
getListtimer = null
|
||
})
|
||
|
||
let isStartLac = false
|
||
const getDeviceListFun = () => {
|
||
getDeviceList().then((res) => {
|
||
// console.log('返回', res.length)
|
||
if (res && res.length > 0 && deviceInformation.value.length !== res.length) {
|
||
console.log("设备变更")
|
||
deviceInformation.value = res
|
||
// refreshAllStopImgs()
|
||
// reloadImg()
|
||
}
|
||
|
||
if (res.length == 0) {
|
||
deviceInformation.value = []
|
||
// refreshAllStopImgs()
|
||
// reloadImg()
|
||
}
|
||
// deviceInformation.value = ['', '', '', '', '', '',]
|
||
}).catch((err) => {
|
||
if (isStartLac) {
|
||
ElMessage.error(`IOSAI服务错误`)
|
||
isStartLac = true
|
||
} else {
|
||
|
||
}
|
||
|
||
})
|
||
}
|
||
|
||
//获取新消息
|
||
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 + '号设备'
|
||
}
|
||
})
|
||
});
|
||
|
||
MesNewList.value = [...mesInfoData];
|
||
})
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
}
|
||
}
|
||
function runTask(key, deviceId, type) {
|
||
console.log('[schedule] 切换到任务:', key, printCurrentTime())
|
||
|
||
forceActivate(key, async () => {
|
||
if (key === 'follow') {
|
||
console.log("进入follow", scheduleEnabled.value)
|
||
if (scheduleEnabled.value) {
|
||
if (!deviceId) {
|
||
await stopAll(100)
|
||
} else {
|
||
passAnchorData(
|
||
{
|
||
deviceList: [deviceId],
|
||
anchorList: [],
|
||
prologueList: getContentpriList(),
|
||
comment: comonList,
|
||
needReply: auto.value
|
||
}
|
||
).then((res) => {
|
||
hostList = []
|
||
})
|
||
|
||
return
|
||
}
|
||
//第一个小时结束后,第二轮开始的时候,直接进入follow
|
||
setTimeout(() => {
|
||
runType.value = 'follow'
|
||
|
||
passAnchorData(
|
||
{
|
||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||
anchorList: [],
|
||
prologueList: getContentpriList(),
|
||
comment: comonList,
|
||
needReply: auto.value
|
||
}
|
||
).then((res) => {
|
||
hostList = []
|
||
})
|
||
}, 1000)
|
||
|
||
return
|
||
}
|
||
|
||
scheduleEnabled.value = true
|
||
initialTextStr.value = '';
|
||
dialogTitle.value = '主播ID';
|
||
showDialog.value = true;
|
||
|
||
} else if (key === 'like') {
|
||
|
||
if (!deviceId) {
|
||
await stopAll(100)
|
||
} else {
|
||
growAccount({ udid: deviceId })
|
||
return
|
||
}
|
||
|
||
setTimeout(() => {
|
||
scheduleEnabled.value = true
|
||
runType.value = 'like'
|
||
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||
}, 1000)
|
||
|
||
} else if (key === 'brushLive') {
|
||
if (!deviceId) {
|
||
await stopAll(100)
|
||
} else {
|
||
watchLiveForGrowth({ udid: deviceId })
|
||
return
|
||
}
|
||
|
||
setTimeout(() => {
|
||
scheduleEnabled.value = true
|
||
runType.value = 'brushLive'
|
||
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
||
}, 1000)
|
||
runType.value = 'brushLive'
|
||
} else if (key === 'listen') {
|
||
if (!deviceId) {
|
||
await stopAll(100)
|
||
} else {
|
||
monitorMessages({ udid: deviceId })
|
||
return
|
||
}
|
||
|
||
setTimeout(() => {
|
||
runType.value = 'listen'
|
||
scheduleEnabled.value = true
|
||
isMonitorOn.value = true
|
||
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
||
}, 1000)
|
||
|
||
}
|
||
})
|
||
}
|
||
|
||
async function stopCurrentMode() {
|
||
// 如果你希望“只停当前片段的设备”,也可以用 stopScript 针对设备循环
|
||
await stopAll(100)
|
||
}
|
||
|
||
/** 恢复:回到 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
|
||
}
|
||
|
||
/**
|
||
* 执行一次中断器函数
|
||
* 该函数会尝试使用设备列表中的每个设备更改账户,并检查是否所有操作都成功
|
||
* @returns {Promise<boolean>} 返回一个Promise,解析为布尔值,表示操作是否全部成功
|
||
*/
|
||
async function runInterrupterOnce() {
|
||
// 定义一个异步函数,用于执行设备账户更改操作
|
||
const promiseFn = async () => {
|
||
// 从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);
|
||
}
|
||
|
||
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() {
|
||
lastInterruptTs = Date.now()
|
||
localStorage.setItem('INT_LAST_TS', String(lastInterruptTs))
|
||
// 先按当前 index 跑一次,保持“即刻对齐”
|
||
runTask(schedulePlan[scheduleState.index].key, null, 1)
|
||
|
||
if (scheduleTimer) clearInterval(scheduleTimer)
|
||
|
||
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 * 60
|
||
|
||
console.log(
|
||
'due=', due,
|
||
'elapsed=', now - lastInterruptTs,
|
||
'threshold=', interruptEveryMin.value * 60_000 * 60
|
||
)
|
||
if (due) {
|
||
interrupting = true
|
||
try {
|
||
// 记录暂停前的进度(用于恢复)
|
||
const cur = schedulePlan[scheduleState.index]
|
||
const elapsedBeforePause = now - scheduleState.startTime
|
||
pauseSnapshot = { index: scheduleState.index, elapsedBeforePause }
|
||
|
||
// 停掉当前片段
|
||
await stopAll(100)
|
||
|
||
|
||
// 执行中断任务(带重试)
|
||
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 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()
|
||
}
|
||
|
||
|
||
function onSave(payload) {
|
||
console.log(payload)
|
||
aiConfig(payload).then((res) => {
|
||
|
||
})
|
||
|
||
}
|
||
|
||
|
||
async function isAiConfig() {
|
||
const res = await window.electronAPI.fileExists();
|
||
|
||
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
|
||
}
|
||
|
||
|
||
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()
|
||
|
||
})
|
||
}
|
||
|
||
//查看主播库主播信息
|
||
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;
|
||
}
|
||
};
|
||
|
||
// 语言选项(也可以使用内置默认)
|
||
// const languages = [
|
||
// { label: '简体中文 (zh-CN)', value: 'zh-CN' },
|
||
// { label: 'English (en)', value: 'en' },
|
||
// { label: '日本語 (ja)', value: 'ja' },
|
||
// ]
|
||
|
||
// 模拟翻译函数:你可以接入自己后端或第三方接口 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 []
|
||
}
|
||
}
|
||
|
||
// 接收“确定”事件返回结果
|
||
function onConfirm({ type, strings, autoBlo }) {
|
||
console.log('✅ 确认返回:', type, strings, autoBlo)
|
||
auto.value = autoBlo
|
||
showtransDlg.value = false
|
||
runType.value = 'follow'
|
||
setContentpriList(strings)
|
||
|
||
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: strings,
|
||
needReply: autoBlo,
|
||
// needTranslate: data.needTranslate,
|
||
}
|
||
).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: strings, //私信对象
|
||
comment: comonList, //评论列表
|
||
needReply: autoBlo, //自动回复
|
||
// needTranslate: data.needTranslate,
|
||
isComment: common.value //是否评论
|
||
}
|
||
).then((res) => {
|
||
ElMessage({ type: 'success', message: '任务开启成功' });
|
||
console.log("启动成功")
|
||
hostList = []
|
||
})
|
||
|
||
}
|
||
}
|
||
|
||
function arrayToString(arr) {
|
||
// 过滤空项并用 \n 连接
|
||
return arr.filter(Boolean).join(' \n')
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
@import '../static/css/video.less';
|
||
</style>
|