Всем привет. Думаю, читатели моих двух предыдущих статей уже заметили одну привычку: если какую-то задачу можно хотя бы частично затащить в Obsidian, я попробую это сделать. Если нельзя, скорее всего, тоже попробую, просто потрачу больше времени.
Познакомился я с ним ещё в универе. Сначала зацепили граф связей и нормальная работа с LaTeX. Кто учил билеты по физике, тот поймёт: формулы, ссылки между темами и быстрый поиск реально спасали.
С вышматом вышло хуже. Возможностей встроенного LaTeX мне уже не хватало, поэтому часть билетов я всё равно расписывал в тетради. В общем, Obsidian с вышматом не справился. Я, если честно, тоже не сразу.
Но к тому моменту программа уже прижилась.
Позже я написал для неё отдельный плагин, который разбирал накопившееся хранилище: искал сиротские заметки, проблемы со структурой и неиспользуемые теги. Это было скорее про личную базу знаний и попытку разобраться, зачем я несколько лет складывал заметки куда придётся.
Теперь захотелось проверить, можно ли приспособить Obsidian ещё и к работе.
Немного контекста
Сейчас я пробую себя во фрилансе. Раньше занимался Java-backend, потом надолго выпал из разработки, успел поработать в продажах и прилично там перегореть.
Сразу возвращаться после такого перерыва на серьёзную backend-вакансию было бы тяжеловато, поэтому я начал с небольших заказов. Так можно постепенно восстановить форму и заодно посмотреть, какие задачи вообще есть на рынке.
Проблема обнаружилась быстро. Новые проекты на Kwork нужно постоянно проверять, а мне было банально лень каждые пять минут открывать биржу и листать выдачу. Kwork, к сожалению, не присылал отдельное уведомление о том, что появился идеальный заказ специально для меня.
В итоге откликался я редко, хорошие проекты уходили, а сама рутина раздражала сильнее, чем работа над заказом.
В какой-то момент стало понятно: либо я продолжаю вручную дежурить на бирже, либо перекладываю это на скрипт.
Первая версия: бот сам приносит новые заказы
Для начала я собрал связку из n8n и небольшого Python-скрипта. Скрипт работает через отдельную библиотеку для Kwork. У неё оказался вполне приличный набор запросов, поэтому писать всё с нуля не пришлось.
Схема такая:
Python-скрипт получает новые заказы из выбранных категорий.
Отправляет их в n8n через webhook.
n8n разбивает общий массив на отдельные заказы с помощью Split Out.
Пара JavaScript-нод считает оценку: учитывает бюджет, количество откликов, процент найма у заказчика и совпадение задачи с моим стеком.
Готовый дайджест уходит в Telegram.

Как это выглядит в workflow
Запуском занимается маленький bash-скрипт на VPS. Раз в полчаса cron дёргает Python, после чего в 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 и уже поверх него добавить новую карточку.
Алгоритм получился простой:
Синхронизироваться с GitHub.
Вставить карточку.
Сделать commit.
Выполнить push.
Если push не проходит из-за того, что за эти несколько секунд в репозитории появились новые изменения, операцию можно повторить уже на свежей версии файла.
Для такого маленького сервиса это оказалось проще, чем учить его автоматически разбираться с rebase и конфликтами.
Что получилось
Теперь сервер раз в полчаса проверяет Kwork, оценивает новые заказы и отправляет дайджест в Telegram. Подходящие проекты одновременно появляются в колонке «Новые» внутри Obsidian.
Дальше всё вручную. Откликнулся, передвинул карточку. Получил ответ, перенёс её ещё на один этап. Взял заказ или получил отказ, отправил в нужную колонку.
Это, конечно, не Salesforce. Зато система не просит создать семь сущностей и заполнить обязательное поле «Источник лида», прежде чем я смогу добавить одну карточку.
Биржу больше не нужно обновлять каждые пять минут. Obsidian остался обычным локальным хранилищем, просто теперь рядом с заметками в нём живёт рабочая доска.
Пока система довольно простая. Она забирает на себя ту часть работы, которую мне самому делать лень, и этого уже достаточно.
Если будет интересно, в следующей статье можно отдельно разобрать оценку заказов, защиту от дублей и мобильную синхронизацию через Git.
