liyong
18 小时以前 2b382a92207dfabf0eb30e743265df5c7c50e7bc
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package com.ruoyi.ai.controller;
 
import com.ruoyi.ai.assistant.ApproveTodoAgent;
import com.ruoyi.ai.assistant.ApproveTodoIntentExecutor;
import com.ruoyi.ai.assistant.FileAnalyzeAgent;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.service.AiFileTextExtractor;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
 
import java.io.IOException;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;
 
@Tag(name = "协同办公助手")
@RestController
@RequestMapping("/xiaozhi")
public class XiaozhiController extends BaseController {
 
    private static final String FILE_ANALYZE_MEMORY_PREFIX = "file-analyze::";
 
    private final ApproveTodoAgent approveTodoAgent;
    private final ApproveTodoIntentExecutor approveTodoIntentExecutor;
    private final FileAnalyzeAgent fileAnalyzeAgent;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiFileTextExtractor aiFileTextExtractor;
    private final AiChatSessionService aiChatSessionService;
 
    public XiaozhiController(ApproveTodoAgent approveTodoAgent,
                             ApproveTodoIntentExecutor approveTodoIntentExecutor,
                             FileAnalyzeAgent fileAnalyzeAgent,
                             AiSessionUserContext aiSessionUserContext,
                             MongoChatMemoryStore mongoChatMemoryStore,
                             AiFileTextExtractor aiFileTextExtractor,
                             AiChatSessionService aiChatSessionService) {
        this.approveTodoAgent = approveTodoAgent;
        this.approveTodoIntentExecutor = approveTodoIntentExecutor;
        this.fileAnalyzeAgent = fileAnalyzeAgent;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiFileTextExtractor = aiFileTextExtractor;
        this.aiChatSessionService = aiChatSessionService;
    }
 
    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
 
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
 
        String directResponse = approveTodoIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
 
        return approveTodoAgent.chat(memoryId, userMessage)
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
 
    @Operation(summary = "上传文件分析")
    @PostMapping(value = "/analyze-file", consumes = "multipart/form-data", produces = "text/stream;charset=utf-8")
    public Flux<String> analyzeFile(@RequestParam("file") MultipartFile file,
                                    @RequestParam(value = "message", required = false) String message,
                                    @RequestParam(value = "memoryId", required = false) String memoryId) {
        String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
        String finalMemoryId = rawMemoryId.startsWith(FILE_ANALYZE_MEMORY_PREFIX)
                ? rawMemoryId
                : FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
 
        LoginUser loginUser = SecurityUtils.getLoginUser();
        aiSessionUserContext.bind(finalMemoryId, loginUser);
 
        String finalMessage = StringUtils.hasText(message) ? message : "请分析这个文件的核心内容并给出总结";
        String fileText;
        try {
            fileText = aiFileTextExtractor.extractText(file);
        } catch (IllegalArgumentException ex) {
            return Flux.just(ex.getMessage());
        } catch (IOException ex) {
            return Flux.just("文件读取失败");
        }
 
        if (!StringUtils.hasText(fileText)) {
            return Flux.just("未提取到有效文件内容");
        }
 
        String limitedContent = fileText.length() > 12000 ? fileText.substring(0, 12000) : fileText;
        String userPrompt = "你将分析用户上传的文件内容。\n"
                + "文件名: " + file.getOriginalFilename() + "\n"
                + "用户问题: " + finalMessage + "\n"
                + "文件内容如下:\n"
                + limitedContent;
 
        aiChatSessionService.touchSession(finalMemoryId, loginUser, "文件分析: " + finalMessage);
 
        return Flux.defer(() -> fileAnalyzeAgent.chat(finalMemoryId, userPrompt))
                .onErrorResume(NoSuchElementException.class, ex -> {
                    // DashScope 在历史消息兼容场景下可能抛 NoSuchElementException,清空后重试一次
                    mongoChatMemoryStore.deleteMessages(finalMemoryId);
                    return fileAnalyzeAgent.chat(finalMemoryId, userPrompt);
                })
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
    }
 
    @Operation(summary = "会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
    }
 
    @Operation(summary = "会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
    }
 
    @Operation(summary = "删除会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
}