可信代码库-AI大模型批量对话接口对话及对话功能开发

This commit is contained in:
付民康
2025-05-13 17:59:24 +08:00
parent 4b7583b049
commit a08cb3f613
9 changed files with 533 additions and 47 deletions

View File

@@ -36,3 +36,6 @@ VITE_USER_CENTER_HOST = 'https://test.gitcode.net/child/user-center/'
VITE_COPILOT_HOST = 'https://test.gitcode.net/child/aichat-app/' VITE_COPILOT_HOST = 'https://test.gitcode.net/child/aichat-app/'
VITE_BASE_URL=/ VITE_BASE_URL=/
# AI大模型接口地址
VITE_AI_API_HOST = ''http://222.20.126.208:8089'

View File

@@ -38,3 +38,6 @@ VITE_USER_CENTER_HOST = 'https://local.gitcode.net:8002'
VITE_COPILOT_HOST = 'https://local.gitcode.net:8003/child/aichat-app' VITE_COPILOT_HOST = 'https://local.gitcode.net:8003/child/aichat-app'
VITE_BASE_URL=/openRepoPortal/ VITE_BASE_URL=/openRepoPortal/
# AI大模型接口地址
VITE_AI_API_HOST = ''http://222.20.126.208:8089'

View File

@@ -38,3 +38,6 @@ VITE_USER_CENTER_HOST = 'https://pre-usercenter-app.gitcode.com'
VITE_COPILOT_HOST = 'https://pre-copilot-app.gitcode.com' VITE_COPILOT_HOST = 'https://pre-copilot-app.gitcode.com'
VITE_BASE_URL=/openRepoPortal/ VITE_BASE_URL=/openRepoPortal/
# AI大模型接口地址
VITE_AI_API_HOST = ''http://222.20.126.208:8089'

View File

@@ -38,3 +38,6 @@ VITE_USER_CENTER_HOST = 'https://usercenter-app.gitcode.com'
VITE_COPILOT_HOST = 'https://copilot-app.gitcode.com' VITE_COPILOT_HOST = 'https://copilot-app.gitcode.com'
VITE_BASE_URL=/openRepoPortal/ VITE_BASE_URL=/openRepoPortal/
# AI大模型接口地址
VITE_AI_API_HOST = ''http://222.20.126.208:8089'

View File

@@ -113,6 +113,115 @@
</div> </div>
</d-tab> </d-tab>
<d-tab id="scdh" title="收藏对话"> <d-tab id="scdh" title="收藏对话">
<d-search icon-position="left" style="width: 100%" placeholder="请输入搜索软件名称" v-model="searchText" @change="handleSearch"></d-search>
<!-- 可滚动内容区域 -->
<div class="scrollable-content">
<d-collapse class="jyh-collapse" v-model="collapseValue">
<d-collapse-item name="today">
<template #title>
<div class="jyh-collapse-title">
<d-icon style="margin-right: 4px" name="icon-delay"></d-icon>今天
</div>
</template>
<div>
<!-- 循环生成卡片列表 -->
<div v-if="cardList.length>0">
<div
v-for="(item, index) in cardList"
:key="index"
:class="['mb-3 jyh-collapse-card', { 'selected-card': selectedIndex === item.id }]"
@click="selectCard(item)"
@mouseenter="hoverIndex = index"
@mouseleave="hoverIndex = null"
>
<!-- 卡片标题 -->
<div class="card-header">
<h4>{{ item.title }}</h4>
<!-- 悬停操作按钮 -->
<div class="card-actions" v-show="hoverIndex === index">
<d-tooltip content="编辑">
<d-icon style="font-size: 14px;margin-right: 2px" @click.stop="handleEdit(item)" name="icon-edit-3"></d-icon>
</d-tooltip>
<d-tooltip content="分享">
<d-icon style="font-size: 14px;margin-right: 2px" @click.stop="handleShare(item)" name="icon-redo-new"></d-icon>
</d-tooltip>
<d-tooltip content="更多">
<d-icon style="font-size: 14px;" @click.stop="handleMore(item)" name="icon-more-operate"></d-icon>
</d-tooltip>
</div>
</div>
<!-- 卡片内容 -->
<div class="card-content">
<div class="content-left">
<img width="16" style="margin-right: 4px" src="@/assets/imgs/jyh/ai/aiLogo2.png" />
<span>{{ item.text }}</span>
</div>
<div class="content-right">
<span class="time-text">{{ formatTime(item.time) }}</span>
</div>
</div>
</div>
</div>
<NoData v-else :small="false"></NoData>
</div>
</d-collapse-item>
<d-collapse-item name="yestoday">
<template #title>
<div class="jyh-collapse-title">
<d-icon style="margin-right: 4px" name="icon-delay"></d-icon>昨天
</div>
</template>
<div>
<div
v-for="(item, index) in cardList"
:key="index"
class="mb-3 jyh-collapse-card"
>
<!-- 卡片标题 -->
<div class="card-header">
<h4>{{ item.title }}</h4>
</div>
<!-- 卡片内容 -->
<div class="card-content">
<div class="content-left">
<img width="16" style="margin-right: 4px" src="@/assets/imgs/jyh/ai/aiLogo2.png" />
<span>{{ item.text }}</span>
</div>
<div class="content-right">
<span class="time-text">{{ formatTime(item.time) }}</span>
</div>
</div>
</div>
</div>
</d-collapse-item>
</d-collapse>
</div>
<!-- 固定在底部的控制栏 -->
<div class="bottom-control-bar">
<div class="control-left">
历史对话保存条数
<d-select
v-model="historyLimit"
style="width: 60px"
size="sm"
class="jyh-select"
>
<d-option value="10">10</d-option>
<d-option value="20">20</d-option>
<d-option value="30">30</d-option>
<d-option value="0">0</d-option>
</d-select>
</div>
<div class="control-right">
<d-button class="jyh-button2" variant="solid" @click="openBatchManageModal">
<template #icon>
<d-icon name="icon-property-setting"></d-icon>
</template>
批量管理
</d-button>
<BatchManageModal ref="BatchManageModalRef"/>
</div>
</div>
</d-tab> </d-tab>
</d-tabs> </d-tabs>
</Card> </Card>

View File

@@ -64,7 +64,7 @@
<!-- <ToolsFloat v-if="showToolsFloat" />--> <!-- <ToolsFloat v-if="showToolsFloat" />-->
<!-- d-layout 结束前添加返回按钮 --> <!-- d-layout 结束前添加返回按钮 -->
<div class="back-in-btn"> <div class="back-in-btn">
<div class="back-in-time-btn" @click="handleBackInTime"> <div class="back-in-time-btn" @click="toggleHistory" :class="{ 'active': isHistoryVisible }">
<i class="icon icon-history" title="查看历史"></i> <i class="icon icon-history" title="查看历史"></i>
</div> </div>
<div class="back-in-time-btn" @click="handleBackInTime"> <div class="back-in-time-btn" @click="handleBackInTime">
@@ -72,7 +72,7 @@
</div> </div>
</div> </div>
<ViewHistoryAside /> <ViewHistoryAside v-show="isHistoryVisible"/>
</d-layout> </d-layout>
</template> </template>
@@ -104,6 +104,9 @@ const refHeader: any = ref(null);
const footerSize = useElementSize(refFooter); const footerSize = useElementSize(refFooter);
const isHome = computed(() => route.name === 'home'); const isHome = computed(() => route.name === 'home');
// 控制 ViewHistoryAside 组件的显示状态
const isHistoryVisible = ref(false);
// aside 设置可见性 header有两层有顶栏信息,两层sidebar 都不可见 // aside 设置可见性 header有两层有顶栏信息,两层sidebar 都不可见
const asideSetShow = computed(() => { const asideSetShow = computed(() => {
if (route.meta.asideMenu === 'drawer') return false; if (route.meta.asideMenu === 'drawer') return false;
@@ -160,26 +163,10 @@ const handleClickMenu = () => {
} }
}; };
const outOfSearch = ref(true); // 在search页面隐藏footer // 切换 ViewHistoryAside 组件的显示与隐藏
router.afterEach((to, form) => { const toggleHistory = () => {
outOfSearch.value = to.name !== 'search'; isHistoryVisible.value =!isHistoryVisible.value;
});
microApp.router.afterEach({
'micoro-app-homeweb-app': (to, from) => {
outOfSearch.value = to.pathname !== '/search';
}
});
const checkPage = (data: any) => {
if (data.type === 'checkPage') {
return 'ready';
}
}; };
onMounted(() => {
microApp.addDataListener('micoro-app-homeweb-app', checkPage, false);
});
onUnmounted(() => {
microApp.removeDataListener('micoro-app-homeweb-app', checkPage);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -230,6 +217,14 @@ onUnmounted(() => {
display: flex; display: flex;
gap: 12px; gap: 12px;
.active {
background-color: #f0f0f0; /* 灰色背景 */
}
.active .icon {
transform: translateY(-4px); /* 图标上移4px */
}
.back-in-time-btn { .back-in-time-btn {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -0,0 +1,311 @@
<template>
<div class="secret-container">
<!-- <div class="title">以下是关于近期预警的公告</div>-->
<div class="mt-3">
<d-table
:key="tableKey"
ref="filterTableRef"
class="jyh-table"
v-if="tableData.length"
:striped="false"
:data="tableData"
>
<template v-for="column in columns" :key="column.key">
<d-column :field="column.key" :header="column.label" :width="column.width || 120" show-overflow-tooltip></d-column>
</template>
</d-table>
<!-- <DataPanel v-else skeleton :card="false" :empty="true">-->
<!-- </DataPanel>-->
<NoData v-else :small="false"></NoData>
</div>
<d-button v-if="isLogin" class="output-btn" @click="exportResult">导出列表</d-button>
<div class="bubble-bottom-operations">
<img onclick="handleRefresh()" src="@/assets/imgs/jyh/ai/icon_generate.png" />
<i class="icon-file" title="复制" onclick="handleFile()"></i>
<i class="icon-like" title="点赞" onclick="handleLike()"></i>
<i class="icon-dislike" title="踩" onclick="handleDislike()"></i>
<i class="icon-clever-customer" title="智能客服" onclick="handleCustomerService()"></i>
<i class="icon-more-operate" title="更多操作" onclick="handleMore()"></i>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, onMounted, watch, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { Message } from 'vue-devui';
import { storeToRefs } from 'pinia';
import NoData from '@/components/NoData/NoData.vue';
import { useAccountStore } from '@/stores/user';
const { formatTime } = useTimeFormat();
import { getHomeNoticeList, getReleaseExport, releasePage, searchLanguageList } from '@/api/jyh';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import { useGlobalInfoStore } from '@/stores/Global';
const router = useRouter();
const route = useRoute();
const props = defineProps(['content'])
debugger
const filterTableRef = ref();
const { namespace } = getOrgInfo();
const userInfo = useAccountStore();
// 定义 tableKey用于强制刷新表格
const tableKey = ref(0);
const tableData = ref([]);
// 定义字段名和中文翻译的映射
const fieldTranslation = {
id: '编号',
task_id: '任务ID',
software_id: '软件ID',
component_id: '组件ID',
vuln_id: '漏洞ID',
cwe_id: 'CWE编号',
severity: '严重程度',
vuln_type: '漏洞类型',
cvss_score: 'CVSS评分',
attack_vector: '攻击向量',
attack_complexity: '攻击复杂度',
user_interaction: '用户交互',
privileges_required: '所需权限',
exploitability_score: '可利用性评分',
impact_score: '影响评分',
description: '描述',
extra_info: '额外信息',
fix_issue: '修复问题',
patch_link: '补丁链接',
source: '来源',
publish_date: '发布日期',
fix_version: '修复版本',
create_time: '创建时间',
update_time: '更新时间',
exploitability: '可利用性',
ai_fix_suggestion: 'AI修复建议',
upgradeVersion: '升级版本',
vulDescription: '漏洞描述',
vulFeature: '漏洞特征'
};
// 定义 columns 数据
const columns = ref([ ]);
// 处理数据,生成列信息
const processData = () => {
debugger
const uniqueKeys = new Set();
tableData.value.forEach((item) => {
Object.keys(item).forEach((key) => uniqueKeys.add(key));
});
columns.value = Array.from(uniqueKeys).map((key) => ({
key,
label: fieldTranslation[key] || key,
width: 120
}));
tableKey.value++;
};
// 监听数据变化,重新处理数据
watch(() => props.content, (newData) => {
tableData.value = JSON.parse(newData);
debugger
processData();
}, { deep: true,immediate:true });
// 跳转
const gotoDetail = (row) => {
// 后续请换成params暂时params传参没传过去
router.push({
path: 'repoDetail/' + row.id
});
};
const exportResult = async () => {
const selectDatas = tableData.value.map((d) => d.id);
if (!tableData.value.length || selectDatas.length <= 0) {
Message.info('未选择任何数据');
return;
}
const { data, error } = await getReleaseExport({
list: selectDatas,
});
if (data && !error) {
// 方法1如果后端返回的是文件流Blob
const blob = new Blob([data.data]);
const downloadUrl = window.URL.createObjectURL(blob);
// 创建一个临时的a标签触发下载
const link = document.createElement('a');
link.href = downloadUrl;
// 从Content-Disposition头中获取文件名或者使用默认文件名
const contentDisposition = data.headers['content-disposition'];
let fileName = 'export.xlsx'; // 默认文件名
// if (contentDisposition) {
// const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/);
// if (fileNameMatch && fileNameMatch[1]) {
// fileName = fileNameMatch[1].replace(/^[^0-9]*|[^0-9.xlsx]*$/g, "");
// }
// }
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
} else {
console.error('导出失败:', error);
// 这里可以添加错误提示比如使用Element Plus的ElMessage
ElMessage.error('文件导出失败');
}
};
// 事件处理函数
function handleRefresh() {
console.log('刷新点击');
// 实际业务代码示例:
// location.reload(); // 重新加载页面
// 或调用API刷新数据
}
function handleFile() {
console.log('文件点击');
// 示例:触发文件上传
// document.getElementById('file-input').click();
}
function handleLike() {
console.log('点赞点击');
// 示例:切换点赞状态
const likeBtn = document.querySelector('.icon-like');
likeBtn.classList.toggle('active');
// 这里可以添加AJAX请求提交点赞状态
}
function handleDislike() {
console.log('踩点击');
// 类似点赞的逻辑
const dislikeBtn = document.querySelector('.icon-dislike');
dislikeBtn.classList.toggle('active');
}
function handleCustomerService() {
console.log('客服点击');
// 示例:打开客服对话框
// openChatWindow();
}
function handleMore() {
console.log('更多操作点击');
// 示例:显示下拉菜单
// showDropdownMenu();
}
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.secret-container {
position: relative;
//min-height: 600px;
background: #ffffff;
padding: 20px;
margin-top: 16px;
border-radius: 15px;
.title {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: bold;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.secret-op {
position: absolute;
right: 0;
top: 0;
}
}
.error {
color: $devui-contrast;
}
:deep(.devui-table__row td) {
padding-top: 16px;
padding-bottom: 16px;
}
:deep(.devui-tag .devui-tag--default) {
background-color: #fff;
border-radius: 12px;
padding: 4px 8px;
font-size: 12px;
line-height: 16px;
margin-right: 8px;
border: 1px solid #e3e3ee;
}
.one-line {
word-break: break-all;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.title {
font-size: 14px;
font-weight: 400;
color: #9a9b9c;
line-height: 20px;
}
.cur-title {
font-size: 14px;
font-weight: 500;
color: #2d2d2e;
line-height: 20px;
}
.output-btn {
border-radius: 16px;
background: #DFDFDF;
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.secret-breadcrumb {
padding: 7px 20px 12px;
background: #ffffff;
}
.bubble-bottom-operations {
margin-top: 16px;
margin-left: auto;
display: flex;
gap: 8px; /* 图标之间的间隔 */
align-items: center; /* 垂直居中图标 */
justify-content: end;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="secret-container">
<McMarkdownCard :content="props.content"></McMarkdownCard>
</div>
</template>
<script setup lang="ts">
import {defineProps} from 'vue'
const props = defineProps(['content'])
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.secret-container {
position: relative;
//min-height: 600px;
background: #ffffff;
padding: 20px;
margin-top: 16px;
border-radius: 15px;
.title {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: bold;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
}
</style>

View File

@@ -22,27 +22,10 @@
<WarningList v-else-if="msg.type===2"/> <WarningList v-else-if="msg.type===2"/>
<!--软件详情--> <!--软件详情-->
<OssDetail v-else-if="msg.type===3"/> <OssDetail v-else-if="msg.type===3"/>
<!--软件详情-->
<McMarkdownCard v-else :content="msg.content"></McMarkdownCard> <AIList v-else-if="msg.type==='df'" :content="msg.content"/>
<!--text或者富文本展示-->
<div class="conversation-cnxw"> <Other v-else :content="msg.content"></Other>
<a class="flex conversation-cnxw-item">
<span class="span2">猜你想问</span>
<img style="height: 15px;margin-right: 8px" width="16" src="@/assets/imgs/jyh/ai/icon_generate.png" />
</a>
<div class="flex conversation-cnxw-item">
<a class="g-header-copilot" @click="onSubmit('我想看Redis的详情')">
<img style="height: 15px;" width="16" src="@/assets/imgs/jyh/ai/icon_cnxw.png" />
<span class="ml-xs">我想看Redis的详情</span>
</a>
</div>
<div class="flex conversation-cnxw-item">
<a class="g-header-copilot" @click="onSubmit('Redis的可信评分是多少')">
<img style="height: 15px;" width="16" src="@/assets/imgs/jyh/ai/icon_cnxw.png" />
<span class="ml-xs">Redis的可信评分是多少</span>
</a>
</div>
</div>
</McBubble> </McBubble>
<!-- <McBubble v-else :loading="msg.loading ?? false" :avatarConfig="msg.avatarConfig">--> <!-- <McBubble v-else :loading="msg.loading ?? false" :avatarConfig="msg.avatarConfig">-->
@@ -56,6 +39,24 @@
<!-- </template>--> <!-- </template>-->
<!-- </McBubble>--> <!-- </McBubble>-->
</template> </template>
<div class="conversation-cnxw">
<a class="flex conversation-cnxw-item">
<span class="span2">猜你想问</span>
<img style="height: 15px;margin-right: 8px" width="16" src="@/assets/imgs/jyh/ai/icon_generate.png" />
</a>
<div class="flex conversation-cnxw-item">
<a class="g-header-copilot" @click="onSubmit('我想看Redis的详情')">
<img style="height: 15px;" width="16" src="@/assets/imgs/jyh/ai/icon_cnxw.png" />
<span class="ml-xs">我想看Redis的详情</span>
</a>
</div>
<div class="flex conversation-cnxw-item">
<a class="g-header-copilot" @click="onSubmit('Redis的可信评分是多少')">
<img style="height: 15px;" width="16" src="@/assets/imgs/jyh/ai/icon_cnxw.png" />
<span class="ml-xs">Redis的可信评分是多少</span>
</a>
</div>
</div>
</div> </div>
<div v-else class="welcome-page"> <div v-else class="welcome-page">
<div class="home-title"> <div class="home-title">
@@ -142,6 +143,15 @@ import GuessYouWantToAsk from './GuessYouWantToAsk.vue';
import OssList from './components/OssList.vue'; import OssList from './components/OssList.vue';
import WarningList from './components/WarningList.vue'; import WarningList from './components/WarningList.vue';
import OssDetail from './components/OssDetail.vue'; import OssDetail from './components/OssDetail.vue';
import Other from './components/Other.vue';
import AIList from './components/AIList.vue';
import { getChat } from '@/api/jyh';
import axios from 'axios';
import { useDiscussGetUserInfo } from '@/api/discussion/hook';
// 获取当前用户信息 & 是否登录
const { isLogin = false, userInfo = {}} = useDiscussGetUserInfo();
debugger
const inputValue = ref(''); const inputValue = ref('');
const startChat = ref(false); const startChat = ref(false);
@@ -201,20 +211,32 @@ const onSubmit = (e, answer = undefined) => {
}); });
}); });
// getAIAnswer(answer ?? e); // getAIAnswer(answer ?? e);
getAIAnswer(mockAnswer,1); getAIAnswer(e,1);
}; };
const getAIAnswer = (content,type) => { const getAIAnswer = async (content) => {
messages.value.push({ messages.value.push({
from: 'ai-model', from: 'ai-model',
type: type || 1, type: '',
content: content, content: '',
avatarPosition: 'side-left', avatarPosition: 'side-left',
avatarConfig: { ...aiModelAvatar }, avatarConfig: { ...aiModelAvatar },
loading: false, loading: true,
}); });
const {data} = await axios.get('http://222.20.126.208:8089/api/v0/chat_use_sql?question='+content);
debugger
const {text,type} = data;
if(type==='text') {
messages.value[messages.value.length - 1].content = text;
messages.value[messages.value.length - 1].loading = false;
messages.value[messages.value.length - 1].type = 'text';
} else if(type==='df') {
messages.value[messages.value.length - 1].content = data[type];
messages.value[messages.value.length - 1].loading = false;
messages.value[messages.value.length - 1].type = 'df';
}
// messages.value.push({ // messages.value.push({
// from: 'ai-model', // from: 'ai-model',
// content: '', // content: '',