瀏覽代碼

基础接口

taiji_caozhaorui 2 周之前
父節點
當前提交
f279080f40

+ 4 - 1
package.json

@@ -16,6 +16,8 @@
     "echarts": "^5.6.0",
     "element-plus": "^2.9.7",
     "fast-glob": "^3.3.3",
+    "highlight.js": "^11.11.1",
+    "markdown-it": "^14.1.0",
     "pinia": "^3.0.1",
     "rollup-plugin-visualizer": "^5.14.0",
     "sass": "^1.83.1",
@@ -26,7 +28,8 @@
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-top-level-await": "^1.4.4",
     "vue": "^3.5.13",
-    "vue-router": "^4.5.0"
+    "vue-router": "^4.5.0",
+    "vue-speech-recognition": "^0.2.1"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.2.1",

+ 1 - 0
src/main.ts

@@ -10,6 +10,7 @@ import './browerPatch'
 import { createPinia } from 'pinia'
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
+import 'highlight.js/styles/github-dark.css';
 
 const app = createApp(App)
 app.use(createPinia())

+ 48 - 0
src/views/smart-ask-answer/chat/answer/index.vue

@@ -0,0 +1,48 @@
+<template>
+  <div class="answer">
+    <div class="markdown-content" v-html="content"></div>
+  </div>
+  <div class="answer">
+    <div class="markdown-content" v-html="markdownCpt"></div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, getCurrentInstance, onMounted, reactive, watch} from "vue";
+import MarkdownIt from 'markdown-it';
+import hljs from 'highlight.js';
+
+const md = new MarkdownIt({
+  html: false,        // 在源码中启用 HTML 标签
+  highlight: (str, lang) => {
+    if (lang && hljs.getLanguage(lang)) {
+      try {
+        return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang }).value}</code></pre>`;
+      } catch (__) {}
+    }
+    return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`;
+  }
+});
+// md.renderer.rules.html_block = (tokens, idx) => {
+//   const content = tokens[idx].content;
+//   if (content.startsWith('<think>')) {
+//     return `<div class="think-box">${content.replace(/<\/?think>/g, '')}</div>`;
+//   }
+//   return content;
+// };
+const props = defineProps({
+  content: {},
+})
+const state: any = reactive({
+})
+const markdownCpt = computed(() => md.render(props.content))
+onMounted(() => {
+  state.token_ = localStorage.getItem('difyToken')
+})
+</script>
+
+<style lang="scss" scoped>
+.answer {
+  border: 1px solid green;
+}
+</style>

+ 24 - 0
src/views/smart-ask-answer/chat/ask/index.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="ask">
+    {{content}}
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, getCurrentInstance, onMounted, reactive, watch} from "vue";
+
+const props = defineProps({
+  content: {},
+})
+const state: any = reactive({
+})
+onMounted(() => {
+  state.token_ = localStorage.getItem('difyToken')
+})
+</script>
+
+<style lang="scss" scoped>
+.ask {
+  border: 1px solid red;
+}
+</style>

+ 116 - 0
src/views/smart-ask-answer/chat/component/voice/index.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="speech-to-text">
+    <button @click="toggleRecognition" :disabled="isSupported === false">
+      {{ isListening ? '停止录音' : '开始语音输入' }}
+    </button>
+    <p v-if="!isSupported" class="error">您的浏览器不支持语音识别功能</p>
+    <div class="result">{{ text }}</div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount } from 'vue';
+
+const isSupported = ref(true);
+const isListening = ref(false);
+const text = ref('');
+const recognition = ref(null);
+
+// 初始化语音识别
+const initSpeechRecognition = () => {
+  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+
+  if (!SpeechRecognition) {
+    isSupported.value = false;
+    return;
+  }
+
+  recognition.value = new SpeechRecognition();
+  recognition.value.continuous = true;
+  recognition.value.interimResults = true;
+  recognition.value.lang = 'zh-CN'; // 设置为中文
+
+  recognition.value.onresult = (event) => {
+    console.log(123)
+    let interimTranscript = '';
+    let finalTranscript = '';
+
+    for (let i = event.resultIndex; i < event.results.length; i++) {
+      const transcript = event.results[i][0].transcript;
+      if (event.results[i].isFinal) {
+        finalTranscript += transcript;
+      } else {
+        interimTranscript += transcript;
+      }
+    }
+
+    text.value = finalTranscript || interimTranscript;
+  };
+
+  recognition.value.onerror = (event) => {
+    console.log(456)
+    console.error('语音识别错误:', event.error);
+    isListening.value = false;
+  };
+
+  recognition.value.onend = () => {
+    if (isListening.value) {
+      recognition.value.start();
+    }
+  };
+};
+
+// 开始/停止语音识别
+const toggleRecognition = () => {
+  if (!recognition.value) return;
+
+  if (!isListening.value) {
+    recognition.value.start();
+    isListening.value = true;
+  } else {
+    recognition.value.stop();
+    isListening.value = false;
+  }
+};
+
+onMounted(() => {
+  initSpeechRecognition();
+});
+
+onBeforeUnmount(() => {
+  if (recognition.value) {
+    recognition.value.stop();
+  }
+});
+</script>
+
+<style scoped>
+.speech-to-text {
+  margin: 20px;
+}
+
+button {
+  padding: 10px 15px;
+  background-color: #42b983;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+button:disabled {
+  background-color: #cccccc;
+  cursor: not-allowed;
+}
+
+.error {
+  color: red;
+}
+
+.result {
+  margin-top: 10px;
+  padding: 10px;
+  border: 1px solid #ddd;
+  min-height: 50px;
+}
+</style>

+ 274 - 0
src/views/smart-ask-answer/chat/dify/fetch.ts

@@ -0,0 +1,274 @@
+import {ElMessage, ElNotification} from "element-plus";
+
+const ContentType = {
+    json: 'application/json',
+    stream: 'text/event-stream',
+    audio: 'audio/mpeg',
+    form: 'application/x-www-form-urlencoded; charset=UTF-8',
+    download: 'application/octet-stream', // for download
+    upload: 'multipart/form-data', // for upload
+}
+const baseOptions = {
+    method: 'GET',
+    mode: 'cors',
+    credentials: 'include', // always send cookies、HTTP Basic authentication.
+    headers: new Headers({
+        'Content-Type': ContentType.json,
+    }),
+    redirect: 'follow',
+}
+const unicodeToChar = (text: string) => {
+    if (!text)
+        return ''
+
+    return text.replace(/\\u[0-9a-f]{4}/g, (_match, p1) => {
+        return String.fromCharCode(parseInt(p1, 16))
+    })
+}
+const handleStream = (
+    response,
+    onData,
+    onCompleted?,
+    onThought?,
+    onMessageEnd?,
+    onMessageReplace?,
+    onFile?,
+    onWorkflowStarted?,
+    onWorkflowFinished?,
+    onNodeStarted?,
+    onNodeFinished?,
+    onIterationStart?,
+    onIterationNext?,
+    onIterationFinish?,
+    onNodeRetry?,
+    onParallelBranchStarted?,
+    onParallelBranchFinished?,
+    onTextChunk?,
+    onTTSChunk?,
+    onTTSEnd?,
+    onTextReplace?,
+) => {
+    if (!response.ok)
+        throw new Error('Network response was not ok')
+
+    const reader = response.body?.getReader()
+    const decoder = new TextDecoder('utf-8')
+    let buffer = ''
+    let bufferObj
+    let isFirstMessage = true
+    const read = () => {
+        let hasError = false
+        reader?.read().then((result: any) => {
+            if (result.done) {
+                onCompleted && onCompleted()
+                return
+            }
+            buffer += decoder.decode(result.value, { stream: true })
+            const lines = buffer.split('\n')
+            try {
+                lines.forEach((message) => {
+                    if (message.startsWith('data: ')) { // check if it starts with data:
+                        try {
+                            bufferObj = JSON.parse(message.substring(6))// remove data: and parse as json
+                        }
+                        catch (e) {
+                            // mute handle message cut off
+                            onData('', isFirstMessage, {
+                                conversationId: bufferObj?.conversation_id,
+                                messageId: bufferObj?.message_id,
+                            })
+                            return
+                        }
+                        if (bufferObj.status === 400 || !bufferObj.event) {
+                            onData('', false, {
+                                conversationId: undefined,
+                                messageId: '',
+                                errorMessage: bufferObj?.message,
+                                errorCode: bufferObj?.code,
+                            })
+                            hasError = true
+                            onCompleted?.(true, bufferObj?.message)
+                            return
+                        }
+                        if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') {
+                            // can not use format here. Because message is splitted.
+                            onData(unicodeToChar(bufferObj.answer), isFirstMessage, {
+                                conversationId: bufferObj.conversation_id,
+                                taskId: bufferObj.task_id,
+                                messageId: bufferObj.id,
+                            })
+                            isFirstMessage = false
+                        }
+                        else if (bufferObj.event === 'agent_thought') {
+                            onThought?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'message_file') {
+                            onFile?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'message_end') {
+                            onMessageEnd?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'message_replace') {
+                            onMessageReplace?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'workflow_started') {
+                            onWorkflowStarted?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'workflow_finished') {
+                            onWorkflowFinished?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'node_started') {
+                            onNodeStarted?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'node_finished') {
+                            onNodeFinished?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'iteration_started') {
+                            onIterationStart?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'iteration_next') {
+                            onIterationNext?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'iteration_completed') {
+                            onIterationFinish?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'node_retry') {
+                            onNodeRetry?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'parallel_branch_started') {
+                            onParallelBranchStarted?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'parallel_branch_finished') {
+                            onParallelBranchFinished?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'text_chunk') {
+                            onTextChunk?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'text_replace') {
+                            onTextReplace?.(bufferObj)
+                        }
+                        else if (bufferObj.event === 'tts_message') {
+                            onTTSChunk?.(bufferObj.message_id, bufferObj.audio, bufferObj.audio_type)
+                        }
+                        else if (bufferObj.event === 'tts_message_end') {
+                            onTTSEnd?.(bufferObj.message_id, bufferObj.audio)
+                        }
+                    }
+                })
+                buffer = lines[lines.length - 1]
+            } catch (e) {
+                onData('', false, {
+                    conversationId: undefined,
+                    messageId: '',
+                    errorMessage: `${e}`,
+                })
+                hasError = true
+                onCompleted?.(true, e as string)
+                return
+            }
+            if (!hasError) {
+                read()
+            }
+        })
+    }
+    read()
+}
+export const ssePost = (url, fetchOptions, otherOptions,) => {
+    const {
+        onData,
+        onCompleted,
+        onThought,
+        onFile,
+        onMessageEnd,
+        onMessageReplace,
+        onWorkflowStarted,
+        onWorkflowFinished,
+        onNodeStarted,
+        onNodeFinished,
+        onIterationStart,
+        onIterationNext,
+        onIterationFinish,
+        onNodeRetry,
+        onParallelBranchStarted,
+        onParallelBranchFinished,
+        onTextChunk,
+        onTTSChunk,
+        onTTSEnd,
+        onTextReplace,
+        onError,
+        getAbortController,
+    } = otherOptions
+    const abortController = new AbortController()
+
+    const options = Object.assign({}, baseOptions, {
+        method: 'POST',
+        signal: abortController.signal,
+    }, fetchOptions)
+
+    const contentType = options.headers.get('Content-Type')
+    if (!contentType)
+        options.headers.set('Content-Type', ContentType.json)
+
+    getAbortController?.(abortController)
+
+    const { body } = options
+    if (body)
+        options.body = JSON.stringify(body)
+
+    const accessToken = localStorage.getItem('difyToken')
+    options.headers.set('Authorization', `Bearer ${accessToken}`)
+    globalThis.fetch(url, options).then((res) => {
+        if (!/^(2|3)\d{2}$/.test(String(res.status))) {
+            if (res.status === 401) {
+                ElMessage.error('状态码错误,' + res)
+                // refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
+                //     ssePost(url, fetchOptions, otherOptions)
+                // }).catch(() => {
+                //     res.json().then((data: any) => {
+                //         if (isPublicAPI) {
+                //             if (data.code === 'web_sso_auth_required')
+                //                 requiredWebSSOLogin()
+                //
+                //             if (data.code === 'unauthorized') {
+                //                 removeAccessToken()
+                //                 globalThis.location.reload()
+                //             }
+                //         }
+                //     })
+                // })
+            }
+            else {
+                res.json().then((data) => {
+                    ElNotification({
+                        message: data.message || 'Server Error',
+                        type: 'error',
+                    })
+                })
+                onError?.('Server Error')
+            }
+            return
+        }
+        return handleStream(res, (str: string, isFirstMessage: boolean, moreInfo) => {
+            if (moreInfo.errorMessage) {
+                onError?.(moreInfo.errorMessage, moreInfo.errorCode)
+                // TypeError: Cannot assign to read only property ... will happen in page leave, so it should be ignored.
+                if (moreInfo.errorMessage !== 'AbortError: The user aborted a request.' && !moreInfo.errorMessage.includes('TypeError: Cannot assign to read only property')) {
+                    ElNotification({
+                        message: moreInfo.errorMessage,
+                        type: 'error',
+                    })
+                }
+                return
+            }
+            onData?.(str, isFirstMessage, moreInfo)
+        }, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onNodeRetry, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace)
+    }).catch((e) => {
+        if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property')) {
+            ElNotification({
+                message: e,
+                type: 'error',
+            })
+        }
+        onError?.(e)
+    })
+}

文件差異過大導致無法顯示
+ 183 - 0
src/views/smart-ask-answer/chat/index.vue