v2.0.14: .doc→.docx в sink + зачистка мёртвого кода
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:
“Naeel”
2026-08-26 13:54:07 +03:00
parent 5b49e88f1e
commit ebdab731ff
3 changed files with 3 additions and 200 deletions
+1 -1
View File
@@ -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
-197
View File
@@ -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).
*
+2 -2
View File
@@ -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>