Skip to main content
Glama
stepanic

linkedin-poster

by stepanic

linkedin-poster

Публикуйте в свою ленту LinkedIn из Claude Code, из оболочки или по cron. Один Cloudflare Worker, без зависимостей, три поверхности: MCP-сервер, чтобы агент мог публиковать напрямую, HTTP API для скриптов и стилистический линтер, который отказывается публиковать текст с признаками машинной прозы.

Создан, потому что хороший пост стоит автоматизировать, а плохое длинное тире не стоит публиковать. В этом README тоже нет длинных тире, в этом и суть.

Claude Code ──MCP──┐
                   ├──> Worker ──> LinkedIn Posts API ──> your feed
curl / CI ──HTTP───┘      │
                          ├──> KV (one 60-day token) + daily cron
                          └──> Telegram group (what happened, and what broke)

Чем это не является

Это публикует от имени одного участника: вас. Это не SaaS для планирования, не мультитенантный инструмент и не затрагивает чужие аккаунты. Всё необходимое настраивается самостоятельно; никакая партнёрская программа LinkedIn не задействована.

Related MCP server: mcp-server-linkedin

Ограничение, определяющее всё

Токены доступа LinkedIn живут 60 дней, а программные refresh-токены доступны только одобренным партнёрам Marketing Developer Platform. Самостоятельно настраиваемое приложение их не получает. Обойти это невозможно, и любое руководство, обещающее настройку LinkedIn-автоматизации «один раз и навсегда», ошибочно.

Что есть: пока ваш текущий токен действителен и вы вошли в LinkedIn, повторный запрос к endpoint авторизации пропускает экран согласия и просто перенаправляет. Так что продление — это один клик каждые семь недель, если успеть до истечения токена. Упустите окно — и снова появится экран согласия, то есть два клика вместо одного.

Именно поэтому сервис поставляется с cron-триггером. Начиная за 14 дней до истечения, он каждое утро выводит напоминание о продлении в начало ежедневной сводки, пока бесшумное продление ещё работает. См. Уведомления.

Настройка

1. Сторона LinkedIn (вручную, около десяти минут)

  1. Вам нужна страница LinkedIn. Портал разработчика требует привязать приложение к ней и подтвердить её администратором.

  2. Создайте приложение на linkedin.com/developers. Запишите Client ID и Client Secret.

  3. Вкладка Products, запросите оба разрешения (это Open Permissions, одобряются мгновенно):

    • Sign In with LinkedIn using OpenID Connect → openid, profile

    • Share on LinkedIn → w_member_social

  4. Вкладка Auth → добавьте redirect URL. Он должен быть HTTPS, абсолютным и совпадать с REDIRECT_URI символ в символ: https://your-worker.example.com/auth/callback

2. Сторона Worker

git clone https://github.com/stepanic/linkedin-poster.git
cd linkedin-poster
npm install

# KV to hold the token
npx wrangler kv namespace create TOKENS
npx wrangler kv namespace create TOKENS --preview
# paste both ids into wrangler.jsonc

# edit wrangler.jsonc: name, routes, REDIRECT_URI, account_id

npx wrangler secret put LINKEDIN_CLIENT_ID
npx wrangler secret put LINKEDIN_CLIENT_SECRET
npx wrangler secret put API_TOKEN     # openssl rand -hex 32, guards /post and /mcp
npx wrangler secret put SETUP_KEY     # openssl rand -hex 32, guards /auth/start
npx wrangler secret put NOTIFY_WEBHOOK    # optional, any URL taking a JSON POST
npx wrangler secret put TELEGRAM_BOT_TOKEN # optional, see Notifications below
npx wrangler secret put TELEGRAM_CHAT_ID

npm run deploy

3. Подключите аккаунт

Откройте https://your-worker.example.com/auth/start?key=<SETUP_KEY> в браузере, один раз подтвердите, готово. Страница покажет дату истечения.

SETUP_KEY — не для вида. Без него любой, кто найдёт URL, сможет привязать свой аккаунт LinkedIn к вашему Worker и перезаписать сохранённый токен.

Использование из Claude Code

claude mcp add --transport http linkedin https://your-worker.example.com/mcp \
  --header "Authorization: Bearer $API_TOKEN"

Три инструмента:

Tool

Что делает

linkedin_post

Проверяет линтером, затем публикует. Отказывает при ошибках, если не указано force: true.

linkedin_check

Проверяет линтером без публикации. Используйте при подготовке черновика.

linkedin_status

От чьего имени публикует и сколько дней осталось у токена.

Затем в сессии: «проверь этот черновик линтером, затем опубликуй».

Использование из оболочки

# dry run: lint only, publish nothing
curl -s -X POST https://your-worker.example.com/post \
  -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
  -d '{"text":"…","dry_run":true}'

# publish
curl -s -X POST https://your-worker.example.com/post \
  -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
  -d '{"text":"…","visibility":"PUBLIC"}'

# token state
curl -s https://your-worker.example.com/status -H "Authorization: Bearer $API_TOKEN"

/post возвращает 201 с URN поста и его публичным URL, 422, когда линтер блокирует публикацию, 503, когда токен отсутствует или недействителен.

Уведомления

Без этого сервис отказывает так же, как любой сервис без присмотра: тихо, и вы узнаёте об этом в день, когда пост не вышел. Группа в Telegram решает эту проблему ценой одного бота.

# 1. @BotFather in Telegram, /newbot, copy the token
npx wrangler secret put TELEGRAM_BOT_TOKEN

# 2. make a group, add the bot, send /start@yourbot in it
#    (a bot with privacy mode on sees only messages that mention it)

# 3. read the group id back, then store it
open "https://your-worker.example.com/telegram/chatid?key=<SETUP_KEY>"
npx wrangler secret put TELEGRAM_CHAT_ID

# 4. prove it works
open "https://your-worker.example.com/telegram/test?key=<SETUP_KEY>"

Что приходит:

Событие

Когда

Ежедневная сводка

каждый запуск cron, включая тихие дни

Напоминание о продлении

начиная за 14 дней, поверх сводки за этот день

Переподключение

сохранён новый токен, с новой датой истечения

Опубликовано

каждый опубликованный пост, со ссылкой

Заблокировано

линтер отклонил черновик, с указанием сработавших правил

Отклонено

LinkedIn отклонил пост, с предположением о причине

Нет рабочего токена

попытка публикации обнаружила, что токен отсутствует или недействителен

Квота

за день превышено 120 из 150 запросов, разрешённых участнику LinkedIn

Два проектных решения, о которых стоит знать, прежде чем что-то менять:

Ежедневная сводка — это сердцебиение. Она уходит, даже если ничего не произошло, поэтому её отсутствие само по себе является сигналом тревоги, и никому не нужно следить за наблюдателем. Именно поэтому напоминание о продлении приходит поверх сводки, а не отдельным сообщением: ровно одно сообщение утром, иначе вы приучите себя просматривать их мельком.

Ссылка на продление содержит одноразовый nonce, а не ключ настройки. Ссылка с SETUP_KEY навсегда оставила бы долгоживущий секрет в логе чата. Вместо этого cron создаёт nonce, действительный семь дней, заменяемый следующим напоминанием и удаляемый в момент сохранения токена. Предпросмотр ссылок отключён во всех сообщениях, чтобы Telegram не мог израсходовать nonce предварительной загрузкой.

Если ничего не настроить, сервис ведёт себя ровно как раньше: NOTIFY_WEBHOOK, если он у вас есть, и лог, если нет.

GET /telegram/preview?key=<SETUP_KEY>&kind=daily отображает любое сообщение без отправки — так можно править формулировки, не засоряя группу. Добавьте &send=1, чтобы увидеть его на телефоне.

Транспорт Bot API протестирован на локальной заглушке, включая миграцию в супергруппу, которая незаметно ломает Telegram-ботов спустя месяцы после настройки:

npm run test:telegram

Линтер

Ошибки блокируют публикацию; предупреждения — нет.

Rule

Severity

Why

no-em-dash, no-en-dash

error

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

max-length

error

LinkedIn отклоняет комментарии длиннее 3000 символов.

empty

error

Очевидно, но легко пропустить при передаче из файла.

no-markdown-bold, no-markdown-link, no-markdown-heading, markdown-bullets

warning

LinkedIn не отображает ни одно из этих форматирований. Звёздочки и решётки показываются буквально.

Дефисы не помечаются никогда. read-only tier, two-day sprint и per-client limits — корректны и должны оставаться. Ошибками являются только U+2014 и U+2013.

Редактирование src/lint.ts — это предусмотренный способ сделать проект своим. Правила кодируют писательские предпочтения одного человека, а не универсальный стандарт.

Что вас удивит

  • Posts API не извлекает URL, но LinkedIn всё равно превращает их в ссылки. Проверено на живом посте: голый URL сокращается до lnkd.in/... и становится кликабельным. Чего вы не получите — это карточки предпросмотра с изображением и заголовком. Для этого нужна миниатюра, загруженная через Images API, плюс блок content.article. Здесь пока не реализовано.

  • LinkedIn-Version обязателен, и версии выводятся из эксплуатации. 202508 перестал работать 17 августа 2026 года. Относитесь к значению в wrangler.jsonc как к пункту, требующему обслуживания.

  • /v2/ugcPosts устарел. Здесь используется /rest/posts, который заменил его.

  • Изменение запрашиваемых областей доступа аннулирует все существующие токены. Вам придётся авторизоваться заново.

  • 150 запросов на участника в день. На участника, а не на приложение.

Структура

Файл

Содержимое

src/index.ts

Маршруты, OAuth-поток, cron-напоминание

src/linkedin.ts

Вызовы OAuth и Posts API, все endpoint'ы в одном месте

src/mcp.ts

MCP-сервер, самописный JSON-RPC, без состояния

src/lint.ts

Стилистический линтер

src/tokens.ts

Запись токена в KV, расчёт срока действия, безопасное по времени сравнение секретов

src/notify.ts

Какие события доходят до группы и в каких формулировках

src/telegram.ts

Транспорт Bot API, повторы, миграция в супергруппу

src/stats.ts

Счётчики дня, которые читает сводка

Локальная разработка

cp .dev.vars.example .dev.vars   # fill in, never commit
npm run types                    # regenerate Env after editing wrangler.jsonc
npm run typecheck
npm run dev

wrangler dev --local даёт вам локальное KV, так что весь процесс, кроме реальных вызовов LinkedIn, можно проверить без учётных данных.

Заметки из реальной настройки

docs/2026-08-27-postavljanje-i-zamke.md описывает, чего на самом деле стоила настройка с нуля: портал отклонял любое имя приложения, содержащее «LinkedIn», необратимая привязка к Page, OAuth-скоупы, которые остаются пустыми, пока не обновишь страницу, %20 против + в параметре scope, отрицательный DNS-кэш, из-за которого работающее развёртывание выглядело мёртвым, и как обрабатывались секреты, чтобы ни один из них не прошёл через терминал.

docs/2026-08-27-telegram-obavijesti.md описывает слой уведомлений: миграцию в супергруппу, которая незаметно ломает Telegram-ботов, почему сердцебиение — это сводка, а не пинг, и почему ссылка на продление содержит nonce.

Лицензия

MIT. Правила в src/lint.ts — мои; механизм можете забирать.

Related MCP Connectors

Related MCP Servers