Всем привет. Думаю, читатели моих двух предыдущих статей уже заметили одну привычку: если какую-то задачу можно хотя бы частично затащить в Obsidian, я попробую это сделать. Если нельзя, скорее всего, тоже попробую, просто потрачу больше времени.

Познакомился я с ним ещё в универе. Сначала зацепили граф связей и нормальная работа с LaTeX. Кто учил билеты по физике, тот поймёт: формулы, ссылки между темами и быстрый поиск реально спасали.

С вышматом вышло хуже. Возможностей встроенного LaTeX мне уже не хватало, поэтому часть билетов я всё равно расписывал в тетради. В общем, Obsidian с вышматом не справился. Я, если честно, тоже не сразу.

Но к тому моменту программа уже прижилась.

Позже я написал для неё отдельный плагин, который разбирал накопившееся хранилище: искал сиротские заметки, проблемы со структурой и неиспользуемые теги. Это было скорее про личную базу знаний и попытку разобраться, зачем я несколько лет складывал заметки куда придётся.

Теперь захотелось проверить, можно ли приспособить Obsidian ещё и к работе.

Немного контекста

Сейчас я пробую себя во фрилансе. Раньше занимался Java-backend, потом надолго выпал из разработки, успел поработать в продажах и прилично там перегореть.

Сразу возвращаться после такого перерыва на серьёзную backend-вакансию было бы тяжеловато, поэтому я начал с небольших заказов. Так можно постепенно восстановить форму и заодно посмотреть, какие задачи вообще есть на рынке.

Проблема обнаружилась быстро. Новые проекты на Kwork нужно постоянно проверять, а мне было банально лень каждые пять минут открывать биржу и листать выдачу. Kwork, к сожалению, не присылал отдельное уведомление о том, что появился идеальный заказ специально для меня.

В итоге откликался я редко, хорошие проекты уходили, а сама рутина раздражала сильнее, чем работа над заказом.

В какой-то момент стало понятно: либо я продолжаю вручную дежурить на бирже, либо перекладываю это на скрипт.

Первая версия: бот сам приносит новые заказы

Для начала я собрал связку из n8n и небольшого Python-скрипта. Скрипт работает через отдельную библиотеку для Kwork. У неё оказался вполне приличный набор запросов, поэтому писать всё с нуля не пришлось.

Схема такая:

  1. Python-скрипт получает новые заказы из выбранных категорий.

  2. Отправляет их в n8n через webhook.

  3. n8n разбивает общий массив на отдельные заказы с помощью Split Out.

  4. Пара JavaScript-нод считает оценку: учитывает бюджет, количество откликов, процент найма у заказчика и совпадение задачи с моим стеком.

  5. Готовый дайджест уходит в Telegram.

    Как это выглядит в workflow
    Как это выглядит в workflow

Запуском занимается маленький bash-скрипт на VPS. Раз в полчаса cron дёргает Python, после чего в Telegram прилетает новая пачка заказов со ссылками и оценками.

А это уже как результат выглядит в Telegram
А это уже как результат выглядит в Telegram

Уже на этом этапе стало заметно удобнее. Не нужно держать Kwork открытым в отдельной вкладке: пришло сообщение, посмотрел, откликнулся на подходящее.

Бот оказался дисциплинированнее меня. Немного обидно, зато полезно.

Основную проблему эта версия уже решала. Можно было пользоваться и больше ничего не трогать.

Но мне захотелось прикрутить сюда Obsidian.

Вторая версия: превращаем Obsidian в мини-CRM

Когда я думал, как добавить сюда ещё и Obsidian, вспомнил про плагин Kanban. Обычно я использовал его для обычных задач, но здесь доска почти сама просилась на роль простенькой CRM.

Колонки у меня получились такие:

Новые → Откликнулся → Ответили → Переговоры → Взял / Отказ

Каждый заказ превращается в отдельную карточку. Откликнулся, перетащил вправо. Заказчик ответил, сделал это ещё раз. Уже не нужно вспоминать, кому я писал три дня назад и почему в переписке внезапно обсуждается интеграция с 1С.

Никакого отдельного сервиса, авторизации в очередной системе и нового интерфейса. Просто доска внутри уже привычного хранилища.

Почти CRM. По крайней мере, вопрос «Где я вообще откликался?» она закрывала.

Теперь нужно было связать локальный Obsidian с сервером.

Для этого я использовал плагин Git. Сама доска хранится в файле kanban.md, который лежит в приватном репозитории на GitHub. Сервер добавляет туда новые карточки и делает push, а ноутбук подтягивает изменения через Obsidian Git.

И здесь появилась следующая проблема: n8n у меня работает в Docker-контейнере и напрямую к репозиторию на хосте не ходит.

Docker исправно изолировал контейнер. То есть сделал ровно то, что от него требовалось, и именно этим мне помешал.

Можно было пробросить внутрь контейнера каталог, поставить Git, передать SSH-ключ и потом следить, чтобы всё это не развалилось после очередного обновления. Вариант рабочий, но для одной простой операции настроек получалось многовато.

Я пошёл другим путём.

На хосте запустил небольшой Python-сервис, который принимает данные от n8n по HTTP. Его задача очень узкая: получить заказ, добавить карточку в kanban.md, сделать commit и push. Дальше он ничего не решает и ни во что не вмешивается.

Получился максимально тупой записчик. В данном случае это комплимент.

Записчик пришлось несколько раз переделать, в основном из-за автоматических Git-операций от имени сервиса. После пары неудачных вариантов и нескольких содержательных диалогов с терминалом схема всё-таки стала работать стабильно.

Архитектура целиком

Вся система выглядит так:

┌─────────────────┐         ┌──────────────┐         ┌─────────────────┐
│   VPS (сервер)  │         │    GitHub    │         │  Ноутбук (я)    │
│                 │         │  приватный   │         │                 │
│ Python-скрипт   │──push──▶│    репо      │◀──pull──│ Obsidian +      │
│ (cron, 30 мин)  │         │ (kanban.md)  │──push──▶│ плагины Git +   │
│ n8n + записчик  │◀────────│              │         │ Kanban          │
└─────────────────┘         └──────────────┘         └─────────────────┘
    пишет только                                       перетаскивает
   в колонку «Новые»                                  карточки по этапам

GitHub здесь находится посередине и работает как точка обмена между сервером и ноутбуком.

Канбан-доска целиком помещается в один Markdown-файл. Плагин Kanban хранит названия колонок как заголовки:

## Новые

Карточки лежат под ними как обычные элементы списка. Для Git в этом файле нет ничего особенного: набор строк, который можно коммитить, сравнивать и сливать.

В целом это одна из самых удобных черт Obsidian. Практически всё в нём можно представить в виде текстового файла. Канбан хранится в Markdown, а Canvas и Excalidraw тоже не запирают данные в полностью закрытом формате. Поэтому со своим хранилищем можно делать почти что угодно, даже если сам Obsidian в автоматизации напрямую не участвует.

Сервер добавляет новые карточки в колонку «Новые» и отправляет изменения в GitHub. На ноутбуке Obsidian Git делает pull, после чего заказы появляются на доске. Я двигаю их по этапам и пушу изменения обратно.

GitHub хранит актуальную версию доски, а Git возит изменения между сервером и ноутбуком.

Отдельный записчик остался на хосте не случайно. Там уже лежат репозиторий, Git и SSH-ключ. Переносить всё это внутрь n8n-контейнера мне не хотелось: больше настроек, больше мест, где можно ошибиться, да и ключ внутри контейнера становится ещё одной вещью, за которой нужно следить.

Проще оставить Git-часть на хосте, а из n8n отправлять обычный HTTP-запрос.

Грабли №1: IPv6 в Docker сломал отправку в Telegram, а ошибка обвинила DNS

Это была самая неприятная проблема, потому что сообщение об ошибке уверенно уводило совсем в другую сторону.

В какой-то момент Telegram-нода в n8n начала падать с текстом:

The DNS server returned an error, perhaps the server is offline

Ошибка была совершенно уверена, что виноват DNS. DNS, к моему удивлению, в этот раз оказался невиновен.

Сначала я решил, что сломался DNS или сам Telegram временно недоступен. С DNS я ненавижу возиться, поэтому надеялся на второе. Но из контейнера api.telegram.org нормально резолвился.

Правда, первым возвращался IPv6-адрес, запись AAAA вида 2001:67c:.... С IPv4 всё выглядело страннее: обычный запрос отдавал No answer.

Причина оказалась в Docker-сети. У неё не было рабочего IPv6-маршрута наружу. Node.js получал IPv6-адрес, пытался подключиться по нему и упирался в отсутствие маршрута.

n8n при этом показывал ошибку про DNS, хотя DNS свою работу выполнил: имя нашёл и адрес вернул. Соединение ломалось уже после этого.

Для проверки я отдельно запросил IPv4-адрес и попробовал обратиться к Telegram из контейнера:

docker exec <n8n_container> sh -c \
  "nslookup -type=A api.telegram.org; wget -qO- --timeout=5 https://api.telegram.org | head -c 80"

IPv4-адрес нашёлся: это был адрес из диапазона 149.154.x.x. Через него wget смог получить HTML-страницу.

Значит, интернет из контейнера был, Telegram отвечал, проблема оставалась только в выборе адреса.

Исправление уместилось в одну переменную окружения. В compose.yml для n8n я добавил:

environment:
  - NODE_OPTIONS=--dns-result-order=ipv4first

После пересоздания контейнера Node.js начал предпочитать IPv4, и Telegram снова заработал.

Одна строка в конфиге и несколько часов диагностики. Всё как обычно.

Кстати, запросы к записчику всё это время проходили без ошибок. Я обращался к нему по числовому IP шлюза Docker-сети, поэтому DNS в этой цепочке вообще не участвовал.

Получалась странная картина: одна HTTP-нода работает, соседняя падает с DNS error. Из-за этого я и потратил на диагностику больше времени, чем рассчитывал.

Теперь, если сервис внутри Docker внезапно «не резолвится», хотя имя нормально разрешается и снаружи всё доступно, я отдельно проверяю, какой адрес выбирает приложение и есть ли у контейнера маршрут до него.

Грабли №2: сервер и Obsidian меняют один файл

Со второй проблемой всё было понятнее. Сервер добавляет карточки в kanban.md, а я в это же время двигаю их между колонками. Рано или поздно Git должен был получить две версии одного файла и попросить разобраться с конфликтом вручную.

А вручную сливать канбан-доску, которую параллельно редактирует скрипт, мне хотелось меньше всего. Я всё-таки автоматизировал процесс, чтобы работать меньше, а не чтобы получить новое хобби.

Git не видит в kanban.md ни карточек, ни статусов. Для него это обычный текст, который он сравнивает построчно. Поэтому шанс конфликта зависит от того, насколько близко друг к другу находятся изменения.

Я разделил зоны ответственности:

  • сервер добавляет карточки только сразу под заголовком ## Новые;

  • существующие карточки он не редактирует;

  • я переношу заказы из «Новых» в остальные колонки и уже там работаю с ними вручную.

Так изменения сервера в основном остаются в верхней части секции «Новые», а мои распределяются по другим участкам файла. Стопроцентной защиты это не даёт, но конфликтов стало заметно меньше.

Есть и неприятный крайний случай. Перенос карточки меняет сразу два места: строка исчезает из «Новых» и появляется в другой колонке. Если сервер в тот же момент вставляет новые строки рядом, Git всё ещё может запутаться.

Поэтому перед ручной работой я стараюсь сначала сделать pull, а после изменений сразу push. Не оставляю изменённую доску лежать локально несколько часов, пока сервер продолжает добавлять новые карточки.

На стороне записчика я тоже убрал лишнюю магию. Сначала перед вставкой он выполнял:

git pull --rebase

Периодически это заканчивалось ошибками, особенно когда локальная копия и удалённая ветка расходились не так, как ожидал скрипт.

В итоге я заменил rebase на две команды:

git fetch origin main
git reset --hard origin/main

Для обычной ручной работы такой подход опасен. reset --hard относится к тем командам, после которых человек внезапно начинает вспоминать, точно ли он сделал commit.

Но записчик не хранит собственного состояния. Перед каждым заказом он может выбросить локальную копию, взять актуальный main с GitHub и уже поверх него добавить новую карточку.

Алгоритм получился простой:

  1. Синхронизироваться с GitHub.

  2. Вставить карточку.

  3. Сделать commit.

  4. Выполнить push.

Если push не проходит из-за того, что за эти несколько секунд в репозитории появились новые изменения, операцию можно повторить уже на свежей версии файла.

Для такого маленького сервиса это оказалось проще, чем учить его автоматически разбираться с rebase и конфликтами.

Что получилось

Теперь сервер раз в полчаса проверяет Kwork, оценивает новые заказы и отправляет дайджест в Telegram. Подходящие проекты одновременно появляются в колонке «Новые» внутри Obsidian.

Дальше всё вручную. Откликнулся, передвинул карточку. Получил ответ, перенёс её ещё на один этап. Взял заказ или получил отказ, отправил в нужную колонку.

Это, конечно, не Salesforce. Зато система не просит создать семь сущностей и заполнить обязательное поле «Источник лида», прежде чем я смогу добавить одну карточку.

Биржу больше не нужно обновлять каждые пять минут. Obsidian остался обычным локальным хранилищем, просто теперь рядом с заметками в нём живёт рабочая доска.

Пока система довольно простая. Она забирает на себя ту часть работы, которую мне самому делать лень, и этого уже достаточно.

Если будет интересно, в следующей статье можно отдельно разобрать оценку заказов, защиту от дублей и мобильную синхронизацию через Git.