+
共 {{ sentences.length }} 句 · 选择 {{ selectedLangs.length }} 种语言
自动回复
-
+
-
+
取消
确定
@@ -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 & Emits(JS 版)
@@ -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;
+}
\ No newline at end of file
diff --git a/src/static/css/video.less b/src/static/css/video.less
index d9312b0..0672371 100644
--- a/src/static/css/video.less
+++ b/src/static/css/video.less
@@ -231,6 +231,11 @@ video {
width: 30px;
margin: 20px;
}
+
+ span {
+ size: 10px;
+ color: red;
+ }
}
.left-button:hover {
diff --git a/src/utils/axios.js b/src/utils/axios.js
index 3e3d30c..67c30e4 100644
--- a/src/utils/axios.js
+++ b/src/utils/axios.js
@@ -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') {
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index d0b41ef..085a6ef 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -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(() => {
diff --git a/src/views/VideoStream.vue b/src/views/VideoStream.vue
index 482ca9a..0f514f0 100644
--- a/src/views/VideoStream.vue
+++ b/src/views/VideoStream.vue
@@ -8,11 +8,13 @@
-
执行主播库
@@ -42,7 +44,9 @@
重置tiktok
获取当前聊天记录
停止任务
-
开启
+
+ 开启
+
@@ -55,11 +59,11 @@
+ :index="selectedDevice" @confirm="onDialogConfirm" @cancel="stopAll(2000)" />
+ storage-key-prefix="demo-translation" @confirm="onConfirm" @cancel="stopAll(2000)" />
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 } 去重并发
+
+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;
+ }
+ });
+};