# Pure Note — 极简高安全私人笔记 + 博客 设计方案 > 版本:v1.1(第 1 轮审评修订版) > 日期:2026-09-08 > 状态:已按 docs/review-round1.md 全部 24 项发现修订(P0×4 / P1×11 / P2×9),可进入 M0 开发 > 调研数据来源:pkg.go.dev / npm registry / GitHub API 在线核实(2026-09-07) --- ## 1. 项目概述与目标 ### 1.1 定位 一款部署在公网服务器上的私人笔记 + 博客程序,B/S 架构: - **给自己用**:Markdown 笔记,支持内嵌图片,内容只分「公开 / 私有」两种; - **给外部看**:公开笔记自动构成一个博客站点,可匿名浏览; - **运营极简**:最终交付 = **一个二进制程序 + 一个数据文件(目录)**,无数据库服务、无缓存服务、无 Node 运行时、无外部依赖。 ### 1.2 核心原则 | 原则 | 落实手段 | | --- | --- | | 安全性极高 | 全链路默认安全:Argon2id 口令哈希、双层 Markdown 清洗防 XSS、严格 CSP、CSRF 双保险、上传魔数校验、最小权限模型,详见 §9 | | 架构极简 | 纯 Go 标准库 HTTP + 5 个直接依赖;SQLite 单文件;前端 SPA 用 `go:embed` 打进二进制;无 ORM、无框架、无微服务 | | 单二进制交付 | CGO 关停、纯 Go SQLite 驱动(modernc.org/sqlite)、`web/dist` 经 Makefile 拷贝到可嵌入目录(§8.3),交叉编译一键出包 | | 权限体系简单健壮 | 笔记仅两种状态(public/private);唯一管理员账号持有编辑权;匿名用户只见 public;服务端统一可见性过滤,不依赖前端隐藏 | | 数据可恢复 | 删除 = 回收站软删除(30 天);每日在线备份;升级前强制备份 | ### 1.3 目标 / 非目标 **目标(IN SCOPE)** - Markdown 笔记 CRUD,内嵌图片(粘贴 / 拖拽上传),公开/私有切换; - 博客展示:首页列表、笔记详情、标签归档、RSS、sitemap; - 单人管理员,登录后编辑;匿名只读公开内容;管理员可在博客皮肤预览私有笔记; - 回收站:删除走软删除,30 天内可恢复,`gc` 到期清除; - 单机部署:systemd / 反代(Caddy 自动 TLS)。 **非目标(OUT OF SCOPE,预留演进位)** - 多用户、多人协作、评论系统(可后续加,不影响本设计); - 全文搜索(SQLite FTS5 可平滑引入,预留); - 版本历史/编辑快照(回收站只保证「误删可恢复」,不做多版本); - 多实例集群、CDN 同步(单实例架构,见 §11 边界说明); - 多端实时同步/移动客户端。 --- ## 2. 需求分析 ### 2.1 功能需求 | 编号 | 模块 | 需求 | | --- | --- | --- | | F1 | 笔记管理 | 创建/编辑 Markdown 笔记;标题、slug、标签、置顶;草稿即「私有」 | | F2 | 权限 | 笔记只有 `public` / `private` 两态;切换即时生效;私有内容任何匿名途径(含 API、标签、RSS、图片直链、HTML meta)不可见 | | F3 | 图片 | 编辑器内粘贴/拖拽上传(≤5MB);Markdown 中引用;图片可见性 = 引用它的笔记可见性的并集;去重存储 | | F4 | 博客 | 匿名访问:首页(公开列表+分页+标签)、详情页、标签页;RSS Feed;sitemap.xml;SEO 基础(仅公开笔记注入 title/description/og meta) | | F5 | 认证 | 单管理员密码登录/登出/**改密**;会话失效/续期;登录与改密防爆破 | | F6 | 站点设置 | 站点标题、副标题、每页条数(白名单字段,绝不含口令哈希) | | F7 | 回收站 | 删除 = 软删除进入回收站;30 天内可恢复;`gc` 到期物理清除 | | F8 | 运维 | 单二进制启动;`--data-dir` 指定数据目录;在线备份子命令;`gc` 子命令(默认 dry-run);版本号 | ### 2.2 非功能需求 | 类别 | 要求 | | --- | --- | | 安全 | OWASP 常见风险全覆盖(XSS/CSRF/注入/会话/上传/点击劫持/MIME 嗅探),上线前过 §9.3 检查单,其中可见性矩阵长期由自动化测试守护(§13) | | 性能 | 个人规模(<10k 笔记、<100k 图片):P95 页面响应 < 300ms;SQLite 游刃有余 | | 可靠性 | 崩溃不损坏数据(WAL);每日自动备份;升级可回滚;优雅停机不丢在途写入 | | 极简 | 直接依赖 ≤6(Go 侧 5 个);无 Node 服务端运行时;运维只需 systemd 单元 ×2(服务 + 每日 gc/备份 timer)+ 反代配置 | | 可维护 | 前后端分离清晰;API 稳定;Markdown 渲染为可替换组件;核心不变量有自动化回归 | --- ## 3. 技术选型调研结论 > 以下版本号均为 2026-09-07 经 pkg.go.dev、npm registry、GitHub API 在线核实。 ### 3.1 后端选型 | 组件 | 选择 | 版本 | 理由 | 备选(被否决原因) | | --- | --- | --- | --- | --- | | 语言 | Go | 1.27.1 | 单一静态二进制、交叉编译、内存安全、并发模型适合 HTTP 服务 | — | | HTTP 路由 | 标准库 `net/http` + 1.22 增强 ServeMux | — | 方法与通配符路由足够支撑小型 JSON API,**零框架依赖** | chi(需要中间件生态时再引入)、gin/echo(非必需) | | SQLite 驱动 | `modernc.org/sqlite` | v1.58.0 | **纯 Go、无 CGO**,内嵌 SQLite 3.53.4,跨平台静态编译,3500+ 依赖方生产可用 | mattn/go-sqlite3(需要 CGO_ENABLED=1 + gcc,破坏单二进制目标) | | Markdown 解析(服务端) | `goldmark` | v1.8.6 | CommonMark 兼容、默认转义原始 HTML 与危险 URL(XSS 安全默认)、GFM/脚注/排版扩展齐全、v1 线成熟稳定 | gomarkdown(仍在 v0)、blackfriday(2020 年停更);goldmark v2.0.1 刚发布,稳定后评估升级 | | HTML 清洗(服务端) | `bluemonday` | v1.0.27 | 白名单式清洗标准方案;与 goldmark 双保险 | — | | 口令哈希 | `golang.org/x/crypto/argon2` | v0.56.0 | **Argon2id**,OWASP 现行参数 m=19456(19MiB)、t=2、p=1;按 **PHC 字符串** 编码存储(§7.3) | bcrypt(同样可接受,但 Argon2 为当前首选) | | 会话 | **自建 DB 会话表**(~80 行) | — | crypto/rand 256bit token + SHA-256 摘要入库,可撤销、可过期清理、零额外依赖;单管理员场景完全可控 | gorilla/sessions v1.4.0(已恢复维护)、scs v2.9.0(轻量备选,含 SQLite store) | | 限流 | `golang.org/x/time/rate` | v0.15.0 | 令牌桶,登录/改密防爆破够用(per-IP + per-账号双维度);桶有数量上限与 TTL 清理(§7.2) | Redis 方案(多实例才需要) | | 静态资源 | `go:embed` + `http.FS` | — | **embed 只能引用本包目录树内文件**:产物经 Makefile 拷贝到 `internal/webui/dist` 再嵌入(§8.3);自写 SPA fallback handler | 外置静态目录 + nginx(违背单二进制) | | 数据访问 | 标准库 `database/sql` 参数化查询 | — | 无 ORM,6 张表手写 SQL 最透明 | GORM 等(依赖重、隐藏细节) | ### 3.2 前端选型 | 组件 | 选择 | 版本 | 理由 | 备选 | | --- | --- | --- | --- | --- | | 框架 | React | 19.2.8 | 生态最大、md 渲染/编辑器组件最丰富 | — | | 语言 | TypeScript | 7.x(不动则 5.9) | 原生编译器版已可用;若第三方库兼容有问题回退 5.9 | — | | 构建 | Vite | 8.2.2 | 事实标准;部署在域名根,`base` 用默认值 `'/'`(相对 base 与 SPA 深链冲突,见 §8.3 与审评报告 P0-1) | — | | 样式/组件 | Tailwind CSS 4 + **shadcn/ui** | 4.3.3 / CLI 4.21.0 | CSS-first 无样板配置;shadcn 复制式组件零运行时、克制美观,正合内容型博客;按需叠加 Radix 原语 | Ant Design 6(重、后台味浓)、Chakra 3、HeroUI、Radix Themes | | 路由 | react-router | 8.3.1 | library mode 成熟,博客+后台两个区足够 | TanStack Router 1.x(类型安全更强,但本项目路由简单) | | 数据请求 | TanStack Query | 5.102.8 | 十几 kB 换来统一 loading/缓存/重试,值得 | 裸 fetch + 自写 hook | | Markdown 渲染 | react-markdown + remark-gfm + rehype-sanitize + **rehype-highlight** | 10.1.0 / 4.0.1 / 6.0.0 / 7.0.2 | 组合标准;高亮选 rehype-highlight(**纯 class 输出、零内联样式**)而非 shiki(inline style),与严格 CSP 兼容 | shiki 4.4.3(准确度最高但产 inline style)、prism-react-renderer(近停更,不推荐) | | Markdown 编辑器 | **CodeMirror 6**(@uiw/react-codemirror + @codemirror/lang-markdown) | 4.25.11 / 6.5.2 | 源码编辑 + 分屏实时预览,零魔法、可预期;图片粘贴自写 handler(≤50 行) | Tiptap 3.31(体验派,WYSIWYG+成熟图片上传,文档全);MDXEditor(重);ByteMD(字节已弃坑)、Vditor 4(体积大、心智旧) | | 状态管理 | React Query + Context(**不引入** zustand) | — | 会话态 + 服务端缓存已覆盖;确需跨页 UI 态再加 zustand(≈1kB) | redux(不必要) | | 图标 | lucide-react | 1.42.0 | 活跃、风格统一、与 shadcn 默认匹配 | — | ### 3.3 关键决策记录(ADR 摘要) 1. **图片存 SQLite BLOB 而不是文件系统** —— 满足「只有一个数据文件」字面含义:备份=拷一个文件;事务一致性;杜绝路径穿越整类风险(文件永不落盘执行)。个人博客场景(单图 ≤5MB、总量 2GB 内)SQLite 毫无压力(理论上限 281TB)。>10MB 的图片一律拒绝引导压缩。 2. **SPA 而非 SSR** —— 管理端+浏览端共用 React 技术栈,极简;SEO 用「服务端渲染 index.html 元信息 + RSS + sitemap」弥补,元信息注入与内容 API 同一条可见性规则(§8.3)。「公开页 Go 模板 SSR」留作 v2 备选(见 §14)。 3. **HTTP 用标准库** —— Go 1.22+ ServeMux 已具备方法路由与 `{param}` 通配符;本项目 API 约 20 个路由,框架收益趋近于零,依赖面以「less is more」取胜。 4. **会话自建而非引库** —— 单管理员、单表、几十行代码,审计面最小;需求升级时切 scs 的成本极低。 5. **图片可见性用「引用表」+ 并集语义** —— 图片通过 `image_refs` 关联笔记,可见性 = 所有引用笔记可见性的**并集**(任一 public 引用即整体公开),并显式声明「已公开过的图片视为已分发,转私无法撤回已泄露副本」(§6.2、§9.1)。 6. **编辑器选 CodeMirror 6 路线** —— 与「极简健壮」一致:所见即所写,不需要维护富文本中间态;组件边界(MarkdownViewer 独立)保证日后可平替 Tiptap。 7. **回收站用软删除(`deleted_at` 列)而非快照表** —— 删除不改动 `image_refs`,可见性过滤统一加 `deleted_at IS NULL` 一个条件即可覆盖全部出口;恢复 = 清空该列,图片引用天然保全;`gc` 到期硬删时引用级联(§6.2)。 --- ## 4. 总体架构 ### 4.1 架构图 ``` 公网 │ HTTPS (Caddy 自动 TLS, 反代, 追加密名 XFF) ▼ ┌──────────────────────────┐ │ pn 单一二进制 (Go) │ │ │ │ net/http + ServeMux │ │ ├─ 公共 API /api/* │──┐ │ ├─ 管理 API /api/admin/*│ │ (匿名 or 会话) │ ├─ /feed.xml /sitemap │ │ │ └─ SPA (go:embed │ │ │ internal/webui/dist) │ │ │ │ │ 中间件链: │ │ │ SecurityHeaders → 日志 → 限流 → │ │ Origin 校验 → MaxBytes → Auth → CSRF → Handler │ │ └────────────┬─────────────┘ │ │ database/sql (SetMaxOpenConns=1) ▼ ┌──────────────────────────┐ │ data/pn.db (SQLite) │ │ notes(含 deleted_at) / │ │ images(BLOB) / image_refs /│ │ sessions / settings │ └──────────────────────────┘ 浏览器: React SPA (公开浏览 + 管理后台) ``` ### 4.2 数据流 **公开读路径**:访客浏览器 → `GET /api/notes/{slug}` → 服务端以 `status='public' AND deleted_at IS NULL`(或有效管理员会话)过滤 → 返回原始 Markdown + 元信息 → 前端 react-markdown 管线(GFM → rehype-sanitize 清洗 → highlight class 高亮)渲染。所有内容经 CSP 约束。 **管理写路径**:管理员登录(Argon2id 校验、限流、Origin 校验)→ 会话 Cookie(`__Host-` 前缀)+ CSRF Token(内存持有,刷新经 `/api/me` 重取)→ 编辑/上传(multipart 魔数校验 → BLOB 入库 + sha256 去重)→ 保存笔记时事务内重建 `image_refs` 引用。 **图片读取路径**:`GET /api/images/{id}` → 查 `image_refs` 关联笔记的可见性并集(任一 `public` 且未删除即可匿名访问,否则要求管理员会话)→ 按可见性返回不同缓存头(公开 `immutable` / 非公开 `no-store`)→ 未授权与不存在统一 404。 --- ## 5. 目录结构(Monorepo) ``` pure-note/ ├── go.mod / go.sum ├── Makefile ├── .gitignore # data/ web/node_modules/ web/dist/ internal/webui/dist/(仅保留 .gitkeep) ├── README.md ├── docs/ │ ├── design.md # 本文档(v1.1) │ └── review-round1.md # 第 1 轮审评报告(修订依据,留档) ├── cmd/ │ └── pn/ │ └── main.go # 入口 + 命令行(start/init/passwd/backup/gc/version) ├── internal/ │ ├── config/ # 环境变量/flag 解析 │ ├── store/ # SQLite 打开、迁移(user_version 上界校验)、DAO │ ├── httpapi/ # 路由注册、handler │ │ ├── public.go / admin.go / auth.go / image.go / feed.go │ ├── middleware/ # securityHeaders / rateLimit / originCheck / auth / csrf(自写) │ ├── auth/ # argon2id(PHC)、会话 token、密码校验 │ ├── markdown/ # goldmark 渲染(RSS/meta 用)+ bluemonday 清洗 │ └── webui/ # go:embed dist + SPA fallback + index.html 元信息注入 │ └── dist/ # 【生成目录】Makefile 自 web/dist 拷贝,.gitignore(仓库保留 .gitkeep 供 M0 编译) ├── web/ # React 前端(独立 Vite 工程) │ ├── package.json / vite.config.ts │ ├── index.html │ └── src/ │ ├── main.tsx / App.tsx / router.tsx │ ├── lib/ # api client、auth、csrf、格式化 │ ├── components/ # shadcn/ui 生成目录 + 业务组件 │ │ ├── markdown/ # MarkdownViewer(渲染管线,可替换) │ │ └── editor/ # Editor(CodeMirror)+ ImagePaste + Toolbar │ ├── features/ # notes / admin 业务 hooks │ └── pages/ # Home / Note / Tags / AdminLogin / AdminList / AdminTrash / AdminEdit └── deploy/ ├── pn.service # systemd 服务单元 ├── pn-maint.timer # 每日 gc + 备份 timer └── Caddyfile # 反代示例 ``` --- ## 6. 数据模型(SQLite) ### 6.1 Schema(DDL) ```sql -- 连接串(DSN)统一注入以下 pragma: -- file:data/pn.db?_pragma=journal_mode(WAL) -- &_pragma=busy_timeout(5000) -- &_pragma=foreign_keys(1) -- &_pragma=synchronous(NORMAL) CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL UNIQUE, -- URL 标识;首次保存定稿(§8.2) title TEXT NOT NULL, summary TEXT NOT NULL DEFAULT '', -- 列表页摘要,可空则截取正文 content TEXT NOT NULL DEFAULT '', -- Markdown 原文 status TEXT NOT NULL DEFAULT 'private' CHECK (status IN ('public','private')), tags TEXT NOT NULL DEFAULT '[]', -- JSON 字符串数组 pinned INTEGER NOT NULL DEFAULT 0, deleted_at INTEGER, -- NULL=正常;非空=回收站(软删除) created_at INTEGER NOT NULL, -- Unix 秒 updated_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_notes_public ON notes (status, pinned, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_notes_deleted ON notes (deleted_at); CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, sha256 TEXT NOT NULL UNIQUE, -- 内容哈希:去重 + ETag mime TEXT NOT NULL, size INTEGER NOT NULL, data BLOB NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS image_refs ( -- 笔记 ↔ 图片多对多 image_id INTEGER NOT NULL REFERENCES images(id) ON DELETE CASCADE, note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE, PRIMARY KEY (image_id, note_id) ); CREATE TABLE IF NOT EXISTS sessions ( token_hash TEXT PRIMARY KEY, -- SHA-256(token 明文),库里不存明文 csrf_token TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); -- 键白名单(§7.1 SettingsDTO 同源): -- admin_password_hash : PHC 串 $argon2id$v=19$m=19456,t=2,p=1$$ -- site_title / site_desc / page_size -- admin_password_hash 永不进入任何 API 响应、不可经 settings 接口写入(§9.3) ``` ### 6.2 关键策略 | 事项 | 策略 | | --- | --- | | 图片去重 | 上传按 `sha256` 查重,同图复用同一行 | | 图片可见性(并集语义) | 匿名可读 ⇔ `∃ ref → note.status='public' AND note.deleted_at IS NULL`;否则仅管理员。**可见性取所有引用笔记的并集**:同一字节的图一旦被任何公开笔记引用即整体公开;「已公开过的图片视为已分发,转私/删除无法撤回已被缓存、外链的副本」——按已知边界接受(§9.1-T10) | | 引用维护 | 保存笔记时在**同一事务**内:删除该笔记全部 refs → 用正则扫描 `content` 中的 `/api/images/(\d+)` → 重建 refs | | 回收站(软删除) | DELETE 仅置 `deleted_at`;refs 不动(恢复时图片天然保全);所有可见性过滤统一追加 `deleted_at IS NULL`;`gc` 将 `deleted_at < now−30天` 物理删除(级联 refs);回收期内 slug 仍被占用(保身份,见 §14) | | 会话 | 随机 256bit token(crypto/rand)→ Cookie 存 token 明文,库存 SHA-256;登录成功即重建行(防会话固定);轮换时 **csrf_token 保持不变**;启动时 + 每小时清理过期行 | | 迁移 | `PRAGMA user_version` 版本化,顺序执行内嵌迁移脚本;**仅追加式**(禁止删列/重命名/改类型);启动时校验 `user_version ≤ 代码支持的最高版本`,超出拒绝启动(`--allow-newer` 显式放行,§10.4) | | 并发 | `db.SetMaxOpenConns(1)` — 单写者串行化,WAL 下个人规模足够;规模上来后按「多读者连接池」扩展 | ### 6.3 数据清理(`pn gc`) 默认 `--dry-run` 只输出清理计划,`--commit` 才执行;systemd timer 每日调用一次(§10.3): 1. 物理删除 `deleted_at < now−30天` 的笔记(refs 级联); 2. 删除图片:0 引用 **且** `created_at < now−7天`(宽限期防止误删编辑中刚上传的图); 3. 删除过期会话。 --- ## 7. 后端设计 ### 7.1 HTTP API 一览 > 统一约定:JSON `{"data": ..., "error": {"code": "...", "message": "..."}}`;状态码语义化(400/401/403/404/409/413/429/500);分页参数 `page ∈ [1,10⁴]`、`page_size ∈ [1,100]`(默认 10),越界返回 400。 **公共(匿名 + 管理员)** | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/api/health` | 健康检查 | | GET | `/api/me` | 匿名 `{authenticated:false}`;已认证 `{authenticated:true, csrf_token}`(刷新后 CSRF token 重取点,§7.3) | | GET | `/api/notes` | 公开笔记列表(`status='public' AND deleted_at IS NULL`),`?page=&page_size=&tag=`,返回元信息(不含全文) | | GET | `/api/notes/{slug}` | 笔记详情:**`public` 或有效管理员会话**可读(响应含 `status` 字段供前端显示「私有预览」);其余统一 404 | | GET | `/api/tags` | 标签聚合(**仅统计公开且未删除的笔记**) | | GET | `/api/images/{id}` | 图片(并集可见性,§6.2);未授权与不存在**统一 404**(防 ID 枚举);缓存头按可见性分流(§7.4) | | GET | `/feed.xml` | RSS 2.0(仅公开;服务端 goldmark+bluemonday 渲染) | | GET | `/sitemap.xml` | 公开笔记 URL 列表 | | GET | `/robots.txt` | 允许全部 + sitemap 指向 | | GET | `/` 及所有非 API 路径 | SPA fallback → 内嵌 index.html(**仅对可见笔记**注入 title/desc/og meta,§8.3) | **管理(会话 + CSRF)** | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/api/auth/login` | `{password}`;Origin 校验 + 限流 + 常量时间比较;返回 CSRF token | | POST | `/api/auth/logout` | 删除会话行 | | GET | `/api/admin/notes` | 全部正常笔记(含私有,不含回收站) | | POST | `/api/admin/notes` | 新建;自动生成唯一 slug(自解冲突,§8.2) | | GET | `/api/admin/notes/{id}` | 单篇(含私有) | | PUT | `/api/admin/notes/{id}` | 更新(事务内重建 image_refs);slug 冲突返回 409 + 字段级错误 | | DELETE | `/api/admin/notes/{id}` | **软删除**:置 `deleted_at` 进入回收站 | | GET | `/api/admin/trash` | 回收站列表 | | POST | `/api/admin/trash/{id}/restore` | 恢复:清空 `deleted_at`(slug 仍被自身占用,无冲突;外部占用则在 30 天内不可能,见 §6.2) | | POST | `/api/admin/images` | multipart 上传 → `{id, url}`;≤5MB | | GET | `/api/admin/images?orphan=1` | 0 引用图片清单(删除前检视,实际清除由 gc 执行) | | GET/PUT | `/api/admin/settings` | **SettingsDTO 白名单**(site_title/site_desc/page_size),读永不序列化 `admin_password_hash`、写只收白名单键 | | POST | `/api/admin/password` | `{old_password, new_password}`;校验旧密码(常量时间)、新密码 ≥12 字符;复用登录限流 | ### 7.2 中间件链与服务器参数 ``` SecurityHeaders(含 HSTS,§9.2) → slog 请求日志(仅错误 + 管理操作,登录失败记 IP 与桶计数) → 全局限流(宽松令牌桶;per-IP 桶上限 4096 条 + 10 分钟 TTL 逐出,防伪造 IP 撑爆内存) → Origin/Referer→Host 校验(**所有** 非 GET/HEAD/OPTIONS 请求,含 /api/auth/*,§9.1-T2) → 路由级 MaxBytesReader(auth/settings ≤64KB;notes JSON ≤1MB;multipart ≤6MB 含边界开销) → 路由 ├─ 公共路由: handler 内 status='public' AND deleted_at IS NULL 过滤 ├─ /api/auth/login: 严格限流(per-IP 10次/5分钟 + per-账号 5次/10分钟,429 + Retry-After) └─ /api/admin/*: 会话校验(401) → CSRF 头校验(403) → handler;响应统一 Cache-Control: no-store ``` `http.Server`:`ReadHeaderTimeout: 5s, ReadTimeout: 120s, WriteTimeout: 120s, IdleTimeout: 120s`(slowloris 与超大 body 防线)。优雅停机:SIGTERM/SIGINT → 停止接受新连接 → `Shutdown(ctx 10s)` 排空在途请求 → 关闭 SQLite 连接(§10.5)。 ### 7.3 认证与会话 1. **初始化**:`pn init`(交互式设密码,或环境变量 `PN_ADMIN_PASSWORD` 非交互)→ Argon2id(m=19456, t=2, p=1, salt=16B 随机, keyLen=32B) **以 PHC 串 `$argon2id$v=19$m=19456,t=2,p=1$$` 存库**——参数随哈希走,未来调参可继续校验旧口令; 2. **登录**:Origin/Referer→Host 校验通过后,per-IP 10 次/5 分钟 + 账号维度 5 次/10 分钟令牌桶(只计失败尝试);校验用 `subtle.ConstantTimeCompare`;失败统一返回 401「用户名或密码错误」不泄露差异; 3. **会话 Cookie**:`__Host-pn_session` = 256bit 随机 token;属性 `Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=7d`(`__Host-` 前缀自带 Secure + 根路径约束); 4. **CSRF**:会话行内存随机 `csrf_token`,登录响应下发、前端仅存内存并附到所有 `/api/admin/*` 变更请求的 `X-CSRF-Token` 头;**刷新页面后经 `GET /api/me` 重取**;明确禁止任何 localStorage 持久化;服务端同时校验 Origin/Referer 与 Host 匹配; 5. **轮换**:活跃访问距过期 < 3 天时重建会话行、旋转 Cookie token,**csrf_token 保持不变**(前端无感); 6. **登出**:删除会话行; 7. **改密**:`POST /api/admin/password` 校验旧密码后写新 PHC 哈希;改密不失效当前会话(可选:改密后删除其他会话——单管理员场景仅本会话,不强制); 8. **口令重置(CLI)**:`pn passwd` 免旧口令直接覆盖哈希并**吊销全部会话**——前提是具备服务器访问权限(读写数据目录即可改库,CLI 提供规范入口优于手工改库);口令来源与 `init` 一致,避免出现在 argv; 9. **开发模式**:`--dev` 允许非 Secure Cookie(否则 `__Host-` 在 `http://局域网IP` 下无法登录);**强制约束:仅当监听地址为 loopback 时允许启动,防止误部署到生产**。 ### 7.4 图片上传与访问 1. 上传:`Content-Type` 白名单 + **魔数字节校验**(PNG/JPEG/WebP/GIF 签名,不信任客户端声明;**SVG 一律拒绝**——携带脚本风险)→ ≤5MB → 读入内存计算 sha256 → `INSERT OR IGNORE` 去重 → 返回 URL;图片**永不落盘**,杜绝 WebShell 执行面; 2. 响应头按可见性分流: - **公开图**:`Content-Type`(入库 mime)、`X-Content-Type-Options: nosniff`、`ETag: ""`、`Cache-Control: public, max-age=31536000, immutable`; - **非公开图**(仅私有笔记引用/孤儿/仅管理员会话):`Cache-Control: private, no-store`,**每次请求重新执行可见性判定**; - **未授权与不存在统一 404**(防自增 ID 枚举); 3. 可选增强(P2):EXIF/元数据剥离。 ### 7.5 Markdown 服务端职责 - **RSS / meta 渲染**:goldmark(GFM 扩展、`html.WithUnsafe=false` 保持转义)→ bluemonday 白名单清洗 → 摘要截断; - **不把 HTML 存库**:库中始终只存 Markdown 原文,渲染确定性交给前端管线,安全策略(清洗规则)升级时新旧笔记全部收益。 ### 7.6 配置与命令行 | 子命令 / 开关 | 说明 | | --- | --- | | `pn start` | 启动;`--addr :8080`、`--data-dir`(默认 `./data`)、`--log-level`、`--log-format text|json`、`--behind-proxy`(声明位于可信反代之后,启用 XFF 处理)、`--dev`(loopback-only,§7.3-8)、`--allow-newer`(跳过 schema 版本上界校验,§10.4);环境变量:`PN_ADMIN_PASSWORD` | | `pn init` | 首次初始化:设口令(Argon2id+PHC)、站点标题 | | `pn passwd` | 重设管理员口令(覆盖旧哈希)并吊销全部会话;口令来源同 `init`(环境变量 `PN_ADMIN_PASSWORD` 或交互输入,不经 argv 避免 ps 泄露)。CLI 可达即具备服务器权限,允许直接重设 | | `pn backup [path]` | 在线备份:`VACUUM INTO`(一致快照,不停服);默认输出 0600 权限 | | `pn gc` | 回收站过期清除 + 孤儿图片清除 + 会话清理;**默认 `--dry-run`**,`--commit` 才执行(§6.3) | | `pn version` | 版本号(build 时注入 commit/时间);亦支持 `-v` / `--version` | --- ## 8. 前端设计 ### 8.1 页面与路由 | 路由 | 页面 | 说明 | | --- | --- | --- | | `/` | 博客首页 | 公开笔记列表(置顶优先、分页)、标签云、站点标题 | | `/notes/:slug` | 笔记详情 | Markdown 渲染、面包屑、上一篇/下一篇;响应 `status=private` 且为管理员时显示「私有预览」横幅 | | `/tags/:tag` | 标签页 | 该标签下公开笔记 | | `/admin/login` | 登录页 | 口令登录、防爆破提示 | | `/admin` | 管理列表 | 全部笔记(公/私标签可见)、状态开关、新建、删除(入回收站) | | `/admin/trash` | 回收站 | 已删列表、恢复 | | `/admin/notes/new` `/admin/notes/:id/edit` | 编辑器 | CodeMirror 源码编辑 + 实时预览(分屏/切换)、元信息侧栏(标题/slug/标签/摘要/公开开关/置顶)、图片粘贴上传 | | `*` | 404 | 统一兜底 | ### 8.2 核心技术要点 **Markdown 渲染管线(浏览态)** ``` Markdown 原文 → react-markdown 10 (remark-gfm: 表格/任务列表/删除线/自动链接) → rehype-sanitize 6(白名单 schema:禁 script/iframe/事件属性/未知协议/style 属性, 允许任务列表 checkbox 所需 input[type=checkbox disabled checked], 加 class 白名单供高亮使用,链接强制 target=_blank + rel="nofollow noopener noreferrer") → rehype-highlight 7(纯 class 高亮,主题 CSS 静态打包,零内联样式) → 渲染(@tailwindcss/typography prose 排版 + 自定义 CodeBlock 组件带复制按钮) ``` 两处防线:链路内的 rehype-sanitize,加上服务端 goldmark 转义与 CSP `script-src 'self'`,即使清洗层被绕过也无法执行脚本。 **编辑器(编辑态)** - CodeMirror 6 + `@codemirror/lang-markdown`,`minimalSetup` + 行号 + 等宽字体;工具栏提供加粗/斜体/链接/代码/表格等 Markdown 辅助插入; - **图片粘贴/拖拽**:自写 paste/drop handler(≤50 行)→ `POST /api/admin/images` → 光标处插入 `![描述](/api/images/{id})`; - **slug 策略**:首次保存时由标题自动生成——中文标题退化为 `post-`,与现有记录(含回收站)冲突时追加 `-2`/`-3` 直至唯一;**首次保存即定稿,此后除非用户手动修改否则永不变化**;用户手改与他人冲突 → 服务端 409 + 字段级错误提示,前端就地高亮; - 自动保存:防抖 2s;大改动前本地兜底(失败时保留编辑态不丢内容)。 **数据与状态** - TanStack Query 管理全部服务端状态(列表/详情缓存 key 按 status 上下文隔离); - 会话状态 `useAuth`(Context + `/api/me`);**CSRF token 仅存内存,刷新后经 `/api/me` 重取,禁止 localStorage**; - 登录态失效(401)统一跳转 `/admin/login`。 **暗色主题** Tailwind v4 `@custom-variant dark` + 根元素 class 切换 + localStorage 持久化 + 跟随系统选项。 ### 8.3 构建与嵌入约定 1. **产物位置与 embed 机制**(审评 P0-2 修正):`go:embed` 不能跨包引用,构建链为:Vite 输出 `web/dist` → Makefile `sync-assets` 目标拷贝至 `internal/webui/dist` → `go:embed dist/*` + `fs.Sub` 在 `internal/webui` 包内嵌入;`internal/webui/dist` 进 .gitignore(保留 `.gitkeep` 占位供 M0 编译); 2. **Vite base**(审评 P0-1 修正):部署在域名根,使用**默认 `base: '/'`**。⚠️ 相对 base(`'./'`)会在 `/notes/:slug` 等深链下把资源解析到 `/notes/assets/…` 导致白屏,**禁止使用**;embed handler 从站点根提供 FS,绝对路径恰好正确命中; 3. **SPA fallback**:`/api/*` 之外命中文件则返回,未命中(SPA 路由)回退 `index.html`; 4. **元信息注入**(审评 P0-4 修正):请求 `/notes/{slug}` 时服务端查询该笔记,**仅当可见(`public` 或管理员会话)**才注入 `/<meta description>/<meta og:*>`,否则回退站点默认 meta;注入经 `html/template` 自动转义,防标题内容打断标签结构; 5. **缓存与指纹**:构建产物内容 hash 文件名 + 长缓存头(嵌入二进制,指纹天然隔离新旧版本); 6. **冒烟测试守门**(进 CI):真实构建 → 启动二进制 → 请求任一公开 slug 页面 → 断言 script/link 以 200 + 正确 MIME 加载(防「深链白屏」类回归)。 --- ## 9. 安全设计 ### 9.1 威胁模型与防护矩阵 | # | 威胁 | 防护措施 | 纵深 | | --- | --- | --- | --- | | T1 | 存储型 XSS(恶意 Markdown/HTML/链接) | goldmark 默认转义(服务端渲染路径);渲染链路 rehype-sanitize 白名单(schema 禁 style 属性);链接协议白名单;CSP `script-src 'self'` 兜底 | 三层 | | T2 | CSRF | 双保险:CSRF token 头校验 + **全 POST 覆盖(含 /api/auth/*)**的 Origin/Referer→Host 校验 + SameSite=Lax;login 的 Origin 校验同时防「跨站表单预热消耗令牌桶把管理员锁在门外」的 DoS 放大 | 三重 | | T3 | 会话劫持/固定 | crypto/rand 256bit token;DB 存哈希;`__Host-` Secure HttpOnly Cookie;登录重建会话;登出删行;7 天过期 + 滑动续期(轮换保持 csrf 不变) | — | | T4 | 口令爆破 | Argon2id(OWASP 参数);per-IP + per-账号令牌桶限流(fail-only);常量时间比较;统一 401 文案;改密复用同一套限流 | — | | T5 | 恶意上传(多态/WebShell) | MIME 白名单 + 魔数校验 + 拒绝 SVG;BLOB 入库存不进文件系统(无执行环境);禁用路径穿越;nosniff;CSP `img-src` 限制;≤5MB | — | | T6 | SQL 注入 | 全部参数化查询;无拼接;无 ORM 动态链 | — | | T7 | 路径穿越/静态文件越界 | 仅从 embed FS 提供文件;无用户可控路径参数 | — | | T8 | 点击劫持 | `Content-Security-Policy: frame-ancestors 'none'` + `X-Frame-Options: DENY` | — | | T9 | MIME 嗅探 | `X-Content-Type-Options: nosniff` 全局 | — | | T10 | 私密笔记越权泄露 | 可见性在**服务端查询层**统一过滤(`status='public' AND deleted_at IS NULL`,覆盖列表/详情/标签/RSS/sitemap/meta 注入/图片直链);图片按并集语义判定;前端标识仅供参考。**已知边界(显式接受)**:图片一旦随公开笔记暴露,已分发副本(代理缓存/爬虫/RSS 阅读器)无法撤回 | 单一可信点 | | T11 | 传输层窃听 | 仅 HTTPS(Caddy 自动证书);`Strict-Transport-Security`(§9.2) | — | | T12 | 依赖供应链漏洞 | Go 依赖面 5 个直接依赖 + govulncheck CI;npm 锁文件 + `npm audit`;定期 dependabot/renovate | — | | T13 | 数据丢失 | WAL;每日 `VACUUM INTO` 备份(0600 权限)+ 异地加密(rclone crypt/age);回收站 30 天缓冲;备份保留期内数据物理仍在(语义已声明,§10.3) | — | ### 9.2 安全 HTTP 头(SecurityHeaders 中间件) ``` Content-Security-Policy: default-src 'self'; script-src 'self'; ← 严格禁 inline script style-src 'self' 'unsafe-inline'; ← 原因仅一项:CodeMirror 经 style-mod 运行时注入 <style>(rehype-highlight 为纯 class,不注入样式, 不构成理由)。硬化路径(P2):Go 每响应下发 nonce 注入 CSP + 前端 EditorView.cspNonce 传入, 即可收紧为 style-src 'self' 'nonce-…' img-src 'self' data:; ← 图片仅本站 + data(粘贴预览) font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; Strict-Transport-Security: max-age=31536000; includeSubDomains Referrer-Policy: strict-origin-when-cross-origin X-Content-Type-Options: nosniff X-Frame-Options: DENY Permissions-Policy: camera=(), microphone=(), geolocation=() ``` 补充约定:`/api/admin/*` 与 `/api/auth/*` 响应统一 `Cache-Control: no-store`(防登出后 bfcache/返回键回看);公共路由(无编辑器页面)可单独下发更严格 CSP `style-src 'self'`。 ### 9.3 上线前安全检查单 - [ ] go vet + `govulncheck ./...` 干净;`npm audit --production` 干净 - [ ] **可见性矩阵自动化用例全绿**(§13),并人工抽查:私有笔记在列表/详情/标签/RSS/sitemap/图片直链/HTML meta 全部出口匿名不可见 - [ ] 投递恶意 Markdown(`<script>`、`javascript:` 链接、事件属性、`<iframe>`、可疑 style)渲染无脚本执行 - [ ] 跨站伪造 POST(含 login/logout)被拒绝;Cookie 属性与 `__Host-` 前缀验证;**刷新页面后 CSRF token 经 `/api/me` 重取可用** - [ ] 上传伪造扩展名的可执行文件/SVG/超限文件被拒 - [ ] 登录接口连续爆破触发 429;登录失败日志含 IP - [ ] `/api/admin/settings` 响应**不含** admin_password_hash;PUT 无法写入非白名单键;改密流程通过 - [ ] 图片访问:私有与不存在统一 404;`curl -I` 对比公开图(immutable)与非公开图(no-store)缓存头 - [ ] 安全头逐项用 curl/浏览器 DevTools 确认(含 HSTS) - [ ] 备份还原演练:停服 → 删除 `-wal`/`-shm` → 替换 db → 恢复成功;备份文件权限 0600 - [ ] 回收站恢复用例通过;`gc --dry-run` 输出审阅后 `--commit`,且不含 7 天内孤儿图片 - [ ] user_version 越界启动被拒(`--allow-newer` 显式放行) - [ ] **构建冒烟测试**通过(§8.3-6) - [ ] Caddy 仅开放 80/443,程序监听 127.0.0.1 --- ## 10. 部署与运维 ### 10.1 构建 ```makefile web: cd web && npm ci && npm run build # go:embed 不能跨包引用,必须把产物拷进 internal/webui 包目录 sync-assets: rm -rf internal/webui/dist && cp -r web/dist internal/webui/dist build: web sync-assets CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \ -o pn ./cmd/pn linux: web sync-assets CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \ -ldflags="-s -w" -o pn-linux-amd64 ./cmd/pn dev: # 双端热重载:air(Go)+ vite dev(proxy /api → 127.0.0.1:8080)+ start --dev air & cd web && npm run dev ``` ### 10.2 运行 ```ini # deploy/pn.service [Unit] Description=Pure Note After=network.target [Service] User=purenote WorkingDirectory=/opt/pure-note ExecStart=/opt/pure-note/pn start --addr 127.0.0.1:8080 \ --data-dir /opt/pure-note/data --behind-proxy Restart=on-failure NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ReadWritePaths=/opt/pure-note/data MemoryDenyWriteExecute=true [Install] WantedBy=multi-user.target ``` ```caddyfile # deploy/Caddyfile example.com { encode zstd gzip reverse_proxy 127.0.0.1:8080 } ``` > 幂等启动:`start` 时若 settings 无口令哈希则拒绝启动并提示先跑 `init`;`user_version` 越界同样拒绝(§10.4)。 ### 10.3 备份与恢复 **调度(systemd timer,替代 root cron)** ```ini # deploy/pn-maint.timer [Unit] Description=Daily Pure Note maintenance (gc + backup) [Timer] OnCalendar=03:00 Persistent=true [Install] WantedBy=timers.target ``` timer 触发 `pn-maint.service`:先 `pn gc --commit`,后 `pn backup /backup/pn-$(date +\%F).db`。 **要点** - 备份进程独立于服务进程运行,不受 unit 沙箱约束;`VACUUM INTO` 在 WAL 下与 start 并发安全(在线一致快照); - `/backup` 目录属主 `purenote`、权限 0700;备份文件**chmod 0600**(内含全部私有笔记、图片、口令哈希、会话哈希的明文副本); - 保留 30 天;**语义声明**:备份保留期内被删除的数据物理上仍存在于备份中; - 异地同步(可选):`rclone` 远端启用 **crypt**(或先 `age` 加密再上传),禁止明文上云; - **恢复 SOP**:停服 → 删除 `data/pn.db-wal` 与 `-shm`(残留会污染还原)→ 以备份文件替换 `pn.db` → 起服。 ### 10.4 升级 1. **升级前第一步固定为 `pn backup`**(写入 SOP); 2. 停服 → 替换二进制 → 起服触发迁移:迁移仅追加式(不删列/重命名/改类型),`user_version` 顺序递增; 3. **启动守卫**:代码校验 `user_version ≤ 本版本支持的最高版本`,超出**拒绝启动**并提示,`--allow-newer` 显式放行(防止旧二进制静默在陌生 schema 上读写); 4. 回滚:若迁移尚未执行(版本一致)可直接换回旧二进制;若已执行且不满足追加式约束,回滚 = 从步骤 1 的备份还原数据文件后再换回旧二进制。 ### 10.5 观测性与开发工作流 - **日志**:标准库 `log/slog`;`--log-format text`(journald 友好)或 `json`;登录失败记 IP 与桶剩余计数;管理操作(增删改)记审计条目;不做纯请求量日志; - **优雅停机**:SIGTERM/SIGINT → `http.Server.Shutdown`(10s 排空)→ SQLite 连接关闭;systemd `ExecStop` 无需特殊配置; - **本地开发**:`make dev` 双进程——Go 侧 air 热重载,前端 `vite dev` 配置 `server.proxy` 把 `/api` 代理到 `127.0.0.1:8080`(**同源是 cookie/CSRF 成立的前提**);Go 侧 `--dev` 放开非 Secure Cookie(loopback-only 守卫)。 --- ## 11. 性能与规模边界 | 维度 | 边界与说明 | | --- | --- | | 笔记量 | 1 万篇内无压力;列表查询走 `idx_notes_public` 复合索引 | | 图片量 | 总 BLOB < 2GB 从容;单图 ≤5MB;公开图 `immutable` 缓存使图片仅首次请求走 DB | | 并发 | `SetMaxOpenConns(1)` 串行写 + WAL;个人博客日流量千级 PV 足够;瓶颈先出现在带宽而非后端 | | 多实例 | **不支持**(内存限流、单库设计);需要时迁移 scs + 共享存储,属新架构决策 | | 限流器内存 | per-IP 桶上限 4096 条 + TTL 逐出(防伪造 XFF 撑爆内存,§14) | | SQLite 通用边界 | 单机单进程;不适用于写入密集型多写者场景(本项目读多写极少,完美契合) | --- ## 12. 开发路线图 | 里程碑 | 内容 | 测试/验收门 | | --- | --- | --- | | M0 骨架 | Go 模块 + SQLite 迁移(user_version 守卫)+ 配置子命令 + 中间件链 + embed 构链(拷贝 + 占位 dist)+ `make build`/`make dev` | `make build` 产出可运行二进制;`start` 起服务;`/api/health` 200 | | M1 公开浏览 | 笔记/标签 API + React 博客三页 + Markdown 渲染管线 + RSS/sitemap + 元信息注入(可见性规则) | 可见性矩阵**公开侧**用例起步;**构建冒烟测试**入 CI | | M2 管理端 | 登录/会话/CSRF(`/api/me` 重取)+ 笔记 CRUD + 回收站 + 图片上传(魔数/缓存头分流)+ 编辑器 + 改密 | **表驱动可见性矩阵完整**(§13)+ 限流/CSRF/魔数/回收站用例**随功能同 PR 交付** | | M3 安全加固 | 全量 §9.3 检查单、CSP 确认(记录 nonce 硬化待办)、CI 齐备(vet+vulncheck+测试+冒烟) | §9.3 全项通过 | | M4 运维 | systemd(服务 + maint timer)、升级演练、hey 压测、备份恢复演练 | 按 §10.3 恢复 SOP 演练一次真还原 | 预估工作量:单人全职约 2–3 周(含安全项验证)。 --- ## 13. 测试策略 **核心不变量 = 可见性/越权矩阵**,表驱动自动化,随功能交付(M2 起)并长期进 CI: | 维度 | 取值 | | --- | --- | | 主体 | `{匿名, 管理员会话}` | | 笔记状态 | `{public, private, trashed(回收站中), 不存在}` | | 出口 | `{列表 /api/notes, 详情 /api/notes/{slug}, 标签 /api/tags, 图片 /api/images/{id}, RSS /feed.xml, sitemap, HTML meta 注入, 管理接口权限}` | 判定规则(全部断言为「匿名不可见 ⇒ 404 或输出中不含」): - 匿名:仅 `public`(且未删除)可见,其余一律 404/不含;图片按并集语义; - 管理员:全部正常笔记可见;回收站仅经 `/api/admin/trash`; - 图片缓存头:公开图 immutable / 非公开 no-store / 未授权与不存在统一 404。 **其他测试组**(Go 集成测试:httptest + 临时目录真实 SQLite): | 组 | 用例要点 | | --- | --- | | 迁移 | 空库 → 最新版本幂等;user_version 越界拒绝启动(`--allow-newer` 放行) | | 认证会话 | 登录成功/失败(401 统一文案)、限流 429、改密(旧错拒绝/新弱拒绝/成功)、轮换后 CSRF 不变、`/api/me` 匿名与认证态 | | CSRF | 无 token 403、错 token 403、跨源 Origin 拒绝(含 login/logout) | | 上传 | 魔数不符/伪造扩展/SVG/超限/超时大 body 各拒绝;去重命中 | | 回收站 | 删除→列表不可见→恢复→可见;30 天过期被 gc 清除;恢复后 refs 完整 | | gc | dry-run 不改数据;7 天内孤儿图片不被删;会话过期清理 | | slug | 同日冲突自动后缀;手改冲突 409 | | 元信息注入 | 公开 slug 注入该笔记 meta;私有 slug(匿名)回退站点默认 meta | 前端测试(vitest):sanitize schema 快照(禁 style/script/iframe、允许任务列表 checkbox)、渲染管线对恶意 Markdown 的冒烟用例。 --- ## 14. 风险与备选方案 | 风险 / 权衡 | 应对 / 备选 | | --- | --- | | 图片「已公开即视为已分发」,转私无法撤回已泄露副本 | 已在 §6.2/§9.1 声明为接受边界;私密图片请勿与公开笔记共用同一文件(去重会合并)——或在编辑器内提示 | | SPA 对搜索引擎弱 | 已建 RSS + sitemap + 服务端 meta 注入(带可见性规则);公开页 Go 模板 SSR 是 v2 备选(保留 API 纯净性,切换成本可控) | | 软删除占用 slug 最多 30 天(保恢复身份) | 可接受:个人站点频道创建频率低;如需立即释放,先在回收站手动「立即清除」(v1.1+ 可选) | | goldmark v1 与 v2 分叉 | 初始锁定 v1.8.x(成熟);v2.0.1 稳定后评估一次性升级 | | TypeScript 7 生态兼容面 | 出现第三方库不兼容即回退 TS 5.9,无架构影响 | | 编辑器路线摇摆 | `MarkdownViewer` 组件边界独立;若日后要 WYSIWYG,仅替换 Editor(Tiptap 3 备选),数据层不变 | | SQLite BLOB 膨胀 | 去重 + 上传上限 + gc 清孤儿;失控时图片降级为「文件系统存储 + DB 存路径」(预留 store 接口) | | 自建会话的实现风险 | 实现面小(单表 + 标准 crypto,见 §13 测试组),并有 scs v2.9 作备选平替 | | 反代之后限流 IP 识别 | 唯一可信代理 = Caddy(`--behind-proxy` 声明);取 `X-Forwarded-For` **最右**一个条目(Caddy 追加),或让 Caddy 覆盖 XFF;桶有上限与 TTL | | CSP `style-src 'unsafe-inline'` 残余面 | v1 保留但注释修正;硬化路径:Go 每响应 nonce + `EditorView.cspNonce` 收紧为 `'nonce-…'`(§9.2),列为 M3+ 待办 | | 构建链(嵌入/深链白屏)回归 | §8.3-6 冒烟测试进 CI,从 M1 起持续守护 | --- ## 附录 A:依赖清单汇总(2026-09 核实) **Go(直接依赖,共 5)** | 依赖 | 版本 | 用途 | | --- | --- | --- | | modernc.org/sqlite | v1.58.0 | 纯 Go SQLite 驱动(无 CGO) | | github.com/yuin/goldmark | v1.8.6 | 服务端 Markdown 渲染(RSS/meta) | | github.com/microcosm-cc/bluemonday | v1.0.27 | 服务端 HTML 白名单清洗 | | golang.org/x/crypto | v0.56.0 | Argon2id | | golang.org/x/time | v0.15.0 | 登录/改密限流 | **前端(核心,devDependencies 另计)** | 依赖 | 版本 | 用途 | | --- | --- | --- | | react / react-dom | 19.2.8 | UI 框架 | | vite / @vitejs/plugin-react | 8.2.2 | 构建(base 默认 `'/'`) | | tailwindcss + @tailwindcss/vite + @tailwindcss/typography | 4.3.3 | 样式体系 | | shadcn/ui(CLI)+ Radix primitives | 4.21.0 | 组件库 | | react-router | 8.3.1 | 路由 | | @tanstack/react-query | 5.102.8 | 服务端状态 | | react-markdown + remark-gfm + rehype-sanitize + rehype-highlight | 10.1.0 / 4.0.1 / 6.0.0 / 7.0.2 | Markdown 渲染与防 XSS | | @uiw/react-codemirror + @codemirror/lang-markdown | 4.25.11 / 6.5.2 | 编辑器 | | lucide-react | 1.42.0 | 图标 | **开发工具(建议)**:air(Go 热重载);vitest(前端测试) **主要参考链接** - https://go.dev/dl/ · https://pkg.go.dev/modernc.org/sqlite - https://github.com/yuin/goldmark · https://github.com/microcosm-cc/bluemonday - https://github.com/facebook/react · https://github.com/vitejs/vite - https://github.com/shadcn-ui/ui · https://github.com/remix-run/react-router - https://github.com/remarkjs/react-markdown · https://github.com/rehypejs/rehype-sanitize - https://github.com/ueberdosis/tiptap · https://github.com/uiwjs/react-codemirror > 实际开发时以 `go mod tidy` / `npm install` 解析到的版本为准,锁定文件(go.sum / package-lock.json)入库。