Skip to content

Ant Desgin X 联调 Dify

1826字约6分钟

DifyViteVueTypeScript

2025-05-05

Ant Desgin X 联调 Dify

0.前提条件

  1. Ant Desogin X 要求 vue 版本不低于 3.5,因此这里直接选择 vite 进行项目的创建。
  2. 创建项目时,指定项目类型为 Vue + TypeScript
  3. 所使用的 node.js 版本信息为 20.17.0

1.环境准备

1.1 创建一个vue项目

使用 vite 进行项目的创建,创建命令如下,在创建项目的时候声明项目类型为 Vue + TypeScript

npm create vite@latest vue-typescript

image-20250507091751418

创建操作执行完成后,进入项目目录下,依次执行如下命令即可运行 Vue 项目。

# 安装项目依赖
npm install
# 运行vue项目
npm run dev

image-20250507132401813

访问终端显示的运行地址,可以看到如下页面,说明项目创建成功。

image-20250507132421868

1.2 安装 ant-design-x-vue

这里直接使用 npm 安装 ant-design-x-vue,命令如下所示:

npm install ant-design-x-vue --save

image-20250507132629577

2.项目开发

  1. 直接使用 Ant Design X Vue 项目官方提供的代码进行测试,代码地址:https://antd-design-x-vue.netlify.app/playground/independent.html,代码内容如下所示:

    <script setup lang="ts">
    import type { AttachmentsProps, BubbleListProps, ConversationsProps, PromptsProps } from 'ant-design-x-vue'
    import type { VNode } from 'vue'
    import {
      CloudUploadOutlined,
      CommentOutlined,
      EllipsisOutlined,
      FireOutlined,
      HeartOutlined,
      PaperClipOutlined,
      PlusOutlined,
      ReadOutlined,
      ShareAltOutlined,
      SmileOutlined,
    } from '@ant-design/icons-vue'
    import { Badge, Button, Flex, Space, Typography, theme } from 'ant-design-vue'
    import {
      Attachments,
      Bubble,
      Conversations,
      Prompts,
      Sender,
      useXAgent,
      useXChat,
      Welcome,
    } from 'ant-design-x-vue'
    import { computed, h, ref, watch } from 'vue'
    
    const { token } = theme.useToken()
    
    const styles = computed(() => {
      return {
        'layout': {
          'width': '100%',
          'min-width': '1000px',
          'height': '722px',
          'border-radius': `${token.value.borderRadius}px`,
          'display': 'flex',
          'background': `${token.value.colorBgContainer}`,
          'font-family': `AlibabaPuHuiTi, ${token.value.fontFamily}, sans-serif`,
        },
        'menu': {
          'background': `${token.value.colorBgLayout}80`,
          'width': '280px',
          'height': '100%',
          'display': 'flex',
          'flex-direction': 'column',
        },
        'conversations': {
          'padding': '0 12px',
          'flex': 1,
          'overflow-y': 'auto',
        },
        'chat': {
          'height': '100%',
          'width': '100%',
          'max-width': '700px',
          'margin': '0 auto',
          'box-sizing': 'border-box',
          'display': 'flex',
          'flex-direction': 'column',
          'padding': `${token.value.paddingLG}px`,
          'gap': '16px',
        },
        'messages': {
          flex: 1,
        },
        'placeholder': {
          'padding-top': '32px',
          'text-align': 'left',
          'flex': 1,
        },
        'sender': {
          'box-shadow': token.value.boxShadow,
        },
        'logo': {
          'display': 'flex',
          'height': '72px',
          'align-items': 'center',
          'justify-content': 'start',
          'padding': '0 24px',
          'box-sizing': 'border-box',
        },
        'logo-img': {
          width: '24px',
          height: '24px',
          display: 'inline-block',
        },
        'logo-span': {
          'display': 'inline-block',
          'margin': '0 8px',
          'font-weight': 'bold',
          'color': token.value.colorText,
          'font-size': '16px',
        },
        'addBtn': {
          background: '#1677ff0f',
          border: '1px solid #1677ff34',
          width: 'calc(100% - 24px)',
          margin: '0 12px 24px 12px',
        },
      } as const
    })
    
    defineOptions({ name: 'PlaygroundIndependentSetup' })
    
    
    function renderTitle(icon: VNode, title: string) {
      return h(Space, { align: 'start' }, [icon, h('span', title)])
    }
    
    const defaultConversationsItems = [
      {
        key: '0',
        label: 'What is Ant Design X?',
      },
    ]
    
    const placeholderPromptsItems: PromptsProps['items'] = [
      {
        key: '1',
        label: renderTitle(h(FireOutlined, { style: { color: '#FF4D4F' } }), 'Hot Topics'),
        description: 'What are you interested in?',
        children: [
          {
            key: '1-1',
            description: `What's new in X?`,
          },
          {
            key: '1-2',
            description: `What's AGI?`,
          },
          {
            key: '1-3',
            description: `Where is the doc?`,
          },
        ],
      },
      {
        key: '2',
        label: renderTitle(h(ReadOutlined, { style: { color: '#1890FF' } }), 'Design Guide'),
        description: 'How to design a good product?',
        children: [
          {
            key: '2-1',
            icon: h(HeartOutlined),
            description: `Know the well`,
          },
          {
            key: '2-2',
            icon: h(SmileOutlined),
            description: `Set the AI role`,
          },
          {
            key: '2-3',
            icon: h(CommentOutlined),
            description: `Express the feeling`,
          },
        ],
      },
    ]
    
    const senderPromptsItems: PromptsProps['items'] = [
      {
        key: '1',
        description: 'Hot Topics',
        icon: h(FireOutlined, { style: { color: '#FF4D4F' } }),
      },
      {
        key: '2',
        description: 'Design Guide',
        icon: h(ReadOutlined, { style: { color: '#1890FF' } }),
      },
    ]
    
    const roles: BubbleListProps['roles'] = {
      ai: {
        placement: 'start',
        typing: { step: 5, interval: 20 },
        styles: {
          content: {
            borderRadius: '16px',
          },
        },
      },
      local: {
        placement: 'end',
        variant: 'shadow',
      },
    }
    
    // ==================== State ====================
    const headerOpen = ref(false)
    const content = ref('')
    const conversationsItems = ref(defaultConversationsItems)
    const activeKey = ref(defaultConversationsItems[0].key)
    const attachedFiles = ref<AttachmentsProps['items']>([])
    const agentRequestLoading = ref(false)
    
    // ==================== Runtime ====================
    const [agent] = useXAgent({
      request: async ({ message }, { onSuccess }) => {
        agentRequestLoading.value = true
        await sleep()
        agentRequestLoading.value = false
        onSuccess(`Mock success return. You said: ${message}`)
      },
    })
    
    const { onRequest, messages, setMessages } = useXChat({
      agent: agent.value,
    })
    
    watch(activeKey, () => {
      if (activeKey.value !== undefined) {
        setMessages([])
      }
    }, { immediate: true })
    
    // ==================== Event ====================
    function onSubmit(nextContent: string) {
      if (!nextContent)
        return
      onRequest(nextContent)
      content.value = ''
    }
    
    const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
      onRequest(info.data.description as string)
    }
    
    function onAddConversation() {
      conversationsItems.value = [
        ...conversationsItems.value,
        {
          key: `${conversationsItems.value.length}`,
          label: `New Conversation ${conversationsItems.value.length}`,
        },
      ]
      activeKey.value = `${conversationsItems.value.length}`
    }
    
    const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
      activeKey.value = key
    }
    
    const handleFileChange: AttachmentsProps['onChange'] = info => attachedFiles.value = info.fileList
    
    // ==================== Nodes ====================
    const placeholderNode = computed(() => h(
      Space,
      { direction: "vertical", size: 16, style: styles.value.placeholder },
      [
        h(
          Welcome,
          {
            variant: "borderless",
            icon: "https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp",
            title: "Hello, I'm Ant Design X",
            description: "Base on Ant Design, AGI product interface solution, create a better intelligent vision~",
            extra: h(Space, {}, [h(Button, { icon: h(ShareAltOutlined) }), h(Button, { icon: h(EllipsisOutlined) })]),
          }
        ),
        h(
          Prompts,
          {
            title: "Do you want?",
            items: placeholderPromptsItems,
            styles: {
              list: {
                width: '100%',
              },
              item: {
                flex: 1,
              },
            },
            onItemClick: onPromptsItemClick,
          }
        )
      ]
    ))
    
    const items = computed<BubbleListProps['items']>(() => {
      if (messages.value.length === 0) {
        return [{ content: placeholderNode, variant: 'borderless' }]
      }
      return messages.value.map(({ id, message, status }) => ({
        key: id,
        loading: status === 'loading',
        role: status === 'local' ? 'local' : 'ai',
        content: message,
      }))
    })
    </script>
    
    <template>
      <div :style="styles.layout">
        <div :style="styles.menu">
          <!-- 🌟 Logo -->
          <div :style="styles.logo">
            <img
              src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
              draggable="false"
              alt="logo"
              :style="styles['logo-img']"
            >
            <span :style="styles['logo-span']">Ant Design X Vue</span>
          </div>
    
          <!-- 🌟 添加会话 -->
          <Button
            type="link"
            :style="styles.addBtn"
            @click="onAddConversation"
          >
            <PlusOutlined />
            New Conversation
          </Button>
    
          <!-- 🌟 会话管理 -->
          <Conversations
            :items="conversationsItems"
            :style="styles.conversations"
            :active-key="activeKey"
            @active-change="onConversationClick"
          />
        </div>
    
        <div :style="styles.chat">
          <!-- 🌟 消息列表 -->
          <Bubble.List
            :items="items"
            :roles="roles"
            :style="styles.messages"
          />
    
          <!-- 🌟 提示词 -->
          <Prompts
            :items="senderPromptsItems"
            @item-click="onPromptsItemClick"
          />
    
          <!-- 🌟 输入框 -->
          <Sender
            :value="content"
            :style="styles.sender"
            :loading="agentRequestLoading"
            @submit="onSubmit"
            @change="value => content = value"
          >
            <template #prefix>
              <Badge :dot="attachedFiles.length > 0 && !headerOpen">
                <Button
                  type="text"
                  @click="() => headerOpen = !headerOpen"
                >
                  <template #icon>
                    <PaperClipOutlined />
                  </template>
                </Button>
              </Badge>
            </template>
    
            <template #header>
              <Sender.Header
                title="Attachments"
                :open="headerOpen"
                :styles="{ content: { padding: 0 } }"
                @open-change="open => headerOpen = open"
              >
                <Attachments
                  :before-upload="() => false"
                  :items="attachedFiles"
                  @change="handleFileChange"
                >
                  <template #placeholder="type">
                    <Flex
                      v-if="type && type.type === 'inline'"
                      align="center"
                      justify="center"
                      vertical
                      gap="2"
                    >
                      <Typography.Text style="font-size: 30px; line-height: 1;">
                        <CloudUploadOutlined />
                      </Typography.Text>
                      <Typography.Title
                        :level="5"
                        style="margin: 0; font-size: 14px; line-height: 1.5;"
                      >
                        Upload files
                      </Typography.Title>
                      <Typography.Text type="secondary">
                        Click or drag files to this area to upload
                      </Typography.Text>
                    </Flex>
                    <Typography.Text v-if="type && type.type === 'drop'">
                      Drop file here
                    </Typography.Text>
                  </template>
                </Attachments>
              </Sender.Header>
            </template>
          </Sender>
        </div>
      </div>
    </template>

    运行后,页面显示内容如下,这里先不对样式做调整,直接进行大模型的调用。

    image-20250507133307851

  2. 这里配置了 Difyapi 进行调用测试,与常规的前后端接口请求方式不同,大模型的接口支持流式模式,基于 SSE(Server-Sent Events) 实现类似打字机输出方式的流式响应,这里对接口的响应数据要进行特殊处理。

    // 解析响应的JSON文本内容
    function tryParseJSON(line: string) {
      try {
        return JSON.parse(line);
      } catch {
        return null;
      }
    }
    
    // 大模型对话
    const [agent] = useXAgent({
      request: async ({ message }, { onSuccess, onUpdate, onError  }) => {
        agentRequestLoading.value = true
        try {
            // http://127.0.0.1:81/v1/chat-messages 这里是dify发送对话消息的接口
          const response = await fetch('http://127.0.0.1:81/v1/chat-messages', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': 'Bearer app-sosbqnUmxQRzvpSmqsDfMAtr', // 秘钥
            },
            body: JSON.stringify({
              "inputs":{},
              "query": message,
              "response_mode": "streaming",
              "conversation_id": "",
              "user": "abc-123",
              "files":[]
            }),
          })
    
          if (!response.ok || !response.body) throw new Error('响应异常');
    
          const reader = response.body.getReader();
          const decoder = new TextDecoder('utf-8');
          let buffer = '';
          let fullText = '';
    
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
    
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';
    
            for (const line of lines) {
              if (!line.startsWith('data:')) continue;
    
              const jsonStr = line.replace(/^data:\s*/, '').trim();
              const parsed = tryParseJSON(jsonStr);
              // 这里是Dify工作流的判断逻辑,如其他大模型,需要具体查看
              if (parsed && parsed.event == 'workflow_finished') {
                const content = parsed.data.outputs.answer;
                fullText += content;
                onUpdate(fullText);
                onSuccess(fullText);  // 添加这行,在工作流完成时调用 onSuccess
              }
            }
          }
        } catch (error) {
          console.error('请求失败:', error)
          onError(error as Error)
        } finally {
          agentRequestLoading.value = false
        }
      },
    })

    页面输入内容,能够正确进行响应,如下图所示

    image-20250507134900531

3.项目部署

这里对新创建的 Vue 项目进行优化,引入配置文件,使得项目更加完整。

  1. 在项目根目录下,创建一下两个文件,分别是开发环境以及生产环境的配置信息:

    • .env.development
    • .env.production

    文件中写入后端大模型的服务地址,如下所示:

    VITE_API_BASE_URL=http://127.0.0.1:81
  2. 修改 Vue 文件中请求大模型的地址,修改为如下样式:

    // 修改前
    const response = await fetch('127.0.0.1:81/v1/chat-messages', {
    // 修改后
    const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/v1/chat-messages`, {
  3. 修改 package.json 文件,执行项目执行不同操作的时候,使用的配置文件分别是什么,如下所示:

      "scripts": {
        "dev": "vite --mode development",
        "build": "vue-tsc -b && vite build --mode production",
        "preview": "vite preview --mode production"
      },
  4. 执行 npm run dev,运行项目进行测试,能够正常请求后端大模型服务,如下所示

    image-20250507140434382

  5. 执行 npm run build,打包当前项目部署到生产环境。

    image-20250507140528581

    部署到远程主机后,能够正常进行请求,项目部署成功。

    image-20250507141307041