简介:面向需要快速搭建Coze智能体对话页面的前端开发者,这份可运行源码提供了一套基于HTML的轻量级实现。页面内部已集成完整的Coze API调用流程,自动生成用户ID并支持多轮对话记忆,使上下文衔接自然流畅;同时采用SSE原生流式输出实现实时回复,配合图片链接自动解析与优化显示、Markdown格式解析,内容呈现更加丰富。压缩包仅7KB,共3个文件,核心的coze-chat.html负责全部页面逻辑,另含一份inscode配置文件与.gitignore,便于在在线开发环境中跑通,也适合本地部署或二次改造。使用时只需将COZE_API_TOKEN和COZE_BOT_ID替换为自己的值,即可完成智能体对接;内置的错误处理机制和调试日志输出,可帮助开发者在联调阶段快速定位问题。目前已有214人学习下载,无论是用于产品原型、智能体演示,还是希望研读Coze API集成细节的前端开发者,都能从中获得一套简洁可用的参考实现。
1. 从“会聊”到“能落地”:Coze智能体对话页面到底解决了什么
很多人以为在 Coze 上做好一个 Bot 就等于交付了,真拿去给业务方演示时才发现,平台自带的预览窗口撑不起“产品”两个字。没有独立对话页面,你没法控制消息样式、没法记录会话历史、没法把它嵌进现有的业务系统里。我见过不少团队卡在这一步:Bot 能力已经调通,但对话页面迟迟没着落,项目就卡在“能跑”和“能用”之间。
这个标题给的就是一个可运行的起点:Coze 智能体对话页面搭建,附带能直接跑的源码。它解决的是“把 Coze Bot 变成一个真正可交付的前端对话界面”这件事——包括连接方式、流式消息处理、历史会话、参数调优和部署细节。适合三类人:一是刚接触 Coze 的新手,想跳过从零写前端的成本;二是要在自己系统里嵌入对话能力的后端或全栈工程师;三是准备把 Coze 方案推向业务方的实施人员。读完后你至少能回答三个问题:源码怎么跑起来,页面怎么改,上线前要注意什么。
2. 搭页面前的第一笔账:Bot能力边界与页面形态选型
2.1 对话页面的两种形态:临时预览页 vs 可分发独立页
Coze 平台本身会为每个 Bot 生成一个预览对话页,但这个页面有明确的使用边界。它适合调试对话逻辑、验证 Prompt 效果、检查知识库引用是否正常,但不适合作为对外交付的页面。原因很实际:Bot 的预览页和你的账号体系绑定,你没法控制它的域名、样式和路由,也没法在上面加业务需要的登录、埋点或多轮会话管理。
独立页面就不一样了。它本质是一个前端项目,通过 Coze 提供的 Open API 向后端发消息,页面自己维护对话状态。你可以把它部署在任何静态托管服务上,也可以嵌进已有的管理后台、网站或小程序里。这个形态的灵活性带来一个直接结果:页面和 Bot 的耦合度很低——只要接口协议不变,你换一个 Bot,只需要改配置,页面逻辑一行都不用动。
我的建议是:项目启动前先问一句“这个页面是给谁用”。给测试人员做回归,用平台预览页就够了;给业务方演示、给真实用户使用、要嵌进现有产品,就必须走独立页面。这不是技术偏好问题,是交付边界问题。独立页面的开发成本主要在初期连接和流式处理,这部分恰恰是本源码要帮你省掉的工作。
2.2 选型对照:什么时候该自己写前端,什么时候该用平台方案
做选型时要分清两个层面:一个是“对话入口”层面,一个是“对话能力”层面。Coze 管的是能力层——大模型调用、插件、知识库、工作流,这些都是 Bot 侧的事;对话入口层的页面展示、消息布局、历史记录、用户输入约束,这些是前端工程的事。两者通过接口衔接,选型的核心就在于“这个衔接由谁来维护”。
先看平台方提供的嵌入方案。它的特点是几分钟就能出一个能聊天的页面,适合内部工具、原型验证、个人项目这类“不需要精细控制”的场景。但代价是页面表现力有限,你改不了气泡样式、加不了富文本卡片、也没法自定义错误边界。再看自己写前端。它的好处是页面完全可控,消息渲染、用户身份、权限控制、数据上报都能按需定制;坏处是你得处理 WebSocket 或流式响应,处理断线重连,处理消息时序。这套代码如果从零写,至少要两三天,而且容易写出隐藏 bug。
本标题给的源码走的是“独立页面”这条路径,但它把最难的部分——连接逻辑和流式处理——提前封装好了。你拿到的不是一个写死页面的玩具项目,而是一个可以换皮、换 Bot、换部署方式的基础工程。所以选型结论就一句话:要快速交付且不关心页面精细度,用平台方案;要控制对话体验和产品形态,用独立页面方案,但请站在现成源码上改,而不是从零开始。
2.3 页面要接的四个核心能力:会话、流式输出、插件、知识库
一个可运行的对话页面,至少要覆盖四条能力链路,缺任何一条都会在真实使用中露馅。
会话链路是最基本的:用户发一条消息,请求到 Bot,Bot 返回结果,页面把结果渲染出来。这里面含着一个容易被忽略的点——多轮上下文。Coze 的 API 本身维护会话状态,但前端的消息列表必须和会话 ID 对应上,否则刷新页面就丢上下文。很多半成品页面就死在“刷新即失忆”这个细节上。
流式输出是第二个链路。大模型返回结果有延迟,如果做成等到全部生成完再一次性渲染,用户会以为页面卡死了。Coze 的接口支持流式响应,页面需要用 ReadableStream 边收边渲染,这个能力直接决定“对话手感”。
插件和知识库是第三、第四条链路。Bot 侧的插件调用在页面上看不到过程,但结果会以文本或卡片形式流入消息流;知识库同理,它决定 Bot 回答时引用什么资料。页面不需要关心插件怎么跑、知识库怎么检索,但要保证消息内容能完整展示——包括插件返回的结构化内容和知识库的引用来源。否则就会出现“Bot 引用了资料,但用户看不到依据”的割裂感。
3. 拿到源码后第一件事:把项目在本地跑起来
3.1 环境准备:Node、依赖管理和 API Key
这份源码是一个标准的前端工程,跑起来的前提是有 Node.js 环境。如果你机器上已经装过 Node,先看版本。我一般要求 18 以上,太低跑不起新版本的构建工具链。安装好之后检查一下包管理器,源码的锁文件决定你用 npm 还是 pnpm 安装依赖,不确定就看根目录的文件名。
API Key 是连接 Coze Bot 的凭证,位置在 Coze 平台的个人访问令牌页面生成。这个 Key 要小心处理:它等于你账号在 API 层面的操作权限,进了代码库就等于泄露。本地调试可以临时写进.env文件,但提交到 Git 之前一定要把.env加进.gitignore。部署时更不要把它直接写进前端代码,正确做法是让后端服务保存 Key,由后端转发请求。
还有一个需要搞清楚的配置项是 Bot ID。它不是你给 Bot 起的名字,而是 Bot 详情页 URL 里那一串 ID,通常在创建 Bot 之后可以在“开发”页面的发布信息里找到。页面靠它来定位“跟哪个 Bot 对话”,填错了页面能打开但不会返回消息。这两个配置(API Key、Bot ID)是本地跑通的关键,先备好再动手。
3.2 最小启动命令:从 clone 到页面出现
拿到源码包后,解开压缩包或 clone 到本地,然后按顺序执行三组命令。第一组是安装依赖,第二组是配置环境变量,第三组是启动开发服务。
# 1. 安装依赖(根据锁文件选择包管理器) npm install # 如果用 pnpm,则执行 pnpm install # 2. 创建环境变量文件 cp .env.example .env # 用编辑器打开 .env,填入 COZE_API_KEY 和 COZE_BOT_ID # 3. 启动本地开发服务 npm run dev执行完最后一条命令,终端会打印一个本地地址,通常是http://localhost:5173或http://localhost:3000,用浏览器打开就能看到对话界面。到这里,最小链路已经通了:浏览器里的页面通过本地服务连接 Coze Bot,发消息能收到回复。
这里有两个逻辑要说明。第一,为什么需要后端转发这一层?因为 API Key 放在浏览器端会直接暴露在用户的网络面板里,任何人都能抓出来盗用。本地开发时用一个轻量后端做转发,是安全上的底线;上线部署时同样要保持这个架构,不能让前端直接持有 Coze 的 Key。第二,为什么启动的是开发服务而不是直接构建?开发服务有热更新,改代码之后浏览器自动刷新,调试效率最高;确认没问题后再用npm run build产出静态文件,那才是部署用的产物。
3.3 配置项逐一说明:Bot ID、Token、接口地址
如果你打开.env文件,至少会见到三个关键配置,每个配置的含义和踩坑点都不一样。
COZE_BOT_ID决定对话发给谁,格式是一串字符,不是 Bot 的中文名。常见错误是把“我的 Bot 叫销售助手”理解成 ID,实际要在 Bot 的发布页找“Bot ID”或取数据接口里看。填错了典型表现是:接口返回成功,但没有消息回传。
COZE_API_TOKEN是访问令牌。它绑定的不是你某一个 Bot,而是你的账号。这意味着如果你有多个 Bot,同一个 Token 可以访问全部。所以 Token 的权限边界其实很大,一定要当密码保管,别随手贴到前端代码里。
API_BASE_URL是接口地址前缀。如果你用的是官方公有云,一般保持默认;但如果你把 Bot 发布到了私有部署方案,或者你的网络环境要求走特定的域名,这就要改成你的实际地址。这里的坑在于:填了不存在的地址,页面请求会直接 CORS 报错;填错路径前缀,可能一路 404。调试时优先在浏览器开发者工具里看请求地址,确认它是不是指向了预想的域名。
4. 把对话页改成你想要的样子:关键代码与参数
4.1 对话窗口的骨架结构:消息列表、输入区、状态位
源码的页面结构并不复杂,按功能划分大致有三个核心区域:消息列表区、输入区、状态区。你可以把这三块想象成一个三明治:中间是消息,下面是输入框,而状态区是顶部的“正在输入”提示或错误提示条。这个骨架决定了用户体感的基本面。
<!-- 页面骨架示意 --> <div class="chat-container"> <!-- 消息列表:渲染所有已发送和已接收的消息 --> <div class="message-list"> <div v-for="msg in messages" :key="msg.id"> <!-- 根据 role 区分用户消息和助手消息的样式 --> </div> </div> <!-- 输入区:文本输入 + 发送按钮 --> <div class="input-area"> <textarea v-model="draft"></textarea> <button @click="sendMessage">发送</button> </div> <!-- 状态区:打字指示、错误提示 --> <div class="status-bar"> <span v-if="isLoading">正在回复...</span> <span v-if="errorMsg">{{ errorMsg }}</span> </div> </div>这段骨架透露出两个设计决策。第一,消息列表和管理状态是分离的——messages数组只负责数据,isLoading和errorMsg单独存状态位。这样做的直接好处是:发送消息时不需要清空列表,收到流式内容时不需要重建整个列表,只往数组里追加内容即可。第二,用户消息和助手消息共用同一个messages数组,靠role字段区分样式。这个方法比“两个数组分别维护”简单得多,能避免会话顺序错乱的经典 bug。
改页面时,你真正要动的往往不是这个骨架,而是里面的消息数据结构。比如你想给助手消息加“来源引用”,只要在消息对象里加一个quote字段,渲染层对应加一个展示块就行。保持数据驱动,别在渲染层写深层 if-else 判断消息类型,这是对后续维护最友好的做法。
4.2 流式回复的处理逻辑:从 fetch 到 ReadableStream
流式回复是本源码里最值得读懂的一段代码。它的任务不是“等所有内容生成完再返回”,而是“边生成边把增量推给页面”。在 fetch 的层面,这对应着读取响应的 body 流,并逐段解析文本。
async function streamChat(messages, onChunk) { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ botId: config.botId, messages }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) break; const chunkText = decoder.decode(value, { stream: true }); // 每次拿到新文本片段,立即渲染到页面上 onChunk(chunkText); } }这里要注意的是decoder.decode(value, { stream: true })这行。大模型的返回内容可能按字节流切分,一个中文字符会被切成两半,如果不加stream: true这个参数,就可能出现乱码。加了这个参数,解码器会缓存未完成的字符片段,等下一个 chunk 补齐再输出。很多人在自己做流式时翻车,问题就出在这个细节上。
而onChunk是个回调函数,它接收每一段增量文本,页面拿到后追加到当前消息内容的末尾。这样用户看到的效果是“打字机”式输出。如果你想在这个基础上做更自然的体验,可以加一个小的节流——把 100 毫秒内的多个 chunk 合并一次渲染,避免频繁操作 DOM 导致页面卡顿。但注意节流不要加在数据累加层,只加在渲染层,否则会丢消息内容。
4.3 从“能跑”到“好用”的三个参数:温度、多轮上下文、引用
页面跑通之后,下一步是调参数。第一个参数是温度。它不在页面上,而是在后端调用 Coze API 时传的temperature字段——控制模型输出的随机性。做客服问答、知识库检索这类场景,建议调低到 0.2 左右,回答稳定少跑偏;做文案生成、头脑风暴,可以调到 0.8 以上。但具体值取决于你的 Bot 设定,一个已经写了“保守回复”Prompt 的 Bot,温度 0.7 也不会发疯,反而可能太僵硬。
第二个参数是多轮上下文。Coze API 本身支持传history或依赖 Bot 侧的会话记忆,但页面上你得决定“每次发消息时,把最近多少轮对话传给后端”。传太多轮,Token 消耗大且模型容易抓错重点;传太少,多轮追问会断上下文。常见的做法是保留最近 10 条消息(用户+助手各 5 轮)。这个值建议做成配置,别写死在代码里。
第三个参数是引用显示。知识库模式下,Coze 返回的内容通常带引用信息,页面上要区分“正文”和“引用来源”。我的习惯是:正文按普通消息渲染,引用折叠成可展开的角标或底部小字,避免消息块高度过大。这里有个容易踩的坑——直接拼接引用和正文会导致换行格式混乱,最好在拿到响应后分两个字段存储,渲染时分开处理。
5. 搭建避坑:Coze对话页面最常见的5个坑
5.1 现象:页面能打开但一直转圈,不发消息也报错
本地开发服务正常,浏览器打开页面也没白屏,但一进页面就显示加载中,没有任何报错提示。这个现象最容易出现在刚配置完.env的环节。
原因基本是环境变量没有生效。开发服务启动时读取.env,但如果你的包管理器或框架用的是不同的环境变量文件命名规则(比如.env.local、.env.development),只建.env可能读不到。另一个常见原因是改了.env之后没有重启开发服务——进程只在启动时加载一次环境变量。
解决办法是:先删掉node_modules/.vite之类的缓存目录(如果是 Vite 工程),然后重启开发服务;如果还不行,在代码里临时console.log一下配置对象,确认botId、token有没有真正被读进来。排查原则是“从配置到网络逐层查”,不要一上来就怀疑后端接口有问题。
5.2 现象:消息发出去没响应,但接口状态码返回 200
接口调用成功(HTTP 200),网络面板也看到了请求和响应,但页面上没有任何消息回传。这个坑比报错更隐蔽,因为它看起来一切正常。
原因通常出在响应体结构上。Coze API 返回的 JSON 结构中,消息内容可能嵌套在data、choices或content的二级字段里,而源码里解析用的路径和你实际拿到的不一致,导致解析结果为空字符串,页面渲染了“空消息”。
解决办法是打开浏览器开发者工具的 Network 面板,点开该请求的 Response 标签,逐层查看返回结构,对照源码里parseResponse这类函数的字段路径。如果是路径不匹配,改解析函数而不是改接口。这里提醒一句:不同接口版本返回结构会变,升级 SDK 或切换 Bot 的发布环境后,务必重新检查解析逻辑。
5.3 现象:流式输出被浏览器缓冲了,内容一次性冒出来
明明代码里写了 ReadableStream,但用户看到的效果不是逐字输出,而是卡一下然后整段出现。这在长回答时特别明显,体验损失很大。
原因是数据经过中间层时被缓冲了。如果你的架构是“前端 → 自己的后端 → Coze”,那么中间层必须把 Coze 的流式响应原样转发,而不是等全部收完再一次性返回。常见做法是借用 Node.js 里stream.pipe能力,或确保后端接口设置Content-Type: text/event-stream。检查顺序是:先看后端回复头里有没有Content-Type: text/event-stream,再看代码里有没有res.end()被提前阻塞调用。
另一个隐蔽原因是代理层压缩。开发时 Vite 代理会做缓冲,生产时 Nginx 如果开启了 gzip 且冲掉了流式标记,同样会造成缓冲。处理方式是关掉对流式接口的 gzip,或让 Nginx 直接proxy_buffering off。这个坑在生产环境出现频率最高,而且只影响流式,接口功能看似完好。
5.4 现象:换了一个 Bot,页面还是旧 Bot 的对话行为
配置里已经把COZE_BOT_ID改成新 Bot 了,也重启了服务,但对话表现仍然是旧 Bot 的风格和知识库内容。
原因是浏览器缓存。页面可能缓存了旧代码,也可能缓存了旧配置。但更可能是:后端转发请求时,请求体里同时传了botId,而页面代码中有另一个地方硬编码了 Bot ID,优先级高于环境变量配置。代码里搜索“botId”字符串,看有没有写死在api/chat路由或页面初始化逻辑中。
建议的排查思路是:在本地请求的 Network 面板里查看实际发出的请求体,确认里面传的 botId 是什么。如果存在写死值,就把所有 botId 来源收敛成统一配置变量。上线阶段也建议在页面加一个“当前 Bot 名称”展示,方便肉眼确认部署的是哪个实例——这个小细节能省掉很多测试阶段的无效沟通。
5.5 现象:页面在手机上打开后布局错乱
PC 端一切正常,换到手机浏览器后,输入框被弹出来的虚拟键盘顶出屏幕,消息列表不能自动滚到底部,气泡样式左右不对称。这类纯前端问题最容易让人误以为是 Coze 对接问题。
原因有三个方向。一是安全区适配缺失——iPhone 的底部横条区域没有留白,输入框被遮挡。二是100vh不等于手机屏幕可见高度,地址栏收起弹出时高度变化,布局就乱。三是消息列表的滚动容器没有用overflow-y-auto独立滚动,而是整页滚动,导致键盘弹起时整个页面压缩。
解决方案对应如下:输入区域套用env(safe-area-inset-bottom)适配;外层容器用100dvh代替100vh作为高度基准;消息列表做成独立滚动容器,不要依赖页面滚动。另外,输入框最好保持textarea而不是input,这样用户能发多行消息,长文本体验好很多。把这个打磨完,页面的移动端体感就能超过大部分国内同类 Demo。
6. 让对话页面真正被业务用起来:从 Demo 到可部署的收尾技巧
6.1 几行代码给页面加上“会话记录”持久层
本地跑通、样式改好,只代表 Demo 完成。真实用户不会接受“刷新页面就忘了刚才聊什么”,所以要给页面加一个轻量的会话持久层。取值上不必上重型数据库,用localStorage即可撑住个人使用和小范围体验场景。
// 保存会话记录到本地 function saveConversation(conversationId, messages) { const key = `coze_conv_${conversationId}`; localStorage.setItem(key, JSON.stringify(messages)); } // 页面加载时恢复最近的会话 function restoreConversation() { const lastId = localStorage.getItem('coze_last_conv_id'); if (!lastId) return []; return JSON.parse(localStorage.getItem(`coze_conv_${lastId}`)) || []; }习惯做法是:发起新对话时生成conversationId并存到localStorage的“最近会话”键里;每次消息更新就调用saveConversation覆盖保存;页面初始化时先尝试恢复,没有历史才显示空状态。注意不要无限制地存,建议最多保留最近 20 条对话记录,超出就删最旧的——localStorage只有大约 5MB 空间,且存储的是纯字符串,长对话很容易超量。
6.2 部署到服务器上的注意事项
页面本身是纯静态资源,最省事的部署方式是构建后丢到 Nginx 或任意静态托管。但如果你保持了“前端页面 + 后端转发”的结构,后端部分就需要一个常驻进程。常见做法是用 Node 写一个轻量服务,Node 进程监听某个端口,Nginx 反向代理前端页面和 API 路由到它。
部署时要检查三件事。第一,后端服务进程必须配置成守护进程,否则 SSH 断开服务就停了;用pm2或 systemd 都可以。第二,API Key 放在后端的环境变量里,不要进构建产物;可以通过服务器的.env文件或进程管理工具注入。第三,Nginx 对 API 路由要关掉代理缓冲,否则之前提到的流式一次性输出的问题会在生产环境复现。
6.3 上线前必须过一遍的六项检查
最后这个检查清单是我每次部署 Coze 对话页面前必跑的六项验证,按顺序执行,能挡住九成上线事故。
1. 浏览器控制台无报错,Network 面板无 4xx/5xx 2. 连续多轮对话上下文正常,第 5 轮能记住第 1 轮的关键信息 3. 刷新页面后,最近一轮对话消息仍在(持久层生效) 4. 切换移动端模拟器,键盘弹出时输入框不被遮挡 5. 长回答以流式逐字输出,不整段等待 6. 断网后页面提示明确错误,不无限加载这六项里最容易遗漏的是第 3 项,因为开发时大多数人都在同一页面里持续调试,很少刷新验证。我吃过这个亏:某次给业务方做演示,中途我接了个电话,回来页面已经自动刷新(开发服务重启),整个对话上下文全丢,业务方当场皱眉。从那以后,我的习惯是先刷新再演示,把恢复逻辑当作主流程的一部分验证,而不是事后补救。这次源码已经把对话页面的连接和核心交互打通了,你要做的只是在这个骨架上补好持久层、部署和验证,希望帮到你少走这一步弯路。
本文还有配套的精品资源,点击获取