J'Blog
← 返回文章列表

从 Pages Router 到 App Router:Next.js 博客架构全面升级与私人 Workspace 实战

从 Pages Router 到 App Router:Next.js 博客架构全面升级与私人 Workspace 实战

# 从 Pages Router 单体到 App Router + 私人 Workspace:我的 Next.js 博客架构升级全记录

> 本文是对 [ARCHITECTURE_UPGRADE.md](./ARCHITECTURE_UPGRADE.md) 的「读者向」总结,并补充 **Workspace 私人工作台**、**AI 任务调度**、**知识库 RAG**、**桌面猫猫** 等后续迭代的实现思路。可直接复制到博客后台发布,或作为团队 onboarding 文档。

---

## 写在前面:为什么要升级

这个项目最初是典型的 **Pages Router + JavaScript 单体**:页面在 `getServerSideProps` 里 `fetch` 自己的 API,配置写在 `next.config.js`,Mongoose 与原生 MongoDB 驱动双轨并存。能跑,但慢、脆、难扩展。

升级目标很明确:

1. **对齐 Next.js 14 主流模型**(App Router、Server Component、Route Handler)
2. **业务与 HTTP 解耦**(`lib/services` 作为唯一数据入口)
3. **类型安全覆盖 UI**(全项目 TypeScript)
4. **在公开博客之上,叠一层仅管理员可用的私人 Workspace**(写作、职业档案、知识库、AI 实验)

---

## 一、架构升级路线图

```mermaid
flowchart TB
subgraph phase1 [阶段一:本地环境]
MONGO[本地 MongoDB + 数据导入]
end

subgraph phase2 [阶段二:服务层]
SVC[lib/services/*]
ENV[.env.local 外置密钥]
end

subgraph phase3 [阶段三:认证]
AUTH[lib/services/auth]
COOKIE[lib/auth/cookies]
SER[lib/serialize]
end

subgraph phase4 [阶段四:API 统一]
WH[withRouteHandler]
ERR[HttpError]
end

subgraph phase5 [阶段五:App Router]
APP[app/**]
API[app/api/**/route.ts]
end

subgraph phase6 [阶段六:TypeScript UI]
TSX[components/**/*.tsx]
TYPES[lib/types/*]
end

subgraph phase7 [阶段七:Workspace 扩展]
WS[/workspaces/*]
AI[LLM Router + RAG]
CAT[桌面猫猫 Mascot]
end

phase1 --> phase2 --> phase3 --> phase4 --> phase5 --> phase6 --> phase7
```

| 阶段 | 核心变化 | 关键知识点 |
|------|----------|------------|
| 一 | 本地 `myblog` 库可联调 | `mongodump` / `mongorestore` |
| 二 | 去掉 SSR 自调 API | 服务层模式、环境变量 |
| 三 | 认证模块化 | JWT + Cookie、`serializeForProps` |
| 四 | API 错误统一 | 包装器模式、薄 Route Handler |
| 五 | App Router 迁移 | RSC vs Client Component、Edge SSE |
| 六 | 全项目 TS | 共享类型、Mongoose 模型显式 import |
| 七 | Workspace + AI | 任务路由、RAG、加密备忘录、Canvas 动画 |

---

## 二、升级前 vs 升级后:一条请求的生命周期

### 2.1 升级前(多一次无意义 HTTP)

```text
浏览器 → pages/index.js (GSSP)
→ fetch(URL + '/api/posts') ← 再打回自己
→ pages/api/posts.js → MongoDB
```

**痛点:** 依赖 `URL` 配置、多一轮延迟、API 500 时 GSSP 解析 HTML 报 `Unexpected token '<'`。

### 2.2 升级后(服务端直连 Service)

```text
浏览器 → app/page.tsx (Server Component)
→ getVisiblePosts() → lib/services/posts.ts → MongoDB

浏览器 → fetch('/api/contact') ← 仅客户端交互走 API
→ app/api/contact/route.ts
→ withRouteHandler → createMessage()
```

```mermaid
sequenceDiagram
participant B as 浏览器
participant P as app/page.tsx
participant S as lib/services/posts
participant DB as MongoDB

B->>P: GET /
P->>S: getVisiblePosts()
S->>DB: Article.find()
DB-->>S: documents
S-->>P: serializePost[]
P-->>B: HTML (RSC)
```

**收益:**

- 服务端渲染更快、更稳
- 密钥只在 `.env.local`,不进构建产物
- 业务逻辑可单测、可复用

---

## 三、分层架构:代码应该放哪里

当前推荐结构(公开博客 + Workspace 共用):

```text
next-blog/
├── app/ # 路由:公开页 + /workspaces/* + API
├── components/ # UI(.tsx,按需 "use client")
├── lib/
│ ├── api/ # HttpError、withRouteHandler
│ ├── auth/ # Cookie、session、require-user
│ ├── ai/ # task-types、model-registry
│ ├── services/ # ★ 业务唯一入口
│ ├── models/ # Mongoose 模型
│ ├── mascot/ # 猫猫精灵定义与加载
│ └── serialize.ts # Date/ObjectId → JSON 安全
├── public/images/cats/ # 猫猫序列帧 + meta.json
└── docs/
├── ARCHITECTURE_UPGRADE.md
└── BLOG_ARCHITECTURE_AND_WORKSPACE.md ← 本文
```

**原则:**

| 层级 | 职责 | 不应做 |
|------|------|--------|
| `app/**/page.tsx` | 组装 UI、服务端取数 | 直接写 Mongoose 查询 |
| `app/api/**/route.ts` | HTTP 边界、鉴权、校验 | 复杂业务逻辑 |
| `lib/services/*` | 业务规则、AI 编排、DB | 操作 `NextResponse` |
| `components/*` | 展示与交互 | 直连数据库 |

---

## 四、认证改造:公开博客 vs 私人 Workspace

早期是「登录即可用上传/Chat」。现在拆成 **访客 / 管理员** 两套体验:

```mermaid
flowchart LR
subgraph public [公开区]
HOME[首页 / 文章]
CHAT[/chat 需登录]
CONTACT[联系表单]
end

subgraph admin [管理员专属]
WS[Workspace /workspaces]
API_WS[/api/workspaces/*]
end

VISITOR[访客] --> HOME
VISITOR --> CONTACT
USER[登录用户] --> CHAT
ADMIN[ADMIN_EMAIL] --> WS
ADMIN --> API_WS
```

**实现要点:**

1. **`ADMIN_EMAIL`**:环境变量指定唯一管理员邮箱
2. **`ALLOW_PUBLIC_SIGNUP=false`**:关闭公开注册;可选邀请码
3. **`middleware.ts`**:Edge 上用 `jose` 校验 JWT(`lib/auth/session.ts`),保护 `/workspaces/*` 与 `/api/workspaces/*`
4. **登录后跳转**:管理员 → `/workspaces` Dashboard,普通用户 → 原首页

**知识点:**

- JWT 存于 `userInfo` Cookie,middleware 与 API 共用同一套 verify 逻辑
- Server Component 用 `requireAdminUserId`;Edge middleware 不能 import Node 专用模块,故 session 校验单独封装

---

## 五、Workspace 功能地图

Workspace 是叠在公开博客上的 **私人操作系统**,模块如下:

```mermaid
mindmap
root((Workspace))
Dashboard
快捷 Todo
知识库 Widget
Todo
标签 / 截止日
拖拽排序
Career
简历 PDF 解析
精选项目生成
Case Study 编辑
Dev Setup
Markdown 导入
AI 归类工具
Vault 备忘录
AES 加密 API Key
一键复制
Knowledge
CRUD 知识点
混合检索 RAG
博客草稿导出
Posts 文章
TipTap 富文本
AI 润色提炼
AI
多模型 Playground
右侧抽屉 + 对话记忆
Mascot
桌面猫猫大佬
鼠标追踪互动
```

| 模块 | 路径 | 后端 Service | 亮点 |
|------|------|--------------|------|
| Dashboard | `/workspaces` | `workspace-dashboard.ts` | 聚合视图 |
| Todo | `/workspaces/todo` | `todo.ts` | 标签、拖拽、关联项目 |
| Career | `/workspaces/career` | `career.ts` | 简历解析 + 多阶段 AI 生成 |
| Dev Setup | `/workspaces/dev-setup` | `dev-setup-ai.ts` | AI 归类 macOS 工具链 |
| 备忘录 | `/workspaces/vault` | `secrets-vault.ts` | AES-256-GCM 加密 |
| 知识库 | `/workspaces/knowledge` | `knowledge*.ts` | 关键词 + 向量混合检索 |
| 文章 | `/workspaces/posts` | `articles-admin.ts` | CRUD + TipTap + AI refine |
| AI 体验 | `/workspaces/ai` | `ai-playground.ts` | 流式对话、多提供商 |

---

## 六、AI 架构:任务调度 + 凭证分离

这是本次迭代里最重要的「架构意识」升级:**模型选择** 与 **API Key 来源** 是两层,不要混在一个 `.env` 里。

### 6.1 任务 → 模型(LLM Router)

```mermaid
flowchart TB
subgraph tasks [业务任务 LlmTask]
T1[career.parse]
T2[knowledge.ask]
T3[posts.refine]
T4[playground.chat]
end

subgraph router [llm-router.ts]
REG[model-registry.ts]
RESOLVE[resolveModelForTask]
end

subgraph env [环境变量覆盖]
E1["LLM_TASK_KNOWLEDGE_ASK=glm-4.5-air"]
E2["LLM_TASK_POSTS_REFINE=glm-4.5-air"]
end

tasks --> RESOLVE
REG --> RESOLVE
env -.-> RESOLVE
RESOLVE --> CALL[completeJsonForTask / streamChatForTask]
```

**代码入口:** 业务层只调 `completeJsonForTask('knowledge.ask', …)`,不直接读 `ZHIPU_MODEL`。

**任务类型定义:** `lib/ai/task-types.ts`(Career、Dev Setup、Knowledge、Posts、Playground、公开 Chat 等 15+ 任务)。

**模型注册表:** `lib/ai/model-registry.ts` — 每个任务默认模型、`maxTokens`、`temperature`;可用 `LLM_TASK_*` 环境变量覆盖。

### 6.2 凭证 → 备忘录优先(llm-api-key.ts)

```mermaid
flowchart LR
REQ[AI 请求] --> R1{显式 apiKey?}
R1 -->|是| USE[使用传入 Key]
R1 -->|否| R2{备忘录有 zhipu Key?}
R2 -->|是| VAULT[secrets-vault 解密]
R2 -->|否| R3{ZHIPU_API_KEY env?}
R3 -->|是| ENV[环境变量兜底]
R3 -->|否| ERR[报错提示配置]
```

**为什么分开:**

- Playground 需要切换不同厂商 Key → 走备忘录
- 公开 `/api/chat` 没有 userId → 走 env 兜底
- Workspace 内 Career / 知识库 / 文章润色 → 传 `userId`,优先读管理员备忘录

**知识点:** OpenAI SDK + 智谱 `baseURL` 兼容接口;`completeJson` 解析 fenced JSON;流式 SSE 在 Route Handler 里 `ReadableStream` piping。

---

## 七、知识库 RAG:从 CRUD 到混合检索

知识库分三阶段落地:

```mermaid
flowchart TB
subgraph p1 [Phase 1: CRUD]
CRUD[KnowledgeEntry 模型]
UI[知识库页面增删改查]
end

subgraph p2 [Phase 2: Embedding]
EMB[embedding.ts 智谱 embedding-2]
IDX[knowledge-index.ts 写入向量]
end

subgraph p3 [Phase 3: Hybrid RAG]
KW[关键词打分]
SEM[余弦相似度]
MERGE[0.35 keyword + 0.65 semantic]
AI[knowledge-ai.ts 问答 / 标签 / 博客草稿]
end

p1 --> p2 --> p3
```

**混合检索公式(简化):**

- 关键词:标题命中 +12、标签 +6、正文词频
- 语义:`cosineSimilarity(queryEmbedding, entry.embedding)`
- 有向量时:`score = normKeyword × 0.35 + semantic × 0.65`

**延伸能力:**

- 问答结果「存为知识点」
- 按项目关联知识点 → 生成简历「个人解说」
- 多知识点 → 生成隐藏博客草稿(`isHide: true`)→ Workspace 文章模块继续编辑发布

**知识点:** RAG 不一定需要 Pinecone;小规模个人库 MongoDB 存 `embedding[]` + 内存余弦即可。生产量大再考虑专用向量库。

---

## 八、Career 模块:多阶段 AI 流水线

Career 是 Workspace 里最重的 AI 编排之一:

```mermaid
sequenceDiagram
participant U as 管理员
participant API as /api/career
participant S as career.ts
participant LLM as llm-router

U->>API: 上传简历 PDF
API->>S: pdf 提取 rawText
U->>API: 解析项目
S->>LLM: career.parse
LLM-->>S: projects[]
Note over S: 补全精选项目 career.extract-single
U->>API: 生成 Case Study
S->>LLM: career.generate-overview
S->>LLM: career.generate-deepdives
Note over S: 结合 knowledge-rag 项目知识块
S-->>U: card + caseStudy + technicalDeepDives
```

**设计点:**

- 简历原文只作「背景」,个人解说 + 知识库块驱动深挖
- `parse-notes.ts` 从分号分隔解说里拆技术点清单,逐条展开
- UI 上精选项目支持 **展开/折叠** 卡片,避免页面过长

---

## 九、文章系统:富文本 + AI 润色

| 能力 | 实现 |
|------|------|
| 存储 | MongoDB `Article`,正文 HTML(TipTap 输出) |
| 公开渲染 | `article-body.ts`:HTML sanitize 或 legacy Markdown 回退 |
| 管理 | `/workspaces/posts` CRUD、草稿/发布、置顶 |
| AI | `article-ai.ts`:`polish-body` / `polish-summary` / `refine-all`,任务 `posts.refine` |

发布时 `revalidatePath('/')` 刷新公开首页 ISR 缓存路径。

---

## 十、备忘录(Secrets Vault)

```text
明文 API Key → AES-256-GCM 加密 → MongoDB SecretsVaultProfile
读取时 decrypt → 仅 server-side
UI 只显示 maskHint(如 sk-****abcd)
```

Playground 与 `resolveLlmApiKey` 通过 `resolveSecretValue(userId, { providerId: 'zhipu' })` 取 Key,**不进前端、不进 Git**。

---

## 十一、桌面猫猫:Canvas 状态机动画

猫猫(大佬)是 **纯客户端装饰层**,不影响业务 API:

```mermaid
stateDiagram-v2
[*] --> Resting: 初始 / 撒娇完成
Resting --> Chasing: 鼠标移动
Chasing --> Greeting: 接近鼠标 APPROACH_DIST
Greeting --> Resting: 播完 sit→sleep 到最后一帧
Resting --> Chasing: 再次移动鼠标
Chasing --> Greeting: 追到并进入 forward 序列
note right of Chasing
walk-loop: 纯 gait 帧循环
loopStart~loopEnd
end note
note right of Greeting
play-forward: 从当前帧连续播
减速→舔毛→躺下,不跳回 walk 开头
end note
note right of Resting
tail-hold: sleep 最后几帧 ping-pong
仅尾巴微动,不重复躺下
end note
```

**技术栈:**

- `requestAnimationFrame` + Canvas 2D 贴图
- 序列帧来自视频抽帧,`meta.json` 定义 timeline segment(walk / sit / sleep)及 `loopStart` / `loopEnd`
- **离散帧时钟**(按 fps 整数步进),避免 float 取模掉帧
- 层容器 `pointer-events-none`,**不挡页面点击**;仅命中猫猫精灵区域时 window 级监听处理拖拽
- `dynamic import` + `requestIdleCallback` 延迟加载,不抢首屏

**优化历程(本次对话迭代):**

1. 整段 timeline 循环 → 卡顿 → 改为片段 + 状态机
2. walk 末尾混入 sit 帧 → `loopEnd` 缩短 + `loopStart` 限定纯 gait
3. 到达鼠标跳帧 → 接近后 `play-forward` 从当前帧向前播,不 reset
4. 甩尾重复整段 sleep → 仅 ping-pong 尾部 5 帧(116–120)

---

## 十二、性能与交互:会不会阻塞页面?

| 项 | 结论 |
|----|------|
| Canvas 动画 | 单猫、每帧一次 draw,开销极小 |
| pointer-events | 全层穿透,不挡按钮链接 |
| 边缘情况 | 猫猫画在按钮上时,可能抢拖拽;光标会变 `grab` |
| 可选优化 | `enabled=false` 或 Tab 隐藏时暂停 RAF;命中时检测下方是否 interactive 元素 |

当前策略:**先保持现状**,Workspace 以效率为主,猫猫在边缘活动,冲突极少。

---

## 十三、API 与错误处理模式

所有 Route Handler 推荐模式:

```typescript
export const POST = withRouteHandler('POST', async (req) => {
const userId = await requireAdminUserId(req);
// 业务 throw HttpError.badRequest(...)
// 包装器统一 JSON { success, message }
});
```

**知识点:**

- `ApiError` vs `HttpError` 工厂方法
- 业务错误 `throw`,避免每个 route 手写 `try/catch`
- Workspace API 统一前缀 `/api/workspaces/*`,middleware 批量保护

---

## 十四、环境变量速查(升级后完整版)

| 变量 | 用途 |
|------|------|
| `MONGODB_URI` | MongoDB 连接 |
| `JWT_SECRET` | 登录令牌 |
| `ADMIN_EMAIL` | Workspace 管理员 |
| `ALLOW_PUBLIC_SIGNUP` | 是否开放注册 |
| `ZHIPU_API_KEY` | 智谱 Key(兜底) |
| `ZHIPU_EMBEDDING_MODEL` | 向量模型,默认 embedding-2 |
| `LLM_TASK_*` | 按任务覆盖模型,如 `LLM_TASK_POSTS_REFINE` |
| `SMTP_*` | 注册通知邮件 |

复制 `.env.example` → `.env.local`,**切勿提交密钥**。

---

## 十五、我学到的架构原则(复盘)

1. **SSR 不要 fetch 自己** — Service 层直连 DB 是一劳永逸的优化。
2. **Route Handler 要薄** — HTTP 与业务分离,错误格式才能统一。
3. **Props 必须可 JSON 化** — Mongoose 文档进 RSC 前必须 `serializeForProps`。
4. **AI 要任务化** — 模型、Key、温度按任务配置,而不是一个全局 `MODEL`。
5. **凭证分层** — 备忘录 > env;Playground 与自动化场景需求不同。
6. **RAG 先简单后复杂** — 混合检索 + Mongo 内向量足够个人知识库。
7. **装饰性 UI 与业务解耦** — 猫猫用 Canvas + 延迟加载,不污染 services。
8. **动画用状态机 + 数据驱动** — timeline 放 `meta.json`,代码只管 playback mode。

---

## 十六、后续可演进方向

| 方向 | 说明 |
|------|------|
| 向量库外置 | 知识点上千条后迁 Qdrant / pgvector |
| ISR / 缓存 | 公开文章列表 Redis 缓存 |
| 类型检查 CI | `npm run typecheck` 独立脚本 |
| 猫猫交互 | 命中检测跳过下方 button/a |
| E2E | Playwright 覆盖登录 + Workspace CRUD |

---

## 附录:公开 API 与 Workspace API 对照

| 类型 | 示例路径 | 鉴权 |
|------|----------|------|
| 公开读 | `GET /api/posts` | 无 |
| 公开写 | `POST /api/contact` | 无 |
| 需登录 | `POST /api/chat` | Cookie session |
| 管理员 | `POST /api/workspaces/knowledge` | ADMIN + middleware |
| 管理员 | `POST /api/workspaces/posts` | AI refine / CRUD |

---

## 参考文档

- 仓库内详细变更清单:[ARCHITECTURE_UPGRADE.md](./ARCHITECTURE_UPGRADE.md)
- 日常上手:[README.md](../README.md)

---

*如果你也在维护一个「老 Next 博客 + 新私人工作台」的双层产品,希望这篇能帮你少踩坑。有问题欢迎在 Issues 讨论。*