Android Compose AI组件库:从流式文本到图像生成的架构实践
最近在忙一个跨平台的智能应用项目AI能力要在Android端做完整的对话、图片生成、语义搜索体验。搭到第三块页面的时候我就开始烦了每次都是把网络流、状态管理、UI拼装从头粘一遍代码长得几乎一样效果却总有细微出入。后来看到Compose那边的AI组件库陆续出来了几个版本翻完代码我觉得这条路是走对了。于是这篇想认真聊聊Android Compose的AI组件库到底在解决什么、核心组件怎么设计、落地成真实项目有哪些躲不开的坑。简单说这类组件库就是把AIGC能力按UI场景拆成可复用组件——流式文本输出、图像生成展示、语义输入等等都封装成Composable函数开发者拿来即用内部自己处理状态、缓存和流式渲染。它适合正在做或计划做AI功能接入的Android工程师、Compose使用者也适合团队里负责组件选型和架构设计的同学参考。不过要真正把这类库用明白得先搞清楚它背后的状态管理和边界划分是怎么做的。1. 从“手搓AI界面”到“Compose AI组件库”到底省掉了什么1.1 接入AI能力过去要写多少样板代码去年我接手的第一个AI聊天页面连服务端交互、增量文本、重试逻辑都是自己写的。那时候最痛苦的不是某个环节特别难而是每个新页面都要把同一套流程重来一遍网络层要处理流式响应收到一段token就要更新一次界面UI层要维护loading、error、历史消息图片生成又要单独写轮询任务状态的代码。写第一遍的时候觉得挺新鲜写到第三个类似页面时已经开始复制粘贴了。我整理过一份对比表格手写方案和组件库方案之间的差距比想象中大维度手写方案组件库方案网络流处理自己封装流式通道处理断线、重连、重试内部封装统一的Flow数据管道状态管理loading、error、内容状态散落各处集中用StateFlow或状态机驱动增量渲染手动拼接文本再刷新顺序靠自觉组件内置流式文本展示天然按token渲染错误与中断每个页面自己写重试UI和中断逻辑组件提供统一的错误态、重试和取消图片生成链路轮询任务状态下载展示容易漏状态状态机缓存渐进式显示一步到位这是组件库最直接的省事点把高频出现的“AI交互形态”沉淀成组件而不是让你每次从字节流开始造轮子。1.2 组件库的抽象边界UI与链路逻辑的划分省事不等于万能。AI组件库和AI SDK不是一回事。SDK负责模型调用、密钥管理、底层API请求组件库更多是把“链路过程”翻译成“界面状态”让UI层直接用声明式的方式表达AI交互。我见过一个错误的做法把聊天组件写得越来越重内置了用户会员等级判断、计费逻辑、敏感词过滤。刚开始觉得“方便”后来团队其他人想在另一个场景复用发现组件根本拎不出来改一行计费逻辑要动整个组件。问题不是封装太多而是封装了不该封装的东西。组件库的边界应该看清自己是什么。它不是业务容器只是“AI界面的轮胎”——发动机是模型能力底盘是网络链路但要让车真正跑起来四个轮子也得专业。计费、权限、业务卡片这些应该通过插槽、参数或上层扩展点交给业务方而不是焊死在组件内部。2. 组件库的能力分层文本、图像、语义三类组件怎么分工2.1 文本流式输出组件目前用得最多的就是流式文本组件。它接收一个流式数据源通常是FlowAiChunk把增量token渲染成带打字机效果的消息气泡顺便把重新生成、停止生成、失败重试这些操作都做成内建的交互。它的核心职责有两个一是数据流转换把底层返回的增量文本变成UI可直接消费的状态二是视觉反馈包括光标闪烁、流式未完提示、整体生成时间等。需要注意它不该管对话历史存哪里、消息卡片用什么格式。这些东西应该交给业务层或者通过自定义内容槽位扩展。实际项目里我会让这个组件自身只负责“一段消息的流式展示”聊天列表的滚动、历史消息、自动滚底由外层的页面组件处理。职责拆分清楚以后调试定位会快很多。2.2 图像生成展示组件图片生成和文本生成最大的区别在于“等待时间长、状态多”。一个任务可能要经历排队、生成中、生成完成、失败期间还要展示进度条或者阶段提示。图像生成展示组件一般内置一个状态机对外只暴露最简单的入参和回调。它的边界同样要控制好。生成引擎是A公司的还是B公司的组件内部不应该关心外面通过适配层把不同引擎的请求统一成同一种任务接口即可。图片鉴黄、生成内容合规这类服务端逻辑也不该混进来组件始终只做“展示生成过程和结果”。我把图像生成组件拆成了三块任务状态显示、图片渐进加载、结果操作栏重新生成、下载、放大预览。三块内部各有缓存策略但对外是一个整体。这样页面调用方拿到的是一句“给我一个图生任务开始展示”完全不用管过程细节。2.3 语义输入组件语义输入是个容易被忽略但又很关键的组件。它负责输入框、提示建议、上下文联想可能还带多模态图文一起发。最常见的交互是用户在输入框输入内容时根据输入联想到候选短语、追问建议或可选的图片素材。这个组件最容易做重。加了向量检索、RAG、用户行为推荐以后组件会迅速膨胀。我的建议是组件只负责“展示候选”和“回传选择结果”候选内容来源于上层注入的FlowListCandidate组件不做检索也不做排序。输入防抖、请求取消也需要在组件内部实现因为这是纯客户端行为不影响业务职责划分。3. 核心实现逻辑流式文本渲染中的数据流架构3.1 从回调地狱到StateFlow数据流选型流式文本组件最核心的设计决策是数据流选型。很多刚接触的人会自然地写出一个回调接口网络层每来一个token就回调一次UI。第一版能跑但很快会发现两个问题回调散落在各个页面很难统一做取消组合生命周期和回调生命周期一旦不同步页面销毁后还在更新UI。我后来全部改成用Kotlin协程的Flow接收上游事件在ViewModel层用StateFlow持有渲染文本。这里的关键是用update方法保证并发场景下状态更新不会丢事件而不是直接用value value deltadata class AiChunk( val delta: String, val finishReason: String? null ) class ChatViewModel : ViewModel() { private val _streamingText MutableStateFlow() val streamingText: StateFlowString _streamingText.asStateFlow() fun send(prompt: String) { viewModelScope.launch { aiClient.streamChat(prompt) .catch { e - _error.update { e.message ?: 生成失败 } } .onEach { chunk - _streamingText.update { it chunk.delta } } .collect() } } }为什么是StateFlow而不是Channel因为UI重组只需要读取最新值不需要消费一个少一个的队列语义。StateFlow会持有最新的文本快照重组时永远能拿到当前完整内容不会出现“我虽然收到了10个token但界面只显示了5个”的尴尬。同时网络层要把底层字节流封装成一个FlowAiChunk。这样上层根本不需要关心底层是哪种传输方式切换模型、切换供应商时只需替换数据源实现UI组件完全不动。3.2 增量渲染与重组的最小化流式场景一个很微妙的问题每次token到达都触发一次重组这是必要的但能否把重组范围控制到最小Compose里状态粒度决定重组范围如果你把整个页面所有挂在同一个MutableStateFlow上那每次token更新都会拖累整个页面重排。这里我最常用的分层方式是这样Composable fun AiStreamText( textFlow: StateFlowString, modifier: Modifier Modifier ) { val text by textFlow.collectAsState() Text(text text, modifier modifier) }把流式文本独立成一个组件外层页面即使有图片列表、按钮栏、输入区它们也都不订阅textFlow。只有这个Text节点会随着每次token到达而重组。这样就把“内容高频变化”的影响锁在了最小作用域里。还要注意的是不要在Text里塞复杂的字符串处理逻辑。比如把Markdown解析、emoji替换、URL自动识别全放在每次重组的composition阶段做会把重组时间拉得很高。正确的做法是先把渲染文本算好缓存起来重组阶段只做“把已有字符串显示出来”这一件事。3.3 中断、切换与Flow取消流式文本组件里最容易被忽略的是“旧任务还没结束新任务已经来了”。如果用户连续发两条消息第一个请求还在生成第二个请求又开始了不处理的话两个流会同时往文本里追加内容产生混乱。用Flow的好处是取消非常干净。在ViewModel里可以用flatMapLatest之类的操作符让新请求发出时自动取消前一个或者在viewModelScope里启动的新协程本来就是可取消的。关键点是组件库内部处理流时一定要把协程作用域绑定到可取消的上下文中避免用全局作用域。实际踩过的一个坑早期版本里我在Composable外部用了rememberCoroutineScope去收集网络流觉得这样代码方便。结果页面退到后台后协程还在跑回来时文本突然跳了一大段。后来改成把收集逻辑放ViewModelUI只负责收集状态一切才稳定下来。4. 图像生成组件的进度状态机与缓存设计4.1 状态机的定义图片生成不像文本那样可以逐token渲染它更多是一个“任务型”交互。一个结果要么还没出要么生成了中间还有排队和逐步出图的过程。这种场景用状态机管理最合适。我在组件里定义的状态一般长这样sealed interface GenState { data object Idle : GenState data class Queued(val position: Int) : GenState data class Generating(val progress: Float) : GenState data class Success(val url: String) : GenState data class Failure(val message: String) : GenState }当输入prompt之后组件内部根据后端任务状态依次切换这些状态。为什么不在回调里直接改几个boolean变量因为布尔值组合起来会失控——“是排队中还是已取消失败了还能不能被新任务覆盖生成中点了重新生成状态怎么迁移”这些问题在布尔变量体系里很难回答但有了状态机每一步迁移都在明面上。这里要强调一下progress的处理。很多接口返回的进度不是真实的有的模型一步就出图有的会在中途卡住不动。组件最好把进度条做成“软进度”也就是根据固定时间递增或按阶段跳变不要纯粹依赖服务端数值否则用户会以为卡死了。4.2 缓存与图片加载图片生成结果的展示有一个天然难点用户可能用同样的prompt生成多次每次都请求网络拿图显然不合理。内存缓存LruCache是基础我通常会再加一层磁盘缓存按prompt的哈希值做key。Compose里加载图片要从网络拿极容易踩的坑是在Composable函数体里直接发网络请求。正确姿势是用produceState或者LaunchedEffect配合图片加载库在协程里执行加载再回调状态。组件库内部如果封装好了这个逻辑外部使用者甚至不需要知道图片具体什么时候加载完成只需要声明“结果图片区域”长什么样。还有一个小细节涉及图像生成列表比如历史生成记录时LazyColumn的item要设置稳定的key否则图片加载状态和缓存会错乱。这个坑在长列表里特别隐蔽表现为滑回去之后图片闪烁或者错误地复用了上一张图的状态。4.3 进度展示与视觉反馈图像生成不只是“等结果”过程体验直接影响用户的耐心。我给组件设计了几个阶段反馈排队时显示队列位置生成中显示阶段名称“正在绘制草图”“正在上色”成功后先出低清预览再渐进加载高清图。这些阶段文案和图标最好做成可配置参数而不是硬编码。不同业务场景文案差别挺大工具类App喜欢“正在生成”内容社区喜欢更有氛围的表达做成参数后组件可以服务多个场景。5. 性能调优在Compose里控制重组范围守住流畅度5.1 高频状态与UI作用域分离AI场景天然是“高频状态更新”的代名词。每来一个token就改一次状态每生成完一张图就要更新一次页面如果状态握在整个页面顶层性能基本不用指望。我判断一个组件性能是否合格主要看三点滑动列表是否掉帧、快速输入时键盘是否卡顿、流式更新时非文本区域是否跟着闪动。其中最后一点最容易被忽视因为它不一定是肉眼可见的闪烁可能是输入框的焦点被重组打断、滚动位置被重置这种更恶性的问题。解决办法是高频状态尽量下沉。AiStreamText内部持有和消费自己的流外层页面不要订阅底层token级状态。外层如果需要“是否正在生成中”这种衍生信息用derivedStateOf或者单独一个低频状态暴露不要直接拿文本内容做判断。5.2 稳定性、Key与扁平化组合Compose推荐用Stable标记纯数据类帮助编译器跳过不必要的重组。AI组件库里的状态类尤其适合做这件事因为文本流式更新时很多子组件其实不需要跟随变化只要数据类标记稳定组合器就能判断出“入参没变跳过重组”。参数稳定性还有另一个维度lambda。如果你给子组件传了一个每次重组都新建的lambda即使其它参数没变子组件也会因为lambda引用变化而无法跳过。在AI高频渲染场景里这个损耗会被放大。使用remember包裹lambda是基本操作更进阶的做法是把稳定属性提升到更靠近调用的位置。最后说扁平化组合。流式文本组件里我尽量避免多层嵌套的AnimatedVisibility来回切换因为动画状态变化本身会触发更高的重组开销。能用透明度直接控制就用透明度少套一层可见性动画滑动流畅度区别很明显。5.3 列表场景的特别处理流式文本不只有一个气泡的场景很多时候是聊天列表里最后一条消息正在流式输出。这时LazyColumn需要同时处理好“最后一项动态变化”“用户手动滑回顶部时不要强行自动滚底”和“历史item不能跟着频繁重组”三个问题。我的经验是历史消息用独立的稳定数据类承载只有最后一条消息订阅流式状态。自动滚底通过LaunchedEffect监听流式文本长度变化来触发同时通过一个布尔值标记用户是否正在手动浏览历史内容——用户在往上翻时绝不自动把列表拽回底部。这里有个很现实的边界情况用户正在阅读生成中某段前面的内容系统又来了一个新token如果每次都强制滚到底部体验会非常糟。很多第一版AI聊天页面都犯过这个毛病。6. 落地上线的校验清单与个人实践经验6.1 流式数据解析与容错组件库把网络流处理封装了但作为接入方你必须清楚底层有可能产生半截数据。流式传输是按块到达的一个完整的JSON可能会被拆成两半如果解析逻辑写得不够健壮就会出现乱码或解析失败。我看过不少项目在这个环节踩坑直接在接收回调里做JSON.parse遇到半截数据就抛异常然后整个生成流程崩掉。正确的做法是在底层先做缓冲累积攒够一个完整的数据帧再解析同时要能处理转义字符、Unicode编码等问题。组件库如果提供默认解析器我强烈建议保留一个“原始增量回调”的口子方便排查线上问题。AI相关的问题经常复现不了有个原始数据出口配合日志定位效率能高很多。6.2 页面生命周期与任务取消我们组在接入第一个AI组件时遇到最典型的问题是页面销毁后任务还在跑。有的表现在加完ViewModel作用域之后就好了有的是因为组件内部自己起了协程但作用域绑错了地方。Compose有个天然优势LaunchedEffect会在离开组合时自动取消协程。组件库如果完全基于Compose特性实现其实生命周期这块相对安全。但如果是网络请求仍然在底层跑你还需要确保取消UI协程时也向服务端发出取消信号否则服务端还在刷显卡额度。给项目定一条规则所有AI生成任务的协程作用域都挂到viewModelScope或者页面级作用域任何”试试看“的临时作用域都不允许用于生产代码。这条规则陪我避免了多次线上异常。6.3 多模型切换与组件接口稳定性很多App后期会换模型供应商从A厂商换成B厂商或者同一厂商不同版本间切换。如果组件库接口把供应商细节暴露给UI调用方切换成本会非常高。我推荐在组件库之上再包一层薄薄的适配层把各家模型的差异统一成一种任务描述。组件库只认识“生成文本”“生成图片”“停止生成”不认识具体是哪家平台。这样未来新增接入方时页面代码一行不用改只加一个适配器实现。组件版本升级后我通常会在测试机上跑一遍固定的验证脚本“发送一条超长prompt、生成中途点击停止、失败后重试、快速连续发两条消息、图片生成过程中杀进程恢复”。这五个场景能覆盖AI组件库80%以上的异常路径跑通了才敢发版。最后分享一个调试小习惯在Debug配置下给流式文本组件外面包一层自写的重组计数函数每次token到达时打印重组次数。如果发现每次更新带动了页面其他区域一起重组就顺着状态提升的层级往上找直到确认高频状态被隔离在最小组件内部。这套方法帮我解决过好几个莫名其妙的滑动卡顿问题。AI组件库的体验好不好最终拼的不是哪个组件炫而是状态管理和异常路径是否扛得住真实用户的手速。

相关新闻

MCP Client 与 LLM 协作实战:工具调用与上下文管理全解析

MCP Client 与 LLM 协作实战:工具调用与上下文管理全解析

1. 从项目背景说起:MCP 到底解决了什么问题说到 MCP(Model Context Protocol,模型上下文协议),我先讲一个痛点场景。假设你手里有 5 个 AI 应用,想给每个应用接上 3 类外部能力:数据库查询、文件…

2026/10/18 20:42:12 阅读更多 →
Lambda表达式详解:语法糖、闭包捕获与主流语言对比

Lambda表达式详解:语法糖、闭包捕获与主流语言对比

我第一次对lambda表达式产生好感,是在一个排序逻辑改了第三次之后。那是个处理订单列表的功能,每次新增一种排序规则,就要新写一个匿名内部类,光模板代码就占了大半屏。后来把排序逻辑整个换成lambda,十几行变成一行&a…

2026/10/18 20:42:12 阅读更多 →
Claude Code终端AI编程助手:配置、权限与高效工作流实战

Claude Code终端AI编程助手:配置、权限与高效工作流实战

1. 为什么把 AI 编程搭在终端里:Claude Code 的定位与它解决的问题最近一个月我基本把日常编码的主流工作流搬进了一个命令行工具里,就是标题里提到的 Claude Code。先说结论:这是一个跑在终端里的 AI 编程助手,它跟网页端聊天窗口…

2026/10/18 20:42:12 阅读更多 →

最新新闻

OpenHarmony奖品详情页开发:从数据模型到状态管理全解析

OpenHarmony奖品详情页开发:从数据模型到状态管理全解析

我先把前面五篇的进度简单对一下,免得有读者是从这一篇才跟上来的:目前这个幸运大转盘项目已经完成了转盘主界面、抽奖动画、奖品管理、中奖记录和中奖弹窗这几个核心模块。这一篇要补上的奖品详情,其实是把"用户点开某个奖品图标之后看…

2026/10/18 21:47:12 阅读更多 →
React Native键盘避让在OpenHarmony上的适配实践

React Native键盘避让在OpenHarmony上的适配实践

1. 为什么键盘避让在 OpenHarmony 上会成为问题?做跨端开发的人最头疼的一类问题,莫过于软键盘弹起来之后,输入框被死死挡住。在 React Native 项目里,大家的第一反应就是用官方提供的 KeyboardAvoidingView 组件;可一…

2026/10/18 21:47:12 阅读更多 →
量化投研遇上大模型:研究记忆的风险边界与工程实践

量化投研遇上大模型:研究记忆的风险边界与工程实践

1. 当量化投研遇上大模型:一个正在被重新定义的边界做量化投研的人这两年应该都有一个共同的感受:数据获取的门槛在肉眼可见地降低,但真正拉开差距的东西,反而越来越模糊了。以前我们拼的是谁的因子库更全、谁的行情清洗更干净、谁…

2026/10/18 21:47:12 阅读更多 →
PL/SQL Developer导出表全攻略:从选项拆解到踩坑排查

PL/SQL Developer导出表全攻略:从选项拆解到踩坑排查

很多PL/SQL Developer用户都会卡在“导出表”这一步。明明菜单里有个Export Tables,可真点进去又不知道该勾哪些选项,导出来的SQL文件拿去别的库一执行就报错,或者数据缺了一半。这问题特别常见,尤其在Oracle DBA和开发人员之间来…

2026/10/18 21:47:12 阅读更多 →
降低AIGC率有哪些方法?AIGC检测里摘要、综述AI率高分别怎么改!

降低AIGC率有哪些方法?AIGC检测里摘要、综述AI率高分别怎么改!

降低AIGC率有哪些方法?AIGC检测里摘要、综述AI率高分别怎么改! 拿到知网AIGC检测报告,你可能会看到这样的情况:全文AI率不算夸张,可摘要那一页的AIGC疑似度超过50%,第二章文献综述连着三四页大段标红&…

2026/10/18 21:47:12 阅读更多 →
Select模型详解:Windows Socket TCP服务端实现与优化

Select模型详解:Windows Socket TCP服务端实现与优化

聊到Windows下的网络编程,“五种I/O模型”绝对是一个绕不开的话题。很多人在面试或者做项目的时候第一次接触这个概念,当场就被Select、WSAAsyncSelect、WSAEventSelect、重叠I/O、完成端口这一串名词搞懵了。说实话,这五个模型确实容易让人望…

2026/10/18 21:46:11 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →