fix: surface Better BibTeX CAYW errors

This commit is contained in:
WesternGua 2026-06-04 22:46:49 +08:00
parent 01c52b5546
commit c7c4d2c80b
9 changed files with 84 additions and 29 deletions

View file

@ -6,6 +6,13 @@
---
## [0.2.6] - 2026-06-04
### 修复
- 修复 Better BibTeX CAYW 端点异常时插件静默处理的问题:当 CAYW 返回非 2xx、非 JSON、空 JSON 或过快空响应时,现在会显示明确的错误提示。
- 插入引用时不再在 CAYW 异常后静默回退或直接消失,而是提示用户检查 Better BibTeX 是否安装且与当前 Zotero 版本兼容。
## [0.2.5] - 2026-06-04
### 修复

View file

@ -1,12 +1,10 @@
# Zotero Citations v0.2.5
# Zotero Citations v0.2.6
## Locator editing reliability and safety
## Better BibTeX CAYW error reporting
- Fixed locator edits sometimes failing with “Could not find the active editor”.
- Blocked locator edits while Zotero is offline to avoid regenerating citations from stale cached metadata.
- Added live reconnect checks so the locator editor unlocks after Zotero comes back online.
- Added Node HTTP fallbacks for Zotero ping and Better BibTeX JSON-RPC reads when Obsidian `requestUrl` misreports the local connector.
- Fixed remote item fetching during locator saves so it no longer loses plugin context and incorrectly reports missing Zotero items.
- Fixed silent handling when the Better BibTeX CAYW endpoint fails or returns an invalid response.
- Insert citation now shows an explicit notice when CAYW returns a non-2xx status, non-JSON body, empty JSON body, or immediate empty response.
- The notice points users to check whether Better BibTeX is installed and compatible with their Zotero version.
---

View file

@ -1,7 +1,7 @@
{
"id": "zotero-citations",
"name": "Zotero Citations",
"version": "0.2.5",
"version": "0.2.6",
"minAppVersion": "1.5.7",
"description": "Zotero citation management with Word export via Pandoc.",
"author": "WesternGua",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "zotero-citations",
"version": "0.2.5",
"version": "0.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "zotero-citations",
"version": "0.2.5",
"version": "0.2.6",
"license": "MIT",
"devDependencies": {
"@codemirror/state": "^6.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "zotero-citations",
"version": "0.2.5",
"version": "0.2.6",
"description": "Zotero citation management with Word export via Pandoc.",
"main": "main.js",
"scripts": {

View file

@ -140,6 +140,15 @@ export class ZoteroAPI {
}
}
async pingBBT(): Promise<boolean> {
try {
const text = await this.httpGet(`${this.baseUrl}/better-bibtex/cayw?probe=true`, 3000, true);
return typeof text === "string";
} catch {
return false;
}
}
// ════════════════════════════════════════════════════════════════════════════
// IMPROVEMENT 2: Dynamic CSL style reading
// ════════════════════════════════════════════════════════════════════════════
@ -242,23 +251,47 @@ export class ZoteroAPI {
// ════════════════════════════════════════════════════════════════════════════
async openCAYW(onReturn?: () => void): Promise<CaywResult[]> {
const startTime = Date.now();
const rawText = await this.httpGet(
`http://127.0.0.1:${this.port}/better-bibtex/cayw?format=json`,
600000 // 10 min
`${this.baseUrl}/better-bibtex/cayw?format=json`,
600000, // 10 min
true, // non-2xx means the CAYW endpoint itself failed
);
const elapsed = Date.now() - startTime;
try {
onReturn?.();
} catch {
// ignore callback errors
}
if (!rawText || rawText === "[]" || rawText === "null" || rawText === "{}") return [];
// BBT returns an empty body when the user cancels the real picker. When the
// endpoint returns immediately, however, Zotero never had a chance to show a
// usable picker; surface that as an explicit plugin error instead of silently
// treating it as a cancellation. This is the failure mode seen when CAYW is
// broken by an incompatible Better BibTeX/Zotero combination.
if (!rawText) {
if (elapsed < 1500) {
throw new ZoteroPickerError(
"Better BibTeX CAYW returned an empty response before the picker could open. Better BibTeX may be disabled or incompatible with this Zotero version.",
);
}
return [];
}
if (rawText === "[]") return [];
if (rawText === "null" || rawText === "{}") {
throw new ZoteroPickerError(
"Better BibTeX CAYW returned an empty JSON response. Better BibTeX may be disabled or incompatible with this Zotero version.",
);
}
let parsed: unknown;
try {
parsed = JSON.parse(rawText);
} catch {
return [];
// Non-JSON response means BBT returned something unexpected, for example
// its plain-text "CAYW failed" error body. Do not swallow it silently.
throw new ZoteroPickerError(`Unexpected response from Better BibTeX CAYW: ${snippet(rawText)}`);
}
const rawItems = this.extractArray(parsed);
@ -533,18 +566,26 @@ ORDER BY i.key, ic.orderIndex;`;
return null;
}
private httpGet(url: string, timeoutMs: number = 30000): Promise<string> {
private httpGet(url: string, timeoutMs: number = 30000, expectStatus2xx = false): Promise<string> {
return new Promise((resolve, reject) => {
const req = nodeHttp.get(url, (res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer | string) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8").trim()));
res.on("end", () => {
const text = Buffer.concat(chunks).toString("utf-8").trim();
const status = res.statusCode ?? 0;
if (expectStatus2xx && (status < 200 || status >= 300)) {
reject(new ZoteroPickerError(`Better BibTeX CAYW HTTP ${status}: ${snippet(text)}`));
return;
}
resolve(text);
});
res.on("error", (e: Error) => reject(new ZoteroConnectionError(e.message)));
});
req.on("error", (e: Error) => reject(new ZoteroConnectionError(e.message)));
req.setTimeout(timeoutMs, () => {
req.destroy();
resolve("");
reject(new ZoteroConnectionError("HTTP timeout"));
});
});
}
@ -764,6 +805,12 @@ function asJsonRpcResponse(value: unknown): JsonRpcResponse {
};
}
function snippet(text: string, max = 200): string {
const compact = text.replace(/\s+/g, " ").trim();
if (!compact) return "(empty response)";
return compact.length > max ? `${compact.slice(0, max)}` : compact;
}
function parseJsonArray(input: unknown): unknown[] {
if (typeof input !== "string") return [];
try {

View file

@ -133,6 +133,8 @@ export const I18N: { zh: I18NDict; en: I18NDict } = {
"command.checkPandoc": "检测 Pandoc 是否可用",
"notice.openPicker": "正在打开 Zotero 引用选择器,请切换到 Zotero 完成选择…",
"notice.connectZoteroFailed": "无法连接 Zotero。请确保 Zotero 已打开且 Better BibTeX 已安装。",
"notice.bbtUnavailable": "检测到 Zotero 已运行,但 Better BibTeX 未响应。请确认 BBT 已安装且与当前 Zotero 版本兼容。",
"notice.bbtCaywError": "Better BibTeX CAYW 未正常返回:{error}。请确认 BBT 已安装且与当前 Zotero 版本兼容。",
"notice.nativePickerFallback": "Zotero 原生引用选择器失败,已回退到插件内搜索面板。",
"notice.pickerError": "Zotero 引用选择器出错:{error}",
"notice.wordDisplayOn": "Word 风格脚注显示:已开启",
@ -287,6 +289,8 @@ export const I18N: { zh: I18NDict; en: I18NDict } = {
"command.checkPandoc": "Check whether Pandoc is available",
"notice.openPicker": "Opening the Zotero citation picker. Switch to Zotero to finish your selection…",
"notice.connectZoteroFailed": "Could not connect to Zotero. Make sure Zotero is open and Better BibTeX is installed.",
"notice.bbtUnavailable": "Zotero is running but Better BibTeX is not responding. Make sure BBT is installed and compatible with your Zotero version.",
"notice.bbtCaywError": "Better BibTeX CAYW did not return normally: {error}. Make sure BBT is installed and compatible with your Zotero version.",
"notice.nativePickerFallback": "The native Zotero citation picker failed. Falling back to the in-plugin search panel.",
"notice.pickerError": "Zotero citation picker error: {error}",
"notice.wordDisplayOn": "Word-style footnote display: on",

View file

@ -105,6 +105,13 @@ export default class ZoteroCitations extends obsidian.Plugin {
await this.loadSettings();
this.api = new ZoteroAPI(this.settings.zoteroPort);
// Warn if Zotero is reachable but BBT's CAYW endpoint is not (version mismatch, BBT not installed, etc.)
void this.api.ping().then(async (zoteroUp) => {
if (!zoteroUp) return;
const bbtUp = await this.api.pingBBT();
if (!bbtUp) new obsidian.Notice(this.t("notice.bbtUnavailable"), 8000);
});
obsidian.addIcon("zotero-z", ZOTERO_ICON);
obsidian.addIcon("zotero-cite", ZOTERO_CITE_ICON);
obsidian.addIcon("zotero-word-display", ZOTERO_WORD_DISPLAY_ICON);
@ -431,16 +438,7 @@ export default class ZoteroCitations extends obsidian.Plugin {
if (err instanceof ZoteroConnectionError) {
new obsidian.Notice(this.t("notice.connectZoteroFailed"), 6000);
} else if (err instanceof ZoteroPickerError) {
new obsidian.Notice(this.t("notice.nativePickerFallback"), 5000);
this.openSearchFallback(
editor,
existingInline?.page,
existingEndnote?.page,
existingInText?.page,
existingInline,
existingEndnote,
existingInText,
);
new obsidian.Notice(this.t("notice.bbtCaywError", { error: err.message || String(err) }), 10000);
} else {
new obsidian.Notice(this.t("notice.pickerError", { error: String(err) }), 6000);
}

View file

@ -4,5 +4,6 @@
"0.2.2": "1.5.7",
"0.2.3": "1.5.7",
"0.2.4": "1.5.7",
"0.2.5": "1.5.7"
"0.2.5": "1.5.7",
"0.2.6": "1.5.7"
}