修复无法获得pdf的问题
This commit is contained in:
@@ -126,6 +126,10 @@ lecture-playing-zh = Playing: translation
|
||||
lecture-status-ready-step2 = Ready: { $count } paragraphs + note synced
|
||||
lecture-no-pdf = No PDF attachment on this item
|
||||
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-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.
|
||||
|
||||
@@ -126,6 +126,10 @@ lecture-playing-zh = 正在播放:中文讲解
|
||||
lecture-status-ready-step2 = 备课完成:{ $count } 段逐段精读可播放,笔记已同步
|
||||
lecture-no-pdf = 当前条目没有可用的 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-platform-hint-mac = macOS:MinerU 路径可在偏好设置 → ChatPapers → 语音伴读 中配置;留空则尝试 PATH 中的 mineru。
|
||||
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). */
|
||||
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 {
|
||||
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)
|
||||
|
||||
const unified = trimmed.replace(/\\/g, "/").replace(/\/+/g, "/");
|
||||
|
||||
const winMatch = unified.match(/^([A-Za-z]:)(\/.*)?$/);
|
||||
if (winMatch) {
|
||||
const drive = winMatch[1];
|
||||
const rest = (winMatch[2] || "")
|
||||
.split("/")
|
||||
.filter((part) => part && part !== ".")
|
||||
.join("\\");
|
||||
return `${drive}\\${rest}`.replace(/\\+$/, "");
|
||||
return rest ? `${drive}\\${rest}` : drive;
|
||||
}
|
||||
return joined
|
||||
.split("/")
|
||||
.filter((part) => part && part !== ".")
|
||||
.join("/")
|
||||
.replace(/\/+$/, "");
|
||||
|
||||
const absolute = unified.startsWith("/");
|
||||
const parts = unified.split("/").filter((part) => part && part !== ".");
|
||||
let result = parts.join("/");
|
||||
if (absolute) result = `/${result}`;
|
||||
return result.replace(/\/+$/, "") || (absolute ? "/" : "");
|
||||
}
|
||||
|
||||
/** Safe single path segment for file or directory names (cross-platform). */
|
||||
|
||||
@@ -7,25 +7,10 @@ import {
|
||||
} from "../platform/paths";
|
||||
|
||||
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());
|
||||
const dir = (Zotero as any).Profile?.dir;
|
||||
if (typeof dir === "string" && dir.trim()) {
|
||||
return dir.trim();
|
||||
}
|
||||
|
||||
throw new Error("Cannot resolve Zotero profile directory");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { findPdfAttachment } from "../../../pdf/extractor";
|
||||
import { getString } from "../../../../utils/locale";
|
||||
import type { PaperAttachmentContext } from "../../domain/types";
|
||||
import {
|
||||
buildCacheKey,
|
||||
@@ -6,12 +7,93 @@ import {
|
||||
paperIdFromCacheKey,
|
||||
} from "../pdf/fileHash";
|
||||
|
||||
function attachmentFilePath(attachment: Zotero.Item): string {
|
||||
const path = attachment.getFilePath?.() as string | false;
|
||||
if (!path) {
|
||||
throw new Error("PDF attachment has no local file path");
|
||||
async function attachmentFilePath(attachment: Zotero.Item): Promise<string> {
|
||||
const att = attachment as Zotero.Item & {
|
||||
getFilePath?: () => string | false;
|
||||
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(
|
||||
@@ -22,13 +104,22 @@ export async function resolvePaperAttachment(
|
||||
throw new Error("NO_PDF");
|
||||
}
|
||||
|
||||
const filePath = attachmentFilePath(attachment);
|
||||
const exists = await IOUtils.exists(filePath);
|
||||
if (!exists) {
|
||||
throw new Error("PDF_FILE_MISSING");
|
||||
let filePath: string;
|
||||
try {
|
||||
filePath = await attachmentFilePath(attachment);
|
||||
} 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 cacheKey = buildCacheKey(attachmentId, fileHash);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import { TtsError } from "../infrastructure/tts/types";
|
||||
import {
|
||||
getRegularItem,
|
||||
resolvePaperAttachment,
|
||||
formatLectureAttachmentError,
|
||||
} from "../infrastructure/zotero/adapter";
|
||||
import { paperIdFromCacheKey } from "../infrastructure/pdf/fileHash";
|
||||
import { chunkById } from "../infrastructure/pdf/chunker";
|
||||
@@ -517,11 +518,8 @@ export class LecturePaneView {
|
||||
}
|
||||
await this.refreshPlayer();
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e instanceof Error && e.message === "NO_PDF"
|
||||
? getString("lecture-no-pdf")
|
||||
: getString("lecture-attach-error");
|
||||
this.setStatus("failed", msg);
|
||||
ztoolkit.log("[ChatPapers:Lecture] refreshFromStore failed", e);
|
||||
this.setStatus("failed", formatLectureAttachmentError(e));
|
||||
this.actionBtn.disabled = true;
|
||||
this.ttsTestBtn.disabled = true;
|
||||
}
|
||||
@@ -1358,7 +1356,13 @@ export class LecturePaneView {
|
||||
this.setStatus(this.paper.lectureStatus, msg);
|
||||
await this.refreshPlayer();
|
||||
} 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);
|
||||
} finally {
|
||||
this.preparing = false;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ChatMessage } from "../llm/types";
|
||||
|
||||
function sessionDir(): string {
|
||||
const profile = (Zotero as any).Profile?.dir || (Zotero as any).getProfileDirectory?.()?.path;
|
||||
if (!profile) {
|
||||
const profile = (Zotero as any).Profile?.dir;
|
||||
if (!profile || typeof profile !== "string") {
|
||||
throw new Error("Cannot resolve Zotero profile directory");
|
||||
}
|
||||
return PathUtils.join(profile, "chatpapers", "sessions");
|
||||
|
||||
Vendored
+4
@@ -44,6 +44,7 @@ export type FluentMessageId =
|
||||
| 'itemmenu-compare'
|
||||
| 'itemmenu-label'
|
||||
| 'lecture-attach-error'
|
||||
| 'lecture-attach-error-detail'
|
||||
| 'lecture-beat-detail-empty'
|
||||
| 'lecture-beat-label'
|
||||
| 'lecture-beat-list-hint'
|
||||
@@ -71,6 +72,9 @@ export type FluentMessageId =
|
||||
| 'lecture-para-play'
|
||||
| 'lecture-para-translation'
|
||||
| 'lecture-paragraph-list-title'
|
||||
| 'lecture-pdf-file-missing'
|
||||
| 'lecture-pdf-not-local'
|
||||
| 'lecture-pdf-read-failed'
|
||||
| 'lecture-pdf-sync-hint'
|
||||
| 'lecture-platform-hint-mac'
|
||||
| 'lecture-platform-hint-other'
|
||||
|
||||
Reference in New Issue
Block a user