Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# Agent Engine
|
||||
|
||||
The agent engine lives under [`crates/agent/`](../../crates/agent/) and is
|
||||
consumed by the backend primarily through
|
||||
[`nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent/). This page is an
|
||||
implementation map for the current workspace, not an extraction plan.
|
||||
|
||||
## Crate Map
|
||||
|
||||
| Crate | Responsibility |
|
||||
| --- | --- |
|
||||
| `nomi-types` | Provider-neutral messages, tool types, compaction types, file state, skill types, and spawner types. |
|
||||
| `nomi-protocol` | Host/agent command and event protocol plus approval state. |
|
||||
| `nomi-compact` | Context compaction and message-window shaping. |
|
||||
| `nomi-config` | Runtime/provider/profile/auth configuration. |
|
||||
| `nomi-providers` | Anthropic, OpenAI-compatible, Bedrock, Vertex, and shared streaming/retry/provider logic. |
|
||||
| `nomi-tools` | Built-in tools and tool registry primitives. |
|
||||
| `nomi-mcp` | MCP client, manager, transports, and tool proxying. |
|
||||
| `nomi-skills` | Skill discovery, frontmatter, loading, and skill-index support. |
|
||||
| `nomi-memory` | Memory storage and retrieval primitives. |
|
||||
| `nomi-agent` | Core engine loop, sessions, compaction glue, confirmations, output sinks, skill tool, requirement tools, and subagent spawning. |
|
||||
| `nomi-cli` | Standalone `nomi` CLI consumer of the engine. |
|
||||
| `nomi-computer` | Desktop computer-use tool implementation. |
|
||||
| `nomi-a11y` | Accessibility helpers for computer-use flows. |
|
||||
| `nomi-browser-engine` | Self-hosted browser/CDP automation engine. |
|
||||
| `nomi-browser` | Browser-use tool facade. |
|
||||
|
||||
The agent crates do not depend on `nomifun-*` backend crates. Backend-to-agent
|
||||
integration normally flows through `nomifun-ai-agent`; feature-gated bridge
|
||||
surfaces in `nomifun-app` and `nomifun-gateway` directly depend on browser and
|
||||
computer-use crates to expose those capabilities as stdio/public tools.
|
||||
|
||||
## Runtime Families
|
||||
|
||||
NomiFun supports several runtime families:
|
||||
|
||||
- **Nomi engine**: in-tree engine from `nomi-agent`, with providers, built-in
|
||||
tools, skills, MCP, memory, browser, and computer-use support.
|
||||
- **ACP-style CLI agents**: Claude Code, Codex, Gemini CLI, Qwen/OpenCode-style
|
||||
integrations, and related CLIs managed by `nomifun-ai-agent`.
|
||||
- **Remote/Open capability surfaces**: external agents connect through
|
||||
companion-token authenticated `/mcp`, `/mcp-agent`, or `/v1` fronts.
|
||||
|
||||
The implementation source of truth for factory behavior is:
|
||||
|
||||
- `crates/backend/nomifun-ai-agent/src/factory/nomi.rs`
|
||||
- `crates/backend/nomifun-ai-agent/src/factory/acp.rs`
|
||||
- `crates/backend/nomifun-ai-agent/src/factory/acp_assembler.rs`
|
||||
|
||||
## MCP And Tool Injection
|
||||
|
||||
MCP/tool availability is assembled per runtime and per session. It is not a
|
||||
single flat list.
|
||||
|
||||
Common sources include:
|
||||
|
||||
- user-configured MCP server rows from `nomifun-mcp`,
|
||||
- requirement declaration tools when AutoWork requires them,
|
||||
- scoped knowledge search when a session has mounted knowledge bases,
|
||||
- Desktop Gateway tools for sessions flagged with desktop-gateway access,
|
||||
- Windows/open helper bridge,
|
||||
- feature-gated computer-use and browser-use stdio bridges,
|
||||
- runtime-native skills or first-message skill injection,
|
||||
- Nomi's native tool registry.
|
||||
|
||||
When documenting tool availability, cite the factory files above rather than
|
||||
assuming all agents receive the same injected servers.
|
||||
|
||||
## Skills
|
||||
|
||||
Skills are instruction/tool bundles whose materialization depends on runtime
|
||||
capability:
|
||||
|
||||
- Nomi has a real `Skill` tool path in the engine.
|
||||
- Native CLI runtimes may receive symlinked/copied skill files or lightweight
|
||||
first-message guidance when the runtime supports it.
|
||||
- Custom workspace or non-native paths can be summarized in a first-message
|
||||
skill index.
|
||||
|
||||
Relevant source files:
|
||||
|
||||
- `crates/backend/nomifun-extension/src/skill_service.rs`
|
||||
- `crates/backend/nomifun-ai-agent/src/capability/skill_manager/mod.rs`
|
||||
- `crates/backend/nomifun-ai-agent/src/capability/first_message_injector.rs`
|
||||
- `crates/agent/nomi-agent/src/skill_tool.rs`
|
||||
|
||||
## Session Flow
|
||||
|
||||
```text
|
||||
UI request
|
||||
-> nomifun-conversation route/service
|
||||
-> nomifun-ai-agent AgentService / WorkerTaskManager
|
||||
-> runtime family factory
|
||||
-> Nomi engine or external CLI process
|
||||
-> AgentStreamEvent
|
||||
-> nomifun-realtime /ws
|
||||
-> renderer stream handlers
|
||||
```
|
||||
|
||||
Nomi-engine sessions run inside the process. ACP-style sessions spawn and manage
|
||||
child CLIs. Public remote capability calls enter through `nomifun-public` and
|
||||
the Desktop Gateway registry rather than the conversation HTTP route.
|
||||
|
||||
## Design Notes
|
||||
|
||||
Older specs describe the agent layer as mechanically extraction-ready and list
|
||||
only 11 crates. Those files are historical. The current code still keeps a
|
||||
strong boundary, but browser/computer bridge work and public gateway surfaces
|
||||
mean the real rule is “primary seam plus documented feature-gated exceptions.”
|
||||
@@ -0,0 +1,104 @@
|
||||
# Agent 引擎
|
||||
|
||||
Agent 引擎位于 [`crates/agent/`](../../crates/agent/),后端主要通过
|
||||
[`nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent/) 消费它。本页是
|
||||
当前 workspace 的实现地图,不再是抽离独立仓库的计划。
|
||||
|
||||
## Crate 地图
|
||||
|
||||
| Crate | 职责 |
|
||||
| --- | --- |
|
||||
| `nomi-types` | Provider 无关的消息、工具类型、压缩类型、文件状态、skill 类型与 spawner 类型。 |
|
||||
| `nomi-protocol` | Host/agent 命令与事件协议,以及工具审批状态。 |
|
||||
| `nomi-compact` | 上下文压缩与消息窗口整理。 |
|
||||
| `nomi-config` | 运行时、provider、profile、auth 配置。 |
|
||||
| `nomi-providers` | Anthropic、OpenAI-compatible、Bedrock、Vertex,以及共享的流式、重试、provider 逻辑。 |
|
||||
| `nomi-tools` | 内置工具与工具注册表原语。 |
|
||||
| `nomi-mcp` | MCP client、manager、transports 与工具代理。 |
|
||||
| `nomi-skills` | Skill 发现、frontmatter、加载与 skill-index 支持。 |
|
||||
| `nomi-memory` | 记忆存储与检索原语。 |
|
||||
| `nomi-agent` | 核心 engine loop、session、压缩粘合、confirmations、output sinks、skill tool、requirement tools 与 subagent spawning。 |
|
||||
| `nomi-cli` | 使用同一引擎的独立 `nomi` CLI。 |
|
||||
| `nomi-computer` | 桌面 computer-use 工具实现。 |
|
||||
| `nomi-a11y` | computer-use 流程使用的 accessibility helper。 |
|
||||
| `nomi-browser-engine` | 自托管 browser/CDP 自动化引擎。 |
|
||||
| `nomi-browser` | Browser-use 工具 facade。 |
|
||||
|
||||
Agent crates 不依赖 `nomifun-*` 后端 crate。常规的后端到 agent 集成通过
|
||||
`nomifun-ai-agent` 进入;`nomifun-app` 与 `nomifun-gateway` 中 feature-gated
|
||||
的桥接面会直接依赖 browser/computer-use crate,以便把这些能力暴露为 stdio
|
||||
或公开工具。
|
||||
|
||||
## Runtime Families
|
||||
|
||||
NomiFun 支持几类运行时:
|
||||
|
||||
- **Nomi engine**:来自 `nomi-agent` 的仓内引擎,带 provider、内置工具、
|
||||
skills、MCP、memory、browser 与 computer-use 支持。
|
||||
- **ACP-style CLI agents**:Claude Code、Codex、Gemini CLI、Qwen/OpenCode
|
||||
风格集成及相关 CLI,由 `nomifun-ai-agent` 管理。
|
||||
- **Remote/Open capability surfaces**:外部 agent 通过 companion-token 认证的
|
||||
`/mcp`、`/mcp-agent` 或 `/v1` 入口连接。
|
||||
|
||||
Factory 行为的源码真相来源:
|
||||
|
||||
- `crates/backend/nomifun-ai-agent/src/factory/nomi.rs`
|
||||
- `crates/backend/nomifun-ai-agent/src/factory/acp.rs`
|
||||
- `crates/backend/nomifun-ai-agent/src/factory/acp_assembler.rs`
|
||||
|
||||
## MCP 与工具注入
|
||||
|
||||
MCP / tool 可用性按运行时与 session 组装,不是一张全局扁平列表。
|
||||
|
||||
常见来源包括:
|
||||
|
||||
- 来自 `nomifun-mcp` 的用户配置 MCP server 行;
|
||||
- AutoWork 需要时注入的 requirement declaration tools;
|
||||
- session 绑定知识库时注入的 scoped knowledge search;
|
||||
- 带 desktop-gateway 权限的 session 使用的 Desktop Gateway tools;
|
||||
- Windows/open helper bridge;
|
||||
- feature-gated computer-use 与 browser-use stdio bridges;
|
||||
- runtime-native skills 或 first-message skill injection;
|
||||
- Nomi 原生工具注册表。
|
||||
|
||||
记录工具可用性时应引用上面的 factory 文件,不要假设所有 agent 都拿到同一组
|
||||
injected servers。
|
||||
|
||||
## Skills
|
||||
|
||||
Skills 是 instruction/tool bundle,其物化方式取决于运行时能力:
|
||||
|
||||
- Nomi 在引擎内有真实的 `Skill` tool 路径。
|
||||
- Native CLI 运行时可能接收 symlink/copy 出来的 skill 文件,或在支持较弱时接收
|
||||
first-message guidance。
|
||||
- Custom workspace 或非 native 路径可以收到 first-message skill index 摘要。
|
||||
|
||||
相关源码:
|
||||
|
||||
- `crates/backend/nomifun-extension/src/skill_service.rs`
|
||||
- `crates/backend/nomifun-ai-agent/src/capability/skill_manager/mod.rs`
|
||||
- `crates/backend/nomifun-ai-agent/src/capability/first_message_injector.rs`
|
||||
- `crates/agent/nomi-agent/src/skill_tool.rs`
|
||||
|
||||
## Session Flow
|
||||
|
||||
```text
|
||||
UI request
|
||||
-> nomifun-conversation route/service
|
||||
-> nomifun-ai-agent AgentService / WorkerTaskManager
|
||||
-> runtime family factory
|
||||
-> Nomi engine or external CLI process
|
||||
-> AgentStreamEvent
|
||||
-> nomifun-realtime /ws
|
||||
-> renderer stream handlers
|
||||
```
|
||||
|
||||
Nomi-engine session 在进程内运行。ACP-style session 会 spawn 并管理子 CLI。
|
||||
公开 remote capability 调用通过 `nomifun-public` 与 Desktop Gateway registry
|
||||
进入,而不是通过 conversation HTTP route。
|
||||
|
||||
## Design Notes
|
||||
|
||||
旧 specs 会把 agent 层描述为“可机械抽离”并只列 11 个 crates。那些文件属于
|
||||
历史资料。当前代码仍保持强边界,但 browser/computer bridge 与 public gateway
|
||||
surfaces 意味着真实规则是“主接缝 + 明确记录的 feature-gated exceptions”。
|
||||
@@ -0,0 +1,112 @@
|
||||
# Backend Crates
|
||||
|
||||
The 29 `nomifun-*` crates under [`crates/backend/`](../../crates/backend/) form
|
||||
the HTTP/WS server. Together they compile into the `nomifun-app` library crate
|
||||
and, via `nomifun-app/src/main.rs`, the **`nomicore`** binary. The two app hosts
|
||||
(`nomifun-desktop` and `nomifun-web`) link `nomifun-app` directly and call
|
||||
`run_embedded_server` or compose `create_router` themselves.
|
||||
|
||||
The grouping below mirrors how the crates depend on each other in the workspace
|
||||
manifest ([`Cargo.toml`](../../Cargo.toml)). It is not a strict layered DAG —
|
||||
some feature crates depend on each other — but it gives a cognitive map that
|
||||
lines up with how a request travels through the server.
|
||||
|
||||
## Agent-layer dependency rule
|
||||
|
||||
The normal product seam is
|
||||
[`nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent/). Feature crates
|
||||
that need agent concepts should consume them through
|
||||
`nomifun_ai_agent::{nomi_config, nomi_types, RequirementSink}` when possible.
|
||||
|
||||
There are deliberate, feature-gated direct-dependency exceptions:
|
||||
|
||||
- [`nomifun-app`](../../crates/backend/nomifun-app/) depends on optional
|
||||
`nomi-computer`, `nomi-browser`, `nomi-config`, `nomi-tools`, and
|
||||
`nomi-types` for the `mcp-computer-stdio` and `mcp-browser-stdio` bridge
|
||||
subcommands.
|
||||
- [`nomifun-gateway`](../../crates/backend/nomifun-gateway/) depends on optional
|
||||
`nomi-browser`, `nomi-computer`, `nomi-config`, `nomi-tools`, and
|
||||
`nomi-types` for the Desktop Gateway browser/computer registries.
|
||||
|
||||
Do not add another direct `nomi-*` dependency without documenting why it cannot
|
||||
go through the normal seam or one of those bridge surfaces.
|
||||
|
||||
## Core, data, realtime, runtime
|
||||
|
||||
| Crate | Responsibility |
|
||||
| --- | --- |
|
||||
| [`nomifun-common`](../../crates/backend/nomifun-common/) | `AppError`, error chain, enums (`AgentType`, `ConversationStatus`, `MessageType`, `McpServerStatus`, ...), id generation (`generate_prefixed_id` for entity IDs, `generate_id` for tokens), AES-GCM `encrypt_string` / `decrypt_string`, `TimestampMs`, pagination helpers, `constants::DEFAULT_HOST/DEFAULT_PORT/BODY_LIMIT/CSRF_*`. |
|
||||
| [`nomifun-api-types`](../../crates/backend/nomifun-api-types/) | Every HTTP request / response DTO, the `WebSocketMessage` envelope, ACP / Nomi / OpenClaw / Remote build-extras. The frontend's TypeScript types mirror this crate. |
|
||||
| [`nomifun-db`](../../crates/backend/nomifun-db/) | SQLite via `sqlx`, embedded migrations, repository traits and Sqlite implementations for users, conversations, MCP, requirements, cron, ACP sessions, assistants, terminal sessions, companion tokens, webhooks, and more. Owns the `Database` handle and `init_database`. |
|
||||
| [`nomifun-realtime`](../../crates/backend/nomifun-realtime/) | `WebSocketManager`, `BroadcastEventBus`, `/ws` upgrade handler with token validation, message router trait, heartbeat timing, per-connection buffer constants. |
|
||||
| [`nomifun-runtime`](../../crates/backend/nomifun-runtime/) | Bundled runtime support for Bun, PATH enhancement for child processes, cross-platform process-tree kill, and a spawn `Builder` with the merged PATH. |
|
||||
| [`nomifun-assets`](../../crates/backend/nomifun-assets/) | Embedded static assets (`include_dir!`) shipped with the server. |
|
||||
|
||||
## Authentication and session
|
||||
|
||||
| Crate | Responsibility |
|
||||
| --- | --- |
|
||||
| [`nomifun-auth`](../../crates/backend/nomifun-auth/) | JWT HS256 (`JwtService`), bcrypt password hashing, login / logout / refresh / change-password / setup routes, `auth_middleware`, **CSRF double-submit cookie** middleware (cookie `nomifun-csrf-token`, header `x-csrf-token`), security-headers middleware, **rate limiting** (auth / api / authenticated-action variants), QR-code login token store, `validate_username` / `validate_password`. Exposes `CurrentUser` for handlers. |
|
||||
|
||||
## The agent seam
|
||||
|
||||
| Crate | Responsibility |
|
||||
| --- | --- |
|
||||
| [`nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent/) | **The single bridge to `crates/agent/`.** Builds the agent factory (ACP / Nomi / OpenClaw / Nanobot / Remote variants), holds the `AgentRegistry` and `WorkerTaskManagerImpl`, persists ACP sessions, broadcasts `AgentStreamEvent`, exposes `agent_routes` (model info, capabilities, slash commands, ...) and `remote_agent_routes`. Re-exports `nomi_config`, `nomi_types`, and `RequirementSink` for the rest of the backend. |
|
||||
|
||||
## Feature crates (the bulk of the product)
|
||||
|
||||
| Crate | Responsibility |
|
||||
| --- | --- |
|
||||
| [`nomifun-conversation`](../../crates/backend/nomifun-conversation/) | Conversation and message CRUD, send-message route, **streaming relay** that fans backend agent tokens onto `/ws`, ACP error recovery, response middleware (e.g. `/cron` slash-command detection, `<think>` stripping), skill resolver / snapshot, runtime-state persistence. |
|
||||
| [`nomifun-mcp`](../../crates/backend/nomifun-mcp/) | MCP server CRUD, **OAuth flow**, multi-CLI sync (`Claude`, `Codex`, `CodeBuddy`, `Gemini`, `Qwen`, `OpenCode`, `Nomi`, `Nomifun` adapters under `adapters/`), connection test, session injection of MCP capabilities (incl. built-in image-gen). |
|
||||
| [`nomifun-extension`](../../crates/backend/nomifun-extension/) | Extension and skill hub: manifests, dependency graph, classifier, install / enable / disable, packs that bundle skills + MCP servers + assistants. |
|
||||
| [`nomifun-team`](../../crates/backend/nomifun-team/) | Multi-agent teams: scheduler, mailbox, task board, crash detection, event loop, the team-MCP server (`mcp/`), the Guide MCP `nomi_create_team` tool, prompts. |
|
||||
| [`nomifun-channel`](../../crates/backend/nomifun-channel/) | External chat-channel adapters (Telegram, Lark, DingTalk, WeChat) — feature-gated. New conversations default to **master-agent mode**: companion persona + the Desktop Gateway tools (opt-out per platform via `assistant.{platform}.masterAgent`). |
|
||||
| [`nomifun-gateway`](../../crates/backend/nomifun-gateway/) | **Desktop Gateway MCP** — in-process HTTP tool server exposing the whole desktop (conversations, cron, companion memory, requirements, and feature-gated browser/computer tools) as `nomi_*` tools to internal and external agent surfaces. Reached internally via the `nomicore mcp-gateway-stdio` bridge. |
|
||||
| [`nomifun-cron`](../../crates/backend/nomifun-cron/) | Scheduled tasks: cron expressions, timezone repair, the cron daemon, slash-command-driven creation. |
|
||||
| [`nomifun-requirement`](../../crates/backend/nomifun-requirement/) | **AutoWork orchestrator** — backend-driven, boot-resume, persistent loop. Speaks to the agent layer through `RequirementSink`. |
|
||||
| [`nomifun-idmm`](../../crates/backend/nomifun-idmm/) | Intelligent Decision-Making Mode: a per-session supervisor that keeps agent / terminal sessions alive through provider faults and decision stalls (rule tier + sidecar model). See [Intelligent Decision](../guides/intelligent-decision.md). |
|
||||
| [`nomifun-webhook`](../../crates/backend/nomifun-webhook/) | Outbound Lark sender, `CompletionNotifier` for finished agent runs. |
|
||||
| [`nomifun-assistant`](../../crates/backend/nomifun-assistant/) | Assistant (preset prompt + skill set + MCP set) CRUD, override resolution, import/export. |
|
||||
| [`nomifun-companion`](../../crates/backend/nomifun-companion/) | Desktop companion state, figure/image assets, memory/persona data, companion public image serving, and companion-bound token integration. |
|
||||
| [`nomifun-knowledge`](../../crates/backend/nomifun-knowledge/) | Knowledge bases, source ingestion, bound-base mount state, and scoped read-only knowledge MCP server. |
|
||||
| [`nomifun-public`](../../crates/backend/nomifun-public/) | Companion-token authenticated public front doors: `/mcp`, `/mcp-agent`, and `/v1`. |
|
||||
| [`nomifun-secret`](../../crates/backend/nomifun-secret/) | Per-companion browser-use secret storage and credential lookup. |
|
||||
|
||||
## Infrastructure features
|
||||
|
||||
| Crate | Responsibility |
|
||||
| --- | --- |
|
||||
| [`nomifun-terminal`](../../crates/backend/nomifun-terminal/) | Terminal sessions backed by `portable-pty`, resize, input/output streaming over WS. |
|
||||
| [`nomifun-shell`](../../crates/backend/nomifun-shell/) | OS shell helpers: open files in the system, speech-to-text against Deepgram or OpenAI, clipboard / paste integration. |
|
||||
| [`nomifun-file`](../../crates/backend/nomifun-file/) | Sandboxed filesystem under the conversation work dir (`browse`, `path_safety`, `watch_service`, `snapshot_service`), zip helpers. |
|
||||
| [`nomifun-office`](../../crates/backend/nomifun-office/) | LibreOffice convert/preview pipeline (Office documents → preview). |
|
||||
| [`nomifun-system`](../../crates/backend/nomifun-system/) | LLM provider / model lookup, app-level settings, sysinfo, app version-check / self-updater scaffold. |
|
||||
|
||||
## The composition root: `nomifun-app`
|
||||
|
||||
[`nomifun-app`](../../crates/backend/nomifun-app/) is what the two host binaries
|
||||
link. It is structured as:
|
||||
|
||||
| Module | Role |
|
||||
| --- | --- |
|
||||
| `cli.rs` | Top-level `nomicore` clap parser: `--host/--port/--data-dir/--work-dir/--app-version/--local/--log-dir/--log-level` plus subcommands `mcp-requirement-stdio`, `mcp-knowledge-stdio`, `mcp-gateway-stdio`, `mcp-open-stdio`, `mcp-computer-stdio`, `mcp-browser-stdio`, `terminal-hook`, `doctor`, `tools`, `call`, and `agent`. The web host calls `Cli::parse_from(["nomifun-web"])` to get a defaulted instance, then overrides what it owns. |
|
||||
| `bootstrap/` | Layered initialization: `tracing_init` (file + console layers), `work_dir` resolution, `builtin_skills` materialization, `environment::{init_environment,init_data_layer}`, `admin::ensure_admin_credentials` for first-run pre-seed in authenticated mode. |
|
||||
| `services.rs` | The `AppServices` god-bag: every feature-crate service wired together with the right repositories. Built once via `AppServices::from_config(database, &config)`. |
|
||||
| `router/` | `create_router(&services)` and the typed `routes`, `state`, `health`, `trace` helpers; `build_assistant_state` / `build_conversation_state` / `build_extension_states` / `build_module_states` / `build_ws_state`. |
|
||||
| `commands/` | CLI subcommand bodies for the server, current stdio MCP bridges, terminal lifecycle hook, diagnostics, and public capability client commands. |
|
||||
| `lib.rs` | Public façade: `run_embedded_server`, `AppServices`, `create_router`, `bootstrap` re-exports. This is the only API the host binaries import. |
|
||||
|
||||
## Checking direct agent dependencies
|
||||
|
||||
If you want to inspect direct `nomi-*` dependencies, scan every backend crate
|
||||
manifest:
|
||||
|
||||
```sh
|
||||
# from the repo root, on a Unix shell
|
||||
rg -l 'nomi-[a-z-]+\\s*=' crates/backend/*/Cargo.toml
|
||||
```
|
||||
|
||||
Expect the primary seam (`nomifun-ai-agent`) plus the feature-gated bridge
|
||||
exceptions described above.
|
||||
@@ -0,0 +1,93 @@
|
||||
# 后端 Crates
|
||||
|
||||
[`crates/backend/`](../../crates/backend/) 下的 29 个 `nomifun-*` crate 共同构成 HTTP/WS 服务器。它们一起编译进 `nomifun-app` 库 crate,并通过 `nomifun-app/src/main.rs` 生成 **`nomicore`** 二进制。两个宿主应用(`nomifun-desktop` 与 `nomifun-web`)直接链接 `nomifun-app`,并自行调用 `run_embedded_server` 或组合 `create_router`。
|
||||
|
||||
下方分组反映了 crate 在工作区清单([`Cargo.toml`](../../Cargo.toml))中相互依赖的方式。这并非严格的分层 DAG —— 部分功能 crate 之间存在依赖 —— 但它提供了一张与请求穿越服务器的路径相吻合的认知地图。
|
||||
|
||||
## Agent 层依赖规则
|
||||
|
||||
正常的产品接缝是 [`nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent/)。需要 agent 概念的功能 crate 应尽量通过 `nomifun_ai_agent::{nomi_config, nomi_types, RequirementSink}` 来消费它们。
|
||||
|
||||
存在有意为之、由 feature 控制的直接依赖例外:
|
||||
|
||||
- [`nomifun-app`](../../crates/backend/nomifun-app/) 为 `mcp-computer-stdio` 与 `mcp-browser-stdio` 桥接子命令,可选依赖 `nomi-computer`、`nomi-browser`、`nomi-config`、`nomi-tools`、`nomi-types`。
|
||||
- [`nomifun-gateway`](../../crates/backend/nomifun-gateway/) 为桌面网关的 browser/computer 注册表,可选依赖 `nomi-browser`、`nomi-computer`、`nomi-config`、`nomi-tools`、`nomi-types`。
|
||||
|
||||
不要在未说明“为何无法走正常接缝或上述桥接面”的情况下,新增其他直接的 `nomi-*` 依赖。
|
||||
|
||||
## 核心、数据、实时、运行时
|
||||
|
||||
| Crate | 职责 |
|
||||
| --- | --- |
|
||||
| [`nomifun-common`](../../crates/backend/nomifun-common/) | `AppError`、错误链、各类枚举(`AgentType`、`ConversationStatus`、`MessageType`、`McpServerStatus` 等)、id 生成(实体 ID 用 `generate_prefixed_id`,令牌用 `generate_id`)、AES-GCM `encrypt_string` / `decrypt_string`、`TimestampMs`、分页辅助、`constants::DEFAULT_HOST/DEFAULT_PORT/BODY_LIMIT/CSRF_*`。 |
|
||||
| [`nomifun-api-types`](../../crates/backend/nomifun-api-types/) | 每个 HTTP 请求 / 响应 DTO,`WebSocketMessage` 信封,ACP / Nomi / OpenClaw / Remote 等扩展。前端 TypeScript 类型镜像该 crate。 |
|
||||
| [`nomifun-db`](../../crates/backend/nomifun-db/) | 通过 `sqlx` 操作 SQLite,内嵌迁移,为用户、会话、MCP、需求、cron、ACP 会话、助手、终端会话、伙伴令牌、知识库、渠道、连接器凭据、IDMM 介入、远程 agent、webhook 等提供仓储 trait 与 Sqlite 实现。持有 `Database` 句柄以及 `init_database`。 |
|
||||
| [`nomifun-realtime`](../../crates/backend/nomifun-realtime/) | `WebSocketManager`、`BroadcastEventBus`,带 token 校验的 `/ws` 升级处理器,消息路由 trait,心跳计时,每连接缓冲常量。 |
|
||||
| [`nomifun-runtime`](../../crates/backend/nomifun-runtime/) | 内嵌 Bun 运行时支持、为子进程增强 `PATH`、跨平台进程树终止,以及携带合并 PATH 的 spawn `Builder`。 |
|
||||
| [`nomifun-assets`](../../crates/backend/nomifun-assets/) | 随服务器一同发布的内嵌静态资源(`include_dir!`)。 |
|
||||
|
||||
## 认证与会话
|
||||
|
||||
| Crate | 职责 |
|
||||
| --- | --- |
|
||||
| [`nomifun-auth`](../../crates/backend/nomifun-auth/) | JWT HS256(`JwtService`)、bcrypt 密码哈希、登录 / 登出 / 刷新 / 修改密码 / 初始化路由、`auth_middleware`、**CSRF 双提交 cookie** 中间件(cookie `nomifun-csrf-token`、header `x-csrf-token`)、安全响应头中间件、**限流**(auth / api / authenticated-action 等变体)、二维码登录 token 存储、`validate_username` / `validate_password`。为 handler 暴露 `CurrentUser`。 |
|
||||
|
||||
## Agent 接缝
|
||||
|
||||
| Crate | 职责 |
|
||||
| --- | --- |
|
||||
| [`nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent/) | **通往 `crates/agent/` 的唯一桥梁。** 构建 agent 工厂(ACP / Nomi / OpenClaw / Nanobot / Remote 等变体),持有 `AgentRegistry` 与 `WorkerTaskManagerImpl`,持久化 ACP 会话,广播 `AgentStreamEvent`,暴露 `agent_routes`(模型信息、能力、斜杠命令等)和 `remote_agent_routes`。再导出 `nomi_config`、`nomi_types` 和 `RequirementSink` 供其余后端使用。 |
|
||||
|
||||
## 功能 crate(产品的主体)
|
||||
|
||||
| Crate | 职责 |
|
||||
| --- | --- |
|
||||
| [`nomifun-conversation`](../../crates/backend/nomifun-conversation/) | 会话与消息 CRUD、send-message 路由、**流式中继**(将后端 agent token 投递到 `/ws`)、ACP 错误恢复、响应中间件(如 `/cron` 斜杠命令检测、`<think>` 剥离)、技能解析 / 快照、运行时状态持久化。 |
|
||||
| [`nomifun-mcp`](../../crates/backend/nomifun-mcp/) | MCP 服务器 CRUD、**OAuth 流程**、多 CLI 同步(`adapters/` 下的 `Claude`、`Codex`、`CodeBuddy`、`Gemini`、`Qwen`、`OpenCode`、`Nomi`、`Nomifun` 适配器)、连接测试、向会话注入 MCP 能力(含内置图像生成)。 |
|
||||
| [`nomifun-extension`](../../crates/backend/nomifun-extension/) | 扩展与技能枢纽:清单、依赖图、分类器、安装 / 启用 / 禁用,捆绑技能 + MCP 服务器 + 助手的扩展包。 |
|
||||
| [`nomifun-team`](../../crates/backend/nomifun-team/) | 多智能协同(多 agent):调度器、信箱、任务板、崩溃检测、事件循环、协同 MCP 服务器(`mcp/`)、Guide MCP 工具、提示词。(`nomifun-team` crate 名与 `team_*` 工具名作为线缆契约有意保留。) |
|
||||
| [`nomifun-channel`](../../crates/backend/nomifun-channel/) | 外部聊天渠道适配器(Telegram、Lark、DingTalk、WeChat)—— 通过 feature 控制。新会话默认进入**主 Agent 模式**:伙伴人格 + 桌面网关工具(可按平台经 `assistant.{platform}.masterAgent` 关闭)。 |
|
||||
| [`nomifun-gateway`](../../crates/backend/nomifun-gateway/) | **桌面网关 MCP** —— 进程内 HTTP 工具服务器,把整个桌面(会话、定时任务、伙伴记忆、需求平台,以及 feature 控制的 browser/computer 工具)以 `nomi_*` 工具暴露给内部与外部 agent 入口。内部经 `nomicore mcp-gateway-stdio` 桥接入。 |
|
||||
| [`nomifun-cron`](../../crates/backend/nomifun-cron/) | 定时任务:cron 表达式、时区修复、cron 守护进程、由斜杠命令驱动的创建。 |
|
||||
| [`nomifun-requirement`](../../crates/backend/nomifun-requirement/) | **AutoWork 编排器** —— 后端驱动、boot-resume、持久循环。通过 `RequirementSink` 与 agent 层通信。 |
|
||||
| [`nomifun-idmm`](../../crates/backend/nomifun-idmm/) | 智能决策模式(IDMM):一个按会话的监督器,在提供商故障与决策停滞中保活智能体 / 终端会话(规则层 + 旁路模型)。详见[智能决策](../guides/intelligent-decision.zh.md)。 |
|
||||
| [`nomifun-webhook`](../../crates/backend/nomifun-webhook/) | 外发飞书消息发送器,agent 运行结束时的 `CompletionNotifier`。 |
|
||||
| [`nomifun-assistant`](../../crates/backend/nomifun-assistant/) | 助手(预设提示词 + 技能集 + MCP 集)的 CRUD、覆盖解析、导入 / 导出。 |
|
||||
| [`nomifun-companion`](../../crates/backend/nomifun-companion/) | 桌面伙伴状态、形象 / 图片资源、记忆 / 人格数据、伙伴公开图片服务,以及伙伴绑定令牌集成。 |
|
||||
| [`nomifun-knowledge`](../../crates/backend/nomifun-knowledge/) | 知识库、来源摄取、绑定库挂载状态,以及作用域只读的知识 MCP 服务器。 |
|
||||
| [`nomifun-public`](../../crates/backend/nomifun-public/) | 由伙伴令牌鉴权的公开对外入口:`/mcp`、`/mcp-agent` 与 `/v1`。 |
|
||||
| [`nomifun-secret`](../../crates/backend/nomifun-secret/) | 按伙伴的 browser-use 密钥存储与凭据查询。 |
|
||||
|
||||
## 基础设施特性
|
||||
|
||||
| Crate | 职责 |
|
||||
| --- | --- |
|
||||
| [`nomifun-terminal`](../../crates/backend/nomifun-terminal/) | 基于 `portable-pty` 的终端会话,支持 resize,通过 WS 进行输入 / 输出流式传输。 |
|
||||
| [`nomifun-shell`](../../crates/backend/nomifun-shell/) | 操作系统外壳辅助:用系统应用打开文件,针对 Deepgram 或 OpenAI 的语音转文字,剪贴板 / 粘贴集成。 |
|
||||
| [`nomifun-file`](../../crates/backend/nomifun-file/) | 在会话工作目录下的沙箱化文件系统(`browse`、`path_safety`、`watch_service`、`snapshot_service`),zip 辅助。 |
|
||||
| [`nomifun-office`](../../crates/backend/nomifun-office/) | LibreOffice 转换 / 预览管线(Office 文档 → 预览)。 |
|
||||
| [`nomifun-system`](../../crates/backend/nomifun-system/) | LLM provider / 模型查询、应用级设置、sysinfo、应用版本检查 / 自更新框架。 |
|
||||
|
||||
## 组合根:`nomifun-app`
|
||||
|
||||
[`nomifun-app`](../../crates/backend/nomifun-app/) 是两个宿主二进制所链接的 crate。其结构如下:
|
||||
|
||||
| 模块 | 角色 |
|
||||
| --- | --- |
|
||||
| `cli.rs` | 顶层 `nomicore` clap 解析器:`--host/--port/--data-dir/--work-dir/--app-version/--local/--log-dir/--log-level`,加上子命令 `mcp-requirement-stdio`、`mcp-knowledge-stdio`、`mcp-gateway-stdio`、`mcp-open-stdio`、`mcp-computer-stdio`、`mcp-browser-stdio`、`terminal-hook`、`doctor`、`tools`、`call`、`agent`。Web 宿主调用 `Cli::parse_from(["nomifun-web"])` 取得带默认值的实例,然后覆盖自身关心的项。 |
|
||||
| `bootstrap/` | 分层初始化:`tracing_init`(文件 + 控制台层)、`work_dir` 解析、`builtin_skills` 物化、`environment::{init_environment,init_data_layer}`、`admin::ensure_admin_credentials`(认证模式下的首次运行预置)。 |
|
||||
| `services.rs` | `AppServices` 大杂烩:每个功能 crate 的服务带着对应仓储一并接好。通过 `AppServices::from_config(database, &config)` 一次构建。 |
|
||||
| `router/` | `create_router(&services)` 以及类型化的 `routes`、`state`、`health`、`trace` 辅助;`build_assistant_state` / `build_conversation_state` / `build_extension_states` / `build_module_states` / `build_ws_state`。 |
|
||||
| `commands/` | CLI 子命令的实现体:服务器、各 stdio MCP bridge、终端生命周期 hook、诊断,以及公开能力客户端命令。 |
|
||||
| `lib.rs` | 公共门面:`run_embedded_server`、`AppServices`、`create_router`、`bootstrap` 再导出。这是宿主二进制唯一引入的 API。 |
|
||||
|
||||
## 在哪里检查依赖规则
|
||||
|
||||
如果你想自行检查直接的 `nomi-*` 依赖,可以扫描每个后端 crate 的清单:
|
||||
|
||||
```sh
|
||||
# from the repo root, on a Unix shell
|
||||
rg -l 'nomi-[a-z-]+\s*=' crates/backend/*/Cargo.toml
|
||||
```
|
||||
|
||||
预期会看到主接缝(`nomifun-ai-agent`)以及上文描述的、由 feature 控制的桥接例外。
|
||||
@@ -0,0 +1,110 @@
|
||||
# Communication
|
||||
|
||||
NomiFun has several transport surfaces. They deliberately serve different
|
||||
callers and security models.
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel | Direction | Carries | Source |
|
||||
| --- | --- | --- | --- |
|
||||
| HTTP REST | UI/browser/client -> backend | CRUD, commands, setup, file operations, terminal input | `nomifun-app` route tree |
|
||||
| WebSocket `/ws` | backend <-> UI | Agent stream events, terminal output, broadcast events, heartbeats | `nomifun-realtime` |
|
||||
| Tauri IPC | SPA -> desktop shell | Desktop-only OS features | `apps/desktop/src/main.rs` + Tauri plugins |
|
||||
| ACP/agent stdio | backend <-> child CLI | External CLI-agent conversation traffic | `nomifun-ai-agent` |
|
||||
| MCP stdio/HTTP | agent/backend/client <-> MCP server | Tools/resources/prompts | `nomi-mcp`, `nomifun-mcp`, `nomifun-public`, bridge subcommands |
|
||||
| Public Remote fronts | external agents/scripts -> backend | MCP tools or REST capability calls | `/mcp`, `/mcp-agent`, `/v1` |
|
||||
|
||||
## Auth Modes
|
||||
|
||||
The backend resolves trust through `nomifun-auth` and the `AppServices`
|
||||
configuration:
|
||||
|
||||
- **Required**: normal web mode. Login cookie is required for `/api/*`; CSRF
|
||||
protects state-changing cookie-authenticated requests.
|
||||
- **NoAuth**: explicit insecure mode, used only through flags such as
|
||||
`--insecure-no-auth` for trusted loopback/private use.
|
||||
- **TrustLocalToken**: desktop shell mode. The webview gets a per-boot secret
|
||||
and sends it as `x-nomi-local-trust`; middleware resolves that request to the
|
||||
trusted local user. This is not the same as the old blanket `--local` story.
|
||||
|
||||
WebSocket auth accepts the normal authenticated browser path and the local-trust
|
||||
path used by the desktop shell.
|
||||
|
||||
## HTTP And WebSocket
|
||||
|
||||
The SPA bridge in `ui/src/common/adapter/httpBridge.ts` selects:
|
||||
|
||||
- same-origin URLs for `nomifun-web`,
|
||||
- `http://127.0.0.1:<window.__backendPort>` for the desktop webview.
|
||||
|
||||
`/ws` is a singleton connection per page lifetime. The backend event bus fans
|
||||
conversation, terminal, cron/requirement, channel, companion, and other events
|
||||
into the WebSocket manager.
|
||||
|
||||
## Tauri IPC
|
||||
|
||||
Rust commands currently registered by the desktop shell include:
|
||||
|
||||
- `check_for_updates`
|
||||
- `sync_companion_windows`
|
||||
- `webui_get_status`
|
||||
- `webui_start`
|
||||
- `webui_stop`
|
||||
- `set_keep_awake`
|
||||
- `set_tray_labels`
|
||||
|
||||
The renderer also uses Tauri JS APIs/plugins for window, dialog, notification,
|
||||
process, autostart, deep-link, updater, and path operations where appropriate.
|
||||
|
||||
## MCP And Agent Bridges
|
||||
|
||||
The current `nomicore` CLI subcommands include:
|
||||
|
||||
- `mcp-requirement-stdio`
|
||||
- `mcp-knowledge-stdio`
|
||||
- `mcp-gateway-stdio`
|
||||
- `mcp-open-stdio`
|
||||
- `mcp-computer-stdio`
|
||||
- `mcp-browser-stdio`
|
||||
- `terminal-hook`
|
||||
- `doctor`
|
||||
- `tools`
|
||||
- `call`
|
||||
- `agent`
|
||||
|
||||
Older docs that mention `mcp-bridge`, `mcp-guide-stdio`, or `mcp-team-stdio`
|
||||
are historical and predate the current bridge set.
|
||||
|
||||
MCP injection differs by runtime:
|
||||
|
||||
- user MCP rows and OAuth-backed HTTP servers come from `nomifun-mcp`,
|
||||
- requirement and knowledge servers are scoped internal MCP servers,
|
||||
- Desktop Gateway tools are exposed through `nomifun-gateway`,
|
||||
- browser/computer bridges are feature-gated,
|
||||
- public `/mcp` and `/mcp-agent` are companion-token authenticated fronts from
|
||||
`nomifun-public`.
|
||||
|
||||
## Public Capability Fronts
|
||||
|
||||
The full app router mounts three companion-token authenticated surfaces outside
|
||||
the normal `/api` browser-auth tree:
|
||||
|
||||
- `/mcp`: general MCP profile for a companion identity,
|
||||
- `/mcp-agent`: curated agent profile,
|
||||
- `/v1`: REST capability adapter, with optional agent profile selection.
|
||||
|
||||
Tokens are per companion. A caller acts as that companion and inherits the
|
||||
associated profile, model/persona choices, and scoped capabilities.
|
||||
|
||||
## Quick Lookup
|
||||
|
||||
| Operation | Transport |
|
||||
| --- | --- |
|
||||
| Login/setup | HTTP `/api/auth/*` |
|
||||
| Conversation send | HTTP `/api/conversations/*` plus streamed `/ws` events |
|
||||
| Terminal input | HTTP terminal route; output over `/ws` |
|
||||
| Desktop keep-awake | Tauri command |
|
||||
| Remote MCP tool call | `/mcp` or `/mcp-agent` |
|
||||
| Remote REST capability call | `/v1` |
|
||||
| Agent CLI conversation | child process stdio managed by `nomifun-ai-agent` |
|
||||
| Internal knowledge search for ACP session | `mcp-knowledge-stdio` bridge |
|
||||
@@ -0,0 +1,180 @@
|
||||
# 通信
|
||||
|
||||
NomiFun 的各个进程 —— SPA、嵌入式后端、agent CLI 与 MCP 服务器 —— 通过五条彼此独立的通道相互对话。它们的职责互不重叠,挑选合适通道的规则在客户端的适配层(`ui/src/common/adapter/`)以及服务端的路由与服务 crate 中得到了固化。
|
||||
|
||||
## 五条通道
|
||||
|
||||
| 通道 | 方向 | 承载 | 位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| HTTP REST | UI ↔ 后端 | 所有请求/响应操作:CRUD、命令调用、文件操作 | `http://127.0.0.1:<port>/api/*` |
|
||||
| WebSocket | 后端 → UI(终端输入 / 心跳时反向) | 流式 agent token、终端输出、广播事件、会话产物 | `/ws` |
|
||||
| Tauri IPC | 仅 UI → 桌面外壳 | 浏览器没有等价物的操作系统外壳特性 | `@tauri-apps/api` 与 Tauri 插件 |
|
||||
| ACP(stdio) | 后端 ↔ agent CLI 子进程 | 一段会话的全部 agent 流量,发往 Claude / Codex / Gemini / Qwen / OpenCode 风格的运行时 | 通过 stdin/stdout 的换行分隔 JSON |
|
||||
| MCP(stdio 或 HTTP) | 后端 ↔ MCP 服务器,agent CLI ↔ MCP 服务器 | 工具调用、资源读取、提示词 | 派生进程或本地 HTTP |
|
||||
|
||||
## HTTP REST
|
||||
|
||||
SPA 的适配层([`httpBridge.ts`](../../ui/src/common/adapter/httpBridge.ts))把每个操作包装成一个有类型的调用:
|
||||
|
||||
```ts
|
||||
// Approximate shape — see httpBridge.ts for the real definitions.
|
||||
const conversation = httpGet<Conversation, { id: string }>(p => `/api/conversations/${p.id}`);
|
||||
const sendMessage = httpPost<SendMessageResponse, SendMessageRequest>(p => `/api/conversations/${p.id}/messages`);
|
||||
```
|
||||
|
||||
线上格式依赖的若干常量:
|
||||
|
||||
- 请求体上限 —— `nomifun_common::constants::BODY_LIMIT`。
|
||||
- CSRF cookie 名 —— `nomifun-csrf-token`。
|
||||
- CSRF header 名 —— `x-csrf-token`。
|
||||
- 默认端口(`nomifun-web`) —— `8787`。
|
||||
- 默认 host —— `127.0.0.1`。
|
||||
|
||||
### CSRF 双提交
|
||||
|
||||
Web 宿主默认以认证模式运行后端。两个 cookie 在认证中扮演角色:
|
||||
|
||||
| Cookie | 由谁设置 | 由谁读取 | HttpOnly |
|
||||
| --- | --- | --- | --- |
|
||||
| 会话 JWT | 登录时由 `nomifun-auth` 设置 | 每次认证请求由 `auth_middleware` 读取 | 是 |
|
||||
| CSRF token(`nomifun-csrf-token`) | 由 `csrf_middleware` 设置(首次缺失时签发) | 浏览器的 `document.cookie`,再由 SPA 回显到 `x-csrf-token` | 否 —— SPA 必须能读到 |
|
||||
|
||||
CSRF 中间件([`crates/backend/nomifun-auth/src/csrf.rs`](../../crates/backend/nomifun-auth/src/csrf.rs))守护 POST / PUT / PATCH / DELETE 请求;安全方法绕过校验。三个豁免路径 —— `/login`、`/api/auth/qr-login`、`/api/auth/setup` —— 会跳过检查,因为它们正用于引导会话本身。桌面外壳使用 `TrustLocalToken`:WebView 呈递本地信任 secret,远程/其他本机客户端仍需正常认证或走 WebUI 登录。`--local` 仅是独立 `nomicore`/开发 Web host 的无鉴权模式。
|
||||
|
||||
### 响应包装
|
||||
|
||||
成功的 JSON 响应包装为 `{ success: true, data: ... }`;错误为 `{ success: false, error: <message>, code: <machine code>, details: ... }`。SPA 的 `httpRequest` 自动解包 `data` 字段,并对非 2xx 响应抛出携带 `status` / `code` / `backendMessage` / `details` 的 `BackendHttpError`,使调用方无需解析消息文本即可在 `code` 上分支。
|
||||
|
||||
## WebSocket —— `/ws`
|
||||
|
||||
一条 WebSocket 承载后端与 SPA 之间的所有流式负载:
|
||||
|
||||
| 事件类别 | 何时发送 | 来源 crate |
|
||||
| --- | --- | --- |
|
||||
| `message.stream` | 模型按块发出 token 时 | `nomifun-conversation::stream_relay` |
|
||||
| `conversation.artifact` | 工具产生了产物(文件 / 图像 / 预览) | `nomifun-conversation::routes_aux` |
|
||||
| `terminal.output` | PTY 产生输出 | `nomifun-terminal` |
|
||||
| 审批请求 / 响应 | 工具调用需要用户批准 | `nomifun-conversation`(经接缝) |
|
||||
| 团队 / agent 事件 | 多 agent 团队状态变化 | `nomifun-team` |
|
||||
| `auth-expired` / 关闭 1008 | 会话 JWT 中途失效 | `nomifun-realtime` |
|
||||
| 心跳(`ping` / `pong`) | 连接保活 | `nomifun-realtime` |
|
||||
|
||||
升级由 `nomifun_realtime::ws_upgrade_handler`([`crates/backend/nomifun-realtime/src/handler.rs`](../../crates/backend/nomifun-realtime/src/handler.rs))处理,它校验通过 cookie 或 `Sec-WebSocket-Protocol` header 携带的 JWT(header 的取值会被原样回显以使握手正确完成)。认证失败时它会发送 `auth-expired` 消息并以 1008 关闭;SPA 同时监听这两个信号(参见 [`browser.ts`](../../ui/src/common/adapter/browser.ts)),并在任一路径上重定向到 `/login`。
|
||||
|
||||
`httpBridge.ts` 中的 SPA WebSocket 逻辑是单例的:每个页面生命周期一个连接、指数退避重连(封顶 30s)、按 JSON 形状(`{ name, data }`)解复用并把事件分发到通过 `wsEmitter(name)` 注册的监听器。两个事件名与 HTTP 路径列表被显式维护,用以**抑制 agent 流式或 PTY 活跃时的嘈杂控制台日志**:
|
||||
|
||||
```ts
|
||||
const NOISY_WS_EVENTS = new Set(['terminal.output', 'message.stream', 'conversation.artifact']);
|
||||
const NOISY_HTTP_FRAGMENTS = ['/input', '/resize'];
|
||||
```
|
||||
|
||||
心跳常量定义在 `nomifun_realtime::types::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, PER_CONNECTION_BUFFER}`。
|
||||
|
||||
## Tauri IPC —— 仅操作系统外壳
|
||||
|
||||
Tauri 外壳采用**反向 IPC**:是 SPA 调用操作系统外壳,绝不反过来。[`apps/desktop/src/main.rs`](../../apps/desktop/src/main.rs) 中注册的 Tauri 命令包括:
|
||||
|
||||
```rust
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
check_for_updates,
|
||||
sync_companion_windows,
|
||||
webui_get_status,
|
||||
webui_start,
|
||||
webui_stop,
|
||||
set_keep_awake,
|
||||
set_tray_labels
|
||||
])
|
||||
```
|
||||
|
||||
其余一切都通过 Tauri 已发布的 JS API —— `@tauri-apps/api` 与 `tauri-plugin-*` crate。SPA 的 `tauriShell.ts` 用 `isTauri()` 守护每个操作,使同一份代码路径在浏览器中变为空操作:
|
||||
|
||||
| 操作 | 插件 |
|
||||
| --- | --- |
|
||||
| 窗口最小化 / 最大化 / 关闭、isMaximized 监听 | `@tauri-apps/api/window` |
|
||||
| 打开原生对话框 | `tauri-plugin-dialog` |
|
||||
| 发送通知 | `tauri-plugin-notification` |
|
||||
| 进程重启 | `tauri-plugin-process` |
|
||||
| 开机自启 | `tauri-plugin-autostart` |
|
||||
| 深链接 `open-url` 事件 | `tauri-plugin-deep-link` |
|
||||
| 单实例锁 | `tauri-plugin-single-instance` |
|
||||
| 自更新检查(唯一的 Rust 命令) | `tauri-plugin-updater` |
|
||||
| OS 路径查询(`home`、`downloads`、`desktop`) | `@tauri-apps/api/path` |
|
||||
|
||||
少数操作没有 Tauri 等价物,已在浏览器中被有意**桩化**(Chrome DevTools Protocol、GPU 恢复、渲染进程日志通道、关闭至托盘)。这些操作在 `tauriShell.ts` 中标记为 `DEGRADE_STUB`,留给未来的 Tauri 移植。
|
||||
|
||||
## ACP —— 通过 stdio 的 agent 运行时
|
||||
|
||||
若干 CLI agent —— Claude Code、Codex、Gemini CLI、Qwen、OpenCode —— 都实现了 **Agent Connection Protocol(ACP)**:在子进程的 stdin/stdout 上的 JSON 消息流。NomiFun 通过 PATH 上预置的 `bun` 运行时把它们当作子进程派生。接缝 crate `nomifun-ai-agent` 持有这些进程的工厂、注册表与 worker-task 管理器;按 agent 划分的元数据(握手响应、可用模型、取消路径)通过 `IAgentMetadataRepository` 存储于 SQLite。
|
||||
|
||||
进程内的流量如下:
|
||||
|
||||
```
|
||||
SPA ──HTTP/WS──▶ nomifun-conversation ──▶ nomifun-ai-agent::AgentService
|
||||
│
|
||||
▼
|
||||
spawn child CLI
|
||||
stdio = piped
|
||||
│
|
||||
▼
|
||||
nomi-protocol on stdin/stdout
|
||||
│
|
||||
▼
|
||||
stream tokens / tool calls
|
||||
│
|
||||
broadcast through nomifun-realtime to /ws
|
||||
```
|
||||
|
||||
`nomi-protocol` crate 定义了分帧与工具审批状态机;`nomifun-ai-agent::protocol::events::AgentStreamEvent` 把协议事件翻译成 SPA 能理解的 `WebSocketMessage`。
|
||||
|
||||
## MCP —— Model Context Protocol
|
||||
|
||||
MCP 服务器对外暴露引擎可调用的工具与资源。当前 `nomifun-app`
|
||||
二进制提供多个 stdio 桥子命令,而不是旧的单一 `mcp-bridge`:
|
||||
|
||||
- `mcp-requirement-stdio`
|
||||
- `mcp-knowledge-stdio`
|
||||
- `mcp-gateway-stdio`
|
||||
- `mcp-open-stdio`
|
||||
- `mcp-computer-stdio`
|
||||
- `mcp-browser-stdio`
|
||||
|
||||
同一二进制还提供 `terminal-hook`、`doctor`、`tools`、`call`、`agent`
|
||||
等运维 / 调用子命令。提到 `mcp-bridge`、`mcp-guide-stdio` 或
|
||||
`mcp-team-stdio` 的旧文档均属于历史资料,早于当前桥集合。
|
||||
|
||||
不同运行时的 MCP 注入来源不同:
|
||||
|
||||
- 用户配置的 MCP 行与 OAuth HTTP MCP 服务器由 `nomifun-mcp` 管理;
|
||||
- Requirement 与 Knowledge 服务器是有作用域的内部 MCP 服务器;
|
||||
- Desktop Gateway 工具通过 `nomifun-gateway` 暴露;
|
||||
- Browser / Computer 桥按 feature gate 启用;
|
||||
- 公开 `/mcp` 与 `/mcp-agent` 由 `nomifun-public` 提供,并使用 companion token 认证。
|
||||
|
||||
针对 HTTP MCP 服务器的 OAuth 流程由 `nomifun-mcp::oauth_service` 处理(PKCE、回调 URI、token 存储)。加密后的 token 通过 AES-GCM 落入 SQLite 的 `oauth_tokens` 仓库(参见 `nomifun-common::crypto::{encrypt_string, decrypt_string}`)。
|
||||
|
||||
## 公开能力入口
|
||||
|
||||
完整 app router 在普通 `/api` browser-auth 树之外挂载三类 companion-token
|
||||
认证入口:
|
||||
|
||||
- `/mcp`:面向 companion 身份的通用 MCP profile;
|
||||
- `/mcp-agent`:策划过的 agent profile;
|
||||
- `/v1`:REST 能力适配器,可选择 agent profile。
|
||||
|
||||
Token 按 companion 发放。调用方以该 companion 身份行动,并继承它关联的
|
||||
profile、模型 / 人格选择与作用域能力。
|
||||
|
||||
## 快速查表:事件 / 传输
|
||||
|
||||
| 事件或操作 | 传输 |
|
||||
| --- | --- |
|
||||
| 登录 / 设置 | HTTP `/api/auth/*` |
|
||||
| 发送会话消息 | HTTP `/api/conversations/*`,流式事件走 `/ws` |
|
||||
| 终端输入 / 输出 | 输入走 HTTP 终端路由,输出走 `/ws` |
|
||||
| 桌面 keep-awake | Tauri command |
|
||||
| 远程 MCP 工具调用 | `/mcp` 或 `/mcp-agent` |
|
||||
| 远程 REST 能力调用 | `/v1` |
|
||||
| Agent CLI 会话 | `nomifun-ai-agent` 管理的子进程 stdio |
|
||||
| ACP 会话内部知识搜索 | `mcp-knowledge-stdio` bridge |
|
||||
|
||||
交叉参考:数据与持久化层见 [`data-and-storage.md`](data-and-storage.zh.md);ACP 协议细节(以及驱动子进程的引擎)见 [`agent-engine.md`](agent-engine.zh.md);SPA 适配层见 [`frontend.md`](frontend.zh.md)。
|
||||
@@ -0,0 +1,312 @@
|
||||
# Data and Storage
|
||||
|
||||
NomiFun keeps its state in three places: a SQLite database (the source of
|
||||
truth for everything structured), a per-installation **data directory**
|
||||
(database file, logs, OS-cached runtimes), and per-conversation **work
|
||||
directories** that hold the files agents read and write. This page explains
|
||||
what lives where, how it's named, and how it's protected.
|
||||
|
||||
## The data directory
|
||||
|
||||
| Host | Default path | Override |
|
||||
| --- | --- | --- |
|
||||
| Desktop (`nomifun-desktop`) | Per-user app data: `%LOCALAPPDATA%\NomiFun\Nomi` on Windows, `~/Library/Application Support/NomiFun/Nomi` on macOS, `$XDG_DATA_HOME/NomiFun/Nomi` (usually `~/.local/share/NomiFun/Nomi`) on Linux. With `NOMIFUN_DATA_DIR` set, becomes `$NOMIFUN_DATA_DIR/Nomi`. Legacy installs under `<system temp>/nomifun-data/Nomi` are auto-relocated on launch (one-shot; the old dir is kept as a backup). | env `NOMIFUN_DATA_DIR` |
|
||||
| Web (`nomifun-web`) and the `nomicore` bin | The **same** per-user directory as the desktop shell — `%LOCALAPPDATA%\NomiFun\Nomi` / `~/Library/Application Support/NomiFun/Nomi` / `$XDG_DATA_HOME/NomiFun/Nomi` (the old `./data`-relative default is gone). With `NOMIFUN_DATA_DIR` set, the value is taken **literally** (no `/Nomi` suffix), so Docker `/data` and systemd `/var/lib/nomifun` deployments are unaffected. | flag `--data-dir` or env `NOMIFUN_DATA_DIR` |
|
||||
|
||||
Inside the data directory:
|
||||
|
||||
```
|
||||
<data_dir>/
|
||||
├── nomifun-backend.db SQLite database (sqlx)
|
||||
├── server.lock exclusive server-lock address file (the lock lives on
|
||||
│ the open OS handle; a leftover file is harmless)
|
||||
├── logs/ tracing-appender file output (rotated daily)
|
||||
├── conversations/ per-conversation workspaces (see below)
|
||||
└── companion/ companion file domain (shared memory hub + per-companion profiles, see below)
|
||||
```
|
||||
|
||||
All three hosts resolve the unset default through one shared helper,
|
||||
[`nomifun_app::cli::default_data_dir()`](../../crates/backend/nomifun-app/src/cli.rs):
|
||||
`dirs::data_local_dir()/NomiFun/Nomi` (the per-user application-data
|
||||
location), with the system temp dir (`<system temp>/nomifun-data/Nomi`)
|
||||
only as an extreme fallback when the OS reports no user dir. Env semantics
|
||||
stay host-specific: the desktop shell appends `"Nomi"` to `NOMIFUN_DATA_DIR`
|
||||
(see [`apps/desktop/src/main.rs`](../../apps/desktop/src/main.rs)), while
|
||||
`nomifun-web` and `nomicore` take the env value literally (a clap `env`
|
||||
binding — new for `nomicore`, which previously ignored the variable).
|
||||
A pre-existing legacy install under `<system temp>/nomifun-data/Nomi` is
|
||||
relocated to the new location once at launch
|
||||
([`apps/desktop/src/relocate.rs`](../../apps/desktop/src/relocate.rs)):
|
||||
data is copied (regenerable caches/logs are left behind), the legacy dir is
|
||||
kept as a backup, and the backend then rewrites absolute paths stored in the
|
||||
database (knowledge-base roots, conversation workspaces, terminal cwds) to
|
||||
the new root.
|
||||
|
||||
### One directory, one state
|
||||
|
||||
Sharing one default across every host is deliberate: the dev loops
|
||||
(`bun run serve:web`, `dev:web`, `dev`) and the installed desktop app
|
||||
read and write the same state, so a provider or companion configured once is
|
||||
testable everywhere, and troubleshooting only ever has one directory to
|
||||
look at. When you *do* want an isolated sandbox, `NOMIFUN_DATA_DIR` or
|
||||
`--data-dir` is the escape hatch. (The dev scripts no longer pass a
|
||||
repo-relative `--data-dir`; the old `data/` and `.dev-data/` directories
|
||||
are not read by anything and their contents are **not** auto-migrated —
|
||||
copy them into the new root or point `NOMIFUN_DATA_DIR` back at them if
|
||||
you still need them.)
|
||||
|
||||
What makes the sharing safe is an **exclusive server lock**: at boot
|
||||
(`bootstrap::init_environment`, before the database is opened) the backend
|
||||
takes an OS-level exclusive advisory lock on `{data_dir}/server.lock`
|
||||
(`fs2`: `flock` on Unix, `LockFileEx` on Windows). The OS releases the lock
|
||||
when the process exits *or crashes*, so a leftover `server.lock` file is
|
||||
harmless and needs no staleness heuristics. A second backend on the same
|
||||
directory fails fast with an error naming the holder (pid + exe) and the
|
||||
two ways out: close the other instance, or point this one at its own
|
||||
directory. The desktop shell now surfaces a backend-startup failure in a
|
||||
native error dialog and exits (previously a silent white window).
|
||||
`nomicore doctor` and the `mcp-*` stdio subcommands are unaffected by the
|
||||
lock (`doctor` is designed to run alongside a live server).
|
||||
|
||||
## SQLite via `sqlx`
|
||||
|
||||
[`nomifun-db`](../../crates/backend/nomifun-db/) is the data layer. Highlights
|
||||
from [`crates/backend/nomifun-db/src/lib.rs`](../../crates/backend/nomifun-db/src/lib.rs):
|
||||
|
||||
- `Database` — owns the `sqlx::SqlitePool` and the migrations. Exposed via
|
||||
`nomifun-db::SqlitePool` re-export.
|
||||
- `init_database` — opens the file, runs embedded migrations.
|
||||
- `init_database_memory` — in-memory variant used by tests.
|
||||
|
||||
The crate exposes ~20 repository **trait + Sqlite-impl** pairs. A non-exhaustive
|
||||
list (see the `pub use repository::{...}` block in `lib.rs` for all of them):
|
||||
|
||||
| Trait | Sqlite implementation | Stores |
|
||||
| --- | --- | --- |
|
||||
| `IUserRepository` | `SqliteUserRepository` | Users, password hashes, the system default user |
|
||||
| `IConversationRepository` | `SqliteConversationRepository` | Conversations + messages, with filters and full-text search rows |
|
||||
| `IAgentMetadataRepository` | `SqliteAgentMetadataRepository` | ACP handshake results, available models, agent-binary metadata |
|
||||
| `IAcpSessionRepository` | `SqliteAcpSessionRepository` | Persistent ACP sessions for resume after restart |
|
||||
| `IMcpServerRepository` | `SqliteMcpServerRepository` | Configured MCP servers (CRUD) |
|
||||
| `IOAuthTokenRepository` | `SqliteOAuthTokenRepository` | Encrypted OAuth tokens for HTTP MCP servers |
|
||||
| `IProviderRepository` | `SqliteProviderRepository` | LLM provider credentials (encrypted) |
|
||||
| `IRemoteAgentRepository` | `SqliteRemoteAgentRepository` | Remote-agent endpoints |
|
||||
| `ITeamRepository` | `SqliteTeamRepository` | Multi-agent teams, tasks, mailbox state |
|
||||
| `IRequirementRepository` | `SqliteRequirementRepository` | AutoWork requirements (intentionally **no foreign key** to conversations — the loop survives conversation deletion) |
|
||||
| `ICronRepository` | `SqliteCronRepository` | Scheduled tasks and their timezone-normalized expressions |
|
||||
| `ITerminalRepository` | `SqliteTerminalRepository` | Terminal session metadata |
|
||||
| `IAssistantRepository` / `IAssistantOverrideRepository` | `SqliteAssistantRepository` / `SqliteAssistantOverrideRepository` | Assistants and per-installation overrides |
|
||||
| `IChannelRepository` | `SqliteChannelRepository` | External chat-channel plugin configs (Telegram / Lark / DingTalk / WeChat) |
|
||||
| `IClientPreferenceRepository` | `SqliteClientPreferenceRepository` | Per-client preferences |
|
||||
| `ITagSettingRepository` | `SqliteTagSettingRepository` | Tag-based grouping (used by AutoWork) |
|
||||
| `ISettingsRepository` | `SqliteSettingsRepository` | Misc app settings |
|
||||
| `IWebhookRepository` | `SqliteWebhookRepository` | Outbound webhook destinations (Lark) |
|
||||
|
||||
A few row-update params types travel alongside (`UpdateAgentHandshakeParams`,
|
||||
`ConversationFilters`, `ConversationRowUpdate`, `MessageRowUpdate`,
|
||||
`MessageSearchRow`, `UpdateCronJobParams`, `UpsertOAuthTokenParams`,
|
||||
`CreateProviderParams`, `UpdateRemoteAgentParams`, `UpdateTeamParams`,
|
||||
`UpdateTaskParams`, etc.). The repository traits are the contract; everything
|
||||
above the data layer talks to them, never to the pool directly.
|
||||
|
||||
### Migrations
|
||||
|
||||
Migrations are SQL files embedded with `sqlx::migrate!`. They run on every
|
||||
boot inside `init_database`. Schemas evolve forward only; downgrades are not
|
||||
supported.
|
||||
|
||||
### Per-conversation foreign-key note
|
||||
|
||||
`requirements` (the AutoWork queue) intentionally has **no foreign key** on
|
||||
`conversation_id`. The AutoWork orchestrator (`nomifun-requirement`) is
|
||||
backend-authoritative and survives conversation deletion — the FK would couple
|
||||
its lifecycle to the conversation's, defeating the boot-resume design. (See
|
||||
the user memory entry "AutoWork backend-authoritative".)
|
||||
|
||||
## Encryption at rest — AES-GCM
|
||||
|
||||
Sensitive strings (provider API keys, OAuth tokens, channel-bot tokens, ...)
|
||||
are encrypted before insertion using AES-256-GCM via
|
||||
`nomifun_common::crypto::{encrypt_string, decrypt_string}` and the
|
||||
encryption key derived in `nomifun_app::derive_encryption_key`.
|
||||
|
||||
The master key is not a file: `derive_encryption_key` is the SHA-256 of the
|
||||
JWT secret, which is resolved at boot as env `JWT_SECRET` → the system
|
||||
user's `jwt_secret` column → freshly generated and persisted to the
|
||||
database. The key is per-installation and never crosses the wire; losing
|
||||
the JWT secret renders all encrypted columns unreadable (this is by design
|
||||
— it is the kill switch).
|
||||
|
||||
The `aes-gcm` crate version pinned in the workspace is `0.10`.
|
||||
|
||||
## Per-conversation workspaces
|
||||
|
||||
Each conversation owns a directory the agent can freely read and write:
|
||||
|
||||
```
|
||||
{work_dir}/conversations/{label}-temp-{conversation_id}/
|
||||
```
|
||||
|
||||
- `work_dir` — the runtime work directory; falls back to the data dir when
|
||||
not set explicitly. Sources, in order: `--work-dir` flag → env
|
||||
`NOMIFUN_WORK_DIR` → `<data_dir>`.
|
||||
- `label` — a short slug derived from the conversation title.
|
||||
- `temp` — literal string; signals these directories are mutable scratch
|
||||
space the user can also drop files into.
|
||||
- `conversation_id` — the conversation's unique id (UUID v7 with a short
|
||||
prefix from `nomifun_common::id`).
|
||||
|
||||
The directory is created lazily the first time the conversation needs it.
|
||||
On conversation deletion the directory is removed (the
|
||||
`OnConversationDelete` hook in `nomifun_common::hooks`). File operations
|
||||
inside it are sandboxed and watched:
|
||||
|
||||
- [`nomifun-file::path_safety`](../../crates/backend/nomifun-file/src/path_safety.rs)
|
||||
rejects paths that escape the workspace (e.g. via `..` or absolute roots).
|
||||
- [`nomifun-file::watch_service`](../../crates/backend/nomifun-file/src/watch_service.rs)
|
||||
uses `notify` to surface filesystem changes back to the SPA over WS.
|
||||
- [`nomifun-file::snapshot_service`](../../crates/backend/nomifun-file/src/snapshot_service/)
|
||||
records before/after snapshots for tool-edit auditability.
|
||||
|
||||
The repo enforces an extra constraint via
|
||||
`nomifun_common::error::workspace_path_has_edge_whitespace_segment`: no
|
||||
directory name in a workspace path may begin or end with whitespace (or
|
||||
consist entirely of whitespace). Such names break Win32 path round-tripping
|
||||
and are visually indistinguishable in any UI. Interior whitespace is fully
|
||||
supported — the default per-user data dir on macOS
|
||||
(`~/Library/Application Support/NomiFun/Nomi`) contains a space, and every
|
||||
process-spawn pipeline passes the workspace as a discrete argument
|
||||
(`Command::current_dir`, PTY cwd, ACP session JSON), which is
|
||||
whitespace-safe.
|
||||
|
||||
### Knowledge-base mounts (`.nomi/knowledge/`)
|
||||
|
||||
When a conversation, terminal session, or companion binding brings knowledge
|
||||
bases into a workspace, they are mounted under
|
||||
`{workspace}/.nomi/knowledge/` — the same `.nomi/` domain as project
|
||||
skills — as junctions/symlinks with a copy fallback, plus a built-in
|
||||
`.gitignore` so mounts never enter version control. A platform-managed
|
||||
`README.md` (retrieval protocol, per-base digests + TOC, write-back
|
||||
rules) is rewritten there on every launch. Legacy mounts under the old
|
||||
`{workspace}/.nomifun/knowledge/` location are cleaned up automatically
|
||||
on the next sync.
|
||||
|
||||
## Companion data (the `companion/` file domain)
|
||||
|
||||
The virtual companion's data deliberately stays **out of the main database's
|
||||
migration system** — it is a file domain that can be exported or wiped
|
||||
as a whole (see the [Companions guide](../guides/companions.md)). The multi-companion
|
||||
layout:
|
||||
|
||||
```
|
||||
<data_dir>/companion/
|
||||
├── shared/ shared memory hub (one copy for all companions)
|
||||
│ ├── config.json SharedCompanionConfig: collect switches, learn interval & model, default_companion_id
|
||||
│ ├── events/YYYYMMDD.jsonl raw events from the collection pipeline (privacy-sensitive; export is opt-in)
|
||||
│ └── memory.db standalone SQLite (PRAGMA user_version ladder):
|
||||
│ shared memories/suggestions/learn history + per-companion runtime
|
||||
│ state (companion_runtime_state: XP, …)
|
||||
└── companions/
|
||||
└── {companion_id}/ companion_{uuid_v7}; the directory is the source of truth
|
||||
└── config.json CompanionProfileConfig: name/character/persona/per-companion model/desktop-companion toggle & position
|
||||
```
|
||||
|
||||
The legacy single-companion layout `companion/nomi/` is migrated automatically on
|
||||
first boot into `shared/` plus a first companion named "Nomi"; the old
|
||||
directory gets a `.migrated` marker and is kept around (cleanup after
|
||||
one release cycle).
|
||||
|
||||
Knowledge bases bound to companions do not live in the `companion/` domain: the
|
||||
bindings are stored in the main database as
|
||||
`knowledge_bindings('companion', companion_id)`, and the base content lives in the
|
||||
knowledge bases' own managed directories (URL-sourced bases keep their
|
||||
fetched markdown snapshots in a `snapshots/` subdirectory there).
|
||||
|
||||
## Bundled bun runtime
|
||||
|
||||
NomiFun ships its own `bun` runtime (1.3.13) so MCP servers and tool
|
||||
subprocesses do not require a system Node.js install:
|
||||
|
||||
| Step | What happens |
|
||||
| --- | --- |
|
||||
| Build time | The bun binary for the target OS/arch is **zstd-compressed** and embedded into `nomifun-runtime` via `include_dir!`. |
|
||||
| First run | `nomifun_runtime::init(&data_dir)` extracts the binary into a **`<data_dir>/runtime/`** subtree (see the runtime-cache details below). |
|
||||
| Boot | `enhance_process_path()` prepends the bun bin dir to the process `PATH` **before any tokio thread is built** (the order is enforced in both host `main.rs` files). |
|
||||
| Spawn | `nomifun_runtime::spawn::Builder` produces children with that merged `PATH` so `npx`, `bun`, and other JS tools resolve correctly. |
|
||||
| Cleanup | `kill_process_tree` cross-platform tree-kills agent / MCP children on cancellation. |
|
||||
|
||||
The runtime cache is anchored to the backend's `data_dir`:
|
||||
[`nomifun_runtime::init(&data_dir)`](../../crates/backend/nomifun-runtime/src/cache.rs)
|
||||
records `<data_dir>/runtime` as the cache root, so on the desktop the bun
|
||||
binary extracts under `<data_dir>/runtime/bun-<version>-<sha12>/` —
|
||||
i.e. `%LOCALAPPDATA%\NomiFun\Nomi\runtime\bun-…\` by default on Windows
|
||||
(the per-user app-data equivalents on macOS/Linux), or
|
||||
`$NOMIFUN_DATA_DIR/Nomi/runtime/bun-…/` when the env var is set. When
|
||||
`init` has not been called (the `mcp-*` subcommands, unit tests, `build.rs`)
|
||||
the cache falls back to the platform cache dir via `dirs::cache_dir()`:
|
||||
`%LOCALAPPDATA%\nomifun\runtime\` on Windows, `~/Library/Caches/nomifun/runtime/`
|
||||
on macOS, `$XDG_CACHE_HOME/nomifun/runtime/` (or `~/.cache/nomifun/runtime/`)
|
||||
on Linux.
|
||||
|
||||
## Logs
|
||||
|
||||
Logs go to `<data_dir>/logs/` via `tracing-appender`. The default level is
|
||||
`info`; override with `--log-level` (e.g. `--log-level info,nomifun_mcp=trace`)
|
||||
or env `RUST_LOG`. The desktop shell additionally keeps a console attached
|
||||
in debug builds (the release build sets `windows_subsystem = "windows"`).
|
||||
|
||||
The logging configuration types — `ResolvedLogging`, `create_file_layer` —
|
||||
live in `nomi_config::logging` (the agent layer's config crate). The
|
||||
backend reaches them through the seam: `nomifun_ai_agent::nomi_config::logging::*`.
|
||||
|
||||
## First-run state
|
||||
|
||||
On a brand-new install the boot sequence is:
|
||||
|
||||
```
|
||||
1. nomifun-runtime::init extract bun into OS cache
|
||||
2. enhance_process_path prepend cache bin dir to PATH
|
||||
3. bootstrap::init_environment resolve work_dir / log_dir, init tracing,
|
||||
take the exclusive {data_dir}/server.lock
|
||||
4. bootstrap::init_data_layer open database, run migrations
|
||||
5. AppServices::from_config instantiate every service
|
||||
6. ensure_admin_credentials (web) pre-seed admin if NOMIFUN_ADMIN_PASSWORD is set
|
||||
7. create_router → axum::serve bind and start serving
|
||||
```
|
||||
|
||||
Step 3 is where a second backend on an already-claimed data dir fails fast
|
||||
(see "One directory, one state" above).
|
||||
|
||||
In the desktop shell step 6 is skipped, but the desktop is not the old blanket
|
||||
`--local` story: it uses `TrustLocalToken` and trusts only its own WebView's
|
||||
per-boot secret. In the web host, if no admin exists and no
|
||||
`NOMIFUN_ADMIN_PASSWORD` is set, the install enters **interactive first-run
|
||||
setup**: the next browser visitor chooses a username and password through
|
||||
`POST /api/auth/setup`. A warning is logged if first-run setup is exposed on a
|
||||
non-loopback bind address.
|
||||
|
||||
## Backups and reinstall
|
||||
|
||||
- **Database** — copy `<data_dir>/nomifun-backend.db` (sqlx single-file SQLite).
|
||||
- **Encryption key** — nothing separate to copy: the key is derived from the
|
||||
JWT secret, which lives in the database (unless supplied via env
|
||||
`JWT_SECRET`), so a database copy carries the encrypted columns *and* the
|
||||
means to read them.
|
||||
- **Workspaces** — copy `<work_dir>/conversations/` if you want to keep the
|
||||
files agents wrote.
|
||||
- **Companion data** — copy `<data_dir>/companion/` (shared memory hub + per-companion
|
||||
profiles), or use the in-app migration bundles instead (see the
|
||||
[Companions guide](../guides/companions.md)).
|
||||
- **Bun runtime cache** — disposable; will be re-extracted on next boot.
|
||||
|
||||
A clean uninstall therefore deletes the data dir, the work dir (if set
|
||||
separately), and the OS cache dir.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- The repository traits and their consumers are catalogued in
|
||||
[`backend-crates.md`](backend-crates.md).
|
||||
- The HTTP routes that hit each repository, and the WS topics that mirror
|
||||
state changes, are summarized in [`communication.md`](communication.md).
|
||||
- The agent-side data (TOML config, skills, file cache) is described in
|
||||
[`agent-engine.md`](agent-engine.md).
|
||||
@@ -0,0 +1,179 @@
|
||||
# 数据与存储
|
||||
|
||||
NomiFun 把状态保存在三个地方:一个 SQLite 数据库(一切结构化数据的真理之源)、一个按安装划分的**数据目录**(数据库文件、日志、操作系统缓存的运行时),以及按会话划分的**工作目录**(agent 读写的文件)。本页解释什么内容存在哪里、怎么命名,以及如何加以保护。
|
||||
|
||||
## 数据目录
|
||||
|
||||
| 宿主 | 默认路径 | 覆盖方式 |
|
||||
| --- | --- | --- |
|
||||
| 桌面(`nomifun-desktop`) | 按用户的应用数据目录:Windows 上的 `%LOCALAPPDATA%\NomiFun\Nomi`,macOS 上的 `~/Library/Application Support/NomiFun/Nomi`,Linux 上的 `$XDG_DATA_HOME/NomiFun/Nomi`(通常为 `~/.local/share/NomiFun/Nomi`)。设置了 `NOMIFUN_DATA_DIR` 时变为 `$NOMIFUN_DATA_DIR/Nomi`。位于 `<system temp>/nomifun-data/Nomi` 的旧版安装会在启动时被自动搬迁(一次性;旧目录保留作备份)。 | 环境变量 `NOMIFUN_DATA_DIR` |
|
||||
| Web(`nomifun-web`)与 `nomicore` bin | 与桌面外壳**完全相同**的按用户目录 —— `%LOCALAPPDATA%\NomiFun\Nomi` / `~/Library/Application Support/NomiFun/Nomi` / `$XDG_DATA_HOME/NomiFun/Nomi`(旧的相对 `./data` 默认值已删除)。设置了 `NOMIFUN_DATA_DIR` 时取**字面值**(不追加 `/Nomi`),因此 Docker `/data`、systemd `/var/lib/nomifun` 部署不受影响。 | 命令行 `--data-dir` 或环境变量 `NOMIFUN_DATA_DIR` |
|
||||
|
||||
数据目录内部:
|
||||
|
||||
```
|
||||
<data_dir>/
|
||||
├── nomifun-backend.db SQLite database (sqlx)
|
||||
├── server.lock exclusive server-lock address file (the lock lives on
|
||||
│ the open OS handle; a leftover file is harmless)
|
||||
├── logs/ tracing-appender file output (rotated daily)
|
||||
├── conversations/ per-conversation workspaces (see below)
|
||||
└── companion/ companion file domain (shared memory hub + per-companion profiles, see below)
|
||||
```
|
||||
|
||||
三个宿主的缺省默认值都经由同一个共享辅助函数解析:[`nomifun_app::cli::default_data_dir()`](../../crates/backend/nomifun-app/src/cli.rs) —— `dirs::data_local_dir()/NomiFun/Nomi`(按用户的 application-data 位置),仅当操作系统报告不出用户目录时才极端回退到系统临时目录(`<system temp>/nomifun-data/Nomi`)。环境变量语义保持各宿主原状:桌面外壳对 `NOMIFUN_DATA_DIR` 追加 `"Nomi"`(见 [`apps/desktop/src/main.rs`](../../apps/desktop/src/main.rs)),而 `nomifun-web` 与 `nomicore` 取其字面值(clap `env` 绑定 —— 对 `nomicore` 是新增的,它以前不读这个变量)。位于 `<system temp>/nomifun-data/Nomi` 的既有旧版安装会在启动时被一次性搬迁到新位置([`apps/desktop/src/relocate.rs`](../../apps/desktop/src/relocate.rs)):数据被复制(可再生的缓存/日志留在原地),旧目录保留作备份,随后后端把数据库中存储的绝对路径(知识库根、会话工作区、终端 cwd)改写到新根。
|
||||
|
||||
### 一个目录,一份状态
|
||||
|
||||
所有宿主共用一个默认值是有意为之:开发循环(`bun run serve:web`、`dev:web`、`dev`)与已安装的桌面应用读写同一份状态,因此 provider 或伙伴配置一次、处处可测,排查问题也永远只有一个目录可看。确实需要隔离沙箱时,`NOMIFUN_DATA_DIR` 或 `--data-dir` 就是逃生舱。(dev 脚本不再传仓库相对的 `--data-dir`;旧的 `data/` 与 `.dev-data/` 目录不再被任何东西读取,其内容也**不会**被自动迁移 —— 还需要的话请手动拷进新根,或用 `NOMIFUN_DATA_DIR` 指回去。)
|
||||
|
||||
让这种共享变得安全的是**排他服务器锁**:启动时(`bootstrap::init_environment`,早于数据库打开)后端对 `{data_dir}/server.lock` 取 OS 级排他 advisory 锁(`fs2`:Unix 上 `flock`,Windows 上 `LockFileEx`)。进程退出*或崩溃*时锁由 OS 释放,因此残留的 `server.lock` 文件无害,不需要任何过期启发式。同一目录上的第二个后端会快速失败,错误信息点名持有者(pid + exe)并给出两条出路:关掉另一个实例,或让这一个指向自己的独立目录。桌面外壳现在会把后端启动失败弹成原生错误对话框并退出(以前是静默白屏)。`nomicore doctor` 与 `mcp-*` stdio 子命令不受该锁影响(`doctor` 设计上就允许与运行中的服务器并存)。
|
||||
|
||||
## 通过 `sqlx` 操作 SQLite
|
||||
|
||||
[`nomifun-db`](../../crates/backend/nomifun-db/) 是数据层。来自 [`crates/backend/nomifun-db/src/lib.rs`](../../crates/backend/nomifun-db/src/lib.rs) 的要点:
|
||||
|
||||
- `Database` —— 持有 `sqlx::SqlitePool` 与迁移。通过 `nomifun-db::SqlitePool` 再导出。
|
||||
- `init_database` —— 打开文件、运行内嵌迁移。
|
||||
- `init_database_memory` —— 测试用的内存版本。
|
||||
|
||||
该 crate 暴露约 20 对仓储 **trait + Sqlite 实现**。下面是非穷尽列表(完整列表见 `lib.rs` 中的 `pub use repository::{...}` 块):
|
||||
|
||||
| Trait | Sqlite 实现 | 存储 |
|
||||
| --- | --- | --- |
|
||||
| `IUserRepository` | `SqliteUserRepository` | 用户、密码哈希、系统默认用户 |
|
||||
| `IConversationRepository` | `SqliteConversationRepository` | 会话 + 消息,含过滤与全文搜索行 |
|
||||
| `IAgentMetadataRepository` | `SqliteAgentMetadataRepository` | ACP 握手结果、可用模型、agent 二进制元数据 |
|
||||
| `IAcpSessionRepository` | `SqliteAcpSessionRepository` | 持久化 ACP 会话(重启后可恢复) |
|
||||
| `IMcpServerRepository` | `SqliteMcpServerRepository` | 已配置的 MCP 服务器(CRUD) |
|
||||
| `IOAuthTokenRepository` | `SqliteOAuthTokenRepository` | HTTP MCP 服务器的加密 OAuth token |
|
||||
| `IProviderRepository` | `SqliteProviderRepository` | LLM provider 凭证(加密) |
|
||||
| `IRemoteAgentRepository` | `SqliteRemoteAgentRepository` | 远程 agent 端点 |
|
||||
| `ITeamRepository` | `SqliteTeamRepository` | 多 agent 团队、任务、信箱状态 |
|
||||
| `IRequirementRepository` | `SqliteRequirementRepository` | AutoWork requirements(**有意不与 conversations 建立外键** —— 即使会话被删除,循环也要存活) |
|
||||
| `ICronRepository` | `SqliteCronRepository` | 定时任务及其按时区归一化的表达式 |
|
||||
| `ITerminalRepository` | `SqliteTerminalRepository` | 终端会话元数据 |
|
||||
| `IAssistantRepository` / `IAssistantOverrideRepository` | `SqliteAssistantRepository` / `SqliteAssistantOverrideRepository` | 助手与按安装的覆盖 |
|
||||
| `IChannelRepository` | `SqliteChannelRepository` | 外部聊天渠道插件配置(Telegram / Lark / DingTalk / WeChat) |
|
||||
| `IClientPreferenceRepository` | `SqliteClientPreferenceRepository` | 按客户端的偏好 |
|
||||
| `ITagSettingRepository` | `SqliteTagSettingRepository` | 基于标签的分组(被 AutoWork 使用) |
|
||||
| `ISettingsRepository` | `SqliteSettingsRepository` | 杂项应用设置 |
|
||||
| `IWebhookRepository` | `SqliteWebhookRepository` | 出站 webhook 目的地(飞书 Lark) |
|
||||
|
||||
伴随其行的若干 update params 类型(`UpdateAgentHandshakeParams`、`ConversationFilters`、`ConversationRowUpdate`、`MessageRowUpdate`、`MessageSearchRow`、`UpdateCronJobParams`、`UpsertOAuthTokenParams`、`CreateProviderParams`、`UpdateRemoteAgentParams`、`UpdateTeamParams`、`UpdateTaskParams` 等等)。仓储 trait 是契约;数据层之上的一切都通过它们对话,绝不直接面对池。
|
||||
|
||||
### 迁移
|
||||
|
||||
迁移是用 `sqlx::migrate!` 内嵌的 SQL 文件。它们在每次启动 `init_database` 时运行。Schema 只向前演进;不支持降级。
|
||||
|
||||
### 按会话的外键说明
|
||||
|
||||
`requirements`(AutoWork 队列)有意**不**为 `conversation_id` 建立外键。AutoWork 编排器(`nomifun-requirement`)是后端权威的,并能在会话被删除后存活 —— 外键会把它的生命周期与会话耦合在一起,破坏 boot-resume 的设计。(见用户记忆条目 “AutoWork backend-authoritative”。)
|
||||
|
||||
## 静态加密 —— AES-GCM
|
||||
|
||||
敏感字符串(provider API key、OAuth token、渠道 bot token 等)在写入前用 AES-256-GCM 加密,由 `nomifun_common::crypto::{encrypt_string, decrypt_string}` 与 `nomifun_app::derive_encryption_key` 中派生的加密密钥承担。
|
||||
|
||||
主密钥并不是一个文件:`derive_encryption_key` 是对 JWT secret 做 SHA-256,而 JWT secret 在启动时按 环境变量 `JWT_SECRET` → 系统用户的 `jwt_secret` 列 → 新生成并持久化进数据库 的顺序解析。该密钥按安装唯一,永不上线传输;丢失 JWT secret 将使所有加密列无法解读(这是有意为之 —— 它就是急停开关)。
|
||||
|
||||
工作区中锁定的 `aes-gcm` crate 版本是 `0.10`。
|
||||
|
||||
## 按会话的工作区
|
||||
|
||||
每个会话拥有一个 agent 可自由读写的目录:
|
||||
|
||||
```
|
||||
{work_dir}/conversations/{label}-temp-{conversation_id}/
|
||||
```
|
||||
|
||||
- `work_dir` —— 运行时工作目录;未显式设置时回退至数据目录。来源依次为:`--work-dir` flag → 环境变量 `NOMIFUN_WORK_DIR` → `<data_dir>`。
|
||||
- `label` —— 由会话标题派生的短 slug。
|
||||
- `temp` —— 字面字符串;表明这些目录是用户也可以投放文件的可写暂存空间。
|
||||
- `conversation_id` —— 会话的唯一 id(带 `nomifun_common::id` 短前缀的 UUID v7)。
|
||||
|
||||
目录在会话首次需要它时才会被创建。会话被删除时该目录被移除(`nomifun_common::hooks` 中的 `OnConversationDelete` 钩子)。其内的文件操作处于沙箱中并被监视:
|
||||
|
||||
- [`nomifun-file::path_safety`](../../crates/backend/nomifun-file/src/path_safety.rs) 拒绝逃出工作区的路径(如 `..` 或绝对根)。
|
||||
- [`nomifun-file::watch_service`](../../crates/backend/nomifun-file/src/watch_service.rs) 借助 `notify` 把文件系统变更通过 WS 反馈给 SPA。
|
||||
- [`nomifun-file::snapshot_service`](../../crates/backend/nomifun-file/src/snapshot_service/) 记录工具编辑前后的快照以便审计。
|
||||
|
||||
仓库通过 `nomifun_common::error::workspace_path_has_edge_whitespace_segment` 强制额外约束:工作区路径的任何目录名不得以空白字符开头或结尾(或整段全为空白)——这类名称会破坏 Win32 路径往返,且在任何 UI 中都无法分辨。目录名内部含空格则完全支持:macOS 默认的用户级数据目录(`~/Library/Application Support/NomiFun/Nomi`)本身就含空格,而所有子进程管道(`Command::current_dir`、PTY cwd、ACP 会话 JSON)均以独立参数传递工作区路径,对空格安全。
|
||||
|
||||
### 知识库挂载(`.nomi/knowledge/`)
|
||||
|
||||
会话、终端会话或伙伴绑定把知识库带入某个工作区时,库会挂载到 `{workspace}/.nomi/knowledge/` 之下——与项目技能同属 `.nomi/` 域——以 junction/symlink 建链、复制兜底,并内置 `.gitignore` 使挂载永不进版本控制。平台托管的 `README.md`(检索协议、各库梗概 + TOC、回写规则)在每次启动时重写。旧位置 `{workspace}/.nomifun/knowledge/` 的遗留挂载会在下次同步时被自动清理。
|
||||
|
||||
## 伙伴数据(`companion/` 文件域)
|
||||
|
||||
数字伙伴的数据刻意**不进主库迁移体系**,而是一个可整体导出/清空的文件域(详见[伙伴指南](../guides/companions.zh.md))。多伙伴布局如下:
|
||||
|
||||
```
|
||||
<data_dir>/companion/
|
||||
├── shared/ 共享记忆中枢(全体伙伴一份)
|
||||
│ ├── config.json SharedCompanionConfig:采集开关、学习间隔与学习模型、default_companion_id
|
||||
│ ├── events/YYYYMMDD.jsonl 采集链路的原始事件(隐私敏感,导出需显式勾选)
|
||||
│ └── memory.db 独立 SQLite(PRAGMA user_version 版本阶梯):
|
||||
│ 共享记忆/建议/学习历史 + 每宠运行态(companion_runtime_state:XP 等)
|
||||
└── companions/
|
||||
└── {companion_id}/ companion_{uuid_v7},目录即真相
|
||||
└── config.json CompanionProfileConfig:名称/形象/人格/每宠模型/桌宠开关与位置
|
||||
```
|
||||
|
||||
旧版单宠布局 `companion/nomi/` 在首次启动时被自动迁移为 `shared/` + 第一只伙伴 "Nomi",原目录写入 `.migrated` 标记后保留(一个版本周期后清理)。
|
||||
|
||||
伙伴绑定的知识库不在 `companion/` 域内:绑定关系存主库 `knowledge_bindings('companion', companion_id)`,知识库内容在知识库自己的托管目录(URL 源知识库抓取的 markdown 快照存于其 `snapshots/` 子目录)。
|
||||
|
||||
## 内置 bun 运行时
|
||||
|
||||
NomiFun 自带其 `bun` 运行时(1.3.13),使 MCP 服务器与工具子进程不需要系统级 Node.js 安装:
|
||||
|
||||
| 步骤 | 发生了什么 |
|
||||
| --- | --- |
|
||||
| 编译期 | 目标 OS/arch 的 bun 二进制经过 **zstd 压缩** 并通过 `include_dir!` 内嵌进 `nomifun-runtime`。 |
|
||||
| 首次运行 | `nomifun_runtime::init(&data_dir)` 把二进制解压到 **`<data_dir>/runtime/`** 子树(详见下文运行时缓存说明)。 |
|
||||
| 启动 | `enhance_process_path()` 把 bun 的 bin 目录前置到进程 `PATH`,**且早于任何 tokio 线程被构建**(顺序在两个宿主的 `main.rs` 中都得到强制)。 |
|
||||
| 派生 | `nomifun_runtime::spawn::Builder` 用合并后的 `PATH` 生产子进程,使 `npx`、`bun` 与其他 JS 工具能正确解析。 |
|
||||
| 清理 | `kill_process_tree` 在取消时跨平台地树状终止 agent / MCP 子进程。 |
|
||||
|
||||
运行时缓存锚定在后端的 `data_dir` 上:[`nomifun_runtime::init(&data_dir)`](../../crates/backend/nomifun-runtime/src/cache.rs) 把 `<data_dir>/runtime` 记为缓存根,因此在桌面上 bun 二进制会解压到 `<data_dir>/runtime/bun-<version>-<sha12>/` —— 即 Windows 上默认的 `%LOCALAPPDATA%\NomiFun\Nomi\runtime\bun-…\`(macOS/Linux 为对应的按用户 app-data 位置),或设置了 env var 时的 `$NOMIFUN_DATA_DIR/Nomi/runtime/bun-…/`。当 `init` 未被调用时(`mcp-*` 子命令、单元测试、`build.rs`),缓存通过 `dirs::cache_dir()` 回退到平台缓存目录:Windows 上的 `%LOCALAPPDATA%\nomifun\runtime\`、macOS 上的 `~/Library/Caches/nomifun/runtime/`、Linux 上的 `$XDG_CACHE_HOME/nomifun/runtime/`(或 `~/.cache/nomifun/runtime/`)。
|
||||
|
||||
## 日志
|
||||
|
||||
日志通过 `tracing-appender` 进入 `<data_dir>/logs/`。默认级别是 `info`;用 `--log-level`(如 `--log-level info,nomifun_mcp=trace`)或环境变量 `RUST_LOG` 覆盖。在 debug 构建中桌面外壳额外保留控制台(release 构建设置 `windows_subsystem = "windows"`)。
|
||||
|
||||
日志配置类型 —— `ResolvedLogging`、`create_file_layer` —— 位于 `nomi_config::logging`(agent 层的配置 crate)。后端通过接缝访问它们:`nomifun_ai_agent::nomi_config::logging::*`。
|
||||
|
||||
## 首次运行状态
|
||||
|
||||
全新安装的启动顺序如下:
|
||||
|
||||
```
|
||||
1. nomifun-runtime::init extract bun into OS cache
|
||||
2. enhance_process_path prepend cache bin dir to PATH
|
||||
3. bootstrap::init_environment resolve work_dir / log_dir, init tracing,
|
||||
take the exclusive {data_dir}/server.lock
|
||||
4. bootstrap::init_data_layer open database, run migrations
|
||||
5. AppServices::from_config instantiate every service
|
||||
6. ensure_admin_credentials (web) pre-seed admin if NOMIFUN_ADMIN_PASSWORD is set
|
||||
7. create_router → axum::serve bind and start serving
|
||||
```
|
||||
|
||||
第 3 步就是第二个后端在已被占用的数据目录上快速失败的地方(见上文「一个目录,一份状态」)。
|
||||
|
||||
桌面外壳跳过第 6 步的管理员预置,但并不是旧式全局 `--local`:它使用 `TrustLocalToken`,只信任自己 WebView 呈递的本次启动 secret。在 Web 宿主中,如果不存在管理员且未设置 `NOMIFUN_ADMIN_PASSWORD`,安装将进入**首次运行的交互式初始化**:下一位访问浏览器的访客通过 `POST /api/auth/setup` 选择用户名与密码。如果首次运行初始化暴露在非 loopback 绑定地址上,会记录一条警告。
|
||||
|
||||
## 备份与重装
|
||||
|
||||
- **数据库** —— 复制 `<data_dir>/nomifun-backend.db`(sqlx 单文件 SQLite)。
|
||||
- **加密密钥** —— 无需单独复制:密钥派生自 JWT secret,而 JWT secret 就存在数据库里(除非经环境变量 `JWT_SECRET` 提供),因此复制数据库即同时带走加密列*与*解读它们的手段。
|
||||
- **工作区** —— 如果想保留 agent 写入的文件,复制 `<work_dir>/conversations/`。
|
||||
- **伙伴数据** —— 复制 `<data_dir>/companion/`(共享记忆中枢 + 每宠配置),或改用应用内的迁移导出包(见[伙伴指南](../guides/companions.zh.md))。
|
||||
- **bun 运行时缓存** —— 可丢弃;下次启动时会重新解压。
|
||||
|
||||
干净卸载因此是删除数据目录、(如果单独设置过)工作目录与 OS 缓存目录。
|
||||
|
||||
## 交叉参考
|
||||
|
||||
- 仓储 trait 及其消费者列在 [`backend-crates.md`](backend-crates.zh.md) 中。
|
||||
- 命中各仓储的 HTTP 路由,以及镜像状态变化的 WS 主题,汇总在 [`communication.md`](communication.zh.md)。
|
||||
- agent 侧的数据(TOML 配置、技能、文件缓存)见 [`agent-engine.md`](agent-engine.zh.md)。
|
||||
@@ -0,0 +1,103 @@
|
||||
# Frontend
|
||||
|
||||
The frontend is a single React 19 SPA in [`ui/`](../../ui/). The Tauri desktop
|
||||
shell and the `nomifun-web` host load the same Vite build from `ui/dist`; the
|
||||
renderer talks to the backend through HTTP and WebSocket, with a small Tauri
|
||||
adapter only for desktop shell operations.
|
||||
|
||||
## Stack
|
||||
|
||||
| Concern | Current choice |
|
||||
| --- | --- |
|
||||
| Framework | React 19 + TypeScript |
|
||||
| Bundler | Vite 6 |
|
||||
| Routing | `react-router-dom` v7 with `HashRouter` |
|
||||
| UI | Arco Design + custom CSS theme layers + UnoCSS |
|
||||
| Data | SWR plus React contexts for app-shaped state |
|
||||
| i18n | `i18next` / `react-i18next`; current app locales are `zh-CN` and `en-US` |
|
||||
| Terminal | `xterm.js` with fit/web-links/webgl addons |
|
||||
| Markdown | `react-markdown`, GFM, KaTeX, Mermaid |
|
||||
|
||||
## Source Layout
|
||||
|
||||
```text
|
||||
ui/src/
|
||||
├── common/ bridge/API/types/util code shared across hosts
|
||||
├── platform/ small substrate for storage/logger/theme/runtime bridge
|
||||
└── renderer/ React app: pages, layout, hooks, services, styles
|
||||
```
|
||||
|
||||
The renderer imports the composite bridge from
|
||||
`ui/src/common/adapter/ipcBridge.ts`. Most product operations are HTTP calls.
|
||||
Tauri-specific operations are guarded behind `isTauri()` and implemented in the
|
||||
adapter layer rather than scattered through pages.
|
||||
|
||||
## Backend URL And Trust
|
||||
|
||||
Desktop:
|
||||
|
||||
- `apps/desktop/src/main.rs` injects `window.__backendPort`.
|
||||
- It also injects a per-boot `window.__nomiLocalTrust` secret.
|
||||
- The init script patches `fetch` and `XMLHttpRequest` so requests to the
|
||||
embedded loopback backend include `x-nomi-local-trust`.
|
||||
|
||||
Web:
|
||||
|
||||
- No port is injected.
|
||||
- The bridge uses same-origin `/api` and `/ws`.
|
||||
- Authenticated web mode uses the session cookie plus CSRF double-submit header.
|
||||
|
||||
## Current Route Map
|
||||
|
||||
The source of truth is
|
||||
[`ui/src/renderer/components/layout/Router.tsx`](../../ui/src/renderer/components/layout/Router.tsx).
|
||||
|
||||
| Route | Surface |
|
||||
| --- | --- |
|
||||
| `/login` | Login / first-run setup. |
|
||||
| `/companion` | Desktop companion window route; outside the normal protected app layout. |
|
||||
| `/guid` | Session start surface. |
|
||||
| `/conversation/:id` | Conversation runtime. |
|
||||
| `/terminal-new` | Terminal creation. |
|
||||
| `/terminal/:id` | Terminal runtime. |
|
||||
| `/models` | Model and agent management. |
|
||||
| `/assistants` | Assistant and skill hub. |
|
||||
| `/mcp` | MCP server management. |
|
||||
| `/open-capabilities` | Remote/public capability exposure. |
|
||||
| `/scheduled`, `/scheduled/:job_id` | Scheduled tasks. |
|
||||
| `/requirements`, `/requirements/extensions`, `/requirements/sources` | Requirements Platform, AutoWork, notification/source extensions. |
|
||||
| `/nomi` | Companion configuration. |
|
||||
| `/knowledge`, `/knowledge/:id` | Knowledge base list/detail. |
|
||||
| `/settings/system` and related settings subroutes | System settings page and sub-sections. |
|
||||
|
||||
Legacy settings paths such as `/settings/model`, `/settings/agent`,
|
||||
`/settings/capabilities`, `/settings/skills-hub`, `/settings/tools`,
|
||||
`/settings/webui`, `/settings/assistants`, and `/settings/webhook` are
|
||||
redirects. Do not document them as primary navigation.
|
||||
|
||||
There is no current `/team/:id` frontend route. Backend team code may still
|
||||
exist, but the product route is not surfaced in the current router.
|
||||
|
||||
## State And Data
|
||||
|
||||
- SWR owns most remote list/detail state.
|
||||
- `AuthProvider`, theme, feedback, preview, and conversation-history contexts
|
||||
own app-shaped state.
|
||||
- `configService` initializes before i18n/theme consumers so early render reads
|
||||
backend-backed preferences.
|
||||
- Realtime events arrive through a singleton WebSocket and are demuxed by event
|
||||
name.
|
||||
|
||||
## Desktop-Specific UX
|
||||
|
||||
Desktop shell behavior is implemented by Tauri commands and plugins:
|
||||
|
||||
- updater check,
|
||||
- companion window reconciliation,
|
||||
- WebUI LAN listener status/start/stop,
|
||||
- keep-awake toggle,
|
||||
- tray label localization,
|
||||
- deep-link forwarding,
|
||||
- tray close behavior.
|
||||
|
||||
Browser builds no-op or degrade desktop-only affordances in the adapter layer.
|
||||
@@ -0,0 +1,129 @@
|
||||
# 前端
|
||||
|
||||
前端是位于 [`ui/`](../../ui/) 的一个 React 19 SPA。两个宿主 —— Tauri 桌面外壳与 `nomifun-web` —— 都加载同一份 Vite 构建产物(`ui/dist`)。渲染进程从不使用 Electron IPC;在两个宿主中它都通过普通的 HTTP 与 WebSocket 与后端通信。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 关注点 | 选择 |
|
||||
| --- | --- |
|
||||
| 框架 | React 19 + TypeScript |
|
||||
| 打包工具 | Vite 6 |
|
||||
| UI 库 | Arco Design(`@arco-design/web-react`)—— 主色 `#4E5969` |
|
||||
| 样式 | UnoCSS(utility 类)+ `ui/src/renderer/styles/themes/` 下按主题划分的 CSS |
|
||||
| 路由 | `react-router-dom` v7 + **`HashRouter`**(对 `file://` 风格宿主与刷新安全至关重要) |
|
||||
| 数据获取 / 缓存 | SWR |
|
||||
| 状态 | React Context(auth、theme、feedback、preview、conversation history)—— 不使用 Redux |
|
||||
| i18n | `i18next` + `react-i18next`,语言包 `zh-CN`、`en-US` |
|
||||
| 编辑器 | Monaco(设置、代码预览)、CodeMirror(更轻量的输入) |
|
||||
| Markdown | `react-markdown` + `remark-gfm` + KaTeX + mermaid |
|
||||
| 终端 | `xterm.js`(含 `xterm-addon-fit`、`xterm-addon-web-links`) |
|
||||
| Service worker | Web 宿主注册了 PWA service worker(参见 [`registerPwa.ts`](../../ui/src/renderer/services/registerPwa.ts));Tauri 外壳显式跳过它 |
|
||||
|
||||
## 三层结构:`common/`、`platform/`、`renderer/`
|
||||
|
||||
`ui/src/` 内的目录划分是承担约定职责的关键。
|
||||
|
||||
```
|
||||
ui/src/
|
||||
├── common/ shared library code (no React)
|
||||
│ ├── adapter/ the bridge factory: HTTP + WS + Tauri shim
|
||||
│ ├── api/ typed API surfaces built on the bridge
|
||||
│ ├── chat/ chat library helpers (rendering hooks, types)
|
||||
│ ├── config/ constants, configService (settings cache)
|
||||
│ ├── platform/ platform-detection helpers
|
||||
│ ├── types/ TypeScript mirrors of nomifun-api-types DTOs
|
||||
│ ├── update/ self-update flow helpers
|
||||
│ ├── utils/ shared utilities (date, hash, ...)
|
||||
│ └── index.ts
|
||||
├── platform/ runtime substrate
|
||||
│ ├── bridge event hub (the legacy "buildProvider/buildEmitter" API)
|
||||
│ ├── logger
|
||||
│ ├── storage
|
||||
│ └── theme
|
||||
├── renderer/ the React app
|
||||
│ ├── pages/ feature pages (conversation, terminal, settings, ...)
|
||||
│ ├── components/ reusable UI components and layout
|
||||
│ ├── hooks/ hooks and React Contexts (Auth, Theme, Feedback, ...)
|
||||
│ ├── services/ i18n, FileService, PasteService, SpeechToTextService, registerPwa
|
||||
│ ├── styles/ Arco overrides and theme variables
|
||||
│ ├── utils/ renderer-specific utilities
|
||||
│ ├── main.tsx entry point (createRoot)
|
||||
│ └── index.html
|
||||
└── shims/ small interop shims pulled in by Vite
|
||||
```
|
||||
|
||||
这种划分是有意设计的:`common/` 不知道 DOM 或 React 的存在;`platform/` 是接好桥事件中心与 logger 的小型基板;`renderer/` 才是真正的应用。这让桥接逻辑可以脱离 React 进行测试,并且如果将来出现第二个客户端目标,可以共享 `common/`。
|
||||
|
||||
## 适配层(桥接层)
|
||||
|
||||
文件位置:[`ui/src/common/adapter/`](../../ui/src/common/adapter/)。
|
||||
|
||||
适配层是前端可移植性故事的核心。它对外暴露一个稳定的形状 —— `provider/invoke` 用于请求—响应,`on/emit` 用于事件 —— 渲染进程的其余部分都消费这个形状。该形状之下,它根据宿主把调用路由到三种传输之一:
|
||||
|
||||
| 适配文件 | 传输 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| [`httpBridge.ts`](../../ui/src/common/adapter/httpBridge.ts) | HTTP `fetch` + 单例 WebSocket | 默认 —— 所有 `/api/*` 与 `/ws` 流量。 |
|
||||
| [`tauriShell.ts`](../../ui/src/common/adapter/tauriShell.ts) | Tauri JS API 与插件(`@tauri-apps/api`、`tauri-plugin-*`) | 仅用于操作系统外壳:窗口控制、对话框、OS 路径、开机启动、通知、深链接、自更新。由 `isTauri()` 守护。 |
|
||||
| [`browser.ts`](../../ui/src/common/adapter/browser.ts) | 进入 platform 事件中心的旧版 WebSocket 桥接 | 把 `platform/` 的 `bridge.emit` 调用接到同一个 `/ws` 端点,并处理 auth 过期重定向。 |
|
||||
|
||||
复合体 —— 由 [`ipcBridge.ts`](../../ui/src/common/adapter/ipcBridge.ts) 导出 —— 才是应用其余部分引入的对象。在渲染进程看来,每次操作都长得一样,无论它最终走的是 HTTP、WS 还是 Tauri-IPC。
|
||||
|
||||
### 解析后端 URL
|
||||
|
||||
渲染进程需要知道与之对话的 URL,而答案因宿主而异:
|
||||
|
||||
```ts
|
||||
// ui/src/common/adapter/httpBridge.ts (excerpted)
|
||||
function getBackendPort(): number {
|
||||
if (typeof window !== 'undefined' && window.__backendPort) {
|
||||
return window.__backendPort; // desktop (Tauri): injected by init script
|
||||
}
|
||||
return globalThis.__backendPort ?? 13400; // last-resort fallback
|
||||
}
|
||||
|
||||
function isWebUiBrowserMode(): boolean {
|
||||
return typeof window !== 'undefined' && !window.__backendPort;
|
||||
}
|
||||
|
||||
export function getBaseUrl(): string {
|
||||
if (isWebUiBrowserMode()) return ''; // same-origin (browser)
|
||||
return `http://127.0.0.1:${getBackendPort()}`; // desktop
|
||||
}
|
||||
```
|
||||
|
||||
在桌面外壳中,Tauri 主进程在任何页面脚本执行之前通过**初始化脚本**注入 `window.__backendPort`(参见 [`apps/desktop/src/main.rs`](../../apps/desktop/src/main.rs))—— 因此渲染进程的第一次调用就能看到正确的端口,无竞争。在 Web 宿主中不会注入端口;`getBaseUrl` 返回 `''`,`fetch` 把 URL 解析到页面自身的来源。
|
||||
|
||||
### CSRF 双提交
|
||||
|
||||
当宿主以认证模式运行(即未带 `--insecure-no-auth` 的 Web 宿主),后端会签发非 HttpOnly 的 cookie `nomifun-csrf-token`。在状态变更请求(POST / PUT / PATCH / DELETE)上,桥读取该 cookie 并把它回显到 `x-csrf-token` 头里。桌面外壳使用 `TrustLocalToken`:WebView 会在请求中带上 `window.__nomiLocalTrust` 注入的本地信任 secret,而不是关闭所有鉴权。
|
||||
|
||||
## 路由 —— `HashRouter`
|
||||
|
||||
[`ui/src/renderer/components/layout/Router.tsx`](../../ui/src/renderer/components/layout/Router.tsx) 是唯一的路由组件。它使用 **`HashRouter`**(形如 `/#/conversation/abc123` 的 URL),原因有两个:
|
||||
|
||||
1. Tauri 外壳通过 `tauri://` / `file://` 协议加载 SPA;`BrowserRouter` 在该协议下经历的页面重新加载(如深链接或应用内导航)后无法保留状态。
|
||||
2. Web 宿主通过 `tower_http::services::ServeDir` 提供 SPA,并启用 `append_index_html_on_directories(true)`。Hash 路由意味着浏览器访问的任何路径都返回 `index.html`,由 SPA 完成其余工作 —— 静态服务器无需自定义 catch-all。
|
||||
|
||||
路由表的顶层条目涵盖会话运行时(`/guid`、`/conversation/:id`)、模型(`/models`)、助手与技能(`/assistants`)、MCP(`/mcp`)、开放能力(`/open-capabilities`)、终端(`/terminal-new`、`/terminal/:id`)、需求/AutoWork(`/requirements/*`、`/autowork` redirect)、定时任务(`/scheduled`、`/scheduled/:job_id`)、桌面伙伴(`/nomi` 配置页、`/companion` 桌面窗口)、知识库(`/knowledge`、`/knowledge/:id`)以及认证(`/login`)。旧 settings 路径只作为重定向保留;当前没有 `/team/:id` 前端路由。
|
||||
|
||||
页面通过 `React.lazy` 加载,使用 `<AppLoader>` 作为 fallback,使初始包保持精简。
|
||||
|
||||
## 状态与数据
|
||||
|
||||
- **SWR** 是主要的数据层。约定是任何列表或详情视图都声明一个 SWR key 字符串及一个 fetcher;HTTP 响应到达后变更操作会调用 `mutate(key)`。`ipcBridge.*.invoke` 的返回值直接喂给 SWR。
|
||||
- **React Context** 承载不属于 SWR 的应用形态状态:认证(`AuthProvider`)、主题(`ThemeProvider`)、反馈 toast(`FeedbackProvider`)、文件预览(`PreviewProvider`),以及对话历史列表(`ConversationHistoryProvider`)。
|
||||
- **`configService`**(`ui/src/common/config/configService.ts`)缓存后端设置;[`main.tsx`](../../ui/src/renderer/main.tsx) 中的入口点会在 i18n / theme 代码加载前启动 `configService.initialize()`,因此这些子系统在首次渲染时读到的是权威设置。
|
||||
|
||||
## 主题
|
||||
|
||||
Arco 的 `ConfigProvider` 在根处包裹应用,主色为 `primaryColor: '#4E5969'`,并按语言提供 locale(`enUS`、`zhCN`、`zhTW`、`jaJP`、`koKR` —— 韩语包用英语日历 / datepicker 字段做了补丁,因为 Arco 的 `koKR` 缺这些)。主题(`light`、`dark`、品牌变体)以纯 CSS 文件叠在 `ui/src/renderer/styles/themes/index.css` 中,通过 `ThemeProvider` 切换。
|
||||
|
||||
UnoCSS 与 Arco 并行提供 utility 类 —— 其配置位于仓库根目录的 `uno.config.ts`。Arco 的自定义覆盖位于 `ui/src/renderer/styles/arco-override.css`。
|
||||
|
||||
## 国际化
|
||||
|
||||
[`ui/src/renderer/services/i18n`](../../ui/src/renderer/services/) 用上述五种语言初始化 `i18next`。字符串按功能组织,解析后的语言通过 `main.tsx` 中的 `arcoLocales` map 流入 Arco。切换语言无需重新加载 —— i18next 与 Arco 都会按新语言重新计算。
|
||||
|
||||
## 一点平台特定的 UX
|
||||
|
||||
桌面外壳在 Windows / Linux 上是**无边框**的([`ui/src/renderer/components/layout/Titlebar/`](../../ui/src/renderer/components/layout/Titlebar/) 中的 React 标题栏通过 `@tauri-apps/api/window` 绘制最小化 / 最大化 / 关闭按钮);macOS 通过 `TitleBarStyle::Overlay` 保留原生交通灯按钮。同一份 SPA 在浏览器中会隐藏标题栏,让浏览器外框处理它。区别在运行时通过 `isTauri()`(定义于 `tauriShell.ts`)来检测。
|
||||
@@ -0,0 +1,172 @@
|
||||
# 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`](backend-crates.md) — the 29 `nomifun-*` backend crates.
|
||||
- [`agent-engine.md`](agent-engine.md) — the 15 `nomi-*` agent crates.
|
||||
- [`frontend.md`](frontend.md) — the React SPA, adapter layer, routing.
|
||||
- [`communication.md`](communication.md) — HTTP / WebSocket / Tauri IPC / ACP / MCP.
|
||||
- [`data-and-storage.md`](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`](../../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 `nomifun` is reserved for technical identifiers only —
|
||||
the npm/JS package id, the Rust crate prefix `nomifun-*`, the Tauri bundle
|
||||
identifier `com.nomifun.desktop`, environment variables `NOMIFUN_*`, 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`](../../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`](../../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`](../../crates/backend/nomifun-app/src/router/routes.rs).
|
||||
@@ -0,0 +1,138 @@
|
||||
# 架构总览
|
||||
|
||||
NomiFun 围绕一个核心原则构建:**一份 Rust 后端、两种宿主形态、一份前端**。无论你启动桌面产品 **NomiFun**,还是自托管 Web 服务器,同一个 `axum` HTTP/WS 服务器(`nomifun-app`,二进制 `nomicore`)都在宿主进程中执行。`ui/` 下的 React 19 SPA 是唯一客户端,它始终通过普通的 HTTP 与 WebSocket 通信 —— 没有 Electron preload,也没有 Tauri 自定义协议。
|
||||
|
||||
本文档是这张地图的总图。配套的四篇文档分别深入介绍各个部分:
|
||||
|
||||
- [`backend-crates.md`](backend-crates.zh.md) —— 29 个 `nomifun-*` crate。
|
||||
- [`agent-engine.md`](agent-engine.zh.md) —— 15 个 `nomi-*` crate(AI 引擎)。
|
||||
- [`frontend.md`](frontend.zh.md) —— React SPA、适配层、路由。
|
||||
- [`communication.md`](communication.zh.md) —— HTTP / WebSocket / Tauri IPC / ACP / MCP。
|
||||
- [`data-and-storage.md`](data-and-storage.zh.md) —— SQLite、工作区、运行时。
|
||||
|
||||
## 双宿主模型
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 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│
|
||||
│ injects window.__nomiLocalTrust │ │ --insecure-no-auth opts │
|
||||
│ AuthPolicy::TrustLocalToken │ │ into no-auth mode │
|
||||
│ Tauri command: check_for_updates │ │ serves SPA as fallback │
|
||||
└────────────────────────────────────┘ └──────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ nomifun-app (binary nomicore) │
|
||||
│ composition root · axum router │
|
||||
│ bootstrap → data layer → services │
|
||||
│ /api · /ws · Routes from 29 crates │
|
||||
└─────────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ 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
|
||||
```
|
||||
|
||||
## 一次请求的流转
|
||||
|
||||
一个典型的用户消息 ——“向会话 X 中的 Claude agent 发送一条聊天” —— 会穿过图中的每一层。下方追踪过程列出了真实参与的类型与文件。
|
||||
|
||||
```
|
||||
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 only backend crate that sees 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)
|
||||
```
|
||||
|
||||
## 三大 crate 分组
|
||||
|
||||
Cargo 工作区(根 [`Cargo.toml`](../../Cargo.toml),`resolver = "3"`,`edition = "2024"`)按三个文件夹分组,使边界不仅在包名中可见,在磁盘上也可见:
|
||||
|
||||
| 目录 | 用途 | Crate 前缀 | 数量 |
|
||||
| --- | --- | --- | --- |
|
||||
| `crates/agent/` | AI 引擎 —— providers、tools、sessions、MCP、skills、browser/computer-use | `nomi-*` | 15 |
|
||||
| `crates/backend/` | HTTP/WS 服务器、数据、认证、各项功能 | `nomifun-*` | 29 |
|
||||
| `crates/shared/` | 真正跨层共享工具 | mixed | 2 |
|
||||
|
||||
agent 分组是**基本自包含的** —— `nomi-*` crate 不引用 `nomifun-*` crate、工作区根目录或 Tauri / sqlx / axum 等后端框架。反向依赖默认通过 `nomifun-ai-agent` 这条接缝汇集,它再导出 `nomi_config`、`nomi_types` 和 `RequirementSink`。当前 `nomifun-app` 与 `nomifun-gateway` 为 browser/computer-use bridge 存在 feature-gated 直接依赖例外;新增例外必须有明确 feature gate 和文档说明。
|
||||
|
||||
## 各部分的位置
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
## 品牌与标识
|
||||
|
||||
- **NomiFun** —— 桌面产品和项目 / 品牌字标(驼峰式书写,N 与 F 大写)。在散文中使用此写法。
|
||||
- 小写的 `nomifun` 仅保留给技术标识符 —— npm/JS 包 id、Rust crate 前缀 `nomifun-*`、Tauri bundle 标识符 `com.nomifun.desktop`、环境变量 `NOMIFUN_*`,以及仓库 / 目录名。
|
||||
|
||||
## 宿主一览
|
||||
|
||||
| 维度 | 桌面(`nomifun-desktop`) | Web(`nomifun-web`) |
|
||||
| --- | --- | --- |
|
||||
| 二进制 | `nomifun-desktop`(Tauri 外壳) | `nomifun-web`(axum 服务器) |
|
||||
| 后端 | 进程内嵌入(独立线程 + tokio runtime) | 进程内嵌入 |
|
||||
| 认证模式 | `TrustLocalToken`:仅信任带本次启动 secret 的 WebView 请求 | 默认要求认证;可通过 `--insecure-no-auth` 关闭 |
|
||||
| 端口 | 启动时选取的空闲 localhost 端口(`bind 127.0.0.1:0`) | `127.0.0.1:8787`(可通过 `--host`/`--port` 配置) |
|
||||
| 后端端口如何送达 SPA | 初始化脚本 `window.__backendPort = <p>` | 同源(`/api` 和 `/ws` 与 SPA 在同一端口提供) |
|
||||
| 静态 SPA | 打包进 Tauri 应用(`tauri.conf.json` 的 distDir) | 由 `tower_http::services::ServeDir` 从 `ui/dist` 提供 |
|
||||
| 操作系统外壳特性 | 窗口控制、深链接、自动更新、开机启动、对话框、通知、单实例 | 无 —— 浏览器即宿主 |
|
||||
| Tauri 命令 | 更新检查、WebUI 状态/启停、companion 同步、keep-awake、托盘标签等桌面能力 | 不适用 |
|
||||
|
||||
桌面二进制的 `main.rs`([`apps/desktop/src/main.rs`](../../apps/desktop/src/main.rs))有意保持精简 —— 大部分逻辑都在 `nomifun_app::run_embedded_server` 中。Web 二进制([`apps/web/src/main.rs`](../../apps/web/src/main.rs))复用同样的引导辅助函数(`init_environment`、`init_data_layer`、`AppServices::from_config`、`create_router`),并补充了 SPA 回退以及首次运行管理员预置(`ensure_admin_credentials`)。
|
||||
Reference in New Issue
Block a user