From bffb8a9ba441e58a45d8b758cf2101f7f9508d5e Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Thu, 22 May 2025 13:31:56 +0800 Subject: [PATCH] add md render --- .../mjga/config/ai/DeepSeekConfiguration.java | 19 +++++++ .../com/zl/mjga/controller/AiController.java | 5 +- backend/src/main/resources/ai.yml | 4 +- backend/src/main/resources/prompt.txt | 1 + frontend/package-lock.json | 49 +++++++++++++++++++ frontend/package.json | 8 ++- frontend/src/views/AiChatView.vue | 27 ++++++++-- 7 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 backend/src/main/resources/prompt.txt diff --git a/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java index 02b8b74..f1d58c3 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java @@ -1,14 +1,25 @@ package com.zl.mjga.config.ai; +import jakarta.annotation.PostConstruct; import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + @Data @Component @ConfigurationProperties(prefix = "deep-seek") public class DeepSeekConfiguration { + @jakarta.annotation.Resource + private ResourceLoader resourceLoader; + private String baseUrl; private String apiKey; private Prompt prompt; @@ -18,4 +29,12 @@ public class DeepSeekConfiguration { public static class Prompt { private String system; } + + @PostConstruct + public void init() throws IOException { + Resource resource = resourceLoader.getResource("classpath:prompt.txt"); + prompt = new Prompt(); + prompt.setSystem(Files.readString(Paths.get(resource.getURI()))); + } } + diff --git a/backend/src/main/java/com/zl/mjga/controller/AiController.java b/backend/src/main/java/com/zl/mjga/controller/AiController.java index e137ce1..e554731 100644 --- a/backend/src/main/java/com/zl/mjga/controller/AiController.java +++ b/backend/src/main/java/com/zl/mjga/controller/AiController.java @@ -8,7 +8,6 @@ import java.time.Duration; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; @@ -34,8 +33,6 @@ public class AiController { .onError(sink::tryEmitError) .start(); return sink.asFlux() - .timeout(Duration.ofSeconds(120)) - .doOnCancel(SecurityContextHolder::clearContext) - .doOnTerminate(SecurityContextHolder::clearContext); + .timeout(Duration.ofSeconds(120)); } } diff --git a/backend/src/main/resources/ai.yml b/backend/src/main/resources/ai.yml index 368bc8a..af6a6cf 100644 --- a/backend/src/main/resources/ai.yml +++ b/backend/src/main/resources/ai.yml @@ -1,6 +1,4 @@ deep-seek: base-url: "https://api.deepseek.com" api-key: "sk-3633b0cd40884b27aa8402a1c5dc029d" - model-name: "deepseek-chat" - prompt: - system: "你是一个名叫「知路智能体」的企业级AI助手,能帮助用户解决各种问题。" + model-name: "deepseek-chat" \ No newline at end of file diff --git a/backend/src/main/resources/prompt.txt b/backend/src/main/resources/prompt.txt new file mode 100644 index 0000000..fa059ed --- /dev/null +++ b/backend/src/main/resources/prompt.txt @@ -0,0 +1 @@ +你是一个名为「知路智能体」的企业级AI助手,严格遵循使用Markdown格式来回复内容。 \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9328407..07fc07c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,9 @@ "@vueuse/core": "^13.0.0", "apexcharts": "^3.46.0", "dayjs": "^1.11.13", + "dompurify": "^3.2.6", "flowbite": "^3.1.2", + "marked": "^15.0.12", "openapi-fetch": "^0.13.5", "pinia": "^3.0.1", "tailwindcss": "^4.0.14", @@ -26,6 +28,8 @@ "@faker-js/faker": "^9.6.0", "@playwright/test": "^1.51.0", "@tsconfig/node22": "^22.0.0", + "@types/dompurify": "^3.0.5", + "@types/marked": "^5.0.2", "@types/node": "^22.13.9", "@vitejs/plugin-vue": "^5.2.1", "@vitest/browser": "^3.0.9", @@ -2127,12 +2131,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "http://mirrors.tencent.com/npm/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "license": "MIT" }, + "node_modules/@types/marked": { + "version": "5.0.2", + "resolved": "http://mirrors.tencent.com/npm/@types/marked/-/marked-5.0.2.tgz", + "integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.15.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", @@ -2163,6 +2184,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "http://mirrors.tencent.com/npm/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", @@ -3327,6 +3355,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "http://mirrors.tencent.com/npm/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4424,6 +4461,18 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "http://mirrors.tencent.com/npm/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index f98c958..7ca8526 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,7 +23,9 @@ "@vueuse/core": "^13.0.0", "apexcharts": "^3.46.0", "dayjs": "^1.11.13", + "dompurify": "^3.2.6", "flowbite": "^3.1.2", + "marked": "^15.0.12", "openapi-fetch": "^0.13.5", "pinia": "^3.0.1", "tailwindcss": "^4.0.14", @@ -36,6 +38,8 @@ "@faker-js/faker": "^9.6.0", "@playwright/test": "^1.51.0", "@tsconfig/node22": "^22.0.0", + "@types/dompurify": "^3.0.5", + "@types/marked": "^5.0.2", "@types/node": "^22.13.9", "@vitejs/plugin-vue": "^5.2.1", "@vitest/browser": "^3.0.9", @@ -52,6 +56,8 @@ "vue-tsc": "^2.2.8" }, "msw": { - "workerDirectory": ["public"] + "workerDirectory": [ + "public" + ] } } diff --git a/frontend/src/views/AiChatView.vue b/frontend/src/views/AiChatView.vue index 37ca504..a9ebdfd 100644 --- a/frontend/src/views/AiChatView.vue +++ b/frontend/src/views/AiChatView.vue @@ -11,9 +11,9 @@ -

- {{ chatElement.content }} -

+
+
@@ -63,6 +63,8 @@ import useUserStore from "../composables/store/useUserStore"; import LoadingIcon from "@/components/icons/LoadingIcon.vue"; import { z } from "zod"; import useAlertStore from "@/composables/store/useAlertStore"; +import { marked } from 'marked'; +import DOMPurify from 'dompurify'; const { messages, chat, isLoading, cancel } = useAiChat(); const { user } = useUserStore(); @@ -70,6 +72,18 @@ const inputMessage = ref(""); const chatContainer = ref(null); const alertStore = useAlertStore(); +marked.setOptions({ + gfm: true, + breaks: true, +}); + + +const renderMarkdown = (content: string) => { + if (!content) return ''; + const rawHtml = marked(content); + return DOMPurify.sanitize(rawHtml as string); +}; + const chatElements = computed(() => { return messages.value.map((message, index) => { return { @@ -80,6 +94,11 @@ const chatElements = computed(() => { }); }); +watch(messages, (newVal) => { + console.log('原始消息:', newVal[newVal.length - 1]); + console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1])); +}, { deep: true }); + watch( chatElements, async () => { @@ -132,3 +151,5 @@ onUnmounted(() => { cancel(); }); + +