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.
- package/lib/controllers.js +6 -5
- package/lib/gemini-chat.js +123 -10
- package/package.json +1 -1
- package/static/lib/image.png +0 -0
- package/image.png +0 -0
- package/static/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,104 @@ 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
|
+
// 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
|
|
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
|
-
//
|
|
174
|
-
const
|
|
279
|
+
// Build PDF part — File 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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
Binary file
|
package/image.png
DELETED
|
Binary file
|
package/static/image.png
DELETED
|
Binary file
|