cookbook v1.0.0 [dev]


Эталонный модуль-справочник — показывает все ключевые паттерны разработки модулей bot_modular.

versiondatecommitфайлов
1.0.02026-09-162d7d04615b3112

README

# cookbook `1.0.0` — эталонный модуль-справочник bot_modular

Полный пример всех ключевых паттернов разработки модулей. Каждый файл модуля
прокомментирован и объясняет свой контракт с ядром.

## Жизненный цикл модуля

```
loader.discover()          → сканирует modules/*/manifest.{yaml,json}
modstate.is_enabled()      → default-off для новых (включить через modsafe)
loader.load_module()       → импортирует module.py, создаёт Module()
  ├─ manifest.validate()   → обязательные поля: name, version, description
  ├─ requires.resolve()    → ждёт зависимости (requires: [...])
  ├─ rights.register()     → регистрирует права из manifest.yaml
  └─ module.setup(ctx)     # ← регистрация API, событий, UI
transport.start()          → после всех модулей
  └─ module.start()        # ← фоновые потоки, подключения
module.stop()              # ← при shutdown
```

## Контракты вкладов

### 1. module.py — ядро модуля

```python
class Module(BaseModule):
    def setup(self, ctx) -> None:
        """Однажды при загрузке. НЕ поднимаем потоки."""
        self.config = ctx.module_config("cookbook")  # defaults < data/env/*.env < overrides
        self.cache = ctx.cache.ns("cookbook")        # изолированный кеш
        self.engine = Engine(self.config, self.cache, ctx)  # бизнес-логика

        # Регистрация API-методов (вызываются: ctx.api.call("cookbook.метод"))
        ctx.api.register("cookbook", "ping", self._api_ping)

        # Подписка на события (публикуются: ctx.events.emit("cookbook.tick"))
        ctx.events.subscribe("cookbook.tick", self._on_tick)

    def start(self) -> None:
        """После setup() всех модулей. Здесь — фоновые потоки."""
        self._thread = threading.Thread(target=self._loop, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        """При shutdown."""
        self._stop_event.set()
        self._thread.join(timeout=5)

    def health(self) -> dict:
        """Для --check и панели мониторинга."""
        return {"ok": True, "module": self.name, "counter": self.engine.count}
```

### 2. engine.py — бизнес-логика

Вынесена из module.py для разделения ответственности:
- module.py — жизненный цикл, регистрация
- engine.py — состояние, вычисления

```python
class Engine:
    def __init__(self, config, cache, ctx):
        self.config = config  # ModuleConfig
        self.cache = cache    # CacheService.Namespace
        self.ctx = ctx        # CoreContext

    @property
    def count(self):
        return self._count

    @count.setter
    def count(self, value):
        self._count = value
        self.cache.set("counter", {"count": value})  # автосохранение
```

### 3. ui_tg.py — Telegram-интерфейс

```python
# Команды бота (строка = публичная, словарь = полный контроль)
TG_COMMANDS = {
    "ping": "проверить живость",
    "counter": {"desc": "счётчик", "right": None, "menu": True},
}

# Кнопки модуля (колбэки: <модуль>:<action>)
TG_CALLBACKS = {
    "increment": {"desc": "+1", "right": None},
}

# Кнопки /start (главное меню)
TG_MENU = [
    {"id": "cookbook_main", "title": "📖 Cookbook", "callback": "counter"},
]

def handle_command(ctx, config, command, text, inv=None):
    # inv = {"chat_id": int, "user_qid": "telegram:<id>"}
    if command == "ping":
        return "🟢 " + ctx.api.call("cookbook.ping")
    return None

def handle_callback(ctx, config, action, args):
    # args = {"chat_id", "message_id", "message_text", "user_qid"}
    # Возврат: str (edit), ("edit", t), ("send", t), ("toast", t), None
    if action == "increment":
        return ("toast", "+1")
    return None
```

### 4. ui_web.py — Web-интерфейс

```python
# REST API (путь обязан начинаться с /api/<mod>/)
ROUTES = [
    ("GET", "/api/cookbook/ping", "ping", {"right": None}),
    ("POST", "/api/cookbook/reset", "reset", {"right": None}),
    ("GET", "/api/cookbook/private", "private", {"right": "cookbook_private"}),
]

# Карточка в кабинете (/api/services)
SERVICE = {
    "key": "cookbook", "icon": "📖", "title": "Cookbook",
    "right": None, "phase": 1, "desc": "Эталон модуля",
}

# Панели мониторинга (/api/monitoring/layout)
MONITOR_PANELS = [
    {"id": "cookbook_counter", "title": "Счётчик",
     "api": "/api/cookbook/counter", "visibility": "public", "refresh_s": 5},
]

# Навигация шапки SPA (/api/nav)
NAV = [
    {"id": "cookbook", "title": "📖 Cookbook", "view": "cookbook", "icon": "📖"},
]

def handle_api(ctx, config, method, req):
    # req = {"http_method", "path", "query", "body", "files",
    #        "user_qid", "session", "remote_ip"}
    from core.errors import UserError

    if method == "ping":
        return {"ok": True, "pong": True}  # → 200

    if method == "reset":
        body = req.get("body") or {}
        ctx.api.call("cookbook.reset")
        return {"ok": True}  # → 200

    if method == "private":
        # Право проверяется транспортом ДО handle_api
        return {"ok": True, "secret": "доступ разрешён"}

    raise UserError("неизвестный метод")  # → 400
```

### 5. ui_ws.py — WebSocket + `web/` — кабинет модуля

```python
WS_PATH = "/ws/cookbook/ticker"  # обязан начинаться с /ws/

Кабинет — два файла, подхватываются транспортом сами (в `app.js` ядра
ничего дописывать не надо):
- `web/view.html` — `<section id="view-<view>">` с разметкой;
- `web/view.js` — `loadFn` + саморгистрация:
  `BotUI.registerService('<ключ SERVICE>', '<view>')` (карточка кабинета →
  вьюха) и `BotUI.registerView('<view>', loadFn)`. Рендерер своей панели —
  `BotUI.registerPanelRenderer('<id панели>', fn)` в том же файле.

from botmod_transport_web.ws import make_base as _make_base
BaseSocket = _make_base()

class WSSocket(BaseSocket):
    RIGHT = "cookbook_manage"  # право для подключения

    def on_authed(self, sess):
        # self.mod_ctx — CoreContext
        # self.ws_sess — данные сессии
        self.write_message(json.dumps({"type": "connected"}))

    def on_message(self, raw):
        msg = json.loads(raw)
        if msg["type"] == "ping":
            self.write_message(json.dumps({"type": "pong"}))

    def on_close(self):
        # Очистка
        pass
```

### 6. manifest.yaml — манифест модуля

```yaml
name: cookbook                    # имя = имя папки (обязательно)
version: 1.0.0                    # семантическое.version
category: dev                     # core | base | extra | dev
description: Эталонный модуль...

# API-методы для /api/registry
api:
  - cookbook.ping
  - cookbook.counter

# События
events_emitted:
  - cookbook.tick
events_subscribed:
  - cookbook.tick

# TG-команды (для справки)
tg_commands:
  - ping
  - counter

# Web-роуты (для справки)
web_routes:
  - GET /api/cookbook/ping
  - GET /api/cookbook/private (right cookbook_private)

# WS-пути (для справки)
ws_routes:
  - /ws/cookbook/ticker

# Подавление ложных срабатываний аудита
audit_allow:
  - NET_IMPORT

# Права модуля (декларация)
rights:
  - name: cookbook_private
    description: Доступ к приватному API
  - name: cookbook_manage
    description: Управление настройками

# Файлы
config_schema: config.schema.json
api_methods:
  - method: ping
    args: "{}"
    returns: "str — 'pong'"
    desc: Проверка живости
env_example: .env.example
```

### 7. config.schema.json — схема конфигурации

```json
{
  "required": [],
  "optional": {
    "FACT_INTERVAL": "60",
    "COUNTER_TICK_INTERVAL": "1"
  }
}
```

Слои значений: `defaults` < `data/env/<name>.env` < `data/runtime_config.json`

### 8. cache.py — кеш-неймспейсы

```python
def counter_ns(ctx):
    return ctx.cache.ns("cookbook")  # data/cache/cookbook/*.json
```

Каждый модуль видит только свой namespace. Файловая блокировка flock.

## Взаимодействие с другими модулями

### Вызов API-метода
```python
result = ctx.api.call("cookbook.ping")
```

### Подписка на события
```python
ctx.events.subscribe("cookbook.tick", self._on_tick)
```

### Проверка права
```python
if ctx.rights.can("telegram:123", "cookbook_private"):
    ...
```

### Использование кеша
```python
ns = ctx.cache.ns("cookbook")
ns.set("key", {"data": "value"})
data = ns.get("key")
data = ns.get_ttl("key", 60)  # с TTL
```

## Типы ошибок

```python
from core.errors import UserError, ConfigError, UpstreamError

# UserError → 400 (плохой ввод, показать пользователю)
raise UserError("неизвестный метод: %s" % method)

# ConfigError → fail-fast при setup (чинить руками)
raise ConfigError("нет обязательного ключа X")

# UpstreamError → 500 (апстрим недоступен, можно ретраить)
raise UpstreamError("ollama недоступен")
```

## Проверка модуля

```bash
# Проверка без запуска
python3 boot.py --check

# Список модулей
python3 boot.py --list-modules

# Вызов API-метода
python3 boot.py --call cookbook.ping

# Runtime override
python3 boot.py --set-override cookbook.FACT_INTERVAL 30
python3 boot.py --clear-override cookbook.FACT_INTERVAL
```

## Создание нового модуля

1. Скопировать `modules/cookbook/` в `modules/mynew/`
2. Переименовать все файлы (cookbook → mynew)
3. Обновить `manifest.yaml` (name, version, description)
4. Обновить `module.py` (имя класса, API-методы)
5. Обновить `ui_tg.py`, `ui_web.py`, `ui_ws.py`
6. Включить через modsafe (web/API)
7. `python3 boot.py --check`

## Зависимости между модулями

Поле `requires` в manifest.yaml:
```yaml
requires:
  - access      # загрузится после access
  - models      # загрузится после models
```

Зависимости разрешаются до setup(). Нет зависимости — модуль пропускается.
Циклы — тоже пропуск (без падения бота).

## Аудит модуля

Ядро сканирует файлы модуля по эвристикам:
- critical: eval, exec, compile, os.system, shell=True, pickle, marshal, ctypes
- high: yaml.load, shutil.rmtree, хардкод секретов, os.exec/popen/fork
- medium: динамический импорт, base64-декодирование, сетевые импорты
- low: чтение окружения, запись файлов

Подавление ложных срабатываний:
```yaml
audit_allow:
  - NET_IMPORT   # сетевой импорт легитимен
  - SUBPROC      # subprocess нужен для вызова внешних программ
```

Хеши файлов: baseline при первом запуске, дальше — detect изменений (changed/new/missing).

Манифест

{
  "name": "cookbook",
  "version": "1.0.0",
  "category": "dev",
  "description": "Эталонный модуль-справочник — показывает все ключевые паттерны разработки модулей bot_modular.",
  "requires": [],
  "rights": [],
  "api_methods": [],
  "web_routes": [],
  "commit": "2d7d04615b31be9f5bbd91d5d61671182164bf39",
  "updated": "2026-09-16 16:35:50 +0300",
  "integrity": "sha256:c265cc1b2719960864c2d0bf6049fcc70e55ac01f9bad97d0a09962c8faf930a",
  "tree_url": "http://10.10.10.30:3000/euk0r/bot_modular/src/branch/main/modules/cookbook",
  "raw_url": "http://10.10.10.30:3000/euk0r/bot_modular/raw/branch/main/modules/cookbook/module.py",
  "files": "[12 файлов — см. вкладки ниже]",
  "note": "Эталонный модуль для примера",
  "_extra": {
    "api": "",
    "events_emitted": "",
    "events_subscribed": "",
    "tg_commands": "",
    "ws_routes": "",
    "audit_allow": "",
    "config_schema": "config.schema.json",
    "env_example": ".env.example"
  }
}

Файлы и исходники

Дерево файлов

  • · корень
    • 12.1 КБ
    • 0.7 КБ
    • 0.2 КБ
    • 5.2 КБ
    • 1.9 КБ
    • 10.8 КБ
    • 0.2 КБ
    • 7.8 КБ
    • 10.6 КБ
    • 7.2 КБ
  • web/
    • 1.5 КБ
    • 2.6 КБ

Предпросмотр

Выберите файл в дереве выше — код откроется здесь.