补齐后端最小功能和前端

This commit is contained in:
ChuNa7a committed 2026-10-16 23:36:56 +08:00
1 parent 0e347aad9e
commit 3e7c9420fe
60 files changed
+8181 -329

No files matched your search

+14 -10
View File
@@ -15,7 +15,8 @@
| --- | --- | --- |
| GET | `/api/health` | 公共,API 存活,不代表数据库或发布同步健康 |
| GET | `/api/posts` | 公共,`{commit,posts:[metadata]}`,仅已发布,日期倒序 |
| GET | `/api/posts/:slug` | 公共,`{meta,markdown,format,author,article_id,assets:[{name,url}]}`,草稿及不存在均 404 |
| GET | `/api/posts/:slug` | 公共,`{meta,markdown,format,author,article_id,entry,pages:[{name,url}],assets:[{name,url}]}`,草稿及不存在均 404 |
| GET | `/api/posts/:slug/pages/:name` | 公共,指定页面的正文、元信息、格式、页面列表和资源地址;非公开或不存在均 404 |
| GET | `/api/assets/:slug/:name` | 公共,仅已发布文章的已验证图片 |
| GET | `/api/stats` | 公共,文章数、标签数、正文源字节数、启动以来文章请求数 |
| POST | `/api/auth/register` | 公共 + Origin;`{username,password}`,创建 reader;可配置关闭 |
@@ -26,7 +27,7 @@
| PUT | `/api/account/password` | 登录 + Origin + CSRF;`{old_password,new_password}`,成功后全部会话失效 |
| GET | `/api/admin/users` | 管理员;返回用户启用状态、角色、前缀及公钥,不含哈希 |
| PATCH | `/api/admin/users/:username` | 管理员 + Origin + CSRF;`{enabled,role}`;兼容可选的旧 `prefixes` 字段 |
| GET | `/api/admin/status` | 管理员;`{commit,total_posts,drafts,indexed_commit}` |
| GET | `/api/admin/status` | 管理员;`{commit,total_posts,drafts,indexed_commit,pending}` |
## 权限与账户
@@ -56,13 +57,16 @@
## 文章与资源
- 文章以作者仓库的 `index.txt` 为登记依据,每行三列:`文章ID posts/文件夹 正文文件名`。
- 站内 `:slug` 为 `作者名~文章ID`,例如 `alice~first`;索引 ID 不随文件夹或文件名变化。
- `markdown` 保留现有正文源字段(含 Frontmatter);`format` 为 `markdown` 或 `text`,纯文本不得当作 HTML 渲染。
- 未登记内容及草稿不通过公共文章/资源接口返回;删除索引项会自动撤回阅读内容并同步删除派生数据库记录。
文章由作者仓库的 `index.toml` 登记,结构、字段和多页样例见[文章发布](publishing.md)。站内 `:slug` 为 `作者名~文章ID`,目录或入口改名不会改变 ID。
## 同步状态
`GET /api/posts/:slug` 返回入口正文。`markdown` 是正文源文本,服务端不再把 Frontmatter 解析为元信息,`entry` 是入口文件名,`pages` 和 `assets` 提供该文章可达页面及已导入图片的 URL。`format` 为 `markdown` 或 `text`,阅读端不得将纯文本直接当作 HTML。
- `/api/admin/status` 的 `commit` 和 `indexed_commit` 为整个作者集合的版本摘要,不是单个作者的 Git HEAD。
- 作者各自的源提交记录在数据库 `blog_articles.git_commit` 和私有集合快照中。
- 数据库失败时 Git 接收与阅读仍可继续,索引在恢复后自动重建;站长审核暂未启用。
`meta` 包含 `title/description/tags/cover/slug/author/created_at/date/updated/status`。`cover` 是文件名或 null,对应 `assets` 中的资源。`author` 由服务端认证身份填写;`created_at` 是首次导入时间,`date` 是首次公开时间,未公开时为空字符串;`updated` 是元信息、可达页面或有效图片实际变化的时间。时间使用 UTC RFC 3339 字符串,不接受作者在索引或 Git 提交中指定。
页面链接如 `chapter.md#part` 由阅读端根据 `pages` 映射到页面 URL,并保留锚点。非公开内容、未登记目录、未链接页面与未引用图片不会由公共接口返回。移除索引条目或入口缺失时,页面和资源入口同时撤回。
## 同步与暂存状态
`/api/admin/status` 的 `commit` 和 `indexed_commit` 为整个生效作者集合的版本摘要,不是单个作者的 Git HEAD。`pending` 的格式为 `{作者名: {commit, reason}}`,仅管理员可读。TOML 无效时保留原生效版本及其数据库记录,暂存状态跨进程重启保存;修复后的正常推送会替换暂存状态并重新导入。
作者的源提交记录在数据库 `blog_articles.git_commit` 和私有集合快照中。数据库失败时 Git 接收与已导入文章阅读仍可继续,恢复后自动重建。站长审核暂未启用。
@@ -2,7 +2,7 @@
## 状态与日期
已接受,2026-10-16。替代 ADR-001 中共享仓库、slug 前缀隔离与固定正文路径的决定;唯一 Git 写入口、SSH 双重授权和账户存储现状不变。
已接受,2026-10-16。索引格式与多页行为现由 [ADR-004](0004-toml-multipage-staging.md) 替代。替代 ADR-001 中共享仓库、slug 前缀隔离与固定正文路径的决定;唯一 Git 写入口、SSH 双重授权和账户存储现状不变。
## 决定
@@ -0,0 +1,27 @@
# ADR-004:TOML 元信息、多页文章与索引暂存
## 状态与日期
已接受,2026-10-16。替代 ADR-003 中三列文本索引、Frontmatter 和入口缺失拒绝推送的约定。作者仓库隔离及唯一 Git 写入口保持有效。
## 决定
根索引使用 `index.toml`,每篇文章由 `[[articles]]` 登记。`id` 和 `folder` 为结构字段,作者可指定的元信息限于 `title`、`description`、`tags`、`cover`、`public` 和 `entry`,其中入口默认 `main.md`。拒绝未知字段,作者与系统时间不从 TOML、Markdown 或 Git author/date 读取。正文直接保存文本,不再解析 Frontmatter。
服务器从入口遍历同目录相对文档链接,导入最多 64 个可达页面及其图片,并额外导入封面。允许页面互相链接和锚点,不允许跨目录读取。未登记目录、不可达文档及未使用图片仍接受安全和容量检查,但不进入阅读快照或数据库。
接收校验区分安全失败和索引失败。非法路径、文件模式、危险内容及配额违规拒绝整个推送。TOML 缺失、语法错误、未知字段、元信息错误以及缺失的页面链接目标暂存提交,不替换该作者已有文章。索引有效但某篇入口不存在时,该文章视为不存在,不保留其旧公开版本。
服务端首次成功导入时生成创建时间,首次公开时生成发布时间。实际元信息、有效页面或图片变化时更新修改时间。修改无关文件、索引排版或其他文章不改变时间。系统时间与生效提交映射保存在私有 `collection.json`,同时同步到数据库。索引错误不会更新文章时间。
接收仓库 HEAD 可以是暂存版本,生效集合独立记录最后成功导入的作者提交。私有集合保存暂存提交和错误,进程重启后仍保留原生效文章。管理员状态接口返回暂存信息。数据库仍只保存生效文章、页面及标签的派生索引,索引失败自动重试,不阻塞已导入文章阅读。
## 取舍
坏索引不会导致旧文章消失,作者必须修复索引并正常推送下一提交。索引有效时,删除入口或删除文章条目才会撤回文章。清空全部文章需显式写入 `articles = []`,空 TOML 文件作为缺少结构字段处理。
系统时间无法仅从作者可伪造的 Git 提交时间恢复,必须备份私有集合。丢失集合后,当前 Git HEAD 若为坏索引,也不足以找回先前生效版本。备份恢复流程必须保留生效提交和系统元信息。
多页接口返回正文源文本、页面清单和资源地址,前端负责相对链接映射与安全渲染。`public = false` 暂时阻止所有公共读取;登录阅读内部文章和站长审核继续列入 TODO,不在本轮实现。
索引格式、迁移和操作示例见[文章发布](../publishing.md)与[运维与迁移](../operations.md)。
@@ -0,0 +1,23 @@
# ADR-005:Astro 阅读前端与同源部署
## 状态与日期
已接受,2026-10-16。按维护者要求增加简单前端,不改变 Git 唯一文章写入口。
## 决定
使用 Astro、TypeScript 与 Node 适配器进行服务端渲染,不增加 React 或浏览器状态框架。首页全部居中,展示施工标题、已完成和未完成部分及导航。文章列表和正文在服务端读取现有公开 API,筛选和分页状态保存在 URL。
注册、登录、公钥、密码与站长授权使用浏览器同源请求。会话继续由后端 Cookie 管理,CSRF 仅保存在当前页面内存。前端不获得数据库口令,不承担权限校验,也不提供在线文章编辑。
Markdown 禁止原始 HTML 并清理渲染结果。相对文档链接和图片只映射到同文章已导入的资源。前端设置 CSP 与禁用缓存,错误时展示真实故障或空状态,不填充示例文章。
Docker Compose 增加独立 Node 前端服务,Caddy 将页面转发到前端,将 `/api/*` 转发到 Rust。前端、API 与数据库只连接内部网络,不暴露宿主端口。ChuSQL 仅运行数据库服务,不启动其 Web 管理端。
通过 `REGISTRY_PREFIX` 选择容器镜像前缀,示例配置采用腾讯云内网镜像。非腾讯云环境需清空或调整该配置,项目不修改宿主 Docker 守护进程。
## 取舍
服务端渲染增加一个轻量 Node 服务,但避免静态构建后文章更新不同步。首页不依赖数据库或文章服务,仍能在服务故障时提供导航。单独运行生产 Node 服务不代理 `/api`,完整部署使用 Caddy;开发模式由 Vite 代理 API。
系统字体保持浏览器本地渲染,不引入外部字体与跟踪请求。评论、互动、内部文章阅读、审核、暗色模式和完整 SEO 继续列入 TODO。
+4 -2
View File
@@ -6,6 +6,8 @@
| --- | --- |
| [ADR-001](0001-git-only-publishing.md) | Git 唯一写入口与身份隔离;共享仓库方案已由 ADR-003 替代 |
| [ADR-002](0002-external-dependencies.md) | 外部依赖子模块、固定提交与 HTTPS 回退 |
| [ADR-003](0003-author-repositories-and-manifest.md) | 作者独立仓库与文章清单 |
| [ADR-003](0003-author-repositories-and-manifest.md) | 作者独立仓库;文本索引方案由 ADR-004 替代 |
| [ADR-004](0004-toml-multipage-staging.md) | TOML 元信息、多页正文、系统时间与索引暂存 |
| [ADR-005](0005-astro-reading-frontend.md) | Astro 前端、同源 API 与 Docker/Caddy 编排 |
旧决策保留其历史背景;当前作者隔离与文章索引规则以 ADR-003 为准。
旧决策保留其历史背景;当前作者隔离与文章索引规则以 ADR-003 和 ADR-004 为准。
+18 -2
View File
@@ -11,6 +11,22 @@
scripts/deploy.sh
```
## 服务与路由
Compose 编排 Astro 前端 `web`、Rust API `api`、文章 SSH 接收 `ssh`、ChuSQL 数据库 `db` 和 Caddy。`init` 初始化持久卷,`db-init` 用于首次数据库授权。
`web` 是博客前端,不是 ChuSQL Web。数据库只构建 `chusql-server`、`csql-bootstrap` 与存储库,不构建或启动 `chusql-web`、`csql-web` 或数据库管理网站。
Caddy 将 `/api/*` 转发到 `api:3001`,其余请求转发到 `web:4321`。Node、API 和数据库只加入内部网络,不映射宿主端口;公网入口为 Caddy 的 HTTP/HTTPS 和文章 SSH。前端容器使用非 root 用户、只读根文件系统和独立资源上限。
## 腾讯云镜像
`.env.example` 默认配置 `REGISTRY_PREFIX=mirror.ccs.tencentyun.com/library/`,为 Debian、Node 与 Caddy 使用腾讯云内网 Docker 镜像。该地址仅供腾讯云内网访问,见[腾讯云官方说明](https://cloud.tencent.com/document/practice/1207/45596)。
在腾讯云内网部署时保留该值;其他环境将 `REGISTRY_PREFIX` 留空以使用原始镜像名,或改为可访问且受信的镜像前缀。项目不修改宿主机 Docker 配置、不关闭 TLS,也不重启 Docker 守护进程。
镜像注册表与 Debian 软件包源是不同配置。`REGISTRY_PREFIX` 控制基础镜像和 Caddy,`DEBIAN_MIRROR` 控制容器构建时的 apt 源。
## 首次部署
先按[开发与依赖](development.md)初始化子模块,再执行以下同架构构建步骤。使用 `scripts/deploy.sh` 时,也须先准备 `.env`。
@@ -22,7 +38,7 @@ cp .env.example .env
chmod 600 .env
# 从标准输入输入管理员口令,Ctrl-D 结束;不要把明文写进命令行历史。
./deploy/bin/chublog-api hash-password
# 将输出以单引号包裹写入 .env 的 ADMIN_PASSWORD_HASH;同时配置域名及两份不同的随机 DB 口令。
# 将输出以单引号包裹写入 .env 的 ADMIN_PASSWORD_HASH;同时配置域名、镜像前缀及两份不同的随机 DB 口令。
docker compose --env-file .env -f deploy/compose.yaml up -d --build
# db 启动就绪后,创建 chublog 库和非超级用户 chublog_index;失败时检查日志后重试。
docker compose --env-file .env -f deploy/compose.yaml run --rm db-init
@@ -42,7 +58,7 @@ docker compose --env-file .env -f deploy/compose.yaml run --rm db-init
## 管理员初始化
首次启动从环境中的哈希创建管理员,后续账号与权限以持久文件为准,修改初始哈希不会覆盖已有账户。
浏览器打开配置的站点即可看到前端。`SITE_TITLE` 控制页眉名称。首次启动从环境中的哈希创建管理员,后续账号与权限以持久文件为准,修改初始哈希不会覆盖已有账户。
## 域名与网络
+5 -1
View File
@@ -55,16 +55,20 @@ cargo fmt --check
cargo clippy --all-targets -- -D warnings
```
前端开发和构建见 [Astro 前端](frontend.md)。
## 联调
```sh
python3 scripts/prepare-deploy.py --build-database
python3 scripts/smoke.py
python3 scripts/smoke.py --key-type rsa
# 安装 Python Playwright 和 Chromium 后,可追加前端真实浏览器验证。
python3 scripts/smoke.py --key-type rsa --browser-test
```
- 联调使用独立随机命名的 Compose 项目、本机 18080/18443/12222 端口,测试结束清理自己创建的容器与卷。
- 覆盖真实 SSH 身份与授权、作者间 Git 对象隔离、非法推送、索引删除、ChuSQL 元信息创建/删除、发布撤回、断库阅读、重启与撤权。
- 覆盖真实 SSH 身份与授权、作者间 Git 对象隔离、非法推送、TOML 暂存与修复、多页链接、系统时间、索引删除、ChuSQL 元信息创建/删除、发布撤回、断库阅读、重启与撤权。
- 该测试会拉取基础镜像,需要 Docker 可用。
## 历史清理后的协作
+68
View File
@@ -0,0 +1,68 @@
# Astro 前端
[返回项目首页](../README.md)
前端位于 `apps/web`,使用 Astro、TypeScript 和 Node 服务端渲染。首页按要求展示居中的“个人博客正在施工中”,下方居中列出已完成和未完成部分。
## 页面
| 地址 | 功能 |
| --- | --- |
| `/` | 施工说明、进度与导航,内容全部居中 |
| `/articles` | 文章列表、标题/简介/作者/标签搜索、标签筛选和分页 |
| `/posts/作者名~文章ID` | 阅读入口文档、图片和文章元信息 |
| `/posts/作者名~文章ID/页面名.md` | 阅读同文章的其他可达页面,保留相对链接和锚点 |
| `/login`、`/login?mode=register` | 登录、注册与错误反馈 |
| `/account` | 公钥登记、密码修改、退出和 Git 发布说明;管理员可授权账户并查看待修复索引 |
未实现的评论、互动、内部文章阅读、审核和热度排序不提供占位操作按钮。文章写入继续通过 Git,不提供在线编辑。
## 本地开发
需要 Node.js 22.22 或更高版本,Docker 前端镜像使用 Node.js 24。先启动 API,并将其 `PUBLIC_ORIGIN` 设为浏览器使用的地址,例如 `http://127.0.0.1:4321`。
```sh
cd apps/web
npm ci
cp .env.example .env
npm run dev
```
打开 `http://127.0.0.1:4321`。`API_BASE_URL` 默认 `http://127.0.0.1:3001`,开发服务器将 `/api` 请求转发到该地址。`PUBLIC_ORIGIN` 用于显示正确的服务器 SSH 主机名,应与后端配置及浏览器地址一致。`SSH_PORT` 控制发布说明中的端口。
```sh
npm run build
npm test
```
构建同时检查 Astro 和 TypeScript。生产运行 `npm start` 只启动页面服务,API 同源转发由 Caddy 提供,见[部署说明](deployment.md)。
## 安全与访问
浏览器使用后端 HttpOnly Cookie,CSRF 只保存在内存,不写入 localStorage。修改请求携带当前会话的 CSRF,Origin 由浏览器发送;后端继续执行全部权限检查。
Markdown 禁用原始 HTML,并在渲染后清理输出。文章内链接只映射到 API 返回的同文章页面和图片;普通外部链接仍受协议限制。纯文本通过转义输出显示。页面设置 CSP、禁用缓存和内容类型嗅探,不加载第三方字体、脚本或跟踪服务。
文章服务故障返回可重试提示,不填充示例数据。注册被关闭、会话失效和限流错误直接显示后端反馈。
## 浏览器检查
浏览器联调需要 Python Playwright 及 Chromium:
```sh
python3 -m pip install playwright
python3 -m playwright install chromium
python3 scripts/smoke.py --key-type rsa --browser-test
```
该命令使用隔离的 Docker 项目,验证真实注册登录、密码修改、公钥保存、站长授权、多页阅读及手机布局,测试后清理自己创建的容器与卷。截图默认放在 `/tmp/chublog-web-screenshots`。系统缺少浏览器库时,按 Playwright 安装说明准备运行环境。
## 项目技能
按维护者要求下载到本项目 `.agents/skills/`,作为开发参考,不是运行时依赖:
- [frontend-design](../.agents/skills/frontend-design/SKILL.md),来源为 [Anthropic skills](https://github.com/anthropics/skills/tree/main/skills/frontend-design)。
- [webapp-testing](../.agents/skills/webapp-testing/SKILL.md),来源为 [Anthropic skills](https://github.com/anthropics/skills/tree/main/skills/webapp-testing)。
- [web-design-guidelines](../.agents/skills/web-design-guidelines/SKILL.md),来源为 [Vercel agent skills](https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines)。
后续会话可继续发现这些技能。本轮已直接读取,按项目要求采用居中施工页、克制排版、键盘焦点、表单标签和真实浏览器检查。
+9 -3
View File
@@ -17,7 +17,7 @@
## 数据库故障与索引重建
- 数据库索引失败不会修改作者 Git 或阻塞已导入文章的 HTTP 阅读;新集合会触发同步,失败至少间隔 30 秒重试,并定期检查索引是否丢失。
- `blog_articles` 保存全部有效登记文章(含草稿)的作者、本地 ID、正文路径、格式、源提交和元信息;`blog_posts/blog_tags` 保留公开检索索引。
- `blog_articles` 保存全部有效登记文章(含草稿)的作者、本地 ID、正文路径、格式、源提交、封面和系统时间;`blog_pages` 保存有效页面路径,`blog_article_tags` 保存全部文章标签;`blog_posts/blog_tags` 保留公开检索索引。
- 恢复数据库后可自动重建;离线命令为 `chublog-api rebuild-index`(必须停止同一数据卷上的 API 进程后运行,防止并发),或等待在线自动重试。
- `indexed_commit:null` 表示尚无已确认可用的索引,旧值表示尚未追平当前内容。
- 重建上述派生表使用 ChuSQL 事务;数据库不可用时保留阅读快照,恢复后重建。
@@ -28,7 +28,7 @@
- 接收 Git 可额外镜像备份。
- 恢复时先停止写入,从同一备份恢复各卷,再启动;不要只恢复 ChuSQL,也不要直接向正式仓库推送。
- 应用启动加载私有集合快照,并继续从各作者 Git 校验导入。
- 丢失派生快照时,可在停机并保留备份后移走损坏的 `collection.json`,由各作者接收仓库重新导入;不要丢失作者仓库或账户授权。
- `collection.json` 还保存生效版本、暂存状态与服务端生成的时间,必须备份。丢失时可从有效作者仓库重新导入正文,但无法还原原始系统时间;若作者当前索引损坏,还需要备份中的上一个生效版本。不要将它当作可随意丢弃的缓存。
- 进程重启会注销所有会话。
本实现对文件写入执行原子替换与 fsync,Git 引用采用受控更新;磁盘故障、主机断电和备份恢复仍需真实环境演练。不要对含正式数据的项目执行 `docker compose down -v`。
@@ -40,9 +40,15 @@
1. 从旧共享仓库导出各作者最新的文章文件(含草稿),按原发布前缀及管理员确认的所有权分给作者;不要复制 `.git` 或旧提交历史。
2. 管理员离线将 `/auth/publishers.json` 的 `repository` 改为 `/srv/ingress/authors`(或自定义 `INGRESS_DIR`),保留用户和公钥授权。新版本 init 服务会创建该根目录。
3. 将旧 API 数据卷根目录的 `content.git` 移入只对管理员可见的备份位置;旧 `published.json` 同样备份。旧 SSH 共享仓库不再供服务使用,应移出接收卷离线保管。不要删除备份或清空用户数据。
4. 启动新服务。每位作者重新 clone 自己的空仓库,复制自己的导出文件,创建三列 `index.txt`,整理成一个首次提交并推送。旧 `slug` 如保留,须与索引 ID 一致。
4. 启动新服务。每位作者重新 clone 自己的空仓库,复制自己的导出文件,将原 Frontmatter 中的标题、简介和标签迁入 `index.toml`,将原发布状态映射到 `public`;为每篇文章指定 ID、目录和入口,移除正文 Frontmatter,整理成一个首次提交并推送。作者和时间由服务端重新记录。
5. 核对草稿隔离、所有文章、数据库索引和新的 `作者名~ID` URL 后再恢复对外服务。旧版本已被其他人克隆的内容无法收回。
- 已有授权配置仍指向旧仓库时,新版本拒绝启动;不会自动重新分发旧历史。
- 原链接到新 ID 的重定向尚未实现,迁移时需另行安排。
- 若自定义 `INGRESS_DIR`,需同步调整镜像内 `deploy/gitconfig` 的受信目录范围;默认仅信任 `/srv/ingress/authors/*`,不使用全局 `safe.directory=*`。
## 从个人仓库的文本索引迁移
停写并备份作者仓库和 `collection.json`,然后把 `index.txt` 改为根目录 `index.toml`。保留原文章 ID 和目录,从正文 Frontmatter 迁移允许的元信息,正文入口不叫 `main.md` 时显式填写 `entry`。删除旧 `index.txt` 和正文 Frontmatter 后,作为一个提交推送。沿用原私有集合快照可保留已有系统时间;旧正文中的日期不会被新版本采纳。
不要提交两种索引并存的仓库。仅根目录的 `index.toml` 在白名单内,其他 TOML 配置仍被拒绝。派生的 `blog_articles` 表若缺少新列,会由索引重建程序重建;该表不得混存评论、授权、审核或站长管理数据。
+100 -67
View File
@@ -16,92 +16,125 @@
- 设回 `reader` 或 `enabled:false` 会撤回发布授权,并使现有网站会话失效。
- 禁止管理员停用自己或取消自己的管理角色。
## 本地 Git 工作流
## 克隆个人文章仓库
- 先通过可信渠道核对服务器 Ed25519 主机密钥指纹;不要禁用主机密钥检查。
- 主机密钥保存在独立持久卷,重建容器不会更换。
- 所有发布者使用同一个 OS 账户 `git`,身份由对应公钥的强制命令绑定,不由 Git author/email 决定。
先通过可信渠道核对服务器主机密钥指纹,再在本机 `~/.ssh/config` 配置连接:
```sh
# ~/.ssh/config
```sshconfig
Host chublog
HostName blog.example.com
Port 2222
User git
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
```
# 无论作者是谁,别名都是 content.git;服务端按认证身份映射个人仓库。
# 首次 clone 自动创建服务器裸仓库,作者无须自己创建裸仓库。
将域名、端口和私钥路径替换为部署者提供的值。主机密钥检查应保持开启。服务器根据公钥识别作者,不使用 Git 的 author/email 判定身份。
```sh
git clone chublog:content.git my-articles
cd my-articles
# 仓库已有内容时,每轮编辑前先 git pull --ff-only。
# 创建 index.txt 和正文,格式见下文。
git add index.txt posts/first
```
每位作者都使用 `content.git` 别名。服务器将别名映射到该作者的独立仓库,并在首次访问时创建裸仓库,作者无需服务器 shell。仓库已有提交时,每轮编辑前先执行 `git pull --ff-only`。
## 创建 TOML 索引
在仓库根目录创建 UTF-8 文件 `index.toml`,每篇文章使用一个 `[[articles]]` 表:
```toml
[[articles]]
id = "first"
folder = "posts/first"
title = "第一篇文章"
description = "文章简介"
tags = ["Rust", "随笔"]
public = true
# cover = "cover.png"
# entry = "main.md"
```
`id` 和 `folder` 用于标识文章及其目录。文章元信息仅接受以下字段:
| 字段 | 含义与默认值 |
| --- | --- |
| `title` | 必填标题,最多 240 字节 |
| `description` | 简介,省略时为空,最多 1000 字节 |
| `tags` | 标签,默认空数组,最多 12 项,每项最多 80 字节 |
| `cover` | 可选封面,填写同文章目录中的图片文件名 |
| `public` | 是否公开,默认 `false` |
| `entry` | 入口文档名,默认 `main.md` |
作者身份由 SSH 公钥绑定的账户确定。创建时间、首次公开时间和修改时间由服务端生成,以 UTC 时间保存。索引不接受 `author`、`date`、`updated`、精华或权限等额外字段。正文不再使用 Frontmatter。
文章 ID 在作者内唯一,允许 1 到 40 位小写字母、数字和中间短横线。目录为 `posts/<目录名>`,目录名最多 80 字节并遵循 slug 规则。ID 和目录名不能使用保留名称,例如 `admin` 或 `api`。同一目录不能登记多次。
文件名只允许 ASCII 字母、数字、短横线、下划线和点,最多 100 字节,不能以点开头或含 `..`。入口允许 `.md`、`.markdown` 和 `.txt`,扩展名须为小写。索引最多 64 KiB、1000 篇文章。要撤回全部文章,明确写入 `articles = []`;空文件缺少文章字段,会暂存并保留原文章。
## 编写多页文章
示例目录:
```text
index.toml
posts/
└── first/
├── main.md
├── chapter.md
└── cover.png
```
`main.md` 内容:
```markdown
# 第一篇文章
[阅读第二页](chapter.md#details)
![封面](cover.png)
```
`chapter.md` 内容:
```markdown
# 第二页
## Details
[返回入口](./main.md)
```
服务端从入口沿文章内相对链接收集页面,允许循环链接,每篇最多 64 个可达页面。链接目标必须是同目录的文本文件,可以带 `#锚点`,不支持子目录、`../`、编码路径和查询参数。相对链接缺少目标文件时,整份索引暂存,原文章不更新。普通 HTTP、HTTPS、mailto 和站内绝对链接继续受安全检查。
未登记目录、从入口无法到达的文档,以及未被页面引用的图片不进入阅读快照或数据库。封面即使未在正文引用,也作为文章资源导入。所有上传文件仍受白名单、安全检查和仓库配额约束。`.txt` 页面通过 `format: text` 返回,阅读端须作为纯文本显示。
参考[索引样例](../content/index.toml)、[入口样例](../content/posts/welcome/main.md)和[第二页样例](../content/posts/welcome/chapter.md)。
## 提交并验证发布
```sh
git add index.toml posts/first
git commit -m 'Publish first article'
git push -u origin main
```
## 文章目录与清单
服务端会校验提交并自动导入。站内文章 ID 为 `作者名~文章ID`,例如 `/api/posts/alice~first`。文章 API 返回入口正文、页面列表和资源地址;页面通过 `/api/posts/alice~first/pages/chapter.md` 读取。前端需将文档相对链接映射到返回的页面 URL,并保留锚点。
仓库结构示例:
```text
index.txt
posts/
└── first/
├── article.md
└── cover.png
```
`index.txt` 为 UTF-8 三列文本,以空格或 Tab 分隔,无表头,允许空行:
```text
first posts/first article.md
notes posts/my-notes notes.markdown
```
- 三列依次为**文章 ID、相对文件夹、正文文件名**。
- ID 在作者内唯一,支持 1–40 位小写字母、数字及中间短横线,另须避开保留名称。
- 目录必须是 `posts/<目录名>`,目录名遵循 slug 规则,最多 80 字节;文件名只允许 ASCII 字母、数字、短横线、下划线与点,不能以点开头或含 `..`。
- 同一目录不能重复登记;正文可以任意命名,扩展名为 `.md`、`.markdown` 或 `.txt`。
- 索引所指目录和正文必须存在,路径穿越和子目录正文被拒绝。
- 索引上限 64 KiB、1000 项。
## 正文与图片
- 正文沿用 YAML Frontmatter(参考[正文样例](../content/posts/welcome/index.md)),包含 `title/date/updated/status/tags/description`;`slug` 可省略,如填写则必须等于索引 ID。
- `.txt` 正文也需要 Frontmatter,API 通过 `format: text` 表明正文应作为纯文本展示;其他正文为 `format: markdown`。
- 图片置于同一目录,通过 `![说明](cover.png)` 引用。
## 新增、修改与撤回
| 作者操作 | 系统处理 |
| 提交内容 | 结果 |
| --- | --- |
| 新建正文并在索引新增一行 | 校验后导入文章,在 ChuSQL 创建作者、ID、正文路径、状态等元信息 |
| 修改正文或索引中的目录/文件名,保留 ID | 更新同一文章;站内 ID 不随目录移动而变化 |
| 删除索引中的一行 | 移除当前阅读内容、图片入口及对应数据库元信息,无需先删除本地正文 |
| 把索引清空 | 移除该作者所有当前文章,不影响其他作者 |
| 有效索引、新增文章且入口存在 | 导入文章并创建数据库元信息与页面记录 |
| 有效索引、修改正文或元信息 | 更新文章,保留首次公开时间;内容实际变化时更新修改时间 |
| 有效索引、删除文章表 | 撤回文章、页面、资源入口及数据库派生记录 |
| 有效索引、入口文件或目录缺失 | 将该文章视为不存在,撤回旧版本;其他有效文章仍可导入 |
| TOML 缺失、语法错误、未知字段或元信息无效 | Git 提交暂存,不修改该作者原有文章,等待修复后的下一次推送 |
| 脚本、非法文件类型、危险路径或内容超限 | 拒绝整个推送,远端分支不前进 |
- 未被索引选中的正文及目录视为无效内容,不导入阅读快照或数据库;图片只有被选中正文引用才作为公开资源导入;仍计算仓库容量并执行安全校验。
- 删除正文但保留其索引项属于错误,整次推送会被拒绝。
- `status: draft` 可保留登记但不公开;物理删除文件和撤回文章均不会擦除 Git 历史。
`public = false` 的内容不会由公共文章、页面或资源接口返回。登录后查看内部文章的功能仍在待办中。删除和撤回不擦除个人 Git 历史,也不收回作者已克隆的副本。
## 提交与并发限制
## 提交限制与状态查询
- MVP 每次只接受**一个直接继承个人远端 main 的提交**,首次仅允许一个根提交。
- 禁止 merge、force push、删除 main、标签及其他分支;多次本地提交应先整理为一次提交,再变基到最新远端。
- 同一作者在多台电脑操作仍需处理并发冲突,不同作者互不争用同一个仓库锁。
每次推送只接受一个直接继承个人远端 `main` 的提交,首次必须是根提交。禁止 merge、force push、删除 `main`、标签和其他分支。多次本地提交须先整理为一次提交,再变基到最新远端。同一作者在多台电脑操作仍需处理冲突,不同作者使用不同仓库锁。
## 作者隔离与文章地址
管理员通过 `GET /api/admin/status` 查看状态。`commit` 是当前生效作者提交映射的集合摘要,`indexed_commit` 是数据库已同步的集合版本。`pending` 按作者返回暂存提交和索引错误。暂存提交不会替换生效版本,API 重启后仍保留原文章和暂存状态。
- 站内文章 ID 为 `作者名~文章ID`,例如 Alice 的 `first` 对应 `/api/posts/alice~first`;不同作者可以使用相同的本地 ID。
- 服务端分别保存每位作者的 Git 对象与历史,SSH 不能指定别人的仓库路径;作者无法通过 clone 或按提交哈希 fetch 读取其他作者草稿。
- 作者自己已克隆的历史仍可在本地保留,不能靠撤回收回副本。
## 发布状态
- API 自动同步,一般约 2 秒,数据库重建、失败重试或较多作者可能延迟。
- `GET /api/admin/status` 的 `commit` 是作者提交映射的集合摘要,不是任一作者 Git 提交;`indexed_commit` 表示数据库已同步的集合版本。
- Git 接受表示接收校验完成,不保证数据库已同步。
- 当前不做站长内容审核,审核计划见 [TODO](../TODO.md)。
同步通常在下一轮后台处理时完成。数据库失败至少间隔 30 秒重试,作者较多或索引重建时可能延迟。Git 接受不代表文章已公开或数据库已同步。站长审核尚未启用,计划见 [TODO](../TODO.md)。
+7 -2
View File
@@ -4,14 +4,14 @@
## 文件白名单
- 仅允许根目录 `index.txt` 和 `posts/<目录>/<文件>` 内的 `.txt/.md/.markdown/.png/.jpg/.jpeg/.webp` 普通文件;扩展名使用小写。
- 仅允许根目录 `index.toml`(唯一允许的 TOML 文件) 和 `posts/<目录>/<文件>` 内的 `.txt/.md/.markdown/.png/.jpg/.jpeg/.webp` 普通文件;扩展名使用小写。
- 拒绝证书扩展名、隐藏文件、`.gitignore`、脚本、配置、符号链接、可执行位、子模块和其他类型。
- 文本中的 PEM 证书/密钥标记也会拒绝。
- Git 接收层能直接拦截,因此非白名单内容使整次推送失败,不静默改变作者提交。
## 容量配额
- 每个文本文件上限 256 KiB;图片单张 2 MiB、每目录最多 12 张、尺寸 4096×4096;每个目录合计 6 MiB。
- 根索引上限 64 KiB,每个文本文件上限 256 KiB;图片单张 2 MiB、每目录最多 12 张、尺寸 4096×4096;每个目录合计 6 MiB。
- 每位作者最多 1000 个目录、14001 个文件、原始文件合计 64 MiB;全站导入快照另限 64 MiB、1000 篇,超出时等待管理员处理。
## Git 接收限额
@@ -21,6 +21,7 @@
## 正文解析
- 同一文章的相对链接仅可引用同目录已验证的文本或图片,禁止跨目录读取;只有从入口可达的最多 64 个页面、页面图片和封面可被导入。
- 使用成熟 Markdown 解析器,拒绝原始 HTML、危险链接协议、外链图片、控制符和双向隐藏字符;代码块是惰性文本,绝不执行 MDX、JS、仓库脚本、构建任务或上传内容。
## SSH 命令与密钥
@@ -48,3 +49,7 @@
- 服务器只返回 JSON Markdown 与图片;后续前端必须把元数据当文本输出,并继续禁用原始 HTML、MDX和危险链接,不能将接口字段直接作为 HTML 执行。
- 隔离不能替代 Git/OpenSSH/解码库的持续安全更新。
## 索引暂存与生效版本
TOML 的语法或元信息错误只更新作者接收仓库中的暂存提交,不替换现有文章或时间记录。文件类型、模式、配额和正文安全检查仍会执行,不能通过破坏 TOML 绕过检查。有效索引中的入口不存在时,该文章不再生效。非公开文章的所有页面和资源均返回 404,防止绕过入口直读。