SpringAI聊天记忆ChatMemory

SpringAI聊天记忆ChatMemory

目录

1、概述

2、基于内存存储聊天记忆

2.1 导入依赖

2.2 创建基于内存的 chatMemory

2.3 简单的代码示例

2.4 结果示例

3、基于Mysql jdbc方式存储聊天记忆

3.1 环境配置

3.2 导入mysql jdbc相关依赖

3.3 配置文件配置ChatMemory初始化配置

3.4 配置ChatMemory

3.4.1 bean 配置mysql 和chat memory

3.4.2 starter-jdbc 进行配置

3.5 支持的数据库JdbcChatMemoryRepositoryDialect

3.6 使用

3.7 结果

4、基于redis 存储聊天记忆

4.1 环境准备

4.2 导入依赖

4.3 bean 配置

4.4 基础使用

4.5 redis存储结果


1、概述

SpringAI的版本查看 https://blog.ZEEKLOG.net/weixin_45948519/article/details/156327249?spm=1011.2415.3001.5331

Spring AI 的 chat-memory 是支撑多轮连贯对话的核心组件,核心解决大语言模型本身的无状态痛点。它的核心作用是存储多轮对话的交互记录,并在后续请求中把对话历史与新请求合并后发送给模型,实现连贯响应。

2、基于内存存储聊天记忆

2.1 导入依赖

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId> </dependency>

2.2 创建基于内存的 chatMemory

import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ChatMemoryConfig { /** * 聊天内存仓库 */ @Bean("inMemoryChatMemoryRepository") ChatMemoryRepository chatMemoryRepository() { return new InMemoryChatMemoryRepository(); } /** * 聊内存储 */ @Bean("inMemoryChatMemory") ChatMemory chatMemory(@Qualifier("inMemoryChatMemoryRepository") ChatMemoryRepository chatMemoryRepository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) // 设置最大消息 默认 20条 也就是10轮对话 .maxMessages(20) .build(); } }

2.3 简单的代码示例

import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; @Slf4j @RestController @RequiredArgsConstructor @RequestMapping("chat/memory") public class ChatMemoryController { private final ChatClient ollamaChatClient; // 基于内存的 ChatMemory private final ChatMemory inMemoryChatMemory; @GetMapping("memory") public String memory() { // 会话编号 每个会话一个会话编号 用于会话记忆隔离 String conversationId = UUID.randomUUID().toString(); String one = ollamaChatClient.prompt() .user("请记住黄霸天是女生") .advisors( PromptChatMemoryAdvisor.builder(inMemoryChatMemory) .conversationId(conversationId) .build(), // MessageChatMemoryAdvisor.builder(inMemoryChatMemory) // .conversationId(conversationId) // .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第一次回复消息 {}", one); String two = ollamaChatClient.prompt() .user("黄霸天是男生还是女生") .advisors( PromptChatMemoryAdvisor.builder(inMemoryChatMemory) .conversationId(conversationId) .build(), // MessageChatMemoryAdvisor.builder(inMemoryChatMemory) // .conversationId(conversationId) // .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第二次回复消息 {}", two); return "ok"; } }

2.4 结果示例

  • 通过日志可看出已经将数据传递给大模型了 此模式为PromptChatMemoryAdvisor 兼容性最好。检索对话历史后,将其整合为一段文本,嵌入到 Prompt 的 “系统文本”(System Prompt)中;不保留对话的原始消息结构,而是把历史转成描述性文本
  • 以下为MessageChatMemoryAdvisor的抓包示例 可以看出 MessageChatMemoryAdvisor是将历史对话信息保留原始结构发送,检索对话历史后,以「消息组」的形式直接添加到 Prompt 中。但是注意并非所有 AI 模型都支持这种 “多消息结构” 的 Prompt(部分模型仅接受单段文本 Prompt)

3、基于Mysql jdbc方式存储聊天记忆

这里拦截器使用的是PromptChatMemoryAdvisor。与MessageChatMemoryAdvisor区别可以看内存存储聊天示例

3.1 环境配置

  • 安装mysql 这里使用docker 的方式快速布置 如果未安装的可以百度一下如何安装docker。建议使用win11进行开发的话都安装一下docker 很方便
# 这个命令为 创建一个mysql8 密码是123456 端口是 16010 的mysql容器 docker run -d --name mysql8.0.20 -p 16010:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_ROOT_HOST=% mysql:8.0.20 # 使用命令 创建数据库 test_db docker exec -it mysql8.0.20 mysql -u root -p123456 -e "CREATE DATABASE IF NOT EXISTS test_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;"

3.2 导入mysql jdbc相关依赖

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> </dependency> <!--mysql驱动--> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

3.3 配置文件配置ChatMemory初始化配置

# 配置数据库初始化为 always spring.ai.chat.memory.repository.jdbc.initialize-schema=always # 配置数据库初始化表的sql文件 可以使用默认值 spring.ai.chat.memory.repository.jdbc.schema=classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-mysql.sql # 配置数据库名称 spring.ai.chat.memory.repository.jdbc.platform=mysql
  • IDEA 按两下 shift 搜索 schema-mysql.sql 可找到默认的数据库文件
  • schema-mysql.sql
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( `conversation_id` VARCHAR(36) NOT NULL, `content` TEXT NOT NULL, `type` ENUM('USER', 'ASSISTANT', 'SYSTEM', 'TOOL') NOT NULL, `timestamp` TIMESTAMP NOT NULL, INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`) );

3.4 配置ChatMemory

3.4.1 bean 配置mysql 和chat memory

import com.zaxxer.hikari.HikariDataSource; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository; import org.springframework.ai.chat.memory.repository.jdbc.MysqlChatMemoryRepositoryDialect; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; @Configuration public class MysqlChatMemoryConfig { // 创建数据源 @Bean(name = "mysqlChatDataSource") DataSource getDateSource() { HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build(); String url = "jdbc:mysql://%s:%s/%s" + "?useUnicode=true&characterEncoding=utf-8&useSSL=false" + "&allowPublicKeyRetrieval=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai"; // 数据库相关配置 dataSource.setJdbcUrl(String.format(url, "127.0.0.1", 16010, "test_db")); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } // 创建jdbc模版 @Bean(name = "mysqlChatJdbcTemplate") JdbcTemplate mysqlChatJdbcTemplate(@Qualifier("mysqlChatDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } // 创建事务管理器 @Bean(name = "mysqlChatTransactionManager") PlatformTransactionManager mysqlChatTransactionManager(@Qualifier("mysqlChatDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } // 创建 chat 数据库存储 @Bean(name = "mysqlChatMemoryRepository") JdbcChatMemoryRepository mysqlChatMemoryRepository( @Qualifier("mysqlChatJdbcTemplate") JdbcTemplate jdbcTemplate, @Qualifier("mysqlChatDataSource") DataSource dataSource, @Qualifier("mysqlChatTransactionManager") PlatformTransactionManager transactionManager) { MysqlChatMemoryRepositoryDialect dialect = new MysqlChatMemoryRepositoryDialect(); return JdbcChatMemoryRepository.builder() .jdbcTemplate(jdbcTemplate) .transactionManager(transactionManager) .dialect(dialect) .dataSource(dataSource) .build(); } // 创建 chatMemory @Bean("mysqlChatMemory") ChatMemory mysqlChatMemory(@Qualifier("mysqlChatMemoryRepository") JdbcChatMemoryRepository repository) { return MessageWindowChatMemory.builder() // 使用数据库存储 .chatMemoryRepository(repository) // 保留最多10条记录 .maxMessages(10) .build(); } }

3.4.2 starter-jdbc 进行配置

  • 导入依赖
<!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
  • 配置文件配置
# 可使用spring 默认数据库配置信息配置 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:16010/test_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456
  • bean配置
@Bean("mysqlChatMemory") ChatMemory mysqlChatMemory(JdbcChatMemoryRepository chatMemoryRepository) { return MessageWindowChatMemory .builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages(10) .build(); }

3.5 支持的数据库JdbcChatMemoryRepositoryDialect

3.6 使用

import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; @Slf4j @RestController @RequiredArgsConstructor @RequestMapping("chat/memory/mysql") public class MysqlChatMemoryController { private final ChatClient dashscopeChatClient; private final ChatMemory mysqlChatMemory; @GetMapping("base") public String base() { // 会话编号 每个会话一个会话编号 用于会话记忆隔离 String conversationId = UUID.randomUUID().toString().replace("-", ""); log.info("conversationId: {}", conversationId); String one = dashscopeChatClient.prompt() .user("记住北京今天的天气是晴天,26摄氏度") .advisors( PromptChatMemoryAdvisor.builder(mysqlChatMemory) .conversationId(conversationId) .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第一次回复消息 {}", one); String two = dashscopeChatClient.prompt() .user("北京今天的天气是什么样的") .advisors( PromptChatMemoryAdvisor.builder(mysqlChatMemory) .conversationId(conversationId) .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第二次回复消息 {}", two); return "ok"; } }

3.7 结果

  • 执行后可查看数据库已经存储到了历史聊天记录了
  • 目前最大聊天记录超过了设定的值就会删除,我们想的是最近的数据传递给大模型,而历史的聊天记录保存下来的话可以自己写一个增强器对历史的数据进行保留。并且如图,目前JdbcChatMemoryRepository里面的做法是直接删除所有的然后添加进去。这种方式里面的时间戳也不是用户真实添加的时间戳。而应该算是最后使用时间而不是创建时间。用户所有的历史聊天记录还是推荐使用自定义增强器对历史记录保存。然后短期的历史记录则通过redis或者内存进行存储发送给大模型
  • 提供一个简单保存增强器示例
 import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.ai.chat.client.ChatClientMessageAggregator; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; import org.springframework.ai.chat.client.advisor.api.CallAdvisor; import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain; import org.springframework.ai.chat.client.advisor.api.StreamAdvisor; import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain; import org.springframework.ai.chat.messages.Message; import reactor.core.publisher.Flux; import java.util.Date; @Slf4j public class SaveAdvisor implements CallAdvisor, StreamAdvisor { private int order = 0; private String conversationId = "def"; public SaveAdvisor(int order, String conversationId) { this.order = order; this.conversationId = conversationId; } @NotNull @Override public ChatClientResponse adviseCall(@NotNull ChatClientRequest chatClientRequest, @NotNull CallAdvisorChain callAdvisorChain) { this.saveRequest(chatClientRequest); ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest); this.saveResponse(chatClientResponse); return chatClientResponse; } @NotNull @Override public Flux<ChatClientResponse> adviseStream(@NotNull ChatClientRequest chatClientRequest, @NotNull StreamAdvisorChain streamAdvisorChain) { this.saveRequest(chatClientRequest); Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest); return (new ChatClientMessageAggregator()).aggregateChatClientResponse(chatClientResponses, this::saveResponse); } public void saveRequest(ChatClientRequest request) { saveMessage(request.prompt().getUserMessage()); } public void saveResponse(ChatClientResponse chatClientResponse) { saveMessage(chatClientResponse.chatResponse().getResult().getOutput()); } public void saveMessage(Message message) { // 这里可以使用MQ异步保存 log.info("保存消息 {} {} {} {}", this.conversationId, message.getMessageType(), message.getText(), new Date()); } @Override public int getOrder() { return this.order; } @NotNull @Override public String getName() { return "SaveAdvisor"; } public static SaveAdvisor.Builder builder() { return new SaveAdvisor.Builder(); } public static final class Builder { private int order = 0; private String conversationId = "def"; public Builder() { } public SaveAdvisor.Builder conversationId(String conversationId) { this.conversationId = conversationId; return this; } public SaveAdvisor.Builder order(int order) { this.order = order; return this; } public SaveAdvisor build() { return new SaveAdvisor(this.order, this.conversationId); } } }

4、基于redis 存储聊天记忆

这里使用的是spring Alibaba 提供的ChatMemoryRepository。

文档所在地址 http://github地址 https://github.com/alibaba/spring-ai-alibaba/tree/1.0.0.3-retriever/community/memories/spring-ai-alibaba-starter-memory-redis

使用版本是1.1.0.0 但是可以查看分支1.0.0.3的文档README

4.1 环境准备

  • 安装redis
docker run -d --name redis-simple -p 16379:6379 redis:7.2.4 redis-server --bind 0.0.0.0 --requirepass "123456" --appendonly yes

4.2 导入依赖

<properties> <java.version>17</java.version> <spring-ai.version>1.1.2</spring-ai.version> <spring-ai-alibaba.version>1.1.0.0</spring-ai-alibaba.version> </properties> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-memory-redis</artifactId> <version>${spring-ai-alibaba.version}</version> </dependency>

4.3 bean 配置

import com.alibaba.cloud.ai.memory.redis.LettuceRedisChatMemoryRepository; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedisChatMemoryConfig { @Bean(name = "lettuceRedisChatMemoryRepository") public LettuceRedisChatMemoryRepository lettuceRedisChatMemoryRepository() { // 创建连接池配置对象 GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>(); // 最大活跃连接数 poolConfig.setMaxTotal(8); // 最大空闲连接数 poolConfig.setMaxIdle(8); // 最小空闲连接数 poolConfig.setMinIdle(2); return LettuceRedisChatMemoryRepository.builder() // redis 地址 .host("127.0.0.1") // 端口 .port(16379) // 密码 .password("123456") // 连接超时时间 .timeout(10000) // 连接池配置对象 .poolConfig(poolConfig) .build(); } @Bean("redisChatMemory") ChatMemory redisChatMemory(@Qualifier("lettuceRedisChatMemoryRepository") LettuceRedisChatMemoryRepository repository) { return MessageWindowChatMemory.builder() // 使用数据库存储 .chatMemoryRepository(repository) // 保留最多10条记录 .maxMessages(10) .build(); } }

4.4 基础使用

import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; @Slf4j @RestController @RequiredArgsConstructor @RequestMapping("chat/memory/redis") public class RedisChatMemoryController { private final ChatClient dashscopeChatClient; private final ChatMemory redisChatMemory; @GetMapping("base") public String base() { // 会话编号 每个会话一个会话编号 用于会话记忆隔离 String conversationId = UUID.randomUUID().toString().replace("-", ""); log.info("conversationId: {}", conversationId); String one = dashscopeChatClient.prompt() .user("记住北京今天的天气是晴天,26摄氏度") .advisors( MessageChatMemoryAdvisor.builder(redisChatMemory) .conversationId(conversationId) .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第一次回复消息 {}", one); String two = dashscopeChatClient.prompt() .user("北京今天的天气是什么样的") .advisors( MessageChatMemoryAdvisor.builder(redisChatMemory) .conversationId(conversationId) .build(), SimpleLoggerAdvisor.builder().build() ).call().content(); log.info("第二次回复消息 {}", two); return "ok"; } }

4.5 redis存储结果

Read more

Flutter for OpenHarmony:queue 异步任务队列管理(并发控制与任务调度) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:queue 异步任务队列管理(并发控制与任务调度) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在 Dart 中,异步操作(Future)通常是并发执行的。如果你在一个 for 循环里发起了 100 个网络请求: for(var url in urls){fetch(url);// 瞬间发出100个请求} 这会导致什么? 1. 服务器爆炸:可能触发 API速率限制(429 Too Many Requests)。 2. 客户端OOM:瞬间创建过多的 Socket 连接和 Buffer。 3. UI 卡顿:大量的 Event Loop 任务阻塞。 我们需要一种机制来限制并发数,或者让任务串行执行。 queue

By Ne0inhk
Flutter 三方库 gtin_toolkit 的鸿蒙化适配指南 - 实现全球标准商品条码(GTIN)的正向解析与合法性校检、支持端侧零售与物流供应链扫码实战

Flutter 三方库 gtin_toolkit 的鸿蒙化适配指南 - 实现全球标准商品条码(GTIN)的正向解析与合法性校检、支持端侧零售与物流供应链扫码实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 gtin_toolkit 的鸿蒙化适配指南 - 实现全球标准商品条码(GTIN)的正向解析与合法性校检、支持端侧零售与物流供应链扫码实战 前言 在进行 Flutter for OpenHarmony 的新零售、仓储管理或跨境物流应用开发时,如何准确识别并验证全球通用的商品条码?GTIN(Global Trade Item Number)涵盖了 EAN-13, EAN-8, UPC-A, UPC-E 以及 ITF-14 等多种格式。gtin_toolkit 是一款专为 GTIN 协议处理设计的工具库。它不仅能解析条码,还能计算动态校检位(Check Digit)。本文将介绍如何在鸿蒙端构建极致的条码数据治理能力。 一、原直观解析 / 概念介绍 1.

By Ne0inhk
Flutter for OpenHarmony:web_socket 纯 Dart 标准 WebSocket 客户端(跨平台兼容性之王) 深度解析与鸿蒙

Flutter for OpenHarmony:web_socket 纯 Dart 标准 WebSocket 客户端(跨平台兼容性之王) 深度解析与鸿蒙

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 虽然 dart:io 提供了 WebSocket 类,dart:html 也提供了 WebSocket 类,但这种“分裂”的 API 设计让编写跨平台(同时支持 Mobile/Web/Desktop)的代码变得异常痛苦。你需要使用条件导入 (if (dart.library.io) ...) 来分别处理。 web_socket 库就是为了解决这个问题而诞生的。它提供了一个统一的、平台无关的WebSocket 接口。 无论你的代码运行在 Android、iOS、Web 还是 OpenHarmony 上,它都会自动选择最底层的实现(在鸿蒙上通常是 dart:io)

By Ne0inhk
Flutter 组件 sse_stream 的适配 鸿蒙Harmony 实战 - 驾驭高性能 Server-Sent Events 流、实现鸿蒙端实时数据推送与长连接保活优化方案

Flutter 组件 sse_stream 的适配 鸿蒙Harmony 实战 - 驾驭高性能 Server-Sent Events 流、实现鸿蒙端实时数据推送与长连接保活优化方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 sse_stream 的适配 鸿蒙Harmony 实战 - 驾驭高性能 Server-Sent Events 流、实现鸿蒙端实时数据推送与长连接保活优化方案 前言 在鸿蒙(OpenHarmony)生态的即时性应用场景中,如金融级实时行情、直播间弹幕以及 AI 模型的流式回复(Streaming Response),我们需要一种比轮询更高效、比 WebSocket 更轻量的数据下发机制。 SSE(Server-Sent Events)作为 HTML5 规范下的长连接利器,以其对 HTTP 协议的完美兼容和自动重连的天生特性,在现代移动开发中大放异彩。 sse_stream 库为 Flutter 提供了精简且强大的 SSE 接入能力。在鸿蒙适配实战中,

By Ne0inhk