import type { ChatMessage } from "../llm/types"; /** Detect messages that already embed full paper dumps (should not be in UI history). */ export function isPaperDump(content: string): boolean { return ( content.includes("【论文正文】") || content.includes("【论文 1】") || content.startsWith("【论文元数据】") || content.startsWith("已收到论文内容") || content.includes("【说明】以上为本次会话的论文材料") ); } /** * Keep short Q&A turns for the model. Truncate very long assistant replies * so paper context + latest question are less likely to be dropped. */ export function sanitizeChatHistory( history: ChatMessage[], options?: { maxMessages?: number; maxAssistantChars?: number }, ): ChatMessage[] { const maxMessages = options?.maxMessages ?? 10; const maxAssistantChars = options?.maxAssistantChars ?? 5000; const cleaned = history .filter((m) => m.role === "user" || m.role === "assistant") .filter((m) => !isPaperDump(m.content)) .map((m) => { if ( m.role === "assistant" && m.content && m.content.length > maxAssistantChars ) { return { ...m, content: m.content.slice(0, maxAssistantChars) + "\n\n…(为保留论文上下文,历史回复已截断)", }; } return m; }); return cleaned.slice(-maxMessages); } /** * Pin paper corpus near the start of the request, then prior Q&A, then the * current task. Avoids stuffing full papers only into the latest message * (which providers often truncate under context pressure on turn 2+). */ export function assemblePaperChatMessages(options: { system: string; paperBlock: string; taskBlock: string; history: ChatMessage[]; }): ChatMessage[] { const messages: ChatMessage[] = [ { role: "system", content: options.system }, { role: "user", content: `${options.paperBlock} 【说明】以上为本次会话的论文材料。后续所有问题与分析任务均基于这些论文;无需用户再次提供全文或文本。`, }, { role: "assistant", content: "已收到并理解以上论文材料。请继续提问或下达分析任务,我将基于这些论文作答,不会要求你再次提供全文。", }, ]; messages.push(...sanitizeChatHistory(options.history)); messages.push({ role: "user", content: options.taskBlock }); return messages; }