weone-daily-post
weone-daily-post — 远程 MCP 服务器
We One Aviation 每日 Instagram + Facebook 帖子的发布后端。 它是一个无状态的 Streamable HTTP MCP 服务器: Claude 负责思考(主题选择、文案、标题),此服务负责副作用(历史记录、图像渲染、存储、Graph API)。 图像是排版生成的,而非 AI 生成的:海报上的文字与提供的文字完全一致。
Claude ──POST /mcp (Bearer)──▶ Render web service (Node 20, Express)
├─ Supabase posts table + post-images bucket
├─ Chromium HTML template → JPEG
└─ Meta Graph IG container/publish, FB photos工具
工具 | 用途 |
| 最新优先的历史记录,最多 200 行: |
| 插入 |
| 将品牌化 HTML 模板渲染为精确尺寸的 sRGB JPEG,上传,并返回一个图像块以及公共 URL。 |
| 先发布到 Instagram,再发布到 Facebook,并记录结果。 |
| 影子模式:将完成的帖子记录为 |
| Meta 令牌到期前的天数 + 已授予的权限范围。 |
每个工具都返回 JSON。成功时返回 {"ok": true, ...};失败时返回 MCP 错误
结果,包含 {"ok": false, "error": {code, message, retryable, details}}。
不会向调用方抛出原始堆栈跟踪。
render_post
template 是 news、subject 或 career 之一 — 与 posts 表使用的三个类别相同。
字段 | 限制 | 说明 |
| 60 个字符 | Barlow 700,最多 3 行。句子大小写,非标题大小写。 |
| 3–4 项,每项 90 个字符 | Barlow 400,每项一个金色标记 |
| 32 个字符,可选 | 金色,由 CSS 转为大写,例如 |
| 90 个字符,可选 | 页脚栏左侧,例如 |
它返回两个内容块:一个图像块(base64 JPEG)和一个文本 块,包含公共 URL、文件名、尺寸、字节大小和渲染时间。
在上传任何内容之前会运行两个保护检查,两者都会指明违规字段:
长度限制,在触及 Chromium 之前检查 — 这是廉价的拒绝方式。
points[2] is 97 characters, limit is 90. Shorten it and retry.页面内测量,在布局之后 — 每个文本框都是一个固定大小的 裁剪框,如果其内容高于或宽于该框,渲染将被拒绝,并返回字段名称和溢出的像素数。这能捕获字符计数无法发现的问题, 例如一个长度合法但超出边缘的不可断开的 80 字符标记。
任一保护检查触发时都不会上传任何内容,因此拒绝只需一秒钟, 修复方法始终是"缩短指定字段"。
图像块仍然会返回,以便您能在上下文中阅读文字,但它不再是正确性检查: 固定模板不会拼错单词或虚构图表。最坏情况的合法输入(58 字符标题加四条 90 字符要点、最宽的合法眉题和页脚)已验证可适配所有三个模板。
publish_socials,逐步说明
对
image_url执行HEAD请求并断言200+content-type: image/jpeg。Meta 会在服务器端获取此 URL,而错误的 URL 会在数小时后以不透明的方式失败。 (拒绝HEAD的存储会改用单字节范围GET。)Instagram —
full_caption = caption + "\n\n" + hashtags.join(' '),上限 为 2200 个字符。只会从末尾丢弃标签;标题正文 永远不会被截断。如果正文单独超过 2200 个字符,调用会在发布任何内容之前以caption_too_long失败。POST {IG_USER_ID}/media→ 每秒轮询一次GET {container}?fields=status_code,status,最多 60 秒 → 仅在FINISHED时发布。在ERROR时,status字符串会原样返回,因为这是 Meta 解释其不满之处的唯一位置。Facebook — 使用
url和message执行POST {FB_PAGE_ID}/photos。无论 Instagram 结果如何都会尝试。记录 —
posts行会获得ig_post_id、fb_post_id、image_url和status=published(两者都成功)、partial(其中一个成功)或failed(两者都失败)。
返回 {ig_post_id, fb_post_id, status, errors: [...]}。单平台失败
绝不会被吞没:它会出现在 errors[] 中,包含平台、失败的阶段以及 Meta 自己的 code / error_subcode / message。
Related MCP server: Meta Social MCP
环境变量
变量 | 必需 | 说明 |
| 是 |
|
| 是 |
|
| 是 | 服务角色密钥。绕过 RLS — 仅限服务器端。切勿将其放入连接器配置中。 |
| 否(默认 | 每次调用使用的 Graph API 版本。 |
| 用于发布 | Instagram 商业账户 ID(数字,非 @用户名)。 |
| 用于发布 | 与该 Instagram 账户关联的 Facebook 主页 ID。 |
| 用于发布 | 长期有效的主页访问令牌,具有 |
| 否 | Render 会设置此项。默认 10000。 |
| 否(默认 | 超过此大小后,内联 base64 预览会被缩小。 |
| 否 | Chrome/Chromium 二进制的显式路径。覆盖各平台默认值。 |
| 否 | 设置为 |
将 .env.example 复制为 .env 以进行本地运行。.env 已被 gitignore — 请保持这样。
设置
1. Supabase
在 SQL 编辑器中运行 migrations/001_init.sql(或执行 supabase db push)。它是
幂等的,并创建:
posts表,包含检查约束和topic上的唯一索引 — 该索引就是重复保护,因此重复保留应该失败,created_at desc和status索引,在
posts上启用 RLS,且无任何策略(只有服务密钥可以进入),公共
post-images存储桶及其公共读取策略。 公共读取是必需的:Meta 自行获取 JPEG,无法提供凭据。
2. Meta
您需要一个与 Facebook 主页关联的 Instagram 商业或创作者账户,
以及一个具有上述权限范围的长期有效主页令牌。在首次运行前使用
check_token() 确认 — 令牌过期是早晨发布失败最常见的原因。
3. 部署到 Render
使用 render.yaml(Blueprint):
将此仓库推送到 GitHub。
Render 仪表板 → 新建 → Blueprint → 选择仓库。它会读取
render.yaml:Node 20、npm ci && npm run build、npm start、 在/health上进行健康检查。Render 会提示输入每个
sync: false变量。粘贴它们。部署,然后检查日志中是否有
server.listening ... auth=configured。auth=MISSING表示MCP_AUTH_TOKEN未设置。
手动方式:
新建 → Web 服务 → 连接仓库。
运行时 Node,构建
npm ci && npm run build,启动npm start。健康检查路径
/health。添加上表中的环境变量,外加
NODE_VERSION=20。
验证:
curl https://<your-service>.onrender.com/health
# {"status":"ok","server":{...},"tools":[...six...],"uptime_s":3}使用 Starter 计划,不要用免费版。 Chromium 在 Node 之上大约需要 400 MB 常驻内存,而免费实例只有 512 MB——它会在渲染中途 OOM,失败表现为一个死掉的 worker,而不是有用的日志行。免费版还会在无活动后休眠,所以每天第一次工具调用还要额外付出 30–60 秒的冷启动。render.yaml 出于这两个原因设置了 starter。
构建时不需要下载浏览器:@sparticuz/chromium 自带二进制文件作为依赖,所以 npm ci && npm run build 就是整个构建。该构建步骤还会把 src/templates/ 复制到 dist/——tsc 只生成 .ts,没有这一步服务器能正常启动,但第一次渲染时会因缺少模板文件而失败。
4. 连接到 Claude
端点是:
https://<your-service>.onrender.com/mcp带有头:
Authorization: Bearer <MCP_AUTH_TOKEN>Claude Code / Cowork CLI:
claude mcp add --transport http weone-social \
https://<your-service>.onrender.com/mcp \
--header "Authorization: Bearer <MCP_AUTH_TOKEN>".mcp.json(项目级,不带 token 提交):
{
"mcpServers": {
"weone-social": {
"type": "http",
"url": "https://<your-service>.onrender.com/mcp",
"headers": { "Authorization": "Bearer ${MCP_AUTH_TOKEN}" }
}
}
}在 Claude 桌面/网页 自定义连接器 对话框中,粘贴相同的 /mcp URL,并将 bearer token 放入请求头字段。认证设计上仅通过请求头——token 永远不会作为查询参数接受,因为 URL 会出现在代理日志和浏览器历史中。
每日运行说明(品牌规则、禁止声明、类别轮换、图片规格、QA 清单)位于 weone-daily-post 技能中,不在此服务器中。本服务刻意不持有任何编辑策略。
本地开发
npm install
cp .env.example .env # fill it in
npm run dev # tsx watch, http://localhost:10000
npm run typecheck
npm run build && npm startnpm run smoke
npm run smoke # render all three templates, upload, print 3 URLs
npm run smoke -- --no-upload # render locally only, no credentials needed渲染每种模板各一张,将三张 JPEG 写入 ./out,上传它们,对每个公开 URL 执行 HEAD 检查并打印三个链接。然后证明两个守卫仍然触发。它不触碰任何 Meta 端点,因此对生产凭据是安全的。上传只需要 SUPABASE_URL 和 SUPABASE_SERVICE_KEY;--no-upload 则什么都不需要。
本地文件在上传前写入,所以即使 Supabase 失败,仍有东西可看。
渲染管线
无头 Chromium 通过 file:// 加载 src/templates/{template}.html,值被写入 DOM,然后对页面截图。相同的输入总是产生相同的像素。
模板 位于
src/templates/。tokens.css保存所有颜色;base.css保存三者共享的骨架。模板文件与其兄弟文件的区别仅在于眉题处理和点标记(新闻:金色规则,主题:编号金色圆圈,职业:金色 V 形)。字体自托管 在
src/templates/fonts/(Barlow 400/600/700 用于所有内容,Cinzel 600 仅用于字标,拉丁子集,OFL)。渲染时不获取任何内容——网络调用会使输出不确定,并在 Render 上静默失败,回退到系统衬线字体。渲染器等待document.fonts.ready,然后断言两个字体确实加载,而不是截图回退字体。用户文本永远不会拼接进标记。 值通过
textContent和createElement进入,所以没有转义错误:标题中的<script>会以字面字符形式出现在海报上。视口 1080×1350,
deviceScaleFactor: 2,所以截图是 2160×2700 并降采样——文字边缘保持清晰。sharp:
resize(1080, 1350, {fit:'cover'})→toColorspace('srgb')→jpeg({quality: 90, chromaSubsampling: '4:4:4'}),剥离元数据。4:4:4 不是装饰——4:2:0 会弄脏彩色文字边缘,而这些海报就是文字。断言编码后的 JPEG 小于 8 MB,并且解码后的尺寸确实是所要求的。
上传为
{yyyy-mm-dd}-{slug}-{6 hex}.jpg(UTC 日期)。每次渲染都有自己的键,永远不会被覆盖——upsert: false。重新渲染一个主题不能改变已经发布先前 URL 的帖子下的图片。cacheControl为 60 秒也是出于同样原因:坏对象在一分钟内可纠正,而不是在 CDN 中固定一年。Meta 在上传后不久、服务器端获取一次 URL,所以不需要长缓存。对象会累积;存储远比帖子上的过时图片便宜。
布局行为
类型缩放到点数。 三个点得到 68px 标题和 36px 正文;四个点得到 60px 和 32px。这是用 CSS 的 :has() 完成的,所以布局决策完全在模板中,渲染器既不知道也不关心。溢出守卫在缩放后运行,所以测量的是缩放后的结果。
内容块在头部规则和底部栏之间垂直居中。 仅靠固定间隙无法保持填充目标,因为文本量变化,所以三个弹性元素共享剩余空间:上方带、下方带和标题下方的间隙。带硬上限为 150px,这强制执行“无大空白边距”;一旦达到上限,多余空间进入标题间隙,读起来像呼吸空间而不是空洞。
代表性内容上的测量垂直填充:74–79%,带 99–124px。一个刻意稀疏的情况(一行标题,三个一行点)位于 68.8%,带处于 150px 上限——对于这么少的文本,这是算术最大值,再提高意味着点分散得太远,不再像列表。
标题是 Barlow 700 句子大小写,行高 1.1,字距 −0.5px。Cinzel 仅存在于“WE ONE AVIATION”字标中。句子大小写不在代码中强制执行——机械地小写标题会破坏 DGCA、ATPL 和 AAI——所以它在 headline 字段描述中指定。
每个模板内联一个扁平 SVG 装饰:宽对角线规则(新闻)、同心罗盘弧(主题)、上升 V 形堆叠(职业)。金色 7%,从右下角文字后面溢出。它们的存在是为了在缩略图尺寸下给构图重量,并且太淡,不会影响文字对比度。
装饰位于
.anchor-wrap内部,一个用overflow: hidden固定到画布的盒子。没有它,绝对定位的图形会超出底部边缘,计入body.scrollHeight,溢出守卫会以恒定的 160px 页面溢出拒绝每次渲染。
标志
src/templates/assets/logo.png 是提供的组合标志:星形/飞机标记在“WE ONE AVIATION”字标上方。头部用 Cinzel 自己渲染该字标,所以 scripts/prepare-logo.mjs 派生 logo-mark.png——仅标记——以避免品牌名称出现两次。它找到非透明像素的水平带并保留最高的,所以以其他分辨率重新导出标志仍然有效。替换 logo.png 后:
npm run prepare-logo浏览器生命周期和内存
一个 Chromium 在进程生命周期内共享,仅在断开连接时重新启动。启动大约需要一秒和几百 MB,对于每篇帖子来说太昂贵了。
渲染内存。 Chromium 在 Node 之上大约需要 400 MB 常驻内存。免费实例是 512 MB,会因此 OOM——部署在渲染中途死亡,没有有用的日志行。使用 Starter 计划。 如果必须留在免费版,预期会重启,并将每次重启后的第一次渲染视为冷启动。
二进制文件来自哪里取决于主机:
主机 | 来源 |
设置了 | 该路径,始终优先 |
Linux(Render) |
|
macOS / 开发 |
|
刻意不使用 --single-process。 它与重用单个浏览器不兼容:在该标志下关闭 BrowserContext 会销毁整个浏览器,所以第二次渲染会失败,错误为 "Target page, context or browser has been closed"。在此代码库上测量:使用它时 3 个上下文中只有 1 个存活,不使用则 3/3 存活。重用是交易中更有价值的一半。如果主机要求,设置 CHROMIUM_SINGLE_PROCESS=1 强制重新启用,并预期每次启动只渲染一次。
错误处理
代码 | 含义 |
| 参数验证失败。 |
| 主题已存在。按设计工作——换一个。 |
| 没有该 |
| Supabase 拒绝了。 |
| 提供商或 sharp 问题。 |
| Meta 将获取的 URL 不是可访问的 JPEG。 |
| 仅标题正文超过 2200 字符。标签自动修剪;正文从不修剪。 |
| Graph API。 |
| 某物超出其预算(图片 60 秒,容器轮询 60 秒,Graph 30 秒)。 |
| 缺少必需的环境变量。 |
Meta 代码 190 和 200 永不重试。 190 是过期或无效的 token,200 是缺少权限;两者都需要人工干预,重试只会消耗速率限制并隐藏真正原因。这些错误返回 retryable: false 和 needs_human 注释,说明该做什么。
每次工具调用记录 tool.start 和 tool.ok/tool.error 及持续时间,每次 Graph 调用记录 graph.call 及方法、端点、状态和耗时毫秒——所以 Render 的日志查看器足以重建运行。
故障排除
症状 | 原因 |
每次请求都返回 | 缺少 Header,或令牌与 |
| 服务上未设置 |
|
|
IG 容器在 | Meta 无法获取图片,或响应缓慢。先在浏览器中检查该 URL。 |
| 令牌已过期。生成一个新的长期 Page 令牌。 |
| 缺少权限范围 — 重新授予 |
| 不是 bug。该主题已存在于 |
当天首次调用超时 | 免费套餐冷启动。先访问 |
渲染中途进程退出,无错误信息 | 内存不足。Chromium 需要约 400 MB;免费实例为 512 MB。请迁移到 |
| 符合预期。缩短该字段名并重新调用 |
|
|
| 已设置 |
Render 上找不到模板文件 | 跳过了 |
布局
src/
server.ts Express, bearer auth, /health, POST /mcp
config.ts Lazy env resolution, constants
log.ts Timed stdout logging
errors.ts AppError / MetaError, the no-retry rule for 190 & 200
supabase.ts posts CRUD + storage upload
meta.ts Graph client, IG container flow, FB photos, debug_token
image.ts Chromium lifecycle, template render, overflow guard, sharp
tools/
register.ts Timing, error envelope, content-block shaping
get_past_topics.ts reserve_topic.ts render_post.ts
publish_socials.ts mark_draft.ts check_token.ts
index.ts
templates/
tokens.css base.css design tokens + shared skeleton
news.html subject.html career.html
fonts/ barlow-400/600/700, cinzel-600 (woff2, self-hosted)
assets/ logo.png (supplied), logo-mark.png (derived)
migrations/001_init.sql
scripts/
smoke.ts render all three, upload, prove both guards fire
copy-templates.mjs build step: tsc emits only .ts, templates must reach dist/
prepare-logo.mjs derives logo-mark.png from logo.png
render.yamlThis server cannot be deployed
Maintenance
Related MCP Connectors
Create, schedule and publish Instagram and Facebook posts from Claude or ChatGPT.
- MarkyOAuthai.mymarky
Create, schedule, and publish on-brand social posts to Instagram, LinkedIn, TikTok, and more.
Multi-platform social media post creation, scheduling, and publishing.
131Create, review, publish and schedule Instagram images, carousels and Reels with AI assistants.
Related MCP Servers
- AlicenseAqualityDmaintenanceMCP server to safely publish posts to multiple Facebook Pages via Meta Graph API, with built-in guardrails for brand voice, banned topics, image requirements, and anti-duplication.4MIT
- FlicenseNot gradedqualityBmaintenanceEnables publishing and managing organic Facebook Page and Instagram content directly through Meta's Graph API without paid third-party services.-
- AlicenseAqualityCmaintenanceEnables publishing to Facebook, Instagram, and YouTube through official APIs using your own OAuth credentials, with support for images, videos, Reels/Stories, and scheduled posts.6MIT
- AlicenseBqualityBmaintenanceEnables reading, publishing, commenting on, and analyzing Instagram Business/Creator accounts through the official Graph API, with multi-account support and an optional self-hosted publishing panel.29MIT