feat: add POST /api/documents/register-local for extension import

Accepts pre-extracted text (no raw file) from the AyrisLegal Electron
app's local OCR pipeline, matches/creates a case by title, and runs
the existing AI analysis flow. Also fixes a bug in uploadDocument:
officeparser@7.x's parseOffice() now returns a result object with
.toText() instead of a plain string, which was breaking PDF/DOCX
manual uploads.
This commit is contained in:
mstfyldz
2026-08-09 11:26:36 +03:00
parent d213d68e0f
commit 67b0398b8c
2 changed files with 129 additions and 45 deletions
+127 -44
View File
@@ -5,6 +5,128 @@ import * as officeParser from 'officeparser';
import Tesseract from 'tesseract.js';
const heicConvert = require('heic-convert');
const ANALYSIS_SYSTEM_PROMPT = `
Sen uzman bir Türk avukatısın. Sana verilen dava dosyasını/belgeyi okuyup analiz edeceksin.
Çıktıyı KESİNLİKLE geçerli bir JSON formatında vermelisin. JSON şu anahtarları içermelidir:
- "taraflar": (string) Davacı ve Davalı bilgileri
- "tarihler": (string) Belgedeki kritik tarihler (Dava tarihi, tebliğ vb.)
- "hukukiDayanak": (string) Belgedeki ilgili kanun maddeleri ve hukuki temeller
- "ilkDegerlendirme": (string) Dosyaya dair ilk stratejik ve hukuki değerlendirmen
Sadece ve sadece JSON döndür, başka hiçbir açıklama ekleme.
`;
// uploadDocument ve registerLocalDocument'in ortak kullandığı AI analiz adımı:
// çıkarılmış metni modele gönderip yapılandırılmış özet JSON'u üretir.
async function runDocumentAnalysis(extractedText: string): Promise<{ summaryJson: any; aiModel: string }> {
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
const response = await fetch(`${runpodUrl}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: aiModel,
messages: [
{ role: 'system', content: ANALYSIS_SYSTEM_PROMPT },
{ role: 'user', content: extractedText.substring(0, 15000) }
],
think: false,
stream: false,
}),
});
if (!response.ok) {
throw new Error(`AI Model request failed: ${response.statusText}`);
}
const data = await response.json();
const aiContent = data.message?.content || (data.choices && data.choices[0]?.message?.content);
let summaryJson: any = {};
try {
summaryJson = JSON.parse(aiContent || '{}');
} catch (e) {
console.error('Failed to parse AI response as JSON:', aiContent);
summaryJson = { error: 'AI did not return valid JSON', raw: aiContent };
}
return { summaryJson, aiModel };
}
// UYAP Evrak İndirici eklentisinden gelen, Electron ana sürecinde YERELDE metni
// çıkarılmış belgeleri kaydeder. Ham dosya baytları bu isteğe hiç dahil değil —
// sadece çıkarılan metin (JSON) geliyor, Supabase Storage'a hiçbir yükleme yapılmıyor.
export const registerLocalDocument = async (req: AuthenticatedRequest, res: Response) => {
try {
const userId = req.user?.id;
const { case_title, filename, extracted_text } = req.body || {};
if (!case_title || !filename || typeof extracted_text !== 'string') {
return res.status(400).json({ error: 'case_title, filename ve extracted_text zorunludur.' });
}
// Dava başlığına göre mevcut case'i bul (case-insensitive), yoksa oluştur.
const { data: existing } = await supabase
.from('cases')
.select('id, title')
.eq('user_id', userId)
.ilike('title', case_title)
.limit(1)
.maybeSingle();
let caseId: string;
if (existing) {
caseId = existing.id;
} else {
const { data: newCase, error: caseError } = await supabase
.from('cases')
.insert([{ title: String(case_title).slice(0, 200), user_id: userId }])
.select('id')
.single();
if (caseError) throw caseError;
caseId = newCase.id;
}
const { data: documentRecord, error: docError } = await supabase
.from('documents')
.insert([{
case_id: caseId,
user_id: userId,
storage_path: 'local', // ham dosya sadece kullanıcının cihazında; Storage'da bir karşılığı yok
filename,
mime_type: null,
file_size: extracted_text.length,
ocr_status: extracted_text ? 'done' : 'failed',
extracted_text,
}])
.select()
.single();
if (docError) throw docError;
let analysisRecord = null;
if (extracted_text.trim()) {
try {
const { summaryJson, aiModel } = await runDocumentAnalysis(extracted_text);
const { data: analysis, error: analysisError } = await supabase
.from('analyses')
.insert([{ case_id: caseId, document_id: documentRecord.id, summary_json: summaryJson, model_used: aiModel }])
.select()
.single();
if (analysisError) throw analysisError;
analysisRecord = analysis;
} catch (analysisErr) {
// Analiz başarısız olsa da belge zaten kaydedildi — sohbet daha sonra
// extracted_text'i yine de kullanabilir.
console.error('Local Document Analysis Error:', analysisErr);
}
}
res.json({ case_id: caseId, document: documentRecord, analysis: analysisRecord });
} catch (error: any) {
console.error('Register Local Document Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => {
try {
const file = req.file;
@@ -39,7 +161,10 @@ export const uploadDocument = async (req: AuthenticatedRequest, res: Response) =
} else {
try {
// officeParser natively supports pdf, docx, doc, rtf, txt, etc.
extractedText = (await officeParser.parseOffice(file.buffer)) as unknown as string;
// officeparser@7.x parseOffice() bir sonuc objesi donduruyor (duz string degil),
// duz metin icin .toText() cagirmak gerekiyor.
const result: any = await officeParser.parseOffice(file.buffer);
extractedText = typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || ''));
} catch (err: any) {
console.error('officeParser Error:', err);
return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' });
@@ -78,49 +203,7 @@ export const uploadDocument = async (req: AuthenticatedRequest, res: Response) =
if (docError) throw docError;
// 3. DeepSeek (OpenAI) ile Hukuki Analiz Çıkarımı
// Promptu tasarlıyoruz. JSON dönmesini istiyoruz.
const systemPrompt = `
Sen uzman bir Türk avukatısın. Sana verilen dava dosyasını/belgeyi okuyup analiz edeceksin.
Çıktıyı KESİNLİKLE geçerli bir JSON formatında vermelisin. JSON şu anahtarları içermelidir:
- "taraflar": (string) Davacı ve Davalı bilgileri
- "tarihler": (string) Belgedeki kritik tarihler (Dava tarihi, tebliğ vb.)
- "hukukiDayanak": (string) Belgedeki ilgili kanun maddeleri ve hukuki temeller
- "ilkDegerlendirme": (string) Dosyaya dair ilk stratejik ve hukuki değerlendirmen
Sadece ve sadece JSON döndür, başka hiçbir açıklama ekleme.
`;
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
const response = await fetch(`${runpodUrl}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: aiModel,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: extractedText.substring(0, 15000) }
],
think: false,
stream: false,
}),
});
if (!response.ok) {
throw new Error(`AI Model request failed: ${response.statusText}`);
}
const data = await response.json();
// Support both custom (data.message.content) and OpenAI standard (data.choices[0].message.content) formats just in case
const aiContent = data.message?.content || (data.choices && data.choices[0]?.message?.content);
let summaryJson = {};
try {
summaryJson = JSON.parse(aiContent || '{}');
} catch (e) {
console.error('Failed to parse AI response as JSON:', aiContent);
summaryJson = { error: 'AI did not return valid JSON', raw: aiContent };
}
const { summaryJson, aiModel } = await runDocumentAnalysis(extractedText);
// 4. Analiz sonucunu veritabanına kaydet (analyses tablosu)
const { data: analysisRecord, error: analysisError } = await supabase
+2 -1
View File
@@ -1,11 +1,12 @@
import { Router } from 'express';
import multer from 'multer';
import { requireAuth } from '../middleware/auth';
import { uploadDocument } from '../controllers/document.controller';
import { uploadDocument, registerLocalDocument } from '../controllers/document.controller';
const router = Router();
const upload = multer({ storage: multer.memoryStorage() });
router.post('/upload', upload.single('file'), requireAuth, uploadDocument);
router.post('/register-local', requireAuth, registerLocalDocument);
export default router;