去年还是前年就在思考如何在 Ghost 中集成 Activitypub 的互动,还找了一个国内博客大佬的实现 ActivityPub 协议的简单实现 - Lawrence Li ,不过这位大佬的方案是完全自己实现协议部分,对我来说有点太复杂了。

最近用 Cloudflare Worker 完成了很多有意思的小东西,对 Worker 能实现什么又有了新一层的了解。 所以今天忽然灵光一闪,Ghost 有 webhook,我就不能利用它和 Worker 实现文章 key 和嘟文 key 的匹配,然后在页面直接加载嘟文互动数据吗?

所以通过和 AI 的几轮互动,我得到了下面这个有意思的东西,应该是能完整实现的。

以下是完整的端到端实现方案,涵盖博客发布同步到 GoToSocial、Cloudflare Worker/KV 存储管理、以及互动数据 API 提供的全流程:


系统架构图

[博客系统] --Webhook--> [Cloudflare Worker] --API--> [GoToSocial]
       ↑                      ↓
       |__[KV存储文章↔Toot映射]__|

一、前置准备

  1. 所需资源

    • Cloudflare 账户(启用 Workers 和 KV)
    • GoToSocial 实例(获取 API 访问令牌)
    • 支持 Webhook 的博客系统(如 Ghost/Hugo 等)
  2. 配置 KV 命名空间

    wrangler kv:namespace create BLOG_TOOT_MAPPING

    wrangler.toml 中添加绑定:

    [[kv_namespaces]]
    binding = "BLOG_TOOT_MAPPING"
    id = "创建的KV命名空间ID"

二、核心实现代码(Cloudflare Worker)

完整 worker.js 代码:

// 配置常量
const GTS_INSTANCE = "https://您的GoToSocial实例域名";
const GTS_TOKEN = "您的GoToSocial访问令牌";
const CACHE_TTL = 600; // 互动数据缓存时间(秒)
 
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname;
 
    // 处理博客Webhook(新文章发布)
    if (path === '/webhook/new-post' && request.method === 'POST') {
      return handleNewPost(request, env);
    }
 
    // 提供互动数据API
    if (path === '/api/interactions' && request.method === 'GET') {
      return getInteractions(url.searchParams, env);
    }
 
    return new Response('Not Found', { status: 404 });
  }
};
 
// 处理新文章Webhook
async function handleNewPost(request, env) {
  try {
    const { post_id, title, url, excerpt } = await request.json();
 
    // 发布到GoToSocial
    const tootResp = await fetch(`${GTS_INSTANCE}/api/v1/statuses`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${GTS_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        status: `新文章发布:${title}\n${url}\n\n${excerpt}`,
        visibility: "public"
      })
    });
 
    const tootData = await tootResp.json();
 
    // 存储映射关系到KV
    await env.BLOG_TOOT_MAPPING.put(
      `post:${post_id}`,
      JSON.stringify({
        toot_id: tootData.id,
        toot_uri: tootData.uri,
        created_at: Date.now()
      })
    );
 
    return new Response('Sync success', { status: 200 });
  } catch (err) {
    return new Response(err.message, { status: 500 });
  }
}
 
// 获取互动数据
async function getInteractions(params, env) {
  const postId = params.get('post_id');
  if (!postId) return new Response('Missing post_id', { status: 400 });
 
  // 从KV获取Toot信息
  const tootData = await env.BLOG_TOOT_MAPPING.get(`post:${postId}`);
  if (!tootData) return new Response('Mapping not found', { status: 404 });
 
  const { toot_id } = JSON.parse(tootData);
 
  // 从GoToSocial获取互动数据(带缓存)
  const gtsResponse = await fetch(
    `${GTS_INSTANCE}/api/v1/statuses/${toot_id}/context`,
    {
      headers: { 
        'Authorization': `Bearer ${GTS_TOKEN}`,
        'CF-Cache-Tag': `interactions_${toot_id}`
      },
      cf: { cacheTtl: CACHE_TTL }
    }
  );
 
  if (!gtsResponse.ok) {
    return new Response('Failed to fetch interactions', { status: 502 });
  }
 
  // 格式化响应数据
  const interactions = await gtsResponse.json();
  const formatted = {
    post_id: postId,
    toot_id: toot_id,
    replies: interactions.descendants.map(item => ({
      id: item.id,
      author: {
        name: item.account.display_name,
        avatar: item.account.avatar
      },
      content: item.content,
      created_at: item.created_at
    })),
    stats: {
      replies_count: interactions.descendants.length,
      // 其他统计信息...
    }
  };
 
  return new Response(JSON.stringify(formatted), {
    headers: { 
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*' // 按需修改
    }
  });
}

三、部署与配置

1. 部署 Worker

# 安装依赖
npm install -g wrangler
 
# 登录Cloudflare
wrangler login
 
# 发布Worker
wrangler publish

2. 配置博客 Webhook

在博客后台设置 Webhook:

  • URL: https://您的worker域名/webhook/new-post

  • 触发事件: ” 文章发布/更新 ”

  • 请求体示例(根据实际博客调整):

    {
      "post_id": "文章唯一ID",
      "title": "文章标题",
      "url": "文章完整URL",
      "excerpt": "文章摘要"
    }

四、使用流程

1. 文章发布同步流程

  1. 作者在博客发布文章
  2. 博客系统发送 Webhook 到 Worker
  3. Worker 自动:
    • 在 GoToSocial 发布 Toot
    • 存储文章 ID 与 Toot ID 的映射到 KV

2. 前端获取互动数据

在博客文章页添加:

<script>
async function loadInteractions(postId) {
  const resp = await fetch(
    `https://您的worker域名/api/interactions?post_id=${postId}`
  );
  const data = await resp.json();
  
  // 示例:渲染回复列表
  data.replies.forEach(reply => {
    const div = document.createElement('div');
    div.innerHTML = `
      <img src="${reply.author.avatar}" width="30">
      <strong>${reply.author.name}</strong>
      <p>${reply.content}</p>
    `;
    document.getElementById('comments').appendChild(div);
  });
}
// 调用示例(从页面元素获取postId)
loadInteractions('当前文章ID'); 
</script>
 
<div id="comments"><!-- 互动内容将在这里渲染 --></div>

五、增强功能建议

  1. 数据更新机制

    • 添加 /webhook/post-update 端点处理文章更新
    • 使用 KV 的 metadata 记录最后同步时间
  2. 安全增强

    // 在handleNewPost开头添加验证
    const auth = request.headers.get('Authorization');
    if (auth !== `Bearer ${WEBHOOK_SECRET}`) {
      return new Response('Unauthorized', { status: 401 });
    }
  3. 监控与日志

    • 在 Worker 中添加日志输出:

      console.log(`Synced post ${post_id} to toot ${tootData.id}`);
    • 在 Cloudflare Dashboard 查看实时日志

  4. 错误恢复

    • 添加定期同步任务(通过 Cron Trigger)
    • 实现 KV 数据校验接口

该方案完整实现了:

  1. 博客→GoToSocial 的自动同步
  2. 使用 Cloudflare KV 持久化映射关系
  3. 提供标准化 API 获取互动数据
  4. 前端友好 JSON 格式响应

根据实际需要,可以调整字段命名、缓存策略或添加更多元数据(如文章分类标签同步)。--- title: Ghost 集成 Activitypub 互动思路 tags: date: 2025-05-21 14:20:29 date modified: 2025-05-21 14:20:42