linfangfang_branch
psnci6hgk 2 months ago
parent caca9fb9f6
commit f30c3cf5bc

@ -49,7 +49,7 @@ const props = defineProps({
default: null
},
modelValue: {
type: Array,
type: [String, Number, Array],
default: () => []
},
selfId: {
@ -184,37 +184,144 @@ function formatDateTime(ms) {
// activeContact
watch(() => [props.activeContact && props.activeContact.schedule ? props.activeContact.schedule.length : 0, currentScheduleEnd.value], () => {
if (pendingPlaceholderTs && pendingReplyTimer) {
//
if (pendingReplyTimer) clearTimeout(pendingReplyTimer)
// schedule currentScheduleEnd
if (pendingPlaceholderTs) {
if (pendingReplyTimer) {
clearTimeout(pendingReplyTimer)
pendingReplyTimer = null
}
let waitMs = 0
if (currentScheduleEnd.value) {
waitMs = Math.max(currentScheduleEnd.value - Date.now(), 0)
} else {
waitMs = 5 * 60 * 1000
}
pendingReplyTimer = setTimeout(() => {
const ph = pendingPlaceholderTs
// schedule AI
pendingReplyTimer = setTimeout(async () => {
pendingReplyTimer = null
const batchId = pendingPlaceholderTs
pendingPlaceholderTs = null
// perform reply for this placeholder
// performAIReply is in scope
try { performAIReply(ph) } catch (e) { /* ignore */ }
//
const lastUser = [...messages.value].reverse().find(m => m.batchId === batchId && m.senderId === props.selfId)
const userText = lastUser ? lastUser.text : ''
await performAIReply(batchId, userText)
}, waitMs)
}
})
}, { immediate: true })
// AI batchId
const performAIReply = async (batchId, userText) => {
const API_KEY = import.meta.env.VITE_ZHIPU_API_KEY
if (!API_KEY) {
messages.value = messages.value.map(m =>
m.batchId === batchId && m.isPlaceholder
? { ...m, text: 'API_KEY 未配置,无法调用 AI', timestamp: Date.now(), isPlaceholder: false }
: m
)
emit('update:modelValue', messages.value)
return
}
try {
const recent = messages.value.slice(Math.max(messages.value.length - 6, 0))
const conversationContext = recent.map(m => `${m.senderId === props.selfId ? '用户' : 'AI'}: ${m.text}`).join('\n')
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)}`
})
}
const prompt = `${persona}\n\n以下是最近的聊天记录\n${conversationContext}\n\n请用轻松的口吻、最多20字回复最后一条用户消息不要重复用户的话。用户最后一句话\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: 50,
temperature: 0.9,
top_p: 0.8
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 10000
})
let reply = res.data.choices?.[0]?.message?.content || ''
if (Array.isArray(reply)) reply = reply.join('\n')
reply = String(reply).trim()
if (!reply) reply = '(刚在忙,你说啥?)'
messages.value = messages.value.map(m =>
m.batchId === batchId && m.isPlaceholder
? { ...m, text: reply, timestamp: Date.now(), isPlaceholder: false }
: m
)
emit('update:modelValue', messages.value)
} catch (err) {
console.error('AI回复错误:', err)
let errMsg = '网络开小差了~'
if (err.response) {
console.error('API响应异常:', err.response.status)
errMsg += ` (状态码: ${err.response.status})`
} else if (err.request) {
console.error('服务器无响应:', err.request)
errMsg = '服务失联,请检查网络'
}
messages.value = messages.value.map(m =>
m.batchId === batchId && m.isPlaceholder
? { ...m, text: errMsg, timestamp: Date.now(), isPlaceholder: false }
: m
)
emit('update:modelValue', messages.value)
}
}
onBeforeUnmount(() => {
if (statusTimer) clearInterval(statusTimer)
if (pendingReplyTimer) clearTimeout(pendingReplyTimer)
})
const handleSend = async () => {
//
console.debug('[Send]', {
draft: draft.value,
activeContact: props.activeContact?.id,
canSend: canSend.value,
msgsCount: messages.value.length
});
if (!canSend.value) return
//
const now = Date.now()
if (messages.value.some(m =>
m.senderId === props.selfId &&
m.text === draft.value.trim() &&
now - m.timestamp < 5000
)) {
return
}
// ID
const messageBatchId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
const userMsg = {
senderId: props.selfId,
text: draft.value.trim(),
timestamp: Date.now()
timestamp: Date.now(),
batchId: messageBatchId // ID
}
let nextMessages = [...messages.value, userMsg]
messages.value = nextMessages
emit('update:modelValue', nextMessages)
@ -223,114 +330,42 @@ const handleSend = async () => {
draft.value = ''
const API_KEY = import.meta.env.VITE_ZHIPU_API_KEY
// prepare placeholder message
const aiMsg = {
// AI - batchId
const aiPlaceholderMsg = {
senderId: props.activeContact?.id || 'ai',
text: '对方正在输入中...',
timestamp: Date.now() + 1
timestamp: Date.now() + 1,
batchId: messageBatchId,
isPlaceholder: true //
}
nextMessages = [...messages.value, aiMsg]
nextMessages = [...messages.value, aiPlaceholderMsg]
messages.value = nextMessages
emit('update:modelValue', nextMessages)
const isBusy = statusClass.value === 'busy' || statusText.value.startsWith('正在')
// 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) {}
}
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: 128,
temperature: 0.7
}, { 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)
}
}
// create placeholder and determine scheduling
const placeholderTs = Date.now()
pendingPlaceholderTs = placeholderTs
// replace last AI placeholder with activity placeholder if busy
// batchIdtimestamp
pendingPlaceholderTs = messageBatchId
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)
// compute wait ms until schedule end
// 5
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)
waitMs = Math.max(currentScheduleEnd.value - Date.now(), 0)
} else {
// fallback to 5 minutes
waitMs = 5 * 60 * 1000
}
// clear previous timer if any
if (pendingReplyTimer) clearTimeout(pendingReplyTimer)
pendingReplyTimer = setTimeout(() => {
pendingReplyTimer = null
pendingPlaceholderTs = null
performAIReply(placeholderTs)
// 便 AI
performAIReply(messageBatchId, userText)
}, 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)
await performAIReply(placeholderTs)
await performAIReply(messageBatchId, userText)
}
}
</script>

@ -185,6 +185,13 @@ const handleFileChange = (e) => {
const reader = new FileReader()
reader.onload = () => {
ocForm.avatar = reader.result
// 稿
try {
const userKey = currentUser.value?.username || 'guest'
localStorage.setItem(`ocDraft_avatar_${userKey}`, JSON.stringify({ avatar: ocForm.avatar, at: Date.now() }))
} catch (e) {
console.error('保存 avatar 草稿失败:', e)
}
}
reader.readAsDataURL(f)
}
@ -252,6 +259,13 @@ const generateAvatarAI = async () => {
ocForm.avatar = imageUrl
console.info('使用返回的远程图片 URL 作为头像(未转为 base64', imageUrl)
ElMessage.success('AI头像生成完成使用返回的图片URL')
// 稿便 OC /
try {
const userKey = currentUser.value?.username || 'guest'
localStorage.setItem(`ocDraft_avatar_${userKey}`, JSON.stringify({ avatar: ocForm.avatar, at: Date.now(), source: 'ai' }))
} catch (e) {
console.error('保存 AI avatar 草稿失败:', e)
}
avatarLoading.value = false
return
}
@ -444,6 +458,19 @@ onMounted(() => {
}
}
})
// 稿
try {
const userKey = currentUser.value?.username || 'guest'
const draft = localStorage.getItem(`ocDraft_avatar_${userKey}`)
if (draft) {
const parsed = JSON.parse(draft)
if (parsed && parsed.avatar && !ocForm.avatar) {
ocForm.avatar = parsed.avatar
}
}
} catch (e) {
console.error('恢复 avatar 草稿失败:', e)
}
}
}
})
@ -564,6 +591,13 @@ const handleSubmit = async () => {
addOC(ocData, currentUser.value.username)
ElMessage.success('OC创建成功')
}
// avatar 稿
try {
const userKey = currentUser.value?.username || 'guest'
localStorage.removeItem(`ocDraft_avatar_${userKey}`)
} catch (e) {
console.error('清除 avatar 草稿失败:', e)
}
router.push('/community')
} catch (error) {
ElMessage.error((isEdit.value ? '保存失败:' : '创建失败:') + error.message)

@ -30,6 +30,9 @@
<div class="personal-info">
<h4>个人信息</h4>
<el-form :model="userInfo" label-width="80px" size="small">
<el-form-item label="昵称">
<el-input v-model="userInfo.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="生日">
<el-date-picker
v-model="userInfo.birthday"

@ -187,19 +187,19 @@ ok——社区地图的地点弹窗中点击对应oc头像可查看oc信息--界
修改个人昵称不同用户登录地图上不显示非当前用户oc的活动--个人信息与oc日程信息关联
导入地区地图和实时天气,并显示在社区地图界面中--一种尝试
ai生成oc头像--AI绘图
0.1.6
增加学校地点!!!
##ai记忆聊天历史并根据聊天历史进行回复迁移数据库
ok-ai生成的头像没有保存到下次运行
自主构建地图
多OC互动Actor: 系统可选OC之间根据日程和位置触发简单互动。
第三方大语言模型API智谱AI用于OC生成和对话等内容
ok AI绘画生成APICogview-3-Flash生成oc形象或头像
ok AI绘画生成APICogview-3-Flash生成oc形象或头像
云服务器和数据库服务
--0.2目标成功调用ai搭建基本社区地图
@ -207,7 +207,7 @@ ok AI绘画生成APICogview-3-Flash生成oc形象或头像
列表抽拉栏
已知有以下用例模型注册、登录、修改个人信息、退出登录、管理OC信息、删除OC、上传头像、AI生成头像、展示社区地图、管理OC日程表、AI互动、日程管理与状态同步、系统管理与维护
自主构建地图

Loading…
Cancel
Save