ChatUI 是一个轻量、可直接部署的 OpenAI 兼容 Web 工具。它以单页前端 + Node.js 本地代理为核心,支持聊天、流式输出、思考内容展示、文本生图、图片编辑、多附件原生输入、Markdown/数学公式/Mermaid 渲染、会话管理、任务恢复、本地图片缓存、使用统计排行榜和 Docker 镜像发布。
项目定位:用尽量少的依赖快速接入第三方大模型网关、私有 OpenAI 兼容服务、聚合 API 或本地模型代理。
- 功能总览
- 界面与交互
- 快速开始
- Docker 部署
- 模型配置
- 聊天能力
- 思考模式
- 图片生成与图片编辑
- 附件能力
- Markdown、公式与图表
- 会话、本地存储与任务恢复
- 使用统计与排行榜
- 服务端 API 与代理
- 环境变量
- 目录结构
- 开发与验证
- 发布与镜像仓库
- 常见问题
- 安全建议
- License
- OpenAI Chat Completions 兼容接口。
- 支持流式输出和普通非流式兜底。
- 支持聊天任务后台 Job 化,刷新后可恢复未完成输出。
- 支持停止当前输出。
- 支持重新生成助手回复。
- 支持编辑用户消息后重发,并替换对应回复。
- 支持会话级聊天模型覆盖:单个会话可选择不同聊天模型,也可跟随全局模型。
- 支持全局 System Prompt。
- 支持会话级 System Prompt 覆盖。
- 支持回复完成提示音。
- 支持模型返回
output_text、标准choices[].message.content、SSE delta 等多种兼容格式。
- 自动判断当前输入应走:
chat:普通聊天。image:文本生成图片。edit_image:图片编辑。
- 可配置独立路由模型;未配置时使用聊天模型。
- 路由只读取文字上下文、附件元数据和图片引用元数据,不把图片二进制、base64 或附件正文发给路由模型。
- 非图片附件上传时直接走聊天,通过 Responses API 的 Base64 原生文件输入发送,不进入图片路由。
- 多图场景支持图片组、图片序号、图片 ID 和最近图片引用元数据。
- 文本生成图片。
- 上传图片后编辑图片。
- 基于上一张生成图继续修改。
- 支持多图返回展示。
- 支持多图编辑上下文保存。
- 支持选择历史生成图引用,内部使用
imgref_/img_标识。 - 支持图片预览。
- 支持单图下载和全部图片下载。
- 支持图片缩略图稳定尺寸,避免加载过程中布局跳动。
- 支持图片本地 IndexedDB 持久化,刷新后恢复历史图片。
- 支持上游返回图片 URL、
b64_json、image_base64。 - 支持无法直连的上游图片通过
/api/image同源代理下载。
- 支持多附件上传。
- 支持点击上传、粘贴上传。
- 支持上传进度展示。
- 支持图片附件预览。
- 支持图片附件压缩:JPEG / PNG / WebP 会尽量压缩到合适大小。
- 支持 BMP 转 PNG。
- 支持图片附件作为多模态聊天内容或图片编辑输入。
- 支持 OpenAI
input_file文档类型,包括 PDF、文本/代码、Word、PowerPoint 和表格文件。 - 文件编码为 Data URL,并以
input_file.file_data直接放入/v1/responses;不会请求/v1/files。 - PDF 支持
auto/low/high页面图像清晰度;非 PDF 不发送detail。 - 原始文档 Blob 缓存到 IndexedDB,编辑或恢复历史消息时重新编码,不把大段 Base64 写入聊天历史。
- 本地
markdown-it渲染 Markdown。 - 支持标题、列表、任务列表、表格、引用、链接、图片、删除线、代码块等常见 GFM 能力。
- 支持 KaTeX 行内公式和块级公式。
- 支持 Mermaid 图表。
- 支持代码块语言标识。
- 支持代码块右上角复制按钮。
- 支持表格横向滚动包装。
- 支持标题自动锚点。
- 支持部分扩展 Markdown:脚注、引用式链接、mark、高/下标、常用 emoji shortcode。
- 当
markdown-it不可用时有内置 legacy renderer 兜底。
- 多会话列表。
- 新建会话。
- 切换会话。
- 重命名会话。
- 删除会话,删除时会确认。
- 每个会话独立保存消息、展示历史、最近图片、提示词、模型选择、Header UUID。
- 支持会话侧边栏收起。
- 移动端支持会话抽屉。
- 支持每个会话输入草稿保留。
- 支持会话标题自动从首条用户消息生成。
- 支持历史消息顺序规范化和去重,避免恢复时顺序错乱。
- 聊天 Job 和图片 Job 使用内存任务仓库。
- 前端使用 SSE 监听任务更新。
- SSE 断开时支持轮询/重连策略。
- 页面刷新后恢复未完成聊天任务。
- 页面刷新后恢复未完成图片生成/编辑任务。
- 输出过程中显示“正在处理/正在生成/正在修改”与已等待时间。
- 上传图片编辑时显示上传进度。
- 用户滚动离开输出焦点时不强制拉回底部。
- 正在输出离开可视焦点时显示“继续查看输出”按钮。
- 点击“继续查看输出”可回到当前输出位置。
- 新建/切换会话不会残留旧会话的输出焦点。
- 可选 PostgreSQL 使用统计,不配置数据库时自动关闭,不影响聊天、生图和附件功能。
- 支持今日排行、昨日排行、总排行,默认每个范围返回前 10 名。
- 支持通过环境变量调整排行榜返回数量。
- 支持个人使用统计,按当前浏览器配置的 API Key 查询。
- 统计范围支持今日、昨日、总计切换。
- 前端采用懒加载:打开弹窗只查当前范围,切换到哪个范围才查询哪个范围,已查询数据会在前端缓存。
- 后端使用独立 PostgreSQL 连接池,连接串、连接池大小、超时和 SSL 均通过环境变量配置。
- 统计模块与聊天、图片、附件和 OpenAI 代理解耦,独立路由为
/api/usage/*。
- 无前端构建步骤,静态资源直接交付。
- 本地 vendored:
markdown-it、KaTeX、KaTeX 字体、Mermaid。 - Node.js HTTP 服务静态托管前端。
- 服务端代理只允许白名单路径。
- 提供
linux/amd64Docker 镜像。 - 推送语义化版本 Git tag 后触发 GitHub Actions 构建镜像。
- 镜像推送到 Docker Hub 和阿里云 ACR。
- 测试覆盖前端 core/services/ui/app、服务端 API、原生附件输入、路由、任务和冒烟流程。
- 左侧会话栏:会话列表、新建会话、重命名、删除、当前会话条数。
- 收起态会话栏:保留展开、新会话、会话入口、模型配置入口。
- 移动端会话入口:小屏幕下通过浮动按钮打开会话抽屉。
- 消息区:展示用户消息、助手消息、错误消息、图片结果、附件预览。
- 输入区:附件按钮、会话提示词按钮、会话生图样式按钮、会话模型按钮、思考开关、发送/停止按钮。
- 配置弹窗:Endpoint、API Key、模型加载、模型选择、图片尺寸、全局提示词、全局生图样式提示词、Header 参数。
- Enter 发送。
- Shift + Enter 换行。
- 中文输入法组合结束后会重新计算输入框高度。
- 文件可通过附件按钮选择,也可直接粘贴。
- 单条文本消息最多 120,000 个字符;粘贴或输入超限内容时会在写入输入框和触发布局计算之前拒绝,建议改为上传文本文件或分段发送。
- 文件处理过程中发送按钮会禁用或提示等待。
- 输出过程中发送按钮切换为停止按钮;只有点击停止按钮才会中断,普通 Enter 不会误触停止。
- 用户消息支持编辑重发。
- 助手消息支持重新生成。
- 消息支持复制。
- 助手回答支持下载为文本文件。
- 代码块支持单独复制。
- 图片支持预览、下载、分享(浏览器支持 Web Share 文件分享时)。
- 会话 System Prompt:可单独设置当前会话提示词。
- 会话生图样式提示词:可单独设置当前会话图片风格要求。
- 会话聊天模型:可让当前会话使用独立聊天模型,或跟随全局聊天模型。
- 会话级设置保存在本地,仅影响当前浏览器当前会话。
Node.js 20.19+
推荐直接使用与容器和 CI 一致的 Node.js 22 LTS。
git clone https://github.com/MrLiuGangQiang/chatui.git
cd chatuinpm cinpm start等价于:
node server.js默认访问:
http://127.0.0.1:8765
默认监听:
HOST=0.0.0.0
PORT=8765
docker build -t chatui .
docker run --rm -p 8765:8765 chatui访问:
http://127.0.0.1:8765
| 仓库 | 镜像地址 | 推荐用途 |
|---|---|---|
| Docker Hub | liugangqiang/chatui |
海外服务器、Docker Hub 默认环境 |
| 阿里云 ACR | registry.cn-hangzhou.aliyuncs.com/liugangqiang/chatui |
国内服务器、阿里云或国内网络环境 |
常用标签:
| 标签 | 说明 |
|---|---|
latest |
最新正式 Release 镜像 |
MAJOR.MINOR.PATCH |
与 GitHub Release 对应的版本号,例如 1.1.76 |
GitHub Release tag 使用
vMAJOR.MINOR.PATCH,镜像标签使用去掉v的MAJOR.MINOR.PATCH。例如 Releasev1.1.76对应镜像liugangqiang/chatui:1.1.76。
docker pull liugangqiang/chatui:latest
docker run -d \
--name chatui \
--restart unless-stopped \
-p 8765:8765 \
liugangqiang/chatui:latest指定版本:
docker pull liugangqiang/chatui:1.1.76
docker run -d \
--name chatui \
--restart unless-stopped \
-p 8765:8765 \
liugangqiang/chatui:1.1.76docker pull registry.cn-hangzhou.aliyuncs.com/liugangqiang/chatui:latest
docker run -d \
--name chatui \
--restart unless-stopped \
-p 8765:8765 \
registry.cn-hangzhou.aliyuncs.com/liugangqiang/chatui:latest指定版本:
docker pull registry.cn-hangzhou.aliyuncs.com/liugangqiang/chatui:1.1.76
docker run -d \
--name chatui \
--restart unless-stopped \
-p 8765:8765 \
registry.cn-hangzhou.aliyuncs.com/liugangqiang/chatui:1.1.76docker pull registry.cn-hangzhou.aliyuncs.com/liugangqiang/chatui:latest
docker stop chatui || true
docker rm chatui || true
docker run -d \
--name chatui \
--restart unless-stopped \
-p 8765:8765 \
registry.cn-hangzhou.aliyuncs.com/liugangqiang/chatui:latest如果需要固定版本,把 latest 换成明确版本号,例如 1.1.76。
打开页面后点击“模型配置”。
| 配置项 | 说明 |
|---|---|
| Endpoint Base URL | OpenAI 兼容接口地址;默认 https://ingress.lfans.cn/v1,也可改成自己的服务,例如 https://api.openai.com/v1 |
| API Key | 接口密钥,保存在浏览器本地 |
| 聊天模型 | 用于聊天、路由判断和文本回复 |
| 路由模型 | 用于判断聊天/生图/修图;为空时使用聊天模型 |
| 生图模型 | 用于图片生成或图片编辑 |
| 图片尺寸 | 生图尺寸,默认 auto |
| System Prompt | 全局聊天系统提示词 |
| 图片样式提示词 | 全局生图/修图风格要求,会附加到图片 prompt |
Endpoint 示例:
https://ingress.lfans.cn/v1
https://api.openai.com/v1
https://your-gateway.example.com/v1
http://127.0.0.1:8000/v1
不要写到具体接口路径,例如不要写成:
https://api.example.com/v1/chat/completions
应写成:
https://api.example.com/v1
点击“加载模型”后,ChatUI 会通过本地代理请求:
GET /models
推荐上游返回:
{
"data": [
{ "id": "gpt-4.1", "type": "chat" },
{ "id": "gpt-image-1", "type": "image_generation" }
]
}也支持数组:
[
{ "id": "chat-model", "type": "chat" },
{ "id": "image-model", "type": "image" }
]聊天模型关键词:
chattextllmlanguagecompletionreasonassistantgptclaudegeminiqwendeepseekllamamistral
生图模型关键词:
imageimage_generationimage-generationimagegenerationvisionpictureimgdallgpt-imagefluxsdstablemidjourneywankling
如果模型没有 type 字段,或 type 为空:
- 名称包含已知聊天、图片或 embedding 关键词时,会按名称推断类型,并显示
按名称识别。 - 名称也无法识别时保留为未知类型,同时进入聊天和生图下拉。
- 未知模型后显示红色
未知类型标记。 - 加载状态会显示未知类型数量,例如
已加载 12 个,3 个未知类型。
如果网关要求额外 Header,可在“参数配置”中添加多条 Header。
Header 值模式:
| 模式 | 说明 |
|---|---|
| 手动值 | 固定 Header 值 |
| 会话级短 UUID | 每个会话生成一次,同一会话内所有请求复用 |
| 消息级短 UUID | 每次发送、刷新或重新生成时生成新值 |
适用场景:请求追踪、租户标识、网关鉴权、链路调试。
聊天请求最终调用:
POST /chat/completions
前端会通过本地代理发送,避免浏览器跨域和直连鉴权问题。
- 默认使用流式输出。
- 支持标准 SSE
data: ...。 - 支持
[DONE]结束标记。 - Supports parsing OpenAI reasoning deltas (
reasoning_content,reasoning) and Responses API reasoning summary events. - 如果流式失败,会尝试普通非流式请求兜底。
- Reasoning requests are never silently downgraded: the selected effort is sent unchanged and upstream errors are returned as-is.
- 助手消息可重新生成。
- 用户消息可编辑后重发。
- 编辑重发会尽量复用原消息位置,并替换对应助手回复。
- 历史恢复时会按
messageIndex/responseIndex规范排序,相同索引固定system → user → assistant。
- 输出中点击发送按钮会执行停止。
- 停止会 abort 当前 run 关联的聊天/图片 Job。
- 如果已有有效内容,会保留已有输出。
- 如果只有占位内容,会显示“用户停止”。
Thinking mode is limited to OpenAI GPT-5 models. When enabled, requests only use the OpenAI reasoning_effort parameter, and returned reasoning is displayed above the final answer.
| UI value / request value | Meaning |
|---|---|
low |
Low reasoning effort |
medium |
Medium reasoning effort |
high |
High reasoning effort |
xhigh |
Extra-high reasoning effort |
max |
Maximum reasoning effort |
none is the internal disabled state and is not shown as a selectable menu item. The legacy minimal value is treated as disabled and is never sent upstream.
- Reasoning is disabled by default.
- The brain icon toggles reasoning mode.
- The effort menu is disabled while reasoning mode is off.
- The toggle and effort selector are locked while a response is streaming.
- No Claude, Google, Qwen, or generic-provider reasoning compatibility parameters are sent.
- The selected effort is never silently downgraded after an upstream error.
在自动模式下,输入明确生图需求会自动走生图流程。
也可手动切换到生图模式。
示例:
生成一张赛博朋克城市夜景,16:9,霓虹灯风格
上传图片后输入修改需求:
把这张图改成赛博朋克风格
系统会调用图片编辑接口。
已有生成图后,可继续输入:
基于上一张图,把背景换成雪山
系统会从 IndexedDB 恢复上一张图作为编辑输入。
- 图片结果可包含多张图。
- 最近生成图会保存为图片组。
- 多图默认按整组参与后续编辑。
- 用户明确“第一张/第二张/左边/右边/全部”时,路由阶段会尝试识别选择范围。
- 图片引用使用:
imgref_...:图片组引用。img_...:单图引用。
- 部分编辑场景会将选中的新结果合并回原图片组上下文。
当前配置中支持:
auto
1024x1024
1024x1536
1536x1024
最终是否支持取决于上游图片模型。
- 点击图片可预览大图。
- 单图下载。
- 全部图片下载。
- 支持浏览器原生分享时可分享图片文件。
- 图片缩略图记录原始尺寸和缩略图尺寸,刷新恢复时保持稳定布局。
- 点击附件按钮选择文件。
- 粘贴文件到输入区。
- 多文件同时上传。
支持识别:
png, jpg, jpeg, gif, webp, bmp, svg
处理能力:
- 图片预览。
- 图片压缩。
- BMP 转 PNG。
- 图片作为聊天多模态内容。
- 图片作为图片编辑输入。
- 图片缓存到 IndexedDB,避免大 base64 长期写入 localStorage。
常见文本/代码文件会编码为 Base64 Data URL,并作为 Responses API 的 input_file.file_data 直接发送。客户端不会另外提取并重复发送文档正文。
PDF 通过 OpenAI 原生文件输入处理。支持在附件标签中选择 auto、low 或 high;该参数只影响 PDF 页面图像处理,PDF 文本仍会被提取。包含页面图像的 PDF 理解需要支持视觉输入的模型。
原生文件输入支持:
| 类型 | 常见扩展名 | 上游处理方式 |
|---|---|---|
| Word / 富文档 | .doc, .docx, .rtf, .odt |
提取文本 |
| PowerPoint / 演示文稿 | .ppt, .pptx, .pps 等 |
提取文本 |
| Excel / 表格 | .xls, .xlsx, .csv, .tsv, .iif 等 |
表格增强;每个 Sheet 最多处理前 1,000 行 |
非 PDF 文件中的嵌入图片和图表不会进入模型视觉上下文;需要保留图表或排版时,请先转换为 PDF。
每个文档必须严格小于 10 MB;同一条 Responses 请求中的全部文档合计也必须严格小于 10 MB。超限文件会在输入区以红色错误提示保留,不会被静默移除。 Base64 编码会使 HTTP JSON 请求体增大约三分之一;使用中转站或反向代理时,应将请求体上限配置为至少 72 MiB。
- 路由模型只看附件元数据,不读取附件正文。
- 聊天模型通过 Responses API 的
input_file.file_data读取原始文档。 - 图片编辑接口只接收图片附件。
- Base64 只在发送和未完成任务恢复时生成;聊天历史保留 IndexedDB Blob 引用,不持久化完整 Data URL。
- native 文档不会同时以内联文本重复发送。
项目将 Markdown、公式和图表资源放在本地:
vendor/markdown-it.min.js
vendor/katex.min.js
vendor/katex.min.css
vendor/fonts/*
vendor/mermaid.min.js
部署时必须包含 vendor/,否则 Markdown、公式或 Mermaid 可能无法渲染。
# 标题
> 引用内容
- [x] 任务列表
- 普通列表
| A | B |
|---|---|
| **粗体** | $a^2+b^2=c^2$ |
```js
console.log('hello')
```行内公式:
$a^2 + b^2 = c^2$块级公式:
$$
E = mc^2
$$也支持:
\( inline math \)
\[ block math \]使用 mermaid 代码块:
```mermaid
flowchart TD
A[输入] --> B{路由}
B --> C[聊天]
B --> D[生图]
B --> E[修图]
```ChatUI 不需要数据库,主要使用浏览器本地存储。
| 数据 | 存储位置 |
|---|---|
| 接口配置 | localStorage |
| API Key | localStorage |
| 会话元信息 | localStorage |
| 聊天规范消息 | localStorage |
| 展示历史 display | localStorage |
| 最近生成图片上下文 | localStorage + IndexedDB |
| 上传文档与上传/生成图片二进制 | IndexedDB |
| 未完成 Job 记录 | localStorage + IndexedDB(大媒体 payload) |
messages保存规范聊天历史。display保存富媒体展示历史。- 恢复时会结合两者修复历史展示。
- 图片不直接写入 localStorage,而是保存
indexeddb://...引用。 - 清空浏览器站点数据会删除配置、历史和图片缓存。
- 聊天任务记录保存为
CHAT_JOB_KEY:<sessionId>。 - 图片任务记录保存为
IMAGE_JOB_KEY:<sessionId>。 - 页面刷新或切换回来时,会尝试恢复未完成任务。
- 如果服务端任务已过期或服务重启导致任务不存在,会显示明确错误并清理过期 pending 状态。
使用统计是可选能力,依赖外部 PostgreSQL 数据库中的使用日志表。未配置数据库连接时,前端统计入口仍可打开,但接口会返回不可用状态,核心聊天、生图、图片编辑和附件输入不受影响。
- 右上角使用统计按钮,点击打开独立统计弹窗。
- 个人统计默认展示今日,并支持今日、昨日、总计切换。
- 排行榜支持今日排行、昨日排行、总排行。
- 部门统计需要服务端配置访问密码后启用,点击右上角刷新按钮左侧的“部门”切换按钮进入;首次进入需输入密码,校验通过后会像 API Key 一样保存到浏览器本地。
- 部门统计支持今日排行、昨日排行、本月排行、上月排行、总排行。
- 部门统计不受排行榜数量限制,会展示所有部门。
- 部门排行可点击部门下钻查看该部门所有成员使用统计。
- 部门统计支持导出标准
.xlsx,第一个 Sheet 为部门排行,后续每个 Sheet 为对应部门人员使用统计;导出包含序号、开始时间、结束时间和各 token 指标。 - 排行榜默认展示前 10 名,可通过环境变量调整。
- 前三名使用金、银、铜视觉样式,但显示文本仍为
1 / 2 / 3。 - 指标包括:总用量、输入、输出、缓存输入、推理输出。
- 百万以上使用
M,亿以上使用B,鼠标悬停可查看完整数值。
- 前端采用懒加载,打开弹窗只查询当前展示范围。
- 切换排行榜 Tab 时,只查询目标范围排行榜。
- 切换个人统计范围时,只查询目标范围个人统计。
- 已加载过的数据会在当前页面生命周期内缓存,重复切换不重复查询。
- 点击刷新按钮只刷新当前展示的个人统计范围和当前排行榜范围。
- 后端使用
pg.Pool连接池复用数据库连接。 - 连接串、分散连接参数、连接池最小/最大连接数、空闲超时、连接超时和 SSL 均通过环境变量配置。
- 本地
npm start会自动读取仓库根目录中被 Git 忽略的.env.local;文件只填补当前进程未设置的变量,不覆盖部署平台已经注入的环境变量。 - 推荐生产环境使用单变量连接串,例如:
POSTGRES_URL='postgres://user:password@postgres-host:5432/database?sslmode=disable'请不要在仓库、镜像或文档中写入真实数据库账号、密码、主机或连接串。
本地开发可在 .env.local 中使用分散参数,避免把凭据写进启动命令或受版本控制文件:
PGHOST=postgres-host
PGPORT=5432
PGDATABASE=database
PGUSER=user
PGPASSWORD=password| API | 方法 | 说明 |
|---|---|---|
/api/version |
GET | 返回当前应用版本,来自根目录唯一版本源 version.json |
/api/image |
POST | 同源图片代理下载,用于上游图片 URL 无法直接加载时 |
/api/chat-stream-jobs |
POST | 注册/启动聊天流式 Job |
/api/usage/overview |
POST | 一次查询排行榜与个人统计,body 包含 api_key、model 和范围 |
/api/usage/rankings |
POST | 查询指定范围排行榜,body 包含 api_key、model 与 range |
/api/usage/personal |
POST | 查询指定范围个人统计,body 包含 api_key、model 与 range |
/api/usage/department/verify |
POST | 校验部门统计访问,body 包含 api_key、model 与 password |
/api/usage/department/summary |
POST | 查询部门汇总,body 包含访问字段与 range |
/api/usage/department/rankings |
POST | 查询部门排行,body 包含访问字段与 range |
/api/usage/department/users |
POST | 查询部门人员统计,body 另含 department_id |
/api/usage/department/export |
POST | 导出部门统计标准 .xlsx |
/api/usage/feedback |
POST | 审核并提交问题反馈,body 包含 api_key、model、可选 route_model 与 content;内容必须包含问题描述、复现描述和期望结果 |
使用统计范围统一支持 today、yesterday、week、last_week、month、last_month、total。统计与反馈入口会先通过当前 API Key 和聊天模型向上游执行访问校验;问题反馈还会由该聊天模型审核内容完整性,只有审核通过才会发送;部门接口还需要部门密码。
| API | 方法 | 说明 |
|---|---|---|
/api/chat-jobs |
POST | 创建聊天 Job |
/api/chat-jobs/:id |
GET | 查询聊天 Job |
/api/chat-jobs/:id/events |
GET | 订阅聊天 Job SSE |
/api/chat-jobs/:id/abort |
POST | 中止聊天 Job |
/api/image-jobs |
POST | 创建图片生成/编辑 Job |
/api/image-jobs/:id |
GET | 查询图片 Job |
/api/image-jobs/:id/events |
GET | 订阅图片 Job SSE |
/api/image-jobs/:id/abort |
POST | 中止图片 Job |
所有 /api/* 且不属于内部 API 的请求会走代理白名单。
允许路径:
/models
/chat/completions
/responses
/images/generations
/images/edits
/openai/image_edit
/openai/image_edit 是本地代理的兼容别名,服务端会将它规范化为上游 /images/edits。托管图片 Job 直接使用 /images/edits。
允许方法:
GET, POST
代理会处理:
baseUrl规范化。apiKey注入 Authorization。- 自定义 Header 透传。
- 上游超时。
- SSE 转发。
- 图片上游路径规则:纯文本生图走
/images/generationsJSON;图片编辑/参考图生成走/images/editsmultipart。前端/本地缓存里的 base64 会在服务端转成文件 Blob,按image[]数组字段上传;多图会重复追加多个image[]字段。 - 流式聊天 Job 同步更新。
- 错误响应标准化。
- 默认
/返回index.html。 - 支持
GET/HEAD。 - 防止路径穿越。
- JS / CSS / JSON / 图片 / 字体 MIME 类型显式设置。
- JS / CSS / HTML 使用
no-cache。 - 其他静态资源默认
public, max-age=3600。 - 如果同目录存在更新的
.br或.gz,会按Accept-Encoding返回预压缩版本。
直接运行 npm start 时,服务端会先读取根目录 .env.local,并且只应用尚未存在于进程环境中的变量。该文件已由 .gitignore 排除,仅供本机开发使用;Docker、CI 和生产部署仍应通过运行环境注入配置。
| 变量 | 默认值 | 说明 |
|---|---|---|
HOST |
0.0.0.0 |
HTTP 监听地址 |
PORT |
8765 |
HTTP 监听端口 |
UPSTREAM_TIMEOUT_MS |
600000 |
上游 API 超时,默认 10 分钟 |
CHATUI_UPSTREAM_PROXY |
not set |
HTTP/HTTPS outbound proxy for public Endpoint requests from the container; takes precedence over HTTPS_PROXY / HTTP_PROXY, for example http://host.docker.internal:7890. Private upstreams bypass this proxy. |
HTTPS_PROXY / HTTP_PROXY |
not set |
Fallback outbound proxy settings when CHATUI_UPSTREAM_PROXY is empty. On a Linux Docker host, do not use 127.0.0.1 unless the proxy runs inside this container; use a container-reachable host or gateway address. |
CHATUI_VERBOSE_LOGS |
not set |
Set to 1 to emit redacted upstream diagnostics; API keys and image/file Base64 payloads are never logged. |
CHATUI_CONTEXT_WINDOW_TOKENS |
262144 |
聊天请求上下文窗口预算,约 256k estimated tokens;超出时会裁剪较早历史并插入自动上下文摘要/摘录,只影响发给模型的 payload,不删除本地会话记录 |
CHATUI_ALLOW_PRIVATE_UPSTREAM |
未设置 | 默认禁止代理访问私有/内网地址;仅在明确需要访问受信任内网模型网关时设为 1,兼容别名为 ALLOW_PRIVATE_UPSTREAM |
JOB_TTL_MS |
3600000 |
JobStore 任务保留时长,默认 1 小时 |
MAX_JOBS_PER_STORE |
200 |
每类任务最多保留数量 |
NODE_ENV |
Docker 中为 production |
Node 运行环境 |
POSTGRES_URL |
未设置 | PostgreSQL 单变量连接串,推荐生产部署使用,例如 postgres://user:password@host:5432/database?sslmode=disable |
POSTGRESQL_URL |
未设置 | PostgreSQL 连接串别名 |
PG_DATABASE_URL |
未设置 | PostgreSQL 连接串别名 |
DATABASE_URL |
未设置 | 通用数据库连接串别名 |
PGHOST / POSTGRES_HOST |
未设置 | PostgreSQL 主机;未使用连接串时生效 |
PGPORT / POSTGRES_PORT |
5432 |
PostgreSQL 端口;未使用连接串时生效 |
PGDATABASE / POSTGRES_DATABASE |
未设置 | PostgreSQL 数据库名;未使用连接串时生效 |
PGUSER / POSTGRES_USER |
未设置 | PostgreSQL 用户名;未使用连接串时生效 |
PGPASSWORD / POSTGRES_PASSWORD |
未设置 | PostgreSQL 密码;未使用连接串时生效 |
PG_POOL_MIN / POSTGRES_POOL_MIN |
0 |
PostgreSQL 连接池最小连接数 |
PG_POOL_MAX / POSTGRES_POOL_MAX |
10 |
PostgreSQL 连接池最大连接数 |
PG_IDLE_TIMEOUT_MS / POSTGRES_IDLE_TIMEOUT_MS |
30000 |
PostgreSQL 连接池空闲连接回收时间 |
PG_CONNECTION_TIMEOUT_MS / POSTGRES_CONNECTION_TIMEOUT_MS |
5000 |
PostgreSQL 建连超时时间 |
PGSSL / POSTGRES_SSL |
未设置 | PostgreSQL SSL 开关;可设为 true / require / false |
USAGE_RANKING_LIMIT |
10 |
使用排行榜每个范围返回数量,非法值回退到 10,最大 100 |
USAGE_STATS_RANKING_LIMIT |
未设置 | 排行榜数量兼容别名 |
USAGE_DEPARTMENT_PASSWORD |
not set |
Password for department statistics; disabled when unset. |
USAGE_STATS_DEPARTMENT_PASSWORD |
not set |
Compatible alias for the department statistics password. |
Docker proxy example (the proxy URL must be reachable from inside the container):
docker run -d --name chatui --restart unless-stopped -p 8765:8765 \
-e CHATUI_UPSTREAM_PROXY=http://host.docker.internal:7890 \
-e CHATUI_VERBOSE_LOGS=1 \
liugangqiang/chatui:latestIf text requests work but image/file chat fails, run docker logs --tail 200 chatui after one failed upload. The log records only target host/path, outbound byte size and the underlying network code (such as ECONNRESET); it does not include credentials or Base64 data.
示例:
HOST=127.0.0.1 PORT=3000 UPSTREAM_TIMEOUT_MS=900000 CHATUI_CONTEXT_WINDOW_TOKENS=524288 node server.js使用统计示例:
POSTGRES_URL='postgres://user:password@postgres-host:5432/database?sslmode=disable' \
PG_POOL_MIN=0 \
PG_POOL_MAX=10 \
USAGE_RANKING_LIMIT=10 \
USAGE_DEPARTMENT_PASSWORD='请替换为强密码' \
node server.js默认安全策略会阻止私有地址上游。只有确实需要访问受信任的内网模型网关时才显式开启:
CHATUI_ALLOW_PRIVATE_UPSTREAM=1 node server.js.
├── app.js # 浏览器端主业务编排入口
├── index.html # 页面结构、模板、配置弹窗、消息模板
├── pages/ # 弹窗按需加载的独立说明页面
│ ├── route.html # 智能任务路由流程图
│ └── files.html # 支持的文件格式与上传约束
├── styles.css # 全局样式、响应式布局、消息/图片/配置面板样式
├── styles/ # 按功能拆分的补充样式
├── server.js # Node HTTP 启动入口
├── config/ # 可公开的运行时配置,不得存放密钥
├── client/ # 前端拆分模块
│ ├── core/ # 纯逻辑:附件、消息、模型、reasoning、图片引用、路由上下文、存储
│ ├── services/ # 请求与 payload:模型、聊天、路由、生图、图片、Job、使用统计
│ ├── ui/ # UI 辅助:消息渲染、图片操作、滚动、实时渲染、文件动作、统计弹窗
│ └── app/ # 应用状态:会话、持久化、运行态、图片缓存、display items
├── server/ # 服务端模块
│ ├── app.js # 服务装配:JobStore、代理、路由、静态服务
│ ├── config/ # 端口、根目录、上游超时、代理 allowlist、版本
│ ├── api/ # HTTP 路由分发
│ ├── db/ # 可选数据库连接池,例如 PostgreSQL
│ ├── usage/ # 使用统计查询仓库
│ ├── http/ # 请求 body、响应、安全头、静态文件服务
│ ├── proxy/ # OpenAI 兼容代理、图片代理、Header 规范化
│ ├── security/ # 上游 URL 安全策略
│ ├── services/ # 服务端用例与外部集成
│ ├── validators/ # API 输入校验
│ ├── logging/ # 安全日志与脱敏
│ ├── errors/ # 统一应用错误
│ └── jobs/ # 聊天任务、图片任务、SSE、abort、内存任务仓库、reasoning
├── shared/ # 浏览器和服务端都可安全使用的共享逻辑
├── test/ # 自动化测试
│ ├── unit/ # 前后端单元与契约测试
│ ├── smoke/ # HTTP 服务冒烟测试
│ └── run-tests.js # 全量测试入口
├── vendor/ # 本地第三方前端资源
│ ├── markdown-it.min.js
│ ├── katex.min.js
│ ├── katex.min.css
│ ├── mermaid.min.js
│ └── fonts/ # KaTeX 字体
├── Dockerfile # Docker 镜像定义
├── .dockerignore # Docker 构建忽略文件
├── .github/workflows/release.yml # Tag 后优先发布阿里云 ACR,再自动同步 Docker Hub
├── CONTRIBUTING.md # 开发规范、目录边界和治理约束
├── package.json
├── package-lock.json
├── version.json
└── README.md
npm test等价于:
node test/run-tests.js提交前完整检查:
npm run check
git diff --check当前测试覆盖:
server.js、app.js、前端模块、服务端测试文件语法检查。- 前端 core:消息、模型、附件、图片引用、路由上下文、reasoning、storage。
- 前端 services:模型、Job、聊天、路由、生图、图片解析。
- 前端 UI:文件动作、实时渲染、滚动、消息渲染、消息操作、图片操作。
- 前端 app:状态、run、会话、持久化、display item、runtime、image store。
- API、Job 生命周期、原生附件输入、路由与 HTTP 服务冒烟。
当前测试以 Node/JSDOM 和 HTTP smoke 为主;真实浏览器 E2E 作为后续增强项。
npm run check:syntax
node test/run-tests.js
node test/run-tests.js unit/server-hardening.test.js
node test/run-tests.js smoke/server-smoke.test.js
node test/run-tests.js --list
node test/run-tests.js unit/usage --timeout=20000测试文件导出测试函数数组,不能直接执行单个 *.test.js 文件;聚焦运行必须通过 test/run-tests.js,否则可能以退出码 0 结束但实际执行 0 项。
node server.js
curl -fsS http://127.0.0.1:8765/api/version
curl -fsS http://127.0.0.1:8765/ >/dev/nullcurl -I http://127.0.0.1:8765/vendor/markdown-it.min.js
curl -I http://127.0.0.1:8765/vendor/katex.min.js
curl -I http://127.0.0.1:8765/vendor/katex.min.css
curl -I http://127.0.0.1:8765/vendor/mermaid.min.js期望:
- JS 返回
Content-Type: application/javascript。 - CSS 返回
Content-Type: text/css。 - 状态码为
200。
项目在 pull request 和 main 推送时运行日常 CI,包括 Node.js 20.19、Node.js 22 的 npm run check,以及 Exact Docker runtime 容器验证。推送 vMAJOR.MINOR.PATCH 格式的正式 Release Git tag 会触发独立的 linux/amd64 发布流程:先在阿里云 ACR 构建、验证和提升镜像,再创建 GitHub Release,最后自动同步同一已验证 digest 到 Docker Hub。
Docker Hub: liugangqiang/chatui
阿里云 ACR: registry.cn-hangzhou.aliyuncs.com/liugangqiang/chatui
除非明确迁移仓库,否则不要在发版时临时改镜像地址。
- 从当前
origin/main准备一个干净候选,运行npm run release:prepare。该命令按a.b.c规则自动递增根目录version.json(c=99的下一版才进位到b+1.0),同步package.json、package-lock.json镜像字段并创建同版本docs/releases/vMAJOR.MINOR.PATCH.md。 - 运行
npm run check;本机有 Docker 时再运行npm run preview:release。 - 将候选提交推送到
main,等待 Node 检查和Exact Docker runtime全部通过。 - 在该已验证提交上创建并推送 annotated
vMAJOR.MINOR.PATCHtag。 - workflow 构建带提交 SHA 和 runtime source fingerprint 的 ACR 候选镜像,以 digest 启动验证,再把同一 digest 提升为版本、
v前缀和latest标签;验证与提升之间不得重建。 - 确认 ACR 和 Docker Hub 标签均解析到已验证 digest,容器
/api/version的版本、Git SHA、source fingerprint 一致。 - 从该版本 Release Notes 创建或确认非 draft GitHub Release;只有镜像 workflow、两个仓库的标签验证与 GitHub Release 都成功后才算发布完成。
| 标签 | 示例 | 说明 |
|---|---|---|
latest |
liugangqiang/chatui:latest |
最新正式版本 |
MAJOR.MINOR.PATCH |
liugangqiang/chatui:1.2.3 |
精确版本标签 |
vMAJOR.MINOR.PATCH |
liugangqiang/chatui:v1.2.3 |
与 Git tag 一致的精确版本标签 |
正式 Release Notes 必须使用正确版本标题并包含实质性的用户说明;可按新增、修改、修复、删除等实际内容分类,不要求制造空章节。Release Notes 不能只复制 commit message。
npm run check
git diff --check本机可用 Docker 时还应运行 npm run preview:release;否则必须等待推送提交的 Exact Docker runtime 成功后才能打 tag。
如果涉及 Docker 镜像内容,确认 Dockerfile 包含必要目录:
COPY client ./client
COPY server ./server
COPY vendor ./vendor说明部署产物缺少 vendor/ 目录。
处理:
- 确认
vendor/markdown-it.min.js存在。 - 确认
vendor/katex.min.js存在。 - 确认
vendor/katex.min.css存在。 - 确认
vendor/mermaid.min.js存在。 - 确认
vendor/fonts/下的 KaTeX 字体存在。 - 重新构建并部署。
通常是 JS/CSS 文件请求返回了 404 HTML 或空内容。
处理:
curl -I http://your-host/vendor/markdown-it.min.js
curl -I http://your-host/vendor/katex.min.css确认状态码和 Content-Type 正确。
检查 /models 返回中的 type 字段。
推荐:
{ "id": "your-chat-model", "type": "chat" }
{ "id": "your-image-model", "type": "image_generation" }如果没有 type,ChatUI 会先根据模型名称推断聊天、图片或 embedding 类型,并显示 按名称识别;名称也无法识别时才标记为 未知类型,并同时出现在聊天和生图下拉中。
检查:
- Endpoint 是否正确。
- 生图模型是否选择正确。
- 模型是否支持 OpenAI 兼容图片接口。
- 图片尺寸是否被该模型支持。
- API Key 是否有生图权限。
- 上游是否支持
/images/generations和/images/edits:纯文本生图走/images/generationsJSON;图片编辑/参考图生成走/images/editsmultipart,服务端会把 base64 输入转成image[]文件数组字段。本地兼容代理仍接受/openai/image_edit并转换为/images/edits。
可能是上游图片 URL 需要鉴权或跨域不可直接访问。ChatUI 会尝试通过 /api/image 同源代理下载,但要求图片 URL 与 Endpoint Base URL 同源。
可能原因:
- 上游接口不支持 streaming。
- 代理或网关没有正确转发 SSE。
- 模型服务返回非标准流式格式。
- 浏览器或网络环境中断 SSE。
ChatUI 会尽量降级为普通请求。
PDF 由上游 Responses API 处理。扫描件、图片型 PDF 需要模型支持视觉输入,并且中转站必须完整支持 input_file.file_data。
建议:
- 确认聊天模型支持 PDF/视觉文件输入。
- 确认中转站实现
/v1/responses中的input_file.file_data,且允许 Base64 请求体通过。 - 将扫描件先做 OCR,或导出为可检索 PDF、文本/Markdown 后再上传。
不会。清空对话只删除当前会话聊天、展示历史和图片上下文,不删除模型配置和 API Key。
会删除该会话引用到的 IndexedDB 图片,并尝试清理孤儿图片缓存。
可能原因:
- 服务端内存 Job 已过期。
- 服务端重启后内存 Job 丢失。
- 浏览器本地 Job 记录被清理。
ChatUI 会显示“任务不存在或服务已重启”等错误,并清理过期 pending 状态。
- 不要把真实 API Key 写入仓库。
- 不要在公共设备上长期保存 API Key。
- 生产环境建议通过 HTTPS 访问。
- 如果使用反向代理,请限制管理入口访问范围。
- 如果接入私有模型网关,请做好鉴权和访问控制。
- 服务端默认阻止代理访问私有地址段以降低 SSRF 风险;不要在公开部署中设置
CHATUI_ALLOW_PRIVATE_UPSTREAM=1。 - 服务端代理只允许
/models、/chat/completions、/responses、/images/generations、/images/edits和兼容别名/openai/image_edit。 - 浏览器尝试加载上游返回的公开图片 URL 时不会附带 API Key;需要鉴权的图片统一回退到
/api/image,由服务端校验图片 URL 与 Endpoint 同源后再请求。 - 当前 Job 存储是单实例内存实现,Job 查询、SSE、中止和删除接口尚未绑定用户身份。面向不可信多用户公开部署时,必须在反向代理或应用层增加认证与会话所有权校验,不能把 Job ID 当作授权凭据。
- 后台任务默认使用内存存储;可通过
JOB_TTL_MS和MAX_JOBS_PER_STORE控制完成任务保留时间和单类任务上限。 vendor/是前端公开资源,不要放任何密钥。- API Key 保存在当前浏览器 localStorage;清空站点数据会删除配置与历史。
按仓库实际 License 为准。