nodebb-plugin-pdf-secure2 1.3.6 → 1.3.7
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/lib/controllers.js +6 -5
- package/lib/gemini-chat.js +66 -10
- package/package.json +1 -1
- package/image.png +0 -0
package/lib/controllers.js
CHANGED
|
@@ -185,20 +185,21 @@ Controllers.handleChat = async function (req, res) {
|
|
|
185
185
|
});
|
|
186
186
|
} catch (err) {
|
|
187
187
|
console.error('[PDF-Secure] Chat error:', err.message, err.status || '', err.code || '');
|
|
188
|
+
const quota = { used: rateResult.used, max: rateResult.max };
|
|
188
189
|
|
|
189
190
|
if (err.message === 'File not found') {
|
|
190
|
-
return res.status(404).json({ error: 'PDF bulunamadı.' });
|
|
191
|
+
return res.status(404).json({ error: 'PDF bulunamadı.', quota });
|
|
191
192
|
}
|
|
192
193
|
if (err.message === 'PDF too large for AI chat') {
|
|
193
|
-
return res.status(413).json({ error: 'Bu PDF çok büyük. AI chat desteklemiyor.' });
|
|
194
|
+
return res.status(413).json({ error: 'Bu PDF çok büyük. AI chat desteklemiyor.', quota });
|
|
194
195
|
}
|
|
195
196
|
if (err.status === 429 || err.message.includes('rate limit') || err.message.includes('quota')) {
|
|
196
|
-
return res.status(429).json({ error: 'AI servisi şu an yoğun. Lütfen birkaç saniye sonra tekrar deneyin.' });
|
|
197
|
+
return res.status(429).json({ error: 'AI servisi şu an yoğun. Lütfen birkaç saniye sonra tekrar deneyin.', quota });
|
|
197
198
|
}
|
|
198
199
|
if (err.status === 401 || err.status === 403 || err.message.includes('API key')) {
|
|
199
|
-
return res.status(503).json({ error: 'AI servisi yapılandırma hatası. Yöneticiyle iletişime geçin.' });
|
|
200
|
+
return res.status(503).json({ error: 'AI servisi yapılandırma hatası. Yöneticiyle iletişime geçin.', quota });
|
|
200
201
|
}
|
|
201
|
-
return res.status(500).json({ error: 'AI yanıt veremedi. Lütfen tekrar deneyin.' });
|
|
202
|
+
return res.status(500).json({ error: 'AI yanıt veremedi. Lütfen tekrar deneyin.', quota });
|
|
202
203
|
}
|
|
203
204
|
};
|
|
204
205
|
|
package/lib/gemini-chat.js
CHANGED
|
@@ -7,10 +7,14 @@ const GeminiChat = module.exports;
|
|
|
7
7
|
|
|
8
8
|
let ai = null;
|
|
9
9
|
|
|
10
|
-
// In-memory cache for PDF base64 data (avoids re-reading from disk)
|
|
10
|
+
// In-memory cache for PDF base64 data (fallback, avoids re-reading from disk)
|
|
11
11
|
const pdfDataCache = new Map(); // filename -> { base64, cachedAt }
|
|
12
12
|
const PDF_DATA_TTL = 30 * 60 * 1000; // 30 minutes
|
|
13
13
|
|
|
14
|
+
// Gemini File API upload cache — upload once, reference many times
|
|
15
|
+
// Saves ~3M+ tokens per request by not sending inline base64 every time
|
|
16
|
+
const fileUploadCache = new Map(); // filename -> { fileUri, mimeType, cachedAt }
|
|
17
|
+
|
|
14
18
|
const SYSTEM_INSTRUCTION = `Sen bir PDF döküman asistanısın. Kurallar:
|
|
15
19
|
- Kullanıcının yazdığı dilde cevap ver
|
|
16
20
|
- Önce kısa ve net cevapla, gerekirse detay ekle
|
|
@@ -82,7 +86,7 @@ const TIER_CONFIG = {
|
|
|
82
86
|
|
|
83
87
|
const MODEL_NAME = 'gemini-2.5-flash';
|
|
84
88
|
|
|
85
|
-
// Periodic cleanup
|
|
89
|
+
// Periodic cleanup of all caches
|
|
86
90
|
const cleanupTimer = setInterval(() => {
|
|
87
91
|
const now = Date.now();
|
|
88
92
|
for (const [key, entry] of pdfDataCache.entries()) {
|
|
@@ -90,6 +94,11 @@ const cleanupTimer = setInterval(() => {
|
|
|
90
94
|
pdfDataCache.delete(key);
|
|
91
95
|
}
|
|
92
96
|
}
|
|
97
|
+
for (const [key, entry] of fileUploadCache.entries()) {
|
|
98
|
+
if (now - entry.cachedAt > PDF_DATA_TTL) {
|
|
99
|
+
fileUploadCache.delete(key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
93
102
|
}, 10 * 60 * 1000);
|
|
94
103
|
cleanupTimer.unref();
|
|
95
104
|
|
|
@@ -112,7 +121,47 @@ GeminiChat.isAvailable = function () {
|
|
|
112
121
|
return !!ai;
|
|
113
122
|
};
|
|
114
123
|
|
|
115
|
-
//
|
|
124
|
+
// Upload PDF to Gemini File API (or return cached reference)
|
|
125
|
+
// Returns { type: 'fileData', fileUri, mimeType } or { type: 'inlineData', mimeType, data }
|
|
126
|
+
async function getOrUploadPdf(filename) {
|
|
127
|
+
// Check file upload cache first
|
|
128
|
+
const cached = fileUploadCache.get(filename);
|
|
129
|
+
if (cached && Date.now() - cached.cachedAt < PDF_DATA_TTL) {
|
|
130
|
+
return { type: 'fileData', fileUri: cached.fileUri, mimeType: cached.mimeType };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const filePath = pdfHandler.resolveFilePath(filename);
|
|
134
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
135
|
+
throw new Error('File not found');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const stats = await fs.promises.stat(filePath);
|
|
139
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
140
|
+
throw new Error('PDF too large for AI chat');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Upload to Gemini File API — file is stored server-side, only URI is sent per request
|
|
145
|
+
const uploadResult = await ai.files.upload({
|
|
146
|
+
file: filePath,
|
|
147
|
+
config: { mimeType: 'application/pdf' },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const fileUri = uploadResult.uri;
|
|
151
|
+
const mimeType = uploadResult.mimeType || 'application/pdf';
|
|
152
|
+
console.log('[PDF-Secure] File API upload OK:', filename, '→', fileUri);
|
|
153
|
+
|
|
154
|
+
fileUploadCache.set(filename, { fileUri, mimeType, cachedAt: Date.now() });
|
|
155
|
+
return { type: 'fileData', fileUri, mimeType };
|
|
156
|
+
} catch (uploadErr) {
|
|
157
|
+
// Fallback: use inline base64 (works but uses many tokens)
|
|
158
|
+
console.warn('[PDF-Secure] File API upload failed, falling back to inline:', uploadErr.message);
|
|
159
|
+
const base64 = await getPdfBase64(filename);
|
|
160
|
+
return { type: 'inlineData', mimeType: 'application/pdf', data: base64 };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Read PDF and cache base64 in memory (fallback for File API failure)
|
|
116
165
|
async function getPdfBase64(filename) {
|
|
117
166
|
const cached = pdfDataCache.get(filename);
|
|
118
167
|
if (cached && Date.now() - cached.cachedAt < PDF_DATA_TTL) {
|
|
@@ -142,7 +191,7 @@ GeminiChat.chat = async function (filename, question, history, tier) {
|
|
|
142
191
|
}
|
|
143
192
|
|
|
144
193
|
const config = TIER_CONFIG[tier] || TIER_CONFIG.premium;
|
|
145
|
-
const
|
|
194
|
+
const pdfRef = await getOrUploadPdf(filename);
|
|
146
195
|
|
|
147
196
|
// Build conversation contents from history (trimmed to last N entries)
|
|
148
197
|
const contents = [];
|
|
@@ -170,12 +219,16 @@ GeminiChat.chat = async function (filename, question, history, tier) {
|
|
|
170
219
|
parts: [{ text: sanitizeHistoryText(question) }],
|
|
171
220
|
});
|
|
172
221
|
|
|
173
|
-
//
|
|
174
|
-
const
|
|
222
|
+
// Build PDF part — File API reference (lightweight) or inline base64 (fallback)
|
|
223
|
+
const pdfPart = pdfRef.type === 'fileData'
|
|
224
|
+
? { fileData: { fileUri: pdfRef.fileUri, mimeType: pdfRef.mimeType } }
|
|
225
|
+
: { inlineData: { mimeType: pdfRef.mimeType, data: pdfRef.data } };
|
|
226
|
+
|
|
227
|
+
const fullContents = [
|
|
175
228
|
{
|
|
176
229
|
role: 'user',
|
|
177
230
|
parts: [
|
|
178
|
-
|
|
231
|
+
pdfPart,
|
|
179
232
|
{ text: 'I am sharing a PDF document with you. Please use it to answer my questions.' },
|
|
180
233
|
],
|
|
181
234
|
},
|
|
@@ -188,7 +241,7 @@ GeminiChat.chat = async function (filename, question, history, tier) {
|
|
|
188
241
|
|
|
189
242
|
const response = await ai.models.generateContent({
|
|
190
243
|
model: MODEL_NAME,
|
|
191
|
-
contents:
|
|
244
|
+
contents: fullContents,
|
|
192
245
|
config: {
|
|
193
246
|
systemInstruction: SYSTEM_INSTRUCTION,
|
|
194
247
|
maxOutputTokens: config.maxOutputTokens,
|
|
@@ -215,7 +268,10 @@ GeminiChat.generateSuggestions = async function (filename) {
|
|
|
215
268
|
return cached.suggestions;
|
|
216
269
|
}
|
|
217
270
|
|
|
218
|
-
const
|
|
271
|
+
const pdfRef = await getOrUploadPdf(filename);
|
|
272
|
+
const pdfPart = pdfRef.type === 'fileData'
|
|
273
|
+
? { fileData: { fileUri: pdfRef.fileUri, mimeType: pdfRef.mimeType } }
|
|
274
|
+
: { inlineData: { mimeType: pdfRef.mimeType, data: pdfRef.data } };
|
|
219
275
|
|
|
220
276
|
const response = await ai.models.generateContent({
|
|
221
277
|
model: MODEL_NAME,
|
|
@@ -223,7 +279,7 @@ GeminiChat.generateSuggestions = async function (filename) {
|
|
|
223
279
|
{
|
|
224
280
|
role: 'user',
|
|
225
281
|
parts: [
|
|
226
|
-
|
|
282
|
+
pdfPart,
|
|
227
283
|
{ text: 'Bu PDF dokümanı için kullanıcının sorabileceği 5 adet akıllı ve spesifik soru öner. Soruları JSON array formatında döndür, başka hiçbir şey yazma. Örnek: ["Soru 1?", "Soru 2?"]' },
|
|
228
284
|
],
|
|
229
285
|
},
|
package/package.json
CHANGED
package/image.png
DELETED
|
Binary file
|