From 837dc74ffcec4f450867f86b6569a30446b58a45 Mon Sep 17 00:00:00 2001 From: CJACK Date: Sun, 3 May 2026 15:25:06 +0800 Subject: [PATCH] feat: implement DS2API_HISTORY.txt transcript parser to merge history into chat messages --- tests/node/chat-history-utils.test.js | 44 +++++++++++++++++++ .../features/chatHistory/chatHistoryUtils.js | 40 ++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/tests/node/chat-history-utils.test.js b/tests/node/chat-history-utils.test.js index b5dd085..05bf2f0 100644 --- a/tests/node/chat-history-utils.test.js +++ b/tests/node/chat-history-utils.test.js @@ -58,3 +58,47 @@ test('chat history strict parser inserts history after system messages', async ( { role: 'user', content: 'latest' }, ]); }); + +test('chat history transcript parser replaces current input file placeholder', async () => { + const { + buildListModeMessages, + } = await loadUtils(); + const t = (key) => key; + const item = { + messages: [{ + role: 'user', + content: 'Continue from the latest state in the attached DS2API_HISTORY.txt context. Treat it as the current working state and answer the latest user request directly.', + }], + history_text: [ + '# DS2API_HISTORY.txt', + 'Prior conversation history and tool progress.', + '', + '=== 1. SYSTEM ===', + 'policy', + '', + '=== 2. USER ===', + 'hello', + '', + '=== 3. ASSISTANT ===', + 'hi', + '', + '=== 4. TOOL ===', + '[name=search_web tool_call_id=call_1]', + '{"ok":true}', + '', + '=== 5. USER ===', + 'latest', + '', + ].join('\n'), + }; + + const result = buildListModeMessages(item, t); + assert.equal(result.historyMerged, true); + assert.deepEqual(result.messages, [ + { role: 'system', content: 'policy' }, + { role: 'user', content: 'hello' }, + { role: 'assistant', content: 'hi' }, + { role: 'tool', content: '[name=search_web tool_call_id=call_1]\n{"ok":true}' }, + { role: 'user', content: 'latest' }, + ]); +}); diff --git a/webui/src/features/chatHistory/chatHistoryUtils.js b/webui/src/features/chatHistory/chatHistoryUtils.js index ae16ebc..6359c39 100644 --- a/webui/src/features/chatHistory/chatHistoryUtils.js +++ b/webui/src/features/chatHistory/chatHistoryUtils.js @@ -15,12 +15,24 @@ const CURRENT_INPUT_FILE_PROMPT = 'Continue from the latest state in the attache const LEGACY_CURRENT_INPUT_FILE_PROMPTS = new Set([ 'The current request and prior conversation context have already been provided. Answer the latest user request directly.', ]) +const HISTORY_TRANSCRIPT_TITLE = '# DS2API_HISTORY.txt' +const HISTORY_TRANSCRIPT_ENTRY_RE = /^===\s+\d+\.\s+([A-Z][A-Z_ -]*)\s+===\s*$/gm function isCurrentInputFilePrompt(value) { const text = String(value || '').trim() return text === CURRENT_INPUT_FILE_PROMPT || LEGACY_CURRENT_INPUT_FILE_PROMPTS.has(text) } +function normalizeHistoryRole(role) { + const normalized = String(role || '').trim().toLowerCase() + if (normalized === 'function') return 'tool' + if (normalized === 'developer') return 'system' + if (normalized === 'system' || normalized === 'user' || normalized === 'assistant' || normalized === 'tool') { + return normalized + } + return normalized || 'system' +} + export function formatDateTime(value, lang) { if (!value) return '-' try { @@ -221,11 +233,37 @@ export function parseStrictHistoryMessages(historyText) { return parsed } +export function parseTranscriptHistoryMessages(historyText) { + const rawText = String(historyText || '') + const titleIndex = rawText.indexOf(HISTORY_TRANSCRIPT_TITLE) + const transcript = titleIndex >= 0 ? rawText.slice(titleIndex) : rawText + const matches = [...transcript.matchAll(HISTORY_TRANSCRIPT_ENTRY_RE)] + if (!matches.length) return null + + const parsed = [] + for (let i = 0; i < matches.length; i += 1) { + const match = matches[i] + const next = matches[i + 1] + const role = normalizeHistoryRole(match[1]) + const start = (match.index || 0) + match[0].length + const end = next ? next.index : transcript.length + const content = transcript.slice(start, end).replace(/^\r?\n/, '').trim() + if (!content) continue + parsed.push({ role, content }) + } + + return parsed.length ? parsed : null +} + +export function parseHistoryMessages(historyText) { + return parseStrictHistoryMessages(historyText) || parseTranscriptHistoryMessages(historyText) +} + export function buildListModeMessages(item, t) { const liveMessages = Array.isArray(item?.messages) && item.messages.length > 0 ? item.messages : [{ role: 'user', content: item?.user_input || t('chatHistory.emptyUserInput') }] - const historyMessages = parseStrictHistoryMessages(item?.history_text) + const historyMessages = parseHistoryMessages(item?.history_text) if (!historyMessages?.length) { return { messages: liveMessages, historyMerged: false }