v2.0.8: конвертация .doc → .docx через convert-service перед upload
Deploy contracts-flask / validate (push) Successful in 0s

This commit is contained in:
2026-07-16 10:42:21 +04:00
parent c677206d03
commit de05d746cc
3 changed files with 65 additions and 10 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
"""Конфигурация приложения — все настройки в одном месте.""" """Конфигурация приложения — все настройки в одном месте."""
import os import os
VERSION = "2.0.7" VERSION = "2.0.8"
LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions") LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
LLM_KEY = os.getenv("LLM_API_KEY", "") LLM_KEY = os.getenv("LLM_API_KEY", "")
+57 -2
View File
@@ -12,6 +12,8 @@
* - statusToHTML() — рендер статуса (↑ N%, ✓) * - statusToHTML() — рендер статуса (↑ N%, ✓)
* - renderFiles() — рендер всей таблицы * - renderFiles() — рендер всей таблицы
var CONVERT_URL = '/convert-doc';
/** /**
* statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1). * statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1).
* *
@@ -26,6 +28,7 @@ function statusToHTML(st) {
if (!st || !st.kind) return ''; if (!st || !st.kind) return '';
switch (st.kind) { switch (st.kind) {
case 'connecting': return '⏳ соединение... ' + (st.elapsed || 0) + 'с'; case 'connecting': return '⏳ соединение... ' + (st.elapsed || 0) + 'с';
case 'converting': return '⏳ конвертация... ' + (st.elapsed || 0) + 'с';
case 'uploading': return '⏳ отправка... ' + (st.elapsed || 0) + 'с'; case 'uploading': return '⏳ отправка... ' + (st.elapsed || 0) + 'с';
case 'uploaded': return '<span class="status-ok">✓</span>'; case 'uploaded': return '<span class="status-ok">✓</span>';
case 'unzipping': return '⏳ распаковка...'; case 'unzipping': return '⏳ распаковка...';
@@ -203,6 +206,40 @@ window.toggleClassifyDetail = async function(i) {
} }
}; };
/**
* convertDoc(file, onProgress) — .doc → .docx через внешний libreoffice-сервис.
* С честным счётчиком времени.
*/
function convertDoc(file, onProgress) {
var startTime = Date.now();
if (onProgress) onProgress({ kind: 'converting', elapsed: 0 });
var fd = new FormData();
fd.append('files', file, file.name);
var timer = setInterval(function() {
var elapsed = Math.floor((Date.now() - startTime) / 1000);
if (onProgress) onProgress({ kind: 'converting', elapsed: elapsed });
}, 1000);
return fetch(CONVERT_URL + '?_=' + Date.now(), { method: 'POST', body: fd })
.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);
throw e;
});
}
/** /**
* ⛔ НЕ МЕНЯТЬ ⛔ uploadFile — fetch-загрузка с честным счётчиком времени. * ⛔ НЕ МЕНЯТЬ ⛔ uploadFile — fetch-загрузка с честным счётчиком времени.
* *
@@ -430,8 +467,17 @@ async function addRegularFile(file, zipSource) {
render(state); render(state);
try { 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-загрузка с честным счётчиком времени // fetch-загрузка с честным счётчиком времени
var resp = await uploadFile(file, function(st) { var resp = await uploadFile(uploadTarget, function(st) {
state.files[rowIdx].status = st; state.files[rowIdx].status = st;
render(state); render(state);
}, zipSource); }, zipSource);
@@ -523,7 +569,16 @@ async function onFilesSelected(newFiles) {
delete entry._pendingFile; delete entry._pendingFile;
var f = pendingFile; var f = pendingFile;
try { try {
var resp = await uploadFile(f, function(st) { // .doc → конвертация через libreoffice-сервис
var uploadTarget = f;
if (f.name.toLowerCase().endsWith('.doc')) {
uploadTarget = await convertDoc(f, function(st) {
entry.status = st;
render(state);
});
entry.name = uploadTarget.name;
}
var resp = await uploadFile(uploadTarget, function(st) {
entry.status = st; entry.status = st;
render(state); render(state);
}); });
+7 -7
View File
@@ -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.7</span></span> <span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.8</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>
@@ -216,11 +216,11 @@
</div> </div>
</div> </div>
<script src="/static/state.js?v=2.0.7"></script> <script src="/static/state.js?v=2.0.8"></script>
<script src="/static/app_utils.js?v=2.0.7"></script> <script src="/static/app_utils.js?v=2.0.8"></script>
<script src="/static/files.js?v=2.0.7"></script> <script src="/static/files.js?v=2.0.8"></script>
<script src="/static/groups.js?v=2.0.7"></script> <script src="/static/groups.js?v=2.0.8"></script>
<script src="/static/compare.js?v=2.0.7"></script> <script src="/static/compare.js?v=2.0.8"></script>
<script src="/static/app.js?v=2.0.7"></script> <script src="/static/app.js?v=2.0.8"></script>
</body> </body>
</html> </html>