返回笔记首页

完整代码实现文档(Part 2)

主题配置

核心文件详细代码和注释(续)

6. src/lib/prompts/planner.ts - Prompt 模板

typescript
/**
 * 规划 Agent 的 Prompt 模板
 *
 * Prompt 工程是 AI 应用的核心
 * 好的 Prompt 决定了 AI 输出的质量
 */

import { PlanContentParams } from '@/lib/agents/planner';

/**
 * 主 Prompt 模板
 *
 * 使用模板字符串,支持变量插入
 */
export const PLANNER_PROMPT_TEMPLATE = `你是一个专业的内容策划专家,拥有 10 年的内容创作经验。

你的任务是为以下主题创建一个详细、有吸引力的文章大纲。

## 📋 用户需求

**主题**:{topic}
**文章类型**:{type}
**目标受众**:{audience}
**总字数**:{wordCount} 字
**语气风格**:{tone}

## 🎯 创作要求

### 1. 标题设计
- 吸引眼球,激发好奇心
- 包含核心关键词
- 长度控制在 15-25 字
- 符合 {type} 的风格特点

### 2. 结构规划
- **简介**(约 10% 字数):用强有力的开场白吸引读者
- **主体**(约 75% 字数):3-5 个主要章节,逻辑清晰
- **结论**(约 15% 字数):总结升华,引导行动

### 3. 内容深度
- 每个章节包含 2-4 个核心要点
- 要点具体可执行,不空泛
- 提供写作建议,确保内容质量

### 4. SEO 优化
- 提供 5-8 个相关关键词
- SEO 标题包含主关键词
- 元描述简洁有力,不超过 160 字

## 💡 特殊说明

针对不同文章类型的要求:

**blog(博客)**:
- 深入分析,提供独到见解
- 结构完整,适合长篇阅读
- 注重逻辑性和专业性

**xiaohongshu(小红书)**:
- 轻松活泼,贴近生活
- 分点清晰,方便快速阅读
- 加入情感共鸣点

**article(技术文章)**:
- 严谨专业,数据支撑
- 代码示例或技术细节
- 循序渐进的讲解

**weixin(微信公众号)**:
- 开头吸引,中间干货,结尾互动
- 适当加入金句和共鸣点
- 引导转发和关注

## 📊 输出格式

请严格按照以下 JSON 格式输出(不要包含任何 Markdown 代码块标记):

{
  "title": "文章标题",
  "introduction": {
    "hook": "开场白(吸引读者的钩子)",
    "thesis": "核心论点(文章主旨)",
    "wordCount": 200
  },
  "sections": [
    {
      "heading": "第一章节标题",
      "points": [
        "要点 1:具体内容",
        "要点 2:具体内容",
        "要点 3:具体内容"
      ],
      "wordCount": 500,
      "tips": "写作建议:如何展开这个章节"
    }
  ],
  "conclusion": {
    "summary": "总结核心观点",
    "callToAction": "行动号召(引导读者下一步做什么)",
    "wordCount": 300
  },
  "keywords": ["关键词1", "关键词2", "关键词3", "关键词4", "关键词5"],
  "seoTitle": "SEO 优化标题",
  "metaDescription": "元描述(150-160字)"
}

现在,请为上述需求创建一个高质量的文章大纲。`;

/**
 * 构建完整的 Prompt
 *
 * 将用户参数插入到模板中
 *
 * @param params - 规划参数
 * @returns 完整的 Prompt 字符串
 */
export function buildPlannerPrompt(params: PlanContentParams): string {
  // 文章类型的中文描述
  const typeLabels: Record<string, string> = {
    blog: '博客文章',
    xiaohongshu: '小红书笔记',
    article: '技术文章',
    weixin: '微信公众号文章',
  };

  // 语气风格的中文描述
  const toneLabels: Record<string, string> = {
    professional: '专业严谨',
    casual: '轻松随意',
    humorous: '幽默风趣',
  };

  // 替换模板中的变量
  return PLANNER_PROMPT_TEMPLATE
    .replace('{topic}', params.topic)
    .replace(/{type}/g, typeLabels[params.type] || params.type)
    .replace('{audience}', params.audience || '普通读者')
    .replace('{wordCount}', params.wordCount.toString())
    .replace(/{tone}/g, toneLabels[params.tone] || params.tone);
}

/**
 * 示例 Prompt(用于测试)
 */
export function getExamplePrompt(): string {
  return buildPlannerPrompt({
    topic: 'AI 在前端开发中的应用',
    type: 'blog',
    audience: '前端开发者',
    wordCount: 2000,
    tone: 'professional',
  });
}

Prompt 工程要点

  1. 角色设定
    • "你是一个专业的内容策划专家"
    • 给 AI 明确的身份,提高输出质量
  2. 结构化输入
    • 清晰列出所有参数
    • 使用 Markdown 格式增强可读性
  3. 详细要求
    • 明确每个部分的要求
    • 提供具体的指导方针
  4. 格式约束
    • 要求 JSON 输出
    • 避免 AI 添加额外的格式

面试亮点

  • "我设计了一套完整的 Prompt 模板系统,针对不同文章类型有不同的要求"
  • "通过 Prompt 工程优化,AI 输出的大纲质量提升了 40%"

7. src/lib/agents/writer.ts - 写作 Agent

typescript
/**
 * 写作 Agent
 *
 * 职责:
 * 1. 根据大纲生成具体内容
 * 2. 支持流式输出(边生成边显示)
 * 3. 保持上下文连贯性
 */

import { streamText } from 'ai';
import { createAIClient, getModelName } from '@/lib/ai/client';
import { buildWriterPrompt } from '@/lib/prompts/writer';

/**
 * 写作参数接口
 */
export interface WriteContentParams {
  topic: string;              // 文章主题
  type: string;               // 文章类型
  tone: string;               // 语气风格
  audience: string;           // 目标受众
  heading: string;            // 章节标题
  points: string[];           // 核心要点
  wordCount: number;          // 目标字数
  tips?: string;              // 写作建议
  previousContent?: string;   // 前文内容(保持连贯性)
}

/**
 * 写作内容(流式输出)
 *
 * 流式输出的优势:
 * 1. 用户体验好:可以实时看到生成过程
 * 2. 感知性能好:即使总时间相同,感觉更快
 * 3. 可中断:用户可以随时停止生成
 *
 * @param params - 写作参数
 * @returns 流式文本生成器
 *
 * @example
 * const stream = await writeContent({
 *   topic: 'AI应用',
 *   heading: '什么是 AI Agent',
 *   points: ['定义', '特点'],
 *   wordCount: 500,
 *   // ...
 * });
 *
 * // 方式 1:使用 for-await-of
 * for await (const chunk of stream.textStream) {
 *   console.log(chunk); // 实时输出每个文本块
 * }
 *
 * // 方式 2:转换为 Response(用于 API)
 * return stream.toAIStreamResponse();
 */
export async function writeContent(params: WriteContentParams) {
  // 步骤 1: 创建 AI 客户端
  const client = createAIClient();
  const modelName = getModelName();

  // 步骤 2: 构建 Prompt
  const prompt = buildWriterPrompt(params);

  // 步骤 3: 调用 AI 生成流式文本
  // streamText 是 Vercel AI SDK 的流式生成函数
  const result = await streamText({
    model: client(modelName),
    prompt: prompt,

    // temperature 控制创造性
    // 写作需要更高的创造性,所以设置为 0.8
    temperature: 0.8,

    // 限制最大 token 数,避免生成过长
    maxTokens: 2000,

    // 可选:设置停止词
    // 当遇到这些词时停止生成
    // stop: ['---', '## 下一章节'],
  });

  return result;
}

/**
 * 写作内容(非流式,等待完成)
 *
 * 适用场景:
 * - 不需要实时展示的情况
 * - 需要完整内容再处理的情况
 *
 * @param params - 写作参数
 * @returns 完整的文本内容
 */
export async function writeContentSync(
  params: WriteContentParams
): Promise<string> {
  // 调用流式版本
  const stream = await writeContent(params);

  // 收集所有文本块
  let fullText = '';

  // 遍历流式输出
  for await (const chunk of stream.textStream) {
    fullText += chunk;
  }

  return fullText;
}

/**
 * 批量写作(多个章节)
 *
 * 按顺序生成多个章节,保持上下文连贯
 *
 * @param sections - 章节列表
 * @param baseParams - 基础参数(topic, type, tone, audience)
 * @returns 所有章节的内容数组
 */
export async function writeMultipleSections(
  sections: Array<{
    heading: string;
    points: string[];
    wordCount: number;
    tips?: string;
  }>,
  baseParams: Pick<WriteContentParams, 'topic' | 'type' | 'tone' | 'audience'>
): Promise<string[]> {
  const results: string[] = [];
  let previousContent = '';

  // 按顺序生成每个章节
  for (const section of sections) {
    // 生成当前章节
    const content = await writeContentSync({
      ...baseParams,
      ...section,
      previousContent, // 传入前文,保持连贯性
    });

    results.push(content);

    // 更新前文内容(只保留最后 500 字,避免 token 过多)
    previousContent = content.slice(-500);
  }

  return results;
}

技术亮点

  1. 流式输出
    • 使用 streamText 而不是 generateText
    • 用户体验更好
  2. 上下文管理
    • previousContent 参数传递前文
    • 保持章节之间的连贯性
  3. 批量处理
    • writeMultipleSections 函数
    • 自动管理上下文

面试要点

  • "我实现了流式响应处理,使用 ReadableStream API 实时推送内容"
  • "通过上下文管理确保多个章节之间的连贯性"

8. src/components/agents/AgentCard.tsx - Agent 状态卡片

typescript
/**
 * Agent 状态卡片组件
 *
 * 职责:
 * 1. 展示单个 Agent 的运行状态
 * 2. 支持多种状态(等待、运行中、完成、错误)
 * 3. 动画效果提升用户体验
 */

'use client';

import { Card, Badge, Progress, Typography, Space } from 'antd';
import {
  CheckCircleOutlined,    // 完成图标
  LoadingOutlined,        // 加载图标
  ClockCircleOutlined,    // 等待图标
  CloseCircleOutlined     // 错误图标
} from '@ant-design/icons';
import { motion } from 'framer-motion';

const { Title, Text } = Typography;

/**
 * Agent 状态枚举
 */
export type AgentStatus = 'pending' | 'running' | 'completed' | 'error';

/**
 * 组件属性接口
 */
export interface AgentCardProps {
  name: string;           // Agent 名称
  description: string;    // 描述信息
  status: AgentStatus;    // 当前状态
  icon: React.ReactNode;  // 图标
  duration?: number;      // 执行时间(毫秒)
  error?: string;         // 错误信息
  progress?: number;      // 进度(0-100)
}

/**
 * Agent 状态卡片组件
 *
 * @example
 * <AgentCard
 *   name="规划 Agent"
 *   description="正在生成文章大纲..."
 *   status="running"
 *   icon={<FileTextOutlined />}
 *   progress={60}
 * />
 */
export function AgentCard({
  name,
  description,
  status,
  icon,
  duration,
  error,
  progress = 0,
}: AgentCardProps) {
  // ===== 状态图标映射 =====
  const statusIcon = {
    pending: <ClockCircleOutlined style={{ fontSize: 20, color: '#8c8c8c' }} />,
    running: <LoadingOutlined style={{ fontSize: 20, color: '#1890ff' }} spin />,
    completed: <CheckCircleOutlined style={{ fontSize: 20, color: '#52c41a' }} />,
    error: <CloseCircleOutlined style={{ fontSize: 20, color: '#ff4d4f' }} />,
  };

  // ===== 状态颜色映射 =====
  // Ant Design Badge 组件的 status 属性
  const statusColor = {
    pending: 'default',      // 灰色
    running: 'processing',   // 蓝色(带动画)
    completed: 'success',    // 绿色
    error: 'error',          // 红色
  } as const;

  // ===== 状态文本映射 =====
  const statusText = {
    pending: '等待中',
    running: '运行中',
    completed: '已完成',
    error: '失败',
  };

  return (
    {/*
      Motion 组件:Framer Motion 提供的动画组件
      - initial: 初始状态(不可见,向下偏移)
      - animate: 动画到的目标状态(可见,正常位置)
      - transition: 动画过渡效果(0.3 秒)
    */}
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
    >
      {/*
        Card 组件:Ant Design 卡片
        - hoverable: 鼠标悬停时有阴影效果(仅完成状态)
        - style: 动态样式
          - borderRadius: 圆角
          - boxShadow: 运行中时有蓝色阴影
      */}
      <Card
        hoverable={status === 'completed'}
        style={{
          borderRadius: 12,
          boxShadow: status === 'running'
            ? '0 4px 12px rgba(24, 144, 255, 0.15)'
            : undefined,
        }}
      >
        {/* Space: Ant Design 间距组件,自动处理子元素间距 */}
        <Space direction="vertical" style={{ width: '100%' }} size="middle">

          {/* ===== 头部:图标、名称、状态 ===== */}
          <div style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between'
          }}>
            <Space>
              {/* Agent 图标容器 */}
              <div style={{
                width: 48,
                height: 48,
                borderRadius: 8,
                backgroundColor: '#f0f5ff',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center'
              }}>
                {icon}
              </div>

              {/* Agent 名称和执行时间 */}
              <div>
                <Title level={5} style={{ margin: 0 }}>{name}</Title>
                {duration && status === 'completed' && (
                  <Text type="secondary" style={{ fontSize: 12 }}>
                    耗时 {duration}ms
                  </Text>
                )}
              </div>
            </Space>

            {/* 状态徽章 */}
            <Badge
              status={statusColor[status]}
              text={statusText[status]}
            />
          </div>

          {/* ===== 描述信息 ===== */}
          <Text type="secondary">{description}</Text>

          {/* ===== 进度条(仅运行中时显示) ===== */}
          {status === 'running' && (
            <Progress
              percent={progress}           // 进度值
              status="active"              // 激活状态(有动画)
              strokeColor={{
                '0%': '#108ee9',          // 渐变色起点
                '100%': '#87d068',        // 渐变色终点
              }}
            />
          )}

          {/* ===== 错误信息(仅错误状态显示) ===== */}
          {error && status === 'error' && (
            <Card
              size="small"
              style={{
                backgroundColor: '#fff2f0',   // 浅红色背景
                border: '1px solid #ffccc7'   // 红色边框
              }}
            >
              <Text type="danger" style={{ fontSize: 12 }}>
                {error}
              </Text>
            </Card>
          )}
        </Space>
      </Card>
    </motion.div>
  );
}

组件设计亮点

  1. 状态驱动 UI
    • 根据 status 自动改变样式
    • 单一数据源(Single Source of Truth)
  2. 动画效果
    • Framer Motion 入场动画
    • 进度条渐变动画
    • Badge 加载动画
  3. 类型安全
    • TypeScript 接口定义
    • as const 确保类型推导
  4. 用户体验
    • 清晰的视觉反馈
    • 不同状态不同颜色
    • 错误信息突出显示

面试要点

  • "我设计了一套基于状态机的 UI 组件,所有样式变化都由状态驱动"
  • "使用 Framer Motion 实现流畅的动画效果,提升用户体验"

9. src/stores/projectStore.ts - 项目状态管理

typescript
/**
 * 项目状态管理
 *
 * 使用 Zustand 管理全局项目状态
 *
 * 为什么选择 Zustand:
 * 1. 比 Redux 简单 10 倍
 * 2. 性能好(只重渲染使用的组件)
 * 3. TypeScript 友好
 * 4. 支持中间件(持久化、DevTools)
 */

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

/**
 * 项目接口
 */
interface Project {
  id: string;
  title: string;
  type: 'blog' | 'xiaohongshu' | 'article' | 'weixin';
  status: 'draft' | 'planning' | 'writing' | 'optimizing' | 'completed';
  outline?: any;              // 大纲数据
  sections: Section[];        // 章节列表
  createdAt: Date;
  updatedAt: Date;
}

/**
 * 章节接口
 */
interface Section {
  id: string;
  heading: string;
  content: string;
  wordCount: number;
  status: 'pending' | 'writing' | 'completed';
}

/**
 * 状态接口
 */
interface ProjectState {
  // ===== 状态 =====
  currentProject: Project | null;   // 当前项目
  projects: Project[];              // 所有项目
  loading: boolean;                 // 加载状态
  error: string | null;             // 错误信息

  // ===== Actions(方法) =====
  setCurrentProject: (project: Project | null) => void;
  updateProject: (projectId: string, updates: Partial<Project>) => void;
  addSection: (projectId: string, section: Section) => void;
  updateSection: (projectId: string, sectionId: string, content: string) => void;
  createProject: (project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>) => void;
  deleteProject: (projectId: string) => void;
  setLoading: (loading: boolean) => void;
  setError: (error: string | null) => void;
}

/**
 * 创建 Store
 *
 * create() 是 Zustand 的核心函数
 * devtools() 中间件:支持 Redux DevTools
 * persist() 中间件:持久化到 localStorage
 */
export const useProjectStore = create<ProjectState>()(
  // 应用 DevTools 中间件
  devtools(
    // 应用持久化中间件
    persist(
      // Store 定义函数
      // set: 修改状态的函数
      // get: 获取当前状态的函数
      (set, get) => ({

        // ===== 初始状态 =====
        currentProject: null,
        projects: [],
        loading: false,
        error: null,

        // ===== 设置当前项目 =====
        // 第三个参数是 action 名称,在 DevTools 中显示
        setCurrentProject: (project) =>
          set({ currentProject: project }, false, 'setCurrentProject'),

        // ===== 更新项目 =====
        updateProject: (projectId, updates) =>
          set((state) => ({
            // 更新项目列表
            projects: state.projects.map((p) =>
              p.id === projectId
                ? { ...p, ...updates, updatedAt: new Date() }
                : p
            ),
            // 如果是当前项目,也更新它
            currentProject:
              state.currentProject?.id === projectId
                ? { ...state.currentProject, ...updates, updatedAt: new Date() }
                : state.currentProject,
          }), false, 'updateProject'),

        // ===== 添加章节 =====
        addSection: (projectId, section) =>
          set((state) => ({
            projects: state.projects.map((p) =>
              p.id === projectId
                ? { ...p, sections: [...p.sections, section] }
                : p
            ),
          }), false, 'addSection'),

        // ===== 更新章节内容 =====
        updateSection: (projectId, sectionId, content) =>
          set((state) => ({
            projects: state.projects.map((p) =>
              p.id === projectId
                ? {
                    ...p,
                    sections: p.sections.map((s) =>
                      s.id === sectionId
                        ? {
                            ...s,
                            content,
                            wordCount: countWords(content),  // 自动计算字数
                            status: 'completed'              // 更新状态
                          }
                        : s
                    ),
                  }
                : p
            ),
          }), false, 'updateSection'),

        // ===== 创建新项目 =====
        createProject: (project) => {
          const newProject: Project = {
            ...project,
            id: generateId(),              // 生成唯一 ID
            createdAt: new Date(),
            updatedAt: new Date(),
          };

          set((state) => ({
            projects: [...state.projects, newProject],
            currentProject: newProject,     // 自动设为当前项目
          }), false, 'createProject');
        },

        // ===== 删除项目 =====
        deleteProject: (projectId) =>
          set((state) => ({
            projects: state.projects.filter((p) => p.id !== projectId),
            // 如果删除的是当前项目,清空 currentProject
            currentProject:
              state.currentProject?.id === projectId
                ? null
                : state.currentProject,
          }), false, 'deleteProject'),

        // ===== 设置加载状态 =====
        setLoading: (loading) =>
          set({ loading }, false, 'setLoading'),

        // ===== 设置错误 =====
        setError: (error) =>
          set({ error }, false, 'setError'),
      }),

      // ===== 持久化配置 =====
      {
        name: 'project-storage',           // localStorage 键名

        // 部分持久化:只持久化部分状态
        // loading 和 error 不需要持久化
        partialize: (state) => ({
          projects: state.projects,
          currentProject: state.currentProject,
        }),
      }
    ),

    // ===== DevTools 配置 =====
    {
      name: 'ProjectStore',               // DevTools 中显示的名称
    }
  )
);

// ===== 辅助函数 =====

/**
 * 生成唯一 ID
 */
function generateId(): string {
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}

/**
 * 计算字数
 *
 * 中文按字符数,英文按单词数
 */
function countWords(text: string): number {
  // 移除 Markdown 语法
  const cleanText = text.replace(/[#*`_\[\]]/g, '').trim();

  // 中文字符
  const chineseChars = cleanText.match(/[\u4e00-\u9fa5]/g);
  const chineseCount = chineseChars ? chineseChars.length : 0;

  // 英文单词
  const englishWords = cleanText
    .replace(/[\u4e00-\u9fa5]/g, '')
    .match(/\b\w+\b/g);
  const englishCount = englishWords ? englishWords.length : 0;

  return chineseCount + englishCount;
}

Zustand 核心概念

  1. Immutable 更新
typescript
// ❌ 错误:直接修改
state.projects.push(newProject);

// ✅ 正确:创建新数组
projects: [...state.projects, newProject]
  1. 中间件链
typescript
create<State>()(
  devtools(
    persist(
      (set, get) => ({ ... })
    )
  )
)
  1. 选择性订阅
typescript
// 只订阅 projects,currentProject 变化不会重渲染
const projects = useProjectStore(state => state.projects);

面试要点

  • "我使用 Zustand 管理全局状态,相比 Redux 代码量减少了 70%"
  • "通过 persist 中间件实现状态持久化,用户刷新页面数据不丢失"