资讯动态

XiaoMate 更新拆解:Markdown 流式渲染、Live2D 点击触控与多端构建实践

发布时间:2026/10/26 6:45:23 来源:尧图企业网站定制
1. 这次更新到底改了什么从聊天到手感再到打包XiaoMate 这个项目我自己跟了有一阵子了它本质上是一个带 Live2D 虚拟形象陪伴的桌面聊天应用跑在 Windows 上用 Markdown 渲染对话内容形象部分靠 Live2D 模型驱动。这次更新官方给的说明很短就三句话聊天体验优化、Live2D 点击触控动作优化、多端构建。但真正动手拆过代码、跑过构建的人都知道这三句话背后藏着的是一整套交互链路和工程链路的调整。我拿到更新之后花了两天时间把改动点逐个过了一遍也重新走了一遍多端构建流程下面就把我看到的、踩到的、以及觉得值得分享的东西完整写出来。先说清楚这篇内容适合谁看。如果你只是普通用户想知道这次更新后聊天是不是更顺了、点模型有没有反应那前面几节够用如果你是开发者想自己改 Live2D 的点击逻辑、或者想把 XiaoMate 这套东西构建到别的平台上那中间关于触控命中判定和多端构建的部分会更对你有用。我会尽量把为什么这么改讲透而不是只告诉你改了哪里。聊天体验这块核心矛盾其实一直存在Markdown 渲染和流式输出是天生打架的。你一边在往消息里追加 token一边又要实时把 Markdown 解析成 HTML稍不注意就会出现公式渲染到一半、代码块高亮闪烁、表格错位这些问题。这次更新明显是在这个方向上做了收敛。Live2D 点击触控则是另一个维度的体验之前点模型基本就是触发一个固定的动作现在改成了根据点击区域和当前状态做差异化响应手感完全不一样。多端构建是最容易被忽略但工程价值最高的一块它决定了这套东西能不能从我电脑上能跑变成别人下载下来也能跑。提示下面涉及的所有操作和参数都是基于我本机 Windows 11 环境实测的不同版本可能有细微差异遇到不一致的地方以你实际环境为准。2. 聊天体验优化Markdown 流式渲染的取舍与实现2.1 为什么流式 Markdown 这么难做很多人觉得 Markdown 渲染不就是调个库的事marked、markdown-it 一行代码就出来了。但一旦和流式输出结合问题就来了。假设模型正在输出一段带公式的内容它可能先吐出$$再吐\frac再吐{a}{b}最后才补上$$。在这个过程中的每一个中间态Markdown 解析器看到的都是不完整甚至语法错误的文本。如果你每来一个 token 就重新解析整段轻则公式区域疯狂闪烁重则解析器直接抛异常把整条消息搞崩。我实测下来这次 XiaoMate 的做法是分段提交 延迟解析。具体来说它不再对每个 token 都触发一次完整渲染而是维护一个缓冲区遇到明确的块级边界比如连续两个换行、代码块的 闭合、公式的 $$ 闭合才把这一段交给渲染器。这样中间态就不会被反复解析视觉上稳定很多。2.2 关键参数与配置思路如果你要自己复现这套逻辑核心是三个东西缓冲区、边界检测、以及渲染节流。我用伪代码把思路写一下语言用 JavaScript因为前端渲染基本都是这套// 流式 Markdown 缓冲渲染的核心思路 let buffer ; let renderTimer null; function onToken(token) { buffer token; // 检测是否到达块级边界 if (isBlockBoundary(buffer)) { flushRender(); } else { // 没到边界也要节流渲染避免长时间无反馈 if (!renderTimer) { renderTimer setTimeout(() { flushRender(); renderTimer null; }, 120); } } } function isBlockBoundary(text) { // 公式闭合、代码块闭合、空行结尾都算边界 const endsWithDoubleDollar /\$\$[\s\S]*\$\$$/.test(text); const endsWithCodeFence /[\s\S]*$/.test(text); const endsWithBlankLine /\n\s*\n$/.test(text); return endsWithDoubleDollar || endsWithCodeFence || endsWithBlankLine; }这里的 120ms 节流值是我反复调出来的。太小了比如 30ms渲染压力大公式还是会闪太大了比如 300ms用户会觉得打字机效果卡顿、不跟手。120ms 左右在流畅感和稳定性之间比较平衡。当然这个值跟你的渲染库性能有关markdown-it 比 marked 稍重一点可以适当放宽到 150ms。2.3 公式和代码块的特殊处理Markdown 数学公式这块热词里提到的markdown 大括号多行公式其实是个典型坑点。多行公式通常用\begin{aligned}...\end{aligned}包裹这种结构在流式输出时几乎必然经历半个环境的中间态。我的处理经验是对公式区域做整体占位。也就是说一旦检测到$$开始就把后续内容先塞进一个占位容器等$$闭合了再整体交给 KaTeX 渲染。这样中间态用户看到的是一个公式加载中的占位块而不是一堆乱码。代码块同理。代码高亮库比如 highlight.js对不完整的代码块处理也不友好所以代码块在闭合前不要触发高亮先按纯文本显示闭合后再高亮。这个细节看起来小但直接决定了聊天窗口会不会闪成一片。注意如果你用的是 markdown-it 加插件的方式务必确认插件对不完整语法的容错性。我踩过的坑是某个公式插件遇到未闭合的$$会直接抛错导致整条消息渲染中断后来换成容错性更好的方案才解决。2.4 消息列表的性能问题聊天记录一多性能就是绕不开的坎。这次更新我注意到消息列表做了虚拟滚动只渲染可视区域内的消息。这个改动对长对话体验提升非常明显。之前聊到几百条的时候滚动会明显掉帧尤其是消息里带 Live2D 表情或者大图的时候。虚拟滚动的实现要点在于动态高度。聊天消息高度是不固定的Markdown 渲染后可能很长也可能很短。常见的做法是先用估算高度占位渲染完成后再回填真实高度。这里有个细节回填高度时要避免触发滚动位置跳动否则用户正在看的内容会突然跳走。我的做法是记录滚动锚点高度变化后按锚点补偿偏移量。3. Live2D 点击触控动作优化从点哪都一样到分区响应3.1 旧版点击逻辑的问题老版本的点击逻辑很简单粗暴整个模型就是一个大热区点哪里都触发同一个动作通常是随机播放一个 tap 动作。用久了就会发现很出戏——你点头部它摸头你点脚它还是摸头完全没有被点到那个部位的感觉。这次更新把点击区域做了划分不同区域触发不同动作甚至同一个区域在不同状态下比如正在说话、正在待机响应也不一样。3.2 命中判定是怎么做的Live2D 模型本身是分层的每个部件头、身体、手臂、头发等在模型文件里都有对应的 drawable。点击命中的核心就是判断点击坐标落在哪个 drawable 的范围内。Live2D SDK 一般提供了 hit test 的接口你传入坐标和 drawable ID它返回是否命中。实际操作中我不会对每个 drawable 都做命中测试那样太碎。我的做法是按语义把 drawable 归组比如把所有头部相关的 drawable 归到head组身体归到body组手臂归到arm组。点击时按优先级从高到低测试这些组命中就停。优先级一般是头部 手臂 身体因为头部区域小、用户点击意图更明确。// 点击命中分组的思路 const hitGroups [ { name: head, drawables: [head, hair_front, hair_back, face], action: tap_head }, { name: arm, drawables: [arm_l, arm_r, hand_l, hand_r], action: tap_arm }, { name: body, drawables: [body, cloth], action: tap_body }, ]; function onTap(x, y) { for (const group of hitGroups) { for (const id of group.drawables) { if (model.hitTest(id, x, y)) { playAction(group.action); return; } } } // 都没命中播放默认动作 playAction(tap_default); }3.3 动作过渡与状态机光有分区还不够动作之间的过渡才是手感的关键。如果每次点击都是硬切动作模型会显得很僵硬。这次更新我观察到动作切换用了淡入淡出过渡而且引入了简单的状态机待机、说话、被点击、特殊动作几个状态之间有条件跳转。状态机的价值在于避免动作打架。比如模型正在播放一段长动作这时候用户又点了一下如果没有状态管理两个动作会叠加或者互相打断看起来就很乱。我的处理是如果当前动作是不可打断的比如一段完整的特殊演出点击就排队等待如果是可打断的比如待机循环点击立即切换。当前状态点击响应过渡方式待机立即播放对应区域动作淡入 0.2s说话中播放动作但压低音量动画幅度淡入 0.3s特殊动作中排队动作结束后播放无被点击动作中忽略或重置计时淡入 0.15s这个表格是我根据实测行为整理的不一定和源码完全一致但逻辑上八九不离十。你可以根据自己的模型动作资源调整过渡时长动作资源多的可以短一点资源少的建议长一点让过渡更自然。3.4 触控和鼠标的差异处理热词里出现了点击触控说明这个项目是考虑触屏设备的。触控和鼠标点击有个本质差异触控有按下-移动-抬起的过程而且手指接触面积大坐标精度低。如果直接拿鼠标的点击逻辑套触控会出现想点头部结果判定到身体的情况。我的处理经验是给触控加一个容差半径。命中测试时把点击坐标周围一定半径比如 20px内的区域都纳入测试只要有一个命中就算命中。同时触控的触发时机应该放在抬起而不是按下因为用户按下后可能会滑动按下就触发容易误触。4. 多端构建让项目从能跑到好分发4.1 多端构建到底在解决什么问题一个项目在自己电脑上跑起来和能打包给别人用中间隔着一整个工程化的距离。多端构建要解决的核心问题是同一套代码怎么在不同平台上产出可运行的产物。XiaoMate 涉及的主要是 Windows 桌面端可能还兼顾 Web 预览或者其他桌面环境。不同平台的差异体现在几个层面运行时环境Node 版本、系统 API、原生依赖Live2D 的 native 库、某些需要编译的模块、以及打包格式exe、安装包、绿色版。我见过太多项目卡在我这边能跑打包出来就白屏或者缺个 dll 直接崩。多端构建的价值就是把这些坑在构建阶段就暴露出来而不是等用户下载了才发现。4.2 构建配置的关键点以 Electron 类项目为例多端构建的核心是 electron-builder 或 electron-forge 的配置。我一般会关注这几个点第一是asar 打包。默认情况下源码会被打进 asar 归档但 Live2D 的模型文件和 native 库往往不能放在 asar 里需要配置asarUnpack把它们解出来。这个坑我踩过模型加载失败排查半天最后发现是路径在 asar 里读不到。{ build: { asar: true, asarUnpack: [ **/node_modules/live2d-native/**, **/resources/models/** ], win: { target: [nsis, portable], icon: build/icon.ico } } }第二是native 模块的架构匹配。如果你的项目依赖了需要编译的模块构建时的目标架构x64、arm64必须和编译时的架构一致。我遇到过在 x64 机器上构建 arm64 包结果 native 模块加载失败的情况。解决办法是用对应的架构重新编译或者用交叉编译工具链。第三是资源路径。开发时用相对路径读模型文件没问题打包后工作目录变了路径就失效了。统一用app.getAppPath()或者process.resourcesPath来拼绝对路径能避免大部分路径问题。4.3 构建产物的验证流程构建出来不等于能用。我一般会走一遍验证流程先在一台干净的机器上或者虚拟机安装运行确认没有依赖缺失然后测试核心功能聊天、模型加载、点击响应都要过一遍最后检查安装包体积如果异常大多半是打包了不该打包的东西比如 node_modules 里的开发依赖、测试资源。提示Windows 上构建时如果遇到权限相关的报错先确认是不是在管理员终端里跑的。有些构建步骤需要写系统目录或者注册表非管理员权限会失败。这个报错信息有时候很隐晦容易被误导到别的地方去排查。4.4 跨平台构建的现实考量热词里出现了 Linux 相关的 Markdown 阅读器说明有用户是跨平台使用的。如果你的目标用户里有 Linux 或 macOS 用户多端构建就要考虑更多。Live2D 的 native 库在不同平台上是不同的二进制文件需要分别准备。构建脚本里要根据目标平台动态选择依赖不能一套配置走天下。我的建议是用 CI 来做多端构建本地只负责开发和调试。CI 上针对每个平台跑独立的构建任务产物分别上传。这样既保证了构建环境的一致性也避免了本地环境差异带来的玄学问题。本地构建偶尔成功偶尔失败八成是环境问题别在本地死磕。5. 常见问题与排查技巧实录5.1 聊天渲染类问题问题一公式渲染闪烁或者显示成源码。这个基本是流式渲染没做缓冲导致的。排查思路是先看是不是每个 token 都触发了渲染如果是加上节流和边界检测。如果加了还闪检查公式插件对不完整语法的容错性必要时对公式区域做整体占位。问题二代码块高亮错乱。多半是代码块没闭合就触发了高亮。确认高亮逻辑是不是在代码块闭合后才执行。另外注意语言标识如果模型输出的语言名不在高亮库支持列表里会回退成纯文本这是正常行为。问题三长对话滚动卡顿。检查有没有做虚拟滚动。没做的话消息一多必卡。做了还卡的话看看是不是每条消息都在重新计算高度把高度缓存起来能缓解不少。5.2 Live2D 交互类问题问题一点击没反应。先确认 hit test 的坐标是不是对的。Live2D 的坐标系和屏幕坐标系往往不一致需要做转换。我踩过的坑是忘了做 DPI 缩放在高分屏上坐标全偏了点击位置和实际命中位置差了一大截。问题二动作切换僵硬或者叠加。检查有没有状态机管理。没有的话加上至少区分可打断和不可打断两类动作。过渡时间也要调太短显得突兀太长显得迟钝。问题三触屏设备误触。加容差半径触发时机改到抬起。如果还是误触考虑加一个最小移动距离阈值手指移动超过阈值就判定为滑动而非点击。5.3 构建打包类问题问题一打包后白屏。九成是资源路径问题。打开开发者工具看控制台报错如果是 404 或者路径找不到检查资源引用方式。统一用绝对路径能解决大部分情况。问题二native 模块加载失败。检查架构是否匹配检查 asarUnpack 有没有配置。用process.arch打印一下运行时架构和构建时的目标架构对比。问题三安装包体积异常。用打包分析工具看看哪些文件占了大头。常见的是把 node_modules 整个打进去了或者把模型源文件未压缩的也打进去了。模型文件可以压缩开发依赖要排除。问题现象最可能原因快速排查方法公式闪烁流式渲染无缓冲检查是否每 token 渲染点击无响应坐标未转换打印点击坐标和命中坐标打包白屏资源路径错误看控制台 404 报错native 加载失败架构不匹配对比 process.arch动作叠加无状态机检查动作调用是否互斥5.4 一些不太好归类但很实用的经验Markdown 换行这个事热词里也提到了。不同渲染器对换行的处理不一样有的把单个换行当br有的当空格。如果你的聊天内容换行显示不对先确认渲染器的breaks选项。这个选项一改整个换行行为就变了很省事。还有 Markdown 图片路径的问题。聊天里如果支持贴图图片路径的处理要小心。相对路径在打包后基本都会失效建议统一转成绝对路径或者 base64 内联。base64 的缺点是体积大但胜在稳定小图用 base64 挺合适。Windows 上跑这类应用偶尔会遇到端口占用的问题。如果应用内置了本地服务启动时报端口被占用用netstat -ano | findstr 端口号找到占用进程确认是不是上次没退干净。开发时这种情况很常见加个启动前检测端口的逻辑能省不少事。6. 我个人的一些实操体会Live2D 模型资源这块热词里live2d 免费模型下载出现频率很高说明很多人是拿免费模型来练手的。我的建议是练手阶段用免费模型完全没问题但要注意模型的授权范围尤其是要分发的时候。另外免费模型的动作资源往往比较少这时候状态机的过渡时间就要调长一点用过渡来弥补动作数量的不足观感会好很多。多端构建这件事我的核心体会是尽早做。不要等开发完了才想起来要打包那时候各种路径、依赖问题堆在一起改起来很痛苦。开发初期就把构建流程跑通之后每加一个功能都验证一遍构建问题会小很多。我见过太多项目在最后打包阶段卡住好几天其实都是早期欠的债。聊天体验的优化是个无底洞永远有可以打磨的地方。但优先级要清楚先保证不崩、不闪、不卡再谈锦上添花的动效和细节。用户对稳定的感知远比对炫酷的感知强烈。一个渲染稳定、响应跟手的聊天窗口比一堆花哨但偶尔抽风的特效有价值得多。最后分享一个小技巧调试 Live2D 点击的时候把命中区域可视化出来用半透明色块标出每个 drawable 的范围。这样点击哪里命中哪里一目了然调分区的时候效率能翻好几倍。这个可视化开关记得在正式构建时关掉别打包进去了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑