Obsidian как мини-CRM: как я научил канбан-доску наполняться заказами с Kwork без моего участия
Всем привет. Из двух прошлых статей вы могли заметить, что к Obsidian у меня особая симпатия — почти всё, чем занимаюсь, я стараюсь на него как-то завязать.
Познакомился с ним ещё в универе. Зацепили графы и плагин для LaTeX. Кто учил билеты, тот поймёт: по физике это выручало здорово. С вышматом не сложилось — там мне LaTeX уже не хватало, и билеты я в итоге всё равно расписывал в тетради. Ну да ладно, это отдельная тема.
В прошлый раз я писал плагин для разбора своего хранилища — это было больше про досуг и ежедневные заметки. Сейчас захотелось другого: посмотреть, чем Obsidian может помочь в работе.
Немного контекста
Сейчас я решил попробовать фриланс. Раньше был Java-backend, но после большого перерыва (успел поработать в продажах и на них прилично перегореть) обратно на серьёзную вакансию так просто не зайти. Фриланс — способ размяться и заодно потрогать разные задачи.
И вот тут вылезла знакомая многим боль. Мне было лень каждые пять минут заходить на Kwork и листать новые заявки. Откликался редко, хорошие заказы проходили мимо. Рутина, которая отъедает больше, чем приносит. А раз рутина — её можно сгрузить на скрипт.
Первая версия: бот, который сам приносит заказы
Взял n8n и небольшой скрипт на Python — тот цепляется к специализированной библиотеке для Kwork. Библиотека, кстати, годная, запросов в ней много.
Алгоритм банальный. Скрипт смотрит новые заказы в выбранных категориях и кидает их в n8n через webhook. Внутри n8n я сначала разбиваю общий поток на отдельные заказы через Split Out, а потом парой узлов на JavaScript прогоняю каждый через свою оценку — бюджет, число откликов, процент найма у заказчика, попадание в стек. Готовый дайджест улетает мне в телегу через бота.
Сверху повесил bash-скрипт, который раз в полчаса дёргает питон на VPS. Получился бот, который дважды в час скидывает свежие заказы. Дальше я просто иду по ссылке и откликаюсь на то, что зацепило. Уже в разы лучше, чем руками обновлять биржу.
Вторая версия: а сюда бы ещё Obsidian
Через какое-то время захотелось поковырять n8n дальше — и, честно, просто из скуки прикрутить сюда любимый Obsidian.
Вспомнил про плагин канбан-досок. Их удобно гонять под задачи, и в голове щёлкнуло: так это же мини-CRM. Дашборд из какого-нибудь Битрикса, только на минималках и локально. Колонки-статусы, карточки-заказы, таскаешь их по этапам: «Новые» → «Откликнулся» → «Ответили» → «Переговоры» → «Взял» или «Отказ».
Оставалось связать локальное хранилище с n8n. Сразу вспомнился удобный плагин синхронизации через Git — он так и называется, Git. Через него всё и завёл: доска лежит markdown-файлом в приватном репозитории на GitHub, сервер дописывает туда заказы и пушит, а на ноуте плагин подтягивает.
Но всплыла тонкость. n8n у меня крутится в Docker-контейнере, и напрямую писать в файл на хосте он не умеет — контейнер отрезан от файловой системы хоста. Пришлось сделать максимально тупой сервис-приёмник на Python. Висит на хосте, принимает заказы от n8n по HTTP через мост между контейнером и хостом, дописывает карточки и коммитит в Git. Тупой — потому что надёжный: чем меньше он умничает, тем меньше поводов сломаться.
Переделывать его пришлось пару раз — Git из-под сервиса вёл себя капризно. Но в итоге всё встало на место.
Как всё устроено
Вот схема целиком. Участников трое, GitHub посередине как точка обмена.

Вся доска — это один файл kanban.md. Плагин Kanban держит колонки как заголовки ## Название, а карточки — строками-списками под ними. Для Git это просто текстовый файл, так что вся синхронизация сводится к версионированию одного файла между сервером и ноутом.
Дальше поток ходит по кругу. Сервер находит заказы, дописывает карточки в «Новые», пушит. На ноуте плагин подтягивает, я вижу свежак, таскаю карточки по этапам и пушу обратно. GitHub — единственный источник правды.
Про записчик пара слов, потому что вопрос «а зачем он вообще» напрашивается. n8n сидит в Docker, а репозиторий, SSH-ключ и сам git — на хосте. Можно было пробросить всё это внутрь контейнера, но тогда пришлось бы тащить туда ключ, а это лишний риск и лишняя возня. Проще оставить git на хосте, а n8n пусть шлёт данные по HTTP — это он и так умеет.
Грабли №1: IPv6 в Docker тихо убил отправку в Telegram
Самая противная история — потому что ошибка показывала пальцем не туда.
В какой-то момент прогоны начали валиться на ноде отправки в Telegram:
The DNS server returned an error, perhaps the server is offline
Первая мысль — сдох DNS или Telegram лёг. Ни то, ни другое. Полез диагностировать изнутри контейнера n8n, и вот что вылезло.
Имя-то резолвится. api.telegram.org прекрасно разрешался — но в IPv6-адрес (запись AAAA, вида 2001:67c:...). А IPv4 отдавался как «No answer». И вот в чём подвох: у Docker-сети контейнера не было рабочего маршрута для IPv6 наружу. n8n брал первый предложенный адрес — то есть IPv6 — и пытался достучаться. Маршрута нет, коннекта нет, а наружу это выходит как «DNS error, сервер offline». Хотя DNS живой и Telegram на месте.
Проверил гипотезу в лоб — попросил именно IPv4 и попробовал дёрнуть по нему:
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 по IPv4 есть — а падает всё потому, что Node.js (на нём работает n8n) по дефолту хватает IPv6, который в никуда.
Чинится одной переменной окружения — велим Node предпочитать IPv4. В compose.yml, в сервис n8n:
environment:
- NODE_OPTIONS=--dns-result-order=ipv4first
Пересоздал контейнер — и отправка в телегу ожила.
И вот что меня всё это время путало. Запросы к моему записчику по той же причине не падали. Записчик я адресую по числовому IP шлюза Docker-сети, а не по имени, а числу DNS не нужен вообще. Поэтому сломанный IPv6 бил только по ветке, которая ходила по домену (Telegram), а ветку, которая ходила по IP, вообще не задевал. Одна нода красная, другая зелёная — и сиди гадай.
Если коротко: контейнер в Docker вдруг «не резолвит» или «сервер offline», а снаружи всё живо — глянь, не хватает ли он IPv6-адрес, до которого у него нет маршрута. Текст ошибки тут врёт как дышит.
Грабли №2: два писателя в одном файле без вечных конфликтов
Тут интересно, потому что чинилось это не кодом, а тем, кто куда пишет.
Расклад простой: и сервер, и я лезем в один kanban.md. Сервер дописывает заказы, я таскаю карточки. Оба меняют файл — значит рано или поздно Git упрётся в два несовместимых правки одного места и вывалит конфликт слияния. Разгребать конфликты в markdown, когда половину правок делает робот, — удовольствие ниже среднего.
Чтобы это обойти, надо понять, как Git вообще смотрит на файл. Никаких «карточек» и «колонок» он не видит. Для него kanban.md — набор строк, и мержит он их построчно. Конфликт возникает не от того, что «оба потрогали доску», а от того, что оба залезли в одну и ту же область файла.
Из этого и родилось правило зон:
-
Сервер пишет только в «Новые» — и вставляет карточки строго наверх, сразу под заголовок
## Новые. Ничего существующего не трогает. -
Я живу во всех остальных колонках — перетаскиваю карточки из «Новых» дальше по этапам.
Сервер орудует в одной секции, мои перемещения — в других. Git видит правки в разных кусках файла и молча их сливает. Конфликтовать нечему.
Есть неочевидный момент, на котором легко подорваться: из «Новых» карточку можно переносить, но нельзя удалять. Вроде результат один — карточка ушла из «Новых». Но для Git это две разные истории. Когда я переношу, главное изменение — строка появляется под другим заголовком, в моей зоне, далеко от сервера. Когда удаляю — всё изменение остаётся внутри «Новых», ровно там, где сервер в эту же секунду что-то вставляет и двигает строки. Два человека правят один абзац — привет, конфликт. Поэтому я карточку из «Новых» только тащу вправо. Перенос безопасен всегда, а удаление — как повезёт, зависит от того, пишет сервер прямо сейчас или нет.
Со стороны записчика зоны подпёрты ещё и стратегией Git. Сначала он перед вставкой делал git pull --rebase и периодически спотыкался — rebase капризничает, когда с удалёнки приходит неоднозначный ответ. Заменил на пару команд:
git fetch origin main
git reset --hard origin/main
Смысл вот в чём. Записчик — процесс без собственного состояния. Своих незакоммиченных правок, которые надо бережно вплетать в чужие, у него нет. Правда всегда на GitHub. Так что вместо умного слияния он тупо подтягивает состояние удалёнки, приравнивает к нему свою копию и уже потом вставляет карточку. Меньше ума — меньше сюрпризов.
Что вышло
Раз в полчаса сервер собирает свежие заказы, прогоняет через оценку, дайджест летит в телегу, а годные лиды падают карточками в «Новые». Я открываю Obsidian, вижу поток заказов и просто гоняю их по этапам. Та же CRM, только локальная, без веба и без единого рубля за облачную синхронизацию.
Рутина, из-за которой всё затевалось, отвалилась. Не надо каждые пять минут дёргать биржу и бояться проспать нормальный заказ — он сам приедет. А Obsidian заодно стал у меня ещё и рабочим дашбордом, что мне, как человеку, который пихает его куда только можно, отдельно в радость.
Если тема зайдёт — в другой раз разберу, что осталось за кадром: как устроена сама оценка заказов, дедуп карточек и мобильная синхронизация через Git.

Автор: Ziverpup

