Просмотр исходного кода

fix: address review feedback on usage logs export

- Replace hardcoded Chinese error strings with English messages
- Add 5-minute polling timeout to prevent UI hanging on stuck jobs
- Check Redis CSV write result and fail job if persist fails
- Add clarifying comment on setTimeout for self-hosted server

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
ding113 2 недель назад
Родитель
Сommit
802f05122b

+ 27 - 15
src/actions/usage-logs.ts

@@ -246,7 +246,17 @@ async function runUsageLogsExportJob(
       return;
     }
 
-    await usageLogsExportCsvStore.set(usageLogsExportCsvKey(jobId), csv);
+    const csvStored = await usageLogsExportCsvStore.set(usageLogsExportCsvKey(jobId), csv);
+    if (!csvStored) {
+      await usageLogsExportStatusStore.set(jobId, {
+        ...currentJob,
+        status: "failed",
+        progressPercent: 0,
+        error: "Failed to persist CSV to Redis",
+      });
+      return;
+    }
+
     await usageLogsExportStatusStore.set(jobId, {
       ...currentJob,
       status: "completed",
@@ -254,7 +264,7 @@ async function runUsageLogsExportJob(
       error: undefined,
     });
   } catch (error) {
-    logger.error("生成使用日志导出任务失败:", error);
+    logger.error("Failed to run usage logs export job:", error);
     const currentJob = await usageLogsExportStatusStore.get(jobId);
     if (!currentJob) {
       return;
@@ -264,7 +274,7 @@ async function runUsageLogsExportJob(
       ...currentJob,
       status: "failed",
       progressPercent: 0,
-      error: error instanceof Error ? error.message : "导出使用日志失败",
+      error: error instanceof Error ? error.message : "Export failed",
     });
   }
 }
@@ -340,17 +350,19 @@ export async function startUsageLogsExport(
     });
 
     if (!stored) {
-      return { ok: false, error: "导出任务初始化失败" };
+      return { ok: false, error: "Export job initialization failed" };
     }
 
+    // Defer to next tick so the action returns the jobId immediately.
+    // Safe for self-hosted Bun server (long-lived process); NOT suitable for serverless.
     setTimeout(() => {
       void runUsageLogsExportJob(jobId, finalFilters);
     }, 0);
 
     return { ok: true, data: { jobId } };
   } catch (error) {
-    logger.error("启动使用日志导出任务失败:", error);
-    const message = error instanceof Error ? error.message : "启动导出任务失败";
+    logger.error("Failed to start usage logs export:", error);
+    const message = error instanceof Error ? error.message : "Failed to start export";
     return { ok: false, error: message };
   }
 }
@@ -366,13 +378,13 @@ export async function getUsageLogsExportStatus(
 
     const job = getUsageLogsExportJob(session, await usageLogsExportStatusStore.get(jobId), jobId);
     if (!job) {
-      return { ok: false, error: "导出任务不存在或已过期" };
+      return { ok: false, error: "Export job not found or expired" };
     }
 
     return { ok: true, data: toUsageLogsExportStatus(job) };
   } catch (error) {
-    logger.error("获取使用日志导出进度失败:", error);
-    const message = error instanceof Error ? error.message : "获取导出进度失败";
+    logger.error("Failed to get usage logs export status:", error);
+    const message = error instanceof Error ? error.message : "Failed to get export status";
     return { ok: false, error: message };
   }
 }
@@ -386,26 +398,26 @@ export async function downloadUsageLogsExport(jobId: string): Promise<ActionResu
 
     const job = getUsageLogsExportJob(session, await usageLogsExportStatusStore.get(jobId), jobId);
     if (!job) {
-      return { ok: false, error: "导出任务不存在或已过期" };
+      return { ok: false, error: "Export job not found or expired" };
     }
 
     if (job.status === "failed") {
-      return { ok: false, error: job.error || "导出使用日志失败" };
+      return { ok: false, error: job.error || "Export failed" };
     }
 
     if (job.status !== "completed") {
-      return { ok: false, error: "导出尚未完成" };
+      return { ok: false, error: "Export not yet completed" };
     }
 
     const csv = await usageLogsExportCsvStore.get(usageLogsExportCsvKey(jobId));
     if (!csv) {
-      return { ok: false, error: "导出文件不存在或已过期" };
+      return { ok: false, error: "Export file not found or expired" };
     }
 
     return { ok: true, data: csv };
   } catch (error) {
-    logger.error("下载使用日志导出文件失败:", error);
-    const message = error instanceof Error ? error.message : "下载导出文件失败";
+    logger.error("Failed to download usage logs export:", error);
+    const message = error instanceof Error ? error.message : "Failed to download export";
     return { ok: false, error: message };
   }
 }

+ 8 - 0
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx

@@ -203,12 +203,20 @@ export function UsageLogsFilters({
       }
 
       const jobId = startResult.data.jobId;
+      const EXPORT_TIMEOUT_MS = 5 * 60 * 1000;
+      const deadline = Date.now() + EXPORT_TIMEOUT_MS;
 
       while (true) {
         if (exportRunIdRef.current !== runId) {
           return;
         }
 
+        if (Date.now() > deadline) {
+          setExportStatus(null);
+          toast.error(t("logs.filters.exportError"));
+          return;
+        }
+
         const statusResult = await getUsageLogsExportStatus(jobId);
         if (exportRunIdRef.current !== runId) {
           return;