Compare commits

..

6 Commits

17 changed files with 786 additions and 44 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

2
.gitignore vendored
View File

@@ -11,5 +11,5 @@ __pycache__/
.env.*
!.env.example
# Local bot archive
# Local application archives
data/

View File

@@ -1,46 +1,60 @@
# Акылдаш — бот-секретарь
# Акылдаш
Версия: `0.2.1`
Акылдаш — проект юридической информационно-аналитической платформы. Цель —
собирать правовые источники на законных основаниях, сохранять их происхождение
и версии, готовить данные для поиска и RAG, а затем предоставлять результаты
через API и пользовательский интерфейс со ссылками на первоисточники.
Бот сохраняет сообщения разрешённых тем Telegram в локальную SQLite-базу и
выгружает обсуждения в Markdown. Внешние Python-зависимости не требуются.
Telegram-бот — только часть рабочего окружения команды, а не основной продукт.
## Запуск
## Текущее состояние
Требуется Python 3.11 или новее.
Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и первая backend-функция
версии `0.1.2`: возобновляемая выгрузка документов из ЦБД Минюста КР.
```bash
export TELEGRAM_BOT_TOKEN='...'
export TELEGRAM_CHAT_ID='-1004242041275'
export TELEGRAM_OWNER_ID='87262245'
export TELEGRAM_REPORT_THREAD_ID='37'
export TELEGRAM_ALLOWED_THREAD_IDS='0,2,4,6,8'
python3 bot.py
| Компонент | Версия | Состояние |
|---|---:|---|
| Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния |
| Backend | `0.1.2` | реализована выгрузка документов ЦБД Минюста КР |
| Frontend | — | ещё не создан |
| Сбор и обработка правовых данных | `0.1.2` | реализован архиватор ЦБД Минюста КР |
| RAG и база знаний | — | ещё не созданы |
## Структура репозитория
```text
docs/ структурированная документация проекта
decisions/ принятые архитектурные и продуктовые решения
operations/ состояние проекта и рабочие процессы
product/ назначение, границы и развитие продукта
team/ материалы для команды
tools/
telegram-bot/ бот рабочего Telegram-пространства
backend/
ingestion/ получение и обновление правовых источников
```
Доступные команды: `/help`, `/status`, `/export`.
Экспорт доступен только пользователю с Telegram ID из `TELEGRAM_OWNER_ID`.
Первый `/export` выгружает всю сохранённую тему, последующие — сообщения после
предыдущей успешно созданной отсечки.
Markdown публикуется в теме `TELEGRAM_REPORT_THREAD_ID`, а в исходной теме
остаётся отсечка со ссылкой и общим хэштегом отчёта.
Каталоги для загрузки и обработки источников, RAG, backend и frontend будут
создаваться с первой реальной задачей в соответствующей области. Это позволит
выбрать структуру по фактическим требованиям, а не поддерживать пустой каркас.
База по умолчанию хранится в `data/secretary.sqlite3`. Сообщения из других
групп и тем не сохраняются. Полный ответ Telegram сохраняется в базе, поэтому
метаданные вложений остаются доступными для последующего скачивания.
Начать знакомство с проектом: [документация](docs/README.md) и
[обзор продукта](docs/product/project-overview.md).
## Проверка
## Конфиденциальные материалы
Секреты, персональные данные, договоры и материалы по правовой защите проекта
не должны храниться в этом репозитории. Для них нужен отдельный закрытый
репозиторий или защищённое хранилище с минимально необходимыми правами доступа,
журналированием и резервным копированием. Здесь допустимы только несекретные
правила и ссылки на такие материалы без раскрытия их содержания.
## Проверка Telegram-бота
```bash
python3 -m unittest -v
python3 -m unittest discover -s tools/telegram-bot -v
```
## Synology
Контейнер `akyldash-bot` работает на образе `python:3.11-slim` с политикой
перезапуска `unless-stopped`. Постоянные данные находятся в
`/volume1/docker/akyldash/data`.
---
Акылдаш v0.2.1
Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан

64
backend/README.md Normal file
View File

@@ -0,0 +1,64 @@
# Backend Акылдаш
Версия: `0.1.2`
Первая backend-область проекта — загрузка правовых документов из официального
Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в
`ingestion/minjust_cbd.py`: его можно запускать как CLI сейчас и импортировать
из будущего планировщика backend без запуска отдельного процесса.
## Пробная загрузка
Из корня репозитория:
```bash
python3 backend/ingestion/minjust_cbd.py --limit 10
```
В интерактивном терминале отображаются процент, количество документов,
ошибки, скорость и примерное оставшееся время.
Полная загрузка выполняется без `--limit`. По умолчанию архив сохраняется в
`data/minjust-cbd`, который исключён из Git. Повторный запуск пропускает уже
загруженные документы; `--refresh` принудительно проверяет их заново.
Если временный идентификатор списка API истечёт во время многодневной загрузки,
скрипт пересоздаст список на текущей странице и продолжит автоматически.
В версии `0.1.0` обновление существующих документов выполняется полной проверкой
через `--refresh`. Инкрементальную проверку по `lastmod` из sitemap следует
добавить вместе с backend-планировщиком, когда будет определена частота запуска.
## Защита API
Запросы выполняются последовательно, по умолчанию не чаще одного в секунду.
Timeout одного ответа — 60 секунд, число попыток — 5, задержка между повторами
растёт экспоненциально. При HTTP 429 учитывается заголовок `Retry-After`.
Лимит действует на один процесс, поэтому одновременно следует запускать только
один экземпляр загрузчика.
Более осторожный режим:
```bash
python3 backend/ingestion/minjust_cbd.py --requests-per-second 0.5
```
## Вызов из будущего backend
```python
from ingestion.minjust_cbd import sync_archive
result = sync_archive(output_path)
```
Планировщик, очередь задач и PostgreSQL пока не добавлены: модуль не зависит от
выбора будущего backend-фреймворка.
## Проверка
```bash
PYTHONPATH=backend python3 -m unittest backend/test_minjust_cbd.py -v
```
---
Акылдаш · Backend v0.1.2 · Frontend — не создан

View File

@@ -0,0 +1,373 @@
#!/usr/bin/env python3
"""Download and archive documents from the Kyrgyz Republic Ministry of Justice."""
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import logging
import os
import sqlite3
import sys
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Iterable
APP_VERSION = "0.1.2"
API_BASE_URL = "https://cbd.minjust.gov.kg/api/v1/OpenData/"
LANGUAGES = {"Rus": "ru", "Kyr": "ky"}
IMAGE_LANGUAGES = {"Russian": "ru", "Kyrgyz": "ky"}
LOGGER = logging.getLogger(__name__)
class CbdClient:
def __init__(
self,
requests_per_second: float = 1,
timeout: int = 60,
retries: int = 5,
) -> None:
if requests_per_second <= 0:
raise ValueError("requests_per_second must be greater than zero")
if timeout <= 0 or retries <= 0:
raise ValueError("timeout and retries must be greater than zero")
# ponytail: per-process limit; add a distributed lock before multiple workers.
self.interval = 1 / requests_per_second
self.timeout = timeout
self.retries = retries
self.last_request = 0.0
self.total_documents = 0
def request_json(self, method: str, parameters: Iterable[tuple[str, object]] = ()):
query = urllib.parse.urlencode(list(parameters), doseq=True)
extension = "" if method == "CheckAvailable" else ".json"
url = f"{API_BASE_URL}{method}{extension}" + (f"?{query}" if query else "")
for attempt in range(self.retries):
retry_delay = 2**attempt
wait = self.interval - (time.monotonic() - self.last_request)
if wait > 0:
time.sleep(wait)
request = urllib.request.Request(
url,
headers={
"Accept": "application/json",
"User-Agent": f"Akyldash/{APP_VERSION} (+https://cbd.minjust.gov.kg)",
},
)
try:
self.last_request = time.monotonic()
with urllib.request.urlopen(request, timeout=self.timeout) as response:
body = response.read()
return json.loads(body) if body else None
except urllib.error.HTTPError as error:
if error.code != 429 and error.code < 500:
raise
if error.code == 429:
try:
retry_delay = max(
retry_delay, float(error.headers.get("Retry-After", 0))
)
except ValueError:
pass
except (TimeoutError, urllib.error.URLError):
pass
if attempt + 1 < self.retries:
time.sleep(retry_delay)
raise RuntimeError(f"Ministry of Justice API request failed: {url}")
def check_available(self) -> None:
self.request_json("CheckAvailable")
def document_codes(self, limit: int | None = None, page_size: int = 1000):
def query_page(page_number: int):
return self.request_json(
"GetDocumentListByQuery",
(
("Property", "Code"),
("PageSize", page_size),
("PageNumber", page_number),
),
)
first = query_page(1)
yielded = 0
page = first
page_number = 1
self.total_documents = min(first["TotalCount"], limit or first["TotalCount"])
while True:
for document in page["Documents"]:
yield document["Code"]
yielded += 1
if limit is not None and yielded >= limit:
return
if yielded >= page["TotalCount"]:
return
page_number += 1
try:
page = self.request_json(
"GetDocumentListById",
(
("DocumentListId", first["Id"]),
("Property", "Code"),
("PageSize", page_size),
("PageNumber", page_number),
),
)
except urllib.error.HTTPError as error:
if error.code != 404:
raise
first = page = query_page(page_number)
self.total_documents = min(
first["TotalCount"], limit or first["TotalCount"]
)
def document(self, code: int) -> dict:
return self.request_json(
"GetDocument",
(
("Code", code),
("Editions.Select", "all"),
("Editions.Data", "all"),
("Editions.Images.Select", "all"),
("Editions.Images.Data", "all"),
),
)
@dataclass(frozen=True)
class SyncResult:
discovered: int = 0
downloaded: int = 0
skipped: int = 0
failed: int = 0
def format_duration(seconds: float) -> str:
seconds = max(0, round(seconds))
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
def progress_line(
result: SyncResult, total: int, elapsed: float, width: int = 24
) -> str:
processed = result.discovered
fraction = processed / total if total else 0
filled = min(width, round(width * fraction))
rate = processed / elapsed if elapsed > 0 else 0
eta = (total - processed) / rate if rate else 0
return (
f"[{'#' * filled}{'-' * (width - filled)}] {fraction:6.2%} "
f"{processed}/{total} downloaded={result.downloaded} "
f"skipped={result.skipped} failed={result.failed} "
f"rate={rate:.2f}/s ETA={format_duration(eta)}"
)
def terminal_progress() -> Callable[[SyncResult, int], None]:
started = time.monotonic()
def update(result: SyncResult, total: int) -> None:
line = progress_line(result, total, time.monotonic() - started)
print(
f"\r{line}",
end="\n" if result.discovered >= total else "",
file=sys.stderr,
flush=True,
)
return update
def connect_manifest(path: Path) -> sqlite3.Connection:
path.parent.mkdir(parents=True, exist_ok=True)
connection = sqlite3.connect(path)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS documents (
code INTEGER PRIMARY KEY,
source_sha256 TEXT NOT NULL,
fetched_at TEXT NOT NULL
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS errors (
code INTEGER PRIMARY KEY,
message TEXT NOT NULL,
failed_at TEXT NOT NULL
)
"""
)
return connection
def atomic_write(path: Path, content: bytes) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as temporary:
temporary.write(content)
temporary_path = Path(temporary.name)
os.replace(temporary_path, path)
def json_bytes(value: object) -> bytes:
return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode()
def archive_document(root: Path, document: dict) -> str:
code = int(document["Code"])
source = json.dumps(document, ensure_ascii=False, sort_keys=True).encode()
source_sha256 = hashlib.sha256(source).hexdigest()
directory = root / "documents" / str(code)
editions = document.get("Editions") or []
metadata = {key: value for key, value in document.items() if key != "Editions"}
atomic_write(directory / "metadata.json", json_bytes(metadata))
for edition in editions:
edition_directory = directory / "editions" / str(int(edition["Code"]))
edition_metadata = {key: value for key, value in edition.items() if key != "Data"}
edition_metadata["Images"] = [
{key: value for key, value in image.items() if key != "Data"}
for image in edition.get("Images") or []
]
atomic_write(edition_directory / "metadata.json", json_bytes(edition_metadata))
for source_language, filename in LANGUAGES.items():
html = (edition.get("Data") or {}).get(source_language)
if html:
atomic_write(edition_directory / f"{filename}.html", html.encode())
for image in edition.get("Images") or []:
if not image.get("Data"):
continue
language = IMAGE_LANGUAGES.get(image.get("Lang"), "unknown")
filename = Path(str(image.get("Name") or "image").replace("\\", "/")).name
if filename in {"", ".", ".."}:
raise ValueError(f"Invalid image filename in document {code}")
atomic_write(
edition_directory / "images" / language / filename,
base64.b64decode(image["Data"], validate=True),
)
return source_sha256
def sync_archive(
output: Path,
client: CbdClient | None = None,
limit: int | None = None,
refresh: bool = False,
progress: Callable[[SyncResult, int], None] | None = None,
) -> SyncResult:
client = client or CbdClient()
output.mkdir(parents=True, exist_ok=True)
connection = connect_manifest(output / "manifest.sqlite3")
known = {row[0] for row in connection.execute("SELECT code FROM documents")}
discovered = downloaded = skipped = failed = 0
client.check_available()
for code in client.document_codes(limit):
discovered += 1
# ponytail: refresh scans all documents; use sitemap lastmod when scheduled
# update checks become frequent enough for the extra mapping logic to pay off.
if code in known and not refresh:
skipped += 1
if progress:
progress(
SyncResult(discovered, downloaded, skipped, failed),
client.total_documents,
)
continue
try:
document = client.document(code)
checksum = archive_document(output, document)
fetched_at = datetime.now(timezone.utc).isoformat()
with connection:
connection.execute(
"""
INSERT INTO documents (code, source_sha256, fetched_at)
VALUES (?, ?, ?)
ON CONFLICT(code) DO UPDATE SET
source_sha256 = excluded.source_sha256,
fetched_at = excluded.fetched_at
""",
(int(document["Code"]), checksum, fetched_at),
)
connection.execute("DELETE FROM errors WHERE code = ?", (code,))
downloaded += 1
except Exception as error: # Continue the long-running archive after one bad record.
with connection:
connection.execute(
"""
INSERT INTO errors (code, message, failed_at) VALUES (?, ?, ?)
ON CONFLICT(code) DO UPDATE SET
message = excluded.message,
failed_at = excluded.failed_at
""",
(code, str(error), datetime.now(timezone.utc).isoformat()),
)
failed += 1
if progress:
progress(
SyncResult(discovered, downloaded, skipped, failed),
client.total_documents,
)
if not progress and discovered % 100 == 0:
LOGGER.info(
"discovered=%s downloaded=%s skipped=%s failed=%s",
discovered,
downloaded,
skipped,
failed,
)
connection.close()
return SyncResult(discovered, downloaded, skipped, failed)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=Path("data/minjust-cbd"))
parser.add_argument("--limit", type=int, help="download only the first N documents")
parser.add_argument("--refresh", action="store_true", help="redownload known documents")
parser.add_argument("--requests-per-second", type=float, default=1)
parser.add_argument("--timeout", type=int, default=60)
parser.add_argument("--retries", type=int, default=5)
parser.add_argument("--version", action="version", version=APP_VERSION)
return parser.parse_args()
def main() -> int:
arguments = parse_args()
if arguments.limit is not None and arguments.limit <= 0:
raise SystemExit("--limit must be greater than zero")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
result = sync_archive(
arguments.output,
CbdClient(
requests_per_second=arguments.requests_per_second,
timeout=arguments.timeout,
retries=arguments.retries,
),
arguments.limit,
arguments.refresh,
terminal_progress() if sys.stderr.isatty() else None,
)
print(
f"discovered={result.discovered} downloaded={result.downloaded} "
f"skipped={result.skipped} failed={result.failed}\n"
f"Akyldash Backend v{APP_VERSION} · Frontend — not created"
)
return int(result.failed > 0)
if __name__ == "__main__":
raise SystemExit(main())

133
backend/test_minjust_cbd.py Normal file
View File

@@ -0,0 +1,133 @@
import base64
import tempfile
import unittest
import urllib.error
from email.message import Message
from pathlib import Path
from unittest.mock import Mock, patch
from ingestion.minjust_cbd import CbdClient, SyncResult, progress_line, sync_archive
class MinjustCbdTest(unittest.TestCase):
def test_archives_document_and_resumes(self):
image = base64.b64encode(b"image").decode()
document = {
"Code": 1,
"Name": {"Rus": "Закон", "Kyr": "Мыйзам"},
"Editions": [
{
"Code": 10,
"Name": {"Rus": "01.01.2026", "Kyr": "01.01.2026"},
"Data": {"Rus": "<p>Закон</p>", "Kyr": "<p>Мыйзам</p>"},
"Images": [
{
"Lang": "Russian",
"Name": "image001.jpg",
"Data": image,
}
],
}
],
}
client = Mock()
client.total_documents = 1
client.document_codes.return_value = [1]
client.document.return_value = document
with tempfile.TemporaryDirectory() as directory:
output = Path(directory)
first = sync_archive(output, client)
second = sync_archive(output, client)
refreshed = sync_archive(output, client, refresh=True)
self.assertEqual(first.downloaded, 1)
self.assertEqual(second.skipped, 1)
self.assertEqual(refreshed.downloaded, 1)
self.assertEqual(client.document.call_count, 2)
self.assertEqual(
(output / "documents/1/editions/10/ru.html").read_text(),
"<p>Закон</p>",
)
self.assertEqual(
(output / "documents/1/editions/10/images/ru/image001.jpg").read_bytes(),
b"image",
)
def test_uses_stable_list_id_for_next_page(self):
client = CbdClient(requests_per_second=1000)
client.request_json = Mock(
side_effect=[
{
"Id": "list-id",
"TotalCount": 3,
"Documents": [{"Code": 1}, {"Code": 2}],
},
{
"Id": "list-id",
"TotalCount": 3,
"Documents": [{"Code": 3}],
},
]
)
self.assertEqual(list(client.document_codes(page_size=2)), [1, 2, 3])
self.assertEqual(
client.request_json.call_args_list[1].args[0], "GetDocumentListById"
)
def test_recreates_expired_list_at_current_page(self):
client = CbdClient(requests_per_second=1000)
client.request_json = Mock(
side_effect=[
{
"Id": "expired-list",
"TotalCount": 3,
"Documents": [{"Code": 1}, {"Code": 2}],
},
urllib.error.HTTPError("https://example.test", 404, "", {}, None),
{
"Id": "new-list",
"TotalCount": 3,
"Documents": [{"Code": 3}],
},
]
)
self.assertEqual(list(client.document_codes(page_size=2)), [1, 2, 3])
recovery_call = client.request_json.call_args_list[2]
self.assertEqual(recovery_call.args[0], "GetDocumentListByQuery")
self.assertIn(("PageNumber", 2), recovery_call.args[1])
def test_formats_progress_with_rate_and_eta(self):
line = progress_line(
SyncResult(discovered=50, downloaded=48, skipped=1, failed=1),
total=100,
elapsed=25,
)
self.assertIn("50.00%", line)
self.assertIn("rate=2.00/s", line)
self.assertIn("ETA=00:00:25", line)
@patch("ingestion.minjust_cbd.time.sleep")
@patch("ingestion.minjust_cbd.urllib.request.urlopen")
def test_respects_retry_after_on_rate_limit(self, urlopen, sleep):
headers = Message()
headers["Retry-After"] = "7"
response = Mock()
response.read.return_value = b"{}"
response.__enter__ = Mock(return_value=response)
response.__exit__ = Mock(return_value=False)
urlopen.side_effect = [
urllib.error.HTTPError("https://example.test", 429, "", headers, None),
response,
]
CbdClient(requests_per_second=1000, retries=2).request_json("Example")
self.assertIn(((7.0,), {}), [(call.args, call.kwargs) for call in sleep.call_args_list])
if __name__ == "__main__":
unittest.main()

32
docs/README.md Normal file
View File

@@ -0,0 +1,32 @@
# Документация Акылдаш
## Продукт
- [Обзор проекта](product/project-overview.md) — назначение, основные области и
правила работы с данными.
## Решения
- [Решение 001](decisions/001-telegram-workspace-mvp.md) — границы MVP
Telegram-инфраструктуры.
## Эксплуатация и рабочий процесс
- [Текущий статус](operations/project-status.md) — выполненные шаги, риски и
ближайшие действия.
- [План Telegram-пространства](operations/telegram-workspace-plan.md) — темы,
роли и этапы развития командного окружения.
## Backend
- [Выгрузка ЦБД Минюста КР](../backend/README.md) — запуск, хранение и проверка
загрузчика правовых документов.
## Команда
- [Навыки работы с ИИ](team/ai-skills-for-beginners.md) — короткие практики для
начинающих.
---
Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан

View File

@@ -78,4 +78,4 @@ Telegram позволяет запретить пользователям отп
---
Акылдаш v0.2.1
Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан

View File

@@ -1,15 +1,18 @@
# Статус проекта
Последняя проверка: 2026-08-03
Последняя проверка: 2026-08-06
Назначение документа: быстро восстановить контекст проекта для участников команды и будущих агентов.
Версия приложения: `0.2.1`
- Telegram-бот: `0.2.2`
- Telegram-бот на Synology: `0.2.1`
- Backend: `0.1.2`
- Frontend: не создан
## Краткий итог
Telegram-инфраструктура и первая версия бота-секретаря подготовлены. Бот сохраняет сообщения разрешённых тем в SQLite, экспортирует обсуждения в Markdown и постоянно запущен в Container Manager на Synology.
Репозиторий переориентирован с отдельного бота на весь проект юридической информационно-аналитической платформы. Telegram-бот выделен в инструмент рабочего окружения. Создана первая backend-функция: возобновляемая выгрузка документов из официального Open Data API ЦБД Минюста КР.
Ближайшая цель — 2026-08-04 зафиксировать MVP с командой, настроить резервное копирование SQLite и затем проверить полный рабочий цикл на реальном обсуждении.
Ближайшая цель — расширить пилотную выборку ЦБД, затем добавить инкрементальную проверку sitemap при создании backend-планировщика.
## Уже сделано
@@ -36,8 +39,9 @@ Telegram-инфраструктура и первая версия бота-се
### Документация и правила
- Подготовлен общий план реализации в `TEAM_AI_TELEGRAM_IMPLEMENTATION_PLAN.md`.
- Зафиксированы границы MVP и правила работы с данными в `docs/decisions/001-mvp-boundaries-and-rules.md`.
- Подготовлен общий план реализации в `docs/operations/telegram-workspace-plan.md`.
- Зафиксированы границы MVP и правила работы с данными в `docs/decisions/001-telegram-workspace-mvp.md`.
- Добавлены обзор всей платформы и единый индекс документации.
- В MVP не входит автоматический вызов API языковой модели.
- Анализ экспортов выполняется вручную в обычном ChatGPT.
- Telegram используется как рабочий штаб, Git/Gitea — как источник утверждённых материалов.
@@ -52,7 +56,8 @@ Telegram-инфраструктура и первая версия бота-се
### Репозиторий и приложение
- Реализован бот-секретарь версии `0.2.1` без внешних Python-зависимостей.
- Реализован бот-секретарь версии `0.2.2` без внешних Python-зависимостей.
- Код бота выделен из корня репозитория в `tools/telegram-bot`.
- Сообщения и полные Telegram-метаданные сохраняются в SQLite.
- Добавлены команды `/help`, `/status` и `/export`.
- Экспорт доступен только владельцу, указанному в `TELEGRAM_OWNER_ID`.
@@ -60,13 +65,18 @@ Telegram-инфраструктура и первая версия бота-се
- Отчёты публикуются в закрытой теме `Отчеты` (`message_thread_id=37`).
- Исходное обсуждение завершается заметной отсечкой со ссылкой и хэштегом отчёта.
- Локальный Git-репозиторий восстановлен и привязан к Gitea.
- Репозиторий организован как основа всего проекта, а не отдельного бота.
- Бот развёрнут в Container Manager на Synology; автозапуск после перезапуска менеджера проверен.
- Реализован backend-загрузчик ЦБД Минюста КР версии `0.1.2` без внешних зависимостей.
- Загрузчик сохраняет метаданные, редакции RU/KY и изображения, а прогресс — в SQLite.
- Пилотная выгрузка двух документов и возобновление без повторного скачивания проверены на живом API.
### Развёртывание
- Бот постоянно запущен на Synology в контейнере `akyldash-bot`.
- Используется официальный образ `python:3.11-slim` и long polling.
- Контейнер работает без root, с read-only root filesystem и политикой `unless-stopped`.
- Доступ к Telegram Bot API идёт через отдельный закрытый прокси-контейнер без опубликованных наружу портов.
- Код, закрытый env-файл и SQLite хранятся в `/volume1/docker/akyldash`.
## Пока не сделано
@@ -118,8 +128,25 @@ Telegram-инфраструктура и первая версия бота-се
## История изменений статуса
### 2026-08-06
- Добавлен терминальный прогрессбар со скоростью и расчётным временем завершения.
- Обработка HTTP 429 учитывает рекомендованную сервером задержку `Retry-After`.
- Добавлено автоматическое восстановление после истечения серверного списка документов.
- Версия backend обновлена до `0.1.2`.
### 2026-08-05
- Создана область `backend/ingestion` для получения правовых источников.
- Реализована возобновляемая выгрузка ЦБД Минюста КР версии `0.1.0`.
- Проверены двуязычные редакции, изображения, SQLite-манифест и повторный запуск.
### 2026-08-03
- Репозиторий перестроен под весь проект юридической платформы; Telegram-бот перенесён в `tools/telegram-bot`.
- Созданы обзор продукта и структурированный индекс документации; будущие каталоги решено создавать по фактическим задачам.
- Закрытые юридические материалы решено хранить отдельно от основного репозитория.
- Версия Telegram-бота обновлена до `0.2.2` из-за изменения структуры запуска.
- Экспорт без параметров переведён с периода в семь дней на диапазон после предыдущей отсечки.
- Бот развёрнут в Container Manager на Synology; автозапуск проверен перезапуском.
- В теме `Работа с ИИ` опубликована первоначальная библиотека практик и отдельный материал о безопасной работе с Codex.
@@ -151,4 +178,4 @@ Telegram-инфраструктура и первая версия бота-се
---
Акылдаш v0.2.1
Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан

View File

@@ -398,4 +398,4 @@ Git сохраняет актуальную версию
---
Акылдаш v0.2.1
Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан

View File

@@ -0,0 +1,47 @@
# Обзор проекта
## Назначение
Акылдаш — юридическая информационно-аналитическая платформа. Она должна помочь
пользователю находить применимые нормы и материалы, понимать их актуальность и
получать ответ со ссылками на проверяемые первоисточники. Платформа не заменяет
профессиональное юридическое заключение.
## Основные области
1. Получение открытых или лицензированных законов, актов и иных правовых
источников.
2. Очистка, нормализация, связывание редакций и сохранение происхождения данных.
3. Хранение структурированных документов и поисковых индексов.
4. Формирование базы знаний и RAG с обязательными ссылками на источники.
5. Backend для доступа к данным и функциям платформы.
6. Frontend для поиска, анализа и работы с результатами.
7. Инструменты рабочего окружения команды, включая Telegram-бота.
Физическая структура каждой области появится вместе с её первой задачей. До
этого список служит картой продукта, а не обещанием заранее выбранной
архитектуры.
## Обязательные принципы
- Источник, дата получения, редакция и лицензия документа должны быть
прослеживаемыми.
- Ответ системы должен отделять найденный факт от вывода модели и ссылаться на
конкретный первоисточник.
- Актуальность правовых данных должна проверяться до использования в ответе.
- Существенные юридические выводы проверяет человек.
- Секреты и персональные данные не попадают в Git, логи и тестовые наборы.
- Данные загружаются только при наличии законного основания и с соблюдением
условий источника.
## Правовая защита проекта
Договоры, заявки, материалы об интеллектуальной собственности и другие
закрытые юридические документы следует хранить отдельно от исходного кода: в
закрытом репозитории или защищённом документном хранилище. Доступ выдаётся
поимённо и только тем, кому он нужен. В основном репозитории можно хранить
несекретный реестр документов, правила доступа и ссылки на место хранения.
---
Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан

View File

@@ -180,4 +180,4 @@
---
Акылдаш v0.2.1
Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан

View File

@@ -0,0 +1,51 @@
# Telegram-бот Акылдаш
Версия: `0.2.2`
Бот сохраняет сообщения разрешённых тем Telegram в локальную SQLite-базу и
выгружает обсуждения в Markdown. Внешние Python-зависимости не требуются.
## Запуск
Требуется Python 3.11 или новее.
```bash
cd tools/telegram-bot
export TELEGRAM_BOT_TOKEN='...'
export TELEGRAM_CHAT_ID='-1004242041275'
export TELEGRAM_OWNER_ID='87262245'
export TELEGRAM_REPORT_THREAD_ID='37'
export TELEGRAM_ALLOWED_THREAD_IDS='0,2,4,6,8'
python3 bot.py
```
Доступные команды: `/help`, `/status`, `/export`. Экспорт доступен только
пользователю с Telegram ID из `TELEGRAM_OWNER_ID`. Первый `/export` выгружает
всю сохранённую тему, последующие — сообщения после предыдущей успешно
созданной отсечки.
Markdown публикуется в теме `TELEGRAM_REPORT_THREAD_ID`, а в исходной теме
остаётся отсечка со ссылкой и общим хэштегом отчёта.
База по умолчанию хранится в `data/secretary.sqlite3`. Сообщения из других
групп и тем не сохраняются. Полный ответ Telegram сохраняется в базе, поэтому
метаданные вложений остаются доступными для последующего скачивания.
## Проверка
```bash
python3 -m unittest -v
```
## Synology
Контейнер `akyldash-bot` работает на образе `python:3.11-slim` через закрытый
прокси-контейнер, с политикой перезапуска `unless-stopped`. Постоянные данные
находятся в `/volume1/docker/akyldash/data`.
При обновлении развёртывания файл `tools/telegram-bot/bot.py` копируется в
каталог контейнера как `bot.py`.
---
Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан

View File

@@ -18,7 +18,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
APP_NAME = "Акылдаш"
APP_VERSION = "0.2.1"
APP_VERSION = "0.2.2"
FOOTER = f"{APP_NAME} v{APP_VERSION}"