Merge pull request #313 from Adonis0123/feature/239-history-export

feat(extension): export history sessions as json
This commit is contained in:
Simon
2026-03-20 17:01:59 +08:00
committed by GitHub
2 changed files with 76 additions and 1 deletions

View File

@@ -1,8 +1,9 @@
import { ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react' import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db' import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
import { downloadHistoryExport } from '@/lib/history-export'
function timeAgo(ts: number): string { function timeAgo(ts: number): string {
const seconds = Math.floor((Date.now() - ts) / 1000) const seconds = Math.floor((Date.now() - ts) / 1000)
@@ -43,6 +44,11 @@ export function HistoryList({
setSessions((prev) => prev.filter((s) => s.id !== id)) setSessions((prev) => prev.filter((s) => s.id !== id))
} }
const handleExport = (e: React.MouseEvent, session: SessionRecord) => {
e.stopPropagation()
downloadHistoryExport(session.task, session.createdAt, session.history)
}
const handleRerun = (e: React.MouseEvent, task: string) => { const handleRerun = (e: React.MouseEvent, task: string) => {
e.stopPropagation() e.stopPropagation()
onRerun(task) onRerun(task)
@@ -124,6 +130,15 @@ export function HistoryList({
> >
<RotateCcw className="size-3" /> <RotateCcw className="size-3" />
</button> </button>
<button
type="button"
onClick={(e) => handleExport(e, session)}
className="p-1 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
title="Export history JSON"
aria-label={`Export history for ${session.task}`}
>
<ArrowDownToLine className="size-3" />
</button>
<button <button
type="button" type="button"
onClick={(e) => handleDelete(e, session.id)} onClick={(e) => handleDelete(e, session.id)}

View File

@@ -0,0 +1,60 @@
import type { HistoricalEvent } from '@page-agent/core'
const EXPORT_FILE_PREFIX = 'page-agent-history'
const MAX_TASK_SLUG_LENGTH = 40
export function serializeHistoryExport(history: HistoricalEvent[]): string {
return `${JSON.stringify(history, null, 2)}\n`
}
export function buildHistoryExportFilename(task: string, createdAt: number): string {
const taskSlug = sanitizeTaskForFilename(task)
const timestamp = formatTimestampForFilename(createdAt)
return taskSlug
? `${EXPORT_FILE_PREFIX}-${taskSlug}-${timestamp}.json`
: `${EXPORT_FILE_PREFIX}-${timestamp}.json`
}
export function downloadHistoryExport(
task: string,
createdAt: number,
history: HistoricalEvent[]
): void {
const filename = buildHistoryExportFilename(task, createdAt)
const content = serializeHistoryExport(history)
const blob = new Blob([content], { type: 'application/json;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
function sanitizeTaskForFilename(task: string): string {
return task
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, MAX_TASK_SLUG_LENGTH)
}
function formatTimestampForFilename(createdAt: number): string {
const date = new Date(createdAt)
const year = date.getFullYear()
const month = pad(date.getMonth() + 1)
const day = pad(date.getDate())
const hours = pad(date.getHours())
const minutes = pad(date.getMinutes())
const seconds = pad(date.getSeconds())
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`
}
function pad(value: number): string {
return value.toString().padStart(2, '0')
}