feat: 添加热词组管理功能并优化热词控制器逻辑
- 在 `HotWordController` 中添加 `HotWordGroupService` 依赖,并更新相关方法以支持租户ID - 重构权限校验逻辑,移除 `isCurrentUserAdmin` 方法,改为使用 `resolveTargetTenantId` 方法dev_na
parent
324e283f41
commit
2d788bac75
|
|
@ -253,6 +253,7 @@
|
|||
| pinyin_list | JSONB | | 鎷奸煶鏁扮粍 |
|
||||
| match_strategy | SMALLINT | DEFAULT 1 | 鍖归厤绛栫暐 (1:绮剧‘, 2:妯$硦) |
|
||||
| category | VARCHAR(50) | | 绫诲埆 (浜哄悕銆佹湳璇瓑) |
|
||||
| hot_word_group_id | BIGINT | | 所属热词组 ID |
|
||||
| weight | INTEGER | DEFAULT 10 | 鏉冮噸 (1-100) |
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:鍚敤, 0:绂佺敤) |
|
||||
| is_synced | SMALLINT | DEFAULT 0 | 宸插悓姝ョ涓夋柟鏍囪 |
|
||||
|
|
@ -264,8 +265,26 @@
|
|||
绱㈠紩锛?
|
||||
- `idx_hotword_tenant`: `(tenant_id)`
|
||||
- `idx_hotword_word`: `(word) WHERE is_deleted = 0`
|
||||
- `idx_hotword_group`: `(hot_word_group_id) WHERE is_deleted = 0`
|
||||
|
||||
### 5.3 `biz_prompt_templates`锛堟彁绀鸿瘝妯℃澘琛級
|
||||
### 5.3 `biz_hot_word_groups`(热词组表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 主键ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||
| group_name | VARCHAR(100) | NOT NULL | 热词组名称 |
|
||||
| creator_id | BIGINT | | 创建人ID |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态(1:启用, 0:禁用) |
|
||||
| remark | VARCHAR(255) | | 备注 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
||||
|
||||
索引:
|
||||
- `idx_hot_word_group_tenant`: `(tenant_id) WHERE is_deleted = 0`
|
||||
- `uk_hot_word_group_name_scope`: `(tenant_id, group_name) WHERE is_deleted = 0`
|
||||
|
||||
### 5.4 `biz_prompt_templates`锛堟彁绀鸿瘝妯℃澘琛級
|
||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
|
|
@ -275,6 +294,7 @@
|
|||
| is_system | SMALLINT | DEFAULT 0 | 鏄惁棰勭疆 (1:鏄? 0:鍚? |
|
||||
| creator_id | BIGINT | | 鍒涘缓浜篒D |
|
||||
| tags | JSONB | | 鏍囩鏁扮粍 |
|
||||
| hot_word_group_id | BIGINT | | 绑定热词组 ID |
|
||||
| usage_count | INTEGER | DEFAULT 0 | 浣跨敤娆℃暟 |
|
||||
| prompt_content | TEXT | NOT NULL | 鎻愮ず璇嶅唴瀹?|
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:鍚敤, 0:绂佺敤) |
|
||||
|
|
@ -286,8 +306,9 @@
|
|||
绱㈠紩锛?
|
||||
- `idx_prompt_tenant`: `(tenant_id)`
|
||||
- `idx_prompt_system`: `(is_system) WHERE is_deleted = 0`
|
||||
- `idx_prompt_group`: `(hot_word_group_id) WHERE is_deleted = 0`
|
||||
|
||||
### 5.4 `biz_asr_models`(ASR 模型管理表)
|
||||
### 5.5 `biz_asr_models`(ASR 模型管理表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 主键ID |
|
||||
|
|
|
|||
|
|
@ -262,6 +262,24 @@ COMMENT ON TABLE biz_speakers IS '声纹发言人基础信息表 (声纹库资
|
|||
-- ----------------------------
|
||||
-- 7. 业务模块 - 热词管理
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS biz_hot_word_groups CASCADE;
|
||||
CREATE TABLE biz_hot_word_groups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL, -- 租户ID
|
||||
group_name VARCHAR(100) NOT NULL, -- 热词组名称
|
||||
creator_id BIGINT, -- 创建人ID
|
||||
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
|
||||
remark VARCHAR(255), -- 备注
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX idx_hot_word_group_tenant ON biz_hot_word_groups (tenant_id) WHERE is_deleted = 0;
|
||||
CREATE UNIQUE INDEX uk_hot_word_group_name_scope ON biz_hot_word_groups (tenant_id, group_name) WHERE is_deleted = 0;
|
||||
|
||||
COMMENT ON TABLE biz_hot_word_groups IS '热词组表';
|
||||
|
||||
DROP TABLE IF EXISTS biz_hot_words CASCADE;
|
||||
CREATE TABLE biz_hot_words (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
|
@ -272,6 +290,7 @@ CREATE TABLE biz_hot_words (
|
|||
pinyin_list text, -- 拼音数组(支持多音字, 如 ["i mi ting", "i mei ting"])
|
||||
match_strategy SMALLINT DEFAULT 1, -- 匹配策略: 1:精确匹配, 2:拼音模糊匹配
|
||||
category VARCHAR(50), -- 类别 (人名、术语、地名)
|
||||
hot_word_group_id BIGINT, -- 所属热词组 ID
|
||||
weight INTEGER DEFAULT 10, -- 权重 (1-100)
|
||||
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
|
||||
is_synced SMALLINT DEFAULT 0, -- 是否已同步至第三方引擎: 0:未同步, 1:已同步
|
||||
|
|
@ -283,6 +302,7 @@ CREATE TABLE biz_hot_words (
|
|||
|
||||
CREATE INDEX idx_hotword_tenant ON biz_hot_words (tenant_id);
|
||||
CREATE INDEX idx_hotword_word ON biz_hot_words (word) WHERE is_deleted = 0;
|
||||
CREATE INDEX idx_hotword_group ON biz_hot_words (hot_word_group_id) WHERE is_deleted = 0;
|
||||
|
||||
COMMENT ON TABLE biz_hot_words IS '语音识别热词表';
|
||||
|
||||
|
|
@ -299,6 +319,7 @@ CREATE TABLE biz_prompt_templates (
|
|||
is_system SMALLINT DEFAULT 0, -- 是否系统预置 (1:是, 0:否)
|
||||
creator_id BIGINT, -- 创建人ID
|
||||
tags text, -- 标签数组 (JSONB)
|
||||
hot_word_group_id BIGINT, -- 绑定热词组 ID
|
||||
usage_count INTEGER DEFAULT 0, -- 使用次数
|
||||
prompt_content TEXT NOT NULL, -- 提示词内容
|
||||
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
|
||||
|
|
@ -310,6 +331,7 @@ CREATE TABLE biz_prompt_templates (
|
|||
|
||||
CREATE INDEX idx_prompt_tenant ON biz_prompt_templates (tenant_id);
|
||||
CREATE INDEX idx_prompt_system ON biz_prompt_templates (is_system) WHERE is_deleted = 0;
|
||||
CREATE INDEX idx_prompt_group ON biz_prompt_templates (hot_word_group_id) WHERE is_deleted = 0;
|
||||
|
||||
COMMENT ON TABLE biz_prompt_templates IS '会议总结提示词模板表';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ 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.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
|
||||
import com.imeeting.entity.biz.HotWordGroup;
|
||||
import com.imeeting.service.biz.HotWordGroupService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
|
|
@ -26,16 +26,18 @@ import java.util.stream.Collectors;
|
|||
public class HotWordController {
|
||||
|
||||
private final HotWordService hotWordService;
|
||||
private final HotWordGroupService hotWordGroupService;
|
||||
|
||||
public HotWordController(HotWordService hotWordService) {
|
||||
public HotWordController(HotWordService hotWordService, HotWordGroupService hotWordGroupService) {
|
||||
this.hotWordService = hotWordService;
|
||||
this.hotWordGroupService = hotWordGroupService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前用户是否具备管理员权限 (平台管理员或租户管理员)
|
||||
*/
|
||||
private boolean isCurrentUserAdmin(LoginUser user) {
|
||||
return Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin());
|
||||
private Long resolveTargetTenantId(LoginUser loginUser, Long tenantId) {
|
||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(tenantId)) {
|
||||
return 0L;
|
||||
}
|
||||
return loginUser.getTenantId();
|
||||
}
|
||||
|
||||
@Operation(summary = "新增热词")
|
||||
|
|
@ -43,63 +45,30 @@ public class HotWordController {
|
|||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<HotWordVO> save(@RequestBody HotWordDTO hotWordDTO) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
|
||||
// 核心校验:只有管理员可以创建公开热词
|
||||
if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !isCurrentUserAdmin(loginUser)) {
|
||||
return ApiResponse.error("无权创建租户公开热词,请设为个人私有");
|
||||
}
|
||||
|
||||
return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId()));
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, hotWordDTO.getTenantId());
|
||||
return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId(), targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改热词")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<HotWordVO> update(@RequestBody HotWordDTO hotWordDTO) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
HotWord existing = hotWordService.getById(hotWordDTO.getId());
|
||||
if (existing == null) return ApiResponse.error("热词不存在");
|
||||
|
||||
boolean isAdmin = isCurrentUserAdmin(loginUser);
|
||||
|
||||
// 核心校验逻辑:
|
||||
// 1. 如果用户尝试将热词设为公开,必须具备管理员权限
|
||||
if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !isAdmin) {
|
||||
return ApiResponse.error("无权将热词设为公开");
|
||||
if (existing == null) {
|
||||
return ApiResponse.error("热词不存在");
|
||||
}
|
||||
|
||||
// 2. 如果是公开热词,只有管理员能改
|
||||
if (Integer.valueOf(1).equals(existing.getIsPublic())) {
|
||||
if (!isAdmin) return ApiResponse.error("无权修改公开热词");
|
||||
} else {
|
||||
// 3. 如果是私有热词,本人或管理员能改
|
||||
if (!existing.getCreatorId().equals(loginUser.getUserId()) && !isAdmin) {
|
||||
return ApiResponse.error("无权修改他人私有热词");
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse.ok(hotWordService.updateHotWord(hotWordDTO));
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
return ApiResponse.ok(hotWordService.updateHotWord(hotWordDTO, loginUser.getUserId(), existing.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除热词")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
HotWord existing = hotWordService.getById(id);
|
||||
if (existing == null) return ApiResponse.ok(true);
|
||||
|
||||
boolean isAdmin = isCurrentUserAdmin(loginUser);
|
||||
|
||||
// 权限校验:公开热词管理员可删,私有热词本人或管理员可删
|
||||
if (Integer.valueOf(1).equals(existing.getIsPublic())) {
|
||||
if (!isAdmin) return ApiResponse.error("无权删除公开热词");
|
||||
} else {
|
||||
if (!existing.getCreatorId().equals(loginUser.getUserId()) && !isAdmin) {
|
||||
return ApiResponse.error("无权删除他人私有热词");
|
||||
}
|
||||
if (existing == null) {
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
return ApiResponse.ok(hotWordService.removeById(id));
|
||||
}
|
||||
|
||||
|
|
@ -111,32 +80,19 @@ public class HotWordController {
|
|||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String word,
|
||||
@RequestParam(required = false) String category,
|
||||
@RequestParam(required = false) Integer isPublic) {
|
||||
|
||||
@RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
boolean isAdmin = isCurrentUserAdmin(loginUser);
|
||||
Long targetTenantId = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) ? tenantId : null;
|
||||
|
||||
LambdaQueryWrapper<HotWord> wrapper = new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getTenantId, loginUser.getTenantId());
|
||||
.like(word != null && !word.isEmpty(), HotWord::getWord, word)
|
||||
.eq(category != null && !category.isEmpty(), HotWord::getCategory, category)
|
||||
.orderByDesc(HotWord::getCreatedAt);
|
||||
wrapper.eq(targetTenantId != null, HotWord::getTenantId, targetTenantId);
|
||||
|
||||
if (!isAdmin) {
|
||||
// 普通用户:只能看到“已公开”的,或者“自己创建”的
|
||||
wrapper.and(w -> w.eq(HotWord::getIsPublic, 1).or().eq(HotWord::getCreatorId, loginUser.getUserId()));
|
||||
}
|
||||
|
||||
// 增加类型过滤
|
||||
if (isPublic != null) {
|
||||
wrapper.eq(HotWord::getIsPublic, isPublic);
|
||||
}
|
||||
|
||||
wrapper.like(word != null && !word.isEmpty(), HotWord::getWord, word)
|
||||
.eq(category != null && !category.isEmpty(), HotWord::getCategory, category)
|
||||
.orderByDesc(HotWord::getIsPublic)
|
||||
.orderByDesc(HotWord::getCreatedAt);
|
||||
|
||||
Page<HotWord> page = hotWordService.page(new Page<>(current, size), wrapper);
|
||||
List<HotWordVO> vos = page.getRecords().stream().map(this::toVO).collect(Collectors.toList());
|
||||
|
||||
|
||||
PageResult<List<HotWordVO>> result = new PageResult<>();
|
||||
result.setTotal(page.getTotal());
|
||||
result.setRecords(vos);
|
||||
|
|
@ -157,14 +113,19 @@ public class HotWordController {
|
|||
vo.setPinyinList(entity.getPinyinList());
|
||||
vo.setMatchStrategy(entity.getMatchStrategy());
|
||||
vo.setCategory(entity.getCategory());
|
||||
vo.setHotWordGroupId(entity.getHotWordGroupId());
|
||||
vo.setWeight(entity.getWeight());
|
||||
vo.setStatus(entity.getStatus());
|
||||
vo.setIsPublic(entity.getIsPublic());
|
||||
vo.setIsPublic(1);
|
||||
vo.setCreatorId(entity.getCreatorId());
|
||||
vo.setIsSynced(entity.getIsSynced());
|
||||
vo.setRemark(entity.getRemark());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
if (entity.getHotWordGroupId() != null) {
|
||||
HotWordGroup group = hotWordGroupService.getById(entity.getHotWordGroupId());
|
||||
vo.setHotWordGroupName(group == null ? null : group.getGroupName());
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ public class PromptTemplateController {
|
|||
return ApiResponse.error("No permission to modify this template");
|
||||
}
|
||||
|
||||
return ApiResponse.ok(promptTemplateService.updateTemplate(dto));
|
||||
return ApiResponse.ok(promptTemplateService.updateTemplate(dto, loginUser.getUserId(), loginUser.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新提示词模板状态")
|
||||
|
|
@ -143,6 +143,20 @@ public class PromptTemplateController {
|
|||
return ApiResponse.ok(promptTemplateService.removeById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询提示词模板详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PromptTemplateVO> detail(@PathVariable Long id) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
return ApiResponse.ok(promptTemplateService.getTemplateDetail(
|
||||
id,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin()
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询提示词模板")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
|
|||
|
|
@ -1,17 +1,44 @@
|
|||
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 HotWordDTO {
|
||||
|
||||
@Schema(description = "热词 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户 ID,平台管理员可传 0 表示平台范围")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "热词内容")
|
||||
private String word;
|
||||
|
||||
@Schema(description = "拼音列表")
|
||||
private List<String> pinyinList;
|
||||
|
||||
@Schema(description = "匹配策略")
|
||||
private Integer matchStrategy;
|
||||
|
||||
@Schema(description = "热词分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "所属热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "权重")
|
||||
private Integer weight;
|
||||
|
||||
@Schema(description = "状态:1-启用,0-禁用")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "是否公开,当前固定为公开")
|
||||
private Integer isPublic;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,57 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "热词信息")
|
||||
public class HotWordVO {
|
||||
|
||||
@Schema(description = "热词 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "热词内容")
|
||||
private String word;
|
||||
|
||||
@Schema(description = "拼音列表")
|
||||
private List<String> pinyinList;
|
||||
|
||||
@Schema(description = "是否公开,当前固定为公开")
|
||||
private Integer isPublic;
|
||||
|
||||
@Schema(description = "创建人 ID")
|
||||
private Long creatorId;
|
||||
|
||||
@Schema(description = "匹配策略")
|
||||
private Integer matchStrategy;
|
||||
|
||||
@Schema(description = "热词分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "所属热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "所属热词组名称")
|
||||
private String hotWordGroupName;
|
||||
|
||||
@Schema(description = "权重")
|
||||
private Integer weight;
|
||||
|
||||
@Schema(description = "状态:1-启用,0-禁用")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "是否已同步")
|
||||
private Integer isSynced;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,42 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "会议总结模板请求参数")
|
||||
public class PromptTemplateDTO {
|
||||
|
||||
@Schema(description = "模板 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户 ID")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "模板名称")
|
||||
private String templateName;
|
||||
|
||||
@Schema(description = "模板描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "模板分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "是否系统模板:1-是,0-否")
|
||||
private Integer isSystem;
|
||||
|
||||
@Schema(description = "标签列表")
|
||||
private java.util.List<String> tags;
|
||||
|
||||
@Schema(description = "绑定热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "模板内容")
|
||||
private String promptContent;
|
||||
|
||||
@Schema(description = "状态:1-启用,0-禁用")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.imeeting.dto.biz;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "提示词模板信息")
|
||||
@Data
|
||||
|
|
@ -23,6 +24,12 @@ public class PromptTemplateVO {
|
|||
private Integer isSystem;
|
||||
@Schema(description = "标签列表")
|
||||
private java.util.List<String> tags;
|
||||
@Schema(description = "绑定热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
@Schema(description = "绑定热词组名称")
|
||||
private String hotWordGroupName;
|
||||
@Schema(description = "绑定热词列表")
|
||||
private List<String> hotWords;
|
||||
@Schema(description = "使用次数")
|
||||
private Integer usageCount;
|
||||
@Schema(description = "提示词正文")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public class RealtimeMeetingRuntimeProfile {
|
|||
private String resolvedSummaryModelName;
|
||||
private Long resolvedPromptId;
|
||||
private String resolvedPromptName;
|
||||
private Long resolvedHotWordGroupId;
|
||||
private String resolvedMode;
|
||||
private String resolvedLanguage;
|
||||
private Integer resolvedUseSpkId;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ public class HotWord extends BaseEntity {
|
|||
@Schema(description = "热词分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "所属热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "权重")
|
||||
private Integer weight;
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ public class PromptTemplate extends BaseEntity {
|
|||
@Schema(description = "业务标签列表")
|
||||
private java.util.List<String> tags;
|
||||
|
||||
@Schema(description = "绑定热词组 ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "使用次数")
|
||||
private Integer usageCount;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,46 @@
|
|||
package com.imeeting.mapper.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface HotWordMapper extends BaseMapper<HotWord> {
|
||||
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Select({
|
||||
"<script>",
|
||||
"SELECT id, tenant_id, word, hot_word_group_id, weight, status, is_synced, remark, created_at, updated_at, is_deleted",
|
||||
"FROM biz_hot_words",
|
||||
"WHERE is_deleted = 0",
|
||||
"AND status = 1",
|
||||
"AND hot_word_group_id = #{groupId}",
|
||||
"ORDER BY created_at DESC",
|
||||
"</script>"
|
||||
})
|
||||
List<HotWord> selectEnabledByGroupIdIgnoreTenant(@Param("groupId") Long groupId);
|
||||
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Select({
|
||||
"<script>",
|
||||
"SELECT id, tenant_id, word, hot_word_group_id, weight, status, is_synced, remark, created_at, updated_at, is_deleted",
|
||||
"FROM biz_hot_words",
|
||||
"WHERE is_deleted = 0",
|
||||
"AND status = 1",
|
||||
"AND hot_word_group_id = #{groupId}",
|
||||
"<if test='words != null and words.size > 0'>",
|
||||
"AND word IN",
|
||||
"<foreach collection='words' item='word' open='(' separator=',' close=')'>",
|
||||
"#{word}",
|
||||
"</foreach>",
|
||||
"</if>",
|
||||
"ORDER BY created_at DESC",
|
||||
"</script>"
|
||||
})
|
||||
List<HotWord> selectEnabledByGroupIdAndWordsIgnoreTenant(@Param("groupId") Long groupId, @Param("words") List<String> words);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import com.imeeting.entity.biz.HotWord;
|
|||
import java.util.List;
|
||||
|
||||
public interface HotWordService extends IService<HotWord> {
|
||||
HotWordVO saveHotWord(HotWordDTO hotWordDTO, Long userId);
|
||||
HotWordVO updateHotWord(HotWordDTO hotWordDTO);
|
||||
HotWordVO saveHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId);
|
||||
HotWordVO updateHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId);
|
||||
List<String> generatePinyin(String word);
|
||||
List<HotWord> listEnabledByGroupIdIgnoreTenant(Long groupId);
|
||||
List<HotWord> listEnabledByGroupIdAndWordsIgnoreTenant(Long groupId, List<String> words);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import java.util.List;
|
|||
|
||||
public interface PromptTemplateService extends IService<PromptTemplate> {
|
||||
PromptTemplateVO saveTemplate(PromptTemplateDTO dto, Long userId, Long tenantId);
|
||||
PromptTemplateVO updateTemplate(PromptTemplateDTO dto);
|
||||
PromptTemplateVO updateTemplate(PromptTemplateDTO dto, Long userId, Long tenantId);
|
||||
PromptTemplateVO getTemplateDetail(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
||||
PageResult<List<PromptTemplateVO>> pageTemplates(Integer current, Integer size, String name, String category,
|
||||
Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
||||
boolean updateUserTemplateStatus(Long templateId, Integer status, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
||||
|
|
|
|||
|
|
@ -275,11 +275,15 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
|
||||
List<Map<String, Object>> hotwords = new ArrayList<>();
|
||||
Object hotWordsObj = taskRecord.getTaskConfig().get("hotWords");
|
||||
Object hotWordGroupIdObj = taskRecord.getTaskConfig().get("hotWordGroupId");
|
||||
if (hotWordsObj instanceof List) {
|
||||
List<String> words = (List<String>) hotWordsObj;
|
||||
if (!words.isEmpty()) {
|
||||
List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getTenantId, meeting.getTenantId()).in(HotWord::getWord, words));
|
||||
List<HotWord> entities = hotWordGroupIdObj instanceof Number groupId
|
||||
? hotWordService.listEnabledByGroupIdAndWordsIgnoreTenant(groupId.longValue(), words)
|
||||
: hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getStatus, 1)
|
||||
.in(HotWord::getWord, words));
|
||||
Map<String, Integer> weightMap = entities.stream()
|
||||
.collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1));
|
||||
for (String w : words) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
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.imeeting.mapper.biz.HotWordMapper;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
||||
|
|
@ -24,34 +26,41 @@ import java.util.stream.Collectors;
|
|||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> implements HotWordService {
|
||||
|
||||
private static final int MAX_HOT_WORDS_PER_GROUP = 200;
|
||||
|
||||
private final HotWordGroupMapper hotWordGroupMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotWordVO saveHotWord(HotWordDTO hotWordDTO, Long userId) {
|
||||
public HotWordVO saveHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId) {
|
||||
HotWord hotWord = new HotWord();
|
||||
copyProperties(hotWordDTO, hotWord);
|
||||
hotWord.setCreatorId(userId);
|
||||
|
||||
hotWord.setHotWordGroupId(validateGroup(hotWordDTO.getHotWordGroupId(), tenantId, null));
|
||||
|
||||
if (hotWord.getPinyinList() == null || hotWord.getPinyinList().isEmpty()) {
|
||||
hotWord.setPinyinList(generatePinyin(hotWord.getWord()));
|
||||
}
|
||||
|
||||
|
||||
this.save(hotWord);
|
||||
return toVO(hotWord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotWordVO updateHotWord(HotWordDTO hotWordDTO) {
|
||||
public HotWordVO updateHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId) {
|
||||
HotWord hotWord = this.getById(hotWordDTO.getId());
|
||||
if (hotWord == null) {
|
||||
throw new RuntimeException("Hotword not found");
|
||||
throw new IllegalArgumentException("热词不存在");
|
||||
}
|
||||
|
||||
String oldWord = hotWord.getWord();
|
||||
copyProperties(hotWordDTO, hotWord);
|
||||
|
||||
hotWord.setHotWordGroupId(validateGroup(hotWordDTO.getHotWordGroupId(), tenantId, hotWord.getId()));
|
||||
|
||||
if (!oldWord.equals(hotWord.getWord()) && (hotWordDTO.getPinyinList() == null || hotWordDTO.getPinyinList().isEmpty())) {
|
||||
hotWord.setPinyinList(generatePinyin(hotWord.getWord()));
|
||||
}
|
||||
|
|
@ -62,7 +71,9 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
|
||||
@Override
|
||||
public List<String> generatePinyin(String word) {
|
||||
if (word == null || word.isEmpty()) return Collections.emptyList();
|
||||
if (word == null || word.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
||||
|
|
@ -75,7 +86,9 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c, format);
|
||||
if (pinyins != null) {
|
||||
for (String py : pinyins) {
|
||||
if (!charPinyins.contains(py)) charPinyins.add(py);
|
||||
if (!charPinyins.contains(py)) {
|
||||
charPinyins.add(py);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
charPinyins.add(String.valueOf(c));
|
||||
|
|
@ -91,6 +104,42 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
return combinations.stream().limit(5).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HotWord> listEnabledByGroupIdIgnoreTenant(Long groupId) {
|
||||
if (groupId == null) {
|
||||
return List.of();
|
||||
}
|
||||
return baseMapper.selectEnabledByGroupIdIgnoreTenant(groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HotWord> listEnabledByGroupIdAndWordsIgnoreTenant(Long groupId, List<String> words) {
|
||||
if (groupId == null) {
|
||||
return List.of();
|
||||
}
|
||||
return baseMapper.selectEnabledByGroupIdAndWordsIgnoreTenant(groupId, words);
|
||||
}
|
||||
|
||||
private Long validateGroup(Long groupId, Long tenantId, Long currentHotWordId) {
|
||||
if (groupId == null) {
|
||||
return null;
|
||||
}
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(groupId);
|
||||
if (group == null || !tenantId.equals(group.getTenantId())) {
|
||||
throw new IllegalArgumentException("热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(group.getStatus())) {
|
||||
throw new IllegalArgumentException("热词组已禁用");
|
||||
}
|
||||
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getHotWordGroupId, groupId)
|
||||
.ne(currentHotWordId != null, HotWord::getId, currentHotWordId));
|
||||
if (currentCount >= MAX_HOT_WORDS_PER_GROUP) {
|
||||
throw new IllegalArgumentException("热词组最多只能包含 200 个热词");
|
||||
}
|
||||
return group.getId();
|
||||
}
|
||||
|
||||
private void generateCombinations(List<List<String>> matrix, int index, String current, List<String> result) {
|
||||
if (index == matrix.size()) {
|
||||
result.add(current.trim());
|
||||
|
|
@ -106,9 +155,10 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
entity.setPinyinList(dto.getPinyinList());
|
||||
entity.setMatchStrategy(dto.getMatchStrategy());
|
||||
entity.setCategory(dto.getCategory());
|
||||
entity.setHotWordGroupId(dto.getHotWordGroupId());
|
||||
entity.setWeight(dto.getWeight());
|
||||
entity.setStatus(dto.getStatus());
|
||||
entity.setIsPublic(dto.getIsPublic());
|
||||
entity.setIsPublic(1);
|
||||
entity.setRemark(dto.getRemark());
|
||||
}
|
||||
|
||||
|
|
@ -119,14 +169,19 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
vo.setPinyinList(entity.getPinyinList());
|
||||
vo.setMatchStrategy(entity.getMatchStrategy());
|
||||
vo.setCategory(entity.getCategory());
|
||||
vo.setHotWordGroupId(entity.getHotWordGroupId());
|
||||
vo.setWeight(entity.getWeight());
|
||||
vo.setStatus(entity.getStatus());
|
||||
vo.setIsPublic(entity.getIsPublic());
|
||||
vo.setIsPublic(1);
|
||||
vo.setCreatorId(entity.getCreatorId());
|
||||
vo.setIsSynced(entity.getIsSynced());
|
||||
vo.setRemark(entity.getRemark());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
if (entity.getHotWordGroupId() != null) {
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(entity.getHotWordGroupId());
|
||||
vo.setHotWordGroupName(group == null ? null : group.getGroupName());
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,16 +77,18 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
asrConfig.put("useSpkId", runtimeProfile.getResolvedUseSpkId());
|
||||
asrConfig.put("enableTextRefine", runtimeProfile.getResolvedEnableTextRefine());
|
||||
|
||||
List<String> finalHotWords = command.getHotWords();
|
||||
List<String> finalHotWords = runtimeProfile.getResolvedHotWords();
|
||||
if (finalHotWords == null || finalHotWords.isEmpty()) {
|
||||
finalHotWords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getTenantId, meeting.getTenantId())
|
||||
.eq(HotWord::getStatus, 1))
|
||||
.stream()
|
||||
.map(HotWord::getWord)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
asrConfig.put("hotWords", finalHotWords);
|
||||
if (runtimeProfile.getResolvedHotWordGroupId() != null) {
|
||||
asrConfig.put("hotWordGroupId", runtimeProfile.getResolvedHotWordGroupId());
|
||||
}
|
||||
asrTask.setTaskConfig(asrConfig);
|
||||
aiTaskService.save(asrTask);
|
||||
|
||||
|
|
@ -579,7 +581,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
resumeConfig.setEnableItn(runtimeProfile.getResolvedEnableItn());
|
||||
resumeConfig.setEnableTextRefine(runtimeProfile.getResolvedEnableTextRefine());
|
||||
resumeConfig.setSaveAudio(runtimeProfile.getResolvedSaveAudio());
|
||||
resumeConfig.setHotwords(resolveRealtimeHotwords(runtimeProfile.getResolvedHotWords(), tenantId));
|
||||
resumeConfig.setHotwords(resolveRealtimeHotwords(runtimeProfile.getResolvedHotWords(), runtimeProfile.getResolvedHotWordGroupId()));
|
||||
return resumeConfig;
|
||||
}
|
||||
|
||||
|
|
@ -617,14 +619,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> resolveRealtimeHotwords(List<String> selectedWords, Long tenantId) {
|
||||
List<HotWord> tenantHotwords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getTenantId, tenantId)
|
||||
.eq(HotWord::getStatus, 1));
|
||||
if (tenantHotwords == null || tenantHotwords.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> resolveRealtimeHotwords(List<String> selectedWords, Long hotWordGroupId) {
|
||||
List<String> effectiveWords = selectedWords == null
|
||||
? List.of()
|
||||
: selectedWords.stream()
|
||||
|
|
@ -632,9 +627,15 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
.map(String::trim)
|
||||
.filter(word -> !word.isEmpty())
|
||||
.toList();
|
||||
|
||||
return tenantHotwords.stream()
|
||||
.filter(item -> effectiveWords.isEmpty() || effectiveWords.contains(item.getWord()))
|
||||
List<HotWord> resolvedHotwords = hotWordGroupId == null
|
||||
? hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getStatus, 1)
|
||||
.in(!effectiveWords.isEmpty(), HotWord::getWord, effectiveWords))
|
||||
: hotWordService.listEnabledByGroupIdAndWordsIgnoreTenant(hotWordGroupId, effectiveWords);
|
||||
if (resolvedHotwords == null || resolvedHotwords.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return resolvedHotwords.stream()
|
||||
.map(this::toRealtimeHotword)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ 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.RealtimeMeetingRuntimeProfile;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import com.imeeting.entity.biz.AsrModel;
|
||||
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.HotWordService;
|
||||
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
|
@ -23,6 +25,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
|||
|
||||
private final AiModelService aiModelService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final HotWordService hotWordService;
|
||||
private final AsrModelMapper asrModelMapper;
|
||||
private final LlmModelMapper llmModelMapper;
|
||||
|
||||
|
|
@ -51,6 +54,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
|||
profile.setResolvedSummaryModelName(summaryModel.getModelName());
|
||||
profile.setResolvedPromptId(promptTemplate.getId());
|
||||
profile.setResolvedPromptName(promptTemplate.getTemplateName());
|
||||
profile.setResolvedHotWordGroupId(resolveHotWordGroupId(promptTemplate, hotWords));
|
||||
profile.setResolvedMode(nonBlank(mode, "2pass"));
|
||||
profile.setResolvedLanguage(nonBlank(language, "auto"));
|
||||
profile.setResolvedUseSpkId(useSpkId != null ? useSpkId : 1);
|
||||
|
|
@ -58,13 +62,43 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
|||
profile.setResolvedEnableItn(enableItn != null ? enableItn : Boolean.TRUE);
|
||||
profile.setResolvedEnableTextRefine(Boolean.TRUE.equals(enableTextRefine));
|
||||
profile.setResolvedSaveAudio(Boolean.TRUE.equals(saveAudio));
|
||||
profile.setResolvedHotWords(hotWords == null ? List.of() : hotWords.stream()
|
||||
profile.setResolvedHotWords(resolveHotWords(promptTemplate, hotWords));
|
||||
return profile;
|
||||
}
|
||||
|
||||
private Long resolveHotWordGroupId(PromptTemplate promptTemplate, List<String> hotWords) {
|
||||
List<String> normalized = normalizeHotWords(hotWords);
|
||||
if (!normalized.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return promptTemplate == null ? null : promptTemplate.getHotWordGroupId();
|
||||
}
|
||||
|
||||
private List<String> resolveHotWords(PromptTemplate promptTemplate, List<String> hotWords) {
|
||||
List<String> normalized = normalizeHotWords(hotWords);
|
||||
if (!normalized.isEmpty()) {
|
||||
return normalized;
|
||||
}
|
||||
if (promptTemplate == null || promptTemplate.getHotWordGroupId() == null) {
|
||||
return List.of();
|
||||
}
|
||||
return hotWordService.listEnabledByGroupIdIgnoreTenant(promptTemplate.getHotWordGroupId())
|
||||
.stream()
|
||||
.map(HotWord::getWord)
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(item -> !item.isEmpty())
|
||||
.distinct()
|
||||
.toList());
|
||||
return profile;
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<String> normalizeHotWords(List<String> hotWords) {
|
||||
return hotWords == null ? List.of() : hotWords.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(item -> !item.isEmpty())
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private AiModelVO resolveModel(String type, Long requestedId, Long tenantId) {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@ package com.imeeting.service.biz.impl;
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.imeeting.dto.biz.PromptTemplateDTO;
|
||||
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import com.imeeting.entity.biz.HotWordGroup;
|
||||
import com.imeeting.entity.biz.PromptTemplate;
|
||||
import com.imeeting.entity.biz.PromptTemplateUserConfig;
|
||||
import com.imeeting.mapper.biz.HotWordGroupMapper;
|
||||
import com.imeeting.mapper.biz.PromptTemplateMapper;
|
||||
import com.imeeting.mapper.biz.PromptTemplateUserConfigMapper;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
|
@ -18,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
|
|
@ -25,44 +30,44 @@ import java.util.stream.Collectors;
|
|||
public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper, PromptTemplate> implements PromptTemplateService {
|
||||
|
||||
private final PromptTemplateUserConfigMapper userConfigMapper;
|
||||
private final HotWordGroupMapper hotWordGroupMapper;
|
||||
private final HotWordService hotWordService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PromptTemplateVO saveTemplate(PromptTemplateDTO dto, Long userId, Long tenantId) {
|
||||
PromptTemplate entity = new PromptTemplate();
|
||||
copyProperties(dto, entity);
|
||||
|
||||
entity.setCreatorId(userId);
|
||||
|
||||
if (dto.getTenantId() != null && dto.getTenantId() == 0L) {
|
||||
entity.setTenantId(0L);
|
||||
} else {
|
||||
entity.setTenantId(tenantId);
|
||||
}
|
||||
|
||||
validateHotWordGroupBinding(dto.getHotWordGroupId(), entity.getTenantId());
|
||||
entity.setUsageCount(0);
|
||||
this.save(entity);
|
||||
return toVO(entity, entity.getStatus());
|
||||
return toVO(entity, entity.getStatus(), queryHotWordGroupMap(java.util.Collections.singletonList(entity.getHotWordGroupId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PromptTemplateVO updateTemplate(PromptTemplateDTO dto) {
|
||||
public PromptTemplateVO updateTemplate(PromptTemplateDTO dto, Long userId, Long tenantId) {
|
||||
PromptTemplate entity = this.getById(dto.getId());
|
||||
if (entity == null) {
|
||||
throw new RuntimeException("Template not found");
|
||||
throw new IllegalArgumentException("模板不存在");
|
||||
}
|
||||
Long targetTenantId = Long.valueOf(0L).equals(entity.getTenantId()) ? 0L : tenantId;
|
||||
validateHotWordGroupBinding(dto.getHotWordGroupId(), targetTenantId);
|
||||
copyProperties(dto, entity);
|
||||
this.updateById(entity);
|
||||
return toVO(entity, entity.getStatus());
|
||||
return toVO(entity, entity.getStatus(), queryHotWordGroupMap(java.util.Collections.singletonList(entity.getHotWordGroupId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<List<PromptTemplateVO>> pageTemplates(Integer current, Integer size, String name, String category,
|
||||
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)
|
||||
.orderByDesc(PromptTemplate::getIsSystem)
|
||||
|
|
@ -71,8 +76,10 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
Page<PromptTemplate> page = this.page(new Page<>(current, size), wrapper);
|
||||
List<PromptTemplate> records = page.getRecords();
|
||||
Map<Long, Integer> userStatusMap = queryUserStatusMap(tenantId, userId, records.stream().map(PromptTemplate::getId).collect(Collectors.toList()));
|
||||
Map<Long, HotWordGroup> hotWordGroupMap = queryHotWordGroupMap(records.stream().map(PromptTemplate::getHotWordGroupId).toList());
|
||||
|
||||
List<PromptTemplateVO> vos = records.stream()
|
||||
.map(template -> toVO(template, effectiveStatus(template.getStatus(), userStatusMap.get(template.getId()))))
|
||||
.map(template -> toVO(template, effectiveStatus(template.getStatus(), userStatusMap.get(template.getId())), hotWordGroupMap))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PageResult<List<PromptTemplateVO>> result = new PageResult<>();
|
||||
|
|
@ -81,6 +88,20 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PromptTemplateVO getTemplateDetail(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||
PromptTemplate template = this.getOne(buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin)
|
||||
.eq(PromptTemplate::getId, templateId)
|
||||
.last("LIMIT 1"));
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("模板不存在");
|
||||
}
|
||||
Map<Long, HotWordGroup> hotWordGroupMap = queryHotWordGroupMap(List.of(template.getHotWordGroupId()));
|
||||
PromptTemplateVO vo = toVO(template, template.getStatus(), hotWordGroupMap);
|
||||
vo.setHotWords(resolveHotWords(template.getHotWordGroupId()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateUserTemplateStatus(Long templateId, Integer status, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||
|
|
@ -128,6 +149,29 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
return effectiveStatus(template.getStatus(), userStatus) == 1;
|
||||
}
|
||||
|
||||
private void validateHotWordGroupBinding(Long hotWordGroupId, Long templateTenantId) {
|
||||
if (hotWordGroupId == null) {
|
||||
return;
|
||||
}
|
||||
if (Long.valueOf(0L).equals(templateTenantId)) {
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(hotWordGroupId);
|
||||
if (group == null || !Long.valueOf(0L).equals(group.getTenantId())) {
|
||||
throw new IllegalArgumentException("平台级模板只能绑定平台级热词组");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(group.getStatus())) {
|
||||
throw new IllegalArgumentException("绑定的热词组已禁用");
|
||||
}
|
||||
return;
|
||||
}
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(hotWordGroupId);
|
||||
if (group == null || !templateTenantId.equals(group.getTenantId())) {
|
||||
throw new IllegalArgumentException("绑定的热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(group.getStatus())) {
|
||||
throw new IllegalArgumentException("绑定的热词组已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<PromptTemplate> buildVisibilityWrapper(Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||
LambdaQueryWrapper<PromptTemplate> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.and(w -> w
|
||||
|
|
@ -154,6 +198,30 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
return statusMap;
|
||||
}
|
||||
|
||||
private Map<Long, HotWordGroup> queryHotWordGroupMap(List<Long> hotWordGroupIds) {
|
||||
List<Long> ids = hotWordGroupIds == null ? List.of() : hotWordGroupIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (ids.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return hotWordGroupMapper.selectByIdsIgnoreTenant(ids).stream()
|
||||
.collect(Collectors.toMap(HotWordGroup::getId, item -> item));
|
||||
}
|
||||
|
||||
private List<String> resolveHotWords(Long hotWordGroupId) {
|
||||
if (hotWordGroupId == null) {
|
||||
return List.of();
|
||||
}
|
||||
return hotWordService.listEnabledByGroupIdIgnoreTenant(hotWordGroupId).stream()
|
||||
.map(HotWord::getWord)
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(item -> !item.isEmpty())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Integer effectiveStatus(Integer templateStatus, Integer userStatus) {
|
||||
if (userStatus != null) {
|
||||
return userStatus;
|
||||
|
|
@ -167,13 +235,14 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
entity.setCategory(dto.getCategory());
|
||||
entity.setIsSystem(dto.getIsSystem());
|
||||
entity.setTenantId(dto.getTenantId());
|
||||
entity.setPromptContent(dto.getPromptContent());
|
||||
entity.setTags(dto.getTags());
|
||||
entity.setHotWordGroupId(dto.getHotWordGroupId());
|
||||
entity.setPromptContent(dto.getPromptContent());
|
||||
entity.setStatus(dto.getStatus());
|
||||
entity.setRemark(dto.getRemark());
|
||||
}
|
||||
|
||||
private PromptTemplateVO toVO(PromptTemplate entity, Integer status) {
|
||||
private PromptTemplateVO toVO(PromptTemplate entity, Integer status, Map<Long, HotWordGroup> hotWordGroupMap) {
|
||||
PromptTemplateVO vo = new PromptTemplateVO();
|
||||
vo.setId(entity.getId());
|
||||
vo.setTenantId(entity.getTenantId());
|
||||
|
|
@ -183,6 +252,10 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
|||
vo.setCategory(entity.getCategory());
|
||||
vo.setIsSystem(entity.getIsSystem());
|
||||
vo.setTags(entity.getTags());
|
||||
Long hotWordGroupId = entity.getHotWordGroupId();
|
||||
vo.setHotWordGroupId(hotWordGroupId);
|
||||
HotWordGroup group = hotWordGroupId == null ? null : hotWordGroupMap.get(hotWordGroupId);
|
||||
vo.setHotWordGroupName(group == null ? null : group.getGroupName());
|
||||
vo.setUsageCount(entity.getUsageCount());
|
||||
vo.setPromptContent(entity.getPromptContent());
|
||||
vo.setStatus(status);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,13 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
resumeConfig.setEnableItn(enableItn);
|
||||
resumeConfig.setEnableTextRefine(enableTextRefine);
|
||||
resumeConfig.setSaveAudio(saveAudio);
|
||||
resumeConfig.setHotwords(hotwords);
|
||||
RealtimeMeetingResumeConfig existingConfig = realtimeMeetingSessionStateService.getStatus(meetingId) == null
|
||||
? null
|
||||
: realtimeMeetingSessionStateService.getStatus(meetingId).getResumeConfig();
|
||||
List<Map<String, Object>> effectiveHotwords = (hotwords == null || hotwords.isEmpty())
|
||||
? (existingConfig == null ? List.of() : existingConfig.getHotwords())
|
||||
: hotwords;
|
||||
resumeConfig.setHotwords(effectiveHotwords);
|
||||
realtimeMeetingSessionStateService.rememberResumeConfig(meetingId, resumeConfig);
|
||||
|
||||
RealtimeSocketSessionData sessionData = new RealtimeSocketSessionData();
|
||||
|
|
@ -107,7 +113,7 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
enableItn,
|
||||
enableTextRefine,
|
||||
saveAudio,
|
||||
hotwords
|
||||
effectiveHotwords
|
||||
));
|
||||
return vo;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
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.RealtimeMeetingRuntimeProfile;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
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.HotWordService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
|
@ -24,9 +27,11 @@ class MeetingRuntimeProfileResolverImplTest {
|
|||
void resolveShouldUseRequestedResourcesAndNormalizeHotWords() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
hotWordService,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
|
@ -70,9 +75,11 @@ class MeetingRuntimeProfileResolverImplTest {
|
|||
void resolveShouldRejectCrossTenantModel() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
hotWordService,
|
||||
mock(AsrModelMapper.class),
|
||||
mock(LlmModelMapper.class)
|
||||
);
|
||||
|
|
@ -95,6 +102,50 @@ class MeetingRuntimeProfileResolverImplTest {
|
|||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveShouldUseTemplateBoundGroupWhenNoExplicitHotWords() {
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||
aiModelService,
|
||||
promptTemplateService,
|
||||
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
|
||||
);
|
||||
|
||||
assertEquals(99L, profile.getResolvedHotWordGroupId());
|
||||
assertIterableEquals(List.of("OpenAI", "Codex"), profile.getResolvedHotWords());
|
||||
}
|
||||
|
||||
private AiModelVO enabledModel(Long id, Long tenantId, String name) {
|
||||
AiModelVO model = new AiModelVO();
|
||||
model.setId(id);
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ export interface HotWordVO {
|
|||
id: number;
|
||||
word: string;
|
||||
pinyinList: string[];
|
||||
isPublic: number;
|
||||
creatorId: number;
|
||||
matchStrategy: number;
|
||||
category: string;
|
||||
hotWordGroupId?: number;
|
||||
hotWordGroupName?: string;
|
||||
weight: number;
|
||||
status: number;
|
||||
isSynced: number;
|
||||
|
|
@ -18,22 +19,23 @@ export interface HotWordVO {
|
|||
|
||||
export interface HotWordDTO {
|
||||
id?: number;
|
||||
tenantId?: number;
|
||||
word: string;
|
||||
pinyinList?: string[];
|
||||
matchStrategy: number;
|
||||
category?: string;
|
||||
hotWordGroupId?: number;
|
||||
weight: number;
|
||||
status: number;
|
||||
isPublic?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export const getHotWordPage = (params: {
|
||||
current: number;
|
||||
size: number;
|
||||
word?: string;
|
||||
export const getHotWordPage = (params: {
|
||||
current: number;
|
||||
size: number;
|
||||
word?: string;
|
||||
category?: string;
|
||||
isPublic?: number;
|
||||
tenantId?: number;
|
||||
}) => {
|
||||
return http.get<{ code: string; data: { records: HotWordVO[]; total: number }; msg: string }>(
|
||||
"/api/biz/hotword/page",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ export interface PromptTemplateVO {
|
|||
category: string;
|
||||
isSystem: number;
|
||||
tags?: string[];
|
||||
hotWordGroupId?: number;
|
||||
hotWordGroupName?: string;
|
||||
hotWords?: string[];
|
||||
usageCount: number;
|
||||
promptContent: string;
|
||||
status: number;
|
||||
|
|
@ -24,15 +27,16 @@ export interface PromptTemplateDTO {
|
|||
category: string;
|
||||
isSystem: number;
|
||||
tags?: string[];
|
||||
hotWordGroupId?: number;
|
||||
promptContent: string;
|
||||
status: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export const getPromptPage = (params: {
|
||||
current: number;
|
||||
size: number;
|
||||
name?: string;
|
||||
export const getPromptPage = (params: {
|
||||
current: number;
|
||||
size: number;
|
||||
name?: string;
|
||||
category?: string;
|
||||
}) => {
|
||||
return http.get<{ code: string; data: { records: PromptTemplateVO[]; total: number }; msg: string }>(
|
||||
|
|
@ -41,6 +45,12 @@ export const getPromptPage = (params: {
|
|||
);
|
||||
};
|
||||
|
||||
export const getPromptDetail = (id: number) => {
|
||||
return http.get<{ code: string; data: PromptTemplateVO; msg: string }>(
|
||||
`/api/biz/prompt/${id}`
|
||||
);
|
||||
};
|
||||
|
||||
export const savePromptTemplate = (data: PromptTemplateDTO) => {
|
||||
return http.post<{ code: string; data: PromptTemplateVO; msg: string }>(
|
||||
"/api/biz/prompt",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,28 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Badge, Button, Card, Col, Form, Input, InputNumber, Modal, Popconfirm, Radio, Row, Select, Space, Table, Tag, Tooltip, Typography, App } from 'antd';
|
||||
import {
|
||||
App,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
GlobalOutlined,
|
||||
FolderOpenOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDict } from "../../hooks/useDict";
|
||||
|
|
@ -18,6 +34,14 @@ import {
|
|||
updateHotWord,
|
||||
type HotWordVO,
|
||||
} from "../../api/business/hotword";
|
||||
import {
|
||||
deleteHotWordGroup,
|
||||
getHotWordGroupOptions,
|
||||
getHotWordGroupPage,
|
||||
saveHotWordGroup,
|
||||
updateHotWordGroup,
|
||||
type HotWordGroupVO,
|
||||
} from "../../api/business/hotwordGroup";
|
||||
import AppPagination from "../../components/shared/AppPagination";
|
||||
|
||||
const { Option } = Select;
|
||||
|
|
@ -27,9 +51,15 @@ type HotWordFormValues = {
|
|||
word: string;
|
||||
pinyin?: string;
|
||||
category?: string;
|
||||
hotWordGroupId?: number;
|
||||
weight: number;
|
||||
status: number;
|
||||
isPublic: number;
|
||||
remark?: string;
|
||||
};
|
||||
|
||||
type HotWordGroupFormValues = {
|
||||
groupName: string;
|
||||
status: number;
|
||||
remark?: string;
|
||||
};
|
||||
|
||||
|
|
@ -37,7 +67,15 @@ const HotWords: React.FC = () => {
|
|||
const { message } = App.useApp();
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<HotWordFormValues>();
|
||||
const [groupForm] = Form.useForm<HotWordGroupFormValues>();
|
||||
const { items: categories } = useDict("biz_hotword_category");
|
||||
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 [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<HotWordVO[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
|
@ -45,23 +83,31 @@ const HotWords: React.FC = () => {
|
|||
const [size, setSize] = useState(10);
|
||||
const [searchWord, setSearchWord] = useState("");
|
||||
const [searchCategory, setSearchCategory] = useState<string | undefined>(undefined);
|
||||
const [searchType, setSearchType] = useState<number | undefined>(undefined);
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
|
||||
const userProfile = useMemo(() => {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
return profileStr ? JSON.parse(profileStr) : {};
|
||||
}, []);
|
||||
const [groupOptions, setGroupOptions] = useState<HotWordGroupVO[]>([]);
|
||||
const [groupManageVisible, setGroupManageVisible] = useState(false);
|
||||
const [groupEditorVisible, setGroupEditorVisible] = useState(false);
|
||||
const [groupLoading, setGroupLoading] = useState(false);
|
||||
const [groupSubmitLoading, setGroupSubmitLoading] = useState(false);
|
||||
const [groupData, setGroupData] = useState<HotWordGroupVO[]>([]);
|
||||
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
|
||||
|
||||
const isAdmin = useMemo(() => {
|
||||
return userProfile.isPlatformAdmin === true || userProfile.isTenantAdmin === true;
|
||||
}, [userProfile]);
|
||||
const groupNameMap = useMemo(
|
||||
() => Object.fromEntries(groupOptions.map((item) => [item.id, item.groupName])) as Record<number, string>,
|
||||
[groupOptions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchData();
|
||||
}, [current, searchCategory, searchType, searchWord, size]);
|
||||
}, [current, searchCategory, searchWord, size]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadGroupOptions();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -71,7 +117,7 @@ const HotWords: React.FC = () => {
|
|||
size,
|
||||
word: searchWord,
|
||||
category: searchCategory,
|
||||
isPublic: searchType,
|
||||
tenantId: isPlatformAdmin ? activeTenantId : undefined,
|
||||
});
|
||||
if (res.data?.data) {
|
||||
setData(res.data.data.records);
|
||||
|
|
@ -82,53 +128,62 @@ const HotWords: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleOpenModal = (record?: HotWordVO) => {
|
||||
if (record?.isPublic === 1 && !isAdmin) {
|
||||
message.error("公开热词仅限管理员修改");
|
||||
return;
|
||||
}
|
||||
const loadGroupOptions = async () => {
|
||||
const res = await getHotWordGroupOptions(isPlatformAdmin ? activeTenantId : undefined);
|
||||
setGroupOptions(res.data?.data || []);
|
||||
};
|
||||
|
||||
const loadGroupPage = async () => {
|
||||
setGroupLoading(true);
|
||||
try {
|
||||
const res = await getHotWordGroupPage({ current: 1, size: 200 });
|
||||
if (isPlatformAdmin) {
|
||||
const scoped = await getHotWordGroupPage({ current: 1, size: 200, tenantId: activeTenantId });
|
||||
setGroupData(scoped.data?.data?.records || []);
|
||||
return;
|
||||
}
|
||||
setGroupData(res.data?.data?.records || []);
|
||||
} finally {
|
||||
setGroupLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenModal = (record?: HotWordVO) => {
|
||||
if (record) {
|
||||
setEditingId(record.id);
|
||||
form.setFieldsValue({
|
||||
word: record.word,
|
||||
pinyin: record.pinyinList?.[0] || "",
|
||||
category: record.category,
|
||||
hotWordGroupId: record.hotWordGroupId,
|
||||
weight: record.weight,
|
||||
status: record.status,
|
||||
isPublic: record.isPublic,
|
||||
remark: record.remark,
|
||||
});
|
||||
} else {
|
||||
setEditingId(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ weight: 2, status: 1, isPublic: 0 });
|
||||
form.setFieldsValue({ weight: 2, status: 1 });
|
||||
}
|
||||
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await deleteHotWord(id);
|
||||
message.success("删除成功");
|
||||
await fetchData();
|
||||
} catch {
|
||||
// handled by interceptor
|
||||
}
|
||||
await deleteHotWord(id);
|
||||
message.success("删除成功");
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitLoading(true);
|
||||
|
||||
const payload = {
|
||||
...values,
|
||||
tenantId: isPlatformAdmin ? activeTenantId : undefined,
|
||||
matchStrategy: 1,
|
||||
pinyinList: values.pinyin ? [values.pinyin.trim()] : [],
|
||||
};
|
||||
|
||||
if (editingId) {
|
||||
await updateHotWord({ ...payload, id: editingId });
|
||||
message.success("更新成功");
|
||||
|
|
@ -136,9 +191,8 @@ const HotWords: React.FC = () => {
|
|||
await saveHotWord(payload);
|
||||
message.success("新增成功");
|
||||
}
|
||||
|
||||
setModalVisible(false);
|
||||
await fetchData();
|
||||
await Promise.all([fetchData(), loadGroupOptions(), groupManageVisible ? loadGroupPage() : Promise.resolve()]);
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
|
|
@ -149,7 +203,6 @@ const HotWords: React.FC = () => {
|
|||
if (!word || form.getFieldValue("pinyin")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getPinyinSuggestion(word);
|
||||
const firstPinyin = res.data?.data?.[0];
|
||||
|
|
@ -161,45 +214,79 @@ const HotWords: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const openGroupManager = async () => {
|
||||
setGroupManageVisible(true);
|
||||
await loadGroupPage();
|
||||
};
|
||||
|
||||
const openGroupEditor = (record?: HotWordGroupVO) => {
|
||||
if (record) {
|
||||
setEditingGroupId(record.id);
|
||||
groupForm.setFieldsValue({
|
||||
groupName: record.groupName,
|
||||
status: record.status,
|
||||
remark: record.remark,
|
||||
});
|
||||
} else {
|
||||
setEditingGroupId(null);
|
||||
groupForm.resetFields();
|
||||
groupForm.setFieldsValue({ status: 1 });
|
||||
}
|
||||
setGroupEditorVisible(true);
|
||||
};
|
||||
|
||||
const handleGroupSubmit = async () => {
|
||||
try {
|
||||
const values = await groupForm.validateFields();
|
||||
setGroupSubmitLoading(true);
|
||||
if (editingGroupId) {
|
||||
await updateHotWordGroup({ ...values, id: editingGroupId, tenantId: isPlatformAdmin ? activeTenantId : undefined });
|
||||
message.success("热词组更新成功");
|
||||
} else {
|
||||
await saveHotWordGroup({ ...values, tenantId: isPlatformAdmin ? activeTenantId : undefined });
|
||||
message.success("热词组创建成功");
|
||||
}
|
||||
setGroupEditorVisible(false);
|
||||
await Promise.all([loadGroupOptions(), loadGroupPage()]);
|
||||
} finally {
|
||||
setGroupSubmitLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteGroup = async (id: number) => {
|
||||
await deleteHotWordGroup(id, isPlatformAdmin ? activeTenantId : undefined);
|
||||
message.success("热词组删除成功");
|
||||
await Promise.all([loadGroupOptions(), loadGroupPage(), fetchData()]);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "热词原文",
|
||||
dataIndex: "word",
|
||||
key: "word",
|
||||
ellipsis: true,
|
||||
render: (text: string, record: HotWordVO) => (
|
||||
<Space style={{ display: 'flex', width: '100%', overflow: 'hidden' }}>
|
||||
<Text strong ellipsis={{ tooltip: text }} style={{ maxWidth: 200, display: 'block' }}>{text}</Text>
|
||||
{record.isPublic === 1 ? (
|
||||
<Tooltip title="租户公开">
|
||||
<GlobalOutlined style={{ color: "#52c41a", flexShrink: 0 }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="个人私有">
|
||||
<UserOutlined style={{ color: "#1890ff", flexShrink: 0 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
render: (text: string) => <Text strong ellipsis={{ tooltip: text }}>{text}</Text>,
|
||||
},
|
||||
{
|
||||
title: "拼音",
|
||||
dataIndex: "pinyinList",
|
||||
key: "pinyinList",
|
||||
render: (list: string[]) =>
|
||||
list?.[0] ? <Tag style={{ borderRadius: 4 }}>{list[0]}</Tag> : <Text type="secondary">-</Text>,
|
||||
render: (list: string[]) => list?.[0] ? <Tag style={{ borderRadius: 4 }}>{list[0]}</Tag> : <Text type="secondary">-</Text>,
|
||||
},
|
||||
{
|
||||
title: "类别",
|
||||
title: "分类",
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
render: (value: string) => categories.find((item) => item.itemValue === value)?.itemLabel || value || "-",
|
||||
},
|
||||
{
|
||||
title: "范围",
|
||||
dataIndex: "isPublic",
|
||||
key: "isPublic",
|
||||
render: (value: number) => (value === 1 ? <Tag color="green">公开</Tag> : <Tag color="blue">私有</Tag>),
|
||||
title: "热词组",
|
||||
dataIndex: "hotWordGroupId",
|
||||
key: "hotWordGroupId",
|
||||
render: (value?: number, record?: HotWordVO) => {
|
||||
const name = record?.hotWordGroupName || (value ? groupNameMap[value] : undefined);
|
||||
return name ? <Tag color="blue">{name}</Tag> : <Text type="secondary">未分组</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "权重",
|
||||
|
|
@ -211,38 +298,77 @@ const HotWords: React.FC = () => {
|
|||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (value: number) =>
|
||||
value === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />,
|
||||
render: (value: number) => value === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_: unknown, record: HotWordVO) => {
|
||||
const isMine = record.creatorId === userProfile.userId;
|
||||
const canEdit = record.isPublic === 1 ? isAdmin : isMine || isAdmin;
|
||||
|
||||
if (!canEdit) {
|
||||
return <Text type="secondary">无权操作</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space size="middle">
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleOpenModal(record)}>
|
||||
编辑
|
||||
render: (_: unknown, record: HotWordVO) => (
|
||||
<Space size="middle">
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleOpenModal(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除这条热词吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText={t("common.confirm")}
|
||||
cancelText={t("common.cancel")}
|
||||
>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除这条热词吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText={t("common.confirm")}
|
||||
cancelText={t("common.cancel")}
|
||||
>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const groupColumns = [
|
||||
{
|
||||
title: "热词组名称",
|
||||
dataIndex: "groupName",
|
||||
key: "groupName",
|
||||
render: (value: string) => <Text strong>{value}</Text>,
|
||||
},
|
||||
{
|
||||
title: "热词数量",
|
||||
dataIndex: "hotWordCount",
|
||||
key: "hotWordCount",
|
||||
render: (value: number) => <Tag color={value >= 200 ? "red" : "processing"}>{value}/200</Tag>,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (value: number) => value === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />,
|
||||
},
|
||||
{
|
||||
title: "备注",
|
||||
dataIndex: "remark",
|
||||
key: "remark",
|
||||
render: (value?: string) => value || "-",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_: unknown, record: HotWordGroupVO) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => openGroupEditor(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除这个热词组吗?"
|
||||
description="删除前必须先解除模板引用并清空组内热词。"
|
||||
onConfirm={() => handleDeleteGroup(record.id)}
|
||||
okText={t("common.confirm")}
|
||||
cancelText={t("common.cancel")}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -256,19 +382,7 @@ const HotWords: React.FC = () => {
|
|||
extra={
|
||||
<Space wrap>
|
||||
<Select
|
||||
placeholder="热词类型"
|
||||
style={{ width: 110 }}
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
setSearchType(value);
|
||||
setCurrent(1);
|
||||
}}
|
||||
>
|
||||
<Option value={1}>租户公开</Option>
|
||||
<Option value={0}>个人私有</Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="按类别筛选"
|
||||
placeholder="按分类筛选"
|
||||
style={{ width: 150 }}
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
|
|
@ -292,6 +406,9 @@ const HotWords: React.FC = () => {
|
|||
}}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<Button icon={<FolderOpenOutlined />} onClick={() => void openGroupManager()}>
|
||||
热词组管理
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
|
||||
新增热词
|
||||
</Button>
|
||||
|
|
@ -299,14 +416,7 @@ const HotWords: React.FC = () => {
|
|||
}
|
||||
>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "24px 24px 0" }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content" }}
|
||||
pagination={false}
|
||||
/>
|
||||
<Table columns={columns} dataSource={data} rowKey="id" loading={loading} scroll={{ x: "max-content" }} pagination={false} />
|
||||
</div>
|
||||
<AppPagination
|
||||
current={current}
|
||||
|
|
@ -322,29 +432,17 @@ const HotWords: React.FC = () => {
|
|||
<Modal
|
||||
title={editingId ? "编辑热词" : "新增热词"}
|
||||
open={modalVisible}
|
||||
onOk={handleSubmit}
|
||||
onOk={() => void handleSubmit()}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
confirmLoading={submitLoading}
|
||||
width={560}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item
|
||||
name="word"
|
||||
label="热词原文"
|
||||
rules={[{ required: true, message: "请输入热词原文" }]}
|
||||
>
|
||||
<Form.Item name="word" label="热词原文" rules={[{ required: true, message: "请输入热词原文" }]}>
|
||||
<Input placeholder="输入识别关键词" onBlur={handleWordBlur} />
|
||||
</Form.Item>
|
||||
|
||||
{/*<Form.Item*/}
|
||||
{/* name="pinyin"*/}
|
||||
{/* label="拼音"*/}
|
||||
{/* tooltip="仅保留一个拼音值,失焦后会自动带出推荐结果"*/}
|
||||
{/*>*/}
|
||||
{/* <Input placeholder="例如:hui yi" />*/}
|
||||
{/*</Form.Item>*/}
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="category" label="热词分类">
|
||||
|
|
@ -357,6 +455,14 @@ const HotWords: React.FC = () => {
|
|||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="hotWordGroupId" label="所属热词组">
|
||||
<Select placeholder="请选择热词组" allowClear options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="weight"
|
||||
|
|
@ -366,9 +472,6 @@ const HotWords: React.FC = () => {
|
|||
<InputNumber min={1} max={5} precision={1} step={0.1} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="status" label="使用状态">
|
||||
<Select>
|
||||
|
|
@ -377,16 +480,6 @@ const HotWords: React.FC = () => {
|
|||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{isAdmin ? (
|
||||
<Col span={12}>
|
||||
<Form.Item name="isPublic" label="租户公开" tooltip="开启后,租户内成员都可以共享这条热词">
|
||||
<Radio.Group>
|
||||
<Radio value={1}>是</Radio>
|
||||
<Radio value={0}>否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
) : null}
|
||||
</Row>
|
||||
|
||||
<Form.Item name="remark" label="备注">
|
||||
|
|
@ -394,6 +487,46 @@ const HotWords: React.FC = () => {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="热词组管理"
|
||||
open={groupManageVisible}
|
||||
onCancel={() => setGroupManageVisible(false)}
|
||||
footer={null}
|
||||
width={900}
|
||||
destroyOnHidden
|
||||
>
|
||||
<div style={{ marginBottom: 16, display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openGroupEditor()}>
|
||||
新增热词组
|
||||
</Button>
|
||||
</div>
|
||||
<Table rowKey="id" columns={groupColumns} dataSource={groupData} loading={groupLoading} pagination={false} />
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={editingGroupId ? "编辑热词组" : "新增热词组"}
|
||||
open={groupEditorVisible}
|
||||
onCancel={() => setGroupEditorVisible(false)}
|
||||
onOk={() => void handleGroupSubmit()}
|
||||
confirmLoading={groupSubmitLoading}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={groupForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="groupName" label="热词组名称" rules={[{ required: true, message: "请输入热词组名称" }]}>
|
||||
<Input placeholder="例如:项目术语、客户名单" maxLength={100} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select>
|
||||
<Option value={1}>启用</Option>
|
||||
<Option value={0}>禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={3} placeholder="说明这个热词组的适用范围" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -984,7 +984,6 @@ const MeetingDetail: React.FC = () => {
|
|||
category: '',
|
||||
weight: 2,
|
||||
status: 1,
|
||||
isPublic: 0,
|
||||
remark: meeting ? `来源于会议:${meeting.title}` : '来源于会议关键词',
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,40 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Input, Space, Drawer, Form, Select, Tag, Popconfirm, Typography, Divider, Tooltip, Row, Col, List, Empty, Skeleton, Switch, Modal, App } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, SaveOutlined, StarFilled } from '@ant-design/icons';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined, PlusOutlined, SaveOutlined, StarFilled } from '@ant-design/icons';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useDict } from '../../hooks/useDict';
|
||||
import {
|
||||
getPromptPage,
|
||||
savePromptTemplate,
|
||||
updatePromptTemplate,
|
||||
deletePromptTemplate,
|
||||
updatePromptStatus,
|
||||
PromptTemplateVO,
|
||||
PromptTemplateDTO
|
||||
} from '../../api/business/prompt';
|
||||
import AppPagination from '../../components/shared/AppPagination';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDict } from '../../hooks/useDict';
|
||||
import {
|
||||
deletePromptTemplate,
|
||||
getPromptDetail,
|
||||
getPromptPage,
|
||||
savePromptTemplate,
|
||||
updatePromptStatus,
|
||||
updatePromptTemplate,
|
||||
type PromptTemplateVO,
|
||||
} from '../../api/business/prompt';
|
||||
import { getHotWordGroupOptions, type HotWordGroupVO } from '../../api/business/hotwordGroup';
|
||||
import AppPagination from '../../components/shared/AppPagination';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Text, Title } = Typography;
|
||||
|
|
@ -27,17 +47,19 @@ 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 templateLevel = Form.useWatch('isSystem', form);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<PromptTemplateVO[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [current, setCurrent] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(12);
|
||||
|
||||
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [previewContent, setPreviewContent] = useState('');
|
||||
const [groupOptions, setGroupOptions] = useState<HotWordGroupVO[]>([]);
|
||||
|
||||
const userProfile = React.useMemo(() => {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
|
|
@ -45,43 +67,46 @@ const PromptTemplates: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
const activeTenantId = React.useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
|
||||
|
||||
const isPlatformAdmin = userProfile.isPlatformAdmin === true;
|
||||
const isTenantAdmin = userProfile.isTenantAdmin === true;
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
void fetchData();
|
||||
}, [current, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadGroupOptions();
|
||||
}, [isPlatformAdmin, templateLevel, activeTenantId]);
|
||||
|
||||
const loadGroupOptions = async () => {
|
||||
const targetTenantId = isPlatformAdmin && Number(templateLevel) === 1 ? 0 : undefined;
|
||||
const res = await getHotWordGroupOptions(targetTenantId ?? (isPlatformAdmin && activeTenantId === 0 ? 0 : undefined));
|
||||
setGroupOptions(res.data?.data || []);
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
const values = searchForm.getFieldsValue();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getPromptPage({
|
||||
current,
|
||||
size: pageSize,
|
||||
name: values.name,
|
||||
category: values.category
|
||||
const res = await getPromptPage({
|
||||
current,
|
||||
size: pageSize,
|
||||
name: values.name,
|
||||
category: values.category,
|
||||
});
|
||||
if (res.data && res.data.data) {
|
||||
if (res.data?.data) {
|
||||
setData(res.data.data.records);
|
||||
setTotal(res.data.data.total);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (id: number, checked: boolean) => {
|
||||
try {
|
||||
await updatePromptStatus(id, checked ? 1 : 0);
|
||||
message.success(checked ? '模板已启用' : '模板已停用');
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
await updatePromptStatus(id, checked ? 1 : 0);
|
||||
message.success(checked ? '模板已启用' : '模板已停用');
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
const handleOpenDrawer = (record?: PromptTemplateVO, isClone = false) => {
|
||||
|
|
@ -91,16 +116,15 @@ const PromptTemplates: React.FC = () => {
|
|||
form.setFieldsValue({
|
||||
...record,
|
||||
templateName: `${record.templateName} (副本)`,
|
||||
isSystem: 0, // 副本强制设为普通模板
|
||||
isSystem: 0,
|
||||
id: undefined,
|
||||
tenantId: undefined
|
||||
tenantId: undefined,
|
||||
});
|
||||
setPreviewContent(record.promptContent);
|
||||
} else {
|
||||
const isPlatformLevel = Number(record.tenantId) === 0 && Number(record.isSystem) === 1;
|
||||
const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1;
|
||||
|
||||
// 权限判定逻辑
|
||||
|
||||
let canEdit = false;
|
||||
if (Number(record.isSystem) === 0) {
|
||||
canEdit = Number(record.creatorId) === currentUserId;
|
||||
|
|
@ -108,8 +132,6 @@ const PromptTemplates: React.FC = () => {
|
|||
canEdit = isPlatformLevel;
|
||||
} else if (isTenantAdmin) {
|
||||
canEdit = Number(record.tenantId) === activeTenantId;
|
||||
} else {
|
||||
canEdit = false;
|
||||
}
|
||||
|
||||
if (!canEdit) {
|
||||
|
|
@ -124,10 +146,9 @@ const PromptTemplates: React.FC = () => {
|
|||
} else {
|
||||
setEditingId(null);
|
||||
form.resetFields();
|
||||
// 租户管理员或平台管理员新增默认选系统/租户预置
|
||||
form.setFieldsValue({
|
||||
status: 1,
|
||||
isSystem: (isTenantAdmin || isPlatformAdmin) ? 1 : 0
|
||||
form.setFieldsValue({
|
||||
status: 1,
|
||||
isSystem: (isTenantAdmin || isPlatformAdmin) ? 1 : 0,
|
||||
});
|
||||
setPreviewContent('');
|
||||
}
|
||||
|
|
@ -135,35 +156,57 @@ const PromptTemplates: React.FC = () => {
|
|||
};
|
||||
|
||||
const showDetail = (record: PromptTemplateVO) => {
|
||||
Modal.info({
|
||||
void (async () => {
|
||||
const detailRes = await getPromptDetail(record.id);
|
||||
const detail = detailRes.data?.data || record;
|
||||
Modal.info({
|
||||
title: record.templateName,
|
||||
width: 800,
|
||||
icon: null,
|
||||
content: (
|
||||
<div style={{ maxHeight: '65vh', overflowY: 'auto', padding: '12px 0' }}>
|
||||
{record.description ? (
|
||||
{detail.description ? (
|
||||
<div style={{ marginBottom: 16, padding: '12px 16px', borderRadius: 8, background: 'var(--app-bg-surface-soft)' }}>
|
||||
<Text type="secondary">{record.description}</Text>
|
||||
<Text type="secondary">{detail.description}</Text>
|
||||
</div>
|
||||
) : null}
|
||||
<ReactMarkdown>{record.promptContent}</ReactMarkdown>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space wrap>
|
||||
{detail.hotWordGroupName ? <Tag color="blue">热词组:{detail.hotWordGroupName}</Tag> : <Tag>未绑定热词组</Tag>}
|
||||
{(detail.tags || []).map((tag) => {
|
||||
const dictItem = dictTags.find((item) => item.itemValue === tag);
|
||||
return <Tag key={tag}>{dictItem ? dictItem.itemLabel : tag}</Tag>;
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
{detail.hotWords && detail.hotWords.length > 0 ? (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 600 }}>绑定热词</div>
|
||||
<Space wrap>
|
||||
{detail.hotWords.map((word) => <Tag key={word}>{word}</Tag>)}
|
||||
</Space>
|
||||
</div>
|
||||
) : detail.hotWordGroupId ? (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text type="secondary">该热词组当前没有热词</Text>
|
||||
</div>
|
||||
) : null}
|
||||
<ReactMarkdown>{detail.promptContent}</ReactMarkdown>
|
||||
</div>
|
||||
),
|
||||
okText: '关闭',
|
||||
maskClosable: true
|
||||
});
|
||||
maskClosable: true,
|
||||
});
|
||||
})();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitLoading(true);
|
||||
|
||||
// 处理 tenantId,如果是新增且是平台管理员设为系统模板,手动设置 tenantId 为 0
|
||||
if (!editingId && isPlatformAdmin && values.isSystem === 1) {
|
||||
values.tenantId = 0;
|
||||
}
|
||||
|
||||
if (editingId) {
|
||||
await updatePromptTemplate({ ...values, id: editingId });
|
||||
message.success('更新成功');
|
||||
|
|
@ -172,9 +215,7 @@ const PromptTemplates: React.FC = () => {
|
|||
message.success('模板已创建');
|
||||
}
|
||||
setDrawerVisible(false);
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
await fetchData();
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
|
|
@ -182,7 +223,7 @@ const PromptTemplates: React.FC = () => {
|
|||
|
||||
const groupedData = React.useMemo(() => {
|
||||
const groups: Record<string, PromptTemplateVO[]> = {};
|
||||
data.forEach(item => {
|
||||
data.forEach((item) => {
|
||||
const cat = item.category || 'default';
|
||||
if (!groups[cat]) groups[cat] = [];
|
||||
groups[cat].push(item);
|
||||
|
|
@ -195,26 +236,17 @@ const PromptTemplates: React.FC = () => {
|
|||
const isPlatformLevel = Number(item.tenantId) === 0 && isSystem;
|
||||
const isTenantLevel = Number(item.tenantId) > 0 && isSystem;
|
||||
const isPersonalLevel = !isSystem;
|
||||
|
||||
// 权限判定逻辑 (使用 Number 强制转换防止类型不匹配)
|
||||
let canEdit = false;
|
||||
const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1;
|
||||
|
||||
let canEdit = false;
|
||||
if (isPersonalLevel) {
|
||||
// 个人模板仅本人可编辑
|
||||
canEdit = Number(item.creatorId) === currentUserId;
|
||||
} else if (isPlatformAdmin) {
|
||||
// 平台管理员管理平台公开模板 (tenantId = 0)
|
||||
canEdit = Number(item.tenantId) === 0;
|
||||
} else if (isTenantAdmin) {
|
||||
// 租户管理员管理本租户公开模板
|
||||
canEdit = Number(item.tenantId) === activeTenantId;
|
||||
} else {
|
||||
// 普通用户不可编辑公开模板
|
||||
canEdit = false;
|
||||
}
|
||||
|
||||
// 标签颜色与文字
|
||||
const levelTag = isPlatformLevel ? (
|
||||
<Tag color="gold" style={{ borderRadius: 4 }}>平台级</Tag>
|
||||
) : isTenantLevel ? (
|
||||
|
|
@ -224,7 +256,7 @@ const PromptTemplates: React.FC = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
<Card
|
||||
key={item.id}
|
||||
hoverable
|
||||
onClick={() => showDetail(item)}
|
||||
|
|
@ -233,23 +265,23 @@ const PromptTemplates: React.FC = () => {
|
|||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, flex: 1 }}>
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: 10,
|
||||
background: isPlatformLevel ? 'color-mix(in srgb, #f5c542 14%, var(--app-bg-surface-strong))' : (isTenantLevel ? 'color-mix(in srgb, var(--app-primary-color) 12%, var(--app-bg-surface-strong))' : 'color-mix(in srgb, #13c2c2 12%, var(--app-bg-surface-strong))'),
|
||||
display: 'flex', justifyContent: 'center', alignItems: 'center', flexShrink: 0
|
||||
<div style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
background: isPlatformLevel ? 'color-mix(in srgb, #f5c542 14%, var(--app-bg-surface-strong))' : (isTenantLevel ? 'color-mix(in srgb, var(--app-primary-color) 12%, var(--app-bg-surface-strong))' : 'color-mix(in srgb, #13c2c2 12%, var(--app-bg-surface-strong))'),
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<StarFilled style={{ fontSize: 20, color: isPlatformLevel ? '#faad14' : (isTenantLevel ? '#1890ff' : '#13c2c2') }} />
|
||||
</div>
|
||||
<div style={{ flexShrink: 0 }}>{levelTag}</div>
|
||||
</div>
|
||||
<Space onClick={e => e.stopPropagation()} style={{ flexShrink: 0, marginLeft: 8 }}>
|
||||
<Space onClick={(e) => e.stopPropagation()} style={{ flexShrink: 0, marginLeft: 8 }}>
|
||||
{canEdit && <EditOutlined style={{ fontSize: 18, color: '#bfbfbf', cursor: 'pointer' }} onClick={() => handleOpenDrawer(item)} />}
|
||||
<Switch
|
||||
size="small"
|
||||
checked={item.status === 1}
|
||||
onChange={(checked) => handleStatusChange(item.id, checked)}
|
||||
disabled={false}
|
||||
/>
|
||||
<Switch size="small" checked={item.status === 1} onChange={(checked) => void handleStatusChange(item.id, checked)} />
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
|
@ -260,29 +292,30 @@ const PromptTemplates: React.FC = () => {
|
|||
{item.description}
|
||||
</Text>
|
||||
) : null}
|
||||
{/*<Text type="secondary" style={{ fontSize: 12 }}>使用次数: {item.usageCount || 0}</Text>*/}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20, height: 22, overflow: 'hidden' }}>
|
||||
{item.tags?.map(tag => {
|
||||
const dictItem = dictTags.find(dt => dt.itemValue === tag);
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12, minHeight: 22 }}>
|
||||
{item.hotWordGroupName ? <Tag color="blue" style={{ margin: 0 }}>热词组:{item.hotWordGroupName}</Tag> : null}
|
||||
{item.tags?.map((tag) => {
|
||||
const dictItem = dictTags.find((dt) => dt.itemValue === tag);
|
||||
return (
|
||||
<Tag key={tag} style={{ margin: 0, border: '1px solid var(--app-border-color)', background: 'var(--app-bg-surface-soft)', color: 'var(--app-text-main)', borderRadius: 4, fontSize: 10, maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{dictItem ? dictItem.itemLabel : tag}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
{!item.hotWordGroupName && (!item.tags || item.tags.length === 0) ? <Text type="secondary">未配置标签</Text> : null}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', paddingTop: 12, borderTop: '1px solid #f5f5f5' }}>
|
||||
<Space onClick={e => e.stopPropagation()}>
|
||||
<Space onClick={(e) => e.stopPropagation()}>
|
||||
<Tooltip title="以此创建">
|
||||
<CopyOutlined style={{ color: '#bfbfbf', cursor: 'pointer', fontSize: 16 }} onClick={() => handleOpenDrawer(item, true)} />
|
||||
</Tooltip>
|
||||
{canEdit && (
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => deletePromptTemplate(item.id).then(fetchData)}
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => deletePromptTemplate(item.id).then(() => fetchData())}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
|
|
@ -308,17 +341,17 @@ const PromptTemplates: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<Card variant="borderless" style={{ borderRadius: 12, marginBottom: 32, background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', boxShadow: 'var(--app-shadow)', backdropFilter: 'blur(16px)' }} styles={{ body: { padding: '20px 24px' } }}>
|
||||
<Form form={searchForm} layout="inline" onFinish={fetchData}>
|
||||
<Form form={searchForm} layout="inline" onFinish={() => void fetchData()}>
|
||||
<Form.Item name="name" label="模板名称"><Input placeholder="请输入..." style={{ width: 180 }} /></Form.Item>
|
||||
<Form.Item name="category" label="分类">
|
||||
<Select placeholder="选择分类" style={{ width: 160 }} allowClear>
|
||||
{categories.map(c => <Option key={c.itemValue} value={c.itemValue}>{c.itemLabel}</Option>)}
|
||||
{categories.map((c) => <Option key={c.itemValue} value={c.itemValue}>{c.itemLabel}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">查询数据</Button>
|
||||
<Button onClick={() => { searchForm.resetFields(); fetchData(); }}>重置</Button>
|
||||
<Button onClick={() => { searchForm.resetFields(); void fetchData(); }}>重置</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
@ -333,8 +366,8 @@ const PromptTemplates: React.FC = () => {
|
|||
) : (
|
||||
<>
|
||||
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '24px 24px 0' }}>
|
||||
{Object.keys(groupedData).map(catKey => {
|
||||
const catLabel = categories.find(c => c.itemValue === catKey)?.itemLabel || catKey;
|
||||
{Object.keys(groupedData).map((catKey) => {
|
||||
const catLabel = categories.find((c) => c.itemValue === catKey)?.itemLabel || catKey;
|
||||
return (
|
||||
<div key={catKey} style={{ marginBottom: 40 }}>
|
||||
<Title level={4} style={{ marginBottom: 24, paddingLeft: 8, borderLeft: '4px solid #1890ff' }}>{catLabel}</Title>
|
||||
|
|
@ -369,7 +402,7 @@ const PromptTemplates: React.FC = () => {
|
|||
extra={
|
||||
<Space>
|
||||
<Button onClick={() => setDrawerVisible(false)}>取消</Button>
|
||||
<Button type="primary" icon={<SaveOutlined />} loading={submitLoading} onClick={handleSubmit}>保存</Button>
|
||||
<Button type="primary" icon={<SaveOutlined />} loading={submitLoading} onClick={() => void handleSubmit()}>保存</Button>
|
||||
</Space>
|
||||
}
|
||||
destroyOnHidden
|
||||
|
|
@ -377,14 +410,16 @@ const PromptTemplates: React.FC = () => {
|
|||
<Form form={form} layout="vertical">
|
||||
<Row gutter={24}>
|
||||
<Col span={(isPlatformAdmin || isTenantAdmin) ? 8 : 12}>
|
||||
<Form.Item name="templateName" label="模板名称" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item name="templateName" label="模板名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{(isPlatformAdmin || isTenantAdmin) && (
|
||||
<Col span={6}>
|
||||
<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>)
|
||||
promptLevels.map((i) => <Option key={i.itemValue} value={Number(i.itemValue)}>{i.itemLabel}</Option>)
|
||||
) : (
|
||||
<>
|
||||
<Option value={1}>{isPlatformAdmin ? '系统预置 (全局)' : '租户预置 (全员)'}</Option>
|
||||
|
|
@ -397,7 +432,9 @@ const PromptTemplates: React.FC = () => {
|
|||
)}
|
||||
<Col span={(isPlatformAdmin || isTenantAdmin) ? 5 : 6}>
|
||||
<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>
|
||||
<Select loading={dictLoading}>
|
||||
{categories.map((i) => <Option key={i.itemValue} value={i.itemValue}>{i.itemLabel}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={isPlatformAdmin ? 5 : 6}>
|
||||
|
|
@ -409,6 +446,7 @@ const PromptTemplates: React.FC = () => {
|
|||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="description" label="模板描述">
|
||||
<Input.TextArea
|
||||
maxLength={255}
|
||||
|
|
@ -417,29 +455,52 @@ const PromptTemplates: React.FC = () => {
|
|||
placeholder="请输入模板描述"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="tags" label="业务标签" tooltip="可从现有标签中选择,也可输入新内容按回车保存">
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="选择或输入新标签"
|
||||
allowClear
|
||||
tokenSeparators={[',', ' ', ';']}
|
||||
>
|
||||
{dictTags.map(t => <Option key={t.itemValue} value={t.itemValue}>{t.itemLabel}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<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>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="hotWordGroupId"
|
||||
label="绑定热词组"
|
||||
tooltip="可选,未绑定则保持兼容"
|
||||
>
|
||||
<Select
|
||||
placeholder="选择热词组"
|
||||
allowClear
|
||||
options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider orientation="left">提示词编辑器 (Markdown 实时预览)</Divider>
|
||||
<Row gutter={24} style={{ height: 'calc(100vh - 400px)' }}>
|
||||
<Col span={12} style={{ height: '100%' }}>
|
||||
<Form.Item name="promptContent" noStyle rules={[{ required: true }]}>
|
||||
<Input.TextArea
|
||||
onChange={e => setPreviewContent(e.target.value)}
|
||||
style={{ height: '100%', fontFamily: 'monospace', resize: 'none', border: '1px solid #d9d9d9', borderRadius: 8, padding: 12 }}
|
||||
<Input.TextArea
|
||||
onChange={(e) => setPreviewContent(e.target.value)}
|
||||
style={{ height: '100%', fontFamily: 'monospace', resize: 'none', border: '1px solid #d9d9d9', borderRadius: 8, padding: 12 }}
|
||||
placeholder="在此输入 Markdown 指令..."
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12} style={{ height: '100%', overflowY: 'auto', background: 'var(--app-bg-surface-soft)', border: '1px solid var(--app-border-color)', borderRadius: 8, padding: '16px 24px' }}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
{form.getFieldValue('hotWordGroupId') ? (
|
||||
<Tag color="blue">
|
||||
绑定热词组:
|
||||
{groupOptions.find((item) => item.id === form.getFieldValue('hotWordGroupId'))?.groupName || '已选择'}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag>未绑定热词组</Tag>
|
||||
)}
|
||||
</div>
|
||||
<div className="markdown-preview"><ReactMarkdown>{previewContent}</ReactMarkdown></div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export function RealtimeAsrSession() {
|
|||
setMeeting(detail);
|
||||
setSessionStatus(realtimeStatus);
|
||||
const fallbackDraft = buildDraftFromStatus(meetingId, detail, realtimeStatus);
|
||||
const resolvedDraft = parsedDraft || fallbackDraft;
|
||||
const resolvedDraft = fallbackDraft || parsedDraft;
|
||||
setSessionDraft(resolvedDraft);
|
||||
if (resolvedDraft) {
|
||||
sessionStorage.setItem(getSessionKey(meetingId), JSON.stringify(resolvedDraft));
|
||||
|
|
|
|||
Loading…
Reference in New Issue