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 — загрузка, конвертация, распаковка."""
|
"""Upload blueprint — загрузка, конвертация, распаковка."""
|
||||||
import io, os, base64, hashlib, zipfile
|
import io, os, base64, hashlib, zipfile
|
||||||
import httpx
|
import httpx
|
||||||
from flask import Blueprint, request, jsonify, send_file
|
from flask import Blueprint, request, jsonify
|
||||||
from services.parse import parse_file
|
from services.parse import parse_file
|
||||||
from db import documents
|
from db import documents
|
||||||
import config
|
import config
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
* - statusToHTML() — рендер статуса (↑ N%, ✓)
|
* - statusToHTML() — рендер статуса (↑ N%, ✓)
|
||||||
* - renderFiles() — рендер всей таблицы
|
* - renderFiles() — рендер всей таблицы
|
||||||
|
|
||||||
var CONVERT_URL = '/convert-doc';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1).
|
* 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).
|
* ⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔ 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).
|
* finalizeUpload() — Завершение цикла загрузки (Фаза 1).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<img src="/static/logo.svg" alt="Nubes">
|
<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);">
|
<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="stepUpload">○ Загрузка</span><span>→</span>
|
||||||
<span id="stepClassify">○ Классификация</span><span>→</span>
|
<span id="stepClassify">○ Классификация</span><span>→</span>
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<script src="/static/state.js?v=2.0.8"></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/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/groups.js?v=2.0.8"></script>
|
||||||
<script src="/static/compare.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>
|
<script src="/static/app.js?v=2.0.13"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user