187b08c3e1
Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { randomUUID } from "crypto";
|
|
import {
|
|
HISTORY_MAX_ITEMS,
|
|
type CalcHistoryCreate,
|
|
type CalcHistoryEntry,
|
|
} from "@/lib/history/types";
|
|
|
|
function getDataDir(): string {
|
|
const configured = process.env.HISTORY_DATA_DIR?.trim();
|
|
if (configured) {
|
|
return configured;
|
|
}
|
|
return path.join(process.cwd(), "data", "history");
|
|
}
|
|
|
|
export function historyStoreErrorMessage(err: unknown): string {
|
|
if (!(err instanceof Error)) {
|
|
return "保存测算历史失败";
|
|
}
|
|
const code = (err as NodeJS.ErrnoException).code;
|
|
const dir = getDataDir();
|
|
if (code === "EACCES" || code === "EPERM") {
|
|
return `测算历史目录无写入权限(${dir}),请检查 HISTORY_DATA_DIR 或目录权限`;
|
|
}
|
|
if (code === "ENOENT") {
|
|
return `测算历史目录不存在(${dir}),请创建目录或配置 HISTORY_DATA_DIR`;
|
|
}
|
|
if (code === "ENOSPC") {
|
|
return "磁盘空间不足,无法保存测算历史";
|
|
}
|
|
return `保存测算历史失败:${err.message}`;
|
|
}
|
|
|
|
function sanitizeUsername(username: string): string {
|
|
const safe = username.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
return safe || "guest";
|
|
}
|
|
|
|
function userFile(username: string): string {
|
|
return path.join(getDataDir(), `${sanitizeUsername(username)}.json`);
|
|
}
|
|
|
|
async function ensureDir(): Promise<void> {
|
|
await fs.mkdir(getDataDir(), { recursive: true });
|
|
}
|
|
|
|
async function readUserHistory(username: string): Promise<CalcHistoryEntry[]> {
|
|
await ensureDir();
|
|
try {
|
|
const raw = await fs.readFile(userFile(username), "utf-8");
|
|
const parsed = JSON.parse(raw) as CalcHistoryEntry[];
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function writeUserHistory(
|
|
username: string,
|
|
entries: CalcHistoryEntry[],
|
|
): Promise<void> {
|
|
await ensureDir();
|
|
const file = userFile(username);
|
|
const tmp = `${file}.tmp`;
|
|
await fs.writeFile(tmp, JSON.stringify(entries, null, 2), "utf-8");
|
|
await fs.rename(tmp, file);
|
|
}
|
|
|
|
export async function listHistoryEntries(
|
|
username: string,
|
|
): Promise<CalcHistoryEntry[]> {
|
|
return readUserHistory(username);
|
|
}
|
|
|
|
export async function addHistoryEntry(
|
|
username: string,
|
|
entry: CalcHistoryCreate,
|
|
): Promise<CalcHistoryEntry> {
|
|
const full: CalcHistoryEntry = {
|
|
...entry,
|
|
id: randomUUID(),
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
const list = [full, ...(await readUserHistory(username))].slice(
|
|
0,
|
|
HISTORY_MAX_ITEMS,
|
|
);
|
|
await writeUserHistory(username, list);
|
|
return full;
|
|
}
|
|
|
|
export async function deleteHistoryEntry(
|
|
username: string,
|
|
id: string,
|
|
): Promise<boolean> {
|
|
const current = await readUserHistory(username);
|
|
const next = current.filter((entry) => entry.id !== id);
|
|
if (next.length === current.length) {
|
|
return false;
|
|
}
|
|
await writeUserHistory(username, next);
|
|
return true;
|
|
}
|