nodebb-plugin-pdf-secure2 1.3.6 → 1.3.8

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.
@@ -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
 
@@ -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,104 @@ GeminiChat.isAvailable = function () {
112
121
  return !!ai;
113
122
  };
114
123
 
115
- // Read PDF and cache base64 in memory
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
+ // Upload to Gemini File API — file is stored server-side, only URI is sent per request
144
+ console.log('[PDF-Secure] === FILE API UPLOAD START ===');
145
+ console.log('[PDF-Secure] Filename:', filename);
146
+ console.log('[PDF-Secure] FilePath:', filePath);
147
+ console.log('[PDF-Secure] FileSize:', stats.size, 'bytes');
148
+ console.log('[PDF-Secure] ai.files exists:', !!ai.files);
149
+ console.log('[PDF-Secure] ai.files.upload exists:', typeof (ai.files && ai.files.upload));
150
+
151
+ const fileBuffer = await fs.promises.readFile(filePath);
152
+ console.log('[PDF-Secure] File read OK, buffer size:', fileBuffer.length);
153
+
154
+ // Try upload with multiple approaches
155
+ let uploadResult;
156
+ const uploadErrors = [];
157
+
158
+ // Approach 1: file path string
159
+ console.log('[PDF-Secure] Trying approach 1: file path string...');
160
+ try {
161
+ uploadResult = await ai.files.upload({
162
+ file: filePath,
163
+ config: { mimeType: 'application/pdf', displayName: filename },
164
+ });
165
+ console.log('[PDF-Secure] Approach 1 SUCCESS! Result:', JSON.stringify(uploadResult).slice(0, 500));
166
+ } catch (err1) {
167
+ console.error('[PDF-Secure] Approach 1 FAILED:', err1.message);
168
+ console.error('[PDF-Secure] Approach 1 error detail:', err1.status || '', err1.code || '', err1.stack?.split('\n').slice(0, 3).join(' | '));
169
+ uploadErrors.push('path: ' + err1.message);
170
+ }
171
+
172
+ // Approach 2: Blob (Node 18+)
173
+ if (!uploadResult) {
174
+ console.log('[PDF-Secure] Trying approach 2: Blob...');
175
+ console.log('[PDF-Secure] Blob available:', typeof Blob !== 'undefined');
176
+ try {
177
+ const blob = new Blob([fileBuffer], { type: 'application/pdf' });
178
+ uploadResult = await ai.files.upload({
179
+ file: blob,
180
+ config: { mimeType: 'application/pdf', displayName: filename },
181
+ });
182
+ console.log('[PDF-Secure] Approach 2 SUCCESS! Result:', JSON.stringify(uploadResult).slice(0, 500));
183
+ } catch (err2) {
184
+ console.error('[PDF-Secure] Approach 2 FAILED:', err2.message);
185
+ uploadErrors.push('blob: ' + err2.message);
186
+ }
187
+ }
188
+
189
+ // Approach 3: Buffer directly
190
+ if (!uploadResult) {
191
+ console.log('[PDF-Secure] Trying approach 3: Buffer...');
192
+ try {
193
+ uploadResult = await ai.files.upload({
194
+ file: fileBuffer,
195
+ config: { mimeType: 'application/pdf', displayName: filename },
196
+ });
197
+ console.log('[PDF-Secure] Approach 3 SUCCESS! Result:', JSON.stringify(uploadResult).slice(0, 500));
198
+ } catch (err3) {
199
+ console.error('[PDF-Secure] Approach 3 FAILED:', err3.message);
200
+ uploadErrors.push('buffer: ' + err3.message);
201
+ }
202
+ }
203
+
204
+ if (uploadResult && uploadResult.uri) {
205
+ const fileUri = uploadResult.uri;
206
+ const mimeType = uploadResult.mimeType || 'application/pdf';
207
+ console.log('[PDF-Secure] === UPLOAD FINAL: SUCCESS ===', fileUri);
208
+
209
+ fileUploadCache.set(filename, { fileUri, mimeType, cachedAt: Date.now() });
210
+ return { type: 'fileData', fileUri, mimeType };
211
+ }
212
+
213
+ // All upload methods failed
214
+ console.error('[PDF-Secure] === UPLOAD FINAL: ALL FAILED ===');
215
+ console.error('[PDF-Secure] Errors:', uploadErrors.join(' | '));
216
+ console.error('[PDF-Secure] Falling back to inline base64 (will use many tokens!)');
217
+ const base64 = fileBuffer.toString('base64');
218
+ return { type: 'inlineData', mimeType: 'application/pdf', data: base64 };
219
+ }
220
+
221
+ // Read PDF and cache base64 in memory (fallback for File API failure)
116
222
  async function getPdfBase64(filename) {
117
223
  const cached = pdfDataCache.get(filename);
118
224
  if (cached && Date.now() - cached.cachedAt < PDF_DATA_TTL) {
@@ -142,7 +248,7 @@ GeminiChat.chat = async function (filename, question, history, tier) {
142
248
  }
143
249
 
144
250
  const config = TIER_CONFIG[tier] || TIER_CONFIG.premium;
145
- const base64Data = await getPdfBase64(filename);
251
+ const pdfRef = await getOrUploadPdf(filename);
146
252
 
147
253
  // Build conversation contents from history (trimmed to last N entries)
148
254
  const contents = [];
@@ -170,12 +276,16 @@ GeminiChat.chat = async function (filename, question, history, tier) {
170
276
  parts: [{ text: sanitizeHistoryText(question) }],
171
277
  });
172
278
 
173
- // Always use inline PDF single API call, no upload/cache overhead
174
- const inlineContents = [
279
+ // Build PDF partFile API reference (lightweight) or inline base64 (fallback)
280
+ const pdfPart = pdfRef.type === 'fileData'
281
+ ? { fileData: { fileUri: pdfRef.fileUri, mimeType: pdfRef.mimeType } }
282
+ : { inlineData: { mimeType: pdfRef.mimeType, data: pdfRef.data } };
283
+
284
+ const fullContents = [
175
285
  {
176
286
  role: 'user',
177
287
  parts: [
178
- { inlineData: { mimeType: 'application/pdf', data: base64Data } },
288
+ pdfPart,
179
289
  { text: 'I am sharing a PDF document with you. Please use it to answer my questions.' },
180
290
  ],
181
291
  },
@@ -188,7 +298,7 @@ GeminiChat.chat = async function (filename, question, history, tier) {
188
298
 
189
299
  const response = await ai.models.generateContent({
190
300
  model: MODEL_NAME,
191
- contents: inlineContents,
301
+ contents: fullContents,
192
302
  config: {
193
303
  systemInstruction: SYSTEM_INSTRUCTION,
194
304
  maxOutputTokens: config.maxOutputTokens,
@@ -215,7 +325,10 @@ GeminiChat.generateSuggestions = async function (filename) {
215
325
  return cached.suggestions;
216
326
  }
217
327
 
218
- const base64Data = await getPdfBase64(filename);
328
+ const pdfRef = await getOrUploadPdf(filename);
329
+ const pdfPart = pdfRef.type === 'fileData'
330
+ ? { fileData: { fileUri: pdfRef.fileUri, mimeType: pdfRef.mimeType } }
331
+ : { inlineData: { mimeType: pdfRef.mimeType, data: pdfRef.data } };
219
332
 
220
333
  const response = await ai.models.generateContent({
221
334
  model: MODEL_NAME,
@@ -223,7 +336,7 @@ GeminiChat.generateSuggestions = async function (filename) {
223
336
  {
224
337
  role: 'user',
225
338
  parts: [
226
- { inlineData: { mimeType: 'application/pdf', data: base64Data } },
339
+ pdfPart,
227
340
  { 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
341
  ],
229
342
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure2",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
Binary file
package/image.png DELETED
Binary file
package/static/image.png DELETED
Binary file