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