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:
@@ -5,6 +5,128 @@ import * as officeParser from 'officeparser';
|
|||||||
import Tesseract from 'tesseract.js';
|
import Tesseract from 'tesseract.js';
|
||||||
const heicConvert = require('heic-convert');
|
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) => {
|
export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
@@ -39,7 +161,10 @@ export const uploadDocument = async (req: AuthenticatedRequest, res: Response) =
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
// officeParser natively supports pdf, docx, doc, rtf, txt, etc.
|
// 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) {
|
} catch (err: any) {
|
||||||
console.error('officeParser Error:', err);
|
console.error('officeParser Error:', err);
|
||||||
return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' });
|
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;
|
if (docError) throw docError;
|
||||||
|
|
||||||
// 3. DeepSeek (OpenAI) ile Hukuki Analiz Çıkarımı
|
// 3. DeepSeek (OpenAI) ile Hukuki Analiz Çıkarımı
|
||||||
// Promptu tasarlıyoruz. JSON dönmesini istiyoruz.
|
const { summaryJson, aiModel } = await runDocumentAnalysis(extractedText);
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Analiz sonucunu veritabanına kaydet (analyses tablosu)
|
// 4. Analiz sonucunu veritabanına kaydet (analyses tablosu)
|
||||||
const { data: analysisRecord, error: analysisError } = await supabase
|
const { data: analysisRecord, error: analysisError } = await supabase
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { requireAuth } from '../middleware/auth';
|
import { requireAuth } from '../middleware/auth';
|
||||||
import { uploadDocument } from '../controllers/document.controller';
|
import { uploadDocument, registerLocalDocument } from '../controllers/document.controller';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
router.post('/upload', upload.single('file'), requireAuth, uploadDocument);
|
router.post('/upload', upload.single('file'), requireAuth, uploadDocument);
|
||||||
|
router.post('/register-local', requireAuth, registerLocalDocument);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Reference in New Issue
Block a user