The Telegram Desk product — the pattern this starter ships for turning a messenger into a personal memory: voice and files come in, meaning is extracted, everything lands in four stores at once, and reminders fire on their own. Load it before touching anything under lib/products/telegram-desk, when the owner wants a bot that remembers, when a message arrives and nothing happens, and before adding any new kind of incoming message. Two things decide the whole design and neither is visible from ...
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
Are you the author of Use Telegram?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/fractera-use-telegram)
---
name: use-telegram
description: >
The Telegram Desk product — the pattern this starter ships for turning a messenger into a personal
memory: voice and files come in, meaning is extracted, everything lands in four stores at once, and
reminders fire on their own. Load it before touching anything under lib/products/telegram-desk, when
the owner wants a bot that remembers, when a message arrives and nothing happens, and before adding
any new kind of incoming message. Two things decide the whole design and neither is visible from the
code: one model call asked to do six jobs performs five of them silently — so intent is routed first
and each branch gets its own short prompt; and a reply is not politeness but a receipt, showing the
same summary that went into the database.
---
# use-telegram
> Informational, not binding. **Know a better way for the case in front of you — do it your way and
> say so.** You are trusted with the creative work on this project.
**Это ПРОДУКТ, а не машинерия платформы.** Он едет в стартере так же, как блог: агент в проекте
клиента копирует его устройство под чеки, полевые заявки, заказы — под всё, что человеку проще
сказать, чем напечатать. Транспорт под ним — `use-channels`.
---
## 1. Устройство: два процесса, пять складов
```
человек → Telegram → служба каналов (:3500, единственный читатель бота)
│ push ↑ send / sendFile
▼ │
POST /api/telegram/hook ──────────────┘
│
┌─────────────────┼──────────────────┬───────────────┐
▼ ▼ ▼ ▼
tgdesk_* (база) медиатека векторы граф знаний
```
🔒 **Служба — единственный читатель бота.** Telegram отдаёт обновление ровно ОДНОМУ спрашивающему;
второй опрос не добавляет возможность, а молча съедает половину сообщений. Приложение никогда не ходит
в Telegram само — ни за обновлениями, ни за файлом.
**Четыре таблицы** (`lib/db/index.ts`, приставка `tgdesk_`):
| Таблица | Держит |
|---|---|
| `tgdesk_messages` | оркестрирующая: время сказанного и время СОБЫТИЯ, направление, текст, сводка, гео, род вложения, флаг денег, связка, заметки об отказах |
| `tgdesk_artifacts` | связь многие-ко-многим со складами: `media` (имя файла) · `vector` (id) · `rag` (источник) |
| `tgdesk_entries` | смысл: заметка, задача, чек, место, идея — со статусом подтверждения и валютой |
| `tgdesk_calendar` | напоминания и встречи: разовые, повторяющиеся, с предупреждением заранее |
🔒 **Артефакты вынесены в таблицу, а не в колонки.** Одно сообщение несёт три фотографии и голос:
колонка `media_id` ломается на второй фотографии, а таблица держит любое их число.
## 2. 🔒 Ветвление — часть логики, а не строчка в промпте
**Одна инструкция, просящая модель сделать шесть дел разом, исполняется на пять.** ✗ четыре просьбы
подряд «напомни через минуту» стали заметками: модель не отказывалась — она просто не заполняла одно
поле. Ни ошибки, ни отказа, просто отсутствующий ключ. Тот же вопрос коротким промптом отвечался
безошибочно.
Поэтому шаг первый отвечает на единственный вопрос — **что это за просьба**, — и только потом работает
своя короткая инструкция:
| Ветвь | Файл | Вызовов модели |
|---|---|---|
| `command` (`/start`) | статический текст | **0** |
| `confirm` («да», «нет») | сравнение слов | **0** |
| `where` (город → зона) | `branches/where.ts` | 1 |
| `meta` («кто ты») | `branches/meta.ts` — личность, **без базы** | 1 |
| `schedule` | `branches/schedule.ts` | 1 |
| `correct` (поправка) | `branches/correct.ts` | 1 |
| `question` | `answer.ts` | 1 |
| `capture` | `branches/capture.ts` | 1 |
🔒 **Маршрутизатор возвращает ТОЛЬКО ветвь.** Соблазн «заодно верни и сводку» и есть та самая болезнь:
как только у вызова появляется второе дело, одно из двух начинает пропадать.
## 3. 🔒 Правила связности: что с чем связано
Ответ обязан находить свой вопрос. Правил четыре, живут в `calendar.ts`:
1. **Открытое ожидание сильнее времени.** Спросили «ставлю?» — ответ относится к этому вопросу,
сколько бы ни прошло: человек отвлёкся, а не передумал. ✗ «Да» через пять минут стало отдельной
заметкой, потому что связь искали окном в три минуты — а окно тут ни при чём.
2. **Окно в три минуты — про сообщения ПОДРЯД** (пересылка с пояснением). Другой род связи, первый не
заменяет.
3. **Ожидание не вечно** — через сутки протухает: человек уже не помнит, о чём спрашивали.
4. **Ожидание одно.** Новое предложение отменяет прежнее неотвеченное, иначе «да» уходит в позавчерашнее.
🔒 **Слабой модели мало знать, что «что-то ждёт», — ей нужен ПРЕДМЕТ.** В подсказку идёт «предложено:
встреча по адресу на 24 августа 16:42 — подтвердить время». Тогда «12:00» очевидно поправляет
предложенное, а не заводит вторую встречу.
🔒 **Связка не склеивает записи.** Каждое сообщение остаётся своей строкой со своими артефактами:
слитые, они потеряли бы возможность найтись по отдельности. Связка — это ссылка.
**Соглашение о пересылке.** Telegram отдаёт автора тремя поколениями полей, и все живы
(`forward_origin`, `forward_from`, `forward_sender_name`). Когда автор скрыт настройками, человек
пишет перед пересылкой «Сообщение от Имя Фамилия» — связка и закон атрибуции переносят имя на
следующее сообщение.
## 4. 🔒 Конверт: почему в граф идёт не голый текст
Каждое сообщение уходит в граф **обёрнутым**: источник, отправитель, дата разговора, дата события,
признаки, автор пересылки, содержимое вложения — и текст последней строкой.
**Граф питается сущностями и связями**, а не буквами: голое «купил ноут» не даёт ему ни одной опоры.
Приставка источника (`telegram/msg-<id>`) отделяет канал от всего, что владелец грузит руками, и
позволяет удалить переписку одной операцией.
**Цена названа честно:** каждое сообщение стоит построения графа. Владелец выбрал полноту, а не порог.
## 5. 🔒 Ответ — расписка, а не вежливость
Что бы ни пришло, ответ показывает **ту самую сводку**, что легла в базу, и **тот самый конверт**, что
ушёл в граф. Человек ловит ошибку в ту же секунду, а не через месяц, когда ищет и не находит.
**Печатается только то, что действительно записано.** Строка «в графе знаний» при неудавшейся загрузке
— ложь, которую обнаружат месяцем позже и уже не свяжут с этим ответом. Файл сохранён, но не прочитан
— так и пишется.
**Деньги и время подтверждаются.** Цифра, записанная неверно, всплывает при подсчёте, когда проверять
поздно. У чека **дата обязательна**: не разобрали — ставим сегодняшнюю и говорим об этом вслух.
Валюта — с чека, а не видно — из `APP-CONFIG`, и это тоже называется.
## 6. Файлы: что читается, а что нет
| Род | Что происходит |
|---|---|
| картинка | медиатека + описание зрячей моделью; **текст на ней перечисляется первым и дословно** |
| голос и звуковой файл | медиатека + расшифровка |
| `txt`, `csv`, `md` | медиатека + чтение как есть |
| PDF | **сохраняется, не читается** — библиотеки в проекте нет |
| видео | **отложено владельцем**: частный случай, разбор дорогой |
🔒 **Разрешение задаётся явно (`detail: "high"`).** ✗ по умолчанию действует `auto`, и для крупной
картинки выбирается низкое: мелкий текст в нём физически не читается, сколько его ни проси. Это про
то, что модель ВИДИТ, а промпт тут бессилен.
🔒 **Файл берётся у службы по петле, а не ссылкой Telegram** — в той ссылке лежит токен бота целиком.
🔒 **Вложение забирается ДО разбора.** Прочитанное с картинки — часть того, что человек сказал: «вот
чек» без суммы это не запись о трате, а бессмысленная строка.
## 7. Время: часовой пояс человека
Сервер живёт в UTC и о человеке не знает ничего: ни Telegram, ни браузер пояса не сообщают. Пока он
неизвестен, «завтра в десять» ставится по Гринвичу — и человек замечает это ровно один раз, проспав
встречу.
Продукт **спрашивает город сам** при первом напоминании и **пишет ответ в `APP-CONFIG`**.
🔒 Это отступление от закона «панель пишет, приложение читает», принятое владельцем: до поля в
настройках мало кто дойдёт, а пояс, о котором не спросили, остаётся пустым навсегда.
🔒 **Спрашиваем ГОРОД, а не зону и не сдвиг.** «UTC+2» врёт полгода из двенадцати, имя зоны — никогда.
Преобразование через `Intl`, без библиотек, и смещение берётся **на ту же дату**: напоминание на конец
октября, поставленное в августе, иначе уедет на час.
**Уже поставленные напоминания новый пояс задним числом НЕ двигает** — это меняло бы то, что человек
подтвердил.
## 8. Часовая стрелка
Планировщика у платформы нет. Напоминание обязано сработать, когда на сайт никто не смотрит, поэтому
служба каналов бьёт в дверь `POST /api/telegram/tick` по расписанию из панели (выключено · минута ·
пять · пятнадцать · час). Что наступило — решает продукт.
🔒 **Ищутся ПРОСРОЧЕННЫЕ, а не «ровно эта минута»:** сервер перезагрузился — напоминание догонит себя
само. И **следующее срабатывание считается от прошлого**, а не от «сейчас»: иначе опоздавшее на час
напоминание навсегда сдвигает своё время, и «в девять утра» через неделю становится полднем.
## 9. Свежий сервер: что настраивается само, а что нет
Адреса дверей и шаг расписания служба подставляет сама — они одинаковы на каждом сервере. **Вручную
остаётся одно: `TELEGRAM_HOOK_SECRET` в `.env.local` слота.** Нет секрета — двери приложения нет, и
бот честно работает по-старому, отвечая из базы знаний; режим при этом выводится, а не хранится.
## 10. Четыре ловушки, каждая уже оплачена
- ✗ **`\b` не знает кириллицы.** Граница слова в JavaScript — это `[A-Za-z0-9_]`; после «да» её нет, и
правило не совпадает никогда. Английское «ok» работало, русское «да» — нет. Дефект прятался за
прежним умолчанием «иначе да»: две ошибки компенсировали друг друга.
- ✗ **Умолчанием подтверждения было «да».** Любое сообщение при ожидающем напоминании молча его
подтверждало.
- ✗ **Сквозной прокси слоя данных читал ответ как текст.** Двоичное приезжало мёртвым: `FF D8` →
`EF BF BD`, зрячая модель отвечала «unsupported image», а файл в медиатеке был мусором. Починено в
платформе.
- ✗ **Слой данных не возвращает `lastInsertRowid`.** Код, написанный по локальной базе, на сервере
получил бы `NaN` и связал артефакты с несуществующим сообщением. Идентификатор читается обратно.
## 11. Перед тем как сказать «работает»
1. Сообщение прошло **живьём**, и вы прочитали `tgdesk_messages`, `tgdesk_artifacts`, `tgdesk_entries`.
2. Колонка `notes` пуста — она и заведена затем, чтобы отказы не были невидимыми.
3. Негативный контроль: чужой секрет → 401 и ни одной строки в базе.
4. Что НЕ проверено — названо вслух. Отчёт о сборке доказательством не является.