Skip to content

Commit 3e60b72

Browse files
Merge pull request #317 from steven-jianhao-li/dev
Dev
2 parents 8095a4f + 195b860 commit 3e60b72

15 files changed

Lines changed: 828 additions & 27 deletions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zotero-ai-butler",
33
"type": "module",
4-
"version": "3.6.1-beta.1",
4+
"version": "3.6.1-beta.2",
55
"description": "Your personal AI butler, automatically and meticulously reads papers and summarizes notes.",
66
"config": {
77
"addonName": "zotero-ai-butler",

src/modules/llmService.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ import {
2020
normalizeReasoningEffortSetting,
2121
resolveReasoningEffort,
2222
} from "./llmproviders/shared/reasoning";
23+
import {
24+
isAbortError,
25+
normalizeAbortError,
26+
throwIfAborted,
27+
} from "./llmproviders/shared/requestAbort";
2328
import type {
2429
ConversationMessage,
30+
LLMAbortSignal,
2531
LLMOptions,
2632
LLMModelInfo,
2733
LLMProviderCapabilities,
@@ -103,6 +109,7 @@ export type LLMTransportOptions = {
103109
timeoutMs?: number;
104110
retry?: boolean;
105111
keyRotation?: boolean;
112+
abortSignal?: LLMAbortSignal;
106113
};
107114

108115
export type LLMGenerateRequest = {
@@ -266,6 +273,7 @@ export class LLMService {
266273
const common: LLMOptions = {
267274
stream: transport?.stream ?? getPref("stream") ?? true,
268275
requestTimeoutMs: transport?.timeoutMs ?? this.getRequestTimeout(),
276+
abortSignal: transport?.abortSignal,
269277
};
270278

271279
if (enableTemperature) {
@@ -496,6 +504,7 @@ export class LLMService {
496504
let lastError: Error | null = null;
497505

498506
for (let attempt = 0; attempt < maxRetries; attempt++) {
507+
throwIfAborted(request.transport?.abortSignal);
499508
const endpoint = route.endpoints[attempt % route.endpoints.length];
500509
try {
501510
const response = await this.generateOnceWithEndpoint(
@@ -506,6 +515,9 @@ export class LLMService {
506515
LLMEndpointManager.markEndpointAttempted(endpoint.id);
507516
return response;
508517
} catch (error: unknown) {
518+
if (isAbortError(error, request.transport?.abortSignal)) {
519+
throw normalizeAbortError(error, request.transport?.abortSignal);
520+
}
509521
LLMEndpointManager.markEndpointAttempted(endpoint.id);
510522
lastError = error instanceof Error ? error : new Error(String(error));
511523
ztoolkit.log(
@@ -524,12 +536,14 @@ export class LLMService {
524536
): Promise<LLMResponse> {
525537
const provider = this.getProviderForEndpoint(endpoint);
526538
const warnings: string[] = [];
539+
throwIfAborted(request.transport?.abortSignal);
527540
const resolved = await this.resolveContent(
528541
provider,
529542
request.content,
530543
warnings,
531544
true,
532545
);
546+
throwIfAborted(request.transport?.abortSignal);
533547
const options = this.buildOptions(
534548
endpoint,
535549
request.generation,
@@ -550,6 +564,9 @@ export class LLMService {
550564
request.onProgress,
551565
);
552566
} catch (error: unknown) {
567+
if (isAbortError(error, options.abortSignal)) {
568+
throw normalizeAbortError(error, options.abortSignal);
569+
}
553570
throw this.toApiCallError(endpoint, error);
554571
}
555572
} else {
@@ -562,6 +579,9 @@ export class LLMService {
562579
request.onProgress,
563580
);
564581
} catch (error: unknown) {
582+
if (isAbortError(error, options.abortSignal)) {
583+
throw normalizeAbortError(error, options.abortSignal);
584+
}
565585
throw this.toApiCallError(endpoint, error);
566586
}
567587
}
@@ -584,9 +604,13 @@ export class LLMService {
584604
let lastError: Error | null = null;
585605

586606
for (let attempt = 0; attempt < maxAttempts; attempt++) {
607+
throwIfAborted(request.transport?.abortSignal);
587608
try {
588609
return await this.generateOnceWithEndpoint(endpoint, request, prompt);
589610
} catch (error: unknown) {
611+
if (isAbortError(error, request.transport?.abortSignal)) {
612+
throw normalizeAbortError(error, request.transport?.abortSignal);
613+
}
590614
lastError = error instanceof Error ? error : new Error(String(error));
591615
ztoolkit.log(
592616
`[LLMService] API failed via ${endpoint.name} (${attempt + 1}/${maxAttempts}): ${lastError.message}`,
@@ -606,12 +630,16 @@ export class LLMService {
606630
let lastError: Error | null = null;
607631

608632
for (let attempt = 0; attempt < maxRetries; attempt++) {
633+
throwIfAborted(request.transport?.abortSignal);
609634
const endpoint = route.endpoints[attempt % route.endpoints.length];
610635
try {
611636
const response = await this.chatOnceWithEndpoint(endpoint, request);
612637
LLMEndpointManager.markEndpointAttempted(endpoint.id);
613638
return response;
614639
} catch (error: unknown) {
640+
if (isAbortError(error, request.transport?.abortSignal)) {
641+
throw normalizeAbortError(error, request.transport?.abortSignal);
642+
}
615643
LLMEndpointManager.markEndpointAttempted(endpoint.id);
616644
lastError = error instanceof Error ? error : new Error(String(error));
617645
ztoolkit.log(
@@ -629,12 +657,14 @@ export class LLMService {
629657
): Promise<LLMResponse> {
630658
const provider = this.getProviderForEndpoint(endpoint);
631659
const warnings: string[] = [];
660+
throwIfAborted(request.transport?.abortSignal);
632661
const resolved = await this.resolveContent(
633662
provider,
634663
request.content,
635664
warnings,
636665
false,
637666
);
667+
throwIfAborted(request.transport?.abortSignal);
638668
if (resolved.mode !== "single") {
639669
throw new Error("Chat requests do not support multi-file input.");
640670
}
@@ -653,6 +683,9 @@ export class LLMService {
653683
request.onProgress,
654684
);
655685
} catch (error: unknown) {
686+
if (isAbortError(error, options.abortSignal)) {
687+
throw normalizeAbortError(error, options.abortSignal);
688+
}
656689
throw this.toApiCallError(endpoint, error);
657690
}
658691
return this.toResponse(
@@ -673,9 +706,13 @@ export class LLMService {
673706
let lastError: Error | null = null;
674707

675708
for (let attempt = 0; attempt < maxAttempts; attempt++) {
709+
throwIfAborted(request.transport?.abortSignal);
676710
try {
677711
return await this.chatOnceWithEndpoint(endpoint, request);
678712
} catch (error: unknown) {
713+
if (isAbortError(error, request.transport?.abortSignal)) {
714+
throw normalizeAbortError(error, request.transport?.abortSignal);
715+
}
679716
lastError = error instanceof Error ? error : new Error(String(error));
680717
ztoolkit.log(
681718
`[LLMService] Chat API failed via ${endpoint.name} (${attempt + 1}/${maxAttempts}): ${lastError.message}`,
@@ -711,6 +748,7 @@ export class LLMService {
711748
let lastError: Error | null = null;
712749

713750
for (let attempt = 0; attempt < maxRetries; attempt++) {
751+
throwIfAborted(request.transport?.abortSignal);
714752
try {
715753
const options = this.buildOptions(
716754
providerId,
@@ -740,6 +778,9 @@ export class LLMService {
740778
if (useKeyRotation) ApiKeyManager.advanceToNextKey(keyManagerId);
741779
return this.toResponse(text, providerId, options, warnings);
742780
} catch (error: unknown) {
781+
if (isAbortError(error, request.transport?.abortSignal)) {
782+
throw normalizeAbortError(error, request.transport?.abortSignal);
783+
}
743784
lastError = error instanceof Error ? error : new Error(String(error));
744785
ztoolkit.log(
745786
`[LLMService] API 调用失败 (尝试 ${attempt + 1}/${maxRetries}): ${lastError.message}`,

src/modules/llmproviders/AnthropicProvider.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import {
1717
parseModelListResponse,
1818
requestModelListJson,
1919
} from "./shared/modelList";
20+
import {
21+
bindAbortSignal,
22+
isAbortError,
23+
normalizeAbortError,
24+
throwIfAborted,
25+
} from "./shared/requestAbort";
2026

2127
export class AnthropicProvider implements ILlmProvider {
2228
readonly id = "anthropic";
@@ -47,6 +53,7 @@ export class AnthropicProvider implements ILlmProvider {
4753

4854
if (!baseUrl) throw new Error("Anthropic API URL 未配置");
4955
if (!apiKey) throw new Error("Anthropic API Key 未配置");
56+
throwIfAborted(options.abortSignal);
5057

5158
const endpoint = `${baseUrl}/v1/messages`;
5259

@@ -94,6 +101,8 @@ export class AnthropicProvider implements ILlmProvider {
94101
let processedLength = 0;
95102
let partialLine = "";
96103
let gotAnyDelta = false;
104+
let abortError: Error | null = null;
105+
let cleanupAbortSignal: (() => void) | undefined;
97106

98107
try {
99108
await Zotero.HTTP.request("POST", endpoint, {
@@ -107,6 +116,13 @@ export class AnthropicProvider implements ILlmProvider {
107116
timeout: options.requestTimeoutMs ?? getRequestTimeoutMs(),
108117
errorDelayMax: 0,
109118
requestObserver: (xmlhttp: XMLHttpRequest) => {
119+
cleanupAbortSignal = bindAbortSignal(
120+
options.abortSignal,
121+
xmlhttp,
122+
(error) => {
123+
abortError = error;
124+
},
125+
);
110126
xmlhttp.onprogress = (e: any) => {
111127
const status = e.target.status;
112128
if (status >= 400) {
@@ -173,6 +189,9 @@ export class AnthropicProvider implements ILlmProvider {
173189
},
174190
});
175191
} catch (error: any) {
192+
if (abortError || isAbortError(error, options.abortSignal)) {
193+
throw normalizeAbortError(abortError || error, options.abortSignal);
194+
}
176195
let errorMessage = error?.message || "Anthropic 请求失败";
177196
try {
178197
const responseText =
@@ -192,6 +211,8 @@ export class AnthropicProvider implements ILlmProvider {
192211
}
193212
if (gotAnyDelta && chunks.length > 0) return chunks.join("");
194213
throw new Error(errorMessage);
214+
} finally {
215+
cleanupAbortSignal?.();
195216
}
196217

197218
const streamed = chunks.join("");
@@ -217,6 +238,7 @@ export class AnthropicProvider implements ILlmProvider {
217238

218239
if (!baseUrl) throw new Error("Anthropic API URL 未配置");
219240
if (!apiKey) throw new Error("Anthropic API Key 未配置");
241+
throwIfAborted(options.abortSignal);
220242

221243
const endpoint = `${baseUrl}/v1/messages`;
222244

@@ -274,6 +296,7 @@ export class AnthropicProvider implements ILlmProvider {
274296
let processedLength = 0;
275297
let partialLine = "";
276298
let abortError: Error | null = null;
299+
let cleanupAbortSignal: (() => void) | undefined;
277300

278301
try {
279302
await Zotero.HTTP.request("POST", endpoint, {
@@ -287,6 +310,13 @@ export class AnthropicProvider implements ILlmProvider {
287310
timeout: options.requestTimeoutMs ?? getRequestTimeoutMs(),
288311
errorDelayMax: 0,
289312
requestObserver: (xmlhttp: XMLHttpRequest) => {
313+
cleanupAbortSignal = bindAbortSignal(
314+
options.abortSignal,
315+
xmlhttp,
316+
(error) => {
317+
abortError = error;
318+
},
319+
);
290320
xmlhttp.onprogress = (e: any) => {
291321
const status = e.target.status;
292322
if (status >= 400) {
@@ -362,7 +392,9 @@ export class AnthropicProvider implements ILlmProvider {
362392
},
363393
});
364394
} catch (error: any) {
365-
if (abortError) throw abortError;
395+
if (abortError || isAbortError(error, options.abortSignal)) {
396+
throw normalizeAbortError(abortError || error, options.abortSignal);
397+
}
366398
let errorMessage = error?.message || "Anthropic 请求失败";
367399
try {
368400
const responseText =
@@ -386,6 +418,8 @@ export class AnthropicProvider implements ILlmProvider {
386418
message: errorMessage,
387419
});
388420
throw new Error(errorMessage);
421+
} finally {
422+
cleanupAbortSignal?.();
389423
}
390424

391425
return chunks.join("");
@@ -575,6 +609,7 @@ export class AnthropicProvider implements ILlmProvider {
575609
if (!baseUrl) throw new Error("Anthropic API URL 未配置");
576610
if (!apiKey) throw new Error("Anthropic API Key 未配置");
577611
if (pdfFiles.length === 0) throw new Error("没有要处理的 PDF 文件");
612+
throwIfAborted(options.abortSignal);
578613

579614
// 构建 document 部分
580615
const documentParts: any[] = [];
@@ -629,6 +664,7 @@ export class AnthropicProvider implements ILlmProvider {
629664
let partialLine = "";
630665
let gotAnyDelta = false;
631666
let abortError: Error | null = null;
667+
let cleanupAbortSignal: (() => void) | undefined;
632668

633669
try {
634670
await Zotero.HTTP.request("POST", endpoint, {
@@ -642,6 +678,13 @@ export class AnthropicProvider implements ILlmProvider {
642678
timeout: options.requestTimeoutMs ?? getRequestTimeoutMs(),
643679
errorDelayMax: 0,
644680
requestObserver: (xmlhttp: XMLHttpRequest) => {
681+
cleanupAbortSignal = bindAbortSignal(
682+
options.abortSignal,
683+
xmlhttp,
684+
(error) => {
685+
abortError = error;
686+
},
687+
);
645688
xmlhttp.onprogress = (e: any) => {
646689
const status = e.target.status;
647690
if (status >= 400) {
@@ -722,9 +765,15 @@ export class AnthropicProvider implements ILlmProvider {
722765
});
723766
} catch (error: any) {
724767
if (abortError) {
768+
if (isAbortError(abortError, options.abortSignal)) {
769+
throw normalizeAbortError(abortError, options.abortSignal);
770+
}
725771
if (gotAnyDelta && chunks.length > 0) return chunks.join("");
726772
throw abortError;
727773
}
774+
if (isAbortError(error, options.abortSignal)) {
775+
throw normalizeAbortError(error, options.abortSignal);
776+
}
728777
let errorMessage = error?.message || "Anthropic 多文件请求失败";
729778
try {
730779
const responseText =
@@ -744,6 +793,8 @@ export class AnthropicProvider implements ILlmProvider {
744793
}
745794
if (gotAnyDelta && chunks.length > 0) return chunks.join("");
746795
throw new Error(errorMessage);
796+
} finally {
797+
cleanupAbortSignal?.();
747798
}
748799

749800
const streamed = chunks.join("");

0 commit comments

Comments
 (0)