v1.0.178: унификация ZIP. addZipFile → base64→File→addRegularFile (единый путь). confirm() при дубликате. unzip.py — только распаковка, без БД.

This commit is contained in:
2026-06-25 10:16:06 +04:00
parent b013c79190
commit 60dd40cf05
3 changed files with 23 additions and 47 deletions
+18 -39
View File
@@ -294,25 +294,21 @@ function applyParseResult(entry, parsed, elapsed) {
}
/**
* addZipFile(file) — Async-action: обработка ZIP-файла (Фаза 1).
* addZipFile(file) — Async-action: обработка ZIP-файла (Фаза 1, упрощена).
*
* 1. Показывает временную строку «⏳ распаковка...»
* 2. Отправляет ZIP на бэкенд (UNZIP_URL)
* 3. Убирает временную строку
* 4. Добавляет файлы из архива в state.files, пропуская дубликаты
* 5. Для каждого: applyParseResult (результат парсинга от бэкенда)
* 1. Отправляет ZIP на бэкенд (только распаковка, без БД/парсинга)
* 2. Для каждого файла из архива: base64 → Blob → File → addRegularFile()
*
* Мутирует state.files, вызывает render(state) после каждого изменения.
* addRegularFile делает ВСЁ: upload, parse, дубликаты, статусы, render.
* Никакого дублирования логики — единый путь для обычных и ZIP-файлов.
*/
async function addZipFile(file) {
// Показать временную строку для ZIP
var zipEntry = { name: file.name, lastModified: file.lastModified, size: file.size, status: { kind: 'unzipping' } };
state.files.push(zipEntry);
var zipIdx = state.files.length - 1;
render(state);
try {
// Отправить ZIP на бэкенд (multipart — бэкенд ищет boundary + PK magic)
var zipResp = await new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', UNZIP_URL);
@@ -331,42 +327,21 @@ async function addZipFile(file) {
state.files.splice(zipIdx, 1);
render(state);
// Добавить файлы из архива (дубликаты перезаписываются)
// Каждый файл из архива — через addRegularFile (единый путь)
for (var zi = 0; zi < zipResp.files.length; zi++) {
var zf = zipResp.files[zi];
if (zf.error) continue; // файл с ошибкой — пропускаем
if (zf.error) continue;
var zEntry = {
name: zf.filename, size: zf.size,
doc_id: zf.doc_id, uploaded: true,
status: { kind: 'parsing' }
};
// base64 → Blob → File
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() });
// Проверить дубликат по имени
var dup = -1;
for (var dj = 0; dj < state.files.length; dj++) {
if (state.files[dj].name === zf.filename) { dup = dj; break; }
}
if (dup >= 0) {
// Заменить существующий (как addRegularFile — перезапись)
state.files[dup] = zEntry;
} else {
state.files.push(zEntry);
dup = state.files.length - 1;
}
// Если это первый файл — запомнить contract_id
if (!state.contractId && zf.contract_id) state.contractId = zf.contract_id;
render(state);
// Применить результат парсинга от бэкенда (clean function)
applyParseResult(state.files[dup], zf.parsed);
render(state);
await addRegularFile(extractedFile);
}
} catch(ze) {
// Ошибка распаковки — показать в строке ZIP
console.error('addZipFile error:', ze, 'UNZIP_URL=', UNZIP_URL);
state.files[zipIdx].status = { kind: 'error', text: 'ZIP: ' + ze.message };
render(state);
}
@@ -393,6 +368,10 @@ async function addRegularFile(file) {
}
var rowIdx;
if (existingIdx >= 0) {
// Файл с таким именем уже есть — спросить пользователя
if (!confirm('Файл «' + file.name + '» уже есть в списке.\n\nOK — перезаписать\nОтмена — пропустить')) {
return; // пользователь выбрал «пропустить»
}
state.files[existingIdx] = entry;
rowIdx = existingIdx;
} else {
+3 -6
View File
@@ -1,20 +1,17 @@
"""Unzip service — extract files from ZIP archive."""
import zipfile, io, base64, json
"""Unzip service — extract files from ZIP archive (no DB, no parse)."""
import zipfile, io, base64
def handle_unzip(rfile, content_length):
"""Parse ZIP upload, return list of extracted files."""
"""Parse ZIP upload, return list of extracted files as base64."""
body = rfile.read(content_length)
# Find file data in raw multipart
# Simple approach: find ZIP bytes after header
idx = body.find(b"\r\n\r\n")
if idx < 0:
return {"ok": False, "error": "invalid multipart"}
# Find first boundary end
raw = body[idx + 4:]
# Find the actual ZIP data (after Content-Disposition etc)
zip_start = raw.find(b"PK\x03\x04")
if zip_start < 0:
return {"ok": False, "error": "not a ZIP file"}
+2 -2
View File
@@ -94,7 +94,7 @@
<div class="card-body">
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:6px;width:100%;">
<div style="text-align:right;font-size:11px;color:var(--muted);margin-bottom:6px;">Порядок определяется автоматически при классификации</div>
<div style="font-size:11px;color:var(--destructive);margin-bottom:6px;">⚠ При совпадении имён файл перезаписывается без предупреждения — в общем списке и внутри ZIP-архивов.</div>
<div style="font-size:11px;color:var(--muted);margin-bottom:6px;">⚠ При совпадении имён — запрос на перезапись (OK / Отмена). Файлы из ZIP-архивов загружаются через тот же поток.</div>
<div class="table-wrap">
<table>
@@ -214,7 +214,7 @@
<h3>Как пользоваться</h3>
<p><b>1. Загрузка</b> — выберите файлы (.docx, .doc, .pdf, .zip). Можно загружать всё вперемешку: договоры, допники, спецификации, архивы. Порядок не важен — классификация разберётся.</p>
<p>ZIP-архивы распаковываются автоматически: файлы из корня архива добавляются в таблицу. ⚠ При совпадении имён файл перезаписывается без предупреждения — в общем списке и внутри архива.</p>
<p>ZIP-архивы распаковываются автоматически: каждый файл из архива проходит тот же путь загрузки и парсинга, что и обычный. При совпадении имён — запрос на перезапись.</p>
<p><b>2. Классификация</b> — нажмите «Классифицировать». Система через LLM определит для каждого файла: тип, номер, дату, контрагента и родительский договор. Прогресс и таймер видны на кнопке.</p>