feat: 添加流式支持;教材知识库 Demo
This commit is contained in:
@ -1,2 +1,2 @@
|
|||||||
DIFY_BASE_URL=https://service3.fenshenzhike.com/v1
|
DIFY_BASE_URL=https://service3.fenshenzhike.com/v1
|
||||||
DIFY_API_KEY=app-58uPYnqyLAtVtBYELHUGkqD9
|
DIFY_API_KEY=app-mS8a08rlxQuxZpdqEfwxJppK
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ logs
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
!.env.oem-hlbrzy
|
!.env.oem-hlbrzy
|
||||||
|
!.env.textbook-demo
|
||||||
|
@ -15,6 +15,10 @@ defineProps({
|
|||||||
default: "tabler:robot-face",
|
default: "tabler:robot-face",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: "select-suggestion", suggestion: string): void;
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -35,7 +39,7 @@ defineProps({
|
|||||||
<span class="text-sm font-medium">{{ name }}</span>
|
<span class="text-sm font-medium">{{ name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="rounded-lg bg-white/50 p-2 text-sm dark:bg-neutral-800/50 break-all text-justify"
|
class="rounded-lg bg-white/50 p-3 text-sm dark:bg-neutral-800/50 break-all text-justify"
|
||||||
>
|
>
|
||||||
<div v-if="message.message" class="prose prose-sm">
|
<div v-if="message.message" class="prose prose-sm">
|
||||||
<!-- {{ message.message }} -->
|
<!-- {{ message.message }} -->
|
||||||
@ -47,6 +51,24 @@ defineProps({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="message.role === 'suggestion'">
|
||||||
|
<div class="">
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
<li
|
||||||
|
v-for="suggestion in message.suggestions"
|
||||||
|
class="rounded-lg bg-white/50 p-2 text-xs dark:bg-neutral-800/50 break-all w-fit inline-flex items-center gap-0.5"
|
||||||
|
:key="suggestion"
|
||||||
|
@click="$emit('select-suggestion', suggestion)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="tabler:question-mark"
|
||||||
|
class="text-primary-500 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 text-base"
|
||||||
|
/>
|
||||||
|
<span>{{ suggestion }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -58,9 +58,14 @@ useHead({
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex flex-col dark:bg-neutral-900/80">
|
<div class="w-full h-full flex flex-col dark:bg-neutral-900/80">
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 w-full px-4 py-3 bg-white/30 backdrop-blur-2xl z-30 dark:bg-neutral-900/80"
|
class="sticky top-0 w-full px-4 py-3 bg-white/50 backdrop-blur-2xl z-30 dark:bg-neutral-900/80"
|
||||||
>
|
>
|
||||||
<h1 class="font-medium">{{ gstate.botName }}</h1>
|
<div class="flex items-center gap-1.5">
|
||||||
|
<Icon name="tabler:book" class="-mt-0.5 text-3xl text-primary-500" />
|
||||||
|
<div class="flex items-end gap-1">
|
||||||
|
<h1 class="text-base font-semibold text-primary-500 leading-none">{{ gstate.botName }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- <USelectMenu v-model="value" color="primary" variant="none" :items="items" class="w-48" size="lg" :ui="{base: 'w-fit'}" /> -->
|
<!-- <USelectMenu v-model="value" color="primary" variant="none" :items="items" class="w-48" size="lg" :ui="{base: 'w-fit'}" /> -->
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -4,7 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2024-11-01",
|
compatibilityDate: "2024-11-01",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ["@nuxt/ui", "@pinia/nuxt"],
|
modules: ["@nuxt/ui", "@pinia/nuxt", "pinia-plugin-persistedstate/nuxt"],
|
||||||
css: ["~/assets/css/main.css"],
|
css: ["~/assets/css/main.css"],
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev --dotenv .env.textbook-demo",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate --dotenv .env.textbook-demo",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
@ -19,6 +19,7 @@
|
|||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"nuxt": "^3.16.2",
|
"nuxt": "^3.16.2",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
|
167
pages/index.vue
167
pages/index.vue
@ -9,56 +9,83 @@ const getPopularInquiriesByRole = () => {
|
|||||||
return {
|
return {
|
||||||
stu: [
|
stu: [
|
||||||
{
|
{
|
||||||
label: "学籍管理",
|
label: "物联网基础",
|
||||||
inquiries: [
|
inquiries: [
|
||||||
{
|
{
|
||||||
question: "学籍注册的要求",
|
question: "物联网的定义是什么",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "病假请假的审批流程",
|
question: "物联网与互联网的关系与区别",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "转专业有哪些条件",
|
question: "物联网有哪些典型应用场景",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "办理休学的流程",
|
question: "物联网系统的四层结构是什么",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "人工智能专业毕业的要求",
|
question: "物联网的本质是什么",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "新基建和物联网的关系",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "物联网工程的生命周期包括哪些阶段",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "学生日常",
|
label: "智慧工地",
|
||||||
inquiries: [
|
inquiries: [
|
||||||
{
|
{
|
||||||
question: "校医院服务时间和报销政策",
|
question: "智慧工地的概念与核心目标",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "学工部的联系方式",
|
question: "智慧工地系统的四层架构是什么",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "宿舍管理制度是怎样的",
|
question: "施工场地工人健康检测系统包括哪些功能",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "助学贷款申请指南",
|
question: "施工场地工人健康检测系统的作用是什么",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "成立社团的流程",
|
question: "智慧工地中常用的传感器有哪些",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "选择传感器需要考虑哪些特性参数",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "传感器安装调试的主要步骤有哪些",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "职业发展",
|
label: "智能交通",
|
||||||
inquiries: [
|
inquiries: [
|
||||||
{
|
{
|
||||||
question: "专升本的条件和要求",
|
question: "智能交通系统的定义和主要目标是什么",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "金融相关专业实习单位有哪些",
|
question: "智能交通系统的四层结构及其功能",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "创业孵化基地申请方式",
|
question: "我国智能交通的发展现状如何",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "国外智能交通发展的主要特点是什么",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "智能交通系统中常用的感知技术有哪些",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "射频识别技术在智能交通中的应用有哪些",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "自动识别技术包括哪些分类",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "交通卡口监控系统需求分析包含哪些方面",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -177,30 +204,84 @@ const onInquirySubmit = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await useFetch<IWorkflowResponse>(
|
// const resp = await useFetch<IWorkflowResponse>(
|
||||||
`${runtimeConfig.public.DifyBaseURL}/workflows/run`,
|
// `${runtimeConfig.public.DifyBaseURL}/workflows/run`,
|
||||||
{
|
// {
|
||||||
method: "post",
|
// method: "post",
|
||||||
headers: {
|
// headers: {
|
||||||
Authorization: `Bearer ${runtimeConfig.public.DifyApiKey}`,
|
// Authorization: `Bearer ${runtimeConfig.public.DifyApiKey}`,
|
||||||
},
|
// },
|
||||||
body: JSON.stringify({
|
// body: JSON.stringify({
|
||||||
inputs: {
|
// inputs: {
|
||||||
question: inquiryInput.value,
|
// query: inquiryInput.value,
|
||||||
role: gstate.currentRole,
|
// },
|
||||||
},
|
// response_mode: "blocking",
|
||||||
response_mode: "blocking",
|
// user: "xsh",
|
||||||
user: "abc-123",
|
// }),
|
||||||
}),
|
// }
|
||||||
}
|
// );
|
||||||
);
|
// gstate.insertOrUpdateMessage({
|
||||||
gstate.insertOrUpdateMessage({
|
// id: botMessageId,
|
||||||
id: botMessageId,
|
// role: "bot",
|
||||||
role: "bot",
|
// message:
|
||||||
message:
|
// resp.data.value?.data.outputs.message.match(regex)?.[1] ||
|
||||||
resp.data.value?.data.outputs.message.match(regex)?.[1] ||
|
// "网络繁忙,请稍后再试",
|
||||||
"网络繁忙,请稍后再试",
|
// });
|
||||||
});
|
http_stream<{
|
||||||
|
query: string
|
||||||
|
response_mode: "blocking" | "streaming"
|
||||||
|
user: string
|
||||||
|
conversation_id: string
|
||||||
|
inputs: Record<string, any>
|
||||||
|
}>(`${runtimeConfig.public.DifyBaseURL}/chat-messages`, {
|
||||||
|
query: inquiryInput.value,
|
||||||
|
response_mode: "streaming",
|
||||||
|
user: "xsh",
|
||||||
|
conversation_id: "",
|
||||||
|
inputs: {}
|
||||||
|
}, {
|
||||||
|
onStart: () => {
|
||||||
|
responding.value = true;
|
||||||
|
gstate.insertOrUpdateMessage({
|
||||||
|
id: botMessageId,
|
||||||
|
role: "bot",
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTextChunk: (message: string) => {
|
||||||
|
gstate.insertOrUpdateMessage({
|
||||||
|
id: botMessageId,
|
||||||
|
role: "bot",
|
||||||
|
message: gstate.messages.find(m => m.id === botMessageId)?.message + message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onComplete: (id, finished_at, messageId) => {
|
||||||
|
responding.value = false;
|
||||||
|
gstate.insertOrUpdateMessage({
|
||||||
|
id: botMessageId,
|
||||||
|
role: "bot",
|
||||||
|
message: gstate.messages.find(m => m.id === botMessageId)?.message || "网络繁忙,请稍后再试",
|
||||||
|
});
|
||||||
|
scrollLastMessageIntoView();
|
||||||
|
fetch(
|
||||||
|
`${runtimeConfig.public.DifyBaseURL}/messages/${messageId}/suggested?user=xsh`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${runtimeConfig.public.DifyApiKey}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).then(res => res.json()).then(data => {
|
||||||
|
if (data?.data?.length) {
|
||||||
|
gstate.insertOrUpdateMessage({
|
||||||
|
id: uuidv4(),
|
||||||
|
role: "suggestion",
|
||||||
|
suggestions: data.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
gstate.insertOrUpdateMessage({
|
gstate.insertOrUpdateMessage({
|
||||||
id: botMessageId,
|
id: botMessageId,
|
||||||
@ -244,6 +325,12 @@ const scrollLastMessageIntoView = () => {
|
|||||||
:name="gstate.botName"
|
:name="gstate.botName"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
:message="message"
|
:message="message"
|
||||||
|
@select-suggestion="
|
||||||
|
(suggestion) => {
|
||||||
|
if (responding) return;
|
||||||
|
inquiryInput = suggestion;
|
||||||
|
onInquirySubmit();
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@ -35,6 +35,9 @@ importers:
|
|||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
|
version: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
|
||||||
|
pinia-plugin-persistedstate:
|
||||||
|
specifier: ^4.4.1
|
||||||
|
version: 4.4.1(@nuxt/kit@3.16.2(magicast@0.3.5))(@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))))(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.4
|
specifier: ^4.1.4
|
||||||
version: 4.1.4
|
version: 4.1.4
|
||||||
@ -1920,6 +1923,9 @@ packages:
|
|||||||
decache@4.6.2:
|
decache@4.6.2:
|
||||||
resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==}
|
resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==}
|
||||||
|
|
||||||
|
deep-pick-omit@1.2.1:
|
||||||
|
resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==}
|
||||||
|
|
||||||
deepmerge@4.3.1:
|
deepmerge@4.3.1:
|
||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -3252,6 +3258,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
pinia-plugin-persistedstate@4.4.1:
|
||||||
|
resolution: {integrity: sha512-lmuMPpXla2zJKjxEq34e1E9P9jxkWEhcVwwioCCE0izG45kkTOvQfCzvwhW3i38cvnaWC7T1eRdkd15Re59ldw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nuxt/kit': '>=3.0.0'
|
||||||
|
'@pinia/nuxt': '>=0.10.0'
|
||||||
|
pinia: '>=3.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@nuxt/kit':
|
||||||
|
optional: true
|
||||||
|
'@pinia/nuxt':
|
||||||
|
optional: true
|
||||||
|
pinia:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pinia@3.0.2:
|
pinia@3.0.2:
|
||||||
resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==}
|
resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -6511,6 +6531,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
callsite: 1.0.0
|
callsite: 1.0.0
|
||||||
|
|
||||||
|
deep-pick-omit@1.2.1: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
default-browser-id@5.0.0: {}
|
default-browser-id@5.0.0: {}
|
||||||
@ -8012,6 +8034,16 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@4.0.2: {}
|
picomatch@4.0.2: {}
|
||||||
|
|
||||||
|
pinia-plugin-persistedstate@4.4.1(@nuxt/kit@3.16.2(magicast@0.3.5))(@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))))(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))):
|
||||||
|
dependencies:
|
||||||
|
deep-pick-omit: 1.2.1
|
||||||
|
defu: 6.1.4
|
||||||
|
destr: 2.0.5
|
||||||
|
optionalDependencies:
|
||||||
|
'@nuxt/kit': 3.16.2(magicast@0.3.5)
|
||||||
|
'@pinia/nuxt': 0.10.1(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
|
||||||
|
pinia: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
|
||||||
|
|
||||||
pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)):
|
pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 7.7.5
|
'@vue/devtools-api': 7.7.5
|
||||||
|
@ -5,7 +5,7 @@ export const useGState = defineStore("global_state", () => {
|
|||||||
const botName = computed(() => {
|
const botName = computed(() => {
|
||||||
switch (currentRole.value) {
|
switch (currentRole.value) {
|
||||||
case "stu":
|
case "stu":
|
||||||
return "AI 辅导员";
|
return "物联网工程导论";
|
||||||
case "tea":
|
case "tea":
|
||||||
return "AI 教研专家";
|
return "AI 教研专家";
|
||||||
case "fans":
|
case "fans":
|
||||||
|
@ -2,8 +2,9 @@ export type VisitorRole = "stu" | "tea" | "fans";
|
|||||||
|
|
||||||
export type LocalMessage = {
|
export type LocalMessage = {
|
||||||
id: string;
|
id: string;
|
||||||
role: "bot" | "user";
|
role: "bot" | "user" | "suggestion";
|
||||||
message?: string;
|
message?: string;
|
||||||
|
suggestions?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IWorkflowResponse {
|
export interface IWorkflowResponse {
|
||||||
|
84
utils/http.ts
Normal file
84
utils/http.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
export const http_stream = async <ReqT>(
|
||||||
|
url: string,
|
||||||
|
params: ReqT,
|
||||||
|
events: {
|
||||||
|
onStart?: (id: string, created_at?: number, messageId?: string | null) => void
|
||||||
|
onTextChunk?: (message: string) => void
|
||||||
|
onComplete?: (id: string | null, finished_at?: number, messageId?: string | null) => void
|
||||||
|
}) => {
|
||||||
|
const { onStart, onTextChunk, onComplete } = events
|
||||||
|
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${runtimeConfig.public.DifyApiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('Network response was not ok')
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader()
|
||||||
|
const decoder = new TextDecoder('utf-8')
|
||||||
|
let buffer = ''
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader?.read() || {}
|
||||||
|
if (done) break
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
|
const parts = buffer.split('\n\n')
|
||||||
|
buffer = parts.pop()!
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part.trim().startsWith('data:')) continue
|
||||||
|
const payload = part.replace(/^data:\s*/, '').trim()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(payload)
|
||||||
|
if (obj?.event && obj.event === 'workflow_started') {
|
||||||
|
onStart?.(obj?.data?.id || null, obj?.data?.created_at || 0, obj?.message_id || null)
|
||||||
|
}
|
||||||
|
if (obj?.event && obj.event === 'workflow_finished') {
|
||||||
|
onComplete?.(obj?.data?.id || null, obj?.data?.finished_at || 0, obj?.message_id || null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Workflow
|
||||||
|
if (obj?.event && obj.event === 'text_chunk') {
|
||||||
|
let text_chunk = obj.data?.text as string
|
||||||
|
if (text_chunk) {
|
||||||
|
if (text_chunk.startsWith('<') && text_chunk.endsWith('>')) {
|
||||||
|
const endTag = text_chunk.match(/<\/[^>]*>/)
|
||||||
|
if (endTag) {
|
||||||
|
text_chunk = text_chunk.replace(endTag[0], '\n' + endTag[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTextChunk?.(text_chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Chatflow
|
||||||
|
if (obj?.event && obj.event === 'message') {
|
||||||
|
let ans = obj.answer
|
||||||
|
if (ans) {
|
||||||
|
if (ans.startsWith('<') && ans.endsWith('>')) {
|
||||||
|
const endTag = ans.match(/<\/[^>]*>/)
|
||||||
|
if (endTag) {
|
||||||
|
ans = ans.replace(endTag[0], '\n' + endTag[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTextChunk?.(ans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onComplete?.(null)
|
||||||
|
}
|
Reference in New Issue
Block a user