PublisherManager - 发布管理器 
PublisherManager 负责协调处理后的文章同时发布到多个平台。它管理发布插件并协调发布工作流程。
概述 
PublisherManager 类:
- 管理多个发布插件
 - 并行发布内容到所有注册的平台
 - 跟踪已发布的文章以便后续更新
 - 提供统一的错误处理和结果报告
 
构造函数 
ts
class PublisherManager {
  constructor(content: string);
}参数:
content(string): 来自 ArticleProcessor 的处理后的 Markdown 内容
方法 
addPlugin 
为特定平台注册发布插件。
ts
addPlugin(plugin: PublisherPlugin): void参数:
plugin(PublisherPlugin): 平台特定的发布插件
示例:
ts
const manager = new PublisherManager(content);
manager.addPlugin(NotionPublisherPlugin({ api_key: "...", page_id: "..." }));
manager.addPlugin(DevToPublisherPlugin({ api_key: "...", published: false }));publish 
同时将文章发布到所有注册的平台。
ts
publish(): Promise<PublishResult[]>返回值:
Promise<PublishResult[]>: 每个平台的结果数组
ts
interface PublishResult {
  pid?: string; // 平台特定的文章 ID
  name?: string; // 插件名称
  success: boolean; // 发布是否成功
  info?: string; // 成功/错误消息
}PublisherPlugin 接口 
所有发布插件必须实现此接口:
ts
interface PublisherPlugin {
  extendsParam?(extendsParam: ExtendsParam): PublisherPlugin; // 接收跟踪的文章数据
  process(articleTitle: string, visit: TVisitor, toMarkdown: ToMarkdown): Promise<PublishResult>;
  update?(article_id: string | undefined, articleTitle: string, content: string): Promise<void>; // 更新现有文章
  name: string; // 插件标识符
  isTraceUpdate?: boolean; // 跟踪以便后续更新
}
interface ExtendsParam {
  pid?: string; // 跟踪中的现有文章 ID
}
type ToMarkdown = () => { content: string };插件参数 
articleTitle: 从第一个 H1 标题提取的文章标题visit: 用于平台特定内容转换的 AST 访问器toMarkdown: 将修改后的 AST 转换回 Markdown 的函数
完整示例 
基础多平台发布 
ts
import { ArticleProcessor, PublisherManager, NotionPublisherPlugin, DevToPublisherPlugin, NativePublisherPlugin } from "@artipub/core";
import path from "path";
// 步骤 1:处理文章
const processor = new ArticleProcessor({
  uploadImgOption: {
    owner: process.env.GITHUB_OWNER!,
    repo: process.env.GITHUB_REPO!,
    dir: "images",
    branch: "main",
    token: process.env.GITHUB_TOKEN!,
    commit_author: "Bot",
    commit_email: "bot@example.com",
  },
});
const { content } = await processor.processMarkdown(path.resolve(__dirname, "./articles/my-post.md"));
// 步骤 2:设置发布器
const publisher = new PublisherManager(content);
// 添加 Notion 发布器
publisher.addPlugin(
  NotionPublisherPlugin({
    api_key: process.env.NOTION_API_KEY!,
    page_id: process.env.NOTION_PAGE_ID!,
  })
);
// 添加 Dev.to 发布器
publisher.addPlugin(
  DevToPublisherPlugin({
    api_key: process.env.DEVTO_API_KEY!,
    published: false, // 保存为草稿
    series: "我的教程系列",
    main_image: "https://example.com/cover.jpg",
    description: "文章描述用于 SEO",
  })
);
// 添加本地文件发布器
publisher.addPlugin(
  NativePublisherPlugin({
    destination_path: "/home/user/blog/content/posts",
    cdn_prefix: "https://cdn.jsdelivr.net/gh",
    res_domain: "raw.githubusercontent.com",
  })
);
// 步骤 3:发布到所有平台
const results = await publisher.publish();
// 步骤 4:处理结果
results.forEach((result) => {
  if (result.success) {
    console.log(`✅ ${result.name}: ${result.info}`);
  } else {
    console.error(`❌ ${result.name}: ${result.info}`);
  }
});高级:自定义发布插件 
ts
import { PublisherPlugin, PublishResult } from "@artipub/core";
import axios from "axios";
// 为您的平台创建自定义发布器
function CustomBlogPublisher(options: CustomBlogOptions): PublisherPlugin {
  return {
    name: "CustomBlog",
    isTraceUpdate: true, // 启用更新跟踪
    extendsParam(params) {
      // 扩展现有文章 ID 以进行更新
      if (params.pid) {
        options.postId = params.pid;
      }
    },
    async process(articleTitle, visit, toMarkdown) {
      try {
        // 为您的平台转换内容
        visit("image", (node) => {
          // 将图片 URL 转换为您的 CDN
          node.url = `https://mycdn.com/proxy?url=${encodeURIComponent(node.url)}`;
        });
        // 删除您的平台不支持的元素
        visit("html", (node, index, parent) => {
          parent.children.splice(index, 1);
        });
        // 获取转换后的内容
        const { content } = toMarkdown();
        // 发布到您的平台
        const endpoint = options.postId ? `https://api.myblog.com/posts/${options.postId}` : "https://api.myblog.com/posts";
        const response = await axios({
          method: options.postId ? "PUT" : "POST",
          url: endpoint,
          headers: {
            Authorization: `Bearer ${options.apiKey}`,
            "Content-Type": "application/json",
          },
          data: {
            title: articleTitle,
            content: content,
            tags: options.tags,
            draft: options.draft,
          },
        });
        return {
          success: true,
          info: `发布到 CustomBlog: ${response.data.url}`,
          pid: response.data.id, // 保存以便后续更新
        };
      } catch (error) {
        return {
          success: false,
          info: `发布失败: ${error.message}`,
        };
      }
    },
  };
}
// 使用自定义发布器
const publisher = new PublisherManager(content);
publisher.addPlugin(
  CustomBlogPublisher({
    apiKey: process.env.CUSTOM_BLOG_API_KEY!,
    tags: ["教程", "javascript"],
    draft: false,
  })
);文章更新跟踪 
当 isTraceUpdate 启用时,ArtiPub 会自动跟踪已发布的文章以便更新:
首次发布:
- 生成唯一的文章 ID
 - 在 
postMapRecords.json中存储平台特定的文章 ID 
后续更新:
- 检索现有的文章 ID
 - 更新现有文章而不是创建重复项
 
postMapRecords.json 结构 
json
{
  "article_unique_id_123": {
    "NotionPublisher": {
      "k": "notion-page-id-456"
    },
    "DevToPublisher": {
      "k": "devto-article-id-789"
    }
  }
}错误处理 
单个插件失败 
默认情况下,如果一个插件失败,其他插件会继续发布:
ts
const results = await publisher.publish();
const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success);
console.log(`成功发布到 ${successful.length}/${results.length} 个平台`);
if (failed.length > 0) {
  console.error(
    "失败的平台:",
    failed.map((f) => f.name)
  );
}快速失败策略 
在第一次失败时停止:
ts
class StrictPublisherManager extends PublisherManager {
  async publish() {
    const results = [];
    for (const plugin of this.plugins) {
      const result = await plugin.process(/* ... */);
      if (!result.success) {
        throw new Error(`发布失败: ${result.info}`);
      }
      results.push(result);
    }
    return results;
  }
}性能优化 
并行发布 
默认情况下,PublisherManager 并行发布到所有平台以获得最佳性能:
ts
// 所有平台同时发布
const results = await publisher.publish();顺序发布 
对于有速率限制的 API 或有序依赖:
ts
async function publishSequentially(publisher: PublisherManager) {
  const results = [];
  // 逐个发布
  for (const plugin of publisher.plugins) {
    const result = await plugin.process(/* ... */);
    results.push(result);
    // 如果需要,添加延迟
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
  return results;
}最佳实践 
环境变量:将 API 密钥和敏感数据存储在环境变量中
bashNOTION_API_KEY=secret_xxx DEVTO_API_KEY=xxx错误恢复:为暂时性故障实现重试逻辑
tsasync function publishWithRetry(publisher, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const results = await publisher.publish(); const failed = results.filter((r) => !r.success); if (failed.length === 0) return results; console.log(`重试 ${i + 1}/${maxRetries} 失败的平台...`); await new Promise((resolve) => setTimeout(resolve, 2000)); } }日志记录:实现全面的日志记录以便调试
tspublisher.addPlugin(withLogging(NotionPublisherPlugin(options), "Notion")); function withLogging(plugin, name) { const original = plugin.process; plugin.process = async (...args) => { console.log(`发布到 ${name}...`); const start = Date.now(); try { const result = await original(...args); console.log(`${name} 在 ${Date.now() - start}ms 内完成`); return result; } catch (error) { console.error(`${name} 失败:`, error); throw error; } }; return plugin; }验证:发布前验证内容
tsif (!content || content.trim().length === 0) { throw new Error("内容为空"); } if (!content.includes("# ")) { console.warn("内容中未找到标题"); }