占位消息优化

linfangfang_branch
杨美曦 2 months ago
parent 4c432aca49
commit 7ddce25e1d

@ -90,6 +90,9 @@ const statusText = ref('在线')
const statusClass = ref('online')
const currentActivity = ref('')
let statusTimer = null
const currentScheduleEnd = ref(null) // ms timestamp
let pendingReplyTimer = null
let pendingPlaceholderTs = null
function computeStatusFromSchedule(oc) {
try {
@ -132,6 +135,8 @@ function computeStatusFromSchedule(oc) {
statusClass.value = 'busy'
currentActivity.value = s.event || '忙碌'
}
// store end time for scheduling reply when busy
currentScheduleEnd.value = end.getTime()
return
}
}
@ -143,15 +148,18 @@ function computeStatusFromSchedule(oc) {
statusText.value = `稍后:${upcoming.time || ''} ${upcoming.event || ''}`
statusClass.value = 'soon'
currentActivity.value = ''
currentScheduleEnd.value = null
} else {
statusText.value = '空闲'
statusClass.value = 'idle'
currentActivity.value = ''
currentScheduleEnd.value = null
}
} catch (e) {
statusText.value = '在线'
statusClass.value = 'online'
currentActivity.value = ''
currentScheduleEnd.value = null
}
}
@ -161,8 +169,44 @@ watch(() => props.activeContact, (c) => {
statusTimer = setInterval(() => computeStatusFromSchedule(props.activeContact || {}), 60 * 1000)
}, { immediate: true })
// HH:MM YYYY-MM-DD HH:MM
function formatDateTime(ms) {
try {
const d = new Date(ms)
const now = new Date()
const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
if (sameDay) return `${hh}:${mm}`
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${hh}:${mm}`
} catch (e) { return '' }
}
// activeContact
watch(() => [props.activeContact && props.activeContact.schedule ? props.activeContact.schedule.length : 0, currentScheduleEnd.value], () => {
if (pendingPlaceholderTs && pendingReplyTimer) {
//
if (pendingReplyTimer) clearTimeout(pendingReplyTimer)
let waitMs = 0
if (currentScheduleEnd.value) {
waitMs = Math.max(currentScheduleEnd.value - Date.now(), 0)
} else {
waitMs = 5 * 60 * 1000
}
pendingReplyTimer = setTimeout(() => {
const ph = pendingPlaceholderTs
pendingReplyTimer = null
pendingPlaceholderTs = null
// perform reply for this placeholder
// performAIReply is in scope
try { performAIReply(ph) } catch (e) { /* ignore */ }
}, waitMs)
}
})
onBeforeUnmount(() => {
if (statusTimer) clearInterval(statusTimer)
if (pendingReplyTimer) clearTimeout(pendingReplyTimer)
})
const handleSend = async () => {
if (!canSend.value) return
@ -178,8 +222,8 @@ const handleSend = async () => {
const userText = draft.value.trim()
draft.value = ''
// AI
const API_KEY = import.meta.env.VITE_ZHIPU_API_KEY
// prepare placeholder message
const aiMsg = {
senderId: props.activeContact?.id || 'ai',
text: 'AI思考中...',
@ -188,137 +232,105 @@ const handleSend = async () => {
nextMessages = [...messages.value, aiMsg]
messages.value = nextMessages
emit('update:modelValue', nextMessages)
const isBusy = statusClass.value === 'busy' || statusText.value.startsWith('正在')
const delayMs = isBusy ? 5 * 60 * 1000 : 0 // 5
// ...
if (isBusy) {
const placeholder = {
...aiMsg,
text: `我正在${currentActivity.value || '忙碌'},请稍等`,
timestamp: Date.now()
}
//
messages.value = [...messages.value.slice(0, -1), placeholder]
emit('update:modelValue', messages.value)
// respond()
}
if (!API_KEY) {
aiMsg.text = 'API_KEY未配置请检查.env文件和重启项目'
messages.value = [...messages.value.slice(0, -1), aiMsg]
emit('update:modelValue', messages.value)
return
}
try {
// prompt OC AI
let persona = ''
if (props.activeContact) {
const c = props.activeContact
const safe = (v) => (v === null || v === undefined) ? '' : String(v).replace(/\s+/g, ' ').trim()
const fmtList = (v) => Array.isArray(v) ? v.join('、') : (v ? safe(v) : '')
persona += `你是${safe(c.name) || '该角色'}`;
// 使
const fields = [
['性别', c.gender || (c.genderOther ? c.genderOther : '')],
['生日', c.birthday],
['年龄', c.age],
['种族', c.race],
['职业', c.occupation],
['发型与发色', c.hair],
['瞳色', c.eyeColor],
['身高', c.height],
['着装风格', c.clothes],
['标志性特征', c.feature],
['核心性格', fmtList(c.personality) || c.personalityOther],
['MBTI', c.mbti],
['优点', c.advantage],
['缺点', c.shortcoming],
['习惯性小动作', c.habit],
['口头禅', c.catchphrase],
['成长环境', c.growup],
['背景故事', c.story],
['秘密', c.secret],
['兴趣爱好', fmtList(c.hobbies)],
['喜欢的食物', c.foodLike],
['讨厌的东西', c.hate],
['理想生活方式', c.life],
['周末日常', c.weekend],
['社区主要活动', c.communityActivity],
['关系期待', fmtList(c.relation) || c.relationOther],
['头像URL', c.avatar]
]
const isBusy = statusClass.value === 'busy' || statusText.value.startsWith('正在')
fields.forEach(([label, value]) => {
if (value || value === 0) {
persona += `${label}${safe(value)}`
}
})
//
if (c.socialCircle && c.socialCircle.length) persona += `社交圈:${c.socialCircle.slice(0,5).map(x => x.name || x).join('、')}`;
if (c.achievements && c.achievements.length) persona += `成就:${c.achievements.slice(0,5).join('、')}`;
// -@
try {
const now = new Date()
const todayStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0')
if (Array.isArray(c.schedule)) {
const todays = c.schedule.filter(s => String(s.date).startsWith(todayStr))
if (todays.length) {
const brief = todays.slice(0,6).map(s => `${s.time || '--:--'}-${(s.event||'').slice(0,20)}@${s.location||''}`)
persona += `今日日程(简要):${brief.join('')}`;
// helper that performs the AI call and replaces the placeholder (matched by ts)
const performAIReply = async (placeholderTs) => {
if (!API_KEY) {
const failMsg = 'API_KEY未配置请检查.env文件和重启项目'
// replace placeholder
messages.value = messages.value.map(m => (m.timestamp === placeholderTs ? { ...m, text: failMsg } : m))
emit('update:modelValue', messages.value)
return
}
try {
// build persona
let persona = ''
if (props.activeContact) {
const c = props.activeContact
const safe = (v) => (v === null || v === undefined) ? '' : String(v).replace(/\s+/g, ' ').trim()
const fmtList = (v) => Array.isArray(v) ? v.join('、') : (v ? safe(v) : '')
persona += `你是${safe(c.name) || '该角色'}`;
const fields = [
['性别', c.gender || (c.genderOther ? c.genderOther : '')],
['生日', c.birthday],
['年龄', c.age],
['职业', c.occupation],
['兴趣', fmtList(c.hobbies)]
]
fields.forEach(([label, value]) => {
if (value || value === 0) persona += `${label}${safe(value)}`
})
// include brief todays
try {
const now = new Date()
const todayStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0')
if (Array.isArray(c.schedule)) {
const todays = c.schedule.filter(s => String(s.date).startsWith(todayStr))
if (todays.length) {
const brief = todays.slice(0,6).map(s => `${s.time || '--:--'}-${(s.event||'').slice(0,20)}@${s.location||''}`)
persona += `今日日程(简要):${brief.join('')}`;
}
}
}
} catch (e) {
// ignore
} catch (e) {}
}
}
// AI
if (currentUser.value && currentUser.value.nickname) {
persona += `对方昵称:${currentUser.value.nickname}`;
if (currentUser.value.occupation) persona += `对方职业:${currentUser.value.occupation}`;
if (currentUser.value.hobbies) persona += `对方爱好:${currentUser.value.hobbies}`;
}
persona += '请用简洁、贴近生活的语气,只回复一句符合自己人设和性格的聊天内容,如果对方没说就不要刻意强调自己知晓对方的爱好,不要混淆对方和自己的爱好,回复的文本长度根据对方的语句进行调整,比如对方只是简单问候就可以简短回应,对于问题则可以以自身人设风格进行较为详细的解答。';
const prompt = `${persona}\n对方说${userText}`;
const res = await axios.post(
'https://open.bigmodel.cn/api/paas/v4/chat/completions',
{
if (currentUser.value && currentUser.value.nickname) persona += `对方昵称:${currentUser.value.nickname}`;
const prompt = `${persona}\n对方说${userText}`
const res = await axios.post('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
model: 'glm-4-flash',
messages: [{ role: 'user', content: prompt }],
max_tokens: 64,
max_tokens: 128,
temperature: 0.7
},
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
}
)
let reply = res.data.choices?.[0]?.message?.content?.trim()
if (!reply) {
reply = 'AI未能生成回复请稍后重试'
}, { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } })
let reply = res.data.choices?.[0]?.message?.content?.trim() || 'AI未能生成回复请稍后重试'
// replace placeholder message with real reply (match by timestamp)
messages.value = messages.value.map(m => (m.timestamp === placeholderTs ? { ...m, text: reply, timestamp: Date.now() } : m))
emit('update:modelValue', messages.value)
} catch (err) {
let errMsg = 'AI接口调用失败'
if (err?.response?.data?.msg) errMsg += `: ${err.response.data.msg}`
else if (err?.response?.data?.message) errMsg += `: ${err.response.data.message}`
else if (err?.message) errMsg += `: ${err.message}`
messages.value = messages.value.map(m => (m.timestamp === placeholderTs ? { ...m, text: errMsg } : m))
emit('update:modelValue', messages.value)
}
aiMsg.text = reply
aiMsg.timestamp = Date.now()
messages.value = [...messages.value.slice(0, -1), aiMsg]
}
// create placeholder and determine scheduling
const placeholderTs = Date.now()
pendingPlaceholderTs = placeholderTs
// replace last AI placeholder with activity placeholder if busy
if (isBusy) {
const freeAt = currentScheduleEnd.value ? formatDateTime(currentScheduleEnd.value) : ''
const placeholderText = freeAt ? `我正在${currentActivity.value || '忙碌'},请稍等,预计空闲时间为 ${freeAt},等会再来找我玩吧` : `我正在${currentActivity.value || '忙碌'},请稍等`
const placeholder = { senderId: props.activeContact?.id || 'ai', text: placeholderText, timestamp: placeholderTs }
messages.value = [...messages.value.slice(0, -1), placeholder]
emit('update:modelValue', messages.value)
} catch (err) {
let errMsg = 'AI接口调用失败'
if (err?.response?.data?.msg) {
errMsg += `: ${err.response.data.msg}`
} else if (err?.response?.data?.message) {
errMsg += `: ${err.response.data.message}`
} else if (err?.message) {
errMsg += `: ${err.message}`
// compute wait ms until schedule end
let waitMs = 0
if (currentScheduleEnd.value) {
const buf = 0 // no extra buffer; you can set 60000 for 1min buffer
waitMs = Math.max(currentScheduleEnd.value + buf - Date.now(), 0)
} else {
// fallback to 5 minutes
waitMs = 5 * 60 * 1000
}
aiMsg.text = errMsg
messages.value = [...messages.value.slice(0, -1), aiMsg]
// clear previous timer if any
if (pendingReplyTimer) clearTimeout(pendingReplyTimer)
pendingReplyTimer = setTimeout(() => {
pendingReplyTimer = null
pendingPlaceholderTs = null
performAIReply(placeholderTs)
}, waitMs)
// also watch schedule changes: if schedule changes, reschedule by clearing timer and recreating in watcher above
return
} else {
// not busy -> immediate perform and replace
// replace the placeholder we created earlier (the simple 'AI...') with a real-running placeholder timestamp
messages.value = [...messages.value.slice(0, -1), { ...aiMsg, timestamp: placeholderTs }]
emit('update:modelValue', messages.value)
// console
// console.error('AI', err)
await performAIReply(placeholderTs)
}
}
</script>

Loading…
Cancel
Save