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

68
src/api/doc/space.ts

@ -1,6 +1,6 @@
import request from '@/utils/request';
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
});
}
/**
*
*/
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
});
}
/**
*
@ -75,4 +100,45 @@ export function doDelSpaceMatter(uid:string,data?: any){
},
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
path?:string;
size?:number;
agent?:boolean;
dir?:boolean;
deleted?: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 sharePermission from './sharePermission.vue';
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 {
Delete,
@ -15,28 +16,38 @@ import {
Download,
Plus,
Edit,
CircleCheckFilled,
Promotion,
Folder,
} from '@element-plus/icons-vue'
import {ElMessage,UploadFile,UploadFiles,ElPagination} from "element-plus";
import preview from './preview.vue';
import aiagent from './agent.vue';
import router from "@/router";
import { formToJSON } from "axios";
const matterList = ref<matterInfo[]>([])
const newdir=ref("") //
const currentHoverRow=ref("") //table
const breadcrumbList=ref<matterInfo[]>([{name:"根目录",uuid:"root", dir:true}]) //
const selectedValue = ref("sixhour") //
const tabSelected=ref<matterInfo[]>([]) //table
const currentNode=ref<matterInfo>({}) //
const dynamicVNode = ref<VNode | null>(null) //permission
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<{
uid:string, //uuid,base64
uid:string, //uuid,base64,Identifierbase64
spaceid:string,
spacename:string,
officeHost:string,
@ -188,7 +199,7 @@ function handleDoubleClick(row:matterInfo,ind?:number){
if(row.dir){
//table
if(typeof(ind)==="number"){
//
//,
if(breadcrumbList.value.length>1){
breadcrumbList.value=breadcrumbList.value.slice(0,ind+1)
currentNode.value=breadcrumbList.value[breadcrumbList.value.length-1]
@ -196,6 +207,11 @@ function handleDoubleClick(row:matterInfo,ind?:number){
}
}else{
//
//TODOagentiddir
if(row.agent){
currentAgent.value={name:row.name,model:false,uuid:row.uuid}
}
currentNode.value=row
breadcrumbList.value.push(row)
onLoadMatterList()
@ -208,6 +224,7 @@ function handleMouseEnter(row:any){
}
//
function handleSingleUpload(response:any){
handleAiUpload(response.data)
fileList.value=[]
onLoadMatterList()
}
@ -218,6 +235,75 @@ interface uploadError{
function handleSigLoadErr(error: Error, uploadFile: UploadFile, uploadFiles:UploadFiles){
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---------------------
//
function onPrivateView(row:matterInfo){
@ -259,6 +345,8 @@ async function onlyOfficeEdit(row:matterInfo){
//
onMounted(() => {
currentNode.value.uuid="root"
//AI
currentAgent.value={name:"通用AI",model:false,uuid:"5bd9b0e9-d3f4-4089-670a-880009e925a8"}
onLoadMatterList()
});
@ -286,20 +374,26 @@ function isOwner(){
<el-row :gutter="24">
<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"
:on-success="handleSingleUpload"
:on-error="handleSigLoadErr"
:show-file-list="false"
:action="apiURL+'/space/upload'" :limit="1">
:on-change="onCustomUpload"
:show-file-list="true"
multiple
:limit="2"
:action="apiURL+'/space/upload'">
<span>上传文件</span>
</el-upload>
<el-button @click="createDir">新建目录</el-button>
<el-button type="danger" plain @click="onDelMatter({uuid:currentNode.uuid,name:currentNode.name,dir:true,
puuid:currentNode.puuid,path:currentNode.path})">删除目录</el-button>
</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="onAiAgent">创建智能体</el-button>
<el-button :icon="Delete" @click="onDeleteSpace">删除</el-button>
</el-button-group>
</el-row>
@ -328,10 +422,10 @@ function isOwner(){
</div>
</template>
</el-table-column>
<el-table-column width="250" align="center">
<template #default="scope">
<div v-show="currentHoverRow === scope.row.name">
<el-button size="small" :icon="Promotion" circle ></el-button>
<el-table-column width="250">
<template #default="scope" >
<el-tag v-if="scope.row.agent" effect="dark" size="small" type="success" round >智能体</el-tag>
<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="View" circle @click="onPrivateView(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>
</div>
<aiagent :agent="currentAgent" :userid="uid" :uuid="currentAgent" :closefunc="()=>{currentAgent.model=false}"></aiagent>
<div v-if="dynamicVNode">
<component :is="dynamicVNode" />
</div>

Loading…
Cancel
Save