Skip to main content
Glama
stepanic

linkedin-poster

by stepanic

linkedin-poster

从 Claude Code、shell 或 cron 任务发布到你自己的 LinkedIn 信息流。一个 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 天,而编程式刷新令牌仅限于获得批准的 Marketing Developer Platform 合作伙伴。自助式应用永远拿不到。这一点绕不过去,任何承诺"设置后就不用管"的 LinkedIn 自动化指南都是错的。

实际情况是:当你的当前令牌仍然有效且你已登录 LinkedIn 时,再次访问授权端点会跳过同意页面,直接重定向。因此,只要在令牌失效前操作,续期就是每七周点一次。错过窗口期,同意页面就会重新出现,变成点两次而不是一次。

这就是为什么这个服务自带 cron 触发器。从到期前 14 天开始,它会在每天早上的每日摘要顶部放一条续期提醒,此时静默续期仍然有效。参见通知。

设置

1. LinkedIn 侧(手动操作,约十分钟)

  1. 你需要一个 LinkedIn 主页。开发者门户要求你把应用关联到一个主页,并由主页管理员验证。

  2. 在 linkedin.com/developers 创建应用。 记下 Client ID 和 Client Secret。

  3. 在 Products 标签页,请求这两项(它们都是开放权限,会立即批准):

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

    • Share on LinkedIn → w_member_social

  4. 在 Auth 标签页 → 添加重定向 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"

三个工具:

工具

作用

linkedin_post

先检查,再发布。有错误时拒绝发布,除非 force: true。

linkedin_check

只检查,不发布。起草时使用。

linkedin_status

以谁的身份发布,以及令牌还剩多少天。

然后,在会话中:"用检查器检查这篇草稿,然后发布它。"

从 shell 使用

# 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 拒绝了帖子,附带对原因的猜测

无可用令牌

发布尝试发现令牌缺失或失效

配额

当天用掉了 LinkedIn 允许成员的 150 次请求中的 120 次

有两个设计决策,在改动之前值得了解:

每日摘要就是心跳。 即使什么都没发生它也会发出,所以它的缺席本身就是警报,不需要什么东西去监控监控者。这也是为什么续期提醒搭载在摘要之上,而不是作为第二条消息单独到达:每天早上恰好一条消息,否则你会训练自己只看一眼。

续期链接携带一次性 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

检查器

错误会阻止发布;警告不会。

规则

严重程度

原因

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 已于 2026 年 8 月 17 日失效。把 wrangler.jsonc 里的这个值当作一项维护事项。

  • /v2/ugcPosts 已弃用。 这里使用的是取代它的 /rest/posts。

  • 更改请求的 scope 会使所有现有令牌失效。 你将需要重新授权。

  • 每个成员每天 150 次请求。 是每个成员,不是每个应用。

目录结构

文件

内容

src/index.ts

路由、OAuth 流程、cron 提醒

src/linkedin.ts

OAuth 和 Posts API 调用,所有端点集中在一处

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" 的应用名称,不可逆的主页关联,刷新页面之前一直为空的 OAuth scope,

Related MCP Connectors

Related MCP Servers