Files
NoteAI/docs/code-review-2026-07-31.md
T

124 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# wojide (NoteAI) 项目健康度分析报告
> 分析日期:2026-07-31 | 分析角色:高级开发工程师(吴八哥)
> 技术栈:Next.js 16 (App Router) · React 19 · TypeScript strict · Prisma 5.10.2 + SQLite · Tailwind 3 · TipTap 3 · Zustand
> 说明:本文仅做分析与现状盘点,**未改动任何代码**。所有修复项需你确认后再动手。
---
## 0. 整体评价
代码质量高于平均水平:无 `any`、无 TODO/FIXME、无原始 SQL、主要 API 均有认证与输入校验、XSS 有 sanitize、登录有限速。**真正的短板不在业务逻辑,而在两处**:
1. **仓库卫生**(数据库入库 + 占位密钥)——会造成真实的数据泄露与越权风险;
2. **内容膨胀路径**(base64 图片 × 全量同步 × localStorage 快照)——笔记应用最典型的性能黑洞。
先把这两类按下,其余都是体验与健壮性层面的优化。
---
## 1. 🔴 高危问题(建议优先处理)
### 1.1 数据库文件被提交进 git(泄露密码哈希 + 全部笔记)
- **证据**`git ls-files` 显示 `prisma/dev.db` 在跟踪列表;`.gitignore` 第 36 行虽有 `prisma/*.db`,但该规则是**后加**的,对已跟踪文件不生效。
- **风险**:任何能访问仓库的人都能拿到 scrypt 密码哈希 + 你的全部笔记正文。哈希被拖库后可离线爆破。
- **修复**
```bash
git rm --cached prisma/dev.db
# 清理历史(破坏性,需评估):git filter-repo --path prisma/dev.db --invert-paths
# 随后在 .gitignore 已覆盖的前提下重新提交;已泄露的密码建议重置
```
- **注意**:若仓库已 push 到 GitHub`origin` 已存在),清历史会影响协作者,需先沟通或确认是私有仓。
### 1.2 会话密钥为占位符 + 部署默认弱密码
- **证据**`README.md:11` 与 `.env` 中 `SESSION_SECRET="replace-with-a-long-random-secret"``DEPLOY.md:162-176` 文档默认密码 `admin`。
- **风险**:若按文档部署而未替换,`SESSION_SECRET` 是公开值,任何人可用它**伪造 HMAC 会话 token 直接登录**。默认密码 `admin` 同样危险。
- **修复**:部署前 `npm run env:init` 生成随机值;在 `src/lib/session.ts` 启动时**显式拒绝**已知占位符(抛出启动错误而非静默运行)。
---
## 2. 🟠 性能问题
| 位置 | 问题 | 建议 |
|---|---|---|
| `src/components/editor.tsx:677` | `Image.configure({ allowBase64: true })`:粘贴图片以 base64 嵌进 content,单页可达数 MB;每次编辑防抖后整页 content PUT 回传 + 入 SQLite + 进 localStorage 历史快照 | 实现图片上传接口存文件,content 只存 URL(**最高优先级性能项**) |
| `src/components/editor.tsx:28` | `import { lowlight } from "lowlight"` 全量导出,把 >800KB 高亮语言打进客户端 bundle | 改用 `createLowlight(common)` 按需注册 |
| `src/app/page.tsx:87-90` | `localHistory` 的 `useMemo` 依赖 `[activePage]`,每次按键 `updatePage` 都替换 activePage 引用 → **每次按键**都执行 localStorage 读取 + 最多 30 个全文快照 JSON.parse | 依赖改为 `[activePage?.id, isHistoryOpen]`,仅面板打开时读取 |
| `src/components/sidebar/tree-view.tsx:284-288` | 每层重建 `buildFolderFileCount`/`buildHasChildrenSet`(无 memo),整树 O(n²);无虚拟化 | 上移到 Sidebar 用 `useMemo`;预构建 parentId→children Map;页面多时虚拟列表 |
| `src/app/api/pages/route.ts:35-46` | 不带参数时返回**全部页面含全部 content**,可能数十 MB 响应 | 默认强制 lightweight 或分页 |
| `src/app/api/pages/[id]/route.ts:185-196` | DELETE 先 `findMany` 全表再逐条 `delete`N 个后代 N+1 条 SQL | schema 加 `onDelete: Cascade`,一条语句搞定 |
| `src/lib/layout.tsx:13-14` | `maximumScale:1, userScalable:false` 禁用缩放,可访问性硬伤 | 移除这两项 |
---
## 3. 🟠 安全性(中危,非紧急但需修)
| 位置 | 问题 | 建议 |
|---|---|---|
| `src/app/api/ai/chat/route.ts:5-18,69` | SSRF 黑名单可绕过:`2130706433`、`0x7f000001`、`[::ffff:127.0.0.1]`、解析到内网的域名、上游 302 均未防 | fetch 前 DNS 解析校验 IP`redirect: "manual"`;或改白名单 |
| `src/lib/rate-limit.ts:48-58` | `getClientIp` 无条件信任 `x-forwarded-for`,直连部署下可换头绕过登录限速 | 仅可信反代时读该头,否则用 socket 地址;叠加全局失败计数 |
| `src/lib/session.ts:94` + `logout/route.ts:7` | 无会话吊销:`tokenId` 从未使用,改密码/登出后旧 token 直到过期(最长 30 天)仍有效 | 存 `sessionVersion`/`passwordChangedAt`,验证时比对 |
| `src/lib/sanitize-html.ts:132-136` | 服务端降级分支只匹配带引号 `on*` 属性、`javascript:` 仅处理 href | 无引号属性也纳入;或干脆服务端拒绝而非降级 |
| `src/app/api/settings/init/route.ts:27-29` | `providedToken !== initToken` 非常量时间比较 | 用 `crypto.timingSafeEqual` |
---
## 4. 🟡 代码质量 / 潜在 Bug(中低危)
| 位置 | 级别 | 问题 | 建议 |
|---|---|---|---|
| `ai-chat-panel.tsx:104-116` / `editor.tsx:1059-1078` | 中 | SSE 流解析无跨 chunk 缓冲,`decoder.decode` 未传 `{stream:true}`,JSON 帧跨 TCP 边界被截断 → 内容丢失 | 维护跨 chunk 字符串缓冲,只消费完整 `data:` 行 |
| `src/lib/search-query.ts:73` + `store.ts:160` | 中 | 全文搜索基于 `page.content`,但 `fetchPages` 默认 lightweightcontent 空),**未打开过的页面正文搜索静默漏检** | 搜索改走服务端 API,或明确提示"仅搜索已加载页面" |
| `src/components/sidebar.tsx:65-73` | 中 | 侧边栏宽度/折叠只写不读 localStorage,初始 state 恒为 `280/false`,持久化形同虚设 | `useState(() => Number(localStorage.getItem("sidebar-width")) || 280)` |
| `src/lib/store.ts:409-415` | 中 | `reorderPages` 浅拷贝后 `page.order = order` **原地突变**,破坏 Zustand 不可变约定 | `newPages.map(p => ids.has(p.id) ? {...p, order} : p)` |
| `src/lib/auth.ts:26` | 低 | 哈希 key 长度异常时 `timingSafeEqual` 抛异常 → 登录 500 而非 401 | 先比长度,不等则 return false |
| `src/app/login/page.tsx:23,29` | 低 | 初始化接口失败时默认 `setIsInitialized(true)`,DB 未初始化+异常时卡登录页 | 失败显示错误+重试按钮 |
| `src/components/editor.tsx:775-889` | 低 | 用 `window.onpointermove =` 赋值会覆盖其他全局处理器 | 改用 `addEventListener` |
| `src/app/api/pages/reorder/route.ts:10` | 低 | `request.json()` 无 `.catch`,非 JSON body 返回 500 | 补 `.catch(() => null)` |
| `src/components/sidebar/tree-view.tsx:268` | 低 | `(isExpanded || hasChildren) && isExpanded` 恒等于 `isExpanded` | 简化为 `isExpanded &&` |
---
## 5. 🟡 架构 / Next.js
- **全量 Client 渲染**`src/app/page.tsx`、`settings/page.tsx` 等全部 `"use client"`,零服务端预取,首屏白屏后客户端再 fetch 全量 pages。App Router 仅当路由用 → 至少把 pages 列表改为 Server Component 预取或 RSC + hydrate。
- **缺失的边界**:仅有根级 `loading.tsx`/`error.tsx`;缺 `not-found.tsx``/settings`、`/login` 无独立 error boundary。
- **proxy 匹配**`src/proxy.ts:31` matcher 未排除 `public/` 下其他资源(如 `/file.svg`),会被 HMAC 校验拦截并跳登录。
- **editor 双 ref hack**`allowOnUpdateRef`/`suppressNextUpdateRef` 规避 setContent 回环,脆弱,选项闭包过期会出怪 bug。
---
## 6. 🟡 依赖与配置
| 项 | 问题 | 建议 |
|---|---|---|
| 未使用依赖 | 源码零引用:`framer-motion`、`@radix-ui/react-tabs`、`@radix-ui/react-slot`、`@tiptap/extension-bubble-menu`、`@tiptap/extension-floating-menu`、`@tiptap/extension-gapcursor`、`@tiptap/extension-link`StarterKit 已含)等 | `npx depcheck` 后移除 |
| 版本错位 | `next 16.1.1` vs `eslint-config-next ^16.1.6``marked v17` 却装 `@types/marked ^5`(旧类型包可能遮蔽正确类型);`lowlight ^2.9.0` 落后 TipTap v3 推荐的 v3 | 对齐版本,删除 `@types/marked` |
| Prisma 落后 | `@prisma/client 5.10.2` 锁定 2024-02 版,落后主线 6.x 两代 | 计划升级 Prisma 6 |
| 命名规范 | 包名 `note-ai` 与目录 `wojide` 不一致;`version 1.0.0304` 用日期当补丁号,非 semver | 统一命名 |
| ESLint 豁免 | `eslint.config.mjs:9-13` 对 editor 目录关闭 `no-explicit-any`/`ban-ts-comment`,给劣化开口子 | 收窄或移除豁免 |
---
## 7. ✅ Top 10 优先修复清单
1. **[高|安全]** `prisma/dev.db` 从 git 移除并清理历史(数据 + 密码哈希泄露)。
2. **[高|安全]** 替换占位 `SESSION_SECRET``session.ts` 启动拒绝占位值。
3. **[高|性能]** 关闭 `allowBase64`,改图片上传接口。
4. **[中|安全]** `session.ts` 增加会话吊销(改密码/登出后旧 token 失效)。
5. **[中|Bug]** SSE 解析加跨 chunk 缓冲(`ai-chat-panel.tsx:104` / `editor.tsx:1059`)。
6. **[中|Bug]** 修复 lightweight 模式正文搜索漏检(改服务端搜索)。
7. **[中|安全]** `ai/chat` SSRF 改 IP 校验 + 禁跟随重定向;`rate-limit` 停止无条件信任 `x-forwarded-for`。
8. **[中|DB]** `schema.prisma` Page 自关联加 `onDelete: Cascade`,简化 DELETE 路由。
9. **[中|Bug]** `sidebar.tsx:65` 补 localStorage 读取恢复;`store.ts:409` 修复 reorder 状态突变。
10. **[中|性能]** `page.tsx:87` 修复每次按键解析 localStorage 历史;`tree-view.tsx:284` 树计算加 memo`editor.tsx:28` lowlight 按需加载。
---
## 8. 后续建议
- **先做第 1、2 项**(仓库卫生):投入小、风险降最大,且涉及 git 历史改写,越早处理越干净。
- **其次第 3 项**(图片上传):是长期性能与存储健康的根基,但改动面较大,建议单独排期。
- **其余按体验痛点优先级**逐步推进,不影响线上安全。
需要我从哪一项开始动手?按你的协作习惯,我会先给方案、你拍板后再改代码。