有限的文本生成任务需要明确的完成事件。下面使用 JSON 包装文本片段,保留空格和换行,收到 done 才确认流正常结束;断线单独显示为错误。
可运行的 WebFlux 接口
在 Java 17+、Spring Boot 3.x 的 WebFlux 项目中使用 spring-boot-starter-webflux,保存为启动类扫描包下的 TextStreamController.java。此例不依赖 AI 服务,先用三段固定文本验证传输。
package example;
import java.time.Duration;import java.util.Map;import org.springframework.http.MediaType;import org.springframework.http.codec.ServerSentEvent;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;
@RestControllerpublic class TextStreamController { @GetMapping(value = "/text-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<Map<String, String>>> stream() { return Flux.just("hello", " ", "world\n") .delayElements(Duration.ofMillis(200)) .map(chunk -> ServerSentEvent.<Map<String, String>>builder(Map.of("d", chunk)).build()) .concatWith(Mono.just(ServerSentEvent.<Map<String, String>>builder(Map.of("d", "")) .event("done").build())); }}启动后用 curl.exe -N http://localhost:8080/text-stream 查看响应,应包含三条 JSON data 和一条 done。concatWith 只在上游正常完成后追加 done,上游报错时不会产生这个完成事件。
浏览器接收
把下面脚本放在与接口同源的网页中,页面需要 <pre id="result"></pre> 和 <p id="status"></p>:
const output = document.getElementById('result')const status = document.getElementById('status')const source = new EventSource('/text-stream')let finished = falsesource.onmessage = event => { try { const data = JSON.parse(event.data) if (typeof data.d !== 'string') throw new Error('文本片段格式不正确') output.textContent += data.d } catch (error) { source.close() status.textContent = error.message }}source.addEventListener('done', () => { finished = true source.close() status.textContent = '生成完成'})source.onerror = () => { if (!finished) { source.close() status.textContent = '连接中断,已接收文本保留,请检查任务状态' }}window.addEventListener('pagehide', () => source.close())应看到 hello world,末尾保留换行。关闭服务器导致的错误不能触发“生成完成”。这里主动关闭错误连接,避免重连重复启动任务;需要恢复时,应把“创建任务”和“订阅已有任务”分开,并设计事件编号和去重。
换成 AI 流时,将 Flux.just(...) 替换为自己的 Flux<String>。如果 done 还代表文件已保存,就必须把保存过程接入流的完成链;固定等待 1 秒不能保证数据库或文件已经写完。
评论