9 天以前 d640da3dac5b5f811284ab9a7c386da1e7ab6739
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
@@ -2,35 +2,161 @@
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 {
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) {
    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) {
        String directResponse = approveTodoIntentExecutor.tryExecute(chatForm.getMessage());
        if (directResponse != null) {
        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(chatForm.getMemoryId(), chatForm.getMessage());
        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()));
    }
}