From bcc7dfea2d166ebe57d675e1581e62d183715374 Mon Sep 17 00:00:00 2001
From: adonis <513554676@qq.com>
Date: Thu, 19 Mar 2026 23:46:58 +0800
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(extension):=20export=20history?=
=?UTF-8?q?=20sessions=20as=20json?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../extension/src/components/HistoryList.tsx | 36 ++++++++---
packages/extension/src/lib/history-export.ts | 60 +++++++++++++++++++
2 files changed, 87 insertions(+), 9 deletions(-)
create mode 100644 packages/extension/src/lib/history-export.ts
diff --git a/packages/extension/src/components/HistoryList.tsx b/packages/extension/src/components/HistoryList.tsx
index c13bae2..e663f3d 100644
--- a/packages/extension/src/components/HistoryList.tsx
+++ b/packages/extension/src/components/HistoryList.tsx
@@ -1,7 +1,8 @@
-import { ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react'
+import { ArrowDownToLine, ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
+import { downloadHistoryExport } from '@/lib/history-export'
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
function timeAgo(ts: number): string {
@@ -41,6 +42,11 @@ export function HistoryList({
setSessions((prev) => prev.filter((s) => s.id !== id))
}
+ const handleExport = (e: React.MouseEvent, session: SessionRecord) => {
+ e.stopPropagation()
+ downloadHistoryExport(session.task, session.createdAt, session.history)
+ }
+
return (
{/* Header */}
@@ -103,14 +109,26 @@ export function HistoryList({
- {/* Delete */}
-
+
+
+
+
))}
diff --git a/packages/extension/src/lib/history-export.ts b/packages/extension/src/lib/history-export.ts
new file mode 100644
index 0000000..83e6ab5
--- /dev/null
+++ b/packages/extension/src/lib/history-export.ts
@@ -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')
+}