编辑ai人设,新消息提醒

This commit is contained in:
2025-09-11 16:46:12 +08:00
parent b2bccb622a
commit 515cbab7c3
9 changed files with 504 additions and 35 deletions

View File

@@ -1,10 +1,11 @@
# iOS 控制服务
# VUE_APP_BASE_LOCAL=http://192.168.1.218:34567/
VUE_APP_BASE_LOCAL=http://127.0.0.1:34567/
# VUE_APP_BASE_LOCAL=http://192.168.1.209:34567/
# 业务后端(开发用内网地址)
# VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com
VUE_APP_BASE_REMOTE=http://192.168.1.144:8101/
VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com
# VUE_APP_BASE_REMOTE=http://192.168.1.144:8101/
# AI 服务
VUE_APP_BASE_SPECIAL=http://ai.zhukeping.com
VUE_APP_BASE_SPECIAL=https://ai.yolozs.com

View File

@@ -5,4 +5,4 @@ VUE_APP_BASE_LOCAL=http://127.0.0.1:34567/
VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com
# AI 服务(如支持 HTTPS最好用 https
VUE_APP_BASE_SPECIAL=http://ai.zhukeping.com
VUE_APP_BASE_SPECIAL=https://ai.yolozs.com

View File

@@ -71,4 +71,8 @@ export function anchorList(data) {
//设置主播列表
export function deleteAnchorWithIds(data) {
return postAxios({ url: 'deleteAnchorWithIds', data })
}
//设置经纪人信息
export function aiConfig(data) {
return postAxios({ url: 'aiConfig', data })
}

View File

@@ -0,0 +1,122 @@
<template>
<el-dialog :model-value="modelValue" :title="title" :width="width" :close-on-click-modal="false" append-to-body
@open="onOpen" @close="onClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="经纪人" prop="agentName">
<el-input v-model="form.agentName" placeholder="请输入经纪人名字" maxlength="50" show-word-limit clearable />
</el-form-item>
<el-form-item label="公会" prop="guildName">
<el-input v-model="form.guildName" placeholder="请输入公会名字" maxlength="50" show-word-limit clearable />
</el-form-item>
<!-- 自由输入的联系工具可不填 -->
<el-form-item label="联系工具" prop="contactTool">
<el-input v-model="form.contactTool" placeholder="例如:微信 / Telegram / 邮箱 / WhatsApp / 其它(可不填)"
maxlength="30" show-word-limit clearable />
</el-form-item>
<!-- 联系方式仅当联系工具已填写时才必填 -->
<el-form-item label="联系方式" prop="contact">
<el-input v-model="form.contact" placeholder="请输入联系方式(当填写了联系工具时必填)" maxlength="100" show-word-limit
clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="onCancel"> </el-button>
<el-button type="primary" :loading="submitting" @click="onConfirm"> </el-button>
</template>
</el-dialog>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
title: { type: String, default: '设置经纪信息' },
width: { type: [String, Number], default: '480px' },
// 初始值:新增 contactTool 字段(可为空)
model: {
type: Object,
default: () => ({
agentName: '',
guildName: '',
contactTool: '',
contact: ''
})
}
})
const emits = defineEmits(['update:modelValue', 'save', 'open'])
const formRef = ref()
const submitting = ref(false)
const form = reactive({
agentName: '',
guildName: '',
contactTool: '',
contact: ''
})
function syncFromProps() {
form.agentName = props.model?.agentName ?? ''
form.guildName = props.model?.guildName ?? ''
form.contactTool = props.model?.contactTool ?? ''
form.contact = props.model?.contact ?? ''
}
watch(() => props.modelValue, v => { if (v) syncFromProps() })
function onOpen() {
syncFromProps()
emits('open')
}
function onClose() { emits('update:modelValue', false) }
function onCancel() { onClose() }
const rules = {
agentName: [{ required: true, message: '请输入经纪人名字', trigger: 'blur' }],
guildName: [{ required: true, message: '请输入公会名字', trigger: 'blur' }],
// contactTool 不必填,不加 required
contact: [
{
validator: (rule, value, cb) => {
const tool = (form.contactTool || '').trim()
const contact = (value || '').trim()
if (tool && !contact) {
return cb(new Error('已填写联系工具时,联系方式为必填'))
}
cb()
},
trigger: ['blur', 'change']
}
]
}
async function onConfirm() {
try {
submitting.value = true
await formRef.value?.validate()
// 提交前做一次 trim
const payload = {
agentName: form.agentName.trim(),
guildName: form.guildName.trim(),
contactTool: form.contactTool.trim(),
contact: form.contact.trim()
}
emits('save', payload)
onClose()
} finally {
submitting.value = false
}
}
</script>
<style scoped>
:deep(.el-dialog__body) {
padding-top: 12px;
}
</style>

View File

@@ -3,7 +3,7 @@
<div class="dialog-content">
<h3 class="text-lg font-bold mb-4">
<img style="margin: 0px 15px;" src="@/assets/video/chatMes.png"></img>
消息内容
消息翻译内容
</h3>
<el-scrollbar class="chat-box">
<div v-for="(msg, index) in messages.filter(m => m.type !== 'time')" :key="index"
@@ -71,6 +71,7 @@ function fallbackCopyTextToClipboard(index, text) {
}
.dialog-content {
margin-top: 5vh;
background: rgb(246, 246, 246);
padding: 0px 20px 20px 20px;
border-radius: 12px;
@@ -81,7 +82,7 @@ function fallbackCopyTextToClipboard(index, text) {
display: flex;
flex-direction: column;
gap: 10px;
height: 80vh;
height: 35vh;
}
.left-message {

View File

@@ -0,0 +1,197 @@
<template>
<div v-if="visible" class="dialog-overlay" @click.self="close">
<div class="dialog-content">
<h3 class="text-lg font-bold mb-4">
<img style="margin: 0 15px;" src="@/assets/video/chatMes.png" />
新消息提醒
</h3>
<el-scrollbar class="chat-box">
<div v-for="(item, idx) in normalizedMessages" :key="idx" class="msg-item">
<div class="meta" @click="onClickMessage(item)">
<span class="name">{{ item.sender || '未知用户' }}</span>
<span class="sep">·</span>
<span class="device">{{ item.device || '未知设备' }}</span>
</div>
<div class="bubble" :class="isSeen(item) ? 'seen' : 'unread'" :style="bubbleStyle(item)"
@click="onClickMessage(item)">
{{ item.text }}
</div>
</div>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
visible: Boolean,
messages: {
type: Array,
required: true,
default: () => []
},
// 可自定义颜色
reminderColor: {
type: String,
// Element Plus 警告浅色(看起来像“提醒”)
default: 'var(--el-color-warning-light-5, #ffe58f)' // 更深一点
},
seenColor: {
type: String,
// EP 填充浅色(更“平静”的已读态)
default: 'var(--el-fill-color-light, #f2f3f5)'
}
})
const emit = defineEmits(['close', 'seen'])
const close = () => emit('close')
// 记录哪些消息已读(用原始索引做 key避免过滤后索引错位
const seenSet = ref(new Set())
// 规范化并带上原始索引,过滤掉 type === 'time'
const normalizedMessages = computed(() => {
const res = []
; (props.messages || []).forEach((m, i) => {
if (m?.type === 'time') return
res.push({
origIndex: i,
raw: m,
sender: m?.sender ?? m?.name ?? m?.user ?? m?.from ?? '',
device: m?.device ?? m?.deviceName ?? m?.udid ?? '',
text: m?.text ?? m?.content ?? ''
})
})
return res
})
// 初始化:如果消息本身标注了 seen/read则置为已读
watch(
() => props.messages,
(list) => {
const s = new Set()
list?.forEach((m, i) => {
if (m && (m.seen === true || m.read === true)) s.add(i)
})
seenSet.value = s
},
{ immediate: true }
)
function isSeen(item) {
return seenSet.value.has(item.origIndex)
}
function markSeen(item) {
if (!isSeen(item)) {
seenSet.value.add(item.origIndex)
// 通知父组件(可选):你也可以在父里把 messages[item.origIndex].seen = true 持久化
emit('seen', { index: item.origIndex, message: item.raw })
}
}
function bubbleStyle(item) {
return {
backgroundColor: isSeen(item) ? props.seenColor : props.reminderColor,
color: '#333'
}
}
// 点击:标记已读 + 复制文本
function onClickMessage(item) {
markSeen(item)
fallbackCopyTextToClipboard(item.text)
}
// 兜底复制
function fallbackCopyTextToClipboard(text) {
// const textArea = document.createElement('textarea')
// textArea.value = text || ''
// textArea.style.position = 'fixed'
// textArea.style.left = '-999999px'
// textArea.style.top = '-999999px'
// document.body.appendChild(textArea)
// textArea.focus()
// textArea.select()
try {
// const ok = document.execCommand('copy')
// ok ? ElMessage.success('已读') : ElMessage.error('复制失败1')
ElMessage.success('已读')
} catch (err) {
// ElMessage.error('复制失败2')
} finally {
// document.body.removeChild(textArea)
}
}
</script>
<style scoped>
.dialog-overlay {
display: flex;
justify-content: center;
align-items: center;
}
.dialog-content {
margin-top: 3vh;
background: rgb(246, 246, 246);
padding: 0 20px 20px 20px;
border-radius: 12px;
width: 320px;
}
.chat-box {
display: flex;
flex-direction: column;
gap: 10px;
height: 35vh;
}
/* 全部左对齐 */
.msg-item {
align-self: flex-start;
width: 100%;
margin-bottom: 12px;
}
.meta {
font-size: 12px;
color: #666;
margin: 2px 0 6px;
display: flex;
align-items: center;
gap: 6px;
}
.meta .name {
font-weight: 600;
color: #333;
}
.meta .sep {
color: #999;
}
.meta .device {
color: #555;
}
.bubble {
padding: 10px 14px;
border-radius: 12px;
max-width: 90%;
display: inline-block;
word-break: break-word;
}
/* 颜色通过内联样式控制(.unread/.seen 仅用于语义钩子) */
.bubble.unread {}
.bubble.seen {}
</style>

View File

@@ -20,7 +20,7 @@ function attachInterceptors(instance) {
instance.interceptors.request.use((config) => {
// 登录/换租户接口可能不需要 token根据你的需求放行
const urlLast = sliceUrl(config.url || '')
if ((urlLast === 'prologue' || urlLast === 'comment')) {
if ((urlLast === 'prologue' || urlLast === 'comment' || urlLast === 'aiChat-logout')) {
config.headers['vvtoken'] = getToken()
}
// 超时 & 通用头
@@ -42,6 +42,8 @@ function attachInterceptors(instance) {
if (data?.code === 0 || data?.code === 200) {
// 成功:返回业务数据(没有就回传原 data
return (data?.data !== undefined) ? data.data : data
} else if (data?.code === 40400) {
router.push('/')
}
// 业务失败:提示 + reject

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('1.1.1');
let version = ref('1.1.4');
onMounted(() => {

View File

@@ -1,6 +1,10 @@
<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>
</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"
@@ -29,6 +33,8 @@
<div class="input-info" v-show="selectedDevice == index">
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
<div class="app-button" @click="stopScript({ udid: device.deviceId })">停止任务</div>
<div class="app-button" @click="runTask(runType, device.deviceId)">开启</div>
<!-- <div class="app-button" @click="mqSend()">mq</div> -->
<!-- <div class="app-button"
@click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true; istranslate = true">
@@ -52,15 +58,23 @@
</div>
</div>
</div>
<div class="right center-justify" @click.self="selectedDevice = 999">
<div class="right center-line" @click.self="selectedDevice = 999">
<!-- <div style="margin: 30px;"></div> -->
<ChatDialog :visible="openShowChat" :messages="chatList" />
<MessageDialogd :visible="openShowChat" :messages="MesNewList" />
</div>
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
@confirm="onDialogConfirm" @cancel="stopAll" />
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" />
</div>
</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="420px">
<div style="display:grid;grid-template-columns: 100px 1fr; gap:12px; align-items:center;">
@@ -106,7 +120,7 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted, onBeforeUnmount, watch, inject, computed } from "vue";
import { ref, reactive, onMounted, onUnmounted, onBeforeUnmount, watch, inject, computed } from "vue";
import { useRouter } from 'vue-router';
import {
setphoneXYinfo, getphoneXYinfo, getUser,
@@ -118,8 +132,11 @@ import { connectSSE } from '@/utils/sseUtils'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import { chat, translationToChinese, translation } from "@/api/chat";
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
import AgentGuildDialog from '@/components/AgentGuildDialog.vue'
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue'; // 根据实际路径修改
import ChatDialog from '@/components/ChatDialog.vue'
import MessageDialogd from '@/components/MessageDialogd.vue'
import { pickTikTokBundleId } from '@/utils/arrUtils'
import { logout } from '@/api/account';
import {
@@ -137,19 +154,76 @@ import {
launchApp,
getChatTextInfo,
setLoginInfo,
aiConfig,
} from '@/api/ios';
import { set } from "lodash";
const router = useRouter();
const openShowChat = ref([true])
//主播库
const showHostDlg = ref(false)
//ai人设
const showMyInfo = ref(false)
// 假设这是你已有的数据
const borkerConfig = reactive({
agentName: '',
guildName: '',
contactTool: '',
contact: ''
})
let hostList = []
//查询列表轮询
let getListtimer = null;
let userdata = getUser();
let chatList = ref([])
let MesNewList = ref([{
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
}, {
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
}, {
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
}, {
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
}, {
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
}, {
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
}, {
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
}, {
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
}, {
sender: 'Alice', // 或 name/user/from
device: 'iPhone 14 Pro', // 或 deviceName/udid
text: '你好呀~', // 或 content
type: 'msg' // 可选;有些是 'time' 会被过滤
},])
let isImg = ref(true)
// 引入刷新方法
const reload = inject("reload")
@@ -172,6 +246,8 @@ let deviceInformation = ref([])
// 你可以用这种方式声明按钮们
//联动用作标记
let batchMode = ref('init'); // 'init' | 'follow'(仅作标记)
//停止中
let stopLoading = null
// 当前是否被其它模式占用(四个互斥按钮专用)
const isLocked = (type) => !!runType.value && runType.value !== type
@@ -642,20 +718,36 @@ function getMesList(deviceId) {
})
}
function stopAll() {
async function stopAll() {
stopLoading = ElLoading.service({
lock: true,
text: '停止中',
background: 'rgba(0, 0, 0, 0.7)',
});
// if (!runType.value) return
deviceInformation.value.forEach((item) => {
stopScript({ udid: item.deviceId }).then((res) => {
console.log(`停止成功:${item.deviceId}`, res, printCurrentTime())
ElMessage.success(`停止成功:${item.deviceId}`)
}).catch((res) => {
console.log(`停止失败`, printCurrentTime())
ElMessage.error(`脚本已停止`)
})
})
scheduleEnabled.value = false
runType.value = ''
batchMode.value = 'init';
try {
// 使用 Promise.all 并行处理所有设备的停止操作
await Promise.all(deviceInformation.value.map(async (item) => {
try {
const res = await stopScript({ udid: item.deviceId })
console.log(`停止成功:${item.deviceId}`, res, printCurrentTime())
ElMessage.success(`停止成功:${item.deviceId}`)
stopLoading.close()
} catch (error) {
console.log(`停止失败`, printCurrentTime())
ElMessage.error(`脚本已停止`)
stopLoading.close()
}
}))
// 所有操作完成后执行以下代码
scheduleEnabled.value = false
runType.value = ''
batchMode.value = 'init'
} catch (error) {
console.error('批量停止过程中发生错误:', error)
}
}
//确认多行文本框内容
@@ -682,6 +774,8 @@ function onDialogConfirm(result, type, index, isMon) {
needReply: isMon
}
).then((res) => {
ElMessage({ type: 'success', message: '任务开启成功' });
hostList = []
})
}
@@ -741,7 +835,6 @@ onMounted(() => {
{ confirmButtonText: '开始', cancelButtonText: '取消', type: 'success' }
)
.then(() => {
ElMessage({ type: 'success', message: '任务开启成功' });
// runType.value = 'follow';
batchMode.value = 'follow';
@@ -749,8 +842,12 @@ onMounted(() => {
// 不直接发;把这“一波”的主播先塞进 hostList然后弹出“私信”输入框
scheduleFlush((items) => {
hostList = (items || []).map(h => ({ id: h.text, country: h.country || '' }))
showScheduleDlg.value = true
})
setTimeout(() => {
showScheduleDlg.value = true
}, 600)
})
.catch(() => {
// 取消:清理状态,丢弃批次
@@ -808,6 +905,8 @@ onUnmounted(() => {
clearInterval(getListtimer)
getListtimer = null
})
let isStartLac = false
const getDeviceListFun = () => {
getDeviceList().then((res) => {
console.log('返回', res.length)
@@ -823,7 +922,12 @@ const getDeviceListFun = () => {
}
// deviceInformation.value = ['', '', '', '', '', '',]
}).catch((err) => {
ElMessage.error(`IOSAI服务错误`)
if (isStartLac) {
ElMessage.error(`IOSAI服务错误`)
isStartLac = true
} else {
}
})
}
@@ -875,15 +979,30 @@ async function uploadLogFile() {
}
}
function runTask(key) {
function runTask(key, deviceId) {
console.log('[schedule] 切换到任务:', key, printCurrentTime())
forceActivate(key, () => {
forceActivate(key, async () => {
if (key === 'follow') {
if (scheduleEnabled.value) {
stopAll()
if (!deviceId) {
await stopAll()
} else {
passAnchorData(
{
deviceList: [deviceId],
anchorList: [],
prologueList: getContentpriList(),
needReply: false
}
).then((res) => {
hostList = []
})
return
}
setTimeout(() => {
runType.value = 'follow'
@@ -903,7 +1022,7 @@ function runTask(key) {
}
scheduleEnabled.value = true
if (hostList.length <= 0) {
if (batchMode.value == 'init') {
dialogTitle.value = '主播ID';
} else {
dialogTitle.value = '私信';
@@ -912,7 +1031,12 @@ function runTask(key) {
} else if (key === 'like') {
stopAll()
if (!deviceId) {
await stopAll()
} else {
growAccount({ udid: deviceId })
return
}
setTimeout(() => {
scheduleEnabled.value = true
@@ -921,7 +1045,12 @@ function runTask(key) {
}, 1000)
} else if (key === 'brushLive') {
stopAll()
if (!deviceId) {
await stopAll()
} else {
watchLiveForGrowth({ udid: deviceId })
return
}
setTimeout(() => {
scheduleEnabled.value = true
@@ -930,7 +1059,12 @@ function runTask(key) {
}, 1000)
runType.value = 'brushLive'
} else if (key === 'listen') {
stopAll()
if (!deviceId) {
await stopAll()
} else {
monitorMessages({ udid: deviceId })
return
}
setTimeout(() => {
runType.value = 'listen'
@@ -1014,6 +1148,14 @@ function printCurrentTime() {
return now.toLocaleString()
}
function onSave(payload) {
console.log(payload)
aiConfig(payload).then((res) => {
})
}
</script>
<style scoped lang="less">