Add API key generator and Zotero script builder

This commit is contained in:
2026-07-06 11:46:31 +05:00
parent 6316c09993
commit 1d5b4f9251
4 changed files with 114 additions and 0 deletions
@@ -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");
});
});
+7
View File
@@ -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");
}
+80
View File
@@ -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(() => {});
}
}
})();`;
}