Compare commits
No commits in common. "master" and "fix_0727" have entirely different histories.
|
|
@ -0,0 +1,227 @@
|
|||
# AGENTS.md(Backend)
|
||||
|
||||
## 一、项目定位
|
||||
|
||||
这是一个 **智能语音识别与总结系统的后台服务**,主要职责包括:
|
||||
|
||||
* 后台管理(用户 / 角色 / 权限)
|
||||
* 设备接入与管理
|
||||
* 任务调度与数据管理
|
||||
* 对接外部 AI 转录服务(仅接口调用,不实现 AI)
|
||||
|
||||
本模块为 **Java 后端服务**,不包含前端页面逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 二、技术栈(必须遵守)
|
||||
|
||||
* Java: **17**
|
||||
* Spring Boot: **3.x**
|
||||
* Web: Spring MVC
|
||||
* Security: **Spring Security + JWT**
|
||||
* ORM: **MyBatis / MyBatis-Plus(禁止 Hibernate / JPA)**
|
||||
* Database: **PostgreSQL**
|
||||
* Cache: Redis
|
||||
* Build Tool: Maven
|
||||
|
||||
⚠️ 禁止引入与以上技术选型冲突的框架与中间件。
|
||||
|
||||
---
|
||||
|
||||
## 三、架构与包结构约定
|
||||
|
||||
### 基础包结构
|
||||
|
||||
```
|
||||
com.xxx.project
|
||||
├── common # 通用工具、常量、异常
|
||||
├── config # Spring / 安全 / Web 配置
|
||||
├── security # JWT、Filter、Security 配置
|
||||
├── auth # 登录、鉴权
|
||||
├── user # 用户管理
|
||||
├── role # 角色管理
|
||||
├── permission # 权限管理
|
||||
├── device # 设备管理
|
||||
├── dict # 字典/配置
|
||||
└── task # 转录/业务任务
|
||||
```
|
||||
|
||||
### 分层规范
|
||||
|
||||
* Controller:仅负责协议与参数校验
|
||||
* Service:业务编排与事务边界
|
||||
* Mapper:只写数据库访问
|
||||
* DTO/VO:显式数据模型,不透传实体
|
||||
* 禁止 Controller 直接调用 Mapper
|
||||
|
||||
---
|
||||
|
||||
## 四、角色与定位
|
||||
|
||||
你是一位**务实型后端开发者 Agent**,目标是:
|
||||
|
||||
> 以最清晰、最朴素、最可验证的方式交付可工作的 Java 服务。
|
||||
> 基本原则
|
||||
> 1. 生成内容必须完整、可运行、不可省略。
|
||||
> 2. 不允许伪代码。
|
||||
> 3. 不允许使用"示例代码"字样。
|
||||
> 4. 不允许省略 import。
|
||||
> 5. 不允许省略异常处理。
|
||||
> 6. 所有写操作必须考虑事务控制。
|
||||
> 7. 所有删除操作必须为逻辑删除(is_deleted)。
|
||||
> 8. 所有表必须包含:
|
||||
> - created_at TIMESTAMP(6)
|
||||
> - updated_at TIMESTAMP(6)
|
||||
> - is_deleted SMALLINT DEFAULT 0
|
||||
### 核心理念
|
||||
|
||||
* 清晰的意图胜于巧妙的代码
|
||||
* 显而易见 > 精妙复杂
|
||||
* 奥卡姆剃刀:不应无必要地增加复杂度
|
||||
* 组合优于继承
|
||||
* 接口优于单例
|
||||
* 显式数据流优于隐式魔法
|
||||
|
||||
### 风格约束
|
||||
|
||||
* 准确、简洁、可维护
|
||||
* 小修改**不输出摘要**
|
||||
* 不炫技、不做“聪明设计”
|
||||
|
||||
---
|
||||
|
||||
## 五、工作流程(强制)
|
||||
|
||||
### 5.1 规划阶段(复杂任务必需)
|
||||
|
||||
### 行为约束
|
||||
1. 在执行任何修改前,必须**阅读并遵守**本项目的设计文档(位于 `docs/design/`)。
|
||||
2. 所有功能改动都必须更新设计文档
|
||||
3. 遵循代码风格、目录结构和 Git 工作流规则
|
||||
|
||||
需求必须先创建:
|
||||
|
||||
`IMPLEMENTATION_PLAN.md`
|
||||
|
||||
```
|
||||
## Stage N: [Name]
|
||||
|
||||
Goal:
|
||||
- 明确可交付物
|
||||
|
||||
Success Criteria:
|
||||
- 可测试的验收标准
|
||||
|
||||
Tests:
|
||||
- 具体测试用例
|
||||
|
||||
Status:
|
||||
- Not Started | In Progress | Complete
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
* 3–5 个阶段
|
||||
* 未完成前不得删除
|
||||
* 未规划禁止直接写实现
|
||||
|
||||
---
|
||||
|
||||
### 5.2 实现循环(TDD Only)
|
||||
|
||||
严格顺序:
|
||||
|
||||
1. 理解
|
||||
|
||||
* 查找 ≥3 个相似实现
|
||||
* 遵循现有项目约定
|
||||
|
||||
2. 测试(Red)
|
||||
|
||||
* 先写失败测试
|
||||
* 只描述行为
|
||||
|
||||
3. 实现(Green)
|
||||
|
||||
* 最小代码通过
|
||||
* 拒绝过度设计
|
||||
|
||||
4. 重构(Refactor)
|
||||
|
||||
* 在测试保护下清理
|
||||
|
||||
---
|
||||
|
||||
### 5.3 三次机会规则
|
||||
|
||||
同一问题最多尝试 **3 次**:
|
||||
|
||||
若失败,必须停止并输出:
|
||||
|
||||
* 已尝试操作
|
||||
* 完整错误
|
||||
* 2–3 个相似方案
|
||||
* 根本性反思
|
||||
|
||||
---
|
||||
### 5.4. 变更同步规则
|
||||
|
||||
当数据库结构发生变更时,必须同步生成:
|
||||
|
||||
- Entity
|
||||
- Mapper
|
||||
- Service
|
||||
- Controller
|
||||
- DTO
|
||||
- VO
|
||||
- 前端类型定义
|
||||
- API 封装
|
||||
- 权限校验调整
|
||||
同步修改backend/design/db_schema.md和backend/design/db_schema_pgsql.sql
|
||||
禁止只修改数据库而不同步代码。
|
||||
|
||||
|
||||
## 六、质量关卡(DoD)
|
||||
|
||||
交付前必须:
|
||||
|
||||
* 可编译
|
||||
* 通过全部测试
|
||||
* 新功能必有测试
|
||||
* 无警告
|
||||
* 不得随意引入新依赖
|
||||
|
||||
---
|
||||
|
||||
## 七、后端设计准则
|
||||
|
||||
* 显式优于隐式
|
||||
* 数据流可追踪
|
||||
* 依赖可替换
|
||||
* 行为可测试
|
||||
* 错误可观测
|
||||
|
||||
**禁止:**
|
||||
|
||||
* 魔法单例
|
||||
* 全局状态
|
||||
* 过早抽象
|
||||
* 与技术栈冲突的框架
|
||||
|
||||
---
|
||||
|
||||
## 八、接口与安全规范
|
||||
|
||||
* 统一返回:`Result<T>`
|
||||
* 必须参数校验
|
||||
* 认证:JWT
|
||||
* 权限:Spring Security
|
||||
* 日志:结构化
|
||||
* 异常:统一处理
|
||||
|
||||
---
|
||||
|
||||
**一句话原则:**
|
||||
|
||||
> 用最朴素的设计 + 最小的改动 + 最确定的测试,
|
||||
> 构建显而易见正确的 Java 后端。
|
||||
|
|
@ -24,7 +24,6 @@
|
|||
<protobuf.version>3.25.8</protobuf.version>
|
||||
<protobuf.plugin.version>0.6.1</protobuf.plugin.version>
|
||||
<os.maven.plugin.version>1.7.1</os.maven.plugin.version>
|
||||
<unisbase.version>1.0.1</unisbase.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
|
@ -167,7 +166,7 @@
|
|||
<dependency>
|
||||
<groupId>com.unisbase</groupId>
|
||||
<artifactId>unisbase-spring-boot-starter</artifactId>
|
||||
<version>${unisbase.version}</version>
|
||||
<version>0.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
|
@ -189,10 +188,6 @@
|
|||
<artifactId>tencentcloud-sdk-java-asr</artifactId>
|
||||
<version>3.1.1470</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ public final class MeetingConstants {
|
|||
public static final String TYPE_OFFLINE = "OFFLINE";
|
||||
public static final String TYPE_REALTIME = "REALTIME";
|
||||
|
||||
public static final String SOURCE_WEB = "WEB";
|
||||
public static final String SOURCE_ANDROID = "ANDROID";
|
||||
|
||||
public static final String DEVICE_MODE_PUBLIC = "PUBLIC";
|
||||
public static final String DEVICE_MODE_PRIVATE = "PRIVATE";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,15 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.exception.BusinessException;
|
||||
import com.unisbase.common.exception.ErrorCodeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class ApiResponseSuccessCodeAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private static final String LEGACY_SUCCESS_CODE = "0";
|
||||
|
|
@ -42,13 +32,4 @@ public class ApiResponseSuccessCodeAdvice implements ResponseBodyAdvice<Object>
|
|||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@ExceptionHandler({MethodArgumentNotValidException.class})
|
||||
public ApiResponse<Void> handleBusinessException(MethodArgumentNotValidException ex) {
|
||||
String msg = ex.getBindingResult().getAllErrors()
|
||||
.stream()
|
||||
.map(DefaultMessageSourceResolvable::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
return new ApiResponse(ErrorCodeEnum.SYSTEM_ERROR.getCode(), msg, (Object) null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactor
|
|||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
|
@ -16,9 +15,6 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
|
|||
@RequiredArgsConstructor
|
||||
public class RealtimeMeetingWebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
private static final String TOMCAT_WS_TEXT_BUFFER_SIZE = "org.apache.tomcat.websocket.textBufferSize";
|
||||
private static final String WS_TEXT_BUFFER_SIZE = "1048576";
|
||||
|
||||
private final RealtimeMeetingProxyWebSocketHandler realtimeMeetingProxyWebSocketHandler;
|
||||
|
||||
@Override
|
||||
|
|
@ -47,9 +43,4 @@ public class RealtimeMeetingWebSocketConfig implements WebSocketConfigurer {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServletContextInitializer realtimeWebSocketBufferInitializer() {
|
||||
return servletContext -> servletContext.setInitParameter(TOMCAT_WS_TEXT_BUFFER_SIZE, WS_TEXT_BUFFER_SIZE);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ public class AndroidAuthController {
|
|||
try {
|
||||
refresh = authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("刷新令牌已失效,请重新登录");
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
}
|
||||
return ApiResponse.ok(refresh);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.imeeting.dto.android.AndroidAuthContext;
|
|||
import com.imeeting.dto.android.AndroidOfflineMeetingCreateCommand;
|
||||
import com.imeeting.dto.android.AndroidMeetingCreateResponse;
|
||||
import com.imeeting.dto.android.AndroidMeetingConfigVo;
|
||||
import com.imeeting.dto.android.QtMeetingUpdateCommand;
|
||||
import com.imeeting.dto.android.AndroidMeetingListItemVO;
|
||||
import com.imeeting.dto.android.AndroidOfflineMeetingConflictVO;
|
||||
import com.imeeting.dto.android.AndroidOfflineMeetingFinishRequest;
|
||||
|
|
@ -52,7 +51,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
|
@ -78,7 +76,6 @@ import java.time.LocalDate;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -294,7 +291,7 @@ public class AndroidMeetingController {
|
|||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
AndroidLoginUserSupport.resolveDisplayName(authContext),
|
||||
"created",
|
||||
"all",
|
||||
null,
|
||||
AndroidLoginUserSupport.isAdmin(authContext)
|
||||
);
|
||||
|
|
@ -410,28 +407,6 @@ public class AndroidMeetingController {
|
|||
return ApiResponse.ok(password);
|
||||
}
|
||||
|
||||
@Operation(summary = "QT更新会议信息")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "更新会议标题、参会人和总结内容",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@PutMapping("/{meetingId}/info")
|
||||
@Log(value = "QT修改会议信息", type = "Android会议管理")
|
||||
public ApiResponse<Boolean> updateMeetingForQt(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@Valid @RequestBody QtMeetingUpdateCommand command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "QT会议", "修改会议信息接口", "meetingId", meetingId);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.updateMeetingForQt(meetingId, command);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除Android会议")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
|
|
@ -479,13 +454,7 @@ public class AndroidMeetingController {
|
|||
? List.of()
|
||||
: promptTemplateList.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||
.collect(Collectors.toList());
|
||||
PromptTemplate effectiveDefault = promptTemplateService.findEffectiveUserDefaultTemplate(tenantId, userId);
|
||||
if (effectiveDefault != null) {
|
||||
enabledTemplates.sort(Comparator.comparing(
|
||||
item -> !Objects.equals(item.getId(), effectiveDefault.getId())
|
||||
));
|
||||
}
|
||||
.toList();
|
||||
resultVo.setTemplateList(enabledTemplates);
|
||||
PageResult<List<AiModelVO>> modelList = aiModelService.pageModels(1, 1000, null, "LLM", tenantId, false);
|
||||
List<AiModelVO> enabledModels = modelList.getRecords() == null
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO;
|
|||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingAuthorizationService;
|
||||
|
|
@ -78,7 +77,6 @@ public class AndroidMeetingRealtimeController {
|
|||
meetingAuthorizationService.assertCanCreateMeeting(authContext);
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile = meetingRuntimeProfileResolver.resolve(
|
||||
authContext.getTenantId(),
|
||||
authContext.getUserId(),
|
||||
command == null ? null : command.getAsrModelId(),
|
||||
command == null ? null : command.getSummaryModelId(),
|
||||
command == null ? null : command.getPromptId(),
|
||||
|
|
@ -98,7 +96,7 @@ public class AndroidMeetingRealtimeController {
|
|||
authContext.getTenantId(),
|
||||
authContext.getUserId(),
|
||||
resolveCreatorName(authContext),
|
||||
MeetingTerminalEnum.CUSTOM_TERMINAL.getCode()
|
||||
MeetingConstants.SOURCE_ANDROID
|
||||
);
|
||||
|
||||
RealtimeMeetingSessionStatusVO status = realtimeMeetingSessionStateService.getStatus(meeting.getId());
|
||||
|
|
@ -168,7 +166,7 @@ public class AndroidMeetingRealtimeController {
|
|||
AndroidRequestLogHelper.logRequest(log, "Android实时会议", "暂停实时会议接口", "meetingId", id);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAuthorizationService.assertCanControlRealtimeMeeting(meeting, authContext, MeetingTerminalEnum.CUSTOM_TERMINAL.getCode());
|
||||
meetingAuthorizationService.assertCanControlRealtimeMeeting(meeting, authContext, MeetingConstants.SOURCE_ANDROID);
|
||||
return ApiResponse.ok(realtimeMeetingSessionStateService.pause(id));
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +185,7 @@ public class AndroidMeetingRealtimeController {
|
|||
AndroidRequestLogHelper.logRequest(log, "Android实时会议", "完成实时会议接口", "meetingId", id, "request", dto);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAuthorizationService.assertCanControlRealtimeMeeting(meeting, authContext, MeetingTerminalEnum.CUSTOM_TERMINAL.getCode());
|
||||
meetingAuthorizationService.assertCanControlRealtimeMeeting(meeting, authContext, MeetingConstants.SOURCE_ANDROID);
|
||||
meetingCommandService.completeRealtimeMeeting(
|
||||
id,
|
||||
dto != null ? dto.getAudioUrl() : null,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package com.imeeting.controller.biz;
|
|||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateDTO;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateResultVO;
|
||||
import com.imeeting.dto.biz.HotWordBatchGroupDTO;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
|
|
@ -54,18 +52,6 @@ public class HotWordController {
|
|||
return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId(), targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量新增热词")
|
||||
@PostMapping("/batch")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "批量新增热词", type = "热词管理")
|
||||
public ApiResponse<HotWordBatchCreateResultVO> saveBatch(@RequestBody HotWordBatchCreateDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
boolean platformAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin());
|
||||
return ApiResponse.ok(
|
||||
hotWordService.saveHotWordsBatch(dto, loginUser.getUserId(), targetTenantId, platformAdmin));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改热词")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
|
|||
|
|
@ -26,8 +26,11 @@ public class HotWordGroupController {
|
|||
this.hotWordGroupService = hotWordGroupService;
|
||||
}
|
||||
|
||||
private Long resolveTargetTenantId(LoginUser loginUser) {
|
||||
return loginUser.getTenantId();
|
||||
private Long resolveTargetTenantId(LoginUser loginUser, Long tenantId) {
|
||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(tenantId)) {
|
||||
return 0L;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Operation(summary = "新增热词组")
|
||||
|
|
@ -36,7 +39,7 @@ public class HotWordGroupController {
|
|||
@Log(value = "新增热词组", type = "热词组管理")
|
||||
public ApiResponse<HotWordGroupVO> save(@RequestBody HotWordGroupDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
return ApiResponse.ok(hotWordGroupService.saveGroup(dto, loginUser.getUserId(), targetTenantId));
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +49,7 @@ public class HotWordGroupController {
|
|||
@Log(value = "修改热词组", type = "热词组管理")
|
||||
public ApiResponse<HotWordGroupVO> update(@RequestBody HotWordGroupDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
HotWordGroupVO existing = hotWordGroupService.listVisibleOptions(targetTenantId).stream()
|
||||
.filter(item -> item.getId().equals(dto.getId()))
|
||||
.findFirst()
|
||||
|
|
@ -54,6 +57,7 @@ public class HotWordGroupController {
|
|||
if (existing == null) {
|
||||
return ApiResponse.error("热词组不存在");
|
||||
}
|
||||
dto.setTenantId(targetTenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.updateGroup(dto));
|
||||
}
|
||||
|
||||
|
|
@ -61,9 +65,9 @@ public class HotWordGroupController {
|
|||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除热词组", type = "热词组管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id, @RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.removeGroupById(id, targetTenantId));
|
||||
}
|
||||
|
||||
|
|
@ -74,18 +78,19 @@ public class HotWordGroupController {
|
|||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.pageGroups(current, size, name, status, targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询热词组选项")
|
||||
@GetMapping("/options")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<HotWordGroupVO>> options() {
|
||||
public ApiResponse<List<HotWordGroupVO>> options(@RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.listVisibleOptions(targetTenantId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
|
|
@ -25,7 +24,6 @@ import com.imeeting.dto.biz.UpdateMeetingSummaryCommand;
|
|||
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
|
|
@ -222,16 +220,13 @@ public class MeetingController {
|
|||
@Operation(summary = "获取会议分享配置")
|
||||
@GetMapping("/share-config")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, String>> getShareConfig(@RequestParam(name = "meetingId", required = false) Long meetingId) {
|
||||
public ApiResponse<Map<String, String>> getShareConfig() {
|
||||
String baseUrl = StringUtils.hasText(h5BaseUrl) ? h5BaseUrl.trim() : "";
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||
}
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("h5BaseUrl", baseUrl);
|
||||
if (meetingId != null) {
|
||||
result.put("h5PreviewUrl", baseUrl + StrUtil.format("/meetings/{}/preview", meetingId));
|
||||
}
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
|
|
@ -247,7 +242,7 @@ public class MeetingController {
|
|||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
MeetingTerminalEnum.WEB.getCode()
|
||||
MeetingConstants.SOURCE_WEB
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +258,7 @@ public class MeetingController {
|
|||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
MeetingTerminalEnum.WEB.getCode()
|
||||
MeetingConstants.SOURCE_WEB
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -410,7 +405,7 @@ public class MeetingController {
|
|||
public ApiResponse<RealtimeMeetingSessionStatusVO> pauseRealtimeMeeting(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingTerminalEnum.WEB.getCode());
|
||||
meetingAccessService.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingConstants.SOURCE_WEB);
|
||||
return ApiResponse.ok(realtimeMeetingSessionStateService.pause(id));
|
||||
}
|
||||
|
||||
|
|
@ -430,7 +425,7 @@ public class MeetingController {
|
|||
command.getEnableItn(),
|
||||
command.getEnableTextRefine(),
|
||||
command.getSaveAudio(),
|
||||
command.getHotWordGroupId(),
|
||||
command.getHotwords(),
|
||||
loginUser
|
||||
));
|
||||
}
|
||||
|
|
@ -441,7 +436,7 @@ public class MeetingController {
|
|||
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id, @RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingTerminalEnum.WEB.getCode());
|
||||
meetingAccessService.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingConstants.SOURCE_WEB);
|
||||
meetingCommandService.completeRealtimeMeeting(
|
||||
id,
|
||||
dto != null ? dto.getAudioUrl() : null,
|
||||
|
|
|
|||
|
|
@ -119,37 +119,6 @@ public class PromptTemplateController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "设置默认提示词模板")
|
||||
@PutMapping("/{id}/default")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "设置默认提示词模板", type = "提示词模板管理")
|
||||
public ApiResponse<Boolean> setDefault(@PathVariable Long id) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
boolean success = promptTemplateService.setUserDefaultTemplate(
|
||||
id,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin()
|
||||
);
|
||||
return success ? ApiResponse.ok(true) : ApiResponse.error("模板不存在、不可用或无权限访问");
|
||||
}
|
||||
|
||||
@Operation(summary = "取消默认提示词模板")
|
||||
@DeleteMapping("/{id}/default")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "取消默认提示词模板", type = "提示词模板管理")
|
||||
public ApiResponse<Boolean> clearDefault(@PathVariable Long id) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
return ApiResponse.ok(promptTemplateService.clearUserDefaultTemplate(
|
||||
id,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin()
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除提示词模板")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
package com.imeeting.controller.qt;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.MeetingAuthorizationService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.annotation.Anonymous;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "Qt会议管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/qt/meetings")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class QtMeetingController {
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final MeetingAuthorizationService meetingAuthorizationService;
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
|
||||
@Operation(summary = "创建 Qt 离线会议")
|
||||
@PostMapping
|
||||
@Anonymous
|
||||
@Log(value = "新增 Qt 离线会议", type = "Qt会议管理")
|
||||
public ApiResponse<MeetingVO> createMeeting(HttpServletRequest request,
|
||||
@Valid @RequestBody CreateMeetingCommand command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Qt会议", "创建离线会议接口", "request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
meetingAuthorizationService.assertCanCreateMeeting(authContext);
|
||||
assertPromptAvailable(command.getPromptId(), authContext);
|
||||
MeetingVO meeting = meetingCommandService.createMeeting(
|
||||
command,
|
||||
authContext.getTenantId(),
|
||||
authContext.getUserId(),
|
||||
resolveCreatorName(authContext),
|
||||
MeetingTerminalEnum.resolve(authContext.getPlatform()).getCode(),
|
||||
authContext.getDeviceId(),
|
||||
resolveSourceDeviceMode(authContext)
|
||||
);
|
||||
return ApiResponse.ok(meeting);
|
||||
}
|
||||
|
||||
private String resolveCreatorName(AndroidAuthContext authContext) {
|
||||
if (hasText(authContext.getDisplayName())) {
|
||||
return authContext.getDisplayName().trim();
|
||||
}
|
||||
if (hasText(authContext.getUsername())) {
|
||||
return authContext.getUsername().trim();
|
||||
}
|
||||
return hasText(authContext.getDeviceId()) ? "qt:" + authContext.getDeviceId().trim() : "qt";
|
||||
}
|
||||
|
||||
private void assertPromptAvailable(Long promptId, AndroidAuthContext authContext) {
|
||||
if (promptId == null) {
|
||||
return;
|
||||
}
|
||||
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||
promptId,
|
||||
authContext.getTenantId(),
|
||||
authContext.getUserId(),
|
||||
authContext.getPlatformAdmin(),
|
||||
authContext.getTenantAdmin()
|
||||
);
|
||||
if (!enabled) {
|
||||
throw new RuntimeException("总结模板不可用");
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveSourceDeviceMode(AndroidAuthContext authContext) {
|
||||
return authContext.isAnonymous()
|
||||
? MeetingConstants.DEVICE_MODE_PUBLIC
|
||||
: MeetingConstants.DEVICE_MODE_PRIVATE;
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ public class CreateMeetingCommand {
|
|||
@NotBlank(message = "音频地址不能为空")
|
||||
private String audioUrl;
|
||||
|
||||
// @NotNull(message = "asrModelId must not be null")
|
||||
@NotNull(message = "asrModelId must not be null")
|
||||
private Long asrModelId;
|
||||
|
||||
@NotNull(message = "summaryModelId must not be null")
|
||||
|
|
@ -37,7 +37,7 @@ public class CreateMeetingCommand {
|
|||
|
||||
private Long chapterModelId;
|
||||
|
||||
@NotNull(message = "总结模板不能为空")
|
||||
@NotNull(message = "promptId must not be null")
|
||||
private Long promptId;
|
||||
|
||||
private Long hotWordGroupId;
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "热词批量新增请求参数")
|
||||
public class HotWordBatchCreateDTO {
|
||||
|
||||
@Schema(description = "租户 ID,平台管理员可传 0 表示平台范围")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "待新增的热词内容列表")
|
||||
private List<String> words;
|
||||
|
||||
@Schema(description = "所属热词组 ID,为空表示未分组")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "热词批量新增结果")
|
||||
public class HotWordBatchCreateResultVO {
|
||||
|
||||
@Schema(description = "实际新增热词数量")
|
||||
private Integer createdCount;
|
||||
|
||||
@Schema(description = "目标热词组中已存在的热词")
|
||||
private List<String> existingWords;
|
||||
}
|
||||
|
|
@ -9,10 +9,10 @@ import java.util.List;
|
|||
@Schema(description = "热词请求参数")
|
||||
public class HotWordDTO {
|
||||
|
||||
@Schema(description = "热词ID")
|
||||
@Schema(description = "热词 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户ID,平台管理员可传0表示平台范围")
|
||||
@Schema(description = "租户 ID,平台管理员可传 0 表示平台范围")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "热词内容")
|
||||
|
|
@ -27,7 +27,7 @@ public class HotWordDTO {
|
|||
@Schema(description = "热词分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "所属热词组ID")
|
||||
@Schema(description = "所属热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "权重")
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import lombok.Data;
|
|||
@Schema(description = "热词组请求参数")
|
||||
public class HotWordGroupDTO {
|
||||
|
||||
@Schema(description = "热词组ID")
|
||||
@Schema(description = "热词组 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户ID,平台管理员可传0表示平台范围")
|
||||
@Schema(description = "租户 ID,平台管理员可传 0 表示平台范围")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "热词组名称")
|
||||
|
|
|
|||
|
|
@ -9,16 +9,16 @@ import java.time.LocalDateTime;
|
|||
@Schema(description = "热词组信息")
|
||||
public class HotWordGroupVO {
|
||||
|
||||
@Schema(description = "热词组ID")
|
||||
@Schema(description = "热词组 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@Schema(description = "租户 ID")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "热词组名称")
|
||||
private String groupName;
|
||||
|
||||
@Schema(description = "创建者ID")
|
||||
@Schema(description = "创建人 ID")
|
||||
private Long creatorId;
|
||||
|
||||
@Schema(description = "状态:1-启用,0-禁用")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import java.util.List;
|
|||
@Schema(description = "热词信息")
|
||||
public class HotWordVO {
|
||||
|
||||
@Schema(description = "热词ID")
|
||||
@Schema(description = "热词 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "热词内容")
|
||||
|
|
@ -22,7 +22,7 @@ public class HotWordVO {
|
|||
@Schema(description = "是否公开,当前固定为公开")
|
||||
private Integer isPublic;
|
||||
|
||||
@Schema(description = "创建者ID")
|
||||
@Schema(description = "创建人 ID")
|
||||
private Long creatorId;
|
||||
|
||||
@Schema(description = "匹配策略")
|
||||
|
|
@ -31,7 +31,7 @@ public class HotWordVO {
|
|||
@Schema(description = "热词分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "所属热词组ID")
|
||||
@Schema(description = "所属热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "所属热词组名称")
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "会议参会人信息")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MeetingParticipantVO {
|
||||
|
||||
@Schema(description = "用户 ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名称")
|
||||
private String displayName;
|
||||
}
|
||||
|
|
@ -42,9 +42,6 @@ public class MeetingVO {
|
|||
@Schema(description = "参会人ID列表")
|
||||
private List<Long> participantIds;
|
||||
|
||||
@Schema(description = "参会人列表,ID 与名称一一对应")
|
||||
private List<MeetingParticipantVO> participantUsers;
|
||||
|
||||
@Schema(description = "标签串")
|
||||
private String tags;
|
||||
|
||||
|
|
@ -75,15 +72,9 @@ public class MeetingVO {
|
|||
@Schema(description = "总结模型ID")
|
||||
private Long summaryModelId;
|
||||
|
||||
@Schema(description = "总结模型名称")
|
||||
private String summaryModelName;
|
||||
|
||||
@Schema(description = "总结模板ID")
|
||||
private Long promptId;
|
||||
|
||||
@Schema(description = "总结模板名称")
|
||||
private String promptName;
|
||||
|
||||
@Schema(description = "最终生效热词组ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ package com.imeeting.dto.biz;
|
|||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class OpenRealtimeSocketSessionCommand {
|
||||
private Long asrModelId;
|
||||
|
|
@ -12,5 +15,5 @@ public class OpenRealtimeSocketSessionCommand {
|
|||
private Boolean enableItn;
|
||||
private Boolean enableTextRefine;
|
||||
private Boolean saveAudio;
|
||||
private Long hotWordGroupId;
|
||||
private List<Map<String, Object>> hotwords;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,14 +30,6 @@ public class PromptTemplateVO {
|
|||
private String hotWordGroupName;
|
||||
@Schema(description = "绑定热词列表")
|
||||
private List<String> hotWords;
|
||||
@Schema(description = "是否为当前用户默认模板")
|
||||
private Boolean isDefault;
|
||||
@Schema(description = "默认模板来源:PERSONAL-个人,TENANT-租户,PLATFORM-平台")
|
||||
private String defaultScope;
|
||||
@Schema(description = "是否为模板所属层级默认模板")
|
||||
private Boolean isTemplateDefault;
|
||||
@Schema(description = "默认模板当前是否有效")
|
||||
private Boolean defaultAvailable;
|
||||
@Schema(description = "使用次数")
|
||||
private Integer usageCount;
|
||||
@Schema(description = "提示词正文")
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public class HotWord extends BaseEntity {
|
|||
@Schema(description = "是否公共热词")
|
||||
private Integer isPublic;
|
||||
|
||||
@Schema(description = "创建者ID")
|
||||
@Schema(description = "创建人ID")
|
||||
private Long creatorId;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
|
|
@ -40,7 +40,7 @@ public class HotWord extends BaseEntity {
|
|||
@Schema(description = "热词分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "所属热词组ID")
|
||||
@Schema(description = "所属热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "权重")
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ import lombok.EqualsAndHashCode;
|
|||
public class HotWordGroup extends BaseEntity {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "热词组ID")
|
||||
@Schema(description = "热词组 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "热词组名称")
|
||||
private String groupName;
|
||||
|
||||
@Schema(description = "创建者ID")
|
||||
@Schema(description = "创建人 ID")
|
||||
private Long creatorId;
|
||||
|
||||
@Schema(description = "备注")
|
||||
|
|
|
|||
|
|
@ -29,9 +29,6 @@ public class PromptTemplate extends BaseEntity {
|
|||
@Schema(description = "是否系统内置")
|
||||
private Integer isSystem;
|
||||
|
||||
@Schema(description = "是否为所属层级默认模板:1-是,0-否")
|
||||
private Integer isDefault;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Long creatorId;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,4 @@ public class PromptTemplateUserConfig extends BaseEntity {
|
|||
|
||||
@Schema(description = "模板ID")
|
||||
private Long templateId;
|
||||
|
||||
@Schema(description = "是否为当前用户默认模板:1-是,0-否")
|
||||
private Integer isDefault;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
package com.imeeting.enums;
|
||||
|
||||
import com.unisbase.common.exception.BusinessException;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum MeetingTerminalEnum {
|
||||
WINDOWS("WINDOWS", "Windows"),
|
||||
MACOS("MACOS", "macOS"),
|
||||
KYLIN("KYLIN", "麒麟"),
|
||||
UOS("UOS", "统信"),
|
||||
HARMONYOS("HARMONYOS", "鸿蒙"),
|
||||
WEB("WEB", "Web端"),
|
||||
CUSTOM_TERMINAL("CUSTOM_TERMINAL", "定制终端");
|
||||
|
||||
private static final String LEGACY_ANDROID_CODE = "ANDROID";
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
MeetingTerminalEnum(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public static boolean isCustomTerminalSource(String source) {
|
||||
return CUSTOM_TERMINAL.matches(source) || LEGACY_ANDROID_CODE.equalsIgnoreCase(source);
|
||||
}
|
||||
|
||||
public static MeetingTerminalEnum resolve(String source) {
|
||||
if (source == null || source.isBlank()) {
|
||||
throw new BusinessException("会议终端类型不能为空");
|
||||
}
|
||||
String normalized = source.trim();
|
||||
if (LEGACY_ANDROID_CODE.equalsIgnoreCase(normalized)) {
|
||||
return CUSTOM_TERMINAL;
|
||||
}
|
||||
for (MeetingTerminalEnum terminal : values()) {
|
||||
if (terminal.matches(normalized)) {
|
||||
return terminal;
|
||||
}
|
||||
}
|
||||
throw new BusinessException("会议终端类型无效: " + source);
|
||||
}
|
||||
|
||||
public static boolean isSameTerminal(String source, String target) {
|
||||
if (isCustomTerminalSource(source) && isCustomTerminalSource(target)) {
|
||||
return true;
|
||||
}
|
||||
return source != null && target != null && source.equalsIgnoreCase(target);
|
||||
}
|
||||
|
||||
public boolean matches(String source) {
|
||||
return source != null && code.equalsIgnoreCase(source);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ import com.imeeting.entity.biz.AiTask;
|
|||
import com.imeeting.entity.biz.LlmModel;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.enums.MeetingStatusEnum;
|
||||
import com.imeeting.mapper.biz.LlmModelMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
|
|
@ -117,7 +116,6 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
: MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile = runtimeProfileResolver.resolve(
|
||||
tenantId,
|
||||
creatorUserId,
|
||||
null,
|
||||
requestedSummaryModelId,
|
||||
requestedPromptId,
|
||||
|
|
@ -139,7 +137,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
normalizeTags(request.getTags()),
|
||||
null,
|
||||
MeetingConstants.TYPE_OFFLINE,
|
||||
MeetingTerminalEnum.CUSTOM_TERMINAL.getCode(),
|
||||
MeetingConstants.SOURCE_ANDROID,
|
||||
tenantId,
|
||||
creatorUserId,
|
||||
resolvedCreatorName,
|
||||
|
|
@ -203,7 +201,6 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
|
||||
RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve(
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
null,
|
||||
effectiveSummaryModelId,
|
||||
effectivePromptId,
|
||||
|
|
@ -281,7 +278,6 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
}
|
||||
RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve(
|
||||
meeting.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
null,
|
||||
effectiveSummaryModelId,
|
||||
effectivePromptId,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.imeeting.service.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateDTO;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateResultVO;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
|
|
@ -11,8 +9,6 @@ import java.util.List;
|
|||
|
||||
public interface HotWordService extends IService<HotWord> {
|
||||
HotWordVO saveHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId);
|
||||
|
||||
HotWordBatchCreateResultVO saveHotWordsBatch(HotWordBatchCreateDTO dto, Long userId, Long tenantId, boolean platformAdmin);
|
||||
HotWordVO updateHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId);
|
||||
Integer updateHotWordGroupBatch(List<Long> ids, Long hotWordGroupId, Long tenantId);
|
||||
List<String> generatePinyin(String word);
|
||||
|
|
|
|||
|
|
@ -12,21 +12,12 @@ import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
|||
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||
import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
|
||||
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
||||
import com.imeeting.dto.android.QtMeetingUpdateCommand;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MeetingCommandService {
|
||||
MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource);
|
||||
|
||||
MeetingVO createMeeting(CreateMeetingCommand command,
|
||||
Long tenantId,
|
||||
Long creatorId,
|
||||
String creatorName,
|
||||
String meetingSource,
|
||||
String sourceDeviceCode,
|
||||
String sourceDeviceMode);
|
||||
|
||||
MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource);
|
||||
|
||||
MeetingVO createPublicDeviceMeeting(PublicDeviceMeetingCreateCommand command,
|
||||
|
|
@ -55,8 +46,6 @@ public interface MeetingCommandService {
|
|||
|
||||
void updateSummaryContent(Long meetingId, String summaryContent);
|
||||
|
||||
void updateMeetingForQt(Long meetingId, QtMeetingUpdateCommand command);
|
||||
|
||||
void reSummary(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt, String summaryDetailLevel);
|
||||
|
||||
void retryTranscription(Long meetingId);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import java.util.List;
|
|||
|
||||
public interface MeetingRuntimeProfileResolver {
|
||||
RealtimeMeetingRuntimeProfile resolve(Long tenantId,
|
||||
Long userId,
|
||||
Long asrModelId,
|
||||
Long summaryModelId,
|
||||
Long promptId,
|
||||
|
|
|
|||
|
|
@ -17,10 +17,4 @@ public interface PromptTemplateService extends IService<PromptTemplate> {
|
|||
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);
|
||||
|
||||
boolean setUserDefaultTemplate(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
||||
|
||||
boolean clearUserDefaultTemplate(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
||||
|
||||
PromptTemplate findEffectiveUserDefaultTemplate(Long tenantId, Long userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import com.imeeting.dto.biz.RealtimeSocketSessionData;
|
|||
import com.imeeting.dto.biz.RealtimeSocketSessionVO;
|
||||
import com.unisbase.security.LoginUser;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface RealtimeMeetingSocketSessionService {
|
||||
RealtimeSocketSessionVO createSession(Long meetingId, Long asrModelId, String mode, String language,
|
||||
Integer useSpkId, Boolean enablePunctuation, Boolean enableItn,
|
||||
Boolean enableTextRefine, Boolean saveAudio,
|
||||
Long hotWordGroupId, LoginUser loginUser);
|
||||
List<Map<String, Object>> hotwords, LoginUser loginUser);
|
||||
|
||||
RealtimeSocketSessionData getSessionData(String sessionToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1610,7 +1610,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
} catch (Exception ex) {
|
||||
failPendingSummaryTask(summaryTask, ex.getMessage());
|
||||
this.updateById(summaryTask);
|
||||
updateProgress(meeting.getId(), -1, "更新状态失败 " + ex.getMessage(), 0);
|
||||
updateProgress(meeting.getId(), -1, "闂佽崵鍠愰悷杈╃不閹达絻浜归柛灞剧☉缁剁偤鏌″搴″箹闁?n8n 缂傚倸鍊搁崐褰掓偋濡ゅ啯鏆滈柟鐐綑缁剁偤寮堕崼顐函鐞? " + ex.getMessage(), 0);
|
||||
log.error("Failed to trigger external n8n webhook for meeting {}", meeting.getId(), ex);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,5 +146,4 @@ public class HotWordGroupServiceImpl extends ServiceImpl<HotWordGroupMapper, Hot
|
|||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ package com.imeeting.service.biz.impl;
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateDTO;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateResultVO;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
|
|
@ -12,9 +10,6 @@ import com.imeeting.entity.biz.HotWordGroup;
|
|||
import com.imeeting.mapper.biz.HotWordGroupMapper;
|
||||
import com.imeeting.mapper.biz.HotWordMapper;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.unisbase.common.exception.BusinessException;
|
||||
import com.unisbase.dto.SysDictItemDTO;
|
||||
import com.unisbase.service.SysDictItemService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
|
|
@ -37,14 +32,9 @@ import java.util.stream.Collectors;
|
|||
@RequiredArgsConstructor
|
||||
public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> implements HotWordService {
|
||||
|
||||
private static final String HOT_WORD_GROUP_LIMIT_DICT_TYPE = "biz_hotword_group_limit";
|
||||
private static final int DEFAULT_MAX_HOT_WORDS_PER_GROUP = 200;
|
||||
private static final int DEFAULT_MATCH_STRATEGY = 1;
|
||||
private static final int DEFAULT_WEIGHT = 2;
|
||||
private static final int ENABLED_STATUS = 1;
|
||||
private static final int MAX_HOT_WORDS_PER_GROUP = 200;
|
||||
|
||||
private final HotWordGroupMapper hotWordGroupMapper;
|
||||
private final SysDictItemService sysDictItemService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
|
|
@ -62,39 +52,12 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
return toVO(hotWord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotWordBatchCreateResultVO saveHotWordsBatch(HotWordBatchCreateDTO dto, Long userId, Long tenantId, boolean platformAdmin) {
|
||||
Set<String> words = normalizeWords(dto.getWords());
|
||||
if (words.isEmpty()) {
|
||||
throw new BusinessException("请选择有效热词");
|
||||
}
|
||||
|
||||
Long groupId = validateGroupForBatchCreate(dto.getHotWordGroupId(), tenantId, platformAdmin);
|
||||
Set<String> existingWords = findExistingWords(words, groupId);
|
||||
List<String> existingWordList = words.stream().filter(existingWords::contains).toList();
|
||||
List<HotWord> hotWords = words.stream()
|
||||
.filter(word -> !existingWords.contains(word))
|
||||
.map(word -> buildBatchHotWord(word, groupId, dto.getRemark(), userId))
|
||||
.toList();
|
||||
HotWordBatchCreateResultVO result = new HotWordBatchCreateResultVO();
|
||||
result.setExistingWords(existingWordList);
|
||||
if (hotWords.isEmpty()) {
|
||||
result.setCreatedCount(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
validateGroupCapacityForBatchCreate(groupId, hotWords.size());
|
||||
result.setCreatedCount(this.saveBatch(hotWords) ? hotWords.size() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotWordVO updateHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId) {
|
||||
HotWord hotWord = this.getById(hotWordDTO.getId());
|
||||
if (hotWord == null) {
|
||||
throw new BusinessException("热词不存在");
|
||||
throw new IllegalArgumentException("热词不存在");
|
||||
}
|
||||
|
||||
String oldWord = hotWord.getWord();
|
||||
|
|
@ -113,20 +76,20 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
@Transactional(rollbackFor = Exception.class)
|
||||
public Integer updateHotWordGroupBatch(List<Long> ids, Long hotWordGroupId, Long tenantId) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
throw new BusinessException("请选择热词");
|
||||
throw new IllegalArgumentException("请选择热词");
|
||||
}
|
||||
Set<Long> uniqueIds = ids.stream()
|
||||
.filter(id -> id != null)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (uniqueIds.isEmpty()) {
|
||||
throw new BusinessException("请选择热词");
|
||||
throw new IllegalArgumentException("请选择热词");
|
||||
}
|
||||
|
||||
List<HotWord> hotWords = this.list(new LambdaQueryWrapper<HotWord>()
|
||||
.in(HotWord::getId, uniqueIds)
|
||||
.eq(HotWord::getTenantId, tenantId));
|
||||
if (hotWords.size() != uniqueIds.size()) {
|
||||
throw new BusinessException("部分热词不存在或无权操作");
|
||||
throw new IllegalArgumentException("部分热词不存在或无权操作");
|
||||
}
|
||||
|
||||
if (hotWordGroupId != null) {
|
||||
|
|
@ -197,17 +160,16 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
}
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(groupId);
|
||||
if (group == null || !tenantId.equals(group.getTenantId())) {
|
||||
throw new BusinessException("热词组不存在");
|
||||
throw new IllegalArgumentException("热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(group.getStatus())) {
|
||||
throw new BusinessException("热词组已禁用");
|
||||
throw new IllegalArgumentException("热词组已禁用");
|
||||
}
|
||||
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getHotWordGroupId, groupId)
|
||||
.ne(currentHotWordId != null, HotWord::getId, currentHotWordId));
|
||||
int maxHotWordsPerGroup = getMaxHotWordsPerGroup();
|
||||
if (currentCount >= maxHotWordsPerGroup) {
|
||||
throwGroupCapacityExceeded(maxHotWordsPerGroup);
|
||||
if (currentCount >= MAX_HOT_WORDS_PER_GROUP) {
|
||||
throw new IllegalArgumentException("热词组最多只能包含 200 个热词");
|
||||
}
|
||||
return group.getId();
|
||||
}
|
||||
|
|
@ -215,10 +177,10 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
private void validateGroupCapacity(Long groupId, Long tenantId, List<HotWord> movingHotWords) {
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(groupId);
|
||||
if (group == null || !tenantId.equals(group.getTenantId())) {
|
||||
throw new BusinessException("热词组不存在");
|
||||
throw new IllegalArgumentException("热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(group.getStatus())) {
|
||||
throw new BusinessException("热词组已禁用");
|
||||
throw new IllegalArgumentException("热词组已禁用");
|
||||
}
|
||||
Set<Long> movingIds = movingHotWords.stream().map(HotWord::getId).collect(Collectors.toSet());
|
||||
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
|
||||
|
|
@ -227,99 +189,11 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
long incomingCount = movingHotWords.stream()
|
||||
.filter(item -> !groupId.equals(item.getHotWordGroupId()))
|
||||
.count();
|
||||
int maxHotWordsPerGroup = getMaxHotWordsPerGroup();
|
||||
if (currentCount + incomingCount > maxHotWordsPerGroup) {
|
||||
throwGroupCapacityExceeded(maxHotWordsPerGroup);
|
||||
if (currentCount + incomingCount > MAX_HOT_WORDS_PER_GROUP) {
|
||||
throw new IllegalArgumentException("热词组最多只能包含 200 个热词");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> normalizeWords(List<String> words) {
|
||||
if (words == null || words.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return words.stream()
|
||||
.filter(word -> word != null)
|
||||
.map(String::trim)
|
||||
.filter(word -> !word.isEmpty() && !"无".equals(word))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private Set<String> findExistingWords(Set<String> words, Long groupId) {
|
||||
return this.list(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(groupId != null, HotWord::getHotWordGroupId, groupId)
|
||||
.isNull(groupId == null, HotWord::getHotWordGroupId)
|
||||
.in(HotWord::getWord, words))
|
||||
.stream()
|
||||
.map(HotWord::getWord)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Long validateGroupForBatchCreate(Long groupId, Long tenantId, boolean platformAdmin) {
|
||||
if (groupId == null) {
|
||||
return null;
|
||||
}
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(groupId);
|
||||
if (group == null || (!platformAdmin && !tenantId.equals(group.getTenantId()))) {
|
||||
throw new BusinessException("热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(ENABLED_STATUS).equals(group.getStatus())) {
|
||||
throw new BusinessException("热词组已禁用");
|
||||
}
|
||||
return group.getId();
|
||||
}
|
||||
|
||||
private void validateGroupCapacityForBatchCreate(Long groupId, int incomingCount) {
|
||||
if (groupId == null) {
|
||||
return;
|
||||
}
|
||||
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getHotWordGroupId, groupId));
|
||||
int maxHotWordsPerGroup = getMaxHotWordsPerGroup();
|
||||
if (currentCount + incomingCount > maxHotWordsPerGroup) {
|
||||
throwGroupCapacityExceeded(maxHotWordsPerGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private HotWord buildBatchHotWord(String word, Long groupId, String remark, Long userId) {
|
||||
HotWord hotWord = new HotWord();
|
||||
hotWord.setWord(word);
|
||||
hotWord.setPinyinList(generatePinyin(word));
|
||||
hotWord.setMatchStrategy(DEFAULT_MATCH_STRATEGY);
|
||||
hotWord.setCategory("");
|
||||
hotWord.setHotWordGroupId(groupId);
|
||||
hotWord.setWeight(DEFAULT_WEIGHT);
|
||||
hotWord.setStatus(ENABLED_STATUS);
|
||||
hotWord.setIsPublic(1);
|
||||
hotWord.setCreatorId(userId);
|
||||
hotWord.setRemark(remark);
|
||||
return hotWord;
|
||||
}
|
||||
|
||||
private int getMaxHotWordsPerGroup() {
|
||||
List<SysDictItemDTO> items = sysDictItemService.getItemsByTypeCode(HOT_WORD_GROUP_LIMIT_DICT_TYPE);
|
||||
if (items == null || items.isEmpty()) {
|
||||
return DEFAULT_MAX_HOT_WORDS_PER_GROUP;
|
||||
}
|
||||
for (SysDictItemDTO item : items) {
|
||||
if (item == null || item.getItemValue() == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
int configuredLimit = Integer.parseInt(item.getItemValue().trim());
|
||||
if (configuredLimit > 0) {
|
||||
return configuredLimit;
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
log.warn("热词组上限字典配置值非法: {}", item.getItemValue());
|
||||
}
|
||||
}
|
||||
return DEFAULT_MAX_HOT_WORDS_PER_GROUP;
|
||||
}
|
||||
|
||||
private void throwGroupCapacityExceeded(int maxHotWordsPerGroup) {
|
||||
throw new BusinessException("热词组最多只能包含 " + maxHotWordsPerGroup + " 个热词");
|
||||
}
|
||||
|
||||
private void generateCombinations(List<List<String>> matrix, int index, String current, List<String> result) {
|
||||
if (index == matrix.size()) {
|
||||
result.add(current.trim());
|
||||
|
|
@ -364,5 +238,4 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.imeeting.service.biz.impl;
|
|||
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.unisbase.security.LoginUser;
|
||||
|
|
@ -107,10 +106,6 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
|
|||
if (meeting.getMeetingSource() == null || meeting.getMeetingSource().isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (MeetingTerminalEnum.isCustomTerminalSource(meeting.getMeetingSource())
|
||||
&& MeetingTerminalEnum.isCustomTerminalSource(currentPlatform)) {
|
||||
return;
|
||||
}
|
||||
if (!meeting.getMeetingSource().equalsIgnoreCase(currentPlatform)) {
|
||||
throw new RuntimeException("不允许跨平台接管实时会议");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.imeeting.service.biz.impl;
|
|||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingAuthorizationService;
|
||||
import com.unisbase.security.LoginUser;
|
||||
|
|
@ -48,7 +47,7 @@ public class MeetingAuthorizationServiceImpl implements MeetingAuthorizationServ
|
|||
}
|
||||
if (meeting.getMeetingSource() != null
|
||||
&& !meeting.getMeetingSource().isBlank()
|
||||
&& !isSameRealtimePlatform(meeting.getMeetingSource(), currentPlatform)) {
|
||||
&& !meeting.getMeetingSource().equalsIgnoreCase(currentPlatform)) {
|
||||
throw new RuntimeException("不允许跨平台接管实时会议");
|
||||
}
|
||||
return;
|
||||
|
|
@ -75,8 +74,4 @@ public class MeetingAuthorizationServiceImpl implements MeetingAuthorizationServ
|
|||
loginUser.setDisplayName(authContext.getDisplayName());
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
private boolean isSameRealtimePlatform(String meetingSource, String currentPlatform) {
|
||||
return MeetingTerminalEnum.isSameTerminal(meetingSource, currentPlatform);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,12 @@ import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
|||
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||
import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
|
||||
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
||||
import com.imeeting.dto.android.QtMeetingUpdateCommand;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
||||
import com.imeeting.enums.BusinessErrorCodeEnum;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.enums.MeetingStatusEnum;
|
||||
import com.imeeting.service.android.AndroidPendingMeetingDraftService;
|
||||
import com.imeeting.service.android.AndroidPushMessageService;
|
||||
|
|
@ -154,19 +152,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) {
|
||||
return createMeeting(command, tenantId, creatorId, creatorName, meetingSource, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MeetingVO createMeeting(CreateMeetingCommand command,
|
||||
Long tenantId,
|
||||
Long creatorId,
|
||||
String creatorName,
|
||||
String meetingSource,
|
||||
String sourceDeviceCode,
|
||||
String sourceDeviceMode) {
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId, creatorId);
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
|
||||
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
||||
String resolvedCreatorName = resolveMeetingUserName(creatorId, creatorName);
|
||||
String hostName = resolveMeetingUserName(hostUserId, resolvedCreatorName);
|
||||
|
|
@ -174,7 +160,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
||||
command.getAudioUrl(), MeetingConstants.TYPE_OFFLINE, meetingSource, tenantId, creatorId, resolvedCreatorName,
|
||||
hostUserId, hostName, runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId(),
|
||||
runtimeProfile.getResolvedHotWordGroupId(), summaryDetailLevel, 0, sourceDeviceCode, sourceDeviceMode);
|
||||
runtimeProfile.getResolvedHotWordGroupId(), summaryDetailLevel, 0);
|
||||
meetingService.save(meeting);
|
||||
|
||||
AiTask asrTask = new AiTask();
|
||||
|
|
@ -246,7 +232,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) {
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId, creatorId);
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
|
||||
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
||||
String resolvedCreatorName = resolveMeetingUserName(creatorId, creatorName);
|
||||
String hostName = resolveMeetingUserName(hostUserId, resolvedCreatorName);
|
||||
|
|
@ -300,7 +286,6 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
String deviceCode) {
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile = meetingRuntimeProfileResolver.resolve(
|
||||
tenantId,
|
||||
creatorId,
|
||||
command.getAsrModelId(),
|
||||
command.getSummaryModelId(),
|
||||
command.getPromptId(),
|
||||
|
|
@ -325,7 +310,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
command.getTags(),
|
||||
null,
|
||||
MeetingConstants.TYPE_OFFLINE,
|
||||
MeetingTerminalEnum.CUSTOM_TERMINAL.getCode(),
|
||||
MeetingConstants.SOURCE_ANDROID,
|
||||
tenantId,
|
||||
creatorId,
|
||||
resolvedCreatorName,
|
||||
|
|
@ -768,23 +753,6 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
meetingSummaryFileService.updateSummaryContent(meeting, summaryContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateMeetingForQt(Long meetingId, QtMeetingUpdateCommand command) {
|
||||
Meeting meeting = meetingService.getById(meetingId);
|
||||
if (meeting == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
}
|
||||
meetingService.update(new LambdaUpdateWrapper<Meeting>()
|
||||
.eq(Meeting::getId, meetingId)
|
||||
.set(Meeting::getTitle, command.getTitle().trim())
|
||||
.set(Meeting::getParticipants, command.getParticipantIds().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(","))));
|
||||
meetingSummaryFileService.updateSummaryContent(meeting, command.getSummaryContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MeetingTranscriptChapterImportResultVO importTranscriptChapters(MeetingTranscriptChapterImportDTO command) {
|
||||
|
|
@ -1561,10 +1529,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
return resumeConfig;
|
||||
}
|
||||
|
||||
private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateMeetingCommand command, Long tenantId, Long userId) {
|
||||
private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateMeetingCommand command, Long tenantId) {
|
||||
return meetingRuntimeProfileResolver.resolve(
|
||||
tenantId,
|
||||
userId,
|
||||
command.getAsrModelId(),
|
||||
command.getSummaryModelId(),
|
||||
command.getPromptId(),
|
||||
|
|
@ -1580,10 +1547,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
);
|
||||
}
|
||||
|
||||
private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateRealtimeMeetingCommand command, Long tenantId, Long userId) {
|
||||
private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateRealtimeMeetingCommand command, Long tenantId) {
|
||||
return meetingRuntimeProfileResolver.resolve(
|
||||
tenantId,
|
||||
userId,
|
||||
command.getAsrModelId(),
|
||||
command.getSummaryModelId(),
|
||||
command.getPromptId(),
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import cn.hutool.core.date.StopWatch;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.HotWordGroup;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.entity.biz.PromptTemplate;
|
||||
import com.imeeting.dto.biz.MeetingParticipantVO;
|
||||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
import com.imeeting.event.MeetingCreatedEvent;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.HotWordGroupService;
|
||||
import com.imeeting.service.biz.MeetingPointsService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import com.unisbase.service.SysTenantUserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
|
@ -44,9 +40,9 @@ import java.time.LocalDateTime;
|
|||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -61,14 +57,11 @@ public class MeetingDomainSupport {
|
|||
private final MeetingTranscriptMapper transcriptMapper;
|
||||
private final MeetingPointsService meetingPointsService;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final SysTenantUserService sysTenantUserService;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||
private final MeetingPlaybackAudioResolver meetingPlaybackAudioResolver;
|
||||
private final HotWordGroupService hotWordGroupService;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
private final AiModelService aiModelService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final SysParamService sysParamService;
|
||||
|
||||
@Value("${unisbase.app.upload-path}")
|
||||
|
|
@ -440,7 +433,6 @@ public class MeetingDomainSupport {
|
|||
vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus());
|
||||
vo.setSummaryModelId(meeting.getSummaryModelId());
|
||||
vo.setPromptId(meeting.getPromptId());
|
||||
fillSummaryConfigurationNames(meeting, vo);
|
||||
fillEffectiveHotWordGroup(meeting, vo);
|
||||
vo.setAiCatalogEnabled(resolveAiCatalogEnabled());
|
||||
vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel()));
|
||||
|
|
@ -461,31 +453,19 @@ public class MeetingDomainSupport {
|
|||
.map(Long::valueOf)
|
||||
.collect(Collectors.toList());
|
||||
vo.setParticipantIds(userIds);
|
||||
vo.setParticipantUsers(Collections.emptyList());
|
||||
if (!userIds.isEmpty()) {
|
||||
List<SysUser> users = sysUserMapper.selectBatchIds(userIds);
|
||||
Map<Long, String> userNameMap = users.stream().collect(Collectors.toMap(
|
||||
SysUser::getUserId,
|
||||
user -> resolveParticipantName(user, meeting.getTenantId())
|
||||
));
|
||||
List<MeetingParticipantVO> participantUsers = userIds.stream()
|
||||
.map(userId -> new MeetingParticipantVO(userId, userNameMap.get(userId)))
|
||||
.collect(Collectors.toList());
|
||||
vo.setParticipantUsers(participantUsers);
|
||||
String names = participantUsers.stream()
|
||||
.map(MeetingParticipantVO::getDisplayName)
|
||||
.filter(Objects::nonNull)
|
||||
String names = users.stream()
|
||||
.map(u -> u.getDisplayName() != null ? u.getDisplayName() : u.getUsername())
|
||||
.collect(Collectors.joining(", "));
|
||||
vo.setParticipants(names);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
vo.setParticipantIds(Collections.emptyList());
|
||||
vo.setParticipantUsers(Collections.emptyList());
|
||||
vo.setParticipants(meeting.getParticipants());
|
||||
}
|
||||
} else {
|
||||
vo.setParticipantIds(Collections.emptyList());
|
||||
vo.setParticipantUsers(Collections.emptyList());
|
||||
}
|
||||
fillLatestTaskAttemptInfo(meeting, vo);
|
||||
if (includeSummary) {
|
||||
|
|
@ -495,28 +475,6 @@ public class MeetingDomainSupport {
|
|||
}
|
||||
}
|
||||
|
||||
private String resolveParticipantName(SysUser user, Long tenantId) {
|
||||
if (user == null || user.getUserId() == null) {
|
||||
return "";
|
||||
}
|
||||
return user.getDisplayName() != null ? user.getDisplayName() : user.getUsername();
|
||||
}
|
||||
|
||||
private void fillSummaryConfigurationNames(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) {
|
||||
if (meeting.getSummaryModelId() != null) {
|
||||
AiModelVO summaryModel = aiModelService.getModelById(meeting.getSummaryModelId(), "LLM");
|
||||
if (summaryModel != null) {
|
||||
vo.setSummaryModelName(summaryModel.getModelName());
|
||||
}
|
||||
}
|
||||
if (meeting.getPromptId() != null) {
|
||||
PromptTemplate promptTemplate = promptTemplateService.getById(meeting.getPromptId());
|
||||
if (promptTemplate != null) {
|
||||
vo.setPromptName(promptTemplate.getTemplateName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fillEffectiveHotWordGroup(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) {
|
||||
Long hotWordGroupId = resolveEffectiveHotWordGroupId(meeting);
|
||||
vo.setHotWordGroupId(hotWordGroupId);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
|||
|
||||
@Override
|
||||
public RealtimeMeetingRuntimeProfile resolve(Long tenantId,
|
||||
Long userId,
|
||||
Long asrModelId,
|
||||
Long summaryModelId,
|
||||
Long promptId,
|
||||
|
|
@ -50,7 +49,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
|||
long resolvedTenantId = tenantId == null ? 0L : tenantId;
|
||||
AiModelVO asrModel = resolveModel("ASR", asrModelId, resolvedTenantId);
|
||||
AiModelVO summaryModel = resolveModel("LLM", summaryModelId, resolvedTenantId);
|
||||
PromptTemplate promptTemplate = resolvePrompt(promptId, resolvedTenantId, userId);
|
||||
PromptTemplate promptTemplate = resolvePrompt(promptId, resolvedTenantId);
|
||||
|
||||
RealtimeMeetingRuntimeProfile profile = new RealtimeMeetingRuntimeProfile();
|
||||
profile.setResolvedAsrModelId(asrModel.getId());
|
||||
|
|
@ -195,7 +194,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
|||
return entity == null ? null : entity.getId();
|
||||
}
|
||||
|
||||
private PromptTemplate resolvePrompt(Long requestedId, Long tenantId, Long userId) {
|
||||
private PromptTemplate resolvePrompt(Long requestedId, Long tenantId) {
|
||||
if (requestedId != null) {
|
||||
PromptTemplate template = promptTemplateService.getById(requestedId);
|
||||
if (template == null) {
|
||||
|
|
@ -205,12 +204,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
|||
return template;
|
||||
}
|
||||
|
||||
PromptTemplate template = promptTemplateService.findEffectiveUserDefaultTemplate(tenantId, userId);
|
||||
if (template != null) {
|
||||
return template;
|
||||
}
|
||||
|
||||
template = promptTemplateService.getOne(new LambdaQueryWrapper<PromptTemplate>()
|
||||
PromptTemplate template = promptTemplateService.getOne(new LambdaQueryWrapper<PromptTemplate>()
|
||||
.eq(PromptTemplate::getStatus, 1)
|
||||
.eq(PromptTemplate::getIsSystem, 1)
|
||||
.and(wrapper -> wrapper.eq(PromptTemplate::getTenantId, tenantId).or().eq(PromptTemplate::getTenantId, 0L))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import com.imeeting.entity.biz.AiTask;
|
|||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.enums.MeetingStatusEnum;
|
||||
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
|
|
@ -164,23 +163,17 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
|
|||
private boolean isAndroidOfflineEmptyUploadFailure(MeetingVO meeting) {
|
||||
return meeting != null
|
||||
&& MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meeting.getMeetingType())
|
||||
&& MeetingTerminalEnum.isCustomTerminalSource(meeting.getMeetingSource())
|
||||
&& hasNoAudioUrl(meeting)
|
||||
&& MeetingConstants.SOURCE_ANDROID.equalsIgnoreCase(meeting.getMeetingSource())
|
||||
&& "FAILED".equalsIgnoreCase(meeting.getAudioSaveStatus());
|
||||
}
|
||||
|
||||
private boolean isAndroidOfflineMeetingWaitingUpload(MeetingVO meeting) {
|
||||
return meeting != null
|
||||
&& MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meeting.getMeetingType())
|
||||
&& MeetingTerminalEnum.isCustomTerminalSource(meeting.getMeetingSource())
|
||||
&& hasNoAudioUrl(meeting)
|
||||
&& MeetingConstants.SOURCE_ANDROID.equalsIgnoreCase(meeting.getMeetingSource())
|
||||
&& !MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(meeting.getOfflineRecordingStatus());
|
||||
}
|
||||
|
||||
private boolean hasNoAudioUrl(MeetingVO meeting) {
|
||||
return meeting.getAudioUrl() == null || meeting.getAudioUrl().isBlank();
|
||||
}
|
||||
|
||||
private boolean isSummarizing(MeetingUnifiedStageContext context) {
|
||||
return isTaskRunning(context.summaryTask())
|
||||
|| isTaskRunning(context.chapterTask())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.imeeting.dto.biz.PromptTemplateDTO;
|
||||
|
|
@ -70,20 +69,9 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||
LambdaQueryWrapper<PromptTemplate> wrapper = buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin);
|
||||
wrapper.like(name != null && !name.isEmpty(), PromptTemplate::getTemplateName, name)
|
||||
.eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category);
|
||||
|
||||
PromptTemplateUserConfig configuredDefault = findUserDefaultConfig(tenantId, userId);
|
||||
Long configuredDefaultId = configuredDefault == null ? null : configuredDefault.getTemplateId();
|
||||
DefaultTemplateSelection defaultSelection = findEffectiveDefaultTemplate(tenantId, userId);
|
||||
PromptTemplate defaultTemplate = defaultSelection == null ? null : defaultSelection.template();
|
||||
if (defaultTemplate == null) {
|
||||
wrapper.orderByAsc(PromptTemplate::getIsSystem)
|
||||
.orderByDesc(PromptTemplate::getTenantId)
|
||||
.orderByDesc(PromptTemplate::getCreatedAt);
|
||||
} else {
|
||||
wrapper.last("ORDER BY CASE WHEN id = " + defaultTemplate.getId()
|
||||
+ " THEN 0 ELSE 1 END, is_system ASC, tenant_id DESC, created_at DESC");
|
||||
}
|
||||
.eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category)
|
||||
.orderByDesc(PromptTemplate::getIsSystem)
|
||||
.orderByDesc(PromptTemplate::getCreatedAt);
|
||||
|
||||
Page<PromptTemplate> page = this.page(new Page<>(current, size), wrapper);
|
||||
List<PromptTemplate> records = page.getRecords();
|
||||
|
|
@ -91,16 +79,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
Map<Long, HotWordGroup> hotWordGroupMap = queryHotWordGroupMap(records.stream().map(PromptTemplate::getHotWordGroupId).toList());
|
||||
|
||||
List<PromptTemplateVO> vos = records.stream()
|
||||
.map(template -> {
|
||||
Integer status = effectiveStatus(template.getIsSystem(), template.getStatus(), userStatusMap.get(template.getId()));
|
||||
PromptTemplateVO vo = toVO(template, status, hotWordGroupMap);
|
||||
boolean isConfiguredPersonalDefault = Objects.equals(configuredDefaultId, template.getId());
|
||||
boolean isEffectiveDefault = defaultTemplate != null && Objects.equals(defaultTemplate.getId(), template.getId());
|
||||
vo.setIsDefault(isEffectiveDefault || isConfiguredPersonalDefault);
|
||||
vo.setDefaultAvailable(isEffectiveDefault);
|
||||
vo.setDefaultScope(isEffectiveDefault ? defaultSelection.scope() : isConfiguredPersonalDefault ? DEFAULT_SCOPE_PERSONAL : null);
|
||||
return vo;
|
||||
})
|
||||
.map(template -> toVO(template, effectiveStatus(template.getIsSystem(), template.getStatus(), userStatusMap.get(template.getId())), hotWordGroupMap))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<List<PromptTemplateVO>> result = new PageResult<>();
|
||||
|
|
@ -118,17 +97,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
throw new IllegalArgumentException("模板不存在");
|
||||
}
|
||||
Map<Long, HotWordGroup> hotWordGroupMap = queryHotWordGroupMap(java.util.Collections.singletonList(template.getHotWordGroupId()));
|
||||
Integer userStatus = queryUserStatusMap(tenantId, userId, java.util.Collections.singletonList(template.getId()))
|
||||
.get(template.getId());
|
||||
Integer status = effectiveStatus(template.getIsSystem(), template.getStatus(), userStatus);
|
||||
PromptTemplateVO vo = toVO(template, status, hotWordGroupMap);
|
||||
PromptTemplateUserConfig configuredDefault = findUserDefaultConfig(tenantId, userId);
|
||||
DefaultTemplateSelection defaultSelection = findEffectiveDefaultTemplate(tenantId, userId);
|
||||
boolean isConfiguredPersonalDefault = configuredDefault != null && Objects.equals(configuredDefault.getTemplateId(), template.getId());
|
||||
boolean isEffectiveDefault = defaultSelection != null && Objects.equals(defaultSelection.template().getId(), template.getId());
|
||||
vo.setIsDefault(isEffectiveDefault || isConfiguredPersonalDefault);
|
||||
vo.setDefaultAvailable(isEffectiveDefault);
|
||||
vo.setDefaultScope(isEffectiveDefault ? defaultSelection.scope() : isConfiguredPersonalDefault ? DEFAULT_SCOPE_PERSONAL : null);
|
||||
PromptTemplateVO vo = toVO(template, template.getStatus(), hotWordGroupMap);
|
||||
vo.setHotWords(resolveHotWords(template.getHotWordGroupId()));
|
||||
return vo;
|
||||
}
|
||||
|
|
@ -186,138 +155,6 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
return effectiveStatus(template.getIsSystem(), template.getStatus(), userStatus) == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean setUserDefaultTemplate(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||
if (Boolean.TRUE.equals(isPlatformAdmin)) {
|
||||
return setSystemDefaultTemplate(templateId, 0L);
|
||||
}
|
||||
if (Boolean.TRUE.equals(isTenantAdmin)) {
|
||||
return setSystemDefaultTemplate(templateId, tenantId);
|
||||
}
|
||||
if (!isTemplateEnabledForUser(templateId, tenantId, userId, isPlatformAdmin, isTenantAdmin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
userConfigMapper.update(null, new LambdaUpdateWrapper<PromptTemplateUserConfig>()
|
||||
.eq(PromptTemplateUserConfig::getTenantId, tenantId)
|
||||
.eq(PromptTemplateUserConfig::getUserId, userId)
|
||||
.eq(PromptTemplateUserConfig::getIsDefault, 1)
|
||||
.set(PromptTemplateUserConfig::getIsDefault, 0));
|
||||
|
||||
PromptTemplateUserConfig existing = findUserConfig(tenantId, userId, templateId);
|
||||
if (existing == null) {
|
||||
PromptTemplateUserConfig entity = new PromptTemplateUserConfig();
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setUserId(userId);
|
||||
entity.setTemplateId(templateId);
|
||||
entity.setStatus(1);
|
||||
entity.setIsDefault(1);
|
||||
return userConfigMapper.insert(entity) > 0;
|
||||
}
|
||||
existing.setIsDefault(1);
|
||||
return userConfigMapper.updateById(existing) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean clearUserDefaultTemplate(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||
if (Boolean.TRUE.equals(isPlatformAdmin)) {
|
||||
return clearSystemDefaultTemplate(templateId, 0L);
|
||||
}
|
||||
if (Boolean.TRUE.equals(isTenantAdmin)) {
|
||||
return clearSystemDefaultTemplate(templateId, tenantId);
|
||||
}
|
||||
userConfigMapper.update(null, new LambdaUpdateWrapper<PromptTemplateUserConfig>()
|
||||
.eq(PromptTemplateUserConfig::getTenantId, tenantId)
|
||||
.eq(PromptTemplateUserConfig::getUserId, userId)
|
||||
.eq(PromptTemplateUserConfig::getTemplateId, templateId)
|
||||
.eq(PromptTemplateUserConfig::getIsDefault, 1)
|
||||
.set(PromptTemplateUserConfig::getIsDefault, 0));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PromptTemplate findEffectiveUserDefaultTemplate(Long tenantId, Long userId) {
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
DefaultTemplateSelection selection = findEffectiveDefaultTemplate(tenantId, userId);
|
||||
return selection == null ? null : selection.template();
|
||||
}
|
||||
|
||||
private boolean setSystemDefaultTemplate(Long templateId, Long scopeTenantId) {
|
||||
PromptTemplate template = this.getById(templateId);
|
||||
if (template == null
|
||||
|| !Integer.valueOf(1).equals(template.getIsSystem())
|
||||
|| !Objects.equals(template.getTenantId(), scopeTenantId)
|
||||
|| !Integer.valueOf(1).equals(template.getStatus())) {
|
||||
return false;
|
||||
}
|
||||
this.update(new LambdaUpdateWrapper<PromptTemplate>()
|
||||
.eq(PromptTemplate::getTenantId, scopeTenantId)
|
||||
.eq(PromptTemplate::getIsSystem, 1)
|
||||
.eq(PromptTemplate::getIsDefault, 1)
|
||||
.set(PromptTemplate::getIsDefault, 0));
|
||||
template.setIsDefault(1);
|
||||
return this.updateById(template);
|
||||
}
|
||||
|
||||
private boolean clearSystemDefaultTemplate(Long templateId, Long scopeTenantId) {
|
||||
return this.update(new LambdaUpdateWrapper<PromptTemplate>()
|
||||
.eq(PromptTemplate::getId, templateId)
|
||||
.eq(PromptTemplate::getTenantId, scopeTenantId)
|
||||
.eq(PromptTemplate::getIsSystem, 1)
|
||||
.eq(PromptTemplate::getIsDefault, 1)
|
||||
.set(PromptTemplate::getIsDefault, 0));
|
||||
}
|
||||
|
||||
private DefaultTemplateSelection findEffectiveDefaultTemplate(Long tenantId, Long userId) {
|
||||
PromptTemplateUserConfig config = findUserDefaultConfig(tenantId, userId);
|
||||
if (config != null && Integer.valueOf(1).equals(config.getStatus())) {
|
||||
PromptTemplate template = findAvailableDefaultTemplate(config.getTemplateId(), tenantId, userId);
|
||||
if (template != null) {
|
||||
return new DefaultTemplateSelection(template, DEFAULT_SCOPE_PERSONAL);
|
||||
}
|
||||
}
|
||||
PromptTemplate template = findAvailableSystemDefaultTemplate(tenantId, tenantId, userId);
|
||||
if (template != null) {
|
||||
return new DefaultTemplateSelection(template, DEFAULT_SCOPE_TENANT);
|
||||
}
|
||||
template = findAvailableSystemDefaultTemplate(0L, tenantId, userId);
|
||||
return template == null ? null : new DefaultTemplateSelection(template, DEFAULT_SCOPE_PLATFORM);
|
||||
}
|
||||
|
||||
private PromptTemplate findAvailableDefaultTemplate(Long templateId, Long tenantId, Long userId) {
|
||||
PromptTemplate template = this.getById(templateId);
|
||||
if (template == null || !Integer.valueOf(1).equals(template.getStatus())) {
|
||||
return null;
|
||||
}
|
||||
if (Integer.valueOf(0).equals(template.getIsSystem()) && !Objects.equals(template.getCreatorId(), userId)) {
|
||||
return null;
|
||||
}
|
||||
return Objects.equals(template.getTenantId(), tenantId) || Long.valueOf(0L).equals(template.getTenantId()) ? template : null;
|
||||
}
|
||||
|
||||
private PromptTemplate findAvailableSystemDefaultTemplate(Long scopeTenantId, Long userTenantId, Long userId) {
|
||||
PromptTemplate template = this.getOne(new LambdaQueryWrapper<PromptTemplate>()
|
||||
.eq(PromptTemplate::getTenantId, scopeTenantId)
|
||||
.eq(PromptTemplate::getIsSystem, 1)
|
||||
.eq(PromptTemplate::getIsDefault, 1)
|
||||
.eq(PromptTemplate::getStatus, 1)
|
||||
.last("LIMIT 1"));
|
||||
if (template == null) {
|
||||
return null;
|
||||
}
|
||||
if (userId == null) {
|
||||
return template;
|
||||
}
|
||||
PromptTemplateUserConfig config = findUserConfig(userTenantId, userId, template.getId());
|
||||
return effectiveStatus(template.getIsSystem(), template.getStatus(), config == null ? null : config.getStatus()) == 1
|
||||
? template
|
||||
: null;
|
||||
}
|
||||
|
||||
private void validateHotWordGroupBinding(Long hotWordGroupId, Long templateTenantId) {
|
||||
if (hotWordGroupId == null) {
|
||||
return;
|
||||
|
|
@ -377,25 +214,6 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
return statusMap;
|
||||
}
|
||||
|
||||
private PromptTemplateUserConfig findUserConfig(Long tenantId, Long userId, Long templateId) {
|
||||
return userConfigMapper.selectOne(new LambdaQueryWrapper<PromptTemplateUserConfig>()
|
||||
.eq(PromptTemplateUserConfig::getTenantId, tenantId)
|
||||
.eq(PromptTemplateUserConfig::getUserId, userId)
|
||||
.eq(PromptTemplateUserConfig::getTemplateId, templateId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private PromptTemplateUserConfig findUserDefaultConfig(Long tenantId, Long userId) {
|
||||
if (tenantId == null || userId == null) {
|
||||
return null;
|
||||
}
|
||||
return userConfigMapper.selectOne(new LambdaQueryWrapper<PromptTemplateUserConfig>()
|
||||
.eq(PromptTemplateUserConfig::getTenantId, tenantId)
|
||||
.eq(PromptTemplateUserConfig::getUserId, userId)
|
||||
.eq(PromptTemplateUserConfig::getIsDefault, 1)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private Map<Long, HotWordGroup> queryHotWordGroupMap(List<Long> hotWordGroupIds) {
|
||||
List<Long> ids = hotWordGroupIds == null ? List.of() : hotWordGroupIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
|
|
@ -452,7 +270,6 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
vo.setDescription(entity.getDescription());
|
||||
vo.setCategory(entity.getCategory());
|
||||
vo.setIsSystem(entity.getIsSystem());
|
||||
vo.setIsTemplateDefault(Integer.valueOf(1).equals(entity.getIsDefault()));
|
||||
vo.setTags(entity.getTags());
|
||||
Long hotWordGroupId = entity.getHotWordGroupId();
|
||||
vo.setHotWordGroupId(hotWordGroupId);
|
||||
|
|
@ -466,11 +283,4 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static final String DEFAULT_SCOPE_PERSONAL = "PERSONAL";
|
||||
private static final String DEFAULT_SCOPE_TENANT = "TENANT";
|
||||
private static final String DEFAULT_SCOPE_PLATFORM = "PLATFORM";
|
||||
|
||||
private record DefaultTemplateSelection(PromptTemplate template, String scope) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,8 @@ import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
|||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||
import com.imeeting.dto.biz.RealtimeSocketSessionData;
|
||||
import com.imeeting.dto.biz.RealtimeSocketSessionVO;
|
||||
import com.imeeting.dto.biz.HotWordGroupVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.service.biz.HotWordGroupService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
||||
|
|
@ -26,8 +21,6 @@ import org.springframework.stereotype.Service;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -40,16 +33,14 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
private final RealtimeMeetingSocketSessionCache socketSessionCache;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final AiModelService aiModelService;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
private final RealtimeAsrChannelFactory realtimeAsrChannelFactory;
|
||||
private final HotWordService hotWordService;
|
||||
private final HotWordGroupService hotWordGroupService;
|
||||
|
||||
@Override
|
||||
public RealtimeSocketSessionVO createSession(Long meetingId, Long asrModelId, String mode, String language,
|
||||
Integer useSpkId, Boolean enablePunctuation, Boolean enableItn,
|
||||
Boolean enableTextRefine, Boolean saveAudio,
|
||||
Long hotWordGroupId, LoginUser loginUser) {
|
||||
List<Map<String, Object>> hotwords, LoginUser loginUser) {
|
||||
if (meetingId == null) {
|
||||
throw new RuntimeException("会议 ID 不能为空");
|
||||
}
|
||||
|
|
@ -58,7 +49,7 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
}
|
||||
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
meetingAccessService.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingTerminalEnum.WEB.getCode());
|
||||
meetingAccessService.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingConstants.SOURCE_WEB);
|
||||
|
||||
realtimeMeetingSessionStateService.initSessionIfAbsent(meetingId, loginUser.getTenantId(), loginUser.getUserId());
|
||||
realtimeMeetingSessionStateService.assertCanOpenSession(meetingId);
|
||||
|
|
@ -77,11 +68,6 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
RealtimeMeetingSessionStatusVO existingStatus = realtimeMeetingSessionStateService.getStatus(meetingId);
|
||||
RealtimeMeetingResumeConfig existingConfig = existingStatus == null ? null : existingStatus.getResumeConfig();
|
||||
|
||||
Long effectiveHotWordGroupId = resolveHotWordGroupId(hotWordGroupId, existingConfig, meeting);
|
||||
List<Map<String, Object>> effectiveHotwords = effectiveHotWordGroupId == null
|
||||
? limitHotwords(existingConfig == null ? List.of() : existingConfig.getHotwords())
|
||||
: resolveGroupHotwords(effectiveHotWordGroupId, loginUser.getTenantId());
|
||||
|
||||
RealtimeMeetingResumeConfig resumeConfig = new RealtimeMeetingResumeConfig();
|
||||
resumeConfig.setAsrModelId(asrModelId);
|
||||
resumeConfig.setMode(mode);
|
||||
|
|
@ -95,8 +81,10 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
resumeConfig.setSpeakerContextId(existingConfig.getSpeakerContextId());
|
||||
resumeConfig.setUpstreamSessionId(existingConfig.getUpstreamSessionId());
|
||||
}
|
||||
List<Map<String, Object>> effectiveHotwords = (hotwords == null || hotwords.isEmpty())
|
||||
? (existingConfig == null ? List.of() : existingConfig.getHotwords())
|
||||
: hotwords;
|
||||
resumeConfig.setHotwords(effectiveHotwords);
|
||||
resumeConfig.setHotWordGroupId(effectiveHotWordGroupId);
|
||||
realtimeMeetingSessionStateService.rememberResumeConfig(meetingId, resumeConfig);
|
||||
|
||||
RealtimeSocketSessionData sessionData = new RealtimeSocketSessionData();
|
||||
|
|
@ -130,40 +118,6 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
return vo;
|
||||
}
|
||||
|
||||
private Long resolveHotWordGroupId(Long requestedGroupId, RealtimeMeetingResumeConfig existingConfig, Meeting meeting) {
|
||||
if (requestedGroupId != null) {
|
||||
return requestedGroupId > 0 ? requestedGroupId : null;
|
||||
}
|
||||
if (existingConfig != null && existingConfig.getHotWordGroupId() != null) {
|
||||
return existingConfig.getHotWordGroupId();
|
||||
}
|
||||
return meeting.getHotWordGroupId();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> resolveGroupHotwords(Long hotWordGroupId, Long tenantId) {
|
||||
boolean visible = hotWordGroupService.listVisibleOptions(tenantId).stream()
|
||||
.map(HotWordGroupVO::getId)
|
||||
.anyMatch(hotWordGroupId::equals);
|
||||
if (!visible) {
|
||||
throw new RuntimeException("热词组不存在或不可用");
|
||||
}
|
||||
return hotWordService.listEnabledByGroupIdIgnoreTenant(hotWordGroupId).stream()
|
||||
.map(this::toRealtimeHotword)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> limitHotwords(List<Map<String, Object>> hotwords) {
|
||||
return hotwords == null ? List.of() : hotwords;
|
||||
}
|
||||
|
||||
private Map<String, Object> toRealtimeHotword(HotWord hotWord) {
|
||||
return Map.of(
|
||||
"hotword", hotWord.getWord(),
|
||||
"weight", BigDecimal.valueOf(hotWord.getWeight() == null ? 20 : hotWord.getWeight())
|
||||
.divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP).doubleValue()
|
||||
);
|
||||
}
|
||||
|
||||
private String resolveRealtimeModelCode(AiModelVO asrModel) {
|
||||
if (asrModel == null) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ public class AndroidPushMessageRetryTask {
|
|||
private final AndroidGatewayPushService androidGatewayPushService;
|
||||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||
|
||||
@Scheduled(fixedDelayString = "${imeeting.android.push.retry-interval-ms:15000}",
|
||||
initialDelayString = "${imeeting.android.push.initial-delay-ms:10000}")
|
||||
@Scheduled(fixedDelayString = "${imeeting.android.push.retry-interval-ms:15000}")
|
||||
public void retryPendingMessages() {
|
||||
taskSecurityContextRunner.callAsPlatformAdmin(() -> {
|
||||
List<AndroidPushMessage> pendingMessages = androidPushMessageService.listPendingMeetingPushMessages();
|
||||
|
|
|
|||
|
|
@ -53,13 +53,6 @@ spring:
|
|||
writetimeout: 5000
|
||||
# 启用调试日志(生产环境建议关闭)
|
||||
debug: true
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migrations
|
||||
# New databases baseline at 0 and execute V1; manually initialized deployments set version to 1.
|
||||
baseline-on-migrate: true
|
||||
baseline-version: ${FLYWAY_BASELINE_VERSION:1}
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
|
|
@ -78,10 +71,6 @@ mybatis-plus:
|
|||
logic-not-delete-value: 0
|
||||
|
||||
unisbase:
|
||||
flyway:
|
||||
base:
|
||||
baseline-enabled: true
|
||||
baseline-version: ${UNIS_BASELINE_VERSION:0.1.0}
|
||||
web:
|
||||
auth-endpoints-enabled: true
|
||||
management-endpoints-enabled: true
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -28,12 +28,12 @@
|
|||
</appender>
|
||||
|
||||
<springProfile name="dev">
|
||||
<logger name="org.flywaydb" level="DEBUG"/>
|
||||
<!-- 4. MyBatis 框架本身日志 -->
|
||||
<logger name="org.apache.ibatis" level="INFO"/>
|
||||
|
||||
<!-- 5. MyBatis Plus -->
|
||||
<logger name="com.baomidou.mybatisplus" level="DEBUG"/>
|
||||
<logger name="io.grpc" level="DEBUG"/>
|
||||
<logger name="io.grpc.netty.shaded.io.grpc.netty" level="DEBUG"/>
|
||||
<logger name="com.imeeting.config.grpc" level="DEBUG"/>
|
||||
<logger name="com.imeeting.grpc" level="DEBUG"/>
|
||||
<logger name="com.imeeting.service.realtime.impl.RealtimeMeetingGrpcSessionServiceImpl" level="DEBUG"/>
|
||||
<logger name="com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl" level="DEBUG"/>
|
||||
</springProfile>
|
||||
|
||||
<root level="INFO">
|
||||
|
|
|
|||
|
|
@ -1,135 +1,71 @@
|
|||
//package com.imeeting.service.biz.impl;
|
||||
//
|
||||
//import com.imeeting.dto.biz.HotWordDTO;
|
||||
//import com.imeeting.dto.biz.HotWordVO;
|
||||
//import com.imeeting.entity.biz.HotWord;
|
||||
//import com.imeeting.entity.biz.HotWordGroup;
|
||||
//import com.imeeting.mapper.biz.HotWordGroupMapper;
|
||||
//import com.unisbase.dto.SysDictItemDTO;
|
||||
//import com.unisbase.service.SysDictItemService;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//
|
||||
//import java.util.List;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
//import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
//import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
//import static org.mockito.ArgumentMatchers.any;
|
||||
//import static org.mockito.Mockito.doAnswer;
|
||||
//import static org.mockito.Mockito.doReturn;
|
||||
//import static org.mockito.Mockito.mock;
|
||||
//import static org.mockito.Mockito.spy;
|
||||
//import static org.mockito.Mockito.when;
|
||||
//
|
||||
//class HotWordServiceImplTest {
|
||||
//
|
||||
// @Test
|
||||
// void saveHotWordShouldRejectWhenGroupLimitReached() {
|
||||
// HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
|
||||
// SysDictItemService sysDictItemService = mock(SysDictItemService.class);
|
||||
// HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper, sysDictItemService));
|
||||
// doReturn(200L).when(service).count(any());
|
||||
//
|
||||
// HotWordGroup group = new HotWordGroup();
|
||||
// group.setId(5L);
|
||||
// group.setTenantId(9L);
|
||||
// group.setGroupName("客户名单");
|
||||
// group.setStatus(1);
|
||||
// when(hotWordGroupMapper.selectById(5L)).thenReturn(group);
|
||||
//
|
||||
// HotWordDTO dto = new HotWordDTO();
|
||||
// dto.setWord("阿里");
|
||||
// dto.setMatchStrategy(1);
|
||||
// dto.setWeight(2);
|
||||
// dto.setStatus(1);
|
||||
// dto.setHotWordGroupId(5L);
|
||||
//
|
||||
// IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
// () -> service.saveHotWord(dto, 7L, 9L));
|
||||
//
|
||||
// assertEquals("热词组最多只能包含 200 个热词", exception.getMessage());
|
||||
// }
|
||||
// @Test
|
||||
// void saveHotWordShouldGeneratePinyinWhenRequestDoesNotProvideIt() {
|
||||
// HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
|
||||
// SysDictItemService sysDictItemService = mock(SysDictItemService.class);
|
||||
// HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper, sysDictItemService));
|
||||
// doAnswer(invocation -> {
|
||||
// HotWord entity = invocation.getArgument(0);
|
||||
// entity.setId(11L);
|
||||
// return true;
|
||||
// }).when(service).save(any(HotWord.class));
|
||||
//
|
||||
// HotWordDTO dto = new HotWordDTO();
|
||||
// dto.setWord("会议");
|
||||
// dto.setMatchStrategy(1);
|
||||
// dto.setWeight(2);
|
||||
// dto.setStatus(1);
|
||||
// dto.setPinyinList(List.of());
|
||||
//
|
||||
// HotWordVO result = service.saveHotWord(dto, 7L, 9L);
|
||||
//
|
||||
// assertFalse(result.getPinyinList().isEmpty());
|
||||
// assertEquals("hui yi", result.getPinyinList().get(0));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void saveHotWordShouldUseConfiguredGroupLimit() {
|
||||
// HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
|
||||
// SysDictItemService sysDictItemService = mock(SysDictItemService.class);
|
||||
// HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper, sysDictItemService));
|
||||
// doReturn(50L).when(service).count(any());
|
||||
//
|
||||
// HotWordGroup group = new HotWordGroup();
|
||||
// group.setId(5L);
|
||||
// group.setTenantId(9L);
|
||||
// group.setStatus(1);
|
||||
// when(hotWordGroupMapper.selectById(5L)).thenReturn(group);
|
||||
//
|
||||
// SysDictItemDTO item = new SysDictItemDTO();
|
||||
// item.setItemValue("50");
|
||||
// when(sysDictItemService.getItemsByTypeCode("biz_hotword_group_limit")).thenReturn(List.of(item));
|
||||
//
|
||||
// HotWordDTO dto = new HotWordDTO();
|
||||
// dto.setWord("阿里");
|
||||
// dto.setMatchStrategy(1);
|
||||
// dto.setWeight(2);
|
||||
// dto.setStatus(1);
|
||||
// dto.setHotWordGroupId(5L);
|
||||
//
|
||||
// IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
// () -> service.saveHotWord(dto, 7L, 9L));
|
||||
//
|
||||
// assertEquals("热词组最多只能包含 50 个热词", exception.getMessage());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void saveHotWordShouldUseDefaultLimitWhenConfiguredValueIsInvalid() {
|
||||
// HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
|
||||
// SysDictItemService sysDictItemService = mock(SysDictItemService.class);
|
||||
// HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper, sysDictItemService));
|
||||
// doReturn(200L).when(service).count(any());
|
||||
//
|
||||
// HotWordGroup group = new HotWordGroup();
|
||||
// group.setId(5L);
|
||||
// group.setTenantId(9L);
|
||||
// group.setStatus(1);
|
||||
// when(hotWordGroupMapper.selectById(5L)).thenReturn(group);
|
||||
//
|
||||
// SysDictItemDTO item = new SysDictItemDTO();
|
||||
// item.setItemValue("50abc");
|
||||
// when(sysDictItemService.getItemsByTypeCode("biz_hotword_group_limit")).thenReturn(List.of(item));
|
||||
//
|
||||
// HotWordDTO dto = new HotWordDTO();
|
||||
// dto.setWord("阿里");
|
||||
// dto.setMatchStrategy(1);
|
||||
// dto.setWeight(2);
|
||||
// dto.setStatus(1);
|
||||
// dto.setHotWordGroupId(5L);
|
||||
//
|
||||
// IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
// () -> service.saveHotWord(dto, 7L, 9L));
|
||||
//
|
||||
// assertEquals("热词组最多只能包含 200 个热词", exception.getMessage());
|
||||
// }
|
||||
//}
|
||||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import com.imeeting.entity.biz.HotWordGroup;
|
||||
import com.imeeting.mapper.biz.HotWordGroupMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class HotWordServiceImplTest {
|
||||
|
||||
@Test
|
||||
void saveHotWordShouldRejectWhenGroupLimitReached() {
|
||||
HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
|
||||
HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper));
|
||||
doReturn(200L).when(service).count(any());
|
||||
|
||||
HotWordGroup group = new HotWordGroup();
|
||||
group.setId(5L);
|
||||
group.setTenantId(9L);
|
||||
group.setGroupName("客户名单");
|
||||
group.setStatus(1);
|
||||
when(hotWordGroupMapper.selectById(5L)).thenReturn(group);
|
||||
|
||||
HotWordDTO dto = new HotWordDTO();
|
||||
dto.setWord("阿里");
|
||||
dto.setMatchStrategy(1);
|
||||
dto.setWeight(2);
|
||||
dto.setStatus(1);
|
||||
dto.setHotWordGroupId(5L);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> service.saveHotWord(dto, 7L, 9L));
|
||||
|
||||
assertEquals("热词组最多只能包含 200 个热词", exception.getMessage());
|
||||
}
|
||||
@Test
|
||||
void saveHotWordShouldGeneratePinyinWhenRequestDoesNotProvideIt() {
|
||||
HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
|
||||
HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper));
|
||||
doAnswer(invocation -> {
|
||||
HotWord entity = invocation.getArgument(0);
|
||||
entity.setId(11L);
|
||||
return true;
|
||||
}).when(service).save(any(HotWord.class));
|
||||
|
||||
HotWordDTO dto = new HotWordDTO();
|
||||
dto.setWord("会议");
|
||||
dto.setMatchStrategy(1);
|
||||
dto.setWeight(2);
|
||||
dto.setStatus(1);
|
||||
dto.setPinyinList(List.of());
|
||||
|
||||
HotWordVO result = service.saveHotWord(dto, 7L, 9L);
|
||||
|
||||
assertFalse(result.getPinyinList().isEmpty());
|
||||
assertEquals("hui yi", result.getPinyinList().get(0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.imeeting.service.biz.impl;
|
|||
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -17,41 +16,33 @@ class MeetingAccessServiceImplTest {
|
|||
|
||||
@Test
|
||||
void allowsRealtimeControlFromSourcePlatform() {
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_REALTIME, MeetingTerminalEnum.CUSTOM_TERMINAL.getCode());
|
||||
LoginUser loginUser = buildLoginUser();
|
||||
|
||||
assertDoesNotThrow(() -> service.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingTerminalEnum.CUSTOM_TERMINAL.getCode()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsCustomTerminalToControlLegacyAndroidRealtimeMeeting() {
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_REALTIME, "ANDROID");
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_REALTIME, MeetingConstants.SOURCE_ANDROID);
|
||||
LoginUser loginUser = buildLoginUser();
|
||||
|
||||
assertDoesNotThrow(() -> service.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingTerminalEnum.CUSTOM_TERMINAL.getCode()));
|
||||
assertDoesNotThrow(() -> service.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingConstants.SOURCE_ANDROID));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCrossPlatformRealtimeControl() {
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_REALTIME, MeetingTerminalEnum.CUSTOM_TERMINAL.getCode());
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_REALTIME, MeetingConstants.SOURCE_ANDROID);
|
||||
LoginUser loginUser = buildLoginUser();
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> service.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingTerminalEnum.WEB.getCode()));
|
||||
() -> service.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingConstants.SOURCE_WEB));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsRealtimeControlForOfflineMeeting() {
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_OFFLINE, MeetingTerminalEnum.WEB.getCode());
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_OFFLINE, MeetingConstants.SOURCE_WEB);
|
||||
LoginUser loginUser = buildLoginUser();
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> service.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingTerminalEnum.WEB.getCode()));
|
||||
() -> service.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingConstants.SOURCE_WEB));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsParticipantToViewAndExportButNotEdit() {
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_OFFLINE, MeetingTerminalEnum.WEB.getCode());
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_OFFLINE, MeetingConstants.SOURCE_WEB);
|
||||
meeting.setParticipants("201,202,203");
|
||||
LoginUser participant = new LoginUser(202L, 100L, "participant", false, false, null);
|
||||
|
||||
|
|
@ -63,7 +54,7 @@ class MeetingAccessServiceImplTest {
|
|||
|
||||
@Test
|
||||
void allowsTenantAdminToEditMeeting() {
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_OFFLINE, MeetingTerminalEnum.WEB.getCode());
|
||||
Meeting meeting = buildMeeting(MeetingConstants.TYPE_OFFLINE, MeetingConstants.SOURCE_WEB);
|
||||
LoginUser tenantAdmin = new LoginUser(300L, 100L, "tenant-admin", false, true, null);
|
||||
|
||||
assertDoesNotThrow(() -> service.assertCanEditMeeting(meeting, tenantAdmin));
|
||||
|
|
|
|||
|
|
@ -1,327 +1,327 @@
|
|||
//package com.imeeting.service.biz.impl;
|
||||
//
|
||||
//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
//import com.imeeting.dto.biz.AiModelVO;
|
||||
//import com.imeeting.dto.biz.HotWordGroupVO;
|
||||
//import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||
//import com.imeeting.entity.biz.AsrModel;
|
||||
//import com.imeeting.entity.biz.HotWord;
|
||||
//import com.imeeting.entity.biz.LlmModel;
|
||||
//import com.imeeting.entity.biz.PromptTemplate;
|
||||
//import com.imeeting.mapper.biz.AsrModelMapper;
|
||||
//import com.imeeting.mapper.biz.LlmModelMapper;
|
||||
//import com.imeeting.service.biz.AiModelService;
|
||||
//import com.imeeting.service.biz.HotWordGroupService;
|
||||
//import com.imeeting.service.biz.HotWordService;
|
||||
//import com.imeeting.service.biz.PromptTemplateService;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//
|
||||
//import java.util.Arrays;
|
||||
//import java.util.List;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
//import static org.junit.jupiter.api.Assertions.assertIterableEquals;
|
||||
//import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
//import static org.mockito.ArgumentMatchers.any;
|
||||
//import static org.mockito.Mockito.mock;
|
||||
//import static org.mockito.Mockito.when;
|
||||
//
|
||||
//class MeetingRuntimeProfileResolverImplTest {
|
||||
//
|
||||
// @Test
|
||||
// void resolveShouldUseRequestedResourcesAndNormalizeHotWords() {
|
||||
// AiModelService aiModelService = mock(AiModelService.class);
|
||||
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
// HotWordService hotWordService = mock(HotWordService.class);
|
||||
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
// aiModelService,
|
||||
// promptTemplateService,
|
||||
// hotWordGroupService,
|
||||
// hotWordService,
|
||||
// mock(AsrModelMapper.class),
|
||||
// mock(LlmModelMapper.class)
|
||||
// );
|
||||
//
|
||||
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
// when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||
// when(promptTemplateService.getById(33L)).thenReturn(enabledPrompt(33L, 1L, "Summary Prompt"));
|
||||
//
|
||||
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
// 1L,
|
||||
// 11L,
|
||||
// 22L,
|
||||
// 33L,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// Boolean.TRUE,
|
||||
// Boolean.TRUE,
|
||||
// null,
|
||||
// Arrays.asList(" alpha ", "", "alpha", "beta", null)
|
||||
// );
|
||||
//
|
||||
// assertEquals(11L, profile.getResolvedAsrModelId());
|
||||
// assertEquals("ASR-Model", profile.getResolvedAsrModelName());
|
||||
// assertEquals(22L, profile.getResolvedSummaryModelId());
|
||||
// assertEquals("LLM-Model", profile.getResolvedSummaryModelName());
|
||||
// assertEquals(33L, profile.getResolvedPromptId());
|
||||
// assertEquals("Summary Prompt", profile.getResolvedPromptName());
|
||||
// assertEquals("2pass", profile.getResolvedMode());
|
||||
// assertEquals("auto", profile.getResolvedLanguage());
|
||||
// assertEquals(1, profile.getResolvedUseSpkId());
|
||||
// assertEquals(Boolean.TRUE, profile.getResolvedEnablePunctuation());
|
||||
// assertEquals(Boolean.TRUE, profile.getResolvedEnableItn());
|
||||
// assertEquals(Boolean.TRUE, profile.getResolvedEnableTextRefine());
|
||||
// assertEquals(Boolean.TRUE, profile.getResolvedSaveAudio());
|
||||
// assertIterableEquals(List.of("alpha", "beta"), profile.getResolvedHotWords());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void resolveShouldRejectCrossTenantModel() {
|
||||
// AiModelService aiModelService = mock(AiModelService.class);
|
||||
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
// HotWordService hotWordService = mock(HotWordService.class);
|
||||
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
// aiModelService,
|
||||
// promptTemplateService,
|
||||
// hotWordGroupService,
|
||||
// hotWordService,
|
||||
// mock(AsrModelMapper.class),
|
||||
// mock(LlmModelMapper.class)
|
||||
// );
|
||||
//
|
||||
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 2L, "ASR-Model"));
|
||||
//
|
||||
// assertThrows(RuntimeException.class, () -> resolver.resolve(
|
||||
// 1L,
|
||||
// 11L,
|
||||
// 22L,
|
||||
// 33L,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// List.of()
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void resolveShouldUseTemplateBoundGroupWhenNoExplicitHotWords() {
|
||||
// AiModelService aiModelService = mock(AiModelService.class);
|
||||
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
// HotWordService hotWordService = mock(HotWordService.class);
|
||||
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
// aiModelService,
|
||||
// promptTemplateService,
|
||||
// hotWordGroupService,
|
||||
// hotWordService,
|
||||
// mock(AsrModelMapper.class),
|
||||
// mock(LlmModelMapper.class)
|
||||
// );
|
||||
//
|
||||
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
// when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||
// PromptTemplate template = enabledPrompt(33L, 0L, "Platform Prompt");
|
||||
// template.setHotWordGroupId(99L);
|
||||
// when(promptTemplateService.getById(33L)).thenReturn(template);
|
||||
//
|
||||
// HotWord hotWord1 = new HotWord();
|
||||
// hotWord1.setWord("OpenAI");
|
||||
// HotWord hotWord2 = new HotWord();
|
||||
// hotWord2.setWord("Codex");
|
||||
// when(hotWordService.listEnabledByGroupIdIgnoreTenant(99L)).thenReturn(List.of(hotWord1, hotWord2));
|
||||
//
|
||||
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
// 1L,
|
||||
// 11L,
|
||||
// 22L,
|
||||
// 33L,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// Boolean.FALSE,
|
||||
// Boolean.FALSE,
|
||||
// null,
|
||||
// null
|
||||
// );
|
||||
//
|
||||
// assertEquals(99L, profile.getResolvedHotWordGroupId());
|
||||
// assertIterableEquals(List.of("OpenAI", "Codex"), profile.getResolvedHotWords());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void resolveShouldFallbackToFirstEnabledModelUsingSortOrder() {
|
||||
// AiModelService aiModelService = mock(AiModelService.class);
|
||||
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
// HotWordService hotWordService = mock(HotWordService.class);
|
||||
// AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
|
||||
// LlmModelMapper llmModelMapper = mock(LlmModelMapper.class);
|
||||
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
// aiModelService,
|
||||
// promptTemplateService,
|
||||
// hotWordGroupService,
|
||||
// hotWordService,
|
||||
// asrModelMapper,
|
||||
// llmModelMapper
|
||||
// );
|
||||
//
|
||||
// when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(null);
|
||||
// when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(null);
|
||||
// when(asrModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(asrEntity(11L));
|
||||
// when(llmModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(llmEntity(22L));
|
||||
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
// when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||
// when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
|
||||
//
|
||||
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
// 1L,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// List.of()
|
||||
// );
|
||||
//
|
||||
// assertEquals(11L, profile.getResolvedAsrModelId());
|
||||
// assertEquals(22L, profile.getResolvedSummaryModelId());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void resolveShouldUseTenantDefaultLlmFromAiModelService() {
|
||||
// AiModelService aiModelService = mock(AiModelService.class);
|
||||
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
// HotWordService hotWordService = mock(HotWordService.class);
|
||||
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
// aiModelService,
|
||||
// promptTemplateService,
|
||||
// hotWordGroupService,
|
||||
// hotWordService,
|
||||
// mock(AsrModelMapper.class),
|
||||
// mock(LlmModelMapper.class)
|
||||
// );
|
||||
//
|
||||
// when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
// when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(enabledModel(77L, 0L, "Tenant Default LLM"));
|
||||
// when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
|
||||
//
|
||||
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
// 1L,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// List.of()
|
||||
// );
|
||||
//
|
||||
// assertEquals(77L, profile.getResolvedSummaryModelId());
|
||||
// assertEquals("Tenant Default LLM", profile.getResolvedSummaryModelName());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void resolveShouldPreferExplicitHotWordGroupOverTemplateBinding() {
|
||||
// AiModelService aiModelService = mock(AiModelService.class);
|
||||
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
// HotWordService hotWordService = mock(HotWordService.class);
|
||||
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
// aiModelService,
|
||||
// promptTemplateService,
|
||||
// hotWordGroupService,
|
||||
// hotWordService,
|
||||
// mock(AsrModelMapper.class),
|
||||
// mock(LlmModelMapper.class)
|
||||
// );
|
||||
//
|
||||
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
// when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||
// PromptTemplate template = enabledPrompt(33L, 1L, "Summary Prompt");
|
||||
// template.setHotWordGroupId(99L);
|
||||
// when(promptTemplateService.getById(33L)).thenReturn(template);
|
||||
//
|
||||
// HotWordGroupVO explicitGroup = new HotWordGroupVO();
|
||||
// explicitGroup.setId(88L);
|
||||
// when(hotWordGroupService.listVisibleOptions(1L)).thenReturn(List.of(explicitGroup));
|
||||
//
|
||||
// HotWord hotWord = new HotWord();
|
||||
// hotWord.setWord("override");
|
||||
// when(hotWordService.listEnabledByGroupIdIgnoreTenant(88L)).thenReturn(List.of(hotWord));
|
||||
//
|
||||
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
// 1L,
|
||||
// 11L,
|
||||
// 22L,
|
||||
// 33L,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// 88L,
|
||||
// List.of()
|
||||
// );
|
||||
//
|
||||
// assertEquals(88L, profile.getResolvedHotWordGroupId());
|
||||
// assertIterableEquals(List.of("override"), profile.getResolvedHotWords());
|
||||
// }
|
||||
//
|
||||
// private AiModelVO enabledModel(Long id, Long tenantId, String name) {
|
||||
// AiModelVO model = new AiModelVO();
|
||||
// model.setId(id);
|
||||
// model.setTenantId(tenantId);
|
||||
// model.setModelName(name);
|
||||
// model.setStatus(1);
|
||||
// return model;
|
||||
// }
|
||||
//
|
||||
// private PromptTemplate enabledPrompt(Long id, Long tenantId, String name) {
|
||||
// PromptTemplate template = new PromptTemplate();
|
||||
// template.setId(id);
|
||||
// template.setTenantId(tenantId);
|
||||
// template.setTemplateName(name);
|
||||
// template.setStatus(1);
|
||||
// return template;
|
||||
// }
|
||||
//
|
||||
// private AsrModel asrEntity(Long id) {
|
||||
// AsrModel entity = new AsrModel();
|
||||
// entity.setId(id);
|
||||
// entity.setStatus(1);
|
||||
// return entity;
|
||||
// }
|
||||
//
|
||||
// private LlmModel llmEntity(Long id) {
|
||||
// LlmModel entity = new LlmModel();
|
||||
// entity.setId(id);
|
||||
// entity.setStatus(1);
|
||||
// return entity;
|
||||
// }
|
||||
//}
|
||||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.dto.biz.HotWordGroupVO;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||
import com.imeeting.entity.biz.AsrModel;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import com.imeeting.entity.biz.LlmModel;
|
||||
import com.imeeting.entity.biz.PromptTemplate;
|
||||
import com.imeeting.mapper.biz.AsrModelMapper;
|
||||
import com.imeeting.mapper.biz.LlmModelMapper;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.service.biz.HotWordGroupService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MeetingRuntimeProfileResolverImplTest {
|
||||
|
||||
@Test
|
||||
void resolveShouldUseRequestedResourcesAndNormalizeHotWords() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
hotWordGroupService,
|
||||
hotWordService,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
||||
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||
when(promptTemplateService.getById(33L)).thenReturn(enabledPrompt(33L, 1L, "Summary Prompt"));
|
||||
|
||||
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
1L,
|
||||
11L,
|
||||
22L,
|
||||
33L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Boolean.TRUE,
|
||||
Boolean.TRUE,
|
||||
null,
|
||||
Arrays.asList(" alpha ", "", "alpha", "beta", null)
|
||||
);
|
||||
|
||||
assertEquals(11L, profile.getResolvedAsrModelId());
|
||||
assertEquals("ASR-Model", profile.getResolvedAsrModelName());
|
||||
assertEquals(22L, profile.getResolvedSummaryModelId());
|
||||
assertEquals("LLM-Model", profile.getResolvedSummaryModelName());
|
||||
assertEquals(33L, profile.getResolvedPromptId());
|
||||
assertEquals("Summary Prompt", profile.getResolvedPromptName());
|
||||
assertEquals("2pass", profile.getResolvedMode());
|
||||
assertEquals("auto", profile.getResolvedLanguage());
|
||||
assertEquals(1, profile.getResolvedUseSpkId());
|
||||
assertEquals(Boolean.TRUE, profile.getResolvedEnablePunctuation());
|
||||
assertEquals(Boolean.TRUE, profile.getResolvedEnableItn());
|
||||
assertEquals(Boolean.TRUE, profile.getResolvedEnableTextRefine());
|
||||
assertEquals(Boolean.TRUE, profile.getResolvedSaveAudio());
|
||||
assertIterableEquals(List.of("alpha", "beta"), profile.getResolvedHotWords());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveShouldRejectCrossTenantModel() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
hotWordGroupService,
|
||||
hotWordService,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
||||
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 2L, "ASR-Model"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> resolver.resolve(
|
||||
1L,
|
||||
11L,
|
||||
22L,
|
||||
33L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveShouldUseTemplateBoundGroupWhenNoExplicitHotWords() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
hotWordGroupService,
|
||||
hotWordService,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
||||
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||
PromptTemplate template = enabledPrompt(33L, 0L, "Platform Prompt");
|
||||
template.setHotWordGroupId(99L);
|
||||
when(promptTemplateService.getById(33L)).thenReturn(template);
|
||||
|
||||
HotWord hotWord1 = new HotWord();
|
||||
hotWord1.setWord("OpenAI");
|
||||
HotWord hotWord2 = new HotWord();
|
||||
hotWord2.setWord("Codex");
|
||||
when(hotWordService.listEnabledByGroupIdIgnoreTenant(99L)).thenReturn(List.of(hotWord1, hotWord2));
|
||||
|
||||
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
1L,
|
||||
11L,
|
||||
22L,
|
||||
33L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Boolean.FALSE,
|
||||
Boolean.FALSE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
assertEquals(99L, profile.getResolvedHotWordGroupId());
|
||||
assertIterableEquals(List.of("OpenAI", "Codex"), profile.getResolvedHotWords());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveShouldFallbackToFirstEnabledModelUsingSortOrder() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
|
||||
LlmModelMapper llmModelMapper = mock(LlmModelMapper.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
hotWordGroupService,
|
||||
hotWordService,
|
||||
asrModelMapper,
|
||||
llmModelMapper
|
||||
);
|
||||
|
||||
when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(null);
|
||||
when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(null);
|
||||
when(asrModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(asrEntity(11L));
|
||||
when(llmModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(llmEntity(22L));
|
||||
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||
when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
|
||||
|
||||
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
1L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertEquals(11L, profile.getResolvedAsrModelId());
|
||||
assertEquals(22L, profile.getResolvedSummaryModelId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveShouldUseTenantDefaultLlmFromAiModelService() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
hotWordGroupService,
|
||||
hotWordService,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
||||
when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(enabledModel(77L, 0L, "Tenant Default LLM"));
|
||||
when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
|
||||
|
||||
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
1L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertEquals(77L, profile.getResolvedSummaryModelId());
|
||||
assertEquals("Tenant Default LLM", profile.getResolvedSummaryModelName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveShouldPreferExplicitHotWordGroupOverTemplateBinding() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
hotWordGroupService,
|
||||
hotWordService,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
||||
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||
PromptTemplate template = enabledPrompt(33L, 1L, "Summary Prompt");
|
||||
template.setHotWordGroupId(99L);
|
||||
when(promptTemplateService.getById(33L)).thenReturn(template);
|
||||
|
||||
HotWordGroupVO explicitGroup = new HotWordGroupVO();
|
||||
explicitGroup.setId(88L);
|
||||
when(hotWordGroupService.listVisibleOptions(1L)).thenReturn(List.of(explicitGroup));
|
||||
|
||||
HotWord hotWord = new HotWord();
|
||||
hotWord.setWord("override");
|
||||
when(hotWordService.listEnabledByGroupIdIgnoreTenant(88L)).thenReturn(List.of(hotWord));
|
||||
|
||||
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||
1L,
|
||||
11L,
|
||||
22L,
|
||||
33L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
88L,
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertEquals(88L, profile.getResolvedHotWordGroupId());
|
||||
assertIterableEquals(List.of("override"), profile.getResolvedHotWords());
|
||||
}
|
||||
|
||||
private AiModelVO enabledModel(Long id, Long tenantId, String name) {
|
||||
AiModelVO model = new AiModelVO();
|
||||
model.setId(id);
|
||||
model.setTenantId(tenantId);
|
||||
model.setModelName(name);
|
||||
model.setStatus(1);
|
||||
return model;
|
||||
}
|
||||
|
||||
private PromptTemplate enabledPrompt(Long id, Long tenantId, String name) {
|
||||
PromptTemplate template = new PromptTemplate();
|
||||
template.setId(id);
|
||||
template.setTenantId(tenantId);
|
||||
template.setTemplateName(name);
|
||||
template.setStatus(1);
|
||||
return template;
|
||||
}
|
||||
|
||||
private AsrModel asrEntity(Long id) {
|
||||
AsrModel entity = new AsrModel();
|
||||
entity.setId(id);
|
||||
entity.setStatus(1);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private LlmModel llmEntity(Long id) {
|
||||
LlmModel entity = new LlmModel();
|
||||
entity.setId(id);
|
||||
entity.setStatus(1);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,14 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_441_732)">
|
||||
<rect width="32" height="32" rx="8" fill="url(#paint0_linear_441_732)"/>
|
||||
<path
|
||||
d="M20 22C20 19.7909 18.2091 18 16 18C13.7909 18 12 19.7909 12 22V11C12 13.2091 13.7909 15 16 15C18.2091 15 20 13.2091 20 11V22Z"
|
||||
fill="url(#paint1_linear_441_732)"/>
|
||||
<rect x="5" y="6" width="7" height="20" rx="3.5" fill="url(#paint2_linear_441_732)"/>
|
||||
<rect x="20" y="8" width="7" height="16" rx="3.5" fill="url(#paint3_linear_441_732)"/>
|
||||
<rect width="32" height="32" rx="10" fill="#2D6BFF"/>
|
||||
<g transform="translate(16 15.6) rotate(35)" stroke="white" fill="none" stroke-width="1.8">
|
||||
<rect x="-5.2" y="-10.2" width="10.4" height="10.4" rx="5.2"/>
|
||||
<path d="M-2.2 0.2 H2.2 L1.1 10.4 H-1.1 Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_441_732" x1="16" y1="2.45643e-07" x2="24.2424" y2="32"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FBFBFB"/>
|
||||
<stop offset="1" stop-color="#D7E4F0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_441_732" x1="19.625" y1="15.625" x2="12.125" y2="15.75"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0088FF"/>
|
||||
<stop offset="1" stop-color="#06CEEB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_441_732" x1="8.5" y1="6" x2="8.5" y2="26" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.408628" stop-color="#0088FF"/>
|
||||
<stop offset="1" stop-color="#006AFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_441_732" x1="23.5" y1="8" x2="23.5" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#20E3FF"/>
|
||||
<stop offset="0.919108" stop-color="#00CAE7"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_441_732">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
d="M12.5 22.6 C14.2 21.6 16 21.6 17.8 22.6 S21.4 23.8 23.2 22.6"
|
||||
stroke="white"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 543 B |
|
|
@ -1,4 +1,4 @@
|
|||
import http from "../http";
|
||||
import http from "../http";
|
||||
|
||||
export interface HotWordVO {
|
||||
id: number;
|
||||
|
|
@ -36,18 +36,6 @@ export interface HotWordBatchGroupDTO {
|
|||
hotWordGroupId?: number;
|
||||
}
|
||||
|
||||
export interface HotWordBatchCreateDTO {
|
||||
tenantId?: number;
|
||||
words: string[];
|
||||
hotWordGroupId?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface HotWordBatchCreateResultVO {
|
||||
createdCount: number;
|
||||
existingWords: string[];
|
||||
}
|
||||
|
||||
export const getHotWordPage = (params: {
|
||||
current: number;
|
||||
size: number;
|
||||
|
|
@ -76,13 +64,6 @@ export const saveHotWord = (data: HotWordDTO) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const createHotWordBatch = (data: HotWordBatchCreateDTO) => {
|
||||
return http.post<{ code: string; data: HotWordBatchCreateResultVO; msg: string }>(
|
||||
"/api/biz/hotword/batch",
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
export const updateHotWord = (data: HotWordDTO) => {
|
||||
return http.put<{ code: string; data: HotWordVO; msg: string }>(
|
||||
"/api/biz/hotword",
|
||||
|
|
@ -110,6 +91,3 @@ export const getPinyinSuggestion = (word: string) => {
|
|||
{ params: { word } }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface HotWordGroupVO {
|
|||
|
||||
export interface HotWordGroupDTO {
|
||||
id?: number;
|
||||
tenantId?: number;
|
||||
groupName: string;
|
||||
status: number;
|
||||
remark?: string;
|
||||
|
|
@ -24,6 +25,7 @@ export const getHotWordGroupPage = (params: {
|
|||
size: number;
|
||||
name?: string;
|
||||
status?: number;
|
||||
tenantId?: number;
|
||||
}) => {
|
||||
return http.get<{ code: string; data: { records: HotWordGroupVO[]; total: number }; msg: string }>(
|
||||
"/api/biz/hotword-group/page",
|
||||
|
|
@ -31,9 +33,10 @@ export const getHotWordGroupPage = (params: {
|
|||
);
|
||||
};
|
||||
|
||||
export const getHotWordGroupOptions = () => {
|
||||
export const getHotWordGroupOptions = (tenantId?: number) => {
|
||||
return http.get<{ code: string; data: HotWordGroupVO[]; msg: string }>(
|
||||
"/api/biz/hotword-group/options",
|
||||
{ params: { tenantId } }
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -51,8 +54,9 @@ export const updateHotWordGroup = (data: HotWordGroupDTO) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const deleteHotWordGroup = (id: number) => {
|
||||
export const deleteHotWordGroup = (id: number, tenantId?: number) => {
|
||||
return http.delete<{ code: string; data: boolean; msg: string }>(
|
||||
`/api/biz/hotword-group/${id}`
|
||||
`/api/biz/hotword-group/${id}`,
|
||||
{ params: { tenantId } }
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,6 @@ const MEETING_UPLOAD_FLOW_TIMEOUT = 600000;
|
|||
const MEETING_DETAIL_TIMEOUT = 120000;
|
||||
|
||||
export type SummaryDetailLevel = "DETAILED" | "STANDARD" | "BRIEF";
|
||||
export type MeetingSource = "WINDOWS" | "MACOS" | "KYLIN" | "UOS" | "HARMONYOS" | "WEB" | "CUSTOM_TERMINAL" | "ANDROID";
|
||||
|
||||
export interface MeetingParticipant {
|
||||
userId: number;
|
||||
displayName: string | null;
|
||||
}
|
||||
|
||||
export interface MeetingCreateConfig {
|
||||
offlineEnabled: boolean;
|
||||
|
|
@ -32,19 +26,16 @@ export interface MeetingVO {
|
|||
meetingTime: string;
|
||||
participants: string;
|
||||
participantIds?: number[];
|
||||
participantUsers?: MeetingParticipant[];
|
||||
tags: string;
|
||||
audioUrl: string;
|
||||
playbackAudioUrl?: string;
|
||||
meetingType?: "OFFLINE" | "REALTIME";
|
||||
meetingSource?: MeetingSource;
|
||||
meetingSource?: "WEB" | "ANDROID";
|
||||
sourceDeviceCode?: string;
|
||||
sourceDeviceMode?: "PUBLIC" | "PRIVATE";
|
||||
summaryDetailLevel?: SummaryDetailLevel;
|
||||
summaryModelId: number;
|
||||
summaryModelName?: string;
|
||||
promptId?: number;
|
||||
promptName?: string;
|
||||
hotWordGroupId?: number;
|
||||
hotWordGroupName?: string;
|
||||
aiCatalogEnabled?: boolean;
|
||||
|
|
@ -239,7 +230,7 @@ export interface RealtimeSocketSessionRequest {
|
|||
enableItn?: boolean;
|
||||
enableTextRefine?: boolean;
|
||||
saveAudio?: boolean;
|
||||
hotWordGroupId?: number;
|
||||
hotwords?: Array<{ hotword: string; weight: number }>;
|
||||
}
|
||||
|
||||
export interface RealtimeMeetingSessionStatus {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ export interface PromptTemplateVO {
|
|||
hotWordGroupId?: number;
|
||||
hotWordGroupName?: string;
|
||||
hotWords?: string[];
|
||||
isDefault?: boolean;
|
||||
defaultScope?: "PERSONAL" | "TENANT" | "PLATFORM";
|
||||
isTemplateDefault?: boolean;
|
||||
defaultAvailable?: boolean;
|
||||
usageCount: number;
|
||||
promptContent: string;
|
||||
status: number;
|
||||
|
|
@ -82,11 +78,3 @@ export const updatePromptStatus = (id: number, status: number) => {
|
|||
{ params: { status } }
|
||||
);
|
||||
};
|
||||
|
||||
export const setPromptDefault = (id: number) => {
|
||||
return http.put<{ code: string; data: boolean; msg: string }>(`/api/biz/prompt/${id}/default`);
|
||||
};
|
||||
|
||||
export const clearPromptDefault = (id: number) => {
|
||||
return http.delete<{ code: string; data: boolean; msg: string }>(`/api/biz/prompt/${id}/default`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import {
|
|||
uploadAudio,
|
||||
} from "../../api/business/meeting";
|
||||
import { getPromptPage, type PromptTemplateVO } from "../../api/business/prompt";
|
||||
import {useHotWordGroupLimit} from "../../hooks/useHotWordGroupLimit";
|
||||
import type { SysUser } from "../../types";
|
||||
import "./MeetingCreateDrawer.css";
|
||||
|
||||
|
|
@ -83,7 +82,7 @@ type RealtimeMeetingSessionDraft = {
|
|||
enableItn: boolean;
|
||||
enableTextRefine: boolean;
|
||||
saveAudio: boolean;
|
||||
hotWordGroupId?: number;
|
||||
hotwords: Array<{ hotword: string; weight: number }>;
|
||||
};
|
||||
|
||||
function resolveAvailableCreateTypes(config: MeetingCreateConfig): MeetingCreateType[] {
|
||||
|
|
@ -122,7 +121,6 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
onSuccess,
|
||||
}) => {
|
||||
const { message } = App.useApp();
|
||||
const {limit: hotWordGroupLimit} = useHotWordGroupLimit();
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
|
|
@ -202,28 +200,24 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
const asrRecords = asrRes.data?.data?.records || [];
|
||||
const llmRecords = llmRes.data?.data?.records || [];
|
||||
const hotwordRecords = hotwordRes.data?.data?.records || [];
|
||||
const activeLlmModels = llmRecords.filter((item: AiModelVO) => item.status === 1);
|
||||
const activePrompts = promptRecords.filter((item: PromptTemplateVO) => item.status === 1);
|
||||
|
||||
setCreateConfig(nextConfig);
|
||||
setConfigLoaded(true);
|
||||
setType(nextType);
|
||||
setAsrModels(asrRecords.filter((item: AiModelVO) => item.status === 1));
|
||||
setLlmModels(activeLlmModels);
|
||||
setLlmModels(llmRecords.filter((item: AiModelVO) => item.status === 1));
|
||||
setPrompts(activePrompts);
|
||||
setHotwordList(hotwordRecords.filter((item: HotWordVO) => item.status === 1));
|
||||
setHotWordGroups((hotWordGroupRes.data.data || []).filter((item: HotWordGroupVO) => item.status === 1));
|
||||
setUserList(users || []);
|
||||
|
||||
const defaultPrompt = activePrompts.find((item: PromptTemplateVO) => item.isDefault && item.defaultAvailable) || activePrompts[0];
|
||||
const defaultSummaryModel = activeLlmModels.find((item: AiModelVO) => item.id === defaultLlm.data.data?.id)
|
||||
|| activeLlmModels.find((item: AiModelVO) => item.isDefault === 1)
|
||||
|| activeLlmModels[0];
|
||||
const defaultPrompt = activePrompts[0];
|
||||
form.setFieldsValue({
|
||||
title: nextType === "upload" ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`,
|
||||
meetingTime: dayjs(),
|
||||
asrModelId: defaultAsr.data.data?.id,
|
||||
summaryModelId: defaultSummaryModel?.id,
|
||||
summaryModelId: defaultLlm.data.data?.id,
|
||||
promptId: defaultPrompt?.id,
|
||||
hotWordGroupId: defaultPrompt?.hotWordGroupId ?? 0,
|
||||
summaryDetailLevel: "STANDARD",
|
||||
|
|
@ -301,7 +295,6 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
message.warning("当前入口已关闭,已切换到可用创建方式");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "upload" && !audioUrl) {
|
||||
message.error("请先上传录音文件");
|
||||
return;
|
||||
|
|
@ -315,14 +308,17 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (!values.promptId) {
|
||||
message.error("总结模板为空");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const { hostUserId, ...meetingValues } = values;
|
||||
const selectedHotWords = meetingValues.hotWordGroupId == null || meetingValues.hotWordGroupId === 0
|
||||
? undefined
|
||||
: hotwordList
|
||||
.filter((item) => item.hotWordGroupId === meetingValues.hotWordGroupId)
|
||||
.map((item) => item.word)
|
||||
.filter((word) => !!word?.trim());
|
||||
|
||||
if (type === "upload") {
|
||||
await createMeeting({
|
||||
...meetingValues,
|
||||
|
|
@ -332,6 +328,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
participants: meetingValues.participants?.join(","),
|
||||
tags: meetingValues.tags?.join(","),
|
||||
summaryDetailLevel: meetingValues.summaryDetailLevel as SummaryDetailLevel,
|
||||
hotWords: selectedHotWords,
|
||||
});
|
||||
message.success("会议发起成功");
|
||||
onSuccess();
|
||||
|
|
@ -339,6 +336,13 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const selectedHotwords = hotwordList
|
||||
.filter((item) => item.hotWordGroupId === meetingValues.hotWordGroupId && meetingValues.hotWordGroupId !== 0)
|
||||
.map((item) => ({
|
||||
hotword: item.word,
|
||||
weight: Number(item.weight || 2) / 10,
|
||||
}));
|
||||
|
||||
const payload: CreateRealtimeMeetingCommand = {
|
||||
...meetingValues,
|
||||
...(hostUserId != null ? { hostUserId } : {}),
|
||||
|
|
@ -353,6 +357,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
enableItn: meetingValues.enableItn !== false,
|
||||
enableTextRefine: !!meetingValues.enableTextRefine,
|
||||
saveAudio: !!meetingValues.saveAudio,
|
||||
hotWords: selectedHotWords,
|
||||
};
|
||||
|
||||
const res = await createRealtimeMeeting(payload);
|
||||
|
|
@ -370,7 +375,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
enableItn: values.enableItn !== false,
|
||||
enableTextRefine: !!values.enableTextRefine,
|
||||
saveAudio: !!values.saveAudio,
|
||||
hotWordGroupId: meetingValues.hotWordGroupId || undefined,
|
||||
hotwords: selectedHotwords,
|
||||
};
|
||||
|
||||
sessionStorage.setItem(getSessionKey(createdMeeting.id), JSON.stringify(sessionDraft));
|
||||
|
|
@ -512,12 +517,9 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="promptId"
|
||||
label="总结模板"
|
||||
>
|
||||
<Form.Item name="promptId" label="总结模板" rules={[{ required: true }]}>
|
||||
{prompts.length > 15 ? (
|
||||
<Select allowClear placeholder="请选择总结模板" showSearch optionFilterProp="children">
|
||||
<Select placeholder="请选择总结模板" showSearch optionFilterProp="children">
|
||||
{prompts.map(p => <Option key={p.id} value={p.id}>{p.templateName}</Option>)}
|
||||
</Select>
|
||||
) : (
|
||||
|
|
@ -545,15 +547,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|||
<Row gutter={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="hotWordGroupId" label="热词组" tooltip={selectedPrompt?.hotWordGroupName ? `默认跟随模板:${selectedPrompt.hotWordGroupName}` : "模板未绑定热词组时可手动选择"} extra={watchedHotWordGroupId != null ? "创建会议时会优先使用这里选中的热词组" : undefined}>
|
||||
<Select
|
||||
placeholder={selectedPrompt?.hotWordGroupId ? "默认已带出模板热词组,可按需修改" : "请选择热词组"}
|
||||
options={[{
|
||||
label: "不使用热词组",
|
||||
value: 0
|
||||
}, ...hotWordGroups.map((item) => ({
|
||||
label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`,
|
||||
value: item.id
|
||||
}))]}/>
|
||||
<Select placeholder={selectedPrompt?.hotWordGroupId ? "默认已带出模板热词组,可按需修改" : "请选择热词组"} options={[{ label: "不使用热词组", value: 0 }, ...hotWordGroups.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
|
|
|
|||
|
|
@ -276,7 +276,6 @@
|
|||
"botCredentialHint": "Use this credential pair to access /mcp with X-Bot-Id and X-Bot-Secret.",
|
||||
"botCredentialHintDesc": "The secret is shown only after generation. Store it securely after copying.",
|
||||
"botBindStatus": "Binding Status",
|
||||
"mcpAddress": "MCP Address",
|
||||
"botBound": "Bound",
|
||||
"botUnbound": "Not Generated",
|
||||
"botSecretHidden": "Hidden. Generate or reset to get a new secret.",
|
||||
|
|
|
|||
|
|
@ -276,7 +276,6 @@
|
|||
"botCredentialHint": "使用这组凭证通过 X-Bot-Id 和 X-Bot-Secret 访问 /mcp。",
|
||||
"botCredentialHintDesc": "Secret 只会在生成后显示一次,请复制后妥善保管。",
|
||||
"botBindStatus": "绑定状态",
|
||||
"mcpAddress": "MCP 地址",
|
||||
"botBound": "已绑定",
|
||||
"botUnbound": "未生成",
|
||||
"botSecretHidden": "已隐藏。如需查看新的 Secret,请重新生成。",
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@
|
|||
|
||||
.permissions-name-cell {
|
||||
display: flex;
|
||||
width: 80%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
App,
|
||||
AutoComplete,
|
||||
|
|
@ -248,7 +248,7 @@ const AiModels: React.FC = () => {
|
|||
const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []);
|
||||
const models = Array.isArray(rawModels) ? rawModels : [];
|
||||
setRemoteModels(models);
|
||||
message.success(`已获取 ${models.length} 个模型`);
|
||||
message.success(`获取到 ${models.length} 个模型`);
|
||||
} finally {
|
||||
setFetchLoading(false);
|
||||
}
|
||||
|
|
@ -372,7 +372,7 @@ const AiModels: React.FC = () => {
|
|||
|
||||
const values = await form.validateFields(["provider", "baseUrl"]);
|
||||
if (String(values.provider || "").toLowerCase() !== "local") {
|
||||
message.warning("仅本地 ASR 模型支持连通性测试");
|
||||
message.warning("只有本地 ASR 支持该连通性测试");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -400,28 +400,28 @@ const AiModels: React.FC = () => {
|
|||
const handleTenantToggle = async (record: AiModelVO, checked: boolean) => {
|
||||
if (checked) {
|
||||
await tenantEnableModel(record.id, activeType);
|
||||
message.success(activeType === "ASR" ? "已启用当前 ASR 模型" : "已启用当前 LLM 模型");
|
||||
message.success(activeType === "ASR" ? "已切换当前 ASR" : "已启用当前 LLM");
|
||||
} else {
|
||||
await tenantDisableModel(record.id, activeType);
|
||||
message.success(activeType === "ASR" ? "已停用当前 ASR 模型" : "已停用当前 LLM 模型");
|
||||
message.success(activeType === "ASR" ? "已关闭当前 ASR" : "已关闭当前 LLM");
|
||||
}
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
const handlePlatformStatusToggle = async (record: AiModelVO, checked: boolean) => {
|
||||
await updatePlatformModelStatus(record.id, activeType, checked ? 1 : 0);
|
||||
message.success(checked ? `平台级 ${activeType} 已启用` : `平台级 ${activeType} 已禁用`);
|
||||
message.success(checked ? `平台级 ${activeType} 已启用` : `平台级 ${activeType} 已禁用`);
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
const handleSyncCurrentAsr = async () => {
|
||||
await syncCurrentAsrSpeakers();
|
||||
message.success("当前 ASR 声纹已同步");
|
||||
message.success("已提交后台同步任务");
|
||||
};
|
||||
|
||||
const handleSetTenantDefault = async (record: AiModelVO) => {
|
||||
await setTenantDefaultModel(record.id, "LLM");
|
||||
message.success("已设置为默认 LLM");
|
||||
message.success("已设置为默认 LLM");
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
|
|
@ -433,16 +433,16 @@ const AiModels: React.FC = () => {
|
|||
render: (text: string, record: AiModelVO) => (
|
||||
<Space>
|
||||
{text}
|
||||
{record.isDefault === 1 && <Tag color="gold">系统默认</Tag>}
|
||||
{record.tenantDefault === 1 && <Tag color="blue">租户默认</Tag>}
|
||||
{record.isDefault === 1 && <Tag color="gold">系统默认</Tag>}
|
||||
{record.tenantDefault === 1 && <Tag color="blue">租户默认</Tag>}
|
||||
{record.tenantId === 0 && (
|
||||
<Tooltip title="平台透传模型">
|
||||
<Tooltip title="平台透传模型">
|
||||
<SafetyCertificateOutlined style={{ color: "#52c41a" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{record.scope && (
|
||||
<Tag bordered={false} color={record.scope === "PLATFORM" ? "geekblue" : "default"}>
|
||||
{record.scope === "PLATFORM" ? "平台级" : "租户级"}
|
||||
{record.scope === "PLATFORM" ? "平台级" : "租户级"}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
|
|
@ -458,7 +458,7 @@ const AiModels: React.FC = () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
title: "模型编码",
|
||||
title: "模型编码",
|
||||
dataIndex: "modelCode",
|
||||
key: "modelCode",
|
||||
},
|
||||
|
|
@ -486,8 +486,8 @@ const AiModels: React.FC = () => {
|
|||
return (
|
||||
<Switch
|
||||
checked={record.tenantEnabled === 1}
|
||||
checkedChildren={activeType === "ASR" ? "已启用" : "已启用"}
|
||||
unCheckedChildren={activeType === "ASR" ? "未启用" : "已停用"}
|
||||
checkedChildren={activeType === "ASR" ? "当前生效" : "已启用"}
|
||||
unCheckedChildren={activeType === "ASR" ? "未启用" : "已关闭"}
|
||||
disabled={status !== 1}
|
||||
onChange={(checked) => void handleTenantToggle(record, checked)}
|
||||
/>
|
||||
|
|
@ -504,7 +504,7 @@ const AiModels: React.FC = () => {
|
|||
<Space>
|
||||
{canSetDefault && (
|
||||
<Button type="link" onClick={() => void handleSetTenantDefault(record)}>
|
||||
{record.tenantDefault === 1 ? "默认 LLM" : "设为默认"}
|
||||
{record.tenantDefault === 1 ? "默认 LLM" : "设为默认"}
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
|
|
@ -513,7 +513,7 @@ const AiModels: React.FC = () => {
|
|||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Popconfirm title="确认删除吗?" onConfirm={() => handleDelete(record)}>
|
||||
<Popconfirm title="确定删除吗?" onConfirm={() => handleDelete(record)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
|
|
@ -528,11 +528,11 @@ const AiModels: React.FC = () => {
|
|||
const leftActions = (
|
||||
<Space wrap>
|
||||
<Button type="primary" icon={<PlusOutlined/>} onClick={() => openDrawer()}>
|
||||
新增模型
|
||||
新增模型
|
||||
</Button>
|
||||
{activeType === "ASR" && (
|
||||
<Button icon={<SyncOutlined/>} onClick={() => void handleSyncCurrentAsr()}>
|
||||
同步当前 ASR 声纹
|
||||
同步当前 ASR 声纹
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
|
|
@ -542,7 +542,7 @@ const AiModels: React.FC = () => {
|
|||
<PageContainer title={null} className="ai-models-page">
|
||||
<SectionCard
|
||||
title="AI 模型配置"
|
||||
description="管理 ASR 语音识别模型和 LLM 大语言模型"
|
||||
description="管理 ASR 语音识别和 LLM 大语言模型。"
|
||||
tabs={
|
||||
<Tabs
|
||||
activeKey={activeType}
|
||||
|
|
@ -551,8 +551,8 @@ const AiModels: React.FC = () => {
|
|||
setCurrent(1);
|
||||
}}
|
||||
items={[
|
||||
{key: "ASR", label: "ASR 模型"},
|
||||
{key: "LLM", label: "LLM 模型"},
|
||||
{ key: "ASR", label: "ASR 模型" },
|
||||
{ key: "LLM", label: "LLM 模型" },
|
||||
]}
|
||||
size="middle"
|
||||
type="card"
|
||||
|
|
@ -566,7 +566,7 @@ const AiModels: React.FC = () => {
|
|||
rightActions={
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder="搜索模型名称"
|
||||
placeholder="搜索模型名称"
|
||||
prefix={<SearchOutlined />}
|
||||
className="ai-models-search"
|
||||
onSearch={(value) => {
|
||||
|
|
@ -603,7 +603,7 @@ const AiModels: React.FC = () => {
|
|||
width={600}
|
||||
open={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
title={<Title level={4} style={{margin: 0}}>{editingId ? "编辑模型" : "新增模型"}</Title>}
|
||||
title={<Title level={4} style={{ margin: 0 }}>{editingId ? "编辑模型" : "新增模型"}</Title>}
|
||||
forceRender
|
||||
extra={
|
||||
<Space>
|
||||
|
|
@ -621,7 +621,7 @@ const AiModels: React.FC = () => {
|
|||
|
||||
<Form.Item label="模型类型">
|
||||
<Tag color={activeType === "ASR" ? "blue" : "purple"}>
|
||||
{activeType === "ASR" ? "语音识别 (ASR)" : "大语言模型 (LLM)"}
|
||||
{activeType === "ASR" ? "语音识别 (ASR)" : "大语言模型 (LLM)"}
|
||||
</Tag>
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -629,19 +629,19 @@ const AiModels: React.FC = () => {
|
|||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="modelName"
|
||||
label="模型名称"
|
||||
rules={[{required: true, message: "请输入显示名称"}, {max: 15, message: "模型名称不能超过15个字符"}]}
|
||||
label="显示名称"
|
||||
rules={[{ required: true, message: "请输入显示名称" }]}
|
||||
>
|
||||
<Input onChange={() => {
|
||||
modelNameAutoFilledRef.current = false;
|
||||
}} maxLength={15} showCount/>
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label="提供商"
|
||||
rules={[{required: true, message: "请选择提供商"}]}
|
||||
rules={[{ required: true, message: "请选择提供商" }]}
|
||||
>
|
||||
<Select allowClear placeholder="请选择">
|
||||
{providers.map((item) => (
|
||||
|
|
@ -656,7 +656,7 @@ const AiModels: React.FC = () => {
|
|||
|
||||
<Row gutter={16} className="app-responsive-form-row">
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="sortOrder" label="排序">
|
||||
<Form.Item name="sortOrder" label="排序值">
|
||||
<InputNumber min={0} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
@ -664,7 +664,7 @@ const AiModels: React.FC = () => {
|
|||
|
||||
{!isTencentProvider && (
|
||||
<>
|
||||
<Form.Item name="baseUrl" label="Base URL" rules={[{required: true, message: "请输入 Base URL"}]}>
|
||||
<Form.Item name="baseUrl" label="Base URL" rules={[{required: true, message: "请输入 Base URL"}]}>
|
||||
<Input placeholder="https://api.example.com"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="apiKey" label="API Key">
|
||||
|
|
@ -686,10 +686,10 @@ const AiModels: React.FC = () => {
|
|||
</Divider>
|
||||
|
||||
<Form.Item
|
||||
label="模型编码"
|
||||
label="模型编码"
|
||||
required={activeType === "LLM"}
|
||||
hidden={activeType === "ASR" && isTencentProvider}
|
||||
tooltip="可从远程列表选择,也可手动输入;该值会作为模型编码传给后端"
|
||||
tooltip="可从远程列表选择,也可手动输入;该值会作为模型编码传给后端"
|
||||
>
|
||||
<Space.Compact style={{ width: "100%" }}>
|
||||
<Form.Item
|
||||
|
|
@ -709,18 +709,18 @@ const AiModels: React.FC = () => {
|
|||
isLocalProvider || String(option?.value || "").toLowerCase().includes(inputValue.toLowerCase())
|
||||
}
|
||||
>
|
||||
<Input allowClear placeholder="请输入或选择模型编码"/>
|
||||
<Input allowClear placeholder="可选择或手动输入模型编码"/>
|
||||
</AutoComplete>
|
||||
</Form.Item>
|
||||
{!isTencentProvider && (
|
||||
<Button icon={<SyncOutlined spin={fetchLoading}/>} onClick={handleFetchRemote} style={{width: 100}}>
|
||||
刷新
|
||||
刷新
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="wsUrl" label="WebSocket 地址"
|
||||
<Form.Item name="wsUrl" label="WebSocket 地址"
|
||||
hidden={!(activeType === "ASR" && createConfig.realtimeEnabled)}>
|
||||
<Input placeholder="wss://api.example.com/v1/ws" />
|
||||
</Form.Item>
|
||||
|
|
@ -728,7 +728,7 @@ const AiModels: React.FC = () => {
|
|||
{activeType === "ASR" && isLocalProvider && (
|
||||
<Row gutter={16} hidden className="app-responsive-form-row">
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="svThreshold" label="声纹阈值">
|
||||
<Form.Item name="svThreshold" label="声纹阈值">
|
||||
<InputNumber min={0} max={1} step={0.01} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
@ -738,7 +738,7 @@ const AiModels: React.FC = () => {
|
|||
{activeType === "ASR" && isTencentProvider && (
|
||||
<Row gutter={16} className="app-responsive-form-row">
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="tencentAppId" label="App ID" rules={[{required: true, message: "请输入 App ID"}]}>
|
||||
<Form.Item name="tencentAppId" label="App ID" rules={[{required: true, message: "请输入 App ID"}]}>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
@ -755,15 +755,15 @@ const AiModels: React.FC = () => {
|
|||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="tencentOfflineModelCode" label="离线识别模型"
|
||||
rules={[{required: true, message: "请输入离线识别模型"}]}>
|
||||
<Input placeholder="例如:16k_zh"/>
|
||||
<Form.Item name="tencentOfflineModelCode" label="离线识别模型"
|
||||
rules={[{required: true, message: "请输入离线识别模型"}]}>
|
||||
<Input placeholder="例如:16k_zh"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="tencentRealtimeModelCode" label="实时识别模型"
|
||||
rules={[{required: true, message: "请输入实时识别模型"}]}>
|
||||
<Input placeholder="例如:16k_zh_realtime"/>
|
||||
<Form.Item name="tencentRealtimeModelCode" label="实时识别模型"
|
||||
rules={[{required: true, message: "请输入实时识别模型"}]}>
|
||||
<Input placeholder="例如:16k_zh_realtime"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
@ -790,7 +790,7 @@ const AiModels: React.FC = () => {
|
|||
name="max_tokens"
|
||||
label="max_tokens"
|
||||
rules={[
|
||||
{required: true, message: "请输入 max_tokens"},
|
||||
{ required: true, message: "请输入 max_tokens" },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
|
|
@ -827,7 +827,7 @@ const AiModels: React.FC = () => {
|
|||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="statusChecked" label="状态" valuePropName="checked">
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" disabled={Boolean(isDefaultChecked)}/>
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" disabled={Boolean(isDefaultChecked)} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
App,
|
||||
Badge,
|
||||
|
|
@ -32,7 +32,6 @@ import {
|
|||
} from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDict } from "../../hooks/useDict";
|
||||
import {useHotWordGroupLimit} from "../../hooks/useHotWordGroupLimit";
|
||||
import {
|
||||
deleteHotWord,
|
||||
getHotWordPage,
|
||||
|
|
@ -98,7 +97,6 @@ const HotWords: React.FC = () => {
|
|||
const [groupForm] = Form.useForm<HotWordGroupFormValues>();
|
||||
const [bulkGroupForm] = Form.useForm<BulkGroupFormValues>();
|
||||
const { items: categories } = useDict("biz_hotword_category");
|
||||
const {limit: hotWordGroupLimit} = useHotWordGroupLimit();
|
||||
const userProfile = useMemo(() => {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
return profileStr ? JSON.parse(profileStr) : {};
|
||||
|
|
@ -114,7 +112,7 @@ const HotWords: React.FC = () => {
|
|||
const [searchWord, setSearchWord] = useState("");
|
||||
const [searchCategory, setSearchCategory] = useState<string | undefined>(undefined);
|
||||
const [searchGroupId, setSearchGroupId] = useState<number | undefined>(undefined);
|
||||
const [hotWordGroupFilter, setHotWordGroupFilter] = useState<HotWordGroupFilter>("ungrouped");
|
||||
const [hotWordGroupFilter, setHotWordGroupFilter] = useState<HotWordGroupFilter>("all");
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
|
|
@ -198,7 +196,7 @@ const HotWords: React.FC = () => {
|
|||
};
|
||||
|
||||
const loadGroupOptions = async () => {
|
||||
const res = await getHotWordGroupOptions();
|
||||
const res = await getHotWordGroupOptions(isPlatformAdmin ? activeTenantId : undefined);
|
||||
setGroupOptions(res.data?.data || []);
|
||||
};
|
||||
|
||||
|
|
@ -210,6 +208,7 @@ const HotWords: React.FC = () => {
|
|||
size: groupSize,
|
||||
name: groupSearchName || undefined,
|
||||
status: groupSearchStatus,
|
||||
tenantId: isPlatformAdmin ? activeTenantId : undefined,
|
||||
});
|
||||
setGroupData(res.data?.data?.records || []);
|
||||
setGroupTotal(res.data?.data?.total || 0);
|
||||
|
|
@ -316,10 +315,10 @@ const HotWords: React.FC = () => {
|
|||
const values = await groupForm.validateFields();
|
||||
setGroupSubmitLoading(true);
|
||||
if (editingGroupId) {
|
||||
await updateHotWordGroup({...values, id: editingGroupId});
|
||||
await updateHotWordGroup({ ...values, id: editingGroupId, tenantId: isPlatformAdmin ? activeTenantId : undefined });
|
||||
message.success("热词组更新成功");
|
||||
} else {
|
||||
await saveHotWordGroup(values);
|
||||
await saveHotWordGroup({ ...values, tenantId: isPlatformAdmin ? activeTenantId : undefined });
|
||||
message.success("热词组创建成功");
|
||||
}
|
||||
setGroupEditorVisible(false);
|
||||
|
|
@ -331,7 +330,7 @@ const HotWords: React.FC = () => {
|
|||
|
||||
const handleDeleteGroup = async (id: number, e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
await deleteHotWordGroup(id);
|
||||
await deleteHotWordGroup(id, isPlatformAdmin ? activeTenantId : undefined);
|
||||
message.success("热词组删除成功");
|
||||
if (searchGroupId === id) {
|
||||
setSearchGroupId(undefined);
|
||||
|
|
@ -360,7 +359,7 @@ const HotWords: React.FC = () => {
|
|||
if (assignedHotWordCount > 0) {
|
||||
Modal.confirm({
|
||||
title: "确认修改热词组?",
|
||||
content: `当前选择的热词中,有 ${assignedHotWordCount} 个已分配热词组。继续后,这些热词的原分组会被后续选择的目标热词组覆盖。`,
|
||||
content: `当前选择的热词中,有 ${assignedHotWordCount} 个已分配热词组。继续后,这些热词的原分组会被你后续选择的目标热词组覆盖。`,
|
||||
okText: "继续修改",
|
||||
cancelText: "取消",
|
||||
onOk: openEditor,
|
||||
|
|
@ -402,36 +401,34 @@ const HotWords: React.FC = () => {
|
|||
setSearchCategory(undefined);
|
||||
setSearchGroupId(undefined);
|
||||
setSelectedGroupName(undefined);
|
||||
setHotWordGroupFilter("ungrouped");
|
||||
setHotWordGroupFilter("all");
|
||||
bulkGroupForm.resetFields();
|
||||
setBulkGroupEditorVisible(false);
|
||||
setCurrent(1);
|
||||
void fetchData({current: 1, word: "", category: null, groupFilter: "ungrouped", searchGroupId: null});
|
||||
void fetchData({ current: 1, word: "", category: null, groupFilter: "all", searchGroupId: null });
|
||||
};
|
||||
|
||||
const handleSelectGroup = (item: GroupListItem) => {
|
||||
setSearchGroupId(item.id);
|
||||
setSelectedGroupName(item.id ? item.groupName : undefined);
|
||||
setHotWordGroupFilter(item.id ?? "ungrouped");
|
||||
setHotWordGroupFilter(item.id ?? "all");
|
||||
setCurrent(1);
|
||||
};
|
||||
|
||||
const hotWordGroupTitle = hotWordGroupFilter === "ungrouped"
|
||||
? "未分组"
|
||||
: typeof hotWordGroupFilter === "number"
|
||||
? selectedGroupName || groupData.find((item) => item.id === hotWordGroupFilter)?.groupName || groupNameMap[hotWordGroupFilter] || "热词列表"
|
||||
: "热词列表";
|
||||
const hotWordGroupTitle = searchGroupId
|
||||
? selectedGroupName || groupData.find((item) => item.id === searchGroupId)?.groupName || groupNameMap[searchGroupId] || "热词列表"
|
||||
: "全部热词";
|
||||
|
||||
const groupFilterOptions = useMemo(
|
||||
() => [
|
||||
{ label: "全部词组", value: "all" as const },
|
||||
{label: "未分组", value: "ungrouped" as const},
|
||||
{ label: "未分配", value: "ungrouped" as const },
|
||||
...groupOptions.map((item) => ({ label: item.groupName, value: item.id })),
|
||||
],
|
||||
[groupOptions]
|
||||
);
|
||||
|
||||
const groupListData: GroupListItem[] = [{id: undefined, groupName: "未分组"}, ...groupData];
|
||||
const groupListData: GroupListItem[] = [{ id: undefined, groupName: "全部热词" }, ...groupData];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
|
@ -561,7 +558,7 @@ const HotWords: React.FC = () => {
|
|||
loading={groupLoading}
|
||||
dataSource={groupListData}
|
||||
renderItem={(item) => {
|
||||
const isSelected = item.id ? hotWordGroupFilter === item.id : hotWordGroupFilter === "ungrouped";
|
||||
const isSelected = searchGroupId === item.id;
|
||||
const actions = [];
|
||||
if (item.id) {
|
||||
actions.push(
|
||||
|
|
@ -608,14 +605,13 @@ const HotWords: React.FC = () => {
|
|||
item.id
|
||||
? (
|
||||
<span className="hotwords-group-item__desc">
|
||||
<Tag
|
||||
color={item.hotWordCount >= hotWordGroupLimit ? "red" : item.status === 1 ? "processing" : "default"}>
|
||||
{item.hotWordCount}/{hotWordGroupLimit}
|
||||
<Tag color={item.hotWordCount >= 200 ? "red" : item.status === 1 ? "processing" : "default"}>
|
||||
{item.hotWordCount}/200
|
||||
</Tag>
|
||||
<span>{item.remark || "暂无备注"}</span>
|
||||
</span>
|
||||
)
|
||||
: "查看未分组热词"
|
||||
: "查看所有热词"
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
|
|
@ -693,8 +689,7 @@ const HotWords: React.FC = () => {
|
|||
className="hotwords-search__category"
|
||||
options={categories.map((c) => ({ label: c.itemLabel, value: c.itemValue }))}
|
||||
/>
|
||||
<Button onClick={handleResetFilters}
|
||||
disabled={selectedHotWordIds.length === 0 && searchWord === "" && !searchCategory && !searchGroupId && hotWordGroupFilter === "ungrouped"}>
|
||||
<Button onClick={handleResetFilters} disabled={selectedHotWordIds.length === 0 && searchWord === "" && !searchCategory && !searchGroupId && hotWordGroupFilter === "all"}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
|
|
@ -769,10 +764,7 @@ const HotWords: React.FC = () => {
|
|||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item name="hotWordGroupId" label="所属热词组">
|
||||
<Select placeholder="请选择热词组" allowClear options={groupOptions.map((item) => ({
|
||||
label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`,
|
||||
value: item.id
|
||||
}))}/>
|
||||
<Select placeholder="请选择热词组" allowClear options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
@ -812,11 +804,8 @@ const HotWords: React.FC = () => {
|
|||
destroyOnHidden
|
||||
>
|
||||
<Form form={groupForm} layout="vertical" className="hotwords-modal-form">
|
||||
<Form.Item name="groupName" label="热词组名称" rules={[{required: true, message: "请输入热词组名称"}, {
|
||||
max: 15,
|
||||
message: "热词组名称不能超过15个字符"
|
||||
}]}>
|
||||
<Input placeholder="例如:项目术语、客户名单" maxLength={15} showCount/>
|
||||
<Form.Item name="groupName" label="热词组名称" rules={[{ required: true, message: "请输入热词组名称" }]}>
|
||||
<Input placeholder="例如:项目术语、客户名单" maxLength={100} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select>
|
||||
|
|
@ -849,10 +838,7 @@ const HotWords: React.FC = () => {
|
|||
placeholder="请选择热词组"
|
||||
options={[
|
||||
{ label: "未分组", value: 0 },
|
||||
...groupOptions.map((item) => ({
|
||||
label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`,
|
||||
value: item.id
|
||||
})),
|
||||
...groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id })),
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
|
|
|||
|
|
@ -49,12 +49,10 @@ import {
|
|||
updateSpeakerInfo,
|
||||
} from '../../api/business/meeting';
|
||||
import { getAiModelDefault, getAiModelPage, AiModelVO } from '../../api/business/aimodel';
|
||||
import {createHotWordBatch} from '../../api/business/hotword';
|
||||
import {getHotWordGroupOptions, type HotWordGroupVO} from '../../api/business/hotwordGroup';
|
||||
import { getHotWordPage, getPinyinSuggestion, saveHotWord } from '../../api/business/hotword';
|
||||
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
|
||||
import { listUsers } from '../../api';
|
||||
import { useDict } from '../../hooks/useDict';
|
||||
import {useHotWordGroupLimit} from '../../hooks/useHotWordGroupLimit';
|
||||
import { SysUser } from '../../types';
|
||||
import PageContainer from "../../components/shared/PageContainer";
|
||||
import SectionCard from "../../components/shared/SectionCard";
|
||||
|
|
@ -250,20 +248,18 @@ const parseBulletList = (content?: string | null) =>
|
|||
const parseOverviewSection = (markdown: string) =>
|
||||
extractSection(markdown, ['全文概要', '概要', '摘要', '概览']) || markdown.replace(/^---[\s\S]*?---/, '').trim();
|
||||
|
||||
const isValidKeyword = (value: string) => value.trim() !== '' && value.trim() !== '无';
|
||||
|
||||
const parseKeywordsSection = (markdown: string, tags: string) => {
|
||||
const section = extractSection(markdown, ['关键词', '关键字', '标签']);
|
||||
const fromSection = parseBulletList(section)
|
||||
.flatMap((line) => line.split(/[,,、/]/))
|
||||
.map((item) => item.trim())
|
||||
.filter(isValidKeyword);
|
||||
.filter(Boolean);
|
||||
|
||||
if (fromSection.length) {
|
||||
return Array.from(new Set(fromSection)).slice(0, 12);
|
||||
}
|
||||
|
||||
return Array.from(new Set((tags || '').split(',').map((item) => item.trim()).filter(isValidKeyword))).slice(0, 12);
|
||||
return Array.from(new Set((tags || '').split(',').map((item) => item.trim()).filter(Boolean))).slice(0, 12);
|
||||
};
|
||||
|
||||
const buildMeetingAnalysis = (
|
||||
|
|
@ -284,7 +280,7 @@ const buildMeetingAnalysis = (
|
|||
return {
|
||||
overview: String(parsed.overview || '').trim(),
|
||||
keywords: Array.from(
|
||||
new Set((Array.isArray(parsed.keywords) ? parsed.keywords : []).map((item) => String(item).trim()).filter(isValidKeyword)),
|
||||
new Set((Array.isArray(parsed.keywords) ? parsed.keywords : []).map((item) => String(item).trim()).filter(Boolean)),
|
||||
).slice(0, 12),
|
||||
chapters: chapters
|
||||
.map((item: any) => ({
|
||||
|
|
@ -1215,7 +1211,6 @@ const ActiveTranscriptRow = React.memo<ActiveTranscriptRowProps>(({
|
|||
|
||||
const MeetingDetail: React.FC = () => {
|
||||
const { message } = App.useApp();
|
||||
const {limit: hotWordGroupLimit, loading: hotWordGroupLimitLoading} = useHotWordGroupLimit();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
|
|
@ -1232,10 +1227,6 @@ const MeetingDetail: React.FC = () => {
|
|||
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||
const [summaryDraft, setSummaryDraft] = useState('');
|
||||
const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
|
||||
const [hotWordGroupModalOpen, setHotWordGroupModalOpen] = useState(false);
|
||||
const [hotWordGroupOptions, setHotWordGroupOptions] = useState<HotWordGroupVO[]>([]);
|
||||
const [selectedHotWordGroupId, setSelectedHotWordGroupId] = useState<number | undefined>();
|
||||
const [hotWordGroupLoading, setHotWordGroupLoading] = useState(false);
|
||||
const [workspaceTab, setWorkspaceTab] = useState<WorkspaceTab>('transcript');
|
||||
const [addingHotwords, setAddingHotwords] = useState(false);
|
||||
const [editingTranscriptId, setEditingTranscriptId] = useState<number | null>(null);
|
||||
|
|
@ -1297,20 +1288,15 @@ const MeetingDetail: React.FC = () => {
|
|||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
const userProfile = useMemo(() => {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
return profileStr ? JSON.parse(profileStr) : {};
|
||||
}, []);
|
||||
const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
|
||||
const isPlatformAdmin = userProfile.isPlatformAdmin === true;
|
||||
|
||||
const analysis = useMemo(
|
||||
() => buildMeetingAnalysis(meeting?.analysis, meeting?.summaryContent, meeting?.tags || ''),
|
||||
[meeting?.analysis, meeting?.summaryContent, meeting?.tags],
|
||||
);
|
||||
const expandKeywords = true;
|
||||
const expandKeywords = false;
|
||||
const visibleKeywords = expandKeywords ? analysis.keywords : analysis.keywords.slice(0, 9);
|
||||
const meetingTags = useMemo(
|
||||
() => (meeting?.tags?.split(',').map((item) => item.trim()).filter(isValidKeyword) || []),
|
||||
() => (meeting?.tags?.split(',').map((item) => item.trim()).filter(Boolean) || []),
|
||||
[meeting?.tags],
|
||||
);
|
||||
const discussionItems = useMemo(() => {
|
||||
|
|
@ -1402,23 +1388,25 @@ const MeetingDetail: React.FC = () => {
|
|||
return buildMeetingPreviewUrl(meetingShareBaseUrl, meetingId);
|
||||
}, [meetingShareBaseUrl, meeting?.id, id]);
|
||||
const summaryModelDisplayName = useMemo(() => {
|
||||
if (meeting?.summaryModelName?.trim()) {
|
||||
return meeting.summaryModelName.trim();
|
||||
const matchedModel = llmModels.find((item) => item.id === meeting?.summaryModelId);
|
||||
if (matchedModel?.modelName) {
|
||||
return matchedModel.modelName;
|
||||
}
|
||||
if (meeting?.summaryModelId) {
|
||||
return `模型 #${meeting.summaryModelId}`;
|
||||
}
|
||||
return '未配置';
|
||||
}, [meeting?.summaryModelId, meeting?.summaryModelName]);
|
||||
}, [llmModels, meeting?.summaryModelId]);
|
||||
const promptDisplayName = useMemo(() => {
|
||||
if (meeting?.promptName?.trim()) {
|
||||
return meeting.promptName.trim();
|
||||
const matchedPrompt = prompts.find((item) => item.id === meeting?.promptId);
|
||||
if (matchedPrompt?.templateName) {
|
||||
return matchedPrompt.templateName;
|
||||
}
|
||||
if (meeting?.promptId) {
|
||||
return `模板 #${meeting.promptId}`;
|
||||
}
|
||||
return '未配置';
|
||||
}, [meeting?.promptId, meeting?.promptName]);
|
||||
}, [meeting?.promptId, prompts]);
|
||||
const hotWordGroupDisplayName = useMemo(() => {
|
||||
if (meeting?.hotWordGroupName?.trim()) {
|
||||
return meeting.hotWordGroupName.trim();
|
||||
|
|
@ -1594,6 +1582,7 @@ const MeetingDetail: React.FC = () => {
|
|||
useEffect(() => {
|
||||
if (!id) return;
|
||||
fetchData(Number(id));
|
||||
loadAiConfigs();
|
||||
loadUsers();
|
||||
}, [id, fetchData]);
|
||||
|
||||
|
|
@ -1659,13 +1648,11 @@ const MeetingDetail: React.FC = () => {
|
|||
getPromptPage({ current: 1, size: 100 }),
|
||||
getAiModelDefault('LLM'),
|
||||
]);
|
||||
const models = (modelRes.data?.data?.records || []).filter((item) => item.status === 1);
|
||||
const promptTemplates = (promptRes.data?.data?.records || []).filter((item) => item.status === 1);
|
||||
setLlmModels(models);
|
||||
setPrompts(promptTemplates);
|
||||
return {models, promptTemplates, defaultModelId: defaultRes.data.data?.id};
|
||||
setLlmModels((modelRes.data?.data?.records || []).filter((item) => item.status === 1));
|
||||
setPrompts((promptRes.data?.data?.records || []).filter((item) => item.status === 1));
|
||||
summaryForm.setFieldsValue({ summaryModelId: defaultRes.data.data?.id });
|
||||
} catch {
|
||||
return {models: [], promptTemplates: [], defaultModelId: undefined};
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1756,19 +1743,17 @@ const MeetingDetail: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleOpenSummaryDrawer = async () => {
|
||||
const {models, promptTemplates, defaultModelId} = await loadAiConfigs();
|
||||
const handleOpenSummaryDrawer = () => {
|
||||
summaryForm.setFieldsValue({
|
||||
summaryModelId:
|
||||
summaryForm.getFieldValue('summaryModelId') ??
|
||||
meeting?.summaryModelId ??
|
||||
defaultModelId ??
|
||||
models.find((model) => model.isDefault === 1)?.id ??
|
||||
models[0]?.id,
|
||||
llmModels.find((model) => model.isDefault === 1)?.id ??
|
||||
llmModels[0]?.id,
|
||||
promptId:
|
||||
summaryForm.getFieldValue('promptId') ??
|
||||
meeting?.promptId ??
|
||||
promptTemplates[0]?.id,
|
||||
prompts[0]?.id,
|
||||
userPrompt: meeting?.lastUserPrompt ?? '',
|
||||
summaryDetailLevel:
|
||||
summaryForm.getFieldValue('summaryDetailLevel') ??
|
||||
|
|
@ -1883,55 +1868,58 @@ const MeetingDetail: React.FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleOpenHotWordGroupModal = async () => {
|
||||
if (!selectedKeywords.length) {
|
||||
message.warning('请先选择关键词');
|
||||
return;
|
||||
}
|
||||
if (hotWordGroupLimitLoading) {
|
||||
message.info('热词组上限配置加载中,请稍后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
setHotWordGroupLoading(true);
|
||||
try {
|
||||
const response = await getHotWordGroupOptions();
|
||||
const options = (response.data?.data || []).filter((item) => item.status === 1 && item.hotWordCount < hotWordGroupLimit);
|
||||
setHotWordGroupOptions(options);
|
||||
setSelectedHotWordGroupId(options.some((item) => item.id === meeting?.hotWordGroupId) ? meeting?.hotWordGroupId : options[0]?.id);
|
||||
setHotWordGroupModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setHotWordGroupLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSelectedHotwords = async () => {
|
||||
const keywords = selectedKeywords.map((item) => item.trim()).filter(isValidKeyword);
|
||||
const keywords = selectedKeywords.map((item) => item.trim()).filter(Boolean);
|
||||
if (!keywords.length) {
|
||||
message.warning('请先选择关键词');
|
||||
return;
|
||||
}
|
||||
if (selectedHotWordGroupId === undefined) {
|
||||
message.warning('请选择热词组');
|
||||
return;
|
||||
}
|
||||
|
||||
setAddingHotwords(true);
|
||||
try {
|
||||
const response = await createHotWordBatch({
|
||||
words: keywords,
|
||||
hotWordGroupId: selectedHotWordGroupId || undefined,
|
||||
remark: meeting ? `来源于会议:${meeting.title}` : '来源于会议关键词',
|
||||
});
|
||||
const result = response.data?.data;
|
||||
const existingWords = result?.existingWords || [];
|
||||
const existingRes = await getHotWordPage({ current: 1, size: 500, word: '' });
|
||||
const existingWords = new Set(
|
||||
(existingRes.data?.data?.records || [])
|
||||
.map((item) => item.word?.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const toCreate = keywords.filter((item) => !existingWords.has(item));
|
||||
|
||||
if (!toCreate.length) {
|
||||
message.info('所选关键词已存在于热词库');
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
toCreate.map((word) =>
|
||||
(async () => {
|
||||
let pinyinList: string[] = [];
|
||||
try {
|
||||
const pinyinRes = await getPinyinSuggestion(word);
|
||||
pinyinList = (pinyinRes.data?.data || []).map((item) => item.trim()).filter(Boolean);
|
||||
} catch {
|
||||
pinyinList = [];
|
||||
}
|
||||
return saveHotWord({
|
||||
word,
|
||||
pinyinList,
|
||||
matchStrategy: 1,
|
||||
category: '',
|
||||
weight: 2,
|
||||
status: 1,
|
||||
remark: meeting ? `来源于会议:${meeting.title}` : '来源于会议关键词',
|
||||
});
|
||||
})(),
|
||||
),
|
||||
);
|
||||
|
||||
const skippedCount = keywords.length - toCreate.length;
|
||||
message.success(
|
||||
`新增 ${result?.createdCount || 0} 个热词${existingWords.length ? `,[${existingWords.join(', ')}]已存在热词组` : ''}`,
|
||||
skippedCount > 0
|
||||
? `已新增 ${toCreate.length} 个热词,跳过 ${skippedCount} 个重复项`
|
||||
: `已新增 ${toCreate.length} 个热词`,
|
||||
);
|
||||
setSelectedKeywords([]);
|
||||
setHotWordGroupModalOpen(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
|
|
@ -2282,8 +2270,8 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
<Switch
|
||||
checked={sharePasswordEnabled}
|
||||
checkedChildren={'开启'}
|
||||
unCheckedChildren={'关闭'}
|
||||
checkedChildren={'\u5f00\u542f'}
|
||||
unCheckedChildren={'\u5173\u95ed'}
|
||||
onChange={handleSharePasswordToggle}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -2292,14 +2280,14 @@ const MeetingDetail: React.FC = () => {
|
|||
<Input
|
||||
value={sharePasswordDraft}
|
||||
maxLength={4}
|
||||
placeholder={'例如 A7K2'}
|
||||
placeholder={'\u4f8b\u5982 A7K2'}
|
||||
onChange={(event) => setSharePasswordDraft(normalizeAccessPasswordInput(event.target.value))}
|
||||
/>
|
||||
<Button onClick={handleRegenerateSharePassword}>{'重置默认'}</Button>
|
||||
<Button onClick={handleRegenerateSharePassword}>{'\u91cd\u7f6e\u9ed8\u8ba4'}</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<Button type="primary" loading={shareSaving} onClick={handleSaveShareAccess}>
|
||||
{'保存密码设置'}
|
||||
{'\u4fdd\u5b58\u5bc6\u7801\u8bbe\u7f6e'}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -2315,7 +2303,7 @@ const MeetingDetail: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
<div className="meeting-share-caption">
|
||||
{'使用手机扫码后将跳转到会议预览页,若已开启密码需手动输入。'}
|
||||
{'\u4f7f\u7528\u624b\u673a\u626b\u7801\u540e\u5c06\u8df3\u8f6c\u5230\u4f1a\u8bae\u9884\u89c8\u9875\uff0c\u82e5\u5df2\u5f00\u542f\u5bc6\u7801\u9700\u624b\u52a8\u8f93\u5165\u3002'}
|
||||
</div>
|
||||
<div className="meeting-share-link-box">
|
||||
<LinkOutlined />
|
||||
|
|
@ -2323,10 +2311,10 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
<div className="meeting-share-actions">
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={handleCopyPreviewLink}>
|
||||
{'复制链接'}
|
||||
{'\u590d\u5236\u94fe\u63a5'}
|
||||
</Button>
|
||||
<Button size="small" type="primary" ghost onClick={handleOpenPreview}>
|
||||
{'打开预览'}
|
||||
{'\u6253\u5f00\u9884\u89c8'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2404,7 +2392,7 @@ const MeetingDetail: React.FC = () => {
|
|||
</Button>
|
||||
)}
|
||||
{canRetrySummary && (
|
||||
<Button icon={<SyncOutlined/>} onClick={() => void handleOpenSummaryDrawer()} disabled={actionLoading}>
|
||||
<Button icon={<SyncOutlined />} onClick={handleOpenSummaryDrawer} disabled={actionLoading}>
|
||||
重新总结
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -2590,9 +2578,9 @@ const MeetingDetail: React.FC = () => {
|
|||
ghost
|
||||
disabled={!selectedKeywords.length}
|
||||
loading={addingHotwords}
|
||||
onClick={() => void handleOpenHotWordGroupModal()}
|
||||
onClick={handleAddSelectedHotwords}
|
||||
>
|
||||
加入热词组 {selectedKeywords.length > 0 ? `(${selectedKeywords.length})` : ''}
|
||||
加入热词 {selectedKeywords.length > 0 ? `(${selectedKeywords.length})` : ''}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -4250,33 +4238,6 @@ const MeetingDetail: React.FC = () => {
|
|||
}
|
||||
`}</style>
|
||||
|
||||
<Modal
|
||||
title="加入热词组"
|
||||
open={hotWordGroupModalOpen}
|
||||
onCancel={() => setHotWordGroupModalOpen(false)}
|
||||
onOk={() => void handleAddSelectedHotwords()}
|
||||
confirmLoading={addingHotwords}
|
||||
okText="加入"
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form layout="vertical" style={{marginTop: 16}}>
|
||||
<Form.Item label="目标热词组" required>
|
||||
<Select
|
||||
value={selectedHotWordGroupId}
|
||||
placeholder={hotWordGroupLoading ? '正在加载热词组' : '请选择热词组'}
|
||||
loading={hotWordGroupLoading}
|
||||
onChange={setSelectedHotWordGroupId}
|
||||
options={[
|
||||
...hotWordGroupOptions.map((item) => ({
|
||||
label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`,
|
||||
value: item.id,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{isOwner && (
|
||||
<Modal title="编辑会议信息" open={editVisible} onOk={handleUpdateBasic} onCancel={() => setEditVisible(false)} confirmLoading={actionLoading} width={600} forceRender>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
|
|
|
|||
|
|
@ -86,16 +86,6 @@ const DEFAULT_CREATE_CONFIG: MeetingCreateConfig = {
|
|||
realtimeEnabled: true,
|
||||
offlineAudioMaxSizeMb: 1024,
|
||||
};
|
||||
const MEETING_SOURCE_LABELS: Record<string, string> = {
|
||||
WINDOWS: "Windows",
|
||||
MACOS: "macOS",
|
||||
KYLIN: "麒麟",
|
||||
UOS: "统信",
|
||||
HARMONYOS: "鸿蒙",
|
||||
WEB: "Web端",
|
||||
CUSTOM_TERMINAL: "定制终端",
|
||||
ANDROID: "定制终端",
|
||||
};
|
||||
|
||||
const isRealtimeMeetingCandidate = (item: MeetingVO) =>
|
||||
item.meetingType === "REALTIME" || (!item.meetingType && item.status === 0 && !item.audioUrl);
|
||||
|
|
@ -104,7 +94,7 @@ const canControlRealtimeFromCurrentPlatform = (item: MeetingVO) =>
|
|||
!item.meetingSource || item.meetingSource === CURRENT_PLATFORM;
|
||||
|
||||
const getMeetingSourceLabel = (source?: MeetingVO["meetingSource"]) =>
|
||||
source ? (MEETING_SOURCE_LABELS[source] ?? source) : MEETING_SOURCE_LABELS.WEB;
|
||||
source === "ANDROID" ? "安卓端" : "Web端";
|
||||
|
||||
const getRealtimeSourceLabel = (item: MeetingVO) => getMeetingSourceLabel(item.meetingSource);
|
||||
|
||||
|
|
@ -323,7 +313,7 @@ const MeetingCardItem: React.FC<{
|
|||
? (progress?.message || progress?.unifiedStatus?.message || config.text)
|
||||
: (progress?.unifiedStatus?.message || progress?.message || "深度分析中...");
|
||||
|
||||
const sourceColor = item.meetingSource === "CUSTOM_TERMINAL" || item.meetingSource === "ANDROID" ? "#10b981" : "#3b82f6";
|
||||
const sourceColor = item.meetingSource === "ANDROID" ? "#10b981" : "#3b82f6";
|
||||
|
||||
return (
|
||||
<List.Item className="meeting-card-list-item">
|
||||
|
|
@ -860,13 +850,7 @@ const Meetings: React.FC = () => {
|
|||
</Button>
|
||||
)}
|
||||
{canManageMeeting(record) && (
|
||||
<Popconfirm
|
||||
title="确定删除吗?"
|
||||
onConfirm={(event) => {
|
||||
event?.stopPropagation();
|
||||
return deleteMeeting(record.id).then(() => fetchData());
|
||||
}}
|
||||
>
|
||||
<Popconfirm title="确定删除吗?" onConfirm={() => deleteMeeting(record.id).then(() => fetchData())}>
|
||||
<Button type="link" danger size="small" onClick={(e) => e.stopPropagation()}>删除</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -40,25 +40,11 @@
|
|||
gap: 4px;
|
||||
}
|
||||
|
||||
.prompt-template-name-cell__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.prompt-template-name-cell__title-row > .ant-typography {
|
||||
min-width: 0;
|
||||
.prompt-template-name-cell > .ant-typography {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prompt-template-name-cell__title-row > .ant-tag {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.prompt-template-description.ant-typography {
|
||||
display: block;
|
||||
max-width: 360px;
|
||||
|
|
|
|||
|
|
@ -21,28 +21,16 @@ import PageContainer from "@/components/shared/PageContainer";
|
|||
import DataListPanel from "@/components/shared/DataListPanel";
|
||||
import FormDrawer from "@/components/shared/FormDrawer";
|
||||
import SectionCard from "@/components/shared/SectionCard";
|
||||
import {
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
PlusOutlined,
|
||||
SaveOutlined,
|
||||
StarFilled,
|
||||
StarOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDict } from '../../hooks/useDict';
|
||||
import {useHotWordGroupLimit} from '../../hooks/useHotWordGroupLimit';
|
||||
import {
|
||||
deletePromptTemplate,
|
||||
clearPromptDefault,
|
||||
getPromptDetail,
|
||||
getPromptPage,
|
||||
savePromptTemplate,
|
||||
setPromptDefault,
|
||||
updatePromptStatus,
|
||||
updatePromptTemplate,
|
||||
type PromptTemplateVO,
|
||||
|
|
@ -70,7 +58,6 @@ const PromptTemplates: React.FC = () => {
|
|||
const { items: categories, loading: dictLoading } = useDict('biz_prompt_category');
|
||||
const { items: dictTags } = useDict('biz_prompt_tag');
|
||||
const { items: promptLevels } = useDict('biz_prompt_level');
|
||||
const {limit: hotWordGroupLimit} = useHotWordGroupLimit();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<PromptTemplateVO[]>([]);
|
||||
|
|
@ -108,7 +95,8 @@ const PromptTemplates: React.FC = () => {
|
|||
}, [isPlatformAdmin, templateLevel, activeTenantId]);
|
||||
|
||||
const loadGroupOptions = async () => {
|
||||
const res = await getHotWordGroupOptions();
|
||||
const targetTenantId = isPlatformAdmin && Number(templateLevel) === 1 ? 0 : undefined;
|
||||
const res = await getHotWordGroupOptions(targetTenantId ?? (isPlatformAdmin && activeTenantId === 0 ? 0 : undefined));
|
||||
setGroupOptions(res.data?.data || []);
|
||||
};
|
||||
|
||||
|
|
@ -165,7 +153,7 @@ const PromptTemplates: React.FC = () => {
|
|||
}
|
||||
|
||||
if (!canEdit) {
|
||||
message.warning("您无权限修改此层级的模板");
|
||||
message.warning('您无权修改此层级的模板');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -210,8 +198,7 @@ const PromptTemplates: React.FC = () => {
|
|||
) : null}
|
||||
<div className="prompt-template-detail__section">
|
||||
<Space wrap>
|
||||
{detail.hotWordGroupName ? <Tag color="blue">热词组:{detail.hotWordGroupName}</Tag> :
|
||||
<Tag>未绑定热词组</Tag>}
|
||||
{detail.hotWordGroupName ? <Tag color="blue">热词组:{detail.hotWordGroupName}</Tag> : <Tag>未绑定热词组</Tag>}
|
||||
{normalizePromptTags(detail.tags).map((tag) => {
|
||||
const dictItem = dictTags.find((item) => item.itemValue === tag);
|
||||
return <Tag key={tag}>{dictItem ? dictItem.itemLabel : tag}</Tag>;
|
||||
|
|
@ -233,7 +220,7 @@ const PromptTemplates: React.FC = () => {
|
|||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{detail.promptContent}</ReactMarkdown>
|
||||
</div>
|
||||
),
|
||||
okText: '关闭',
|
||||
okText: '关闭',
|
||||
maskClosable: true,
|
||||
});
|
||||
})();
|
||||
|
|
@ -250,7 +237,7 @@ const PromptTemplates: React.FC = () => {
|
|||
message.success('更新成功');
|
||||
} else {
|
||||
await savePromptTemplate(values);
|
||||
message.success("模板创建成功");
|
||||
message.success('模板已创建');
|
||||
}
|
||||
setDrawerVisible(false);
|
||||
await fetchData();
|
||||
|
|
@ -308,13 +295,12 @@ const PromptTemplates: React.FC = () => {
|
|||
width: 280,
|
||||
render: (_: unknown, item: PromptTemplateVO) => (
|
||||
<div className="prompt-template-name-cell">
|
||||
<div className="prompt-template-name-cell__title-row">
|
||||
<Text strong ellipsis={{tooltip: item.templateName}}>{item.templateName}</Text>
|
||||
{item.isDefault ? <Tag
|
||||
color={item.defaultAvailable ? 'gold' : 'default'}>{item.defaultAvailable ? '默认' : '默认已失效'}</Tag> : null}
|
||||
</div>
|
||||
{item.description ? <Text type="secondary" className="prompt-template-description"
|
||||
ellipsis={{tooltip: item.description}}>{item.description}</Text> : null}
|
||||
<Text strong ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text>
|
||||
{item.description ? (
|
||||
<Text type="secondary" className="prompt-template-description" ellipsis={{ tooltip: item.description }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -333,27 +319,44 @@ const PromptTemplates: React.FC = () => {
|
|||
return <Tag color={level.color} className="prompt-template-level-tag">{level.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '热词组',
|
||||
dataIndex: 'hotWordGroupName',
|
||||
width: 180,
|
||||
render: (name: string) => name ? <Tag color="blue">{name}</Tag> : <Text type="secondary">未绑定</Text>,
|
||||
},
|
||||
{
|
||||
title: '业务标签',
|
||||
dataIndex: 'tags',
|
||||
width: 220,
|
||||
minWidth: 220,
|
||||
render: (tags: unknown) => {
|
||||
const tagList = normalizePromptTags(tags);
|
||||
if (!tagList.length) return <Text type="secondary">未配置</Text>;
|
||||
return <Space size={[4, 4]} wrap className="prompt-template-tags-cell">
|
||||
{tagList.slice(0, 3).map((tag) => <Tag
|
||||
key={tag}>{dictTags.find((item) => item.itemValue === tag)?.itemLabel || tag}</Tag>)}
|
||||
{tagList.length > 3 ? <Tag>+{tagList.length - 3}</Tag> : null}
|
||||
</Space>;
|
||||
if (!tagList.length) {
|
||||
return <Text type="secondary">未配置</Text>;
|
||||
}
|
||||
return (
|
||||
<Space size={[4, 4]} wrap className="prompt-template-tags-cell">
|
||||
{tagList.slice(0, 3).map((tag) => {
|
||||
const dictItem = dictTags.find((dt) => dt.itemValue === tag);
|
||||
return <Tag key={tag}>{dictItem ? dictItem.itemLabel : tag}</Tag>;
|
||||
})}
|
||||
{tagList.length > 3 ? <Tag>+{tagList.length - 3}</Tag> : null}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (_: unknown, item: PromptTemplateVO) => <Switch checked={item.status === 1}
|
||||
onChange={(checked) => void handleStatusChange(item.id, checked)}
|
||||
onClick={(_, event) => event.stopPropagation()}/>,
|
||||
render: (_: unknown, item: PromptTemplateVO) => (
|
||||
<Switch
|
||||
size="small"
|
||||
checked={item.status === 1}
|
||||
onClick={(_, event) => event.stopPropagation()}
|
||||
onChange={(checked) => void handleStatusChange(item.id, checked)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
|
@ -362,61 +365,28 @@ const PromptTemplates: React.FC = () => {
|
|||
fixed: 'right' as const,
|
||||
render: (_: unknown, item: PromptTemplateVO) => {
|
||||
const canEdit = canManageTemplate(item);
|
||||
const canManageDefault = isPlatformAdmin
|
||||
? item.isSystem === 1 && Number(item.tenantId) === 0
|
||||
: isTenantAdmin
|
||||
? item.isSystem === 1 && Number(item.tenantId) === activeTenantId
|
||||
: item.status === 1;
|
||||
const isCurrentScopeDefault = isPlatformAdmin || isTenantAdmin
|
||||
? item.isTemplateDefault === true
|
||||
: item.isDefault === true && item.defaultScope === "PERSONAL";
|
||||
const defaultActionLabel = isPlatformAdmin
|
||||
? "平台默认"
|
||||
: isTenantAdmin
|
||||
? "租户默认"
|
||||
: "个人默认";
|
||||
return (
|
||||
<Space size={2} onClick={(e) => e.stopPropagation()}>
|
||||
<Tooltip title="查看">
|
||||
<Button type="text" size="small" icon={<EyeOutlined/>} onClick={() => showDetail(item)}
|
||||
aria-label="查看模板"/>
|
||||
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => showDetail(item)} aria-label="查看模板" />
|
||||
</Tooltip>
|
||||
{canEdit && (
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" size="small" icon={<EditOutlined/>} onClick={() => handleOpenDrawer(item)}
|
||||
aria-label="编辑模板"/>
|
||||
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleOpenDrawer(item)} aria-label="编辑模板" />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="以此创建">
|
||||
<Button type="text" size="small" icon={<CopyOutlined/>} onClick={() => handleOpenDrawer(item, true)}
|
||||
aria-label="以此创建模板"/>
|
||||
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleOpenDrawer(item, true)} aria-label="以此创建模板" />
|
||||
</Tooltip>
|
||||
{canManageDefault && (
|
||||
<Tooltip title={isCurrentScopeDefault ? `取消${defaultActionLabel}` : `设为${defaultActionLabel}`}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={isCurrentScopeDefault ? <StarFilled/> : <StarOutlined/>}
|
||||
onClick={() => {
|
||||
const request = isCurrentScopeDefault ? clearPromptDefault(item.id) : setPromptDefault(item.id);
|
||||
request.then(() => {
|
||||
message.success(isCurrentScopeDefault ? `已取消${defaultActionLabel}` : `已设为${defaultActionLabel}`);
|
||||
void fetchData();
|
||||
});
|
||||
}}
|
||||
aria-label={isCurrentScopeDefault ? `取消${defaultActionLabel}` : `设为${defaultActionLabel}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Popconfirm
|
||||
title="确认删除该模板吗?"
|
||||
title="确定删除?"
|
||||
onConfirm={() => deletePromptTemplate(item.id).then(() => fetchData())}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined/>} aria-label="删除模板"/>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} aria-label="删除模板" />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
|
|
@ -430,7 +400,7 @@ const PromptTemplates: React.FC = () => {
|
|||
<PageContainer title={null} className="prompt-templates-page">
|
||||
<SectionCard
|
||||
title="提示词模板"
|
||||
description="配置 AI 任务所需的提示词模板"
|
||||
description="管理 AI 会议总结的提示词模板库。"
|
||||
>
|
||||
<DataListPanel
|
||||
className="prompt-templates-list-panel"
|
||||
|
|
@ -443,7 +413,7 @@ const PromptTemplates: React.FC = () => {
|
|||
<Form layout="inline" onFinish={handleSearch} className="prompt-templates-search">
|
||||
<Form.Item label="模板名称">
|
||||
<Input
|
||||
placeholder="请输入模板名称"
|
||||
placeholder="请输入..."
|
||||
className="prompt-templates-search__name"
|
||||
value={queryDraft.name}
|
||||
onChange={(event) => setQueryDraft((currentDraft) => ({ ...currentDraft, name: event.target.value }))}
|
||||
|
|
@ -489,13 +459,13 @@ const PromptTemplates: React.FC = () => {
|
|||
pagination={false}
|
||||
scroll={{ x: "max(100%, 1400px)", y: "100%" }}
|
||||
onRow={(record) => ({ onClick: () => showDetail(record) })}
|
||||
locale={{emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无可用模板"/>}}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无可用模板" /> }}
|
||||
/>
|
||||
</DataListPanel>
|
||||
</SectionCard>
|
||||
|
||||
<FormDrawer
|
||||
title={editingId ? '编辑模板' : '创建模板'}
|
||||
title={editingId ? '编辑模板' : '创建新模板'}
|
||||
size="md"
|
||||
width="min(1536px, 80vw)"
|
||||
className="prompt-template-form-drawer"
|
||||
|
|
@ -523,20 +493,19 @@ const PromptTemplates: React.FC = () => {
|
|||
>
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={12} xl={6}>
|
||||
<Form.Item name="templateName" label="模板名称"
|
||||
rules={[{required: true}, {max: 15, message: "模板名称不能超过15个字符"}]}>
|
||||
<Input maxLength={15} showCount/>
|
||||
<Form.Item name="templateName" label="模板名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{(isPlatformAdmin || isTenantAdmin) && (
|
||||
<Col xs={24} md={12} xl={6}>
|
||||
<Form.Item name="isSystem" label="模板层级" rules={[{required: true}]}>
|
||||
<Select placeholder="请选择模板层级">
|
||||
<Form.Item name="isSystem" label="模板属性" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择属性">
|
||||
{promptLevels.length > 0 ? (
|
||||
promptLevels.map((i) => <Option key={i.itemValue} value={Number(i.itemValue)}>{i.itemLabel}</Option>)
|
||||
) : (
|
||||
<>
|
||||
<Option value={1}>{isPlatformAdmin ? '系统预置(全局)' : '租户预置(全员)'}</Option>
|
||||
<Option value={1}>{isPlatformAdmin ? '系统预置 (全局)' : '租户预置 (全员)'}</Option>
|
||||
<Option value={0}>个人模板</Option>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -545,7 +514,7 @@ const PromptTemplates: React.FC = () => {
|
|||
</Col>
|
||||
)}
|
||||
<Col xs={24} md={12} xl={6}>
|
||||
<Form.Item name="category" label="分类" rules={[{required: true}]}>
|
||||
<Form.Item name="category" label="分类" rules={[{ required: true }]}>
|
||||
<Select loading={dictLoading}>
|
||||
{categories.map((i) => <Option key={i.itemValue} value={i.itemValue}>{i.itemLabel}</Option>)}
|
||||
</Select>
|
||||
|
|
@ -572,7 +541,7 @@ const PromptTemplates: React.FC = () => {
|
|||
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} xl={12}>
|
||||
<Form.Item name="tags" label="业务标签" tooltip="可选择已有标签,也可直接输入新标签">
|
||||
<Form.Item name="tags" label="业务标签" tooltip="可从现有标签中选择,也可输入新内容按回车保存">
|
||||
<Select mode="tags" placeholder="选择或输入新标签" allowClear tokenSeparators={[',', ' ', ';']}>
|
||||
{dictTags.map((item) => <Option key={item.itemValue} value={item.itemValue}>{item.itemLabel}</Option>)}
|
||||
</Select>
|
||||
|
|
@ -581,16 +550,13 @@ const PromptTemplates: React.FC = () => {
|
|||
<Col xs={24} xl={12}>
|
||||
<Form.Item
|
||||
name="hotWordGroupId"
|
||||
label="热词组"
|
||||
label="绑定热词组"
|
||||
tooltip="可选,未绑定则保持兼容"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择热词组"
|
||||
placeholder="选择热词组"
|
||||
allowClear
|
||||
options={groupOptions.map((item) => ({
|
||||
label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`,
|
||||
value: item.id
|
||||
}))}
|
||||
options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
@ -599,7 +565,7 @@ const PromptTemplates: React.FC = () => {
|
|||
<Row gutter={[12, 16]} className="prompt-template-editor-header">
|
||||
<Col xs={24} xl={12} className="prompt-template-editor-header__col">
|
||||
<div className="prompt-template-editor-title">
|
||||
提示词编辑器(Markdown 实时预览)
|
||||
提示词编辑器 (Markdown 实时预览)
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} xl={12} className="prompt-template-editor-header__col">
|
||||
|
|
@ -621,7 +587,7 @@ const PromptTemplates: React.FC = () => {
|
|||
<Input.TextArea
|
||||
onChange={(e) => setPreviewContent(e.target.value)}
|
||||
className="prompt-template-editor__input"
|
||||
placeholder="在此输入 Markdown 提示词..."
|
||||
placeholder="在此输入 Markdown 指令..."
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -29,16 +29,6 @@ import {
|
|||
const SAMPLE_RATE = 16000;
|
||||
const CHUNK_SIZE = 1280;
|
||||
const CURRENT_PLATFORM = "WEB" as const;
|
||||
const MEETING_SOURCE_LABELS: Record<string, string> = {
|
||||
WINDOWS: "Windows",
|
||||
MACOS: "macOS",
|
||||
KYLIN: "麒麟",
|
||||
UOS: "统信",
|
||||
HARMONYOS: "鸿蒙",
|
||||
WEB: "Web端",
|
||||
CUSTOM_TERMINAL: "定制终端",
|
||||
ANDROID: "定制终端",
|
||||
};
|
||||
|
||||
type WsSpeaker = string | { name?: string; user_id?: string | number } | undefined;
|
||||
type WsMessage = {
|
||||
|
|
@ -97,7 +87,7 @@ type RealtimeMeetingSessionDraft = {
|
|||
enableItn: boolean;
|
||||
enableTextRefine: boolean;
|
||||
saveAudio: boolean;
|
||||
hotWordGroupId?: number;
|
||||
hotwords: Array<{ hotword: string; weight: number }>;
|
||||
};
|
||||
|
||||
function getSessionKey(meetingId: number) {
|
||||
|
|
@ -122,7 +112,7 @@ function buildDraftFromStatus(meetingId: number, meeting: MeetingVO | null, stat
|
|||
enableItn: config.enableItn !== false,
|
||||
enableTextRefine: !!config.enableTextRefine,
|
||||
saveAudio: !!config.saveAudio,
|
||||
hotWordGroupId: config.hotWordGroupId,
|
||||
hotwords: config.hotwords || [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +283,7 @@ export function RealtimeAsrSession() {
|
|||
return;
|
||||
}
|
||||
if (detail.meetingSource && detail.meetingSource !== CURRENT_PLATFORM) {
|
||||
const sourceLabel = MEETING_SOURCE_LABELS[detail.meetingSource] ?? detail.meetingSource;
|
||||
const sourceLabel = detail.meetingSource === "ANDROID" ? "安卓端" : "Web 端";
|
||||
message.warning(`该实时会议需在${sourceLabel}继续,当前仅支持查看详情`);
|
||||
navigate(`/meetings/${meetingId}`);
|
||||
return;
|
||||
|
|
@ -616,7 +606,7 @@ export function RealtimeAsrSession() {
|
|||
enableItn: sessionDraft.enableItn !== false,
|
||||
enableTextRefine: !!sessionDraft.enableTextRefine,
|
||||
saveAudio: !!sessionDraft.saveAudio,
|
||||
hotWordGroupId: sessionDraft.hotWordGroupId,
|
||||
hotwords: sessionDraft.hotwords || [],
|
||||
});
|
||||
const socketSession = socketSessionRes.data.data;
|
||||
|
||||
|
|
|
|||
|
|
@ -36,16 +36,6 @@ type RecentCard = {
|
|||
};
|
||||
|
||||
const RECENT_CARD_READ_STORAGE_KEY = "home_recent_card_read_ids";
|
||||
const MEETING_SOURCE_LABELS: Record<string, string> = {
|
||||
WINDOWS: "Windows",
|
||||
MACOS: "macOS",
|
||||
KYLIN: "麒麟",
|
||||
UOS: "统信",
|
||||
HARMONYOS: "鸿蒙",
|
||||
WEB: "Web端",
|
||||
CUSTOM_TERMINAL: "定制终端",
|
||||
ANDROID: "定制终端",
|
||||
};
|
||||
|
||||
const fallbackRecentCards: RecentCard[] = [
|
||||
{
|
||||
|
|
@ -324,7 +314,7 @@ export default function HomePage() {
|
|||
<div className="home-recent-card-tags">
|
||||
{recentTaskMap.get(String(card.id))?.meetingSource && (
|
||||
<Tag key={`${card.id}-source`} className="home-recent-card-tag" bordered={false}>
|
||||
{MEETING_SOURCE_LABELS[recentTaskMap.get(String(card.id))?.meetingSource ?? "WEB"] ?? "Web端"}
|
||||
{recentTaskMap.get(String(card.id))?.meetingSource === "ANDROID" ? "安卓端" : "Web端"}
|
||||
</Tag>
|
||||
)}
|
||||
{card.tags.slice(0, 4).map((tag) => (
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ export default function Profile() {
|
|||
};
|
||||
|
||||
const renderValue = (value?: string) => value || "-";
|
||||
const mcpAddress = typeof window === "undefined" ? "/mcp" : `${window.location.origin}/mcp`;
|
||||
const avatarUrlValue = Form.useWatch("avatarUrl", profileForm) as string | undefined;
|
||||
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
||||
const userStatus = user ? (user.status === 0 ? <Tag color="red">禁用</Tag> : <Tag color="green">启用</Tag>) : "-";
|
||||
|
|
@ -479,12 +478,6 @@ export default function Profile() {
|
|||
<span>{t("profile.botBindStatus")}</span>
|
||||
<strong>{credential?.bound ? <Tag color="success">{t("profile.botBound")}</Tag> : <Tag>{t("profile.botUnbound")}</Tag>}</strong>
|
||||
</div>
|
||||
<div className="profile-credential-item">
|
||||
<span>{t("profile.mcpAddress")}</span>
|
||||
<Paragraph copyable={{text: mcpAddress}} className="profile-copy-value">
|
||||
{mcpAddress}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="profile-credential-item profile-credential-item--wide">
|
||||
<span>X-Bot-Id</span>
|
||||
{credential?.botId ? (
|
||||
|
|
|
|||
|
|
@ -25,11 +25,6 @@ export interface TokenResponse {
|
|||
refreshExpiresInDays: number;
|
||||
}
|
||||
|
||||
export interface MeetingParticipant {
|
||||
userId: number;
|
||||
displayName: string | null;
|
||||
}
|
||||
|
||||
export interface MeetingVO {
|
||||
id: number;
|
||||
tenantId: number;
|
||||
|
|
@ -41,13 +36,12 @@ export interface MeetingVO {
|
|||
meetingTime: string;
|
||||
participants: string;
|
||||
participantIds?: number[];
|
||||
participantUsers?: MeetingParticipant[];
|
||||
tags: string;
|
||||
audioUrl: string;
|
||||
playbackAudioUrl?: string;
|
||||
duration?: number;
|
||||
meetingType?: "OFFLINE" | "REALTIME";
|
||||
meetingSource?: "WINDOWS" | "MACOS" | "KYLIN" | "UOS" | "HARMONYOS" | "WEB" | "CUSTOM_TERMINAL" | "ANDROID";
|
||||
meetingSource?: "WEB" | "ANDROID";
|
||||
sourceDeviceCode?: string;
|
||||
sourceDeviceMode?: "PUBLIC" | "PRIVATE";
|
||||
summaryDetailLevel?: "DETAILED" | "STANDARD" | "BRIEF";
|
||||
|
|
|
|||
Loading…
Reference in New Issue