小红书多工具集成模式实战:如何连接 CLI/MCP/API 构建统一工作流

引言

在构建复杂的内容创作系统时,往往需要整合多个外部工具和服务。这些工具可能来自不同的技术栈——有些是命令行工具(CLI),有些是 Model Context Protocol(MCP)服务,有些则是 REST API。如何将这些异构工具统一集成到一条工作流中,是系统架构的核心挑战。本文通过实际案例,详细解析一种多工具集成的架构设计与实现方案。

一、工具分类与集成策略

1.1 工具类型谱系

系统涉及的工具按技术形态可分为三类:

┌─────────────────────────────────────────────────────────────┐
│                     工具类型谱系                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │    CLI      │  │    MCP      │  │    API      │          │
│  │  命令行工具  │  │ 协议服务    │  │  REST服务   │          │
│  ├─────────────┤  ├─────────────┤  ├─────────────┤          │
│  │ xhs-cli     │  │ xhs-mcp     │  │ Jina Reader │          │
│  │ redbook     │  │ douyin-mcp  │  │ Exa Search  │          │
│  │ xhs-cover   │  │ weibo-mcp   │  │ Twitter API │          │
│  └─────────────┘  └─────────────┘  └─────────────┘          │
│                                                              │
└─────────────────────────────────────────────────────────────┘

1.2 统一抽象接口设计

无论工具的技术形态如何,系统都将其抽象为统一的调用接口:

// 工具调用结果标准化
interface ToolResult<T = any> {
  ok: boolean;
  payload?: T;
  blocker?: string;  // 失败原因
}

// 工具元数据标准化
interface ToolMetadata {
  id: string;
  label: string;
  status: "ready" | "partial" | "needs_auth" | "candidate";
  detail: string;
  command: string;
}

二、CLI 工具集成

2.1 子进程调用封装

CLI 工具通过 Node.js 的子进程机制调用,封装为通用的 runCommand 函数:

function runCommand(command, args, cwd, timeoutMs = 18000) {
  return new Promise((resolve) => {
    const child = spawn(command, args, {
      cwd,
      env: { ...process.env, FORCE_COLOR: "0" },
      stdio: ["ignore", "pipe", "pipe"],
    });

    let stdout = "";
    let stderr = "";
    let timedOut = false;

    // 超时控制
    const timer = setTimeout(() => {
      timedOut = true;
      child.kill("SIGTERM");
    }, timeoutMs);

    child.stdout.on("data", (chunk) => {
      stdout += chunk.toString();
    });

    child.stderr.on("data", (chunk) => {
      stderr += chunk.toString();
    });

    child.on("close", (code) => {
      clearTimeout(timer);
      resolve({ code, stdout, stderr, timedOut });
    });
  });
}

2.2 CLI 工具调用模式

以小红书样本采集 CLI 为例:

export async function fetchNoteEvidenceWithXhsCli(noteUrl) {
  const result = await runCommand(
    "uv",                                              // 命令
    ["run", "--python", "3.11", "xhs", "read", noteUrl, "--json"],  // 参数
    xhsCliRepoDir,                                     // 工作目录
    24000                                              // 超时 24秒
  );

  // 错误处理
  if (result.timedOut) {
    return { ok: false, blocker: "xhs-cli 读取超时" };
  }
  if (result.code !== 0) {
    return { ok: false, blocker: extractError(result.stderr, result.stdout) };
  }

  // JSON 解析
  const payload = safeJsonParse(result.stdout.trim());
  if (!payload) {
    return { ok: false, blocker: "xhs-cli 返回非 JSON" };
  }

  // 数据归一化
  return { ok: true, payload: normalizeReadPayload(unwrapPayload(payload)) };
}

2.3 搜索功能集成

export async function searchNotesWithXhsCli(keyword, limit = 3) {
  const result = await runCommand(
    "uv",
    ["run", "--python", "3.11", "xhs", "search", keyword, "--json"],
    xhsCliRepoDir,
    24000
  );

  if (result.timedOut) {
    return { ok: false, blocker: "xhs-cli 搜索超时" };
  }
  if (result.code !== 0) {
    return { ok: false, blocker: extractError(result.stderr, result.stdout) || "搜索失败" };
  }

  const payload = safeJsonParse(result.stdout.trim());
  if (!payload) {
    return { ok: false, blocker: "xhs-cli 搜索返回非 JSON" };
  }

  // 搜索结果归一化
  const normalized = normalizeSearchResults(unwrapPayload(payload), limit);
  return { ok: true, payload: normalized };
}

function normalizeSearchResults(payload, limit = 3) {
  const items = Array.isArray(payload?.items) ? payload.items : [];
  return items
    .map((item) => {
      const noteCard = item.note_card || {};
      const noteId = item.id || noteCard.note_id || "";
      const xsecToken = item.xsec_token || noteCard.xsec_token || "";

      return {
        noteId,
        title: noteCard.display_title || noteCard.title || "",
        author: noteCard.user?.nickname || noteCard.user?.nick_name || "",
        url: buildNoteWebUrl(noteId, xsecToken, "pc_search"),
        metrics: normalizeMetrics(noteCard.interact_info || {}),
        raw: item,
      };
    })
    .filter(item => item.noteId && item.url)
    .slice(0, limit);
}

三、MCP 服务集成

3.1 MCP 工具目录

系统维护了一个 MCP 工具目录,记录所有可用的 MCP 服务:

const officialPlatformMatrix = [
  {
    id: "web",
    platform: "网页",
    readyOutOfBox: "阅读任意网页",
    localStatus: "ready",
    localDetail: "Jina Reader 可直接用"
  },
  {
    id: "youtube",
    platform: "YouTube",
    readyOutOfBox: "字幕提取 + 视频搜索",
    localStatus: "ready",
    localDetail: "yt-dlp 已安装"
  },
  {
    id: "xiaohongshu",
    platform: "小红书",
    unlockAfterConfig: "阅读、搜索、发帖、评论、点赞",
    localStatus: "needs_auth",
    localDetail: "xhs-cli 已安装,需登录后验证"
  },
  {
    id: "twitter",
    platform: "Twitter/X",
    readyOutOfBox: "读单条推文",
    localStatus: "ready",
    localDetail: "twitter-cli 已安装"
  },
  {
    id: "bilibili",
    platform: "B站",
    readyOutOfBox: "本地:字幕提取 + 搜索",
    localStatus: "ready",
    localDetail: "bili + yt-dlp 已安装"
  },
  // ... 更多平台
];

3.2 MCP 层分类

MCP 服务按功能层次划分:

const acquisitionLayers = [
  {
    id: "exa",
    layer1: "聚合搜索层",
    layer2: "Exa",
    status: "ready",
    delivery: "结构化搜索结果 + highlights + publishedDate",
  },
  {
    id: "jina-reader",
    layer1: "网页抓取层",
    layer2: "Jina Reader",
    status: "ready",
    delivery: "网页正文抽取",
  },
  {
    id: "firecrawl",
    layer1: "网页抓取层",
    layer2: "Firecrawl",
    status: "candidate",
    delivery: "抓取、搜索、抽取、深度站点采集",
  },
];

const contentLayers = [
  {
    id: "xiaohongshu",
    layer1: "社媒平台源",
    layer2: "小红书 / xhs-cli",
    status: "needs_auth",
    delivery: "搜索、阅读、评论、热点、feed",
  },
  {
    id: "twitter",
    layer1: "社媒平台源",
    layer2: "Twitter / X",
    status: "ready",
    delivery: "搜索、时间线、帖子全文、长文",
  },
  {
    id: "reddit",
    layer1: "社区源",
    layer2: "Reddit / rdt-cli",
    status: "needs_auth",
    delivery: "搜索、帖子、评论、subreddit",
  },
];

const deliveryLayers = [
  {
    id: "markdown",
    layer1: "文档交付",
    layer2: "Markdown",
    status: "ready",
    delivery: "原始热点池 + 基础版 + 小红书版",
  },
  {
    id: "feishu",
    layer1: "协同交付",
    layer2: "飞书",
    status: "partial",
    delivery: "飞书文档、表格、消息推送",
  },
];

四、API 服务集成

4.1 HTTP API 调用模式

对于 REST API 服务,采用 fetch 或专用 HTTP 客户端:

// 假设的外部 API 集成示例
export async function fetchWithJinaReader(url) {
  const readerUrl = `https://r.jina.ai/${encodeURIComponent(url)}`;

  try {
    const response = await fetch(readerUrl, {
      headers: {
        "Accept": "application/json",
      },
    });

    if (!response.ok) {
      return { ok: false, blocker: `HTTP ${response.status}` };
    }

    const contentType = response.headers.get("content-type");
    if (contentType?.includes("application/json")) {
      const data = await response.json();
      return { ok: true, payload: data };
    }

    const text = await response.text();
    return { ok: true, payload: { content: text } };
  } catch (err) {
    return { ok: false, blocker: err.message };
  }
}

4.2 API 错误处理与重试

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const result = await fetch(url, options);
      if (result.ok) return { ok: true, payload: await result.json() };

      // 特定状态码处理
      if (result.status === 429) {
        await sleep(Math.pow(2, i) * 1000);  // 指数退避
        continue;
      }

      return { ok: false, blocker: `HTTP ${result.status}` };
    } catch (err) {
      if (i === retries - 1) {
        return { ok: false, blocker: err.message };
      }
      await sleep(1000);
    }
  }
  return { ok: false, blocker: "重试次数耗尽" };
}

五、工具链自动编排

5.1 多工具协同管道

当单一工具无法完成目标时,系统自动编排多个工具形成管道:

export async function enrichSourcesWithTools(sources) {
  const xhsInstalled = await pathExists(path.join(xhsCliRepoDir, ".venv"));
  const redbookReady = await pathExists(path.join(redbookRepoDir, "dist", "cli.js"));

  const expanded = [];

  for (const item of sources) {
    // 搜索词类型 -> 先搜索再读取详情
    if (item.type === "search_query") {
      if (!xhsInstalled) {
        expanded.push({ ...item, toolEvidence: null, blocker: "样本搜索器未就绪" });
        continue;
      }

      const searchResult = await searchNotesWithXhsCli(item.raw, 3);
      if (!searchResult.ok) {
        expanded.push({ ...item, toolEvidence: null, blocker: searchResult.blocker });
        continue;
      }

      // 将搜索结果展开为独立条目
      searchResult.payload.forEach((result, index) => {
        expanded.push({
          id: `${item.id}-hit-${index + 1}`,
          type: "note_url",
          raw: result.title,
          url: result.url,
          manualNote: `来自搜索词:${item.raw}`,
          derivedFrom: item.id,
        });
      });
      continue;
    }

    // 主页链接类型 -> 标记阻塞(链路未通)
    if (item.type === "home_url") {
      expanded.push({
        ...item,
        toolEvidence: null,
        blocker: "主页链路未打通,需补 bridge / MCP / 发布器链路后再接。",
      });
      continue;
    }

    // 笔记链接 -> 读取 + 分析
    if (item.type === "note_url") {
      if (!xhsInstalled) {
        expanded.push({ ...item, toolEvidence: null, blocker: "样本读取器未就绪" });
        continue;
      }

      // Step 1: 读取笔记
      const evidence = await fetchNoteEvidenceWithXhsCli(item.url);
      if (!evidence.ok) {
        expanded.push({ ...item, toolEvidence: null, blocker: evidence.blocker });
        continue;
      }

      const payload = evidence.payload || {};
      const body = payload.desc || payload.content || "";

      const toolEvidence = {
        title: payload.title || item.raw || "",
        body,
        author: payload.user?.nickname || payload.nickname || "",
        noteId: payload.note_id || payload.id || "",
        webUrl: item.url,
        metrics: normalizeMetrics(payload.interact_info || {}),
        hashtags: Array.isArray(payload.tag_list)
          ? payload.tag_list.map(tag => tag.name).filter(Boolean)
          : [],
        paragraphCount: paragraphCount(body),
        preview: String(body || "").slice(0, 180),
        analysis: null,
      };

      // Step 2: 爆款分析(可选)
      if (redbookReady) {
        const analysis = await analyzeNoteWithRedbook(item.url);
        if (analysis.ok) {
          toolEvidence.analysis = analysis.payload;
        } else {
          toolEvidence.analysisBlocker = analysis.blocker;
        }
      }

      enriched.push({ ...item, toolEvidence });
    }
  }

  // 去重
  const deduped = [];
  const seenNoteUrls = new Set();
  for (const item of expanded) {
    if (item.type === "note_url" && item.url) {
      if (seenNoteUrls.has(item.url)) continue;
      seenNoteUrls.add(item.url);
    }
    deduped.push(item);
  }

  return deduped;
}

5.2 工具状态检查

系统启动时检查所有工具的就绪状态:

export async function getToolStatusCatalog() {
  const repos = await loadRepoCatalog();

  const statusMap = {
    "xiaohongshu-cli": {
      id: "xiaohongshu-cli",
      label: "样本采集器",
      status: (await pathExists(path.join(xhsCliRepoDir, ".venv")))
        ? "可读样本"
        : "待安装依赖",
      detail: "本机已验证 status / search / read",
      command: "cd repos/xhs-cli && uv run --python 3.11 xhs status --json",
    },
    redbook: {
      id: "redbook",
      label: "爆文拆解器",
      status: (await pathExists(path.join(redbookRepoDir, "dist", "cli.js")))
        ? "可做拆解"
        : "待构建",
      detail: "本机已验证 analyze-viral / viral-template",
      command: "cd repos/redbook && npm install && npm run build",
    },
    "xhs-cover-md": {
      id: "xhs-cover-md",
      label: "封面工坊",
      status: (await pathExists(path.join(xhsCoverDir, "scripts", "capture.sh")))
        ? "可出图"
        : "缺脚本",
      detail: "本机已验证 HTML -> PNG",
      command: "cd repos/xhs-cover-md && ./scripts/capture.sh input.html output.png",
    },
    // ... 更多工具
  };

  return repos.map((repo) => ({
    ...repo,
    runtime: statusMap[repo.id] || {
      id: repo.id,
      label: repo.displayName || repo.name,
      status: repo.status || "未配置",
      detail: repo.role || "",
      command: "",
    },
  }));
}

六、实战:构建完整工具链

6.1 爆款分析管道

完整的爆款分析需要多个工具协同:

export async function analyzeNoteWithRedbook(noteUrl) {
  const result = await runCommand(
    "node",
    [
      "dist/cli.js",
      "analyze-viral",
      noteUrl,
      "--cookie-source", "chrome",
      "--json"
    ],
    redbookRepoDir,
    30000  // 30秒超时
  );

  if (result.timedOut) {
    return { ok: false, blocker: "redbook analyze-viral 超时" };
  }
  if (result.code !== 0) {
    return { ok: false, blocker: extractError(result.stderr, result.stdout) };
  }

  const payload = safeJsonParse(result.stdout.trim());
  if (!payload) {
    return { ok: false, blocker: "redbook analyze-viral 返回非 JSON" };
  }

  // 数据归一化
  return { ok: true, payload: normalizeViralAnalysis(payload) };
}

function normalizeViralAnalysis(payload) {
  if (!payload || typeof payload !== "object") return null;

  return {
    note: payload.note || null,
    score: payload.score || null,
    hook: payload.hook || null,
    content: payload.content || null,
    engagement: payload.engagement || null,
    comments: payload.comments || null,
    relative: payload.relative || null,
    fetchedAt: payload.fetchedAt || "",
    summary: [
      payload.hook?.title ? `标题=${payload.hook.title}` : "",
      payload.engagement?.likes ? `点赞=${payload.engagement.likes}` : "",
      payload.engagement?.comments ? `评论=${payload.engagement.comments}` : "",
      payload.content?.paragraphCount ? `段落=${payload.content.paragraphCount}` : "",
    ].filter(Boolean).join(" · "),
  };
}

6.2 模板提取管道

export async function buildViralTemplate(noteUrls) {
  const urls = [...new Set((noteUrls || []).filter(Boolean))].slice(0, 3);

  if (!urls.length) {
    return { ok: false, blocker: "缺少样本笔记链接" };
  }

  const result = await runCommand(
    "node",
    [
      "dist/cli.js",
      "viral-template",
      ...urls,
      "--cookie-source", "chrome",
      "--json"
    ],
    redbookRepoDir,
    36000  // 36秒超时
  );

  if (result.timedOut) {
    return { ok: false, blocker: "redbook viral-template 超时" };
  }
  if (result.code !== 0) {
    return { ok: false, blocker: extractError(result.stderr, result.stdout) };
  }

  const payload = safeJsonParse(result.stdout.trim());
  if (!payload) {
    return { ok: false, blocker: "redbook viral-template 返回非 JSON" };
  }

  return { ok: true, payload: normalizeTemplateAnalysis(payload) };
}

function normalizeTemplateAnalysis(payload) {
  if (!payload || typeof payload !== "object") return null;

  return {
    dominantHookPatterns: payload.dominantHookPatterns || [],
    titleStructure: payload.titleStructure || null,
    bodyStructure: payload.bodyStructure || null,
    engagementProfile: payload.engagementProfile || null,
    audienceSignals: payload.audienceSignals || null,
    sourceNotes: payload.sourceNotes || [],
    generatedAt: payload.generatedAt || "",
  };
}

6.3 封面渲染管道

export async function renderCoverPng(htmlPath, outputPngPath) {
  const result = await runCommand(
    path.join(xhsCoverDir, "scripts", "capture.sh"),
    [htmlPath, outputPngPath],
    xhsCoverDir,
    30000
  );

  if (result.timedOut) {
    return { ok: false, blocker: "xhs-cover-md 出图超时" };
  }
  if (result.code !== 0) {
    return { ok: false, blocker: extractError(result.stderr, result.stdout) };
  }

  return { ok: true, detail: result.stdout.trim() };
}

七、工具链监控与降级

7.1 统一错误处理

function extractError(stderr, stdout) {
  const text = `${stderr || ""}\n${stdout || ""}`.trim();
  return text.split("\n").filter(Boolean).slice(-3).join(" | ");
}

// 安全 JSON 解析
function safeJsonParse(text) {
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

// 解包 payload(处理嵌套结构)
function unwrapPayload(payload) {
  if (!payload || typeof payload !== "object") return payload;
  if ("data" in payload && payload.data) return payload.data;
  if ("result" in payload && payload.result) return payload.result;
  return payload;
}

7.2 降级策略

async function buildInsights(brief, signals, sourceItems) {
  const preset = SCENARIO_PRESETS[brief.scenarioId];
  const context = fallbackContext(brief, signals);
  const rankedEvidence = evidenceItems(sourceItems);
  const topEvidence = rankedEvidence[0];

  // 尝试 LLM 生成
  try {
    const { generateInsights: llmInsights } = await import("./llm.mjs");
    const llmResult = await llmInsights({ /* ... */ });

    if (llmResult?.mainHook) {
      return { preset, context, insights: [ /* 转换结果 */ ] };
    }
  } catch (err) {
    console.warn(`LLM buildInsights failed: ${err.message}`);
  }

  // 降级:使用本地规则
  return {
    preset,
    context,
    insights: [
      {
        label: "主钩子",
        value: topEvidence?.analysis?.summary
          || "具体场景 + 明确受众 + 可执行动作",
      },
      // ...
    ],
  };
}

八、架构优势总结

8.1 工具无关性

核心编排逻辑与具体工具解耦,新增或替换工具只需在适配层处理:

// 新增工具只需实现统一接口
export async function newToolAdapter(params) {
  const result = await runCommand(/* ... */);
  return { ok: true, payload: normalizeNewToolOutput(result) };
}

8.2 状态可视化

统一的工具目录系统提供了全局工具状态视图:

const toolCatalog = await getToolStatusCatalog();
// [
//   { id: "xhs-cli", label: "样本采集器", status: "可读样本", ... },
//   { id: "redbook", label: "爆文拆解器", status: "可做拆解", ... },
//   ...
// ]

8.3 管道容错

单点故障不会导致整体流程中断:

for (const item of sources) {
  try {
    const result = await processWithTool(item);
    results.push({ ...item, result });
  } catch (err) {
    // 单条失败不影响其他
    results.push({ ...item, result: null, blocker: err.message });
  }
}

结语

本文详细解析了多工具集成的架构设计与实现方案,涵盖 CLI、MCP、API 三种技术形态的集成模式,以及工具链编排、状态管理、错误处理和降级策略。这种设计使得系统能够灵活整合各种外部工具,构建高效的自动化工作流。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐