From 60dd40cf05143f2859e65363ab12cd83e4adb203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 25 Jun 2026 10:16:06 +0400 Subject: [PATCH] =?UTF-8?q?v1.0.178:=20=D1=83=D0=BD=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20ZIP.=20addZipFile=20=E2=86=92?= =?UTF-8?q?=20base64=E2=86=92File=E2=86=92addRegularFile=20(=D0=B5=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=D1=8B=D0=B9=20=D0=BF=D1=83=D1=82=D1=8C).=20confirm?= =?UTF-8?q?()=20=D0=BF=D1=80=D0=B8=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D0=B5.=20unzip.py=20=E2=80=94=20=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D1=80=D0=B0=D1=81=D0=BF=D0=B0=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0,=20=D0=B1=D0=B5=D0=B7=20=D0=91=D0=94.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/files.js | 57 +++++++++++++--------------------------- deploy/services/unzip.py | 9 +++---- index.cfm | 4 +-- 3 files changed, 23 insertions(+), 47 deletions(-) diff --git a/deploy/files.js b/deploy/files.js index 7da51bd..2c63abc 100644 --- a/deploy/files.js +++ b/deploy/files.js @@ -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 { diff --git a/deploy/services/unzip.py b/deploy/services/unzip.py index 3fbcb1f..b4b0b67 100644 --- a/deploy/services/unzip.py +++ b/deploy/services/unzip.py @@ -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"} diff --git a/index.cfm b/index.cfm index 479ad70..dd4f265 100644 --- a/index.cfm +++ b/index.cfm @@ -94,7 +94,7 @@
Порядок определяется автоматически при классификации
-
⚠ При совпадении имён файл перезаписывается без предупреждения — в общем списке и внутри ZIP-архивов.
+
⚠ При совпадении имён — запрос на перезапись (OK / Отмена). Файлы из ZIP-архивов загружаются через тот же поток.
@@ -214,7 +214,7 @@

Как пользоваться

1. Загрузка — выберите файлы (.docx, .doc, .pdf, .zip). Можно загружать всё вперемешку: договоры, допники, спецификации, архивы. Порядок не важен — классификация разберётся.

-

ZIP-архивы распаковываются автоматически: файлы из корня архива добавляются в таблицу. ⚠ При совпадении имён файл перезаписывается без предупреждения — в общем списке и внутри архива.

+

ZIP-архивы распаковываются автоматически: каждый файл из архива проходит тот же путь загрузки и парсинга, что и обычный. При совпадении имён — запрос на перезапись.

2. Классификация — нажмите «Классифицировать». Система через LLM определит для каждого файла: тип, номер, дату, контрагента и родительский договор. Прогресс и таймер видны на кнопке.