新增 dashmap 依赖。create_backup 探测 pg_dump 优先(含 schema,签名头前置)、
不可用回退纯 SQL(COPY TO STDOUT 逐表,按表精确进度)。restore_backup 校验
签名头(仅本系统备份)+ 路径穿越防护 + 二次确认,探测 psql 执行。
list_backups/delete_backup 管理备份,GET /api/database/backups/{name} 下载
(admin 鉴权 + 白名单)。tasks.rs DashMap 进度表(1h 惰性 GC),前端每 1.5s
轮询 get_task_progress 显示进度条/stage,完成后刷新列表。
backups/ gitignored 不直接暴露。AGENTS.md 同步新增 CodeMirror 子项目与
数据库管理章节。
242 lines
17 KiB
Markdown
242 lines
17 KiB
Markdown
# AGENTS.md
|
||
|
||
## Workflow
|
||
|
||
- **每完成一个功能点立即提交**。Agent 自主判断提交时机——当一个逻辑完整的改动通过验证(编译通过 / 测试通过)后,无需等待用户指令,直接 `git add` + `git commit`。
|
||
- 提交粒度按"功能点"而非"文件":相关联的多文件改动合并为一个提交,不相关的改动拆成多个提交。
|
||
- 提交信息遵循现有风格:`type(scope): 简述`,正文(可选)说明动机与关键改动。常见 type:`feat` / `fix` / `docs` / `refactor` / `chore` / `perf`。
|
||
- 只在用户明确要求时才 `git push`。提交到本地即可,不主动推送。
|
||
|
||
## Development Commands
|
||
|
||
```bash
|
||
make dev # tailwindcss watch + dx serve (needs PostgreSQL)
|
||
make build # build-editor → highlight-css → tailwindcss → doc → dx build --release
|
||
make build-linux # same as build but targets x86_64-unknown-linux-musl
|
||
make css # one-shot CSS
|
||
make css-watch # watch mode
|
||
make test # cargo test + vitest (tiptap-editor + lightbox + yggdrasil-core libs)
|
||
make doc # cargo doc (ayu 主题) → 拷贝到 public/doc/,随 build 发布
|
||
make doc-open # 同 doc,生成后自动用浏览器打开(本地预览,不拷贝)
|
||
make clean # cargo clean + rm public/style.css
|
||
```
|
||
|
||
**Build order matters**: `make build` runs `build-editor` → `highlight-css` (`cargo run --bin generate_highlight_css`) → `tailwindcss --minify` → `doc` (`cargo doc` + 拷贝到 `public/doc/`) → `dx build --release`. Do not run `dx build --release` alone.
|
||
|
||
## Prerequisites
|
||
|
||
- Rust 1.95+ with `wasm32-unknown-unknown` target
|
||
- `dx` CLI (`cargo install dioxus-cli`)
|
||
- `tailwindcss` CLI v4 — install via `npm install -g @tailwindcss/cli` (v4 splits the CLI into its own package; the `tailwindcss` core package has no `bin`), or use the standalone binary
|
||
- PostgreSQL running locally
|
||
|
||
## Environment
|
||
|
||
Create `.env` (not committed):
|
||
|
||
```
|
||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/yggdrasil
|
||
RUST_LOG=info
|
||
```
|
||
|
||
Optional tuning via env vars (all have sane defaults):
|
||
|
||
```
|
||
WEBP_QUALITY=85.0 # 0.0–100.0, clamped
|
||
WEBP_METHOD=2 # 0–6, clamped
|
||
MAX_IMAGE_DIMENSION=8192 # max single side in px, min 512, no upper limit
|
||
MAX_IMAGE_PIXELS=50000000 # max total pixels (~7k×7k), min 1M, no upper limit
|
||
RATE_LIMIT_STRICT_PER_SEC=1
|
||
RATE_LIMIT_STRICT_BURST=5
|
||
RATE_LIMIT_UPLOAD_PER_SEC=2
|
||
RATE_LIMIT_UPLOAD_BURST=15
|
||
RATE_LIMIT_IMAGE_PER_SEC=10
|
||
RATE_LIMIT_IMAGE_BURST=50
|
||
RATE_LIMIT_COMMENT_PER_SEC=1 # comment posting
|
||
RATE_LIMIT_COMMENT_BURST=5
|
||
RATE_LIMIT_UNKNOWN_PER_SEC=30 # fallback bucket when real client IP can't be determined
|
||
RATE_LIMIT_UNKNOWN_BURST=100
|
||
DB_POOL_SIZE=20 # database connection pool size
|
||
MIGRATE_STARTUP_TIMEOUT_SECS=30 # how long startup waits for PostgreSQL before giving up
|
||
STATEMENT_TIMEOUT_SECS=30 # per-query timeout; slow queries are canceled to protect the pool
|
||
SSR_CACHE_SECS=3600 # incremental SSR cache TTL
|
||
```
|
||
|
||
Session / security tuning:
|
||
|
||
```
|
||
COOKIE_SECURE=false # set true/1/yes to add Secure flag to session cookie
|
||
TRUSTED_PROXY_COUNT=0 # number of reverse proxies in front of the app; used to extract real client IP from X-Forwarded-For
|
||
APP_BASE_URL= # e.g. https://your-domain.example — trusted origin for CSRF checks on write requests; unset falls back to Host header + X-Forwarded-Proto
|
||
```
|
||
|
||
Run migrations before first dev server start:
|
||
|
||
```bash
|
||
./migrate.sh # auto-creates DB, runs migrations/ in order
|
||
```
|
||
|
||
## Architecture: Conditional Compilation
|
||
|
||
Dioxus 0.7 fullstack project with **two independent gates** — the most common source of compilation errors.
|
||
|
||
| Gate | Applies to | Used for |
|
||
|------|-----------|----------|
|
||
| `#[cfg(feature = "server")]` | Server binary only | DB, env loading, background tasks, server function bodies, highlight, WebP, caching |
|
||
| `#[cfg(target_arch = "wasm32")]` | WASM frontend only | localStorage, DOM APIs, web_sys calls, theme detection |
|
||
|
||
**Critical**: Both default features (`web` + `server`) are enabled in `Cargo.toml`. The `dx` CLI handles feature selection during builds.
|
||
|
||
**Stub pattern**: `src/db/mod.rs` provides a `DummyPool` when `server` feature is disabled — do not remove.
|
||
|
||
**Server-only helpers**: `src/auth/password.rs`, `src/auth/session.rs`, `src/api/auth.rs`, `src/api/comments/helpers.rs`, and several model helper methods are gated with `#[cfg(feature = "server")]` because they are only called from server function bodies, which are stripped in WASM builds.
|
||
|
||
**Server-only dependencies**: Crates that are only used behind `#[cfg(feature = "server")]` (e.g., `argon2`, `uuid`, `regex`, `pulldown-cmark`, `rand`, `http`, `sha2`, `hex`, plus the pre-existing optional server stack) are declared as `optional = true` in `Cargo.toml` and enabled only through the `server` feature. They are not compiled into the WASM frontend.
|
||
|
||
## Dual API Architecture
|
||
|
||
The server exposes two distinct API patterns:
|
||
|
||
1. **Dioxus server functions** (`#[server(Name, "/api")]` in `src/api/`) — auto-routed, callable from both client and server Rust. Spread across `src/api/auth.rs`, `src/api/posts/`, `src/api/comments/`, and `src/api/settings.rs`.
|
||
|
||
2. **Axum routes** (registered in `src/main.rs`) — manual `axum::Router` for endpoints that don't fit the server-function model:
|
||
- `POST /api/upload` — image upload (multipart, auth-required, rate-limited)
|
||
- `GET /uploads/{*path}` — image serving with on-the-fly resize/rotate/convert (query params: `w`, `h`, `thumb`, `rotate`, `format`, `quality`)
|
||
|
||
## Server Module Structure
|
||
|
||
```
|
||
src/api/ — server functions + Axum handlers
|
||
auth.rs — login, register, session validation
|
||
comments/ — comment CRUD + approval/spam/trash server functions
|
||
markdown.rs — Markdown→HTML rendering (pulldown-cmark + ammonia sanitization)
|
||
image.rs — image serving with processing pipeline + disk+memory cache
|
||
upload.rs — image upload, auto-converts to WebP
|
||
rate_limit.rs — governor-based rate limiting (5 tiers: strict/upload/image/comment/unknown)
|
||
settings.rs — site settings server functions (trash retention, etc.)
|
||
slug.rs — URL slug generation
|
||
posts/ — CRUD server functions for blog posts
|
||
src/auth/ — password hashing (Argon2) + session token management
|
||
src/bin/ — generate_highlight_css (build-time CSS generation)
|
||
src/cache.rs — moka future-based caches for posts, tags, stats
|
||
src/components/ — Dioxus UI components
|
||
src/db/ — PostgreSQL pool (deadpool-postgres, LazyLock global)
|
||
src/hooks/ — shared Dioxus hooks
|
||
src/models/ — Post, User, Tag data models
|
||
src/pages/ — route page components (frontend + admin)
|
||
src/tasks/ — background tokio tasks (session cleanup)
|
||
src/theme.rs — light/dark theme with SSR cookie + WASM localStorage
|
||
src/webp.rs — zenwebp encode/decode (image crate has no WebP)
|
||
```
|
||
|
||
## Tiptap Editor Subproject
|
||
|
||
Rich-text editor in `libs/tiptap-editor/`, built as an IIFE library exposing `window.TiptapEditor`.
|
||
|
||
- Output: `public/tiptap/`
|
||
- `make build` runs `npm ci --include=dev && npm run build` inside `libs/tiptap-editor`; the `build` script is `tsc --noEmit && vite build` (Vite 8 / Rolldown, type-check before bundle)
|
||
- Unit tests: `npm test` (Vitest 4 + happy-dom), covering `UploadCoordinator` counts/lifecycle, `UploadImageNodeView` rendering/callbacks, and `isValidUrl`
|
||
- `src/pages/admin/write.rs` initializes via `src/tiptap_bridge.rs` (wasm-bindgen bindings): injects `Closure` callbacks (`onUpdate`/`onReady`/`onUploadEvent`/`onImageUpload`) into `TiptapEditor.create`, holds the instance + closures in `EditorHandle` (Drop calls `destroy()`). No `js_sys::eval`, no `window` globals, no polling.
|
||
|
||
Do not edit `public/tiptap/` — they are build artifacts.
|
||
|
||
## Lightbox Subproject
|
||
|
||
Image lightbox (click-to-zoom) in `libs/lightbox/`, built as an IIFE library. Unlike the Tiptap editor, it is **not** wired through wasm-bindgen; the built `lightbox.js` is injected globally via `Dioxus.toml` (`script = ["/lightbox/lightbox.js"]`), and `src/components/post/post_content.rs` sets `window.__lightboxSelectors` (e.g. `['.post-content', '.entry-cover']`) before the script loads. The IIFE tail reads this config and self-initializes; `post_content.rs` also calls it directly as a fallback if the script was already loaded.
|
||
|
||
- Output: `public/lightbox/` (`lightbox.js`, `lightbox.css`, `lightbox.js.map`)
|
||
- `make build` runs `npm ci --include=dev && npm run build` inside `libs/lightbox`; the `build` script is `tsc --noEmit && vite build`
|
||
- Unit tests: `npm test` (Vitest, 23 tests), covering `geometry` math and `lightbox` rendering/lifecycle
|
||
|
||
Do not edit `public/lightbox/` — they are build artifacts.
|
||
|
||
## Yggdrasil-Core Subproject
|
||
|
||
Core JavaScript bundle in `libs/yggdrasil-core/`, built as an IIFE library exposing `window.__initPostContent` and `window.__startThemeTransition` (and future core JS entry points). This is the designated home for all new core JS — add to it rather than creating new `public/js/` scripts.
|
||
|
||
- Output: `public/yggdrasil-core/` (`yggdrasil-core.js`, `yggdrasil-core.css`, `yggdrasil-core.js.map`)
|
||
- `make build` runs `npm ci --include=dev && npm run build` inside `libs/yggdrasil-core`; the `build` script is `tsc --noEmit && vite build` (Vite 8 / Rolldown, type-check before bundle). Source is ES module, output is IIFE (`formats: ['iife']` in `vite.config.ts`) because Dioxus `[web.resource] script` injects bare `<script src>` without `type="module"` support.
|
||
- Injected globally via `Dioxus.toml` (`script` + `style` arrays). Rust calls the entry points via `js_sys::eval("window.__xxx(...)")` with an `if (window.__xxx)` guard to survive script-load ordering races.
|
||
- Unit tests: `npm test` (Vitest 4 + happy-dom), covering `post-content` copy-button lifecycle and `theme-transition` fallback paths (happy-dom lacks `startViewTransition`, naturally covering the degradation path).
|
||
- Theme reveal animation uses the View Transitions API: JS synchronously toggles the `dark` class inside `startViewTransition`'s callback (so the browser snapshots the new state), CSS `@keyframes tt-reveal` expands `::view-transition-new(root)` via `clip-path: circle()` from the click point; Rust's `theme.set()` aligns state afterward (idempotent — `use_effect` sets the same class again). `prefers-reduced-motion` and browsers without VT fall back to instant switch.
|
||
|
||
Do not edit `public/yggdrasil-core/` — they are build artifacts.
|
||
|
||
## CodeMirror Editor Subproject
|
||
|
||
CodeMirror 6 code editor in `libs/codemirror-editor/`, built as an IIFE library exposing `window.CodeMirrorEditor` (object literal `{ create(containerId, options) }`) and `window.EditorOptions` (a class so `new EditorOptions()` survives TS erasure from wasm). Mirrors the tiptap-editor packaging pattern exactly.
|
||
|
||
- Output: `public/codemirror/` (`editor.js`, `editor.js.map`). No CSS file — themes are JS `Extension`s from `@catppuccin/codemirror` (Latte light / Mocha dark, matching the syntax-highlighting `themes/`).
|
||
- `make build` runs `pnpm ci --include=dev && pnpm run build` inside `libs/codemirror-editor`; the `build` script is `tsc --noEmit && vite build` (Vite 8 / Rolldown, type-check before bundle). Output is IIFE (`formats: ['iife']`, `exports: 'default'`).
|
||
- Features: `@codemirror/lang-sql` with live schema autocomplete (schema injected via `set_schema` from `get_db_schema` server function), `@replit/codemirror-vim` keymap (injected before other keymaps), Catppuccin theme hot-swap via `Compartment.reconfigure` (no instance rebuild — preserves Vim state/cursor/undo).
|
||
- Rust bridge: `src/codemirror_bridge.rs` mirrors `src/tiptap_bridge.rs` — `get_module()` uses `js_sys::Reflect::get` + `unchecked_into` (object literal, not a constructor, so NOT an extern fn call and NOT `dyn_into`); `EditorHandle` holds the instance + all `Closure`s; `impl Drop` calls `destroy()`.
|
||
- Shared data types `SqlSchema`/`SqlTable` (serde) compile on both targets; the wasm-bindgen externs live in a `#[cfg(target_arch = "wasm32")] mod wasm`.
|
||
- Unit tests: `pnpm test` (Vitest 4 + happy-dom), covering getValue/setValue, setTheme (Compartment), setSchema, vim toggle, onChange.
|
||
- Injected via `Dioxus.toml` `script` array (`/codemirror/editor.js`).
|
||
|
||
Do not edit `public/codemirror/` — they are build artifacts.
|
||
|
||
## Database Management (`/admin/system`)
|
||
|
||
Admin area at `/admin/system` (menu "系统") with 5 tabs: 数据库状态 / 服务器状态 / SQL 控制台 / 数据导出 / 备份恢复. All gated by `get_current_admin_user` (admin-only). Backend in `src/api/database/` (status/system_status/sql_console/schema/export/backup/tasks), page in `src/pages/admin/system.rs`.
|
||
|
||
- **SQL 控制台** is full read-write with 4 guards: (1) `sqlparser` AST gates — `DROP DATABASE`/`DROP SCHEMA`/`CREATE DATABASE` absolutely forbidden (string pre-check); `DROP`/`TRUNCATE`/`ALTER` require a `confirm_dangerous` checkbox; (2) `UPDATE`/`DELETE` without `WHERE` rejected; (3) `STATEMENT_TIMEOUT_SECS` query timeout (pool-level GUC); (4) frontend write-confirm dialog. Multi-statement disabled by default. Results capped at 500 rows.
|
||
- **备份恢复** uses `dashmap` task-progress table; `create_backup`/`restore_backup` return a task_id immediately and poll `get_task_progress`. Backup prefers `pg_dump` (full, incl. schema), falls back to per-table `COPY TO STDOUT` (data only) when `pg_dump` is unavailable. Backup files carry a `-- YGGDRASIL BACKUP v1` signature header; restore rejects non-system files. `backups/` is gitignored and served only via `GET /api/database/backups/{filename}` (admin-gated, path-allowlist).
|
||
- **服务器状态** uses `sysinfo` (optional, server feature) with a background sampler (`SYSINFO_SAMPLE_SECS`, default 0.5s) writing to a `RwLock<SystemSnapshot>`; server functions read the snapshot (zero sampling cost), so frontend can poll high-frequency. `src/cache.rs` exposes moka hit-rate via `AtomicU64` hit/miss counters per cache + `cache_stats()`.
|
||
- New env var: `SYSINFO_SAMPLE_SECS` (sysinfo sampling interval in seconds, supports decimals).
|
||
- New deps (all `optional = true`, server feature): `sysinfo`, `sqlparser`, `dashmap`.
|
||
|
||
## Syntax Highlighting Pipeline
|
||
|
||
- `themes/` contains Catppuccin Latte (light) and Mocha (dark) `.tmTheme` files
|
||
- `syntaxes/` has custom Sublime syntax definitions (Kotlin, Swift)
|
||
- `src/bin/generate_highlight_css.rs` generates `public/highlight.css` with class-based rules scoped under `.md-content pre code`, with `.dark` prefix for dark mode
|
||
- `src/highlight.rs` uses syntect at runtime for code block highlighting
|
||
- All gated behind `#[cfg(feature = "server")]`
|
||
|
||
## Auth & Session
|
||
|
||
- **Registration**: first user becomes `admin`; subsequent registrations rejected with `"Registration is closed"`
|
||
- **Login**: sets an HttpOnly cookie via `FullstackContext::add_response_header`
|
||
- **Session validation**: `get_current_user` reads `session` cookie, queries `sessions` + `users` tables
|
||
- **Background cleanup**: `tasks::session_cleanup::run_cleanup()` deletes expired sessions every hour
|
||
|
||
## Caching
|
||
|
||
- **Post/tag caches** (`src/cache.rs`): moka future-based, TTL varies by data type (60s–600s). Invalidated on writes.
|
||
- **Image processing cache** (`src/api/image.rs`): two-tier — in-memory moka cache + disk cache in `uploads/.cache/`. Keyed by path + query params.
|
||
|
||
## Testing
|
||
|
||
```bash
|
||
make test # cargo test (Rust, 402 tests) + vitest (tiptap-editor 46 tests, lightbox 23 tests, yggdrasil-core 9 tests)
|
||
dx check # Dioxus type-check (catches component/Router issues)
|
||
cargo clippy # lint
|
||
```
|
||
|
||
Most tests use `#[cfg(all(test, feature = "server"))]` — they only run when the server feature is active (which is the default). No integration tests requiring a database connection. The two `libs/` subprojects (tiptap-editor, lightbox) run their own vitest suites.
|
||
|
||
## Image Processing Constraints
|
||
|
||
- The `image` crate is configured **without** WebP support (`default-features = false, features = ["jpeg", "png", "gif"]`). Do not add WebP to the image crate features.
|
||
- All WebP encode/decode goes through `zenwebp` via `src/webp.rs`.
|
||
- Upload pipeline auto-converts non-GIF/non-WebP images to WebP, keeping original format if WebP is larger.
|
||
- Image serving supports on-the-fly resize (`w`, `h`), thumbnail (`thumb=WxH`), rotation (90/180/270), and format conversion.
|
||
|
||
## Build Artifacts (gitignored)
|
||
|
||
- `public/style.css` — Tailwind output
|
||
- `public/highlight.css` — generated by `generate_highlight_css` binary
|
||
- `public/tiptap/` — Vite build output (editor)
|
||
- `public/lightbox/` — Vite build output (lightbox)
|
||
- `public/yggdrasil-core/` — Vite build output (core JS bundle)
|
||
- `/dist`, `/.dioxus`, `/target`
|
||
- `node_modules` (inside `libs/tiptap-editor/`, `libs/lightbox/`, and `libs/yggdrasil-core/`)
|
||
- `uploads/.cache/` — image processing disk cache
|
||
|
||
## Notes
|
||
|
||
- `rand` is optional and only enabled by the `server` feature; it is not compiled into the WASM frontend.
|
||
- `#[allow(unused_mut, unused_variables)]` on `Write` component is intentional — `mut` signals are used in `#[cfg(target_arch = "wasm32")]` blocks stripped in server builds.
|
||
- Server uses incremental rendering with 300s cache (`IncrementalRendererConfig` in `src/main.rs`).
|