mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-06 17:35:30 +08:00
289 lines
11 KiB
JavaScript
289 lines
11 KiB
JavaScript
export const LIMIT_OPTIONS = [0, 10, 20, 50]
|
||
export const DISABLED_LIMIT = 0
|
||
export const MESSAGE_COLLAPSE_AT = 700
|
||
export const VIEW_MODE_KEY = 'ds2api_chat_history_view_mode'
|
||
|
||
const BEGIN_SENTENCE_MARKER = '<|begin▁of▁sentence|>'
|
||
const SYSTEM_MARKER = '<|System|>'
|
||
const USER_MARKER = '<|User|>'
|
||
const ASSISTANT_MARKER = '<|Assistant|>'
|
||
const TOOL_MARKER = '<|Tool|>'
|
||
const END_INSTRUCTIONS_MARKER = '<|end▁of▁instructions|>'
|
||
const END_SENTENCE_MARKER = '<|end▁of▁sentence|>'
|
||
const END_TOOL_RESULTS_MARKER = '<|end▁of▁toolresults|>'
|
||
const CURRENT_INPUT_FILE_PROMPT = '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.'
|
||
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 {
|
||
return new Intl.DateTimeFormat(lang === 'zh' ? 'zh-CN' : 'en-US', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
}).format(new Date(value))
|
||
} catch {
|
||
return '-'
|
||
}
|
||
}
|
||
|
||
export function formatElapsed(ms, t) {
|
||
if (!ms) return t('chatHistory.metaUnknown')
|
||
if (ms < 1000) return `${ms}ms`
|
||
return `${(ms / 1000).toFixed(ms < 10_000 ? 2 : 1)}s`
|
||
}
|
||
|
||
export function previewText(item) {
|
||
return item?.preview || item?.content || item?.reasoning_content || item?.error || item?.user_input || ''
|
||
}
|
||
|
||
export function statusTone(status) {
|
||
switch (status) {
|
||
case 'success':
|
||
return 'border-emerald-500/20 bg-emerald-500/10 text-emerald-600'
|
||
case 'error':
|
||
return 'border-destructive/20 bg-destructive/10 text-destructive'
|
||
case 'stopped':
|
||
return 'border-amber-500/20 bg-amber-500/10 text-amber-600'
|
||
default:
|
||
return 'border-border bg-secondary/60 text-muted-foreground'
|
||
}
|
||
}
|
||
|
||
export function downloadTextFile(filename, text) {
|
||
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
||
const url = URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = filename
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
URL.revokeObjectURL(url)
|
||
}
|
||
|
||
function fallbackCopyText(text) {
|
||
const textArea = document.createElement('textarea')
|
||
textArea.value = text
|
||
textArea.setAttribute('readonly', '')
|
||
textArea.style.position = 'fixed'
|
||
textArea.style.top = '-9999px'
|
||
textArea.style.left = '-9999px'
|
||
|
||
document.body.appendChild(textArea)
|
||
textArea.focus()
|
||
textArea.select()
|
||
|
||
let copied = false
|
||
try {
|
||
copied = document.execCommand('copy')
|
||
} finally {
|
||
document.body.removeChild(textArea)
|
||
}
|
||
|
||
if (!copied) {
|
||
throw new Error('copy failed')
|
||
}
|
||
}
|
||
|
||
export async function copyTextWithFallback(text) {
|
||
try {
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text)
|
||
return
|
||
}
|
||
} catch {
|
||
// Fall through to execCommand fallback.
|
||
}
|
||
fallbackCopyText(text)
|
||
}
|
||
|
||
function skipWhitespace(text, start) {
|
||
let cursor = start
|
||
while (cursor < text.length && /\s/.test(text[cursor])) {
|
||
cursor += 1
|
||
}
|
||
return cursor
|
||
}
|
||
|
||
export function parseStrictHistoryMessages(historyText) {
|
||
const rawText = String(historyText || '')
|
||
const beginIndex = rawText.indexOf(BEGIN_SENTENCE_MARKER)
|
||
if (beginIndex < 0) return null
|
||
|
||
const transcript = rawText.slice(beginIndex)
|
||
let cursor = BEGIN_SENTENCE_MARKER.length
|
||
const parsed = []
|
||
let expectedRole = null
|
||
let trailingAssistantPromptOnly = false
|
||
|
||
while (cursor < transcript.length) {
|
||
if (expectedRole === null) {
|
||
if (transcript.startsWith(SYSTEM_MARKER, cursor)) {
|
||
expectedRole = 'system'
|
||
} else if (transcript.startsWith(USER_MARKER, cursor)) {
|
||
expectedRole = 'user'
|
||
} else if (transcript.startsWith(ASSISTANT_MARKER, cursor)) {
|
||
expectedRole = 'assistant'
|
||
} else if (transcript.slice(cursor).trim() === '') {
|
||
break
|
||
} else {
|
||
return null
|
||
}
|
||
}
|
||
|
||
if (transcript.startsWith(SYSTEM_MARKER, cursor)) {
|
||
if (expectedRole !== 'system') return null
|
||
cursor += SYSTEM_MARKER.length
|
||
const nextInstructionsEnd = transcript.indexOf(END_INSTRUCTIONS_MARKER, cursor)
|
||
if (nextInstructionsEnd < 0) return null
|
||
parsed.push({ role: 'system', content: transcript.slice(cursor, nextInstructionsEnd) })
|
||
cursor = nextInstructionsEnd + END_INSTRUCTIONS_MARKER.length
|
||
expectedRole = 'user'
|
||
continue
|
||
}
|
||
|
||
if (transcript.startsWith(USER_MARKER, cursor)) {
|
||
if (expectedRole !== 'user' && expectedRole !== 'user_or_tool' && expectedRole !== 'assistant_or_user') return null
|
||
cursor += USER_MARKER.length
|
||
const nextAssistant = transcript.indexOf(ASSISTANT_MARKER, cursor)
|
||
const nextTool = transcript.indexOf(TOOL_MARKER, cursor)
|
||
const nextSentenceEnd = transcript.indexOf(END_SENTENCE_MARKER, cursor)
|
||
let nextRoleIndex = nextAssistant
|
||
if (nextRoleIndex < 0 || (nextTool >= 0 && nextTool < nextRoleIndex)) {
|
||
nextRoleIndex = nextTool
|
||
}
|
||
if (nextRoleIndex < 0) return null
|
||
if (nextSentenceEnd >= 0 && nextSentenceEnd < nextRoleIndex) {
|
||
const assistantStart = skipWhitespace(transcript, nextSentenceEnd + END_SENTENCE_MARKER.length)
|
||
if (!transcript.startsWith(ASSISTANT_MARKER, assistantStart)) return null
|
||
parsed.push({ role: 'user', content: transcript.slice(cursor, nextSentenceEnd) })
|
||
cursor = assistantStart
|
||
expectedRole = 'assistant'
|
||
continue
|
||
}
|
||
parsed.push({ role: 'user', content: transcript.slice(cursor, nextRoleIndex) })
|
||
if (transcript.startsWith(TOOL_MARKER, nextRoleIndex)) {
|
||
cursor = nextRoleIndex
|
||
expectedRole = 'tool'
|
||
continue
|
||
}
|
||
const assistantStart = nextRoleIndex + ASSISTANT_MARKER.length
|
||
if (transcript.indexOf(END_SENTENCE_MARKER, assistantStart) < 0) {
|
||
trailingAssistantPromptOnly = true
|
||
cursor = assistantStart
|
||
break
|
||
}
|
||
cursor = nextRoleIndex
|
||
expectedRole = 'assistant'
|
||
continue
|
||
}
|
||
|
||
if (transcript.startsWith(ASSISTANT_MARKER, cursor)) {
|
||
if (expectedRole !== 'assistant' && expectedRole !== 'assistant_or_user') return null
|
||
cursor += ASSISTANT_MARKER.length
|
||
const nextSentenceEnd = transcript.indexOf(END_SENTENCE_MARKER, cursor)
|
||
if (nextSentenceEnd < 0) return null
|
||
parsed.push({ role: 'assistant', content: transcript.slice(cursor, nextSentenceEnd) })
|
||
cursor = nextSentenceEnd + END_SENTENCE_MARKER.length
|
||
expectedRole = 'user_or_tool'
|
||
continue
|
||
}
|
||
|
||
if (transcript.startsWith(TOOL_MARKER, cursor)) {
|
||
if (expectedRole !== 'tool' && expectedRole !== 'user' && expectedRole !== 'user_or_tool') return null
|
||
cursor += TOOL_MARKER.length
|
||
const nextToolResultsEnd = transcript.indexOf(END_TOOL_RESULTS_MARKER, cursor)
|
||
if (nextToolResultsEnd < 0) return null
|
||
parsed.push({ role: 'tool', content: transcript.slice(cursor, nextToolResultsEnd) })
|
||
cursor = nextToolResultsEnd + END_TOOL_RESULTS_MARKER.length
|
||
expectedRole = 'assistant_or_user'
|
||
continue
|
||
}
|
||
|
||
if (parsed.length && (expectedRole === 'user' || expectedRole === 'user_or_tool' || expectedRole === 'assistant_or_user')) break
|
||
if (transcript.slice(cursor).trim() === '') break
|
||
return null
|
||
}
|
||
|
||
if (!parsed.length) return null
|
||
if (!trailingAssistantPromptOnly && parsed[parsed.length - 1]?.role !== 'assistant') return null
|
||
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 = parseHistoryMessages(item?.history_text)
|
||
|
||
if (!historyMessages?.length) {
|
||
return { messages: liveMessages, historyMerged: false }
|
||
}
|
||
|
||
const placeholderOnly = liveMessages.length === 1
|
||
&& String(liveMessages[0]?.role || '').trim().toLowerCase() === 'user'
|
||
&& isCurrentInputFilePrompt(liveMessages[0]?.content)
|
||
|
||
if (placeholderOnly) {
|
||
return { messages: historyMessages, historyMerged: true }
|
||
}
|
||
|
||
const insertAt = liveMessages.findIndex(message => {
|
||
const role = String(message?.role || '').trim().toLowerCase()
|
||
return role !== 'system' && role !== 'developer'
|
||
})
|
||
const mergedMessages = [...liveMessages]
|
||
mergedMessages.splice(insertAt < 0 ? mergedMessages.length : insertAt, 0, ...historyMessages)
|
||
|
||
return { messages: mergedMessages, historyMerged: true }
|
||
}
|