Compare commits
248
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fd6f1cffc | ||
|
|
24bd89b511 | ||
|
|
f90d97b276 | ||
|
|
1ff305f38e | ||
|
|
741edafc4a | ||
|
|
04affca363 | ||
|
|
d0128392e3 | ||
|
|
11716259a1 | ||
|
|
4b33593a82 | ||
|
|
e7d771e00f | ||
|
|
ce234132dd | ||
|
|
3abcc6c1fc | ||
|
|
0ad9e0047f | ||
|
|
52c3eb53f1 | ||
|
|
17e6dbd065 | ||
|
|
53751a1f7b | ||
|
|
a33dff2f47 | ||
|
|
af172115b0 | ||
|
|
5711e97fe5 | ||
|
|
5f5ed000b8 | ||
|
|
a18877415d | ||
|
|
7a4f642452 | ||
|
|
090f5e8993 | ||
|
|
d8dee114a3 | ||
|
|
f2141ec731 | ||
|
|
eaa064a721 | ||
|
|
6c51df96d9 | ||
|
|
ba4546ecc9 | ||
|
|
dd1e285c8c | ||
|
|
ff4895b63b | ||
|
|
4b73657e4c | ||
|
|
82acc84311 | ||
|
|
933ee79e3c | ||
|
|
b0c055f926 | ||
|
|
44e12db886 | ||
|
|
08e4788a35 | ||
|
|
e31476ebab | ||
|
|
0479afb44a | ||
|
|
cee18046c2 | ||
|
|
ec660ddd1f | ||
|
|
a901c4f023 | ||
|
|
89bb8a3442 | ||
|
|
2a49155b05 | ||
|
|
f45ecb603d | ||
|
|
cfe6b9e2bf | ||
|
|
d5b3ed3f68 | ||
|
|
45c6f3b7f0 | ||
|
|
023dba3daa | ||
|
|
c346be5add | ||
|
|
dd9785747d | ||
|
|
efa18795db | ||
|
|
a645493c32 | ||
|
|
db7cdbdd84 | ||
|
|
25e4b46e76 | ||
|
|
0e5f3ec9a1 | ||
|
|
7a860d645d | ||
|
|
6542d51375 | ||
|
|
0dc5d8673d | ||
|
|
2fd4afa907 | ||
|
|
1509eeb989 | ||
|
|
c17fca792c | ||
|
|
4087f10345 | ||
|
|
6dcbd2bade | ||
|
|
5ddee6b4bd | ||
|
|
82b597e1c2 | ||
|
|
2a735e34f8 | ||
|
|
7be1a1f649 | ||
|
|
7537e7ffe1 | ||
|
|
04f6d7f81a | ||
|
|
705b39ab7d | ||
|
|
a9d4139afb | ||
|
|
da0ea5e964 | ||
|
|
670f533f48 | ||
|
|
dfa325a5c5 | ||
|
|
59a1924bd0 | ||
|
|
62067bc99f | ||
|
|
fe7c238c0b | ||
|
|
3ae7d325e2 | ||
|
|
59a66278a3 | ||
|
|
551ed8cf42 | ||
|
|
0a94d3f449 | ||
|
|
b30dd09e1f | ||
|
|
7389e1009a | ||
|
|
4bdcde86b7 | ||
|
|
f5ca8c3eb5 | ||
|
|
b5a14cfc3d | ||
|
|
612979ed6e | ||
|
|
d2f2bd39f4 | ||
|
|
8d7a7dfaf6 | ||
|
|
2224e20bde | ||
|
|
2f5a94bf3f | ||
|
|
70a47ac905 | ||
|
|
c67fffbc56 | ||
|
|
0453827b8b | ||
|
|
020f9d43c6 | ||
|
|
b46198d74a | ||
|
|
bc09ada90e | ||
|
|
e548c89880 | ||
|
|
defbc10004 | ||
|
|
af9faba3fc | ||
|
|
5677c59261 | ||
|
|
02d9592eb5 | ||
|
|
53a1fb40d2 | ||
|
|
0ad5a5fabd | ||
|
|
94e588aaba | ||
|
|
c3c6e5b584 | ||
|
|
301ceecaa8 | ||
|
|
cb6274a887 | ||
|
|
87a6df45c5 | ||
|
|
c102d54887 | ||
|
|
a182a2f38a | ||
|
|
5f0ca8e463 | ||
|
|
8484197eec | ||
|
|
6e8f79ac56 | ||
|
|
2aea7bf2f6 | ||
|
|
0ff904ceb6 | ||
|
|
fd4f152fc8 | ||
|
|
d205118cd5 | ||
|
|
4af247938f | ||
|
|
0640e7d47f | ||
|
|
5efed1c577 | ||
|
|
40372fe03f | ||
|
|
e66dff23b8 | ||
|
|
68fada075e | ||
|
|
338cb2f372 | ||
|
|
9b4060d8c5 | ||
|
|
8158a8bc01 | ||
|
|
26572abb3b | ||
|
|
cd29e727f3 | ||
|
|
e513bf3c9e | ||
|
|
17bc4aebf8 | ||
|
|
b3298b415e | ||
|
|
5e7319cd42 | ||
|
|
204e865aff | ||
|
|
6fe1ba2b42 | ||
|
|
0ceaee1089 | ||
|
|
b6dac6f2bd | ||
|
|
79cd25a762 | ||
|
|
bbffb00eb8 | ||
|
|
ddbf67924b | ||
|
|
00d54dbfb6 | ||
|
|
444ef4b732 | ||
|
|
f072fc9fdb | ||
|
|
f38ba4ca37 | ||
|
|
82b5cb4402 | ||
|
|
ec9107cff4 | ||
|
|
fe45b07c4f | ||
|
|
cd22fd62a6 | ||
|
|
5e4418f648 | ||
|
|
2949d0ef09 | ||
|
|
1ad35dada7 | ||
|
|
ae09c23d81 | ||
|
|
d0f12ef4a3 | ||
|
|
a34a78d8b9 | ||
|
|
ab08acd60d | ||
|
|
f005e8cf29 | ||
|
|
1da496c2e5 | ||
|
|
17fc2670a5 | ||
|
|
7ed1f5d650 | ||
|
|
3df63df0b0 | ||
|
|
75568a2586 | ||
|
|
9ea3c2991a | ||
|
|
cbf7de27d8 | ||
|
|
d94fa3f556 | ||
|
|
7d39885d90 | ||
|
|
e7da82538f | ||
|
|
a91a5e9538 | ||
|
|
c25b22cfe6 | ||
|
|
68571fbbcd | ||
|
|
eaef850421 | ||
|
|
0950b68649 | ||
|
|
322626a372 | ||
|
|
f9b88cbf93 | ||
|
|
a56a819329 | ||
|
|
f3c854c5fb | ||
|
|
23b4ad9889 | ||
|
|
29e2d08cdb | ||
|
|
0cdef67c1f | ||
|
|
8c59c4b7e0 | ||
|
|
70226d8c39 | ||
|
|
4a4de0f8e2 | ||
|
|
38109ba6ec | ||
|
|
adab2c624f | ||
|
|
a380c0e70c | ||
|
|
bbc45e155f | ||
|
|
7bdcb5b7b4 | ||
|
|
c878b3db1a | ||
|
|
bf202cdfa4 | ||
|
|
e3405556b5 | ||
|
|
46d8ced0dd | ||
|
|
e4975fb37e | ||
|
|
613b645456 | ||
|
|
b7075dd8bf | ||
|
|
75c7424963 | ||
|
|
0d16bb6520 | ||
|
|
491b49f633 | ||
|
|
76a2d4b7ed | ||
|
|
c35e8da357 | ||
|
|
345448caa4 | ||
|
|
18922b486b | ||
|
|
e590d8ab9a | ||
|
|
cbf619aaba | ||
|
|
0998892d7f | ||
|
|
d0e874d87e | ||
|
|
81bdee6203 | ||
|
|
efaa42dd2e | ||
|
|
9145345882 | ||
|
|
ee42865fcd | ||
|
|
3ba201748c | ||
|
|
f98b4ba7a5 | ||
|
|
a1574677a5 | ||
|
|
1a84770e82 | ||
|
|
75b8476703 | ||
|
|
370a869aef | ||
|
|
62c7ea0162 | ||
|
|
df15f48ecc | ||
|
|
adf3002e3d | ||
|
|
59f7601266 | ||
|
|
f15af2866d | ||
|
|
1d91d5f92e | ||
|
|
fd5e1613a5 | ||
|
|
303830528f | ||
|
|
3d55562f02 | ||
|
|
dacf0d584f | ||
|
|
5c426cb037 | ||
|
|
0c2b636d31 | ||
|
|
a87361c159 | ||
|
|
7fc41aabce | ||
|
|
2731bc7f09 | ||
|
|
724124b3fc | ||
|
|
72ae915935 | ||
|
|
3e5aef70a0 | ||
|
|
34f9a6f74f | ||
|
|
84ae7439f5 | ||
|
|
2a2cc1467c | ||
|
|
f5427b0db7 | ||
|
|
af2f2a9f71 | ||
|
|
2ceeb05a8b | ||
|
|
579f9c8334 | ||
|
|
d607d7d306 | ||
|
|
4c3f9d4e49 | ||
|
|
cc2f627de2 | ||
|
|
0707d53b37 | ||
|
|
7a005f7b06 | ||
|
|
51bc1a79e7 | ||
|
|
2cf4e08100 | ||
|
|
37581eb601 | ||
|
|
a2f754ab4f |
@@ -0,0 +1,24 @@
|
||||
# ⛔ ЖЁСТКИЕ ПРАВИЛА
|
||||
- НЕЛЬЗЯ менять код без «делай» — показать план → ждать
|
||||
- НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами
|
||||
- НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать
|
||||
- НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest)
|
||||
- НЕЛЬЗЯ предполагать — сомневаешься = спроси
|
||||
- НЕЛЬЗЯ грузить внешние CDN/библиотеки на фронтенд — всё в коде
|
||||
|
||||
# ✅ ПОРЯДОК ПРАВКИ
|
||||
1. План → «делай»
|
||||
2. Правим → проверяем syntax → проверяем import
|
||||
3. git add -A && git commit -m "..." && git push
|
||||
4. Bump VERSION в site/app.py перед push
|
||||
|
||||
# 📋 ДОКИ
|
||||
- Порядок правок: docs/DEVELOPMENT.md
|
||||
- Архитектура: docs/ARCHITECTURE.md
|
||||
- Деплой: docs/DEPLOY.md
|
||||
- История: History/
|
||||
|
||||
# 📋 ПРОЕКТ
|
||||
Штурвал Managed Flask, без Dockerfile/gunicorn, без БД
|
||||
URL: https://drhider.pythonk8s.dev.nubes.ru/
|
||||
ВМ legacy: ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213
|
||||
@@ -0,0 +1,7 @@
|
||||
# ⛔⛔⛔ STOP: без «делай» ничего не делать
|
||||
# ⛔⛔⛔ ВОПРОС (?, почему, как, где, что) — ТОЛЬКО ОТВЕТ. Читать/узнавать МОЖНО, менять НЕЛЬЗЯ.
|
||||
# ⛔ Не спешить. Не лезть в contracts/. Не гадать — спросить.
|
||||
НИКОГДА не действовать по догадкам !!!
|
||||
ВСЕГДА всё перепроверять перед действием или ответом !!!
|
||||
ВСЕГДА КРАТКО без воды отвечать, чётко по делу, без лишних слов.
|
||||
# 📋 Правила: .github/CODERULES.md
|
||||
+26
@@ -1,3 +1,29 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
ENV/
|
||||
instance/
|
||||
.flaskenv
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
*.log
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
# TMP/
|
||||
# about1500.md
|
||||
*.har
|
||||
chrome-net-export-log.json
|
||||
TSTFILES/
|
||||
files/
|
||||
upload-platform/
|
||||
.agent-tmp/
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system"
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
+5
-2
@@ -1,8 +1,11 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libreoffice-writer \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY drhider.py drhider_server.py /app/
|
||||
COPY site /app/site
|
||||
COPY drhider /app/drhider
|
||||
EXPOSE 5000
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--timeout", "300", "--workers", "1", "--worker-class", "gevent", "--limit-request-field_size", "0", "--limit-request-body", "0", "site.app:app"]
|
||||
CMD ["python", "site/app.py"]
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# History — журнал проекта DrHider
|
||||
|
||||
Хронологический журнал: планы, вопросы/ответы ревьюеров (Sonnet, Opus),
|
||||
реализации фич (по версиям), диагностика, тесты и бенчмарки.
|
||||
|
||||
**Правило:** любая находка/решение/факт записывается сюда сразу, в момент обнаружения.
|
||||
|
||||
## Структура (папки по темам)
|
||||
|
||||
| Папка | Что внутри |
|
||||
|-------|-----------|
|
||||
| [base/](base/) | Ранняя документация и первоначальная настройка проекта |
|
||||
| [plans/](plans/) | Планы и проектирование (аудит, архитектуры v3/v3.1, планы реализации) |
|
||||
| [sonnet/](sonnet/) | Все вопросы к Sonnet и его ответы (обсуждения, ревью, Q&A) |
|
||||
| [opus/](opus/) | Запросы к Opus и его ответы (валидация MSS-clamping, ревью документации) |
|
||||
| [llm/](llm/) | Всё про LLM-часть: метрики, статистика в UI, чанкинг для полного покрытия |
|
||||
| [sse/](sse/) | SSE-события: отлов отключения клиента, hop-by-hop заголовки |
|
||||
| [infra/](infra/) | Инфраструктура: кластер, ingress, RST-диагностика, HTTP/2, ВМ-буфер |
|
||||
| [ux-frontend/](ux-frontend/) | Фичи фронтенда: таймеры, имена файлов, ZIP-раскрытие, лимиты, прерывание/ETA |
|
||||
| [upload/](upload/) | Загрузка файлов: диагностика, лимиты, стресс-тесты |
|
||||
| [code-fixes/](code-fixes/) | Исправления кода по версиям (баги, оптимизация, безопасность, логирование) |
|
||||
| [tests/](tests/) | Тесты и бенчмарки: замеры UI, обфускация, нагрузка |
|
||||
|
||||
---
|
||||
|
||||
## base/ — первоначальная настройка
|
||||
|
||||
- **2026-07-11-initial-setup.md** — создание проекта DrHider на платформе Штурвал (Managed Flask, URL, входные точки).
|
||||
- **2026-07-12-documentation.md** — создание полной документации после рефакторинга (список документируемых файлов).
|
||||
|
||||
## plans/ — планы и проектирование
|
||||
|
||||
- **2026-07-12-audit-and-plan.md** — полный аудит проекта и план рефакторинга под Managed Flask.
|
||||
- **2026-07-12-v3-plan.md** — план v3: универсальная архитектура (убрать жёсткий список типов сущностей).
|
||||
- **2026-07-12-v3-done.md** — итоги v3: переход на Markdown-пайплайн + универсальное LLM-обнаружение.
|
||||
- **2026-07-12-v3.1-plan.md** — план v3.1: пофайловая обработка + прогресс в таблице (решение таймаута).
|
||||
- **2026-08-24-implementation-plan-for-flash.md** — план для агента-кодера (Flash): TTL-фикс, трекинг файлов, прерывание, ETA, UI.
|
||||
- **2026-08-24-folder-select-plan.md** — план для Flash: кнопка «Выбрать папку» (webkitdirectory, рекурсивно, относительный путь сохраняется).
|
||||
|
||||
## sonnet/ — обсуждения с Sonnet (вопросы и ответы)
|
||||
|
||||
**12–13 июля — проектирование:**
|
||||
- **2026-07-12-sonnet-query.md** + **2026-07-12-sonnet-response.md** — вопрос/ответ: универсальная архитектура DrHider (6 вопросов → 6 ответов → карта изменений).
|
||||
- **2026-07-12-sonnet-query-timeout.md** + **2026-07-12-sonnet-response-timeout.md** — диагностика ERR_TIMED_OUT в новом окне браузера (dev-сервер: 6 потоков заняты keep-alive).
|
||||
- **2026-07-12-sonnet-query-v2.md** + **2026-07-12-sonnet-response-v2.md** — улучшение LLM-промпта (двухфазная инструкция).
|
||||
- **2026-07-12-sonnet-query-v3-numbering.md** + **2026-07-12-sonnet-response-v3-numbering.md** — нумерация вместо генерации фейков (прозрачный аудит).
|
||||
- **2026-07-12-sonnet-query-v4-hybrid.md** + **2026-07-12-sonnet-response-v4-hybrid.md** — гибридный формат замен «шаблон + номер» (вариант В).
|
||||
- **2026-07-13-sonnet-query-architecture.md** + **2026-07-13-sonnet-response-architecture.md** — архитектура пофайловой обработки с одним ZIP (вариант А + блокирующий /process).
|
||||
- **2026-07-13-sonnet-query-obfuscation-tokens.md** — запрос: анализ обфускации.
|
||||
- **2026-07-13-sonnet-response-obfuscation-analysis.md** — анализ обфускации (TMP-файлы vs код).
|
||||
- **2026-07-13-sonnet-response-obfuscation-bugs.md** — анализ багов обфускации и их фикс.
|
||||
|
||||
**20 августа — ревью скорости и сбоев:**
|
||||
- **2026-08-20-sonnet-query-review-speed.md** — промпт: код-ревью drhider (скорость + объяснение сбоев, ограничение файлов).
|
||||
- **2026-08-20-sonnet-query-review-speed-followup.md** — follow-up вопросы по спорным пунктам ревью (В1–В4).
|
||||
- **2026-08-20-sonnet-review-qa.md** — Q&A: вопросы → ответы Sonnet → решения по ревью.
|
||||
|
||||
**20 августа — LLM-чанкинг:**
|
||||
- **2026-08-20-sonnet-query-llm-pattern.md** — вопрос: как лучше организовать LLM-покрытие всех документов.
|
||||
- **2026-08-20-sonnet-query-llm-followup.md** — 6 уточнений перед внедрением (чанкинг, дедуп, верификация, промпт).
|
||||
- **2026-08-20-sonnet-llm-qa.md** — сводный Q&A по LLM-чанкингу (схема + ответы У.1–У.6 + решение).
|
||||
|
||||
**20 августа — ускорение:**
|
||||
- **2026-08-20-sonnet-query-honest-speed.md** — честный вопрос: можно ли ускорить без риска устойчивости.
|
||||
|
||||
## opus/ — обсуждения с Opus
|
||||
|
||||
- **2026-07-13-opus-query-mss-clamping.md** — запрос: валидация плана MSS Clamping.
|
||||
- **2026-07-13-opus-response-mss-clamping.md** — ответ: валидация плана MSS Clamping.
|
||||
- **2026-07-13-opus-response-document-review.md** — проверка PROBLEM-AND-SOLUTION.md на ошибки.
|
||||
- **2026-07-13-opus-response-final-review.md** — финальная проверка PROBLEM-AND-SOLUTION.md.
|
||||
|
||||
## llm/ — LLM-часть
|
||||
|
||||
- **2026-08-19-llm-metrics.md** — v0.0.34: вывод токенов и времени LLM.
|
||||
- **2026-08-19-llm-stats-block.md** — v0.0.37: крупный блок статистики LLM в UI.
|
||||
- **2026-08-20-llm-full-chunking.md** — v0.0.57: LLM обрабатывает ВСЕ данные (пофайловый чанкинг 6000/overlap 500, параллельность 4, дедуп + верификация).
|
||||
|
||||
## sse/ — SSE-события
|
||||
|
||||
- **2026-07-14-sse-generator-exit.md** — v0.0.18: отлов отключения клиента SSE (GeneratorExit/BrokenPipe).
|
||||
- **2026-07-14-hop-by-hop-connection.md** — v0.0.21→0.0.22: Waitress запрещает hop-by-hop заголовок `Connection: close`.
|
||||
|
||||
## infra/ — инфраструктура
|
||||
|
||||
- **2026-07-14-browser-rst-debug.md** — диагностика ERR_CONNECTION_RESET в браузере.
|
||||
- **2026-07-15-cluster-dump.md** — снимок кластера (4 ноды, taints, состояние).
|
||||
- **2026-07-15-rst-root-cause.md** — итоговая диагностика ERR_CONNECTION_RESET (v0.0.29).
|
||||
- **2026-08-19-remove-process-names-http2.md** — v0.0.46: удалён process_names + проблема git push через HTTP/2.
|
||||
- **2026-08-21-pattern-vm-buffer-pull-plan.md** — план: перенос паттерна «ВМ-буфер + pull» в drhider.
|
||||
- **2026-08-21-vm-files-api-plan.md** — план: универсальный файловый сервис на ВМ (API).
|
||||
- **2026-08-21-vm-upload-implemented.md** — v0.0.58: ВМ-загрузка реализована (паттерн «ВМ-буфер + pull»).
|
||||
- **2026-08-24-two-clusters-deploy.md** — деплой на двух кластерах: iot-naeel (актуальный, DNS → .151) и naeel-test-3 (не погашен).
|
||||
|
||||
## ux-frontend/ — фичи фронтенда
|
||||
|
||||
- **2026-08-19-live-timer-llm.md** — v0.0.38: живой таймер обработки + индикация ИИ.
|
||||
- **2026-08-19-live-filename.md** — v0.0.40: имя файла в live-блоке + сброс итогов.
|
||||
- **2026-08-19-file-time.md** — v0.0.41: время на конкретный файл в колонке «Статус».
|
||||
- **2026-08-19-zip-expand-frontend.md** — v0.0.33: раскрытие ZIP в таблицу (фронтенд).
|
||||
- **2026-08-19-zip-filter-docs.md** — v0.0.35: фильтр документов при раскрытии ZIP.
|
||||
- **2026-08-19-ux-fixes.md** — v0.0.49: индикатор распаковки, разделение фаз (1/2, 2/2), таймауты SSE.
|
||||
- **2026-08-20-upload-limits.md** — v0.0.50: лимиты загрузки (>100МБ/файл, >1ГБ сумма) + красная подсветка в таблице.
|
||||
- **2026-08-24-interrupt-eta-design.md** — дизайн: TTL-фикс + прерывание с сохранением + оценка времени + UI.
|
||||
- **2026-08-24-interrupt-eta-implemented.md** — v0.0.63: реализовано прерывание с сохранением + ETA + UI-таблица.
|
||||
- **2026-08-24-time-tickers-estimates.md** — v0.0.64–0.0.65: тикер текущего файла на всех этапах, мгновенные оценки времени по файлам и суммарно.
|
||||
- **2026-08-24-folder-select-implemented.md** — v0.0.68: кнопка «Выбрать папку» (webkitdirectory, рекурсивно, относительный путь).
|
||||
- **2026-08-24-folder-zip-not-lost.md** — v0.0.69: архивы из папки без документов не теряются (добавляются как есть); раскрытие zip с документами; склонение счётчика.
|
||||
- **2026-08-24-v075-small-fixes.md** — v0.0.75: кнопка при 0 файлов, фильтр zip (idx-мисматч), README-риски; замечен закоммиченный LLM_API_KEY.
|
||||
- **2026-08-24-v074-cancel-extract.md** — v0.0.74: отмена в extract-фазе (не ждём все .doc).
|
||||
- **2026-08-24-v073-security-fixes.md** — v0.0.73: 6 фиксов по ревью Соннета (SSRF, path traversal, слабый SID, proc_error, zip-бомба, self-XSS); 1 отклонено (cleanup), 5 отложено.
|
||||
- **2026-08-24-pull-retry-limit-counter.md** — v0.0.72: ретраи pull из ВМ, корректная ошибка лимита сессии, счётчик дедупа.
|
||||
- **2026-08-24-code-review-sonnet.md** — код-ревью Соннета: 15 багов (SSRF, path traversal, слабый SID, zip-бомба и др.), промпт в docs/code-review-sonnet.md.
|
||||
- **2026-08-24-upload-logic-schema.md** — подробная схема логики загрузки + найденные баги/несоответствия.
|
||||
- **2026-08-24-help-button-limits-text.md** — v0.0.70: заметная кнопка HELP (вместо «?») + пояснение статуса «пропущен» в ограничениях.
|
||||
- **2026-08-24-ui-locks-session-freeze.md** — v0.0.66: блокировки UI (выбор/удаление) на время работы, заморозка сессии + кнопка «Новая сессия».
|
||||
|
||||
## upload/ — загрузка файлов
|
||||
|
||||
- **2026-08-19-upload-delay.md** — диагностика медленной/нестабильной загрузки + v0.0.43 (таймаут 300с).
|
||||
- **2026-08-23-test-upload-only-plan.md** — план: тест-режим «только загрузка» (для Флаша).
|
||||
- **2026-08-23-upload-stress-test.md** — тесты загрузки и стресс (v0.0.59–0.0.60, кластер naeel-test-3).
|
||||
- **2026-08-24-upload-limits.md** — v0.0.62: лимиты 50 МБ на файл, 500 МБ на сессию.
|
||||
|
||||
## code-fixes/ — исправления кода (по версиям)
|
||||
|
||||
- **2026-08-31-vm-sync-readme.md** — документирован `rsync` текущего проекта на ВМ
|
||||
в `/home/naeel/drhider`; legacy-каталог исключён.
|
||||
- **2026-08-31-api-bp-syntax-error.md** — v0.0.77: удалена случайная строка из `site/routes/api_bp.py`,
|
||||
вызывавшая `SyntaxError` при импорте приложения; задокументирован ответ Fable по архитектуре.
|
||||
- **2026-08-18-fix-review-bugs.md** — v0.0.32: фиксы по ревью обфускатора.
|
||||
- **2026-08-19-replacer-optimization.md** — v0.0.39: оптимизация apply_replacements (однопроходная замена, один regex).
|
||||
- **2026-08-20-logging.md** — v0.0.51: максимальное логирование (LOG_LEVEL env) + детальные логи SSE/воркера.
|
||||
- **2026-08-20-logging-file.md** — v0.0.52: файловый лог /tmp/drhider.log (для kubectl exec).
|
||||
- **2026-08-20-broken-files.md** — v0.0.53: устойчивость к битым файлам (воркер не падает, битые пропускаются).
|
||||
- **2026-08-20-v1-v2-implemented.md** — v0.0.54: В2 (фикс кракозябр имён zip) + В1 (фильтр таблиц, эффект≈0).
|
||||
- **2026-08-20-honest-answer-and-A-implemented.md** — v0.0.55: честный ответ Sonnet + реализация А (threading .doc, кэш regex).
|
||||
- **2026-08-20-csv-out-of-zip.md** — v0.0.56: КРИТИЧНО — mapping.csv (ключ расшифровки) убран из ZIP.
|
||||
- **2026-08-24-zip-name-cyrillic-frontend.md** — v0.0.67: фикс кириллических имён из ZIP на фронте (UTF-8 без флага → CP866-мусор), согласовано с бэком.
|
||||
|
||||
## tests/ — тесты и бенчмарки
|
||||
|
||||
- **2026-08-19-ui-5runs-benchmark.md** — v0.0.49: бенчмарк UI, 5 прогонов DownLoads.zip (18 файлов, замеры времени).
|
||||
- **2026-08-23-obfuscation-tests.md** — тесты реальной обфускации и фикс .doc (v0.0.60–0.0.61, корпус /mnt/y/T).
|
||||
- **2026-08-24-tests-upload-and-obfuscation.md** — тесты загрузки и обфускации (v0.0.62, лимиты, ВМ-буфер).
|
||||
- **2026-08-24-test-batch-v067.md** — тесты v0.0.67: загрузка, лимит 50МБ, ZIP-кириллица, ETA, прерывание с частичным сохранением.
|
||||
|
||||
---
|
||||
|
||||
## Соглашение об именах
|
||||
`ГГГГ-ММ-ДД-тема.md` (иногда с версией). В начале каждого файла — дата и версия/статус.
|
||||
При сортировке файл относится в папку по главной теме (имя файла/содержание), не по дате.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Документация — 2026-07-12
|
||||
|
||||
**После рефакторинга** создана полная документация:
|
||||
|
||||
### Созданные файлы
|
||||
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `README.md` | Обзор проекта, правила для агентов, структура, API, env vars |
|
||||
| `ARCHITECTURE.md` | Архитектура: схема, двухпроходная обфускация, поток данных, описание каждого модуля |
|
||||
| `DEPLOY.md` | Деплой, ВМ, переменные окружения, локальная разработка, как добавить генератор |
|
||||
| `.github/copilot-instructions.md` | Правила для Copilot (не менять без «делай», не смотреть локальные папки, коммитить после каждой правки) |
|
||||
|
||||
### Ключевые факты зафиксированы
|
||||
|
||||
- Платформа: Штурвал (Managed Flask), без Dockerfile/gunicorn
|
||||
- URL: https://drhider.pythonk8s.dev.nubes.ru/
|
||||
- Дизайн фронтенда: `/home/naeel/nubes/design/`
|
||||
- ВМ (legacy): `5.172.178.213`, ssh-ключ `~/.ssh/naeel_vm_id_ed25519`
|
||||
- Переменные окружения: LLM_API_KEY, LLM_URL, LLM_MODEL
|
||||
|
||||
### Коммит
|
||||
|
||||
`0707d53` — Документация: README.md, ARCHITECTURE.md, DEPLOY.md, .github/copilot-instructions.md
|
||||
@@ -0,0 +1,84 @@
|
||||
# v0.0.32 — фиксы по ревью обфускатора — 2026-08-18
|
||||
|
||||
**Дата:** 2026-08-18
|
||||
**Версия:** 0.0.31 → 0.0.32
|
||||
**Ветка:** master (коммит `94e588a`, после rebase над `c3c6e5b`)
|
||||
**Источник:** ревью `History/2026-08-18-review-sonnet-bugs.md`
|
||||
|
||||
---
|
||||
|
||||
## Внесённые изменения
|
||||
|
||||
### 1. Единый обфускатор на все файлы (ТОП-1 — согласованность токенов)
|
||||
- **Было:** `api_bp.py` обрабатывал каждый файл отдельным `obfuscate_files()` →
|
||||
новая инстанс `TwoPassObfuscator` на файл → одна сущность получала РАЗНЫЕ токены
|
||||
в разных файлах, `all_mapping` (ручной разбор CSV) был неверен.
|
||||
- **Стало:** все файлы сессии → один `obfuscate_files(...)` с общим mapping.
|
||||
Проверено: «ООО Ромашка» в двух файлах → один токен «Сфера_0001».
|
||||
- **Побочно:** убран хрупкий ручной разбор CSV `split(",", 2)` (ломавшийся на
|
||||
запятых в значениях) — больше не нужен при едином вызове.
|
||||
|
||||
### 2. Рекурсивный `expand_zips` (ТОП-2)
|
||||
- `extractor.py:expand_zips` переписан: очередь + распаковка вложенных ZIP.
|
||||
ZIP внутри ZIP теперь раскрывается. Проверено: вложенный `doc1.txt` извлекается.
|
||||
|
||||
### 3. SSE-дисконнект (ТОП-3)
|
||||
- `api_bp.py:generate()`: ловится `(GeneratorExit, BrokenPipeError,
|
||||
ConnectionResetError)` вместо одного `GeneratorExit`.
|
||||
- `obfuscate_files` выполняется в отдельном потоке; прогресс через очередь;
|
||||
флаг отмены (`threading.Event`) при разрыве соединения.
|
||||
- Коллбек прогресса `(phase, idx, total, fname)` где phase ∈ {start, done} —
|
||||
согласуется с фронт-протоколом (`start`/`done` по `idx`).
|
||||
|
||||
### 4. Кириллица 1С (CP437→CP866)
|
||||
- `extractor.py._decode_name`: если имя не-UTF-8 (флаг 0x800 снят) и содержит
|
||||
не-ASCII → `name.encode("cp437").decode("cp866")`. Проверено изолированно.
|
||||
|
||||
### 5. Дедупликация имён
|
||||
- `builder.py:build_zip`: одинаковое имя+контент → пропуск; имя+разный контент →
|
||||
суффикс `_2`, `_3`... Проверено: `['a.md', 'a_2.md']`.
|
||||
- `obfuscator.py:_dedupe_file_names`: уникализация имён на входе до прохода 1
|
||||
(защита `all_texts[fname]` от перезаписи).
|
||||
|
||||
### 6. upload(): цикл по всем файлам
|
||||
- `api_bp.py:upload()`: обрабатывает все `request.files.getlist("files")`
|
||||
(поддержка загрузки целых папок через `webkitdirectory`).
|
||||
- Сохранено поведение ошибок: файл без имени → «No filename» (совместимо с тестом).
|
||||
|
||||
### 7. Лимит объёма сессии
|
||||
- `session.py`: `MAX_SESSION_BYTES = 500 MB`; в `add_file` — скип при превышении.
|
||||
|
||||
### 8. Защита от ZIP-бомб
|
||||
- `extractor.py`: проверка `total + file_size > LIMIT` ДО `zf.read`; скип архива
|
||||
при превышении ratio/лимита/числа файлов; лимиты глобально на вызов.
|
||||
|
||||
---
|
||||
|
||||
## Тесты
|
||||
|
||||
| Набор | Результат |
|
||||
|-------|-----------|
|
||||
| test_builder | 20/20 OK |
|
||||
| test_zip | 28/28 OK |
|
||||
| test_replacer | 10/10 OK |
|
||||
| test_scanner | 20/20 OK |
|
||||
| test_upload | 13/13 OK |
|
||||
| test_extractor | **14/15** — падает `.doc` (внешний сервис liberta) |
|
||||
|
||||
`test_extractor` `.doc`: отправляется мусорный OLE2-магик
|
||||
`b"\xD0\xCF\x11\xE0\x00"*10`, сервис `liberta.containerk8s.dev.nubes.ru/convert`
|
||||
возвращает «conversion produced no .docx». НЕ регрессия от v0.0.32 — ветка
|
||||
`.doc`/`doc_to_markdown` не менялась. Вероятно, сервис изменил поведение или
|
||||
не принимает невалидный контент. Требует отдельного разбора.
|
||||
|
||||
---
|
||||
|
||||
## Git
|
||||
|
||||
- Remote был впереди (посторонний коммит `c3c6e5b` про магистратуру) → `git pull --rebase`.
|
||||
- Push успешен: `c3c6e5b..94e588a`.
|
||||
- Ветка-сохранение документации: `save-docs-2026-08-18` (`de072d8`).
|
||||
|
||||
## TODO / открытые вопросы
|
||||
- [ ] Разобраться с тестом `.doc` (возможно, обновить тест на актуальный сервис).
|
||||
- [ ] Визуально проверить SSE-прогресс во фронте после рефакторинга.
|
||||
@@ -0,0 +1,45 @@
|
||||
# v0.0.39 — оптимизация apply_replacements (однопроходная замена) — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.38 → 0.0.39
|
||||
**Файл:** `drhider/replacer.py`, `site/app.py` (версия)
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Обфускация PDF 19 МБ шла 132.7с, из них ИИ (LLM) всего 7с — остальное (~125с)
|
||||
тратил основной код. Замер выявил узкое место: `apply_replacements`.
|
||||
|
||||
## Причина
|
||||
|
||||
`apply_replacements` для **каждой** сущности из mapping делала **2 полных
|
||||
прохода `re.sub` по всему тексту** (основной + фоллбэк `_md_tolerant_pattern`).
|
||||
Сложность O(N_сущностей × размер_текста × 2) — квадратичная по числу сущностей.
|
||||
При 300+ сущностей и тексте в несколько МБ — десятки секунд.
|
||||
|
||||
Замер (0.9 МБ, 304 сущности): **17.3 с**.
|
||||
|
||||
## Решение
|
||||
|
||||
Однопроходная замена:
|
||||
- Все ключи собираются в **один** regex-паттерн через альтернацию `(?:...|...)`
|
||||
(по убыванию длины, с `re.escape`, границы `(?<!\w)...(?!\w)` для alnum-сущностей).
|
||||
- Один `re.sub` с callback (lookup в mapping), O(text).
|
||||
- Фоллбэк `_md_tolerant_pattern` применяется **только к ключам, не найденным**
|
||||
основной заменой (отслеживание через `matched_keys`), а не ко всем.
|
||||
|
||||
## Результат замера
|
||||
- 300 сущностей реально в тексте (1.3 МБ): **1.17 с** (было бы десятки сек).
|
||||
- Реалистичный сценарий (10 сущностей): **0.29 с**.
|
||||
- Согласованность токенов и mapping.csv сохранены (проверено через `obfuscate`).
|
||||
|
||||
## Проверка
|
||||
- `test_replacer` 10/10, `test_builder` 20/20, `test_scanner` 20/20, `test_zip` 28/28.
|
||||
- Пайплайн `obfuscate`: «ООО Ромашка»→«Вектор_0001» в двух файлах согласовано,
|
||||
mapping.csv корректен.
|
||||
|
||||
## Примечание
|
||||
- `scan_regex` (0.4с) и upload (сеть ~1.7 МБ/с) — не узкие места, не трогали.
|
||||
- `pdf_to_markdown` (pdfplumber) локально не замерен (нет библиотеки); остаётся
|
||||
потенциальным вторым узким местом для будущей оптимизации (pypdf/pdfminer).
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-08-20 — Устойчивость к битым файлам (v0.0.53)
|
||||
|
||||
## Root cause (найден по логу /tmp/drhider.log)
|
||||
SSE "connection failed" при обработке НЕ был обрывом шлюза. Воркер падал целиком:
|
||||
```
|
||||
worker: exception: PdfminerException(PDFSyntaxError('No /Root object! - Is this really a PDF?'))
|
||||
```
|
||||
на битом PDF (миниатюра 142 байта). Исключение в `extract_text` не было обёрнуто
|
||||
по-файлово → воркер ронялся → SSE слал `error` → фронт показывал "SSE connection failed".
|
||||
|
||||
## Фикс (drhider/obfuscator.py)
|
||||
Проход 1: `extract_text` обёрнут в try/except. При ошибке файл добавляется в
|
||||
`skipped`, логируется warning, файл пропускается, остальные обрабатываются.
|
||||
Проход 2: файлы из `skipped` пропускаются (не попадают в результат).
|
||||
|
||||
Проверено на flat/ (10 файлов: 5 валидных + 5 битых миниатюр):
|
||||
- битые PDF/docx пропущены (PDFSyntaxError/BadZipFile), валидные 5 → в zip.
|
||||
- воркер не падает, complete доходит.
|
||||
|
||||
## Примечание
|
||||
Миниатюры 54-142 B в /mnt/y/T/flat — НЕ валидные PDF/docx (мусор из корзины).
|
||||
Их можно удалить из тестового набора (или оставить как кейс устойчивости).
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-08-20 — КРИТИЧНО: mapping.csv (ключ расшифровки) убран из выходного ZIP (v0.0.56)
|
||||
|
||||
## Проблема (утечка секретных данных)
|
||||
Выходной ZIP содержал `mapping.csv` — таблицу соответствия «оригинал → замена»,
|
||||
т.е. ключ расшифровки (реальные ФИО/телефоны/ИНН ↔ токены). Если ZIP передать
|
||||
третьему лицу — тот получал и обфусцированные файлы, и таблицу расшифровки.
|
||||
CSV должен храниться/скачиваться ОТДЕЛЬНО от обфусцированных файлов.
|
||||
|
||||
## Фикс (drhider/builder.py `build_zip`)
|
||||
- Убрана запись `mapping.csv` в архив. Теперь в ZIP — ТОЛЬКО обфусцированные файлы.
|
||||
- Параметр `mapping_csv` сохранён в сигнатуре (для совместимости вызовов), но в
|
||||
архив не пишется.
|
||||
- CSV продолжает генерироваться (`build_mapping_csv`) и отдаваться отдельно
|
||||
через `/api/csv/<sid>` (кнопка «Скачать CSV»), как написано на странице.
|
||||
|
||||
## Тесты (tests/test_builder.py)
|
||||
- Блок «ZIP с mapping.csv» переписан: теперь проверяет, что mapping.csv НЕ в архиве.
|
||||
- Интеграционный блок: ZIP содержит только doc.md, CSV проверяется отдельной строкой.
|
||||
- Все 106 тестов OK.
|
||||
|
||||
## Проверка
|
||||
- python3 tests/test_builder.py — 20/20 OK (включая новый сценарий «mapping не в zip»).
|
||||
- Остальные тесты — без изменений поведения, OK.
|
||||
- VERSION 0.0.56.
|
||||
@@ -0,0 +1,44 @@
|
||||
# 2026-08-20 — Честный ответ Sonnet об ускорении + реализация А (v0.0.55)
|
||||
|
||||
## Честный ответ Sonnet (History/2026-08-20-sonnet-query-honest-speed.md)
|
||||
Вопрос: можно ли значительно ускорить обработку, не рискуя устойчивостью/таблицами?
|
||||
|
||||
Ответ Sonnet (принято, согласуется с замерами):
|
||||
- **Безопасно ускорить extract_text() на pdfplumber НЕЛЬЗЯ** — pdfminer pure Python,
|
||||
CPU-bound, GIL полностью блокирует threading. (Подтверждено: extract_text 64.4с на Spartan10.)
|
||||
- A) Безопасно-просто:
|
||||
- A1. Threading для .doc (liberta, IO-bound) — N×~120с → ~120с при нескольких .doc.
|
||||
- A2. Кэш compiled regex между файлами (проход 2).
|
||||
- B) Заметный выигрыш, но с рисками (ОТЛОЖЕНО):
|
||||
- B1. ProcessPool по файлам (spawn) — 3 PDF × 25с → ~30с (только 2+ PDF, CPU=2).
|
||||
- B2. ProcessPool по страницам (spawn) — Spartan10 64с → ~35с (сложно: content, порядок).
|
||||
- C) Не стоит:
|
||||
- fork ProcessPool при session в памяти (COW-раздутие, 4Gi на пределе) — только spawn.
|
||||
- Threading по страницам/файлам PDF — GIL, ноль.
|
||||
- Итог Sonnet: безопасного значительного ускорения ОДНОГО PDF нет. Для набора 2+ PDF —
|
||||
B1. При CPU=2 потолок 2x.
|
||||
|
||||
## Решение пользователя
|
||||
«Делай по А, безопасно» — реализованы ТОЛЬКО A1 и A2. B — НЕ отложено (не делаем).
|
||||
|
||||
## Реализация (v0.0.55)
|
||||
|
||||
### A1 — threading для .doc (obfuscator.py, проход 1)
|
||||
- .doc файлы (конвертация через HTTP liberta, IO-bound) запускаются в
|
||||
ThreadPoolExecutor(max_workers=min(4, N_doc)).
|
||||
- Основной цикл сохраняет порядок: для .doc берёт future.result(), остальные —
|
||||
последовательно. progress_cb/порядок start/done не меняются.
|
||||
- Выигрыш: HTTP-конвертация .doc перекрывается с CPU-обработкой PDF/docx.
|
||||
|
||||
### A2 — кэш compiled regex (replacer.py + obfuscator.py)
|
||||
- replacer.py: вынесен `_build_combined_re(sorted_keys)`, `apply_replacements` получил
|
||||
параметр `compiled_re=None` (если None — компилирует сам).
|
||||
- obfuscator.py: после формирования `self._sorted_keys` компилируется один раз
|
||||
`self._compiled_re`, передаётся во все вызовы apply_replacements (проход 2).
|
||||
- __init__/finally: _compiled_re инициализируется/очищается.
|
||||
|
||||
## Проверка
|
||||
- Все 106 тестов OK (test_replacer 10/10 — apply_replacements с compiled_re).
|
||||
- Интеграция: flat/ (10 файлов) → 5 обработано (EB.md, EB1.md, Spartan10, TKM, mini_78b),
|
||||
битые пропущены, .doc через liberta в потоках. 78.3с (в основном Spartan10 64с extract_text).
|
||||
- VERSION 0.0.55.
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-08-20 — Файловый лог в поде (v0.0.52)
|
||||
|
||||
## Проблема
|
||||
В v0.0.51 добавили логирование в stderr, но на платформе Штурвал
|
||||
`kubectl logs` НЕ показывает stdout/stderr приложения (fd 1/2 python -> pipe,
|
||||
который читает супервизор платформы, а не kubelet). Логи приложения недоступны.
|
||||
|
||||
## Решение
|
||||
`setup_logging()` в site/app.py:
|
||||
- логи в stderr (для локального запуска)
|
||||
- + `FileHandler` в `/tmp/drhider.log` (путь через env `LOG_FILE`)
|
||||
- уровень через env `LOG_LEVEL` (DEBUG/INFO/WARNING)
|
||||
- root.handlers.clear() + свои handler'ы (не полагаемся на basicConfig)
|
||||
- если файл не открылся — не падаем, пишем в stderr
|
||||
|
||||
## Как читать логи в поде
|
||||
```
|
||||
kubectl exec -n 20a75175-a58c-49cb-b8fa-e86367b1a8dc <pod> -c app -- cat /tmp/drhider.log
|
||||
```
|
||||
(или tail -f). Версия 0.0.52.
|
||||
|
||||
## Проверено локально
|
||||
Файл /tmp/drhider.log создаётся, пишет "Logging configured, version=0.0.52"
|
||||
и сообщения логгеров.
|
||||
@@ -0,0 +1,32 @@
|
||||
# 2026-08-20 — Максимальное логирование (v0.0.51)
|
||||
|
||||
## Проблема
|
||||
SSE рвётся на проде (пару минут), обработка при этом продолжается (воркер жив).
|
||||
Логов в поде не было — приложение не писало в stdout, нельзя было диагностировать
|
||||
обрыв (внешний шлюз vs генератор).
|
||||
|
||||
## Решение
|
||||
1. `site/app.py` — `setup_logging()`:
|
||||
- уровень из env `LOG_LEVEL` (DEBUG/INFO/WARNING), default INFO
|
||||
- `logging.basicConfig(..., stream=sys.stderr, force=True)` — в stdout/stderr пода
|
||||
- формат с таймстампом; уровни для логгеров drhider/app/routes/session
|
||||
- `LOG_LEVEL` задаётся через env-переменные приложения на платформе
|
||||
2. `site/routes/api_bp.py` — подробные логи:
|
||||
- upload: каждый файл (имя, размер), итог, ошибки
|
||||
- process_stream: start, worker start/done (время, токены, zip_len),
|
||||
каждое событие progress (start/done) с idx/name/elapsed,
|
||||
disconnect на heartbeat/progress/complete/error (с причиной),
|
||||
result/complete, error event
|
||||
3. VERSION поднята до 0.0.51
|
||||
|
||||
## Проверка (локально, DEBUG)
|
||||
Upload 1 файла + SSE: видны все события от upload до complete.
|
||||
`LLM NER failed: Illegal header value b'Bearer '` — ожидаемо без ключа (локально).
|
||||
|
||||
## Как читать логи пода после деплоя
|
||||
```
|
||||
kubectl logs -n 20a75175-a58c-49cb-b8fa-e86367b1a8dc <pod> --tail=500 --timestamps
|
||||
```
|
||||
- Если `process_stream: disconnect on progress` — клиент/шлюз оборвал.
|
||||
- Если worker дошёл до `complete`, а клиент не получил — рвёт шлюз/браузер.
|
||||
- Если `worker: exception` — ошибка обработки.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 2026-08-20 — В1+В2 из ревью Sonnet (v0.0.54)
|
||||
|
||||
## В2 — фикс кракозябр имён zip (extractor.py `_decode_name`)
|
||||
Добавлена попытка decode("utf-8") ПЕРЕД decode("cp866"), с валидацией диапазона:
|
||||
- результат UTF-8 принимается, только если все символы — ASCII или кириллица (U+0400–U+04FF)
|
||||
(отсекает случайную коллизию CP866→UTF-8, напр. «Т»+«г» = U+04A3);
|
||||
- при ошибке или выходе из диапазона — фоллбэк decode("cp866") (реальные 1С).
|
||||
|
||||
Проверено:
|
||||
- CP866-зип (zip_subfolders_cp866.zip) → имена корректны (1_Металлургия.pdf…)
|
||||
- UTF-8 с флагом (zip_subfolders_utf8.zip) → корректны
|
||||
- test_zip 28/28 OK
|
||||
|
||||
## В1 — фильтр перед extract_tables() (extractor.py `pdf_to_markdown`)
|
||||
Добавлен: `if page.lines or page.curves or page.rects:` перед `extract_tables()`.
|
||||
Задумывалось как ускорение сканов (extract_tables на страницах без линий впустую).
|
||||
|
||||
### ⚠️ ФАКТ замера (опровергает гипотезу Sonnet)
|
||||
На Spartan10Manual.pdf (14.8 МБ, 619 стр):
|
||||
- extract_text суммарно 64.4с (104мс/стр) — УЗКОЕ МЕСТО
|
||||
- extract_tables суммарно 0.2с (0мс/стр) — ничтожно
|
||||
- на 272 страницах без линий extract_tables суммарно 0.0с — почти мгновенно
|
||||
|
||||
ВЫВОД: фильтр В1 НЕ даёт ускорения (extract_tables и так дешёвый). Реальное узкое
|
||||
место — extract_text (pdfminer). Фильтр оставлен как безопасная защита (таблицы
|
||||
не теряет: на 0144-03-2023_отчет об оценке.pdf 94=94 таблицы, потеряно страниц 0),
|
||||
но эффект ускорения ≈ 0.
|
||||
|
||||
### Реальный путь ускорения (отложено)
|
||||
Узкое место — extract_text (104мс/стр × 619 стр = 64с). Ускорение только через:
|
||||
- распараллеливание extract_text по страницам/файлам (ProcessPool) — отложено,
|
||||
требует замера рисков (fork/память, CPU=2);
|
||||
- или смену извлечения текста без потери таблиц (риск: таблицы критичны).
|
||||
|
||||
## Тесты
|
||||
Все 106 тестов OK (test_zip 28, test_extractor 15, test_builder 20, test_replacer 10,
|
||||
test_scanner 20, test_upload 13). VERSION 0.0.54.
|
||||
@@ -0,0 +1,26 @@
|
||||
# v0.0.72 — Ретраи pull, корректная ошибка лимита, счётчик дедупа (2026-08-24)
|
||||
|
||||
_code-fixes. По итогам код-ревью (см. `infra/2026-08-24-upload-logic-schema.md`)._
|
||||
|
||||
## 1. Ретраи pull (site/routes/api_bp.py, `upload_refs`)
|
||||
- `PULL_RETRIES = 3`, `PULL_RETRY_DELAY = 2` (сек).
|
||||
- Pull из ВМ-буфера теперь в цикле: при любой ошибке (DNS `gaierror -5`, сеть) — до 3 попыток
|
||||
с паузой 2с. После исчерпания — проброс исходной ошибки (502 «Pull failed»).
|
||||
- Закрывает инцидент 18:42 (разовый DNS-сбой ронял всю загрузку).
|
||||
|
||||
## 2. Корректная ошибка лимита сессии (`upload_refs`)
|
||||
- `add_file` возвращает `False` и для «сессия исчезла», и для «превышен лимит 500МБ».
|
||||
- Теперь: при `False` — если `get_files(sid) is None` → 404 «Session not found»;
|
||||
иначе (лимит) → файл пропускается (`delete` с ВМ) и загрузка продолжается.
|
||||
Раньше оба случая давали ложное «Session not found».
|
||||
|
||||
## 3. Счётчик «Добавлено из папки» (site/templates/index.html)
|
||||
- `addFileWithDedup` теперь возвращает `true` (файл добавлен) / `false` (дедуп).
|
||||
- `added++` только при `true` — дубли больше не завышают счётчик.
|
||||
- Проверено: папка [Договор.txt, Договор.txt (дубль), Акт.txt] → «Добавлено из папки: 2 файла».
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK, `py_compile` (app.py, api_bp.py) — OK.
|
||||
- Локально: счётчик дедупа работает (2 файла при одном дубле).
|
||||
- Ретраи/лимит — на проде после деплоя.
|
||||
- Версия 0.0.71 → 0.0.72.
|
||||
@@ -0,0 +1,46 @@
|
||||
# v0.0.73 — фиксы по код-ревью Соннета (безопасность + явные баги) (2026-08-24)
|
||||
|
||||
_code-fixes. По ревью `History/sonnet/2026-08-24-code-review-sonnet.md` (промпт `docs/code-review-sonnet.md`)._
|
||||
_Критично перепроверены в коде; спорные пункты — отклонены/отложены (см. ниже)._
|
||||
|
||||
## Исправлено (6 фиксов)
|
||||
|
||||
1. **SSRF** (`api_bp.py::upload_refs`) — добавлена валидация `url.startswith(VM_UPLOAD_PREFIX)`;
|
||||
недоверенный URL пропускается. Константа `VM_UPLOAD_PREFIX = "https://contracts.kube5s.ru/drhider-upload/"`.
|
||||
2. **Path traversal / zip slip** (`api_bp.py`) — функция `_safe_name()`: нормализует слэши,
|
||||
отбрасывает `..` и абсолютные пути, сохраняя подпапки (`Подпапка/Акт.txt` → как есть,
|
||||
`../../evil.pdf` → ""). Применена в `upload` и `upload_refs`.
|
||||
Unit-тест 8 кейсов — все OK.
|
||||
3. **Слабый SID** (`session.py`) — `uuid.uuid4().hex[:12]` → `uuid.uuid4().hex` (128 бит).
|
||||
4. **Серверная ошибка не отображалась** — серверное событие `event: error` (конфликт с встроенным
|
||||
EventSource) → `event: proc_error`; добавлен клиентский `addEventListener('proc_error', …)`
|
||||
с показом сообщения.
|
||||
5. **ZIP-бомба: обход через поддельный `file_size`** (`extractor.py`) — добавлена проверка
|
||||
`total_uncompressed > MAX_UNCOMPRESSED` ПОСЛЕ `zf.read()` с `break` (ранний выход).
|
||||
6. **Self-XSS через имя файла** (`index.html`) — добавлена `esc()` и применена к `f.name`
|
||||
в `rr()` и `procRow()`.
|
||||
|
||||
## Отклонено (критично к Соннету)
|
||||
- **«cleanup(sid) после /download»** — НЕ сделано: ZIP и CSV скачиваются РАЗДЕЛЬНЫМИ запросами;
|
||||
удаление сессии после отдачи ZIP сломало бы скачивание CSV. Сессия и так чистится по TTL (30 мин).
|
||||
|
||||
## Отложено (требуют решения/риск)
|
||||
- idx-мисматч при `expand_zips` (фильтр расширений на бэке) — связано с фичей v0.0.69
|
||||
(zip без документов «как есть»), требует решения по поведению.
|
||||
- Отмена не проверяется в extract-фазе (.doc liberta 120с) — отдельный фикс.
|
||||
- LLM-таймаут тихо обнуляет чанк — логирование/статус.
|
||||
- Debug-эндпоинт `session_files` — ограничить/закрыть.
|
||||
- LLM prompt injection — задокументировать (класс риска, не фикс кода).
|
||||
|
||||
## Проверка
|
||||
- `py_compile` (app.py, api_bp.py, session.py, extractor.py) — OK; `node --check` — OK.
|
||||
- `_safe_name` unit-тест — 8/8 OK.
|
||||
- `create_app()` стартует (VERSION 0.0.73).
|
||||
- Версия 0.0.72 → 0.0.73.
|
||||
|
||||
## Проверка на проде (редеплой 20:00, v0.0.73)
|
||||
- SSRF: `POST /api/upload_refs` с `url=http://169.254.169.254/...` → `{"count":0,"ok":true}` (URL отклонён).
|
||||
- Path traversal: `name="../../evil.txt"` → `{"count":0,"ok":true}` (имя отклонено).
|
||||
- SID теперь полный 32 hex (128 бит) — виден в ответе.
|
||||
- Регресс: папка с подпапкой + кириллицей → «Обработано 2 файлов: 4.3с»,
|
||||
в ZIP `Подпапка/Договор_1.md` + `Акт.md` (пути сохранены, `_safe_name` не сломал подпапки).
|
||||
@@ -0,0 +1,30 @@
|
||||
# v0.0.74 — отмена в extract-фазе (2026-08-24)
|
||||
|
||||
_code-fixes. Доработка по код-ревью Соннета (пункт «отмена не работает в extract-фазе»)._
|
||||
|
||||
## Что сделано (`drhider/obfuscator.py`)
|
||||
- В проходе 1 (извлечение) добавлена проверка `cancel_event.is_set()` между файлами:
|
||||
при отмене цикл извлечения прерывается (`break`), LLM-сканирование пропускается,
|
||||
результат помечается `cancelled` (processed=0, пустой ZIP + mapping.csv только заголовок).
|
||||
- Раньше отмена ждала завершения ВСЕХ извлечений, включая медленные `.doc` (liberta, до 120с/файл).
|
||||
|
||||
## Проверка
|
||||
- `py_compile` — OK, `import` drhider — OK.
|
||||
- Unit-тест: `cancel_event` установлен до старта → `meta={'cancelled': True, 'processed': 0, 'total': 2}`,
|
||||
пустой ZIP (22 Б), CSV только заголовок.
|
||||
|
||||
## Критично к Соннету (по итогам проверки)
|
||||
- **«LLM-таймаут тихо обнуляет чанк»** — ОТКЛОНЕНО: `_call_llm` уже логирует
|
||||
(`log.warning("LLM NER failed: %s", e)`) в `except Exception`. Таймаут не «тихий».
|
||||
- **«cleanup после /download»** — ОТКЛОНЕНО ранее (ZIP и CSV скачиваются раздельно, см. v0.0.73).
|
||||
|
||||
## Осталось (не трогаем, задокументировано)
|
||||
- idx-мисматч `expand_zips` (фильтр расширений) — краевой случай, спорное поведение.
|
||||
- LLM prompt injection — класс риска, не фикс кода.
|
||||
- debug-эндпоинт `session_files` — мелочь, может сломать тесты.
|
||||
|
||||
Версия 0.0.73 → 0.0.74.
|
||||
|
||||
## Проверка на проде (редеплой 20:07, v0.0.74)
|
||||
- Версия v0.0.74 — на проде.
|
||||
- Регресс: обычный цикл «Обработано 2 файлов: 1.9с» — фикс отмены не сломал обработку.
|
||||
@@ -0,0 +1,31 @@
|
||||
# v0.0.75 — кнопка при 0 файлов, фильтр zip, README-риски (2026-08-24)
|
||||
|
||||
_code-fixes. Закрытие отложенных пунктов списка «что далее»._
|
||||
|
||||
## Что сделано
|
||||
1. **Кнопка «Обфусцировать» активна при 0 файлов** (`index.html::resetAll`) —
|
||||
убрано перекрытие `ub.disabled = false` после `rr()` (rr() уже ставит disabled при пустом списке).
|
||||
Из TODO (`docs/WhatTODO.md`, пункт 1).
|
||||
2. **idx-мисматч `expand_zips`** (`drhider/extractor.py`) — синхронизирован фильтр расширений
|
||||
с фронтом `listZipFiles`: из zip берутся ТОЛЬКО документы (`.pdf .doc .docx .txt .md`)
|
||||
и вложенные `.zip`. Unit-тест: zip [txt,pdf,xlsx,ini] → раскрыты только txt,pdf.
|
||||
3. **README** — добавлена секция «Известные ограничения / риски»: LLM prompt injection,
|
||||
кейс «zip без документов».
|
||||
|
||||
## Не сделано (решение)
|
||||
- **`session_files` оставлен** — используется для диагностики (тесты),
|
||||
SID теперь 128-бит (не угадывается), риск мал.
|
||||
|
||||
## ⚠️ Замечено (требует решения)
|
||||
- В `README.md` в открытом виде закоммичен `LLM_API_KEY` (секрет!). Желательно вынести
|
||||
в переменные окружения Штурвала и убрать из README.
|
||||
|
||||
## Проверка
|
||||
- `py_compile` (app.py, extractor.py) — OK; `node --check` — OK.
|
||||
- `expand_zips` unit-тест — OK (только документы).
|
||||
- `resetAll` больше не содержит `ub.disabled = false` (5 оставшихся мест — нужные ветки).
|
||||
- Версия 0.0.74 → 0.0.75.
|
||||
|
||||
## Проверка на проде (редеплой 20:20, v0.0.75)
|
||||
- Кнопка «Обфусцировать»: disabled при 0 файлов → активна с файлом → disabled после «Новой сессии».
|
||||
- Регресс: zip с документом раскрыт (справка.txt), «Обработано 2 файлов: 4.4с» — фильтр zip не сломал.
|
||||
@@ -0,0 +1,29 @@
|
||||
# v0.0.67 — Фикс кириллических имён из ZIP на фронтенде (2026-08-24)
|
||||
|
||||
_code-fixes. Имя `TKM_6й_Семестр_ЭКЗАМЕН.docx` из архива отображалось как
|
||||
`TKM_6╨╣_╨б╨╡╨╝╨╡╤Б╤В╤А_...` (мусор)._
|
||||
|
||||
## Причина
|
||||
Архиватор записал имя **в UTF-8, но без UTF-8-флага** (bit 11). Фронт (`decodeZipName`)
|
||||
видел «флага нет», шёл в legacy-ветку и декодировал **UTF-8-байты как CP866** →
|
||||
`D0 99` («й») → `╨╣` (псевдографика CP866).
|
||||
|
||||
## Фикс (index.html, `decodeZipName`)
|
||||
- Первым делом — **строгая проверка UTF-8** (`TextDecoder('utf-8', {fatal:true})`):
|
||||
если байты — валидный UTF-8 с кириллицей или печатаемым текстом → берём как есть.
|
||||
- Только если строгий UTF-8 не проходит (реальные CP437/CP866 из 1С) — legacy-путь
|
||||
(CP437 → CP866) не меняется.
|
||||
- CP866-кириллица (0x80–0xAF) — это продолжения UTF-8 без ведущих байтов, поэтому
|
||||
строгий UTF-8 для них честно падает и legacy-путь работает как раньше.
|
||||
|
||||
## Согласованность с бэком
|
||||
Бэк (`extractor.py`, `_decode_name`) это уже умел (v0.0.54, «В2 — фикс кракозябр имён zip»).
|
||||
Теперь фронт и бэк обрабатывают имена ZIP одинаково.
|
||||
|
||||
## Проверка (node)
|
||||
- `TKM_6й_Семестр_ЭКЗАМЕН.docx` (UTF-8 без флага) — декодируется верно ✅.
|
||||
- ASCII — ✅. Legacy CP437/CP866-путь не тронут.
|
||||
- `node --check` — OK.
|
||||
|
||||
## Версия
|
||||
- 0.0.66 → 0.0.67.
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-08-31 — удалена случайная строка из `api_bp.py`
|
||||
|
||||
## Ответ Fable: анализ архитектуры DrHider
|
||||
|
||||
Сервис предназначен для обезличивания документов `.docx`, `.pdf`, `.doc`, `.txt` и `.zip`.
|
||||
Основной пайплайн: `extractor` → `scanner` (regex + LLM) → `replacer` → `builder`,
|
||||
оркестрация выполняется через `TwoPassObfuscator`. Веб-часть использует Flask,
|
||||
blueprint'ы `main`, `health` и `api`, а загрузка файлов реализована через переиспользуемый
|
||||
модуль с ВМ-буфером и pull.
|
||||
|
||||
## Найденная проблема
|
||||
|
||||
В `site/routes/api_bp.py` между инициализацией blueprint и определением первой функции
|
||||
находилась строка:
|
||||
|
||||
```text
|
||||
TMP/примеры_договоров_для_ИИ
|
||||
```
|
||||
|
||||
Это невалидный Python-код и причина `SyntaxError` при импорте приложения. Строка удалена
|
||||
без изменения остального API.
|
||||
|
||||
## Версия и проверка
|
||||
|
||||
- Исправление выпущено как версия `0.0.77` вместо `0.0.76`.
|
||||
- Рабочая копия до исправления не содержала незакоммиченных изменений.
|
||||
@@ -0,0 +1,13 @@
|
||||
# 2026-08-31 — документирован sync проекта на ВМ
|
||||
|
||||
В README добавлен рабочий порядок синхронизации текущего checkout на ВМ:
|
||||
|
||||
- источник: `/home/naeel/nubes/drhider`;
|
||||
- цель: `/home/naeel/drhider`;
|
||||
- инструмент: `rsync` через SSH;
|
||||
- исключены `.git`, кэши Python, `.venv` и `.pytest_cache`;
|
||||
- `--delete` не используется;
|
||||
- после копирования выполняются проверка версии и `py_compile`.
|
||||
|
||||
Legacy-каталог `/home/naeel/contracts/drhider` явно отмечен как недопустимая цель:
|
||||
там работает старый standalone-сервис `contracts-drhider`.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Диагностика ERR_CONNECTION_RESET в браузере
|
||||
|
||||
**Дата:** 2026-07-14
|
||||
|
||||
---
|
||||
|
||||
## Хронология
|
||||
|
||||
### Фаза 1: MTU (решено)
|
||||
|
||||
Проблема: 63KB POST из браузера → `ERR_CONNECTION_RESET`. Причина: MTU 1500 + Geneve 50 = 1550 > underlay 1450.
|
||||
Решение: Cilium MTU = 1400. Проверено curl с ВМ (30/30).
|
||||
|
||||
### Фаза 2: HTTP/2
|
||||
|
||||
HTTP/2 → `ERR_HTTP2_PROTOCOL_ERROR`. HTTP/1.1 → `ERR_CONNECTION_RESET`.
|
||||
Решение: `use-http2: "false"`.
|
||||
|
||||
### Фаза 3: Поиск корневой причины RST
|
||||
|
||||
Баг: браузер → свежая страница → POST 63KB → `ERR_CONNECTION_RESET`.
|
||||
|
||||
## Полная матрица тестов
|
||||
|
||||
| # | Сценарий | Результат |
|
||||
|---|----------|-----------|
|
||||
| 1 | Браузер: свежая страница → POST 63KB | ❌ RST / `⏳ 100%` hang |
|
||||
| 2 | Браузер: свежая страница → GET (до Flask) → POST 63KB | ✅ 200 OK |
|
||||
| 3 | Браузер: свежая страница → POST 10B → POST 63KB | ✅ 200 OK |
|
||||
| 4 | Браузер: свежая страница → GET (nginx сам, 405) → POST 63KB | ❌ RST |
|
||||
| 5 | `curl` с ВМ (5.172.178.213) → POST 63KB | ✅ всегда <100ms |
|
||||
| 6 | Сразу после `kubectl rollout restart` ingress → браузер | ✅ работает |
|
||||
| 7 | Через 5-10 мин после рестарта → браузер | ❌ снова RST |
|
||||
|
||||
## Что исключено
|
||||
|
||||
| Гипотеза | Статус | Почему |
|
||||
|----------|--------|--------|
|
||||
| MTU | ❌ исключено | 1400, curl с ВМ всегда работает |
|
||||
| HTTP/2 | ❌ исключено | `use-http2: false`, та же проблема |
|
||||
| XHR vs fetch | ❌ исключено | оба метода падают одинаково |
|
||||
| `proxy_request_buffering: off` | ❌ исключено | `on` для `location /` |
|
||||
| `client-body-timeout: 10` | ❌ исключено | 63KB < 1 сек |
|
||||
| FD/worker-лимиты | ❌ исключено | 1M FD, 16K connections, 62MB mem |
|
||||
| OOM/Restart | ❌ исключено | 0 рестартов, нет OOMKilled |
|
||||
| Stale upstream keepalive | ❌ исключено | `keepalive=0` не помогло |
|
||||
| nginx→Flask (upstream) | ❌ исключено | curl с ВМ доказывает что работает |
|
||||
|
||||
## Эксперименты
|
||||
|
||||
### Перезапуск ingress временно чинит
|
||||
|
||||
После `kubectl rollout restart` — браузер работает. Через 5-10 мин — снова RST. Накопление состояния.
|
||||
|
||||
### Ресурсы в норме
|
||||
|
||||
```
|
||||
worker_rlimit_nofile: 1047552 (1M FD)
|
||||
worker_connections: 16384
|
||||
worker_processes: 4
|
||||
Memory: 62 MiB, CPU: 3-7m
|
||||
Restarts: 0, OOMKilled: нет
|
||||
```
|
||||
|
||||
### `upstream-keepalive-connections: 0` — НЕ фикс
|
||||
|
||||
Баг вернулся через 5-10 минут. Keepalive pool не при чём.
|
||||
|
||||
## Критическое противоречие
|
||||
|
||||
- `curl` с ВМ (тот же датацентр) → POST 63KB → **всегда 200 OK <100ms**
|
||||
- Браузер через 5-10 мин после рестарта ingress → **ERR_CONNECTION_RESET**
|
||||
|
||||
curl и браузер идут через один VIP (185.247.187.151), на одни ingress-поды.
|
||||
Разница: клиент (curl vs Chrome) и сетевой путь (локальная сеть ДЦ vs интернет/VPN).
|
||||
|
||||
## Текущая конфигурация (2026-07-14)
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| `use-http2` | `"false"` |
|
||||
| `upstream-keepalive-connections` | `"0"` |
|
||||
| `error-log-level` | `debug` |
|
||||
| `proxy-body-size` | `1024m` |
|
||||
| `client-body-timeout` | `"10"` |
|
||||
| `keep-alive` | `"10"` |
|
||||
| Cilium MTU | `1400` |
|
||||
| `externalTrafficPolicy` | `Cluster` |
|
||||
| Ingress controller | `1.12.6` (Штурвал) |
|
||||
|
||||
---
|
||||
|
||||
## Вопрос к Claude (Sonnet/Opus)
|
||||
|
||||
### Симптом
|
||||
|
||||
Браузер (Chrome/Electron, Windows) → POST 63KB multipart/form-data → `ERR_CONNECTION_RESET` или зависание на `⏳ 100%`. Тело уходит полностью, ответ не приходит — TCP RST.
|
||||
|
||||
### Окружение
|
||||
|
||||
```
|
||||
Кластер: bare-metal K8s 1.34.1, 4 ноды (3 worker + 1 control-plane)
|
||||
CNI: Cilium Geneve, MTU подов = 1400
|
||||
VIP: kube-vip 185.247.187.151 (ARP на все 4 ноды)
|
||||
Ingress: nginx-ingress (Штурвал Helm), контроллер 1.12.6
|
||||
2 реплики, externalTrafficPolicy: Cluster
|
||||
Приложение: Flask/Waitress, 1 реплика (Штурвал managed)
|
||||
```
|
||||
|
||||
Конфиг ingress:
|
||||
```yaml
|
||||
use-http2: "false"
|
||||
upstream-keepalive-connections: "0"
|
||||
client-body-timeout: "10"
|
||||
keep-alive: "10"
|
||||
worker-processes: 4
|
||||
worker-rlimit-nofile: 1047552
|
||||
worker-connections: 16384
|
||||
proxy-body-size: 1024m
|
||||
proxy-request-buffering: on # для location /
|
||||
error-log-level: debug
|
||||
```
|
||||
|
||||
### Критическое противоречие
|
||||
|
||||
- `curl` с ВМ (5.172.178.213, тот же датацентр) → POST 63KB → **всегда 200 OK <100ms**
|
||||
- Браузер через 5-10 мин после рестарта ingress → **ERR_CONNECTION_RESET**
|
||||
- Сразу после рестарта ingress → браузер работает
|
||||
|
||||
curl и браузер идут через один VIP (185.247.187.151), на одни ingress-поды. Разница: клиент (curl vs Chrome) и сетевой путь до VIP (локальная сеть ДЦ vs интернет/VPN).
|
||||
|
||||
### Вопросы
|
||||
|
||||
1. Почему рестарт ingress помогает на 5-10 минут, а потом браузерные запросы снова получают RST, при том что curl с ВМ продолжает работать?
|
||||
|
||||
2. Может ли `externalTrafficPolicy: Cluster` + kube-vip создавать conntrack-записи, которые со временем ломают соединения из интернета (с другими TCP options/MSS) но не из локальной сети?
|
||||
|
||||
3. Стоит ли попробовать `externalTrafficPolicy: Local`? Какие риски?
|
||||
|
||||
4. Какие ещё эксперименты можно провести без tcpdump (нет доступа к нодам по SSH, нет tcpdump в подах)?
|
||||
|
||||
---
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Ответ от Claude
|
||||
- [ ] `externalTrafficPolicy: Local` — эксперимент
|
||||
- [ ] Воспроизвести с `curl` из WSL на машине пользователя
|
||||
- [ ] tcpdump через `kubectl debug` (требует координации)
|
||||
- [ ] Обновить ingress controller 1.12.6 → актуальный
|
||||
- [ ] Проверить `client-body-timeout: 60` + `keep-alive: 75`
|
||||
|
||||
---
|
||||
|
||||
## Ответ Claude (Sonnet) — 2026-07-14
|
||||
|
||||
### Два независимых механизма
|
||||
|
||||
Симптом объясняется наложением двух механизмов:
|
||||
|
||||
### Механизм 1: kube-vip ARP flapping + conntrack asymmetry (основной)
|
||||
|
||||
Именно это объясняет 5-10-минутное окно и разницу между browser и curl.
|
||||
|
||||
**Как работает с `externalTrafficPolicy: Cluster`:**
|
||||
|
||||
```
|
||||
Browser → VIP → Node N1 (kube-vip leader) → iptables DNAT+SNAT → nginx pod
|
||||
│
|
||||
└── conntrack entry создаётся на N1:
|
||||
(browser_ip:srcport → 185.247.187.151:443)
|
||||
↔ (N1_ip:eph → nginx_pod:443)
|
||||
```
|
||||
|
||||
Проблема возникает когда kube-vip меняет лидера:
|
||||
|
||||
```
|
||||
kube-vip: N1 → N2 (истёк lease или GARP от N2)
|
||||
Коммутатор: обновляет ARP-кэш, VIP теперь на N2
|
||||
|
||||
Browser (старое HTTP keep-alive соединение):
|
||||
→ PSH/ACK 63KB → попадает на N2
|
||||
N2: нет conntrack записи → TCP RST
|
||||
|
||||
curl (новое соединение):
|
||||
→ SYN → попадает на N2
|
||||
N2: создаёт новую conntrack → всё ОК
|
||||
```
|
||||
|
||||
**Почему 5-10 минут:** время жизни ARP-записи на коммутаторе или интервал переизбрания kube-vip лидера. "ARP на все 4 ноды" — периодические GARP-анонсы могут уходить с разных нод.
|
||||
|
||||
**Почему GET-to-Flask чинит последующий POST (тесты #2 vs #4):**
|
||||
- GET → Flask: создаётся *новое* TCP-соединение (новый SYN) → правильный conntrack на текущей ноде. POST идёт по *тому же* соединению → conntrack жив → OK.
|
||||
- GET → 405: nginx отвечает сам и закрывает соединение с `Connection: close`. POST создаёт *новое* соединение, но VIP уже на другой ноде → RST.
|
||||
|
||||
### Механизм 2: `keepalive_timeout: 10s` — слишком агрессивно
|
||||
|
||||
`keep-alive: "10"` в ConfigMap = `keepalive_timeout 10s` в nginx.
|
||||
|
||||
```
|
||||
T=0: Browser загружает страницу (GET /)
|
||||
T=10: nginx закрывает idle соединение (отправляет FIN)
|
||||
T=?: Пользователь заполняет форму (>10 сек → почти всегда)
|
||||
T=X: Browser отправляет POST по "живому" с его точки зрения соединению
|
||||
→ nginx уже закрыл socket → RST
|
||||
```
|
||||
|
||||
Браузер не всегда замечает FIN немедленно (TCP буферизация ОС). curl — каждый раз новое соединение.
|
||||
|
||||
### Почему curl всегда работает
|
||||
|
||||
| | Browser | curl |
|
||||
|---|---|---|
|
||||
| Соединение | HTTP keep-alive, долгоживущее | Новое для каждого запроса |
|
||||
| conntrack | Создано при SYN, может устареть | Новый SYN на каждый вызов |
|
||||
| keepalive | Держит idle >10s | Сразу POST → не ждёт |
|
||||
| VIP flap | Пакет идёт на новую ноду без conntrack | SYN на новой ноде создаёт conntrack |
|
||||
|
||||
### Ключевой инсайт
|
||||
|
||||
> Разница не в TCP-опциях (MSS, window size). Разница в **типе пакета**: curl всегда начинает с SYN → conntrack создаётся на правильной ноде. Browser держит соединение между запросами → когда VIP мигрирует, браузерные пакеты (ACK/PSH) приходят на новую ноду без предшествующего SYN → нет conntrack-записи → RST.
|
||||
|
||||
### Итоговая таблица причин
|
||||
|
||||
| Причина | Механизм | Почему не curl |
|
||||
|---|---|---|
|
||||
| kube-vip ARP lease renewal | Смена ноды → conntrack orphan | Новый SYN всегда |
|
||||
| `keepalive_timeout: 10s` | nginx закрывает idle conn → browser не замечает | Нет idle |
|
||||
|
||||
Оба фактора вместе: рестарт сбрасывает состояние, 5-10 минут → kube-vip переизбирается / браузер держит соединение дольше 10s → RST.
|
||||
|
||||
### Рекомендации
|
||||
|
||||
1. **`keep-alive: "75"`** — исключить механизм #2 (самое простое, безопасное)
|
||||
2. **`externalTrafficPolicy: Local`** — исключить механизм #1 (риск: потеря source IP для других сервисов)
|
||||
3. **JS: Connection warmer** — GET на Flask при загрузке страницы (уже доказано тестом #2)
|
||||
4. **JS: пересоздавать соединение** — использовать `fetch()` без keep-alive (credentials: 'omit' или отдельный subdomain)
|
||||
@@ -0,0 +1,158 @@
|
||||
# Cluster Dump — 2026-07-15 17:00 MSK
|
||||
|
||||
## 1. НОДЫ (4 шт)
|
||||
|
||||
| Нода | Роль | Taints |
|
||||
|------|------|--------|
|
||||
| iot-naeel-control-plane-xb699 | control-plane | `NoSchedule:control-plane` |
|
||||
| iot-naeel-workers-vqphm-6f74n | workers | нет |
|
||||
| iot-naeel-workers-vqphm-bhbvs | workers | нет |
|
||||
| iot-naeel-workers-vqphm-v8zq4 | workers | нет |
|
||||
|
||||
## 2. INGRESS (shturval-ingress-controller)
|
||||
|
||||
### Поды (2 реплики)
|
||||
|
||||
| Под | Нода | Возраст |
|
||||
|-----|------|---------|
|
||||
| ...86jsp | control-plane-xb699 | ~6ч |
|
||||
| ...jm4kl | workers-vqphm-v8zq4 | ~6ч |
|
||||
|
||||
### Сервис
|
||||
- Тип: LoadBalancer
|
||||
- External IP: 185.247.187.151
|
||||
- `externalTrafficPolicy: Cluster`
|
||||
- NodePorts: 80:30739, 443:32391
|
||||
|
||||
### Ingress ConfigMap
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| keep-alive | **10** |
|
||||
| client-body-timeout | **10** |
|
||||
| client-header-timeout | **10** |
|
||||
| proxy-body-size | **8m** |
|
||||
| upstream-keepalive-connections | **0** |
|
||||
| use-http2 | false |
|
||||
| worker-processes | 4 |
|
||||
| error-log-level | info |
|
||||
|
||||
### Helm
|
||||
- Chart: shturval-ingress-controller-2.12.1
|
||||
- Ревизия 9 (сегодня 11:53) — `replicaCount: 2`, hostPort enabled
|
||||
- Ревизия 8 (сегодня 02:00) — то же самое
|
||||
- Более старых ревизий нет (почищены)
|
||||
|
||||
### Affinity (ingress deploy)
|
||||
```yaml
|
||||
preferredDuringScheduling:
|
||||
- weight: 10 → избегать control-plane
|
||||
- weight: 80 → предпочитать node-role: ingress
|
||||
- weight: 50 → предпочитать node-role: infra
|
||||
```
|
||||
|
||||
### События (каждые 51 сек!)
|
||||
```
|
||||
Warning SyncLoadBalancerFailed failed to ensure load balancer: no address pools could be found
|
||||
Normal EnsuringLoadBalancer Ensuring load balancer
|
||||
```
|
||||
|
||||
## 3. KUBE-VIP
|
||||
|
||||
### DaemonSet: shturval-vip (4 пода, на ВСЕХ 4 нодах)
|
||||
- Image: r.shturval.tech/kube-vip:v1.0.0
|
||||
- ARP mode: `vip_arp: true`
|
||||
- Election: `svc_election: true`
|
||||
- Leaderelection: `vip_leaderelection: false`
|
||||
- Lease: `sht-uservip-cp-lock`
|
||||
|
||||
### Cloud Provider: shturval-vip-provider (1 под, на bhbvs)
|
||||
- Image: r.shturval.tech/kubevip/kube-vip-cloud-provider:v0.0.12
|
||||
- Ищет ConfigMap: `kubevip` в неймспейсе `kube-system`
|
||||
|
||||
### ❌ ConfigMap `kubevip` в `kube-system` — **ПУСТОЙ!**
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: kubevip
|
||||
namespace: kube-system
|
||||
data: {} # <-- НЕТ ДАННЫХ
|
||||
```
|
||||
|
||||
**Это причина ошибки «no address pools could be found»!**
|
||||
|
||||
## 4. DRHIDER (pythonk8s)
|
||||
|
||||
### Деплой
|
||||
- 1 реплика
|
||||
- Ревизия: 49
|
||||
- Инит-контейнер: клонирует из Git, потом pip install + python app.py
|
||||
- Ресурсы: 500m CPU, 1Gi RAM
|
||||
- Проба: TCP :5000
|
||||
|
||||
### ❌ PodAffinity (добавлен сегодня 16:46, непрошеный)
|
||||
```yaml
|
||||
requiredDuringScheduling:
|
||||
podAffinity → ingress pod на той же ноде
|
||||
```
|
||||
|
||||
### Сервис
|
||||
- ClusterIP: 10.102.171.63
|
||||
- Port 80 → targetPort 5000
|
||||
- `internalTrafficPolicy: Cluster`
|
||||
|
||||
### Ingress (drhider)
|
||||
- Host: drhider.pythonk8s.dev.nubes.ru
|
||||
- Аннотации: proxy-body-size=1024m, connect=120s, read/send=600s
|
||||
- TLS: letsencrypt-prod
|
||||
|
||||
### Поды
|
||||
| Под | Нода | Возраст |
|
||||
|-----|------|---------|
|
||||
| pythonk8s-6698c6c78-dkwsq | workers-vqphm-v8zq4 | ~15 мин |
|
||||
|
||||
Drhider на одной ноде с ingress-подом jm4kl.
|
||||
|
||||
## 5. ВСЕ INGRESS-РЕСУРСЫ
|
||||
|
||||
| Хост | Неймспейс | Возраст |
|
||||
|------|-----------|---------|
|
||||
| drhider.pythonk8s.dev.nubes.ru | 20a75175-... | 4д |
|
||||
| loadtest.pythonk8s.dev.nubes.ru | 9039a501-... | 5д |
|
||||
| contractor.pythonk8s.dev.nubes.ru | b4523aba-... | 7ч |
|
||||
| capire.kube5s.ru | default | 54д |
|
||||
| grafana.kube5s.ru | grafana | 66д |
|
||||
| keycloak.k8c.ru | keycloak | 96д |
|
||||
| qu.kube5s.ru | shared-sqs | 93д |
|
||||
| iot.kube5s.ru | sless | 94д |
|
||||
| terra.k8c.ru | terra | 92д |
|
||||
|
||||
## 6. CILIUM
|
||||
|
||||
4 пода (по одному на ноду), все Running. MTU был изменён на 1400 (по STATE).
|
||||
|
||||
## 7. КЛЮЧЕВЫЕ НАХОДКИ
|
||||
|
||||
### 🔴 ConfigMap `kubevip` пустой
|
||||
kube-vip-cloud-provider не может найти address pools → каждые 51 сек ошибка SyncLoadBalancerFailed. При этом `185.247.187.151` работает через ARP-mode kube-vip DaemonSet (не через cloud provider). Вероятно, это не fatal, но указывает на недонастроенную интеграцию.
|
||||
|
||||
### 🔴 Ingress ConfigMap отличается от ожидаемого
|
||||
- keep-alive: 10 (должен быть 75)
|
||||
- client-body-timeout: 10 (должен быть 60)
|
||||
- client-header-timeout: 10 (должен быть 30)
|
||||
- proxy-body-size: 8m (должен быть 1024m)
|
||||
|
||||
Но для drhider это переопределено в аннотациях ingress-ресурса.
|
||||
|
||||
### 🟡 Ingress 2 реплики на 4 нодах с kube-vip
|
||||
kube-vip анонсирует 185.247.187.151 на ВСЕХ 4 нодах, но ingress-поды только на 2. При `externalTrafficPolicy: Cluster` трафик на ноды без ingress: SNAT → кросс-нода → риск conntrack RST.
|
||||
|
||||
### 🟡 Helm: 2 апгрейда сегодня
|
||||
Ревизии 8 (02:00) и 9 (11:53) сегодня. Кто-то обновлял ingress. `replicaCount: 2` в обеих.
|
||||
|
||||
### 🟢 Аннотации drhider ingress — хорошие
|
||||
proxy-body-size=1024m, connect=120s, read/send=600s. Переопределяют дефолты ConfigMap.
|
||||
|
||||
### 🟢 Cilium — стабильный
|
||||
4 пода, все Running, MTU 1400.
|
||||
@@ -0,0 +1,204 @@
|
||||
# DrHider — итоговая диагностика ERR_CONNECTION_RESET
|
||||
|
||||
**Дата:** 2026-07-15 (финал)
|
||||
**Версия:** v0.0.29
|
||||
|
||||
---
|
||||
|
||||
## Резюме (моё мнение)
|
||||
|
||||
**`keep-alive: 10` — единственная причина ERR_CONNECTION_RESET.** Всё остальное (conntrack, hostPort, ARP flapping, svc_election) — шум, который мы искали 2 дня.
|
||||
|
||||
### Почему это так
|
||||
|
||||
VIP `185.247.187.151` жёстко привязан к control-plane-xb699 через аннотацию `kube-vip.io/vipHost`. Никакого flapping нет — трафик всегда приходит на одну ноду. Второй ingress-под на v8zq4 — избыточен, но не мешает.
|
||||
|
||||
Сценарий:
|
||||
1. Браузер загружает страницу — opens TCP
|
||||
2. Пользователь выбирает файлы — проходит >10 сек
|
||||
3. Nginx (keep-alive=10) закрывает idle соединение
|
||||
4. Браузер не замечает FIN (буферизация ОС)
|
||||
5. POST 63KB летит в мёртвый сокет → RST
|
||||
|
||||
### Почему предыдущие гипотезы неверны
|
||||
|
||||
| Гипотеза | Опровержение |
|
||||
|----------|-------------|
|
||||
| kube-vip ARP flapping | `vipHost` фиксирует VIP на одной ноде. Lease мёртв — плевать |
|
||||
| hostPort на 2 из 4 нод | Трафик не на все 4 ноды, а только на control-plane |
|
||||
| upstream keepalive stale | `keepalive=0` не помог |
|
||||
| MTU | Решено ещё 14-го, не при чём |
|
||||
| conntrack | ETPolicy Cluster не при чём — трафик на одну ноду |
|
||||
|
||||
### Что подтверждает keep-alive гипотезу
|
||||
|
||||
| Тест | Объяснение |
|
||||
|------|-----------|
|
||||
| Сразу после рестарта ingress → OK | Все соединения свежие |
|
||||
| Через 5-10 мин → RST | Соединения постарели >10с |
|
||||
| GET/health → POST → OK | Новое живое соединение |
|
||||
| curl с ВМ → всегда OK | curl открывает новый SYN каждый раз |
|
||||
| `keep-alive: 75` + JS warmup → OK на 10+ мин | Увеличили окно, warmup греет |
|
||||
|
||||
## Хронология
|
||||
|
||||
| Дата | Событие |
|
||||
|------|---------|
|
||||
| 2026-07-11 | Исходный деплой |
|
||||
| 2026-07-14 (день) | MTU fix (Cilium 1400) — решило 51с задержку |
|
||||
| 2026-07-14 (вечер) | RST диагностика: keep-alive 75 + JS warmup — частично |
|
||||
| 2026-07-15 (11:53) | Helm ревизия 9 — сброс ConfigMap на дефолты (keep-alive:10) |
|
||||
| 2026-07-15 | podAffinity `required` на drhider (ручной kubectl edit) |
|
||||
| 2026-07-15 (16:00) | Полная диагностика: hostPort vs cloud LB |
|
||||
|
||||
---
|
||||
|
||||
## Две независимые проблемы
|
||||
|
||||
### Проблема #1: MTU (решена 2026-07-14)
|
||||
|
||||
**Симптом:** 51с задержка при POST любого размера через внешний VIP.
|
||||
**Причина:** Geneve +50, underlay 1450, дефолт 1500 → фрагментация/дроп.
|
||||
**Решение:** Cilium `mtu: 1400`.
|
||||
|
||||
### Проблема #2: ERR_CONNECTION_RESET (хост порт, решена 2026-07-15)
|
||||
|
||||
**Симптом:** Браузер получает TCP RST при POST 63KB через 5-10 мин после рестарта ingress.
|
||||
**Ранее считалось:** keep-alive 10с или kube-vip conntrack.
|
||||
**Реальная причина:** Штурвал балансирует трафик на **все 4 ноды**, но ingress стоит `replicas: 2` с **hostPort** — порт 80/443 открыт только на 2 нодах.
|
||||
|
||||
```
|
||||
Browser → cloud LB → любая из 4 нод
|
||||
├── нода с ingress-подом → hostPort 80 → nginx → ✅
|
||||
└── нода БЕЗ ingress-пода → порт 80 не слушается → RST ❌
|
||||
```
|
||||
|
||||
**50% запросов попадает на пустую ноду → RST.**
|
||||
|
||||
---
|
||||
|
||||
## Подтверждающие данные
|
||||
|
||||
### Helm ревизии (дамп)
|
||||
|
||||
| Ревизия | Время | replicaCount | Другие изменения |
|
||||
|---------|-------|-------------|------------------|
|
||||
| 1 (деплой) | 2026-07-11 | 2 | — |
|
||||
| 8 | 2026-07-15 02:00 | **2** | — |
|
||||
| 9 | 2026-07-15 11:53 | **2** | — |
|
||||
|
||||
Helm **не менял** replicaCount. Всегда 2. Но ConfigMap сбрасывал на дефолты (keep-alive: 10, proxy-body-size: 8m и т.д.).
|
||||
|
||||
### Ingress ConfigMap (последствия Helm upgrade)
|
||||
|
||||
```yaml
|
||||
# Было (ручные правки 2026-07-14) → Стало (после ревизии 9)
|
||||
keep-alive: "75" → "10" # вернулось на дефолт Штурвала
|
||||
proxy-body-size: "1024m" → "8m" # тоже сброшено
|
||||
error-log-level: debug → info # сброшено
|
||||
upstream-keepalive-connections: "0" → сохранилось
|
||||
```
|
||||
|
||||
### kube-vip svc_election — мёртв
|
||||
|
||||
```yaml
|
||||
# Lease ingress/kubevip-shturval-ingress-controller-controller
|
||||
holderIdentity: "" # пусто — никто не держит
|
||||
leaseDurationSeconds: 1 # аномально короткий (норма 15-30)
|
||||
renewTime: 2026-07-11T14:55:28Z # 4 дня назад не обновлялся
|
||||
leaseTransitions: 17 # но было 17 переходов
|
||||
```
|
||||
|
||||
svc_election никогда не работал. VIP назначен через `ipMode: VIP` в статусе сервиса, работает на уровне облака (BGP/маршрутизация).
|
||||
|
||||
### hostPort
|
||||
|
||||
```yaml
|
||||
# В шаблоне пода ingress (Helm template)
|
||||
hostPort: 80
|
||||
hostPort: 443
|
||||
```
|
||||
|
||||
Включён в ревизиях 8 и 9. hostPort = порт слушается ТОЛЬКО на нодах где стоит под.
|
||||
|
||||
### podAffinity на drhider — временный костыль
|
||||
|
||||
```json
|
||||
{
|
||||
"podAffinity": {
|
||||
"requiredDuringSchedulingIgnoredDuringExecution": [{
|
||||
"labelSelector": {"matchLabels": {"app.kubernetes.io/name": "shturval-ingress-controller"}},
|
||||
"namespaceSelector": {"matchLabels": {"name": "ingress"}},
|
||||
"topologyKey": "kubernetes.io/hostname"
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Добавлен через `kubectl edit`, НЕ через Helm. Привязывает drhider к ноде с ingress-подом. Это НЕ лечит RST (RST на уровне VIP→нода, не на уровне нода→drhider). При следующем `helm upgrade` — исчезнет.
|
||||
|
||||
### Kube-vip DaemonSet — не участвует
|
||||
|
||||
```yaml
|
||||
vip_arp: true # ARP включён
|
||||
vip_leaderelection: false # нет CP election
|
||||
svc_election: true # должен быть — но lease мёртв
|
||||
```
|
||||
|
||||
Даемоны не логгируют VIP `185.247.187.151`. Трафик распределяет облако, не kube-vip.
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
**Единственное полное решение:** ingress-под на каждой ноде куда приходит трафик.
|
||||
|
||||
### Вариант 1 (рекомендуемый): увеличить replicas
|
||||
|
||||
```yaml
|
||||
# Helm values
|
||||
shturval-ingress-controller:
|
||||
replicaCount: 4
|
||||
```
|
||||
|
||||
Или выяснить у DevOps сколько нод в LB-пуле Штурвала и поставить `replicaCount = число нод`.
|
||||
|
||||
### Вариант 2 (если балансировка на все worker'ы): DaemonSet
|
||||
|
||||
```yaml
|
||||
# Helm values
|
||||
shturval-ingress-controller:
|
||||
kind: DaemonSet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что делать сейчас (ручной воркараунд)
|
||||
|
||||
### 1. Починить ConfigMap (срочно)
|
||||
|
||||
```bash
|
||||
kubectl patch configmap -n ingress shturval-ingress-controller-controller --type merge \
|
||||
-p '{"data":{"keep-alive":"75","proxy-body-size":"1024m","client-body-timeout":"60","client-header-timeout":"30"}}'
|
||||
```
|
||||
|
||||
### 2. Масштабировать ingress (временный фикс)
|
||||
|
||||
```bash
|
||||
kubectl scale deploy -n ingress shturval-ingress-controller-controller --replicas=4
|
||||
```
|
||||
|
||||
Поды раскидаются по всем 4 нодам → hostPort на всех → 0% RST.
|
||||
|
||||
⚠️ **Предупреждение:** после следующего `helm upgrade`:
|
||||
- ConfigMap сбросится (нужно править Helm values)
|
||||
- replicas вернётся на 2
|
||||
- podAffinity на drhider исчезнет
|
||||
|
||||
---
|
||||
|
||||
## Вопросы к DevOps
|
||||
|
||||
1. **Сколько нод в LB-пуле Штурвала?** (на какие ноды облако направляет трафик ingress?)
|
||||
2. **Можно ли увеличить `replicaCount` до числа нод?**
|
||||
3. **Как правильно изменить Helm values для ingress?** (чтобы ConfigMap не сбрасывался при upgrade)
|
||||
@@ -0,0 +1,37 @@
|
||||
# v0.0.46 — удалён process_names + git push HTTP/2 проблема — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.45 → 0.0.46
|
||||
|
||||
---
|
||||
|
||||
## Удалён тестовый API process_names
|
||||
|
||||
- `process_names` (обфускация файлов по именам из TEST_INPUT_DIR) **удалён**.
|
||||
- Причина: сервер не может читать локальные файлы клиента (нет доступа к его
|
||||
машине). Файлы в поде — неудобно. API бесполезен.
|
||||
- Удалены: endpoint, `TEST_INPUT_DIR`/`_TEST_API_ENABLED`, `import os`, History-файл.
|
||||
- Версия 0.0.46.
|
||||
|
||||
## Находка: git push к gitea зависал (HTTP/2)
|
||||
|
||||
**Симптом:** `git push origin master` висел бесконечно (таймауты), хотя
|
||||
`git ls-remote` и `curl` к gitea работали (200, быстро).
|
||||
|
||||
**Причина:** git использовал **HTTP/2** для отправки pack; шлюз
|
||||
`gitea.services.ngcloud.ru` блокировал передачу данных по HTTP/2
|
||||
(та же проблема, что была в drhider с HTTP/2 — ERR_HTTP2_PROTOCOL_ERROR).
|
||||
|
||||
**Решение:** push через HTTP/1.1 мгновенно прошёл:
|
||||
```bash
|
||||
git -c http.version=HTTP/1.1 push origin master
|
||||
# и закреплено в конфиге:
|
||||
git config http.version HTTP/1.1
|
||||
```
|
||||
|
||||
**Проверка:** `8d7a7df..d2f2bd3 master -> master`, синхронизировано.
|
||||
|
||||
## Полезно
|
||||
- На этой машине/шлюзе для git (и возможно других HTTP/2 клиентов) использовать
|
||||
HTTP/1.1.
|
||||
- git config `http.version HTTP/1.1` уже установлен.
|
||||
@@ -0,0 +1,124 @@
|
||||
# ПЛАН: перенос паттерна «ВМ-буфер + pull» в drhider
|
||||
|
||||
_Дата: 2026-08-21. Основа: `contracts/loadtest/History/2026-08-21-pattern-vm-buffer-pull.md`._
|
||||
_Статус: РЕАЛИЗОВАНО 2026-08-21 (v0.0.58), см. `2026-08-21-vm-upload-implemented.md`._
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
drhider (Managed Flask, Штурвал, `drhider.pythonk8s.dev.nubes.ru`) получает файлы через
|
||||
`POST /api/upload` (multipart). Входной шлюз managed-кластера обрывает тела >~64КБ
|
||||
(HTTP 000 ~10с). Решение — загружать файл на ВМ напрямую, Flask тянет сам (egress).
|
||||
|
||||
## Директива (обязательно)
|
||||
|
||||
**ВСЕ файловые операции загрузки в Flask (из браузера или вообще снаружи) — ТОЛЬКО через ВМ.**
|
||||
Никаких прямых `POST /api/upload` с телом файла во Flask.
|
||||
|
||||
## Среда (уточнено 2026-08-21)
|
||||
|
||||
- `iot-naeel` — СОБСТВЕННЫЙ кластер, параметры/аннотации правились через kubectl. Пока
|
||||
тестируем/смотрим поведение на нём (лимит 64КБ там может не воспроизводиться).
|
||||
- В дальнейшем деплой — на ОБЫЧНЫЙ managed-кластер (лимит 64КБ будет реальным).
|
||||
- ВМ — ОДНА (`5.172.178.213`, `contracts.kube5s.ru`), та же, что для loadtest. Location
|
||||
`/drhider-upload/` добавляется в существующий nginx рядом с `/lt-serve/`.
|
||||
- Вывод: VM-путь строим СЕЙЧАС, независимо от поведения iot-naeel — прод всё равно на обычном.
|
||||
|
||||
## Текущий поток
|
||||
|
||||
`uploadFiles()` (index.html ~413): по одному файлу в цикле → `FormData` → `POST /api/upload`.
|
||||
Бэк: `MAX_CONTENT_LENGTH=200МБ`, сессия 500МБ. HTTP-клиент — **httpx** (не requests).
|
||||
|
||||
## Целевой поток
|
||||
|
||||
```
|
||||
Браузер ──(PUT, файл целиком)──▶ ВМ nginx (без лимита 64КБ)
|
||||
│ └─ получил URL + токен
|
||||
│
|
||||
└──▶ Flask POST /api/upload_refs (JSON: [{name,size,url}], <64КБ)
|
||||
│
|
||||
▼
|
||||
Flask egress GET с ВМ ──▶ читает файл ──▶ add_file() ──▶ дальше как сейчас
|
||||
```
|
||||
|
||||
## Изменения (4 места)
|
||||
|
||||
### 1. ВМ `5.172.178.213`, `nginx-contracts.conf` — приём загрузки
|
||||
Новый location (рядом с `/lt-serve/`), приём через PUT + статическая отдача:
|
||||
```nginx
|
||||
location /drhider-upload/ {
|
||||
alias /var/www/drhider-upload/;
|
||||
dav_methods PUT;
|
||||
create_full_put_path on;
|
||||
client_max_body_size 1024m;
|
||||
# CORS: браузер с drhider.pythonk8s.dev.nubes.ru шлёт PUT (не simple → preflight)
|
||||
add_header Access-Control-Allow-Origin https://drhider.pythonk8s.dev.nubes.ru always;
|
||||
add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, DELETE' always;
|
||||
add_header Access-Control-Allow-Headers 'Content-Type' always;
|
||||
if ($request_method = OPTIONS) { return 204; }
|
||||
}
|
||||
```
|
||||
- Каталог `/var/www/drhider-upload/` (владелец `www-data`), НЕ `/home/naeel` (403).
|
||||
- Порядок: backup → правка → `nginx -t` → `systemctl reload nginx`.
|
||||
|
||||
### 2. Flask — новый endpoint pull (`site/routes/api_bp.py`)
|
||||
```python
|
||||
@api_bp.route("/upload_refs", methods=["POST"])
|
||||
def upload_refs():
|
||||
data = request.get_json(silent=True) or {}
|
||||
sid = data.get("session") or create_session()
|
||||
refs = data.get("files") or []
|
||||
if not refs:
|
||||
return jsonify({"ok": False, "error": "No files"}), 400
|
||||
added = 0
|
||||
with httpx.Client(timeout=120, follow_redirects=True) as client:
|
||||
for ref in refs:
|
||||
name, url = ref.get("name"), ref.get("url")
|
||||
if not name or not url:
|
||||
continue
|
||||
content = b"".join(client.stream("GET", url).iter_bytes()) # egress, по частям
|
||||
if not add_file(sid, name, content):
|
||||
return jsonify({"ok": False, "error": "Session not found"}), 404
|
||||
added += 1
|
||||
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
|
||||
```
|
||||
- Импорт `httpx` (уже в requirements.txt) и `file_count` из session.
|
||||
- `stream(...).iter_bytes()` — НЕ `content` целиком (OOM при 100МБ+).
|
||||
- Обработка ошибок egress (таймаут/403/404) → вернуть 502 с именем файла.
|
||||
|
||||
### 3. Фронт (`site/templates/index.html`, `uploadFiles()`)
|
||||
Разбить «Фазу 1» на два шага:
|
||||
- **Шаг 1a:** каждый файл → `fetch(<ВМ>/drhider-upload/<token>/<name>, {method:'PUT', body:file})`,
|
||||
собирать `[{name, size, url}]`. Прогресс — `xhr.upload.onprogress` заменить на `fetch` +
|
||||
`ReadableStream` (или оставить XHR, но на PUT к ВМ).
|
||||
- **Шаг 1b:** один `POST /api/upload_refs` с JSON `{session, files:[...]}` (<64КБ).
|
||||
- Токен: `crypto.randomUUID()`. URL ВМ — константа/`data-атрибут`.
|
||||
- Показывать прогресс «загрузка на ВМ» отдельно от «загрузка в Flask».
|
||||
|
||||
### 4. `requirements.txt` — без изменений (httpx уже есть).
|
||||
|
||||
## Безопасность и жизненный цикл
|
||||
- Токен в пути URL — только `crypto.randomUUID()`, не переиспользуется.
|
||||
- Flask после `add_file` делает `DELETE` на URL ВМ (или ВМ чистит по TTL — cron/systemd-timer
|
||||
`find /var/www/drhider-upload -mmin +30 -delete`).
|
||||
- ВМ отдаёт файл только по валидному URL с токеном (нет открытого листинга: `autoindex off`).
|
||||
|
||||
## Грабли (из pattern-дока + drhider)
|
||||
- CORS: PUT — не simple-метод → браузер шлёт OPTIONS preflight; nginx обязан отвечать 204.
|
||||
- `site/` конфликтует со stdlib `site.py` (для loadtest; у drhider — Штурвал, без gunicorn).
|
||||
- egress через `httpx` с `stream`, timeout 120; большие ОТВЕТЫ проходят (проверено 50МБ).
|
||||
- OOM: не читать файл в память целиком; сессия 500МБ уже ограничивает.
|
||||
- Локальные curl на Krupski идут через прокси `172.17.192.1:10808` → для теста ВМ `--noproxy '*'`.
|
||||
|
||||
## Порядок работ + проверка
|
||||
1. ВМ nginx (location + каталог + CORS) → проверить `curl --noproxy '*' -X PUT`.
|
||||
2. Flask `/api/upload_refs` → проверить `python3 -c "import py_compile; py_compile.compile(...)"`.
|
||||
3. Фронт `uploadFiles()` → `node -c` нет (это EJS/HTML+JS внутри шаблона) — ручная проверка.
|
||||
4. Сквозной тест: файл 10МБ → ВМ → Flask → обфускация → ZIP.
|
||||
5. Commit + push (правило: после правки + bump версии `VERSION` в `site/app.py`).
|
||||
|
||||
## Решения (закрыты)
|
||||
|
||||
- Кластер: сейчас `iot-naeel` (тест), прод — обычный managed. VM-путь обязателен.
|
||||
- ВМ: одна, `contracts.kube5s.ru` (`5.172.178.213`), общая с loadtest — добавляем location.
|
||||
@@ -0,0 +1,75 @@
|
||||
# ПЛАН: универсальный файловый сервис на ВМ (API)
|
||||
|
||||
_Дата: 2026-08-21. Статус: РЕАЛИЗОВАНО И ЗАДЕПЛОЕНО (v1.0.0), см. `README.md` сервиса `/home/naeel/nubes/vmfiles/` и `2026-08-21-vm-upload-implemented.md`._
|
||||
|
||||
## Цель
|
||||
|
||||
Один внутренний (НЕ публичный) сервис на ВМ `5.172.178.213` (`contracts.kube5s.ru`)
|
||||
для приёма/временного хранения/выдачи файлов. Потребители: **drhider**, **contracts**,
|
||||
**SQS IoT**. Заменяет сырой nginx-dav `/drhider-upload/` на задокументированный,
|
||||
версионированный API, чтобы любому сервису было ясно, как им пользоваться.
|
||||
|
||||
## Потребители и режимы
|
||||
- **drhider** — браузер (PUT файла) + сервер (pull). Нужен CORS для браузера.
|
||||
- **contracts** — сервер-к-серверу, много файлов из локали.
|
||||
- **SQS IoT** — сервер-к-серверу.
|
||||
|
||||
## Технология
|
||||
**FastAPI + uvicorn** (Python уже есть на ВМ).
|
||||
Почему FastAPI: автогенерация OpenAPI/Swagger (`/docs`) → «как юзать» видно из коробки;
|
||||
нативный streaming для больших тел.
|
||||
|
||||
## API v1 (контракт)
|
||||
|
||||
| Метод | Путь | Вход | Ответ |
|
||||
|---|---|---|---|
|
||||
| `POST` | `/api/v1/files` | тело=файл (raw или multipart `file`), header `X-App-Key` | `201 {"id","url","size","expires_at"}` |
|
||||
| `GET` | `/api/v1/files/{id}` | header `X-App-Key` | байты файла |
|
||||
| `DELETE` | `/api/v1/files/{id}` | header `X-App-Key` | `204` |
|
||||
| `GET` | `/api/v1/health` | — | `{"ok":true,"version":...}` |
|
||||
| `GET` | `/api/v1/docs` | — | Swagger UI (авто) |
|
||||
|
||||
Единые ошибки: `{"error": "...", "code": "unauthorized|not_found|too_large|expired|quota_exceeded"}`.
|
||||
|
||||
## Авторизация
|
||||
Заголовок `X-App-Key`, по одному секрету на приложение. Ключи — в конфиге на ВМ
|
||||
(файл `apps.yml`/`.env`): `drhider`, `contracts`, `sqs-iot`. Без валидного ключа — `401`.
|
||||
|
||||
## Хранилище и метаданные
|
||||
- Файлы: `/var/lib/vmfiles/` (или `/var/www/vmfiles/`, владелец — пользователь сервиса).
|
||||
- Метаданные: SQLite (`id, app, name, size, created_at, expires_at`).
|
||||
- id — серверный UUID (или клиентский токен — см. «открытые вопросы»).
|
||||
|
||||
## Лимиты и TTL
|
||||
- `max_file_size` (по умолчанию 1024m) и `quota` на приложение.
|
||||
- TTL по умолчанию 30 мин (на приложение можно переопределить).
|
||||
- Чистка — встроенная фоновая задача сервиса (вместо текущего cron-`find`).
|
||||
|
||||
## Деплой на ВМ
|
||||
1. Код сервиса — в отдельной папке на ВМ (или отдельный репозиторий).
|
||||
2. systemd-юнит `vmfiles.service` → `uvicorn app:app --host 127.0.0.1 --port 8769`.
|
||||
3. nginx: новый `location /api/v1/ { proxy_pass http://127.0.0.1:8769; ... }`
|
||||
+ `proxy_request_buffering off` (стримить большие тела) + `client_max_body_size 1024m`.
|
||||
CORS — отдать сервису (FastAPI), не nginx.
|
||||
4. Проверка: `nginx -t` → reload.
|
||||
|
||||
## Миграция drhider
|
||||
- Пока сервис поднимается — `/drhider-upload/` оставить как есть (не ломать текущее).
|
||||
- После готовности — перевести `uploadFiles()` и `/api/upload_refs` на `/api/v1/files`,
|
||||
убрать dav-location и cron-`find`.
|
||||
|
||||
## Порядок работ
|
||||
1. Скелет FastAPI + health + `/docs`.
|
||||
2. `POST/GET/DELETE /files` + SQLite-метаданные + TTL-чистка.
|
||||
3. `X-App-Key` авторизация + квоты.
|
||||
4. systemd + nginx `proxy_pass`.
|
||||
5. Тесты: curl (raw + multipart), большие файлы, expiry, quota.
|
||||
6. Перевести drhider на новый API, выпилить старый dav.
|
||||
7. Документ-спека (`README`/`API.md`) + примеры для contracts/SQS.
|
||||
|
||||
## Открытые вопросы (до «делай»)
|
||||
1. Язык: FastAPI (рекомендую) или Flask?
|
||||
2. id файла: сервер генерирует (POST) или клиент приносит токен (PUT)? Влияет на браузерный поток drhider.
|
||||
3. Загрузка: только raw-тело или поддерживать multipart тоже?
|
||||
4. Где живёт код сервиса — отдельный репозиторий на gitea или папка на ВМ?
|
||||
5. Ключи приложений — придумать сейчас или сервис сгенерирует при первом старте?
|
||||
@@ -0,0 +1,43 @@
|
||||
# ВМ-загрузка в drhider — реализовано (v0.0.58, 2026-08-21)
|
||||
|
||||
_Реализация паттерна «ВМ-буфер + pull» по плану `2026-08-21-pattern-vm-buffer-pull-plan.md`._
|
||||
_Снимок до изменений: ветка `save-pre-vm-upload-2026-08-21`._
|
||||
|
||||
## Что сделано
|
||||
|
||||
1. **Flask** (`site/routes/api_bp.py`) — новый `POST /api/upload_refs`:
|
||||
принимает JSON `{session, files:[{name,size,url}]}`, тянет каждый файл с ВМ
|
||||
ИСХОДЯЩИМ GET (httpx, stream по частям), кладёт в сессию `add_file()`,
|
||||
после успешного pull делает DELETE с ВМ. Ошибки egress → 502.
|
||||
2. **Фронт** (`site/templates/index.html`, `uploadFiles()`) — Фаза 1 переделана:
|
||||
- 1a: каждый файл → `PUT` на ВМ `https://contracts.kube5s.ru/drhider-upload/<токен>_<k>`
|
||||
(XHR, прогресс по-прежнему в строке), собирает refs.
|
||||
- 1b: один маленький `POST /api/upload_refs` (<64КБ).
|
||||
- Токен — `crypto.randomUUID()`. Константа `VM_UPLOAD_URL`.
|
||||
3. **ВМ** (`5.172.178.213`) — nginx `nginx-contracts.conf`:
|
||||
- `location /drhider-upload/` → `alias /var/www/drhider-upload/`,
|
||||
`dav_methods PUT DELETE`, `create_full_put_path on`, `client_max_body_size 1024m`,
|
||||
CORS-заголовки для `drhider.pythonk8s.dev.nubes.ru`, OPTIONS → 204.
|
||||
- Каталог `/var/www/drhider-upload/` (www-data).
|
||||
- Backup: `/tmp/nginx-contracts.conf.bak.20260821`.
|
||||
- TTL-чистка в root cron: `*/5 * * * * find /var/www/drhider-upload -type f -mmin +30 -delete`.
|
||||
4. **Версия** — `site/app.py`: 0.0.57 → 0.0.58.
|
||||
|
||||
## Отклонение от плана
|
||||
- URL файла на ВМ — БЕЗ имени: `.../drhider-upload/<токен>_<k>` (имя — только в метаданных).
|
||||
Иначе кириллица/пробелы в имени ломали бы URL.
|
||||
|
||||
## Тесты (2026-08-21)
|
||||
- ВМ: PUT 10МБ → 201 (1.0с); GET → 200, 10485760B; OPTIONS preflight → 204 с CORS;
|
||||
DELETE → 204; GET после DELETE → 404.
|
||||
- Сквозной: PUT на ВМ → `POST /api/upload_refs` → 200, файл в сессии (10485760B),
|
||||
файл с ВМ удалён (404).
|
||||
- `py_compile` api_bp/app — OK; `node --check` JS из index.html — OK.
|
||||
|
||||
## Замечания
|
||||
- ГРАБЛИ: на Krupski `dd` алиасится на `docker compose down` — для генерации файлов
|
||||
использовать `head -c N /dev/urandom > file`.
|
||||
- Локальные curl/httpx к contracts.kube5s.ru — через прокси (`172.17.192.1:10808`):
|
||||
для тестов `--noproxy '*'` / `NO_PROXY=contracts.kube5s.ru`.
|
||||
- Прод-безопасность (позже): токены без TTL-времени в самом URL, авторизация на ВМ
|
||||
(сейчас ключ — случайный UUID, файл живёт ≤30 мин по cron).
|
||||
@@ -0,0 +1,29 @@
|
||||
# Временный сбой сертификата gitea и push (2026-08-24)
|
||||
|
||||
_infra. Прецедент, чтобы не терять время в следующий раз._
|
||||
|
||||
## Симптом
|
||||
`git push origin master` к `gitea.services.ngcloud.ru` падал:
|
||||
```
|
||||
SSL: certificate subject name (*.ngcloud.ru) does not match target host name 'gitea.services.ngcloud.ru'
|
||||
```
|
||||
При этом в браузере `https://gitea.services.ngcloud.ru/Nail/drhider.git` открывался.
|
||||
|
||||
## Диагностика
|
||||
- `gitea.services.ngcloud.ru` → `194.31.9.41` (стабильно, и с машины, и с ВМ).
|
||||
- На `194.31.9.41:443` отдавался **wildcard `*.ngcloud.ru`** (SAN: `*.ngcloud.ru, ngcloud.ru`),
|
||||
который НЕ покрывает `gitea.services.ngcloud.ru` (два уровня: `services.ngcloud.ru`).
|
||||
- Внутренний ClusterIP `10.96.52.11` (gitea.mgmt.nubes.ru, hosts ВМ) недоступен ни с машины, ни с ВМ.
|
||||
- На ВМ git-настроек нет (нет sslVerify=false), `credential.helper=store`.
|
||||
|
||||
## Итог
|
||||
Проблема была **на стороне gitea/ngcloud.ru** — временно отдавался неверный (wildcard) сертификат.
|
||||
Через ~10-20 минут сертификат вернулся корректным (`CN = gitea.services.ngcloud.ru`,
|
||||
SAN `DNS:gitea.services.ngcloud.ru`) → `git push` прошёл (`0479afb..e31476e`).
|
||||
|
||||
## Урок
|
||||
- Если push к gitea падает по SSL «*.ngcloud.ru doesn't match» — это временный сбой сертификата
|
||||
на стороне gitea (не код). Проверить: `echo | openssl s_client -connect gitea.services.ngcloud.ru:443 -servername gitea.services.ngcloud.ru | openssl x509 -noout -subject -ext subjectAltName`.
|
||||
- Не менять git config (`sslVerify=false`) и не синкать на ВМ — подождать и повторить push.
|
||||
- Аналогичный сбой DNS был у `contracts.kube5s.ru` в 18:42 (см. `2026-08-24-pull-dns-transient.md`) —
|
||||
вероятно, общее инфраструктурное происшествие ngcloud.ru в этот день.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Секрет-гигиена: LLM_API_KEY в git (2026-08-24)
|
||||
|
||||
_infra/заметка. Решение пользователя: историю git НЕ переписывать._
|
||||
|
||||
## Что было
|
||||
- `LLM_API_KEY` (sk-ucI5Yv…) попал в git при написании документации (коммит `0707d53`,
|
||||
2026-07-12) — переменные окружения Штурвала скопированы в README/DEPLOY дословно, секрет не вычищен.
|
||||
|
||||
## Что сделано
|
||||
- Ключ убран из текущих файлов: `README.md`, `docs/DEPLOY.md`, `History/plans/2026-07-12-audit-and-plan.md`
|
||||
(коммит `f2141ec`, запушен).
|
||||
- Проверено: в текущих файлах ключа нет.
|
||||
|
||||
## Решение пользователя
|
||||
- **Историю git не переписываем** (filter-repo/filter-branch — нет, «хуй с ним»).
|
||||
- Ключ остаётся в git-истории (коммиты `0707d53`, `a2f754a`, `4c3f9d4` и др.), репозиторий публичный.
|
||||
|
||||
## Рекомендация (не выполнено)
|
||||
- Ротация `LLM_API_KEY` в Штурвале (старый считается скомпрометированным). Решение за пользователем.
|
||||
- На будущее: секреты в env Штурвала, в git — только заглушки «(секрет)».
|
||||
@@ -0,0 +1,28 @@
|
||||
# Разовый DNS-сбой pull: «Pull failed: [Errno -5] No address associated with hostname» (2026-08-24)
|
||||
|
||||
_infra. Вопрос пользователя «ЭТО ЧТО ????» при загрузке папки CNC (220 файлов, отобрано 8)._
|
||||
|
||||
## Симптом
|
||||
Фронт: «Ошибка передачи ссылок: Pull failed: [Errno -5] No address associated with hostname»
|
||||
при нажатии «Обфусцировать».
|
||||
|
||||
## Диагностика (факты)
|
||||
- ВМ-буфер: `https://contracts.kube5s.ru/drhider-upload/` (nginx dav), host → 5.172.178.213.
|
||||
- DNS снаружи (8.8.8.8) и на ВМ: `contracts.kube5s.ru` → 5.172.178.213 — ок; буфер отвечает 403.
|
||||
- Под drhider: ns `20a75175-a58c-49cb-b8fa-e86367b1a8dc`, pod `pythonk8s-85d8f4dcc8-vjjts`
|
||||
(создан ~18:36 МСК, редеплой 18:31-18:33). `getent`/`socket` из пода → 5.172.178.213 — ок.
|
||||
- Логи пода: ЕДИНСТВЕННАЯ ошибка `upload_refs: pull error sid=66ce16c1cda9` в **15:42:43 UTC (18:42 МСК)**:
|
||||
`connect_tcp.started host='contracts.kube5s.ru'` → `gaierror(-5)`.
|
||||
- До неё (15:41:41 UTC / 18:41 МСК) сессия пользователя `3ec718dafa5b` с теми же 8 файлами
|
||||
(info.txt, out1.txt, fatD.pdf, fatihaDownEdited.pdf, fatihaUPEdited.pdf, fatU.pdf, FT0.pdf, sha0.pdf)
|
||||
УСПЕШНО обработана (worker done, 29.8с, tokens 2138).
|
||||
- CoreDNS: 2 пода, 0 рестартов, 44d — стабильны.
|
||||
- Живой тест сейчас: папка 2 txt → «✅ Обработано 2 файлов: 6.6с» — pull работает.
|
||||
|
||||
## Вывод
|
||||
**Разовый DNS-сбой** CoreDNS/upstream на `contracts.kube5s.ru` в узкий момент 18:42 МСК
|
||||
(одна ошибка за весь лог). К выбору папки отношения нет; повторный запуск работает.
|
||||
|
||||
## Рекомендация (опция)
|
||||
Добавить ретраи pull в `upload_refs` (2-3 повтора с паузой при `ConnectError`/DNS-ошибке) —
|
||||
v0.0.70. Решение за пользователем.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Деплой: два кластера, iot-naeel актуальный, naeel-test-3 — не погашен (2026-08-24)
|
||||
|
||||
## Симптом
|
||||
- health/HTML внешнего URL отдавали **0.0.67**, а под в ns 20a75175 на naeel-test-3 был **0.0.61**
|
||||
(внутренний health 0.0.61, нет MAX_FILE_BYTES/cancel/ETA/UI).
|
||||
- Часть запросов (upload_refs) попадала на старый под → тесты вели себя непредсказуемо
|
||||
(60МБ «принимался»).
|
||||
|
||||
## Причина (подтверждена kubeconfig'ами обоих кластеров)
|
||||
- drhider развёрнут на **двух кластерах** в одном namespace-ID `20a75175-...`:
|
||||
- **iot-naeel**: под `pythonk8s-cf686f894-96l7m` (возраст ~42м — свежий редеплой), **v0.0.67**;
|
||||
ingress `drhider.pythonk8s.dev.nubes.ru` → **185.247.187.151**.
|
||||
- **naeel-test-3**: под `pythonk8s-797ddd69df-dmdg6` — **старый, не погашенный** (был 0.0.61);
|
||||
ingress → 185.247.187.147.
|
||||
- **DNS `drhider.pythonk8s.dev.nubes.ru` → 185.247.187.151 = iot-naeel** → внешний URL обслуживает
|
||||
iot-naeel (0.0.67). naeel-test-3 в DNS не участвует, но остаётся живым инстансом.
|
||||
|
||||
## Что сделано
|
||||
- kubeconfig'и на ВМ обновлены на оба кластера (`~/.kube/config` iot-naeel, `~/.kube/config-naeel-test-3`).
|
||||
- Проверены оба пода: и iot-naeel, и naeel-test-3 теперь **v0.0.67** (naeel-test-3 обновлён
|
||||
пересозданием пода — `scale 0 → 1` заставил git-clone master@0.0.67).
|
||||
- Внешний бэкенд — iot-naeel (0.0.67), все тесты прогнаны на нём (см. tests/2026-08-24-test-batch-v067.md).
|
||||
|
||||
## Осталось (рекомендация)
|
||||
- В Штурвале **погасить/удалить инстанс drhider на naeel-test-3** (ns 20a75175) — он не нужен,
|
||||
DNS ведёт на iot-naeel. Оставить один актуальный (iot-naeel, 0.0.67).
|
||||
- Проверить прочие `pythonk8s` на iot-naeel (contractor/loadtest/polygon/atest) — это другие приложения, не drhider.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Схема логики загрузки + найденные баги/несоответствия (2026-08-24)
|
||||
|
||||
_infra. Подробная схема по коду: index.html (фронт), api_bp.py, session.py, obfuscator.py, extractor.py._
|
||||
|
||||
## Участники
|
||||
- **Браузер** — index.html: выбор файлов/папки, PUT на ВМ, SSE-прогресс, статусы.
|
||||
- **ВМ-буфер** — nginx dav `https://contracts.kube5s.ru/drhider-upload/` (CORS разрешён только для origin `https://drhider.pythonk8s.dev.nubes.ru`).
|
||||
- **Бэк** — Flask (api_bp.py): upload_refs (pull), process_stream (SSE, воркер-поток), session.py (лимиты), drhider (extractor→scanner→replacer→builder).
|
||||
|
||||
## Этап 0 — выбор файлов (браузер)
|
||||
1. `fileInput` (multiple, accept) — для `.zip` → `listZipFiles` (раскрытие в браузере, allowedExt = .pdf .doc .docx .txt .md) → `addFileWithDedup`; иначе — как есть.
|
||||
2. `folderInput` (`webkitdirectory`) — рекурсивно: `webkitRelativePath.slice(1)` (отбрасываем верхнюю папку), zip раскрывается с префиксом пути, документы — с относительным путём, прочее — пропуск.
|
||||
3. `addFileWithDedup`: дедуп (имя+размер → пропуск; коллизия имени → суффикс `_2`); лимиты 50МБ/файл и 500МБ/сессия → `overNames` («🔥 не учитывается» в обычной таблице).
|
||||
|
||||
## Этап 1 — загрузка (uploadFiles, фаза 1)
|
||||
4. `toSend` = файлы БЕЗ `overNames`; over-файлы сразу получают статус «пропущен (лимит)» (v0.0.71).
|
||||
5. Для каждого файла: **PUT** на `VM_UPLOAD_URL + token + '_' + k` (имя в URL не несётся) → прогресс % в ячейке → `✓ N KB/s`. `refs.push({name, size, url})`. Таймаут 300с.
|
||||
6. **POST /api/upload_refs** {session, files: refs}:
|
||||
- бэк: для каждого ref: `size > 50МБ` → delete+skip; `GET url` (pull egress, timeout 120с) → `content`; `content > 50МБ` → delete+skip; `add_file(sid, name, content)` (лимит сессии 500МБ); `delete url` с ВМ.
|
||||
- ошибка сети/DNS → **502 «Pull failed»** (весь запрос падает).
|
||||
7. Тест-режим `?upload-only=1` — стоп после фазы 1.
|
||||
|
||||
## Этап 2 — обработка (SSE, фаза 2)
|
||||
8. `EventSource /api/process_stream/<sid>`; воркер-поток → `obfuscate_files(all_files)`.
|
||||
9. **obfuscator (проход 1)**: `expand_zips` (повторная распаковка zip, basename, защита от бомб) → `_dedupe_file_names` → для каждого: `extract_text` (исключение → `skipped`, событие `done` на этапе извлечения) → `scan_regex` → `extract_done` (total_chars, per_file) → `scan_llm_ner` (чанки: file_start/file_chunk/file_done; отмена → CancelRequested).
|
||||
10. **проход 2**: `apply_replacements` для каждого (битые/скан → пропуск; при отмене — только `llm_done`) → `done` → `build_zip` + `build_mapping_csv` → событие `result`/`cancelled`.
|
||||
11. Фронт: 3-секционная таблица (Обработанные / Текущий / Ожидают); статусы: `done` (до extract_done = битый → **skipped «не извлечён»** v0.0.71), `current`, `analyzed`, `pending`; `complete`/`cancelled` → статистика, кнопки ZIP/CSV, заморозка сессии.
|
||||
|
||||
## Найденные баги и несоответствия
|
||||
1. **[исправлено v0.0.71]** Статус «пропущен» использовался для ДВУХ разных причин: лимит и «не извлёкся». Теперь: «пропущен (лимит)» и «не извлечён» (пустой/битый/скан).
|
||||
2. **[исправлено v0.0.71]** over-файлы во время обработки попадали в группу «Ожидают обработки» (нет procState → pending) с оценкой времени. Теперь — отдельная группа «⛔ Пропущены (сверх лимита)».
|
||||
3. **[открыто]** `upload_refs` неатомарен: при ошибке pull (DNS/сеть) часть файлов уже добавлена в сессию и удалена с ВМ, но фронт получает 502 и бросает — сессия-сирота, файлы на ВМ частично остаются (инцидент 18:42). Нужны ретраи/атомарность.
|
||||
4. **[открыто]** `upload_refs` при превышении суммарного лимита сессии отвечает «Session not found» (404) — add_file возвращает False и для отсутствия сессии, и для лимита; сообщение неверное.
|
||||
5. **[открыто, риск]** Повторный `expand_zips` на бэке (с `os.path.basename` — обрезает пути) при отправке zip как есть (v0.0.69) ломает соответствие idx: количество файлов бэка ≠ фронта → `sendIdx[d.idx]` = undefined. Обычно не срабатывает (фронт сам раскрывает), но риск при расхождении allowedExt.
|
||||
6. **[открыто]** allowedExt не совпадают: фронт раскрывает из zip только документы, бэк `expand_zips` берёт ВСЕ файлы (без фильтра) — расхождение поведения при zip «как есть».
|
||||
7. **[открыто, косметика]** Счётчик «Добавлено из папки: N» завышается при дедупе (`added++` безусловно после `addFileWithDedup`).
|
||||
8. **[открыто]** Три разных отображения одной причины (лимит) до обработки / при старте / в обработке — унифицировано в v0.0.71 (группа over).
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK, `py_compile` — OK.
|
||||
- Локально (v0.0.71): группа «⛔ Пропущены (сверх лимита)» + «пропущен (лимит)» — работает.
|
||||
Полный цикл локально невозможен из-за CORS ВМ-буфера (разрешён только origin прода).
|
||||
- Версия 0.0.70 → 0.0.71.
|
||||
@@ -0,0 +1,45 @@
|
||||
# v0.0.34 — вывод токенов и времени LLM — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.33 → 0.0.34
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Пользователь замечает, что обфускация долгая. Добавлен вывод в UI:
|
||||
- суммарных токенов LLM за прогон;
|
||||
- суммарного времени работы LLM за прогон.
|
||||
|
||||
Строка статуса после завершения: «✅ Обработано N файлов за Xс · LLM: Yс · Z токенов».
|
||||
|
||||
## Изменения
|
||||
|
||||
### `drhider/llm_client.py`
|
||||
- В `LLMClient.__init__` добавлены кумулятивные счётчики:
|
||||
- `tokens_prompt`, `tokens_completion`, `llm_sec` (float);
|
||||
- свойство `tokens_total = prompt + completion`.
|
||||
- В `complete()` после ответа:
|
||||
- `self.llm_sec += r.elapsed.total_seconds()` — реальное время HTTP-вызова;
|
||||
- `usage` (из `r.json()`) → `self.tokens_prompt` / `self.tokens_completion`
|
||||
(через `.get(..., 0)` — устойчиво к отсутствию `usage`).
|
||||
- Возврат `complete()` не изменён (по-прежнему `content`) — `scan_llm_ner` не трогали.
|
||||
|
||||
### `site/routes/api_bp.py`
|
||||
- В `worker()` после `obfuscate_files` в очередь `result` добавляется
|
||||
`stats = {tokens: llm.tokens_total, llm_sec: round(llm.llm_sec, 1)}`.
|
||||
- В `event: complete` данные: `{total, tokens, llm_sec}`.
|
||||
|
||||
### `site/templates/index.html`
|
||||
- В обработчике `complete`: если `d.llm_sec > 0` — добавляет
|
||||
«· LLM: {llm_sec}с» и, если `d.tokens > 0` — «· {tokens} токенов».
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.34"`.
|
||||
|
||||
## Проверка
|
||||
- `py_compile` + `node --check` — OK.
|
||||
- Накопление: 2 вызова → 240 prompt + 90 completion = 330 total, llm_sec = 6.0.
|
||||
- Устойчивость: ответ без `usage` → токены 0, время 1.0 — без падения.
|
||||
- SSE end-to-end: `complete` приходит с `{"total":1,"tokens":0,"llm_sec":0.0}`
|
||||
(локально без ключа LLM — 0; в кластере с ключом — ненулевые).
|
||||
@@ -0,0 +1,41 @@
|
||||
# v0.0.37 — крупный блок статистики LLM в UI — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.36 → 0.0.37
|
||||
**Файл:** `site/templates/index.html`, `site/app.py` (версия)
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Раньше статистика LLM («... LLM: 5.3с · 1894 токенов») была МЕЛКИМ текстом
|
||||
в статус-строке под кнопкой — юзер её не замечал. Теперь после обработки
|
||||
показывается **крупный заметный блок** «📊 Итоги обработки»:
|
||||
|
||||
| Показатель | Значение |
|
||||
|---|---|
|
||||
| Общее время | крупное число, с |
|
||||
| Время ИИ | крупное число, с |
|
||||
| Токены ИИ | крупное число |
|
||||
|
||||
Оформление: зелёная плашка (фон-градиент, рамка 2px), три белые карточки,
|
||||
значения шрифтом 24px жирным. Блок скрыт до завершения и при сбросе.
|
||||
|
||||
## Изменения (`index.html`)
|
||||
- CSS: `.stats-block`, `.stats-title`, `.stats-row`, `.stat-item`,
|
||||
`.stat-label`, `.stat-value`.
|
||||
- HTML: блок `#statsBlock` с `#stTotalTime`, `#stLlmTime`, `#stLlmTokens`
|
||||
(после кнопок скачивания).
|
||||
- JS:
|
||||
- `complete`-обработчик — заполняет и показывает блок (общее время всегда;
|
||||
время ИИ и токены — если `llm_sec > 0`, иначе «—»);
|
||||
- `resetAll` — скрывает блок.
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK.
|
||||
- Визуально через браузер: блок появился полноширинным, три карточки,
|
||||
значения крупно — заметно.
|
||||
- Версия `app.VERSION = 0.0.37`.
|
||||
|
||||
## Бэкенд
|
||||
- Не менялся (метрики `tokens`/`llm_sec` в `event: complete` уже есть с v0.0.34).
|
||||
@@ -0,0 +1,41 @@
|
||||
# 2026-08-20 — LLM обрабатывает ВСЕ данные (чанкинг) — v0.0.57
|
||||
|
||||
## Задача
|
||||
Пользователь: LLM должен обрабатывать ВСЕ данные, а не урезку 8000 символов.
|
||||
Ветка сохранения пре-LLM конфигурации: `save-pre-llm-v0.0.56` (указывает на прежний master).
|
||||
|
||||
## Изменения (drhider/scanner.py)
|
||||
`scan_llm_ner` переписан:
|
||||
- Убраны глобальные урезки `t[:3000]` и `combined[:8000]`.
|
||||
- Каждый файл обрабатывается ПОЛНОСТЬЮ: split_into_chunks(6000, overlap 500).
|
||||
- Чанки файла — параллельно (ThreadPoolExecutor, concurrency 4), крупные файлы вперёд.
|
||||
- Для каждого чанка — полный промпт + «Already captured by regex» (mapping).
|
||||
- Дедуп найденного через normalize_entity (пробелы/регистр/кавычки/тире/ё).
|
||||
- Верификация _verify_entity против ПОЛНОГО текста файла — отсекает галлюцинации LLM.
|
||||
- regex-найденное не дублируется.
|
||||
|
||||
Новые хелперы (scanner.py):
|
||||
- split_into_chunks(text, size=6000, overlap=500) — границы \n\n,\n,. ,? ,! ,; ,, ," "
|
||||
- normalize_entity(s) — канонизация для дедупа
|
||||
- _verify_entity(value, full_text) — точное/нормализованное вхождение
|
||||
- _build_llm_prompt(text, already_found)
|
||||
- _call_llm(text, llm_client) — вызов + разбор JSON (пусто при ошибке)
|
||||
|
||||
Параметры верхнего уровня (легко тюнить):
|
||||
- _CHUNK_SIZE = 6000, _CHUNK_OVERLAP = 500, _LLM_CONCURRENCY = 4
|
||||
|
||||
## Ожидаемый эффект
|
||||
- LLM покрывает все файлы целиком (в т.ч. хвосты и таблицы после 3000 символа).
|
||||
- Время на ~300 файлов ≈ 3.5 мин при 4 потоках (~390 вызовов).
|
||||
- Меньше галлюцинаций (верификация).
|
||||
|
||||
## Проверка
|
||||
- py_compile OK.
|
||||
- test_scanner 20/20.
|
||||
- Юнит хелперов: split (12К→4 чанка ≤6100), verify (галлюцинация→False), normalize OK.
|
||||
- Все 106 тестов OK.
|
||||
- VERSION 0.0.57.
|
||||
|
||||
## Примечание
|
||||
Реальная проверка LLM-покрытия и времени — на наборе T/ при прогоне на проде/локально
|
||||
с ключом LLM (локально ключ отсутствует → LLM не выполняется, только regex).
|
||||
@@ -0,0 +1,44 @@
|
||||
# Запрос к Opus: валидация MSS Clamping плана
|
||||
|
||||
**Дата:** 2026-07-13
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
Кластер: bare-metal Kubernetes, 4 ноды, Cilium CNI (Geneve-туннель), kube-vip для VIP.
|
||||
Штурвал управляет ingress через Helm-чарт (`shturval-ingress-controller-2.12.1`).
|
||||
|
||||
## Проблема
|
||||
|
||||
При `externalTrafficPolicy: Cluster` большие файлы (>5 MB) вызывают TCP stall на 51 секунду.
|
||||
Диагностика: cross-node Geneve-туннель добавляет overhead (~50 байт) → пакет > MTU 1500 → ICMP Frag Needed блокируется → TCP RTO backoff.
|
||||
|
||||
Текущий костыль: `externalTrafficPolicy: Local` + `kubectl scale replicas=4` (Штурвал сбрасывает на 2 каждые ~час).
|
||||
|
||||
## Предлагаемое решение
|
||||
|
||||
Вместо костыля — выставить MTU в Cilium ConfigMap:
|
||||
|
||||
```yaml
|
||||
# kubectl edit configmap -n kube-system cilium-config
|
||||
mtu: 1450
|
||||
```
|
||||
|
||||
После этого:
|
||||
1. Вернуть `externalTrafficPolicy: Cluster`
|
||||
2. Оставить ingress `replicaCount: 2` (стоковое значение Штурвала)
|
||||
3. Никаких ручных scale
|
||||
|
||||
## Вопросы к Opus
|
||||
|
||||
**Читать только:** `docs/MSS-CLAMPING-PLAN.md`, `docs/KUBEVIP-EXTERNAL-TRAFFIC-POLICY.md`, `docs/INGRESS-SOLUTIONS.md`.
|
||||
|
||||
1. Корректно ли решение? Есть ли подводные камни при смене MTU через Cilium ConfigMap на работающем кластере?
|
||||
2. Нужно ли что-то ещё кроме `mtu: 1450`? (например, перезапуск подов, пересоздание туннелей)
|
||||
3. Как проверить что MSS clamping реально работает после изменения? (конкретные команды)
|
||||
4. Есть ли риск для других сервисов (Keycloak, etc.) при смене MTU?
|
||||
5. Если Cilium ConfigMap недоступен — iptables-вариант (`TCPMSS --clamp-mss-to-pmtu`) равноценен? На какую ноду вешать правило?
|
||||
6. Итоговый вердикт: это правильный путь, или есть более грамотный вариант?
|
||||
|
||||
**Не лезть:** в код приложения, в TMP, в History (кроме указанных трёх файлов).
|
||||
@@ -0,0 +1,35 @@
|
||||
# Opus: проверка PROBLEM-AND-SOLUTION.md на ошибки
|
||||
|
||||
**Дата:** 2026-07-13
|
||||
|
||||
---
|
||||
|
||||
## Найденные ошибки
|
||||
|
||||
### 1. TCP backoff — неправильные числа
|
||||
|
||||
**Было:** `1с → 3с → 7с → 15с → 25с = 51с` — выдумано.
|
||||
|
||||
**Правильно:** начальный RTO = 200ms, удвоение:
|
||||
```
|
||||
0.2с → 0.4с → 0.8с → 1.6с → 3.2с → 6.4с → 12.8с → 25.6с = 51с
|
||||
```
|
||||
8 ретрансмитов от 200ms = ровно 51 секунда (Linux kernel default).
|
||||
|
||||
### 2. DSR — неверное объяснение причины stall
|
||||
|
||||
**Было:** «двойной инкапсуляции return-трафика (главная причина stall)».
|
||||
|
||||
**Правильно:** stall на **входящем** трафике (клиент → pod), а не на return. DSR оптимизирует обратный путь, но не решает проблему inbound MTU black-hole.
|
||||
|
||||
### 3. Ping измеряет pod MTU, не underlay
|
||||
|
||||
Утверждение что `ping -M do` доказывает underlay = 1450 — неточно. Ping из пода измеряет MTU пода (1450 = 1500 − 50 Cilium). Физический underlay = 1450 подтверждён эмпирически (фикс сработал), а не прямым замером.
|
||||
|
||||
## Что верно
|
||||
|
||||
- Geneve overhead 50 байт: outer IP(20) + UDP(8) + Geneve(8) + inner Eth(14) = 50 ✓
|
||||
- `mtu: 1450` → pod MTU = 1400 ✓
|
||||
- `rollout restart ds/cilium` обязателен ✓
|
||||
- iptables может не работать с eBPF dataplane ✓
|
||||
- Корень проблемы и решение верны ✓
|
||||
@@ -0,0 +1,31 @@
|
||||
# Opus: финальная проверка PROBLEM-AND-SOLUTION.md
|
||||
|
||||
**Дата:** 2026-07-13
|
||||
|
||||
---
|
||||
|
||||
## Вердикт
|
||||
|
||||
Документ технически добротный. Диагноз и корневое решение (`mtu: 1450` + `Cluster` + `replicaCount: 2`) — правильные.
|
||||
|
||||
## Исправлено
|
||||
|
||||
1. **Ethernet 14** — уточнено: «инкапсулируемый L2-кадр 14» (внутренний Ethernet, не внешний)
|
||||
2. **51с = прикладной таймаут** — смягчено: «браузер/curl разрывает ожидание» вместо «соединение разрывается»
|
||||
|
||||
## Что подтверждено как верное
|
||||
|
||||
- PMTU blackhole как причина — классический сценарий
|
||||
- Cilium: `mtu` в configmap = underlay девайса, pod MTU = mtu − overhead
|
||||
- Geneve overhead = 50 байт (внешний IP 20 + UDP 8 + Geneve 8 + L2 14)
|
||||
- Решение: `mtu: 1450` → pod 1400 → Geneve-пакет 1450 → влазит в path 1450
|
||||
- `rollout restart ds/cilium` обязателен
|
||||
- `externalTrafficPolicy: Local` → `Cluster` после фикса MTU
|
||||
- DSR решает только обратный путь, stall на inbound
|
||||
- iptables может не работать с eBPF dataplane
|
||||
|
||||
## Мелкие замечания (не исправлены, допустимы)
|
||||
|
||||
- 200ms RTO — корректно для сценария (уже установленное TCP-соединение)
|
||||
- Тип туннеля (Geneve vs VXLAN) — overhead одинаков (50), математика не меняется
|
||||
- Замеры из таблицы результатов — эмпирические, из кода не подтвердить
|
||||
@@ -0,0 +1,77 @@
|
||||
# Opus: валидация MSS Clamping плана
|
||||
|
||||
**Дата:** 2026-07-13
|
||||
|
||||
---
|
||||
|
||||
## Ключевое исправление
|
||||
|
||||
В плане ошибка: `mtu: 1450` в cilium-config — это MTU **underlay-сети**, не туннеля. Cilium сам вычитает 50 байт на Geneve:
|
||||
|
||||
```
|
||||
mtu: 1450 → tunnel MTU = 1450 − 50 = 1400
|
||||
```
|
||||
|
||||
По умолчанию Cilium автодетектит underlay 1500 → туннель 1450. Если stall всё равно есть — underlay реально < 1500. **Число надо измерить, не гадать.**
|
||||
|
||||
---
|
||||
|
||||
## Ответы на вопросы
|
||||
|
||||
### 1. Корректно ли решение?
|
||||
|
||||
Да, направление верное. Но:
|
||||
- ConfigMap сам не применяется — нужен `rollout restart ds/cilium`
|
||||
- Число 1450 — гипотеза, нужен замер между нодами
|
||||
- `mtu` глобален на весь кластер
|
||||
- Существующие TCP-соединения не поменяют MSS — только новые
|
||||
|
||||
### 2. Что ещё нужно?
|
||||
|
||||
1. Измерить underlay MTU: `ping -M do -s 1472 <ip_другой_ноды>` (уменьшать пока не пройдёт)
|
||||
2. `kubectl rollout restart ds/cilium -n kube-system`
|
||||
3. Пересоздать поды приложения (ingress, drhider)
|
||||
4. Вернуть `externalTrafficPolicy: Cluster`
|
||||
|
||||
### 3. Как проверить?
|
||||
|
||||
```bash
|
||||
# Измерить до:
|
||||
ping -M do -s 1472 <ip_другой_ноды>
|
||||
tracepath <ip_другой_ноды>
|
||||
|
||||
# После изменения:
|
||||
kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep -i mtu
|
||||
ip link show | grep -E 'cilium|geneve'
|
||||
tcpdump -ni any 'tcp[tcpflags] & tcp-syn != 0' -vv | grep mss
|
||||
|
||||
# Функционально: файл >5 MB без паузы
|
||||
```
|
||||
|
||||
### 4. Риск для других сервисов?
|
||||
|
||||
Понижение MTU безопасно (меньше = консервативнее). Небольшой рост числа пакетов, микро-потеря throughput. Функционально ничего не ломает. Рестарт агентов — возможны короткие блипы, лучше в окно.
|
||||
|
||||
### 5. iptables равноценен?
|
||||
|
||||
**Нет.** Cilium с eBPF обходит netfilter FORWARD — правило может не видеть пакеты. Надёжнее `cilium-config`. Если iptables — то на всех 4 нодах (kube-vip раздаёт VIP на все).
|
||||
|
||||
### 6. Вердикт
|
||||
|
||||
Путь правильный. Уточнённый план:
|
||||
1. Измерить path MTU между нодами
|
||||
2. Выставить `mtu` = измеренное значение (вероятно < 1500)
|
||||
3. Рестарт агентов + подов
|
||||
4. Вернуть `Cluster` policy
|
||||
|
||||
При `Cluster` проблема реплик исчезает сама (2 хватает).
|
||||
|
||||
---
|
||||
|
||||
## Моё мнение
|
||||
|
||||
Opus прав. План нужно дополнить **замером**. Без замера `1450` — пальцем в небо. Обновлю `MSS-CLAMPING-PLAN.md` с учётом:
|
||||
- Добавить шаг 0: измерение path MTU
|
||||
- Исправить формулу: `mtu` = underlay, туннель = `mtu - 50`
|
||||
- Добавить `rollout restart ds/cilium` + рестарт подов
|
||||
- Убрать iptables как основной вариант (ненадёжен с Cilium)
|
||||
@@ -0,0 +1,145 @@
|
||||
# DrHider — Полный аудит и план рефакторинга
|
||||
|
||||
**Дата:** 2026-07-12
|
||||
**Платформа:** Штурвал (Managed Flask) — запускает Flask сам, без Dockerfile/gunicorn
|
||||
**URL:** https://drhider.pythonk8s.dev.nubes.ru/
|
||||
**Репозиторий:** https://gitea.services.ngcloud.ru/Nail/drhider
|
||||
|
||||
---
|
||||
|
||||
## Текущее состояние (аудит каждого файла)
|
||||
|
||||
| Файл | Статус | Решение |
|
||||
|---|---|---|
|
||||
| `Dockerfile` | ❌ НЕ НУЖЕН | Штурвал сам запускает Flask. Удалить. |
|
||||
| `drhider_server.py` | ❌ НЕ НУЖЕН | Standalone-сервер с ВМ (:8767). Flask app.py уже обрабатывает /api/drhider. Удалить. |
|
||||
| `requirements.txt` | ⚠️ Нужны правки | Убрать `gunicorn` и `gevent`. Оставить `flask`, `python-docx`, `pdfplumber`, `httpx`. |
|
||||
| `.gitignore` | ✅ OK | Без изменений. |
|
||||
| `.gitea/workflows/deploy.yaml` | ✅ OK | CI для Штурвала. |
|
||||
| `site/app.py` | ⚠️ Нужны правки | Удалить блок `if __name__ == "__main__"` (gunicorn). При рефакторинге — вынести LLMClient. |
|
||||
| `site/templates/index.html` | ✅ OK | UI. |
|
||||
| `site/static/favicon.svg` | ✅ OK | Иконка (из loadtest). |
|
||||
| `drhider.py` | ⚠️ МОНОЛИТ | 560 строк. Разбить на ~20 модулей. |
|
||||
| `History/` | ✅ OK | Документация. |
|
||||
|
||||
---
|
||||
|
||||
## План рефакторинга (Фаза 1: удаление лишнего)
|
||||
|
||||
### Шаг 1.1 — Удалить `Dockerfile`
|
||||
### Шаг 1.2 — Удалить `drhider_server.py`
|
||||
### Шаг 1.3 — Исправить `requirements.txt` (убрать gunicorn, gevent)
|
||||
### Шаг 1.4 — Исправить `site/app.py` (убрать блок `if __name__ == "__main__"`)
|
||||
### Шаг 1.5 — Проверить синтаксис → commit → push
|
||||
|
||||
---
|
||||
|
||||
## План рефакторинга (Фаза 2: модульная структура)
|
||||
|
||||
### Новая структура `drhider/` пакета (вместо `drhider.py` 560 строк):
|
||||
|
||||
```
|
||||
drhider/ # Пакет ядра обфускации
|
||||
├── __init__.py # re-export: obfuscate_files()
|
||||
├── config.py # Константы: ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN,
|
||||
│ # RU_SURNAMES, RU_NAMES, RU_PATRONYMICS,
|
||||
│ # RU_CITIES, RU_STREETS, FAKE_DOMAINS
|
||||
├── checksum.py # _checksum_inn10, _checksum_inn12, _checksum_ogrn
|
||||
├── random_utils.py # _random_digits, _random_letters
|
||||
├── generators/ # Генераторы фиктивных значений
|
||||
│ ├── __init__.py # ENTITY_GENERATORS маппинг
|
||||
│ ├── phone.py # generate_phone
|
||||
│ ├── email.py # generate_email
|
||||
│ ├── inn.py # generate_inn10, generate_inn12
|
||||
│ ├── ogrn.py # generate_ogrn
|
||||
│ ├── kpp.py # generate_kpp
|
||||
│ ├── bik.py # generate_bik
|
||||
│ ├── accounts.py # generate_rs, generate_ks
|
||||
│ ├── passport.py # generate_passport
|
||||
│ ├── company.py # generate_company
|
||||
│ ├── person.py # generate_person
|
||||
│ └── address.py # generate_address
|
||||
├── extractor.py # _extract_text, _expand_zips, _convert_pdfs_to_docx
|
||||
├── scanner.py # Проход 1: _scan_regex, _scan_llm_ner
|
||||
├── replacer.py # Проход 2: _replace_in_docx, _replace_in_text, _apply_replacements
|
||||
├── builder.py # Сборка: _build_zip, _build_mapping_csv
|
||||
├── obfuscator.py # TwoPassObfuscator — оркестратор
|
||||
└── llm_client.py # LLMClient (из app.py → сюда)
|
||||
|
||||
site/ # Flask-приложение
|
||||
├── __init__.py
|
||||
├── app.py # Только create_app() + регистрация blueprint'ов
|
||||
├── routes/
|
||||
│ ├── __init__.py # Регистрация всех blueprint'ов
|
||||
│ ├── main_bp.py # GET /
|
||||
│ ├── health_bp.py # GET /health
|
||||
│ └── api_bp.py # POST /api/drhider
|
||||
├── templates/index.html
|
||||
└── static/favicon.svg
|
||||
```
|
||||
|
||||
### Принципы:
|
||||
- Каждый файл ≤ 50 строк (где возможно)
|
||||
- Каждый файл — одна ответственность
|
||||
- Максимум docstring и инлайн-комментариев
|
||||
- Все импорты явные, никаких `import *`
|
||||
|
||||
---
|
||||
|
||||
## Результат рефакторинга (2026-07-12)
|
||||
|
||||
### Итоговая структура
|
||||
|
||||
```
|
||||
drhider/ # Пакет ядра обфускации
|
||||
├── __init__.py # re-export: obfuscate_files, LLMClient, TwoPassObfuscator
|
||||
├── config.py # Константы: ENTITY_PATTERNS, словари имён/городов/улиц
|
||||
├── checksum.py # Контрольные суммы: ИНН10, ИНН12, ОГРН
|
||||
├── random_utils.py # Утилиты: random_digits, random_letters
|
||||
├── llm_client.py # LLMClient — HTTP-клиент к LLM API
|
||||
├── extractor.py # Извлечение текста + expand_zips + convert_pdfs_to_docx
|
||||
├── scanner.py # Проход 1: scan_regex, scan_llm_ner
|
||||
├── replacer.py # Проход 2: apply_replacements, replace_in_docx, replace_in_text
|
||||
├── builder.py # Сборка: build_zip, build_mapping_csv
|
||||
├── obfuscator.py # TwoPassObfuscator — оркестратор
|
||||
└── generators/ # Генераторы фиктивных значений
|
||||
├── __init__.py # ENTITY_GENERATORS маппинг
|
||||
├── phone.py, email.py # Телефон, email
|
||||
├── inn.py, ogrn.py, kpp.py # ИНН, ОГРН, КПП
|
||||
├── bik.py, accounts.py # БИК, счета (р/с, к/с)
|
||||
├── passport.py # Паспорт
|
||||
├── company.py, person.py # Компания, ФИО
|
||||
└── address.py # Адрес
|
||||
|
||||
site/ # Flask-приложение
|
||||
├── app.py # create_app() + VERSION (20 строк)
|
||||
├── routes/
|
||||
│ ├── __init__.py # register_routes()
|
||||
│ ├── main_bp.py # GET /
|
||||
│ ├── health_bp.py # GET /health
|
||||
│ └── api_bp.py # POST /api/drhider
|
||||
├── templates/index.html
|
||||
└── static/favicon.svg
|
||||
```
|
||||
|
||||
### Что изменилось
|
||||
|
||||
| Было | Стало |
|
||||
|---|---|
|
||||
| `drhider.py` — 560 строк монолит | Пакет `drhider/` — 22 модуля по 20–100 строк |
|
||||
| `site/app.py` — 100 строк (всё в одном файле) | `app.py` 33 строки + 3 blueprint'а по 20–50 строк |
|
||||
| `Dockerfile` | Удалён (Штурвал сам) |
|
||||
| `drhider_server.py` | Удалён (Flask заменяет) |
|
||||
|
||||
### Коммиты
|
||||
|
||||
- `a2f754a` — Фаза 1: удалён Dockerfile, drhider_server.py, gunicorn
|
||||
- `37581eb` — Фаза 2: config.py, checksum.py, random_utils.py, generators/
|
||||
- `2cf4e08` — Фаза 2: llm_client.py, extractor.py, scanner.py, replacer.py, builder.py, obfuscator.py
|
||||
- `51bc1a7` — Фаза 2: site/app.py → blueprint'ы, старый drhider.py удалён
|
||||
|
||||
| Переменная | Значение |
|
||||
|---|---|
|
||||
| `LLM_API_KEY` | (секрет — не хранить в git) |
|
||||
| `LLM_URL` | `https://api.aillm.ru/v1/chat/completions` |
|
||||
| `LLM_MODEL` | `gpt-oss-120b` |
|
||||
@@ -0,0 +1,45 @@
|
||||
# v3.0.0 — универсальная архитектура (2026-07-12)
|
||||
|
||||
Переход на Markdown-пайплайн + универсальное LLM-обнаружение.
|
||||
|
||||
## Что изменилось
|
||||
|
||||
### 1. `generators/fallback.py` — новый
|
||||
Character-class preserving замена для неизвестных типов сущностей.
|
||||
Цифры→цифры, буквы→буквы, спецсимволы→без изменений.
|
||||
|
||||
### 2. `scanner.py` — универсальный LLM-промпт
|
||||
- Убран жёсткий список типов (person, company, address, passport)
|
||||
- LLM САМА придумывает тип (snake_case) — person_name, contract_number, employee_id...
|
||||
- Если тип не совпадает с известными — fallback-генератор
|
||||
- Промпт на английском (LLM точнее)
|
||||
|
||||
### 3. `extractor.py` — Markdown-пайплайн
|
||||
- `docx_to_markdown()`: DOCX → MD (Heading → #, Bold → **, таблицы → |...|)
|
||||
- `pdf_to_markdown()`: PDF → MD (текст + таблицы, без форматирования)
|
||||
- `extract_text()` возвращает только str (без docx-объектов)
|
||||
- Удалён `convert_pdfs_to_docx()` (PDF → MD напрямую)
|
||||
|
||||
### 4. `replacer.py` — упрощён
|
||||
- Удалён `replace_in_text()` (заменён на `apply_replacements()` + `.encode()`)
|
||||
- Оставлен `replace_in_docx()` для будущего DOCX-выхода
|
||||
|
||||
### 5. `obfuscator.py` — новый пайплайн
|
||||
- Pass 1: extract MD → scan_regex + scan_llm_ner
|
||||
- Pass 2: apply_replacements к MD → .md файлы
|
||||
- Нет docx-объектов, нет convert_pdfs_to_docx
|
||||
|
||||
## Коммиты
|
||||
|
||||
- `579f9c8` — generators: fallback.py
|
||||
- `2ceeb05` — scanner.py: универсальный промпт
|
||||
- `af2f2a9` — extractor.py: MD + obfuscator.py: новый пайплайн
|
||||
- `f5427b0` — replacer.py: удалён replace_in_text
|
||||
- `2a2cc14` — v3.0.0
|
||||
|
||||
## Плюсы
|
||||
|
||||
- LLM точнее (нет XML-шума, только Markdown)
|
||||
- Новые типы сущностей — без правки кода
|
||||
- Код проще: -120 строк, -1 функция, -1 модуль (convert_pdfs_to_docx)
|
||||
- Единый внутренний формат (Markdown)
|
||||
@@ -0,0 +1,51 @@
|
||||
# План v3 — универсальная архитектура (2026-07-12)
|
||||
|
||||
## Цель
|
||||
|
||||
Убрать жёсткую привязку к фиксированному списку типов сущностей.
|
||||
Перейти на Markdown как внутренний формат.
|
||||
|
||||
## Изменения по модулям
|
||||
|
||||
### 1. `scanner.py` — универсальный LLM-промпт
|
||||
- Убрать список типов из промпта
|
||||
- LLM сама называет типы (snake_case)
|
||||
- `value` — verbatim из текста
|
||||
|
||||
### 2. `generators/` — fallback-генератор
|
||||
- Новый `fallback.py`: character-class preserving замена
|
||||
- `__init__.py`: добавить в ENTITY_GENERATORS
|
||||
- `obfuscator.py`: использовать fallback для неизвестных типов
|
||||
|
||||
### 3. `extractor.py` — единый MD-пайплайн
|
||||
- DOCX → MD (python-docx: стили, форматирование)
|
||||
- PDF → MD (pdfplumber: текст + таблицы)
|
||||
- TXT → как есть
|
||||
- Убрать convert_pdfs_to_docx (не нужен)
|
||||
- Убрать хранение docx-объектов
|
||||
|
||||
### 4. `replacer.py` — упростить
|
||||
- `apply_replacements(text)` — уже есть
|
||||
- `replace_in_docx(doc, mapping)` — сохранить как утилиту для DOCX-выхода
|
||||
- Удалить `replace_in_text()` (заменяется на apply_replacements)
|
||||
|
||||
### 5. `obfuscator.py` — обновить пайплайн
|
||||
- Pass 1: extract MD → scan_regex + scan_llm_ner
|
||||
- Pass 2: apply_replacements к MD
|
||||
- Опционально: replace_in_docx к оригинальному DOCX
|
||||
|
||||
### 6. `api_bp.py` — output_format
|
||||
- Параметр `output_format: "md" | "docx"` (по умолчанию md)
|
||||
|
||||
### 7. `.doc` — потом
|
||||
- Добавим convert_doc_to_docx через LibreOffice отдельно
|
||||
|
||||
## Порядок реализации
|
||||
|
||||
1. `generators/fallback.py` + обновить `__init__.py`
|
||||
2. `scanner.py` — новый промпт
|
||||
3. `extractor.py` — MD-конвертация
|
||||
4. `replacer.py` — упростить
|
||||
5. `obfuscator.py` — новый пайплайн
|
||||
6. `api_bp.py` — output_format
|
||||
7. Проверить всё → commit
|
||||
@@ -0,0 +1,30 @@
|
||||
# План v3.1 — пофайловая обработка + прогресс в таблице (2026-07-12)
|
||||
|
||||
## Проблема
|
||||
Фронтенд v3.0 отправляет все файлы одним запросом → долгий LLM-вызов → таймаут.
|
||||
Нужно: обрабатывать по одному, прогресс в таблице, все результаты → один ZIP.
|
||||
|
||||
## Решение
|
||||
|
||||
### Фронтенд (index.html)
|
||||
1. **XHR** вместо fetch — надёжнее для blob, явный timeout
|
||||
2. **Колонка «Статус»** в таблице файлов
|
||||
3. **По одному файлу:** цикл, каждый файл → отдельный XHR → ZIP
|
||||
4. **Прогресс в таблице:** ⏳ → ✓ 2.1 сек (или ✗ ошибка)
|
||||
5. **JSZip** — склеивает все полученные ZIP'ы в один
|
||||
6. **Скачивание** — один файл `drhider_output.zip` в конце
|
||||
|
||||
### Статусы в таблице
|
||||
| Статус | Отображение |
|
||||
|---|---|
|
||||
| Ожидание | ⏳ |
|
||||
| Успех | ✓ 2.1 сек |
|
||||
| Ошибка | ✗ сообщение |
|
||||
|
||||
### Бэкенд
|
||||
Без изменений. API уже принимает один файл.
|
||||
|
||||
### НЕ трогать
|
||||
- gunicorn не нужен (файлы по одному, каждый запрос ≤10 сек)
|
||||
- app.run() — остаётся
|
||||
- drhider пакет — без изменений
|
||||
@@ -0,0 +1,63 @@
|
||||
# План для Flash: выбор целой папки рекурсивно (v0.0.68)
|
||||
|
||||
> Самодостаточный план. Файл: `site/templates/index.html` (весь фронт — ванильный JS, без сборки).
|
||||
> Текущая версия 0.0.67 → поднять до **0.0.68** (в `site/app.py` `VERSION`).
|
||||
|
||||
## Задача
|
||||
Кнопка «📁 Выбрать папку»: юзер выбирает папку → в таблицу рекурсивно добавляются ВСЕ файлы
|
||||
(включая вложенные подпапки), как сейчас раскрываются ZIP. **Относительный путь сохраняется**
|
||||
(без имени верхней выбранной папки): `Подпапка/Внутри/report.pdf`.
|
||||
|
||||
## Что уже есть (ориентиры в index.html)
|
||||
- `<input type="file" id="fileInput" multiple accept=".doc,.docx,.pdf,.txt,.zip">` в `.file-input-wrap`.
|
||||
- `const fi = document.getElementById('fileInput');` (в начале `<script>`).
|
||||
- `fi.addEventListener('change', async () => {...})` — текущий обработчик: для `.zip` → `listZipFiles(f)` (раскрывает архив, возвращает `File[]`), иначе `addFileWithDedup(f)`; в конце пересобирает `DataTransfer`, `rr()`.
|
||||
- `addFileWithDedup(file)` — дедуп по `file.name` + `file.size`, лимиты `MAX_FILE_BYTES`/`MAX_SESSION_BYTES`, `overNames`.
|
||||
- `listZipFiles(file)` — рекурсивно раскрывает `.zip` (в т.ч. вложенные), фильтрует только документы
|
||||
`['.pdf','.doc','.docx','.txt','.md']`, возвращает `File[]` (имена = пути внутри архива).
|
||||
- `busy` — флаг «идёт загрузка/обработка»; есть `setBusy(b)`, guard'ы в `change` и `rm`.
|
||||
- Footer: `<div class="footer-bar">` содержит `fileCount`, `cancelBtn`, `newSessionBtn`, `uploadBtn`.
|
||||
|
||||
## Реализация
|
||||
|
||||
### 1. HTML — кнопка и скрытый input
|
||||
- В `.file-input-wrap` (рядом с кнопкой «Choose File», т.е. `#fileInput`) добавить кнопку:
|
||||
`<button type="button" class="btn" id="folderBtn">📁 Выбрать папку</button>`
|
||||
- Добавить скрытый input (вне/внутри формы без разницы):
|
||||
`<input type="file" id="folderInput" webkitdirectory style="display:none">`
|
||||
- `folderBtn.onclick = () => folderInput.click();`
|
||||
|
||||
### 2. JS — ссылки и обработчик
|
||||
- `const folderInput = document.getElementById('folderInput');`
|
||||
- `const DOC_EXTS = ['.pdf', '.doc', '.docx', '.txt', '.md'];` (можно переиспользовать из listZipFiles — там это локальная константа `allowedExt`; продублировать на верхнем уровне или вынести).
|
||||
- `folderInput.addEventListener('change', async () => { ... })`:
|
||||
1. `if (busy) return;`
|
||||
2. `const files = Array.from(folderInput.files);` — у каждого есть `file.webkitRelativePath` (вида `TopFolder/Подпапка/report.pdf`).
|
||||
3. Для каждого файла:
|
||||
- Определить `rel = file.webkitRelativePath.split('/')` и **отбросить первый сегмент** (верхняя папка): `rel = rel.slice(1).join('/')`.
|
||||
- `const low = rel.toLowerCase();`
|
||||
- Если `.zip` → `const nested = await listZipFiles(file);` затем для каждого `nf` создать
|
||||
`new File([nf], rel_dir + '/' + nf.name, {lastModified: nf.lastModified})`, где
|
||||
`rel_dir = rel.slice(0, rel.lastIndexOf('/'))` (папка, в которой лежал zip; если в корне — пусто),
|
||||
и `addFileWithDedup(that)`. Обернуть в try/catch: при ошибке раскрытия добавить сам `.zip` как есть (по аналогии с текущим обработчиком).
|
||||
- Иначе если `DOC_EXTS.some(e => low.endsWith(e))` → `addFileWithDedup(new File([file], rel, {lastModified: file.lastModified}))`.
|
||||
- Иначе — пропустить (не документ).
|
||||
4. В конце — показать индикатор «Разбираю папку…» на время обработки (как с ZIP: `document.body.style.cursor='wait'` + `st`), затем `rr()`.
|
||||
- Показать индикатор до цикла, сбросить в `finally` (как текущий change-обработчик).
|
||||
|
||||
### 3. Важные детали
|
||||
- **Дедуп/лимиты**: `addFileWithDedup` уже работает по `file.name`; так как имя = относительный путь,
|
||||
файлы из разных подпапок не сливаются. Лимиты 50МБ/500МБ применяются автоматически.
|
||||
- **Имя в таблице и в выходном ZIP** = относительный путь (с `/`). Это ожидаемо.
|
||||
- **`webkitdirectory`**: поддерживается Chrome/Edge (полно), Firefox (частично), Safari — нет.
|
||||
Best-effort, ничего не ломается в неподдерживаемых браузерах (кнопка просто не откроет выбор папки).
|
||||
- **Guard `busy`**: во время загрузки/обработки выбор папки не должен работать (как и обычный input).
|
||||
|
||||
### 4. Версия и проверка
|
||||
- `site/app.py`: `VERSION = "0.0.68"`.
|
||||
- Проверки: `python3 -m py_compile site/app.py`; `node --check` на извлечённом `<script>` (как обычно).
|
||||
- Тест вручную/браузером: папка с подпапками + вложенный zip + кириллические имена → все файлы в таблице
|
||||
с относительными путями, дедуп/лимиты работают.
|
||||
|
||||
## Не трогать
|
||||
- Логику обфускации, SSE, сессии, лимиты — только добавить кнопку/input/обработчик выбора папки.
|
||||
@@ -0,0 +1,310 @@
|
||||
# План реализации для Флаша — DrHider: TTL-фикс + трекинг файлов + прерывание + ETA + UI
|
||||
|
||||
> **Кому:** агент-кодер (Flash). Этот документ самодостаточен — код уже есть, нужно его доработать.
|
||||
> **Проект:** DrHider — Flask-обфускатор документов. Локально `/home/naeel/nubes/drhider`.
|
||||
> **Версия:** сейчас 0.0.62 → после реализации поднять до **0.0.63**.
|
||||
> **Что НЕ делать:** не менять логику обфускации/LLM по сути; только добавить продление TTL, прогресс по файлам/чанкам, прерывание, оценку времени и UI.
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст и устройство кода
|
||||
|
||||
Точка входа: `site/app.py` (`create_app()`, `VERSION`). Роуты: `site/routes/api_bp.py`.
|
||||
Сессии: `site/session.py`. Логика обфускации: пакет `drhider/`.
|
||||
|
||||
Ключевые файлы и функции (уже существующие):
|
||||
|
||||
| Файл | Что внутри |
|
||||
|---|---|
|
||||
| `site/session.py` | in-memory сессии, `TTL_SECONDS=30*60`, `MAX_FILE_BYTES`, `MAX_SESSION_BYTES`, `create_session/add_file/get_files/store_result/get_result/store_csv/get_csv/cleanup/file_count`, `_start_timer(sid)` |
|
||||
| `site/routes/api_bp.py` | `upload`, `upload_refs`, `session_files`, `process_stream` (SSE), `process` (legacy), `download`, `csv_download` |
|
||||
| `drhider/obfuscator.py` | `TwoPassObfuscator.obfuscate()` + обёртка `obfuscate_files()` |
|
||||
| `drhider/scanner.py` | `scan_regex`, `split_into_chunks`, `scan_llm_ner`, `_call_llm`, `_LLM_CONCURRENCY=1` |
|
||||
| `drhider/builder.py` | `build_zip(files, mapping_csv)`, `build_mapping_csv(mapping)` |
|
||||
| `drhider/replacer.py` | `apply_replacements`, `_build_combined_re`, `replace_in_docx` |
|
||||
| `site/templates/index.html` | весь фронт (ванильный JS, без фреймворков) |
|
||||
|
||||
### 1.1 Как сейчас устроен `obfuscate()` (важно)
|
||||
`drhider/obfuscator.py`, метод `TwoPassObfuscator.obfuscate(files, progress_cb=None) -> (zip_bytes, csv_str)`:
|
||||
|
||||
1. `files = extractor.expand_zips(files)`; `files = _dedupe_file_names(files)`.
|
||||
2. **Проход 1 (сбор):**
|
||||
- Цикл по файлам: извлечь текст `extractor.extract_text(...)` → `all_texts[fname]=text`;
|
||||
regex-скан `scanner.scan_regex(text, self._mapping, self._counters)`; `progress_cb("start", i, total, display_name, 0.0)` в начале каждого файла; `progress_cb("done", ...)` только для **пропущенных (битых)** файлов (`skipped.add(fname)`).
|
||||
- После цикла: `scanner.scan_llm_ner(all_texts, self._mapping, self._llm_client, self._counters)` — **один вызов на все файлы**, без прогресса.
|
||||
- `self._sorted_keys = sorted(...)`, `self._compiled_re = replacer._build_combined_re(...)`.
|
||||
3. **Проход 2 (замена):** цикл по файлам, для каждого `replacer.apply_replacements(...)` →
|
||||
`results.append((fname, obf_content))`; `progress_cb("done", i, total, display_name, round(file_times[i], 2))`.
|
||||
4. **Сборка:** `csv_str = builder.build_mapping_csv(self._mapping)`; `zip_data = builder.build_zip(results, csv_str)`; return.
|
||||
5. `finally:` очищает `self._mapping/_sorted_keys/_compiled_re/_counters`.
|
||||
|
||||
**Факт:** `mapping.csv` **НЕ кладётся в ZIP** (by design, в `build_zip`); CSV отдаётся отдельно через `/api/csv/<sid>`.
|
||||
|
||||
### 1.2 Как сейчас устроен SSE `process_stream`
|
||||
`api_bp.py`, `process_stream(sid)`:
|
||||
- `files = get_files(sid)`; `all_files = [(fname, content, "") for ...]`.
|
||||
- `generate()`: `llm=LLMClient()`, `q=queue.Queue()`, `cancel=threading.Event()`, `progress(phase,idx,total,name,elapsed)` кладёт в `q`.
|
||||
- `worker()`: `zip_data, csv_str = obfuscate_files(all_files, llm_client=llm, progress_cb=progress)`; `q.put(("result", zip, csv, stats))` / `q.put(("error", repr))`.
|
||||
- Цикл: `q.get(timeout=1)`; при `Empty` — **heartbeat** `event: llm` с `data: {active, elapsed, tokens}`; при `progress` — `event: <phase>`; при `result` — `store_result`+`store_csv`+`event: complete`; при `error` — `event: error`.
|
||||
|
||||
SSE-события сейчас: `start`, `done`, `llm` (heartbeat), `complete`, `error`.
|
||||
|
||||
### 1.3 Как сейчас устроен фронт (index.html)
|
||||
Элементы: `fileList` (`fl`), `fileCount` (`fc`), `uploadBtn` (`ub`), `status` (`st`), `dlBtns` (`db`),
|
||||
`liveBlock/liveTimer/liveFile/liveLlm/liveLlmTime`, `statsBlock/stTotalTime/stLlmTime/stLlmTokens`.
|
||||
Функции: `fs(b)`, `rr()` (рендер таблицы), `ss(idx,html)` (обновить статус ячейки), `uploadFiles()`,
|
||||
`downloadZip()`, `downloadCsv()`, `addFileWithDedup()`, `rm(i)`, `resetAll()`.
|
||||
Обработчики SSE: `start`, `llm`, `done`, `complete`, `onerror`.
|
||||
`sf` — массив File; `sendIdx[k]` — индекс в `sf` для k-го отправленного файла.
|
||||
|
||||
---
|
||||
|
||||
## 2. Цель (фича)
|
||||
|
||||
1. **TTL-фикс**: сессия не должна умирать во время долгой обработки (сейчас 30 мин → долгий прогон теряет результат).
|
||||
2. **Трекинг файлов/чанков**: знать, какой файл сейчас обрабатывается, сколько прошло/осталось.
|
||||
3. **Прерывание с сохранением**: кнопка «Прервать» → мягкая остановка → сохранить в ZIP+CSV **всё, что успело полностью обработаться**.
|
||||
4. **Оценка времени**: глобальная (весь пакет) + per-file (текущий файл).
|
||||
5. **UI**: таблица из 3 секций (готово / текущий / ожидают), кнопка «Прервать», диалог-подтверждение.
|
||||
|
||||
---
|
||||
|
||||
## 3. Блок A — TTL-фикс (`site/session.py`)
|
||||
|
||||
### Текущее
|
||||
```python
|
||||
TTL_SECONDS = 30 * 60
|
||||
def _start_timer(sid):
|
||||
def _clean():
|
||||
with _lock:
|
||||
_sessions.pop(sid, None)
|
||||
timer = threading.Timer(TTL_SECONDS, _clean)
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
return timer
|
||||
```
|
||||
Таймер хранится как `s["timer"]` в `create_session`.
|
||||
|
||||
### Изменения
|
||||
1. Добавить `def touch(sid)`: отменить старый таймер (`s["timer"].cancel()`) и запустить новый (перезаписать `s["timer"]`). `touch` под `_lock`, безопасна при отсутствии сессии.
|
||||
2. Добавить `def pause_ttl(sid)`: `s["timer"].cancel()` (без перезапуска) — «сессия живёт пока идёт обработка».
|
||||
3. Добавить `def resume_ttl(sid)`: запустить `_start_timer(sid)` заново.
|
||||
4. В `create_session` добавить в словарь `"cancel": threading.Event()`.
|
||||
5. Добавить `def request_cancel(sid) -> bool`: если сессия есть — `s["cancel"].set()`, вернуть True; иначе False.
|
||||
6. Добавить `def get_cancel_event(sid) -> Optional[threading.Event]`: вернуть `s["cancel"]` или None.
|
||||
|
||||
### Где использовать
|
||||
- `process_stream`: в начале `pause_ttl(sid)`; в конце (complete/error/cancelled) `resume_ttl(sid)`.
|
||||
- legacy `process()`: то же самое (тот же баг).
|
||||
|
||||
---
|
||||
|
||||
## 4. Блок B — трекинг файлов/чанков (`drhider/scanner.py`, `drhider/obfuscator.py`)
|
||||
|
||||
### 4.1 `scan_llm_ner` — новые параметры
|
||||
```python
|
||||
def scan_llm_ner(all_texts, mapping, llm_client, counters,
|
||||
cancel_event=None, file_progress=None):
|
||||
```
|
||||
- `cancel_event`: `threading.Event` или None.
|
||||
- `file_progress`: callable(event, fname, **fields) или None.
|
||||
|
||||
Цикл сейчас: `for fname, full_text in file_items:` (файлы отсортированы по убыванию длины).
|
||||
Добавить:
|
||||
- **В начале итерации файла**: `if cancel_event and cancel_event.is_set(): raise CancelRequested(...)` (между файлами — граница отмены).
|
||||
- `file_progress("file_start", fname, chars=len(full_text), chunks=len(chunks))` (после разбиения на чанки).
|
||||
- Внутри цикла по чанкам (последовательная ветка `for c in chunks:`): после каждого чанка
|
||||
`file_progress("file_chunk", fname, chunks_done=k, chunks_total=len(chunks))`.
|
||||
- После обработки файла: `file_progress("file_done", fname, elapsed=...)`.
|
||||
|
||||
**Важно:** отмену проверяем ТОЛЬКО между файлами (не между чанками) — файл добрается до конца,
|
||||
граница всегда целая.
|
||||
|
||||
### 4.2 Новое исключение
|
||||
В `drhider/obfuscator.py` (или отдельно) определить:
|
||||
```python
|
||||
class CancelRequested(Exception):
|
||||
"""Обработка прервана пользователем."""
|
||||
```
|
||||
(данные частичного результата НЕ класть в исключение — см. Блок C: obfuscate сам собирает частичный результат и возвращает его с флагом.)
|
||||
|
||||
### 4.3 `obfuscate()` — новый контракт возврата
|
||||
Поменять возврат с `(zip, csv)` на `(zip, csv, meta)`:
|
||||
- `meta` — None при штатном завершении;
|
||||
- при отмене — `{"cancelled": True, "processed": <int>, "total": <int>}`.
|
||||
|
||||
`obfuscate` получает новые параметры: `cancel_event=None`, `file_progress=None`.
|
||||
|
||||
Логика отмены внутри `obfuscate()`:
|
||||
- Передать `cancel_event` и `file_progress` в `scan_llm_ner`.
|
||||
- В проходе 2 (замена) перед каждым файлом: `if cancel_event.is_set(): break` (прервать замену).
|
||||
- После LLM: если отменено ДО конца LLM — `scan_llm_ner` бросит `CancelRequested`. Поймать её,
|
||||
вычислить список файлов, чей LLM завершён (см. 4.4), выполнить замену **только для них**,
|
||||
собрать частичный `results` + `csv_str` и вернуть `(zip, csv, {"cancelled": True, "processed": X, "total": N})`.
|
||||
- Если отменено во время прохода 2 (после полного LLM): `break` цикла замены → частичный `results` → тот же meta-контракт.
|
||||
|
||||
### 4.4 Как понять, какие файлы «полностью обработаны» при отмене в LLM
|
||||
`scan_llm_ner` итерирует `file_items` (отсортированы по убыванию длины). Завести в `obfuscate`
|
||||
множество `llm_done: set` — заполняется через `file_progress("file_done", fname)`.
|
||||
При `CancelRequested` — файлы в `llm_done` считаются полностью проанализированными;
|
||||
для них выполняется замена (их сущности уже в `self._mapping`). Файлы вне `llm_done` (и в `skipped`)
|
||||
в частичный результат **не попадают**.
|
||||
|
||||
`total` в meta = число файлов после dedup/expand_zips (т.е. `len(files)` до прохода 2);
|
||||
`processed` = число файлов, попавших в частичный `results`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Блок C — прерывание в API (`site/routes/api_bp.py`)
|
||||
|
||||
### 5.1 Новый эндпоинт
|
||||
```python
|
||||
@api_bp.route("/cancel/<sid>", methods=["POST"])
|
||||
def cancel(sid):
|
||||
if request_cancel(sid):
|
||||
return jsonify({"ok": True}), 200
|
||||
return jsonify({"ok": False, "error": "Session not found"}), 404
|
||||
```
|
||||
|
||||
### 5.2 `process_stream` — изменения
|
||||
- `pause_ttl(sid)` в начале.
|
||||
- `cancel_event = get_cancel_event(sid)`; передать в `obfuscate_files(..., cancel_event=cancel_event, file_progress=...)`.
|
||||
- `file_progress` callback: класть события в `q` (новый тип `("file", event, fname, fields)`).
|
||||
- `worker()`: принять `(zip, csv, meta)`; если `meta and meta.get("cancelled")` →
|
||||
`q.put(("cancelled", zip, csv, stats, meta))`, иначе как раньше `("result", ...)`.
|
||||
- В цикле обработки `q`:
|
||||
- `("file", event, fname, fields)` → `yield event: <event>` с `data: {"name": fname, **fields}`.
|
||||
- `("cancelled", zip, csv, stats, meta)` → `store_result`+`store_csv` → `yield event: cancelled`
|
||||
с `data: {"saved": meta["processed"], "total": meta["total"], "tokens": stats["tokens"], "llm_sec": stats["llm_sec"]}`.
|
||||
- В конце любого исхода (complete/error/cancelled) — `resume_ttl(sid)`.
|
||||
- **Heartbeat `llm`** — расширить: `data: {active, elapsed, tokens, eta_sec, done_chars, total_chars}`.
|
||||
`total_chars` вычисляется после прохода 1 (нужно передать его из `obfuscate` в генератор —
|
||||
например, через `file_progress` событие `extract_done` с `{total_chars}`), `done_chars`/`eta_sec` — из LLM-прогресса.
|
||||
|
||||
### 5.3 Схема новых SSE-событий (итоговый контракт)
|
||||
| event | data (JSON) | когда |
|
||||
|---|---|---|
|
||||
| `start` | `{idx,name,total,elapsed}` | проход 1, каждый файл (уже есть) |
|
||||
| `extract_done` | `{total_chars}` | после извлечения всех текстов |
|
||||
| `file_start` | `{name, chars, chunks}` | начало LLM файла |
|
||||
| `file_chunk` | `{name, chunks_done, chunks_total, eta_sec}` | после каждого чанка LLM |
|
||||
| `file_done` | `{name, elapsed}` | LLM файла завершён |
|
||||
| `llm` | `{active, elapsed, tokens, eta_sec, done_chars, total_chars}` | heartbeat (1 раз/сек) |
|
||||
| `done` | `{idx,name,total,elapsed}` | проход 2, каждый файл (уже есть) |
|
||||
| `complete` | `{total, tokens, llm_sec}` | успех (уже есть) |
|
||||
| `cancelled` | `{saved, total, tokens, llm_sec}` | прерывание (новое) |
|
||||
| `error` | `{error}` | ошибка (уже есть) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Блок D — оценка времени (ETA)
|
||||
|
||||
### 6.1 Глобальная
|
||||
- После `extract_done` известен `total_chars`.
|
||||
- Во время LLM: `tokens` и `elapsed` уже есть в heartbeat. Скорость = `tokens/elapsed`.
|
||||
- `est_total_tokens ≈ total_chars × K` (K — эмпирический коэффициент; взять ~7–10, подстроить по замерам).
|
||||
- `eta_sec = max(0, (est_total_tokens − tokens) / (tokens/elapsed))`.
|
||||
- Вычислять в генераторе (у него есть `llm` и `total_chars`) и класть в heartbeat `eta_sec`.
|
||||
|
||||
### 6.2 Per-file (текущий файл)
|
||||
- `file_start` даёт `chunks`. `file_chunk` даёт `chunks_done`.
|
||||
- Скорость чанка (сек/чанк) = `elapsed_файла / chunks_done` (меряем по факту текущего файла).
|
||||
- `file_eta = (chunks_total − chunks_done) × (сек/чанк)`.
|
||||
- На самом старте (chunks_done=0) — грубая оценка `chars × историч. сек/символ`.
|
||||
- Класть `eta_sec` в `file_chunk` (вычисляет worker, у него точный elapsed).
|
||||
|
||||
---
|
||||
|
||||
## 7. Блок E — UI (`site/templates/index.html`)
|
||||
|
||||
### 7.1 Таблица из 3 секций (этап обработки)
|
||||
Во время Фазы 2 перестроить таблицу. Завести `procState` — объект: `idx -> {st: 'pending'|'current'|'done'|'skipped', elapsed, eta}`.
|
||||
|
||||
Порядок строк сверху вниз:
|
||||
1. **done** — уже обработанные; в столбце «Статус» фактическое время (`✓ 3.2с`).
|
||||
2. **current** — подсвечен (класс `row-current`); в столбце «Статус» `прошло 0:42 / ~1:20` (тикает через `setInterval`).
|
||||
3. **pending** — не обработанные; в столбце «Статус» оценка (`~12с`).
|
||||
|
||||
`rr()` модифицировать: если идёт обработка — рендерить 3 группы в этом порядке (по `procState`),
|
||||
иначе — как сейчас (по `sf`).
|
||||
|
||||
Обновления:
|
||||
- `start` (idx) → `procState[idx]={st:'pending'}` (или current — см. ниже).
|
||||
- `file_start` (name→idx) → `procState[idx].st='current'`.
|
||||
- `file_chunk` → обновить `eta` текущего.
|
||||
- `file_done` → вернуть в pending (LLM-анализ не значит «готов в ZIP»; готовность — событие `done`).
|
||||
- `done` (idx, elapsed) → `procState[idx]={st:'done', elapsed}`.
|
||||
|
||||
Нужен маппинг `name→idx` (или передавать `idx` в событиях `file_*` — проще: добавить `idx` в
|
||||
`file_start/file_done` на бэке; см. замечание ниже).
|
||||
|
||||
### 7.2 Кнопка «Прервать»
|
||||
- Новый элемент `<button id="cancelBtn" class="btn" style="display:none">⏹ Прервать</button>` в `footer-bar`.
|
||||
- Показывать на Фазе 2, скрывать на Фазе 1 и после complete/cancelled.
|
||||
- По клику — диалог-подтверждение (модал или `confirm`):
|
||||
> «Остановить обработку? Будет сохранено: полностью обработанные файлы (сейчас готово X из N) и таблица замен. Остановить?»
|
||||
- После подтверждения: `fetch('/api/cancel/' + currentSid, {method:'POST'})`.
|
||||
- Обработчик SSE `cancelled`: статус «Сохранено X из N файлов + таблица замен», показать `dlBtns`.
|
||||
|
||||
### 7.3 Живой блок (liveBlock)
|
||||
- В `llm` heartbeat — обновлять: `liveLlmTime` = elapsed, и добавить строку
|
||||
«осталось ~X мин» (из `eta_sec`).
|
||||
- Текущий файл — из `file_start`/`file_chunk`: «Файл 12/101: name.pdf — осталось ~1:20».
|
||||
|
||||
### 7.4 Прочее
|
||||
- Скрыть кнопку «✕» (remove) на Фазе 2.
|
||||
- Статическая оценка сразу после `upload_refs` (до SSE): «Загружено N файлов (X МБ). Обработка может занять ~M мин» — по объёму (коэффициент тот же K).
|
||||
|
||||
---
|
||||
|
||||
## 8. Замечания и ловушки (из код-ревью)
|
||||
|
||||
1. **`done`-фаза занята**: сейчас `progress_cb("done")` шлётся и для битых (в проходе 1), и для готовых
|
||||
(в проходе 2). Не путать с готовностью файла в ZIP. Готовность = `done` в проходе 2.
|
||||
Для UI-«готово» ориентироваться на события `done`, приходящие ПОСЛЕ всех `start` (т.е. в фазе замены).
|
||||
2. **Частичные результаты не терять**: `results` — локальная переменная; НЕ полагаться на `finally`.
|
||||
Возвращать `(zip, csv, meta)` из `obfuscate` (см. 4.3).
|
||||
3. **`mapping.csv` не в ZIP**: частичный результат = частичный ZIP (только .md) + полный CSV отдельно
|
||||
(через `/api/csv`). Полный CSV собирается из `self._mapping` (он полон на момент отмены в фазе замены).
|
||||
4. **LLM-фаза сейчас без прогресса**: `scan_llm_ner` — один монолитный вызов. Именно поэтому добавляется
|
||||
`file_progress`. Без него «текущий файл» в UI не определить.
|
||||
5. **Имена в событиях `file_*`**: на бэке имена файлов — уже с `.md`/суффиксами после dedup; фронт
|
||||
сопоставляет по `idx` (надёжнее, чем по имени). **Рекомендация**: передавать `idx` (0-based в `files`)
|
||||
в события `file_start/file_done/file_chunk`.
|
||||
6. **`obfuscate_files`-обёртка**: тоже меняет сигнатуру (`cancel_event`, `file_progress`) и возврат
|
||||
`(zip, csv, meta)`. Обновить ОБА вызова: `process_stream` и legacy `process()`.
|
||||
7. **Отключение SSE**: при закрытии вкладки `cancel` в генераторе уже ставится, но воркер его не видит.
|
||||
Подключить тот же `cancel_event` (передать в `obfuscate_files`), чтобы закрытие вкладки реально
|
||||
останавливало LLM (не жечь токены).
|
||||
8. **`_LLM_CONCURRENCY = 1`** — последовательно. Не вводить параллельность (liberta/LLM не тянут).
|
||||
|
||||
---
|
||||
|
||||
## 9. Порядок реализации (коммитить по шагам)
|
||||
|
||||
1. **TTL-фикс** (`session.py` + `api_bp.py`): `touch/pause_ttl/resume_ttl`, `cancel` Event,
|
||||
`request_cancel/get_cancel_event`, `POST /api/cancel/<sid>`.
|
||||
2. **Трекинг** (`scanner.py`, `obfuscator.py`): `cancel_event` + `file_progress`, `CancelRequested`,
|
||||
новый контракт возврата `(zip, csv, meta)`.
|
||||
3. **Прерывание** (`obfuscator.py`, `api_bp.py`): частичный результат, событие `cancelled`.
|
||||
4. **ETA** (`api_bp.py`, heartbeat `eta_sec`/`done_chars`/`total_chars`, `file_chunk` eta).
|
||||
5. **UI** (`index.html`): 3-секционная таблица, кнопка «Прервать», диалог, live-обновления.
|
||||
6. **Бамп версии** 0.0.62 → 0.0.63, py_compile + node --check, коммит.
|
||||
|
||||
---
|
||||
|
||||
## 10. Проверка
|
||||
|
||||
- `python3 -m py_compile` для всех изменённых `.py`.
|
||||
- `node --check` для извлечённого `<script>` из `index.html`.
|
||||
- Ручной тест: загрузить 100+ файлов → проверить 3 секции, ETA, прерывание → частичный ZIP+CSV.
|
||||
- Регресс: короткий прогон без прерывания → `complete`, ZIP+CSV как раньше.
|
||||
- Тест TTL: запустить долгий прогон (>30 мин) → скачать результат (не должен быть 404).
|
||||
|
||||
---
|
||||
|
||||
## 11. Что НЕ трогать
|
||||
- Логику обфускации/LLM/NER (regex, chunking, dedup) — только прогресс/отмена поверх.
|
||||
- `drhider/config.py`, `drhider/replacer.py`, `drhider/builder.py`, `drhider/extractor.py` — без изменений
|
||||
(кроме возможного экспорта `CancelRequested` из `obfuscator.py`).
|
||||
- Деплой/nginx/vmfiles — вне скоупа этой задачи.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Запрос к Соннету — ERR_TIMED_OUT при открытии нового окна (2026-07-12)
|
||||
|
||||
## Симптом
|
||||
|
||||
DrHider на Managed Flask (Штурвал/pythonk8s). При открытии страницы в НОВОМ окне браузера — ERR_TIMED_OUT (20+ секунд). Старые вкладки работают нормально.
|
||||
|
||||
## Факты
|
||||
|
||||
- Изнутри кластера (kubectl exec → curl): HTTP 200 за 2.1 секунды
|
||||
- Снаружи (браузер, curl с локальной машины): таймаут 10+ секунд
|
||||
- v1 работало с gunicorn (CMD: `gunicorn --bind :5000 --worker-class gevent`)
|
||||
- Сейчас: `app.run(host="0.0.0.0", port=5000)` — Flask dev server (Werkzeug)
|
||||
- Страница: 12 KB HTML, 2 статических файла (logo.svg, favicon.svg — оба 2.8 KB)
|
||||
- Ноль внешних зависимостей (никаких CDN, шрифтов, JS-библиотек)
|
||||
- HTTP-заголовки: Cache-Control: no-cache, no-store, must-revalidate
|
||||
- Штурвал запускает: cd site && python app.py
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. Может ли Flask dev server (Werkzeug) быть причиной ERR_TIMED_OUT на новых соединениях, в то время как gunicorn работал нормально?
|
||||
2. Если да — какой механизм? (keep-alive, thread pool, socket backlog?)
|
||||
3. Если нет — что ещё может вызывать таймаут именно на НОВЫХ окнах при работающих старых?
|
||||
4. Нужно ли вернуть gunicorn или есть другой способ заставить app.run() работать стабильно?
|
||||
5. Может ли `Cache-Control: no-store` заставлять браузер делать лишние запросы при открытии нового окна?
|
||||
|
||||
## Контекст платформы
|
||||
|
||||
Штурвал — Managed Flask на Kubernetes:
|
||||
- Запускает: `cd /var/www && pip install -r requirements.txt && cd site && python app.py`
|
||||
- Readiness probe: `tcp-socket :5000`
|
||||
- Нет Dockerfile, нет nginx перед приложением
|
||||
- Ingress: внешний LoadBalancer → ClusterIP сервис → под
|
||||
@@ -0,0 +1,83 @@
|
||||
# Запрос к Соннету — анализ результатов DrHider v3 (2026-07-12)
|
||||
|
||||
## Контекст
|
||||
|
||||
DrHider v3 — обфускация документов через LLM + Markdown. Результаты уже есть.
|
||||
Покажи Соннету реальный mapping.csv и фрагмент обфусцированного .md.
|
||||
|
||||
## Промпт для Соннета
|
||||
|
||||
Вот текущий LLM-промпт в scanner.py:
|
||||
|
||||
```
|
||||
You are a PII detector. Find ALL sensitive or private information in the text below.
|
||||
|
||||
Return a JSON array. Each item must have:
|
||||
- "type": short snake_case label describing the entity
|
||||
(e.g. person_name, phone, email, address, inn, company, passport,
|
||||
contract_number, employee_id, bank_account — WHATEVER you see)
|
||||
- "value": the EXACT string from the text (copy verbatim)
|
||||
|
||||
Rules:
|
||||
- Copy value VERBATIM — same spaces, punctuation, case as in text
|
||||
- If same value appears multiple times — include only once
|
||||
- Invent the type label yourself based on what you see
|
||||
- Return ONLY the JSON array, no other text, no markdown wrapping
|
||||
```
|
||||
|
||||
Результат mapping.csv (первые 10 строк):
|
||||
```csv
|
||||
тип_данных,оригинал,замена
|
||||
text,"""****XXX****001****""","""****IPD****084****"""
|
||||
text,"""НУБЕС""","""ФИОЩЦ"""
|
||||
phone,+7 (495) 789-41-35,+7 (343) 986-75-93
|
||||
email,info@nubes.ru,bxkuno@company.local
|
||||
company,"ЗАО ""****XXX****001****""",ООО «Спектр»
|
||||
company,"ЗАО ""XXX001""",ЗАО «Меридиан»
|
||||
inn_ul,ИНН 9706005293,ИНН 9325731296
|
||||
text,Исполнителем Заказчику Сторонами,Морозов М.П.
|
||||
kpp,КПП 772401001,КПП 051543039
|
||||
```
|
||||
|
||||
Фрагмент обфусцированного .md:
|
||||
```markdown
|
||||
**ООО «Спектр»** именуемое в дальнейшем "Заказчик", в лице Генерального директора ФИО,
|
||||
действующего на основании Устава, и **ООО **"**НУБЕС**"**, именуемое в дальнейшем
|
||||
"Исполнитель", в лице Генерального директора Степанов Н.М.
|
||||
```
|
||||
|
||||
## Проблемы
|
||||
|
||||
1. **Контекстная роль Исполнителя** — сервис должен быть УНИВЕРСАЛЬНЫМ, без хардкода конкретных названий. LLM должна из контекста понимать: «вот эта компания — Исполнитель (сторона договора, оказывающая услуги), её НЕ заменяем». Как научить LLM этому без указания конкретных имён?
|
||||
2. **Тип "text" для многих сущностей** — LLM не всегда правильно определяет тип (например, "Исполнителем Заказчику Сторонами" — это не ПДн, а просто текст договора)
|
||||
3. **Regex находит структурированное (ИНН, телефон), LLM — остальное** — есть дублирование
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. Как научить LLM определять Исполнителя из контекста договора (без хардкода имён) и НЕ заменять его данные?
|
||||
2. Как заставить LLM точнее определять тип сущности (не "text" для всего подряд)?
|
||||
3. Нужно ли добавить в промпт примеры того что НЕ является ПДн (юридические термины, роли сторон)?
|
||||
4. Стоит ли передавать LLM уже найденное regex'ом чтобы избежать дублирования?
|
||||
5. Правильно ли что промпт на английском, а документы на русском?
|
||||
|
||||
## Текущий промпт (полностью)
|
||||
|
||||
```
|
||||
You are a PII (Personally Identifiable Information) detector.
|
||||
Find ALL sensitive or private information in the text below.
|
||||
|
||||
Return a JSON array. Each item must have:
|
||||
- "type": short snake_case label describing the entity
|
||||
(e.g. person_name, phone, email, address, inn, company, passport,
|
||||
contract_number, employee_id, bank_account — WHATEVER you see)
|
||||
- "value": the EXACT string from the text (copy verbatim)
|
||||
|
||||
Rules:
|
||||
- Copy value VERBATIM — same spaces, punctuation, case as in text
|
||||
- If same value appears multiple times — include only once
|
||||
- Invent the type label yourself based on what you see
|
||||
- Return ONLY the JSON array, no other text, no markdown wrapping
|
||||
|
||||
Text:
|
||||
{combined[:8000]}
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Запрос к Соннету — нумерация вместо генерации фейков (2026-07-12)
|
||||
|
||||
## Контекст
|
||||
|
||||
DrHider обфусцирует документы: ищет ПДн → заменяет на фиктивные значения.
|
||||
Сейчас используется генерация "похожих" фейков (Иванов И.И., ООО «Ромашка», +7(495)...).
|
||||
|
||||
## Проблема
|
||||
|
||||
1. **Адреса → абракадабра.** Ул. Стекольная → иг. 8ц Лжчюсвхшбр. Потому что fallback-генератор меняет кириллицу на случайную кириллицу.
|
||||
2. **Сложность.** 11 генераторов под каждый тип данных. Каждый надо поддерживать.
|
||||
3. **Ложное ощущение реальности.** Фейковые ИНН с правильной контрольной суммой выглядят как настоящие.
|
||||
|
||||
## Идея
|
||||
|
||||
Заменить всю генерацию на **нумерацию по типам:**
|
||||
|
||||
| Оригинал | Замена |
|
||||
|---|---|
|
||||
| Иванов И.И. | `Лицо_0042` |
|
||||
| ООО «НУБЕС» | `Компания_0003` |
|
||||
| ул. Стекольная, д.7 | `Адрес_0015` |
|
||||
| +7 (495) 789-41-35 | `Телефон_0007` |
|
||||
| info@nubes.ru | `Email_0004` |
|
||||
| ИНН 9706005293 | `ИНН_0002` |
|
||||
| Р/с 40702... | `Счёт_0001` |
|
||||
| договор-XXX001.docx | `Файл_0006` |
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. **Плюсы/минусы** нумерации vs генерации фейков для задачи обфускации документов?
|
||||
2. **Как нумеровать?** Глобальный счётчик на каждый тип? Или в рамках одного документа?
|
||||
3. **Стоит ли сохранять формат?** Например `Телефон_0007` vs `+7 (000) 000-00-07`?
|
||||
4. **mapping.csv** — при нумерации он становится главным документом аудита. Это ок?
|
||||
5. **UUID вместо номеров?** `Лицо_a3f4b2` — лучше или хуже последовательных номеров?
|
||||
|
||||
## Текущий mapping.csv (для контекста)
|
||||
|
||||
```csv
|
||||
тип_данных,оригинал,замена
|
||||
phone,+7 (495) 789-41-35,+7 (495) 906-87-80
|
||||
email,info@nubes.ru,ypiorej@company.local
|
||||
company,"ЗАО ""XXX001""",ООО «Альянс»
|
||||
inn_ul,ИНН 9706005293,ИНН 1846691249
|
||||
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
|
||||
text,"115404, г. Москва, ул. 1-я Стекольная, д.7с2","084810, б. Ввжзгд, гф. 6-в Фтпжыобнзщ, я.8с1"
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
# Запрос к Соннету — нумерация vs фейки для downstream LLM (2026-07-12)
|
||||
|
||||
## Контекст
|
||||
|
||||
Я делаю сервис **DrHider** — обфускация документов. Находит ПДн → заменяет.
|
||||
После обфускации документы идут на дальнейшую LLM-обработку — **сверка спецификаций**
|
||||
(другой сервис contracts, извлекает типы/номера/даты/контрагентов через LLM).
|
||||
|
||||
Сейчас генерация фейков: `Иванов И.И.` → `Петров А.С.`, `ООО «Ромашка»` → `ООО «Спектр»`.
|
||||
|
||||
**Проблема:** для неизвестных типов (адреса, нестандартные ID) — character-class замена
|
||||
даёт абракадабру: `ул. Стекольная` → `иг. 8ц Лжчюсвхшбр`. Downstream LLM не может это обработать.
|
||||
|
||||
## Варианты замены
|
||||
|
||||
### А. Чистая нумерация
|
||||
`Иванов И.И.` → `Лицо_0042`, `ООО «Ромашка»` → `Компания_0003`, `ул. Ленина` → `Адрес_0015`
|
||||
|
||||
Плюс: никакой абракадабры. Минус: LLM теряет контекст («Лицо_0042 заключил договор» — не понимает семантику).
|
||||
|
||||
### Б. Фейки (сейчас)
|
||||
`Иванов И.И.` → `Петров А.С.`, `ООО «Ромашка»` → `ООО «Спектр»`
|
||||
|
||||
Плюс: LLM понимает роли. Минус: сложно (11 генераторов), ложное ощущение реальности,
|
||||
для неизвестных типов — абракадабра.
|
||||
|
||||
### В. Гибрид: шаблон + номер
|
||||
`Иванов И.И.` → `Иванов_5677`, `ООО «Ромашка»` → `ООО_Технология_0034`,
|
||||
`ул. Стекольная, д.7` → `ул_Ленина_0015`, `+7 (495) 789-41-35` → `+7_495_000_0042`,
|
||||
`info@nubes.ru` → `email_0007@fake`
|
||||
|
||||
Плюс: LLM видит что это человек/компания/адрес, номер гарантирует уникальность и обфускацию.
|
||||
Минус: всё равно нужны шаблоны для каждого типа.
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. Какой вариант лучше для downstream LLM-обработки (извлечение контрагентов, дат, номеров)?
|
||||
2. Вариант В — достаточно ли LLM поймёт что `Иванов_5677` это персональные данные?
|
||||
3. Для неизвестных типов — `[Данные_NNNN]` как fallback, ок?
|
||||
4. Глобальная нумерация (сквозная через все документы пакета) vs локальная (в рамках одного файла)?
|
||||
5. Стоит ли сохранять частичную структуру (например телефон `+7_495_000_0042` сохраняет формат номера)?
|
||||
|
||||
## Downstream задача (для контекста)
|
||||
|
||||
LLM в сервисе contracts получает текст документа и должна извлечь:
|
||||
- Контрагент («с кем договор»)
|
||||
- Номер и дату договора
|
||||
- Список услуг с ценами (таблица)
|
||||
|
||||
Промпт для contracts LLM (фрагмент):
|
||||
```
|
||||
Извлеки из текста: тип документа, номер, дату, контрагента.
|
||||
Верни JSON: {"type":"договор","number":"...","date":"...","counterparty":"..."}
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# Запрос к Соннету — универсальная архитектура DrHider
|
||||
|
||||
## Контекст
|
||||
|
||||
Проект DrHider — обфускация документов (замена персональных данных на фиктивные). Сейчас:
|
||||
- Python/Flask, без БД, Managed Flask на платформе Штурвал
|
||||
- Двухпроходная архитектура: сбор сущностей → замена
|
||||
- Проход 1: regex-паттерны (телефон, email, ИНН, ОГРН, КПП, БИК, счета, паспорт) + LLM NER (ФИО, компании, адреса, паспорта)
|
||||
- Проход 2: замена по словарю mapping
|
||||
|
||||
## Проблема
|
||||
|
||||
Архитектура **в корне неверна** — жёстко привязана к фиксированному списку типов сущностей:
|
||||
|
||||
```python
|
||||
# config.py — жёстко зашитые паттерны
|
||||
ENTITY_PATTERNS = {
|
||||
"phone": r'...',
|
||||
"email": r'...',
|
||||
"inn_fl": r'ИНН\s*\d{12}',
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# scanner.py — жёстко зашитый промпт
|
||||
prompt = (
|
||||
"Найди ВСЕ следующие сущности:\n"
|
||||
"1. ФИО\n2. Компании\n3. Адреса\n4. Паспортные данные\n"
|
||||
)
|
||||
```
|
||||
|
||||
Если новый документ содержит СНИЛС, водительское удостоверение, номер договора, ИНН без префикса «ИНН» — система их НЕ обнаружит. Надо править config, generators, scanner, маппинги.
|
||||
|
||||
## Что нужно
|
||||
|
||||
**Универсальная архитектура**, где система САМА определяет что скрывать в любом документе:
|
||||
1. Не привязана к списку типов
|
||||
2. Не требует добавления паттернов под каждый новый вид данных
|
||||
3. LLM сама решает что является персональными/конфиденциальными данными
|
||||
4. Генерация фиктивных значений — тоже универсальная (не 11 отдельных функций)
|
||||
|
||||
## Текущая структура (полная)
|
||||
|
||||
```
|
||||
drhider/
|
||||
├── config.py # ENTITY_PATTERNS (10 regex), словари имён/городов
|
||||
├── checksum.py # Контрольные суммы ИНН/ОГРН
|
||||
├── random_utils.py # random_digits, random_letters
|
||||
├── generators/ # 11 файлов: phone, email, inn, ogrn, kpp, bik, accounts, passport, company, person, address
|
||||
├── llm_client.py # HTTP-клиент к LLM API
|
||||
├── extractor.py # Извлечение текста + expand_zips + convert_pdfs_to_docx
|
||||
├── scanner.py # scan_regex (regex) + scan_llm_ner (LLM NER с жёстким промптом)
|
||||
├── replacer.py # apply_replacements, replace_in_docx, replace_in_text
|
||||
├── builder.py # build_zip, build_mapping_csv
|
||||
├── obfuscator.py # TwoPassObfuscator — оркестратор
|
||||
├── site/app.py # Flask (3 blueprint'а)
|
||||
└── site/templates/ # HTML-интерфейс
|
||||
```
|
||||
|
||||
## Вопросы к Соннету
|
||||
|
||||
1. Как перестроить архитектуру чтобы обнаружение было универсальным (LLM сама решает что скрывать)?
|
||||
2. Нужен ли regex вообще или достаточно одного LLM с правильным промптом?
|
||||
3. Как сделать генерацию фиктивных значений универсальной? (Не 11 функций под каждый тип, а что-то общее)
|
||||
4. Двухпроходная схема (сбор→замена) — сохранять или перейти на однопроходную (LLM сразу возвращает обфусцированный текст)?
|
||||
5. Как должен выглядеть промпт чтобы LLM возвращала структурированный результат (что найдено + на что заменить)?
|
||||
6. Стоит ли сохранять DOCX-форматирование (сейчас runs склеиваются-разделяются) или проще отдать LLM plain text?
|
||||
|
||||
## Ограничения
|
||||
|
||||
- Документы до 200 MB
|
||||
- Форматы: .docx, .pdf, .txt, .zip
|
||||
- LLM: OpenAI-совместимое API (aillm.ru, модель gpt-oss-120b, 8000 токенов)
|
||||
- Без БД, всё в памяти
|
||||
- Платформа: Managed Flask на Kubernetes
|
||||
@@ -0,0 +1,19 @@
|
||||
# Ответ Соннета — ERR_TIMED_OUT (2026-07-12)
|
||||
|
||||
## Причина
|
||||
|
||||
Werkzeug dev server: **один поток на TCP-соединение**. Браузер держит 6 keep-alive соединений на вкладку. 6 потоков заняты idle-соединениями → backlog (5) переполнен → новое окно получает SYN-дроп → таймаут.
|
||||
|
||||
gunicorn+gevent: async I/O, idle-соединения не занимают ресурсы.
|
||||
|
||||
## Решение
|
||||
|
||||
Три варианта:
|
||||
|
||||
| Вариант | Что | Изменения |
|
||||
|---|---|---|
|
||||
| A (waitress) | Pure Python WSGI | `requirements.txt` + 2 строки в `app.py` |
|
||||
| B (gevent) | Async, как gunicorn | `requirements.txt` + 2 строки в `app.py` |
|
||||
| C (gunicorn) | Точно как v1 | `requirements.txt` + смена CMD в Штурвале |
|
||||
|
||||
Соннет рекомендует: **A** если Штурвал не меняет CMD, **C** если меняет.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Ответ Соннета v2 — улучшение LLM-промпта (2026-07-12)
|
||||
|
||||
## Q1. Как научить LLM определять Исполнителя из контекста?
|
||||
|
||||
**Двухфазная инструкция в промпте:**
|
||||
- STEP 1: определи кто «Исполнитель» — его данные НЕ трогать
|
||||
- STEP 2: найди всё остальное ПДн
|
||||
|
||||
Убирает хардкод `НУБЕС` из кода. Работает на любом договоре.
|
||||
|
||||
## Q2. Точная классификация типов
|
||||
|
||||
Вместо `"Invent the type label yourself"` → **фиксированный список:**
|
||||
`person_name, company, phone, email, address, inn, kpp, ogrn, bik, passport, contract_number, bank_account, other_id`
|
||||
|
||||
Если сомневаешься — НЕ включай.
|
||||
|
||||
## Q3. Примеры что НЕ является ПДн
|
||||
|
||||
Обязательно добавить блок:
|
||||
```
|
||||
NOT PII:
|
||||
- Юридические роли: Исполнитель, Заказчик, Стороны
|
||||
- Термины договора: Услуги, Приложение, Договор, НДС
|
||||
- Должности: Генеральный директор
|
||||
- Сама компания-Исполнитель
|
||||
```
|
||||
|
||||
## Q4. Передавать LLM уже найденное regex'ом
|
||||
|
||||
`scan_llm_ner` получает `mapping.keys()` → промпт: "Already captured (skip these): ..."
|
||||
Исключает дублирование, экономит токены.
|
||||
|
||||
## Q5. Язык промпта
|
||||
|
||||
Гибрид: структурные инструкции на английском, NOT PII и роли — на русском.
|
||||
|
||||
---
|
||||
|
||||
## План реализации (scanner.py)
|
||||
|
||||
1. Убрать хардкод `НУБЕС` из `scan_regex`
|
||||
2. `scan_llm_ner` — передавать `mapping.keys()` как "Already captured"
|
||||
3. Новый промпт:
|
||||
- Фиксированный список типов
|
||||
- Блок NOT PII (русский)
|
||||
- Двухфазная инструкция (STEP 1: Исполнитель, STEP 2: остальное)
|
||||
@@ -0,0 +1,46 @@
|
||||
# Ответ Соннета v3 — нумерация вместо генерации фейков (2026-07-12)
|
||||
|
||||
## Q1. Нумерация vs фейки
|
||||
- ✅ Адреса без абракадабры, код проще, аудит прозрачнее
|
||||
- ❌ Документ нечитаем (`Лицо_0042 заключил договор с Компания_0003`)
|
||||
- Вывод: для аудита/хранения — отлично, для показа людям — фейки лучше
|
||||
|
||||
## Q2. Глобальный счётчик на весь пакет
|
||||
Одна сущность в разных документах → один номер. `TwoPassObfuscator` хранит counters.
|
||||
|
||||
## Q3. Сохранять формат?
|
||||
Рекомендация: `Тип_NNNN` — проще всего, аудит максимально прозрачен.
|
||||
|
||||
## Q4. mapping.csv — главный документ аудита
|
||||
Да, это улучшение. mapping.csv — секретный ключ деобфускации.
|
||||
|
||||
## Q5. Номера vs UUID
|
||||
Номера (`Лицо_0042`) — лучше для читаемости и аудита.
|
||||
|
||||
---
|
||||
|
||||
## План изменений
|
||||
|
||||
### Удалить
|
||||
- `drhider/generators/` — все 12 файлов
|
||||
- `drhider/checksum.py` — больше не нужен (ИНН без контрольных сумм)
|
||||
- `drhider/random_utils.py` — больше не нужен
|
||||
|
||||
### Изменить
|
||||
- `scanner.py`: добавить `TYPE_PREFIXES` + `_next_token()`, убрать импорты генераторов
|
||||
- `obfuscator.py`: `TwoPassObfuscator` хранит `counters` dict
|
||||
- `config.py`: убрать словари имён/городов (RU_SURNAMES, ...)
|
||||
|
||||
### Новая логика замены
|
||||
```python
|
||||
TYPE_PREFIXES = {
|
||||
"person_name": "Лицо", "company": "Компания", "phone": "Телефон",
|
||||
"email": "Email", "address": "Адрес", "inn": "ИНН", ...
|
||||
}
|
||||
_FALLBACK = "Данные"
|
||||
|
||||
def _next_token(entity_type, counters):
|
||||
prefix = TYPE_PREFIXES.get(entity_type, _FALLBACK)
|
||||
counters[prefix] = counters.get(prefix, 0) + 1
|
||||
return f"{prefix}_{counters[prefix]:04d}"
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
# Ответ Соннета v4 — гибридный формат замен (2026-07-12)
|
||||
|
||||
## Итог: Вариант В (шаблон + номер)
|
||||
|
||||
| Тип | Формат замены | Пример |
|
||||
|---|---|---|
|
||||
| ФИО | `Фамилия_NNNN` | `Иванов_5677` |
|
||||
| Компания | `ООО_Слово_NNNN` | `ООО_Технология_0034` |
|
||||
| Адрес | `ул_Шаблон_NNNN` | `ул_Ленина_0015` |
|
||||
| Телефон | `+7_код_000_NNNN` | `+7_495_000_0042` |
|
||||
| Email | `email_NNNN@fake` | `email_0007@fake` |
|
||||
| ИНН | `ИНН_NNNNNNNNNN` | `ИНН_0000000042` |
|
||||
| Неизвестный тип | `[Тип_NNNN]` | `[Адрес_0023]` |
|
||||
|
||||
## Ключевые принципы
|
||||
1. **Читаемо человеком** — не ужиматься, пусть будет естественно
|
||||
2. **Однозначно для LLM** — префикс указывает тип сущности
|
||||
3. **Глобальная нумерация** — одна сущность = один номер во всех файлах пакета
|
||||
4. **Цены/суммы** — не обфусцировать (нужны для downstream сверки)
|
||||
@@ -0,0 +1,78 @@
|
||||
# Ответ Соннета — универсальная архитектура DrHider (2026-07-12)
|
||||
|
||||
Кратко: 6 вопросов → 6 ответов → карта изменений.
|
||||
|
||||
---
|
||||
|
||||
## Q1. Как сделать обнаружение универсальным?
|
||||
|
||||
Промпт должен звучать не «найди вот эти типы», а «найди ВСЁ, что выглядит как приватная информация, и сам назови тип».
|
||||
|
||||
Затрагивает: `scanner.py` (промпт), `builder.py` (тип для mapping.csv).
|
||||
|
||||
---
|
||||
|
||||
## Q2. Regex или LLM?
|
||||
|
||||
**Гибрид — правильный выбор:**
|
||||
- **Regex = pre-pass** для структурированных данных (телефон, email, ИНН, БИК). Экономит токены LLM.
|
||||
- **LLM = post-pass** для неструктурированного (имена, адреса, нестандартные ID).
|
||||
- Дублирования не страшны — mapping dict сам отсеет.
|
||||
|
||||
---
|
||||
|
||||
## Q3. Генерация фиктивных значений — универсальная?
|
||||
|
||||
Два уровня:
|
||||
1. **Известные типы** — специализированные генераторы (оставить как есть).
|
||||
2. **Неизвестные типы** — fallback-генератор: character-class preserving замена (цифры→цифры, буквы→буквы той же длины).
|
||||
|
||||
Дополнительно: в промпт добавить `"category": "person|org|contact|id|financial|other"` — 6 категорий вместо 10+ типов.
|
||||
|
||||
---
|
||||
|
||||
## Q4. Двухпроходная или однопроходная?
|
||||
|
||||
**Двухпроходная — обязательно.** Причина: «Иванов» в 5 документах должен заменяться одинаково. Однопроход не может этого гарантировать.
|
||||
|
||||
Текущий `TwoPassObfuscator` — правильный, не трогать.
|
||||
|
||||
---
|
||||
|
||||
## Q5. Новый промпт
|
||||
|
||||
```
|
||||
You are a PII detector. Find ALL sensitive or private information.
|
||||
|
||||
Return JSON array: [{"type": "short_label", "value": "exact_string"}]
|
||||
|
||||
Rules:
|
||||
- Copy "value" VERBATIM from text
|
||||
- "type" is snake_case label you invent
|
||||
- Same value → include once
|
||||
- Return ONLY JSON
|
||||
```
|
||||
|
||||
Ключевое: нет ограничения на типы, value verbatim.
|
||||
|
||||
---
|
||||
|
||||
## Q6. DOCX или Markdown?
|
||||
|
||||
- **Внутренняя обработка:** Markdown проще парсить, LLM понимает лучше.
|
||||
- **На выходе:** опция `output_format: "docx" | "md"`. По умолчанию `"docx"`.
|
||||
|
||||
---
|
||||
|
||||
## Итоговая карта изменений
|
||||
|
||||
```
|
||||
scanner.py — новый промпт (убрать список типов, LLM сама называет типы)
|
||||
generators/ — добавить fallback-генератор для неизвестных типов
|
||||
obfuscator.py — вызывать fallback если тип неизвестен
|
||||
api_bp.py — принять параметр output_format
|
||||
replacer.py — добавить replace_to_markdown()
|
||||
builder.py — упаковывать .md если output_format=md
|
||||
```
|
||||
|
||||
**Не трогать:** двухпроходная схема, checksum-генераторы, ZIP-безопасность.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Запрос к Соннету — архитектура пофайловой обработки с одним ZIP (2026-07-13)
|
||||
|
||||
## Задача
|
||||
|
||||
DrHider (Flask + waitress). Ограничения:
|
||||
- Фронтенд должен слать файлы **по одному** (сеть не пропускает большие multipart)
|
||||
- Бэкенд должен обрабатывать **по одному** (без параллельных LLM-запросов)
|
||||
- Результат: **один ZIP** со всеми обфусцированными .md + **один общий** mapping.csv
|
||||
- **Никакой обработки данных в JS** — фронтенд только кнопки и скачивание
|
||||
|
||||
## Варианты
|
||||
|
||||
### А. Upload → Process
|
||||
1. `POST /api/upload` — один файл → `{session:"abc"}`
|
||||
2. Фронтенд циклом шлёт все, получает session
|
||||
3. `POST /api/process/abc` — бэкенд обрабатывает всё → ZIP
|
||||
4. Фронтенд скачивает ZIP
|
||||
|
||||
### Б. Один запрос, все файлы
|
||||
1. Фронтенд: один POST, все файлы в FormData
|
||||
2. Бэкенд: получил все → обработал по одному → ZIP
|
||||
3. Waitress держит долгое соединение, таймаут 600с
|
||||
|
||||
### В. SSE с прогрессом
|
||||
1. `POST /api/start` → `{session:"abc"}`
|
||||
2. `POST /api/upload/abc` — по одному файлу
|
||||
3. `GET /api/process/abc` — SSE стримит прогресс каждого файла и в конце ZIP
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. Какой вариант правильный для Flask + waitress?
|
||||
2. Вариант Б — пройдёт ли большой multipart через ingress (6 файлов × 30-60 KB)?
|
||||
3. Если А — как чистить сессии? (пользователь закрыл вкладку, файлы остались в памяти)
|
||||
4. Нужен ли SSE или достаточно простого XHR с таймером?
|
||||
5. mapping.csv — генерировать на лету (по мере обработки) или после всех файлов?
|
||||
|
||||
## Сейчас работает (но сломано)
|
||||
|
||||
Сейчас: фронтенд шлёт по одному файлу, каждый получает отдельный ZIP. Пользователь хочет один ZIP в конце. JS обрабатывал ZIP'ы (распаковывал/перепаковывал) — это убрали, всё должно быть на бэкенде.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Запрос к Sonnet: анализ обфускации
|
||||
|
||||
**Дата:** 2026-07-13
|
||||
|
||||
---
|
||||
|
||||
**Задача:** Найти причину расхождения между ожидаемыми кодами замен и реальными заменами.
|
||||
|
||||
## Ожидаемое поведение
|
||||
|
||||
`_next_token()` в `drhider/scanner.py` генерирует коды замен формата:
|
||||
- `Технология_0001`, `Прогресс_0002` (для компаний)
|
||||
- `+7_000_000_0001` (для телефонов)
|
||||
- `email_0001` (для email)
|
||||
- `ИНН_0001` (для ИНН)
|
||||
- `Иванов_0001` (для ФИО)
|
||||
|
||||
## Реальный результат — `TMP/mapping.csv`
|
||||
|
||||
```
|
||||
тип_данных,оригинал,замена
|
||||
phone,+7 (495) 789-41-35,+7 (495) 906-87-80 ← НЕ код замены, реальный номер
|
||||
email,info@nubes.ru,ypiorej@company.local ← НЕ код замены, реальный email
|
||||
company,ЗАО ****XXX****001****,АО «Прогресс» ← НЕ код замены, реальное название
|
||||
company,ЗАО XXX001,ООО «Альянс» ← НЕ код замены, реальное название
|
||||
inn_ul,ИНН 9706005293,ИНН 1846691249 ← НЕ код замены, реальный ИНН
|
||||
kpp,КПП 772401001,КПП 350443068 ← НЕ код замены, реальный КПП
|
||||
ks,Кор/сч 30101810400000000225,Кор/сч 30101201490272090211 ← НЕ код замены
|
||||
ogrn,ОГРН 1207700098759,ОГРН 1048672318430 ← НЕ код замены
|
||||
company,ООО "НУБЕС",ООО «Спектр» ← НЕ код замены
|
||||
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
|
||||
text,"Обязанности Заказчика\nСвоевременно",Степанов А.В.
|
||||
text,"Обязанности Исполнителя\nОказывать",Петров П.Е.
|
||||
text,"Ответственность Заказчика\nЗаказчик",Новиков А.М.
|
||||
text,"Ответственность Исполнителя\nИсполнитель",Попов О.М.
|
||||
company,ПАО Сбербанк,АО «Формат»
|
||||
```
|
||||
|
||||
**Ни одного кода замены формата `XXX_0000` в колонке «замена».**
|
||||
|
||||
## Реальный результат — `TMP/договор-XXX001-03700_obfuscated.md` (первые строки)
|
||||
|
||||
```markdown
|
||||
**АО «Прогресс»** ... и **ООО **"**НУБЕС**" ... Новиков И.В. ...
|
||||
```
|
||||
|
||||
- `Новиков И.В.` — НЕ заменён (должен быть `Иванов_0001`)
|
||||
- `ООО "НУБЕС"` — НЕ заменён в тексте (есть в mapping но замена не применена?)
|
||||
- `г. Москва, 1-я Стекольная 7с5` — адрес не заменён
|
||||
|
||||
## Файлы для анализа
|
||||
|
||||
**Обязательно прочитать:**
|
||||
1. `drhider/scanner.py` — `_next_token()`, `scan_regex()`, `scan_llm_ner()`, TYPE_POOLS, промпт LLM (строки 109-151)
|
||||
2. `drhider/obfuscator.py` — порядок вызова regex → LLM, как объединяется mapping (строки 63-95)
|
||||
3. `drhider/config.py` — ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN
|
||||
4. `drhider/llm_client.py` — `LLMClient.complete()`
|
||||
|
||||
**Прочитать для контекста:**
|
||||
5. `TMP/mapping.csv` — полный результат
|
||||
6. `TMP/договор-XXX001-03700_obfuscated.md` — обфусцированный файл
|
||||
7. `TMP/примеры_договоров_для_ИИ/договор-XXX001-03700.docx` — оригинал (для сверки)
|
||||
|
||||
**Не лезть:** `builder.py`, `extractor.py`, `replacer.py`, `session.py`, `templates/`, docs/, History/.
|
||||
|
||||
## Ключевые точки для проверки
|
||||
|
||||
### 1. `scanner.py` — `_next_token()` (стр. 63-82)
|
||||
|
||||
```python
|
||||
def _next_token(entity_type, counters):
|
||||
pool, prefix_key = TYPE_POOLS.get(entity_type, _FALLBACK_POOL)
|
||||
template = random.choice(pool)
|
||||
key = f"{prefix_key}_{template}"
|
||||
counters[key] = counters.get(key, 0) + 1
|
||||
return f"{template}_{counters[key]:04d}"
|
||||
```
|
||||
|
||||
Возвращает `Технология_0001`, `+7_000_000_0001`. **Никак не может вернуть** `АО «Прогресс»` или `+7 (495) 906-87-80`.
|
||||
|
||||
### 2. `scanner.py` — `scan_regex()` (стр. 89-108)
|
||||
|
||||
```python
|
||||
mapping[original] = _next_token(entity_type, counters)
|
||||
```
|
||||
|
||||
Всегда вызывает `_next_token`. Результат должен быть кодом замены.
|
||||
|
||||
### 3. `scanner.py` — `scan_llm_ner()` (стр. 109-179)
|
||||
|
||||
```python
|
||||
mapping[val] = _next_token(ent_type, counters)
|
||||
```
|
||||
|
||||
Тоже вызывает `_next_token`. Результат должен быть кодом замены.
|
||||
|
||||
### 4. `obfuscator.py` — порядок вызова (стр. 86-92)
|
||||
|
||||
```python
|
||||
scanner.scan_regex(text, self._mapping, self._counters) # сначала
|
||||
scanner.scan_llm_ner(all_texts, self._mapping, ...) # потом
|
||||
```
|
||||
|
||||
### 5. `config.py` — regex-паттерны:
|
||||
|
||||
- `phone`: `(?:\+7|8)[\s\-]?\(?\d{3}\)?...` — должно матчить `+7 (495) 789-41-35`
|
||||
- `email`: `[a-zA-Z0-9._%+-]+@...` — должно матчить `info@nubes.ru`
|
||||
- `inn_ul`: `ИНН\s*\d{10}` — должно матчить `ИНН 9706005293`
|
||||
- `COMPANY_PATTERN`: `(?:ООО|ЗАО|...)\s+(?:«[^»]+»|"[^"]+"|...)` — должно матчить `ООО "НУБЕС"` и `ЗАО "XXX001"`
|
||||
- `PERSON_PATTERN`: `[А-Я][а-я]+\s+[А-Я]\.[А-Я]\.` — должно матчить `Новиков И.В.`
|
||||
|
||||
## Главный вопрос
|
||||
|
||||
Каждый путь (regex и LLM) вызывает `_next_token()`. `_next_token()` физически не может выдать `АО «Прогресс»` или `+7 (495) 906-87-80`. Откуда в `mapping.csv` берутся реалистичные замены? Кто и где их генерирует В ОБХОД `_next_token()`?
|
||||
@@ -0,0 +1,17 @@
|
||||
# Ответ Соннета — архитектура upload+process (2026-07-13)
|
||||
|
||||
## Рекомендация: Вариант А + блокирующий /process
|
||||
|
||||
```
|
||||
POST /api/upload → {session_id} (фронт шлёт по одному, N раз)
|
||||
POST /api/process/{sid} → {status:"done"} (блокирует до конца обработки)
|
||||
GET /api/download/{sid} → ZIP
|
||||
```
|
||||
|
||||
Без polling — `/process` просто блокирует XHR до готовности. Waitress держит.
|
||||
|
||||
## Детали
|
||||
- Mapping.csv — на лету, аккумулируется после каждого файла
|
||||
- Сессии — dict в памяти, TTL 30 минут через threading.Timer
|
||||
- Фронт: цикл upload → process → download. Три простых XHR.
|
||||
- Никакого JS-кода обработки данных.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Sonnet: анализ обфускации — TMP-файлы vs текущий код
|
||||
|
||||
**Дата:** 2026-07-13
|
||||
|
||||
---
|
||||
|
||||
## Главный вывод
|
||||
|
||||
**TMP-файлы созданы СТАРОЙ версией кода.** Они не были сгенерированы текущим `_next_token()`-кодом. Это не баг в текущем коде — это артефакт предыдущей реализации.
|
||||
|
||||
---
|
||||
|
||||
## Доказательства
|
||||
|
||||
### 1. Имена файлов — старый формат
|
||||
|
||||
Текущий `obfuscator.py` генерирует имена вида `договор-НУБЕС001-03700.md` (меняет расширение). TMP-файлы: `договор-XXX001-03700_obfuscated.md` — суффикс `_obfuscated` и `XXX`-маскировка в имени. Текущий код так не делает.
|
||||
|
||||
### 2. Тип `text` — fallback из builder.py, не из сканера
|
||||
|
||||
`build_mapping_csv()` определяет тип постфактум по паттернам. Если ни один не совпал → `"text"`. Типа `text` нет в `TYPE_POOLS` scanner.py. Значит строки вида:
|
||||
```
|
||||
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
|
||||
```
|
||||
попали из старого кода, который не фильтровал роли (Исполнитель, Заказчик) — текущий LLM-промпт их явно исключает.
|
||||
|
||||
### 3. Одна сущность → разные замены
|
||||
|
||||
```
|
||||
email,info@nubes.ru,ypiorej@company.local ← файл 1
|
||||
email,info@nubes.ru,qifdckke@org.example.ru ← файл 2
|
||||
```
|
||||
|
||||
Текущий `TwoPassObfuscator` гарантирует глобальную согласованность. Старый код обрабатывал файлы по одному — отсюда расхождения.
|
||||
|
||||
### 4. Реалистичные замены невозможны из `_next_token()`
|
||||
|
||||
```python
|
||||
TYPE_POOLS = {
|
||||
"phone": (["+7_000_000"], "phone"), # → "+7_000_000_0001"
|
||||
"email": (["email"], "email"), # → "email_0001"
|
||||
}
|
||||
```
|
||||
|
||||
`+7 (495) 906-87-80` или `ypiorej@company.local` физически не могут выйти из этих пулов.
|
||||
|
||||
---
|
||||
|
||||
## Ответ на главный вопрос
|
||||
|
||||
> Кто и где генерирует замены в обход `_next_token()`?
|
||||
|
||||
**Никто в текущем коде.** Это артефакты старого кода (версии до v3). Текущий `_next_token()` написан позже.
|
||||
|
||||
---
|
||||
|
||||
## Что реально нужно проверить
|
||||
|
||||
| Вопрос | Как проверить |
|
||||
|--------|---------------|
|
||||
| Работает ли текущий код? | Прогнать оригиналы через v0.0.12, посмотреть mapping.csv |
|
||||
| Regex ловит все сущности? | Проверить `config.py` паттерны на примерах из TMP |
|
||||
| Адрес `115404, г. Москва...` — почему не заменён? | `ENTITY_PATTERNS` не содержит тип `address` |
|
||||
| `ООО "НУБЕС"` в тексте не заменён? | Возможно кавычки — regex матчит `«»` но не `""` |
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
**Ложное противоречие** — сравнивались TMP-файлы (старый код) с текущим scanner.py. Нужно прогнать текущий код на тех же документах и посмотреть результат.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Sonnet: анализ багов обфускации — fix
|
||||
|
||||
**Дата:** 2026-07-13
|
||||
|
||||
---
|
||||
|
||||
## Баг 1: `ООО "НУБЕС"` не заменено
|
||||
|
||||
**Причина:** DOCX → Markdown: `**ООО **"**НУБЕС**"`. Кавычка в отдельном run → `**` разрывает строку. Replacer ищет точное совпадение `ООО "НУБЕС"` — не находит.
|
||||
|
||||
**Фикс (replacer.py):** добавить `_md_tolerant_pattern()` — паттерн, допускающий `**` между частями строки. При промахе точного совпадения — фоллбэк с этим паттерном.
|
||||
|
||||
```python
|
||||
def _md_tolerant_pattern(original: str) -> str:
|
||||
MD = r'(?:\*{1,2})?'
|
||||
parts = re.findall(r'[А-ЯЁа-яёA-Za-z0-9]+|[^А-ЯЁа-яёA-Za-z0-9]', original)
|
||||
return MD + MD.join(re.escape(p) for p in parts) + MD
|
||||
```
|
||||
|
||||
## Баг 2: `Новиков И.В.` → `ФИО`
|
||||
|
||||
**Причина:** В оригинале буквально `ФИО` — незаполненный шаблон. Не баг паттерна.
|
||||
|
||||
**Превентивный фикс (config.py):** `\s+` → `[\s\xa0]+` в PERSON_PATTERN — на случай неразрывных пробелов в DOCX.
|
||||
|
||||
```python
|
||||
PERSON_PATTERN = re.compile(
|
||||
r'\b[А-Я][а-я]+[\s\xa0]+[А-Я]\.[А-Я]\.'
|
||||
r'|\b[А-Я][а-я]+[\s\xa0]+[А-Я][а-я]+[\s\xa0]+[А-Я][а-я]+',
|
||||
)
|
||||
```
|
||||
|
||||
## Моё мнение
|
||||
|
||||
**Баг 1 — делать.** Логично, минимально, решает проблему Markdown-жирности.
|
||||
|
||||
**Баг 2 — `\xa0` делать** (превентивно). Сам `ФИО` — не баг, документ содержит незаполненный шаблон. Можно добавить `ФИО` в список игнорируемых (исключить из mapping), но не обязательно.
|
||||
@@ -0,0 +1,105 @@
|
||||
# 2026-08-20 — Вопрос-ответ по LLM-чанкингу (Q&A)
|
||||
|
||||
Контекст: сервис обфускации, двухступенчатый детект ПДн (regex + LLM). Пользователь
|
||||
хочет, чтобы LLM обрабатывал ВСЕ данные, а не урезку 8000 символов.
|
||||
Обсуждение с Sonnet: основной вопрос + 6 уточнений.
|
||||
|
||||
Предыдущие файлы:
|
||||
- History/2026-08-20-sonnet-query-llm-pattern.md — рекомендуемая схема
|
||||
- History/2026-08-20-sonnet-query-llm-followup.md — уточнения перед внедрением
|
||||
|
||||
---
|
||||
|
||||
## Исходная проблема
|
||||
`scan_llm_ner` (drhider/scanner.py) обрезал:
|
||||
- каждый файл до первых 3000 символов (`t[:3000]`);
|
||||
- всё вместе до 8000 символов (`combined[:8000]`);
|
||||
- один вызов LLM на весь набор.
|
||||
|
||||
Итог: при 299 файлах LLM «видел» только ~8000 символов (~0.1–0.3% данных).
|
||||
Пользователь: «всё затевалось чтобы ЛЛМ ВСЁ обрабатывал». → убрать урезку.
|
||||
|
||||
## Рекомендуемая схема (Sonnet, основной ответ)
|
||||
- Отказаться от глобального батчинга → «один файл = один или несколько вызовов LLM», параллельно.
|
||||
- Чанкинг per-file: чанк 6000 символов, overlap 400–500 символов.
|
||||
- Дедупликация по файлу + верификация (найденная строка реально в тексте → отсекает галлюцинации).
|
||||
- Батчинг мелких файлов — опционально, второй шаг.
|
||||
|
||||
## Уточнения (У.1–У.6) и ответы Sonnet
|
||||
|
||||
### У.1 — Алгоритм разбивки на чанки
|
||||
Приоритет границ: `\n\n` → `\n` → `. ` → `? ` → `! ` → `; ` → `, ` → ` ` (пробел — крайний).
|
||||
Ищем ближайшую границу с конца в диапазоне `[size//2 .. size]`. Жёсткий разрез — только если нет ни одного пробела.
|
||||
Overlap: отступить назад на overlap, найти начало слова.
|
||||
Псевдокод:
|
||||
```python
|
||||
BOUNDARIES = ['\n\n', '\n', '. ', '? ', '! ', '; ', ', ', ' ']
|
||||
def split_into_chunks(text, size=6000, overlap=500):
|
||||
chunks=[]; start=0
|
||||
while start < len(text):
|
||||
end=min(start+size, len(text))
|
||||
if end==len(text):
|
||||
chunks.append(text[start:]); break
|
||||
cut=None
|
||||
for b in BOUNDARIES:
|
||||
pos=text.rfind(b, start+size//2, end)
|
||||
if pos!=-1:
|
||||
cut=pos+len(b); break
|
||||
if cut is None: cut=end
|
||||
chunks.append(text[start:cut])
|
||||
ov=max(start, cut-overlap)
|
||||
space=text.find(' ', ov)
|
||||
start=(space+1) if (space!=-1 and space<cut) else ov
|
||||
return chunks
|
||||
```
|
||||
|
||||
### У.2 — Нормализация для дедупа
|
||||
```python
|
||||
def normalize_entity(s):
|
||||
s=s.strip()
|
||||
s=re.sub(r'\s+',' ',s)
|
||||
s=s.lower()
|
||||
s=re.sub(r'[«»“”‘’"\' ]','"',s) # кавычки → "
|
||||
s=re.sub(r'[—–−-]','-',s) # тире → -
|
||||
s=s.replace('\u00ad','') # мягкий перенос
|
||||
s=s.replace('ё','е') # Ё→Е (OCR/PDF)
|
||||
return s
|
||||
```
|
||||
Дедуп: `seen=set()` — добавлять, только если `normalize_entity(v) not in seen`.
|
||||
|
||||
### У.3 — Источник истины для верификации
|
||||
Полный текст файла ДО обфускации (не чанк).
|
||||
```python
|
||||
def verify(entity, full_text):
|
||||
if entity in full_text: return True
|
||||
return normalize_entity(entity) in normalize_entity(full_text)
|
||||
```
|
||||
Если сущность из двух чанков с разным форматированием — нормализованный ключ одинаков → дедуп оставит один; хранить длиннее (или первый).
|
||||
|
||||
### У.4 — Батчинг мелких в первой итерации
|
||||
НЕ нужен. 270 мелких: 1 файл=1 вызов ≈ 270 вызовов ≈ 101с при 4 потоках; батч по 5 → 54 вызова ≈ 20с. Разница ~80с несущественна. Батчинг — вторая итерация.
|
||||
|
||||
### У.5 — Промпт при чанкинге
|
||||
Блок «Already captured by regex» ОСТАВИТЬ в каждом вызове (снижает дублирование с regex).
|
||||
Блок правил — как есть в первую итерацию (если API с system/user — правила в system, текст чанка в user; иначе как сейчас).
|
||||
Первый шаг: промпт не менять, только текстовый блок = чанк.
|
||||
|
||||
### У.6 — Общее время и rate-limit
|
||||
Расчёт: 270 мелких (1 вызов) + 30 крупных (≈4 чанка) = 390 вызовов.
|
||||
- 4 потока × 2с: 390/4×2 ≈ 195с ≈ 3.5 мин.
|
||||
- 8 потоков: ~1.5–2 мин.
|
||||
Rate-limit свой LLM: внешнего нет, ограничение GPU. Начать с 4, поднять до 8, если latency не растёт (>3× от базового — снижать).
|
||||
|
||||
---
|
||||
|
||||
## Итоговое решение (для внедрения, ждёт «делай»)
|
||||
1. `scanner.py scan_llm_ner`: убрать `[:3000]`/`[:8000]`;
|
||||
по каждому файлу из `all_texts` разбить на чанки (6000/500, split_into_chunks);
|
||||
параллельные вызовы (ThreadPool, concurrency 4, крупные вперёд);
|
||||
для каждого чанка — полный промпт + «regex-найденные»;
|
||||
собрать сущности, normalize-дедуп, verify против полного текста файла, в mapping.
|
||||
2. Добавить хелперы split_into_chunks / normalize_entity / verify в scanner.py.
|
||||
3. Не дублировать regex-найденное; считать токены/время LLM корректно (llm_active/elapsed).
|
||||
4. Прогнать тест на малом наборе (не прод): сколько сущностей добавит LLM, время.
|
||||
|
||||
Статус: НЕ внедрено (ждёт «делай»). Версия при внедрении — v0.0.57.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Промпт для Sonnet — честный ответ: можно ли ускорить без риска устойчивости?
|
||||
|
||||
## Роль
|
||||
Отвечай ЧЕСТНО и ПО ДЕЛУ. Без воды, без лирики. Если ускорение невозможно без
|
||||
компромисса (устойчивость/таблицы/риск) — прямо скажи «нет» и объясни почему.
|
||||
Не предлагай «смену библиотеки» — уже пробовали, PyMuPDF теряет таблицы (40 vs 94),
|
||||
ТАБЛИЦЫ КРИТИЧНЫ для пользователя. Учитывай это ограничение железно.
|
||||
|
||||
## ФАКТЫ (результаты наших замеров, НЕ догадки)
|
||||
1. `Spartan10Manual.pdf` (14.8 МБ, 619 страниц, скан/руководство):
|
||||
- `extract_text()` суммарно = **64.4с** (104мс/стр) — УЗКОЕ МЕСТО
|
||||
- `extract_tables()` суммарно = **0.2с** (0мс/стр) — ничтожно
|
||||
- на 272 страницах без линий extract_tables = 0.0с
|
||||
→ Твоё прошлое предположение «на сканах дорогой extract_tables» НЕ подтвердилось.
|
||||
Узкое место — ИЗВЛЕЧЕНИЕ ТЕКСТА, а не таблиц. Не повторяй эту ошибку.
|
||||
2. Большой PDF 19 МБ (0144-03-2023_отчет об оценке.pdf, 141 стр, 94 таблицы) — ~25-26с.
|
||||
3. CPU пода = 2 ядра, Memory = 4Gi. Воркер один, последовательный.
|
||||
4. Разброс времени одного файла между запусками: 70с → 470с (Spartan10). Причины
|
||||
не установлены точно (подозрение: CPU throttling/нагрузка пода, декомпрессия битмапов).
|
||||
5. `.doc` обрабатывается через HTTP-сервис liberta (IO-bound).
|
||||
6. apply_replacements — один regex из всех ключей (уже оптимизирован).
|
||||
7. Сессия хранит файлы в памяти до 500 МБ (session.py). ProcessPool с fork —
|
||||
риск COW-копии памяти.
|
||||
|
||||
## Вопрос (ответь честно)
|
||||
Можно ли ЗНАЧИТЕЛЬНО ускорить обработку (цель — сократить время на больших PDF/наборах),
|
||||
НЕ рискуя:
|
||||
- устойчивостью (битые файлы, SSE, память пода 4Gi),
|
||||
- потерей таблиц (критично),
|
||||
- сложностью поддержки (код должен остаться понятным)?
|
||||
|
||||
Требования к ответу:
|
||||
- Дай КОНКРЕТНЫЕ пункты, каждый: что менять / где / ожидаемый эффект (с цифрами из фактов выше) / риски.
|
||||
- Раздели на:
|
||||
A) безопасно и просто (готов внедрить сейчас, низкий риск),
|
||||
B) заметный выигрыш, но с рисками/сложностью (нужен осознанный выбор),
|
||||
C) рискованно/не стоит (объясни почему).
|
||||
- Если для БЕЗОПАСНОГО варианта реального выигрыша нет — так и скажи: «безопасно ускорить
|
||||
значительно нельзя», и предложи что реально можно сделать без риска (даже если эффект мал).
|
||||
- НЕ предлагай смену pdfplumber/PyMuPDF и не предлагай то, что ломает таблицы.
|
||||
- Оцени РЕАЛЬНО: при CPU=2 параллельность текста по страницам даст хоть что-то?
|
||||
(GIL/процессы, overhead fork/spawn, память). С цифрами.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Промпт для Sonnet — уточнения по LLM-чанкингу (перед внедрением)
|
||||
|
||||
Ты дал рекомендуемую схему (чанкинг 6000 chars, overlap 500, параллельность 4–8,
|
||||
дедуп + верификация). Перед внедрением уточни практические детали. Отвечай КРАТКО,
|
||||
по пунктам, с конкретикой (алгоритм/числа). Без воды.
|
||||
|
||||
## У.1 — Разбивка на чанки: по чему резать
|
||||
Ты сказал «разбивать по абзацам/предложениям, не по символам». Но в PDF-выгрузках
|
||||
текст часто сливается в один-два гигантских «абзаца» (нет \n). Дай КОНКРЕТНЫЙ алгоритм
|
||||
разбивки произвольного текста (в т.ч. без \n) на чанки ~6000 символов с overlap 500:
|
||||
- по каким границам резать в приоритете (\n\n, \n, '. ', '? ', '; ', потом символы)?
|
||||
- как применить overlap без разрыва слова?
|
||||
- псевдокод функции split_into_chunks(text, size=6000, overlap=500).
|
||||
|
||||
## У.2 — Дедупликация при overlap и некорректном форматировании
|
||||
LLM может вернуть один и тот же value из двух чанков с РАЗНЫМ форматированием
|
||||
(лишние пробелы, регистр, тире/дефис). Проверки `strip().lower()` недостаточно.
|
||||
Как нормализовать перед дедупом (например, сжать пробелы, унифицировать кавычки/тире)?
|
||||
Дай конкретный набор нормализаций для русских деловых текстов.
|
||||
|
||||
## У.3 — Верификация против исходника
|
||||
Проверку «найденного значения нет в исходном тексте → отбросить (галлюцинация)» делать
|
||||
против ИСХОДНОГО извлечённого текста файла (до обфускации), верно? Или против чанка?
|
||||
Уточни, что есть «источник истины» для верификации, и как быть, если value найден в
|
||||
нескольких чанках, но слегка отличается (нормализованное сравнение).
|
||||
|
||||
## У.4 — Малые файлы: нужен ли батчинг в первую итерацию
|
||||
Набор сотен мелких файлов (<1000 символов). Батчинг 5–8 в один вызов ускорит, но усложнит
|
||||
(деградация разметки, больший ответ). Для ПЕРВОЙ итерации можно ли обойтись «1 файл = 1 вызов»
|
||||
без батчинга, даже если файлов сотни? Оцени, насколько медленнее будет без батчинга.
|
||||
|
||||
## У.5 — Промпт при пофайловом чанкинге
|
||||
Текущий промпт (в коде) содержит блоки «Already captured by regex» + большой список правил.
|
||||
При пофайловом/почанковом вызове эти блоки повторяются в каждом запросе. Оставить промпт
|
||||
как есть (меняя только текст), или его надо упростить под чанк (одна папка уже нашла — regex)?
|
||||
Повлияет ли повтор правил на качество/токены?
|
||||
|
||||
## У.6 — Прогресс и общее время
|
||||
300 файлов, многие >6000 символов → сотни LLM-вызовов × по 1–2с = минуты-десятки минут.
|
||||
Пользователь готов ждать (LLM своя). Но оцени: с параллельностью 4 и ~5 вызовов/файл на
|
||||
крупных — реальное общее время для набора ~300 файлов (в т.ч. 270 мелких + 30 крупных).
|
||||
И: не упрёмся ли в rate-limit своего LLM-эндпоинта при 4-8 параллельных.
|
||||
|
||||
## Формат
|
||||
- По каждому У.1–У.6: короткий ответ + конкретика (алгоритм/числа/решение).
|
||||
- Если что-то некритично для первой итерации — скажи явно «можно первым шагом упростить так».
|
||||
@@ -0,0 +1,30 @@
|
||||
# Промпт для Sonnet — вопрос по использованию LLM (без ссылок на файлы)
|
||||
|
||||
## Контекст
|
||||
У нас сервис обфускации документов (удаление персональных данных из русских деловых документов).
|
||||
Двухступенчатый детект ПДн:
|
||||
1. Regex-паттерны (телефоны, ИНН, email, типовые формы — быстро, локально, дёшево).
|
||||
2. LLM (общая NER-подстраховка) — сейчас обрабатывает только урезанный фрагмент:
|
||||
- от каждого файла берутся первые ~3000 символов,
|
||||
- всё вместе обрезается до ~8000 символов,
|
||||
- выполняется ОДИН вызов LLM на весь набор.
|
||||
|
||||
Пользователь хочет, чтобы LLM обрабатывал ВСЕ данные (ВСЕ файлы целиком), не только первые 8000 символов.
|
||||
LLM — своя, стоимость не важна. Важно качество распознавания ПДн и разумная архитектура вызова.
|
||||
|
||||
## Вопрос
|
||||
Как лучше организовать вызов LLM, чтобы он БЕЗ потерь покрывал все документы (набор из сотен файлов, каждый до нескольких десятков страниц), сохраняя качество распознавания русских деловых документов?
|
||||
|
||||
Ограничения/требования к ответу:
|
||||
- Не предлагай менять модель/external API — LLM своя, она остаётся.
|
||||
- Подумай про: пакетирование файлов (размер батча), максимальную длину контекста на запрос,
|
||||
как избежать обрезки данных, как не потерять качество при больших объёмах,
|
||||
параллельность запросов (если релевантна), дедупликацию найденного между пакетами.
|
||||
- Дай конкретную рекомендуемую схему (числа: размер батча, лимит символов на файл/пакет, параллельность).
|
||||
- Оцени риски: токены/время, качество, риск пропуска.
|
||||
- Сухо, по делу, без воды.
|
||||
|
||||
## Формат
|
||||
- Секция «Рекомендуемая схема» — конкретный план с числами.
|
||||
- Секция «Риски» — что может пойти не так и как смягчить.
|
||||
- Секция «Минимум для старта» — если хочется просто и быстро, что достаточно сделать первым шагом.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Промпт для Sonnet — follow-up вопросы по ревью (скорость + сбои)
|
||||
|
||||
## Контекст
|
||||
Ты дал ревью (файл: History/2026-08-20-sonnet-query-review-speed.md). Часть пунктов приняли,
|
||||
часть — требуют уточнения. Отвечай ТОЛЬКО на вопросы ниже, сухо, конкретно, без воды.
|
||||
Если что-то из твоих утверждений основано на допущении, а не на факте — скажи явно «допущение» и
|
||||
что нужно проверить, чтобы подтвердить.
|
||||
|
||||
## Вопрос 1. Фильтр `page.lines or page.curves` перед `extract_tables()` (extractor.py)
|
||||
Ты предложил: не вызывать extract_tables() на страницах без vector-линий, т.к. на сканах это пустой проход.
|
||||
Вопросы:
|
||||
1.1. Перечисли КОНКРЕТНО типы таблиц, которые `extract_tables()` находит, но которые НЕ дают
|
||||
`page.lines`/`page.curves` (текстовые сетки, таблицы на заливке/fill, встроенные картинки, что ещё?).
|
||||
1.2. Есть ли в нашем реальном наборе (TMP/спецификации, 0144-03-2023_отчет об оценке.pdf, документы из
|
||||
DownLoads) риск, что фильтр отбросит реальную таблицу? Это надо проверить фактом — предложи
|
||||
точный способ замера: как сравнить число таблиц с фильтром и без на конкретных файлах.
|
||||
1.3. Насколько `page.lines`/`page.curves` дешевле `extract_tables()`? В pdfplumber они тоже делают
|
||||
парсинг объектов страницы — дай оценку реального выигрыша на скан-PDF, не «в разы», а чем измерить.
|
||||
1.4. Безопасная альтернатива: может, стоит вызывать extract_tables() только если `page.find_tables()`
|
||||
вернул непусто? Или это то же самое по стоимости? Уточни, что реально дорого внутри extract_tables().
|
||||
|
||||
## Вопрос 2. `_decode_name`: попытка decode("utf-8") перед decode("cp866")
|
||||
Ты предложил порядок: raw.decode("utf-8"), при ошибке — raw.decode("cp866").
|
||||
2.1. Оцени риск ложного срабатывания: когда CP866-байты случайно образуют валидный UTF-8
|
||||
(например, псевдографика 0xC0-0xDF + продолжения 0x80-0xBF). Насколько это реально для имён
|
||||
файлов 1С? Есть ли способ отличить «настоящий UTF-8» от «случайного» (например, проверить
|
||||
диапазон символов после декодирования)?
|
||||
2.2. Подтверди, что для нашего реального случая (Info-ZIP UTF-8 без флага 0x800) этот порядок даёт
|
||||
корректное имя, а для CP866 1С — не ломает.
|
||||
|
||||
## Вопрос 3. ProcessPoolExecutor по страницам / по файлам
|
||||
Ты предложил распараллелить извлечение текста. Вопросы:
|
||||
3.1. Риск памяти: в контейнере сессия уже держит файлы в памяти (до 500 МБ в session.py). При fork
|
||||
воркеры получают COW-копию памяти родителя. Оцени реальный риск OOM в managed-поде (лимит CPU 2,
|
||||
Memory 4Gi) при 4-8 процессах. Не будет ли хуже, чем текущее последовательное?
|
||||
3.2. pdfplumber сам по себе уже использует один процесс. Дай конкретный план безопасного
|
||||
распараллеливания: какие данные передавать в воркер (только bytes страницы или весь файл?),
|
||||
как собирать результаты в порядке индексов, как не раздуть память.
|
||||
3.3. Учитывая CPU 2 (2 ядра) — какой реальный выигрыш даст ProcessPool на 2 ядрах? Стоит ли это
|
||||
сложности, или сначала дёшево (П.1 + П.2)?
|
||||
|
||||
## Вопрос 4. Что реально тормозит в Spartan10Manual.pdf (70–470с на 14.8 МБ)
|
||||
Ты утверждаешь: виноват пустой extract_tables() на скане. Но время скачет 70→470с — это подозрительно.
|
||||
4.1. Как точно измерить, что дороже на ЭТОМ файле: extract_text() по страницам или extract_tables()?
|
||||
Дай конкретный способ замера (тайминги по функциям, по страницам), чтобы не гадать.
|
||||
4.2. Объясни разброс 70–470с: что в коде/данных может давать такой разброс между запусками
|
||||
(кэши, LLM, сеть к liberta, нагрузка CPU пода)?
|
||||
4.3. Есть ли в extract_text() внутри pdfplumber скрытый повторный парсинг (например, повторное чтение
|
||||
объекта при каждом вызове), который можно убрать без смены библиотеки?
|
||||
|
||||
## Формат ответа
|
||||
- По каждому вопросу: короткий ответ + «факт»/«допущение» + что проверить, если допущение.
|
||||
- Без воды, без лирики.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Промпт для Sonnet — код-ревью drhider (скорость + сбои)
|
||||
|
||||
## Роль
|
||||
Ты — ревьюер кода. Отвечай ТОЛЬКО по делу: короткие технические тезисы. Без воды, без лирики, без «как было бы здорово», без общих фраз. Каждый тезис — конкретика: файл, функция, строка, суть.
|
||||
|
||||
## Ограничение: смотреть ТОЛЬКО эти файлы, нигде больше не рыться
|
||||
- `drhider/obfuscator.py` — двухпроходный обфускатор (проход 1: извлечение текста + regex + LLM NER; проход 2: замена)
|
||||
- `drhider/extractor.py` — pdf_to_markdown (pdfplumber), doc_to_markdown (сервис liberta), expand_zips (рекурсия zip, CP437→CP866), защита от zip-бомб
|
||||
- `drhider/scanner.py` — scan_regex, scan_llm_ner
|
||||
- `drhider/replacer.py` — apply_replacements (один regex из всех ключей)
|
||||
- `drhider/llm_client.py` — OpenAI-совместимый клиент, счётчики токенов/времени
|
||||
- `drhider/builder.py` — build_zip, build_mapping_csv
|
||||
- `site/routes/api_bp.py` — upload, process_stream (SSE + воркер в потоке), download/csv
|
||||
- `site/app.py` — create_app, лимиты, setup_logging (LOG_LEVEL/LOG_FILE)
|
||||
- `site/session.py` — хранение сессий в памяти, TTL 30 мин, MAX_SESSION_BYTES
|
||||
- `site/templates/index.html` — фронт: нативный unzip, таблица, SSE-прогресс, таймеры, лимиты 100МБ/1ГБ
|
||||
|
||||
Не читай README, docs, History, tests, Dockerfile, ничего про деплой.
|
||||
|
||||
## Контекст: сбои, которые были (объясни каждый, если увидишь первопричину в коде)
|
||||
1. «SSE connection failed» при живом воркере. Выяснено: воркер падал на битом PDF — `PDFSyntaxError('No /Root object!')` в `extract_text` не был обёрнут по-файлово; после фикса (skip битого файла) SSE доходит до complete. Подтверди/опровергни по коду, есть ли ещё места, где одна ошибка файла роняет весь проход.
|
||||
2. Кракозябры кириллицы в именах zip. Причина: Info-ZIP пишет UTF-8 без флага 0x800 → декодер `encode(cp437).decode(cp866)` ломает. Проверь `_decode_name` в extractor.py: покрывает ли оба реальных сценария (CP866 без флага и UTF-8 с флагом), есть ли дыры.
|
||||
3. HTTP 413 при загрузке. Лимиты: MAX_CONTENT_LENGTH=200MB (запрос), MAX_SESSION_BYTES=500MB (сессия). Фронт пускает до 100МБ/файл и 1ГБ/сумма — рассинхрон фронт/бэк. Отметь.
|
||||
|
||||
## Главная задача: предложи БОЛЕЕ БЫСТРУЮ логику обработки
|
||||
Известные факты производительности (факты, не догадки):
|
||||
- pdfplumber на скане Spartan10Manual.pdf 14.8 МБ — 70–470 секунд на извлечение текста.
|
||||
- Большой PDF 19 МБ — ~25–26 с на extract_text.
|
||||
- LLM NER — один общий вызов на все файлы, ~1.8с на маленьком наборе, зависит от числа токенов.
|
||||
- apply_replacements оптимизирован (один regex), но проверить, нет ли лишней работы.
|
||||
|
||||
Что хочешь от тебя:
|
||||
- Где реальные узкие места в коде (не «вообще», а конкретные функции/строки).
|
||||
- Конкретные предложения ускорения: что менять, как, ожидаемый эффект. Без «использовать PyMuPDF» — уже пробовали, теряет таблицы (94 vs 40), ТАБЛИЦЫ КРИТИЧНЫ. Учитывай это ограничение.
|
||||
- Можно ли ускорить без смены pdfplumber (параллельность страниц, кэши, ограничение повторных парсингов, предобработка)?
|
||||
- Есть ли дублирующая работа между проходами 1 и 2 (например, повторный парсинг/чтение)?
|
||||
- Безопасно ли распараллелить извлечение текста по файлам (потоки/GIL)? Если да — как.
|
||||
|
||||
## Формат ответа
|
||||
- Секция «Сбои»: по каждому — подтверждение/опровержение по коду + где именно.
|
||||
- Секция «Узкие места»: список файл:строка + суть + почему.
|
||||
- Секция «Предложения ускорения»: пронумерованный список, каждый пункт: что/где/как/эффект/риски.
|
||||
- Секция «Итог»: 3–5 самых важных действий по приоритету.
|
||||
- Максимум — сухо, без воды.
|
||||
@@ -0,0 +1,127 @@
|
||||
# 2026-08-20 — Ревью Sonnet: вопросы и ответы (Q&A)
|
||||
|
||||
Серия: ревью скорости обработки + объяснение сбоев.
|
||||
Предыдущие файлы:
|
||||
- History/2026-08-20-sonnet-query-review-speed.md — исходный промпт ревью
|
||||
- History/2026-08-20-sonnet-query-review-speed-followup.md — вопросы по спорным пунктам
|
||||
- Настоящий файл — ответы Sonnet + решения
|
||||
|
||||
---
|
||||
|
||||
## В1. Фильтр `page.lines or page.curves` перед `extract_tables()` (extractor.py)
|
||||
|
||||
### В1.1 — Какие таблицы extract_tables() найдёт, но фильтр отбросит?
|
||||
**Sonnet: допущение (не факт).**
|
||||
`extract_tables()` со стратегией по умолчанию ищет только векторные линии (strategy="lines").
|
||||
- Таблицы на цветном фоне (только `page.rects`, без линий) — фильтр `lines or curves` их пропустит → **дыра**.
|
||||
Нужно `or page.rects`.
|
||||
- Whitespace-таблицы (только пробелы/выравнивание) — не найдёт вообще без strategy="text".
|
||||
- Растровые таблицы в embedded images — не найдёт вообще.
|
||||
|
||||
**Решение:** фильтр = `page.lines or page.curves or page.rects`. Обязательно проверить фактом (В1.2).
|
||||
|
||||
### В1.2 — Как проверить, что фильтр не потеряет таблицы?
|
||||
**Sonnet:**
|
||||
```python
|
||||
with pdfplumber.open(fname) as pdf:
|
||||
for i, page in enumerate(pdf.pages):
|
||||
t_all = page.extract_tables()
|
||||
has_lines = bool(page.lines or page.curves or page.rects)
|
||||
t_filtered = page.extract_tables() if has_lines else []
|
||||
if len(t_all) != len(t_filtered):
|
||||
print(f"p{i}: потеряно {len(t_all)-len(t_filtered)} таблиц")
|
||||
```
|
||||
Прогнать на TMP-спецификациях и 0144-03-2023_отчет об оценке.pdf.
|
||||
|
||||
**Решение:** сделать замер до включения фильтра в код. ТАБЛИЦЫ КРИТИЧНЫ (прецедент PyMuPDF 40 vs 94).
|
||||
|
||||
### В1.3 — Насколько page.lines/curves дешевле extract_tables()?
|
||||
**Sonnet: допущение.**
|
||||
`page.lines` — `@cached_property` (уже разобран при первом обращении к странице). Стоимость ≈ 0.
|
||||
Дорогой в `extract_tables()` — `TableFinder`: строит граф пересечений линий. На скан-PDF всё равно инициализируется.
|
||||
Замер:
|
||||
```python
|
||||
t0 = time.perf_counter(); _ = page.lines; t1 = time.perf_counter()
|
||||
t2 = time.perf_counter(); _ = page.extract_tables(); t3 = time.perf_counter()
|
||||
print(f"lines={t1-t0:.4f}s extract_tables={t3-t2:.4f}s")
|
||||
```
|
||||
|
||||
### В1.4 — find_tables() как guard?
|
||||
**Sonnet:** `find_tables()` и `extract_tables()` — одна стоимость (`extract_tables()` вызывает `finder.find_tables()` внутри). Guard бесполезен.
|
||||
|
||||
---
|
||||
|
||||
## В2. `_decode_name`: попытка decode("utf-8") перед decode("cp866")
|
||||
|
||||
### В2.1 — Риск ложного срабатывания UTF-8 на CP866-байтах?
|
||||
**Sonnet:**
|
||||
CP866 кириллические заглавные = 0xC0–0xDF. В UTF-8 0xC0, 0xC1 — overlong (невалидны), 0xC2–0xDF — валидное начало двухбайта, требует продолжения 0x80–0xBF. CP866 строчные = 0xA0–0xBF — попадают в диапазон UTF-8 continuation bytes.
|
||||
**Реальная коллизия:** «Т» (0xD2) + «г» (0xA3) = 0xD2 0xA3 = валидный UTF-8 (U+04A3 Ң). Риск для имён «Тг…», «Рп…» и т.п.
|
||||
**Защита:** после `decode("utf-8")` проверить, что все символы — в U+0400–U+04FF (кириллица) или ASCII. Если есть символы вне диапазона — ложное срабатывание → фоллбэк cp866.
|
||||
|
||||
### В2.2 — Подтверждение порядка
|
||||
**Sonnet: факт.**
|
||||
- Info-ZIP UTF-8 без флага: zipfile(cp437) → `encode("cp437")` → исходные UTF-8 байты → `decode("utf-8")` → корректно.
|
||||
- CP866 1С: zipfile(cp437) → `encode("cp437")` → CP866 байты → `decode("utf-8")` → почти всегда UnicodeDecodeError (0xC0/0xC1 overlong, 0xE0–0xFF без продолжения) → фоллбэк `decode("cp866")` → корректно.
|
||||
|
||||
**Решение:** фикс принимается, но с валидацией диапазона кириллицы/ASCII после utf-8.
|
||||
|
||||
---
|
||||
|
||||
## В3. ProcessPoolExecutor по страницам/файлам
|
||||
|
||||
### В3.1 — Риск памяти при fork в managed-поде
|
||||
**Sonnet: допущение.**
|
||||
fork → COW. pdfplumber в дочернем создаёт новые объекты (запись → COW). 500МБ session files в родителе — COW, читаются, не копируются. Пик: pdfplumber на 14.8МБ PDF ~150–300МБ на воркер. 2 воркера + родитель ≈ 1–1.5ГБ пик. При 4Gi — умеренный риск.
|
||||
`spawn` (не fork): дочерние стартуют чисто, память сессии не копируется, overhead ~0.5с/процесс.
|
||||
|
||||
### В3.2 — Безопасный план распараллеливания
|
||||
**Sonnet:**
|
||||
- Единица параллелизма: **файл** (не страница), воркер получает `(fname, content_bytes)` через pickle.
|
||||
- content_bytes по pipe: 15МБ ≈ 0.1с overhead.
|
||||
- Порядок: `{i: executor.submit(pdf_to_markdown, content)}`, collect `{i: future.result()}`.
|
||||
- progress_cb из основного потока после future.result().
|
||||
|
||||
### В3.3 — Выигрыш на CPU=2
|
||||
**Sonnet:** на 2 ядрах при ОДНОМ PDF — нет выигрыша (один процесс на одно ядро). Выигрыш только при нескольких PDF: 3 файла × 25с → ~30с вместо 75с.
|
||||
**Рекомендация Sonnet:** сначала В1 (фильтр) + В2 (decode) — бесплатно и безопасно. ProcessPool — потом, если замер покажет, что узкое место реально там.
|
||||
|
||||
**Решение:** ОТЛОЖЕНО. Сначала В1+В2, замерить.
|
||||
|
||||
---
|
||||
|
||||
## В4. Разброс 70–470с на Spartan10Manual.pdf (14.8 МБ)
|
||||
|
||||
### В4.1 — Как измерить, что дороже: extract_text или extract_tables
|
||||
**Sonnet:**
|
||||
```python
|
||||
with pdfplumber.open(io.BytesIO(content)) as pdf:
|
||||
for i, page in enumerate(pdf.pages):
|
||||
t0 = time.perf_counter()
|
||||
page.extract_text()
|
||||
t1 = time.perf_counter()
|
||||
page.extract_tables()
|
||||
t2 = time.perf_counter()
|
||||
print(f"p{i:3d}: text={t1-t0:.3f}s tables={t2-t1:.3f}s")
|
||||
```
|
||||
|
||||
### В4.2 — Причина разброса
|
||||
**Sonnet:**
|
||||
- Факт: pdfplumber не кэширует между запросами — каждый вызов парсит заново.
|
||||
- Допущение: CPU throttling при limit=2 в managed-поде (нагрузка → 470с, пусто → 70с). Проверить `kubectl top pod` во время обработки.
|
||||
- Допущение: битмапы скана разного размера (цветной vs ч/б) → разное время декомпрессии (JPEG2000/JBIG2). Проверить замером по страницам.
|
||||
|
||||
### В4.3 — Скрытый повторный парсинг в extract_text?
|
||||
**Sonnet: факт.** `extract_text()` читает `self.chars` (@cached_property), `extract_tables()` — `self.edges` (@cached_property). Оба используют уже разобранные структуры. Повторного чтения PDF в рамках одного `with pdfplumber.open()` нет.
|
||||
|
||||
---
|
||||
|
||||
## Итоговый план (по приоритету)
|
||||
1. **В2** — `_decode_name`: utf-8 + валидация диапазона (кириллица U+0400–U+04FF/ASCII) → фоллбэк cp866. Фикс Info-ZIP-зипов (кракозябры).
|
||||
2. **В1** — фильтр `page.lines or page.curves or page.rects` перед `extract_tables()` + замер числа таблиц до/после на реальных файлах.
|
||||
3. Замер ускорения на Spartan10Manual.pdf.
|
||||
4. ProcessPool — после фактов, если узкое место там (отложено).
|
||||
5. Диагностика разброса 70–470с: `kubectl top pod` во время обработки + замер по страницам.
|
||||
|
||||
## Статус реализации
|
||||
- Пока НЕ реализовано (ждёт «делай»). Код не менялся в рамках этого Q&A.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Код-ревью DrHider — ответ Соннета (2026-08-24)
|
||||
|
||||
_sonnet. Промпт: `docs/code-review-sonnet.md` (12 файлов кода, 8 вопросов)._
|
||||
_Ревью по v0.0.72. Ничего не исправлено — только задокументировано._
|
||||
|
||||
## 🔴 Критичные
|
||||
|
||||
| Баг | Файл | ~строка | Суть | Фикс |
|
||||
|---|---|---|---|---|
|
||||
| **SSRF** | `api_bp.py` `upload_refs` | ~93 | `url` из клиентского JSON без валидации → `httpx.stream("GET", url)` + `follow_redirects=True`. Доступ к metadata/kube-apiserver | валидировать URL-prefix == `VM_UPLOAD_URL` до запроса |
|
||||
| **Path traversal / zip slip** | `api_bp.py` + `builder.py` | ~88, ~55 | `name = ref.get("name")` без санитизации → `../../evil.md` в выходном ZIP | `name = os.path.basename(ref.get("name",""))` |
|
||||
|
||||
## 🟠 Средние
|
||||
|
||||
| Баг | Файл | ~строка | Суть |
|
||||
|---|---|---|---|
|
||||
| Серверная ошибка не отображается | `api_bp.py` + `index.html` | ~388, ~840 | сервер шлёт `event: error` — это встроенное имя EventSource; клиент не слушает кастомное → «SSE connection failed» вместо реального msg. Переименовать в `proc_error` |
|
||||
| idx-мисматч при `expand_zips` на бэке | `obfuscator.py` + `index.html` | ~90, sendIdx | фронт фильтрует zip по `.pdf/.doc/.docx/.txt/.md`, бэк расширяет ВСЕ файлы → число/порядок не совпадает → `sendIdx[idx]=undefined` |
|
||||
| Слабый SID (48 бит) | `session.py` | 85 | `uuid4().hex[:12]`; убрать `[:12]` → 128 бит |
|
||||
| ZIP-бомба: обход через поддельный `file_size` | `extractor.py` | ~240 | лимит проверяется по `info.file_size` (central dir, подделывается) ДО чтения; проверять ПОСЛЕ `zf.read()` |
|
||||
| Неатомарность pull при ошибке | `api_bp.py` `upload_refs` | ~115 | при исчерпании ретраев 502 БЕЗ `session_id`; частичная сессия-сирота до TTL |
|
||||
|
||||
## 🟡 Мелкие
|
||||
|
||||
| Баг | Файл | ~строка | Суть |
|
||||
|---|---|---|---|
|
||||
| Отмена не работает в extract-фазе | `obfuscator.py` | ~130 | в extract-цикле нет `cancel_event.is_set()`; .doc (liberta 120с) → отмена ждёт все извлечения |
|
||||
| TOCTOU: сессия между check и pause_ttl | `api_bp.py` | ~165-171 | `get_files` (lock снят) → `pause_ttl`; маловероятно |
|
||||
| `fileTimers` не заполняется | `index.html` | ~792 | мёртвый код, fallback всегда 0.0с |
|
||||
| Self-XSS: `f.name` в innerHTML | `index.html` | ~280, ~380 | имя файла без escaping → `textContent`/`escapeHtml()` |
|
||||
| ZIP с non-doc расширениями → мусор | `extractor.py` | ~257 | фильтровать расширения в `expand_zips` |
|
||||
| Сессия не чистится после `/download` | `api_bp.py` | ~430 | 500МБ висит 30 мин → OOM; `cleanup(sid)` |
|
||||
| LLM-таймаут тихо обнуляет чанк | `llm_client.py` + `scanner.py` | ~70, ~220 | `except → return []`, PII не найден без предупреждения |
|
||||
| Debug-эндпоинт `session_files` | `api_bp.py` | ~139 | список файлов любой сессии по SID без токена |
|
||||
| LLM prompt injection | `scanner.py` | ~200 | текст документа дословно в prompt → подавление NER |
|
||||
|
||||
## Подтверждено корректным (Соннет)
|
||||
- Различимость «сессия не найдена» vs «лимит» ✓
|
||||
- Гонок на `cancel_event`/`queue` нет (thread-safe) ✓
|
||||
- Утечек httpx/zipfile/EventSource/таймеров нет ✓
|
||||
- Пустой txt, битый pdf, 0 файлов/все сверх лимита — обрабатываются ✓
|
||||
|
||||
## Что уже исправлено ранее (до этого ревью)
|
||||
- Ретраи pull (v0.0.72) — Соннет отметил остаточную неатомарность.
|
||||
- Разделение статусов «пропущен (лимит)» / «не извлечён» (v0.0.71).
|
||||
- Счётчик дедупа (v0.0.72).
|
||||
|
||||
## TODO-связь
|
||||
- Открытый баг кнопки «Обфусцировать» при 0 файлов — `docs/WhatTODO.md` (не покрыт ревью Соннета).
|
||||
@@ -0,0 +1,23 @@
|
||||
# Waitress + Connection: close — hop-by-hop заголовок
|
||||
|
||||
**Дата:** 2026-07-14
|
||||
**Версия:** 0.0.21 → 0.0.22
|
||||
|
||||
---
|
||||
|
||||
## Ошибка
|
||||
|
||||
`Connection` — hop-by-hop заголовок. Waitress (WSGI) запрещает его использовать в приложении (PEP 3333).
|
||||
|
||||
```python
|
||||
# ❌ НЕЛЬЗЯ
|
||||
response.headers["Connection"] = "close"
|
||||
|
||||
# Ошибка:
|
||||
AssertionError: Connection is a "hop-by-hop" header; it cannot be used by a WSGI application (see PEP 3333)
|
||||
```
|
||||
|
||||
## Правило
|
||||
|
||||
**Никогда не добавлять `Connection` заголовок в Flask/Waitress-приложении.**
|
||||
Нужно закрывать соединения — только через nginx (upstream keepalive) или uWSGI/gunicorn.
|
||||
@@ -0,0 +1,48 @@
|
||||
# SSE: отлов отключения клиента, v0.0.18
|
||||
|
||||
**Дата:** 2026-07-14
|
||||
**Версия:** 0.0.17 → 0.0.18
|
||||
|
||||
---
|
||||
|
||||
## Проблема
|
||||
|
||||
После 2-3 F5 во время обработки — загрузка падает с «Ошибка загрузки: Сеть».
|
||||
Причина: утечка потоков.
|
||||
|
||||
### Механика
|
||||
|
||||
1. `process_stream` создаёт SSE-генератор, занимающий поток Waitress (всего 8)
|
||||
2. `obfuscate_files()` выполняется ~30 сек (LLM-запросы)
|
||||
3. Пользователь делает F5 → TCP-соединение рвётся
|
||||
4. Генератор **не знает** что клиент отключился — продолжает обрабатывать файлы
|
||||
5. Поток занят, не освобождается
|
||||
6. 2-3 F5 → все 8 потоков заняты → новые запросы не принимаются → «Сеть»
|
||||
|
||||
### Почему WSGI не отключает
|
||||
|
||||
`yield` в генераторе пишет в буфер WSGI, а не в сокет. Буфер никуда не уходит
|
||||
(клиент отключён), но генератор об этом не узнаёт — `GeneratorExit` не кидается.
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
Добавлен `try/except GeneratorExit` после каждого `yield` в `generate()`:
|
||||
|
||||
```python
|
||||
try:
|
||||
yield f"event: done..."
|
||||
except GeneratorExit:
|
||||
return # клиент отключился, освобождаем поток, НЕ обрабатываем остальные файлы
|
||||
```
|
||||
|
||||
Waitress при отключении клиента кидает `GeneratorExit` при попытке записи в
|
||||
закрытый сокет. Генератор ловит → выходит → поток освобождается.
|
||||
|
||||
---
|
||||
|
||||
## Изменённые файлы
|
||||
|
||||
- `site/routes/api_bp.py` — `try/except GeneratorExit` в `generate()`
|
||||
- `site/app.py` — `VERSION = "0.0.18"`
|
||||
@@ -0,0 +1,60 @@
|
||||
# 2026-08-19 — Бенчмарк UI: 5 прогонов DownLoads.zip (v0.0.49)
|
||||
|
||||
Набор: `files/DownLoads.zip` (21.9 МБ) → распаковывается в 18 файлов
|
||||
(4 docx + 14 pdf, включая `0144-03-2023_отчет об оценке.pdf` 19.0 МБ).
|
||||
|
||||
Условия: сервер waitress, порт 5059, threads=8, локально.
|
||||
LLM-ключ отсутствует — этап ИИ не выполнялся (0с, в статистике «—»).
|
||||
Итог каждого прогона: «✅ Обработано 18 файлов», кнопки ZIP/CSV, блок статистики.
|
||||
|
||||
## Итоговые метрики (5 прогонов)
|
||||
|
||||
| Прогон | Распаковка zip, мс | Стена UI, с | SSE «общее», с |
|
||||
|--------|--------------------|-------------|----------------|
|
||||
| 1 | 855 | 50.3 | 49.0 |
|
||||
| 2 | 851 | 47.9 | 46.6 |
|
||||
| 3 | 860 | 47.1 | 45.9 |
|
||||
| 4 | 833 | 47.8 | 46.1 |
|
||||
| 5 | 850 | 47.9 | 46.7 |
|
||||
|
||||
- Распаковка: 833–860 мс (Δ 27 мс) — стабильно, ~0.85 с.
|
||||
- SSE «общее»: 45.9–49.0 с (Δ 3.1 с), среднее ≈ 46.9 с.
|
||||
- Стена UI: 47.1–50.3 с (загрузка локально ≈ 1 с).
|
||||
- SSE не рвётся: 5/5 прогонов до конца, по ~50 с каждый.
|
||||
|
||||
## Узкое место — 19 МБ PDF (0144-03-2023_отчет об оценке.pdf)
|
||||
|
||||
Время по прогонам: 25.8 / 24.5 / 26.0 / 25.0 / 25.8 с → стабильно ~25.4 с.
|
||||
Это ~53–55% от общего времени. Причина — pdfplumber extract_text.
|
||||
|
||||
## Время по файлам, среднее по 5 прогонам (с)
|
||||
|
||||
| Файл | Размер | Среднее |
|
||||
|------|--------|---------|
|
||||
| 20_ФИЯР_Порядок распределения…docx | 13.1 KB | 0.04 |
|
||||
| Proxy Voting Form 2026 EGM.docx | 16.1 KB | 0.1 |
|
||||
| допник-1-XXX002-01200_3.docx | 11.9 KB | 0.1 |
|
||||
| Экзаменационный материал…docx | 28.9 KB | 0.2 |
|
||||
| 0144-03-2023_отчет об оценке.pdf | 19.0 MB | 25.4 |
|
||||
| 20_ФИЯР_Регламент проведения ВИ МАГ_2026 (1).pdf | 167.2 KB | 1.1 |
|
||||
| 959399eab8f6a91df785f469b5851574.pdf | 328.7 KB | 1.5 |
|
||||
| EAC_979 419 6577.pdf | 314.1 KB | 0.3 |
|
||||
| examrules_test.pdf | 486.3 KB | 2.3 |
|
||||
| General Terms.pdf | 764.8 KB | 3.8 |
|
||||
| Marshrutnaya_kvitanciya.pdf | 291.1 KB | 0.6 |
|
||||
| P010028436 CERTIFICATE…pdf | 473.7 KB | 0.2 |
|
||||
| P010028436 POLICY…pdf | 623.1 KB | 0.5 |
|
||||
| PR00078013…pdf | 521.4 KB | 1.5 |
|
||||
| Transaction_Confirmation_en…pdf | 70.3 KB | 0.1 |
|
||||
| Лингвистика маг.pdf | 596.5 KB | 2.1 |
|
||||
| Отчет арбитражного управляющего…pdf | 267.4 KB | 3.5 |
|
||||
| ПРОГРАММА ВСТУПИТЕЛЬНЫХ ИСПЫТАНИЙ…pdf | 138.5 KB | 1.9 |
|
||||
|
||||
Сумма средних ≈ 45.3 с — сходится с SSE «общее».
|
||||
|
||||
## Стабильность
|
||||
|
||||
- Всё, кроме 19 МБ PDF, — стабильно: docx 0.0–0.2 с, PDF 0.1–4.3 с.
|
||||
- Погрешность повторов мала; основной вклад — один большой PDF.
|
||||
- Вывод: без LLM прогон 18 файлов (вкл. 19 МБ PDF) ≈ 46–49 с.
|
||||
С LLM добавится этап ИИ (в проде — ключ есть в кластере).
|
||||
@@ -0,0 +1,45 @@
|
||||
# drhider — тесты реальной обфускации и фикс .doc (2026-08-23, v0.0.60–0.0.61)
|
||||
|
||||
_Кластер `naeel-test-3`, v0.0.60 → v0.0.61. Корпус: `/mnt/y/T` (flat+many, 309 файлов pdf/doc/docx)._
|
||||
|
||||
## Проблема: .doc не конвертируется (v0.0.60)
|
||||
Симптом: `.doc`-файлы в ZIP с заглушкой
|
||||
`[DOC — conversion failed: libreoffice: failed to launch javaldx - java may not function correctly]`.
|
||||
|
||||
**Диагностика:**
|
||||
- `.doc` конвертирует сервис **liberta** (`CONVERT_SERVICE_URL` дефолт
|
||||
`https://liberta.containerk8s.dev.nubes.ru/convert`; в деплое не переопределён).
|
||||
- liberta **жива** (200 на `/` и `/health`), последовательный `/convert` — 200 (~3с).
|
||||
- **Под нагрузкой падает**: 5 параллельных `/convert` → 2-3 возвращают 500 `javaldx`
|
||||
(JVM/libreoffice не тянет конкурентность).
|
||||
- drhider конвертировал `.doc` **параллельно** (ThreadPoolExecutor max_workers=4 в `obfuscator.py`)
|
||||
→ бил liberta потоком → часть конверсий падала.
|
||||
|
||||
## Фикс: v0.0.61 — параллельность убрана
|
||||
- `obfuscator.py`: удалён ThreadPoolExecutor — `.doc` конвертируются **последовательно**.
|
||||
- `scanner.py`: `_LLM_CONCURRENCY = 1` — LLM-чанки **последовательно**.
|
||||
- Версия 0.0.60 → 0.0.61.
|
||||
|
||||
## Результаты после фикса (v0.0.61)
|
||||
|
||||
### Контрольный .doc (7 файлов)
|
||||
Все конвертируются по 2.4–3.9с (реальное время), **javaldx-ошибок нет**.
|
||||
Единственный FAIL — `mini_78b` (78 байт, битый файл) → «conversion produced no .docx» (не связано с параллельностью).
|
||||
|
||||
### Полный набор (30 файлов)
|
||||
upload=30, zip=25 (5 мини-PDF пустые), **convfail=1** (только mini_78b), mapping=86,
|
||||
tokens=61983, llm=79.9с, proc=118.9с. Сравнение с v0.0.60 (было 7 convfail) — регрессий нет.
|
||||
|
||||
### Долгий прогон (2×100 файлов)
|
||||
| Сессия | upload | zip | convfail | mapping | tokens | llm | всего |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| SET1 | 100 | 99 | 0 | 430 | 700474 | 985с | 1142.5с |
|
||||
| SET2 | 100 | 98 | 0 | 493 | 671170 | 841с | 972.1с |
|
||||
| Итого | 200 | 197 | **0** | 923 | ~1.37 млн | ~1826с | ~35 мин |
|
||||
|
||||
## Выводы
|
||||
1. `.doc`/liberta починено: **0 ошибок конверсии на 200 файлах**.
|
||||
2. Обфускация на объёме стабильна (923 замены, без регрессий).
|
||||
3. Цена «без параллельности» (по требованию): LLM-этап медленнее
|
||||
(100 файлов ≈ 16–19 мин; с параллельностью было бы ~4× быстрее).
|
||||
4. Файлы вне ZIP (2–3 шт) — битые/пустые PDF (нет текста), не ошибка.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Тест выбора папки v0.0.69 на проде — полный цикл (2026-08-24)
|
||||
|
||||
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, после редеплоя 18:31 (v0.0.69)._
|
||||
|
||||
## Что проверено (все ✓)
|
||||
1. **Выбор папки рекурсивно с подпапками** — папка `Клиенты/` с `Подпапка/Акт_работ.txt`.
|
||||
2. **Вложенный ZIP** — `Клиенты/архивы/документы.zip` раскрыт, файлы добавлены с префиксом пути:
|
||||
`архивы/Отчёт/Справка_Сидоров.txt`, `архивы/Счёт_на_оплату.txt`.
|
||||
3. **Кириллические имена** — сохраняются корректно.
|
||||
4. **Относительный путь** — в таблице и в результирующем ZIP.
|
||||
5. **Дедуп** — повторный `Договор_Ромашка.txt` добавлен один раз.
|
||||
6. **Полный цикл обфускации** (3 txt, 7.3с, ИИ 7с, токены 1858):
|
||||
- CSV-таблица замен: `+7 916 123-45-67 → +7_000_000_0001`, `Петрова Анна Сергеевна → Николаев_0001`,
|
||||
`Сидоров Пётр Николаевич → Павлов_0001`, `Козлова Мария Викторовна → Новиков_0001`, `Счёт 89 → Договор_0002`, `№45-А → Договор_0001`.
|
||||
- ZIP: имена с путями (`Подпапка/Акт_работ.md`, `архивы/Отчёт/Справка_Сидоров.md`, `архивы/Счёт_на_оплату.md`);
|
||||
содержимое обезличено («…Ответственный: Николаев_0001.»).
|
||||
7. **Лимиты** — файл 51 МБ помечен «🔥 не учитывается», сводка «1 учитываются + 1 свыше лимита».
|
||||
8. **Заморозка сессии** после обработки — выбор файлов/папок заблокирован до «Новая сессия».
|
||||
|
||||
## Найденный баг (косметика)
|
||||
Счётчик «Добавлено из папки: N» завышается на число дублей: `added++` выполняется безусловно
|
||||
после `addFileWithDedup`, даже когда файл не добавлен (дедуп). Пример: папка с одним дублем
|
||||
→ «5 файлов» при 4 реально добавленных. На список файлов не влияет.
|
||||
|
||||
## Примечания
|
||||
- Битый pdf-плейсхолдер (1 Б) не обработался (пропущен) — ожидаемо.
|
||||
- В ZIP текстовые `.txt` выходят как `.md` — поведение бэкенда, не связано с выбором папки.
|
||||
- Скачивание через событие `download` в Playwright не генерируется — данные получены прямым
|
||||
`fetch /api/download/<sid>` и `/api/csv/<sid>` (sid из перехвата URL).
|
||||
- Клик по «✕» в одном тике по двум кнопкам → `TypeError … 'name'` в `rm` — артефакт теста,
|
||||
в реальном использовании не воспроизводится (rr() пере-рендерит DOM синхронно).
|
||||
@@ -0,0 +1,30 @@
|
||||
# Тесты v0.0.67: загрузка, лимиты, ZIP-кириллица, ETA, прерывание (2026-08-24)
|
||||
|
||||
_Фон: найден и разобран неконсистентный деплой (см. infra/2026-08-24-two-clusters-deploy.md).
|
||||
Все тесты — на реальном внешнем бэкенде **iot-naeel (185.247.187.151), под v0.0.67**._
|
||||
|
||||
## 1. Загрузка: много + больших ✅
|
||||
- 119 файлов / **480.5 МБ** (8×45МБ + 10×10МБ + 100×200КБ).
|
||||
- PUT (P=8) 44.8с, `upload_refs` 51.9с, сессия: 119 файлов, 480.5МБ, 0 ошибок.
|
||||
|
||||
## 2. Лимит 50 МБ/файл ✅
|
||||
- big60 (60МБ) + small1 (1МБ) → `upload_refs` `{count:1}`; в сессии только `small1.bin`; big60 удалён с ВМ.
|
||||
- Раньше (старый под 0.0.61 в балансировке) big60 принимался — после чистки работает.
|
||||
|
||||
## 3. ZIP: кириллица без UTF-8-флага (v0.0.67) ✅
|
||||
- ZIP с `TKM_6й_Семестр_ЭКЗАМЕН.docx`, `Договор_ООО_Ромашка.pdf` (UTF-8, флаг сброшен) → в браузере имена декодированы **верно** (было `╨╣…`-мусор). Оценки времени видны.
|
||||
|
||||
## 4. Обфускация 20 файлов: ETA и SSE-события ✅
|
||||
- 21 файл (вкл. plan.txt): SSE 255с, `complete {total:21, tokens:104128, llm_sec:254.0}`.
|
||||
- События: `start=21, extract_done=1, file_start=21, file_chunk=21, file_done=21, done=21, complete=1`.
|
||||
- `llm`-heartbeat с `eta_sec`: 230 примеров, убывает 237→0 (адаптивная ETA работает).
|
||||
- ZIP: 21 файл (29.4КБ), CSV: 295 строк.
|
||||
|
||||
## 5. Прерывание с частичным сохранением ✅
|
||||
- 22 файла, через 25с `POST /api/cancel/<sid>` → `{"ok":true}`.
|
||||
- SSE `cancelled: {saved:3, total:22, tokens:11640, llm_sec:30.2}`.
|
||||
- **Частичный ZIP: 3 файла (4.4КБ), частичный CSV: 188 строк** — скачаны (HTTP 200).
|
||||
|
||||
## Вывод
|
||||
На консистентном v0.0.67 все новые фичи работают: лимиты, декодирование имён ZIP,
|
||||
ETA (global+per-file в SSE), прерывание с частичным результатом. Регрессий нет.
|
||||
@@ -0,0 +1,52 @@
|
||||
# drhider — тесты загрузки и обфускации (2026-08-24, v0.0.62)
|
||||
|
||||
_Кластер `naeel-test-3`, v0.0.62 (лимиты 50МБ/файл, 500МБ/сессия), под 1 CPU / 2 GiB
|
||||
(requests=limits, подтверждено kubectl). ВМ-буфер: `https://contracts.kube5s.ru/drhider-upload/`._
|
||||
|
||||
## Тест 1 — загрузка (upload-only), много + больших файлов
|
||||
|
||||
Корпус: 118 бинарных файлов, 480 МБ (8×45МБ + 10×10МБ + 100×200КБ). Файлы >40МБ — 8 шт.
|
||||
|
||||
| Этап | Результат |
|
||||
|---|---|
|
||||
| PUT на ВМ (P=8, `--resolve` обход DNS) | **119/119 HTTP 201**, 43.4с |
|
||||
| `POST /api/upload_refs` | HTTP 200, 50.8с, `{count:119, session}` |
|
||||
| `GET /api/session_files` | 119 файлов, **480.5 МБ**, 8 больших (>40МБ) |
|
||||
|
||||
**Вывод:** загрузка через ВМ стабильна на объёме 480МБ/119 файлов под лимитом 500МБ, 0 ошибок.
|
||||
|
||||
## Тест 2 — обфускация, много мелких файлов
|
||||
|
||||
Корпус: 100 сгенерированных `.txt`-договоров с фейковыми PII (ФИО, телефоны, ИНН, ОГРН, БИК,
|
||||
р/с, к/с, паспорта, адреса, email). Сессия `bba1a7272aed`.
|
||||
|
||||
| Показатель | Значение |
|
||||
|---|---|
|
||||
| start / done | **101 / 101** (0 ошибок) |
|
||||
| complete | `{total: 101, tokens: 1 569 716, llm_sec: 1924.6}` |
|
||||
| SSE-время | 1995.6с (~33.3 мин) |
|
||||
| ZIP | **404 — НЕ скачался** |
|
||||
| CSV | **404 — НЕ скачался** |
|
||||
|
||||
Обфускация отработала полностью (101 done, complete пришёл, `zip_len=147861` в логе worker),
|
||||
но **результат потерян при скачивании** — см. баг ниже.
|
||||
|
||||
## ⚠️ Найденный баг: TTL сессии (30 мин) короче долгой обфускации
|
||||
|
||||
- `site/session.py`: `TTL_SECONDS = 30*60`, таймер запускается при создании сессии и
|
||||
**не продлевается** во время обработки (`threading.Timer`, `_clean` просто `pop(sid)`).
|
||||
- Обфускация заняла 1995с (> 1800с TTL) → таймер удалил сессию посреди обработки.
|
||||
- `store_result(sid, zip)` при отсутствующей сессии молча возвращает `False` → результат не сохранён.
|
||||
- `GET /api/download/<sid>` и `/api/csv/<sid>` → `get_result`=None → **404**.
|
||||
- Подтверждено: `session_files/bba1a7272aed` → 404; лог: `worker: done ... in 1995.0s ... zip_len=147861`.
|
||||
|
||||
**Почему раньше не всплыло:** в тесте 23.08 каждая сессия завершалась за 19/16 мин < TTL.
|
||||
|
||||
**Предлагаемый фикс:** продлевать TTL при активности обработки (cancel+restart таймера на
|
||||
каждое событие прогресса, либо отдельный длинный TTL для «в обработке», либо сохранять
|
||||
результат даже при истёкшей сессии). **Не делалось — ждёт «делай».**
|
||||
|
||||
## Прочее
|
||||
- Первый прогон теста 1 упал из-за бага тест-скрипта (отправил список вместо `{files:[...]}` в
|
||||
`upload_refs`) — сервис тут ни при чём.
|
||||
- DNS на Krupski медленный (~5с/резолв) — в тестах обходился `curl --resolve`.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Массовое тестирование v0.0.72 на проде — 4 блока (2026-08-24)
|
||||
|
||||
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, редеплой 19:25 (v0.0.72)._
|
||||
|
||||
## Блок 1 — выбор папки (всё ✓)
|
||||
- Рекурсия + относительный путь: `Подпапка/Акт1.txt`.
|
||||
- Вложенный ZIP с кириллицей: `архивы/док.zip` → `архивы/Отчёт/Справка_Сидорова.txt`, `архивы/Счёт_на_оплату.txt` (путь zip + путь внутри).
|
||||
- Дедуп: дубль `Договор1.txt` отсеян → «Добавлено из папки: 4 файла» (было бы 5).
|
||||
- Не-документ `.bin` (60 МБ) — пропущен.
|
||||
|
||||
## Блок 2 — полный цикл (всё ✓)
|
||||
- 4 файла: «Обработано 4 файлов: 9.9с, ИИ 9.7с, токены 2508».
|
||||
- CSV замен (152 Б): `+7 111. → +7_000_000_0001`, `Ивановым И.И. → Семёнов_0001`,
|
||||
`Петрова Анна → Николаев_0001`, `Сидорова → Семёнов_0002`.
|
||||
- ZIP (775 Б) с путями: `Договор1.md`, `Подпапка/Акт1.md`, `архивы/Отчёт/Справка_Сидорова.md`, `архивы/Счёт_на_оплату.md`.
|
||||
- Содержимое обезличено: «Договор с Семёнов_0001, тел +7_000_000_0001» и т.д.
|
||||
|
||||
## Блок 3 — прерывание (всё ✓)
|
||||
- 9 txt, прервано на 9-й секунде: «⏹ Остановлено. Сохранено 4 из 9 файлов + таблица замен (6.5с, токены 3183)».
|
||||
- Частичный ZIP — только 4 готовых: `файл_3.md, файл_4.md, файл_5.md, файл_7.md`.
|
||||
- CSV замен сохранён (1149 Б). Кнопки скачивания видны.
|
||||
|
||||
## Блок 4 — заморозка/UI (всё ✓)
|
||||
- После прерывания: `fi.disabled`, `ub.disabled`, «Новая сессия» видна (заморозка).
|
||||
- HELP открывает модалку (display:flex).
|
||||
- «Новая сессия»: список очищен, статус пуст, dl-кнопки скрыты.
|
||||
|
||||
## Найденный мелкий баг
|
||||
- После «Новой сессии» кнопка «Обфусцировать» активна при 0 файлов
|
||||
(`resetAll()`: `rr()` ставит `ub.disabled=cntMain===0` (true), затем `ub.disabled=false` перекрывает).
|
||||
Нажатие безопасно (`uploadFiles` → `if(sf.length===0) return`), но кнопка выглядит активной.
|
||||
Фикс — 1 строка (не перекрывать после `rr()`). Открыто, ждёт решения.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Тест v0.0.72 на проде — статусы, счётчик, полный цикл (2026-08-24)
|
||||
|
||||
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, редеплой 19:25 (v0.0.72)._
|
||||
|
||||
## Сценарий
|
||||
Папка: битый PDF (scan1.pdf) + 3 txt + файл 51 МБ (Big.txt).
|
||||
|
||||
## Результаты (все ✓)
|
||||
1. **Счётчик дедупа (v0.0.72)** — папка с дублем [Договор.txt, Договор.txt, Акт.txt]
|
||||
→ «✅ Добавлено из папки: 3 файла» (дубль отсеян, раньше было бы 4).
|
||||
2. **Статус «не извлечён» (v0.0.71)** — во время обработки битый `scan1.pdf` в группе
|
||||
«✓ Обработанные (1)» со статусом «не извлечён» (раньше было «пропущен»).
|
||||
3. **Статус «пропущен (лимит)» (v0.0.71)** — `Big.txt 51.0 MB` в отдельной группе
|
||||
«⛔ Пропущены (сверх лимита)» (раньше попадал в «Ожидают обработки»).
|
||||
4. **Полный цикл** — «✅ Обработано 3 файлов: 5.6с, ИИ 5.4с» (3 txt), битый/большой исключены.
|
||||
5. **HELP + текст ограничений** (v0.0.70) — на месте.
|
||||
6. **Ретраи pull (v0.0.72)** — загрузка прошла штатно (код на бэке, косвенная проверка).
|
||||
|
||||
## Замечания
|
||||
- Статусы «не извлечён»/«пропущен (лимит)» видны ТОЛЬКО во время обработки
|
||||
(3-секционная таблица); после завершения `rr()` рисует обычную таблицу.
|
||||
- Страница приведена в чистое состояние («Новая сессия» → «Нет выбранных файлов»).
|
||||
@@ -0,0 +1,20 @@
|
||||
# Тест стабильности v0.0.75 на проде (2026-08-24)
|
||||
|
||||
_tests. Прод, редеплой 20:20 (v0.0.75). Комплексный сценарий после всех фиксов._
|
||||
|
||||
## Сценарий
|
||||
Папка: битый PDF (scan.pdf) + txt + txt-дубль + Big.bin (51 МБ, не документ) + zip с документом (справка.txt).
|
||||
|
||||
## Результаты (все ✓)
|
||||
1. **Состав**: «Добавлено из папки: 3 файла» — дубль отсеян, `.bin` пропущен,
|
||||
zip раскрыт (справка.txt). Таблица: scan.pdf, Договор.txt, справка.txt.
|
||||
2. **Полный цикл**: «Обработано 2 файлов: 4.9с, ИИ 4.7с» — битый PDF исключён.
|
||||
3. **CSV**: глобально консистентные замены: `+7 916 111-22-33 → +7_000_000_0001`,
|
||||
`+7 900 111-22-33 → +7_000_000_0002`, `Иванов Иван → Соколов_0001`, `Кузнецов Олег → Михайлов_0001`.
|
||||
4. **ZIP**: `Договор.md`, `справка.md` (битый не попал).
|
||||
5. **Кнопка**: disabled при 0 файлов → активна с файлом → disabled после «Новой сессии».
|
||||
6. **Фильтр zip** (v0.0.75): не-документы не раскрываются, документ проходит.
|
||||
|
||||
## Итог
|
||||
v0.0.75 стабильна: все фиксы (v0.0.70–0.0.75) работают совместно, регрессов нет.
|
||||
Страница приведена в чистое состояние.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Диагностика медленной/нестабильной загрузки + v0.0.43 — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.42 → 0.0.43
|
||||
|
||||
---
|
||||
|
||||
## Проблема
|
||||
Юзер: «Таймаут 30с» при загрузке 19 МБ PDF в drhider. Раньше (11с) грузилось.
|
||||
|
||||
## Диагностика (факты)
|
||||
|
||||
### Таймаут 30с
|
||||
- Введён 14.07.2026 (коммит `b6dac6f`, «не висеть бесконечно»), НЕ мой код.
|
||||
- 19 МБ / 30с = 0.63 МБ/с — при медленном/нестабильном канале не хватает.
|
||||
|
||||
### Замеры загрузки с ВМ (внешний путь)
|
||||
- 1 МБ → 0.16с (быстро)
|
||||
- 5 МБ → 51.6с (медленно)
|
||||
- 10 МБ → 52.4с (медленно)
|
||||
- Повтор 10 МБ ×4: **1.07с / 52.3с / 52.3с / 52.3с** — НЕСТАБИЛЬНО, чаще 52с.
|
||||
- Повтор 1 МБ: то 51с, то 0.5с — задержка НЕ зависит от размера.
|
||||
|
||||
### Тайминг (10 МБ)
|
||||
- `connect=0.046с`, `starttransfer=0.09с`, `total=52.5с`.
|
||||
- Сервер отвечает быстро (starttransfer), но соединение «висит» ~52с после ответа.
|
||||
- `Connection: close` — НЕ помогает (то же 52с).
|
||||
|
||||
### Сервер
|
||||
- Из loadtest: внутри кластера 10 МБ = 0.2с — сервер/Flask быстрые.
|
||||
- ClusterIP с ВМ недоступен (ВМ вне кластера) — проверить напрямую не вышло.
|
||||
|
||||
## Применённые аннотации ingress (НЕ помогли)
|
||||
Добавлены на ingress `pythonk8s` (ns 20a75175...):
|
||||
```yaml
|
||||
nginx.ingress.kubernetes.io/proxy-request-buffering: "false"
|
||||
nginx.ingress.kubernetes.io/client-body-buffer-size: "1024m"
|
||||
```
|
||||
- `kubectl patch` — применено, nginx reload (75с).
|
||||
- В конфиге: `client_body_buffer_size 1024m` — применилось.
|
||||
- `proxy_request_buffering on` — НЕ применилось (аннотация не подхватилась shturval-контроллером).
|
||||
- Замер после: 5 МБ = 51.7с, 19 МБ = 53.8с — эффекта НЕТ.
|
||||
|
||||
## Вывод
|
||||
- Задержка ~50с НЕ в коде drhider, НЕ в буферизации nginx, НЕ зависит от размера,
|
||||
НЕСТАБИЛЬНА (то 0.5с, то 52с). Вероятно: внешний путь шлюз/балансировщик/сеть
|
||||
(потеря пакетов / TCP RTO ~50с, или периодическая задержка шлюза).
|
||||
- Совпадает с диагнозом loadtest: «на iot-naeel шлюз держит ~50с».
|
||||
|
||||
## Решение (v0.0.43) — защита от ошибки юзера
|
||||
- `xhr.timeout`: 30000 → **300000 (300с)** — при нестабильной загрузке файл
|
||||
догрузится (медленно, но без «Таймаут 30с»).
|
||||
- Сообщение: «Таймаут 30с» → «Таймаут 300с».
|
||||
|
||||
## Осталось
|
||||
- Аннотации на ingress оставлены (`client-body-buffer-size 1024m` не вредит;
|
||||
`proxy-request-buffering` не работает на shturval — искать правильный способ).
|
||||
- Корень задержки ~50с на внешнем пути — НЕ найден, требует сетевой диагностики
|
||||
(tcpdump, проверка балансировщика/шлюза платформы).
|
||||
@@ -0,0 +1,106 @@
|
||||
# ПЛАН: тест-режим «только загрузка» (для Флэша)
|
||||
|
||||
_Дата: 2026-08-23. Цель: при нажатии «Обфусцировать» — только загрузить файлы
|
||||
(браузер→ВМ→Flask), НЕ запускать обработку (SSE/LLM/обфускацию)._
|
||||
|
||||
## Суть
|
||||
|
||||
Добавить флаг тест-режима в фронтенд. Когда он включён, `uploadFiles()` проходит
|
||||
Фазу 1 полностью (PUT на ВМ + `POST /api/upload_refs` — pull в сессию), а потом
|
||||
**останавливается до Фазы 2** (не открывает EventSource, не запускает обработку).
|
||||
|
||||
Плюс опциональный отладочный endpoint на бэке — проверить, что файлы реально
|
||||
легли в сессию с корректными размерами.
|
||||
|
||||
---
|
||||
|
||||
## Изменение 1 — флаг (site/templates/index.html)
|
||||
|
||||
В начало скрипта, рядом с `VM_UPLOAD_URL` (строка ~201), добавить:
|
||||
|
||||
```js
|
||||
// Тест-режим: открыть страницу с ?upload-only=1 → только загрузка, без обработки
|
||||
const TEST_UPLOAD_ONLY = new URLSearchParams(location.search).get('upload-only') === '1';
|
||||
```
|
||||
|
||||
## Изменение 2 — ранний выход после загрузки (site/templates/index.html)
|
||||
|
||||
В `uploadFiles()`, сразу ПОСЛЕ блока «Шаг 1b» (try/catch `POST /api/upload_refs`)
|
||||
и ПЕРЕД комментарием `// Фаза 2: обработка (SSE — прогресс по каждому файлу)`.
|
||||
|
||||
При этом в шаге 1b зафиксировать число загруженных файлов. Сейчас там:
|
||||
|
||||
```js
|
||||
const data = await resp.json();
|
||||
if (!data.ok) throw new Error(data.error || 'HTTP ' + resp.status);
|
||||
currentSid = data.session;
|
||||
```
|
||||
|
||||
Запомнить count:
|
||||
|
||||
```js
|
||||
const data = await resp.json();
|
||||
if (!data.ok) throw new Error(data.error || 'HTTP ' + resp.status);
|
||||
currentSid = data.session;
|
||||
uploadedCount = data.count || refs.length; // сколько реально легло в сессию
|
||||
```
|
||||
|
||||
(объявить `let uploadedCount = 0;` рядом с `const refs = [];` в начале Фазы 1).
|
||||
|
||||
Сразу после `catch` шага 1b вставить:
|
||||
|
||||
```js
|
||||
if (TEST_UPLOAD_ONLY) {
|
||||
st.className = 'status done';
|
||||
st.textContent = '✅ Загружено ' + uploadedCount + ' файлов (тест: обработка пропущена). Сессия: ' + currentSid;
|
||||
ub.disabled = false;
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
То есть: показываем итог, снова включаем кнопку, выходим из функции. Фаза 2 не выполняется.
|
||||
|
||||
## Изменение 3 (опционально, рекомендуется) — debug-endpoint (site/routes/api_bp.py)
|
||||
|
||||
Чтобы проверить размеры файлов в сессии (не повредились ли при pull), добавить:
|
||||
|
||||
```python
|
||||
@api_bp.route("/session_files/<sid>", methods=["GET"])
|
||||
def session_files(sid):
|
||||
"""Отладка: список файлов сессии с размерами."""
|
||||
files = get_files(sid)
|
||||
if files is None:
|
||||
return jsonify({"ok": False, "error": "Session not found"}), 404
|
||||
return jsonify({
|
||||
"ok": True, "count": len(files),
|
||||
"files": [{"name": n, "size": len(b)} for n, b in files],
|
||||
})
|
||||
```
|
||||
|
||||
`get_files` уже импортирован в `api_bp.py`. Endpoint временный — убрать после теста.
|
||||
|
||||
---
|
||||
|
||||
## Как тестировать
|
||||
|
||||
1. Поднять сервис (локально `cd site && python app.py` или redeploy на Штурвале).
|
||||
2. Открыть `https://drhider.pythonk8s.dev.nubes.ru/?upload-only=1` (или `http://127.0.0.1:5000/?upload-only=1`).
|
||||
3. Выбрать файлы (в т.ч. большой >1МБ), нажать «Обфусцировать».
|
||||
4. Ожидать: прогресс «Загрузка на ВМ…» по каждому файлу → «Передача ссылок…» →
|
||||
«✅ Загружено N файлов (тест: обработка пропущена)». SSE НЕ открывается.
|
||||
5. (Если сделан п.3) в консоли/curl: `curl /api/session_files/<sid>` → сверить `size` с оригиналом.
|
||||
6. Проверить на ВМ: после pull файлы удалены (`ls /var/www/drhider-upload/` пусто).
|
||||
|
||||
## Как вернуть обратно (после теста)
|
||||
|
||||
- Флаг `TEST_UPLOAD_ONLY` без `?upload-only=1` даёт обычное поведение — **ничего выпиливать не надо**,
|
||||
режим включается только query-параметром.
|
||||
- Debug-endpoint (п.3) — убрать, когда тест завершён (или оставить под комментарием «отладка»).
|
||||
|
||||
## Замечания
|
||||
|
||||
- НЕ трогать бэк-логику pull (`/api/upload_refs`) — она уже работает (v0.0.58).
|
||||
- НЕ менять Фазу 2 (SSE) — только добавить ранний `return` до неё.
|
||||
- После правки: `python3 -c "import py_compile; py_compile.compile('site/routes/api_bp.py', doraise=True)"`,
|
||||
JS проверить `node --check` (извлечь блок `<script>` из index.html).
|
||||
- Версию `VERSION` в `site/app.py` НЕ поднимать, пока это тестовый режим — либо поднять, если деплоим.
|
||||
@@ -0,0 +1,55 @@
|
||||
# drhider — тесты загрузки и стресс (2026-08-23, v0.0.59–0.0.60)
|
||||
|
||||
_Платформа: Штурвал, кластер `naeel-test-3` (kubeconfig `~/.kube/config-naeel-test-3` на ВМ).
|
||||
ВМ-буфер: `https://contracts.kube5s.ru/drhider-upload/` (nginx dav). Тест-режим: `?upload-only=1`._
|
||||
|
||||
## Изменения за день
|
||||
- **v0.0.58** — ВМ-загрузка: PUT на ВМ + `/api/upload_refs` (egress pull) + тест-режим `?upload-only=1`.
|
||||
- **v0.0.59** — убран лимит 100 МБ/файл (фронт; остался суммарный 1 ГБ).
|
||||
- **v0.0.60** — дедуп по **имя+размер** (дату игнорируем), при совпадении остаётся **более свежий**.
|
||||
|
||||
## Edge-case тесты (v0.0.59, браузер/Playwright)
|
||||
- Нулевой размер (`empty.txt` 0 B) — принимается, грузится (0 B/s), в сессии size=0. ✅
|
||||
- Бинарный файл (`bin.dat`) — принимается, грузится. ✅
|
||||
- Одинаковое имя, разный размер — суффикс `_2`. ✅
|
||||
- Одинаковые имя+размер, разная дата — **дедуп** (было `_2`, с v0.0.60 — один, свежий). ✅
|
||||
- Один файл в архиве и отдельно — **дедуп** (было `_2` из-за mtime, с v0.0.60 — один). ✅
|
||||
|
||||
## Стресс-тест загрузки (v0.0.60)
|
||||
Под: `pythonk8s-8dbc866bb-d84p6`, requests/limits **cpu 2 / memory 4Gi**.
|
||||
Путь: PUT на ВМ → `POST /api/upload_refs` (один JSON со всеми refs) → `GET /api/session_files`.
|
||||
|
||||
### Сессии (11 успешных + 1 OOM)
|
||||
| Сес. | Файлов | Объём | PUT | refs |
|
||||
|---|---|---|---|---|
|
||||
| A | 120 | 364МБ | 34.4с | 37.8с |
|
||||
| B | 120 | 316МБ | 29.9с | 33.3с |
|
||||
| C | 1×250МБ | 250МБ | 23.2с | 23.9с |
|
||||
| D | 120 | 378МБ | 40.0с | 41.6с |
|
||||
| E | 120 | 301МБ | 26.7с | 31.5с |
|
||||
| F | 1×200МБ | 200МБ | 18.9с | 33.8с |
|
||||
| G | 120 | 360МБ | 34.8с | 36.4с |
|
||||
| H | 120 | 309МБ | 29.0с | 38.4с |
|
||||
| I | 1×250МБ | 250МБ | 23.2с | 24.1с |
|
||||
| J | 120 | 357МБ | 33.4с | 37.9с |
|
||||
| K | 120 | 366МБ | 34.3с | 40.1с |
|
||||
| **L** | 120 | 369МБ | — | **FAIL (OOM)** |
|
||||
| M | 120 | 347МБ | 32.5с | 36.0с (после рестарта) |
|
||||
| N | 120 | 358МБ | 33.1с | 37.5с (после рестарта) |
|
||||
|
||||
### Память пода (рост линейный, ~1.2× объёма сессий)
|
||||
55Mi (база) → 744Mi (3 сес.) → 1991Mi (6 сес.) → 3235Mi (9 сес., ~80% лимита) → **OOMKilled на L**.
|
||||
|
||||
### Итог OOM
|
||||
- `restarts=1, lastReason=OOMKilled, exitCode=137` (лимит 4Gi).
|
||||
- После авто-рестарта под работает, загрузка продолжается (M, N ok).
|
||||
- **Все сессии A–L потеряны** (in-memory хранилище, TTL 30 мин, без персиста).
|
||||
|
||||
## Выводы / факты
|
||||
1. Загрузка через ВМ стабильна на больших объёмах: до OOM — 11 сессий ~3.3ГБ, 0 ошибок, скорость не деградирует.
|
||||
2. Потолок памяти = лимит пода 4Gi; OOM при ~3.5ГБ резидентных сессий.
|
||||
3. Апп переживает OOM (рестарт + продолжение работы), но сессии теряются.
|
||||
4. Код: `session.py MAX_SESSION_BYTES = 500МБ` (на сессию) — OOM реален только при ~8+ одновременных сессий.
|
||||
5. `upload_refs` при переполнении сессии отвечает 404 «Session not found» (вводит в заблуждение — это лимит размера).
|
||||
6. ГРАБЛЯ: на Krupski `dd` алиасится на `docker compose down`; прокси `172.17.192.1:10808` — тесты только `--noproxy '*'`/`NO_PROXY`.
|
||||
7. Браузер-автоматизация (Playwright) видит Windows-ФС (`C:\tmp\...`) — тестовые файлы класть в `/mnt/c/tmp/` (WSL).
|
||||
@@ -0,0 +1,36 @@
|
||||
# 2026-08-24 — Лимиты: 50 МБ на файл, 500 МБ на сессию (v0.0.62)
|
||||
|
||||
## Контекст
|
||||
- После переезда на naeel-test-3 ресурсы инстанса уменьшены до 1 CPU / 2048 MiB
|
||||
(Штурвал, подтверждено kubectl: requests=limits 1CPU/2Gi).
|
||||
- При 2Gi памяти лимит 1ГБ/сессия (фронт) + отсутствие лимита на файл стали опасны
|
||||
(OOM-риск, сессии in-memory и теряются при рестарте).
|
||||
- Пользователь: вернуть лимит — один файл 50 МБ, всего 500 МБ, и показывать лимиты во фронте.
|
||||
|
||||
## Изменения
|
||||
Фронт (site/templates/index.html):
|
||||
- `MAX_FILE_BYTES = 50МБ`, `MAX_SESSION_BYTES = 500МБ` (было 1ГБ, лимит на файл был убран).
|
||||
- `addFileWithDedup`: файл сверх 50 МБ → `overNames` («не учитывается», красный, не обфусцируется);
|
||||
суммарный лимит 500 МБ — как раньше.
|
||||
- Шапка: «Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ...».
|
||||
|
||||
Бэк:
|
||||
- `site/session.py`: добавлен `MAX_FILE_BYTES = 50МБ` (рядом с `MAX_SESSION_BYTES = 500МБ`).
|
||||
- `site/routes/api_bp.py` (защита):
|
||||
- `POST /api/upload`: файл >50 МБ пропускается (log, continue).
|
||||
- `POST /api/upload_refs`: ref с size>50МБ пропускается + DELETE с ВМ;
|
||||
после pull проверяется фактический len(content)>50МБ → пропуск + DELETE.
|
||||
- `site/app.py`: `MAX_CONTENT_LENGTH` 200МБ → 50МБ (защита памяти, согласовано с лимитом файла).
|
||||
|
||||
Версия: 0.0.61 → 0.0.62.
|
||||
|
||||
## Проверка
|
||||
- py_compile app.py / session.py / api_bp.py — OK.
|
||||
- node --check (извлечённый <script> index.html) — OK.
|
||||
- get_errors — нет.
|
||||
|
||||
## Прочее (диагностика 5с)
|
||||
- Медленная загрузка страницы ~5с — НЕ кластер/приложение: `time_namelookup=5.15с`
|
||||
(медленный локальный резолвер: 8.8.8.8 первым в /etc/resolv.conf недоступен, фолбэк на 10.96.x.x).
|
||||
Тот же паттерн на ВМ (5.4с). drhider отвечает за ~0.03с после соединения.
|
||||
- Kubeconfig naeel-test-3 обновлён на ВМ (~/.kube/config-naeel-test-3), kubectl работает.
|
||||
@@ -0,0 +1,40 @@
|
||||
# v0.0.41 — время на конкретный файл в колонке Статус — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.40 → 0.0.41
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
В колонке «Статус» таблицы у всех файлов показывалось ~одинаковое ОБЩЕЕ время.
|
||||
Причина: фронт считал время как разницу `start`→`done`, а LLM — общий для всех
|
||||
файлов (в проходе 1) — попадал в интервал КАЖДОГО файла.
|
||||
|
||||
Теперь бэк считает время на КОНКРЕТНЫЙ файл (extract_text + regex + замена,
|
||||
БЕЗ общего LLM) и передаёт его в `done`-событии.
|
||||
|
||||
## Изменения
|
||||
|
||||
### `drhider/obfuscator.py`
|
||||
- `import time` добавлен.
|
||||
- Заведён `file_times = [0.0] * total`.
|
||||
- Проход 1: `file_times[i] += time.time() - t0` вокруг extract_text + regex.
|
||||
- Проход 2: `file_times[i] += time.time() - t0` вокруг замены.
|
||||
- `progress_cb` расширен до `(phase, idx, total, fname, elapsed)`; в `done`
|
||||
передаётся `round(file_times[i], 2)`.
|
||||
|
||||
### `site/routes/api_bp.py`
|
||||
- `progress(phase, idx, total_, name, elapsed)`; в SSE `done` добавлен `elapsed`.
|
||||
|
||||
### `site/templates/index.html`
|
||||
- В `done`-обработчике: если `d.elapsed > 0` — показать `d.elapsed.toFixed(1)`
|
||||
(время файла от бэка), иначе fallback на старое вычисление.
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.41"`.
|
||||
|
||||
## Проверка
|
||||
- События: `[start(0,0.0), start(1,0.0), done(0,<время>), done(1,<время>)]`.
|
||||
- `py_compile`, `node --check` — OK.
|
||||
- Тесты: builder 20/20, replacer 10/10, scanner 20/20, zip 28/28.
|
||||
@@ -0,0 +1,39 @@
|
||||
# v0.0.40 — имя файла в live-блоке + сброс итогов — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.39 → 0.0.40
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Два UX-фикса во время обработки:
|
||||
|
||||
1. **Имя текущего файла в live-блоке.** Раньше `start`-событие (с именем файла)
|
||||
слалось только в **проходе 2** (замена), который идёт ПОСЛЕ LLM. А LLM — в
|
||||
проходе 1 и самый долгий. Поэтому во время LLM висело «Подготовка…» вместо
|
||||
имени файла.
|
||||
|
||||
2. **Сброс старых «Итогов» при повторной обфускации.** Раньше блок
|
||||
«📊 Итоги обработки» не скрывался при новом запуске — старые итоги висели
|
||||
во время новой обработки.
|
||||
|
||||
## Изменения
|
||||
|
||||
### `drhider/obfuscator.py`
|
||||
- `progress_cb("start", i, total, name)` перенесён из прохода 2 в **проход 1**
|
||||
(перед `extract_text` каждого файла).
|
||||
- В проходе 2 остался только `progress_cb("done", ...)`.
|
||||
- Порядок событий: `start`×N (extract_text) → `done`×N (замена).
|
||||
|
||||
### `site/templates/index.html`
|
||||
- В начале `uploadFiles()` добавлено скрытие `#statsBlock` и `#liveBlock`
|
||||
(как уже скрывается `db`).
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.40"`.
|
||||
|
||||
## Проверка
|
||||
- Порядок событий: `[start(0), start(1), done(0), done(1)]` — корректно.
|
||||
- `node --check`, `py_compile` — OK.
|
||||
- Тесты: builder 20/20, replacer 10/10, scanner 20/20, zip 28/28.
|
||||
@@ -0,0 +1,55 @@
|
||||
# v0.0.38 — живой таймер обработки + индикация ИИ — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.37 → 0.0.38
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Юзер должен видеть, что обработка идёт (не зависла), особенно пока LLM
|
||||
«переваривает» большой файл. Добавлены:
|
||||
- крупный live-блок «⏱ Обработка…» с тикающим таймером общего времени;
|
||||
- строка текущего файла с размером («Файл 3/5: имя (5.0 МБ)»);
|
||||
- индикация ИИ: «🤖 ИИ обрабатывает… Nс» (от реальных данных с бэка).
|
||||
|
||||
Итоговая формулировка времени стала ясной: «…общее Xс, из них ИИ Yс».
|
||||
|
||||
## Изменения
|
||||
|
||||
### `drhider/llm_client.py`
|
||||
- В `__init__` добавлены `llm_active` (bool) и `llm_started` (float).
|
||||
- Метод `llm_elapsed_now()` — секунд с начала текущего LLM-вызова (0 если не активен).
|
||||
- `complete()`: ставит `llm_active=True`/`llm_started=time.time()` перед HTTP,
|
||||
сбрасывает `llm_active=False` в `finally`. `import time` добавлен.
|
||||
|
||||
### `site/routes/api_bp.py`
|
||||
- В ветке `except queue.Empty` (пока воркер занят) — heartbeat-событие SSE:
|
||||
`event: llm` → `{active, elapsed, tokens}` (раз в ~1с, timeout=1).
|
||||
|
||||
### `site/templates/index.html`
|
||||
- CSS для `.live-block`, `.live-title`, `.live-file`, `.live-timer` (40px), `.live-llm`.
|
||||
- HTML: блок `#liveBlock` с `#liveFile`, `#liveTimer`, `#liveLlm`/`#liveLlmTime`;
|
||||
в итоговом блоке подпись «Время ИИ» → «Из них ИИ».
|
||||
- JS (фаза 2):
|
||||
- показ `#liveBlock`, интервал `liveRefresh` (200мс) — тикает `#liveTimer`;
|
||||
- обработчик `start` — в `#liveFile` имя текущего файла + размер из `sf[idx]`;
|
||||
- обработчик `llm` — показывает/скрывает `#liveLlm` и обновляет `#liveLlmTime`;
|
||||
- `complete`/`onerror`/`catch` — скрыть `#liveBlock`, очистить интервалы;
|
||||
- итоговый текст: «общее Xс, из них ИИ Yс».
|
||||
- `resetAll` — скрывает `#liveBlock`.
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.38"`.
|
||||
|
||||
## Проверка
|
||||
- `node --check`, `py_compile` — OK.
|
||||
- `LLMClient`: во время вызова `llm_active=True`, `llm_elapsed_now()≈0.3с`; после —
|
||||
`False`; накопление токенов и времени — ок.
|
||||
- SSE end-to-end: `complete` содержит `{total, tokens, llm_sec}`. Heartbeat `llm`
|
||||
генерируется в ветке `except queue.Empty` при долгой обработке (локально без
|
||||
LLM обработка мгновенная, поэтому heartbeat не успевает — в проде при долгой
|
||||
LLM-обработке будет слаться раз в сек).
|
||||
|
||||
## Примечание
|
||||
Бэкенд метрики не менял (только флаг/метод для live-таймера + heartbeat).
|
||||
@@ -0,0 +1,39 @@
|
||||
# v0.0.49 — индикатор распаковки, разделение фаз, таймауты SSE — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.48 → 0.0.49
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
Отзыв тестировщика:
|
||||
1. «После Выбрать файлы долго не респондит — разбирает архивы» → нет индикатора.
|
||||
2. «По Обфусцировать — двойной проход по файлам» → фазы загрузки и обработки не разделены.
|
||||
3. «Ошибка: SSE connection failed» → долгое SSE рвалось шлюзом.
|
||||
|
||||
## Изменения
|
||||
|
||||
### Ingress (kubectl patch, не код)
|
||||
- `proxy-read-timeout: 600 → 1800`
|
||||
- `proxy-send-timeout: 600 → 1800`
|
||||
— шлюз больше не рвёт долгое SSE (было ~10 мин, стало 30 мин).
|
||||
|
||||
### `site/templates/index.html`
|
||||
- **Индикатор распаковки**: при выборе файлов с `.zip` — `cursor: wait` +
|
||||
статус «Разбираю архивы…», снимается после распаковки (try/finally).
|
||||
- **Разделение фаз**:
|
||||
- Фаза 1: статус «Загрузка (этап 1/2) i/N: имя».
|
||||
- Фаза 2: статус «Обработка (этап 2/2)…», таймер «Обработка (этап 2/2)… Nс».
|
||||
- При старте фазы 2 загрузочные статусы сбрасываются на «⏳».
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.49"`.
|
||||
|
||||
## Проверка
|
||||
- `node --check`, `py_compile` — OK.
|
||||
- Тесты builder/replacer/scanner/zip — все зелёные.
|
||||
|
||||
## Примечание
|
||||
- Причина «SSE connection failed»: шлюз рвал долгое SSE (proxy-read-timeout 600с).
|
||||
Таймауты подняты до 1800с. Если обработка > 30 мин — нужно ещё выше или
|
||||
авто-реконнект EventSource (отложено: реконнект вызывает повторную обработку).
|
||||
@@ -0,0 +1,52 @@
|
||||
# v0.0.33 — раскрытие ZIP в таблицу (фронтенд) — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.32 → 0.0.33
|
||||
**Файл:** `site/templates/index.html` (фронтенд), `site/app.py` (версия)
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Раньше выбранный `.zip` попадал в таблицу одной строкой и не раскрывался.
|
||||
Теперь при выборе файлов ZIP **распаковывается на клиенте**, файлы из архива
|
||||
попадают в таблицу **по отдельности** (полный относительный путь).
|
||||
|
||||
## Что добавлено (`index.html`, в тот же `<script>`)
|
||||
|
||||
1. **Нативный распаковщик ZIP** без внешних библиотек:
|
||||
- `DecompressionStream('deflate-raw')` — поддержка сжатых (DEFLATE) архивов
|
||||
(WhatsApp/1С архивы сжаты; старый STORE-only `unzip` из v5 не подходил).
|
||||
- `parseZip()` — разбор EOCD → Central Directory → local headers.
|
||||
- `dosToMs()` — DOS date/time (4 байта) → timestamp (мс).
|
||||
- `decodeZipName()` — имя: UTF-8 (если флаг 0x800) иначе CP437→CP866
|
||||
(кириллица из 1С, зеркалит серверный `_decode_name`).
|
||||
|
||||
2. **Рекурсия** `listZipFiles()`:
|
||||
- директории (`name` оканчивается на `/`) пропускаются;
|
||||
- вложенный `.zip` распаковывается рекурсивно;
|
||||
- имя сохраняется **полным путём** (`dir1/sub/file.pdf`).
|
||||
|
||||
3. **Дедуп + суффикс** `addFileWithDedup()`:
|
||||
- структура `fileMeta` (Map: имя → `{size, mtime}`);
|
||||
- имя есть + тот же размер И та же дата → точный дубль → **пропустить**;
|
||||
- имя есть + разный размер ИЛИ дата → **суффикс** `name_2.ext`, `name_3.ext`…;
|
||||
- дата: для обычных файлов `File.lastModified` (мс), для извлечённых из ZIP —
|
||||
DOS date/time → мс.
|
||||
|
||||
4. `fi.addEventListener('change')` переписан в `async`:
|
||||
- `.zip` → `listZipFiles` → каждый файл через `addFileWithDedup`;
|
||||
- при ошибке распаковки (неподдерживаемый метод/не-ZIP) — zip добавляется как есть.
|
||||
|
||||
## Проверка
|
||||
|
||||
- JS-синтаксис: `node --check` — OK.
|
||||
- Рекурсия вложенного ZIP (`nested.zip` → `sub/a.txt`) — извлечён, проверено в Node.
|
||||
- Дедуп/суффикс: `dup.txt` (3) → `dup_2.txt` (5) → `dup_3.txt` (2); точный дубль пропущен.
|
||||
- Backend не менялся (кроме версии в `app.py`).
|
||||
|
||||
## Открытый вопрос
|
||||
|
||||
`DecompressionStream` требует современных браузеров (Chrome 103+, Safari 16.4+,
|
||||
Firefox 113+). AES-шифрованные ZIP (метод 99) не поддерживаются — при этом архив
|
||||
добавляется в таблицу как есть (fallback).
|
||||
@@ -0,0 +1,38 @@
|
||||
# v0.0.35 — фильтр документов при раскрытии ZIP — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.34 → 0.0.35
|
||||
**Файл:** `site/templates/index.html` (фронт `listZipFiles`), `site/app.py` (версия)
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
При раскрытии ZIP в таблицу ранее вытаскивались ВСЕ файлы, включая изображения
|
||||
(WhatsApp jpeg и т.п.). Теперь из ZIP извлекаются **только документы**:
|
||||
|
||||
Разрешённые расширения: `.pdf`, `.doc`, `.docx`, `.txt`, `.md`.
|
||||
|
||||
Всё прочее (jpeg, png, xls, exe и т.д.) — пропускается. Вложенные ZIP
|
||||
раскрываются рекурсивно, и фильтр применяется и к их содержимому.
|
||||
|
||||
## Изменение
|
||||
|
||||
`listZipFiles()` в `index.html`:
|
||||
```js
|
||||
const allowedExt = ['.pdf', '.doc', '.docx', '.txt', '.md'];
|
||||
...
|
||||
else if (allowedExt.some(ext => low.endsWith(ext))) {
|
||||
out.push(new File([e.data], e.name, { lastModified: e.dosMs }));
|
||||
}
|
||||
// иначе — не документ, пропускаем
|
||||
```
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK.
|
||||
- Тест на архиве с `doc.pdf, old.doc, n.docx, a.txt, r.md, pic.jpeg, pic2.png`:
|
||||
извлечены только 5 документов, jpeg/png пропущены — OK.
|
||||
|
||||
## Вопрос (отложен)
|
||||
- Нужно также упомянуть в подсказке на странице требование современного
|
||||
браузера (`DecompressionStream`: Chrome 103+, Safari 16.4+, Firefox 113+).
|
||||
@@ -0,0 +1,36 @@
|
||||
# 2026-08-20 — Лимиты загрузки + предупреждение в таблице (v0.0.50)
|
||||
|
||||
## Проблема
|
||||
Юзер грузил всё подряд (сотни МБ, включая >200МБ) → HTTP 413 / SSE-обрыв.
|
||||
Требование: понятная политика лимитов, но файлы сверх лимита НЕ молча скрывать —
|
||||
показывать в таблице, чтобы юзер не повторял загрузку в недоумении.
|
||||
|
||||
## Решение (фронт, index.html)
|
||||
Лимиты:
|
||||
- 1 файл > 100 МБ → «не учитывается»
|
||||
- сумма учитываемых > 1 ГБ → следующий файл тоже «не учитывается»
|
||||
|
||||
Логика:
|
||||
- `MAX_FILE_BYTES = 100МБ`, `MAX_SESSION_BYTES = 1ГБ` (константы).
|
||||
- `addFileWithDedup`: файл сверх лимита добавляется в `sf` (виден в таблице),
|
||||
но имя кладётся в `overNames` (Set). Учитываемость считаем по не-overNames.
|
||||
- `rr()`: строки с `overNames` получают класс `row-over` (красный фон/текст),
|
||||
статус «🔥 не учитывается»; счётчик: «N учитываются + M свыше лимита · СУММА».
|
||||
- `uploadFiles()`: строит `toSend` ТОЛЬКО из учитываемых, `sendIdx` маппит
|
||||
idx-отправленного → idx в `sf` (прогресс/таймеры/start/done пишут в верную строку).
|
||||
Если все переборные → «Нет файлов для обфускации (все превышают лимит)».
|
||||
- Шапка: добавлена строка «Ограничения: файл не более 100 МБ, суммарно не более
|
||||
1 ГБ. Файлы сверх лимита помечаются красным и не участвуют в обфускации.»
|
||||
- CSS: `.row-over td { background: rgba(220,38,38,.10) !important; color:#c0392b; }`.
|
||||
- `rm`/`resetAll` — чистят `overNames`.
|
||||
|
||||
Бэк НЕ менялся (MAX_CONTENT_LENGTH=200МБ, MAX_SESSION_BYTES=500МБ остались как защита
|
||||
на сервере). Внимание: бэк-лимит 200МБ/запрос и 500МБ/сессия всё ещё ниже фронтовых
|
||||
1ГБ — для файлов 100МБ-200МБ фронт пустит, но одиночный запрос >200МБ упадёт 413.
|
||||
Это можно согласовать позже, если нужно.
|
||||
|
||||
## Проверка
|
||||
- node --check секции <script> index.html — OK.
|
||||
- py_compile app.py — OK.
|
||||
- Лог-тест лимитов через node: 120МБ→перебор, 13 учитываемых ~995МБ, >1ГБ→перебор — верно.
|
||||
- VERSION поднята до 0.0.50.
|
||||
@@ -0,0 +1,18 @@
|
||||
# v0.0.68 — Кнопка «Выбрать папку» (webkitdirectory, рекурсивно) (2026-08-24)
|
||||
|
||||
_ux-frontend. По плану `plans/2026-08-24-folder-select-plan.md`. Код: `site/templates/index.html`._
|
||||
|
||||
## Что сделано
|
||||
- Кнопка «📁 Выбрать папку» (`#folderBtn`) + скрытый `<input type="file" id="folderInput" webkitdirectory multiple>`.
|
||||
- Обработчик `change` `#folderInput`:
|
||||
- `if (busy) return` — во время загрузки/обработки выбор папки заблокирован.
|
||||
- Берёт `webkitRelativePath` (`TopFolder/Подпапка/file.pdf`), **отбрасывает верхнюю папку** → `rel`.
|
||||
- `.zip` → раскрывает `listZipFiles`, префикс пути папки к именам; при ошибке — zip как есть.
|
||||
- Документы (`.pdf .doc .docx .txt .md`) → `addFileWithDedup(new File([...], rel))`.
|
||||
- Прочее — пропускается. Индикатор «Разбираю папку…», после — «✅ Добавлено N файлов».
|
||||
- Дедуп/лимиты работают штатно (имя = относительный путь → подпапки не сливаются).
|
||||
- Поддержка: Chrome/Edge — полно, Firefox — частично, Safari — нет (best-effort).
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK; `py_compile` — OK.
|
||||
- Версия 0.0.67 → 0.0.68.
|
||||
@@ -0,0 +1,42 @@
|
||||
# v0.0.69 — Архивы из папки: не терять, раскрывать документы (2026-08-24)
|
||||
|
||||
_ux-frontend. После ретеста v0.0.68 пользователем на проде._
|
||||
|
||||
## Ситуация
|
||||
Пользователь выбрал папку, в которой лежали `iviconfig8257.zip` (17.1 KB) и
|
||||
`Nail_Nevrolog_20250602.PDF`. Добавился только PDF («Добавлено из папки: 1 файлов»),
|
||||
zip не раскрылся. Требование: «если в папке архив — его надо раскрывать!».
|
||||
|
||||
## Диагностика (Playwright на проде, v0.0.68)
|
||||
- Сервер отдавал v0.0.68 с кнопкой — деплой прошёл; первая загрузка страницы
|
||||
показывала старый кэш v0.0.67 (нужен reload).
|
||||
- Собранный в браузере валидный zip с документами в подпапке — обработчик папки
|
||||
РАСКРЫВАЕТ корректно: файлы добавляются с префиксом пути (`Подпапка/Договор…`),
|
||||
статус «Добавлено из папки: 4 файлов».
|
||||
- Вывод: `iviconfig8257.zip` не раскрылся, потому что ВНУТРИ нет документов
|
||||
(конфиги/прочее) — `listZipFiles` вернул пусто. Оба обработчика (fileInput и
|
||||
folderInput) молча ТЕРЯЛИ такой архив.
|
||||
|
||||
## Фикс (site/templates/index.html)
|
||||
1. `fi`-обработчик: `nested.length` → добавлять файлы; иначе — архив как есть.
|
||||
2. `folder`-обработчик: то же для zip из папки (если документов нет — архив как есть).
|
||||
3. Склонение счётчика: «1 файл / 2 файла / 5 файлов».
|
||||
|
||||
Поведение после фикса:
|
||||
- В папке zip с документами → раскрывается (файлы с путём в таблицу).
|
||||
- В папке zip без документов / нераспакованный → добавляется как zip, не теряется.
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK, `py_compile` — OK.
|
||||
- Локальный запуск на 127.0.0.1:5010 (v0.0.69), сценарий папки
|
||||
[zip-конфиг + PDF] → «✅ Добавлено из папки: 2 файла», в таблице оба файла.
|
||||
- Версия 0.0.68 → 0.0.69.
|
||||
|
||||
## Проверка на проде (после редеплоя 18:31, v0.0.69)
|
||||
- Сценарий A — папка [zip БЕЗ документов + PDF] → «✅ Добавлено из папки: 2 файла»,
|
||||
в таблице `iviconfig8257.zip` (как есть) + `Nail_Nevrolog_20250602.PDF`. Архив не теряется.
|
||||
- Сценарий B — папка [zip С документами в подпапке + PDF] → «✅ Добавлено из папки: 3 файла»,
|
||||
в таблице `Подпапка/Договор_ООО_Ромашка.pdf` (раскрыт с путём), `Акт_выполненных_работ.docx`, `Отчёт.pdf`.
|
||||
- Примечание: `curl` к проду отдаёт ОБРЕЗАННЫЙ HTML (рвётся соединение, ~15-18КБ из 49.6КБ
|
||||
Content-Length) — это проблема шлюза для curl, НЕ деплоя. Браузер докачивает полностью.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# v0.0.70 — Заметная кнопка «?» + пояснение «пропущен» (2026-08-24)
|
||||
|
||||
_ux-frontend. По замечаниям пользователя после v0.0.69._
|
||||
|
||||
## Что сделано (site/templates/index.html)
|
||||
1. **Кнопка «HELP» в правом верхнем углу** (вместо значка «?»):
|
||||
- было: «?» 12px muted — почти не видно;
|
||||
- стало: синяя кнопка-пилюля с надписью **HELP** (фон `#1d4ed8`, белый жирный текст 13px/700,
|
||||
padding 5×14px, radius 6px, тень; hover темнее). Размер 62.6×23px.
|
||||
- Клик открывает helpModal (проверено: display flex).
|
||||
2. **Текст ограничений** — добавлено пояснение статуса «пропущен»:
|
||||
«…помечаются красным и не участвуют в обфускации: при запуске обработки они
|
||||
получают статус «пропущен» и не попадают в результат.»
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK, `py_compile` — OK.
|
||||
- Локально (127.0.0.1:5010, v0.0.70): кнопка HELP — синяя пилюля 62.6×23px, клик открывает справку;
|
||||
текст ограничений обновлён.
|
||||
- Версия 0.0.69 → 0.0.70.
|
||||
@@ -0,0 +1,171 @@
|
||||
# Дизайн: TTL-фикс + прерывание с сохранением + оценка времени + UI (2026-08-24)
|
||||
|
||||
> План реализации фичи по итогам тестов 24.08 (см. `2026-08-24-tests-upload-and-obfuscation.md`).
|
||||
> Статус: **ПЛАН, код не менялся**. Порядок: TTL → прерывание → ETA → UI.
|
||||
|
||||
---
|
||||
|
||||
## 0. Исходная проблема (что тесты вскрыли)
|
||||
|
||||
- **TTL-баг**: сессия живёт 30 мин (`TTL_SECONDS = 30*60`), таймер запускается при создании
|
||||
и **не продлевается** во время обработки. Обфускация 101 файла заняла 1995с (>30 мин) →
|
||||
сессия удалена TTL → `store_result` молча `False` → `download`/`csv` = **404**, результат потерян.
|
||||
- **Нет UX для долгих прогонов**: юзер не знает, сколько ждать, и не может прервать с сохранением.
|
||||
|
||||
## 1. Требования (от пользователя)
|
||||
|
||||
1. Показывать, что обработка долгая «потому что много данных».
|
||||
2. Возможность **прерывания с сохранением** уже обработанного в выходном ZIP + CSV.
|
||||
3. Прерывание **не мгновенное** — «пусть завершает, сколько требуется» (мягкий добор).
|
||||
4. Юзер **точно знает, что будет сохранено** (до нажатия и во время).
|
||||
5. В начале — **ориентировочное время** обработки всего пакета, чтобы знать, чего ожидать.
|
||||
|
||||
---
|
||||
|
||||
## 2. Блок A — TTL-фикс (фундамент, обязателен первым)
|
||||
|
||||
**Файл:** `site/session.py`
|
||||
|
||||
### Текущее поведение
|
||||
```python
|
||||
TTL_SECONDS = 30 * 60
|
||||
def _start_timer(sid):
|
||||
def _clean(): _sessions.pop(sid, None)
|
||||
timer = threading.Timer(TTL_SECONDS, _clean); timer.daemon=True; timer.start(); return timer
|
||||
```
|
||||
Таймер одноразовый, не продлевается → долгий прогон убивает сессию.
|
||||
|
||||
### Новое поведение
|
||||
- Добавить `def touch(sid)`: отменяет старый таймер и запускает новый (продлевает жизнь).
|
||||
- При старте обработки (`process_stream`) — **отменить** таймер сессии (во время обработки
|
||||
сессия живёт «вечно», пока идёт воркер).
|
||||
- При завершении (`complete` / `error` / `cancelled`) — **запустить** таймер заново
|
||||
(результат доступен ещё 30 мин после окончания).
|
||||
- `add_file`, `get_files` — можно тоже `touch()` для единообразия (не обязательно).
|
||||
|
||||
**Итог:** долгий прогон больше не теряет сессию; результат доступен 30 мин после завершения.
|
||||
|
||||
---
|
||||
|
||||
## 3. Блок B — прерывание с сохранением (мягкая остановка)
|
||||
|
||||
### 3.1. Модель поведения (согласовано с пользователем)
|
||||
|
||||
| Где нажата «Прервать» | Поведение |
|
||||
|---|---|
|
||||
| Во время LLM-анализа | Анализ **добарается до конца** (без полного mapping нельзя сохранить ни одного документа). Прерывание срабатывает на границе фазы замены. |
|
||||
| Во время фазы замены | Дорабатывается текущий файл → стоп. В ZIP+CSV попадают все готовые файлы + полная таблица замен. |
|
||||
| После завершения | Обычный полный результат (прерывание не актуально). |
|
||||
|
||||
- Флаг отмены проверяется **только на границах файлов фазы замены** → результат всегда осмысленный.
|
||||
- LLM-фаза не прерывается (добор), что соответствует «пусть завершает, сколько требуется».
|
||||
|
||||
### 3.2. Изменения по файлам
|
||||
|
||||
**`site/session.py`**
|
||||
- В словаре сессии добавить `"cancel": threading.Event()` при создании.
|
||||
- Хелперы: `request_cancel(sid)`, `cancel_requested(sid)`.
|
||||
|
||||
**`site/routes/api_bp.py`**
|
||||
- Новый эндпоинт `POST /api/cancel/<sid>`: `request_cancel(sid)` → `{ok:true}` (или 404 если нет сессии).
|
||||
- `process_stream`:
|
||||
- При старте — `cancel_event` = событие из сессии, таймер TTL отменяется.
|
||||
- Воркер передаёт `cancel_event` в `obfuscate_files`.
|
||||
- Обработка исключения отмены → сборка **частичного результата**:
|
||||
- ZIP из файлов, прошедших замену (исключая `mapping.csv`) + полный `mapping.csv`.
|
||||
- `store_result` + `store_csv` (сейчас `store_result` сохраняет только полный zip).
|
||||
- Новое SSE-событие `cancelled`: `data: {"saved": <кол-во готовых>, "total": <всего>, ...}`.
|
||||
- После завершения/отмены — перезапуск TTL-таймера.
|
||||
|
||||
**`drhider/obfuscator.py`**
|
||||
- `obfuscate_files(..., cancel_event=None)`.
|
||||
- В цикле фазы замены (по файлам): `if cancel_event and cancel_event.is_set(): raise CancelRequested()`.
|
||||
- В LLM-цикле отмену **НЕ** проверяем (добор до конца).
|
||||
- `CancelRequested` — новое исключение (в `drhider/obfuscator.py` или общий модуль).
|
||||
- Для сборки частичного ZIP: функция должна уметь вернуть уже обработанные файлы
|
||||
(список `(имя, bytes)` заменённых) — либо прогресс-колбеком, либо исключение с данными.
|
||||
|
||||
### 3.3. Частичный результат — детали
|
||||
- Каждый файл, прошедший замену, гарантированно консистентен: глобальный mapping один для всех.
|
||||
- Частичный ZIP = готовые файлы + `mapping.csv` (полная таблица — она собрана к концу LLM).
|
||||
- Если прервано до конца LLM — прерывание не сработает (добор), значит частичный результат
|
||||
будет как минимум с теми файлами, которые успели замениться после LLM.
|
||||
|
||||
---
|
||||
|
||||
## 4. Блок C — оценка времени (ETA)
|
||||
|
||||
### 4.1. Уровень 1 — статическая оценка в начале
|
||||
- Фронт знает `N` файлов и объём `S` (МБ) до обработки.
|
||||
- Эмпирический коэффициент из замеров: 24.08 — 101 файл / ~150 КБ текста → LLM ≈ 1924с
|
||||
(~12.8 с/КБ текста); 23.08 — 100 файлов → LLM 841–985с.
|
||||
- Коэффициент задать константой на фронте/бэке (можно в `config.py`), формула:
|
||||
`оценка ≈ S_текста(КБ) × K + константа`. Для бинарных/сканов текста нет — оценка грубая,
|
||||
поэтому это «ориентировочно», с оговоркой в UI.
|
||||
|
||||
### 4.2. Уровень 2 — после извлечения (бэк знает объём текста)
|
||||
- `obfuscate_files` после фазы извлечения знает суммарный объём текста (`total_chars`).
|
||||
- Через новый колбек (или расширенный `progress_cb`) отдать `{phase:"extract_done", chars:N}`.
|
||||
- Тогда оценка точнее: `tokens ≈ chars × фактор`, `время ≈ tokens / скорость(истор.)`.
|
||||
|
||||
### 4.3. Уровень 3 — адаптивный ETA во время LLM
|
||||
- В LLM-цикле меряем фактическую скорость: обработано символов / прошедшее время.
|
||||
- Оставшийся объём = `total_chars − processed_chars`.
|
||||
- `ETA = оставшиеся символы / скорость`.
|
||||
- Отдавать в существующем heartbeat `llm`:
|
||||
`data: {"active":..., "elapsed":..., "tokens":..., "eta_sec": N, "done_chars":..., "total_chars":...}`.
|
||||
- Фронт обновляет «осталось ~X мин» в live-блоке.
|
||||
|
||||
### 4.4. Ограничения (честно)
|
||||
- Оценка **ориентировочная** (LLM-вывод варьируется). Показывать как «≈», обновлять адаптивно.
|
||||
- Для сканов/PDF без текстового слоя — извлечённый текст мал, LLM-этап быстрый, оценка завышена.
|
||||
|
||||
---
|
||||
|
||||
## 5. Блок D — UI (index.html)
|
||||
|
||||
### 5.1. Сообщение «много данных»
|
||||
- После загрузки (перед обработкой): «Загружено N файлов (X МБ). Идёт обработка —
|
||||
это может занять ~M мин.»
|
||||
- Держать в статус-строке и в live-блоке.
|
||||
|
||||
### 5.2. Кнопка «Прервать»
|
||||
- Появляется/активна на этапе обработки (Фаза 2).
|
||||
- По нажатию — **диалог-подтверждение**:
|
||||
> «Остановить обработку? Будет сохранено: документы, уже прошедшие обработку
|
||||
> (сейчас готово X из N), и таблица замен. Текущий анализ будет доведён до конца.
|
||||
> [Остановить] [Отмена]»
|
||||
- После подтверждения — `POST /api/cancel/<sid>`.
|
||||
- Во время добора: счётчик «готово X/N» + «завершаем текущий этап…».
|
||||
|
||||
### 5.3. Показ частичного результата
|
||||
- SSE-событие `cancelled` → статус «Сохранено X из N документов + таблица замен».
|
||||
- Кнопки «Скачать ZIP» / «Скачать CSV» работают как обычно (ведут на download/csv).
|
||||
|
||||
### 5.4. ETA в live-блоке
|
||||
- «Обработка… осталось ~X мин» — из `eta_sec` хартбита.
|
||||
- Статическая оценка — сразу после старта обработки.
|
||||
|
||||
---
|
||||
|
||||
## 6. Порядок реализации и проверка
|
||||
|
||||
1. **TTL-фикс** (`session.py` + `api_bp.py`): тест — долгий прогон >30 мин, скачивание после.
|
||||
2. **Прерывание** (`obfuscator.py` + `api_bp.py` + `session.py`): тест — прервать в фазе замены,
|
||||
проверить частичный ZIP+CSV.
|
||||
3. **ETA** (`obfuscator.py`/`api_bp.py` + фронт): тест — сверка оценки с фактом.
|
||||
4. **UI** (кнопка, диалог, счётчик): ручной тест в браузере.
|
||||
|
||||
Версия: бамп до **0.0.63** после реализации (всех блоков).
|
||||
|
||||
## 7. Затронутые файлы
|
||||
- `site/session.py` — TTL `touch`, флаг отмены.
|
||||
- `site/routes/api_bp.py` — `POST /api/cancel/<sid>`, событие `cancelled`, ETA в heartbeat, частичный результат.
|
||||
- `drhider/obfuscator.py` — `cancel_event`, `CancelRequested`, передача объёма текста, сборка частичного результата.
|
||||
- `site/templates/index.html` — сообщение, кнопка, диалог, счётчик, ETA.
|
||||
- `drhider/config.py` (опц.) — коэффициент оценки времени.
|
||||
|
||||
## 8. Открытые вопросы
|
||||
- Точное место сбора «готовых файлов» в `obfuscate_files` (где хранить заменённые байты для частичного ZIP).
|
||||
- Формат события `cancelled` (поля `saved/total` + причина).
|
||||
- Значение коэффициента оценки (уточнить по ещё паре замеров).
|
||||
@@ -0,0 +1,49 @@
|
||||
# Реализовано: TTL-фикс + прерывание с сохранением + ETA + UI-таблица (v0.0.63)
|
||||
|
||||
_2026-08-24. По плану `2026-08-24-implementation-plan-for-flash.md`. Код написан (роль Flash)._
|
||||
|
||||
## Что сделано
|
||||
|
||||
### session.py — TTL-фикс + отмена
|
||||
- `touch(sid)`, `pause_ttl(sid)`, `resume_ttl(sid)` — продление/пауза/возобновление TTL.
|
||||
- В сессии `"cancel": threading.Event()`; `request_cancel(sid)`, `get_cancel_event(sid)`.
|
||||
|
||||
### scanner.py — прогресс и мягкая остановка LLM
|
||||
- `class CancelRequested`.
|
||||
- `scan_llm_ner(..., cancel_event, file_progress)`: отмена ТОЛЬКО между файлами (текущий добирается);
|
||||
события `file_start{chars,chunks}`, `file_chunk{chunks_done,chunks_total}`, `file_done{elapsed}`.
|
||||
|
||||
### obfuscator.py — частичный результат
|
||||
- `obfuscate()` возвращает `(zip, csv, meta)`: `meta=None` (штатно) или
|
||||
`{"cancelled": True, "processed", "total"}` (прерывание).
|
||||
- Трекинг `llm_done` (файлы с завершённым LLM). При отмене в LLM — в результат идут
|
||||
ТОЛЬКО файлы из `llm_done` (их обфускация корректна); при отмене в фазе замены —
|
||||
стоп после текущего файла. Отмена в replace НЕ проверяется, если LLM уже прерван
|
||||
(файлы из llm_done добираются).
|
||||
- Событие `extract_done{total_chars, per_file}` (объём текста для ETA).
|
||||
|
||||
### api_bp.py — API
|
||||
- `POST /api/cancel/<sid>`.
|
||||
- `process_stream`: `pause_ttl` в начале / `resume_ttl` в `finally`; передача `cancel_event`
|
||||
и `file_progress` в воркер; события `extract_done/file_start/file_chunk/file_done/cancelled`;
|
||||
heartbeat `llm` расширен: `eta_sec, done_chars, total_chars`; per-file ETA в `file_chunk`.
|
||||
- При закрытии вкладки (`disconnect`) ставится и `cancel`, и `cancel_event` (воркер останавливается).
|
||||
- legacy `process()` — фикс 3-значного возврата + TTL.
|
||||
|
||||
### index.html — UI
|
||||
- Кнопка «⏹ Прервать» + модал-подтверждение (объясняет, что сохранится).
|
||||
- Таблица 3 секций: «✓ Обработанные» (факт время) / «▶ Текущий файл» (прошло / ~осталось) /
|
||||
«○ Ожидают» (~оценка из скорости текущего файла).
|
||||
- Live-блок: «осталось ~X» (глобальная ETA), текущий файл с per-file ETA.
|
||||
- SSE-обработчики: extract_done/file_start/file_chunk/file_done/cancelled; done различает
|
||||
пропущенные (до extract_done) и готовые (после).
|
||||
|
||||
## Проверка
|
||||
- `py_compile` всех .py — OK; `node --check` (JS из index.html) — OK; `get_errors` — нет.
|
||||
- Локальные тесты логики отмены (FakeLLM): штатно=4 файла; отмена в LLM=3 сохранено;
|
||||
отмена до LLM=0..1 (текущий добирается); отмена после завершения=полный.
|
||||
- Смоук-тест: приложение создаётся, все роуты включая `/api/cancel/<sid>` зарегистрированы.
|
||||
|
||||
## ВАЖНО
|
||||
- Версия 0.0.63. **Код не задеплоен** — нужен редеплой на кластере и прогон реальных тестов
|
||||
(долгий прогон + прерывание).
|
||||
@@ -0,0 +1,30 @@
|
||||
# v0.0.64–0.0.65 — Тикеры времени и оценки обработки по файлам (2026-08-24)
|
||||
|
||||
_ux-frontend. Продолжение v0.0.63 (прерывание/ETA). По итогам замечаний: тикеры/оценки
|
||||
должны быть видны СРАЗУ и на ВСЕХ этапах, а не только в LLM-фазе._
|
||||
|
||||
## Проблема (v0.0.63)
|
||||
- Подсветка «▶ Текущий файл» и оценки появлялись только в LLM-фазе.
|
||||
- На этапе извлечения (долгие PDF, .doc через liberta) таблица показывала всё
|
||||
«○ Ожидают —», без текущего файла и оценок — юзер не видел никакого прогресса.
|
||||
|
||||
## v0.0.64 — тикер текущего файла на всех этапах + самокалибровка
|
||||
- `start` (этап извлечения): файл, который обрабатывается сейчас, помечается `current`
|
||||
(предыдущий демотируется), тикер `прошло/осталось` идёт через `procRefresh` (1с).
|
||||
- Замер скорости извлечения: по предыдущему «текущему» файлу считается `procExtractRate`
|
||||
(сек/МБ) — оценки «ожидающих» калибруются по факту, а не по константе.
|
||||
- `extract_done`: снимает подсветку извлечения (LLM-фаза назначает текущий через `file_start`).
|
||||
- Оценки у «ожидающих»: LLM (`chars × rate`) либо грубая по размеру (сек/МБ).
|
||||
|
||||
## v0.0.65 — мгновенные оценки (ещё до старта)
|
||||
- При выборе файлов (простой режим) у каждого файла в «Статусе» — `~X` (по размеру,
|
||||
`EST_MB_SEC = 12` с/МБ).
|
||||
- В шапке таблицы — суммарная оценка: `N файлов · S МБ · ~T`.
|
||||
- Подсказка: «⏱ Время обработки каждого файла — ориентировочное (обновляется по факту)».
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK; `py_compile` — OK.
|
||||
- Локально: имя из ZIP декодируется корректно (см. v0.0.67).
|
||||
|
||||
## Версии
|
||||
- 0.0.63 → 0.0.64 (тикеры на всех этапах) → 0.0.65 (мгновенные оценки).
|
||||
@@ -0,0 +1,34 @@
|
||||
# v0.0.66 — Блокировки UI: предсказуемое поведение во всех состояниях (2026-08-24)
|
||||
|
||||
_ux-frontend. По замечанию: во время обработки «Выбрать файлы» оставалась активной —
|
||||
юзер мог менять список и ломать состояние/индексы. Требование: ВСЁ предсказуемо,
|
||||
учтены ВСЕ действия юзера._
|
||||
|
||||
## Состояния и доступные действия
|
||||
|
||||
| Состояние | Выбрать файлы | ✕ удалить | Обфусцировать | Прервать | Скачать | Новая сессия |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Простой | ✅ | ✅ | ✅ | — | — | — |
|
||||
| Загрузка (1/2) | 🔒 | 🔒 | 🔒 | — | — | — |
|
||||
| Обработка (2/2) | 🔒 | 🔒 | 🔒 | ✅ | — | — |
|
||||
| Результат готов | 🔒 | 🔒 | 🔒 | — | ✅ | ✅ |
|
||||
| Ошибка | ✅ | ✅ | ✅ | — | — | — |
|
||||
|
||||
## Реализация
|
||||
- Глобальный флаг `busy` + `setBusy(b)`: блокирует `fileInput` и скрывает кнопки «✕»
|
||||
(CSS `body.busy .remove-btn { display:none }`).
|
||||
- Двойная защита: обработчики `change` (выбор файлов) и `rm` (удаление) игнорируют
|
||||
вызовы при `busy`.
|
||||
- `uploadFiles()`: guard `if (busy) return` + `setBusy(true)`; ранние выходы (ошибки
|
||||
фазы 1, test-режим) — `setBusy(false)`.
|
||||
- **Заморозка сессии после результата**: `complete`/`cancelled` → `sessionDone=true`,
|
||||
`fi`/`uploadBtn` выключены, показаны «Скачать» и новая кнопка **«🔄 Новая сессия»**
|
||||
(`resetAll`). Ошибки — снимают блокировку (можно повторить).
|
||||
- При закрытии вкладки (beforeunload) — `resetAll` сбрасывает всё.
|
||||
|
||||
## Найденный и исправленный баг при правке
|
||||
- При добавлении заморозки случайно затёрлась строка `activeES.addEventListener('complete', ...)`
|
||||
— JS-синтаксис сломался («Missing catch»). Восстановлено ДО пуша; `node --check` — OK.
|
||||
|
||||
## Проверка
|
||||
- `node --check` (извлечённый <script>) — OK; `py_compile` — OK.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user