GitHub 活动自动转博客:MCP 工作流自动化
展示用 MCP 连接 GitHub、Notion、DEV.to 自动生成周报和博客,提升开发者工作流效率的实践案例。
展示用 MCP 连接 GitHub、Notion、DEV.to 自动生成周报和博客,提升开发者工作流效率的实践案例。
Notion MCP 挑战赛参赛作品 🧠
这是一个 Notion MCP 挑战赛的参赛项目。
每周一站会,总会有人问:“你上周做了什么?”而每到这时,我都会盯着屏幕,努力回忆。我是在周三还是周四合并了那个 PR?那次重构是在 auth 模块里,还是在 pipeline 里?我到底改过多少个仓库?
我受够了这种大脑一片空白的时刻。于是,我构建了 DevNotion——一个由 Mastra 驱动的三 Agent pipeline。它会收集我一整周的 GitHub 活动,用 Gemini 将这些活动讲述成一篇第一人称博客文章,然后发布到 Notion(以带有结构化表格的计划页形式)和 DEV.to(作为文章草稿)。每周日,通过 GitHub Actions 自动运行。
周一再也不会失忆了。博客会自己写好。
通过 GraphQL 收集我的 GitHub 活动——commit、PR、issue、代码审查、discussion、语言统计和贡献连续天数。
使用 Gemini 将原始数据讲述成一篇轻松自然的第一人称博客文章(如果 LLM 不可用,则使用确定性的 fallback)。
同时发布到两个平台:Notion——一个计划页,包含统计表格、仓库明细、PR/issue/review 表格、语言明细以及完整博客文章;DEV.to——一篇等待审阅的文章草稿。
Notion——一个计划页,包含统计表格、仓库明细、PR/issue/review 表格、语言明细以及完整博客文章。
DEV.to——一篇等待审阅的文章草稿。
3 个专业化 Agent——每个 Agent 专注做好一件事(收集、讲述、发布)。
仅在真正能创造价值的环节使用 LLM——收集和发布都是确定性的,没有 token 开销。
4 种博客语气配置——casual(默认)、professional、technical、storytelling。
计划式 Notion 页面——不只是一整面文字,而是包含统计、仓库、PR、issue、review 和编程语言等信息的结构化表格。
Notion MCP 集成——通过 Model Context Protocol 使用完整的 Notion API 能力。
Notion Markdown Content API——直接向页面写入富 Markdown 内容(真正改变游戏规则的功能)。
DEV.to 草稿发布——文章会以草稿形式创建,随时可以审阅和发布。
GitHub Actions CI——每周定时任务(周日 08:00 UTC)并支持手动触发。
README 中的博客日志——CI 每次运行后都会自动 commit 一张指标表格。
Fallback 链——即使 Gemini 出现故障,也始终能够生成博客文章。
全方位限流——Notion 和 DEV.to API 都使用 p-queue + p-retry。
整个 pipeline 只在真正能创造价值的地方使用 LLM——也就是内容讲述。收集和发布都是纯函数调用。没有 token 开销,没有幻觉风险,执行速度也更快。
我本可以构建一个包办一切的超级 Agent。但这样做无异于给自己挖坑:
在确定性工作上浪费 token(获取 GitHub 数据根本不需要 LLM)。
编造 URL 和统计数据(发布步骤绝不能凭空捏造内容)。
制造调试噩梦(单体流程到底是哪一部分失败了?)。
因此,每个 Agent 都各司其职。workflow 将它们串联起来:
export const weeklyDispatchWorkflow = createWorkflow({
id: 'weekly-dispatch',
inputSchema: z.object({ weekStart: z.string() }),
outputSchema: PublishOutputSchema,
})
.then(harvestStep)
.then(narrateStep)
.then(publishStep)
.commit();
三个步骤通过 .then() 串联,最后作为一个完整 workflow 提交。Mastra 会自动处理步骤之间的数据传递。
Harvest 步骤会直接调用 GitHub 的 GraphQL API——不需要 Agent 进行推理:
const harvestStep = createStep({
id: 'harvest-github',
inputSchema: z.object({ weekStart: z.string() }),
outputSchema: WeeklyDataSchema,
execute: async ({ inputData }) => {
const data = await fetchWeeklyContributions(inputData.weekStart);
return WeeklyDataSchema.parse(data);
},
});
一条 GraphQL 查询就能获取这一周的 commit、PR、issue、review、discussion、语言统计和贡献连续天数。返回结果会通过 Zod schema 进行验证。整个过程没有 LLM 参与——这就是纯粹的数据获取。
这是 LLM 真正发挥价值的地方。Narrator Agent 会接收原始 JSON,并写出一篇读起来就像是我亲自写的博客文章。system prompt 中包含完整的个性设定——它使用第一人称写作,了解我的技术栈(Python、Rust、TypeScript),会提及我的 OSS 工作,并且能够匹配四种语气配置中的任意一种。
但 LLM 有时并不稳定。因此,Narrate 步骤设置了一条 fallback 链:
const narrateStep = createStep({
id: 'narrate',
execute: async ({ inputData, mastra }) => {
const agent = mastra!.getAgent('narrator-agent');
let blog;
try {
const result = await agent.generate(prompt);
const parsed = parseFrontmatter(result.text);
if (parsed.success) {
blog = parsed.data.blog;
} else {
blog = buildFallbackNarration(inputData).blog;
}
} catch (err) {
blog = buildFallbackNarration(inputData).blog;
}
return { blog, weeklyData: inputData };
},
});
Gemini 会生成一篇带有 YAML frontmatter 的 Markdown 博客文章。
如果解析失败 → 确定性 fallback 会根据原始数据构建一篇基础文章。
即使 Gemini 完全不可用,也始终能够生成一篇博客文章。
Publish 步骤才是事情真正变得有趣的地方。它并不是简单地把文本一股脑塞进 Notion,而是会构建一个带有结构化表格的计划页:
const publishStep = createStep({
id: 'publish',
execute: async ({ inputData }) => {
const { blog, weeklyData } = inputData;
// 1. Create Notion page
const createResult = await createNotionPage(title);
// 2. Create DEV.to draft (so the link goes into the Notion planner)
const devtoResult = await createDevtoArticle({
title: blog.headline,
body_markdown: buildDevtoMarkdown(blog),
tags: blog.tags,
published: false,
});
// 3. Write planner markdown to Notion (includes DEV.to link)
const plannerMd = buildPlannerMarkdown(weeklyData, blog, links);
await writeNotionMarkdown(notionPageId, plannerMd);
},
});
执行顺序非常重要:在写入 Notion 页面内容之前,必须先创建 DEV.to 草稿,这样 Notion 计划页中才能包含指向 DEV.to 草稿的链接。这才是跨平台链接的正确做法。
每个 Notion 页面都包含:
Published Links 表格——Notion 页面 URL + DEV.to 草稿编辑链接。
Week at a Glance——commit、PR、issue、review、新增/删除代码行数和贡献连续天数。
Active Repositories——仓库名称、commit、编程语言和代码行变更。
Pull Requests/Issues/Reviews/Discussions——结构化表格。
Languages——按 commit 数量排列的主要编程语言。
完整博客文章——分隔线下方是生成的讲述内容。
在 GitHub 上为 DevNotion 点 Star。
这是最令我兴奋的部分。DevNotion 通过两种互补方式使用 Notion MCP Server:
Publisher Agent 通过 Mastra 的 MCP client 与官方 @notionhq/notion-mcp-server 集成。这让 Agent 能够通过 Model Context Protocol 使用完整的 Notion API 能力:
import { MCPClient } from '@mastra/mcp';
export const notionMcp = new MCPClient({
servers: {
notion: {
command: 'npx',
args: ['-y', '@notionhq/notion-mcp-server'],
env: {
OPENAPI_MCP_HEADERS: JSON.stringify({
Authorization: `Bearer ${env.NOTION_TOKEN}`,
'Notion-Version': '2022-06-28',
}),
},
},
},
timeout: 30000,
});
MCP tools 会延迟加载,并提供优雅的 fallback——如果 MCP server 启动失败,直接调用的 tools 仍然可以独立工作:
export async function getNotionMcpTools(): Promise<Record<string, any>> {
try {
return await notionMcp.listTools();
} catch (err) {
console.warn('MCP: Notion MCP server unavailable, using direct tools only');
return {};
}
}
Publisher Agent 会合并两套 tools——MCP tools 用于提供完整的 Notion API 能力,而直接 tools 则用于实现 MCP 尚未覆盖的功能:
// Direct tools (Markdown Content API + DEV.to — not available via MCP)
const directTools = {
createNotionPage: createNotionPageTool,
writeMarkdown: writeMarkdownTool,
searchNotion: searchNotionTool,
updateNotionPage: updateNotionPageTool,
};
// Merge: Notion MCP tools + direct tools
const mcpTools = await getNotionMcpTools();
const tools = { ...mcpTools, ...directTools };
这种双轨方案让 Publisher Agent 能够兼得两者之长——在 Mastra playground 中进行交互式操作时,可以使用 MCP 广泛的 API 能力;运行自动化 workflow 时,则可以使用直接 tools。
正是 Notion 的这项功能,让计划式页面成为可能。无需逐个构造 Notion block(这种方式既痛苦,又很容易触发限流),我只需要通过一次 API 调用,就能将整个页面以 Markdown 形式写入:
const response = await fetch(
`https://api.notion.com/v1/pages/${pageId}/markdown`,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${env.NOTION_TOKEN}`,
'Content-Type': 'application/json',
'Notion-Version': '2026-03-11',
},
body: JSON.stringify({
type: 'replace_content',
replace_content: { new_str: markdown },
}),
},
);
只需一个 PATCH 请求,就可以使用富 Markdown 替换整个页面的内容——包括表格、标题、引用块、链接、代码块以及其他所有内容。正是它支撑起了计划式布局:结构化统计表格与完整博客文章,可以通过一次 API 调用全部写入。
Notion API 每秒大约允许发送 3 个请求。每次 Notion 调用(包括 MCP 和直接调用)都会经过一个共享的限流器:
const queue = new PQueue({ concurrency: 1, interval: 334, intervalCap: 1 });
async function rateLimited<T>(fn: () => Promise<T>): Promise<T> {
return queue.add(() => pRetry(fn, { retries: 3 })) as Promise<T>;
}
p-queue 用于限制并发,p-retry 用于处理暂时性故障。这是我踩坑之后才学到的经验——如果不做限流,当你在短时间内连续创建页面、写入 Markdown 和更新图标时,Notion API 会用铺天盖地的 429 把你彻底淹没。
Notion(3 req/s)、DEV.to(30 req/30s)、GitHub GraphQL(5000 points/hr)——每个 API 都有自己的限流规则。最终,我给所有服务都套上了 p-queue + p-retry wrapper。三个服务中的限流代码几乎完全相同,老实说,它们或许应该被提取成一个共享工具。但与过早抽象相比,保留三段相似的代码更好。
一开始,我使用 Gemini 原生的 JSON schema 来获得结构化输出(agent.generate(prompt, { structuredOutput: { schema } }))。它确实可以工作,但每次调用都会额外增加 20~40 秒。改为生成纯文本并解析 YAML frontmatter 后,速度提升到了原来的 3~4 倍,可靠性却丝毫不差。少数解析失败的情况,则由确定性 fallback 兜底。
在这个项目中,我已经换过三个 Gemini 模型:gemini-2.5-flash-preview-04-17(已退役)、gemini-2.5-flash(稳定,但生成结构化输出时很慢),以及现在使用的 gemini-3-flash-preview(当前模型)。得到的教训是:始终通过 env vars 配置模型。硬编码模型 ID,迟早会导致部署崩溃。
Mastra 和我的代码都依赖 Zod,但使用的版本不同。同时存在两个 Zod 实例,意味着一个实例中的 z.string() 无法被另一个实例识别——schema 验证就这样悄无声息地失败了。修复方法是在 package.json 中加入一行配置:
{
"pnpm": {
"overrides": {
"zod": "$zod"
}
}
}
这会强制 pnpm 将依赖去重为同一个 Zod 版本。我花了太长时间才找出这个问题。
Harvest 和 Publish 步骤最初都是完整的 Agent 调用。但当任务只是“调用这个 GraphQL endpoint 并返回结果”时,LLM 并不能带来任何价值。切换到直接函数调用后,pipeline 变得更快、更便宜,也更可预测。只有在需要创造力或推理能力时才使用 LLM——其他地方,写个函数就够了。
这个项目由 Mastra、Gemini、Notion API,以及大量咖啡共同构建而成。如果你也曾忘记自己上周做过什么,不妨试试 DevNotion。
部分评论可能只有登录后的访客才能看到。请登录以查看全部评论。
如果需要采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。