- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
10 KiB
Architecture Overview
NomiFun is built around a single principle: one Rust backend, two host modes,
one frontend. Whether you launch the desktop product NomiFun or self-host the
web server, the same axum HTTP/WS server (nomifun-app, binary nomicore)
executes inside the host process. The React 19 SPA in ui/ is the only client,
and it always speaks plain HTTP and WebSocket — no Electron preload, no Tauri
custom protocol.
This document is the map. The four siblings drill into the parts:
backend-crates.md— the 29nomifun-*backend crates.agent-engine.md— the 15nomi-*agent crates.frontend.md— the React SPA, adapter layer, routing.communication.md— HTTP / WebSocket / Tauri IPC / ACP / MCP.data-and-storage.md— SQLite, workspaces, runtimes.
The two-host model
┌─────────────────────────────────────┐
│ ui/ React 19 SPA (Vite build) │
│ HashRouter · SWR · Arco · UnoCSS │
│ http://127.0.0.1:<port>/api + /ws│
└─────────────────────────────────────┘
▲ ▲
HTTP/REST│ WebSocket│ /ws
│ │
┌───────────────── desktop ─────┴────┐ ┌─────── web ───────┴──────┐
│ apps/desktop (nomifun-desktop) │ │ apps/web (nomifun-web) │
│ Tauri 2 shell · WebView2/WKWebKit │ │ standalone axum server │
│ ─ thread "nomifun-backend" │ │ serves /api + /ws │
│ └ tokio · nomifun_app embedded │ │ + ServeDir(ui/dist) SPA │
│ picks free localhost port, │ │ port 8787 (default) │
│ injects window.__backendPort │ │ authenticated by default│
│ uses TrustLocalToken auth │ │ --insecure-no-auth opts │
│ injects x-nomi-local-trust │ │ into no-auth mode │
│ Tauri commands for desktop shell │ │ serves SPA as fallback │
└────────────────────────────────────┘ └──────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────┐
│ nomifun-app (binary nomicore) │
│ composition root · axum router │
│ bootstrap → data layer → services │
│ /api · /ws · public /mcp · /v1 │
└─────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ nomifun-* (29) │ │ nomi-* (15) │
│ backend crates │◀─▶│ agent engine crates │
│ data, auth, MCP, │ │ via the SEAM: │
│ conversation, etc. │ │ nomifun-ai-agent │
└─────────────────────┘ └─────────────────────┘
│
├─▶ SQLite (sqlx) see data-and-storage.md
├─▶ ACP agent CLIs see agent-engine.md
├─▶ MCP stdio bridges see communication.md
└─▶ bundled bun runtime see data-and-storage.md
How a request flows
A typical user message — "send a chat to my Claude agent in conversation X" — crosses every layer in the diagram. The trace below names the real types and files that participate.
1. UI keypress → React handler
ui/src/renderer/pages/conversation/...
calls ipcBridge.conversation.sendMessage.invoke(...)
(a thin wrapper produced by the adapter factory in ui/src/common/adapter)
2. httpBridge → fetch
ui/src/common/adapter/httpBridge.ts
POST http://127.0.0.1:<port>/api/conversations/{id}/messages
In WebUI mode, the CSRF cookie is echoed into x-csrf-token (double-submit).
3. axum router (composition root)
crates/backend/nomifun-app/src/router/ — assembled in create_router()
middlewares: trace, body-limit, CORS, auth, CSRF, rate-limit, response wrapper
4. Conversation service
crates/backend/nomifun-conversation/src/service.rs
persists the message, looks up the conversation's bound agent
5. Agent seam
crates/backend/nomifun-ai-agent — the primary backend bridge to nomi-*
AgentRegistry / WorkerTaskManager dispatches to the right agent kind
6. Agent run
nomi-agent drives the engine: providers (anthropic/openai/bedrock/vertex),
tools (bash/read/write/...), MCP servers, skills, plan/confirm/output sinks
For ACP-protocol agents (Claude Code, Codex, Gemini CLI, ...), the backend
speaks ACP over stdio to a child process spawned with the bundled runtime
7. Streaming back to the UI
nomifun-realtime broadcasts each token as a WS event over /ws
ui/src/common/adapter/httpBridge.ts ensureWs() routes events to listeners
8. UI renders the streaming reply (react-markdown + KaTeX + mermaid)
The three crate groups
The Cargo workspace (root Cargo.toml, resolver = "3",
edition = "2024") is grouped into three folders so the boundaries are visible
on disk, not just in package names:
| Folder | Purpose | Crate prefix | Count |
|---|---|---|---|
crates/agent/ |
AI engine — providers, tools, sessions, MCP, skills, computer/browser use | nomi-* |
15 |
crates/backend/ |
The HTTP/WS server, data, auth, features, public capability gateway | nomifun-* |
29 |
crates/shared/ |
Cross-layer utilities used by both groups | mixed | 2 |
The agent group is self-contained — no nomi-* crate references any
nomifun-* crate, the workspace root, or frameworks like Tauri / sqlx / axum.
The reverse direction normally goes through nomifun-ai-agent, which re-exports
nomi_config, nomi_types, and RequirementSink for backend consumers.
nomifun-app and nomifun-gateway have feature-gated direct dependencies for
browser/computer bridge surfaces; those are documented exceptions, not the
default pattern.
What lives where
nomifun-tauri/
├─ apps/
│ ├─ desktop/ nomifun-desktop (Tauri 2 shell, this is "NomiFun" the product)
│ └─ web/ nomifun-web (standalone server: /api + SPA on one port)
├─ crates/
│ ├─ agent/ 15 nomi-* crates → see agent-engine.md
│ ├─ backend/ 29 nomifun-* crates → see backend-crates.md
│ └─ shared/ 2 shared crates
├─ ui/ React 19 + Vite 6 + Arco + UnoCSS → see frontend.md
└─ docs/
├─ architecture/ (this folder)
└─ specs/ dated engineering design specs
Brand and identifiers
- NomiFun — the desktop product and project / brand wordmark (camelCase, capital N and F). "NomiFun is an AI Workstation (desktop app plus self-hosted web server)."
- The lowercase
nomifunis reserved for technical identifiers only — the npm/JS package id, the Rust crate prefixnomifun-*, the Tauri bundle identifiercom.nomifun.desktop, environment variablesNOMIFUN_*, and repository / directory names.
Hosts at a glance
| Aspect | Desktop (nomifun-desktop) |
Web (nomifun-web) |
|---|---|---|
| Binary | nomifun-desktop (Tauri shell) |
nomifun-web (axum server) |
| Backend | embedded in-process (own thread + tokio runtime) | embedded in-process |
| Auth mode | TrustLocalToken: the desktop webview receives a per-boot secret and sends it as x-nomi-local-trust |
required by default; opt-out via --insecure-no-auth |
| Port | a free localhost port chosen at boot (bind 127.0.0.1:0) |
127.0.0.1:8787 (configurable via --host/--port) |
| Backend port reaches the SPA via | initialization script window.__backendPort = <p> |
same-origin (/api and /ws served on the same port as the SPA) |
| Static SPA | bundled into the Tauri app (tauri.conf.json distDir) |
served by tower_http::services::ServeDir from ui/dist |
| OS-shell features | window controls, deep-link, updater, autostart, dialog, notification, single-instance | none — browser is the host |
| Tauri commands | update check, companion-window sync, WebUI LAN status/start/stop, keep-awake, tray labels | not applicable |
The desktop also has an optional LAN WebUI listener controlled by Tauri commands
(webui_start, webui_stop, webui_get_status). That listener is separate
from the loopback listener used by the desktop's own webview.
The desktop binary's main.rs (apps/desktop/src/main.rs)
is intentionally short — the bulk of the logic is nomifun_app::run_embedded_server.
The web binary (apps/web/src/main.rs) reuses the
same boot helpers (init_environment, init_data_layer, AppServices::from_config,
create_router) and adds the SPA fallback plus first-run admin provisioning
(ensure_admin_credentials).
The full app router also exposes companion-token authenticated public fronts at
/mcp, /mcp-agent, and /v1. These are intentionally separate from the
normal /api browser-auth tree and are mounted in
crates/backend/nomifun-app/src/router/routes.rs.