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

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,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 插件 |
| ACPstdio | 后端 ↔ agent CLI 子进程 | 一段会话的全部 agent 流量,发往 Claude / Codex / Gemini / Qwen / OpenCode 风格的运行时 | 通过 stdin/stdout 的换行分隔 JSON |
| MCPstdio 或 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 ProtocolACP**:在子进程的 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 独立 SQLitePRAGMA user_version 版本阶梯):
│ 共享记忆/建议/学习历史 + 每宠运行态(companion_runtime_stateXP 等)
└── 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)。
+103
View File
@@ -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` |
| 样式 | UnoCSSutility 类)+ `ui/src/renderer/styles/themes/` 下按主题划分的 CSS |
| 路由 | `react-router-dom` v7 + **`HashRouter`**(对 `file://` 风格宿主与刷新安全至关重要) |
| 数据获取 / 缓存 | SWR |
| 状态 | React Contextauth、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`)来检测。
+172
View File
@@ -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-*` crateAI 引擎)。
- [`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`)。