Ver Fonte

Merge branch 'master' of http://gitlab.smartai.com/zhupei/smartai2dian0

hanyang há 1 ano atrás
pai
commit
57b5478c0f
34 ficheiros alterados com 1551 adições e 318 exclusões
  1. 1 0
      src/base/storage.js
  2. 32 0
      src/base/store/use-all-agents.js
  3. 13 0
      src/modules/conversation-board/api-conversation-board.js
  4. 67 0
      src/modules/conversation-board/use-agent-list-4-board.js
  5. 89 0
      src/modules/conversation-board/use-agent-list-4-history.js
  6. 122 9
      src/modules/conversation-chat/DialogInput.vue
  7. 16 9
      src/modules/conversation-chat/DialogItem.vue
  8. 31 7
      src/modules/conversation-chat/DialogList.vue
  9. 20 0
      src/modules/conversation-chat/DialogSuggestion.vue
  10. 20 0
      src/modules/conversation-chat/DialogWelcome.vue
  11. 47 0
      src/modules/conversation-chat/dialog-item/DialogAnswer.vue
  12. 38 0
      src/modules/conversation-chat/dialog-item/DialogQuestion.vue
  13. 40 0
      src/modules/conversation-chat/dialog-item/dialog-item-common.css
  14. 178 0
      src/modules/conversation-chat/use-websocket-chat.js
  15. 113 0
      src/modules/conversation-dialog/api-dialog.js
  16. 4 5
      src/modules/conversation-history/BtnHistoryItemDelete.vue
  17. 0 1
      src/modules/conversation-history/HistoryItem.vue
  18. 8 74
      src/modules/conversation-history/HistoryTypeDropdown.vue
  19. 35 0
      src/modules/permission/Modal.vue
  20. 51 16
      src/modules/permission/TreeParts.vue
  21. 71 0
      src/modules/permission/account/AccountForm.vue
  22. 1 1
      src/modules/permission/account/DeptConfigModal.vue
  23. 0 89
      src/modules/permission/account/EditModal.vue
  24. 7 5
      src/modules/permission/account/index.vue
  25. 10 23
      src/modules/permission/org/OrgForm.vue
  26. 33 9
      src/modules/permission/org/index.vue
  27. 82 0
      src/modules/permission/resource/ResourceForm.vue
  28. 72 0
      src/modules/permission/resource/Upload.vue
  29. 2 3
      src/views/ConversationBoard.vue
  30. 3 2
      src/views/ConversationHistory.vue
  31. 136 1
      src/views/ConversationQA.vue
  32. 11 57
      src/views/conversation/ConversationBoardContainer.vue
  33. 195 3
      src/views/conversation/ConversationChatContainer.vue
  34. 3 4
      src/views/conversation/ConversationHistoryContainer.vue

+ 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>
 

+ 35 - 0
src/modules/permission/Modal.vue

@@ -0,0 +1,35 @@
+<template>
+  <a-modal v-model:visible="visible" :title="title" @ok="ok" @cancel="editHandleCancel" width="40%">
+    <slot></slot>
+  </a-modal>
+</template>
+
+<script setup>
+import { toRefs } from 'vue';
+
+// 定义组件的 props(根据实际需要进行修改)
+const props = defineProps({
+  visible: Boolean,
+  title: {
+    type: String,
+    required: true,
+    default: '标题'
+  }
+});
+
+// 事件发射器
+const emit = defineEmits(['ok', 'cancel']);
+
+const { visible, title } = toRefs(props);
+
+// 处理确认事件
+const ok = () => {
+  // 进行表单验证和提交逻辑
+  // 根据需要增加表单验证逻辑
+  emit('ok');
+}
+
+const editHandleCancel = () => {
+  emit('cancel');
+}
+</script>

+ 51 - 16
src/modules/permission/TreeParts.vue

@@ -3,21 +3,21 @@
     <template #title>
       <span>机构</span>
       <a-tooltip content="点击后可通过拖拽更改机构结构,再次点击来关闭" background-color="#165DFF">
-        <a-button type="outline" style="float: right;" @click="toggleDraggable" :style="{ color: buttonColor }"
+        <a-button :type="is_lock ? 'secondary' : 'outline'" style="float: right;" @click="toggleDraggable"
           v-has-permi="'/base/syorganization!update'">
-          <component :is="lock_icon" />
+          <component :is="is_lock ? 'IconUnlock' : 'IconLock'" />
         </a-button>
       </a-tooltip>
     </template>
-    <a-tree class="tree-demo" :draggable="draggable" blockNode :data="treeData" :show-line="true" :fieldNames="{
+    <a-tree :draggable="is_lock" blockNode :data="treeData" :show-line="true" :fieldNames="{
       key: 'deptId',
       title: 'deptName',
       children: 'children',
-    }" @drop="onDrop" @select="showDetail">
+    }" @drop="onDrop" @select="nodeClick">
       <template #extra="nodeData">
-        <IconPlus class="icon-plus" @click="() => onIconClick(nodeData)" />
+        <IconPlus class="icon-plus" @click="() => onAddBtnClick(nodeData)" />
         <a-popconfirm v-if="nodeData.deptName != 'root'" content="请确认是否删除?" type="success"
-          @ok="() => onIconClickDelete(nodeData)">
+          @ok="() => onDeleteBtnClickDelete(nodeData)">
           <IconDelete class="icon-delete" />
         </a-popconfirm>
       </template>
@@ -36,24 +36,59 @@ const props = defineProps({
 
 const { treeData } = toRefs(props);
 
+const emit = defineEmits(['nodeClick', 'onAddBtnClick', 'onDeleteBtnClickDelete']);
 
-let draggable = ref(false);
-let lock_icon = ref("IconLock");
+let is_lock = ref(false);
 
 const toggleDraggable = () => {
-  draggable.value = !draggable.value;
-  buttonColor.value = draggable.value ? '#575757' : '';
-  lock_icon.value = draggable.value ? 'IconUnlock' : 'IconLock';
+  is_lock.value = !is_lock.value;
 };
 
-const showDetail = (id) => {
-  OrganizationById(id).then((res) => {
-    deptform.value = { ...res.data };
-  });
+const nodeClick = (id) => {
+  emit('nodeClick', id);
+
+};
+const onAddBtnClick = (nodeData) => {
+  emit('addBtnClick', nodeData);
+}
+const onDeleteBtnClickDelete = (nodeData) => {
+  emit('deleteBtnClickDelete', nodeData);
 };
 
-const onIconClickDelete = (nodeData) => {
+const onDrop = ({ dragNode, dropNode, dropPosition }) => {
+  const data = treeData.value;
+  OrganizationUpdate({
+    orderNum: '0',
+    parentId: dropNode.deptId,
+    deptId: dragNode.deptId,
+  });
+  const loop = (data, key, callback) => {
+    data.some((item, index, arr) => {
+      if (item.deptId === key) {
+        callback(item, index, arr);
+        return true;
+      }
+      if (item.children) {
+        return loop(item.children, key, callback);
+      }
+      return false;
+    });
+  };
+
+  loop(data, dragNode.deptId, (_, index, arr) => {
+    arr.splice(index, 1);
+  });
 
+  if (dropPosition === 0) {
+    loop(data, dropNode.deptId, (item) => {
+      item.children = item.children || [];
+      item.children.push(dragNode);
+    });
+  } else {
+    loop(data, dropNode.deptId, (_, index, arr) => {
+      arr.splice(dropPosition < 0 ? index : index + 1, 0, dragNode);
+    });
+  }
 };
 </script>
 

+ 71 - 0
src/modules/permission/account/AccountForm.vue

@@ -0,0 +1,71 @@
+<template>
+  <a-form :model="edit_form" auto-label-width>
+    <a-row :gutter="24">
+      <a-col :span="12">
+        <a-form-item field="loginName" label="用户名" :rules="[
+          { required: true, message: '用户名必填' },
+          { maxLength: 50, message: '长度不超过50' },
+        ]">
+          <a-input v-model="edit_form.loginName" />
+        </a-form-item>
+      </a-col>
+      <a-col :span="12">
+        <a-form-item field="userName" label="姓名">
+          <a-input v-model="edit_form.userName" />
+        </a-form-item>
+      </a-col>
+    </a-row>
+    <a-row :gutter="24">
+      <a-col :span="12">
+        <a-form-item field="phoneNumber" label="手机号">
+          <a-input v-model="edit_form.phoneNumber" />
+        </a-form-item>
+      </a-col>
+      <a-col :span="12">
+        <a-form-item required field="email" label="邮箱" :rules="[
+          { required: true, message: '邮箱必填' },
+          { maxLength: 50, message: '长度不超过50' },
+        ]">
+          <a-input v-model="edit_form.email" />
+        </a-form-item>
+      </a-col>
+    </a-row>
+    <a-row :gutter="24">
+      <a-col :span="12">
+        <a-form-item field="password" label="密码">
+          <a-input v-model="edit_form.password" type="password" />
+        </a-form-item>
+      </a-col>
+      <a-col :span="12">
+        <a-form-item field="role" label="角色">
+          <a-select multiple v-model="edit_form.role" :options="roles" @change="roleChange">
+          </a-select>
+        </a-form-item>
+      </a-col>
+    </a-row>
+  </a-form>
+</template>
+
+<script setup>
+import { toRefs } from 'vue';
+
+const props = defineProps({
+  edit_form: Object,
+  roles: Array,
+});
+
+// 事件发射器
+const emit = defineEmits(['ok', 'cancel']);
+
+const { edit_form, roles } = toRefs(props);
+
+// 角色变化处理
+const roleChange = (selectedRoles) => {
+
+  edit_form.role = selectedRoles;
+}
+</script>
+
+<style lang="css" scoped>
+/* 这里可以添加自定义样式 */
+</style>

+ 1 - 1
src/modules/permission/account/DeptConfigModal.vue

@@ -40,7 +40,7 @@ const props = defineProps({
   },
   record: {
     type: Object,
-    default: false
+    default: () => { }
   }
 });
 

+ 0 - 89
src/modules/permission/account/EditModal.vue

@@ -1,89 +0,0 @@
-<template>
-  <a-modal v-model:visible="visible" :title="title" @ok="ok" @cancel="editHandleCancel" width="40%">
-    <a-form ref="formRef" :model="edit_form" auto-label-width>
-      <a-row :gutter="24">
-        <a-col :span="12">
-          <a-form-item field="loginName" label="用户名" :rules="[
-            { required: true, message: '用户名必填' },
-            { maxLength: 50, message: '长度不超过50' },
-          ]">
-            <a-input v-model="edit_form.loginName" />
-          </a-form-item>
-        </a-col>
-        <a-col :span="12">
-          <a-form-item field="userName" label="姓名">
-            <a-input v-model="edit_form.userName" />
-          </a-form-item>
-        </a-col>
-      </a-row>
-      <a-row :gutter="24">
-        <a-col :span="12">
-          <a-form-item field="phoneNumber" label="手机号">
-            <a-input v-model="edit_form.phoneNumber" />
-          </a-form-item>
-        </a-col>
-        <a-col :span="12">
-          <a-form-item required field="email" label="邮箱" :rules="[
-            { required: true, message: '邮箱必填' },
-            { maxLength: 50, message: '长度不超过50' },
-          ]">
-            <a-input v-model="edit_form.email" />
-          </a-form-item>
-        </a-col>
-      </a-row>
-      <a-row :gutter="24">
-        <a-col :span="12">
-          <a-form-item field="password" label="密码">
-            <a-input v-model="edit_form.password" type="password" />
-          </a-form-item>
-        </a-col>
-        <a-col :span="12">
-          <a-form-item field="role" label="角色">
-            <a-select multiple v-model="edit_form.role" :options="roles" :field-names="field_names"
-              @change="roleChange">
-            </a-select>
-          </a-form-item>
-        </a-col>
-      </a-row>
-    </a-form>
-  </a-modal>
-</template>
-
-<script setup>
-import { toRefs } from 'vue';
-
-// 定义组件的 props(根据实际需要进行修改)
-const props = defineProps({
-  edit_form: Object,
-  visible: Boolean,
-  title: String,
-  roles: Array,
-  field_names: Object
-});
-
-// 事件发射器
-const emit = defineEmits(['ok', 'cancel']);
-
-const { edit_form, visible, title, roles, field_names } = toRefs(props);
-
-// 处理确认事件
-const ok = () => {
-  // 进行表单验证和提交逻辑
-  // 根据需要增加表单验证逻辑
-  emit('ok', edit_form);
-}
-
-const editHandleCancel = () => {
-  emit('cancel');
-  visible.value = false;
-}
-
-// 角色变化处理
-const roleChange = (selectedRoles) => {
-  edit_form.role = selectedRoles;
-}
-</script>
-
-<style lang="css" scoped>
-/* 这里可以添加自定义样式 */
-</style>

+ 7 - 5
src/modules/permission/account/index.vue

@@ -21,8 +21,9 @@
     </DataTable>
   </a-card>
 
-  <EditModal :visible.sync="edit_visible" :edit_form="edit_form" @ok="editModalSave" @cancel="handleEdit(null, false)"
-    :title="'编辑'" />
+  <Modal :visible.sync="edit_visible" @ok="editModalSave" @cancel="handleEdit(null, false)" :title="'编辑'">
+    <AccountForm :edit_form="edit_form" />
+  </Modal>
 
   <ViewPermission :visible.sync="view_permission_visible" @cancel="handleView(null, false)"
     @close="handleView(null, false)" :view_form="view_form" :menu_permissions_list="menu_permissions_list"
@@ -36,10 +37,11 @@
 
 <script setup>
 import { ref, reactive } from 'vue';
-import { Modal } from '@arco-design/web-vue';
+import { Modal as ArcoModal } from '@arco-design/web-vue';
 import DataTable from '../../../views/permission/DataTable.vue';
 import Action from '../../../views/permission/Action.vue';
-import EditModal from './EditModal.vue';
+import Modal from '../Modal.vue';
+import AccountForm from './AccountForm.vue';
 import ViewPermission from './ViewPermission.vue';
 import DeptConfigModal from './DeptConfigModal.vue';
 
@@ -243,7 +245,7 @@ const refreshClick = () => {
 }
 
 const handleResetPassword = (record) => {
-  Modal.success({
+  ArcoModal.success({
     title: '重置密码',
     content: '该用户密码重置为000000',
   });

+ 10 - 23
src/modules/permission/org/OrgForm.vue

@@ -1,30 +1,29 @@
 <template>
-  <a-form :model="dept_form" layout="horizontal" ref="formRef">
+  <a-form :model="org_form" layout="horizontal" ref="formRef">
     <a-form-item field="parentName" label="上级机构" disabled>
-      <a-input v-model="dept_form.parentName" />
+      <a-input v-model="org_form.parentName" />
     </a-form-item>
     <a-form-item field="status" label="机构状态">
-      <a-switch checked-value="0" unchecked-value="1" v-model="dept_form.status"></a-switch>
+      <a-switch checked-value="0" unchecked-value="1" v-model="org_form.status"></a-switch>
     </a-form-item>
     <a-form-item field="deptName" label="机构名称"
       :rules="[{ required: true, message: '机构名称必填' }, { maxLength: 50, message: '长度不超过50' }]">
-      <a-input v-model="dept_form.deptName" />
+      <a-input v-model="org_form.deptName" />
     </a-form-item>
     <a-form-item field="leader" label="联系人"
       :rules="[{ required: true, message: '联系人必填' }, { maxLength: 50, message: '长度不超过50' }]">
-      <a-input v-model="dept_form.leader" />
+      <a-input v-model="org_form.leader" />
     </a-form-item>
     <a-form-item field="phone" label="联系电话"
       :rules="[{ required: true, message: '联系电话必填' }, { maxLength: 50, message: '长度不超过50' }]">
-      <a-input v-model="dept_form.phone" />
+      <a-input v-model="org_form.phone" />
     </a-form-item>
     <a-form-item field="address" label="机构地址">
-      <a-input v-model="dept_form.address" />
+      <a-input v-model="org_form.address" />
     </a-form-item>
     <a-form-item>
       <a-space>
-        <a-button @click="handleSubmit">保存</a-button>
-        <a-button @click="handleReset">重置</a-button>
+        <slot name="actions"></slot>
       </a-space>
     </a-form-item>
   </a-form>
@@ -35,7 +34,7 @@ import { toRefs } from 'vue';
 
 // 接收父组件传入的参数
 const props = defineProps({
-  dept_form: {
+  org_form: {
     type: Object,
     required: true,
     default: () => ({
@@ -50,20 +49,8 @@ const props = defineProps({
 });
 
 // 将 props 解构
-const { dept_form } = toRefs(props);
+const { org_form } = toRefs(props);
 
-// 定义要抛出的事件
-const emit = defineEmits(['save', 'reset']);
-
-// 处理保存逻辑
-const handleSubmit = () => {
-  emit('save', dept_form.value);
-};
-
-// 处理重置逻辑
-const handleReset = () => {
-  emit('reset');
-};
 </script>
 
 <style lang="css" scoped></style>

+ 33 - 9
src/modules/permission/org/index.vue

@@ -1,13 +1,23 @@
 <template>
   <div class="organization-container">
     <div class="left">
-      <TreeParts :treeData="org_tree_data" />
+      <TreeParts :treeData="org_tree_data" @nodeClick="onNodeClick" @addBtnClick="onAddBtnClick"
+        @deleteBtnClick="onDeleteBtnClick" />
     </div>
     <div class="right">
       <a-card title="详情" style="height: 100%;">
-        <OrgForm :dept_form="dept_form" @save="orgFromSave" @reset="orgFromReset"></OrgForm>
+        <OrgForm :org_form="org_form" @save="orgFromSave" @reset="orgFromReset">
+          <template #actions>
+            <a-button @click="orgFromSave">保存</a-button>
+            <a-button @click="orgFromReset">重置</a-button>
+          </template>
+        </OrgForm>
       </a-card>
     </div>
+
+    <Modal :visible.sync="org_visible" @ok="orgFromSave" @cancel="org_visible = false" :title="'新增'">
+      <OrgForm></OrgForm>
+    </Modal>
   </div>
 </template>
 
@@ -15,6 +25,9 @@
 import { ref, reactive, toRefs } from 'vue';
 import TreeParts from '../TreeParts.vue';
 import OrgForm from './OrgForm.vue';
+import Modal from '../Modal.vue';
+
+const org_visible = ref(false);
 
 const org_tree_data = ref([
   {
@@ -142,7 +155,7 @@ const org_tree_data = ref([
   }
 ]);
 
-const dept_form = reactive({
+const org_form = reactive({
   parentName: '',
   status: 0,
   deptName: '',
@@ -150,18 +163,29 @@ const dept_form = reactive({
   phone: '',
   address: ''
 });
+// 树节点操作相关
+const onNodeClick = (id) => {
+  console.log('id', id)
+}
+const onAddBtnClick = (nodeData) => {
+  console.log('nodeData', nodeData)
+  org_visible.value = true;
+}
+const onDeleteBtnClick = (nodeData) => {
+  console.log('nodeData', nodeData)
+}
 
 const orgFromSave = (data) => {
   console.log(data);
 }
 
 const orgFromReset = () => {
-  dept_form.parentName = '';
-  dept_form.status = 0;
-  dept_form.deptName = '';
-  dept_form.leader = '';
-  dept_form.phone = '';
-  dept_form.address = '';
+  org_form.parentName = '';
+  org_form.status = 0;
+  org_form.deptName = '';
+  org_form.leader = '';
+  org_form.phone = '';
+  org_form.address = '';
 }
 </script>
 

+ 82 - 0
src/modules/permission/resource/ResourceForm.vue

@@ -0,0 +1,82 @@
+<template>
+  <a-form :model="resource_form" layout="horizontal">
+    <a-form-item field="parentName" label="上级资源" disabled>
+      <a-input v-model="resource_form.parentName" />
+    </a-form-item>
+    <a-form-item field="status" label="资源状态">
+      <a-switch checked-value="0" unchecked-value="1" v-model="resource_form.status"></a-switch>
+    </a-form-item>
+    <a-form-item field="menuName" label="资源名称"
+      :rules="[{ required: true, message: '资源名称必填' }, { maxLength: 50, message: '长度不超过50' }]">
+      <a-input v-model="resource_form.menuName" />
+    </a-form-item>
+    <a-form-item field="menuName" label="资源图标">
+      <Upload :action="uploadAction" :limit="1" :url="resource_form.icon" @update:fileList="updateFileList"
+        @success="handleSuccess"></Upload>
+    </a-form-item>
+    <a-form-item field="menuType" label="资源类型" :rules="[{ required: true, message: '资源类型必填' }]">
+      <a-select v-model="resource_form.menuType" :options="options" :field-names="fieldNames"
+        :style="{ width: '320px' }" placeholder="请选择" />
+    </a-form-item>
+    <a-form-item field="description" label="提示词">
+      <a-input v-model="resource_form.description" placeholder="请输入提示词" />
+    </a-form-item>
+    <a-form-item field="perms" label="资源控制权限字符"
+      :rules="[{ required: true, message: '资源控制权限字符必填' }, { maxLength: 50, message: '长度不超过50' }]">
+      <a-input v-model="resource_form.perms" />
+    </a-form-item>
+    <a-form-item field="component" label="资源地址" style="align: start"
+      :rules="[{ required: true, message: '资源地址必填' }, { maxLength: 50, message: '长度不超过50' }]">
+      <a-input v-model="resource_form.component" />
+    </a-form-item>
+    <a-form-item>
+      <a-space>
+        <slot name="actions"></slot>
+      </a-space>
+    </a-form-item>
+  </a-form>
+</template>
+
+<script setup>
+import { toRefs } from 'vue';
+import Upload from './Upload.vue';
+
+const uploadAction = '/api/v1/llm/upload'; // 替换为你的上传API
+const fileList = ref([]);
+const imageUrls = ref([]);
+
+// 接收父组件传入的参数
+const props = defineProps({
+  resource_form: {
+    type: Object,
+    required: true,
+    default: () => ({
+      parentName: '',
+      status: 0,
+      deptName: '',
+      leader: '',
+      phone: '',
+      address: ''
+    })
+  }
+});
+
+// 将 props 解构
+const { resource_form } = toRefs(props);
+
+const updateFileList = (newFileList) => {
+  fileList.value = newFileList;
+};
+
+const handleSuccess = (urls) => {
+  uploadUrl.value = urls;
+
+  const urlsArr = urls.map((url) => {
+    return httpUrl + url;
+  });
+  imageUrls.value = urlsArr; // 拿到上传的图片地址
+};
+
+</script>
+
+<style lang="css" scoped></style>

+ 72 - 0
src/modules/permission/resource/Upload.vue

@@ -0,0 +1,72 @@
+<template>
+  <a-upload v-model:fileList="fileList" list-type="picture-card" :limit="limit" :action="action" @change="handleChange"
+    @before-remove="beforeRemove" image-preview />
+</template>
+
+<script setup>
+import { computed, ref, onMounted, watch, watchEffect } from 'vue';
+
+const props = defineProps({
+  limit: {
+    type: Number,
+    default: 1,
+  },
+  action: String, // 上传的服务器地址
+  url: String, //回显的文件地址
+});
+
+const emit = defineEmits(['fileListChange', 'fileListRemove', 'success']);
+const urls = computed(() => props.url);
+const fileList = ref([]);
+
+watch(
+  () => props.url,
+  (newVal) => {
+    if (newVal) {
+      fileList.value = newVal.split(',').map((item) => ({
+        uid: item,
+        name: item,
+        status: 'done',
+        url: item,
+      }));
+    }
+  },
+  {
+    deep: true, // 开启深度监听
+  }
+);
+
+onMounted(() => {
+  if (urls.value) {
+    fileList.value = urls.value.split(',').map((item) => ({
+      uid: item,
+      name: item,
+      status: 'done',
+      url: item,
+    }));
+  }
+
+  // console.log(
+  //   window.location.origin,
+  //   import.meta.env.VITE_API_BASE_URL,
+  //   8988
+  // );
+});
+
+// console.log(urls.value, 8988);
+const beforeRemove = (file) => {
+  emit('fileListRemove');
+  fileList.value = [];
+};
+
+const handleChange = (fileList) => {
+  emit('fileListChange', fileList);
+  const successFiles = fileList.filter((item) => item.status === 'done');
+  if (successFiles.length > 0) {
+    emit(
+      'success',
+      successFiles.map((item) => item.response.data)
+    );
+  }
+};
+</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
   });
 };