|
@@ -6,9 +6,10 @@ import com.pharmacopoeia.dto.FeedbackRequest;
|
|
|
import com.pharmacopoeia.dto.ImageChatRequest;
|
|
import com.pharmacopoeia.dto.ImageChatRequest;
|
|
|
import com.pharmacopoeia.dto.MultimodalChatRequest;
|
|
import com.pharmacopoeia.dto.MultimodalChatRequest;
|
|
|
import com.pharmacopoeia.service.*;
|
|
import com.pharmacopoeia.service.*;
|
|
|
-import org.springframework.http.MediaType;
|
|
|
|
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.http.codec.ServerSentEvent;
|
|
import org.springframework.http.codec.ServerSentEvent;
|
|
|
|
|
+import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
import reactor.core.publisher.Flux;
|
|
import reactor.core.publisher.Flux;
|
|
|
import reactor.core.publisher.Sinks;
|
|
import reactor.core.publisher.Sinks;
|
|
@@ -33,11 +34,13 @@ public class ChatController {
|
|
|
private final QACacheService qaCache;
|
|
private final QACacheService qaCache;
|
|
|
private final JdbcTemplate jdbc;
|
|
private final JdbcTemplate jdbc;
|
|
|
private final QwenProperties props;
|
|
private final QwenProperties props;
|
|
|
|
|
+ private final HttpServletRequest request;
|
|
|
|
|
|
|
|
public ChatController(RetrieverService rs, LLMService ls, PromptService ps,
|
|
public ChatController(RetrieverService rs, LLMService ls, PromptService ps,
|
|
|
ChatPersistenceService cps, RerankerService rrs,
|
|
ChatPersistenceService cps, RerankerService rrs,
|
|
|
QACacheService qaCache,
|
|
QACacheService qaCache,
|
|
|
- JdbcTemplate jdbc, QwenProperties props) {
|
|
|
|
|
|
|
+ JdbcTemplate jdbc, QwenProperties props,
|
|
|
|
|
+ HttpServletRequest request) {
|
|
|
this.retrieverService = rs;
|
|
this.retrieverService = rs;
|
|
|
this.llmService = ls;
|
|
this.llmService = ls;
|
|
|
this.promptService = ps;
|
|
this.promptService = ps;
|
|
@@ -46,6 +49,7 @@ public class ChatController {
|
|
|
this.qaCache = qaCache;
|
|
this.qaCache = qaCache;
|
|
|
this.jdbc = jdbc;
|
|
this.jdbc = jdbc;
|
|
|
this.props = props;
|
|
this.props = props;
|
|
|
|
|
+ this.request = request;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@PostMapping("/ask")
|
|
@PostMapping("/ask")
|
|
@@ -63,8 +67,8 @@ public class ChatController {
|
|
|
String intent = (String) cached.getOrDefault("intent", "");
|
|
String intent = (String) cached.getOrDefault("intent", "");
|
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
|
List<Map<String, Object>> sources = (List<Map<String, Object>>) cached.getOrDefault("sources", List.of());
|
|
List<Map<String, Object>> sources = (List<Map<String, Object>>) cached.getOrDefault("sources", List.of());
|
|
|
- persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
|
|
- persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user", query, intent, null);
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", answer, intent, sources);
|
|
|
return ResponseEntity.ok(Map.of(
|
|
return ResponseEntity.ok(Map.of(
|
|
|
"answer", answer,
|
|
"answer", answer,
|
|
|
"sources", sources,
|
|
"sources", sources,
|
|
@@ -82,8 +86,8 @@ public class ChatController {
|
|
|
String intent = (String) waited.getOrDefault("intent", "");
|
|
String intent = (String) waited.getOrDefault("intent", "");
|
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
|
List<Map<String, Object>> sources = (List<Map<String, Object>>) waited.getOrDefault("sources", List.of());
|
|
List<Map<String, Object>> sources = (List<Map<String, Object>>) waited.getOrDefault("sources", List.of());
|
|
|
- persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
|
|
- persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user", query, intent, null);
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", answer, intent, sources);
|
|
|
return ResponseEntity.ok(Map.of(
|
|
return ResponseEntity.ok(Map.of(
|
|
|
"answer", answer, "sources", sources, "intent", intent,
|
|
"answer", answer, "sources", sources, "intent", intent,
|
|
|
"conversation_id", cid, "cached", true
|
|
"conversation_id", cid, "cached", true
|
|
@@ -101,8 +105,8 @@ public class ChatController {
|
|
|
String answer = llmAnswer;
|
|
String answer = llmAnswer;
|
|
|
|
|
|
|
|
List<Map<String, Object>> sources = buildSources(docs);
|
|
List<Map<String, Object>> sources = buildSources(docs);
|
|
|
- persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
|
|
- persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user", query, intent, null);
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
|
// 写入全局缓存
|
|
// 写入全局缓存
|
|
|
qaCache.put(normalized, answer, intent, sources);
|
|
qaCache.put(normalized, answer, intent, sources);
|
|
@@ -168,8 +172,8 @@ public class ChatController {
|
|
|
} catch (Exception ignored) {}
|
|
} catch (Exception ignored) {}
|
|
|
|
|
|
|
|
String finalAnswer = cleanAnswer(fullAnswer.toString());
|
|
String finalAnswer = cleanAnswer(fullAnswer.toString());
|
|
|
- persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
|
|
- persistenceService.saveMessage(cid, "assistant", finalAnswer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user", query, intent, null);
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", finalAnswer, intent, sources);
|
|
|
|
|
|
|
|
// 写入全局缓存
|
|
// 写入全局缓存
|
|
|
qaCache.put(normalized, finalAnswer, intent, sources);
|
|
qaCache.put(normalized, finalAnswer, intent, sources);
|
|
@@ -191,8 +195,8 @@ public class ChatController {
|
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
|
List<Map<String, Object>> sources = (List<Map<String, Object>>) cached.getOrDefault("sources", List.of());
|
|
List<Map<String, Object>> sources = (List<Map<String, Object>>) cached.getOrDefault("sources", List.of());
|
|
|
|
|
|
|
|
- persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
|
|
- persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user", query, intent, null);
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
|
return Flux.create(sink -> {
|
|
return Flux.create(sink -> {
|
|
|
sink.next(ServerSentEvent.<String>builder().event("intent").data(intent).build());
|
|
sink.next(ServerSentEvent.<String>builder().event("intent").data(intent).build());
|
|
@@ -245,10 +249,10 @@ public class ChatController {
|
|
|
String answer = llmAnswer;
|
|
String answer = llmAnswer;
|
|
|
|
|
|
|
|
List<Map<String, Object>> sources = buildSources(docs);
|
|
List<Map<String, Object>> sources = buildSources(docs);
|
|
|
- persistenceService.saveMessage(cid, "user",
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user",
|
|
|
request.getMessage().isBlank() ? "[图片]" : "[图片] " + request.getMessage(),
|
|
request.getMessage().isBlank() ? "[图片]" : "[图片] " + request.getMessage(),
|
|
|
intent, null);
|
|
intent, null);
|
|
|
- persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
|
return ResponseEntity.ok(Map.of(
|
|
return ResponseEntity.ok(Map.of(
|
|
|
"answer", answer,
|
|
"answer", answer,
|
|
@@ -322,10 +326,10 @@ public class ChatController {
|
|
|
} catch (Exception ignored) {}
|
|
} catch (Exception ignored) {}
|
|
|
|
|
|
|
|
String finalAnswer = cleanAnswer(fullAnswer.toString());
|
|
String finalAnswer = cleanAnswer(fullAnswer.toString());
|
|
|
- persistenceService.saveMessage(cid, "user",
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user",
|
|
|
request.getMessage().isBlank() ? "[图片]" : "[图片] " + request.getMessage(),
|
|
request.getMessage().isBlank() ? "[图片]" : "[图片] " + request.getMessage(),
|
|
|
intent, null);
|
|
intent, null);
|
|
|
- persistenceService.saveMessage(cid, "assistant", finalAnswer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", finalAnswer, intent, sources);
|
|
|
sink.tryEmitComplete();
|
|
sink.tryEmitComplete();
|
|
|
})
|
|
})
|
|
|
.doOnError(sink::tryEmitError)
|
|
.doOnError(sink::tryEmitError)
|
|
@@ -390,8 +394,8 @@ public class ChatController {
|
|
|
List<Map<String, Object>> sources = buildSources(docs);
|
|
List<Map<String, Object>> sources = buildSources(docs);
|
|
|
String userMsg = !request.getMessage().isBlank() ? request.getMessage()
|
|
String userMsg = !request.getMessage().isBlank() ? request.getMessage()
|
|
|
: !ocrText.isEmpty() ? "[" + mediaLabel + "]" : request.getMessage();
|
|
: !ocrText.isEmpty() ? "[" + mediaLabel + "]" : request.getMessage();
|
|
|
- persistenceService.saveMessage(cid, "user", userMsg, intent, null);
|
|
|
|
|
- persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user", userMsg, intent, null);
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
|
|
return ResponseEntity.ok(Map.of(
|
|
return ResponseEntity.ok(Map.of(
|
|
|
"answer", answer,
|
|
"answer", answer,
|
|
@@ -585,8 +589,8 @@ public class ChatController {
|
|
|
String finalAnswer = cleanAnswer(fullAnswer.toString());
|
|
String finalAnswer = cleanAnswer(fullAnswer.toString());
|
|
|
String userMsg = !request.getMessage().isBlank() ? request.getMessage()
|
|
String userMsg = !request.getMessage().isBlank() ? request.getMessage()
|
|
|
: !ocrText.isEmpty() ? "[" + mediaLabel + "]" : "";
|
|
: !ocrText.isEmpty() ? "[" + mediaLabel + "]" : "";
|
|
|
- persistenceService.saveMessage(cid, "user", userMsg, intent, null);
|
|
|
|
|
- persistenceService.saveMessage(cid, "assistant", finalAnswer, intent, sources);
|
|
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "user", userMsg, intent, null);
|
|
|
|
|
+ persistenceService.saveMessage(getCurrentUserKey(), cid, "assistant", finalAnswer, intent, sources);
|
|
|
sink.tryEmitComplete();
|
|
sink.tryEmitComplete();
|
|
|
})
|
|
})
|
|
|
.doOnError(sink::tryEmitError)
|
|
.doOnError(sink::tryEmitError)
|
|
@@ -599,7 +603,7 @@ public class ChatController {
|
|
|
@RequestParam(defaultValue = "1") int page,
|
|
@RequestParam(defaultValue = "1") int page,
|
|
|
@RequestParam(defaultValue = "20") int pageSize) {
|
|
@RequestParam(defaultValue = "20") int pageSize) {
|
|
|
// getHistory 现在直接返回包含 items/page/page_size/total/total_pages 的 Map
|
|
// getHistory 现在直接返回包含 items/page/page_size/total/total_pages 的 Map
|
|
|
- var result = persistenceService.getHistory(page, pageSize);
|
|
|
|
|
|
|
+ var result = persistenceService.getHistory(getCurrentUserKey(), page, pageSize);
|
|
|
return ResponseEntity.ok(result);
|
|
return ResponseEntity.ok(result);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -609,6 +613,14 @@ public class ChatController {
|
|
|
return ResponseEntity.ok(Map.of("conversation_id", cid, "messages", msgs));
|
|
return ResponseEntity.ok(Map.of("conversation_id", cid, "messages", msgs));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 返回最近 N 条消息,供前端恢复对话(微信 WebView 等 IndexedDB 不可用场景) */
|
|
|
|
|
+ @GetMapping("/recent-messages")
|
|
|
|
|
+ public ResponseEntity<Map<String, Object>> getRecentMessages(
|
|
|
|
|
+ @RequestParam(defaultValue = "50") int limit) {
|
|
|
|
|
+ var msgs = persistenceService.getRecentMessages(getCurrentUserKey(), Math.min(limit, 200));
|
|
|
|
|
+ return ResponseEntity.ok(Map.of("messages", msgs));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@PostMapping("/feedback")
|
|
@PostMapping("/feedback")
|
|
|
public ResponseEntity<Map<String, Object>> submitFeedback(@RequestBody FeedbackRequest request) {
|
|
public ResponseEntity<Map<String, Object>> submitFeedback(@RequestBody FeedbackRequest request) {
|
|
|
persistenceService.updateFeedback(request.getMessageId(), request.getFeedback());
|
|
persistenceService.updateFeedback(request.getMessageId(), request.getFeedback());
|
|
@@ -751,6 +763,21 @@ public class ChatController {
|
|
|
return storedSection;
|
|
return storedSection;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 从 SecurityContext 获取当前用户标识(JWT subject),未登录则用 IP 隔离 */
|
|
|
|
|
+ private String getCurrentUserKey() {
|
|
|
|
|
+ var auth = SecurityContextHolder.getContext().getAuthentication();
|
|
|
|
|
+ if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) {
|
|
|
|
|
+ return auth.getName();
|
|
|
|
|
+ }
|
|
|
|
|
+ // 未登录用 IP 隔离,避免不同手机会话串了
|
|
|
|
|
+ String ip = request.getRemoteAddr();
|
|
|
|
|
+ String forwarded = request.getHeader("X-Forwarded-For");
|
|
|
|
|
+ if (forwarded != null && !forwarded.isBlank()) {
|
|
|
|
|
+ ip = forwarded.split(",")[0].trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ return "ip:" + ip;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private String cleanAnswer(String text) {
|
|
private String cleanAnswer(String text) {
|
|
|
if (text == null) {
|
|
if (text == null) {
|
|
|
return "";
|
|
return "";
|