Chandra开源大模型实战:Ollama WebUI源码二次开发,增加历史会话导出功能
1. 为什么需要给Chandra加个“导出键”
你有没有过这样的经历:和AI聊了半小时,从写周报到改简历再到生成会议纪要,内容越积越多,结果一刷新页面,所有对话全没了?或者想把某次特别精彩的问答整理成文档发给同事,却只能靠截图、复制粘贴,手忙脚乱还容易漏行?
Chandra镜像确实很轻快——本地跑gemma:2b,不联网、不传数据、秒响应,但它的WebUI界面太“干净”了,干净到连个“保存聊天记录”的按钮都没有。这不是设计缺陷,而是Ollama官方WebUI的默认取舍:聚焦实时交互,弱化历史管理。
可真实工作场景里,我们不是在做Demo,而是在用AI干活。一次高质量对话就是一份可复用的知识资产。导出历史会话,不是锦上添花的功能,而是让Chandra从“玩具级聊天框”升级为“个人AI工作台”的关键一步。
本文不讲高深理论,也不堆砌配置参数。我会带你从零开始,真正动手修改Chandra所依赖的Ollama WebUI源码,一行一行加功能、测效果、打包部署。整个过程不需要你懂React全家桶,只要你会看HTML结构、能写基础JavaScript、会运行npm命令——就像修自家书桌抽屉,拧几颗螺丝,换块木板,完事就能用。
你将获得:
- 一个带“导出当前会话”按钮的真实可用WebUI
- 完整可复用的二次开发流程(适配任何Ollama WebUI版本)
- 导出文件为标准JSON格式,兼容Obsidian、Notion、Typora等主流工具
- 零外部依赖,所有代码都在前端完成,不碰后端API
前置知识只要一条:你知道git clone是下载代码,npm run dev是启动本地服务。其余,咱们边做边聊。
2. 拆解Chandra的WebUI结构:找到那个“能改的地方”
Chandra镜像的前端,本质是Ollama官方维护的Ollama WebUI项目的一个定制分支。它不是黑盒,而是一套清晰的前端工程:Vite构建、React编写、Tailwind CSS样式。我们要加功能,就得先看清它的骨架。
2.1 快速定位核心文件
启动Chandra镜像后,访问http://localhost:3000打开界面。右键→“查看页面源代码”,你会发现所有资源都来自/static/路径。这说明前端是静态打包产物。但源码在哪?答案就在镜像的构建逻辑里。
进入Ollama WebUI官方仓库,主目录下有三个关键区域:
src/App.tsx:整个应用的根组件,负责路由和全局状态src/components/ChatWindow.tsx:聊天窗口主体,包含消息列表、输入框、发送按钮src/components/ChatHistory.tsx:左侧历史会话列表,管理会话ID、标题、时间戳
我们要加“导出”功能,最自然的位置是当前会话的上下文操作区——也就是每条聊天记录右上角那个“⋯”菜单。但Chandra默认没这个菜单。所以第一步,得先给它“长”出来。
2.2 分析现有会话管理逻辑
打开src/components/ChatWindow.tsx,搜索关键词useEffect或messages,很快能找到消息加载的核心逻辑:
// src/components/ChatWindow.tsx 约第85行 const loadMessages = useCallback(async () => { if (!currentChatId) return; const res = await fetch(`/api/chat/${currentChatId}/messages`); const data = await res.json(); setMessages(data); }, [currentChatId]);看到没?它通过/api/chat/{id}/messages这个API拉取当前会话全部消息。这个接口返回的就是标准JSON数组,结构类似:
[ { "role": "user", "content": "你好" }, { "role": "assistant", "content": "你好!我是Chandra。" } ]这意味着:导出功能完全可以在前端实现——我们不需要改后端,只要拿到这个数组,用JSON.stringify()格式化,再触发浏览器下载即可。安全、简单、零侵入。
2.3 设计导出按钮的落点
现在问题变成:按钮放哪?有三个合理选项:
- 顶部工具栏:在聊天窗口右上角,和“新建会话”“删除会话”并列
- 消息列表底部:在最后一条消息下方,加一个醒目按钮
- 右键菜单:长按某条消息弹出“导出本条”或“导出全部”
对普通用户最友好、开发成本最低的是第一种。Chandra的顶部栏已有New Chat和Delete Chat按钮,我们加一个Export,风格统一,位置直观,符合用户心智模型。
打开src/components/ChatWindow.tsx,找到渲染顶部栏的代码块(通常在return语句开头附近),你会看到类似这样的结构:
<div className="flex items-center justify-between p-4 border-b"> <h2 className="text-lg font-semibold">Chat #{currentChatId}</h2> <div className="flex space-x-2"> <button onClick={handleNewChat} className="...">New Chat</button> <button onClick={handleDeleteChat} className="...">Delete</button> </div> </div>这就是我们的“插入点”。
3. 动手写代码:三步实现导出功能
3.1 第一步:添加导出按钮UI
在<div className="flex space-x-2">内部,<button>标签之间,插入新的导出按钮:
<button onClick={handleExportChat} className="px-3 py-1.5 text-sm font-medium rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 transition-colors" title="导出当前会话为JSON文件" > Export </button>注意这里用了onClick={handleExportChat},表示点击时调用一个叫handleExportChat的函数。这个函数还没写,别急,马上来。
3.2 第二步:实现导出逻辑
在同一个文件ChatWindow.tsx中,找到const handleDeleteChat = ...这类函数定义的位置,在它下方,新增handleExportChat函数:
const handleExportChat = () => { if (!currentChatId || messages.length === 0) return; // 构建导出文件名:chat_20240520_143022.json const now = new Date(); const filename = `chat_${now.toISOString().slice(0, 10)}_${now.toTimeString().slice(0, 8).replace(/:/g, '')}.json`; // 将消息数组转为JSON字符串,并添加元信息 const exportData = { exportedAt: now.toISOString(), chatId: currentChatId, model: 'gemma:2b', messages: messages.map(msg => ({ role: msg.role, content: msg.content, timestamp: msg.timestamp || new Date().toISOString() })) }; const jsonStr = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); };这段代码做了五件事:
- 校验是否有会话ID和消息,避免空导出
- 生成带日期时间的文件名,避免覆盖
- 构建结构化JSON对象,包含导出时间、会话ID、模型名、消息列表
- 将JSON转为Blob对象,创建临时下载URL
- 模拟点击触发浏览器下载,并清理内存
它不依赖任何第三方库,纯原生JavaScript,兼容所有现代浏览器。
3.3 第三步:优化用户体验细节
现在按钮有了,功能也通了,但还差一点“人味”。比如:
- 用户点击后,按钮应该短暂变灰,防止重复点击
- 导出成功后,给个轻量提示,而不是静默完成
我们在handleExportChat开头加个loading状态,在结尾加个Toast提示:
const [isExporting, setIsExporting] = useState(false); const handleExportChat = () => { if (!currentChatId || messages.length === 0 || isExporting) return; setIsExporting(true); // ... 原有导出逻辑 ... // 导出完成后 setIsExporting(false); // 显示成功提示(使用原生alert或更优雅的Toast) if (typeof window !== 'undefined') { // 简单起见,用原生提示;生产环境建议替换为自定义Toast组件 alert(` 已导出 ${messages.length} 条消息到 ${filename}`); } };同时,把按钮的className更新一下,加入禁用态样式:
<button onClick={handleExportChat} disabled={isExporting} className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${ isExporting ? 'bg-gray-300 cursor-not-allowed' : 'bg-gray-100 hover:bg-gray-200 text-gray-700' }`} title={isExporting ? "正在导出..." : "导出当前会话为JSON文件"} > {isExporting ? 'Exporting...' : 'Export'} </button>至此,功能闭环。按钮点击→生成JSON→触发下载→提示成功,一气呵成。
4. 构建与部署:让修改生效到Chandra镜像
写完代码只是第一步。Chandra是Docker镜像,我们必须把修改后的前端代码重新打包,再集成进镜像。
4.1 本地验证:先跑起来看看
进入你克隆的Ollama WebUI源码目录(如果你还没克隆,请执行):
git clone https://github.com/ollama-webui/ollama-webui.git cd ollama-webui安装依赖并启动开发服务器:
npm install npm run dev打开http://localhost:5173,你应该能看到和Chandra一模一样的界面,但右上角多了一个Export按钮。随便聊几句,点击它——文件立刻下载,打开JSON,内容清晰完整。
4.2 打包生产版本
确认功能无误后,执行构建:
npm run build构建完成后,dist/目录下生成所有静态文件。这就是我们要塞进Docker镜像的前端资源。
4.3 修改Chandra镜像Dockerfile
Chandra镜像的Dockerfile(通常在CSDN星图镜像广场提供)会类似这样:
FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 3000 CMD ["nginx", "-g", "daemon off;"]关键点在于:它用的是官方Ollama WebUI的源码构建。我们要做的,就是把上面构建好的dist/目录,直接替换掉镜像中原本的前端资源。
最稳妥的方式是:基于Chandra镜像,写一个极简的Dockerfile,只做一件事——把你的dist/目录COPY进去:
FROM registry.cn-hangzhou.aliyuncs.com/csdn-mirror/chandra:latest COPY ./dist/ /usr/share/nginx/html/然后构建新镜像:
docker build -t my-chandra-export . docker run -d -p 3000:3000 --name chandra-export my-chandra-export访问http://localhost:3000,导出按钮已就位。
4.4 一键部署到CSDN星图(可选)
如果你希望把这个增强版Chandra分享给团队,可以直接上传到CSDN星图镜像广场。上传时,在镜像描述中注明:“已集成历史会话导出功能,点击右上角Export按钮即可保存为JSON”。
5. 进阶思考:不只是导出,还能做什么
导出功能看似简单,但它打开了Chandra能力边界的第一道门。基于这个基础,你可以轻松延伸出更多实用能力:
5.1 导入功能:让知识流动起来
有了导出,自然需要导入。只需在ChatWindow.tsx中加一个Import按钮,读取用户选择的JSON文件,解析后调用Ollama API的/api/chat接口创建新会话。代码量不到20行。
5.2 会话分组与标签
当前Chandra的历史列表只按时间排序。你可以修改ChatHistory.tsx,为每个会话添加tag字段(如“工作”“学习”“创意”),支持按标签筛选。数据存在localStorage里,无需后端。
5.3 Markdown导出(给内容创作者)
很多用户导出是为了发公众号或写博客。把JSON里的content字段用marked库转成Markdown,再包装成.md文件下载,体验直接提升一个档次。
这些都不是空中楼阁。它们共享同一个底层逻辑:理解Ollama WebUI的数据流,信任前端的可控性,用最小改动解决最大痛点。
Chandra的价值,从来不在它预装了哪个模型,而在于它给你留了一扇开着的门——门后是完整的、可触摸的、属于你自己的AI工作流。
6. 总结:你刚刚完成的,是一次真正的工程实践
回顾整个过程,你没有调用任何神秘API,没有配置复杂中间件,甚至没动一行后端代码。你只是:
- 看懂了一个开源项目的文件结构
- 在正确的位置加了三段逻辑清晰的代码
- 用标准Web技术完成了数据导出
- 把成果打包进容器,一键运行
这恰恰是AI时代最该被重视的能力:不迷信黑盒,不畏惧源码,用工程思维把大模型真正“用起来”。
Chandra的gemma:2b模型很小,但你的这次修改,让它承载了真实的工作价值。下次当你导出一份会议纪要、一份产品需求草稿、一份学习笔记时,你会记得:这个按钮,是你亲手拧上去的。
技术的价值,永远体现在它如何服务于人的具体行动。而你,已经开始了。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。