试验性的记忆功能 (#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

@ -3,7 +3,9 @@ import { Chaite } from 'chaite'
import { intoUserMessage, toYunzai } from '../utils/message.js'
import common from '../../../lib/common/common.js'
import { getGroupContextPrompt } from '../utils/group.js'
import {formatTimeToBeiJing} from '../utils/common.js'
import { formatTimeToBeiJing } from '../utils/common.js'
import { extractTextFromUserMessage, processUserMemory } from '../models/memory/userMemoryManager.js'
import { buildMemoryPrompt } from '../models/memory/prompt.js'
export class bym extends plugin {
constructor () {
@ -83,6 +85,7 @@ export class bym extends plugin {
toggleMode: ChatGPTConfig.basic.toggleMode,
togglePrefix: ChatGPTConfig.basic.togglePrefix
})
const userText = extractTextFromUserMessage(userMessage) || e.msg || ''
// 伪人不记录历史
// sendMessageOption.disableHistoryRead = true
// sendMessageOption.disableHistorySave = true
@ -98,9 +101,29 @@ export class bym extends plugin {
this.reply(forwardElement)
}
}
const systemSegments = []
if (sendMessageOption.systemOverride) {
systemSegments.push(sendMessageOption.systemOverride)
}
if (userText) {
const memoryPrompt = await buildMemoryPrompt({
userId: e.sender.user_id + '',
groupId: e.isGroup ? e.group_id + '' : null,
queryText: userText
})
if (memoryPrompt) {
systemSegments.push(memoryPrompt)
logger.debug(`[Memory] bym memory prompt: ${memoryPrompt}`)
}
}
if (ChatGPTConfig.llm.enableGroupContext && e.isGroup) {
const contextPrompt = await getGroupContextPrompt(e, ChatGPTConfig.llm.groupContextLength)
sendMessageOption.systemOverride = sendMessageOption.systemOverride ? sendMessageOption.systemOverride + '\n' + contextPrompt : contextPrompt
if (contextPrompt) {
systemSegments.push(contextPrompt)
}
}
if (systemSegments.length > 0) {
sendMessageOption.systemOverride = systemSegments.join('\n\n')
}
// 发送
const response = await Chaite.getInstance().sendMessage(userMessage, e, {
@ -120,5 +143,13 @@ export class bym extends plugin {
await e.reply(forwardElement, false, { recallMsg: recall ? 10 : 0 })
}
}
await processUserMemory({
event: e,
userMessage,
userText,
conversationId: sendMessageOption.conversationId,
assistantContents: response.contents,
assistantMessageId: response.id
})
}
}

View file

@ -2,7 +2,9 @@ import Config from '../config/config.js'
import { Chaite, SendMessageOption } from 'chaite'
import { getPreset, intoUserMessage, toYunzai } from '../utils/message.js'
import { YunzaiUserState } from '../models/chaite/storage/lowdb/user_state_storage.js'
import { getGroupContextPrompt, getGroupHistory } from '../utils/group.js'
import { getGroupContextPrompt } from '../utils/group.js'
import { buildMemoryPrompt } from '../models/memory/prompt.js'
import { extractTextFromUserMessage, processUserMemory } from '../models/memory/userMemoryManager.js'
import * as crypto from 'node:crypto'
export class Chat extends plugin {
@ -11,7 +13,8 @@ export class Chat extends plugin {
name: 'ChatGPT-Plugin对话',
dsc: 'ChatGPT-Plugin对话',
event: 'message',
priority: 500,
// 应🥑要求降低优先级
priority: 555500,
rule: [
{
reg: '^[^#][sS]*',
@ -63,12 +66,34 @@ export class Chat extends plugin {
toggleMode: Config.basic.toggleMode,
togglePrefix: Config.basic.togglePrefix
})
const userText = extractTextFromUserMessage(userMessage) || e.msg || ''
sendMessageOptions.conversationId = state?.current?.conversationId
sendMessageOptions.parentMessageId = state?.current?.messageId || state?.conversations.find(c => c.id === sendMessageOptions.conversationId)?.lastMessageId
const systemSegments = []
const baseSystem = sendMessageOptions.systemOverride || preset.sendMessageOption?.systemOverride || ''
if (baseSystem) {
systemSegments.push(baseSystem)
}
if (userText) {
const memoryPrompt = await buildMemoryPrompt({
userId: e.sender.user_id + '',
groupId: e.isGroup ? e.group_id + '' : null,
queryText: userText
})
if (memoryPrompt) {
systemSegments.push(memoryPrompt)
logger.debug(`[Memory] memory prompt: ${memoryPrompt}`)
}
}
const enableGroupContext = (preset.groupContext === 'use_system' || !preset.groupContext) ? Config.llm.enableGroupContext : (preset.groupContext === 'enabled')
if (enableGroupContext && e.isGroup) {
const contextPrompt = await getGroupContextPrompt(e, Config.llm.groupContextLength)
sendMessageOptions.systemOverride = sendMessageOptions.systemOverride ? sendMessageOptions.systemOverride + '\n' + contextPrompt : (preset.sendMessageOption.systemOverride + contextPrompt)
if (contextPrompt) {
systemSegments.push(contextPrompt)
}
}
if (systemSegments.length > 0) {
sendMessageOptions.systemOverride = systemSegments.join('\n\n')
}
const response = await Chaite.getInstance().sendMessage(userMessage, e, {
...sendMessageOptions,
@ -95,5 +120,13 @@ export class Chat extends plugin {
for (let forwardElement of forward) {
this.reply(forwardElement)
}
await processUserMemory({
event: e,
userMessage,
userText,
conversationId: sendMessageOptions.conversationId,
assistantContents: response.contents,
assistantMessageId: response.id
})
}
}

224
apps/memory.js Normal file
View file

@ -0,0 +1,224 @@
import Config from '../config/config.js'
import { GroupMessageCollector } from '../models/memory/collector.js'
import { memoryService } from '../models/memory/service.js'
import common from '../../../lib/common/common.js'
const collector = new GroupMessageCollector()
function isGroupManager (e) {
if (e.isMaster) {
return true
}
if (!e.member) {
return false
}
if (typeof e.member.is_admin !== 'undefined') {
return e.member.is_admin || e.member.is_owner
}
if (typeof e.member.role !== 'undefined') {
return ['admin', 'owner'].includes(e.member.role)
}
return false
}
export class MemoryManager extends plugin {
constructor () {
const cmdPrefix = Config.basic.commandPrefix || '#chatgpt'
super({
name: 'ChatGPT-Plugin记忆系统',
dsc: '处理记忆系统相关的采集与管理',
event: 'message',
priority: 550,
rule: [
// {
// reg: '[\\s\\S]+',
// fnc: 'collect',
// log: false
// },
{
reg: '^#?(我的)?记忆$',
fnc: 'showUserMemory'
},
{
reg: '^#?他的记忆$',
fnc: 'showTargetUserMemory'
},
{
reg: '^#?(删除|清除)(我的)?记忆\\s*(\\d+)$',
fnc: 'deleteUserMemory'
},
{
reg: '^#?(本群|群)记忆$',
fnc: 'showGroupMemory'
},
{
reg: '^#?(删除|移除)群记忆\\s*(\\d+)$',
fnc: 'deleteGroupMemory'
},
{
reg: `^${cmdPrefix}记忆列表$`,
fnc: 'adminMemoryOverview',
permission: 'master'
}
]
})
// 兼容miao和trss气死了
let task = {
name: 'ChatGPT-群记忆轮询',
cron: '*/1 * * * *',
fnc: this.pollHistoryTask.bind(this),
log: false
}
this.task = [task]
}
async collect (e) {
collector.push(e)
return false
}
async showUserMemory (e) {
if (!memoryService.isUserMemoryEnabled(e.sender.user_id)) {
await e.reply('私人记忆未开启或您未被授权。')
return false
}
const memories = memoryService.listUserMemories(e.sender.user_id, e.isGroup ? e.group_id : null)
if (!memories.length) {
await e.reply('🧠 您的记忆:\n暂无记录~')
return true
}
const msgs = memories.map(item =>
`${item.id}. ${item.value}(更新时间:${item.updated_at}`
)
const forwardMsg = await common.makeForwardMsg(e, ['🧠 您的记忆:', ...msgs], '私人记忆列表')
await e.reply(forwardMsg)
return true
}
async showTargetUserMemory (e) {
if (!e.isGroup) {
await e.reply('该指令仅可在群聊中使用。')
return false
}
const at = e.at || (e.message?.find(m => m.type === 'at')?.qq)
if (!at) {
await e.reply('请@要查询的用户。')
return false
}
if (!memoryService.isUserMemoryEnabled(at)) {
await e.reply('该用户未开启私人记忆或未被授权。')
return false
}
const memories = memoryService.listUserMemories(at, e.group_id)
if (!memories.length) {
await e.reply('🧠 TA的记忆\n暂无记录~')
return true
}
const msgs = memories.map(item =>
`${item.id}. ${item.value}(更新时间:${item.updated_at}`
)
const forwardMsg = await common.makeForwardMsg(e, ['🧠 TA的记忆', ...msgs], 'TA的记忆列表')
await e.reply(forwardMsg)
return true
}
async deleteUserMemory (e) {
const match = e.msg.match(/(\d+)$/)
if (!match) {
return false
}
const memoryId = Number(match[1])
if (!memoryId) {
return false
}
if (!memoryService.isUserMemoryEnabled(e.sender.user_id)) {
await e.reply('私人记忆未开启或您未被授权。')
return false
}
const success = memoryService.deleteUserMemory(memoryId, e.sender.user_id)
await e.reply(success ? '已删除指定记忆。' : '未找到对应的记忆条目。')
return success
}
async showGroupMemory (e) {
if (!e.isGroup) {
await e.reply('该指令仅可在群聊中使用。')
return false
}
if (!memoryService.isGroupMemoryEnabled(e.group_id)) {
await e.reply('本群尚未开启记忆功能。')
return false
}
await collector.flush(e.group_id)
const facts = memoryService.listGroupFacts(e.group_id)
if (!facts.length) {
await e.reply('📚 本群记忆:\n暂无群记忆。')
return true
}
const msgs = facts.map(item => {
const topic = item.topic ? `${item.topic}` : ''
return `${item.id}. ${topic}${item.fact}`
})
const forwardMsg = await common.makeForwardMsg(e, ['📚 本群记忆:', ...msgs], '群记忆列表')
await e.reply(forwardMsg)
return true
}
async deleteGroupMemory (e) {
if (!e.isGroup) {
await e.reply('该指令仅可在群聊中使用。')
return false
}
if (!memoryService.isGroupMemoryEnabled(e.group_id)) {
await e.reply('本群尚未开启记忆功能。')
return false
}
if (!isGroupManager(e)) {
await e.reply('仅限主人或群管理员管理群记忆。')
return false
}
await collector.flush(e.group_id)
const match = e.msg.match(/(\d+)$/)
if (!match) {
return false
}
const factId = Number(match[1])
if (!factId) {
return false
}
const success = memoryService.deleteGroupFact(e.group_id, factId)
await e.reply(success ? '已删除群记忆。' : '未找到对应的群记忆。')
return success
}
async adminMemoryOverview (e) {
const enabledGroups = (Config.memory?.group?.enabledGroups || []).map(String)
const groupLines = enabledGroups.length ? enabledGroups.join(', ') : '暂无'
const userStatus = Config.memory?.user?.enable ? '已启用' : '未启用'
await e.reply(`记忆系统概览:\n- 群记忆开关:${Config.memory?.group?.enable ? '已启用' : '未启用'}\n- 已启用群:${groupLines}\n- 私人记忆:${userStatus}`)
return true
}
async pollHistoryTask () {
try {
await collector.tickHistoryPolling()
} catch (err) {
logger.error('[Memory] scheduled history poll failed:', err)
}
return false
}
}