diff --git a/src/plugins/chat.ts b/src/plugins/chat.ts index 6cd87fa..c68959e 100644 --- a/src/plugins/chat.ts +++ b/src/plugins/chat.ts @@ -15,6 +15,8 @@ import { ChatLunaChatModel } from 'koishi-plugin-chatluna/llm-core/platform/mode import { parseRawModelName } from 'koishi-plugin-chatluna/llm-core/utils/count_tokens' import { Config } from '..' import { + CharacterAfterChatEventPayload, + CharacterBeforeChatEventPayload, ChatLunaChain, GroupTemp, GuildConfig, @@ -76,6 +78,16 @@ interface NextReplyToolGroup { const replyToolProgress = '__character_reply_progress__' +function getCharacterSessionKey(session: Session) { + return `${session.isDirect ? 'private' : 'group'}:${ + session.isDirect ? session.userId : session.guildId + }` +} + +function getCharacterConversationId(session: Session) { + return session.isDirect ? session.userId : session.guildId +} + class PendingMessageQueue extends MessageQueue { private _messages: { message: Message @@ -1496,6 +1508,7 @@ async function prepareMessages( ): Promise<{ completionMessages: BaseMessage[] persistedHumanMessage: BaseMessage + systemPrompt: string }> { const { recentMessages, lastMessage, contextMessages } = await formatMessage( @@ -1507,17 +1520,6 @@ async function prepareMessages( focusMessage ) - const formattedSystemPrompt = await currentPreset.system.format( - { - time: '', - stickers: '', - status: '' - }, - session.app.chatluna.promptRenderer, - { - session - } - ) if (!chain) { logger.debug('messages_new: ' + JSON.stringify(recentMessages)) logger.debug('messages_last: ' + JSON.stringify(lastMessage)) @@ -1533,8 +1535,10 @@ async function prepareMessages( .replaceAll('}', '}}') const built = { preset: currentPreset.name, - conversationId: session.isDirect ? session.userId : session.guildId + conversationId: getCharacterConversationId(session) } + const sessionKey = getCharacterSessionKey(session) + const conversationId = built.conversationId let historyNewMessages = recentMessages if ( @@ -1565,41 +1569,78 @@ async function prepareMessages( temp.lastHistoryNew = recentMessages.slice() const userPrompt = formatReplyUserPrompt(session, config) + const timestamp = formatTimestamp(new Date()) + const persistedHistoryNew = recentMessages + .join('\n\n') + .replaceAll('{', '{{') + .replaceAll('}', '}}') + const systemVariables: Record = { + time: '', + stickers: '', + status: '' + } + const inputVariables: Record = { + history_new: historyNewMessages + .join('\n\n') + .replaceAll('{', '{{') + .replaceAll('}', '}}'), + history_last: historyLast, + time: timestamp, + stickers: '', + status: temp.status ?? currentPreset.status ?? '', + trigger_reason: triggerReasonText, + prompt: session.content, + built + } + const persistedInputVariables: Record = { + ...inputVariables, + history_new: persistedHistoryNew, + time: timestamp + } + + const beforePayload: CharacterBeforeChatEventPayload = { + session, + sessionKey, + conversationId, + presetName: currentPreset.name, + preset: currentPreset, + messages: messages.slice(), + focusMessage, + triggerReason, + systemVariables, + inputVariables, + persistedInputVariables + } + + try { + await ctx.parallel('chatluna_character/before-chat', beforePayload) + } catch (error) { + logger.error(error) + } + + const formattedSystemPrompt = await currentPreset.system.format( + systemVariables, + session.app.chatluna.promptRenderer, + { + session + } + ) const humanMessage = new HumanMessage( (await currentPreset.input.format( - { - history_new: historyNewMessages - .join('\n\n') - .replaceAll('{', '{{') - .replaceAll('}', '}}'), - history_last: historyLast, - time: formatTimestamp(new Date()), - stickers: '', - status: temp.status ?? currentPreset.status ?? '', - trigger_reason: triggerReasonText, - prompt: session.content, - built - }, + inputVariables, session.app.chatluna.promptRenderer, { session } )) + (userPrompt.length > 0 ? `\n\n${userPrompt}` : '') ) + const finalPersistedInputVariables = { + ...inputVariables, + ...persistedInputVariables, + history_new: persistedHistoryNew + } const prompt = await currentPreset.input.format( - { - history_new: recentMessages - .join('\n\n') - .replaceAll('{', '{{') - .replaceAll('}', '}}'), - history_last: historyLast, - time: formatTimestamp(new Date()), - stickers: '', - status: temp.status ?? currentPreset.status ?? '', - trigger_reason: triggerReasonText, - prompt: session.content, - built - }, + finalPersistedInputVariables, session.app.chatluna.promptRenderer, { session @@ -1705,7 +1746,8 @@ async function prepareMessages( return { completionMessages, - persistedHumanMessage + persistedHumanMessage, + systemPrompt: formattedSystemPrompt } } @@ -2106,7 +2148,7 @@ export async function apply(ctx: Context, config: Config) { const temp = await service.getTemp(session, latestMessages) const focusMessage = latestMessages[latestMessages.length - 1] - const { completionMessages, persistedHumanMessage } = + const { completionMessages, persistedHumanMessage, systemPrompt } = await prepareMessages( ctx, latestMessages, @@ -2288,7 +2330,32 @@ export async function apply(ctx: Context, config: Config) { nextReplyReasons ) - service.muteAtLeast(session, copyOfConfig.coolDownTime * 1000) + const completedMessages = + service.getMessages(key) ?? persistedMessages + const afterPayload: CharacterAfterChatEventPayload = { + session, + sessionKey: key, + conversationId: getCharacterConversationId(session), + presetName: currentPreset.name, + preset: currentPreset, + systemPrompt, + messages: completedMessages.slice(), + focusMessage, + triggerReason, + persistedHumanMessage, + lastResponseMessage, + completionMessages: temp.completionMessages.slice(), + status: latestStatus + } + + service + .muteAtLeast(session, copyOfConfig.coolDownTime * 1000) + .then(() => + ctx.parallel('chatluna_character/after-chat', afterPayload) + ) + .catch((error) => { + logger.error(error) + }) } catch (e) { logger.error(e) } finally { diff --git a/src/service/message.ts b/src/service/message.ts index b7ac10a..2c96a44 100644 --- a/src/service/message.ts +++ b/src/service/message.ts @@ -4,6 +4,7 @@ import { ObjectLock } from 'koishi-plugin-chatluna/utils/lock' import { Config } from '..' import { Preset } from '../preset' import { + CharacterClearChatHistoryEventPayload, CharacterReplyToolField, GroupLock, GroupTemp, @@ -30,6 +31,23 @@ function getMessageKey(message: Message) { return `${message.id}\n${message.messageId ?? ''}\n${message.timestamp ?? 0}\n${message.content}` } +function toClearChatHistoryPayload( + sessionKey: string +): CharacterClearChatHistoryEventPayload { + const isDirect = sessionKey.startsWith('private:') + const conversationId = isDirect + ? sessionKey.slice('private:'.length) + : sessionKey.startsWith('group:') + ? sessionKey.slice('group:'.length) + : sessionKey + + return { + sessionKey, + conversationId, + isDirect + } +} + export class MessageCollector extends Service { private _messages: Record = {} @@ -445,6 +463,17 @@ export class MessageCollector extends Service { return this._getMutex(groupId).lock() } + private _emitClearChatHistory(sessionKey: string) { + this.ctx + .parallel( + 'chatluna_character/clear-chat-history', + toClearChatHistoryPayload(sessionKey) + ) + .catch((error) => { + this.logger.error(error) + }) + } + async clear(groupId?: string) { if (groupId) { const isDirect = groupId.startsWith('private:') @@ -493,11 +522,18 @@ export class MessageCollector extends Service { } finally { unlock() } + this._emitClearChatHistory(groupId) return } // For clear-all, acquire locks in sorted order to prevent deadlocks - const groupIds = Object.keys(this._groupLocks).sort() + const groupIds = Array.from( + new Set([ + ...Object.keys(this._groupLocks), + ...Object.keys(this._messages), + ...Object.keys(this._groupTemp) + ]) + ).sort() const unlocks: (() => void)[] = [] for (const gid of groupIds) { unlocks.push(await this._lockByGroupId(gid)) @@ -559,6 +595,10 @@ export class MessageCollector extends Service { unlocks[i]() } } + + for (const groupId of groupIds) { + this._emitClearChatHistory(groupId) + } } async broadcastOnBot( @@ -667,7 +707,11 @@ export class MessageCollector extends Service { quotedElements ) })(), - name: session.quote?.user?.name, + name: getNotEmptyString( + session.quote?.user?.nick, + session.quote?.user?.name, + session.quote?.user?.id + ), id: session.quote?.user?.id, messageId: session.quote.id, timestamp: session.quote.timestamp @@ -677,10 +721,11 @@ export class MessageCollector extends Service { const message: Message = { content, name: getNotEmptyString( - session.author?.nick, - session.author?.name, + session.event.user?.nick, session.event.user?.name, - session.username + session.author?.username, + session.username, + session.userId ), id: session.author.id, messageId: session.messageId, @@ -874,7 +919,7 @@ export class MessageCollector extends Service { const maxMessageSize = Object.assign({}, this._config, globalConfig, guildConfig) .maxMessages ?? 40 - let groupArray = this._messages[groupId] ?? [] + const groupArray = this._messages[groupId] ?? [] groupArray.push(message) diff --git a/src/types.ts b/src/types.ts index ada584e..3d9c681 100644 --- a/src/types.ts +++ b/src/types.ts @@ -167,6 +167,42 @@ export interface PresetTemplate { path?: string } +export interface CharacterBeforeChatEventPayload { + session: Session + sessionKey: string + conversationId: string | undefined + presetName: string + preset: PresetTemplate + messages: Message[] + focusMessage?: Message + triggerReason?: string + systemVariables: Record + inputVariables: Record + persistedInputVariables: Record +} + +export interface CharacterAfterChatEventPayload { + session: Session + sessionKey: string + conversationId: string | undefined + presetName: string + preset: PresetTemplate + systemPrompt: string + messages: Message[] + focusMessage?: Message + triggerReason?: string + persistedHumanMessage: BaseMessage + lastResponseMessage?: BaseMessage + completionMessages: BaseMessage[] + status?: string | null +} + +export interface CharacterClearChatHistoryEventPayload { + sessionKey: string + conversationId: string + isDirect: boolean +} + export interface GroupInfo { messageCount: number messageWait?: boolean @@ -296,4 +332,16 @@ declare module 'koishi' { chathub_character_variable: CharacterVariableRecord chathub_character_wake_up_reply: WakeUpReplyRecord } + + interface Events { + 'chatluna_character/before-chat': ( + payload: CharacterBeforeChatEventPayload + ) => void | Promise + 'chatluna_character/after-chat': ( + payload: CharacterAfterChatEventPayload + ) => void | Promise + 'chatluna_character/clear-chat-history': ( + payload: CharacterClearChatHistoryEventPayload + ) => void | Promise + } } diff --git a/src/utils/history.ts b/src/utils/history.ts index 4a6cc8f..2874492 100644 --- a/src/utils/history.ts +++ b/src/utils/history.ts @@ -355,9 +355,9 @@ async function toBotMsg( return { content, name: getNotEmptyString( - msg.member?.name, msg.user?.nick, msg.user?.name, + msg.member?.name, id ), id, @@ -371,8 +371,8 @@ async function toBotMsg( msg.quote.elements ?? h.parse(msg.quote.content ?? '') ), name: getNotEmptyString( - msg.quote.user?.name, msg.quote.user?.nick, + msg.quote.user?.name, msg.quote.user?.id ), id: msg.quote.user?.id,