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';
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user