feat: context message in message group (#8833)

* 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): 移除分组消息中的冗余排序操作

原代码在分组消息时已经按原始索引顺序添加,无需再次排序
This commit is contained in:
Phantom 2025-08-10 18:17:56 +08:00 committed by GitHub
parent 6b8ba9d273
commit 96a4c95a3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 230 additions and 69 deletions

View File

@ -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 <translate_input> 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 <translate_input> 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

View File

@ -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": {

View File

@ -405,7 +405,10 @@
"regenerate": {
"model": "モデルを切り替え"
},
"useful": "役立つ"
"useful": {
"label": "上下文として設定する",
"tip": "このメッセージは、このメッセージセットの中でコンテキストに含まれるために選択されます"
}
},
"multiple": {
"select": {

View File

@ -405,7 +405,10 @@
"regenerate": {
"model": "Переключить модель"
},
"useful": "Полезно"
"useful": {
"label": "установить в качестве контекста",
"tip": "В этой группе сообщений данное сообщение будет выбрано для включения в контекст"
}
},
"multiple": {
"select": {

View File

@ -405,7 +405,10 @@
"regenerate": {
"model": "切换模型"
},
"useful": "有用"
"useful": {
"label": "设置为上下文",
"tip": "在这组消息中,该消息将被选择加入上下文"
}
},
"multiple": {
"select": {

View File

@ -405,7 +405,10 @@
"regenerate": {
"model": "切換模型"
},
"useful": "有用"
"useful": {
"label": "設置為上下文",
"tip": "在這組訊息中,該訊息將被選擇加入上下文"
}
},
"multiple": {
"select": {

View File

@ -405,7 +405,10 @@
"regenerate": {
"model": "Εναλλαγή μοντέλου"
},
"useful": "Χρήσιμο"
"useful": {
"label": "Ορισμός ως πλαίσιο αναφοράς",
"tip": "Σε αυτή την ομάδα μηνυμάτων, αυτό το μήνυμα θα επιλεγεί για να συμπεριληφθεί στο πλαίσιο"
}
},
"multiple": {
"select": {

View File

@ -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": {

View File

@ -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",

View File

@ -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",

View File

@ -35,6 +35,8 @@ interface Props {
isGrouped?: boolean
isStreaming?: boolean
onSetMessages?: Dispatch<SetStateAction<Message[]>>
onUpdateUseful?: (msgId: string) => void
isGroupContextMessage?: boolean
}
const logger = loggerService.withContext('MessageItem')
@ -56,7 +58,9 @@ const MessageItem: FC<Props> = ({
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<Props> = ({
model={model}
key={getModelUniqId(model)}
topic={topic}
isGroupContextMessage={isGroupContextMessage}
/>
{isEditing && (
<MessageEditor
@ -202,6 +207,7 @@ const MessageItem: FC<Props> = ({
isGrouped={isGrouped}
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
setModel={setModel}
onUpdateUseful={onUpdateUseful}
/>
</MessageFooter>
)}

View File

@ -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<MultiModelMessageStyle>(
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
}
])}>
<MessageItem {...messageProps} />
<MessageItem
onUpdateUseful={onUpdateUseful}
isGroupContextMessage={isGrouped && message.id === groupContextMessageId}
{...messageProps}
/>
</MessageWrapper>
)
@ -202,7 +251,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
selected: message.id === selectedMessageId
}
])}>
<MessageItem {...messageProps} />
<MessageItem onUpdateUseful={onUpdateUseful} {...messageProps} />
</MessageWrapper>
}
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 (
<MessageEditingProvider>
<GroupContainer

View File

@ -1,4 +1,5 @@
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { HStack } from '@renderer/components/Layout'
import UserPopup from '@renderer/components/Popups/UserPopup'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { getModelLogo } from '@renderer/config/models'
@ -12,8 +13,9 @@ import { getModelName } from '@renderer/services/ModelService'
import type { Assistant, Model, Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { firstLetter, isEmoji, removeLeadingEmoji } from '@renderer/utils'
import { Avatar, Checkbox } from 'antd'
import { Avatar, Checkbox, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { Sparkle } from 'lucide-react'
import { FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -23,6 +25,7 @@ interface Props {
assistant: Assistant
model?: Model
topic: Topic
isGroupContextMessage?: boolean
}
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
@ -30,7 +33,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
return modelId ? getModelLogo(modelId) : undefined
}
const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic }) => {
const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic, isGroupContextMessage }) => {
const avatar = useAvatar()
const { theme } = useTheme()
const { userName, sidebarIcons } = useSettings()
@ -107,9 +110,16 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic }) =>
</>
)}
<UserWrap>
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
{username}
</UserName>
<HStack alignItems="center">
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
{username}
</UserName>
{isGroupContextMessage && (
<Tooltip title={t('chat.message.useful.tip')}>
<Sparkle fill="var(--color-primary)" strokeWidth={0} size={18} />
</Tooltip>
)}
</HStack>
<InfoWrap className="message-header-info-wrap">
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</InfoWrap>
@ -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)')};

View File

@ -52,11 +52,22 @@ interface Props {
isAssistantMessage: boolean
messageContainerRef: React.RefObject<HTMLDivElement>
setModel: (model: Model) => void
onUpdateUseful?: (msgId: string) => void
}
const MessageMenubar: FC<Props> = (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> = (props) => {
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
// const assistantModel = assistant?.model
const {
editMessage,
deleteMessage,
resendMessage,
regenerateAssistantMessage,
@ -402,9 +412,9 @@ const MessageMenubar: FC<Props> = (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> = (props) => {
</Dropdown>
)}
{isAssistantMessage && isGrouped && (
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
<Tooltip title={t('chat.message.useful.label')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
{message.useful ? (
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />

View File

@ -278,7 +278,19 @@ const Messages: React.FC<MessagesProps> = ({ 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 (
<MessagesContainer

View File

@ -45,6 +45,7 @@ import {
} from '@renderer/utils/abortController'
import { isAbortError } from '@renderer/utils/error'
import { extractInfoFromXML, ExtractResults } from '@renderer/utils/extract'
import { filterAdjacentUserMessaegs, filterLastAssistantMessage } from '@renderer/utils/messageUtils/filters'
import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
import {
buildSystemPromptWithThinkTool,
@ -66,7 +67,7 @@ import {
import { processKnowledgeSearch } from './KnowledgeService'
import { MemoryProcessor } from './MemoryProcessor'
import {
filterContextMessages,
filterAfterContextClearMessages,
filterEmptyMessages,
filterUsefulMessages,
filterUserRoleStartMessages
@ -441,7 +442,7 @@ export async function fetchChatCompletion({
const AI = new AiProvider(provider)
// Make sure that 'Clear Context' works for all scenarios including external tool and normal chat.
messages = filterContextMessages(messages)
const filteredMessages1 = filterAfterContextClearMessages(messages)
const lastUserMessage = findLast(messages, (m) => 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

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}

View File

@ -203,6 +203,7 @@ export const findTranslationBlocks = (message: Message): TranslationMessageBlock
}
/**
* @deprecated
*
* @param blocks
* @returns