marketing-pipeline-automation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMarketing 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
undefinedbash
undefinedNode.js 18+ required
需要 Node.js 18+ 版本
node --version
node --version
Package manager
包管理器
npm --version
npm --version
or
或
pnpm --version
undefinedpnpm --version
undefinedClone and Setup
克隆与配置
bash
git clone https://github.com/pennydinh/marketing-pineline-share.git
cd marketing-pineline-sharebash
git clone https://github.com/pennydinh/marketing-pineline-share.git
cd marketing-pineline-shareInstall dependencies
安装依赖
npm install
npm install
or
或
pnpm install
undefinedpnpm install
undefinedEnvironment Configuration
环境变量配置
Create a file in the root directory:
.env.localbash
undefined在项目根目录创建文件:
.env.localbash
undefinedAI 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
undefinedNEXT_PUBLIC_API_URL=http://localhost:3000
undefinedKey Commands
核心命令
Development
开发环境
bash
undefinedbash
undefinedStart 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
undefinednpm run lint
undefinedRemotion Video Rendering
Remotion 视频渲染
bash
undefinedbash
undefinedRender 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
undefinednpm run remotion:upgrade
undefinedCore 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 variablesmarketing-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(', ')}`);
}
}