diff --git a/.claude/skills/article-formatter/SKILL.md b/.claude/skills/article-formatter/SKILL.md new file mode 100644 index 0000000..caae059 --- /dev/null +++ b/.claude/skills/article-formatter/SKILL.md @@ -0,0 +1,51 @@ +--- +name: article-formatter +description: 为指定的 Markdown 文章添加完整的 frontmatter 头信息,包括 title、slug、category、readingTime 等字段。当用户需要格式化文章、添加文章头信息时使用此 Skill。 +--- + +# Markdown 文章格式化器 + +为 Markdown 文章添加标准的 frontmatter 头信息。 + +## 功能 + +- **自动生成标题** - 从内容提取或使用文件名 +- **生成 slug** - 自动转换为 URL 友好格式 +- **识别分类** - 根据文件所在目录 +- **计算阅读时间** - 根据字数自动计算 +- **添加封面图** - 可选参数 + +## 使用方法 + +```bash +# 格式化文章(不含封面图) +python3 .claude/skills/article-formatter/format.py "docs/templates/essay/article.md" + +# 格式化文章(含封面图) +python3 .claude/skills/article-formatter/format.py "docs/templates/essay/article.md" "https://example.com/cover.jpg" +``` + +## 生成的 Frontmatter + +```yaml +--- +title: 文章标题 +slug: article-slug +published: true +featured: false +category: 分类 +publishedAt: 2026-01-18 +readingTime: 10 +coverImage: https://example.com/cover.jpg # 可选 +--- +``` + +## 分类映射 + +| 目录 | 分类 | +| ------ | ---------- | +| AI/ | ai | +| js/ | javascript | +| React/ | react | +| essay/ | essay | +| 其他 | 目录名小写 | diff --git a/.claude/skills/article-formatter/format.py b/.claude/skills/article-formatter/format.py new file mode 100755 index 0000000..e77fe81 --- /dev/null +++ b/.claude/skills/article-formatter/format.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Markdown 文章格式化器 - 为单篇文章添加 frontmatter +""" + +import sys +from pathlib import Path +import re +from datetime import datetime + +def generate_slug(text): + """生成 slug""" + # 移除中文字符,只保留英文、数字 + text = re.sub(r'[\u4e00-\u9fff]+', '-', text) + text = text.lower() + text = re.sub(r'[^\w\s-]', '', text) + text = re.sub(r'\s+', '-', text) + text = re.sub(r'-+', '-', text) + text = text.strip('-') + return text if text else 'article' + +def extract_title(content): + """提取标题""" + # 尝试从第一个 # 标题提取 + match = re.search(r'^#\s+(.+)$', content, re.MULTILINE) + if match: + return match.group(1).strip() + # 使用第一行 + first_line = content.split('\n')[0].strip() + return first_line[:50] if first_line else "未命名文章" + +def calculate_reading_time(content): + """计算阅读时间""" + # 移除 frontmatter + if content.startswith('---'): + end = content.find('---', 3) + if end != -1: + content = content[end+3:] + + chinese = len(re.findall(r'[\u4e00-\u9fff]', content)) + english = len(re.findall(r'[a-zA-Z]+', content)) + return max(1, round((chinese + english) / 400)) + +def get_category(file_path): + """从路径获取分类""" + parent = file_path.parent.name + mapping = {'AI': 'ai', 'js': 'javascript', 'React': 'react', 'essay': 'essay'} + return mapping.get(parent, parent.lower()) + +def add_frontmatter(file_path, cover_image=None): + """添加 frontmatter""" + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 检查是否已有 + if content.startswith('---'): + print(" ⚠️ 已有 frontmatter,跳过") + return False + + title = extract_title(content) + slug = generate_slug(title) + category = get_category(file_path) + reading_time = calculate_reading_time(content) + published_at = datetime.fromtimestamp(file_path.stat().st_mtime).strftime('%Y-%m-%d') + + # 构建 frontmatter + fm = f"""--- +title: {title} +slug: {slug} +published: true +featured: false +category: {category} +publishedAt: {published_at} +readingTime: {reading_time}""" + + if cover_image: + fm += f"\ncoverImage: {cover_image}" + + fm += "\n---\n\n" + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(fm + content) + + return True + +def main(): + if len(sys.argv) < 2: + print("用法: python3 format.py <文章路径> [封面图链接]") + sys.exit(1) + + file_path = Path(sys.argv[1]) + cover_image = sys.argv[2] if len(sys.argv) > 2 else None + + if not file_path.exists(): + print(f"❌ 文件不存在: {file_path}") + sys.exit(1) + + print(f"📝 {file_path}") + + if add_frontmatter(file_path, cover_image): + print(" ✅ 格式化完成") + +if __name__ == "__main__": + main() diff --git a/.claude/skills/cover-image-fixer/SKILL.md b/.claude/skills/cover-image-fixer/SKILL.md new file mode 100644 index 0000000..3ced6ab --- /dev/null +++ b/.claude/skills/cover-image-fixer/SKILL.md @@ -0,0 +1,68 @@ +--- +name: cover-image-fixer +description: 自动检索 docs/templates 目录中没有 coverImage 的 Markdown 文章,并添加封面图链接。当用户需要批量添加封面图、检查缺失封面图时自动使用此 Skill。 +--- + +# 封面图修复器 + +自动检索并添加文章封面图。 + +## 功能 + +- **扫描检测** - 扫描 docs/templates 目录下所有 Markdown 文件 +- **识别缺失** - 找出 frontmatter 中没有 coverImage 的文章 +- **批量添加** - 为缺失封面图的文章添加链接 +- **备份保护** - 修改前自动备份原文件 + +## 使用方法 + +```bash +python3 .claude/skills/cover-image-fixer/fix-covers.py "https://your-cover-image-url.com" +``` + +## 工作流程 + +``` +1. 扫描 docs/templates/**/*.md +2. 解析每个文件的 frontmatter +3. 检查是否包含 coverImage 字段 +4. 为缺失的文章添加封面图链接 +5. 保存并报告修改结果 +``` + +## Frontmatter 格式 + +```yaml +--- +title: 文章标题 +slug: article-slug +date: 2025-01-15 +category: 分类 +coverImage: https://example.com/cover.jpg ← 需要添加这个 +--- +``` + +## 依赖 + +```bash +pip3 install pyyaml +``` + +## 示例 + +```bash +# 添加封面图 +python3 .claude/skills/cover-image-fixer/fix-covers.py "https://s41.ax1x.com/2026/01/17/pZyCDTH.jpg" + +# 输出示例: +# 📋 扫描 docs/templates 目录... +# 📊 发现 3 篇文章缺少封面图 +# +# [1/3] docs/templates/essay/article1.md +# ✅ 已添加封面图 +# +# [2/3] docs/templates/js/article2.md +# ✅ 已添加封面图 +# +# ✅ 完成! 成功为 3 篇文章添加封面图 +``` diff --git a/.claude/skills/cover-image-fixer/fix-covers.py b/.claude/skills/cover-image-fixer/fix-covers.py new file mode 100755 index 0000000..536e614 --- /dev/null +++ b/.claude/skills/cover-image-fixer/fix-covers.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +封面图修复器 - 自动为缺少 coverImage 的文章添加封面图 +""" + +import sys +from pathlib import Path +import re + +def find_articles_without_cover(): + """查找所有没有 coverImage 的文章""" + templates_dir = Path("docs/templates") + articles = [] + + for md_file in templates_dir.rglob("*.md"): + with open(md_file, 'r', encoding='utf-8') as f: + content = f.read() + + # 检查是否有 frontmatter + if not content.startswith('---'): + continue + + # 提取 frontmatter + frontmatter_end = content.find('---', 3) + if frontmatter_end == -1: + continue + + frontmatter = content[3:frontmatter_end] + + # 检查是否已有 coverImage + if 'coverImage:' not in frontmatter: + articles.append(md_file) + + return articles + + +def add_cover_image(file_path, cover_url): + """为文章添加封面图""" + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 找到 frontmatter 的结束位置 + frontmatter_end = content.find('---', 3) + if frontmatter_end == -1: + return False, "无法找到 frontmatter 结束标记" + + # 在 frontmatter 结束前插入 coverImage + before = content[:frontmatter_end] + after = content[frontmatter_end:] + + # 检查 frontmatter 中是否有其他字段,决定换行 + if before.strip() and not before.rstrip().endswith('\n'): + new_content = before + f"\ncoverImage: {cover_url}\n" + after + else: + new_content = before + f"coverImage: {cover_url}\n" + after + + # 写入文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + + return True, "成功添加封面图" + + +def main(): + """主函数""" + if len(sys.argv) < 2: + print("❌ 请提供封面图链接") + print("用法: python3 fix-covers.py \"https://your-cover-image-url.com\"") + sys.exit(1) + + cover_url = sys.argv[1].strip() + + print("📋 扫描 docs/templates 目录...") + + articles = find_articles_without_cover() + + if not articles: + print("✅ 所有文章都已有封面图!") + return + + print(f"📊 发现 {len(articles)} 篇文章缺少封面图") + print() + + success_count = 0 + failed_count = 0 + + for i, article in enumerate(articles, 1): + rel_path = article.relative_to(Path.cwd()) + print(f"[{i}/{len(articles)}] {rel_path}") + + success, msg = add_cover_image(article, cover_url) + + if success: + print(f" ✅ {msg}") + success_count += 1 + else: + print(f" ❌ {msg}") + failed_count += 1 + print() + + print("=" * 50) + print(f"✅ 完成! 成功: {success_count}, 失败: {failed_count}") + print("=" * 50) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/wallpaper-fetcher/SKILL.md b/.claude/skills/wallpaper-fetcher/SKILL.md new file mode 100644 index 0000000..a5eddfb --- /dev/null +++ b/.claude/skills/wallpaper-fetcher/SKILL.md @@ -0,0 +1,69 @@ +--- +name: wallpaper-fetcher +description: 从好壁纸网批量获取随机风景壁纸并压缩优化。每次运行自动下载10张壁纸到桌面 skills-img 文件夹,按日期时间分批保存。当用户请求获取壁纸、风景壁纸、批量下载时自动使用此 Skill。 +--- + +# 壁纸批量获取器 + +批量获取风景壁纸并压缩优化,自动保存到桌面。 + +## 功能 + +- **随机获取** - 从好壁纸网随机页码(1-2500页)获取壁纸 +- **智能筛选** - 优先选择风景类壁纸(山、水、云、日落等) +- **批量下载** - 每次自动下载 10 张 +- **自动压缩** - 使用 PIL 优化图片质量和尺寸 +- **分批保存** - 按日期时间创建子文件夹,方便管理 + +## 使用方法 + +```bash +python3 .claude/skills/wallpaper-fetcher/batch-fetch.py +``` + +## 输出结构 + +``` +~/Desktop/skills-img/ +├── 20260118_000216/ # 第一批(10张) +│ ├── wallpaper_01.jpg +│ ├── wallpaper_02.jpg +│ └── ... +├── 20260118_000344/ # 第二批(10张) +│ ├── wallpaper_01.jpg +│ └── ... +└── ... +``` + +## 压缩参数 + +- **质量**: 75% +- **最大尺寸**: 1920px(保持宽高比) +- **格式**: JPEG(优化压缩) + +## 依赖 + +```bash +pip3 install requests beautifulsoup4 Pillow +``` + +## 示例输出 + +``` +============================================================ +🖼️ 批量壁纸获取器 - 10张风景壁纸 +============================================================ + +📁 保存目录: /Users/mi/Desktop/skills-img/20260118_000344 +📅 批次时间: 2026-01-18 00:03:44 + +🎯 开始获取 10 张壁纸... + +[1/10] ✅ wallpaper_01.jpg (77.4 KB) +[2/10] ✅ wallpaper_02.jpg (44.5 KB) +... +[10/10] ✅ wallpaper_10.jpg (51.1 KB) + +✅ 批量获取完成! +============================================================ +``` diff --git a/.claude/skills/wallpaper-fetcher/batch-fetch.py b/.claude/skills/wallpaper-fetcher/batch-fetch.py new file mode 100755 index 0000000..5208554 --- /dev/null +++ b/.claude/skills/wallpaper-fetcher/batch-fetch.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" +批量壁纸获取器 - 每次获取10张风景壁纸 +保存到桌面 skills-img 文件夹 +""" + +import os +import sys +from pathlib import Path +import time +import random + +# Configuration - 保存到桌面 +BASE_DIR = Path.home() / "Desktop" / "skills-img" +BATCH_SIZE = 20 # 获取20张 + + +def fetch_scenic_wallpaper(): + """从好壁纸网获取随机风景壁纸""" + page_num = random.randint(1, 2500) + url = f"https://haowallpaper.com/homeView?page={page_num}" + + try: + import requests + from bs4 import BeautifulSoup + + time.sleep(random.uniform(0.3, 1.0)) + + response = requests.get(url, timeout=10, headers={ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }) + response.raise_for_status() + + soup = BeautifulSoup(response.text, 'html.parser') + wallpapers = [] + + for img in soup.find_all('img'): + src = img.get('src') or img.get('data-src') or img.get('data-original') + if src and 'getCroppingImg' in src: + if src.startswith('//'): + src = 'https:' + src + elif src.startswith('/'): + src = 'https://haowallpaper.com' + src + + alt_text = img.get('alt', '') + title_text = img.get('title', '') + description = f"{alt_text} {title_text}".strip() + + wallpapers.append({'url': src, 'description': description}) + + if not wallpapers: + return None + + # 去重 + seen_urls = set() + unique_wallpapers = [] + for wp in wallpapers: + if wp['url'] not in seen_urls: + seen_urls.add(wp['url']) + unique_wallpapers.append(wp) + + # 风景关键词 + scenic_keywords = [ + '风景', '山', '水', '海', '湖', '河', '自然', '天空', '云', '雾', + 'mountain', 'water', 'sea', 'lake', 'river', 'nature', 'sky', 'cloud', + '日出', '日落', '森林', '树', '花', '草地', 'sunset', 'sunrise', + 'forest', 'tree', 'flower', 'grass', 'landscape', 'scenery' + ] + + # 过滤风景类壁纸 + scenic_wallpapers = [] + for wp in unique_wallpapers: + desc_lower = wp['description'].lower() + if any(kw.lower() in desc_lower for kw in scenic_keywords): + scenic_wallpapers.append(wp) + + return random.choice(scenic_wallpapers) if scenic_wallpapers else random.choice(unique_wallpapers) + + except Exception: + return None + + +def download_image(url, output_path): + """下载图片""" + try: + import requests + response = requests.get(url, timeout=30, headers={ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }) + response.raise_for_status() + with open(output_path, 'wb') as f: + f.write(response.content) + return True + except Exception: + return False + + +def compress_image(input_path, output_path, quality=75, max_dimension=1920): + """使用 PIL 压缩图片""" + try: + from PIL import Image + img = Image.open(input_path) + + # 缩放过大的图片 + if max(img.size) > max_dimension: + ratio = max_dimension / max(img.size) + new_size = tuple(int(dim * ratio) for dim in img.size) + img = img.resize(new_size, Image.Resampling.LANCZOS) + + # 转换为 RGB + if img.mode == 'RGBA': + background = Image.new('RGB', img.size, (255, 255, 255)) + background.paste(img, mask=img.split()[3]) + img = background + elif img.mode != 'RGB': + img = img.convert('RGB') + + img.save(output_path, 'JPEG', quality=quality, optimize=True, progressive=True) + return True + except Exception: + return False + + +def main(): + """批量获取10张壁纸""" + from datetime import datetime + + now = datetime.now() + datetime_str = now.strftime("%Y%m%d_%H%M%S") + OUTPUT_DIR = BASE_DIR / datetime_str + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + + print(f"🖼️ 批量获取器 - {BATCH_SIZE}张风景壁纸") + print(f"📁 {OUTPUT_DIR}") + print() + + downloaded = [] + + for i in range(BATCH_SIZE): + print(f"[{i+1}/{BATCH_SIZE}]", end=" ") + + wallpaper = fetch_scenic_wallpaper() + if not wallpaper: + print("❌") + continue + + filename = f"wallpaper_{i+1:02d}.jpg" + temp_path = OUTPUT_DIR / f"temp_{filename}" + final_path = OUTPUT_DIR / filename + + if download_image(wallpaper['url'], temp_path) and compress_image(temp_path, final_path): + temp_path.unlink(missing_ok=True) + size_kb = final_path.stat().st_size / 1024 + print(f"✅ {filename} ({size_kb:.1f} KB)") + downloaded.append(final_path) + else: + print("❌") + + time.sleep(random.uniform(0.2, 0.5)) + + print() + print(f"✅ 完成! 成功下载 {len(downloaded)} 张壁纸到 {OUTPUT_DIR}") + + +if __name__ == "__main__": + main() diff --git a/.env.backup b/.env.backup new file mode 100644 index 0000000..a932a17 --- /dev/null +++ b/.env.backup @@ -0,0 +1 @@ +DATABASE_URL="file:./prisma/dev.db" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1126724..96f0e3c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -71,13 +71,20 @@ jobs: DATABASE_URL: ${{ secrets.DATABASE_URL }} NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} NEXTAUTH_URL: ${{ secrets.NEXTAUTH_URL }} + KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }} + KIMI_BASE_URL: ${{ secrets.KIMI_BASE_URL }} + KIMI_MODEL: ${{ secrets.KIMI_MODEL }} + OLLAMA_BASE_URL: ${{ secrets.OLLAMA_BASE_URL }} + OLLAMA_EMBEDDING_MODEL: ${{ secrets.OLLAMA_EMBEDDING_MODEL }} + CHROMADB_HOST: ${{ secrets.CHROMADB_HOST }} + CHROMADB_PORT: ${{ secrets.CHROMADB_PORT }} with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SERVER_SSH_KEY }} port: ${{ secrets.SERVER_PORT || 22 }} command_timeout: 20m - envs: DATABASE_URL,NEXTAUTH_SECRET,NEXTAUTH_URL + envs: DATABASE_URL,NEXTAUTH_SECRET,NEXTAUTH_URL,KIMI_API_KEY,KIMI_BASE_URL,KIMI_MODEL,OLLAMA_BASE_URL,OLLAMA_EMBEDDING_MODEL,CHROMADB_HOST,CHROMADB_PORT script: | cd /www/wwwroot/my-next-app @@ -88,6 +95,21 @@ jobs: NEXTAUTH_URL="${NEXTAUTH_URL}" NODE_ENV="production" EOF + + # 添加 AI 配置(如果提供了 AI 相关的 Secrets) + if [ -n "${KIMI_API_KEY}" ]; then + echo "KIMI_API_KEY=\"${KIMI_API_KEY}\"" >> .env.production + echo "KIMI_BASE_URL=\"${KIMI_BASE_URL:-https://api.moonshot.cn/v1}\"" >> .env.production + echo "KIMI_MODEL=\"${KIMI_MODEL:-moonshot-v1-32k}\"" >> .env.production + echo "OLLAMA_BASE_URL=\"${OLLAMA_BASE_URL:-http://localhost:11434}\"" >> .env.production + echo "OLLAMA_EMBEDDING_MODEL=\"${OLLAMA_EMBEDDING_MODEL:-nomic-embed-text}\"" >> .env.production + echo "CHROMADB_HOST=\"${CHROMADB_HOST:-localhost}\"" >> .env.production + echo "CHROMADB_PORT=\"${CHROMADB_PORT:-8000}\"" >> .env.production + echo "✅ AI 配置已添加" + else + echo "⚠️ 未配置 AI 相关环境变量(AI 功能将不可用)" + fi + chmod 600 .env.production echo "环境变量文件已更新" @@ -101,6 +123,19 @@ jobs: echo "=== 清理旧的 Next.js 构建产物 ===" rm -rf .next + echo "=== 检查并设置 Swap 空间 ===" + if [ -f /swapfile ]; then + echo "✅ Swap 文件已存在" + swapon --show + else + echo "创建 4GB Swap 文件以避免 OOM..." + sudo fallocate -l 4G /swapfile 2>/dev/null || sudo dd if=/dev/zero of=/swapfile bs=1M count=4096 + sudo chmod 600 /swapfile + sudo mkswap /swapfile + sudo swapon /swapfile + echo "✅ Swap 已启用" + free -h + fi echo "=== 构建 Next.js 项目 ===" echo "开始时间: $(date '+%Y-%m-%d %H:%M:%S')" @@ -137,6 +172,79 @@ jobs: echo "Build ID: $(cat .next/BUILD_ID)" ls -lh .next/ | head -20 + # 启动 AI 服务(如果配置了 AI 相关环境变量) + - name: 启动 AI 服务 + uses: appleboy/ssh-action@master + env: + KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }} + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + port: ${{ secrets.SERVER_PORT || 22 }} + command_timeout: 10m + script: | + cd /www/wwwroot/my-next-app + + # 如果没有配置 Kimi API Key,跳过 AI 服务启动 + if [ -z "${KIMI_API_KEY}" ]; then + echo "⚠️ 未配置 AI 服务,跳过 Ollama 和 ChromaDB 启动" + exit 0 + fi + + echo "=== 启动 AI 服务 ===" + + # 1. 启动 Ollama + echo "🤖 检查 Ollama 服务..." + if pgrep -f "ollama serve" > /dev/null; then + echo "✅ Ollama 已在运行" + else + echo "启动 Ollama..." + nohup ollama serve > /tmp/ollama.log 2>&1 & + sleep 5 + + if curl -s http://localhost:11434/api/tags > /dev/null 2>&1; then + echo "✅ Ollama 启动成功" + else + echo "❌ Ollama 启动失败,查看日志: tail -f /tmp/ollama.log" + exit 1 + fi + fi + + # 2. 检查并下载 Embedding 模型 + echo "🔍 检查 Embedding 模型..." + if ollama list 2>/dev/null | grep -q "nomic-embed-text"; then + echo "✅ 模型已安装" + else + echo "下载 nomic-embed-text 模型..." + ollama pull nomic-embed-text + echo "✅ 模型下载完成" + fi + + # 3. 启动 ChromaDB + echo "📦 检查 ChromaDB 服务..." + if pgrep -f "chromadb" > /dev/null || pgrep -f "chroma run" > /dev/null; then + echo "✅ ChromaDB 已在运行" + else + # 创建数据目录 + mkdir -p /www/wwwroot/my-next-app/data/chroma + + echo "启动 ChromaDB..." + nohup npx chromadb run --path /www/wwwroot/my-next-app/data/chroma --port 8000 > /tmp/chromadb.log 2>&1 & + sleep 5 + + if curl -s http://localhost:8000/api/v1/heartbeat > /dev/null 2>&1; then + echo "✅ ChromaDB 启动成功" + else + echo "❌ ChromaDB 启动失败,查看日志: tail -f /tmp/chromadb.log" + exit 1 + fi + fi + + echo "✅ AI 服务启动完成" + echo " Ollama: http://localhost:11434" + echo " ChromaDB: http://localhost:8000" + # 重启 PM2 应用 - name: 重启应用 uses: appleboy/ssh-action@master @@ -169,11 +277,11 @@ jobs: echo "✅ .env.production 文件存在" echo "=== 停止旧应用进程 ===" - pm2 stop spring-lament-blog || true + pm2 stop spring-broken-ai-blog || true sleep 2 echo "=== 删除旧进程 ===" - pm2 delete spring-lament-blog || true + pm2 delete spring-broken-ai-blog || true echo "=== 启动应用 ===" pm2 start ecosystem.config.js --env production --update-env @@ -184,7 +292,7 @@ jobs: echo "=== 验证应用状态 ===" pm2 list - if pm2 list | grep -q "spring-lament-blog.*online"; then + if pm2 list | grep -q "spring-broken-ai-blog.*online"; then echo "✅ 应用启动成功" echo "=== 检查端口 3000 是否监听 ===" @@ -196,9 +304,9 @@ jobs: else echo "❌ 应用启动失败" echo "=== PM2 进程详情 ===" - pm2 describe spring-lament-blog || true + pm2 describe spring-broken-ai-blog || true echo "=== 最近日志 ===" - pm2 logs spring-lament-blog --lines 50 --nostream || true + pm2 logs spring-broken-ai-blog --lines 50 --nostream || true exit 1 fi diff --git a/CLAUDE.md b/CLAUDE.md index 26b5b36..aa0bfcb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,14 +1,16 @@ # Spring Broken AI Blog 博客系统 - Claude 开发文档 -这是一个基于 Next.js 15 + TypeScript + shadcn/ui + NextAuth.js 构建的现代化个人博客系统。 +这是一个基于 Next.js 15 + TypeScript + shadcn/ui + NextAuth.js 构建的现代化个人博客系统,集成了 AI 智能助手和 RAG (检索增强生成) 功能。 ## 🚀 项目概览 -Spring Broken AI Blog 是一个全栈博客系统,专注于**高效创作**和**优雅展示**。系统采用现代化技术栈,提供完整的内容管理功能和用户友好的管理界面。 +Spring Broken AI Blog 是一个全栈博客系统,专注于**高效创作**和**优雅展示**。系统采用现代化技术栈,提供完整的内容管理功能和用户友好的管理界面,并通过 AI 技术增强写作体验。 ### 核心特性 -- ✅ **现代化前端**: Next.js 15 + App Router +#### 基础功能 + +- ✅ **现代化前端**: Next.js 15 + App Router + Turbopack - ✅ **类型安全**: 全栈 TypeScript 支持 - ✅ **无头组件**: shadcn/ui + Radix UI + Tailwind CSS - ✅ **身份认证**: NextAuth.js v4 + JWT 策略 @@ -16,39 +18,81 @@ Spring Broken AI Blog 是一个全栈博客系统,专注于**高效创作**和 - ✅ **代码质量**: ESLint + Prettier + Husky - ✅ **响应式设计**: 移动端友好的界面 +#### AI 功能亮点 + +- 🤖 **智能写作助手**: 基于 Kimi API 的 AI 辅助创作 +- 🧠 **向量索引系统**: ChromaDB + Ollama 实现本地向量存储 +- 🔍 **RAG 聊天功能**: 基于文章内容的智能问答 +- ✨ **AI 补全功能**: 编辑器内的智能内容续写 +- 📝 **智能推荐**: AI 自动推荐分类和标签 +- 💬 **流式输出**: 实时展示 AI 生成内容 + ## 📁 项目结构 ``` -Spring Broken AI Blog Blog/ +Spring-Broken-AI-Blog/ ├── src/ -│ ├── app/ # Next.js App Router 页面 -│ │ ├── admin/ # 管理后台页面 -│ │ ├── login/ # 登录页面 -│ │ ├── api/auth/ # NextAuth.js API 路由 -│ │ ├── globals.css # 全局样式 -│ │ └── layout.tsx # 根布局 -│ ├── components/ # React 组件 -│ │ ├── ui/ # shadcn/ui 基础组件 -│ │ ├── admin/ # 管理后台组件 -│ │ └── providers/ # 上下文提供器 -│ ├── lib/ # 工具库和配置 -│ │ ├── auth.ts # NextAuth.js 配置 -│ │ └── utils.ts # 工具函数 -│ └── types/ # TypeScript 类型定义 -├── prisma/ # Prisma 数据库配置 -│ ├── schema.prisma # 数据库模型 -│ └── seed.ts # 数据库种子 -├── docs/ # 项目文档 -├── components.json # shadcn/ui 配置 -├── tailwind.config.ts # Tailwind CSS 配置 -└── middleware.ts # Next.js 中间件 (路由保护) +│ ├── app/ # Next.js App Router 页面 +│ │ ├── admin/ # 管理后台页面 +│ │ │ ├── page.tsx # 后台首页 +│ │ │ ├── posts/ # 文章管理 +│ │ │ ├── categories/ # 分类管理 +│ │ │ ├── tags/ # 标签管理 +│ │ │ ├── profile/ # 个人资料 +│ │ │ └── settings/ # 系统设置 +│ │ ├── login/ # 登录页面 +│ │ ├── api/ # API 路由 +│ │ │ ├── auth/ # NextAuth.js API +│ │ │ ├── admin/ # 管理后台 API +│ │ │ └── ai/ # AI 功能 API +│ │ ├── posts/[slug]/ # 文章详情页 +│ │ ├── category/[slug]/ # 分类页面 +│ │ ├── globals.css # 全局样式 +│ │ └── layout.tsx # 根布局 +│ ├── components/ # React 组件 +│ │ ├── ui/ # shadcn/ui 基础组件 +│ │ ├── admin/ # 管理后台组件 +│ │ │ ├── ai-assistant.tsx # AI 写作助手 +│ │ │ ├── rag-chat.tsx # RAG 聊天组件 +│ │ │ ├── post-editor.tsx # 文章编辑器 +│ │ │ └── publish-dialog.tsx # 发布对话框 +│ │ ├── markdown/ # Markdown 渲染组件 +│ │ ├── posts/ # 文章展示组件 +│ │ ├── providers/ # 上下文提供器 +│ │ └── layout/ # 布局组件 +│ ├── lib/ # 工具库和配置 +│ │ ├── auth.ts # NextAuth.js 配置 +│ │ ├── prisma.ts # Prisma 客户端 +│ │ ├── utils.ts # 工具函数 +│ │ ├── ai/ # AI 相关 +│ │ │ ├── client.ts # AI 客户端 (Kimi + Ollama) +│ │ │ ├── prompts/ # AI 提示词 +│ │ │ └── rag.ts # RAG 实现 +│ │ ├── vector/ # 向量索引 +│ │ │ ├── chunker.ts # 文本分块 +│ │ │ ├── indexer.ts # 索引管理 +│ │ │ └── store.ts # 向量存储 (ChromaDB) +│ │ └── editor/ # 编辑器相关 +│ │ ├── ai-completion-extension.ts +│ │ └── markdown-converter.ts +│ └── types/ # TypeScript 类型定义 +├── prisma/ # Prisma 数据库配置 +│ ├── schema.prisma # 数据库模型 +│ ├── seed.ts # 数据库种子 +│ └── dev.db # SQLite 数据库 (开发环境) +├── docs/ # 项目文档 +├── public/ # 静态资源 +├── components.json # shadcn/ui 配置 +├── tailwind.config.ts # Tailwind CSS 配置 +├── middleware.ts # Next.js 中间件 (路由保护) +└── ecosystem.config.js # PM2 配置文件 ``` ## 🛠️ 技术栈 ### 核心框架 -- **Next.js 15**: React 全栈框架,使用 App Router +- **Next.js 15**: React 全栈框架,使用 App Router + Turbopack - **TypeScript**: 静态类型检查 - **React 18**: 用户界面库 @@ -58,12 +102,23 @@ Spring Broken AI Blog Blog/ - **Radix UI**: 无头 UI 原语 - **Tailwind CSS**: 实用优先的 CSS 框架 - **Lucide React**: 现代化图标库 +- **Novel**: Notion 风格的编辑器 +- **react-markdown**: Markdown 渲染 +- **remark/rehype**: Markdown 处理插件 +- **highlight.js**: 代码高亮 + +### AI 能力 + +- **Kimi API (Moonshot AI)**: AI 对话和生成 +- **Ollama**: 本地 Embedding 生成 (nomic-embed-text 模型) +- **ChromaDB**: 向量数据库,用于 RAG 检索 +- **OpenAI SDK**: 兼容 Kimi API 的调用方式 ### 数据层 -- **Prisma**: 现代化 ORM -- **SQLite**: 开发环境数据库 -- **PostgreSQL**: 生产环境数据库 (计划) +- **Prisma 6.16.1**: 现代化 ORM +- **SQLite**: 开发/生产环境数据库 +- **Prisma Adapter**: NextAuth.js 数据库适配器 ### 身份认证 @@ -78,6 +133,12 @@ Spring Broken AI Blog Blog/ - **Husky**: Git hooks 管理 - **lint-staged**: 暂存区文件检查 +### 部署工具 + +- **PM2**: Node.js 进程管理器 +- **Nginx**: Web 服务器和反向代理 +- **GitHub Actions**: CI/CD 自动化部署 (可选) + ## 🔧 快速开始 ### 环境要求 @@ -87,19 +148,77 @@ Node.js >= 18.0.0 npm >= 8.0.0 ``` -### 安装依赖 +### AI 功能依赖 (可选) + +如需使用 AI 功能,需要安装以下服务: + +#### 1. Ollama (用于向量生成) + +```bash +# macOS +brew install ollama + +# Linux +curl -fsSL https://ollama.com/install.sh | sh + +# 启动 Ollama 服务 +ollama serve + +# 拉取 Embedding 模型 +ollama pull nomic-embed-text +``` + +#### 2. ChromaDB (用于向量存储) + +```bash +# 使用 Docker +docker run -d --name chromadb -p 8000:8000 chromadb/chroma:latest + +# 或直接使用 Python +pip install chromadb +chroma run --host localhost --port 8000 +``` + +### 安装步骤 ```bash -# 克隆项目 +# 1. 克隆项目 git clone -cd "Spring Broken AI Blog" +cd Spring-Broken-AI-Blog -# 安装依赖 +# 2. 安装依赖 npm install -# 设置环境变量 -cp .env.example .env -# 编辑 .env 文件配置数据库连接和认证密钥 +# 3. 配置环境变量 +cp .env.example .env.local +``` + +编辑 `.env.local` 文件: + +```bash +# 数据库配置 +DATABASE_URL="file:./prisma/dev.db" + +# NextAuth 配置 +NEXTAUTH_SECRET="your-secret-key-at-least-32-characters-long" +NEXTAUTH_URL="http://localhost:7777" + +# 管理员账户 (seed 时使用) +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="0919" + +# AI 配置 (可选) +KIMI_API_KEY="your-kimi-api-key" +KIMI_BASE_URL="https://api.moonshot.cn/v1" +KIMI_MODEL="moonshot-v1-32k" + +# Ollama 配置 (用于向量生成) +OLLAMA_BASE_URL="http://localhost:11434" +OLLAMA_EMBEDDING_MODEL="nomic-embed-text" + +# ChromaDB 配置 (用于向量存储) +CHROMADB_HOST="localhost" +CHROMADB_PORT="8000" ``` ### 数据库设置 @@ -118,13 +237,13 @@ npm run db:seed ### 启动开发服务器 ```bash -# 启动开发服务器 +# 启动开发服务器 (端口 7777) npm run dev # 访问应用 -# 前台: http://localhost:3000 -# 登录: http://localhost:3000/login -# 后台: http://localhost:3000/admin +# 前台: http://localhost:7777 +# 登录: http://localhost:7777/login +# 后台: http://localhost:7777/admin ``` ### 默认管理员账户 @@ -161,6 +280,80 @@ npm run build - **pre-commit**: 自动格式化代码,运行 ESLint - **commit-msg**: 检查提交信息格式 +### 常用命令 + +```bash +# 开发 +npm run dev # 启动开发服务器 (端口 7777) +npm run build # 构建生产版本 +npm run start # 启动生产服务器 (端口 3000) + +# 数据库 +npm run db:generate # 生成 Prisma 客户端 +npm run db:push # 推送 schema 到数据库 +npm run db:migrate # 创建迁移 +npm run db:seed # 填充种子数据 +npm run db:studio # 打开 Prisma Studio +npm run db:reset # 重置数据库 + +# PM2 管理 +npm run pm2:start # 启动 PM2 进程 +npm run pm2:restart # 重启 PM2 进程 +npm run pm2:stop # 停止 PM2 进程 +npm run pm2:delete # 删除 PM2 进程 +``` + +### AI 功能开发 + +#### 1. 使用 AI 客户端 + +```typescript +import { getAIClient } from "@/lib/ai/client"; + +// 获取客户端实例 +const aiClient = getAIClient(); + +// 非流式对话 +const response = await aiClient.chat([{ role: "user", content: "你好" }]); +console.log(response.content); + +// 流式对话 +await aiClient.chatStream( + [{ role: "user", content: "写一篇文章" }], + {}, + (chunk) => { + console.log(chunk); // 实时输出 + } +); +``` + +#### 2. 文章向量索引 + +```typescript +import { indexPost, indexAllPosts } from "@/lib/vector/indexer"; + +// 索引单篇文章 +await indexPost("post-id"); + +// 强制重新索引 +await indexPost("post-id", { force: true }); + +// 批量索引所有文章 +const result = await indexAllPosts({ force: true }); +console.log( + `成功: ${result.indexed}, 跳过: ${result.skipped}, 失败: ${result.failed}` +); +``` + +#### 3. RAG 聊天 + +```typescript +import { ragChat } from "@/lib/ai/rag"; + +// 使用 RAG 进行问答 +const answer = await ragChat("如何使用 Next.js?"); +``` + ### 组件开发 #### 创建 UI 组件 @@ -218,11 +411,18 @@ app/ │ ├── page.tsx # 管理首页 /admin │ ├── posts/ │ │ ├── page.tsx # 文章列表 /admin/posts -│ │ └── new/page.tsx # 新建文章 /admin/posts/new +│ │ ├── new/page.tsx # 新建文章 /admin/posts/new +│ │ └── [id]/edit/ # 编辑文章 /admin/posts/123/edit │ ├── categories/page.tsx # 分类管理 /admin/categories +│ ├── tags/page.tsx # 标签管理 /admin/tags +│ ├── profile/page.tsx # 个人资料 /admin/profile │ └── layout.tsx # 管理后台布局 +├── posts/[slug]/page.tsx # 文章详情 /posts/hello-world +├── category/[slug]/page.tsx # 分类页面 /category/frontend └── api/ - └── auth/[...nextauth]/route.ts # 认证 API + ├── auth/[...nextauth]/ # 认证 API + ├── admin/ # 管理后台 API + └── ai/ # AI 功能 API ``` #### 页面模板 @@ -230,10 +430,10 @@ app/ ```tsx // app/admin/example/page.tsx import { Metadata } from "next"; -import AdminLayout from "@/components/admin/admin-layout"; +import AdminLayout from "@/components/admin/clean-admin-layout"; export const metadata: Metadata = { - title: "示例页面 - Spring Broken AI Blog Blog", + title: "示例页面 - Spring Broken AI Blog", description: "这是一个示例页面", }; @@ -254,22 +454,22 @@ export default function ExamplePage() { ### 数据库操作 -#### 模型定义 +#### 数据模型 + +项目包含以下核心数据模型: ```prisma -// prisma/schema.prisma -model Post { - id String @id @default(cuid()) - title String - content String? - published Boolean @default(false) - authorId String - author User @relation(fields: [authorId], references: [id]) - categories Category[] - tags Tag[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} +// 用户系统 +User - 用户账户 +Profile - 用户资料 (一对一) +Role - 用户角色 (USER/ADMIN) + +// 内容管理 +Post - 文章 +PostVectorIndex - 文章向量索引 (AI 功能) +Category - 分类 +Tag - 标签 +PostTag - 文章标签关联 (多对多) ``` #### 数据库查询 @@ -277,17 +477,15 @@ model Post { ```typescript import { prisma } from "@/lib/prisma"; -// 获取文章列表 +// 获取文章列表 (包含关联数据) export async function getPosts() { return await prisma.post.findMany({ include: { - author: true, - categories: true, - tags: true, - }, - orderBy: { - createdAt: "desc", + author: { select: { username: true, avatar: true } }, + category: true, + tags: { include: { tag: true } }, }, + orderBy: { createdAt: "desc" }, }); } @@ -296,9 +494,7 @@ export async function createPost(data: CreatePostData) { return await prisma.post.create({ data: { ...data, - author: { - connect: { id: data.authorId }, - }, + author: { connect: { id: data.authorId } }, }, }); } @@ -333,7 +529,7 @@ export default withAuth( ); export const config = { - matcher: ["/admin/:path*"], + matcher: ["/admin/:path*", "/api/admin/:path*"], }; ``` @@ -428,48 +624,215 @@ npm run dev ## 🚀 部署 -### 构建生产版本 +### 生产环境配置 + +#### 1. 环境变量配置 + +创建 `.env.production` 文件: ```bash -# 构建应用 +# 数据库配置 (生产环境) +DATABASE_URL="file:./prisma/prod.db" + +# NextAuth 配置 +NEXTAUTH_SECRET="your-production-secret-key-at-least-32-characters" +NEXTAUTH_URL="http://your-domain.com" + +# AI 配置 (可选) +KIMI_API_KEY="your-kimi-api-key" +KIMI_BASE_URL="https://api.moonshot.cn/v1" +KIMI_MODEL="moonshot-v1-32k" + +# Ollama 配置 +OLLAMA_BASE_URL="http://localhost:11434" +OLLAMA_EMBEDDING_MODEL="nomic-embed-text" + +# ChromaDB 配置 +CHROMADB_HOST="localhost" +CHROMADB_PORT="8000" +``` + +#### 2. 数据库初始化 + +```bash +# 生成 Prisma 客户端 (生产环境) +npm run db:generate:prod + +# 推送 schema (生产环境) +npm run db:push:prod + +# 填充种子数据 (生产环境) +npm run db:seed:prod +``` + +#### 3. 构建和启动 + +```bash +# 构建项目 npm run build -# 启动生产服务器 +# 使用 PM2 启动 +npm run pm2:start + +# 或直接启动 npm start ``` -### 环境变量配置 +### PM2 进程管理 + +项目包含 PM2 配置文件 `ecosystem.config.js`: + +```javascript +module.exports = { + apps: [ + { + name: "spring-broken-ai-blog", + script: "node_modules/next/dist/bin/next", + args: "start -p 3000", + instances: 1, + exec_mode: "cluster", + env: { + NODE_ENV: "production", + }, + }, + ], +}; +``` + +### Nginx 配置示例 + +```nginx +server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### 部署脚本 + +项目提供了便捷的部署脚本: ```bash -# 生产环境环境变量 -DATABASE_URL="postgresql://username:password@localhost:5432/spring_broken_ai" -NEXTAUTH_SECRET="your-production-secret-key" -NEXTAUTH_URL="http://powder.icu/" -ADMIN_USERNAME="admin" -ADMIN_PASSWORD="your-secure-password" +# 完整部署流程 (构建 + 数据库设置) +npm run deploy:setup:prod + +# 仅构建 +npm run deploy:build ``` -### Docker 部署 (计划中) +### Docker 部署 (可选) ```dockerfile # Dockerfile (示例) FROM node:18-alpine WORKDIR /app + +# 安装依赖 COPY package*.json ./ RUN npm ci --only=production + +# 复制源码 COPY . . + +# 生成 Prisma 客户端 +RUN npx prisma generate + +# 构建应用 RUN npm run build + EXPOSE 3000 + CMD ["npm", "start"] ``` +## 🎯 AI 功能亮点 + +### 1. 智能写作助手 + +- **位置**: `src/components/admin/ai-assistant.tsx` +- **功能**: 基于 Kimi API 的 AI 辅助创作 +- **特性**: + - 多种写作模式: 续写、扩展、润色、总结 + - 流式输出,实时展示生成内容 + - 支持自定义提示词 + +### 2. RAG 聊天系统 + +- **位置**: `src/components/admin/rag-chat.tsx` +- **功能**: 基于文章内容的智能问答 +- **技术**: + - 向量检索: ChromaDB + Ollama Embedding + - 语义分块: 智能文本分块算法 + - 上下文注入: 检索结果注入提示词 + +### 3. AI 补全扩展 + +- **位置**: `src/lib/editor/ai-completion-extension.ts` +- **功能**: 编辑器内的智能内容续写 +- **实现**: 基于 ProseMirror 的编辑器扩展 + +### 4. 智能推荐 + +- **位置**: `src/components/admin/publish-dialog/` +- **功能**: AI 自动推荐分类和标签 +- **实现**: 基于文章内容的 NLP 分析 + +### 5. 向量索引系统 + +- **位置**: `src/lib/vector/` +- **功能**: 文章内容的向量化存储 +- **组件**: + - `chunker.ts`: 智能文本分块 + - `indexer.ts`: 索引管理 + - `store.ts`: ChromaDB 存储 + +## 📚 项目文档 + +项目包含丰富的技术文档: + +### 核心文档 + +- [README.md](./README.md) - 项目概览和快速开始 +- [CLAUDE.md](./CLAUDE.md) - 开发指南 (本文档) + +### AI 功能文档 + +- [AI集成实现指南](./docs/ai-ts/AI集成实现指南.md) - AI 功能完整实现 +- [AI功能亮点总结](./docs/ai-features/AI功能亮点总结.md) - AI 技术亮点 +- [向量索引系统技术文档](./docs/ai-features/向量索引系统技术文档.md) - RAG 实现 +- [流式输出实现文档](./docs/ai-features/流式输出实现文档.md) - 流式输出 +- [AI补全扩展技术文档](./docs/ai-features/AI补全扩展技术文档.md) - 编辑器扩展 + +### 开发指南 + +- [Next.js全栈开发完全指南](./docs/goodblog/frontend/react/Next.js全栈开发完全指南.md) - Next.js 教程 +- [启动指南](./docs/启动指南.md) - AI 服务启动 +- [部署指南](./docs/部署指南.md) - 生产环境部署 + +### 技术分享 + +- [浅谈Vibe Coding](./docs/resume/浅谈Vibe Coding.md) - Vibe Coding 理念 +- [从零到一:在博客系统中实践AI Agent开发](./docs/goodblog/AI/从零到一:在博客系统中实践AI Agent开发.md) - AI Agent 实践 + ## 🤝 贡献指南 ### 开发流程 1. Fork 项目仓库 2. 创建功能分支: `git checkout -b feature/new-feature` -3. 提交更改: `git commit -m 'Add new feature'` +3. 提交更改: `git commit -m 'feat: add new feature'` 4. 推送到分支: `git push origin feature/new-feature` 5. 创建 Pull Request @@ -479,6 +842,7 @@ CMD ["npm", "start"] - 遵循 ESLint 和 Prettier 规则 - 为新功能添加适当的注释 - 保持组件的单一职责原则 +- 提交信息遵循 Conventional Commits 规范 ### 提交信息规范 @@ -490,17 +854,55 @@ style: 代码格式调整 refactor: 代码重构 test: 添加测试 chore: 构建过程或工具变更 +perf: 性能优化 +ci: CI/CD 相关 ``` -## 📖 相关文档 +## 🔍 常见问题 + +### Q: AI 功能无法使用? -- [shadcn/ui 无头组件库使用指南](./docs/shadcn-ui-guide.md) -- [Phase 1 实现指南](./docs/phase-1-implementation-guide.md) -- [Phase 2 认证系统指南](./docs/phase-2-authentication-guide.md) +**A**: 确保以下服务已启动: + +- Ollama 服务: `ollama serve` +- ChromaDB 服务: `chroma run --host localhost --port 8000` +- 已配置环境变量: `.env.local` 中配置 `KIMI_API_KEY` + +### Q: 向量索引失败? + +**A**: 检查: + +1. Ollama 服务是否运行 +2. 是否已拉取模型: `ollama pull nomic-embed-text` +3. ChromaDB 服务是否启动 +4. 查看控制台错误日志 + +### Q: 数据库迁移错误? + +**A**: 尝试: + +```bash +# 重置数据库 +npm run db:reset + +# 或手动删除后重新生成 +rm prisma/dev.db +npm run db:generate +npm run db:push +npm run db:seed +``` + +### Q: PM2 启动失败? + +**A**: 检查: + +1. 是否已构建: `npm run build` +2. 端口 3000 是否被占用 +3. 查看日志: `pm2 logs spring-broken-ai-blog` ## 📞 支持与反馈 -如果你在使用过程中遇到问题或有建议,欢迎: +如果你在使用过程中遇到问题或有建议,欢迎: 1. 查阅项目文档 2. 搜索已有的 Issues @@ -513,6 +915,8 @@ chore: 构建过程或工具变更 --- -**Spring Broken AI Blog Blog** - 专注于高效创作和优雅展示的现代化博客系统 +**Spring Broken AI Blog** - 集成 AI 功能的现代化博客系统 + +🔗 **技术栈**: Next.js 15 + TypeScript + shadcn/ui + NextAuth.js + Prisma + Tailwind CSS + Kimi AI + ChromaDB -🔗 **技术栈**: Next.js 15 + TypeScript + shadcn/ui + NextAuth.js + Prisma + Tailwind CSS +🌟 **特色**: 智能写作助手 | RAG 聊天 | 向量检索 | 流式输出 diff --git a/README.md b/README.md index 0ac24a6..d741807 100644 --- a/README.md +++ b/README.md @@ -1,2539 +1,574 @@ -# Next.js全栈开发完全指南 +# Spring Broken AI Blog 🚀 -> 基于Spring Broken AI Blog项目的实战经验,帮助前端同学快速掌握全栈开发技能 +一个基于 **Next.js 15 + TypeScript + AI** 构建的现代化智能博客系统,集成了 AI 智能助手和 RAG (检索增强生成) 功能。 -## 🚀 项目介绍 +**首页** +![首页](/public/images/broken/shouye.png) -Spring Broken AI Blog 是一个基于 Next.js 15 的现代化全栈博客系统,专注于优雅的写作体验和流畅的阅读感受。本项目采用最新的 App Router 架构,集成了完整的认证系统、内容管理、数据存储等功能,是学习全栈开发的绝佳实践项目。 +**文章详情** +![详情](/public/images/broken/详情.png) -### 主要功能 +**后台管理** +![详情](/public/images/broken/后台仪表盘.png) -- 📝 **Markdown 编辑器** - 支持实时预览、代码高亮、数学公式 -- 🔐 **用户认证** - NextAuth.js 完整认证流程 -- 📊 **内容管理** - 文章 CRUD、分类标签、草稿发布 -- 🎨 **现代 UI** - Tailwind CSS + shadcn/ui 组件库 -- 🚀 **一键部署** - GitHub Actions + PM2 + Nginx +## 🎯 核心特性 -## 📁 项目目录结构 +### 基础功能 -``` -Spring Broken AI Blog/ -├── src/ -│ ├── app/ # Next.js App Router 页面 -│ │ ├── admin/ # 后台管理页面 -│ │ ├── api/ # API 路由 -│ │ ├── auth/ # 认证相关页面 -│ │ └── posts/ # 文章展示页面 -│ ├── components/ # React 组件 -│ │ ├── admin/ # 管理后台组件 -│ │ ├── ui/ # 基础 UI 组件 -│ │ └── markdown/ # Markdown 渲染组件 -│ ├── lib/ # 工具库 -│ └── types/ # TypeScript 类型定义 -├── prisma/ # 数据库相关 -│ ├── schema.prisma # 数据模型定义 -│ └── migrations/ # 数据库迁移文件 -├── docs/ # 项目文档 -├── public/ # 静态资源 -└── scripts/ # 部署脚本 -``` - -## 🛠️ 技术栈 - -- **框架**: Next.js 15 + TypeScript -- **数据库**: Prisma + SQLite -- **认证**: NextAuth.js -- **样式**: Tailwind CSS + shadcn/ui -- **部署**: PM2 + Nginx + GitHub Actions - -## 🚀 快速启动 - -### 环境要求 - -- Node.js >= 18.17.0 -- npm >= 9.0.0 -- **Ollama**(用于 AI 功能的向量生成,详见下方说明) - -### 安装和运行 - -```bash -# 1. 克隆项目 -git clone https://github.com/flawlessv/Spring-Lament-Blog.git -cd Spring-Lament-Blog - -# 2. 安装依赖 -npm install - -# 3. 启动服务(AI 功能需要) -# 启动 ChromaDB(选一种方式) -chroma run --host localhost --port 8000 -# 或(如果有 Docker) -docker run -d --name chromadb -p 8000:8000 chromadb/chroma:latest - -# 启动 Ollama(首次需要先安装和拉取模型) -# macOS: brew install ollama -# Linux: curl -fsSL https://ollama.com/install.sh | sh -ollama pull nomic-embed-text # 首次需要 -ollama serve - -# 4. 配置环境变量 -cp .env.example .env.local - -# 5. 初始化数据库 -npm run db:push -npm run db:seed - -# 6. 启动项目(新开终端) -npm run dev -``` - -> **⚠️ AI 功能需要:** ChromaDB 和 Ollama 服务必须运行。查看 [启动指南](./docs/启动指南.md) 了解详情。 - -### 常用命令 - -```bash -# 开发 -npm run dev # 启动开发服务器 -npm run build # 构建生产版本 -npm run start # 启动生产服务器 - -# 数据库 -npm run db:push # 推送数据库 schema -npm run db:seed # 填充初始数据 -npm run db:studio # 打开 Prisma Studio - -# 代码质量 -npm run lint # 代码检查 -npm run format # 代码格式化 -``` - -## 前言 - -最近部门在大力推全栈开发,作为一名前端开发者,想要入门全栈开发,那么这份指南就是为你准备的。我们将通过一个真实的博客项目(Spring Broken AI Blog),从零开始学习如何使用Next.js 15构建现代化的全栈应用以及后端、数据库、部署等相关知识简介。 - -## 文章大纲 - -**学习路径:先概念 → 再技术 → 后实践** - -1. **概念理解篇**(第1-3章):后端本质、Node.js、Next.js框架 -2. **技术深入篇**(第4-7章):App Router、数据流转、Prisma ORM、数据模型 -3. **项目实战篇**(第8-10章):项目结构、认证系统、CRUD操作 -4. **部署运维篇**(第11-12章):部署实战、性能优化 - ---- - -## 第1章:后端的本质 - -### 什么是后端? - -**后端(Backend)**是应用程序的服务器端部分,负责处理业务逻辑、数据管理和服务器通信。简单来说,后端就是"管数据的"。 - -### 前后端职责划分 - -**前端职责:** - -- 用户界面展示、用户交互、数据展示、用户体验 - -**后端职责:** - -- 数据存储、业务逻辑、API接口、安全控制 - -### 为什么前端同学要学后端? - -1. **大势所趋**:目前Vibe Coding盛行,AI全栈开发工程师可能是未来趋势 -2. **职业发展**:全栈开发者更受市场欢迎 -3. **项目理解**:知道数据如何流转,写出更好的前端代码 -4. **独立开发**:可以独立完成整个项目 - -### Next.js全栈开发优势 - -传统开发需要前端项目+后端项目+数据库,而Next.js全栈框架可以: - -- 一个项目包含前后端 -- 统一的代码库和部署流程 -- 更好的开发体验和性能优化 - ---- - -## 第2章:Node.js入门 - -### JavaScript Runtime运行时 - -在开始学习Node.js之前,我们需要理解一个核心概念:**Runtime(运行时)**。 - -一段JavaScript代码本质上就是字符串: - -```javascript -console.log("hello world"); -``` - -这段字符串能被执行吗?不能,它需要运行环境。 - -**Runtime就是代码的执行环境**。没有Runtime,代码就无法执行,就是一堆字符串。 - -### 浏览器 vs Node.js - -**浏览器Runtime:** - -- 内置JavaScript解释器 -- 提供DOM、BOM等浏览器API -- 只能运行在浏览器中 -- 主要用于用户界面开发 - -**Node.js Runtime:** - -- 基于Chrome V8引擎 -- 提供文件系统、HTTP等服务器API -- 可以运行在任何操作系统 -- 主要用于服务器端开发 - -### Node.js的核心能力 - -Node.js作为服务器端JavaScript运行时,提供了以下核心能力: - -#### 1. 文件系统操作 - -```javascript -const fs = require("fs"); -const path = require("path"); - -// 读取文件 -const data = fs.readFileSync("data.txt", "utf8"); -console.log(data); - -// 写入文件 -fs.writeFileSync("output.txt", "Hello Node.js", "utf8"); -``` - -#### 2. HTTP服务器 - -```javascript -const http = require("http"); - -const server = http.createServer((req, res) => { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ message: "Hello from Node.js" })); -}); - -server.listen(3000, () => { - console.log("服务器运行在 http://localhost:3000"); -}); -``` - -#### 3. 数据库操作 - -```javascript -// 使用Prisma ORM操作数据库 -const { PrismaClient } = require("@prisma/client"); -const prisma = new PrismaClient(); - -// 创建用户 -await prisma.user.create({ - data: { - name: "张三", - email: "zhang@example.com", - }, -}); - -// 查询用户 -const users = await prisma.user.findMany(); -``` - -#### 4. 第三方API调用 - -```javascript -// 调用外部API -const response = await fetch("https://api.example.com/data"); -const data = await response.json(); -``` - -### 为什么选择Node.js? - -对于前端同学来说,选择Node.js学习后端有以下优势: - -1. **语言统一**:前后端都用JavaScript,无需学习新语言 -2. **生态丰富**:npm包管理器,海量第三方库 -3. **性能优秀**:基于V8引擎,执行效率高 -4. **社区活跃**:大量教程和开源项目 - -### 学习目标 - -通过本章,你应该理解: - -- Runtime是代码的执行环境 -- Node.js提供了服务器端JavaScript运行能力 -- Node.js的核心功能:文件操作、HTTP服务、数据库操作 -- 为什么前端同学选择Node.js学习后端最合适 - -在下一章,我们将学习Next.js,这是一个基于Node.js的全栈框架,让全栈开发变得更加简单。 - ---- - -## 第3章:全栈框架Next.js - -### 传统开发方式的痛点 - -在传统的Web开发中,我们需要维护多个独立的项目: - -``` -项目结构: -├── frontend/ # React前端项目 -│ ├── src/ -│ │ ├── components/ # React组件 -│ │ ├── pages/ # 页面组件 -│ │ ├── hooks/ # 自定义Hook -│ │ ├── utils/ # 工具函数 -│ │ ├── services/ # API调用 -│ │ ├── store/ # 状态管理 -│ │ └── types/ # TypeScript类型 -│ ├── public/ # 静态资源 -│ ├── package.json -│ └── ... -├── backend/ # Node.js后端项目 -│ ├── src/ -│ │ ├── controllers/ # 控制器 -│ │ ├── services/ # 业务逻辑 -│ │ ├── models/ # 数据模型 -│ │ ├── routes/ # 路由定义 -│ │ ├── middleware/ # 中间件 -│ │ ├── utils/ # 工具函数 -│ │ └── config/ # 配置文件 -│ ├── package.json -│ └── ... -└── database/ # 数据库 - ├── migrations/ # 数据库迁移 - ├── seeds/ # 初始数据 - └── schema.sql # 数据库架构 -``` - -这种方式的缺点: - -- **开发复杂**:需要同时维护多个项目 -- **部署复杂**:需要分别部署前端和后端 -- **协调困难**:前后端接口需要协商 -- **类型安全**:前后端数据类型不一致 - -### Next.js的解决方案 - -Next.js是Vercel开发的React全栈框架,解决了传统开发的问题: - -**一个项目,前后端统一:** - -``` -项目结构: -├── app/ # 页面和API路由 -│ ├── page.tsx # 前端页面 -│ ├── api/ # 后端API -│ └── layout.tsx # 布局组件 -├── components/ # React组件 -├── lib/ # 工具函数 -└── prisma/ # 数据库 -``` - -### Next.js的核心优势 - -#### 0. Turbopack构建系统 - -Next.js 15引入了基于Rust的Turbopack构建系统,相比传统的Webpack有显著优势: - -**Turbopack优势:** - -- **启动速度快**:开发环境启动速度比Webpack快10倍 -- **增量编译**:只编译变更的部分,开发时几乎瞬时更新 -- **内存占用低**:更高效的内存使用,减少内存泄漏 -- **原生支持**:原生支持TypeScript、JSX、CSS等 -- **未来架构**:基于Rust,为Next.js未来发展奠定基础 - -**对比效果:** - -```bash -# Webpack (传统) -npm run dev # 启动时间: 10-30秒 -# 文件变更后刷新: 2-5秒 - -# Turbopack (Next.js 15) -npm run dev # 启动时间: 1-3秒 -# 文件变更后刷新: <100ms -``` - -#### 1. 文件系统路由 - -Next.js使用文件系统作为路由系统,非常直观: - -``` -app/ -├── page.tsx → / -├── about/page.tsx → /about -├── posts/ -│ ├── page.tsx → /posts -│ └── [slug]/ -│ └── page.tsx → /posts/hello-world -└── api/ - └── posts/ - └── route.ts → /api/posts -``` - -#### 2. 服务端渲染(SSR) - -Next.js支持多种渲染模式: - -- **SSR**:服务端渲染,SEO友好 -- **SSG**:静态生成,性能最佳 -- **ISR**:增量静态再生,平衡性能和更新 - -#### 3. API路由 - -在Next.js中,API路由就是普通的文件: - -```typescript -// app/api/posts/route.ts -export async function GET() { - const posts = await prisma.post.findMany(); - return Response.json(posts); -} - -export async function POST(request: Request) { - const data = await request.json(); - const post = await prisma.post.create({ data }); - return Response.json(post); -} -``` - -#### 4. 类型安全 - -Next.js + TypeScript提供端到端的类型安全: - -```typescript -// 前端组件 -interface Post { - id: string; - title: string; - content: string; -} - -// API路由 -export async function GET(): Promise> { - // 类型安全的数据查询 -} -``` - -### 为什么选择Next.js? - -1. **学习成本低**:基于React,前端同学容易上手 -2. **开发效率高**:约定大于配置,减少样板代码 -3. **性能优秀**:自动代码分割、图片优化、缓存策略 -4. **生态完善**:丰富的插件和工具链 -5. **部署简单**:支持Vercel一键部署 - -### 学习目标 - -通过本章,你应该理解: - -- 传统前后端分离开发的痛点 -- Next.js如何解决这些问题 -- Next.js的核心优势:文件路由、SSR、API路由、类型安全 -- 为什么Next.js是前端同学学习全栈的最佳选择 - -在下一章,我们将深入学习Next.js 15的App Router,这是Next.js最新的路由系统。 - ---- - -## 第4章:Next.js 15 App Router核心 - -### 版本说明 - -本指南基于**Next.js 15.0.0**版本,这是Next.js的最新稳定版本,带来了许多性能优化和新特性。 - -### "约定大于配置"的设计哲学 - -Next.js遵循"约定大于配置"的设计理念,通过文件命名和目录结构来定义应用的行为,而不是通过复杂的配置文件。 - -### 核心文件约定 - -在Next.js 15的App Router中,每个文件都有特定的作用: - -#### 1. page.tsx - 页面组件 - -```typescript -// app/posts/page.tsx -export default function PostsPage() { - return
文章列表页面
-} -``` - -这个文件自动成为`/posts`路由的页面组件。 - -#### 2. route.ts - API路由 - -```typescript -// app/api/posts/route.ts -export async function GET() { - return Response.json({ message: "获取文章列表" }); -} - -export async function POST(request: Request) { - const data = await request.json(); - return Response.json({ message: "创建文章", data }); -} -``` - -这个文件自动成为`/api/posts`的API端点。 - -#### 3. layout.tsx - 布局组件 - -```typescript -// app/layout.tsx -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - -
网站头部
- {children} -
网站底部
- - - ) -} -``` - -布局组件会包裹所有子页面。 - -#### 4. loading.tsx - 加载状态 - -```typescript -// app/posts/loading.tsx -export default function Loading() { - return
加载中...
-} -``` - -当页面加载时自动显示。 - -#### 5. error.tsx - 错误处理 - -```typescript -// app/posts/error.tsx -'use client' - -export default function Error({ - error, - reset, -}: { - error: Error & { digest?: string } - reset: () => void -}) { - return ( -
-

出错了!

- -
- ) -} -``` - -当页面出错时自动显示。 - -### 文件系统路由规则 - -Next.js 15使用文件系统来定义路由,非常直观: - -``` -app/ -├── page.tsx → / -├── about/ -│ └── page.tsx → /about -├── posts/ -│ ├── page.tsx → /posts -│ ├── loading.tsx → 加载状态 -│ ├── error.tsx → 错误处理 -│ └── [slug]/ -│ ├── page.tsx → /posts/hello-world -│ └── not-found.tsx → 404页面 -└── api/ - ├── posts/ - │ └── route.ts → /api/posts - └── posts/ - └── [id]/ - └── route.ts → /api/posts/123 -``` - -### Server Components vs Client Components - -Next.js 15默认使用Server Components,但也可以使用Client Components: - -#### Server Components(默认) - -```typescript -// app/posts/page.tsx - 服务端组件 -import { prisma } from '@/lib/prisma' - -export default async function PostsPage() { - // 在服务端执行,可以直接访问数据库 - const posts = await prisma.post.findMany() - - return ( -
- {posts.map(post => ( -
{post.title}
- ))} -
- ) -} -``` - -#### Client Components - -```typescript -// app/components/PostForm.tsx - 客户端组件 -'use client' - -import { useState } from 'react' - -export default function PostForm() { - const [title, setTitle] = useState('') - - const handleSubmit = () => { - // 客户端交互逻辑 - } - - return ( -
- setTitle(e.target.value)} - /> -
- ) -} -``` - -### 动态路由 - -Next.js支持动态路由,使用方括号语法: - -```typescript -// app/posts/[slug]/page.tsx -interface Props { - params: { slug: string } -} - -export default async function PostPage({ params }: Props) { - const post = await prisma.post.findUnique({ - where: { slug: params.slug } - }) - - return
{post?.title}
-} -``` - -### 学习目标 - -通过本章,你应该理解: - -- Next.js 15的App Router核心概念 -- 各种文件类型的作用:page.tsx、route.ts、layout.tsx等 -- 文件系统路由的规则和约定 -- Server Components和Client Components的区别 -- 动态路由的使用方法 - -在下一章,我们将通过博客项目的实际代码,学习完整的数据流转过程。 - ---- - -## 第5章:博客项目数据流转 - -### 项目概述 - -Spring Broken AI Blog是一个基于Next.js 15的现代化博客系统,包含: - -- **前台功能**:文章展示、分类浏览、标签筛选 -- **后台管理**:文章CRUD、用户管理、数据统计 -- **技术栈**:Next.js 15 + Prisma + NextAuth + SQLite - -### 完整的数据流转过程 - -让我们通过一个具体的例子,看看数据是如何在系统中流转的: - -#### 场景:用户访问文章详情页 - -**1. 用户访问URL** - -``` -用户访问:/posts/hello-world -``` - -**2. 路由匹配** - -``` -app/posts/[slug]/page.tsx -``` - -**3. 服务端组件执行** - -```typescript -// app/posts/[slug]/page.tsx -import { prisma } from '@/lib/prisma' - -interface Props { - params: { slug: string } -} - -export default async function PostPage({ params }: Props) { - // 1. 从数据库查询文章数据 - const post = await prisma.post.findUnique({ - where: { slug: params.slug }, - include: { - author: { select: { name: true, avatar: true } }, - category: true, - tags: true - } - }) - - if (!post) { - return
文章不存在
- } - - // 2. 渲染页面 - return ( -
-

{post.title}

-
{post.content}
-
作者:{post.author.name}
-
分类:{post.category.name}
-
- ) -} -``` - -**4. 数据库查询** - -```typescript -// lib/prisma.ts -import { PrismaClient } from "@prisma/client"; - -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; -}; - -export const prisma = globalForPrisma.prisma ?? new PrismaClient(); -``` - -**5. 返回渲染结果** - -- 服务端渲染完成 -- 返回HTML给浏览器 -- 浏览器显示页面 - -### API路由的数据流转 - -#### 场景:创建新文章 - -**1. 前端表单提交** - -```typescript -// app/admin/posts/new/page.tsx -'use client' - -export default function NewPostPage() { - const handleSubmit = async (data: FormData) => { - const response = await fetch('/api/admin/posts', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }) - - if (response.ok) { - router.push('/admin/posts') - } - } - - return -} -``` - -**2. API路由处理** - -```typescript -// app/api/admin/posts/route.ts -import { prisma } from "@/lib/prisma"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; - -export async function POST(request: Request) { - // 1. 验证用户身份 - const session = await getServerSession(authOptions); - if (!session) { - return Response.json({ error: "未授权" }, { status: 401 }); - } - - // 2. 解析请求数据 - const data = await request.json(); - - // 3. 数据验证 - if (!data.title || !data.content) { - return Response.json({ error: "标题和内容不能为空" }, { status: 400 }); - } - - // 4. 保存到数据库 - const post = await prisma.post.create({ - data: { - title: data.title, - content: data.content, - slug: generateSlug(data.title), - authorId: session.user.id, - }, - }); - - // 5. 返回结果 - return Response.json({ post }); -} -``` - -**3. 数据库操作** - -```typescript -// Prisma自动生成的SQL -INSERT INTO Post (title, content, slug, authorId) -VALUES (?, ?, ?, ?) -``` - -### 前后端在同一个项目的好处 - -#### 1. 类型安全 - -```typescript -// 共享类型定义 -interface Post { - id: string; - title: string; - content: string; - slug: string; - authorId: string; -} - -// 前端使用 -const [posts, setPosts] = useState([]); - -// API路由使用 -export async function GET(): Promise> { - const posts = await prisma.post.findMany(); - return Response.json(posts); -} -``` - -#### 2. 代码复用 - -```typescript -// lib/utils.ts - 共享工具函数 -export function generateSlug(title: string): string { - return title - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); -} - -// 前端使用 -const slug = generateSlug(formData.title); - -// 后端使用 -const post = await prisma.post.create({ - data: { slug: generateSlug(data.title) }, -}); -``` - -#### 3. 统一部署 - -```bash -# 一个命令部署整个应用 -npm run build -npm start -``` - -### 学习目标 - -通过本章,你应该理解: - -- 完整的数据流转过程:用户请求 → 路由匹配 → 服务端组件 → 数据库查询 → 渲染返回 -- API路由的处理流程:请求验证 → 数据解析 → 业务逻辑 → 数据库操作 → 响应返回 -- 前后端统一开发的优势:类型安全、代码复用、统一部署 -- 如何在Next.js中实现完整的数据流转 - -在下一章,我们将深入学习Prisma ORM,这是操作数据库的核心工具。 - ---- - -## 第6章:Prisma ORM - -### 什么是ORM? - -**ORM(Object-Relational Mapping)**是对象关系映射,是一种编程技术,用于在面向对象编程语言中管理关系型数据库。 - -简单来说,ORM让我们可以用面向对象的方式操作数据库,而不需要写SQL语句。 - -### 传统SQL vs Prisma对比 - -#### 传统SQL方式 - -```sql --- 创建用户 -INSERT INTO users (name, email, password) -VALUES ('张三', 'zhang@example.com', 'hashed_password'); - --- 查询用户 -SELECT * FROM users WHERE email = 'zhang@example.com'; - --- 更新用户 -UPDATE users SET name = '李四' WHERE id = 1; - --- 删除用户 -DELETE FROM users WHERE id = 1; -``` - -#### Prisma方式 - -```typescript -// 创建用户 -await prisma.user.create({ - data: { - name: "张三", - email: "zhang@example.com", - password: "hashed_password", - }, -}); - -// 查询用户 -const user = await prisma.user.findUnique({ - where: { email: "zhang@example.com" }, -}); - -// 更新用户 -await prisma.user.update({ - where: { id: 1 }, - data: { name: "李四" }, -}); - -// 删除用户 -await prisma.user.delete({ - where: { id: 1 }, -}); -``` - -### Schema定义详解 - -Prisma使用`schema.prisma`文件来定义数据库结构: - -```prisma -// prisma/schema.prisma -// Prisma客户端生成器配置,指定生成JavaScript/TypeScript客户端 -generator client { - provider = "prisma-client-js" -} -// 数据库连接配置,指定数据库类型和连接URL -datasource db { - provider = "sqlite" - url = env("DATABASE_URL") -} -// 用户数据模型定义 -model User { - id String @id @default(cuid()) - email String @unique - name String? - password String - role Role @default(USER) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - posts Post[] - profile Profile? - - @@map("users") -} -// @relation装饰器用于定义表之间的关联关系,指定外键字段和引用字段 - -model Post { - id String @id @default(cuid()) - title String - slug String @unique - content String - published Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - authorId String - author User @relation(fields: [authorId], references: [id]) - - categoryId String? - category Category? @relation(fields: [categoryId], references: [id]) - - tags PostTag[] - - @@map("posts") -} - -model Category { - id String @id @default(cuid()) - name String @unique - slug String @unique - description String? - - posts Post[] - - @@map("categories") -} - -model Tag { - id String @id @default(cuid()) - name String @unique - slug String @unique - color String? - - posts PostTag[] - - @@map("tags") -} - -model PostTag { - postId String - post Post @relation(fields: [postId], references: [id]) - tagId String - tag Tag @relation(fields: [tagId], references: [id]) - - @@id([postId, tagId]) - @@map("post_tags") -} - -model Profile { - id String @id @default(cuid()) - bio String? - avatar String? - - userId String @unique - user User @relation(fields: [userId], references: [id]) - - @@map("profiles") -} - -enum Role { - USER - ADMIN -} -``` - -### 关键概念解析 - -#### 1. 字段类型 - -```prisma -model User { - id String @id @default(cuid()) // 主键,自动生成ID - email String @unique // 唯一字段 - name String? // 可选字段 - password String // 必填字段 - role Role @default(USER) // 枚举类型,默认值 - createdAt DateTime @default(now()) // 时间戳,默认当前时间 - updatedAt DateTime @updatedAt // 更新时间,自动维护 -} -``` - -#### 2. 表关联关系 - -**一对多关系 (One-to-Many)** - -```prisma -model User { - id String @id @default(cuid()) - posts Post[] // 一个用户可以有多个文章 -} - -model Post { - id String @id @default(cuid()) - authorId String - author User @relation(fields: [authorId], references: [id]) -} -``` +- ✅ **现代化前端**: Next.js 15 + App Router + Turbopack +- ✅ **类型安全**: 全栈 TypeScript 支持 +- ✅ **无头组件**: shadcn/ui + Radix UI + Tailwind CSS +- ✅ **身份认证**: NextAuth.js v4 + JWT 策略 +- ✅ **数据库**: Prisma ORM + SQLite/PostgreSQL +- ✅ **代码质量**: ESLint + Prettier + Husky +- ✅ **响应式设计**: 移动端友好的界面 -**一对一关系 (One-to-One)** +### AI 功能亮点 -```prisma -model User { - id String @id @default(cuid()) - profile Profile? // 一个用户最多有一个资料 -} +- 🤖 **智能写作助手**: 基于 Kimi API 的 AI 辅助创作 +- 🧠 **向量索引系统**: ChromaDB + Ollama 实现本地向量存储 +- 🔍 **RAG 聊天功能**: 基于文章内容的智能问答 +- ✨ **AI 补全功能**: 编辑器内的智能内容续写 +- 📝 **智能推荐**: AI 自动推荐分类和标签 +- 💬 **流式输出**: 实时展示 AI 生成内容 -model Profile { - id String @id @default(cuid()) - userId String @unique - user User @relation(fields: [userId], references: [id]) -} -``` - -**多对多关系 (Many-to-Many)** - -```prisma -model Post { - id String @id @default(cuid()) - tags PostTag[] // 通过中间表实现多对多 -} - -model Tag { - id String @id @default(cuid()) - posts PostTag[] -} - -model PostTag { - postId String - post Post @relation(fields: [postId], references: [id]) - tagId String - tag Tag @relation(fields: [tagId], references: [id]) - - @@id([postId, tagId]) // 复合主键 -} -``` +## 🛠️ 技术栈 -### 基础查询操作 +### 核心框架 -#### 1. 创建数据 (Create) +- **Next.js 15**: React 全栈框架,使用 App Router + Turbopack +- **TypeScript**: 静态类型检查 +- **React 18**: 用户界面库 -```typescript -// 创建单个记录 -const user = await prisma.user.create({ - data: { - name: "张三", - email: "zhang@example.com", - password: "hashed_password", - }, -}); - -// 创建关联数据 -const post = await prisma.post.create({ - data: { - title: "我的第一篇文章", - content: "文章内容...", - slug: "my-first-post", - author: { - connect: { id: user.id }, - }, - }, -}); -``` +### UI 系统 -#### 2. 查询数据 (Read) +- **shadcn/ui**: 无头组件库 +- **Radix UI**: 无头 UI 原语 +- **Tailwind CSS**: 实用优先的 CSS 框架 +- **Lucide React**: 现代化图标库 +- **Novel**: Notion 风格的编辑器 +- **react-markdown**: Markdown 渲染 +- **highlight.js**: 代码高亮 -```typescript -// 查询所有记录 -const users = await prisma.user.findMany(); - -// 查询单个记录 -const user = await prisma.user.findUnique({ - where: { email: "zhang@example.com" }, -}); - -// 条件查询 -const posts = await prisma.post.findMany({ - where: { - published: true, - author: { - name: "张三", - }, - }, -}); - -// 关联查询 - 一次性获取文章及其作者、分类、标签信息 -const postsWithAuthor = await prisma.post.findMany({ - include: { - author: true, - category: true, - tags: true, - }, -}); - -// 查询结果示例: -// [ -// { -// id: "1", -// title: "Next.js指南", -// content: "...", -// author: { id: "1", name: "张三", email: "zhang@example.com" }, -// category: { id: "1", name: "前端技术" }, -// tags: [{ id: "1", name: "Next.js" }, { id: "2", name: "React" }] -// } -// ] - -// 对比:普通查询 - 只获取文章基本信息 -const posts = await prisma.post.findMany(); -// 查询结果示例: -// [ -// { -// id: "1", -// title: "Next.js指南", -// content: "...", -// authorId: "1", // 只有ID,没有作者详细信息 -// categoryId: "1" // 只有ID,没有分类详细信息 -// } -// ] - -// 如果用普通查询获取完整信息,需要多次查询: -const posts2 = await prisma.post.findMany(); -const authors = await prisma.user.findMany(); -const categories = await prisma.category.findMany(); -// 然后在代码中手动关联数据... -``` +### AI 能力 -#### 3. 更新数据 (Update) +- **Kimi API (Moonshot AI)**: AI 对话和生成 +- **Ollama**: 本地 Embedding 生成 (nomic-embed-text 模型) +- **ChromaDB**: 向量数据库,用于 RAG 检索 +- **OpenAI SDK**: 兼容 Kimi API 的调用方式 -```typescript -// 更新单个记录 -const updatedUser = await prisma.user.update({ - where: { id: user.id }, - data: { name: "李四" }, -}); - -// 批量更新 -await prisma.post.updateMany({ - where: { published: false }, - data: { published: true }, -}); -``` +### 数据层 -#### 4. 删除数据 (Delete) +- **Prisma 6.16.1**: 现代化 ORM +- **SQLite**: 开发/生产环境数据库 +- **Prisma Adapter**: NextAuth.js 数据库适配器 -```typescript -// 删除单个记录 -await prisma.user.delete({ - where: { id: user.id }, -}); - -// 批量删除 -await prisma.post.deleteMany({ - where: { published: false }, -}); -``` +### 身份认证 -### Migration迁移 +- **NextAuth.js v4**: 身份认证库 +- **JWT**: 会话管理策略 +- **bcryptjs**: 密码哈希 -当Schema发生变化时,需要运行迁移来更新数据库。以下是完整的Prisma使用流程: +### 部署工具 -#### Prisma完整使用流程 +- **PM2**: Node.js 进程管理器 +- **Nginx**: Web 服务器和反向代理 -**1. 安装Prisma** +## 📁 项目结构 -```bash -# 安装Prisma CLI和客户端 -npm install prisma @prisma/client ``` +Spring-Broken-AI-Blog/ +├── src/ +│ ├── app/ # Next.js App Router 页面 +│ │ ├── admin/ # 管理后台页面 +│ │ │ ├── page.tsx # 后台首页 +│ │ │ ├── posts/ # 文章管理 +│ │ │ ├── categories/ # 分类管理 +│ │ │ ├── tags/ # 标签管理 +│ │ │ ├── profile/ # 个人资料 +│ │ │ └── settings/ # 系统设置 +│ │ ├── login/ # 登录页面 +│ │ ├── api/ # API 路由 +│ │ │ ├── auth/ # NextAuth.js API +│ │ │ ├── admin/ # 管理后台 API +│ │ │ └── ai/ # AI 功能 API +│ │ ├── posts/[slug]/ # 文章详情页 +│ │ ├── category/[slug]/ # 分类页面 +│ │ ├── globals.css # 全局样式 +│ │ └── layout.tsx # 根布局 +│ ├── components/ # React 组件 +│ │ ├── ui/ # shadcn/ui 基础组件 +│ │ ├── admin/ # 管理后台组件 +│ │ │ ├── ai-assistant.tsx # AI 写作助手 +│ │ │ ├── rag-chat.tsx # RAG 聊天组件 +│ │ │ ├── post-editor.tsx # 文章编辑器 +│ │ │ └── publish-dialog.tsx # 发布对话框 +│ │ ├── markdown/ # Markdown 渲染组件 +│ │ ├── posts/ # 文章展示组件 +│ │ ├── providers/ # 上下文提供器 +│ │ └── layout/ # 布局组件 +│ ├── lib/ # 工具库和配置 +│ │ ├── auth.ts # NextAuth.js 配置 +│ │ ├── prisma.ts # Prisma 客户端 +│ │ ├── utils.ts # 工具函数 +│ │ ├── ai/ # AI 相关 +│ │ │ ├── client.ts # AI 客户端 (Kimi + Ollama) +│ │ │ ├── prompts/ # AI 提示词 +│ │ │ └── rag.ts # RAG 实现 +│ │ ├── vector/ # 向量索引 +│ │ │ ├── chunker.ts # 文本分块 +│ │ │ ├── indexer.ts # 索引管理 +│ │ │ └── store.ts # 向量存储 (ChromaDB) +│ │ └── editor/ # 编辑器相关 +│ │ ├── ai-completion-extension.ts +│ │ └── markdown-converter.ts +│ └── types/ # TypeScript 类型定义 +├── prisma/ # Prisma 数据库配置 +│ ├── schema.prisma # 数据库模型 +│ ├── seed.ts # 数据库种子 +│ └── dev.db # SQLite 数据库 (开发环境) +├── scripts/ # 脚本工具 +│ ├── ai/ # AI 服务脚本 +│ │ ├── start-ai.sh # 启动 AI 服务 (开发) +│ │ └── stop-ai.sh # 停止 AI 服务 (开发) +│ └── README.md # 脚本说明文档 +├── docs/ # 项目文档 +├── public/ # 静态资源 +├── components.json # shadcn/ui 配置 +├── tailwind.config.ts # Tailwind CSS 配置 +├── middleware.ts # Next.js 中间件 (路由保护) +└── ecosystem.config.js # PM2 配置文件 +``` + +## 🚀 快速开始 -**2. 初始化Prisma** +### 环境要求 ```bash -# 初始化Prisma配置 -npx prisma init +Node.js >= 18.0.0 +npm >= 8.0.0 ``` -**3. 编写Schema** +### AI 功能依赖 (可选) -编辑`prisma/schema.prisma`文件定义数据模型 +如需使用 AI 功能,需要安装以下服务: -**4. 生成Prisma Client** +#### 1. Ollama (用于向量生成) ```bash -# 生成TypeScript类型化的Prisma客户端 -npx prisma generate -``` +# macOS +brew install ollama -这一步会: +# Linux +curl -fsSL https://ollama.com/install.sh | sh -- 根据schema.prisma生成Prisma Client代码 -- 创建TypeScript类型定义 -- 在node_modules/.prisma/client中生成客户端代码 - -**5. 推送Schema到数据库** +# 启动 Ollama 服务 +ollama serve -```bash -# 将schema同步到数据库(适用于开发环境) -npx prisma db push +# 拉取 Embedding 模型 +ollama pull nomic-embed-text ``` -这一步会: - -- 读取schema.prisma文件 -- 创建或更新数据库表结构 -- 不生成migration文件 - -**6. 创建Migration(生产环境推荐)** +#### 2. ChromaDB (用于向量存储) ```bash -# 生成迁移文件 -npx prisma migrate dev --name add-user-role -``` - -这一步会: - -- 创建migration文件记录schema变更 -- 应用变更到数据库 -- 自动运行`prisma generate` - -**7. 应用Migration到生产环境** +# 使用 Docker +docker run -d --name chromadb -p 8000:8000 chromadb/chroma:latest -```bash -# 在生产环境应用migration -npx prisma migrate deploy +# 或直接使用 Python +pip install chromadb +chroma run --host localhost --port 8000 ``` -**流程对比:** +### 安装步骤 -| 场景 | 使用命令 | 说明 | -| ---------------- | ----------------------- | ------------------------ | -| 开发环境快速测试 | `prisma db push` | 快速同步,不记录变更历史 | -| 正式开发 | `prisma migrate dev` | 记录变更历史,可回滚 | -| 生产部署 | `prisma migrate deploy` | 安全地应用所有migration | - -### 学习目标 - -通过本章,你应该理解: - -- ORM的概念和优势 -- Prisma Schema的定义方法 -- 各种字段类型和约束 -- 表关联关系的设计 -- 基本的CRUD操作 -- Migration迁移的作用 - -在下一章,我们将深入分析博客项目的数据模型,学习如何设计复杂的数据结构。 - ---- - -## 第7章:博客数据模型解析 - -### 项目数据模型概览 +```bash +# 1. 克隆项目 +git clone +cd Spring-Broken-AI-Blog -Spring Broken AI Blog的数据模型包含以下核心实体: +# 2. 安装依赖 +npm install +# 3. 配置环境变量 +cp .env.example .env.local ``` -User (用户) -├── Profile (个人资料) - 一对一 -├── Post (文章) - 一对多 -└── Role (角色) - 枚举 - -Post (文章) -├── User (作者) - 多对一 (多篇文章对应一个作者) -├── Category (分类) - 多对一 (多篇文章对应一个分类) -└── Tag (标签) - 多对多 (多篇文章对应多个标签) -Category (分类) -└── Post (文章) - 一对多 +编辑 `.env.local` 文件: -Tag (标签) -└── Post (文章) - 多对多 -``` +```bash +# 数据库配置 +DATABASE_URL="file:./prisma/dev.db" -### 核心模型详解 +# NextAuth 配置 +NEXTAUTH_SECRET="your-secret-key-at-least-32-characters-long" +NEXTAUTH_URL="http://localhost:7777" -#### 1. User模型 - 用户管理 +# 管理员账户 (seed 时使用) +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="0919" -```prisma -model User { - id String @id @default(cuid()) - email String @unique - name String? - password String - role Role @default(USER) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +# AI 配置 (可选) +KIMI_API_KEY="your-kimi-api-key" +KIMI_BASE_URL="https://api.moonshot.cn/v1" +KIMI_MODEL="moonshot-v1-32k" - posts Post[] - profile Profile? +# Ollama 配置 (用于向量生成) +OLLAMA_BASE_URL="http://localhost:11434" +OLLAMA_EMBEDDING_MODEL="nomic-embed-text" - @@map("users") -} +# ChromaDB 配置 (用于向量存储) +CHROMADB_HOST="localhost" +CHROMADB_PORT="8000" ``` -**字段说明:** +### 数据库设置 -- `id`: 主键,使用cuid()生成唯一ID -- `email`: 邮箱,唯一约束,用于登录 -- `name`: 姓名,可选字段 -- `password`: 密码,存储加密后的哈希值 -- `role`: 角色,枚举类型(USER/ADMIN) -- `createdAt/updatedAt`: 时间戳,自动维护 +```bash +# 生成 Prisma 客户端 +npm run db:generate -**实际应用:** +# 推送数据库架构 +npm run db:push -```typescript -// 创建管理员用户 -const admin = await prisma.user.create({ - data: { - email: "admin@blog.com", - name: "博客管理员", - password: await bcrypt.hash("password123", 10), - role: "ADMIN", - }, -}); - -// 查询用户及其文章 -const userWithPosts = await prisma.user.findUnique({ - where: { id: userId }, - include: { - posts: { - where: { published: true }, - orderBy: { createdAt: "desc" }, - }, - }, -}); +# 填充初始数据 +npm run db:seed ``` -#### 2. Post模型 - 文章管理 - -```prisma -model Post { - id String @id @default(cuid()) - title String - slug String @unique - content String - excerpt String? - coverImage String? - published Boolean @default(false) - publishedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - authorId String - author User @relation(fields: [authorId], references: [id]) - - categoryId String? - category Category? @relation(fields: [categoryId], references: [id]) +### 启动开发服务器 - tags PostTag[] +```bash +# 启动开发服务器 (端口 7777) +npm run dev - @@map("posts") -} +# 访问应用 +# 前台: http://localhost:7777 +# 登录: http://localhost:7777/login +# 后台: http://localhost:7777/admin ``` -**字段说明:** - -- `title`: 文章标题 -- `slug`: URL友好的标识符,唯一约束 -- `content`: 文章内容(Markdown格式) -- `excerpt`: 文章摘要,可选 -- `coverImage`: 封面图片URL,可选 -- `published`: 发布状态,默认草稿 -- `publishedAt`: 发布时间,可选 - -**实际应用:** +### 默认管理员账户 -```typescript -// 创建文章 -const post = await prisma.post.create({ - data: { - title: "Next.js全栈开发指南", - slug: "nextjs-fullstack-guide", - content: "# 指南内容...", - excerpt: "学习Next.js全栈开发", - authorId: user.id, - categoryId: category.id, - tags: { - create: [ - { tag: { connect: { id: tag1.id } } }, - { tag: { connect: { id: tag2.id } } }, - ], - }, - }, -}); - -// 查询文章详情(包含所有关联数据) -const postDetail = await prisma.post.findUnique({ - where: { slug: "nextjs-fullstack-guide" }, - include: { - author: { select: { name: true, avatar: true } }, - category: true, - tags: { include: { tag: true } }, - }, -}); ``` - -#### 3. Category模型 - 分类管理 - -```prisma -model Category { - id String @id @default(cuid()) - name String @unique - slug String @unique - description String? - - posts Post[] - - @@map("categories") -} +用户名: admin +密码: 0919 ``` -**实际应用:** +--- -```typescript -// 创建分类 -const category = await prisma.category.create({ - data: { - name: "前端技术", - slug: "frontend", - description: "前端开发相关文章", - }, -}); - -// 查询分类及其文章数量 -const categoriesWithCount = await prisma.category.findMany({ - include: { - _count: { - select: { posts: true }, - }, - }, -}); -``` +## 🚀 快速启动 -#### 4. Tag模型 - 标签管理 +### 方式一: 仅启动博客 (不含 AI 功能) -```prisma -model Tag { - id String @id @default(cuid()) - name String @unique - slug String @unique - color String? +```bash +# 1. 安装依赖 +npm install - posts PostTag[] +# 2. 配置环境变量 +cp .env.example .env.local +# 编辑 .env.local,配置数据库和 NextAuth - @@map("tags") -} -``` +# 3. 初始化数据库 +npm run db:generate && npm run db:push && npm run db:seed -**实际应用:** +# 4. 启动开发服务器 +npm run dev -```typescript -// 创建标签 -const tag = await prisma.tag.create({ - data: { - name: "Next.js", - slug: "nextjs", - color: "#000000", - }, -}); - -// 查询热门标签 -const popularTags = await prisma.tag.findMany({ - include: { - _count: { - select: { posts: true }, - }, - }, - orderBy: { - posts: { _count: "desc" }, - }, - take: 10, -}); +# 访问 http://localhost:7777 ``` -#### 5. PostTag模型 - 文章标签关联 - -```prisma -model PostTag { - postId String - post Post @relation(fields: [postId], references: [id]) - tagId String - tag Tag @relation(fields: [tagId], references: [id]) +### 方式二: 完整启动 (含 AI 功能) - @@id([postId, tagId]) - @@map("post_tags") -} -``` - -**实际应用:** +```bash +# 1. 安装依赖 +npm install -```typescript -// 为文章添加标签 -await prisma.postTag.create({ - data: { - postId: post.id, - tagId: tag.id, - }, -}); - -// 查询文章的所有标签 -const postWithTags = await prisma.post.findUnique({ - where: { id: postId }, - include: { - tags: { - include: { tag: true }, - }, - }, -}); -``` +# 2. 安装 Ollama (macOS) +brew install ollama -### 复杂查询示例 +# 3. 启动 AI 服务 +./scripts/ai/start-ai.sh -#### 1. 分页查询文章列表 +# 4. 配置环境变量 +cp .env.example .env.local +# 编辑 .env.local,添加 Kimi API Key: +# KIMI_API_KEY="sk-your-key-here" -```typescript -const posts = await prisma.post.findMany({ - where: { published: true }, - include: { - author: { select: { name: true, avatar: true } }, - category: true, - tags: { include: { tag: true } }, - }, - orderBy: { publishedAt: "desc" }, - skip: (page - 1) * limit, // 跳过前面的记录数,实现分页 - take: limit, // 限制返回的记录数量 -}); -``` +# 5. 初始化数据库 +npm run db:generate && npm run db:push && npm run db:seed -#### 2. 按分类筛选文章 +# 6. 启动开发服务器 +npm run dev -```typescript -const postsByCategory = await prisma.post.findMany({ - where: { - published: true, - category: { - slug: "frontend", - }, - }, - include: { - author: true, - category: true, - }, -}); +# 访问 http://localhost:7777 ``` -#### 3. 标签云查询 +**停止 AI 服务:** -```typescript -const tagCloud = await prisma.tag.findMany({ - include: { - _count: { - select: { posts: true }, - }, - }, - orderBy: { - posts: { _count: "desc" }, - }, -}); +```bash +./scripts/ai/stop-ai.sh ``` -### 数据模型设计原则 - -#### 1. 规范化设计 - -- 避免数据冗余 -- 使用外键建立关联 -- 合理使用索引 - -#### 2. 性能考虑 - -- 主键使用cuid()而非自增ID -- 为常用查询字段添加索引 -- 使用include控制查询深度 - -#### 3. 扩展性 +### AI 服务启动说明 -- 预留可选字段 -- 使用枚举类型 -- 考虑未来需求 +项目提供了 `start-ai.sh` 和 `stop-ai.sh` 脚本来管理 AI 服务: -### 学习目标 +- **start-ai.sh**: 自动启动 Ollama (向量生成) 和 ChromaDB (向量存储) +- **stop-ai.sh**: 停止所有 AI 服务 -通过本章,你应该理解: +**注意**: -- 博客项目的完整数据模型设计 -- 各种关联关系的实际应用 -- 复杂查询的实现方法 -- 数据模型设计的最佳实践 -- 如何在Prisma中实现复杂的业务逻辑 - -在下一章,我们将学习项目的整体结构,了解各个目录和文件的作用。 +- 首次运行 `start-ai.sh` 会自动下载 `nomic-embed-text` 模型 (约 274MB) +- 需要申请 [Kimi API Key](https://platform.moonshot.cn/) 才能使用 AI 对话功能 +- 详细的启动指南和故障排查请查看 [启动指南.md](./docs/guides/启动指南.md) --- -## 第8章:项目结构全解析 - -### 项目目录结构 - -Spring Broken AI Blog采用Next.js 15的App Router架构,目录结构如下: - -``` -Spring-Lament-Blog/ -├── src/ # 源代码目录 -│ ├── app/ # Next.js App Router -│ │ ├── admin/ # 管理后台页面 -│ │ │ ├── layout.tsx # 后台布局 -│ │ │ ├── page.tsx # 后台首页 -│ │ │ ├── posts/ # 文章管理 -│ │ │ │ ├── page.tsx # 文章列表 -│ │ │ │ ├── new/ # 新建文章 -│ │ │ │ └── [id]/ # 编辑文章 -│ │ │ ├── categories/ # 分类管理 -│ │ │ ├── tags/ # 标签管理 -│ │ │ └── profile/ # 个人资料 -│ │ ├── api/ # API路由 -│ │ │ ├── admin/ # 后台API -│ │ │ │ ├── posts/ # 文章API -│ │ │ │ ├── categories/ # 分类API -│ │ │ │ └── tags/ # 标签API -│ │ │ ├── auth/ # 认证API -│ │ │ └── posts/ # 公开API -│ │ ├── posts/ # 文章展示页面 -│ │ │ └── [slug]/ # 文章详情 -│ │ ├── category/ # 分类页面 -│ │ ├── login/ # 登录页面 -│ │ ├── layout.tsx # 根布局 -│ │ └── page.tsx # 首页 -│ ├── components/ # React组件 -│ │ ├── admin/ # 后台组件 -│ │ │ ├── post-editor.tsx # 文章编辑器 -│ │ │ ├── unified-posts-table.tsx # 文章表格 -│ │ │ └── ... -│ │ ├── ui/ # shadcn/ui组件 -│ │ │ ├── button.tsx # 按钮组件 -│ │ │ ├── form.tsx # 表单组件 -│ │ │ └── ... -│ │ ├── markdown/ # Markdown组件 -│ │ │ ├── markdown-renderer.tsx -│ │ │ └── code-block.tsx -│ │ └── layout/ # 布局组件 -│ ├── lib/ # 工具函数库 -│ │ ├── auth.ts # NextAuth配置 -│ │ ├── prisma.ts # Prisma客户端 -│ │ └── utils.ts # 通用工具 -│ └── types/ # TypeScript类型 -├── prisma/ # 数据库相关 -│ ├── schema.prisma # 数据模型定义 -│ ├── seed.ts # 初始数据 -│ └── dev.db # SQLite数据库 -├── public/ # 静态资源 -├── docs/ # 项目文档 -├── scripts/ # 部署脚本 -├── package.json # 项目配置 -├── next.config.js # Next.js配置 -├── tailwind.config.ts # Tailwind配置 -└── tsconfig.json # TypeScript配置 -``` - -### 核心目录详解 - -#### 1. src/app/ - Next.js App Router - -**页面路由 (Pages)** - -```typescript -// app/page.tsx - 首页 -export default function HomePage() { - return
博客首页
-} - -// app/posts/[slug]/page.tsx - 文章详情页 -interface Props { - params: { slug: string } -} - -export default async function PostPage({ params }: Props) { - const post = await prisma.post.findUnique({ - where: { slug: params.slug } - }) - - return
{post?.title}
-} -``` - -**API路由 (API Routes)** - -```typescript -// app/api/posts/route.ts - 文章API -export async function GET() { - const posts = await prisma.post.findMany(); - return Response.json(posts); -} - -export async function POST(request: Request) { - const data = await request.json(); - const post = await prisma.post.create({ data }); - return Response.json(post); -} -``` - -**布局组件 (Layouts)** - -```typescript -// app/layout.tsx - 根布局 -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - -
- {children} -