2.9.1最终稳定版

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

View File

@@ -31,3 +31,8 @@ export function comment() {
export function logout(data) {
return postAxios({ url: 'api/user/aiChat-logout', data })
}
//获取账号状态
export function health(data) {
return getAxios({ url: 'api/common/health' })
}

View File

@@ -3,7 +3,7 @@
:show-close="false" @close="onClose">
<template #header>
<div class="flex items-center justify-between w-full">
<div class="text-lg font-semibold">多语翻译</div>
<div class="text-lg font-semibold">打招呼内容</div>
<!-- <div class="text-xs text-gray-500">可输入多句文本 选择语种 一键翻译 可编辑&持久化 确定返回某语种结果</div> -->
</div>
</template>
@@ -21,6 +21,12 @@
<el-button type="success" @click="exportPrologue()">
导入私信
</el-button>
<el-tooltip class="box-item" effect="dark" content="关闭后,招呼内容将不再翻译" placement="bottom">
<el-switch v-model="isTranslation" class="fancy-switch" :width="70" inline-prompt
active-text="翻译" inactive-text="翻译" style="margin-left:16px" />
</el-tooltip>
<el-text style="margin-left: 10px;" type="primary"> </el-text>
</div>
</div>
</template>
@@ -43,7 +49,7 @@
</el-card>
<!-- 语种与翻译控制区 -->
<el-card shadow="never" class="!border rounded-xl">
<el-card v-show="isTranslation" shadow="never" class="!border rounded-xl">
<template #header>
<div class="flex items-center justify-between">
<span class="font-medium">语种与翻译</span>
@@ -90,7 +96,7 @@
<div class="col-span-12 lg:col-span-6">
<el-alert title="说明" type="info" :closable="false" show-icon class="mb-3"
description="选择语种后点击『翻译』。翻译结果可在下方标签页逐条手动调整,自动保存开启时会同步到本地。" />
description="选择语种后点击『翻译』。翻译结果可在下方标签页逐条手动调整,选择对应地区国家所有语言语种。" />
</div>
</div>
@@ -136,16 +142,17 @@
<template #footer>
<div class="flex items-center justify-between w-full">
<div class="text-xs text-gray-500">
<div class="text-xs text-gray-500" v-show="isTranslation">
{{ sentences.length }} · 选择 {{ selectedLangs.length }} 种语言
<!-- <template v-if="activeTab"> · 当前返回{{ getLangLabel(activeTab) }}</template> -->
</div>
<div class="space-x-2">
自动回复
<el-tooltip class="box-item" effect="dark" content="开启后,检测到新私信将自动回复" placement="bottom">
<el-tooltip class="box-item" effect="dark"
:content="userdata.aiReplay ? '开启后,检测到新私信将自动回复' : '未开通,请联系管理员开通'" placement="bottom">
<el-switch style="margin-right: 20px;" v-model="auto" />
<el-switch :disabled="!userdata.aiReplay" style="margin-right: 20px;" v-model="auto" />
</el-tooltip>
<el-button @click="onClose">取消</el-button>
<el-button type="primary" :disabled="!canConfirm" @click="onConfirm">确定</el-button>
@@ -159,6 +166,10 @@
import { reactive, ref, watch, computed, onMounted } from 'vue'
import { prologue } from '@/api/account';
import { Loading } from '@element-plus/icons-vue' // ✅ 旋转图标
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUser } from '@/stores/storage'
let userdata = ref(getUser());
const suppressCancelNext = ref(false) // ✅ 下次关闭是否屏蔽 cancel 事件
/**
* Props & EmitsJS 版)
@@ -179,7 +190,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue', 'confirm'])
let auto = ref(true);
let auto = ref(false);
const visible = computed({
get: () => props.modelValue,
@@ -198,6 +209,21 @@ const autoSave = ref(true)
const activeTab = ref('')
const engine = ref('custom') // 'custom' | 'local'
const MAX_SENTENCES = 30
function clampSentences(list) {
const trimmed = list.slice(0, MAX_SENTENCES)
if (list.length > MAX_SENTENCES) {
ElMessage({
type: 'warning',
message: `最多支持 ${MAX_SENTENCES} 条,已保留前 ${MAX_SENTENCES} 条。`,
duration: 2500,
})
}
return trimmed
}
const isTranslation = ref(false)
watch(selectedLangs, (langs) => {
const keep = new Set(langs)
@@ -220,8 +246,8 @@ watch(selectedLangs, (langs) => {
/** 默认语言选项 */
const defaultLanguages = [
{ label: '英语', value: 'en' },
{ label: '简体中文', value: 'zh-CN' },
{ label: '繁体中文', value: 'zh-TW' },
{ label: '简体中文', value: 'zh' },
{ label: '繁体中文', value: 'zh_tw' },
{ label: '俄语', value: 'ru' },
{ label: '日语', value: 'ja' },
{ label: '韩语', value: 'ko' },
@@ -329,16 +355,28 @@ function importFromTextarea() {
.split(/\r?\n/)
.map((s) => s.trim())
.filter(Boolean)
sentences.splice(0, sentences.length, ...lines)
const limited = clampSentences(lines)
sentences.splice(0, sentences.length, ...limited)
syncLengths()
if (autoSave.value) saveToLocal()
}
//新增一行时限制
function addSentence() {
if (sentences.length >= MAX_SENTENCES) {
ElMessage({
type: 'warning',
message: `最多只可添加 ${MAX_SENTENCES} 条。`,
})
return
}
sentences.push('')
syncLengths()
if (autoSave.value) saveToLocal()
}
function removeSentence(i) {
sentences.splice(i, 1)
syncLengths()
@@ -432,11 +470,12 @@ function loadFromLocal() {
const key = (k) => `${storagePrefix.value}:${k}`
try {
const s = JSON.parse(localStorage.getItem(key('sentences')) || '[]')
const limited = clampSentences(Array.isArray(s) ? s : [])
const langs = JSON.parse(localStorage.getItem(key('selectedLangs')) || '[]')
const a = JSON.parse(localStorage.getItem(key('activeTab')) || '""')
const t = JSON.parse(localStorage.getItem(key('translations')) || '{}')
sentences.splice(0, sentences.length, ...(Array.isArray(s) ? s : []))
sentences.splice(0, sentences.length, ...limited)
selectedLangs.value = Array.isArray(langs) ? langs : []
activeTab.value = typeof a === 'string' ? a : ''
@@ -460,14 +499,46 @@ watch([sentences, selectedLangs, translations, activeTab], () => {
if (autoSave.value) saveToLocal()
}, { deep: true })
function onConfirm() {
async function onConfirm() {
if (!activeTab.value) return
suppressCancelNext.value = true // ✅ 告诉下次关闭不要触发 cancel
// 拷贝成普通对象,避免把响应式引用直接抛给外部
// 如果关闭了翻译功能,则先提示
if (!isTranslation.value) {
try {
await ElMessageBox.confirm(
'当前「翻译」开关已关闭,打招呼内容将不会被翻译,只会使用原文发送。是否继续?',
'提示',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
customClass: 'confirm-box-sm',
}
)
// 用户点击“继续” -> 继续执行提交逻辑
} catch {
// 用户点击“取消” -> 直接退出
ElMessage({
type: 'info',
message: '已取消发送',
})
return
}
}
// === 以下是原逻辑 ===
suppressCancelNext.value = true
const out = JSON.parse(JSON.stringify(translations))
// 追加 yolo 为原始内容(源句子数组)
// 追加原始内容
out.yolo = sentences.slice()
emit('confirm', { type: props.type, strings: out, autoBlo: auto.value })
emit('confirm', {
type: props.type,
strings: out,
autoBlo: auto.value,
needTranslate: isTranslation.value,
})
}
function onClose() {
@@ -536,4 +607,73 @@ onMounted(() => {
.rounded-xl {
border-radius: 0.75rem;
}
/* === 紧凑版 fancy-switch不依赖外部库 === */
:deep(.fancy-switch) {
vertical-align: middle;
}
/* 轨道部分(略大于默认) */
:deep(.fancy-switch .el-switch__core) {
height: 28px;
/* 比默认略高 */
border-radius: 9999px;
border: 1px solid rgba(255, 255, 255, 0.14);
transition: all 0.25s ease;
font-weight: 600;
}
/* 滑块部分(略缩小) */
:deep(.fancy-switch .el-switch__action) {
width: 22px;
height: 22px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
transition: all 0.25s ease;
}
/* 关闭态:暗色背景,灰色文字 */
:deep(.fancy-switch:not(.is-checked) .el-switch__core) {
background: linear-gradient(180deg, #1f2937, #111827) !important;
color: #9CA3AF;
}
/* 开启态:蓝绿渐变高亮 */
:deep(.fancy-switch.is-checked .el-switch__core) {
background: linear-gradient(90deg, #60a5fa, #34d399) !important;
color: #0b1220;
box-shadow:
0 0 0 2px rgba(96, 165, 250, .18),
0 4px 18px rgba(52, 211, 153, .25);
}
/* 滑块开启态:更亮一点 */
:deep(.fancy-switch.is-checked .el-switch__action) {
background: #fff;
box-shadow: 0 4px 16px rgba(52, 211, 153, .3);
}
/* 呼吸动画(更柔和版本) */
@keyframes glowPulseSmall {
0% {
box-shadow: 0 0 0 2px rgba(96, 165, 250, .16), 0 4px 18px rgba(52, 211, 153, .25);
}
50% {
box-shadow: 0 0 0 5px rgba(96, 165, 250, .08), 0 4px 22px rgba(52, 211, 153, .30);
}
100% {
box-shadow: 0 0 0 2px rgba(96, 165, 250, .16), 0 4px 18px rgba(52, 211, 153, .25);
}
}
:deep(.fancy-switch.is-checked .el-switch__core) {
animation: glowPulseSmall 2.5s ease-in-out infinite;
}
/* 键盘焦点高亮 */
:deep(.fancy-switch .el-switch__input:focus-visible + .el-switch__core) {
outline: 2px solid rgba(96, 165, 250, .55);
outline-offset: 2px;
}
</style>

View File

@@ -231,6 +231,11 @@ video {
width: 30px;
margin: 20px;
}
span {
size: 10px;
color: red;
}
}
.left-button:hover {

View File

@@ -20,11 +20,11 @@ function attachInterceptors(instance) {
instance.interceptors.request.use((config) => {
// 登录/换租户接口可能不需要 token根据你的需求放行
const urlLast = sliceUrl(config.url || '')
if ((urlLast === 'prologue' || urlLast === 'comment' || urlLast === 'aiChat-logout' || urlLast === 'updates')) {
if ((urlLast === 'prologue' || urlLast === 'comment' || urlLast === 'aiChat-logout' || urlLast === 'updates' || urlLast === 'health')) {
config.headers['vvtoken'] = getToken()
}
// 超时 & 通用头
config.timeout = 600000
config.timeout = 180000
if (!config.headers) config.headers = {}
// 大多数 POST 走 x-www-form-urlencoded保持你原来的行为
// console.log(config.method)
@@ -38,7 +38,8 @@ function attachInterceptors(instance) {
// 响应拦截器
instance.interceptors.response.use(
(response) => {
const data = response.data
const data = response.data // 请求的 返回数据
const url = response.config.url // 请求的 url
if (data?.code === 0 || data?.code === 200) {
// 成功:返回业务数据(没有就回传原 data
return (data?.data !== undefined) ? data.data : data
@@ -62,7 +63,12 @@ function attachInterceptors(instance) {
if (error.code === 'ERR_NETWORK') {
// 你原来的 isStart 逻辑如果要保留,只控制是否弹 toast但**不要 return**
if (!isStart) {
ElMessage.error('网络请求失败')
if (url === 'deviceList') {
} else {
ElMessage.error('网络请求失败')
}
}
isStart = false
} else if (error.code === 'ECONNABORTED') {

View File

@@ -79,7 +79,7 @@ import { getToken, setToken, setUser, setUserPass, getUserPass } from '@/stores/
import { ElLoading, ElMessage } from 'element-plus';
import { passToken } from '@/api/ios';
let version = ref('2.7.0');
let version = ref('2.8.0');
onMounted(() => {

View File

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