marketing-pipeline-automation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Marketing Pipeline Automation

营销内容流水线自动化

Skill by ara.so — Marketing Skills collection.
This skill enables AI agents to help developers build and configure an automated content creation pipeline that handles research, scriptwriting, content generation, and video rendering using Claude 3, OpenAI, and Remotion.
ara.so提供的技能——营销技能合集。
本技能可让AI Agent帮助开发者构建并配置自动化内容创作流水线,借助Claude 3、OpenAI和Remotion完成调研、脚本撰写、内容生成及视频渲染全流程。

What This Project Does

项目功能介绍

The Marketing Pipeline is an end-to-end content automation system that:
  • Auto-crawls trending news from sources like TechCrunch, a16z, Twitter/X, LinkedIn (last 24h)
  • Generates content in multiple formats (Top Lists, POV, Case Studies, How-to) using Claude/OpenAI
  • Supports bilingual output (English and Vietnamese) with customizable tone
  • Renders videos automatically from written content using Remotion
  • Optimizes for platforms like Reels, TikTok, and Shorts
营销内容流水线是一套端到端的内容自动化系统,具备以下功能:
  • 自动爬取TechCrunch、a16z、Twitter/X、LinkedIn等平台近24小时的热门资讯
  • 多格式内容生成:借助Claude/OpenAI生成榜单类、观点类、案例研究类、教程类等多种格式内容
  • 双语支持:支持英文和越南语输出,可自定义语气风格
  • 自动视频渲染:通过Remotion将文字内容自动转换为视频
  • 平台适配优化:针对Reels、TikTok、Shorts等短视频平台进行优化

Installation

安装步骤

Prerequisites

前置要求

bash
undefined
bash
undefined

Node.js 18+ required

需要 Node.js 18+ 版本

node --version
node --version

Package manager

包管理器

npm --version
npm --version

or

pnpm --version
undefined
pnpm --version
undefined

Clone and Setup

克隆与配置

bash
git clone https://github.com/pennydinh/marketing-pineline-share.git
cd marketing-pineline-share
bash
git clone https://github.com/pennydinh/marketing-pineline-share.git
cd marketing-pineline-share

Install dependencies

安装依赖

npm install
npm install

or

pnpm install
undefined
pnpm install
undefined

Environment Configuration

环境变量配置

Create a
.env.local
file in the root directory:
bash
undefined
在项目根目录创建
.env.local
文件:
bash
undefined

AI Provider Keys

AI 服务商密钥

ANTHROPIC_API_KEY=your_claude_api_key OPENAI_API_KEY=your_openai_api_key
ANTHROPIC_API_KEY=your_claude_api_key OPENAI_API_KEY=your_openai_api_key

Research & Crawling APIs

调研与爬取 API

RAPIDAPI_KEY=your_rapidapi_key
RAPIDAPI_KEY=your_rapidapi_key

Video Rendering (Remotion)

视频渲染(Remotion)

REMOTION_WEBHOOK_SECRET=your_webhook_secret
REMOTION_WEBHOOK_SECRET=your_webhook_secret

Database (if applicable)

数据库(如需使用)

DATABASE_URL=postgresql://user:password@localhost:5432/marketing_pipeline
DATABASE_URL=postgresql://user:password@localhost:5432/marketing_pipeline

Next.js Configuration

Next.js 配置

NEXT_PUBLIC_API_URL=http://localhost:3000
undefined
NEXT_PUBLIC_API_URL=http://localhost:3000
undefined

Key Commands

核心命令

Development

开发环境

bash
undefined
bash
undefined

Start development server

启动开发服务器

npm run dev
npm run dev

Build for production

构建生产版本

npm run build
npm run build

Start production server

启动生产服务器

npm run start
npm run start

Run linter

运行代码检查

npm run lint
undefined
npm run lint
undefined

Remotion Video Rendering

Remotion 视频渲染

bash
undefined
bash
undefined

Render single video

渲染单个视频

npm run remotion:render
npm run remotion:render

Preview Remotion composition

预览 Remotion 合成内容

npm run remotion:preview
npm run remotion:preview

Upgrade Remotion

升级 Remotion

npm run remotion:upgrade
undefined
npm run remotion:upgrade
undefined

Core Architecture

核心架构

Project Structure

项目结构

marketing-pineline-share/
├── src/
│   ├── app/              # Next.js app directory
│   ├── components/       # React components
│   ├── lib/
│   │   ├── ai/          # AI integration (Claude, OpenAI)
│   │   ├── crawlers/    # Content crawling modules
│   │   ├── generators/  # Content generation logic
│   │   └── video/       # Video rendering (Remotion)
│   └── utils/           # Utility functions
├── public/              # Static assets
├── remotion/            # Remotion video templates
└── .env.local          # Environment variables
marketing-pineline-share/
├── src/
│   ├── app/              # Next.js 应用目录
│   ├── components/       # React 组件
│   ├── lib/
│   │   ├── ai/          # AI 集成模块(Claude、OpenAI)
│   │   ├── crawlers/    # 内容爬取模块
│   │   ├── generators/  # 内容生成逻辑
│   │   └── video/       # 视频渲染模块(Remotion)
│   └── utils/           # 工具函数
├── public/              # 静态资源
├── remotion/            # Remotion 视频模板
└── .env.local          # 环境变量文件

API Usage Patterns

API 使用示例

1. Content Research & Crawling

1. 内容调研与爬取

typescript
// src/lib/crawlers/news-crawler.ts
import axios from 'axios';

interface NewsSource {
  title: string;
  url: string;
  publishedAt: string;
  content: string;
}

export async function crawlTechNews(keyword: string): Promise<NewsSource[]> {
  const options = {
    method: 'GET',
    url: 'https://news-api.p.rapidapi.com/v1/search',
    params: {
      q: keyword,
      lang: 'en',
      sort_by: 'published_at',
      from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Last 24h
    },
    headers: {
      'X-RapidAPI-Key': process.env.RAPIDAPI_KEY!,
      'X-RapidAPI-Host': 'news-api.p.rapidapi.com'
    }
  };

  try {
    const response = await axios.request(options);
    return response.data.articles.slice(0, 10);
  } catch (error) {
    console.error('News crawling error:', error);
    return [];
  }
}
typescript
// src/lib/crawlers/news-crawler.ts
import axios from 'axios';

interface NewsSource {
  title: string;
  url: string;
  publishedAt: string;
  content: string;
}

export async function crawlTechNews(keyword: string): Promise<NewsSource[]> {
  const options = {
    method: 'GET',
    url: 'https://news-api.p.rapidapi.com/v1/search',
    params: {
      q: keyword,
      lang: 'en',
      sort_by: 'published_at',
      from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // 近24小时
    },
    headers: {
      'X-RapidAPI-Key': process.env.RAPIDAPI_KEY!,
      'X-RapidAPI-Host': 'news-api.p.rapidapi.com'
    }
  };

  try {
    const response = await axios.request(options);
    return response.data.articles.slice(0, 10);
  } catch (error) {
    console.error('News crawling error:', error);
    return [];
  }
}

2. AI Content Generation with Claude

2. 基于 Claude 的 AI 内容生成

typescript
// src/lib/ai/claude-generator.ts
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
});

interface ContentRequest {
  keyword: string;
  format: 'toplist' | 'pov' | 'case-study' | 'how-to';
  language: 'en' | 'vi';
  tone: 'expert' | 'friendly' | 'humorous';
  researchData: string;
}

export async function generateContentWithClaude(
  request: ContentRequest
): Promise<string> {
  const systemPrompt = `You are an expert content creator specializing in ${request.format} format. 
  Write in ${request.language === 'vi' ? 'Vietnamese' : 'English'} with a ${request.tone} tone.
  Use the provided research data to create data-backed, trending content.`;

  const userPrompt = `Create a ${request.format} article about "${request.keyword}" using this research:
  
${request.researchData}

Requirements:
- Include real data and insights from the research
- Make it engaging and trend-focused
- Optimize for social media sharing
- Add relevant statistics and quotes`;

  const message = await anthropic.messages.create({
    model: 'claude-3-5-sonnet-20241022',
    max_tokens: 4096,
    system: systemPrompt,
    messages: [
      {
        role: 'user',
        content: userPrompt,
      },
    ],
  });

  return message.content[0].type === 'text' ? message.content[0].text : '';
}
typescript
// src/lib/ai/claude-generator.ts
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
});

interface ContentRequest {
  keyword: string;
  format: 'toplist' | 'pov' | 'case-study' | 'how-to';
  language: 'en' | 'vi';
  tone: 'expert' | 'friendly' | 'humorous';
  researchData: string;
}

export async function generateContentWithClaude(
  request: ContentRequest
): Promise<string> {
  const systemPrompt = `You are an expert content creator specializing in ${request.format} format. 
  Write in ${request.language === 'vi' ? 'Vietnamese' : 'English'} with a ${request.tone} tone.
  Use the provided research data to create data-backed, trending content.`;

  const userPrompt = `Create a ${request.format} article about "${request.keyword}" using this research:
  
${request.researchData}

Requirements:
- Include real data and insights from the research
- Make it engaging and trend-focused
- Optimize for social media sharing
- Add relevant statistics and quotes`;

  const message = await anthropic.messages.create({
    model: 'claude-3-5-sonnet-20241022',
    max_tokens: 4096,
    system: systemPrompt,
    messages: [
      {
        role: 'user',
        content: userPrompt,
      },
    ],
  });

  return message.content[0].type === 'text' ? message.content[0].text : '';
}

3. OpenAI Alternative

3. OpenAI 替代方案

typescript
// src/lib/ai/openai-generator.ts
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY!,
});

export async function generateContentWithOpenAI(
  request: ContentRequest
): Promise<string> {
  const completion = await openai.chat.completions.create({
    model: 'gpt-4-turbo-preview',
    messages: [
      {
        role: 'system',
        content: `You are an expert content creator specializing in ${request.format} format.`,
      },
      {
        role: 'user',
        content: `Create a ${request.format} article about "${request.keyword}" using this research:\n\n${request.researchData}`,
      },
    ],
    temperature: 0.7,
    max_tokens: 3000,
  });

  return completion.choices[0].message.content || '';
}
typescript
// src/lib/ai/openai-generator.ts
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY!,
});

export async function generateContentWithOpenAI(
  request: ContentRequest
): Promise<string> {
  const completion = await openai.chat.completions.create({
    model: 'gpt-4-turbo-preview',
    messages: [
      {
        role: 'system',
        content: `You are an expert content creator specializing in ${request.format} format.`,
      },
      {
        role: 'user',
        content: `Create a ${request.format} article about "${request.keyword}" using this research:\n\n${request.researchData}`,
      },
    ],
    temperature: 0.7,
    max_tokens: 3000,
  });

  return completion.choices[0].message.content || '';
}

4. Video Generation with Remotion

4. 基于 Remotion 的视频生成

typescript
// src/lib/video/render-video.ts
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import path from 'path';

interface VideoConfig {
  title: string;
  content: string;
  aspectRatio: '9:16' | '16:9' | '1:1'; // TikTok, YouTube, Instagram
}

export async function renderContentVideo(
  config: VideoConfig
): Promise<string> {
  const compositionId = 'ContentVideo';
  const bundleLocation = await bundle({
    entryPoint: path.resolve('./remotion/index.ts'),
    webpackOverride: (config) => config,
  });

  const composition = await selectComposition({
    serveUrl: bundleLocation,
    id: compositionId,
    inputProps: {
      title: config.title,
      content: config.content,
      aspectRatio: config.aspectRatio,
    },
  });

  const outputLocation = path.join(
    process.cwd(),
    'public',
    'videos',
    `${Date.now()}.mp4`
  );

  await renderMedia({
    composition,
    serveUrl: bundleLocation,
    codec: 'h264',
    outputLocation,
    inputProps: composition.props,
  });

  return outputLocation;
}
typescript
// src/lib/video/render-video.ts
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import path from 'path';

interface VideoConfig {
  title: string;
  content: string;
  aspectRatio: '9:16' | '16:9' | '1:1'; // TikTok、YouTube、Instagram 适配
}

export async function renderContentVideo(
  config: VideoConfig
): Promise<string> {
  const compositionId = 'ContentVideo';
  const bundleLocation = await bundle({
    entryPoint: path.resolve('./remotion/index.ts'),
    webpackOverride: (config) => config,
  });

  const composition = await selectComposition({
    serveUrl: bundleLocation,
    id: compositionId,
    inputProps: {
      title: config.title,
      content: config.content,
      aspectRatio: config.aspectRatio,
    },
  });

  const outputLocation = path.join(
    process.cwd(),
    'public',
    'videos',
    `${Date.now()}.mp4`
  );

  await renderMedia({
    composition,
    serveUrl: bundleLocation,
    codec: 'h264',
    outputLocation,
    inputProps: composition.props,
  });

  return outputLocation;
}

5. Complete Pipeline Implementation

5. 完整流水线实现

typescript
// src/lib/pipeline/content-pipeline.ts
import { crawlTechNews } from '../crawlers/news-crawler';
import { generateContentWithClaude } from '../ai/claude-generator';
import { renderContentVideo } from '../video/render-video';

export interface PipelineConfig {
  keyword: string;
  format: 'toplist' | 'pov' | 'case-study' | 'how-to';
  language: 'en' | 'vi';
  tone: 'expert' | 'friendly' | 'humorous';
  generateVideo: boolean;
  videoAspectRatio?: '9:16' | '16:9' | '1:1';
}

export async function runContentPipeline(config: PipelineConfig) {
  console.log(`Starting pipeline for keyword: ${config.keyword}`);

  // Step 1: Research & Crawl
  console.log('Step 1: Crawling news sources...');
  const newsArticles = await crawlTechNews(config.keyword);
  const researchData = newsArticles
    .map(
      (article) =>
        `Title: ${article.title}\nURL: ${article.url}\nContent: ${article.content}\n---`
    )
    .join('\n\n');

  // Step 2: Generate Content
  console.log('Step 2: Generating content with AI...');
  const generatedContent = await generateContentWithClaude({
    keyword: config.keyword,
    format: config.format,
    language: config.language,
    tone: config.tone,
    researchData,
  });

  // Step 3: Render Video (if enabled)
  let videoPath: string | null = null;
  if (config.generateVideo) {
    console.log('Step 3: Rendering video...');
    videoPath = await renderContentVideo({
      title: config.keyword,
      content: generatedContent,
      aspectRatio: config.videoAspectRatio || '16:9',
    });
  }

  return {
    content: generatedContent,
    videoPath,
    sources: newsArticles,
    timestamp: new Date().toISOString(),
  };
}
typescript
// src/lib/pipeline/content-pipeline.ts
import { crawlTechNews } from '../crawlers/news-crawler';
import { generateContentWithClaude } from '../ai/claude-generator';
import { renderContentVideo } from '../video/render-video';

export interface PipelineConfig {
  keyword: string;
  format: 'toplist' | 'pov' | 'case-study' | 'how-to';
  language: 'en' | 'vi';
  tone: 'expert' | 'friendly' | 'humorous';
  generateVideo: boolean;
  videoAspectRatio?: '9:16' | '16:9' | '1:1';
}

export async function runContentPipeline(config: PipelineConfig) {
  console.log(`Starting pipeline for keyword: ${config.keyword}`);

  // 步骤1:调研与爬取
  console.log('Step 1: Crawling news sources...');
  const newsArticles = await crawlTechNews(config.keyword);
  const researchData = newsArticles
    .map(
      (article) =>
        `Title: ${article.title}\nURL: ${article.url}\nContent: ${article.content}\n---`
    )
    .join('\n\n');

  // 步骤2:内容生成
  console.log('Step 2: Generating content with AI...');
  const generatedContent = await generateContentWithClaude({
    keyword: config.keyword,
    format: config.format,
    language: config.language,
    tone: config.tone,
    researchData,
  });

  // 步骤3:视频渲染(若启用)
  let videoPath: string | null = null;
  if (config.generateVideo) {
    console.log('Step 3: Rendering video...');
    videoPath = await renderContentVideo({
      title: config.keyword,
      content: generatedContent,
      aspectRatio: config.videoAspectRatio || '16:9',
    });
  }

  return {
    content: generatedContent,
    videoPath,
    sources: newsArticles,
    timestamp: new Date().toISOString(),
  };
}

Next.js API Route Example

Next.js API 路由示例

typescript
// src/app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { runContentPipeline } from '@/lib/pipeline/content-pipeline';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    
    const result = await runContentPipeline({
      keyword: body.keyword,
      format: body.format || 'toplist',
      language: body.language || 'en',
      tone: body.tone || 'expert',
      generateVideo: body.generateVideo || false,
      videoAspectRatio: body.videoAspectRatio || '16:9',
    });

    return NextResponse.json({
      success: true,
      data: result,
    });
  } catch (error) {
    console.error('Pipeline error:', error);
    return NextResponse.json(
      { success: false, error: 'Content generation failed' },
      { status: 500 }
    );
  }
}
typescript
// src/app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { runContentPipeline } from '@/lib/pipeline/content-pipeline';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    
    const result = await runContentPipeline({
      keyword: body.keyword,
      format: body.format || 'toplist',
      language: body.language || 'en',
      tone: body.tone || 'expert',
      generateVideo: body.generateVideo || false,
      videoAspectRatio: body.videoAspectRatio || '16:9',
    });

    return NextResponse.json({
      success: true,
      data: result,
    });
  } catch (error) {
    console.error('Pipeline error:', error);
    return NextResponse.json(
      { success: false, error: 'Content generation failed' },
      { status: 500 }
    );
  }
}

Frontend Component Example

前端组件示例

typescript
// src/components/ContentGenerator.tsx
'use client';

import { useState } from 'react';

export default function ContentGenerator() {
  const [keyword, setKeyword] = useState('');
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState<any>(null);

  const handleGenerate = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          keyword,
          format: 'toplist',
          language: 'en',
          tone: 'expert',
          generateVideo: true,
          videoAspectRatio: '16:9',
        }),
      });

      const data = await response.json();
      setResult(data.data);
    } catch (error) {
      console.error('Generation failed:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="p-6">
      <input
        type="text"
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        placeholder="Enter keyword..."
        className="border p-2 rounded w-full"
      />
      <button
        onClick={handleGenerate}
        disabled={loading}
        className="mt-4 bg-blue-600 text-white px-6 py-2 rounded"
      >
        {loading ? 'Generating...' : 'Generate Content'}
      </button>

      {result && (
        <div className="mt-6">
          <h3 className="font-bold">Generated Content:</h3>
          <div className="prose">{result.content}</div>
          {result.videoPath && (
            <video src={result.videoPath} controls className="mt-4" />
          )}
        </div>
      )}
    </div>
  );
}
typescript
// src/components/ContentGenerator.tsx
'use client';

import { useState } from 'react';

export default function ContentGenerator() {
  const [keyword, setKeyword] = useState('');
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState<any>(null);

  const handleGenerate = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          keyword,
          format: 'toplist',
          language: 'en',
          tone: 'expert',
          generateVideo: true,
          videoAspectRatio: '16:9',
        }),
      });

      const data = await response.json();
      setResult(data.data);
    } catch (error) {
      console.error('Generation failed:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="p-6">
      <input
        type="text"
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        placeholder="输入关键词..."
        className="border p-2 rounded w-full"
      />
      <button
        onClick={handleGenerate}
        disabled={loading}
        className="mt-4 bg-blue-600 text-white px-6 py-2 rounded"
      >
        {loading ? '生成中...' : '生成内容'}
      </button>

      {result && (
        <div className="mt-6">
          <h3 className="font-bold">生成内容:</h3>
          <div className="prose">{result.content}</div>
          {result.videoPath && (
            <video src={result.videoPath} controls className="mt-4" />
          )}
        </div>
      )}
    </div>
  );
}

Remotion Video Template

Remotion 视频模板

typescript
// remotion/ContentVideo.tsx
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';

interface ContentVideoProps {
  title: string;
  content: string;
  aspectRatio: '9:16' | '16:9' | '1:1';
}

export const ContentVideo: React.FC<ContentVideoProps> = ({
  title,
  content,
}) => {
  const frame = useCurrentFrame();
  const opacity = interpolate(frame, [0, 30], [0, 1]);

  return (
    <AbsoluteFill
      style={{
        backgroundColor: '#000',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      <div style={{ opacity }}>
        <h1 style={{ color: '#fff', fontSize: '48px' }}>{title}</h1>
        <p style={{ color: '#fff', fontSize: '24px', marginTop: '20px' }}>
          {content.substring(0, 200)}...
        </p>
      </div>
    </AbsoluteFill>
  );
};
typescript
// remotion/ContentVideo.tsx
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';

interface ContentVideoProps {
  title: string;
  content: string;
  aspectRatio: '9:16' | '16:9' | '1:1';
}

export const ContentVideo: React.FC<ContentVideoProps> = ({
  title,
  content,
}) => {
  const frame = useCurrentFrame();
  const opacity = interpolate(frame, [0, 30], [0, 1]);

  return (
    <AbsoluteFill
      style={{
        backgroundColor: '#000',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      <div style={{ opacity }}>
        <h1 style={{ color: '#fff', fontSize: '48px' }}>{title}</h1>
        <p style={{ color: '#fff', fontSize: '24px', marginTop: '20px' }}>
          {content.substring(0, 200)}...
        </p>
      </div>
    </AbsoluteFill>
  );
};

Common Patterns

常用模式

Batch Content Generation

批量内容生成

typescript
async function generateBatchContent(keywords: string[]) {
  const results = await Promise.all(
    keywords.map((keyword) =>
      runContentPipeline({
        keyword,
        format: 'toplist',
        language: 'en',
        tone: 'expert',
        generateVideo: false,
      })
    )
  );
  return results;
}
typescript
async function generateBatchContent(keywords: string[]) {
  const results = await Promise.all(
    keywords.map((keyword) =>
      runContentPipeline({
        keyword,
        format: 'toplist',
        language: 'en',
        tone: 'expert',
        generateVideo: false,
      })
    )
  );
  return results;
}

Scheduled Content Pipeline

定时内容流水线

typescript
// Use with cron or task scheduler
import cron from 'node-cron';

cron.schedule('0 9 * * *', async () => {
  // Run daily at 9 AM
  const trendingKeywords = ['AI', 'Web3', 'Marketing Automation'];
  await generateBatchContent(trendingKeywords);
});
typescript
// 配合 cron 或任务调度器使用
import cron from 'node-cron';

cron.schedule('0 9 * * *', async () => {
  // 每天上午9点运行
  const trendingKeywords = ['AI', 'Web3', 'Marketing Automation'];
  await generateBatchContent(trendingKeywords);
});

Troubleshooting

故障排查

API Rate Limits

API 速率限制

typescript
// Implement rate limiting
import pLimit from 'p-limit';

const limit = pLimit(3); // Max 3 concurrent requests

const results = await Promise.all(
  keywords.map((keyword) =>
    limit(() => runContentPipeline({ keyword, /* ... */ }))
  )
);
typescript
// 实现速率限制
import pLimit from 'p-limit';

const limit = pLimit(3); // 最大3个并发请求

const results = await Promise.all(
  keywords.map((keyword) =>
    limit(() => runContentPipeline({ keyword, /* ... */ }))
  )
);

Video Rendering Timeout

视频渲染超时

typescript
// Increase timeout in Remotion config
await renderMedia({
  composition,
  serveUrl: bundleLocation,
  codec: 'h264',
  outputLocation,
  timeoutInMilliseconds: 300000, // 5 minutes
});
typescript
// 在 Remotion 配置中增加超时时间
await renderMedia({
  composition,
  serveUrl: bundleLocation,
  codec: 'h264',
  outputLocation,
  timeoutInMilliseconds: 300000, // 5分钟
});

Claude/OpenAI Error Handling

Claude/OpenAI 错误处理

typescript
async function generateWithRetry(request: ContentRequest, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await generateContentWithClaude(request);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}
typescript
async function generateWithRetry(request: ContentRequest, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await generateContentWithClaude(request);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

Environment Variable Validation

环境变量验证

typescript
// src/lib/utils/validate-env.ts
export function validateEnv() {
  const required = [
    'ANTHROPIC_API_KEY',
    'OPENAI_API_KEY',
    'RAPIDAPI_KEY',
  ];

  const missing = required.filter((key) => !process.env[key]);

  if (missing.length > 0) {
    throw new Error(`Missing environment variables: ${missing.join(', ')}`);
  }
}
typescript
// src/lib/utils/validate-env.ts
export function validateEnv() {
  const required = [
    'ANTHROPIC_API_KEY',
    'OPENAI_API_KEY',
    'RAPIDAPI_KEY',
  ];

  const missing = required.filter((key) => !process.env[key]);

  if (missing.length > 0) {
    throw new Error(`缺失环境变量:${missing.join(', ')}`);
  }
}