awesome/docs/ARCHITECTURE.md
cheney dec103546e feat: awesome index 全功能实现
- M1-M2: 数据层与核心API(DuckDB, entries/tags/comments/search服务)
- M3: SSR页面(EJS模板,接真实数据)
- M4: 管理后台(登录/用户/内容源/AI ingest)
- M5: 采集层(scheduler+rss/html/eryajf适配器)
- M6: MCP只读接口(Streamable HTTP, 4工具)
- M7: Docker部署(Dockerfile+docker-compose)
- 测试: 10个用例全部通过
- 修复: DuckDB连接参数绑定、状态机迁移、标签树操作
2026-08-28 09:32:48 +08:00

17 KiB
Raw Blame History

Awesome Index · 框架设计

目标:把 design/prototype/ 的静态原型落地为一个由 DuckDB 驱动、全 JavaScript 编写、 对人与对 AIMCP同时提供服务、可 Docker 部署的小型数据库网站。 本文档回答三个问题:模块怎么分、每个模块负责什么、模块对应哪个目录。


1. 总体架构

经典三横层,外加一个并列的机器读者入口:

                 ┌──────────────────────────────────────────┐
   人类读者 ────▶ │ 视图层 views/ (EJS SSR)  +  public/ 静态   │
                 │ 页面内 <script> 直接 fetch('/api/…') 混入  │
                 └───────────────┬──────────────────────────┘
                                 │
   AI 读者 ─────▶ ┌──────────────┴──────────────────────────┐
   (MCP)         │        接口层 routes/HTTP + MCP        │
                 │  参数校验(zod) → 调用 service → 统一响应    │
                 └───────────────┬──────────────────────────┘
                                 │
   外部站点 ────▶ ┌──────────────┴──────────────────────────┐
   周刊/RSS      │      采集层 ingest/(定时拉取与整理)        │
                 │  scheduler → adapter 解析 → 去重 → 入库     │
                 └───────────────┬──────────────────────────┘
                                 │
                 ┌───────────────┴──────────────────────────┐
                 │   业务服务层 services/(唯一持有 SQL 处)     │
                 │   条目 / 标签树 / 评论 / 账号 / 内容源 / 搜索 │
                 └───────────────┬──────────────────────────┘
                                 │
                 ┌───────────────┴──────────────────────────┐
                 │        数据层 db/DuckDB 单文件库)         │
                 │        data/awesome.duckdb                │
                 └──────────────────────────────────────────┘

关键技术决策

决策点 选择 理由
运行时 Node.js 22 LTS + ESM 全站 JavaScript 要求22 为当前 LTS
Web 框架 Express 5 成熟、薄、够用;不引入大而全框架
数据库 DuckDB@duckdb/node-api 官方驱动) 选型既定;单文件库,随 volume 持久化
ORM 不引入services 内手写参数化 SQL 规模小避免多一层抽象SQL 可审查
模板 EJS 服务端渲染骨架 页面可直接输出后端数据(「混入后台调用」的另一半),渐进增强
校验 zod 路由层统一输入校验
会话 express-session默认 MemoryStore 单实例部署,无需外部存储
MCP @modelcontextprotocol/sdkStreamable HTTP 挂在 /mcp,只读,复用 service 层
测试 node:test + 临时 DuckDB 文件 零额外测试框架依赖

2. 目录结构

awesome/
├── package.json               # 依赖与 scriptsdev / start / test / migrate
├── Dockerfile                 # 多阶段构建node:22-alpine
├── docker-compose.yml         # 单服务编排 + data 卷 + healthcheck
├── .env.example               # 配置样例
├── .gitignore                 # node_modules / data / .env
│
├── src/
│   ├── index.js               # 【入口】读取配置 → 连库 → 迁移 → 起 HTTP 服务
│   ├── app.js                 # 【装配】Express 实例:中间件顺序 + 挂载全部路由
│   ├── config.js              # 【配置】环境变量集中读取与默认值
│   │
│   ├── db/                    # ── 数据层 ──
│   │   ├── connection.js      #   DuckDB 连接单例(含初始化与查询助手)
│   │   ├── schema.sql         #   全部建表 DDL唯一 schema 事实来源)
│   │   └── migrate.js         #   启动时执行 DDL + 幂等种子数据(初始管理员等)
│   │
│   ├── routes/                # ── 接口层(薄:校验 → service → 响应)──
│   │   ├── pages.routes.js    #   页面路由GET 首页/列表/详情/登录/管理台壳
│   │   ├── entries.routes.js  #   /api/entries…搜索、随机、详情、增改灰删
│   │   ├── tags.routes.js     #   /api/tags…树读取、新增、移动、删除
│   │   ├── comments.routes.js #   /api/comments…发布、列表、删除
│   │   ├── admin.routes.js    #   /api/admin…账号 CRUD、内容源开关、AI 录入入口)
│   │   └── mcp.routes.js      #   /mcp 与 /healthz 的挂载转发
│   │
│   ├── services/              # ── 业务服务层(核心规则所在,唯一写 SQL 处)──
│   │   ├── entry.service.js   #   条目 CRUD、状态机、核验时间
│   │   ├── tag.service.js     #   标签树:增删、移动(防环)、子级上移继承
│   │   ├── comment.service.js #   评论发布/删除
│   │   ├── user.service.js    #   账号与角色admin / editor
│   │   ├── source.service.js  #   内容源注册表human / ai / feed 三类源的配置与策略
│   │   └── search.service.js  #   搜索DuckDB FTS、过滤组合、random_entry
│   │
│   ├── ingest/                # ── 采集层:自动化内容的来源(拉取与整理)──
│   │   ├── scheduler.js       #   定时器:按每条 feed 源的 cron 计划触发 pipeline
│   │   ├── pipeline.js        #   单次采集编排fetch → 解析 → 去重 → 入库 → 运行记录
│   │   └── adapters/          #   解析适配器(可插拔,一个外部站点一个文件)
│   │       ├── rss.adapter.js           # 通用 RSS/Atom多数博客周刊直接可用
│   │       ├── html.adapter.js          # 通用网页CSS 选择器规则配置化抽取
│   │       └── eryajf-weekly.adapter.js # 站点专用wiki.eryajf.net 学习周刊逐期解析
│   │
│   ├── mcp/                   # ── 机器读者入口 ──
│   │   ├── server.js          #   SDK Server + StreamableHTTPServerTransport 装配
│   │   └── tools.js           #   search_entries / get_entry / random_entry / list_tags
│   │                          #   (工具实现 = 直接调用 services只读
│   │
│   ├── middleware/
│   │   ├── auth.js            #   requireLogin / requireRole('admin')AI Key 校验
│   │   └── errors.js          #   404 兜底 + 统一错误处理器 + 请求日志
│   │
│   └── views/                 # ── 视图(由原型平移改造)──
│       ├── layouts/base.ejs   #   公共 <head>、顶栏、页脚骨架
│       ├── index.ejs          #   首页 ← design/prototype/index.html
│       ├── list.ejs           #   列表页 ← list.html
│       ├── detail.ejs         #   详情页 ← detail.html
│       ├── login.ejs          #   登录页 ← login.html
│       └── admin/             #   管理台各分区 ← admin.html 拆分
│           ├── accounts.ejs  tags.ejs  content.ejs  comments.ejs  sources.ejs
│
├── public/                    # ── 静态资源Express.static 直出)──
│   ├── css/main.css           #   设计系统 ← design/prototype/assets/css/main.css
│   ├── js/main.js             #   通用交互 ← assets/js/main.js去演示逻辑
│   ├── js/admin.js            #   管理台交互(分区切换、表格操作)
│   └── favicon.svg
│
├── data/                      # DuckDB 库文件目录docker volume 挂载点,不入库)
│
└── test/
    ├── helpers.js             # 构建临时库 + 生成 app 实例的工具
    ├── tags.test.js           # 标签树移动/删除规则
    ├── entries.test.js        # 状态机与搜索
    └── mcp.test.js            # 四个工具冒烟测试

3. 模块职责

3.1 入口与装配 —— src/index.jssrc/app.js

  • 负责启动顺序编排config → db.connect → migrate → app → listen中间件装配静态目录、session、json 解析、路由挂载、错误处理最后注册)。
  • 不做:任何业务逻辑。

3.2 配置 —— src/config.js

  • 负责:集中读取环境变量并给默认值:PORT(3000)、DUCKDB_PATH(./data/awesome.duckdb)、SESSION_SECRETADMIN_INIT_PASSWORDAI_INGEST_KEYMCP_READONLY(true)。
  • 约束:任何模块不得直接读 process.env,一律 import config。

3.3 数据层 —— src/db/

  • 负责连接单例与查询助手prepared statement 封装);schema.sql 为全库唯一 DDL 来源;migrate.js 幂等CREATE TABLE IF NOT EXISTS + 种子:初始 admin、内置「人工录入」源
  • 不做:不含业务规则;不了解 HTTP。

3.4 接口层 —— src/routes/

  • 负责URL → handler 映射zod 解析 query/body调用对应 service包装统一响应 { ok: true, data } / { ok: false, error: { code, message } }
  • 不做SQL、业务判定如「能否灰掉」属于 service

3.5 业务服务层 —— src/services/(核心)

service 核心规则
entry.service 状态机:pending → active / greyedactive ⇄ greyed 可逆;删除仅物理删除且需 admin。写入时维护 updated_at、人工改动刷新 verified_at
tag.service 树操作:新增子标签;移动时校验目标不是自身后代(防环);删除父标签时子标签自动上移一级sort 权重排序
comment.service 登录或匿名昵称均可发布删除为物理删除admin 或本人)
user.service 角色 admin / editoreditor 不能管理账号与内容源
source.service 内容源注册:三类来源——human(人工录入,常开)/ aiAI 接口,可停用)/ feed外部站点自动拉取URL、适配器名、cron 计划策略字段AI/Feed 新条目默认落 pending,「直发」开关关闭时才直接 active;记录每源的最近运行状态
search.service 组合过滤(关键字 + 类型 + 标签集合,标签含后代);基于 DuckDB FTS 的全文索引;random_entry() 从 active 集合随机

3.6 采集层 —— src/ingest/(自动化内容的来源)

内容源不只是「人工 / AI 接口」两种手动通道,还包含从外部站点定时拉取并整理的 自动通道。首个落地来源:二丫讲梵学习周刊 https://wiki.eryajf.net/weekly/ VuePress 站点,自带 /rss.xml;每期页面内含若干开源项目条目)。

  • 负责
    • scheduler.js:进程内定时器,扫描 enabled 的 feed 源,到达各自 cron 计划即触发一次采集(单实例内互斥锁防重入);
    • pipeline.js:单次采集编排——adapter.fetch(url) 产出原始条目数组 { title, url, description, extra } → 规范化 → 按规范化 URL 去重(已存在即 skip→ 经 entry.service.create() 入库(状态按源策略:默认 pending 待人工核验)→ 写一条运行记录;
    • adapters/:解析器可插拔。优先写通用 rss.adapter.js(覆盖大多数周刊/博客); RSS 拿不到结构化项目列表时用站点专用适配器,如 eryajf-weekly.adapter.js (拉取最新一期 HTML抽取期号、每期内的项目名/仓库链接/推荐语);
    • 失败不抛出中断进程:错误记入运行记录并在管理台可见。
  • 不做:不做 UI 判断、不直接写 SQL经 services不改已存在条目更新由人工在管理台完成

3.7 MCP 模块 —— src/mcp/

  • 负责:把四个只读工具暴露为 MCP Streamable HTTPPOST/GET/DELETE /mcpsearch_entriesget_entryrandom_entrylist_tags;实现即一行 service 调用,保证人看的 API 与 AI 用的 MCP 永远同源同权
  • 不做:任何写操作(登录页已声明:此门不对机器开放)。

3.8 中间件 —— src/middleware/

  • auth.jssession 登录态;requireRole('admin') 保护 /api/admin/*X-Ingest-Key 校验 AI 录入来源并标记 source='ai'
  • errors.js:业务错误 → HTTP 状态码映射;兜底 500简量请求日志。

3.9 视图 —— src/views/(「混入后台调用」模式)

  • EJS 渲染骨架与首屏必需数据(标题、统计、首屏卡片),保证无 JS 也完整可读;
  • 交互性部分(搜索联想、筛选联动、管理台表格、评论提交)由页面内 <script> 直接 fetch('/api/…') 完成——即需求所述「直接在页面中混入后台调用」;
  • 原型中的演示数据与演示 toast 全部替换为真实接口调用。

3.10 静态资源 —— public/

  • 设计系统 CSS 与通用 JS 从原型原样平移(这是本次设计阶段的资产);仅删除 demo 提交拦截,改为真实提交。

4. 数据模型概要(支撑模块边界)

users(id, username UNIQUE, password_hash, role, status, last_login_at)
sources(id, kind 'human'|'ai'|'feed', name, enabled,
        url, adapter, cron_expr,              -- feed 源专用:拉取地址 / 适配器 / 计划
        api_key_hash, direct_publish,         -- ai 源专用
        last_run_at, last_run_status)
source_runs(id, source_id → sources.id, started_at, finished_at,
            status, found, created, skipped, error)   -- feed 每次采集一条运行记录
entries(id, title, slug UNIQUE, type, description_md, url, license,
        stars, status 'active'|'greyed'|'pending', source_id → sources.id,
        verified_at, created_at, updated_at)
tags(id, name, parent_id → tags.id, sort)          -- 嵌套树
entry_tags(entry_id, tag_id)                        -- 多对多
comments(id, entry_id, author_name, user_id NULL, body, created_at)
audit_logs(id, actor, action, target, created_at)   -- 人工改动审计

外键方向决定依赖routes → services → dbservices 之间允许单向调用 (如 entry.service 调 tag.service 校验标签存在),禁止循环。


5. 主要 HTTP 端点(与 MCP 的对应)

方法与路径 说明 MCP 工具
GET /api/entries?q&type&tags&sort&page 搜索/浏览tags 含后代展开) search_entries
GET /api/entries/random 随机一条 random_entry
GET /api/entries/:slug 详情(含机读 JSON 所需全字段) get_entry
POST/PATCH/DELETE /api/entries… 增改灰删登录AI 走 ingest key → pending
GET /api/tags/tree · POST/PATCH/DELETE /api/tags… 标签树读写 list_tags(读)
POST /api/comments · DELETE /api/comments/:id 评论
POST /api/admin/login · /api/admin/users… · /api/admin/sources… 登录、账号、内容源feed 源含 URL/适配器/计划配置)
POST /api/admin/sources/:id/run · GET /api/admin/sources/:id/runs 手动触发一次拉取 · 运行历史found/created/skipped
GET /healthz 存活检查compose healthcheck 用)

6. Docker 部署形态

services:
  web:
    build: .
    ports: ["3000:3000"]
    volumes: ["./data:/app/data"]        # DuckDB 单文件持久化
    environment:
      SESSION_SECRET: ${SESSION_SECRET}
      ADMIN_INIT_PASSWORD: ${ADMIN_INIT_PASSWORD}
      AI_INGEST_KEY: ${AI_INGEST_KEY}
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
  • 镜像:两阶段构建,产物仅 node_modules(prune --omit=dev) + src + public
  • 单进程单实例MemoryStore session 与 DuckDB 文件写入都以此为前提);
  • 升级 = 换镜像重启,数据留在宿主机 ./data

7. 实现里程碑(建议顺序)

  1. M1 地基config / db / schema / migrate + 种子 → GET /healthz 通;
  2. M2 核心 APIentries / tags含树规则/ comments 的 service 与路由 + 单测;
  3. M3 页面接管views 平移原型,页面 fetch 替换演示数据(首页、列表、详情);
  4. M4 管理侧与内容源auth、admin 各分区接真数据、AI ingest key 通道;
  5. M5 采集层scheduler + rss.adapter 通用链路 → eryajf-weekly.adapter.js 首个站点适配器跑通「拉取 → pending → 管理台核验上架」闭环 + 运行记录;
  6. M6 MCP:四工具挂载 + 冒烟测试;
  7. M7 交付Dockerfile / compose / README。