Compare commits
10 Commits
0de07e733a
...
3b431b978a
| Author | SHA1 | Date |
|---|---|---|
|
|
3b431b978a | |
|
|
7188f8ab71 | |
|
|
a8ac40d6fb | |
|
|
a2200c0296 | |
|
|
43599fea1d | |
|
|
407e93c208 | |
|
|
4361e7fa0f | |
|
|
d49550807b | |
|
|
bbad3da13a | |
|
|
2acc0d4239 |
|
|
@ -369,3 +369,4 @@ VideoAnalysis/WebUI/node_modules/
|
||||||
VideoAnalysis/WebUI/dist/
|
VideoAnalysis/WebUI/dist/
|
||||||
VideoAnalysis/WebUI/.vscode/
|
VideoAnalysis/WebUI/.vscode/
|
||||||
/VideoAnalysis/device_id.config
|
/VideoAnalysis/device_id.config
|
||||||
|
/Learn.VideoAnalysis.API/device_id.config
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ namespace Learn.VideoAnalysis.API.Expand
|
||||||
Console.WriteLine($"{DateTime.Now}=>初始化 Coravel");
|
Console.WriteLine($"{DateTime.Now}=>初始化 Coravel");
|
||||||
service.AddScheduler();
|
service.AddScheduler();
|
||||||
service.AddTransient<TaskFileClearJob>();
|
service.AddTransient<TaskFileClearJob>();
|
||||||
|
service.AddTransient<ClearAllCacheJob>();
|
||||||
service.AddTransient<NodePackageJob>();
|
service.AddTransient<NodePackageJob>();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,25 @@ namespace Learn.VideoAnalysis.API.Expand
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context"></param>
|
/// <param name="context"></param>
|
||||||
/// <exception cref="CustomException"></exception>
|
/// <exception cref="CustomException"></exception>
|
||||||
public void ExecutingFileCached(ActionExecutingContext context)
|
public void ExecutingCached(ActionExecutingContext context)
|
||||||
{
|
{
|
||||||
//特殊处理:ResultIgnore,不进行返回结果包装,原样输出
|
//特殊处理:ResultIgnore,不进行返回结果包装,原样输出
|
||||||
var endpoint = context.HttpContext.GetEndpoint();
|
var endpoint = context.HttpContext.GetEndpoint();
|
||||||
|
var request = context.HttpContext.Request;
|
||||||
|
// 1. 只有非 GET 请求且不是文件上传时才读取 Body
|
||||||
|
if (!request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !request.HasFormContentType)
|
||||||
|
{
|
||||||
|
// 确保从头开始读
|
||||||
|
request.Body.Position = 0;
|
||||||
|
// 使用 leaveOpen: true 防止 StreamReader 关闭底层的 Request.Body
|
||||||
|
using (var reader = new System.IO.StreamReader(request.Body, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true))
|
||||||
|
{
|
||||||
|
var body = reader.ReadToEnd();
|
||||||
|
context.HttpContext.Items["RequestBodyRaw"] = body;
|
||||||
|
request.Body.Position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
// 直接返回原始结果,不封装
|
// 直接返回原始结果,不封装
|
||||||
if (endpoint?.Metadata.GetMetadata<HttpLogEnable>() == null) return;
|
if (endpoint?.Metadata.GetMetadata<HttpLogEnable>() == null) return;
|
||||||
if (context.HttpContext.Request.HasFormContentType &&
|
if (context.HttpContext.Request.HasFormContentType &&
|
||||||
|
|
@ -74,6 +89,8 @@ namespace Learn.VideoAnalysis.API.Expand
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行接口前400 处理
|
/// 执行接口前400 处理
|
||||||
|
|
@ -149,54 +166,32 @@ namespace Learn.VideoAnalysis.API.Expand
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task AddHttpLogAsync(HttpContext context, BaseReturn<object>? result = null, Exception? e = null)
|
public async Task AddHttpLogAsync(HttpContext context, BaseReturn<object>? result = null, Exception? e = null)
|
||||||
{
|
{
|
||||||
//特殊处理:ResultIgnore,不进行返回结果包装,原样输出
|
|
||||||
var endpoint = context.GetEndpoint();
|
|
||||||
// 所有请求都记录
|
|
||||||
//if (endpoint?.Metadata.GetMetadata<HttpLogEnable>() == null&& e is null) return;
|
|
||||||
|
|
||||||
string request = null;
|
string request = null;
|
||||||
var logId = Yitter.IdGenerator.YitIdHelper.NextId();
|
var logId = Yitter.IdGenerator.YitIdHelper.NextId();
|
||||||
if (!context.Request.Method.Equals("GET", StringComparison.InvariantCultureIgnoreCase))
|
if (!context.Request.Method.Equals("GET", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
context.Request.EnableBuffering();
|
var fileArr = context.Items.ContainsKey("FileCached") ? context.Items["FileCached"] as (IFormFile file, MemoryStream stream)[] : null;
|
||||||
//记录请求参数
|
if (context.Request.HasFormContentType && fileArr != null)
|
||||||
if (context.Request.Body.CanSeek)
|
|
||||||
{
|
{
|
||||||
try
|
string uploadsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UploadLogs", logId.ToString());
|
||||||
|
if (!Directory.Exists(uploadsFolder))
|
||||||
|
Directory.CreateDirectory(uploadsFolder);
|
||||||
|
foreach (var fileInfo in fileArr)
|
||||||
{
|
{
|
||||||
var fileArr = context.Items.ContainsKey("FileCached") ? context.Items["FileCached"] as (IFormFile file, MemoryStream stream)[] : null;
|
string uniqueFileName = Guid.NewGuid().ToString().Substring(0, 5) + "_" + Path.GetFileName(fileInfo.file.FileName);
|
||||||
if (context.Request.HasFormContentType && fileArr != null)
|
using var stream = new FileStream(Path.Combine(uploadsFolder, uniqueFileName), FileMode.Create, FileAccess.Write);
|
||||||
{
|
fileInfo.stream.Position = 0;
|
||||||
// 设置保存目录(例如:项目根目录下的Uploads文件夹)
|
await fileInfo.stream.CopyToAsync(stream);
|
||||||
string uploadsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UploadLogs", logId.ToString());
|
fileInfo.stream.Dispose();
|
||||||
// 创建目录(如果不存在)
|
|
||||||
if (!Directory.Exists(uploadsFolder))
|
|
||||||
Directory.CreateDirectory(uploadsFolder);
|
|
||||||
foreach (var fileInfo in fileArr)
|
|
||||||
{
|
|
||||||
// 生成安全文件名(防止路径遍历攻击)
|
|
||||||
string uniqueFileName = Guid.NewGuid().ToString().Substring(0, 5) + "_" + Path.GetFileName(fileInfo.file.FileName);
|
|
||||||
// 保存文件
|
|
||||||
using var stream = new FileStream(Path.Combine(uploadsFolder, uniqueFileName), FileMode.Create, FileAccess.Write);
|
|
||||||
fileInfo.stream.Position = 0;
|
|
||||||
await fileInfo.stream.CopyToAsync(stream);
|
|
||||||
fileInfo.stream.Dispose();
|
|
||||||
}
|
|
||||||
request = $"请求体包含{context.Request.Form.Files.Count()}个文件 目录 {uploadsFolder}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Request.Body.Position = 0;
|
|
||||||
using var sr = new System.IO.StreamReader(context.Request.Body);
|
|
||||||
request = await sr.ReadToEndAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
request = "处理请求入参时发生了错误 \r\n" + ex.ToString() + "\r\n 原有请求数据 " + request;
|
|
||||||
}
|
}
|
||||||
|
request = $"请求体包含{context.Request.Form.Files.Count()}个文件 目录 {uploadsFolder}";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if (context.Items.ContainsKey("RequestBodyRaw"))
|
||||||
|
{
|
||||||
|
request = context.Items["RequestBodyRaw"] as string;
|
||||||
|
}
|
||||||
//写入队列
|
//写入队列
|
||||||
await DbScoped.Sugar.CopyNew()
|
await DbScoped.Sugar.CopyNew()
|
||||||
.Insertable<HttpLog>(new HttpLog
|
.Insertable<HttpLog>(new HttpLog
|
||||||
|
|
@ -205,7 +200,7 @@ namespace Learn.VideoAnalysis.API.Expand
|
||||||
Url = context.Request.Path + context.Request.QueryString,
|
Url = context.Request.Path + context.Request.QueryString,
|
||||||
Method = context.Request.Method,
|
Method = context.Request.Method,
|
||||||
Request = request,
|
Request = request,
|
||||||
IP = $"{context.Connection?.RemoteIpAddress?.ToString()}",
|
IP = GetClientIp(context),
|
||||||
ResponseCode = result?.Code ?? -1,
|
ResponseCode = result?.Code ?? -1,
|
||||||
Response = (result != null ? JsonSerializer.Serialize(result) : null) ,
|
Response = (result != null ? JsonSerializer.Serialize(result) : null) ,
|
||||||
Authorization = context.Request.Headers.ContainsKey("Authorization")
|
Authorization = context.Request.Headers.ContainsKey("Authorization")
|
||||||
|
|
@ -218,21 +213,31 @@ namespace Learn.VideoAnalysis.API.Expand
|
||||||
}).ExecuteCommandAsync();
|
}).ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetClientIp(HttpContext context)
|
||||||
|
{
|
||||||
|
var headers = context.Request.Headers;
|
||||||
|
var ip = headers["X-Forwarded-For"].FirstOrDefault();
|
||||||
|
if (!string.IsNullOrWhiteSpace(ip))
|
||||||
|
{
|
||||||
|
ip = ip.Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim();
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(ip))
|
||||||
|
{
|
||||||
|
ip = headers["X-Real-IP"].FirstOrDefault();
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(ip) && context.Connection.RemoteIpAddress != null)
|
||||||
|
{
|
||||||
|
ip = context.Connection.RemoteIpAddress.ToString();
|
||||||
|
}
|
||||||
|
return ip ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public override async void OnActionExecuting(ActionExecutingContext context)
|
public override async void OnActionExecuting(ActionExecutingContext context)
|
||||||
{
|
{
|
||||||
// 过期的
|
|
||||||
//if (context.HttpContext.GetEndpoint()?
|
|
||||||
// .Metadata.GetMetadata<IAllowAnonymous>() is null)
|
|
||||||
//{
|
|
||||||
// context.Result = new UnauthorizedResult();
|
|
||||||
// return;
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
Executing400(context);
|
Executing400(context);
|
||||||
ExecutingFileCached(context);
|
ExecutingCached(context);
|
||||||
base.OnActionExecuting(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -248,6 +253,9 @@ namespace Learn.VideoAnalysis.API.Expand
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"{DateTime.Now}=>接口规范出现异常");
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
Console.WriteLine(ex.StackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnActionExecuted(context);
|
base.OnActionExecuted(context);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Coravel;
|
using Coravel;
|
||||||
using Coravel.Scheduling.Schedule;
|
using Coravel.Scheduling.Schedule;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -21,15 +21,22 @@ namespace Learn.VideoAnalysis.Expand
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
service.AddTransient<TaskFileClearJob>();
|
service.AddTransient<TaskFileClearJob>();
|
||||||
#endif
|
#endif
|
||||||
|
service.AddTransient<ClearAllCacheJob>();
|
||||||
service.AddTransient<NodePackageJob>();
|
service.AddTransient<NodePackageJob>();
|
||||||
|
// 注册心跳 Job
|
||||||
|
service.AddTransient<DeviceHeartbeatJob>();
|
||||||
}
|
}
|
||||||
public static void UseCoravelExpand(this IApplicationBuilder provider)
|
public static void UseCoravelExpand(this IApplicationBuilder provider)
|
||||||
{
|
{
|
||||||
provider.ApplicationServices.UseScheduler(scheduler =>
|
provider.ApplicationServices.UseScheduler(scheduler =>
|
||||||
{
|
{
|
||||||
//任务缓存清理
|
//任务缓存清理
|
||||||
scheduler.Schedule<TaskFileClearJob>().HourlyAt(10);
|
scheduler.Schedule<ClearAllCacheJob>().HourlyAt(10);
|
||||||
//scheduler.Schedule<TaskFileClearJob>().DailyAt(1,0);
|
//在线心跳 30秒一次
|
||||||
|
scheduler.Schedule<DeviceHeartbeatJob>().EveryThirtySeconds();
|
||||||
|
//强制清理所有缓存内容
|
||||||
|
//scheduler.Schedule<ClearAllCacheJob>().Hourly();
|
||||||
|
//scheduler.Schedule<ClearAllCacheJob>().EverySeconds(40);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,6 @@ namespace Learn.VideoAnalysis
|
||||||
AppCommon.Services = app.Services;
|
AppCommon.Services = app.Services;
|
||||||
app.UseMiddleware<BasicAuthMiddleware>("Swagger");
|
app.UseMiddleware<BasicAuthMiddleware>("Swagger");
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
//开启redis队列服务
|
|
||||||
_ = app.Services.GetRequiredService<RedisInit>();
|
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
app.UseExceptionHandler("/Error");
|
app.UseExceptionHandler("/Error");
|
||||||
|
|
@ -133,7 +131,12 @@ namespace Learn.VideoAnalysis
|
||||||
app.UseCorsExpand();
|
app.UseCorsExpand();
|
||||||
app.UseSqlSugarExpand();
|
app.UseSqlSugarExpand();
|
||||||
app.UseCoravelExpand();
|
app.UseCoravelExpand();
|
||||||
app.UseServiceSystem();
|
app.UseServiceSystem(() =>
|
||||||
|
{
|
||||||
|
//开启redis队列服务
|
||||||
|
_ = AppCommon.Services.GetRequiredService<RedisInit>();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ VITE_PUBLIC_PATH = /
|
||||||
VITE_ROUTER_HISTORY = "hash"
|
VITE_ROUTER_HISTORY = "hash"
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_BASEURL = "http://192.168.2.33:5238"
|
VITE_API_BASEURL = "http://192.168.2.33:7532"
|
||||||
|
|
||||||
|
|
||||||
# # 接口地址
|
# # 接口地址
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,22 @@ export interface ShowTaskInfoRes {
|
||||||
videoKnows: VideoKnowRes[];
|
videoKnows: VideoKnowRes[];
|
||||||
mediaUrl: string;
|
mediaUrl: string;
|
||||||
}
|
}
|
||||||
|
export interface VideoTaskWorkflow {
|
||||||
|
id: number;
|
||||||
|
videoTaskId: number;
|
||||||
|
workflowName: string;
|
||||||
|
currentStep: string;
|
||||||
|
currentStepValue: number;
|
||||||
|
message?: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RowRloadResult {
|
export interface RowRloadResult {
|
||||||
progress: string;
|
progress: string;
|
||||||
lastEnum: string;
|
lastEnum: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
|
workflows: VideoTaskWorkflow[]; // 新增字段
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 刷新任务实时数据 */
|
/** 刷新任务实时数据 */
|
||||||
|
|
@ -42,21 +53,35 @@ export const RowRload = (id: any) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 重试任务 */
|
/** 重试任务 (VideoSliceWorkflow) */
|
||||||
export const ReStart = (id: any, selectEnum: number) => {
|
export const ReStart = (id: any, selectEnum: number) => {
|
||||||
return http.request<any>("get", "/api/VideoTask/ReStart", {
|
return http.request<any>("get", "/api/VideoTask/ReStart", {
|
||||||
params: { id, selectEnum }
|
params: { id, selectEnum }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 重试任务 (TidySlideWorkflow) */
|
||||||
|
export const ReStartTidySlide = (id: any, selectEnum: number) => {
|
||||||
|
return http.request<any>("get", "/api/VideoTask/ReStartTidySlide", {
|
||||||
|
params: { id, selectEnum }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/** 展示数据 */
|
|
||||||
|
/** 展示数据 (VideoSliceWorkflow) */
|
||||||
export const ShowTaskInfo = (id: any) => {
|
export const ShowTaskInfo = (id: any) => {
|
||||||
return http.request<ShowTaskInfoRes>("get", "/api/VideoTask/ShowTaskInfo", {
|
return http.request<ShowTaskInfoRes>("get", "/api/VideoTask/ShowTaskInfo", {
|
||||||
params: { id }
|
params: { id }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 展示数据 (TidySlideWorkflow) */
|
||||||
|
export const ShowTidySlideTaskInfo = (id: any) => {
|
||||||
|
return http.request<any>("get", "/api/VideoTask/ShowTidySlideTaskInfo", {
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** 展示数据 */
|
/** 展示数据 */
|
||||||
|
|
@ -66,6 +91,11 @@ export const RunningTaskList = (data: any) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 获取在线设备列表 */
|
||||||
|
export const GetOnlineDevices = () => {
|
||||||
|
return http.request<string[]>("get", "/api/VideoTask/OnlineDevices");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/** 展示数据 */
|
/** 展示数据 */
|
||||||
export const ErrorTaskList = (data: any) => {
|
export const ErrorTaskList = (data: any) => {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import LayNavMix from "../lay-sidebar/NavMix.vue";
|
||||||
import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vue";
|
import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vue";
|
||||||
import LaySidebarBreadCrumb from "../lay-sidebar/components/SidebarBreadCrumb.vue";
|
import LaySidebarBreadCrumb from "../lay-sidebar/components/SidebarBreadCrumb.vue";
|
||||||
import LaySidebarTopCollapse from "../lay-sidebar/components/SidebarTopCollapse.vue";
|
import LaySidebarTopCollapse from "../lay-sidebar/components/SidebarTopCollapse.vue";
|
||||||
|
import LinkIcon from "~icons/ri/links-fill"; // 引入链接图标
|
||||||
|
|
||||||
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
|
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
|
||||||
import Setting from "~icons/ri/settings-3-line";
|
import Setting from "~icons/ri/settings-3-line";
|
||||||
|
|
@ -21,6 +22,11 @@ const {
|
||||||
avatarsStyle,
|
avatarsStyle,
|
||||||
toggleSideBar,
|
toggleSideBar,
|
||||||
} = useNav();
|
} = useNav();
|
||||||
|
|
||||||
|
function openSwagger() {
|
||||||
|
const swaggerUrl = `${window.location.protocol}//${window.location.hostname}:7532/swagger/index.html`;
|
||||||
|
window.open(swaggerUrl, "_blank");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -40,6 +46,14 @@ const {
|
||||||
<LayNavMix v-if="layout === 'mix'" />
|
<LayNavMix v-if="layout === 'mix'" />
|
||||||
|
|
||||||
<div v-if="layout === 'vertical'" class="vertical-header-right">
|
<div v-if="layout === 'vertical'" class="vertical-header-right">
|
||||||
|
<!-- Swagger 链接 -->
|
||||||
|
<span
|
||||||
|
class="set-icon navbar-bg-hover"
|
||||||
|
title="打开 Swagger 文档"
|
||||||
|
@click="openSwagger"
|
||||||
|
>
|
||||||
|
<IconifyIconOffline :icon="LinkIcon" />
|
||||||
|
</span>
|
||||||
<!-- 菜单搜索 -->
|
<!-- 菜单搜索 -->
|
||||||
<LaySearch id="header-search" />
|
<LaySearch id="header-search" />
|
||||||
<!-- 全屏 -->
|
<!-- 全屏 -->
|
||||||
|
|
|
||||||
|
|
@ -493,19 +493,11 @@ function openMenu(tag, e) {
|
||||||
function tagOnClick(item) {
|
function tagOnClick(item) {
|
||||||
const { name, path } = item;
|
const { name, path } = item;
|
||||||
if (name) {
|
if (name) {
|
||||||
if (item.query) {
|
router.push({
|
||||||
router.push({
|
name,
|
||||||
name,
|
query: item.query,
|
||||||
query: item.query
|
params: item.params
|
||||||
});
|
});
|
||||||
} else if (item.params) {
|
|
||||||
router.push({
|
|
||||||
name,
|
|
||||||
params: item.params
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
router.push({ name });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
router.push({ path });
|
router.push({ path });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,23 @@ export default {
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "Home",
|
name: "Home",
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: "/welcome",
|
redirect: "/welcome/monitor",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ep/home-filled",
|
icon: "ep/home-filled",
|
||||||
title: "首页",
|
title: "首页",
|
||||||
rank: 0
|
rank: 0
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: "/welcome/monitor",
|
||||||
|
name: "monitor",
|
||||||
|
component: () => import("@/views/welcome/monitor.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "设备监控",
|
||||||
|
showLink: true,
|
||||||
|
keepAlive: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/welcome/runningTask",
|
path: "/welcome/runningTask",
|
||||||
name: "runningTask",
|
name: "runningTask",
|
||||||
|
|
@ -32,7 +42,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/welcome/showTask",
|
path: "/welcome/showTask_:id",
|
||||||
name: "showTask",
|
name: "showTask",
|
||||||
component: () => import("@/views/welcome/showTask.vue"),
|
component: () => import("@/views/welcome/showTask.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
|
|
@ -41,6 +51,16 @@ export default {
|
||||||
keepAlive:true,
|
keepAlive:true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/welcome/showTidySlideTask_:id",
|
||||||
|
name: "showTidySlideTask",
|
||||||
|
component: () => import("@/views/welcome/showTidySlideTask.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "TidySlide任务预览",
|
||||||
|
showLink: false,
|
||||||
|
keepAlive: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/welcome/errorTask",
|
path: "/welcome/errorTask",
|
||||||
name: "errorTask",
|
name: "errorTask",
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,19 @@ import { hTableAPI } from "@/api/hTable";
|
||||||
import { getenum } from "@/api/enum";
|
import { getenum } from "@/api/enum";
|
||||||
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
|
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { ReStart, RowRload } from "@/api/videoTask";
|
import { ReStart, ReStartTidySlide, RowRload } from "@/api/videoTask";
|
||||||
import { Refresh } from "@element-plus/icons-vue";
|
import { Refresh } from "@element-plus/icons-vue";
|
||||||
import { message } from "@/utils/message";
|
import { message } from "@/utils/message";
|
||||||
import { json } from "stream/consumers";
|
import { json } from "stream/consumers";
|
||||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||||
|
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
workflowRegistry,
|
||||||
|
getWorkflowConfig,
|
||||||
|
StepData,
|
||||||
|
WorkflowConfig,
|
||||||
|
} from "./workflowConfig";
|
||||||
|
|
||||||
const ControllerName = "VideoTask";
|
const ControllerName = "VideoTask";
|
||||||
|
|
||||||
|
|
@ -103,8 +109,9 @@ const dialogRef = ref({
|
||||||
visible: false,
|
visible: false,
|
||||||
value: null as any,
|
value: null as any,
|
||||||
data: Object as any,
|
data: Object as any,
|
||||||
|
workflowName: "VideoSliceWorkflow", // 新增:记录当前操作的工作流
|
||||||
|
workflowOptions: [] as ComboModel[], // 新增:存储当前工作流对应的步骤选项
|
||||||
});
|
});
|
||||||
let redisChannelEnum = ref<ComboModel[]>([]);
|
|
||||||
const showTable = ref(false);
|
const showTable = ref(false);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
//初始化数据
|
//初始化数据
|
||||||
|
|
@ -112,38 +119,71 @@ onMounted(async () => {
|
||||||
tableData.column.videoType.setting.datasource = await getenum("AttachmentsInfoType");
|
tableData.column.videoType.setting.datasource = await getenum("AttachmentsInfoType");
|
||||||
tableData.column.lastEnum.setting.datasource = await getenum("RedisChannelEnum");
|
tableData.column.lastEnum.setting.datasource = await getenum("RedisChannelEnum");
|
||||||
tableData.column.subject.setting.datasource = await getenum("SubjectEnum");
|
tableData.column.subject.setting.datasource = await getenum("SubjectEnum");
|
||||||
redisChannelEnum.value = tableData.column.lastEnum.setting.datasource;
|
|
||||||
|
// 动态加载所有注册工作流的枚举选项
|
||||||
|
for (const key in workflowRegistry) {
|
||||||
|
const config = workflowRegistry[key];
|
||||||
|
config.enumOptions = await getenum(config.enumName);
|
||||||
|
}
|
||||||
|
|
||||||
showTable.value = true;
|
showTable.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function showDialog(row) {
|
// 修改 showDialog,接收 workflow 参数
|
||||||
|
async function showDialog(row: any, workflowName: string = "VideoSliceWorkflow") {
|
||||||
dialogRef.value.data = row;
|
dialogRef.value.data = row;
|
||||||
dialogRef.value.value = row.lastEnum;
|
dialogRef.value.workflowName = workflowName;
|
||||||
|
|
||||||
|
const config = getWorkflowConfig(workflowName);
|
||||||
|
dialogRef.value.workflowOptions = config.enumOptions || [];
|
||||||
|
|
||||||
|
// 尝试查找该任务在该工作流下的当前状态
|
||||||
|
const wf = row.TaskInfo?.workflows?.find((w) => w.workflowName === workflowName);
|
||||||
|
// 如果找不到对应工作流状态(可能是旧数据),且是默认工作流,尝试使用 LastEnum
|
||||||
|
if (!wf && workflowName === "VideoSliceWorkflow") {
|
||||||
|
dialogRef.value.value = row.lastEnum;
|
||||||
|
} else {
|
||||||
|
dialogRef.value.value = wf ? wf.currentStep : null;
|
||||||
|
}
|
||||||
|
|
||||||
dialogRef.value.visible = true;
|
dialogRef.value.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitRowRload() {
|
async function submitRowRload() {
|
||||||
await ReStart(dialogRef.value.data.id, dialogRef.value.value);
|
const { id } = dialogRef.value.data;
|
||||||
dialogRef.value.visible = false;
|
const { value, workflowName } = dialogRef.value;
|
||||||
message("重试任务", { type: "success" });
|
|
||||||
|
const config = getWorkflowConfig(workflowName);
|
||||||
|
if (config && config.retryApi) {
|
||||||
|
await config.retryApi(id, value);
|
||||||
|
dialogRef.value.visible = false;
|
||||||
|
message(`重试任务 (${workflowName})`, { type: "success" });
|
||||||
|
} else {
|
||||||
|
message(`未找到工作流配置 (${workflowName})`, { type: "error" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function expandChange(row: any, expandedRows: any[]) {
|
async function expandChange(row: any, expandedRows: any[]) {
|
||||||
if (expandedRows.find((s) => s == row)) RloadTaskInfo(row);
|
if (expandedRows.find((s) => s == row)) RloadTaskInfo(row);
|
||||||
}
|
}
|
||||||
function previewTask(row: any) {
|
function previewTask(row: any, workflowName: string) {
|
||||||
let pageName = "showTask";
|
const idStr = row.id.toString();
|
||||||
let queryData = { id: row.id.toString() };
|
let queryData = { id: idStr };
|
||||||
|
const config = getWorkflowConfig(workflowName);
|
||||||
|
const pageName = config.previewRouteName;
|
||||||
|
|
||||||
useMultiTagsStoreHook().handleTags("push", {
|
useMultiTagsStoreHook().handleTags("push", {
|
||||||
path: `/welcome/showTask_` + row.id,
|
path: `/welcome/${pageName}_` + idStr,
|
||||||
name: pageName,
|
name: pageName,
|
||||||
query: queryData,
|
query: queryData,
|
||||||
|
params: { id: idStr },
|
||||||
meta: {
|
meta: {
|
||||||
title: `任务预览` + row.id.toString().slice(-4),
|
title: `${workflowName}预览` + idStr.slice(-4),
|
||||||
dynamicLevel: 3,
|
dynamicLevel: 3,
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// 路由跳转
|
// 路由跳转
|
||||||
route.push({ name: pageName, query: queryData });
|
route.push({ name: pageName, params: { id: idStr }, query: queryData });
|
||||||
}
|
}
|
||||||
function firstLetterToLower(str) {
|
function firstLetterToLower(str) {
|
||||||
if (typeof str !== "string" || !str) return str;
|
if (typeof str !== "string" || !str) return str;
|
||||||
|
|
@ -153,26 +193,27 @@ async function RloadTaskInfo(row: any) {
|
||||||
let res = await RowRload(row.id);
|
let res = await RowRload(row.id);
|
||||||
row.TaskInfo = res;
|
row.TaskInfo = res;
|
||||||
row.TaskInfo.logs = row.TaskInfo.logs.reverse();
|
row.TaskInfo.logs = row.TaskInfo.logs.reverse();
|
||||||
row.TaskInfo.stepData = JSON.parse(JSON.stringify(stepData.value));
|
|
||||||
row.TaskInfo.active = row.TaskInfo.stepData.findIndex(
|
// 兼容旧逻辑:如果 workflows 为空,根据 LastEnum 构造一个默认的 VideoSliceWorkflow 状态
|
||||||
(s) => s.title == row.TaskInfo.lastEnum
|
if (!row.TaskInfo.workflows || row.TaskInfo.workflows.length === 0) {
|
||||||
);
|
row.TaskInfo.workflows = [
|
||||||
if (row.TaskInfo.startTime != null) {
|
{
|
||||||
for (const element of row.TaskInfo.stepData) {
|
id: 0,
|
||||||
element.time = formatDateToChinese(
|
videoTaskId: row.id,
|
||||||
row.TaskInfo.startTime[firstLetterToLower(element.title)]
|
workflowName: "VideoSliceWorkflow",
|
||||||
);
|
currentStep: row.TaskInfo.lastEnum,
|
||||||
let i = row.TaskInfo.stepData.indexOf(element);
|
currentStepValue: 0, // 暂不重要
|
||||||
if (i < row.TaskInfo.active) {
|
updateTime: new Date().toISOString(),
|
||||||
element.status = "success";
|
},
|
||||||
} else if (row.TaskInfo.active == 6 && element.value == 60) {
|
{
|
||||||
element.status = "success";
|
id: 0,
|
||||||
} else if (i == row.TaskInfo.active) {
|
videoTaskId: row.id,
|
||||||
element.status = "process";
|
workflowName: "TidySlideWorkflow",
|
||||||
} else {
|
currentStep: row.TaskInfo.lastEnum,
|
||||||
element.status = "wait";
|
currentStepValue: 0, // 暂不重要
|
||||||
}
|
updateTime: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function formatDateToChinese(dateString) {
|
function formatDateToChinese(dateString) {
|
||||||
|
|
@ -194,15 +235,47 @@ interface StepData {
|
||||||
title: string;
|
title: string;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
const stepData = ref<StepData[]>([
|
// 删除硬编码的步骤定义
|
||||||
{ status: "wait", time: null, title: "下载文件", value: 5 },
|
|
||||||
{ status: "wait", time: null, title: "分离音频", value: 10 },
|
function getWorkflowSteps(workflowName: string) {
|
||||||
{ status: "wait", time: null, title: "解析字幕", value: 20 },
|
const config = getWorkflowConfig(workflowName);
|
||||||
{ status: "wait", time: null, title: "AI课程类型", value: 30 },
|
return config.steps || [];
|
||||||
{ status: "wait", time: null, title: "AI模型分析", value: 40 },
|
}
|
||||||
{ status: "wait", time: null, title: "AI分析试题", value: 50 },
|
|
||||||
{ status: "wait", time: null, title: "结束任务", value: 60 },
|
function getWorkflowStepActive(wf: any) {
|
||||||
]);
|
const steps = getWorkflowSteps(wf.workflowName);
|
||||||
|
return steps.findIndex((s) => s.title === wf.currentStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowStepStatus(wf: any, step: StepData) {
|
||||||
|
const steps = getWorkflowSteps(wf.workflowName);
|
||||||
|
const activeIndex = steps.findIndex((s) => s.title === wf.currentStep);
|
||||||
|
const stepIndex = steps.findIndex((s) => s.title === step.title);
|
||||||
|
|
||||||
|
if (stepIndex < activeIndex) return "success";
|
||||||
|
if (stepIndex === activeIndex) return "process";
|
||||||
|
return "wait";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowLogs(logs: any[], workflowName: string) {
|
||||||
|
if (!logs) return [];
|
||||||
|
// 过滤出属于当前工作流的日志
|
||||||
|
// 1. 如果日志有 workflowName 字段且匹配
|
||||||
|
// 2. 兼容旧数据:如果没有 workflowName,且是 VideoSliceWorkflow,则显示所有无归属日志
|
||||||
|
return logs.filter((log) => {
|
||||||
|
if (log.workflowName) {
|
||||||
|
return log.workflowName === workflowName;
|
||||||
|
}
|
||||||
|
// 旧日志归类到默认工作流
|
||||||
|
if (workflowName === "VideoSliceWorkflow") return true;
|
||||||
|
|
||||||
|
// 兼容重命名: 如果当前是 TidySlideWorkflow, 尝试匹配旧的 UploadWorkflow 日志
|
||||||
|
if (workflowName === "TidySlideWorkflow" && log.workflowName === "UploadWorkflow")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -231,58 +304,111 @@ const stepData = ref<StepData[]>([
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<span>进度</span>
|
<span>任务进度</span>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ props.row.TaskInfo.lastEnum }} {{ props.row.TaskInfo.progress }}
|
{{ props.row.TaskInfo.lastEnum }}
|
||||||
|
<!-- 全局进度展示,如果需要可以保留,或完全移入 Tab 中 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span>操作</span>
|
<span>全局操作</span>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:icon="Refresh"
|
:icon="Refresh"
|
||||||
@click="RloadTaskInfo(props.row)"
|
@click="RloadTaskInfo(props.row)"
|
||||||
circle
|
circle
|
||||||
|
title="刷新任务详情"
|
||||||
/>
|
/>
|
||||||
<el-button type="danger" @click="showDialog(props.row)">重试</el-button>
|
|
||||||
<el-button type="primary" @click="previewTask(props.row)">预览</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid_item_full_width" v-if="props.row.TaskInfo != null">
|
<div class="grid_item_full_width" v-if="props.row.TaskInfo != null">
|
||||||
<span>步骤</span>
|
<el-tabs type="border-card">
|
||||||
<el-steps
|
<el-tab-pane
|
||||||
class="content"
|
v-for="wf in props.row.TaskInfo.workflows"
|
||||||
style="max-width: 100%"
|
:key="wf.id"
|
||||||
:active="props.row.TaskInfo?.active"
|
:label="wf.workflowName"
|
||||||
align-center
|
|
||||||
>
|
|
||||||
<el-step
|
|
||||||
v-for="s in props.row.TaskInfo.stepData"
|
|
||||||
:title="s.title"
|
|
||||||
:description="s.time"
|
|
||||||
:status="s.status"
|
|
||||||
/>
|
|
||||||
</el-steps>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid_item_full_width" v-if="props.row.TaskInfo != null">
|
|
||||||
<span>日志</span>
|
|
||||||
<div class="content">
|
|
||||||
<el-timeline
|
|
||||||
style="max-height: 200px; padding-top: 2px; overflow-y: scroll"
|
|
||||||
>
|
>
|
||||||
<el-timeline-item
|
<div class="flex justify-between items-center mb-4">
|
||||||
v-for="(activity, index) in props.row.TaskInfo.logs"
|
<div class="text-gray-600">
|
||||||
:key="index"
|
<div class="font-bold mb-1">
|
||||||
:timestamp="activity.createTime"
|
当前状态: {{ wf.currentStep }}
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
wf.workflowName === 'VideoSliceWorkflow' &&
|
||||||
|
props.row.TaskInfo.progress
|
||||||
|
"
|
||||||
|
class="ml-2 text-primary"
|
||||||
|
>
|
||||||
|
({{ props.row.TaskInfo.progress }})
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
wf.workflowName === 'TidySlideWorkflow' &&
|
||||||
|
props.row.TaskInfo.tidySlideProgress
|
||||||
|
"
|
||||||
|
class="ml-2 text-blue-600"
|
||||||
|
>
|
||||||
|
({{ props.row.TaskInfo.tidySlideProgress }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
更新时间: {{ formatDateToChinese(wf.updateTime) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="showDialog(props.row, wf.workflowName)"
|
||||||
|
>
|
||||||
|
重试此工作流
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="previewTask(props.row, wf.workflowName)"
|
||||||
|
>
|
||||||
|
预览结果
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-steps
|
||||||
|
class="content"
|
||||||
|
style="max-width: 100%"
|
||||||
|
:active="getWorkflowStepActive(wf)"
|
||||||
|
align-center
|
||||||
>
|
>
|
||||||
{{ activity.message }}
|
<el-step
|
||||||
</el-timeline-item>
|
v-for="s in getWorkflowSteps(wf.workflowName)"
|
||||||
</el-timeline>
|
:key="s.title"
|
||||||
</div>
|
:title="s.title"
|
||||||
|
:status="getWorkflowStepStatus(wf, s)"
|
||||||
|
/>
|
||||||
|
</el-steps>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="font-bold mb-2">工作流日志</div>
|
||||||
|
<el-timeline
|
||||||
|
style="max-height: 300px; padding-top: 2px; overflow-y: scroll"
|
||||||
|
>
|
||||||
|
<el-timeline-item
|
||||||
|
v-for="(log, index) in getWorkflowLogs(
|
||||||
|
props.row.TaskInfo.logs,
|
||||||
|
wf.workflowName
|
||||||
|
)"
|
||||||
|
:key="index"
|
||||||
|
:timestamp="log.createTime"
|
||||||
|
:type="log.message?.includes('异常') ? 'danger' : 'primary'"
|
||||||
|
>
|
||||||
|
{{ log.message }}
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -300,7 +426,7 @@ const stepData = ref<StepData[]>([
|
||||||
style="width: 240px"
|
style="width: 240px"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in redisChannelEnum"
|
v-for="item in dialogRef.workflowOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.text"
|
:label="item.text"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
import { RunningTaskList, GetOnlineDevices } from "@/api/videoTask";
|
||||||
|
import videoTask from "./index.vue";
|
||||||
|
import { SearchConditions, TableConfig } from "@/components/hTable/hTable";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: `monitor`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const devices = ref<string[]>([]);
|
||||||
|
const selectedDevice = ref<string>("all");
|
||||||
|
const videoTaskKey = ref(0);
|
||||||
|
|
||||||
|
const fetchDevices = async () => {
|
||||||
|
const res = await GetOnlineDevices();
|
||||||
|
devices.value = res;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function searchCallback(s: SearchConditions, tv: TableConfig): Promise<boolean> {
|
||||||
|
// Pass deviceId to API
|
||||||
|
const params = { ...s, DeviceId: selectedDevice.value };
|
||||||
|
let res = await RunningTaskList(params);
|
||||||
|
tv.data = res.data.map((s: any, i: number) => {
|
||||||
|
return { ...s, customId: i };
|
||||||
|
});
|
||||||
|
tv.pageData = res;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeviceClick = (device: string) => {
|
||||||
|
if (selectedDevice.value === device) return;
|
||||||
|
selectedDevice.value = device;
|
||||||
|
videoTaskKey.value++; // Force re-render to trigger initial search with new device
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchDevices();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-card class="mb-4">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header flex justify-between items-center">
|
||||||
|
<span>在线设备 ({{ devices.length }})</span>
|
||||||
|
<el-button text @click="fetchDevices">刷新设备</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="device-list">
|
||||||
|
<el-tag
|
||||||
|
class="mx-1 cursor-pointer"
|
||||||
|
:effect="selectedDevice === 'all' ? 'dark' : 'plain'"
|
||||||
|
@click="handleDeviceClick('all')"
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-for="device in devices"
|
||||||
|
:key="device"
|
||||||
|
class="mx-1 cursor-pointer"
|
||||||
|
:effect="selectedDevice === device ? 'dark' : 'plain'"
|
||||||
|
@click="handleDeviceClick(device)"
|
||||||
|
>
|
||||||
|
{{ device }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<videoTask :key="videoTaskKey" :searchCallback="searchCallback"></videoTask>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.device-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<div class="p-4 bg-white rounded-lg shadow-md min-h-[500px]">
|
||||||
|
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-2">
|
||||||
|
TidySlide 任务结果预览 (ID: {{ taskId }})
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex justify-center items-center h-64">
|
||||||
|
<el-icon class="is-loading text-4xl text-blue-500"><Loading /></el-icon>
|
||||||
|
<span class="ml-2 text-gray-500">加载中...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="taskResult" class="space-y-6">
|
||||||
|
<!-- 基本信息卡片 -->
|
||||||
|
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-bold text-gray-600 w-24">任务ID:</span>
|
||||||
|
<span class="text-gray-800">{{ taskResult.videoTaskId }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-bold text-gray-600 w-24">VOD VideoId:</span>
|
||||||
|
<span class="font-mono text-blue-600 bg-blue-50 px-2 py-1 rounded">{{ taskResult.videoId }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-bold text-gray-600 w-24">创建时间:</span>
|
||||||
|
<span class="text-gray-800">{{ formatDate(taskResult.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-bold text-gray-600 w-24">媒体地址:</span>
|
||||||
|
<span v-if="taskResult.mediaUrl" class="text-gray-800 truncate max-w-xs" :title="taskResult.mediaUrl">{{ taskResult.mediaUrl }}</span>
|
||||||
|
<span v-else class="text-gray-400 italic">暂无播放地址</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放器区域 (如果有播放地址) -->
|
||||||
|
<div v-if="taskResult.mediaUrl" class="mt-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-3 text-gray-700">视频预览</h3>
|
||||||
|
<div class="aspect-w-16 aspect-h-9 bg-black rounded-lg overflow-hidden shadow-lg">
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
class="w-full h-full object-contain"
|
||||||
|
:src="taskResult.mediaUrl"
|
||||||
|
>
|
||||||
|
您的浏览器不支持 HTML5 视频。
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="mt-6 p-8 text-center bg-yellow-50 rounded-lg border border-yellow-100">
|
||||||
|
<el-icon class="text-yellow-500 text-3xl mb-2"><Warning /></el-icon>
|
||||||
|
<p class="text-yellow-700">
|
||||||
|
该视频已上传至 VOD,但尚未生成播放地址。<br/>
|
||||||
|
请前往阿里云 VOD 控制台查看 VideoId: <strong>{{ taskResult.videoId }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col justify-center items-center h-64 text-gray-400">
|
||||||
|
<el-icon class="text-5xl mb-4"><DocumentDelete /></el-icon>
|
||||||
|
<span>未找到相关任务结果</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { ShowTidySlideTaskInfo } from "@/api/videoTask";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { isEmpty } from "@pureadmin/utils";
|
||||||
|
import { Loading, Warning, DocumentDelete } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "showTidySlideTask",
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const loading = ref(false);
|
||||||
|
const taskId = ref("");
|
||||||
|
const taskResult = ref<any>(null);
|
||||||
|
|
||||||
|
function formatDate(dateString: string) {
|
||||||
|
if (!dateString) return "-";
|
||||||
|
return new Date(dateString).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const data = isEmpty(route.params) ? route.query : route.params;
|
||||||
|
|
||||||
|
if (isEmpty(data.id) || data.id == null) {
|
||||||
|
message("无效的任务ID", { type: "warning" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskId.value = data.id.toString();
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ShowTidySlideTaskInfo(taskId.value);
|
||||||
|
if (res) {
|
||||||
|
taskResult.value = res;
|
||||||
|
} else {
|
||||||
|
message("未找到任务结果数据", { type: "info" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
message("获取任务结果失败", { type: "error" });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 使用 Tailwind CSS 类,无需额外样式 */
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { ReStart, ReStartTidySlide } from "@/api/videoTask";
|
||||||
|
import { ComboModel } from "@/components/hTable/hTable";
|
||||||
|
|
||||||
|
export interface StepData {
|
||||||
|
status: "" | "wait" | "process" | "finish" | "error" | "success";
|
||||||
|
time: string | null;
|
||||||
|
title: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowConfig {
|
||||||
|
name: string;
|
||||||
|
steps: StepData[];
|
||||||
|
enumName: string; // 后端枚举名称,用于获取下拉列表
|
||||||
|
previewRouteName: string; // 预览页面路由名称
|
||||||
|
retryApi: (id: any, selectEnum: number) => Promise<any>; // 重试 API 函数
|
||||||
|
enumOptions?: ComboModel[]; // 运行时加载的枚举选项
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认视频切片工作流步骤
|
||||||
|
const videoSliceSteps: StepData[] = [
|
||||||
|
{ status: "wait", time: null, title: "下载文件", value: 5 },
|
||||||
|
{ status: "wait", time: null, title: "分离音频", value: 10 },
|
||||||
|
{ status: "wait", time: null, title: "解析字幕", value: 20 },
|
||||||
|
{ status: "wait", time: null, title: "AI课程类型", value: 30 },
|
||||||
|
{ status: "wait", time: null, title: "AI模型分析", value: 40 },
|
||||||
|
{ status: "wait", time: null, title: "AI分析试题", value: 50 },
|
||||||
|
{ status: "wait", time: null, title: "结束任务", value: 60 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 视频合并工作流步骤
|
||||||
|
const tidySlideSteps: StepData[] = [
|
||||||
|
{ status: "wait", time: null, title: "下载文件", value: 10 },
|
||||||
|
{ status: "wait", time: null, title: "合并切片", value: 20 },
|
||||||
|
{ status: "wait", time: null, title: "上传视频", value: 30 },
|
||||||
|
{ status: "wait", time: null, title: "结束任务", value: 40 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const workflowRegistry: Record<string, WorkflowConfig> = {
|
||||||
|
VideoSliceWorkflow: {
|
||||||
|
name: "VideoSliceWorkflow",
|
||||||
|
steps: videoSliceSteps,
|
||||||
|
enumName: "RedisChannelEnum",
|
||||||
|
previewRouteName: "showTask",
|
||||||
|
retryApi: ReStart,
|
||||||
|
},
|
||||||
|
TidySlideWorkflow: {
|
||||||
|
name: "TidySlideWorkflow",
|
||||||
|
steps: tidySlideSteps,
|
||||||
|
enumName: "RedisTidySlideChannelEnum",
|
||||||
|
previewRouteName: "showTidySlideTask",
|
||||||
|
retryApi: ReStartTidySlide,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getWorkflowConfig(workflowName: string): WorkflowConfig {
|
||||||
|
// 默认返回 VideoSliceWorkflow 配置,兼容旧数据
|
||||||
|
return (
|
||||||
|
workflowRegistry[workflowName] || workflowRegistry["VideoSliceWorkflow"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
"ChatGpt": {
|
"ChatGpt": {
|
||||||
//"Host": "https://api.g4f.icu/",
|
//"Host": "https://api.g4f.icu/",
|
||||||
"Host": "https://api.oaibest.com/",
|
"Host": "https://api.oaibest.com/",
|
||||||
"ApiKey": "sk-D15tBln31N7dI9Fi7lds7OySFv5tOEK7DMNsG5rY2E6DCr4s",
|
"ApiKey": "sk-uuCt3AZHawc9B543Yq5bxluO8aW35ArCY5fFnkh2LaJpFYA7",
|
||||||
"Path": "v1/chat/completions"
|
"Path": "v1/chat/completions"
|
||||||
},
|
},
|
||||||
"DeepSeek": {
|
"DeepSeek": {
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
//"ConnectionString": "AllowLoadLocalInfile=true;Server=rm-2vc20nd3d11g0oh6g2o.rwlb.cn-chengdu.rds.aliyuncs.com;User ID=marking;Password=poiuytPOIUYT098765)(*&^%;Port=3306;Database=learn.videoanalysis;CharSet=utf8mb4;pooling=true;SslMode=None",
|
//"ConnectionString": "AllowLoadLocalInfile=true;Server=rm-2vc20nd3d11g0oh6g2o.rwlb.cn-chengdu.rds.aliyuncs.com;User ID=marking;Password=poiuytPOIUYT098765)(*&^%;Port=3306;Database=learn.videoanalysis;CharSet=utf8mb4;pooling=true;SslMode=None",
|
||||||
|
|
||||||
"SqlType": "MySql",
|
"SqlType": "MySql",
|
||||||
"UpdateTable": false
|
"UpdateTable": true
|
||||||
},
|
},
|
||||||
"AlibabaCloudVod": {
|
"AlibabaCloudVod": {
|
||||||
"AccessKeyId": "LTAI5tDC6p9h747B7FHbgwkH",
|
"AccessKeyId": "LTAI5tDC6p9h747B7FHbgwkH",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using FFmpeg.NET.Events;
|
using FFmpeg.NET.Events;
|
||||||
using FFmpeg.NET;
|
using FFmpeg.NET;
|
||||||
using VideoAnalysisCore.AICore.SherpaOnnx;
|
using VideoAnalysisCore.AICore.SherpaOnnx;
|
||||||
using VideoAnalysisCore.Common;
|
using VideoAnalysisCore.Common;
|
||||||
|
|
@ -42,10 +42,10 @@ namespace VideoAnalysisCore.AICore.FFMPGE
|
||||||
? $"/usr/bin/ffmpeg"
|
? $"/usr/bin/ffmpeg"
|
||||||
: Path.Combine(AppCommon.AIModelFile, "ffmpeg.exe");
|
: Path.Combine(AppCommon.AIModelFile, "ffmpeg.exe");
|
||||||
private Repository<VideoTask> videoTaskDB { get; set; }
|
private Repository<VideoTask> videoTaskDB { get; set; }
|
||||||
private RedisManager redisManager { get; set; }
|
private VideoSliceWorkflowManager _workflowManager { get; set; }
|
||||||
public FFMPGEHandle(RedisManager redisManager, Repository<VideoTask> videoTaskDB)
|
public FFMPGEHandle(VideoSliceWorkflowManager workflowManager, Repository<VideoTask> videoTaskDB)
|
||||||
{
|
{
|
||||||
this.redisManager = redisManager;
|
_workflowManager = workflowManager;
|
||||||
this.videoTaskDB = videoTaskDB;
|
this.videoTaskDB = videoTaskDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,16 +69,16 @@ namespace VideoAnalysisCore.AICore.FFMPGE
|
||||||
var filePath = Path.Combine(localPath, "ppt.mp4");
|
var filePath = Path.Combine(localPath, "ppt.mp4");
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
{
|
{
|
||||||
redisManager.AddTaskLog(task,"存在PPT Code但未能找到对应资源文件");
|
_workflowManager.AddTaskLog(task,"存在PPT Code但未能找到对应资源文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var ffmpeg = new Engine(FFmpegPath);
|
var ffmpeg = new Engine(FFmpegPath);
|
||||||
var cToken = new CancellationToken();
|
var cToken = new CancellationToken();
|
||||||
redisManager.SetTaskProgress(task, "Frame=>10%");
|
_workflowManager.SetTaskProgress(task, "Frame=>10%");
|
||||||
|
|
||||||
foreach (string jpgFile in Directory.GetFiles(localPath, "*.jpg"))
|
foreach (string jpgFile in Directory.GetFiles(localPath, "*.jpg"))
|
||||||
File.Delete(jpgFile);
|
File.Delete(jpgFile);
|
||||||
redisManager.SetTaskProgress(task, "Frame=>20%");
|
_workflowManager.SetTaskProgress(task, "Frame=>20%");
|
||||||
|
|
||||||
await ffmpeg.ExecuteAsync($"-i {filePath} -vf \"fps=1/{intervalSec},scale=960:540\" {localPath}/{ExpandFunction.FrameName}%03d.jpg", cToken);
|
await ffmpeg.ExecuteAsync($"-i {filePath} -vf \"fps=1/{intervalSec},scale=960:540\" {localPath}/{ExpandFunction.FrameName}%03d.jpg", cToken);
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ namespace VideoAnalysisCore.AICore.FFMPGE
|
||||||
var frameFiles = Directory.GetFiles(localPath, "*.jpg")
|
var frameFiles = Directory.GetFiles(localPath, "*.jpg")
|
||||||
.OrderBy(f => f)
|
.OrderBy(f => f)
|
||||||
.ToList();
|
.ToList();
|
||||||
redisManager.SetTaskProgress(task, "Frame=>80%");
|
_workflowManager.SetTaskProgress(task, "Frame=>80%");
|
||||||
Image<Rgb24> prevFrame = null;
|
Image<Rgb24> prevFrame = null;
|
||||||
var keyFrames = new List<int>(10) { 5};
|
var keyFrames = new List<int>(10) { 5};
|
||||||
foreach (var frameFile in frameFiles)
|
foreach (var frameFile in frameFiles)
|
||||||
|
|
@ -208,5 +208,55 @@ namespace VideoAnalysisCore.AICore.FFMPGE
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合并音频和视频并切片
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="task"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task MergeAndSliceAsync(string task)
|
||||||
|
{
|
||||||
|
var taskID = long.Parse(task);
|
||||||
|
var localPath = task.LocalPath();
|
||||||
|
var pptPath = Path.Combine(localPath, "ppt.mp4");
|
||||||
|
var taskPath = Path.Combine(localPath, "task.mp4");
|
||||||
|
var mergedPath = Path.Combine(localPath, "merged.mp4");
|
||||||
|
var m3u8Path = Path.Combine(localPath, "out.m3u8");
|
||||||
|
|
||||||
|
if (!File.Exists(pptPath)) throw new FileNotFoundException("PPT视频文件未找到", pptPath);
|
||||||
|
if (!File.Exists(taskPath)) throw new FileNotFoundException("任务视频文件未找到", taskPath);
|
||||||
|
|
||||||
|
var ffmpeg = new Engine(FFmpegPath);
|
||||||
|
var cToken = new CancellationToken();
|
||||||
|
|
||||||
|
// 1. 合并 PPT视频(画面) + 任务视频(音频) -> merged.mp4
|
||||||
|
// -map 0:v 取第一个输入(ppt)的视频流
|
||||||
|
// -map 1:a 取第二个输入(task)的音频流
|
||||||
|
// -c:v copy 复制视频流不转码
|
||||||
|
// -c:a aac 音频转码为aac (兼容性好)
|
||||||
|
// -strict experimental 允许使用aac
|
||||||
|
// -shortest 以最短的流为准
|
||||||
|
var mergeArgs = $"-i \"{pptPath}\" -i \"{taskPath}\" -map 0:v -map 1:a -c:v copy -c:a aac -strict experimental -shortest \"{mergedPath}\" -y";
|
||||||
|
|
||||||
|
await _workflowManager.AddTaskLog(task, "开始合并视频与音频...");
|
||||||
|
await ffmpeg.ExecuteAsync(mergeArgs, cToken);
|
||||||
|
|
||||||
|
if (!File.Exists(mergedPath)) throw new Exception("视频合并失败");
|
||||||
|
|
||||||
|
// 2. 切片 merged.mp4 -> out.m3u8
|
||||||
|
// -c copy 直接复制流 (因为上一步已经是 mp4/aac)
|
||||||
|
// -f hls HLS格式
|
||||||
|
// -hls_time 10 切片时长10秒
|
||||||
|
// -hls_list_size 0 包含所有切片
|
||||||
|
// -hls_segment_filename out%03d.ts 切片文件名
|
||||||
|
var sliceArgs = $"-i \"{mergedPath}\" -c copy -f hls -hls_time 10 -hls_list_size 0 -hls_segment_filename \"{Path.Combine(localPath, "out%03d.ts")}\" \"{m3u8Path}\" -y";
|
||||||
|
|
||||||
|
await _workflowManager.AddTaskLog(task, "开始视频切片...");
|
||||||
|
await ffmpeg.ExecuteAsync(sliceArgs, cToken);
|
||||||
|
|
||||||
|
if (!File.Exists(m3u8Path)) throw new Exception("视频切片失败");
|
||||||
|
|
||||||
|
// 更新任务状态或路径? 目前只需要生成文件
|
||||||
|
await _workflowManager.AddTaskLog(task, "视频处理完成");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
public static void AddGPTService(this IServiceCollection services)
|
public static void AddGPTService(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<DeepSeekGPTClient>();
|
services.AddSingleton<DeepSeekGPTClient>();
|
||||||
|
services.AddSingleton<BSET_DeepSeekGPTClient>();
|
||||||
services.AddSingleton<BestAIClient>();
|
services.AddSingleton<BestAIClient>();
|
||||||
services.AddSingleton<GeminiGPTClient>();
|
services.AddSingleton<GeminiGPTClient>();
|
||||||
services.AddSingleton<IBserGPTWorkflow, GTP_Analysis_1>();
|
services.AddSingleton<IBserGPTWorkflow, GTP_Analysis_1>();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using VideoAnalysisCore.Common;
|
using VideoAnalysisCore.Common;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -26,7 +26,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly RedisManager redisManager;
|
private readonly RedisManager redisManager;
|
||||||
|
|
||||||
public BestAIClient(IHttpClientFactory httpClientFactory, RedisManager redisManager) : base(httpClientFactory, redisManager)
|
public BestAIClient(IHttpClientFactory httpClientFactory, RedisManager redisManager, VideoSliceWorkflowManager workflowManager) : base(httpClientFactory, redisManager, workflowManager)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
this.redisManager = redisManager;
|
this.redisManager = redisManager;
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,13 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
|
|
||||||
public const string Deepseek_Reasoner = "deepseek-reasoner";
|
public const string Deepseek_Reasoner = "deepseek-reasoner";
|
||||||
public const string Deepseek_Chat = "deepseek-chat";
|
public const string Deepseek_Chat = "deepseek-chat";
|
||||||
|
public const string Deepseek_v32 = "deepseek-v3.2";
|
||||||
|
public const string Deepseek_v32_thinking = "deepseek-v3.2-thinking";
|
||||||
|
|
||||||
|
//渠道限制没有并发
|
||||||
public const string Gemini_3_Chat_thinking = "gemini-3-pro-preview-thinking";
|
//public const string Gemini_3_Chat_thinking = "gemini-3-pro-preview-thinking";
|
||||||
public const string Gemini_3_Chat = "gemini-3-pro-preview";
|
public const string Gemini_3_Chat = "gemini-3-pro-preview";
|
||||||
|
public const string Gemini_3_Chat_flash = "gemini-3-flash-preview";
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
using VideoAnalysisCore.Common;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
||||||
|
{
|
||||||
|
|
||||||
|
public class BSET_DeepSeekGPTClient : GPTClient
|
||||||
|
{
|
||||||
|
|
||||||
|
public override GptConfig Config { get; set; } = AppCommon.Config.ChatGpt.ChatGpt;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly RedisManager redisManager;
|
||||||
|
|
||||||
|
public BSET_DeepSeekGPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager, VideoSliceWorkflowManager workflowManager)
|
||||||
|
: base(httpClientFactory, redisManager, workflowManager)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
this.redisManager = redisManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 请求AI
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">返回JSON类型</typeparam>
|
||||||
|
/// <param name="task">任务id</param>
|
||||||
|
/// <param name="postMessages">提示词</param>
|
||||||
|
/// <param name="title">任务类型</param>
|
||||||
|
/// <param name="model">GPT版本</param>
|
||||||
|
/// <param name="max_tokens">最大token <para>不设置默认最大值 16000/8000</para></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public override async Task<T> ChatAsync<T>(string task, string postMessages, string title, string model = ChatGPTType.Deepseek_v32, int max_tokens = 8000)
|
||||||
|
{
|
||||||
|
Message[] messageArr = [
|
||||||
|
new Message(postMessages,"user"),
|
||||||
|
];
|
||||||
|
messageArr = messageArr.Where(s => s != null).ToArray();
|
||||||
|
if (max_tokens > 8000 && (model is null || model == ChatGPTType.Deepseek_v32))
|
||||||
|
max_tokens = 8000;
|
||||||
|
var chatReq = new ChatRequest
|
||||||
|
{
|
||||||
|
taskId = task,
|
||||||
|
title = title,
|
||||||
|
model = model ?? ChatGPTType.Deepseek_v32_thinking,
|
||||||
|
max_tokens = model == ChatGPTType.Deepseek_v32_thinking ? 32000 : max_tokens,
|
||||||
|
stream = true,
|
||||||
|
messages = messageArr
|
||||||
|
};
|
||||||
|
chatReq.modalities = null;
|
||||||
|
return await ChatAsync<T>(chatReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using VideoAnalysisCore.Common;
|
using VideoAnalysisCore.Common;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -21,8 +21,8 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly RedisManager redisManager;
|
private readonly RedisManager redisManager;
|
||||||
|
|
||||||
public DeepSeekGPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager)
|
public DeepSeekGPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager, VideoSliceWorkflowManager workflowManager)
|
||||||
: base(httpClientFactory, redisManager)
|
: base(httpClientFactory, redisManager, workflowManager)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
this.redisManager = redisManager;
|
this.redisManager = redisManager;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using VideoAnalysisCore.Common;
|
using VideoAnalysisCore.Common;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -24,12 +24,14 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly RedisManager redisManager;
|
private readonly RedisManager redisManager;
|
||||||
|
private readonly VideoSliceWorkflowManager _workflowManager;
|
||||||
|
|
||||||
|
|
||||||
public GPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager)
|
public GPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager, VideoSliceWorkflowManager workflowManager)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
this.redisManager = redisManager;
|
this.redisManager = redisManager;
|
||||||
|
_workflowManager = workflowManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -73,16 +75,16 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
var chatResp = await PostJsonStreamAsync(Config.Host + Config.Path, chatReq, Config.ApiKey);
|
var chatResp = await PostJsonStreamAsync(Config.Host + Config.Path, chatReq, Config.ApiKey);
|
||||||
if (!chatResp.IsSuccessStatusCode)
|
if (!chatResp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(chatReq.taskId, "==>请求GPT服务器异常 " + chatResp?.StatusCode + " " + await chatResp.Content.ReadAsStringAsync());
|
await _workflowManager.AddTaskLog(chatReq.taskId, "==>请求GPT服务器异常 " + chatResp?.StatusCode + " " + await chatResp.Content.ReadAsStringAsync());
|
||||||
if (--i < 0)
|
if (--i < 0)
|
||||||
{
|
{
|
||||||
throw new Exception("请求GPT服务器失败次数过多");
|
throw new Exception("请求GPT服务器失败次数过多");
|
||||||
}
|
}
|
||||||
goto PostJsonStream;
|
goto PostJsonStream;
|
||||||
}
|
}
|
||||||
using var stream = chatResp.Content.ReadAsStream();
|
using var stream = await chatResp.Content.ReadAsStreamAsync();
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||||
string line;
|
string? line;
|
||||||
var messageBuilder = new StringBuilder();
|
var messageBuilder = new StringBuilder();
|
||||||
var messageBuilder1 = new StringBuilder();
|
var messageBuilder1 = new StringBuilder();
|
||||||
var lastChat = new ChatResSSE();
|
var lastChat = new ChatResSSE();
|
||||||
|
|
@ -94,13 +96,23 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
//最长分析分析时间1.5小时 或者重试读取 1w次
|
//最长分析分析时间1.5小时 或者重试读取 1w次
|
||||||
while (maxLoop > 0 && DateTime.Now < endTime)
|
while (maxLoop > 0 && DateTime.Now < endTime)
|
||||||
{
|
{
|
||||||
line = reader.ReadLine();
|
try
|
||||||
if (line is null || string.IsNullOrEmpty(line) || line.StartsWith(": keep-alive"))
|
|
||||||
{
|
{
|
||||||
Thread.Sleep(50);
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
|
||||||
|
line = await reader.ReadLineAsync(cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(chatReq.taskId, "==>流式响应超时(3分钟),尝试重新读取...");
|
||||||
maxLoop--;
|
maxLoop--;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (line is null || string.IsNullOrWhiteSpace(line) || line.StartsWith(": keep-alive"))
|
||||||
|
{
|
||||||
|
await Task.Delay(50);
|
||||||
|
maxLoop--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
else if (line.EndsWith("[DONE]"))
|
else if (line.EndsWith("[DONE]"))
|
||||||
{
|
{
|
||||||
// 表示一条消息结束
|
// 表示一条消息结束
|
||||||
|
|
@ -133,17 +145,17 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
{
|
{
|
||||||
var steamCount = messageBuilder.Length + messageBuilder1.Length;
|
var steamCount = messageBuilder.Length + messageBuilder1.Length;
|
||||||
if (++threshold % 30 == 0)
|
if (++threshold % 30 == 0)
|
||||||
redisManager.SetTaskProgress(chatReq.taskId, "steam=>" + steamCount);
|
_workflowManager.SetTaskProgress(chatReq.taskId, "steam=>" + steamCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(chatReq.taskId, "异常 ChatSSE=>" + line + "\r\n" + e.Message + "\r\n" + e.StackTrace);
|
await _workflowManager.AddTaskLog(chatReq.taskId, "异常 ChatSSE=>" + line + "\r\n" + e.Message + "\r\n" + e.StackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Exception(DateTime.Now + "==>AI请求超时 " + chatReq.taskId);
|
throw new Exception(DateTime.Now + "==>AI请求超时 " + chatReq.taskId);
|
||||||
//await redisManager.AddTaskLog(chatReq.taskId, DateTime.Now + "==>AI请求超时 " + chatReq.taskId);
|
//await _workflowManager.AddTaskLog(chatReq.taskId, DateTime.Now + "==>AI请求超时 " + chatReq.taskId);
|
||||||
//return null;
|
//return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,10 +226,10 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(chatRep.taskId, $"==>GPT结果解析错误 重试剩余{tryCount} {ex.Message} {ex.StackTrace}");
|
await _workflowManager.AddTaskLog(chatRep.taskId, $"==>GPT结果解析错误 重试剩余{tryCount} {ex.Message} {ex.StackTrace}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await redisManager.AddTaskLog(chatRep.taskId, $"==>GPT请求失败次数过多!!!");
|
await _workflowManager.AddTaskLog(chatRep.taskId, $"==>GPT请求失败次数过多!!!");
|
||||||
throw new Exception(DateTime.Now + "==>GPT请求失败次数过多!!!");
|
throw new Exception(DateTime.Now + "==>GPT请求失败次数过多!!!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +269,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
{e.StackTrace}
|
{e.StackTrace}
|
||||||
==============================================
|
==============================================
|
||||||
""";
|
""";
|
||||||
await redisManager.AddTaskLog(data.taskId, $"==>GPT Http请求失败 {msg} 1秒后重试");
|
await _workflowManager.AddTaskLog(data.taskId, $"==>GPT Http请求失败 {msg} 1秒后重试");
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using VideoAnalysisCore.Common;
|
using VideoAnalysisCore.Common;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using VideoAnalysisCore.Model;
|
using VideoAnalysisCore.Model;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -37,9 +37,11 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
{
|
{
|
||||||
private readonly GeminiGPTClient geminiClient;
|
private readonly GeminiGPTClient geminiClient;
|
||||||
private readonly DeepSeekGPTClient deepSeekClient;
|
private readonly DeepSeekGPTClient deepSeekClient;
|
||||||
|
private readonly BSET_DeepSeekGPTClient bset_deepSeekClient;
|
||||||
private readonly BestAIClient chatGPTClient;
|
private readonly BestAIClient chatGPTClient;
|
||||||
private readonly Repository<CourseGradingCriteria> criteriaDB;
|
private readonly Repository<CourseGradingCriteria> criteriaDB;
|
||||||
private readonly RedisManager redisManager;
|
private readonly RedisManager redisManager;
|
||||||
|
private readonly VideoSliceWorkflowManager _workflowManager;
|
||||||
private readonly Repository<VideoTask> videoTaskDB;
|
private readonly Repository<VideoTask> videoTaskDB;
|
||||||
private readonly Repository<VideoKonwPoint> videoKonwPointDB;
|
private readonly Repository<VideoKonwPoint> videoKonwPointDB;
|
||||||
private readonly Repository<VideoTaskStage> videoTaskStageDB;
|
private readonly Repository<VideoTaskStage> videoTaskStageDB;
|
||||||
|
|
@ -55,7 +57,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
/// <param name="logger"></param>
|
/// <param name="logger"></param>
|
||||||
public GTP_Analysis_1(DeepSeekGPTClient moonshotClient, Repository<CourseGradingCriteria> criteria, Repository<VideoTask> videoTaskDB,
|
public GTP_Analysis_1(DeepSeekGPTClient moonshotClient, Repository<CourseGradingCriteria> criteria, Repository<VideoTask> videoTaskDB,
|
||||||
Repository<KnowledgeInfo> knowledgeInfoDB, Repository<VideoKonwPoint> videoKonwPointDB, SimpLetexClient simpLetexClient,
|
Repository<KnowledgeInfo> knowledgeInfoDB, Repository<VideoKonwPoint> videoKonwPointDB, SimpLetexClient simpLetexClient,
|
||||||
Repository<VideoQuestion> videoQuestionDB, OssClient ossClient, Repository<VideoQuestionKonw> videoQuestionKonwDB, RedisManager redisManager, BestAIClient chatGPTClient, GeminiGPTClient geminiClient, Repository<VideoTaskStage> videoTaskStageDB)
|
Repository<VideoQuestion> videoQuestionDB, OssClient ossClient, Repository<VideoQuestionKonw> videoQuestionKonwDB, RedisManager redisManager, VideoSliceWorkflowManager workflowManager, BestAIClient chatGPTClient, GeminiGPTClient geminiClient, Repository<VideoTaskStage> videoTaskStageDB, BSET_DeepSeekGPTClient bset_deepSeekClient)
|
||||||
{
|
{
|
||||||
deepSeekClient = moonshotClient;
|
deepSeekClient = moonshotClient;
|
||||||
criteriaDB = criteria;
|
criteriaDB = criteria;
|
||||||
|
|
@ -67,9 +69,11 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
this.ossClient = ossClient;
|
this.ossClient = ossClient;
|
||||||
this.videoQuestionKonwDB = videoQuestionKonwDB;
|
this.videoQuestionKonwDB = videoQuestionKonwDB;
|
||||||
this.redisManager = redisManager;
|
this.redisManager = redisManager;
|
||||||
|
_workflowManager = workflowManager;
|
||||||
this.chatGPTClient = chatGPTClient;
|
this.chatGPTClient = chatGPTClient;
|
||||||
this.geminiClient = geminiClient;
|
this.geminiClient = geminiClient;
|
||||||
this.videoTaskStageDB = videoTaskStageDB;
|
this.videoTaskStageDB = videoTaskStageDB;
|
||||||
|
this.bset_deepSeekClient = bset_deepSeekClient;
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取分段内容对应的章节知识点
|
/// 获取分段内容对应的章节知识点
|
||||||
|
|
@ -108,7 +112,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
知识点列表(Id|Name):{knows}
|
知识点列表(Id|Name):{knows}
|
||||||
输出格式示例:{checkResFormat1}
|
输出格式示例:{checkResFormat1}
|
||||||
""";
|
""";
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, "==>2.开始分析视频内容知识点");
|
await _workflowManager.AddTaskLog(taskInfo.Id, "==>2.开始分析视频内容知识点");
|
||||||
List<VideoKnowRes> konwRes;
|
List<VideoKnowRes> konwRes;
|
||||||
var knowOK = false;
|
var knowOK = false;
|
||||||
var chatClentArr = new GPTClient[] { chatGPTClient, geminiClient, deepSeekClient };
|
var chatClentArr = new GPTClient[] { chatGPTClient, geminiClient, deepSeekClient };
|
||||||
|
|
@ -127,7 +131,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
}
|
}
|
||||||
if (!knowOK)
|
if (!knowOK)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, "GPT未能分析出有效的分段的知识点");
|
await _workflowManager.AddTaskLog(taskInfo.Id, "GPT未能分析出有效的分段的知识点");
|
||||||
throw new Exception("GPT未能分析出有效的分段的知识点");
|
throw new Exception("GPT未能分析出有效的分段的知识点");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,8 +193,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
$"字幕列表 {rCaptionArr}。" +
|
$"字幕列表 {rCaptionArr}。" +
|
||||||
$"输出格式 json字符串 对象格式{fileNameResFormat}";
|
$"输出格式 json字符串 对象格式{fileNameResFormat}";
|
||||||
var task = taskInfo.Id.ToString();
|
var task = taskInfo.Id.ToString();
|
||||||
var fileNameInfoRes = await geminiClient.ChatAsync<FileNameInfo>
|
var fileNameInfoRes = await geminiClient.ChatAsync<FileNameInfo>(task, fileNamePostMessages, "授课章节");
|
||||||
(task, fileNamePostMessages, "授课章节");
|
|
||||||
taskInfo.Sections = fileNameInfoRes.授课章节;
|
taskInfo.Sections = fileNameInfoRes.授课章节;
|
||||||
await videoTaskDB.AsUpdateable()
|
await videoTaskDB.AsUpdateable()
|
||||||
.SetColumns(it => it.Sections == fileNameInfoRes.授课章节)
|
.SetColumns(it => it.Sections == fileNameInfoRes.授课章节)
|
||||||
|
|
@ -298,10 +301,12 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
var newCaptionsList = new List<SenseVoiceRes>(captionsArr.Length);
|
var newCaptionsList = new List<SenseVoiceRes>(captionsArr.Length);
|
||||||
var spanCount = 75;
|
var spanCount = 75;
|
||||||
var totalCount = captionsArr.Length / spanCount + 1;
|
var totalCount = captionsArr.Length / spanCount + 1;
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>字幕优化");
|
||||||
|
|
||||||
Func<string, Task<List<SenseVoiceInput>>>[] chatClentArr =
|
Func<string, Task<List<SenseVoiceInput>>>[] chatClentArr =
|
||||||
[
|
[
|
||||||
|
async (m)=>await bset_deepSeekClient
|
||||||
|
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Deepseek_v32,8_000),
|
||||||
async (m)=>await deepSeekClient
|
async (m)=>await deepSeekClient
|
||||||
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Deepseek_Chat,8_000),
|
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Deepseek_Chat,8_000),
|
||||||
async (m)=>await chatGPTClient
|
async (m)=>await chatGPTClient
|
||||||
|
|
@ -361,12 +366,12 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
if (cArr.Count() - resData.Count() < 5)
|
if (cArr.Count() - resData.Count() < 5)
|
||||||
break;
|
break;
|
||||||
else
|
else
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 重试{i} 剩余{captionsArr.Length - (decimal)newCaptionsList.Count}条字幕");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 重试{i} 剩余{captionsArr.Length - (decimal)newCaptionsList.Count}条字幕");
|
||||||
}
|
}
|
||||||
if (cArr.Count() - resData.Count() > 5)
|
if (cArr.Count() - resData.Count() > 5)
|
||||||
{
|
{
|
||||||
resData = cStrArr.ToList();
|
resData = cStrArr.ToList();
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 采用原始值");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 采用原始值");
|
||||||
}
|
}
|
||||||
newCaptionsList.AddRange(resData.Select((el, i) => new SenseVoiceRes()
|
newCaptionsList.AddRange(resData.Select((el, i) => new SenseVoiceRes()
|
||||||
{
|
{
|
||||||
|
|
@ -425,13 +430,13 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
字幕列表 {captions.Captions} 字幕结束!
|
字幕列表 {captions.Captions} 字幕结束!
|
||||||
""";
|
""";
|
||||||
|
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"开始分析视频内容 {tryCount}");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"开始分析视频内容 {tryCount}");
|
||||||
var res = await geminiClient.ChatAsync<List<VideoKnowRes>>(taskInfo.Id.ToString(), postMessages, "分析字幕", ChatGPTType.Gemini_3_Chat_thinking);
|
var res = await geminiClient.ChatAsync<List<VideoKnowRes>>(taskInfo.Id.ToString(), postMessages, "分析字幕", ChatGPTType.Gemini_3_Chat);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"分析视频内容失败 {tryCount} \r\n{ex.Message}\r\n{ex.StackTrace}");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"分析视频内容失败 {tryCount} \r\n{ex.Message}\r\n{ex.StackTrace}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -569,7 +574,9 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task<SenseVoiceRes[]> AnalysisVideoQuestions(VideoTask taskInfo, List<KnowledgeInfo> knowledgeInfos)
|
private async Task<SenseVoiceRes[]> AnalysisVideoQuestions(VideoTask taskInfo, List<KnowledgeInfo> knowledgeInfos)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>{taskInfo.Id} 提取试题");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>提取试题功能已禁用");
|
||||||
|
return null;
|
||||||
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>{taskInfo.Id} 提取试题");
|
||||||
if (taskInfo is null || string.IsNullOrEmpty(taskInfo.PPTKeyFrame))
|
if (taskInfo is null || string.IsNullOrEmpty(taskInfo.PPTKeyFrame))
|
||||||
return null;
|
return null;
|
||||||
var farmeArr = JsonSerializer.Deserialize<int[]>(taskInfo.PPTKeyFrame);
|
var farmeArr = JsonSerializer.Deserialize<int[]>(taskInfo.PPTKeyFrame);
|
||||||
|
|
@ -610,7 +617,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
break;
|
break;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>{taskInfo.Id} 提取{knowInfoArr.StartTime}秒试题的试题内容");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>{taskInfo.Id} 提取{knowInfoArr.StartTime}秒试题的试题内容");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
//var knowArr=JsonSerializer.Serialize(knowInfoArr.Select(s => new { s.KnowPointId, s.KnowPoint }));
|
//var knowArr=JsonSerializer.Serialize(knowInfoArr.Select(s => new { s.KnowPointId, s.KnowPoint }));
|
||||||
|
|
@ -669,7 +676,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>{taskInfo.Id} 提取{knowInfoArr.StartTime}秒试题出现错误 {ex.Message}");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>{taskInfo.Id} 提取{knowInfoArr.StartTime}秒试题出现错误 {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -700,6 +707,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<TaskRes> GetKnow(string task)
|
public async Task<TaskRes> GetKnow(string task)
|
||||||
{
|
{
|
||||||
|
|
||||||
var taskId = long.Parse(task);
|
var taskId = long.Parse(task);
|
||||||
var taskInfo = await videoTaskDB.AsQueryable()
|
var taskInfo = await videoTaskDB.AsQueryable()
|
||||||
.Where(s => s.Id == taskId)
|
.Where(s => s.Id == taskId)
|
||||||
|
|
@ -746,7 +754,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
var homework = await DetectHomeworkAssignment(taskInfo, captions, sections);
|
var homework = await DetectHomeworkAssignment(taskInfo, captions, sections);
|
||||||
if (homework != null)
|
if (homework != null)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>识别到作业布置 {homework.Content}");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>识别到作业布置 {homework.Content}");
|
||||||
await redisManager.Redis.HMSetAsync(RedisExpandKey.Task(task), "Homework", homework);
|
await redisManager.Redis.HMSetAsync(RedisExpandKey.Task(task), "Homework", homework);
|
||||||
}
|
}
|
||||||
var maxVideoTime = captions?.TimeBase?.LastOrDefault()?.End ?? 0;
|
var maxVideoTime = captions?.TimeBase?.LastOrDefault()?.End ?? 0;
|
||||||
|
|
@ -763,16 +771,16 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
//校验结果质量
|
//校验结果质量
|
||||||
var checkRes = await VerifySpanQuality(questionRes, taskInfo, captions, sections, Course_Id);
|
var checkRes = await VerifySpanQuality(questionRes, taskInfo, captions, sections, Course_Id);
|
||||||
|
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>课堂内容AI分析结果 得分=>{checkRes.Score}");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>课堂内容AI分析结果 得分=>{checkRes.Score}");
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>改进意见 {checkRes.Suggestion}");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>改进意见 {checkRes.Suggestion}");
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>扣分原因 {checkRes.MinusScore}");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>扣分原因 {checkRes.MinusScore}");
|
||||||
// 质量复检
|
// 质量复检
|
||||||
//if (checkRes != null)
|
//if (checkRes != null)
|
||||||
//{
|
//{
|
||||||
// var improved = await ImproveSpanBySuggestion(questionRes, taskInfo, captions, sections, "扣分原因 {checkRes.MinusScore} \n 改进意见 {checkRes.Suggestion}");
|
// var improved = await ImproveSpanBySuggestion(questionRes, taskInfo, captions, sections, "扣分原因 {checkRes.MinusScore} \n 改进意见 {checkRes.Suggestion}");
|
||||||
// var improvedCheck = await VerifySpanQuality(improved, taskInfo, captions, sections, Course_Id);
|
// var improvedCheck = await VerifySpanQuality(improved, taskInfo, captions, sections, Course_Id);
|
||||||
// await redisManager.AddTaskLog(taskInfo.Id, $"==>优化后复检得分=>{improvedCheck.Score}");
|
// await _workflowManager.AddTaskLog(taskInfo.Id, $"==>优化后复检得分=>{improvedCheck.Score}");
|
||||||
// await redisManager.AddTaskLog(taskInfo.Id, $"==>优化后扣分原因 {improvedCheck.MinusScore}");
|
// await _workflowManager.AddTaskLog(taskInfo.Id, $"==>优化后扣分原因 {improvedCheck.MinusScore}");
|
||||||
// if (improved != null)
|
// if (improved != null)
|
||||||
// {
|
// {
|
||||||
// if (improvedCheck != null && improvedCheck.Score >= 90 && improvedCheck.Score > checkRes.Score)
|
// if (improvedCheck != null && improvedCheck.Score >= 90 && improvedCheck.Score > checkRes.Score)
|
||||||
|
|
@ -781,7 +789,7 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
// }
|
// }
|
||||||
// else
|
// else
|
||||||
// {
|
// {
|
||||||
// await redisManager.AddTaskLog(taskInfo.Id, $"==>优化之后的得分降低/得分过低");
|
// await _workflowManager.AddTaskLog(taskInfo.Id, $"==>优化之后的得分降低/得分过低");
|
||||||
// continue;
|
// continue;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
@ -817,10 +825,10 @@ namespace VideoAnalysisCore.AICore.GPT
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>课堂内容AI分析结果不合格!即将重试 剩余次数{tryCount}");
|
await _workflowManager.AddTaskLog(taskInfo.Id, $"==>课堂内容AI分析结果不合格!即将重试 剩余次数{tryCount}");
|
||||||
if (questionRes.Any(s => s.KeepTime < 30))
|
if (questionRes.Any(s => s.KeepTime < 30))
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(taskInfo.Id, "==>视频分段过短!! 重新进行AI分析");
|
await _workflowManager.AddTaskLog(taskInfo.Id, "==>视频分段过短!! 重新进行AI分析");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using VideoAnalysisCore.Common;
|
using VideoAnalysisCore.Common;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -22,7 +22,7 @@ namespace VideoAnalysisCore.AICore.GPT.Gemini
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly RedisManager redisManager;
|
private readonly RedisManager redisManager;
|
||||||
|
|
||||||
public GeminiGPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager) : base(httpClientFactory, redisManager)
|
public GeminiGPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager, VideoSliceWorkflowManager workflowManager) : base(httpClientFactory, redisManager, workflowManager)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
this.redisManager = redisManager;
|
this.redisManager = redisManager;
|
||||||
|
|
@ -46,7 +46,7 @@ namespace VideoAnalysisCore.AICore.GPT.Gemini
|
||||||
Message[] messageArr = [
|
Message[] messageArr = [
|
||||||
new Message(postMessages,"user"),
|
new Message(postMessages,"user"),
|
||||||
];
|
];
|
||||||
model = model ?? ChatGPTType.Gemini_3_Chat;
|
model = model ?? ChatGPTType.Gemini_3_Chat_flash;
|
||||||
messageArr = messageArr.Where(s => s != null).ToArray();
|
messageArr = messageArr.Where(s => s != null).ToArray();
|
||||||
var chatReq = new ChatRequest
|
var chatReq = new ChatRequest
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SherpaOnnx;
|
using SherpaOnnx;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
|
|
@ -49,18 +49,18 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SherpaVad
|
public class SherpaVad
|
||||||
{
|
{
|
||||||
static VadModelConfig VADModelConfig = default!;
|
private VadModelConfig VADModelConfig;
|
||||||
|
|
||||||
private readonly RedisManager redisManager;
|
private readonly VideoSliceWorkflowManager _workflowManager;
|
||||||
private int WindowSize = 512;
|
private int WindowSize = 512;
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly VoiceActivityDetector vad;
|
private readonly VoiceActivityDetector vad;
|
||||||
private Func<int, float[], OfflineStream> Callback;
|
private Func<int, float[], OfflineStream> Callback;
|
||||||
|
|
||||||
|
|
||||||
public SherpaVad(RedisManager redisManager, IServiceProvider serviceProvider)
|
public SherpaVad(VideoSliceWorkflowManager workflowManager, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
this.redisManager = redisManager;
|
_workflowManager = workflowManager;
|
||||||
this.serviceProvider = serviceProvider;
|
this.serviceProvider = serviceProvider;
|
||||||
VADModelConfig = new VadModelConfig();
|
VADModelConfig = new VadModelConfig();
|
||||||
|
|
||||||
|
|
@ -137,6 +137,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
|
||||||
// 使用 Span 操作原始数据
|
// 使用 Span 操作原始数据
|
||||||
ReadOnlySpan<float> allSamples = reader.Samples.AsSpan();
|
ReadOnlySpan<float> allSamples = reader.Samples.AsSpan();
|
||||||
int numSamples = allSamples.Length;
|
int numSamples = allSamples.Length;
|
||||||
|
VADModelConfig.SampleRate = reader.SampleRate;
|
||||||
int sampleRate = VADModelConfig.SampleRate;
|
int sampleRate = VADModelConfig.SampleRate;
|
||||||
int numIter = numSamples / WindowSize;
|
int numIter = numSamples / WindowSize;
|
||||||
var totalSecond = numSamples / (float)sampleRate;
|
var totalSecond = numSamples / (float)sampleRate;
|
||||||
|
|
@ -170,7 +171,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
|
||||||
while (!vad.IsEmpty())
|
while (!vad.IsEmpty())
|
||||||
{
|
{
|
||||||
var p = ReadNext(vad,res, totalSecond);
|
var p = ReadNext(vad,res, totalSecond);
|
||||||
if (p != null) redisManager.SetTaskProgress(task, p + "%");
|
if (p != null) _workflowManager.SetTaskProgress(task, p + "%");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,19 +179,19 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
|
||||||
while (!vad.IsEmpty())
|
while (!vad.IsEmpty())
|
||||||
{
|
{
|
||||||
var p = ReadNext(vad, res, totalSecond);
|
var p = ReadNext(vad, res, totalSecond);
|
||||||
if(p!= null) redisManager.SetTaskProgress(task, p + "%");
|
if(p!= null) _workflowManager.SetTaskProgress(task, p + "%");
|
||||||
}
|
}
|
||||||
//如果携带任务ID
|
//如果携带任务ID
|
||||||
if (!string.IsNullOrEmpty(task))
|
if (!string.IsNullOrEmpty(task))
|
||||||
{
|
{
|
||||||
_ = redisManager.AddTaskLog(task, "==>字幕数量" + res.Count);
|
_ = _workflowManager.AddTaskLog(task, "==>字幕数量" + res.Count);
|
||||||
var captionsStr = res.ToJson();
|
var captionsStr = res.ToJson();
|
||||||
_ = serviceProvider.GetRequiredService<Repository<VideoTask>>()
|
_ = serviceProvider.GetRequiredService<Repository<VideoTask>>()
|
||||||
.AsUpdateable()
|
.AsUpdateable()
|
||||||
.SetColumns(it => it.Captions == captionsStr)
|
.SetColumns(it => it.Captions == captionsStr)
|
||||||
.Where(it => it.Id == long.Parse(task))
|
.Where(it => it.Id == long.Parse(task))
|
||||||
.ExecuteCommandAsync();
|
.ExecuteCommandAsync();
|
||||||
_ = redisManager.Redis.HMSetAsync(RedisExpandKey.Task(task), "Captions", res);
|
_ = _workflowManager.Redis.HMSetAsync(RedisExpandKey.Task(task), "Captions", res);
|
||||||
//分析完成视频字幕后继续接收任务
|
//分析完成视频字幕后继续接收任务
|
||||||
//redisManager.NewTask();
|
//redisManager.NewTask();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using FFmpeg.NET.Services;
|
using FFmpeg.NET.Services;
|
||||||
using FreeRedis;
|
using FreeRedis;
|
||||||
using Microsoft.Extensions.DependencyModel;
|
using Microsoft.Extensions.DependencyModel;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
@ -109,11 +109,11 @@ namespace VideoAnalysisCore.Common
|
||||||
public static string FrameName = "frame_";
|
public static string FrameName = "frame_";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 删除 AI分析任务的缓存文件
|
/// 删除 AI分析任务视频/PPT的缓存文件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="taskId"></param>
|
/// <param name="taskId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async Task<bool> DeleteTaskFileAsync(long? taskId,RedisManager redisManager)
|
public static async Task<bool> DeleteTaskFileAsync(long? taskId, dynamic workflowManager)
|
||||||
{
|
{
|
||||||
if (taskId is null || taskId == 0) return false;
|
if (taskId is null || taskId == 0) return false;
|
||||||
var path = taskId.ToString().LocalPath();
|
var path = taskId.ToString().LocalPath();
|
||||||
|
|
@ -129,21 +129,45 @@ namespace VideoAnalysisCore.Common
|
||||||
{
|
{
|
||||||
File.Delete(filePath);
|
File.Delete(filePath);
|
||||||
|
|
||||||
await redisManager.AddTaskLog(taskId, $"已成功删除文件: {filePath}");
|
await workflowManager.AddTaskLog(taskId, $"已成功删除文件: {filePath}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(taskId, $"删除文件 {filePath} 时出错: {ex.Message}");
|
await workflowManager.AddTaskLog(taskId, $"删除文件 {filePath} 时出错: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//else
|
//else
|
||||||
//{
|
//{
|
||||||
// await redisManager.AddTaskLog(chatReq.taskId, $"文件不存在,跳过删除: {filePath}");
|
// await workflowManager.AddTaskLog(chatReq.taskId, $"文件不存在,跳过删除: {filePath}");
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除 AI分析任务的缓存文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<bool> DeleteTaskAllFileAsync(long? taskId, dynamic workflowManager)
|
||||||
|
{
|
||||||
|
if (taskId is null || taskId == 0) return false;
|
||||||
|
var path = taskId.ToString().LocalPath();
|
||||||
|
if (!string.IsNullOrEmpty(path) && Directory.Exists(path))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(path, true);
|
||||||
|
await workflowManager.AddTaskLog(taskId, $"已清理所有缓存文件: {taskId}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await workflowManager.AddTaskLog(taskId, $"删除缓存文件 {taskId} 时出错: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 对象转化为JSON字符串
|
/// 对象转化为JSON字符串
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using SqlSugar.IOC;
|
using SqlSugar.IOC;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -70,6 +70,21 @@ namespace VideoAnalysisCore.Common
|
||||||
/// 授权配置
|
/// 授权配置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AuthKeyConfig AuthKey { get; set; } = new AuthKeyConfig();
|
public AuthKeyConfig AuthKey { get; set; } = new AuthKeyConfig();
|
||||||
|
/// <summary>
|
||||||
|
/// 工作流配置
|
||||||
|
/// </summary>
|
||||||
|
public WorkflowConfig Workflow { get; set; } = new WorkflowConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkflowConfig
|
||||||
|
{
|
||||||
|
public WorkflowItemConfig Default { get; set; } = new WorkflowItemConfig();
|
||||||
|
public WorkflowItemConfig TidySlide { get; set; } = new WorkflowItemConfig { Enabled = true };
|
||||||
|
}
|
||||||
|
public class WorkflowItemConfig
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public int Concurrency { get; set; } = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AuthKeyConfig
|
public class AuthKeyConfig
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,9 @@ namespace VideoAnalysisCore.Common
|
||||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!context.Request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !context.Request.HasFormContentType)
|
||||||
|
context.Request.EnableBuffering();
|
||||||
await _next(context);
|
await _next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Downloader;
|
using Downloader;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using SqlSugar.IOC;
|
using SqlSugar.IOC;
|
||||||
|
|
@ -14,6 +14,7 @@ using VideoAnalysisCore.Model.Enum;
|
||||||
using AlibabaCloud.SDK.Vod20170321;
|
using AlibabaCloud.SDK.Vod20170321;
|
||||||
using UserCenter.Model.Enum;
|
using UserCenter.Model.Enum;
|
||||||
using System.Security.Policy;
|
using System.Security.Policy;
|
||||||
|
using AlibabaCloud.SDK.Vod20170321.Models;
|
||||||
|
|
||||||
namespace VideoAnalysisCore.Common
|
namespace VideoAnalysisCore.Common
|
||||||
{
|
{
|
||||||
|
|
@ -94,15 +95,30 @@ namespace VideoAnalysisCore.Common
|
||||||
private readonly Repository<NodePackageInfo> packageInfoTaskDB;
|
private readonly Repository<NodePackageInfo> packageInfoTaskDB;
|
||||||
private readonly Client vodClient;
|
private readonly Client vodClient;
|
||||||
private readonly RedisManager redisManager;
|
private readonly RedisManager redisManager;
|
||||||
|
private readonly IServiceProvider serviceProvider;
|
||||||
readonly string taskVideoName = "task.mp4";
|
readonly string taskVideoName = "task.mp4";
|
||||||
readonly string taskPPTVideoName = "ppt.mp4";
|
readonly string taskPPTVideoName = "ppt.mp4";
|
||||||
|
|
||||||
public DownloadFile(Repository<VideoTask> videoTaskDB, Client vodClient, Repository<NodePackageInfo> nackageInfoTaskDB, RedisManager redisManager)
|
// 注入工作流管理器,用于更新进度
|
||||||
|
private readonly VideoSliceWorkflowManager _videoSliceWorkflowManager;
|
||||||
|
private readonly TidySlideWorkflowManager _tidySlideWorkflowManager;
|
||||||
|
|
||||||
|
public DownloadFile(Repository<VideoTask> videoTaskDB, Client vodClient, Repository<NodePackageInfo> nackageInfoTaskDB, RedisManager redisManager, IServiceProvider serviceProvider, VideoSliceWorkflowManager videoSliceWorkflowManager, TidySlideWorkflowManager tidySlideWorkflowManager)
|
||||||
{
|
{
|
||||||
this.videoTaskDB = videoTaskDB;
|
this.videoTaskDB = videoTaskDB;
|
||||||
this.vodClient = vodClient;
|
this.vodClient = vodClient;
|
||||||
this.packageInfoTaskDB = nackageInfoTaskDB;
|
this.packageInfoTaskDB = nackageInfoTaskDB;
|
||||||
this.redisManager = redisManager;
|
this.redisManager = redisManager;
|
||||||
|
this.serviceProvider = serviceProvider;
|
||||||
|
_videoSliceWorkflowManager = videoSliceWorkflowManager;
|
||||||
|
_tidySlideWorkflowManager = tidySlideWorkflowManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:根据工作流名称获取对应的管理器实例
|
||||||
|
private dynamic GetWorkflowManager(string workflowName)
|
||||||
|
{
|
||||||
|
if (workflowName == "TidySlideWorkflow") return _tidySlideWorkflowManager;
|
||||||
|
return _videoSliceWorkflowManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 Content-Type 映射文件后缀
|
// 根据 Content-Type 映射文件后缀
|
||||||
|
|
@ -129,7 +145,7 @@ namespace VideoAnalysisCore.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="task"></param>
|
/// <param name="task"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task RunTask(string task)
|
public async Task RunTask(string task, string workflowName = "VideoSliceWorkflow")
|
||||||
{
|
{
|
||||||
var taskId = long.Parse(task);
|
var taskId = long.Parse(task);
|
||||||
//获取资源文件 地址
|
//获取资源文件 地址
|
||||||
|
|
@ -149,7 +165,7 @@ namespace VideoAnalysisCore.Common
|
||||||
}
|
}
|
||||||
if (string.IsNullOrEmpty(fileUrl))
|
if (string.IsNullOrEmpty(fileUrl))
|
||||||
{
|
{
|
||||||
var videoInfo = await vodClient.GetPlayInfoAsync(new AlibabaCloud.SDK.Vod20170321.Models.GetPlayInfoRequest()
|
var videoInfo = await vodClient.GetPlayInfoAsync(new GetPlayInfoRequest()
|
||||||
{
|
{
|
||||||
VideoId = taskInfo.TagId,
|
VideoId = taskInfo.TagId,
|
||||||
Formats = "mp4",
|
Formats = "mp4",
|
||||||
|
|
@ -198,45 +214,20 @@ namespace VideoAnalysisCore.Common
|
||||||
if (!string.IsNullOrEmpty(taskInfo.PPTVideoUrl))
|
if (!string.IsNullOrEmpty(taskInfo.PPTVideoUrl))
|
||||||
{
|
{
|
||||||
await Download(taskInfo.PPTVideoUrl, localPath, taskPPTVideoName,
|
await Download(taskInfo.PPTVideoUrl, localPath, taskPPTVideoName,
|
||||||
(s, e) => redisManager.SetTaskProgress(task, "PPT->" + Math.Round(e.ProgressPercentage, 1)
|
(s, e) => GetWorkflowManager(workflowName).SetTaskProgress(task, "PPT->" + Math.Round(e.ProgressPercentage, 1))
|
||||||
));
|
);
|
||||||
//try
|
|
||||||
//{
|
|
||||||
// var url = string.Empty;
|
|
||||||
// if (taskInfo.PPTVideoCode.Contains("http"))
|
|
||||||
// url = taskInfo.PPTVideoCode;
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// var videoInfo = await vodClient.GetPlayInfoAsync(new AlibabaCloud.SDK.Vod20170321.Models.GetPlayInfoRequest()
|
|
||||||
// {
|
|
||||||
// VideoId = taskInfo.PPTVideoCode,
|
|
||||||
// Formats = "mp4",
|
|
||||||
// OutputType = "cdn",
|
|
||||||
// AuthTimeout = 3600 * 24 * 12,
|
|
||||||
// });
|
|
||||||
// if (videoInfo is null || videoInfo.StatusCode != 200 && !videoInfo.Body.PlayInfoList.PlayInfo.Any())
|
|
||||||
// throw new Exception($"{DateTime.Now} 视频订阅=>获取阿里云视频信息失败 VideoCode {taskInfo.TagId} StatusCode {videoInfo?.StatusCode}");
|
|
||||||
// url = videoInfo.Body.PlayInfoList.PlayInfo.First().PlayURL;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//catch
|
|
||||||
//{
|
|
||||||
// throw;
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//下载原视频
|
//下载原视频
|
||||||
await Download(fileUrl, localPath, taskVideoName,
|
await Download(fileUrl, localPath, taskVideoName,
|
||||||
(s, e) => redisManager.SetTaskProgress(task, Math.Round(e.ProgressPercentage,1)
|
(s, e) => GetWorkflowManager(workflowName).SetTaskProgress(task, Math.Round(e.ProgressPercentage, 1))
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -267,7 +258,26 @@ namespace VideoAnalysisCore.Common
|
||||||
{
|
{
|
||||||
if (download.Status == DownloadStatus.Failed && e.Error != null)
|
if (download.Status == DownloadStatus.Failed && e.Error != null)
|
||||||
{
|
{
|
||||||
res.SetException(e.Error);
|
// 检查磁盘空间不足异常
|
||||||
|
if (e.Error.Message.Contains("not enough space on the disk", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{DateTime.Now} 下载失败:磁盘空间不足。尝试清理缓存...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var clearJob = scope.ServiceProvider.GetRequiredService<TaskFileClearJob>();
|
||||||
|
await clearJob.Invoke();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"清理缓存失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
res.SetException(new Exception($"磁盘空间不足,下载失败。请手动清理盘符 {Path.GetPathRoot(localPath)}。详细错误:{e.Error.Message}"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res.SetException(e.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (download.Status == DownloadStatus.Completed)
|
else if (download.Status == DownloadStatus.Completed)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Coravel;
|
using Coravel;
|
||||||
using Coravel.Scheduling.Schedule;
|
using Coravel.Scheduling.Schedule;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting.Server;
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
|
|
@ -15,6 +15,7 @@ using VideoAnalysisCore.Job;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using VideoAnalysisCore.Common;
|
using VideoAnalysisCore.Common;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using FreeRedis;
|
||||||
|
|
||||||
namespace VideoAnalysisCore.Common.Expand
|
namespace VideoAnalysisCore.Common.Expand
|
||||||
{
|
{
|
||||||
|
|
@ -24,13 +25,18 @@ namespace VideoAnalysisCore.Common.Expand
|
||||||
/// 系统服务
|
/// 系统服务
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="app1"></param>
|
/// <param name="app1"></param>
|
||||||
public static void UseServiceSystem(this IHost app1)
|
public static void UseServiceSystem(this IHost app1,Action? action=null)
|
||||||
{
|
{
|
||||||
var app = app1.Services;
|
var app = app1.Services;
|
||||||
// 注册启动后的回调
|
// 注册启动后的回调
|
||||||
app.GetRequiredService<IHostApplicationLifetime>()
|
app.GetRequiredService<IHostApplicationLifetime>()
|
||||||
.ApplicationStarted.Register(() =>
|
.ApplicationStarted.Register(() =>
|
||||||
{
|
{
|
||||||
|
// 立即上线一次,防止等待 Job 调度周期的空白
|
||||||
|
var redis = AppCommon.Services.GetService<RedisClient>();
|
||||||
|
// redis?.SAdd(RedisExpandKey.OnlineDevices, AppCommon.Config.ID); // 移除
|
||||||
|
redis?.Set(RedisExpandKey.DeviceHeartbeat(AppCommon.Config.ID.ToString()), DateTime.Now.ToString(), 60);
|
||||||
|
|
||||||
var server = AppCommon.Services.GetRequiredService<IServer>();
|
var server = AppCommon.Services.GetRequiredService<IServer>();
|
||||||
var addressFeature = server.Features.Get<IServerAddressesFeature>();
|
var addressFeature = server.Features.Get<IServerAddressesFeature>();
|
||||||
Console.WriteLine("===========================================");
|
Console.WriteLine("===========================================");
|
||||||
|
|
@ -46,8 +52,22 @@ namespace VideoAnalysisCore.Common.Expand
|
||||||
.Replace("+", "127.0.0.1");
|
.Replace("+", "127.0.0.1");
|
||||||
var uri = new Uri(normalizedAddress);
|
var uri = new Uri(normalizedAddress);
|
||||||
int port = uri.Port; // 这里的 port 就是你要的数字 (int)
|
int port = uri.Port; // 这里的 port 就是你要的数字 (int)
|
||||||
OpenBrowser($"http://localhost:{uri.Port}/ui/index.html");
|
if (OperatingSystem.IsWindows())
|
||||||
|
OpenBrowser($"http://localhost:{uri.Port}/ui/index.html");
|
||||||
|
if(action != null)
|
||||||
|
action();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 注册程序停止时的回调
|
||||||
|
app.GetRequiredService<IHostApplicationLifetime>()
|
||||||
|
.ApplicationStopping.Register(() =>
|
||||||
|
{
|
||||||
|
// 注销设备下线
|
||||||
|
var redis = AppCommon.Services.GetService<RedisClient>();
|
||||||
|
// redis?.SRem(RedisExpandKey.OnlineDevices, AppCommon.Config.ID); // 移除
|
||||||
|
redis?.Del(RedisExpandKey.DeviceHeartbeat(AppCommon.Config.ID.ToString())); // 立即删除心跳Key
|
||||||
|
Console.WriteLine($"设备 {AppCommon.Config.ID} 已下线");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跨平台打开浏览器的方法
|
// 跨平台打开浏览器的方法
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
using AlibabaCloud.SDK.Vod20170321;
|
||||||
|
using AlibabaCloud.SDK.Vod20170321.Models;
|
||||||
|
using Aliyun.OSS;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SqlSugar.IOC;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using VideoAnalysisCore.Model;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Yitter.IdGenerator;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Common.Expand
|
||||||
|
{
|
||||||
|
public static class TidySlideExpand
|
||||||
|
{
|
||||||
|
public static void AddTidySlideExpand(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<TidySlideHandle>();
|
||||||
|
services.AddTransient<Repository<TidySlideTaskResult>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TidySlideHandle
|
||||||
|
{
|
||||||
|
private readonly Client _vodClient;
|
||||||
|
private readonly Repository<VideoTask> _videoTaskDB;
|
||||||
|
private readonly Repository<TidySlideTaskResult> _tidySlideTaskResultDB;
|
||||||
|
private readonly RedisManager _redisManager;
|
||||||
|
private readonly OssClient _ossClient; // 使用系统统一注入的 OSS Client
|
||||||
|
private readonly TidySlideWorkflowManager _workflowManager; // 注入工作流管理器
|
||||||
|
|
||||||
|
public TidySlideHandle(Client vodClient, Repository<VideoTask> videoTaskDB, Repository<TidySlideTaskResult> tidySlideTaskResultDB, RedisManager redisManager, OssClient ossClient, TidySlideWorkflowManager workflowManager)
|
||||||
|
{
|
||||||
|
_vodClient = vodClient;
|
||||||
|
_videoTaskDB = videoTaskDB;
|
||||||
|
_tidySlideTaskResultDB = tidySlideTaskResultDB;
|
||||||
|
_redisManager = redisManager;
|
||||||
|
_ossClient = ossClient;
|
||||||
|
_workflowManager = workflowManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAsync(string task)
|
||||||
|
{
|
||||||
|
var taskId = long.Parse(task);
|
||||||
|
var localPath = task.LocalPath();
|
||||||
|
var m3u8Path = Path.Combine(localPath, "out.m3u8");
|
||||||
|
|
||||||
|
if (!File.Exists(m3u8Path))
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, "未找到 m3u8 文件,无法进行切片上传");
|
||||||
|
throw new FileNotFoundException("M3U8文件未找到", m3u8Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有切片文件 (out*.ts)
|
||||||
|
var tsFiles = Directory.GetFiles(localPath, "out*.ts");
|
||||||
|
if (tsFiles.Length == 0)
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, "未找到 ts 切片文件");
|
||||||
|
throw new FileNotFoundException("TS切片文件未找到");
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = $"Task_{taskId}_{DateTime.Now:yyyyMMddHHmmss}";
|
||||||
|
|
||||||
|
await _workflowManager.AddTaskLog(task, "正在获取VOD上传凭证...");
|
||||||
|
|
||||||
|
// 1. 获取上传凭证和地址
|
||||||
|
// 注意:VOD上传m3u8时,FileName必须以 .m3u8 结尾
|
||||||
|
var request = new CreateUploadVideoRequest
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
FileName = "out.m3u8", // 必须是 m3u8 文件名
|
||||||
|
Description = "视频分析_PPT清洗 ",
|
||||||
|
Tags = "PPT清洗", // 可选:设置标签
|
||||||
|
CateId = 1000709090,
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _vodClient.CreateUploadVideoAsync(request);
|
||||||
|
if (response.Body == null || string.IsNullOrEmpty(response.Body.UploadAddress) || string.IsNullOrEmpty(response.Body.UploadAuth))
|
||||||
|
{
|
||||||
|
throw new Exception($"获取上传凭证失败: RequestId={response.Body?.RequestId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var videoId = response.Body.VideoId;
|
||||||
|
var uploadAddressStr = response.Body.UploadAddress;
|
||||||
|
var uploadAuthStr = response.Body.UploadAuth;
|
||||||
|
|
||||||
|
await _workflowManager.AddTaskLog(task, $"获取凭证成功,VideoId: {videoId}");
|
||||||
|
|
||||||
|
// 2. 解析凭证 (Base64 -> JSON)
|
||||||
|
var addressJson = JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(uploadAddressStr)));
|
||||||
|
var authJson = JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(uploadAuthStr)));
|
||||||
|
|
||||||
|
var endpoint = addressJson["Endpoint"]?.ToString();
|
||||||
|
var bucket = addressJson["Bucket"]?.ToString();
|
||||||
|
// 这是 VOD 分配的 m3u8 存储路径,例如 "sv/243d.../out.m3u8"
|
||||||
|
var objectName = addressJson["FileName"]?.ToString();
|
||||||
|
var accessKeyId = authJson["AccessKeyId"]?.ToString();
|
||||||
|
var accessKeySecret = authJson["AccessKeySecret"]?.ToString();
|
||||||
|
var securityToken = authJson["SecurityToken"]?.ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(endpoint) || string.IsNullOrEmpty(bucket) || string.IsNullOrEmpty(objectName))
|
||||||
|
throw new Exception("解析上传地址失败");
|
||||||
|
|
||||||
|
// 修正 Endpoint 格式 (如果缺少协议头)
|
||||||
|
if (!endpoint.StartsWith("http"))
|
||||||
|
endpoint = "https://" + endpoint;
|
||||||
|
|
||||||
|
// 3. 构造 OSS 客户端 (使用临时凭证)
|
||||||
|
var ossClient = new OssClient(endpoint, accessKeyId, accessKeySecret, securityToken);
|
||||||
|
|
||||||
|
// 4. 确定 OSS 目录前缀
|
||||||
|
// VOD 返回的 objectName 是完整的文件路径,我们需要提取目录部分来存放 .ts 文件
|
||||||
|
// 例如: objectName = "sv/5903240e-19544975a64/out.m3u8"
|
||||||
|
// 则 prefix = "sv/5903240e-19544975a64/"
|
||||||
|
var ossPrefix = objectName.Substring(0, objectName.LastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
await _workflowManager.AddTaskLog(task, $"开始上传文件到 VOD OSS (Bucket: {bucket}, Prefix: {ossPrefix})...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// A. 上传所有 TS 切片
|
||||||
|
await _workflowManager.AddTaskLog(task, $"开始上传 TS 切片 (共 {tsFiles.Length} 个)...");
|
||||||
|
for (int i = 0; i < tsFiles.Length; i++)
|
||||||
|
{
|
||||||
|
var tsFile = tsFiles[i];
|
||||||
|
var fileName = Path.GetFileName(tsFile);
|
||||||
|
var tsObjectKey = ossPrefix + fileName;
|
||||||
|
|
||||||
|
var tsRetryCount = 0;
|
||||||
|
var tsMaxRetries = 10;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var fs = File.OpenRead(tsFile);
|
||||||
|
ossClient.PutObject(bucket, tsObjectKey, fs);
|
||||||
|
break; // Upload successful, break retry loop
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
tsRetryCount++;
|
||||||
|
if (tsRetryCount >= tsMaxRetries)
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, $"上传 TS 切片 {fileName} 失败,已重试 {tsMaxRetries} 次: {ex.Message}");
|
||||||
|
throw; // Re-throw exception to stop the process
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, $"上传 TS 切片 {fileName} 失败 (第 {tsRetryCount} 次重试): {ex.Message},1秒后重试...");
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新上传进度
|
||||||
|
if (i % 5 == 0) // 每5个文件更新一次进度
|
||||||
|
{
|
||||||
|
var progress = Math.Round((double)i / tsFiles.Length * 100, 1);
|
||||||
|
_workflowManager.SetTaskProgress(taskId, $"Upload->{progress}%");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. 上传 m3u8 索引文件
|
||||||
|
// 必须使用 VOD 指定的 objectName
|
||||||
|
await _workflowManager.AddTaskLog(task, "开始上传 m3u8 索引文件...");
|
||||||
|
|
||||||
|
var m3u8RetryCount = 0;
|
||||||
|
var m3u8MaxRetries = 3;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var fs = File.OpenRead(m3u8Path))
|
||||||
|
{
|
||||||
|
ossClient.PutObject(bucket, objectName, fs);
|
||||||
|
}
|
||||||
|
break; // Upload successful
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
m3u8RetryCount++;
|
||||||
|
if (m3u8RetryCount >= m3u8MaxRetries)
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, $"上传 m3u8 文件失败,已重试 {m3u8MaxRetries} 次: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, $"上传 m3u8 文件失败 (第 {m3u8RetryCount} 次重试): {ex.Message},1秒后重试...");
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _workflowManager.AddTaskLog(task, "上传成功");
|
||||||
|
|
||||||
|
// 5. 更新数据库
|
||||||
|
// 对于 VOD 托管视频,我们主要存储 VideoId (TagId),播放地址通常由前端调用 VOD 接口获取
|
||||||
|
// 或者我们可以尝试获取播放地址存入 MediaUrl
|
||||||
|
|
||||||
|
|
||||||
|
await _tidySlideTaskResultDB.InsertAsync(new TidySlideTaskResult()
|
||||||
|
{
|
||||||
|
Id = YitIdHelper.NextId(),
|
||||||
|
VideoTaskId = long.Parse(task),
|
||||||
|
VideoId = videoId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, $"上传 VOD OSS 异常: {ex.Message}");
|
||||||
|
|
||||||
|
// 如果已获取 VideoId 但上传失败,则删除 VOD 记录
|
||||||
|
if (!string.IsNullOrEmpty(videoId))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, $"正在回滚删除 VOD 视频记录 (VideoId: {videoId})...");
|
||||||
|
var deleteRequest = new DeleteVideoRequest { VideoIds = videoId };
|
||||||
|
await _vodClient.DeleteVideoAsync(deleteRequest);
|
||||||
|
await _workflowManager.AddTaskLog(task, "VOD 视频记录删除成功");
|
||||||
|
}
|
||||||
|
catch (Exception deleteEx)
|
||||||
|
{
|
||||||
|
await _workflowManager.AddTaskLog(task, $"回滚删除 VOD 视频记录失败: {deleteEx.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using FreeRedis;
|
using FreeRedis;
|
||||||
using FreeRedis.Internal;
|
using FreeRedis.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
@ -42,6 +42,10 @@ namespace VideoAnalysisCore.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ChannelKey = BaseKey + "TaskChannel";
|
public const string ChannelKey = BaseKey + "TaskChannel";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// TidySlide 工作流 Channel key
|
||||||
|
/// </summary>
|
||||||
|
public const string TidySlideChannelKey = BaseKey + "TidySlideTaskChannel";
|
||||||
|
/// <summary>
|
||||||
/// 下载文件
|
/// 下载文件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string DownloadFile = ChannelKey + "DownloadFile";
|
public const string DownloadFile = ChannelKey + "DownloadFile";
|
||||||
|
|
@ -77,6 +81,14 @@ namespace VideoAnalysisCore.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string Task(object taskId) => BaseKey + "Info:" + taskId;
|
public static string Task(object taskId) => BaseKey + "Info:" + taskId;
|
||||||
public static string IDTask => BaseKey + "Services:" + AppCommon.Config.ID;
|
public static string IDTask => BaseKey + "Services:" + AppCommon.Config.ID;
|
||||||
|
/// <summary>
|
||||||
|
/// 在线设备Key集合 (已弃用,直接扫描 Heartbeat)
|
||||||
|
/// </summary>
|
||||||
|
// public static string OnlineDevices => BaseKey + "OnlineDevices";
|
||||||
|
/// <summary>
|
||||||
|
/// 设备心跳Key前缀 (VideoAnalysis:Heartbeat:{DeviceId})
|
||||||
|
/// </summary>
|
||||||
|
public static string DeviceHeartbeat(string deviceId) => BaseKey + "Heartbeat:" + deviceId;
|
||||||
public static string TaskGPT(object taskId) => BaseKey + "GPTCached:" + taskId;
|
public static string TaskGPT(object taskId) => BaseKey + "GPTCached:" + taskId;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化 redis
|
/// 初始化 redis
|
||||||
|
|
@ -102,72 +114,24 @@ namespace VideoAnalysisCore.Common
|
||||||
JsonSerializer.Deserialize(json, type);
|
JsonSerializer.Deserialize(json, type);
|
||||||
service.AddSingleton(redis);
|
service.AddSingleton(redis);
|
||||||
service.AddSingleton<RedisManager>();
|
service.AddSingleton<RedisManager>();
|
||||||
|
service.AddVideoSliceWorkflow();
|
||||||
|
service.AddTidySlideWorkflow();
|
||||||
|
|
||||||
|
// 注册心跳 Job
|
||||||
|
// service.AddTransient<DeviceHeartbeatJob>(); // 迁移到 CoravelExpand 中统一管理
|
||||||
|
// service.AddTransient<TaskFileClearJob>();
|
||||||
|
// service.AddTransient<ClearAllCacheJob>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RedisInit
|
public class RedisInit
|
||||||
{
|
{
|
||||||
|
|
||||||
public FFMPGEHandle FFMPGE { get; set; }
|
public RedisInit(IServiceProvider serviceProvider)
|
||||||
public SenseVoice senseVoice { get; set; }
|
|
||||||
public FunASRNano funASRNano { get; set; }
|
|
||||||
public RedisManager redisManager { get; set; }
|
|
||||||
|
|
||||||
public RedisInit(FFMPGEHandle fFMPGE, SenseVoice senseVoice, RedisManager redisManager, FunASRNano funASRNano)
|
|
||||||
{
|
{
|
||||||
FFMPGE = fFMPGE;
|
serviceProvider.GetService<VideoSliceWorkflowInit>();
|
||||||
this.senseVoice = senseVoice;
|
serviceProvider.GetService<TidySlideWorkflowInit>();
|
||||||
this.funASRNano = funASRNano;
|
// serviceProvider.GetService<RedisManager>().InitChannel(); // 已废弃,由各工作流自行初始化
|
||||||
this.redisManager = redisManager;
|
|
||||||
Init();
|
|
||||||
redisManager.InitChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Init()
|
|
||||||
{
|
|
||||||
var SubscribeList = RedisManager.SubscribeList;
|
|
||||||
SubscribeList.Add(RedisChannelEnum.排队中, async (task) =>
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
});
|
|
||||||
SubscribeList.Add(RedisChannelEnum.下载文件, async (task) =>
|
|
||||||
{
|
|
||||||
using var scope = AppCommon.Services?.CreateScope();
|
|
||||||
if (scope is null || scope.ServiceProvider.GetService<DownloadFile>() is null)
|
|
||||||
throw new Exception("DownloadFile 未注入");
|
|
||||||
else
|
|
||||||
await scope.ServiceProvider.GetService<DownloadFile>()?.RunTask(task);
|
|
||||||
});
|
|
||||||
SubscribeList.Add(RedisChannelEnum.分离音频, FFMPGE.RunAsync);
|
|
||||||
SubscribeList.Add(RedisChannelEnum.解析字幕, senseVoice.RunTask);
|
|
||||||
//SubscribeList.Add(RedisChannelEnum.解析字幕, funASRNano.RunTask);
|
|
||||||
//SubscribeList.Add(RedisChannelEnum.解析说话人,Speaker.Run);
|
|
||||||
SubscribeList.Add(RedisChannelEnum.AI课程类型, async (task) =>
|
|
||||||
{
|
|
||||||
using var scope = AppCommon.Services?.CreateScope();
|
|
||||||
if (scope is null || scope.ServiceProvider.GetService<IBserGPTWorkflow>() is null)
|
|
||||||
throw new Exception("IBserGPT 未注入");
|
|
||||||
else
|
|
||||||
await scope.ServiceProvider.GetService<IBserGPTWorkflow>()?.GetVideoType(task);
|
|
||||||
});
|
|
||||||
SubscribeList.Add(RedisChannelEnum.AI模型分析, async (task) =>
|
|
||||||
{
|
|
||||||
using var scope = AppCommon.Services?.CreateScope();
|
|
||||||
if (scope is null || scope.ServiceProvider.GetService<IBserGPTWorkflow>() is null)
|
|
||||||
throw new Exception("IBserGPT 未注入");
|
|
||||||
else
|
|
||||||
await scope.ServiceProvider?.GetService<IBserGPTWorkflow>()?.GetKnow(task);
|
|
||||||
});
|
|
||||||
SubscribeList.Add(RedisChannelEnum.AI分析试题, async (task) =>
|
|
||||||
{
|
|
||||||
using var scope = AppCommon.Services?.CreateScope();
|
|
||||||
if (scope is null || scope.ServiceProvider.GetService<IBserGPTWorkflow>() is null)
|
|
||||||
throw new Exception("IBserGPT 未注入");
|
|
||||||
else
|
|
||||||
await scope.ServiceProvider?.GetService<IBserGPTWorkflow>()?.GetVideoQuestion(task);
|
|
||||||
});
|
|
||||||
SubscribeList.Add(RedisChannelEnum.结束任务, redisManager.TaskEnd);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -221,440 +185,22 @@ namespace VideoAnalysisCore.Common
|
||||||
tran.Exec();
|
tran.Exec();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// <summary>
|
|
||||||
/// 添加日志
|
// AddTaskLog 已迁移至 WorkflowBase
|
||||||
/// </summary>
|
|
||||||
/// <param name="taskId">任务id</param>
|
|
||||||
/// <param name="msg">内容</param>
|
|
||||||
public async Task AddTaskLog(object taskId, string msg)
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
Console.WriteLine($"{DateTime.Now.ToString("MM-dd HH:mm:ss")} => {taskId} \r\n{msg}\r\n");
|
|
||||||
#endif
|
|
||||||
await Redis.RPushAsync(RedisExpandKey.TaskLog,
|
|
||||||
new TaskLog()
|
|
||||||
{
|
|
||||||
VideoTaskId = long.Parse(taskId.ToString()),
|
|
||||||
CreateTime = DateTime.Now,
|
|
||||||
Message = msg
|
|
||||||
});
|
|
||||||
var count = 50;
|
|
||||||
lock (RedisExpandKey.TaskLog)
|
|
||||||
{
|
|
||||||
var oldTaskCount = Redis.LLen(RedisExpandKey.TaskLog);
|
|
||||||
if (oldTaskCount > count)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var insertData = Redis.LRange<TaskLog>(RedisExpandKey.TaskLog, 0, count -1);
|
|
||||||
taskLogDB.CopyNew().AsInsertable(insertData).ExecuteCommand();
|
|
||||||
//同步删除redis
|
|
||||||
Redis.LTrim(RedisExpandKey.TaskLog, count, 1000);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine("写入任务日志出错" + "\r\n" + ex.Message + "\r\n" + ex.StackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取任务进度
|
/// 获取任务进度
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="taskId"></param>
|
/// <param name="taskId"></param>
|
||||||
public float GetTaskProgress(object taskId)
|
/// <param name="workflowName">工作流名称(可选)</param>
|
||||||
|
public string GetTaskProgress(object taskId, string workflowName = "VideoSliceWorkflow")
|
||||||
{
|
{
|
||||||
return Redis.HMGet<float>(RedisExpandKey.Task(taskId), "Progress")[0];
|
var fieldName = workflowName == "VideoSliceWorkflow" ? "Progress" : $"Progress:{workflowName}";
|
||||||
}
|
return Redis.HMGet<string>(RedisExpandKey.Task(taskId), fieldName)[0] ?? "";
|
||||||
/// <summary>
|
|
||||||
/// 设置任务进度
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="p">进度百分比</param>
|
|
||||||
/// <param name="taskId"></param>
|
|
||||||
public void SetTaskProgress(object taskId, object p)
|
|
||||||
{
|
|
||||||
Redis.HMSet(RedisExpandKey.Task(taskId), "Progress", p.ToString());
|
|
||||||
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// 将任务 插入 队列
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="enum">枚举</param>
|
|
||||||
/// <param name="taskId">任务id</param>
|
|
||||||
public async Task InsertChannel(RedisChannelEnum @enum, object taskId)
|
|
||||||
{
|
|
||||||
if (taskId is null) throw new Exception("taskId为空");
|
|
||||||
if (Redis is null) throw new Exception("redis未初始化");
|
|
||||||
|
|
||||||
var tId = taskId.ToString();
|
|
||||||
|
|
||||||
await AddTaskLog(tId, "==> 开始执行任务 ");
|
|
||||||
|
|
||||||
await ProcessTaskFlow(@enum, taskId, tId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessTaskFlow(RedisChannelEnum currentStep, object taskId, string tId)
|
// 注意:SetTaskProgress, TaskEnd, SetTaskErrorMessage, ClearTaskError, SetTaskError
|
||||||
{
|
// 已迁移至 WorkflowBase,此处移除或标记为已废弃
|
||||||
try
|
|
||||||
{
|
|
||||||
// 确保有初始校验
|
|
||||||
if (!SubscribeList.ContainsKey(currentStep))
|
|
||||||
throw new Exception($"{currentStep} 未实现");
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (StopTask)
|
|
||||||
{
|
|
||||||
await AddTaskLog(tId, "==> 手动停止任务 ");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 1. 记录步骤开始时间
|
|
||||||
await UpdateStepTimeAsync(taskId, currentStep);
|
|
||||||
// 2. 执行当前步骤业务逻辑
|
|
||||||
await TouchChannel(currentStep, tId, SubscribeList[currentStep]);
|
|
||||||
// 3. 准备下一步
|
|
||||||
var nextStepNullable = currentStep.NextEnum();
|
|
||||||
if (nextStepNullable == null) break; // 流程结束
|
|
||||||
|
|
||||||
var nextStep = nextStepNullable.Value;
|
|
||||||
// 4. 特殊分流:解析字幕完成后,后续步骤转后台并行处理
|
|
||||||
if (currentStep == RedisChannelEnum.解析字幕)
|
|
||||||
{
|
|
||||||
DispatchBackgroundFlow(nextStep, taskId, tId);
|
|
||||||
return; // 释放当前主控线程
|
|
||||||
}
|
|
||||||
// 5. 继续循环
|
|
||||||
currentStep = nextStep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await SetTaskErrorMessage(long.Parse(tId), ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// 每次流程结束(无论是正常结束、异常还是分流退出),都尝试延长过期时间
|
|
||||||
// 注意:如果是分流退出,这里也会执行,保证 key 活跃
|
|
||||||
await Redis.ExpireAsync(RedisExpandKey.Task(taskId), 60 * 60 * 24 * 15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 更新任务步骤时间
|
|
||||||
/// </summary>
|
|
||||||
private async Task UpdateStepTimeAsync(object taskId, RedisChannelEnum step)
|
|
||||||
{
|
|
||||||
// 获取现有时间字典(如果不存在则新建)
|
|
||||||
// 注意:HMGet 返回的是数组,取第一个元素
|
|
||||||
var result = await Redis.HMGetAsync<Dictionary<RedisChannelEnum, DateTime>>(RedisExpandKey.Task(taskId), "StartTime");
|
|
||||||
var startTime = result?.FirstOrDefault() ?? new Dictionary<RedisChannelEnum, DateTime>();
|
|
||||||
|
|
||||||
// 更新时间
|
|
||||||
startTime[step] = DateTime.Now;
|
|
||||||
|
|
||||||
// 写回 Redis
|
|
||||||
await Redis.HMSetAsync(RedisExpandKey.Task(taskId), "StartTime", startTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 分发后续任务到动态线程池
|
|
||||||
/// </summary>
|
|
||||||
private void DispatchBackgroundFlow(RedisChannelEnum startStep, object taskId, string tId)
|
|
||||||
{
|
|
||||||
var bgTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await ProcessTaskFlow(startStep, taskId, tId);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
RunningTasks.TryRemove(tId, out _);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
RunningTasks.TryAdd(tId, bgTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TaskEnd(string task)
|
|
||||||
{
|
|
||||||
var tId = long.Parse(task);
|
|
||||||
//var gptRes = (await Redis
|
|
||||||
// .HMGetAsync<TaskRes>(RedisExpandKey.Task(task), "ChatAnalysis")).FirstOrDefault();
|
|
||||||
//if (gptRes is null)
|
|
||||||
// throw new Exception("未能读取到GPT处理结果");
|
|
||||||
//删除任务执行状态
|
|
||||||
await Redis.LRemAsync(RedisExpandKey.IDTask, 1, task);
|
|
||||||
var taskData = await videoTaskDB
|
|
||||||
.CopyNew()
|
|
||||||
.GetFirstAsync(s => s.Id == tId);
|
|
||||||
if (taskData.Captions == "[]")
|
|
||||||
taskData.Captions = (await Redis.HMGetAsync(RedisExpandKey.Task(task), "Captions")).First();
|
|
||||||
//if (taskData.Speaker == "[]")
|
|
||||||
// taskData.Speaker = (await Redis.HMGetAsync(RedisExpandKey.Task(task), "Speaker"))?.FirstOrDefault()??"[]";
|
|
||||||
|
|
||||||
|
|
||||||
//未使用结果暂时屏蔽
|
|
||||||
//taskData.ChatAnalysis = JsonSerializer.Serialize(gptRes);
|
|
||||||
taskData.ChatAnalysisScore = 0;
|
|
||||||
taskData.ErrorMessage = string.Empty;
|
|
||||||
taskData.LastEnum = RedisChannelEnum.结束任务;
|
|
||||||
taskData.EndTime = DateTime.Now;
|
|
||||||
await videoTaskDB.CopyNew().AsUpdateable(taskData)
|
|
||||||
.UpdateColumns(it => new
|
|
||||||
{
|
|
||||||
//it.ChatAnalysis,
|
|
||||||
it.Captions,
|
|
||||||
it.Speaker,
|
|
||||||
it.ChatAnalysisScore,
|
|
||||||
it.ErrorMessage,
|
|
||||||
it.TotalTokens,
|
|
||||||
it.LastEnum,
|
|
||||||
it.EndTime
|
|
||||||
}).ExecuteCommandAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await ExpandFunction.DeleteTaskFileAsync(tId, this);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
//NewTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化 队列 任务
|
|
||||||
/// </summary>
|
|
||||||
public async Task InitChannel()
|
|
||||||
{
|
|
||||||
if (Redis is null) throw new Exception("redis未初始化");
|
|
||||||
//处理之前程序结束前未能执行完的情况
|
|
||||||
var oldTaskCount = Redis.LLen(RedisExpandKey.IDTask);
|
|
||||||
//重试任务并发过多可能会导致程序崩溃
|
|
||||||
// 未能重新分析的中断任务 则单独开一个网页来处理
|
|
||||||
if (oldTaskCount > 0)
|
|
||||||
{
|
|
||||||
//获取所有未完成的任务
|
|
||||||
var oldTaskArr = Redis.LRange(RedisExpandKey.IDTask, 0, -1);
|
|
||||||
Console.WriteLine($"{DateTime.Now:HH:mm:ss}-------------> 发现 {oldTaskArr.Length} 个未完成任务,准备恢复...");
|
|
||||||
|
|
||||||
//使用信号量限制并发数(5),防止崩溃
|
|
||||||
using var semaphore = new System.Threading.SemaphoreSlim(5);
|
|
||||||
var retryTaskArr = new List<Task>();
|
|
||||||
|
|
||||||
foreach (var oldTask in oldTaskArr)
|
|
||||||
{
|
|
||||||
await semaphore.WaitAsync();
|
|
||||||
var res = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await AddTaskLog(oldTask, DateTime.Now.ToString("HH:mm:ss") + "-------------> 接收上次未完成任务 " + oldTask);
|
|
||||||
await ClearTaskError(long.Parse(oldTask));
|
|
||||||
var lastEnum = (await Redis.HMGetAsync<RedisChannelEnum>(RedisExpandKey.Task(oldTask), "LastEnum")).FirstOrDefault();
|
|
||||||
await InsertChannel(lastEnum, oldTask);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await SetTaskErrorMessage(long.Parse(oldTask), ex);
|
|
||||||
Console.WriteLine($"恢复任务 {oldTask} 失败: {ex.Message}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
semaphore.Release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
retryTaskArr.Add(res);
|
|
||||||
}
|
|
||||||
//等待所有 重试任务完成后接收新任务
|
|
||||||
await Task.WhenAll(retryTaskArr);
|
|
||||||
|
|
||||||
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-------------> 所有未完成任务处理完毕!");
|
|
||||||
ReceivingTaskAsync();
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-------------> 接收新任务!");
|
|
||||||
ReceivingTaskAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// 停止接收新任务
|
|
||||||
/// </summary>
|
|
||||||
public void StopTaskAsync()
|
|
||||||
{
|
|
||||||
StopTask = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_cts?.Cancel();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 开始接收新任务
|
|
||||||
/// </summary>
|
|
||||||
public void RestartTask()
|
|
||||||
{
|
|
||||||
StopTask = false;
|
|
||||||
NewTask();
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// 重新执行新任务
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public void NewTask()
|
|
||||||
{
|
|
||||||
// 取消 消费机的任务订阅
|
|
||||||
if (StopTask)
|
|
||||||
{
|
|
||||||
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-------------> 接收任务已经暂停 ");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ReceivingTaskAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 重新接收新任务
|
|
||||||
/// </summary>
|
|
||||||
public void ReceivingTaskAsync()
|
|
||||||
{
|
|
||||||
if (AppCommon.Config.TaskSetting.IS_Server)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"{DateTime.Now} =>服务端不接收任务");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lock (Redis)
|
|
||||||
{
|
|
||||||
// 如果任务正在运行且未完成,直接返回
|
|
||||||
if (_workerTask != null && !_workerTask.IsCompleted)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_cts = new CancellationTokenSource();
|
|
||||||
var token = _cts.Token;
|
|
||||||
|
|
||||||
_workerTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"{DateTime.Now} => 开始监听任务队列...");
|
|
||||||
while (!token.IsCancellationRequested && !StopTask)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 使用 BLPop 阻塞式获取任务,超时5秒以便检查取消状态
|
|
||||||
var taskId = Redis.BLPop(RedisExpandKey.ChannelKey, 5);
|
|
||||||
if (!string.IsNullOrEmpty(taskId))
|
|
||||||
{
|
|
||||||
Redis.LPush(RedisExpandKey.IDTask, taskId);
|
|
||||||
await AddTaskLog(taskId, "-------------> 接收到任务 ");
|
|
||||||
// await等待任务处理完成,确保串行执行
|
|
||||||
await InsertChannel(RedisChannelEnum.下载文件, taskId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"任务监听异常: {ex.Message}");
|
|
||||||
await Task.Delay(2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Console.WriteLine($"{DateTime.Now} => 停止监听任务队列.");
|
|
||||||
}, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 写入任务异常
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="taskID"></param>
|
|
||||||
/// <param name="ex"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> SetTaskErrorMessage(long taskID, Exception? ex)
|
|
||||||
{
|
|
||||||
var error = string.Empty;
|
|
||||||
if (ex != null)
|
|
||||||
{
|
|
||||||
|
|
||||||
await Redis.LRemAsync(RedisExpandKey.IDTask, 1, taskID.ToString());
|
|
||||||
//执行任务时出现异常
|
|
||||||
error = ex.Message + ex.StackTrace;
|
|
||||||
await AddTaskLog(taskID, $""" 出现异常 {ex.Message} {ex.StackTrace} """);
|
|
||||||
//清除失败任务 重新接收任务
|
|
||||||
NewTask();
|
|
||||||
}
|
|
||||||
return await SetTaskError(taskID, error);
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// 清除 任务的错误信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="taskID"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> ClearTaskError(long taskID) => await SetTaskError(taskID, string.Empty);
|
|
||||||
/// <summary>
|
|
||||||
/// 修改任务的错误信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="taskID"></param>
|
|
||||||
/// <param name="error"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> SetTaskError(long taskID, string? error)
|
|
||||||
{
|
|
||||||
var vDB = AppCommon.Services.GetService<Repository<VideoTask>>();
|
|
||||||
Redis.HMSet(RedisExpandKey.Task(taskID), "ErrorMessage", error);
|
|
||||||
return await vDB.CopyNew().AsUpdateable()
|
|
||||||
.SetColumns(it => it.ErrorMessage == error)//SetColumns是可以叠加的 写2个就2个字段赋值
|
|
||||||
.Where(it => it.Id == taskID)
|
|
||||||
.ExecuteCommandAsync() == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 触发
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key"></param>
|
|
||||||
/// <param name="taskId"></param>
|
|
||||||
/// <param name="action"></param>
|
|
||||||
public async Task TouchChannel(RedisChannelEnum key, string taskId, Func<string, Task> action = null)
|
|
||||||
{
|
|
||||||
if (taskId is null) return;
|
|
||||||
var tID = long.Parse(taskId);
|
|
||||||
if (action is not null)
|
|
||||||
{
|
|
||||||
var tryCount = 1;
|
|
||||||
for (int i = 0; i < tryCount; i++)
|
|
||||||
{
|
|
||||||
await AddTaskLog(taskId, " 开始执行 " + key + " " + taskId);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Redis.HMSet(RedisExpandKey.Task(taskId), "LastEnum", key);
|
|
||||||
Redis.HMSet(RedisExpandKey.Task(taskId), "Progress", 0);
|
|
||||||
var vDB = AppCommon.Services.GetService<Repository<VideoTask>>();
|
|
||||||
await vDB.CopyNew().AsUpdateable()
|
|
||||||
.SetColumns(it => it.LastEnum == key)
|
|
||||||
.Where(it => it.Id == tID)
|
|
||||||
.ExecuteCommandAsync();
|
|
||||||
await action(taskId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
|
|
||||||
await AddTaskLog(taskId, $""" 出现异常 {ex.Message} {ex.StackTrace} """);
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
await AddTaskLog(taskId, "稍后后重试." + key + " " + taskId);
|
|
||||||
if (i + 1 == tryCount)
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await AddTaskLog(taskId, "任务函数 未实现." + key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
using FreeRedis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SqlSugar.IOC;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using VideoAnalysisCore.AICore.FFMPGE;
|
||||||
|
using VideoAnalysisCore.Common.Expand;
|
||||||
|
using VideoAnalysisCore.Model;
|
||||||
|
using VideoAnalysisCore.Model.Enum;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Common
|
||||||
|
{
|
||||||
|
public static class TidySlideWorkflowExpand
|
||||||
|
{
|
||||||
|
public static void AddTidySlideWorkflow(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// 只有在配置启用时才注册
|
||||||
|
if (AppCommon.Config.Workflow.TidySlide.Enabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{DateTime.Now}=>初始化 视频合并工作流(TidySlide)");
|
||||||
|
services.AddTidySlideExpand(); // Register TidySlideHandle
|
||||||
|
services.AddSingleton<TidySlideWorkflowManager>();
|
||||||
|
services.AddSingleton<TidySlideWorkflowInit>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TidySlideWorkflowInit
|
||||||
|
{
|
||||||
|
private readonly TidySlideWorkflowManager _manager;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public TidySlideWorkflowInit(TidySlideWorkflowManager manager, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_manager = manager;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
Init();
|
||||||
|
_manager.InitChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init()
|
||||||
|
{
|
||||||
|
var SubscribeList = _manager.SubscribeList;
|
||||||
|
SubscribeList.Add(RedisTidySlideChannelEnum.排队中, async (task) => await Task.CompletedTask);
|
||||||
|
SubscribeList.Add(RedisTidySlideChannelEnum.下载文件, async (task) =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var downloadService = scope.ServiceProvider.GetRequiredService<DownloadFile>();
|
||||||
|
await downloadService.RunTask(task, "TidySlideWorkflow");
|
||||||
|
});
|
||||||
|
SubscribeList.Add(RedisTidySlideChannelEnum.合并切片, async (task) =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var ffmpegService = scope.ServiceProvider.GetRequiredService<FFMPGEHandle>();
|
||||||
|
await ffmpegService.MergeAndSliceAsync(task);
|
||||||
|
});
|
||||||
|
SubscribeList.Add(RedisTidySlideChannelEnum.上传视频, async (task) =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var uploadService = scope.ServiceProvider.GetRequiredService<TidySlideHandle>();
|
||||||
|
await uploadService.RunAsync(task);
|
||||||
|
});
|
||||||
|
SubscribeList.Add(RedisTidySlideChannelEnum.结束任务, _manager.TaskEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TidySlideWorkflowManager : WorkflowBase<RedisTidySlideChannelEnum>
|
||||||
|
{
|
||||||
|
public TidySlideWorkflowManager(RedisClient redis, RedisManager redisManager) : base(redis, redisManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string ChannelKey => RedisExpandKey.TidySlideChannelKey;
|
||||||
|
protected override int Concurrency => AppCommon.Config.Workflow.TidySlide.Concurrency;
|
||||||
|
protected override string WorkflowName => "TidySlideWorkflow"; // 显式指定
|
||||||
|
|
||||||
|
protected override async Task UpdateTaskStateAsync(string taskId, RedisTidySlideChannelEnum step)
|
||||||
|
{
|
||||||
|
// TidySlide 工作流只更新 VideoTaskWorkflow 表,不污染 VideoTask.LastEnum
|
||||||
|
// 调用基类实现即可
|
||||||
|
await base.UpdateTaskStateAsync(taskId, step);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task TaskEnd(string task)
|
||||||
|
{
|
||||||
|
var tId = long.Parse(task);
|
||||||
|
await base.TaskEnd(task);
|
||||||
|
|
||||||
|
// TidySlide 工作流结束时清理文件
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ExpandFunction.DeleteTaskAllFileAsync(tId, this);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
using FreeRedis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SqlSugar.IOC;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using VideoAnalysisCore.AICore.FFMPGE;
|
||||||
|
using VideoAnalysisCore.AICore.GPT;
|
||||||
|
using VideoAnalysisCore.AICore.SherpaOnnx;
|
||||||
|
using VideoAnalysisCore.AICore.Whisper;
|
||||||
|
using VideoAnalysisCore.Common.Expand;
|
||||||
|
using VideoAnalysisCore.Model;
|
||||||
|
using VideoAnalysisCore.Model.Enum;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AI视频切片工作流
|
||||||
|
/// </summary>
|
||||||
|
public static class VideoSliceWorkflowExpand
|
||||||
|
{
|
||||||
|
public static void AddVideoSliceWorkflow(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (AppCommon.Config.Workflow.Default.Enabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{DateTime.Now}=>初始化 AI切片工作流");
|
||||||
|
services.AddSingleton<VideoSliceWorkflowManager>();
|
||||||
|
services.AddSingleton<VideoSliceWorkflowInit>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoSliceWorkflowInit
|
||||||
|
{
|
||||||
|
private readonly VideoSliceWorkflowManager _manager;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly FFMPGEHandle _ffmpeg;
|
||||||
|
private readonly SenseVoice _senseVoice;
|
||||||
|
|
||||||
|
public VideoSliceWorkflowInit(VideoSliceWorkflowManager manager, IServiceProvider serviceProvider, FFMPGEHandle ffmpeg, SenseVoice senseVoice)
|
||||||
|
{
|
||||||
|
_manager = manager;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_ffmpeg = ffmpeg;
|
||||||
|
_senseVoice = senseVoice;
|
||||||
|
Init();
|
||||||
|
_manager.InitChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init()
|
||||||
|
{
|
||||||
|
var SubscribeList = _manager.SubscribeList;
|
||||||
|
SubscribeList.Add(RedisChannelEnum.排队中, async (task) => await Task.CompletedTask);
|
||||||
|
SubscribeList.Add(RedisChannelEnum.下载文件, async (task) =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var downloadService = scope.ServiceProvider.GetService<DownloadFile>();
|
||||||
|
if (downloadService is null) throw new Exception("DownloadFile 未注入");
|
||||||
|
await downloadService.RunTask(task, "VideoSliceWorkflow");
|
||||||
|
});
|
||||||
|
SubscribeList.Add(RedisChannelEnum.分离音频, _ffmpeg.RunAsync);
|
||||||
|
SubscribeList.Add(RedisChannelEnum.解析字幕, _senseVoice.RunTask);
|
||||||
|
|
||||||
|
SubscribeList.Add(RedisChannelEnum.AI课程类型, async (task) =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var service = scope.ServiceProvider.GetService<IBserGPTWorkflow>();
|
||||||
|
if (service is null) throw new Exception("IBserGPT 未注入");
|
||||||
|
await service.GetVideoType(task);
|
||||||
|
});
|
||||||
|
SubscribeList.Add(RedisChannelEnum.AI模型分析, async (task) =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var service = scope.ServiceProvider.GetService<IBserGPTWorkflow>();
|
||||||
|
if (service is null) throw new Exception("IBserGPT 未注入");
|
||||||
|
await service.GetKnow(task);
|
||||||
|
});
|
||||||
|
SubscribeList.Add(RedisChannelEnum.AI分析试题, async (task) =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var service = scope.ServiceProvider.GetService<IBserGPTWorkflow>();
|
||||||
|
if (service is null) throw new Exception("IBserGPT 未注入");
|
||||||
|
await service.GetVideoQuestion(task);
|
||||||
|
});
|
||||||
|
SubscribeList.Add(RedisChannelEnum.结束任务, _manager.TaskEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoSliceWorkflowManager : WorkflowBase<RedisChannelEnum>
|
||||||
|
{
|
||||||
|
public VideoSliceWorkflowManager(RedisClient redis, RedisManager redisManager) : base(redis, redisManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string ChannelKey => RedisExpandKey.ChannelKey;
|
||||||
|
protected override int Concurrency => AppCommon.Config.Workflow.Default.Concurrency;
|
||||||
|
protected override string WorkflowName => "VideoSliceWorkflow"; // 显式指定,避免重构改名风险
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重写状态更新逻辑:保持兼容性,继续更新 VideoTask.LastEnum
|
||||||
|
/// </summary>
|
||||||
|
protected override async Task UpdateTaskStateAsync(string taskId, RedisChannelEnum step)
|
||||||
|
{
|
||||||
|
var tID = long.Parse(taskId);
|
||||||
|
// 1. 调用基类方法,更新 VideoTaskWorkflow 表 (可选,如果想双写)
|
||||||
|
await base.UpdateTaskStateAsync(taskId, step);
|
||||||
|
|
||||||
|
// 2. 更新旧的 VideoTask 表,保持前端兼容
|
||||||
|
using var scope = AppCommon.Services.CreateScope();
|
||||||
|
var vDB = scope.ServiceProvider.GetService<Repository<VideoTask>>();
|
||||||
|
if (vDB != null)
|
||||||
|
{
|
||||||
|
await vDB.CopyNew().AsUpdateable()
|
||||||
|
.SetColumns(it => it.LastEnum == step)
|
||||||
|
.Where(it => it.Id == tID)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task HandleSpecialFlowAsync(RedisChannelEnum currentStep, RedisChannelEnum nextStep, string taskId)
|
||||||
|
{
|
||||||
|
// 4. 特殊分流:解析字幕完成后,后续步骤转后台并行处理
|
||||||
|
if (currentStep == RedisChannelEnum.解析字幕)
|
||||||
|
{
|
||||||
|
await DispatchBackgroundFlow(nextStep, taskId, taskId);
|
||||||
|
throw new WorkflowFlowSwitchException(); // 抛出异常以中断当前流程(基类捕获)
|
||||||
|
}
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task TaskEnd(string task)
|
||||||
|
{
|
||||||
|
var tId = long.Parse(task);
|
||||||
|
await base.TaskEnd(task);
|
||||||
|
|
||||||
|
// 原 RedisManager.TaskEnd 逻辑迁移至此
|
||||||
|
using var scope = AppCommon.Services.CreateScope();
|
||||||
|
var videoTaskDB = scope.ServiceProvider.GetService<Repository<VideoTask>>();
|
||||||
|
|
||||||
|
if (videoTaskDB == null) return;
|
||||||
|
|
||||||
|
var taskData = await videoTaskDB.CopyNew().GetFirstAsync(s => s.Id == tId);
|
||||||
|
|
||||||
|
if (taskData.Captions == "[]")
|
||||||
|
taskData.Captions = (await Redis.HMGetAsync(RedisExpandKey.Task(task), "Captions")).First();
|
||||||
|
|
||||||
|
taskData.ChatAnalysisScore = 0;
|
||||||
|
taskData.ErrorMessage = string.Empty;
|
||||||
|
taskData.LastEnum = RedisChannelEnum.结束任务;
|
||||||
|
taskData.EndTime = DateTime.Now;
|
||||||
|
|
||||||
|
await videoTaskDB.CopyNew().AsUpdateable(taskData)
|
||||||
|
.UpdateColumns(it => new
|
||||||
|
{
|
||||||
|
it.Captions,
|
||||||
|
it.Speaker,
|
||||||
|
it.ChatAnalysisScore,
|
||||||
|
it.ErrorMessage,
|
||||||
|
it.TotalTokens,
|
||||||
|
it.LastEnum,
|
||||||
|
it.EndTime
|
||||||
|
}).ExecuteCommandAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ExpandFunction.DeleteTaskAllFileAsync(tId, this);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,465 @@
|
||||||
|
using FreeRedis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SqlSugar;
|
||||||
|
using SqlSugar.IOC;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using VideoAnalysisCore.Model;
|
||||||
|
using VideoAnalysisCore.Model.Enum;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Common
|
||||||
|
{
|
||||||
|
public class WorkflowFlowSwitchException : Exception { }
|
||||||
|
|
||||||
|
public abstract class WorkflowBase<TEnum> where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
public bool StopTask { get; set; } = false;
|
||||||
|
public Dictionary<TEnum, Func<string, Task>> SubscribeList = new Dictionary<TEnum, Func<string, Task>>();
|
||||||
|
public readonly RedisClient Redis;
|
||||||
|
public readonly RedisManager RedisManager;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private List<Task> _workerTasks = new List<Task>();
|
||||||
|
public static ConcurrentDictionary<string, Task> RunningTasks = new ConcurrentDictionary<string, Task>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作流的Redis队列key
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string ChannelKey { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// 单工作流并发数量
|
||||||
|
/// </summary>
|
||||||
|
protected abstract int Concurrency { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作流名称 (e.g. "VideoSliceWorkflow")
|
||||||
|
/// <para>默认通过类名去除 "Manager" 后缀生成,子类可重写</para>
|
||||||
|
/// </summary>
|
||||||
|
protected virtual string WorkflowName => this.GetType().Name.Replace("Manager", "");
|
||||||
|
|
||||||
|
public WorkflowBase(RedisClient redis, RedisManager redisManager)
|
||||||
|
{
|
||||||
|
Redis = redis;
|
||||||
|
RedisManager = redisManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void InitChannel()
|
||||||
|
{
|
||||||
|
if (AppCommon.Config.TaskSetting.IS_Server) return;
|
||||||
|
//处理之前程序结束前未能执行完的情况
|
||||||
|
var oldTaskCount = Redis.LLen(RedisExpandKey.IDTask);
|
||||||
|
//重试任务并发过多可能会导致程序崩溃
|
||||||
|
// 未能重新分析的中断任务 则单独开一个网页来处理
|
||||||
|
if (oldTaskCount > 0)
|
||||||
|
{
|
||||||
|
//获取所有未完成的任务
|
||||||
|
var oldTaskArr = Redis.LRange(RedisExpandKey.IDTask, 0, -1);
|
||||||
|
Console.WriteLine($"{DateTime.Now:HH:mm:ss}-------------> 发现 {oldTaskArr.Length} 个未完成任务,准备恢复...");
|
||||||
|
|
||||||
|
//使用信号量限制并发数(5),防止崩溃
|
||||||
|
using var semaphore = new System.Threading.SemaphoreSlim(5);
|
||||||
|
var retryTaskArr = new List<Task>();
|
||||||
|
|
||||||
|
foreach (var oldTask in oldTaskArr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 检查该任务是否属于当前工作流(根据 LastEnum 的类型)
|
||||||
|
var lastEnumStr = (await Redis.HMGetAsync<string>(RedisExpandKey.Task(oldTask), "LastEnum")).FirstOrDefault();
|
||||||
|
|
||||||
|
// 尝试解析为当前工作流的枚举
|
||||||
|
if (!string.IsNullOrEmpty(lastEnumStr) && Enum.TryParse(typeof(TEnum), lastEnumStr, true, out var result))
|
||||||
|
{
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
var res = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await AddTaskLog(oldTask, DateTime.Now.ToString("HH:mm:ss") + $"-------------> 接收上次未完成任务 [{typeof(TEnum).Name}] " + oldTask, WorkflowName);
|
||||||
|
await ClearTaskError(long.Parse(oldTask));
|
||||||
|
|
||||||
|
var lastEnum = (TEnum)result;
|
||||||
|
await InsertChannel(lastEnum, oldTask);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await SetTaskErrorMessage(long.Parse(oldTask), ex);
|
||||||
|
Console.WriteLine($"恢复任务 {oldTask} 失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
retryTaskArr.Add(res);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 如果无法解析为当前工作流的枚举,说明该任务属于其他工作流,跳过
|
||||||
|
// Console.WriteLine($"任务 {oldTask} 不属于工作流 {typeof(TEnum).Name},跳过");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"检查任务 {oldTask} 状态失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//等待所有 重试任务完成后接收新任务
|
||||||
|
await Task.WhenAll(retryTaskArr);
|
||||||
|
|
||||||
|
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-------------> 所有未完成任务处理完毕!");
|
||||||
|
ReceivingTaskAsync();
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReceivingTaskAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 重新执行任务
|
||||||
|
/// </summary>
|
||||||
|
public void ReceivingTaskAsync()
|
||||||
|
{
|
||||||
|
int concurrency = Concurrency;
|
||||||
|
if (concurrency <= 0) concurrency = 1;
|
||||||
|
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
var token = _cts.Token;
|
||||||
|
|
||||||
|
for (int i = 0; i < concurrency; i++)
|
||||||
|
{
|
||||||
|
var index = i;
|
||||||
|
var task = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{DateTime.Now} ==> 开始监听 [{typeof(TEnum).Name}] 队列 [{index}]...");
|
||||||
|
while (!token.IsCancellationRequested && !StopTask)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var taskId = Redis.BLPop(ChannelKey, 5);
|
||||||
|
if (!string.IsNullOrEmpty(taskId))
|
||||||
|
{
|
||||||
|
Redis.LPush(RedisExpandKey.IDTask, taskId);
|
||||||
|
// 获取第一个枚举值作为起始步骤
|
||||||
|
var firstStep = Enum.GetValues(typeof(TEnum)).Cast<TEnum>().FirstOrDefault();
|
||||||
|
if (Convert.ToInt32(firstStep) == 0) // 跳过排队中
|
||||||
|
{
|
||||||
|
var next = firstStep.NextEnum();
|
||||||
|
if (next.HasValue) firstStep = next.Value;
|
||||||
|
}
|
||||||
|
await AddTaskLog(taskId, $"==> 接收到任务 [{typeof(TEnum).Name}] ", WorkflowName);
|
||||||
|
|
||||||
|
await InsertChannel(firstStep, taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"任务监听异常: {ex.Message}");
|
||||||
|
await Task.Delay(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
_workerTasks.Add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InsertChannel(TEnum @enum, string taskId)
|
||||||
|
{
|
||||||
|
await AddTaskLog(taskId, "==> 开始执行任务 ", WorkflowName);
|
||||||
|
await ProcessTaskFlow(@enum, taskId, taskId);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 异步流程判定条件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentStep"></param>
|
||||||
|
/// <param name="nextStep"></param>
|
||||||
|
/// <param name="taskId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task HandleSpecialFlowAsync(TEnum currentStep, TEnum nextStep, string taskId)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主处理任务流程
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentStep"></param>
|
||||||
|
/// <param name="taskId"></param>
|
||||||
|
/// <param name="tId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected async Task ProcessTaskFlow(TEnum currentStep, string taskId, string tId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!SubscribeList.ContainsKey(currentStep))
|
||||||
|
throw new Exception($"{currentStep} 未实现");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (StopTask)
|
||||||
|
{
|
||||||
|
await AddTaskLog(tId, "==> 手动停止任务 ", WorkflowName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 1. 记录步骤开始时间 (需要转换 RedisChannelEnum 才能调用 UpdateStepTimeAsync,如果类型不匹配则需要适配)
|
||||||
|
// 这里简化,暂不记录非主流程的时间,或者需要在 RedisManager 增加泛型支持
|
||||||
|
// await RedisManager.UpdateStepTimeAsync(taskId, currentStep);
|
||||||
|
|
||||||
|
// 2. 执行当前步骤
|
||||||
|
await TouchChannel(currentStep, tId, SubscribeList[currentStep]);
|
||||||
|
|
||||||
|
// 3. 准备下一步
|
||||||
|
var nextStepNullable = currentStep.NextEnum();
|
||||||
|
if (nextStepNullable == null) break;
|
||||||
|
|
||||||
|
var nextStep = nextStepNullable.Value;
|
||||||
|
|
||||||
|
// 4. 特殊分流处理
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await HandleSpecialFlowAsync(currentStep, nextStep, taskId);
|
||||||
|
}
|
||||||
|
catch (WorkflowFlowSwitchException)
|
||||||
|
{
|
||||||
|
return; // 流程切换,退出当前循环
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep = nextStep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await SetTaskErrorMessage(long.Parse(tId), ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await Redis.ExpireAsync(RedisExpandKey.Task(taskId), 60 * 60 * 24 * 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新任务状态
|
||||||
|
/// <para>默认实现:保存到 WorkflowState 表。子类可重写以保存到特定表(如 VideoTask)</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId">任务ID</param>
|
||||||
|
/// <param name="step">当前步骤枚举</param>
|
||||||
|
protected virtual async Task UpdateTaskStateAsync(string taskId, TEnum step)
|
||||||
|
{
|
||||||
|
var tID = long.Parse(taskId);
|
||||||
|
var stepName = step.ToString();
|
||||||
|
var stepValue = Convert.ToInt32(step);
|
||||||
|
|
||||||
|
using var scope = AppCommon.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetService<ISqlSugarClient>();
|
||||||
|
if (db == null) return;
|
||||||
|
|
||||||
|
// 尝试更新或插入 WorkflowState
|
||||||
|
// 注意:这里假设 VideoTaskWorkflow 表存在且已正确配置
|
||||||
|
// 如果不想引入新表依赖,也可以在这里留空,由子类实现
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 使用 upsert 逻辑
|
||||||
|
var existing = await db.Queryable<VideoTaskWorkflow>()
|
||||||
|
.FirstAsync(it => it.VideoTaskId == tID && it.WorkflowName == WorkflowName);
|
||||||
|
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await db.Insertable(new VideoTaskWorkflow
|
||||||
|
{
|
||||||
|
Id = Yitter.IdGenerator.YitIdHelper.NextId(),
|
||||||
|
VideoTaskId = tID,
|
||||||
|
WorkflowName = WorkflowName,
|
||||||
|
CurrentStep = stepName,
|
||||||
|
CurrentStepValue = stepValue,
|
||||||
|
UpdateTime = DateTime.Now
|
||||||
|
}).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.CurrentStep = stepName;
|
||||||
|
existing.CurrentStepValue = stepValue;
|
||||||
|
existing.UpdateTime = DateTime.Now;
|
||||||
|
await db.Updateable(existing).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"更新工作流状态失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task TouchChannel(TEnum key, string taskId, Func<string, Task> action)
|
||||||
|
{
|
||||||
|
var tID = long.Parse(taskId);
|
||||||
|
await AddTaskLog(taskId, " 开始执行 " + key + " " + taskId, WorkflowName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 更新 Redis 状态 (通用)
|
||||||
|
Redis.HMSet(RedisExpandKey.Task(taskId), "LastEnum", key.ToString());
|
||||||
|
// 使用新的进度设置方法
|
||||||
|
SetTaskProgress(taskId, 0);
|
||||||
|
|
||||||
|
// 调用状态更新逻辑 (由子类决定存储位置)
|
||||||
|
await UpdateTaskStateAsync(taskId, key);
|
||||||
|
|
||||||
|
await action(taskId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await AddTaskLog(taskId, $""" 出现异常 {ex.Message} {ex.StackTrace} """);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置任务进度
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId"></param>
|
||||||
|
/// <param name="p">进度百分比</param>
|
||||||
|
public void SetTaskProgress(object taskId, object p)
|
||||||
|
{
|
||||||
|
var fieldName = WorkflowName == "VideoSliceWorkflow" ? "Progress" : $"Progress:{WorkflowName}";
|
||||||
|
Redis.HMSet(RedisExpandKey.Task(taskId), fieldName, p.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务结束处理
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId"></param>
|
||||||
|
public virtual async Task TaskEnd(string taskId)
|
||||||
|
{
|
||||||
|
var tId = long.Parse(taskId);
|
||||||
|
|
||||||
|
//删除任务执行状态
|
||||||
|
await Redis.LRemAsync(RedisExpandKey.IDTask, 1, taskId);
|
||||||
|
|
||||||
|
// 更新 VideoTaskWorkflow 表状态为结束
|
||||||
|
// 注意:这里假设结束状态对应的枚举值为 100,或者子类重写此方法
|
||||||
|
// 由于泛型限制,无法直接获取 TEnum 的结束值,建议子类重写或在此处做通用处理
|
||||||
|
// 这里仅做基础清理,具体业务逻辑建议在子类重写
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入任务异常
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskID"></param>
|
||||||
|
/// <param name="ex"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> SetTaskErrorMessage(long taskID, Exception? ex)
|
||||||
|
{
|
||||||
|
var error = string.Empty;
|
||||||
|
if (ex != null)
|
||||||
|
{
|
||||||
|
await Redis.LRemAsync(RedisExpandKey.IDTask, 1, taskID.ToString());
|
||||||
|
//执行任务时出现异常
|
||||||
|
error = ex.Message + ex.StackTrace;
|
||||||
|
await AddTaskLog(taskID, $""" 出现异常 {ex.Message} {ex.StackTrace} """);
|
||||||
|
}
|
||||||
|
return await SetTaskError(taskID, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除 任务的错误信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskID"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> ClearTaskError(long taskID) => await SetTaskError(taskID, string.Empty);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改任务的错误信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskID"></param>
|
||||||
|
/// <param name="error"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> SetTaskError(long taskID, string? error)
|
||||||
|
{
|
||||||
|
using var scope = AppCommon.Services.CreateScope();
|
||||||
|
var vDB = scope.ServiceProvider.GetService<Repository<VideoTask>>();
|
||||||
|
if (vDB == null) return false;
|
||||||
|
|
||||||
|
Redis.HMSet(RedisExpandKey.Task(taskID), "ErrorMessage", error);
|
||||||
|
|
||||||
|
// 同时更新 VideoTaskWorkflow 表的错误信息(如果需要)
|
||||||
|
|
||||||
|
return await vDB.CopyNew().AsUpdateable()
|
||||||
|
.SetColumns(it => it.ErrorMessage == error)
|
||||||
|
.Where(it => it.Id == taskID)
|
||||||
|
.ExecuteCommandAsync() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加日志
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId">任务id</param>
|
||||||
|
/// <param name="msg">内容</param>
|
||||||
|
/// <param name="workflowName">工作流名称(可选,默认为当前工作流)</param>
|
||||||
|
public async Task AddTaskLog(object taskId, string msg, string? workflowName = null)
|
||||||
|
{
|
||||||
|
var wfName = workflowName ?? WorkflowName;
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine($"{DateTime.Now.ToString("MM-dd HH:mm:ss")} => {taskId} [{wfName}] \r\n{msg}\r\n");
|
||||||
|
#endif
|
||||||
|
await Redis.RPushAsync(RedisExpandKey.TaskLog,
|
||||||
|
new TaskLog()
|
||||||
|
{
|
||||||
|
VideoTaskId = long.Parse(taskId.ToString()),
|
||||||
|
CreateTime = DateTime.Now,
|
||||||
|
Message = msg,
|
||||||
|
DeviceId = AppCommon.Config.ID,
|
||||||
|
WorkflowName = wfName
|
||||||
|
});
|
||||||
|
var count = 50;
|
||||||
|
lock (RedisExpandKey.TaskLog)
|
||||||
|
{
|
||||||
|
var oldTaskCount = Redis.LLen(RedisExpandKey.TaskLog);
|
||||||
|
if (oldTaskCount > count)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = AppCommon.Services.CreateScope();
|
||||||
|
var taskLogDB = scope.ServiceProvider.GetService<Repository<TaskLog>>();
|
||||||
|
if (taskLogDB != null)
|
||||||
|
{
|
||||||
|
var insertData = Redis.LRange<TaskLog>(RedisExpandKey.TaskLog, 0, count - 1);
|
||||||
|
taskLogDB.CopyNew().AsInsertable(insertData).ExecuteCommand();
|
||||||
|
//同步删除redis
|
||||||
|
Redis.LTrim(RedisExpandKey.TaskLog, count, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("写入任务日志出错" + "\r\n" + ex.Message + "\r\n" + ex.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 异步流程
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startStep"></param>
|
||||||
|
/// <param name="taskId"></param>
|
||||||
|
/// <param name="tId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected async Task DispatchBackgroundFlow(TEnum startStep, string taskId, string tId)
|
||||||
|
{
|
||||||
|
var bgTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessTaskFlow(startStep, taskId, tId);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RunningTasks.TryRemove(tId, out _);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
RunningTasks.TryAdd(tId, bgTask);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,6 @@ namespace VideoAnalysisCore.Controllers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 通用接口
|
/// 通用接口
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(AuthenticationSchemes = Authentication.vdAdmin)]
|
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class PublicController : ControllerBase
|
public class PublicController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
|
||||||
using FFmpeg.NET.Services;
|
using FFmpeg.NET.Services;
|
||||||
using MapsterMapper;
|
using MapsterMapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
@ -25,6 +25,14 @@ using Yitter.IdGenerator;
|
||||||
|
|
||||||
namespace VideoAnalysisCore.Controllers
|
namespace VideoAnalysisCore.Controllers
|
||||||
{
|
{
|
||||||
|
public class RunningTaskListReq : QueryRequestBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备ID (可选,若为空则默认获取当前节点)
|
||||||
|
/// </summary>
|
||||||
|
public string? DeviceId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 路由菜单
|
/// 路由菜单
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -39,13 +47,16 @@ namespace VideoAnalysisCore.Controllers
|
||||||
|
|
||||||
|
|
||||||
readonly RedisManager redisManager;
|
readonly RedisManager redisManager;
|
||||||
|
readonly TidySlideWorkflowManager tidySlideWorkflowManager;
|
||||||
|
readonly VideoSliceWorkflowManager videoSliceWorkflowManager;
|
||||||
|
|
||||||
public readonly SenseVoice senseVoice;
|
public readonly SenseVoice senseVoice;
|
||||||
public readonly FunASRNano funASRNano;
|
public readonly FunASRNano funASRNano;
|
||||||
|
|
||||||
private readonly IMapper mp;
|
private readonly IMapper mp;
|
||||||
public VideoTaskController(Repository<VideoTask> baseService, RedisManager redisManager,
|
public VideoTaskController(Repository<VideoTask> baseService, RedisManager redisManager,
|
||||||
Repository<VideoQuestion> videoQuestionDB,
|
Repository<VideoQuestion> videoQuestionDB,
|
||||||
Repository<VideoQuestionKonw> videoQuestionKonwDB, Repository<VideoKonwPoint> videoKonwPointDB, SenseVoice senseVoice, IMapper mp, Repository<TaskLog> taskLogDB, FunASRNano funASRNano, Repository<VideoTaskStage> videoTaskStageDB) : base(baseService)
|
Repository<VideoQuestionKonw> videoQuestionKonwDB, Repository<VideoKonwPoint> videoKonwPointDB, SenseVoice senseVoice, IMapper mp, Repository<TaskLog> taskLogDB, FunASRNano funASRNano, Repository<VideoTaskStage> videoTaskStageDB, TidySlideWorkflowManager tidySlideWorkflowManager, VideoSliceWorkflowManager videoSliceWorkflowManager) : base(baseService)
|
||||||
{
|
{
|
||||||
this.baseService = baseService;
|
this.baseService = baseService;
|
||||||
this.redisManager = redisManager;
|
this.redisManager = redisManager;
|
||||||
|
|
@ -57,6 +68,8 @@ namespace VideoAnalysisCore.Controllers
|
||||||
this.taskLogDB = taskLogDB;
|
this.taskLogDB = taskLogDB;
|
||||||
this.funASRNano = funASRNano;
|
this.funASRNano = funASRNano;
|
||||||
this.videoTaskStageDB = videoTaskStageDB;
|
this.videoTaskStageDB = videoTaskStageDB;
|
||||||
|
this.tidySlideWorkflowManager = tidySlideWorkflowManager;
|
||||||
|
this.videoSliceWorkflowManager = videoSliceWorkflowManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -83,6 +96,7 @@ namespace VideoAnalysisCore.Controllers
|
||||||
/// <param name="url">文件流</param>
|
/// <param name="url">文件流</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet(Name = "InitDbTable")]
|
[HttpGet(Name = "InitDbTable")]
|
||||||
|
[AllowAnonymous]
|
||||||
public IActionResult InitDbTable()
|
public IActionResult InitDbTable()
|
||||||
{
|
{
|
||||||
var b = AppCommon.Config.DB.UpdateTable;
|
var b = AppCommon.Config.DB.UpdateTable;
|
||||||
|
|
@ -118,9 +132,9 @@ namespace VideoAnalysisCore.Controllers
|
||||||
public IActionResult StartTask(bool task)
|
public IActionResult StartTask(bool task)
|
||||||
{
|
{
|
||||||
if (task)
|
if (task)
|
||||||
redisManager.RestartTask();
|
videoSliceWorkflowManager.StopTask=false;
|
||||||
else
|
else
|
||||||
redisManager.StopTaskAsync();
|
videoSliceWorkflowManager.StopTask=true;
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -313,14 +327,30 @@ namespace VideoAnalysisCore.Controllers
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task ReStart(long id, RedisChannelEnum selectEnum)
|
public async Task ReStart(long id, RedisChannelEnum selectEnum)
|
||||||
{
|
{
|
||||||
await redisManager.AddTaskLog(id,"手动重试任务");
|
await videoSliceWorkflowManager.AddTaskLog(id, "手动重试任务");
|
||||||
await redisManager.ClearTaskError(id);
|
await videoSliceWorkflowManager.ClearTaskError(id);
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
await redisManager.InsertChannel(selectEnum, id)
|
await videoSliceWorkflowManager.InsertChannel(selectEnum, id.ToString())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重试任务 (TidySlide 工作流)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">任务id</param>
|
||||||
|
/// <param name="selectEnum">任务类型</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task ReStartTidySlide(long id, RedisTidySlideChannelEnum selectEnum)
|
||||||
|
{
|
||||||
|
await tidySlideWorkflowManager.AddTaskLog(id, "手动重试 TidySlide 任务");
|
||||||
|
await tidySlideWorkflowManager.ClearTaskError(id);
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
await tidySlideWorkflowManager.InsertChannel(selectEnum, id.ToString())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 刷新数据
|
/// 刷新数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -332,7 +362,7 @@ namespace VideoAnalysisCore.Controllers
|
||||||
if (id == 0)
|
if (id == 0)
|
||||||
return BadRequest("无效id");
|
return BadRequest("无效id");
|
||||||
var d = await redisManager.Redis.HMGetAsync<string>(RedisExpandKey.Task(id),
|
var d = await redisManager.Redis.HMGetAsync<string>(RedisExpandKey.Task(id),
|
||||||
"Progress", "LastEnum", "StartTime", "ErrorMessage");
|
"Progress", "LastEnum", "StartTime", "ErrorMessage", "Progress:TidySlideWorkflow"); // 获取所有可能的进度字段
|
||||||
|
|
||||||
var logArr = await taskLogDB.AsQueryable()
|
var logArr = await taskLogDB.AsQueryable()
|
||||||
.Where(s => s.VideoTaskId == id)
|
.Where(s => s.VideoTaskId == id)
|
||||||
|
|
@ -343,16 +373,22 @@ namespace VideoAnalysisCore.Controllers
|
||||||
|
|
||||||
logArr = logArr.Concat(insertData).ToArray();
|
logArr = logArr.Concat(insertData).ToArray();
|
||||||
|
|
||||||
|
// 获取所有相关工作流的状态
|
||||||
|
var workflows = await baseService.Context.Queryable<VideoTaskWorkflow>()
|
||||||
|
.Where(w => w.VideoTaskId == id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Progress = d[0],
|
Progress = d[0],
|
||||||
|
TidySlideProgress = d[4], // 返回 TidySlideWorkflow 的进度
|
||||||
LastEnum = d[1]?.ToEnum<RedisChannelEnum>().ToString(),
|
LastEnum = d[1]?.ToEnum<RedisChannelEnum>().ToString(),
|
||||||
StartTime = d[2] != null
|
StartTime = d[2] != null
|
||||||
? JsonSerializer.Deserialize<Dictionary<RedisChannelEnum, DateTime>>(d[2])
|
? JsonSerializer.Deserialize<Dictionary<RedisChannelEnum, DateTime>>(d[2])
|
||||||
: null,
|
: null,
|
||||||
ErrorMessage = d[3],
|
ErrorMessage = d[3],
|
||||||
Logs = logArr,
|
Logs = logArr,
|
||||||
|
Workflows = workflows // 返回工作流列表
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -419,15 +455,59 @@ namespace VideoAnalysisCore.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取在线设备列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult OnlineDevices()
|
||||||
|
{
|
||||||
|
// 扫描 Heartbeat Key
|
||||||
|
var pattern = RedisExpandKey.DeviceHeartbeat("*");
|
||||||
|
var keys = redisManager.Redis.Scan(pattern, 1000).ToList();
|
||||||
|
var prefix = RedisExpandKey.DeviceHeartbeat("");
|
||||||
|
var devices = keys.Select(k => k.Replace(prefix, "")).ToList();
|
||||||
|
return Ok(devices);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行中的任务
|
/// 执行中的任务
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="model">查询模型</param>
|
/// <param name="model">查询模型</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<object> RunningTaskList([FromBody] QueryRequestBase model)
|
public async Task<object> RunningTaskList([FromBody] RunningTaskListReq model)
|
||||||
{
|
{
|
||||||
var oldTaskArr = redisManager.Redis.LRange<long>(RedisExpandKey.IDTask, 0, 999);
|
List<long> oldTaskArr;
|
||||||
|
if (string.IsNullOrEmpty(model.DeviceId))
|
||||||
|
{
|
||||||
|
// 默认获取当前节点
|
||||||
|
oldTaskArr = redisManager.Redis.LRange<long>(RedisExpandKey.IDTask, 0, 999).ToList();
|
||||||
|
}
|
||||||
|
else if (model.DeviceId == "all")
|
||||||
|
{
|
||||||
|
// 获取所有在线节点
|
||||||
|
oldTaskArr = new List<long>();
|
||||||
|
// 直接扫描 Heartbeat Key 获取在线设备
|
||||||
|
var pattern = RedisExpandKey.DeviceHeartbeat("*");
|
||||||
|
var keys = redisManager.Redis.Scan(pattern, 1000).ToList();
|
||||||
|
var prefix = RedisExpandKey.DeviceHeartbeat("");
|
||||||
|
var onlineDevices = keys.Select(k => k.Replace(prefix, "")).ToList();
|
||||||
|
|
||||||
|
foreach (var deviceId in onlineDevices)
|
||||||
|
{
|
||||||
|
var key = RedisExpandKey.BaseKey + "Services:" + deviceId;
|
||||||
|
var tasks = redisManager.Redis.LRange<long>(key, 0, 999);
|
||||||
|
oldTaskArr.AddRange(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 获取指定节点
|
||||||
|
var key = RedisExpandKey.BaseKey + "Services:" + model.DeviceId;
|
||||||
|
oldTaskArr = redisManager.Redis.LRange<long>(key, 0, 999).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
var sqlquery = base.BaseQuery(model)
|
var sqlquery = base.BaseQuery(model)
|
||||||
.Where(s => oldTaskArr.Contains(s.Id))
|
.Where(s => oldTaskArr.Contains(s.Id))
|
||||||
.Select(s => new VideoTask
|
.Select(s => new VideoTask
|
||||||
|
|
@ -492,10 +572,25 @@ namespace VideoAnalysisCore.Controllers
|
||||||
.Where(s=>s.VideoTaskId == id);
|
.Where(s=>s.VideoTaskId == id);
|
||||||
|
|
||||||
return logArr.Concat(insertData);
|
return logArr.Concat(insertData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预览 TidySlide 任务结果
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">任务id</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<object> ShowTidySlideTaskInfo(long id)
|
||||||
|
{
|
||||||
|
var db = baseService.Context;
|
||||||
|
var result = await db.Queryable<TidySlideTaskResult>()
|
||||||
|
.Where(s => s.VideoTaskId == id)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
return BadRequest("未找到 TidySlide 任务结果");
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using Coravel.Invocable;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using VideoAnalysisCore.Common;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Job
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 每小时强制清理缓存文件夹下所有内容的任务
|
||||||
|
/// </summary>
|
||||||
|
public class ClearAllCacheJob : IInvocable
|
||||||
|
{
|
||||||
|
public Task Invoke()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheDir = AppCommon.TaskCachedFile;
|
||||||
|
if (!Directory.Exists(cacheDir))
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"{DateTime.Now} 开始强制清理缓存目录: {cacheDir}");
|
||||||
|
|
||||||
|
// 获取所有子目录
|
||||||
|
var directories = Directory.GetDirectories(cacheDir);
|
||||||
|
var i = 0;
|
||||||
|
foreach (var dir in directories)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 检查文件夹创建时间,如果是30分钟前的则删除(防止删除正在运行的任务)
|
||||||
|
if (Directory.GetCreationTime(dir) < DateTime.Now.AddMinutes(-30))
|
||||||
|
{
|
||||||
|
Directory.Delete(dir, true);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 正在使用的文件夹会抛出异常,忽略即可
|
||||||
|
Console.WriteLine($"清理目录 {dir} 时发生错误 (可能正在使用): {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine($"已删除过期缓存数量 {i}");
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"强制清理缓存任务发生异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
using Coravel.Invocable;
|
||||||
|
using FreeRedis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using VideoAnalysisCore.Common;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Job
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备心跳上报任务
|
||||||
|
/// </summary>
|
||||||
|
public class DeviceHeartbeatJob : IInvocable
|
||||||
|
{
|
||||||
|
public Task Invoke()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var redis = AppCommon.Services.GetService<RedisClient>();
|
||||||
|
if (redis == null) return Task.CompletedTask;
|
||||||
|
|
||||||
|
var deviceId = AppCommon.Config.ID.ToString();
|
||||||
|
|
||||||
|
// 1. 发送心跳 (设置一个带过期时间的Key)
|
||||||
|
// 只有当程序正常运行时,这个Key才会不断被续期
|
||||||
|
// 过期时间设为 60秒,Job每30秒执行一次
|
||||||
|
redis.Set(RedisExpandKey.DeviceHeartbeat(deviceId), DateTime.Now.ToString(), 60);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"心跳任务异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AlibabaCloud.SDK.Vod20170321;
|
using AlibabaCloud.SDK.Vod20170321;
|
||||||
using Coravel.Invocable;
|
using Coravel.Invocable;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -24,23 +24,23 @@ namespace VideoAnalysisCore.Job
|
||||||
public class TaskFileClearJob : IInvocable
|
public class TaskFileClearJob : IInvocable
|
||||||
{
|
{
|
||||||
private readonly Repository<VideoTask> videotaskDB;
|
private readonly Repository<VideoTask> videotaskDB;
|
||||||
private readonly RedisManager redisManager;
|
private readonly VideoSliceWorkflowManager _videoSliceWorkflowManager;
|
||||||
|
|
||||||
public TaskFileClearJob(Repository<VideoTask> videotaskDB, RedisManager redisManager)
|
public TaskFileClearJob(Repository<VideoTask> videotaskDB, VideoSliceWorkflowManager videoSliceWorkflowManager)
|
||||||
{
|
{
|
||||||
this.videotaskDB = videotaskDB;
|
this.videotaskDB = videotaskDB;
|
||||||
this.redisManager = redisManager;
|
_videoSliceWorkflowManager = videoSliceWorkflowManager;
|
||||||
}
|
}
|
||||||
public void DeleteTaskAllCaches()
|
public async Task DeleteTaskAllCachesAsync()
|
||||||
{
|
{
|
||||||
|
|
||||||
var startTime = -5;
|
var startTime = 0;
|
||||||
var timeSpan = startTime - 999;
|
var timeSpan = startTime - 999;
|
||||||
// 计算 6 天前已完成任务缓存
|
// 计算 {startTime} 天前已完成任务缓存
|
||||||
DateTime twoDaysAgo = DateTime.Now.AddDays(startTime);
|
DateTime twoDaysAgo = DateTime.Now.AddDays(startTime);
|
||||||
DateTime endDaysAgo = DateTime.Now.AddDays(timeSpan);
|
DateTime endDaysAgo = DateTime.Now.AddDays(timeSpan);
|
||||||
|
|
||||||
// 查询 2 天前任务执行完成的记录
|
// 查询 {startTime} 天前任务执行完成的记录
|
||||||
var completedTasks = videotaskDB.AsQueryable()
|
var completedTasks = videotaskDB.AsQueryable()
|
||||||
.Where(t => (
|
.Where(t => (
|
||||||
//筛选 结束任务 或者 错误任务
|
//筛选 结束任务 或者 错误任务
|
||||||
|
|
@ -54,23 +54,9 @@ namespace VideoAnalysisCore.Job
|
||||||
|
|
||||||
// 遍历查询结果,删除缓存文件
|
// 遍历查询结果,删除缓存文件
|
||||||
foreach (var taskId in completedTasks)
|
foreach (var taskId in completedTasks)
|
||||||
{
|
await ExpandFunction.DeleteTaskAllFileAsync(taskId, _videoSliceWorkflowManager);
|
||||||
var path = taskId.ToString().LocalPath();
|
|
||||||
if (!string.IsNullOrEmpty(path) && Directory.Exists(path))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(path, true);
|
|
||||||
Console.WriteLine($"已删除缓存文件: {taskId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"删除缓存文件 {taskId} 时出错: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public async void DeleteTaskVideoCaches()
|
public async Task DeleteTaskVideoCachesAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -94,7 +80,7 @@ namespace VideoAnalysisCore.Job
|
||||||
|
|
||||||
// 遍历查询结果,删除缓存文件
|
// 遍历查询结果,删除缓存文件
|
||||||
foreach (var taskId in completedTasks)
|
foreach (var taskId in completedTasks)
|
||||||
await ExpandFunction.DeleteTaskFileAsync(taskId, redisManager);
|
await ExpandFunction.DeleteTaskFileAsync(taskId, _videoSliceWorkflowManager);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -104,8 +90,8 @@ namespace VideoAnalysisCore.Job
|
||||||
public async Task Invoke()
|
public async Task Invoke()
|
||||||
{
|
{
|
||||||
Console.WriteLine($"{DateTime.Now} 执行=>{this.GetType().FullName}");
|
Console.WriteLine($"{DateTime.Now} 执行=>{this.GetType().FullName}");
|
||||||
DeleteTaskVideoCaches();
|
await DeleteTaskVideoCachesAsync();
|
||||||
DeleteTaskAllCaches();
|
await DeleteTaskAllCachesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Model.Enum
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// TidySlide 工作流 Redis 频道枚举
|
||||||
|
/// </summary>
|
||||||
|
public enum RedisTidySlideChannelEnum
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 排队中
|
||||||
|
/// </summary>
|
||||||
|
排队中 = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 下载文件
|
||||||
|
/// </summary>
|
||||||
|
下载文件 = 10,
|
||||||
|
/// <summary>
|
||||||
|
/// 合并切片
|
||||||
|
/// </summary>
|
||||||
|
合并切片 = 20,
|
||||||
|
/// <summary>
|
||||||
|
/// 上传视频
|
||||||
|
/// </summary>
|
||||||
|
上传视频 = 30,
|
||||||
|
/// <summary>
|
||||||
|
/// 结束任务
|
||||||
|
/// </summary>
|
||||||
|
结束任务 = 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -35,5 +35,15 @@ namespace VideoAnalysisCore.Model
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SugarColumn(ColumnDataType ="text",IsNullable = true)]
|
[SugarColumn(ColumnDataType ="text",IsNullable = true)]
|
||||||
public string? Message { get; set; }
|
public string? Message { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 设备id
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public int? DeviceId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 工作流名称
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 50, IsNullable = true)]
|
||||||
|
public string? WorkflowName { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using VideoAnalysisCore.Model.Interface;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Model
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 整理PPT视频切片任务结果表
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("tidyslidetaskresult")]
|
||||||
|
public class TidySlideTaskResult : IDB
|
||||||
|
{
|
||||||
|
[SugarColumn(IsPrimaryKey = true)]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关联的任务ID
|
||||||
|
/// </summary>
|
||||||
|
public long VideoTaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 视频ID (VOD VideoId)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 100)]
|
||||||
|
public string VideoId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 媒体路径 (OSS 地址或播放地址)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 500, IsNullable = true)]
|
||||||
|
public string? MediaUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreateTime { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using VideoAnalysisCore.Model.Interface;
|
||||||
|
|
||||||
|
namespace VideoAnalysisCore.Model
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 任务工作流状态表
|
||||||
|
/// <para>用于记录单个任务在不同工作流中的执行状态</para>
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("videotask_workflow")]
|
||||||
|
public class VideoTaskWorkflow : IDB
|
||||||
|
{
|
||||||
|
[SugarColumn(IsPrimaryKey = true)]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关联的任务ID
|
||||||
|
/// </summary>
|
||||||
|
public long VideoTaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作流名称 (e.g. "VideoSlice", "Upload")
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 50)]
|
||||||
|
public string WorkflowName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前步骤 (枚举的字符串表示)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 50)]
|
||||||
|
public string CurrentStep { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前步骤 (枚举的整数值)
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentStepValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态信息/错误信息
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 500, IsNullable = true)]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UpdateTime { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue