From 96a4c95a3a13b5d3dc511fa0664fe392d870ccd3 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:17:56 +0800 Subject: [PATCH] feat: context message in message group (#8833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * stash * docs(newMessage): 修正注释中的拼写错误 * refactor(MessageGroup): 优化组件逻辑和状态管理 重构消息组件的状态管理和逻辑顺序,提升代码可读性 将相关状态和逻辑分组,并提取公共变量 * feat(消息组件): 添加消息有用性更新功能 在MessageGroup组件中实现onUpdateUseful回调,用于更新消息的有用状态 当标记某条消息为有用时,自动取消其他消息的有用标记 * fix(i18n): 更新多语言翻译文件中的键值 - 将中文简体中的"useful"键值从"有用"改为"设置为上下文" - 在其他语言文件中为"useful"键添加待翻译标记 - 在部分语言文件中添加"merge"、"longRunning"等新键的待翻译标记 * feat(消息组): 添加群组上下文消息标识和有用消息提示 为消息组添加上下文消息标识功能,当消息被标记为有用时显示特殊标识 优化消息菜单栏的有用按钮提示文本 修复消息菜单栏依赖项数组不完整的问题 * feat(i18n): 更新多语言翻译文件并改进自动翻译脚本 为"useful"字段添加label和tip翻译,完善多个语言的翻译内容 改进自动翻译脚本,使用语言映射替换文件名 * docs(i18n): 更新多语言文件中上下文提示的翻译文本 * docs(messageUtils): 标记废弃工具调用结果消息构造函数 标记 `构造带工具调用结果的消息内容` 函数为废弃状态,后续将移除 * refactor(消息过滤): 重命名filterContextMessages为filterAfterContextClearMessages以更准确描述功能 * fix(MessageGroup): 修复依赖数组中缺少groupContextMessageId的问题 * feat(消息过滤): 添加根据上下文数量过滤消息的功能 * refactor(消息过滤): 拆分消息过滤逻辑并添加日志 将filterUsefulMessages函数拆分为多个独立函数,提高代码可维护性 添加日志输出以便调试消息过滤过程 * refactor(消息过滤): 优化聊天消息过滤逻辑并添加调试日志 重构消息过滤流程,将原有单步过滤拆分为多步处理 添加调试日志以跟踪各阶段过滤结果 * refactor(messageUtils): 移除未使用的logger并优化消息过滤逻辑 移除未使用的logger导入和调用,添加filterAdjacentUserMessaegs过滤步骤优化消息处理流程 * refactor(消息服务): 重构获取上下文消息数量的逻辑 使用 filterContextMessages 工具函数替代 lodash 的 takeRight 和手动计算逻辑 * fix(消息工具): 修复分组消息排序顺序错误 * fix(消息过滤): 优化消息组过滤逻辑,保留有用消息或最后一条消息 修改 filterUsefulMessages 函数注释以更清晰说明过滤逻辑 在 MessageGroup 组件中使用 lodash 的 last 方法获取最后一条消息 * fix(MessageGroup): 修复消息有用性更新逻辑的错误 处理消息有用性状态更新时,添加对消息存在性的检查并优化状态切换逻辑 * fix(Messages): 修复分组消息内部顺序不正确的问题 由于displayMessages是倒序的,导致分组后的消息内部顺序也是倒序的。通过toReversed()将每个分组内部的消息顺序再次反转,确保正确显示 * fix(消息过滤): 修改未标记有用消息的保留策略,从保留最后一条改为第一条 * fix: 将onUpdateUseful属性改为可选以处理未定义情况 * refactor(ApiService): 移除冗余的日志记录调用 * docs(types): 去除Message类型中useful字段的过时注释 * refactor(messageUtils): 移除分组消息中的冗余排序操作 原代码在分组消息时已经按原始索引顺序添加,无需再次排序 --- scripts/auto-translate-i18n.ts | 13 ++- src/renderer/src/i18n/locales/en-us.json | 5 +- src/renderer/src/i18n/locales/ja-jp.json | 5 +- src/renderer/src/i18n/locales/ru-ru.json | 5 +- src/renderer/src/i18n/locales/zh-cn.json | 5 +- src/renderer/src/i18n/locales/zh-tw.json | 5 +- src/renderer/src/i18n/translate/el-gr.json | 5 +- src/renderer/src/i18n/translate/es-es.json | 5 +- src/renderer/src/i18n/translate/fr-fr.json | 7 +- src/renderer/src/i18n/translate/pt-pt.json | 7 +- .../src/pages/home/Messages/Message.tsx | 8 +- .../src/pages/home/Messages/MessageGroup.tsx | 79 ++++++++++++++++--- .../src/pages/home/Messages/MessageHeader.tsx | 22 ++++-- .../pages/home/Messages/MessageMenubar.tsx | 22 ++++-- .../src/pages/home/Messages/Messages.tsx | 14 +++- src/renderer/src/services/ApiService.ts | 13 ++- src/renderer/src/services/MessagesService.ts | 23 ++---- src/renderer/src/services/TokenService.ts | 4 +- .../src/utils/messageUtils/filters.ts | 51 +++++++++--- src/renderer/src/utils/messageUtils/find.ts | 1 + 20 files changed, 230 insertions(+), 69 deletions(-) diff --git a/scripts/auto-translate-i18n.ts b/scripts/auto-translate-i18n.ts index 50345647d1..2efa7fec54 100644 --- a/scripts/auto-translate-i18n.ts +++ b/scripts/auto-translate-i18n.ts @@ -24,6 +24,17 @@ const openai = new OpenAI({ baseURL: BASE_URL }) +const languageMap = { + 'en-us': 'English', + 'ja-jp': 'Japanese', + 'ru-ru': 'Russian', + 'zh-tw': 'Traditional Chinese', + 'el-gr': 'Greek', + 'es-es': 'Spanish', + 'fr-fr': 'French', + 'pt-pt': 'Portuguese' +} + const PROMPT = ` You are a translation expert. Your sole responsibility is to translate the text enclosed within from the source language into {{target_language}}. Output only the translated text, preserving the original format, and without including any explanations, headers such as "TRANSLATE", or the tags. @@ -117,7 +128,7 @@ const main = async () => { console.error(`解析 ${filename} 出错,跳过此文件。`, error) continue } - const systemPrompt = PROMPT.replace('{{target_language}}', filename) + const systemPrompt = PROMPT.replace('{{target_language}}', languageMap[filename]) const result = await translateRecursively(targetJson, systemPrompt) count += 1 diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 901d9e9e01..eee7b34534 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -405,7 +405,10 @@ "regenerate": { "model": "Switch Model" }, - "useful": "Helpful" + "useful": { + "label": "Set as context", + "tip": "In this group of messages, this message will be selected to join the context" + } }, "multiple": { "select": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 903f395a3c..ad2fb0e808 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -405,7 +405,10 @@ "regenerate": { "model": "モデルを切り替え" }, - "useful": "役立つ" + "useful": { + "label": "上下文として設定する", + "tip": "このメッセージは、このメッセージセットの中でコンテキストに含まれるために選択されます" + } }, "multiple": { "select": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 1c44a19f45..eaa02fa76a 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -405,7 +405,10 @@ "regenerate": { "model": "Переключить модель" }, - "useful": "Полезно" + "useful": { + "label": "установить в качестве контекста", + "tip": "В этой группе сообщений данное сообщение будет выбрано для включения в контекст" + } }, "multiple": { "select": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8da54d8125..e35a5f8cd5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -405,7 +405,10 @@ "regenerate": { "model": "切换模型" }, - "useful": "有用" + "useful": { + "label": "设置为上下文", + "tip": "在这组消息中,该消息将被选择加入上下文" + } }, "multiple": { "select": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3a7c38f951..32fd14cd7e 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -405,7 +405,10 @@ "regenerate": { "model": "切換模型" }, - "useful": "有用" + "useful": { + "label": "設置為上下文", + "tip": "在這組訊息中,該訊息將被選擇加入上下文" + } }, "multiple": { "select": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 1b6ef88a65..dd0f579c7a 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -405,7 +405,10 @@ "regenerate": { "model": "Εναλλαγή μοντέλου" }, - "useful": "Χρήσιμο" + "useful": { + "label": "Ορισμός ως πλαίσιο αναφοράς", + "tip": "Σε αυτή την ομάδα μηνυμάτων, αυτό το μήνυμα θα επιλεγεί για να συμπεριληφθεί στο πλαίσιο" + } }, "multiple": { "select": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index c3d6f397ba..ac9c21e936 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -405,7 +405,10 @@ "regenerate": { "model": "Cambiar modelo" }, - "useful": "Útil" + "useful": { + "label": "establecer como contexto", + "tip": "En este grupo de mensajes, este mensaje se seleccionará para unirse al contexto" + } }, "multiple": { "select": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index ac1321a0ba..295dd0d600 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -405,7 +405,10 @@ "regenerate": { "model": "Changer de modèle" }, - "useful": "Utile" + "useful": { + "label": "Définir comme contexte", + "tip": "Dans ce groupe de messages, ce message sera sélectionné pour être inclus dans le contexte" + } }, "multiple": { "select": { @@ -2769,7 +2772,7 @@ "jsonSaveSuccess": "Configuration JSON sauvegardée", "logoUrl": "Адрес логотипа", "longRunning": "Mode d'exécution prolongée", - "longRunningTooltip": "Activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de délai d'attente lorsqu'il reçoit une notification de progression et prolonge le délai d'expiration maximal à 10 minutes.", + "longRunningTooltip": "Une fois activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de temporisation à la réception des notifications de progression, et prolonge le délai d'expiration maximal à 10 minutes.", "missingDependencies": "Manquantes, veuillez les installer pour continuer", "more": { "awesome": "Liste sélectionnée de serveurs MCP", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index b49a501432..b62bd9b20f 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -405,7 +405,10 @@ "regenerate": { "model": "Trocar modelo" }, - "useful": "Útil" + "useful": { + "label": "Definido como contexto", + "tip": "Neste conjunto de mensagens, esta mensagem será selecionada para ingressar no contexto" + } }, "multiple": { "select": { @@ -2769,7 +2772,7 @@ "jsonSaveSuccess": "Configuração JSON salva com sucesso", "logoUrl": "URL do Logotipo", "longRunning": "Modo de execução prolongada", - "longRunningTooltip": "Ativado, o servidor suporta tarefas de longa duração, reiniciando o temporizador de tempo limite ao receber notificações de progresso e prolongando o tempo máximo de tempo limite para 10 minutos.", + "longRunningTooltip": "Quando ativado, o servidor suporta tarefas de longa duração, redefinindo o temporizador de tempo limite ao receber notificações de progresso e estendendo o tempo máximo de tempo limite para 10 minutos.", "missingDependencies": "Ausente, instale para continuar", "more": { "awesome": "Lista selecionada de servidores MCP", diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 0c20f0d842..af69c18a4b 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -35,6 +35,8 @@ interface Props { isGrouped?: boolean isStreaming?: boolean onSetMessages?: Dispatch> + onUpdateUseful?: (msgId: string) => void + isGroupContextMessage?: boolean } const logger = loggerService.withContext('MessageItem') @@ -56,7 +58,9 @@ const MessageItem: FC = ({ index, hideMenuBar = false, isGrouped, - isStreaming = false + isStreaming = false, + onUpdateUseful, + isGroupContextMessage }) => { const { t } = useTranslation() const { assistant, setModel } = useAssistant(message.assistantId) @@ -166,6 +170,7 @@ const MessageItem: FC = ({ model={model} key={getModelUniqId(model)} topic={topic} + isGroupContextMessage={isGroupContextMessage} /> {isEditing && ( = ({ isGrouped={isGrouped} messageContainerRef={messageContainerRef as React.RefObject} setModel={setModel} + onUpdateUseful={onUpdateUseful} /> )} diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index a38afb730e..0f10d0c54b 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -1,3 +1,4 @@ +import { loggerService } from '@logger' import Scrollbar from '@renderer/components/Scrollbar' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' import { useChatContext } from '@renderer/hooks/useChatContext' @@ -16,6 +17,7 @@ import { useChatMaxWidth } from '../Chat' import MessageItem from './Message' import MessageGroupMenuBar from './MessageGroupMenuBar' +const logger = loggerService.withContext('MessageGroup') interface Props { messages: (Message & { index: number })[] topic: Topic @@ -23,14 +25,24 @@ interface Props { } const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { + const messageLength = messages.length + + // Hooks const { editMessage } = useMessageOperations(topic) const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings() const { isMultiSelectMode } = useChatContext(topic) - const messageLength = messages.length + const maxWidth = useChatMaxWidth() + const isGrouped = isMultiSelectMode ? false : messageLength > 1 && messages.every((m) => m.role === 'assistant') + + // States const [_multiModelMessageStyle, setMultiModelMessageStyle] = useState( messages[0].multiModelMessageStyle || multiModelMessageStyleSetting ) + const [selectedIndex, setSelectedIndex] = useState(messageLength - 1) + + // Refs + const prevMessageLengthRef = useRef(messageLength) // 对于单模型消息,采用简单的样式,避免 overflow 影响内部的 sticky 效果 const multiModelMessageStyle = useMemo( @@ -38,8 +50,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { [_multiModelMessageStyle, messageLength] ) - const prevMessageLengthRef = useRef(messageLength) - const [selectedIndex, setSelectedIndex] = useState(messageLength - 1) + const isGrid = multiModelMessageStyle === 'grid' const selectedMessageId = useMemo(() => { if (messages.length === 1) return messages[0]?.id @@ -67,9 +78,6 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { [editMessage, selectedMessageId] ) - const isGrouped = isMultiSelectMode ? false : messageLength > 1 && messages.every((m) => m.role === 'assistant') - const isGrid = multiModelMessageStyle === 'grid' - useEffect(() => { if (messageLength > prevMessageLengthRef.current) { setSelectedIndex(messageLength - 1) @@ -164,6 +172,43 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { return () => messages.forEach((message) => registerMessageElement?.(message.id, null)) }, [messages, registerMessageElement]) + const onUpdateUseful = useCallback( + (msgId: string) => { + const message = messages.find((msg) => msg.id === msgId) + if (!message) { + logger.error("the message to update doesn't exist in this group") + return + } + if (message.useful) { + editMessage(msgId, { useful: undefined }) + return + } else { + const toResetUsefulMsgs = messages.filter((msg) => msg.id !== msgId && msg.useful) + toResetUsefulMsgs.forEach(async (msg) => { + editMessage(msg.id, { + useful: undefined + }) + }) + editMessage(msgId, { useful: true }) + } + }, + [editMessage, messages] + ) + + const groupContextMessageId = useMemo(() => { + // NOTE: 旧数据可能存在一组消息有多个useful的情况,只取第一个,不再另作迁移 + // find first useful + const usefulMsg = messages.find((msg) => msg.useful) + if (usefulMsg) { + return usefulMsg.id + } else if (messages.length > 0) { + return messages[0].id + } else { + logger.warn('Empty message group') + return '' + } + }, [messages]) + const renderMessage = useCallback( (message: Message & { index: number }) => { const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped @@ -184,7 +229,11 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { selected: message.id === selectedMessageId } ])}> - + ) @@ -202,7 +251,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { selected: message.id === selectedMessageId } ])}> - + } trigger={gridPopoverTrigger} @@ -217,11 +266,19 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { return messageContent }, - [isGrid, isGrouped, topic, multiModelMessageStyle, messages.length, selectedMessageId, gridPopoverTrigger] + [ + isGrid, + isGrouped, + topic, + multiModelMessageStyle, + messages.length, + selectedMessageId, + onUpdateUseful, + groupContextMessageId, + gridPopoverTrigger + ] ) - const maxWidth = useChatMaxWidth() - return ( { @@ -30,7 +33,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => { return modelId ? getModelLogo(modelId) : undefined } -const MessageHeader: FC = memo(({ assistant, model, message, topic }) => { +const MessageHeader: FC = memo(({ assistant, model, message, topic, isGroupContextMessage }) => { const avatar = useAvatar() const { theme } = useTheme() const { userName, sidebarIcons } = useSettings() @@ -107,9 +110,16 @@ const MessageHeader: FC = memo(({ assistant, model, message, topic }) => )} - - {username} - + + + {username} + + {isGroupContextMessage && ( + + + + )} + {dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')} @@ -150,7 +160,7 @@ const InfoWrap = styled.div` gap: 4px; ` -const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>` +const UserName = styled.span<{ isBubbleStyle?: boolean; theme?: string }>` font-size: 14px; font-weight: 600; color: ${(props) => (props.isBubbleStyle && props.theme === 'dark' ? 'white' : 'var(--color-text)')}; diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 4fe08bb55a..36dd4f39ad 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -52,11 +52,22 @@ interface Props { isAssistantMessage: boolean messageContainerRef: React.RefObject setModel: (model: Model) => void + onUpdateUseful?: (msgId: string) => void } const MessageMenubar: FC = (props) => { - const { message, index, isGrouped, isLastMessage, isAssistantMessage, assistant, topic, model, messageContainerRef } = - props + const { + message, + index, + isGrouped, + isLastMessage, + isAssistantMessage, + assistant, + topic, + model, + messageContainerRef, + onUpdateUseful + } = props const { t } = useTranslation() const { toggleMultiSelectMode } = useChatContext(props.topic) const [copied, setCopied] = useState(false) @@ -65,7 +76,6 @@ const MessageMenubar: FC = (props) => { const [showDeleteTooltip, setShowDeleteTooltip] = useState(false) // const assistantModel = assistant?.model const { - editMessage, deleteMessage, resendMessage, regenerateAssistantMessage, @@ -402,9 +412,9 @@ const MessageMenubar: FC = (props) => { const onUseful = useCallback( (e: React.MouseEvent) => { e.stopPropagation() - editMessage(message.id, { useful: !message.useful }) + onUpdateUseful?.(message.id) }, - [message, editMessage] + [message.id, onUpdateUseful] ) const blockEntities = useSelector(messageBlocksSelectors.selectEntities) @@ -546,7 +556,7 @@ const MessageMenubar: FC = (props) => { )} {isAssistantMessage && isGrouped && ( - + {message.useful ? ( diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index d552f85137..d8a650412a 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -278,7 +278,19 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o requestAnimationFrame(() => onComponentUpdate?.()) }, [onComponentUpdate]) - const groupedMessages = useMemo(() => Object.entries(getGroupedMessages(displayMessages)), [displayMessages]) + // NOTE: 因为displayMessages是倒序的,所以得到的groupedMessages每个group内部也是倒序的,需要再倒一遍 + const groupedMessages = useMemo(() => { + const grouped = Object.entries(getGroupedMessages(displayMessages)) + const newGrouped: { + [key: string]: (Message & { + index: number + })[] + } = {} + grouped.forEach(([key, group]) => { + newGrouped[key] = group.toReversed() + }) + return Object.entries(newGrouped) + }, [displayMessages]) return ( m.role === 'user') const lastAnswer = findLast(messages, (m) => m.role === 'assistant') @@ -459,10 +460,14 @@ export async function fetchChatCompletion({ const { maxTokens, contextCount } = getAssistantSettings(assistant) - const filteredMessages = filterUsefulMessages(messages) + const filteredMessages2 = filterUsefulMessages(filteredMessages1) + + const filteredMessages3 = filterLastAssistantMessage(filteredMessages2) + + const filteredMessages4 = filterAdjacentUserMessaegs(filteredMessages3) const _messages = filterUserRoleStartMessages( - filterEmptyMessages(filterContextMessages(takeRight(filteredMessages, contextCount + 2))) // 取原来几个provider的最大值 + filterEmptyMessages(filterAfterContextClearMessages(takeRight(filteredMessages4, contextCount + 2))) // 取原来几个provider的最大值 ) // FIXME: qwen3即使关闭思考仍然会导致enableReasoning的结果为true diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index c73f22b5d6..22f72f0fb9 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -21,10 +21,10 @@ import { createMessage, resetMessage } from '@renderer/utils/messageUtils/create' +import { filterContextMessages } from '@renderer/utils/messageUtils/filters' import { getMainTextContent } from '@renderer/utils/messageUtils/find' import dayjs from 'dayjs' import { t } from 'i18next' -import { takeRight } from 'lodash' import { NavigateFunction } from 'react-router' import { getAssistantById, getAssistantProvider, getDefaultModel } from './AssistantService' @@ -34,7 +34,7 @@ import FileManager from './FileManager' const logger = loggerService.withContext('MessagesService') export { - filterContextMessages, + filterAfterContextClearMessages, filterEmptyMessages, filterMessages, filterUsefulMessages, @@ -43,23 +43,14 @@ export { } from '@renderer/utils/messageUtils/filters' export function getContextCount(assistant: Assistant, messages: Message[]) { - const rawContextCount = assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT - const maxContextCount = rawContextCount === MAX_CONTEXT_COUNT ? UNLIMITED_CONTEXT_COUNT : rawContextCount + const settingContextCount = assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT + const actualContextCount = settingContextCount === MAX_CONTEXT_COUNT ? UNLIMITED_CONTEXT_COUNT : settingContextCount - const _messages = takeRight(messages, maxContextCount) - - const clearIndex = _messages.findLastIndex((message) => message.type === 'clear') - - let currentContextCount = 0 - if (clearIndex === -1) { - currentContextCount = _messages.length - } else { - currentContextCount = _messages.length - (clearIndex + 1) - } + const contextMsgs = filterContextMessages(messages, actualContextCount) return { - current: currentContextCount, - max: rawContextCount + current: contextMsgs.length, + max: settingContextCount } } diff --git a/src/renderer/src/services/TokenService.ts b/src/renderer/src/services/TokenService.ts index e1b6d48b1d..bb5a64621c 100644 --- a/src/renderer/src/services/TokenService.ts +++ b/src/renderer/src/services/TokenService.ts @@ -5,7 +5,7 @@ import { flatten, takeRight } from 'lodash' import { approximateTokenSize } from 'tokenx' import { getAssistantSettings } from './AssistantService' -import { filterContextMessages, filterMessages } from './MessagesService' +import { filterAfterContextClearMessages, filterMessages } from './MessagesService' interface MessageItem { name?: string @@ -167,7 +167,7 @@ export async function estimateMessagesUsage({ export async function estimateHistoryTokens(assistant: Assistant, msgs: Message[]) { const { contextCount } = getAssistantSettings(assistant) const maxContextCount = contextCount - const messages = filterMessages(filterContextMessages(takeRight(msgs, maxContextCount))) + const messages = filterMessages(filterAfterContextClearMessages(takeRight(msgs, maxContextCount))) // 有 usage 数据的消息,快速计算总数 const uasageTokens = messages diff --git a/src/renderer/src/utils/messageUtils/filters.ts b/src/renderer/src/utils/messageUtils/filters.ts index 6422ad0761..ababfc5ce1 100644 --- a/src/renderer/src/utils/messageUtils/filters.ts +++ b/src/renderer/src/utils/messageUtils/filters.ts @@ -4,11 +4,13 @@ import type { Message } from '@renderer/types/newMessage' // Assuming correct Me import { MessageBlockType } from '@renderer/types/newMessage' // May need Block types if refactoring to use them // import type { MessageBlock, MainTextMessageBlock } from '@renderer/types/newMessageTypes'; -import { remove } from 'lodash' +import { remove, takeRight } from 'lodash' import { isEmpty } from 'lodash' // Assuming getGroupedMessages is also moved here or imported // import { getGroupedMessages } from './path/to/getGroupedMessages'; +// const logger = loggerService.withContext('Utils.filter') + /** * Filters out messages of type '@' or 'clear' and messages without main text content. */ @@ -27,7 +29,7 @@ export const filterMessages = (messages: Message[]) => { /** * Filters messages to include only those after the last 'clear' type message. */ -export function filterContextMessages(messages: Message[]): Message[] { +export function filterAfterContextClearMessages(messages: Message[]): Message[] { const clearIndex = messages.findLastIndex((message) => message.type === 'clear') if (clearIndex === -1) { @@ -95,17 +97,16 @@ export function getGroupedMessages(messages: Message[]): { [key: string]: (Messa groups[key] = [] } groups[key].push({ ...message, index }) // Add message with its original index - // Sort by index within group to maintain original order - groups[key].sort((a, b) => b.index - a.index) }) return groups } /** * Filters messages based on the 'useful' flag and message role sequences. + * Only remain one message in a group. Either useful or fallback to the last message in the group. */ export function filterUsefulMessages(messages: Message[]): Message[] { - let _messages = [...messages] + const _messages = [...messages] const groupedMessages = getGroupedMessages(messages) Object.entries(groupedMessages).forEach(([key, groupedMsgs]) => { @@ -119,8 +120,8 @@ export function filterUsefulMessages(messages: Message[]): Message[] { } }) } else if (groupedMsgs.length > 0) { - // Keep only the last message if none are marked useful - const messagesToRemove = groupedMsgs.slice(0, -1) + // Keep only the first message if none are marked useful + const messagesToRemove = groupedMsgs.slice(1) messagesToRemove.forEach((m) => { remove(_messages, (o) => o.id === m.id) }) @@ -128,17 +129,23 @@ export function filterUsefulMessages(messages: Message[]): Message[] { } }) + return _messages +} + +export function filterLastAssistantMessage(messages: Message[]): Message[] { + const _messages = [...messages] // Remove trailing assistant messages while (_messages.length > 0 && _messages[_messages.length - 1].role === 'assistant') { _messages.pop() } + return _messages +} +export function filterAdjacentUserMessaegs(messages: Message[]): Message[] { // Filter adjacent user messages, keeping only the last one - _messages = _messages.filter((message, index, origin) => { + return messages.filter((message, index, origin) => { return !(message.role === 'user' && index + 1 < origin.length && origin[index + 1].role === 'user') }) - - return _messages } // Note: getGroupedMessages might also need to be moved or imported. @@ -154,3 +161,27 @@ export function filterUsefulMessages(messages: Message[]): Message[] { // }) // return groups // } + +/** + * Filters and processes messages based on context requirements + * @param messages - Array of messages to be filtered + * @param contextCount - Number of messages to keep in context (excluding new user and assistant messages) + * @returns Filtered array of messages that: + * 1. Only includes messages after the last context clear + * 2. Only includes useful message in a group (based on useful flag) + * 3. Limited to contextCount + 2 messages (including space for new user/assistant messages) + * 4. Starts from first user message + * 5. Excludes empty messages + */ +export function filterContextMessages(messages: Message[], contextCount: number): Message[] { + // NOTE: 和 fetchCompletions 中过滤消息的逻辑相同。 + // 按理说 fetchCompletions 也可以复用这个函数,不过 fetchCompletions 不敢随便乱改,后面再考虑重构吧 + const afterContextClearMsgs = filterAfterContextClearMessages(messages) + const usefulMsgs = filterUsefulMessages(afterContextClearMsgs) + const adjacentRemovedMsgs = filterAdjacentUserMessaegs(usefulMsgs) + const filteredMessages = filterUserRoleStartMessages( + filterEmptyMessages(takeRight(adjacentRemovedMsgs, contextCount)) + ) + + return filteredMessages +} diff --git a/src/renderer/src/utils/messageUtils/find.ts b/src/renderer/src/utils/messageUtils/find.ts index 0da2ea3444..47f16a895b 100644 --- a/src/renderer/src/utils/messageUtils/find.ts +++ b/src/renderer/src/utils/messageUtils/find.ts @@ -203,6 +203,7 @@ export const findTranslationBlocks = (message: Message): TranslationMessageBlock } /** + * @deprecated * 构造带工具调用结果的消息内容 * @param blocks * @returns