v2.0.14: .doc→.docx в sink + зачистка мёртвого кода
Deploy contracts-flask / validate (push) Canceled after 0s
Deploy contracts-flask / validate (push) Canceled after 0s
- contracts_upload_sink: конвертация .doc через внешний сервис (фронт грузит напрямую) - удалены: convertDoc/addZipFile/addRegularFile (фронт), /convert-doc,/api/convert_refs,/api/unzip_refs, _pull_from_ref и хелперы pull (бэк)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""Upload blueprint — загрузка, конвертация, распаковка."""
|
||||
import io, os, base64, hashlib, zipfile
|
||||
import httpx
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
from flask import Blueprint, request, jsonify
|
||||
from services.parse import parse_file
|
||||
from db import documents
|
||||
import config
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
* - statusToHTML() — рендер статуса (↑ N%, ✓)
|
||||
* - renderFiles() — рендер всей таблицы
|
||||
|
||||
var CONVERT_URL = '/convert-doc';
|
||||
|
||||
/**
|
||||
* statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1).
|
||||
*
|
||||
@@ -206,49 +204,6 @@ window.toggleClassifyDetail = async function(i) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* convertDoc(file, onProgress) — .doc → .docx через ВМ-буфер (паттерн drhider).
|
||||
* Фаза 1: PUT .doc на ВМ. Фаза 2: /api/convert_refs → pull → конвертация → .docx.
|
||||
*/
|
||||
function convertDoc(file, onProgress) {
|
||||
var startTime = Date.now();
|
||||
if (onProgress) onProgress({ kind: 'converting', elapsed: 0 });
|
||||
var token = crypto.randomUUID();
|
||||
var vmUrl = VM_UPLOAD_URL + token + '_0';
|
||||
|
||||
var timer = setInterval(function() {
|
||||
var elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
if (onProgress) onProgress({ kind: 'converting', elapsed: elapsed });
|
||||
}, 1000);
|
||||
|
||||
return fetch(vmUrl, { method: 'PUT', body: file, headers: { 'Content-Type': 'application/octet-stream' } })
|
||||
.then(function(r) {
|
||||
if (!r.ok) throw new Error('Конвертация: VM upload HTTP ' + r.status);
|
||||
return fetch('/api/convert_refs?_=' + Date.now(), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ files: [{ name: file.name, size: file.size, url: vmUrl }] })
|
||||
});
|
||||
})
|
||||
.then(function(r) {
|
||||
if (!r.ok) throw new Error('Конвертация: HTTP ' + r.status);
|
||||
return r.blob();
|
||||
})
|
||||
.then(function(blob) {
|
||||
clearInterval(timer);
|
||||
if (blob.size === 0) throw new Error('Конвертация: пустой результат');
|
||||
return new File([blob], file.name.replace(/\.doc$/i, '.docx'), {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
lastModified: Date.now()
|
||||
});
|
||||
})
|
||||
.catch(function(e) {
|
||||
clearInterval(timer);
|
||||
if (e.message === 'Failed to fetch' || e.name === 'TypeError') throw new Error('Конвертация: Сеть');
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔ uploadFile — загрузка через ВМ-буфер (паттерн drhider).
|
||||
*
|
||||
@@ -380,158 +335,6 @@ function applyParseResult(entry, parsed, elapsed) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* addZipFile(file) — Async-action: обработка ZIP-файла (Фаза 1, упрощена).
|
||||
*
|
||||
* 1. Отправляет ZIP на бэкенд (только распаковка, без БД/парсинга)
|
||||
* 2. Для каждого файла из архива: base64 → Blob → File → addRegularFile()
|
||||
*
|
||||
* addRegularFile делает ВСЁ: upload, parse, дубликаты, статусы, render.
|
||||
* Никакого дублирования логики — единый путь для обычных и ZIP-файлов.
|
||||
*/
|
||||
async function addZipFile(file) {
|
||||
var zipEntry = { name: file.name, lastModified: file.lastModified, size: file.size, status: { kind: 'unzipping' } };
|
||||
state.files.push(zipEntry);
|
||||
render(state);
|
||||
|
||||
try {
|
||||
// Шаг 1: распаковать ZIP через ВМ-буфер (паттерн drhider)
|
||||
var token = crypto.randomUUID();
|
||||
var vmUrl = VM_UPLOAD_URL + token + '_0';
|
||||
var putResp = await fetch(vmUrl, { method: 'PUT', body: file, headers: { 'Content-Type': 'application/octet-stream' } });
|
||||
if (!putResp.ok) throw new Error('unzip failed: VM upload HTTP ' + putResp.status);
|
||||
var refsResp = await fetch('/api/unzip_refs?_=' + Date.now(), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ files: [{ name: file.name, size: file.size, url: vmUrl }] })
|
||||
});
|
||||
if (!refsResp.ok) throw new Error('unzip failed: HTTP ' + refsResp.status);
|
||||
var zipResp = await refsResp.json();
|
||||
if (!zipResp.ok || !zipResp.files) throw new Error('unzip failed');
|
||||
|
||||
// Подтверждение: показать первые 10 файлов + итог
|
||||
var preview = zipResp.files.slice(0, 10).map(function(f) { return ' • ' + f.filename + ' (' + (f.size/1024).toFixed(1) + ' KB)'; }).join('\n');
|
||||
if (total > 10) preview += '\n ... и ещё ' + (total - 10) + ' файлов';
|
||||
if (!confirm('Архив «' + file.name + '» — ' + total + ' файлов:\n\n' + preview + '\n\nЗагрузить эти файлы?')) {
|
||||
// Отмена — убрать строку ZIP
|
||||
var zipPos2 = state.files.indexOf(zipEntry);
|
||||
if (zipPos2 >= 0) state.files.splice(zipPos2, 1);
|
||||
render(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Шаг 2: обработать каждый файл из архива
|
||||
// Временная строка ZIP остаётся в таблице — обновляем её статус
|
||||
var total = zipResp.files.length;
|
||||
for (var zi = 0; zi < total; zi++) {
|
||||
var zf = zipResp.files[zi];
|
||||
zipEntry.status = { kind: 'unzipping' }; // обновляем статус
|
||||
render(state);
|
||||
|
||||
if (zf.error) continue;
|
||||
|
||||
// base64 → File → addRegularFile (единый путь)
|
||||
var byteStr = atob(zf.data_b64);
|
||||
var bytes = new Uint8Array(byteStr.length);
|
||||
for (var b = 0; b < byteStr.length; b++) bytes[b] = byteStr.charCodeAt(b);
|
||||
var mime = {docx:'application/vnd.openxmlformats-officedocument.wordprocessingml.document',doc:'application/msword',pdf:'application/pdf'}[zf.ext] || 'application/octet-stream';
|
||||
var extractedFile = new File([bytes], zf.filename, { type: mime, lastModified: Date.now() });
|
||||
|
||||
await addRegularFile(extractedFile, file.name);
|
||||
}
|
||||
|
||||
// Шаг 3: убрать временную строку ZIP (только после успешной обработки всех файлов)
|
||||
var zipPos = state.files.indexOf(zipEntry);
|
||||
if (zipPos >= 0) state.files.splice(zipPos, 1);
|
||||
render(state);
|
||||
|
||||
} catch(ze) {
|
||||
// Ошибка — показать на строке ZIP (zipEntry всё ещё в state.files)
|
||||
zipEntry.status = { kind: 'error', text: 'ZIP: ' + ze.message };
|
||||
render(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* addRegularFile(file) — Async-action: обработка обычного файла (Фаза 1).
|
||||
*
|
||||
* 1. Добавляет/заменяет файл в state.files
|
||||
* 2. Загружает через XHR (uploadFile) с индикатором прогресса
|
||||
* 3. После загрузки: проставляет doc_id, contractId
|
||||
* 4. applyParseResult — применяет результат парсинга
|
||||
*
|
||||
* Мутирует state.files, вызывает render(state) после каждого изменения.
|
||||
*/
|
||||
async function addRegularFile(file, zipSource) {
|
||||
// Создать запись с начальным статусом
|
||||
var entry = { name: file.name, lastModified: file.lastModified, size: file.size, file: file, status: { kind: 'connecting', elapsed: 0 }, zip_source: zipSource || null };
|
||||
|
||||
// ДЕДУПЛИКАЦИЯ: ключ = (zip_source, name), чтобы одноимённые файлы из разных ZIP не затирались
|
||||
var dupKey = (zipSource || '') + '/' + file.name;
|
||||
var existingIdx = -1;
|
||||
for (var j = 0; j < state.files.length; j++) {
|
||||
var ejKey = (state.files[j].zip_source || '') + '/' + state.files[j].name;
|
||||
if (ejKey === dupKey) { existingIdx = j; break; }
|
||||
}
|
||||
var rowIdx;
|
||||
if (existingIdx >= 0) {
|
||||
// Файл с таким именем уже есть — спросить пользователя
|
||||
if (!confirm('Файл «' + file.name + '» уже есть в списке.\n\nOK — перезаписать\nОтмена — пропустить')) {
|
||||
return; // пользователь выбрал «пропустить»
|
||||
}
|
||||
state.files[existingIdx] = entry;
|
||||
rowIdx = existingIdx;
|
||||
} else {
|
||||
state.files.push(entry);
|
||||
rowIdx = state.files.length - 1;
|
||||
}
|
||||
render(state);
|
||||
|
||||
try {
|
||||
// .doc → конвертация через libreoffice-сервис
|
||||
var uploadTarget = file;
|
||||
if (file.name.toLowerCase().endsWith('.doc')) {
|
||||
uploadTarget = await convertDoc(file, function(st) {
|
||||
state.files[rowIdx].status = st;
|
||||
render(state);
|
||||
});
|
||||
state.files[rowIdx].name = uploadTarget.name;
|
||||
}
|
||||
// fetch-загрузка с честным счётчиком времени
|
||||
var resp = await uploadFile(uploadTarget, function(st) {
|
||||
state.files[rowIdx].status = st;
|
||||
render(state);
|
||||
}, zipSource);
|
||||
// Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys)
|
||||
if (resp && resp.contract_id) state.contractId = resp.contract_id;
|
||||
state.files[rowIdx].doc_id = resp.doc_id;
|
||||
|
||||
// Дубликат?
|
||||
if (resp && resp.duplicate_of) {
|
||||
state.files[rowIdx].status = { kind: 'duplicate' };
|
||||
render(state);
|
||||
return;
|
||||
}
|
||||
// Warning от парсинга?
|
||||
if (resp && resp.warning) {
|
||||
state.files[rowIdx].status = { kind: 'warning', text: resp.warning };
|
||||
render(state);
|
||||
} else {
|
||||
state.files[rowIdx].status = { kind: 'uploaded' };
|
||||
}
|
||||
state.files[rowIdx].uploaded = true;
|
||||
|
||||
// Авто-парсинг: бэкенд парсит всё (PDF + DOCX + DOC)
|
||||
var t0 = Date.now();
|
||||
var pr = resp.parsed;
|
||||
var elapsed = pr && pr.status === 'parsed' ? ((Date.now() - t0) / 1000).toFixed(1) : null;
|
||||
applyParseResult(state.files[rowIdx], pr, elapsed);
|
||||
} catch(err) {
|
||||
state.files[rowIdx].status = { kind: 'error', text: err.message };
|
||||
}
|
||||
render(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* finalizeUpload() — Завершение цикла загрузки (Фаза 1).
|
||||
*
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<img src="/static/logo.svg" alt="Nubes">
|
||||
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.13</span></span>
|
||||
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.14</span></span>
|
||||
<div id="pipelineStepper" style="display:flex;gap:8px;font-size:11px;align-items:center;color:var(--muted);">
|
||||
<span id="stepUpload">○ Загрузка</span><span>→</span>
|
||||
<span id="stepClassify">○ Классификация</span><span>→</span>
|
||||
@@ -226,7 +226,7 @@
|
||||
</script>
|
||||
<script src="/static/state.js?v=2.0.8"></script>
|
||||
<script src="/static/app_utils.js?v=2.0.8"></script>
|
||||
<script src="/static/files.js?v=2.0.13"></script>
|
||||
<script src="/static/files.js?v=2.0.14"></script>
|
||||
<script src="/static/groups.js?v=2.0.8"></script>
|
||||
<script src="/static/compare.js?v=2.0.8"></script>
|
||||
<script src="/static/app.js?v=2.0.13"></script>
|
||||
|
||||
Reference in New Issue
Block a user