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') +}