Skip to content

feat: 为 Character 增加外部记忆适配生命周期事件#82

Open
Procyon-Nan wants to merge 1 commit into
ChatLunaLab:mainfrom
Procyon-Nan:ProcyonNAN/character-livingmemory-hooks
Open

feat: 为 Character 增加外部记忆适配生命周期事件#82
Procyon-Nan wants to merge 1 commit into
ChatLunaLab:mainfrom
Procyon-Nan:ProcyonNAN/character-livingmemory-hooks

Conversation

@Procyon-Nan
Copy link
Copy Markdown

@Procyon-Nan Procyon-Nan commented May 18, 2026

概要

本 PR 为 chatluna-character 增加最小生命周期事件,供 livingmemory 等外部插件在不侵入 Character 主逻辑的情况下完成记忆注入、回复后处理和缓存清理。

同时统一部分消息历史中的发言人名称来源,优先使用 QQ 昵称/名称,避免同一用户因群名片差异在记忆或历史上下文中被识别成不同人。

主要改动

  • 新增 chatluna_character/before-chatchatluna_character/after-chatchatluna_character/clear-chat-history 三个事件及对应 payload 类型。
  • before-chat 在 prompt 渲染前暴露 systemVariablesinputVariablespersistedInputVariables,允许外部插件注入 {living_memory} 等变量。
  • after-chat 在 Character 成功回复并完成内部状态更新后异步发布上下文,包括已渲染的 systemPrompt、消息列表、用户消息、最后回复和状态。
  • clear-chat-history 在 Character 内部清理完成后异步通知外部插件清理自身缓存。
  • 外部监听器异常或耗时不会阻塞 Character 回复、response lock 释放、pending trigger 处理或 clear 流程。
  • 实时消息、引用消息和通用历史消息的发言人名称优先使用 QQ 昵称/名称;OneBot 历史仍保留 nickname 优先于群名片的行为。

影响面

  • 新事件仅作为外部插件的最小接入面;无监听器时 Character 原有流程应保持一致。

Summary by CodeRabbit

  • New Features

    • Implemented character-scoped session and conversation tracking with event lifecycle management for chat interactions.
    • Chat history clearing now emits trackable events that plugins can subscribe to.
  • Improvements

    • Enhanced message author and quote attribution with improved name resolution.
    • Strengthened prompt rendering through explicit variable management.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Warning

Rate limit exceeded

@Procyon-Nan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 23 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88045eea-a695-4249-b015-504dac03d026

📥 Commits

Reviewing files that changed from the base of the PR and between 10d7043 and 2bed5f9.

📒 Files selected for processing (4)
  • src/plugins/chat.ts
  • src/service/message.ts
  • src/types.ts
  • src/utils/history.ts
📝 Walkthrough

Walkthrough

This PR adds three new typed event hooks (chatluna_character/before-chat, /after-chat, /clear-chat-history) to the chat lifecycle, integrates them throughout the chat pipeline with proper session/conversation scoping, and improves message author name resolution fallback order across the collector and history utilities.

Changes

Chat Lifecycle Event Hooks

Layer / File(s) Summary
Event Payload Type Contracts
src/types.ts
Exports three new TypeScript interfaces for chat event payloads (CharacterBeforeChatEventPayload, CharacterAfterChatEventPayload, CharacterClearChatHistoryEventPayload) and extends the Koishi module augmentation with strongly typed event handler signatures for each hook.
Before-Chat Hook Setup and Variable Preparation
src/plugins/chat.ts
Introduces getCharacterSessionKey() and getCharacterConversationId() helpers for consistent session scoping; refactors prepareMessages() to construct systemVariables, inputVariables, and persistedInputVariables; builds a CharacterBeforeChatEventPayload; dispatches the before-chat hook via ctx.parallel() with error logging; and threads the new systemPrompt through the return type for downstream use.
After-Chat Hook Dispatch and Scheduling
src/plugins/chat.ts
Updates the apply() flow to destructure and pass systemPrompt from prepareMessages(); builds a CharacterAfterChatEventPayload including systemPrompt, conversationId, and completedMessages; and schedules the after-chat dispatch via service.muteAtLeast().then().catch() for proper async error handling.
Clear-Chat-History Event Emission
src/service/message.ts
Imports the clear-history event payload type; adds toClearChatHistoryPayload() to compute conversationId and isDirect from the session key; introduces _emitClearChatHistory() to dispatch the event via ctx.parallel(); updates clear() to emit events for single-group clears and expands clearedGroupIds by unioning _groupLocks, _messages, and _groupTemp keys for clear-all scenarios.

Message Author Name Resolution Improvements

Layer / File(s) Summary
Message Collector Name Resolution Updates
src/service/message.ts
Reorders getNotEmptyString() arguments for quote author and broadcasted message author name resolution to prefer user.nick and user.name before member.name and session fallbacks; also refactors _addMessage() to obtain the per-group message array directly from _messages.
History Utility Name Resolution Order
src/utils/history.ts
Reorders getNotEmptyString() arguments in toBotMsg() for both main message and quoted message name resolution to prefer user.nick and user.name before member.name.

Sequence Diagram

sequenceDiagram
  participant Client
  participant PluginChat
  participant MessageCollector
  participant EventBus

  Client->>PluginChat: trigger chat message
  PluginChat->>PluginChat: prepareMessages()
  PluginChat->>PluginChat: build systemVariables, inputVariables
  PluginChat->>EventBus: emit chatluna_character/before-chat
  EventBus-->>PluginChat: before-chat hook complete
  PluginChat->>MessageCollector: collect and process messages
  MessageCollector->>MessageCollector: build completedMessages
  PluginChat->>MessageCollector: service.muteAtLeast()
  MessageCollector-->>PluginChat: mute complete
  PluginChat->>EventBus: emit chatluna_character/after-chat
  EventBus-->>PluginChat: after-chat hook complete
  PluginChat-->>Client: return response

  Client->>MessageCollector: clear chat history
  MessageCollector->>MessageCollector: compute conversationId, isDirect
  MessageCollector->>EventBus: emit chatluna_character/clear-chat-history
  EventBus-->>MessageCollector: clear-chat-history hook complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ChatLunaLab/chatluna-character#37: Both PRs modify src/service/message.ts's MessageCollector/clear() flow around chat-history handling (main PR emits a chatluna_character/clear-chat-history event; related PR adds history/status persistence boundaries tied to clearing).
  • ChatLunaLab/chatluna-character#56: Both PRs modify src/plugins/chat.ts—specifically the prepareMessages() logic in ways that affect message/context handling and return/payload construction.

Suggested reviewers

  • dingyi222666

Poem

🐰 Hooks upon the chat's great stage,
Before and after, page by page,
With names resolved and histories clear,
Events dispatched for all to hear!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding external memory lifecycle events to the Character plugin. It accurately reflects the primary objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces lifecycle events for character interactions—specifically before-chat, after-chat, and clear-chat-history—to allow external plugins to hook into the chat process. It also refactors variable handling in the message preparation phase and improves user name resolution logic across the service. The review feedback suggests expanding the locking mechanism in the history clearing method to prevent potential race conditions and recommends consolidating timestamp generation to ensure consistency and proper synchronization of variables when modified by event listeners.

Comment thread src/service/message.ts Outdated
Comment on lines 530 to 540
const groupIds = Object.keys(this._groupLocks).sort()
const clearedGroupIds = Array.from(
new Set([
...groupIds,
...Object.keys(this._messages),
...Object.keys(this._groupTemp)
])
).sort()
const unlocks: (() => void)[] = []
for (const gid of groupIds) {
unlocks.push(await this._lockByGroupId(gid))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In the clear method (clear-all case), the code only acquires locks for sessions already present in _groupLocks. However, it attempts to clear data and notify listeners for all sessions in clearedGroupIds (which includes those in _messages or _groupTemp). This can lead to race conditions if a session is active but hasn't been locked yet. Redefining groupIds to include all relevant session IDs ensures proper locking and cleanup for all active sessions throughout the function.

Suggested change
const groupIds = Object.keys(this._groupLocks).sort()
const clearedGroupIds = Array.from(
new Set([
...groupIds,
...Object.keys(this._messages),
...Object.keys(this._groupTemp)
])
).sort()
const unlocks: (() => void)[] = []
for (const gid of groupIds) {
unlocks.push(await this._lockByGroupId(gid))
const groupIds = Array.from(
new Set([
...Object.keys(this._groupLocks),
...Object.keys(this._messages),
...Object.keys(this._groupTemp)
])
).sort()
const clearedGroupIds = groupIds
const unlocks: (() => void)[] = []
for (const gid of groupIds) {
unlocks.push(await this._lockByGroupId(gid))
}

Comment thread src/plugins/chat.ts
Comment on lines +1556 to +1576
const inputVariables: Record<string, unknown> = {
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
}
const persistedInputVariables: Record<string, unknown> = {
...inputVariables,
history_new: recentMessages
.join('\n\n')
.replaceAll('{', '{{')
.replaceAll('}', '}}'),
time: formatTimestamp(new Date())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There are two points for improvement in this block:

  1. The current time is formatted twice using new Date(). It is better to calculate it once and reuse it to ensure consistency between inputVariables and persistedInputVariables.
  2. persistedInputVariables is created as a shallow copy of inputVariables before the chatluna_character/before-chat event is emitted. If an external plugin injects new variables into inputVariables during the event, these changes will not be reflected in persistedInputVariables (and thus missing from the persisted history prompt) unless the listener explicitly modifies both. Consider deriving persistedInputVariables after the event emission or ensuring they stay in sync.
    const now = new Date()
    const timestamp = formatTimestamp(now)
    const inputVariables: Record<string, unknown> = {
        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<string, unknown> = {
        ...inputVariables,
        history_new: recentMessages
            .join('\n\n')
            .replaceAll('{', '{{')
            .replaceAll('}', '}}'),
        time: timestamp
    }

新增 before-chat、after-chat、clear-chat-history 生命周期事件及对应类型,作为 livingmemory 等外部插件的最小接入面。

before-chat 在 prompt 渲染前暴露可注入变量表;after-chat 和 clear-chat-history 均异步发布,避免外部插件阻塞 Character 回复、锁释放或清理流程。

统一实时消息、引用消息和历史消息的发言人名称优先级,优先使用 QQ 昵称或名称,减少群名片导致的身份分裂。

处理 PR review:clear-all 对所有相关 session key 统一加锁;prompt 变量复用同一 timestamp,并让 persisted 变量继承 before-chat 后的 inputVariables。
@Procyon-Nan Procyon-Nan force-pushed the ProcyonNAN/character-livingmemory-hooks branch from 10d7043 to 2bed5f9 Compare May 18, 2026 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant