完善细节

This commit is contained in:
BBIT-Kai
2025-11-14 18:08:04 +08:00
parent d39c37f896
commit 530cede0bd
11 changed files with 561 additions and 14 deletions
+3 -2
View File
@@ -61,7 +61,7 @@ async def main():
# MCP服务-ailab # MCP服务-ailab
endpoint_url_ai_lab = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D" endpoint_url_ai_lab = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D"
# endpoint_url_ai_lab = "ws://ce_bot_mcp:8004/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D" # endpoint_url_ai_lab = "ws://ce_bot_mcp:8004/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D"
task_mcp1 = asyncio.create_task(init_mcp_server(endpoint_url_ai_lab)) # task_mcp1 = asyncio.create_task(init_mcp_server(endpoint_url_ai_lab))
# MCP服务-ql # MCP服务-ql
endpoint_url_ql = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=8ZmCzp7FzsbxwHOg2%2FvBQkxrC3QWJiI%2B4iTfouExinjcT8ZgLwQfFUtgcMInI7St" endpoint_url_ql = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=8ZmCzp7FzsbxwHOg2%2FvBQkxrC3QWJiI%2B4iTfouExinjcT8ZgLwQfFUtgcMInI7St"
@@ -70,7 +70,8 @@ async def main():
# RabbitMQ服务 # RabbitMQ服务
task_mq = asyncio.create_task(mq_pull_analysis_async()) task_mq = asyncio.create_task(mq_pull_analysis_async())
await asyncio.gather(task_api, task_mcp1, task_mcp2, task_mq) # await asyncio.gather(task_api, task_mcp1, task_mcp2, task_mq)
await asyncio.gather(task_api, task_mcp2, task_mq)
if __name__ == "__main__": if __name__ == "__main__":
+7
View File
@@ -22,6 +22,13 @@ def push_file(bucket_name, object_name, file_bytes, contents, content_type):
) )
def get_upload_token(bucket_name, object_name, xpires=timedelta(hours=1)):
upload_url = minio_client.presigned_put_object(
bucket_name=bucket_name, object_name=object_name, expires=xpires
)
return {"upload_url": upload_url, "object_name": object_name}
def get_temp_url(bucket_name, object_name): def get_temp_url(bucket_name, object_name):
return minio_client.presigned_get_object( return minio_client.presigned_get_object(
bucket_name, object_name, expires=timedelta(seconds=3600) bucket_name, object_name, expires=timedelta(seconds=3600)
+2 -2
View File
@@ -1,7 +1,7 @@
from utils.GlobalVariable import LOCAL_IP from utils.GlobalVariable import LOCAL_IP
RABBIT_HOST = LOCAL_IP RABBIT_HOST = LOCAL_IP
RABBIT_VHOST = "/bbit_ai" RABBIT_VHOST = "bbit_ai"
RABBIT_USER = "bbit_ai" RABBIT_USER = "ai_lab"
RABBIT_PASSWORD = "123456" RABBIT_PASSWORD = "123456"
QUEUE_NAME = "analysis_queue" QUEUE_NAME = "analysis_queue"
+4 -4
View File
@@ -4,15 +4,15 @@ from uuid import UUID
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
import db.postgres as pg import db.postgres as pg
import utils.MyUtils as utils
from agent.serviceAgent import get_service_agent_reply
from config.security import get_user_id_from_token from config.security import get_user_id_from_token
from llm.memLLM import take_memory
from llm.titleChain import get_title
from models.BaseResponse import BaseResponse from models.BaseResponse import BaseResponse
from models.ChatRequest import ChatRequest from models.ChatRequest import ChatRequest
serviceRouter = APIRouter() serviceRouter = APIRouter()
from llm.titleChain import get_title
from agent.serviceAgent import get_service_agent_reply
from llm.memLLM import take_memory
import utils.MyUtils as utils
# 对话列表 # 对话列表
+9
View File
@@ -1,8 +1,10 @@
import uuid
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, File, Form, Depends, Query from fastapi import APIRouter, File, Form, Depends, Query
import db.postgres as pg import db.postgres as pg
from config.minIO import get_upload_token
from config.security import get_user_id_from_token from config.security import get_user_id_from_token
from llm.ticketLLM import * from llm.ticketLLM import *
from models.BaseResponse import BaseResponse from models.BaseResponse import BaseResponse
@@ -149,3 +151,10 @@ def getSilkwormCocoonAnalysisTasks(
"items": items, "items": items,
} }
) )
@visionRouter.post("/getIVASCUploadToken")
def getIVASCUploadToken():
# 生成唯一文件名,避免覆盖
object_name = f"raw/{uuid.uuid4()}"
return get_upload_token("video-sca", object_name)
+1 -1
View File
@@ -13,7 +13,7 @@
}, },
{ {
"type": "chrome", "type": "chrome",
"name": "主干AI实验室", "name": "AI实验室",
"request": "launch", "request": "launch",
"url": "http://localhost:5666", "url": "http://localhost:5666",
"env": { "NODE_ENV": "development" }, "env": { "NODE_ENV": "development" },
+1 -1
View File
@@ -1,5 +1,5 @@
# 应用标题 # 应用标题
VITE_APP_TITLE=主干AI实验室 VITE_APP_TITLE=AI实验室
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离
VITE_APP_NAMESPACE=vben-web-antd VITE_APP_NAMESPACE=vben-web-antd
@@ -7,7 +7,7 @@ const routes: RouteRecordRaw[] = [
{ {
meta: { meta: {
icon: 'ic:round-remove-red-eye', icon: 'ic:round-remove-red-eye',
authority: ['iva', 'sca', 'sca2', 'ysa', 'ticket', 'license'], authority: ['iva', 'iva-sc', 'sca', 'sca2', 'ysa', 'ticket', 'license'],
keepAlive: true, keepAlive: true,
order: 2, order: 2,
title: $t('计算机视觉'), title: $t('计算机视觉'),
@@ -21,11 +21,22 @@ const routes: RouteRecordRaw[] = [
meta: { meta: {
authority: ['iva'], authority: ['iva'],
icon: 'mdi:video', icon: 'mdi:video',
title: $t('ai.intelligence_video_analysis'), title: '工作视频分析',
keepAlive: true, keepAlive: true,
}, },
component: () => import('#/views/cv/iva/index.vue'), component: () => import('#/views/cv/iva/index.vue'),
}, },
{
name: 'IVA-sc',
path: '/cv/iva-sc',
meta: {
authority: ['iva-sc'],
icon: 'mdi:video-image',
title: '蚕茧视频分析',
keepAlive: true,
},
component: () => import('#/views/cv/iva-sc/index.vue'),
},
{ {
name: 'SCA', name: 'SCA',
path: '/cv/sca', path: '/cv/sca',
@@ -97,6 +97,16 @@ const routes: RouteRecordRaw[] = [
title: '数据可视化', title: '数据可视化',
}, },
}, },
{
name: 'VMManager-webui',
path: '/set/VMManager',
component: IFrameView,
meta: {
icon: 'mdi:monitor-dashboard',
link: `https://10.10.12.100/ui/#/host`,
title: '虚拟机管理',
},
},
], ],
}, },
]; ];
@@ -0,0 +1,502 @@
<script setup lang="ts">
import type Player from 'video.js/dist/types/player';
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch,
} from 'vue';
import { AnalysisOverview, useVbenModal } from '@vben/common-ui';
import {
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Button, Form, Input, message } from 'ant-design-vue';
import videojs from 'video.js';
import * as api from '#/api';
import { createImageTaskV2 } from '#/api';
import 'video.js/dist/video-js.css';
const list = ref<any[]>([]);
const error = ref<null | string>(null);
const filterKeyword = ref('');
const activeTab = ref<'detail' | 'video'>('detail');
const selectedItem = ref<any>(null);
const detailList = ref<any[]>([]);
const videoEl = ref<HTMLVideoElement | null>(null);
const player = ref<null | Player>(null);
async function loadList() {
error.value = null;
const res = await api.refreshVideoList(filterKeyword.value);
list.value = res || [];
}
function refreshList() {
filterKeyword.value = '';
loadList();
message.success('视频列表加载完成');
}
function createTask() {
modalApi.open();
}
async function selectItem(item: any) {
const res = await api.refreshVideoDetail(item.v_id);
selectedItem.value = res;
refreshLineChart();
}
const tabs = [
{ key: 'detail', label: '分析详情' },
{ key: 'video', label: '分析视频' },
];
let overviewItems: AnalysisOverviewItem[];
// 监听关键词变化,调用防抖接口
watch(filterKeyword, () => {
loadList();
});
watch(selectedItem, () => {
overviewItems = [
{
icon: SvgDownloadIcon,
title: '出现的蚕茧数量',
totalTitle: '',
totalValue: 0,
value: selectedItem.value.v_a_count_people,
},
{
icon: SvgCardIcon,
title: '出现最多的种类',
totalTitle: '',
totalValue: 0,
value: selectedItem.value.v_a_max_action,
},
{
icon: SvgCakeIcon,
title: '最多同框蚕茧数量',
totalTitle: '',
totalValue: 0,
value: selectedItem.value.v_a_total_people,
},
{
icon: SvgBellIcon,
title: '时间',
totalTitle: '最长停留时间',
totalValue: 0,
value: selectedItem.value.v_a_max_stay_time,
},
];
});
watch([activeTab, selectedItem], async ([tab]) => {
if (tab === 'video' && selectedItem.value?.v_video_play_path) {
refreshVideoPlayer();
}
});
onMounted(() => {
loadList();
});
onBeforeUnmount(() => {
player.value?.dispose();
player.value = null;
});
// ✅ 切换视频项时销毁并重建
function refreshVideoPlayer() {
nextTick(() => {
if (player.value) {
player.value.src([
{
src: selectedItem.value.v_video_play_path,
type: 'video/mp4',
},
]);
} else {
if (!videoEl.value) return;
player.value = videojs(videoEl.value, {
controls: true,
autoplay: false,
preload: 'auto',
sources: [
{
src: selectedItem.value.v_video_play_path,
type: 'video/mp4',
},
],
});
}
});
}
const showInfoStr = ref<Record<string, number | string>>({});
const chartRef1 = ref<EchartsUIType>();
const { renderEcharts: renderEcharts1 } = useEcharts(chartRef1);
const chartRef2 = ref<EchartsUIType>();
const { renderEcharts: renderEcharts2 } = useEcharts(chartRef2);
function refreshLineChart() {
const data = selectedItem.value;
showInfoStr.value = {
项目名: data.v_name,
文件名: data.v_file_name,
文件大小: `${data.v_size} MB`,
总时长: `${data.v_duration}`,
分辨率: data.v_resolution,
视频编码格式: data.v_video_codec,
音频编码格式: data.v_audio_codec,
总体比特率: data.v_overall_bit_rate,
};
detailList.value = data.v_details_list || [];
const detail = selectedItem.value.v_a_details;
let yTotalData = Array.isArray(detail.yTotalData)
? detail.yTotalData.map((item: any) => [item.first, item.second])
: []; // 默认值为空数组
let yMaskedData = Array.isArray(detail.yMaskedData)
? detail.yMaskedData.map((item: any) => [item.first, item.second])
: []; // 默认值为空数组
const areaData = Array.isArray(detail.areaData)
? detail.areaData.map((actionGroup: any) => {
return actionGroup.map((action: any) => {
return { xAxis: action.xAxis, itemStyle: action.itemStyle };
});
})
: []; // 默认值为空数组
yTotalData = yTotalData.map((item: any) => [
new Date(item[0]).getTime(),
item[1],
]);
yMaskedData = yMaskedData.map((item: any) => [
new Date(item[0]).getTime(),
item[1],
]);
renderEcharts1({
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
series: [
{
name: '总人数',
type: 'line',
step: 'end',
data: yTotalData,
markArea: {
itemStyle: { color: 'rgba(255, 173, 177, 0.4)' },
data: areaData,
},
},
{ name: '口罩佩戴人数', type: 'line', step: 'end', data: yMaskedData },
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#019680',
width: 1,
},
},
trigger: 'axis',
},
xAxis: { type: 'time' },
yAxis: { type: 'value' },
});
const maskedRatio = data.v_a_average_masked_ratio * 100;
const noMaskedRatio = 100 - maskedRatio;
renderEcharts2({
legend: { top: '5%', left: 'center' },
series: [
{
animationDelay() {
return Math.random() * 100;
},
animationEasing: 'exponentialInOut',
animationType: 'scale',
avoidLabelOverlap: false,
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: [
{
value: maskedRatio,
name: '非正茧(%)',
},
{
value: noMaskedRatio,
name: '正茧(%)',
},
],
emphasis: {
label: {
fontSize: '12',
fontWeight: 'bold',
show: true,
},
},
itemStyle: {
borderRadius: 10,
borderWidth: 3,
},
label: {
position: 'center',
show: false,
},
labelLine: {
show: false,
},
padAngle: 5,
name: '正茧平均占比',
radius: ['40%', '70%'],
type: 'pie',
},
],
tooltip: {
trigger: 'item',
},
});
if (activeTab.value === 'video' && player.value) {
// 如果当前是视频标签页,刷新播放器
refreshVideoPlayer();
}
}
const projectName = ref('');
const fileName = ref('');
const selectedFile = ref<File | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const [Modal, modalApi] = useVbenModal({
title: '新建蚕茧分析任务',
class: 'w-[600px]',
onCancel() {
modalApi.close();
},
onConfirm() {
if (!selectedFile.value) {
message.warning('请选择蚕茧图片');
return;
}
uploadFile();
},
});
async function uploadFile() {
// 先关闭弹窗
modalApi.close();
const formData = new FormData();
formData.append('file', selectedFile.value!);
formData.append('projectName', projectName.value);
await createImageTaskV2(formData).then(() => {
// 清空表单
projectName.value = '';
fileName.value = '';
selectedFile.value = null;
});
}
function selectFile() {
fileInputRef.value?.click();
}
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
selectedFile.value = files[0];
fileName.value = files[0].name;
}
}
// 分页参数
const page = ref(1);
const pageSize = ref(9);
const total = ref(0); // 总条数
// 计算总页数
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
function changePage(newPage) {
page.value = newPage;
loadList();
}
</script>
<template>
<div class="flex h-full w-full flex-col">
<BaseModal />
<Modal>
<Form layout="vertical">
<Form.Item label="任务名称">
<Input v-model:value="projectName" />
</Form.Item>
<Form.Item label="上传视频*" required>
<div
@click="selectFile"
style="
padding: 16px;
text-align: center;
cursor: pointer;
border: 1px dashed #d9d9d9;
"
>
{{ fileName || '点击选择文件' }}
<input
type="file"
accept="video/*"
ref="fileInputRef"
@change="handleFileChange"
style="display: none"
/>
</div>
</Form.Item>
</Form>
</Modal>
<div class="flex h-full w-full bg-gray-50">
<!-- 左侧筛选 + 列表 -->
<div class="flex w-64 flex-col border-r bg-white p-4">
<!-- 按钮组 -->
<div class="mb-4 flex justify-between space-x-2">
<Button type="primary" @click="createTask" class="flex-1">
新建任务
</Button>
<Button @click="refreshList" class="flex-1"> 刷新列表 </Button>
</div>
<!-- 筛选框 -->
<input
v-model="filterKeyword"
placeholder="筛选分析任务"
class="focus:ring-primary mb-4 rounded-md border px-3 py-2 focus:outline-none focus:ring-2"
/>
<!-- 列表 -->
<div class="flex-1 space-y-2 overflow-auto">
<div
v-for="item in list"
:key="item.v_id"
@click="selectItem(item)"
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
:class="{ 'bg-gray-100': item.v_id === selectedItem?.v_id }"
>
<div class="text-base font-medium">{{ item.v_name }}</div>
<div class="text-sm text-gray-400">{{ item.v_a_time }}</div>
</div>
</div>
<!-- 分页 -->
<div class="mt-2 flex justify-center space-x-2">
<button
:disabled="page === 1"
@click="changePage(page - 1)"
class="rounded border px-1"
>
上一页
</button>
<span> {{ page }} / {{ totalPages }} </span>
<button
:disabled="page === totalPages"
@click="changePage(page + 1)"
class="rounded border px-1"
>
下一页
</button>
</div>
</div>
<!-- 右侧Tab 内容区 -->
<div class="flex flex-1 flex-col overflow-hidden p-6">
<!-- Tab 标题 -->
<div class="mb-4 flex shrink-0 space-x-4 border-b">
<button
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key as 'detail' | 'video'"
class="px-4 py-2"
:class="[
activeTab === tab.key
? 'border-primary text-primary border-b-2'
: 'hover:text-primary text-gray-500',
]"
>
{{ tab.label }}
</button>
</div>
<!-- Tab 内容滚动区域 -->
<div class="flex-1 overflow-auto">
<div
v-if="!selectedItem"
class="flex h-full items-center justify-center text-gray-400"
>
请先选择左侧列表中的分析任务
</div>
<template v-else>
<div
v-show="activeTab === 'detail'"
class="flex h-full flex-col gap-4"
>
<!-- 主内容区域左右结构 -->
<div class="flex flex-1 gap-4">
<!-- 左侧 -->
<div class="flex w-72 flex-col gap-4">
<!-- 视频基础信息展示 -->
<div
class="w-full rounded border bg-white p-4"
id="video_base_info"
>
<div
v-for="(value, key) in showInfoStr"
:key="key"
class="mb-2 flex text-sm text-gray-700"
>
<div class="w-32 font-medium text-gray-900">
{{ key }}
</div>
<div class="flex-1 break-all text-gray-600">
{{ value || '—' }}
</div>
</div>
</div>
<!-- 空白卡片 -->
<div class="h-[300px] flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef2" />
</div>
</div>
<!-- 右侧 -->
<div class="flex flex-1 flex-col gap-4">
<!-- 四个统计卡片 -->
<AnalysisOverview
:items="overviewItems"
class="grid grid-cols-4 gap-4"
/>
<!-- 折线图区域 -->
<div class="flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef1" />
</div>
</div>
</div>
</div>
<div v-show="activeTab === 'video'" class="flex h-full space-x-4">
<video
ref="videoEl"
class="video-js vjs-default-skin h-full w-full"
preload="auto"
controls
></video>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
@@ -19,9 +19,16 @@ const cv: WorkbenchQuickNavItem[] = [
color: '#3fb27f', color: '#3fb27f',
authority: ['iva'], authority: ['iva'],
icon: 'mdi:video', icon: 'mdi:video',
title: '视频智能分析', title: '工作视频分析',
url: '/cv/iva', url: '/cv/iva',
}, },
{
color: '#3fb27f',
authority: ['iva-sc'],
icon: 'mdi:video-image',
title: '蚕茧视频分析',
url: '/cv/iva-sc',
},
{ {
color: '#3fb27f', color: '#3fb27f',
authority: ['sca'], authority: ['sca'],
@@ -139,7 +146,7 @@ function getGreeting() {
{{ getGreeting() }}, {{ userStore.userInfo?.username }}, {{ getGreeting() }}, {{ userStore.userInfo?.username }},
开始您一天的工作吧 开始您一天的工作吧
</template> </template>
<template #description> 欢迎使用主干AI实验室</template> <template #description> 欢迎使用AI实验室</template>
</WorkbenchHeader> </WorkbenchHeader>
<div class="mt-5 flex flex-col lg:flex-row"> <div class="mt-5 flex flex-col lg:flex-row">