'use client'; import { useEffect, useState, type MouseEvent, type ReactNode } from 'react'; import { Terminal, ArrowUpRight, ArrowRight, ArrowLeft, Code2, Database, Layers3, ListTodo, MemoryStick, Container, Server, Activity, BookOpen, Download, Monitor, X, Check, Copy, Clock3, Video as VideoIcon, ChevronRight, FileCode2, ExternalLink, } from 'lucide-react'; import { Sidebar, SidebarProvider, SidebarContent, SidebarHeader, SidebarFooter, SidebarTrigger, useSidebar, } from '@/components/ui/sidebar'; import { Button } from '@/components/ui/button'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell, } from '@/components/ui/table'; import { ProtocolDemo, HttpDemo, AsyncDemo } from './demos'; import type { Lesson, Video } from './lesson-types'; import lessonData from './lessons.json'; import videoData from './videos.json'; const lessons = lessonData as Lesson[]; const videos = videoData as Record; const modules = [ { name: 'API 服务开发', slug: 'api', icon: Code2 }, { name: '数据库开发', slug: 'database', icon: Database }, { name: 'API + 数据库', slug: 'api-database', icon: Layers3 }, { name: '异步任务开发', slug: 'tasks', icon: ListTodo }, { name: 'Redis 缓存', slug: 'redis', icon: MemoryStick }, { name: 'Docker 容器化', slug: 'docker', icon: Container }, { name: 'Linux 部署', slug: 'linux', icon: Server }, { name: '线上问题排查', slug: 'troubleshooting', icon: Activity }, ]; const phases = [ { name: '起步与 HTTP', range: '01—04', description: '让服务运行,读懂请求,约束输入与输出。', from: 0, to: 4, }, { name: 'REST 接口', range: '05—08', description: '围绕资源编写增删改查,处理错误与公共依赖。', from: 4, to: 8, }, { name: '异步与调用', range: '09—10', description: '理解 I/O 等待,完成服务调用与浏览器联调。', from: 8, to: 10, }, { name: '流式与实时通信', range: '11—16', description: '逐段读取响应,建立双向连接,完成接口验收。', from: 10, to: 16, }, ]; const lessonSections = [ ['objectives', '本节目标'], ['video', '课程视频'], ['concepts', '必要概念'], ['steps', '动手实现'], ['checks', '验收结果'], ['pitfalls', '常见错误'], ['exercise', '课后练习'], ['references', '技术文档'], ]; const overviewSections = [ ['path', '学习路径'], ['communication', '通信方式演示'], ['project', '贯穿项目'], ['ready', '完成标准'], ]; type Navigate = (module: string, lesson?: string) => void; function routeLink(module: string, lesson?: string) { return lesson ? `?lesson=${lesson}` : module === 'api' ? '?module=api' : `?module=${module}`; } function LocalLink({ module = 'api', lesson, navigate, children, className, ...props }: { module?: string; lesson?: string; navigate: Navigate; children: ReactNode; className?: string; 'aria-current'?: 'page'; }) { return ( ) => { if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); navigate(module, lesson); }} > {children} ); } function Navigation({ active, lesson, navigate, }: { active: string; lesson?: string; navigate: Navigate; }) { const { setOpenMobile } = useSidebar(); const go: Navigate = (m, l) => { navigate(m, l); setOpenMobile(false); }; return ( Agent 开发后端基础 · 补修课
课程目录 08 MODULES
{active === 'api' && lesson && ( )}
从 Python 到后端服务先运行,再理解,再修改。
); } export default function Course() { const [module, setModule] = useState('api'); const [lessonId, setLessonId] = useState(); const [presentation, setPresentation] = useState(false); const [notFound, setNotFound] = useState(false); const lesson = lessons.find((l) => l.id === lessonId); const current = modules.find((m) => m.slug === module)!; useEffect(() => { const read = () => { const q = new URLSearchParams(location.search); const id = q.get('lesson'); const mod = q.get('module') || 'api'; const validLesson = id ? lessons.some((l) => l.id === id) : true; const validModule = modules.some((m) => m.slug === mod); setNotFound(!validLesson || !validModule); setLessonId(validLesson && id ? id : undefined); setModule(validModule ? mod : 'api'); if (id && validLesson) setModule('api'); }; read(); window.addEventListener('popstate', read); return () => window.removeEventListener('popstate', read); }, []); useEffect(() => { document.title = `${lesson?.title || current.name} · Agent 开发补修课`; }, [lesson, current.name]); const navigate: Navigate = (mod, id) => { setModule(mod); setLessonId(id); setNotFound(false); history.pushState({}, '', routeLink(mod, id)); window.scrollTo({ top: 0, behavior: 'instant' }); requestAnimationFrame(() => document.querySelector('h1')?.focus({ preventScroll: true }), ); }; return ( 跳到课件正文
后端基础 / {current.name} {lesson && ( <> / {lesson.id} )}
FastAPI 文档
{notFound ? (

未找到该课件

该地址中的模块或课时不存在,请从课程目录重新选择。

返回 API 服务开发
) : module !== 'api' ? ( m.slug === module) + 1} navigate={navigate} /> ) : lesson ? ( ) : ( )}
Agent 开发 · 后端基础补修课 API 模块 / 课件版本 01
); } function Overview({ navigate }: { navigate: Navigate }) { const minutes = lessons.reduce((a, l) => a + l.minutes, 0); return ( <>
MODULE 01 服务与通信

API 服务开发.

把 Python 函数变成可以被调用的服务。
从第一个请求开始,完成 REST 接口、流式输出与双向通信。

Python 3.11+ FastAPI / Pydantic 2 {lessons.length} 节实战课 建议录制约 {Math.floor(minutes / 60)} 小时 {minutes % 60} 分
开始第一节 下载示例项目 课件可读 · 视频待录制
BUILD 01

贯穿项目:笔记与消息服务

创建与查询笔记,校验请求,逐段输出消息,建立实时连接。数据暂存在内存中,重启后清空。

学习路径

每节:概念 → 实战 → 验收
{phases.map((p, i) => (
{String(i + 1).padStart(2, '0')}

{p.name}

{p.description}

{p.range}
{lessons.slice(p.from, p.to).map((l) => ( {l.id}
{l.title}

{l.summary}

{l.demo && 交互} {l.minutes} 分
))}
))}

HTTP、SSE 与 WebSocket 通信过程

进入第 11 节

REST 描述接口设计约定;SSE 和 WebSocket 描述不同的通信方式。流式响应本身仍然可以基于 HTTP。

示例项目接口

api-lab / 本地运行

一个可以通过浏览器、命令行和测试代码调用的 FastAPI 服务。消息由固定逻辑生成,运行示例不需要模型 API Key。

入口 可观察的结果 对应课时 {[ ['GET /health', '返回服务状态', '01'], ['GET / POST /notes', '查询与创建笔记', '05—06'], [ 'GET / PUT / DELETE /notes/{id}', '读取、完整替换与删除', '06—07', ], ['GET /private', '校验 X-API-Key 请求头', '08'], ['GET /slow', '并发等待 I/O', '09'], ['GET /upstream-demo', '调用上游接口并处理超时', '10'], ['POST /generate', '一次返回完整 JSON', '11'], ['GET /stream', '连续收到 token,最后收到 done', '12—13'], ['WS /ws', '收发 echo 与 ping / pong 消息', '14—15'], ].map(([path, result, id]) => ( {path} {result} {id} ))}

配套代码与浏览器客户端

包含 README、版本依赖、接口测试与 web-client.html。

下载 ZIP

模块完成标准

用实际结果判断是否掌握
    {[ '能从空目录启动服务,并用 /docs 或 curl 发出请求。', '能完成笔记的创建、查询、修改与删除,区分 201、204、401、404、422。', '能解释 await 等待期间另一个请求为什么可以被处理。', '能逐段显示 SSE 数据,在完成或取消时释放连接。', '能建立 WebSocket 连接,收发消息并处理断开。', '能运行测试,独立定位参数、方法、跨域与连接问题。', ].map((s) => (
  • {s}
  • ))}
后续模块 数据库开发 待本模块完善后编写
); } function PendingModule({ name, index, navigate, }: { name: string; index: number; navigate: Navigate; }) { return (
MODULE {String(index).padStart(2, '0')} 课程目录

{name} .

待编写

该模块已保留课程入口。
API 服务开发模块完善后,再补充本模块的课件、实战与视频。

返回 API 服务开发
); } function CodeBlock({ code, lang = 'text', filename, }: { code: string; lang?: string; filename?: string; }) { const [copied, setCopied] = useState(false); const [error, setError] = useState(false); useEffect(() => { if (!copied && !error) return; const t = setTimeout(() => { setCopied(false); setError(false); }, 2200); return () => clearTimeout(t); }, [copied, error]); const copy = async () => { try { await navigator.clipboard.writeText(code); setCopied(true); } catch { setError(true); } }; const highlight = (line: string) => line .split( /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|#[^\n]*|\b(?:from|import|async|await|def|return|class|if|else|elif|for|in|while|try|except|finally|raise|with|as|yield|True|False|None|const|let|new|function|throw)\b)/g, ) .map((part, i) => ( {part} )); return (
{filename || lang}
        
          {code.split('\n').map((line, i) => (
            
              
              {highlight(line) || ' '}
              {'\n'}
            
          ))}
        
      
); } function VideoBlock({ id }: { id: string }) { const video = videos[id]; const valid = video && /^(https?:\/\/|\/(?!\/))/.test(video.url); return (
{valid ? ( video.type === 'file' ? (
无法播放时,直接打开视频
) : (
{video.title || '观看本节课程视频'} 在课程视频平台打开
) ) : (
本节视频待录制 可先按下方课件完成实战步骤。
课件已就绪
)}
); } function LessonPage({ lesson: l, navigate, }: { lesson: Lesson; navigate: Navigate; }) { const index = lessons.findIndex((x) => x.id === l.id); return (
API 服务开发 · 全部课时
LESSON {l.id} / 16 {l.group}

{l.title}

{l.summary}

{l.minutes} 分钟建议录制 实战课件 {l.demo ? '含交互演示' : '含代码与练习'}

本节目标

    {l.objectives.map((o, i) => (
  • {String(i + 1).padStart(2, '0')} {o}
  • ))}

必要概念

先明确代码在做什么
{l.concepts.map((c, i) => (

{i + 1}. {c.title}

{c.body}

))} {l.demo === 'http' ? ( ) : l.demo === 'async' ? ( ) : l.demo === 'protocol' ? ( ) : null}

动手实现

{l.steps.length} 个步骤
{l.steps.map((s, i) => (
{String(i + 1).padStart(2, '0')}

{s.title}

{s.body}

{s.code && ( )}
))}

验收结果

检查响应与运行行为
    {l.checks.map((c) => (
  • {c}
  • ))}

常见错误

现象 → 原因 → 处理
{l.pitfalls.map((p) => (

{p.symptom}

原因 {p.cause}

处理 {p.fix}

))}

扩展练习

课后练习

{l.exercise.task}

查看提示

{l.exercise.hint}

查看参考答案
{l.exercise.answer}

技术文档

对照原始定义与框架用法
{l.refs.map((r) => ( {r.label} ))}
{index > 0 ? ( 上一节 {lessons[index - 1].title} ) : ( 模块概览 查看完整学习路径 )} {index < lessons.length - 1 ? ( 下一节 {lessons[index + 1].title} ) : ( 本模块课件结束 返回模块完成标准 )}
); }