内容
# Lumen AI 聊天机器人
一个自托管的 Flask 聊天机器人,具有实时流媒体、逐对话 Docker 沙盒、MCP 服务器支持和零构建步骤的前端。
Lumen 是为开发者设计的,他们想要一个功能齐全的本地 AI 聊天应用程序,而无需复杂的后端基础设施。后台使用普通的 Flask,前端使用原生浏览器 ES 模块直接提供服务 - 无需打包器、无需框架、无需部署仪式。
<img width="2890" height="2178" alt="ChatGPT Image May 19, 2026, 07_53_37 PM-Photoroom (2)" src="https://github.com/user-attachments/assets/4d21b2aa-5925-48eb-a7f6-f71bc78d10f7" />
---
## 功能特点
**模型支持**
- 任何 OpenAI 兼容的 API - OpenAI、Ollama、LM Studio 或本地代理
- 在浏览器 UI 中配置 API 密钥、基准 URL、模型和系统提示
- 代理模型列表获取,您可以在不离开聊天的情况下选择模型
**流媒体**
- 响应通过服务器发送事件(SSE)逐 token 流式传输
- 取消流式传输过程中;部分响应会自动保存
- 如果您导航离开并返回,则可以重新附加到正在进行的流
**MCP(模型上下文协议)**
- 通过 UI 或 `mcp.json` 添加 MCP 服务器
- 工具元数据将 MCP 服务器名称与模型面向的工具名称分开
- 持久的 MCP 标准输入/输出会话池 - 会话在每次对话时打开一次,并在容器停止之前在所有回合中重复使用
- 批准或拒绝单个工具调用;为每个服务器启用自动批准
- 工具活动内联渲染:参数、运行状态和结果
**持久内存**
- 模型通过 `~/.lumen/memory.md` 在所有对话中记住事实
- 安装读写到 `/memory.md` 以便模型可以使用其文件工具进行更新
- 内存内容在每个回合开始时注入到系统提示中
**逐对话 Docker 沙盒**
- 每个对话都有自己的 Docker 容器和工作区目录
- 工作区在容器内挂载为 `/workspace`
- MCP 服务器在容器内运行 - 代码执行与主机隔离
- 容器在空闲时自动停止(可配置超时)
- 停止会话的旧容器在启动时清理
**文件处理**
- 通过浏览器面板将文件上传到对话工作区
- 在线预览文本/代码/Markdown 文件;下载任意工作区文件
- 图像按 SHA-256 内容哈希存储,并作为视觉输入发送到兼容的模型
**用户界面**
- Markdown、语法高亮代码块和 KaTeX 数学渲染
- 通过浏览器 Web Speech API 进行语音输入
- 首次交换后自动生成对话标题
- 创建、重命名、删除和搜索对话
- 在消息页脚使用箭头在编辑消息和重新生成响应分支之间切换
- 可自定义主题、强调色、字体大小、侧边栏、时间戳等 - 存储在 `localStorage` 中
**架构**
- 无数据库 - 对话是纯 JSON 文件,位于 `~/.lumen/` 下
- 无前端构建步骤 - Flask 直接提供 `templates/` 和 `static/` 服务
- 无打包器 - 浏览器 ES 模块使用 `type="module"` 导入
---
## 快速入门
```bash
# 1. 克隆
git clone https://github.com/yossifibrahem/lumen-ai-chat.git
cd lumen-ai-chat
# 2. 创建并激活虚拟环境
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. 安装依赖
pip install -r requirements.txt
# 4. 构建 Docker 沙盒镜像(MCP 工具需要)
docker build -f Dockerfile.sandbox -t lumen-sandbox .
# 5. 启动应用程序
python app.py
```
打开 **http://localhost:8080**,然后打开设置面板输入您的 API 密钥、基准 URL 和模型名称。
对于局域网测试,Web 开发服务器默认绑定到 `0.0.0.0`,因此同一受信任网络上的其他设备可以打开 `http://<your-computer-ip>:8080`。如果您希望浏览器访问仅限于同一台机器,请设置 `LUMEN_HOST=127.0.0.1`。
> 如果 Docker 未运行或 `lumen-sandbox` 镜像未构建,应用程序将启动并显示一个设置屏幕,带有重试/构建沙盒镜像操作。
### 内置支持的工具
建议将这些 MCP 服务器与此应用程序一起使用:
- [Agent Tools MCP 服务器 - view、create_file、str_replace、bash_tool](https://github.com/yossifibrahem/file-tools-mcp-server)
- [Exa MCP 服务器用于网络搜索](https://github.com/exa-labs/exa-mcp-server)
## 桌面应用程序 *(实验性)*
桌面版是现有 Flask 应用程序周围的 Electron 外壳。它直接从 `desktop/main.js` 启动 Flask 应用程序对象,在稳定的本地端口上打开原生窗口,并保留相同的 `~/.lumen/` 数据文件夹、Docker 沙盒镜像、MCP 配置、对话和工作区。稳定端口保留浏览器 `localStorage` 在同一来源,因此保存的 UI 设置在应用程序重启后仍然存在。不需要对 Flask 应用程序源文件进行桌面特定更改。
首先安装 Python 依赖项:
```bash
pip install -r requirements.txt
```
安装桌面依赖项:
```bash
npm install
```
在开发中运行桌面应用程序:
```bash
npm run desktop
```
构建打包的桌面应用程序:
```bash
npm run dist
```
打包的应用程序仍然需要 Python 和 Python 依赖项在机器上可用。要强制使用特定的 Python 可执行文件,请在启动桌面应用程序之前设置 `LUMEN_PYTHON`。
桌面模式默认使用端口 `38492`,因此本地 UI 首选项与相同的 `127.0.0.1` 来源绑定。如果该端口已被占用,请关闭其他 Lumen 实例或将 `LUMEN_DESKTOP_PORT` 设置为另一个稳定的空闲端口。Electron 包装器控制桌面主机/端口本身;`app.py` 对于正常 Web/服务器使用保持不变。
在 Windows 和 Linux 上,Electron 使用无边框窗口,并从 `desktop/titlebar.css` 和 `desktop/titlebar.js` 注入安静的桌面特定标题栏。标题栏在左侧保留一个小应用程序图标,在中心显示应用程序名称,并在右侧放置窗口控件。只有标题栏背景遵循应用程序强调色调;图标、居中标题、分隔符和悬停状态保持与 UI 其余部分相同的应用程序主题颜色。macOS 保留原生标题栏。
### 应用程序图标
桌面图标文件位于 `desktop/assets/`:
- `icon.svg` 是可编辑的源图标。
- `icon.png` 用于开发窗口和 Linux 构建。
- `icon.ico` 用于 Windows 打包构建。
替换图标文件后,重新运行 `npm run dist` 以创建具有更新图标的新安装程序/可执行文件。浏览器图标仍然来自 `static/favicon.svg`。
---
## 安装
### 先决条件
| 需求 | 版本 | 备注 |
|---|---|---|
| Python | 3.10+ | Flask 后端 |
| Docker | 20.10+ | MCP 沙盒容器所需 |
| Node.js / npm | 当前 LTS | 可选 - 需要通过 `npx` 启动的 MCP 服务器 |
| OpenAI 兼容的 API | - | OpenAI、Ollama、LM Studio 或兼容的代理 |
### Python 依赖项
```bash
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
```
```
flask>=3.0.0
flask-cors>=4.0.0
openai>=1.30.0
mcp>=1.0.0
```
### 构建沙盒镜像
所有 MCP 服务器都在逐对话的 Docker 容器中运行。在启动应用程序之前,一次构建镜像:
```bash
docker build -f Dockerfile.sandbox -t lumen-sandbox .
```
镜像名称可通过 `LUMEN_SANDBOX_IMAGE` 配置(默认:`lumen-sandbox`)。
### 生产部署
对于单进程生产样式部署,使用 Gunicorn:
```bash
gunicorn -c gunicorn.conf.py "app:create_app()"
```
`gunicorn.conf.py` 默认配置为 **一个 worker 和多个线程**。活动流状态(取消事件、重新附加缓冲区)存储在进程内存中,因此不支持多个 worker 进程,直到流状态移动到共享存储。
---
## 配置
### 应用程序内设置
在浏览器中打开设置面板以配置:
| 设置 | 描述 |
|---|---|
| API 密钥 | 提供商 API 密钥 - 服务器端存储,永远不会在 `localStorage` 中 |
| 基准 URL | OpenAI 兼容的端点,例如 `https://api.openai.com/v1` |
| 模型 | 下一个请求的模型 ID |
| 系统提示 | 可选指令,附加到每个对话中 |
打开 **容器/高级设置** 面板以配置:
| 设置 | 描述 |
|---|---|
| 沙盒镜像 | 沙盒容器的 Docker 镜像名称 |
| 容器内存 | 每个容器的内存限制(例如 `512m`、`1g`) |
| 容器 CPU | 每个容器的 CPU 配额 |
| 容器网络 | Docker 网络模式 |
| 容器空闲超时 | 空闲容器停止前的秒数;`0` 禁用 |
| 最大文件预览字节 | 浏览器内文本文件预览加载的最大字节数 |
| 最大文件列表条目 | 返回的最大工作区目录条目数 |
| 最大上传字节 | 最大文件上传大小 |
> 如果设置被环境变量锁定,则该字段在 UI 中被禁用,并显示哪个变量控制它。
本地模型提供商通常使用:
```
http://localhost:11434/v1 # Ollama
http://localhost:1234/v1 # LM Studio
```
### 环境变量
| 变量 | 默认值 | 描述 |
|---|---|---|
| `OPENAI_API_KEY` | - | 覆盖保存的 API 密钥 |
| `OPENAI_BASE_URL` | - | 覆盖保存的基准 URL |
| `OPENAI_API_BASE` | - | `OPENAI_BASE_URL` 的备用别名 |
| `LUMEN_CONFIG_FILE` | `~/.lumen/config.json` | 服务器端 API 配置路径 |
| `LUMEN_CONFIG_CACHE_TTL` | `5` | 缓存 API 配置读取的秒数 |
| `LUMEN_ADVANCED_CONFIG_FILE` | `~/.lumen/advanced_config.json` | 高级/容器设置配置文件路径 |
| `LUMEN_MCP_CONFIG_FILE` | `~/.lumen/mcp.json` | MCP 服务器配置文件路径 |
| `LUMEN_MCP_CONFIG_CACHE_TTL` | `5` | 缓存 MCP 配置读取的秒数 |
| `LUMEN_SANDBOX_IMAGE` | `lumen-sandbox` | 沙盒容器的 Docker 镜像 |
| `LUMEN_CONTAINERS_ROOT` | `~/.lumen/containers` | 每个对话的工作区的主机目录 |
| `LUMEN_CONTAINER_MEMORY` | `512m` | 每个沙盒容器的内存限制 |
| `LUMEN_CONTAINER_CPUS` | `1` | 每个沙盒容器的 CPU 配额 |
| `LUMEN_CONTAINER_NETWORK` | `bridge` | Docker 网络模式 |
| `LUMEN_CONTAINER_PREFIX` | `lumen-chat-` | 生成的容器名称的前缀 |
| `LUMEN_CONTAINER_IDLE_TIMEOUT` | `600` | 空闲容器停止前的秒数;`0` 禁用 |
| `LUMEN_MAX_CONTENT_LENGTH` | `62914560` | Flask 请求体上限(字节) |
| `LUMEN_CORS_ORIGINS` | `http://localhost:8080,...` | 以逗号分隔的允许来源列表 |
| `LUMEN_MAX_FILE_PREVIEW_BYTES` | `524288` | 浏览器内文本文件预览的最大字节数 |
| `LUMEN_MAX_FILE_LIST_ENTRIES` | `500` | 返回的最大工作区目录条目数 |
| `LUMEN_MAX_UPLOAD_BYTES` | `52428800` | 最大文件上传大小(字节) |
容器/高级设置的环境变量(`LUMEN_SANDBOX_IMAGE`、`LUMEN_CONTAINER_*`、`LUMEN_MAX_*`)具有最高优先级,并锁定相应的 UI 字段,因此操作员值无法从浏览器覆盖。
### 持久数据
所有运行时数据存储在 `~/.lumen/` 之外:
```
~/.lumen/
├── config.json # 服务器端 API 提供商配置
├── advanced_config.json # 容器和文件处理设置(由 UI 编写)
├── mcp.json # MCP 服务器配置
├── memory.md # 持久的跨聊天内存;挂载在每个容器中的 /memory.md
├── conversations/ # 每个对话一个 JSON 文件
├── containers/ # 每个对话一个工作区目录
└── images/ # 上传的图像按 SHA-256 哈希键控
```
### MCP 配置
MCP 服务器在 `~/.lumen/mcp.json` 中配置(或通过 `LUMEN_MCP_CONFIG_FILE` 设置路径)。设置面板可以从浏览器读取和写入此文件。
所有 MCP 服务器都在对话的 Docker 容器中运行,工作区在 `/workspace` 处挂载。在服务器参数中使用 `/workspace` 相对路径。
**示例 `mcp.json`:**
```json
{
"mcpServers": {
"agent_tools": {
"command": "node",
"args": [
"/path/to/file-tools-mcp-server/dist/index.js"
]
},
"search": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": {
"BRAVE_API_KEY": "your-api-key-here"
}
}
}
}
```
每个服务器的 UI 设置(启用、自动批准、图标)存储在浏览器 `localStorage` 下 `lumen_mcp_server_settings` - 不在 `mcp.json` 中。
模型面向的工具名称直接使用 MCP 工具名称。匹配的 MCP 服务器在 `_build_server_params` 中单独发送,这让后端调度调用时无需在可见工具名称上添加服务器前缀。工具描述应保持干净和语义(`tool.description || tool.name`)。
---
## 用法
### 基本聊天
1. 运行 `python app.py` 并打开 `http://localhost:8080`。
2. 从侧边栏创建新对话。
3. 打开设置并输入您的 API 密钥、基准 URL 和模型。
4. 输入消息并按 **Enter**。使用 **Shift + Enter** 换行。
### 上传文件
打开工作区面板,上传文件,它将出现在对话容器内的 `/workspace/uploads/` 下。在浏览器中预览文本文件或直接下载任意工作区文件。
### 使用 MCP 工具
1. 如果尚未构建沙盒镜像:`docker build -f Dockerfile.sandbox -t lumen-sandbox .`
2. 打开 MCP 设置面板并添加服务器(命令、参数、环境变量)。
3. 保存配置并点击 **加载工具** - 必须打开对话才能发现工具。
4. 发送消息,要求模型使用工具。
5. 在提示时批准或拒绝工具调用(或启用每个服务器的自动批准)。
工具活动内联显示工具名称、参数、运行状态和结果。
### 取消流
单击响应流式传输时单击停止按钮。服务器标记流为已取消,并保存已生成的任何内容。
---
## 架构
### 后端
| 文件 | 责任 |
|---|---|
| `app.py` | Flask 应用程序工厂;启动要求状态处理;CORS;关闭清理 |
| `app_config.py` | 服务器端 API 密钥和提供商配置存储;环境变量覆盖;公共安全元数据 |
| `advanced_config.py` | 服务器端容器和文件处理设置;三层优先级(env > file > defaults);UI 可编辑,env-lock 支持 |
| `runtime_requirements.py` | Docker 可用性和沙盒镜像检查;流式构建日志生成器 |
| `fs_utils.py` | `atomic_replace` 助手,用于安全临时文件替换;Windows 重试逻辑 |
| `docker_path_utils.py` | 跨平台 Docker 卷路径转换 - 将 Windows 驱动器字母路径转换为有效的 Linux 容器挂载目标 |
| `routes.py` | 薄的蓝图注册垫圈 - 注册五个路由组蓝图 |
| `routes_startup.py` | 设置屏幕、健康探测、Docker/镜像需求检查、流式沙盒镜像构建 |
| `routes_conversations.py` | 对话框 CRUD、工作区路径、容器状态、危险删除 |
| `routes_chat.py` | 流式传输、取消、批准、设置、先进/容器设置、模型列表 |
| `routes_mcp.py` | MCP 配置、工具发现、直接工具调用 |
| `routes_files.py` | 工作区文件列表、上传、预览、下载、图像存储 |
| `chat_turn_service.py` | 单个聊天回合编排:流式传输、工具批准、MCP 调用、持久性 |
| `title_service.py` | 自动生成的对话标题:`_SET_TITLE_TOOL` 定义、文本转换、提取和 `generate_title` |
| `tool_approval.py` | 待批工具批准门控:`_pending_approvals` 字典、锁、`request_tool_approval`、`resolve_tool_approval` |
| `streaming.py` | 类型 OpenAI 流事件生成器;SSE 序列化帮助程序 |
| `mcp_service.py` | MCP 配置、工具发现、工具调用;为使用 `_build_server_params` 导出 |
| `mcp_session_pool.py` | `McpSessionPool` 类:工作协程、生命周期、重试逻辑,用于持久跨回合重用 |
| `mcp_adapters.py` | 包装 MCP 命令用于 Docker `exec`;提取并挂载主机卷路径 |
| `container_service.py` | Docker 容器生命周期:创建、启动、停止、空闲收割、工作区管理 |
| `workspace_service.py` | 对话工作区的安全文件操作;路径遍历保护 |
| `store.py` | 对话框和图像的文件系统持久性;缓存的对话索引 |
### 聊天流程
`chat_turn_service.py` 中的单个聊天回合:
1. 从服务器端配置构建 OpenAI 客户端。
2. 读取 `~/.lumen/memory.md` 并将其内容注入系统消息。
3. 预挂载 |
| `mcp_adapters.py` | 封装 MCP 命令用于 Docker `exec`;提取并挂载主机卷路径 |
| `container_service.py` | Docker 容器生命周期:创建、启动、停止、空闲收割、workspace 管理 |
| `workspace_service.py` | 对话框内安全文件操作;路径遍历保护 |
| `store.py` | 会话和图像的文件系统持久性;缓存的会话索引 |
### 前端
前端是纯浏览器 ES 模块 — 无需构建步骤,无需框架。`templates/index.html` 以 `type="module"` 导入 `static/js/app.js`。
| 模块 | 责任 |
|---|---|
| `state.js` | 单个共享的可变状态对象 |
| `app.js` | 引导、事件绑定、启动加载 |
| `chat_send.js` | 发送流程、流启动、SSE 事件处理 |
| `chat_payloads.js` | 构建 API 消息负载,包括图像和文件上下文 |
| `chat_attachments.js` | 待处理图像和文件附件生命周期 |
| `chat_edit.js` | 编辑、重新发送和重新生成帮助器 |
| `chat_branches.js` | 编辑/重新生成的分支快照和分支切换 |
| `stream_consumer.js` | SSE 响应读取器 |
| `renderer.js` | 从渲染器子模块重新导出所有公共符号;现有调用者的唯一导入目标 |
| `renderer_core.js` | `scrollToBottom`、`stickToBottom`、`messagesEl`、`createMessageRow` |
| `renderer_groups.js` | 块分组、`tryGroupBlock`、`updateGroupLabel`、`attachCollapsible`、`prepareAssistantRow` |
| `renderer_thinking.js` | `createThinkingBlock`、`updateThinkingBlock`、`finalizeThinkingBlock`、`appendThinkingBlock` |
| `renderer_attachments.js` | `normalizeContentAttachments`、`renderAttachmentCard`、`getRawText`、`appendContentParts` |
| `renderer_tools.js` | 工具条状态、`cancelAllToolApprovals`、`appendToolResultInline` |
| `renderer_actions.js` | 消息页脚中的复制/编辑/重新生成按钮和分支箭头 |
| `mcp.js` | MCP 配置 UI、工具加载、启用/自动批准切换 |
| `file_panel.js` | 工作区浏览器、预览和下载 |
| `conversations.js` | 会话 CRUD 和侧边栏 |
| `settings.js` | API 和聊天设置 UI |
| `markdown.js` | Markdown、代码高亮、KaTeX、安全的工作区文件链接 |
| `tool_adapters/` | 每个工具的显示适配器(`agent_tools.js`、`exa.js`) |
前端维护两个并行的历史记录:`state.messages`(面向模型/API)和 `state.displayLog`(面向 UI)。它们具有不同的结构和索引 — 不要混淆它们。编辑消息和重新生成的响应的分支存储在 `displayLog` 中,以便可见的对话可以切换路径,同时保留保存的模型历史记录。
---
## 测试
安装测试依赖:
```bash
pip install -r requirements-dev.txt
```
运行完整测试套件:
```bash
pytest
```
测试完全隔离:`conftest.py` 将文件系统路径重定向到 `tmp_path`,修补应用程序工厂中的运行时需求检查,并存根容器操作。无需运行 Docker 守护进程、API 密钥或实时服务器。
| 测试文件 | 测试内容 |
|---|---|
| `test_app_config.py` | 配置加载/保存、环境变量覆盖、公共配置、原子持久性 |
| `test_store.py` | SHA-256 图像命名、会话 CRUD、缓存索引、并发性 |
| `test_workspace_service.py` | 路径遍历拒绝、预览限制、上传冲突处理 |
| `test_chat_turn_service.py` | 工具批准、标题提取、`TurnRecorder` 节流/完成 |
| `test_streaming.py` | 事件排序、工具增量累积、并行工具、取消 |
| `test_mcp_service.py` | 配置缓存、格式错误的配置、`McpSessionPool` 相同任务清理 |
| `test_mcp_adapters.py` | Docker 执行参数、项目根目录检测、卷重复消除 |
| `test_container_service.py` | 容器命名、执行 argv/env、名称冲突、空闲回收 |
| `test_routes.py` | HTTP 路由、错误路径、会话更新白名单 |
| `test_tool_approval.py` | 批准门:请求/解决生命周期、并发批准、取消事件解锁 |
| `test_title_service.py` | 标题工具定义、消息到文本转换、从模型响应中提取标题 |
还应在任何 JavaScript 更改后对前端模块进行 lint:
```bash
find static/js -name '*.js' -print0 | xargs -0 -n1 node --check
```
---
## 贡献
### 工作流程
```bash
git clone https://github.com/yossifibrahem/lumen-ai-chat.git
cd lumen-ai-chat
python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
git checkout -b feature/your-change
```
### 代码库指南
- 保持 Flask 路由处理程序精简 — 业务逻辑位于服务模块中。
- 将工作区路径安全检查保留在 `workspace_service.py` 中;永远不要削弱遍历限制。
- 尽可能将持久数据访问保留在 `store.py` 中。
- 不要在聊天或模型请求正文中发送 API 密钥 — 使用 `app_config.py` 和 `/api/settings`。
- 除非项目有意采用构建步骤,否则避免在前端引入构建步骤。
- 在 `static/js/` 下保持浏览器代码模块化;优先添加工具适配器,而不是在渲染器模块中硬编码工具名称。
- 在更改架构、设置、配置或面向代理的行为时,请更新 `README.md` 和 `devs.md`。
### 拉取请求检查清单
- 所有测试通过(`pytest`)并且前端模块 lint 清洁(`node --check`)。
- 描述更改内容和原因;包括手动测试步骤和结果。
- 将不相关的格式化更改排除在功能性拉取请求之外。
- 确认应用程序在 Docker 运行的情况下仍然可以在本地启动。
- 确认没有 API 密钥、会话数据或本地工作区文件被提交。
### 报告问题
在提交错误报告时,请包括操作系统、Python 版本、浏览器和版本、是否安装并运行 Docker、重现步骤、预期和实际行为,以及任何相关的终端或浏览器控制台输出。
---
## 已知限制
**活动流重新连接是进程本地的。** 取消事件和流重放缓冲区存储在进程内存中(`routes_chat.py`)。这与默认的单工作器 Gunicorn 配置配合良好,但不适用于多个工作器进程。长期解决方案:将流状态移动到 Redis 或共享代理。
**无身份验证。** Lumen 是本地优先的,并且未针对公开暴露进行强化。不要在未经身份验证、速率限制和更严格的 CORS 的情况下公开部署。
**无数据库或迁移。** 会话 JSON 形状必须保持向后兼容,或者必须有意添加迁移代码。
---
## 许可证
MIT — 为喜欢本地控制、可读代码和无需 14 步部署仪式的 AI 工具而构建。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器