paper-search-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +165 -0
- package/LICENSE +21 -0
- package/README-sc.md +642 -0
- package/README.md +642 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +637 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/ConfigService.d.ts +26 -0
- package/dist/config/ConfigService.d.ts.map +1 -0
- package/dist/config/ConfigService.js +145 -0
- package/dist/config/ConfigService.js.map +1 -0
- package/dist/config/constants.d.ts +140 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +93 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/core/diagnostics.d.ts +43 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +544 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/handleToolCall.d.ts +8 -0
- package/dist/core/handleToolCall.d.ts.map +1 -0
- package/dist/core/handleToolCall.js +440 -0
- package/dist/core/handleToolCall.js.map +1 -0
- package/dist/core/schemas.d.ts +454 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +322 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/searchers.d.ts +45 -0
- package/dist/core/searchers.d.ts.map +1 -0
- package/dist/core/searchers.js +73 -0
- package/dist/core/searchers.js.map +1 -0
- package/dist/core/tools.d.ts +7 -0
- package/dist/core/tools.d.ts.map +1 -0
- package/dist/core/tools.js +640 -0
- package/dist/core/tools.js.map +1 -0
- package/dist/models/Paper.d.ts +64 -0
- package/dist/models/Paper.d.ts.map +1 -0
- package/dist/models/Paper.js +70 -0
- package/dist/models/Paper.js.map +1 -0
- package/dist/platforms/ArxivSearcher.d.ts +64 -0
- package/dist/platforms/ArxivSearcher.d.ts.map +1 -0
- package/dist/platforms/ArxivSearcher.js +531 -0
- package/dist/platforms/ArxivSearcher.js.map +1 -0
- package/dist/platforms/BioRxivSearcher.d.ts +47 -0
- package/dist/platforms/BioRxivSearcher.d.ts.map +1 -0
- package/dist/platforms/BioRxivSearcher.js +196 -0
- package/dist/platforms/BioRxivSearcher.js.map +1 -0
- package/dist/platforms/CORESearcher.d.ts +16 -0
- package/dist/platforms/CORESearcher.d.ts.map +1 -0
- package/dist/platforms/CORESearcher.js +148 -0
- package/dist/platforms/CORESearcher.js.map +1 -0
- package/dist/platforms/CrossrefSearcher.d.ts +34 -0
- package/dist/platforms/CrossrefSearcher.d.ts.map +1 -0
- package/dist/platforms/CrossrefSearcher.js +339 -0
- package/dist/platforms/CrossrefSearcher.js.map +1 -0
- package/dist/platforms/EuropePMCSearcher.d.ts +20 -0
- package/dist/platforms/EuropePMCSearcher.d.ts.map +1 -0
- package/dist/platforms/EuropePMCSearcher.js +173 -0
- package/dist/platforms/EuropePMCSearcher.js.map +1 -0
- package/dist/platforms/GoogleScholarSearcher.d.ts +77 -0
- package/dist/platforms/GoogleScholarSearcher.d.ts.map +1 -0
- package/dist/platforms/GoogleScholarSearcher.js +262 -0
- package/dist/platforms/GoogleScholarSearcher.js.map +1 -0
- package/dist/platforms/IACRSearcher.d.ts +51 -0
- package/dist/platforms/IACRSearcher.d.ts.map +1 -0
- package/dist/platforms/IACRSearcher.js +339 -0
- package/dist/platforms/IACRSearcher.js.map +1 -0
- package/dist/platforms/OpenAIRESearcher.d.ts +22 -0
- package/dist/platforms/OpenAIRESearcher.d.ts.map +1 -0
- package/dist/platforms/OpenAIRESearcher.js +223 -0
- package/dist/platforms/OpenAIRESearcher.js.map +1 -0
- package/dist/platforms/OpenAlexSearcher.d.ts +14 -0
- package/dist/platforms/OpenAlexSearcher.d.ts.map +1 -0
- package/dist/platforms/OpenAlexSearcher.js +114 -0
- package/dist/platforms/OpenAlexSearcher.js.map +1 -0
- package/dist/platforms/PMCSearcher.d.ts +20 -0
- package/dist/platforms/PMCSearcher.d.ts.map +1 -0
- package/dist/platforms/PMCSearcher.js +177 -0
- package/dist/platforms/PMCSearcher.js.map +1 -0
- package/dist/platforms/PaperSource.d.ts +143 -0
- package/dist/platforms/PaperSource.d.ts.map +1 -0
- package/dist/platforms/PaperSource.js +125 -0
- package/dist/platforms/PaperSource.js.map +1 -0
- package/dist/platforms/PubMedSearcher.d.ts +104 -0
- package/dist/platforms/PubMedSearcher.d.ts.map +1 -0
- package/dist/platforms/PubMedSearcher.js +422 -0
- package/dist/platforms/PubMedSearcher.js.map +1 -0
- package/dist/platforms/SciHubSearcher.d.ts +66 -0
- package/dist/platforms/SciHubSearcher.d.ts.map +1 -0
- package/dist/platforms/SciHubSearcher.js +398 -0
- package/dist/platforms/SciHubSearcher.js.map +1 -0
- package/dist/platforms/ScienceDirectSearcher.d.ts +42 -0
- package/dist/platforms/ScienceDirectSearcher.d.ts.map +1 -0
- package/dist/platforms/ScienceDirectSearcher.js +326 -0
- package/dist/platforms/ScienceDirectSearcher.js.map +1 -0
- package/dist/platforms/ScopusSearcher.d.ts +43 -0
- package/dist/platforms/ScopusSearcher.d.ts.map +1 -0
- package/dist/platforms/ScopusSearcher.js +364 -0
- package/dist/platforms/ScopusSearcher.js.map +1 -0
- package/dist/platforms/SemanticScholarSearcher.d.ts +96 -0
- package/dist/platforms/SemanticScholarSearcher.d.ts.map +1 -0
- package/dist/platforms/SemanticScholarSearcher.js +419 -0
- package/dist/platforms/SemanticScholarSearcher.js.map +1 -0
- package/dist/platforms/SpringerSearcher.d.ts +54 -0
- package/dist/platforms/SpringerSearcher.d.ts.map +1 -0
- package/dist/platforms/SpringerSearcher.js +407 -0
- package/dist/platforms/SpringerSearcher.js.map +1 -0
- package/dist/platforms/UnpaywallSearcher.d.ts +18 -0
- package/dist/platforms/UnpaywallSearcher.d.ts.map +1 -0
- package/dist/platforms/UnpaywallSearcher.js +115 -0
- package/dist/platforms/UnpaywallSearcher.js.map +1 -0
- package/dist/platforms/WebOfScienceSearcher.d.ts +111 -0
- package/dist/platforms/WebOfScienceSearcher.d.ts.map +1 -0
- package/dist/platforms/WebOfScienceSearcher.js +500 -0
- package/dist/platforms/WebOfScienceSearcher.js.map +1 -0
- package/dist/platforms/WileySearcher.d.ts +44 -0
- package/dist/platforms/WileySearcher.d.ts.map +1 -0
- package/dist/platforms/WileySearcher.js +148 -0
- package/dist/platforms/WileySearcher.js.map +1 -0
- package/dist/services/CitationService.d.ts +66 -0
- package/dist/services/CitationService.d.ts.map +1 -0
- package/dist/services/CitationService.js +237 -0
- package/dist/services/CitationService.js.map +1 -0
- package/dist/services/MultiSourceSearchService.d.ts +19 -0
- package/dist/services/MultiSourceSearchService.d.ts.map +1 -0
- package/dist/services/MultiSourceSearchService.js +96 -0
- package/dist/services/MultiSourceSearchService.js.map +1 -0
- package/dist/services/OpenAccessFallbackService.d.ts +20 -0
- package/dist/services/OpenAccessFallbackService.d.ts.map +1 -0
- package/dist/services/OpenAccessFallbackService.js +124 -0
- package/dist/services/OpenAccessFallbackService.js.map +1 -0
- package/dist/utils/ErrorHandler.d.ts +99 -0
- package/dist/utils/ErrorHandler.d.ts.map +1 -0
- package/dist/utils/ErrorHandler.js +266 -0
- package/dist/utils/ErrorHandler.js.map +1 -0
- package/dist/utils/Logger.d.ts +6 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +26 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/PDFExtractor.d.ts +34 -0
- package/dist/utils/PDFExtractor.d.ts.map +1 -0
- package/dist/utils/PDFExtractor.js +130 -0
- package/dist/utils/PDFExtractor.js.map +1 -0
- package/dist/utils/PdfDownload.d.ts +7 -0
- package/dist/utils/PdfDownload.d.ts.map +1 -0
- package/dist/utils/PdfDownload.js +52 -0
- package/dist/utils/PdfDownload.js.map +1 -0
- package/dist/utils/QuotaManager.d.ts +32 -0
- package/dist/utils/QuotaManager.d.ts.map +1 -0
- package/dist/utils/QuotaManager.js +95 -0
- package/dist/utils/QuotaManager.js.map +1 -0
- package/dist/utils/RateLimiter.d.ts +50 -0
- package/dist/utils/RateLimiter.d.ts.map +1 -0
- package/dist/utils/RateLimiter.js +121 -0
- package/dist/utils/RateLimiter.js.map +1 -0
- package/dist/utils/RequestCache.d.ts +26 -0
- package/dist/utils/RequestCache.d.ts.map +1 -0
- package/dist/utils/RequestCache.js +66 -0
- package/dist/utils/RequestCache.js.map +1 -0
- package/dist/utils/SecurityUtils.d.ts +80 -0
- package/dist/utils/SecurityUtils.d.ts.map +1 -0
- package/dist/utils/SecurityUtils.js +357 -0
- package/dist/utils/SecurityUtils.js.map +1 -0
- package/package.json +111 -0
- package/skills/paper-search/SKILL.md +192 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
// @ts-ignore - pdf-parse doesn't have type definitions
|
|
5
|
+
import pdf from 'pdf-parse';
|
|
6
|
+
import { logDebug, logWarn } from './Logger.js';
|
|
7
|
+
import { TIMEOUTS, USER_AGENT } from '../config/constants.js';
|
|
8
|
+
export class PDFExtractor {
|
|
9
|
+
/**
|
|
10
|
+
* Download PDF from URL and extract text
|
|
11
|
+
*/
|
|
12
|
+
async extractFromUrl(url, options = {}) {
|
|
13
|
+
try {
|
|
14
|
+
logDebug(`Downloading PDF from: ${url}`);
|
|
15
|
+
const response = await axios.get(url, {
|
|
16
|
+
responseType: 'arraybuffer',
|
|
17
|
+
timeout: TIMEOUTS.DOWNLOAD,
|
|
18
|
+
headers: {
|
|
19
|
+
'User-Agent': USER_AGENT,
|
|
20
|
+
'Accept': 'application/pdf'
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
if (response.status !== 200) {
|
|
24
|
+
throw new Error(`Failed to download PDF: HTTP ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
const buffer = Buffer.from(response.data);
|
|
27
|
+
return await this.extractFromBuffer(buffer, options);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logWarn(`Error downloading PDF from ${url}:`, error.message);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract text from PDF file
|
|
36
|
+
*/
|
|
37
|
+
async extractFromFile(filePath, options = {}) {
|
|
38
|
+
try {
|
|
39
|
+
if (!fs.existsSync(filePath)) {
|
|
40
|
+
throw new Error(`PDF file not found: ${filePath}`);
|
|
41
|
+
}
|
|
42
|
+
logDebug(`Extracting text from PDF file: ${filePath}`);
|
|
43
|
+
const dataBuffer = fs.readFileSync(filePath);
|
|
44
|
+
return await this.extractFromBuffer(dataBuffer, options);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logWarn(`Error extracting PDF from file ${filePath}:`, error.message);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract text from PDF buffer
|
|
53
|
+
*/
|
|
54
|
+
async extractFromBuffer(buffer, options = {}) {
|
|
55
|
+
try {
|
|
56
|
+
const pdfOptions = {};
|
|
57
|
+
if (options.maxPages) {
|
|
58
|
+
pdfOptions.max = options.maxPages;
|
|
59
|
+
}
|
|
60
|
+
const data = await pdf(buffer, pdfOptions);
|
|
61
|
+
let text = data.text;
|
|
62
|
+
if (options.cleanText !== false) {
|
|
63
|
+
text = this.cleanText(text);
|
|
64
|
+
}
|
|
65
|
+
logDebug(`Extracted ${data.numpages} pages, ${text.length} characters`);
|
|
66
|
+
return {
|
|
67
|
+
text,
|
|
68
|
+
numPages: data.numpages,
|
|
69
|
+
info: data.info,
|
|
70
|
+
metadata: data.metadata
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logWarn('Error parsing PDF:', error.message);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Clean extracted text
|
|
80
|
+
*/
|
|
81
|
+
cleanText(text) {
|
|
82
|
+
return text
|
|
83
|
+
// Remove excessive whitespace
|
|
84
|
+
.replace(/\s+/g, ' ')
|
|
85
|
+
// Remove page numbers (common patterns)
|
|
86
|
+
.replace(/\n\s*\d+\s*\n/g, '\n')
|
|
87
|
+
// Remove headers/footers (lines with only numbers or short text)
|
|
88
|
+
.replace(/\n\s*[\w\s]{1,30}\s*\n/g, '\n')
|
|
89
|
+
// Normalize line breaks
|
|
90
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
91
|
+
// Trim
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Download PDF and save to file
|
|
96
|
+
*/
|
|
97
|
+
async downloadPdf(url, savePath) {
|
|
98
|
+
try {
|
|
99
|
+
logDebug(`Downloading PDF from ${url} to ${savePath}`);
|
|
100
|
+
// Ensure directory exists
|
|
101
|
+
const dir = path.dirname(savePath);
|
|
102
|
+
if (!fs.existsSync(dir)) {
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
const response = await axios.get(url, {
|
|
106
|
+
responseType: 'stream',
|
|
107
|
+
timeout: TIMEOUTS.DOWNLOAD,
|
|
108
|
+
headers: {
|
|
109
|
+
'User-Agent': USER_AGENT,
|
|
110
|
+
'Accept': 'application/pdf'
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const writer = fs.createWriteStream(savePath);
|
|
114
|
+
response.data.pipe(writer);
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
writer.on('finish', () => {
|
|
117
|
+
logDebug(`PDF saved to: ${savePath}`);
|
|
118
|
+
resolve(savePath);
|
|
119
|
+
});
|
|
120
|
+
writer.on('error', reject);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
logWarn(`Error downloading PDF:`, error.message);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export default PDFExtractor;
|
|
130
|
+
//# sourceMappingURL=PDFExtractor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PDFExtractor.js","sourceRoot":"","sources":["../../src/utils/PDFExtractor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface PdfDownloadOptions {
|
|
2
|
+
headers?: Record<string, string>;
|
|
3
|
+
}
|
|
4
|
+
export declare function safeFilename(value: string, fallback?: string): string;
|
|
5
|
+
export declare function isPdfBuffer(buffer: Buffer, contentType?: string): boolean;
|
|
6
|
+
export declare function downloadPdfFromUrl(pdfUrl: string, savePath: string, filenameHint: string, options?: PdfDownloadOptions): Promise<string>;
|
|
7
|
+
//# sourceMappingURL=PdfDownload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PdfDownload.d.ts","sourceRoot":"","sources":["../../src/utils/PdfDownload.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAU,GAAG,MAAM,CAGtE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,SAAK,GAAG,OAAO,CAErE;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CA4CjB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { TIMEOUTS, USER_AGENT } from '../config/constants.js';
|
|
5
|
+
export function safeFilename(value, fallback = 'paper') {
|
|
6
|
+
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^[_\-.]+|[_\-.]+$/g, '');
|
|
7
|
+
return (safe || fallback).slice(0, 120);
|
|
8
|
+
}
|
|
9
|
+
export function isPdfBuffer(buffer, contentType = '') {
|
|
10
|
+
return contentType.toLowerCase().includes('pdf') || buffer.subarray(0, 4).toString() === '%PDF';
|
|
11
|
+
}
|
|
12
|
+
export async function downloadPdfFromUrl(pdfUrl, savePath, filenameHint, options = {}) {
|
|
13
|
+
if (!pdfUrl) {
|
|
14
|
+
throw new Error('Missing PDF URL');
|
|
15
|
+
}
|
|
16
|
+
if (pdfUrl.startsWith('ftp://')) {
|
|
17
|
+
throw new Error(`FTP PDF links are not supported by the Node downloader: ${pdfUrl}`);
|
|
18
|
+
}
|
|
19
|
+
fs.mkdirSync(savePath, { recursive: true });
|
|
20
|
+
const outputPath = path.join(savePath, `${safeFilename(filenameHint)}.pdf`);
|
|
21
|
+
const response = await axios.get(pdfUrl, {
|
|
22
|
+
responseType: 'arraybuffer',
|
|
23
|
+
timeout: TIMEOUTS.DOWNLOAD,
|
|
24
|
+
maxRedirects: 5,
|
|
25
|
+
headers: {
|
|
26
|
+
'User-Agent': USER_AGENT,
|
|
27
|
+
Accept: 'application/pdf,*/*',
|
|
28
|
+
...(options.headers || {})
|
|
29
|
+
},
|
|
30
|
+
validateStatus: status => status < 500
|
|
31
|
+
});
|
|
32
|
+
if (response.status >= 400) {
|
|
33
|
+
const errorBuffer = Buffer.from(response.data || []);
|
|
34
|
+
const snippet = errorBuffer.subarray(0, 240).toString('utf8').replace(/\s+/g, ' ').trim();
|
|
35
|
+
const challenge = response.headers['cf-mitigated'] || /cloudflare|challenge|just a moment/i.test(snippet);
|
|
36
|
+
const reason = challenge ? 'provider returned an HTML anti-bot challenge instead of a PDF' : 'provider refused the request';
|
|
37
|
+
throw new Error(`PDF download failed with HTTP ${response.status}: ${reason}`);
|
|
38
|
+
}
|
|
39
|
+
const buffer = Buffer.from(response.data);
|
|
40
|
+
const contentType = String(response.headers['content-type'] || '').toLowerCase();
|
|
41
|
+
if (!isPdfBuffer(buffer, contentType)) {
|
|
42
|
+
const snippet = buffer.subarray(0, 240).toString('utf8').replace(/\s+/g, ' ').trim();
|
|
43
|
+
const challenge = response.headers['cf-mitigated'] || /cloudflare|challenge|preparing to download|proof-of-work/i.test(snippet);
|
|
44
|
+
const reason = challenge
|
|
45
|
+
? 'the provider returned an HTML challenge page instead of a PDF'
|
|
46
|
+
: `content-type was ${contentType || 'unknown'}`;
|
|
47
|
+
throw new Error(`Resolved URL did not return a PDF (${reason}): ${pdfUrl}`);
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(outputPath, buffer);
|
|
50
|
+
return outputPath;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=PdfDownload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PdfDownload.js","sourceRoot":"","sources":["../../src/utils/PdfDownload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAM9D,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,QAAQ,GAAG,OAAO;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IACvF,OAAO,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,WAAW,GAAG,EAAE;IAC1D,OAAO,WAAW,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,MAAM,CAAC;AAClG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAc,EACd,QAAgB,EAChB,YAAoB,EACpB,UAA8B,EAAE;IAEhC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,2DAA2D,MAAM,EAAE,CAAC,CAAC;IACvF,CAAC;IAED,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAE5E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAc,MAAM,EAAE;QACpD,YAAY,EAAE,aAAa;QAC3B,OAAO,EAAE,QAAQ,CAAC,QAAQ;QAC1B,YAAY,EAAE,CAAC;QACf,OAAO,EAAE;YACP,YAAY,EAAE,UAAU;YACxB,MAAM,EAAE,qBAAqB;YAC7B,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;SAC3B;QACD,cAAc,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,GAAG;KACvC,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QAC3B,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1F,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,qCAAqC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1G,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,+DAA+D,CAAC,CAAC,CAAC,8BAA8B,CAAC;QAC5H,MAAM,IAAI,KAAK,CAAC,iCAAiC,QAAQ,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC;IACjF,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACjF,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACrF,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,2DAA2D,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChI,MAAM,MAAM,GAAG,SAAS;YACtB,CAAC,CAAC,+DAA+D;YACjE,CAAC,CAAC,oBAAoB,WAAW,IAAI,SAAS,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,sCAAsC,MAAM,MAAM,MAAM,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,UAAU,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface QuotaConfig {
|
|
2
|
+
dailyLimit: number;
|
|
3
|
+
envPrefix?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface QuotaStatus {
|
|
6
|
+
platform: string;
|
|
7
|
+
used: number;
|
|
8
|
+
limit: number;
|
|
9
|
+
remaining: number;
|
|
10
|
+
resetAt: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class QuotaExhaustedError extends Error {
|
|
13
|
+
readonly platform: string;
|
|
14
|
+
readonly limit: number;
|
|
15
|
+
readonly resetAt: string;
|
|
16
|
+
constructor(platform: string, limit: number, resetAt: string);
|
|
17
|
+
}
|
|
18
|
+
export declare class QuotaManager {
|
|
19
|
+
private static instance;
|
|
20
|
+
private quotas;
|
|
21
|
+
private constructor();
|
|
22
|
+
static getInstance(): QuotaManager;
|
|
23
|
+
registerPlatform(platform: string, config: QuotaConfig): void;
|
|
24
|
+
checkQuota(platform: string): void;
|
|
25
|
+
incrementUsage(platform: string): void;
|
|
26
|
+
getStatus(platform: string): QuotaStatus | null;
|
|
27
|
+
private resetIfNeeded;
|
|
28
|
+
private getDayKey;
|
|
29
|
+
private getNextResetTime;
|
|
30
|
+
}
|
|
31
|
+
export default QuotaManager;
|
|
32
|
+
//# sourceMappingURL=QuotaManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuotaManager.d.ts","sourceRoot":"","sources":["../../src/utils/QuotaManager.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,QAAQ,EAAE,MAAM;aAChB,KAAK,EAAE,MAAM;aACb,OAAO,EAAE,MAAM;gBAFf,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM;CAKlC;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAe;IACtC,OAAO,CAAC,MAAM,CAA2E;IAEzF,OAAO;IAEP,MAAM,CAAC,WAAW,IAAI,YAAY;IAOlC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAc7D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAmBlC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAetC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAiB/C,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,gBAAgB;CAMzB;AAED,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { logDebug, logWarn } from './Logger.js';
|
|
2
|
+
export class QuotaExhaustedError extends Error {
|
|
3
|
+
platform;
|
|
4
|
+
limit;
|
|
5
|
+
resetAt;
|
|
6
|
+
constructor(platform, limit, resetAt) {
|
|
7
|
+
super(`${platform} daily quota exhausted (${limit}/day). Resets at ${resetAt}`);
|
|
8
|
+
this.platform = platform;
|
|
9
|
+
this.limit = limit;
|
|
10
|
+
this.resetAt = resetAt;
|
|
11
|
+
this.name = 'QuotaExhaustedError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class QuotaManager {
|
|
15
|
+
static instance;
|
|
16
|
+
quotas = new Map();
|
|
17
|
+
constructor() { }
|
|
18
|
+
static getInstance() {
|
|
19
|
+
if (!QuotaManager.instance) {
|
|
20
|
+
QuotaManager.instance = new QuotaManager();
|
|
21
|
+
}
|
|
22
|
+
return QuotaManager.instance;
|
|
23
|
+
}
|
|
24
|
+
registerPlatform(platform, config) {
|
|
25
|
+
const envVar = config.envPrefix ? `${config.envPrefix}_DAILY_LIMIT` : undefined;
|
|
26
|
+
const limitFromEnv = envVar ? Number(process.env[envVar]) : NaN;
|
|
27
|
+
const limit = Number.isFinite(limitFromEnv) && limitFromEnv > 0 ? limitFromEnv : config.dailyLimit;
|
|
28
|
+
this.quotas.set(platform, {
|
|
29
|
+
limit,
|
|
30
|
+
used: 0,
|
|
31
|
+
dayKey: this.getDayKey()
|
|
32
|
+
});
|
|
33
|
+
logDebug(`QuotaManager: Registered ${platform} with daily limit ${limit}`);
|
|
34
|
+
}
|
|
35
|
+
checkQuota(platform) {
|
|
36
|
+
const quota = this.quotas.get(platform);
|
|
37
|
+
if (!quota) {
|
|
38
|
+
logWarn(`QuotaManager: Platform ${platform} not registered`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.resetIfNeeded(platform, quota);
|
|
42
|
+
if (quota.limit <= 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (quota.used >= quota.limit) {
|
|
46
|
+
const resetAt = this.getNextResetTime();
|
|
47
|
+
throw new QuotaExhaustedError(platform, quota.limit, resetAt);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
incrementUsage(platform) {
|
|
51
|
+
const quota = this.quotas.get(platform);
|
|
52
|
+
if (!quota) {
|
|
53
|
+
logWarn(`QuotaManager: Platform ${platform} not registered`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.resetIfNeeded(platform, quota);
|
|
57
|
+
if (quota.limit > 0) {
|
|
58
|
+
quota.used++;
|
|
59
|
+
logDebug(`QuotaManager: ${platform} usage: ${quota.used}/${quota.limit}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
getStatus(platform) {
|
|
63
|
+
const quota = this.quotas.get(platform);
|
|
64
|
+
if (!quota) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
this.resetIfNeeded(platform, quota);
|
|
68
|
+
return {
|
|
69
|
+
platform,
|
|
70
|
+
used: quota.used,
|
|
71
|
+
limit: quota.limit,
|
|
72
|
+
remaining: Math.max(0, quota.limit - quota.used),
|
|
73
|
+
resetAt: this.getNextResetTime()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
resetIfNeeded(platform, quota) {
|
|
77
|
+
const currentKey = this.getDayKey();
|
|
78
|
+
if (currentKey !== quota.dayKey) {
|
|
79
|
+
quota.dayKey = currentKey;
|
|
80
|
+
quota.used = 0;
|
|
81
|
+
logDebug(`QuotaManager: Reset ${platform} quota for new day ${currentKey}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
getDayKey(date = new Date()) {
|
|
85
|
+
return date.toISOString().split('T')[0];
|
|
86
|
+
}
|
|
87
|
+
getNextResetTime() {
|
|
88
|
+
const tomorrow = new Date();
|
|
89
|
+
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
|
|
90
|
+
tomorrow.setUTCHours(0, 0, 0, 0);
|
|
91
|
+
return tomorrow.toISOString();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export default QuotaManager;
|
|
95
|
+
//# sourceMappingURL=QuotaManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuotaManager.js","sourceRoot":"","sources":["../../src/utils/QuotaManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求速率限制器
|
|
3
|
+
* 用于控制API请求频率,遵守各平台的使用限制
|
|
4
|
+
*/
|
|
5
|
+
export interface RateLimiterOptions {
|
|
6
|
+
/** 每秒最大请求数 */
|
|
7
|
+
requestsPerSecond: number;
|
|
8
|
+
/** 突发请求容量 */
|
|
9
|
+
burstCapacity?: number;
|
|
10
|
+
/** 是否启用调试日志 */
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class RateLimiter {
|
|
14
|
+
private readonly requestsPerSecond;
|
|
15
|
+
private readonly intervalMs;
|
|
16
|
+
private readonly burstCapacity;
|
|
17
|
+
private readonly debug;
|
|
18
|
+
private tokens;
|
|
19
|
+
private lastRefill;
|
|
20
|
+
private intervalHandle;
|
|
21
|
+
private readonly pendingRequests;
|
|
22
|
+
constructor(options: RateLimiterOptions);
|
|
23
|
+
/**
|
|
24
|
+
* 等待直到可以发送请求
|
|
25
|
+
*/
|
|
26
|
+
waitForPermission(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* 补充令牌(令牌桶算法)
|
|
29
|
+
*/
|
|
30
|
+
private refillTokens;
|
|
31
|
+
/**
|
|
32
|
+
* 处理等待中的请求
|
|
33
|
+
*/
|
|
34
|
+
private processPendingRequests;
|
|
35
|
+
/**
|
|
36
|
+
* 获取当前状态
|
|
37
|
+
*/
|
|
38
|
+
getStatus(): {
|
|
39
|
+
availableTokens: number;
|
|
40
|
+
maxTokens: number;
|
|
41
|
+
requestsPerSecond: number;
|
|
42
|
+
pendingRequests: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* 清理过期的等待请求(超过30秒)
|
|
46
|
+
*/
|
|
47
|
+
cleanup(): void;
|
|
48
|
+
dispose(): void;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=RateLimiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../../src/utils/RateLimiter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,kBAAkB;IACjC,cAAc;IACd,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;IAEhC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAGxB;gBAEI,OAAO,EAAE,kBAAkB;IAevC;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBxC;;OAEG;IACH,OAAO,CAAC,YAAY;IAepB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAiB9B;;OAEG;IACH,SAAS,IAAI;QACX,eAAe,EAAE,MAAM,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;QAClB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;KACzB;IAUD;;OAEG;IACH,OAAO,IAAI,IAAI;IAsBf,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求速率限制器
|
|
3
|
+
* 用于控制API请求频率,遵守各平台的使用限制
|
|
4
|
+
*/
|
|
5
|
+
import { logDebug } from './Logger.js';
|
|
6
|
+
export class RateLimiter {
|
|
7
|
+
requestsPerSecond;
|
|
8
|
+
intervalMs;
|
|
9
|
+
burstCapacity;
|
|
10
|
+
debug;
|
|
11
|
+
tokens;
|
|
12
|
+
lastRefill;
|
|
13
|
+
intervalHandle;
|
|
14
|
+
pendingRequests = [];
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.requestsPerSecond = options.requestsPerSecond;
|
|
17
|
+
this.intervalMs = 1000 / this.requestsPerSecond;
|
|
18
|
+
this.burstCapacity = options.burstCapacity || this.requestsPerSecond;
|
|
19
|
+
this.debug = options.debug || false;
|
|
20
|
+
this.tokens = this.burstCapacity;
|
|
21
|
+
this.lastRefill = Date.now();
|
|
22
|
+
// 定期处理等待中的请求
|
|
23
|
+
this.intervalHandle = setInterval(() => this.processPendingRequests(), Math.min(this.intervalMs, 100));
|
|
24
|
+
// Don't keep the process alive just because of the limiter interval.
|
|
25
|
+
this.intervalHandle.unref?.();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 等待直到可以发送请求
|
|
29
|
+
*/
|
|
30
|
+
async waitForPermission() {
|
|
31
|
+
this.refillTokens();
|
|
32
|
+
if (this.tokens > 0) {
|
|
33
|
+
this.tokens--;
|
|
34
|
+
if (this.debug) {
|
|
35
|
+
logDebug(`RateLimiter: Request allowed, ${this.tokens} tokens remaining`);
|
|
36
|
+
}
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
// 没有可用令牌,加入等待队列
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
this.pendingRequests.push({
|
|
42
|
+
resolve,
|
|
43
|
+
timestamp: Date.now()
|
|
44
|
+
});
|
|
45
|
+
if (this.debug) {
|
|
46
|
+
logDebug(`RateLimiter: Request queued, ${this.pendingRequests.length} waiting`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 补充令牌(令牌桶算法)
|
|
52
|
+
*/
|
|
53
|
+
refillTokens() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const timePassed = now - this.lastRefill;
|
|
56
|
+
if (timePassed >= this.intervalMs) {
|
|
57
|
+
const tokensToAdd = Math.floor(timePassed / this.intervalMs);
|
|
58
|
+
this.tokens = Math.min(this.burstCapacity, this.tokens + tokensToAdd);
|
|
59
|
+
this.lastRefill = now;
|
|
60
|
+
if (this.debug && tokensToAdd > 0) {
|
|
61
|
+
logDebug(`RateLimiter: Added ${tokensToAdd} tokens, total: ${this.tokens}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 处理等待中的请求
|
|
67
|
+
*/
|
|
68
|
+
processPendingRequests() {
|
|
69
|
+
this.refillTokens();
|
|
70
|
+
while (this.tokens > 0 && this.pendingRequests.length > 0) {
|
|
71
|
+
const request = this.pendingRequests.shift();
|
|
72
|
+
if (request) {
|
|
73
|
+
this.tokens--;
|
|
74
|
+
request.resolve();
|
|
75
|
+
if (this.debug) {
|
|
76
|
+
const waitTime = Date.now() - request.timestamp;
|
|
77
|
+
logDebug(`RateLimiter: Released waiting request (waited ${waitTime}ms), ${this.tokens} tokens remaining`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 获取当前状态
|
|
84
|
+
*/
|
|
85
|
+
getStatus() {
|
|
86
|
+
this.refillTokens();
|
|
87
|
+
return {
|
|
88
|
+
availableTokens: this.tokens,
|
|
89
|
+
maxTokens: this.burstCapacity,
|
|
90
|
+
requestsPerSecond: this.requestsPerSecond,
|
|
91
|
+
pendingRequests: this.pendingRequests.length
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 清理过期的等待请求(超过30秒)
|
|
96
|
+
*/
|
|
97
|
+
cleanup() {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const timeoutMs = 30000; // 30秒超时
|
|
100
|
+
let removedCount = 0;
|
|
101
|
+
while (this.pendingRequests.length > 0) {
|
|
102
|
+
const first = this.pendingRequests[0];
|
|
103
|
+
if (now - first.timestamp > timeoutMs) {
|
|
104
|
+
this.pendingRequests.shift();
|
|
105
|
+
// 拒绝过期的请求
|
|
106
|
+
first.resolve(); // 或者可以reject,但这里选择允许继续
|
|
107
|
+
removedCount++;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (this.debug && removedCount > 0) {
|
|
114
|
+
logDebug(`RateLimiter: Cleaned up ${removedCount} expired requests`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
dispose() {
|
|
118
|
+
clearInterval(this.intervalHandle);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=RateLimiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../../src/utils/RateLimiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface CacheOptions {
|
|
2
|
+
maxSize?: number;
|
|
3
|
+
ttlMs?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface CacheStats {
|
|
6
|
+
size: number;
|
|
7
|
+
maxSize: number;
|
|
8
|
+
hits: number;
|
|
9
|
+
misses: number;
|
|
10
|
+
hitRate: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class RequestCache<T = any> {
|
|
13
|
+
private cache;
|
|
14
|
+
private hits;
|
|
15
|
+
private misses;
|
|
16
|
+
constructor(options?: CacheOptions);
|
|
17
|
+
generateKey(platform: string, query: string, options?: Record<string, any>): string;
|
|
18
|
+
get(key: string): T | undefined;
|
|
19
|
+
set(key: string, value: T): void;
|
|
20
|
+
has(key: string): boolean;
|
|
21
|
+
delete(key: string): boolean;
|
|
22
|
+
clear(): void;
|
|
23
|
+
getStats(): CacheStats;
|
|
24
|
+
}
|
|
25
|
+
export default RequestCache;
|
|
26
|
+
//# sourceMappingURL=RequestCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequestCache.d.ts","sourceRoot":"","sources":["../../src/utils/RequestCache.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,YAAY,CAAC,CAAC,GAAG,GAAG;IAC/B,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,MAAM,CAAa;gBAEf,OAAO,GAAE,YAAiB;IActC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM;IASnF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAY/B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAKhC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI5B,KAAK,IAAI,IAAI;IAOb,QAAQ,IAAI,UAAU;CAUvB;AAED,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { logDebug } from './Logger.js';
|
|
4
|
+
export class RequestCache {
|
|
5
|
+
cache;
|
|
6
|
+
hits = 0;
|
|
7
|
+
misses = 0;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
const maxSize = options.maxSize || 100;
|
|
10
|
+
const ttlMs = options.ttlMs || 3600000; // 1 hour default
|
|
11
|
+
this.cache = new LRUCache({
|
|
12
|
+
max: maxSize,
|
|
13
|
+
ttl: ttlMs,
|
|
14
|
+
updateAgeOnGet: true,
|
|
15
|
+
updateAgeOnHas: false
|
|
16
|
+
});
|
|
17
|
+
logDebug(`RequestCache initialized: maxSize=${maxSize}, ttl=${ttlMs}ms`);
|
|
18
|
+
}
|
|
19
|
+
generateKey(platform, query, options) {
|
|
20
|
+
const data = JSON.stringify({
|
|
21
|
+
platform,
|
|
22
|
+
query: query.toLowerCase().trim(),
|
|
23
|
+
options: options || {}
|
|
24
|
+
});
|
|
25
|
+
return createHash('sha256').update(data).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
get(key) {
|
|
28
|
+
const value = this.cache.get(key);
|
|
29
|
+
if (value !== undefined) {
|
|
30
|
+
this.hits++;
|
|
31
|
+
logDebug(`Cache hit: ${key.substring(0, 8)}...`);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
this.misses++;
|
|
35
|
+
logDebug(`Cache miss: ${key.substring(0, 8)}...`);
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
set(key, value) {
|
|
39
|
+
this.cache.set(key, value);
|
|
40
|
+
logDebug(`Cache set: ${key.substring(0, 8)}...`);
|
|
41
|
+
}
|
|
42
|
+
has(key) {
|
|
43
|
+
return this.cache.has(key);
|
|
44
|
+
}
|
|
45
|
+
delete(key) {
|
|
46
|
+
return this.cache.delete(key);
|
|
47
|
+
}
|
|
48
|
+
clear() {
|
|
49
|
+
this.cache.clear();
|
|
50
|
+
this.hits = 0;
|
|
51
|
+
this.misses = 0;
|
|
52
|
+
logDebug('Cache cleared');
|
|
53
|
+
}
|
|
54
|
+
getStats() {
|
|
55
|
+
const total = this.hits + this.misses;
|
|
56
|
+
return {
|
|
57
|
+
size: this.cache.size,
|
|
58
|
+
maxSize: this.cache.max,
|
|
59
|
+
hits: this.hits,
|
|
60
|
+
misses: this.misses,
|
|
61
|
+
hitRate: total > 0 ? this.hits / total : 0
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export default RequestCache;
|
|
66
|
+
//# sourceMappingURL=RequestCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequestCache.js","sourceRoot":"","sources":["../../src/utils/RequestCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAevC,MAAM,OAAO,YAAY;IACf,KAAK,CAAM;IACX,IAAI,GAAW,CAAC,CAAC;IACjB,MAAM,GAAW,CAAC,CAAC;IAE3B,YAAY,UAAwB,EAAE;QACpC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,GAAG,CAAC;QACvC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,CAAC,iBAAiB;QAEzD,IAAI,CAAC,KAAK,GAAG,IAAI,QAAQ,CAAC;YACxB,GAAG,EAAE,OAAO;YACZ,GAAG,EAAE,KAAK;YACV,cAAc,EAAE,IAAI;YACpB,cAAc,EAAE,KAAK;SACtB,CAAC,CAAC;QAEH,QAAQ,CAAC,qCAAqC,OAAO,SAAS,KAAK,IAAI,CAAC,CAAC;IAC3E,CAAC;IAED,WAAW,CAAC,QAAgB,EAAE,KAAa,EAAE,OAA6B;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;YAC1B,QAAQ;YACR,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE;YACjC,OAAO,EAAE,OAAO,IAAI,EAAE;SACvB,CAAC,CAAC;QACH,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC;IAED,GAAG,CAAC,GAAW;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,QAAQ,CAAC,cAAc,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;YACjD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,QAAQ,CAAC,eAAe,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAClD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAQ;QACvB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3B,QAAQ,CAAC,cAAc,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;IACnD,CAAC;IAED,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChB,QAAQ,CAAC,eAAe,CAAC,CAAC;IAC5B,CAAC;IAED,QAAQ;QACN,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;QACtC,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI;YACrB,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG;YACvB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,OAAO,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;SAC3C,CAAC;IACJ,CAAC;CACF;AAED,eAAe,YAAY,CAAC"}
|