Add API key generator and Zotero script builder
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { PDF_KEY_PREFIX, generatePdfKey } from "@/lib/clean-pdf/api-key";
|
||||||
|
|
||||||
|
describe("generatePdfKey", () => {
|
||||||
|
it("формат sbpdf_<base64url>, достаточная длина", () => {
|
||||||
|
const key = generatePdfKey();
|
||||||
|
expect(key.startsWith(PDF_KEY_PREFIX)).toBe(true);
|
||||||
|
expect(key.length).toBeGreaterThanOrEqual(PDF_KEY_PREFIX.length + 32);
|
||||||
|
expect(key.slice(PDF_KEY_PREFIX.length)).toMatch(/^[A-Za-z0-9_-]+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("две генерации различаются", () => {
|
||||||
|
expect(generatePdfKey()).not.toBe(generatePdfKey());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildZoteroScript } from "@/lib/clean-pdf/zotero-script";
|
||||||
|
|
||||||
|
describe("buildZoteroScript", () => {
|
||||||
|
it("подставляет ключ и адрес школы", () => {
|
||||||
|
const s = buildZoteroScript({ apiKey: "sbpdf_test123", baseUrl: "https://school.second-brain.ru" });
|
||||||
|
expect(s).toContain("'sbpdf_test123'");
|
||||||
|
expect(s).toContain("'https://school.second-brain.ru'");
|
||||||
|
expect(s).toContain("/api/pdf?url=");
|
||||||
|
expect(s).not.toContain("YOUR_KEY");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
|
||||||
|
export const PDF_KEY_PREFIX = "sbpdf_";
|
||||||
|
|
||||||
|
export function generatePdfKey(): string {
|
||||||
|
return PDF_KEY_PREFIX + randomBytes(24).toString("base64url");
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Адаптация скрипта Clean PDF (pdf.brainysnipe.ru/zotero-script.js) под школу.
|
||||||
|
export function buildZoteroScript({ apiKey, baseUrl }: { apiKey: string; baseUrl: string }): string {
|
||||||
|
return `// ── Настройки ──────────────────────────────────────────────────────────────
|
||||||
|
const API_KEY = '${apiKey}'; // персональный ключ из school.second-brain.ru/tools/clean-pdf
|
||||||
|
const SERVICE_URL = '${baseUrl}';
|
||||||
|
const FORMAT = 'A4'; // 'A4' или 'Letter'
|
||||||
|
const THEME = 'light'; // 'light' или 'dark'
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (typeof item === "undefined" || !item) return;
|
||||||
|
(async function () {
|
||||||
|
let tempFilePath = null;
|
||||||
|
try {
|
||||||
|
function joinPath(dir, filename) {
|
||||||
|
if (typeof OS !== "undefined" && OS.Path && OS.Path.join) return OS.Path.join(dir, filename);
|
||||||
|
const separator = dir.includes("\\\\") ? "\\\\" : "/";
|
||||||
|
return dir.replace(/[\\\\/]+$/, "") + separator + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetItem = item;
|
||||||
|
if (!targetItem.isRegularItem()) {
|
||||||
|
if (targetItem.isAttachment() || targetItem.isNote())
|
||||||
|
targetItem = Zotero.Items.get(targetItem.parentID);
|
||||||
|
}
|
||||||
|
if (!targetItem?.isRegularItem()) {
|
||||||
|
Zotero.alert(null, "Чистый PDF", "Выберите обычную запись Zotero.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = targetItem.getField("url");
|
||||||
|
if (!url) { Zotero.alert(null, "Чистый PDF", "У записи нет URL."); return; }
|
||||||
|
|
||||||
|
const pw = new Zotero.ProgressWindow();
|
||||||
|
pw.changeHeadline("Чистый PDF");
|
||||||
|
const progress = new pw.ItemProgress(targetItem.getImageSrc(), targetItem.getField("title"));
|
||||||
|
pw.show();
|
||||||
|
pw.addDescription("Генерируем PDF…");
|
||||||
|
|
||||||
|
const response = await Zotero.getMainWindow().fetch(
|
||||||
|
SERVICE_URL + "/api/pdf?url=" + encodeURIComponent(url) + "&format=" + FORMAT + "&theme=" + THEME,
|
||||||
|
{ headers: { 'Authorization': 'Bearer ' + API_KEY } }
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.json().catch(() => ({}));
|
||||||
|
progress.setError();
|
||||||
|
pw.addDescription("Ошибка: " + (body.error || ("HTTP " + response.status)));
|
||||||
|
pw.startCloseTimer(8000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8Array = new Uint8Array(await (await response.blob()).arrayBuffer());
|
||||||
|
if (uint8Array.length < 1000) {
|
||||||
|
progress.setError(); pw.addDescription("Пришёл пустой PDF."); pw.startCloseTimer(8000); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeTitle = (targetItem.getField("title") || "Document").replace(/[\\\\/:"*?<>|]+/g, "_").slice(0, 140);
|
||||||
|
tempFilePath = joinPath(Zotero.getTempDirectory().path, safeTitle + ".pdf");
|
||||||
|
await Zotero.getMainWindow().IOUtils.write(tempFilePath, uint8Array);
|
||||||
|
await Zotero.Attachments.importFromFile({ file: tempFilePath, parentItemID: targetItem.id, contentType: "application/pdf" });
|
||||||
|
|
||||||
|
for (const attId of targetItem.getAttachments()) {
|
||||||
|
const att = Zotero.Items.get(attId);
|
||||||
|
const ct = att?.attachmentContentType || "";
|
||||||
|
if (ct === "text/html" || ct === "application/zip") await att.eraseTx();
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.setProgress(100);
|
||||||
|
const usesCount = response.headers.get('X-Uses-Count');
|
||||||
|
const maxUses = response.headers.get('X-Max-Uses');
|
||||||
|
const usageInfo = (usesCount && maxUses) ? (" (" + usesCount + "/" + maxUses + " за месяц)") : "";
|
||||||
|
pw.addDescription("PDF прикреплён." + usageInfo);
|
||||||
|
pw.startCloseTimer(4000);
|
||||||
|
} catch (e) {
|
||||||
|
Zotero.alert(null, "Ошибка", e.toString());
|
||||||
|
} finally {
|
||||||
|
if (tempFilePath) {
|
||||||
|
await Zotero.getMainWindow().IOUtils.remove(tempFilePath, { ignoreAbsent: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user