试验性的记忆功能 (#812)

* feat: memory basic

* fix: chaite ver

* fix: update prompt

* fix: memory cursor and extract prompt

* fix: memory retrieval bug

* fix: memory retrieval bug

* fix: one more attempt by codex

* fix: messages prompt error

* fix: one more time by codex

* fix: metrics by codex

* fix: memory forward

* fix: memory show update time
This commit is contained in:
ikechan8370 2025-11-07 16:40:26 +08:00 committed by GitHub
parent db386ccaf2
commit 8bfce5402f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 4382 additions and 103 deletions

View file

@ -0,0 +1,129 @@
import { Chaite } from 'chaite'
import * as crypto from 'node:crypto'
import { extractUserMemories } from './extractor.js'
import { memoryService } from './service.js'
const USER_MEMORY_CONTEXT_LIMIT = 6
export function extractTextFromContents (contents) {
if (!Array.isArray(contents)) {
return ''
}
return contents
.filter(item => item && item.type === 'text')
.map(item => item.text || '')
.join('\n')
.trim()
}
export function extractTextFromUserMessage (userMessage) {
if (!userMessage?.content) {
return ''
}
return userMessage.content
.filter(item => item.type === 'text')
.map(item => item.text || '')
.join('\n')
.trim()
}
function normaliseMemoriesInput (memories, sourceId) {
return (memories || []).map(mem => {
if (typeof mem === 'string') {
return {
value: mem,
source_message_id: sourceId
}
}
if (mem && typeof mem === 'object') {
const cloned = { ...mem }
if (!cloned.source_message_id && sourceId) {
cloned.source_message_id = sourceId
}
if (!cloned.value && cloned.fact) {
cloned.value = cloned.fact
}
if (!cloned.value && cloned.text) {
cloned.value = cloned.text
}
return cloned
}
return {
value: String(mem),
source_message_id: sourceId
}
})
}
export async function processUserMemory ({ event, userMessage, userText, conversationId, assistantContents, assistantMessageId }) {
const e = event
if (!memoryService.isUserMemoryEnabled(e.sender.user_id)) {
return
}
const snippets = []
const userMessageId = e.message_id || e.seq || userMessage?.id || crypto.randomUUID()
const senderName = e.sender?.card || e.sender?.nickname || String(e.sender?.user_id || '')
try {
const historyManager = Chaite.getInstance()?.getHistoryManager?.()
if (historyManager && conversationId) {
const history = await historyManager.getHistory(null, conversationId)
const filtered = (history || [])
.filter(msg => ['user', 'assistant'].includes(msg.role))
.map(msg => ({
role: msg.role,
text: extractTextFromContents(msg.content),
nickname: msg.role === 'user' ? senderName : '机器人',
message_id: msg.id
}))
.filter(item => item.text)
if (filtered.length > 0) {
const limited = filtered.slice(-USER_MEMORY_CONTEXT_LIMIT * 2)
snippets.push(...limited)
}
}
} catch (err) {
logger.warn('Failed to collect user memory context:', err)
}
if (assistantContents) {
const assistantText = extractTextFromContents(assistantContents)
if (assistantText) {
snippets.push({
role: 'assistant',
text: assistantText,
nickname: '机器人',
message_id: assistantMessageId || crypto.randomUUID()
})
}
}
if (userText && !snippets.some(item => item.message_id === userMessageId)) {
snippets.push({
role: 'user',
text: userText,
nickname: senderName,
message_id: userMessageId
})
}
if (snippets.length === 0) {
return
}
const existingRecords = memoryService.listUserMemories(e.sender.user_id, e.isGroup ? e.group_id : null, 50)
const existingTexts = existingRecords.map(record => record.value).filter(Boolean)
const memories = await extractUserMemories(snippets, existingTexts)
if (!memories || memories.length === 0) {
return
}
const enriched = normaliseMemoriesInput(memories, userMessageId)
memoryService.upsertUserMemories(
e.sender.user_id,
e.isGroup ? e.group_id : null,
enriched
)
}
export { USER_MEMORY_CONTEXT_LIMIT }