From 86b36167861a2dee2b57e2dade6c912f67a8aa63 Mon Sep 17 00:00:00 2001 From: chenhao Date: Mon, 9 Mar 2026 14:44:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E5=92=8C=E4=BB=BB=E5=8A=A1=E6=A8=A1=E5=9E=8B=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96ASR=E5=92=8C=E6=80=BB=E7=BB=93=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 `Meeting` 实体中的 `asrModelId`, `summaryModelId`, `promptContent`, `useSpkId`, `hotWords`, `summaryContent` 字段,新增 `latestSummaryTaskId` 字段 - 更新数据库表结构,移除相关字段并添加 `latest_summary_task_id` 字段 - 在 `AiTask` 实体中新增 `task_config` 和 `result_file_path` 字段 - 重构 `AiTaskServiceImpl` 中的 ASR 和总结任务处理逻辑,支持任务配置和结果文件路径 - 重构 `MeetingServiceImpl` 中的会议创建和更新逻辑,分离 ASR 和总结任务 - 优化 `PromptTemplateServiceImpl`,新增用户模板状态管理功能 --- backend/design/db_schema.md | 9 +- backend/design/db_schema_pgsql.sql | 9 +- .../controller/biz/MeetingController.java | 27 ++ .../biz/PromptTemplateController.java | 107 +++--- .../java/com/imeeting/dto/biz/MeetingVO.java | 1 - .../java/com/imeeting/entity/biz/AiTask.java | 5 + .../java/com/imeeting/entity/biz/Meeting.java | 13 +- .../service/biz/PromptTemplateService.java | 3 +- .../service/biz/impl/AiTaskServiceImpl.java | 177 ++++++---- .../service/biz/impl/MeetingServiceImpl.java | 104 +++++- .../biz/impl/PromptTemplateServiceImpl.java | 145 ++++++--- .../java/com/imeeting/biz/SummaryTest.java | 304 +++++++++--------- .../src/pages/business/PromptTemplates.tsx | 31 +- 13 files changed, 575 insertions(+), 360 deletions(-) diff --git a/backend/design/db_schema.md b/backend/design/db_schema.md index 393bb14..ffdfca9 100644 --- a/backend/design/db_schema.md +++ b/backend/design/db_schema.md @@ -316,10 +316,8 @@ | id | BIGSERIAL | PK | 主键ID | | tenant_id | BIGINT | NOT NULL | 租户ID | | title | VARCHAR(200) | NOT NULL | 会议标题 | -| asr_model_id | BIGINT | | 使用的 ASR 模型 | -| summary_model_id | BIGINT | | 使用的 LLM 模型 | -| prompt_content | TEXT | | **[快照]** 发起任务时的提示词模板内容 | -| summary_content | TEXT | | **[固化]** 最终生成的 Markdown 总结内容 | +| audio_url | VARCHAR(500) | | 专属音频路径 | +| latest_summary_task_id | BIGINT | | 最新成功的总结任务ID | | status | SMALLINT | DEFAULT 0 | 0:待处理, 1:识别中, 2:总结中, 3:已完成, 4:失败 | ### 5.6 `biz_meeting_transcripts`(转录明细表) @@ -335,8 +333,11 @@ | 字段 | 类型 | 约束 | 说明 | | --- | --- | --- | --- | | id | BIGSERIAL | PK | 主键ID | +| meeting_id | BIGINT | NOT NULL | 关联会议ID | | task_type | VARCHAR(20) | | ASR / SUMMARY | | request_data | JSONB | | 请求原始数据 | | response_data | JSONB | | 响应原始数据 | +| task_config | TEXT | | **[快照]** 任务配置(模型ID、提示词模板等) | +| result_file_path | VARCHAR(500) | | 结果文件相对路径 (如MD总结文件) | | status | SMALLINT | | 0:排队, 1:处理中, 2:成功, 3:失败 | diff --git a/backend/design/db_schema_pgsql.sql b/backend/design/db_schema_pgsql.sql index 9d3a79e..2aaf5be 100644 --- a/backend/design/db_schema_pgsql.sql +++ b/backend/design/db_schema_pgsql.sql @@ -341,12 +341,7 @@ CREATE TABLE biz_meetings ( audio_url VARCHAR(500), creator_id BIGINT, -- 发起人ID creator_name VARCHAR(100), -- 发起人姓名 - asr_model_id BIGINT, -- ASR模型ID - summary_model_id BIGINT, -- LLM模型ID - prompt_content TEXT, -- 发起任务时的提示词模板快照 - use_spk_id SMALLINT DEFAULT 1, -- 是否开启声纹识别 (1:开启, 0:关闭) - hot_words text, -- 任务发起时的热词快照 - summary_content TEXT, -- Markdown 总结结果 + latest_summary_task_id BIGINT, -- 最新成功总结任务ID status SMALLINT DEFAULT 0, -- 0:待处理, 1:处理中, 2:成功, 3:失败 created_at TIMESTAMP(6) NOT NULL DEFAULT now(), updated_at TIMESTAMP(6) NOT NULL DEFAULT now(), @@ -381,6 +376,8 @@ CREATE TABLE biz_ai_tasks ( status SMALLINT DEFAULT 0, -- 0:排队, 1:执行中, 2:成功, 3:失败 request_data text, -- 请求三方原始JSON response_data text, -- 三方返回原始JSON + task_config text, -- 任务配置参数快照 + result_file_path VARCHAR(500), -- 结果文件路径 error_msg TEXT, -- 错误堆栈 started_at TIMESTAMP(6), completed_at TIMESTAMP(6) diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java index fd63f42..df68d07 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java @@ -9,6 +9,7 @@ import com.imeeting.dto.biz.MeetingVO; import com.imeeting.entity.biz.Meeting; import com.imeeting.security.LoginUser; import com.imeeting.service.biz.MeetingService; +import com.imeeting.service.biz.PromptTemplateService; import org.apache.fontbox.ttf.TrueTypeCollection; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -54,13 +55,16 @@ import java.util.regex.Pattern; public class MeetingController { private final MeetingService meetingService; + private final PromptTemplateService promptTemplateService; private final StringRedisTemplate redisTemplate; private final String uploadPath; public MeetingController(MeetingService meetingService, + PromptTemplateService promptTemplateService, StringRedisTemplate redisTemplate, @Value("${app.upload-path}") String uploadPath) { this.meetingService = meetingService; + this.promptTemplateService = promptTemplateService; this.redisTemplate = redisTemplate; this.uploadPath = uploadPath; } @@ -113,6 +117,18 @@ public class MeetingController { @PreAuthorize("isAuthenticated()") public ApiResponse create(@RequestBody MeetingDTO dto) { LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (dto.getPromptId() != null) { + boolean enabled = promptTemplateService.isTemplateEnabledForUser( + dto.getPromptId(), + loginUser.getTenantId(), + loginUser.getUserId(), + loginUser.getIsPlatformAdmin(), + loginUser.getIsTenantAdmin() + ); + if (!enabled) { + return ApiResponse.error("总结模板不可用或已被你禁用"); + } + } dto.setTenantId(loginUser.getTenantId()); dto.setCreatorId(loginUser.getUserId()); dto.setCreatorName(loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername()); @@ -205,9 +221,20 @@ public class MeetingController { @PostMapping("/re-summary") @PreAuthorize("isAuthenticated()") public ApiResponse reSummary(@RequestBody Map params) { + LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Long meetingId = Long.valueOf(params.get("meetingId").toString()); Long summaryModelId = Long.valueOf(params.get("summaryModelId").toString()); Long promptId = Long.valueOf(params.get("promptId").toString()); + boolean enabled = promptTemplateService.isTemplateEnabledForUser( + promptId, + loginUser.getTenantId(), + loginUser.getUserId(), + loginUser.getIsPlatformAdmin(), + loginUser.getIsTenantAdmin() + ); + if (!enabled) { + return ApiResponse.error("总结模板不可用或已被你禁用"); + } meetingService.reSummary(meetingId, summaryModelId, promptId); return ApiResponse.ok(true); diff --git a/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java b/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java index c0b14f2..4ac0c41 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java @@ -27,27 +27,20 @@ public class PromptTemplateController { @PreAuthorize("isAuthenticated()") public ApiResponse save(@RequestBody PromptTemplateDTO dto) { LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - - // 权限校验逻辑 + if (Integer.valueOf(1).equals(dto.getIsSystem())) { - // 只有平台管理员能创建平台级模板(tenantId=0) - // 只有租户管理员能创建租户级模板(tenantId>0) if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) { - return ApiResponse.error("无权创建系统模板"); + return ApiResponse.error("No permission to create public template"); } - - // 如果是租户管理员创建系统模板,确保 tenantId 是其所属租户 if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { dto.setTenantId(loginUser.getTenantId()); - } else { - // 平台管理员:如果 DTO 没传 tenantId,默认设为 0 - if (dto.getTenantId() == null) dto.setTenantId(0L); + } else if (dto.getTenantId() == null) { + dto.setTenantId(0L); } } else { - // 普通模板 dto.setTenantId(loginUser.getTenantId()); } - + return ApiResponse.ok(promptTemplateService.saveTemplate(dto, loginUser.getUserId(), loginUser.getTenantId())); } @@ -57,26 +50,24 @@ public class PromptTemplateController { LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); PromptTemplate existing = promptTemplateService.getById(dto.getId()); if (existing == null) { - return ApiResponse.error("模板不存在"); + return ApiResponse.error("Template not found"); } - - // 核心权限判定 + boolean canModify = false; - if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { - // 平台管理员只能修改平台级模板 (tenantId = 0) - canModify = existing.getTenantId() == 0L; - } else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) { - // 租户管理员可以修改本租户的所有模板 (租户预置 + 个人模板) - canModify = existing.getTenantId().equals(loginUser.getTenantId()); - } else { - // 普通用户仅限自己的个人模板 + if (Integer.valueOf(0).equals(existing.getIsSystem())) { canModify = existing.getCreatorId().equals(loginUser.getUserId()); + } else { + if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { + canModify = existing.getTenantId() == 0L; + } else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) { + canModify = existing.getTenantId().equals(loginUser.getTenantId()); + } } if (!canModify) { - return ApiResponse.error("无权修改此模板"); + return ApiResponse.error("No permission to modify this template"); } - + return ApiResponse.ok(promptTemplateService.updateTemplate(dto)); } @@ -85,23 +76,36 @@ public class PromptTemplateController { public ApiResponse updateStatus(@PathVariable Long id, @RequestParam Integer status) { LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); PromptTemplate existing = promptTemplateService.getById(id); - if (existing == null) return ApiResponse.error("模板不存在"); - - boolean canModify = false; - if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { - canModify = existing.getTenantId() == 0L; - } else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) { - canModify = existing.getTenantId().equals(loginUser.getTenantId()); - } else { - canModify = existing.getCreatorId().equals(loginUser.getUserId()); + if (existing == null) { + return ApiResponse.error("Template not found"); } - if (!canModify) { - return ApiResponse.error("无权修改此模板"); + boolean canGlobalModify = false; + if (Integer.valueOf(1).equals(existing.getIsSystem())) { + if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(existing.getTenantId())) { + canGlobalModify = true; + } else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin()) && existing.getTenantId().equals(loginUser.getTenantId())) { + canGlobalModify = true; + } } - existing.setStatus(status); - return ApiResponse.ok(promptTemplateService.updateById(existing)); + if (canGlobalModify) { + existing.setStatus(status); + return ApiResponse.ok(promptTemplateService.updateById(existing)); + } + + boolean success = promptTemplateService.updateUserTemplateStatus( + id, + status, + loginUser.getTenantId(), + loginUser.getUserId(), + loginUser.getIsPlatformAdmin(), + loginUser.getIsTenantAdmin() + ); + if (!success) { + return ApiResponse.error("Template not found or no permission"); + } + return ApiResponse.ok(true); } @DeleteMapping("/{id}") @@ -114,16 +118,18 @@ public class PromptTemplateController { } boolean canModify = false; - if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { - canModify = existing.getTenantId() == 0L; - } else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) { - canModify = existing.getTenantId().equals(loginUser.getTenantId()); - } else { + if (Integer.valueOf(0).equals(existing.getIsSystem())) { canModify = existing.getCreatorId().equals(loginUser.getUserId()); + } else { + if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { + canModify = existing.getTenantId() == 0L; + } else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) { + canModify = existing.getTenantId().equals(loginUser.getTenantId()); + } } if (!canModify) { - return ApiResponse.error("无权删除此模板"); + return ApiResponse.error("No permission to delete this template"); } return ApiResponse.ok(promptTemplateService.removeById(id)); @@ -136,11 +142,16 @@ public class PromptTemplateController { @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String name, @RequestParam(required = false) String category) { - + LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return ApiResponse.ok(promptTemplateService.pageTemplates( - current, size, name, category, - loginUser.getTenantId(), loginUser.getUserId(), - loginUser.getIsPlatformAdmin(), loginUser.getIsTenantAdmin())); + current, + size, + name, + category, + loginUser.getTenantId(), + loginUser.getUserId(), + loginUser.getIsPlatformAdmin(), + loginUser.getIsTenantAdmin())); } } diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java index e374d29..14fce4e 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java @@ -17,7 +17,6 @@ public class MeetingVO { private String participants; private String tags; - private Integer useSpkId; private String audioUrl; private String summaryContent; private Integer status; diff --git a/backend/src/main/java/com/imeeting/entity/biz/AiTask.java b/backend/src/main/java/com/imeeting/entity/biz/AiTask.java index 7c79be0..abf5d36 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/AiTask.java +++ b/backend/src/main/java/com/imeeting/entity/biz/AiTask.java @@ -28,6 +28,11 @@ public class AiTask { @TableField(typeHandler = JacksonTypeHandler.class) private Map responseData; + @TableField(typeHandler = JacksonTypeHandler.class) + private Map taskConfig; + + private String resultFilePath; + private String errorMsg; private LocalDateTime startedAt; diff --git a/backend/src/main/java/com/imeeting/entity/biz/Meeting.java b/backend/src/main/java/com/imeeting/entity/biz/Meeting.java index 3c36993..8e05954 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/Meeting.java +++ b/backend/src/main/java/com/imeeting/entity/biz/Meeting.java @@ -33,16 +33,5 @@ public class Meeting extends BaseEntity { private String creatorName; - private Long asrModelId; - - private Long summaryModelId; - - private String promptContent; - - private Integer useSpkId; - - @TableField(typeHandler = JacksonTypeHandler.class) - private List hotWords; - - private String summaryContent; + private Long latestSummaryTaskId; } diff --git a/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java b/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java index a38bc63..9d1ae45 100644 --- a/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java +++ b/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java @@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.imeeting.dto.biz.PromptTemplateDTO; import com.imeeting.dto.biz.PromptTemplateVO; import com.imeeting.entity.biz.PromptTemplate; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.imeeting.common.PageResult; import java.util.List; @@ -14,4 +13,6 @@ public interface PromptTemplateService extends IService { PromptTemplateVO updateTemplate(PromptTemplateDTO dto); PageResult> pageTemplates(Integer current, Integer size, String name, String category, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); + boolean updateUserTemplateStatus(Long templateId, Integer status, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); + boolean isTemplateEnabledForUser(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index fdcf686..40a8ac2 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -32,6 +32,9 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.time.LocalDateTime; import java.util.*; @@ -54,6 +57,9 @@ public class AiTaskServiceImpl extends ServiceImpl impleme @Value("${app.server-base-url}") private String serverBaseUrl; + @Value("${app.upload-path}") + private String uploadPath; + private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); @@ -72,11 +78,34 @@ public class AiTaskServiceImpl extends ServiceImpl impleme Meeting meeting = meetingMapper.selectById(meetingId); if (meeting == null) return; - // 1. 执行 ASR 识别 (含接管逻辑) - String asrText = processAsrTask(meeting); - - // 2. 执行 LLM 总结 - processSummaryTask(meeting, asrText); + AiTask asrTask = this.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meetingId) + .eq(AiTask::getTaskType, "ASR") + .orderByDesc(AiTask::getId) + .last("limit 1")); + + String asrText = ""; + if (asrTask != null && asrTask.getStatus() == 0) { + asrText = processAsrTask(meeting, asrTask); + } else { + List transcripts = transcriptMapper.selectList(new LambdaQueryWrapper() + .eq(MeetingTranscript::getMeetingId, meetingId) + .orderByAsc(MeetingTranscript::getStartTime)); + asrText = transcripts.stream() + .map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent()) + .collect(Collectors.joining("\n")); + } + + AiTask sumTask = this.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meetingId) + .eq(AiTask::getTaskType, "SUMMARY") + .orderByDesc(AiTask::getId) + .last("limit 1")); + if (sumTask != null && sumTask.getStatus() == 0) { + processSummaryTask(meeting, asrText, sumTask); + } else if (meeting.getStatus() != 3) { + updateMeetingStatus(meetingId, 3); + } redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId)); } catch (Exception e) { @@ -106,7 +135,14 @@ public class AiTaskServiceImpl extends ServiceImpl impleme .map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent()) .collect(Collectors.joining("\n")); - processSummaryTask(meeting, asrText); + AiTask sumTask = this.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meetingId) + .eq(AiTask::getTaskType, "SUMMARY") + .orderByDesc(AiTask::getId) + .last("limit 1")); + if (sumTask != null && sumTask.getStatus() == 0) { + processSummaryTask(meeting, asrText, sumTask); + } } catch (Exception e) { log.error("Re-summary failed for meeting {}", meetingId, e); updateMeetingStatus(meetingId, 4); @@ -114,60 +150,34 @@ public class AiTaskServiceImpl extends ServiceImpl impleme } } - private String processAsrTask(Meeting meeting) throws Exception { + private String processAsrTask(Meeting meeting, AiTask taskRecord) throws Exception { updateMeetingStatus(meeting.getId(), 1); - AiModel asrModel = aiModelService.getById(meeting.getAsrModelId()); + taskRecord.setStatus(1); + taskRecord.setStartedAt(LocalDateTime.now()); + this.updateById(taskRecord); + + Long asrModelId = Long.valueOf(taskRecord.getTaskConfig().get("asrModelId").toString()); + AiModel asrModel = aiModelService.getById(asrModelId); if (asrModel == null) throw new RuntimeException("ASR模型配置不存在"); String submitUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/recognition" : asrModel.getBaseUrl() + "/api/tasks/recognition"; String taskId = null; - AiTask taskRecord = null; - // --- 核心:接管逻辑 --- - List existingTasks = this.list(new LambdaQueryWrapper() - .eq(AiTask::getMeetingId, meeting.getId()) - .eq(AiTask::getTaskType, "ASR") - .orderByDesc(AiTask::getStartedAt)); - - if (!existingTasks.isEmpty()) { - AiTask lastTask = existingTasks.get(0); - if (lastTask.getResponseData() != null && lastTask.getResponseData().get("task_id") != null) { - String potentialTaskId = lastTask.getResponseData().get("task_id").toString(); - log.info("Attempting to resume ASR task {} for meeting {}", potentialTaskId, meeting.getId()); - - // 验证旧 taskId 是否依然有效 - String checkUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + potentialTaskId : asrModel.getBaseUrl() + "/api/tasks/" + potentialTaskId; - try { - String checkResp = get(checkUrl); - JsonNode node = objectMapper.readTree(checkResp); - if (node.path("code").asInt() == 200) { - taskId = potentialTaskId; - taskRecord = lastTask; - log.info("Successfully resumed ASR task {}", taskId); - } - } catch (Exception e) { - log.warn("Old task ID {} is invalid, will start a new one", potentialTaskId); - } - } - } - - // 如果没有可接管的任务,则发起新任务 - if (taskId == null) { - updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0); - Map req = buildAsrRequest(meeting); - taskRecord = createAiTask(meeting.getId(), "ASR", req); - - String respBody = postJson(submitUrl, req); - JsonNode submitNode = objectMapper.readTree(respBody); - if (submitNode.path("code").asInt() != 200) { - updateAiTaskFail(taskRecord, "Submission Failed: " + respBody); - throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText()); - } - taskId = submitNode.path("data").path("task_id").asText(); - taskRecord.setResponseData(Map.of("task_id", taskId)); - this.updateById(taskRecord); + updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0); + Map req = buildAsrRequest(meeting, taskRecord); + taskRecord.setRequestData(req); + this.updateById(taskRecord); + + String respBody = postJson(submitUrl, req); + JsonNode submitNode = objectMapper.readTree(respBody); + if (submitNode.path("code").asInt() != 200) { + updateAiTaskFail(taskRecord, "Submission Failed: " + respBody); + throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText()); } + taskId = submitNode.path("data").path("task_id").asText(); + taskRecord.setResponseData(Map.of("task_id", taskId)); + this.updateById(taskRecord); String queryUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + taskId : asrModel.getBaseUrl() + "/api/tasks/" + taskId; @@ -210,7 +220,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme return saveTranscripts(meeting, resultNode); } - private Map buildAsrRequest(Meeting meeting) { + private Map buildAsrRequest(Meeting meeting, AiTask taskRecord) { Map req = new HashMap<>(); String rawAudioUrl = meeting.getAudioUrl(); String encodedAudioUrl = Arrays.stream(rawAudioUrl.split("/")) @@ -220,15 +230,22 @@ public class AiTaskServiceImpl extends ServiceImpl impleme }) .collect(Collectors.joining("/")); req.put("file_path", serverBaseUrl + (encodedAudioUrl.startsWith("/") ? "" : "/") + encodedAudioUrl); - req.put("use_spk_id", meeting.getUseSpkId() != null && meeting.getUseSpkId() == 1); + + Object useSpkObj = taskRecord.getTaskConfig().get("useSpkId"); + boolean useSpk = useSpkObj != null && useSpkObj.toString().equals("1"); + req.put("use_spk_id", useSpk); List> hotwords = new ArrayList<>(); - if (meeting.getHotWords() != null && !meeting.getHotWords().isEmpty()) { - List entities = hotWordService.list(new LambdaQueryWrapper() - .eq(HotWord::getTenantId, meeting.getTenantId()).in(HotWord::getWord, meeting.getHotWords())); - Map weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1)); - for (String w : meeting.getHotWords()) { - hotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0)); + Object hotWordsObj = taskRecord.getTaskConfig().get("hotWords"); + if (hotWordsObj instanceof List) { + List words = (List) hotWordsObj; + if (!words.isEmpty()) { + List entities = hotWordService.list(new LambdaQueryWrapper() + .eq(HotWord::getTenantId, meeting.getTenantId()).in(HotWord::getWord, words)); + Map weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1)); + for (String w : words) { + hotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0)); + } } } req.put("hotwords", hotwords); @@ -272,22 +289,32 @@ public class AiTaskServiceImpl extends ServiceImpl impleme return sb.toString(); } - private void processSummaryTask(Meeting meeting, String asrText) throws Exception { + private void processSummaryTask(Meeting meeting, String asrText, AiTask taskRecord) throws Exception { updateMeetingStatus(meeting.getId(), 2); updateProgress(meeting.getId(), 90, "正在生成智能总结纪要...", 0); - AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId()); + taskRecord.setStatus(1); + taskRecord.setStartedAt(LocalDateTime.now()); + this.updateById(taskRecord); + + Long summaryModelId = Long.valueOf(taskRecord.getTaskConfig().get("summaryModelId").toString()); + AiModel llmModel = aiModelService.getById(summaryModelId); if (llmModel == null) return; + + String promptContent = taskRecord.getTaskConfig().get("promptContent") != null ? + taskRecord.getTaskConfig().get("promptContent").toString() : ""; Map req = new HashMap<>(); req.put("model", llmModel.getModelCode()); req.put("temperature", llmModel.getTemperature()); req.put("messages", List.of( - Map.of("role", "system", "content", meeting.getPromptContent()), + Map.of("role", "system", "content", promptContent), Map.of("role", "user", "content", "请总结以下会议内容:\n" + asrText) )); - AiTask taskRecord = createAiTask(meeting.getId(), "SUMMARY", req); + taskRecord.setRequestData(req); + this.updateById(taskRecord); + String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions"); HttpRequest request = HttpRequest.newBuilder() @@ -302,10 +329,28 @@ public class AiTaskServiceImpl extends ServiceImpl impleme if (response.statusCode() == 200 && respNode.has("choices")) { String content = respNode.path("choices").path(0).path("message").path("content").asText(); - meeting.setSummaryContent(content); + + // Save to File + String timestamp = java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); + String fileName = "summary_" + timestamp + ".md"; + String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/"; + Path targetDir = Paths.get(basePath, "meetings", String.valueOf(meeting.getId()), "summaries"); + Files.createDirectories(targetDir); + Path filePath = targetDir.resolve(fileName); + + String frontMatter = "---\n" + + "生成时间: " + LocalDateTime.now() + "\n" + + "使用模型: " + llmModel.getModelName() + "\n" + + "---\n\n"; + Files.writeString(filePath, frontMatter + content, StandardCharsets.UTF_8); + + taskRecord.setResultFilePath("meetings/" + meeting.getId() + "/summaries/" + fileName); + updateAiTaskSuccess(taskRecord, respNode); + + meeting.setLatestSummaryTaskId(taskRecord.getId()); meeting.setStatus(3); meetingMapper.updateById(meeting); - updateAiTaskSuccess(taskRecord, respNode); + updateProgress(meeting.getId(), 100, "全流程分析完成", 0); } else { updateAiTaskFail(taskRecord, "LLM Summary failed: " + response.body()); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java index e27a2bb..b8bb797 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java @@ -10,6 +10,7 @@ import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingTranscriptVO; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.AiModel; +import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.PromptTemplate; import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.entity.biz.HotWord; @@ -24,16 +25,27 @@ import com.imeeting.service.biz.PromptTemplateService; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.HotWordService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.FileSystemUtils; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.List; import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor public class MeetingServiceImpl extends ServiceImpl implements MeetingService { @@ -46,6 +58,9 @@ public class MeetingServiceImpl extends ServiceImpl impl private final SysUserMapper sysUserMapper; private final ApplicationEventPublisher eventPublisher; + @Value("${app.upload-path}") + private String uploadPath; + @Override @Transactional(rollbackFor = Exception.class) public MeetingVO createMeeting(MeetingDTO dto) { @@ -54,31 +69,80 @@ public class MeetingServiceImpl extends ServiceImpl impl meeting.setMeetingTime(dto.getMeetingTime()); meeting.setParticipants(dto.getParticipants()); meeting.setTags(dto.getTags()); - meeting.setAudioUrl(dto.getAudioUrl()); - meeting.setAsrModelId(dto.getAsrModelId()); - meeting.setSummaryModelId(dto.getSummaryModelId()); - meeting.setUseSpkId(dto.getUseSpkId() != null ? dto.getUseSpkId() : 1); meeting.setCreatorId(dto.getCreatorId()); meeting.setCreatorName(dto.getCreatorName()); - - if (dto.getPromptId() != null) { - PromptTemplate template = promptTemplateService.getById(dto.getPromptId()); - if (template != null) { - meeting.setPromptContent(template.getPromptContent()); + meeting.setTenantId(dto.getTenantId() != null ? dto.getTenantId() : 0L); + meeting.setStatus(0); + + this.save(meeting); // Save to get meeting ID + + // File moving logic + String audioUrl = dto.getAudioUrl(); + if (audioUrl != null && audioUrl.startsWith("/api/static/audio/")) { + try { + String fileName = audioUrl.substring(audioUrl.lastIndexOf("/") + 1); + String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/"; + Path sourcePath = Paths.get(basePath, "audio", fileName); + + if (Files.exists(sourcePath)) { + String ext = ""; + int dotIdx = fileName.lastIndexOf('.'); + if (dotIdx > 0) { + ext = fileName.substring(dotIdx); + } + String targetDir = basePath + "meetings/" + meeting.getId(); + Files.createDirectories(Paths.get(targetDir)); + Path targetPath = Paths.get(targetDir, "source_audio" + ext); + Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); + + audioUrl = "/api/static/meetings/" + meeting.getId() + "/source_audio" + ext; + } + } catch (Exception e) { + log.error("Failed to move audio file for meeting {}", meeting.getId(), e); + throw new RuntimeException("文件处理失败: " + e.getMessage()); } } + meeting.setAudioUrl(audioUrl); + this.updateById(meeting); + // ASR Task + AiTask asrTask = new AiTask(); + asrTask.setMeetingId(meeting.getId()); + asrTask.setTaskType("ASR"); + asrTask.setStatus(0); + + Map asrConfig = new HashMap<>(); + asrConfig.put("asrModelId", dto.getAsrModelId()); + asrConfig.put("useSpkId", dto.getUseSpkId() != null ? dto.getUseSpkId() : 1); + List finalHotWords = dto.getHotWords(); if (finalHotWords == null || finalHotWords.isEmpty()) { finalHotWords = hotWordService.list(new LambdaQueryWrapper() - .eq(HotWord::getTenantId, dto.getTenantId()) + .eq(HotWord::getTenantId, meeting.getTenantId()) .eq(HotWord::getStatus, 1)) .stream().map(HotWord::getWord).collect(Collectors.toList()); } - meeting.setHotWords(finalHotWords); + asrConfig.put("hotWords", finalHotWords); + asrTask.setTaskConfig(asrConfig); + aiTaskService.save(asrTask); + + // SUMMARY Task + AiTask sumTask = new AiTask(); + sumTask.setMeetingId(meeting.getId()); + sumTask.setTaskType("SUMMARY"); + sumTask.setStatus(0); + + Map sumConfig = new HashMap<>(); + sumConfig.put("summaryModelId", dto.getSummaryModelId()); + if (dto.getPromptId() != null) { + PromptTemplate template = promptTemplateService.getById(dto.getPromptId()); + if (template != null) { + sumConfig.put("promptContent", template.getPromptContent()); + } + } + sumTask.setTaskConfig(sumConfig); + aiTaskService.save(sumTask); - meeting.setStatus(0); - this.save(meeting); eventPublisher.publishEvent(new MeetingCreatedEvent(meeting.getId())); return toVO(meeting); } @@ -165,13 +229,21 @@ public class MeetingServiceImpl extends ServiceImpl impl Meeting meeting = this.getById(meetingId); if (meeting == null) throw new RuntimeException("Meeting not found"); - meeting.setSummaryModelId(summaryModelId); + AiTask sumTask = new AiTask(); + sumTask.setMeetingId(meetingId); + sumTask.setTaskType("SUMMARY"); + sumTask.setStatus(0); + + Map sumConfig = new HashMap<>(); + sumConfig.put("summaryModelId", summaryModelId); if (promptId != null) { PromptTemplate template = promptTemplateService.getById(promptId); if (template != null) { - meeting.setPromptContent(template.getPromptContent()); + sumConfig.put("promptContent", template.getPromptContent()); } } + sumTask.setTaskConfig(sumConfig); + aiTaskService.save(sumTask); meeting.setStatus(2); this.updateById(meeting); @@ -216,10 +288,8 @@ public class MeetingServiceImpl extends ServiceImpl impl vo.setTitle(meeting.getTitle()); vo.setMeetingTime(meeting.getMeetingTime()); vo.setTags(meeting.getTags()); - vo.setUseSpkId(meeting.getUseSpkId()); vo.setAudioUrl(meeting.getAudioUrl()); vo.setStatus(meeting.getStatus()); - vo.setSummaryContent(meeting.getSummaryContent()); vo.setCreatedAt(meeting.getCreatedAt()); if (meeting.getParticipants() != null && !meeting.getParticipants().isEmpty()) { diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java index 9f5ca2c..7ff6efb 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java @@ -7,17 +7,25 @@ import com.imeeting.common.PageResult; import com.imeeting.dto.biz.PromptTemplateDTO; import com.imeeting.dto.biz.PromptTemplateVO; import com.imeeting.entity.biz.PromptTemplate; +import com.imeeting.entity.biz.PromptTemplateUserConfig; import com.imeeting.mapper.biz.PromptTemplateMapper; +import com.imeeting.mapper.biz.PromptTemplateUserConfigMapper; import com.imeeting.service.biz.PromptTemplateService; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service +@RequiredArgsConstructor public class PromptTemplateServiceImpl extends ServiceImpl implements PromptTemplateService { + private final PromptTemplateUserConfigMapper userConfigMapper; + @Override @Transactional(rollbackFor = Exception.class) public PromptTemplateVO saveTemplate(PromptTemplateDTO dto, Long userId, Long tenantId) { @@ -26,7 +34,6 @@ public class PromptTemplateServiceImpl extends ServiceImpl> pageTemplates(Integer current, Integer size, String name, String category, - Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) { - - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - - // 核心过滤逻辑:分层可见性 (精细化处理) - if (Boolean.TRUE.equals(isPlatformAdmin)) { - // 平台管理员:可以看到所有平台级模板 (tenantId=0) - wrapper.eq(PromptTemplate::getTenantId, 0L); - } else if (Boolean.TRUE.equals(isTenantAdmin)) { - // 租户管理员: - // 1. 本租户所有模板 (tenantId=currentTenantId) - // 2. 平台预置 (tenantId=0 & isSystem=1) - wrapper.and(w -> w - .eq(PromptTemplate::getTenantId, tenantId) - .or(sw -> sw.eq(PromptTemplate::getTenantId, 0L).eq(PromptTemplate::getIsSystem, 1)) - ); - } else { - // 普通个人用户: - // 1. 个人创建 (creatorId=currentUserId) - // 2. 平台预置 (tenantId=0 & isSystem=1) - // 3. 租户预置 (tenantId=currentTenantId & isSystem=1) - wrapper.and(w -> w - .eq(PromptTemplate::getCreatorId, userId) - .or(sw -> sw.eq(PromptTemplate::getTenantId, 0L).eq(PromptTemplate::getIsSystem, 1)) - .or(sw -> sw.eq(PromptTemplate::getTenantId, tenantId).eq(PromptTemplate::getIsSystem, 1)) - ); - } + public PageResult> pageTemplates(Integer current, Integer size, String name, String category, + Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) { + + LambdaQueryWrapper wrapper = buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin); - // 通用过滤条件 wrapper.like(name != null && !name.isEmpty(), PromptTemplate::getTemplateName, name) - .eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category) - .orderByDesc(PromptTemplate::getIsSystem) - .orderByDesc(PromptTemplate::getCreatedAt); + .eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category) + .orderByDesc(PromptTemplate::getIsSystem) + .orderByDesc(PromptTemplate::getCreatedAt); Page page = this.page(new Page<>(current, size), wrapper); - List vos = page.getRecords().stream().map(this::toVO).collect(Collectors.toList()); - + List records = page.getRecords(); + Map userStatusMap = queryUserStatusMap(tenantId, userId, records.stream().map(PromptTemplate::getId).collect(Collectors.toList())); + List vos = records.stream() + .map(template -> toVO(template, effectiveStatus(template.getStatus(), userStatusMap.get(template.getId())))) + .collect(Collectors.toList()); + PageResult> result = new PageResult<>(); result.setTotal(page.getTotal()); result.setRecords(vos); return result; } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateUserTemplateStatus(Long templateId, Integer status, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) { + PromptTemplate template = this.getOne(buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin) + .eq(PromptTemplate::getId, templateId) + .last("LIMIT 1")); + if (template == null) { + return false; + } + + PromptTemplateUserConfig existing = userConfigMapper.selectOne(new LambdaQueryWrapper() + .eq(PromptTemplateUserConfig::getTenantId, tenantId) + .eq(PromptTemplateUserConfig::getUserId, userId) + .eq(PromptTemplateUserConfig::getTemplateId, templateId) + .last("LIMIT 1")); + + if (existing != null) { + existing.setStatus(status); + return userConfigMapper.updateById(existing) > 0; + } + + PromptTemplateUserConfig entity = new PromptTemplateUserConfig(); + entity.setTenantId(tenantId); + entity.setUserId(userId); + entity.setTemplateId(templateId); + entity.setStatus(status); + return userConfigMapper.insert(entity) > 0; + } + + @Override + public boolean isTemplateEnabledForUser(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) { + PromptTemplate template = this.getOne(buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin) + .eq(PromptTemplate::getId, templateId) + .last("LIMIT 1")); + if (template == null) { + return false; + } + + PromptTemplateUserConfig config = userConfigMapper.selectOne(new LambdaQueryWrapper() + .eq(PromptTemplateUserConfig::getTenantId, tenantId) + .eq(PromptTemplateUserConfig::getUserId, userId) + .eq(PromptTemplateUserConfig::getTemplateId, templateId) + .last("LIMIT 1")); + Integer userStatus = config == null ? null : config.getStatus(); + return effectiveStatus(template.getStatus(), userStatus) == 1; + } + + private LambdaQueryWrapper buildVisibilityWrapper(Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.and(w -> w + .eq(PromptTemplate::getCreatorId, userId) + .or(sw -> sw.eq(PromptTemplate::getTenantId, 0L).eq(PromptTemplate::getIsSystem, 1)) + .or(sw -> sw.eq(PromptTemplate::getTenantId, tenantId).eq(PromptTemplate::getIsSystem, 1)) + ); + return wrapper; + } + + private Map queryUserStatusMap(Long tenantId, Long userId, List templateIds) { + if (templateIds == null || templateIds.isEmpty()) { + return Map.of(); + } + List configs = userConfigMapper.selectList(new LambdaQueryWrapper() + .eq(PromptTemplateUserConfig::getTenantId, tenantId) + .eq(PromptTemplateUserConfig::getUserId, userId) + .in(PromptTemplateUserConfig::getTemplateId, templateIds)); + + Map statusMap = new HashMap<>(); + for (PromptTemplateUserConfig config : configs) { + statusMap.put(config.getTemplateId(), config.getStatus()); + } + return statusMap; + } + + private Integer effectiveStatus(Integer templateStatus, Integer userStatus) { + if (userStatus != null) { + return userStatus; + } + return templateStatus == null ? 1 : templateStatus; + } + private void copyProperties(PromptTemplateDTO dto, PromptTemplate entity) { entity.setTemplateName(dto.getTemplateName()); entity.setCategory(dto.getCategory()); @@ -107,7 +172,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl transcripts = transcriptMapper.selectList( - new LambdaQueryWrapper() - .eq(MeetingTranscript::getMeetingId, testMeetingId) - .orderByAsc(MeetingTranscript::getStartTime) - ); - - if (transcripts.isEmpty()) { - System.out.println("⚠️ 警告:该会议暂无转录明细数据 (MeetingTranscript)"); - // 如果没明细,您可以选择是否继续,或者手动造一点 - // return; - } - - String realAsrText = transcripts.stream() - .map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent()) - .collect(Collectors.joining("\n")); - - System.out.println("\n--- [DEBUG] 提取到的真实转录文本 ---"); - System.out.println(realAsrText); - - AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId()); - if (llmModel == null) { - System.out.println("❌ 错误:该会议未绑定总结模型配置"); - return; - } - - System.out.println("\n✅ 基础数据加载成功"); - System.out.println(" 模型名称: " + llmModel.getModelName()); - System.out.println(" 提示词模板快照: " + (meeting.getPromptContent() != null && meeting.getPromptContent().length() > 50 - ? meeting.getPromptContent().substring(0, 50) + "..." - : meeting.getPromptContent())); - - // --- 步骤 2: 构造请求 Payload --- - Map req = new HashMap<>(); - req.put("model", llmModel.getModelCode()); - req.put("temperature", llmModel.getTemperature()); - - List> messages = new ArrayList<>(); - // 系统角色注入 Prompt - messages.add(Map.of("role", "system", "content", meeting.getPromptContent() != null ? meeting.getPromptContent() : "请总结以下会议内容")); - // 用户角色注入 真实的 ASR 文本 - messages.add(Map.of("role", "user", "content", "以下是会议转录全文:\n" + realAsrText)); - req.put("messages", messages); - - String jsonPayload = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(req); - System.out.println("\n--- [DEBUG] 发送给 AI 的请求 JSON ---"); - System.out.println(jsonPayload); - - // --- 步骤 3: 发起网络请求 --- - String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions"); - System.out.println("\n--- [DEBUG] 目标 URL: " + url); - - HttpClient client = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) - .build(); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + llmModel.getApiKey()) - .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) - .build(); - - System.out.println("⏳ 正在请求第三方 AI 接口..."); - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - - System.out.println("\n--- [DEBUG] 接口返回状态码: " + response.statusCode()); - System.out.println("--- [DEBUG] 接口返回 Raw Body ---"); - System.out.println(response.body()); - - // --- 步骤 4: 解析结果 --- - if (response.statusCode() == 200) { - JsonNode respNode = objectMapper.readTree(response.body()); - if (respNode.has("choices")) { - String finalContent = respNode.get("choices").get(0).get("message").get("content").asText(); - System.out.println("\n✨ 总结生成成功!结果如下:"); - System.out.println("------------------------------------"); - System.out.println(finalContent); - System.out.println("------------------------------------"); - - // 可选:将结果更新回数据库以便前端查看 - // meeting.setSummaryContent(finalContent); - // meetingMapper.updateById(meeting); - } else { - System.out.println("❌ 错误:返回结果中不包含 'choices' 字段,请检查厂商 API 适配。"); - } - } else { - System.out.println("❌ 接口请求失败,请检查 BaseUrl 和 ApiKey 是否正确。"); - } - } catch (Exception e) { - System.out.println("❌ 网络异常:" + e.getMessage()); - e.printStackTrace(); - } - } -} +//package com.imeeting.biz; +// +//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +//import com.fasterxml.jackson.databind.JsonNode; +//import com.fasterxml.jackson.databind.ObjectMapper; +//import com.imeeting.entity.biz.AiModel; +//import com.imeeting.entity.biz.Meeting; +//import com.imeeting.entity.biz.MeetingTranscript; +//import com.imeeting.mapper.biz.MeetingMapper; +//import com.imeeting.mapper.biz.MeetingTranscriptMapper; +//import com.imeeting.service.biz.AiModelService; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +// +//import java.net.URI; +//import java.net.http.HttpClient; +//import java.net.http.HttpRequest; +//import java.net.http.HttpResponse; +//import java.time.Duration; +//import java.util.ArrayList; +//import java.util.HashMap; +//import java.util.List; +//import java.util.Map; +//import java.util.stream.Collectors; +// +///** +// * 总结模块分步分析测试类 - 真实数据版 +// */ +//@SpringBootTest +//public class SummaryTest { +// +// @Autowired +// private MeetingMapper meetingMapper; +// +// @Autowired +// private MeetingTranscriptMapper transcriptMapper; +// +// @Autowired +// private AiModelService aiModelService; +// +// @Autowired +// private ObjectMapper objectMapper; +// +// @Test +// public void testManualSummary() throws Exception { +// // --- 步骤 1: 准备测试数据 --- +// // 请替换为您数据库中真实的 meetingId +// Long testMeetingId = 3L; +// +// Meeting meeting = meetingMapper.selectById(testMeetingId); +// if (meeting == null) { +// System.out.println("❌ 错误:未找到 ID 为 " + testMeetingId + " 的会议记录"); +// return; +// } +// +// // 获取真实的 ASR 转录数据 +// List transcripts = transcriptMapper.selectList( +// new LambdaQueryWrapper() +// .eq(MeetingTranscript::getMeetingId, testMeetingId) +// .orderByAsc(MeetingTranscript::getStartTime) +// ); +// +// if (transcripts.isEmpty()) { +// System.out.println("⚠️ 警告:该会议暂无转录明细数据 (MeetingTranscript)"); +// // 如果没明细,您可以选择是否继续,或者手动造一点 +// // return; +// } +// +// String realAsrText = transcripts.stream() +// .map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent()) +// .collect(Collectors.joining("\n")); +// +// System.out.println("\n--- [DEBUG] 提取到的真实转录文本 ---"); +// System.out.println(realAsrText); +// +// AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId()); +// if (llmModel == null) { +// System.out.println("❌ 错误:该会议未绑定总结模型配置"); +// return; +// } +// +// System.out.println("\n✅ 基础数据加载成功"); +// System.out.println(" 模型名称: " + llmModel.getModelName()); +// System.out.println(" 提示词模板快照: " + (meeting.getPromptContent() != null && meeting.getPromptContent().length() > 50 +// ? meeting.getPromptContent().substring(0, 50) + "..." +// : meeting.getPromptContent())); +// +// // --- 步骤 2: 构造请求 Payload --- +// Map req = new HashMap<>(); +// req.put("model", llmModel.getModelCode()); +// req.put("temperature", llmModel.getTemperature()); +// +// List> messages = new ArrayList<>(); +// // 系统角色注入 Prompt +// messages.add(Map.of("role", "system", "content", meeting.getPromptContent() != null ? meeting.getPromptContent() : "请总结以下会议内容")); +// // 用户角色注入 真实的 ASR 文本 +// messages.add(Map.of("role", "user", "content", "以下是会议转录全文:\n" + realAsrText)); +// req.put("messages", messages); +// +// String jsonPayload = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(req); +// System.out.println("\n--- [DEBUG] 发送给 AI 的请求 JSON ---"); +// System.out.println(jsonPayload); +// +// // --- 步骤 3: 发起网络请求 --- +// String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions"); +// System.out.println("\n--- [DEBUG] 目标 URL: " + url); +// +// HttpClient client = HttpClient.newBuilder() +// .connectTimeout(Duration.ofSeconds(10)) +// .build(); +// +// HttpRequest request = HttpRequest.newBuilder() +// .uri(URI.create(url)) +// .header("Content-Type", "application/json") +// .header("Authorization", "Bearer " + llmModel.getApiKey()) +// .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) +// .build(); +// +// System.out.println("⏳ 正在请求第三方 AI 接口..."); +// try { +// HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); +// +// System.out.println("\n--- [DEBUG] 接口返回状态码: " + response.statusCode()); +// System.out.println("--- [DEBUG] 接口返回 Raw Body ---"); +// System.out.println(response.body()); +// +// // --- 步骤 4: 解析结果 --- +// if (response.statusCode() == 200) { +// JsonNode respNode = objectMapper.readTree(response.body()); +// if (respNode.has("choices")) { +// String finalContent = respNode.get("choices").get(0).get("message").get("content").asText(); +// System.out.println("\n✨ 总结生成成功!结果如下:"); +// System.out.println("------------------------------------"); +// System.out.println(finalContent); +// System.out.println("------------------------------------"); +// +// // 可选:将结果更新回数据库以便前端查看 +// // meeting.setSummaryContent(finalContent); +// // meetingMapper.updateById(meeting); +// } else { +// System.out.println("❌ 错误:返回结果中不包含 'choices' 字段,请检查厂商 API 适配。"); +// } +// } else { +// System.out.println("❌ 接口请求失败,请检查 BaseUrl 和 ApiKey 是否正确。"); +// } +// } catch (Exception e) { +// System.out.println("❌ 网络异常:" + e.getMessage()); +// e.printStackTrace(); +// } +// } +//} diff --git a/frontend/src/pages/business/PromptTemplates.tsx b/frontend/src/pages/business/PromptTemplates.tsx index 0f99116..d56e81a 100644 --- a/frontend/src/pages/business/PromptTemplates.tsx +++ b/frontend/src/pages/business/PromptTemplates.tsx @@ -95,17 +95,19 @@ const PromptTemplates: React.FC = () => { }); setPreviewContent(record.promptContent); } else { - const isPlatformLevel = Number(record.tenantId) === 0; + const isPlatformLevel = Number(record.tenantId) === 0 && Number(record.isSystem) === 1; const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1; // 权限判定逻辑 let canEdit = false; - if (isPlatformAdmin) { + if (Number(record.isSystem) === 0) { + canEdit = Number(record.creatorId) === currentUserId; + } else if (isPlatformAdmin) { canEdit = isPlatformLevel; } else if (isTenantAdmin) { canEdit = Number(record.tenantId) === activeTenantId; } else { - canEdit = Number(record.creatorId) === currentUserId; + canEdit = false; } if (!canEdit) { @@ -191,15 +193,18 @@ const PromptTemplates: React.FC = () => { let canEdit = false; const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1; - if (isPlatformAdmin) { - // 平台管理员管理平台下的所有 (tenantId = 0) + if (isPersonalLevel) { + // 个人模板仅本人可编辑 + canEdit = Number(item.creatorId) === currentUserId; + } else if (isPlatformAdmin) { + // 平台管理员管理平台公开模板 (tenantId = 0) canEdit = Number(item.tenantId) === 0; } else if (isTenantAdmin) { - // 租户管理员管理本租户所有模板 + // 租户管理员管理本租户公开模板 canEdit = Number(item.tenantId) === activeTenantId; } else { - // 普通用户仅限自己的个人模板 - canEdit = Number(item.creatorId) === currentUserId; + // 普通用户不可编辑公开模板 + canEdit = false; } // 标签颜色与文字 @@ -236,15 +241,15 @@ const PromptTemplates: React.FC = () => { size="small" checked={item.status === 1} onChange={(checked) => handleStatusChange(item.id, checked)} - disabled={!canEdit} + disabled={false} /> - {/*
*/} - {/* {item.templateName}*/} - {/* 使用次数: {item.usageCount || 0}*/} - {/*
*/} +
+ {item.templateName} + {/*使用次数: {item.usageCount || 0}*/} +
{item.tags?.map(tag => {