Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,295 @@
# Web Server Deployment
`nomifun-web` is the **headless, self-host** way to run NomiFun. It is the same Rust backend that the [desktop app](./desktop-app.md) embeds, but built as a standalone binary that also serves the SPA (`ui/dist`) on the same port. There is no GUI, no WebView, no `DISPLAY` requirement — it runs anywhere a Linux/macOS/Windows server will run a static binary.
Unlike the desktop shell, **`nomifun-web` requires authentication by default**. The first browser visitor either creates the admin account interactively (first-run setup), or you pre-seed credentials with `NOMIFUN_ADMIN_PASSWORD`.
> If you want to expose an *existing* desktop install for remote access without setting up a server, see [WebUI Remote Access](./webui-remote-access.md). That is a per-instance feature; this guide is for a dedicated server.
```text
Browser / phone / LAN nomifun-web (one process, one port)
┌──────────────────┐ ┌───────────────────────────────────────┐
│ SPA + login │ HTTP / WS │ axum router │
│ (ui/dist) │ ────────────► │ ├─ / → SPA (ui/dist) │
└──────────────────┘ │ ├─ /api/* → REST handlers │
│ ├─ /ws → WebSocket events │
│ └─ /login … → auth (on by default)│
│ │
│ embedded backend (nomifun-app) │
│ └─ SQLite · agents · cron · channels │
└───────────────────────────────────────┘
```
## Quick start
### Run the binary directly
```bash
cargo build --release -p nomifun-web
./target/release/nomifun-web --host 127.0.0.1 --port 8787 \
--data-dir ./data --dist ./ui/dist
```
Then open `http://127.0.0.1:8787` and the first visit lets you create the admin account. After that, the setup endpoint returns `409 Conflict` and the only way in is via the login form (or `NOMIFUN_ADMIN_PASSWORD`).
![First-run admin setup screen](../images/webserver-02-first-run-setup.png)
### Or via Cargo, from the repo
```bash
bun install
bun run build:ui # produces ui/dist
cargo run -p nomifun-web # picks up the default --dist=../../ui/dist
```
## CLI flags and environment variables
All flags below are read by `apps/web/src/main.rs`. Each has an environment-variable counterpart for systemd / Docker / orchestrators.
| Flag | Env var | Default | Purpose |
|---|---|---|---|
| `--host` | `NOMIFUN_WEB_HOST` | `127.0.0.1` | IP to bind on. `0.0.0.0` accepts LAN/VPN/public traffic; pre-seed the admin or complete first-run setup before broad exposure. |
| `--port` | `NOMIFUN_WEB_PORT` | `8787` | TCP port. Serves the API, the WebSocket at `/ws`, and the SPA. |
| `--data-dir` | `NOMIFUN_DATA_DIR` | per-user dir | Backend data dir (SQLite database, agent state, logs, Bun cache). Defaults to the per-user location shared with the desktop app (`%LOCALAPPDATA%\NomiFun\Nomi`, `~/Library/Application Support/NomiFun/Nomi`, `$XDG_DATA_HOME/NomiFun/Nomi`). **Still set an explicit absolute path in production.** |
| `--dist` | `NOMIFUN_WEB_DIST` | `../../ui/dist` | Directory containing the built SPA. **Set this explicitly when deploying.** |
| `--admin-user` | `NOMIFUN_ADMIN_USERNAME` | `admin` | Username used when pre-seeding the first admin. Ignored once an admin exists. |
| `--admin-password` | `NOMIFUN_ADMIN_PASSWORD` | — | Pre-seed the first admin password at boot, skipping interactive setup. Ignored once an admin exists. |
| `--insecure-no-auth` | `NOMIFUN_WEB_INSECURE_NO_AUTH` | `false` | **DANGER.** Disables authentication entirely (desktop-style local mode). Only use on loopback or a fully trusted private network. |
| — | `NOMIFUN_HTTPS` | `false` | When `true`, session and CSRF cookies are flagged `Secure`. Set this whenever the app is reached over HTTPS (e.g. behind a TLS reverse proxy). |
| — | `SHELL` | platform default | Shell used by the agent engine when spawning processes. Set to `/bin/bash` on Linux servers if `$SHELL` is unset. |
Boolean envs accept `1`, `true`, `yes`, `on` (case-insensitive).
A bad `--host` (anything that does not parse as an IP) fails fast at startup with a clear error rather than a cryptic socket error.
At startup the backend takes an OS-level exclusive lock on `{data_dir}/server.lock`**one backend instance per data dir**. A second process pointed at the same directory fails fast with an error naming the current holder (pid + exe); to deploy multiple instances, give each its own `NOMIFUN_DATA_DIR` / `--data-dir`. The OS releases the lock on exit or crash, so a leftover `server.lock` file is harmless.
### Password and username rules
When the admin account is created (interactively or via pre-seed), values are validated server-side:
- **Username**: 332 chars, `[a-zA-Z0-9_-]`, must not start or end with `-` / `_`.
- **Password**: 8128 chars, rejected if it appears in a small common-passwords list (`password`, `12345678`, `qwertyui`, …).
A weak `NOMIFUN_ADMIN_PASSWORD` will refuse to boot. A weak interactively-typed password will return `400` with the validation message.
## First-run admin provisioning
There are two supported paths.
### Interactive (default)
Leave `NOMIFUN_ADMIN_PASSWORD` unset. On a fresh data dir the install is "uninitialised": `GET /api/auth/status` reports `needs_setup: true`, the SPA shows the first-run form, and the **first browser visitor's chosen username + password become the admin** via an atomic `POST /api/auth/setup`. The write is a conditional UPDATE — even two concurrent first-run requests cannot both win; the loser receives `409 Conflict`.
> **Security note — the first-run window.** Between the moment the server is reachable and the moment you complete setup, anyone who can reach the port can claim the admin account. On a non-loopback bind the server logs a loud warning. Mitigate by completing setup over a trusted tunnel/VPN first, or pre-seed (next section) so the install is initialised before it goes live.
### Pre-seeded (recommended for automation)
Provide `NOMIFUN_ADMIN_PASSWORD` (and optionally `NOMIFUN_ADMIN_USERNAME`, default `admin`) before first boot. The bootstrap routine hashes and stores the credentials atomically, the first-run setup endpoint returns `409` from the very first start, and there is no window for someone else to claim the account.
```bash
NOMIFUN_ADMIN_USERNAME=alice \
NOMIFUN_ADMIN_PASSWORD='change-me-to-something-strong' \
nomifun-web --host 0.0.0.0 --port 8787 \
--data-dir /var/lib/nomifun --dist /opt/nomifun/web
```
The pre-seed is **idempotent** — once an admin exists, the env vars are ignored on subsequent boots. To rotate credentials, use the in-app change-password / change-username flow rather than the env vars.
## Docker
The repo ships a multi-stage `Dockerfile` and a `docker-compose.yml`. The image:
1. Builds the SPA with Bun.
2. Compiles `nomifun-web` from the workspace.
3. Assembles a slim `debian:bookworm-slim` runtime that includes `bun`, `git`, and `ripgrep`.
It exposes port `8787` and uses `/data` as the data volume.
### Compose
```bash
docker compose up -d --build
# then open http://<server-ip>:8787 and create the first admin
```
`restart: unless-stopped` makes the service start on host boot — installing it *is* enabling it. The default ports block publishes `8787:8787` directly; pre-seed the admin or complete setup on a trusted network before exposing it broadly. Add TLS (next section) before exposing to the internet.
Verify readiness:
```bash
docker compose logs -f nomifun
# look for: "nomifun-web: embedded backend + SPA on one port"
```
The compose file mounts a named volume `nomifun-data:/data` which holds the SQLite DB, logs, the Bun runtime cache, and per-agent state. Back this up with the same care as any other database.
### Pre-seeding the admin in Compose
Uncomment the `environment:` block:
```yaml
environment:
NOMIFUN_ADMIN_USERNAME: admin
NOMIFUN_ADMIN_PASSWORD: "change-me-to-something-strong"
NOMIFUN_HTTPS: "true" # when fronted by Caddy / nginx with TLS
```
### Building behind a slow registry
The Rust stage accepts a `CARGO_REGISTRY_MIRROR` build arg for cargo registry mirroring (e.g. on a network where crates.io is slow):
```bash
docker build --build-arg CARGO_REGISTRY_MIRROR=https://rsproxy.cn/index/ -t nomifun-web:local .
```
```text
$ docker compose up -d
[+] Running 2/2
✔ Network nomifun_default Created
✔ Container nomifun-web Started
$ docker compose logs -f web
nomifun-web | listening on 0.0.0.0:8787 (auth: enabled)
```
## TLS via Caddy reverse proxy
A `Caddyfile` is included for Caddy 2. Caddy auto-provisions HTTPS certificates (Let's Encrypt or ZeroSSL by default) and proxies to the app. The WebSocket upgrade at `/ws` passes through automatically, no extra config required.
```caddy
your.domain.com {
encode zstd gzip
reverse_proxy nomifun:8787
}
```
To enable the Caddy service in `docker-compose.yml`:
1. Edit `Caddyfile` and replace `your.domain.com` with your real domain.
2. Set `NOMIFUN_HTTPS=true` in the `nomifun` service env (so cookies get the `Secure` flag).
3. Replace `ports: ["8787:8787"]` with `expose: ["8787"]` so only Caddy is published.
4. Uncomment the `caddy:` service and the `caddy-data` / `caddy-config` volumes.
5. `docker compose up -d`.
The app already provides its own login screen, so **do not configure HTTP basic auth in Caddy** — Caddy's job is purely TLS termination and proxying.
For a LAN-only host without a public domain you can use an internal name with `tls internal`, or just publish port `8787` directly without Caddy (the in-app login still protects it).
## systemd (Linux server, no Docker)
The repo includes `packaging/linux/nomifun-web.service` and a long-form Linux deployment guide at `packaging/linux/README.md`.
### Build artifacts
You need a Linux build host (cross-compiling the C dependencies from Windows is painful — the easiest workaround is to extract the binary from the Docker image with `docker cp`). On Linux:
```bash
bun install
bun run build:ui # → ui/dist (~21MB)
cargo build --release -p nomifun-web # → target/release/nomifun-web
```
### Layout
```
/opt/nomifun/nomifun-web # the binary
/opt/nomifun/web/ # contents of ui/dist
/var/lib/nomifun/ # data dir (created by systemd's StateDirectory)
```
```bash
sudo useradd --system --home /var/lib/nomifun --shell /usr/sbin/nologin nomifun
sudo mkdir -p /opt/nomifun/web
sudo cp target/release/nomifun-web /opt/nomifun/
sudo cp -r ui/dist/. /opt/nomifun/web/
```
### Bun must be on the system `PATH`
The agent engine requires **`bun ≥ 1.3.13`** as a runtime dependency. Because the service runs under a `nologin` system account, an install in someone's `~/.bun/bin/` is invisible to it. Pick one:
- **System install**: `curl -fsSL https://bun.sh/install | bash`, then `sudo install ~/.bun/bin/bun /usr/local/bin/bun`.
- **Embed in the binary**: build with `NOMIFUN_EMBED_BUN=1 cargo build --release -p nomifun-web`. Bun is bundled into the binary and self-extracts into the data dir on first run.
Verify: `sudo -u nomifun -s -- which bun` must return a path. Otherwise the first agent spawn will fail with an opaque error.
### Install the unit
```bash
sudo cp packaging/linux/nomifun-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nomifun-web
sudo systemctl status nomifun-web
```
The shipped unit:
- Binds `127.0.0.1:8787` by default. Change `NOMIFUN_WEB_HOST` to
`0.0.0.0` only after first-run setup is complete or
`NOMIFUN_ADMIN_PASSWORD` is configured.
- Sets `NOMIFUN_DATA_DIR=/var/lib/nomifun` to match the systemd-managed `StateDirectory=nomifun`. **Keep these two in sync** — if you drop the env line, the data dir silently falls back to the service user's per-user directory (`$XDG_DATA_HOME/NomiFun/Nomi`, typically `~nomifun/.local/share/NomiFun/Nomi`), decoupled from systemd state.
- Runs as a dedicated `nomifun` user (`User=nomifun`, `Group=nomifun`).
- Restarts on failure with a 3 s backoff.
- Applies moderate hardening (`NoNewPrivileges=yes`, `PrivateTmp=yes`). **Do not add** `ProtectHome=yes` or strict `ProtectSystem` — the agent engine reads/writes operator-directed files and over-sandboxing breaks core features.
To enable HTTPS cookies behind a TLS proxy, uncomment:
```ini
Environment=NOMIFUN_HTTPS=true
```
To pre-seed the admin instead of interactive setup:
```ini
Environment=NOMIFUN_ADMIN_USERNAME=admin
Environment=NOMIFUN_ADMIN_PASSWORD=change-me-to-something-strong
```
```text
$ sudo systemctl status nomifun-web
● nomifun-web.service - NomiFun web host
Loaded: loaded (/etc/systemd/system/nomifun-web.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-06-25 09:12:03 UTC
Main PID: 12345 (nomifun-web)
CGroup: /system.slice/nomifun-web.service
└─12345 /usr/local/bin/nomifun-web --host 127.0.0.1 --port 8787 …
nomifun-web[12345]: listening on 127.0.0.1:8787 (auth: enabled)
```
## Linux runtime dependencies
| Dependency | Required? | Notes |
|---|---|---|
| `glibc` + `ca-certificates` | Yes | sqlite is statically linked, TLS uses rustls — **no openssl, no libsqlite needed**. |
| `bun` ≥ 1.3.13 | **Yes** | Agent execution runtime. 1.1.38 has an stdin bug; do not use. Already inside the Docker image. |
| `node` / `npm` / `npx` | Recommended | Many user-configured MCP stdio servers launch via `npx -y …`. |
| `git` | Recommended | Skill discovery and a few built-in tools. |
| `ripgrep` (`rg`) | Recommended | Code-search backend. Falls back to `grep` if missing. |
| `DISPLAY` / X11 / WebView | **No** | `nomifun-web` is fully headless. |
## Security checklist
- **Use TLS for any public deployment.** Cookies and login credentials over plain HTTP can be sniffed. Behind a TLS proxy, set `NOMIFUN_HTTPS=true` so the session cookie is flagged `Secure`.
- **Strong admin password.** The validator rejects passwords below 8 chars and a few obvious dictionary entries, but it does not enforce a strength score — pick something long and random. Change it from the in-app flow whenever you suspect compromise; the change-password endpoint rotates the JWT secret, invalidating every existing session.
- **Close the first-run window** with `NOMIFUN_ADMIN_PASSWORD` for any host that becomes reachable before you are ready to interactively complete setup. Alternatively keep the service on `127.0.0.1` until setup is finished, then intentionally bind `0.0.0.0`.
- **`--insecure-no-auth` is hostile by default.** It disables authentication completely; *anyone* who can reach the port becomes a privileged user with shell, file, and agent access. Only use on a loopback bind or a fully trusted private network. The server logs a warning when it is enabled on a non-loopback address.
- The backend has terminal, filesystem, and agent execution capabilities — running it remotely is, by design, equivalent to giving yourself remote code execution on the host. Auth + TLS are the floor, not the ceiling. Treat the data dir and the admin password the same way you would treat root credentials.
## Troubleshooting
**`invalid --host '<value>'`.** Pass an IP literal (`127.0.0.1`, `0.0.0.0`, an explicit interface IP). Hostnames are not parsed.
**Cookies don't stick over HTTPS.** Set `NOMIFUN_HTTPS=true` so the `Secure` flag is added. Without it, browsers reject the cookie on HTTPS responses.
**Agent commands fail with `bun: command not found` under systemd.** Install bun system-wide (see the bun-on-PATH section above) or rebuild with `NOMIFUN_EMBED_BUN=1`.
**Healthcheck.** Use `GET /health` for process liveness. Use
`GET /api/auth/status` only when the caller also needs setup/auth state.
## See also
- [Running NomiFun as a Desktop App](./desktop-app.md)
- [WebUI Remote Access](./webui-remote-access.md) — turn an existing desktop install into a remotely-accessible server (without provisioning a separate machine).
- `packaging/linux/README.md` — deeper Linux notes (mostly Chinese; this guide subsumes the English content).
- `apps/web/src/main.rs` — the source of truth for flags, env vars, and bootstrapping order.