Selaa lähdekoodia

feat: 完善对话部分的组件

zhupei@smartai.com 1 vuosi sitten
vanhempi
sitoutus
507db45102

+ 1 - 0
src/base/storage.js

@@ -11,6 +11,7 @@ import { MyCookie } from './storage/cookie';
 export const localST_Theme = new MyLocalStorage('theme'); // 主题
 export const localST_Locale = new MyLocalStorage('locale'); // 多语言
 export const localST_RecentAgent = new MyLocalStorage('recent_agent'); // 近期使用的agent
+export const localST_AllAgent = new MyLocalStorage('full_agent'); // 所有可用的agent
 
 /**
  * session storage定义

+ 32 - 0
src/base/store/use-all-agents.js

@@ -1,5 +1,26 @@
 import { defineStore } from 'pinia';
 import { jsonStringify, jsonParse } from '../../utils/utils';
+import { localST_AllAgent } from '../storage';
+
+function allLocalSet(arr) {
+  // 在本地保存记录
+  const result_str = jsonStringify(arr);
+
+  localST_AllAgent.setItem(result_str);
+}
+
+function allLocalGet() {
+  const result_str = localST_AllAgent.getItem();
+
+  const result_list = jsonParse(result_str);
+
+  if (result_list === null) {
+    // json解析发生了错误
+    return [];
+  } else {
+    return result_list;
+  }
+}
 
 /**
  * 系统所有可用的agents列表
@@ -16,12 +37,23 @@ export const useAllAgentStore = defineStore('all_agents', {
      * @returns
      */
     getList() {
+      const cur_list = this.agents;
+
+      if (cur_list.length === 0) {
+        const recent_list = allLocalGet();
+
+        this.agents = recent_list;
+
+        return this.agents;
+      }
+
       return this.agents;
     },
     setList(arr) {
       const list = jsonParse(jsonStringify(arr));
 
       this.agents = list;
+      allLocalSet(list);
     }
   }
 });

+ 13 - 0
src/modules/conversation-board/api-conversation-board.js

@@ -72,6 +72,19 @@ const res_list_example = {
  * @see https://app.apifox.com/link/project/5404857/apis/api-233232945
  */
 export const conversationBoardAgentList = (params) => {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      resolve(res_list_example);
+    }, 1000);
+  });
+};
+
+/**
+ * 获取所有对话可用的智能体
+ * 正式环境需要使用的会话列表
+ * @see https://app.apifox.com/link/project/5404857/apis/api-233232945
+ */
+export const conversationBoardAgentListBackup = (params) => {
   const request_url = `/api/dialog/list`;
 
   const send_data = {

+ 67 - 0
src/modules/conversation-board/use-agent-list-4-board.js

@@ -0,0 +1,67 @@
+import { ref } from 'vue';
+
+import { conversationBoardAgentList } from './api-conversation-board';
+
+import { useAllAgentStore } from '../../base/store/use-all-agents';
+
+/**
+ * 获取Agent list
+ * 用于会话 智能体大全
+ * @param {object} opts
+ * @returns
+ */
+export default function useAgentList4Board(opts) {
+  // api请求的状态
+  const api_status = ref(1); // api状态 1 请求未启动 2 请求进心中 3 请求已成功 4 请求失败(网络成功,但是code不等于200) 5 请求出错
+  const api_message = ref(''); // 请求消息 与 api_status 对应的 message 消息内容
+
+  // 历史记录列表数据
+  const list_agents = ref([]); // 历史记录的列表数据
+
+  const allAgents = useAllAgentStore();
+
+  /**
+   * 调用api请求
+   */
+  const handleApiRequest = (opts) => {
+    // 进入加载状态
+    api_status.value = 2;
+    api_message.value = '';
+    conversationBoardAgentList(opts)
+      .then((res) => {
+        if (res.code / 1 === 200) {
+          // 成功
+          const res_list = res.data; // 历史记录列表
+
+          list_agents.value = res_list;
+
+          // 如果是获取所有项目的操作,则将结果进行缓存
+          if (opts && Object.keys(opts).length === 0) {
+            // 在store中缓存数据
+            allAgents.setList(res_list);
+          }
+
+          // api请求成功
+          api_status.value = 3;
+          api_message.value = 'success';
+        } else {
+          // api请求成功,但是业务失败
+          api_status.value = 4;
+          api_message.value = res.msg;
+        }
+      })
+      .catch((err) => {
+        console.log(err);
+        // api请求出错
+        api_status.value = 5;
+        api_message.value = err.message;
+      });
+  };
+
+  return {
+    getAgentList: handleApiRequest,
+    api_status,
+    api_message,
+    list_agents
+  };
+}

+ 89 - 0
src/modules/conversation-board/use-agent-list-4-history.js

@@ -0,0 +1,89 @@
+import { ref } from 'vue';
+import { conversationBoardAgentList } from './api-conversation-board';
+
+import { useAllAgentStore } from '../../base/store/use-all-agents';
+
+/**
+ * 获取Agent list
+ * 用于会话历史记录类别筛选
+ * @param {function} onSuccess
+ * @returns
+ */
+export default function useAgentList4History(onSuccess) {
+  // api请求的状态
+  const api_status = ref(1); // api状态 1 请求未启动 2 请求进心中 3 请求已成功 4 请求失败(网络成功,但是code不等于200) 5 请求出错
+  const api_message = ref(''); // 请求消息 与 api_status 对应的 message 消息内容
+
+  // 历史记录列表数据
+  const list_agents = ref([]); // 历史记录的列表数据
+
+  const allAgents = useAllAgentStore();
+
+  /**
+   * 处理请求参数
+   */
+  const formatListOptions = () => {
+    return {};
+  };
+
+  /**
+   * 调用api请求
+   */
+  const handleApiRequest = () => {
+    // TODO: 从本地store中获取,而非api
+    // 否则每次进入此页面,都需要调用接口
+    const cache_agents = allAgents.agents;
+    if (cache_agents.length !== 0) {
+      if (typeof onSuccess === 'function') {
+        onSuccess(cache_agents[0]);
+      }
+
+      list_agents.value = cache_agents;
+      return;
+    }
+
+    // 处理查询参数
+    const opts = formatListOptions();
+
+    // 进入加载状态
+    api_status.value = 2;
+    api_message.value = '';
+    conversationBoardAgentList(opts)
+      .then((res) => {
+        if (res.code / 1 === 200) {
+          // 成功
+          const res_list = res.data; // 历史记录列表
+          if (res_list.length !== 0) {
+            if (typeof onSuccess === 'function') {
+              onSuccess(res_list[0]);
+            }
+          }
+
+          list_agents.value = res_list;
+          // 在store中缓存数据
+          allAgents.setList(res_list);
+
+          // api请求成功
+          api_status.value = 3;
+          api_message.value = 'success';
+        } else {
+          // api请求成功,但是业务失败
+          api_status.value = 4;
+          api_message.value = err.message;
+        }
+      })
+      .catch((err) => {
+        console.log(err);
+        // api请求出错
+        api_status.value = 5;
+        api_message.value = err.msg;
+      });
+  };
+
+  return {
+    getAgentList: handleApiRequest,
+    api_status,
+    api_message,
+    list_agents
+  };
+}

+ 122 - 9
src/modules/conversation-chat/DialogInput.vue

@@ -1,21 +1,134 @@
 <template>
   <div class="dialog-input">
-    <div>会话输入组件</div>
+    <div>
+      <a-textarea placeholder="输入文本内容" allow-clear />
+    </div>
+    <div>
+      <div>
+        <div>联网查询</div>
+      </div>
+
+      <div>
+        <div v-if="enable_frequent_question">常用问题定义</div>
+        <div v-if="enable_file_upload">文件上传</div>
+        <div v-if="enable_voice_recognition">语音识别</div>
+        <div @click="handleSubmit">发送</div>
+      </div>
+    </div>
+
+    <div>文件上传预览区: 当点击文件上传,选择某个文件后,此处需要展示进行预览。</div>
   </div>
 </template>
 
-<script setup></script>
+<script setup>
+const props = defineProps({
+  /**
+   * 是否禁用操作
+   * 默认 false
+   * 当此值为 true 时,此组件的任何功能都不可用
+   * 当对话正在进行时,此值应为 true,意味着不允许继续对话
+   * 直到一轮对话结束,或者一轮对话终止,此值才应该变为 false
+   */
+  disabled: {
+    type: Boolean,
+    default: false,
+    required: false
+  },
+  /**
+   * 是否开启文件上传功能
+   * 默认 true,即默认开启
+   * 当此值为 false 时,该组件将没有文件上传按钮
+   */
+  enable_file_upload: {
+    type: Boolean,
+    default: true,
+    required: false
+  },
+  /**
+   * 是否开启 常用问题 功能
+   * 如果此值为false,则 常用问题 定义功能不可用
+   */
+  enable_frequent_question: {
+    type: Boolean,
+    default: true,
+    required: false
+  },
+  /**
+   * 是否开启音频识别功能
+   * 如果此值为 false,则语音识别功能不可用
+   */
+  enable_voice_recognition: {
+    type: Boolean,
+    default: false,
+    required: false
+  },
+  /**
+   * 在 enable_file_upload === true 的前提下
+   * 文件上传的类型,默认是 *,不限类型
+   * 对应html属性 accept
+   * 对应 arco.design Upload accept 属性
+   * @see https://arco.design/vue/component/upload#API
+   * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#htmlattrdefaccept
+   */
+  file_upload_type: {
+    type: String,
+    default: '*',
+    required: false
+  },
+  /**
+   * 在 enable_file_upload === true 的前提下
+   * 文件上传的数量限制
+   * 默认值为0,表示不限制
+   * 对应 arco.design Upload limit 属性
+   */
+  file_upload_limit: {
+    type: Number,
+    default: 0,
+    required: false
+  },
+  /**
+   * 输入框文本占位提示
+   */
+  text_placeholder: {
+    type: String
+  },
+  /**
+   * 输入的文本长度限制
+   * 默认值为0,表示不限制长度
+   * 对应 arco.design textarea max-length 属性
+   * @see https://arco.design/vue/component/textarea#API
+   */
+  text_limit: {
+    type: Number,
+    default: 0,
+    required: false
+  },
+  /**
+   * 是否显示字数统计
+   * 对应 arco.design textarea show-word-limit
+   * @see https://arco.design/vue/component/textarea#API
+   */
+  show_text_count: {
+    type: Boolean,
+    default: false,
+    required: false
+  }
+});
 
-<style lang="css" scoped>
-.dialog-input {
-  /* margin-top: 30px;
-  margin-bottom: 30px; */
+const emit = defineEmits(['inputTextChange', 'inputFileChange', 'submit']);
 
-  /* text-align: center; */
+const handleSubmit = () => {
+  // 检查是否是禁用状态
+  if (props.disabled) {
+    return;
+  }
 
-  /* font-size: 32px;
-  font-weight: bold; */
+  emit('submit');
+};
+</script>
 
+<style lang="css" scoped>
+.dialog-input {
   color: var(--color-text-1);
 }
 </style>

+ 16 - 9
src/modules/conversation-chat/DialogItem.vue

@@ -1,18 +1,25 @@
 <template>
-  <div class="dialog-item">
-    <div>对话的每一项</div>
+  <div class="dialog-item-container">
+    <DialogQuestion />
+    <DialogAnswer />
   </div>
 </template>
 
-<script setup></script>
+<script setup>
+import DialogQuestion from './dialog-item/DialogQuestion.vue';
+import DialogAnswer from './dialog-item/DialogAnswer.vue';
 
-<style lang="css" scoped>
-.dialog-item {
-  margin-top: 30px;
-  margin-bottom: 30px;
+const props = defineProps({});
+</script>
 
-  /* text-align: center; */
+<style lang="css" scoped>
+.dialog-item-container {
+}
 
+/* .dialog-item {
   color: var(--color-text-1);
-}
+
+  margin-top: 5px;
+  margin-bottom: 5px;
+} */
 </style>

+ 31 - 7
src/modules/conversation-chat/DialogList.vue

@@ -1,6 +1,10 @@
 <template>
   <a-scrollbar type="track" :style="container_style">
-    <div class="dialog-list">
+    <div class="dialog-welcome" v-if="show_welcome">
+      <DialogWelcome />
+    </div>
+
+    <div class="dialog-list" v-if="!show_welcome">
       <DialogItem />
 
       <DialogItem />
@@ -23,16 +27,41 @@
       <DialogItem />
       <DialogItem />
       <DialogItem />
+
+      <DialogSuggestion :suggestion="suggestion" />
     </div>
   </a-scrollbar>
 </template>
 
 <script setup>
 import { computed } from 'vue';
+import DialogWelcome from './DialogWelcome.vue';
 import DialogItem from './DialogItem.vue';
+import DialogSuggestion from './DialogSuggestion.vue';
 
 const props = defineProps({
-  height: Number
+  height: Number,
+
+  /**
+   * 智能体的ID
+   */
+  agent_id: String,
+
+  /**
+   * 对话内容数据项
+   */
+  list: Array,
+
+  /**
+   * 推荐项
+   */
+  suggestion: Object,
+
+  /**
+   * 是否显示欢迎页
+   * 当用户进入对话,还未发送消息时,应该显示欢迎页面
+   */
+  show_welcome: Boolean
 });
 
 const container_style = computed(() => {
@@ -45,11 +74,6 @@ const container_style = computed(() => {
 
 <style lang="css" scoped>
 .dialog-list {
-  /* margin-top: 30px;
-  margin-bottom: 30px; */
-
-  /* text-align: center; */
-
   color: var(--color-text-1);
 }
 </style>

+ 20 - 0
src/modules/conversation-chat/DialogSuggestion.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="dialog-suggestion">
+    <div>对话推荐项,当一轮对话结束后,可能有其他推荐的内容项目,在对话结束处进行展示。</div>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  suggestion: Object
+});
+</script>
+
+<style lang="css" scoped>
+.dialog-suggestion {
+  margin-top: 30px;
+  margin-bottom: 30px;
+
+  color: var(--color-text-1);
+}
+</style>

+ 20 - 0
src/modules/conversation-chat/DialogWelcome.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="dialog-welcome-content">
+    <div>对话欢迎内容:欢迎内容应该按照agent进行配置,最好是从后端进行返回</div>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  suggestion: Object
+});
+</script>
+
+<style lang="css" scoped>
+.dialog-welcome {
+  margin-top: 30px;
+  margin-bottom: 30px;
+
+  color: var(--color-text-1);
+}
+</style>

+ 47 - 0
src/modules/conversation-chat/dialog-item/DialogAnswer.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="dialog-item dialog-anwser">
+    <div class="dialog-item-select answer-select"></div>
+
+    <div class="dialog-main answer-main">
+      <div class="dialog-icon answer-icon"></div>
+      <div class="dialog-content answer-content">
+        <div class="dialog-chat-core-content">这里是聊天内容的区域</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+/**
+ * 智能体的回答内容,可能需要包括下面一些功能
+ *
+ * 1. 文档引用,预览
+ * 2. 图片展示、编辑、预览
+ * 3. 代码展示
+ * 4. 回答反馈:分享、点赞、复制
+ */
+</script>
+
+<style src="./dialog-item-common.css" />
+<style lang="css" scoped>
+.dialog-anwser {
+  color: var(--color-text-1);
+
+  display: flex;
+  justify-content: space-between;
+}
+
+.answer-select {
+}
+
+.answer-main {
+}
+.answer-content {
+  display: flex;
+  justify-content: flex-start;
+}
+
+.answer-icon {
+  margin-right: 5px;
+}
+</style>

+ 38 - 0
src/modules/conversation-chat/dialog-item/DialogQuestion.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="dialog-item dialog-question">
+    <div class="dialog-item-select question-select"></div>
+
+    <div class="dialog-main question-main">
+      <div class="dialog-content question-content">
+        <div class="dialog-chat-core-content">这里是聊天内容的区域</div>
+      </div>
+      <div class="dialog-icon question-icon"></div>
+    </div>
+  </div>
+</template>
+
+<script setup></script>
+
+<style src="./dialog-item-common.css" />
+<style lang="css" scoped>
+.dialog-question {
+  color: var(--color-text-1);
+
+  display: flex;
+  justify-content: space-between;
+}
+
+.question-select {
+}
+
+.question-main {
+}
+
+.question-content {
+  display: flex;
+  justify-content: flex-end;
+}
+.question-icon {
+  margin-left: 5px;
+}
+</style>

+ 40 - 0
src/modules/conversation-chat/dialog-item/dialog-item-common.css

@@ -0,0 +1,40 @@
+.dialog-item {
+  color: var(--color-text-1);
+
+  display: flex;
+  justify-content: space-between;
+
+  margin-top: 5px;
+  margin-bottom: 5px;
+  /* background-color: #fff; */
+}
+
+.dialog-item-select {
+  width: 20px;
+  height: 20px;
+  background-color: red;
+}
+
+.dialog-main {
+  /* background-color: green; */
+  flex-grow: 1;
+
+  display: flex;
+}
+.dialog-content {
+  flex-grow: 1;
+
+  display: flex;
+}
+.dialog-chat-core-content {
+  background-color: #ccc;
+  border-radius: 5px;
+  padding: 8px;
+}
+.dialog-icon {
+  width: 40px;
+  height: 40px;
+  background-color: blue;
+  border-radius: 25px;
+  flex-shrink: 0;
+}

+ 178 - 0
src/modules/conversation-chat/use-websocket-chat.js

@@ -0,0 +1,178 @@
+import { ref } from 'vue';
+import { kuky_Authorization } from '../../base/storage';
+
+/**
+ * 获取websocket连接的地址
+ * @param {string} type - 对话类型,可选值:report/excel/chat
+ * @param {string} agent_id - 对话使用的agent ID
+ * @param {string} chat_id - 对话记录的ID
+ * @returns
+ */
+function getWSUrl(type, agent_id, chat_id) {
+  const host = location.host;
+  const token = kuky_Authorization.get();
+
+  if (type === 'report') {
+    return `ws://${host}/api/${type}/ws/${agent_id}/${chat_id}?token=${token}`;
+  } else if (type === 'chat') {
+    return `ws://${host}/api/${type}/ws/${agent_id}/${chat_id}?token=${token}`;
+  } else if (type === 'excel') {
+    return `ws://${host}/api/document/ws/excel`;
+  } else {
+    return `ws://${host}/api/chat/ws/${agent_id}/${chat_id}?token=${token}`;
+  }
+}
+
+/**
+ * 确保一个值是函数
+ * 如果值本身是函数,则返回值本身
+ * 如果值本身不是函数,则返回一个空函数
+ * @param {function} fn
+ * @returns
+ */
+function protectedFunction(fn) {
+  if (typeof fn === 'function') {
+    return fn;
+  }
+
+  return Function.prototype;
+}
+
+/**
+ * 使用websocket进行对话
+ * @see https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API
+ * @param {string} agent_id - agent id
+ * @param {string} chat_id - 对话ID
+ * @param {string} type - 对话类型,可选值:report/excel/chat
+ * @param {object} trigger - 触发的方法
+ * @param {function} trigger.onMessage - 当收到消息时触发的方法
+ * @param {function} trigger.onClose - 当websocket关闭时触发的方法
+ * @param {function} trigger.onError - 当websocket出错时触发的方法
+ * @returns
+ */
+export default function useWebSocketChat(trigger) {
+  let delay_message_list = [];
+
+  const onMessage = protectedFunction(trigger.onMessage);
+  const onClose = protectedFunction(trigger.onClose);
+  const onError = protectedFunction(trigger.onError);
+
+  /**
+   * websocket当前状态
+   * @see https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/readyState
+   * 0 CONNECTING 套接字已经创建,但是连接尚未打开
+   * 1 OPEN 连接已经打开,准备进行通信
+   * 2 CLOSING 连接正在关闭中
+   * 3 CLOSED 连接已关闭或者无法打开
+   * -1 此处自定义值,未启动
+   */
+  const websocket_status = ref(-1);
+
+  /**
+   * 创建一个websocket连接
+   * @returns
+   */
+  function createWSConnection(agent_id, chat_id, type) {
+    const wsUrl = getWSUrl(type, agent_id, chat_id);
+
+    const wskt = new WebSocket(wsUrl);
+
+    // 当 WebSocket 打开时...
+    wskt.onopen = function (event) {
+      delay_message_list.forEach((item) => {
+        wskt.send(item);
+      });
+
+      // 所有消息发送完成后,清空消息队列
+      delay_message_list = [];
+    };
+
+    // 当 WebSocket 关闭时...
+    wskt.onclose = function (event) {
+      onClose(event);
+    };
+
+    // 当有错误发生时...
+    wskt.onerror = function (error) {
+      onError(error);
+    };
+
+    /**
+     * 当收到websocket消息时
+     * @param {WebSocketEvent} event
+     */
+    wskt.onmessage = (event) => {
+      onMessage(event);
+    };
+
+    return wskt;
+  }
+
+  let websocket = null;
+
+  return {
+    /**
+     * 初始化websocket连接
+     * @param {string} agent_id
+     * @param {string} chat_id
+     * @param {string} type
+     */
+    initWSConnection(agent_id, chat_id, type) {
+      if (websocket === null) {
+        // websocket未创建:立即创建
+        websocket = createWSConnection(agent_id, chat_id, type);
+      }
+
+      return websocket;
+    },
+    /**
+     * 发送会话消息
+     * @param {string} msg
+     */
+    sendWSMessage(msg) {
+      if (websocket === null) {
+        // websocket未创建:立即创建
+        websocket = createWSConnection();
+      }
+
+      // 判断socket是否开启
+      // 如果开启,则直接发送,如果没有开启,则将数据推入消息队列
+      if (websocket.readyState === 0) {
+        delay_message_list.push(msg);
+      }
+
+      if (websocket.readyState === 1) {
+        websocket.send(msg);
+      }
+
+      if (websocket.readyState === 2) {
+        // 正在关闭
+        console.log('web socket正在关闭');
+      }
+
+      if (websocket.readyState === 3) {
+        delay_message_list.push(msg);
+
+        // 已关闭: 重新创建socket
+        websocket = createWSConnection();
+      }
+    },
+    /**
+     * 获取websocket实例本身
+     * @returns
+     */
+    getWSInstance() {
+      return websocket;
+    },
+    /**
+     * 结束对话,断开websocket连接
+     */
+    closeWSConnect() {
+      if (websocket === null) {
+        return;
+      }
+
+      websocket.close();
+    }
+  };
+}

+ 113 - 0
src/modules/conversation-dialog/api-dialog.js

@@ -1,3 +1,7 @@
+import ajax from '../../base/ajax';
+
+import { kuky_Authorization } from '../../base/storage';
+
 /**
  * 获取会话历史记录,来自于小数系统
  * http://192.168.20.119:9301/api/agent/90d533729c0211efbf6b0242ac160006/sessions
@@ -11,3 +15,112 @@
  * 新建会话
  * http://192.168.20.119:9301/api/agent/get-chat-id/42e4fcdc9bea11efac300242ac160006
  */
+
+/**
+ * 上传文档
+ * @param {string} id - agentId
+ * @param {files} files - files
+ * @returns {Promise<Object>}
+ */
+const uploadFiles = (id, file) => {
+  return ajax.post(`/api/files/upload/${id}`, file, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+const uploadExcels = (file) => {
+  return ajax.post('/api/document/excel/upload', file, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+const uploadFile = (id, chatId, file) => {
+  return ajax.post(`/api/files/upload/${id}?chat_id=${chatId}`, file, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+/**
+ * 新建对话
+ * @returns {Promise<Object>}
+ */
+const createDialog = (agent_id) => {
+  // 返回数据示例: {"code":200,"msg":"","data":{"chat_id":"44f835504943467f9a9cde261299a06f"}}
+
+  return ajax.get(`/api/agent/get-chat-id/${agent_id}`);
+};
+
+/**
+ * 获取某个agent的所有对话记录
+ * @returns {Promise<Object>}
+ */
+const getDialogs = (agent_id) => {
+  return ajax.get(`/api/agent/${agent_id}/sessions`);
+};
+
+/**
+ * 获取某个对话的对话记录
+ * @returns {Promise<Object>}
+ */
+const getHistoryLogs = (agent_id, conversation_id) => {
+  return ajax.get(`/api/agent/${agent_id}/${conversation_id}/session_log`);
+};
+
+// 对话过程:
+
+// ws://localhost/api/chat/ws/42e4fcdc9bea11efac300242ac160006/44f835504943467f9a9cde261299a06f?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIiwidXNlcl9pZCI6MjEsImV4cCI6MTczNDAxMTU4NH0.tLVyD5ds3BqJ8UCZ1pnaUVBhqZ0leqXNUQhG5U2A2u0
+
+const create = (token, agentId, dialogId, type) => {
+  // const wsParam = type === 'report' ? 'report' : 'chat';
+  // const wsUrl =
+  //   type === 'excel'
+  //     ? `ws://${location.host}/api/document/ws/excel`
+  //     : `ws://${location.host}/api/${wsParam}/ws/${agentId}/${dialogId}?token=${token}`;
+
+  const wsUrl = getWSUrl(type, agentId, dialogId);
+
+  const ws = new WebSocket(wsUrl);
+  // 当 WebSocket 打开时...
+  ws.onopen = function (event) {
+    console.log('WebSocket 连接已打开');
+  };
+
+  // 当 WebSocket 关闭时...
+  ws.onclose = function (event) {
+    console.log(event.code);
+  };
+
+  // 当有错误发生时...
+  ws.onerror = function (error) {
+    console.error('WebSocket 出错: ', error);
+  };
+  return ws;
+};
+
+/**
+ * 获取websocket连接的地址
+ * @param {string} type - 对话类型,可选值:report/excel/chat
+ * @param {string} agent_id - 对话使用的agent ID
+ * @param {string} dialog_id - 对话记录的ID
+ * @returns
+ */
+function getWSUrl(type, agent_id, dialog_id) {
+  const host = location.host;
+  const token = kuky_Authorization.get();
+
+  if (type === 'report') {
+    return `ws://${host}/api/${type}/ws/${agent_id}/${dialog_id}?token=${token}`;
+  } else if (type === 'chat') {
+    return `ws://${host}/api/${type}/ws/${agent_id}/${dialog_id}?token=${token}`;
+  } else if (type === 'excel') {
+    return `ws://${host}/api/document/ws/excel`;
+  } else {
+    return `ws://${host}/api/chat/ws/${agent_id}/${dialog_id}?token=${token}`;
+  }
+}

+ 4 - 5
src/modules/conversation-history/BtnHistoryItemDelete.vue

@@ -40,12 +40,11 @@ const handleClick = () => {
 
     // content: '删除后无法恢复'
     onOk(e) {
-      console.log('确定');
-
+      // console.log('确定');
       // handleConfirmDelete();
     },
     async onBeforeOk() {
-      console.log('ok之前1');
+      // console.log('ok之前1');
 
       // await (() => {
       //   return new Promise((resolve) => {
@@ -53,7 +52,7 @@ const handleClick = () => {
       //   });
       // })();
 
-      console.log('ok之前2');
+      // console.log('ok之前2');
 
       try {
         const result = await handleConfirmDeleteV2();
@@ -70,7 +69,7 @@ const handleClick = () => {
       }
     },
     onCancel() {
-      console.log('取消');
+      // console.log('取消');
     }
   });
 };

+ 0 - 1
src/modules/conversation-history/HistoryItem.vue

@@ -125,7 +125,6 @@ const time_display = computed(() => {
  * 分发ID值
  */
 const handleClick = () => {
-  console.log('点击一下');
   emit('active', props.history.id);
 };
 

+ 8 - 74
src/modules/conversation-history/HistoryTypeDropdown.vue

@@ -9,7 +9,7 @@
         v-for="(item, index) of list_agents"
         :key="item.id"
         :disabled="disabled_options"
-        @click="handleOptionClick(item)"
+        @click="handleChatTypeConfirm(item)"
         >{{ item.name }}</a-doption
       >
     </template>
@@ -18,43 +18,33 @@
 
 <script setup>
 import { ref, computed, onMounted } from 'vue';
-import { conversationBoardAgentList } from '../conversation-board/api-conversation-board';
 
-import { useAllAgentStore } from '../../base/store/use-all-agents';
+import useAgentList4History from '../conversation-board/use-agent-list-4-history';
 
 const props = defineProps({
   /**
    * api状态
    * 如果状态值为2,则不允许切换
    */
-  api_status: Number
+  list_status: Number
 });
 
 const btn_label = ref('');
 
-// api请求的状态
-const api_status = ref(1); // api状态 1 请求未启动 2 请求进心中 3 请求已成功 4 请求失败(网络成功,但是code不等于200) 5 请求出错
-const api_message = ref(''); // 请求消息 与 api_status 对应的 message 消息内容
-
-// 历史记录列表数据
-const list_agents = ref([]); // 历史记录的列表数据
+const { api_status, api_message, list_agents, getAgentList } = useAgentList4History((item) => {
+  handleChatTypeConfirm(item);
+});
 
 const emit = defineEmits(['typeSelected']);
 
-const allAgents = useAllAgentStore();
-
 /**
  * 是否禁用下拉菜单
  * 当列表正在加载时,应该禁用下拉菜单
  */
 const disabled_options = computed(() => {
-  return props.api_status === 2;
+  return props.list_status === 2;
 });
 
-const handleOptionClick = (item) => {
-  handleChatTypeConfirm(item);
-};
-
 /**
  * 已经确定类型
  */
@@ -64,65 +54,9 @@ const handleChatTypeConfirm = (item) => {
   emit('typeSelected', item);
 };
 
-/**
- * 处理请求参数
- */
-const formatListOptions = () => {
-  return {};
-};
-
-/**
- * 调用api请求
- */
-const handleApiRequest = () => {
-  // TODO: 从本地store中获取,而非api
-  // 否则每次进入此页面,都需要调用接口
-  const cache_agents = allAgents.agents;
-  if (cache_agents.length !== 0) {
-    handleChatTypeConfirm(cache_agents[0]);
-    list_agents.value = cache_agents;
-    return;
-  }
-
-  // 处理查询参数
-  const opts = formatListOptions();
-
-  // 进入加载状态
-  api_status.value = 2;
-  api_message.value = '';
-  conversationBoardAgentList(opts)
-    .then((res) => {
-      if (res.code / 1 === 200) {
-        // 成功
-        const res_list = res.data; // 历史记录列表
-        if (res_list.length !== 0) {
-          handleChatTypeConfirm(res_list[0]);
-        }
-
-        list_agents.value = res_list;
-        // 在store中缓存数据
-        allAgents.setList(res_list);
-
-        // api请求成功
-        api_status.value = 3;
-        api_message.value = 'success';
-      } else {
-        // api请求成功,但是业务失败
-        api_status.value = 4;
-        api_message.value = err.message;
-      }
-    })
-    .catch((err) => {
-      console.log(err);
-      // api请求出错
-      api_status.value = 5;
-      api_message.value = err.msg;
-    });
-};
-
 onMounted(() => {
   // 调用api
-  handleApiRequest();
+  getAgentList();
 });
 </script>
 

+ 2 - 3
src/views/ConversationBoard.vue

@@ -26,12 +26,11 @@ const content_style = {
   backgroundSize: 'contain'
 };
 
-const handleAgentActive = () => {
+const handleAgentActive = (target_id) => {
   router.push({
     path: '/chat/qa',
     query: {
-      id: 'hsjaklfja',
-      age: 18
+      agent: target_id
     }
   });
 };

+ 3 - 2
src/views/ConversationHistory.vue

@@ -26,11 +26,12 @@ const content_style = {
   backgroundSize: 'cover'
 };
 
-const handleHistoryActive = ({ target_id }) => {
+const handleHistoryActive = ({ target_id, agent_id }) => {
   router.push({
     path: '/chat/qa',
     query: {
-      id: target_id
+      session: target_id,
+      agent: agent_id
     }
   });
 };

+ 136 - 1
src/views/ConversationQA.vue

@@ -3,12 +3,26 @@
     <ContentContainer>
       <ChatAsideMenu />
 
-      <ConversationChatContainer />
+      <template v-if="query_value_status === 2">
+        <ConversationChatContainer :agent_id="chat_agent" :session_id="chat_session" />
+      </template>
+
+      <template v-if="query_value_status === 3">
+        <a-result status="error" :title="query_value_message">
+          <template #extra>
+            <a-space>
+              <a-button type="primary" @click="handleBack">返回</a-button>
+            </a-space>
+          </template>
+        </a-result>
+      </template>
     </ContentContainer>
   </LayoutExperience>
 </template>
 
 <script setup>
+import { ref, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 import LayoutExperience from './layout/LayoutExperience.vue';
 import ContentContainer from './layout/ContentContainer.vue';
 
@@ -16,7 +30,128 @@ import ChatAsideMenu from './conversation/ChatAsideMenu.vue';
 
 import ConversationChatContainer from './conversation/ConversationChatContainer.vue';
 
+import { useAllAgentStore } from '../base/store/use-all-agents';
+
+/**
+ * 此页面为对话页面
+ * 用户可能从如下几个地方进入对话页面:
+ * 1. 从对话历史记录,点击某个记录,进入对话页面,查看此记录的对话内容,此时,路由会存在两个参数
+ *    agent 历史记录对应的agent ID
+ *    session 历史记录的ID
+ * 2. 从对话智能体大全页面,点击某个智能体,进入对话页面,新建该智能体对应的对话,此时,路由会有一个参数
+ *    agent 对应的agent ID
+ * 此页面大致流程如下:
+ * 1. 检查参数是否合法
+ *    A. 检查是否存在必须参数 agent
+ *    B. 如果agent参数存在,则检查agent值是否在缓存中存在,如果不存在,则意味着有人在地址栏修改参数值,无效的agent值于对话没有意义,故,直接做报错处理
+ * 2. 在参数合法的情况下,进入对话流程
+ *
+ * 根据必须参数 agent ID,可能会存在如下相关的操作:
+ * 1. 此agent对应的主题背景
+ * 2. 此agent可用、不可用的功能设定
+ */
+
+const route = useRoute();
+const router = useRouter();
+
+const allAgent = useAllAgentStore();
+
+const chat_agent = ref('');
+const chat_session = ref('');
+
+const query_value_status = ref(1); // 1 初始化状态 2 合法有效 3 非法无效,报错
+const query_value_message = ref('');
+
+const handleBack = () => {
+  router.back();
+};
+
+/**
+ * 检查是否存在合法的agent参数值
+ * @param agent_id
+ */
+async function isValidAgent(agent_id) {
+  if (!agent_id) {
+    return false;
+  }
+
+  // 检查缓存数据中是否存在此agent id值
+  const agent_list = allAgent.getList();
+
+  if (agent_list.length === 0) {
+    // 通过接口获取所有的agent数据
+    // TOOD: 如果agent_list为空,则意味着内存和本地存储中都没有记录,则需要通过接口获取agent列表数据
+    return true;
+  } else {
+    const has_cache = agent_list.find((item) => item.id === agent_id);
+
+    if (!has_cache) {
+      // 缓存数据中不存在此ID值,非法的操作
+      // 可能是有人通过地址栏修改参数值
+      return false;
+    }
+
+    return true;
+  }
+}
+
+onMounted(async () => {
+  const query = route.query;
+
+  const agent_id = query.agent;
+  const session_id = query.session;
+
+  if (!agent_id) {
+    // 不存在参数
+    query_value_status.value = 3;
+    query_value_message.value = '非法操作:参数异常';
+
+    return;
+  }
+
+  // 有agent参数
+  // 判断已经缓存的agent列表是否存在
+
+  const is_valid = await isValidAgent(agent_id);
+
+  if (!is_valid) {
+    // 不存在参数
+    query_value_status.value = 3;
+    query_value_message.value = '非法操作:参数异常';
+
+    return;
+  }
+
+  query_value_status.value = 2;
+  query_value_message.value = 'success';
+
+  chat_agent.value = agent_id;
+  chat_session.value = session_id;
+});
+
 /**
  * 处理路由参数
+ * 1. 历史记录里,点击某个历史记录,进入对话页面,查看历史对话,可以进行后续对话
+ *    显示历史对话
+ * 2. 智能体大全,点击某个智能体,进入对话页面,创建新对话
+ * 3. 近期使用的智能体,点击某个智能体,进入对话页面,创建新对话
+ *
+ * 对话中可能存在的其他功能:分享、常用语输入、推荐、文件上传
+ * 当上次对话正在输出时,不允许下一条提问
+ *
+ * 首页:用户直接提问
+ *
+ * 主题背景自定义
+ */
+
+/**
+ * 从历史记录过来的路由参数:
+ * session 历史记录对应的ID
+ * agent agent id
+ */
+
+/**
+ * 从agent过来的路由参数
+ * agent
  */
 </script>

+ 11 - 57
src/views/conversation/ConversationBoardContainer.vue

@@ -44,28 +44,13 @@ import BoardWelcome from '../../modules/conversation-board/BoardWelcome.vue';
 import BoardTags from '../../modules/conversation-board/BoardTags.vue';
 import BoardAgentList from '../../modules/conversation-board/BoardAgentList.vue';
 
-import { conversationBoardAgentList } from '../../modules/conversation-board/api-conversation-board';
-import { useAllAgentStore } from '../../base/store/use-all-agents';
+import useAgentList4Board from '../../modules/conversation-board/use-agent-list-4-board';
 
 const emit = defineEmits(['agentActive']);
 
 const selected_id = ref('');
 
-// api请求的状态
-const api_status = ref(1); // api状态 1 请求未启动 2 请求进心中 3 请求已成功 4 请求失败(网络成功,但是code不等于200) 5 请求出错
-const api_message = ref(''); // 请求消息 与 api_status 对应的 message 消息内容
-
-// 历史记录列表数据
-const list_agents = ref([]); // 历史记录的列表数据
-
-// store中保存的agents
-const allAgents = useAllAgentStore();
-
-const handleTagChange = (tag_id) => {
-  selected_id.value = tag_id;
-
-  handleApiRequest();
-};
+const { api_status, api_message, list_agents, getAgentList } = useAgentList4Board();
 
 const handleItemActive = (target_id) => {
   emit('agentActive', target_id);
@@ -84,52 +69,21 @@ const formatListOptions = () => {
   return obj;
 };
 
-/**
- * 调用api请求
- */
-const handleApiRequest = () => {
-  // const cache_agents = allAgents.agents;
-  // if (cache_agents.length !== 0) {
-  //   list_agents.value = cache_agents;
-  //   return;
-  // }
-
-  // 处理查询参数
+const handleRequest = () => {
   const opts = formatListOptions();
 
-  // 进入加载状态
-  api_status.value = 2;
-  api_message.value = '';
-  conversationBoardAgentList(opts)
-    .then((res) => {
-      if (res.code / 1 === 200) {
-        // 成功
-        const res_list = res.data; // 历史记录列表
-
-        list_agents.value = res_list;
-        // // 在store中缓存数据
-        // allAgents.setList(res_list);
-
-        // api请求成功
-        api_status.value = 3;
-        api_message.value = 'success';
-      } else {
-        // api请求成功,但是业务失败
-        api_status.value = 4;
-        api_message.value = res.msg;
-      }
-    })
-    .catch((err) => {
-      console.log(err);
-      // api请求出错
-      api_status.value = 5;
-      api_message.value = err.message;
-    });
+  getAgentList(opts);
+};
+
+const handleTagChange = (tag_id) => {
+  selected_id.value = tag_id;
+
+  handleRequest();
 };
 
 onMounted(() => {
   // 调用api
-  handleApiRequest();
+  handleRequest();
 });
 </script>
 

+ 195 - 3
src/views/conversation/ConversationChatContainer.vue

@@ -8,9 +8,25 @@
       <DialogHeader class="conversation-chat-header" :style="{ height: header_height + 'px' }" />
 
       <div class="conversation-chat-content">
-        <DialogList class="conversation-chat-list" :height="list_height" />
+        <DialogList
+          class="conversation-chat-list"
+          :height="list_height"
+          :agent_id="agent_id"
+          :list="chat_log_list"
+          :suggestion="chat_suggestion"
+          :show_welcome="show_welcome"
+          @share="handleChatShare"
+          @submit="handleSuggestSubmit"
+        />
 
-        <DialogInput class="conversation-chat-input" :style="{ height: input_height + 'px' }" />
+        <DialogInput
+          class="conversation-chat-input"
+          :style="{ height: input_height + 'px' }"
+          :disabled="input_disabled"
+          @inputTextChange="handleTextChange"
+          @inputFileChange="handleFileChange"
+          @submit="handleInputSubmit"
+        />
       </div>
 
       <DialogFooter class="conversation-chat-footer" :style="{ height: footer_height + 'px' }" />
@@ -34,17 +50,27 @@ import DialogAsideRight from '../../modules/conversation-chat/DialogAsideRight.v
 
 import useEleSize from '../../utils/use-ele-size';
 
+import useWebSocketChat from '../../modules/conversation-chat/use-websocket-chat';
+import { jsonParse, jsonStringify } from '../../utils/utils';
+
 /**
  * 1. 历史会话进入此页面,展示历史会话内容
  * 2. 点击智能体,新建会话
  */
 
 const props = defineProps({
+  /**
+   * 智能体的ID
+   * 此参数必须传递
+   */
+  agent_id: String,
   /**
    * 会话ID
    * 如果有此参数,则意味着获取该对话的记录内容
    */
-  chat_id: String
+  session_id: String,
+
+  chat_type: String
 });
 
 const header_height = 30;
@@ -59,6 +85,172 @@ const { width: chat_width, height: chat_height } = useEleSize('chat_main');
 const list_height = computed(() => {
   return chat_height.value - header_height - footer_height - input_height;
 });
+
+// ------------------------------------------------------------------
+// ------------------------------------------------------------------
+// ------------------------------------------------------------------
+
+/**
+ * 对话数据列表
+ */
+const chat_log_list = ref([]);
+const chat_suggestion = ref({});
+
+/**
+ * 是否显示欢迎页
+ * 当用户进入对话页面,还未正式开始对话时
+ * 对话区域会显示欢迎内容
+ * 当用户开始对话时,欢迎内容消失,对话列表开始展示
+ */
+const show_welcome = ref(false);
+
+/**
+ * 分享
+ * @param chat_id
+ */
+const handleChatShare = (chat_id) => {};
+/**
+ * 在聊天界面点击推荐项
+ * 继续对话
+ */
+const handleSuggestSubmit = (suggest) => {};
+
+// ---------------------------------------------
+
+/**
+ * 输入区域数据控制
+ */
+const input_text = ref('');
+const input_files = ref([]);
+
+/**
+ * 是否禁用输入组件
+ */
+const input_disabled = ref(false);
+
+/**
+ * 输入文本发生了变化
+ * @param value
+ */
+const handleTextChange = (value) => {};
+/**
+ * 输入的文件发生了变化
+ * @param files
+ */
+const handleFileChange = (files) => {};
+/**
+ * 输入已经确认要提交了
+ * 1. 文件上传
+ * 2. 创建会话
+ *    get-chat-id
+ *    连接socket
+ * 3. socket 发送文本与文件数据内容
+ * 4. 接收 socket 返回的数据项
+ */
+const handleInputSubmit = () => {
+  /**
+   * 创建会话 get chat id
+   */
+
+  /**
+   * 上传文件,获取文件参数
+   */
+
+  /**
+   * 组装socket参数
+   * 发送websocket消息
+   */
+
+  const msg = jsonStringify({
+    message: '消息内容'
+  });
+
+  handleWSSend(msg);
+};
+
+/**
+ * ------------------------------------------------------------
+ * websocket定义以及初始化
+ */
+const wsTrigger = {
+  /**
+   * 当websocket消息回来后
+   * @param e
+   */
+  onMessage: handleWSMessage,
+  /**
+   * 当websocket消息出错时
+   * @param e
+   */
+  onError: handleWSError,
+  /**
+   * 当websocket消息关闭时
+   * @param e
+   */
+  onClose: handleWSClose
+};
+
+/**
+ * 初始化websocket相关的一些方法
+ */
+const { initWSConnection, sendWSMessage, getWSInstance, closeWSConnect } = useWebSocketChat(wsTrigger);
+
+/**
+ * 发送websocket消息
+ * @param chat_id - 对话记录的ID
+ * @param value
+ */
+function handleWSSend(chat_id, value) {
+  // 初始化websocket连接
+  initWSConnection(props.agent_id, chat_id, props.chat_type);
+  sendWSMessage(value);
+}
+
+/**
+ * 接收websocket消息
+ * @param e
+ */
+function handleWSMessage(e) {
+  const res_data = e.data;
+  const res_content = jsonParse(res_data);
+
+  /**
+   * 类别:
+   * close
+   * error
+   * message
+   * stream
+   */
+  const res_type = res_content.type;
+  const res_message = res_content.message; // 消息内容
+}
+
+/**
+ * 处理websocket错误
+ * @param e
+ */
+function handleWSError(e) {}
+
+/**
+ * 处理websocket关闭事件
+ * @param e
+ */
+function handleWSClose(e) {}
+
+onMounted(() => {
+  // 组件挂载
+  if (props.session_id) {
+    // 有历史记录:获取历史记录内容进行展示
+  } else {
+    // 无历史记录:等待创建会话内容,列表区域显示欢迎界面
+  }
+});
+
+// ---------------------------------------------------------
+onUnmounted(() => {
+  // 组件卸载,关闭组件内的websocket连接
+  closeWSConnect();
+});
 </script>
 
 <style lang="css" scoped>

+ 3 - 4
src/views/conversation/ConversationHistoryContainer.vue

@@ -6,7 +6,7 @@
       <HistoryName />
 
       <HistorySearchInput @search="handleHistorySearch">
-        <HistoryTypeDropdown @typeSelected="handleTypeSelected" :api_status="api_status" />
+        <HistoryTypeDropdown @typeSelected="handleTypeSelected" :list_status="api_status" />
       </HistorySearchInput>
 
       <HistoryList
@@ -82,7 +82,7 @@ const last_month_zero = yesterday_zero - ONE_MONTH; // 上个月(30天之前的)
 
 /**
  * 将数据进行分类
- * 依据时间戳,将列表数据分成三类型:今天、本月、更早
+ * 依据时间戳,将列表数据分成三类型:今天、本月、更早
  * 今天:从今天零点开始到现在
  * 本月:从今天零点之前,往前推30天
  * 更早:在30天之前的数据
@@ -194,8 +194,6 @@ const handleApiRequest = () => {
       }
     })
     .catch((err) => {
-      // console.log(err, '这是错误吗??');
-
       // api请求出错
       api_status.value = 5;
       api_message.value = err.message;
@@ -227,6 +225,7 @@ const handleListLoadMore = () => {
  */
 const handleListItemActive = (history_id) => {
   emit('historyActive', {
+    agent_id: agent_id.value, // agent_id值
     target_id: history_id
   });
 };