Browse Source

集成AI智能体

pull/1/head
han2015 4 months ago
parent
commit
831813abe5
  1. 3
      package.json
  2. 68
      src/api/doc/space.ts
  3. 1
      src/api/doc/type.ts
  4. 214
      src/views/doc/agent.vue
  5. 126
      src/views/doc/space.vue

3
package.json

@ -40,6 +40,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@crazydos/vue-markdown": "^1.1.4",
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.1.0",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@onlyoffice/document-editor-vue": "^1.5.0", "@onlyoffice/document-editor-vue": "^1.5.0",
@ -72,6 +73,8 @@
"quill-image-resize-custom-module": "^4.1.7", "quill-image-resize-custom-module": "^4.1.7",
"quill-image-uploader": "^1.3.0", "quill-image-uploader": "^1.3.0",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"screenfull": "^6.0.0", "screenfull": "^6.0.0",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",

68
src/api/doc/space.ts

@ -1,6 +1,6 @@
import request from '@/utils/request'; import request from '@/utils/request';
import { AxiosPromise } from 'axios'; import { AxiosPromise } from 'axios';
import { matterPage,createDir,matterTreeList} from './type'; import { matterPage,createDir,matterTreeList,matterInfo} from './type';
/** /**
* *
@ -31,6 +31,31 @@ export function doCreateSpaceDir(uid:string,data?: createDir){
data: data data: data
}); });
} }
/**
*
*/
export function doFileUpload(params:FormData,_url:string): AxiosPromise<matterInfo> {
return request({
url: _url,
method: 'post',
data: params,
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
export function doCreateAiagent(uid:string,data?: {space:string,matter:string}){
return request({
url: '/hxpan/api/space/aiagent',
method: 'post',
headers: {
'Identifier':uid,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data
});
}
/** /**
* *
@ -76,3 +101,44 @@ export function doDelSpaceMatter(uid:string,data?: any){
data: data data: data
}); });
} }
//-------------------AI backend APIs---------------
/**
*
*/
export function doAiTraining(_url:string,data?: any){
return request({
url: '/aibot'+_url,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data
});
}
export interface aiChatData{
inputs:object;
query:string;
response_mode:string;
conversation_id?:string;
user:string,
file?:[]
}
/**
* api
*/
export function doAiChat(_url:string,data: aiChatData,sig?:AbortSignal){
return fetch(
_url,{
method: 'post',
headers: {
'Content-Type': 'application/json'
},
signal:sig,
body: JSON.stringify(data)
}
)
}

1
src/api/doc/type.ts

@ -25,6 +25,7 @@ export interface matterInfo{
puuid?:string; // parent dir uuid puuid?:string; // parent dir uuid
path?:string; path?:string;
size?:number; size?:number;
agent?:boolean;
dir?:boolean; dir?:boolean;
deleted?:boolean; deleted?:boolean;
expireInfinity?:boolean; expireInfinity?:boolean;

214
src/views/doc/agent.vue

@ -0,0 +1,214 @@
<!--
@ 作者: han2015
@ 时间: 2025-05-12 15:39:13
@ 备注: aibot组件
-->
<script lang="ts" setup>
import {
Promotion,
Remove
} from '@element-plus/icons-vue'
import { doAiTraining,doAiChat,aiChatData} from "@/api/doc/space"
import {ElText,ElInput, ButtonInstance} from "element-plus";
import { VueMarkdown } from '@crazydos/vue-markdown'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import { display } from 'html2canvas/dist/types/css/property-descriptors/display';
//
const checkedModel = ref([])
//
const aimodels = [{name:'新对话',key:"context"}, {name:'联网检索',key:"onlineSearch"}, {name:'公司知识库',key:"useDataset"}]
const baseURL=import.meta.env.VITE_APP_BASE_API
const conversation=ref("")
const myquestion=ref('')
const controller = ref<AbortController | null>(null)
const inputState=ref(true)
const interact_msg=ref<{ask:boolean,think:string,content:string}[]>([])
const props = withDefaults(defineProps<{
userid:string,
closefunc:()=>void,
agent:{model:boolean,name:string,uuid:string}
}>(),{})
const respMsg=ref("")
/* 中断函数,供按钮调用 */
function abortFetch() {
if (controller.value) {
controller.value.abort()
controller.value = null
}
}
async function onSendTextToAI(){
if(myquestion.value==="")return;
inputState.value=false
interact_msg.value.push({ask:true,think:"", content:myquestion.value})
const params={
"onlineSearch":"否",
"useDataset":"否"
}
for (let item of checkedModel.value){
if(item==="context") conversation.value=""
if(item==="onlineSearch") params.onlineSearch="是"
if(item==="useDataset") params.useDataset="是"
}
controller.value = new AbortController();
try{
const res= await doAiChat(`${baseURL}/aibot/agents/${props.agent.uuid}/chat`,{
inputs: params,
query:myquestion.value,
response_mode:"streaming",
conversation_id:conversation.value,
user:atob(props.userid),
},controller.value.signal
)
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
myquestion.value=""
const reader = res.body!.getReader();
const decoder = new TextDecoder('utf-8');
let chunk = ''; //
while (true) {
const {done, value} = await reader.read()
if (done) break;
//
chunk += decoder.decode(value, {stream: true});
const lines = chunk.split(/\n/);
chunk = lines.pop() || ''
for (const line of lines) {
if (!line.trim()) continue; //
if (line.startsWith('data: ')) {
const data = line.slice(6);
const json = JSON.parse(data);
if(json.event==="message"){
conversation.value=json.conversation_id
respMsg.value+=json.answer
}
}
}
}
}catch (e: any) {
if (e.name === 'AbortError') {
console.log('用户手动中断')
}
inputState.value=true
}
const arr=respMsg.value.split("</think>")
if(arr.length===1){
interact_msg.value.push({ask:false,think:"",content:arr[0]})
}else{
//
interact_msg.value.push({ask:false,think:arr[0],content:arr[1]})
}
respMsg.value=""
}
//
onMounted(() => {
//loadKnownLibList()
});
</script>
<template>
<el-drawer
:model-value="agent.model"
:title="agent.name+' : 知识库'"
direction="rtl"
size="60%"
@close="closefunc"
:style="{padding:'17px'}">
<el-row :gutter="24" style="height: 99%;">
<el-col style="position: relative;background: white;">
<div class="reply_area" >
<template v-for="msg of interact_msg">
<el-text v-if="msg.ask" class="t_ask" >{{ msg.content }}</el-text>
<div v-else class="t_resp">
<el-text style="white-space: pre-line" v-html="msg.think"></el-text>
<VueMarkdown :markdown="msg.content" :rehype-plugins="[rehypeRaw]" :remark-plugins="[remarkGfm]" ></VueMarkdown>
</div>
</template>
<VueMarkdown :markdown="respMsg" :rehype-plugins="[rehypeRaw]" class="t_resp"></VueMarkdown>
</div>
<div class="question_com">
<el-checkbox-group v-model="checkedModel" style="display:flex; margin-bottom: 10px;">
<el-checkbox-button v-for="mod in aimodels" :value="mod.key">
{{ mod.name }}
</el-checkbox-button>
</el-checkbox-group>
<el-input placeholder="问灵犀..." v-model="myquestion" input-style="border-radius: 20px;"
resize="none" :autosize="{minRows: 4}" type="textarea" />
<el-button :style="{display :inputState ? '':'none'}" type="primary" :icon="Promotion" circle @click="onSendTextToAI"/>
<el-button :style="{display :inputState ? 'none':''}" type="primary" :icon="Remove" circle @click="abortFetch"/>
<span>内容由 AI 生成请仔细甄别</span>
</div>
</el-col>
</el-row>
</el-drawer>
</template>
<style lang="scss" scoped>
.app_container {
padding: 10px 30px 0px 30px;
height: 100%;
width: 100%;
}
.question_com{
position: fixed;
bottom: 25px;
width: 52%;
margin: 0 50px;
text-align: center;
display: block;
button{
position: absolute;
bottom: 25px;
right: 5px;
}
}
.reply_area{
display: flex;
flex-direction: column;
margin: 15px 15px 110px 0px;
}
.t_ask{
align-self: end;
line-height: 34px;
background-color: rgb(188 211 241);
padding: 0 30px;
border-radius:10px;
margin: 15px;
}
.t_resp{
align-self: start;
line-height: 30px;
margin: 20px 33px;
font-size: 18px;
color: black;
}
.dynamic-width-message-box-byme .el-message-box__message{
width: 100%;
}
</style>
<style>
think {
color: #939393;
margin-bottom: 8px;
display: block;
}
</style>

126
src/views/doc/space.vue

@ -7,7 +7,8 @@
import { getFileIcon, readableSize,fileType} from "./tools" import { getFileIcon, readableSize,fileType} from "./tools"
import sharePermission from './sharePermission.vue'; import sharePermission from './sharePermission.vue';
import { matterPage,matterInfo} from "@/api/doc/type" import { matterPage,matterInfo} from "@/api/doc/type"
import { doAccessManage,getSpaceMatterList,doCreateSpaceDir,doDelSpaceMatter,doDelSpace} from "@/api/doc/space" import { doAccessManage,getSpaceMatterList,doCreateSpaceDir,doDelSpaceMatter,doDelSpace,
doAiTraining ,doCreateAiagent,doFileUpload} from "@/api/doc/space"
import { h, proxyRefs } from 'vue' import { h, proxyRefs } from 'vue'
import { import {
Delete, Delete,
@ -15,28 +16,38 @@ import {
Download, Download,
Plus, Plus,
Edit, Edit,
CircleCheckFilled,
Promotion, Promotion,
Folder, Folder,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import {ElMessage,UploadFile,UploadFiles,ElPagination} from "element-plus"; import {ElMessage,UploadFile,UploadFiles,ElPagination} from "element-plus";
import preview from './preview.vue'; import preview from './preview.vue';
import aiagent from './agent.vue';
import router from "@/router"; import router from "@/router";
import { formToJSON } from "axios";
const matterList = ref<matterInfo[]>([]) const matterList = ref<matterInfo[]>([])
const newdir=ref("") // const newdir=ref("") //
const currentHoverRow=ref("") //table const currentHoverRow=ref("") //table
const breadcrumbList=ref<matterInfo[]>([{name:"根目录",uuid:"root", dir:true}]) // const breadcrumbList=ref<matterInfo[]>([{name:"根目录",uuid:"root", dir:true}]) //
const selectedValue = ref("sixhour") //
const tabSelected=ref<matterInfo[]>([]) //table const tabSelected=ref<matterInfo[]>([]) //table
const currentNode=ref<matterInfo>({}) // const currentNode=ref<matterInfo>({}) //
const dynamicVNode = ref<VNode | null>(null) //permission const dynamicVNode = ref<VNode | null>(null) //permission
const paginInfo = ref({ page: 0, total: 0 }) const paginInfo = ref({ page: 0, total: 0 })
const filesUpload=ref()
//-----------AI---------------------
const aiAgentVNode = ref<VNode | null>(null) //ai chat
//const agent=ref<{model:boolean,name:string}>({})
const currentAgent=ref<{model:boolean,name:string,uuid:string}>({})
//---------------------------------
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
uid:string, //uuid,base64 uid:string, //uuid,base64,Identifierbase64
spaceid:string, spaceid:string,
spacename:string, spacename:string,
officeHost:string, officeHost:string,
@ -188,7 +199,7 @@ function handleDoubleClick(row:matterInfo,ind?:number){
if(row.dir){ if(row.dir){
//table //table
if(typeof(ind)==="number"){ if(typeof(ind)==="number"){
// //,
if(breadcrumbList.value.length>1){ if(breadcrumbList.value.length>1){
breadcrumbList.value=breadcrumbList.value.slice(0,ind+1) breadcrumbList.value=breadcrumbList.value.slice(0,ind+1)
currentNode.value=breadcrumbList.value[breadcrumbList.value.length-1] currentNode.value=breadcrumbList.value[breadcrumbList.value.length-1]
@ -196,6 +207,11 @@ function handleDoubleClick(row:matterInfo,ind?:number){
} }
}else{ }else{
// //
//TODOagentiddir
if(row.agent){
currentAgent.value={name:row.name,model:false,uuid:row.uuid}
}
currentNode.value=row currentNode.value=row
breadcrumbList.value.push(row) breadcrumbList.value.push(row)
onLoadMatterList() onLoadMatterList()
@ -208,6 +224,7 @@ function handleMouseEnter(row:any){
} }
// //
function handleSingleUpload(response:any){ function handleSingleUpload(response:any){
handleAiUpload(response.data)
fileList.value=[] fileList.value=[]
onLoadMatterList() onLoadMatterList()
} }
@ -218,6 +235,75 @@ interface uploadError{
function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){ function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){
ElMessage.error(JSON.parse(error.message).msg) ElMessage.error(JSON.parse(error.message).msg)
} }
function onCustomUpload(files:any){
console.log(files)
console.log(fileList.value,"<<<<<<<<<<<<<")
if (fileList.value.length === 0) {
alert('请先选择文件')
}
console.log("sdfsdfsdf")
return
for (const file of fileList.value) {
const formData = new FormData()
formData.append('file', file)
const fields=new FormData()
fields.append("space",uploadFormData.value.space)
fields.append("puuid",uploadFormData.value.puuid)
fields.append("file","uploadFormData.value.space")
try {
doFileUpload(fields,'/hxpan/api/space/upload')
} catch (err) {
console.error('上传失败:', err)
}
}
//
fileList.value = []
filesUpload.value.clearFiles()
}
//-------------------------AI section--------------
//
function onAiAgent(){
if(currentNode.value.uuid=="root"){
alert("根目录不支持创建智能体")
return
}
if(currentNode.value.agent){
alert("当前目录已经是智能体目录")
return
}
ElMessageBox.confirm(`确认创建智能体( ${currentNode.value.name}) ?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(()=>{
doCreateAiagent(props.uid,{
space:props.spaceid,
matter:currentNode.value.uuid
}).then(()=>{
router.replace({ query: { t: Date.now() } })
})
})
}
function handleAiUpload(info:matterInfo){
if (currentNode.value.puuid){
doAiTraining(`/agents/${currentAgent.value.uuid}/updates`,{"matter":info.uuid}).then(resp=>{
console.log(resp)
})
}
}
//-------------------------------------------------
//-------------------edit & preive file for space--------------------- //-------------------edit & preive file for space---------------------
// //
function onPrivateView(row:matterInfo){ function onPrivateView(row:matterInfo){
@ -259,6 +345,8 @@ async function onlyOfficeEdit(row:matterInfo){
// //
onMounted(() => { onMounted(() => {
currentNode.value.uuid="root" currentNode.value.uuid="root"
//AI
currentAgent.value={name:"通用AI",model:false,uuid:"5bd9b0e9-d3f4-4089-670a-880009e925a8"}
onLoadMatterList() onLoadMatterList()
}); });
@ -286,20 +374,26 @@ function isOwner(){
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="14"> <el-col :span="14">
<el-upload class="el-button el-button--default" :file-list="fileList" <el-upload class="el-button el-button--default"
ref="filesUpload"
:file-list="fileList"
:auto-upload="false"
:data="uploadFormData" :data="uploadFormData"
:on-success="handleSingleUpload" :on-change="onCustomUpload"
:on-error="handleSigLoadErr" :show-file-list="true"
:show-file-list="false" multiple
:action="apiURL+'/space/upload'" :limit="1"> :limit="2"
:action="apiURL+'/space/upload'">
<span>上传文件</span> <span>上传文件</span>
</el-upload> </el-upload>
<el-button @click="createDir">新建目录</el-button> <el-button @click="createDir">新建目录</el-button>
<el-button type="danger" plain @click="onDelMatter({uuid:currentNode.uuid,name:currentNode.name,dir:true, <el-button type="danger" plain @click="onDelMatter({uuid:currentNode.uuid,name:currentNode.name,dir:true,
puuid:currentNode.puuid,path:currentNode.path})">删除目录</el-button> puuid:currentNode.puuid,path:currentNode.path})">删除目录</el-button>
</el-col> </el-col>
<el-button-group class="control" v-if="isOwner()" style="margin-right: 10px;margin-left: auto;"> <el-button style="margin-left: auto;" @click="()=>currentAgent.model=true">AI助手</el-button>
<el-button-group v-if="isOwner()" class="control" style="margin: 0 10px;">
<el-button :icon="Plus" @click="onAccessManage">成员</el-button> <el-button :icon="Plus" @click="onAccessManage">成员</el-button>
<el-button :icon="Plus" @click="onAiAgent">创建智能体</el-button>
<el-button :icon="Delete" @click="onDeleteSpace">删除</el-button> <el-button :icon="Delete" @click="onDeleteSpace">删除</el-button>
</el-button-group> </el-button-group>
</el-row> </el-row>
@ -328,10 +422,10 @@ function isOwner(){
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column width="250" align="center"> <el-table-column width="250">
<template #default="scope"> <template #default="scope" >
<div v-show="currentHoverRow === scope.row.name"> <el-tag v-if="scope.row.agent" effect="dark" size="small" type="success" round >智能体</el-tag>
<el-button size="small" :icon="Promotion" circle ></el-button> <div v-show="currentHoverRow === scope.row.name" style="display:inline; margin-left:15px">
<el-button size="small" :icon="Edit" circle @click="onlyOfficeEdit(scope.row)"></el-button> <el-button size="small" :icon="Edit" circle @click="onlyOfficeEdit(scope.row)"></el-button>
<el-button size="small" :icon="View" circle @click="onPrivateView(scope.row)"></el-button> <el-button size="small" :icon="View" circle @click="onPrivateView(scope.row)"></el-button>
<el-button size="small" :icon="Download" circle @click="onDownload(scope.row)"></el-button> <el-button size="small" :icon="Download" circle @click="onDownload(scope.row)"></el-button>
@ -354,6 +448,8 @@ function isOwner(){
</el-row> </el-row>
</div> </div>
<aiagent :agent="currentAgent" :userid="uid" :uuid="currentAgent" :closefunc="()=>{currentAgent.model=false}"></aiagent>
<div v-if="dynamicVNode"> <div v-if="dynamicVNode">
<component :is="dynamicVNode" /> <component :is="dynamicVNode" />
</div> </div>

Loading…
Cancel
Save