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
+470
View File
@@ -0,0 +1,470 @@
# 通用AI工作台底座平台 — 需求文档
> 文档编号:REQ-COMMONSP-001
> 版本:v2.0.0
> 日期:2026-03-14
> 状态:单租户优先架构
> 项目缩写:COMMONSPCommons Platform
---
## 1. 引言
### 1.1 背景
nomifun-tauri 是目前已有的通用AI工作台底座,采用 Rust(axum) + Vue 前端架构,本地优先(SQLite)+ 插件驱动。核心能力包括 Agent Engine、Skills System、Knowledge RAG、MCP Gateway、Channels Bus。
现有系统是**单租户单用户**架构,需要演进为**单租户优先 + 可扩展租户模型**平台,支持政府/企业/教育三大领域定制,即 PrivBox-AIStation 软硬一体化产品的软件核心。
**架构决策**:PrivBox 典型部署场景为单台设备私有化部署(医院/学校/企业),物理隔离已满足数据安全需求。因此 Phase 1 采用**单租户优先**模式,保留租户抽象作为扩展点,未来需要多租户 SaaS 时改动最小。
本文档定义 **Phase 1 MVP** 的完整需求,目标是建立单租户定制框架(主题/领域/品牌)、多用户管理体系、审计日志能力,为后续三大领域包奠定底座。
### 1.2 范围
| 范围 | 说明 |
|------|------|
| **本文档** | Phase 1 MVP 软件系统需求 |
| **不包括** | 硬件适配(PrivBox HAL)、领域包开发(GovPack/EntPack/EduPack)、插件市场 |
| **技术栈** | Rust(axum) + Vue + SQLite/sqlx(复用 nomifun-tauri 技术栈) |
### 1.3 与现有 nomifun-tauri 的关系
```
nomifun-tauri (当前)
│ Phase 1: 单租户优先 + 主题引擎 + 领域框架 + 多用户
nomifun-tauri (演进后) = Commons Platform v1.0
│ - 默认单租户,数据天然隔离
│ - 保留 tenant_id 字段,未来扩展多租户成本低
│ Phase 2: 三大领域包
PrivBox-AIStation 软件层
```
### 1.4 架构决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 租户隔离策略 | 无需激活 | 单机部署物理隔离已满足需求 |
| 数据层 tenant_id | 保留字段 | 未来扩展多租户 SaaS 成本最低 |
| 默认租户 | 自动创建 | 首次启动自动初始化,无需手动注册 |
| 租户配置 | 保留 | 用于单租户的品牌定制、主题切换 |
---
## 2. 术语表
| 术语 | 英文 | 定义 |
|------|------|------|
| **租户配置** | Tenant Config | 单租户场景下的全局配置,包含品牌、主题、功能开关 |
| **主题** | Theme | 视觉定制包,包含配色、Logo、品牌元素 |
| **领域** | Domain | 行业分类(Government/Enterprise/Education/Custom |
| **领域配置** | Domain Config | 领域特定的初始化参数、分类体系、模板 |
| **技能包** | Skill Pack | 一组相关技能的集合,可按领域打包 |
| **知识库模板** | Knowledge Template | 领域特定的知识库分类和初始数据 |
| **RBAC** | Role-Based Access Control | 基于角色的访问控制 |
| **审计日志** | Audit Log | 记录所有敏感操作的不可篡改日志 |
---
## 3. 角色定义
| 角色 | 说明 | 权限范围 |
|------|------|---------|
| **系统管理员** | 安装部署后的初始管理员 | 所有配置、用户管理、功能开关 |
| **普通用户** | 终端用户 | 使用 AI 对话、技能、知识库等业务功能 |
> **说明**:单租户模式下,"租户管理员"等价于"系统管理员"。保留"系统管理员"角色,未来扩展多租户时再区分"系统管理员"和"租户管理员"。
---
## 4. 功能性需求
### 4.1 租户系统(单租户优先)
#### FR-TENANT-001:默认租户自动初始化
- WHEN 首次启动系统,THEN 系统 SHALL 自动创建默认租户配置
- WHEN 初始化时,THEN 用户 SHALL 提供组织名称、选择领域类型(Government/Enterprise/Education/Custom
- WHEN 初始化完成后,THEN 系统 SHALL 自动应用对应的预设主题和领域配置
#### FR-TENANT-002:租户配置(系统配置)
- WHEN 系统管理员访问配置页,THEN 系统 SHALL 展示并允许修改:组织名称、品牌定制、功能开关、数据策略
- WHEN 功能开关变更时,THEN 系统 SHALL 实时生效,无需重启
- **说明**:单租户模式下,所有配置直接生效,无需租户上下文路由
#### FR-TENANT-003:数据迁移与备份
- WHEN 系统管理员触发数据备份,THEN 系统 SHALL 导出全部数据(SQLite schema + 文件存储)
- WHEN 导入备份时,THEN 系统 SHALL 验证数据完整性并重建
#### FR-TENANT-004:多租户扩展点(预留)
- WHEN 未来需要支持多租户 SaaSTHEN 系统 SHALL 可通过 tenant_id 字段实现数据隔离
- **说明**Phase 1 所有表保留 tenant_id 字段,但默认不激活隔离逻辑
### 4.2 主题引擎
#### FR-THEME-001:动态主题切换
- WHEN 系统管理员切换主题,THEN 系统 SHALL 在 100ms 内完成切换,前端无需刷新
- WHEN 用户访问工作台时,THEN 系统 SHALL 自动应用当前配置的主题
#### FR-THEME-002:预设主题包
- WHEN 选择预设主题,THEN 系统 SHALL 支持以下主题:
- **政务蓝主题**:GovAI 政务配色体系
- **企业专业主题**:商务蓝色调
- **学术清新主题**:简洁学术风格
- WHEN 主题包含 Logo 定制,THEN 系统 SHALL 支持上传 PNG/SVG 格式
#### FR-THEME-003:主题预览
- WHEN 系统管理员在主题编辑器中修改,THEN 系统 SHALL 提供实时预览
- WHEN 预览满意后,THEN 管理员 SHALL 一键发布生效
### 4.3 领域框架
#### FR-DOMAIN-001:领域配置 Schema
- WHEN 初始化系统时,THEN 系统 SHALL 根据选择的领域类型加载对应的领域配置 Schema
- WHEN 领域配置包含分类体系时,THEN 系统 SHALL 自动创建对应的知识库分类和应用模板入口
#### FR-DOMAIN-002:领域初始化向导
- WHEN 首次配置系统,THEN 系统 SHALL 提供领域初始化向导,引导配置:
- 部门/科室设置(如适用)
- 知识库分类初始化
- 预置技能包选择
#### FR-DOMAIN-003:领域特定技能包注册
- WHEN 安装领域技能包时,THEN 系统 SHALL 自动注册到该领域的技能目录
- WHEN 技能包与领域不兼容时,THEN 系统 SHALL 提示警告但不阻止安装
### 4.4 多用户认证与权限
#### FR-AUTH-001:用户注册与登录
- WHEN 新用户注册时,THEN 系统 SHALL 支持用户名+密码方式
- WHEN 用户登录时,THEN 系统 SHALL 使用现有的 JWT 认证机制
- WHEN 登录失败时,THEN 系统 SHALL 返回错误信息,不泄露账户是否存在
#### FR-AUTH-002:角色权限模型(RBAC
- WHEN 系统管理员创建用户时,THEN 系统 SHALL 支持分配角色(普通用户/系统管理员)
- WHEN 普通用户访问管理员功能时,THEN 系统 SHALL 返回 403 权限不足
- WHEN 角色权限变更时,THEN 系统 SHALL 实时生效
#### FR-AUTH-003:会话管理
- WHEN 用户登录后,THEN 系统 SHALL 生成带刷新令牌的 JWT
- WHEN JWT 过期时,THEN 系统 SHALL 支持静默刷新
- WHEN 用户登出时,THEN 系统 SHALL 使 refresh token 失效
### 4.5 审计日志
#### FR-AUDIT-001:操作审计记录
- WHEN 用户执行以下操作时,THEN 系统 SHALL 记录审计日志:
- 用户登录/登出
- 系统配置变更
- 用户创建/删除/权限变更
- 知识库上传/删除
- 敏感技能包安装/卸载
- WHEN 记录审计日志时,THEN 系统 SHALL 包含:操作时间、操作用户、操作类型、操作对象、操作结果、来源IP
#### FR-AUDIT-002:审计日志查询
- WHEN 系统管理员访问审计日志页,THEN 系统 SHALL 支持按时间范围、操作类型、用户筛选
- WHEN 导出审计日志时,THEN 系统 SHALL 支持 CSV 格式导出
#### FR-AUDIT-003:审计日志不可篡改
- WHEN 审计日志写入后,THEN 系统 SHALL 保证不可修改、不可删除
- WHEN 需要追加日志时,THEN 系统 SHALL 仅允许追加操作
### 4.6 配置导入导出
#### FR-CONFIG-001:配置导出
- WHEN 系统管理员触发配置导出,THEN 系统 SHALL 导出完整配置(不含敏感数据)
- WHEN 导出时,THEN 系统 SHALL 包含系统配置、用户列表、知识库索引、主题配置
#### FR-CONFIG-002:配置导入
- WHEN 管理员导入配置文件,THEN 系统 SHALL 验证配置合法性
- WHEN 配置导入时,THEN 系统 SHALL 支持合并或覆盖模式
---
## 5. 非功能性需求
### 5.1 性能
| 指标 | 目标值 |
|------|--------|
| 冷启动时间 | < 3s |
| 主题切换延迟 | < 100ms |
| 租户创建时间 | < 1min |
| 对话响应延迟 | < 500ms(不含模型推理) |
### 5.2 安全
| 需求 | 说明 |
|------|------|
| 数据隔离 | 单机部署物理隔离已满足,无需额外租户隔离 |
| 传输加密 | HTTPS/TLS 1.3WebSocket + REST |
| 存储加密 | SQLite 数据库加密(AES-256,可选) |
| 密钥管理 | JWT secret 独立存储,不硬编码 |
### 5.3 可用性
| 需求 | 说明 |
|------|------|
| 离线优先 | 网络中断时核心功能可用 |
| 错误恢复 | 异常操作自动回滚,不破坏数据完整性 |
| 日志诊断 | 完整的错误日志,便于问题定位 |
### 5.4 可扩展性
| 需求 | 说明 |
|------|------|
| 领域扩展 | 新增领域类型时,无需修改核心代码 |
| 插件集成 | 保持与现有 MCP 插件系统的兼容性 |
| 数据迁移 | 未来可平滑迁移到 PostgreSQL |
---
## 6. 关键约束与假设
### 6.1 约束
1. **复用 nomifun-tauri**:Phase 1 不重写现有模块,在现有架构上扩展单租户定制能力
2. **SQLite 本地优先**:继续使用 SQLite 作为主要数据存储,暂不引入 PostgreSQL
3. **前端改动最小化**:主题引擎优先通过 CSS 变量 + 动态加载实现,减少 Vue 组件改动
4. **不涉及硬件**:本文档不包含 PrivBox 硬件适配内容
5. **单租户优先**:不激活多租户隔离逻辑,简化开发和部署
### 6.2 假设
1. 假设 Phase 1 部署场景为单机私有化部署
2. 假设物理隔离已满足数据安全需求
3. 假设管理员可通过管理后台完成所有配置操作(无需命令行)
4. 假设审计日志存储在 SQLite 中
---
## 7. 数据模型(核心扩展)
### 7.1 新增数据模型
```rust
// 系统配置(单租户模式,单条记录)
struct SystemConfig {
id: String, // "default"
org_name: String, // 组织名称
domain: DomainType, // Government / Enterprise / Education / Custom
created_at: TimestampMs,
updated_at: TimestampMs,
}
// 品牌配置(单租户模式)
struct BrandingConfig {
config_id: String, // "default"
logo_url: Option<String>,
logo_svg: Option<String>, // SVG 内联
primary_color: String,
secondary_color: String,
accent_color: String,
theme_preset: ThemePreset,
}
// 功能开关(单租户模式)
struct FeatureFlags {
config_id: String,
app_store: bool,
skill_market: bool,
multi_user: bool,
cloud_sync: bool,
}
// 领域配置
struct DomainConfig {
id: i64, // 自增主键
domain_type: DomainType,
config_json: String, // 领域特定配置的 JSON 序列化
created_at: TimestampMs,
}
// 审计日志
struct AuditLog {
id: i64, // 自增主键
user_id: String,
action: String,
object_type: String,
object_id: Option<String>,
result: String, // success / failure
source_ip: Option<String>,
extra: String, // JSON 额外信息
created_at: TimestampMs,
}
// 扩展:用户表新增角色
struct User {
// ... 现有字段 ...
role: UserRole, // admin / user
}
```
### 7.2 现有模型扩展
| 现有表 | 扩展字段 | 说明 |
|--------|---------|------|
| `users` | `role` | 用户角色(保留 tenant_id 字段但不使用) |
| `conversations` | - | 保持不变(单租户无需隔离) |
| `messages` | - | 保持不变 |
| `knowledge_bases` | - | 保持不变 |
| `providers` | - | 保持不变 |
| `assistants` | - | 保持不变 |
### 7.3 多租户扩展点(预留)
> 以下字段保留以备未来扩展,Phase 1 不激活隔离逻辑
```rust
// 未来多租户场景下启用
struct FutureMultiTenant {
tenant_id: String, // 保留字段
}
// 所有表可选添加 tenant_id
ALTER TABLE users ADD COLUMN tenant_id TEXT; -- "default"
```
---
## 8. API 设计(核心接口)
### 8.1 系统配置
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/config` | GET | 获取系统配置(组织名、领域) |
| `/api/config` | PUT | 更新系统配置 |
| `/api/config/init` | POST | 初始化系统(首次启动) |
| `/api/config/backup` | POST | 触发数据备份 |
| `/api/config/restore` | POST | 导入备份 |
### 8.2 用户与认证
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/auth/register` | POST | 用户注册 |
| `/api/auth/login` | POST | 用户登录 |
| `/api/auth/logout` | POST | 用户登出 |
| `/api/auth/refresh` | POST | 刷新 Token |
| `/api/users` | GET | 列出所有用户(管理员) |
| `/api/users` | POST | 创建用户(管理员) |
| `/api/users/{id}` | PUT | 更新用户(管理员) |
| `/api/users/{id}` | DELETE | 删除用户(管理员) |
### 8.3 主题与品牌配置
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/branding` | GET | 获取当前品牌配置 |
| `/api/branding` | PUT | 更新品牌配置 |
| `/api/branding/presets` | GET | 获取预设主题列表 |
| `/api/branding/preview` | POST | 预览主题 |
### 8.4 审计日志
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/audit-logs` | GET | 查询审计日志 |
| `/api/audit-logs/export` | GET | 导出审计日志(CSV |
### 8.5 领域配置
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/domains` | GET | 获取当前领域配置 |
| `/api/domains/init` | POST | 领域初始化向导 |
| `/api/domains/skill-packs` | GET | 获取领域技能包列表 |
---
## 9. 前端改造要点
### 9.1 视图层新增
| 视图 | 说明 |
|------|------|
| 系统初始化向导 | 首次启动引导创建管理员、选择领域 |
| 用户管理 | 管理员管理系统内用户 |
| 品牌配置 | 可视化配置 Logo、主题配色 |
| 审计日志查看 | 筛选和导出审计日志 |
| 领域初始化向导 | 创建后引导配置部门/知识库/技能包 |
### 9.2 现有视图改造
| 视图 | 改造内容 |
|------|---------|
| 登录页 | 新增注册入口、首次启动引导 |
| 工作台 | 顶部栏根据品牌配置动态渲染 Logo/配色 |
| 设置页 | 新增系统配置、用户管理、品牌配置入口 |
### 9.3 CSS 变量主题方案
```css
/* 主题变量注入点(替换原有硬编码颜色) */
:root {
--color-primary: var(--theme-primary, #1a56db);
--color-secondary: var(--theme-secondary, #6b7280);
--color-bg: var(--theme-bg, #ffffff);
--color-surface: var(--theme-surface, #f9fafb);
--color-text: var(--theme-text, #111827);
--color-border: var(--theme-border, #e5e7eb);
--logo-url: var(--theme-logo, url('/assets/default-logo.png'));
}
/* 动态切换 */
.theme-gov {
--theme-primary: #1e3a8a; /* 政务蓝 */
--theme-bg: #f0f4f8;
--theme-surface: #ffffff;
--theme-text: #1a202c;
}
.theme-enterprise {
--theme-primary: #2563eb; /* 企业蓝 */
--theme-bg: #f8fafc;
--theme-surface: #ffffff;
--theme-text: #0f172a;
}
.theme-academic {
--theme-primary: #059669; /* 学术绿 */
--theme-bg: #fafaf9;
--theme-surface: #f5f5f4;
--theme-text: #1c1917;
}
```
---
## 10. 风险与依赖
### 10.1 技术风险
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 单租户限制 | 未来需要多租户时改动成本 | 保留 tenant_id 字段,隔离逻辑模块化 |
| SQLite 并发限制 | 多用户同时使用性能下降 | 监控慢查询,必要时迁移 PostgreSQL |
| 前端主题系统侵入性 | 改动范围过大 | 使用 CSS 变量 + 动态类名,避免大规模重构 |
### 10.2 依赖项
| 依赖 | 说明 |
|------|------|
| nomifun-tauri 主分支 | Phase 1 基于最新 stable 版本开发 |
| nomifun-auth | 复用现有 JWT 认证,扩展角色体系 |
| nomifun-db | 复用现有 sqlx 基础设施,扩展 schema |
### 10.3 决策点(已确认)
- [x] 租户隔离策略:**单租户优先**,不激活数据层隔离
- [x] 是否需要多租户 SaaS 支持:**Phase 1 不支持**,保留扩展点
- [x] 审计日志存储:**同库单表**,按用户筛选
- [x] 主题编辑器:**先做配置文件形式**,UI 可视化可选
---
> 文档结束 — 已调整为**单租户优先**架构,请检查确认。
+410
View File
@@ -0,0 +1,410 @@
# 通用AI工作台底座平台 — 产品需求文档
> 文档编号:PRD-COMMONSP-001
> 版本:v1.0.0
> 日期:2026-03-14
> 状态:待确认
> 项目缩写:COMMONSPCommons Platform
---
## 1. 产品概述与定位
### 1.1 产品定位
Commons Platform 是 nomifun-tauri 的演进版本,面向 **PrivBox 软硬一体化产品**的软件核心,定位为**私有化部署的 AI 工作台底座**
**核心价值主张**
- 开箱即用的私有化部署方案
- 零配置的多领域适配能力(政务/企业/教育)
- 品牌级定制(主题、Logo、品牌色)
- 企业级安全与审计
### 1.2 产品范围
| 包含 | 不包含 |
|------|--------|
| 单租户定制框架 | 硬件适配(PrivBox HAL |
| 多用户管理与 RBAC | 领域包开发(GovPack/EntPack/EduPack |
| 主题引擎 | 插件市场 |
| 领域框架(Schema + 向导) | 多租户 SaaS 模式 |
| 审计日志 | 云端同步 |
### 1.3 目标用户
| 用户类型 | 描述 | 使用场景 |
|----------|------|----------|
| **系统管理员** | IT 运维人员/项目实施工程师 | 部署配置、品牌定制、用户管理 |
| **普通用户** | 一线业务人员 | AI 对话、技能使用、知识库检索 |
---
## 2. 目标与成功指标
### 2.1 产品目标
| 目标 | 衡量指标 | 目标值 |
|------|----------|--------|
| 部署效率 | 从下载到首次对话的时间 | ≤ 10 分钟 |
| 配置效率 | 品牌定制(Logo + 主题)耗时 | ≤ 5 分钟 |
| 用户满意度 | 管理员 NPS | ≥ 40 |
| 系统稳定性 | 月度可用性 | ≥ 99.5% |
### 2.2 MVP 成功标准
- [ ] 系统可完成首次启动初始化向导
- [ ] 管理员可配置品牌主题(Logo + 配色)
- [ ] 支持 3 种预设主题一键切换
- [ ] 管理员可创建/管理普通用户
- [ ] 普通用户可正常使用 AI 对话功能
- [ ] 敏感操作自动记录审计日志
- [ ] 审计日志支持查询和 CSV 导出
---
## 3. 用户画像与核心场景
### 3.1 场景 1:政务内网部署
**用户**:某市大数据局 IT 负责人老王
**痛点**
- 现有 AI 产品无法在内网部署,数据必须出网
- 需要符合政务风格的界面(蓝色主题、官方 Logo)
- 需要记录谁在什么时候使用了什么功能(审计要求)
**解法**
- 完全离线部署,数据不出网
- 一键切换政务蓝主题,上传官方 Logo
- 审计日志记录所有敏感操作,支持导出
---
### 3.2 场景 2:企业知识库场景
**用户**:某科技公司行政主管小李
**痛点**
- 员工众多,需要分账号管理
- 不同的部门需要不同的 AI 助手配置
- 需要将公司规章制度导入知识库
**解法**
- 多用户体系 + 角色权限控制
- 知识库支持多分类管理
- 领域框架支持按部门初始化
---
### 3.3 场景 3:学校教学场景
**用户**:某高校信息中心张老师
**痛点**
- 需要符合学校视觉规范的界面
- 老师和学生需要不同的使用权限
- 希望开箱即用,不需要复杂配置
**解法**
- 预设学术清新主题
- RBAC 支持教师/学生角色
- 首次启动向导,5 步完成初始化
---
## 4. 功能清单与优先级(MoSCoW)
### 4.1 Must HaveM
| 功能 | 需求编号 | 说明 | 验收标准 |
|------|----------|------|----------|
| 系统初始化向导 | FR-TENANT-001 | 首次启动引导配置 | 5 步内完成组织配置 |
| 品牌配置 | FR-TENANT-002 | Logo + 主题配色 | 支持 PNG/SVG 上传 |
| 预设主题 | FR-THEME-002 | 政务蓝/企业蓝/学术绿 | 一键切换即时生效 |
| 用户管理 | FR-AUTH-002 | 创建/编辑/删除用户 | 支持 admin/user 角色 |
| 用户认证 | FR-AUTH-001 | 注册/登录/登出 | JWT + 刷新令牌 |
| 审计日志 | FR-AUDIT-001 | 记录敏感操作 | 登录/配置/用户变更 |
| 审计查询 | FR-AUDIT-002 | 日志筛选和导出 | 支持时间/类型/用户筛选 |
### 4.2 Should HaveS
| 功能 | 需求编号 | 说明 | 验收标准 |
|------|----------|------|----------|
| 主题预览 | FR-THEME-003 | 修改前实时预览 | 预览与发布后一致 |
| 领域初始化向导 | FR-DOMAIN-002 | 引导配置领域 | 部门/知识库/技能包 |
| 领域配置 Schema | FR-DOMAIN-001 | 预置领域参数 | Government/Enterprise/Education |
| 会话管理 | FR-AUTH-003 | JWT 刷新/登出 | 静默刷新正常 |
| 数据备份 | FR-TENANT-003 | 导出/导入数据 | 完整性校验通过 |
### 4.3 Could HaveC
| 功能 | 需求编号 | 说明 | 验收标准 |
|------|----------|------|----------|
| 领域技能包注册 | FR-DOMAIN-003 | 按领域加载技能 | 技能目录正确分类 |
| 功能开关 | FR-TENANT-002 | 按需启用功能 | 开关实时生效 |
| 配置导入导出 | FR-CONFIG-001/002 | 迁移配置 | 支持合并/覆盖模式 |
### 4.4 Won't HaveW
| 功能 | 原因 |
|------|------|
| 多租户 SaaS | Phase 1 单租户优先,物理隔离已够用 |
| 插件市场 | Phase 2 领域包阶段处理 |
| 云端同步 | 私有化部署场景不需要 |
| SQLite 加密 | Phase 1 可选,留作后续增强 |
---
## 5. 关键流程
### 5.1 首次启动流程
```
┌─────────────────────────────────────────────────────────┐
│ 首次启动 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 检测未初始化 │
│ │ │
│ ▼ │
│ 2. 欢迎页 → 创建系统管理员 │
│ │ │
│ ▼ │
│ 3. 组织配置 → 输入组织名称 │
│ │ │
│ ▼ │
│ 4. 领域选择 → Government/Enterprise/Education/Custom │
│ │ │
│ ▼ │
│ 5. 主题选择 → 选择预设主题或跳过 │
│ │ │
│ ▼ │
│ 6. 初始化完成 → 跳转登录页 │
│ │
└─────────────────────────────────────────────────────────┘
```
### 5.2 品牌定制流程
```
┌─────────────────────────────────────────────────────────┐
│ 品牌定制 │
├─────────────────────────────────────────────────────────┤
│ │
│ 管理员进入「设置 → 品牌配置」 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 预设主题选择 │ ← 一键应用政务蓝/企业蓝/学术绿 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 自定义颜色 │ ← 调色板选择主色/辅色/强调色 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Logo 上传 │ ← PNG/SVG,支持拖拽 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 实时预览 │ ← 所见即所得 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ 「发布」→ 立即生效 │
│ │
└─────────────────────────────────────────────────────────┘
```
### 5.3 审计日志流程
```
┌─────────────────────────────────────────────────────────┐
│ 审计日志 │
├─────────────────────────────────────────────────────────┤
│ │
│ 敏感操作触发(如用户登录/配置变更/用户管理) │
│ │ │
│ ▼ │
│ 写入审计日志(操作时间/用户/类型/对象/结果/IP) │
│ │ │
│ ▼ │
│ 管理员进入「设置 → 审计日志」 │
│ │ │
│ ▼ │
│ 筛选条件 → 时间范围/操作类型/操作用户 │
│ │ │
│ ▼ │
│ 展示日志列表,支持分页 │
│ │ │
│ ▼ │
│ 导出 CSV(如需要) │
│ │
└─────────────────────────────────────────────────────────┘
```
---
## 6. 角色权限矩阵
### 6.1 功能权限矩阵
| 功能 | 系统管理员 | 普通用户 |
|------|:----------:|:--------:|
| **系统初始化** | ✅ 创建首个管理员 | ❌ |
| **系统配置** | | |
| - 查看配置 | ✅ | ❌ |
| - 修改配置 | ✅ | ❌ |
| **品牌配置** | | |
| - 查看品牌 | ✅ | ❌ |
| - 修改品牌 | ✅ | ❌ |
| **用户管理** | | |
| - 查看用户列表 | ✅ | ❌ |
| - 创建/编辑用户 | ✅ | ❌ |
| - 删除用户 | ✅ | ❌ |
| **AI 对话** | ✅ | ✅ |
| **知识库管理** | ✅ | ✅(受限) |
| **审计日志** | | |
| - 查看日志 | ✅ | ❌ |
| - 导出日志 | ✅ | ❌ |
| **数据备份** | ✅ | ❌ |
### 6.2 权限说明
| 角色 | 描述 | 默认账号 |
|------|------|----------|
| **系统管理员 (admin)** | 首次启动时创建,拥有所有配置权限 | 初始化向导创建 |
| **普通用户 (user)** | 仅能使用业务功能,无配置权限 | 管理员创建 |
---
## 7. 版本规划
### 7.1 Phase 1 MVP
**目标**:夯实基础框架,支持单租户私有化部署
| 模块 | 功能 | 优先级 |
|------|------|--------|
| 初始化向导 | 首次启动引导 | P0 |
| 用户体系 | 注册/登录/多用户/RBAC | P0 |
| 品牌配置 | 预设主题/自定义配色/Logo | P0 |
| 审计日志 | 记录/查询/导出 | P0 |
| 领域框架 | 领域 Schema/初始化向导 | P1 |
**交付物**
- 可运行的 MVP 版本
- 3 套预设主题
- 基础审计日志功能
### 7.2 Phase 2 领域包
**目标**:完善三大领域包
| 领域包 | 内容 |
|--------|------|
| GovPack | 政务蓝主题、政务知识库分类、公文处理技能 |
| EntPack | 企业主题、企业知识库分类、CRM 集成技能 |
| EduPack | 学术主题、课程知识库分类、教学辅助技能 |
### 7.3 Phase 3 开放生态
**目标**:开放插件市场和 API
- 插件市场
- 开放 API
- 多租户 SaaS 模式
---
## 8. 非功能性要求
### 8.1 性能要求
| 指标 | 目标 | 说明 |
|------|------|------|
| 冷启动时间 | ≤ 3s | 从点击图标到可交互 |
| 主题切换延迟 | ≤ 100ms | 前端 CSS 变量切换 |
| 初始化向导完成时间 | ≤ 2min | 5 步配置完成 |
| 对话响应延迟 | ≤ 500ms | 不含模型推理时间 |
### 8.2 安全要求
| 要求 | 说明 |
|------|------|
| 认证机制 | JWT + 刷新令牌,secret 不硬编码 |
| 权限控制 | RBAC403 正确返回 |
| 审计日志 | 不可篡改,仅追加 |
| 数据隔离 | 单机物理隔离 |
### 8.3 可用性要求
| 要求 | 说明 |
|------|------|
| 离线可用 | 核心功能断网可用 |
| 错误恢复 | 异常自动回滚,不破坏数据 |
| 错误提示 | 友好提示,不暴露敏感信息 |
### 8.4 兼容性要求
| 要求 | 说明 |
|------|------|
| 浏览器 | Chrome/Firefox/Safari/Edge 最新两个版本 |
| 操作系统 | Windows/macOS/Linux |
| 插件系统 | 兼容现有 MCP 插件 |
---
## 9. 依赖与风险
### 9.1 技术依赖
| 依赖 | 版本 | 说明 |
|------|------|------|
| nomifun-tauri | latest stable | 基础框架 |
| Rust | 1.70+ | 后端运行时 |
| Vue | 3.x | 前端框架 |
| sqlx | 0.7+ | 数据库访问 |
| SQLite | 3.x | 数据存储 |
### 9.2 项目风险
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|----------|
| 前端主题改动侵入性 | 中 | 高 | CSS 变量方案,专人负责 |
| 审计日志性能影响 | 中 | 中 | 异步写入,定期归档 |
| SQLite 并发瓶颈 | 低 | 中 | 监控慢查询,预留迁移方案 |
### 9.3 外部依赖
| 依赖 | 说明 | 风险 |
|------|------|------|
| nomifun-tauri 主分支 | 基于最新版本开发 | 低 |
| 前端组件库 | 假设使用现有组件库 | 低 |
---
## 10. 附录
### 10.1 术语对照
| 英文 | 中文 | 说明 |
|------|------|------|
| Branding | 品牌配置 | Logo、配色等视觉元素 |
| Domain | 领域 | Government/Enterprise/Education |
| RBAC | 基于角色的访问控制 | Role-Based Access Control |
| Audit Log | 审计日志 | 操作记录 |
### 10.2 参考文档
| 文档 | 路径 |
|------|------|
| 需求文档 | `0-req-COMMONSP.md` |
| 演进规划 | `commons-ai-platform-evolution.md` |
---
> 文档结束 — 请检查以上 PRD,如有歧义或需调整,请告知后再推进下一阶段。
+532
View File
@@ -0,0 +1,532 @@
# Commons Platform Phase 1 MVP — 开发任务文档
> 文档编号:TASK-COMMONSP-001
> 版本:v1.1.0
> 日期:2026-03-14
> 状态:MVP 阶段大部分完成
> 项目缩写:COMMONSPCommons Platform
> 最后更新:2026-03-14
---
## 0. 进度摘要
### 0.1 MVP 阶段完成情况
| 模块 | 状态 | 完成度 |
|------|------|--------|
| 数据库迁移 | ✅ 完成 | 100% |
| 用户体系 | ✅ 完成 | 100% |
| 初始化向导 | ✅ 完成 | 100% |
| 品牌配置 | ✅ 完成 | 100% |
| 审计日志 | ✅ 完成 | 100% |
| 领域配置 | ✅ 完成 | 100% |
| 登录页改造 | ✅ 完成 | 100% |
### 0.2 待完成任务
| 任务 | 优先级 | 说明 |
|------|--------|------|
| 数据备份接口 | P1 | S1 阶段,涉及完整数据导出/导入 |
| 前端主题动态加载 | P1 | 需要从后端获取品牌配置并设置 CSS 变量 |
| 管理后台布局优化 | P2 | 侧边栏导航、权限控制等 |
---
## 1. 任务总览
### 1.1 阶段划分
| 阶段 | 范围 | 优先级 |
|------|------|--------|
| **MVP** | 核心功能:初始化向导 + 用户体系 + 品牌配置 + 审计日志 | P0 |
| **S1** | 重要功能:主题预览 + 领域向导 + 数据备份 | P1 |
| **S2** | 增强功能:领域技能包 + 功能开关 + 配置迁移 | P2 |
### 1.2 模块依赖关系
```
┌─────────────────────────────────────────────────────────┐
│ 模块依赖图 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ 数据库迁移 │ ← 所有模块依赖 │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 用户体系 │ ──▶ │ 审计日志 │ │
│ └──────┬──────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 初始化向导 │ ──▶ │ 品牌配置 │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
---
## 2. MVP 阶段任务(P0
### 2.1 数据库迁移
**目标**:扩展 nomifun-tauri 数据库 schema,支持用户角色、品牌配置、审计日志
**依赖**:无
#### 任务 2.1.1:创建数据库迁移文件
- [x] 创建 `002_user_role.sql` 迁移文件
- 目标:为 `users` 表添加 `role` 字段(`admin`/`user`
- 验收标准:迁移可正常执行,不破坏现有数据
- 需求映射:FR-AUTH-002
- [x] 创建 `003_system_config.sql` 迁移文件
- 目标:创建 `system_config` 表(组织名、领域类型)
- 验收标准:迁移可正常执行
- 需求映射:FR-TENANT-001
- [x] 创建 `004_branding_config.sql` 迁移文件
- 目标:创建 `branding_config` 表(Logo、主题配色)
- 验收标准:迁移可正常执行
- 需求映射:FR-THEME-001, FR-THEME-002
- [x] 创建 `005_audit_log.sql` 迁移文件
- 目标:创建 `audit_log`
- 验收标准:迁移可正常执行,支持大量写入
- 需求映射:FR-AUDIT-001
- [x] 创建 `006_domain_config.sql` 迁移文件
- 目标:创建 `domain_config`
- 验收标准:迁移可正常执行
- 需求映射:FR-DOMAIN-001
#### 任务 2.1.2:定义 Rust 数据模型
- [x]`nomifun-db/src/models/` 下新增模型文件
- 目标:定义 `UserRole``SystemConfig``BrandingConfig``AuditLog``DomainConfig` 模型
- 验收标准:模型字段与数据库表一致,实现 `FromRow` trait
- 依赖:2.1.1
---
### 2.2 用户体系
**目标**:实现多用户注册、登录、RBAC 权限控制
**依赖**2.1
#### 任务 2.2.1:扩展用户注册接口
- [x] 修改 `POST /api/auth/register` 接口
- 目标:首次启动时创建 admin 用户,后续注册为普通用户
- 验收标准:
- 未初始化时,第一个注册用户自动成为 admin
- 已初始化后,注册用户默认为 user 角色
- 需求映射:FR-AUTH-001, FR-AUTH-002
- **状态**:✅ 已完成(通过 `/api/auth/setup` 实现)
#### 任务 2.2.2:扩展登录接口
- [x] 修改 `POST /login` 接口
- 目标:返回用户角色信息到前端
- 验收标准:JWT payload 包含 `role` 字段
- 需求映射:FR-AUTH-001
- **状态**:✅ 已完成
#### 任务 2.2.3:添加用户管理接口
- [x] 添加 `GET /api/users` 接口
- 目标:列出租户内所有用户(仅 admin 可访问)
- 验收标准:返回用户列表,不包含密码
- 需求映射:FR-AUTH-002
- **状态**:✅ 已完成
- [x] 添加 `POST /api/users` 接口
- 目标:创建新用户(仅 admin 可访问)
- 验收标准:可指定角色,密码加密存储
- 需求映射:FR-AUTH-002
- **状态**:✅ 已完成
- [x] 添加 `PUT /api/users/{id}` 接口
- 目标:更新用户信息(仅 admin 可访问)
- 验收标准:可修改角色,不可修改自己
- 需求映射:FR-AUTH-002
- **状态**:✅ 已完成
- [x] 添加 `DELETE /api/users/{id}` 接口
- 目标:删除用户(仅 admin 可访问)
- 验收标准:不可删除自己,最后一个 admin 不能删除
- 需求映射:FR-AUTH-002
- **状态**:✅ 已完成
#### 任务 2.2.4:添加权限中间件
- [x] 实现 `require_admin` 中间件
- 目标:检查 JWT 中的 role 字段
- 验收标准:非 admin 用户返回 403
- 需求映射:FR-AUTH-002
- **状态**:✅ 已完成
---
### 2.3 初始化向导
**目标**:首次启动引导用户完成系统配置
**依赖**2.1, 2.2
#### 任务 2.3.1:添加初始化状态检查
- [x] 添加 `GET /api/system/initialized` 接口
- 目标:检查系统是否已完成初始化
- 验收标准:返回 `{ initialized: bool }`
- **状态**:✅ 已完成(依赖 SystemConfigRepository
#### 任务 2.3.2:添加初始化接口
- [x] 添加 `POST /api/system/init` 接口
- 目标:创建系统管理员、设置组织信息、选择领域、预设主题
- 验收标准:
- 仅在未初始化时可用
- 创建 admin 用户
- 保存组织名和领域配置
- 应用预设主题
- 需求映射:FR-TENANT-001
- **状态**:✅ 已完成
#### 任务 2.3.3:前端初始化向导页面
- [x] 创建 `src/pages/setupWizard/SetupWizard.tsx` 组件
- 目标:5 步引导用户完成初始化
- 步骤:
1. 欢迎页(检测未初始化状态)
2. 创建管理员账号
3. 配置组织信息
4. 选择领域类型
5. 选择预设主题
- 验收标准:
- 每步验证输入
- 完成后跳转到登录页
- 需求映射:FR-TENANT-001
- **状态**:✅ 已完成(需要 `/api/auth/setup` 后端支持)
- **文件**
- `ui/src/renderer/pages/setupWizard/SetupWizard.tsx`
- `ui/src/renderer/pages/setupWizard/SetupWizard.css`
- `ui/src/renderer/services/i18n/locales/zh-CN/setupWizard.json`
- `ui/src/renderer/services/i18n/locales/en-US/setupWizard.json`
- **路由**:添加 `/setup` 路由,`needsSetup` 时自动重定向
---
### 2.4 品牌配置
**目标**:支持预设主题和自定义品牌配置
**依赖**2.1
#### 任务 2.4.1:添加品牌配置接口
- [x] 添加 `GET /api/system/branding` 接口
- 目标:获取当前品牌配置
- 验收标准:返回 Logo URL、主题配色
- 需求映射:FR-THEME-001
- **状态**:✅ 已完成
- [x] 添加 `PATCH /api/system/branding` 接口
- 目标:更新品牌配置(仅 admin 可访问)
- 验收标准:保存配色和 Logo 配置
- 需求映射:FR-THEME-001, FR-THEME-002
- **状态**:✅ 已完成
- [x] 添加 `POST /api/system/branding/logo` 接口
- 目标:上传 Logo 文件
- 验收标准:支持 PNG/SVG,保存到 static 目录
- 需求映射:FR-THEME-002
- **状态**:✅ 已完成(支持 PNG/SVG/JPG,保存到 data_dir/logos/
- [x] 添加 `GET /api/system/branding/presets` 接口
- 目标:获取预设主题列表
- 验收标准:返回政务蓝/企业蓝/学术绿主题配置
- 需求映射:FR-THEME-002
- **状态**:✅ 已完成
- [x] 添加 `POST /api/system/branding/presets/{preset_id}` 接口
- 目标:应用预设主题
- 验收标准:一键切换主题
- 需求映射:FR-THEME-002
- **状态**:✅ 已完成
#### 任务 2.4.2:前端品牌配置页面
- [x] 创建 `src/views/admin/BrandingConfig.vue` 组件
- 目标:可视化配置品牌
- 功能:
- 预设主题选择(点击一键应用)
- 颜色选择器(主色/辅色/强调色)
- Logo 上传(支持拖拽)
- 实时预览
- 验收标准:
- 预览与发布后一致
- 切换主题 < 100ms
- 需求映射:FR-THEME-001, FR-THEME-002, FR-THEME-003
- **状态**:✅ 已完成(`ui/src/renderer/pages/settings/BrandingConfig.tsx`
#### 任务 2.4.3:前端主题动态加载
- [ ] 修改 `src/App.vue` 或主题入口
- 目标:从后端获取品牌配置,动态设置 CSS 变量
- 验收标准:
- 页面加载时获取品牌配置
- 设置 `--theme-*` CSS 变量
- 主题切换无需刷新
- 需求映射:FR-THEME-001
---
### 2.5 审计日志
**目标**:记录敏感操作,支持查询和导出
**依赖**2.1
#### 任务 2.5.1:添加审计日志服务
- [x] 创建审计日志数据库层
- 目标:封装审计日志写入和查询逻辑
- 功能:
- 异步写入日志
- 自动补充时间戳
- 支持分页查询和筛选
- 验收标准:写入不影响主业务响应时间
- 需求映射:FR-AUDIT-001
- **状态**:✅ 已完成(`nomifun-db::IAuditLogRepository`
#### 任务 2.5.2:审计日志切面
- [x] 在关键接口添加审计日志调用
- 目标:登录/登出、配置变更、用户管理操作自动记录
- 验收标准:操作被正确记录到 audit_log 表
- 需求映射:FR-AUDIT-001
- **状态**:✅ 已完成(已在 system_init、branding_config、domain_config 等操作中集成)
#### 任务 2.5.3:添加审计日志查询接口
- [x] 添加 `GET /api/system/audit-logs` 接口
- 目标:查询审计日志(仅 admin 可访问)
- 参数:`page``page_size``start_date``end_date``action``user_id``category``status`
- 验收标准:支持分页和筛选
- 需求映射:FR-AUDIT-002
- **状态**:✅ 已完成
- [x] 添加 `POST /api/system/audit-logs` 接口
- 目标:创建新的审计日志条目
- 验收标准:写入成功并返回日志条目
- 需求映射:FR-AUDIT-001
- **状态**:✅ 已完成
- [x] 添加 `GET /api/system/audit-logs/{id}` 接口
- 目标:获取单条审计日志
- 验收标准:返回指定ID的日志条目
- 需求映射:FR-AUDIT-002
- **状态**:✅ 已完成
- [x] 添加 `GET /api/system/audit-logs/export` 接口
- 目标:导出审计日志为 CSV
- 验收标准:导出文件可被 Excel 正确打开
- 需求映射:FR-AUDIT-002
- **状态**:✅ 已完成(支持导出最多 10,000 条记录)
#### 任务 2.5.4:前端审计日志页面
- [ ] 创建 `src/views/admin/AuditLogs.vue` 组件
- 目标:展示和查询审计日志
- 功能:
- 筛选条件(时间范围、操作类型、用户)
- 日志列表展示
- CSV 导出按钮
- 验收标准:日志按时间倒序展示
- 需求映射:FR-AUDIT-002
---
## 3. S1 阶段任务(P1
### 3.1 领域向导
**目标**:引导用户配置领域特定的参数
**依赖**2.1, 2.3
#### 任务 3.1.1:添加领域配置接口
- [x] 添加 `GET /api/domains` 接口
- 目标:获取当前领域配置
- 验收标准:返回领域类型和配置参数
- **状态**:✅ 已完成(`GET /api/system/domain-config`
- [ ] 添加 `POST /api/domains/init` 接口
- 目标:领域初始化向导
- 功能:根据领域类型初始化部门/知识库分类/技能包
- 验收标准:按领域类型加载不同的初始化模板
- **备注**:已通过 `POST /api/system/init` + 预设应用实现
- [ ] 添加 `GET /api/domains/skill-packs` 接口
- 目标:获取领域技能包列表
- 验收标准:返回该领域可用的技能包
- **备注**:属于 MCP 系统功能,暂不在此实现
#### 任务 3.1.2:前端领域配置页面
- [x] 创建 `src/views/admin/DomainConfig.vue` 组件
- 目标:领域初始化向导
- 功能:部门设置、知识库分类、预置技能包选择
- **状态**:✅ 已完成(`ui/src/renderer/pages/admin/DomainConfigPage.tsx`
---
### 3.2 数据备份
**目标**:支持数据导出和导入
**依赖**2.1
#### 任务 3.2.1:添加备份接口
- [ ] 添加 `POST /api/config/backup` 接口
- 目标:导出完整数据备份
- 验收标准:生成包含 schema 和数据的 zip 文件
- [ ] 添加 `POST /api/config/restore` 接口
- 目标:导入备份文件
- 验收标准:验证完整性后重建数据
#### 任务 3.2.2:前端备份页面
- [ ] 在设置页添加备份功能
- 目标:一键备份和导入
- 功能:下载备份文件、上传恢复
---
## 4. S2 阶段任务(P2
### 4.1 功能开关
**目标**:按需启用/禁用某些功能
**依赖**2.1
#### 任务 4.1.1:添加功能开关表和接口
- [ ] 创建 `feature_flags`
- [ ] 添加 `GET/PUT /api/features` 接口
#### 任务 4.1.2:前端功能开关控件
- [ ] 在设置页添加功能开关
---
### 4.2 配置导入导出
**目标**:迁移系统配置
**依赖**2.1
#### 任务 4.2.1:添加配置导出接口
- [ ] 添加 `GET /api/config/export` 接口
- 目标:导出配置(不含用户数据)
- 验收标准:导出 JSON 可被导入
#### 任务 4.2.2:添加配置导入接口
- [ ] 添加 `POST /api/config/import` 接口
- 目标:导入配置,支持合并或覆盖模式
---
## 5. 前端通用任务
### 5.1 管理后台布局
- [ ] 创建 `src/views/admin/` 目录结构
- [ ] 添加管理员侧边栏导航
- [ ] 添加管理员权限控制(未登录/非 admin 跳转)
### 5.2 登录页改造
- [x] 添加注册入口
- **状态**:✅ 已完成(通过 `/setup` 向导和管理员创建用户实现)
- [x] 首次启动时重定向到初始化向导
- **状态**:✅ 已完成(Router 中已实现 needsSetup 重定向)
### 5.3 设置页改造
- [ ] 添加系统配置入口(admin
- [ ] 添加品牌配置入口(admin
- [ ] 添加用户管理入口(admin
- [ ] 添加审计日志入口(admin
- [ ] 添加数据备份入口(admin
---
## 6. 验收与测试
### 6.1 构建验证
- [ ] `cargo build --release` 成功
- [ ] `npm run build` 成功
- [ ] 数据库迁移无报错
### 6.2 功能验收
| 功能 | 验收标准 |
|------|----------|
| 首次启动 | 访问根路径自动跳转到初始化向导 |
| 初始化向导 | 5 步内完成配置,admin 用户创建成功 |
| 登录/登出 | JWT 正确生成,刷新令牌正常 |
| 用户管理 | admin 可创建/编辑/删除用户,普通用户无法访问 |
| 品牌配置 | 预设主题一键切换,主题 < 100ms 生效 |
| Logo 上传 | PNG/SVG 上传成功,页面正确显示 |
| 审计日志 | 登录/配置变更/用户管理操作自动记录 |
| 审计查询 | 支持筛选和 CSV 导出 |
### 6.3 回归测试
- [ ] 现有 AI 对话功能正常
- [ ] 现有知识库功能正常
- [ ] 现有插件系统正常
---
## 7. 进度记录
### 7.1 完成项
| 日期 | 任务 | 状态 | 备注 |
|------|------|------|------|
| 2026-03-14 | 需求文档确认 | ✅ | v2.0.0 单租户优先 |
| 2026-03-14 | PRD 确认 | ✅ | v1.0.0 |
| 2026-03-14 | 2.1 数据库迁移 | ✅ | 6个迁移文件 + 6个模型定义 |
| 2026-03-14 | 2.3.1 初始化状态检查 API | ✅ | `GET /api/system/initialized` |
| 2026-03-14 | 2.3.2 初始化接口 | ✅ | `POST /api/system/init` |
| 2026-03-14 | 2.3.3 前端初始化向导 | ✅ | 5步向导组件 + i18n + 路由 |
| 2026-03-14 | 2.4.1 品牌配置接口(部分) | ✅ | GET/PATCH branding + presets API |
| 2026-03-14 | 2.5.1 审计日志数据库层 | ✅ | `IAuditLogRepository` + CRUD |
| 2026-03-14 | 2.5.3 审计日志查询接口 | ✅ | GET/POST/GET(id) audit-logs API |
| 2026-03-14 | 2.2 用户体系 | ✅ | CRUD 用户管理 + require_admin 中间件 |
### 7.2 变更记录
| 日期 | 变更内容 | 原因 |
|------|----------|------|
| 2026-03-14 | 多租户改为单租户优先 | 私有化部署场景物理隔离已够用 |
---
> 文档结束 — 请检查确认后开始阶段 5(按任务文档执行开发)。
@@ -0,0 +1,232 @@
# Phase 用户体系实现 - 完成报告
## 📋 任务概述
按照任务文档 `2-task-COMMONSP.md`**2.2 用户体系** 部分,完成了多用户注册、登录、RBAC 权限控制的完整实现。
## ✅ 已完成功能
### 1. 权限中间件(nomifun-auth
**文件**: `crates/backend/nomifun-auth/src/middleware.rs`
- ✅ 新增 `require_admin_middleware` - 检查用户是否为 admin 角色
- ✅ 返回 HTTP 403 给非 admin 用户
- ✅ 必须在 `auth_middleware` 之后应用
- ✅ 导出到公共 API
```rust
pub async fn require_admin_middleware(
request: Request,
next: Next,
) -> Result<Response, AppError>
```
### 2. 用户管理 APInomifun-auth
**文件**: `crates/backend/nomifun-auth/src/routes.rs`
#### 2.1 列出所有用户
- **端点**: `GET /api/users`
- **权限**: Admin only
- **功能**: 返回所有用户列表(不含密码)
- **响应**: `Vec<UserListItem>`
#### 2.2 创建用户
- **端点**: `POST /api/users`
- **权限**: Admin only
- **功能**: 创建新用户,支持指定角色(admin/user)
- **验证**: 用户名、密码强度、角色有效性
- **安全**: 密码使用 bcrypt 加密
#### 2.3 获取用户详情
- **端点**: `GET /api/users/:id`
- **权限**: Admin only
- **功能**: 返回指定用户的详细信息
#### 2.4 更新用户
- **端点**: `PUT /api/users/:id`
- **权限**: Admin only
- **功能**: 更新用户名、邮箱、角色
- **保护**:
- ❌ 不允许修改自己的账号
- ❌ 不允许降级最后一个 admin
#### 2.5 删除用户
- **端点**: `DELETE /api/users/:id`
- **权限**: Admin only
- **功能**: 删除指定用户
- **保护**:
- ❌ 不允许删除自己
- ❌ 不允许删除最后一个 admin
### 3. 数据库层扩展(nomifun-db
**文件**: `crates/backend/nomifun-db/src/repository/`
#### 3.1 新增 Repository 方法
**user.rs (trait)**:
```rust
async fn create_user_with_role(&self, username: &str, password_hash: &str, role: &str) -> Result<User, DbError>;
async fn update_role(&self, user_id: &str, role: &str) -> Result<(), DbError>;
async fn delete_user(&self, user_id: &str) -> Result<(), DbError>;
```
**sqlite_user.rs (实现)**:
-`create_user_with_role()` - 创建用户时指定角色
-`update_role()` - 更新用户角色
-`delete_user()` - 删除用户
### 4. API 类型定义(nomifun-api-types
**文件**: `crates/backend/nomifun-api-types/src/auth.rs`
已有类型(本次使用):
- `UserListItem` - 用户列表项
- `CreateUserRequest` - 创建用户请求
- `UpdateUserRequest` - 更新用户请求
## 🔒 安全保护
### 权限控制
- ✅ 所有用户管理接口都需要 admin 角色
- ✅ 使用 `require_admin_middleware` 统一保护
- ✅ 非 admin 用户返回 HTTP 403
### 数据保护
- ✅ 密码使用 bcrypt 加密存储
- ✅ API 响应不包含密码字段
- ✅ 用户名和密码验证(长度、复杂度)
### 业务规则
- ✅ 管理员不能修改/删除自己
- ✅ 系统至少保留一个 admin
- ✅ 不能将最后一个 admin 降级为 user
## 🧪 测试
### 编译测试
```bash
cargo build --workspace
```
**结果**: ✅ 编译成功,无错误
### 警告修复
- ✅ 消除了 `nomifun-db` 的未使用导入警告
- ✅ 消除了 `nomifun-auth` 的未使用变量警告
- ✅ 为预定义常量添加 `#[allow(dead_code)]`
### 功能测试
已提供测试脚本: `test_user_management.sh`
**测试流程**:
1. 检查系统状态
2. 创建/登录 admin 用户
3. 列出所有用户
4. 创建新用户
5. 获取用户详情
6. 更新用户角色
7. 删除用户
8. 验证最终状态
**运行测试**:
```bash
chmod +x test_user_management.sh
./test_user_management.sh
```
## 📊 完成度统计
### 任务 2.2.1 - 扩展用户注册接口
- [x] 首次启动创建 admin(通过 `/api/auth/setup`
- [x] JWT 包含角色信息
### 任务 2.2.2 - 扩展登录接口
- [x] 登录返回用户角色
- [x] JWT payload 包含 role 字段
### 任务 2.2.3 - 添加用户管理接口
- [x] GET /api/users - 列出所有用户
- [x] POST /api/users - 创建新用户
- [x] GET /api/users/:id - 获取用户详情
- [x] PUT /api/users/:id - 更新用户
- [x] DELETE /api/users/:id - 删除用户
### 任务 2.2.4 - 添加权限中间件
- [x] require_admin_middleware 实现
- [x] 非 admin 返回 403
**总体完成度**: 100% ✅
## 📝 文档更新
- ✅ 更新 `docs/0703/2-task-COMMONSP.md`
- ✅ 标记所有 2.2 子任务为完成
- ✅ 更新完成项记录表
## 🎯 下一步建议
根据任务文档顺序,下一步可以:
1. **继续后端功能**
- 2.4.2 前端品牌配置页面
- 2.5.2 审计日志切面集成
- 2.5.4 前端审计日志页面
2. **前端管理后台**
- 管理员侧边栏导航
- 用户管理页面
- 权限控制(路由守卫)
3. **完善功能**
- Logo 上传功能
- CSV 导出功能
- 前端主题动态加载
## 📦 变更文件清单
### 新增/修改文件
```
crates/backend/nomifun-auth/src/middleware.rs # 新增 require_admin_middleware
crates/backend/nomifun-auth/src/lib.rs # 导出 require_admin_middleware
crates/backend/nomifun-auth/src/routes.rs # 新增 5 个用户管理端点
crates/backend/nomifun-db/src/repository/user.rs # 新增 3 个 trait 方法
crates/backend/nomifun-db/src/repository/sqlite_user.rs # 实现 3 个方法
crates/backend/nomifun-db/src/repository/system_config.rs # 清理未使用导入
crates/backend/nomifun-db/src/models/audit_log.rs # 添加 allow(dead_code)
docs/0703/2-task-COMMONSP.md # 更新任务状态
test_user_management.sh # 新增测试脚本
```
## ✨ 技术亮点
1. **类型安全**: 所有接口使用强类型请求/响应
2. **错误处理**: 统一的 `AppError` 错误处理
3. **异步处理**: 密码加密在独立线程执行
4. **日志记录**: 关键操作记录审计日志
5. **代码质量**: 零编译错误,最小化警告
## 🔄 Git 提交建议
```bash
git add .
git commit -m "feat: implement user management system (Task 2.2)
- Add require_admin_middleware for RBAC
- Implement 5 user management endpoints (CRUD)
- Add create_user_with_role, update_role, delete_user to repository
- Protect against self-modification and last admin removal
- Clean up compiler warnings
- Add user management test script
Closes: Task 2.2 (User System)"
```
---
**报告生成时间**: 2026-03-14
**任务状态**: ✅ 完成
**构建状态**: ✅ 通过
**测试状态**: ⏳ 待运行(需启动应用)
@@ -0,0 +1,672 @@
# 通用AI工作台底座平台 — 架构演进方案
> 文档编号:ARCH-EVOL-001
> 版本:v1.0.0
> 日期:2026-07-03
> 状态:初稿
---
## 1. 项目愿景与定位
### 1.1 核心目标
构建一个**领域无关、部署灵活、能力开放**的通用AI工作台底座平台,支持企业、政府、教育三大领域的AI应用快速落地。
### 1.2 关键特性
| 特性 | 描述 |
|------|------|
| **底座优先** | 提供通用原子能力,业务逻辑通过配置和扩展实现 |
| **多租户本地部署** | 每个租户数据隔离,支持私有化部署 |
| **领域适配层** | 通过主题、技能包、知识库模板实现领域定制 |
| **开放能力总线** | MCP + REST,任意智能体可接入 |
| **渐进式演进** | 从最小可用到完整平台,平滑升级路径 |
### 1.3 目标用户画像
| 领域 | 典型用户 | 核心痛点 |
|------|---------|---------|
| **政府** | 政务办公人员、科室干部 | 机密数据不敢上云、公文写作效率低、政策检索困难 |
| **企业** | 行政、财务、法务、销售 | 流程繁琐、知识分散、重复劳动多 |
| **教育** | 教师、科研人员、教务管理 | 备课耗时、论文写作繁琐、数据分析困难 |
---
## 2. 架构设计
### 2.1 总体架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 租户层 (Tenant Layer) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 政府租户 │ │ 企业租户 │ │ 教育租户 │ │ 更多租户... │ │
│ │ 政务蓝主题 │ │ 企业蓝主题 │ │ 学术蓝主题 │ │ 自定义主题 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 领域适配层 (Domain Layer) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 技能包商店 │ │ 知识库模板 │ │ 应用模板 │ │
│ │ (Skills) │ │ (Templates) │ │ (Apps) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 核心引擎层 (Engine Layer) │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Agent │ │ Skills │ │ Knowledge │ │ MCP │ │ Channels │ │
│ │ Engine │ │ System │ │ RAG │ │ Gateway │ │ Bus │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 基础设施层 (Infra Layer) │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ SQLite │ │ Auth │ │ Storage │ │ Event │ │
│ │ (Local) │ │ Service │ │ Service │ │ Bus │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2.2 核心分层职责
| 层级 | 职责 | 扩展点 |
|------|------|-------|
| **租户层** | 品牌定制、主题切换、租户配置 | 新增领域时注册主题包 |
| **领域适配层** | 技能包管理、知识库模板、应用市场 | 扩展点最多,按需加载 |
| **核心引擎层** | AI对话、自动化、技能执行、RAG、MCP | 稳定,新增Agent类型 |
| **基础设施层** | 数据持久化、认证、文件存储、事件总线 | 底层能力,变更少 |
### 2.3 技术选型
```
┌─────────────────────────────────────────────────────────────────┐
│ 底座技术栈 │
├─────────────────────────────────────────────────────────────────┤
│ Rust (nomifun-tauri后端) │ 企业/政府首选(性能+安全) │
│ ──────────────────────────────────────────────────────────── │
│ Python Worker (可选扩展) │ AI模型/PPT生成等重型计算 │
│ ──────────────────────────────────────────────────────────── │
│ React 19 + Vite (前端) │ 跨平台,成熟生态 │
│ ──────────────────────────────────────────────────────────── │
│ SQLite + sqlx (本地存储) │ 零依赖部署,数据主权清晰 │
│ ──────────────────────────────────────────────────────────── │
│ PostgreSQL (可选云端存储) │ 多租户共享场景 │
└─────────────────────────────────────────────────────────────────┘
```
---
## 3. 模块划分
### 3.1 核心模块矩阵
| 模块 | 复用 nomifun-tauri | 需新增 | 优先级 |
|------|-------------------|--------|--------|
| Agent Engine | 100% | - | P0 |
| Skills System | 100% | - | P0 |
| Knowledge RAG | 80% | 领域模板 | P1 |
| MCP Gateway | 100% | - | P0 |
| Channels Bus | 90% | 领域适配 | P1 |
| Auth Service | 60% | 多租户适配 | P0 |
| Storage Service | 80% | 云端存储 | P2 |
| Theme System | 30% | 领域主题引擎 | P1 |
| App Templates | 20% | 应用商店框架 | P1 |
| Tenant Config | 0% | 新建核心模块 | P0 |
### 3.2 租户配置模型
```rust
// 租户配置核心模型
struct TenantConfig {
id: Uuid,
name: String,
domain: DomainType, // 政府 / 企业 / 教育 / 自定义
// 品牌定制
branding: BrandingConfig {
logo_url: String,
primary_color: Color,
theme_preset: ThemePreset,
},
// 功能开关
features: FeatureFlags {
app_store: bool,
skill_market: bool,
multi_user: bool,
cloud_sync: bool,
},
// 数据策略
data_policy: DataPolicy {
storage: StorageType, // local / cloud / hybrid
retention_days: u32,
encryption: bool,
},
// 领域配置
domain_config: DomainConfig,
}
```
### 3.3 领域适配模型
```rust
// 领域配置
enum DomainConfig {
Government(GovConfig), // 政务领域
Enterprise(EntConfig), // 企业领域
Education(EduConfig), // 教育领域
Custom(CustomConfig), // 自定义领域
}
// 政务配置示例
struct GovConfig {
// 预置应用分类
app_categories: Vec<AppCategory>,
// 公文模板
doc_templates: Vec<DocTemplate>,
// 知识库分类
knowledge_domains: Vec<KnowledgeDomain>,
// 审批流程
approval_workflows: Vec<Workflow>,
}
// 企业配置示例
struct EntConfig {
// 部门模板
departments: Vec<Department>,
// 业务流程
business_processes: Vec<Process>,
// 知识库分类
knowledge_domains: Vec<KnowledgeDomain>,
}
```
---
## 4. 演进路线
### Phase 1: 底座夯实 (MVP)
**目标**: 完善多租户基础设施,建立领域扩展框架
#### 1.1 租户系统
- [ ] 租户注册与配置管理
- [ ] 多租户数据隔离 (SQLite schema per tenant)
- [ ] 租户级别配置持久化
- [ ] 租户迁移与备份机制
#### 1.2 主题引擎
- [ ] 动态主题切换系统
- [ ] 预设主题包:
- [ ] 政务蓝主题 (GovAI配色体系)
- [ ] 企业专业主题
- [ ] 学术清新主题
- [ ] 主题预览与即时切换
#### 1.3 领域框架
- [ ] 领域配置schema定义
- [ ] 领域初始化向导
- [ ] 领域特定技能包注册
- [ ] 领域知识库模板
#### 1.4 基础设施增强
- [ ] 多用户认证系统 (OAuth/SSO集成预留)
- [ ] 角色权限模型 (RBAC)
- [ ] 操作审计日志
- [ ] 配置导出/导入
**Phase 1 交付物**:
```
底座平台 v1.0
├── 多租户管理后台
├── 3套预设主题
├── 领域配置框架
└── 完整文档
```
---
### Phase 2: 领域深化 (Domain Depth)
**目标**: 深耕政府/企业/教育三大领域,沉淀领域资产
#### 2.1 政府领域包
- [ ] 政务应用商店
- [ ] 公文写作助手
- [ ] 政策法规检索
- [ ] 会议纪要生成
- [ ] 信访答复辅助
- [ ] 政务知识库模板
- [ ] 法规库分类
- [ ] 政策解读模板
- [ ] 公文格式库
- [ ] 政务审批流程
- [ ] 多级审核
- [ ] 痕迹保留
- [ ] 归档管理
#### 2.2 企业领域包
- [ ] 企业应用商店
- [ ] 合同审查助手
- [ ] 财务报告生成
- [ ] 销售话术优化
- [ ] HR招聘辅助
- [ ] 企业知识库模板
- [ ] 产品知识库
- [ ] 流程规范库
- [ ] 培训资料库
- [ ] 企业协同流程
- [ ] 部门协作
- [ ] 任务分发
- [ ] 进度追踪
#### 2.3 教育领域包
- [ ] 教育应用商店
- [ ] 教案生成助手
- [ ] 论文润色工具
- [ ] 考试命题辅助
- [ ] 学术翻译助手
- [ ] 教育知识库模板
- [ ] 课程资料库
- [ ] 学术文献库
- [ ] 试题题库
- [ ] 教育分析工具
- [ ] 学情分析
- [ ] 教学质量评估
- [ ] 科研趋势分析
#### 2.4 技能市场
- [ ] 技能包发布与管理
- [ ] 技能包市场浏览
- [ ] 技能包安装/卸载
- [ ] 技能包评分与评论
- [ ] 技能包版本管理
**Phase 2 交付物**:
```
底座平台 v2.0
├── 3套完整领域包
├── 技能市场
├── 知识库模板市场
└── 领域应用商店
```
---
### Phase 3: 生态扩展 (Ecosystem)
**目标**: 建立开放生态,支持第三方扩展
#### 3.1 开放API
- [ ] 租户管理 API
- [ ] 应用管理 API
- [ ] 知识库 API
- [ ] 技能包 API
- [ ] Webhook 事件订阅
#### 3.2 插件系统
- [ ] 插件SDK (Rust/Python/JS)
- [ ] 插件市场
- [ ] 插件沙箱执行
- [ ] 插件审核机制
#### 3.3 数据集成
- [ ] 企业微信/钉钉/飞书集成
- [ ] LDAP/AD 目录集成
- [ ] 数据库直连 (MySQL/PostgreSQL)
- [ ] 文件存储集成 (S3/OSS/MinIO)
#### 3.4 高级功能
- [ ] 多租户联邦学习
- [ ] 跨租户知识共享
- [ ] 私有模型部署支持
- [ ] 边缘节点支持
**Phase 3 交付物**:
```
底座平台 v3.0
├── 完整开放API
├── 插件系统
├── 企业集成适配器
└── 联邦学习框架
```
---
## 5. 各领域适配方案
### 5.1 政府领域适配
```
┌─────────────────────────────────────────────────────────────┐
│ 政府领域架构 │
├─────────────────────────────────────────────────────────────┤
│ 特色能力 │
│ ├─ 数据不出内网 (本地SQLite优先) │
│ ├─ 等保合规 (审计日志、操作留痕) │
│ ├─ 公文格式标准化 (党政/行政/人大等) │
│ ├─ 多级审批流程 │
│ └─ 涉密数据隔离 │
│ │
│ 借鉴 GovAI │
│ ├─ 政务蓝配色体系 │
│ ├─ 多租户用户管理 │
│ ├─ 应用审核机制 │
│ └─ RAG知识库分类 │
└─────────────────────────────────────────────────────────────┘
```
### 5.2 企业领域适配
```
┌─────────────────────────────────────────────────────────────┐
│ 企业领域架构 │
├─────────────────────────────────────────────────────────────┤
│ 特色能力 │
│ ├─ 多部门协同 (权限矩阵) │
│ ├─ 业务系统集成 (ERP/CRM/OA) │
│ ├─ 私有知识沉淀 │
│ ├─ 流程自动化 │
│ └─ 成本控制 (token配额管理) │
│ │
│ 扩展方向 │
│ ├─ 销售域: 客户画像、话术推荐、竞品分析 │
│ ├─ 研发域: 代码审查、文档生成、技术方案 │
│ ├─ 财务域: 报表分析、预算预测、合规检查 │
│ └─ HR域: 简历筛选、面试题库、入职指引 │
└─────────────────────────────────────────────────────────────┘
```
### 5.3 教育领域适配
```
┌─────────────────────────────────────────────────────────────┐
│ 教育领域架构 │
├─────────────────────────────────────────────────────────────┤
│ 特色能力 │
│ ├─ 学术规范 (引用格式、查重) │
│ ├─ 多语言支持 (学术翻译) │
│ ├─ 版权合规 (知识库版权检测) │
│ ├─ 教研协同 │
│ └─ 学生隐私保护 (FERPA/GDPR) │
│ │
│ 扩展方向 │
│ ├─ K12: 教案生成、试题组卷、作业批改 │
│ ├─ 高等教育: 论文写作、科研助手、学术检索 │
│ ├─ 企业培训: 课程开发、培训评估、能力认证 │
│ └─ 终身学习: 技能提升、职业规划、学习推荐 │
└─────────────────────────────────────────────────────────────┘
```
---
## 6. 数据架构
### 6.1 多租户数据隔离策略
| 策略 | 适用场景 | 优点 | 缺点 |
|------|---------|------|------|
| **Schema隔离** | 高安全租户 | 完全隔离、性能好 | 管理复杂 |
| **行级隔离** | 普通租户 | 管理简单、成本低 | 隔离性弱 |
| **混合模式** | 敏感数据 | 灵活 | 实现复杂 |
### 6.2 数据流设计
```
┌──────────────────────────────────────────────────────────────────┐
│ 数据流架构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 用户输入 ──▶ 租户路由 ──▶ 领域处理器 ──▶ 知识库检索 │
│ │ │
│ ▼ │
│ Agent Engine │
│ │ │
│ ▼ │
│ 输出存储 ◀── 审计日志 ◀── 响应生成 ◀── 工具调用 │
│ │
│ 知识库写入: │
│ 文档上传 ──▶ 领域分块 ──▶ 向量嵌入 ──▶ 租户向量库 │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 6.3 知识库RAG增强
```rust
// 领域感知RAG配置
struct DomainRAGConfig {
// 分块策略
chunk_strategy: ChunkStrategy,
// 嵌入模型
embedding_model: String,
// 检索策略
retrieval: RetrievalConfig {
top_k: u32,
similarity_threshold: f32,
rerank: bool,
},
// 领域特定后处理
post_processors: Vec<Box<dyn PostProcessor>>,
}
// 后处理器示例
trait PostProcessor: Send + Sync {
fn process(&self, chunks: &mut Vec<RetrievedChunk>, query: &str);
}
// 领域后处理器
struct GovPostProcessor {
// 政策时效性权重
policy_date_weight: f32,
// 法规效力等级权重
legal_level_weight: f32,
}
```
---
## 7. 安全架构
### 7.1 安全层级
```
┌─────────────────────────────────────────────────────────────┐
│ 安全架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 身份认证 │
│ ├─ 多因素认证 (TOTP/WebAuthn) │
│ ├─ SSO集成 (SAML/OIDC) │
│ └─ 会话管理 (JWT + 刷新令牌) │
│ │
│ 访问控制 │
│ ├─ RBAC (角色权限) │
│ ├─ ABAC (属性权限) │
│ └─ 数据边界 (租户隔离) │
│ │
│ 数据安全 │
│ ├─ 传输加密 (TLS 1.3) │
│ ├─ 存储加密 (AES-256) │
│ └─ 密钥管理 (HSM/KMS预留) │
│ │
│ 审计合规 │
│ ├─ 操作审计 (CRUD全记录) │
│ ├─ 数据导出审计 │
│ └─ 合规报告生成 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 7.2 政府合规特别设计
| 合规要求 | 实现方案 |
|---------|---------|
| 等保2.0 | 安全计算环境、安全通信网络、安全区域边界 |
| 数据不出网 | 本地SQLite优先、可选单向网闸 |
| 隐私保护 | 数据脱敏、匿名化处理、最小采集 |
| 审计追溯 | 完整操作日志、不可篡改存储 |
---
## 8. 部署架构
### 8.1 部署模式
| 模式 | 适用场景 | 数据位置 | 维护成本 |
|------|---------|---------|---------|
| **单机本地** | 个人/小团队 | 本地磁盘 | 低 |
| **局域网私有** | 政府部门/敏感企业 | 内网服务器 | 中 |
| **私有云** | 中大型企业 | 私有云存储 | 中 |
| **混合云** | 多分支机构 | 边缘+中心 | 高 |
### 8.2 部署拓扑
```
┌─────────────────────────────────────────────────────────────┐
│ 私有化部署拓扑 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [客户端] ──▶ [反向代理/Nginx] ──▶ [NomiFun Backend] │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ SQLite │ │
│ │ (租户数据) │ │
│ └───────────────┘ │
│ │ │
│ ┌───────────────┐ │
│ │ 文件存储 │ │
│ │ (本地/MinIO) │ │
│ └───────────────┘ │
│ │
│ 可选扩展: │
│ ├─ [GPU节点] ──▶ LLM推理加速 │
│ ├─ [向量库] ──▶ pgvector高性能检索 │
│ └─ [备份服务] ──▶ 定时备份到对象存储 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 9. 迁移与演进策略
### 9.1 从 nomifun-tauri 迁移
```
nomifun-tauri (当前)
│ Phase 1: 多租户抽象层
┌─────────────────────────┐
│ + TenantConfig │
│ + TenantContext │
│ + DomainLayer │
│ + ThemeEngine │
└─────────────────────────┘
│ Phase 2: 领域资产
┌─────────────────────────┐
│ + GovDomainPack │
│ + EntDomainPack │
│ + EduDomainPack │
│ + SkillMarket │
└─────────────────────────┘
│ Phase 3: 生态开放
┌─────────────────────────┐
│ + PluginSDK │
│ + OpenAPI │
│ + EnterpriseAdapters │
└─────────────────────────┘
```
### 9.2 从 GovAI 借鉴
| GovAI 资产 | 借鉴方式 |
|-----------|---------|
| 政务蓝配色 | 转为nomifun主题包 |
| 应用类型系统 | 适配为nomifun Skills |
| 多租户管理 | 重写(架构不同) |
| RAG流程 | 直接复用思路 |
| PPT生成 | 封装为Skill |
---
## 10. 风险与挑战
### 10.1 技术风险
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 多租户隔离不完善 | 数据泄露 | Phase1重点测试 |
| 领域抽象过度设计 | 灵活性降低 | 按需扩展 |
| 性能瓶颈 | 用户体验差 | 性能测试+优化 |
### 10.2 业务风险
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 领域需求差异大 | 开发成本高 | MVP验证+迭代 |
| 竞品压力 | 市场占用低 | 差异化定位 |
| 合规不确定性 | 合规风险 | 持续跟踪政策 |
---
## 11. 成功指标
### 11.1 技术指标
| 指标 | 目标值 |
|------|--------|
| 冷启动时间 | < 3s |
| 对话响应延迟 (TTFT) | < 500ms (本地模型) |
| 租户创建时间 | < 1min |
| 主题切换延迟 | < 100ms |
### 11.2 业务指标
| 指标 | Phase 1 | Phase 2 | Phase 3 |
|------|---------|---------|---------|
| 租户数 | 10 | 100 | 1000+ |
| 活跃用户/租户 | 10 | 50 | 200+ |
| 技能包数量 | 20 | 200 | 1000+ |
| 知识库文档数/租户 | 100 | 1000 | 10000+ |
---
## 12. 后续步骤
### 12.1 立即行动
1. **Phase 1 详细设计**: 完善多租户抽象层设计
2. **技术验证**: 实现最小租户隔离Demo
3. **主题系统**: 基于GovAI配色创建政务蓝主题
### 12.2 决策点
- [ ] 确认多租户隔离策略 (Schema vs 行级)
- [ ] 确定向量数据库选型 (SQLite-vss vs pgvector)
- [ ] 明确商业化路径
---
> 文档结束 — 如有疑问或需要细化特定模块,请告知。
@@ -0,0 +1,735 @@
# PrivBox-AIStation 软硬一体化产品体系
> 文档编号:ARCH-INTEGRATION-001
> 版本:v1.0.0
> 日期:2026-07-03
> 状态:正式发布
---
## 一、产品愿景与定位
### 1.1 一句话定位
**PrivBox-AIStation = 算力盒子 × 通用AI工作台底座 × 行业应用集**
一款"开箱即用、数据不出门、能力在身边"的私有化AI工作站,满足高安全要求客户的AI落地需求。
### 1.2 核心价值
| 价值维度 | 具体体现 |
|---------|---------|
| **硬件开箱即用** | 国产NPU算力,预装模型,接显示器/网线即可使用 |
| **软件开箱即用** | 浏览器访问工作台,30分钟内用上AI能力 |
| **数据不出门** | 本地推理,物理隔离,零数据外传 |
| **能力在身边** | 离线可用,断网也能AI辅助 |
| **持续可演进** | 软件持续升级,模型持续优化 |
### 1.3 目标客户
| 客户类型 | 核心痛点 | 解决方案 |
|---------|---------|---------|
| **党政机关** | 涉密数据不敢上云、公文写作效率低、政策检索困难 | 政务行业包 + 等保合规 |
| **国企/央企** | 公有AI存在泄露风险、长期使用成本高 | 企业行业包 + 私有化部署 |
| **金融机构** | 财务/风控数据敏感、合规要求严格 | 行业定制包 + 审计日志 |
| **军队/国防** | 全链路国产化、物理隔离要求 | 主私盒集群 + 全离线 |
| **科研/教育** | 涉密课题数据、学术规范需求 | 教育行业包 + 隐私保护 |
---
## 二、三层产品架构
### 2.1 架构总览
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 第三层:行业应用集 (Industry Packs) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ 政务包 │ │ 企业包 │ │ 教育包 │ │ 定制包 │ │
│ │ GovPack │ │ EntPack │ │ EduPack │ │ CustomPack │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 第二层:通用AI工作台底座 (Commons Platform) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 租户层 │ 领域适配层 │ 核心引擎层 │ 基础设施层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ 多租户配置 │ 技能包商店 │ Agent Engine │ SQLite存储 │ │
│ │ 主题引擎 │ 知识库模板 │ Skills System │ Auth服务 │ │
│ │ 品牌定制 │ 应用模板 │ RAG引擎 │ Storage服务 │ │
│ │ │ │ MCP Gateway │ Event Bus │ │
│ └─────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 第一层:PrivBox 硬件抽象层 (Hardware Abstraction) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 轻私盒 │ │ 主私盒 │ │ 主私盒集群 │ │
│ │ 100TOPS/48GB│ │ 400TOPS/192GB│ │ 横向扩展 │ │
│ │ 单用户 │ │ 5-20人共享 │ │ 超大规模 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2.2 层次关系
```
硬件层 (PrivBox)
│ 提供算力、存储、网络、物理安全
软件底座层 (Commons Platform)
│ 提供Agent引擎、Skills系统、RAG能力、多租户管理
行业应用层 (Industry Packs)
│ 提供垂直场景的技能包、知识库模板、工作流
交付产品 (PrivBox-AIStation)
```
### 2.3 各层职责
| 层次 | 稳定性 | 变更频率 | 主要维护方 |
|------|--------|---------|-----------|
| **硬件层** | 最高 | 硬件迭代时 | 硬件团队 |
| **软件底座层** | 高 | 季度迭代 | 软件团队 |
| **行业应用层** | 中 | 按需定制 | 行业团队/合作伙伴 |
---
## 三、第一层:PrivBox 硬件层
### 3.1 硬件规格
| 规格项 | 轻私盒 | 主私盒 | 主私盒集群 |
|--------|--------|--------|-----------|
| **定位** | 个人专属AI工位 | 部门级AI协作中枢 | 超大规模/涉密单位 |
| **AI算力** | 100 TOPS | 400 TOPS | 400×N TOPS |
| **主控CPU** | RISC-V国产 | 兆芯 | 兆芯×N |
| **内存** | 32GB合封+16GB | 128GB统一+64GB | 线性扩展 |
| **存储** | 500GB microSD | 500GB M.2+扩展 | 横向扩展 |
| **并发用户** | 1人 | 5-20人 | 50-200人 |
| **网络** | 千兆网口 | 千兆网口 | 万兆内网 |
| **体积** | 13×13×5cm | 20×20×12cm | 机架式 |
| **噪音** | <30dB | 38-43dB | 机房级 |
| **功耗** | 低 | 中 | 高 |
| **典型价格** | ¥8,000 | ¥25,000 | ¥80,000+ |
### 3.2 软件配置对照
| 规格项 | 轻私盒 | 主私盒 |
|--------|--------|--------|
| **预装模型** | Qwen2.5-7B-Instruct (INT8) | Qwen2.5-14B-Instruct (INT8) |
| **向量模型** | bge-small-zh | bge-base-zh |
| **ASR模型** | SenseVoice | SenseVoice |
| **可用推理内存** | ~20GB | ~80-90GB |
| **可运行模型范围** | 7B-13B | 14B-70B |
### 3.3 硬件抽象层 (HAL)
```
┌─────────────────────────────────────────────────────────────┐
│ 硬件抽象层 (HAL) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ NPU调度器 │ │ 内存管理器 │ │ 存储管理器 │ │
│ │ - 模型加载 │ │ - 统一内存 │ │ - 本地存储 │ │
│ │ - 并发控制 │ │ - KV Cache │ │ - 备份还原 │ │
│ │ - 算力分配 │ │ - 溢出处理 │ │ - 加密存储 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 网络管理器 │ │ 更新管理器 │ │ 监控管理器 │ │
│ │ - 内网访问 │ │ - USB更新 │ │ - 温度监控 │ │
│ │ - 离线模式 │ │ - 增量推送 │ │ - 状态上报 │ │
│ │ - 安全策略 │ │ - 回滚机制 │ │ - 日志收集 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 四、第二层:通用AI工作台底座
### 4.1 技术架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 通用AI工作台底座 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 租户层 (Tenant Layer) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 政府租户 │ │ 企业租户 │ │ 教育租户 │ │ │
│ │ │ 政务蓝主题 │ │ 企业蓝主题 │ │ 学术蓝主题 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 领域适配层 (Domain Layer) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 技能包商店 │ │ 知识库模板 │ │ 应用模板 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 核心引擎层 (Engine Layer) │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Agent │ │ Skills │ │ Knowledge │ │ MCP │ │ │
│ │ │ Engine │ │ System │ │ RAG │ │ Gateway │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 基础设施层 (Infra Layer) │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ SQLite │ │ Auth │ │ Storage │ │ Event │ │ │
│ │ │ (Local) │ │ Service │ │ Service │ │ Bus │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PrivBox HAL 适配层 │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ NPU推理 │ │ 模型服务 │ │ 离线更新 │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 4.2 核心模块矩阵
| 模块 | 来源 | 说明 | 与PrivBox集成 |
|------|------|------|--------------|
| **Agent Engine** | nomifun-tauri 100% | 对话、工具调用、Agent编排 | 对接NPU推理 |
| **Skills System** | nomifun-tauri 100% | 技能包加载、执行、管理 | 预装+按需安装 |
| **Knowledge RAG** | nomifun-tauri 80% | 文档检索、向量搜索 | 使用本地向量库 |
| **MCP Gateway** | nomifun-tauri 100% | 协议转换、插件接入 | 预留扩展 |
| **Auth Service** | nomifun-tauri 60% | 单用户认证 | 增强多用户 |
| **Tenant Config** | 新开发 | 多租户配置、隔离 | 核心新增 |
| **Theme Engine** | 新开发 | 主题切换、品牌定制 | 政务蓝首发 |
| **App Templates** | 新开发 | 应用创建框架 | 按行业包 |
### 4.3 多租户配置模型
```rust
// 租户配置核心模型
struct TenantConfig {
id: Uuid,
name: String,
domain: DomainType, // Government / Enterprise / Education / Custom
// 硬件绑定
hardware: HardwareBinding {
device_id: String,
device_type: DeviceType, // LightBox / MainBox / Cluster
max_users: u32,
},
// 品牌定制
branding: BrandingConfig {
logo_url: String,
primary_color: Color,
theme_preset: ThemePreset,
},
// 功能开关
features: FeatureFlags {
app_store: bool,
skill_market: bool,
multi_user: bool,
cloud_sync: bool,
},
// 数据策略
data_policy: DataPolicy {
storage: StorageType, // local / encrypted_local
retention_days: u32,
encryption: bool,
},
// 领域配置
domain_config: DomainConfig,
}
```
---
## 五、第三层:行业应用集
### 5.1 政务行业包 (GovPack)
```
┌─────────────────────────────────────────────────────────────┐
│ 政务行业包 (GovPack) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 【应用商店】 │
│ ├─ 公文写作: 通知/请示/报告/函等15种法定公文 │
│ ├─ 政策解读: 政策文件智能分析、要点提取 │
│ ├─ 法规检索: 常用法规自然语言检索 │
│ ├─ 会议纪要: 语音转写+结构化纪要生成 │
│ ├─ 信访答复: 信访件智能分类+答复草稿 │
│ ├─ 敏感词检测: 对外文稿自动审查 │
│ └─ 合规检查: 公文格式校验、制度合规性 │
│ │
│ 【预置知识库】 │
│ ├─ 法规政策库 (国家级/省级/市级) │
│ ├─ 公文格式库 (党政/行政/人大/政协) │
│ ├─ 常用语料库 (领导讲话、致辞) │
│ └─ 业务术语库 (各部委办局专业术语) │
│ │
│ 【特色功能】 │
│ ├─ 等保2.0合规模式 │
│ ├─ 操作审计日志 (不可篡改) │
│ ├─ 涉密文件处理流程 │
│ └─ 多级审批工作流 │
│ │
│ 【适用硬件】 主私盒 / 主私盒集群 │
│ 【授权模式】 年度授权 + 服务费 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 5.2 企业行业包 (EntPack)
```
┌─────────────────────────────────────────────────────────────┐
│ 企业行业包 (EntPack) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 【应用商店】 │
│ ├─ 合同审查: 条款风险识别、不利条款标注 │
│ ├─ 报告生成: 工作报告/分析报告自动撰写 │
│ ├─ 销售助手: 客户画像、话术推荐、竞品分析 │
│ ├─ 财务分析: 报表解读、异常检测、趋势预测 │
│ ├─ HR助手: 简历筛选、面试题库、入职指引 │
│ ├─ 代码辅助: 代码补全、审查、文档生成 │
│ └─ 数据分析: 业务数据智能分析、可视化 │
│ │
│ 【预置知识库】 │
│ ├─ 产品知识库 │
│ ├─ 流程规范库 │
│ ├─ 培训资料库 │
│ └─ 行业报告库 │
│ │
│ 【特色功能】 │
│ ├─ 多部门权限隔离 │
│ ├─ 部门知识库共享 │
│ ├─ 业务系统对接 (可选) │
│ └─ Token配额管理 │
│ │
│ 【适用硬件】 主私盒 / 主私盒集群 │
│ 【授权模式】 年度授权 + 服务费 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 5.3 教育行业包 (EduPack)
```
┌─────────────────────────────────────────────────────────────┐
│ 教育行业包 (EduPack) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 【应用商店】 │
│ ├─ 教案生成: 课程目标/教学过程/作业设计 │
│ ├─ 论文助手: 写作辅助、润色、查重 │
│ ├─ 试题生成: 选择/填空/简答多题型组卷 │
│ ├─ 学术翻译: 中英互译、专业术语 │
│ ├─ 文献综述: 多篇文献智能归纳 │
│ ├─ 学情分析: 学习数据可视化、报告生成 │
│ └─ 学术检索: 论文查找、引用分析 │
│ │
│ 【预置知识库】 │
│ ├─ 课程资料库 │
│ ├─ 学术文献库 │
│ ├─ 试题题库 │
│ └─ 教学案例库 │
│ │
│ 【特色功能】 │
│ ├─ 学术规范检查 │
│ ├─ 引用格式自动规范化 │
│ ├─ 版权检测 (文字重复率) │
│ └─ 学生隐私保护模式 │
│ │
│ 【适用硬件】 主私盒 / 轻私盒(教师个人) │
│ 【授权模式】 年度授权 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 六、产品矩阵 (SKU Design)
### 6.1 SKU总表
| SKU | 硬件 | 软件版本 | 行业包 | 目标客户 | 定价参考 |
|-----|------|---------|--------|---------|---------|
| **AIStation-Personal** | 轻私盒 | 通用底座(单租户) | 基础技能包 | 领导干部、专家 | ¥11,000 |
| **AIStation-Standard** | 主私盒 | 通用底座(多租户) | 标准技能包 | 中小企业、事业单位 | ¥36,000 |
| **AIStation-Government** | 主私盒 | 政务版底座 | GovPack政务包 | 党政机关 | ¥48,000 |
| **AIStation-Enterprise** | 主私盒 | 企业版底座 | EntPack企业包 | 国企、金融、涉密 | ¥55,000 |
| **AIStation-Education** | 主私盒 | 教育版底座 | EduPack教育包 | 高校、研究机构 | ¥40,000 |
| **AIStation-Custom** | 主私盒集群 | 定制版底座 | CustomPack | 超大型组织 | 项目制 |
### 6.2 软件配置对照表
| 软件能力 | Personal | Standard | Government | Enterprise | Education |
|---------|----------|----------|-----------|-----------|-----------|
| **多用户支持** | ❌ 单用户 | ✅ 多用户 | ✅ 多用户+部门 | ✅ 多用户+部门 | ✅ 多用户 |
| **RBAC权限** | ❌ | ✅ | ✅ | ✅ | ✅ |
| **审计日志** | ❌ | ✅ | ✅+等保 | ✅+合规 | ✅ |
| **主题定制** | ❌ | ✅ | ✅政务蓝 | ✅企业蓝 | ✅学术蓝 |
| **行业应用** | 基础包 | 标准包 | 政务包 | 企业包 | 教育包 |
| **知识库** | 个人级 | 部门级 | 部门+法规 | 部门+产品 | 课程+文献 |
| **API开放** | ❌ | 预留 | ✅ | ✅ | 预留 |
| **年度服务** | 1,000 | 3,000 | 8,000 | 10,000 | 5,000 |
---
## 七、数据流与集成
### 7.1 端到端数据流
```
┌──────────────────────────────────────────────────────────────────┐
│ 端到端数据流 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 用户输入 (浏览器) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Web 前端 (Vue) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 对话UI │ │ 应用UI │ │ 管理UI │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └────────┼────────────┼────────────┼────────────────────┘ │
│ │ │ │ │
│ └────────────┴────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 通用AI工作台底座 (Rust Backend) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 租户路由层 │ │ │
│ │ │ 用户认证 → 租户隔离 → 权限校验 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────────┼─────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Agent │ │ Skills │ │ RAG │ │ │
│ │ │ Engine │ │ System │ │ Engine │ │ │
│ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ └─────────────┴─────────────┘ │ │
│ │ │ │ │
│ └─────────────────────────┼───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PrivBox HAL 适配层 │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 模型服务层 (本地推理) │ │ │
│ │ │ │ │ │
│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │
│ │ │ │ 主模型API │ │ 向量检索 │ │ ASR服务 │ │ │ │
│ │ │ │ (LLM) │ │ (Vector) │ │ (语音) │ │ │ │
│ │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ └─────────────┴─────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ └───────────────────────┼───────────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ NPU 推理引擎 (国产NPU) │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 本地数据存储 │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ SQLite │ │ 向量存储 │ │ 文件存储 │ │ │
│ │ │ (结构化) │ │ (向量) │ │ (文档) │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 7.2 关键集成点
| 集成点 | 说明 | 技术方案 |
|--------|------|---------|
| **前端→后端** | Web UI与Rust Backend通信 | WebSocket + REST API |
| **后端→模型** | Agent调用LLM推理 | HTTP/gRPC 本地调用 |
| **后端→向量库** | RAG检索 | SQLite-vss / 内置向量 |
| **后端→存储** | 文档/文件存储 | 本地文件系统 |
| **底座→HAL** | 软件调用硬件能力 | FFI / IPC |
---
## 八、交付与部署
### 8.1 标准化交付流程
```
┌─────────────────────────────────────────────────────────────┐
│ 标准化交付流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 阶段1: 需求确认 (1-3天) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 客户场景调研 │ │
│ │ • 行业包选择 │ │
│ │ • 数据迁移需求评估 │ │
│ │ • 输出: 需求确认单 + 交付清单 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 阶段2: 工厂预配置 (3-5天) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 硬件检测与烧录 │ │
│ │ • 软件镜像预装 (底座+行业包) │ │
│ │ • 品牌定制配置 (Logo、主题) │ │
│ │ • 预置知识库导入 │ │
│ │ • 输出: 预配置完成清单 + 检测报告 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 阶段3: 现场交付 (1-2天) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 设备部署上架 │ │
│ │ • 网络配置与联调 │ │
│ │ • 管理员培训 │ │
│ │ • 初始化数据导入 │ │
│ │ • 功能验收测试 │ │
│ │ • 输出: 交付验收单 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 阶段4: 运维保障 (持续) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 远程运维支持 (可选,需客户授权) │ │
│ │ • 定期巡检服务 (可选) │ │
│ │ • 软件版本升级 (内网推送/USB) │ │
│ │ • 故障响应与处理 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 8.2 部署模式
| 模式 | 适用场景 | 网络要求 | 运维方式 |
|------|---------|---------|---------|
| **单机隔离** | 涉密单位、高安全场景 | 完全离线 | 物理运维 |
| **内网部署** | 普通政府/企业 | 内网连接 | 远程运维 |
| **混合部署** | 多分支机构 | 内网+专线 | 分级运维 |
### 8.3 镜像结构
```
┌─────────────────────────────────────────────────────────────┐
│ 软件镜像结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ PrivBox-AIStation-镜像-v1.0.0 │
│ ├── boot/ # 系统启动文件 │
│ ├── privbox-hal/ # 硬件抽象层 │
│ ├── commons-platform/ # 通用AI工作台底座 │
│ │ ├── agent-engine # Agent引擎 │
│ │ ├── skills-system # 技能系统 │
│ │ ├── rag-engine # RAG引擎 │
│ │ ├── tenant-config # 多租户配置 │
│ │ └── theme-engine # 主题引擎 │
│ ├── industry-pack/ # 行业应用包 (按SKU选择) │
│ │ ├── gov-pack/ # 政务包 │
│ │ ├── ent-pack/ # 企业包 │
│ │ └── edu-pack/ # 教育包 │
│ ├── models/ # 预装模型 │
│ │ ├── llm/ # 主模型 (7B/14B) │
│ │ ├── embedding/ # 向量模型 │
│ │ └── asr/ # ASR模型 │
│ └── data/ # 数据目录 (运行时) │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 九、演进路线
### 9.1 整体演进时间线
```
2026 Q3 2026 Q4 2027 Q1 2027 Q4
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ v1.0 │ │ v2.0 │ │ v3.0 │ │ v4.0 │
│ MVP发布 │ │ 规模化 │ │ 生态化 │ │ 智能化 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
• PrivBox适配 • 政务包v2 • 开放API • AI Agent
• 基础底座 • 企业包v1 • 插件市场 • 知识图谱
• 政务包v1 • 教育包v1 • 第三方集成 • 多模态
• 试点部署 • 运维平台 • 合作伙伴 • 行业深度AI
```
### 9.2 各层演进
#### 硬件层演进
| 时间 | 硬件版本 | 升级内容 |
|------|---------|---------|
| 2026 Q3 | 当前版本 | 轻私盒 + 主私盒 |
| 2027 Q1 | 升级版 | 主私盒Pro (更大算力) |
| 2027 Q2 | 集群版 | 主私盒集群控制器 |
| 2027 Q4 | 新一代 | 新一代芯片平台 |
#### 底座层演进
| 阶段 | 版本 | 功能 |
|------|------|------|
| MVP | v1.0 | 多租户、主题引擎、基础Skills |
| 扩展 | v2.0 | RBAC、审计日志、API开放 |
| 完善 | v3.0 | 插件市场、SSO集成、MCP扩展 |
| 智能 | v4.0 | Agent编排、知识图谱、多模态 |
#### 行业包演进
| 包类型 | v1.0 | v2.0 | v3.0 |
|--------|------|------|------|
| **GovPack** | 基础应用+法规库 | 工作流+审批+更多应用 | 政策分析+智能决策 |
| **EntPack** | 基础应用+知识库 | 业务流程+系统对接 | 智能分析+预测 |
| **EduPack** | 基础应用+文献库 | 试题库+学情分析 | 智能教研+推荐 |
---
## 十、商业模式
### 10.1 收入结构
| 收入类型 | 模式 | 说明 | 占比目标 |
|---------|------|------|---------|
| **硬件销售** | 一次性 | PrivBox设备销售 | 60% |
| **软件授权** | 一次性/年度 | AI工作台软件授权 | 25% |
| **行业包授权** | 一次性/年度 | 行业应用包授权 | 10% |
| **服务费** | 年度 | 部署/培训/运维服务 | 5% |
### 10.2 定价策略
| SKU | 硬件成本 | 软件授权 | 年度服务 | 合计首年 |
|-----|---------|---------|---------|---------|
| Personal | 8,000 | 2,000 | 1,000 | **11,000** |
| Standard | 25,000 | 8,000 | 3,000 | **36,000** |
| Government | 25,000 | 15,000 | 8,000 | **48,000** |
| Enterprise | 25,000 | 20,000 | 10,000 | **55,000** |
| Education | 25,000 | 10,000 | 5,000 | **40,000** |
### 10.3 渠道策略
```
┌─────────────────────────────────────────────────────────────┐
│ 销售渠道 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 直销 │
│ └─ 大客户部: 政府/国企/涉密单位 (高价值客户) │
│ │
│ 渠道商 │
│ ├─ 信创集成商: 政务行业渠道 (重点) │
│ ├─ 企业IT服务商: 企业市场渠道 │
│ └─ 行业软件商: 垂直领域渠道 (教育、医疗) │
│ │
│ 生态合作 │
│ ├─ OA厂商合作: 集成到办公系统 │
│ ├─ 政务云合作: 纳入政务云解决方案 │
│ ├─ 集成商合作: 作为AI算力模块输出 │
│ └─ 渠道商合作: 区域代理 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 十一、风险与应对
### 11.1 技术风险
| 风险 | 影响 | 应对措施 |
|------|------|---------|
| 国产NPU框架成熟度 | 性能和稳定性 | 深度测试,建立备选模型 |
| 多租户隔离安全 | 数据泄露风险 | Phase1重点测试,审计覆盖 |
| 离线场景兼容性 | 用户体验问题 | 充分测试,建立问题清单 |
### 11.2 市场风险
| 风险 | 影响 | 应对措施 |
|------|------|---------|
| 竞品压力 | 市场占有 | 差异化定位,强调软硬一体 |
| 客户预算周期 | 销售周期 | 分阶段交付,降低决策门槛 |
| 政策变化 | 合规要求 | 持续跟踪,提前布局 |
### 11.3 运营风险
| 风险 | 影响 | 应对措施 |
|------|------|---------|
| 服务能力不足 | 客户满意度 | 建立服务网络,合作伙伴赋能 |
| 软件版本碎片化 | 维护成本 | 标准化镜像,版本管控 |
| 定制需求膨胀 | 项目成本 | 明确范围,建立行业包标准 |
---
## 十二、文档关联
| 文档 | 编号 | 说明 |
|------|------|------|
| **通用AI工作台底座架构演进方案** | ARCH-EVOL-001 | 软件底座的三阶段演进规划 |
| **PrivBox软硬一体化整合方案** | ARCH-PROPOSAL-001 | 软硬整合产品化建议 |
| **本文档** | ARCH-INTEGRATION-001 | 三者整合关系完整说明 |
---
## 十三、下一步行动
### 立即行动 (1-2周)
1. **技术可行性验证**
- nomifun与PrivBox NPU适配测试
- 7B/14B模型运行效果验证
- 输出性能基准报告
2. **产品定义确认**
- 确定SKU矩阵与定价
- 确定首批行业包范围
- 确定交付流程
3. **试点客户储备**
- 筛选2-3家有意向客户
- 了解具体需求
### 短期目标 (1-2月)
1. 完成PrivBox适配版工作台开发
2. 完成政务行业包v1.0
3. 完成首批试点客户部署
---
> 文档结束 — 如需进一步细化某个模块,请告知。
@@ -0,0 +1,537 @@
# PrivBox + 通用AI工作台 — 软硬一体化整合方案
> 文档类型:战略建议书
> 版本:v1.0
> 日期:2026-07-03
---
## 一、整合价值分析
### 1.1 优势互补
| 维度 | PrivBox硬件 | 通用AI工作台底座 | 整合效果 |
|------|------------|-----------------|---------|
| **算力** | 100-400 TOPS国产NPU | 需要算力承载 | 提供完整算力解决方案 |
| **数据安全** | 物理隔离、数据不出盒 | 本地优先架构 | 双重安全背书 |
| **软件能力** | 通用工作台 | Agent+Skills+RAG | 垂直领域深度适配 |
| **交付形态** | 硬件盒子 | 软件license | 软硬件一体化产品 |
| **信创合规** | 全栈国产 | 国产框架兼容 | 满足信创要求 |
### 1.2 目标客户痛点
```
┌─────────────────────────────────────────────────────────────┐
│ 目标客户核心痛点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 数据安全焦虑 │
│ ├─ 涉密数据不敢上云 │
│ ├─ 公有AI存在数据泄露风险 │
│ └─ 监管要求数据本地化存储 │
│ │
│ 2. 合规压力 │
│ ├─ 等保2.0/分级保护认证要求 │
│ ├─ 信创替代政策 │
│ └─ 数据出境合规 │
│ │
│ 3. 使用成本 │
│ ├─ 公有云AI按token计费,长期成本高 │
│ ├─ 多人使用需要多个账号 │
│ └─ 缺乏一次性买断方案 │
│ │
│ 4. 场景适配 │
│ ├─ 通用AI工具缺乏垂直场景 │
│ ├─ 公文写作、政策解读需要专业能力 │
│ └─ 知识库需要预置行业数据 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 1.3 整合核心价值
**一句话定位**
> "开箱即用的私有化AI工作站 — 数据不出门,能力在身边"
**差异化卖点**
1. **硬件+软件+算法** 三位一体交付
2. **全栈国产化** 满足信创要求
3. **零运维** 开机即用
4. **可演进** 软件持续升级
---
## 二、产品矩阵设计
### 2.1 产品层级
```
┌─────────────────────────────────────────────────────────────┐
│ 产品矩阵 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PrivBox AIStation Pro │ │
│ │ 主私盒 + 行业版AI工作台 + 行业包 │ │
│ │ 目标:政府/国企/大型企业部门 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PrivBox AIStation Standard │ │
│ │ 主私盒 + 通用AI工作台 + 标准技能包 │ │
│ │ 目标:中小企业、事业单位 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PrivBox AIStation Personal │ │
│ │ 轻私盒 + 个人AI工作台 + 基础技能包 │ │
│ │ 目标:领导/专家/涉密岗位个人 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 SKU设计
| SKU | 硬件 | 软件 | 定价策略 | 目标客户 |
|-----|------|------|---------|---------|
| **AIStation-Personal** | 轻私盒 | 个人工作台 + 基础技能包 | 硬件成本+软件授权 | 领导干部、专家 |
| **AIStation-Standard** | 主私盒 | 通用工作台 + 标准技能包 | 硬件成本+软件授权 | 中小企业、事业单位 |
| **AIStation-Government** | 主私盒 | 政务版工作台 + 政府技能包 | 硬件成本+软件授权+服务费 | 党政机关 |
| **AIStation-Enterprise** | 主私盒 | 企业版工作台 + 行业技能包 | 硬件成本+软件授权+服务费 | 国企、金融、涉密单位 |
| **AIStation-Custom** | 主私盒集群 | 定制工作台 + 深度行业包 | 项目制 | 超大型组织 |
---
## 三、软件架构适配
### 3.1 适配PrivBox硬件规格
```
┌─────────────────────────────────────────────────────────────┐
│ 软件配置与硬件规格匹配 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 轻私盒 (100 TOPS / 32+16 GB) │
│ ├─ 主模型: Qwen2.5-7B-Instruct (INT8) │
│ ├─ 向量模型: bge-small-zh (INT8) │
│ ├─ ASR模型: SenseVoice (INT8) │
│ ├─ 并发用户: 1人 │
│ └─ 典型应用: 对话、写作、摘要、语音输入 │
│ │
│ 主私盒 (400 TOPS / 128+64 GB) │
│ ├─ 主模型: Qwen2.5-14B-Instruct (INT8) │
│ ├─ 向量模型: bge-base-zh (INT8) │
│ ├─ ASR模型: SenseVoice (INT8) │
│ ├─ 并发用户: 5-20人 │
│ └─ 典型应用: 全模块 + 知识库 + 会议转写 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 3.2 软件分层架构
```rust
// PrivBox适配层
mod privbox {
// 硬件抽象层
mod hardware {
// NPU调度
// 内存管理
// 存储管理
// 散热策略
}
// 推理引擎适配
mod inference {
// 国产NPU推理框架
// 模型加载与切换
// 并发调度
}
// 离线更新机制
mod update {
// USB批量更新
// 内网增量推送
// 回滚机制
}
}
```
### 3.3 离线优先架构
```
┌─────────────────────────────────────────────────────────────┐
│ 离线优先架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户请求 ──▶ 租户路由 ──▶ 本地推理 ──▶ 结果返回 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 模型服务层 │ │
│ │ (本地运行) │ │
│ └─────────────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 主模型API │ │ 向量检索 │ │ ASR服务 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ 可选扩展 (需联网): │
│ ├─ 云端模型补强 (非涉密数据) │
│ ├─ 远程运维管理 │
│ └─ 增量模型更新 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 四、行业应用包设计
### 4.1 政务行业包 (Government Pack)
```
┌─────────────────────────────────────────────────────────────┐
│ 政务行业包 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 预装应用 │
│ ├─ 公文写作: 通知/请示/报告/函等15种法定公文 │
│ ├─ 政策解读: 政策文件智能分析、要点提取 │
│ ├─ 法规检索: 常用法规自然语言检索 │
│ ├─ 会议纪要: 语音转写+结构化纪要生成 │
│ ├─ 信访答复: 信访件智能分类+答复草稿 │
│ └─ 敏感词检测: 对外文稿自动审查 │
│ │
│ 预置知识库 │
│ ├─ 法规政策库 (国家级/省级/市级) │
│ ├─ 公文格式库 (党政/行政/人大/政协) │
│ ├─ 常用语料库 (领导讲话、致辞) │
│ └─ 业务术语库 (各部委办局专业术语) │
│ │
│ 特色功能 │
│ ├─ 等保2.0合规模式 │
│ ├─ 操作审计日志 (不可篡改) │
│ ├─ 涉密文件处理流程 │
│ └─ 多级审批工作流 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 4.2 企业行业包 (Enterprise Pack)
```
┌─────────────────────────────────────────────────────────────┐
│ 企业行业包 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 预装应用 │
│ ├─ 合同审查: 条款风险识别、不利条款标注 │
│ ├─ 报告生成: 工作报告/分析报告自动撰写 │
│ ├─ 销售助手: 客户画像、话术推荐、竞品分析 │
│ ├─ 财务分析: 报表解读、异常检测、趋势预测 │
│ ├─ HR助手: 简历筛选、面试题库、入职指引 │
│ └─ 代码辅助: 代码补全、审查、文档生成 │
│ │
│ 预置知识库 │
│ ├─ 产品知识库 │
│ ├─ 流程规范库 │
│ ├─ 培训资料库 │
│ └─ 行业报告库 │
│ │
│ 特色功能 │
│ ├─ 多部门权限隔离 │
│ ├─ 部门知识库共享 │
│ ├─ 业务系统对接 (可选) │
│ └─ Token配额管理 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 4.3 教育行业包 (Education Pack)
```
┌─────────────────────────────────────────────────────────────┐
│ 教育行业包 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 预装应用 │
│ ├─ 教案生成: 课程目标/教学过程/作业设计 │
│ ├─ 论文助手: 写作辅助、润色、查重 │
│ ├─ 试题生成: 选择/填空/简答多题型组卷 │
│ ├─ 学术翻译: 中英互译、专业术语 │
│ ├─ 文献综述: 多篇文献智能归纳 │
│ └─ 学情分析: 学习数据可视化、报告生成 │
│ │
│ 预置知识库 │
│ ├─ 课程资料库 │
│ ├─ 学术文献库 │
│ ├─ 试题题库 │
│ └─ 教学案例库 │
│ │
│ 特色功能 │
│ ├─ 学术规范检查 │
│ ├─ 引用格式自动规范化 │
│ ├─ 版权检测 (文字重复率) │
│ └─ 学生隐私保护模式 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 五、交付与部署方案
### 5.1 标准化交付流程
```
┌─────────────────────────────────────────────────────────────┐
│ 交付流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 需求确认 (1-3天) │
│ ├─ 客户场景调研 │
│ ├─ 行业包选择 │
│ ├─ 数据迁移需求 │
│ └─ 输出: 交付清单 │
│ │
│ 2. 工厂预配置 (3-5天) │
│ ├─ 硬件检测与烧录 │
│ ├─ 软件镜像预装 │
│ ├─ 行业包定制配置 │
│ └─ 输出: 预配置完成清单 │
│ │
│ 3. 现场交付 (1-2天) │
│ ├─ 设备部署上架 │
│ ├─ 网络配置与联调 │
│ ├─ 初始化数据导入 │
│ ├─ 管理员培训 │
│ └─ 输出: 交付验收单 │
│ │
│ 4. 运维保障 │
│ ├─ 远程运维支持 (可选) │
│ ├─ 定期巡检服务 (可选) │
│ └─ 软件版本升级 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 5.2 部署模式
| 部署模式 | 适用场景 | 网络要求 | 运维模式 |
|---------|---------|---------|---------|
| **单机隔离** | 涉密单位、高安全场景 | 完全离线 | 物理运维 |
| **内网部署** | 普通政府/企业 | 内网连接 | 远程运维 |
| **混合部署** | 多分支机构 | 内网+专线 | 分级运维 |
### 5.3 运维体系
```
┌─────────────────────────────────────────────────────────────┐
│ 运维体系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 监控体系 │
│ ├─ 硬件状态监控 (温度/负载/存储) │
│ ├─ 模型服务监控 (请求量/延迟/错误率) │
│ ├─ 用户行为监控 (活跃度/使用排行) │
│ └─ 安全事件监控 (异常登录/越权访问) │
│ │
│ 升级机制 │
│ ├─ 离线升级: USB批量推送 │
│ ├─ 内网升级: 增量差分更新 │
│ └─ 热更新: 无感知的模型切换 │
│ │
│ 故障处理 │
│ ├─ 本地日志导出 │
│ ├─ 远程诊断 (可选,需客户授权) │
│ └─ 备机置换 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 六、商业模式建议
### 6.1 定价策略
| 收入类型 | 模式 | 说明 |
|---------|------|------|
| **硬件销售** | 一次性 | PrivBox设备销售 |
| **软件授权** | 一次性/年度 | AI工作台软件授权 |
| **行业包授权** | 一次性/年度 | 行业应用包授权 |
| **服务费** | 年度 | 部署/培训/运维服务 |
| **升级费** | 按需 | 大版本升级 |
**建议定价区间** (参考)
| SKU | 硬件成本 | 软件授权 | 年度服务 | 合计首年 |
|-----|---------|---------|---------|---------|
| Personal | 8,000 | 2,000 | 1,000 | 11,000 |
| Standard | 25,000 | 8,000 | 3,000 | 36,000 |
| Government | 25,000 | 15,000 | 8,000 | 48,000 |
| Enterprise | 25,000 | 20,000 | 10,000 | 55,000 |
### 6.2 渠道策略
```
┌─────────────────────────────────────────────────────────────┐
│ 销售渠道 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 直销 │
│ └─ 大客户部: 政府/国企/涉密单位 │
│ │
│ 渠道商 │
│ ├─ 信创集成商: 政务行业渠道 │
│ ├─ 企业IT服务商: 企业市场渠道 │
│ └─ 行业软件商: 垂直领域渠道 │
│ │
│ 生态合作 │
│ ├─ OA厂商合作: 集成到办公系统 │
│ ├─ 政务云合作: 纳入政务云解决方案 │
│ └─ 集成商合作: 作为AI算力模块 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 七、风险与挑战
### 7.1 技术风险
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 国产NPU推理框架成熟度 | 性能和稳定性 | 深度测试,建立备选方案 |
| 模型与硬件适配 | 运行效率 | 针对性优化,建立性能基准 |
| 离线场景兼容性问题 | 用户体验 | 充分测试,建立问题清单 |
### 7.2 市场风险
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 竞品压力 | 市场占有 | 差异化定位,强调软硬一体 |
| 客户预算周期 | 销售周期 | 分阶段交付,降低决策门槛 |
| 政策变化 | 合规要求 | 持续跟踪,提前布局 |
### 7.3 运营风险
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 服务能力不足 | 客户满意度 | 建立服务网络,合作伙伴赋能 |
| 软件版本碎片化 | 维护成本 | 标准化镜像,版本管控 |
| 定制需求膨胀 | 项目成本 | 明确范围,建立行业包标准 |
---
## 八、实施路线图
### 8.1 Phase 1: 产品化 (Q3 2026)
```
目标: 完成PrivBox + 通用工作台的产品化
里程碑:
├─ M1: 适配层开发完成 (nomifun → PrivBox)
├─ M2: 基础工作台功能验证
├─ M3: 首批行业包 (政务) 开发
└─ M4: 试点客户部署 (2-3家)
产出:
├─ PrivBox-AIStation 软件镜像
├─ 政务行业包 v1.0
└─ 部署文档与培训材料
```
### 8.2 Phase 2: 规模化 (Q4 2026)
```
目标: 建立销售与服务体系
里程碑:
├─ M1: 签约5-10家渠道商
├─ M2: 企业/教育行业包发布
├─ M3: 建立远程运维平台
└─ M4: 完成50+客户部署
产出:
├─ 企业行业包 v1.0
├─ 教育行业包 v1.0
└─ 运维平台 v1.0
```
### 8.3 Phase 3: 生态化 (2027)
```
目标: 建立开放生态
里程碑:
├─ M1: 开放API发布
├─ M2: 第三方应用市场
├─ M3: 合作伙伴计划
└─ M4: 行业解决方案认证
产出:
├─ 开放平台 v1.0
├─ 应用市场
└─ 合作伙伴体系
```
---
## 九、关键成功因素
### 9.1 产品层面
- **开箱即用**: 用户拿到设备后30分钟内能用上AI能力
- **稳定可靠**: 7×24小时稳定运行,故障率<1%
- **持续演进**: 软件持续更新,模型持续优化
### 9.2 商务层面
- **信任建立**: 充分的案例背书,安全认证
- **服务保障**: 快速响应,专业支持
- **价值可视化**: 让客户看到AI带来的效率提升
### 9.3 生态层面
- **合作伙伴**: 建立覆盖主要区域的销售服务网络
- **行业深耕**: 针对重点行业打造标杆案例
- **标准输出**: 参与行业标准制定,建立话语权
---
## 十、下一步行动建议
### 立即行动 (1-2周)
1. **技术可行性验证**
- 完成nomifun与PrivBox NPU的适配测试
- 验证7B/14B模型在PrivBox上的运行效果
- 输出性能基准报告
2. **产品定义确认**
- 确定产品矩阵与SKU定义
- 确定首批行业包范围
- 确定定价策略
3. **试点客户储备**
- 筛选2-3家有意向的试点客户
- 了解具体需求和使用场景
### 短期目标 (1-2月)
1. 完成PrivBox适配版工作台开发
2. 完成政务行业包v1.0
3. 完成首批试点客户部署
---
> 文档结束 — 如需进一步细化某个模块或开始执行,请告知。
+54
View File
@@ -0,0 +1,54 @@
# NomiFun Documentation
This folder contains the current technical, operator, and contributor
documentation for **NomiFun**. It holds only material that matches the current
implementation; historical design specs and audits are not maintained in the
repo — consult git history when you need them.
> New to the project? Start with
> [Getting Started -> Introduction](getting-started/introduction.md).
> Chinese docs start at [README.zh.md](README.zh.md).
## Start Here
| Need | Read |
| --- | --- |
| Understand what NomiFun is | [getting-started/introduction.md](getting-started/introduction.md) |
| Install or run locally | [getting-started/installation.md](getting-started/installation.md) |
| Try the app quickly | [getting-started/quick-start.md](getting-started/quick-start.md) |
| Understand the current architecture | [architecture/overview.md](architecture/overview.md) |
| Build or package the project | [contributing/building-and-packaging.md](contributing/building-and-packaging.md) |
| Look up flags, env vars, or API groups | [reference/](reference/) |
| Contribute to the project | [../CONTRIBUTING.md](../CONTRIBUTING.md) |
| Community expectations | [../CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) |
| Report a security issue | [../SECURITY.md](../SECURITY.md) |
| Release notes and release process | [../CHANGELOG.md](../CHANGELOG.md), [../RELEASING.md](../RELEASING.md) |
## Current Documentation
```text
docs/
├── getting-started/ introduction, installation, quick start
├── guides/ current product/operator guides
├── architecture/ current system architecture and implementation map
├── reference/ configuration, API overview, troubleshooting, FAQ
├── contributing/ development, project structure, build/package notes
├── skills/ exported skill docs for external agents
└── images/ screenshot manifest and referenced images
```
Current top-level user surfaces include conversations, terminals, model
management, assistants, MCP, open capabilities, requirements/AutoWork,
scheduled tasks, companions, knowledge, and feature-gated computer/browser
automation. The frontend source of truth is
`ui/src/renderer/components/layout/Router.tsx`.
## Editing Rules
- Keep English and Simplified Chinese siblings in sync when both exist.
- Prefer linking to source files for implementation facts rather than repeating
fragile line-by-line state.
- Do not document redirected legacy UI paths as primary navigation.
- When a feature is not surfaced in `Router.tsx`, do not present it as an active
user feature even if backend routes still exist.
- For scripts, use `package.json` and `bun run help` as the source of truth.
+51
View File
@@ -0,0 +1,51 @@
# NomiFun 文档
本目录保存 **NomiFun** 当前的技术文档、运维文档与贡献者文档。这里只保留与
当前实现一致的内容;历史设计稿与审计记录不在仓库中维护,需要时请查阅 git
历史。
> 初次接触项目请从
> [入门 -> 项目介绍](getting-started/introduction.zh.md) 开始。
> English docs start at [README.md](README.md).
## 从这里开始
| 目标 | 阅读 |
| --- | --- |
| 了解 NomiFun 是什么 | [getting-started/introduction.zh.md](getting-started/introduction.zh.md) |
| 安装或本地运行 | [getting-started/installation.zh.md](getting-started/installation.zh.md) |
| 快速试用 | [getting-started/quick-start.zh.md](getting-started/quick-start.zh.md) |
| 理解当前架构 | [architecture/overview.zh.md](architecture/overview.zh.md) |
| 构建或打包项目 | [contributing/building-and-packaging.zh.md](contributing/building-and-packaging.zh.md) |
| 查询参数、环境变量或 API 分组 | [reference/](reference/) |
| 参与贡献 | [../CONTRIBUTING.md](../CONTRIBUTING.md) |
| 社区行为准则 | [../CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) |
| 报告安全问题 | [../SECURITY.md](../SECURITY.md) |
| 版本记录与发布流程 | [../CHANGELOG.md](../CHANGELOG.md)、[../RELEASING.md](../RELEASING.md) |
## 当前文档
```text
docs/
├── getting-started/ 介绍、安装、快速开始
├── guides/ 当前产品与运维指南
├── architecture/ 当前系统架构与实现地图
├── reference/ 配置、API 概览、排障、FAQ
├── contributing/ 开发、项目结构、构建与打包
├── skills/ 面向外部 agent 的 skill 文档
└── images/ 截图清单与图片资源
```
当前顶层用户界面包括会话、终端、模型管理、助手、MCP、开放能力、
需求/AutoWork、定时任务、伙伴、知识库,以及 feature-gated 的
computer/browser 自动化能力。前端路由真相来源是
`ui/src/renderer/components/layout/Router.tsx`
## 编辑规则
- 有中英文兄弟文件时,保持两者同步。
- 实现事实优先链接源码,不重复容易漂移的细节。
- 不把已重定向的旧 UI 路径写成主导航。
- 如果某功能没有出现在 `Router.tsx` 的当前产品路由中,即使后端仍有
route,也不要把它写成活跃用户功能。
- 脚本说明以 `package.json``bun run help` 为准。
@@ -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`)。
@@ -0,0 +1,160 @@
# Building and Packaging
This page covers release artifacts from the current **NomiFun** monorepo: the
React SPA, `nomifun-web`, Tauri desktop bundles, updater payloads, Docker, and
native Linux service files.
For day-to-day loops, see [`development.md`](development.md). For operator
deployment, see [`../guides/web-server-deployment.md`](../guides/web-server-deployment.md).
## Current Status
| Artifact | Current state |
| --- | --- |
| SPA (`ui/dist`) | Built by `bun run build:ui`; consumed by desktop and web hosts. |
| `nomifun-web` | Supported self-hosted binary; auth on by default. |
| Tauri desktop bundles | Built by `bun run build` for the current OS. |
| macOS Developer ID signing + notarization | Supported through `bun run build:signed` when local Apple signing credentials are configured. |
| Tauri updater artifacts | `bun run build:updater` emits updater `.sig` files; production endpoint/key management still needs release setup. |
| Docker / Compose | Local image and compose stack are supported; no public registry image is promised here. |
| Native Linux + systemd | Unit and README live under `packaging/linux/`. |
| Windows signing | Requires an external code-signing certificate; not configured by this repository. |
## SPA
```bash
bun run build:ui
```
Output: `ui/dist/`.
Desktop builds bundle this directory through `frontendDist` in
`apps/desktop/tauri.conf.json`. `nomifun-web` serves it from `--dist` /
`NOMIFUN_WEB_DIST`; when running from the repo, the default points at
`../../ui/dist` from `apps/web`.
## Web Binary
```bash
bun run build:ui
cargo build --release -p nomifun-web
```
Runtime requirements:
- built SPA directory;
- writable data directory;
- Bun on `PATH`, unless the binary was built with `NOMIFUN_EMBED_BUN=1`;
- configured auth/admin flow, or explicit `--insecure-no-auth` for trusted
loopback-only development.
Example:
```bash
target/release/nomifun-web --host 127.0.0.1 --port 8787 --dist ui/dist
```
First browser visit creates the admin account unless `NOMIFUN_ADMIN_USERNAME`
and `NOMIFUN_ADMIN_PASSWORD` pre-seed it.
## Desktop Bundles
```bash
bun run build
```
This runs Tauri build with `apps/desktop/tauri.conf.json`, builds the SPA first,
then creates OS-specific bundles under `target/release/bundle/`.
Product identity comes from `apps/desktop/tauri.conf.json`:
- `productName: "NomiFun"`
- `identifier: "com.nomifun.desktop"`
- version from workspace package metadata
- dev URL `http://localhost:5173`
- bundled frontend `../../ui/dist`
Tauri desktop bundles are best built on their target OS. Cross-OS desktop
packaging is not part of the supported workflow.
## macOS Signing and Notarization
Unsigned/ad-hoc macOS artifacts are useful for local testing but are not suitable
for distributing to other people. To produce a Developer ID signed and notarized
DMG:
```bash
cp apps/desktop/signing/.env.signing.example apps/desktop/signing/.env.signing
# fill local Apple signing/notary values
bun run build:signed
```
The real `.env.signing` file and Apple private keys are ignored by git. The
wrapper script is [`scripts/desktop-build-signed.sh`](../../scripts/desktop-build-signed.sh);
the detailed setup guide is
[`apps/desktop/signing/README.md`](../../apps/desktop/signing/README.md).
## Updater Artifacts
```bash
bun run build:updater
```
This enables Tauri's `createUpdaterArtifacts` and emits `.sig` files next to the
installers. These signatures are for the Tauri updater, not for OS trust. macOS
Gatekeeper still requires Developer ID signing/notarization; Windows still needs
code signing.
The updater scaffold exists, but a production release still needs:
- production updater key management;
- hosted `latest.json` endpoint;
- release-channel policy;
- renderer flow for download/apply/restart beyond the current check surface.
See [`apps/desktop/updater/README.md`](../../apps/desktop/updater/README.md).
## Docker
```bash
docker compose up -d --build
```
The root `Dockerfile` builds the SPA with Bun, builds `nomifun-web` in release
mode, and copies the binary plus `ui/dist` into a slim runtime image. Compose
starts one `nomifun` service on port `8787` with `/data` as `NOMIFUN_DATA_DIR`.
Open `http://<server>:8787` after boot. If no admin was pre-seeded, the first
reachable browser gets the first-run admin setup screen.
The optional Caddy service in `docker-compose.yml` is commented out; use it or a
similar reverse proxy for TLS and set `NOMIFUN_HTTPS=true` when the browser
reaches the app over HTTPS.
## Native Linux + systemd
See [`packaging/linux/README.md`](../../packaging/linux/README.md). The short
shape is:
```bash
bun install
bun run build:ui
cargo build --release -p nomifun-web
sudo cp target/release/nomifun-web /opt/nomifun/
sudo cp -r ui/dist/. /opt/nomifun/web/
sudo cp packaging/linux/nomifun-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nomifun-web
```
For systemd, set `SHELL` explicitly if agent child processes need a shell; a
nologin service user often has none.
## Checks Before Sharing an Artifact
- Run `cargo check --workspace`.
- Run `bun run build:ui`.
- For desktop, build on the target OS and smoke-test launch.
- For macOS distribution, validate `codesign`, `spctl`, and `xcrun stapler`.
- For web/Docker, verify first-run admin setup, login, `/health`, and WebSocket
connection through the intended host/reverse proxy.
@@ -0,0 +1,148 @@
# 构建与打包
本页说明当前 **NomiFun** monorepo 能产出的发布物:React SPA、`nomifun-web`
Tauri 桌面包、updater 产物、Docker 镜像和 Linux systemd 部署文件。
日常开发循环见 [`development.zh.md`](development.zh.md)。部署运行见
[`../guides/web-server-deployment.zh.md`](../guides/web-server-deployment.zh.md)。
## 当前状态
| 产物 | 当前状态 |
| --- | --- |
| SPA (`ui/dist`) | `bun run build:ui` 构建;桌面和 Web host 都使用它。 |
| `nomifun-web` | 支持的自托管 binary;默认开启鉴权。 |
| Tauri 桌面包 | `bun run build` 为当前 OS 构建。 |
| macOS Developer ID 签名 + 公证 | 已有 `bun run build:signed` 包装脚本;需要本机 Apple 签名配置。 |
| Tauri updater 产物 | `bun run build:updater` 会生成 updater `.sig`;生产 endpoint/key 管理仍需发布配置。 |
| Docker / Compose | 支持本地构建与 compose 运行;本文不承诺公开 registry 镜像。 |
| Native Linux + systemd | `packaging/linux/` 提供 unit 和说明。 |
| Windows 签名 | 需要外部代码签名证书;仓库内未配置。 |
## SPA
```bash
bun run build:ui
```
输出目录是 `ui/dist/`
桌面构建通过 `apps/desktop/tauri.conf.json``frontendDist` 打包该目录。
`nomifun-web` 通过 `--dist` / `NOMIFUN_WEB_DIST` 服务它;从仓库内运行时,
默认路径相对 `apps/web` 指向 `../../ui/dist`
## Web Binary
```bash
bun run build:ui
cargo build --release -p nomifun-web
```
运行要求:
- 已构建的 SPA 目录;
- 可写数据目录;
- `PATH` 上有 Bun,除非构建时使用 `NOMIFUN_EMBED_BUN=1`
- 默认鉴权/admin 初始化流程,或仅在可信本地开发中显式使用 `--insecure-no-auth`
示例:
```bash
target/release/nomifun-web --host 127.0.0.1 --port 8787 --dist ui/dist
```
未预置 `NOMIFUN_ADMIN_USERNAME` / `NOMIFUN_ADMIN_PASSWORD` 时,首次浏览器访问会创建管理员。
## 桌面包
```bash
bun run build
```
该命令调用 Tauri build,先构建 SPA,再在 `target/release/bundle/` 下生成当前
OS 的安装包/应用包。
产品身份来自 `apps/desktop/tauri.conf.json`
- `productName: "NomiFun"`
- `identifier: "com.nomifun.desktop"`
- 版本来自 workspace package metadata
- dev URL `http://localhost:5173`
- bundled frontend `../../ui/dist`
桌面包应在目标 OS 上构建。跨 OS 桌面打包不是当前支持流程。
## macOS 签名与公证
ad-hoc 签名产物只适合本地测试,不适合发给别人。生成 Developer ID 签名并公证的 DMG
```bash
cp apps/desktop/signing/.env.signing.example apps/desktop/signing/.env.signing
# 填写本机 Apple 签名/公证信息
bun run build:signed
```
真实 `.env.signing` 与 Apple 私钥不会入库。包装脚本在
[`scripts/desktop-build-signed.sh`](../../scripts/desktop-build-signed.sh),详细配置见
[`apps/desktop/signing/README.md`](../../apps/desktop/signing/README.md)。
## Updater 产物
```bash
bun run build:updater
```
该命令启用 Tauri `createUpdaterArtifacts`,在安装包旁生成 `.sig`。这些签名只给
Tauri updater 使用,不等于 OS 信任:macOS 仍需要 Developer ID 签名/公证;
Windows 仍需要代码签名证书。
生产发布仍需补齐:
- 生产 updater 密钥管理;
- 托管 `latest.json` endpoint
- 发布 channel 策略;
- renderer 中下载、应用、重启的完整流程。
见 [`apps/desktop/updater/README.md`](../../apps/desktop/updater/README.md)。
## Docker
```bash
docker compose up -d --build
```
`Dockerfile` 用 Bun 构建 SPA,用 Cargo 构建 release `nomifun-web`,再把 binary
`ui/dist` 复制到 slim runtime image。Compose 启动一个 `nomifun` 服务,
端口 `8787``/data` 作为 `NOMIFUN_DATA_DIR`
启动后访问 `http://<server>:8787`。如果没有预置管理员,第一个能访问到的浏览器
会看到首次管理员设置页。
`docker-compose.yml` 中的 Caddy 服务默认注释。需要 TLS 时可启用它或使用其他反向代理;
浏览器通过 HTTPS 访问时设置 `NOMIFUN_HTTPS=true`
## Native Linux + systemd
见 [`packaging/linux/README.md`](../../packaging/linux/README.md)。基本形态:
```bash
bun install
bun run build:ui
cargo build --release -p nomifun-web
sudo cp target/release/nomifun-web /opt/nomifun/
sudo cp -r ui/dist/. /opt/nomifun/web/
sudo cp packaging/linux/nomifun-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nomifun-web
```
systemd 环境中,如果 agent 子进程需要 shell,请显式设置 `SHELL`nologin 服务用户
通常没有可用 shell。
## 分发前检查
- `cargo check --workspace`
- `bun run build:ui`
- 桌面包在目标 OS 上构建并 smoke test 启动。
- macOS 分发前验证 `codesign``spctl``xcrun stapler`
- Web/Docker 验证首次管理员设置、登录、`/health` 和目标反向代理下的 WebSocket。
@@ -0,0 +1,161 @@
# Development
This page is for people changing **NomiFun** itself: the React SPA, the Rust
backend, the agent engine, or the Tauri shell. If you only want to install or
operate the product, start with
[`../getting-started/installation.md`](../getting-started/installation.md) or
[`../guides/web-server-deployment.md`](../guides/web-server-deployment.md).
The current repository is the active Tauri monorepo. Earlier Electron-era phase
plans and audits are not kept in the repo; consult git history if you need that
background.
## Prerequisites
| Tool | Minimum | Why |
| --- | --- | --- |
| Rust | stable, edition 2024 | Workspace uses resolver `3` and edition `2024`. |
| Bun | >= 1.3.13 | Frontend package manager, Vite runner, and runtime dependency for agent tooling. |
| Tauri CLI v2 | from `devDependencies` | Invoked through `bun run dev`, `bun run build`, and related scripts. |
| Git | recent | Required by development workflows and several built-in tools. |
| Native build tools | platform-specific | Needed for SQLite, TLS, libgit2, WebKit/WebView dependencies, and bundled native crates. |
Platform notes:
- Windows: MSVC C++ build tools and WebView2 runtime.
- macOS: Xcode Command Line Tools.
- Linux: `build-essential cmake clang pkg-config perl git`; desktop builds also need WebKitGTK 4.1 development headers.
## Install
```bash
git clone <repo-url> nomifun-tauri
cd nomifun-tauri
bun install
cargo check --workspace
```
The root `package.json` has one Bun workspace: `ui/`. Rust crates are resolved by
the root `Cargo.toml`.
## Development Loops
| Command | Use when | What runs |
| --- | --- | --- |
| `bun run dev:ui` | UI-only work that can tolerate missing API calls | Vite on `http://localhost:5173`; no backend. |
| `bun run dev:web` | Browser + backend iteration with auth disabled | `nomifun-web --port 8787 --dist ui/dist --insecure-no-auth` plus Vite dev server. |
| `bun run serve:web` | Running the production-style web host from source | `nomifun-web` on `http://127.0.0.1:8787`; serves built `ui/dist`; auth on by default. |
| `bun run dev` | Desktop/Tauri work | Tauri dev shell, Vite, and embedded backend under the desktop local-trust policy. |
`serve:web` expects a built SPA:
```bash
bun run build:ui
bun run serve:web
```
`dev:web` is a convenience loop that starts API and UI together. It uses
`--insecure-no-auth`, so keep it on localhost or an isolated network.
The desktop loop does **not** use the old Electron process model. The Tauri
shell links `nomifun-app`, starts the backend in-process on a free localhost
port, injects `window.__backendPort` and `window.__nomiLocalTrust`, and the
renderer presents that per-boot trust secret on every request.
## Verification
| Command | Coverage |
| --- | --- |
| `cargo check --workspace` | All Rust crates and app hosts compile. |
| `cargo test -p <crate>` | Focused Rust tests for one crate. |
| `bun run typecheck` | Renderer TypeScript. |
| `bun run check:i18n` | Generated i18n key types are up to date. |
| `bun run check:theme` | Theme token contract. |
| `bun run help --check` | Root script help output is current. |
| `bun run build:ui` | Production Vite build. |
| `bun run build` | Tauri desktop release bundle for the current OS. |
For a broad pre-PR check, run:
```bash
cargo check --workspace
bun run typecheck
bun run check:i18n
bun run check:theme
bun run help --check
```
## Backend CLI
`nomifun-app` still ships a standalone `nomicore` binary. The app hosts do not
spawn it, but it is useful for diagnostics, stdio MCP bridges, and public
capability calls.
Current subcommands:
- `mcp-requirement-stdio`
- `mcp-knowledge-stdio`
- `mcp-gateway-stdio`
- `mcp-open-stdio`
- `mcp-computer-stdio`
- `mcp-browser-stdio`
- `terminal-hook --event <kind>`
- `doctor`
- `tools`
- `call <name> [json-args]`
- `agent "<goal>"`
When agents fail to launch, start with:
```bash
cargo run -p nomifun-app --bin nomicore -- doctor
```
It probes installed agent CLIs from the same PATH shape the backend uses and
prints a table to stdout.
## Data and Work Directories
All hosts share the same unset default data directory:
- Windows: `%LOCALAPPDATA%\NomiFun\Nomi`
- macOS: `~/Library/Application Support/NomiFun/Nomi`
- Linux: `$XDG_DATA_HOME/NomiFun/Nomi` or `~/.local/share/NomiFun/Nomi`
The data dir contains SQLite state, logs, Bun runtime cache, extension data,
agent state, and other persistent local state. The backend takes an exclusive
`server.lock` before opening the database, so two live backends cannot use the
same data directory at the same time.
For isolated development, set an explicit directory:
```bash
NOMIFUN_DATA_DIR=/tmp/nomifun-dev bun run serve:web
NOMIFUN_DATA_DIR=/tmp/nomifun-dev bun run dev
```
Desktop app semantics append the channel-specific `Nomi` leaf; web and
`nomicore` take the env value literally. See
[`../reference/configuration.md`](../reference/configuration.md) before relying
on this in automation.
`NOMIFUN_WORK_DIR` controls where conversation workspaces are created. If unset,
the backend falls back to the data dir.
## Logs
Logs go to stdout and to `<data-dir>/logs/nomicore.log`. Use:
```bash
NOMIFUN_LOG_LEVEL='info,nomifun_mcp=trace' bun run serve:web
```
or pass `--log-level` to `nomicore` / `nomifun-web` directly. The value is a
`tracing_subscriber::EnvFilter` directive.
## Where to Read Next
- [`project-structure.md`](project-structure.md) for the repo map.
- [`../architecture/backend-crates.md`](../architecture/backend-crates.md) for crate ownership.
- [`../architecture/frontend.md`](../architecture/frontend.md) for routes and host adapters.
- [`building-and-packaging.md`](building-and-packaging.md) for release artifacts.
@@ -0,0 +1,154 @@
# 开发
本页面向修改 **NomiFun** 仓库本身的人:React SPA、Rust 后端、agent 引擎或
Tauri 桌面壳。如果只是安装或部署,请先看
[`../getting-started/installation.zh.md`](../getting-started/installation.zh.md)
或 [`../guides/web-server-deployment.zh.md`](../guides/web-server-deployment.zh.md)。
当前仓库已经是活跃的 Tauri monorepo。旧 Electron 迁移阶段的计划、审计与设计稿
不在仓库中保留;需要这些背景时请查阅 git 历史。
## 前置工具
| 工具 | 最低要求 | 用途 |
| --- | --- | --- |
| Rust | stableedition 2024 | Workspace 使用 resolver `3` 和 edition `2024`。 |
| Bun | >= 1.3.13 | 前端包管理、Vite runner,也被 agent 运行时使用。 |
| Tauri CLI v2 | 来自 `devDependencies` | 通过 `bun run dev` / `bun run build` 调用,无需全局安装。 |
| Git | 较新版本 | 开发流程和部分内置工具需要。 |
| 原生编译工具 | 按平台 | SQLite、TLS、libgit2、WebKit/WebView 与原生 crate 需要。 |
平台提示:
- WindowsMSVC C++ Build Tools 与 WebView2 runtime。
- macOSXcode Command Line Tools。
- Linux`build-essential cmake clang pkg-config perl git`;构建桌面端还需要 WebKitGTK 4.1 开发包。
## 安装依赖
```bash
git clone <repo-url> nomifun-tauri
cd nomifun-tauri
bun install
cargo check --workspace
```
`package.json` 只有一个 Bun workspace`ui/`。Rust crate 由根 `Cargo.toml`
管理。
## 开发循环
| 命令 | 适用场景 | 实际运行内容 |
| --- | --- | --- |
| `bun run dev:ui` | 纯 UI 工作,可接受 API 请求失败 | Vite on `http://localhost:5173`,不启动后端。 |
| `bun run dev:web` | 浏览器 + 后端联调,关闭登录 | `nomifun-web --port 8787 --dist ui/dist --insecure-no-auth` 加 Vite。 |
| `bun run serve:web` | 从源码跑生产形态 Web host | `nomifun-web` on `http://127.0.0.1:8787`,服务 `ui/dist`,默认开启登录。 |
| `bun run dev` | 桌面/Tauri 开发 | Tauri dev shell、Vite、桌面本地信任策略下的嵌入式后端。 |
`serve:web` 需要先构建 SPA
```bash
bun run build:ui
bun run serve:web
```
`dev:web` 会同时启动 API 与 UI,并使用 `--insecure-no-auth`,只适合 localhost
或隔离网络。
桌面循环已经不是旧 Electron 模型。Tauri shell 直接链接 `nomifun-app`,在进程内
启动后端,选择一个空闲 localhost 端口,注入 `window.__backendPort`
`window.__nomiLocalTrust`,renderer 每次请求都会带上这个本次启动生成的信任
secret。
## 验证命令
| 命令 | 覆盖范围 |
| --- | --- |
| `cargo check --workspace` | 所有 Rust crate 和 app host 编译。 |
| `cargo test -p <crate>` | 单个 crate 的 Rust 测试。 |
| `bun run typecheck` | Renderer TypeScript。 |
| `bun run check:i18n` | i18n key 类型生成是否最新。 |
| `bun run check:theme` | 主题 token 契约。 |
| `bun run help --check` | 根脚本帮助文本。 |
| `bun run build:ui` | 生产 Vite 构建。 |
| `bun run build` | 当前 OS 的 Tauri 桌面包。 |
提交前常用组合:
```bash
cargo check --workspace
bun run typecheck
bun run check:i18n
bun run check:theme
bun run help --check
```
## 后端 CLI
`nomifun-app` 仍然提供独立 `nomicore` binary。app host 不会 spawn 它,但诊断、
stdio MCP bridge 和公开能力调用仍会用到。
当前子命令:
- `mcp-requirement-stdio`
- `mcp-knowledge-stdio`
- `mcp-gateway-stdio`
- `mcp-open-stdio`
- `mcp-computer-stdio`
- `mcp-browser-stdio`
- `terminal-hook --event <kind>`
- `doctor`
- `tools`
- `call <name> [json-args]`
- `agent "<goal>"`
agent 无法启动时,先跑:
```bash
cargo run -p nomifun-app --bin nomicore -- doctor
```
它会按后端看到的 PATH 探测各个 agent CLI,并把结果打印到 stdout。
## 数据目录与工作目录
所有 host 未显式覆盖时共享同一个默认数据目录:
- Windows`%LOCALAPPDATA%\NomiFun\Nomi`
- macOS`~/Library/Application Support/NomiFun/Nomi`
- Linux`$XDG_DATA_HOME/NomiFun/Nomi``~/.local/share/NomiFun/Nomi`
数据目录包含 SQLite、日志、Bun runtime cache、extension 数据和 agent 状态。
后端启动时会先拿 `{data_dir}/server.lock` 独占锁,避免两个活跃后端同时写同一
目录。
隔离开发环境时显式指定:
```bash
NOMIFUN_DATA_DIR=/tmp/nomifun-dev bun run serve:web
NOMIFUN_DATA_DIR=/tmp/nomifun-dev bun run dev
```
桌面端会追加 channel 对应的 `Nomi` leafWeb host 和 `nomicore` 按 env 值字面
使用。自动化脚本依赖这个行为前,请先读
[`../reference/configuration.zh.md`](../reference/configuration.zh.md)。
`NOMIFUN_WORK_DIR` 控制会话工作区位置;未设置时回退到数据目录。
## 日志
日志同时写 stdout 和 `<data-dir>/logs/nomicore.log`。示例:
```bash
NOMIFUN_LOG_LEVEL='info,nomifun_mcp=trace' bun run serve:web
```
也可以把 `--log-level` 直接传给 `nomicore` / `nomifun-web`。该值是
`tracing_subscriber::EnvFilter` 语法。
## 继续阅读
- [`project-structure.zh.md`](project-structure.zh.md)
- [`../architecture/backend-crates.md`](../architecture/backend-crates.md)
- [`../architecture/frontend.md`](../architecture/frontend.md)
- [`building-and-packaging.zh.md`](building-and-packaging.zh.md)
@@ -0,0 +1,222 @@
# Project Structure
This is the authoritative repo map for **NomiFun**. It tells you which
directory holds what, what each Rust crate is responsible for, and the one
architectural rule that keeps the agent engine extractable. For the
deep-dive on backend layering see
[`../architecture/backend-crates.md`](../architecture/backend-crates.md);
for the runtime story (how the two app hosts boot the same backend) see
[`../architecture/overview.md`](../architecture/overview.md).
## Top-level layout
```
nomifun-tauri/
├── apps/
│ ├── web/ nomifun-web bin: standalone server (API + SPA)
│ └── desktop/ nomifun-desktop bin: Tauri shell (embedded backend)
├── crates/
│ ├── agent/ 15 nomi-* crates — the AI agent engine
│ ├── backend/ 29 nomifun-* crates — the HTTP/WS backend
│ └── shared/ 2 genuine cross-layer crates
├── ui/ React SPA (Vite + UnoCSS), the only Bun workspace
│ ├── src/common/ cross-host code: API clients, types, utils
│ ├── src/platform/ tiny host bridge (storage / logger / theme)
│ ├── src/renderer/ pages, components, hooks, services, styles
│ ├── public/ static assets
│ ├── index.html Vite entry
│ └── vite.config.ts Vite config
├── docs/
│ ├── getting-started/ install + first run
│ ├── guides/ task-focused how-tos for end users
│ ├── architecture/ how NomiFun is built (runtime, crates, frontend)
│ ├── reference/ configuration, API surface, troubleshooting
│ ├── contributing/ this directory
│ ├── specs/ dated engineering design docs (historical)
│ ├── audit/ dated audit reports (historical)
│ ├── superpowers/ session-scoped planning artifacts (historical)
│ └── archive/ historical-doc policy
├── packaging/
│ └── linux/ nomifun-web.service systemd unit + README
├── Cargo.toml Rust workspace (resolver "3", edition 2024)
├── package.json root scripts (dev:ui/build, web, dev/build)
├── Dockerfile nomifun-web container image
├── docker-compose.yml single-service compose for the web host
├── Caddyfile optional TLS reverse proxy (commented in compose)
├── README.md project introduction
└── STATUS.md current technical status snapshot
```
The Cargo workspace members are exactly:
```toml
[workspace]
resolver = "3"
members = ["crates/agent/*", "crates/backend/*", "crates/shared/*", "apps/web", "apps/desktop"]
```
`crates/shared/*` is now active. Keep new shared crates rare: if a crate belongs
only to the backend or only to the agent engine, keep it in that owning group.
## App hosts
| Path | Binary | Role |
| --- | --- | --- |
| [`apps/web`](../../apps/web) | `nomifun-web` | Standalone server. Boots the unified backend in-process and serves the built SPA from the same port. Authentication on by default; `--insecure-no-auth` opts back into the desktop trust model. Replaces the old Node `web-host`. |
| [`apps/desktop`](../../apps/desktop) | `nomifun-desktop` | Tauri shell. Picks a free localhost port, starts the same backend in-process, injects `window.__backendPort` and `window.__nomiLocalTrust`, and loads the SPA into the WebView. Single-instance + dialog + notification + deep-link + updater plugins registered. |
Both hosts link `nomifun-app` directly — there is no spawned `nomicore`
binary in either flow. The `nomicore` binary still exists as the
`[[bin]]` of `nomifun-app` for headless / CI use and for the
`nomicore doctor` self-check.
## Crate groups
The Rust crates are grouped by origin and naming convention. The grouping
is the migration unit: each top-level directory under `crates/` corresponds
to a future independent repository.
| Directory | Prefix | Count | Role | Future repo |
| --- | --- | --- | --- | --- |
| [`crates/agent/`](../../crates/agent) | `nomi-*` | 15 | AI agent engine. Self-contained — no dependency on any `nomifun-*` crate. | historical extraction target |
| [`crates/backend/`](../../crates/backend) | `nomifun-*` | 29 | HTTP/WS server, data layer, auth, sessions, cron, knowledge, terminal, companion, public gateway, ... | historical extraction target |
| [`crates/shared/`](../../crates/shared) | mixed | 2 | Cross-layer utilities used by both sides. | shared |
## The agent-layer seam
Backend feature code should normally go through
[`crates/backend/nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent)
when it needs agent types or agent execution. Most backend crates import
agent-facing types via
`nomifun_ai_agent::{nomi_config, nomi_types, RequirementSink}`.
The current workspace has feature-gated direct-dependency exceptions in
`nomifun-app` and `nomifun-gateway` for browser/computer-use bridge tooling.
When you add a new backend crate that needs an agent type:
1. Prefer not to add `nomi-* = ...` to your `Cargo.toml`.
2. Re-export what you need through `nomifun-ai-agent` or use what is already
re-exported there.
3. Consume it via `use nomifun_ai_agent::nomi_types::...;` etc.
4. If a direct dependency is required for a bridge/facade, gate it behind a
feature and document the exception in the crate manifest and architecture
docs.
Why: this keeps the agent engine mostly independent and prevents feature crates
from silently tying themselves to engine internals.
## `crates/agent/` — 15 `nomi-*` crates (the AI agent engine)
| Crate | One-line role |
| --- | --- |
| [`nomi-types`](../../crates/agent/nomi-types) | Pure, provider-neutral data types shared across all `nomi-*` crates. No dependencies on other agent crates. |
| [`nomi-protocol`](../../crates/agent/nomi-protocol) | JSON stream protocol for host ↔ agent communication: events (agent → host), commands (host → agent), approval manager. |
| [`nomi-compact`](../../crates/agent/nomi-compact) | Conversation-window compaction: fold / json / level / sanitize / TOON formatting. |
| [`nomi-config`](../../crates/agent/nomi-config) | Runtime configuration layer — `Config`, `ProviderCompat`, auth, hooks, provider-specific configs, file-cache. |
| [`nomi-providers`](../../crates/agent/nomi-providers) | LLM provider clients: Anthropic, Bedrock, OpenAI, Vertex; shared retry / streaming. |
| [`nomi-tools`](../../crates/agent/nomi-tools) | Built-in tools registry: bash, edit, glob, grep, read, tool-search, file-cache. |
| [`nomi-mcp`](../../crates/agent/nomi-mcp) | MCP client used by the agent: config, manager, protocol, tool-proxy, transports. |
| [`nomi-skills`](../../crates/agent/nomi-skills) | Skills system: discovery, frontmatter, loader, executor, hooks, conditional / context modifiers, bundled. |
| [`nomi-memory`](../../crates/agent/nomi-memory) | Long-term cross-session memory — preferences, feedback, project context, external references. |
| [`nomi-agent`](../../crates/agent/nomi-agent) | Core engine: session orchestration, bootstrap, commands, compaction, confirm, output sinks. |
| [`nomi-cli`](../../crates/agent/nomi-cli) | Standalone `nomi` binary that drives the engine without a host process. |
| [`nomi-computer`](../../crates/agent/nomi-computer) | Desktop computer-use tool implementation. |
| [`nomi-a11y`](../../crates/agent/nomi-a11y) | Accessibility helpers used by computer-use flows. |
| [`nomi-browser-engine`](../../crates/agent/nomi-browser-engine) | Self-hosted browser/CDP automation engine. |
| [`nomi-browser`](../../crates/agent/nomi-browser) | Browser-use tool layer. |
## `crates/backend/` — 29 `nomifun-*` crates (the backend)
| Crate | One-line role |
| --- | --- |
| [`nomifun-common`](../../crates/backend/nomifun-common) | Shared primitives: `AppError`, enums, ID generation, AES-GCM crypto, timestamps, pagination, common constants. |
| [`nomifun-assets`](../../crates/backend/nomifun-assets) | Backend-served static logo assets (`include_dir!`). |
| [`nomifun-db`](../../crates/backend/nomifun-db) | SQLite layer: `init_database`, embedded migrations, models, repository traits + sqlx implementations. |
| [`nomifun-api-types`](../../crates/backend/nomifun-api-types) | Every HTTP request/response DTO and the `WebSocketMessage` envelope; the renderer's TS types mirror this crate. |
| [`nomifun-realtime`](../../crates/backend/nomifun-realtime) | WebSocket connection manager, broadcaster, token-validated upgrade handler, message router. |
| [`nomifun-runtime`](../../crates/backend/nomifun-runtime) | Embeds bun (zstd-compressed) at build time, extracts to OS cache on first run; `enhance_process_path` merge for child processes. |
| [`nomifun-auth`](../../crates/backend/nomifun-auth) | JWT auth, bcrypt, login / refresh / setup routes, CSRF double-submit, security headers, rate limiting, `CurrentUser` extractor. |
| [`nomifun-system`](../../crates/backend/nomifun-system) | System services: provider management, model fetching, settings, version checks, Bedrock probe. |
| [`nomifun-file`](../../crates/backend/nomifun-file) | Filesystem operations: read/write, path safety, file watching, snapshots, zip. |
| [`nomifun-office`](../../crates/backend/nomifun-office) | Office-document preview, format conversion, proxy, snapshot management. |
| [`nomifun-shell`](../../crates/backend/nomifun-shell) | OS shell integration: opener, tool detection, speech-to-text. |
| [`nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent) | **The single bridge to `crates/agent/`.** Agent factory, registry, worker dispatch, ACP session persistence; re-exports `nomi_config` / `nomi_types` / `RequirementSink`. |
| [`nomifun-mcp`](../../crates/backend/nomifun-mcp) | MCP server config, multi-agent sync adapters, OAuth, connection testing. |
| [`nomifun-conversation`](../../crates/backend/nomifun-conversation) | Conversation + message CRUD with streaming relay, ACP error recovery, response middleware. |
| [`nomifun-extension`](../../crates/backend/nomifun-extension) | Extension registry: manifest parsing, hub installer, skill scanning, lifecycle hooks. |
| [`nomifun-channel`](../../crates/backend/nomifun-channel) | External channel integration: plugin system, pairing handshake, per-session messaging, formatter. |
| [`nomifun-team`](../../crates/backend/nomifun-team) | Multi-agent team sessions: role-based prompts, task board, mailbox, scheduling, crash detection. |
| [`nomifun-cron`](../../crates/backend/nomifun-cron) | Scheduled-job engine: cron scheduler, executor, lifecycle event emitter, busy-guard. |
| [`nomifun-requirement`](../../crates/backend/nomifun-requirement) | Requirements Platform: CRUD store + AutoWork orchestrator + completion notifier hooks. |
| [`nomifun-idmm`](../../crates/backend/nomifun-idmm) | Intelligent Decision-Making Mode: per-session supervision keeping agent / terminal sessions alive through provider faults. |
| [`nomifun-webhook`](../../crates/backend/nomifun-webhook) | Webhook management + AutoWork completion notifications (Lark/飞书 custom bots), per-tag bindings. |
| [`nomifun-terminal`](../../crates/backend/nomifun-terminal) | PTY-backed terminal sessions managed alongside conversations; streams output via the realtime broadcaster. |
| [`nomifun-assistant`](../../crates/backend/nomifun-assistant) | User-authored assistant management; merges built-in + user + extension assistants for `GET /api/assistants`. |
| [`nomifun-knowledge`](../../crates/backend/nomifun-knowledge) | Knowledge bases, bound-base state, and scoped knowledge MCP search. |
| [`nomifun-companion`](../../crates/backend/nomifun-companion) | Desktop companions, figures, shared memory, and companion-bound state. |
| [`nomifun-gateway`](../../crates/backend/nomifun-gateway) | Desktop Gateway MCP registry and platform capability tools. |
| [`nomifun-public`](../../crates/backend/nomifun-public) | Public `/mcp`, `/mcp-agent`, and `/v1` front doors with companion-token auth. |
| [`nomifun-secret`](../../crates/backend/nomifun-secret) | Per-companion browser-use secret storage. |
| [`nomifun-app`](../../crates/backend/nomifun-app) | Application crate: assembles every domain crate into the axum server with DI + middleware. Ships the `nomicore` binary. |
> The full backend layering — request lifecycle, who owns which routes, the
> agent seam in detail — is in
> [`../architecture/backend-crates.md`](../architecture/backend-crates.md).
## `apps/web` and `apps/desktop`
Both app crates are thin: they parse a small CLI, call into `nomifun-app`'s
public boot helpers, and own the shape of the host process.
```text
apps/web/src/main.rs ~165 lines
init runtime → init data layer → AppServices → create_router →
ServeDir(ui/dist) fallback → axum::serve
apps/desktop/src/main.rs ~250 lines
pick free port → init runtime → spawn embedded backend on a tokio
thread → tauri::Builder with single-instance/dialog/notification/
deep-link/updater plugins → window init-script injects window.__backendPort
```
`nomifun-app` exposes the boot entry as a library: `bootstrap`, `cli`,
`commands`, and a `run_embedded_server` helper, plus `AppServices` and
`create_router`. The `nomicore` bin is just one of three consumers.
## `ui/` — the React SPA
The frontend is a single Bun workspace, built with **plain Vite + UnoCSS**
(no `electron-vite`).
| Path | What lives here |
| --- | --- |
| [`ui/src/common/`](../../ui/src/common) | Cross-host code reused regardless of shell: `adapter/` (HTTP / WS bridges), `api/`, `chat/`, `config/`, `platform/`, `types/`, `update/`, `utils/`, plus the package barrel `index.ts`. |
| [`ui/src/platform/`](../../ui/src/platform) | The tiny host-bridge layer: `bridge.ts`, `logger.ts`, `storage.ts`, `theme.ts`. The renderer never imports Tauri / Electron APIs directly — it goes through this layer. |
| [`ui/src/renderer/`](../../ui/src/renderer) | The app itself: `pages/`, `components/`, `hooks/`, `services/`, `styles/`, `utils/`, `assets/`, `main.tsx`, `index.html`, `types.d.ts`. |
| [`ui/src/common/utils/shims/`](../../ui/src/common/utils/shims) | Stubs for renderer-safe compatibility paths and build-time aliases. |
| [`ui/public/`](../../ui/public) | Static assets copied straight to `ui/dist/` (icons, etc.). |
| `ui/vite.config.ts` | Vite config, including the externalized-shim aliases. |
| `ui/uno.config.ts` | UnoCSS preset config. |
| `ui/tsconfig.json` | TypeScript paths and aliases that match the directory shape above. |
## Other references
| Path | Contents |
| --- | --- |
| [`STATUS.md`](../../STATUS.md) | Current technical status snapshot. |
| [`apps/desktop/updater/README.md`](../../apps/desktop/updater/README.md) | Auto-update scaffold and release-key notes. |
| [`packaging/linux/README.md`](../../packaging/linux) | Headless Linux deployment: Docker (recommended), or native binary + systemd unit. |
## Where artifacts go
| Build | Output |
| --- | --- |
| `bun run build:ui` | `ui/dist/` (the SPA) |
| `cargo build -p nomifun-web` | `target/<profile>/nomifun-web` |
| `cargo build -p nomifun-app --bin nomicore` | `target/<profile>/nomicore` |
| `bun run build` | `target/<profile>/bundle/<format>/...` (per-OS Tauri bundles) |
| `docker compose build` | local image `nomifun-web:local` |
`target/`, `ui/dist/`, `data/`, and `node_modules/` are all gitignored. See
[`building-and-packaging.md`](building-and-packaging.md) for the per-output
details.
@@ -0,0 +1,89 @@
# 项目结构
这是 **NomiFun** 的当前仓库地图。后端分层详见
[`../architecture/backend-crates.md`](../architecture/backend-crates.md),运行时总览见
[`../architecture/overview.md`](../architecture/overview.md)。
## 顶层布局
```text
nomifun-tauri/
├── apps/
│ ├── web/ nomifun-web:独立 Web/API host
│ └── desktop/ nomifun-desktopTauri 桌面壳
├── crates/
│ ├── agent/ 15 个 nomi-* crateAI agent 引擎
│ ├── backend/ 29 个 nomifun-* crateHTTP/WS 后端
│ └── shared/ 2 个真正跨层共享 crate
├── ui/ React SPAVite + UnoCSS,唯一 Bun workspace
├── docs/ 当前文档、历史设计、审计与归档说明
├── packaging/linux/ nomifun-web systemd unit 与部署说明
├── Cargo.toml Rust workspace
├── package.json Bun/Tauri/Cargo 入口脚本
├── Dockerfile nomifun-web 容器镜像
├── docker-compose.yml Web host compose 示例
├── Caddyfile 可选 TLS reverse proxy
├── README.md
└── STATUS.md
```
Cargo workspace 当前成员:
```toml
members = ["crates/agent/*", "crates/backend/*", "crates/shared/*", "apps/web", "apps/desktop"]
```
## App Host
| 路径 | Binary | 职责 |
| --- | --- | --- |
| [`apps/web`](../../apps/web) | `nomifun-web` | 独立 Web/API host。进程内启动 `nomifun-app`,同端口提供 API、WebSocket 和 SPA。默认开启登录;`--insecure-no-auth` 仅供可信本地开发。 |
| [`apps/desktop`](../../apps/desktop) | `nomifun-desktop` | Tauri 桌面壳。进程内启动同一个后端,选择空闲 localhost 端口,注入 `window.__backendPort``window.__nomiLocalTrust`WebView 通过本地信任 token 访问后端。 |
两个 host 都直接链接 `nomifun-app``nomicore` 仍作为 `nomifun-app` 的独立
binary 存在,用于诊断、stdio MCP bridge、公开能力调用和无头场景;桌面/Web host
不会 spawn 它。
## Crate 分组
| 目录 | 前缀 | 数量 | 职责 |
| --- | --- | --- | --- |
| [`crates/agent/`](../../crates/agent) | `nomi-*` | 15 | AI agent 引擎,尽量保持独立。 |
| [`crates/backend/`](../../crates/backend) | `nomifun-*` | 29 | HTTP/WS 后端、数据层、认证、会话、cron、knowledge、terminal、companion、public gateway 等。 |
| [`crates/shared/`](../../crates/shared) | mixed | 2 | 真正跨 agent/backend 使用的共享工具。 |
## Agent 层接缝
后端代码需要 agent 类型或执行能力时,默认应通过
[`crates/backend/nomifun-ai-agent`](../../crates/backend/nomifun-ai-agent)。
它再导出常用的 `nomi_config``nomi_types``RequirementSink`
当前 workspace 仍存在少数 feature-gated 直接依赖例外:`nomifun-app`
`nomifun-gateway` 为 browser/computer-use bridge 工具直接触达部分 `nomi-*`
crate。新增后端 crate 时不要随手添加 `nomi-*` 依赖;若确实是 bridge/facade
例外,需要用 feature gate 并在架构文档中说明。
## 关键目录
| 路径 | 内容 |
| --- | --- |
| [`ui/src/common/`](../../ui/src/common) | 跨 host 的 API client、类型、adapter、工具函数。 |
| [`ui/src/platform/`](../../ui/src/platform) | host bridgestorage、logger、theme、平台能力。 |
| [`ui/src/renderer/`](../../ui/src/renderer) | 页面、组件、hooks、服务、样式和 renderer 入口。 |
| [`ui/src/common/utils/shims/`](../../ui/src/common/utils/shims) | renderer-safe 兼容 shim 与构建别名目标。 |
| [`docs/getting-started/`](../getting-started) | 安装与首次运行。 |
| [`docs/guides/`](../guides) | 用户任务指南。 |
| [`docs/architecture/`](../architecture) | 当前架构说明。 |
| [`docs/reference/`](../reference) | 配置、API、FAQ、troubleshooting。 |
## 制品位置
| 构建 | 输出 |
| --- | --- |
| `bun run build:ui` | `ui/dist/` |
| `cargo build -p nomifun-web` | `target/<profile>/nomifun-web` |
| `cargo build -p nomifun-app --bin nomicore` | `target/<profile>/nomicore` |
| `bun run build` | `target/<profile>/bundle/<format>/...` |
| `docker compose build` | 本地镜像 `nomifun-web:local` |
更多打包细节见 [`building-and-packaging.zh.md`](building-and-packaging.zh.md)。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,295 @@
# Installation
NomiFun has two host modes that share one Rust backend (see
[Introduction](introduction.md)). This page covers all three ways to install
it today:
- [Desktop app from source](#desktop-app-from-source) — `nomifun-desktop`
(Tauri shell), desktop local-trust, single-user.
- [Web server from source](#web-server-from-source) — `nomifun-web`,
authenticated, self-hosted.
- [Docker / Docker Compose](#docker--docker-compose) — the same web server,
containerised.
> **Official pre-built installers are not yet published.** Desktop bundles,
> macOS signing, updater artifacts, Docker, and native Linux service files can
> be built locally; there is not yet an official public release channel. Until
> then, every install path below builds from source. See
> [`../contributing/building-and-packaging.md`](../contributing/building-and-packaging.md)
> for the current packaging notes.
## Prerequisites
You need a working build toolchain regardless of which mode you target. The
exact requirements:
| Tool | Minimum | Why | Notes |
| --- | --- | --- | --- |
| **Rust** | stable, edition 2024 | Compile the backend (and the Tauri shell, for desktop). | Install via [`rustup`](https://rustup.rs/). The workspace pins `edition = "2024"` and `resolver = "3"`. |
| **Bun** | **≥ 1.3.13** | Frontend package manager + build (and a hard runtime dependency of the agent engine). | `1.1.38` has a stdin bug — do not use it. |
| **Tauri CLI** | v2 | Build the desktop shell. | Pulled in as a `devDependency`; no global install needed. |
| **Git** | any recent | Clone, plus skill discovery and some built-in tools. | |
| **C/C++ build tools** | platform-specific | `rusqlite` (bundled), `aws-lc-rs`, `libgit2-sys`. | Windows: MSVC + WebView2 runtime. macOS: Xcode CLT. Linux: `build-essential cmake clang pkg-config perl`. |
Optional but recommended on the host that runs Nomi (not for building):
- **`ripgrep`** — code-search backend; falls back to `grep` if missing.
- **`node` / `npm` / `npx`** — many user-installed MCP stdio servers launch
via `npx -y …`.
### Clone the repo
```bash
git clone <your-fork-or-mirror>/nomifun-tauri.git
cd nomifun-tauri
```
The rest of this page assumes the repository root is your working directory.
### Install JS dependencies
```bash
bun install
```
This populates `node_modules/` for the workspace and `ui/`. Re-run it any
time `package.json` or `ui/package.json` changes.
## Desktop app from source
The desktop app is a Tauri 2 shell (`apps/desktop`, binary
`nomifun-desktop`) that links the backend in-process and starts it on a free
localhost port under the desktop `TrustLocalToken` policy. Its own WebView
receives a per-boot local trust secret, so there is no login screen in the
desktop window.
### Run in development
```bash
bun run dev
```
What this does, end-to-end:
1. Tauri's `beforeDevCommand` runs `bun run --filter=./ui dev` to start the
Vite dev server on `http://localhost:5173`.
2. `cargo` compiles `nomifun-desktop` (and the workspace it depends on).
3. The shell starts, picks a free port, spawns the embedded backend, and
loads the Vite dev URL. Hot-reload works on the renderer side; the backend
restarts only when its Rust code changes.
You will see a tracing line like `Server listening on 127.0.0.1:54760` in the
console — that is the embedded backend. The renderer reads
`window.__backendPort` (injected by the Tauri shell as an init script) so the
SPA always knows where to call `/api`.
![nomifun-desktop running in dev with the embedded backend](../images/gs-02-desktop-dev.png)
### Build a release binary
```bash
bun run build:ui # build the SPA into ui/dist
bun run build # tauri build → installers + standalone binary
```
`tauri build` produces:
- A standalone executable under
`target/release/nomifun-desktop` (`.exe` on Windows).
- Platform installers under `target/release/bundle/``.msi`/`.exe`
(Windows), `.dmg`/`.app` (macOS), `.deb`/`.AppImage` (Linux).
`bun run build` artifacts are suitable for local testing. For distributable
macOS builds, configure `apps/desktop/signing/.env.signing` and use
`bun run build:signed`. Windows signing still requires an external certificate.
To test the updater scaffold, use `bun run build:updater`, which sets
`bundle.createUpdaterArtifacts` to true. The updater endpoint and public key in
`apps/desktop/tauri.conf.json` must be replaced before shipping any update.
### Where data lives (desktop)
The desktop app stores its database and runtime files under the per-user
application-data directory, joined with `Nomi`:
| OS | Default path |
| --- | --- |
| Windows | `%LOCALAPPDATA%\NomiFun\Nomi` (e.g. `C:\Users\<you>\AppData\Local\NomiFun\Nomi`) |
| macOS | `~/Library/Application Support/NomiFun/Nomi` |
| Linux | `$XDG_DATA_HOME/NomiFun/Nomi` (usually `~/.local/share/NomiFun/Nomi`) |
Override with `NOMIFUN_DATA_DIR=<absolute path>` before launching — the
shell appends `/Nomi`, so the dir becomes `$NOMIFUN_DATA_DIR/Nomi`.
> Older builds defaulted to `<system temp>/nomifun-data/Nomi`, where OS temp
> cleanup could destroy user data. On first launch the app now relocates such
> a legacy install to the per-user location automatically (one-shot): data is
> copied, absolute paths inside the database are rewritten, and the old
> directory is kept as a backup. If the relocation cannot complete, the app
> starts from the legacy directory and retries on the next launch.
> Note: the app's user-facing name is `NomiFun` everywhere — the bundle
> product name (`apps/desktop/tauri.conf.json`), the runtime window title,
> and release artifacts. The data folder keeps its existing `/Nomi`
> suffix for compatibility with current installs. Internal identifiers keep the legacy `nomifun`
> name by design (crates, `NOMIFUN_*` env vars, the `com.nomifun.*`
> bundle identifier).
## Web server from source
`nomifun-web` is an axum server that mounts the same backend in-process
**and** serves the built SPA on the same port (default `8787`). It is the
right path for self-hosting on a LAN, VPN, or VPS.
### Build and run
```bash
bun install
bun run build:ui # ui/dist — required before serving in non-dev mode
bun run serve:web # = cargo run -p nomifun-web
```
By default the server binds `127.0.0.1:8787` and uses the same per-user
data directory as the desktop app (see
[Where data lives (desktop)](#where-data-lives-desktop)):
```text
nomifun-web: embedded backend + SPA on one port
listening on 127.0.0.1:8787 auth=required dist=../../ui/dist
```
On a machine that also has the desktop app installed, a bare `nomifun-web`
run opens the desktop app's data directly — an exclusive `server.lock`
guarantees the two backends never run on that directory at the same time.
Open `http://127.0.0.1:8787` in a browser. On the very first visit you will
be sent to a setup screen — the username and password you type **become the
initial admin account**. After that, login is required for everyone.
![First-run admin setup in the browser](../images/gs-03-web-first-run-setup.png)
### Common flags
`nomifun-web` (defined in `apps/web/src/main.rs`) accepts both CLI flags and
environment variables:
| Flag | Env var | Default | Meaning |
| --- | --- | --- | --- |
| `--host` | `NOMIFUN_WEB_HOST` | `127.0.0.1` | Bind address. Use `0.0.0.0` only when you intend LAN/VPN/public access; pre-seed or complete admin setup first. |
| `--port` | `NOMIFUN_WEB_PORT` | `8787` | Port for both `/api` and the SPA. |
| `--data-dir` | `NOMIFUN_DATA_DIR` | _per-user app-data dir, same as the [desktop default](#where-data-lives-desktop)_ | Backend data dir (db / logs / bun cache / agent state). The env value is taken literally (no `/Nomi` suffix). Use an absolute path in production. |
| `--dist` | `NOMIFUN_WEB_DIST` | `../../ui/dist` | SPA static directory. **Set this explicitly when running outside the repo root.** |
| `--admin-user` | `NOMIFUN_ADMIN_USERNAME` | `admin` | Username for pre-seeded admin (only honoured before the admin exists). |
| `--admin-password` | `NOMIFUN_ADMIN_PASSWORD` | _(none — interactive first-run setup)_ | Pre-seed the admin password and skip interactive first-run. |
| `--insecure-no-auth` | `NOMIFUN_WEB_INSECURE_NO_AUTH` | `false` | **Danger.** Disable authentication entirely (desktop-style local mode). Loopback / trusted private network only. |
| _(env only)_ | `NOMIFUN_HTTPS` | `false` | Set to `true` when fronted by TLS so cookies get the `Secure` flag. |
Example, opening it up to the LAN with a pre-seeded admin:
```bash
nomifun-web \
--host 0.0.0.0 --port 8787 \
--data-dir /var/lib/nomifun \
--dist /opt/nomifun/web \
--admin-user admin \
--admin-password "change-me-to-something-strong"
```
For full deployment guidance — systemd unit, reverse-proxy, and security
notes — see
[`../guides/web-server-deployment.md`](../guides/web-server-deployment.md).
## Docker / Docker Compose
The repository ships a multi-stage `Dockerfile` and a `docker-compose.yml`
that produce a **headless** (no GUI) image: SPA + `nomifun-web` + `bun` on
`debian:bookworm-slim`.
### Quick start with Compose
From the repo root:
```bash
docker compose up -d --build
# then open http://<server-ip>:8787
```
The service is configured with `restart: unless-stopped` so installing it
**is** enabling it on boot. Persistent state (SQLite database, logs, bun
cache, agent state) lives in the named volume `nomifun-data` mounted at
`/data` inside the container.
The image's defaults are tuned for container life:
```text
NOMIFUN_WEB_HOST=0.0.0.0
NOMIFUN_WEB_PORT=8787
NOMIFUN_DATA_DIR=/data
NOMIFUN_WEB_DIST=/opt/nomifun/web
SHELL=/bin/bash
```
Authentication is on, but first-run setup can be claimed by the first browser
that reaches the service. Pre-seed the admin or complete setup on a trusted
network before publishing port `8787` broadly. For anything reachable from the
internet, put TLS in front of it — the bundled `Caddyfile` and the
commented-out `caddy` service in `docker-compose.yml` are the recommended path.
Set
`NOMIFUN_HTTPS=true` on the `nomifun` service when you do, so the session
cookie gains the `Secure` flag.
### Pre-seed the admin (recommended for non-interactive setup)
The first browser visit otherwise wins the admin account; pre-seeding closes
that race window:
```yaml
# docker-compose.yml — under services.nomifun
environment:
NOMIFUN_ADMIN_USERNAME: admin
NOMIFUN_ADMIN_PASSWORD: "change-me-to-something-strong"
NOMIFUN_HTTPS: "true" # only when behind a TLS proxy
```
### Speeding up Rust builds
The Rust stage uses BuildKit cache mounts (`/usr/local/cargo/registry` and
`/src/target`), so a one-line source change recompiles in seconds. To use a
mirror for the cargo registry (e.g. on slow links):
```bash
docker build --build-arg CARGO_REGISTRY_MIRROR=https://rsproxy.cn/index/ .
```
For the long-form deployment guide (TLS, reverse-proxy patterns, systemd
unit, security caveats) see
[`../guides/web-server-deployment.md`](../guides/web-server-deployment.md).
## Verifying your install
A 30-second smoke test you can run after either path:
```bash
# Rust workspace compiles cleanly
cargo check --workspace
# All three binaries build
cargo build --workspace --bins
# → target/(debug|release)/{nomicore, nomifun-web, nomifun-desktop}
# Web host responds with the SPA + auth status
curl -sS http://127.0.0.1:8787/ | head -c 200
curl -sS http://127.0.0.1:8787/api/auth/status
# → 200 {"success":true,"needs_setup":..., "user_count":...}
```
If you see `nomifun-web: embedded backend + SPA on one port` in the logs and
`/api/auth/status` returns JSON, the backend is up and the SPA is being
served from the same port.
## What's next
- [Quick Start](quick-start.md) — your first conversation in Nomi.
- [`../guides/web-server-deployment.md`](../guides/web-server-deployment.md)
— production hardening for the web host.
- [`../contributing/development.md`](../contributing/development.md)
— set up a developer loop (renderer hot-reload, backend rebuild, debug tools).
@@ -0,0 +1,275 @@
# 安装
NomiFun 有两种宿主模式,共享同一个 Rust 后端(参见
[简介](introduction.zh.md))。本页覆盖目前可行的全部三种安装方式:
- [从源码构建桌面应用](#从源码构建桌面应用) —— `nomifun-desktop`
(Tauri 外壳),桌面本地信任,单用户。
- [从源码构建 Web 服务](#从源码构建-web-服务) —— `nomifun-web`
带鉴权,自托管。
- [Docker / Docker Compose](#docker--docker-compose) —— 同一个 Web 服务
的容器化方案。
> **官方预构建安装包尚未发布。** 桌面包、macOS 签名、updater 产物、Docker
> 和 native Linux service 都可以本地构建;但还没有官方公开发布渠道。下面所有
> 安装路径都需要从源码构建。当前打包说明见
> [`../contributing/building-and-packaging.zh.md`](../contributing/building-and-packaging.zh.md)。
## 前置条件
无论你选择哪种模式,都需要一套可工作的构建工具链。具体要求如下:
| 工具 | 最低版本 | 用途 | 备注 |
| --- | --- | --- | --- |
| **Rust** | stableedition 2024 | 编译后端(桌面端还需编译 Tauri 外壳)。 | 通过 [`rustup`](https://rustup.rs/) 安装。工作区固定使用 `edition = "2024"``resolver = "3"`。 |
| **Bun** | **≥ 1.3.13** | 前端包管理器与构建(同时也是智能体引擎的硬运行时依赖)。 | `1.1.38` 存在 stdin 缺陷——请勿使用。 |
| **Tauri CLI** | v2 | 构建桌面外壳。 | 作为 `devDependency` 引入;无需全局安装。 |
| **Git** | 任何近期版本 | 克隆仓库,以及技能发现与若干内置工具会用到。 | |
| **C/C++ 构建工具** | 因平台而异 | `rusqlite`bundled)、`aws-lc-rs``libgit2-sys` 需要。 | WindowsMSVC + WebView2 运行时。macOSXcode CLT。Linux`build-essential cmake clang pkg-config perl`。 |
在运行 NomiFun 的宿主上推荐安装(构建机不需要):
- **`ripgrep`** —— 代码搜索后端;不可用时回退到 `grep`
- **`node` / `npm` / `npx`** —— 许多用户安装的 MCP stdio 服务通过
`npx -y …` 启动。
### 克隆仓库
```bash
git clone <your-fork-or-mirror>/nomifun-tauri.git
cd nomifun-tauri
```
本页其余内容均假设你的工作目录为仓库根目录。
### 安装 JS 依赖
```bash
bun install
```
这会为整个工作区与 `ui/` 填充 `node_modules/`。每当 `package.json`
`ui/package.json` 发生改变时都需重新执行。
## 从源码构建桌面应用
桌面应用是一个 Tauri 2 外壳(`apps/desktop`,二进制
`nomifun-desktop`),它在进程内链接后端,并在一个空闲 localhost 端口上以
桌面 `TrustLocalToken` 策略启动后端。自己的 WebView 会收到每次启动生成的
本地信任 secret,因此桌面窗口没有登录界面。
### 在开发模式下运行
```bash
bun run dev
```
完整流程如下:
1. Tauri 的 `beforeDevCommand` 执行 `bun run --filter=./ui dev`,启动
Vite 开发服务器在 `http://localhost:5173`
2. `cargo` 编译 `nomifun-desktop`(以及它依赖的整个工作区)。
3. 外壳启动,挑选一个空闲端口,派生嵌入式后端,并加载 Vite 开发地址。
渲染端支持热重载;后端只在其 Rust 代码变更时才会重启。
你会在控制台中看到类似 `Server listening on 127.0.0.1:54760` 的 tracing
日志——这就是嵌入式后端。渲染端会读取 `window.__backendPort`(由 Tauri
外壳作为 init script 注入),因此 SPA 始终知道 `/api` 该往哪里调用。
![以开发模式运行的 nomifun-desktop 与嵌入式后端](../images/gs-02-desktop-dev.png)
### 构建 Release 二进制
```bash
bun run build:ui # 将 SPA 构建到 ui/dist
bun run build # tauri build → 安装包 + 独立二进制
```
`tauri build` 会产出:
- 位于 `target/release/nomifun-desktop`Windows 上为 `.exe`)的独立可
执行文件。
- 位于 `target/release/bundle/` 下的平台安装包——`.msi`/`.exe`
Windows)、`.dmg`/`.app`macOS)、`.deb`/`.AppImage`Linux)。
`bun run build` 产物适合本地测试。要分发 macOS 构建,请配置
`apps/desktop/signing/.env.signing` 并使用 `bun run build:signed`。Windows
签名仍需要外部证书。若要测试 updater 骨架,可使用 `bun run build:updater`
它会把 `bundle.createUpdaterArtifacts` 设为 true。发布任何更新前,必须替换
`apps/desktop/tauri.conf.json` 中的 updater endpoint 与公钥。
### 数据存放位置(桌面端)
桌面应用把数据库与运行时文件存放在按用户区分的应用数据目录下,
再拼接 `Nomi`
| 操作系统 | 默认路径 |
| --- | --- |
| Windows | `%LOCALAPPDATA%\NomiFun\Nomi`(例如 `C:\Users\<you>\AppData\Local\NomiFun\Nomi` |
| macOS | `~/Library/Application Support/NomiFun/Nomi` |
| Linux | `$XDG_DATA_HOME/NomiFun/Nomi`(通常为 `~/.local/share/NomiFun/Nomi` |
启动前可通过 `NOMIFUN_DATA_DIR=<absolute path>` 覆盖——外壳会附加
`/Nomi`,因此目录会变成 `$NOMIFUN_DATA_DIR/Nomi`
> 旧版本默认使用 `<system temp>/nomifun-data/Nomi`,操作系统的临时目录
> 清理可能在那里销毁用户数据。现在应用首次启动时会自动把这类旧安装
> 搬迁到按用户区分的位置(一次性):复制数据、改写数据库内的绝对路径,
> 并把旧目录保留作备份。若搬迁无法完成,应用会先从旧目录启动,并在
> 下次启动时重试。
> 提示:应用对用户呈现的名称统一为 `Nomi`——bundle 产品名
> `apps/desktop/tauri.conf.json`)、运行时窗口标题、数据文件夹都用
> `Nomi`。内部标识符按设计仍保留旧的 `nomifun` 名(crate、`NOMIFUN_*`
> 环境变量、`com.nomifun.*` bundle 标识符)。
## 从源码构建 Web 服务
`nomifun-web` 是一个 axum 服务,**既**在进程内挂载同一个后端,**又**在
同一个端口(默认 `8787`)上提供已构建的 SPA。它适合在 LAN、VPN 或
VPS 上自托管。
### 构建并运行
```bash
bun install
bun run build:ui # ui/dist —— 在非开发模式下提供服务前必须先构建
bun run serve:web # = cargo run -p nomifun-web
```
默认情况下,服务绑定在 `127.0.0.1:8787`,并使用与桌面应用相同的按用户
数据目录(参见[数据存放位置(桌面端)](#数据存放位置桌面端)):
```text
nomifun-web: embedded backend + SPA on one port
listening on 127.0.0.1:8787 auth=required dist=../../ui/dist
```
在同时装有桌面应用的机器上,直接裸跑 `nomifun-web` 会打开桌面应用的
数据——排他的 `server.lock` 保证两个后端不会同时运行在该目录上。
在浏览器中打开 `http://127.0.0.1:8787`。首次访问会被引导到设置页——
你输入的用户名与密码**将成为初始管理员账户**。此后所有人都需要登录。
![浏览器中的首次启动管理员设置](../images/gs-03-web-first-run-setup.png)
### 常用参数
`nomifun-web`(定义在 `apps/web/src/main.rs` 中)同时接受 CLI 参数与
环境变量:
| 参数 | 环境变量 | 默认值 | 含义 |
| --- | --- | --- | --- |
| `--host` | `NOMIFUN_WEB_HOST` | `127.0.0.1` | 绑定地址。仅在确实需要 LAN/VPN/公网访问时使用 `0.0.0.0`;请先预置或完成管理员设置。 |
| `--port` | `NOMIFUN_WEB_PORT` | `8787` | `/api` 与 SPA 共用的端口。 |
| `--data-dir` | `NOMIFUN_DATA_DIR` | _按用户应用数据目录,与[桌面端默认](#数据存放位置桌面端)相同_ | 后端数据目录(数据库 / 日志 / bun 缓存 / 智能体状态)。环境变量取字面值(不附加 `/Nomi`)。生产环境请使用绝对路径。 |
| `--dist` | `NOMIFUN_WEB_DIST` | `../../ui/dist` | SPA 静态目录。**在仓库根目录之外运行时务必显式指定。** |
| `--admin-user` | `NOMIFUN_ADMIN_USERNAME` | `admin` | 预置管理员的用户名(仅在管理员尚未存在时生效)。 |
| `--admin-password` | `NOMIFUN_ADMIN_PASSWORD` | _(无 —— 交互式首次设置)_ | 预置管理员密码并跳过交互式首次启动。 |
| `--insecure-no-auth` | `NOMIFUN_WEB_INSECURE_NO_AUTH` | `false` | **危险。** 完全禁用鉴权(桌面式本地模式)。仅限 loopback / 受信任的私有网络。 |
| _(仅环境变量)_ | `NOMIFUN_HTTPS` | `false` | 当前面有 TLS 终止时设为 `true`,使 cookie 获得 `Secure` 标记。 |
例:将其开放到 LAN,并预置管理员:
```bash
nomifun-web \
--host 0.0.0.0 --port 8787 \
--data-dir /var/lib/nomifun \
--dist /opt/nomifun/web \
--admin-user admin \
--admin-password "change-me-to-something-strong"
```
完整的部署指南——systemd unit、反向代理与安全注意事项——请参见
[`../guides/web-server-deployment.md`](../guides/web-server-deployment.md)。
## Docker / Docker Compose
仓库附带一份多阶段 `Dockerfile` 与一份 `docker-compose.yml`,会构建出
一个**无 GUI**镜像:在 `debian:bookworm-slim` 上的 SPA + `nomifun-web` +
`bun`
### 用 Compose 快速上手
在仓库根目录:
```bash
docker compose up -d --build
# 然后访问 http://<server-ip>:8787
```
服务配置了 `restart: unless-stopped`,所以**安装即等同于开机自启**。
持久化状态(SQLite 数据库、日志、bun 缓存、智能体状态)存放在挂载到容器
`/data` 的命名卷 `nomifun-data` 中。
镜像默认值已针对容器生命周期进行了调优:
```text
NOMIFUN_WEB_HOST=0.0.0.0
NOMIFUN_WEB_PORT=8787
NOMIFUN_DATA_DIR=/data
NOMIFUN_WEB_DIST=/opt/nomifun/web
SHELL=/bin/bash
```
鉴权已开启,但首次设置可被第一个访问到服务的浏览器认领。请先预置
管理员或在受信网络完成 setup,再大范围发布 `8787`。对于公网可达的部署,
请在前面加一层 TLS——附带的 `Caddyfile``docker-compose.yml` 中被注释掉的
`caddy` 服务即是推荐做法。届时记得把 `nomifun` 服务的 `NOMIFUN_HTTPS=true`
打开,让会话 cookie 获得 `Secure` 标记。
### 预置管理员(推荐用于非交互式部署)
否则首次访问浏览器的人会获得管理员账号;预置管理员可以关闭这个竞态
窗口:
```yaml
# docker-compose.yml —— 在 services.nomifun 下
environment:
NOMIFUN_ADMIN_USERNAME: admin
NOMIFUN_ADMIN_PASSWORD: "change-me-to-something-strong"
NOMIFUN_HTTPS: "true" # 仅当部署在 TLS 反向代理之后时
```
### 加速 Rust 构建
Rust 阶段使用了 BuildKit cache mounts`/usr/local/cargo/registry`
`/src/target`),所以一行源代码改动只需几秒就能重新编译。如需为 cargo
注册表配置镜像(例如在网络较慢时):
```bash
docker build --build-arg CARGO_REGISTRY_MIRROR=https://rsproxy.cn/index/ .
```
完整的部署指南(TLS、反向代理模式、systemd unit、安全注意事项)请参见
[`../guides/web-server-deployment.md`](../guides/web-server-deployment.md)。
## 验证你的安装
无论走哪条路径,都可以做一次 30 秒的快速检验:
```bash
# Rust 工作区编译干净
cargo check --workspace
# 三个二进制都能构建
cargo build --workspace --bins
# → target/(debug|release)/{nomicore, nomifun-web, nomifun-desktop}
# Web 主机响应 SPA + 鉴权状态
curl -sS http://127.0.0.1:8787/ | head -c 200
curl -sS http://127.0.0.1:8787/api/auth/status
# → 200 {"success":true,"needs_setup":..., "user_count":...}
```
如果你在日志中看到 `nomifun-web: embedded backend + SPA on one port`
并且 `/api/auth/status` 返回了 JSON,则后端已启动,并且 SPA 在同一个
端口上提供服务。
## 接下来
- [快速上手](quick-start.zh.md) —— 你在 NomiFun 中的第一段会话。
- [`../guides/web-server-deployment.md`](../guides/web-server-deployment.md)
—— Web 主机的生产环境加固。
- [`../contributing/development.zh.md`](../contributing/development.zh.md)
—— 搭建开发环路(渲染端热重载、后端重建、调试工具)。
@@ -0,0 +1,85 @@
# Introduction
**NomiFun** is an open-source AI workstation and coding workspace. It unifies
multiple AI runtimes, LLM providers, MCP servers, skills, terminals, knowledge
bases, scheduled work, and companion/remote capability surfaces in one local-first
application.
> Ready to run it? Start with [Installation](installation.md), then
> [Quick Start](quick-start.md). For the full documentation map, see
> [docs/README.md](../README.md).
![NomiFun guide / landing page](../images/gs-01-introduction-hero.png)
## What NomiFun Solves
Modern AI workflows are scattered across separate CLIs, terminals, browser
tabs, MCP servers, and local scripts. NomiFun pulls them into one workspace:
- **Many agents, one surface.** Use the built-in Nomi engine or external
ACP-style CLIs such as Claude Code, Codex, Gemini CLI, Qwen, and OpenCode.
- **One workspace per conversation.** Conversations can own files, previews,
diffs, terminals, and knowledge bindings instead of living as isolated chat
transcripts.
- **Backend-driven automation.** Scheduled tasks, AutoWork requirements,
terminal sessions, channel integrations, and completion notifications are
durable backend services, not foreground browser-tab state.
- **Extensible capability layer.** MCP servers, skills, assistants, browser use,
computer use, and public remote capability fronts can be composed per runtime.
- **Local-first deployment.** Run it as a Tauri desktop app or a self-hosted web
server. You provide the model/API credentials and decide where the data lives.
NomiFun is not a no-code SaaS chat product. It is infrastructure for users who
are comfortable configuring agents, providers, local tools, and self-hosted
services.
## Two Hosts, One Backend
Both hosts run the same Rust backend (`nomifun-app`) in-process and load the
same React SPA (`ui/dist`).
| Mode | Binary | Auth model | Typical use |
| --- | --- | --- | --- |
| Desktop app | `nomifun-desktop` | Per-boot local trust token injected into the desktop webview | Personal workstation |
| Web server | `nomifun-web` | Login required by default; first-run setup or pre-seeded admin | Browser / LAN / server deployment |
```text
nomifun-desktop
Tauri shell -> embedded backend on 127.0.0.1:<ephemeral> -> same SPA
nomifun-web
axum server -> /api + /ws + static ui/dist on one port (default 8787)
```
For implementation details, see [Architecture Overview](../architecture/overview.md).
## Main Surfaces
- **Home & conversations** (`/guid`): start and continue AI sessions.
- **Terminals**: PTY-backed agent or shell sessions inside the app.
- **Models**: providers, local agent detection, global IDMM/failover settings.
- **Assistant & Skill**: assistant personas and skill management.
- **MCP**: local MCP server configuration.
- **Open Capabilities**: WebUI remote access, remote MCP, and REST capability
exposure.
- **Requirements / AutoWork**: backend-owned queue processing and completion
notifications.
- **Scheduled tasks**: recurring or one-shot jobs.
- **Desktop Companion** (`/nomi`): companion configuration, memory, and remote binding.
- **Knowledge**: local knowledge-base management and session bindings.
The current frontend route source is
`ui/src/renderer/components/layout/Router.tsx`.
## Project Status
NomiFun is in active development. [STATUS.md](../../STATUS.md) is the compact
current-state snapshot. Design and audit history is not kept in the repo; consult
git history for past decisions.
## Acknowledgments
NomiFun began as a fork of the open-source
[AionUi](https://github.com/iOfficeAI/AionUi) project and has since been
substantially refactored around a Tauri + Rust architecture. NomiFun is released
under the Apache-2.0 License.
@@ -0,0 +1,73 @@
# 简介
**NomiFun** 是一个面向 AI agent 工作流的本地优先工作台。它把多种
CLI agent、内置 Nomi 引擎、模型提供商、MCP 服务、技能、终端、计划任务
和远程 WebUI 收拢到同一个 Rust + Tauri monorepo 中。
> 想立刻开始?请先读 [安装](installation.zh.md),再读
> [快速上手](quick-start.zh.md)。完整文档地图见 [`../README.zh.md`](../README.zh.md)。
![NomiFun 引导页](../images/gs-01-introduction-hero.png)
## 它解决什么问题
真实的 AI 工作流经常被拆散在多个地方:一个终端跑 Claude Code,一个终端跑
Codex,浏览器里开着自托管页面,旁边还有单独的 MCP 服务和项目脚本。
NomiFun 的目标不是再做一个聊天框,而是把这些运行时接到同一个工作区:
- **一个会话入口,多种 agent。** 会话可以选择内置 Nomi、Claude Code、
Codex、Gemini、Qwen、OpenCode、CodeBuddy 等后端。
- **一个模型目录,多处复用。** 在 `/models` 配好 Anthropic、OpenAI、
Bedrock、Vertex 或兼容接口后,支持这些模型的 agent 和助手可以复用。
- **一个工作区,不只是消息流。** 会话有工作目录、文件树、预览面板和后端
管理的 PTY 终端。
- **后端驱动的自动化。** 计划任务、AutoWork、IDMM、WebUI 远程访问、
MCP 暴露和频道能力都由 Rust 后端持久化管理。
- **桌面与 Web 共用后端。** Tauri 桌面端和 `nomifun-web` 自托管服务使用
同一套 `nomifun-app` 后端与同一份 React SPA。
NomiFun 更适合已经在用 agent 做真实工作的用户。它要求你理解 API key、
本地数据目录、CLI agent 安装和自托管边界;它不是零配置的 SaaS 聊天产品。
## 两种运行方式
| 模式 | 二进制 | 鉴权模型 | 典型用途 |
| --- | --- | --- | --- |
| 桌面应用 | `nomifun-desktop` | 桌面外壳使用本地信任 token 访问嵌入式后端;远程浏览器仍需登录 | 单机工作站、日常开发 |
| Web 服务 | `nomifun-web` | 默认开启登录;首次访问创建管理员 | LAN/VPN/VPS 自托管 |
桌面模式会在进程内启动 `nomifun-app`,监听一个随机 localhost 端口,并通过
每次启动生成的本地信任 token 让 WebView 免登录访问。WebUI 远程访问打开后,
额外的 LAN 监听器仍然要求远程浏览器登录。
`nomifun-web` 则在一个端口上同时提供 SPA 与 API,默认端口是 `8787`。Docker
和 systemd 部署都走这条路径。
## 当前功能地图
- **会话与工作区**`/guid` 创建会话,`/conversation/:id` 运行会话。
- **模型配置**`/models` 管理提供商、模型、凭据和全局故障转移队列。
- **助手与技能**`/assistants` 管理助手;其中 `tab=skills` 管理技能。
- **MCP**`/mcp` 管理 MCP server、连接测试、OAuth 和 agent 配置同步。
- **开放能力**`/open-capabilities` 管理 WebUI 远程访问、MCP/API 暴露等外部入口。
- **桌面伙伴**`/nomi` 管理伙伴、远程频道绑定和 companion 相关设置。
- **终端**`/terminal-new` 创建、`/terminal/:id` 运行后端 PTY。
- **计划任务**`/scheduled` 管理 cron 触发的会话任务。
- **AutoWork**`/requirements` 管理需求看板和自动执行。
更多内部结构见 [`../architecture/`](../architecture/),用户指南见
[`../guides/`](../guides/)。
## 项目状态
NomiFun 仍在活跃开发中,但已经不是旧的 Electron 多仓迁移状态。当前仓库是
Rust workspace + Tauri desktop + Web host 的单仓结构。顶层
[`../../STATUS.md`](../../STATUS.md) 记录当前状态;历史设计稿与审计记录不在
仓库中保留,需要时请查阅 git 历史。
## 接下来
- [安装](installation.zh.md)
- [快速上手](quick-start.zh.md)
- [开发环境](../contributing/development.zh.md)
- [Web 服务部署](../guides/web-server-deployment.zh.md)
@@ -0,0 +1,82 @@
# Quick Start
A short walkthrough for the first useful NomiFun session. This assumes the repo
is already installed; otherwise start with [Installation](installation.md).
## 1. Launch
Desktop development:
```bash
bun run dev
```
The desktop shell starts the backend on a private loopback port and injects a
local trust token into its own webview. There is no desktop login screen.
Web server:
```bash
bun run serve:web
# open http://127.0.0.1:8787
```
The web host requires login by default. On a fresh data directory, the first
visitor creates the initial admin unless you pre-seed `NOMIFUN_ADMIN_PASSWORD`.
![First-run admin setup](../images/gs-04-quickstart-login.png)
## 2. Start From `/guid`
After auth, the app opens `/guid`. This is the default session start surface.
You can choose:
- an agent/runtime,
- a model/provider when the runtime supports it,
- an assistant persona,
- skills or MCP tools,
- a workspace path,
- and the first prompt.
![The /guid landing page](../images/gs-05-quickstart-guid.png)
## 3. Configure A Model
Open **Models** (`/models`) and configure at least one provider or detected CLI
agent. The page owns provider credentials, agent availability, and global
reliability settings such as IDMM and model failover.
![Model settings](../images/gs-06-quickstart-model-settings.png)
For the simplest first run, use the built-in Nomi engine with an API provider
you have credentials for. External CLIs such as Claude Code, Codex, Gemini CLI,
Qwen, and OpenCode must also be installed on the host before NomiFun can use
them.
## 4. Send The First Message
Back on `/guid`:
1. Choose an agent.
2. Choose a model if required.
3. Optionally choose an assistant.
4. Type a prompt.
5. Send with the button or `Ctrl/Cmd+Enter`.
NomiFun creates a conversation and navigates to `/conversation/<id>`.
## 5. Use The Workspace
Each conversation can use a working directory. Inside a conversation you can
inspect messages, tool calls, file edits, previews, and terminal sessions.
Useful next pages:
- [Terminal](../guides/terminal.md)
- [MCP & Skills](../guides/mcp-and-skills.md)
- [Assistants](../guides/assistants.md)
- [AutoWork & Requirements](../guides/autowork-requirements.md)
- [Scheduled Tasks](../guides/scheduled-tasks.md)
- [Web Server Deployment](../guides/web-server-deployment.md)
- [Architecture Overview](../architecture/overview.md)
@@ -0,0 +1,98 @@
# 快速上手
这页带你完成第一段 NomiFun 会话。若还没有安装,请先看
[安装](installation.zh.md)。
桌面应用和 Web 服务使用同一份 UI。差异主要在鉴权:桌面 WebView 通过本地
信任 token 免登录;`nomifun-web` 和 WebUI 远程访问需要登录。
## 1. 启动
### 桌面开发模式
```bash
bun run dev
```
这会启动 Vite、编译 Tauri shell,并在桌面进程中嵌入 `nomifun-app` 后端。
后端会选择一个空闲 localhost 端口,桌面 WebView 自动携带本地信任 token。
### Web 服务模式
```bash
bun run build:ui
bun run serve:web
```
访问 `http://127.0.0.1:8787`。首次访问会创建管理员账号,之后需要登录。
![首次启动管理员设置(仅 Web](../images/gs-04-quickstart-login.png)
## 2. 认识首页
登录后默认进入 `/guid`。这里提供开始会话所需的几件事:
- **agent 选择**:选择 Nomi、Claude Code、Codex、Gemini、Qwen、OpenCode 等后端。
- **模型选择**:对支持模型切换的 agent 选择 provider 和 model id。
- **助手**:选择预设 persona、system prompt、技能与工具组合。
- **工具与工作区**:选择本次会话可见的 MCP server 和技能,并确认工作目录。
- **输入框**:输入第一条提示词,必要时用 `@` 引用文件、技能或助手。
![`/guid` 首页](../images/gs-05-quickstart-guid.png)
## 3. 配置模型
发送第一条消息前,至少需要配置一个可用模型。打开 `/models`
- 添加 Anthropic、OpenAI、Bedrock、Vertex 或兼容 OpenAI/Anthropic 协议的 provider。
- 为 provider 填写 API key、base URL 和默认模型。
- 如需无人值守长任务,可配置 **Model Failover Queue**,让 Nomi 引擎在模型失败、
限流或不可用时按顺序尝试备用模型。
![模型设置](../images/gs-06-quickstart-model-settings.png)
外部 CLI agent 仍需要在宿主机上安装对应 CLI;`/models` 只解决模型凭据和
模型选择,不会替你安装第三方 CLI。
## 4. 创建第一段会话
回到 `/guid`
1. 选择 **Nomi**,它不依赖外部 CLI,最适合首跑验证。
2. 选择一个已配置的模型。
3. 可选:选择一个助手。
4. 输入提示词,例如:
> 写一个返回第 n 个斐波那契数的 Python 函数,并附一个小测试。
5. 发送。
NomiFun 会创建新会话并进入 `/conversation/:id`,随后开始流式输出。
## 5. 使用会话工作区
每段会话都有独立工作目录。会话页通常包含:
- **消息流**:模型回复、工具调用、文件变更和执行状态。
- **文件树**:显示本会话工作目录中的文件。
- **预览面板**:预览代码、Markdown、PDF、Office、HTML 和 diff。
- **终端**:从 `/terminal-new` 或会话内入口启动 PTY,默认挂载到工作目录。
可以让 agent 把刚才的函数写入文件,然后在文件树和预览面板里检查结果。
## 6. 常用入口
- `/assistants`:管理助手;`/assistants?tab=skills` 管理技能。
- `/mcp`:管理 MCP server、连接测试、OAuth 和导入/同步。
- `/open-capabilities`:管理 WebUI 远程访问和对外能力暴露。
- `/scheduled`:创建 cron 会话任务;支持从会话带上下文创建。
- `/requirements`AutoWork 需求看板。
- `/nomi`:Nomi 伙伴、远程频道绑定和 companion 设置。
接下来可以继续阅读:
- [MCP 与技能](../guides/mcp-and-skills.zh.md)
- [助手](../guides/assistants.zh.md)
- [终端](../guides/terminal.zh.md)
- [WebUI 远程访问](../guides/webui-remote-access.zh.md)
- [Web 服务部署](../guides/web-server-deployment.zh.md)
+102
View File
@@ -0,0 +1,102 @@
# Assistants
An **assistant** is a reusable persona package for an agent: display metadata,
default agent backend, optional model preferences, system prompt, and skill
selection.
Open the current page at **`/assistants`**. The legacy
`/settings/assistants` route redirects to `/assistants?tab=assistants`.
![Assistants list and drawer](../images/assistants-01-list.png)
## Sources
Assistants are merged from three sources:
| Source | Where it comes from | Editable? |
| --- | --- | --- |
| Builtin | Embedded manifest under `crates/backend/nomifun-app/assets/builtin-assistants/`, loaded by `BuiltinAssistantRegistry`. | Content is read-only; enable/sort/last-used state and builtin `preset_agent_type` override are stored separately. |
| Custom | User-created rows in the `assistants` table plus files in the data dir. | Fully editable and deletable. |
| Extension | Installed extensions via `resolvers::assistant`. | Read-only from this page; manage the extension lifecycle instead. |
The merged list is returned by `GET /api/assistants`.
## What an Assistant Owns
Key fields:
- `id`, `source`, `name`, `description`, `avatar`
- `preset_agent_type`: default backend such as `nomi`, `claude`, `codex`, `gemini`
- `models`: optional preferred model ids
- `prompts` / `prompts_i18n`: assistant instructions
- `enabled_skills`: skills attached when starting a session
- `enabled`, `sort_order`, `last_used_at`
- tag metadata used by the picker and filters
Custom assistant rule files live under the data dir:
- `assistant-rules/`
- `assistant-skills/`
- `assistant-avatars/`
Deleting a custom assistant removes its associated files.
## Editing Rules
| Field / action | Builtin | Extension | Custom |
| --- | --- | --- | --- |
| Enable / disable | yes | yes | yes |
| Sort / last-used state | yes | yes | yes |
| Change default agent backend | builtin override only | no | yes |
| Edit name / description / avatar | no | no | yes |
| Edit prompt / skill text | no | no | yes |
| Delete | no | no | yes |
Builtin mutations are stored in `assistant_overrides`. Extension assistants are
owned by their extension and intentionally read-only here.
![Assistant editor drawer](../images/assistants-02-editor.png)
## Skills
The Skills tab is also under `/assistants`:
- `/assistants?tab=assistants`
- `/assistants?tab=skills`
Assistant `enabled_skills` are merged with auto-injected builtin skills when a
session starts. The materialization rules are implemented by the skill routes
and backend-specific agent adapters; users do not need to copy skill folders
manually for normal use.
For MCP servers, use `/mcp`; skills and MCP servers are related but managed on
separate pages now.
## API
| Operation | Endpoint |
| --- | --- |
| List / create | `GET`, `POST /api/assistants` |
| Update / delete | `PUT`, `DELETE /api/assistants/{id}` |
| State override | `PATCH /api/assistants/{id}/state` |
| Avatar | `GET /api/assistants/{id}/avatar` |
| Bulk import | `POST /api/assistants/import` |
| Tags | `GET`, `POST /api/assistant-tags`; `PUT`, `DELETE /api/assistant-tags/{key}` |
Rule and assistant-skill file reads/writes go through `/api/skills/assistant-*`
routes so builtin, extension, and user sources can be dispatched correctly.
## Notes
- Creating an assistant without `preset_agent_type` requires at least one
configured provider; the service defaults to `nomi` when possible.
- CLI-backed agents still require their CLI to be installed on the host. Picking
`claude`, `codex`, or `gemini` as an assistant backend does not install those
tools.
- Import from legacy JSON is insert-only and idempotent: existing ids are
skipped, and invalid rows are reported per assistant.
## Related
- [MCP & Skills](./mcp-and-skills.md)
- [Model Failover Queue](./model-routing.md)
@@ -0,0 +1,95 @@
# 助手
**助手**是一套可复用的 agent persona:展示信息、默认 agent 后端、可选模型偏好、
system prompt 和技能选择。
当前入口是 **`/assistants`**。旧 `/settings/assistants` 会重定向到
`/assistants?tab=assistants`
![助手列表和抽屉](../images/assistants-01-list.png)
## 来源
助手由三类来源合并:
| 来源 | 来自哪里 | 是否可编辑 |
| --- | --- | --- |
| Builtin | 嵌入在 `crates/backend/nomifun-app/assets/builtin-assistants/` 的 manifest,由 `BuiltinAssistantRegistry` 加载。 | 内容只读;启用、排序、最近使用和 builtin `preset_agent_type` 覆盖单独存储。 |
| Custom | 用户创建的 `assistants` 表记录和数据目录中的文件。 | 可完整编辑和删除。 |
| Extension | 已安装扩展通过 `resolvers::assistant` 提供。 | 此页只读;生命周期由扩展管理。 |
合并后的列表由 `GET /api/assistants` 返回。
## 助手包含什么
关键字段:
- `id``source``name``description``avatar`
- `preset_agent_type`:默认后端,例如 `nomi``claude``codex``gemini`
- `models`:可选模型偏好
- `prompts` / `prompts_i18n`:助手指令
- `enabled_skills`:启动会话时附加的技能
- `enabled``sort_order``last_used_at`
- picker/filter 使用的标签元数据
自定义助手文件位于数据目录:
- `assistant-rules/`
- `assistant-skills/`
- `assistant-avatars/`
删除自定义助手会清理关联文件。
## 编辑规则
| 字段 / 操作 | Builtin | Extension | Custom |
| --- | --- | --- | --- |
| 启用 / 禁用 | yes | yes | yes |
| 排序 / 最近使用 | yes | yes | yes |
| 修改默认 agent 后端 | 仅 builtin override | no | yes |
| 编辑名称 / 描述 / 头像 | no | no | yes |
| 编辑 prompt / skill 文本 | no | no | yes |
| 删除 | no | no | yes |
Builtin 的可变状态写入 `assistant_overrides`。Extension 助手由扩展拥有,因此在
这里只读。
![助手编辑抽屉](../images/assistants-02-editor.png)
## 技能
技能 tab 也在 `/assistants` 下:
- `/assistants?tab=assistants`
- `/assistants?tab=skills`
助手的 `enabled_skills` 会在 session start 时与自动注入的 builtin 技能合并。
后端会按不同 agent 后端的规则 materialize 技能;正常使用时不需要手动复制技能目录。
MCP server 已独立到 `/mcp`。技能和 MCP 都能扩展 agent 能力,但当前是分开管理。
## API
| 操作 | Endpoint |
| --- | --- |
| 列表 / 创建 | `GET`, `POST /api/assistants` |
| 更新 / 删除 | `PUT`, `DELETE /api/assistants/{id}` |
| 状态覆盖 | `PATCH /api/assistants/{id}/state` |
| 头像 | `GET /api/assistants/{id}/avatar` |
| 批量导入 | `POST /api/assistants/import` |
| 标签 | `GET`, `POST /api/assistant-tags`; `PUT`, `DELETE /api/assistant-tags/{key}` |
助手规则和助手技能文件通过 `/api/skills/assistant-*` 路由读写,以便正确分发到
builtin、extension 或 user source。
## 注意
- 创建助手但未指定 `preset_agent_type` 时,需要至少有一个已配置 provider;能推断时默认使用 `nomi`
- CLI 型 agent 仍需要宿主机安装对应 CLI。选择 `claude``codex``gemini`
不会自动安装这些工具。
- 旧 JSON 导入是 insert-only 且幂等的:已有 id 会跳过,错误按行报告。
## 相关
- [MCP 与技能](./mcp-and-skills.zh.md)
- [模型故障转移队列](./model-routing.zh.md)
@@ -0,0 +1,307 @@
# AutoWork & Requirements
AutoWork is Nomi's flagship automation: a **requirements board** plus an
**orchestrator** that drives an AI agent (or an agent CLI in a terminal) to
work through those requirements one at a time, without you holding its hand.
You file requirements, group them by tag, bind a tag to a session
(conversation or terminal), and the orchestrator claims, executes, and
finalises them in order. When a requirement reaches a terminal state it can
fire a **completion notifier** (Lark/飞书 webhook) so your team hears about
it the moment it lands.
Everything described here is **backend-authoritative**: AutoWork resumes on
boot and runs whether or not you have the UI open.
![AutoWork tag-sessions overview](../images/autowork-01-tag-sessions.png)
## Concepts
| Term | What it means |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Requirement** | A unit of work: title, content (the actual instructions), tag, an `order_key` (string compared lexicographically), and a status. Stored in SQLite. |
| **Tag** | A free-form string used to group requirements into a queue. Bindings, kanban columns, and webhook routing all key off the tag. |
| **Status** | `pending``in_progress``done` (or `failed` / `cancelled`). The kanban view has one column per status. |
| **Claim & lease** | The orchestrator atomically transitions the lowest-`order_key` `pending` requirement in a tag to `in_progress` and writes a lease that expires. |
| **Lease sweeper** | A background task (every 60 s) that re-pends `in_progress` rows whose lease expired and whose owning session is no longer live — so a crash never orphans work. |
| **Orchestrator** | The per-target loop that claims → injects → waits → finalises → repeats. One loop per bound session. Persistent: it idles when the queue drains, it does not exit. |
| **Target** | The thing executing the work. Two kinds: a **conversation** (an AI agent), or a **terminal** (a real CLI agent over a PTY). |
| **Turn completion** | How a turn signals "done." For agent targets, the agent ends its turn (or calls a Nomi-only tool); for terminal targets, the terminal simply goes quiescent — a clean end-of-turn. |
| **Completion notifier** | A Lark/飞书 webhook fired when a requirement reaches `done`/`failed`/`cancelled`. Bound per tag. |
| **IDMM** | Intelligent Decision-Making Mode — a session supervisor that keeps targets alive through provider faults and decision stalls. Stacks on AutoWork. |
## Lifecycle of one requirement
```
pending ──claim_next()──▶ in_progress (lease) ──injection──▶ agent / CLI runs
│ │
▼ ▼
sweeper re-pends if lease Finish event / quiescence
expires & loop is gone │
done | failed | cancelled
CompletionNotifier fires (best-effort)
```
The orchestrator does **not** exit when the tag is empty. It awaits a wake
notification (with a 10 s safety-net poll) and keeps claiming forever, so a
new requirement filed against a bound tag is picked up almost instantly.
It exits only when:
- you disable AutoWork on that target,
- the binding hits its `max_requirements` cap (which is then persisted as
disabled, so the cap survives a restart), or
- a terminal target's row is deleted (a terminal whose PTY merely exited
idles and waits for re-launch — it does not stop).
## Three views
AutoWork's data is the same in every view; the views are different lenses.
### Requirements list — `/requirements`
The flat table. Filter by tag, status, or free-text search. Bulk-delete
selected rows. Open a row to see its detail drawer; **Edit** lives at
`/requirements/:id/edit`. **New requirement** opens the list with
`/requirements?new=1`; the old `/requirements/new` route redirects there.
![Requirements list](../images/autowork-02-list.png)
### Board — `/requirements?view=board`
One column per status for a chosen tag. Drag-and-drop is intentionally not
the way to change status here; use the detail drawer. The board re-fetches
on every `requirements.*` realtime event so it tracks the orchestrator
live.
![Requirements kanban](../images/autowork-03-kanban.png)
### Tag sessions — `需求平台 → 扩展能力 → 自动执行`
The AutoWork admin (`/requirements/extensions?tab=autowork`). Lists every
tag, every binding (which conversations and terminals are bound to which
tag), and the live run-state for each binding (`Idle`, `Active` while a
turn is in flight). The per-tag completion webhook now lives one tab over,
in **通知** (see [Completion notifications](#completion-notifications--lark--http--slack)).
This is where you watch the fleet. To **start** AutoWork on a binding, open
the session itself and toggle AutoWork there — that is the canonical place
to bind a tag, set `max_requirements`, and persist the configuration.
![Tag sessions admin](../images/autowork-04-tag-sessions.png)
## Filing a requirement
Press **New requirement** from the list page (or navigate to
`/requirements?new=1`). The form has:
- **Title** — short label.
- **Tag** — pick an existing tag or type a new one. Tags are created on
first use.
- **Content** — the actual instructions the agent / CLI will be handed.
Write it like you would write a ticket: enough context that the agent can
start without asking back, plus a clear definition of done.
- **Order key** — a string used for queue order. Lexicographic, so common
patterns are `1.0`, `1.1`, `1.2.0` etc. Lower is earlier.
- **Status** — defaults to `pending`. You can manually mark a row `done` or
`cancelled` from here too.
Submit and the row is queued. If a session is already bound to that tag, it
is woken up immediately and starts on this requirement (assuming nothing
else is in flight ahead of it).
## Binding a session: agent vs terminal
A binding is `(target_kind, target_id, tag, max_requirements?)`. There are
exactly two target kinds.
### Agent target (a conversation)
Open any conversation. The header has an **AutoWork** control. Pick a tag,
optionally set a completion cap, and enable.
What happens per turn:
1. The orchestrator claims the next `pending` requirement in that tag.
2. It builds an injection prompt that names the requirement and tells the
agent how to signal completion. The exact contract is **engine-aware**:
- On Nomi-engine sessions only, the agent has the
`requirement_complete` / `requirement_update_status` tools registered
and the prompt asks the model to call them.
- On every other engine (ACP / Codex / Gemini / Openclaw / Nanobot /
Remote), the agent has no requirement tools registered, so the prompt
uses the **tool-free contract**: do the work, end the turn with a
plain-text completion note, and the platform records `done`
automatically when the turn finishes cleanly. Failures are surfaced in
plain text (the prompt asks the model to start the final line with
`Requirement failed:` followed by the reason).
3. The injected message is hidden from the user-visible transcript.
4. The orchestrator subscribes to the agent's stream and waits for a
`Finish` (clean) or `Error`/timeout (re-pend or fail). It also captures
the agent's prose into a tail-bounded **completion note** that is stored
on the requirement and, on tool-free engines, becomes the report sent
downstream.
5. When the turn ends cleanly, `finalize_if_needed` records the row as
`done` and fires the notifier.
### Terminal target (an agent CLI over a PTY)
Open a terminal whose preset is `claude` or `codex` (a plain shell is not
eligible). Gemini terminals can be run manually, but the backend does not
accept them as terminal AutoWork targets yet because the turn lifecycle and
completion contract are not wired into the orchestrator. The header has the
same **AutoWork** control for eligible terminals. Bind a tag and enable.
What happens per turn:
1. The orchestrator subscribes to the terminal's live output stream
**before** injecting (so nothing is missed).
2. It writes the requirement prompt into the PTY wrapped in bracketed-paste
markers (`ESC [200~ … ESC [201~`) followed by `CR`, so the multi-line
text lands as a single paste in the CLI's editor and Enter actually
submits.
3. The prompt just asks the agent to do the work and **end its turn** — there
is no marker to print. Scraping a protocol string out of an interactive TUI
proved unreliable (cursor-painted output, no clean newlines, the model
mis-copying a code), so completion is detected from the turn itself.
4. When the output goes **quiescent** (silent for ≥ 10 s after a 3 s minimum,
with the PTY still alive) the agent has finished and gone idle — the turn is
recorded as `done`, the same clean-finish contract a tool-free chat agent
uses.
5. If the agent cannot complete the requirement it is asked to say so in plain
text (e.g. a final `Requirement failed:` line); such turns still finish as
`done` at the platform level, so review the conversation when in doubt.
6. PTY death mid-turn → re-pend. Hard turn timeout is 1 hour.
> **Full Auto recommended.** A turn that hits an interactive approval
> prompt will block until the timeout. Each agent CLI has a non-interactive
> flag the terminal's "Full Auto" mode adds for you (see
> [Terminals → Creating a terminal](./terminal.md#creating-a-terminal)).
A terminal that has been bound but whose PTY has exited keeps its loop
alive in idle: the moment you re-launch the terminal, AutoWork resumes
where it left off — no need to toggle the bind off and on.
## Boot resume — it runs without you
The orchestrator's running set is in-memory, but every binding's `enabled`,
`tag`, and `max_requirements` are persisted (in conversation `extra.autowork`
or the terminal's `autowork` column). On process start the backend lists
every user, walks every tag binding, and **spawns the loops itself**. You do
not need to open the session page for AutoWork to work; the UI just shows
you what is already running.
This is why "AutoWork only worked while I had the tab open" is a bug, not a
feature. If you observe it, check the orchestrator logs for resume failures
on that user / target.
## Completion notifications (Lark / HTTP / Slack)
When a requirement transitions to a terminal state, the
`CompletionNotifier` is invoked. Today it does this:
1. Look up the **per-tag setting** for the requirement's tag — if the tag
has no setting or no bound webhook, the notifier silently no-ops. If
the tag's event filter (**完成 / 失败 / 待复核**) excludes this
transition, it also no-ops.
2. Look up the bound webhook by id; if it is disabled, no-op.
3. Build a payload for the webhook's platform — a **Lark/飞书** interactive
card, a **通用 HTTP** JSON body, or a **Slack** message — carrying these
fields:
`需求id` · `需求名` · `需求内容` (truncated to 500 chars) ·
`完成状态` (`done`/`failed`/`cancelled`) ·
`完成记录(报告)` (the completion note captured during the turn,
truncated to 500 chars).
4. POST to the webhook URL. If the webhook has a secret configured, the
request is signed with the standard Lark custom-bot scheme
(`HMAC-SHA256(key="{ts}\n{secret}", msg="")`, base64).
5. Failure is logged at `warn` and swallowed — a flaky webhook never
affects requirement state.
### Setting it up
Notification setup now lives entirely inside the platform at
**需求平台 → 扩展能力 → 通知** (`/requirements/extensions?tab=notify`) —
channel and routing sit side by side on the one sub-tab.
1. In the **通知** sub-tab, **Create webhook**: give it a name, pick the
platform (**Lark/飞书**, **通用 HTTP**, or **Slack**), paste the URL,
and (optionally) the matching secret. Use **Test** to send a card and
verify the bot is reachable.
2. Under **触发规则** in the same sub-tab, find the tag and pick the
webhook from the per-tag dropdown. You can also filter which events
fire — **完成 / 失败 / 待复核** — so a tag only notifies on the states
you care about. The setting is saved per tag.
You can change which webhook a tag points to at any time, including
clearing the binding to mute notifications for that tag.
![Per-tag webhook routing](../images/autowork-05-webhook-binding.png)
## IDMM — keeping turns alive through stalls
IDMM is a separate, optional supervisor (`nomifun-idmm`). It watches a
session and intervenes when a stall is detected:
- **Rule tier (no LLM)** — provider error, repeated retries, model spinning
on a tool call, etc. — handled with a deterministic policy.
- **Sidecar tier** — a lightweight backup model is asked to make the next
decision so the session does not hang.
When AutoWork starts a turn, it asks IDMM (if wired) to **ensure
supervision** of the target for the duration of that turn. The two
features compose: AutoWork drives forward progress, IDMM keeps each turn
from getting stuck so it actually reaches a terminal state instead of
timing out. Toggle IDMM from the same place as AutoWork (the session
header).
See `crates/backend/nomifun-idmm/` for the per-tier policy detail and the
intervention log API.
> For the full picture — the rule tier, the sidecar model, session keep-alive
> and when to turn it on — see the dedicated
> [Intelligent Decision (IDMM)](intelligent-decision.md) guide.
## Routes & API
| What | Where |
| --------------------------------- | ---------------------------------------------------------------- |
| Requirements list | `/requirements` |
| Board (per tag) | `/requirements?view=board` |
| Tag sessions admin (自动执行) | `/requirements/extensions?tab=autowork` |
| Notification config (通知) | `/requirements/extensions?tab=notify` |
| New / edit | `/requirements?new=1`, `/requirements/:id/edit` |
| Legacy `/requirements/new`, `/requirements/kanban` | redirect to the current query-param routes |
| Legacy `/autowork`, `/requirements/tag-sessions` | redirect to `/requirements/extensions?tab=autowork` |
| Legacy `/settings/webhook`, `/other` | redirect to `/requirements/extensions?tab=notify` |
| List / create requirement | `GET /api/requirements`, `POST /api/requirements` |
| Tags | `GET /api/requirements/tags` |
| Tag bindings (admin) | `GET /api/requirements/tag-bindings` |
| Per-tag board | `GET /api/requirements/board?tag=…` |
| Get / update / delete | `GET|PUT|DELETE /api/requirements/:id` |
| Status / complete / claim | `POST /api/requirements/:id/status`, `…/complete`, `…/claim` |
| AutoWork toggle / state | `POST /api/requirements/autowork`, `GET …/autowork/:kind/:tid` |
| Webhooks | `GET|POST /api/webhooks`, `…/{id}`, `…/{id}/test` |
| Per-tag webhook | `GET|PUT /api/tags/:tag/settings` |
## Implementation notes (for the curious)
- `requirements.conversation_id` intentionally has **no foreign key** to the
conversations table. A requirement is created and rotates through
conversations as it gets re-pended; tying it to a single conversation
with referential integrity made cleanups awkward and added no real
safety. Treat the column as advisory.
- The orchestrator's `wake` Notify is shared with `RequirementService`;
every state transition that re-pends or creates work fires it, and the
loop is armed-then-awaited around each `claim_next()` call so a wake
arriving between "claim returned None" and "await" is never lost.
- The terminal injection wraps the prompt in bracketed-paste markers so the
multi-line text lands as one paste, and submits with a separate `CR` written
a beat later (a CR in the same write would be swallowed by the paste-burst
detection modern agent TUIs use).
- The completion note for tool-free engines is bounded (`MAX_NOTE_CHARS =
4000`) and **tail-biased** — agents tend to summarise at the end, so the
tail is what we keep when truncation is needed.
@@ -0,0 +1,191 @@
# AutoWork 与 Requirements
AutoWork 是 NomiFun 的旗舰自动化能力:一块 **需求看板**requirements board)加上一个 **编排器**orchestrator),由它驱动 AI 智能体(或运行在终端中的 agent CLI)逐条处理这些需求,无需你全程盯着。
你登记需求,按 tag 分组,把 tag 绑定到一个会话(对话或终端),编排器就会按顺序认领、执行并完结它们。当某条需求进入终态时,可以触发 **完成通知**Lark/飞书 webhook),让你的团队第一时间知道结果。
这里描述的所有内容都是 **后端权威** 的:AutoWork 在进程启动时自动恢复,无论你是否打开 UI 都会运行。
![AutoWork tag-sessions 总览](../images/autowork-01-tag-sessions.png)
## 概念
| 术语 | 含义 |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Requirement** | 一个工作单元:标题、内容(实际指令)、tag、`order_key`(按字典序比较的字符串)以及状态。存储在 SQLite 中。 |
| **Tag** | 任意字符串,用来把需求归入一个队列。绑定关系、看板列以及 webhook 路由都以 tag 为键。 |
| **Status** | `pending``in_progress``done`(或 `failed` / `cancelled`)。看板视图每个状态对应一列。 |
| **Claim & lease** | 编排器原子地把某 tag 中 `order_key` 最小的 `pending` 需求转为 `in_progress`,并写入一份带过期时间的租约(lease)。 |
| **Lease sweeper** | 一个后台任务(每 60 秒一次),会把租约已过期、且持有它的会话已不在的 `in_progress` 行重置回 `pending`——这样崩溃永远不会让任务孤立。 |
| **Orchestrator** | 每个目标对应一个的循环:认领 → 注入 → 等待 → 完结 → 重复。每个绑定的会话有一个循环。它是常驻的:队列空了就空闲等待,不会退出。 |
| **Target** | 实际执行工作的对象。两种:**会话**(一个 AI 智能体),或者 **终端**(通过 PTY 运行的真实 CLI 智能体)。 |
| **回合完成** | 一轮如何宣告"完成"。对智能体目标来说,是该智能体结束本轮回复(或调用 Nomi 专属工具);对终端目标来说,是终端输出静默下来(干净收尾)。 |
| **Completion notifier** | 当需求进入 `done`/`failed`/`cancelled` 时触发的 Lark/飞书 webhook。按 tag 绑定。 |
| **IDMM** | 智能决策模式(Intelligent Decision-Making Mode)——一个会话级监督器,能在 provider 故障和决策卡顿时让目标继续存活。可与 AutoWork 叠加使用。 |
## 单条需求的生命周期
```
pending ──claim_next()──▶ in_progress (lease) ──injection──▶ agent / CLI runs
│ │
▼ ▼
sweeper re-pends if lease Finish event / quiescence
expires & loop is gone │
done | failed | cancelled
CompletionNotifier fires (best-effort)
```
当 tag 为空时编排器 **不会** 退出。它会等待唤醒通知(外加一个 10 秒兜底轮询),并永久持续认领,因此向已绑定的 tag 新提交的需求几乎是即时被拾取。
它仅在以下情况退出:
- 你对该目标关闭了 AutoWork;
- 绑定触达了 `max_requirements` 上限(此时配置会被持久化为已禁用,使该上限在重启后依然生效);或
- 某个终端目标对应的行被删除(仅仅是 PTY 退出的终端会进入空闲并等待重新启动——它并不会停止循环)。
## 三种视图
AutoWork 在每个视图中的数据完全相同,视图只是不同的"镜头"。
### 需求列表 — `/requirements`
扁平表格。可按 tag、状态或全文搜索过滤。可批量删除选中行。点击行可以打开详情抽屉;**编辑** 路径是 `/requirements/:id/edit`**新建需求** 通过 `/requirements?new=1` 打开,旧的 `/requirements/new` 会重定向到这里。
![需求列表](../images/autowork-02-list.png)
### 看板 — `/requirements?view=board`
针对所选 tag,每个状态一列。这里有意 **不** 通过拖拽来改状态;请使用详情抽屉。看板会在每次 `requirements.*` 实时事件触发时重取数据,因此能跟随编排器实时变化。
![需求看板](../images/autowork-03-kanban.png)
### Tag sessions — `需求平台 → 扩展能力 → 自动执行`
AutoWork 的管理面板(`/requirements/extensions?tab=autowork`)。列出所有 tag、所有绑定(哪些会话和终端绑定到哪个 tag)、每条绑定的实时运行状态(`Idle`,或正在执行某轮时为 `Active`)。每个 tag 的完成 webhook 现在在旁边的 **通知** tab`/requirements/extensions?tab=notify`)。
这里是你"巡视舰队"的地方。要在某条绑定上 **启动** AutoWork,请打开会话本身并在那里切换 AutoWork 开关——那才是绑定 tag、设置 `max_requirements` 和持久化配置的标准位置。
![Tag sessions 管理面板](../images/autowork-04-tag-sessions.png)
## 提交一条需求
在列表页点击 **新建需求**(或访问 `/requirements?new=1`)。表单包含:
- **标题**:简短的标签。
- **Tag**:选择已有 tag 或键入一个新值。tag 在首次使用时会被创建。
- **内容**:交给智能体 / CLI 的实际指令。当作 ticket 来写:上下文足够让智能体不必反问就能开始,并附上清晰的"完成定义"。
- **Order key**:用于队列排序的字符串。按字典序排列,因此常见模式如 `1.0``1.1``1.2.0` 等等。值越小越早。
- **状态**:默认是 `pending`。你也可以在这里手动把某行标记为 `done``cancelled`
提交后该行进入队列。如果已有会话绑定到该 tag,它会立刻被唤醒并开始处理这条需求(前提是没有别的需求排在它前面)。
## 绑定会话:智能体 vs 终端
一条绑定形如 `(target_kind, target_id, tag, max_requirements?)`。target kind 只有两种。
### 智能体目标(一个会话)
打开任意会话。头部有一个 **AutoWork** 控件。选择 tag,可选地设置完成上限,然后启用。
每一轮中发生的事:
1. 编排器认领该 tag 中下一条 `pending` 需求。
2. 它构造一段注入 prompt,点名该需求并告知智能体如何上报完成状态。具体协议是 **engine-aware** 的:
- 仅在 Nomi-engine 会话上,智能体会注册 `requirement_complete` / `requirement_update_status` 工具,并由 prompt 要求模型调用它们。
- 在所有其他 engineACP / Codex / Gemini / Openclaw / Nanobot / Remote)上,智能体不会注册任何 requirement 工具,因此 prompt 使用 **无工具协议**:把工作做完,以一段纯文本完成说明结束本轮,平台会在本轮干净结束后自动记为 `done`。失败通过纯文本上报(prompt 要求模型把最后一行以 `Requirement failed:` 起头,紧跟原因)。
3. 注入消息会从用户可见的对话记录中隐藏。
4. 编排器订阅该智能体的流,等待 `Finish`(干净)或 `Error`/超时(重置回 pending 或 fail)。同时它会把智能体的文本输出捕获到一份 tail-bounded 的 **completion note**,存到该需求上;在无工具协议的 engine 上,这份 note 就是发到下游的报告。
5. 当本轮干净结束时,`finalize_if_needed` 把该行记为 `done` 并触发通知器。
### 终端目标(运行在 PTY 中的 agent CLI
打开预设为 `claude``codex` 的终端(普通 shell 不符合条件)。
Gemini 终端可以手动运行,但后端目前不会接受它作为终端 AutoWork 目标:
它的回合生命周期和完成契约还没有接入编排器。符合条件的终端头部会显示同一个
**AutoWork** 控件;绑定一个 tag 并启用即可。
每一轮中发生的事:
1. 编排器在注入 **之前** 订阅终端的实时输出流(这样不会漏任何字节)。
2. 它向 PTY 写入需求 prompt,外面包了一对 bracketed-paste 标记(`ESC [200~ … ESC [201~`),后跟 `CR`,使多行文本作为单次粘贴落入 CLI 的编辑器,并由 Enter 实际提交。
3. prompt 只要求 agent 把活干完、**结束本轮回复**——不需要打印任何标记。从交互式 TUI 里抓协议字符串被证明不可靠(光标重绘输出、没有干净的换行、模型抄错 code),所以完成判定改为基于回合本身。
4. 当输出 **静默**(在最少 3 秒后≥10 秒无输出,且 PTY 还活着),说明 agent 已干完并回到空闲——本轮记为 `done`,与无工具的对话 agent 用的是同一套「干净收尾即完成」契约。
5. 如果 agent 无法完成,会被要求用纯文本明确说明(例如最后一行以 `Requirement failed:` 开头);这类回合在平台层面仍记为 `done`,拿不准时请回看对话。
6. 中途 PTY 死亡 → 重置回 pending。整轮硬超时是 1 小时。
> **强烈推荐 Full Auto。** 一旦本轮撞到交互式批准提示,会一直阻塞到超时。每个 agent CLI 都有一个非交互式开关,终端的 "Full Auto" 模式会替你加上(参见 [Terminals → Creating a terminal](./terminal.zh.md#creating-a-terminal))。
被绑定但 PTY 已退出的终端会让循环以空闲方式存活:当你重新启动该终端时,AutoWork 会从中断处继续——无需先关闭再开启绑定。
## 启动恢复——它在你不在场时也会运行
编排器的运行集合存放在内存中,但每条绑定的 `enabled``tag``max_requirements` 都已持久化(在会话的 `extra.autowork` 或终端的 `autowork` 列中)。进程启动时后端会列出每个用户、遍历每条 tag 绑定,并 **自行启动** 这些循环。要让 AutoWork 工作你不必去打开会话页面;UI 只是把已经在跑的状态展示给你看。
这就是为什么"AutoWork 只在我开着标签页时才工作"是一个 bug 而不是 feature。如果你观察到这种现象,去检查编排器日志中是否有该用户/目标的 resume 失败记录。
## 完成通知(Lark / 飞书)
当需求进入终态时,会调用 `CompletionNotifier`。今天它做的事:
1. 查找该需求 tag 的 **per-tag 设置**——如果该 tag 没有设置或没有绑定 webhook,通知器静默 no-op。
2. 按 id 查找绑定的 webhook;如果它处于禁用状态,no-op。
3. 构造一张 Lark 互动卡片,字段如下:
`需求id` · `需求名` · `需求内容`(截断到 500 字符) ·
`完成状态``done`/`failed`/`cancelled` ·
`完成记录(报告)`(本轮中捕获的 completion note,截断到 500 字符)。
4. POST 到 webhook URL。如果该 webhook 配置了 secret,请求会按 Lark 自定义机器人的标准方案签名(`HMAC-SHA256(key="{ts}\n{secret}", msg="")`base64)。
5. 失败会以 `warn` 记录并吞掉——一个不稳定的 webhook 永远不会影响需求状态。
### 配置步骤
1. 进入 **需求平台 → 扩展能力 → 通知**`/requirements/extensions?tab=notify`)并 **Create webhook**:填写名称、Lark 自定义机器人 URL,以及(可选的)匹配 secret。点 **Test** 发一张卡片,验证机器人可达。
2. 在同一个 **通知** tab 里找到该 tag,从 per-tag 下拉框中挑选 webhook。设置按 tag 保存。
你可以随时改变某个 tag 指向哪个 webhook,包括清空绑定以静音该 tag 的通知。
![Per-tag webhook 路由](../images/autowork-05-webhook-binding.png)
## IDMM——让卡顿中的本轮继续存活
IDMM 是一个独立、可选的监督器(`nomifun-idmm`)。它监视会话,并在检测到卡顿时介入:
- **规则层(无 LLM**:provider 报错、反复重试、模型在工具调用上转圈等等——以确定性策略处理。
- **Sidecar 层**:调用一个轻量备用模型来下达下一步决策,避免会话挂死。
当 AutoWork 启动一轮时,它会请求 IDMM(如果已对接)在本轮持续期间 **保证监督** 该目标。两个特性可以组合:AutoWork 推动前进,IDMM 让每一轮不至于卡死,从而真正进入终态而不是超时。IDMM 与 AutoWork 在同一处切换(会话头部)。
每层策略的细节和介入日志 API 见 `crates/backend/nomifun-idmm/`
> 想看完整全貌 —— 规则层、旁路模型、会话保活,以及何时开启 —— 参见专门的
> [智能决策(IDMM](intelligent-decision.zh.md)指南。
## 路由与 API
| 用途 | 位置 |
| --------------------------------- | ----------------------------------------------------------------- |
| 需求列表 | `/requirements` |
| 看板(按 tag) | `/requirements?view=board` |
| Tag sessions 管理 | `/requirements/extensions?tab=autowork` |
| 通知配置 | `/requirements/extensions?tab=notify` |
| 新建 / 编辑 | `/requirements?new=1``/requirements/:id/edit` |
| 旧版 `/autowork``/requirements/tag-sessions` | 重定向到 `/requirements/extensions?tab=autowork` |
| 旧版 `/requirements/new``/requirements/kanban` | 重定向到当前 query-param 路由 |
| 列出 / 创建需求 | `GET /api/requirements``POST /api/requirements` |
| Tags | `GET /api/requirements/tags` |
| Tag 绑定(管理) | `GET /api/requirements/tag-bindings` |
| Per-tag 看板 | `GET /api/requirements/board?tag=…` |
| 获取 / 更新 / 删除 | `GET|PUT|DELETE /api/requirements/:id` |
| 状态 / 完成 / 认领 | `POST /api/requirements/:id/status``…/complete``…/claim` |
| AutoWork 开关 / 状态 | `POST /api/requirements/autowork``GET …/autowork/:kind/:tid` |
| Webhooks | `GET|POST /api/webhooks``…/{id}``…/{id}/test` |
| Per-tag webhook | `GET|PUT /api/tags/:tag/settings` |
## 实现注记(写给好奇的你)
- `requirements.conversation_id` 有意 **不带外键** 指向 conversations 表。一条需求一旦创建,会随着重置回 pending 在多个会话之间轮转;用引用完整性把它绑死到单个 conversation 会让清理逻辑变得别扭,而且并不会带来真正的安全保障。请把该列视为参考性字段。
- 编排器的 `wake` Notify 与 `RequirementService` 共用;任何会重置回 pending 或创建工作的状态变更都会触发它,循环也会在每次 `claim_next()` 调用前后用 armed-then-await 的方式包起来,因此在"claim 返回 None"和"await"之间到达的唤醒永远不会丢。
- 终端注入仍用 bracketed-paste 标记把多行 prompt 作为单次粘贴落入,再在一拍之后单独写一个 `CR` 提交(与 paste 同批写入的 CR 会被现代 agent TUI 的 paste-burst 检测吞掉)。
- 无工具 engine 的 completion note 有上限(`MAX_NOTE_CHARS = 4000`)且 **偏向尾部**——智能体倾向于在末尾做总结,因此当需要截断时我们保留尾部。
+306
View File
@@ -0,0 +1,306 @@
# Channels
A **channel** lets you operate a NomiFun agent from an external chat app —
Telegram, Lark / 飞书, DingTalk, WeChat — instead of sitting in front of
the desktop window. You enable a plugin, paste in its credentials,
authorize a chat user with a one-time code, and from then on messages
to your bot are dispatched to the agent and its replies come back into
the same thread.
Channels are useful when:
- you want to brief an agent from your phone or a group chat;
- you want a workspace-aware agent reachable from a team's existing IM;
- you want long-running tasks ([AutoWork](./autowork-requirements.md))
to be kickable from outside the desktop without spinning up the WebUI.
> Each platform plugin is a Cargo feature on `nomifun-channel`
> (`telegram`, `lark`, `dingtalk`, `weixin`). The default NomiFun build
> ships with all of them on; if you build the backend yourself with a
> non-default feature set, the corresponding tab simply disappears.
![Channels settings overview](../images/channels-01-overview.png)
## Where to find it
Open the Nomi page (`/nomi`), select a companion, and switch to the
**Remote** tab (`/nomi?companion=<id>&tab=remote`). That tab lists the
remote connectors for the selected companion — built-in (Telegram,
Lark, DingTalk, WeChat, WeCom, Slack, Discord, extensions). For each
plugin you'll see:
- a status pill (`stopped` / `connected`),
- the bot username once connected,
- the number of currently authorised users,
- a per-channel **default agent** + **default model** selector.
Slack / Discord / WeCom appear as built-in placeholders today — the
backend wiring is feature-gated and still being built out for those
two; Telegram / Lark / DingTalk / WeChat are the ones you can run
today.
## How a channel works
```
external IM ──▶ plugin (long-poll / WebSocket)
ChannelManager ◀─▶ PairingService
SessionManager ──▶ agent / conversation
```
- **Plugin** owns the platform-specific connection (Telegram long-poll
with exponential backoff, Lark / DingTalk WebSocket, WeChat QR-code
login over SSE).
- **PairingService** turns "I'm John on Telegram, let me in" into a
6-digit code that you approve from the desktop UI.
- **SessionManager** maps `(platform_user, chat_id)` to an agent
conversation, so each external chat is a stable session and follow-up
messages land in the same agent.
- **Orchestrator** plumbs incoming messages into the agent stream and
the agent's replies back out as edits to the same IM message
(everything except WeChat supports message editing — WeChat falls
back to sending follow-up replies).
## Setting up each platform
### Telegram
1. Talk to [`@BotFather`](https://t.me/BotFather) and create a bot.
Save the token (looks like `123456:ABC-DEF…`).
2. In **Nomi → Remote → Telegram**, paste the token.
3. Click **Test** — the backend calls `getMe` and shows the bot
username on success.
4. Click **Enable**. The plugin starts long-polling
(25 s timeout, exponential backoff up to 10 reconnects).
To pair a Telegram user with the desktop, the user messages your bot;
the bot replies with a 6-digit code (10-minute TTL). Paste / type the
code into **Nomi → Remote → Pending pairings** on the desktop
and click **Approve**. From then on that Telegram user can chat with
the agent.
### Lark / Feishu
1. Create a custom app in the Lark developer console with the events
you need (text message, card action, bot menu).
2. Copy the **App ID**, **App Secret**, and (optional) **Encrypt key /
Verification token**.
3. Paste them into the Lark form in the Channels tab and click
**Enable**.
The Lark plugin connects via Lark's WebSocket long-connection (no
public webhook needed), with a 60-second event-dedup cleanup loop and
fragment reassembly. Replies are sent as **interactive cards** because
Lark's API only supports editing card messages.
### DingTalk
1. Create an internal app in DingTalk Developer Backstage with **Stream
Mode** enabled.
2. Copy the **Client ID** and **Client Secret** into the DingTalk form
and enable.
The DingTalk plugin opens a WebSocket using the standard DingTalk
stream-mode handshake; pairing flow is identical to Telegram.
### WeChat
1. WeChat is QR-code login. Click **Enable** on the WeChat plugin —
the backend opens an SSE stream (`POST /api/channel/weixin/login/start`)
that pushes QR-code refresh events.
2. Scan the QR with the WeChat app, confirm the login, and the plugin
transitions to `connected`.
WeChat does **not** support message editing — replies are delivered as
new messages in the same chat instead of in-place edits.
## Pairing and authorising users
A pairing request comes in two ways:
1. The platform user messages the bot for the first time (Telegram
/Lark / DingTalk). The plugin auto-creates a pending request and
replies to the user with the code.
2. You can approve / reject the pending request from
**Nomi → Remote → Pending pairings** or programmatically
via `POST /api/channel/pairings/approve` and
`POST /api/channel/pairings/reject`.
Approved users are listed in **Authorised users**, with `last active`.
You can revoke at any time (`POST /api/channel/users/revoke`); the
service also cleans up that user's open sessions so the next message
re-pairs from scratch.
![Pairing approval](../images/channels-02-pairing.png)
## Master Agent mode
By default, every channel conversation runs in **Master Agent mode**:
the remote message is greeted by the Nomi companion itself. The conversation
inherits the companion's personality and memories, and the agent is wired to
the **Desktop Gateway** tools, so from your phone you're not talking
to an isolated chat bot — you're talking to the agent that runs your
desktop.
What the gateway tools (all prefixed `nomi_*`, 32 of them today) let the
remote agent do on your behalf:
- **Conversations** — list every conversation with its runtime state,
inspect one (status plus the latest messages, including an in-flight
streaming reply), send a message or task prompt into any
conversation, create new ones, update or delete old ones
(`nomi_list_conversations`, `nomi_conversation_status`,
`nomi_send_to_conversation`, `nomi_create_conversation`,
`nomi_update_conversation`, `nomi_delete_conversation`).
- **Scheduled tasks** — list / create / update / delete cron jobs
(`nomi_cron_list`, `nomi_cron_create`, `nomi_cron_update`,
`nomi_cron_delete`).
- **Long-term memory** — read and write the companion's global memory bank
(`nomi_memory_list`, `nomi_memory_save`, `nomi_memory_update`,
`nomi_memory_delete`).
- **Requirements** — browse and manage the requirements platform
(`nomi_requirement_list`, `nomi_requirement_create`,
`nomi_requirement_update`, `nomi_requirement_delete`).
- **Terminals & supervision** — list terminal sessions, create new ones
(optionally binding knowledge bases via `knowledge_base_ids`), and
read / toggle a terminal's AutoWork binding and IDMM supervision
(`nomi_list_terminals`, `nomi_create_terminal`, `nomi_get_autowork`,
`nomi_set_autowork`, `nomi_get_idmm`, `nomi_set_idmm`).
- **Knowledge bases** — browse bases and bindings, rebind a
conversation / terminal / companion, create a new base, write markdown
files into one, trigger the AI digest, or fetch a URL as markdown —
so the companion can deposit knowledge on its own
(`nomi_knowledge_list_bases`, `nomi_knowledge_get_binding`,
`nomi_knowledge_set_binding`, `nomi_knowledge_create_base`,
`nomi_knowledge_write_file`, `nomi_knowledge_autogen`,
`nomi_knowledge_fetch_url`). `nomi_knowledge_create_base` with
`urls` fetches in the background — the call returns immediately, so
don't create the base a second time while waiting; the base's
description appearing means the fetch + digest pipeline is done.
- **Providers** — list the configured LLM providers
(`nomi_list_providers`).
So *"move my daily-report cron to 9 am and tell me what's running
right now"* is a single Lark message.
**Turning it off.** Each platform panel has a **Master Agent mode**
switch next to the default-model selector. It's on by default; the
preference is stored per platform as `assistant.<platform>.masterAgent`
in the client preferences (missing value = on). Switching it off
reverts that platform to the legacy behavior — each remote chat gets a
plain standalone conversation, with no companion persona and no gateway
tools. Like the model selector, toggling the switch calls
`POST /api/channel/settings/sync` and clears the platform's active
sessions, so the next inbound message starts a conversation in the new
mode.
**Choosing which companion greets the channel.** With [multiple companions](./companions.md),
bots are bound to companions **per channel row**: each row of
`assistant_plugins` is one bot (the same platform can host several —
e.g. one Feishu in-house app per companion), its `companion_id` decides which companion
answers, and the `UNIQUE(type, bot_key)` constraint structurally
guarantees **one bot is never bound to two companions** (bot identity: Feishu
`app_id`, the Telegram bot id, DingTalk `client_id`, …). Binding or
unbinding calls `POST /api/channel/settings/companion` with a `plugin_id`,
which persists the row and resets **that channel's** active sessions in
one step — the next inbound message is greeted by the new companion's persona,
model, and knowledge mounts (the conversation carries `extra.companionId`).
Connecting a bot from a companion's **Remote** tab creates the channel row and
binds it to that companion in one go. A row without a companion binding falls back
to the legacy per-platform preference `assistant.<platform>.companionId`, then
to the **default companion**; if the bound companion is later deleted, the channel
falls back to the default companion and the sessions are likewise reset.
Memory is shared across the whole companion family: no matter how many bots
and channels you connect, their conversations flow into the same single
memory pipeline, so switching companions never loses memories.
**How it relates to the agent / model pickers.** The per-platform
**Default agent** still decides which engine answers; the gateway
tools are injected for any agent type, while the companion persona and
memory ride on the Nomi engine. Model resolution in master mode:
the platform's **Default model** (if set) wins, otherwise the
conversation falls back to the bound companion's own model.
## Picking the agent and model
Each platform has a **Default agent** and **Default model** selector
in its config form. The platform stores them as
`assistant.<platform>.defaultModel` in the client config, so:
- a message from Telegram routes to whatever agent / model you picked
for Telegram;
- a message from Lark can route to a different agent;
- changing the selector calls `POST /api/channel/settings/sync`, which
clears any active sessions for that platform — the next inbound
message re-creates them with the new defaults.
The model selector is the same Gemini-flavoured component the desktop
uses, so any provider you've configured (Anthropic, OpenAI-compatible
custom URL, Gemini-with-Google-auth, Bedrock, …) is available here.
## What works from the IM side
The platform-agnostic abstraction (`UnifiedIncomingMessage`,
`UnifiedOutgoingMessage`, `UnifiedAction`) covers:
- **Plain text** — both directions.
- **Edited streaming responses** — incremental updates from the agent
are edited into the in-flight bot message (not on WeChat).
- **Action buttons** — confirmation prompts, retry actions, etc.,
rendered as inline keyboards (Telegram), interactive-card buttons
(Lark), or platform equivalents.
- **Bot mention / require-mention** — group chats can be configured
to only respond when the bot is `@`-mentioned.
What you don't get from the IM side (yet):
- spawning teams (use the desktop / web UI for that);
- file uploads beyond what the platform plugin natively understands;
- per-user workspace selection — the agent's workspace is the one set
on the conversation it routed to.
## Routes & API
| What | Where |
| ------------------------------- | ------------------------------------------------------- |
| Channels UI | `/nomi?companion=<id>&tab=remote` |
| List plugins / status | `GET /api/channel/plugins` |
| Enable / disable | `POST /api/channel/plugins/enable`, `…/disable` |
| Test credentials | `POST /api/channel/plugins/test` |
| Pending pairings | `GET /api/channel/pairings` |
| Approve / reject pairing | `POST /api/channel/pairings/approve`, `…/reject` |
| Authorised users | `GET /api/channel/users`, `POST .../users/revoke` |
| Active sessions | `GET /api/channel/sessions` |
| Sync (clear sessions on change) | `POST /api/channel/settings/sync` |
| Bind master-agent companion | `POST /api/channel/settings/companion` |
| WeChat QR login SSE | `POST /api/channel/weixin/login/start` |
## Notes
- Plugin lifecycle is a state machine —
`Created → Initializing → Ready → Starting → Running → Stopping → Stopped`,
with any step able to transition to `Error`. The status pill in the
UI is this enum.
- A revoked user's session is torn down before the user row is
deleted. The next message from that platform user will trigger a new
pairing code.
- Pairing codes are 6 digits, generated with `getrandom`, with a
10-minute TTL. The pairing service runs a periodic sweep that
expires pending codes whose TTL has passed.
- WeChat is feature-gated separately because its dependency tree is
heavier (QR / login / auth flow). If you build with
`--no-default-features`, you'll see the placeholder card but no
enable button.
## Related
- [Companions](./companions.md) — multi-companion management, shared memory, and the
per-companion knowledge bindings that ride on channel conversations.
- [AutoWork & Requirements](./autowork-requirements.md) — file a
requirement from a chat, get notified when it lands via a webhook to
Lark / HTTP / Slack (configured at **需求平台 → 扩展能力 → 通知**).
- [Web Server Deployment](./web-server-deployment.md) — exposes the
same channels when you self-host the backend on a server.
+208
View File
@@ -0,0 +1,208 @@
# Channels
通过 **channel**,你可以从外部聊天应用——Telegram、Lark / 飞书、钉钉、微信——操作 NomiFun 的智能体,而不必坐在桌面客户端前面。你启用一个插件,粘贴它的凭证,用一次性验证码授权一个聊天用户,从此发到你机器人的消息就会被分发到智能体,智能体的回复也会回到同一个会话。
Channel 适用于以下场景:
- 你想从手机或群聊里给智能体下达指令;
- 你希望让一个工作区感知的智能体能从团队现有 IM 中触达;
- 你希望长时任务([AutoWork](./autowork-requirements.zh.md))能从桌面之外被发起,而不必启动 WebUI。
> 每个平台插件都是 `nomifun-channel` 上的一个 Cargo feature`telegram`、`lark`、`dingtalk`、`weixin`)。NomiFun 的默认构建把它们全部打开;如果你用非默认 feature 集合自行构建后端,对应的 tab 就直接消失。
![Channels 设置总览](../images/channels-01-overview.png)
## 在哪里找
打开 Nomi 页面(`/nomi`),选择一只伙伴,然后进入 **Remote** tab`/nomi?companion=<id>&tab=remote`)。这个 tab 会列出该伙伴可用的远程连接器——内置的(Telegram、Lark、DingTalk、WeChat、WeCom、Slack、Discord、扩展)。每个插件你能看到:
- 一个状态药丸(`stopped` / `connected`);
- 连接成功后的 bot 用户名;
- 当前已授权用户数;
- 一个 per-channel 的 **默认 agent** + **默认模型** 选择器。
Slack / Discord / WeCom 目前作为内置占位符出现——这两者的后端接线被 feature gate 覆盖且仍在搭建中;今天可用的是 Telegram / Lark / DingTalk / WeChat。
## channel 是怎么工作的
```
external IM ──▶ plugin (long-poll / WebSocket)
ChannelManager ◀─▶ PairingService
SessionManager ──▶ agent / conversation
```
- **Plugin** 持有平台特定连接(Telegram 长轮询带指数退避,Lark / 钉钉 WebSocket,微信通过 SSE 上的 QR-code 登录)。
- **PairingService** 把"我是 Telegram 上的 John,让我进来"变成一个由你在桌面 UI 上批准的 6 位验证码。
- **SessionManager** 把 `(platform_user, chat_id)` 映射到一个智能体会话,因此每个外部聊天都是一个稳定 session,后续消息落到同一个智能体。
- **Orchestrator** 把进入的消息接到智能体流,并把智能体的回复以"对同一条 IM 消息编辑"的形式送回(除微信外都支持消息编辑——微信会回退为发送追加回复)。
## 各平台配置步骤
### Telegram
1. 找 [`@BotFather`](https://t.me/BotFather) 创建一个 bot,保存 token(形如 `123456:ABC-DEF…`)。
2.**Nomi → Remote → Telegram** 粘入 token。
3.**Test**——后端会调 `getMe`,成功后显示 bot 用户名。
4.**Enable**。插件开始长轮询(25 s 超时,指数退避,最多 10 次重连)。
为了把 Telegram 用户与桌面端配对:用户给你的 bot 发消息;bot 用一个 6 位验证码(10 分钟 TTL)回复。在桌面端的 **Nomi → Remote → Pending pairings** 中粘入或键入该验证码并点 **Approve**。从此该 Telegram 用户即可与智能体对话。
### Lark / 飞书
1. 在飞书开发者控制台创建一个自定义 app,开启你需要的事件(文本消息、卡片动作、bot 菜单)。
2. 复制 **App ID**、**App Secret**,以及(可选)**Encrypt key / Verification token**。
3. 把它们填入 Channels tab 中的 Lark 表单,点 **Enable**
Lark 插件通过飞书的 WebSocket 长连接接入(无需公网 webhook),带一个 60 秒的事件去重清理循环和分片重组。回复以 **互动卡片** 形式发送,因为飞书 API 只支持编辑卡片消息。
### 钉钉
1. 在钉钉开发者后台创建一个内部 app,启用 **Stream Mode**
2.**Client ID****Client Secret** 填入 DingTalk 表单并启用。
钉钉插件通过标准 stream-mode 握手打开 WebSocket;配对流程与 Telegram 一致。
### 微信
1. 微信用 QR-code 登录。在 WeChat 插件上点 **Enable**——后端会打开一个 SSE 流(`POST /api/channel/weixin/login/start`)推送 QR-code 刷新事件。
2. 用微信 app 扫码确认登录,插件转为 `connected`
微信 **不支持** 消息编辑——回复以新消息形式投递到同一聊天,而不是就地编辑。
## 配对与授权用户
配对请求有两种来源:
1. 平台用户首次给 bot 发消息(Telegram / Lark / 钉钉)。插件自动创建一份待处理请求,并把验证码回复给用户。
2. 你可以在 **Nomi → Remote → Pending pairings** 中批准 / 拒绝待处理请求,或以编程方式调用 `POST /api/channel/pairings/approve``POST /api/channel/pairings/reject`
已批准用户会出现在 **Authorised users** 中,并显示 `last active`。你可以随时撤销(`POST /api/channel/users/revoke`);服务也会清理该用户的活跃 session,使下一条消息从头开始重新配对。
![配对批准](../images/channels-02-pairing.png)
## 主 Agent 模式 (Master Agent)
默认情况下,每个 channel 会话都运行在 **主 Agent 模式**:远程消息由
Nomi 伙伴本尊接待。会话继承伙伴的人格与记忆,并且 agent 接上了
**Desktop Gateway** 工具——所以你在手机上对话的不是一个孤立的聊天
bot,而是那个掌管你整个桌面的 agent。
网关工具(统一前缀 `nomi_*`,目前共 32 个)能替你做的事:
- **会话**——列出所有会话及其运行态,查看单个会话(状态 + 最近消息,
含进行中的流式回复),向任意会话注入消息或任务 prompt,新建会话,
修改与删除旧会话(`nomi_list_conversations``nomi_conversation_status`
`nomi_send_to_conversation``nomi_create_conversation`
`nomi_update_conversation``nomi_delete_conversation`)。
- **定时任务**——列出 / 创建 / 修改 / 删除 cron 任务
`nomi_cron_list``nomi_cron_create``nomi_cron_update`
`nomi_cron_delete`)。
- **长期记忆**——读写伙伴的全局记忆库(`nomi_memory_list`
`nomi_memory_save``nomi_memory_update``nomi_memory_delete`)。
- **需求平台**——浏览与管理需求平台(`nomi_requirement_list`
`nomi_requirement_create``nomi_requirement_update`
`nomi_requirement_delete`)。
- **终端与监督**——列出终端会话、创建新终端(可经 `knowledge_base_ids`
顺带绑定知识库),以及读取 / 切换某个终端的 AutoWork 绑定与 IDMM
监督(`nomi_list_terminals``nomi_create_terminal`
`nomi_get_autowork``nomi_set_autowork``nomi_get_idmm`
`nomi_set_idmm`)。
- **知识库**——浏览知识库与绑定关系,改绑会话 / 终端 / 伙伴,新建
知识库,向库内写 markdown 文件,触发 AI 梗概生成,或把一个 URL
抓取为 markdown——伙伴可以自主沉淀知识
`nomi_knowledge_list_bases``nomi_knowledge_get_binding`
`nomi_knowledge_set_binding``nomi_knowledge_create_base`
`nomi_knowledge_write_file``nomi_knowledge_autogen`
`nomi_knowledge_fetch_url`)。`nomi_knowledge_create_base`
`urls` 时抓取为后台异步——工具立即返回,等待期间勿重复建库;
库描述(description)出现即代表抓取与梗概流水线已完成。
- **Provider**——列出已配置的 LLM provider`nomi_list_providers`)。
于是"把我的日报 cron 改到早上 9 点,再说说现在桌面上有什么在跑"
只需要一条飞书消息。
**如何关闭。** 每个平台面板里,默认模型选择器旁边有一个
**主 Agent 模式** 开关。默认开启;偏好按平台存为客户端配置中的
`assistant.<platform>.masterAgent`(无值 = 开启)。关闭后该平台回退
为旧行为——每个远程聊天只得到一个普通独立会话,没有伙伴人格也没有
网关工具。与模型选择器一样,切换开关会调
`POST /api/channel/settings/sync` 并清掉该平台的活跃 session,下一条
进来的消息会以新模式重新创建会话。
**选择由哪只伙伴接待。** 有了[多伙伴](./companions.zh.md)之后,机器人按
**渠道行**绑定伙伴:`assistant_plugins` 每行代表一个机器人(同一平台
可以接入多个机器人,比如飞书上为每只伙伴各开一个企业自建应用),行上
`companion_id` 决定由哪只伙伴接待,`UNIQUE(type, bot_key)` 唯一约束从结构
上保证**同一个机器人永远不会被绑到第二只伙伴**(bot 身份:飞书
`app_id`、Telegram bot id、钉钉 `client_id`……)。绑定 / 解绑走
`POST /api/channel/settings/companion`(带 `plugin_id`),一步完成持久化与
**该渠道** session 的重置——下一条进来的消息由新宠的人格、模型与知识
库挂载接待(会话带 `extra.companionId`)。在伙伴面板的 **远程连接** tab 里
为某只伙伴连接机器人,就是「新建渠道行 + 绑定该宠」一步完成。未绑定
伙伴的渠道行回退到旧的平台级偏好 `assistant.<platform>.companionId`,再回退
**默认伙伴**;被绑定的伙伴若之后被删除,自动回退默认宠并同样重置
session。记忆是全家共享的:不管多少个机器人、多少个渠道,会话数据都
汇入同一套记忆体系,换宠不会丢失任何记忆。
**与 agent / 模型配置的关系。** 平台级 **默认 agent** 仍然决定由哪个
引擎应答;网关工具对任意 agent 类型都会注入,而伙伴人格与记忆搭载在
Nomi 引擎上。主 Agent 模式下的模型解析顺序:平台 **默认模型**(若已
设置)优先,否则回退到所绑定伙伴自己的模型。
## 选择 agent 和模型
每个平台都在它的配置表单里有 **默认 agent****默认模型** 选择器。平台把它们存为客户端配置中的 `assistant.<platform>.defaultModel`,因此:
- 来自 Telegram 的消息路由到你为 Telegram 选的智能体 / 模型;
- 来自飞书的消息可以路由到一个不同的智能体;
- 改动选择器会调 `POST /api/channel/settings/sync`,它会清掉该平台的活跃 session——下一条进来的消息会用新的默认值重新创建。
模型选择器与桌面端使用的是同一个 Gemini 风味组件,所以你配置过的任何 providerAnthropic、OpenAI 兼容自定义 URL、带 Google 认证的 Gemini、Bedrock,……)这里都可用。
## 从 IM 端能做什么
平台无关抽象(`UnifiedIncomingMessage``UnifiedOutgoingMessage``UnifiedAction`)覆盖:
- **纯文本**——双向。
- **流式编辑回复**——智能体的增量更新会被编辑进正在飞行的 bot 消息(微信除外)。
- **动作按钮**——确认 prompt、重试动作等等,渲染为 inline keyboardTelegram)、互动卡片按钮(Lark)或对应平台的等价物。
- **Bot mention / require-mention**——群聊可配置为只在 bot 被 `@` 时才回应。
从 IM 端目前还做不到:
- 创建 team(请用桌面 / web UI);
- 超出平台插件原生能力的文件上传;
- per-user 工作区选择——智能体的工作区就是它路由到的会话上设的那个。
## 路由与 API
| 用途 | 位置 |
| ------------------------------- | ---------------------------------------------------------- |
| Channels UI | `/nomi?companion=<id>&tab=remote` |
| 列出插件 / 状态 | `GET /api/channel/plugins` |
| 启用 / 禁用 | `POST /api/channel/plugins/enable``…/disable` |
| 测试凭证 | `POST /api/channel/plugins/test` |
| 待处理配对 | `GET /api/channel/pairings` |
| 批准 / 拒绝配对 | `POST /api/channel/pairings/approve``…/reject` |
| 已授权用户 | `GET /api/channel/users``POST .../users/revoke` |
| 活跃 session | `GET /api/channel/sessions` |
| 同步(变更时清掉 session) | `POST /api/channel/settings/sync` |
| 绑定主 Agent 伙伴 | `POST /api/channel/settings/companion` |
| 微信 QR 登录 SSE | `POST /api/channel/weixin/login/start` |
## 注记
- 插件生命周期是一个状态机——`Created → Initializing → Ready → Starting → Running → Stopping → Stopped`,每一步都可能转到 `Error`。UI 上的状态药丸就是这个枚举。
- 撤销用户时,session 会先于该 user row 被拆掉。来自该平台用户的下一条消息会触发新的配对码。
- 配对码 6 位,由 `getrandom` 生成,TTL 10 分钟。配对服务运行一个周期清扫,把 TTL 已过的待处理码过期掉。
- 微信单独被 feature gate 控制,因为它的依赖树更重(QR / 登录 / 鉴权流)。如果你用 `--no-default-features` 构建,会看到占位卡片但没有启用按钮。
## 相关
- [伙伴(Companions](./companions.zh.md)——多伙伴管理、共享记忆,以及搭载在渠道会话上的每宠知识库绑定。
- [AutoWork & Requirements](./autowork-requirements.zh.md)——从聊天里登记一条需求,再用 webhook 卡片把通知打回飞书。
- [Web Server Deployment](./web-server-deployment.zh.md)——当你在服务器上自托管后端时同样能暴露这些 channel。
+253
View File
@@ -0,0 +1,253 @@
# Companions
Nomi's virtual companion has grown from "a single nomi" into a **multi-companion
family**: you can create several companions, use them side by side, raise
them separately, and give each its own name, character, persona, and
chat model. Each companion can also be bound to its own **dedicated knowledge
bases** (turning it into a finance companion, a literature companion, a coding companion,
…), while every companion **shares one memory hub** — collection and learning
run as a single global pipeline, so whatever one companion learns, the whole
family remembers. Memories, companions, and knowledge bases can each be
packed into a `.zip` bundle for export/import, making machine-to-machine
migration painless.
> The entry point is the **Desktop Companion** page in the sidebar (the `/nomi`
> route); the right-click menu of any desktop companion window ("Open chat")
> deep-links there too.
## Page layout: companion switcher + two tab domains
The top of the Desktop Companion page is the **companion switcher bar**: one card per companion
(character thumbnail + name + level) plus a **New companion** button. The
selected companion drives the **companion-domain** tabs below; everything that is
global lives in the **shared-domain** tabs:
| Domain | Tab | Contents |
| --- | --- | --- |
| Companion domain (follows the switcher) | Overview | **Desktop-companion toggle** + that companion's level / XP / mood + shared stats |
| | Chat | That companion's own companion threads |
| | Model & Knowledge | Chat model picker / **knowledge bindings** |
| | Remote | That companion's IM bots (bound per companion — see the [channels guide](./channels.md)) |
| | Settings | Name / character / persona / quiet hours / delete companion |
| Shared domain (one per install) | Memories · Collect · Learn · Suggestions | The shared memory hub (one copy for all companions) |
| | Migrate | Export / import migration bundles (see below) |
## Creating and managing companions
1. Click **New companion** on the switcher bar, pick a name and one of the
six characters (mochi / ink / roux / pixel / bolt / boo).
2. **The first companion automatically becomes the default companion** (its card
carries a "default" badge). The default companion is the fallback whenever
a channel has no explicit binding (see the channels section below).
3. In a companion's **Settings** tab you can rename it at any time (takes
effect immediately), swap the character, tune the persona (preset or
custom), **pick a chat model just for this companion**, and toggle the
desktop companion plus its quiet hours.
4. **Deleting a companion** cascades: its companion conversations, runtime
state (XP, …), and `('companion', companionId)` knowledge bindings are removed
together; if you delete the default companion, the default role moves on
to the next one. Deleting down to zero companions is allowed (the shared
memory hub exists independently of companions — collection and learning
keep running).
On disk each companion is a directory — `{data_dir}/companion/companions/{companion_id}/config.json`,
**the directory is the source of truth** — which is also the unit the
companion bundle exports and imports.
### Multiple desktop companions on screen
Every companion with the desktop-companion switch enabled gets its own desktop
window (transparent, always-on-top, draggable; window label
`companion-{companionId}`). Several can share the screen; keeping it to 5 or fewer
is recommended (each window is an independent WebView instance — the
UI warns about performance beyond that but does not enforce a limit).
Right-click any desktop companion to jump straight to its chat.
## The shared memory hub
All companions share one set of memory facilities under
`{data_dir}/companion/shared/`:
- **Collection** — a single pipeline subscribes to the global event
bus, gathers your working data according to the collect switches,
and writes `shared/events/YYYYMMDD.jsonl`.
- **Learning** — a single learner incrementally distills events into
long-term memories on the configured interval, stored in
`shared/memory.db`. The learning pipeline uses the **learn model
from the shared config** (independent of each companion's chat model — one
pipeline, one budget).
- Memories saved during any companion's chat, and memories produced by
learning, are **visible to every companion** — switch companions mid-stream and
the new one remembers everything that happened before.
### XP and mood attribution
| Source | Credited to |
| --- | --- |
| Learning-run output (scored by events processed + new memories) | **All companions** (the family grows together) |
| Suggestion adopted (+20) | **All companions** |
| Companion chat turn (+2) | Only the companion in that conversation |
| Memory saved during chat (+5) | Only that companion |
**Mood is global**: it is produced by learning runs and stored in
shared state, so all companions share one mood (per-companion mood/personality
divergence is reserved for a later version).
## Binding knowledge bases to a companion
In a companion's **Model & Knowledge tab → Knowledge** section, use the binding
control to mount one or more knowledge bases on that companion (the binding
is `('companion', companionId)`). Scope of effect:
- The companion's **companion chats** and the **channel conversations** it
greets (conversations carrying `extra.companionId`) mount that companion's bound
knowledge bases — searchable during the conversation. Regular
conversations without a companionId keep their conversation-level bindings;
the two are **not merged**.
- **What the agent sees**: bases are mounted at
`{workspace}/.nomi/knowledge/`, and the injected context carries, per
base, the description + an AI digest + "when to consult" hints + a
budgeted table of contents (20 entries per base / 60 global,
directories aggregated beyond that), plus an explicit retrieval
protocol — the agent is told to look things up rather than answer
from memory.
- **Write-back** comes in two modes, briefly:
- **staged** — knowledge produced during a conversation first lands
in the base's `_inbox/` (isolated per conversation) for you to
review on the knowledge page before it is committed;
- **direct** — skips staging and writes straight into the base.
- **AI bootstrap**: the **AI generate** button on the knowledge page
(list edit modal and detail page) calls
`POST /api/knowledge/bases/{id}/autogen` to produce the base's
description and `README.md`; a `.zip` import auto-fills an empty
description. Requires a configured AI provider (`409` otherwise).
- **URL sources**: a base can be created from up to 16 URLs.
*snapshot* mode fetches them at creation, converts each page to
markdown under the base's `snapshots/` (pages over 32 KB are
AI-compressed) and auto-generates the digest — refreshable from the
detail page; *live* mode lets the agent fetch at runtime (engines
without a web tool can call the gateway tool
`nomi_knowledge_fetch_url`). Only public `http/https` URLs are
accepted (SSRF guard).
- The companion can also **grow its own libraries**: the Desktop Gateway
ships seven knowledge tools (list / bindings / create / write /
autogen / fetch-url), and knowledge-deposit tips are built into the
companion's system prompt — a companion or channel chat can create a base
and distill notes into it unprompted. When
`nomi_knowledge_create_base` is called with `urls`, the fetching runs
as a background job — the tool returns immediately, so the agent must
not create the base again just because the snapshots haven't appeared
yet; once the base's description shows up, the fetch + digest
pipeline has finished.
Bind different bases to different companions and you get a "finance companion", a
"literature companion", a "coding companion" — persona, model, and knowledge are
all per-companion, while memory stays shared.
## Binding a companion to a channel
Each IM platform (Telegram / Lark / DingTalk / WeChat) can bind its own
greeter companion for remote messages: open the companion's **Remote**
tab (`/nomi?companion=<id>&tab=remote`) and connect or rebind the bot
there. The binding is still persisted as `assistant.{platform}.companionId`
for legacy platform-level preferences when a channel row has no direct
companion binding. With no binding the **default companion** takes over;
switching the binding resets that channel's active sessions (the next
message is greeted by the new companion); if a bound companion is deleted,
the platform falls back to the default companion and the sessions are
likewise reset. See the "Master Agent mode" section of the
[Channels guide](./channels.md).
> A companionId grants no permissions (memory is shared anyway): it only
> selects persona / model / knowledge mounts — unlike the
> `desktopGateway` marker, which grants gateway tools.
## Export / import: migrating between machines
The shared-domain **Migrate** tab offers three kinds of `.zip` bundles
(the migration UI is desktop-only; paths are picked with the system
dialog):
| Bundle | Contents | Import semantics |
| --- | --- | --- |
| **Memory bundle** | All long-term memories + learning history + mood; **optionally** the raw event data (checkbox) | **Merged with dedup** into local memories (original timestamps and sources preserved) |
| **Companion bundle** | One companion's persona / character / settings / XP + the **name list** of its bound knowledge bases (`knowledge_refs`) | Creates a new companion under a fresh id, name conflicts get a "(2)" suffix; knowledge refs are matched **by name** against local bases to rebuild bindings — unmatched names are listed so you can import those knowledge bundles first and bind manually |
| **Knowledge-base bundle** | Base metadata + the md file tree verbatim | Lands as a new knowledge base, name conflicts get "(2)" |
Migration steps:
1. Old machine: export the **memory bundle** (tick events only if you
want them) → export a **companion bundle** per companion → export a
**knowledge-base bundle** per base.
2. New machine: import the **knowledge-base bundles** first (so companion
bundles can rebuild bindings by name) → then the **companion bundles**
then the **memory bundle**.
3. Check each companion's model setting: model config travels verbatim in
the bundle, but if the new machine has no matching provider it shows
as unconfigured — re-select in settings.
### Privacy boundaries
- `events/*.jsonl` is **raw collected data containing your working
content verbatim** — it is **not** exported by default; it only
enters the memory bundle when you explicitly tick "include raw event
data".
- **Chat history does not travel with the companion bundle**: companion
conversation logs live in the main database; the companion bundle carries
only persona and settings. Chat logs stay on the original machine.
## Automatic migration of legacy data
After upgrading from the single-companion version, the first boot detects the
legacy layout `{data_dir}/companion/nomi/`: if it exists and `companion/shared/`
does not, it is automatically migrated into the shared memory hub plus
a first companion (default name **"Nomi"**, inheriting the existing XP /
persona / character / model / desktop-companion position / companion
threads). The migration is idempotent and re-entrant; on completion a
`.migrated` marker is written into the legacy directory, which is kept
around (to be cleaned up after one release cycle). No manual action is
needed.
## Manual walkthrough checklist
To verify a multi-companion setup end to end, walk through in order:
1. **Create two companions**: create companions A and B, rename them, change
characters; confirm the first one carries the "default" badge.
2. **Bind one base each**: bind knowledge base X to A and Y to B (companion
Model & Knowledge tab → Knowledge).
3. **Retrieval isolation**: in A's and B's chats, ask about content
that only exists in X / Y respectively; confirm A only hits X and B
only hits Y.
4. **Shared memory round-trip**: in A's chat, have it remember
something (save a memory); switch to B's chat and ask — confirm B
knows it.
5. **Export/import roundtrip**: export the memory bundle + A's companion
bundle + base X's bundle; (on a new machine or after a wipe) import
in the order knowledge base → companion → memory; confirm the rebuilt A
has its binding restored automatically and memories merge without
duplicates.
6. **Channel companion switch**: on some channel platform, switch the greeter
companion from A to B; confirm the active sessions are reset and the next
remote message is greeted with B's persona and B's knowledge mounts.
## Routes & API
| What | Where |
| --- | --- |
| List / create companions | `GET/POST /api/companion/companions` |
| Companion detail / update / delete | `GET/PATCH/DELETE /api/companion/companions/{companionId}` |
| Shared config (collect / learn / default companion) | `GET/PATCH /api/companion/config` |
| Per-companion companion threads | `GET /api/companion/companions/{companionId}/companion/threads`, `…/companion/active` |
| Export memory bundle | `POST /api/companion/export/memory` (`{dest_path, include_events}`) |
| Export companion bundle | `POST /api/companion/export/companions/{companionId}` |
| Import memory / companion bundle | `POST /api/companion/import` (dispatched by manifest.kind) |
| Export / import knowledge-base bundle | `POST /api/knowledge/bases/{id}/export`, `POST /api/knowledge/bases/import` |
| Bind a companion to a channel | `POST /api/channel/settings/companion` |
## Related
- [Channels](./channels.md) — channel Master Agent mode and per-platform
companion binding.
- [Data and Storage](../architecture/data-and-storage.md) — the `companion/`
data directory layout.
+127
View File
@@ -0,0 +1,127 @@
# 伙伴(Companions
NomiFun 的数字伙伴从「单个 nomi」升级为**多伙伴家庭**:你可以创建多个伙伴并同时使用、分别培养、自定义名称/形象/人格,每个伙伴可以使用自己的聊天模型、绑定自己的**专属知识库**(演进出金融/文学/coding/情感等专业伙伴);而所有伙伴**共享同一个记忆中枢**——采集与学习是一条全局链路,任何一个伙伴学到的东西全家都记得。记忆、伙伴、知识库都可以打包成 `.zip` 导出/导入,换机平滑迁移。
> 入口是侧边栏的 **桌面伙伴** 页(即 `/nomi` 路由);任意桌面伙伴窗口右键菜单的「打开聊天」也会深链到这里。
## 页面结构:伙伴切换条 + 双域 Tab
「桌面伙伴」页顶部是**伙伴切换条**:每个伙伴一张卡片(形象缩略图 + 名字 + 等级),加一个「新建伙伴」按钮。当前选中的伙伴驱动下面的**伙伴域** Tab;与伙伴无关的全局数据归**共享域** Tab:
| 域 | Tab | 内容 |
| --- | --- | --- |
| 伙伴域(随切换条变化) | 总览 | **桌面伙伴开关** + 该伙伴的等级 / XP / mood + 共享统计 |
| | 聊天 | 该伙伴自己的陪伴会话线程 |
| | 模型&知识 | 聊天模型选择 / **知识库绑定** |
| | 远程连接 | 该伙伴的 IM 机器人(按伙伴绑定,详见[渠道指南](./channels.zh.md) |
| | 设置 | 名称 / 形象 / 人格 / 勿扰 / 删除伙伴 |
| 共享域(全局唯一) | 记忆 · 数据采集 · 学习 · 建议 | 共享记忆中枢(所有伙伴同一份) |
| | 迁移 | 导出 / 导入迁移包(见下文) |
## 创建与管理多伙伴
1. 点伙伴条上的**新建伙伴**,起个名字、挑一个形象(mochi / ink / roux / pixel / bolt / boo 六款)即可。
2. **第一个伙伴自动成为默认伙伴**(卡片带「默认」徽标)。默认伙伴是渠道未显式绑定时的回退对象(见下文渠道一节)。
3. 在该伙伴的**设置** Tab 里可以随时改名(即时生效)、换形象、调人格(预设或自定义)、**为这个伙伴单独选聊天模型**、开关桌面伙伴窗口与勿扰时段。
4. **删除伙伴**会级联清理:它的陪伴会话、运行时状态(XP 等)、`('companion', companionId)` 知识库绑定一并移除;若删的是默认伙伴,默认资格自动顺延给下一个。删除允许删到零个(共享记忆中枢独立于伙伴存在,采集/学习照常运行)。
每个伙伴在磁盘上是一个目录:`{data_dir}/companion/companions/{companion_id}/config.json`,**目录即真相**——这也是伙伴包导出/导入的单位。
### 多个桌面伙伴同屏
每个开启了桌面伙伴开关的伙伴拥有自己的桌面窗口(透明、置顶、可拖动,窗口 label 为 `companion-{companionId}`)。多个可以同屏共处;建议同屏不超过 5 个(每个窗口是独立的 WebView 实例,开太多影响性能,UI 会提示但不硬限)。右键任意桌面伙伴窗口可直达它的聊天页。
## 共享记忆中枢
所有伙伴共用 `{data_dir}/companion/shared/` 下的同一套记忆设施:
- **采集**:单条链路订阅全局事件总线,按开关采集你的工作数据,写入 `shared/events/YYYYMMDD.jsonl`
- **学习**:单个学习器按设定间隔增量蒸馏事件为长期记忆,存入 `shared/memory.db`。学习链路使用**共享配置里的学习模型**(与每个伙伴的聊天模型相互独立,单链路单预算)。
- 任何一个伙伴聊天时保存的记忆、学习产出的记忆,**对全体伙伴可见**——换一个伙伴继续聊,它记得之前发生的一切。
### XP 与 mood 的归属规则
| 来源 | 归属 |
| --- | --- |
| 学习 run 产出(按处理事件数 + 新记忆数计分) | **所有伙伴**(家庭共同成长) |
| 建议被采纳(+20 | **所有伙伴** |
| 陪伴聊天轮次(+2) | 仅参与对话的那个伙伴 |
| 聊天中保存记忆(+5) | 仅该伙伴 |
**mood 是全局的**:由学习 run 产出、存共享状态,所有伙伴同一 mood(按伙伴分化的人格化 mood 留待后续版本)。
## 给伙伴绑定知识库
在伙伴的**模型&知识 Tab → 知识库**区域,用绑定控件为这个伙伴挂载一个或多个知识库(绑定关系为 `('companion', companionId)`)。生效范围:
- 该伙伴的**陪伴聊天**与它接待的**渠道会话**(会话上带 `extra.companionId`)都会挂载这个伙伴绑定的知识库——对话时可检索;不带 companionId 的普通会话维持原有的会话级绑定,二者**不合并**。
- **agent 看到什么**:库挂载到 `{workspace}/.nomi/knowledge/`,注入的上下文按库携带 描述 + AI 梗概 +「何时查阅」提示 + 按预算的目录(每库 20 条 / 全局 60 条,超出按目录聚合),外加一份显式检索协议——要求 agent 先查再答,而不是凭记忆作答。
- **回写(回血)**两种模式,简述如下:
- **staged(暂存)**——对话中产生的知识回写先落入知识库的 `_inbox/`(按会话隔离),由你在知识库页面审阅后入库;
- **direct(直写)**——跳过暂存直接写入知识库正文。
- **AI 自动生成**:知识库页面的「AI 生成」按钮(列表编辑 Modal 与详情页都有)调 `POST /api/knowledge/bases/{id}/autogen`,生成库的描述与 `README.md``.zip` 导入会自动补全空描述。需要已配置 AI Provider(否则返回 `409`)。
- **URL 知识源**:创建知识库时可给出最多 16 条 URL。*snapshot* 模式在创建时抓取并把每页转为 markdown 落入库的 `snapshots/`(超过 32 KB 的页面由 AI 压缩),并自动生成梗概——详情页可刷新快照;*live* 模式留给 agent 运行期实时抓取(无网络工具的引擎可调网关工具 `nomi_knowledge_fetch_url`)。仅接受公网 `http/https` URLSSRF 防护)。
- 伙伴也能**自己养库**Desktop Gateway 提供 7 个知识工具(列表 / 绑定 / 建库 / 写文件 / AI 生成 / 抓取 URL),且伙伴系统提示里内置了「知识沉淀技巧」——陪伴或渠道聊天中它可以不经吩咐就建库并把心得沉淀进去。注意 `nomi_knowledge_create_base``urls` 建库时,URL 抓取在**后台异步**执行——工具立即返回,agent 勿因快照尚未出现而重复建库;库描述(description)生成出来即代表抓取与梗概流水线已完成。
给不同的伙伴绑不同的库,就得到了「金融伙伴」「文学伙伴」「coding 伙伴」——人格、模型、知识三件套都按伙伴独立,记忆共享。
## 渠道绑定伙伴
每个 IM 平台(Telegram / Lark / 钉钉 / 微信)可以各绑一个伙伴来接待远程消息:打开该伙伴的 **Remote** tab`/nomi?companion=<id>&tab=remote`),在那里连接或改绑 bot。未绑定渠道行时仍会读取旧的平台级偏好 `assistant.{platform}.companionId` 作为兼容回退。未绑定时回退**默认伙伴**;切换绑定会重置该渠道的活跃会话(下一条消息由新伙伴接待);被绑定的伙伴若被删除,自动回退默认伙伴并同样重置会话。详见 [Channels 指南](./channels.zh.md)的「主 Agent 模式」一节。
> companionId 不授予任何权限(记忆本就共享):它只决定 persona / 模型 / 知识库挂载,与授予网关工具的 `desktopGateway` 标记性质不同。
## 导出 / 导入:换机迁移
共享域的**迁移** Tab 提供三种 `.zip` 迁移包(仅桌面版提供迁移 UI;路径用系统对话框选取):
| 包 | 内容 | 导入语义 |
| --- | --- | --- |
| **记忆包** | 全部长期记忆 + 学习历史 + mood;**可选**勾选包含原始事件数据 | 与本机记忆**合并去重**(保留原时间戳与来源) |
| **伙伴包** | 单个伙伴的人格 / 形象 / 设置 / XP + 它绑定的知识库**名称清单**(`knowledge_refs`) | 以新 id 创建新伙伴,名称冲突自动缀 "(2)";知识库引用**按名称**匹配本机已有库自动重建绑定,匹配不到的列出来提示你先导入对应知识库包再手动绑定 |
| **知识库包** | 知识库元数据 + md 文件树原样 | 新建知识库落地,名称冲突缀 "(2)" |
换机迁移步骤:
1. 旧机:导出**记忆包**(按需勾选事件数据)→ 逐个导出**伙伴包** → 逐库导出**知识库包**。
2. 新机:先导入**知识库包**(让伙伴包的绑定重建能按名匹配上)→ 导入**伙伴包** → 导入**记忆包**。
3. 检查每个伙伴的模型设置:模型配置原样随包带走,但若新机没有配置对应 provider,会显示未配置,需在设置里重选。
### 隐私边界
- `events/*.jsonl` 是**原始采集数据,包含你的工作内容原文**——默认**不**导出,只有显式勾选「包含原始事件数据」才会进记忆包。
- **聊天历史不随伙伴包迁移**:陪伴会话记录存在主数据库里,伙伴包只带人格与设置。聊天记录留在原机。
## 旧版数据自动迁移
从单伙伴版本升级后,首次启动会自动检测旧布局 `{data_dir}/companion/nomi/`:若存在且尚无 `companion/shared/`,自动迁移为共享记忆中枢 + 第一个伙伴(默认名 **"Nomi"**,继承原有 XP / 人格 / 形象 / 模型 / 桌面伙伴位置 / 陪伴会话线程)。迁移幂等可重入,完成后在旧目录写入 `.migrated` 标记并保留原目录(一个版本周期后清理)。无需任何手工操作。
## 手工走查清单
验证一套多伙伴部署是否健康,按序走一遍:
1. **建两个伙伴**:新建 A、B 两个伙伴,分别改名、换形象;确认第一个带「默认」徽标。
2. **各绑一库**:给 A 绑知识库 X、给 B 绑知识库 Y(伙伴模型&知识 Tab → 知识库)。
3. **各自检索**:分别在 A、B 的聊天里提问只在 X / Y 中存在的内容,确认 A 只命中 X、B 只命中 Y。
4. **共享记忆互通**:在 A 的聊天里让它记住一件事(保存记忆),切到 B 的聊天提问,确认 B 知道。
5. **导出导入 roundtrip**:导出记忆包 + A 的伙伴包 + 知识库 X 的包;(换机或清空后)按「知识库 → 伙伴 → 记忆」顺序导入,确认 A 重建后绑定自动恢复、记忆合并无重复。
6. **渠道切换伙伴**:在某个渠道平台把接待伙伴从 A 切到 B,确认活跃会话被重置、下一条远程消息由 B 的人格接待并挂 B 的知识库。
## 路由与 API
| 用途 | 位置 |
| --- | --- |
| 伙伴列表 / 创建 | `GET/POST /api/companion/companions` |
| 伙伴详情 / 修改 / 删除 | `GET/PATCH/DELETE /api/companion/companions/{companionId}` |
| 共享配置(采集 / 学习 / 默认伙伴) | `GET/PATCH /api/companion/config` |
| 每个伙伴的陪伴线程 | `GET /api/companion/companions/{companionId}/companion/threads``…/companion/active` |
| 导出记忆包 | `POST /api/companion/export/memory``{dest_path, include_events}` |
| 导出伙伴包 | `POST /api/companion/export/companions/{companionId}` |
| 导入记忆包 / 伙伴包 | `POST /api/companion/import`(按 manifest.kind 分发) |
| 导出 / 导入知识库包 | `POST /api/knowledge/bases/{id}/export``POST /api/knowledge/bases/import` |
| 渠道绑定伙伴 | `POST /api/channel/settings/companion` |
## 相关
- [Channels](./channels.zh.md) —— 渠道主 Agent 模式与每平台伙伴绑定。
- [数据与存储](../architecture/data-and-storage.zh.md) —— `companion/` 数据目录布局。
@@ -0,0 +1,157 @@
# Computer Use And Browser Use
NomiFun exposes two optional automation capability families to agents:
- **Computer use**: screenshots, mouse/keyboard input, window enumeration, and
focus control through the in-process Rust implementation (`nomi-computer`,
with accessibility helpers in `nomi-a11y`).
- **Browser use**: Chrome automation through the in-process Rust CDP engine
(`nomi-browser-engine`) and the tool facade (`nomi-browser`).
Both are high-privilege capabilities. In the desktop product UI they are
compiled in and enabled by default so a user can opt out from Settings. In
headless/server hosts they are omitted or disabled unless the host explicitly
enables the relevant build feature and runtime flag.
## Current Architecture
The old external `@playwright/mcp` sidecar path and its boot-time Node/npm/
Chromium provisioning have been removed. Browser use now runs through the
native CDP engine. ACP/Codex-style sessions can reach the same engine through
the `mcp-browser-stdio` bridge.
Computer use is desktop-oriented. It can observe the screen and synthesize
input, so it is compiled into desktop/Nomi CLI builds but omitted from the
headless web/server build.
## Enabling And Disabling Capabilities
### Desktop Settings
The desktop app exposes both toggles under System Settings:
- **Browser Use** (`/settings/browser-use`)
- **Computer Use** (`/settings/computer-use`)
Current desktop builds default both toggles to **on** when the corresponding
feature is compiled. Turning either toggle off persists a user preference and
prevents new sessions from receiving that capability.
### Per Session
Create or update a session with capability flags in `extra`:
```json
{ "computerUse": true, "browserUse": true }
```
Both camelCase and snake_case keys are accepted by compatibility paths.
### Host Environment
```bash
NOMIFUN_COMPUTER_USE=1
NOMIFUN_BROWSER_USE=1
```
These set default availability for Nomi-engine sessions in the host where they
are read. They do not bypass build-time feature gates.
### Nomi Engine Config
`~/.nomi/config.toml` or project `.nomi/config.toml`:
```toml
[tools]
max_recent_images = 3
[tools.computer]
enabled = true
max_screenshot_edge = 1568
[tools.browser]
enabled = true
headless = false
allowed_origins = []
```
`browser_path` and `idle_timeout_secs` are legacy compatibility fields; the
native engine manages browser acquisition and lifecycle itself. On first use,
the engine can acquire Chrome for Testing into its own user-data area without
requiring Node, npm, or Playwright.
## Build Matrix
| Host | Computer use | Browser use |
| --- | --- | --- |
| `nomifun-desktop` | Compiled by the `computer-use` feature | Compiled by the `browser-use` feature |
| `nomi` CLI | Enabled in the current `nomi-cli` build | Not enabled in the current `nomi-cli` manifest |
| `nomifun-web` / Docker | Not compiled | Not compiled in the current headless web host |
Web/server builds should not promise desktop or managed-browser control. If a
config enables these tools in a host that was built without the relevant
features, the backend should warn rather than expose a non-working tool.
## macOS Permissions
Computer use needs OS permissions the first time it is used:
- **Accessibility**: required for mouse/keyboard input and accessibility tree
operations.
- **Screen Recording**: required for screenshots. A black screenshot usually
means this permission is missing.
These run **in-process inside the desktop app**, so the permission must be
granted to **NomiFun itself** (the entry named "NomiFun" in System Settings),
not to the terminal/editor — and a freshly-granted permission only takes effect
after the app is **completely quit and reopened** (macOS does not hot-load TCC
grants into a running process). Permission-failure messages name "NomiFun"
explicitly so the guidance is unambiguous.
Settings → Computer Use surfaces a live status panel (macOS): it shows whether
Accessibility / Screen Recording are *in effect for the running process*
which is authoritative, since a System Settings toggle bound to a stale
code-signing identity reads "Not in effect" even while it looks on — with
buttons that deep-link to the exact Privacy pane and trigger the OS prompt.
Backed by `GET/POST /api/computer/permissions[/request|/open-settings]`
(`nomi_computer::permissions``AXIsProcessTrusted` /
`CG*ScreenCaptureAccess`).
> **Stale grant.** If a toggle is clearly on yet computer use still fails, the
> grant is bound to an older build's identity. Quit NomiFun, run
> `tccutil reset Accessibility com.nomifun.desktop` and
> `tccutil reset ScreenCapture com.nomifun.desktop`, relaunch, re-grant, and
> fully restart once more.
## Approval Semantics
- Read-only computer actions such as `screenshot`, `cursor_position`,
`list_windows`, and `wait` are treated as info-level operations.
- Mutating computer actions such as click, type, scroll, drag, and
`focus_window` are execution-level operations and require approval in default
modes.
- Plan mode hides the whole computer-use tool.
- Browser actions derive approval from behavior: observation is info-level;
navigation, clicking, typing, and other page mutations are execution-level.
Recommended loop: observe with a screenshot or browser snapshot, perform one
small operation, then observe again.
## Image And Token Hygiene
- Screenshots are downsampled to a maximum long edge of
`max_screenshot_edge` pixels, with coordinates mapped back to real screen
coordinates.
- The conversation keeps only the most recent `max_recent_images` image-bearing
tool results to avoid unbounded token growth.
- OpenAI-compatible tool messages cannot carry images directly; image data is
sent as a following user message with a source call id. Anthropic, Bedrock,
and Vertex use native image blocks where supported.
- External MCP image results pass through the same image pipeline with a
per-image size cap.
## Related Docs
- [Agent Engine](../architecture/agent-engine.md)
- [MCP And Skills](mcp-and-skills.md)
- [Remote Capability API](remote-capability-api.md)
@@ -0,0 +1,101 @@
# Computer Use 与 Browser Use(计算机控制与浏览器自动化)
NomiFun agent 内置/接入两项可选的系统级能力:
- **Computer**computer use,进程内 Rust):截屏、鼠标键盘合成输入、窗口枚举/聚焦——让 agent 看到并操作本机桌面。crate:`nomi-computer`xcap + enigo)。
- **Browser**browser use,进程内自研 CDP 引擎):通过内置浏览器引擎驱动 Chrome 完成导航、读取、点击、填表等,以单工具 `Browser` 暴露。crate`nomi-browser-engine`(自研 Rust CDP+ `nomi-browser`(facade)。首次启用时引擎按需自动获取 Chrome(`acquire.rs` 内置 CfT 下载/解压),无需 Node/npm。由 `nomi-agent::bootstrap` 在启用且 `browser-use` feature 开启时注册 `BrowserTool`
> 注:早期的外接 `@playwright/mcp` sidecar 与其 boot-time provisioning(装 node/npm/Chromium**已移除**browser use 现统一走进程内自研 CDP 引擎,是唯一浏览器路径。ACP/codex 经 `mcp-browser-stdio`native facade)接入同一引擎。
>
> 当前文档只描述已落地路径:桌面端的系统设置开关、进程内
> browser/computer 工具,以及对应的 build feature 门控。
两者都是高权限能力。当前桌面产品构建在对应 feature 存在时默认开启,
用户可在系统设置中关闭;无头 Web/服务器构建则不承诺桌面控制或托管
浏览器能力。
## 启用与关闭方式
### 1. 桌面端系统设置(推荐)
桌面应用在系统设置中提供两个页面:
- **Browser Use**`/settings/browser-use`
- **Computer Use**`/settings/computer-use`
当前桌面构建默认把两个开关设为开启;关闭任一开关会持久化到用户偏好,
后续新会话不会获得对应能力。
### 2. 会话级
创建会话时在 `extra` 中传开关(camelCase 与 snake_case 均可):
```json
{ "computerUse": true, "browserUse": true }
```
### 3. 宿主级环境变量
```bash
NOMIFUN_COMPUTER_USE=1 # 所有 nomi 会话默认启用 Computer
NOMIFUN_BROWSER_USE=1 # 所有 nomi 会话默认启用 Browser(进程内 native CDP 引擎)
```
### 4. nomi CLI / 配置文件
`~/.nomi/config.toml` 或项目 `.nomi/config.toml`
```toml
[tools]
max_recent_images = 3 # 历史中保留图片的工具结果条数(旧图自动剥离省 token)
[tools.computer]
enabled = true
max_screenshot_edge = 1568 # 截图长边像素上限
[tools.browser]
enabled = true
headless = false # 服务器部署建议 true
allowed_origins = [] # 可选 origin 白名单;空=全放行,仅纵深防御
# 注:browser_path / idle_timeout_secs 已弃用(native 引擎自管浏览器与生命周期),保留 #[serde(default)] 仅为旧配置兼容。
```
启用 Browser 后,native 引擎首次使用时自动获取 Chrome(CfT 下载到引擎专属 user-data-dir,不污染用户浏览器),无需预装 Node/npm/Playwright。
## 构建形态(feature 门控)
| 宿主 | Computer(进程内) | Browser(进程内 native CDP |
|---|---|---|
| 桌面应用(nomifun-desktop | ✅ 默认编译(`computer-use` feature | ✅(`browser-use` feature;首次自动获取 Chrome |
| nomi CLI | ✅ 当前 `nomi-cli` manifest 启用 | ❌ 当前 `nomi-cli` manifest 未启用 |
| Web/服务器(nomifun-web、Docker | ❌ 不编译(无显示器;xcap/enigo 不进二进制) | ❌ 当前 headless web host 未启用 `browser-use` feature |
`computer-use` feature 链:`apps/desktop``nomifun-app``nomifun-ai-agent``nomi-agent``nomi-computer`。Web 构建若配置中误开 computer,仅记录 warning,不报错。Browser 由 `browser-use` feature 门控(`nomi-browser` / `nomi-browser-engine`)。
## macOS 权限
Computer 能力首次使用需在「系统设置 → 隐私与安全性」中授权宿主应用:
- **辅助功能(Accessibility)**:鼠标键盘合成输入需要此项(未来 a11y 树读取/动作亦只需此项)。
- **屏幕录制(Screen Recording)**:截图需要此项(截图全黑或失败时检查)。
当前为反应式诊断:权限缺失时,工具结果会给出授权指引。
## 工具语义与审批
- Computer 为单工具 + `action` 参数形态。
- 只读 action`screenshot``cursor_position``list_windows``wait`)按 **Info** 类审批——AutoEdit/Default 模式自动放行;操作类 action(点击、输入、滚动、拖拽、`focus_window` 等)按 **Exec** 类——Default 模式需用户确认。
- Plan mode 下 Computer 整工具不可见(只读规划阶段不操作桌面)。
- Browsernative CDP)工具按动作语义派生审批类别:只读观察(如 `observe`/快照)→ Info,写操作(导航、点击、输入等)→ Exec。
- 推荐工作流:`screenshot` 观察 → 操作 → 再次 `screenshot` 验证。
## 截图与 token 治理
- 截图自动降采样到长边 ≤ `max_screenshot_edge`(默认 1568pxAnthropic 视觉推荐区间),文本中标注缩放后尺寸;模型给的坐标自动映射回真实屏幕(含 Retina 缩放)。
- 历史消息中只保留最近 `max_recent_images`(默认 3)个带图结果的图片,更早的图片在轮次结束时剥离(文本保留),避免会话文件与请求 token 膨胀。
- OpenAI 协议的 tool 消息不支持图片:图片以紧随其后的 user 消息(`image_url` data URI)传递,并标注来源 call id。Anthropic/Bedrock/Vertex 走原生 `tool_result` 图片块。
- 外接 MCP 工具回传的图片同样经 `McpToolProxy` 映射进图片管道(单图 ≤ 5 MiB 上限)。
## 替代路径:其他外接 MCP
除内置 Computer 与 native Browser 外,仍可外接任意社区 MCP server(在 MCP 设置中添加),与上述能力互不冲突(工具名不同)。
+152
View File
@@ -0,0 +1,152 @@
# Running NomiFun as a Desktop App
The desktop app (`nomifun-desktop`) is a [Tauri](https://tauri.app/) shell that links the Rust backend (`nomifun-app`) **into the same process**. There is no spawned backend binary, no Electron, no bundled `nomicore`. The shell starts the backend as an async task on a free `127.0.0.1` port, then loads the bundled SPA (`ui/dist`) into a WebView and points it at `http://127.0.0.1:<port>/api`.
The desktop WebView does not show a login screen. Instead, the embedded backend
runs under `AuthPolicy::TrustLocalToken`: the shell injects a per-boot local
trust secret into its own WebView, and only requests carrying that secret are
treated as the desktop user. If you want login + remote browser/phone access,
see [WebUI Remote Access](./webui-remote-access.md) for the in-app feature, or
[Self-Host the Web Server](./web-server-deployment.md) for the standalone
server.
![NomiFun desktop main window](../images/desktop-01-main-window.png)
## Quick start
### Prerequisites
The desktop app requires:
- A platform Tauri supports (Windows 10+, macOS 11+, mainstream Linux distros).
- A WebView runtime: **WebView2** on Windows (preinstalled on Win 11; on Win 10 install the [Evergreen Bootstrapper](https://developer.microsoft.com/microsoft-edge/webview2/)), **WKWebView** on macOS (built-in), **WebKitGTK** on Linux (`libwebkit2gtk-4.1-0`).
- For development: Rust toolchain, [Bun](https://bun.sh) ≥ 1.3.13, and the platform Tauri build deps (see the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/)).
### Run from source (development)
From the repo root:
```bash
bun install
bun run dev
```
This runs `tauri dev --config apps/desktop/tauri.conf.json`. It starts the Vite dev server (`http://localhost:5173`) for the SPA, builds and launches `nomifun-desktop`, and the embedded backend is started on a fresh free localhost port at every boot.
### Build a release bundle
```bash
bun run build
```
Output bundles land under `target/release/bundle/` per platform (NSIS installer + MSI on Windows, `.app` + `.dmg` on macOS, `.deb` + `.AppImage` on Linux). To produce signed updater artifacts (extra `.sig` files), use `bun run build:updater` after configuring signing keys (see [Updater status](#updater-status) below).
A successful build prints the bundle locations, for example on macOS:
```text
$ bun run build
Compiling nomifun-app v0.1.0
Finished `release` profile [optimized] target(s)
Bundling NomiFun.app (macos)
Bundling NomiFun_0.1.0_aarch64.dmg (macos)
Finished 2 bundles at:
target/release/bundle/macos/NomiFun.app
target/release/bundle/dmg/NomiFun_0.1.0_aarch64.dmg
```
## Window and titlebar
The main window is **frameless** on Windows and Linux: the React titlebar component draws min/maximize/close on the same row as the in-app navigation. On macOS the native traffic-light buttons are kept via Tauri's `Overlay` title-bar style, with content extending under the bar.
- Default size: `1280 × 832`, minimum `880 × 600`.
- Resizable everywhere (edge-resize and Snap still work on Windows even without OS-drawn decorations).
- Title bar: `NomiFun`.
> The exact chrome differs per OS: a frameless titlebar with in-app controls on
> Windows and Linux, and the native traffic-light buttons (content under an
> `Overlay` bar) on macOS.
## Single instance
`tauri-plugin-single-instance` enforces a single running copy of the app on Windows and Linux. Trying to launch a second `nomifun-desktop` will silently focus the existing window instead of starting another backend on a different port.
## Deep links
The app registers the `nomifun://` URL scheme (configured in `apps/desktop/tauri.conf.json` under `plugins.deep-link.desktop.schemes`). When the OS launches Nomi via a `nomifun://...` URL, the shell forwards the URLs to the renderer over the Tauri event `deep-link://received`. The renderer can subscribe with `listen('deep-link://received', ...)` from `@tauri-apps/api/event` to handle the payload.
`register_all()` is called at startup to install the scheme; on platforms that need an out-of-band registration step (some Linux desktops, dev contexts) the call is best-effort and a failure is ignored.
## Autostart
The shell ships `tauri-plugin-autostart` so the renderer can opt the app into "launch at login" via the plugin's invoke API. On macOS this uses a `LaunchAgent`; on Windows the registry's `Run` key; on Linux a `.desktop` file in the autostart folder. The user-facing toggle lives in app settings.
## Notifications
`tauri-plugin-notification` is enabled. The renderer can show OS-level notifications (e.g. when an agent finishes a long task or AutoWork has results). On macOS the user is asked for permission the first time; on Windows, notifications use the modern Action Center; on Linux they go through `libnotify`.
## Where data is stored
The desktop app persists the SQLite database, agent state, logs, and the Bun runtime cache under the per-user application-data directory — **`%LOCALAPPDATA%\NomiFun\Nomi`** on Windows, **`~/Library/Application Support/NomiFun/Nomi`** on macOS, **`$XDG_DATA_HOME/NomiFun/Nomi`** on Linux (resolved by the shared `nomifun_app::cli::default_data_dir()`). This is the same default the `nomifun-web` host and the dev scripts use, so a provider or companion configured in one host is visible in the others.
Set `NOMIFUN_DATA_DIR=<absolute path>` before launching the app and the data dir becomes `$NOMIFUN_DATA_DIR/Nomi`. The backend takes an exclusive `server.lock` on the data dir at startup; if it fails to start — for example because another instance already holds the directory — the desktop shell shows a native error dialog and exits.
> Older builds defaulted to `<system temp>/nomifun-data/Nomi`. An install found there is relocated to the per-user location automatically on launch (one-shot): data is copied, absolute paths stored in the database are rewritten, and the legacy directory is kept as a backup. Regenerable caches (the extracted Bun runtime, logs, browser profile, …) are not carried over — they rebuild on first use.
To start fresh, **quit the app** and delete that directory. To migrate, copy the directory to a new machine.
```text
~/Library/Application Support/NomiFun/Nomi/ # macOS (see paths above for Windows/Linux)
├── nomifun-backend.db # SQLite state (conversations, settings, sessions, …)
├── logs/ # nomicore.log
├── companion/ # companions + the shared memory hub
├── knowledge/ # managed knowledge bases
├── runtime/ # extracted Bun runtime cache (regenerable)
└── server.lock # exclusive lock held while a backend is running
```
## Authentication and local trust
The desktop shell does not expose the old blanket no-auth backend to every
localhost caller. It starts the embedded backend with `TrustLocalToken`, injects
`window.__nomiLocalTrust` into the WebView, and the renderer presents that secret
on HTTP and WebSocket calls. A process that only knows
`127.0.0.1:<port>/api` is not automatically trusted.
The desktop app is still a single-user tool: the OS account that starts it owns
everything the agent can do, including shell and file access.
If you want to access the same install from another device, do **not** expose the embedded port. Use one of:
- **WebUI remote access** (a per-instance feature, see [WebUI Remote Access](./webui-remote-access.md)) — turns on a separate authenticated server and gives you a QR-code login.
- **Self-hosted web server** ([Web Server Deployment](./web-server-deployment.md)) — runs the same backend headlessly under `nomifun-web` with auth required.
## Updater status
The Tauri updater plugin (`tauri-plugin-updater`) is wired in and the renderer exposes `invoke('check_for_updates')` (returns the new version string or `null` if up to date). However:
- The endpoint configured in `apps/desktop/tauri.conf.json` (`plugins.updater.endpoints`) is a **placeholder** (`https://REPLACE-WITH-YOUR-HOST/...`). Until you replace it with a real HTTPS URL serving a signed `latest.json`, the updater check will fail.
- The included `pubkey` is a **development key** generated for local testing. **Replace it before any public release** and store your private key in a CI secret.
- `bun run build:updater` produces signed update artifacts (extra `.sig` files next to each installer).
The full updater flow (signing env vars, `latest.json` schema, supported platform
keys) is documented in `apps/desktop/updater/README.md`. OS-level code signing /
notarization is separate. macOS Developer ID signing and notarization are wired
through `bun run build:signed` and documented in
`apps/desktop/signing/README.md`; Windows signing still requires an external
code-signing certificate.
## Troubleshooting
**The window opens to a blank white area.**
Make sure the WebView runtime is installed (WebView2 on Windows 10 needs the Evergreen Bootstrapper). On Linux, `libwebkit2gtk-4.1-0` is required.
**"Failed to bind backend port".**
Another process is holding `127.0.0.1` ephemeral ports. The backend tries `pick_free_port()` and falls back to `8799` if that fails — quit any other NomiFun instance and try again.
**Agent commands fail with `bun: command not found`.**
The agent engine spawns Bun as a child process for tool execution. Install Bun (`curl -fsSL https://bun.sh/install | bash`) and make sure it is on the system `PATH`, or build the desktop bundle with `NOMIFUN_EMBED_BUN=1` to embed it.
## See also
- [Web Server Deployment](./web-server-deployment.md) — run the same backend headlessly under `nomifun-web`.
- [WebUI Remote Access](./webui-remote-access.md) — expose your desktop instance for remote browser/phone use.
+146
View File
@@ -0,0 +1,146 @@
# 以桌面应用方式运行 NomiFun
桌面应用 (`nomifun-desktop`) 是一个 [Tauri](https://tauri.app/) 外壳,**在同一进程内**链接 Rust 后端 (`nomifun-app`)。这里没有派生的后端二进制,没有 Electron,也没有捆绑的 `nomicore`。外壳在一个空闲的 `127.0.0.1` 端口上将后端启动为异步任务,然后将打包好的 SPA (`ui/dist`) 加载进 WebView,并使其指向 `http://127.0.0.1:<port>/api`
桌面 WebView 不显示登录页。嵌入式后端使用 `AuthPolicy::TrustLocalToken`
外壳把每次启动生成的本地信任 secret 注入自己的 WebView,只有携带该 secret
的请求会被视为桌面用户。如果你想要登录 + 远程浏览器/手机访问,请参阅
[WebUI 远程访问](./webui-remote-access.zh.md)(应用内功能),或
[自托管 Web 服务器](./web-server-deployment.zh.md)(独立服务器)。
![NomiFun 桌面主窗口](../images/desktop-01-main-window.png)
## 快速开始
### 前置条件
桌面应用需要:
- Tauri 支持的平台 (Windows 10+、macOS 11+、主流 Linux 发行版)。
- WebView 运行时:Windows 上的 **WebView2** (Win 11 预装;Win 10 上请安装 [Evergreen Bootstrapper](https://developer.microsoft.com/microsoft-edge/webview2/))macOS 上的 **WKWebView** (内置)Linux 上的 **WebKitGTK** (`libwebkit2gtk-4.1-0`)。
- 用于开发:Rust 工具链、[Bun](https://bun.sh) ≥ 1.3.13,以及对应平台的 Tauri 构建依赖 (参见 [Tauri 前置条件](https://v2.tauri.app/start/prerequisites/))。
### 从源码运行 (开发模式)
在仓库根目录:
```bash
bun install
bun run dev
```
这会执行 `tauri dev --config apps/desktop/tauri.conf.json`。它启动 Vite 开发服务器 (`http://localhost:5173`) 来托管 SPA,构建并启动 `nomifun-desktop`,并在每次启动时在一个全新的空闲 localhost 端口上启动嵌入的后端。
### 构建发布包
```bash
bun run build
```
输出包按平台落到 `target/release/bundle/` 下 (Windows 上是 NSIS 安装器 + MSImacOS 上是 `.app` + `.dmg`Linux 上是 `.deb` + `.AppImage`)。要生成签名的更新器构件 (额外的 `.sig` 文件),请在配置好签名密钥后使用 `bun run build:updater` (参见下方[更新器状态](#更新器状态))。
构建成功后会打印包的位置,例如在 macOS 上:
```text
$ bun run build
Compiling nomifun-app v0.1.0
Finished `release` profile [optimized] target(s)
Bundling NomiFun.app (macos)
Bundling NomiFun_0.1.0_aarch64.dmg (macos)
Finished 2 bundles at:
target/release/bundle/macos/NomiFun.app
target/release/bundle/dmg/NomiFun_0.1.0_aarch64.dmg
```
## 窗口与标题栏
主窗口在 Windows 和 Linux 上是**无边框**的:React 标题栏组件在与应用内导航同一行绘制最小化/最大化/关闭按钮。在 macOS 上,原生的红绿灯按钮通过 Tauri 的 `Overlay` 标题栏样式得以保留,内容延伸至栏底之下。
- 默认尺寸:`1280 × 832`,最小 `880 × 600`
- 各处都可调整大小 (即使没有 OS 绘制的装饰,Windows 上的边缘调整和 Snap 仍然可用)。
- 标题栏:`NomiFun`
> 窗口边框因系统而异:Windows / Linux 上是带应用内控件的无边框标题栏,macOS
> 上保留原生红绿灯按钮(内容延伸至 `Overlay` 栏下)。
## 单实例
`tauri-plugin-single-instance` 在 Windows 和 Linux 上强制应用只运行一个副本。试图启动第二个 `nomifun-desktop` 不会在另一个端口上启动新的后端,而是会静默地聚焦到已有的窗口。
## 深度链接
应用注册了 `nomifun://` URL 协议 (在 `apps/desktop/tauri.conf.json``plugins.deep-link.desktop.schemes` 下配置)。当操作系统通过 `nomifun://...` URL 启动 Nomi 时,外壳会通过 Tauri 事件 `deep-link://received` 将 URL 转发给渲染进程。渲染进程可以使用 `@tauri-apps/api/event` 中的 `listen('deep-link://received', ...)` 订阅以处理负载。
启动时会调用 `register_all()` 来安装该协议;在需要带外注册步骤的平台上 (某些 Linux 桌面、开发环境),该调用是尽力而为的,失败会被忽略。
## 自启动
外壳附带 `tauri-plugin-autostart`,使得渲染进程可以通过插件的 invoke API 让应用加入 "登录时启动"。在 macOS 上这使用 `LaunchAgent`;在 Windows 上使用注册表的 `Run` 键;在 Linux 上则使用 autostart 文件夹中的 `.desktop` 文件。面向用户的开关位于应用设置中。
## 通知
`tauri-plugin-notification` 已启用。渲染进程可以显示 OS 级别的通知 (例如,当 agent 完成一个长任务或 AutoWork 有结果时)。在 macOS 上,第一次会请求用户授权;在 Windows 上,通知使用现代的操作中心;在 Linux 上则通过 `libnotify`
## 数据存储位置
桌面应用将 SQLite 数据库、agent 状态、日志和 Bun 运行时缓存持久化到按用户的应用数据目录下 —— Windows 上是 **`%LOCALAPPDATA%\NomiFun\Nomi`**macOS 上是 **`~/Library/Application Support/NomiFun/Nomi`**Linux 上是 **`$XDG_DATA_HOME/NomiFun/Nomi`** (由共享的 `nomifun_app::cli::default_data_dir()` 解析)。这与 `nomifun-web` 宿主和开发脚本使用的是同一个默认目录,因此在一个宿主里配置的 provider 或伙伴在其他宿主里同样可见。
在启动应用前设置 `NOMIFUN_DATA_DIR=<absolute path>`,数据目录就会变为 `$NOMIFUN_DATA_DIR/Nomi`。后端启动时会对数据目录取排他的 `server.lock`;若启动失败 (例如该目录已被另一个实例占用),桌面外壳会弹出原生错误对话框并退出。
> 旧版本默认使用 `<system temp>/nomifun-data/Nomi`。在那里发现的安装会在启动时自动迁移到按用户位置 (一次性):数据被复制,数据库中存储的绝对路径会被改写,旧目录保留作为备份。可再生的缓存 (解压出的 Bun 运行时、日志、浏览器配置 …) 不会带过去 —— 它们会在首次使用时重建。
要重新开始,**退出应用**并删除该目录。要迁移,将该目录复制到新机器上即可。
```text
~/Library/Application Support/NomiFun/Nomi/ # macOSWindows/Linux 路径见上文)
├── nomifun-backend.db # SQLite 状态(会话、设置、session 等)
├── logs/ # nomicore.log
├── companion/ # 伙伴 + 共享记忆中枢
├── knowledge/ # 受管理的知识库
├── runtime/ # 解压出的 Bun 运行时缓存(可再生)
└── server.lock # 后端运行期间持有的排他锁
```
## 认证与本地信任
桌面外壳不会把旧式完全无鉴权后端暴露给所有 localhost 调用者。它以
`TrustLocalToken` 启动嵌入式后端,向 WebView 注入 `window.__nomiLocalTrust`
渲染端在 HTTP 与 WebSocket 请求中呈递该 secret。只知道
`127.0.0.1:<port>/api` 的其他进程不会自动被信任。
桌面应用仍是单用户工具:启动它的 OS 账户拥有 agent 能做的一切,包括 shell
和文件访问。
如果你想从另一台设备访问同一个安装,**不要**直接暴露嵌入的端口。请使用以下之一:
- **WebUI 远程访问** (一个按实例启用的功能,参见 [WebUI 远程访问](./webui-remote-access.zh.md)) —— 启动一个独立的认证服务器并提供二维码登录。
- **自托管 Web 服务器** ([Web 服务器部署](./web-server-deployment.zh.md)) —— 在 `nomifun-web` 下以无头方式运行同一个后端,并要求认证。
## 更新器状态
Tauri 更新器插件 (`tauri-plugin-updater`) 已接入,渲染进程暴露了 `invoke('check_for_updates')` (返回新版本字符串,若已是最新则返回 `null`)。然而:
-`apps/desktop/tauri.conf.json` 中配置的端点 (`plugins.updater.endpoints`) 是一个**占位符** (`https://REPLACE-WITH-YOUR-HOST/...`)。在你将其替换为一个提供已签名的 `latest.json` 的真实 HTTPS URL 之前,更新器检查会失败。
- 包含的 `pubkey` 是一个为本地测试生成的**开发密钥**。**在任何公开发布前请替换它**,并将私钥存储在 CI 密钥中。
- `bun run build:updater` 会生成已签名的更新构件 (在每个安装器旁边附带 `.sig` 文件)。
完整 updater 流程(签名环境变量、`latest.json` schema、支持的平台键)在
`apps/desktop/updater/README.md` 中。OS 级别代码签名/公证是另一层:macOS
Developer ID 签名与公证已通过 `bun run build:signed`
`apps/desktop/signing/README.md` 接好;Windows 签名仍需要外部代码签名证书。
## 故障排查
**窗口打开后是空白白屏。**
确保已安装 WebView 运行时 (Windows 10 上的 WebView2 需要 Evergreen Bootstrapper)。在 Linux 上需要 `libwebkit2gtk-4.1-0`
**"Failed to bind backend port"。**
另一个进程占用了 `127.0.0.1` 临时端口。后端会尝试 `pick_free_port()`,失败时回退到 `8799` —— 退出任何其他 NomiFun 实例后再试。
**Agent 命令失败并报 `bun: command not found`。**
Agent 引擎会派生 Bun 作为子进程来执行工具。请安装 Bun (`curl -fsSL https://bun.sh/install | bash`) 并确保它在系统 `PATH` 上,或者使用 `NOMIFUN_EMBED_BUN=1` 构建桌面包以将其嵌入。
## 另请参阅
- [Web 服务器部署](./web-server-deployment.zh.md) —— 在 `nomifun-web` 下以无头方式运行同一个后端。
- [WebUI 远程访问](./webui-remote-access.zh.md) —— 暴露你的桌面实例供远程浏览器/手机使用。
@@ -0,0 +1,115 @@
# Intelligent Decision (IDMM)
**IDMM** — Intelligent Decision-Making Mode — is Nomi's reliability layer for
unattended work. It is a **session supervisor** that watches each turn and
intervenes the moment it stalls, so a long, automated run reaches a terminal
state instead of hanging on a provider hiccup or a model that has stopped
making progress.
If [AutoWork](autowork-requirements.md) is the engine that drives work
*forward*, IDMM is the guard that keeps each turn *moving*. The two are designed
to compose: AutoWork claims and executes requirements; IDMM makes sure every
turn it starts actually finishes.
> IDMM is an **optional** supervisor (the `nomifun-idmm` crate). You turn it on
> per session, from the same place you toggle AutoWork — the session header.
## Why it exists
Agent turns fail in boring, recoverable ways far more often than they fail in
interesting ones:
- a provider returns a transient `429` / `5xx` and the turn would otherwise give
up;
- the model retries the same failing call in a loop;
- the model spins on a tool call and never decides what to do next;
- the turn simply goes quiet and would eventually hit a hard timeout.
For an interactive session you would just nudge it yourself. For an *unattended*
session — an AutoWork queue running overnight, a scheduled job, a multi-agent
teammate — there is nobody watching. IDMM is that watcher.
## The two tiers
When IDMM detects a stall it resolves it with the cheapest mechanism that can,
escalating only when it must.
### Rule tier (no LLM)
A deterministic policy handles the common, mechanical stalls **without calling a
model at all** — so it is fast and free:
- **Provider faults** — transient errors and rate limits are absorbed and the
turn is retried under a sane backoff instead of failing outright.
- **Retry loops** — repeated identical retries are detected and broken.
- **Tool-spin** — a model that keeps re-issuing the same tool call without
progress is steered back on track.
Most interventions never get past this tier.
### Sidecar tier (a backup model)
When a stall is genuinely a *decision* problem — the main model is stuck and a
rule cannot resolve it — IDMM asks a **lightweight sidecar model** to make the
next decision so the session does not deadlock. The sidecar is a small, cheap
"second opinion" model: its only job is to unstick the turn, not to take over
the work.
This is the **bypass model** in product terms: a model that sits beside the main
agent and steps in only when needed.
## Session guard & keep-alive
Together, the rule tier and the sidecar form the **session guard**: IDMM keeps
the target alive through faults and decision stalls and shepherds the turn to a
terminal state. This is what "session keep-alive" means in Nomi — not a dumb
heartbeat, but an active supervisor that resolves the thing that would otherwise
have stalled the turn.
## How it composes with AutoWork
IDMM and AutoWork are independent but complementary:
- **AutoWork** claims the next requirement, injects it, waits for the turn to
finish, and finalises it.
- When AutoWork starts a turn, it asks IDMM (if wired) to **ensure supervision**
of that target for the duration of the turn.
- **IDMM** keeps that turn from getting stuck, so it reaches `done` / `failed`
cleanly instead of timing out.
The net effect: AutoWork provides forward progress; IDMM provides liveness. A
queue can run for hours, unattended, and individual transient failures no longer
abort the run.
```
AutoWork: claim ─▶ inject ─▶ [ turn runs ] ─▶ finalize (done/failed)
│ ensure supervision
IDMM guard ──▶ rule tier ──▶ (escalate) ──▶ sidecar model
```
See `crates/backend/nomifun-idmm/` for the per-tier policy detail and the
intervention log API.
## Enabling it
IDMM is toggled from the **session header**, the same control surface as
AutoWork. Turn it on for a conversation or a terminal target that you intend to
leave running unattended. There is nothing to configure for the rule tier; the
sidecar tier uses a lightweight model from your configured providers.
## When to use it
- **Use it** for any unattended run: AutoWork queues, scheduled
([cron](scheduled-tasks.md)) jobs, overnight batches, or long terminal-driven
agent sessions.
- **You may not need it** for short, interactive sessions where you are watching
the turn and can intervene yourself.
## See also
- [AutoWork & Requirements](autowork-requirements.md) — the engine IDMM most
often guards.
- [Scheduled Tasks](scheduled-tasks.md) — unattended jobs that benefit from
supervision.
- [Terminal](terminal.md) — IDMM can supervise long-running terminal targets.
@@ -0,0 +1,97 @@
# 智能决策(IDMM
**IDMM**——Intelligent Decision-Making Mode,智能决策模式——是 Nomi 面向
无人值守任务的稳定性层。它是一个**会话监督器**,盯守每一轮对话,一旦停滞
立即介入,让长时间自动化任务跑到终态,而不是卡在一次提供商抖动、或一个
不再向前推进的模型上。
如果说 [AutoWork](autowork-requirements.zh.md) 是推动工作**向前**的引擎,
IDMM 就是让每一轮持续**运转**的守卫。两者天生互补:AutoWork 负责认领并执行
需求,IDMM 则确保它启动的每一轮都真的能跑完。
> IDMM 是一个**可选**的监督器(`nomifun-idmm` crate)。在会话头部——与开启
> AutoWork 相同的位置——按会话开启。
## 为什么需要它
智能体对话失败,绝大多数是无聊、可恢复的方式,而不是什么有趣的原因:
- 提供商返回一次瞬时的 `429` / `5xx`,对话本会就此放弃;
- 模型反复重试同一个失败调用,陷入循环;
- 模型在某个工具调用上空转,迟迟不决定下一步;
- 对话干脆陷入沉默,最终撞上硬性超时。
交互式会话里,你自己推一把就好。但**无人值守**的会话——通宵跑的 AutoWork
队列、定时任务、多智能协同里的某个队友——没有人盯着。IDMM 就是那个盯守者。
## 两个层级
检测到停滞时,IDMM 会用能解决问题的、最省的手段去化解,必要时才逐级升级。
### 规则层(无需 LLM
一套确定性策略**完全不调用模型**就能处理常见的机械性停滞——又快又省:
- **提供商故障**——瞬时错误与限流被吸收,对话在合理退避下重试,而不是直接
失败。
- **重试循环**——识别并打断反复出现的相同重试。
- **工具空转**——对于不断重复发起同一工具调用却毫无进展的模型,把它拉回
正轨。
大多数介入都到此为止,不会进入下一层。
### 旁路模型层(备用模型)
当停滞确实是一个*决策*问题——主模型卡住了、规则无法化解——IDMM 会请一个
**轻量旁路模型**做出下一步决策,让会话不至于死锁。旁路模型是一个小而便宜的
「第二意见」模型:它唯一的职责是把这一轮解开,而不是接管整个工作。
这就是产品语境里的**旁路模型(Sidecar)**:一个待在主智能体旁边、仅在需要
时才介入的模型。
## 会话守卫与会话保活
规则层与旁路模型合在一起,构成了**会话守卫(Session guard**IDMM 在故障与
决策停滞中保活目标,并把这一轮护送到终态。这正是 Nomi 所说的「会话保活」
——不是一个傻乎乎的心跳,而是一个主动的监督器,去解决那个本会让对话卡死的
根因。
## 它如何与 AutoWork 协同
IDMM 与 AutoWork 相互独立、彼此互补:
- **AutoWork** 认领下一条需求、注入、等待这一轮跑完、并完成它。
- 当 AutoWork 启动一轮时,会请求 IDMM(若已接入)在这一轮期间**确保对该目标
的监督**。
- **IDMM** 让这一轮不至于卡住,从而干净地抵达 `done` / `failed`,而不是超时。
最终效果是:AutoWork 提供向前的推进力,IDMM 提供存活性。一个队列可以无人值守
地跑上数小时,单次的瞬时失败不再让整轮任务中止。
```
AutoWork: 认领 ─▶ 注入 ─▶ [ 对话运行 ] ─▶ 完成(done/failed
│ 确保监督
IDMM 守卫 ──▶ 规则层 ──▶ (升级)──▶ 旁路模型
```
每层策略的细节和介入日志 API 见 `crates/backend/nomifun-idmm/`
## 开启方式
IDMM 在**会话头部**开启,与 AutoWork 是同一处控制入口。对你打算无人值守长跑
的会话或终端目标打开它即可。规则层无需任何配置;旁路模型层会从你配置好的
提供商里使用一个轻量模型。
## 何时使用
- **建议开启**:任何无人值守的长跑——AutoWork 队列、定时
[cron](scheduled-tasks.zh.md))任务、通宵批处理,或长时间运行的终端型
agent 会话。
- **可以不开**:你自己盯着、随时能介入的短交互会话。
## 另请参阅
- [AutoWork 与需求](autowork-requirements.zh.md)——IDMM 最常守护的引擎。
- [定时任务](scheduled-tasks.zh.md)——受益于监督的无人值守任务。
- [终端](terminal.zh.md)——IDMM 可以监督长时间运行的终端目标。
+112
View File
@@ -0,0 +1,112 @@
# MCP & Skills
NomiFun has two extension mechanisms that are easy to confuse:
- **MCP servers** are external tool servers. They expose callable tools over
stdio, HTTP, or SSE.
- **Skills** are markdown/folder knowledge bundles. They tell an agent how to do
a workflow; they are not long-running tool servers.
Current pages:
| Capability | Page |
| --- | --- |
| MCP servers | `/mcp` |
| Skills | `/assistants?tab=skills` |
| Assistants | `/assistants?tab=assistants` |
| Public/remote capability exposure | `/open-capabilities` |
Legacy settings URLs redirect to these pages.
## MCP Servers
Open `/mcp` to add, import, test, enable, disable, and sync MCP servers.
![MCP page](../images/mcp-01-capabilities.png)
Each server row owns:
- name;
- transport: `stdio`, `http`, or `sse`;
- command / args / env for stdio, or URL for HTTP/SSE;
- raw imported JSON, when imported from another agent config;
- enabled state;
- last connection-test result.
Connection test uses a temporary MCP client, performs the handshake, lists tools,
and persists the result. Failure codes include command-not-found, permission,
timeout, HTTP, RPC, and protocol errors.
OAuth-backed HTTP/SSE servers use the `/api/mcp/oauth/*` flow.
## Importing and Syncing Agent Configs
`GET /api/mcp/agent-configs` detects MCP config files from supported local agent
CLIs. The UI lets you import detected servers into NomiFun and push the NomiFun
list back to selected agent configs when an adapter supports writing.
This sync is config management only. A conversation still decides which MCP
servers are visible for that session.
## Per-Conversation Selection
Enabling an MCP server globally makes it available. It does not inject it into
every agent automatically. Conversation/session setup builds the final MCP list
from:
- globally enabled servers;
- the servers selected for that conversation;
- built-in bridge servers required by the active capability set.
The resulting list is passed to the agent session start payload.
## MCP API
| Operation | Endpoint |
| --- | --- |
| List / create | `GET`, `POST /api/mcp/servers` |
| Import batch | `POST /api/mcp/servers/import` |
| Get / update / delete | `GET`, `PUT`, `DELETE /api/mcp/servers/{id}` |
| Toggle | `POST /api/mcp/servers/{id}/toggle` |
| Test connection | `POST /api/mcp/test-connection` |
| Detect agent configs | `GET /api/mcp/agent-configs` |
| OAuth | `POST /api/mcp/oauth/check-status`, `/login`, `/logout`; `GET /api/mcp/oauth/authenticated` |
## Skills
Open `/assistants?tab=skills`.
![Skills tab](../images/mcp-03-skills.png)
A skill is either a single markdown file or a directory containing `SKILL.md`.
Sources:
| Source | Meaning |
| --- | --- |
| Builtin | Shipped with the app. Some are auto-injected. |
| Custom | Imported by the user or placed in a configured skill directory. |
| Extension | Provided by an installed extension. |
Skills can be tagged, imported, exported/symlinked, scanned from external paths,
or materialized for a specific agent backend.
## Skill API
| Operation | Endpoint |
| --- | --- |
| List | `GET /api/skills` |
| Builtin auto-injected list | `GET /api/skills/builtin-auto` |
| Tags | `PUT /api/skills/{name}/tags` |
| Info / paths | `POST /api/skills/info`, `GET /api/skills/paths` |
| Import / export / delete | `POST /api/skills/import`, `POST /api/skills/import-symlink`, `POST /api/skills/export-symlink`, `DELETE /api/skills/{name}` |
| Scan / detect paths | `POST /api/skills/scan`, `GET /api/skills/detect-paths`, `GET /api/skills/detect-external` |
| Materialize for agent | `POST /api/skills/materialize-for-agent` |
| Assistant rule/skill files | `/api/skills/assistant-rule/*`, `/api/skills/assistant-skill/*` |
| External paths | `GET`, `POST`, `DELETE /api/skills/external-paths` |
| Skills market | `POST /api/skills/market/enable`, `POST /api/skills/market/disable` |
## Related
- [Assistants](./assistants.md)
- [Remote Capability API](./remote-capability-api.md)
- [Terminal](./terminal.md)
@@ -0,0 +1,105 @@
# MCP 与技能
NomiFun 有两种容易混淆的扩展机制:
- **MCP server** 是外部工具服务器,通过 stdio、HTTP 或 SSE 暴露可调用工具。
- **技能** 是 markdown/文件夹知识包,告诉 agent 如何完成某个工作流;它不是常驻工具服务器。
当前页面:
| 能力 | 页面 |
| --- | --- |
| MCP server | `/mcp` |
| 技能 | `/assistants?tab=skills` |
| 助手 | `/assistants?tab=assistants` |
| 对外能力暴露 | `/open-capabilities` |
旧 Settings URL 会重定向到这些页面。
## MCP Server
打开 `/mcp` 可以新增、导入、测试、启用/禁用和同步 MCP server。
![MCP 页面](../images/mcp-01-capabilities.png)
每条 server 记录包含:
- 名称;
- transport`stdio``http``sse`
- stdio 的 command / args / env,或 HTTP/SSE 的 URL
- 从其他 agent 配置导入时保留的 raw JSON
- enabled 状态;
- 最近一次连接测试结果。
连接测试会启动临时 MCP client,完成握手、列出工具并持久化结果。失败码覆盖命令
不存在、权限、超时、HTTP、RPC 和协议错误。
需要 OAuth 的 HTTP/SSE server 走 `/api/mcp/oauth/*` 流程。
## 导入和同步 Agent 配置
`GET /api/mcp/agent-configs` 会探测已支持本地 agent CLI 的 MCP 配置。UI 可把探测到
的 server 导入 NomiFun,也可在 adapter 支持写入时把 NomiFun 的 MCP 列表同步回选中的
agent 配置。
这只是配置管理。某次会话最终能看到哪些 MCP server,仍由该会话的选择决定。
## 每会话选择
全局启用 MCP server 只是让它可用,不会自动注入每个 agent。会话启动时最终 MCP 列表来自:
- 全局 enabled server
- 该会话选择的 server
- 当前能力集需要的 builtin bridge server。
最终列表会进入 agent session start payload。
## MCP API
| 操作 | Endpoint |
| --- | --- |
| 列表 / 创建 | `GET`, `POST /api/mcp/servers` |
| 批量导入 | `POST /api/mcp/servers/import` |
| 获取 / 更新 / 删除 | `GET`, `PUT`, `DELETE /api/mcp/servers/{id}` |
| 启用切换 | `POST /api/mcp/servers/{id}/toggle` |
| 连接测试 | `POST /api/mcp/test-connection` |
| 探测 agent 配置 | `GET /api/mcp/agent-configs` |
| OAuth | `POST /api/mcp/oauth/check-status`, `/login`, `/logout`; `GET /api/mcp/oauth/authenticated` |
## 技能
打开 `/assistants?tab=skills`
![技能页](../images/mcp-03-skills.png)
技能可以是单个 markdown 文件,也可以是包含 `SKILL.md` 的目录。
| 来源 | 含义 |
| --- | --- |
| Builtin | 随应用发布;部分会自动注入。 |
| Custom | 用户导入或放入配置目录。 |
| Extension | 已安装扩展提供。 |
技能可打标签、导入、导出/符号链接、扫描外部目录,也可按某个 agent 后端进行
materialize。
## 技能 API
| 操作 | Endpoint |
| --- | --- |
| 列表 | `GET /api/skills` |
| 自动注入 builtin 列表 | `GET /api/skills/builtin-auto` |
| 标签 | `PUT /api/skills/{name}/tags` |
| 信息 / 路径 | `POST /api/skills/info`, `GET /api/skills/paths` |
| 导入 / 导出 / 删除 | `POST /api/skills/import`, `POST /api/skills/import-symlink`, `POST /api/skills/export-symlink`, `DELETE /api/skills/{name}` |
| 扫描 / 探测路径 | `POST /api/skills/scan`, `GET /api/skills/detect-paths`, `GET /api/skills/detect-external` |
| 为 agent materialize | `POST /api/skills/materialize-for-agent` |
| 助手规则/技能文件 | `/api/skills/assistant-rule/*`, `/api/skills/assistant-skill/*` |
| 外部路径 | `GET`, `POST`, `DELETE /api/skills/external-paths` |
| 技能市场 | `POST /api/skills/market/enable`, `POST /api/skills/market/disable` |
## 相关
- [助手](./assistants.zh.md)
- [远程能力 API](./remote-capability-api.zh.md)
- [终端](./terminal.zh.md)
@@ -0,0 +1,52 @@
# Model Failover Queue
The current feature behind model-routing settings is a **model failover queue**,
not a credential round-robin pool.
It lets Nomi-engine conversations try a configured sequence of backup models
when a provider fault is detected. ACP/CLI agents are not included in this
feature because their provider calls happen inside external runtimes.
## What It Does
- Stores a global default queue under `agent.model_failover`.
- Allows per-conversation overrides under `extra.model_failover`.
- Applies only to Nomi-engine conversations.
- Can be used by IDMM fault-watch flows when that session has failover enabled.
- Does not distribute load across API keys.
- Does not make all CLI agents share a common pool.
## When To Use It
Use model failover when a Nomi-engine session should recover from transient
provider/model faults without requiring manual model switching.
Typical queue:
```text
primary model -> cheaper backup -> stronger backup -> manual review
```
The queue is about reliability, not quota aggregation. If every configured
provider is down or the prompt/tool state is invalid, failover cannot make the
turn succeed.
## How It Relates To IDMM
IDMM has separate fault and decision watches. Model failover belongs to the
fault side: when a provider fault is classified as recoverable and failover is
enabled, IDMM can ask the conversation runtime to retry through the configured
queue.
AutoWork then sits one layer above both features: it keeps a tagged work queue
moving, while IDMM/model failover try to keep each claimed turn alive.
## Source Of Truth
- `crates/backend/nomifun-conversation/src/model_failover.rs`
- `crates/backend/nomifun-conversation/src/failover_seam.rs`
- `crates/backend/nomifun-app/src/router/model_failover.rs`
- `crates/backend/nomifun-idmm/src/policy.rs`
Older copies of this page described multi-credential round-robin routing. That
was not the current implementation and should not be used as operator guidance.
@@ -0,0 +1,48 @@
# 模型故障转移队列
当前模型路由设置背后的实现是**模型故障转移队列**,不是多凭据轮询池。
它允许 Nomi 引擎会话在检测到提供商故障时,按你配置的顺序尝试备用模型。
ACP/CLI 智能体不包含在这个功能里,因为它们的提供商调用发生在外部运行时内部。
## 它做什么
- 全局默认队列存储在 `agent.model_failover`
- 单个会话可以通过 `extra.model_failover` 覆盖。
- 只作用于 Nomi 引擎会话。
- 可被 IDMM 的故障监视流程使用。
- 不会在 API Key 之间分摊负载。
- 不会让所有 CLI 智能体共享同一个模型池。
## 什么时候使用
当一个 Nomi 引擎会话需要在临时提供商/模型故障后自动换用备用模型时,使用模型
故障转移。
常见队列:
```text
主模型 -> 便宜备用模型 -> 更强备用模型 -> 人工检查
```
这个队列解决的是可靠性,不是额度聚合。如果所有配置的提供商都不可用,或者
prompt / tool 状态本身无效,故障转移也无法让这一轮成功。
## 与 IDMM 的关系
IDMM 有独立的故障监视和决策停滞监视。模型故障转移属于故障侧:当某个提供商
故障被判定为可恢复,且该会话启用了故障转移时,IDMM 可以让会话运行时按配置
队列重试。
AutoWork 位于更上一层:它负责让标签队列继续认领和推进需求,而 IDMM / 模型
故障转移负责尽量让每个已认领的回合活下来。
## 真相来源
- `crates/backend/nomifun-conversation/src/model_failover.rs`
- `crates/backend/nomifun-conversation/src/failover_seam.rs`
- `crates/backend/nomifun-app/src/router/model_failover.rs`
- `crates/backend/nomifun-idmm/src/policy.rs`
本页旧版本曾把该功能描述成多凭据 round-robin 路由。那不是当前实现,不应作为
运维或用户指南使用。
@@ -0,0 +1,168 @@
# Remote Capability API Examples
These examples use one companion access token bound to one companion. Replace
`$HOST` with your NomiFun host and `$TOKEN` with the token shown when it was
created.
```bash
export HOST=127.0.0.1:25808
export TOKEN=<companion-access-token>
```
## MCP Client
For Claude Code, Cursor, or any MCP client that supports Streamable HTTP:
```json
{
"mcpServers": {
"nomifun": {
"type": "streamable-http",
"url": "http://$HOST/mcp-agent",
"headers": {
"Authorization": "Bearer $TOKEN"
}
}
}
}
```
Use `/mcp-agent` for the curated worker surface. Use `/mcp` only when you need
the broader platform-control surface.
## curl
List curated tools:
```bash
curl -s "http://$HOST/v1/tools?profile=agent" \
-H "Authorization: Bearer $TOKEN"
```
Delegate a task to an autonomous NomiFun agent:
```bash
curl -s -X POST "http://$HOST/v1/tools/nomi_agent_run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"goal":"Research competitor pricing and write notes.md","timeout_secs":600}'
```
Poll a long-running delegated task:
```bash
curl -s -X POST "http://$HOST/v1/tools/nomi_agent_result" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"conversation_id":123}'
```
Call any discovered tool:
```bash
curl -s -X POST "http://$HOST/v1/tools/<tool_name>" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"argument":"value"}'
```
For a confirmation-required destructive action, first show the returned
challenge to the user. Retry only after explicit approval:
```bash
curl -s -X POST "http://$HOST/v1/tools/<tool_name>" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"argument":"value","confirm":true}'
```
## SSE Streaming
```bash
curl -N -X POST "http://$HOST/v1/tools/nomi_agent_run/stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"goal":"Summarize this repository"}'
```
The final event is:
```json
{"type":"__result__","data":{"result":{}}}
```
## Python REST
```python
import requests
base = f"http://{HOST}"
headers = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
response = requests.post(
f"{base}/v1/tools/nomi_agent_run",
headers=headers,
json={"goal": "Research competitor pricing and write notes.md"},
)
print(response.json())
```
## Python Streamable HTTP MCP
```python
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
async def main():
headers = {"Authorization": "Bearer " + TOKEN}
async with streamablehttp_client(
"http://%s/mcp-agent" % HOST,
headers=headers,
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([tool.name for tool in tools.tools])
result = await session.call_tool(
"nomi_agent_run",
{"goal": "Research competitor pricing and write notes.md"},
)
print(result)
```
## Headless Server Token Seed
For a local headless server:
```bash
export NOMIFUN_COMPANION_TOKEN="$(openssl rand -hex 32)"
nomifun-web --host 127.0.0.1 --port 8787
```
For LAN or public access, finish admin setup first, bind intentionally, and
place the server behind TLS:
```bash
nomifun-web --host 0.0.0.0 --port 8787
```
## OpenAPI
Generate a typed client from:
```bash
curl -s "http://$HOST/v1/openapi.json?profile=agent" \
-H "Authorization: Bearer $TOKEN" > nomifun-openapi.json
```
## Notes
- MCP clients should prefer `/mcp-agent`.
- Scripts and automation systems can use `/v1/tools/{name}` directly.
- Use `/v1/tools/{name}/stream` when live progress matters.
- Tokens can be revoked with
`DELETE /api/webui/companions/{id}/access-token` from a trusted local
desktop context.
@@ -0,0 +1,149 @@
# Remote 能力 API · 对接示例 cookbook
> 配套 `remote-capability-api.zh.md`。所有示例用同一枚**伙伴访问令牌**(绑定到某个具体伙伴,调用即以该伙伴身份运行);端点 = WebUI/LAN 端口(默认 `25808`)或 `nomifun-web` 的服务端口。下文用 `$HOST`/`$TOKEN` 占位。
## 0. 先决:拿到端点 + 令牌
- **端点**`http://<你的实例IP>:25808`(开启 WebUI 远程访问后),或本机 `http://127.0.0.1:<port>`
- **令牌(运维侧一次性发放,绑定到一个伙伴)**:
- 桌面应用:WebUI/远程访问面板为某个伙伴点「生成访问令牌」(明文只显示一次)。
- 无头服务器:启动时 `NOMIFUN_COMPANION_TOKEN=$(openssl rand -hex 32) nomifun-web --host 127.0.0.1 --port 8787`,绑定到默认伙伴,把这串 hex 当令牌。
- 本机可信上下文(桌面 webview / dev NoAuth)可 `curl -X POST http://127.0.0.1:<port>/api/webui/companions/<companion-id>/access-token`(远程/普通 curl 会 403——铸造刻意只限本地可信)。
- 拿到后:`export TOKEN=<令牌>`;所有请求带 `Authorization: Bearer $TOKEN`
- **以伙伴身份运行**:调用继承所绑定伙伴的模型/人格/知识库;`nomi_agent_run` 不带 `model` 时用该伙伴的 profile 模型,**所以该伙伴要先配置好可用模型**(否则铸造响应里会带 `warning`)。
- **能力发现**`GET /v1/tools`(或 `/v1/tools?profile=agent` 精瘦集)列出所有工具名 + 描述 + JSON Schema;下文工具名以此为准(`nomi_agent_run` 一定有)。
---
## 1. MCP 客户端(Claude Code / Cursor / 任意 MCP Agent)—— 旗舰
最省事:把 NomiFun 作为一个 Streamable-HTTP MCP server 配进去。Claude Code / Cursor 的 `mcpServers`
```json
{
"mcpServers": {
"nomifun": {
"type": "streamable-http",
"url": "http://$HOST:25808/mcp-agent",
"headers": { "Authorization": "Bearer $TOKEN" }
}
}
}
```
- `/mcp-agent` = curated「干活」工具集(agent/browser/computer/knowledge/files);要全平台控制面用 `/mcp`
- 连上后 `tools/list` 即见 `nomi_*` 工具,`tools/call` 驱动。委派整件事就调 `nomi_agent_run`
Python 通用 MCP SDK
```python
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
async def main():
headers = {"Authorization": "Bearer " + TOKEN}
async with streamablehttp_client("http://%s:25808/mcp" % HOST, headers=headers) as (r, w, _):
async with ClientSession(r, w) as s:
await s.initialize()
tools = await s.list_tools()
res = await s.call_tool("nomi_agent_run", {"goal": "调研 X 并写 notes.md"})
print(res)
```
---
## 2. curlHTTP/REST,最通用)
```bash
# 列能力(精瘦 agent 档)
curl -s "http://$HOST:25808/v1/tools?profile=agent" -H "Authorization: Bearer $TOKEN"
# 委派一个目标(一句话把活交给一个自治 nomi agent)
curl -s -X POST "http://$HOST:25808/v1/tools/nomi_agent_run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"goal":"调研竞品定价并写入 notes.md","timeout_secs":600}'
# => 200 {"result":{"conversation_id":123,"status":"completed","text":"..."}}
# 长任务 => {"result":{"conversation_id":123,"status":"running",...}},之后轮询:
curl -s -X POST "http://$HOST:25808/v1/tools/nomi_agent_result" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"conversation_id":123}'
# 调任意能力(名字来自 /v1/tools)
curl -s -X POST "http://$HOST:25808/v1/tools/<tool_name>" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{...args...}'
# 危险操作:先返回 {"needs_confirmation":true,...}(HTTP 409) → 向用户复述后带 confirm 重试
curl -s -X POST "http://$HOST:25808/v1/tools/<tool>" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{...args..., "confirm": true}'
```
**结果信封**:成功 `200 {"result": <payload>}`;工具报错 `422 {"error":..}`;需确认 `409 {"needs_confirmation":true,..}`;未知工具 `404`;无/错令牌 `401`
### 流式(SSE
```bash
curl -N -X POST "http://$HOST:25808/v1/tools/nomi_agent_run/stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"goal":"..."}'
# 每行一个 data: JSON 事件(agent 的 text/tool_call delta),
# 末帧 data: {"type":"__result__","data":{"result":{...终值...}}}
```
---
## 3. PythonREST + SSE
```python
import requests, json
BASE = "http://%s:25808" % HOST
H = {"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"}
# 调用
r = requests.post(f"{BASE}/v1/tools/nomi_agent_run", headers=H, json={"goal": "..."})
print(r.json()) # {"result": {...}} / {"error":...} / {"needs_confirmation":...}
# 流式
with requests.post(f"{BASE}/v1/tools/nomi_agent_run/stream", headers=H,
json={"goal": "..."}, stream=True) as resp:
for line in resp.iter_lines():
if line and line.startswith(b"data: "):
ev = json.loads(line[6:])
if ev.get("type") == "__result__":
print("FINAL:", ev["data"]); break
print("delta:", ev)
```
---
## 4. nomicore CLI(人/脚本)
```bash
export NOMIFUN_URL=http://$HOST:25808
export NOMIFUN_COMPANION_TOKEN=$TOKEN
nomicore tools # 离线列出 Remote 能力(无需运行实例)
nomicore call nomi_agent_run '{"goal":"..."}'
nomicore agent "调研竞品定价并总结" # nomi_agent_run 的便捷包装
```
---
## 5. 任意 HTTP 自动化(n8n / Zapier / Make / shell 脚本)
把一个 HTTP 节点指向 `POST http://$HOST:25808/v1/tools/{name}`Header `Authorization: Bearer $TOKEN`Body = 该工具的 JSON 参数。零 SDK。
## 6. 从 OpenAPI 生成客户端
`GET http://$HOST:25808/v1/openapi.json[?profile=agent]` 是 OpenAPI 3.1 契约 —— 喂给 `openapi-generator` 生成任意语言的 typed client,或导入 Postman/Insomnia/Bruno。
## 7. 接进别的 LLM agent 框架(LangChain / OpenAI tool-calling / 自研)
`GET /v1/tools` 每个工具自带 `name` + `description` + `input_schema`(标准 JSON Schema)。把它们直接注册成你框架的工具列表;模型决定调用某工具时,转一发 `POST /v1/tools/{name}`(带 `confirm` 处理 409)。等于让 NomiFun 全平台能力即插即用地成为你 agent 的工具集。
---
## 备注
- **安全**:持令牌即全权(≈授予 RCE 等价能力);只发给可信客户端,公网前置 TLS 反代,令牌可吊销(`DELETE /api/webui/companions/{id}/access-token`,只影响对应伙伴)。
- **MCP vs REST 选择**agent/MCP 客户端用 `/mcp`(-agent);脚本/自动化/其它语言用 `/v1`;要实时进度用 `/v1/tools/{name}/stream`(SSE) 或(MCP 端长任务)`nomi_agent_run``{status:running}` 句柄 + `nomi_agent_result` 轮询。
@@ -0,0 +1,185 @@
# Remote Capability API
NomiFun can expose its platform capabilities through a network-reachable,
token-authenticated MCP and REST front door. A trusted external agent or MCP
client can connect with a URL plus a companion access token and then call the
same capability registry used by the desktop app.
Each token is bound to one companion. Calls made with that token run as that
companion and inherit its profile model, persona, and knowledge context.
For copy-ready integrations, see
[Remote Capability API Examples](./remote-capability-api-examples.md).
## Security Model
A companion access token is high privilege. It can drive agents, read and
write files through exposed tools, and in desktop builds may operate browser or
computer-use capabilities. Treat it like remote code execution authority:
- Give tokens only to clients and agents you trust.
- Prefer loopback, VPN, or a private network.
- Put TLS, firewall rules, and rate limits in front of any public exposure.
- Rotate or revoke tokens immediately if they leave your control.
- Sensitive tools such as secrets and factory reset are not exposed on the
remote surface by default.
- Destructive tools require a confirmation retry: the first call returns a
confirmation challenge; the caller must show the action to the user and retry
with `confirm: true`.
## Endpoints
The network front door is mounted by the same backend process as the Web UI.
| Endpoint | Purpose |
| --- | --- |
| `/mcp` | Full Streamable-HTTP MCP server. |
| `/mcp-agent` | Curated MCP profile for external working agents. |
| `/v1/tools` | REST tool discovery. Add `?profile=agent` for the curated set. |
| `/v1/tools/{name}` | REST tool call. |
| `/v1/tools/{name}/stream` | SSE streaming wrapper for tools that emit progress. |
| `/v1/openapi.json` | OpenAPI 3.1 description for the REST tool surface. |
Authenticate every request with:
```http
Authorization: Bearer <companion-access-token>
```
Common base URLs:
- Desktop remote access: `http://<LAN-IP>:25808`
- Standalone server: `http://<host>:8787` unless you changed the port
- Local development or embedded desktop backend: `http://127.0.0.1:<port>`
## Creating A Companion Token
Tokens are stored hashed. The plaintext token is shown only once.
### Desktop App
Use the Open Capabilities / remote access UI, or call the trusted local API
from the desktop WebView context:
```bash
curl -X POST \
http://127.0.0.1:<loopback-port>/api/webui/companions/<companion-id>/access-token
```
The response returns the plaintext token once:
```json
{
"success": true,
"data": {
"token": "<64-character-hex-token>",
"companion_id": "<companion-id>"
}
}
```
Status and revoke use the same path:
```bash
curl http://127.0.0.1:<loopback-port>/api/webui/companions/<companion-id>/access-token
curl -X DELETE \
http://127.0.0.1:<loopback-port>/api/webui/companions/<companion-id>/access-token
```
These token-management endpoints require local trust. A remote browser or plain
curl client cannot mint tokens.
### Headless `nomifun-web`
Seed a token at startup with `NOMIFUN_COMPANION_TOKEN`. The value binds to the
default companion when no token is already configured:
```bash
NOMIFUN_COMPANION_TOKEN="$(openssl rand -hex 32)" \
nomifun-web --host 127.0.0.1 --port 8787
```
Use the generated hex string as the Bearer token. For non-local exposure,
finish admin setup first and put the server behind TLS.
## MCP Client Configuration
Example Streamable-HTTP MCP configuration:
```json
{
"mcpServers": {
"nomifun": {
"type": "streamable-http",
"url": "http://127.0.0.1:25808/mcp-agent",
"headers": {
"Authorization": "Bearer <companion-access-token>"
}
}
}
}
```
Use `/mcp-agent` when an external agent mostly needs work tools
(agent/browser/computer/knowledge/files). Use `/mcp` when you intentionally
want the broader platform control surface.
## REST Tool Calls
Discover tools:
```bash
curl -s "http://127.0.0.1:25808/v1/tools?profile=agent" \
-H "Authorization: Bearer $TOKEN"
```
Run a delegated NomiFun agent task:
```bash
curl -s -X POST "http://127.0.0.1:25808/v1/tools/nomi_agent_run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"goal":"Research competitors and write notes.md","timeout_secs":600}'
```
Poll a long-running task:
```bash
curl -s -X POST "http://127.0.0.1:25808/v1/tools/nomi_agent_result" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"conversation_id":123}'
```
Successful REST calls return `200 {"result": ...}`. Tool validation failures
return `422`, unknown tools return `404`, invalid tokens return `401`, and
confirmation-required calls return `409`.
## Streaming
SSE streaming is available for tools that report progress:
```bash
curl -N -X POST "http://127.0.0.1:25808/v1/tools/nomi_agent_run/stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"goal":"Summarize the current repository"}'
```
Each event is a `data: <json>` line. The final event uses
`{"type":"__result__","data":{"result":...}}`.
## Companion Context
Because the caller runs as the bound companion, `nomi_agent_run` can use that
companion's configured model when no `model` argument is supplied. Configure a
usable provider/model for the companion before relying on model-backed tools;
token creation may warn if the companion has no usable model.
## Related Docs
- [Remote Capability API Examples](./remote-capability-api-examples.md)
- [WebUI Remote Access](./webui-remote-access.md)
- [Web Server Deployment](./web-server-deployment.md)
- [Computer Use And Browser Use](./computer-browser-use.md)
@@ -0,0 +1,106 @@
# Remote 能力 API(外部伙伴 / MCP 接入指南)
NomiFun 把整个平台的能力(agent / browser / computer / 知识库 / 文件 / 以及平台控制)通过一个**网络可达、伙伴访问令牌鉴权的 MCP 端点**暴露出来。任何 MCP 客户端(Claude Code、Cursor、自研 LLM agent)填一个 URL + 一枚访问令牌,就能像"桌面伙伴"一样驱动平台——这就是"**外部伙伴**"。每枚令牌**绑定到一个具体伙伴**:持令牌调用即以该伙伴的身份运行,继承它的 profile 模型 / 人格 / 知识库,互不串扰。
> 📋 **可复制的对接示例**MCP 客户端 / curl / Python / CLI / 自动化 / OpenAPI codegen / LLM 框架)见 **`remote-capability-api-examples.zh.md`**。
## ⚠️ 安全须知
持有伙伴访问令牌即可调用平台能力,**等价于授予远程代码执行(RCE)能力**(可驱动 agent、读写文件、操作 computer/browser)。因此:
- 只把令牌交给你信任的客户端/agent。
- 仅在可信网络暴露;公网暴露务必前置 TLS 反代 + 防火墙。
- 令牌可随时吊销/轮换(见下);吊销只影响对应伙伴,其它伙伴的令牌不受影响。
- 默认安全栏:危险能力(`secret.*``system.factory_reset` 等)在 Remote 面被拒;破坏性操作需二次确认(协议级握手,见「权限模型」)。
## 端点
`/mcp`MCP Streamable-HTTP)随后端进程内挂载,与 WebUI 共用监听器:
- **本机**`http://127.0.0.1:<port>/mcp`(桌面应用的回环端口,或 `nomifun-web` 的服务端口)
- **局域网/远程**:开启 WebUI 远程访问后 `http://<你的IP>:25808/mcp`
鉴权:HTTP 头 `Authorization: Bearer <伙伴访问令牌>`
## 一、获取伙伴访问令牌
令牌**只存哈希、明文只在铸造时返回一次**,且**绑定到一个具体伙伴**(`{id}` = 伙伴 id)。两种获取方式:
### 桌面应用(本机可信客户端)
桌面 webview 自带本地信任,可直接调本地端点(也会有 UI 入口)。下文 `<companion-id>` 为要绑定的伙伴 id
```bash
# 铸造(返回明文一次,并绑定到该伙伴)
curl -X POST http://127.0.0.1:<loopback-port>/api/webui/companions/<companion-id>/access-token
# => {"success":true,"data":{"token":"<64位hex令牌>","companion_id":"<companion-id>"}}
# 若该伙伴尚无可用模型,data 还会带 "warning":"…"(令牌照常铸造,但 nomi_agent_run 等
# 需要模型的能力会失败,先去「模型管理」配置)
# 查询是否已配置(不返回令牌)
curl http://127.0.0.1:<loopback-port>/api/webui/companions/<companion-id>/access-token
# => {"success":true,"data":{"configured":true}}
# 吊销
curl -X DELETE http://127.0.0.1:<loopback-port>/api/webui/companions/<companion-id>/access-token
# => {"success":true,"data":{"configured":false}}
```
> 这些 `/api/webui/companions/{id}/access-token` 端点仅本地可信客户端可达(`require_local_trust`),远程浏览器拿不到。每个伙伴各持一枚令牌(再次铸造会覆盖旧令牌)。
### 无头服务器(headless `nomifun-web`
无头部署用环境变量在启动时播种,**绑定到默认伙伴**(仅当该令牌尚未配置时生效,不覆盖已有;若实例中尚无任何伙伴会跳过并告警):
```bash
NOMIFUN_COMPANION_TOKEN="$(openssl rand -hex 32)" \
nomifun-web --host 127.0.0.1 --port 8787
```
把这串 hex 作为客户端的 Bearer 令牌。
## 二、连接 MCP 客户端
### Claude Code / 通用 MCP 客户端(Streamable-HTTP
```json
{
"mcpServers": {
"nomifun": {
"type": "streamable-http",
"url": "http://127.0.0.1:25808/mcp",
"headers": { "Authorization": "Bearer <伙伴访问令牌>" }
}
}
}
```
连上后 `tools/list` 即可看到平台在 Remote 面暴露的工具(`nomi_*`);`tools/call` 驱动。
## 三、权限模型(Remote 面)
外部调用方落在 `Surface::Remote`,权限矩阵:
| 能力危险级 | Remote 行为 |
|---|---|
| 读 / 写 | 允许 |
| 破坏性(删除等) | 需确认:先返回 `{"needs_confirmation":true,...}`,agent 复述动作征得用户同意后,带 `"confirm": true` 重试 |
| 敏感(`secret.*` / `factory_reset` | **拒绝**(默认不在 Remote 暴露) |
被拒的工具**不出现在 `tools/list`**(更好的 UX + 纵深防御)。
## 四、能力继承
平台能力通过同一条能力总线(`nomifun-gateway` 的 Capability Registry)暴露到 MCP/HTTP/CLI/Skill 等外部面。新增能力时,应同时评估它是否适合 Remote surface、是否需要确认,以及是否应进入 `/mcp-agent` 精简集。
调用方**以令牌所绑定的伙伴身份运行**:继承该伙伴的 profile 模型、人格与知识库,伙伴之间彼此隔离。因此 `nomi_agent_run` 在不显式指定 `model` 时,会解析所绑定伙伴的 profile 模型——**该伙伴必须配置好可用模型**(否则铸造时会返回 `warning`,且需要模型的能力会失败)。
## 当前可用面
-**MCP**`/mcp`(全量 ~140 工具)+ `/mcp-agent`curated 干活子集)。
-**委派目标**`nomi_agent_run(goal,workspace?,model?,timeout_secs?)` 一句话把任务交给一个自治 nomi agent,跑完返回终稿;长任务返回 `{status:running}` 句柄,用 `nomi_agent_result(conversation_id)` 轮询。
-**HTTP REST**`POST /v1/tools/{name}``GET /v1/tools[?profile=agent]``GET /v1/openapi.json[?profile=agent]`OpenAPI 3.1,同令牌)。
-**CLI**`nomicore tools`(离线列能力)、`nomicore call <name> [json]``nomicore agent "<目标>"`(读 `NOMIFUN_URL`/`NOMIFUN_COMPANION_TOKEN``--url`/`--token`)。
-**Skill**`docs/skills/drive-nomifun/SKILL.md` —— 教外部 agent 如何连上并驱动 NomiFun(可发布到技能市场)。
-**Computer**:桌面版(`computer-use` 构建)暴露 `nomi_computer_*`snapshot/click/type/key/scroll/launch/screenshot/…),外部调用方可驱动桌面(headless/web 构建不含)。
-**流式**`POST /v1/tools/{name}/stream`SSE)—— 流式工具(如 `nomi_agent_run`)实时吐 `{type:..}` delta,末帧 `{type:"__result__"}` 带终值;非流式工具仅末帧。`nomi_agent_run` 已流式(订阅 agent 广播逐条转发)。
@@ -0,0 +1,236 @@
# Scheduled Tasks (Cron)
A scheduled task in NomiFun is a recurring (or one-shot) job that fires at a
time you choose and drives an AI agent to do something. You can configure it
from the Scheduled Tasks page, run it on demand, attach a personalised
**skill** so the agent always behaves the right way for that job, and you
can ask any agent in chat to manage tasks for you using a built-in cron
skill.
> Looking for one-off async work that should run as soon as possible, not on
> a clock? See [AutoWork & Requirements](./autowork-requirements.md). Need a
> live shell instead? See [In-App Terminals](./terminal.md).
![Scheduled tasks list](../images/cron-01-list.png)
## What a job does
`nomifun-cron` is a backend scheduler + executor:
- The **scheduler** computes the next fire time for each enabled job using
a 5-field (Unix) or 6-field (seconds-prefixed) cron expression — both are
accepted; a 5-field expression is normalised to 6 fields by prepending
`0` for seconds. Schedules can also be a single absolute timestamp
(`At { at_ms }`) or a fixed interval (`Every { every_ms }`).
- A timezone (e.g. `Asia/Shanghai`, `America/Los_Angeles`) is honoured per
job, so `0 9 * * MON` means 09:00 in **that** zone, not UTC.
- The **executor** drives the job's agent when the timer fires. Two
execution modes:
- **`new_conversation`** — start a fresh conversation per fire. The job
carries the workspace, agent, model, and prompt; the executor creates
the conversation, broadcasts a `cron_trigger` artifact (so the chat
UI shows "this conversation was started by a scheduled task"), and
sends the prompt.
- **`existing`** — reuse the conversation that owns the job. Each fire
sends the prompt as a new message in that same thread. Good for
"remind me", "summarise the day", or any job where continuity matters.
- A **busy guard** prevents the same conversation from being entered
concurrently. If the previous run is still going when the next fire
lands, the new run is skipped (logged as `skipped`).
- A **missed-trigger handler** runs at boot and after the OS wakes from
sleep (`/api/cron/internal/system-resume`). It walks every enabled job
whose `next_run` is in the past and emits a system message so you can
see that a fire was missed (e.g. while your laptop was asleep), then
re-arms the timer for the next cron tick.
- Each fire is recorded with a status — `ok` / `error` / `skipped` /
`missed` — and (when applicable) a link to the conversation that
resulted, so the detail page can show you the run history.
## Creating a job
Open **Scheduled Tasks** from the sidebar (route: `/scheduled`) and press
**New task**. The dialog covers four areas.
### Frequency
Pick from a small set of presets — `Manual` (no automatic schedule, fire
only via Run now), `Hourly`, `Daily`, `Weekdays` (`MON-FRI`), `Weekly`, or
`Custom`. Presets render an editable cron expression in the builder; pick
**Custom** to type one directly. The builder validates as you type.
Cron syntax cheat-sheet (5-field — seconds field is added automatically):
```
* * * * *
│ │ │ │ └─ day of week (06 or SUNSAT, MON-FRI works)
│ │ │ └──── month (112 or JANDEC)
│ │ └─────── day of month (131)
│ └────────── hour (023)
└───────────── minute (059)
```
The job's timezone is set on creation (defaults to your browser's IANA
zone) and stored on the row; if a job's stored zone is invalid for any
reason, the detail page offers a one-click repair to your local zone.
### Agent
Pick the agent that runs each fire. Three flavours show up in the picker:
- **CLI agents** — `claude` / `codex` / `gemini` (whatever the backend
detected on `PATH`). The job records the backend label and uses ACP
end-to-end.
- **Nomi (built-in)** — uses Nomi's own engine with your selected
provider/model.
- **Preset assistants** — pre-configured agent personalities; the job
records the assistant id.
The **Advanced** section lets you override the workspace (working
directory the agent runs in), the model, and arbitrary `config_options`
key/value pairs that get forwarded to the agent factory. No directory name
in a workspace path may begin or end with whitespace — that is enforced
server-side; the form will surface the error. Interior spaces
(`My Project`) are fine.
### Execution mode
Choose `new_conversation` or `existing` (called "specified conversation"
in the UI when you also pick which one). The detail page later shows you
the resulting conversation(s).
### Prompt + name
The **prompt** is what gets sent to the agent each time. Write it as a
**self-contained instruction** — the agent will not get to see your
original "I want this" framing, only this prompt. Patterns like:
- `Reply with a short weekly meeting reminder that includes the current date and time.`
- `Search for the latest AI news from this week and produce a concise bullet-point summary report.`
- `Run the weekly database health check and post the results back here.`
…work better than restating the user's wish. **Name** is just a label.
![Create scheduled task dialog](../images/cron-02-create-dialog.png)
## Running, pausing, deleting
The list view (`/scheduled`) shows every job, its next fire, and an enable
toggle. From the detail page (`/scheduled/:job_id`) you can:
- **Run now** — fires the job immediately, regardless of schedule. The
busy guard still applies.
- **Pause / Resume** — stops further fires without deleting the row.
- **Edit** — same dialog as create, in edit mode.
- **Delete** — removes the job and its per-job skill directory. Conversations
created by previous runs remain in the conversation list and can be deleted
separately.
The detail page also lists the conversations created by this job, sorted
by activity — useful when the job runs in `new_conversation` mode and
fans out one thread per fire.
![Scheduled task detail](../images/cron-03-detail.png)
## Keep-Awake
Cron jobs only fire while the host process is running. The list page has a
**Keep system awake while NomiFun is running** toggle that asks the OS to
inhibit sleep (Windows: `SetThreadExecutionState`, macOS: `caffeinate`,
Linux: `systemd-inhibit` where available) so jobs you set up on a laptop
do not silently miss their fires the moment the lid closes.
If a fire is missed because the system slept anyway (or NomiFun was not
running), the missed-trigger handler at next boot/wake will record a
`missed` run and post a system message into the affected conversation,
then re-arm the timer for the next normal fire.
## Skills attached to a job
A **skill** is a `SKILL.md` file the agent reads when it joins a session
— same mechanism the rest of Nomi uses, but with a per-job scope. You can
write/edit the skill on the detail page; behind the scenes the file is
written to the data directory under `cron/skills/cron-<job_id>/SKILL.md`,
and the executor injects it into the agent's session each fire.
Use cases:
- A consistent **persona** for that job's output (style, tone, format).
- **Tool/MCP** preferences (which servers to enable, which to ignore).
- Workspace-specific conventions (commit message style, directory
layout, deployment quirks).
The job has its own skill directory (named with the job id, prefixed
`cron-`), so two jobs sharing the same workspace can carry different
behaviour without colliding. Deleting the job removes its skill
directory.
There is also an automatic **skill-suggest** detector that watches the
agent's output during a run; when it produces a clean candidate skill
(matching the expected format and not just a placeholder template), the
detector creates a `skill_suggest` artifact in the conversation so you
can review and save it as the job's skill in one click.
## Managing tasks from chat — the built-in `cron` skill
NomiFun ships a built-in auto-inject skill named `cron` that any agent can
load when you ask it to "set up a reminder", "schedule X every Monday",
etc. The conversation middleware then watches the agent's reply for the
following directive blocks and runs them through the cron service:
| Directive | Meaning |
| -------------------- | ------------------------------------------------------- |
| `[CRON_LIST]` | List the cron jobs scoped to the current conversation. |
| `[CRON_CREATE]…[/CRON_CREATE]` | Create a job (fields: `name`, `schedule`, `schedule_description`, `message`). |
| `[CRON_UPDATE: <id>]…[/CRON_UPDATE]` | Update an existing job in place. |
| `[CRON_DELETE: <id>]` | Delete a job by id. |
The middleware **strips** these blocks from what the user sees and posts
the system response (`Created cron job 'X'`, `No scheduled tasks`, etc.)
back into the conversation. So in chat it looks like a normal back-and-
forth; behind the scenes the agent emitted a directive and the platform
executed it.
The skill is constrained to **one task per conversation** by design —
this keeps the loop simple ("query, then act") and avoids duplicate jobs
piling up when you re-ask. To manage many jobs at once, use the
Scheduled Tasks page directly.
## Routes & API
| What | Where |
| ------------------------------- | ---------------------------------------------------------------- |
| List page | `/scheduled` |
| Detail page | `/scheduled/:job_id` |
| List / create job | `GET /api/cron/jobs`, `POST /api/cron/jobs` |
| Get / update / delete | `GET|PUT|DELETE /api/cron/jobs/:id` |
| Run now | `POST /api/cron/jobs/:id/run` |
| List conversations for a job | `GET /api/cron/jobs/:id/conversations` |
| Per-job skill | `GET|POST|DELETE /api/cron/jobs/:id/skill` |
| System resume (internal) | `POST /api/cron/internal/system-resume` (requires internal hdr) |
Realtime events the UI subscribes to: `cron.job-created`,
`cron.job-updated`, `cron.job-removed`, and `cron.job-executed`. A missed
fire is represented as a `cron.job-executed` payload whose status is
`missed`.
## Troubleshooting
- **The job did not fire on time.** Was the host running and awake at
that moment? If you closed the laptop or the app, look at the next
conversation entry after wake — the missed-trigger handler will have
posted a `missed` notice and re-armed the timer.
- **My cron expression is rejected.** Both 5-field (`m h dom mon dow`)
and 6-field (`s m h dom mon dow`) forms are valid. Validate it locally
with [crontab.guru](https://crontab.guru/) or the in-dialog builder.
- **Jobs run but the agent does the wrong thing.** Re-read the prompt as
if you had no other context. It must tell the agent exactly what to
produce. Then consider attaching a skill to lock in the behaviour.
- **Two scheduled fires collide.** The busy guard skips overlapping
runs in `existing` mode (the run is recorded as `skipped`). If you
expect long-running fires, switch the job to `new_conversation` so
each fire gets its own thread.
- **A `cron` directive in chat did nothing.** The middleware no-ops if
the cron service is not wired (e.g. some test harnesses); in a normal
app build it is always wired. If a directive is malformed (missing
closing tag, missing `schedule`), it is silently dropped — re-prompt
the agent with cleaner input.
@@ -0,0 +1,221 @@
# 定时任务 (Cron)
NomiFun 中的一个定时任务是一个在你选择的时间触发的循环 (或一次性)
任务,它会驱动一个 AI agent 去做某件事。你可以从定时任务页面
配置它、按需运行它、给它附加一个个性化的**技能**让 agent 在
该任务下始终以正确的方式行事,并且你可以使用一个内置的 cron
技能在聊天中让任何 agent 帮你管理任务。
> 找的是应该尽快运行的一次性异步工作,而不是按时钟来的?参见
> [AutoWork & Requirements](./autowork-requirements.md)。需要一个
> 实时 shell?参见 [应用内终端](./terminal.zh.md)。
![定时任务列表](../images/cron-01-list.png)
## 一个任务做什么
`nomifun-cron` 是一个后端调度器 + 执行器:
- **调度器**用一个 5 字段 (Unix) 或 6 字段 (秒前缀) 的 cron 表达式
为每个已启用的任务计算下一次触发时间 —— 两者都接受;
5 字段表达式会通过在前面加 `0` 作为秒被规范化为 6 字段。
调度也可以是一个绝对时间戳 (`At { at_ms }`) 或一个固定间隔
(`Every { every_ms }`)。
- 每个任务的时区 (例如 `Asia/Shanghai``America/Los_Angeles`)
会被尊重,所以 `0 9 * * MON` 表示**那个**时区的 09:00,而不是 UTC。
- **执行器**在定时器触发时驱动该任务的 agent。两种执行模式:
- **`new_conversation`** —— 每次触发开启一个新会话。该任务
携带 workspace、agent、model 和 prompt;执行器会创建会话、
广播一个 `cron_trigger` 工件 (这样聊天 UI 会显示
"本会话由一个定时任务发起"),然后发送 prompt。
- **`existing`** —— 复用拥有该任务的会话。每次触发把 prompt
作为一条新消息发送到同一个线程中。适合 "提醒我"、
"总结今天" 或任何延续性重要的任务。
- 一个**忙碌守卫**防止同一个会话被并发进入。如果上一次运行
在下一次触发到来时还在进行中,新的运行会被跳过 (记录为 `skipped`)。
- 一个**漏触发处理器**会在启动时和操作系统从睡眠中醒来后运行
(`/api/cron/internal/system-resume`)。它会遍历每个 `next_run`
在过去的已启用任务并发出一条系统消息,让你能看到漏掉一次
触发 (例如笔记本休眠时),然后为下一个 cron 节拍重新装定定时器。
- 每次触发会以一个状态被记录 —— `ok` / `error` / `skipped` /
`missed` —— 并且 (在适用时) 附带一个指向所产生会话的链接,
这样详情页可以向你展示运行历史。
## 创建一个任务
从侧边栏打开 **定时任务** (路由:`/scheduled`) 并按
**New task**。对话框涵盖四个区域。
### 频率
从一组小的预设中选择 —— `Manual` (无自动调度,仅通过 Run now 触发)、
`Hourly``Daily``Weekdays` (`MON-FRI`)、`Weekly`
`Custom`。预设会在 builder 中渲染出可编辑的 cron 表达式;选
**Custom** 直接键入。Builder 在你键入时进行校验。
Cron 语法速查 (5 字段 —— 秒字段会自动添加):
```
* * * * *
│ │ │ │ └─ 星期几 (06 或 SUNSATMON-FRI 可用)
│ │ │ └──── 月 (112 或 JANDEC)
│ │ └─────── 月中第几天 (1–31)
│ └────────── 小时 (0–23)
└───────────── 分钟 (0–59)
```
任务的时区在创建时设置 (默认是你浏览器的 IANA 时区) 并存储在
该行中;如果一个任务存储的时区因故无效,详情页会提供一键
修复到你的本地时区。
### Agent
选择每次触发运行的 agent。选择器中会显示三种类型:
- **CLI agent** —— `claude` / `codex` / `gemini` (后端在 `PATH`
上检测到的任何一个)。该任务记录后端标签并端到端使用 ACP。
- **Nomi (内置)** —— 使用 Nomi 自有引擎以及你选择的
provider/model。
- **Preset assistant** —— 预先配置的 agent 人格;该任务记录
assistant id。
**Advanced** 部分让你覆盖 workspace (agent 的工作目录)、model
以及任意的 `config_options` 键值对,它们会被转发给 agent 工厂。
Workspace 路径不能包含空白片段 —— 这一点在服务端被强制;
表单会把错误显式呈现出来。
### 执行模式
选择 `new_conversation``existing` (在 UI 中当你同时选择具体
是哪一个时被称为 "specified conversation")。详情页之后会向你
展示得到的会话。
### Prompt + 名称
**Prompt** 是每次发送给 agent 的内容。请把它写成一个**自包含的
指令** —— agent 看不到你原本 "我想要这个" 的框架,只看到这个
prompt。诸如下面这些模式:
- `Reply with a short weekly meeting reminder that includes the current date and time.`
- `Search for the latest AI news from this week and produce a concise bullet-point summary report.`
- `Run the weekly database health check and post the results back here.`
…比重述用户愿望要好。**Name** 只是一个标签。
![创建定时任务对话框](../images/cron-02-create-dialog.png)
## 运行、暂停、删除
列表视图 (`/scheduled`) 显示每一个任务、它的下一次触发,以及
一个启用开关。在详情页 (`/scheduled/:job_id`) 你可以:
- **Run now** —— 立即触发该任务,无视调度。忙碌守卫仍然适用。
- **Pause / Resume** —— 停止后续触发但不删除该行。
- **Edit** —— 与创建相同的对话框,处于编辑模式。
- **Delete** —— 删除该任务及其按任务生成的技能目录。此前运行创建的
会话会保留在会话列表中,可按需单独删除。
详情页还会列出本任务创建的会话,按活跃度排序 —— 当任务
`new_conversation` 模式运行并为每次触发各分出一个线程时
非常有用。
![定时任务详情](../images/cron-03-detail.png)
## 保持唤醒
只有当宿主进程在运行时,cron 任务才会触发。列表页有一个
**NomiFun 运行时保持系统唤醒**开关,它会请求 OS 抑制睡眠
(Windows`SetThreadExecutionState`macOS`caffeinate`
Linux 上若可用为 `systemd-inhibit`),这样你在笔记本上设置的
任务不会在合上盖子那一刻悄悄漏触发。
如果一次触发还是因为系统进入睡眠 (或 NomiFun 没在运行) 而漏掉,
下一次启动/唤醒时的漏触发处理器会记录一次 `missed` 运行并
向受影响的会话中投送一条系统消息,然后为下一次正常触发
重新装定定时器。
## 附加到任务的技能
一个**技能**是一个 `SKILL.md` 文件,agent 会在加入会话时读取它
—— 与 NomiFun 其他地方使用的相同机制,但作用域是按任务的。
你可以在详情页编写/编辑该技能;在幕后该文件会被写入数据
目录下的 `cron/skills/cron-<job_id>/SKILL.md`,执行器会在每次
触发时把它注入到 agent 的会话中。
用例:
- 任务输出的一致**人格** (风格、语气、格式)。
- **工具/MCP** 偏好 (启用哪些服务器、忽略哪些)。
- 工作区特定的约定 (commit 信息风格、目录布局、部署细节)。
任务有自己的技能目录 (以 job id 命名,前缀 `cron-`),所以共享同一
workspace 的两个任务可以承载不同的行为而不冲突。删除任务会
移除其技能目录。
还有一个自动的**技能建议**检测器,它会在运行期间观察 agent 的
输出;当它产出一个干净的候选技能时 (符合预期格式且不只是占位
模板),检测器会在会话中创建一个 `skill_suggest` 工件,让你
可以审查并一键将其保存为该任务的技能。
## 在聊天中管理任务 —— 内置的 `cron` 技能
NomiFun 附带一个名为 `cron` 的内置自动注入技能,任何 agent 都可以
在你让它"设置一个提醒"、"每周一安排 X" 等时加载它。
然后会话中间件会观察 agent 的回复中以下的指令块,并通过 cron
服务运行它们:
| 指令 | 含义 |
| --------------------- | ------------------------------------------------------- |
| `[CRON_LIST]` | 列出当前会话作用域内的 cron 任务。 |
| `[CRON_CREATE]…[/CRON_CREATE]` | 创建一个任务 (字段:`name``schedule``schedule_description``message`)。 |
| `[CRON_UPDATE: <id>]…[/CRON_UPDATE]` | 原地更新一个已有任务。 |
| `[CRON_DELETE: <id>]` | 按 id 删除一个任务。 |
中间件会从用户看到的内容中**剥离**这些块,并把系统响应
(`Created cron job 'X'``No scheduled tasks` 等) 投回到会话中。
所以在聊天里看起来像是正常的来回;幕后是 agent 发出了一个
指令,平台执行了它。
该技能在设计上被限制为**每个会话一个任务** —— 这让循环保持
简单 ("查询,然后行动") 并避免了你重新询问时重复任务堆积。
要一次管理多个任务,请直接使用定时任务页面。
## 路由 & API
| 内容 | 位置 |
| ------------------------------- | ----------------------------------------------------------------- |
| 列表页面 | `/scheduled` |
| 详情页面 | `/scheduled/:job_id` |
| 列出 / 创建任务 | `GET /api/cron/jobs``POST /api/cron/jobs` |
| 获取 / 更新 / 删除 | `GET|PUT|DELETE /api/cron/jobs/:id` |
| 立即运行 | `POST /api/cron/jobs/:id/run` |
| 列出某任务的会话 | `GET /api/cron/jobs/:id/conversations` |
| 每任务技能 | `GET|POST|DELETE /api/cron/jobs/:id/skill` |
| 系统恢复 (内部) | `POST /api/cron/internal/system-resume` (需要内部 header) |
UI 订阅的实时事件:`cron.job-created``cron.job-updated`
`cron.job-removed``cron.job-executed`。漏触发会作为一次
`cron.job-executed` 事件上报,payload 中的状态是 `missed`
## 故障排查
- **任务没有按时触发。** 那时宿主在运行且处于唤醒状态吗?
如果你合上了笔记本或关闭了应用,请查看唤醒后的下一条会话
条目 —— 漏触发处理器会投送一条 `missed` 通知并重新装定
定时器。
- **我的 cron 表达式被拒绝了。** 5 字段 (`m h dom mon dow`)
和 6 字段 (`s m h dom mon dow`) 形式都是合法的。在本地用
[crontab.guru](https://crontab.guru/) 或对话框中的 builder
校验。
- **任务运行了但 agent 做错了事。** 重新阅读 prompt,假设你没有
其他上下文。它必须告诉 agent 准确要产出什么。然后考虑附加
一个技能以锁定行为。
- **两次定时触发碰撞了。** 忙碌守卫会在 `existing` 模式下跳过
重叠的运行 (该运行被记录为 `skipped`)。如果你预期触发会
长时间运行,请把任务切换到 `new_conversation` 模式,让每次
触发各得一个线程。
- **聊天中的 `cron` 指令什么也没做。** 如果 cron 服务没有接入
(例如某些测试 harness),中间件会 no-op;在正常 app build 中
它总是接入的。如果指令格式错误 (缺失闭合标签、缺失
`schedule`),它会被静默丢弃 —— 用更干净的输入重新提示
agent。
+183
View File
@@ -0,0 +1,183 @@
# In-App Terminals
Nomi ships a real terminal inside the app. Each terminal is a backend-managed
PTY session you can drive interactively from your browser/desktop window — and
that AutoWork can drive on your behalf when you bind it to a tag.
> Need the automation guide? See [AutoWork & Requirements](./autowork-requirements.md).
> Need to run an agent on a schedule? See [Scheduled Tasks](./scheduled-tasks.md).
![Nomi in-app terminal](../images/terminal-01-session.png)
## What an in-app terminal is
When you create a terminal, the backend (`nomifun-terminal`) spawns a child
process attached to a real pseudo-terminal via [`portable-pty`]. The session
has three pieces:
- **Persistent metadata** — id, name, working directory, command + args, env,
preset/backend, permission mode, current size (cols × rows), pinned flag,
exit status. Stored in SQLite so the session entry survives restarts.
- **A live PTY** (only while the child is running) — the OS pseudo-terminal,
its byte-stream output, and a scrollback buffer the backend keeps for late
joiners.
- **Realtime events on the WebSocket bus** — every chunk of PTY output is
base64-encoded and broadcast as `terminal.output`. Lifecycle events
(`terminal.created`, `terminal.updated`, `terminal.exit`, `terminal.removed`)
ride the same bus. The xterm.js view in the renderer subscribes and renders
the stream.
A PTY child cannot be paused or moved between processes: when the child exits,
the row stays but the live PTY is gone. Re-launching is in-place — the same
session id keeps a fresh process attached, so you do not get a new sidebar
entry every time you restart a CLI.
[`portable-pty`]: https://crates.io/crates/portable-pty
## Creating a terminal
Open the Terminal create page (the **+** button in the terminal sidebar
section, or navigate to `/terminal-new`). You pick five things:
1. **Workspace** — the working directory the child process will be spawned in.
Recent workspaces are remembered.
2. **Preset**`Shell`, `Claude Code`, `Codex`, or `Gemini`. The shell preset
resolves to your platform's login shell at launch time (Windows:
PowerShell/`cmd`, macOS/Linux: `$SHELL`); the agent presets launch the
matching CLI binary that must already be installed and on `PATH`.
3. **Permission mode** (agent presets only) — `Default` (interactive
approvals) or `Full Auto` (the CLI's own non-interactive flag is added):
| Preset | Full-auto flag |
| ------------ | ------------------------------------------- |
| `claude` | `--dangerously-skip-permissions` |
| `codex` | `--dangerously-bypass-approvals-and-sandbox`|
| `gemini` | `--yolo` |
These bypass the CLI's interactive approval prompt — needed for AutoWork to
drive a turn end-to-end without a human pressing Enter, but the same flags
give the CLI broad capability on your machine. Treat full-auto terminals
like a logged-in shell.
4. **Launch command** — the dialog renders the resolved `command + args` into
an editable field. Tweak it freely (extra flags, alternative entry point,
etc.) before pressing **Launch**.
5. **Knowledge bases** (optional) — multi-select one or more knowledge bases
to bind to this session. Bound bases are mounted at
`{workspace}/.nomi/knowledge/` before the child spawns, together with a
generated `README.md` (retrieval protocol + per-base digests + TOC +
write-back rules); the `claude` preset additionally gets an
`--append-system-prompt` pointer to that README. Rebinding takes effect on
the next re-launch. (The gateway tool `nomi_create_terminal` accepts the
same binding via `knowledge_base_ids`.)
![Terminal create page](../images/terminal-02-create-page.png)
The backend persists the row and spawns the child. The page navigates to
`/terminal/<id>` and you start receiving live output.
## Driving a terminal
The session page is xterm.js wired to the realtime stream:
- **Type** to send keystrokes to the PTY. The send box also accepts paste with
bracketed-paste markers, so multi-line text becomes one paste rather than a
flurry of Enters.
- **Resize** the panel and the backend resizes the PTY accordingly (`SIGWINCH`
is delivered to the child). The new dimensions are persisted.
- **Re-launch** when the child has exited: a single button kills any leftover
PTY for the same id, spawns a fresh process with the stored command + cwd
+ env, clears the view, and the same `terminal.<id>` subscription picks up
the new output. You keep the same sidebar entry.
- **Rename / pin** from the session header (renames broadcast as
`terminal.updated`; pinned terminals float to the top of the sidebar).
- **Kill** stops the child but keeps the row (it transitions to `exited` and
becomes re-launchable). **Delete** kills the child and removes the row
entirely.
![Driving a terminal session](../images/terminal-03-driving-session.png)
## Streaming model
Output flows over a single WebSocket. While you are looking at a session, your
client receives `terminal.output` events for that id and renders them. The
backend keeps a scrollback buffer in memory while the PTY is live: when you
open a terminal that is already running, the GET response includes a
base64-encoded `scrollback_b64` snapshot, so xterm replays history before live
events stream in.
Client-to-server input goes the other direction over a small REST endpoint
(base64-encoded bytes). The backend writes those bytes straight to the PTY's
stdin.
## Terminals as automation targets
The same in-memory PTY map that powers the UI is shared with the **AutoWork
orchestrator** in `nomifun-requirement` via the `TerminalDriver` trait. That
trait lets AutoWork:
- Subscribe to a copy of the terminal's live output (it watches for completion
markers and detects quiescence — see the AutoWork guide for the contract).
- Write input bytes to the PTY (it injects the requirement prompt wrapped in
bracketed-paste so a multi-line instruction lands as a single paste).
- Check liveness, read the row's metadata (user, backend, mode), and read or
write a per-terminal `autowork` config blob.
In other words: **a terminal you create here is automatable by AutoWork**.
Bind a tag from the AutoWork toolbar in the session header, and the
orchestrator will start claiming requirements and feeding them to the CLI
running in this terminal. Only agent-CLI terminals (`claude`, `codex`,
`gemini`) are eligible — a plain shell can be driven manually but is not an
AutoWork target. The orchestrator also recommends Full Auto mode, because a
turn that hits an interactive approval prompt will block until it times out.
If the workspace has knowledge bases mounted (`{cwd}/.nomi/knowledge/`
exists), AutoWork- and cron-driven prompts are automatically prefixed with a
one-line hint pointing the CLI at the mounted `README.md` before it starts
working.
If the PTY exits while AutoWork is still bound, the loop does not stop — it
idles and waits for you to re-launch the terminal, then resumes claiming
where it left off. If you delete the row, the loop stops for good.
## IDMM (decision-stall supervision)
Long-running CLI sessions sometimes stall: the provider drops, the model
spins on a tool call, the CLI prints a confirmation prompt nobody answers.
The IDMM (Intelligent Decision-Making Mode) supervisor watches a session and
intervenes — first with rule-based nudges (no LLM), then by calling a sidecar
backup model — so the turn reaches a terminal state instead of hanging until
the AutoWork timeout fires.
You can enable IDMM per-terminal from the same session header (the **IDMM**
control next to AutoWork). It works whether or not AutoWork is also bound;
when both are on, AutoWork ensures IDMM is supervising for the duration of
each turn.
## Routes & API
| What | Where |
| -------------------------- | ------------------------------------------- |
| Create page | `/terminal-new` |
| Session page | `/terminal/:id` |
| List / create | `GET /api/terminals`, `POST /api/terminals` |
| Get / update / delete | `GET|PATCH|DELETE /api/terminals/:id` |
| Send input | `POST /api/terminals/:id/input` |
| Resize | `POST /api/terminals/:id/resize` |
| Kill child | `POST /api/terminals/:id/kill` |
| Re-launch in place | `POST /api/terminals/:id/relaunch` |
| Live output / lifecycle | WebSocket events `terminal.*` |
## Troubleshooting
- **The CLI is not found.** The agent presets call `claude`, `codex`, or
`gemini` directly — they must be on the `PATH` of whatever account is
running the backend. Either install the CLI globally or edit the launch
command to use an absolute path before launching.
- **AutoWork bind is greyed out.** Only `claude`/`codex` terminals are
AutoWork targets today. A plain shell preset cannot be bound, and Gemini
terminal AutoWork is not wired into the backend completion contract yet.
- **Re-launch keeps reusing the same env / cwd.** That is intentional — the
session row stores them. To change them, create a new terminal with the
desired settings.
- **The output is garbled after resize.** Some TUIs need a redraw on
`SIGWINCH`. Press `Ctrl-L` (or your CLI's redraw shortcut).
+167
View File
@@ -0,0 +1,167 @@
# 应用内终端
Nomi 在应用内附带了一个真正的终端。每个终端都是一个由后端管理的
PTY 会话,你可以从浏览器/桌面窗口中以交互方式驱动它 —— 当你把它
绑定到一个 tag 上时,AutoWork 也可以代你来驱动它。
> 需要自动化指南?参见 [AutoWork & Requirements](./autowork-requirements.md)。
> 需要按计划运行 agent?参见 [定时任务](./scheduled-tasks.zh.md)。
![Nomi 应用内终端](../images/terminal-01-session.png)
## 应用内终端是什么
当你创建一个终端时,后端 (`nomifun-terminal`) 会通过
[`portable-pty`] 派生一个连接到真实伪终端的子进程。该会话由三部分组成:
- **持久化元数据** —— id、名称、工作目录、命令 + 参数、env、
preset/backend、权限模式、当前尺寸 (列 × 行)、pinned 标记、
退出状态。存储在 SQLite 中,所以会话条目在重启后仍然存在。
- **一个活跃的 PTY** (仅在子进程运行时存在) —— OS 伪终端、
其字节流输出,以及后端为后加入者保留的回滚缓冲区。
- **WebSocket 总线上的实时事件** —— PTY 输出的每一块都会被
base64 编码并以 `terminal.output` 广播。生命周期事件
(`terminal.created``terminal.updated``terminal.exit``terminal.removed`)
也走同一条总线。渲染进程中的 xterm.js 视图订阅并渲染这条流。
PTY 子进程不能被暂停或在进程间迁移:当子进程退出时,
列表行保留,但活跃的 PTY 没了。重新启动是原地进行的 —— 同一个会话 id
会附上一个全新的进程,所以你不会每次重启 CLI 都得到一个新的侧边栏
条目。
[`portable-pty`]: https://crates.io/crates/portable-pty
## 创建终端
打开终端创建页面 (终端侧边栏区段中的 **+** 按钮,或导航到
`/terminal-new`)。你需要选择五样东西:
1. **Workspace** —— 子进程将在其中派生的工作目录。
最近使用过的 workspace 会被记住。
2. **Preset** —— `Shell``Claude Code``Codex``Gemini`。shell
preset 会在启动时解析为你平台的 login shell (Windows
PowerShell/`cmd`macOS/Linux`$SHELL`)agent preset 会启动
对应的 CLI 二进制,该二进制必须已安装并在 `PATH` 上。
3. **权限模式** (仅 agent preset) —— `Default` (交互式审批)
`Full Auto` (会附加该 CLI 自身的非交互式 flag):
| Preset | Full-auto flag |
| ------------ | ------------------------------------------- |
| `claude` | `--dangerously-skip-permissions` |
| `codex` | `--dangerously-bypass-approvals-and-sandbox`|
| `gemini` | `--yolo` |
这些 flag 会绕过 CLI 的交互式审批提示 —— 这是 AutoWork 在没有
人按回车的情况下端到端驱动一轮所必需的,但同样的 flag 也赋予了
CLI 在你机器上的广泛能力。请把 full-auto 终端当作已登录的 shell 来对待。
4. **启动命令** —— 对话框会把解析后的 `command + args` 渲染到
一个可编辑字段中。在按下 **Launch** 之前可以自由调整 (额外
flag、替代入口点等)。
5. **知识库** (可选) —— 多选一个或多个知识库绑定到本会话。绑定的库
会在子进程派生前挂载到 `{workspace}/.nomi/knowledge/`,并生成一份
`README.md` (检索协议 + 各库梗概 + TOC + 回写规则);`claude`
preset 还会额外附加一条指向该 README 的 `--append-system-prompt`
指针。改绑在下次重新启动时生效。(网关工具 `nomi_create_terminal`
通过 `knowledge_base_ids` 支持同样的绑定。)
![终端创建页面](../images/terminal-02-create-page.png)
后端会持久化该行并派生子进程。页面会跳转到
`/terminal/<id>`,然后你开始接收实时输出。
## 驱动终端
会话页面是与实时流相连的 xterm.js:
- **键入** 把击键发送给 PTY。发送框也接受带 bracketed-paste 标记
的粘贴,所以多行文本会变成一次粘贴而不是一连串的回车。
- **调整大小** 调整面板大小,后端会相应调整 PTY 尺寸 (会向子进程
发送 `SIGWINCH`)。新的尺寸会被持久化。
- **重新启动** 在子进程退出后:单个按钮会杀掉同一 id 的任何残留
PTY,使用存储的命令 + cwd + env 派生一个新的进程,清空视图,
同样的 `terminal.<id>` 订阅会接管新的输出。你保留同一个侧边栏条目。
- **重命名 / 置顶** 从会话头进行 (重命名会作为
`terminal.updated` 广播;置顶的终端会浮到侧边栏顶部)。
- **Kill** 停止子进程但保留行 (它会转换为 `exited` 并可重新启动)。
**Delete** 杀掉子进程并完全移除该行。
![驱动一个终端会话](../images/terminal-03-driving-session.png)
## 流模型
输出走单个 WebSocket。当你正在查看一个会话时,你的客户端
会接收到该 id 的 `terminal.output` 事件并渲染它们。在 PTY 活跃期间,
后端在内存中保留一个回滚缓冲区:当你打开一个已经在运行的
终端时,GET 响应会包含一个 base64 编码的 `scrollback_b64` 快照,
所以 xterm 会先回放历史记录,然后实时事件再流入。
客户端到服务器的输入走另一个方向,通过一个小的 REST 端点
(base64 编码的字节)。后端会把这些字节直接写到 PTY 的 stdin。
## 终端作为自动化目标
驱动 UI 的同一个内存中的 PTY 映射通过 `TerminalDriver` trait
`nomifun-requirement` 中的 **AutoWork orchestrator** 共享。该
trait 让 AutoWork
- 订阅终端实时输出的副本 (它会监视完成标记并检测静默 ——
契约见 AutoWork 指南)。
- 向 PTY 写入输入字节 (它把 requirement prompt 包装在
bracketed-paste 中注入,使得多行指令会作为单次粘贴落地)。
- 检查存活性,读取该行的元数据 (user、backend、mode),并读取或
写入每个终端的 `autowork` 配置 blob。
换句话说:**你在这里创建的终端可被 AutoWork 自动化**。
在会话头的 AutoWork 工具栏上绑定一个 tagorchestrator
就会开始认领 requirement 并把它们喂给运行在该终端中的 CLI。
只有 agent-CLI 终端 (`claude``codex``gemini`) 才符合条件 ——
普通的 shell 可以手动驱动但不是 AutoWork 目标。orchestrator 也
推荐使用 Full Auto 模式,因为一轮如果撞上交互式审批提示
会一直阻塞直到超时。
如果工作区挂载了知识库 (存在 `{cwd}/.nomi/knowledge/`)AutoWork 与
cron 驱动注入的 prompt 会自动前置一行提示,让 CLI 先阅读挂载目录里的
`README.md` 再开工。
如果在 AutoWork 仍绑定时 PTY 退出,循环不会停止 —— 它会
空转并等待你重新启动该终端,然后从中断处继续认领。
如果你删除该行,循环会彻底停止。
## IDMM (决策停滞监督)
长时间运行的 CLI 会话有时会停滞:provider 掉线,模型在某个工具
调用上空转,CLI 打印了一个无人回答的确认提示。IDMM
(Intelligent Decision-Making Mode) supervisor 会监视会话并介入 ——
先用基于规则的轻推 (无 LLM),然后调用一个 sidecar 备用模型 ——
这样这一轮会到达一个终态,而不是挂起到 AutoWork 超时触发。
你可以在同一个会话头 (AutoWork 旁边的 **IDMM** 控件) 中按终端
启用 IDMM。无论 AutoWork 是否同时绑定它都能工作;当两者都开启
时,AutoWork 会确保 IDMM 在每一轮的全程都在监督。
## 路由 & API
| 内容 | 位置 |
| ------------------------ | --------------------------------------------- |
| 创建页面 | `/terminal-new` |
| 会话页面 | `/terminal/:id` |
| 列出 / 创建 | `GET /api/terminals``POST /api/terminals` |
| 获取 / 更新 / 删除 | `GET|PATCH|DELETE /api/terminals/:id` |
| 发送输入 | `POST /api/terminals/:id/input` |
| 调整大小 | `POST /api/terminals/:id/resize` |
| 杀掉子进程 | `POST /api/terminals/:id/kill` |
| 原地重新启动 | `POST /api/terminals/:id/relaunch` |
| 实时输出 / 生命周期 | WebSocket 事件 `terminal.*` |
## 故障排查
- **找不到 CLI。** Agent preset 直接调用 `claude``codex`
`gemini` —— 它们必须在运行后端的账户的 `PATH` 上。要么全局安装
CLI,要么在启动前编辑启动命令使用绝对路径。
- **AutoWork 绑定是灰色的。** 当前只有 `claude`/`codex` 终端才是
AutoWork 目标。普通 shell preset 不能被绑定;Gemini 终端 AutoWork
还没有接入后端的完成契约。
- **重新启动一直复用同一个 env / cwd。** 这是有意为之 —— 会话
行存储着它们。要修改它们,请用想要的设置创建一个新的终端。
- **调整大小后输出乱了。** 一些 TUI 在 `SIGWINCH` 时需要重绘。
`Ctrl-L` (或你 CLI 的重绘快捷键)。
@@ -0,0 +1,295 @@
# Web Server Deployment
`nomifun-web` is the **headless, self-host** way to run NomiFun. It is the same Rust backend that the [desktop app](./desktop-app.md) embeds, but built as a standalone binary that also serves the SPA (`ui/dist`) on the same port. There is no GUI, no WebView, no `DISPLAY` requirement — it runs anywhere a Linux/macOS/Windows server will run a static binary.
Unlike the desktop shell, **`nomifun-web` requires authentication by default**. The first browser visitor either creates the admin account interactively (first-run setup), or you pre-seed credentials with `NOMIFUN_ADMIN_PASSWORD`.
> If you want to expose an *existing* desktop install for remote access without setting up a server, see [WebUI Remote Access](./webui-remote-access.md). That is a per-instance feature; this guide is for a dedicated server.
```text
Browser / phone / LAN nomifun-web (one process, one port)
┌──────────────────┐ ┌───────────────────────────────────────┐
│ SPA + login │ HTTP / WS │ axum router │
│ (ui/dist) │ ────────────► │ ├─ / → SPA (ui/dist) │
└──────────────────┘ │ ├─ /api/* → REST handlers │
│ ├─ /ws → WebSocket events │
│ └─ /login … → auth (on by default)│
│ │
│ embedded backend (nomifun-app) │
│ └─ SQLite · agents · cron · channels │
└───────────────────────────────────────┘
```
## Quick start
### Run the binary directly
```bash
cargo build --release -p nomifun-web
./target/release/nomifun-web --host 127.0.0.1 --port 8787 \
--data-dir ./data --dist ./ui/dist
```
Then open `http://127.0.0.1:8787` and the first visit lets you create the admin account. After that, the setup endpoint returns `409 Conflict` and the only way in is via the login form (or `NOMIFUN_ADMIN_PASSWORD`).
![First-run admin setup screen](../images/webserver-02-first-run-setup.png)
### Or via Cargo, from the repo
```bash
bun install
bun run build:ui # produces ui/dist
cargo run -p nomifun-web # picks up the default --dist=../../ui/dist
```
## CLI flags and environment variables
All flags below are read by `apps/web/src/main.rs`. Each has an environment-variable counterpart for systemd / Docker / orchestrators.
| Flag | Env var | Default | Purpose |
|---|---|---|---|
| `--host` | `NOMIFUN_WEB_HOST` | `127.0.0.1` | IP to bind on. `0.0.0.0` accepts LAN/VPN/public traffic; pre-seed the admin or complete first-run setup before broad exposure. |
| `--port` | `NOMIFUN_WEB_PORT` | `8787` | TCP port. Serves the API, the WebSocket at `/ws`, and the SPA. |
| `--data-dir` | `NOMIFUN_DATA_DIR` | per-user dir | Backend data dir (SQLite database, agent state, logs, Bun cache). Defaults to the per-user location shared with the desktop app (`%LOCALAPPDATA%\NomiFun\Nomi`, `~/Library/Application Support/NomiFun/Nomi`, `$XDG_DATA_HOME/NomiFun/Nomi`). **Still set an explicit absolute path in production.** |
| `--dist` | `NOMIFUN_WEB_DIST` | `../../ui/dist` | Directory containing the built SPA. **Set this explicitly when deploying.** |
| `--admin-user` | `NOMIFUN_ADMIN_USERNAME` | `admin` | Username used when pre-seeding the first admin. Ignored once an admin exists. |
| `--admin-password` | `NOMIFUN_ADMIN_PASSWORD` | — | Pre-seed the first admin password at boot, skipping interactive setup. Ignored once an admin exists. |
| `--insecure-no-auth` | `NOMIFUN_WEB_INSECURE_NO_AUTH` | `false` | **DANGER.** Disables authentication entirely (desktop-style local mode). Only use on loopback or a fully trusted private network. |
| — | `NOMIFUN_HTTPS` | `false` | When `true`, session and CSRF cookies are flagged `Secure`. Set this whenever the app is reached over HTTPS (e.g. behind a TLS reverse proxy). |
| — | `SHELL` | platform default | Shell used by the agent engine when spawning processes. Set to `/bin/bash` on Linux servers if `$SHELL` is unset. |
Boolean envs accept `1`, `true`, `yes`, `on` (case-insensitive).
A bad `--host` (anything that does not parse as an IP) fails fast at startup with a clear error rather than a cryptic socket error.
At startup the backend takes an OS-level exclusive lock on `{data_dir}/server.lock`**one backend instance per data dir**. A second process pointed at the same directory fails fast with an error naming the current holder (pid + exe); to deploy multiple instances, give each its own `NOMIFUN_DATA_DIR` / `--data-dir`. The OS releases the lock on exit or crash, so a leftover `server.lock` file is harmless.
### Password and username rules
When the admin account is created (interactively or via pre-seed), values are validated server-side:
- **Username**: 332 chars, `[a-zA-Z0-9_-]`, must not start or end with `-` / `_`.
- **Password**: 8128 chars, rejected if it appears in a small common-passwords list (`password`, `12345678`, `qwertyui`, …).
A weak `NOMIFUN_ADMIN_PASSWORD` will refuse to boot. A weak interactively-typed password will return `400` with the validation message.
## First-run admin provisioning
There are two supported paths.
### Interactive (default)
Leave `NOMIFUN_ADMIN_PASSWORD` unset. On a fresh data dir the install is "uninitialised": `GET /api/auth/status` reports `needs_setup: true`, the SPA shows the first-run form, and the **first browser visitor's chosen username + password become the admin** via an atomic `POST /api/auth/setup`. The write is a conditional UPDATE — even two concurrent first-run requests cannot both win; the loser receives `409 Conflict`.
> **Security note — the first-run window.** Between the moment the server is reachable and the moment you complete setup, anyone who can reach the port can claim the admin account. On a non-loopback bind the server logs a loud warning. Mitigate by completing setup over a trusted tunnel/VPN first, or pre-seed (next section) so the install is initialised before it goes live.
### Pre-seeded (recommended for automation)
Provide `NOMIFUN_ADMIN_PASSWORD` (and optionally `NOMIFUN_ADMIN_USERNAME`, default `admin`) before first boot. The bootstrap routine hashes and stores the credentials atomically, the first-run setup endpoint returns `409` from the very first start, and there is no window for someone else to claim the account.
```bash
NOMIFUN_ADMIN_USERNAME=alice \
NOMIFUN_ADMIN_PASSWORD='change-me-to-something-strong' \
nomifun-web --host 0.0.0.0 --port 8787 \
--data-dir /var/lib/nomifun --dist /opt/nomifun/web
```
The pre-seed is **idempotent** — once an admin exists, the env vars are ignored on subsequent boots. To rotate credentials, use the in-app change-password / change-username flow rather than the env vars.
## Docker
The repo ships a multi-stage `Dockerfile` and a `docker-compose.yml`. The image:
1. Builds the SPA with Bun.
2. Compiles `nomifun-web` from the workspace.
3. Assembles a slim `debian:bookworm-slim` runtime that includes `bun`, `git`, and `ripgrep`.
It exposes port `8787` and uses `/data` as the data volume.
### Compose
```bash
docker compose up -d --build
# then open http://<server-ip>:8787 and create the first admin
```
`restart: unless-stopped` makes the service start on host boot — installing it *is* enabling it. The default ports block publishes `8787:8787` directly; pre-seed the admin or complete setup on a trusted network before exposing it broadly. Add TLS (next section) before exposing to the internet.
Verify readiness:
```bash
docker compose logs -f nomifun
# look for: "nomifun-web: embedded backend + SPA on one port"
```
The compose file mounts a named volume `nomifun-data:/data` which holds the SQLite DB, logs, the Bun runtime cache, and per-agent state. Back this up with the same care as any other database.
### Pre-seeding the admin in Compose
Uncomment the `environment:` block:
```yaml
environment:
NOMIFUN_ADMIN_USERNAME: admin
NOMIFUN_ADMIN_PASSWORD: "change-me-to-something-strong"
NOMIFUN_HTTPS: "true" # when fronted by Caddy / nginx with TLS
```
### Building behind a slow registry
The Rust stage accepts a `CARGO_REGISTRY_MIRROR` build arg for cargo registry mirroring (e.g. on a network where crates.io is slow):
```bash
docker build --build-arg CARGO_REGISTRY_MIRROR=https://rsproxy.cn/index/ -t nomifun-web:local .
```
```text
$ docker compose up -d
[+] Running 2/2
✔ Network nomifun_default Created
✔ Container nomifun-web Started
$ docker compose logs -f web
nomifun-web | listening on 0.0.0.0:8787 (auth: enabled)
```
## TLS via Caddy reverse proxy
A `Caddyfile` is included for Caddy 2. Caddy auto-provisions HTTPS certificates (Let's Encrypt or ZeroSSL by default) and proxies to the app. The WebSocket upgrade at `/ws` passes through automatically, no extra config required.
```caddy
your.domain.com {
encode zstd gzip
reverse_proxy nomifun:8787
}
```
To enable the Caddy service in `docker-compose.yml`:
1. Edit `Caddyfile` and replace `your.domain.com` with your real domain.
2. Set `NOMIFUN_HTTPS=true` in the `nomifun` service env (so cookies get the `Secure` flag).
3. Replace `ports: ["8787:8787"]` with `expose: ["8787"]` so only Caddy is published.
4. Uncomment the `caddy:` service and the `caddy-data` / `caddy-config` volumes.
5. `docker compose up -d`.
The app already provides its own login screen, so **do not configure HTTP basic auth in Caddy** — Caddy's job is purely TLS termination and proxying.
For a LAN-only host without a public domain you can use an internal name with `tls internal`, or just publish port `8787` directly without Caddy (the in-app login still protects it).
## systemd (Linux server, no Docker)
The repo includes `packaging/linux/nomifun-web.service` and a long-form Linux deployment guide at `packaging/linux/README.md`.
### Build artifacts
You need a Linux build host (cross-compiling the C dependencies from Windows is painful — the easiest workaround is to extract the binary from the Docker image with `docker cp`). On Linux:
```bash
bun install
bun run build:ui # → ui/dist (~21MB)
cargo build --release -p nomifun-web # → target/release/nomifun-web
```
### Layout
```
/opt/nomifun/nomifun-web # the binary
/opt/nomifun/web/ # contents of ui/dist
/var/lib/nomifun/ # data dir (created by systemd's StateDirectory)
```
```bash
sudo useradd --system --home /var/lib/nomifun --shell /usr/sbin/nologin nomifun
sudo mkdir -p /opt/nomifun/web
sudo cp target/release/nomifun-web /opt/nomifun/
sudo cp -r ui/dist/. /opt/nomifun/web/
```
### Bun must be on the system `PATH`
The agent engine requires **`bun ≥ 1.3.13`** as a runtime dependency. Because the service runs under a `nologin` system account, an install in someone's `~/.bun/bin/` is invisible to it. Pick one:
- **System install**: `curl -fsSL https://bun.sh/install | bash`, then `sudo install ~/.bun/bin/bun /usr/local/bin/bun`.
- **Embed in the binary**: build with `NOMIFUN_EMBED_BUN=1 cargo build --release -p nomifun-web`. Bun is bundled into the binary and self-extracts into the data dir on first run.
Verify: `sudo -u nomifun -s -- which bun` must return a path. Otherwise the first agent spawn will fail with an opaque error.
### Install the unit
```bash
sudo cp packaging/linux/nomifun-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nomifun-web
sudo systemctl status nomifun-web
```
The shipped unit:
- Binds `127.0.0.1:8787` by default. Change `NOMIFUN_WEB_HOST` to
`0.0.0.0` only after first-run setup is complete or
`NOMIFUN_ADMIN_PASSWORD` is configured.
- Sets `NOMIFUN_DATA_DIR=/var/lib/nomifun` to match the systemd-managed `StateDirectory=nomifun`. **Keep these two in sync** — if you drop the env line, the data dir silently falls back to the service user's per-user directory (`$XDG_DATA_HOME/NomiFun/Nomi`, typically `~nomifun/.local/share/NomiFun/Nomi`), decoupled from systemd state.
- Runs as a dedicated `nomifun` user (`User=nomifun`, `Group=nomifun`).
- Restarts on failure with a 3 s backoff.
- Applies moderate hardening (`NoNewPrivileges=yes`, `PrivateTmp=yes`). **Do not add** `ProtectHome=yes` or strict `ProtectSystem` — the agent engine reads/writes operator-directed files and over-sandboxing breaks core features.
To enable HTTPS cookies behind a TLS proxy, uncomment:
```ini
Environment=NOMIFUN_HTTPS=true
```
To pre-seed the admin instead of interactive setup:
```ini
Environment=NOMIFUN_ADMIN_USERNAME=admin
Environment=NOMIFUN_ADMIN_PASSWORD=change-me-to-something-strong
```
```text
$ sudo systemctl status nomifun-web
● nomifun-web.service - NomiFun web host
Loaded: loaded (/etc/systemd/system/nomifun-web.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-06-25 09:12:03 UTC
Main PID: 12345 (nomifun-web)
CGroup: /system.slice/nomifun-web.service
└─12345 /usr/local/bin/nomifun-web --host 127.0.0.1 --port 8787 …
nomifun-web[12345]: listening on 127.0.0.1:8787 (auth: enabled)
```
## Linux runtime dependencies
| Dependency | Required? | Notes |
|---|---|---|
| `glibc` + `ca-certificates` | Yes | sqlite is statically linked, TLS uses rustls — **no openssl, no libsqlite needed**. |
| `bun` ≥ 1.3.13 | **Yes** | Agent execution runtime. 1.1.38 has an stdin bug; do not use. Already inside the Docker image. |
| `node` / `npm` / `npx` | Recommended | Many user-configured MCP stdio servers launch via `npx -y …`. |
| `git` | Recommended | Skill discovery and a few built-in tools. |
| `ripgrep` (`rg`) | Recommended | Code-search backend. Falls back to `grep` if missing. |
| `DISPLAY` / X11 / WebView | **No** | `nomifun-web` is fully headless. |
## Security checklist
- **Use TLS for any public deployment.** Cookies and login credentials over plain HTTP can be sniffed. Behind a TLS proxy, set `NOMIFUN_HTTPS=true` so the session cookie is flagged `Secure`.
- **Strong admin password.** The validator rejects passwords below 8 chars and a few obvious dictionary entries, but it does not enforce a strength score — pick something long and random. Change it from the in-app flow whenever you suspect compromise; the change-password endpoint rotates the JWT secret, invalidating every existing session.
- **Close the first-run window** with `NOMIFUN_ADMIN_PASSWORD` for any host that becomes reachable before you are ready to interactively complete setup. Alternatively keep the service on `127.0.0.1` until setup is finished, then intentionally bind `0.0.0.0`.
- **`--insecure-no-auth` is hostile by default.** It disables authentication completely; *anyone* who can reach the port becomes a privileged user with shell, file, and agent access. Only use on a loopback bind or a fully trusted private network. The server logs a warning when it is enabled on a non-loopback address.
- The backend has terminal, filesystem, and agent execution capabilities — running it remotely is, by design, equivalent to giving yourself remote code execution on the host. Auth + TLS are the floor, not the ceiling. Treat the data dir and the admin password the same way you would treat root credentials.
## Troubleshooting
**`invalid --host '<value>'`.** Pass an IP literal (`127.0.0.1`, `0.0.0.0`, an explicit interface IP). Hostnames are not parsed.
**Cookies don't stick over HTTPS.** Set `NOMIFUN_HTTPS=true` so the `Secure` flag is added. Without it, browsers reject the cookie on HTTPS responses.
**Agent commands fail with `bun: command not found` under systemd.** Install bun system-wide (see the bun-on-PATH section above) or rebuild with `NOMIFUN_EMBED_BUN=1`.
**Healthcheck.** Use `GET /health` for process liveness. Use
`GET /api/auth/status` only when the caller also needs setup/auth state.
## See also
- [Running NomiFun as a Desktop App](./desktop-app.md)
- [WebUI Remote Access](./webui-remote-access.md) — turn an existing desktop install into a remotely-accessible server (without provisioning a separate machine).
- `packaging/linux/README.md` — deeper Linux notes (mostly Chinese; this guide subsumes the English content).
- `apps/web/src/main.rs` — the source of truth for flags, env vars, and bootstrapping order.
@@ -0,0 +1,294 @@
# Web 服务器部署
`nomifun-web` 是 NomiFun 的**无头、自托管**运行方式。它与 [桌面应用](./desktop-app.zh.md)嵌入的后端是同一个 Rust 后端,但被构建为一个独立二进制,并且会在同一个端口上同时提供 SPA (`ui/dist`)。它没有 GUI,没有 WebView,也不需要 `DISPLAY` —— 任何能运行静态二进制的 Linux/macOS/Windows 服务器上都能跑。
与桌面外壳不同,**`nomifun-web` 默认要求认证**。第一个浏览器访问者要么以交互方式创建管理员账户 (首次运行设置),要么你通过 `NOMIFUN_ADMIN_PASSWORD` 预置凭据。
> 如果你想暴露一个*已有的*桌面安装以便远程访问,而不需要搭建服务器,请参阅 [WebUI 远程访问](./webui-remote-access.zh.md)。那是一个按实例启用的功能;本指南面向的是专用服务器。
```text
浏览器 / 手机 / 局域网 nomifun-web(单进程、单端口)
┌──────────────────┐ ┌───────────────────────────────────────┐
│ SPA + 登录 │ HTTP / WS │ axum router │
│ (ui/dist) │ ────────────► │ ├─ / → SPA (ui/dist) │
└──────────────────┘ │ ├─ /api/* → REST handlers │
│ ├─ /ws → WebSocket 事件 │
│ └─ /login … → 鉴权(默认开启) │
│ │
│ 进程内后端 (nomifun-app) │
│ └─ SQLite · agents · cron · channels │
└───────────────────────────────────────┘
```
## 快速开始
### 直接运行二进制
```bash
cargo build --release -p nomifun-web
./target/release/nomifun-web --host 127.0.0.1 --port 8787 \
--data-dir ./data --dist ./ui/dist
```
然后打开 `http://127.0.0.1:8787`,首次访问时让你创建管理员账户。之后,setup 端点会返回 `409 Conflict`,唯一的进入方式就是通过登录表单 (或 `NOMIFUN_ADMIN_PASSWORD`)。
![首次运行管理员设置界面](../images/webserver-02-first-run-setup.png)
### 或者从仓库通过 Cargo 运行
```bash
bun install
bun run build:ui # 产出 ui/dist
cargo run -p nomifun-web # 会自动使用默认 --dist=../../ui/dist
```
## CLI 参数和环境变量
下方所有参数由 `apps/web/src/main.rs` 读取。每个都有对应的环境变量,方便用于 systemd / Docker / 编排器。
| 参数 | 环境变量 | 默认值 | 用途 |
|---|---|---|---|
| `--host` | `NOMIFUN_WEB_HOST` | `127.0.0.1` | 绑定的 IP。`0.0.0.0` 会接收 LAN/VPN/公网流量;大范围暴露前请先预置管理员或完成首次设置。 |
| `--port` | `NOMIFUN_WEB_PORT` | `8787` | TCP 端口。提供 API、`/ws` 处的 WebSocket,以及 SPA。 |
| `--data-dir` | `NOMIFUN_DATA_DIR` | 按用户目录 | 后端数据目录 (SQLite 数据库、agent 状态、日志、Bun 缓存)。默认是与桌面应用共享的按用户位置 (`%LOCALAPPDATA%\NomiFun\Nomi``~/Library/Application Support/NomiFun/Nomi``$XDG_DATA_HOME/NomiFun/Nomi`)。**生产环境请仍显式指定绝对路径。** |
| `--dist` | `NOMIFUN_WEB_DIST` | `../../ui/dist` | 构建好的 SPA 所在目录。**部署时请显式设置。** |
| `--admin-user` | `NOMIFUN_ADMIN_USERNAME` | `admin` | 预置首个管理员时使用的用户名。一旦管理员存在则被忽略。 |
| `--admin-password` | `NOMIFUN_ADMIN_PASSWORD` | — | 在启动时预置首个管理员密码,跳过交互式设置。一旦管理员存在则被忽略。 |
| `--insecure-no-auth` | `NOMIFUN_WEB_INSECURE_NO_AUTH` | `false` | **危险。** 完全禁用认证 (类似桌面的本地模式)。仅在 loopback 或完全可信的私有网络上使用。 |
| — | `NOMIFUN_HTTPS` | `false` | 当为 `true` 时,session 和 CSRF cookie 会带上 `Secure` 标记。每当应用通过 HTTPS 访问 (例如位于 TLS 反向代理之后) 时都应设置。 |
| — | `SHELL` | 平台默认 | Agent 引擎派生进程时使用的 shell。在 Linux 服务器上若 `$SHELL` 未设置,请设为 `/bin/bash`。 |
布尔类环境变量接受 `1``true``yes``on` (大小写不敏感)。
错误的 `--host` (任何无法解析为 IP 的内容) 会在启动时快速失败并给出清晰错误,而不是抛出晦涩的 socket 错误。
后端启动时会对 `{data_dir}/server.lock` 取 OS 级排他锁 —— **同一数据目录只允许一个后端实例**。第二个指向同一目录的进程会快速失败,错误信息会指出当前持有者 (pid + exe);要部署多个实例,请为每个实例指定各自独立的 `NOMIFUN_DATA_DIR` / `--data-dir`。锁在进程退出或崩溃时由 OS 自动释放,残留的 `server.lock` 文件是无害的。
### 密码与用户名规则
当管理员账户被创建时 (无论是交互式还是预置),值都会在服务端校验:
- **用户名**332 字符,`[a-zA-Z0-9_-]`,不能以 `-` / `_` 开头或结尾。
- **密码**:8–128 字符,若出现在一个小型常见密码列表中 (`password``12345678``qwertyui` …) 则被拒绝。
弱的 `NOMIFUN_ADMIN_PASSWORD` 会拒绝启动。交互式输入的弱密码会返回 `400` 并附带校验信息。
## 首次运行管理员配置
支持两种路径。
### 交互式 (默认)
不设置 `NOMIFUN_ADMIN_PASSWORD`。在新的数据目录上,安装处于 "未初始化" 状态:`GET /api/auth/status` 会报告 `needs_setup: true`,SPA 显示首次运行表单,**第一个浏览器访问者所选的用户名 + 密码会通过原子化的 `POST /api/auth/setup` 成为管理员**。该写入是一个条件性 UPDATE —— 即便两个并发的首次运行请求也无法同时获胜;输者会收到 `409 Conflict`
> **安全提示 —— 首次运行窗口期。** 在服务器可达的那一刻起,到你完成设置之间,任何能到达该端口的人都可以认领管理员账户。在非 loopback 绑定上,服务器会记录一条醒目的警告。可通过先在受信任的隧道/VPN 上完成设置来缓解,或预置 (见下一节) 让安装在上线前就已初始化。
### 预置 (推荐用于自动化)
在首次启动前提供 `NOMIFUN_ADMIN_PASSWORD` (以及可选的 `NOMIFUN_ADMIN_USERNAME`,默认 `admin`)。引导例程会原子地哈希并存储凭据,从首次启动开始首次运行 setup 端点就会返回 `409`,没有任何窗口让别人来认领账户。
```bash
NOMIFUN_ADMIN_USERNAME=alice \
NOMIFUN_ADMIN_PASSWORD='change-me-to-something-strong' \
nomifun-web --host 0.0.0.0 --port 8787 \
--data-dir /var/lib/nomifun --dist /opt/nomifun/web
```
预置是**幂等的** —— 一旦管理员存在,后续启动时这些环境变量会被忽略。要轮换凭据,请使用应用内的修改密码 / 修改用户名流程,而不是环境变量。
## Docker
仓库附带一个多阶段 `Dockerfile` 和一个 `docker-compose.yml`。镜像会:
1. 用 Bun 构建 SPA。
2. 从 workspace 编译 `nomifun-web`
3. 组装一个精简的 `debian:bookworm-slim` 运行时,包含 `bun``git``ripgrep`
它暴露端口 `8787`,并使用 `/data` 作为数据卷。
### Compose
```bash
docker compose up -d --build
# 然后打开 http://<server-ip>:8787 并创建首位管理员
```
`restart: unless-stopped` 让服务在主机启动时启动 —— 安装它*就是*启用它。默认的 ports 块直接发布 `8787:8787`;请先预置管理员或在受信网络完成首次设置,再大范围暴露。暴露到公网前请加上 TLS (下一节)。
验证就绪:
```bash
docker compose logs -f nomifun
# 查找:“nomifun-web: embedded backend + SPA on one port”
```
compose 文件挂载了一个名为 `nomifun-data:/data` 的具名卷,其中保存着 SQLite DB、日志、Bun 运行时缓存以及每个 agent 的状态。请像对待其他数据库一样仔细备份。
### 在 Compose 中预置管理员
取消 `environment:` 块的注释:
```yaml
environment:
NOMIFUN_ADMIN_USERNAME: admin
NOMIFUN_ADMIN_PASSWORD: "change-me-to-something-strong"
NOMIFUN_HTTPS: "true" # 在 Caddy / nginx 加 TLS 前置时启用
```
### 在缓慢的 registry 后构建
Rust 阶段接受一个 `CARGO_REGISTRY_MIRROR` 构建参数用于 cargo 注册表镜像 (例如在 crates.io 较慢的网络上)
```bash
docker build --build-arg CARGO_REGISTRY_MIRROR=https://rsproxy.cn/index/ -t nomifun-web:local .
```
```text
$ docker compose up -d
[+] Running 2/2
✔ Network nomifun_default Created
✔ Container nomifun-web Started
$ docker compose logs -f web
nomifun-web | listening on 0.0.0.0:8787 (auth: enabled)
```
## 通过 Caddy 反向代理实现 TLS
仓库附带一个用于 Caddy 2 的 `Caddyfile`。Caddy 会自动签发 HTTPS 证书 (默认 Let's Encrypt 或 ZeroSSL) 并代理到应用。`/ws` 处的 WebSocket 升级会自动透传,无需额外配置。
```caddy
your.domain.com {
encode zstd gzip
reverse_proxy nomifun:8787
}
```
要在 `docker-compose.yml` 中启用 Caddy 服务:
1. 编辑 `Caddyfile` 并把 `your.domain.com` 替换为你的真实域名。
2.`nomifun` 服务的环境变量中设置 `NOMIFUN_HTTPS=true` (这样 cookie 会带上 `Secure` 标记)。
3.`ports: ["8787:8787"]` 替换为 `expose: ["8787"]`,让只有 Caddy 对外发布。
4. 取消 `caddy:` 服务以及 `caddy-data` / `caddy-config` 卷的注释。
5. `docker compose up -d`
应用本身已经提供了登录界面,所以**不要在 Caddy 里配置 HTTP basic auth** —— Caddy 的职责只是 TLS 终结和代理。
对于没有公网域名的仅局域网主机,可以使用一个内部名加上 `tls internal`,或者干脆不加 Caddy 直接发布端口 `8787` (应用内登录依然提供保护)。
## systemd (Linux 服务器,无 Docker)
仓库包含 `packaging/linux/nomifun-web.service` 以及一份长篇 Linux 部署指南 `packaging/linux/README.md`
### 构建产物
你需要一台 Linux 构建主机 (从 Windows 交叉编译 C 依赖很痛苦 —— 最简单的变通是用 `docker cp` 从 Docker 镜像中提取二进制)。在 Linux 上:
```bash
bun install
bun run build:ui # → ui/dist (~21MB)
cargo build --release -p nomifun-web # → target/release/nomifun-web
```
### 布局
```
/opt/nomifun/nomifun-web # 二进制
/opt/nomifun/web/ # ui/dist 的内容
/var/lib/nomifun/ # 数据目录 (由 systemd 的 StateDirectory 创建)
```
```bash
sudo useradd --system --home /var/lib/nomifun --shell /usr/sbin/nologin nomifun
sudo mkdir -p /opt/nomifun/web
sudo cp target/release/nomifun-web /opt/nomifun/
sudo cp -r ui/dist/. /opt/nomifun/web/
```
### Bun 必须在系统 `PATH` 上
Agent 引擎需要 **`bun ≥ 1.3.13`** 作为运行时依赖。由于服务以一个 `nologin` 系统账户运行,安装在某个用户 `~/.bun/bin/` 下对它来说是不可见的。请二选一:
- **系统级安装**`curl -fsSL https://bun.sh/install | bash`,然后 `sudo install ~/.bun/bin/bun /usr/local/bin/bun`
- **嵌入二进制**:使用 `NOMIFUN_EMBED_BUN=1 cargo build --release -p nomifun-web` 构建。Bun 会被打包进二进制中,并在首次运行时自解压到数据目录。
验证:`sudo -u nomifun -s -- which bun` 必须返回一个路径。否则首次 agent 派生会以一个晦涩的错误失败。
### 安装 unit
```bash
sudo cp packaging/linux/nomifun-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nomifun-web
sudo systemctl status nomifun-web
```
附带的 unit
- 默认绑定 `127.0.0.1:8787`。只有在首次设置完成或已配置
`NOMIFUN_ADMIN_PASSWORD` 后,才应把 `NOMIFUN_WEB_HOST` 改成
`0.0.0.0`
- 设置 `NOMIFUN_DATA_DIR=/var/lib/nomifun` 以匹配 systemd 管理的 `StateDirectory=nomifun`。**保持这两者同步** —— 如果你删除该环境变量行,数据目录会静默回退到服务用户的按用户目录 (`$XDG_DATA_HOME/NomiFun/Nomi`,通常是 `~nomifun/.local/share/NomiFun/Nomi`),与 systemd state 解耦。
- 以专用的 `nomifun` 用户运行 (`User=nomifun``Group=nomifun`)。
- 失败时以 3 秒退避重启。
- 应用适度的硬化 (`NoNewPrivileges=yes``PrivateTmp=yes`)。**不要添加** `ProtectHome=yes` 或严格的 `ProtectSystem` —— agent 引擎需要读写操作员指定的文件,过度沙箱化会破坏核心功能。
要在 TLS 代理后启用 HTTPS cookie,请取消注释:
```ini
Environment=NOMIFUN_HTTPS=true
```
要预置管理员而不是交互式设置:
```ini
Environment=NOMIFUN_ADMIN_USERNAME=admin
Environment=NOMIFUN_ADMIN_PASSWORD=change-me-to-something-strong
```
```text
$ sudo systemctl status nomifun-web
● nomifun-web.service - NomiFun web host
Loaded: loaded (/etc/systemd/system/nomifun-web.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-06-25 09:12:03 UTC
Main PID: 12345 (nomifun-web)
CGroup: /system.slice/nomifun-web.service
└─12345 /usr/local/bin/nomifun-web --host 127.0.0.1 --port 8787 …
nomifun-web[12345]: listening on 127.0.0.1:8787 (auth: enabled)
```
## Linux 运行时依赖
| 依赖 | 是否必需 | 说明 |
|---|---|---|
| `glibc` + `ca-certificates` | 是 | sqlite 是静态链接的,TLS 使用 rustls —— **不需要 openssl,不需要 libsqlite**。 |
| `bun` ≥ 1.3.13 | **是** | Agent 执行运行时。1.1.38 有 stdin bug;不要使用。Docker 镜像里已包含。 |
| `node` / `npm` / `npx` | 推荐 | 许多用户配置的 MCP stdio 服务器通过 `npx -y …` 启动。 |
| `git` | 推荐 | 技能发现和一些内置工具会使用。 |
| `ripgrep` (`rg`) | 推荐 | 代码搜索后端。缺失时回退到 `grep`。 |
| `DISPLAY` / X11 / WebView | **否** | `nomifun-web` 是完全无头的。 |
## 安全检查清单
- **任何公网部署都要使用 TLS。** 通过明文 HTTP 传输的 cookie 和登录凭据可能被嗅探。在 TLS 代理后请设置 `NOMIFUN_HTTPS=true`,让 session cookie 带上 `Secure` 标记。
- **强管理员密码。** 校验器会拒绝长度低于 8 字符的密码以及一些显而易见的字典条目,但它并不强制执行强度评分 —— 请选择长且随机的内容。怀疑被泄露时,请通过应用内流程修改它;修改密码端点会轮换 JWT 密钥,使所有现有会话失效。
- 对于任何在你能进行交互式设置之前就已可达的主机,请用 `NOMIFUN_ADMIN_PASSWORD` **关闭首次运行窗口期**。另一种做法是先保持 `127.0.0.1`,完成设置后再显式绑定 `0.0.0.0`
- **`--insecure-no-auth` 默认是敌对的。** 它完全禁用认证;*任何*能到达该端口的人都会成为拥有 shell、文件和 agent 访问权限的特权用户。仅在 loopback 绑定或完全可信的私有网络上使用。当它在非 loopback 地址上启用时,服务器会记录警告。
- 后端拥有终端、文件系统和 agent 执行能力 —— 远程运行它,本设计上等同于给自己开通了对该主机的远程代码执行。Auth + TLS 是底线,不是上限。请像对待 root 凭据一样对待数据目录和管理员密码。
## 故障排查
**`invalid --host '<value>'`。** 请传入一个 IP 字面量 (`127.0.0.1``0.0.0.0`、显式接口 IP)。不解析主机名。
**HTTPS 下 cookie 无法保留。** 设置 `NOMIFUN_HTTPS=true` 以加上 `Secure` 标记。否则浏览器会在 HTTPS 响应中拒绝该 cookie。
**在 systemd 下 agent 命令失败并报 `bun: command not found`。** 请系统级安装 bun (参见上面的 bun-on-PATH 一节) 或使用 `NOMIFUN_EMBED_BUN=1` 重新构建。
**健康检查。** 使用 `GET /health` 作为进程存活探针;只有在调用方还需要设置 / 认证状态时,才使用 `GET /api/auth/status`
## 另请参阅
- [以桌面应用方式运行 NomiFun](./desktop-app.zh.md)
- [WebUI 远程访问](./webui-remote-access.zh.md) —— 把现有桌面安装变成一个可远程访问的服务器 (无需另置一台机器)。
- `packaging/linux/README.md` —— 更深入的 Linux 笔记 (主要是中文;本指南涵盖了其英文部分)。
- `apps/web/src/main.rs` —— 参数、环境变量和引导顺序的真相之源。
@@ -0,0 +1,111 @@
# WebUI Remote Access
The desktop app already runs a backend on a localhost port for its own webview — why not just expose it? Because exposing an unauthenticated backend on a LAN would hand every device on that network full shell, file, and agent access.
**WebUI remote access** solves that. The desktop backend runs under a *trust-local-token* policy: the desktop's own webview is trusted via a per-boot secret it presents on every request (so you never log in locally), while any other client must authenticate. With one switch, an additional listener is bound on a stable LAN port that serves the app to remote browsers behind a login (password + QR), so you can use Nomi from your phone or another browser without giving up local-mode convenience.
This is **per-instance** — it lives inside your already-running desktop app — and is distinct from the dedicated [Web Server Deployment](./web-server-deployment.md). Use this when you have an existing desktop install and just want to reach it from another device on the same network. Use the dedicated server when you want a long-lived headless deployment.
![Open Capabilities WebUI panel](../images/webui-01-settings-overview.png)
## Where to find it
Open **Open Capabilities** (route `/open-capabilities`) and use the WebUI
remote-access panel. The legacy `/settings/webui` route redirects there.
- **WebUI remote access** controls the desktop LAN listener described in this guide.
- Other cards on the page manage public/remote capability exposure and should be
reviewed separately before enabling them.
> The WebUI remote-access controls are meaningful inside the desktop shell. In a
> browser tab against `nomifun-web`, you are already using the dedicated Web host;
> use [Web Server Deployment](./web-server-deployment.md) settings instead.
## What enabling it does
Toggling **Enable WebUI** on starts an additional authenticated server inside the desktop process:
- **Default port `25808`** (`25809` in dev mode, `25810` when `NOMIFUN_MULTI_INSTANCE=1`).
- An admin user (default name `admin`) is provisioned with a freshly generated random password — shown in plaintext **once**, on this first start, so you can copy it.
- The server's lifetime is tracked by the desktop main process; the toggle reflects the *real* server state, not a remembered preference, so a silent failure (port conflict, etc.) leaves the switch off rather than misleading you into thinking it is up.
## Architecture: two listeners, one backend
The desktop process serves its backend on **two** sockets that share one in-process router (built once):
- A **permanent loopback listener** on an ephemeral port — the desktop's own webview, trusted via the per-boot secret. Always up; never disturbed by toggling remote access.
- An **on-demand LAN listener** on `0.0.0.0:25808` — bound only when you enable remote access, torn down when you disable it. Remote browsers reach this one and must log in. Trust is the secret (which only the desktop webview holds), *not* "arrived on loopback", so other OS accounts on a shared workstation and same-host reverse proxies are **not** auto-trusted. The LAN listener additionally enforces a Host/Origin allow-list (IP/localhost only, blocking DNS-rebinding) and rate-limits by real peer address.
Because of the exclusive data-dir lock, the desktop process is the only backend on its data directory — so the LAN listener lives *inside* the desktop app, it is not a co-running `nomifun-web`.
## Binding and the access URL
Enabling remote access binds **`0.0.0.0:25808`** (`25809` in dev; falls back to an ephemeral port if `25808` is taken) so other devices on your network can reach it. The displayed URL adapts:
- **The desktop's own machine**: `http://localhost:<port>`.
- **Remote (LAN/VPN)**: `http://<your-LAN-IP>:<port>` (e.g. `http://192.168.1.42:25808`). The candidate interface addresses are detected from the host's network interfaces; on a VPN host with multiple adapters, confirm the advertised address is the one your phone can actually reach.
A copy button copies the URL; clicking it opens it in your default external browser. The QR-code login is shown while the LAN listener is running.
## Login: username and password
The credentials panel shows:
- **Username** — defaults to `admin`. Editable via the pencil icon (server-side validation: 332 chars, `[a-zA-Z0-9_-]`, must not start or end with `-` / `_`).
- **Initial password** — shown in plaintext on the *very first* start, masked as `******` after that. The plaintext can be copied while it is visible. Once you copy it (or the first session ends), it switches to masked permanently.
- The plaintext is only shown once because the backend stores a bcrypt hash, not the plaintext. After the first display, even the desktop UI cannot recover the original.
To change the password later, click the pencil icon next to the masked field. The form requires the new password and a confirmation; on success the new value is hashed and persisted, and the cached plaintext is cleared. The password validator rejects values shorter than 8 characters and a small list of common passwords (`password`, `12345678`, …).
The "reset password" path (when you forget it) generates a fresh 16-character random password server-side; a one-time displayed value, like the initial one.
![Login screen on the remote browser](../images/webui-03-login-screen.png)
## QR-code login
While WebUI is enabled (the LAN listener is running), a QR code appears in the credentials card.
- Scanning it from your phone opens `http://<host>:<port>/qr-login?token=<one-time>` in the phone's default browser.
- That URL hits a static page that calls `POST /api/auth/qr-login` with the token. The token is single-use and validated atomically; the server returns a session cookie + JWT and the page redirects to `/`.
- Tokens **expire after 5 minutes**; the UI auto-refreshes the QR every 4 minutes so a panel left open does not invalidate.
- A copy button next to the QR copies the full login URL (useful if your phone cannot scan), and a refresh button regenerates the token on demand.
QR login always logs you in as the configured WebUI admin (the primary admin user), regardless of how many users exist in the database — it is the per-instance "skip the password form" shortcut, not a multi-user feature.
![QR code login on phone](../images/webui-04-qr-login-phone.png)
## How this differs from `nomifun-web`
| | WebUI remote access | `nomifun-web` (Web Server Deployment) |
|---|---|---|
| Where it runs | Inside your already-running desktop app | A separate, headless binary |
| GUI required to start | Yes (the Settings toggle) | No |
| Admin provisioning | Auto-generated password on first enable | Interactive first-run setup, or `NOMIFUN_ADMIN_PASSWORD` |
| Default port | `25808` (prod), `25809` (dev) | `8787` |
| Survives reboot | Only if your desktop app is running | Yes, with systemd / Docker `restart: unless-stopped` |
| TLS | None built in (LAN-oriented) | Caddy / nginx in front; `NOMIFUN_HTTPS=true` |
| Use case | Quick remote access from a phone on the same network | A real always-on server |
If you find yourself leaving the desktop app running on a server-like box just so the WebUI server stays up, that is the cue to switch to a dedicated [Web Server Deployment](./web-server-deployment.md).
## Security notes
- The server listens on plain HTTP. Use it on a **trusted local network** (your home Wi-Fi, a VPN, Tailscale, etc.). For exposure beyond that, deploy `nomifun-web` behind a TLS reverse proxy instead.
- The admin user has the same capabilities as the local desktop user: shell access, file access, agent execution. Treat the admin password and QR tokens accordingly.
- Changing the password (in-app or via reset) invalidates all existing sessions because the JWT signing secret rotates atomically with the password update.
- The QR token is one-shot — once scanned and consumed it cannot be reused. A leaked token is therefore self-limiting, but a leaked URL **before** scanning still grants login. Don't post screenshots of the QR.
## Troubleshooting
**Toggle flips back to off immediately.** Another process is bound to the WebUI port. Pick a different port if you can configure it from the UI; otherwise stop whatever is holding `25808`.
**The QR code shows but my phone gets a connection error.** Check the LAN IP shown in the access URL — if your machine has multiple interfaces (Wi-Fi + Ethernet, VPN adapters), the auto-detected address may not be the one your phone can reach. Confirm your phone is on the same network/subnet, and that the firewall allowed Nomi when prompted.
**`./qr-login?token=…` says "Login failed: …".** The token expired (5-minute TTL) or has already been consumed. Click the refresh button next to the QR to mint a new one.
**I forgot the admin password.** Use the reset button (the pencil + reset icon next to the masked password), then sign in with the freshly generated value — it is shown once.
## See also
- [Running NomiFun as a Desktop App](./desktop-app.md)
- [Web Server Deployment](./web-server-deployment.md) — when you want a real always-on server, not a desktop side-channel.
@@ -0,0 +1,110 @@
# WebUI 远程访问
桌面应用本来就在一个 localhost 端口上为自己的 webview 运行了一个后端 —— 为什么不直接把它暴露出去?因为把一个无认证的后端放到 LAN 上,等于把对该网络上每台设备完全的 shell、文件和 agent 访问权拱手相让。
**WebUI 远程访问**正是为此而生。桌面后端运行在 *trust-local-token* 策略下:桌面自己的 webview 通过一个每次启动生成的密钥被信任(每个请求都携带它,所以本机从不需要登录),而任何其他客户端都必须认证。一键即可在一个稳定的 LAN 端口上额外绑定一个监听器,把应用经登录(密码 + 二维码)服务给远程浏览器,让你在不放弃本地模式便利性的前提下,从手机或另一个浏览器使用 Nomi。
它是**按实例启用**的 —— 它存在于你已经运行着的桌面应用内 —— 与专用的 [Web 服务器部署](./web-server-deployment.zh.md)是不同的。当你已经有一个桌面安装并且只想从同一网络上的另一台设备访问它时,请使用本功能。当你想要一个长期运行的无头部署时,请使用专用服务器。
![开放能力 WebUI 面板](../images/webui-01-settings-overview.png)
## 在哪里找到它
打开 **开放能力**(路由 `/open-capabilities`)中的 WebUI 远程访问面板。旧
`/settings/webui` 路由会重定向到这里。
- **WebUI 远程访问** 控制本指南描述的桌面 LAN listener。
- 页面上的其他卡片管理 public/remote capability 暴露,启用前应单独审查。
> WebUI 远程访问控制主要用于桌面壳。若你正在浏览器里访问 `nomifun-web`
> 说明已经在使用专用 Web host;请按 [Web 服务部署](./web-server-deployment.zh.md)
> 的方式配置。
## 启用它做了什么
打开 **Enable WebUI** 会在桌面进程内启动一个额外的认证服务器:
- **默认端口 `25808`** (开发模式下为 `25809`,当 `NOMIFUN_MULTI_INSTANCE=1` 时为 `25810`)。
- 一个管理员用户 (默认名 `admin`) 会被开通,并带有一个新生成的随机密码 —— 在首次启动时**仅以明文显示一次**,以便你复制。
- 服务器的生命周期由桌面主进程跟踪;该开关反映服务器的*真实*状态,而不是某个被记住的偏好,所以一次静默失败 (端口冲突等) 会让开关保持关闭,而不是误导你以为它已开启。
## 架构:双监听器,一个后端
桌面进程在**两个** socket 上服务后端,二者共享同一个(只构建一次的)路由:
- 一个 **永久 loopback 监听器**(随机端口)—— 桌面自己的 webview,通过每启动密钥被信任。始终在线;切换远程访问从不打断它。
- 一个 **按需 LAN 监听器**`0.0.0.0:25808`)—— 仅在你开启远程访问时绑定,关闭时拆除。远程浏览器连这个,且必须登录。信任的依据是**密钥**(只有桌面 webview 持有),而非"来自 loopback",因此共享工作站上的其他 OS 账户、以及同机反向代理都**不会**被自动信任。LAN 监听器还强制 Host/Origin 白名单(仅 IP/localhost,阻断 DNS-rebinding),并按真实对端地址限流。
由于数据目录独占锁,桌面进程是其数据目录上唯一的后端 —— 所以 LAN 监听器活在桌面应用*内部*,它不是另跑的 `nomifun-web`
## 绑定与访问 URL
开启远程访问会绑定 **`0.0.0.0:25808`**(开发模式 `25809`;若 `25808` 被占用则回退到一个随机端口),使你网络上的其他设备可以访问。显示的 URL 会自适应:
- **桌面本机**`http://localhost:<port>`
- **远程 (LAN/VPN)**`http://<your-LAN-IP>:<port>`(例如 `http://192.168.1.42:25808`)。候选网卡地址从主机网络接口探测;在带多个网卡的 VPN 主机上,请确认广播出的地址是手机真正可达的那个。
复制按钮会复制 URL;点击它会在默认外部浏览器中打开。LAN 监听器运行时会显示二维码登录。
## 登录:用户名和密码
凭据面板显示:
- **用户名** —— 默认 `admin`。可通过铅笔图标编辑 (服务端校验:3–32 字符,`[a-zA-Z0-9_-]`,不能以 `-` / `_` 开头或结尾)。
- **初始密码** —— 在*仅*第一次启动时以明文显示,之后被遮蔽为 `******`。在它可见时可以复制明文。一旦你复制了它 (或第一次会话结束),它就永久切换为遮蔽状态。
- 明文只显示一次,因为后端存储的是 bcrypt 哈希,而不是明文。在第一次显示之后,连桌面 UI 也无法恢复原始值。
要稍后修改密码,点击被遮蔽字段旁的铅笔图标。表单需要新密码和一次确认;成功后新值会被哈希并持久化,缓存的明文会被清除。密码校验器会拒绝长度低于 8 字符的值以及一小列常见密码 (`password``12345678` …)。
"重置密码" 路径 (当你忘记时) 会在服务端生成一个新的 16 字符随机密码;像初始密码一样是一次性显示的值。
![远程浏览器上的登录界面](../images/webui-03-login-screen.png)
## 二维码登录
WebUI 启用时(局域网监听器运行中),凭据卡中会出现一个二维码。
- 用手机扫描会在手机的默认浏览器中打开 `http://<host>:<port>/qr-login?token=<one-time>`
- 该 URL 命中一个静态页面,该页面调用 `POST /api/auth/qr-login` 并带上 token。token 是一次性使用的,并被原子性地校验;服务器返回一个 session cookie + JWT,页面跳转到 `/`
- Token **5 分钟后过期**;UI 每 4 分钟自动刷新一次二维码,避免一个一直开着的面板失效。
- 二维码旁的复制按钮会复制完整的登录 URL (在你手机无法扫描时有用),刷新按钮则可以按需重新生成 token。
无论数据库中存在多少用户,二维码登录始终把你登入为已配置的 WebUI 管理员 (主管理员) —— 它是按实例的"跳过密码表单"的捷径,而不是一个多用户功能。
![手机上的二维码登录](../images/webui-04-qr-login-phone.png)
## 与 `nomifun-web` 的区别
| | WebUI 远程访问 | `nomifun-web` (Web 服务器部署) |
|---|---|---|
| 运行位置 | 在你已经运行的桌面应用内 | 一个独立的、无头的二进制 |
| 启动是否需要 GUI | 是 (设置开关) | 否 |
| 管理员配置 | 首次启用时自动生成密码 | 交互式首次运行设置,或 `NOMIFUN_ADMIN_PASSWORD` |
| 默认端口 | `25808` (生产)`25809` (开发) | `8787` |
| 是否在重启后保留 | 仅当桌面应用在运行时 | 是,配合 systemd / Docker 的 `restart: unless-stopped` |
| TLS | 没有内建 (面向 LAN) | 前置 Caddy / nginx`NOMIFUN_HTTPS=true` |
| 适用场景 | 从同一网络上的手机快速远程访问 | 真正的常开服务器 |
如果你发现自己只是为了让 WebUI 服务器保持开启而把桌面应用一直跑在某台类服务器机器上,那就是切换到专用 [Web 服务器部署](./web-server-deployment.zh.md) 的信号。
## 安全说明
- 服务器监听明文 HTTP。请在**可信本地网络** (家里 Wi-Fi、VPN、Tailscale 等) 上使用。要超出这个范围暴露,请改为在 TLS 反向代理后部署 `nomifun-web`
- 管理员用户拥有与本地桌面用户相同的能力:shell 访问、文件访问、agent 执行。请相应对待管理员密码和 QR token。
- 修改密码 (在应用内或通过重置) 会使所有现有会话失效,因为 JWT 签名密钥会随密码更新一同原子轮换。
- QR token 是一次性的 —— 一旦扫描并被消费就无法重用。因此被泄露的 token 自我限制有限,但**扫描之前**被泄露的 URL 仍能授予登录权。不要发布二维码的截图。
## 故障排查
**开关立即弹回 off。** 另一个进程绑定了 WebUI 端口。如果可以从 UI 配置就换一个端口;否则停掉占用 `25808` 的程序。
**二维码显示了但手机连不上。** 检查访问 URL 中显示的 LAN IP —— 如果你的机器有多个接口 (Wi-Fi + 以太网、VPN 适配器),自动检测到的地址可能不是手机能到达的那个。确认手机和电脑在同一网络/子网,且首次绑定时已在防火墙提示中允许 Nomi。
**`./qr-login?token=…` 提示 "Login failed: …"。** Token 已过期 (5 分钟 TTL) 或已被消费过。点击二维码旁的刷新按钮铸造一个新的。
**我忘了管理员密码。** 使用重置按钮 (被遮蔽密码旁的铅笔 + 重置图标),然后用新生成的值登录 —— 它只显示一次。
## 另请参阅
- [以桌面应用方式运行 NomiFun](./desktop-app.zh.md)
- [Web 服务器部署](./web-server-deployment.zh.md) —— 当你想要一个真正的常开服务器,而不是桌面侧通道时。
+125
View File
@@ -0,0 +1,125 @@
# Screenshot Manifest
This file tracks every screenshot referenced by the documentation. All images
are captured from the **real running NomiFun app** (not mockups) and saved into
this `docs/images/` directory.
## Naming scheme
```
<module-prefix>-<NN>-<slug>.png
```
- `module-prefix` — namespace owned by one doc area (see table below).
- `NN` — two-digit sequence within that module (`01`, `02`, …).
- `slug` — short kebab-case description.
## How to capture (current process)
The desktop app and the `nomifun-web` host render the **same** production SPA
(`ui/dist`), so in-app screens are pixel-identical between them. Capture in-app
screens from the web host (scriptable, no native window needed); capture native
window chrome from the installed app.
1. **Build the SPA** (if changed): `bun run build:ui`.
2. **Run a throwaway, no-auth host on an isolated data dir** (so it never
touches your real data or collides with the desktop app's database):
```bash
target/debug/nomifun-web --insecure-no-auth --port 8799 \
--dist ui/dist --data-dir /tmp/nomifun-shots
```
3. **Seed synthetic demo data** through the local API (no real credentials),
e.g. companions, requirements, a knowledge base, a terminal session — see
`docs/images` git history for the exact `curl` payloads used.
4. **Drive a headless browser** (Python Playwright 1.51, already installed) at
dark theme, `1440×900`, `device_scale_factor=2`:
```python
ctx = browser.new_context(viewport={"width":1440,"height":900},
color_scheme="dark", device_scale_factor=2)
ctx.add_init_script("localStorage.setItem('__nomifun_theme','dark')")
page.goto("http://127.0.0.1:8799/#/<route>")
```
5. **Auth-mode screens** (login / first-run setup): run the host **without**
`--insecure-no-auth`. No admin yet → first-run setup; pass
`--admin-user/--admin-password` → login screen.
6. **Native window** (titlebar / tray): capture from the installed
`/Applications/NomiFun.app` with macOS window capture. Requires Screen
Recording permission for the capturing process.
## Module prefixes
| Prefix | Owner doc area |
| --- | --- |
| `gs-` | getting-started/ |
| `desktop-` | guides/desktop-app |
| `webserver-` | guides/web-server-deployment |
| `webui-` | guides/webui-remote-access |
| `terminal-` | guides/terminal |
| `autowork-` | guides/autowork-requirements |
| `cron-` | guides/scheduled-tasks |
| `channels-` | guides/channels |
| `mcp-` | guides/mcp-and-skills |
| `assistants-` | guides/assistants |
| `readme-` | root README showcase |
## Manifest
Routes use the `#/` hash-router scheme. **Status** is `live` (captured from the
current app) or `pending` (needs an environment this machine can't provide —
see notes).
| Id | Route / screen | Host | Caption | Used in | Status |
| --- | --- | --- | --- | --- | --- |
| `readme-01-workbench-overview.png` | desktop app / conversation workbench | desktop | Main desktop workbench with conversation, companion, and live metrics | README / README.zh-CN | live |
| `gs-01-introduction-hero.png` | `#/guid` | web | Home / new-session page | getting-started/introduction | live |
| `gs-02-desktop-dev.png` | `#/guid` | web (interim) | Desktop app home — app content; native window chrome pending | getting-started/introduction | live\* |
| `gs-03-web-first-run-setup.png` | `#/login` (no admin) | web (auth) | First-run admin setup | getting-started/installation | live |
| `gs-04-quickstart-login.png` | `#/login` (admin exists) | web (auth) | Login screen | getting-started/quick-start | live |
| `gs-05-quickstart-guid.png` | `#/guid` | web | Home page (agent bar + input) | getting-started/quick-start | live |
| `gs-06-quickstart-model-settings.png` | `#/models` | web | Model & Agent settings | getting-started/quick-start | live || `desktop-01-main-window.png` | `#/guid` | web (interim) | Desktop main window content; native chrome pending | guides/desktop-app | live\* |
| `webui-01-settings-overview.png` | `#/open-capabilities` | web | Open Capabilities panel | guides/webui-remote-access | live || `webui-03-login-screen.png` | `#/login` | web (auth) | Login screen on a remote browser | guides/webui-remote-access | live |
| `webui-04-qr-login-phone.png` | `#/login` @ 390px | web (phone) | Login on a phone-width viewport | guides/webui-remote-access | live |
| `terminal-01-session.png` | `#/terminal/:id` | web | In-app terminal session | guides/terminal | live |
| `terminal-02-create-page.png` | `#/terminal-new` | web | Terminal create page | guides/terminal | live |
| `terminal-03-driving-session.png` | `#/terminal/:id` | web | Driving a terminal (live output) | guides/terminal | live || `autowork-01-tag-sessions.png` | `#/requirements/extensions?tab=autowork` | web | AutoWork tag-sessions overview | guides/autowork-requirements | live |
| `autowork-02-list.png` | `#/requirements` | web | Requirements list | guides/autowork-requirements | live |
| `autowork-03-kanban.png` | `#/requirements?view=board` | web | Requirements board (pending/in-progress/done/…) | guides/autowork-requirements | live |
| `autowork-04-tag-sessions.png` | `#/requirements/extensions?tab=autowork` | web | Tag-sessions table | guides/autowork-requirements | live |
| `autowork-05-webhook-binding.png` | `#/requirements/extensions?tab=notify` | web | Notify / webhook tab | guides/autowork-requirements | live |
| `cron-01-list.png` | `#/scheduled` | web | Scheduled Tasks list + keep-awake banner | guides/scheduled-tasks | live |
| `cron-02-create-dialog.png` | `#/scheduled` (New task) | web | Create scheduled task dialog | guides/scheduled-tasks | live |
| `cron-03-detail.png` | `#/scheduled/:job_id` | web | Job detail: schedule, Run now, history | guides/scheduled-tasks | live || `channels-01-overview.png` | `#/nomi?tab=remote` | web | Companion Remote tab — channel overview | guides/channels | live |
| `channels-02-pairing.png` | `#/nomi?tab=remote` (connect) | web | Channel connect / settings dialog | guides/channels | live || `mcp-01-capabilities.png` | `#/mcp` | web | MCP page | guides/mcp-and-skills | live |
| `mcp-03-skills.png` | `#/assistants?tab=skills` | web | Skills tab | guides/mcp-and-skills | live |
| `assistants-01-list.png` | `#/assistants?tab=assistants` | web | Assistants list (builtin library) | guides/assistants | live |
| `assistants-02-editor.png` | `#/assistants?tab=assistants` (edit) | web | Assistant editor drawer | guides/assistants | live |
| `webserver-02-first-run-setup.png` | `#/login` (no admin) | web (auth) | First-run admin setup | guides/web-server-deployment | live |
## Not screenshots (rendered as commands / diagrams)
These were intentionally **not** captured as screenshots — they read better as
copy-pasteable commands or an ASCII diagram, and several are platform-specific
(Linux/Windows) and not reproducible on a macOS dev machine:
- `web-server-deployment`: high-level architecture (ASCII diagram), `docker
compose up` output, `systemctl status` output.
- `desktop-app`: `bun run build` output, the per-OS titlebar note, and the data
directory layout (shown as a directory tree).
## Pending screenshots
`gs-02` and `desktop-01` are marked `live\*`: they currently show the **real
current app content** captured from the web host, but **without the native
window chrome** (titlebar / traffic-lights / tray). Replace them with a true
native capture from the installed `/Applications/NomiFun.app` (needs Screen
Recording permission, or a manual `Cmd+Shift+4` window capture) when convenient.
A few data-heavy views are intentionally **not** screenshotted — a live
conversation reply, the per-job cron skill editor, the channel model selector,
an AutoWork-bound terminal, and the desktop-only WebUI-enabled state. They need
a configured LLM provider, a live IM bot, or the desktop LAN feature; the
surrounding guide prose covers them instead.
Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none" role="img" aria-label="NomiFun">
<defs>
<linearGradient id="logo-bg" x1="0" y1="0" x2="0" y2="80" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1B1822"/><stop offset="1" stop-color="#0B0A10"/></linearGradient>
<linearGradient id="logo-bowl" x1="15" y1="49" x2="65" y2="69" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#FF9FB4"/><stop offset="1" stop-color="#FF6F91"/></linearGradient>
</defs>
<rect width="80" height="80" rx="18" fill="url(#logo-bg)"/>
<path d="M33 17 q-4.5 -4 0 -8.5" stroke="#FF8FA8" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M40 15 q-4.5 -4 0 -8.5" stroke="#FFB3C4" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M47 17 q-4.5 -4 0 -8.5" stroke="#FF8FA8" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M22 46 Q22 27 40 27 Q58 27 58 46 Z" fill="#FFFFFF"/>
<path d="M14 49 H66 Q61.5 70 40 70 Q18.5 70 14 49 Z" fill="url(#logo-bowl)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630" fill="none" role="img" aria-label="NomiFun">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1200" y2="630" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#131019"/>
<stop offset="1" stop-color="#07060B"/>
</linearGradient>
<radialGradient id="glow" cx="0.28" cy="0.1" r="0.7">
<stop offset="0" stop-color="#FF6F91" stop-opacity="0.22"/>
<stop offset="1" stop-color="#FF6F91" stop-opacity="0"/>
</radialGradient>
<radialGradient id="glow2" cx="0.9" cy="0.95" r="0.6">
<stop offset="0" stop-color="#7583B2" stop-opacity="0.20"/>
<stop offset="1" stop-color="#7583B2" stop-opacity="0"/>
</radialGradient>
<linearGradient id="bowl" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#FF9FB4"/>
<stop offset="1" stop-color="#FF6F91"/>
</linearGradient>
<linearGradient id="pinktext" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#FFB3C4"/>
<stop offset="1" stop-color="#FF6F91"/>
</linearGradient>
</defs>
<rect width="1200" height="630" fill="url(#bg)"/>
<rect width="1200" height="630" fill="url(#glow)"/>
<rect width="1200" height="630" fill="url(#glow2)"/>
<!-- Logo mark, scaled -->
<g transform="translate(110,150) scale(3.2)">
<rect width="80" height="80" rx="18" fill="#1B1822"/>
<path d="M33 17 q-4.5 -4 0 -8.5" stroke="#FF8FA8" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M40 15 q-4.5 -4 0 -8.5" stroke="#FFB3C4" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M47 17 q-4.5 -4 0 -8.5" stroke="#FF8FA8" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M22 46 Q22 27 40 27 Q58 27 58 46 Z" fill="#FFFFFF"/>
<path d="M14 49 H66 Q61.5 70 40 70 Q18.5 70 14 49 Z" fill="url(#bowl)"/>
</g>
<text x="430" y="250" font-family="Inter, 'PingFang SC', sans-serif" font-size="86" font-weight="800" fill="#F5F3F7">NomiFun</text>
<text x="432" y="320" font-family="Inter, 'PingFang SC', sans-serif" font-size="34" font-weight="600" fill="url(#pinktext)">完全开源 · 本地优先的超级 AI 工作站</text>
<text x="432" y="378" font-family="Inter, 'PingFang SC', sans-serif" font-size="26" font-weight="400" fill="#B8B3C4">Fully open-source, local-first super AI workstation</text>
<g font-family="Inter, 'PingFang SC', sans-serif" font-size="22" font-weight="500" fill="#B8B3C4">
<rect x="432" y="430" width="190" height="46" rx="23" fill="#1B1822" stroke="#322E3C"/>
<text x="458" y="460">数据全在本地</text>
<rect x="640" y="430" width="160" height="46" rx="23" fill="#1B1822" stroke="#322E3C"/>
<text x="666" y="460">免费商用</text>
<rect x="818" y="430" width="190" height="46" rx="23" fill="#1B1822" stroke="#322E3C"/>
<text x="844" y="460">Apache-2.0</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

@@ -0,0 +1,147 @@
# API Overview
NomiFun's backend (`nomifun-app`, binary `nomicore`) exposes a single axum HTTP server. The SPA, the desktop shell, and any external integration all talk to it the same way: JSON over HTTP for command/query, WebSocket for streaming events.
This page is an **orientation**, not an exhaustive endpoint reference. The full surface lives in the route modules under `crates/backend/`; the source is the canonical reference. Group base paths and the routers that own each one are listed below — start there.
## Base URL
| Host | Default base URL | Notes |
|---|---|---|
| `nomifun-desktop` | `http://127.0.0.1:<picked-port>` | Picks a free localhost port at startup. The renderer learns the port over IPC and uses `/api` and `/ws` against it. |
| `nomifun-web` | `http://<host>:<port>` (`http://127.0.0.1:8787` by default) | Same backend, served alongside the SPA on one port. |
| `nomicore` standalone | `http://127.0.0.1:25808` | Backend run on its own — useful for debugging. |
The SPA uses **relative paths** (`/api/...`, `/ws`). There is no separate API server to point clients at — the SPA and the API are co-located.
## Authentication model
NomiFun runs under one of three auth policies, decided at startup:
### Authenticated mode (default for `nomifun-web`)
- Login via `POST /login` returns a session JWT in both a cookie (`nomifun-session`, `HttpOnly`) and the JSON body. Subsequent requests authenticate via the cookie or an `Authorization: Bearer …` header.
- State-changing requests must additionally include the CSRF header `x-csrf-token` matching the `nomifun-csrf-token` cookie (Double Submit Cookie pattern). Safe methods (`GET`, `HEAD`, `OPTIONS`) bypass CSRF; the login/setup/qr-login endpoints are exempt because they have no session yet.
- WebSocket upgrades carry the same JWT — typically via `Sec-WebSocket-Protocol`, fetched from `GET /api/ws-token`. The `/ws` route is exempt from CSRF (no cookie-based double-submit on a WebSocket upgrade) but is otherwise authenticated.
- Rate limiters apply per-client to login attempts, general API traffic, and authenticated state-changing actions.
### Desktop local-trust mode (`nomifun-desktop`)
- The embedded backend uses `AuthPolicy::TrustLocalToken`.
- The desktop WebView receives a per-boot secret (`window.__nomiLocalTrust`) and
presents it on HTTP/WebSocket requests.
- Other clients, even on the same machine, are not trusted unless they have a
normal authenticated session. This is what makes WebUI remote access safe to
expose behind login.
### No-auth local mode (`--local` on `nomicore`, or `--insecure-no-auth` on the web host)
- Authentication and CSRF are turned off entirely. Every request acts as `system_default_user`.
- A permissive CORS layer is added so the desktop WebView (and tooling) can call the API freely.
- Local-only routes such as `/api/auth/internal/*` and `/api/webui/*` become reachable.
The trust boundary in local mode is the network — only ever expose it on loopback or a fully trusted private network. The web host loudly logs a warning if `--insecure-no-auth` is combined with a non-loopback bind.
## Body size and limits
- The default request body limit is **10 MiB** (`BODY_LIMIT` in `nomifun-common`). Routes that legitimately need more (file upload, ZIP creation, …) install their own larger limit — `/api/fs/upload` accepts up to 30 MiB.
- Remote images downloaded on behalf of the user are capped at 5 MiB and follow at most 5 redirects.
## Route groups
Each group is owned by a specific crate. The base path is the actual URL prefix mounted into the app router; auth applies in authenticated mode and desktop local-trust mode.
| Group | Base path | Auth | Owning crate / file |
|---|---|---|---|
| Health | `/health` | public | [`router/health.rs`](../../crates/backend/nomifun-app/src/router/health.rs) |
| Auth — login / setup / status / refresh | `/login`, `/logout`, `/api/auth/*`, `/api/ws-token`, `/qr-login` | mixed (login/setup/qr-login: public; rest: authenticated) | [`nomifun-auth/src/routes.rs`](../../crates/backend/nomifun-auth/src/routes.rs) |
| Auth — local-only admin/internal | `/api/webui/*`, `/api/auth/internal/*` | local mode only | same as above |
| Conversations | `/api/conversations/*`, `/api/messages/search` | authenticated | [`nomifun-conversation/src/routes.rs`](../../crates/backend/nomifun-conversation/src/routes.rs), [`routes_aux.rs`](../../crates/backend/nomifun-conversation/src/routes_aux.rs) |
| Agents (local CLI agents) | `/api/agents/*` | authenticated | [`nomifun-ai-agent/src/routes/agent.rs`](../../crates/backend/nomifun-ai-agent/src/routes/agent.rs) |
| Remote agents | `/api/remote-agents/*` | authenticated | [`nomifun-ai-agent/src/routes/remote.rs`](../../crates/backend/nomifun-ai-agent/src/routes/remote.rs) |
| Assistants | `/api/assistants/*` | authenticated | [`nomifun-assistant/src/routes.rs`](../../crates/backend/nomifun-assistant/src/routes.rs) |
| Assistant tags | `/api/assistant-tags/*` | authenticated | same as above |
| MCP servers | `/api/mcp/*` | authenticated | [`nomifun-mcp/src/routes.rs`](../../crates/backend/nomifun-mcp/src/routes.rs) |
| Skills | `/api/skills/*` | authenticated | [`nomifun-extension/src/skill_routes.rs`](../../crates/backend/nomifun-extension/src/skill_routes.rs) |
| Extensions | `/api/extensions/*` | authenticated | [`nomifun-extension/src/routes.rs`](../../crates/backend/nomifun-extension/src/routes.rs) |
| Hub (extension marketplace) | `/api/hub/*` | authenticated | [`nomifun-extension/src/hub_routes.rs`](../../crates/backend/nomifun-extension/src/hub_routes.rs) |
| Cron jobs | `/api/cron/*` | authenticated | [`nomifun-cron/src/routes.rs`](../../crates/backend/nomifun-cron/src/routes.rs) |
| Channels (IM bridges) | `/api/channel/*` | authenticated | [`nomifun-channel/src/routes.rs`](../../crates/backend/nomifun-channel/src/routes.rs) |
| Webhooks + tag settings | `/api/webhooks/*`, `/api/tags/{tag}/settings` | authenticated | [`nomifun-webhook/src/routes.rs`](../../crates/backend/nomifun-webhook/src/routes.rs) |
| Requirements (project board) | `/api/requirements/*` | authenticated | [`nomifun-requirement/src/routes.rs`](../../crates/backend/nomifun-requirement/src/routes.rs) |
| AutoWork / IDMM | `/api/idmm/*`, `/api/requirements/autowork*` | authenticated | [`nomifun-idmm/src/routes.rs`](../../crates/backend/nomifun-idmm/src/routes.rs) |
| Teams (backend implementation surface; no current frontend guide route) | `/api/teams/*` | authenticated | [`nomifun-team/src/routes.rs`](../../crates/backend/nomifun-team/src/routes.rs) |
| Terminals | `/api/terminals/*` | authenticated | [`nomifun-terminal/src/routes.rs`](../../crates/backend/nomifun-terminal/src/routes.rs) |
| Terminal knowledge registration helpers | `/api/terminals/mcp-register-template`, `/api/terminals/register-knowledge*`, `/api/terminals/knowledge-global-status` | authenticated | [`router/health.rs`](../../crates/backend/nomifun-app/src/router/health.rs) |
| Knowledge bases | `/api/knowledge/*` | authenticated | [`nomifun-knowledge/src/routes.rs`](../../crates/backend/nomifun-knowledge/src/routes.rs) |
| Companion | `/api/companion/*` | authenticated | [`nomifun-companion/src/routes.rs`](../../crates/backend/nomifun-companion/src/routes.rs) |
| Companion access tokens for WebUI/public capability use | `/api/webui/companions/{id}/access-token` | authenticated/local WebUI admin flow | [`router/companion_token_routes.rs`](../../crates/backend/nomifun-app/src/router/companion_token_routes.rs) |
| Browser-use secrets | `/api/browser-secrets/*` | authenticated | [`nomifun-secret/src/routes.rs`](../../crates/backend/nomifun-secret/src/routes.rs) |
| Filesystem | `/api/fs/*` | authenticated | [`nomifun-file/src/routes.rs`](../../crates/backend/nomifun-file/src/routes.rs) |
| Office preview | `/api/word-preview/*`, `/api/excel-preview/*`, `/api/ppt-preview/*`, `/api/document/convert`, `/api/preview-history/*`, `/api/star-office/detect` | authenticated | [`nomifun-office/src/routes.rs`](../../crates/backend/nomifun-office/src/routes.rs) |
| Office iframe proxies | `/api/ppt-proxy/*`, `/api/office-watch-proxy/*` | public (serve iframe content; no auth) | same as above |
| Settings + providers + system info | `/api/settings`, `/api/providers/*`, `/api/system/*` | authenticated | [`nomifun-system/src/routes.rs`](../../crates/backend/nomifun-system/src/routes.rs) |
| Global model failover queue | `/api/agent/model-failover` | authenticated | [`router/model_failover.rs`](../../crates/backend/nomifun-app/src/router/model_failover.rs) |
| Connection probes (Bedrock, …) | `/api/bedrock/test-connection` | authenticated | [`nomifun-system/src/bedrock_probe/routes.rs`](../../crates/backend/nomifun-system/src/bedrock_probe/routes.rs) |
| Shell helpers + STT | `/api/shell/*`, `/api/stt` | authenticated | [`nomifun-shell/src/routes.rs`](../../crates/backend/nomifun-shell/src/routes.rs) |
| Public assets (logos) | `/api/assets/logos/*` | public | [`nomifun-assets/src/routes.rs`](../../crates/backend/nomifun-assets/src/routes.rs) |
| Public MCP front door | `/mcp/*` | companion-token / configured public auth | [`nomifun-public/src/router.rs`](../../crates/backend/nomifun-public/src/router.rs) |
| Public MCP agent front door | `/mcp-agent/*` | companion-token / configured public auth | [`nomifun-public/src/router.rs`](../../crates/backend/nomifun-public/src/router.rs) |
| Remote capability REST API | `/v1/*` | companion-token | [`nomifun-public/src/rest.rs`](../../crates/backend/nomifun-public/src/rest.rs) |
| Realtime WebSocket | `/ws` | authenticated (token in `Sec-WebSocket-Protocol` or query) | [`nomifun-realtime/src/handler.rs`](../../crates/backend/nomifun-realtime/src/handler.rs) |
For the exact set of methods on each route, read the corresponding `routes.rs` file — every router declares its routes inline.
### Selected auth endpoints
These are the auth endpoints clients are most likely to interact with directly:
| Method + path | Purpose |
|---|---|
| `POST /login` | Username + password login. Returns `{success, user, token}` and sets the session cookie. CSRF-exempt. Rate-limited. |
| `POST /api/auth/setup` | One-time first-run admin creation on a fresh install. Atomic; concurrent callers race on a conditional UPDATE so only one wins (the others get `409 Conflict`). CSRF-exempt. |
| `POST /logout` | Blacklists the current token; clears the session cookie. |
| `GET /api/auth/status` | Public — reports `{needs_setup, user_count, is_authenticated}`. Useful as a liveness/health probe. |
| `GET /api/auth/user` | Returns the current `{id, username}`. |
| `POST /api/auth/change-password` | Changes the current user's password and rotates the JWT secret (invalidating every other session). |
| `POST /api/auth/refresh` | Refreshes a token that is still valid but near expiry. |
| `GET /api/ws-token` | Returns the token to use for the WebSocket upgrade. |
| `POST /api/auth/qr-login` | Consume a one-shot QR-login token (issued via the WebUI remote-access flow). |
| `GET /qr-login` | Static HTML page that completes a QR login redirect from a phone scanner. |
## WebSocket event model
`/ws` is the single bidirectional channel for streaming updates: agent token streams, terminal output, requirement / cron / team state changes, etc.
- Authentication: a JWT obtained from `GET /api/ws-token`, sent in the WebSocket `Sec-WebSocket-Protocol` header (or `Authorization`). Invalid or expired token → server sends an `auth-expired` event and closes with code `1008`. No token at all → close with `1008`, reason `"no token provided"`.
- After a successful upgrade, every message is a JSON object with a `type` and a `payload`. Messages are pushed by the server when domain events occur (a new agent token, a terminal byte, a requirement transition); clients usually do not need to send anything back. The server multiplexes a single `BroadcastEventBus` to every connected client.
- Heartbeats: ping every 30s, timeout at 60s (`HEARTBEAT_INTERVAL_MS` / `HEARTBEAT_TIMEOUT_MS`).
- Close codes: `1000` for a normal close, `1008` for policy violations (auth failure, invalid token).
The set of `type` values is open-ended — extensions and feature modules emit their own. Treat unknown types as forward-compatible: ignore them.
## Response envelope
Most JSON responses use the same shape (`ApiResponse<T>` from `nomifun-api-types`):
```json
{ "success": true, "data": { ... } }
```
Errors are returned with the appropriate HTTP status and a body like:
```json
{ "success": false, "error": "Invalid username or password" }
```
The login/setup/refresh handlers return slightly enriched envelopes (`LoginResponse`, `RefreshResponse`) — they include the token or user object inline.
## Source-of-truth pointers
The list above is meant to get you to the right module. From there, read the source — every router declares its routes in one place, and every handler is in the same file or the next one over. The router assembly itself is in [`crates/backend/nomifun-app/src/router/routes.rs`](../../crates/backend/nomifun-app/src/router/routes.rs); the middleware stack (CSRF, security headers, body limit, optional CORS) is also there.
## See also
- [Configuration Reference](./configuration.md) — flags, env vars, the auth secret resolution order.
- [Troubleshooting](./troubleshooting.md) — common API and WebSocket failure modes.
- [Web Server Deployment](../guides/web-server-deployment.md) — exposing the API over the network behind TLS.
@@ -0,0 +1,183 @@
# API 概览
NomiFun 的后端(`nomifun-app`,二进制 `nomicore`)对外暴露的是单一的
axum HTTP 服务。SPA、桌面外壳,以及任何外部集成,与它沟通的方式都一样:
HTTP 上的 JSON 用于命令/查询,WebSocket 用于流式事件。
本页是一份**导览**,不是穷尽式的端点参考。完整的接口面位于
`crates/backend/` 下的各路由模块;源码即权威参考。下方列出了各分组的
基础路径与对应的路由 owner——请从那里开始查阅。
## Base URL
| 宿主 | 默认 base URL | 备注 |
|---|---|---|
| `nomifun-desktop` | `http://127.0.0.1:<picked-port>` | 启动时挑选一个空闲的 localhost 端口。渲染端通过 IPC 获知端口号,并以此向 `/api``/ws` 发起调用。 |
| `nomifun-web` | `http://<host>:<port>`(默认 `http://127.0.0.1:8787`) | 同一个后端,与 SPA 一并在同一个端口上提供。 |
| `nomicore` 独立运行 | `http://127.0.0.1:25808` | 单独运行后端——便于调试。 |
SPA 使用**相对路径**`/api/...``/ws`)。客户端不需要指向另一台 API
服务——SPA 与 API 同址。
## 鉴权模型
NomiFun 启动时进入三种鉴权策略之一:
### 已鉴权模式(`nomifun-web` 默认)
- 通过 `POST /login` 登录,返回一个会话 JWT,同时写入 cookie
`nomifun-session``HttpOnly`)与 JSON body。后续请求依靠该 cookie
`Authorization: Bearer …` 请求头进行鉴权。
- 状态变更类请求还必须附带 CSRF 请求头 `x-csrf-token`,其值需与
`nomifun-csrf-token` cookie 匹配(Double Submit Cookie 模式)。安全
方法(`GET``HEAD``OPTIONS`)跳过 CSRF;登录/设置/二维码登录端点
因尚无会话被豁免。
- WebSocket 升级携带同一份 JWT——通常通过 `Sec-WebSocket-Protocol`
传输,可由 `GET /api/ws-token` 获取。`/ws` 路由对 CSRF 豁免(在
WebSocket 升级中无法做基于 cookie 的双提交),但仍需鉴权。
- 限流器分别按客户端作用于登录尝试、一般 API 流量与已鉴权的状态变更
动作。
### 桌面本地信任模式(`nomifun-desktop`
- 嵌入式后端使用 `AuthPolicy::TrustLocalToken`
- 桌面 WebView 会得到每次启动生成的 secret(`window.__nomiLocalTrust`),并在 HTTP/WebSocket 请求中呈递它。
- 其他客户端即使在同一台机器上,也不会因为来自 loopback 自动受信任;除非它拥有正常登录会话。这也是 WebUI 远程访问可以放在登录后的原因。
### 无鉴权本地模式(`nomicore --local`,或 Web 宿主 `--insecure-no-auth`
- 鉴权与 CSRF 完全关闭。每个请求都以 `system_default_user` 身份执行。
- 加入一层宽松的 CORS,使桌面 WebView(以及工具)可以自由调用 API。
- 仅本地可达的路由(如 `/api/auth/internal/*``/api/webui/*`)变为
可达。
本地模式下的信任边界是网络——只能将其暴露在 loopback 或完全受信任的
私有网络上。Web 宿主在 `--insecure-no-auth` 与非 loopback 绑定同时使用
时会大声地打印警告日志。
## 请求体大小与上限
- 请求体的默认大小上限是 **10 MiB**`nomifun-common` 中的
`BODY_LIMIT`)。确实需要更大的路由(文件上传、ZIP 创建等)会安装自己
的更大限制——`/api/fs/upload` 接受最大 30 MiB。
- 代用户下载的远程图片上限为 5 MiB,最多跟随 5 次重定向。
## 路由分组
每个分组归属一个特定的 crate。下表中的基础路径就是挂载到 app router 中
的实际 URL 前缀;鉴权在已鉴权模式和桌面本地信任模式下生效。
| 分组 | 基础路径 | 鉴权 | 归属 crate / 文件 |
|---|---|---|---|
| 健康检查 | `/health` | 公共 | [`router/health.rs`](../../crates/backend/nomifun-app/src/router/health.rs) |
| 鉴权 —— 登录 / 设置 / 状态 / 刷新 | `/login``/logout``/api/auth/*``/api/ws-token``/qr-login` | 混合(登录/设置/qr-login:公共;其余:已鉴权) | [`nomifun-auth/src/routes.rs`](../../crates/backend/nomifun-auth/src/routes.rs) |
| 鉴权 —— 仅本地 admin/internal | `/api/webui/*``/api/auth/internal/*` | 仅本地模式 | 同上 |
| 会话 | `/api/conversations/*``/api/messages/search` | 已鉴权 | [`nomifun-conversation/src/routes.rs`](../../crates/backend/nomifun-conversation/src/routes.rs)、[`routes_aux.rs`](../../crates/backend/nomifun-conversation/src/routes_aux.rs) |
| 智能体(本地 CLI 智能体) | `/api/agents/*` | 已鉴权 | [`nomifun-ai-agent/src/routes/agent.rs`](../../crates/backend/nomifun-ai-agent/src/routes/agent.rs) |
| 远程智能体 | `/api/remote-agents/*` | 已鉴权 | [`nomifun-ai-agent/src/routes/remote.rs`](../../crates/backend/nomifun-ai-agent/src/routes/remote.rs) |
| 助手 | `/api/assistants/*` | 已鉴权 | [`nomifun-assistant/src/routes.rs`](../../crates/backend/nomifun-assistant/src/routes.rs) |
| 助手标签 | `/api/assistant-tags/*` | 已鉴权 | 同上 |
| MCP 服务 | `/api/mcp/*` | 已鉴权 | [`nomifun-mcp/src/routes.rs`](../../crates/backend/nomifun-mcp/src/routes.rs) |
| 技能 | `/api/skills/*` | 已鉴权 | [`nomifun-extension/src/skill_routes.rs`](../../crates/backend/nomifun-extension/src/skill_routes.rs) |
| 扩展 | `/api/extensions/*` | 已鉴权 | [`nomifun-extension/src/routes.rs`](../../crates/backend/nomifun-extension/src/routes.rs) |
| Hub(扩展市场) | `/api/hub/*` | 已鉴权 | [`nomifun-extension/src/hub_routes.rs`](../../crates/backend/nomifun-extension/src/hub_routes.rs) |
| 计划任务 | `/api/cron/*` | 已鉴权 | [`nomifun-cron/src/routes.rs`](../../crates/backend/nomifun-cron/src/routes.rs) |
| 频道(IM 桥) | `/api/channel/*` | 已鉴权 | [`nomifun-channel/src/routes.rs`](../../crates/backend/nomifun-channel/src/routes.rs) |
| Webhook + 标签设置 | `/api/webhooks/*``/api/tags/{tag}/settings` | 已鉴权 | [`nomifun-webhook/src/routes.rs`](../../crates/backend/nomifun-webhook/src/routes.rs) |
| 需求(项目看板) | `/api/requirements/*` | 已鉴权 | [`nomifun-requirement/src/routes.rs`](../../crates/backend/nomifun-requirement/src/routes.rs) |
| AutoWork / IDMM | `/api/idmm/*``/api/requirements/autowork*` | 已鉴权 | [`nomifun-idmm/src/routes.rs`](../../crates/backend/nomifun-idmm/src/routes.rs) |
| 团队(后端实现面;当前没有对应用户指南路由) | `/api/teams/*` | 已鉴权 | [`nomifun-team/src/routes.rs`](../../crates/backend/nomifun-team/src/routes.rs) |
| 终端 | `/api/terminals/*` | 已鉴权 | [`nomifun-terminal/src/routes.rs`](../../crates/backend/nomifun-terminal/src/routes.rs) |
| 终端 knowledge 注册辅助 | `/api/terminals/mcp-register-template``/api/terminals/register-knowledge*``/api/terminals/knowledge-global-status` | 已鉴权 | [`router/health.rs`](../../crates/backend/nomifun-app/src/router/health.rs) |
| 知识库 | `/api/knowledge/*` | 已鉴权 | [`nomifun-knowledge/src/routes.rs`](../../crates/backend/nomifun-knowledge/src/routes.rs) |
| 伙伴 | `/api/companion/*` | 已鉴权 | [`nomifun-companion/src/routes.rs`](../../crates/backend/nomifun-companion/src/routes.rs) |
| WebUI/public 能力 companion token | `/api/webui/companions/{id}/access-token` | 已鉴权 / 本地 WebUI admin 流 | [`router/companion_token_routes.rs`](../../crates/backend/nomifun-app/src/router/companion_token_routes.rs) |
| Browser-use secrets | `/api/browser-secrets/*` | 已鉴权 | [`nomifun-secret/src/routes.rs`](../../crates/backend/nomifun-secret/src/routes.rs) |
| 文件系统 | `/api/fs/*` | 已鉴权 | [`nomifun-file/src/routes.rs`](../../crates/backend/nomifun-file/src/routes.rs) |
| Office 预览 | `/api/word-preview/*``/api/excel-preview/*``/api/ppt-preview/*``/api/document/convert``/api/preview-history/*``/api/star-office/detect` | 已鉴权 | [`nomifun-office/src/routes.rs`](../../crates/backend/nomifun-office/src/routes.rs) |
| Office iframe 代理 | `/api/ppt-proxy/*``/api/office-watch-proxy/*` | 公共(提供 iframe 内容;不鉴权) | 同上 |
| 设置 + 提供商 + 系统信息 | `/api/settings``/api/providers/*``/api/system/*` | 已鉴权 | [`nomifun-system/src/routes.rs`](../../crates/backend/nomifun-system/src/routes.rs) |
| 全局模型故障转移队列 | `/api/agent/model-failover` | 已鉴权 | [`router/model_failover.rs`](../../crates/backend/nomifun-app/src/router/model_failover.rs) |
| 连接探测(Bedrock 等) | `/api/bedrock/test-connection` | 已鉴权 | [`nomifun-system/src/bedrock_probe/routes.rs`](../../crates/backend/nomifun-system/src/bedrock_probe/routes.rs) |
| Shell 辅助 + STT | `/api/shell/*``/api/stt` | 已鉴权 | [`nomifun-shell/src/routes.rs`](../../crates/backend/nomifun-shell/src/routes.rs) |
| 公共资源(logo | `/api/assets/logos/*` | 公共 | [`nomifun-assets/src/routes.rs`](../../crates/backend/nomifun-assets/src/routes.rs) |
| Public MCP front door | `/mcp/*` | companion-token / 已配置 public auth | [`nomifun-public/src/router.rs`](../../crates/backend/nomifun-public/src/router.rs) |
| Public MCP agent front door | `/mcp-agent/*` | companion-token / 已配置 public auth | [`nomifun-public/src/router.rs`](../../crates/backend/nomifun-public/src/router.rs) |
| Remote capability REST API | `/v1/*` | companion-token | [`nomifun-public/src/rest.rs`](../../crates/backend/nomifun-public/src/rest.rs) |
| 实时 WebSocket | `/ws` | 已鉴权(token 通过 `Sec-WebSocket-Protocol` 或查询串传递) | [`nomifun-realtime/src/handler.rs`](../../crates/backend/nomifun-realtime/src/handler.rs) |
如需各路由具体支持的方法,请阅读对应的 `routes.rs` 文件——每个 router
都在源文件内联声明自身的路由。
### 选取的鉴权端点
下面这些是客户端最常直接交互的鉴权端点:
| 方法 + 路径 | 用途 |
|---|---|
| `POST /login` | 用户名 + 密码登录。返回 `{success, user, token}` 并设置会话 cookie。CSRF 豁免。带限流。 |
| `POST /api/auth/setup` | 全新安装上的一次性首位管理员创建。原子操作;并发调用通过条件 UPDATE 竞争,只有一个会赢(其余得到 `409 Conflict`)。CSRF 豁免。 |
| `POST /logout` | 将当前 token 加入黑名单;清除会话 cookie。 |
| `GET /api/auth/status` | 公共——返回 `{needs_setup, user_count, is_authenticated}`。可作为 liveness/health 探针。 |
| `GET /api/auth/user` | 返回当前 `{id, username}`。 |
| `POST /api/auth/change-password` | 修改当前用户密码并轮换 JWT 密钥(使其他会话全部失效)。 |
| `POST /api/auth/refresh` | 刷新仍然有效但接近过期的 token。 |
| `GET /api/ws-token` | 返回用于 WebSocket 升级的 token。 |
| `POST /api/auth/qr-login` | 消费一次性的二维码登录 token(由 WebUI 远程访问流程下发)。 |
| `GET /qr-login` | 静态 HTML 页面,用于完成来自手机扫码的二维码登录跳转。 |
## WebSocket 事件模型
`/ws` 是用于流式更新的单一双向通道:智能体 token 流、终端输出,
需求/计划任务/团队的状态变化等等。
- 鉴权:通过 `GET /api/ws-token` 获得的 JWT,放在 WebSocket 的
`Sec-WebSocket-Protocol` 请求头中(或 `Authorization`)。token 无效或
过期 → 服务端发出 `auth-expired` 事件并以 `1008` 关闭。完全没有
token → 以 `1008` 关闭,原因为 `"no token provided"`
- 升级成功后,每条消息都是带 `type``payload` 的 JSON 对象。当域内
事件发生时(新的智能体 token、一个终端字节、需求状态切换),由
服务端推送;客户端通常无需回送任何内容。服务端把单一的
`BroadcastEventBus` 多路复用给所有已连接客户端。
- 心跳:每 30 秒 ping 一次,60 秒超时(`HEARTBEAT_INTERVAL_MS` /
`HEARTBEAT_TIMEOUT_MS`)。
- 关闭码:`1000` 表示正常关闭;`1008` 表示策略违规(鉴权失败、token
无效)。
`type` 取值集合是开放的——扩展与功能模块会发出各自的类型。请把未知
类型当作向前兼容的:忽略它们即可。
## 响应包络
绝大多数 JSON 响应使用同一种形状(来自 `nomifun-api-types`
`ApiResponse<T>`):
```json
{ "success": true, "data": { ... } }
```
错误使用恰当的 HTTP 状态码返回,body 形如:
```json
{ "success": false, "error": "Invalid username or password" }
```
登录/设置/刷新这几个 handler 会返回略微富化的包络
`LoginResponse``RefreshResponse`)——它们会把 token 或 user 对象
内联在响应中。
## 真值来源指引
上面的列表只是为了把你引导到对的模块。到达后请阅读源码——每个 router
在一处声明全部路由,每个 handler 都在同一个文件或紧挨着的下一个文件
里。Router 装配本身位于
[`crates/backend/nomifun-app/src/router/routes.rs`](../../crates/backend/nomifun-app/src/router/routes.rs)
中间件栈(CSRF、安全响应头、请求体上限、可选的 CORS)也在那里。
## 另见
- [配置参考](./configuration.zh.md) —— 参数、环境变量、鉴权密钥解析顺序。
- [疑难排查](./troubleshooting.zh.md) —— 常见的 API 与 WebSocket 故障
形态。
- [Web 服务部署](../guides/web-server-deployment.md) —— 在 TLS 之后把
API 暴露到网络上。

Some files were not shown because too many files have changed in this diff Show More