
controller
package com.xmkjsoft.ssegpt;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@CrossOrigin
@RestController
@RequestMapping("/chat")
public class ChatController {
@Value("${deepseek.api.key}")
private String apiKey;
@Value("${deepseek.api.url}")
private String apiUrl;
// 简单保存 prompt 用,实际可优化
private final ConcurrentHashMap<String, String> promptMap = new ConcurrentHashMap<>();
@PostMapping("/start")
public StartResponse startChat(@RequestBody ChatRequest userRequest) {
String id = UUID.randomUUID().toString();
promptMap.put(id, userRequest.getPrompt());
return new StartResponse(id);
}
@GetMapping("/stream")
public SseEmitter stream(@RequestParam String id) {
SseEmitter emitter = new SseEmitter(0L);
String prompt = promptMap.get(id);
if (prompt == null) {
emitter.completeWithError(new IllegalArgumentException("无效的id"));
return emitter;
}
new Thread(() -> {
try {
String json = """
{
"model": "deepseek-chat",
"stream": true,
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "%s" }
]
}
""".formatted(prompt);
URL url = new URL(apiUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
conn.setDoOutput(true);
conn.getOutputStream().write(json.getBytes());
try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("data: ")) {
String data = line.substring("data: ".length());
if ("[DONE]".equals(data)) break;
emitter.send(data);
}
}
}
emitter.complete();
promptMap.remove(id);
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
@Data
public static class ChatRequest {
private String prompt;
}
@Data
public static class StartResponse {
private final String id;
}
}
application.properties
spring.application.name=ssegpt
#deepseek.model=deepseek-reasoner
deepseek.model=deepseek-chat
deepseek.api.key=自己去获取
deepseek.api.url=https://api.deepseek.com/v1/chat/completions
index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>SSE Chat Demo</title>
</head>
<body>
<textarea id="input" rows="3" cols="40" placeholder="输入你的问题"></textarea><br/>
<button id="sendBtn">发送</button>
<div id="output" style="white-space: pre-wrap; border: 1px solid #ccc; padding: 10px; margin-top: 10px;"></div>
<script>
document.getElementById('sendBtn').onclick = () => {
const prompt = document.getElementById('input').value.trim();
if (!prompt) return alert("请输入内容");
fetch('/chat/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
}).then(res => res.json())
.then(data => {
if (!data.id) throw new Error("启动失败");
listenSSE(data.id);
}).catch(err => alert("启动错误:" + err));
};
function listenSSE(id) {
const evtSource = new EventSource('/chat/stream?id=' + encodeURIComponent(id));
const output = document.getElementById('output');
output.textContent = ''; // 清空之前内容
evtSource.onmessage = (e) => {
if (e.data === '[DONE]') {
evtSource.close();
return;
}
try {
const json = JSON.parse(e.data);
const content = json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content;
if (content !== undefined) {
output.textContent += content;
}
} catch (err) {
output.textContent += e.data;
}
};
evtSource.onerror = (e) => {
console.error("连接错误", e);
evtSource.close();
};
}
</script>
</body>
</html>
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>