修正windows下无法保存的问题

This commit is contained in:
yhy
2026-08-30 08:46:04 +08:00
parent 3702527613
commit 455059eebe
12 changed files with 147 additions and 32 deletions
@@ -22,6 +22,43 @@ export function joinPath(...parts: string[]): string {
return PathUtils.join(...parts.filter(Boolean));
}
/** Normalize a filesystem path for the current OS. */
export function normalizeStoragePath(input: string): string {
const trimmed = (input || "").trim();
if (!trimmed) return "";
const joined = trimmed.replace(/[\\/]+/g, "/");
if (/^[A-Za-z]:\//.test(joined)) {
const drive = joined.slice(0, 2);
const rest = joined
.slice(2)
.split("/")
.filter((part) => part && part !== ".")
.join("\\");
return `${drive}\\${rest}`.replace(/\\+$/, "");
}
return joined
.split("/")
.filter((part) => part && part !== ".")
.join("/")
.replace(/\/+$/, "");
}
/** Safe single path segment for file or directory names (cross-platform). */
export function sanitizePathSegment(input: string, maxLen = 120): string {
const cleaned = String(input || "")
.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_")
.replace(/[.\s]+$/, "")
.trim();
if (!cleaned) return "_";
if (cleaned.length <= maxLen) return cleaned;
let hash = 0;
for (let i = 0; i < input.length; i++) {
hash = (hash * 31 + input.charCodeAt(i)) | 0;
}
const tail = Math.abs(hash).toString(36);
return `${cleaned.slice(0, Math.max(1, maxLen - tail.length - 1))}_${tail}`;
}
/** Convert local file path to file:// URL for <audio src>. */
export function pathToFileUrl(filePath: string): string {
const normalized = filePath.replace(/\\/g, "/");
@@ -1,7 +1,9 @@
import type { BeatType, SummaryJson } from "../../domain/types";
import type { TextChunk } from "../pdf/chunker";
import { joinPath } from "../platform/paths";
import { ensureLectureDirs, lectureDataRoot } from "./paths";
import {
ensureLectureDirs,
lectureDataFilePath,
} from "./paths";
export interface StoredBeat {
id: string;
@@ -73,8 +75,7 @@ export interface LectureData {
}
function lectureDataPath(cacheKey: string): string {
const safe = cacheKey.replace(/[^a-zA-Z0-9_|.-]/g, "_");
return joinPath(lectureDataRoot(), "data", `${safe}.json`);
return lectureDataFilePath(cacheKey);
}
export async function loadLectureData(
@@ -94,11 +95,6 @@ export async function loadLectureData(
export async function saveLectureData(data: LectureData): Promise<void> {
await ensureLectureDirs();
const dir = joinPath(lectureDataRoot(), "data");
await IOUtils.makeDirectory(dir, {
createAncestors: true,
ignoreExisting: true,
});
data.updatedAt = Date.now();
await IOUtils.writeUTF8(
lectureDataPath(data.cacheKey),
@@ -1,18 +1,50 @@
import { joinPath } from "../platform/paths";
import { getPref } from "../../../../utils/prefs";
import { getString } from "../../../../utils/locale";
import {
joinPath,
normalizeStoragePath,
sanitizePathSegment,
} from "../platform/paths";
function profileDir(): string {
const profile =
(Zotero as any).Profile?.dir ||
(Zotero as any).getProfileDirectory?.()?.path;
if (!profile) {
throw new Error("Cannot resolve Zotero profile directory");
function resolveProfileDir(): string {
const z = Zotero as any;
const candidates: unknown[] = [
z.Profile?.dir,
typeof z.getProfileDirectory === "function" ? z.getProfileDirectory() : undefined,
];
for (const candidate of candidates) {
const path =
typeof candidate === "string"
? candidate
: candidate &&
typeof candidate === "object" &&
"path" in candidate &&
typeof (candidate as { path: unknown }).path === "string"
? (candidate as { path: string }).path
: "";
if (path.trim()) return normalizeStoragePath(path.trim());
}
return profile;
throw new Error("Cannot resolve Zotero profile directory");
}
/** `{Profile}/chatpapers/lecture/` — voice lecture data root */
function customLectureDataDir(): string {
return String(getPref("lectureDataDir") || "").trim();
}
/** Map logical cacheKey (may contain `|`) to a filesystem-safe directory/file stem. */
export function cacheKeyDirName(cacheKey: string): string {
return sanitizePathSegment(cacheKey, 100);
}
/** Resolved voice-lecture data root (custom pref or `{Profile}/chatpapers/lecture/`). */
export function lectureDataRoot(): string {
return joinPath(profileDir(), "chatpapers", "lecture");
const custom = customLectureDataDir();
if (custom) {
return normalizeStoragePath(custom);
}
return joinPath(resolveProfileDir(), "chatpapers", "lecture");
}
export function lectureDbPath(): string {
@@ -24,12 +56,16 @@ export function lectureIndexPath(): string {
return joinPath(lectureDataRoot(), "cache-index.json");
}
export function lectureDataFilePath(cacheKey: string): string {
return joinPath(lectureDataRoot(), "data", `${cacheKeyDirName(cacheKey)}.json`);
}
export function audioRootDir(): string {
return joinPath(lectureDataRoot(), "audio");
}
export function audioDirForPaper(cacheKey: string): string {
return joinPath(audioRootDir(), cacheKey);
return joinPath(audioRootDir(), cacheKeyDirName(cacheKey));
}
export function audioFilePath(
@@ -37,30 +73,41 @@ export function audioFilePath(
unitId: string,
ext: "wav" | "mp3" = "wav",
): string {
return joinPath(audioDirForPaper(cacheKey), `${unitId}.${ext}`);
return joinPath(
audioDirForPaper(cacheKey),
`${sanitizePathSegment(unitId, 80)}.${ext}`,
);
}
export function lectureSamplePath(name: string): string {
return joinPath(lectureDataRoot(), "samples", name);
return joinPath(lectureDataRoot(), "samples", sanitizePathSegment(name, 80));
}
export async function ensureSampleDir(): Promise<string> {
const dir = joinPath(lectureDataRoot(), "samples");
await IOUtils.makeDirectory(dir, {
await ensureDir(dir);
return dir;
}
async function ensureDir(path: string): Promise<void> {
await IOUtils.makeDirectory(path, {
createAncestors: true,
ignoreExisting: true,
});
return dir;
}
export async function ensureLectureDirs(): Promise<void> {
const root = lectureDataRoot();
await IOUtils.makeDirectory(root, {
createAncestors: true,
ignoreExisting: true,
});
await IOUtils.makeDirectory(audioRootDir(), {
createAncestors: true,
ignoreExisting: true,
});
try {
await ensureDir(root);
await ensureDir(joinPath(root, "data"));
await ensureDir(audioRootDir());
} catch (e) {
const detail = e instanceof Error ? e.message : String(e);
throw new Error(
getString("lecture-storage-write-failed", {
args: { path: root, detail },
}),
);
}
}
+7
View File
@@ -25,6 +25,9 @@ export function registerLecturePrefsScripts(win: Window) {
const mineruPathInput = doc.getElementById(
"chatpapers-pref-mineru-path",
) as HTMLInputElement | null;
const lectureDataDirInput = doc.getElementById(
"chatpapers-pref-lecture-data-dir",
) as HTMLInputElement | null;
const hint = doc.getElementById("chatpapers-pref-tts-hint");
const piperBox = doc.getElementById("chatpapers-pref-piper-box");
@@ -83,5 +86,9 @@ export function registerLecturePrefsScripts(win: Window) {
setPref("mineruPath", mineruPathInput.value);
});
lectureDataDirInput?.addEventListener("change", () => {
setPref("lectureDataDir", lectureDataDirInput.value.trim());
});
updateHint();
}