修复无法获得pdf的问题

This commit is contained in:
yhy
2026-08-30 09:02:11 +08:00
parent 455059eebe
commit af2ee38443
8 changed files with 151 additions and 49 deletions
+4
View File
@@ -126,6 +126,10 @@ lecture-playing-zh = Playing: translation
lecture-status-ready-step2 = Ready: { $count } paragraphs + note synced lecture-status-ready-step2 = Ready: { $count } paragraphs + note synced
lecture-no-pdf = No PDF attachment on this item lecture-no-pdf = No PDF attachment on this item
lecture-attach-error = Cannot read PDF attachment lecture-attach-error = Cannot read PDF attachment
lecture-attach-error-detail = Cannot read PDF attachment: { $detail }
lecture-pdf-not-local = PDF is not available locally. Open the PDF in Zotero first, or ensure the attachment is synced or linked to a local file.
lecture-pdf-file-missing = Local PDF file is missing or the path is invalid. Check Zotero storage settings or re-link the file.
lecture-pdf-read-failed = Cannot read PDF file contents. Check that the file is valid and readable.
lecture-mount-error = Failed to load voice lecture pane lecture-mount-error = Failed to load voice lecture pane
lecture-platform-hint-mac = macOS: set MinerU path under Preferences → ChatPapers → Voice lecture, or leave empty to use mineru on PATH. lecture-platform-hint-mac = macOS: set MinerU path under Preferences → ChatPapers → Voice lecture, or leave empty to use mineru on PATH.
lecture-platform-hint-win = Windows: set the full path to mineru.exe under Preferences → ChatPapers → Voice lecture. lecture-platform-hint-win = Windows: set the full path to mineru.exe under Preferences → ChatPapers → Voice lecture.
+4
View File
@@ -126,6 +126,10 @@ lecture-playing-zh = 正在播放:中文讲解
lecture-status-ready-step2 = 备课完成:{ $count } 段逐段精读可播放,笔记已同步 lecture-status-ready-step2 = 备课完成:{ $count } 段逐段精读可播放,笔记已同步
lecture-no-pdf = 当前条目没有可用的 PDF 附件 lecture-no-pdf = 当前条目没有可用的 PDF 附件
lecture-attach-error = 无法读取 PDF 附件 lecture-attach-error = 无法读取 PDF 附件
lecture-attach-error-detail = 无法读取 PDF 附件:{ $detail }
lecture-pdf-not-local = PDF 未下载到本地。请先在 Zotero 中双击打开该 PDF,或确认附件已同步/已链接到本机文件。
lecture-pdf-file-missing = PDF 本地文件不存在或路径无效。请检查 Zotero 存储设置,或重新关联 PDF 文件。
lecture-pdf-read-failed = 无法读取 PDF 文件内容。请确认文件未损坏,且 Zotero 有读取权限。
lecture-mount-error = 语音伴读面板加载失败 lecture-mount-error = 语音伴读面板加载失败
lecture-platform-hint-mac = macOSMinerU 路径可在偏好设置 → ChatPapers → 语音伴读 中配置;留空则尝试 PATH 中的 mineru。 lecture-platform-hint-mac = macOSMinerU 路径可在偏好设置 → ChatPapers → 语音伴读 中配置;留空则尝试 PATH 中的 mineru。
lecture-platform-hint-win = Windows:请在偏好设置中填写 MinerU 可执行文件完整路径(如 mineru.exe)。 lecture-platform-hint-win = Windows:请在偏好设置中填写 MinerU 可执行文件完整路径(如 mineru.exe)。
@@ -19,28 +19,38 @@ export function normalizeExecutablePath(input: string): string {
/** Join path segments using Zotero PathUtils (cross-platform). */ /** Join path segments using Zotero PathUtils (cross-platform). */
export function joinPath(...parts: string[]): string { export function joinPath(...parts: string[]): string {
return PathUtils.join(...parts.filter(Boolean)); const filtered = parts.filter(Boolean);
if (!filtered.length) return "";
try {
return PathUtils.join(...filtered);
} catch (e) {
ztoolkit.log("[ChatPapers] PathUtils.join failed", filtered, e);
throw e;
}
} }
/** Normalize a filesystem path for the current OS. */ /** Normalize user-provided storage directory paths (prefs). */
export function normalizeStoragePath(input: string): string { export function normalizeStoragePath(input: string): string {
const trimmed = (input || "").trim(); const trimmed = (input || "").trim();
if (!trimmed) return ""; if (!trimmed) return "";
const joined = trimmed.replace(/[\\/]+/g, "/");
if (/^[A-Za-z]:\//.test(joined)) { const unified = trimmed.replace(/\\/g, "/").replace(/\/+/g, "/");
const drive = joined.slice(0, 2);
const rest = joined const winMatch = unified.match(/^([A-Za-z]:)(\/.*)?$/);
.slice(2) if (winMatch) {
const drive = winMatch[1];
const rest = (winMatch[2] || "")
.split("/") .split("/")
.filter((part) => part && part !== ".") .filter((part) => part && part !== ".")
.join("\\"); .join("\\");
return `${drive}\\${rest}`.replace(/\\+$/, ""); return rest ? `${drive}\\${rest}` : drive;
} }
return joined
.split("/") const absolute = unified.startsWith("/");
.filter((part) => part && part !== ".") const parts = unified.split("/").filter((part) => part && part !== ".");
.join("/") let result = parts.join("/");
.replace(/\/+$/, ""); if (absolute) result = `/${result}`;
return result.replace(/\/+$/, "") || (absolute ? "/" : "");
} }
/** Safe single path segment for file or directory names (cross-platform). */ /** Safe single path segment for file or directory names (cross-platform). */
@@ -7,25 +7,10 @@ import {
} from "../platform/paths"; } from "../platform/paths";
function resolveProfileDir(): string { function resolveProfileDir(): string {
const z = Zotero as any; const dir = (Zotero as any).Profile?.dir;
const candidates: unknown[] = [ if (typeof dir === "string" && dir.trim()) {
z.Profile?.dir, return dir.trim();
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());
} }
throw new Error("Cannot resolve Zotero profile directory"); throw new Error("Cannot resolve Zotero profile directory");
} }
@@ -1,4 +1,5 @@
import { findPdfAttachment } from "../../../pdf/extractor"; import { findPdfAttachment } from "../../../pdf/extractor";
import { getString } from "../../../../utils/locale";
import type { PaperAttachmentContext } from "../../domain/types"; import type { PaperAttachmentContext } from "../../domain/types";
import { import {
buildCacheKey, buildCacheKey,
@@ -6,12 +7,93 @@ import {
paperIdFromCacheKey, paperIdFromCacheKey,
} from "../pdf/fileHash"; } from "../pdf/fileHash";
function attachmentFilePath(attachment: Zotero.Item): string { async function attachmentFilePath(attachment: Zotero.Item): Promise<string> {
const path = attachment.getFilePath?.() as string | false; const att = attachment as Zotero.Item & {
if (!path) { getFilePath?: () => string | false;
throw new Error("PDF attachment has no local file path"); getFilePathAsync?: () => Promise<string | false>;
};
if (typeof att.getFilePathAsync === "function") {
try {
const asyncPath = await att.getFilePathAsync();
if (asyncPath && typeof asyncPath === "string") {
return asyncPath;
}
} catch (e) {
ztoolkit.log("[ChatPapers:Lecture] getFilePathAsync failed", e);
}
}
const syncPath = att.getFilePath?.();
if (syncPath && typeof syncPath === "string") {
return syncPath;
}
const zAtt = (Zotero as any).Attachments;
if (zAtt?.getFilePath) {
try {
const path = zAtt.getFilePath(attachment);
if (path && typeof path === "string") return path;
} catch (e) {
ztoolkit.log("[ChatPapers:Lecture] Attachments.getFilePath failed", e);
}
}
throw new Error("PDF_NOT_LOCAL");
}
async function attachmentFileHash(
attachment: Zotero.Item,
filePath: string,
): Promise<string> {
try {
return await computeFileHash(filePath);
} catch (e) {
ztoolkit.log("[ChatPapers:Lecture] computeFileHash failed", filePath, e);
const att = attachment as Zotero.Item & {
attachmentHash?: Promise<string | undefined>;
};
if (att.attachmentHash) {
try {
const md5 = await att.attachmentHash;
if (md5 && typeof md5 === "string") {
return `md5:${md5}`;
}
} catch (hashErr) {
ztoolkit.log("[ChatPapers:Lecture] attachmentHash fallback failed", hashErr);
}
}
throw new Error("PDF_READ_FAILED");
}
}
async function ensureAttachmentFileReadable(filePath: string): Promise<void> {
try {
if (await IOUtils.exists(filePath)) return;
} catch (e) {
ztoolkit.log("[ChatPapers:Lecture] IOUtils.exists failed", filePath, e);
}
throw new Error("PDF_FILE_MISSING");
}
export function formatLectureAttachmentError(error: unknown): string {
if (!(error instanceof Error)) {
return getString("lecture-attach-error");
}
switch (error.message) {
case "NO_PDF":
return getString("lecture-no-pdf");
case "PDF_NOT_LOCAL":
return getString("lecture-pdf-not-local");
case "PDF_FILE_MISSING":
return getString("lecture-pdf-file-missing");
case "PDF_READ_FAILED":
return getString("lecture-pdf-read-failed");
default:
return getString("lecture-attach-error-detail", {
args: { detail: error.message },
});
} }
return path;
} }
export async function resolvePaperAttachment( export async function resolvePaperAttachment(
@@ -22,13 +104,22 @@ export async function resolvePaperAttachment(
throw new Error("NO_PDF"); throw new Error("NO_PDF");
} }
const filePath = attachmentFilePath(attachment); let filePath: string;
const exists = await IOUtils.exists(filePath); try {
if (!exists) { filePath = await attachmentFilePath(attachment);
throw new Error("PDF_FILE_MISSING"); } catch (e) {
if (e instanceof Error && e.message === "PDF_NOT_LOCAL") throw e;
ztoolkit.log("[ChatPapers:Lecture] resolve attachment path failed", {
attachmentId: attachment.id,
linkMode: (attachment as any).attachmentLinkMode,
e,
});
throw new Error("PDF_NOT_LOCAL");
} }
const fileHash = await computeFileHash(filePath); await ensureAttachmentFileReadable(filePath);
const fileHash = await attachmentFileHash(attachment, filePath);
const attachmentId = String(attachment.id); const attachmentId = String(attachment.id);
const cacheKey = buildCacheKey(attachmentId, fileHash); const cacheKey = buildCacheKey(attachmentId, fileHash);
+10 -6
View File
@@ -38,6 +38,7 @@ import { TtsError } from "../infrastructure/tts/types";
import { import {
getRegularItem, getRegularItem,
resolvePaperAttachment, resolvePaperAttachment,
formatLectureAttachmentError,
} from "../infrastructure/zotero/adapter"; } from "../infrastructure/zotero/adapter";
import { paperIdFromCacheKey } from "../infrastructure/pdf/fileHash"; import { paperIdFromCacheKey } from "../infrastructure/pdf/fileHash";
import { chunkById } from "../infrastructure/pdf/chunker"; import { chunkById } from "../infrastructure/pdf/chunker";
@@ -517,11 +518,8 @@ export class LecturePaneView {
} }
await this.refreshPlayer(); await this.refreshPlayer();
} catch (e) { } catch (e) {
const msg = ztoolkit.log("[ChatPapers:Lecture] refreshFromStore failed", e);
e instanceof Error && e.message === "NO_PDF" this.setStatus("failed", formatLectureAttachmentError(e));
? getString("lecture-no-pdf")
: getString("lecture-attach-error");
this.setStatus("failed", msg);
this.actionBtn.disabled = true; this.actionBtn.disabled = true;
this.ttsTestBtn.disabled = true; this.ttsTestBtn.disabled = true;
} }
@@ -1358,7 +1356,13 @@ export class LecturePaneView {
this.setStatus(this.paper.lectureStatus, msg); this.setStatus(this.paper.lectureStatus, msg);
await this.refreshPlayer(); await this.refreshPlayer();
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); ztoolkit.log("[ChatPapers:Lecture] onStartPrepare failed", e);
const msg =
e instanceof Error && e.message.startsWith("PDF_")
? formatLectureAttachmentError(e)
: e instanceof Error
? e.message
: String(e);
this.setStatus("failed", msg); this.setStatus("failed", msg);
} finally { } finally {
this.preparing = false; this.preparing = false;
+2 -2
View File
@@ -1,8 +1,8 @@
import type { ChatMessage } from "../llm/types"; import type { ChatMessage } from "../llm/types";
function sessionDir(): string { function sessionDir(): string {
const profile = (Zotero as any).Profile?.dir || (Zotero as any).getProfileDirectory?.()?.path; const profile = (Zotero as any).Profile?.dir;
if (!profile) { if (!profile || typeof profile !== "string") {
throw new Error("Cannot resolve Zotero profile directory"); throw new Error("Cannot resolve Zotero profile directory");
} }
return PathUtils.join(profile, "chatpapers", "sessions"); return PathUtils.join(profile, "chatpapers", "sessions");
+4
View File
@@ -44,6 +44,7 @@ export type FluentMessageId =
| 'itemmenu-compare' | 'itemmenu-compare'
| 'itemmenu-label' | 'itemmenu-label'
| 'lecture-attach-error' | 'lecture-attach-error'
| 'lecture-attach-error-detail'
| 'lecture-beat-detail-empty' | 'lecture-beat-detail-empty'
| 'lecture-beat-label' | 'lecture-beat-label'
| 'lecture-beat-list-hint' | 'lecture-beat-list-hint'
@@ -71,6 +72,9 @@ export type FluentMessageId =
| 'lecture-para-play' | 'lecture-para-play'
| 'lecture-para-translation' | 'lecture-para-translation'
| 'lecture-paragraph-list-title' | 'lecture-paragraph-list-title'
| 'lecture-pdf-file-missing'
| 'lecture-pdf-not-local'
| 'lecture-pdf-read-failed'
| 'lecture-pdf-sync-hint' | 'lecture-pdf-sync-hint'
| 'lecture-platform-hint-mac' | 'lecture-platform-hint-mac'
| 'lecture-platform-hint-other' | 'lecture-platform-hint-other'