zhou zhou
16 小时以前 80a6d9236ade191a5de0975abe4de5a6e7e63915
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.vincent.rsf.server.ai.controller;
 
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
import com.vincent.rsf.server.ai.dto.AiChatRequest;
import com.vincent.rsf.server.ai.service.AiChatService;
import com.vincent.rsf.server.system.controller.BaseController;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
import java.util.UUID;
 
@RestController
@Slf4j
@RequiredArgsConstructor
public class AiChatController extends BaseController {
 
    private final AiChatService aiChatService;
 
    /**
     * 返回当前用户在指定 Prompt 场景下的 AI 运行时快照。
     * 这里不会真正触发模型调用,只负责把当前生效的模型、Prompt、
     * 已挂载 MCP 以及会话记忆概况一次性返回给前端抽屉初始化使用。
     */
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/ai/chat/runtime")
    public R runtime(@RequestParam(required = false) String promptCode,
                     @RequestParam(required = false) Long sessionId) {
        return R.ok().add(aiChatService.getRuntime(promptCode, sessionId, getLoginUserId(), getTenantId()));
    }
 
    /**
     * 查询当前登录用户在指定 Prompt 下的历史会话列表。
     * 前端左侧会话栏依赖该接口做会话切换、搜索和刷新。
     */
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/ai/chat/sessions")
    public R sessions(@RequestParam(required = false) String promptCode,
                      @RequestParam(required = false) String keyword) {
        return R.ok().add(aiChatService.listSessions(promptCode, keyword, getLoginUserId(), getTenantId()));
    }
 
    /**
     * 软删除单个 AI 会话,同时级联删除会话下的消息记录。
     */
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/ai/chat/session/remove/{sessionId}")
    public R removeSession(@PathVariable Long sessionId) {
        aiChatService.removeSession(sessionId, getLoginUserId(), getTenantId());
        return R.ok("Delete Success").add(sessionId);
    }
 
    /**
     * 更新会话标题,供前端重命名会话时调用。
     */
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/ai/chat/session/rename/{sessionId}")
    public R renameSession(@PathVariable Long sessionId, @RequestBody AiChatSessionRenameRequest request) {
        return R.ok("Update Success").add(aiChatService.renameSession(sessionId, request, getLoginUserId(), getTenantId()));
    }
 
    /**
     * 更新会话置顶状态。
     * 置顶只影响当前用户的会话排序,不改变会话内容和记忆。
     */
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/ai/chat/session/pin/{sessionId}")
    public R pinSession(@PathVariable Long sessionId, @RequestBody AiChatSessionPinRequest request) {
        return R.ok("Update Success").add(aiChatService.pinSession(sessionId, request, getLoginUserId(), getTenantId()));
    }
 
    /**
     * 清空指定会话的持久化消息、摘要记忆和事实记忆。
     * 会话本身保留,便于前端继续在同一个 sessionId 上发起新对话。
     */
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/ai/chat/session/memory/clear/{sessionId}")
    public R clearSessionMemory(@PathVariable Long sessionId) {
        aiChatService.clearSessionMemory(sessionId, getLoginUserId(), getTenantId());
        return R.ok("Clear Success").add(sessionId);
    }
 
    /**
     * 只保留会话最近一轮问答,用于主动裁剪上下文窗口。
     */
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/ai/chat/session/memory/retain-latest/{sessionId}")
    public R retainLatestRound(@PathVariable Long sessionId) {
        aiChatService.retainLatestRound(sessionId, getLoginUserId(), getTenantId());
        return R.ok("Retain Success").add(sessionId);
    }
 
    /**
     * 以 SSE 方式启动 AI 对话。
     * 控制器只负责生成 requestId、记录入口日志和把鉴权上下文透传给服务层,
     * 真正的流式推理、工具调用和记忆落库都在服务层完成。
     */
    @PreAuthorize("isAuthenticated()")
    @PostMapping(value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter stream(@RequestBody AiChatRequest request) {
        String requestId = StringUtils.hasText(request.getRequestId())
                ? request.getRequestId().trim()
                : UUID.randomUUID().toString().replace("-", "");
        request.setRequestId(requestId);
        log.info("AI chat request accepted, requestId={}, userId={}, tenantId={}, sessionId={}",
                requestId, getLoginUserId(), getTenantId(), request.getSessionId());
        return aiChatService.stream(request, getLoginUserId(), getTenantId());
    }
}